「Linux 系统编程」进程组、会话、守护进程

1 进程组(Process Group)

1.1 进程组概念

进程组 (Process Group) 是一个或多个进程的集合。有时也被称为作业。

进程组 为了更方便地管理多个相关的进程,特别是实现对一组进程的信号发送和作业控制(如 fg, bg 命令)。


1.2 进程组特性

  1. 每个进程都属于一个且只属于一个进程组。

  2. 每个进程组都有一个唯一的进程组 ID (PGID)。这个 ID 在数值上 等于进程组的组长进程 (Process Group Leader) 的进程 ID (PID)。

  3. 父进程创建子进程时,子进程默认继承父进程的进程组 ID,即 父子同属一个进程组

  4. 进程组的生命周期独立于组长进程。只要组内还有任何一个进程存在,该进程组就存在,不会被销毁。


1.3 进程组操作

  • 杀死整个进程组kill -SIGKILL -<PGID>-PGID 负号告诉 kill 命令这是一个进程组 ID。

  • 设置进程组 ID:一个进程可以使用 setpgid() 函数来将自己或其子进程加入到一个现有的进程组或创建一个新的进程组。



2 会话(Session)

2.1 会话的概念

会话(Session) 是一个更高层次的抽象,它是一个或多个进程 的集合。

  • 一个会话包含一个或多个进程组。
  • 一个会话 通常与一个控制终端关联(如 /dev/tty1, /dev/pts/0)。
  • 一个会话中,同一时间 只能有一个进程组在前台运行可以与终端交互;其他进程组则在后台运行。

2.2 会话示例分析

在上图的示例中,先打开一个终端,输入了 cat | cat | cat | wc -l 命令;然后打开了一个新的终端,输入了 ps ajx 命令。

  1. 会话与控制终端关联

    可以看到系统创建了两个独立的会话(SID=15734SID=15761),分别对应两个控制终端 pts/0pts/1。每个会话 由一个会话首进程(通常是 bash)管理,并包含一个或多个进程组。

    • 会话 1(SID=15734:控制终端为 pts/0。会话首进程是 bash(PID=15734),其 SID 与 PID 相同,表明它是该会话的首进程。

    • 会话 2(SID=15761:控制终端为 pts/1。会话首进程是另一个 bash(PID=15761),其 SID 与 PID 相同。

    每个会话都有一个唯一的控制终端。控制终端是进程与用户交互的桥梁,负责处理输入/输出和信号。


  1. 会话内的进程组

    会话 1(pts/0 中:

    进程组 1PGID=15734):包含会话首进程 bashPID=15734)。这是一个 后台进程组(STAT 为 Ss,没有 +),因为 当前终端被其他进程组占用

    进程组 2PGID=15746):包含多个 cat 进程(PID=15746, 15747, 15748)和一个 wc -l 进程(PID=15749)。这些进程的 STAT 为 S+,表示它们处于 睡眠状态 且属于 前台进程组。这意味着正在 pts/0 终端上运行一个管道命令(cat | cat | cat | wc -l),该命令 占据了控制终端 pts/0

    所有进程的 SID 都是 15734,表明它们属于同一个会话。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    会话 (SID = bash的PID)
    |
    ├── 控制终端: /dev/pts/0
    |
    ├── 进程组 (PGID = bash的PID) [后台进程组]
    | |
    | └── 进程: bash
    |
    └── 进程组 (PGID = ls命令的PID) [前台进程组]
    |
    ├── 进程: cat
    ├── 进程: cat
    ├── 进程: cat
    └── 进程: wc -l

    会话 2(pts/1 中:

    进程组 3PGID=15761):包含会话首进程 bashPID=15761)。这是一个后台进程组(STAT 为 Ss),因为 当前终端被 ps 进程占用

    进程组 4PGID=15769):包含 ps ajx 进程(PID=15769)。STAT 为 R+,表示它 正在运行 且是 前台进程组。正在 pts/1 终端上执行 ps ajx 命令。

    所有进程的 SID 都是 15761,表明它们属于同一个会话。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    会话 (SID = bash的PID)
    |
    ├── 控制终端: /dev/pts/1
    |
    ├── 进程组 (PGID = bash 的PID) [后台进程组]
    | |
    | └── 进程: bash
    |
    └── 进程组 (PGID = ps 命令的PID) [前台进程组]
    |
    └── 进程: ps ajx

  2. 会话首进程和进程组组长

    • 会话首进程的 PID 通常等于 SID。例如,PID=15734 的 bash 是会话 1 的首进程,PID=15761 的 bash 是会话 2 的首进程。

    • 进程组组长的 PID 通常等于 PGID。例如,在会话 1 中,PGID=15746 的进程组长是第一个 cat 进程(PID=15746)。


  1. 前台和后台进程组

    • 在每个会话中,只有一个进程组可以是前台进程组(STAT 有 +),其他进程组是后台进程组。

      • 在会话 1 中,进程组 15746 是前台进程组,正在执行管道命令。

      • 在会话 2 中,进程组 15769 是前台进程组,正在执行 ps 命令。

    • 用户可以通过 shell 命令(如 fg、bg)在前后台之间切换进程组。


