Chapter 9. Process Relationships


Every process has a parent process (the initial kernel-level process is usually its own parent). The parent is notified when the child terminates, and the parent can obtain the child’s exit status.

This chapter details process groups and the concept of session introduced by POSIX.1, as well as relationship between the login shell that is invoked when a user logs in and all the processes that are started from the login shell.

The concept of UNIX system signal mechanism in Chapter 10 is needed.

Terminal Logins

In early UNIX systems, the terminals (dumb terminals that are hard-wired connected to the host) were either local (directly connected) or remote (connected through a modem). These logins came through a terminal device driver in the kernel. [p285]

As bitmapped graphical terminals became available, windowing systems were developed to provide users with new ways to interact with host computers. Applications were developed to create "terminal windows" to emulate character-based terminals, allowing users to interact with hosts in familiar ways (i.e., via the shell command line).

Today, some platforms allow you to start a windowing system after logging in, whereas other platforms automatically start the windowing system for you. In the latter case, you might still have to log in, depending on how the windowing system is configured (some windowing systems can be configured to log you in automatically).

The procedure that we now describe is used to log in to a UNIX system using a terminal. The procedure is similar regardless of the type of terminal we use. It could be a:

BSD Terminal Logins

The file /etc/ttys (created by the system administrator) has one line per terminal device. Each line specifies the name of the device and other parameters (e.g. baud rate) that are passed to the getty program.

After the system is bootstrapped, the kernel creates the init process (PID 1) which brings the system up in multiuser mode. The init process reads the file /etc/ttys and, for every terminal device that allows a login, does a fork followed by an exec of the program getty.

Figure 9.2 State of processes after login has been invoked

All the processes shown in the figure above have a real user ID of 0 and an effective user ID of 0 (they all have superuser privileges). All the processes other than the original init process have a parent process ID of 1.

This is the traditional authentication procedure used on UNIX systems. Modern UNIX systems have evolved to support multiple authentication procedures. FreeBSD, Linux, Mac OS X, and Solaris all support a more flexible scheme known as PAM (Pluggable Authentication Modules). PAM allows an administrator to configure the authentication methods to be used to access services that are written to use the PAM library. [p288]

If we log in correctly, login will:

The login can optionally print the message-of-the-day file, check for new mail, and performs other tasks.

Since it is called by a superuser process, setuid changes all three user IDs: the real user ID, effective user ID, and saved set-user-ID. The call to setgid that was done earlier by login has the same effect on all three group IDs.

At this point, our login shell is running. Its parent process ID is the original init process (process ID 1), so when our login shell terminates, init is sent a SIGCHLD signal and it starts the whole procedure over again for this terminal. File descriptors 0, 1, and 2 for our login shell are set to the terminal device. See the figure below:

Figure 9.3 Arrangement of processes after everything is set for a terminal login

Our login shell now reads its start-up files (.profile for the Bourne shell and Korn shell; .bash_profile, .bash_login, or .profile for the GNU Bourne-again shell; and .cshrc and .login for the C shell). These start-up files usually change some of the environment variables and add many other variables to the environment. For example, most users set their own PATH and often prompt for the actual terminal type (TERM). When the start-up files are done, we finally get the shell’s prompt and can enter commands.

Mac OS X Terminal Logins

On Mac OS X, the terminal login process follows essentially the same steps as in the BSD login process (since Mac OS X is based in part on FreeBSD) with the following differences:

Linux Terminal Logins

The Linux login procedure is very similar to the BSD procedure. The login command is derived from 4.3BSD. The main difference is in terminal configuration.

Some Linux distributions ship with a version of the init program that uses administrative files patterned after System V’s init file formats. where /etc/inittab specifies the terminal devices for which init should start a getty process. Other Linux distributions, such as Ubuntu, ship with a version of init that is known as "Upstart". It uses configuration files named *.conf that are stored in the /etc/init directory. For example, the specifications for running getty on /dev/tty1 might be found in the file /etc/init/tty1.conf.

