【Linux】进程间关系与守护进程详解:从进程组到作业控制到守护进程实现

文章目录

    • 进程间关系与守护进程详解:从进程组到作业控制到守护进程实现
    • 一、进程组
      • [1.1 什么是进程组](#1.1 什么是进程组)
      • [1.2 进程组的组长进程](#1.2 进程组的组长进程)
        • [1.2.1 观察组长进程](#1.2.1 观察组长进程)
      • [1.3 进程组的生命周期](#1.3 进程组的生命周期)
      • [1.4 进程组的作用](#1.4 进程组的作用)
    • 二、会话
      • [2.1 什么是会话](#2.1 什么是会话)
      • [2.2 进程组的形成](#2.2 进程组的形成)
        • [2.2.1 实际验证](#2.2.1 实际验证)
      • [2.3 创建会话](#2.3 创建会话)
        • [2.3.1 setsid的效果](#2.3.1 setsid的效果)
        • [2.3.2 使用限制](#2.3.2 使用限制)
      • [2.4 会话ID(SID)](#2.4 会话ID(SID))
    • 三、控制终端
      • [3.1 什么是控制终端](#3.1 什么是控制终端)
      • [3.2 会话与控制终端的关系](#3.2 会话与控制终端的关系)
      • [3.3 信号与控制终端](#3.3 信号与控制终端)
      • [3.4 进程、进程组、会话、控制终端的关系图](#3.4 进程、进程组、会话、控制终端的关系图)
    • 四、作业控制
      • [4.1 什么是作业](#4.1 什么是作业)
      • [4.2 前台作业和后台作业](#4.2 前台作业和后台作业)
      • [4.3 作业号](#4.3 作业号)
      • [4.4 默认作业](#4.4 默认作业)
      • [4.5 作业状态](#4.5 作业状态)
      • [4.6 作业的挂起](#4.6 作业的挂起)
      • [4.7 作业的切回](#4.7 作业的切回)
      • [4.8 查看作业列表](#4.8 查看作业列表)
      • [4.9 作业控制的信号](#4.9 作业控制的信号)
    • 五、守护进程
      • [5.1 什么是守护进程](#5.1 什么是守护进程)
      • [5.2 守护进程的创建步骤](#5.2 守护进程的创建步骤)
        • [5.2.1 忽略信号](#5.2.1 忽略信号)
        • [5.2.2 fork并让父进程退出](#5.2.2 fork并让父进程退出)
        • [5.2.3 创建新会话](#5.2.3 创建新会话)
        • [5.2.4 更改工作目录](#5.2.4 更改工作目录)
        • [5.2.5 关闭文件描述符](#5.2.5 关闭文件描述符)
      • [5.3 守护进程实现(完整代码)](#5.3 守护进程实现(完整代码))
      • [5.4 将服务守护进程化](#5.4 将服务守护进程化)
      • [5.5 验证守护进程](#5.5 验证守护进程)
    • 六、本篇总结
      • [6.1 核心要点](#6.1 核心要点)
      • [6.2 容易混淆的点](#6.2 容易混淆的点)

进程间关系与守护进程详解:从进程组到作业控制到守护进程实现

💬 开篇:之前我们学习了进程的基本概念、进程创建、进程控制等内容。但在实际的Linux系统中,进程之间并不是孤立存在的,它们通过进程组、会话等机制组织在一起,形成了复杂的进程间关系。理解这些关系,对于理解Shell的作业控制、实现守护进程等都至关重要。这一篇从进程组讲起,到会话、控制终端、作业控制,最后到守护进程的实现,系统地讲解进程间的组织关系,并手把手教你实现一个守护进程。掌握了这些知识,你就能理解Linux系统中进程管理的精髓。

👍 点赞、收藏与分享:这篇会把进程组、会话、守护进程等核心概念讲透,并提供完整的守护进程实现代码。如果对你有帮助,请点赞收藏!

🚀 循序渐进:从进程组开始,到会话,到控制终端,到作业控制,到守护进程,一步步理解进程间的组织关系和守护进程的实现原理。


一、进程组

1.1 什么是进程组

进程组(Process Group)是一个或多个进程的集合。

我们知道每个进程都有一个唯一的进程ID(PID),实际上每个进程还属于一个进程组,每个进程组也有一个唯一的进程组ID(PGID)。

PGID的类型 :和PID一样,PGID也是正整数,可以用pid_t类型存储。

查看进程组ID

bash 复制代码
ps -eo pid,pgid,ppid,comm | grep test

输出示例:

bash 复制代码
PID  PGID  PPID  COMMAND
2830 2830  2259  test

解释:

  • PID:进程ID(2830)
  • PGID:进程组ID(2830)
  • PPID:父进程ID(2259)
  • COMMAND:命令名称(test)

从这个例子可以看出,进程2830的PID和PGID相同,说明它是进程组的组长进程。

1.2 进程组的组长进程

组长进程(Process Group Leader):进程组ID等于其进程ID的进程。

特点

  • 每个进程组都有一个组长进程
  • 组长进程的PID = PGID
  • 组长进程可以创建进程组、创建该组中的进程
1.2.1 观察组长进程
bash 复制代码
ps -o pid,pgid,ppid,comm | cat

输出示例:

bash 复制代码
PID  PGID  PPID  COMMAND
2806 2806  2805  bash
2880 2880  2806  ps
2881 2880  2806  cat

分析:

  • bash进程:PID=2806, PGID=2806,是组长进程
  • ps进程:PID=2880, PGID=2880,是组长进程(创建了新的进程组)
  • cat进程:PID=2881, PGID=2880,属于ps进程组(不是组长)

为什么ps和cat在同一个进程组?

因为我们用管道连接了它们:ps ... | cat,Shell会把它们放在同一个进程组中。

1.3 进程组的生命周期

创建:当第一个进程加入时创建。

存在条件:只要进程组中有一个进程存在,进程组就存在。

销毁:当进程组中最后一个进程离开(终止或加入其他进程组)时销毁。

重要:进程组的存在与组长进程是否终止无关。

示例:

bash 复制代码
进程组PGID=100,包含3个进程:
- 进程100(组长)
- 进程101
- 进程102

1. 进程100终止 → 进程组仍然存在(还有101、102)
2. 进程101终止 → 进程组仍然存在(还有102)
3. 进程102终止 → 进程组销毁(没有进程了)

1.4 进程组的作用

统一管理:可以对整个进程组发送信号,组内所有进程都会收到。

例如:

bash 复制代码
kill -9 -PGID  # 向整个进程组发送SIGKILL信号

作业控制:Shell通过进程组实现作业控制(后面详细讲)。


二、会话

2.1 什么是会话

会话(Session)是一个或多个进程组的集合。

bash 复制代码
会话
├── 进程组1
│   ├── 进程A
│   └── 进程B
├── 进程组2
│   ├── 进程C
│   ├── 进程D
│   └── 进程E
└── 进程组3
    └── 进程F

会话ID(SID):每个会话有唯一的会话ID。

会话首进程(Session Leader):创建会话的进程,其PID等于SID。

2.2 进程组的形成

通常通过管道将几个进程组成一个进程组。

示例:

bash 复制代码
proc2 | proc3 &
proc4 | proc5 | proc6 &

这两个命令分别创建了两个进程组:

  • 进程组1:proc2、proc3(后台运行)
  • 进程组2:proc4、proc5、proc6(后台运行)
2.2.1 实际验证
bash 复制代码
# 创建进程组(后台运行)
sleep 100 | sleep 200 | sleep 300 &

# 查看列标题
ps axj | head -n1

# 过滤sleep进程
ps axj | grep sleep | grep -v grep

输出示例:

bash 复制代码
PPID   PID  PGID   SID  TTY  TPGID  STAT  UID  TIME  COMMAND
2806  4223  4223  2780  pts/2  4229   S   1000  0:00  sleep 100
2806  4224  4223  2780  pts/2  4229   S   1000  0:00  sleep 200
2806  4225  4223  2780  pts/2  4229   S   1000  0:00  sleep 300

分析:

  • 三个sleep进程的PGID都是4223(同一个进程组)
  • 第一个sleep进程(PID=4223)是组长进程(PID=PGID)
  • 它们的SID都是2780(属于同一个会话)

2.3 创建会话

setsid系统调用

c 复制代码
#include <unistd.h>

/*
 * 功能:创建会话
 * 返回值:成功返回SID,失败返回-1
 */
pid_t setsid(void);
2.3.1 setsid的效果

调用setsid后会发生:

  1. 创建新会话:调用进程成为新会话的会话首进程(Session Leader)
  2. 创建新进程组:调用进程成为新进程组的组长进程,新PGID = 调用进程的PID
  3. 脱离控制终端:如果调用前有控制终端,调用后会脱离控制终端
2.3.2 使用限制

重要:如果调用进程已经是进程组组长,setsid会失败并返回-1。

避免错误的方法

c 复制代码
// 1. fork创建子进程
pid_t pid = fork();

if (pid > 0) {
    // 2. 父进程退出
    exit(0);
}

// 3. 子进程继续,调用setsid
// 子进程继承了父进程的PGID,但有新的PID
// 因此子进程肯定不是组长进程
setsid();

原理

  • 子进程继承父进程的PGID
  • 子进程的PID是新分配的
  • 所以子进程的PID ≠ PGID,不是组长进程
  • setsid调用成功

2.4 会话ID(SID)

会话ID:会话首进程的进程ID。

因为会话首进程总是某个进程组的组长进程,所以:

bash 复制代码
SID = 会话首进程的PID = 会话首进程的PGID

三、控制终端

3.1 什么是控制终端

控制终端(Controlling Terminal):与会话关联的终端设备。

工作流程

bash 复制代码
1. 用户通过终端登录系统
   ↓
2. 系统为用户创建Shell进程
   ↓
3. 这个终端成为Shell进程的控制终端
   ↓
4. Shell启动的所有进程都继承这个控制终端

控制终端的作用

  • 进程的标准输入、标准输出、标准错误默认指向控制终端
  • 读标准输入 = 读用户键盘输入
  • 写标准输出/标准错误 = 输出到显示器

3.2 会话与控制终端的关系

关系列表

  1. 一个会话可以有一个控制终端(也可以没有)

  2. 会话首进程打开终端后,该终端成为会话的控制终端

  3. 会话首进程称为控制进程(Controlling Process)

  4. 一个会话可以有多个进程组

    • 一个前台进程组(Foreground Process Group)
    • 零个或多个后台进程组(Background Process Group)
  5. 终端输入和终端信号只影响前台进程组

3.3 信号与控制终端

终端信号

按键 信号 效果
Ctrl+C SIGINT 中断前台进程组的所有进程
Ctrl+\ SIGQUIT 终止前台进程组的所有进程(生成core dump)
Ctrl+Z SIGTSTP 暂停前台进程组的所有进程

重要:这些信号只发送给前台进程组,后台进程组不受影响。

调制解调器断开:如果终端检测到调制解调器断开(或网络断开),会向控制进程发送SIGHUP(挂断)信号。

3.4 进程、进程组、会话、控制终端的关系图

bash 复制代码
终端(控制终端)
    ↓
会话(Session, SID=1000)
    ├── 前台进程组(PGID=2000)
    │   ├── 进程A(PID=2000, 组长)
    │   └── 进程B(PID=2001)
    ├── 后台进程组1(PGID=3000)
    │   ├── 进程C(PID=3000, 组长)
    │   └── 进程D(PID=3001)
    └── 后台进程组2(PGID=4000)
        └── 进程E(PID=4000, 组长)

用户按Ctrl+C → SIGINT发送给进程A和进程B(前台进程组)

四、作业控制

4.1 什么是作业

作业(Job):用户为完成某项任务而启动的一个或多个进程。

作业的组成

  • 单个进程:sleep 100
  • 多个进程(管道):cat file | grep pattern | sort

作业控制(Job Control):Shell对作业的管理机制,包括前台作业、后台作业的切换和控制。

4.2 前台作业和后台作业

前台作业(Foreground Job)

  • Shell等待作业完成
  • 接收用户的键盘输入
  • 可以接收终端信号(Ctrl+C、Ctrl+Z等)

后台作业(Background Job)

  • Shell不等待作业完成,立即返回提示符
  • 不接收键盘输入
  • 不受Ctrl+C、Ctrl+Z等影响

启动后台作业 :在命令后加&符号。

bash 复制代码
# 前台作业
cat /etc/filesystems | head -n 5

# 后台作业
cat /etc/filesystems | grep ext &

4.3 作业号

作业号(Job Number):Shell为每个后台作业分配的编号。

示例:

bash 复制代码
cat /etc/filesystems | grep ext &

输出:

bash 复制代码
[1] 2202
ext4
ext3
ext2

[1]+ 完成    cat /etc/filesystems | grep --color=auto ext

解释:

  • [1]:作业号
  • 2202:该作业中第一个进程的PID
  • ext4、ext3、ext2:作业的输出结果
  • [1]+ 完成:作业完成的提示

4.4 默认作业

默认作业 :用+标记,是最近被放到后台的作业。

次默认作业 :用-标记,是倒数第二个被放到后台的作业。

规则

  • 一个用户只能有一个默认作业(+)
  • 只能有一个次默认作业(-)
  • 当默认作业退出后,次默认作业(-)变成默认作业(+)

示例:

bash 复制代码
sleep 100 &  # 作业1,默认作业
sleep 200 &  # 作业2,成为新的默认作业,作业1变成次默认作业
sleep 300 &  # 作业3,成为新的默认作业,作业2变成次默认作业

jobs

输出:

复制代码
[1]   运行中               sleep 100 &
[2]-  运行中               sleep 200 &
[3]+  运行中               sleep 300 &

4.5 作业状态

状态 含义
运行中(Running) 作业正在前台或后台运行
已停止(Stopped) 作业被Ctrl+Z暂停
完成(Done) 作业正常结束
终止(Terminated) 作业被信号杀死

4.6 作业的挂起

挂起(Suspend):将前台作业暂停,放到后台。

方法 :按Ctrl+Z键。

示例:

c 复制代码
// 死循环程序
#include <stdio.h>

int main()
{
    while (1) {
        printf("hello\n");
    }
    return 0;
}

运行并挂起:

bash 复制代码
./test
# 按Ctrl+Z

[1]+ 已停止               ./test

效果

  • 作业被暂停(状态变为Stopped)
  • 作业被放到后台
  • Shell返回提示符

4.7 作业的切回

fg命令:将后台作业切换到前台。

语法

bash 复制代码
fg [作业号]
fg %作业号
fg %命令名
fg %%      # 默认作业
fg %-      # 次默认作业

示例:

bash 复制代码
# 切换作业1到前台
fg 1
fg %1

# 切换默认作业到前台
fg
fg %%

# 按命令名切换
fg %test

bg命令 :让后台停bash

bg [作业号]

4.8 查看作业列表

jobs命令:查看当前用户的后台作业和挂起的作业。

语法

bash 复制代码
jobs       # 显示作业列表
jobs -l    # 显示详细信息(包含PID)
jobs -p    # 只显示PID

示例:

bash 复制代码
sleep 300 &
./test
# 按Ctrl+Z

jobs -l

输出:

bash 复制代码
[1]- 2265 运行中               sleep 300 &
[2]+ 2267 停止                 ./test

4.9 作业控制的信号

按键 信号 效果
Ctrl+C SIGINT 中断前台进程组
Ctrl+\ SIGQUIT 退出前台进程组(生成core dump)
Ctrl+Z SIGTSTP 挂起前台进程组

重要:这些信号只影响前台进程组,后台进程组不受影响。


五、守护进程

5.1 什么是守护进程

守护进程(Daemon):在后台运行的特殊进程,不与任何终端关联。

特点

  • 长期运行(通常从系统启动到关闭)
  • 没有控制终端
  • 不受用户登录/登出影响
  • 通常以root权限运行

常见守护进程

  • sshd:SSH服务
  • httpd/nginx:Web服务器
  • mysqld:MySQL数据库
  • crond:定时任务服务

5.2 守护进程的创建步骤

5.2.1 忽略信号
cpp 复制代码
// 忽略可能导致进程异常退出的信号
signal(SIGCHLD, SIG_IGN);  // 忽略子进程结束信号
signal(SIGPIPE, SIG_IGN);  // 忽略管道破裂信号

原因

  • SIGCHLD:避免子进程变成僵尸进程
  • SIGPIPE:避免写入已关闭的管道时进程被杀死
5.2.2 fork并让父进程退出
cpp 复制代码
if (fork() > 0) {
    exit(0);  // 父进程退出
}
// 以下代码由子进程执行

目的

  • 确保调用进程不是进程组组长(为setsid做准备)
  • 让Shell认为命令已经执行完成,返回提示符
5.2.3 创建新会话
cpp 复制代码
setsid();

效果

  • 子进程成为新会话的首进程
  • 子进程成为新进程组的组长进程
  • 子进程脱离控制终端
5.2.4 更改工作目录
cpp 复制代码
chdir("/");  // 切换到根目录

原因

  • 守护进程可能长期运行
  • 如果工作目录在某个挂载点上,会导致该挂载点无法卸载
  • 切换到根目录避免这个问题
5.2.5 关闭文件描述符
cpp 复制代码
// 关闭标准输入、标准输出、标准错误
close(0);
close(1);
close(2);

// 或者重定向到/dev/null
int fd = open("/dev/null", O_RDWR);
if (fd > 0) {
    dup2(fd, 0);  // stdin重定向到/dev/null
    dup2(fd, 1);  // stdout重定向到/dev/null
    dup2(fd, 2);  // stderr重定向到/dev/null
    close(fd);
}

原因

  • 守护进程没有控制终端,不需要stdin/stdout/stderr
  • 关闭或重定向到/dev/null避免意外的I/O操作

5.3 守护进程实现(完整代码)

cpp 复制代码
#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);
    
    // 2. fork并让父进程退出,确保子进程不是组长
    if (fork() > 0) {
        exit(0);
    }
    
    // 3. 创建新会话(子进程执行)
    setsid();
    
    // 4. 是否更改工作目录到根目录
    if (ischdir) {
        chdir(root);
    }
    
    // 5. 处理标准输入/输出/错误
    if (isclose) {
        // 直接关闭
        close(0);
        close(1);
        close(2);
    } else {
        // 重定向到/dev/null(推荐)
        int fd = open(dev_null, O_RDWR);
        if (fd > 0) {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}

5.4 将服务守护进程化

cpp 复制代码
// ./server port
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]);
    
    // 调用Daemon函数,将进程守护进程化
    Daemon(false, false);
    
    // 创建TCP服务器
    std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
    svr->Loop();
    
    return 0;
}

参数说明

  • Daemon(false, false)

    • 第一个false:不更改工作目录(保持当前目录)
    • 第二个false:不直接关闭stdin/stdout/stderr,而是重定向到/dev/null

5.5 验证守护进程

启动服务器

bash 复制代码
./server 9090 &

查看进程

bash 复制代码
ps axj | grep server | grep -v grep

输出示例:

bash 复制代码
PPID   PID  PGID   SID  TTY      TPGID  STAT   UID   TIME  COMMAND
   1  5678  5678  5678  ?           -1  Ss    1000   0:00  ./server 9090

关键特征

  • PPID=1:父进程是init(进程被init收养)
  • TTY=?:没有控制终端
  • SID=PID=PGID:是会话首进程和进程组组长

测试

  • 退出终端,守护进程继续运行
  • 可以通过kill命令停止:kill PID

六、本篇总结

6.1 核心要点

进程组

  • 一个或多个进程的集合
  • 每个进程组有唯一的PGID
  • 组长进程:PID = PGID
  • 生命周期:从创建到最后一个进程离开

会话

  • 一个或多个进程组的集合
  • 每个会话有唯一的SID
  • 会话首进程:创建会话的进程
  • setsid创建新会话(调用进程不能是组长)

控制终端

  • 与会话关联的终端设备
  • 一个会话最多有一个控制终端
  • 终端信号(Ctrl+C、Ctrl+Z等)只影响前台进程组

作业控制

  • 作业:用户启动的一个或多个进程
  • 前台作业:接收键盘输入,受终端信号影响
  • 后台作业:不接收输入,不受Ctrl+C等影响
  • jobs查看作业,fg切换到前台,bg让后台作业继续运行

守护进程

  • 后台长期运行,没有控制终端
  • 创建步骤:忽略信号→fork→setsid→chdir→关闭fd
  • 不受用户登录/登出影响

6.2 容易混淆的点

  1. PID、PGID、SID的关系

    • PID:进程ID(唯一标识进程)
    • PGID:进程组ID(进程所属的进程组)
    • SID:会话ID(进程所属的会话)
  2. 组长进程和会话首进程

    • 组长进程:PID = PGID
    • 会话首进程:PID = SID
    • 会话首进程一定是某个进程组的组长
  3. 为什么setsid前要fork

    • setsid要求调用进程不是组长
    • fork后子进程继承父进程的PGID,但有新的PID
    • 所以子进程肯定不是组长
  4. 前台进程组和后台进程组

    • 一个会话只有一个前台进程组
    • 终端信号只发送给前台进程组
    • 后台进程组不受Ctrl+C、Ctrl+Z影响
  5. 守护进程为什么要关闭stdin/stdout/stderr

    • 守护进程没有控制终端
    • 如果不关闭,读写这些fd可能导致错误
    • 重定向到/dev/null是最安全的做法
  6. 守护进程为什么要chdir("/")

    • 避免守护进程的工作目录所在文件系统无法卸载
    • 根目录永远不会被卸载

💬 总结:这一篇系统讲解了进程组、会话、控制终端、作业控制和守护进程的概念与实现。理解了进程间的组织关系,就能理解Shell的工作原理、作业控制机制。掌握了守护进程的创建方法,就能实现稳定的后台服务。这些知识是Linux系统编程的重要基础,对于理解操作系统的进程管理机制非常有帮助。
👍 点赞、收藏与分享:如果这篇帮你理解了进程组、会话和守护进程,请点赞收藏!

相关推荐
Mr_Xuhhh2 小时前
C++11实现线程池
开发语言·c++·算法
Fcy6482 小时前
Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)
linux·运维·服务器·僵尸进程·孤儿进程·进程状态
用户254701008882 小时前
类和对象笔记
c++
ZFB00012 小时前
【麒麟桌面系统】V10-SP1 2503 系统知识——救援模式显示异常
linux·kylin
第七序章2 小时前
【Linux学习笔记】初识Linux —— 理解gcc编译器
linux·运维·服务器·开发语言·人工智能·笔记·学习
迎仔2 小时前
A-总览:GPU驱动运维系列总览
linux·运维
John_ToDebug2 小时前
Chromium回调机制的隐秘角落:当const &参数遇见base::BindOnce
c++·chrome·性能优化
tiantangzhixia2 小时前
Master PDF Linux 平台的 5.9.35 版本安装与自用
linux·pdf·master pdf
消失的旧时光-19432 小时前
C++ 拷贝构造、拷贝赋值、移动构造、移动赋值 —— 四大对象语义完全梳理
开发语言·c++