Processes

At any one moment in time only a single program is being executed on a CPU.  In a second of time, multiple programs may have run, each for perhaps 100 milliseconds, which makes it appear as if the processor is running programs in parallel.

Since the processor is switching back and forth between programs, the system must keep track of data for each program.  The stored data for a running program is called a process.  The act of swapping one processes off the CPU for another is called a context switch.

A process has memory (not necessarily contiguous) allocated for it to store

  • the executable code that is running
  • a stack
  • heap
  • environmental variables
  • initialized global variables
  • BSS (block started by symbol) that contains uninitialized variables

Each process has a unique process identifier (pid).  You can view the pids (and other information) of the process running on a Linux machine by issuing the following command on the command line.

ps -ax

Process Creation

The fork() system call is the only way to create a new process in a POSIX compliant operating system.

#include <unistd.h>
pid_t fork(void);

When fork() is executed it creates a new child process. The child process is a near identical copy of the parent process that called fork().  The two processes (parent and child) share the same address space where their executable code is stored, but they have different stacks and context switch state information so that when they execute they can diverge in their execution.  Immediately, following the call to fork(), the child’s stack contains the same information as its parent’s stack and both processes continue executing at the point of execution following the call to fork() that created the child.  The fork() method however returns a value of 0 to the child process and the pid of the child process to the parent process.  This allows the programmer to inspect the return value for fork() and have the child process do one thing and the parent process do another.

 Changing the Executable Code of a Process

Fork() is often used to create a new process that runs a different executable file than that of the parent.  This is accomplished by using one of the C standard library exec functions defined in <unistd.h>.  These functions do not return on success.  Instead the text, data, and stack of the calling process are replaced by that of the program loaded and execution begins at the entry point of the new executable.

    pid_t pid;
    int status;

    if ((pid = fork()) == 0) {                    // child process
        char *path = "./print_time";
        char *cmd = "print_time";
        char *arg = "Child 1";

        if(execl(path, cmd, arg, NULL) == -1) {
            printf("%s\n", strerror(errno));
            exit(1);
        }
    } else {                                      // parent process
        if (wait(&status) == -1) {
            printf("%s\n", strerror(errno));
            exit(1);
        }
    }

The execl() function takes a c-string specifying the path to an executable file followed by a NULL terminated list of arguments that are to be passed to new executable file.  The first argument in the list is traditionally the name of the executable.  In the example above, we assume there is a program named print_time that resides in the current working directory, hence the use of “./” in the path variable.  We also assume that print_time takes a string (“Child 1”) as an argument.  Other examples can be found here.

The wait() function, defined in <sys/wait.h>, halts execution of the calling process until a child process’ state changes (e.g. it terminates) or a signal handler interrupts the call.

/proc

All information about a process’ state is stored by the kernel in memory.  On Linux, the kernel maintains what is known as the /proc file system for this purpose. The state information stored includes:

  • process state
  • program counter (next instruction)
  • data in CPU registers
  • scheduling information
  • memory management information
  • accounting information
  • I/O status information

The data in /proc is mounted to the root file system and can be view by a root user.

Process States

In between creation and termination, a process can be in one of three states:

  • Running (actually using the CPU at that instant)
  • Ready (runnable; waiting to run on the CPU)
  • Blocked (unable to run until some external event happens)

The scheduler is the process that decides which process will run and for how long.

4 transitions are possible:

  • Ready -> Running     (scheduler decides to run the process)
  • Running -> Ready     (context switch)
  • Running ->Blocked   (I/O or event wait)
  • Blocked -> Ready      (I/O or event completion)

The states listed above are generalized and should not be confused with the actual states used by a specific kernel.  For example, in the Linux kernel, there are 5 states.  A process that is in the TASK_RUNNING state is either running on the CPU or is in the runnable queue.

Process Termination

Processes can terminate due to the following

  • normal end of code
  • gracefully termination by programmer after error (using exit())
  • terminated by OS due to fatal error
  • killed by another process (using kill())

The kill() system call can be used to send a signal to the OS requesting a process be terminated.

int kill(pid_t pid, int sig);

The parameters to kill include an integer indicating the pid of the process to which the signal is to be sent and an integer indicating the type of signal to be sent.

Two common signals are SIGTERM (15) and SIGKILL (9).  If SIGTERM is received, the OS may forward the signal to the process.  If the process has registered a signal handler it can catch the signal and execute the code in the signal handler rather than automatically terminating.  The SIGKILL signal requests immediate termination and if the process which issues the kill() system call has authority over the killee, the process is terminated.

A list of signals for a Linux system can be found at /usr/include/bits/signum.h.  An example showing the GNU C signum.h file can be seen here.

Example

#include <stdio.h>

void main() {
    while(1) {
        printf(“X ”);
    }
}

If you compile and run the example above you will see “X” being repeatedly printed to stdout.  By pressing CTRL+C, the shell will send the SIGINT signal to the process causing it to terminate.

You can also send a signal to a process using the kill command on the command line as shown below.  Here the -9 flag indicates that a SIGKILL signal should be sent to the process having the pid 5678.

kill -9 5678

Orphan and Zombie Processes

If a parent process terminates before a child process, the child process is called an orphan process and is inherited by systemd.

If the child process terminates while the parent is still running, the child’s process information stays in memory until the parent calls wait() which returns the child’s return status.  While the child’s process information is in memory, the child’s process (though terminated) is called a zombie process.

© 2017 – 2018, Eric. All rights reserved.