Skip to main content

Lab 2: Process Manager

Due: September 30 @ 11:59 PM

In this lab you'll create a small library called Subsubprocess (ssp) that acts as a subreaper in addition to creating and monitoring processes. Your implementation should use the concepts learned during the lectures, along with some new system calls. As a change, you create library called `libssp`` this lab, not an executable. You'll be using Git to submit your work and save your progress.

Setup

Ensure you're in the ece344 directory within VSCode. Make sure you have the latest skeleton code from us by running: git pull upstream main.

This will create a merge, which you should be able to do cleanly. If you don't know how to do this read Pro Git. Be sure to read chapter 3.1 and 3.2 fully. This is how software developers coordinate and work together in large projects. For this course, you should always merge to maintain proper history between yourself and the provided repository. You should never rebase in this course, and in general you should never rebase unless you have a good reason to. It will be important to keep your code up-to-date during this lab as the test cases may change with your help.

You can finally run: cd ssp to begin the lab.

Task

You're going to create a process manager similar to the Python subprocess module in some respects. Your version is going to be a C library with the following API:

void ssp_init();
int ssp_create(char *const *argv, int fd0, int fd1, int fd2);
void ssp_send_signal(int ssp_id, int signum);
int ssp_get_status(int ssp_id);
void ssp_wait();
void ssp_print();

The description of what each function should do is below:

void ssp_init()

This will always be called once before a user makes any other call to your library. You should initialize or setup anything you need here.

int ssp_create(char *const *argv, int fd0, int fd1, int fd2)

You will create a new process in this function, that new process should eventually call execvp(argv[0], argv). You must set file descriptors 0, 1, and 2 to match the arguments fd0, fd1, and fd2 through calls to dup2. Afterwards you must then close all other file descriptors except for 0, 1, and 2.

