Processes
A process is a running program with its own address space. POSIX lets a program create another one with fork, replace itself with exec, and wait for a child with waitpid. The three calls are the foundation of every shell and server in this part, and they all work in this container.
fork duplicates the process
fork returns twice: zero in the child and the child's process id in the parent, so one if separates the two programs. Both copies continue after the call with the same variables, but their own copies of them. The parent waits so the output order is stable.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
printf("child: pid %d\n", (int) getpid());
return 0;
}
printf("parent: child %d\n", (int) pid);
waitpid(pid, NULL, 0);
return 0;
}
gcc -Wall -Wextra fork.c -o fork
./fork
parent: child 2361
child: pid 2361
The numbers are process ids and change on every run; the relationship does not: the parent prints the child's id and the child prints the same number from getpid. Without the waitpid the two lines could appear in either order.
Waiting and the exit status
waitpid fills an int with the child's status, but the value is not the exit code: it packs the exit code, the signal and a flag. WIFEXITED says whether the child ended normally, and WEXITSTATUS extracts the code. A child that calls _exit returns immediately.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
_exit(7);
}
int status = 0;
if (waitpid(pid, &status, 0) != pid) {
return 1;
}
printf("exited: %d\n", WIFEXITED(status) != 0);
printf("code: %d\n", WEXITSTATUS(status));
return 0;
}
gcc -Wall -Wextra status.c -o status
./status
exited: 1
code: 7
The child never returns from main, yet the code is 7 because _exit takes the status directly. The distinction between exit and _exit is the flush of the standard streams: the child in a fork should use the latter to avoid writing the parent's buffered output twice.
exec replaces the program
exec loads a new program into the current process and never returns on success. The execlp form searches PATH and takes the arguments one by one, ending with a null pointer. A child that execs keeps its process id and its open file descriptors.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
execlp("echo", "echo", "hello from exec", (char *) NULL);
perror("exec");
_exit(1);
}
int status = 0;
waitpid(pid, &status, 0);
printf("child status: %d\n", WEXITSTATUS(status));
return 0;
}
gcc -Wall -Wextra exec.c -o exec
./exec
hello from exec
child status: 0
The greeting comes from /bin/echo, not from this program, and the status is zero because echo succeeded. The (char *) NULL cast is the usual way to satisfy the variadic parameter list; the null pointer marks the end of the arguments.
Forgetting the exit after exec
When exec fails, it returns -1 and the child continues with the code that follows. If that code is the parent's path, both processes print and both wait, and the output appears twice. The failed call is reported only by the line that follows it.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0) {
execlp("no-such-program", "no-such-program", (char *) NULL);
printf("child continues after failed exec\n");
}
waitpid(pid, NULL, 0);
printf("parent done\n");
return 0;
}
gcc -Wall -Wextra noexit.c -o noexit
./noexit
child continues after failed exec
parent done
parent done
The child printed the parent's line because it never left the function. The fix is the _exit(1) from the exec section, placed right after execlp, so a failed exec ends the child instead of merging it with the parent. The child's status is then 1, which the parent can report.
Exercises
- Fork two children and wait for both, printing each exit status.
- Run /bin/ls with an argument through execlp and check the status in the parent.
- Make a child exit with status 3 and show that WEXITSTATUS returns 3 while the raw status is 768.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
forkreturn in the parent and in the child?Answer
The child id in the parent and zero in the child
The parent receives the new process id, the child receives zero, and an error returns -1 in the parent.
-
Why is the raw status from
waitpidnot the exit code?Answer
It packs the exit code, signal and a flag
WIFEXITEDandWEXITSTATUSextract the parts, because the integer holds more than the return value. -
What happens to the program after a successful
exec?Answer
It never returns to the old code
execreplaces the process image, so the call returns only when it fails and setserrno. -
Why should a forked child use
_exitrather thanexit?Answer
It avoids flushing the parent's buffers twice
exitflushes the standard streams, and the child inherited the parent's buffered data, which would then be written again. -
What does this program print?
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(void){int f[2];pipe(f);pid_t p=fork();if(p==0){int id=(int)getpid();write(f[1],&id,sizeof id);_exit(0);}close(f[1]);int child=0;read(f[0],&child,sizeof child);printf("same: %d\n",child==(int)p);waitpid(p,0,0);return 0;}Answer
same: 1
The child sends its pid through the pipe, and it equals the value
forkreturned to the parent. -
What does this program print?
#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> int main(void){pid_t p=fork();if(p==0)_exit(7);int s=0;waitpid(p,&s,0);printf("%d %d\n",WIFEXITED(s)!=0,WEXITSTATUS(s));return 0;}Answer
1 7
The child ended normally, so
WIFEXITEDis true and the extracted code is 7. -
What does this program print?
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(void){pid_t p=fork();if(p==0){execlp("echo","echo","hello from exec",(char*)NULL);_exit(1);}int s=0;waitpid(p,&s,0);printf("child status: %d\n",WEXITSTATUS(s));return 0;}Answer
hello from exec / child status: 0
/bin/echowrites the greeting and exits successfully, and the parent prints the zero status. -
What does this program print?
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(void){pid_t p=fork();if(p==0){execlp("no-such-program","no-such-program",(char*)NULL);printf("child continues after failed exec\n");}waitpid(p,0,0);printf("parent done\n");return 0;}Answer
child continues after failed exec / parent done / parent done
The failed child falls through to the parent's path, so both processes print the final line.
-
When
execfails and the child does not call_exit, what does the child do?Answer
It continues with the code after
execexecreturns -1 on failure, so without an_exitthe child runs the following code. -
What does
waitpid(pid, NULL, 0)do in the parent?Answer
It waits for that child to finish
The call blocks until the named child changes state, which keeps the output order stable.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev