Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 503, comments - 13, trackbacks - 0, articles - 1

          1、引言

          隨著Android系統(tǒng)的不斷升級(jí),即時(shí)通訊網(wǎng)技術(shù)群和社區(qū)里的IM和推送開發(fā)的程序員們,對(duì)于進(jìn)程保活這件事是越來越悲觀,必竟系統(tǒng)對(duì)各種保活黑科技的限制越來越多了,想超越系統(tǒng)的摯肘,難度越來越大。

          但保活這件事就像“激情”之后的余味,總是讓人欲罷不能,想放棄又不甘心。那么,除了像上篇《2020年了,Android后臺(tái)保活還有戲嗎?看我如何優(yōu)雅的實(shí)現(xiàn)!》這樣的正經(jīng)白名單方式,不正經(jīng)的“黑科技”是否還有發(fā)揮的余地?

          答案是肯定的,“黑科技”仍發(fā)揮的余地。不是“黑科技”不行,而是技術(shù)沒到位。

          研究TIM的保活是一次偶然機(jī)會(huì),發(fā)現(xiàn)在安全中心關(guān)閉了它的自啟動(dòng)功能的情況下, 一鍵清理、強(qiáng)力清理等各大招都無法徹底殺掉TIM,系統(tǒng)的自啟動(dòng)攔截也沒能阻止TIM的永生,這引起了我強(qiáng)烈的興趣,于是便有了本文。

          本文將從Andriod系統(tǒng)層面為你深入剖析騰訊TIM這款I(lǐng)M應(yīng)用的超強(qiáng)保活能力,希望能給你帶來更多Android方面的靈感。

           

          * 特別申明:本文的技術(shù)研究和分析過程,僅供技術(shù)愛好者學(xué)習(xí)的用途,請(qǐng)勿用作非法用途。

          擴(kuò)展知識(shí):騰訊TIM是什么?(以下文字來自百度百科)

          TIM是由騰訊公司于2016年11月發(fā)布的多平臺(tái)IM客戶端應(yīng)用。TIM是在QQ輕聊版的基礎(chǔ)上加入了協(xié)同辦公服務(wù)的支持,可QQ號(hào)登錄,以及好友、消息同步等,適合辦公使用。

          (本文同步發(fā)布于:http://www.52im.net/thread-2893-1-1.html

          2、本文作者 

          袁輝輝:2019年5月加入字節(jié)跳動(dòng)移動(dòng)平臺(tái)部。畢業(yè)于西安電子科技大,曾就職于小米、聯(lián)想、IBM。

          之前主要經(jīng)歷從事Android手機(jī)系統(tǒng)研發(fā),在上一份小米MIUI系統(tǒng)組工作期間主要負(fù)責(zé)小米手機(jī)Android Framework架構(gòu)優(yōu)化、系統(tǒng)穩(wěn)定、技術(shù)預(yù)研、平臺(tái)建設(shè)等工作。熱衷于研究Android系統(tǒng)內(nèi)核技術(shù),對(duì)Android系統(tǒng)框架有著深刻理解與豐富的實(shí)戰(zhàn)經(jīng)驗(yàn),編寫近200篇高質(zhì)量文章,多次受邀參加業(yè)內(nèi)Android技術(shù)大會(huì)演講。

          3、保活技術(shù)回顧

          Android保活技術(shù)的進(jìn)化,可以分為幾個(gè)階段。

          第一個(gè)階段:也就是各種“黑科技”盛行的時(shí)代,比如某Q搞出來的1像素、后臺(tái)無聲音樂(某運(yùn)動(dòng)計(jì)步APP就干過)等等。

          這個(gè)階段的一些典型主要技術(shù)手段,可以看以下這幾篇文章:

          1. 應(yīng)用保活終極總結(jié)(一):Android6.0以下的雙進(jìn)程守護(hù)保活實(shí)踐
          2. Android進(jìn)程保活詳解:一篇文章解決你的所有疑問
          3. 微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)保活實(shí)戰(zhàn)分享(進(jìn)程保活篇)

          第二個(gè)階段:到了Android 6.0時(shí)代以后,Android保活就開始有點(diǎn)技術(shù)難度了,之前的各種無腦保活方法開始慢慢失效。

          這個(gè)階段的一些典型技術(shù)手段,可以讀讀以下這幾篇文章:

          1. 應(yīng)用保活終極總結(jié)(二):Android6.0及以上的保活實(shí)踐(進(jìn)程防殺篇)
          2. 應(yīng)用保活終極總結(jié)(三):Android6.0及以上的保活實(shí)踐(被殺復(fù)活篇)

          第三個(gè)階段:進(jìn)入Android 8.0時(shí)代,Android直接在系統(tǒng)層面進(jìn)行了各種越來越嚴(yán)格的管控,可以用的保活手段越來越少,保活技術(shù)的發(fā)展方向已發(fā)分化為兩個(gè)方向——要么用白名單的方式走正經(jīng)的保活路徑、要么越來越“黑”一“黑”到底(比如本文將要介紹的TIM的保活手段)。

          這個(gè)階段可以用的保活已經(jīng)手段不多了,以下幾篇盤點(diǎn)了目前的一些技術(shù)可行性現(xiàn)狀等:

          1. Android P正式版即將到來:后臺(tái)應(yīng)用保活、消息推送的真正噩夢(mèng)
          2. 全面盤點(diǎn)當(dāng)前Android后臺(tái)保活方案的真實(shí)運(yùn)行效果(截止2019年前)
          3. 2020年了,Android后臺(tái)保活還有戲嗎?看我如何優(yōu)雅的實(shí)現(xiàn)!

          4、什么是保活?

          保活就是在用戶主動(dòng)殺進(jìn)程,或者系統(tǒng)基于當(dāng)前內(nèi)存不足狀態(tài)而觸發(fā)清理進(jìn)程后,該進(jìn)程設(shè)法讓自己免于被殺的命運(yùn)或者被殺后能立刻重生的手段。

          保活是”應(yīng)用的蜜罐,系統(tǒng)的腫瘤“,應(yīng)用高保活率給自己贏得在線時(shí)長(zhǎng),甚至做各種應(yīng)用想做而用戶不期望的行為,給系統(tǒng)帶來的是不必要的耗電,以及系統(tǒng)額外的性能負(fù)擔(dān)。

          保活方案一直就層出不窮,APP開發(fā)們不斷地絞盡腦汁讓自己的應(yīng)用能存活得時(shí)間更長(zhǎng), 主要思路有以下兩個(gè)。

          提升進(jìn)程優(yōu)先級(jí),降低被殺概率:

          • 1)比如監(jiān)聽SCREEN_ON/OFF廣播,啟動(dòng)一像素的透明Activity;
          • 2)啟動(dòng)空通知,提升fg-service;
          • ... ...

          進(jìn)程被殺后,重新拉起進(jìn)程:

          • 1)監(jiān)聽系統(tǒng)或者第3方廣播拉起進(jìn)程。但目前安全中心/Whetstone已攔截;
          • 2)Native fork進(jìn)程相互監(jiān)聽,監(jiān)聽到父進(jìn)程被殺,則通過am命令啟動(dòng)進(jìn)程。force-stop會(huì)殺整個(gè)進(jìn)程組,所以這個(gè)方法幾乎很難生效了。

          5、初步分析

          5.1 初識(shí)TIM

          執(zhí)行命令adb shell ps | grep tencent.tim,可見TIM共有4個(gè)進(jìn)程, 其父進(jìn)程都是Zygote:

          root@gityuan:/ # ps | grep tencent.tim

          u0_a146   27965 551   1230992 43964 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:Daemon

          u0_a146   27996 551   1252492 54032 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:MSF

          u0_a146   28364 551   1348616 89204 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:mail

          u0_a146   31587 551   1406128 147976 SyS_epoll_ 00f6df4bf0 S com.tencent.tim

          5.2 一鍵清理看現(xiàn)象,排查初步懷疑

          以下是對(duì)TIM執(zhí)行一鍵清理后的日志:

          12-21 21:12:20.265  1053  1075 I am_kill : [0,4892,com.tencent.tim:Daemon,5,stop com.tencent.tim: from pid 4617]

          12-21 21:12:20.272  1053  1075 I am_kill : [0,5276,com.tencent.tim:mail,2,stop com.tencent.tim: from pid 4617]

          12-21 21:12:20.305  1053  1075 I am_kill : [0,4928,com.tencent.tim,2,stop com.tencent.tim: from pid 4617]

          12-21 21:12:20.330  1053  1075 I am_kill : [0,4910,com.tencent.tim:MSF,0,stop com.tencent.tim: from pid 4617]

          12-21 21:13:59.920  1053  1466 I am_proc_start: [0,5487,10146,com.tencent.tim:MSF,service,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService]

          12-21 21:13:59.984  1053  1604 I am_proc_start: [0,5516,10146,com.tencent.tim,content provider,com.tencent.tim/com.tencent.mqq.shared_file_accessor.ContentProviderImpl]

          Force-stop是系統(tǒng)提供的殺進(jìn)程最為徹底的方式,詳見文章《Android進(jìn)程絕殺技–forceStop》。從日志可以發(fā)現(xiàn)一鍵清理后TIM的4個(gè)進(jìn)程全部都已被Force-stop。但進(jìn)程com.tencent.tim:MSF立刻就被DaemonMsfService服務(wù)啟動(dòng)過程而拉起。

          問題1:安全中心已配置了禁止TIM的自啟動(dòng), 并且安全中心和系統(tǒng)都有對(duì)進(jìn)程自啟動(dòng)以及級(jí)聯(lián)啟動(dòng)的嚴(yán)格限制,為何會(huì)有漏網(wǎng)之魚?

          懷疑1: 是否安全中心自啟動(dòng)沒能有效限制,以及微信/QQ跟TIM有所級(jí)聯(lián),比如com.tencent.mobileqq.app.DaemonMsfService服務(wù)名中以com.tencent.mobileqq(QQ的包名)開頭。

          經(jīng)過dumpsys以及反復(fù)驗(yàn)證后排除了這種可能性,如下:

          12-21 21:12:20.266  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          12-21 21:12:20.291  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          12-21 21:12:20.323  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          12-21 21:12:20.323  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          12-21 21:12:20.331  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          12-21 21:12:20.332  1053  1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

          懷疑2: 是否在TIM進(jìn)程被殺后, 收到BinderDied后的死亡回調(diào)過程中將Service再次拉起,這個(gè)情況也很快就被排除, 因?yàn)閒orce-stop這種冷面強(qiáng)力殺手, 并不會(huì)等到死亡回調(diào)再去清理進(jìn)程相關(guān)信息,而是直接連根拔起,并不會(huì)走到AMS的死亡回調(diào)。

          懷疑3: TIM設(shè)置了alarm機(jī)制,在callApp為空符合特征, 但經(jīng)過分析這里就是普通的startService, 非startServiceInPackage(), 也排除了這種可能性:

          //啟動(dòng)DaemonAssistService時(shí),callApp為空,只有通過PendingIntent方式才可能出現(xiàn)這種情況

          12-21 21:56:54.653 3181 3195 I am_start_service: [-1,NULL,10146,com.tencent.tim:Daemon,com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService}]

          12-21 21:56:56.666 3181 3827 I am_start_service: [-1,NULL,10146,com.tencent.tim:MSF,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService}]

          既然排除以上3種可能,直接上斷點(diǎn)來看看吧。

          5.3 Android Studio斷點(diǎn)分析

          一上斷點(diǎn)就發(fā)現(xiàn)了意外的一幕: 

          問題2:startService()的callingPid怎么可能等于0?

          5.3.1)分析callingPid=0:

          為什么說上面是意外的一幕呢?這需要對(duì)binder底層原理有一定深入理解,才能看出一些端倪,那就是此處callingPid=0是不合理邏輯的。很多人可能不太理解為何就不合乎邏輯, 這要從Binder原理說起, startService()這個(gè)Binder call是屬于同步binder調(diào)用, 對(duì)于binder調(diào)用過程,只有異步Binder調(diào)用的情況下callingPid=0才會(huì)為空, 因?yàn)椴恍枰猺eply應(yīng)答數(shù)據(jù)給發(fā)送binder請(qǐng)求的那一端。 但如果是同步的,則必須要給出callingPid,否則無法將應(yīng)答數(shù)據(jù)回傳給發(fā)送方。 這是由Binder Driver所決定的,見如下Binder Driver核心代碼。

          (1) Binder發(fā)起端:根據(jù)當(dāng)前ONE_WAY來決定是否設(shè)置from線程

          binder_transaction(...) {

              ...

              if(!reply && !(tr->flags & TF_ONE_WAY))

                  t->from = thread;

              else

                  t->from = NULL;

              }

              ...

          }

          (2) Binder接收端: 根據(jù)from線程是否為空, 來決定sender_pid是否為0. 這便是Java層所說的callingPid

          binder_thread_read(...) {

              ...

              t_from = binder_get_txn_from(t);

              if(t_from) {

                  structtask_struct *sender = t_from->proc->tsk;

           

                  tr.sender_pid = task_tgid_nr_ns(sender,

                                  task_active_pid_ns(current));

              } else{

                  tr.sender_pid = 0;

              }

              ...

          }

          上述代碼表明: 同步的Binder調(diào)用的情況下則callingPid必定不等于0。

          下面告訴大家如何看一個(gè)Binder調(diào)用是否同步, 如下圖最后一個(gè)參數(shù)代表的是FLAG_ONEWAY值,等于0則代表的是同步, 等于1則代表的是異步。

          以上代碼是framework的框架代碼,startService最終都會(huì)調(diào)用到這里來,所以callingPid必然是不可能出現(xiàn)為0的情況,讓我們看不透到底哪個(gè)進(jìn)程把com.tencent.tim: Daemon拉起的。

          5.3.2)揭秘:

          從前面的分析來看callingPid是不可能為0的, 但從結(jié)果來看的確是0, 出現(xiàn)矛盾就一定有反常規(guī)存在,難道是存在同步的Binder調(diào)用,也存在同時(shí)callingPid=0的case?答案是No.

          從源碼角度來看是沒有這種可能性存在,后面再進(jìn)一步追蹤flags值的變化,從如下的flags=17,可以確定的是此處的startService的binder call是ONE_WAY的,這就可以確定的確是發(fā)起了異步的Binder調(diào)用。

          代碼如下: 

          雖然callingPid=0,但從callUid=10146可以確定的一點(diǎn)是com.tencent.tim: Daemon進(jìn)程是被來自TIM應(yīng)用自身的某個(gè)進(jìn)程所拉起的。

          5.4 小結(jié)

          通過前面的初步分析,先整理一下思路,有以下初步結(jié)論:

          • 1)TIM至少有4個(gè)進(jìn)程,且都是由Zygote進(jìn)程fork, 保活是通過startService被拉起;
          • 2)排除 安全中心的對(duì)TIM限制自啟動(dòng)功能失效的情況;
          • 3)排除 TIM進(jìn)程被殺后的Binder死亡回調(diào)過程通過Service重新拉起進(jìn)程;
          • 4)排除 alarm機(jī)制 拉起進(jìn)程;
          • 5)從callingPid=0,可以得出TIM沒有走常規(guī)的系統(tǒng)框架中提供的startService()接口來啟動(dòng)服務(wù),而是自定義的方式;
          • 6)從callingUid=10146, 可以得出TIM救活自己的方式,是通過TIM自身,而非系統(tǒng)或者第三方應(yīng)用拉起。

          到此不難得出一個(gè)猜想: 首先TIM應(yīng)用能做到監(jiān)聽?wèi)?yīng)用進(jìn)程被殺的情況, 其次是TIM應(yīng)用自身替換掉或者自定義一套Binder調(diào)用,主動(dòng)跟Binder驅(qū)動(dòng)進(jìn)行數(shù)據(jù)交互。

          6、深入分析

          6.1 尋求規(guī)律

          TIM應(yīng)用有4個(gè)進(jìn)程,不斷反復(fù)地嘗試殺TIM每一個(gè)進(jìn)程后,觀察自啟動(dòng)的情況后。 發(fā)現(xiàn)了一個(gè)規(guī)律:com.tencent.tim: Daemon和com.tencent.tim:MSF進(jìn)程任一被殺,都會(huì)先把對(duì)方進(jìn)程拉起,然后跟著自殺后,再重啟。

          接下來就把范圍鎖定在這兩個(gè)進(jìn)程,然后來tracing信號(hào)處理情況。

          6.2 從signal角度來分析

          打開signal開關(guān):

          root@gityuan:/ # echo 1 > /d/tracing/events/signal/enable

          root@gityuan:/ # echo 1 > /d/tracing/tracing_on

          執(zhí)行如下命令抓取tracing log:

          root@cancro/: cat/d/tracing/trace_pipe

          日志如下:

          //通過adb shell kill-9 10649,  將com.tencent.tim:Daemon進(jìn)程殺掉

                 sh-22775 [000] d..2 18844.276419: signal_generate: sig=9 errno=0 code=0 comm=cent.tim:Daemon pid=10649 grp=1 res=0

          //線程Thread-89 將tencent.tim:MSF進(jìn)程也殺掉了

                Thread-89-10712 [000] dn.2 18844.340735: signal_generate: sig=9 errno=0 code=0 comm=tencent.tim:MSF pid=10669 grp=1 res=0

            Binder:14682_4-14845 [000] d..2 18844.340779: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

            Binder:14682_1-14694 [000] d..2 18844.341418: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

            Binder:14682_2-14697 [000] d..2 18844.345075: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

           tencent.tim:MSF-14682 [000] dn.2 18844.345115: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=

          從這里,可以發(fā)現(xiàn)com.tencent.tim: Daemon進(jìn)程是由于其中一個(gè)線程Thread-89所殺,但從名字來看Thread-xxx,很明顯是系統(tǒng)自動(dòng)生成的編號(hào)。

          問題3:進(jìn)程內(nèi)的名叫“Thread-89”的線程具有什么特點(diǎn),如何做到把進(jìn)程殺掉?

          從下面的截圖,可以看出MSF進(jìn)程的這個(gè)特殊的線程當(dāng)前在執(zhí)行flock_lock操作,這個(gè)明顯是一個(gè)文件加鎖的操作, 這個(gè)方法很快就引起了我的注意。同理Daemon進(jìn)程也有一個(gè)這樣的線程, 離真相有近了一步。 

           

          再來看看調(diào)用棧情況:

          Cmd line: com.tencent.tim:Daemon

          "Thread-89"prio=10 tid=12 Native

            | group="main"sCount=1 dsCount=0 obj=0x32c07460 self=0xf3382000

            | sysTid=10712 nice=-8 cgrp=bg_non_interactive sched=0/0handle=0xee824930

            | state=S schedstat=( 44972457 14188383 124 ) utm=1 stm=3 core=0 HZ=100

            | stack=0xee722000-0xee724000 stackSize=1038KB

            | held mutexes=

            kernel: __switch_to+0x74/0x8c

            kernel: flock_lock_file_wait+0x2a4/0x318

            kernel: SyS_flock+0x19c/0x1a8

            kernel: el0_svc_naked+0x20/0x28

            native: #00 pc 000423d4  /system/lib/libc.so (flock+8)

            native: #01 pc 0000195d  /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+64)

           ...

            native: #29 pc 0000191f  /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+2)

            native: #30 pc 0000191d  /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc)

            native: #31 pc 0000191b  /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+102)

            ...

            native: #63 pc 000018d1  /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+28)

            at com.libwatermelon.WaterDaemon.doDaemon2(Native method)

            at com.libwatermelon.strategy.WaterStrategy2$2.run(WaterStrategy2.java:111)

          從這個(gè)線程的調(diào)用棧中的名字, notify_and_waitfor讓我想到了這極有可能用于監(jiān)聽文件來獲知進(jìn)程是否存活。 為了進(jìn)一步觀察這個(gè)特殊線程的工作使命, 這里還不需要GDB, 祭出strace大招應(yīng)該就差不多。

          6.3 利用strace分析

          root@gityuan:/ # strace -CttTip 22829 -CttTip 22793

          結(jié)果如下:

          flock基礎(chǔ)知識(shí)簡(jiǎn)介:

          flock是Linux文件鎖,用于多個(gè)進(jìn)程同時(shí)操作同一個(gè)文件時(shí),通過加鎖機(jī)制保證數(shù)據(jù)的完整,flock使用場(chǎng)景之一,便是用于檢測(cè)進(jìn)程是否存在。flock屬于建議性的鎖,而非強(qiáng)制性鎖,只是進(jìn)程可以直接操作正被另一個(gè)進(jìn)程用flock鎖住的文件, 原因在于flock只檢測(cè)文件是否加鎖,內(nèi)核并不會(huì)強(qiáng)制阻塞其他進(jìn)程的讀寫操作,這便是建議性鎖的內(nèi)核策略。

          方法原型: intflock(intfd, intoperation)

          第一個(gè)參數(shù)是文件描述符,第二參數(shù)指定鎖的類型,有以下3個(gè)可選值:

          • 1)LOCK_SH: 共享鎖, 同一時(shí)間運(yùn)行多個(gè)進(jìn)程同時(shí)持有該共享鎖;
          • 2)LOCK_EX: 排它鎖,只允許一個(gè)進(jìn)程持有該鎖;
          • 3)LOCK_UN: 移除該進(jìn)程的該文件所持有的鎖。

          從strace可以推測(cè)出:com.tencent.tim:MSF進(jìn)程的監(jiān)控線程執(zhí)行排它鎖LOCK_EX類型的flock,嘗試去獲取某個(gè)文件,而該文件已被com.tencent.tim: Daemon進(jìn)程所持有,所以MSF進(jìn)程會(huì)被阻塞知道鎖的釋放,而一旦Daemon進(jìn)程被殺,系統(tǒng)就會(huì)回收所有資源(包括文件),這是Linux內(nèi)核負(fù)責(zé)完成的。

          當(dāng)Daemon進(jìn)程的文件被回收,就會(huì)釋放flock, 從而MSF進(jìn)程可以獲取該鎖,從而吐出“lock file success”的信息。 MSF得知Daemon進(jìn)程被殺,然后執(zhí)行一行ioctl(11, BINDER_WRITE_READ, 0xffffffffee823ed0) = 0 <0.000867> 。

          這個(gè)應(yīng)該就是TIM進(jìn)程自身實(shí)現(xiàn)了一套執(zhí)行startService的Binder調(diào)用,向Binder驅(qū)動(dòng)發(fā)送 BINDER_WRITE_READ的ioctl命令。 再然后發(fā)送kill SIGKILL將自身MSF進(jìn)程殺掉,同樣的道理可以再次被拉起。

          分析到這里,看執(zhí)行了writev操作, 應(yīng)該就是Log操作, 有一個(gè)關(guān)鍵詞到 Watermelon 吸引了我的注意力, 搜索 Watermelon 關(guān)鍵詞,果然找到新的一片天地。

          6.4 TIM日志

          //舊的MSF進(jìn)程

          24538 24562 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

          24538 24562 E Watermelon: Watch >>>>Daemon<<<<< Daed !!

          24538 24562 E Watermelon: java_callback:onDaemonDead

          24538 24562 V Watermelon: onDaemonDead

          24576 24576 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

          24576 24576 E Watermelon: Watch >>>>Daemon<<<<< Daed !!

          24576 24576 E Watermelon: process exit

          //新daemon進(jìn)程

          25103 25103 V Watermelon: initDaemon processName=com.tencent.tim:Daemon

          25103 25103 E Watermelon: onDaemonAssistantCreate

          25134 25134 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2

          //app_d進(jìn)程

          25137 25137 D Watermelon: pipe readdatasize >> 316 <<

          25137 25137 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

          25137 25137 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_p1

          25137 25137 I Watermelon: sIActivityManager==NULL

          25137 25137 I Watermelon: BpActivityManager init

          //新daemon

          25103 25120 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

          25103 25120 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

          25137 25137 I Watermelon: BpActivityManager init end

          //app_d進(jìn)程

          25137 25137 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

          25137 25137 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

          //新MSF進(jìn)程

          25119 25119 V Watermelon: initDaemon processName=com.tencent.tim:MSF

          25119 25119 V Watermelon: mConfigurations.PERSISTENT_CONFIG.PROCESS_NAME=com.tencent.tim:MSF

          25119 25119 E Watermelon: onPersistentCreate

          25153 25153 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2

          25119 25144 D Watermelon: pipe write len=324

          25159 25159 D Watermelon: pipe readdatasize >> 324 <<

          25159 25159 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

          25159 25159 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_d1

          25159 25159 I Watermelon: sIActivityManager==NULL

          25159 25159 I Watermelon: BpActivityManager init

          25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

          25119 25144 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

          25159 25159 I Watermelon: BpActivityManager init end

          //各進(jìn)程進(jìn)入監(jiān)聽就緒狀態(tài)

          25159 25159 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

          25159 25159 D Watermelon: lock filesuccess  >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

          25119 25144 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

          25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

          25159 25159 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

          25159 25159 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

          25137 25137 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

          25137 25137 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

          25103 25120 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

          25103 25120 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

          再?gòu)钠渲械慕厝『诵钠危?/strong>

          25159 25159 I Watermelon: BpActivityManager init

          25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

          25119 25144 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

          不難看出:

          • 1)TIM自身通過向servicemanager查詢來獲取AMS的代理BpActivityManager, 然后自己去寫startService通信過程的數(shù)據(jù);
          • 2)TIM通過兩個(gè)進(jìn)程通過flock來相互監(jiān)聽對(duì)方進(jìn)程存活狀態(tài);
          • 3)監(jiān)聽的文件有比如:/data/user/0/com.tencent.tim/app_indicators/indicator_d2。

          6.5 indicator文件

          進(jìn)一步查看TIM所監(jiān)聽的路徑下/data/user/0/com.tencent.tim/app_indicators/, 發(fā)現(xiàn)有4個(gè)監(jiān)聽文件: 

          問題4:為何需要4個(gè)indicator文件?

          進(jìn)一步延伸:通過查看flock,再次發(fā)現(xiàn)新大陸,原來除了Daemon和MSF進(jìn)程各有一個(gè)監(jiān)聽文件的線程, 還有兩個(gè)由init進(jìn)程作為父進(jìn)程的app_d進(jìn)程也監(jiān)聽文件:

          gityuan@13203:~/gityuan$ adb shell ps-t | grep-i flock

          u0_a146   10668 10649 1143304 85876 flock_lock 00f6e1e3d8 S Thread-85

          u0_a146   10712 10669 1158552 89664 flock_lock 00f6e1e3d8 S Thread-89

          u0_a146   10687 1     12768  564   flock_lock 00f73113d8 S app_d

          u0_a146   10717 1     12768  560   flock_lock 00f74353d8 S app_d

          不難發(fā)現(xiàn),以上幾個(gè)進(jìn)程/線程的uid=10146,進(jìn)一步通過ps命名查找。

          再一次刷新對(duì)TIM應(yīng)用的認(rèn)識(shí):原來TIM有6個(gè)進(jìn)程,其中還有2個(gè)是掛在init進(jìn)程下,名字跟tencent沒有關(guān)系,差點(diǎn)錯(cuò)過了這兩個(gè)特殊的進(jìn)程。

          這兩個(gè)app_d進(jìn)程其實(shí)也是做著同樣的相互監(jiān)聽的工作, 應(yīng)該是備選方案。當(dāng)有概率恰巧Daemon和MSF進(jìn)程同時(shí)被殺而來不及互保的情況下,那么可以走緊急通道app_d 將TIM進(jìn)程拉起。可謂是暗藏玄機(jī), 6個(gè)進(jìn)程中有4個(gè)進(jìn)程可以相互保活, 以保證TIM進(jìn)程永生。

          問題5: 這4個(gè)進(jìn)程到達(dá)是什么如何相互監(jiān)聽的呢?

          通過不斷分析被殺與重啟前后的規(guī)律與特征,得出進(jìn)程與監(jiān)聽文件的關(guān)系圖: 

          進(jìn)一步揭露面紗,得到如下結(jié)論:

          • 1)Daemon與MSF兩進(jìn)程等待對(duì)方所持有的鎖,兩個(gè)app_d進(jìn)程相互等待對(duì)方所持有的鎖;
          • 2)app_d1進(jìn)程被殺, 則app_d2觀察后通過拉起DaemonMsfService服務(wù)來啟動(dòng)MSF進(jìn)程,然后跟著被殺;
          • 3)app_d2進(jìn)程被殺,則app_d1觀察后通過拉起DaemonAssistService服務(wù)來啟動(dòng)Daemon進(jìn)程,然后跟著被殺;
          • 4)Daemon與MSF兩進(jìn)程, 如果殺掉其中一個(gè),則另個(gè)一個(gè)進(jìn)程觀察后通過拉起服務(wù)方式來啟動(dòng)對(duì)方進(jìn)程,然后跟著被殺;然后app_d兩個(gè)進(jìn)程也跟著重啟。

          另外猜想:監(jiān)測(cè)indicator_p1和indicator_p2的兩個(gè)進(jìn)程有關(guān)聯(lián),indicator_d1和indicator_d2的進(jìn)程有關(guān)聯(lián),后面會(huì)驗(yàn)證。

          到這里又有出現(xiàn)新的疑問:Daemon進(jìn)程死后,MSF進(jìn)程通過flock能監(jiān)測(cè)到該事件,可是app_d進(jìn)程又是如何得知的呢? app_d得知之后,又為何要再次自殺重啟?

          6.6 從cgroup角度來分析

          root@gityuan:/acct/uid_10146/pid_10649# cat cgroup.procs                       

          10649    //Daemon

          10687    //app_d

          root@gityuan:/acct/uid_10146/pid_10669# cat cgroup.procs                       

          10669   //MSF

          10717  //app_d

          從而,進(jìn)一步獲取更多關(guān)于TIM深層次的關(guān)聯(lián),通過查看cgroup發(fā)現(xiàn),Daemon和app_d1是同一個(gè)group的, MSF和app_d2是同一個(gè)group的。

          問題6: app_d到底是如何創(chuàng)建出來?又是如何成為init進(jìn)程的子進(jìn)程的?

          從進(jìn)程創(chuàng)建與退出的角度來看看來看:

          //5170(MSF進(jìn)程) --> 5192 --> 5201(退出) --> 5211(存活)

          tencent.tim:MSF-5170  [001] ...1 55659.446062: sched_process_fork: comm=tencent.tim:MSF pid=5170 child_comm=tencent.tim:MSF child_pid=519

          Thread-300-5192  [000] ...1 55659.489621: sched_process_fork: comm=Thread-300 pid=5192 child_comm=Thread-300 child_pid=5201

          <...>-5201  [003] ...1  55659.501074: sched_process_exec: filename=/data/user/0/com.tencent.tim/app_bin/daemon2pid=5201 old_pid=5201

          daemon2-5201  [009] ...1  55659.533492: sched_process_fork: comm=daemon2 pid=5201 child_comm=daemon2 child_pid=5211

          daemon2-5201  [009] ...1  55659.535169: sched_process_exit: comm=daemon2 pid=5201 prio=120

          daemon2-5201  [009] d..3  55659.535341: signal_generate: sig=17 errno=0 code=262145 comm=Thread-300 pid=5192 grp=1 res=1

          說明:其中一個(gè)app_d進(jìn)程是由MSF進(jìn)程,通過兩次fork,然后父進(jìn)程退出,從而成為了孤兒進(jìn)程,然后托孤給init進(jìn)程,這是Linux進(jìn)程機(jī)制所保證的。 同理,另一個(gè)app_d進(jìn)程是由Daemon進(jìn)程所fork。到這里,那么總算是認(rèn)清的app_d的由來。 app_d是由于cgroup關(guān)聯(lián)所以可以得知Daemon進(jìn)程的情況。 關(guān)于重啟的原因是為了重新建立互動(dòng)的關(guān)系。

          問題7:為何單殺daemon,會(huì)牽連app_d進(jìn)程被殺,這是什么原理?

          解答:從殺進(jìn)程的日志上來是調(diào)用killProcessGroup()殺進(jìn)程,可事實(shí)上adb只調(diào)用kill -9 pid的方式,單殺一個(gè)進(jìn)程,怎么就牽連了app_d進(jìn)程。 這是由于當(dāng)daemon進(jìn)程被殺后,死亡回調(diào)會(huì)回來后,在binderDied()的過程執(zhí)行了killProcessGroup()。

          如果從Linux內(nèi)核層面,研究過Binder死亡回調(diào)機(jī)制的童鞋,到這里還就會(huì)有想到一個(gè)新的疑問如下。

          問題8:app_d是由daemon進(jìn)程間接fork出來的, 會(huì)共享binder fd,所以即便daemon進(jìn)程被殺,死亡回調(diào)也不會(huì)觸發(fā),這又是何觸發(fā)的呢?

          解答:由于app_d進(jìn)程被fork后,馬上執(zhí)行了exec()系的函數(shù), 而在ProcessState打開Binder驅(qū)動(dòng)的時(shí)候, 有一個(gè)非常重要的flag, 那就是O_CLOEXEC。

          采用O_CLOEXEC方式打開的問題,當(dāng)新創(chuàng)建的進(jìn)程調(diào)用exec()函數(shù)成功后,文件描述符會(huì)自動(dòng)關(guān)閉, 代碼如下:

          6.7 剖根問底

          問題9:TIM到底對(duì)Binder框架做了什么級(jí)別的修改?這4個(gè)互保進(jìn)程,既然callingPid=0,有沒有辦法知道到底是由誰(shuí)拉起誰(shuí)的?

          前面既然說了,TIM強(qiáng)行修改了ONEWAY的方式。可以去掉該flags, 為了調(diào)試,這里就針對(duì)TIM,并且code=34(即START_SERVICE_TRANSACTION), 并且修改flag的case下:

          從實(shí)驗(yàn)結(jié)果來看,通過修改IPCThreadState.cpp代碼, 完成control住了 TIM的所有修改, 這里可以說明:

          TIM分別在Java層和Native層,主動(dòng)向ServiceManager進(jìn)程查詢AMS后,獲取BpActivityManager代理對(duì)象,然后繼續(xù)使用框架中的IPCThreadState跟Binder驅(qū)動(dòng)交互,并沒有替換掉libbinder.so。

          其實(shí),還可以更高級(jí)的玩法,連IPCThreadState這些框架通信代碼也不使用, 徹底地去自定義Binder交互代碼,類似于servicemanager的方式。可以自己封裝ioctl(),直接talkWithDriver。TIM保活還有改進(jìn)空間, 提供保活變種方案,這樣的話,上面的調(diào)試代碼也攔截不了其對(duì)flags修改為ONEWAY的過程。 即使如此,一切都在Control之中, 完全可以在Binder Driver中攔截再定位其策略, 玩得再高級(jí)也主要活動(dòng)在用戶態(tài), 內(nèi)核態(tài)的策略還是相對(duì)安全的, 此所謂“魔高一座,道高一尺”。

          另外,通過增加上面的臨時(shí)代碼,再次多次實(shí)驗(yàn)對(duì)比,可以得出如下關(guān)系圖:

          二度fork是指前面介紹了,fork后再fork,然后托孤,無論如何跟最初的進(jìn)程都屬于同一個(gè)group,有著級(jí)聯(lián)被殺關(guān)系。

          • 1)殺掉Daemon進(jìn)程,則MSF進(jìn)程觀察到會(huì)去拉起Daemon進(jìn)程; 同時(shí)app_d1因?yàn)橥粋€(gè)group而被殺,則app_d2進(jìn)程觀察到也拉起Daemon進(jìn)程,這就是雙保險(xiǎn);
          • 2)殺掉app_d1進(jìn)程, 則app_d2進(jìn)程觀察到會(huì)拉起MSF進(jìn)程;
          • 3)直接force-stop進(jìn)程, 則6個(gè)進(jìn)程都會(huì)被殺,只是殺的過程并非所有進(jìn)程同一時(shí)刻點(diǎn)被殺,而是有前后順序,所以造成能自啟。

          6.8 分析思路歸納

          我們來回顧一下上面的過程:

          • 1)先有了初步分析過程中對(duì)一些常規(guī)套路的可能性的排除,并嗅到callingPid=0的異常舉動(dòng);
          • 2)沿著蛛絲馬跡,不斷反復(fù)嘗試殺進(jìn)程,從中尋找更多的規(guī)律,不斷地向自己提出疑問;
          • 3)結(jié)合signal,strace, traces,ps,binder,linux,kill等技能 不斷地解答自己的疑惑。

          解系統(tǒng)層的問題,更像是偵探破案的感覺,要有敏銳的嗅覺,抓住蛛絲馬跡,加上”大膽猜想,小心驗(yàn)證“ , 終究能找到案件的真相。 此所謂”點(diǎn)動(dòng)成線,線動(dòng)成面,面動(dòng)成體“, 從零星的點(diǎn)滴勾畫出全方面立體化的真相。

          歸納下,主要提出過這些疑惑:

          • 問題1:安全中心已配置了禁止TIM的自啟動(dòng), 并且安全中心和Whetstone都有對(duì)進(jìn)程自啟動(dòng)以及級(jí)聯(lián)啟動(dòng)的嚴(yán)格限制, 為何會(huì)有漏網(wǎng)之魚?
          • 問題2:startService()的callingPid怎么可能等于0?
          • 問題3:進(jìn)程內(nèi)的名叫“Thread-89”的線程具有什么特點(diǎn),如何做到把進(jìn)程殺掉?
          • 問題4:為何需要4個(gè)indicator文件?
          • 問題5: 這4個(gè)進(jìn)程到達(dá)是什么如何相互監(jiān)聽的呢?
          • 問題6: app_d到底是如何創(chuàng)建出來?又是如何成為init進(jìn)程的子進(jìn)程的?
          • 問題7:為何單殺daemon,會(huì)牽連app_d進(jìn)程被殺,這是什么原理?
          • 問題8:app_d是由daemon進(jìn)程間接fork出來的, 會(huì)共享binder fd,所以即便daemon進(jìn)程被殺,死亡回調(diào)也不會(huì)觸發(fā),這又是何觸發(fā)的呢?
          • 問題9:TIM到底對(duì)Binder框架做了什么級(jí)別的修改?這4個(gè)互保進(jìn)程,既然callingPid=0,有沒有辦法知道到底是由誰(shuí)拉起誰(shuí)的?

          7、本文總結(jié)

          總結(jié)一下TIM的保活技術(shù)要點(diǎn),我們可以得出以下經(jīng)驗(yàn):

          • 1)通過flock的文件排它鎖方式來監(jiān)聽進(jìn)程存活狀態(tài)
          • 1.1)先采用一對(duì)普通的進(jìn)程Daemon和MSF相互監(jiān)聽文件的方式來獲得對(duì)方進(jìn)程是否存活的狀態(tài);
          • 1.2)同時(shí)再采用一對(duì)退孤給init進(jìn)程的app_d進(jìn)程相互監(jiān)聽文件的方式來獲得對(duì)方進(jìn)程是否存活的狀態(tài); 而這兩個(gè)進(jìn)程都有間接由Daemon和MSF進(jìn)程所fork而來;雙重保險(xiǎn)。
          • 2)不采用系統(tǒng)框架中startService的Binder框架代碼,而是自身在Native層通過自己去查詢獲取BpActivityManager代理對(duì)象, 然后自己實(shí)現(xiàn)startService接口,并修改為ONEWAY的binder調(diào)用,既增加分析問題的難度,也進(jìn)一步隱藏自身策略;
          • 3)當(dāng)監(jiān)聽進(jìn)程死亡,則通過自身實(shí)現(xiàn)的StartService的Binder call去拉起對(duì)方進(jìn)程,系統(tǒng)對(duì)于這種方式啟動(dòng)進(jìn)程并沒有攔截機(jī)制。

          這種flock的方式至少比網(wǎng)上常說的通過循環(huán)監(jiān)聽的方式,要強(qiáng)很多。

          比往常的互保更厲害的是TIM共有6個(gè)進(jìn)程(說明:使用過程也還會(huì)創(chuàng)建一些進(jìn)程),其中4個(gè)進(jìn)程,形成兩組互動(dòng)進(jìn)程,其中一組利用Linux進(jìn)程托孤原理,可謂是隱藏得很深來互保,進(jìn)一步確保進(jìn)程永生。

          當(dāng)然,進(jìn)程收到signal信號(hào)后,如果恰巧這四個(gè)進(jìn)程在同一個(gè)時(shí)刻點(diǎn)退出,那么還是有概率會(huì)被殺。 

          不走系統(tǒng)框架代碼,自己去實(shí)現(xiàn)啟動(dòng)服務(wù)的binder call也是一大亮點(diǎn),不過還有更高級(jí)的玩法,直接封裝ioctl跟驅(qū)動(dòng)交互。之前針對(duì)這個(gè)問題,做過反保活方案,后來為了某些功能緣故又放開對(duì)這個(gè)的限制,這里就不再繼續(xù)展開了。

          附錄:有關(guān)IM/推送的進(jìn)程保活/網(wǎng)絡(luò)保活方成的文章匯總

          應(yīng)用保活終極總結(jié)(一):Android6.0以下的雙進(jìn)程守護(hù)保活實(shí)踐

          應(yīng)用保活終極總結(jié)(二):Android6.0及以上的保活實(shí)踐(進(jìn)程防殺篇)

          應(yīng)用保活終極總結(jié)(三):Android6.0及以上的保活實(shí)踐(被殺復(fù)活篇)

          Android進(jìn)程保活詳解:一篇文章解決你的所有疑問

          Android端消息推送總結(jié):實(shí)現(xiàn)原理、心跳保活、遇到的問題等

          深入的聊聊Android消息推送這件小事

          為何基于TCP協(xié)議的移動(dòng)端IM仍然需要心跳保活機(jī)制?

          微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)保活實(shí)戰(zhàn)分享(進(jìn)程保活篇)

          微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)保活實(shí)戰(zhàn)分享(網(wǎng)絡(luò)保活篇)

          移動(dòng)端IM實(shí)踐:實(shí)現(xiàn)Android版微信的智能心跳機(jī)制

          移動(dòng)端IM實(shí)踐:WhatsApp、Line、微信的心跳策略分析

          Android P正式版即將到來:后臺(tái)應(yīng)用保活、消息推送的真正噩夢(mèng)

          全面盤點(diǎn)當(dāng)前Android后臺(tái)保活方案的真實(shí)運(yùn)行效果(截止2019年前)

          一文讀懂即時(shí)通訊應(yīng)用中的網(wǎng)絡(luò)心跳包機(jī)制:作用、原理、實(shí)現(xiàn)思路等

          融云技術(shù)分享:融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路保活技術(shù)實(shí)踐

          正確理解IM長(zhǎng)連接的心跳及重連機(jī)制,并動(dòng)手實(shí)現(xiàn)(有完整IM源碼)

          2020年了,Android后臺(tái)保活還有戲嗎?看我如何優(yōu)雅的實(shí)現(xiàn)!

          史上最強(qiáng)Android保活思路:深入剖析騰訊TIM的進(jìn)程永生技術(shù)

          >> 更多同類文章 ……

          (本文同步發(fā)布于:http://www.52im.net/thread-2893-1-1.html



          作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


          只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


          網(wǎng)站導(dǎo)航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 双柏县| 醴陵市| 潍坊市| 穆棱市| 乐陵市| 仙居县| 博兴县| 长白| 白朗县| 宜丰县| 科技| 罗甸县| 仙桃市| 洛川县| 突泉县| 宜黄县| 顺昌县| 济南市| 获嘉县| 新宁县| 深圳市| 汾阳市| 双牌县| 宣武区| 钦州市| 和田县| 永泰县| 丰宁| 康定县| 安仁县| 阿克苏市| 南澳县| 安塞县| 凤山市| 西丰县| 贵港市| 祥云县| 石阡县| 大关县| 镇江市| 溧水县|