Depending on the version of getty in use, the terminal characteristics are specified either on the command line (as with agetty) or in the file /etc/gettydefs (as with mgetty).

Solaris Terminal Logins


Network Logins

The main difference between a serial terminal login and a network login is that the connection between the terminal and the computer isn’t point-to-point. In this case, login is simply a service available, just like any other network service, such as FTP or SMTP.

With the terminal logins, init knows which terminal devices are enabled for logins and spawns a getty process for each device. In the case of network logins, however, all the logins come through the kernel’s network interface drivers (e.g., the Ethernet driver), and we don’t know ahead of time how many of these will occur. Instead of having a process waiting for each possible login, we now have to wait for a network connection request to arrive.

To allow the same software to process logins over both terminal logins and network logins, a software driver called a pseudo terminal (detailed in Chapter 19) is used to emulate the behavior of a serial terminal and map terminal operations to network operations, and vice versa.

BSD Network Logins

In BSD, the inetd process, sometimes called the Internet superserver, waits for most network connections.

As part of the system start-up, init invokes a shell that executes the shell script /etc/rc, which starts inetd along with other daemons. Once the shell script terminates, the parent process of inetd becomes init; inetd waits for TCP/IP connection requests to arrive at the host. When a connection request arrives for it to handle, inetd does a fork and exec of the appropriate program.

Assume a TCP connection request arrives for the TELNET server (a remote login application). The remote user initiates the login by starting the TELNET client:

telnet hostname

The client opens a TCP connection to hostname and the user who started the client program is now logged in to the server’s host. The figure below shows the sequence of processes involved in executing the TELNET server, called telnetd:

Figure 9.4 Sequence of processes involved in executing TELNET server

Then, the telnetd process then opens a pseudo terminal device and splits into two processes using fork, which do the following:

Figure 9.5 Arrangement of processes after everything is set for a network login

Whether we log in through a terminal (Figure 9.3) or a network (Figure 9.5), we have a login shell with its standard input, standard output, and standard error connected to either a terminal device or a pseudo terminal device.

In the coming sections, we'll see that the login shell is the start of a POSIX.1 session, and that the terminal or pseudo terminal is the controlling terminal for the session.

Mac OS X Network Logins

The network login on Mac OS X is identical to that on BSD, except that the telnet daemon is run from launchd. By default, the telnet daemon is disabled on Mac OS X (although it can be enabled with the launchctl(1) command). The preferred way to perform a network login on Mac OS X is with ssh, the secure shell command.

Linux Network Logins

Network logins under Linux are the same as under BSD, except that some distributions use an alternative inetd process called the extended Internet services daemon, xinetd. The xinetd process provides a finer level of control over services it starts compared to inetd.

Solaris Network Logins


Process Groups

In addition to having a process ID, each process belongs to a process group.

The function getpgrp returns the process group ID of the calling process. The getpgid function took a pid argument and returned the process group for that process.


#include <unistd.h>

pid_t getpgrp(void);
/* Returns: process group ID of calling process */

pid_t getpgid(pid_t pid);
/* Returns: process group ID if OK, −1 on error */

For getpgid, if pid is 0, the process group ID of the calling process is returned. Thus,


is equivalent to:


Each process group can have a process group leader, whose process group ID equals to its process ID.

Process group lifetime

The process group life time is the period of time that begins when the group is created and ends when the last remaining process leaves the group. It is possible for a process group leader to create a process group, create processes in the group, and then terminate. The process group still exists, as long as at least one process is in the group, regardless of whether the group leader terminates. The last remaining process in the process group can either terminate or enter some other process group.

setpgid function

A process can join an existing process group or creates a new process group by calling setpgid.


#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid);

/* Returns: 0 if OK, −1 on error */

The setpgid function sets the process group ID of the process whose process ID equals pid to pgid.



Job-control shells

In most job-control shells, this function is called after a fork to have the parent set the process group ID of the child, and to have the child set its own process group ID. One of these calls is redundant, but by doing both, we are guaranteed that the child is placed into its own process group before either process assumes that this has happened. If we didn’t do this, we would have a race condition, since the child’s process group membership would depend on which process executes first. (See Doubts and Solutions for details) [p294]

Process groups and signals

We can send a signal to either a single process (identified by its process ID) or a process group (identified by its process group ID). Similarly, the waitpid function lets us wait for either a single process or one process from a specified process group.


A session is a collection of one or more process groups.

Figure 9.6 Arrangement of processes into process groups and sessions

The processes in a process group are usually placed there by a shell pipeline. The arrangement in the figure above is generated by the shell commands of the form:

proc1 | proc2 &
proc3 | proc4 | proc5

The setsid function

A process establishes a new session by calling the setsid function.


#include <unistd.h>

pid_t setsid(void);

/* Returns: process group ID if OK, −1 on error */

If the calling process is not a process group leader, this function creates a new session. Three things happen:

  1. The process becomes the session leader of this new session. (A session leader is the process that creates a session.) The process is the only process in this new session
  2. The process becomes the process group leader of a new process group. The new process group ID is the process ID of the calling process.
  3. The process has no controlling terminal. If the process had a controlling terminal before calling setsid, that association is broken.

This function returns an error if the caller is already a process group leader.

Ensuring the successful call of setsid

Since the setsid function returns an error if the caller is a process group leader, to ensure this is not the case, the usual practice is to call fork and have the parent terminate and the child continue. It is guaranteed that the child is not a process group leader, because the process group ID of the parent is inherited by the child, but the child gets a new process ID. Hence, it is impossible for the child’s process ID to equal its inherited process group ID.

Session Leader and Session ID

The Single UNIX Specification talks only about a "session leader"; there is no "session ID" similar to a process ID or a process group ID. A session leader is a single process that has a unique process ID, so we could talk about a session ID that is the process ID of the session leader. This concept of a session ID was introduced in SVR4.

The getsid function

The getsid function returns the process group ID of a process’s session leader.


#include <unistd.h>

pid_t getsid(pid_t pid);

/* Returns: session leader’s process group ID if OK, −1 on error */

If pid is 0, getsid returns the process group ID of the calling process’s session leader. For security reasons, some implementations may restrict the calling process from obtaining the process group ID of the session leader if pid doesn’t belong to the same session as the caller.

Controlling Terminal

Sessions and process groups have a few other characteristics.

These characteristics are shown in the figure below:

Figure 9.7 Process groups and sessions showing controlling terminal

Usually, the controlling terminal is established automatically when we log in.

Mechanisms of allocating a controlling terminal

System V

Systems derived from UNIX System V allocate the controlling terminal for a session when the session leader opens the first terminal device that is not already associated with a session, as long as the call to open does not specify the O_NOCTTY flag.


BSD-based systems allocate the controlling terminal for a session when the session leader calls ioctl with a request argument of TIOCSCTTY (the third argument is a null pointer). The session cannot already have a controlling terminal for this call to succeed. Normally, this call to ioctl follows a call to setsid, which guarantees that the process is a session leader without a controlling terminal.

Note that although Mac OS X 10.6.8 is derived from BSD, it behaves like System V when allocating a controlling terminal.

Method FreeBSD 8.0 Linux 3.2.0 Mac OS X 10.6.8 Solaris 10
open without O_NOCTTY x x x
TIOCSCTTY ioctl command x x x x

When a program wants to talk to the controlling terminal, regardless of whether the standard input or standard output is redirected, it can open the file /dev/tty. This special file is a synonym within the kernel for the controlling terminal. If the program doesn’t have a controlling terminal, the open of this device will fail.

The crypt command and getpass function

The classic example is the getpass(3) function, which reads a password (with terminal echoing turned off, of course). [p298]

