CS 332 w22 — Lab 2 FAQ

  1. How do you clean up the resources for a process?
    • Things that need to be cleaned up: thread, address space (memory), inode, open files, proc struct
    • Existing code in proc_exit handles the first three (proc_detach_thread + thread_exit, as_destroy, fs_release_inode)
    • To clean up open files, fs_close_file needs to be called on each one
    • To clean up the proc struct, use proc_free
  2. How to use a condition variable
    • Suppose in proc_wait, the parent has located the struct for a child it wants to wait on (in a variable child)
    • Suppose we have a function get_status that takes a proc struct and returns its current status (STATUS_ALIVE or another value if the process has exited)
      • Maybe each proc struct has a field status to store the current status of that process (would be initialized to STATUS_ALIVE
      • Then we could just do child->status
    • The parent could take a busy-waiting approach, sitting in a loop checking the child's status over and over:

      while(get_status(child) == STATUS_ALIVE) {}
      
    • For reasons of efficiency, we would prefer the parent go to sleep rather than spinning in a loop
    • Having a thread go to sleep until some condition is met is the entire purpose of a condition variable
    • So we have the parent check the status of the child, and if it hasn't exited, the parent uses a condition variable to go to sleep
    • But where does the condition variable come from?
      • In general, one condition variable should correspond to one condition/event that threads might wait for
      • Since 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?
    • Since the parent wants to wait until the particular child exits, it can wait on the condition variable associated with the child

      while(get_status(child) == STATUS_ALIVE) {
          condvar_wait(&child->wait_cv, ????);
      }
      
    • We also need to wait while holding a lock to protect the internal list of waiting threads maintained by the condition variable
    • Many possible lock designs are possible—a simple one is to use the global ptable_lock to protect shared process data

      while(get_status(child) == STATUS_ALIVE) {
          condvar_wait(&child->wait_cv, &ptable_lock);
      }
      
    • Remember that the lock (ptable_lock) should be held when waiting (calling condvar_wait)
      • condvar_wait will add the current thread to the waiting list, release the lock, and put the thread to sleep
      • When a waiting thread resumes, it will acquire the lock before returning from condvar_wait
    • Ok, so the parent is waiting for child to exit—how does it know when to wake up?
      • In this design, a child process should use condvar_signal on its condition varible when it exits
      • If its parent is waiting, the parent will wake up (which we want)
      • If its parent isn't waiting, the signal will have no effect (which is fine)
    • So in proc_exit, use

      condvar_signal(&p->wait_cv);
      

      where p is the current process to send a signal on that process's condition variable

    • Note that locks and condition variables need to be initialized once before they can be used, via the spinlock_init and condvar_init functions.
  3. When a process exits, what should it do with its children
    • The children should be able to keep running
    • The solution the writeup hints at is pretty simple: an exiting process hands off its children to the initial process (globally defined init_proc), which doesn't exit
      • Hand off means that however you're tracking what a process's parent is, update that information to change the parent of the children to init_proc
      • If you look at user/init.c, you can see all the initial process does is wait for its child processes to exit

        #include <lib/usyscall.h>
        #include <lib/stdio.h>
        #include <lib/stddef.h>
        
        int main(int argc, char** argv) {
            int pid = spawn("sh");
            if (pid < 0) {
                printf("init process fails to spawn children\n");
            }
            while (1) {
                // just waits for all processes to terminate
                wait(-1, NULL); 
            }
            exit(1);
            return 0;
        }
        
    • There are certainly viable designs where a child can tell in proc_exit if its parent has exited or not, and can act accordingly
  4. Is blocking the same as waiting
    • Yes, blocking and waiting both refer to when a process is stopped from making progress
    • As we've seen, a waiting process can be busy-waiting or sleeping (sleeping is preferred where possible, efficiency-wise)
  5. Trap frame? What is it? What does it do? How does it work?
    • The trap frame is saved information from the user program that trapped into the kernel via a system call
    • When returning to user-mode, this information is used to resume running the user program
    • Here's the struct trapframe definition in include/kernel/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;
      };
      

      As you can see, it's basically saving all the CPU registers, so those values can be restored when going back to user-mode

    • To make the child's trap frame a copy of the parent's, we just want to copy the parent's struct to the child:

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

      where t is the thread created for the child

    • To change the return value in the child process, we want to change the value in the register that stores a function's return value: rax
      • This is exactly what tf_set_return does:

        void
        tf_set_return(struct trapframe *tf, uint64_t retval)
        {
            kassert(tf);
            tf->rax = retval;
        }
        
  6. What kind of statuses are there?
    • A status can be any integer
    • The value of STATUS_ALIVE is reserved, so no process will ever exit with that status
  7. Would parent process wait for children that aren't theirs?
    • No, if a process calls wait with a pid that isn't one of its children, wait should return ERR_CHILD
  8. What is "magic" in ptable (which is in list.h)?
    • This is something added by the original authors of osv, which I assume has some purpose related to aligning things in memory
    • As far as I've been able to investigate, it has no effect, and can be removed
  9. Address space—how do we duplicate this?
    • Use as_copy_as to copy one address space to another
  10. How does parent process know when to occasionally go through children and prune off zombies? Is there a signal or something?
    • The only way a parent process cleans up its children is by calling wait