目录
- 1. 进程组
- 1.1 进程组概念
- 1.2 组长进程
- 2. 会话
- 2.1 什么是会话
- 2.2 如何创建会话(setsid函数)
- 2.3 会话ID(SID)
- 3. 控制终端
- 3.1 概念 先说一下什么是控制终端?
- 3.2 会话、进程组及控制终端之间的联系
- 4. 作业 & 作业控制
- 4.1 什么是作业(job)和作业控制(Job Control)?
- 4.2 作业号
- 4.3 作业状态
- 4.4 作业的挂起与切回
- 1. 作业挂起
- 2. 作业切回
- 4.5 查看后台执行或挂起的作业
- 4.6 作业控制相关的信号
- 5. 守护进程
- 5.1 守护进程的定义
- 5.2 守护进程与后台进程的核心区别
- 5.3 守护进程的创建
- 1. 通过setsid函数手动创建守护进程
- 2. C库函数daemon创建守护进程
- 6. 使用示例 - 网络计算器
1. 进程组
1.1 进程组概念
之前我们提到了进程的概念, 其实每一个进程除了有一个进程ID(PID)之外还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。每一个进程组也有一个唯一的进程组ID(PGID),并且这个PGID类似于进程ID,同样是一个正整数,可以存放在pid_t数据类型中。
$ps-eopid,pgid,ppid,comm|greptest#结果如下PID PGIDPPIDCOMMAND283028302259test# -e 选项表示every的意思, 表示输出每一个进程信息# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列1.2 组长进程
每一个进程组都有一个组长进程。 组长进程的ID等于其进程ID。我们可以通过ps命令看到组长进程的现象:
[node@localhost code]$ps-opid,pgid,ppid,comm|cat# 输出结果PID PGIDPPIDCOMMAND280628062805bash288028802806ps288128802806cat从结果上看ps进程的PID和PGID相同, 那也就是说明ps进程是该进程组的组长进程, 该进程组包括ps和cat两个进程。
进程组组长的作用: 进程组组长可以创建一个进程组或者在该组中创建进程。
进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。
注意: 只要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
2. 会话
2.1 什么是会话
刚刚我们谈到了进程组的概念, 那么会话又是什么呢? 会话其实和进程组息息相关, 会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)
通常我们都是使用管道将几个进程编成一个进程组。 如上图的进程组2和进程组3可能是由下列命令形成的:
[node@localhost code]$ proc2|proc3&[node@localhost code]$ proc4|proc5|proc6&# &表示将进程组放在后台执行我们举一个例子观察一下这个现象:
# 用管道和sleep组成一个进程组放在后台运行[node@localhost code]$sleep100|sleep200|sleep300&# 查看ps命令打出来的列描述信息[node@localhost code]$psaxj|head-n1# 过滤sleep相关的进程信息[node@localhost code]$psaxj|grepsleep|grep-vgrep# a选项表示不仅列当前⽤户的进程,也列出所有其他⽤户的进程# x选项表示不仅列有控制终端的进程,也列出所有⽆控制终端的进程# j选项表示列出与作业控制相关的信息, 作业控制后续会讲# grep的-v选项表示反向过滤, 即不过滤带有grep字段相关的进程# 结果如下PPIDPID PGID SID TTY TPGID STATUIDTIME COMMAND2806422342232780pts/24229S10000:00sleep1002806422442232780pts/24229S10000:00sleep2002806422542232780pts/24229S10000:00sleep300从上述结果来看3个进程对应的PGID相同, 即属于同一个进程组。
2.2 如何创建会话(setsid函数)
可以调用setsid函数来创建一个会话,前提是调用进程不能是一个进程组的组长。
#include<unistd.h>`/* *功能:创建会话 *返回值:创建成功返回SID, 失败返回-1 */pid_tsetsid(void);该接口调用之后会发生:
调用进程会变成新会话的会话首进程。 此时, 新会话中只有唯一的一个进程
调用进程会变成进程组组长。 新进程组ID就是当前调用进程ID
该进程没有控制终端。 如果在调用setsid之前该进程存在控制终端, 则调用之后会切断联系
需要注意的是:这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是先调用fork创建子进程, 父进程终止, 子进程继续执行, 因为子进程会继承父进程的进程组ID, 而进程ID则是新分配的, 就不会出现错误的情况。
2.3 会话ID(SID)
上边我们提到了会话ID, 那么会话ID是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程ID的单个进程, 那么我们可以将会话首进程的进程ID当做是会话ID。
注意:会话ID在有些地方也被称为 会话首进程的进程组ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
3. 控制终端
3.1 概念 先说一下什么是控制终端?
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
3.2 会话、进程组及控制终端之间的联系
会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。
这些特性的关系如下图所示:
4. 作业 & 作业控制
4.1 什么是作业(job)和作业控制(Job Control)?
作业:是针对用户来讲,用户完成某项任务而启动的进程。一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程管道。
作业控制:Shell分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制。
例如下列命令就是一个作业,它包括两个命令,在执行时Shell将在前台启动由两个进程组成的作业:
[node@localhost code]$cat/etc/filesystems|head-n5xfs ext4 ext3 ext2 nodev proc4.2 作业号
需要在后台执行的程序或命令称为后台命令,只需在命令末尾添加&符号,即可让Shell识别并将其置于后台运行。这种情况下,Shell无需等待该后台命令执行完毕,就能立即接收并处理用户输入的新命令。此外,当后台命令对应的进程执行完成后,会向Shell返回对应的作业号以及进程ID(PID)。
例如下面的命令在后台启动了一个作业, 该作业由两个进程组成, 两个进程都在后台运行:
[node@localhost code]$cat/etc/filesystems|grepext&[1]2202ext4 ext3 ext2[1]+ 完成cat/etc/filesystems|grep--color=auto ext 后台作业启动时,Shell 返回[1]2202:[1]就是作业号;2202 是该作业主进程的 PID。 作业执行完成后,Shell 返回[1]+ 完成...: 这里的[1]同样是该作业的作业号,确认是编号1的作业执行完毕; 额外的 + 是辅助标记,表示该作业是当前 Shell 中最近一个被放入后台的作业,后续使用 fg/bg 命令时,若不指定作业号,默认操作的就是这个带 + 标记的作业。+ : 表示该作业号是默认作业
- :表示该作业即将成为默认作业
无符号: 表示其他作业
4.3 作业状态
常见的作业状态如下表所示:
| 作业状态 | 含义 |
|---|---|
| 正在运行【Running】 | 后台作业(&),表示正在执行 |
| 完成【Done】 | 作业已完成,返回的状态码为0 |
| 完成并退出【Done(code)】 | 作业已经完成并退出,返回的状态码为非0 |
| 已停止【Stopped】 | 前台作业,当前被Ctrl+Z挂起 |
| 已终止【Terminated】 | 作业被终止 |
4.4 作业的挂起与切回
1. 作业挂起
我们在执行某个作业时,可以通过Ctrl+Z 键将该作业挂起,然后Shell会显示相关的作业号、状态以及所执行的命令信息。例如我们运行一个死循环的程序, 通过Ctrl+Z 将该作业挂起, 观察一下对应的作业状态:
#include<stdio.h>intmain(){while(1){printf("hello\n");}return0;}下面我运行这个程序, 通过Ctrl+Z 将该作业挂起:
# 运行可执行程序[node@localhost code]$ ./test#键入Ctrl + Z观察现象运行结果如下:
# 结果依次对应作业号 默认作业 作业状态 运行程序信息[1]+ 已停止 ./test可以发现通过Ctrl+Z 将作业挂起, 该作业状态已经变为了停止状态
2. 作业切回
如果想将挂起的作业切回,可以通过fg 命令, fg 后面可以跟作业号或作业的命令名称。如
果参数缺省则会默认将作业号为1的作业切到前台来执行,若当前系统只有一个作业在后台进行,
则可以直接使用fg命令不带参数直接切回。 具体的参数参考如下:
| 参数 | 含义 |
|---|---|
| %n | n为正整数,表示作业号 |
| %string | 以字符串开头的命令对应的作业 |
| %?string | 包含字符串的命令所对应的作业 |
| %+或%% | 最近提交的一个作业 |
| %- | 倒数第二个提交的作业 |
例如我们把刚刚挂起来的./test作业切回到前台:
[node@localhost code]$fg%%运行结果为开始无限循环打印hello , 可以发现该作业已经切换到前台了。
注意:当通过fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有“+”的作业号的作业
4.5 查看后台执行或挂起的作业
我们可以直接通过输入 jobs 命令查看本用户当前后台执行或挂起的作业
参数 -l 则显示作业的详细信息
参数 -p 则只显示作业的PID
例如, 我们先在后台及前台运行两个作业, 并将前台作业挂起, 来用 jobs 命令查看作业相关的信息:
# 在后台运行一个作业sleep[node@localhost code]$sleep300&# 运行刚才的死循环可执行程序[node@localhost code]$ ./test# 键入Ctrl + Z 挂起作业# 使用jobs命令查看后台及挂起的作业[node@localhost code]$jobs-l运行结果如下所示:
# 结果依次对应作业号 默认作业 作业状态 运行程序信息[1]-2265运行中sleep300&[2]+2267停止 ./test4.6 作业控制相关的信号
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。 在unix系统中, 存在3个特殊字符可以使得终端驱动程序产生信号, 并将信号发送至前台进程组作业, 它们分别是:
Ctrl + C: 中断字符, 会产生 SIGINT 信号Ctrl + \: 退出字符, 会产生 SIGQUIT 信号Ctrl + Z:挂起字符, 会产生 STGTSTP 信号
终端的I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。我们可以通过下体来看到作业控制的功能:
5. 守护进程
5.1 守护进程的定义
守护进程(Daemon)是Linux 系统中脱离终端控制、在后台长期运行的系统级进程,专门用于提供持续的系统服务(如sshd远程登录服务、nginx网页服务、crond定时任务服务)。
它的核心特征是完全独立于用户会话,即使启动它的终端关闭、用户退出登录,守护进程依然会持续运行,其生命周期通常与系统启动 / 停止绑定。
5.2 守护进程与后台进程的核心区别
守护进程是特殊的后台进程,但普通后台进程和守护进程的设计目标、独立性有本质差异,具体对比如下:
| 对比维度 | 普通后台进程 | 守护进程 |
|---|---|---|
| 终端关联 | 与启动终端(tty)绑定 | 完全脱离终端,无控制终端 |
| 会话归属 | 属于用户会话(Session) | 独立会话,无归属用户会话 |
| 生命周期 | 依赖用户会话,终端关闭则退出 | 与系统生命周期绑定,长期运行 |
| 标准流处理 | 可能输出到终端(需手动重定向) | 强制重定向到/dev/null |
| 父进程 | 启动它的终端进程(如bash) | 被init/systemd进程收养 |
| 启动方式 | 命令行加&或bg命令 | 手动fork+setsid或daemon()函数 |
| 典型用途 | 临时后台任务(如下载、编译) | 系统服务(如 Web 服务器、数据库) |
补充说明:
普通后台进程的局限性
普通后台进程本质还是用户会话的一部分,当终端关闭时,系统会向该会话内的所有进程发送
SIGHUP信号,后台进程默认会收到信号并退出。示例:
./test.sh &启动的后台脚本,关闭终端后会停止运行。守护进程的独立性保障
守护进程通过
fork()+setsid()系统调用,完成脱离终端、创建独立会话的核心操作,再通过重定向标准流、切换工作目录到/,彻底摆脱对用户环境的依赖,最终被init/systemd进程接管,成为系统级进程。
总结
- 后台进程是终端后台运行的进程集合,包含守护进程;
- 守护进程是后台进程的子集,是具备系统级独立性、专为长期服务设计的特殊进程。
5.3 守护进程的创建
1. 通过setsid函数手动创建守护进程
#pragmaonce#include<iostream>#include<cstdlib>#include<signal.h>#include<unistd.h>#include<fcntl.h>#include<sys/types.h>#include<sys/stat.h>constchar*root="/";constchar*dev_null="/dev/null";voidDaemon(boolischdir,boolisclose){// 1. 忽略可能引起程序异常退出的信号signal(SIGCHLD,SIG_IGN);signal(SIGPIPE,SIG_IGN);// 2. 让自己不要成为组长:新建子进程并将进程组的组长(父进程)退出if(fork()>0)exit(0);// 3. 设置让自己成为一个新的会话,后面的代码其实是子进程在走setsid();// 4. 每一个进程都有自己的CWD,是否将当前进程的CWD更改成为 / 根目录if(ischdir)chdir(root);//chdir() 是 Linux 系统调用,作用是将当前进程的工作目录切换到指定路径(这里是根目录)。// 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了if(isclose){close(0);close(1);close(2);}else{// 这里一般建议就用这种intfd=open(dev_null,O_RDWR);if(fd>0){dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);}}}第4,5条解释:
第 4 点:守护进程当前工作目录(CWD)的处理
核心目的
切换 / 保留守护进程的当前工作目录(Current Working Directory,简称 CWD),避免占用无效资源、防止路径相关问题影响守护进程长期运行。
代码逻辑拆解
- 参数
ischdir:是一个 “开关”,让调用者选择是否修改当前工作目录,提供灵活配置。chdir(root)执行逻辑:
root是预定义的常量"/"(系统根目录);chdir()是 Linux 系统调用,作用是将当前进程的工作目录切换到指定路径(这里是根目录)。(重点)关键补充:为什么要切换到根目录?
守护进程是长期运行的后台进程,若不切换工作目录,会存在两个核心问题:
- 问题 1:占用原文件系统,导致无法卸载。比如守护进程从 U 盘(路径
/media/usb)启动,若不切换工作目录,守护进程运行期间,U 盘无法被正常卸载(系统会提示 “资源被占用”)。- 问题 2:根目录是最稳定的文件系统。
"/"是 Linux 系统的根文件系统,不会被随意挂载 / 卸载,适合守护进程长期运行的特性,避免因原工作目录的文件系统变动导致守护进程异常。- 补充:若
ischdir传入false,则保留守护进程启动时的原工作目录,适用于守护进程需要依赖特定路径读取 / 写入文件的场景。第 5 点:解除守护进程与标准 I/O(终端)的关联
核心目的
切断守护进程与原终端的标准输入(0)、标准输出(1)、标准错误(2)的绑定关系,符合守护进程 “无终端交互、后台静默运行” 的核心特性。
背景铺垫
守护进程启动前,会继承父进程(Shell 终端)的标准 I/O,默认绑定到该终端:
- 标准输入(0):从终端读取用户输入;
- 标准输出(1):向终端打印正常信息;
- 标准错误(2):向终端打印错误信息。
但守护进程无需与用户交互,也不应该占用终端资源,因此需要关闭或重定向这三个文件描述符,代码提供了两种方案。
代码逻辑拆解(分两种情况)(重点理解)
情况 1:
isclose为true(直接关闭标准 I/O)
- 代码:
close(0); close(1); close(2);
- 分别关闭标准输入、标准输出、标准错误对应的文件描述符;
- 特点:实现简单,直接切断与原终端的关联。
- 潜在风险:后续若进程中有代码尝试写入
stdout(标准输出)或stderr(标准错误),会因文件描述符无效导致程序异常(虽然前面忽略了SIGPIPE信号,但仍可能触发其他错误),因此不推荐优先使用。情况 2:
isclose为false(推荐,重定向到/dev/null)这是代码中标注 “一般建议就用这种” 的最佳实践,拆解步骤如下:
/dev/null是什么(重点):Linux 系统中的 “空设备”(也叫 “黑洞”),有两个核心特性:
- 写入到
/dev/null的所有数据会被直接丢弃,不占用任何磁盘空间;- 从
/dev/null读取数据,会立即返回EOF(无数据可读),不会阻塞进程。- 代码步骤拆解:
int fd = open(dev_null, O_RDWR);:以 “可读可写” 模式打开/dev/null,获取一个文件描述符fd(通常是 3,因为 0/1/2 尚未被关闭);dup2(fd, 0);:将标准输入(0)重定向到/dev/null—— 后续进程尝试读取终端输入,等价于从/dev/null读取,直接返回EOF,不会阻塞等待用户输入;dup2(fd, 1);:将标准输出(1)重定向到/dev/null—— 后续进程的正常打印信息,会被直接丢弃,不会显示到原终端;dup2(fd, 2);:将标准错误(2)重定向到/dev/null—— 后续进程的错误打印信息,也会被直接丢弃,不会显示到原终端;close(fd);:重定向完成后,关闭/dev/null的文件描述符fd—— 因为 0/1/2 已经关联了/dev/null,无需保留额外的fd,节省系统文件描述符资源。- 核心优势:既切断了与原终端的关联,又避免了后续操作无效文件描述符的风险,是守护进程标准 I/O 处理的最优解。
总结
- 第 4 点(工作目录):为了避免占用无效文件系统,保障守护进程长期稳定运行,提供切换到根目录的选项;
- 第 5 点(标准 I/O):为了解除与原终端的关联,实现守护进程静默后台运行,推荐重定向到
/dev/null的安全方案;- 两点都是守护进程编写的 “标准步骤”,目的是让进程符合守护进程的核心特性,避免运行异常。
2. C库函数daemon创建守护进程
#include<unistd.h>intdaemon(intnochdir,intnoclose);// 【功能】// 将调用进程转换为符合 Linux 规范的「守护进程」(后台长期静默运行进程),仅用于 Unix/Linux 环境;// 内部封装 fork()/setsid() 等核心步骤,脱离原终端控制,内核会将其转为无终端后台进程,同时可配置工作目录和标准 I/O 处理。// 【参数】// nochdir: int → 工作目录保留开关(布尔语义,0 为 false,非 0 为 true);// 0:默认行为,调用 chdir("/") 切换进程工作目录到系统根目录;// 非 0:保留进程调用 daemon() 时的原当前工作目录,不做切换;// 常用值:0(推荐,避免占用原文件系统导致无法卸载);// noclose: int → 标准 I/O 保留开关(布尔语义,0 为 false,非 0 为 true);// 0:默认行为,将标准输入/输出/错误(0/1/2)重定向到 /dev/null(空设备);// 非 0:保留进程原标准 I/O,仍与原终端保持关联(不推荐守护进程使用);// 常用值:0(推荐,实现后台静默运行,无终端交互冗余输出)。// 【返回值】// 成功:返回 0;// 失败:返回 -1,并设置 errno:// EAGAIN → fork() 失败,系统进程数达到上限,无法创建子进程;// ENOMEM → fork() 失败,系统内存不足,无法为新进程分配内核资源;// EPERM → setsid() 失败,调用进程无足够权限创建新会话(如无 root 权限且受限制)。6. 使用示例 - 网络计算器
网络计算器实现 - 自定义套接字+序列化+守护进程-CSDN博客