Linux内核中kill_orphaned_pgrp函数是如何处理孤儿进程组的?

摘要:Linux 3.2 kill_orphaned_pgrp 函数 前言 之前研究进程的退出 do_exit, 其中一个重点就是 exit_notify, 遂对这个函数的用法产生了兴趣, 并且进行了一番研究. 1.想象一个场景 如果你玩过 sh
Linux 3.2 kill_orphaned_pgrp 函数 前言 之前研究进程的退出 do_exit, 其中一个重点就是 exit_notify, 遂对这个函数的用法产生了兴趣, 并且进行了一番研究. 1.想象一个场景 如果你玩过 shell, 你应该能理解下面的一个场景: sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % sleep 100 ^Z zsh: suspended sleep 100 sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % nc -l 8080 ^Z zsh: suspended nc -l 8080 sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % jobs [1] - suspended sleep 100 [2] + suspended nc -l 8080 sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % fg %1 [1] - continued sleep 100 ^Z zsh: suspended sleep 100 sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % bg %1 [1] - continued sleep 100 sudo-su-bash@Sudo-su-BashdeMacBook-Air ~ % exit 对, 按下 Ctrl+Z 可以让前台的进程暂时挂起, 然后使用 bg 可以让进程在后台运行. 但是随之而来的就有一个问题: 上面的代码, 我最后敲了 exit, shell挂了, 但是 sleep 和 nc 还挂在后台呢! 所以, 该怎么处置这两个怪胎? 接下来这个函数, 就是专门处置这个怪胎的. 2.函数的原型 这个函数的意图其实很简单, 看名字也可以看得出来, 就是检测是否为孤儿进程组, 并且唤醒/杀死孤儿进程组的所有 Stopped 状态的进程. 此函数的原型如下 static void kill_orphaned_pgrp(struct task_struct *tsk, struct task_struct *parent) { struct pid *pgrp = task_pgrp(tsk); struct task_struct *ignored_task = tsk; if (!parent) parent = tsk->real_parent; else ignored_task = NULL; if (task_pgrp(parent) != pgrp && task_session(parent) == task_session(tsk) && will_become_orphaned_pgrp(pgrp, ignored_task) && has_stopped_jobs(pgrp)) { __kill_pgrp_info(SIGHUP, SEND_SIG_PRIV, pgrp); __kill_pgrp_info(SIGCONT, SEND_SIG_PRIV, pgrp); } } 初次看到, 肯定会感觉有点绕, 后面的代码勉强能看懂, 但是前面的代码是什么鬼? 为什么parent为null的时候, 会被自动赋值为父进程? ignored_task又是干什么的? 3. 下面一段代码的含义 3.1 孤儿进程组的判定 在开始之前, 我们先来看一个函数: //will_become_orphaned_pgrp static int will_become_orphaned_pgrp(struct pid *pgrp, struct task_struct *ignored_task) { //确定每个进程的父进程都在同一个进程组 struct task_struct *p; do_each_pid_task(pgrp, PIDTYPE_PGID, p) { if ((p == ignored_task) || //被忽略的进程 (p->exit_state && thread_group_empty(p)) || //这个进程只剩下一个线程了, 并且这个线程也不在运行 is_global_init(p->real_parent)) //父进程全局init continue; if (task_pgrp(p->real_parent) != pgrp && task_session(p->real_parent) == task_session(p)) return 0; } while_each_pid_task(pgrp, PIDTYPE_PGID, p); return 1; } struct task_struct { int exit_state; //这个就是传说中进程状态存储的地方, 也就是你们之前看到的 TASK_ZOMBIE, TASK_DEAD所存在的地方 //0 代表 TASK_RUNNING } 所以 will_become_orphaned_pgrp 其实就是判定进程组是否为孤儿进程组, 逻辑已经在上面写的很清楚了, 但凡进程组中有一个子进程: 父进程不为init 父子进程不在一个组里, 但是在一个会话中 (这个在 shell 中特别重要!) 子进程中的某个进程在运行, 或者这个进程有一个或者多个子线程 子进程不是被忽略的进程 换句话说, 如果这个进程组只要有一个正在运行的/多线程的 && 父进程不是init进程的 && 父子进程不在一组的进程, 那么这个进程组就不是孤儿进程组. 3.2 下面一段代码的含义 来看这段代码: if (task_pgrp(parent) != pgrp && task_session(parent) == task_session(tsk) && will_become_orphaned_pgrp(pgrp, ignored_task) && has_stopped_jobs(pgrp)) { __kill_pgrp_info(SIGHUP, SEND_SIG_PRIV, pgrp); __kill_pgrp_info(SIGCONT, SEND_SIG_PRIV, pgrp); } 最核心的是 __kill_pgrp_info 函数, 这个函数向孤儿进程组的所有子进程全部发送一个 SIGHUP(终止) 或 SIGCONT(唤醒进程) 信号. 这样, 就算父进程挂了, 子进程会被立刻唤醒, 就如开头的场景一样, sleep会跑完. 那上面的条件呢? 首先, 还是祖传的性能加速: task_pgrp(parent) != pgrp && //在不同进程组 task_session(parent) == task_session(tsk) //在一个会话 其实把这两个条件去掉, 就剩最后两个条件, 内核照样能判定 pgrp 是不是孤儿进程组, 但是这样写的问题在于 will_become_orphaned_pgrp 需要遍历整个进程组! 那万一 pgrp 包含成百上千个进程, 开销岂不是上天了. 而加了这两个条件后, 如果这两个条件有一个不满足, 那么它们就不是孤儿进程组, 下面的信号发送就不会执行, 这样就能避免每次都 will_become_orphaned_pgrp 了. 然后下面两个条件, will_become_orphaned_pgrp 和 has_stopped_jobs, 这两个函数就是正儿八经的判断了, 后者用来判定是否有 Stopped/Suspended 的进程(就例如上面的 nc 进程). 4. 那上面一段代码呢? 来, 我们来看上面一段: struct pid *pgrp = task_pgrp(tsk); struct task_struct *ignored_task = tsk; if (!parent) parent = tsk->real_parent; else ignored_task = NULL; 4.1 parent = NULL 的情况 我们溯一下源: static void exit_notify(struct task_struct *tsk, int group_dead) { bool autoreap; /* * This does two things: * * A. Make init inherit all the child processes * B. Check to see if any process groups have become orphaned * as a result of our exiting, and if they haveOU any stopped * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2) */ forget_original_parent(tsk); //假设进程组的进程全部改为 init, 后面要考 exit_task_namespaces(tsk); write_lock_irq(&tasklist_lock); if (group_dead) kill_orphaned_pgrp(tsk->group_leader, NULL); } 其中 group_dead 的含义是线程组已经死了, 也就是进程死了. 我们假设这么一个情景: 假设上面的图中, A 是 shell, B 是子进程, C,D 是 B fork出来的子进程, 它们在一个进程组 pgrp, 绿色是 Shell, 黄色是 Stopped Task. 如图所示, 现在 B 快要死了, 那么经历寻父 (就上面的 forget_original_parent) 后, B的子进程全部丢给了 init, 然后现在开始执行这个函数 //ignored_task = B //parent = A task_pgrp(parent) != pgrp //B 和 A 不在一个进程组 task_session(parent) == task_session(tsk) //假设 B 和 A 在一个会话中 will_become_orphaned_pgrp(pgrp, ignored_task) //看到了吗? 因为 B 快死了, 所以得被忽略, 但是忽略后, C D 都指向 init, 这个进程组是孤儿进程组了 has_stopped_jobs(pgrp) //C 是 Stopped Task 所以, 这个进程组属于孤儿进程组, 需要把 C 唤醒, 让 C 继续执行. 事实上, C 也应该继续执行, 你不能指望 init 把它唤醒, 对不对. 所以, 这种情况对应的是 这个进程退出的时候, 检查当前的进程组是否因为它的退出, 而变成了孤儿进程组. 若是, 则这个进程的唤醒机制就会被触发. 4.2 parent != NULL 的情况 我们继续溯一下源: static void forget_original_parent(struct task_struct *father) { list_for_each_entry_safe(p, n, &father->children, sibling) { //大概的意思就是遍历father的所有子进程, p是子进程 //...(更改子进程的父亲) reparent_leader(father, p, &dead_children); } } static void reparent_leader(struct task_struct *father, struct task_struct *p, struct list_head *dead) { //...(全部注释掉) //上面代码大概干了以下工作: // 如果子进程本身就是僵尸, 那么通知父进程来收尸 kill_orphaned_pgrp(p, father); } 诶! 还记得上面的 forget_original_parent 的调用位置吗? 那也就是说, 此时此刻, 虽然有 father 传到 kill_orphaned_pgrp 函数里了, 但是 father 才是我们要回收的进程. 为了理解这个函数调用的意思, 我们把文章开头的情境代入进去看看, 如下图. 继续看上面的代码. struct pid *pgrp = task_pgrp(tsk); struct task_struct *ignored_task = tsk; if (!parent) parent = tsk->real_parent; else ignored_task = NULL; 根据上面的情境和代码, shell 输入 exit 后退出了, 两个子进程的 real_parent 也改为了 init, 但是 shell 的 children 并没有因此清空, 还是连着这些子进程的(就是我画的红线部分). 此时此刻内核开始遍历 shell 的子进程, 假设最后一个(划重点!)遍历到进程 sleep. 我们可以得到如下信息: //ignored_task 不存在 //parent 为 sleep task_pgrp(parent) != pgrp //sleep 和 shell 不在一个进程组 task_session(parent) == task_session(tsk) //sleep 和 shell 在一个会话中 will_become_orphaned_pgrp(pgrp, ignored_task) //shell 已经退出了, 此时此刻, sleep 和 nc 的初始进程都是 init, 符合孤儿进程的定义. //现在你知道为什么是最后一个了吧, 因为不是最后一个的话, 就还有进程和 shell 连着. has_stopped_jobs(pgrp) //nc 还在睡着呢! OK, 符合上述所有条件, 所以nc会被唤醒, 否则就没进程唤醒它了. 所以, 这种情况对应的是 父进程退出, 最后一个子进程 reparented 后, 父进程的唤醒机制就会被触发. 5. 最后一个问题 那么, 你有没有想过一个问题: 4.1 节的那张图,在 B 退出的时候, 4.2 节的那个 reparent_leader 下的 kill_orphaned_pgrp 函数照样会被触发, 但是此时此刻会出现一个现象: task_pgrp(parent) != pgrp //第一个条件就不满足, B和C同组 //下面的条件全都不会触发 但是, 去掉 B 这个快死了的进程后, C 和 D 所在的进程组是属于孤儿进程组的. 那么这个"误判"不会有影响吗? 实际上, 这个确实是误判了, 没错. 但是, 这正是 4.1 节函数的目的: 最终检查/保险, 就是在这个进程退出后, 再对自身所在的进程组进行一轮检查, 一笔勾销之前的"误判". The End 本期文章写到这, 感谢大家的观看哦~萌新初涉 Linux 内核, 有错误也请多多指正~ 版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处! 作者: Sudo-su-Bash (Alien-Bash) 发布时间: 2026-02-26 原文链接: https://www.cnblogs.com/SudosuBash/p/19639255