一,守护进程
1,会话的概念
对话的结构包括进程组,进程,线程
调度器的调度策略是以线程为大内进行调度的1,多进程并发和多线程并发
- 多进程是操作系统为了多任务而自然演化出来的,先有了各种实现,后来才被标准化,所以是「先有事实,再有规范」;
- 多线程是为了高效并发而主动设计的,先定好了模型和接口规范,再去各个系统实现,所以从一开始就更「规范」。
所以一般来说很多都要支持多线程并发的
2,前台进程组和后台进程组
前台进程组:一个终端有且仅有一个,独占标准输入(可以读取键盘),可以正常标准输出。
后台进程组:将前台运行的进程组切换到后台状态继续运行,禁止读取外部键盘输入,但可以正常输出。
如果后台进程组强行尝试读取标准输入(键盘),操作系统会发送信号将其暂停,而不是杀死。
比如前台正在压缩一个大文件,占用命令行无法操作,我们在压缩指令后面加
&,就能把它放到后台进程组运行,不影响前台使用命令行。为什么后台进程组尝试读标准输入,会被暂停?
这是操作系统为了保护终端交互设计的强制规则,原理超简单:
- 你的键盘输入只能给一个对象(前台进程组),这是终端的独占资源;
- 后台进程如果能随便读键盘,就会和前台进程抢夺输入(比如你敲命令,被后台程序偷偷读走),导致命令行彻底混乱;
- 内核检测到:后台进程试图读取终端标准输入 → 立刻给这个进程发送
SIGTTIN信号;- 这个信号的默认行为是暂停进程(不是杀死!),进程会停止运行,直到你把它切回前台。
2,函数的理解
首先创建守护进程对应的函数是
上面这个是对于这个函数的描述,
如果调用进程不是进程组组长 ,
setsid()会创建一个新会话 。
调用进程将成为新会话的会话组长 (即其会话 ID 会被设置为与自身进程 ID 一致)。调用进程同时也会成为该会话内一个新进程组的进程组组长 (即其进程组 ID 会被设置为与自身进程 ID 一致)。
调用进程将是这个新进程组和新会话中唯一 的进程。
新会话在初始状态下没有控制终端。
setsid()创建新会话的那一瞬间 ✅这个新会话里 有且仅有 1 个进程组 ✅
这个进程组里 有且仅有 1 个进程 (就是调用
setsid()的进程)这里很难理解,主播也是好不容易画出了这个示意图,有错误可以随时指出
下面让我们逐步来理解这些话
1,如果调用进程不是进程组组长 ,
setsid()会创建一个新会话。2,调用进程将成为新会话的会话组长 (即其会话 ID 会被设置为与自身进程 ID 一致)。
调用进程同时也会成为该会话内一个新进程组的进程组组长 (即其进程组 ID 会被设置为与自身进程 ID 一致)。新会话在初始状态下没有控制终端。
一、会话ID=自身进程ID的作用
- 彻底脱离原会话 原来的会话 ID 是绑定终端的,现在把会话 ID 改成自己的 PID,意味着:
- 这个进程不再属于原来的终端会话,和原终端、原会话彻底解绑。
- 新会话没有控制终端,关终端、退 SSH 都不会影响这个进程。
- 成为会话的「根」 会话 ID = PID,说明这个进程是新会话的创建者和唯一组长 ,整个会话的生命周期由它主导:
- 只有它能为这个会话申请控制终端(一般守护进程不会这么做)。
- 会话里后续所有进程组、子进程,都归这个会话管理。
- 全局唯一标识PID 是系统里唯一的,所以新会话 ID 也天然唯一,不会和其他会话冲突。
二、进程组ID=自身进程ID的作用
- 彻底脱离原进程组 原来的进程组 ID 是父进程或其他组长的,现在改成自己的 PID,意味着:
- 这个进程不再属于原来的进程组 ,不受原进程组的信号影响(比如原进程组收到
Ctrl+C,不会传给它)。- 成为进程组的「根」 进程组 ID = PID,说明这个进程是新进程组的唯一组长 :
- 它可以管理组内后续创建的子进程(比如给整个组发信号)。
- 新进程组和原进程组完全隔离,信号、资源都互不干扰。
- 保证进程组独立每个进程组有唯一 ID,用 PID 保证不会和其他进程组重复,避免管理混乱。
三、组合起来的终极效果 ✨
这两个设置同时生效后,进程就变成了 **「无父无母、自己当家」的独立个体 **:
- ✅ 不受终端控制 :关终端不会杀死它(守护进程的核心要求)。
- ✅ 不受原进程组 / 会话信号干扰 :比如
Ctrl+C、终端挂断信号都传不到它这里。- ✅ 自己管理自己的后代:后续创建的子进程都会归到它的会话 / 进程组里,由它统一管理。
- ✅ 系统层面唯一标识:会话 ID、进程组 ID 都等于它的 PID,内核能精准识别和管理。
3,查看守护进程
不难看到红色框住的TTY为?的,那么就是说明这个是脱离终端控制的,接下来就看PID,PGIDSID是不是一样的
还有就是PPID一般都是为1的
为什么守护进程的 PPID 几乎都是 1?这不是巧合,是人为故意做的!
核心一句话
守护进程要彻底断绝所有依赖:
- 用
setsid()断绝终端的依赖- 让父进程主动退出 ,断绝父进程 的依赖 → 自动被系统
PID=1进程收养所以守护进程的 PPID 天然就是 1!分步拆解(标准守护进程的强制步骤)
这是 Linux 写守护进程固定不变的套路,一步都不能少:
步骤 1:父进程 fork 创建子进程
父进程(比如终端 shell、你的程序)生一个子进程。此时:
- 子进程 PPID = 父进程 PID
- 子进程是进程组的普通成员(不是组长)
步骤 2:父进程直接退出、自杀!(最关键一步)
父进程立刻死掉,子进程变成孤儿进程。系统规则:
孤儿进程必须被系统的「始祖进程」
PID=1(init/systemd)收养此时:
- 子进程 PPID 直接变成 1
- 子进程彻底没有了「原来的父进程」,无牵无挂
步骤 3:子进程调用
setsid()因为父进程已经退出,子进程不是进程组组长,满足调用条件:
- 创建新会话,脱离终端
- 会话 ID = 进程组 ID=PID
为什么非要让 PPID=1?(3 个核心理由)
- 父进程会 "拖累" 守护进程
原来的父进程(比如终端、普通程序)随时可能关闭:
- 父进程一死,子进程就会被系统骚扰
- 让父进程主动退出,从根源上杜绝拖累
PID=1是系统唯一永远不死的进程
PID=1(init/systemd)是系统启动第一个进程,关机才会停- 它专门负责收养孤儿、回收进程资源,绝对靠谱
- 守护进程交给它管,永远不用担心没人兜底
- 满足
setsid()的硬性前提这是最现实的原因:只有父进程退出,子进程才不是进程组组长,才能调用
setsid()为了调用setsid脱离终端,必须先让父进程死 → PPID 自然变 1。单实例的守护进程,同一时刻只运行运行一次: 锁文件 /var/run/name.pid
每个守护进程启动时都会将pid写入锁文件中
启动脚本文件:/etc/rc.d/rc.local 不同系统位置不同
二,守护进程的实例
cpp#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include <unistd.h> #define FNAME "/tmp/out" static int daemonize(){ pid_t pid; int fd; pid = fork(); if(pid < 0){ perror("fork()"); return -1; } if(pid > 0){ // 父进程直接退出 exit(0); } // 重定向输入输出到 /dev/null(丢弃所有终端输出) fd = open("/dev/null",O_RDWR); if(fd < 0){ perror("open()"); return -1; } dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if(fd > 2) close(fd); setsid(); // 创建新会话,脱离终端 chdir("/"); // 切换工作目录到根目录,防止占用磁盘 return 0; } int main(){ FILE* fp; if(daemonize()){ exit(1); } fp = fopen(FNAME, "w"); if(fp == NULL){ perror("fopen()"); exit(1); } // 死循环,每秒写入数字 for(int i = 0; ;i++){ fprintf(fp,"%d\n", i); fflush(NULL); // 刷新缓冲区,立即写入文件 sleep(1); } exit(0); }
可以观察到这这些都是一样的,说明创建成功了
杀死进程就是 kill 进程号
三,系统日志
系统日志
syslogd服务, 所有需要写系统日志的操作将其交给syslogd
openlog(); 关联系统日志服务
syslog();
closelog();
通过信号可以将异常终止转变为正常终止
cpp#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> #include<syslog.h> #include<errno.h> #include<string.h> #define FNAME "/tmp/out" static int daemonize(){ pid_t pid; int fd; pid = fork(); if(pid < 0){ // perror("fork()"); return -1; } if(pid > 0){ // 父进程直接退出 exit(0); } // 重定向输入输出到 /dev/null(丢弃所有终端输出) fd = open("/dev/null",O_RDWR); if(fd < 0){ // perror("open()"); return -1; } dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if(fd > 2) close(fd); setsid(); // 创建新会话,脱离终端 chdir("/"); // 切换工作目录到根目录,防止占用磁盘 return 0; } int main(){ FILE* fp; //我接下来要打日志了,我的名字叫 // mydaemon,日志带上 PID,我是守护进程类型的程序! openlog("mydaemon", LOG_PID, LOG_DAEMON); if(daemonize()){ //补药写\n,因为格式是syslog服务决定的 syslog(LOG_ERR, "daemonize() failed!"); exit(1); } else{ syslog(LOG_INFO, "daemonize() successded!"); } fp = fopen(FNAME, "w"); if(fp == NULL){ // perror("fopen()"); syslog(LOG_ERR, "fopen():%s", strerror(errno)); exit(1); } syslog(LOG_INFO, "%s was opend.", FNAME); // 死循环,每秒写入数字 for(int i = 0; ;i++){ fprintf(fp,"%d\n", i); fflush(NULL); // 刷新缓冲区,立即写入文件 syslog(LOG_DEBUG, "%d is printed.", i); sleep(1); } fclose(fp); closelog(); exit(0); }可以看到程序已经运行了
可以看到这个日志已经输出进来了
【Linux系统编程】进程 守护进程与实现/系统日志
一只自律的鸡2026-03-19 11:30
相关推荐
tobias.b1 小时前
ubuntu 系统维护门豪杰2 小时前
使用WSL2安装Ubuntu子系统2501_946490382 小时前
城市地标光影呈现技术实践——Hirender集群与多边形融合带在《生命之树》的实操解析面对疾风叭!哈撒给2 小时前
Linux之Docker使用JDK21安装包制作JDK21镜像包九皇叔叔2 小时前
【保姆级教程】CentOS 7.5/RHEL 7.x 编译安装 Redis 6.0.1绘梨衣的sakura路2 小时前
[特殊字符] 2026 年 AI 自动化新范式:OpenClaw 核心 Skill 精选与实战指南RisunJan2 小时前
Linux命令-mail (发送和接收电子邮件)万象.2 小时前
Linux套接字socket编程(含TCP,UDP)历程里程碑2 小时前
39. 从零实现UDP服务器实战(带源码) V1版本 - Echo server








