pete > courses > CS 315 Fall 26 > lecture 11: fork, exec, and wait


Lecture 11: fork, exec, and wait

Goals


what’s wrong with this program: write-to-fd3.c

for starters, it’s weird that the file descriptors for write(2) and close(2) are hard-coded

in the past, in these places, we’ve used file descriptors returned by open(2)

now, we have discussed three special file descriptors available to most processes by default:

but none of these are 3, and no file is ever opened, so this seems doomed


aside: getpid(2) is a system call that tells a process its own numerical id

recall our discussion of the ps program, which shows all running processes

and that each process is identified by a number

this is the process id; getpid(2) is how a process can learn its id


back to the problem, we can confirm our hypothesis with strace:

$ strace -o strace.out ./write-to-fd3

the last few lines of strace.out:

getpid()                                = 20418
write(3, "Hello again from process 20418\n", 31) = -1 EBADF (Bad file descriptor)
close(3)                                = -1 EBADF (Bad file descriptor)

we see the getpid call work

then both the write and close fail

strace helpfully tells us the value of errno at the time of their respective failures so we can gain insight into their cause

as we expected, file descriptor 3 is invalid


looking further up on the file, we see lots of syscalls that now make more sense: opening libraries, mapping libraries, setting memory permissions on these mappings (mprotect), etc

but look at the very first line:

execve("./write-to-fd3", ["./write-to-fd3"], 0x7ffce2abae60 /* 37 vars */) = 0

that’s a new one

to the manpage!

execve() executes the program pointed to by filename. This causes the program
that is currently being run by the calling process to be replaced with a new
program, with newly initialized stack, heap, and (initialized and
uninitialized) data segments.

what on earth does this mean?

"executes the program pointed to by filename" makes sense, but what’s this talk of replacement?

it means exactly what it seems to imply: the currently-running program is replaced by a new one

the old instructions, stack, heap, and other data are completely replaced by a brand new collection of instructions, an empty stack, heap, etc

and the program counter is set to the beginning of the new program’s main function


in practice, this syscall is almost never used directly

instead, we use a variety of wrapper functions from libc that provide different ways of specifying the parameters

they are all described in the exec(3) manpage (and I will refer to them collectively as "exec" even though none of them is called precisely that)

the primary differences are how the command-line arguments are specified to the program (a "l"ist or a "v"ector) and whether the program to be executed is searched for in the "p"ath

explanation by example shortly


but first, recall the argv array that contains the command-line arguments to the program

argv[0] is the name of the program itself

argv[1] is the first argument

argv[2] is the second argument

and so on

here’s a program that demonstrates it: print-args.c

(it’s the same as the argv.c program from lecture 06, with the addition that it prints its pid, which will be useful to us later today)

this collection of arguments is one of the three bits of information that needs to be communicated to exec to do its job

the other parts are the actual program file to execute and the environment (the latter of which we will defer for a few lectures)


so with that in mind, here’s the first example:

exec1.c

the first parameter to execl is the name of the program to execute

the second parameter is the value of argv[0]

the third parameter signifies the end of the argument list

note that the first two parameters are the same

exec uses the first parameter to find the program to execute

exec uses the second parameter to set up argv (and is otherwise ignored by exec)

by convention they match, but they don’t have to

the first one needs to be the program to execute


this is going to fail, however

let’s use strace to see how/why:

$ strace -o strace.out ./exec1
$ tail -n 3 strace.out
execve("ls", ["ls"], 0x7ffc29978018 /* 37 vars */) = -1 ENOENT (No such file or directory)
exit_group(0)                           = ?
+++ exited with 0 +++

because it can’t find the program ls


if we give it the full path to find the program "ls", it works, as demonstrated by this program: exec2.c

but clearly we do not give the full, absolute path to every program we want to run (eg, gcc, gdb, valgrind), so how does this work?


when you ask Linux to run a program (eg, "ls") it looks in a collection of specific directories to find the compiled "ls" program

this collection of directories is called the PATH

it is an environment variable (more later)

and we can see its value from the shell:

$ echo $PATH
/home/pete/.cabal/bin:/home/pete/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

it’s a colon-separated list of directories

when Linux is instructed to consult the PATH, it searches each directory (in order) to see if it contains a file with the given name

if it finds such a file, it executes it


the "p" variants of exec(3) consult the PATH, so this program works: exec3.c


as the manpage says, certain aspects of the old program disappear when the new program is executed: notably, pretty much everything in memory (instructions, stack, heap, etc)

some things are not replaced, however; much of this stuff falls under the category of "kernel state"—ie, the information about the process that the kernel keeps track of

for instance, process id and open file descriptors

that’s right: when a running process calls exec, the new, replacement program has the same process id

additionally, the new program has access to all the same file descriptors of the previous


to demonstrate this, we’re going to exec a program we wrote ourselves

specifically, the print-args.c program from earlier that prints its arguments and pid

here’s the program we’ll use to exec it: simple-exec.c

if what I said earlier is true, this program should print out its own process id, then a message saying it’s about to exec, and then print-args should take over, and we should NEVER see the result of the final printf because it doesn’t exist after exec does its thing

instead, we should see the output produced by print-args

and in that case, given what I’ve claimed about exec, we should see the same pid

$ ./simple-exec
This is process 22211
exec'ing ./print-args...
This is process 22211
arg 0 is "./print-args"
arg 1 is "hello"
arg 2 is "there"

confirmed.


and now to the idea that open file descriptors are retained

here’s a program that opens a file and then execs: less-simple-exec.c

and here’s our program from earlier that writes to a (hopefully-already-open) file descriptor: write-to-fd3.c

if what I claim is true, output-file should contain a line from less-simple-exec and another line from write-to-fd3

