前言
我们知道,一个任务在消亡退出时通常会将子任务(后面用孤儿或者children表示)托付其他任务(后面用reaper表示)以便以后“收尸”,这个流程就是“托孤”。
托孤会为子任务寻找一个新的parent作为new reaper,那这个new reaper选谁呢?怎么选呢?会是传说中的1号进程么?这就是本文要去探索的问题。
一、单身父亲的谢幕
作为一个单身父亲,没有任何兄弟姐妹的线程,tid也不为1的线程,带着一点点遗憾走完旅程的最后一段;
这个时候内核会看到它还有一个孩子,嗷嗷待哺的孩子,然后找到了单身父亲所在pid命名空间中child reaper的负责人pid_ns->child_reaper---(一般是1号线程)
这个child_reaper作为天生的pid namespace的child reaper理论上是应该主动承担起托孤的大任,但是内核办事总是会小心翼翼:
(1) 内核首先找到退出线程(后面为方便讨论,称作father)所在pid命名空间pid_ns,然后通过pid_ns->child_reaper就找到了命名空间默认的child reaper; 默认情况下,pid_ns->child_reaper都是1号线程,除非发生了重大变故(请思考,什么时候pid_ns->child_reaper不是1号线程?)。
(2) 内核要确认将要退出的这个fater不是pid_ns->child_reaper,(单身父亲当然不是pid_ns->child_reaper);
(3) 内核还要判断fater有没有children,如果没有children就直接退出“托孤”流程,这里单身父亲有children,所以,得托孤;
(4) 内核本来是先找到pid命令空间负责人pid_ns->child_reaper,但是pid_ns->child_reaper呢也不是省油的灯,凭啥这样的事儿都给我呀;
内核一看,好像也说的有道理,可不能“能者多劳”把活都扔给人家呀,对吧;于是呀就去看看fater有没有还活着的兄弟姐妹可以帮忙收养可怜的孤儿,就去调用find_alive_thread(father)函数,找呀找,这个呀就是去查fater的线程组p->signal->thread_head链表,如果链表上有一个线程,且它还没有死,也就是法医还没有给它贴上PF_EXITING的标签,它就是活着的,就得去承担这个“收养”的责任。
(5) 可是呀,世事难料啊,这个单身父亲呀,它没有兄弟姐妹,内核呀就又把那凌厉而期许的目光投向了 pid_ns->child_reaper, pid_ns->child_reaper就战战兢兢的说呀,fater虽然没有兄弟姐妹,但是保不准它还有其他亲人呢?比如这个孤儿有没有爷爷呀,爷爷不在的话,爷爷倍有没有兄弟姐妹呀?爷爷辈没有亲属,爷爷的爷爷.....无穷尽也
(6) 内核呀,拿着这事儿脑袋疼,但是也没法,就去找呗,看看fater还有没有在世的亲人----看看孤儿的爷爷还在不....一辈一辈往上走...
诶,但是呀也不能逮到一个爷爷倍的就让它老人家的收养孤儿,首先哪得看看fater的各个祖先有没有给fater留个信儿,这个父亲有没有备用收养者father->signal->has_child_subreaper 一个新任务被创建的时候,如果创建它的parent设置了has_child_subreaper或者is_child_subreaper属性则新任务就会标志p->signal->has_child_subreaper;也就是说新任务的父亲或者祖先中有一个任务是"is_child_subreaper",即子任务的备选收养者。这里有必要解释一下两个英文单词"has"和"is":"has"表示已经有,"is"表示我就是,所以has_child_subreaper和is_child_subreaper
两个成员通过名字就可精确的搞定。所以说呀,书上经常教育我们函数命名要有意义在这里也体现出来。
好了,回过头来继续看"托孤"流程,这里要去检查father的祖先是否可以收养孤儿,首先就得判断father是否有备用reaper,即if (father->signal->has_child_subreaper),如果father确实明确说了自己死了后还有备选的reaper,那么内核就得开始去查father的祖先,通过reaper = father; 然后reaper = reaper->real_parent挨个挨个的查,查什么呢?
首先,查father的父辈到底是否是孤儿的备选收养者,即if (!reaper->signal->is_child_subreaper),如果不是就往还要往更老的祖先倍查,说难听了就是王祖坟上刨;
其次,祖先倍儿如果和上面的pid_ns->child_reaper是同一个组,那坏事儿了,pid_ns->child_reaper责任难逃,自己就是那个祖先倍儿,这个收养孤儿的事呀是赖不掉了;
再次,一直往祖先倍查,一层一层又一层,最终到谁了呀,到女娲娘娘init_task了,第一批小人就是她造的,要真到这里那就说明father的祖先倍就没有适合孤儿收养的,得得得,if (reaper == &init_task) 满足就结束吧,留给pid_ns->child_reaper去干这孤儿收养吧。
最后,如果真找到一位合适祖先作为备选收养者,那么就从这个祖先的线程组中找一个活着的线程来担当这个孤儿收养的角色吧。
(7)不论如何,最终还是为孤儿找到合适的reaper,接下来就给它安排安排吧。
也许father的孩子不止一个,所以要找到father的所有children,将他们的real_parent和parent依次设置为新的reaper。
list_for_each_entry(p, &father->children, sibling) {
for_each_thread(p, t) {
t->real_parent = reaper;
BUG_ON((!t->ptrace) != (t->parent == father));
if (likely(!t->ptrace))
t->parent = t->real_parent;
二、亚当的毁灭
在第一章我们了解单身父亲的谢幕,内容相当精彩,不但了解了单身父亲谢幕的情况,还了解了其他情况,如父亲有兄弟姐妹,父亲有祖先等等情况下的托孤策略。但是第一章有一种情况我们
略过了:即在第一章第(2)节中,如果即将消亡father就是当前pid namespace中的child_reaper的情况。你想想呀,pid_ns->child_reaper那可是命名空间的1号进程,一般来将所有任务都是它的
后代,这个father要是命名空间的1号进程,它要退出了好像问题有点严重了...还会有托孤吗?这个命名空间是继续存在还是走向毁灭呢?
内核秉承稳定第一的理念,处理起事情来总是小心翼翼。首先它想到的就是退出的father是否还有活着的兄弟姐妹,有的话赶紧扶正上位,可别让整个命名空间群龙无首呀。
reaper = find_alive_thread(father); //否则就是pid_ns->child_reaper进程要退出了,找到线程组中第一个“alive”的进程
if (reaper) {
pid_ns->child_reaper = reaper; //另立新王
return reaper;
}
上面的情况虽然不够完美,但也不失为一个解决方案。但是如果father没有兄弟姐妹呢?那就无发可说了。
要是father所在的命名空间不是init命名空间还好说,将整个命名空间毁灭了便作罢;要是father所在命名空间就是最上层的初始命名空间init_pid_ns,那father又是整个命名空间的1号进程,
那father不就是那个天煞的init进程么,那个创造了夏娃的亚当么?哦mygod...这可是死罪呀....这种情况下内核就直接panic,你说可怕不可怕。
三、内核在托孤流程的变化
上文都是以linux-4.4开源内核为背景来分析的,和其他版本的内核大体流程相差不大;只有一处有所改动(https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v5.3&id=c6c70f4455d1eda91065e93cc4f7eddf4499b105)补丁内容如下:
From c6c70f4455d1eda91065e93cc4f7eddf4499b105 Mon Sep 17 00:00:00 2001
From: Oleg Nesterov <oleg@redhat.com>
Date: Mon, 30 Jan 2017 19:17:35 +0100
Subject: exit: fix the setns() && PR_SET_CHILD_SUBREAPER interaction
find_new_reaper() checks same_thread_group(reaper, child_reaper) to
prevent the cross-namespace reparenting but this is not enough if the
exiting parent was injected by setns() + fork().
Suppose we have a process P in the root namespace and some namespace X.
P does setns() to enter the X namespace, and forks the child C.
C forks a grandchild G and exits.
The grandchild G should be re-parented to X->child_reaper, but in this
case the ->real_parent chain does not lead to ->child_reaper, so it will
be wrongly reparanted to P's sub-reaper or a global init.
Signed-off-by: Oleg Nesterov <oleg@redhat.com>
Signed-off-by: Eric W. Biederman <ebiederm@xmission.com>
---
kernel/exit.c | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/kernel/exit.c b/kernel/exit.c
index 8f14b86..5cfbd59 100644
--- a/kernel/exit.c
+++ b/kernel/exit.c
@@ -578,15 +578,18 @@ static struct task_struct *find_new_reaper(struct task_struct *father,
return thread;
if (father->signal->has_child_subreaper) {
+ unsigned int ns_level = task_pid(father)->level;
/*
* Find the first ->is_child_subreaper ancestor in our pid_ns.
- * We start from father to ensure we can not look into another
- * namespace, this is safe because all its threads are dead.
+ * We can't check reaper != child_reaper to ensure we do not
+ * cross the namespaces, the exiting parent could be injected
+ * by setns() + fork().
+ * We check pid->level, this is slightly more efficient than
+ * task_active_pid_ns(reaper) != task_active_pid_ns(father).
*/
- for (reaper = father;
- !same_thread_group(reaper, child_reaper);
+ for (reaper = father->real_parent;
+ task_pid(reaper)->level == ns_level;
reaper = reaper->real_parent) {
- /* call_usermodehelper() descendants need this check */
if (reaper == &init_task)
break;
if (!reaper->signal->is_child_subreaper)
--
cgit v1.1
新的补丁在为孤儿寻找“祖先”时,并不会一直往“祖坟”上刨,而是限制到当前命名空间内;也就是说祖先的回溯是不会超过本命名空间的。这个该对对容器场景下的一些应用会有所影响。举个例子:
docker-containerd-shim 创建了任务p,然后将P加入到一个容器命名空间N,然后任务p再fork()出一个任务g,当任务p退出时,旧内核的托孤流程实现上有可能最终会让docker-conainerd-shim任务成为new reaper去"收尸";而新内核的策略最终最终也只是将任务g托孤给命名空间N的1号进程(或者 pid_ns->child_reaper)。
四、饭后甜点
好了,本期内容就到这里结束了。欣赏了如此经常的进程托孤流程后,你是否还意犹未尽呢?没关系,我还为大家准备了一些饭后甜点:
场景描述:进程A通过pthread_create()函数创建线程A.0 ,然后线程A.0又调用fork()创建了A.0.0
问1:A.0.0 通过getppid() or /proc/$A.0.0/status获取的ppid是什么?
问2:A.0和A.0.0是什么关系
场景描述: A pthread_create() A.0、A.1,另外A还通过fork()创建了进程A.2
问3: A与A.0、A.1、A.2是父子关系么?
问4: A与A.0、A.1、A.2是在同一个线程组么?
问5: A退出时A.0、A.1、A.2的parent会改变么?变为什么?
问6:A的children是什么?
场景描述:A使用fork()创建B, B使用fork()创建了C
问7:B退出后,C的parent会是谁?
场景描述:如果任务A是某个命名空间的1号进程,A pthread_create()创建了A.1,A.2,然后再通过fork()创建了A.3
问8:如果A退出,A.1,A.2,A.3会退出么? 如果不退出他们的parent会发生改变么?
场景描述:init命名空间的任务A通过fork()创建了进程A1,然后使用setns()将A1加入到了命名空间NS1
问9:A1的parent是谁?
问10:如果A任务退出,A1会退出么?如果不会退出,它的parent是谁?