Skip to main content.

Project 4: Writing Files + Improved Shell

Due: Tuesday, Nov 27, 11:59:59 p.m.

Table of Contents:

Objective

In this project you will implement functions for deleting and writing files, and add several new commands to your shell. At the end of the project, you will have a fully functional single-process operating system about as powerful as CP/M, an early PC operating system. If that doesn't get your juices flowing, I don't know what will.

Background

Like reading files, writing files requires that you understand the file system and how it keeps track of the names of the files on the disk and their locations. As such, there is no new background for this project. However, you may want to review the material on the Disk Directory and, especially the Disk Map given in the Background section of Project 3.

Getting Started

Go to the GitHub Assignment to set up your repository. Start your VM, open a terminal, and clone your GitHub repository When finished, you should have a directory named project4_username containing the files.

Project 4 - Refactored

Since kernel.c was getting pretty long, I refactored the code into several modules and updated the Makefile accordingly. While I prefer you would use your code for a better sense of ownership, I know that refactoring your code may be a more onerous task that you would like at this point. If you use my code, make sure you understand the functions I provide.

The "default" filesys.c uses structs for the disk directory information. If you don't want to use structs, copy filesys.c and kernellib.c from the nostructs directory.

Your [new] code should be put into the appropriate files.

Step 1: Writing a Disk Sector

The functionality for reading files was based on being able to read a sector. Similarly, the functionality for writing files will be based on being able to write a sector. Create a new function named writeSector in your kernel with the prototype:
int writeSector(char *buffer, int sector);

Writing sectors can be accomplished using the same BIOS call as reading sectors. The only difference is that AH should equal 3 instead of 2. Thus, you should be able to copy your readSector function and modify it fairly easily to write the data in buffer to the indicated sector. You can refer to the Project 2 assignment for the details of the BIOS call used in the readSector function. To test your writeSector function, you can have your main function write some known values into the first sector (0) or the last sector (2879) on the disk. You can then use the hexdump program to read the floppya.img disk image. If writeSector is working correctly the values that were written should appear at the beginning or the end of the disk image.

Since we do not want user programs writing arbitrary sectors on the disk, we will not provide access to writeSector through the system call interface.

Step 2: Deleting a File

Deleting a file requires two steps:

These two steps do not actually erase the file (or even its full filename) from the disk. Rather, it just makes the directory entry and sectors used by the file available to be used for new files. The advantages of deleting files in this way are that it is fast and it also becomes possible to undelete a file (at least until its directory entry or sectors are reused). Most modern operating systems use a deletion process similar to this one.

Add a deleteFile function to your kernel with the following signature:
int deleteFile(char *fname);

This function should delete the named file using the two-step process described above. If the file is found and deleted, this function should return 1. If the file to be deleted cannot be found on the disk, this function should return -1. After the Disk Map and Disk Directory are modified, they will need to be written back to the disk to save the changes.

Test your deleteFile function by having your main function delete a file (or files). Then, open the floppya.img disk image and examine the Disk Map and Disk Directory to ensure that they have been modified to reflect the deleted files.

System Call

Add and test a system call for deleting a file by modifying your handleInterrupt21 function so that it provides the following service:

 deleteFile:        	delete a file from the disk.
        AX:             0x07
        BX:             The name of the file to be deleted.
        CX:             Unused
        DX:             Unused
        Return:     	1 if the file is successfully deleted or
                        -1 if the file cannot be found.
 

Step 3. Writing a File

Writing a file requires finding an empty entry in the Disk Directory and finding a free sector in the Disk Map for each sector making up the file. The data for each sector is written into a free sector on the disk and the sector numbers that are used are entered into the Disk Directory entry for the file. The modified Disk Directory and Disk Map must then be written back to the disk to save the changes.

Add a writeFile function to your kernel with the signature:
int writeFile(char *fname, char *buffer, int sectors);

This function should write sectors * 512 bytes of data from the buffer into a file with the name indicated by fname, where sectors is the number of sectors to be written. If the file indicated by fname already exists, the new file overwrites it. The maximum number of sectors that may be written is 26. If sectors is larger than 26, then only the first 26 sectors should be written. If the file is successfully written, this function returns the number of sectors that were written. If there is no Disk Directory entry available for the new file, this function should return -1 and the file is not written. If the Disk Map contains fewer than sectors free sectors, this function should write as many sectors as possible and return -2.

If you didn't decompose your kernel's functions for the last project, make sure you decompose for this project. What are good, reusable pieces of code that should be put into functions?

One way to test your writeFile function is to use your readFile function to read a file into a buffer then write it to a new file. Once you have written the new file you can then use readFile again to read it in and print it out. If the contents are the same as the original file, that is an indication (but not a guarantee) that your writeFile function is working correctly.

What else should you test?

System Call

Add and test a system call for writing a file by modifying your handleInterrupt21 function so that it provides the following service:

        writeFile:  write a file to the disk.
             AX:                  0x08
             BX:                  The name of the file to be written.
             CX:                  The address of a buffer containing the data for the file.
             DX:                  The number of sectors to be written.
             Return:     	  The number of sectors written or
                                  -1 if there is no Disk Directory entry available for the file or
                                  -2 if there are insufficient free sectors to hold the file.

