LAB1: Introduction

The shell is the interface provided by an operating system to its services. In this assignment, you will implement a simple shell for an UNIX-based operating system. Your shell will provide basic functionalities such as the ability to run multiple programs, directory tree navigation, I/O redirection and inter-process communication.

Learning outcomes

You will get an understanding of how a simple UNIX shell can be implemented. You will become familiar with pipes, I/O redirection, environment variables, etc.

Assignment

Task Read the man pages for the following system calls (Linux: man -S 2 fork etc).

Task Fetch the incomplete UNIX shell source code and ensure that it builds:

  1. Move to the lab1_shell directory and build the shell:

    cd lab1_shell make

This should produce an executable, sh, which is an incomplete shell.

  1. Run the tests and watch them fail.

    make test

During this assignment, you will change two functions in the file sh.c, parse_line and run_program, to make the tests pass.

Task Implement the functionality required to run a single program. Modify the run_program function such that you will be able to execute:

Use the fork and wait system calls. Your implementation must handle both programs that exist in the $PATH environment variable and programs that are given explicitly via their path. Check that the current user can access and execute the program file (use the access function). The global variable path_dir_list is a circular list containing the directories in the $PATH variable.

Task Extend the shell such that it can run a sequence of programs. Modify the gettoken function to handle ; between commands. You should be able to execute:

Task Add support for background processes. This should allow you to start a long-running process in the background, but immediately return to the shell prompt. Implementation-wise, this means that the shell does not need to wait for the child process to terminate. Once the child process terminates it will become a zombie process, that has exited but has been not waited for by the parent. You can see these processes by running ps. To remove these zombie processes, the shell should regularly check for exited children using a non-blocking waitpid system call with the WNOHANG flag as an argument. You should be able to execute:

Task Add directory tree navigation by implementing the cd command. The following commands should work:

Observe that cd is a shell built-in, not a program in $PATH.

Task Implement I/O redirection Allow for the standard output of a program to be redirected to a file and for the contents of a file to be redirected to the standard input.

The dup2 system call might be useful.

Task Implement inter-process communication using pipes Redirect the output from one process to the input of another process.

Use the pipe and close system calls. Small changes in the parse_line(void) function might be needed.

Hand-in

Ensure the make test returns a PASS result. Once you are done, push the changes to a branch with a name starting with lab1.

git push origin HEAD:lab1

Then go to the Gitlab page, create a merge request from the lab1 branch to master, and assign it to one of the instructors (as explained in Lab 0).

Hints

The approximate size of the code changes is:

sh.c     | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------

LAB2: Introduction

Signals are an important IPC mechanism present in most operating systems. Usually they are ready to use and require the least setup out of all other IPC mechanisms. However, they are useful in specific cases which might not suit all situations.

Learning outcomes

Understand and get experience with UNIX signals.

Assignment

Core dumps

Write a C program which crashes. Try to make it short, e.g. less than 30 bytes. Run it and see what happens (not much should happen). In general it is useful to get feedback from the system about where in the source code a program crashes. You will learn that next.

A core file is a dump of the memory and the register state of a process. Producing such a file is called dumping core or making a core dump. Debuggers can read core files so you can do post-mortem debugging, i.e. inspect a process after its death.

To enable this, issue the following command in the shell:

ulimit -c unlimited

On some systems (e.g. Ubuntu 18.04), you also have to disable the apport service:

sudo service apport stop

Compile with debugging info (i.e. -g) your crashing program and rerun it. You should get a core file, either in the same directory you ran the program from or in a specific directory dedicated to these. (E.g. on the student machines, check the /var/lib/apport/coredump/ directory.) Then you can run gdb a.out core to get info about where it crashed.

Sending signals to a process

Signals can be sent to a process, as above, due to an event in the program itself. But they can also be sent from outside the process. You can do so by using the kill command. Read the manual pages for this command, man 1 kill. For a complete list of signals, you can read man 7 signal.

Task Write a program which is an infinite loop, start your program and put in the background (e.g. ./a.out &). Now send the SIGINT signal to the process and see what happens. You can do so with the command kill -2 pid, or with the longer kill -INT pid. Did you get a core file? You should not. SIGINT causes a normal termination.

Now start the program again, but this time send the SIGQUIT signal to the process and see what happens. SIGQUIT is signal number 3. SIGQUIT is one of the signals which cause an abnormal termination and a core dump. Now you should get a core file. Again type gdb a.out core. You should be able to see e.g. where the program was when it received the signal. Post mortem debugging can be very useful when a program terminates unexpectedly.

Checking the return status of a program

Task Write a program (in a file called exitcode.c) that takes another program as argument, executes it, prints the PID of the process, prints its exit status and also whether it terminated normally or abnormally. If the process was terminated by a signal, it should also print that together with the signal number. You will have to use fork, exec, waitpid system calls and the WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG macros. You can test these by sending signals using the kill command.

C API for signal handling

A process has three choices when it receives a signal:

ISO C defines the following signals (i.e. they must be supported also by non-UNIX C compilers):

A signal set (of type sigset_t) is a set of signal numbers. Typically a variable of type sigset_t is declared and then initialized using either sigemptyset or sigfillset, followed by sigaddset or sigdelset. See the man page for sigemptyset.

To install a signal handler for a signal using the UNIX interface directly, the sigaction system call is used. A signal set (sigset_t) specifies a set of signals which are put on hold (i.e. ignored) while the installed signal handler is executing.