2.3 创建会话函数 setsid()

  • getsid() 函数

获取当前进程的会话 ID

1
pid_t getsid(pid_t pid);
  • setsid() 函数

创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID

1
pid_t setsid();    

创建会话函数使用注意事项

  1. 调用者不能是进程组组长。如果调用者是组长,setsid() 会失败。这是创建新会话的关键一步,目的是让调用进程“脱离”原有的组织关系。

  2. 调用 setsid() 成功后,该进程会:

    • 成为一个 新会话的首进程 (Session Leader);
    • 成为一个新进程组的组长
    • 脱离原来的控制终端(新的会话 没有控制终端)。
  3. 通常需要一定的权限(但许多现代 Linux 发行版如 Ubuntu 允许普通用户创建会话)。



3 守护进程及创建

3.1 守护进程概念

守护进程 (Daemon Process) 是一种 长期运行的后台服务进程独立于控制终端。它通常随着系统启动而启动,在系统关闭时才终止。

特点

  • 名字通常以 d 结尾(如 sshd, httpd, systemd)。
  • 不受用户登录和注销的影响。
  • 通常在后台运行没有控制终端不会意外接收来自终端的信号(如 SIGHUP)。

3.2 守护进程创建步骤

  1. 创建子进程,父进程退出fork() 后让父进程立即退出。这样子进程变为孤儿进程,被 init/systemd 接管,同时 shell 会认为命令已执行完毕。

  2. 子进程创建新会话:子进程调用 setsid(),成为新会话的首进程和新进程组的组长,从而完全脱离原有的控制终端。

  3. 改变工作目录:使用 chdir("/") 或某个安全目录。这是为了防止守护进程的工作目录阻止系统卸载其所在的文件系统(如 /tmp/home)。

  4. 重设文件权限掩码:使用 umask(0)umask(022)。这将影响守护进程后续创建文件的默认权限,使其拥有更大的灵活性。

  5. 关闭/重定向文件描述符

    • 关闭从父进程继承的所有不需要的文件描述符。
    • 标准输入、标准输出、标准错误(文件描述符 0, 1, 2)重定向到 /dev/null 或其他日志文件。这是为了防止守护进程意外地从控制终端读/写数据。

守护进程的核心逻辑:实现守护进程需要执行的任务,通常是一个无限循环。


3.3 守护进程创建代码示例

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
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void sys_err(const char *str) {
perror(str);
exit(1);
}

int main() {
pid_t pid;
int ret, fd;

// 1. 创建子进程,父进程退出
pid = fork();
if (pid > 0) {
exit(0);
}

// 2. 子进程创建新会话
pid = setsid();
if (pid == -1) {
sys_err("setsid error");
}

// 3. (可选)改变工作目录
ret = chdir("/home/marisa/testDir");
if (ret == -1) {
sys_err("chdir error");
}

// 4. 重设文件权限掩码
umask(0022); // 新文件权限将是 755 (rwxr-xr-x)

// 5. 关闭/重定向文件描述符
close(STDIN_FILENO); // 关闭标准输入(0)

// 打开 /dev/null
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
sys_err("open error");
}
// 将标准输出(1)和标准错误(2)重定向到 /dev/null
// 这样任何printf或perror输出都会被丢弃,不会干扰终端
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 6. 守护进程的核心工作循环
while (1) {
// 执行守护进程的实际工作,例如监听网络端口、处理文件等。
sleep(1);
}

return 0;
}

编译并执行程序后,输入 ps ajx 命令可能看到的守护进程如下:

1
1859  16007  16007  16007 ?   -1  Ss   1000   0:00 ./session_test

可以看到,这个守护进程没有关联的控制终端(TTY 字段显示 ?)。只能用 kill <PID>killall mydaemon 来杀死它。


「Linux 系统编程」进程组、会话、守护进程
https://marisamagic.github.io/2025/09/13/20250913/
作者
MarisaMagic
发布于
2025年9月13日
许可协议