守护进程及其编程流程

一、进程、进程组、会话

进程 (Process)

  • 程序运行后的实例,系统分配资源的最小单位;
  • 每个进程有唯一标识:PID (进程 ID)

进程组 (Process Group)

  • 由「一个或多个进程」组成的集合;
  • 有一个【组长进程】:第一个创建该组的进程,组长的 PID = 整个进程组的 PGID (进程组 ID)
  • 组内其他进程是【组员进程】,组员的 PGID 都等于组长的 PID;
  • 只要进程组里还有一个进程存活,这个进程组就存在。

会话 (Session)

  • 由「一个或多个进程组」组成的集合;
  • 有一个【会话首进程】:打开终端后,运行的第一个进程(比如登录后的 bash);
  • 会话的唯一标识:SID (会话 ID) = 会话首进程的 PID;
  • 一个会话 对应一个「控制终端」(比如你的 xshell / 终端窗口),终端的信号(Ctrl+C、关闭终端)会传给会话内的进程。

三者层级关系(最核心,一句话理清,)

终端 → 会话(SID) → 多个进程组(PGID) → 每个进程组包含多个进程(PID)

二、守护进程的目标及其标准步骤

其目标就是让进程脱离原终端和原会话,在后台长期稳定运行,不受终端关闭的影响。

其标准步骤如下:

  • 第一次 fork() 并退出父进程

    • 子进程成为孤儿进程,被 init 收养
    • 关键:子进程不是原进程组组长 ,满足 setsid() 调用条件
  • setsid() 创建新会话

    • 子进程脱离原终端和原会话
    • 成为新会话首进程 + 新进程组组长
    • ❌ 注意:进程组组长无法调用 setsid()
  • 第二次 fork() 并退出父进程

    • 最终进程(孙子进程)不再是会话首进程
    • 彻底避免重新关联终端
  • chdir("/") 切换工作目录到根

    • 防止原目录被卸载导致进程崩溃
  • umask(0) 重置文件权限掩码

    • 清除原进程的权限限制,保证创建文件时的完整权限
  • close() 关闭所有文件描述符

    • 彻底脱离终端的输入输出关联
  • (可选)处理僵死进程

    • 通过 waitpid() 或信号机制回收子进程资源

三、会话与进程组核心知识点

1. 层级关系

终端 → 会话(SID) → 进程组(PGID) → 进程(PID)

  • 一个终端对应一个会话,一个会话包含多个进程组
  • 图片说明:
    • 上图:原会话(绑定终端),包含多个进程组
    • 下图:setsid() 后创建的新会话(脱离终端),仅包含一个进程组

2. 进程组判定规则

  • 组长进程PID == PGID(进程组 ID 等于组长 PID)
  • 组员进程PGID == 组长PID
  • 结论:若进程已是原进程组组长,无法调用 setsid() 创建新会话

3. 会话首进程特性

  • 会话中第一个运行的进程,其 PID 即为会话的 SID
  • setsid() 调用后,子进程成为新会话首进程 + 新进程组组长
  • 第二次 fork() 的目的:让最终进程不再是会话首进程,避免重新获取终端

四、关键结论

  • 关闭终端不影响守护进程的原因:守护进程已脱离原会话,不再与终端绑定
  • 两次 fork() 的意义:第一次让子进程非组长,第二次让最终进程非会话首进程
  • setsid() 的核心作用:创建新会话、脱离原终端、成为新会话 / 进程组的首进程

五、Linux标准守护进程 代码以及完整解释

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>

