Android 信號處理面面觀 之 trace 文件含義(轉)


在前一章 Android 信號處理面面觀 之 信號定義、行為和來源  中,我們討論過,Android 應用在收到異常終止信號(SIGQUIT)時,沒有遵循傳統 UNIX信號模型的默認行為 (終止 + core )。而是打印出trace 文件來,以利於記錄應用異常終止的原因。 本文就重點分析 trace 文件是怎么產生的,並詳細解釋trace文件的各個字段的含義。

 

一. TRACE 文件的產生

Trace文件是 android davik 虛擬機在收到異常終止信號 (SIGQUIT)時產生的。 最經常的觸發條件是 android應用中產生了 FC (force close)。由於是該文件的產生是在 DVM里,所以只有運行 dvm實例的進程(如普通的java應用,java服務等)才會產生該文件,android 本地應用 (native app,指 運行在 android lib層,用c/c++編寫的linux應用、庫、服務等)在收到 SIGQUIT時是不會產生 trace文件的。

如上文Android 信號處理面面觀 之 信號定義、行為和來源所述,我們可以在終端通過adb發送SIGQUIT給應用來生成trace文件。

 

二. TRACE文件的實現

相關實現在以下幾個文件中:

dalvik/vm/init.h [.c]

davik/vm/SignalCatcher.h[.c]

dalvik/vm/Thread.h[.c]

Android ICS 實現文件后綴是 .cpp。

 

實現過程分以下幾步:

Step #1:  DVM初始化時,設置信號屏蔽字,屏蔽要特殊處理的信號(SIGQUIT, SIGUSR1, SIGUSR2)。由於信號處理方式是進程范圍起作用的, 這意味着該進程里所有的線程都將屏蔽該信號。 實現代碼在init.c中如下:

  1. int dvmStartup(int argc, const char* const argv[], bool ignoreUnrecognized,  
  2.     JNIEnv* pEnv)  
  3. {  
  4.     ...  
  5.     /* configure signal handling */  
  6.     if (!gDvm.reduceSignals)  
  7.         blockSignals();  
  8.     ...  
  9. }  

blockSignals()的實現很簡答,它是通過 sigprocmask() 函數調用實現的,代碼在init.c如下:
  1. /*  
  2.  * Configure signals.  We need to block SIGQUIT so that the signal only  
  3.  * reaches the dump-stack-trace thread.  
  4.  *  
  5.  * This can be disabled with the "-Xrs" flag.  
  6.  */  
  7. static void blockSignals()  
  8. {  
  9.     sigset_t mask;  
  10.     int cc;  
  11.   
  12.     sigemptyset(&mask);  
  13.     sigaddset(&mask, SIGQUIT);  
  14.     sigaddset(&mask, SIGUSR1);      // used to initiate heap dump  
  15. #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)  
  16.     sigaddset(&mask, SIGUSR2);      // used to investigate JIT internals  
  17. #endif  
  18.     //sigaddset(&mask, SIGPIPE);  
  19.     cc = sigprocmask(SIG_BLOCK, &mask, NULL);  
  20.     assert(cc == 0);  
  21. }  

Step #2: DVM 生成單獨的信號處理線程,用來對三個信號做特殊處理 (init.c):
  1. /*  
  2.  * Do non-zygote-mode initialization.  This is done during VM init for  
  3.  * standard startup, or after a "zygote fork" when creating a new process.  
  4.  */  
  5. bool dvmInitAfterZygote(void)  
  6. {  
  7.     ...  
  8.     /* start signal catcher thread that dumps stacks on SIGQUIT */  
  9.     if (!gDvm.reduceSignals && !gDvm.noQuitHandler) {  
  10.         if (!dvmSignalCatcherStartup())  
  11.             return false;  
  12.     }  
  13.     ...  
  14. }  
