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

一、 进程组

1.1 进程组的本质

进程组:是一个或多个进程的集合,主要用于作业控制。

进程组具有以下特征:

  • 唯一标识:每个进程组拥有一个进程组 ID(PGID),其数据类型与 PID 完全相同。

  • 成员关系:一个进程组由一个或多个进程组成,组内进程共享同一个 PGID。

  • 用途:主要用于作业控制中的信号分发和状态管理。

1.2 组长进程

组长进程:进程的 PID 等于其所在进程组的 PGID, 即 PGID == PID 时,该进程为组长。

组长进程的职责:

  • 创建进程组:当某个进程首次被放入一个全新的进程组时,它自动成为该组的组长。

  • 在组内派生子进程:子进程默认继承父进程的 PGID(本质是子进程PCB写实拷贝自父进程)。

进程组的生命周期:

  • 起点:组长进程创建进程组的时刻。

  • 终点:组内最后一个进程终止或转入其他进程组的时刻。

  • 关键点:组长进程的终止并不意味着进程组消亡,只要组内仍有其他存活进程,进程组就继续存在。

1.3 示例解释

通常我们都是使用管道将多个进程编成一个进程组

复制代码
hamber@VM-0-14-ubuntu:~$ sleep 100 | sleep 200 | sleep 300 &
hamber@VM-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 (PID=281066, PGID=281066)
  ├── ps (PID=310950, PGID=310950)   	 ← 组长,PGID 等于自身 PID
  └── cat (PID=310951, PGID=310950) 	 ← 加入 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" &)

- 被 Ctrl+Z 挂起后尚未恢复的作业

- 甚至是你启动的子 Shell(bash 嵌套调用)

它们都有一个共同的身份标签:它们都是"这次登录"产生的,会话就是用来给这批进程贴上同一个标签的容器。

当你在终端上执行 ssh user@server 登录后,到最终 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)
  │
  ├── 控制终端(最多一个)
  │     ├── 控制进程 = 会话首进程
  │     ├── 前台进程组(同一时刻最多一个)
  │     └── 后台进程组(零个或多个)
  │
  └── 信号分发规则:
        ├── Ctrl+C  → SIGINT  → 前台进程组所有成员
        ├── Ctrl+\  → SIGQUIT → 前台进程组所有成员
        ├── Ctrl+Z  → SIGTSTP → 前台进程组所有成员
        └── 终端断开 → SIGHUP → 控制进程(会话首进程)

3.2 进程组的分类

在一个会话内部,进程组并非平级罗列,而是根据其与控制终端的关系被严格划分为:

  • 前台进程组

  • 后台进程组

3.2.1 前台进程组

定义:当前正在"使用"控制终端的那个进程组,在同一时刻,一个会话中有且仅有一个前台进程组。

前台进程组核心特征:

  • 可以从控制终端读取输入(read()、fgets() 等不会被阻塞或拒绝)。

  • 向控制终端写入输出时不受限制。

  • 接收来自终端的信号:Ctrl+C → SIGINT,Ctrl+Z → SIGTSTP,Ctrl+\ → SIGQUIT。

典型场景:

你在 Shell 中输入 vim file.txt 并回车,Shell 将 vim 所在的进程组设为前台进程组。此时你的键盘输入直接送达 vim,Ctrl+C 也是发给 vim。

3.2.2 后台进程组

定义:属于某个会话,且不是该会话当前前台进程组的所有进程组,都是后台进程组。

类比理解:

复制代码
场景:“老师上课”

1. 会话 = 一个班级。

- 例如:“高三一班”,所有在这个班里的人和事,都属于这个会话。

2. 前台进程组 = 当前正在被老师“点名提问”的那个同学。

- 只有这个同学能直接跟老师对话(接收键盘输入),他的回答全班都能听到(输出到屏幕)。

- 此时,全班同学的目光和注意力都集中在他身上。

3. 后台进程组 = 班里其他所有正在埋头做题的同学。
  • "属于某个会话" = 他们都是"高三一班"的人(同一班级)。

  • "且不是该会话当前前台进程组"= 只要这个同学现在没有被老师点名站起来回答问题。

  • "都是后台进程组"= 那无论他在做数学卷子、在翻书、还是在发呆,他都属于"后台"状态。

后台进程组的限制:

  • 不能从控制终端读取输入。

    • 如果尝试 read(),内核会向该进程组发送 SIGTTIN 信号,默认行为是暂停进程(SIGSTOP 效果)。

    • 这是为了防止后台进程偷偷抢走用户的键盘输入。

  • 写入控制终端通常被允许,但可以通过 stty tostop 禁止。

    • 若禁止后后台进程尝试写终端,内核发送 SIGTTOU 信号,同样导致暂停。

    • 这避免了后台输出意外覆盖前台程序的界面。

  • 不接收终端产生的信号。

    • Ctrl+C 只影响前台进程组,后台进程组完全不受影响。