The getpass function is called by the crypt(1) program and can be used in a pipeline. For example:

crypt < salaries | lpr

It decrypts the file salaries and pipes the output to the print spooler. Because crypt reads its input file on its standard input, the standard input can’t be used to enter the password. Also, crypt is designed so that we have to enter the encryption password each time we run the program, to prevent us from saving the password in a file (which could be a security hole).

tcgetpgrp, tcsetpgrp, and tcgetsid Functions

We need a way to tell the kernel which process group is the foreground process group, so that the terminal device driver knows where to send the terminal input and the terminal-generated signals. (Figure 9.7)


#include <unistd.h>

pid_t tcgetpgrp(int fd);
/* Returns: process group ID of foreground process group if OK, −1 on error */

int tcsetpgrp(int fd, pid_t pgrpid);
/* Returns: 0 if OK, −1 on error */

These two functions are normally called by job-control shells.

The tcgetsid function allows an application to obtain the process group ID for the session leader given a file descriptor for the controlling TTY.


#include <termios.h>

pid_t tcgetsid(int fd);

/* Returns: session leader’s process group ID if OK, −1 on error */

Applications that need to manage controlling terminals can use tcgetsid to identify the session ID of the controlling terminal’s session leader, which is equivalent to the session leader’s process group ID.

Job Control

Job control allows us to start multiple jobs (groups of processes) from a single terminal and to control which jobs can access the terminal and which jobs are run in the background. Job control requires three forms of support:

  1. A shell that supports job control
  2. The terminal driver in the kernel must support job control
  3. The kernel must support certain job-control signals

From our perspective, when using job control from a shell, we can start a job in either the foreground or the background. A job is simply a collection of processes, often a pipeline of processes.

For example, start a job consisting of one process in the foreground:

vi main.c

Start two jobs in the background (all the processes invoked by these background jobs are in the background.):

pr *.c | lpr &
make all &

Korn shell example

When we start a background job, the shell assigns it a job identifier and prints one or more of the process IDs.

$ make all > Make.out &
[1] 1475
$ pr *.c | lpr &
[2] 1490
$   # just press RETURN
[2] + Done pr *.c | lpr &
[1] + Done make all > Make.out &

While we can have a foreground job and one or more background jobs, only the foreground job receives terminal input (the characters that we enter at the terminal). It is not an error for a background job to try to read from the terminal, but the terminal driver detects this and sends a special signal to the background job: SIGTTIN. This signal normally stops the background job; by using the shell, we are notified of this event and can bring the job into the foreground so that it can read from the terminal.

cat > &   # start in background, but it’ll read from standard input
[1] 1681
$                  # we press RETURN
[1] + Stopped (SIGTTIN) cat > &
$ fg %1            # bring job number 1 into the foreground
cat >     # the shell tells us which job is now in the foreground
hello, world       # enter one line
ˆD                 # type the end-of-file character
$ cat     # check that the one line was put into the file
hello, world

Note that this example doesn’t work on Mac OS X 10.6.8. When we try to bring the cat command into the foreground, the read fails with errno set to EINTR. Since Mac OS X is based on FreeBSD, and FreeBSD works as expected, this must be a bug in Mac OS X.

There is an option that we can allow or disallow a background job to send its output to the controlling terminal. Normally, we use the stty(1) command to change this option.

$ cat &   # execute in background
[1] 1719
$ hello, world     # the output from the background job appears after the prompt
we press RETURN
[1] + Done cat &
$ stty tostop      # disable ability of background jobs to output to controlling terminal
$ cat &   # try it again in the background
[1] 1721
$                  # we press RETURN and find the job is stopped
[1] + Stopped(SIGTTOU) cat &
$ fg %1            # resume stopped job in the foreground
cat       # the shell tells us which job is now in the foreground
hello, world       # and here is its output

