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

  1. Fork two children and wait for both, printing each exit status.
  2. Run /bin/ls with an argument through execlp and check the status in the parent.
  3. 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.

  1. What does fork return 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.

  2. Why is the raw status from waitpid not the exit code?

    Answer

    It packs the exit code, signal and a flag

    WIFEXITED and WEXITSTATUS extract the parts, because the integer holds more than the return value.

  3. What happens to the program after a successful exec?

    Answer

    It never returns to the old code

    exec replaces the process image, so the call returns only when it fails and sets errno.

  4. Why should a forked child use _exit rather than exit?

    Answer

    It avoids flushing the parent's buffers twice

    exit flushes the standard streams, and the child inherited the parent's buffered data, which would then be written again.

  5. 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 fork returned to the parent.

  6. 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 WIFEXITED is true and the extracted code is 7.

  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/echo writes the greeting and exits successfully, and the parent prints the zero status.

  8. 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.

  9. When exec fails and the child does not call _exit, what does the child do?

    Answer

    It continues with the code after exec

    exec returns -1 on failure, so without an _exit the child runs the following code.

  10. 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

Banner image