CS360 Lecture notes -- Fork

  • Jim Plank
  • Directory: /blugreen/homes/plank/cs360/notes/Fork
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Fork/lecture.html
    This is a lecture covering: This stuff is all in the book as well, in chapter 8, which is a very readable chapter.

    Getpid, getppid

    If you remember from the vocabulary list, a "process" is an executing instance of a program. In Unix, you can make a listing of all the processes that you are currently running with the ps command. Try the following:
    UNIX> ps x
    25919 co IW    0:00 -csh (csh)
    29620 co IW    0:00 xinit
    29621 co S     0:32 X :0
    29645 p0 S     0:01 xclock -geometry 100x100-0-0 -update 60
    29646 p0 S     0:01 spermwatch -newmail xloadimage -onroot /blugreen/homes/plan
    29647 p0 IW    0:00 /bin/sh /blugreen/homes/plank/bin/xyobiff
    29669 p0 S     0:01 twm
    29676 p1 IW    0:00 yobiff plank
      381 p2 S     0:01 -sh (csh)
      713 p2 R     0:00 ps x
      693 p3 S     0:00 vi lecture
    29678 p3 IW    0:00 -sh (csh)
    29684 p4 IW    0:00 -sh (csh)
    29686 p5 IW    0:00 -sh (csh)
    29685 p6 IW    0:00 -sh (csh)
    UNIX>
    
    You should see something like the above. As you should notice, every csh has its own process. The first column of numbers are the "Process ID" numbers, which are called "pid"s. Every process has a non-negative pid, which is unique to that process on that machine while the process is executing. There are two special processes with pids 0 and 1: the scheduler and the init program. You can see them using ps aux: (on some machines, you'll see the init process as "/etc/init", and the scheduler as "sched". Also, some machines use different options for ps -- if "ps aux" doesn't work, try "ps -ef").
    UNIX> ps aux
    ...
    root         1  0.0  0.0   52    0 ?  IW   Mar 16  0:01 /sbin/init -
    ...
    root         0  0.0  0.0    0    0 ?  D    Mar 16  0:16 swapper
    ...
    plank      756 15.4  1.5  216  468 p2 R    11:55   0:00 ps aux
    plank      381  0.0  0.6  116  196 p2 S    11:19   0:01 -sh (csh)
    ...
    UNIX>
    
    These processes are always running while the machine is running.

    Every process has a "parent" process. This is the process that created it. You can get your pid and your parent's pid by using the getpid() and getppid() commands. The program showpid.c is a simple program that prints out its pid, and its parent's pid:

    main()
    {
      printf("My pid = %d.  My parent's pid = %d\n", getpid(), getppid());
    }
    
    Each time you run it, it will show a different pid, but the same parent pid:
    UNIX> showpid
    My pid = 854.  My parent's pid = 381
    UNIX> showpid
    My pid = 855.  My parent's pid = 381
    UNIX> showpid
    My pid = 856.  My parent's pid = 381
    UNIX> showpid
    My pid = 857.  My parent's pid = 381
    UNIX> ps x
    ...
      381 p2 S     0:01 -sh (csh)
    ...
    UNIX>
    
    As you can see, the csh is the parent of each of these. This is because you typed the commands into the csh, which then created the showpid processes.

    Fork

    In Unix, all processes are created with the system call fork(). This is without exception. What fork() does is the following:

    It creates a new process which is a copy of the calling process. That means that it copies the caller's memory (code, globals, heap and stack), registers, and open files. The calling process returns from fork() with the pid of the newly created process (which is called the "child" process. The calling process is called the "parent" process). The newly created process, as it is a duplicate of the parent, also returns from the fork() call (this is because it is a duplicate -- it has the same memory and registers, and thus the same stack pointer, frame pointer, and program counter, and thus has to return from the fork() call). It returns with a value of zero. This is how you know what process you're in when fork() returns.

    Look at simpfork.c:

    main()
    {
      int i;
    
      printf("simpfork: pid = %d\n", getpid());
      i = fork();
      printf("Did a fork.  It returned %d.  getpid = %d.  getppid = %d\n",
        i, getpid(), getppid());
    }
    
    When it is run, the following happens:
    UNIX> simpfork
    simpfork: pid = 914
    Did a fork.  It returned 915.  getpid = 914.  getppid = 381
    Did a fork.  It returned 0.  getpid = 915.  getppid = 914
    UNIX>
    

    So, what is going on? When simpfork is executed, it has a pid of 914. Next it calls fork() creating a duplicate process with a pid of 915. The parent gains control of the CPU, and returns from fork() with a return value of the 915 -- this is the child's pid. It prints out this return value, its own pid, and the pid of csh, which is still 381. Then it exits. Next, the child gets the CPU and returns from fork() with a value of 0. It prints out that value, its pid, and the pid of the parent.

    Note, there is no guarantee which process gains control of the CPU first after a fork(). It could be the parent, and it could be the child. When I executed simpfork a second time, the child got control first:

    UNIX> simpfork 
    simpfork: pid = 928
    Did a fork.  It returned 0.  getpid = 929.  getppid = 928
    Did a fork.  It returned 929.  getpid = 928.  getppid = 381
    UNIX>
    
    (on some machines, it does appear that the child always gets control first, but you should not rely on such a fact when writing code).
    Now, look at simpfork2.c. It calls fork(), and has the parent exit immediately. The child calls sleep(5), which makes it sleep for 5 seconds, and then it prints out its pid, and its parent's pid. What is subtle about this? Well, by the time the child wakes up from sleeping, the parent has exited, and its process is no longer. Thus, getppid() can't return the old value of the parent's pid, as that pid is no longer valid. What Unix does in these cases is transfer the parentage of the child's program. Specifically, when the parent of a program exits, the init program (pid 1) becomes its parent. Thus, when the child prints out its parent after sleeping, it prints out pid 1:
    UNIX> simpfork2
    Child.  getpid() = 1301, getppid() = 1300
    Parent exiting now
    UNIX> After sleeping.  getpid() = 1301, getppid() = 1
    
    Note that the "UNIX>" prompt returns once the parent returns, even though the child is still running. This is because csh waits only for the parent to complete, not for any other processes.
    Simpfork3.c is a simple program to show that the parent's address space is indeed copied to the child during the fork. After the fork, each process has memory locations for j and K. Thus, when the child changes j to 201 and K to 301, it only affects its values of j and K, and not the parent's:
    UNIX> simpfork3
    Before forking: j = 200, K = 300
    After forking, child: j = 201, K = 301
    After forking, parent: j = 200, K = 300
    UNIX>
    
    Interestingly, if we redirect the output of simpfork3 to a file, we see the following behavior:
    UNIX> simpfork3 > output
    UNIX> cat output
    Before forking: j = 200, K = 300
    After forking, child: j = 201, K = 301
    Before forking: j = 200, K = 300
    After forking, parent: j = 200, K = 300
    UNIX>
    
    This is explained in the book, and I'll explain it here. When redirecting output to a terminal, stdout is buffered line by line -- that is, once you do a putchar('\n') or equivalent, the buffer is written to standard output with a "write(1, ...)". However, when stdout is redirected to a file, the stdio library buffers on a coarser scale -- not writing until some large buffer (probably 4K or 8K characters) is full. Thus, at the time of the fork() call, the "Before forking:" string has not been written to fd=1. Instead, it has been buffered in the standard I/O library. That buffer is part of simpfork3's address space, and is thus copied to the child process when fork() is called. Thus, when the bytes are finally flushed from the buffer, the "Before forking: ..." string is written to the file twice. This is an important thing to realize. It looks strange but has a logical explanation.
    Simpfork4.c shows how open files are shared across fork() calls. Specifically, all open file descriptors are duplicated. Thus, the file opened for writing in simpfork4.c is open in both the parent and child processes. Note that the open file is the same -- when one writes to the end of the file, the seek pointer is changed in both the parent and the child. This is because both processes point to the same open file structure in the operating system. If the two did not share the same open file pointer, then the parent's write would overwrite the child's write.
    UNIX> simpfork4
    UNIX> cat tmpfile
    Before forking
    Child: After forking: Seek pointer = 15
    Parent: After forking: Seek pointer = 55
    UNIX>
    

    This is also explained in the book.

    I will go over more on sharing file descriptors in the dup lecture.