Table of Contents
Process Control Syscalls
In addition to the file system syscalls, PintOS currently only supports one syscall, exit
, which terminates the calling process. You will add support for the following new syscalls: practice
, halt
, exec
, and wait
.
Implementation Details
Try to avoid writing large amounts of repetitive code for implementing system calls. Each system call argument, whether an integer or a pointer, takes up 4 bytes on the stack. You should be able to take advantage of this to avoid writing much near-identical code for retrieving each system call’s arguments from the stack.
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). Beware: a 4-byte memory region (e.g. a 32-bit integer) may consist of 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 (callprocess_exit
). Before callingprocess_exit
you should make sure the return code from the process is set 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.
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.
More Details for exec
, wait
, and exit
In the context of this task, there are two situations when the execution of a thread cannot proceed until an event has happened in another thread.
- Wait in the kernel for a new process to finish attempt loading the executable file while implementing
process_start
(for theexec
syscall). - Wait for a child process to finish running while implementing
process_wait
(for thewait
syscall).
This synchronization between threads is achieved using semaphores (please read the Study Guide Semaphores). PintOS already provides a semaphore implementation, declared in src/thread/synch.h
.
Study the interface of the semaphore functions and describe the sequence of calls required for a thread to be able to wait for an event that happens in another thread.
Implementing the syscall exec
should make use of the process_execute()
function, defined in src/userprog/process.c
. You have already seen this function as it is used by the operating system entry point to run commands that are given as arguments to the kernel. The major difference between the existing implementation of process_execute()
and the requirements for the exec
system call is that the process_execute()
may return before the executable program is loaded.
The current implementation of process_execute()
calls the thread_create()
function, which, among other things, creates a new kernel thread and schedules it for execution. When this thread is scheduled, its execution starts with the start_process()
function. Observe that process_execute()
and start_process()
are running on different threads and we want to block the execution of process_execute()
until start_process()
has attempted to load the executable from disk (i.e. the call to load() has returned).
To achieve this synchronization, we need a semaphore in the process_execute()
function and pass a pointer to it to the thread_create
function. Observe that any pointer that we pass in the aux
(the last) argument of thread_create()
is forwarded as the first (and only) argument of the start_process()
function. So instead of passing only a pointer to the command line, we can use this argument to pass a pointer to a struct that contains the command line, a pointer to the semaphore, and other information that may be useful.
For implementing the wait
system call we have to consider that the call to this function has to block the calling process until the waited-for process has exited.
To implement the required system calls, we need a way to keep track for each thread what its children and parent are. For this purpose, fields can be added as needed to struct thread
(declared in src/threads/thread.h
) and/or struct process
. In addition, we also need a way to pass the exit code of a thread to its parent. In the current implementation the memory backing the struct thread
/struct process
is deallocated when the thread is dying, in thread_schedule_tail()
. A good idea is to keep this structure around until any waits in the parent are dealt with. This can be implemented by adding a list of dying child-threads in struct thread
/struct process
and moving the dying thread to this list instead of deallocating the memory immediately.
Syscall Signatures
practice
int practice (int i)
A “fake” syscall designed to get you familiar with the syscall interface This syscall increments the passed in integer argument by 1 and returns it back to the user.
halt
void halt (void)
Terminates PintOS by calling the shutdown_power_off
function in src/devices/shutdown.h
. This should be seldom used, because you lose some information about possible deadlock situations, etc.
exit
void exit (int status)
Terminates the current user program, returning status to the kernel. If the process’s parent waits for it (see below), this is the status that will be returned. Conventionally, a status of 0
indicates success and nonzero values indicate errors. Every user program that finishes normally calls exit
- even a program that returns from main calls exit
indirectly (see Program Startup). In order to make the test suite pass, you need to print out the exit status of each user program when it exits. The format should be %s: exit(%d)
followed by a newline, where the process name and exit code respectively substitute %s
and %d
. You may need to modify the existing implementation of exit to support other syscalls.
exec
pid_t exec (const char *cmd_line)
Runs the executable whose name is given in cmd_line, passing any given arguments, and returns the new process’s program id (pid
). If the program cannot load or run for any reason, return -1
. Thus, the parent process cannot return from a call to exec
until it knows whether the child process successfully loaded its executable. You must use appropriate synchronization to ensure this.
Keep in mind exec
is different from Unix exec
. It can be thought of as a Unix fork
+ Unix exec
.
wait
int wait (pid_t pid)
Waits for a child process pid
and retrieves the child’s exit status. If pid
is still alive, waits until it terminates. Then, returns the status that pid
passed to exit
. If pid
did not call exit
but was terminated by the kernel (e.g. killed due to an exception), wait
must return -1
. It is perfectly legal for a parent process to wait
for child processes that have already terminated by the time the parent calls wait
, but the kernel must still allow the parent to retrieve its child’s exit status, or learn that the child was terminated by the kernel.
The wait
syscall must fail and return -1
immediately if any of the following conditions are true:
pid
does not refer to a direct child of the calling process.pid
is a direct child of the calling process if and only if the calling process receivedpid
as a return value from a successful call to exec. Note that children are not inherited: ifA
spawns childB
andB
spawns child processC
, thenA
cannotwait
forC
, even ifB
is dead. A call towait(C)
by processA
must fail. Similarly, orphaned processes are not assigned to a new parent if their parent process exits before they do.- The process that calls
wait
has already calledwait
onpid
. That is, a process maywait
for any given child at most once.
Processes may spawn any number of children, wait
for them in any order, and may even exit without having waited for some or all of their children. Your design should consider all the ways in which waits can occur. All of a process’s resources, including its struct process
, must be freed whether its parent ever waits for it or not, and regardless of whether the child exits before or after its parent.
You must ensure that PintOS does not terminate until the initial process exits. The supplied PintOS code tries to do this by calling the process_wait
function in src/userprog/process.c
from the main function in src/threads/init.c
. We suggest that you implement the process_wait
function in src/userprog/process.c
according to the docstring and then implement wait
in terms of process_wait
.
Implementing wait
requires considerably more work than the other syscalls.
The last task for this project is described here: Floating Point Syscalls.