孤儿进程组导致系统重启

问题背景

  • 腾讯天天系列游戏
    天天爱消除, 天天酷跑,天天连萌,天天飞车,天天炫斗, 天天逆战……
  • 如何复现
    玩着玩着游戏,可能低概率的出现游戏 ANR, JE, NE 等情况
    接着出现android 上层重启(出现开机动画,然后恢复到keyguard)

分析

  • 机器重启的原因
    zygote 接收到SIG 1(SIGHUP) , 从而zygote 退出,android 上层重启。
    孤儿进程组导致系统重启_第1张图片

原理分析

下面是Android进程创建关系图

会话VS进程组VS进程VS线程
孤儿进程组导致系统重启_第2张图片

  • init 启动service 时,如果service 参数中定义了console 并且启用了uart console, 那么就会重新 设置session, session id (sid) 为service 的PID. 否则session id 依旧是0 (继承于init).
    task_struct.group_leader.pids[PIDTYPE_SID]
  • 每个process fork 的时候会重新设置它的process group(pgrp), 并且为它的parent PID. 如果是fork 线程, 那么process group 依旧为parent 的process group.
    task_struct.group_leader.pids[PIDTYPE_PGID]
  • Zygote 启动每个app process 后,都会强制设置它们的process group 为它自己.(虽然是对端的pgid, 但实际还是zygote) ZygoteConnection.setChildPgid()。
    孤儿进程组导致系统重启_第3张图片
  • pid=847 进程(包括轻量级进程,即线程)号
  • comm=应用程序或命令的名字
  • task_state=s 任务的状态,R:runnign, S:sleeping (TASK_INTERRUPTIBLE), D:disk sleep (TASK_UNINTERRUPTIBLE), T: stopped, T:tracing stop,Z:zombie, X:dead
  • ppid=134 父进程ID
  • pgid= 134 进程组号
  • sid= 0 该任务所在的会话组ID

孤儿进程组

  • POSIX.1 将孤儿进程组(orphaned process group) 定义为:
    该组的成员的父进程要么是该组的成员,要么不是该组所属session 的成员, 要么父进程是init.
    反过来说,一个进程组不是孤儿进程组的条件是,该组中有一个进程,其父进程属于同一个会话的另外一个组,父进程为init 除外.

  • 在父进程终止,进程组成为孤儿进程组时, 如果进程组中有stop 状态(t/T) 的process/thread, POSIX.1 要求向新的孤儿进程组中每一个进程发送SIGHUP, 接着又向其发送SIGCONT. (以保证进程要么退出,要么继续进行,而非stop 状态)

    孤儿进程组导致系统重启_第4张图片
    正常状态下的进程创建
    孤儿进程组导致系统重启_第5张图片
    出现异常时候,父进程异常退出,更新父进程为init进程
    孤儿进程组导致系统重启_第6张图片
    天天游戏的进程状态图

孤儿进程组导致系统重启_第7张图片
正常流程中,检测到异常,重启游戏后,自己退出

假如此时有process 处于STOP (T) 状态,机器必然打到孤儿进程组SIG 1 -> SIG 18 case, 杀掉整个zygote Process Group, 机器重启,原理图如下所示:
孤儿进程组导致系统重启_第8张图片

  • 最快复现手法
  • 首先开启天天游戏
  • 随意抓一个APK , 强制性stop, SIG 19: kill -19 pid
  • Ps 一下找到天天游戏的PID, 然后 kill -9 pid, 强制性杀掉天天游戏,注意不能随意发SIG 1 等其他signal 它会自己catch, 无法杀死它
  • 机器自动重启

原理分析

  • 天天游戏使用双process 机制,游戏主进程启动safe-debug 进程,safe-debug负责监测主进程的状态,并收集异常信息,一旦主进程异常,可重启主进程,恢复游戏,并且这两个进程都处于zygote 的process group 域中。
  • 天天游戏会自行处理异常信号,并且会使用ptrace/signal stop 手法,抓取相关的资讯,与android 本身的debug机制有冲突,比如无法直接抓java backtrace, 无法RTT.
  • 天天游戏一旦退出,就会导致safe-debug 变成孤儿进程,并且parent 变成init;而孤儿进程自己退出时,一旦当时zygote 域内有stop 的thread/process, 孤儿进程将在全域内SIG 1 , SIG 18. 导致zygote 和 system-server 被杀,机器重启。