Installing signal handlers

Task Extend your infinite-loop program (in a file called loop1.c) to print a message every time the user hits Ctrl-C (SIGINT).

Task Now, install a signal handler for the signal SIGINT which is itself an infinite loop. While it is looping, you should ignore signal USR1 but handle USR2. For the signal USR2, you should install a signal handler which prints out a suitable message. While a signal handler is executing due to a signal s then s itself is blocked.

Masking signals

Suppose your program is executing some critical code in which it does not want to be terminated or receive certain signals. One way to achieve that is to ignore the relevant signals (except SIGKILL which cannot be ignored). An alternative is to block the signals, or putting them on hold. We used the blocking of a signal when a signal handler was executing (i.e. the signal itself) but we can also block them without executing in a signal handler. The system call sigprocmask is used for this. It blocks some signals but not all.

Task Create another program (in file loop2.c) that executes a loop for about 10 seconds. Make it print a message before entering the loop. After the loop, it should print all the signals it received during the execution of the loop. How can you portably collect the signals even if you don't know how many there are? Your code should work even if a new signal number is added, so testing each known signal explicitly is not permitted. See sigprocmask, sigpending, and sigismember.

Alarms

Finally, one can use alarm to get the SIGALRM signal after a specified number of seconds. Task Write another program (in file loop3.c) containing a loop and install the appropriate signal handler to count how many iterations of the loop can be executed in 10 seconds. This is sometimes a useful way to benchmark a routine. Consider using a global boolean variable to control the termination of the loop. In the signal handler, change the value of the variable such that the loop exits. Try your program with and without optimization (e.g. -O4).

Hand-in

The source files for the programs exitcode.c, loop1.c, loop2.c, loop3.c. Once you are done, push the changes to a branch with a name starting with lab2.

git push origin HEAD:lab2

LAB3: Introduction

NOTE: You may choose to implement this assignment in C or in Zig Links to an external site. (a newer system programming language). You should see this as a challenge and an opportunity to learn another language. The description below is made for the C version, but the differences in program structure are minor. Some specific instructions for the Zig version are given at the end.

In this assigment you will add virtual memory support to a simulator of a very simple CPU. As part of this you will implement three (3) page-replacement algorithms.

Learning outcomes

Understand the memory paging mechanism.

Before you start

Make sure you are familiar with the concepts: virtual memory, FIFO and second-chance page replacement algorithms, optimal page replacement, Bélády's anomaly.

Consider the following page access sequence: 1 2 3 4 1 2 5 1 2 3 4 5. Simulate (paper & pencil) this sequence on a machine with 3 memory frames using FIFO page replacement. How many page faults do you get. Simulate the same sequence on a machine with 4 memory frames. How many page faults do you get now? Why?

Assignment

Set up

  1. The source code for this assigment is available in the lab3_vm directory. The whole machine simulator is contained in the machine.c file. The assembly program that will run on the simulator is fac.s.
  2. Type make to check that everything builds.

Study the machine simulator

Make sure you understand what the functions read_memory and write_memory are doing and where they are called from. Study the page_table_entry_t and coremap_entry_t data structures.

Note also that:

Study the fac.s program

The program is computing 12! and writes the result in register R3. Be prepared to modify the program such that it computes 2!. Answer the following questions:

Implement the FIFO page replacement algorithm

Complete the following functions:

You can run the program by typing make run-fifo. How many page faults do you get?

Implement the second chance page replacement algorithm

Complete the second_chance_replace function. You can run the program by typing make run-sc. How many page faults do you get now? Compare with the FIFO algorithm.

Optimize the number of disk writes

Add a new variable to the simulator that count the disk writes and print it out at the end of the program. Use the page_table_entry_t::modified field to reduce the number of disk writes.

Optimal page replacement

Implement the Optimal page replacement algorithm and compare with the page replacement algorithms you have already implemented. Keep a trace of virtual page accesses when you run the program with one of the paging algorithms you have already implemented and then simulate the optimal page replacement algorithm on that trace.

Hand-in

Once you have implemented all of the above page replacement algorithms, add a README file to the repository describing the differences between them. Ensure that your code build and then push the changes to a branch with a name starting with lab3.

git push origin HEAD:lab3

Hints

The approximate size of the code changes is:

machine.c | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 163 insertions(+), 10 deletions(-)

Zig

Uses zig version 0.12.0-dev.1108+aeadcb391 (tested also with 0.11.0)

Make Zig visible

Run /usr/local/cs/EDAF35/setup in the terminal you will use on your the lab machine. If you run this on your own machine make sure you install zig yourself - it's already installed on the Linux lab machines.

Build the Program

In the directory with build.zig run zig build.

Run the Unit Tests

To run the tests from main.zig: try zig build test - tests should fail for the skeleton code. You may extend these tests if you wish.

Run the Program

Try zig build run which will give you:

Options are:
   --fifo or --second-chance
   optionally --debug
error: ChooseOneReplacement
...

You need to pick either fifo or second-chance to run either of your implemented functions. So zig build run -- --fifo a.s will run the machine on code from file a.s using FIFO policy. You may leave out the file name if you want to use fac.s file by default. For more debug prints use --debug. Alternatively you can use directly the executable in zig-out/bin.

Code Changes for the Zig Version

Take the following with a grain of salt since you might implement things differently:

lab3_vm/zig/src/main.zig | 51 +++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 43 insertions(+), 8 deletions(-)