lunduniversity.lu.se

Computer Science

Faculty of Engineering, LTH

Denna sida på svenska This page in English

Lab3: Advanced PintOS System Calls

Introduction

One of the main functionalities provided by operating system is the management of user processes. To achieve this in PintOS, you will implement three system calls exec(), wait() and exit().

Learning outcomes

After completing this assignment you will have an understanding on how an operating system manages user processes.

Assignment

In this assignment, you continue the work on implementing system calls, that you have started in the previous assignment. The system calls you need to implement are:

  • pid_t exec (const char * cmd_line ) - runs the executable given in cmd_line, providing any existing arguments;
  • void exit (int status) - terminates the current process and returns its status to the kernel;
  • int wait (pid_t child) - waits for a child process to finish and returns its exit status.

Before proceeding, re-read sections 3.1, 3.3.4, 3.5, A.2 and A.3 from the PintOS documentation.

The main building directory for this assignment is src/userprog/build. To run the tests, run make check in this directory. All of them are expected to fail, since nothing is implemented.

In this task there are two situations when the execution of a thread cannot proceed until an event has happened in another thread. This synchronization between threads is achieved using semaphores. PintOS already provides a semaphore implementation, declared in src/thread/synch.h.

Task Study the interface of the semaphore function and describe the sequence of calls required for a thread to be able to wait for an event that happens in another thread.

Task: The first step is to implement the exec() system call.

This makes use of the process_execute() function, defined in src/userprog/process.c. This function is already 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 argument of thread_create() is forwarded as the load_p argument of the start_process() function. So instead of passing only a pointer to the executable name, we can use this argument to pass a pointer to a struct that contains the executable name, a pointer to the semaphore and other information that may be useful.

Task: Implement the wait() and exit() system calls. When implementing the wait() 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. 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 thread struct 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 and moving the dying thread to this list instead of deallocating the memory immediately.

Hand-in

  • Answers to the questions listed above.
  • The modified PintOS source code that is passing all the make check tests, except the following:
FAIL tests/userprog/rox-simple
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
FAIL tests/userprog/bad-read
FAIL tests/userprog/bad-write
FAIL tests/userprog/bad-read2
FAIL tests/userprog/bad-write2
FAIL tests/userprog/bad-jump
FAIL tests/userprog/bad-jump2
FAIL tests/userprog/no-vm/multi-oom

Hints

  • This assignment does not require writing any assembly code.
  • The data structures needed to solve this exercise are lists, arrays and structs. In particular, you will not need to use hash maps.
  • The estimated time needed to solve this exercise is 12h.
  • The approximate size of the code changes is:
src/threads/thread.c     |  24 ++++++++++++++++++++++++
src/threads/thread.h     |  13 +++++++++++++
src/userprog/Make.vars   |   4 ++--
src/userprog/exception.c |   3 +++
src/userprog/process.c   | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/userprog/syscall.c   |  69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--