内核层面分析

孤儿进程组的条件是进程组中进程的父进程都是当前进程组中的进程,或者是其他session中的进程。当孤儿进程组产生的时候,如果孤儿进程组中有TASK_STOP的进程,那么就发送SIGHUP和SIGCONT信号给这个进程组,这个顺序是不能变的,我们知道进程在进程在TASK_STOP的时候是不能响应信号的,只有当进程继续运行的时候,才能响应之前的信号。如果先发送SIGCONT信号再发送SIGHUP信号,那么SIGCONT信号后,进程就开始重新进入运行态,这个和马上响应SIGHUP信号的用意相悖。所以这个时候需要在进程stop的过程中首先发送SIGHUP信号,为的是让进程运行之后马上执行SIGHUP信号。
这两个信号是发送给有处于TASK_STOP状态的进程的进程组的,所以进程组中正在运行的进程,如果没有建立SIGHUP信号处理函数,那么运行的进程就会因为SIGHUP退出。
在进程退出的时候,在线程组都退出了,就会判断当前进程是否是孤儿进程组,如果是孤儿进程组就发送SIGHUP和SIGCONT信号。
代码:kernel/exit.c

static void exit_notify(struct task_struct *tsk, int group_dead)
{
        int signal;
        void *cookie;

        /*
         * 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 have any stopped
         * jobs, send them a SIGHUP and then a SIGCONT. (POSIX 3.2.2.2)
         */
        forget_original_parent(tsk);
        exit_task_namespaces(tsk);

        write_lock_irq(&tasklist_lock);
        if (group_dead)
        //判断是否是孤儿进程组,tsk如果是线程,那么group_leader就是线程组首进程
        kill_orphaned_pgrp(tsk->group_leader, NULL); 

kill_orphaned_pgrp函数就是查看进程退出后,是否变为了孤儿进程,如果是孤儿进程,并且有stop的进程,那么就向整个进程组发送SIGHUP,SIGCONT。

函数kill_orphaned_pgrp的一段代码:

if (task_pgrp(parent) != pgrp && //tsk和parent是同一session下的不同进程组
            task_session(parent) == task_session(tsk) &&//
            will_become_orphaned_pgrp(pgrp, ignored_task) &&//判断是否是孤儿进程组
            has_stopped_jobs(pgrp)) { //如果进程组中有处于TASK_STOP状态的进程
                __kill_pgrp_info(SIGHUP, SEND_SIG_PRIV, pgrp); //先发送SIGHUP在发送SIGCONT
                __kill_pgrp_info(SIGCONT, SEND_SIG_PRIV, pgrp);
        }

判断pgrp进程组是孤儿进程组,通俗的的说就是进程组首进程的退出会导致孤儿进程组的产生。

代码:kernel/exit.c

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) ||                  //ignored_task就是将要退出的进程,所以不需要考虑
                    (p->exit_state && thread_group_empty(p)) ||    //进程退出并且这个线程组中没有其他的线程了
                    is_global_init(p->real_parent))
                        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;
}

在判断如果是孤儿进程组的时候,如果是同时这个进程组有处于TASK_STOP的进程,那么就向这个进程组发送SIGHUP和SIGCONT信号,首先进程在STOP的过程中是不能响应SIGHUP信号,这样SIGCONT信号处理完这个进程会处于运行态,会去处理SIGHUP信号。信号在kill函数的最后要去看下进程是否需要唤醒。如果进程处于stop状态并且kill的SIGCONT信号需要被唤醒,还有就是SIGKILL信号,需要被唤醒,及时响应。kill函数最后回调用这个函数signal_wake_up, 看下signal_wake_up的实现

