《Linux网络编程·加餐》进程间关系与守护进程

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、进程组

[1.1 什么是进程组](#1.1 什么是进程组)

[1.2 组长进程](#1.2 组长进程)

[1.2.1 进程组组长的作用及生命周期](#1.2.1 进程组组长的作用及生命周期)

[1.3 补充:ps 命令选项 -e/-o](#1.3 补充:ps 命令选项 -e/-o)

二、会话

[2.1 什么是会话](#2.1 什么是会话)

[2.2 Linux Session(会话)与登录完整过程分析](#2.2 Linux Session(会话)与登录完整过程分析)

[2.3 补充:grep 命令选项 -E](#2.3 补充:grep 命令选项 -E)

[2.4 会话 ID(SID)](#2.4 会话 ID(SID))

三、控制终端

[3.1 什么是控制终端](#3.1 什么是控制终端)

[3.2 会话、进程组和控制终端关系](#3.2 会话、进程组和控制终端关系)

[3.3 关键易错点及关系链路总结](#3.3 关键易错点及关系链路总结)

四、作业控制

[4.1 什么是作业 job](#4.1 什么是作业 job)

[4.2 什么是作业控制 Job Control](#4.2 什么是作业控制 Job Control)

[4.3 作业号](#4.3 作业号)

[4.3.1 后台命令 & 的本质](#4.3.1 后台命令 & 的本质)

[4.3.2 默认作业 + 与 −](#4.3.2 默认作业 + 与 −)

[4.4 作业状态](#4.4 作业状态)

[4.5 作业的挂起与切回](#4.5 作业的挂起与切回)

[4.5.1 作业挂起 Ctrl+Z](#4.5.1 作业挂起 Ctrl+Z)

[4.5.2 作业切回 fg 命令](#4.5.2 作业切回 fg 命令)

[4.6 作业控制相关的信号](#4.6 作业控制相关的信号)

五、守护进程

[5.1 什么是守护进程](#5.1 什么是守护进程)

[5.2 守护进程的核心条件](#5.2 守护进程的核心条件)

[5.3 如何创建会话 setsid ()](#5.3 如何创建会话 setsid ())

[5.3.1 为什么不能由进程组组长调用 setsid?](#5.3.1 为什么不能由进程组组长调用 setsid?)

[5.4 守护进程代码设计](#5.4 守护进程代码设计)

[5.4.1 为什么存在 nochdir 参数?](#5.4.1 为什么存在 nochdir 参数?)

[5.4.2 为什么存在 noclose 参数?](#5.4.2 为什么存在 noclose 参数?)

[补充:/dev/null 文件](#补充:/dev/null 文件)

结束语


前言

在 Linux 系统编程中,进程组、会话、控制终端与作业控制,是理解 Shell 交互逻辑、后台任务、守护进程的底层基础。很多人日常熟练使用&后台运行、Ctrl+Z挂起、fg/bg切换作业,却并不清楚这些命令在内核层面如何运作。

本篇将从进程组的基础概念入手,讲解进程组长的生命周期,结合ps实操命令观察进程组信息;接着延伸到会话 Session,拆解 SSH 登录时会话的完整建立流程,搞懂会话首进程与 SID 的含义;再梳理会话‑进程组‑控制终端三者之间的关联,理清前台、后台进程组的划分逻辑,解释终端快捷键产生信号的底层原理。之后围绕作业控制,讲解作业号、后台&的本质、作业状态切换,剖析Ctrl+CCtrl+Z等按键对应的信号行为。最后落脚到守护进程,解析setsid()创建会话的约束条件,一步步完成守护进程代码实现,解释nochdirnoclose参数以及/dev/null的作用。

读完本文,你可以打通 Shell 交互底层逻辑,不仅知其命令用法,更能理解内核层面的模型,为信号、进程编程、守护进程开发打下扎实理论基础。

一、进程组

1.1 什么是进程组

在之前的学习中我们已经了解了进程的基本概念,每一个进程都有一个唯一的进程 ID(PID)用来标识自身。但实际上,除了 PID 之外,每个进程还归属于一个进程组(PGID)

进程组一个或者多个进程的集合一个进程组可以包含多个进程 。每一个进程组也有一个唯一的进程组 ID(PGID),PGID 和 PID 类似,同样是一个正整数,可以存放在pid_t数据类型中。

在 Linux 内核中,task_struct结构体里有pgrp字段记录进程所属的进程组 ID,进程组是内核进行进程批量管理的基本单位。

1.2 组长进程

每一个进程组都有一个组长进程 ,组长进程的 ID 等于其进程 ID,也就是说组长进程的 PID 和 PGID 是相同的。我们可以通过ps命令观察到组长进程的现象:

bash 复制代码
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat

输出结果:

bash 复制代码
PID   PGID  PPID COMMAND
2806  2806  2805 bash
2880  2880  2806 ps
2881  2880  2806 cat

从结果上看,ps进程的 PID(2880)和 PGID(2880)相同,说明**ps进程是该进程组的组长进程** ,该进程组包括pscat两个进程。

在 Shell 中执行管道命令 时,Shell 会为整条管道创建一个新的进程组 ,管道中的第一个命令通常成为组长进程,所以这也是为什么 ps 的PID 和 PGID 相同,并且 ps 的 PGID 和 cat 的 PGID 相同的原因。

1.2.1 进程组组长的作用及生命周期

进程组组长的作用

  • 进程组组长可以创建一个进程组,也可以创建该组中的其他进程。
  • 组长进程是进程组的创建者,但它并不拥有对组内其他进程的特殊控制权,组内进程之间是平等的。

进程组的生命周期

  • 进程组从创建开始,到其中最后一个进程离开为止。
  • **注意:只要某个进程组中还有一个进程存在,该进程组就存在,这与其组长进程是否已经终止无关。**组长进程退出后,进程组依然存在,剩余进程的 PGID 不会发生变化,直到组内最后一个进程退出,进程组才会被内核销毁。

为什么即使进程组组长都被终止了这个进程组还能存在呢?原因后面讲解了setsid函数时会进行解释。

1.3 补充:ps 命令选项 -e/-o

ps 作为 Linux 用来查看进程快照的工具,‑e‑o 是非常高频的组合参数,常用于排查进程 PID、PGID、PPID 等属性。

-e(等价于 -A

  • 全称:every ,显示系统内全部进程
  • 默认不加 -eps 默认只输出当前终端会话下启动的进程,很多后台进程看不到。
  • 加上 -e:遍历系统所有进程,不管属于哪个用户、哪个终端,全部展示出来。

示例:ps -e,打印整机全部进程。

-o:自定义输出字段(格式化输出)

  • ‑o 允许手动指定需要打印的列,以逗号,分隔多个字段,逗号前后可以加空格
  • 语法格式:
bash 复制代码
ps -o 字段1,字段2,字段3
  • 示例中:ps -eo pid,pgid,ppid,comm
    • pid:进程 ID
    • pgid:进程组 ID
    • ppid:父进程 ID
    • comm:程序命令名(进程名)

注意:‑o只定义输出哪些列,不会扩大进程的查询范围;所以一般搭配 -e 使用:‑eo,代表:查看全部进程 ,并且只输出我指定的这几列

完整命令拆解:

bash 复制代码
ps -eo pid,ppid,pgid,cmd | head -1 && ps -eo pid,ppid,pgid,cmd | grep process_task

输出结果:

bash 复制代码
    PID    PPID    PGID CMD
2972349 2970661 2972349 ./process_task
2972350 2972349 2972349 ./process_task
2972354 2972040 2972353 grep --color=auto process_task

易踩坑点

  1. -o 只控制显示哪些列 ,不会改变进程集合;不加‑e就只会看到当前会话进程。
  2. 字段之间用逗号分隔,逗号不能加引号;逗号两侧可以带空格。
  3. -eo 等价于 -e -o,是合并简写写法。

二、会话

2.1 什么是会话

刚刚我们谈到了进程组的概念,那么会话又是什么呢?会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)。

2.2 Linux Session(会话)与登录完整过程分析

核心概念

  • 会话 Session :一次登录连接对应的进程集合,1 个 Xshell/SSH 终端窗口就对应1 个会话
  • 会话首进程 (session leader) :创建这个会话的进程,登录场景下就是bash会话 ID (SID) = bash 的 PID
  • 一个会话内部可以包含多个进程组(job 作业) ;同一时刻,会话只能有1 个前台进程组,其余为后台进程组。
  • bash 自身独占一个进程组;我们敲命令执行的业务程序,会新建其他进程组。

SSH/Xshell 登录完整流程

  1. **客户端连接:**Xshell(SSH 客户端)通过网络向 Linux 服务器发起 ssh 连接请求。
  2. 认证阶段: 服务器sshd守护进程接收连接,校验用户名、密码 / 密钥,完成用户登录认证。
  3. 创建新会话(Session): 登录成功之后,操作系统会为本次登录新建一个会话
  4. 生成 bash 会话首进程: sshd 会 fork、exec 启动bash。 bash 调用setsid()把自己的 PID 设置为会话 ID (SID) ,bash 就成为这个会话的会话首进程。 同时 bash 自己形成独立的进程组(PID=PGID),也就是图里的进程组 1
  5. 分配终端控制: 该会话会绑定当前伪终端(/dev/pts/X),这个终端就是会话的控制终端。 输入输出全部交给这个 bash,我们就拿到可以敲命令的 shell 窗口。
  6. 用户执行命令,产生更多进程组
  • 在 bash 敲一条普通命令、管道命令,shell 会 fork 出新进程,创建新的进程组(job)
  • 例:proc2 | proc3,管道整条命令是一个作业,形成进程组 2;
  • 再执行proc4;proc5;proc6,又生成进程组 3。
  • bash 仍然留在自己原始的进程组 1,不参与业务进程组。

一个 Session 下允许多个进程组并存,但只有一个前台进程组,可以读取键盘输入;其余放后台。

2.3 补充:grep 命令选项 -E

-E 全称 --extended-regexp,开启扩展正则表达式模式

-E选项使用方式:

bash 复制代码
grep -E 'sleep|process_task'

|扩展 正则中代表或,匹配包含 sleep 或者 process_task 的行。

如果不加 -E,基础正则里的 | 只是普通字符 ,没有 "或" 的含义;想要或逻辑需要写成**\| 转义**。

等价写法

  1. grep -E 'sleep|process_task'
  2. egrep 'sleep|process_task'egrep 就是 grep -E 的别名
  3. 不使用 - E 的等价写法:grep 'sleep\|process_task'

2.4 会话 ID(SID)

上边我们提到了会话ID,那么会话ID是什么呢?我们可以先说一下会话首进程 ,会话首进程是具有唯一进程ID的单个进程 ,那么我们可以将会话首进程的进程ID当做是会话ID。

注意:会话ID在有些地方也被称为会话首进程的进程组ID,因为会话首进程总是一个进程组的组长进程,所以两者是等价的。

三、控制终端

3.1 什么是控制终端

在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端

在没有做重定向的前提下,进程的标准输入、标准输出、标准错误全部绑定到控制终端:

  • 进程读取标准输入,本质就是读取键盘输入;
  • 进程向标准输出、标准错误写数据,内容就打印到终端显示器上。

3.2 会话、进程组和控制终端关系

会话、进程组和控制终端之间存在紧密的约束关系,核心规则整理如下:

  • 一个会话 最多拥有一个控制终端 。会话首进程打开终端设备(物理终端或者 SSH 伪终端/dev/pts/*)之后,该终端就成为本次会话的控制终端。
  • 和控制终端建立连接的会话首进程,叫做控制进程 ,登录场景下就是 bash
  • 同一个会话下可以存在多个进程组,划分为1 个前台进程组 + 若干个后台进程组
  • 只要会话绑定了控制终端,就一定存在唯一的前台进程组,会话剩下全部进程组都属于后台进程组。
  • 在终端按下中断快捷键Ctrl + C、退出快捷键Ctrl + \,内核会把对应的信号发送给前台进程组里面的全部进程,而不是只发给某一个进程。
  • 当终端检测到网络、物理链路断开(例如关闭 Xshell/SSH 窗口),内核会向控制进程(会话首进程 bash)发送挂断信号SIGHUP

这些特性的关系如下图所示:

补充拓展:

  1. 后台进程组默认无法读取控制终端键盘输入,如果尝试读标准输入会收到SIGTTIN信号,进程会停止;
  2. 关闭终端窗口,bash 收到 SIGHUP,bash 退出,会话销毁;会话内的进程默认也会收到 SIGHUP;nohup就是让进程忽略挂断信号,脱离终端继续运行(后续会讲解);
  3. 控制终端属于会话层面的属性,不是单个进程属性,组内所有进程共享同一个控制终端。

3.3 关键易错点及关系链路总结

关键易错点:

  1. 控制终端属性跟随 PCB,fork 会继承;但是 exec 不会改变。
  2. 快捷键信号只发给前台进程组,后台进程不会收到 Ctrl+C。
  3. SSH 断开,最先收到 SIGHUP 的是会话首进程 (bash) ,不是业务子进程。这就是为什么后台运行程序需要 nohup,用来忽略 SIGHUP 信号
  4. 一个会话最多一个控制终端;一个终端同一时刻只属于一个会话。
  5. 后台程序尝试读取控制终端键盘,会收到SIGTTIN信号,默认暂停进程。

关系链路:

bash 复制代码
终端设备 → 会话(session) → 会话首进程(控制进程bash)
        ↓
会话内划分多个进程组
    ├─前台进程组(占用终端,接收键盘信号)
    └─后台进程组(无法读键盘)

四、作业控制

4.1 什么是作业 job

作业是 Shell 用户视角 的任务概念,内核层面没有 "job" 这个结构体,内核只认识进程、进程组、会话

  • 一个作业 = 用户在 Shell 敲一条命令启动的任务。
  • 作业可以只有单个进程sleep 100
  • 作业可以包含多个进程(管道)ps ajx | grep sleep,管道每一段都是独立进程,它们同属同一个进程组,也就是同一个 job

本质:Shell 的 job 几乎等价于内核的进程组 。Shell 给每个 job 分配一个作业编号 [1] [2],内核用 PGID 标识进程组。

4.2 什么是作业控制 Job Control

Shell 管理前后台,操作对象不是单个进程,而是作业(进程组)

  • 前台作业 :占有控制终端,可以读取键盘输入;一个会话同一时刻只能有 1 个前台作业 。按下Ctrl+C / Ctrl+\信号发给整个前台作业(整个进程组所有进程)。
  • 后台作业 :不占用终端键盘输入;一个 Shell 可以同时存在任意多个后台作业 。后台作业尝试读键盘会收到SIGTTIN信号,进程被暂停。

job、进程组、会话三者关系:

  1. 内核:Session 会话 → 多个进程组 (PGID) → 多个 task_struct 进程。
  2. Shell 用户层 :把一个进程组包装成一个 Job 作业,给它分配 job 号。

1 个 Job ↔ 1 个进程组。一个会话(bash)里面可以有很多 job。

示例:

bash 复制代码
ps ajx | grep sleep | wc -l &

管道产生 3 个进程,PGID 完全相同,属于同一个进程组,也就是同一个后台 jobjobs只会看到一条作业记录。

4.3作业号

放在后台执行的程序或命令 称为后台命令 ,可以在命令的后面加上**& 符号** 从而让Shell识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)

4.3.1 后台命令 & 的本质

例如下面的命令在后台启动了一个作业,该作业由两个进程组成,两个进程都在后台运行:

bash 复制代码
cat /etc/filesystems | grep ext &
  • 命令末尾加 & → Shell 把这条命令作为后台作业启动。
  • Shell 不阻塞等待它执行完,立刻返回提示符,可以继续敲下一条命令。
  • 这条管道命令包含 2 个进程 (cat + grep),但它们属于同一个进程组(同一个作业) ,所以 Shell 只分配 1 个作业号

关键:作业号是 Shell 层概念,一个作业(管道)对应一个进程组,不管里面有多少个进程。

执行结果如下:

bash 复制代码
[1] 2202          ← 第1行:作业号 + 进程组ID(PGID)
ext4              ← 第2行:grep 过滤结果
ext3              ← 第3行
ext2              ← 第4行
# 按下回车
[1]+  完成    cat /etc/filesystems | grep --color=auto ext   ← 第6行

第 1 行 [1] 2202

  • [1]作业号(Shell 分配,从 1 递增)
  • 2202进程组 ID(PGID),也是管道中最后一个进程的 PID
  • 注意:管道里 cat 和 grep 是两个独立进程,但只显示一个数字 ------ 因为显示的是进程组领头进程的 PID,不是全部进程。

第 2 - 4 行 ext4 ext3 ext2

  • 后台作业的标准输出仍然打印到控制终端(后台作业可以写终端,只是不能读键盘)。

第 6 行 [1]+ 完成 ...

  • 这行是作业结束通知 ,Shell 不会主动打断你,而是在你下一次按回车 / 执行命令时 才打印出来(避免污染当前输入行)。

4.3.2 默认作业 +

Shell 维护一个作业栈,用两个特殊标记区分优先级:

标记 含义 作用
+ 当前默认作业 不带作业号的 fg / bg / wait 默认操作它
次默认作业 上一个默认作业,当 + 作业退出后,它自动递补为新的 +
无符号 普通后台作业 必须显式指定 %n 才能操作

规则:

  1. 同一时刻只有一个 + 作业,只有一个 作业。
  2. 新启动的后台作业会挤掉原来的 + :原来的 + 变成 ,新作业变成 +
  3. + 作业结束后, 自动升级为 +

实操演示:

bash 复制代码
sleep 100 &     # [1]  →  [1]+  (唯一作业,既是+也是-)
sleep 200 &     # [2]  →  [2]+  [1]-  (新作业变+,旧作业降为-)
jobs
# [1]-  Running   sleep 100
# [2]+  Running   sleep 200
fg              # 不带参数,默认把 [2]+ 提到前台

4.4 作业状态

常见的作业状态如下表所示:

作业状态 含义
正在运行【Running】 后台作业(&),表示正在执行
完成【Done】 作业已完成,返回的状态码为 0
完成并退出【Done (code)】 作业已完成并退出,返回的状态码为非 0
已停止【Stopped】 前台作业,当前被 Ctrl+Z 挂起
已经终止【Terminated】 作业被终止

4.5 作业的挂起与切回

4.5.1 作业挂起 Ctrl+Z

在前台运行程序时按下 Ctrl + Z: 终端会向整个前台进程组(当前 Job 作业) 发送 SIGTSTP 信号,进程组内所有进程暂停执行(停止状态) ,不是退出,进程还在内核中留存。

示例:运行死循环程序 ./test1,按下 Ctrl+Z 输出:[1]+ 已停止 ./test1

  • [1]:作业号
  • +:代表是默认作业
  • 已停止:作业被挂起,进程暂停,不再运行代码,资源没有释放。

区分:

  • Ctrl+C:发送SIGINT,终止前台作业,进程直接退出;
  • Ctrl+Z:发送SIGTSTP暂停前台作业,进程保留,进入后台停止状态
    重要:Ctrl+Z 只对前台作业生效;后台作业按此快捷键不会产生任何影响。

4.5.2 作业切回 fg 命令

fg:把后台停止 / 后台运行的作业切回前台,重新占有控制终端。

  • 不带参数 fg:操作带 +默认作业
  • 支持多种作业选择语法,表格整理:(不需要掌握,了解即可)
参数写法 含义
%n n 为数字,指定作业号,例如 %1
%string 匹配以 string 开头的命令的作业
%?string 匹配命令中包含 string的作业
%+ / %% 最近的默认作业(带+标记),等价不带参数 fg
%- 倒数第二个作业(带-标记)

示例:fg %%,等价直接写fg,把带+标记的挂起作业切回前台,程序恢复继续运行,继续运行死循环程序 ./test1

4.6 作业控制相关的信号

上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 SIGTSTP 信号发送至前台进程组作业中的所有进程,后台进程组中的作业不受影响。在 unix 系统中,存在 3 个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是:

  • Ctrl + C:中断字符,会产生 SIGINT 信号
  • Ctrl + \:退出字符,会产生 SIGQUIT 信号
  • Ctrl + Z:挂起字符,会产生 SIGTSTP 信号

终端的 I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。我们可以通过下体来看到作业控制的功能:

五、守护进程

5.1 什么是守护进程

守护进程是脱离控制终端、后台长期运行的服务进程。

  • 典型例子:sshdmysqldnginxsystemd
  • 特征:
    • 不属于任何终端,关闭终端、SSH 断开不会导致进程退出;不受SIGHUP挂起信号影响。
    • 在后台默默提供服务,没有交互界面。
    • 不属于 Shell 的作业(job),jobs命令看不到它。

对比普通后台 & 进程:

./a.out & 放到后台作业,它仍然属于当前会话、仍然绑定控制终端 。SSH 断开,会话首进程 bash 收到SIGHUP,bash 会向会话内所有子进程发送SIGHUP,普通后台进程大概率退出。

👉 普通&后台 ≠ 守护进程

普通后台进程为什么会随 SSH 断开退出?

bash 复制代码
SSH连接 → 伪终端pts → 会话(session)首进程bash
    ↳ ./a.out & (后台job,属于这个session,继承控制终端)

SSH 网络断开 → 终端检测链路断开 → 发送SIGHUP给会话首进程 bash。 bash 收到 SIGHUP,默认转发 SIGHUP 给会话内所有进程,于是后台进程退出。

5.2 守护进程的核心条件

要真正成为守护进程,必须脱离原来的会话、脱离原来的进程组、脱离控制终端。回顾前面学习的 Unix 体系:

会话 Session → 进程组 → 进程;一个会话绑定一个控制终端。 进程默认继承父进程的会话 ID、进程组 ID、控制终端 。 fork 出来的子进程,和父进程在同一个会话,自然继承控制终端

想要摆脱原有会话和终端,就必须创建一个全新会话

既然守护进程要求进程脱离原有会话与控制终端,那么操作系统如何让一个进程脱离旧会话、生成全新会话?这就引出系统调用 setsid() ------创建会话

5.3 如何创建会话 setsid ()

setsid () 函数原型:

cpp 复制代码
#include <unistd.h>
pid_t setsid(void);

调用前提:调用进程不能是进程组组长,否则调用失败返回‑1。

调用成功后发生三件事:

  1. 创建全新会话 :调用进程成为新会话的会话首进程(session leader),会话 ID = 当前进程 PID。
  2. 创建全新进程组:调用进程成为新进程组组长,进程组 ID = 当前进程 PID。
  3. 进程脱离原来的控制终端 ;新会话此时没有任何控制终端

这正是守护进程想要的效果:脱离旧会话、旧进程组、丢掉控制终端。

5.3.1 为什么不能由进程组组长调用 setsid?

设计约束:防止一个会话内的进程随意 "跳槽" 到新会话,破坏会话 / 进程组层级。

解决办法:先 fork,子进程不是进程组组长,在子进程内部调用setsid()

5.4 守护进程代码设计

守护进程标准步骤:

  1. fork() 创建子进程,父进程直接 exit 退出;子进程继续运行。子进程不是原进程组组长,满足 setsid 调用条件。
  2. 子进程调用 setsid()创建新会话,脱离旧会话、旧进程组、脱离控制终端。
  3. 可选:再次 fork,避免会话首进程重新打开终端设备获得控制终端。
  4. 修改工作目录chdir("/"),防止占用挂载点。
  5. 设置文件掩码umask(0)
  6. 重定向标准输入、输出、错误到/dev/null,彻底断绝终端 IO。
cpp 复制代码
#ifndef DAEMON_HPP
#define DAEMON_HPP
#include <iostream>
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Common.hpp"
using namespace LogModule;
const std::string dev = "/dev/null";
// 系统调用守护进程化:int daemon(int nochdir, int noclose);
// nochdir为0表示更改进程的工作路径到根目录"/",非0则不操作
// noclose为0表示重定向标准输入、标准输出、标准错误到/dev/null,非0则不操作
// 封装守护进程化
void Daemon(int nochdir, int noclose)
{
// 1、忽略IO、子进程退出等相关信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2、创建子进程,父进程直接退出,子进程成为后台进程
if (fork() &gt; 0)
{
    exit(OK);
}

// 3. 创建新会话,成为会话首进程和进程组组长,脱离原控制终端
setsid(); // 成为一个独立的会话

// 可选:切换工作目录到根目录
if (nochdir == 0) // 更改进程的工作路径,为什么??
{
    chdir("/");
}

// 4、孤儿进程依然可能和显示器、键盘、stdin、stdout、stderr相关联
// 守护进程:不从键盘输入,也不需要向显示器上输出打印
// 方法一:关闭文件描述符0、1、2 -- 不推荐(后续如果进程存在std::cout、std::cin操作会存在问题)
// 方法二:打开/dev/null,重定向标准输入、标准输出、标准错误到/dev/null(dup2)
if (noclose == 0)
{
    int fd = open(dev.c_str(), O_RDWR);
    if (fd &lt; 0)
    {
        LOG(LogLevel::FATAL) &lt;&lt; "open " &lt;&lt; dev &lt;&lt; " error";
        exit(OPEN_ERR);
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd); // 必须关闭临时文件描述符,避免资源泄漏
}
}
#endif

5.4.1 为什么存在 nochdir 参数?

5.4.2 为什么存在 noclose 参数?

补充:/dev/null 文件

结束语

本文完整梳理了 Linux 下进程组、会话、控制终端、作业控制以及守护进程整套知识体系。进程组把一批相关进程归为一组,进程组长负责管理整个组;会话则是登录连接的整体载体,登录时通过 setsid 创建会话,bash 成为会话首进程。控制终端与会话绑定,区分前台进程组和后台进程组,我们熟悉的 Ctrl+CCtrl+Z 本质就是终端向前台进程组发送信号。作业控制就是 shell 对作业的调度管理,实现任务后台运行、挂起、前后台切换。

守护进程正是利用会话的特性,脱离原有控制终端、会话与进程组,变成不受终端影响的后台服务。setsid() 的调用限制、工作目录重定向、标准输入输出重定向到 /dev/null,每一步都有对应的底层原因。很多命令行操作看似简单,背后都依托这套会话‑进程组模型。理解这套模型,能够帮助我们看懂信号投递规则、处理后台任务异常、正确编写守护进程代码,也能为后续信号编程、服务器程序开发筑牢底层认知。

相关推荐
GeW15 分钟前
RHCE高频考题精讲,精准突击快速拿证
linux
weixin_430750931 小时前
开源ids网络入侵检测系统Security Onion部署
网络·security onion·开源ids入侵检测系统
wtblszn1 小时前
污水厂可视化管理物联网解决方案
服务器·网络·物联网
bgy66661 小时前
openstack核心指南
服务器·网络·openstack
2401_862880821 小时前
Linux应用层开发 --- UDP&TCP
linux·运维·tcp/ip·udp
Dontla1 小时前
手机远程连接WSL2中的Codex CLI:Tailscale私有网络+Termius+OpenSSH+tmux实战(CodexCli)
网络·智能手机
网安蟹佬霸2 小时前
2026网络安全每日学习计划:每天学多久?学什么?怎么安排?照着做就行!
网络·学习·安全·web安全·学习路线·网安
念恒123062 小时前
传输层协议TCP
服务器·网络·tcp/ip
爱研究的小梁2 小时前
打破时延‑带宽悖论|乾元通 QMP‑UDP,为无人载具提供低延时高带宽原生通信方案
网络·网络协议·udp