10. Processes
A program is executable code stored somewhere. A process is one running instance of a program, together with the environment and execution state required to keep it running.
That state includes:
- CPU registers and the current instruction;
- the process's address space: code, globals, heap and stack;
- open file descriptors;
- a process ID and parent relationship;
- environment variables and other OS-managed properties.
The address space is the collection of memory addresses which this process is allowed to use, together with the contents and permissions mapped at those addresses. It is the process's private view of memory, not necessarily a direct map of physical RAM. The same numerical address can therefore refer to different data in two different processes.
Running the same program twice creates two processes. They execute the same instructions but normally have separate memory and separate process IDs.
Process IDs and Families#
Each process has a positive integer process ID, or PID. It also has a parent PID:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void) {
printf("pid: %d\n", (int)getpid());
printf("parent: %d\n", (int)getppid());
return 0;
}PIDs are recycled after processes terminate, so a PID is not a permanent identity. Processes form a changing parent-child hierarchy. A parent can create children; on Unix-like systems, if a parent terminates while a child continues, the child is reparented to an appropriate system process or subreaper which can eventually reap it.
flowchart TD
INIT["system process"] --> SHELL["shell"]
SHELL --> EDITOR["editor"]
SHELL --> PROGRAM["your program"]
PROGRAM --> CHILD1["child process"]
PROGRAM --> CHILD2["child process"]
Environment Variables#
Environment variables are strings of the form name=value supplied to a process. Common examples include:
| Variable | Typical meaning |
|---|---|
PATH |
directories searched for commands |
HOME |
user's home directory |
LANG |
locale and preferred language/encoding behaviour |
TZ |
timezone |
Shell syntax can set a variable for the current shell and its future children:
export STATUS=greatIn C:
#include <stdlib.h>
const char *home = getenv("HOME");
if (home != NULL) {
printf("home: %s\n", home);
}
if (setenv("STATUS", "great", 1) == -1) {
perror("setenv");
}getenv() returns NULL when the name is absent. Do not pass that directly to %s.
The entire environment is available through environ on POSIX systems:
extern char **environ;
for (size_t i = 0; environ[i] != NULL; i++) {
puts(environ[i]);
}Environment variables are inherited as process state; they are not global variables shared live between processes. Changing a parent's environment after a child has been created does not retroactively update the child's copy.
Some systems also allow main(int argc, char *argv[], char *envp[]), but POSIX does not standardise that third parameter. environ and getenv() are the course's more portable interfaces.
Inspecting Processes from the Shell#
Unix tools expose different views of running processes:
| Command | Purpose |
|---|---|
ps |
snapshot of selected processes |
top / htop |
continuously updated resource usage |
w |
logged-in users and their activity |
kill |
send a signal to a process; despite the name, not every signal terminates it |
These are programs which use operating-system interfaces themselves. They are not privileged windows into magical state; the kernel decides which process information and operations the caller is permitted to access.
Multitasking and Context Switches#
Many processes appear to run simultaneously. On one CPU core, the OS can create this illusion by running one process for a short time, saving its state, then restoring another process's state. This handover is a context switch.
flowchart TB
A1["Process A"] --> B1["Process B"] --> C1["Process C"] --> A2["Process A again"]
A scheduler is the part of the kernel which chooses which ready process or thread receives a CPU next. A time slice is the short interval for which it may run before a timer gives the scheduler another chance to choose. A process blocks when it cannot make progress yet—for example, while waiting for keyboard input—so the scheduler can run something useful instead of wasting that CPU time.
A process may therefore stop running because its time slice ended, because it blocked waiting for I/O, or because another scheduling decision was required. The scheduler balances responsiveness, fairness, throughput and other requirements. With multiple cores, some processes can genuinely run in parallel; see Concurrency and Threads.
exec: Replace the Current Program#
The exec family loads a new program into the current process. It does not create another process.
#include <unistd.h>
int execve(const char *path, char *const argv[], char *const envp[]);On success:
- the process keeps its PID;
- a new program image replaces its old code, globals, heap and stack;
- open file descriptors normally survive unless marked close-on-exec;
- execution begins in the new program;
execve()does not return.
extern char **environ;
char *arguments[] = {"/bin/echo", "good-bye", "cruel", "world", NULL};
execve("/bin/echo", arguments, environ);
// Reaching this point means execve failed.
perror("execve");
return 1;argv[0] conventionally names the program, and both argv and envp must end with a null pointer.
Convenience variants differ in how arguments and environments are supplied. For example, execv() inherits the existing environment, while execvp() also searches directories in PATH. Check an assignment's restrictions before choosing one.
flowchart LR
BEFORE["PID 412<br/>running your program<br/>old address space"] -->|"execv('/bin/date', ...)"| AFTER["PID 412<br/>running /bin/date<br/>new address space"]
fork: Duplicate the Current Process#
fork() creates a child process by duplicating the calling process:
Note
The official 26T2 material labels fork() obsolete for new code and recommends posix_spawn() when it fits the task. We still study fork() because it makes process duplication, descriptor inheritance, pipes and shell-style execution easier to understand, and because existing Unix code uses it. Do not assume that “covered in the course” means “preferred for a new program”.
pid_t fork(void);Both processes return from the call, but receive different results:
-1in the original process means creation failed;0is returned in the child;- the child's PID is returned in the parent.
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
printf("child: pid=%d\n", (int)getpid());
} else {
printf("parent: child pid=%d\n", (int)pid);
}After fork(), the scheduler may run either process first. Do not infer parent-before-child ordering from the order of the branches in the source code; use synchronisation such as waitpid() when an order is required.
The child begins with a copy of the parent's memory. Afterwards, ordinary writes to variables affect only that process's copy:
int x = 10;
pid_t pid = fork();
if (pid == 0) {
x = 99;
printf("child x = %d\n", x);
} else if (pid > 0) {
printf("parent x = %d\n", x); // still 10
}File descriptors are inherited too. An open file description is the kernel record behind one particular opening of a file; it stores state such as the current offset and status flags. Parent and child descriptors derived from the same pre-fork() descriptor refer to the same open file description, so they share an offset. This makes communication and redirection possible, but unsynchronised writes can interleave. File Systems develops the complete descriptor-table model.
Practice: Quick Checks#
- What are the three classes of return value from
fork(), and which process sees each successful value? - Does
execv()create a process? Name two pieces of process state which survive a successfulexecv()and two which are replaced.
Note
- Quick Check Answers
-1means failure and no child exists. The child receives0; the parent receives the child's positive PID.- No. The PID and normally the open file descriptors survive, while the program's code and address-space contents are replaced. Some other attributes have specific inheritance rules, so consult the manual when they matter.
Counting Forked Processes#
Every process which reaches an unconditional fork() creates one additional process. Therefore:
fork();
fork();
fork();produces 2^3 = 8 processes, because the first fork leaves two processes to execute the second, and then four execute the third.
flowchart TD
S["start<br/>1 process"] --> F1["after fork 1<br/>2 processes"]
F1 --> F2["after fork 2<br/>4 processes"]
F2 --> F3["after fork 3<br/>8 processes"]
Conditions alter the count. Draw a process tree and follow exactly which branches reach each call.
Worked trace: a conditional second fork#
pid_t first = fork();
if (first == 0) {
fork();
}
puts("done");After the first successful fork, there are two processes: the original parent, where first > 0, and its child, where first == 0. Only the child enters the if and performs the second fork:
| Process | Value of first |
Reaches the second fork()? |
Prints |
|---|---|---|---|
| original parent | positive child PID | no | once |
| first child | 0 |
yes | once |
| new grandchild | 0 because it was created by that call |
it begins after the call | once |
There are therefore three processes and three possible done lines, not four. The grandchild inherits the first child's variables, so its copy of first is also zero. The printed order is unspecified because no process waits for another.
Caution
- Fork bombs
A loop in which every existing process forks grows exponentially and can make a machine unusable. Never experiment with an uncontrolled sequence of forks.
Waiting and Exit Status#
A parent can wait for a child to change state:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);For the usual case, pass the child's PID and 0 as the options:
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("exit status: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("signal: %d\n", WTERMSIG(status));
}The integer filled by waitpid() is an encoded status word, not directly the child's exit code. Use macros such as WIFEXITED and WEXITSTATUS.
When a child terminates, a small record remains until its parent collects the status. During this time it is a zombie. The process is no longer executing, but its process-table entry still consumes a resource. A long-running parent must reap its children.
To reap a child simply means collecting this final record with wait() or waitpid(). Think of termination and cleanup as two separate events: the child stops first, then its parent acknowledges the result and lets the kernel discard the remaining bookkeeping.
An orphan is different: it is a still-running process whose original parent terminated.
Practice: Tracing and Calculation#
- Assuming every call succeeds, how many processes reach
puts("done");after two consecutive unconditional calls tofork()? How many times candonebe printed? - A child exits with status
7, andwaitpid()stores an encoded value instatus. Why isstatus == 7the wrong test, and what should the parent use?
Note
- Tracing and Calculation Answers
3. Four processes reach the line, so done can be printed four times. Each existing process is duplicated by the second fork.
4. The status word also encodes how the child terminated. First test WIFEXITED(status) and then obtain the exit code with WEXITSTATUS(status).
Practice: Challenge#
- A parent changes
STATUSfromoldtonewafterfork(). The child has not calledexec()yet. Which value doesgetenv("STATUS")return in the child, and why? - Distinguish a zombie from an orphaned child. Which operation prevents a terminated child from remaining a zombie?
Note
- Challenge Answers
5. The child sees its inherited copy of old. fork() duplicates process state; later changes to the parent's environment do not mutate the child's separate address space.
6. A zombie has terminated but still has unreaped status information. An orphan is still running after its parent exits and is reparented. The appropriate parent or subreaper prevents a zombie from persisting by calling wait() or waitpid().
Note
- Further reading
The course's official Processes topic notes collect its expected interfaces and examples. The Linux manual pages for fork(), execve() and waitpid() document the exact inherited and replaced state.
exit() and _exit()#
exit(status) performs normal library cleanup:
- calls functions registered with
atexit(); - flushes and closes
stdiostreams; - terminates the process and makes the status available to its parent.
_exit(status) terminates immediately without flushing stdio or running those handlers. A child may use _exit() after an exec() failure to avoid flushing buffers copied from the parent.
Normal child termination also causes the parent to receive SIGCHLD. Signals are asynchronous notifications; COMP1521 mainly observes this one through waitpid() rather than building complex signal handlers.
if (pid == 0) {
char *argv[] = {"/bin/date", "--utc", NULL};
execv(argv[0], argv);
perror("execv");
_exit(127);
}Note
Checkpoint
fork()creates a second process, and both processes continue from the call with different return values.exec()replaces the current program but keeps the process identity.waitpid()gives the parent an ordering point and collects the child's encoded termination status.
Fork, Exec and Wait Together#
The classic shell-like pattern is:

