linux进程组,会话,控制终端关系

本文最后更新于:2025年11月19日 下午

从思考题开始

先打开一个终端,运行如下命令:

1
2
$ ping www.baidu.com &
$ (ping www.qq.com &)

我们可以看到两个进程都在后台运行,ping结果都打印到当前终端。
如果此时我关闭当前终端,这两个ping程序会如何。

结论:
ping baidu会停止。
ping qq会持续运行,但是没有输出。

进程组、会话、控制终端

进程组

每个进程都属于一个进程组,进程组是一个或多个进程的集合。进程组一般也可理解为作业。
进程组有一个组长进程,组长进程的ID也是进程组ID。组长创建该组中的进程后可以退出,进程组中只要有一个进程存在,进程组就存在,和组长进程是否退出无关。

会话

会话由一个或多个进程组组成。进程可以调用setsid函数建立一个新的会话。
调用setsid的进程如果不是一个进程组的组长,那么创建会话成功,否则失败。所以创建守护进程前会先fork,然后在子进程中调用setsid。
setsid创建新会话成功后,它会做如下的事情:
(1)该进程新会话的会话首进程(session leader)
(2)该进程成为一个新进程组的组长进程。
(3)该进程脱离控制终端

控制终端

(1)一个会话可以有一个控制终端,这个控制终端通常是终端设备(终端登录),或者伪终端设备(网络登录)。
(2)建立与终端连接的会话首进程成为控制进程
(3)一个会话中的进程组分为前台进程组和后台进程组
(4)一个会话如果有控制终端,那么它是一个前台进程组,其他的为后台进程组
(5)终端发出的信号(如ctrl + c, ctrl + z)将送到前台进程组的所有进程

后台进程组与控制终端

后台进程组如何与控制终端交互呢?
如果后台进程组要从终端读取输入,那么会产生SIGTTIN信号,默认动作时暂停该进程。串口会有打印,表明该进程已暂停。我们可通过作业控制把该进程放回前台。
如下:(fg是作业控制命令)

1
2
3
4
5
6
$ read &      
[1] 72715
[1] + 72715 suspended (tty input) read
$ fg
[1] + 72715 continued read
fdasfdsf

如果后台进程要从终端输出,默认会直接输出到会话的控制终端。如果会话禁止了后台作业输出至控制终端(命令:stty tostop),那么进程会收到SIGTTOU信号,默认动作是暂停。

1
2
3
4
5
6
7
$ stty tostop 
$ echo 123 &
[3] 75041
[3] + 75041 suspended (tty output) echo 123
$ fg
[3] 75041 continued echo 123
123

stty -tostop 可以开启后台作业输出到控制终端,默认就是开启的
如果**后台进程组要设置控制终端,那么也会遇到SIGTTOU信号(也有可能是SIGSTOP信号)**,默认动作也是暂停该进程。
比如top命令就会设置终端。

1
2
3
$ top -n1 > /dev/null &
[3] 88773
[3] + 88773 suspended (signal) top -n1 > /dev/null

孤儿进程组

进程组中的所有进程的父进程都不在所属的会话中,那么这个进程组就是孤儿进程组。
因为孤儿进程组的父进程(init进程,ubuntu中为systemd进程)不在它所属的会话中,所以如果孤儿进程暂停后,会导致没有人去唤醒它。所以给孤儿进程发送SIGTTIO,SIGTTOU,SIGSTOP都没有意义。

  • 如果孤儿进程要读终端、设置终端、关闭后台进程组输出到会话的控制终端后写终端,都会产生EIO错误,而不会产生信号使进程暂停。

关系图

  • 写终端产生SIGTTOU这里指:设置终端或者关闭后台进程组输出到会话的控制终端后写终端。

回到开头

1
2
$ ping www.baidu.com &
$ (ping www.qq.com &)

运行两条ping之后的进程关系如下:

