08.【网络】Linux进程组、会话、作业控制与守护进程核心知识点 - 进程在终端中到底是怎样组织的
目录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命令看到组长进程的现象[nodelocalhost code]$ps-opid,pgid,ppid,comm|cat# 输出结果PID PGIDPPIDCOMMAND280628062805bash288028802806ps288128802806cat从结果上看ps进程的PID和PGID相同 那也就是说明ps进程是该进程组的组长进程 该进程组包括ps和cat两个进程。进程组组长的作用 进程组组长可以创建一个进程组或者在该组中创建进程。进程组的生命周期 从进程组创建开始到其中最后一个进程离开为止。注意 只要某个进程组中有一个进程存在 则该进程组就存在 这与其组长进程是否已经终止无关。2. 会话2.1 什么是会话刚刚我们谈到了进程组的概念 那么会话又是什么呢 会话其实和进程组息息相关 会话可以看成是一个或多个进程组的集合 一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)通常我们都是使用管道将几个进程编成一个进程组。 如上图的进程组2和进程组3可能是由下列命令形成的[nodelocalhost code]$ proc2|proc3[nodelocalhost code]$ proc4|proc5|proc6# 表示将进程组放在后台执行我们举一个例子观察一下这个现象# 用管道和sleep组成一个进程组放在后台运行[nodelocalhost code]$sleep100|sleep200|sleep300# 查看ps命令打出来的列描述信息[nodelocalhost code]$psaxj|head-n1# 过滤sleep相关的进程信息[nodelocalhost 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函数来创建一个会话前提是调用进程不能是一个进程组的组长。#includeunistd.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 会话、进程组及控制终端之间的联系会话、进程组以及控制终端还有一些其他的关系我们在下边详细介绍一下一个会话可以有一个控制终端通常会话首进程打开一个终端终端设备或伪终端设备后该终端就成为该会话的控制终端。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。如果一个会话有一个控制终端则它有一个前台进程组会话中的其他进程组则为后台进程组。无论何时进入终端的中断键ctrlc或退出键ctrl\就会将中断信号发送给前台进程组的所有进程。如果终端接口检测到调制解调器或网络已经断开则将挂断信号发送给控制进程会话首进程。这些特性的关系如下图所示4. 作业 作业控制4.1 什么是作业(job)和作业控制(Job Control)作业是针对用户来讲用户完成某项任务而启动的进程。一个作业既可以只包含一个进程也可以包含多个进程进程之间互相协作完成任务 通常是一个进程管道。作业控制Shell分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成一个后台作业也可以由多个进程组成Shell可以同时运行一个前台作业和任意多个后台作业这称为作业控制。例如下列命令就是一个作业它包括两个命令在执行时Shell将在前台启动由两个进程组成的作业[nodelocalhost code]$cat/etc/filesystems|head-n5xfs ext4 ext3 ext2 nodev proc4.2 作业号需要在后台执行的程序或命令称为后台命令只需在命令末尾添加符号即可让Shell识别并将其置于后台运行。这种情况下Shell无需等待该后台命令执行完毕就能立即接收并处理用户输入的新命令。此外当后台命令对应的进程执行完成后会向Shell返回对应的作业号以及进程IDPID。例如下面的命令在后台启动了一个作业 该作业由两个进程组成 两个进程都在后台运行:[nodelocalhost code]$cat/etc/filesystems|grepext[1]2202ext4 ext3 ext2[1] 完成cat/etc/filesystems|grep--colorauto ext 后台作业启动时Shell 返回[1]2202[1]就是作业号2202 是该作业主进程的 PID。 作业执行完成后Shell 返回[1] 完成... 这里的[1]同样是该作业的作业号确认是编号1的作业执行完毕 额外的 是辅助标记表示该作业是当前 Shell 中最近一个被放入后台的作业后续使用 fg/bg 命令时若不指定作业号默认操作的就是这个带 标记的作业。 : 表示该作业号是默认作业- 表示该作业即将成为默认作业无符号 表示其他作业4.3 作业状态常见的作业状态如下表所示作业状态含义正在运行【Running】后台作业表示正在执行完成【Done】作业已完成返回的状态码为0完成并退出【Done(code)】作业已经完成并退出返回的状态码为非0已停止【Stopped】前台作业当前被CtrlZ挂起已终止【Terminated】作业被终止4.4 作业的挂起与切回1. 作业挂起我们在执行某个作业时可以通过CtrlZ 键将该作业挂起然后Shell会显示相关的作业号、状态以及所执行的命令信息。例如我们运行一个死循环的程序 通过CtrlZ 将该作业挂起 观察一下对应的作业状态#includestdio.hintmain(){while(1){printf(hello\n);}return0;}下面我运行这个程序 通过CtrlZ 将该作业挂起# 运行可执行程序[nodelocalhost code]$ ./test#键入Ctrl Z观察现象运行结果如下# 结果依次对应作业号 默认作业 作业状态 运行程序信息[1] 已停止 ./test可以发现通过CtrlZ 将作业挂起 该作业状态已经变为了停止状态2. 作业切回如果想将挂起的作业切回可以通过fg 命令 fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为1的作业切到前台来执行若当前系统只有一个作业在后台进行则可以直接使用fg命令不带参数直接切回。 具体的参数参考如下参数含义%nn为正整数表示作业号%string以字符串开头的命令对应的作业%?string包含字符串的命令所对应的作业%或%%最近提交的一个作业%-倒数第二个提交的作业例如我们把刚刚挂起来的./test作业切回到前台[nodelocalhost code]$fg%%运行结果为开始无限循环打印hello 可以发现该作业已经切换到前台了。注意当通过fg 命令切回作业时若没有指定作业参数此时会将默认作业切到前台执行即带有“”的作业号的作业4.5 查看后台执行或挂起的作业我们可以直接通过输入 jobs 命令查看本用户当前后台执行或挂起的作业参数 -l 则显示作业的详细信息参数 -p 则只显示作业的PID例如 我们先在后台及前台运行两个作业 并将前台作业挂起 来用 jobs 命令查看作业相关的信息# 在后台运行一个作业sleep[nodelocalhost code]$sleep300# 运行刚才的死循环可执行程序[nodelocalhost code]$ ./test# 键入Ctrl Z 挂起作业# 使用jobs命令查看后台及挂起的作业[nodelocalhost 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命令手动forksetsid或daemon()函数典型用途临时后台任务如下载、编译系统服务如 Web 服务器、数据库补充说明普通后台进程的局限性普通后台进程本质还是用户会话的一部分当终端关闭时系统会向该会话内的所有进程发送SIGHUP信号后台进程默认会收到信号并退出。示例./test.sh 启动的后台脚本关闭终端后会停止运行。守护进程的独立性保障守护进程通过fork()setsid()系统调用完成脱离终端、创建独立会话的核心操作再通过重定向标准流、切换工作目录到/彻底摆脱对用户环境的依赖最终被init/systemd进程接管成为系统级进程。总结后台进程是终端后台运行的进程集合包含守护进程守护进程是后台进程的子集是具备系统级独立性、专为长期服务设计的特殊进程。5.3 守护进程的创建1. 通过setsid函数手动创建守护进程#pragmaonce#includeiostream#includecstdlib#includesignal.h#includeunistd.h#includefcntl.h#includesys/types.h#includesys/stat.hconstchar*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{// 这里一般建议就用这种intfdopen(dev_null,O_RDWR);if(fd0){dup2(fd,0);dup2(fd,1);dup2(fd,2);close(fd);}}}第45条解释第 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向终端打印错误信息。但守护进程无需与用户交互也不应该占用终端资源因此需要关闭或重定向这三个文件描述符代码提供了两种方案。代码逻辑拆解分两种情况重点理解情况 1isclose为true直接关闭标准 I/O代码close(0); close(1); close(2);分别关闭标准输入、标准输出、标准错误对应的文件描述符特点实现简单直接切断与原终端的关联。潜在风险后续若进程中有代码尝试写入stdout标准输出或stderr标准错误会因文件描述符无效导致程序异常虽然前面忽略了SIGPIPE信号但仍可能触发其他错误因此不推荐优先使用。情况 2isclose为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创建守护进程#includeunistd.hintdaemon(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博客