COMP1521 1,546 words·8 min read

10. Processes

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.

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:

C
#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.

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:

BASH
export STATUS=great

In C:

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:

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

A process may 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.

C
#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.
C
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.

fork: Duplicate the Current Process#

fork() creates a child process by duplicating the calling process:

C
pid_t fork(void);

Both processes return from the call, but receive different results:

  • -1 in the original process means creation failed;
  • 0 is returned in the child;
  • the child's PID is returned in the parent.
C
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:

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

Counting Forked Processes#

Every process which reaches an unconditional fork() creates one additional process. Therefore:

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

Conditions alter the count. Draw a process tree and follow exactly which branches reach each call.

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:

C
#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:

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

An orphan is different: it is a still-running process whose original parent terminated.

exit() and _exit()#

exit(status) performs normal library cleanup:

  • calls functions registered with atexit();
  • flushes and closes stdio streams;
  • 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.

C
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:

C
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;
}

posix_spawn#

posix_spawn() combines process creation with execution through a standard interface:

C
#include <errno.h>
#include <spawn.h>
#include <stdio.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;
}

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.

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.

C
// 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.

Common Mistakes#

  • Saying exec() creates a new process. It replaces the current program and keeps the PID.
  • Forgetting the NULL terminator in an argument array.
  • Putting code after successful exec() and expecting it to run.
  • Forgetting that both parent and child continue after fork().
  • Failing to handle fork() == -1.
  • Reading status from waitpid() as though it were a plain exit code.
  • Constructing shell command strings from untrusted input.

Practice#

  1. What are the three possible return values of fork(), and what does each mean?
  2. Does execv() create a new process? What happens to the process ID (PID)?
  3. Why should you use _exit() instead of exit() in a child process if execv() fails?
  4. What is a zombie process, and how does a parent process prevent zombies?
  5. How does posix_spawn() differ from fork() + execv() in return value error handling?

Note

- Answers

  1. -1: Error (fork failed, no child created); 0: Returned to the newly created child process; > 0: The child's PID returned to the parent process.
  2. No. execv() replaces the memory and program of the current process with a new program image. The PID remains unchanged.
  3. exit() flushes user-space stdio buffers and runs atexit handlers, which may duplicate buffered output already flushed (or to be flushed) by the parent. _exit() terminates the child immediately at the kernel level without running libc exit handlers.
  4. A zombie process is a terminated process whose entry in the process table remains until its parent collects its exit status. Parents prevent zombies by calling wait() or waitpid().
  5. posix_spawn() returns an integer error number directly (0 on success, > 0 error code), whereas fork() and execv() return -1 and set errno.

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.