四、作业控制

4.1 作业的认识

作业:是针对Shell在用户层面的概念,表示用户为完成某项任务而启动的一个或多个进程的集合,进程间互相协作完成任务。

例如:作业通常是一个进程管道

复制代码
# 这是一个作业,包含三个进程,前台执行
cat access.log | grep "404" | wc -l

# 后台执行作业
cat access.log | grep "404" | wc -l &
# 输出示例:[1] 2345
# [1] 是作业号(Job Number),2345 是组长进程的 PID

作业与进程组:作业与进程组相等于一枚硬币的两面。

  • 对于用户层面而言,进程组执行用户指定的任务被视为做作业

  • 对于内核层面而言,进程组是作业的底层实现机制。

4.2 作业号与标识符

作业号:是 Shell 内部的小整数(1、2、3...),由 Shell 分配和管理。

复制代码
hamber@VM-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 (启动命令)
        │
        |
        ▼
       前台运行				                   暂停运行
    ┌────────────┐    Ctrl+Z (SIGTSTP)     ┌──────────┐
    │ Foreground │ ──────────────────────→ │ Stopped  │ 
    │ (Running)  │                         └──────────┘
    └────────────┘
        │                                 	    │
        │ &                   	                │ fg / bg + SIGCONT
        ▼ 										▼
       后台启动							 	   前台恢复
    ┌────────────┐                     ┌──────────────┐
    │ Background │ ←────────────────── │ Foreground   │ 
    │ (Running)  │     fg %N           │ (Resumed)    │ 
    └────────────┘                     └──────────────┘
        │
        │ 正常退出或被信号终止
        ▼
    ┌───────────┐
    │ Completed │ 
    └───────────┘

4.4 作业相关命令

命令 功能
command & 在后台启动作业
Ctrl+Z 挂起当前前台作业(发送 SIGTSTP 给前台进程组)
jobs 列出当前 Shell 的所有作业及其状态
jobs -l 列出作业及其所有进程的 PID
jobs -p 仅列出组长进程的 PID
fg %N 将第 N 号作业调回前台继续执行
bg %N 让第 N 号已停止的作业在后台继续执行(发送 SIGCONT)
kill %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 进程(PID=1)收养,成为孤儿进程,不会导致僵尸进程。

5.2.3 第三步:创建新会话

复制代码
pid_t sid = setsid();
if (sid < 0) 
{
    perror("setsid");
    exit(EXIT_FAILURE);
}

创建新会话后的效果:

  • 子进程成为新会话的首进程(SID = PID)。

  • 子进程成为新进程组的组长(PGID = PID)。

  • 彻底脱离原控制终端。

  • 此时该进程不再属于任何终端会话,Ctrl+C、Ctrl+Z、终端关闭都不会影响它。

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存在标准输入 fd=0、标准输出 fd=1、标准错误fd=2, 进行指向,引用计数为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
  │
  ├── 前台进程组(同一时刻最多一个)
  │     接收 Ctrl+C / Ctrl+Z 信号
  │     可读写终端
  │
  ├── 后台进程组 1
  │     不接收终端信号
  │
  └── 后台进程组 2
        不接收终端信号

守护进程:
  通过 fork + setsid 脱离上述h结构
  无会话归属(或自建新会话)
  无控制终端
  不受任何用户操作影响
  独立运行直至系统关闭或手动终止
相关推荐
大侠归来2 小时前
Ubuntu 搭建 APUE 环境:从源码编译到第一个示例跑通
linux·运维·ubuntu
shdkfbbv3 小时前
【无标题】
linux·运维·服务器
周洲08304 小时前
STM32片内Flash读写深度详解|掉电参数保存、底层原理
linux·前端·stm32
shimly1234564 小时前
比 find + grep 更好用的 nm + addr2line
linux
迷途之人不知返4 小时前
【进程控制】-3-进程等待
linux
_upupup5 小时前
包管理器和vim
linux·服务器
一颗小树x5 小时前
NVIDIA 驱动故障排查实录:内核升级导致 nvidia-smi 失效
linux·服务器·nvidia驱动·启动失败·内核升级
_smart_boy__5 小时前
I.MX6U开发板Uboot无法ping Ubuntu问题解决方案(二)
linux·运维·ubuntu
DianSan_ERP5 小时前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化