Step 4. User Library Improvements

Add functions to the user library for deleting a file and writing a file to disk.

Step 5. Shell Improvements

Add new functionality to your shell, as described below.

Shell Command: delete <file>

Extend your shell so that it recognizes the command: delete <file>. This command should remove the specified file from the disk. If the file does not exist on the disk, the shell should print “File not found.”

Shell Command: copy <src> <dest>

Extend your shell so that it recognizes the command: copy <src> <dest>. This command should make a copy of the file <src> as a new file <dest>. If the destination file already exists, it should be overwritten. If the source file cannot be found, then the shell should print “File not found.” If the destination file cannot be created because the directory is full, the shell should print "Disk directory is full." If the disk becomes full before the entire source file is copied, the shell should print "Disk is full."

Shell Command: dir

Extend your shell so that it recognizes the command: dir. This command should display a list of the files currently stored on the disk. Since we do not want to allow general direct access to sectors, you need to add a specialized function in userlib.c to access the disk directory contents. There are various ways you could handle this. You may need to update kernel.c accordingly.

If you have the printInt function, you might also have the dir command display the size, in sectors, of each file.

Step 6: Finalizing your Submission

(If you choose to complete the bonus features, come back to this after completing them.)

Running your kernel should look pretty much like it did at the end of Step 5: a running shell. I didn't explicitly talk as much about testing in this project. Since this is the fourth OS project, I hope you know how to test it. Review your code and make sure you have done appropriate error handling and documenting of your code.

Bonus Features

I will provide only limited help on the bonuses.

If you complete a bonus, it should be documented in comments so that I can find it easily.

  1. Create a simple line-based text editor that can be executed as a user program, e.g., nano. When executed your text editor should prompt the user to enter a filename into which the entered text will be stored. The editor should then read lines of text from the user, storing them into a buffer. The editor should not allow the user to enter more text than will fit in a file on the system. When the user types CTRL-D (ASCII 0x04) followed by the return key, the editor saves the file and terminates. If the file specified by the user already exists, it is overwritten with the new text. Have your Makefile compile your editor and add it to your disk image.
  2. Most modern operating systems make it possible to have multiple names for the same file. In Windows these are called shortcuts, in Mac OS they are called aliases, and in Unix they are called symbolic links. You can learn more about symbolic links in Unix by typing the command man ln in the terminal window.

    Add a system call to your kernel that creates a symbolic link to a file. Your system call should be implemented by modifying your handleInterrupt21 function so that it provides the following service:

           	linkToFile:           create a symbolic link to a file
                       AX:        0xA0
                       BX:        The name of the file to link to.
                       CX:        The name of the link.
                       DX:        Unused
                       Return:    1 if the link is successfully created or
                                  -1 if the file to link to does not exist or
                                  -2 if the link cannot be created because the Disk Directory is full.
          

    The name of the symbolic link should appear in the Disk Directory. To indicate that the Disk Directory entry is a symbolic link and not an actual file, the first bytes of the entry that indicate the file’s sectors should be set to 0xFF. The next six bytes can then be used to indicate the filename of the file to which the symbolic link points.

    Create a user library function for creating symbolic links and add a symlnk <src> <dest> command to your shell.

    When completed, all of your OS functionality should work equally well on real files and symbolic links. For everything except deleting a file, commands operating on symbolic links should behave exactly as if they were operating on the actual file (e.g., type, copy). When a symbolic link is deleted, only the link is deleted; the original file is unchanged.

    Note that if the file pointed to by a symbolic link is deleted the link should remain. An attempt to type or copy such a symbolic link should report “File not found.” This also implies that if a new file is created with the same name as pointed to by the link, then the link will be a link to the new file!

  3. Implement command history in your shell. In most Unix shells, the up and down arrow keys will scroll back and forth through a list of some number of previously entered commands. The previous commands are displayed at the command prompt, and the user can press enter to execute that command.
  4. Implement scripting in your shell. The execute command should be able to execute either a standard executable program or a shell script. Shell scripts should be standard text files that begin with the character sequence "#!". In this case, the shebang is essentially a magic number for identifying shell scripts. If the file that is executed is a shell script, the shell should read each line from the file and attempt to execute it as if it were typed at the command line. Note that with this functionality you can now use your text editor to create a program (a shell script) that you can then run!

Submission

GitHub Classroom will make a snapshot of your repository at the deadline. It can only see the code that you pushed to the repository. Your repository should contain your source code and the updated Makefile.

I should be able to clone/pull your code and run build.sh (or make) and then run.sh to see your kernel running. You can (and should) follow the same process that I will follow to verify that your code is all there and correct.

Assessment

You will be graded on:

Acknowledgement

This assignment as well as the accompanying files and source code have been adopted with minor adaptations from those developed by Michael Black at American University. His paper "Build an operating system from scratch: a project for an introductory operating systems course" can be found in the ACM Digital Library.