在上一篇文章当中我们讲到了 status 的结构[一定能看懂的] 进程管理 | 等待进程 | Waitpid | status | pid重用,其中涉及到的 核心转储 部分当时没有讲
我们可以在这里结合信号,来进行一个感性的理解,其实主要就是在 gdb 调试当中使用的,core ==term +core dump,Core退出的可以被核心转储的以便于后序快速定位问题。
云服务器未默认采用核心转储 是架构特性与运维需求共同作用的结果,但其提供的替代方案在效率和安全性上更适应云计算场景
维度 | 核心转储 | 云服务器常规方案 |
---|---|---|
故障定位粒度 | 内存级精确 | 日志/指标级 |
资源消耗 | 高(GB级存储) | 低(KB-MB级日志) |
安全性 | 高风险(含敏感数据) | 可控(结构化日志过滤) |
适用场景 | 本地物理服务器调试 | 分布式云环境运维 |
关于其中涉及到信号部分的理解,可以见去年暑假的这篇文章:[Linux][OS][详解信号的产生]
会话 | 守护进程
让我们用 公司组织架构 的比喻来解释这些概念
一、终端(Terminal)—— 公司的前台
生活实例:
公司的前台是员工与外部沟通的接口,负责接收客户需求、转接电话等。
技术解释:
终端是用户与操作系统交互的入口(如 Windows 的命令行或 Mac 的 Terminal)。所有输入输出都通过终端完成。
# 查看当前终端关联的进程
echo $$ # 输出当前终端进程的 ID
二、进程组(Process Group)—— 项目组
生活实例:
公司接到一个项目,成立一个项目组(包含开发、测试、设计等成员)。
技术解释:
进程组是多个相关进程的集合,通常由一条 Shell 命令创建(如管道命令)
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid == 0) {
printf("子进程 PID=%d,进程组 ID=%d\n", getpid(), getpgrp());
} else {
printf("父进程 PID=%d,进程组 ID=%d\n", getpid(), getpgrp());
}
sleep(10);
return 0;
}
运行结果:
父进程和子进程属于同一个进程组,组 ID 是父进程的 PID(类似项目组组长)
三、会话(Session)—— 项目周期
生活实例:
公司启动一个长期项目周期,包含多个阶段(需求、开发、测试等),每个阶段对应一个项目组。
技术解释:
会话是多个进程组的集合,由用户登录终端时创建。一个会话对应一个终端。
# 查看当前会话 ID
ps -o pid,ppid,pgid,sid,comm
关键特性:
- 关闭终端(如退出 SSH)会向会话发送 SIGHUP 信号,导致会话终止
- 前台进程组(如正在运行的命令)可以接收终端输入,后台进程组则不行
四、守护进程(Daemon)—— 24 小时值班的保安
生活实例:
保安需要 7x24 小时工作,不受公司前台关闭影响,所以说其实可以理解为它就是一个后台进程组
技术解释:
守护进程是脱离终端、长期运行的后台进程(如 Nginx、MySQL)
代码示例:
- 通过
fork
创建一个子进程,此时父子进程属于同一个进程组,父进程是组长。 - 如果
id > 0
说明是父进程,父进程直接退出,一个进程组中,进程组长退出,进程组其他进程继续运行,不会退出。 - 因此子进程不会退出,继承了父进程的所有信息,并且还不是进程组长。
于是子进程调用setsid
,自己创建一个会话,自己做话首进程,至此子进程就是一个守护进程了!
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
pid_t pid = fork();
if (pid > 0) exit(0); // 1. 父进程退出
setsid(); // 2. 创建新会话(脱离原终端)
chdir("/"); // 3. 切换工作目录到根
umask(0); // 4. 重置文件权限掩码
// 关闭所有文件描述符(此处简化为关闭标准流)
close(0); close(1); close(2);
while(1) { // 5. 守护进程主逻辑
// 执行定时任务(如日志清理)
sleep(3600);
}
return 0;
}
关键点:
setsid()
:脱离原终端,成为新会话的首进程
关闭文件描述符:避免占用终端资源
- ( 子进程的 三个标准流,都指向
/dev/pts/0
,这就是当前的终端文件。 - 如果当前终端退出,那么如果这个守护进程还向终端输出内容,就会导致错误
- 因此还要改变它的输出流
/dev/null
是一个Linux
提供的文件,它可以接收任何输入,但是不论输入什么都会被系统丢弃。因此如果一个程序有输出,但是又不希望接收到它的输出时,就可以把输出重定向到/dev/null
下。
// 关闭标准输入、输出和错误
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 打开/dev/null作为标准输入、输出和错误
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY);
五、setsid
比喻:
保安队长(守护进程)需要:
- 脱离原部门(终端会话) →
setsid()
- 自己成立新部门(新会话) → 成为会话组长
- 不再受原部门影响 → 独立运行
技术总结:
概念 |
比喻 |
代码/命令 |
关键特性 |
终端 |
公司前台 |
|
用户交互入口 |
进程组(父子) |
项目组 |
|
共享终端信号 |
会话 |
项目周期 |
|
关联终端,管理多个进程组 |
守护进程 |
24小时保安 |
|
脱离终端,长期运行 |
daemon
setsid
的用法还是有点复杂了,在setsid 生成新会话后还要close,open- 为此
Linux
提供了另一个系统调用daemon
- 它封装了上述所有过程,可以快速创建一个守护进程。需要头文件
<unistd.h>
。
参数:
nochdir
:改变工作目录
- 传入
0
:改变工作目录为根目录- 传入
1
:保持当前工作目录
nclose
:改变输入输出流
- 传入
0
:输入输出流重定向到/dev/null
- 传入
1
:不改变输入输出流
原先的代码就可以变成:
#include <unistd.h>
#include <sys/types.h>
int main()
{
daemon(0, 0);
while(true)
{}
return 0;
}
这就是一个功能全面的守护进程了。