When we disallow background jobs from writing to the controlling terminal, cat will block when it tries to write to its standard output, because the terminal driver identifies the write as coming from a background process and sends the job the SIGTTOU signal. When we use the shell’s fg command to bring the job into the foreground, the job completes.

The figure below summarizes some of the features of job control that have been described so far:

Figure 9.9 Summary of job control features with foreground and background jobs, and terminal driver

Job control was originally designed and implemented before windowing terminals were widespread. It is a required feature of POSIX.1. [p302-303]

Shell Execution of Programs

This section examines how the shells execute programs and how this relates to the concepts of process groups, controlling terminals, and sessions

The shell without job control: the Bourne shell on Solaris

For example, with the classic Bourne shell running on Solaris, we execute:

ps -o pid,ppid,pgid,sid,comm

The output is

  949   947   949  949 sh
 1774   949   949  949 ps
Terminal process group ID: tpgid option of the ps(1) command


Some platforms support an tpgid option to have the ps(1) command print the process group ID associated with the session’s controlling terminal. This value would be shown under the TPGID column:

ps -o pid,ppid,pgid,sid,tpgid,comm

Note that it is misleading to associate a process with a terminal process group ID (the TPGID column):

If we execute the command in the background:

ps -o pid,ppid,pgid,sid,comm &

The only value that changes is the process ID of the command:

  949   947   949  949 sh
 1812   949   949  949 ps

This shell doesn’t know about job control, so the background job is not put into its own process group and the controlling terminal isn’t taken away from the background job.

To see how this shell handles a pipeline, we execute:

ps -o pid,ppid,pgid,sid,comm | cat1

The output is:

  949   947   949  949 sh
 1823   949   949  949 cat1
 1824  1823   949  949 ps

The program cat1 is just a copy of the standard cat program, with a different name. The last process in the pipeline (cat) is the child of the shell and that the first process in the pipeline (ps) is a child of the last process. It appears that the shell forks a copy of itself and that this copy then forks to make each of the previous processes in the pipeline.

If we execute the pipeline in the background:

ps -o pid,ppid,pgid,sid,comm | cat1 &

Only the process IDs change. Since the shell doesn’t handle job control, the process group ID of the background processes remains 949, as does the process group ID of the session

If a background process tries to read from its controlling terminal, like:

cat > &

Without job control, the shell automatically redirects the standard input of a background process to /dev/null, if the process doesn’t redirect standard input itself. A read from /dev/null generates an end of file. This means that our background cat process immediately reads an end of file and terminates normally.

The previous paragraph adequately handles the case of a background process accessing the controlling terminal through its standard input, but what happens if a background process specifically opens /dev/tty and reads from the controlling terminal? The answer is "It depends", but the result is probably not what we want. For example:

crypt < salaries | lpr &

This pipeline is run in the background, but the crypt program opens /dev/tty, changes the terminal characteristics (to disable echoing), reads from the device, and resets the terminal characteristics. The prompt Password: from crypt is printed on the terminal, but what we enter (the encryption password) is read by the shell, which tries to execute a command of that name. The next line we enter to the shell is taken as the password, and the file is not encrypted correctly, sending junk to the printer. Here we have two processes trying to read from the same device at the same time, and the result depends on the system. Job control, as described earlier, handles this multiplexing of a single terminal between multiple processes in a better fashion. [p304]

If we execute three processes in the pipeline, we can examine the process control used by this shell:

ps -o pid,ppid,pgid,sid,comm | cat1 | cat2

The output is: [p305]

  949   947   949  949 sh
 1988   949   949  949 cat2
 1989  1988   949  949 ps
 1990  1988   949  949 cat1

Again, the last process in the pipeline is the child of the shell, and all previous processes in the pipeline are children of the last process. See the figure below:

Figure 9.10 Processes in the pipeline ps | cat1 | cat2 when invoked by Bourne shell

Since the last process in the pipeline is the child of the login shell, the shell is notified when that process (cat2) terminates.

