Chapter 9. Process Relationships¶
Introduction¶
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:
- character-based terminal,
- a graphical terminal emulating a simple character-based terminal,
- or a graphical terminal running a windowing system.
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
.
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.
- The
init
processexec
s thegetty
program with an empty environment. getty
callsopen
to open terminal device for reading and writing. File descriptors 0, 1, and 2 are set to the device.- Then,
getty
outputs something likelogin:
and waits for us to enter our user name.getty
can detect special characters to change the terminal's speed (baud rate). [p287] -
When we enter our user name,
getty
’s job is complete, and it then invokes thelogin
program, similar to:execle("/bin/login", "login", "-p", username, (char *)0, envp);
-
Though
init
invokesgetty
with an empty environment,getty
creates an environment forlogin
(theenvp
argument) with the name of the terminal (something likeTERM=foo
, where the type of terminalfoo
is taken from thegettytab
file) and any environment strings that are specified in thegettytab
. The-p
flag tologin
tells it to preserve the environment that it is passed and to add to that environment, not replace it. login
does the following things:- It calls
getpwnam
to fetch our password file entry. - It calls
getpass(3)
to display the promptPassword:
and read our password (with echoing disabled). - It calls
crypt(3)
to encrypt the password that we entered and compares the encrypted result to thepw_passwd
field from our shadow password file entry. - If the login attempt fails because of an invalid password (after a few tries),
login
callsexit
with an argument of 1. This termination will be noticed by the parent (init
), and it will do anotherfork
followed by anexec
ofgetty
, starting the procedure over again for this terminal.
- It calls
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:
- Change to our home directory (
chdir
) - Change the ownership of our terminal device (
chown
) so we own it - Change the access permissions for our terminal device so we have permission to read from and write to it
- Set our group IDs by calling
setgid
andinitgroups
- Initialize the environment with all the information that login has:
- our home directory (
HOME
), - shell (
SHELL
), - user name (
USER
andLOGNAME
), - and a default path (
PATH
).
- our home directory (
-
Change to our user ID (
setuid
) and invoke our login shell, as inexecl("/bin/sh", "-sh", (char *)0);
The minus sign as the first character of
argv[0]
is a flag to all the shells that indicates they are being invoked as a login shell. The shells can look at this character and modify their start-up accordingly.
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:
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:
- The work of
init
is performed bylaunchd
. - We are presented with a graphical-based login screen from the start.
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¶
[p290]
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
:
Then, the telnetd
process then opens a pseudo terminal device and splits into two processes using fork
, which do the following:
- The parent (
telnetd
) handles the communication across the network connection. - The child
exec
s thelogin
program. - The parent and the child are connected through the pseudo terminal. Before doing the
exec
, the child sets up file descriptors 0, 1, and 2 to the pseudo terminal. - If we log in correctly, login performs the same steps described in Section 9.2: it changes to our home directory and sets our group IDs, user ID, and our initial environment. Then
login
replaces itself with our login shell by callingexec
.
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¶
[p293]
Process Groups¶
In addition to having a process ID, each process belongs to a process group.
- A process group is a collection of one or more processes (usually associated with the same job) that can receive signals from the same terminal.
- Each process group has a unique process group ID. Process group IDs are similar to process IDs: they are positive integers and can be stored in a
pid_t
data type.
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,
getpgid(0);
is equivalent to:
getpgrp();
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.
Arguments:
- If pid == pgid, the process specified by pid becomes a process group leader.
- If pid == 0, the process ID of the caller is used.
- If pgid == 0, then the specified pid is used as the process group ID.
Rules:
- A process can set the process group ID of only itself or any of its children.
- A process cannot change the process group ID of one of its children after that child has called one of the
exec
functions.
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.
Sessions¶
A session is a collection of one or more process groups.
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:
- 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
- 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.
- 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.
- A session can have a single controlling terminal. This is usually the terminal device (in the case of a terminal login) or pseudo terminal device (in the case of a network login) on which we log in.
- The session leader that establishes the connection to the controlling terminal is called the controlling process.
- The process groups within a session can be divided into a single foreground process group and one or more background process groups.
- If a session has a controlling terminal, it has a single foreground process group and all other process groups in the session are background process groups.
- Whenever we press the terminal’s interrupt key (often DELETE or Control-C), the interrupt signal is sent to all processes in the foreground process group.
- Whenever we press the terminal’s quit key (often Control-backslash), the quit signal is sent to all processes in the foreground process group.
- If a modem (or network) disconnect is detected by the terminal interface, the hang-up signal is sent to the controlling process (the session leader).
These characteristics are shown in the figure below:
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¶
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 */
- The function
tcgetpgrp
returns the process group ID of the foreground process group associated with the terminal open on fd. - If the process has a controlling terminal, the process can call
tcsetpgrp
to set the foreground process group ID to pgrpid. The value of pgrpid must be the process group ID of a process group in the same session, and fd must refer to the controlling terminal of the session.
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:
- A shell that supports job control
- The terminal driver in the kernel must support job control
- 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 &
- The
make
is job number 1 and the starting process ID is 1475. The next pipeline is job number 2 and the process ID of the first process is 1490. - When the jobs are done and we press RETURN, the shell tells us that the jobs are complete. The reason we have to press RETURN is to have the shell print its prompt. The shell doesn’t print the changed status of background jobs at any random time (only after we press RETURN and right before it prints its prompt, to let us enter a new command line). If the shell didn’t do this, it could produce output while we were entering an input line.
- The interaction with the terminal driver arises because a special terminal character affects the foreground job. The terminal driver looks for three special characters, which generate signals to (all processes in ) the foreground process group:
SIGINT
: generated by the interrupt character (typically DELETE or Control-C).SIGQUIT
: generated by the quit character (typically Control-backslash).SIGTSTP
: generated by the suspend character (typically Control-Z).
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 > temp.foo & # start in background, but it’ll read from standard input [1] 1681 $ # we press RETURN [1] + Stopped (SIGTTIN) cat > temp.foo & $ fg %1 # bring job number 1 into the foreground cat > temp.foo # the shell tells us which job is now in the foreground hello, world # enter one line ˆD # type the end-of-file character $ cat temp.foo # check that the one line was put into the file hello, world
SIGTTIN
: When the backgroundcat
tries to read its standard input (the controlling terminal), the terminal driver, knowing that it is a background job, sends theSIGTTIN
signal to the background job.- The shell detects the change in status of its child (see
wait
andwaitpid
function in Section 8.6) and tells us that the job has been stopped. - The shell’s
fg
command move the stopped job into the foreground, which causes the shell to place the job into the foreground process group (tcsetpgrp) and send the continue signal (SIGCONT
) to the process group. - Since it is now in the foreground process group, the job can read from the controlling terminal.
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 temp.foo & # execute in background [1] 1719 $ hello, world # the output from the background job appears after the prompt we press RETURN [1] + Done cat temp.foo & $ stty tostop # disable ability of background jobs to output to controlling terminal $ cat temp.foo & # try it again in the background [1] 1721 $ # we press RETURN and find the job is stopped [1] + Stopped(SIGTTOU) cat temp.foo & $ fg %1 # resume stopped job in the foreground cat temp.foo # 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:
- The solid lines through the terminal driver box mean that the terminal I/O and the terminal-generated signals are always connected from the foreground process group to the actual terminal.
- The dashed line corresponding to the
SIGTTOU
signal means that whether the output from a process in the background process group appears on the terminal is an option.
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
PID PPID PGID SID COMMAND 949 947 949 949 sh 1774 949 949 949 ps
- The parent of the
ps
command is the shell. - Both the shell and the
ps
command are in the same session and foreground process group (949), because that is what you get when you execute a command with a shell that doesn’t support job control.
Terminal process group ID: tpgid
option of the ps(1)
command¶
[p303]
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):
- A process does not have a terminal process control group. A process belongs to a process group, and the process group belongs to a session.
- The session may or may not have a controlling terminal.
- If the session does have a controlling terminal, then the terminal device knows the process group ID of the foreground process. This value can be set in the terminal driver with the
tcsetpgrp
function (Figure 9.9).
- If the session does have a controlling terminal, then the terminal device knows the process group ID of the foreground process. This value can be set in the terminal driver with the
- The foreground process group ID is an attribute of the terminal, not the process. This value from the terminal device driver is what
ps
prints as the TPGID. If it finds that the session doesn’t have a controlling terminal,ps
prints either 0 or −1, depending on the platform.
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:
PID PPID PGID SID 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:
PID PPID PGID SID COMMAND 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 fork
s 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 > temp.foo &
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]
PID PPID PGID SID COMMAND 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:
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:
PID PPID PGID SID TPGID COMMAND 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:
- The Bourne-again shell places the foreground job (
ps
) into its own process group (5796). - The
ps
command is the process group leader and the only process in this process group. This process group is the foreground process group, since it has the controlling terminal. - The login shell is a background process group while the
ps
command executes. - Both process groups, 2837 and 5796, are members of the same session.
Executing this process in the background:
ps -o pid,ppid,pgid,sid,tpgid,comm &
gives us:
PID PPID PGID SID TPGID COMMAND 2837 2818 2837 2837 2837 bash 5797 2837 5797 2837 2837 ps
ps
command is again placed into its own process group.- The process group (5797) is no longer the foreground process group but a background process group.
- The foreground process group is our login shell, as indicated by TPGID of 2837.
Executing two processes in a pipeline, as in:
ps -o pid,ppid,pgid,sid,tpgid,comm | cat1
gives us:
PID PPID PGID SID TPGID COMMAND 2837 2818 2837 2837 5799 bash 5799 2837 5799 2837 5799 ps 5800 2837 5799 2837 5799 cat1
- Both processes,
ps
andcat1
, are placed into a new process group (5799), which is the foreground process group. - The login shell is the parent of both processes. This is different from the Bourne shell, which created the last process (
cat1
) in the pipeline first, and this process is the parent of first process (ps
).
If we execute this pipeline in the background:
ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 &
The output:
PID PPID PGID SID TPGID COMMAND 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).
[p307]
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 fork
ed a child that stops, and the parent is about to exit.
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(), (long)tcgetpgrp(STDIN_FILENO)); fflush(stdout); } int main(void) { char c; pid_t pid; pr_ids("parent"); if ((pid = fork()) < 0) { err_sys("fork error"); } else if (pid > 0) { /* parent */ sleep(5); /* sleep to let child stop itself */ } else { /* child */ pr_ids("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); } exit(0); }
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]
- The shell places the foreground process into its own process group (6099) and the shell itself stays in its own process group (2837). The child inherits the process group of its parent (6099).
- After
fork
, the parent sleeps for 5 seconds. This is our (imperfect) way of letting the child execute before the parent terminates. - The child establishes a signal handler for the hang-up signal (
SIGHUP
) so we can see whether it is sent to the child. (signal handlers are discussed in Chapter 10) - The child sends itself the stop signal (
SIGTSTP
) with thekill
function. This stops the child, similar to our stopping a foreground job with our terminal’s suspend character (Control-Z). - When the parent terminates, the child is orphaned, so the child’s parent process ID becomes 1, which is the
init
process ID. - At this point, the child is a member of an orphaned process group:
- The POSIX.1 definition of an orphaned process group: one in which the parent of every member is either itself a member of the group or is not a member of the group’s session. Another way of saying this is: the process group is not orphaned as long as a process in the group has a parent in a different process group but in the same session.
- If the process group is not orphaned, there is a chance that one of those parents in a different process group but in the same session will restart a stopped process in the process group that is not orphaned. Here, the parent of every process in the group (e.g., process 1 is the parent of process 6100) belongs to another session.
- Since the process group is orphaned when the parent terminates, and the process group contains a stopped process, POSIX.1 requires that every process in the newly orphaned process group be sent the hang-up signal (
SIGHUP
) followed by the continue signal (SIGCONT
).- This causes the child to be continued, after processing the hang-up signal. The default action for the hang-up signal is to terminate the process, so we have to provide a signal handler to catch the signal. We therefore expect the
printf
in thesig_hup
function to appear before theprintf
in thepr_ids
function.
- This causes the child to be continued, after processing the hang-up signal. The default action for the hang-up signal is to terminate the process, so we have to provide a signal handler to catch the signal. We therefore expect the
- Note that the shell prompt appears with the output from the child, because two processes (login shell and the child) are writing to the terminal. The parent process ID of the child has become 1.
- After calling
pr_ids
in the child, the program tries to read from standard input. POSIX.1 specifies that theread
is to return an error witherrno
set toEIO
(whose value is 5 on this system) in this situation. [p309]- As discussed earlier in this chapter, when a process in a background process group tries to read from its controlling terminal,
SIGTTIN
is generated for the background process group. But for an orphaned process group, if the kernel were to stop it with this signal, the processes in the process group would probably never be continued.
- As discussed earlier in this chapter, when a process in a background process group tries to read from its controlling terminal,
- Finally, our child was placed in a background process group when the parent terminated, since the parent was executed as a foreground job by the shell.
FreeBSD Implementation¶
The figure below shows the FreeBSD implementation of sessions and process groups:
session
structure is allocated for each session (each timesetsid
is called).s_count
: number of process groups in the session. When this counter is decremented to 0, the structure can be freed.s_leader
: pointer to the proc structure of the session leader.s_ttyvp
: pointer to the vnode structure of the controlling terminal.s_ttyp
: pointer to the tty structure of the controlling terminal.s_sid
: session ID. (Not part of the Single UNIX Specification)
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.
tty
structure is contained in the kernel for each terminal device and each pseudo terminal device.t_session
points to thesession
structure that has this terminal as its controlling terminal. This pointer is used by the terminal to send a hangup signal to the session leader if the terminal loses carrier (Figure 9.7). Note that thetty
andsession
structure point to each other.t_pgrp
points to thepgrp
structure of the foreground process group. This field is used by the terminal driver to send signals to the foreground process group. The three signals generated by entering special characters that are sent to the foreground process group are:- interrupt
- quit
- suspend
t_termios
is a structure containing all the special characters and related information for this terminal, such as baud rate, whether echo is enabled, and so on.t_winsize
is awinsize
structure that contains the current size of the terminal window. When the size of the terminal window changes, theSIGWINCH
signal is sent to the foreground process group.
The kernel finds the foreground process group of a particular session by following fields of pointers, starting with the session
structure:
- Follow
s_ttyp
of thesession
structure to get to thetty
structure (controlling terminal). -
Follow
t_pgrp
of thetty
structure to get to thepgrp
structure (foreground process group). -
pgrp
structure contains the information for a particular process group.pg_id
is the process group ID.pg_session
points to thesession
structure for the session to which this process group belongs.pg_members
is a pointer to the list ofproc
structures that are members of this process group. Thep_pglist
structure in thatproc
structure is a doubly linked list entry that points to both the next process and the previous process in the group. [p311]
vnode
structureis allocated when the controlling terminal device is opened. All references to/dev/tty
in a process go through thisvnode
structure.
Summary¶
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¶
Verbatim¶
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.
Solution:
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.
- Stack Overflow
- Launching Jobs in the GNU C Library