You will not rely on library users to properly manage file descriptors (and in fact they can't since your library calls fork). You may find some interesting file descriptors left open by VSCode. Your experience with Lab 1 will help you close the other file descriptors, since each process has an fd directory. As a hint: within this directory only consider files with d_type set to DT_LNK.

The execvp wrapper will re-use the current env variable, and search for the program given by argv[0] using the PATH environment variable (this variable is a list of directories to search for executables). If execvp fails you should immediately exit with errno set by the execvp call. Your library should record the process ID of the newly created process, the name (you need to copy the argv[0] string, the library user may re-use the memory for something else, so you can't rely on it), and its status.

You should record the created process and initially set the status to -1 to indicate it's running. You should return a unique ssp_id that your library will use to refer to this created process. The IDs should be sequential and start with 0.

void ssp_send_signal(int ssp_id, int signum)

You should send a signal signum to the process referred to by ssp_id. If the process is no longer running, you should not return an error and instead do nothing.

int ssp_get_status(int ssp_id)

You should return the current status of the process referred to by ssp_id without blocking.

void ssp_wait()

This function should block and only return when all processes created through ssp_create terminate. As a sanity check, all processes should have a status between 0 and 255 after this call completes.

void ssp_print()

This is a non-blocking call that outputs the PID, name, and current status of every process created through ssp_create. This should reflect the current state of the processes, so you should query them in this function.

You should start by printing a header, which will be PID right-justified with a width of 7 characters, a space, then CMD left-justified to the width of the longest process name, a space, then STATUS. After the header, for each process created by ssp_create you should output its pid, name, and current status. Recall that the name is your copied argv[0] string.

status

The status of each process should match its exit status if it exits normally. However, if the process terminates through a signal you should set the status to be equal to the signal number plus 128. Recall that a status of -1 means the process is currently active.

Errors

You need to check for and properly handle errors. Some errors are expected and should be handled without additional output or exiting the process. For fatal errors, you should exit with the errno of the first fatal error.

Become a Subreaper (20% of the Grade)

This task may make your implementation more complex, or you may have to scrap your first attempt. It's advised to complete the other parts of the lab first. However, when you're ready you should add a call to prctl(PR_SET_CHILD_SUBREAPER, 1); in `ssp_init`` to become a subreaper.

A subreaper will adopt all orphan processes created by child processes. In other words, your process will be the new parent when an orphan process gets re-parented (instead of init). It'll be your job to call waitpid on any adopted process as soon as they terminate.

As part of being a subreaper you should record any time an adopted process terminates. You should record its pid and status. For its name you should simply call it "<unknown>". Add these unknown processes to be displayed when you call ssp_print after all the processes managed directly by the library. These processes should be output in the order they terminate.

Building

First, make sure you're in the ssp directory if you're not already. After, run the following commands:

meson setup build
meson compile -C build

Whenever you make changes, you can run the compile command again. You should only need to run setup once.

Testing

You cannot execute your library directly, however you can run the test programs manually. Please find the files in tests/*.c. You should be able to read and understand what they're doing with your library. You'll find the executables in build/tests/*.

You may also choose to run the test suite provided with the command:

meson test --print-errorlogs -C build

Grading

Run the ./grade.py script in the directory. This will rebuild your program, run the tests, and give you a grade out of 100 based on your test results. Note that these test cases may not be complete, more may be added before the due date, or there may be hidden test cases. These labs are new, so we may need to change.

Tips

You'll want to read the documentation on some more C functions (some are light syscall wrappers). Some header files you'll need to use are provided for you in the skeleton code. You may include additional parts of the standard library. It's highly recommended to at least use the following functions:

open fdopendir readdir closedir dup2 waitpid fork execvp malloc perror exit

Reading Test Cases for Clarification

Often the best way to understand software is to read code, it's best to get practice in labs.

Each test consists of two parts, a .py file that checks the expected output and a .c file that compiles with your library. We'll go over the single-cat test to demonstrate. First, if you just want to run the test by itself, you can do:

meson test --print-errorlogs tests/single-cat -C build

First, let's read the C file that defines main and uses your library, it's located at tests/single-cat.c:

#include "ssp.h"

#include <assert.h>
#include <errno.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
ssp_init();

char *const cat_argv[] = {
"cat",
NULL,
};

int pipedes[2];
int err = pipe(pipedes);
if (err == -1) {
exit(errno);
}
int cat_id = ssp_create(cat_argv, pipedes[0], 1, 2);
close(pipedes[0]);

int status = ssp_get_status(cat_id);
ssp_print();
assert(status == -1);

close(pipedes[1]);

ssp_wait();
status = ssp_get_status(cat_id);
ssp_print();
assert(status == 0);

return 0;
}

The test starts by calling ssp_init();, as all tests do. After, it defines an array of C strings to use as the argv argument of ssp_create and a pipe. This program wants to create a new cat process, and make file descriptor 0 of cat point to the read end of this pipe.

When the new process starts running the cat program you created in ssp_create, it should only have 3 file descriptors open. File descriptor 0 should be the read end of the pipe, 1 should be the same as this process, and 2 should also be the same as this process. You should not modify the callers file descriptors, they are responsible for them. You should only modify the file descriptors in your newly created process within ssp_create. ssp_create returns a new ID, which is how you keep track of processes, it should be 0 in this case.

In this test, the cat process should block until we close our write end of the pipe. We call ssp_get_status with the ID returned by ssp_create. In this case it should be 0, and the return value of ssp_get_status should be -1 indicating that the child cat process is still running. It then calls ssp_print which should print to standard out that the process is still running as well.

The test then closes the write end of the pipe, this causes the cat program to terminate and become a zombie. The test calls ssp_wait so that it can acknowledge the terminated process and the OS can clean it up. Your code in ssp_wait should read the exit status of the cat process and store it.

Next, the test calls ssp_get_status again for the cat process, which should now return 0, indicating the process terminated without errorr. The test calls ssp_print again which should show the updated exit status.

As long as every assert statement in the test is true, it'll check the output of all your ssp_print calls. The output checking code is in

#!/usr/bin/env python3

from base import test

expected = [
' PID CMD STATUS\n',
' cat -1\n',
' PID CMD STATUS\n',
' cat 0\n',
]

test(expected)

The only important part is the expected variable, that contains the expected output of each line of your program. It shows the correct spacing and the expected output of the CMD and STATUS. Note that it explicitly does not include any values in the PID by design, you'll likely get a different pid every time you run the program.

If you're debugging your implementation, it may be best to run the executable directly, outside the testing framework. In order to run this test you can run:

build/tests/single-cat

Since you're just running a normal executable again, you can use tools like strace to see what's going on. For this lab I'd suggest using the -f option so you can see system calls from new processes:

strace -f build/tests/single-cat

Submission

Simply push your code using git push origin main (or simply git push) to submit it. You need to create your own commits to push, you can use as many as you'd like. You'll need to use the git add and git commit commands. You may push as many commits as you want, your latest commit that modifies the lab files counts as your submission. For submission time we will only look at the timestamp on our server. We will never use your commit times (or file access times) as proof of submission, only when you push your code to the course Git server.

Common Issues

I Didn't Check for Errors for Every Function

You absolutely should. You may waste hours and hours debugging something because you didn't detect an error and exit early. For this lab you may find it helpful to exit with a known exit status like 42, so you know when a child process has that exit status it was because of a failure. Especially if you're modifying file descriptors this may be the only easy way to debug.

This includes checking for errors from opendir, and readdir. readdir is especially annoying to check for errors, but you should still do it! If you read the documentation you'll find you should handle it similar to the following snippet:

while (1) {
errno = 0;
entry = readdir(dir);
if (entry == NULL) {
// At this point we don't know if we're done or if there's an error.
if (errno == 0) {
// No error, we're done! Stop this crazy loop.
}
else {
// There was an error! errno is set to indicate what it is.
// Panic! Exit! Do something!
}
}
}

Signals Causing System Calls to Fail

We saw this in Lecture 6 in signal-example.c, you may find it annoying or impossible to handle system calls failing with EINTR (like in signal-example-2.c). An alternative is to add some flags to your struct sigaction, as below:

new_action.sa_flags = SA_RESTART | SA_NOCLDSTOP;

SA_RESTART will automatically restart system calls (so you won't see EINTR), and SA_NOCLDSTOP will force the kernel to only send your process SIGCHLD if the child terminates (and not any other event).

Signals Missing Children

Note that if a SIGCHLD is sent while you are in the signal handler, a new signal will not be triggered. Your handler needs to make sure that there are no more terminated children before it exits.

VSCode Broke with the Devcontainer on Windows

There seems to be an issue outlined here. Make sure to pull the latest code from upstream that modifies devcontainer.json. After, go to File > Preferences, and search for the "Mount Wayland Socket" option and make sure it's turned off. It should correspond to the following VSCode settings:

"customizations": {
"vscode": {
"settings": {
"dev.containers.mountWaylandSocket": false
}
}
}

After that press F1 and search for "Rebuild and Reopen Container". Click that and let everything rebuild.