Table of Contents

File Operation Syscalls

For this task, you will also need to implement the following file operation syscalls: create, remove, open, filesize, read, write, seek, tell, and close. PintOS already contains a basic file system which implements a lot of these functionalities, meaning you will not need to implement any file operations yourself. Your implementations will simply call the appropriate functions in the file system library.

Keep in mind that most testing programs use the write syscall for output. Until you implement write syscall, most test programs will not work.

General Notes

Before we start implementing the handling of system calls, let’s locate the syscall_handler function and study its contents. For this task, you will have to modify the file src/userprog/syscall.c. The function syscall_handler is the main dispatch point you will use to invoke the required functionalities for the syscalls to implement. As a starting point for you it already implements the syscall exit (SYS_EXEC), see src/lib/syscall-nr.h for the list of predefined syscall identifiers you can use.

To implement syscalls, you first need a way to safely read and write memory that’s in a user process’s virtual address space. The syscall arguments are located on the user process’s stack, right above the user process’s stack pointer. Also, you are not allowed to have the kernel crash while trying to dereference an invalid or NULL pointer. For example, if the stack pointer is invalid when a user program makes a syscall, the kernel ought not crash when trying to read syscall arguments from the stack. Additionally, some syscall arguments are pointers to buffers inside the user process’s address space. Those buffer pointers could be invalid as well, as should be the whole memory region the buffer pointer refers to.

You will need to gracefully handle cases where a syscall cannot be completed due to invalid memory access including NULL pointers, invalid pointers (e.g. pointing to unmapped memory), and illegal pointers (e.g. pointing to kernel memory). This includes checking all arguments that are being passed to syscalls.

Beware: a 4-byte memory region (e.g. a 32-bit integer) may consist of e.g., 2 bytes of valid memory and 2 bytes of invalid memory, if the memory lies on a page boundary. You should handle these cases by terminating the user process (call process_exit). Before calling process_exit you should set the exitcode for the running process to -1.

We recommend testing this part of your code before implementing any other system call functionality. See Accessing User Memory and Syscalls for more information.

We recommend that you implement each system call in a separate function and use the syscall_handler function just to dispatch to the proper implementation. You should start by implementing the write system call, in particular for the case when fd == STDOUT_FILENO (forward the arguments to putbuf declared in src/lib/kernel/stdio.h). This enables the use of printf in user-space code and thus facilitates debugging. This will also allow for more tests to pass (run by make check) as the testing infrastructure verifies the correctness of the output generated by a test.

Observe that the argument to the syscall_handler is a pointer to a struct intr_frame. This structure contains information about the CPU state at the moment when the interrupt occurred. For implementing system calls, you will need to read the arguments starting from the address in f->esp, while a return value, if it exists, has to be written to f->eax.

As an example, figure 1 below depicts the memory layout for the arguments to the create system call. These arguments are made accessible to the system call handler at the address f->esp. The first word (4 bytes) represents the system call number (no), the second word, a pointer to a null-terminated array of characters (fn), while the third word is an integer representing the size of the file.

Anatomy of a System Call

Fig. 1 Memory layout of the arguments for the create system call

To avoid the use of pointer arithmetic to access these values, we recommend that you overlay a structure at the address f->esp. The members of this structure depend on the system call that is handled. For the create system call this can be achieved as in the following code snippet:

struct create_args {
    int id;
    const char *file;
    unsigned initial_size;
};

struct create_args *args = (struct create_args *) f->esp;

Implementation Details

PintOS’ file system is not thread-safe, so you must make sure that your file operation syscalls do not call multiple file system functions concurrently. In Project 3, you will add more sophisticated synchronization to the PintOS file system, but for this project, you are permitted to use a global lock on file operation syscalls (i.e. treat it as a single critical section to ensure thread safety). We recommend that you avoid modifying the src/filesys/ directory in this project. Please read the Study Guide section about Locks for more information.

While a user process is running, you must ensure that nobody can modify its executable on disk. The rox-* tests check that this has been implemented correctly. The functions file_deny_write and file_allow_write can assist with this feature. Denying writes to executables backing live processes is important because an operating system may load code pages from the file lazily, or may page out some code pages and reload them from the file later. In PintOS, this is technically not a concern because the file is loaded into memory in its entirety before execution begins, and PintOS does not implement demand paging of any sort. However, you are still required to implement this, as it is good practice. The function file_deny_write should be invoked after the executable image was successfully loaded (see load in src/userprog/process.c) and the function file_allow_write should be invoked in process_exit.

