
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[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+C、Ctrl+Z等按键对应的信号行为。最后落脚到守护进程,解析setsid()创建会话的约束条件,一步步完成守护进程代码实现,解释nochdir、noclose参数以及/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进程是该进程组的组长进程** ,该进程组包括ps和cat两个进程。
在 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 ,显示系统内全部进程。
- 默认不加
-e:ps默认只输出当前终端会话下启动的进程,很多后台进程看不到。 - 加上
-e:遍历系统所有进程,不管属于哪个用户、哪个终端,全部展示出来。
示例:
ps -e,打印整机全部进程。
-o:自定义输出字段(格式化输出)
‑o允许手动指定需要打印的列,以逗号,分隔多个字段,逗号前后可以加空格。- 语法格式:
bash
ps -o 字段1,字段2,字段3
- 示例中:
ps -eo pid,pgid,ppid,commpid:进程 IDpgid:进程组 IDppid:父进程 IDcomm:程序命令名(进程名)
注意:
‑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

易踩坑点
-o只控制显示哪些列 ,不会改变进程集合;不加‑e就只会看到当前会话进程。- 字段之间用逗号分隔,逗号不能加引号;逗号两侧可以带空格。
-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 登录完整流程
- **客户端连接:**Xshell(SSH 客户端)通过网络向 Linux 服务器发起 ssh 连接请求。
- 认证阶段: 服务器
sshd守护进程接收连接,校验用户名、密码 / 密钥,完成用户登录认证。 - 创建新会话(Session): 登录成功之后,操作系统会为本次登录新建一个会话。
- 生成 bash 会话首进程: sshd 会 fork、exec 启动
bash。 bash 调用setsid(),把自己的 PID 设置为会话 ID (SID) ,bash 就成为这个会话的会话首进程。 同时 bash 自己形成独立的进程组(PID=PGID),也就是图里的进程组 1。 - 分配终端控制: 该会话会绑定当前伪终端(
/dev/pts/X),这个终端就是会话的控制终端。 输入输出全部交给这个 bash,我们就拿到可以敲命令的 shell 窗口。 - 用户执行命令,产生更多进程组
- 在 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,基础正则里的|只是普通字符 ,没有 "或" 的含义;想要或逻辑需要写成**\|转义**。
等价写法
grep -E 'sleep|process_task'egrep 'sleep|process_task'(egrep 就是 grep -E 的别名)- 不使用 - 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。
这些特性的关系如下图所示:

补充拓展:
- 后台进程组默认无法读取控制终端键盘输入,如果尝试读标准输入会收到
SIGTTIN信号,进程会停止;- 关闭终端窗口,bash 收到 SIGHUP,bash 退出,会话销毁;会话内的进程默认也会收到 SIGHUP;
nohup就是让进程忽略挂断信号,脱离终端继续运行(后续会讲解);- 控制终端属于会话层面的属性,不是单个进程属性,组内所有进程共享同一个控制终端。
3.3 关键易错点及关系链路总结
关键易错点:
- 控制终端属性跟随 PCB,fork 会继承;但是 exec 不会改变。
- 快捷键信号只发给前台进程组,后台进程不会收到 Ctrl+C。
- SSH 断开,最先收到 SIGHUP 的是会话首进程 (bash) ,不是业务子进程。这就是为什么后台运行程序需要
nohup,用来忽略 SIGHUP 信号。 - 一个会话最多一个控制终端;一个终端同一时刻只属于一个会话。
- 后台程序尝试读取控制终端键盘,会收到
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、进程组、会话三者关系:
- 内核:Session 会话 → 多个进程组 (PGID) → 多个 task_struct 进程。
- Shell 用户层 :把一个进程组包装成一个 Job 作业,给它分配 job 号。
1 个 Job ↔ 1 个进程组。一个会话(bash)里面可以有很多 job。
示例:
bash
ps ajx | grep sleep | wc -l &
管道产生 3 个进程,PGID 完全相同,属于同一个进程组,也就是同一个后台 job ,jobs只会看到一条作业记录。

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 才能操作 |
规则:
- 同一时刻只有一个
+作业,只有一个−作业。 - 新启动的后台作业会挤掉原来的
+:原来的+变成−,新作业变成+。 +作业结束后,−自动升级为+。
实操演示:
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 什么是守护进程
守护进程是脱离控制终端、后台长期运行的服务进程。
- 典型例子:
sshd、mysqld、nginx、systemd。 - 特征:
- 不属于任何终端,关闭终端、SSH 断开不会导致进程退出;不受
SIGHUP挂起信号影响。 - 在后台默默提供服务,没有交互界面。
- 不属于 Shell 的作业(job),
jobs命令看不到它。
- 不属于任何终端,关闭终端、SSH 断开不会导致进程退出;不受
对比普通后台
&进程:
./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。
调用成功后发生三件事:
- 创建全新会话 :调用进程成为新会话的会话首进程(session leader),会话 ID = 当前进程 PID。
- 创建全新进程组:调用进程成为新进程组组长,进程组 ID = 当前进程 PID。
- 进程脱离原来的控制终端 ;新会话此时没有任何控制终端。
这正是守护进程想要的效果:脱离旧会话、旧进程组、丢掉控制终端。
5.3.1 为什么不能由进程组组长调用 setsid?
设计约束:防止一个会话内的进程随意 "跳槽" 到新会话,破坏会话 / 进程组层级。


解决办法:先 fork,子进程不是进程组组长,在子进程内部调用
setsid()。
5.4 守护进程代码设计
守护进程标准步骤:
fork()创建子进程,父进程直接 exit 退出;子进程继续运行。子进程不是原进程组组长,满足 setsid 调用条件。- 子进程调用
setsid():创建新会话,脱离旧会话、旧进程组、脱离控制终端。 - 可选:再次 fork,避免会话首进程重新打开终端设备获得控制终端。
- 修改工作目录
chdir("/"),防止占用挂载点。 - 设置文件掩码
umask(0)。 - 重定向标准输入、输出、错误到
/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() > 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 < 0)
{
LOG(LogLevel::FATAL) << "open " << dev << " 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+C、Ctrl+Z本质就是终端向前台进程组发送信号。作业控制就是 shell 对作业的调度管理,实现任务后台运行、挂起、前后台切换。守护进程正是利用会话的特性,脱离原有控制终端、会话与进程组,变成不受终端影响的后台服务。
setsid()的调用限制、工作目录重定向、标准输入输出重定向到/dev/null,每一步都有对应的底层原因。很多命令行操作看似简单,背后都依托这套会话‑进程组模型。理解这套模型,能够帮助我们看懂信号投递规则、处理后台任务异常、正确编写守护进程代码,也能为后续信号编程、服务器程序开发筑牢底层认知。