首页 / 资讯中心 / 文章详情

Linux系统篇48:番外篇(一)——进程组、会话与守护进程,Ctrl+C 打不中后台、终端一关进程就死,原因都在这一层关系

Linux系统篇48:番外篇(一)——进程组、会话与守护进程,Ctrl+C 打不中后台、终端一关进程就死,原因都在这一层关系 ★ FEATURED ARTICLE
本文收录于「流浪」的系列专栏Linux系统⚙️C数据结构与算法PythonLangChain LangGraph️MySQL 数据库Git 工具计算机网络LLM大厂面试、八股学习筑基专栏 博客主页流浪 原创首发于 CSDN前言讲进程控制时提过./可执行占住终端、加退到后台也讲过 CtrlC 变成 SIGINT 投给前台但后台为什么打不中、终端一关为什么跟着没前面没交代。这篇补中间那层进程组、会话、控制终端怎么套在一起守护进程又怎么一次摘干净。一、一次作业在终端里就是一个进程组1.1 三个 sleep 的父进程是同一个1. 管道作业由 shell 统一建组敲sleep 1000 | sleep 2000 | sleep 3000 shell 返回作业号和 PID三个 sleep 在后台一起睡着管道要求前一个的输出接后一个的输入三个进程必须同时启动、一起被管用作业控制格式打出它们的身份psaxj|head-1psaxj|grepsleep|grep-vgrep2. 三行输出里 PPID、PGID、SID 各自成组j是 BSD 的作业控制格式等价于-o ppid,pid,pgid,sid,tty,tpgid,stat,uid,time,command示例里 bash 的 PID 是 1358016三个 sleep 的 PID 是 1358036、1358037、1358038PPID 全是 1358016说明三个都是同一个 bash fork 出来的互为兄弟但兄弟只说明来源相同绑成整体的是 PGID 那一列1.2 PGID 相同说明三个进程在同一个组里1. PGID 就是组长的 PID手册的定义很直接进程组 ID 等同于该组组长的 PID一次作业对应一个进程组组内共享同一个 PGID也是内核投递信号的单位kill -PGID能一次打中整组作业控制全靠这个2. 管道作业的组长是最左边那个进程示例里 PGID 是1358306正好等于sleep 1000的 PID——管道最左边这个进程当了组长所以 PGID 不等于 bash 的 1358016组是这条管道自己的不是 shell 的交互 shell 为每个作业单独建组同样的命令写进脚本跑没有作业控制子进程直接继承 shell 的 PGID单进程作业同理./可执行 丢到后台它自己就是组长作业在终端里的身份从来不是「一个进程」而是「一个进程组」。二、一个会话对应一次登录SID 全会话统一2.1 同一会话里所有进程的 SID 相同SIDSession ID会话首进程session leader的 PID会话是一个或多个进程组的集合用于终端作业控制、信号管理。1. SID 取会话首进程的 PID上面三行的 SID 都是 1358016也就是 bash 的 PIDbash 是会话首进程会话 ID 直接取它的 PID一个会话可以装多个进程组但不管装几个组SID 都是同一个值2. 会话是进程组的上层容器层级固定两层会话包含进程组进程组包含进程一个进程同时只属于一个组、一个会话这个归属 fork 时整份继承、exec 也不改所以新起的进程默认落在父进程的会话里换组用setpgid换会话只有setsid一条路而且带着「不能是组长」的硬门槛2.2 登录一次就新建一个会话1. xshell 登录时系统做了四件事分配一个伪终端设备/dev/pts/N这是连接在系统里的落脚点fork 一个子进程exec 成 bash就是窗口里看到的 shellbash 调 setsid 建新会话自己成为会话首进程SID 与 PGID 都等于自己的 PID于是 bash 既是独立进程又独占一个进程组是会话里资格最老的成员2. 之后敲的每条命令都继承同一个 SID在 bash 里起的进程 SID 一律继承 1358016PGID 由 shell 按作业重新分配同一个窗口里ps axj | grep pts/0会看到一长串 SID 相同的进程多开一个窗口就是多一个会话新的 pts 设备、新的 bash、新的 SID会话解决的是「这个终端上的一切归谁管」终端只有一个登录的人却要开很多作业会话把它们圈成可整体撤销的单位。三、控制终端决定了信号的投递范围3.1 bash 一出生就拿着三个指向终端的描述符1. 0、1、2 全部指向同一个终端设备会话首进程的标准输入、输出、错误都指向/dev/pts/0是登录程序开好再传下来的不是 bash 自己 open 的终端关掉时它们还留在原地这正是后面要重定向的原因一个会话最多挂一个控制终端终端同一时刻只认一个前台进程组2. /proc 里能看到这三条链接ls-l/proc/1358016/fdps axj的 TTY 列写 pts/0 说明进程还挂在终端上写?说明已经脱离TPGID 列写的是终端当前的前台进程组示例里是 1358016也就是 bash 所在的组3.2 键盘信号只投前台进程组终端挂断才牵连后台1. CtrlC 的投递范围是前台进程组按下 CtrlC终端驱动把它翻译成 SIGINT投给当前终端上前台进程组的每个成员键盘只有一个输入的数据必须明确交给一个进程组而它只给前台进程组所以会话内部的进程组必须分成前台和后台一次会话有且只有一个前台进程组后台进程组可以有多个后台作业的组不在前台键盘输入碰不到它所以打不中fg 把它拉回前台就能打中直接启动一个任务就敲不进命令是它占着前台丢到后台还能继续敲是前台让回给了 shell作业控制就是这套关系的入口jobs 列出后台任务fg 把指定任务提到前台CtrlZ 把前台任务暂停并切到后台bg 让停下的任务继续跑从按键到信号位图这条链路在《Linux系统篇29——信号一从 CtrlC 到内核位图》里拆过这里只取结论2. 终端挂断时 SIGHUP 沿会话传下来会话挂着控制终端时终端一旦断开关窗口、SSH 掉线内核给会话首进程发 SIGHUPbash 收到后退出退出时再向自己名下的各作业转发一遍 SIGHUP作业没忽略这个信号就跟着死这就是服务器上进程跑着跑着就没了的完整链条3. nohup 只挡信号setsid 才换会话nohup cmd 做的是让 cmd 忽略 SIGHUP会话没换、终端关系还在setsid cmd换的是会话本身进程直接脱离原终端bash 的转发也够不着它两者都能让进程活过掉线只有后者改了进程关系CtrlC 打不中后台是投递范围的问题终端一关全死是会话归属的问题。两件事都指向同一个东西——控制终端。四、setsid 建立新会话并脱离控制终端4.1 组长身份是调用 setsid 的硬门槛1. 组长调用必然返回 EPERMsetsid(2)的失败条件只要有任何进程的 PGID 等于调用者 PID就返回 -1 并把 errno 置成 EPERM自己就是组长时这条必然成立所以组长调 setsid 一定失败手册给的理由是保住两级层次组长带着组进新会话、组里其他人留在原会话会话和组就断了2. fork 一次让子进程获得调用资格fork 出的子进程拿到新 PID不可能等于继承来的 PGID天生不是组长父进程_exit之后子进程去调 setsid 就能成功这一退一留顺手把进程变成孤儿被上层收养——「守护进程的父进程是 1」是这么来的4.2 setsid 成功后 SID 和 PGID 都变成自己的 PID1. 四项状态一起变项调用前fork 出的子进程调用后SIDbash 的 PID自己的 PIDPGID父进程所在组的 PID自己的 PID身份普通组员会话首进程 新组组长控制终端/dev/pts/0无TTY 显示?手册原话调用者成为新会话首进程SID 等于自己 PID同时成为新会话里新组的组长PGID 等于自己 PID新会话一开始没有任何控制终端要重新获得得走credentials(7)那套流程ps axj上可见两个变化TTY 从 pts/0 变?STAT 里多出s标记2. setsid 不搬文件描述符setsid 改的是归属关系0、1、2 还指着原来那个终端设备没关也没换终端已经关了往这几条写就出错终端还在输出仍会漏到原来那个窗口所以标准流得守护进程自己重定向setsid 不替它做五、守护进程由 setsid 加五步环境处理组成5.1 六步各自解决一个问题1. 前四步改身份后三步改环境fork 后父进程退出让子进程不是组长能调 setsid顺带转入后台被 init 或 subreaper 收养setsid()建新会话脱离控制终端成为新会话首进程再 fork 一次让干活的进程不是会话首进程永远拿不回控制终端chdir(/)切到根目录不占着某个挂载点避免umount报设备忙umask(0)清掉权限掩码文件权限完全由open、mkdir的 mode 参数决定0、1、2 重定向到/dev/null断掉残留的终端引用读立即 EOF写直接丢弃2. 二次 fork 防的是重新拿到终端会话首进程只要 open 一个 tty 设备节点就可能重新挂上控制终端程序或依赖库某一处open漏了O_NOCTTY一次 fork 的守护进程就会被拽回终端再 fork 一次干活的进程不是会话首进程就彻底没这个机会这套流程是 SysV 的老做法systemd 托管的服务由它代劳不需要程序自己写。5.2 my_daemon.c 完整实现六步#includestdio.h#includestdlib.h#includeunistd.h#includefcntl.h#includesys/stat.h#includetime.h#defineLOG_FILE/tmp/my_daemon.logintmain(void){pid_tpidfork();if(pid0){perror(fork);return1;}if(pid0)exit(0);// 第一步父进程退出子进程不再是组长if(setsid()0){perror(setsid);return1;}// 第二步建新会话脱离控制终端pidfork();if(pid0){perror(fork);return1;}if(pid0)exit(0);// 第三步再退一次保证不是会话首进程chdir(/);// 第四步不占任何挂载点umask(0);// 第五步权限由 open 的 mode 直接决定intfdopen(/dev/null,O_RDWR);if(fd0)return1;dup2(fd,STDIN_FILENO);// 第六步三条标准流全部指向 /dev/nulldup2(fd,STDOUT_FILENO);dup2(fd,STDERR_FILENO);if(fdSTDERR_FILENO)close(fd);while(1){// 干活循环每 5 秒往日志里写一行FILE*fpfopen(LOG_FILE,a);if(fp){time_tnowtime(NULL);fprintf(fp,daemon alive: %s,ctime(now));fclose(fp);}sleep(5);}return0;}编译运行一行搞定gcc my_daemon.c -o my_daemon ./my_daemon。命令返回说明父进程已退没有前台进程占着终端。5.3 跑起来看三处1. ps 一屏验身份psaxj|head-1psajx|grepmy_daemon|grep-vgrepPPID 是 1说明原父进程已退出被收养了PGID 与 SID 都等于它自己的 PID说明它是新会话首进程、新组组长TTY 是?说明控制终端已经摘掉2. /proc 一屏验重定向ls-l/proc/pid/fd0、1、2 三条链接全部指向/dev/null和终端再无关系如果还指着/dev/pts/N说明第六步没做或者做在了 fork 之前3. 日志一屏验存活tail-f/tmp/my_daemon.log每 5 秒多一行说明进程在终端关掉之后照样活着三处对上四条判据齐全父为 1、自成会话、无控制终端、标准流已重定向守护进程不是特殊进程类型是普通进程把「会话、进程组、控制终端、标准流」四样东西逐条切断之后的结果。六、孤儿被收养僵尸等父进程回收6.1 两种进程只差有没有人收尾1. 孤儿进程被 init 或 subreaper 收养父进程先退出、子进程还活着子进程成了孤儿被 PID 1或设了 subreaper 的祖先接管孤儿照常运行退出时由收养者回收不会留下僵尸fork 加 setsid 造出来的就是这一条所以守护进程天然是孤儿2. 僵尸进程是退出了但没人回收子进程先退出、父进程没有wait进程实体没了task_struct还留着等父进程取退出码这份残留占一个 PID 表项ps里状态显示Z杀不掉也唤不醒父进程退出后僵尸才由收养者收走父进程长期不退僵尸就一直堆着七、全篇总结1. 一次作业在终端里是一个进程组PGID 等于组长的 PID管道作业的组长是管道最左边的进程交互 shell 为每个作业单独建组脚本里不建子进程直接继承 shell 的 PGID2. 一个会话对应一次登录会话首进程的 PID 就是 SID会话内所有进程 SID 相同换组用 setpgid换会话只有 setsid而且调用者不能是组长3. 控制终端决定信号投给谁CtrlC 只投前台进程组后台作业打不中终端挂断时 SIGHUP 先给会话首进程再由 shell 转给各作业4. setsid 一次改掉身份和终端关系成功后 SID 与 PGID 都等于自己的 PID身份变成会话首进程兼新组组长控制终端断开、TTY 显示?但描述符不会跟着搬走5. 守护进程是这套关系的标准组合两次 fork 加 setsid 解决身份chdir、umask、重定向解决环境判据四条父为 1、自成会话、无终端、标准流已重定向八、文末面试题8.1 推导题1. fork 出来的子进程PGID 和 SID 会变吗答推导不会。fork 只分配新的 PID进程组和会话归属从父进程整份继承子进程直接落在父进程的组和会话里。改组用setpgid换会话用setsid都不会在 fork 时自动发生。2. 一条管道里的三个进程属于同一个进程组吗答推导属于。管道作业在 shell 里是一个 job交互 shell 会为它单独建组组长是管道最左边的进程另外两个成员的 PGID 都指向组长。三者 PPID 也相同但 PPID 相同只说明兄弟关系真正绑定的是 PGID。3. setsid 为什么必须先 fork 一次答推导setsid 的失败条件是「有任何进程的 PGID 等于调用者 PID」自己就是组长时这条必然成立调用直接返回 EPERM。fork 出的子进程 PID 是新的不可能等于继承来的 PGID由它调用才能成功。内核这么拦是避免组长带着组进新会话、组里其他人留在原会话把两层结构撕开。4. 守护进程为什么通常 fork 两次答推导第一次 fork 让调用 setsid 的进程不是组长否则 setsid 直接失败。第二次 fork 让真正干活的进程不再是会话首进程——会话首进程打开 tty 设备时可能重新获得控制终端程序某处open漏了O_NOCTTY就会中招不是首进程就永远没这个机会。5. setsid 之后为什么还要把 0、1、2 重定向到 /dev/null答推导setsid 只改会话和进程组的归属不碰已打开的描述符0、1、2 仍指向原来那个终端设备。终端关闭后往这些描述符写就出错终端还在输出也会漏到别人的窗口。重定向之后读立即 EOF、写直接丢弃才算和终端彻底断干净。8.2 真题1. SSH 断开后之前启动的进程为什么可能挂掉答推导 · 已对照解析转述终端挂断时内核向会话首进程发 SIGHUPshell 收到后退出退出时再向自己名下的作业转发 SIGHUP作业没忽略这个信号就会终止。要让进程活下来要么忽略 SIGHUPnohup要么换新会话setsid要么交给 systemd 托管。【真题·转述自 CSDN《搞懂Linux进程间关系进程组、会话与控制终端》】2. 创建守护进程为什么要 fork 两次答推导 · 已对照解析转述第一次 fork 让子进程不是组长否则 setsid 会失败setsid 之后子进程成为新会话首进程第二次 fork 让干活的进程不再是会话首进程从而无法重新获得控制终端父进程退出后它被 init 接管成为后台服务进程。【真题·转述自 Today I Learned《为什么创建 Daemon 进程要 Fork 两次》】结语进程组管一次作业会话管一次登录控制终端决定信号投给谁守护进程把这四层关系一次摘干净。评论区聊聊你在服务器上把进程跑丢过几次觉得有用点个赞Linux 系统篇持续更新。
阅读完成 · 觉得有帮助?
咨询建站