Lec. 14: exec()/fork()/wait() cycles for Process Management
Table of Contents
1 Executing a Programing
In the last lesson, we briefly discussed how a program loads into a
process. We continue that discussion now by overviewing the exec
family of system calls.
Recall that an exec
call will load a new program into the process
and replace the current running program with the one specified. For
example, consider this program, which will execute the ls -l
command
in the current directory:
There are three main versions of exec
which we will focus on:
execv(char * path, char * argv[])
: given the path to the program and an argument array, load and execute the programexecvp(char * file, char * argv[])
: given a file(name) of the program and an argument array, find the file in the environmentPATH
and execute the programexecvpe(char * file, char * argv[], char * envp[])
given a file(name), an argument array, and the enviroment settings, within the enviroment, search thePATH
for the program named file and execute with the arguments.
Each version of execute provides slightly different functionality. For
this discussion, we will focus on execv
and execvp
; we will
discuss execvpe
latter in the semester.
1.1 Using execv
and execvp
The primary difference between execv
and execvp
is that with
execv
you have to provide the full path to the binary file (i.e.,
the program). The argv[]
array is the same otherwise. For example:
/*execv_ls-l.c*/ #include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char * argv[]){ //argv array for: /bin/ls -l char * ls_args[] = { "/bin/ls" , "-l", NULL}; // ^ // all argv arrays must be ___| // NULL terminated //execute the program execv( ls_args[0], ls_args); // ^ ^ // | | // Name of program argv array // is ls_args[0] for ls_args //only get here on error perror("execv"); return 2; }
aviv@saddleback: demo $ ./execv_ls-l total 120 -rwxr-x--- 1 aviv scs 9890 Feb 24 14:13 exec_other -rw-r----- 1 aviv scs 151 Feb 24 11:43 exec_other.c -rwxr-x--- 1 aviv scs 9977 Feb 24 14:13 execv_ls-l -rw-r----- 1 aviv scs 559 Feb 24 11:42 execv_ls-l.c -rwxr-x--- 1 aviv scs 9979 Feb 24 14:13 execvp_ls-l -rw-r----- 1 aviv scs 360 Feb 24 11:59 execvp_ls-l.c -rw-r----- 1 aviv scs 559 Feb 24 11:58 execvp_ls-l.c~ -rwxr-x--- 1 aviv scs 10023 Feb 24 14:13 first_fork -rw-r----- 1 aviv scs 532 Feb 23 08:06 first_fork.c -rwxr-x--- 1 aviv scs 10345 Feb 24 14:13 fork_exec_wait -rw-r----- 1 aviv scs 1158 Feb 23 08:06 fork_exec_wait.c -rwxr-x--- 1 aviv scs 10278 Feb 24 14:13 get_exitstatus -rw-r----- 1 aviv scs 1379 Feb 23 08:06 get_exitstatus.c -rwxr-x--- 1 aviv scs 9985 Feb 24 14:13 get_pid_ppid -rw-r----- 1 aviv scs 294 Feb 23 08:06 get_pid_ppid.c -rw-r----- 1 aviv scs 99 Feb 23 08:06 Makefile
With execvp
, you do not need to specify the full path because
execvp
will search the local environment variable PATH
for the
executable. Recall, that this is how the shell command which
works:
aviv@saddleback: demo $ which ls /bin/ls
which
will find the name of the command along the path:
aviv@saddleback: demo $ echo $PATH /home/scs/aviv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
In the case for ls
this occurs in /bin
. Using execvp
will
perform this look up for you, and so can simplify the code some:
aviv@saddleback: demo $ cat execvp_ls-l.c #include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char * argv[]){ //argv array for: ls -l char * ls_args[] = { "ls" , "-l", NULL}; // ^ // use the name ls // rather than the // path to /bin/ls execvp( ls_args[0], ls_args); //only get here on error perror("execv"); return 2; }
aviv@saddleback: demo $ ./execvp_ls-l total 120 -rwxr-x--- 1 aviv scs 9890 Feb 24 14:13 exec_other -rw-r----- 1 aviv scs 151 Feb 24 11:43 exec_other.c -rwxr-x--- 1 aviv scs 9977 Feb 24 14:13 execv_ls-l -rw-r----- 1 aviv scs 559 Feb 24 11:42 execv_ls-l.c -rwxr-x--- 1 aviv scs 9979 Feb 24 14:13 execvp_ls-l -rw-r----- 1 aviv scs 360 Feb 24 11:59 execvp_ls-l.c -rw-r----- 1 aviv scs 559 Feb 24 11:58 execvp_ls-l.c~ -rwxr-x--- 1 aviv scs 10023 Feb 24 14:13 first_fork -rw-r----- 1 aviv scs 532 Feb 23 08:06 first_fork.c -rwxr-x--- 1 aviv scs 10345 Feb 24 14:13 fork_exec_wait -rw-r----- 1 aviv scs 1158 Feb 23 08:06 fork_exec_wait.c -rwxr-x--- 1 aviv scs 10278 Feb 24 14:13 get_exitstatus -rw-r----- 1 aviv scs 1379 Feb 23 08:06 get_exitstatus.c -rwxr-x--- 1 aviv scs 9985 Feb 24 14:13 get_pid_ppid -rw-r----- 1 aviv scs 294 Feb 23 08:06 get_pid_ppid.c -rw-r----- 1 aviv scs 99 Feb 23 08:06 Makefile
You might be wondering: why use execv
at all when you have execvp
?
There are a few good reasons, but the most relevant is for
security. The PATH
can be changed by the user to circumvent which
programs are found during lookup. For example, what happens if there
was another program called ls
along the path, but this time that
program removed the whole file system. execvp
would call the wrong
ls … and boom. execv
forces issues and ensures that the whole path
to the executable is provided.
1.2 The argv[]
argument to execv
and execvp
The last item to consider in the exec
calls is the argv
array. This is the same as the argv
array argument to main;
essentially, when you call exec
you are calling that programs main
function.
Just like in main, the argv array must be NULL terminated. So when we do this:
char * ls_args[] = { "ls" , "-l", NULL};
We are setting up the argv array like so:
.-----. ls_args -> | .--+--> "/bin/ls" |-----| | .--+--> "-l" |-----| | .--+--> NULL '-----'
Because the argv array for exec is the same as main, it becomes quite trivially to write a program that just executes another program as specified on the command line. For example:
aviv@saddleback: demo $ ./exec_other ls -l total 120 -rwxr-x--- 1 aviv scs 9890 Feb 24 14:13 exec_other -rw-r----- 1 aviv scs 151 Feb 24 11:43 exec_other.c -rwxr-x--- 1 aviv scs 9977 Feb 24 14:13 execv_ls-l -rw-r----- 1 aviv scs 559 Feb 24 11:42 execv_ls-l.c -rwxr-x--- 1 aviv scs 9979 Feb 24 14:13 execvp_ls-l -rw-r----- 1 aviv scs 360 Feb 24 11:59 execvp_ls-l.c -rw-r----- 1 aviv scs 559 Feb 24 11:58 execvp_ls-l.c~ -rwxr-x--- 1 aviv scs 10023 Feb 24 14:13 first_fork -rw-r----- 1 aviv scs 532 Feb 23 08:06 first_fork.c -rwxr-x--- 1 aviv scs 10345 Feb 24 14:13 fork_exec_wait -rw-r----- 1 aviv scs 1158 Feb 23 08:06 fork_exec_wait.c -rwxr-x--- 1 aviv scs 10278 Feb 24 14:13 get_exitstatus -rw-r----- 1 aviv scs 1379 Feb 23 08:06 get_exitstatus.c -rwxr-x--- 1 aviv scs 9985 Feb 24 14:13 get_pid_ppid -rw-r----- 1 aviv scs 294 Feb 23 08:06 get_pid_ppid.c -rw-r----- 1 aviv scs 99 Feb 23 08:06 Makefile aviv@saddleback: demo $ ./exec_other cat exec_oth exec_other exec_other.c aviv@saddleback: demo $ ./exec_other cat exec_other.c #include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(int argce, char * argv[]){ execvp( (argv+1)[0], argv+1); perror("execvp"); }
As you can see in the program (which used cat to output itself!) we
are using pointer manipulation to set up the argv
array. At the
start, the argv is like:
.-----. argv -> | .--+--> "./exec_other" |-----| | .--+--> "ls" |-----| | .--+--> "-l" |-----| | .--+--> NULL '-----'
After point manipulation:
.-----. | .--+--> "./exec_other" |-----| argv+1 -> | .--+--> "ls" |-----| | .--+--> "-l" |-----| | .--+--> NULL '-----'
Which is a valid argv
array for executing ls
.
2 Creating a new Process
So far, we've only loaded programs and executed them as an already
running process. This is not creating a new process, and for that we
need a new system call. The fork()
system call will duplicate the
calling process and create a new process with a new process
identifier.
2.1 fork()
With the exception of two O.S. processes, the kernel and init process,
all process are spawned from another process. The procedure of
creating a new process is called forking: An exact copy of the
process, memory values and open resources, is produced. The original
process that forked, is called the parent, while the newly created,
duplicate process is called the child. Let's look at an example of a
process forking using the fork()
system call:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t c_pid; c_pid = fork(); //duplicate if( c_pid == 0 ){ //child: The return of fork() is zero printf("Child: I'm the child: %d\n", c_pid); }else if (c_pid > 0){ //parent: The return of fork() is the process of id of the child printf("Parent: I'm the parent: %d\n", c_pid); }else{ //error: The return of fork() is negative perror("fork failed"); _exit(2); //exit failure, hard } return 0; //success }
The fork()
system call is unlike any other function call you've seen
so far. It returns twice, once in the parent and once in child, and it
returns different values in the parent and the child.
To follow the logic, you first need to realize that once fork()
is
called, the Operating System is creating a whole new process which is
an exact copy of the original process. At this point, fork()
still
hasen't returned because the O.S. is context switched in, and now it
must return from fork()
twice, once in the child process and once in
the parent, where execution in both process can continue.
2.2 Process identifiers or pid
Every process has a unique identifier, the process identifier or
pid
. This value is assigned by the operating system when the process
is created and is a 2-byte number (or a short). There is a special
typedef
for the process identifier, pid_t
, which we will use.
In the above sample code, after the call to fork()
, the parent's return value
from fork()
is the process id of the newly created child
process. The child, however, has a return value of 0. On error,
fork()
, returns -1. Then you should bail with _exit()
because
something terrible happened.
One nice way to see a visual of the parent process relationship is
using the bash command pstree
:
#> pstree -ah init ├─NetworkManager │ ├─dhclient -d -4 -sf /usr/lib/NetworkManager/nm-dhcp-client.action -pf /var/run/sendsigs.omit.d/network-manager.dhclient-eth0.pid -lf... │ └─2*[{NetworkManager}] ├─accounts-daemon │ └─{accounts-daemon} (...)
At the top is the init
process, which is the parent of all
proces. Somewhere down tree is my login shell
(...) ├─sshd -D │ └─sshd │ └─sshd │ └─bash │ ├─emacs get_exitstatus.c │ ├─emacs foursons.c │ ├─emacs Makefile │ ├─emacs get_exitstatus.c │ ├─emacs fork_exec_wait.c │ ├─emacs mail_reports.py │ └─pstree -ah (...)
And you can see that the process of getting to a bash
shell via ssh
requires a number of forks and child process.
2.3 Retrieving Process Identifiers: getpid()
and getppid()
With fork()
, the parent can learn the process id of the child, but
the child doesn't know its own process id (or pid) after the fork
nor does it know its parents process id. For that matter, the parent
doesn't know its own process id either. There are two system calls to
retrieve this information:
//retrieve the current process id pid_t getpid(void); //retrieve the parent's process id pid_t getppid(void);
There is no way for a process to directly retrieve its child pid
because any process may have multiple children. Instead, a process must
maintain that information directly through the values returned
from a fork()
. Here is a sample program that prints the current
process id and the parent's process id.
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t pid, ppid; //get the process'es pid pid = getpid(); //get the parrent of this process'es pid ppid = getppid(); printf("My pid is: %d\n",pid); printf("My parent's pid is %d\n", ppid); return 0; }
If we run this program a bunch of times, we will see output like this:
#> ./get_pid_ppid My pid is: 14307 My parent's pid is 13790 #> ./get_pid_ppid My pid is: 14308 My parent's pid is 13790 #> ./get_pid_ppid My pid is: 14309 My parent's pid is 13790
Every time the program runs, it has a different process id (or
pid). Every process must have a unique pid, and the O.S. applies a
policy for reusing process id's as processes terminate. But, the
parent's pid is the same. If you think for a second, this makes sense:
What's the parent of the program? The shell! We can see this by
echo
'ing $$
, which is special bash variable that stores the pid of
the shell:
#> echo $$ 13790
Whenever you execute a program on the shell, what's really going on is
the shell is forking, and the new child is exec
'ing the new
program. One thing to consider, though, is that when a process forks,
the parent and the child continue executing in parallel: Why doesn't
the shell come back immediately and ask the user to enter a new
command? The shell instead waits for the child to finish process
before prompting again, and there is a system call called wait()
to
just do that.
3 Waiting on a child with wait()
The wait()
system call is used by a parent process to wait for the
status of the child to change. A status change can occur for a number
of reasons, the program stopped or continued, but we'll only concern
ourselves with the most common status change: the program terminated
or exited. (We will discuss stopped and continued in later lessons.)
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
Once the parent calls wait()
, it will block until a child changes
state. In essence, it is waiting on its children to terminate. This is
described as a blocking function because it blocks and does not
continue until an event is complete.
Once it returns, wait()
will returns the pid of the child process
that terminated (or -1 if the process has no children), and wait()
takes an integer pointer as an argument. At that memory address, it
will set the termination status of the child process. As mentioned
in the previous lesson, part of the termination status is the exit
status, but it also contains other information for how a program
terminated, like if it had a SEGFAULT
.
3.1 Checking the Status of children
To learn about the exit status of a program we can use the macros from
sys/wait.h
which check the termination status and return the exit
status. From the main page:
WIFEXITED(status) returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main(). WEXITSTATUS(status) returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should only be employed if WIFEXITED returned true.
There are other checks of the termination status, and refer to the manual page for more detail. Below is some example code for checking the exit status of forked child. You can see that the child delays its exit by 2 seconds with a call to sleep.
/*get_exitstatus.c*/ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main(){ pid_t c_pid, pid; int status; c_pid = fork(); //duplicate if( c_pid == 0 ){ //child pid = getpid(); printf("Child: %d: I'm the child\n", pid, c_pid); printf("Child: sleeping for 2-seconds, then exiting with status 12\n"); //sleep for 2 seconds sleep(2); //exit with statys 12 exit(12); }else if (c_pid > 0){ //parent //waiting for child to terminate pid = wait(&status); if ( WIFEXITED(status) ){ printf("Parent: Child exited with status: %d\n", WEXITSTATUS(status)); } }else{ //error: The return of fork() is negative perror("fork failed"); _exit(2); //exit failure, hard } return 0; //success }
4 Fork/Exec/Wait Cycle
We now have all the parts to write a program that will execute another program and wait for that program to finish. This reminds me of another program we've already used in this class… the shell, but you'll get to that later in the lab.
For now, consider the example code below which executes ls
on the
/bin
directory:
/*fork_exec_wait.c*/ #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char * argv[]){ //arguments for ls, will run: ls -l /bin char * ls_args[3] = { "ls", "-l", NULL} ; pid_t c_pid, pid; int status; c_pid = fork(); if (c_pid == 0){ /* CHILD */ printf("Child: executing ls\n"); //execute ls execvp( ls_args[0], ls_args); //only get here if exec failed perror("execve failed"); }else if (c_pid > 0){ /* PARENT */ if( (pid = wait(&status)) < 0){ perror("wait"); _exit(1); } printf("Parent: finished\n"); }else{ perror("fork failed"); _exit(1); } return 0; //return success }
And the execution:
aviv@saddleback: demo $ ./fork_exec_wait Child: executing ls total 5120 -rwxr-xr-x 2 root wheel 18480 Sep 9 18:44 [ -r-xr-xr-x 1 root wheel 628736 Sep 26 22:03 bash -rwxr-xr-x 1 root wheel 19552 Sep 9 18:57 cat -rwxr-xr-x 1 root wheel 30112 Sep 9 18:50 chmod -rwxr-xr-x 1 root wheel 24768 Sep 9 18:49 cp -rwxr-xr-x 2 root wheel 370096 Sep 9 18:40 csh -rwxr-xr-x 1 root wheel 24400 Sep 9 18:44 date -rwxr-xr-x 1 root wheel 27888 Sep 9 18:50 dd -rwxr-xr-x 1 root wheel 23472 Sep 9 18:49 df -r-xr-xr-x 1 root wheel 14176 Sep 9 19:27 domainname -rwxr-xr-x 1 root wheel 14048 Sep 9 18:44 echo -rwxr-xr-x 1 root wheel 49904 Sep 9 18:57 ed -rwxr-xr-x 1 root wheel 19008 Sep 9 18:44 expr -rwxr-xr-x 1 root wheel 14208 Sep 9 18:44 hostname -rwxr-xr-x 1 root wheel 14560 Sep 9 18:44 kill -r-xr-xr-x 1 root wheel 1394560 Sep 9 19:59 ksh -rwxr-xr-x 1 root wheel 77728 Sep 9 19:32 launchctl -rwxr-xr-x 2 root wheel 14944 Sep 9 18:49 link -rwxr-xr-x 2 root wheel 14944 Sep 9 18:49 ln -rwxr-xr-x 1 root wheel 34640 Sep 9 18:49 ls -rwxr-xr-x 1 root wheel 14512 Sep 9 18:50 mkdir -rwxr-xr-x 1 root wheel 20160 Sep 9 18:49 mv -rwxr-xr-x 1 root wheel 106816 Sep 9 18:49 pax -rwsr-xr-x 1 root wheel 46688 Sep 9 18:59 ps -rwxr-xr-x 1 root wheel 14208 Sep 9 18:44 pwd -r-sr-xr-x 1 root wheel 25216 Sep 9 19:27 rcp -rwxr-xr-x 2 root wheel 19760 Sep 9 18:49 rm -rwxr-xr-x 1 root wheel 14080 Sep 9 18:49 rmdir -r-xr-xr-x 1 root wheel 628800 Sep 26 22:03 sh -rwxr-xr-x 1 root wheel 14016 Sep 9 18:44 sleep -rwxr-xr-x 1 root wheel 28064 Sep 9 18:59 stty -rwxr-xr-x 1 root wheel 34224 Sep 9 21:59 sync -rwxr-xr-x 2 root wheel 370096 Sep 9 18:40 tcsh -rwxr-xr-x 2 root wheel 18480 Sep 9 18:44 test -rwxr-xr-x 2 root wheel 19760 Sep 9 18:49 unlink -rwxr-xr-x 1 root wheel 14112 Sep 9 19:32 wait4path -rwxr-xr-x 1 root wheel 551232 Sep 9 19:19 zsh Parent: finished
The parent first forks a child process. In the child process, the
execution is replaced by ls
which prints the output. Meanwhile, the
parent wait's for the execution to complete before continuing.
Imagine now this process occurring in a loop, and instead of running
ls
, the user provides the program that should run. That's a shell,
and that's what you will be doing in the next lab.