$ ./less-simple-exec
$ cat output-file
Hello from process 22259
Hello again from process 22259

again, confirmed.

and note again that the pids match


exec is the only way we run new programs on UNIX

it is often said that exec is the only function that never returns

because one program calls it but then that program is replaced with the new program before it ever appears to finish

conceptually, execution "continues" at the beginning of the main function in the new program, but since there is no state carried over, "continuation" isn’t a helpful way to think about it

but that all begs the question: how can we ever have more than one process?

enter fork(2):

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.

this one is weirder than exec

I said earlier that exec is called but never returns

well fork is the only function that is called once and returns twice

what on earth does this mean?

it means that it returns in both the parent and the child; in the child it returns the value zero and in the parent it returns the pid of the new child

(as the manpage says, the original process is called the parent and the new process is called the child)

to make sense of this, consider the situation before fork: we have a single process, and its program counter (the register that holds the address of the instruction being executed) contains the address of the "call" instruction that calls fork

when the call instruction is run, the return address is saved in the stack

then, inside fork, the process is duplicated and we now have a child with the exact same state as the parent—including the return address

so both the original parent and the new child will return to the same address

meanwhile, the internals of fork have manipulated the stack of both parent and child such that the return value of the parent is the pid of the child, and the return value of the child is zero

thus, each process can tell whether it is the parent or the child (which is often important, as we shall see shortly)

this is what is meant by fork being called once and returning twice


let’s see it in action: fork1.c

this is the typical pattern: call fork and then check whether it returned zero or not, thus we can cause the parent and child to do different things

so here’s how the program is going to execute

we ask the shell to run the program fork1 and a single process is created, with the program counter set to the beginning of main

then this process calls fork, which creates a child

the next code run in the parent will be the if statement

and the next code run in the child will also be the if statement

in the child, the condition will be true and the first message will be printed

in the parent, the condition will be false and the second message will be printed

to wit:

$ ./fork1
I am the parent because fork returned 22932, which is the child's pid.
I am the child because fork returned 0.

to make the whole process-id arrangement more clear, here’s another program: fork2.c

in this program, both the parent and child call getpid to ask the kernel for their process id

$ ./fork2
I am the parent and my child's pid (fork returned) is 22965.
My (the parent) pid is 22964.
I am the child and fork returned 0.
Wait, no, I am the child and my pid is 22965.

note that the process id in the child (as returned by getpid) matches the value returned by fork in the parent


just like every program is run with the exec system call, so every process (except exactly one) is created using the fork system call

when the computer is turned on, the kernel is given control first and does its initialization song and dance

when that is done, the kernel creates out of thin air a single process, historically called init, with pid 1

init then forks and execs the program that prints the login prompt and processes your username and password

upon successfully logging in, that program forks (maybe) and execs (definitely) your shell

when you run startxfce4, the shell forks and execs the graphical environment

when you click the terminal button in the GUI, the GUI forks and execs the terminal program

when you click the web browser button in the GUI, the GUI forks and execs your web browser

and so on and so forth


in both the preceding examples, the parent ran first

we cannot depend on this being the case

that is, we must assume that scheduling (the kernel’s task of deciding which process gets to run on the processor) is non-deterministic

if we want one or the other process to run first, we have to make that happen ourselves

in the context of fork and exec, the primary way to do that is the wait syscall

when called, wait will… wait until any (single) child process exits, and then it will return information about that now-dead child process

if a child has already exited by the time the parent calls wait, then wait will return immediately

if there are many children (ie, a single process called fork a bunch of times) wait will gather the results of exactly one terminated child


example: fork-and-wait.c

corresponding output:

$ ./fork-and-wait
I am the parent and my child's pid is 1303706.
Parent is waiting for child to exit...
I am the child, my pid is 1303706.
I, the child just woke up.
My child, pid 1303706, exited with value 42.

here’s what happened:

the parent forked, printed out its two messages, and then started to wait

this means that the child gets its turn to run

which means it prints its message, sleeps for a second, and then exits with the value 42

upon the child exiting, the parent is woken up and prints the message showing the exit value


this exposes an interesting subtlety of process management

when a process exits, the kernel keeps some information about how it exited (eg, its exit value)

that information must be kept until the parent calls wait

when a child has exited but the parent has yet to call wait, the child is referred to as a zombie process (or, much less entertainingly, a defunct process)

calling wait to process that child termination is often referred to as reaping


let’s get crazy: interleaving.c

this program will fork three times, assign a different id (unrelated to pid) to each child, and then each child will print its id 10 times, sleeping a bit between each

the purpose of sleeping a bit is to let the kernel (re)schedule the competing processes, with the goal of showing that the scheduling is indeed non-deterministic

if we run it multiple times, we see a different output order each time

nondeterminism indeed

(it’s not actually non-deterministic, but the determinism is hidden deep inside the kernel and incorporates far more factors than we can reasonably account for, so it’s best to think of it as non-deterministic)


we also sometimes see something a bit weird near the end: it looks like the shell prompt is printed out before the children have finished their output

why would this be?

if we consider the code, it seems that the parent could exit before all of the children themselves exit

when the parent exits, the shell (which has itself called wait on the program) is notified and thinks it’s time to print the next prompt

the shell cannot know that the children are still running and producing output, which is why we see the strange ordering


another creepy vocabulary word

when a parent exits while its children are still running, those children are said to be orphans

which raises the question: when those children exit, who calls wait to gather their exit info?

the answer is when a parent exits, the kernel automatically re-parents the orphans to the init process (discussed above), whose primary job is to call wait on them

and yes, this task is indeed referred to as reaping zombie orphan children


we can prevent this unpleasant output ordering by having the parent wait: interleaving-with-wait.c

note that it must wait once for every child it forked

but now all child output shows up before the next shell prompt is printed

Last modified: