Android 系統穩定性 - ANR(二)

文章都爲原創,轉載請註明出處,未經允許而盜用者追究法律責任。
很久之前寫的了,留着有點浪費,共享之。
編寫者:李文棟

P.S. OpenOffice粘貼過來後格式有些混亂。

1.2 如何分析ANR問題

引起ANR問題的根本原因,總的來說可以歸納爲兩類:

  • 應用進程自身引起的,例如:

主線程阻塞、掛起、死循環

應用進程的其他線程的CPU佔用率高,使得主線程無法搶佔到CPU時間片

  • 其他進程間接引起的,例如:

當前應用進程進行進程間通信請求其他進程,其他進程的操作長時間沒有反饋

其他進程的CPU佔用率高,使得當前應用進程無法搶佔到CPU時間片

分析ANR問題時,以上述可能的幾種原因爲線索,通過分析各種日誌信息,大多數情況下你就可以很容易找到問題所在了。



注意:我很誠懇的向讀者說明,確實有一些ANR問題很難調查清楚,因爲整個系統不穩定的因素很多,例如Linux Kernel本身的bug引起的內存碎片過多、硬件損壞等。這類比較底層的原因引起的ANR問題往往無從查起,並且這根本不是應用程序的問題,浪費了應用開發人員很多時間,如果你從事過整個系統的開發和維護工作的話會深有體會。所以我不能保證瞭解了本章的所有內容後能夠解決一切ANR問題,如果出現了很疑難的ANR問題,我建議最好去和做驅動和內核的朋友聊聊,或者,如果問題只是個十萬分之一的偶然現象,不影響程序的正常運行,我倒是建議不去理它。

1.2.1 分析ANR的利器

Android會在ANR發生時輸出很多有用的信息幫助分析問題原因,我們先來看一下ANR的異常信息,使用logcat命令查看會得到類似如下的log

//WindowManager所在的進程是system_server,進程號是127

I/WindowManager( 127): Input event dispatching timed out sending to com.example.anrdemo/com.example.anrdemo.ANRActivity

 

//system_server進程中的ActivityManagerService請求kernel5033進程發送SIGNAL_QUIT請求

//你可以在shell中使用命令達到相同的目的:adb shell kill -3 5033

//和其他的Java虛擬機一樣,SIGNAL_QUIT也是Dalvik內部支持的功能之一

I/Process ( 127): Sending signal. PID: 5033 SIG: 3

 

//5033進程的虛擬機實例接收到SIGNAL_QUIT信號後會將進程中各個線程的函數堆棧信息輸出到traces.txt文件中

//發生ANR的進程正常情況下會第一個輸出

I/dalvikvm( 5033): threadid=4: reacting to signal 3

I/dalvikvm( 5033): Wrote stack traces to '/data/anr/traces.txt'

... ...//另外還有其他一些進程

 

//隨後會輸出CPU使用情況

E/ActivityManager( 127): ANR in com.example.anrdemo (com.example.anrdemo/.ANRActivity)

//Reason表示導致ANR問題的直接原因

E/ActivityManager( 127): Reason: keyDispatchingTimedOut

E/ActivityManager( 127): Load: 3.85 / 3.41 / 3.16

 

//請注意ago,表示ANR發生之前的一段時間內的CPU使用率,並不是某一時刻的值

E/ActivityManager( 127): CPU usage from 26835ms to 3662ms ago with 99% awake:

E/ActivityManager( 127): 9.4% 98/mediaserver: 9.4% user + 0% kernel

E/ActivityManager( 127): 8.9% 127/system_server: 6.9% user + 2% kernel / faults: 1823 minor

... ...

E/ActivityManager( 127): +0% 5033/com.example.anrdemo: 0% user + 0% kernel

E/ActivityManager( 127): 39% TOTAL: 32% user + 6.1% kernel

 

//這裏是later,表示ANR發生之後

E/ActivityManager( 127): CPU usage from 601ms to 1132ms later with 99% awake:

E/ActivityManager( 127): 10% 127/system_server: 1.7% user + 8.9% kernel / faults: 5 minor

E/ActivityManager( 127): 10% 163/InputDispatcher: 1.7% user + 8.9% kernel

