Section 6

Today’s section is about creating new processes in WeensyOS.

fork

Every operating system needs to support process creation—some way for one process to start another one. Process creation always involves the kernel.

QUESTION. Why must process creation involve the kernel?

In UNIX-derived operating systems, the basic process creation system call is called fork. Here’s its description (based on the macOS and Linux manual pages):

fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes performed by one process do not affect the other, though, so the memory spaces quickly diverge.

In most other ways, the processes are identical, but the child process has its own unique process ID that doesn’t match any other existing process. (We’ll learn about other differences in the next unit.)

If fork() succeeds, it returns a value of 0 to the child process and returns the process ID of the child process to the parent process. Otherwise, a value of −1 is returned to the parent process and no child process is created. Errors that might cause fork() to fail include ENOMEM (fork() failed to allocate the necessary kernel structures because memory is tight) and EAGAIN (the maximum number of process IDs was reached).

This example program uses fork to create a child process. The child prints its ID and immediately exits.

#include <print>
#include <unistd.h>

int main() {
    std::print(stderr, "Hello from parent process with PID {}\n", getpid());
    pid_t p = fork();
    if (p == 0) {
        std::print(stderr, "Hello from child process with PID {}\n", getpid());
        exit(0);
    } else if (p > 0) {
        std::print(stderr, "Parent thinks child process has PID {}\n", p);
    } else {
        std::print(stderr, "fork failed\n");
    }
}

Example output in Docker:

cs61-user@bfa9437376b4:~/cs61-sections/fork$ c++ -std=gnu++23 fork.cc
cs61-user@bfa9437376b4:~/cs61-sections/fork$ ./a.out
Hello from parent process with PID 60
Parent thinks child process has PID 61
Hello from child process with PID 61

QUESTION. What would happen if you reordered the function so fork() happened before the first std::print?

QUESTION. Can this program ever print “I am my own child”? Why or why not?

#include <print>
#include <unistd.h>

int main() {
    pid_t original_pid = getpid();
    pid_t p = fork();
    if (p == original_pid) {
        std::print(stderr, "I am my own child!!!\n");
    }
}

QUESTION. Can this program ever print “I am my own child”? Why or why not?

#include <print>
#include <unistd.h>

int main() {
    pid_t original_pid = getpid();
    fork();
    pid_t p = getpid();
    if (p == original_pid) {
        std::print(stderr, "I am my own child!!!\n");
    }
}

WeensyOS sys_fork

In pset 3b, you’re asked to implement sys_fork() for WeensyOS. This involves three broad steps:

  1. Allocate a new process table entry and PID.
  2. Allocate a new page table, and copy the parent’s page table into the child’s. Kernel and reserved mappings are shared, but any user-accessible memory (except the console) must be copied. This ensures that “memory writes performed by one process do not affect the other”.
  3. Initialize the child process’s registers, including its return register, and return the child process ID to the parent process.

Steps 1 and 2 can fail—but exactly how can they fail? In groups, work through all the different ways in which a sys_fork call can fail. Be as specific as you can.

Assuming that a sys_fork system call fails, what should the kernel do? Discuss!

Memory copy helpers

Let’s look at a couple kernel helper functions that resemble functions you’ll need in the problem set. These functions also will give you practice with vmiter. The function specifications are:

int addrspace_copy(x86_64_pagetable* dst, x86_64_pagetable* src)
Copy all virtual memory mappings from src into dst in the range [0, MEMSIZE_VIRTUAL). Return 0 on success and a negative error code on error. If this function completes successfully, then for any va with 0 <= va < MEMSIZE_VIRTUAL, dst and src should map that va identically (to the same physical address, if any, and with the same permissions).
void addrspace_free_user(x86_64_pagetable* pt)
Free all user-accessible memory mapped by pt in the range [0, MEMSIZE_VIRTUAL), not including the page mapped at CONSOLE_ADDR. (Free a page by passing its kernel virtual address—that is, the vmiter::kptr()—to the kfree function.)

Implement these functions in groups and then discuss.

QUESTION. A function like addrspace_free_user could be used to clean up a partially-forked child process after a full copy attempt failed. But is that function enough? What other resources would need to be freed to clean up after the failed fork? If there are any, write a function that frees them.

From helpers to sys_fork

Discuss how to use the helpers in an implementation of sys_fork. How would you change addrspace_copy to handle sys_fork’s requirements? When would you call addrspace_free_user and any other functions? Sketch your implementations and discuss potential pitfalls.

Spawn

If you have time, let’s look at a different interface for process creation. The sys_spawn system call starts a process by name, rather than by copying the current process. Its interface is as follows:

pid_t sys_spawn(const char* program)
Start a new process running a named program. program must equal one of the processes supported by WeensyOS; in the lecture repository, for instance, it can be "alice", "eve", "hello", "pipereader", "pipewriter", "recurse", or "spawn". On success returns the ID of the child process; on failure returns a negative error code.

EXERCISE. The p-spawn.cc process tries to start a copy of the p-eve program—by calling sys_spawn("eve"). Update the lecture repository, cd kernel within Docker, and use make run-spawn to run this program. What’s going on?

EXERCISE. Complete the unprivileged portion of the system call implementation for sys_spawn.

EXERCISE. Now complete the privileged portion of the system call implementation for sys_spawn. You don’t need to handle error cases, but make it so that make run-spawn successfully runs both “Sheng” and “Eve”. (Hint: You may be able to delegate most work to process_setup!)

EXERCISE. Change p-spawn.cc to call sys_spawn("nonexistent") rather than sys_spawn("eve"). What happens? Why? Is this behavior sensible, and if not, how can you fix it?

EXERCISE. Change p-spawn.cc to call sys_spawn("alice") rather than sys_spawn("eve"). What happens? Why? Is this behavior sensible, and if not, how can you fix it?

DISCUSSION. What other potential errors remain? Can we make syscall_spawn more robust?