dvmSignalCatcherStartup() 實現在 SignalCatcher.c 中:
  1. /*  
  2.  * Crank up the signal catcher thread.  
  3.  *  
  4.  * Returns immediately.  
  5.  */  
  6. bool dvmSignalCatcherStartup(void)  
  7. {  
  8.     gDvm.haltSignalCatcher = false;  
  9.   
  10.     if (!dvmCreateInternalThread(&gDvm.signalCatcherHandle,  
  11.                 "Signal Catcher", signalCatcherThreadStart, NULL))  
  12.         return false;  
  13.   
  14.     return true;  
  15. }  

我們看到,DVM調用dvmCreateInternalThread()來生成一個新的內部線程 來專門處理dvm進程里的信號。 后面我們會看到,dvmCreateInternalThread()其實是使用pthread_create()來產生新的線程。 該線程的處理函數是 signalCatcherThreadStart()。  (dvm里所謂的 內部線程,就是用來幫助dvm實現本身使用的線程,比如 信號處理線程,binder線程,Compiler線程,JDWP線程等,而不是應用程序申請的線程。 在后面我們計划用專門的一章來討論DVM線程模式)

signalCatcherThreadStart() 實現框架如下:

  1. /*  
  2.  * Sleep in sigwait() until a signal arrives.  
  3.  */  
  4. static void* signalCatcherThreadStart(void* arg)  
  5. {  
  6.     ...  
  7.     /* set up mask with signals we want to handle */  
  8.     sigemptyset(&mask);  
  9.     sigaddset(&mask, SIGQUIT);  
  10.     sigaddset(&mask, SIGUSR1);  
  11. #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)  
  12.     sigaddset(&mask, SIGUSR2);  
  13. #endif  
  14.     ...  
  15.     while (true) {  
  16.     ...  
  17. loop:  
  18.         cc = sigwait(&mask, &rcvd);  
  19.         ...  
  20.         switch (rcvd) {  
  21.         case SIGQUIT:  
  22.             handleSigQuit();  
  23.             break;  
  24.         case SIGUSR1:  
  25.             handleSigUsr1();  
  26.             break;  
  27. #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)  
  28.         case SIGUSR2:  
  29.             handleSigUsr2();  
  30.             break;  
  31. #endif  
  32.         ...  
  33. }  
它首先設置我們要處理的信號集(SIGQUIT, SIGUSR1, SIGUSR2), 然后 調用 sigwait()。 我們知道sigwait()會在當前的線程里 重新 打開 指定的信號屏蔽字屏蔽的信號集。  在剛才的分析中,我們看到,dvm在啟動時,首先在整個進程里設置信號屏蔽字屏蔽掉三個信號,sigwait()的調用,使的這三個信號只在 SignalCatcher線程里響應。

至此我們已經能夠看到,dvm對三個信號分別所做的特殊用途:

1. SIGUSR1 被用來 做手工垃圾收集。處理函數是 HandleSigUsr1()

  1. static void handleSigUsr1(void)  
  2. {  
  3.     LOGI("SIGUSR1 forcing GC (no HPROF)\n");  
  4.     dvmCollectGarbage(false);  
  5. }  

2. SIGUSR2 被用來做 JIT的調試。如果JIT下編譯時打開,收到SIGUSR2是dvm會dump出相關的調試信息。處理邏輯如下:
  1. #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)  
  2. /*  
  3.  * Respond to a SIGUSR2 by dumping some JIT stats and possibly resetting  
  4.  * the code cache.  
  5.  */  
  6. static void handleSigUsr2(void)  
  7. {  
  8.     static int codeCacheResetCount = 0;  
  9.     if ((--codeCacheResetCount & 7) == 0) {  
  10.         gDvmJit.codeCacheFull = true;  
  11.     } else {  
  12.         dvmCompilerDumpStats();  
  13.         /* Stress-test unchain all */  
  14.         dvmJitUnchainAll();  
  15.         LOGD("Send %d more signals to rest the code cache",  
  16.              codeCacheResetCount & 7);  
  17.     }  
  18. }  
  19. #endif  

由於以上兩個信號都僅用於DVM的內部實現的調試,本文不作詳細的分析。讀者可以在終端通過adb發送 SIGUSR1 和SIGUSR2信號來觀察它的行為。

 

3.  SIGQUIT 用來 輸出trace文件,以記錄異常終止是dvm的上下文信息.

SIGQUIT的處理函數如下所示:

  1. static void handleSigQuit(void)  
  2. {   ...  
  3.     dvmSuspendAllThreads(SUSPEND_FOR_STACK_DUMP);  
  4.   
  5.     if (gDvm.stackTraceFile == NULL) {  
  6.         /* just dump to log */  
  7.         DebugOutputTarget target;  
  8.         dvmCreateLogOutputTarget(&target, ANDROID_LOG_INFO, LOG_TAG);  
  9.         dvmDumpAllThreadsEx(&target, true);  
  10.     } else {  
  11.         /* write to memory buffer */  
  12.         FILE* memfp = open_memstream(&traceBuf, &traceLen);  
  13.         if (memfp == NULL) {  
  14.             LOGE("Unable to create memstream for stack traces\n");  
  15.             traceBuf = NULL;        /* make sure it didn't touch this */  
  16.             /* continue on */  
  17.         } else {  
  18.             logThreadStacks(memfp);  
  19.             fclose(memfp);  
  20.         }  
  21.     }  
  22.   
  23. #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)  
  24.     dvmCompilerDumpStats();  
  25. #endif  
  26.   
  27.     dvmResumeAllThreads(SUSPEND_FOR_STACK_DUMP);  
  28.   
  29.     if (traceBuf != NULL) {  
  30.         int fd = open(gDvm.stackTraceFile, O_WRONLY | O_APPEND | O_CREAT, 0666);  
  31.         if (fd < 0) {  
  32.             LOGE("Unable to open stack trace file '%s': %s\n",  
  33.                 gDvm.stackTraceFile, strerror(errno));  
  34.         } else {  
  35.         ...  
  36.         }  
  37.     ...  
  38. }  

它首先查看有木有指定 trace輸出文件,沒有就將trace信息打印到log里。如果有,就先將trace信息打印到內存文件中,然后再講改內存文件內容輸出到指定trace文件中。

有些讀者肯能覺得奇怪,為什么指定了trace文件后,不直接打印trace信息到trace文件中呢。 原因是 trace文件實際上記錄的是當前運行的所有的線程的上下文信息。他需要 暫停所有的線程才能輸出。 dvmSuspendAllThreads(SUSPEND_FOR_STACK_DUMP);的調用正式這個目的。可以看出,這個操作代價是很高的,它把當前所有的線程都停了下來。執行的時間越短,對正常運行的線程的影響越小。 輸出信息到內存比直接到外部文件要快得多。所以 dvm采取了先輸出到內存,馬上恢復線程程,然后就可以慢慢的輸出到外部文件里了。

而這真正的輸出信息實現在 logThreadStacks()中:

  1. static void logThreadStacks(FILE* fp)  
  2. {  
  3.     dvmPrintDebugMessage(&target,  
  4.         "\n\n----- pid %d at %04d-%02d-%02d %02d:%02d:%02d -----\n",  
  5.         pid, ptm->tm_year + 1900, ptm->tm_mon+1, ptm->tm_mday,  
  6.         ptm->tm_hour, ptm->tm_min, ptm->tm_sec);  
  7.     printProcessName(&target);  
  8.     dvmPrintDebugMessage(&target, "\n");  
  9.     dvmDumpAllThreadsEx(&target, true);  
  10.     fprintf(fp, "----- end %d -----\n", pid);  
  11. }  
該函數打印了trace文件的框架,其輸出類似如下所示:
  1. ----- pid 503 at 2011-11-21 21:59:12 -----  
  2. Cmd line: com.android.phone  
  3.   
  4. <Thread_info>  
  5.   
  6. ----- end 503 -----  
它顯示當前dvm進程的進程id,名字,輸出的時間。最重要的所有線程的上下文信息是有函數 dvmDumpAllThreadsEx()里實現的,該函數定義在 thread.c里:
  1. void dvmDumpAllThreadsEx(const DebugOutputTarget* target, bool grabLock)  
  2. {  
  3.     Thread* thread;  
  4.   
  5.     dvmPrintDebugMessage(target, "DALVIK THREADS:\n");  
  6.   
  7. #ifdef HAVE_ANDROID_OS  
  8.     dvmPrintDebugMessage(target,  
  9.         "(mutexes: tll=%x tsl=%x tscl=%x ghl=%x hwl=%x hwll=%x)\n",  
  10.         gDvm.threadListLock.value,  
  11.         gDvm._threadSuspendLock.value,  
  12.         gDvm.threadSuspendCountLock.value,  
  13.         gDvm.gcHeapLock.value,  
  14.         gDvm.heapWorkerLock.value,  
  15.         gDvm.heapWorkerListLock.value);  
  16. #endif  
  17.   
  18.     if (grabLock)  
  19.         dvmLockThreadList(dvmThreadSelf());  
  20.   
  21.     thread = gDvm.threadList;  
  22.     while (thread != NULL) {  
  23.         dvmDumpThreadEx(target, thread, false);  
  24.   
  25.         /* verify link */  
  26.         assert(thread->next == NULL || thread->next->prev == thread);  
  27.   
  28.         thread = thread->next;  
  29.     }  
  30.   
  31.     if (grabLock)  
  32.         dvmUnlockThreadList();  
  33. }  

它的輸出格式如下:
  1. DALVIK THREADS:  
  2. (mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)  
  3. "main" prio=5 tid=1 NATIVE  
  4.   | group="main" sCount=1 dsCount=0 obj=0x400246a0 self=0x12770  
  5.   | sysTid=503 nice=0 sched=0/0 cgrp=default handle=-1342909272  
  6.   | schedstat=( 15165039025 12197235258 23068 ) utm=182 stm=1334 core=0  
  7.   at android.os.MessageQueue.nativePollOnce(Native Method)  
  8.   at android.os.MessageQueue.next(MessageQueue.java:119)  
  9.   at android.os.Looper.loop(Looper.java:122)  
  10.   at android.app.ActivityThread.main(ActivityThread.java:4134)  
  11.   at java.lang.reflect.Method.invokeNative(Native Method)  
  12.   at java.lang.reflect.Method.invoke(Method.java:491)  
  13.   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)  
  14.   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)  
  15.   at dalvik.system.NativeStart.main(Native Method)  

至此, 我們可以很清楚的 解析 trace文件中 thread信息的含義了:

1. 第一行是 固定的頭, 指明下面的都是 當前運行的 dvm thread :“DALVIK THREADS:”

2. 第二行輸出的是該 進程里各種線程互斥量的值。(具體的互斥量的作用在 dalvik 線程一章 單獨陳述)

3. 第三行輸出分別是 線程的名字(“main”),線程優先級(“prio=5”),線程id(“tid=1”) 以及線程的 類型(“NATIVE”)

4. 第四行分別是線程所述的線程組 (“main”),線程被正常掛起的次處(“sCount=1”),線程因調試而掛起次數(”dsCount=0“),當前線程所關聯的java線程對象(”obj=0x400246a0“)以及該線程本身的地址(“self=0x12770”)。

5. 第五行 顯示 線程調度信息。 分別是該線程在linux系統下得本地線程id (“ sysTid=503”),線程的調度有優先級(“nice=0”),調度策略(sched=0/0),優先組屬(“cgrp=default”)以及 處理函數地址(“handle=-1342909272”)

6 第六行 顯示更多該線程當前上下文,分別是 調度狀態(從 /proc/[pid]/task/[tid]/schedstat讀出)(“schedstat=( 15165039025 12197235258 23068 )”),以及該線程運行信息 ,它們是 線程用戶態下使用的時間值(單位是jiffies)(“utm=182”), 內核態下得調度時間值(“stm=1334”),以及最后運行改線程的cup標識(“core=0”);

7.后面幾行輸出 該線程 調用棧。

 

有了以上信息,我們便更容易分析出app是為什么被異常終止的了。我們會在單獨的一章分析, 怎樣利用trace文件里的信息尋找app異常終止的原因。敬請期待


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM