孤兒進程組導致系統重啓

問題背景

  • 騰訊天天系列遊戲
    天天愛消除, 天天酷跑,天天連萌,天天飛車,天天炫鬥, 天天逆戰……
  • 如何復現
    玩着玩着遊戲,可能低概率的出現遊戲 ANR, JE, NE 等情況
    接着出現android 上層重啓(出現開機動畫,然後恢復到keyguard)

分析

  • 機器重啓的原因
    zygote 接收到SIG 1(SIGHUP) , 從而zygote 退出,android 上層重啓。
    重啓Kernel Log

原理分析

下面是Android進程創建關係圖

會話VS進程組VS進程VS線程
進程創建關係圖

  • 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()。
    這裏寫圖片描述
  • 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 狀態)

    這裏寫圖片描述
    正常狀態下的進程創建
    這裏寫圖片描述
    出現異常時候,父進程異常退出,更新父進程爲init進程
    這裏寫圖片描述
    天天遊戲的進程狀態圖

這裏寫圖片描述
正常流程中,檢測到異常,重啓遊戲後,自己退出

假如此時有process 處於STOP (T) 狀態,機器必然打到孤兒進程組SIG 1 -> SIG 18 case, 殺掉整個zygote Process Group, 機器重啓,原理圖如下所示:
這裏寫圖片描述

  • 最快復現手法
  • 首先開啓天天遊戲
  • 隨意抓一個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
    執行:
    這裏寫圖片描述
  • adb shell kill -19 other APK
  • adb shell kill -9 this apk
  • adb shell kill -9 logcat
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章