一、进程、进程组、会话

进程 (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 个固定步骤】(面试必背,按顺序记)
- 第一次 fork,父进程退出 → 子进程非进程组组长,能调用 setsid ()
- 调用 setsid () → 脱离原终端 + 原会话,成新会话首进程 + 新进程组组长
- 第二次 fork,当前进程退出 → 最终进程非会话首进程,杜绝获取终端
- chdir ("/") → 切换根目录,防原目录卸载崩溃
- umask (0) → 重置权限掩码,保证文件完整权限
- 关闭所有文件描述符 → 彻底解绑终端
3.3 个高频面试题(答案直接背)
-
为什么要 fork 两次?
- 第一次 fork:让子进程不是进程组组长,满足 setsid () 调用条件;
- 第二次 fork:让最终进程不是会话首进程,彻底避免重新关联终端。
-
setsid()的作用是什么?(3 点必答)- 脱离原终端和原会话;
- 成为新会话的会话首进程;
- 成为新进程组的进程组组长。
-
为什么守护进程关闭终端也不会退出?
- 核心原因:通过 setsid () 脱离了原会话和原终端,进程不再属于终端对应的会话,终端关闭的信号不会传递给进程,进程独立后台运行。