【Linux笔记】Linux进程与守护进程

发布时间:2026/10/1 22:05:00
【Linux笔记】Linux进程与守护进程 一、 进程组1.1 进程组的本质进程组是一个或多个进程的集合主要用于作业控制。进程组具有以下特征唯一标识每个进程组拥有一个进程组 IDPGID其数据类型与 PID 完全相同。成员关系一个进程组由一个或多个进程组成组内进程共享同一个 PGID。用途主要用于作业控制中的信号分发和状态管理。1.2 组长进程组长进程进程的 PID 等于其所在进程组的 PGID, 即PGID PID时该进程为组长。组长进程的职责创建进程组当某个进程首次被放入一个全新的进程组时它自动成为该组的组长。在组内派生子进程子进程默认继承父进程的 PGID本质是子进程PCB写实拷贝自父进程。进程组的生命周期起点组长进程创建进程组的时刻。终点组内最后一个进程终止或转入其他进程组的时刻。关键点组长进程的终止并不意味着进程组消亡,只要组内仍有其他存活进程进程组就继续存在。1.3 示例解释通常我们都是使用管道将多个进程编成一个进程组hamberVM-0-14-ubuntu:~$ sleep 100 | sleep 200 | sleep 300 hamberVM-0-14-ubuntu:~$ ps axj |head -1 ps ajx | grep sleep | grep -v grep PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 281066 312705 312705 281066 pts/1 313057 S 1003 0:00 sleep 100 281066 312706 312705 281066 pts/1 313057 S 1003 0:00 sleep 200 281066 312707 312705 281066 pts/1 313057 S 1003 0:00 sleep 300在 Shell 中执行ps -o pid,pgid,ppid,comm | cat时ps -o pid,pgid,ppid,comm | cat PID PGID PPID COMMAND 281066 281066 281058 bash 310950 310950 281066 ps 310951 310950 281066 cat bash (PID281066, PGID281066) ├── ps (PID310950, PGID310950) ← 组长PGID 等于自身 PID └── cat (PID310951, PGID310950) ← 加入 ps 所在的进程组二、会话2.1 会话的本质会话是比进程组更高一层的抽象它是一个或多个进程组的集合。会话的设计目的是将一次完整的用户登录交互从登录到退出所涉及的所有进程组织在一起。2.2 会话的完整生命周期将一次完整的用户登录交互从登录到退出所涉及的所有进程组织在一起。这期间发生的所有事情构成了一次完整的交互它包括- Shell 本身bash/zsh - 你执行的每一条命令及其子进程vim file.txt、gcc main.c、make - 管道链中的所有进程cat log | grep error | sort | uniq -c - 后台运行的任务sleep 3600 、find / -name *.tmp - 被 CtrlZ 挂起后尚未恢复的作业 - 甚至是你启动的子 Shellbash 嵌套调用它们都有一个共同的身份标签它们都是这次登录产生的会话就是用来给这批进程贴上同一个标签的容器。当你在终端上执行ssh userserver登录后到最终exit退出为止。用户 ssh 登录 │ ▼ login/sshd 创建新会话 (setsid) │ SID Shell PID │ 绑定控制终端 /dev/pts/N │ ├── 用户执行各种命令进程不断创建、终止 ├── 进程组不断创建、消亡 ├── 前台进程组反复切换 │ ... 可能持续数小时、数天 ... │ ▼ 用户 exit / 关闭终端 / 网络断开 │ ▼ 控制终端释放 SIGHUP 发送给会话首进程 会话内所有进程收到清理信号 最后一个进程退出 → 会话消亡三、控制终端3.1 控制终端的认识控制终端是一个与会话绑定的终端设备每个会话最多拥有一个控制终端每个控制终端也最多被一个会话所控制。终端设备由内核管理的、带有输入输出处理管线的双向字符流通道。关键点控制终端不是终端设备本身的属性而是会话的属性例如对同一个/dev/pts/0设备文件在被某个会话设为控制终端时才是控制终端;在没有会话绑定它时它只是一个普通的字符设备。对于同一个/dev/pts/0设备文件而言普通打开终端设备 作为控制终端 ───────────────── ──────────────── open(/dev/pts/0, O_RDWR) 通过特定机制建立控制关系 仅获得一个文件描述符 会话获得信号路由能力 无特殊内核行为 内核自动分发 SIGINT/SIGTSTP/SIGHUP 任何进程都可以打开 仅会话首进程可以建立或继承 关闭 fd 即断开 需要显式脱离或会话终结才断开控制终端与会话会话 (SID) │ ├── 控制终端最多一个 │ ├── 控制进程 会话首进程 │ ├── 前台进程组同一时刻最多一个 │ └── 后台进程组零个或多个 │ └── 信号分发规则 ├── CtrlC → SIGINT → 前台进程组所有成员 ├── Ctrl\ → SIGQUIT → 前台进程组所有成员 ├── CtrlZ → SIGTSTP → 前台进程组所有成员 └── 终端断开 → SIGHUP → 控制进程会话首进程3.2 进程组的分类在一个会话内部进程组并非平级罗列而是根据其与控制终端的关系被严格划分为前台进程组后台进程组3.2.1 前台进程组定义当前正在使用控制终端的那个进程组在同一时刻一个会话中有且仅有一个前台进程组。前台进程组核心特征可以从控制终端读取输入read()、fgets()等不会被阻塞或拒绝。向控制终端写入输出时不受限制。接收来自终端的信号CtrlC → SIGINTCtrlZ → SIGTSTPCtrl\ → SIGQUIT。典型场景你在 Shell 中输入vim file.txt并回车Shell 将vim所在的进程组设为前台进程组。此时你的键盘输入直接送达vimCtrlC 也是发给vim。3.2.2 后台进程组定义属于某个会话且不是该会话当前前台进程组的所有进程组都是后台进程组。类比理解场景“老师上课” 1. 会话 一个班级。 - 例如“高三一班”所有在这个班里的人和事都属于这个会话。 2. 前台进程组 当前正在被老师“点名提问”的那个同学。 - 只有这个同学能直接跟老师对话接收键盘输入他的回答全班都能听到输出到屏幕。 - 此时全班同学的目光和注意力都集中在他身上。 3. 后台进程组 班里其他所有正在埋头做题的同学。属于某个会话 他们都是高三一班”的人同一班级。且不是该会话当前前台进程组 只要这个同学现在没有被老师点名站起来回答问题。都是后台进程组 那无论他在做数学卷子、在翻书、还是在发呆他都属于后台”状态。后台进程组的限制不能从控制终端读取输入。如果尝试read()内核会向该进程组发送 SIGTTIN 信号默认行为是暂停进程SIGSTOP 效果。这是为了防止后台进程偷偷抢走用户的键盘输入。写入控制终端通常被允许但可以通过stty tostop禁止。若禁止后后台进程尝试写终端内核发送 SIGTTOU 信号同样导致暂停。这避免了后台输出意外覆盖前台程序的界面。不接收终端产生的信号。CtrlC 只影响前台进程组后台进程组完全不受影响。四、作业控制4.1 作业的认识作业是针对Shell在用户层面的概念表示用户为完成某项任务而启动的一个或多个进程的集合进程间互相协作完成任务。例如作业通常是一个进程管道# 这是一个作业包含三个进程前台执行 cat access.log | grep 404 | wc -l # 后台执行作业 cat access.log | grep 404 | wc -l # 输出示例[1] 2345 # [1] 是作业号Job Number2345 是组长进程的 PID作业与进程组作业与进程组相等于一枚硬币的两面。对于用户层面而言进程组执行用户指定的任务被视为做作业对于内核层面而言进程组是作业的底层实现机制。4.2 作业号与标识符作业号是 Shell 内部的小整数1、2、3...由 Shell 分配和管理。hamberVM-0-14-ubuntu:~$ jobs ↓ 作业号 [1]- Running sleep 1 | sleep 2 | sleep 10 [2] Running sleep 1 | sleep 2 | sleep 12 标识符%%符号用于引用作业%1、%2按编号引用。%或%%当前默认作业最近被放入后台或挂起的作业。%-上一个默认作业当前默认作业之前的那个。4.3 作业状态转换Start command 启动命令 │ | ▼ 前台运行 暂停运行 ┌────────────┐ CtrlZ (SIGTSTP) ┌──────────┐ │ Foreground │ ──────────────────────→ │ Stopped │ │ (Running) │ └──────────┘ └────────────┘ │ │ │ │ fg / bg SIGCONT ▼ ▼ 后台启动 前台恢复 ┌────────────┐ ┌──────────────┐ │ Background │ ←────────────────── │ Foreground │ │ (Running) │ fg %N │ (Resumed) │ └────────────┘ └──────────────┘ │ │ 正常退出或被信号终止 ▼ ┌───────────┐ │ Completed │ └───────────┘4.4 作业相关命令命令功能command 在后台启动作业CtrlZ挂起当前前台作业发送 SIGTSTP 给前台进程组jobs列出当前 Shell 的所有作业及其状态jobs -l列出作业及其所有进程的 PIDjobs -p仅列出组长进程的 PIDfg %N将第 N 号作业调回前台继续执行bg %N让第 N 号已停止的作业在后台继续执行发送 SIGCONTkill %N向第 N 号作业的所有进程发送 SIGTERM五、守护进程5.1 守护进程的认识守护进程存在于独立的会话中脱离原有的控制终端是在后台长期运行的特殊进程。守护进程的典型特征没有控制终端独立于用户登录会话。不产生终端输出日志写入文件或系统日志。生命周期很长往往伴随整个系统运行周期。命名惯例进程名以d结尾如sshd、httpd、crond、syslogd。需要守护进程化的原因以网络服务器为例若服务器进程直接由用户终端启动当用户关闭终端或 SSH 断开时SIGHUP 信号会终止服务器。守护进程化使服务器脱离任何终端和会话不再受用户登录状态影响。5.2 守护进程化的步骤5.2.1 第一步忽略特定信号signal(SIGCHLD, SIG_IGN); // 忽略子进程退出信号由内核进行回收子进程避免僵尸进程 signal(SIGPIPE, SIG_IGN); // 忽略管道断裂信号避免 write 导致进程终止忽略特定信号SIGCHLD的原因守护进程的核心特征是长期运行且无交互它通常在主循环中处理业务逻辑如接受网络连接、处理定时任务而不是在一个专门的子进程回收循环中等待。SIGCHLD默认动作是 SIG_DFL而 SIG_DFL 对 SIGCHLD 的语义是不做任何事。这意味着内核不会自动回收子进程显式忽略后内核会自动回收子进程资源无需父进程调用wait()。忽略特定信号SIGPIPE的原因在一个网络服务器守护进程如果用户关闭客户端而此时服务端还在向客户端写入时会触发SIGPIPE信号导致服务端进程被信号杀死从而引起服务器进程被关闭。SIGPIPE当向已关闭读端的管道或 socket 写入时触发。忽略后write()返回 -1 并设置errno EPIPE程序可自行处理错误而非直接崩溃。5.2.2 第二步fork 并让父进程退出pid_t pid fork(); if (pid 0) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 父进程退出 exit(EXIT_SUCCESS); } // 此后代码仅在子进程中执行核心目的: 让子进程能够创建新会话创建新会话的前提是当前进程不是进程组长PID PGID确保子进程不是进程组组长。子进程 PID 是新的而 PGID 继承自父进程PGID 是进程组长的PID因为PID具有唯一性所以子进程的PID不可能等于PGID因为父进程退出子进程被 init 进程PID1收养成为孤儿进程不会导致僵尸进程。5.2.3 第三步创建新会话pid_t sid setsid(); if (sid 0) { perror(setsid); exit(EXIT_FAILURE); }创建新会话后的效果子进程成为新会话的首进程SID PID。子进程成为新进程组的组长PGID PID。彻底脱离原控制终端。此时该进程不再属于任何终端会话CtrlC、CtrlZ、终端关闭都不会影响它。5.2.4 更改工作目录将守护进程的工作路径放置在根目录下if (chdir(/) 0) { perror(chdir); exit(EXIT_FAILURE); }5.2.5 重置文件描述符守护进程存在于独立的会话中失去了控制终端但失去控制终端 ≠ 文件描述符自动关闭。fd 0/1/2 仍然打开着仍然指向原来的设备节点这可能导致的问题阻止终端/文件系统卸载只要守护进程持有这些 fd原有对应的设备或文件系统就无法被释放或卸载写入已失效的终端导致不可预测行为第三方库的隐式 I/O将守护进程的文件描述符重定向到 /dev/null的原因写入即消失所有重定向或写入到该 /dev/null 文件的数据都会被操作系统内核直接丢弃不占用任何磁盘空间。读取即空如果你尝试从它里面读取内容它会立刻返回一个 文件结束符EOF即什么都读不到。// 方式一关闭后重定向到 /dev/null int fd open(/dev/null, O_RDWR); if (fd ! -1) { dup2(fd, STDIN_FILENO); // fd 0 → /dev/null dup2(fd, STDOUT_FILENO); // fd 1 → /dev/null dup2(fd, STDERR_FILENO); // fd 2 → /dev/null close(fd); //关闭fd文件描述此时/dev/null存在标准输入 fd0、标准输出 fd1、标准错误fd2, 进行指向引用计数为3。 }5.3 完整的守护进程化函数实现// Daemon.hpp #include unistd.h #include stdlib.h #include signal.h #include fcntl.h #include sys/stat.h void Daemon() { // 1. 忽略可能干扰的信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); // 2. fork父进程退出 pid_t pid fork(); if (pid 0) { exit(EXIT_FAILURE); } if (pid 0) { exit(EXIT_SUCCESS); } // 3. 创建新会话脱离控制终端 if (setsid() 0) { exit(EXIT_FAILURE); } // 4. 更改工作目录 if (chdir(/) 0) { exit(EXIT_FAILURE); } // 5. 重置文件描述符到 /dev/null int fd open(/dev/null, O_RDWR); //读写的方式打开 if (fd ! -1) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd STDERR_FILENO) { close(fd); } } }六、总结用户登录 │ ▼ 创建会话 (Session) │ SID Shell 的 PID │ 控制终端 /dev/pts/N │ ├── Shell 进程会话首进程 / 控制进程 │ PGID PID SID │ ├── 前台进程组同一时刻最多一个 │ 接收 CtrlC / CtrlZ 信号 │ 可读写终端 │ ├── 后台进程组 1 │ 不接收终端信号 │ └── 后台进程组 2 不接收终端信号 守护进程 通过 fork setsid 脱离上述h结构 无会话归属或自建新会话 无控制终端 不受任何用户操作影响 独立运行直至系统关闭或手动终止