李秋豪 阅读(100) 评论(0)

在类Unix系统中,用户通常会跟各种相关的进程打交道。虽然在登录的时候只有一个终端进程(用户对应的登录shell ,通过这个shell启动各种程序和服务),但通常不久以后就会产生许多相关的进程,例如进行如下动作:

  • 在后台运行无交互的程序(例如bash命令中末位的"&")
  • 通过shell的 job control在各种交互进程之间切换
  • 通过管道启动一组程序
  • 在图形环境下(例如X window system)启用多个终端窗口

为了管理这些进程,内核便对这些进程进行了分组,称其为进程组,几个进程组又构成一个会话。例如下图所示,lsless在一个进程组里,而grepwc在一个进程组里。这两个进程组又同属于一个会话。

下面具体讲一讲进程组和会话。


1.1 进程组

每一个进程都属于一个“进程组”,当一个进程被创建的时候,它默认是其父进程所在组的成员。传统上,一个进程的组ID(pgid)等于这个组的第一个成员(也称为进程组领导)。

可以使用ps j这个命令获取进程的PPID (父进程ID), PID (本进程 ID), PGID (进程组 ID) and SID (会话 ID)。

frank@under:~$ ps j
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
24120 24125 24125 24125 pts/5    24125 Ss+   1000   0:00 bash
24120 30633 30633 30633 pts/6    31468 Ss    1000   0:00 bash
30633 31468 31468 30633 pts/6    31468 R+    1000   0:00 ps j

当使用不具有工作管理(job control)的shell时(例如ash ),每一个shell创建的子进程都会和shell在同一个进程组和会话里。当使用具有工作管理的shell时(例如bash ),如果使用管道(参考:Pipes: A Brief Introduction), 那么这些管道连接起来的进程单独组成一个进程组,和shell在一个会话中。例如:

% cat paper | ideal | pic | tbl | eqn | ditroff > out

这几个程序运行后的进程都在一个进程组里面。

前台进程组

每一个会话最多有一个进程组是“前台进程组”,控制终端(下面的第二部分会讲)会将输入和信号 传给该进程组的成员(例如你在终端按下Ctrl+C就会向前台进程组发送SIGINT信号)。进程可以通过系统调用 tcgetpgrp(fd)获取所在会话的前台进程组ID,其中fd对应会话的控制终端的文件描述符;也可以通过tcsetpgrp(fd,pgrp)设置所在会话的前台进程组,其中fd对应会话的控制终端的文件描述符, pgrp是这个会话中的一个进程组。

那么如何得到fd呢? ctermid() 调用会返回控制终端的名字,在符合 POSIX标准的系统上,它会返回 /dev/tty 这个文件,然后我们就可以用系统调用open()打开这个文件从而得到文件描述符了。

后台进程组

在一个会话中,除前台进程组外的进程组都称为“后台进程组”,这些后台进程组的进程不参与终端的输入输出,如果它们尝试从终端读取数据,会收到SIGTTIN信号(其默认操作是Stop),同时终端会通知用户:

SIGTTIN   21,21,26    Stop    Terminal input for background process

但是如果后台进程忽略或者blockSIGTTIN信号了,或者它所在的进程组是一个“孤儿进程组”(下面有讲),那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTIN信号。当后台进程尝试向终端写操作时,它可能会受到SIGTTOU信号:

SIGTTOU   22,22,27    Stop    Terminal output for background process

同样地,如果后台进程忽略或者blockSIGTTOU信号了,或者它所在的进程组是一个“孤儿进程组”,那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTOU信号。

为了让后台进程加入前台进程组,可以使用fg命令。

相应操作

可以通过系统调用 setpgid()将进程加入到另一个进程组中:

int setpgid(pid_t pid, pid_t pgid);

其中pid是要操作的进程,0代表本进程;pgid是要加入的进程组,0代表要加入的进程组的pgid是这个进程的pid(也就是说,这个组的组领导就是这个进程)。