The tests for Project 3 depend on some of the same syscalls that you are implementing for this project, and you may have to modify your implementations of some of these syscalls to support additional features required for Project 3. While you certainly will not be able to plan too far in advance for Project 3, we recommend you write good code that can be easily modified by keeping good documentation.

Syscall Signatures

The create and remove system calls are straightforward to implement. You can simply forward to the existing functions filesys_create and filesys_cremove declared in src/filesys/filesys.h.

create

bool create (const char *file, unsigned initial_size)

Creates a new file called file initially initial_size bytes in size. Returns true if successful, false otherwise. Creating a new file does not open it: opening the new file is a separate operation which would require an open system call.

remove

bool remove (const char *file)

Deletes the file named file. Returns true if successful, false otherwise. A file may be removed regardless of whether it is open or closed, and removing an open file does not close it. See this section of the FAQ for more details.

The system calls open, filesize, read, write, seek, tell, and close require special handling for the cases when fd == STDOUT_FILENO or fd == STDIN_FILENO. Here you need to use the I/O functions declared in src/lib/stdio.h.

For all other cases, you will add a list of file descriptors referred to from the process control block, maintaining a list of associations between a file descriptor (fd) and a struct file * pointer as returned from the internal PintOS file system APIs.

open

int open (const char *file)

Opens the file named file. Returns a nonnegative integer handle called a “file descriptor” (fd), or -1 if the file could not be opened.

File descriptors numbered 0 and 1 are reserved for the console: 0 (STDIN_FILENO) is standard input and 1 (STDOUT_FILENO) is standard output. open should never return either of these file descriptors, which are valid as system call arguments only as explicitly described below.

Each process has an independent set of file descriptors. File descriptors in PintOS are not inherited by child processes.

When a single file is opened more than once, whether by a single process or different processes, each open returns a new file descriptor. Different file descriptors for a single file are closed independently in separate calls to close and they do not share a file position.

filesize

int filesize (int fd)

Returns the size, in bytes, of the open file with file descriptor fd. Returns -1 if fd does not correspond to an entry in the file descriptor table.

read

int read (int fd, void *buffer, unsigned size)

Reads size bytes from the file open as fd into buffer. Returns the number of bytes actually read (0 at end of file), or -1 if the file could not be read (due to a condition other than end of file, such as fd not corresponding to an entry in the file descriptor table). STDIN_FILENO reads from the keyboard using the input_getc function in src/devices/input.c.

write

int write (int fd, const void *buffer, unsigned size)

Writes size bytes from buffer to the open file with file descriptor fd. Returns the number of bytes actually written, which may be less than size if some bytes could not be written. Returns -1 if fd does not correspond to an entry in the file descriptor table.

Writing past end-of-file would normally extend the file, but file growth is not implemented by the basic file system. The expected behavior is to write as many bytes as possible up to end-of-file and return the actual number written, or 0 if no bytes could be written at all.

File descriptor 1 (STDOUT_FILENO) writes to the console. Your code to write to the console should write all of buffer in one call to the putbuf function lib/kernel/console.c, at least as long as size is not bigger than a few hundred bytes. It is reasonable to break up larger buffers. Otherwise, lines of text output by different processes may end up interleaved on the console, confusing both human readers and our autograder.

seek

void seek (int fd, unsigned position)

Changes the next byte to be read or written in open file fd to position, expressed in bytes from the beginning of the file. Thus, a position of 0 is the file’s start. If fd does not correspond to an entry in the file descriptor table, this function should do nothing.

A seek past the current end of a file is not an error. A later read obtains 0 bytes, indicating end of file. A later write extends the file, filling any unwritten gap with zeros. However, in PintOS files have a fixed length until Project 3 is complete, so writes past end-of-file will return an error. These semantics are implemented in the file system and do not require any special effort in the syscall implementation.

tell

int tell(int fd)

Returns the position of the next byte to be read or written in open file fd, expressed in bytes from the beginning of the file. If the operation is unsuccessful, it can either exit with -1 or it can just fail silently.

close

void close (int fd)

Closes file descriptor fd. Exiting or terminating a process must implicitly close all its open file descriptors, as if by calling this function for each one. If the operation is unsuccessful, it can either exit with -1 or it can just fail silently.

The next task in the series of implementing syscalls for this project is described here: Process Control Syscalls