进程间关系与守护进程
1. 进程组 (Process Group)
1.1 什么是进程组?
除了拥有唯一的进程 ID (PID) 之外,每一个进程还属于一个进程组。
-
概念:进程组是一个或多个进程的集合。
-
PGID:每个进程组都有一个唯一的进程组 ID (PGID),类似于 PID,是一个正整数,存放在
pid_t数据类型中。
可以通过 ps 命令查看进程所属的进程组:
# -e: 显示所有进程 (every)# -o: 自定义输出列,以逗号作为定界符ps -eo pid,pgid,ppid,comm | grep test1.2 组长进程
每一个进程组都有一个组长进程。
-
特征:组长进程的 PID 等于其 PGID(即
PID == PGID)。 -
作用:组长进程可以创建一个进程组,或者创建该组中的进程。
-
示例:
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat PID PGID PPID COMMAND 2806 2806 2805 bash 2880 2880 2806 ps # ps 是组长进程,PID=2880, PGID=2880 2881 2880 2806 cat # cat 属于 ps 创建的进程组,PGID=2880说明:从结果上看,ps 进程的 PID 和 PGID 相同,说明 ps 是该进程组的组长进程,该进程组包含 ps 和 cat 两个进程。_
1.3 进程组的生命周期
-
从进程组创建开始,到其中最后一个进程离开为止。
-
注意:只要某个进程组中有一个进程存在,该进程组就存在,这与组长进程是否已经终止无关。
2. 会话 (Session)
2.1 什么是会话?
会话与进程组息息相关,可以看成是一个或多个进程组的集合。
-
SID:每一个会话也有一个唯一的会话 ID (SID)。
-
形成方式:通常我们使用管道将几个进程编成一个进程组,放到后台执行。
# 使用管道将多个命令编成进程组,并放在后台(&)执行 [node@localhost code]$ proc2 | proc3 & [node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &- 查看会话信息:
[node@localhost code]$ ps axj | head -n1 [node@localhost code]$ ps axj | grep sleep | grep -v grep参数解析:
-
-a:列出当前用户以及所有其他用户的进程。 -
-x:列出所有有控制终端和无控制终端的进程。 -
-j:列出与作业控制相关的信息(如 PGID, SID 等)。 -
grep -v grep:反向过滤,排除 grep 自身的进程。 (结果中属于同一个管道命令的多个进程,其 PGID 和 SID 是相同的。)
2.2 如何创建会话
可以通过调用 setsid() 函数来创建一个新的会话。
#include <unistd.h>/* * 功能:创建会话 * 返回值:创建成功返回 SID,失败返回 -1 */pid_t setsid(void);调用前提:调用进程不能是一个进程组的组长,否则会报错。
调用后的变化:
-
调用进程变成新会话的会话首进程。此时新会话中只有该进程一个进程。
-
调用进程变成新进程组的组长。新进程组 ID 就是当前调用进程的 PID。
-
脱离控制终端。如果调用前该进程有控制终端,调用后会切断联系。
标准实践(避免报错):通常先调用
fork()创建子进程,然后父进程直接退出 (exit),子进程继续执行setsid()。因为子进程继承了父进程的组 ID,而获得了新的 PID,从而保证了子进程绝不会是组长进程。
2.3 会话首进程与 SID
-
会话首进程:具有唯一 PID 的单个进程,它是会话的创建者。
-
SID(会话 ID):会话首进程的 PID 即被当做是会话 ID。(由于会话首进程总是一个进程组的组长,因此 SID 很多时候也等同于该会话首进程的 PGID)。
3. 控制终端 (Controlling Terminal)
3.1 概念
在 UNIX 系统中,用户通过终端登录系统后会得到一个 Shell 进程,这个终端就成为了该 Shell 进程的控制终端。
-
控制终端的信息保存在进程的 PCB 中。
-
fork()出来的子进程会继承父进程的 PCB 信息,因此由 Shell 启动的其他进程默认也共用这个控制终端。 -
默认情况下,进程的标准输入、标准输出、标准错误都指向控制终端(即从键盘读,向显示器写)。
3.2 终端与会话、进程组的关系特性
-
一个会话可以有一个控制终端。通常会话首进程打开一个终端后,该终端就成为该会话的控制终端。
-
建立了与控制终端连接的会话首进程被称为控制进程。
-
一个会话中的进程组可被分为:一个前台进程组 和 一个或多个后台进程组。
-
只要会话有控制终端,它就一定会有一个前台进程组。
-
信号产生与传递:
-
键盘中断:键入中断键 (
Ctrl+C) 或退出键 (Ctrl+\) 时,终端驱动程序会将对应的信号发送给前台进程组的所有进程。 -
网络断开:如果终端接口检测到连接(如调制解调器或网络)已断开,会将挂断信号(SIGHUP)发送给控制进程(会话首进程)。
-
4. 作业与作业控制 (Job Control)
4.1 什么是作业与作业控制?
-
作业 (Job):针对用户而言,用户为完成某项任务而启动的进程(或进程管道组合)。一个作业可以包含一个或多个互相协作的进程。
-
作业控制:Shell 分前后台控制的对象是作业(或进程组),而不是单个进程。Shell 可以同时运行一个前台作业和多个后台作业,这种机制称为作业控制。
4.2 作业号与后台执行
在命令后加 & 符号,可以让 Shell 识别这是一个后台命令(后台作业)。
后台命令无需等待执行完成,终端可立即接收新命令。后台作业提交后,会返回一个作业号和 PID。
[node@localhost code]$ cat /etc/filesystems | grep ext &[1] 2202 # [作业号] PID-
默认作业 (+) 与即将成为默认作业 (-):
-
+:表示该作业号是当前的默认作业。 -
-:表示当默认作业退出后,该作业即将成为下一个默认作业。 -
无符号:其他普通后台作业。
-
4.3 作业状态
常见作业状态对照表:
| 作业状态 | 含义说明 |
|---|---|
| 正在运行 [Running] | 后台作业 (&),表示正在正常执行中 |
| 完成 [Done] | 作业已完成,返回的状态码为 0 |
| 完成并退出 [Done(code)] | 作业已完成并退出,返回的状态码非 0 |
| 已停止 [Stopped] | 运行中的作业被挂起(如被 Ctrl+Z 暂停) |
| 已终止 [Terminated] | 作业被异常终止 |
4.4 作业的挂起与切回
-
作业挂起 (
Ctrl + Z):- 在执行前台作业时,键入
Ctrl + Z会将SIGTSTP信号发送给前台进程组的所有进程,使作业挂起变为停止状态 (Stopped)。
- 在执行前台作业时,键入
-
作业切回 (
fg):-
将挂起或后台的作业切回前台执行。
-
语法格式:
fg [参数] -
参数说明:
-
不带参数:默认将带有
+号的默认作业切回前台。 -
%n:n 为正整数,表示切回指定的作业号。 -
%string:切回命令以该字符串开头的作业。 -
%?string:切回命令包含该字符串的作业。 -
%+或%%:切回最近提交的作业。 -
%-:切回倒数第二个提交的作业。
-
-
4.5 查看作业
使用 jobs 命令查看当前用户在后台执行或被挂起的作业。
-
jobs:查看简略信息。 -
jobs -l:显示作业的详细信息(包含 PID 等)。 -
jobs -p:只显示作业的 PID。
4.6 终端信号总结
这 3 个特殊按键可以使终端驱动程序产生信号,并将信号发送至前台进程组(后台进程不受影响):
-
Ctrl + C:中断字符,产生SIGINT信号。 -
Ctrl + \:退出字符,产生SIGQUIT信号。 -
Ctrl + Z:挂起字符,产生SIGTSTP信号。
5. 守护进程 (Daemon Process)
守护进程是脱离于终端并在后台运行的特殊进程。为了将一个普通程序转化为守护进程,需要执行以下核心步骤:
5.1 守护进程的创建代码封装 (Daemon.hpp)
#pragma once#include <iostream>#include <cstdlib>#include <signal.h>#include <unistd.h>#include <fcntl.h>#include <sys/types.h>#include <sys/stat.h>
const char *root = "/";const char *dev_null = "/dev/null";
void Daemon(bool ischdir, bool isclose){ // 1. 忽略可能引起程序异常退出的信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 重点:防止向断开的连接写数据导致进程因 SIGPIPE 崩溃
// 2. 保证自己不是组长进程,以便后续顺利调用 setsid() if (fork() > 0) { exit(0); // 父进程退出,子进程继续 }
// 3. 设置自己成为一个新的会话,彻底脱离原有的控制终端 setsid();
// 4. 更改当前工作目录 (CWD) if (ischdir) { chdir(root); }
// 5. 处理标准输入、输出、错误 (0, 1, 2) if (isclose) { // 直接关闭 close(0); close(1); close(2); } else { // 推荐做法:重定向到 /dev/null(数据黑洞) int fd = open(dev_null, O_RDWR); if (fd > 0) { dup2(fd, 0); // 标准输入重定向到 /dev/null dup2(fd, 1); // 标准输出重定向到 /dev/null dup2(fd, 2); // 标准错误重定向到 /dev/null close(fd); } }}5.2 如何将服务守护进程化
在网络服务(如 TcpServer)启动之前,调用我们封装好的 Daemon 函数即可让服务以守护进程的形态在后台悄悄运行。
// main.cpp 示例int main(int argc, char *argv[]){ if(argc != 2) { std::cout << "Usage: " << argv[0] << " port" << std::endl; return 0; }
uint16_t localport = std::stoi(argv[1]);
// 调用守护进程化接口 // false, false 意味着:不更改工作目录到根目录,不直接 close fd 而是重定向到 /dev/null Daemon(false, false);
// 启动业务逻辑 std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest)); svr->Loop();
return 0;}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

