What??? Another OS???? :((
In the final practicum, we will continue task-4 from the previous module 4 practicum. This time, we will create a simple filesystem that can be used to store the files we create. The filesystem that we will create will use a simple data storage method, which is by storing file data into blocks provided by the filesystem. If you can't wait to start working on the tasks, you can search TODO in this workspace. Here is an overview of what you will do in the final practicum this time.
- Create a filesystem that can be used to store the files we create.
- Complete the kernel to be able to read and write files into the filesystem that we have created.
- Create a simple shell that can be used to access the filesystem that we have created.
The explanations in the final practicum will often use hexadecimal numbers. The use of hexadecimal numbers is indicated by the prefix 0x. If you are not familiar with hexadecimal numbers, you can use a calculator that supports hexadecimal mode or use hexadecimal to decimal conversion.
If you have passed module 4, you must be familiar with the disk structure that we will use. The disk we use consists of several blocks. Hereafter, blocks will be called sectors. Each sector has a size of 512 bytes. The first sector will be used as the boot sector, which contains the compilation of bootloader.asm. The second sector to the 15th sector will be used to store the text code of the kernel we created.
Looking at the results from module 4, here is the disk structure we will be using. It can be viewed using an application like HxD or using the hexdump or xxd command.
For ease of illustration, the disk structure will be depicted as follows.
One sector will be described as one block. The sector addresses will be renumbered from 0x00. So the first sector will have the address 0x00, the second sector will have the address 0x01, and so on. One row will contain 16 sectors. So the first row will contain sectors with addresses 0x00 to 0x0F, the second row will contain sectors with addresses 0x10 to 0x1F, and so on.
To find the sector address in the contents of the floppy.img file, we can convert the sector address into a byte address as shown above.
The filesystem that will be created will use several components, namely map, node, and data. The map will be stored as 1 sector in sector 0x100. Node will be stored as many as 2 sectors in the 0x101 and 0x102 sectors. Data will be stored as 1 sector in the 0x103 sector.
Here is an illustration of the filesystem structure that we will create.
The map will be used to mark the blocks on the disk that have been used by files. Each block will have a status of 0x00 if the sector has not been used, and 0x01 if the sector has been used. For example, since the 0x00 sector has been used by the bootloader, the contents of the 0th map will be 0x01. The map component will be used when we want to write files to disk to know which sectors we can use.
Here is an illustration of the map component.
The map will be 1 sector (512 bytes) in size. The 0th item to the 15th item in the map will have a status of 0x01 because it has been used by the operating system. The 16th item to the 255th item will have a status of 0x00 because it has not been used. Starting from item 256 (0x100) to item 511 (0x1FF) will be marked as used sectors. This is because we do not allow files to write data on sectors above the 0x100 sector.
Nodes will be used to store information from the files or directories we create. Each node will have a size of 16 bytes. Thus, there will be a total of 64 node items that can be stored. Here is an illustration of the node component.
The following is an explanation of each item on the node.
-
P: The first column of the node item serves as the parent node pointer of the node in question and will be
0xFFif the parent of the node is the root node.For example, at the 1st index node, the value of the first column is
0x00. This indicates that the 1st index node is the parent node of the 0th index node. While at the 0th index node, the value of the first column is0xFF. This indicates that the parent node of the 0th index node is the root node. -
D: The second column of the node item serves as the index pointer of the data component that will be used to store the file data. If the value of the second column is
0xFF, then the node is a directory.For example, at the 0th index node, the value of the second column is
0x00. This means that the data information from the file can be accessed at the 0th index data component. While at the 1st index node, the value of the second column is0xFF. This means that the node is a directory. -
Node name: The third to last column of the node item serves as the name of the node. The name of the node will have a maximum length of 13 characters (the last character is a null character).
The data component will be used to indicate the sectors used to store the file data. Each data item will have a size of 16 bytes. Thus, a total of 32 data items can be stored. Here is an illustration of the data component.
Each column in the data item will indicate the sector address used to store the file data. Since a single byte can only indicate sector addresses up to 255 (0xFF), we can only store sector addresses up to sector 0xFF. Hence, the 256th to the end map item will be marked as a used sector.
The following is an illustration of the three filesystem components described earlier.
In this task, you are asked to create readSector and writeSector syscalls that will be used to read from disk to memory and write from memory to disk.
Here is the implementation of readSector and its explanation.
void readSector(byte* buf, int sector) {
int ah = 0x02; // read sector service number
int al = 0x01; // number of sectors to read
int ch = div(sector, 36); // cylinder number
int cl = mod(sector, 18) + 1; // sector number
int dh = mod(div(sector, 18), 2); // head number
int dl = 0x00; // drive number
interrupt(
0x13,
ah << 8 | al,
buf,
ch << 8 | cl,
dh << 8 | dl
);
}-
The interrupt vector to be used is
0x13to perform disk I/O operations. -
The
ahregister will be filled with0x02indicating areadoperation. -
The
alregister will be filled with0x01indicating the number of sectors to be read. -
The
chandclregisters will be filled with the cylinder and sector numbers to be read.On a floppy disk, there are 2 heads, 18 sectors per track, and 36 tracks per cylinder. Thus, the cylinder number will be calculated by dividing the sector number by 36. Meanwhile, the sector number will be calculated by taking the remainder of the division of the sector number by 18 and adding 1.
-
The
dhanddlregisters will be filled with the head and drive numbers to be used.On a floppy disk, there are 2 heads. So, the head number will be calculated by dividing the sector number by 18 and taking the remainder of the division by 2. While the drive number will be filled with
0x00which indicates the first drive.
For writeSector, you can use the same implementation as readSector by replacing the register value ah with 0x03 indicating a write operation.
In filesystem.h, there are some constants and data types that will be used to help in the implementation of the filesystem. You are asked to implement the fsRead function that will be used to read a directory or file from the filesystem. The fsRead function will accept the following parameters.
void fsRead(struct file_metadata* metadata, enum fs_return* status);-
metadatais a pointer tofile_metadatawhich will be used to store information of the file or directory to be read.The
file_metadatastructure will have the following structure.struct file_metadata { byte parent_index; unsigned int filesize; char node_name[MAX_FILENAME]; byte buffer[FS_MAX_SECTOR * SECTOR_SIZE]; };
parent_indexis the index of the parent node of the file or directory to be read.filesizeis the size of the file to be read. Thefilesizecontains 0 in thefsReadfunction call.node_nameis the name of the file or directory to be read.bufferis a pointer to a buffer that will be used to store data from the file or directory to be read. Thebuffercontains0x00in thefsReadfunction call.
-
statusis a pointer tofs_returnwhich will be used to store the status of the performed operation.
The steps to perform in the fsRead function are as follows.
-
Read the filesystem from disk to memory.
-
Iterate through each node item to find a node that has a name corresponding to
metadata->node_nameand a parent index corresponding tometadata->parent_index. -
If the searched node is not found, then set
statuswithFS_R_NODE_NOT_FOUND. -
If the found node is a directory, then set
statuswithFS_R_TYPE_IS_DIRECTORY. -
If the found node is a file, then the next process is as follows.
- Set
metadata->filesizewith 0. - Perform i iterations from 0 to
FS_MAX_SECTOR - If the i-th data index of the found node is
0x00, then stop iterating. - Perform
readSectorto read data from the sector pointed by the data at the data index with the i-th sector stored intometadata->buffer + i * SECTOR_SIZE. - Add
SECTOR_SIZEtometadata->filesize.
- Set
-
Set
statustoFS_R_SUCCESS.
Next you are asked to implement the fsWrite function that will be used to write files to the filesystem. The fsWrite function will accept the same parameters as fsRead as follows.
void fsWrite(struct file_metadata* metadata, enum fs_return* status);In the fsWrite function, the received metadata will contain the following information.
parent_indexis the index of the parent node of the file to be written. Ifparent_indexis0xFF, then the file to be written will be stored in the root directory.filesizeis the size of the file to be written. Iffilesizeis 0, then the file to be written is a directory.node_nameis the name of the file to be written.bufferis a pointer to a buffer containing the data of the file to be written.
The steps to perform in the fsWrite function are as follows.
-
Read the filesystem from disk to memory.
-
Iterate through each node item to find a node that has a name equal to
metadata->node_nameand a parent index equal tometadata->parent_index. If the searched node is found, then setstatuswithFS_R_NODE_ALREADY_EXISTSand exit. -
Next, search for an empty node (the node name is an empty string) and store its index. If an empty node is not found, then set
statuswithFS_W_NO_FREE_NODEand exit. -
Iterate through each data item to find the empty data (the 0th data sector address is
0x00) and store the index. If empty data is not found, then setstatuswithFS_W_NO_FREE_DATAand exit. -
Iterate through each map item and count the empty blocks (block status is
0x00orfalse). If the empty blocks are less thanmetadata->filesize/SECTOR_SIZE, then setstatuswithFS_W_NOT_ENOUGH_SPACEand exit. -
Set the name of the found node with
metadata->node_name, parent index withmetadata->parent_index, and data index with the empty data index. -
Write the data in the following way.
-
Create a counter variable that will be used to count the number of sectors that have been written (will be called j).
-
Iterate i from 0 to
SECTOR_SIZE. -
If the map item at the i-th index is
0x00, then write index i into the j-th sector data item and write the data from the buffer into the i-th sector. -
Writing can use the
writeSectorfunction ofmetadata->buffer + i * SECTOR_SIZE. -
Add 1 to j.
-
-
Write the changed filesystem back to disk.
-
Set
statuswithFS_W_SUCCESS.
After successfully implementing the fsRead and fsWrite functions, the next step is to create a simple shell. The shell will use a read-eval-print-loop (REPL) that will receive commands from the user and execute them. In this task, you are asked to implement the printCWD function which will be used to display the current working directory (CWD) of the shell.
The printCWD function will accept a byte cwd parameter that indicates the index node of the current working directory. The function will display the path from the root (/) to the node pointed by cwd. If cwd is 0xFF, then the path displayed is /. Each node displayed will be separated by a / character.
Next, you are required to implement the parseCommand function that will be used to parse the commands given by the user. The parseCommand function will accept the following parameters.
void parseCommand(char* buf, char* cmd, char arg[2][64]);bufis a string that contains the command given by the user.cmdis a string that will be used to store the command given by the user.argis an array of strings that will be used to store the arguments of the command given by the user.
Since there will only be 2 arguments given by the user, arg will have a size of 2. If the argument given by the user is 1, then arg[1] will contain an empty string. If the argument given by the user is 0, then arg[0] and arg[1] will contain the empty string.
The cd function will be used to change the current working directory of the shell. The following is the specification of the cd function.
-
cd <dirname>can move the current working directory to the directory below the current working directory. -
cd ..will move the current working directory to the parent directory of the current working directory. -
cd /will move the current working directory to the root directory. -
cdcan only move the current working directory to a directory, it cannot move the current working directory to a file. -
Implementation of relative path and absolute path is not required.
The ls function will be used to display the contents of the directory. The following is the specification of the ls function.
-
lswill display the contents of the current working directory. -
ls .will display the contents of the current working directory. -
ls <dirname>will display the contents of the directory under the current working directory. -
lscan only display the contents of directories, not the contents of files. -
Implementation of relative path and absolute path is not required.
The mv function will be used to move files or directories. Here is the specification of the mv function.
-
mv <filename> <dirname>/<outputname>will move the file under current working directory to the directory under current working directory. -
mv <filename> /<outputname>will move files under the current working directory to the root directory. -
mv <filename>../<outputname>will move files under the current working directory to the parent directory of the current working directory. -
mvcan only move files, not directories. -
Implementation of relative path and absolute path is not required.
The cp function will be used to copy files. Here is the specification of the cp function.
-
cp <filename> <dirname>/<outputname>will copy the file under current working directory to the directory under current working directory. -
cp <filename> /<outputname>will copy files under the current working directory to the root directory. -
cp <filename>../<outputname>will copy files under the current working directory to the parent directory of the current working directory. -
cpcan only copy files, not directories. -
Implementation of relative path and absolute path is not required.
The cat function will be used to display the contents of the file. Here is the specification of the cat function.
-
cat <filename>will display the contents of the file under current working directory. -
Implementation of relative path and absolute path is not required.
The mkdir function will be used to create a directory. Here is the specification of the mkdir function.
mkdir <dirname>will create a directory under the current working directory.
To test, you can run make build run in the terminal to compile and run the OS. Then close the OS and run make generate test=1 to population files and directories into the filesystem (change the value of 1 to the appropriate test number). After that, restart the OS with make run and try out the shell commands you have implemented.
Here is the filesystem structure that will be used in this test.
/
├─ dir1
│ ├─ dir1-1
│ │ └─ dir1-1-1
│ └─ dir1-2
│ └─ dirname
├─ dir2
│ └─ dirname
└─ dir3
Here is the filesystem structure that will be used in this test.
/
├─ file-0
├─ dir-1
│ └─ dir-2
│ └─ . . .
│ └─ dir-62
└─ file-63
Here is the filesystem structure that will be used in this test.
/
├─ 1024
├─ 4096
├─ 8192_0
├─ 8192_1
├─ ...
└─ 8192_13
Here is the filesystem structure that will be used in this test.
/
├─ dir1
│ ├─ katanya
│ ├─ dir3
│ │ ├─ bikin
│ │ ├─ fp
│ │ └─ dir4
│ ├─ dir5
│ │ ├─ cuma
│ │ └─ seminggu
├─ dir2
└─ doang
-
For filesystem debugging, you can check using hexedit on Linux or HxD on Windows. With the sector map information
0x100, nodes0x101and0x102, and data0x103, you can find out the data stored on the filesystem. To get the byte offset of the sector, you can use the formulaoffset = sector * 512oroffset = sector * 0x200. For example to find out the contents of the filesystem map, you can open HxD and hexedit by pressingCtrl + Gand enter the byte offset of the sector map (0x100 * 0x200 = 0x20000). -
bccdoes not provide as much error checking asgcc. You can usegccto perform error checking at compile time. -
Due to the use of
bccin ANSI C mode, you cannot declare variables in the middle of a code block or scope. Variables must be declared at the beginning of the code block or scope. -
Always run
makein thepracticum-finaldirectory, not in a subdirectory. -
A little sneak peek of what you will be making.
Bochs.for.Windows.-.Display.2024-06-10.03-51-51.mp4