使用 setpgid() 要注意以下几点:

  1. 一个进程可能将pgid设置为自己或者它所在组的其他成员,这样的操作可能不会改变其他任意进程的进程组,即使这个进程有root权限。
  2. 会话头进程(第二部分会介绍)不能改变自己所在的进程组。
  3. 一个进程不能被加入到另一个会话中的进程组中,换句话说,setpgid只能在一个会话中使用。由于setpgid()只能将进程在本会话中“移动”,所以两个会话不可能有相同的进程组或者进程。

一个进程可以通过 getpgrp()系统调用获得自己所在组的ID,也可以通过 getpgid(p) 获得pid为p的进程所在组的组ID,当p为0时,获得本进程的组ID:

pid_t getpgid(pid_t pid)
pid_t getpgrp(void)

下面是一个简单的示例图:


断开连接(我对于Linux下的终端、shell通信机制不了解,这个地方暂时贴上参考资料里的原文,以后懂了再翻译)

If the terminal goes away by modem hangup, and the line was not local, then a SIGHUP is sent to the session leader. Any further reads from the gone terminal return EOF. (Or possibly -1 with errno set to EIO.)

If the terminal is the slave side of a pseudotty, and the master side is closed (for the last time), then a SIGHUP is sent to the foreground process group of the slave side.

When the session leader dies, a SIGHUP is sent to all processes in the foreground process group. Moreover, the terminal stops being the controlling terminal of this session (so that it can become the controlling terminal of another session).

Thus, if the terminal goes away and the session leader is a job control shell, then it can handle things for its descendants, e.g. by sending them again a SIGHUP. If on the other hand the session leader is an innocent process that does not catch SIGHUP, it will die, and all foreground processes get a SIGHUP.


1.2 孤儿进程组

现在我们来讨论当会话消失的时候进程是如何终止的。

假设有一个在终端下运行的会话,其会话头是一个shell。当这个shell存在时,会话中的进程组处于不同的环境中,可能在运行,也可能被挂起了。当终端关闭时,如果它正在运行,当终端关闭后它就无法读入或者输出了;如果它被挂起了,则它可能永远不会被唤醒(也不会终止)。在这种情况下,原会话的进程组就被称为“孤儿进程组”。POSIX定义为该进程组的父进程也是该进程组的成员或者是别的会话的成员。总之,只要一个进程组的父进程在同一会话的不同组中,它就不是孤儿进程组。

当一个进程组成为孤儿进程组之后,在这个进程组中的每一个进程都会被发送一个SIGHUP信号——通常进程将会被正常关闭。对于收到SIGHUP信号后,选择不终止的程序将会被发送一个SIGCONT,这个信号将会重启任何被挂起的进程。这个信号流程能够关闭大多数的进程并保证剩下的是正在运行的进程(不被挂起)。

当进程被“遗弃”后,它被强制和控制终端分离(其他的用户便可以使用这个终端)。原来的会话ID继续被保留(不作为新生进程的PID)直到每一个会话中的程序退出。


2.1 会话

当一个用户注销的时候,内核会终止用户之前启动的所有进程(不然这些进程会在那一直运行并等待输入的到来)。为了简化这个任务,内核将几个进程组并为一个“会话”。会话的ID就是通过setsid()启动这个会话的进程的PID (也就是这个会话的第一个进程,通常是用户的shell),这个进程也称为“会话头”,它随后产生的所有子孙进程都默认在这个会话里。

进程也可以使用setsid使自己离开自己的会话,其参数为空,返回新的会话的ID:

#include <unistd.h>

pid_t setsid(void);


2.2 控制终端

每一个会话有且仅有一个对应的终端,会话中的进程从这个终端得到输入并输出,该终端被称为“控制终端”(controlling terminal)。这个终端可能是机器本地的控制台、桌面环境的伪终端、网络上的伪终端等等。

虽然一个会话对应的控制终端是可以改变的,但这通常都是由初始化用户登录环境的那个进程设定的。




主要参考:

  1. The Process Model of Linux Application Development
  2. Processes (里面有关于进程和线程的简略介绍)
  3. 《深入理解计算机系统》第三版