void signal_wake_up(struct task_struct *t, int resume)
{
        unsigned int mask;

        set_tsk_thread_flag(t, TIF_SIGPENDING); //标志这个进程有信号需要处理

        /*
         * For SIGKILL, we want to wake it up in the stopped/traced/killable
         * case. We don't check t->state here because there is a race with it
         * executing another processor and just now entering stopped state.
         * By using wake_up_state, we ensure the process will wake up and
         * handle its death signal. 
         */
        mask = TASK_INTERRUPTIBLE; //进程处于TASK_INTERRUPTIBLE得进程可以被唤醒
        if (resume) 
                mask |= TASK_WAKEKILL;
        if (!wake_up_state(t, mask)) 
    //如果是运行态,并且运行在其他cpu得进程,那么kick_process的作用就是让进程没有延迟的进入内核态,快速响应信号
                kick_process(t);
}

这里需要说的一点 TASK_INTERRUPTIBLE状态就是进程处于睡眠状态,但是这种睡眠状态可以被信号打断,但是如果进程处于TASK_UNINTERRUPTIBLE深度睡眠,那么这时候信号是不能唤醒这种进程的,即使是SIGKILL信号也不行.对于TASK_UNINTERRUPTIBLE的状态的还不是不理解,不理解的点有这么几点:
1. 在计算cpuload的时候,为什么要算上这个TASK_UNINTERRUPTIBLE的进程。
2. 如果进程在处于TASK_UNINTERRUPTIBLE状态,那么是不响应信号的,那么是通过什么机制转换到running状态的
对于TASK_WAKEKILL状态的用法还没时间看懂。
这里能看到的就是在函数wake_up_state 中会判断进程t的状态不是TASK_INTERRUPTIBLE和TASK_WAKEKILL的就不唤醒了。所以这里处于TASK_STOP的进程是不能被SIGHUP信号唤醒的。

函数try_to_wake_up

static int try_to_wake_up(struct task_struct *p, unsigned int state,
                          int wake_flags)
{
        int cpu, orig_cpu, this_cpu, success = 0;
        unsigned long flags;
        unsigned long en_flags = ENQUEUE_WAKEUP;
        struct rq *rq;
        //禁止内核抢占调度
        this_cpu = get_cpu();      
        smp_wmb();
        rq = task_rq_lock(p, &flags);
        //判断进程状态,如果不是TASK_INTERRUPTIBLE或者TASK_WAKEKILL状态,就直接退出了
        if (!(p->state & state))                      
        goto out;

那么SIGCONT信号如何唤醒进程状态TASK_STOP的进程,这里在kill函数的prepare_signal函数中,会判断如果是SIGCONT信号,那么会在那个进程的状态上加上TASK_INTERRUPTIBLE,这样SIGCONT信号就能唤醒这个进程了。

else if (sig == SIGCONT) {
                unsigned int why;
                /*
                 * Remove all stop signals from all queues,
                 * and wake all threads.
                 */
                rm_from_queue(SIG_KERNEL_STOP_MASK, &signal->shared_pending);//从进程共用信号队列中移除stop类信号
                t = p;
                do {    //对于进程组而言,让每一个线程都继续执行
                        unsigned int state;
                        rm_from_queue(SIG_KERNEL_STOP_MASK, &t->pending);    //从线程私有信号队列中移除stop类信号
                        state = __TASK_STOPPED;
                        if (sig_user_defined(t, SIGCONT) && !sigismember(&t->blocked, SIGCONT)) {
                                set_tsk_thread_flag(t, TIF_SIGPENDING);
                                state |= TASK_INTERRUPTIBLE;            //设置 TASK_INTERRUPTIBLE,为了使进程wakeup
                        }
                        wake_up_state(t, state);                      //唤醒进程
                } while_each_thread(p, t);

解决办法

  • 这个问题由腾讯天天系列游戏设计框架引起,需要腾讯自行修改。
  • 最为简单的修改方式是,将天天-safe-debug 进程设置在它自己的process group 当中,而非zygote 的process group 当中。

简单复现手法

  • 写一个简单的APK
    执行:
    孤儿进程组导致系统重启_第9张图片
  • adb shell kill -19 other APK
  • adb shell kill -9 this apk
  • adb shell kill -9 logcat

你可能感兴趣的:(android系统分析,孤儿进程组)