View on GitHub

blog

Stay hungry. Stay foolish.

进程ID及进程间的关系

进程ID

进程相关的 ID 有多种,除了进程标识 PID 外,还包括:线程组标识 TGID,进程组标识 PGID,回话标识 SIDTGID/PGID/SID 分别是相关线程组长/进程组长/回话 leader 进程的 PID

下面分别介绍这几种ID。

PID

TGID

PGID

enum pid_type
{
	PIDTYPE_PID,
	PIDTYPE_TGID,
	PIDTYPE_PGID,
	PIDTYPE_SID,
	PIDTYPE_MAX,
};

SID

PID/TGID/PGID/SID总结

用一幅图来总结 PID/TGID/PGID/SID

进程ID

进程间关系

内核中所有进程的 task_struct 会形成多种组织关系。根据进程的创建过程会有亲属关系,进程间的父子关系组织成一个进程树;根据用户登录活动会有会话和进程组关系。

亲属关系

进程通过 fork() 创建出一个子进程,就形成来父子关系,如果创建出多个子进程,那么这些子进程间属于兄弟关系。可以用 pstree 命令查看当前系统的进程树。

$ pstree -p
systemd(1)─┬─ModemManager(759)─┬─{ModemManager}(802)
           │                   └─{ModemManager}(806)
           ├─NetworkManager(685)─┬─{NetworkManager}(743)
           │                     └─{NetworkManager}(750)
           ├─acpid(675)
           ├─agetty(814)
           ├─avahi-daemon(679)───avahi-daemon(712)
           ├─bluetoothd(680)
           ├─canonical-livep(754)─┬─{canonical-livep}(1224)
           │                      ├─{canonical-livep}(1225)
           │                      ├─{canonical-livep}(1226)
...

进程描述符 task_structparent 指向父进程,children指向子进程链表的头部,sibling 把当前进程插入到兄弟链表中。

通常情况下,real_parentparent 是一样的。如果在 bash 上使用 GDB 来 debug 一个进程,这时候进程的 parentGDB ,进程的 real_parentbash

pstree

当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源。而子进程在结束时,会向它的父进程发送 SIGCHLD 信号。因此父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。

如果父进程提前结束,那么子进程将把1号进程 init 作为父进程。总之,进程都有父进程,负责进程结束后的资源回收。在子进程退出且父进程完成回收前,子进程变成僵尸进程。僵尸进程持续的时间通常比较短,在父进程回收它的资源后就会消亡。如果父进程没有处理子进程的终止,那么子进程就会一直处于僵尸状态。

会话、进程组关系

Linux 系统中可以有多个会话(session),每个会话可以包含多个进程组,每个进程组可以包含多个进程。

会话是用户登录系统到退出的所有活动,从登录到结束前创建的所有进程都属于这次会话。登录后第一个被创建的进程(通常是 shell),被称为 会话 leader

进程组用于作业控制。一个终端上可以启动多个作业,也就是进程组,并能控制哪个作业在前台,前台作业可以访问终端,哪些作业运行在后台,不能读写终端。

我们来看一个会话和进程组的例子。

$ cat | head
hello
hello
^Z
[1]+  已停止               cat | head
$ ps j | more
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
   1522    1532    1532    1532 pts/0       1762 Ss    1000   0:00 -bash
   1532    1760    1760    1532 pts/0       1762 T     1000   0:00 cat
   1532    1761    1760    1532 pts/0       1762 T     1000   0:00 head
   1532    1762    1762    1532 pts/0       1762 R+    1000   0:00 ps j
   1532    1763    1762    1532 pts/0       1762 S+    1000   0:00 more

上面的命令通过 cat | head 创建了第一个进程组,包含 cathead 两个进程。这时这个作业是前台任务,可以控制终端。当我们按下 Ctrl + z,会发送信号 SIGTSTP 给前台进程组的所有进程,该信号的缺省行为是暂停作业执行。暂停的作业会让出终端,并且进程不会再被调度,直到它们收到 SIGCONT 信号恢复执行。

然后我们通过 ps j | more 创建了另一个进程组,包含 psmore 两个进程。ps 的参数 j 表示用任务格式显示进程。输出中的 STAT 列是进程的状态码,前面的大写字母表示进程状态,我们可以从 psman page 查看其含义:

D    uninterruptible sleep (usually IO)
I    Idle kernel thread
R    running or runnable (on run queue)
S    interruptible sleep (waiting for an event to complete)
T    stopped by job control signal
t    stopped by debugger during the tracing
W    paging (not valid since the 2.6.xx kernel)
X    dead (should never be seen)
Z    defunct ("zombie") process, terminated but not reaped by its parent

某些进程除了大写字母代表的进程状态,还跟着一个附加符号:

从输出可以看出,bash 是这个会话的 leader 进程,它的 PIDPGIDSID 相同,都是1532 。这个会话其他所有进程的 SID 也都是 1532

cat | head 进程组的 PGID1760ps j | more 进程组的 PGID1762。用管道连接的进程包含在同一个进程组中,每个进程组内第一个进程成为 Group Leader,并以 Group Leader 的 PID 作为组内进程的 PGID

会话有一个前台进程组,还可以有一个或多个后台进程组,只有前台作业可以从终端读写数据。示例的进程组关系如图:

session

注意到上图中显示,终端设备可以向进程组发送信号。我们可以在终端输入特殊字符向前台进程发送信号:

只有前台进程可以从终端接收输入,也只有前台进程才被允许向终端输出。如果一个后台作业中的进程试图进行终端读写操作,终端会向整个作业发送 SIGTTOUSIGTTIN 信号,默认的行为是暂停进程。

当终端关闭时,会向整个会话发送 SIGHUP 信号,通常情况下,这个会话的所有进程都会被终止。如果想让运用在后台的进程不随着 session 的结束而退出,可以使用 nohup 命令忽略 SIGHUP 信号:

$ nohup command >cmd.log 2>&1 &

即使 shell 结束,运行于后台的进程也能无视 SIGHUP 信号继续执行。另外一个方法是可以让进程运行在 screentmux 这种终端多路复用器(terminal multiplexer)中。