// 该代码是【Linux标准守护进程】完整实现,核心:脱离终端、后台永久运行、不受终端关闭影响
int main()
{
    // ======== 【第一步:第一次fork创建子进程,父进程直接退出】 核心必记 ✔
    // fork()返回值:父进程得到>0的子进程PID,子进程得到0
    pid_t pid = fork();
    if (pid != 0)  // 父进程分支
    {
        exit(0);   // 父进程退出,子进程被系统init进程接管,成为孤儿进程
        // 核心目的:让子进程 不是【进程组组长】,满足后续setsid()的调用条件
    }

    // ======== 【第二步:调用setsid() 创建新会话,脱离原终端】 重中之重 ✔
    setsid();
    // 该函数的3个核心作用(面试必考,必须背):
    // 1. 让当前进程 脱离「原会话+原终端」,彻底切断和终端的关联
    // 2. 当前进程 成为 新会话的【会话首进程】
    // 3. 当前进程 成为 新进程组的【进程组组长】
    // 注意:进程组组长 无法调用setsid(),所以第一步的fork必不可少!

    // ======== 【第三步:第二次fork创建孙子进程,当前进程退出】 核心必记 ✔
    pid = fork();
    if (pid != 0)  // 此时的父进程 是新会话首进程
    {
        exit(0);   // 退出该进程,只留孙子进程运行
        // 核心目的:让最终运行的进程 不是会话首进程
        // 会话首进程有概率重新获取终端,这一步彻底杜绝,保证守护进程纯后台运行
    }

    // ======== 【第四步:切换工作目录到根目录 /】 ✔
    chdir("/");
    // 原因:进程默认继承父进程的工作目录,如果原目录被卸载/删除,进程会报错崩溃
    // 切换到根目录,根目录永远不会被卸载,保证进程稳定运行

    // ======== 【第五步:重置文件权限掩码 umask(0)】 ✔
    umask(0);
    // umask:权限掩码,新文件的最终权限 = 默认权限 - umask值
    // 文件默认权限666,目录默认777;默认umask非0会限制权限
    // 设为0:关闭权限限制,让守护进程创建文件/目录时拥有完整权限,避免权限不足问题

    // ======== 【第六步:关闭所有打开的文件描述符】 ✔
    for (int i = 0; i < getdtablesize(); i++)
    {
        close(i);
    }
    // 原因:进程默认继承父进程的文件描述符(0=标准输入 1=标准输出 2=标准错误)
    // 这些描述符都关联原终端,脱离终端后这些句柄无意义,关闭后彻底和终端解绑
    // getdtablesize():获取当前进程能打开的最大文件描述符数量,循环全关最稳妥

    // ======== 【守护进程的业务逻辑】 死循环保证永久运行 ✔
    while (1)  // 无限循环,让进程一直后台运行
    {
        // 每隔5秒,往 /tmp/c2305d.log 日志文件追加写入当前系统时间
        FILE* fp = fopen("/tmp/c2305d.log", "a"); // a=追加模式,不会覆盖原有内容
        if (fp == NULL)  // 打开文件失败则退出循环
        {
            break;
        }

        time_t tv;
        time(&tv);  // 获取当前系统时间戳
        // 格式化时间为字符串,写入日志文件
        fprintf(fp, "Time is %s", asctime(localtime(&tv)));
        fclose(fp); // 必须关闭文件,释放资源

        sleep(5);   // 休眠5秒,循环执行
    }

    return 0;
}
1.守护进程核心定义

脱离终端、在 Linux 后台长期稳定运行的进程,关闭终端不会导致进程退出,不受终端信号影响(如 nginx、sshd 都是守护进程)

2.守护进程创建【6 个固定步骤】(面试必背,按顺序记)
  1. 第一次 fork,父进程退出 → 子进程非进程组组长,能调用 setsid ()
  2. 调用 setsid () → 脱离原终端 + 原会话,成新会话首进程 + 新进程组组长
  3. 第二次 fork,当前进程退出 → 最终进程非会话首进程,杜绝获取终端
  4. chdir ("/") → 切换根目录,防原目录卸载崩溃
  5. umask (0) → 重置权限掩码,保证文件完整权限
  6. 关闭所有文件描述符 → 彻底解绑终端
3.3 个高频面试题(答案直接背)
  1. 为什么要 fork 两次

    • 第一次 fork:让子进程不是进程组组长,满足 setsid () 调用条件;
    • 第二次 fork:让最终进程不是会话首进程,彻底避免重新关联终端。
  2. setsid() 的作用是什么?(3 点必答)

    • 脱离原终端和原会话;
    • 成为新会话的会话首进程;
    • 成为新进程组的进程组组长。
  3. 为什么守护进程关闭终端也不会退出?

    • 核心原因:通过 setsid () 脱离了原会话和原终端,进程不再属于终端对应的会话,终端关闭的信号不会传递给进程,进程独立后台运行。
相关推荐
~黄夫人~1 小时前
Ansible自动化运维:快速入门,从 “批量化执行” 开始
运维·自动化·ansible
式5162 小时前
RAG检索增强生成基础(二)RAG项目实战之Milvus Docker环境配置
运维·docker·容器
久违8162 小时前
PHP 安全与部署知识总结
linux·ubuntu·php
Yeats_Liao2 小时前
容器化部署:基于Docker的推理环境隔离与迁移
运维·docker·容器
开开心心就好2 小时前
内存清理工具点击清理,自动间隔自启
linux·运维·服务器·安全·硬件架构·材料工程·1024程序员节
txinyu的博客2 小时前
连接池问题
服务器·网络·c++
CTO Plus技术服务中2 小时前
大厂面试笔记和参考答案!浏览器自动化、自动化测试、自动化运维与开发、办公自动化
运维·笔记·面试
数据知道2 小时前
万字详解 PostgreSQL 的详细安装方式(Linux、Windows、macOS、Docker 全平台覆盖)
linux·windows·postgresql
YYYing.2 小时前
【计算机网络 | 第七篇】计网之传输层(一)—— 传输层概述与协议头分析
服务器·网络·网络协议·tcp/ip·计算机网络·udp