1
2
3
4
5
6
7
8
$ ps -o tty,pid,ppid,pgid,sid,cmd
TT PID PPID PGID SID CMD
pts/1 31034 11477 31034 31034 zsh
pts/1 74391 31034 74391 31034 zsh
pts/1 74479 31034 74479 31034 zsh
pts/1 92787 31034 92787 31034 ping www.baidu.com
pts/1 92839 6653 92838 31034 ping www.qq.com
pts/1 93025 31034 93025 31034 ps -o tty,pid,ppid,pgid,sid,cmd

两条ping命令都属于同一个会话31034,都拥有控制终端pts/1。两者属于不同的后台进程组。ping qq的父进程是6653(systemd),它所在的进程组为孤儿进程组。
关闭当前终端后,zsh退出产生SIGHUP信号给子进程,子进程链式产生SIGHUP给子子进程,SIGHUP默认动作是退出,所以当前会话内有亲缘关系的进程都会退出。
因为ping qq进程的父进程不在当前会话内,所以它不会退出
另外开一个终端,查看到ping qq进程如下:

1
2
TT           PID    PPID    PGID     SID CMD
? 92839 6653 92838 31034 ping www.qq.com

只有关联的终端没有了,strace跟踪它的话,会发现,它的输出都会产生EIO错误。

1
write(1, "64 bytes from 240e:97c:2f:2::4c "..., 86) = -1 EIO (输入/输出错误)

编写守护进程

守护进程的主要意义在于摆脱控制终端的影响。
一般的编写方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/syslog.h>
#include <unistd.h>

int main(int argc, char *argv[])
{

pid_t pid, sid;
int i;

openlog("daemon_syslog", LOG_PID, LOG_DAEMON);
syslog(LOG_INFO, "run...");

pid = fork(); // 第1步
if (pid < 0)
exit(-1);
else if (pid > 0)
exit(0); // 父进程第一次退出

if ((sid = setsid()) < 0) // 第2步
{
syslog(LOG_ERR, "%s\n", "setsid");
exit(-1);
}

// 第3步 第二次父进程退出
if ((pid = fork()) > 0)
exit(0);
if ((sid = chdir("/")) < 0) // 第4步
{
syslog(LOG_ERR, "%s\n", "chdir");
exit(-1);
}
umask(0); // 第5步
// 第6步:关闭继承的文件描述符
for (i = 0; i < getdtablesize(); i++)
{
close(i);
}
while (1)
{
// do_something();
}
closelog();
exit(0);
}

讲解:

  • 因为守护进程没有控制终端,标准输入输出都会关闭, 所以一般采用syslog进行输出。
  • 第一次fork,可以确保进程不是进程组的组长进程。这是调用setsid的前提。
  • setsid可以脱离当前的控制终端,创建新的会话,但是当前进程为会话的首进程。
  • 为防止程序再次手动打开的终端成为控制终端,我们进行了第二次fork。因为会话首进程打开的终端才有可能变为控制终端。这一步也可以省略。
  • 更改工作目录为’/‘,避免当前工作在其他目录上(如usb目录),导致挂载的文件系统无法卸载。
  • 设置umask,继承的umask可能不是我们想要的。
  • 关闭所有继承的描述符。

如上步骤创建守护进程看起来很长,有一个库函数daemon可以简化。

daemon函数

1
2
#include <unistd.h>
int daemon(int nochdir, int noclose);
  • nochdir如果为0,则将工作目录设置为’/‘。否则不会更改。
  • noclose如果为0,则将标准输入,标准输出,标准错误输出重定向到/dev/null。否则不会更改。

注意:
根据MAN文档说明,GNU C库的daemon实现来自BSD,它只fork一次。而不是fork两次。即调用返回后,当前进程是新会话的首进程。在遵循 System V 语义的系统(例如 Linux)上,这意味着如果守护程序打开的终端还不是另一个会话的控制终端,则该终端将无意中成为守护程序的控制终端。


人生苦短,远离bug Leon, 2024-06-09

linux进程组,会话,控制终端关系
https://leon0625.github.io/2024/06/09/9f0cd45aea89/
作者
leon.liu
发布于
2024年6月9日
许可协议