The shell with job control: Bourne-again shell on Linux

Starting with this example, foreground process group are shown in bolder font.

The command:

ps -o pid,ppid,pgid,sid,tpgid,comm

gives us:

  2837   2818   2837  2837   5796  bash
  5796   2837   5796  2837   5796  ps

We can see the result, which is different from the Bourne shell example:

Executing this process in the background:

ps -o pid,ppid,pgid,sid,tpgid,comm &

gives us:

  2837   2818   2837  2837   2837  bash
  5797   2837   5797  2837   2837  ps

Executing two processes in a pipeline, as in:

ps -o pid,ppid,pgid,sid,tpgid,comm | cat1

gives us:

  2837   2818   2837  2837   5799  bash
  5799   2837   5799  2837   5799  ps
  5800   2837   5799  2837   5799  cat1

If we execute this pipeline in the background:

ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 &

The output:

  2837   2818   2837  2837   2837  bash
  5801   2837   5801  2837   2837  ps
  5802   2837   5801  2837   2837  cat1

The results are similar, but now ps and cat1 are placed in the same background process group (5801).


Orphaned Process Groups

A process whose parent terminates is called an orphan and is inherited by the init process. The entire process groups that can be orphaned and this section discusses how POSIX.1 handles this situation.

Example of a process whose child is stopped

The following figure shows a situation: the parent process has forked a child that stops, and the parent is about to exit.

Figure 9.11 Example of a process group about to be orphaned

The program that creates an orphaned process group is shown below:

#include "apue.h"
#include <errno.h>

static void
sig_hup(int signo)
    printf("SIGHUP received, pid = %ld\n", (long)getpid());

static void
pr_ids(char *name)
    printf("%s: pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n",
        name, (long)getpid(), (long)getppid(), (long)getpgrp(),

    char    c;
    pid_t   pid;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0) {   /* parent */
        sleep(5);       /* sleep to let child stop itself */
    } else {            /* child */
        signal(SIGHUP, sig_hup);    /* establish signal handler */
        kill(getpid(), SIGTSTP);    /* stop ourself */
        pr_ids("child");    /* prints only if we're continued */
        if (read(STDIN_FILENO, &c, 1) != 1)
            printf("read error %d on controlling TTY\n", errno);

Result in a job-control shell:

$ ./a.out
parent: pid = 6099, ppid = 2837, pgrp = 6099, tpgrp = 6099
child: pid = 6100, ppid = 6099, pgrp = 6099, tpgrp = 6099
$ SIGHUP received, pid = 6100
child: pid = 6100, ppid = 1, pgrp = 6099, tpgrp = 2837
read error 5 on controlling TTY

Analysis: [p307-309]

FreeBSD Implementation

The figure below shows the FreeBSD implementation of sessions and process groups:

Figure 9.13 FreeBSD implementation of sessions and process groups

When setsid is called, a new session structure is allocated within the kernel. s_count is set to 1, s_leader is set to point to the proc structure of the calling process, s_sid is set to the process ID, and s_ttyvp and s_ttyp are set to null pointers, since the new session doesn’t have a controlling terminal.

The kernel finds the foreground process group of a particular session by following fields of pointers, starting with the session structure:


This chapter describes relation between groups of processes, sessions, which are made up of process groups. Job control is a feature supported by most UNIX systems. The controlling terminal for a process, /dev/tty, is also involved in these process relationships.

Doubts and Solutions


p294 on fork's race condition concerning setpgid

In most job-control shells, this function is called after a fork to have the parent set the process group ID of the child, and to have the child set its own process group ID. One of these calls is redundant, but by doing both, we are guaranteed that the child is placed into its own process group before either process assumes that this has happened. If we didn’t do this, we would have a race condition, since the child’s process group membership would depend on which process executes first.


The shell (parent) wants and ensures the process to be in the right process group at any time before either of the child and parent continues execution.