Lab Assignment 2: Multiprocessing - Frequently Asked Questions

Questions:

  1. proc_init or proc_spawn?
  2. how to copy the address space?
  3. return two different values?
  4. where to store child processes?
  5. how to use condition variables?

1. Should I use proc_init or proc_spawn?

Take a look at what they both do. Although proc_spawn helpfully calls proc_init, it does some other stuff that we don’t necessarily need. You’ll probably have more success calling proc_init and copying anything useful you find in proc_spawn.

2. How do I copy the address space from the parent to the child?

There is a handy function as_copy_as that will let you copy the address space:

/*
 * Copy src address space's memregions into dst_as.
 * Return ERR_OK on success, ERR_NOMEM if fails to allocate memregion in dst.
 */
err_t as_copy_as(struct addrspace *src_as, struct addrspace *dst_as);

Remember everything that’s in the address space—that includes the stack and heap, but also the code region, globals, strings, and other read-only data. This means you shouldn’t need to re-load the binary for the process, because it’s ideally already available in the parent’s address space. Also, the trap frame, mentioned below, will handle the stack pointer and program counter.

3. How do I return two different values?

If you have successfully created a new process and its corresponding thread, as well as copied the parent’s address space to the child, your new thread is ready to “return” to the “original” code as if it had been there before. Your return value won’t be coming from the return of proc_fork and therefore sys_fork.

Instead, the child needs to update the “trap frame”, which contains the state of the registers before the trap occurred. The trape frame is the saved information from the user program (the “thread state”) that got trapped into the kernel via a system call. This information is used when returning to user mode to make sure the program keeps running as if nothing has happened.

Here’s part of the code for a trap frame (from arch/x86_64/include/arch/trap.h):

// Trap frame
struct trapframe {
    // Pushed by trap handler
    uint64_t rax;
    uint64_t rbx;
    uint64_t rcx;
    uint64_t rdx;
    uint64_t rbp;
    uint64_t rsi;
    uint64_t rdi;
    uint64_t r8;
    uint64_t r9;
    uint64_t r10;
    uint64_t r11;
    uint64_t r12;
    uint64_t r13;
    uint64_t r14;
    uint64_t r15;
    uint64_t trapnum;

    /* error code, pushed by hardware or 0 by software */
    uint64_t err;
    uint64_t rip;
    uint64_t cs;
    uint64_t rflags;
    /* ss:rsp is always pushed in long mode */
    uint64_t rsp;
    uint64_t ss;
};

Do you remember where the return value from a function goes in x86-64, by convention? You don’t need to worry, as there is a function to help you (which just sets the value to put into %rax, the location that convention dictates):

/* Set return value of the trapframe */
void tf_set_return(struct trapframe *tf, uint64_t retval);

As an aside, make sure that when you’re setting up the child that you copy the trap frame contents (but not the pointer itself) to the child process’s thread. If you have a child thread t, you can do this with:

*t->tf = *thread_current()->tf;

4. Where should I store the child processes?

This is really up to you. It is probably better to make it work first to make sure you understand the goals and the tests, and then go back and clean it up later. This is also a benefit of having a git repo—get it working, add/commit/push, and then try to optimize it a bit.

You could try having an array (or many arrays!) in the parent struct proc, but then you have a static limit on the number of possible processes, which isn’t great. It’d be better to have (or take advantage of) an existing linked-list structure. This is how ptable is stored.

As long as you make sure not to dereference a NULL/freed struct proc for a child that has already exited, the design is your choice!

5. How do I use condition variables?

First off, let’s imagine you don’t use condition variables. Suppose that the parent has a function getStatus that takes a struct proc address and returns the current status (e.g., STATUS_ALIVE or something else if the process has exited). For a given child, the parent could take a busy-waiting approach, spinning using CPU cycles, like this:

while (getStatus(child) == STATUS_ALIVE)
{
    // waiting for child to change status
}

This is really wasteful; the CPU could be making other progress during that time. So, instead, we want the parent to go to sleep until the child’s status changes. We can do this with a condition variable.

Aside: where does the condition variable come from?

  • In general, one condition variable should correspond to one condition/event that processes might wait for.
  • As each process can independently wait for a child, it would be natural to have a separate condition variable for each process—maybe add it to the proc struct?

In the busy-waiting loop above, the parent wants to wait for a specific child to exit, so it could wait on a condition variable (below called wait_cv) associated with that child:

while (getStatus(child) == STATUS_ALIVE)
{
    condvar_wait(&child->wait_cv, ???);
}

Remember that we need to wait while holding a lock to protect the internal list of waiting threads managed by the condition variable. Many lock choices are possible; a simple one is just to use the global ptable_lock:

while (getStatus(child) == STATUS_ALIVE)
{
    condvar_wait(&child->wait_cv, &ptable_lock);
}

Of course, this is maybe slow; you could think about ways to reduce the amount you reuse this one big lock for lots of little things. Also, don’t forget that you need to call condvar_wait in a loop, rather than just an if; the wakeup should be thought of as a suggestion that maybe something interesting has happened, not a guarantee (remember spurious wakeups?).

Finally, we need to think about how the parent knows when to wake up:

  • In the design presented above, a child process should use condvar_signal on its condition variable when it exits.
  • If the parent is waiting, the parent will wake up (yay!).
  • If the parent isn’t waiting, the signal will have no effect (no problem there).

So, in proc_exit, you’d need:

condvar_signal(&p->wait_cv);

assuming that p is the current process that is exiting.

Finally finally, remember that you need to initialize condition variables and locks before they can be used; you should call the spinlock_init and condvar_init functions exactly once, probably in some initialization function you have…