E/ActivityManager( 127): 1.7% 127/system_server: 1.7% user + 0% kernel

E/ActivityManager( 127): 1.7% 135/SurfaceFlinger: 0% user + 1.7% kernel

E/ActivityManager( 127): 1.7% 2814/Binder Thread #: 1.7% user + 0% kernel

... ...

E/ActivityManager( 127): 37% TOTAL: 27% user + 9.2% kernel



log中能夠知道,發生ANRAndroid爲我們提供了兩種“利器”:traces文件和CPU使用率。以上做了簡單註釋,不過稍後再詳細分析它們。

1.2.2 ANR信息是如何輸出的

我們再來看看這些log是怎樣被輸出的,這很重要,知其然,也要知其所以然。代碼在ActivityManagerService類中,找到它的appNotResponding函數。



final void appNotResponding(ProcessRecord app, ActivityRecord activity,

ActivityRecord parent, final String annotation) {

//firstPidslastPids兩個集合存放那些將會在traces中輸出信息的進程的進程號

ArrayList<Integer> firstPids = new ArrayList<Integer>(5);

SparseArray<Boolean> lastPids = new SparseArray<Boolean>(20);

 

//mControllerIActivityController接口的實例,是爲Monkey測試程序預留的,默認爲null

if (mController != null) {

... ...

}

 

long anrTime = SystemClock.uptimeMillis();

if (MONITOR_CPU_USAGE) {

updateCpuStatsNow(); //更新CPU使用率

}

 

synchronized (this) { //一些特定條件下會忽略ANR

if (mShuttingDown) {

Slog.i(TAG, "During shutdown skipping ANR: " + app + " " + annotation);

return;

} else if (app.notResponding) {

Slog.i(TAG, "Skipping duplicate ANR: " + app + " " + annotation);

return;

} else if (app.crashing) {

Slog.i(TAG, "Crashing app skipping ANR: " + app + " " + annotation);

return;

}

 

//使用一個標誌變量避免同一個應用在沒有處理完時重複輸出log

app.notResponding = true;

... ...

 

//當前發生ANR的應用進程被第一個添加進firstPids集合中

firstPids.add(app.pid);

... ...

}

... ...

 

//dumpStackTraces是輸出traces文件的函數

File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids, null);

 

String cpuInfo = null;

if (MONITOR_CPU_USAGE) { //MONITOR_CPU_USAGE默認爲true

updateCpuStatsNow(); //再次更新CPU信息

synchronized (mProcessStatsThread) {

//輸出ANR發生前一段時間內的CPU使用率

cpuInfo = mProcessStats.printCurrentState(anrTime);

}

info.append(processStats.printCurrentLoad());

info.append(cpuInfo);

}

//輸出ANR發生後一段時間內的CPU使用率

info.append(processStats.printCurrentState(anrTime));

... ...

 

//ANR信息同時輸出到DropBox

addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,

cpuInfo, tracesFile, null);

... ...

 

//Android4.0中可以設置是否不顯示ANR提示對話框,如果設置的話就不會顯示對話框,並且會殺掉ANR進程

boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),

Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;

synchronized (this) {

if (!showBackground && !app.isInterestingToUserLocked() && app.pid != MY_PID) {

... ...

Process.killProcessQuiet(app.pid);

return;

}

... ...

 

// 顯示ANR提示對話框

Message msg = Message.obtain();

HashMap map = new HashMap();

msg.what = SHOW_NOT_RESPONDING_MSG;

msg.obj = map;

map.put("app", app);

if (activity != null) {

map.put("activity", activity);

}

mHandler.sendMessage(msg);

}

}

 

有三個關鍵點需要注意:

當前發生ANR的應用進程被第一個添加進firstPids集合中,所以會第一個向traces文件中寫入信息。反過來說,traces文件中出現的第一個進程正常情況下就是發生ANR的那個進程。不過有時候會很不湊巧,發生ANR的進程還沒有來得及輸出trace信息,就由於某種原因退出了,所以偶爾會遇到traces文件中找不到發生ANR的進程信息的情況。

dumpStackTraces是輸出traces文件的函數,接下來分析這個函數

③ addErrorToDropBox函數將ANR信息同時輸出到DropBox中,它也是個非常有用的日誌存放工具,後面也會分析它的作用。



public static File dumpStackTraces(boolean clearTraces, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//系統屬性“dalvik.vm.stack-trace-file”用來配置trace信息輸出文件

String tracesPath = SystemProperties.get("dalvik.vm.stack-trace-file", null);

if (tracesPath == null || tracesPath.length() == 0) {

return null;

}

 

File tracesFile = new File(tracesPath);

try {

File tracesDir = tracesFile.getParentFile();

if (!tracesDir.exists()) tracesFile.mkdirs();

//FileUtils.setPermissions是個很有用的函數,設置文件屬性時經常會用到

FileUtils.setPermissions(tracesDir.getPath(), 0775, -1, -1); // drwxrwxr-x

 

//clearTracestrue,會刪除舊文件,創建新文件

if (clearTraces && tracesFile.exists()) tracesFile.delete();

tracesFile.createNewFile();

FileUtils.setPermissions(tracesFile.getPath(), 0666, -1, -1); // -rw-rw-rw-

} catch (IOException e) {

Slog.w(TAG, "Unable to prepare ANR traces file: " + tracesPath, e);

return null;

}

 

//一個重載函數

dumpStackTraces(tracesPath, firstPids, processStats, lastPids, nativeProcs);

return tracesFile;

}

 



有兩個關鍵點需要注意:

聰明的你肯定已經知道,之所以trace信息會輸出到“/data/anr/traces.txt”文件中,就是系統屬性“dalvik.vm.stack-trace-file”設置的。你可以通過在設備的shell中使用setpropgetprop對系統屬性進行設置和讀取:

getpropdalvik.vm.stack-trace-file

setprop dalvik.vm.stack-trace-file /tmp/stack-traces.txt

每次發生ANR時都會刪除舊的traces文件,重新創建新文件。也就是說Android只保留最後一次發生ANR時的traces信息,那麼以前的traces信息就丟失了麼?稍後回答。

接着來看重載的dumpStackTraces函數。

 

