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 callingprocess_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.
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 functionsfile_deny_write
andfile_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 functionfile_deny_write
should be invoked after the executable image was successfully loaded (seeload
insrc/userprog/process.c
) and the functionfile_allow_write
should be invoked inprocess_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
initiallyinitial_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 anopen
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
and1
are reserved for the console:0
(STDIN_FILENO
) is standard input and1
(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
iffd
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 asfd
intobuffer
. 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 asfd
not corresponding to an entry in the file descriptor table).STDIN_FILENO
reads from the keyboard using theinput_getc
function insrc/devices/input.c
.
write
int write (int fd, const void *buffer, unsigned size)
Writes
size
bytes frombuffer
to the open file with file descriptorfd
. Returns the number of bytes actually written, which may be less than size if some bytes could not be written. Returns-1
iffd
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 theputbuf
functionlib/kernel/console.c
, at least as long assize
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
toposition
, expressed in bytes from the beginning of the file. Thus, aposition
of0
is the file’s start. Iffd
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