尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux 服务器如何从“普通程序”变成“后台服务”?一文吃透 PGID/SID、作业控制与守护进程底层机制

Linux 服务器如何从“普通程序”变成“后台服务”?一文吃透 PGID/SID、作业控制与守护进程底层机制 第一部分 进程间关系的基石 —— 进程组PGID、会话SID与控制终端绑定机制一、 一个“学校机房上机”的生活故事想象一下这样一个场景你今天去学校机房上机写代码整个过程有三个层面的东西你学生个体在系统里干活的最基础单元。你的上机小组老师布置了一个作业“清洗数据并统计行数”你叫了两个室友三个人流水线分工合作你负责读文件室友 A 负责过滤室友 B 负责数数。你们三人组成了一个“流水线作业小组”。整间机房你登录的这台电脑屏幕和键盘你插上校园卡登录进电脑屏幕亮起键盘归你用。这台电脑和面前的这套桌面就是你的“独立上机空间”。在 Linux 操作系统内核里就是照着这套一模一样的逻辑设计的二、 第一层推演为什么要有“进程组”从单人到小组1. 从“一个人”到“一个小组”单个人进程 PID平时你在终端敲一个./test就相当于你一个人在干活系统给你分配一个工号叫PID。搭伙干活的小组进程组 PGID很多时候单靠一个程序搞不定事情。比如你在终端输入cat bigfile | grep error | wc -l这行命令一敲系统瞬间创建了 3 个进程cat、grep、wc。它们三者之间连着管道必须生死与共、配合完成同一个任务。为了方便统一管理这三个干活的人操作系统干脆把他们拉进一个群这个群就叫进程组PGID。2. 谁来当“组长”一个小团队必须有个带头的管道命令里最左边的第一个进程比如上面的cat谁先站出来挑头谁就是组长。组长的身份证号PID直接作为整个小组的群号PGID。组长判定原则只要一个人的PID PGID那他就是组长。字符画拆解用户在终端敲下cat bigfile | grep error | wc -l | -------------------------------------------------- | | | v v v [ 进程 1: cat ] [ 进程 2: grep ] [ 进程 3: wc ] * 工号 PID: 1001 * 工号 PID: 1002 * 工号 PID: 1003 * 组号 PGID: 1001 * 组号 PGID: 1001 * 组号 PGID: 1001 | (自己是 1001, 组号也是 1001, 所以 cat 是该小组组长!)三、 第二层推演为什么要有“会话”从小组到整间教室现在你明白了一个命令流水线就是一个小组。那你在终端里可以干很多事情啊你开了第一个小组在后台解压大文件又开了第二个小组在后台编译 C 项目同时你前台还在用vim写代码。这些形形色色的小组全都是你这次打开终端登录系统搞出来的。1. 什么是“会话Session, SID”当你用 Xshell 输入账号密码连上 Linux 那一刻系统就为你分配了一个专属活动室会话 Session。谁是这个活动室的开门人和管理员呢就是你的登录窗口bash命令行终端。bash是整个活动室的会话首进程大堂经理它的工号就是这个活动室的房间号SID。2. 会话里面装了什么一个会话活动室里装了你在这次登录期间启动的所有进程组。四、 第三层推演控制终端为什么CtrlC这么听话既然你的活动室里有这么多小组在同时干活那问题来了你面前的物理键盘和屏幕控制终端到底归谁用操作系统立下了两条非常公平的规矩1. 规矩一键盘和屏幕只能由“一个前台小组”独占比如你正在运行./test它在屏幕上狂刷屏并且等着你按键盘输入。此时它就是前台小组前台进程组。那些在后台默默下载、默默编译的小组叫后台小组后台进程组。后台小组不能抢键盘输入。2. 规矩二打断信号CtrlC精准投递当你在键盘上猛按Ctrl C时你在物理上按的是键盘但操作系统怎么知道你想杀谁操作系统会把中断信号只发给当前霸占着屏幕和键盘的前台小组你的后台下载、后台编译小组完全不会被误杀它们安然无恙。五、 最后的致命一击为什么你关掉 Xshell服务就死了现在整个逻辑链条完全闭合了请看这个过程你的服务器程序./tcp_server 8080 虽然加了扔到了后台但它依然在你这次登录分配的“活动室会话”里面。当你直接关掉电脑、退出 Xshell 或拔掉网线时活动室的管理中心检测到“糟糕客户端的屏幕和键盘都断开了这间活动室没人要了”管理中心会向这个活动室的开门人bash发送一张清场通知单 ——SIGHUP信号挂断信号意思是电话被挂断了。bash收到清场通知后准备下班下班前它会把通知单广播给这个活动室里的所有小组“下班清场了全部就地解散”你的./tcp_server收到这张通知单默认反应就是立刻自杀退出。字符画总结[ 你点击右上角叉掉 Xshell ] --- 终端断开连接 | v [ 操作系统向活动室 (Session) 广播 SIGHUP 清场信号 ] | ---------------------------- v v [ 前台小组被杀死 ] [ 你的后台 ./tcp_server 被杀死 ]六、 面试题与解析面试题请用通俗的语言解释为什么加了后台运行的程序在关闭终端后依然可能会退出详细解析与标准答案使用仅仅是把任务放到了当前会话的后台进程组让出了键盘和屏幕输入权但它并没有脱离当前的会话Session。该进程的生命周期依然与打开该终端的控制进程bash绑定在一起。一旦终端窗口关闭或网络连接断开操作系统内核会向该会话的所有进程组发送SIGHUP挂断信号。进程在未对SIGHUP做特殊忽略处理的情况下默认行为就是终止退出。第二部分 终端的调度艺术 —— 作业控制Job Control与前后台状态流转一、 业务引入什么是“作业Job”平时我们在终端敲命令经常会遇到这种尴尬场面你写了一个死循环程序./test正在前台疯狂打印屏幕全被它占满了你按回车敲其他命令根本没反应或者你正在用vim写代码突然想临时查一下某个头文件难道要把vim彻底关掉退出来查吗通俗比喻舞台与后台模型终端屏幕就像一个小剧场舞台前面只有一个麦克风输入/输出。作业Job就是为了完成某一个演出任务上台的一组人比如由管道连起来的一串程序cat | grep。前台作业正拿着麦克风在舞台中央表演的演员。你必须等他演完或者把他按暂停你才能说上话。后台作业在幕后默默化装、搬道具的演员不占用舞台上的麦克风。二、 核心三大按键与快捷操作遥控器按键操作系统在键盘驱动里埋了 3 个具有最高统治权的“快捷手势”它们专门用来对付前台作业1.Ctrl C直接终结斩立决底层原理向当前舞台中央的前台作业发送SIGINT2号中断信号。大白话嫌台上演员演得烂直接拿大锤敲晕拖走程序立即死亡。2.Ctrl \致命退出带遗体检查的死刑底层原理向当前前台作业发送SIGQUIT3号退出信号并在磁盘生成 Core Dump核心转储文件。大白话不仅把人打死还要拍下死亡现场的遗体照片供事后验尸Debug。3.Ctrl Z暂停并打入冷宫定海神针底层原理向当前前台作业发送SIGTSTP20号停止信号。大白话按一下暂停键演员立刻定在原地不准动被系统直接一脚踹到后台的“暂停区”并把舞台麦克风重新交还给你的 Shell 终端三、 常用作业调度命令玩转前后台我们来看这几个每天都会用到的调度命令[ 舞台中央: 前台运行 ] | | Ctrl Z (挂起/暂停) v [ 后台冷宫: Stopped 暂停状态 ] | --- 输入 bg %作业号 --- [ 后台活动区: Running 运行状态 ] | | ---------- 输入 fg %作业号 ---1. 启动就直接进后台符号命令sleep 100 | sleep 200 效果系统会返回一个[1] 4223。[1]这是作业号Job ID你在本终端给它编的序号。4223这是该作业在系统里的真实进程号PID。2. 查看后台所有的演员名单jobs命令jobs -l-l表示把每个进程的 PID 也详细打印出来。看懂标记加号代表默认作业。如果你敲命令不写作业号系统默认就对它操作。-减号代表第二顺位候选作业。当带有的作业退出了它就升级为。状态分为Running后台正在跑、Stopped被暂停了、Done已执行完毕。3. 把后台暂停的人叫醒在后台继续跑bgBackground命令bg %1%1代表 1 号作业。效果原本被CtrlZ暂停住的 1 号作业重新恢复心跳在后台继续默默运行。4. 把后台干活的人请回前台舞台fgForeground命令fg %1效果把 1 号作业直接拉到前台屏幕重新交给他他又霸占了你的键盘。四、 动手实操小演示跟着走一遍写一段最简单的死循环 C 代码test.cc#includeiostream#includeunistd.hintmain(){while(true){std::coutI am running...std::endl;sleep(1);}return0;}在终端里执行以下连续动作直观感受状态迁移# 1. 编译并运行g test.cc-otest./test# 2. 屏幕开始疯狂刷屏此时按下键盘Ctrl Z# 终端提示[1] 已停止 ./test# 此时你拿回了终端控制权./test 被冻结在后台了# 3. 查看后台作业状态jobs-l# 会看到状态是 Stopped# 4. 让它在后台恢复运行不再刷屏阻塞你敲命令bg%1# 5. 再次拉回前台fg%1# 此时屏幕又开始被接管刷屏按 Ctrl C 彻底杀死它五、 高频面试题与硬核解析面试题 1作业号Job ID和进程号Process ID / PID有什么区别详细解析与标准答案作用范围不同PID是操作系统全局唯一的全系统任何进程都不会重复而Job ID仅仅在当前这个终端Shell 会话内部有效你在另一个终端开一个任务它的作业号也可能是[1]。一对多关系一个作业号可以对应多个进程。比如cat file | grep a 是一个作业只有一个 Job ID但底层会同时生成两个进程对应两个不同的 PID。面试题 2为什么我们在终端里按CtrlZ可以暂停前台程序但对加了的后台程序按CtrlZ却毫无反应详细解析与标准答案终端的快捷键信号CtrlC对应SIGINT、CtrlZ对应SIGTSTP是由内核的终端驱动程序负责解析的。终端驱动程序有严格的权限约束它只会将键盘生成的信号精准递送给当前会话中的“前台进程组”。后台进程组没有控制终端的直接读写权因此按键产生的信号根本不会投递给后台作业。第三部分 服务的自立门户 —— 守护进程Daemon底层原理与标准 6 步法一、 什么是守护进程精灵进程 / Daemon通俗比喻毕业离校与自立门户普通后台进程就像住校的大学生虽然你在寝室里睡觉不露面在后台但只要学校放假关校门清场关闭终端宿管大爷bash就会把你轰走。守护进程Daemon就像毕业自立门户的打工人。你在校外自己租了房子、买了房产证新会话学校关门、宿管下班跟你没有半毛钱关系。你直接受当地政府系统 1 号进程systemd/init直接管辖。守护进程的核心特征脱离终端没有控制终端在ps axj中TTY显示为?。孤儿领养父进程 IDPPID直接变成1由操作系统祖宗进程接管领养。自立门户自己是新会话的建立者也是新进程组的组长PID PGID SID。默默奉献不与键盘和屏幕有任何交互生命周期随操作系统开机而生、关机而灭。二、 核心系统调用精讲为了完成自立门户操作系统为我们提供了几个关键系统调用① 系统调用setsid()—— 颁发独立房产证#includeunistd.hpid_tsetsid(void);系统调用详细信息使调用进程摆脱原会话、原进程组和原控制终端建立一个全新的会话。调用成功后该进程成为新会话的会话首进程SID PID同时成为新进程组的组长PGID PID。致命约束调用者绝对不能是当前进程组的组长如果组长调用setsid内核直接报错返回 -1。一句大白话说明小组成员可以脱离原团队自立门户当掌门但原小组的组长已经被“组长”名分锁死了系统不允许组长直接自立门户。② 系统调用dup2()—— 管道暴力重定向#includeunistd.hintdup2(intoldfd,intnewfd);系统调用详细信息将已经打开的文件描述符oldfd复制覆盖到newfd上。如果newfd已经打开内核会先关闭它。一句大白话说明把本来接在显示器和键盘上的自来水管0号输入、1号输出、2号错误暴力弯折接到一个黑洞文件里去。三、 守护进程化的标准 6 步法蜕变流水线如何巧妙绕过“组长不能调用setsid”的死穴并完成真正的脱胎换骨标准 6 步法如下[ 1. 忽略致命信号 ] ---------- signal(SIGPIPE, SIG_IGN), signal(SIGCHLD, SIG_IGN) | [ 2. 借刀杀人: fork() ] ------ 父进程 exit(0) 退出子进程继承 PGID 成为普通组员 | [ 3. 自立门户: setsid() ] ---- 子进程不是组长成功调用 setsid() 建立新会话 | [ 4. 更改工作目录 ] ---------- chdir(/)防止当前磁盘目录被占用无法卸载 | [ 5. 重设文件权限掩码 ] ------ umask(0)拿回完整的文件创建权限 | [ 6. 隐身黑洞: 重定向 0/1/2 ] - 打开 /dev/null用 dup2 把标准输入/输出/错误全塞进去逐步深度推演第一步忽略信号自我防暴signal(SIGPIPE, SIG_IGN)网络通信中如果客户端强行断开服务端继续write会触发系统发送SIGPIPE导致崩溃必须忽略。第二步fork() 父进程退出最精妙的一步刚才说了进程组组长调用setsid()必死。既然如此我们直接fork()生一个子进程因为组长的名分留在父进程身上子进程只是一个“普通的组员”。此时父进程立刻exit(0)自杀子进程继续往下走这样就完美满足了“非组长调用”的铁律。第三步调用setsid()自立门户子进程非组长调用setsid()瞬间成功脱离终端成为新 Session 的大 Boss第四步chdir(/)防占用如果程序是在一个 U 盘或者挂载的磁盘目录下启动的它一直活着会导致该磁盘永远显示“设备繁忙”无法卸载。因此通常把它移到系统的根目录/。第五步umask(0)权限自由清除从终端继承来的文件权限掩码保证后续创建日志、文件时拥有最纯净的权限。第六步重定向到/dev/null防吐死什么是/dev/null它是 Linux 内核提供的一个“黑洞设备”无底洞。往里面写的数据全部人间蒸发从里面读数据会直接读到 EOF。因为脱离终端后已经没有显示器了如果程序里还有std::cout向已失效的终端写数据会报错因此把0, 1, 2全部通过dup2重定向到/dev/null。四、 核心代码落地Daemon.hpp根据上述 6 步法我们写出标准的工业级头文件#pragmaonce#includeiostream#includecstdlib#includeunistd.h#includesignal.h#includesys/types.h#includesys/stat.h#includefcntl.hnamespaceDaemonModule{constchar*default_root/;constchar*dev_null/dev/null;// ischdir: 是否切换工作目录到根目录// isclose: 是否彻底关闭或重定向 0, 1, 2voidDaemon(boolischdirfalse,boolisclosetrue){// 1. 忽略可能引起程序异常退出的信号signal(SIGCHLD,SIG_IGN);signal(SIGPIPE,SIG_IGN);// 2. 借刀杀人让自己绝对不要成为组长进程if(fork()0){exit(0);// 父进程是组长直接退出}// 3. 此时只有子进程能走到这里调用 setsid 自立门户建立新会话setsid();// 4. 更改当前进程的工作目录if(ischdir){chdir(default_root);}// 5. 重设文件掩码umask(0);// 6. 已经变成守护进程不再与终端输入输出关联重定向到黑洞if(isclose){intfdopen(dev_null,O_RDWR);if(fd0){dup2(fd,0);// 标准输入 - /dev/nulldup2(fd,1);// 标准输出 - /dev/nulldup2(fd,2);// 标准错误 - /dev/nullclose(fd);// 复制完成后关闭原 fd}}}}五、 高频面试题与硬核解析面试题 1在实现守护进程时为什么必须执行一次fork()并且让父进程exit(0)详细解析与标准答案核心原因是系统调用setsid()的硬性约束调用进程绝不能是当前进程组的组长Process Group Leader。当我们在命令行启动一个程序时该程序默认就是它所在进程组的组长PID PGID。通过调用fork()子进程会继承父进程的进程组 IDPGID但子进程分配到了一个全新的进程 IDPID因此子进程必然满足PID ! PGID即子进程绝不是组长。此时让父进程退出子进程就能合法、安全地调用setsid()成功自立门户建立新会话。面试题 2为什么守护进程要将标准输入、标准输出、标准错误0, 1, 2重定向到/dev/null而不是直接close(0); close(1); close(2);详细解析与标准答案如果直接close关闭 0、1、2之后程序在打开第一个文件或网络套接字socket时根据 Linux 的文件描述符分配规则“分配最小且未占用的 fd”新打开的sockfd就会拿到描述符0。若后续代码或第三方库中无意间调用了一句printf或std::cin就会向原本属于网络套接字的描述符0错误读写直接破坏正常的网络通信协议重定向到/dev/null既占住了 0、1、2 的坑位又将所有多余的终端打印安全吞噬保证了后续socket从 3 号描述符开始安全分配。第四部分 工程实战与状态观测 —— 网络服务守护化与生命周期管理一、 实战装配给网络服务器装上“守护之心”守护进程的代码模块化封装Daemon.hpp完成后上层的使用变得极其简洁。以我们的网络计算器服务端入口TcpServerMain.cc为例只需要在初始化网络服务之前调用一次Daemon()即可#includeiostream#includememory#includestring#includeTcpServer.hpp#includeDaemon.hpp// 使用方法: ./tcp_server 8080intmain(intargc,char*argv[]){if(argc!2){std::coutUsage:\n\t argv[0] portstd::endl;return1;}uint16_tportstd::stoi(argv[1]);// 1. 在创建任何套接字之前完成守护进程化自立门户// ischdir false: 保持在当前工作路径方便相对路径写日志// isclose true: 开启 0/1/2 重定向到 /dev/nullDaemonModule::Daemon(false,true);// 2. 正常初始化并进入网络监听死循环std::unique_ptrTcpServersvrstd::make_uniqueTcpServer(port);svr-Init();svr-Loop();return0;}二、 现象观察守护进程启动后的直观变化当你在 Linux 终端执行./tcp_server 8080时会观察到两个极为明显的现象命令行瞬间返回敲击回车后程序并没有阻塞卡住你的终端而是直接弹回了命令提示符因为父进程调用exit(0)闪电退出了子进程在后台自立门户继续跑。终端没有任何字符输出哪怕服务端代码里写满了std::cout TcpServer running...屏幕上也空空如也因为标准输出1已经被dup2导流到了/dev/null吞噬。三、 核心侦查技能使用ps axj辨别真正的守护进程操作系统到底有没有把我们的程序变成合法的守护进程我们使用系统的状态指令进行验证psaxj|head-n1psaxj|greptcp_server|grep-vgrep命令参数拆解a不仅列出当前用户的进程也列出所有其他用户的进程x不仅列出有控制终端的进程也列出所有无控制终端的进程j列出与作业控制PPID, PID, PGID, SID, TTY相关的核心信息。字符画拆解守护进程在系统里的特征画像PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 1 4885 4885 4885 ? -1 Ssl 1000 0:00 ./tcp_server 8080 ^ ^ ^ ^ ^ ^ | ---------- | | | | | - TPGID-1: 没有前台进程组绑定 | | -------------- TTY?: 彻底脱离控制终端 | ----------------------- PID PGID SID: 自立门户自己是组长且是会话首进程 ---------------------------------- PPID1: 被系统的 1 号进程 (systemd/init) 领养只要你的进程满足了上面图中的PPID 1、PID PGID SID以及TTY ?这三大金标准它就已经是标准的守护进程。四、 守护进程的生命周期与运维管理既然守护进程没有终端按CtrlC和CtrlZ对它完全无效我们平时怎么与它交互、如何关闭它1. 查看输出与日志定位因为标准输出和标准错误被重定向到了/dev/null所以工业级守护进程必须具备落盘日志系统如打入log.txt或系统日志/var/log/messages。通过tail -f log.txt观察它的实时心跳。2. 安全关闭守护进程由于它脱离了前台终止它需要向其发送进程信号优雅退出向其发送SIGTERM15号信号请求正常终止kill-154885强制斩杀若程序卡死发送SIGKILL9号信号内核强制清除kill-94885一键按名关闭killalltcp_server五、 高频面试题与硬核解析面试题 1如何验证一个后台运行的进程是真正的“守护进程”而不是普通的“后台作业”详细解析与标准答案通过ps axj命令查看进程的关键字段信息查看TTY终端列普通后台作业依然绑定着虚拟终端如pts/0、pts/1而真正的守护进程TTY列必须显示为?代表无控制终端。查看父进程PPID普通后台作业的父进程依然是启动它的终端bash而守护进程的父进程必须是1号进程systemd或init。查看PID、PGID与SID守护进程通过setsid()自立门户必然满足PID PGID SID即它自身是独立会话首进程也是独立进程组的组长。面试题 2Linux 系统函数库其实自带了一个daemon()库函数为什么很多工业级开源项目如 Redis/Nginx更倾向于自己手写实现一套Daemonize详细解析与标准答案系统自带的int daemon(int nochdir, int noclose);虽然封装了基本的fork、setsid和重定向但存在局限信号屏蔽不够灵活手写实现可以在派生前精确屏蔽如SIGPIPE、SIGHUP等关键信号重定向可控性差标准daemon()内部默认将 0/1/2 导向/dev/null而自实现版本可以根据配置无缝将标准输出/错误重定向到指定的自定义日志文件文件描述符与锁管理很多工业级组件在自立门户后需要记录 PID 文件单例锁机制防止重复启动手写逻辑能更好地穿插 PID 文件锁和权限umask重置操作。
返回列表