private static void dumpStackTraces(String tracesPath, ArrayList<Integer> firstPids,

ProcessStats processStats, SparseArray<Boolean> lastPids, String[] nativeProcs) {

//使用FileObserver監聽應用進程是否已經完成寫入traces文件的操作

//Android在判斷桌面壁紙文件是否設置完成時也是用的FileObserver,很有用的類

FileObserver observer = new FileObserver(tracesPath, FileObserver.CLOSE_WRITE) {

public synchronized void onEvent(int event, String path) { notify(); }

};

... ...

//首先輸出firstPids集合中指定的進程,這些也是對ANR問題來說最重要的進程

if (firstPids != null) {

try {

int num = firstPids.size();

for (int i = 0; i < num; i++) {

synchronized (observer) {

//前面提到的SIGNAL_QUIT

Process.sendSignal(firstPids.get(i), Process.SIGNAL_QUIT);

observer.wait(200);

... ...

}





提示:如果你在解決其他問題時也需要查看Java進程中各個線程的函數堆棧信息,就可以使用向目標進程發送SIGNAL_QUIT3)這個技巧。其實這個名稱有點誤導,它並不會讓進程真正退出。



1.2.3 DropBox

剛纔留了一個問題:Android只保留最後一次發生ANR時的traces信息,那麼以前的traces信息就丟失了麼?爲了增強Android的異常信息收集管理能力,從2.2開始增加了DropBox功能。

DropBox(簡稱DB)是系統進程中的一個服務,在system_server進程啓動時創建,並且它沒有運行在單獨的線程中,而是運行在system_serverServerThread線程中。我們可以將ServerThread稱作system_server的主線程,ServerThread線程除了啓動並維護各個服務外,還負責檢測一些重要的服務是否死鎖,這一點到後面的Watchdog部分再分析<!-- Watchdog寫完後注意補充章節號 -->DB被創建的代碼如下。

SystemServer.java → ServerThread.run()

Slog.i(TAG, "DropBox Service");

ServiceManager.addService(Context.DROPBOX_SERVICE, //服務名稱爲“dropbox”

new DropBoxManagerService(context, new File("/data/system/dropbox")));

 

“/data/system/dropbox”DB指定的文件存放位置。下面來看一下DB服務的主要功能。

1. DropBoxManagerService

DropBoxManagerService(簡稱DBMS)就是DB服務的本尊,它的主要功能接口包括以下幾個函數:

public voidadd(DropBoxManager.Entryentry)

DBMS將所有要添加的日誌都用DropBoxManager.Entry類型的對象表示,通過add函數添加,並且直到目前爲止一個Entry對象對應着一個日誌文件。

publicboolean isTagEnabled(String tag)

通過給每一個Entry設置一個tag可以標識不同類型的日誌,並且可以靈活的啓用/禁用某種類型的日誌,isTagEnabled用來判斷指定類型的日誌是否被啓用/禁用了,一旦禁用就不會再記錄這種類型的日誌。默認是不禁用任何類型的日誌的。稍後說明如何啓用/禁用日誌。

publicsynchronized DropBoxManager.Entry getNextEntry(String tag, long millis)

我們可以通過getNextEntry函數獲取指定類型和指定時間點之後的第一條日誌,要使用這個功能應用程序需要有android.permission.READ_LOGS”的權限,並且在使用完畢返回的Entry對象後要調用其close函數確保關閉日誌文件的文件描述符(如果不關閉的話可能造成進程打開的文件描述符超過1024而崩潰,Android中限制每個進程的文件描述符上限爲1024

 

DBMS提供了很多的配置項用來限制對磁盤的使用,通過SettingsProvider應用程序維護,數據存放在其settings.db數據庫中。這些配置項也都有默認值,羅列如下:

  • Settings.Secure.DROPBOX_AGE_SECONDS = "dropbox_age_seconds"

日誌文件保存的最長時間,默認3

  • Settings.Secure.DROPBOX_MAX_FILES = "dropbox_max_files"

日誌文件的最大數量,默認值是1000

  • Settings.Secure.DROPBOX_QUOTA_KB = "dropbox_quota_kb"

磁盤空間最大使用量

  • Settings.Secure.DROPBOX_QUOTA_PERCENT = "dropbox_quota_percent"

 

  • Settings.Secure.DROPBOX_RESERVE_PERCENT = "dropbox_reserve_percent"

 

  • Settings.Secure.DROPBOX_TAG_PREFIX = "dropbox:"

 

//應用程序可以利用DropBox來做事情,收集日誌等

1.2.4 traces.txt

終於到大明星出場的時候了,一起來看一下traces.txt的廬山真面目。以下是筆者寫的一個演示程序製造出的一次ANRtrace信息:



//文件中輸出的第一個進程的trace信息,正是發生ANR的演示程序

//開頭顯示進程號、ANR發生的時間點和進程名稱

----- pid 9183 at 2012-09-28 22:20:42 -----

Cmd line: com.example.anrdemo

 

DALVIK THREADS: //以下是各個線程的函數堆棧信息

//mutexes表示虛擬機實例中各種線程相關對象鎖的value

(mutexes: tll=0 tsl=0 tscl=0 ghl=0 hwl=0 hwll=0)

//依次是:線程名、線程優先級、線程創建時的序號、線程當前狀態

"main" prio=5 tid=1 TIMED_WAIT

//依次是:線程組名稱、suspendCountdebugSuspendCount、線程的Java對象地址、線程的Native對象地址

| group="main" sCount=1 dsCount=0 obj=0x4025b1b8 self=0xce68

//sysTid是線程號,主線程的線程號和進程號相同

| sysTid=9183 nice=0 sched=0/0 cgrp=default handle=-1345002368

| schedstat=( 140838632 210998525 213 )

at java.lang.VMThread.sleep(Native Method)

at java.lang.Thread.sleep(Thread.java:1213)

at java.lang.Thread.sleep(Thread.java:1195)

at com.example.anrdemo.ANRActivity.makeANR(ANRActivity.java:44)

at com.example.anrdemo.ANRActivity.onClick(ANRActivity.java:38)

at android.view.View.performClick(View.java:2486)

at android.view.View$PerformClick.run(View.java:9130)

at android.os.Handler.handleCallback(Handler.java:587)

at android.os.Handler.dispatchMessage(Handler.java:92)

at android.os.Looper.loop(Looper.java:130)

at android.app.ActivityThread.main(ActivityThread.java:3703)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:507)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)

at dalvik.system.NativeStart.main(Native Method)

 

//Binder線程是進程的線程池中用來處理binder請求的線程

"Binder Thread #2" prio=5 tid=8 NATIVE

| group="main" sCount=1 dsCount=0 obj=0x40750b90 self=0x1440b8

| sysTid=9190 nice=0 sched=0/0 cgrp=default handle=1476256

| schedstat=( 915528 18463135 4 )

at dalvik.system.NativeStart.run(Native Method)

 

"Binder Thread #1" prio=5 tid=7 NATIVE

| group="main" sCount=1 dsCount=0 obj=0x4074f848 self=0x78d40

| sysTid=9189 nice=0 sched=0/0 cgrp=default handle=1308088

| schedstat=( 3509523 25543212 10 )

at dalvik.system.NativeStart.run(Native Method)

 

//線程名稱後面標識有daemon,說明這是個守護線程

"Compiler" daemon prio=5 tid=6 VMWAIT

| group="system" sCount=1 dsCount=0 obj=0x4074b928 self=0x141e78

| sysTid=9188 nice=0 sched=0/0 cgrp=default handle=1506000

| schedstat=( 21606438 21636964 101 )

at dalvik.system.NativeStart.run(Native Method)

 

//JDWP線程是支持虛擬機調試的線程,不需要關心

"JDWP" daemon prio=5 tid=5 VMWAIT

| group="system" sCount=1 dsCount=0 obj=0x4074b878 self=0x16c958

| sysTid=9187 nice=0 sched=0/0 cgrp=default handle=1510224

| schedstat=( 366211 2807617 7 )

at dalvik.system.NativeStart.run(Native Method)

//“Signal Catcher”負責接收和處理kernel發送的各種信號,例如SIGNAL_QUITSIGNAL_USR1等就是被該線程

//接收到,這個文件的內容就是由該線程負責輸出的,可以看到它的狀態是RUNNABLE,不過此線程也不需要關心

"Signal Catcher" daemon prio=5 tid=4 RUNNABLE

| group="system" sCount=0 dsCount=0 obj=0x4074b7b8 self=0x150008

| sysTid=9186 nice=0 sched=0/0 cgrp=default handle=1501664

| schedstat=( 1708985 6286621 9 )

at dalvik.system.NativeStart.run(Native Method)

 

"GC" daemon prio=5 tid=3 VMWAIT

| group="system" sCount=1 dsCount=0 obj=0x4074b710 self=0x168010

| sysTid=9185 nice=0 sched=0/0 cgrp=default handle=1503184

| schedstat=( 305176 4821778 2 )

at dalvik.system.NativeStart.run(Native Method)

 

"HeapWorker" daemon prio=5 tid=2 VMWAIT

| group="system" sCount=1 dsCount=0 obj=0x4074b658 self=0x16a080

| sysTid=9184 nice=0 sched=0/0 cgrp=default handle=550856

| schedstat=( 33691407 26336669 15 )

at dalvik.system.NativeStart.run(Native Method)

 

----- end 9183 -----

 

----- pid 127 at 2012-09-28 22:20:42 -----

Cmd line: system_server

... ...

//省略其他進程的信息

 



有一個關鍵點需要注意:

線程有很多狀態,瞭解這些狀態的意義對分析ANR的原因是有幫助的,總結如下:

 

Thread.java中定義的狀態

Thread.cpp中定義的狀態

說明

TERMINATED

ZOMBIE

線程死亡,終止運行

RUNNABLE

RUNNING/RUNNABLE

線程可運行或正在運行

TIMED_WAITING

TIMED_WAIT

執行了帶有超時參數的waitsleepjoin函數

BLOCKED

MONITOR

線程阻塞,等待獲取對象鎖

WAITING

WAIT

執行了無超時參數的wait函數

NEW

INITIALIZING

新建,正在初始化,爲其分配資源

NEW

STARTING

新建,正在啓動

RUNNABLE

NATIVE

正在執行JNI本地函數

WAITING

VMWAIT

正在等待VM資源

RUNNABLE

SUSPENDED

線程暫停,通常是由於GCdebug被暫停

 

UNKNOWN

未知狀態

 

Thread.java中的狀態和Thread.cpp中的狀態是有對應關係的。可以看到前者更加概括,也比較容易理解,面向Java的使用者;而後者更詳細,面向虛擬機內部的環境。traces.txt中顯示的線程狀態都是Thread.cpp中定義的。另外,所有的線程都是遵循POSIX標準的本地線程。關於線程更多的說明可以查閱源碼/dalvik/vm/Thread.cpp中的說明。<!-- 線程的ThreadGroup最好也寫進去 -->

traces.txt文件中的這些信息是由每個Dalvik進程的SignalCatcher線程輸出的,相關代碼可以查看/dalvik/vm/目錄下的SignalCatcher.cpp::logThreadStacks函數和Thread.cpp:: dvmDumpAllThreadsEx函數。另外請注意,輸出堆棧信息時SignalCatcher會暫停所有線程。

通過該文件很容易就能知道問題進程的主線程發生ANR時正在執行怎樣的操作。例如上述示例, ANRActivitymakeANR函數中執行線程sleep時發生ANR,可以推測sleep時間過長,超過了超時上限導致。這是一種比較簡單的情況,實際開發中會遇到很多詭異的、更加複雜的情況,在後面的實例講解一節會詳細說明。

1.2.5 CPU使用率

這部分的內容主要涉及到Linux的相關知識,數據是從/proc/stat”文件中讀取的,Android中僅僅是彙總和記錄這些數據而已,熟悉Linux的讀者可以跳過本節內容。

前面簡單說明了CPU使用率信息,我們回顧一下,這次會有更多的知識點要說明。



E/ActivityManager( 127): ANR in com.example.anrdemo (com.example.anrdemo/.ANRActivity)

E/ActivityManager( 127): Reason: keyDispatchingTimedOut

E/ActivityManager( 127): Load: 3.85 / 3.41 / 3.16 //➀ CPU平均負載

 

//ANR發生之前的一段時間內的CPU使用率

E/ActivityManager( 127): CPU usage from 26835ms to 3662ms ago with 99% awake://

E/ActivityManager( 127): 9.4% 98/mediaserver: 9.4% user + 0% kernel

E/ActivityManager( 127): 8.9% 127/system_server: 6.9% user + 2% kernel / faults: 1823 minor //⑤ minor或者major的頁錯誤次數

... ...

E/ActivityManager( 127)://+0% 5033/com.example.anrdemo: 0% user + 0% kernel

E/ActivityManager( 127): 39% TOTAL: 32% user + 6.1% kernel

 

//ANR發生之後的一段時間內的CPU使用率

E/ActivityManager( 127): CPU usage from 601ms to 1132ms later with 99% awake:

E/ActivityManager( 127): 10% 127/system_server: 1.7% user + 8.9% kernel / faults: 5 minor

E/ActivityManager( 127): 10% 163/InputDispatcher: 1.7% user + 8.9% kernel

E/ActivityManager( 127): 1.7% 127/system_server: 1.7% user + 0% kernel

E/ActivityManager( 127): 1.7% 135/SurfaceFlinger: 0% user + 1.7% kernel

E/ActivityManager( 127): 1.7% 2814/Binder Thread #: 1.7% user + 0% kernel

... ...

E/ActivityManager( 127): 37% TOTAL: 27% user + 9.2% kernel



以上信息其實包含了兩個概念:CPU負載和CPU使用率,它們是不同的。不過負載的概念主要是做大型服務器端應用時關注的性能指標,在Android開發中我們更加關注的是使用率。下面詳細說明,有八個關鍵點需要注意:

① CPU負載/平均負載

CPU負載是指某一時刻系統中運行隊列長度之和加上當前正在CPU上運行的進程數,而CPU平均負載可以理解爲一段時間內正在使用和等待使用CPU的活動進程的平均數量。在Linux中“活動進程”是指當前狀態爲運行或不可中斷阻塞的進程。通常所說的負載其實就是指平均負載。

用一個從網上看到的很生動的例子來說明(不考慮CPU時間片的限制),把設備中的一個單核CPU比作一個電話亭,把進程比作正在使用和等待使用電話的人,假如有一個人正在打電話,有三個人在排隊等待,此刻電話亭的負載就是4。使用中會不斷的有人打完電話離開,也會不斷的有其他人排隊等待,爲了得到一個有參考價值的負載值,可以規定每隔5秒記錄一下電話亭的負載,並將某一時刻之前的一分鐘、五分鐘、十五分鐘的的負載情況分別求平均值,最終就得到了三個時段的平均負載。

實際上我們通常關心的就是在某一時刻的前一分鐘、五分鐘、十五分鐘的CPU平均負載,例如以上日誌中這三個值分別是3.853.413.16,說明前一分鐘內正在使用和等待使用CPU的活動進程平均有3.85個,依此類推。在大型服務器端應用中主要關注的是第五分鐘和第十五分鐘的兩個值,但是Android主要應用在便攜手持設備中,有特殊的軟硬件環境和應用場景,短時間內的系統的較高負載就有可能造成ANR,所以筆者認爲一分鐘內的平均負載相對來說更具有參考價值。

CPU的負載和使用率沒有必然關係,有可能只有一個進程在使用CPU,但執行的是複雜的操作;也有可能等待和正在使用CPU的進程很多,但每個進程執行的都是簡單操作。

實際處理問題時偶爾會遇到由於平均負載高引起的ANR,典型的特徵就是系統中應用進程數量多,CPU總使用率較高,但是每個進程的CPU使用率不高,當前應用進程主線程沒有異常阻塞,一分鐘內的CPU平均負載較高。



提示:Linux內核不斷進行着CPU負載的記錄,我們可以在任意時刻通過在shell中執行“cat /proc/loadavg”查看。



② ANR發生之前和之後一段時間的CPU使用率

CPU使用率可以理解爲一段時間(記作T)內除CPU空閒時間(記作I)之外的時間與這段時間T的比值,用公式表示可以寫爲:

CPU使用率= (T – I) / T

而時間段T是兩個採樣時間點的時間差值。

之所以可以這樣計算,是因爲Linux內核會把從系統啓動開始到當前時刻CPU活動的所有時間信息都記錄下來,我們可以通過查看“/proc/stat”文件獲取這些信息。主要包括以下幾種時間,這些時間都是從系統啓動開始計算的,單位都是0.01秒:

userCPU在用戶態的運行時間,不包括nice值爲負數的進程運行的時間

nice CPU在用戶態並且nice值爲負數的進程運行的時間

systemCPU在內核態運行的時間

idleCPU空閒時間,不包括iowait時間

iowaitCPU等待I/O操作的時間

irqCPU硬中斷的時間

softirqCPU軟中斷的時間



注意:隨着Linux內核版本的不同,包含的時間類型有可能不同,例如2.6.11中增加的stealstolen等。但根據Android源碼,我們只需要關心以上七種類型即可。



CPU使用率的計算是在ProcessStats類中實現的。如果在某兩個時刻T1T2T1 < T2)進行採樣記錄,CPU使用率的整個算法可以歸納爲以下幾個公式:

userTime = (user2 + nice2) – (user1 + nice1)

systemTime = system2 - system1

idleTime = idle2 - idle1

iowaitTime = iowait2 - iowait1

irqTime = irq2 - irq1

softirqTime = softirq2 - softirq1

TotalTime = userTime + systemTime + idleTime + iowaitTime + irqTime + softirqTime

有了以上數據就可以計算具體的使用率了,例如用戶態CPU使用率爲:

userCpuUsage = userTime / TotalTime

依此類推可以計算其他類型的使用率。而整個時間段內CPU使用率爲:

CpuUsage = (TotalTime – idleTime) / TotalTime

以上計算的是整個系統的CPU使用率,對於指定進程的使用率是通過讀取該進程的“/proc/進程號/stat”文件計算的,而對於指定進程的指定線程的使用率是通過讀取該線程的“/proc/進程號/task/線程號/stat”文件計算的。進程和線程的CPU使用率只包含該進程或線程的總使用率、用戶態使用率和內核態使用率。

AMS在執行appNotResponding函數過程中,共輸出了兩個時間段的CPU使用率,通常情況下在ANR發生時間點之前和之後各有一段。兩段使用率都是兩次調用ProcessStats對象的update函數,每次調用update函數時會保留上一時間點的採樣數據,並記錄當前時間點的採樣數據。然後再調用ProcessStats對象的printCurrentState函數,按照上述公式歸納的算法計算CPU使用率,並輸出最終的結果。再詳細看一下代碼:

ActivityManagerService.java → appNotResponding

//第一次使用成員變量mProcessStats採樣

if (MONITOR_CPU_USAGE) {

updateCpuStatsNow();

}

... ...

 

//聲明瞭一個局部變量,參數true表示包括線程信息

final ProcessStats processStats = new ProcessStats(true);

//processStats作爲實參傳入,在dumpStackTraces中相隔500毫秒兩次調用其update函數進行採樣

File tracesFile = dumpStackTraces(true, firstPids, processStats, lastPids);

 

String cpuInfo = null;

if (MONITOR_CPU_USAGE) {

//因爲在第一次調用後,可能由於輸出trace信息等操作,中間執行了較長的時間,所以有第二次使用成員變量

//mProcessStats採樣,儘量使得采樣時間點靠後。

//此函數中要求連續兩次採樣時間間隔不少於5秒,所以一般不會執行第二次採樣。一旦執行,就會出現兩個採樣

//時間點一個在ANR發生之前,另一個在其之後,或者兩個時間點都在ANR發生之後的情況。

updateCpuStatsNow();

synchronized (mProcessStatsThread) {

//mProcessStats是成員變量,創建對象時傳入的參數是false,所以不包括線程信息

//此處先輸出ANR發生之前一段時間內的CPU使用率

cpuInfo = mProcessStats.printCurrentState(anrTime);

}

info.append(processStats.printCurrentLoad());

info.append(cpuInfo);

}

//processStats對象是在ANR發生後創建並採樣的,所以輸出的是ANR發生之後一段時間內的CPU使用率

info.append(processStats.printCurrentState(anrTime));



非睡眠時間百分比

在記CPU使用率的每個採樣時間點時使用了兩種記錄方法:SystemClock.uptimeMillis()SystemClock.elapsedRealtime(),兩者的區別就是uptimeMillis不包含睡眠時間,所以兩個採樣時間點之間的uptimeMilliselapsedRealtime之比就是非睡眠時間百分比。

頁錯誤次數

進程的CPU使用率最後輸出的“faults: xxx minor/major”部分表示的是頁錯誤次數,當次數爲0時不顯示。major是指Major Page Fault(主要頁錯誤,簡稱MPF),內核在讀取數據時會先後查找CPU的高速緩存和物理內存,如果找不到會發出一個MPF信息,請求將數據加載到內存。Minor是指Minor Page Fault(次要頁錯誤,簡稱MnPF),磁盤數據被加載到內存後,內核再次讀取時,會發出一個MnPF信息。一個文件第一次被讀寫時會有很多的MPF,被緩存到內存後再次訪問MPF就會很少,MnPF反而變多,這是內核爲減少效率低下的磁盤I/O操作採用的緩存技術的結果。

如果ANR發生時發現CPU使用率中iowait佔比很高,可以通過查看進程的major次數來推斷是哪個進程在進行磁盤I/O操作。<!-- 求證一下 -->

新增和移除的進程或線程

如果一個進程或線程的CPU使用率前有“+”,說明該進程或線程是在最後兩次CPU使用率採樣時間段內新建的;反之如果是“-”,說明該進程或線程在採樣時間段內終止了;如果是空,說明該進程或線程是在倒數第二次採樣時間點之前已經存在。



至此,所有與ANR相關的日誌內容都已介紹完畢,相信讀者以後處理ANR問題時能夠有的放矢了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章