sequenceDiagram
participant P as Parent
participant C as Child
P->>C: fork()
Note over P: receives child PID
Note over C: receives 0
C->>C: execv(new program)
P->>P: waitpid(child PID)
C-->>P: termination status
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
char *argv[] = {"/bin/date", "--utc", NULL};
execv(argv[0], argv);
perror("execv");
_exit(127);
}
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}Practice: Writing and Debugging#
- Complete the child-side error rule: after
fork(), callexecv(...); if it returns, report the error and terminate with which function—exit()or_exit()—and why? - Find the bug:
char *args[] = {"wc", "-l"}; execvp(args[0], args);. What exact correction is required?
Note
- Writing and Debugging Answers
7. Use _exit() after reporting the failed execv(). It avoids flushing copied stdio buffers or running inherited atexit handlers in the child.
8. An exec argument vector must end with a null pointer: char *args[] = {"wc", "-l", NULL};.
posix_spawn#
posix_spawn() combines process creation with execution through a standard interface:
#include <errno.h>
#include <spawn.h>
#include <stdio.h>
#include <sys/wait.h>
extern char **environ;
pid_t pid;
char *argv[] = {"/bin/date", "--utc", NULL};
int error = posix_spawn(&pid, argv[0], NULL, NULL, argv, environ);
if (error != 0) {
errno = error;
perror("posix_spawn");
return 1;
}
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}Unlike many POSIX functions, posix_spawn() returns an error number directly rather than returning -1 and setting errno. The example assigns it to errno only so perror() can produce a readable message. A successful spawn means the new process was created; it does not mean that the new program has already finished successfully. Waiting and interpreting the encoded status remain separate parent responsibilities.
posix_spawnp() searches PATH. File-action objects can arrange redirection and pipes before the child program begins; see Pipes and File Redirection.
Why system() is Dangerous#
system(command) gives a string to a shell. That is convenient for throwaway code, but any untrusted text embedded in the command can be interpreted as shell syntax.
// Unsafe if filename came from a user:
char command[1024];
snprintf(command, sizeof command, "cat %s", filename);
system(command);A filename containing spaces, semicolons or command substitutions changes the meaning. Prefer posix_spawn() or fork() plus exec(), where each argument is a separate string and is not reparsed as shell code.
Practice: Cumulative#
- Write the control-flow skeleton for a parent which creates one child, has the child run
/usr/bin/wc -l input.txt, waits for that exact child, and reports whether it exited normally. Include the important error paths and the terminatingNULLinargv.
Note
- Cumulative Process Answer
The child branch builds a null-terminated argument vector and calls execv; returning from execv means failure, so it reports the error and calls _exit. The parent waits using the positive PID returned by fork.
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
char *argv[] = {"/usr/bin/wc", "-l", "input.txt", NULL};
execv(argv[0], argv);
perror("execv");
_exit(127);
}
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
printf("wc exited with %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("wc was terminated by signal %d\n", WTERMSIG(status));
}Processes give independent programs separate address spaces. Concurrency and Threads considers the contrasting design: several execution streams inside one process, deliberately sharing memory and descriptors.