The fork() syscall
The fork() syscall is used to create a new process. The process created by fork() returns 0 to the child process,
and the child’s PID to the parent process. To the Operating System it looks like there are two copies of the program running,
and both are going to return from the fork() call.
The child process doesn’t run from main(),it comes to life as if it had called fork() itself.
The child has its own copy of the address space (stack, heap, fd, registers). Either the parent or the child can run first The CPU Scheduler decides which process to run first in a non-deterministic order. This non-determinism leads to different problems, especially in multi-threaded programs.
The wait() syscall
Used in the parent to wait for any child process. If the parent happens to run first it will immediately call wait();
this syscall won’t return until the child has run and exited. It returns the PID of the child process on success, -1 on failure.
If you have multiple children and need to wait for a specific one use waitpid(pid, &status, 0) with pid being the child’s PID
The exec() syscall
It is used when you want to run a program that is different from the calling program. The exec() family of functions replaces the current process with a new process; Given the name of an executable, and some arguments, exec() loads code and static data from that executable and overwrites its current code segment and current static data with it; the heap and stack and other parts of the memory space of the program are re-initialized.
Exercises
Exercise 1
Write a program that calls fork(). Before calling fork(), have the main process access a variable and set its value to something. What value is the variable in the child process? What happens to the variable when both the child and parent change the value of x?
#include <stdio.h>
#include <stdlib.h>
#include <unistrd.h>
int main(int argc, char *argv[]) {
printf("cli arg: %s\n", argv[1]);
argv[1] = "changed";
int rc = fork();
if(rc < 0) {
exit(1);
} else if (rc == 0) {
printf("child arg: %s\n", argv[1]);
argv[1] = "child"
printf("child changed arg: %s\n", argv[1]);
} else {
int wc = wait(NULL);
printf("parent arg: %s\n",argv[1]);
}
return 0;
}
The child process has its own copy of the address space (stack, heap, fd) => changes made to the address space of the child process have no effect on the address space of the parent
Exercise 2
Write a program that opens a file with the open() system call and then calls fork() to create a new process. Can both the child and the parent access the file descriptor returned by open()? What happens when they are writing to the file concurrently i.e at the same time?
#include <stdio.h>
#include <stdlib.h>
#include <unistrd.h>
#include <fcntl.h>
int main(int argc, char *argv[]) {
int fd = open("test.txt", 0_RDWR);
if (fd == -1) {
perror("open");
exit(1);
}
int rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
} else if (rc == 0) {
// child process
write(fd, "Hello, world from child!\n", 24);
close(fd);
} else {
// parent process
write(fd, "Hello, world from parent!\n", 25);
close(fd);
}
return 0;
}
The child gets a copy of the parent’s fd table. Each process has its own fd table; they’re two entries that point to the same open file description - Both use the same fd, they share the same file offset and see the same data.
When both processes have called close(fd)
- Refcount on the open file descriptor goes to 0. There was one
open()-> one kernel “open file description”. Afterfork()there are two fd (parent, child) pointing at it, so refcount = 2. Eachclose(fd)in parent then child drops the refcount by 1 - Kernel frees that open file description. The fd is no longer valid in either process. The kernel drops its reference to the inode, flushes any buffered data for that description and the file is no longer “open” by this program.
- The file on disk is unchanged
Exercise 3
Write a program using fork(). The child process should print “hello”; the parent process should print “bye”. You should try to ensure that the child process always prints first. Can you do this without calling wait() in the parent?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char *argv[]) {
sigset_t block_mask, wait_mask;
struct sigaction sa;
/* Block SIGUSR1 so it stays pending until we explicitly wait */
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGUSR1);
sigprocmask(SIG_BLOCK, &block_mask, NULL);
/* Safe disposition for SIGUSR1
* Default is terminate. We only need the signal to wake us from sigsuspend;
* SIG_IGN means "deliver it (clears pending) but do nothing" so we don't exit. */
sigemptyset(&sa.sa_mask);
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
int rc = fork();
if (rc < 0) {
perror("fork");
exit(1);
}
if (rc == 0) {
printf("hello!\n");
kill(getppid(), SIGUSR1);
return 0;
}
/* Wait only for SIGUSR1
* sigsuspend(mask) uses mask as the BLOCK mask while waiting.
* To be woken only by SIGUSR1, we must block all other signals:
* wait_mask = all signals minus SIGUSR1. */
sigfillset(&wait_mask);
sigdelset(&wait_mask, SIGUSR1);
sigsuspend(&wait_mask);
printf("bye!\n");
return 0;
}
Exercise 4
Write a program that calls fork() and then calls some form of exec() to run the program bin/ls. See if you can try all of the variants of exec() including execl(), execle(), execlp(), execv(), execvp(), execVp(). Why do you think there are so many variants of the same basic call?
Different wrappers exist to cover different ways of passing the program name, arguments, and environment. Two naming patterns:
- l vs v — how you pass arguments
- l (list): variadic list, e.g. execl(“/bin/ls”, “ls”, “-l”, NULL) — handy when the number of arguments is fixed at compile time.
- v (vector): array of pointers, e.g. execv(path, argv) where argv is char *argv[] — handy when you build the argument list at runtime (e.g. from user input or parsing).
- p vs no p — how the program is found
- No p: you pass the full path to the executable, e.g. execl(“/bin/ls”, …).
- p: you pass a filename and the kernel looks it up in PATH, e.g. execlp(“ls”, “ls”, “-l”, NULL) — like a shell does.
- e vs no e — environment
- No e: the new program gets the current process’s environment (inherited).
- e: you pass the environment explicitly as a third (or final) argument, e.g. execle(path, arg0, …, (char *)NULL, envp) — for controlled or minimal environments.
So the “many” versions are the 2×2×2 combinations: (list | vector) × (path | PATH lookup) × (inherit env | pass env) → execl, execv, execlp, execvp, execle, execve, execvpe (and on some systems execlpe). One underlying syscall is usually execve; the rest are wrappers that build the right arguments and call it.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
} else if(pid == 0) {
// replace the child process with the ls command
// execl writes to the standard output and the standard error
// if it succeeds, ls will run and print the output to the standard output and the standard error, and then exit.
execl("/bin/ls", "ls", "-l", NULL);
perror("execl");
// Execle is like execl, but it takes an environment variable as an argument.
// execle("/bin/ls", "ls", "-la", NULL, NULL);
// perror("execle");
// Execvp is like execl, but it takes a variable number of arguments.
// execvp("/bin/ls", (char *[]){"ls", "-la", NULL});
// perror("execvp");
} else {
// parent process
printf("Hello from parent process\n");
}
return 0;
}
Exercise 5
Write a program that uses wait() to wait the child process to finish in the parent. What does wait() return? What happens if you use wait() in the child?
If the child (the process that got 0 from fork()) calls wait(): It’s waiting for its children, not for the parent. The child normally has no children, so it has no one to wait for. wait() returns -1 and sets errno to ECHILD (“no child processes”). So the child doesn’t wait for the parent or for itself; it just gets an error. The parent is the one that should call wait() (or waitpid()) to reap the child and get its exit status.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
} else if(pid == 0) {
printf("child: %d\n", getpid());
} else {
// Wait for a specific child process to exit
pid_t child_pid = waitpid(pid, NULL, 0);
// Wait for any child process to exit
// wait(NULL);
printf("waited for child: %d\n", child_pid);
}
return 0;
}
Exercise 6
Write a program that creates a child process and then in the child closes the standard output STDOUT_FILENO. What happens if the child calls printf() to print some output after closing the descriptor?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int pid = fork();
if(pid < 0) {
perror("fork");
exit(1);
} else if(pid == 0) {
// child process
if (close(STDOUT_FILENO) == -1) {
perror("close stdout");
}
// this will not print because stdout is closed
printf("child: %d\n", getpid());
} else {
// parent process
printf("parent: %d\n", getpid());
}
return 0;
}