Android 爛裝置想跑人臉辨識4 – 記憶體控場師LMKD

上一篇靠著 256MB 的 LZ4 ZRAM,我們好不容易從系統服務手裡搶回了 360MB 的安全緩衝。

但在此 7×24 小時運作的戶外門禁機上,這種平靜隨時會被打破:夏天高溫讓相機晶片漏電流暴增、訪客頻繁刷臉導致特徵快取激增、或者是某個除錯日誌沒清乾淨,記憶體水位只要一瞬間跌破警報線,Android 的終極死神就會降臨。

這個死神叫做 LMKD(Low Memory Killer Daemon)。

在桌面 Linux 系統上,當記憶體見底時,核心的 OOM Killer 會根據壞人分數(badness score)挑一個吃最多記憶體的傢伙一槍斃命。但在 Android 的世界裡,Google 嫌 OOM Killer 太粗暴且反應太慢,親手設計了一套精密的階級清洗制度。

身為技術負責人,你的唯一任務就是:把人臉辨識主程式偽裝成不可侵犯的系統特權,並在系統真正窒息前的微秒級瞬間,逼迫 LMKD 先去砍別人。


1. 殺戮階級制:oom_score_adj 的政治學

Android 系統裡的所有應用行程,都被貼上了一張名為 oom_score_adj 的評分標籤,範圍從 -1000 到 1000。數字越大,死期越近:

-1000 : SYSTEM_ADJ          (Linux 核心、init、LMKD 自身,神聖不可侵犯)
 -900 : PERSISTENT_SERVICE  (系統核心服務,如 SurfaceFlinger、Telephony)
 -800 : PERSISTENT_PROC     (系統持久化行程,掛了全機重啟)
    0 : FOREGROUND_APP      (使用者目前正在看著、點擊的前景 App)
  100 : VISIBLE_APP         (可見但不在頂層,例如被彈窗遮擋)
  200 : PERCEPTIBLE_APP     (正在背景播音樂、錄音的行程)
  900 : CACHED_APP_MIN      (背景快取行程,隨時待宰)
 1000 : CACHED_APP_MAX      (純砲灰,記憶體稍有波動第一個死)

在普通的 Android 手機上,前台 Activity 擁有最高的 oom_score_adj = 0。
但門禁機不是休閒手機。如果哪天系統跳出一個看不見的系統對話框,或者 Launcher 重啟了一下,你的主程式瞬間就會被降級成 VISIBLE_APP (100) 甚至 CACHED_APP (900),下一秒就被 LMKD 當場斃命。

我們的目標只有一個:將人臉辨識主程式的地位,直接焊死在 -800(PERSISTENT_PROC)的免死特權區。


2. 動刀 AOSP:OomAdjuster.java 源碼封神

如果你只是一個寫上層 App 的工程師,你最多只能在 Service 裡呼叫 startForeground(),把分數壓到 200,這在記憶體極限壓榨時根本不保險。

我們擁有完整的 BSP 源碼編譯權限。在 Android 10 中,Google 將評分計算邏輯從 ProcessList.java 抽離至獨立的 frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java。這裡藏著決定行程命運的關鍵代碼:

// 修改 OomAdjuster.java:在 ActivityManagerService 計算評分時直接截斷
private final int computeOomAdjLocked(ProcessRecord app, int cachedAdj, ...) {
    // 識別門禁機人臉辨識核心套件名
    if (app.info.packageName.equals("com.edge.ai.facegate")) {
        // 強行將分數鎖死在 PERSISTENT_PROC 級別,禁止任何狀態降級
        app.curAdj = ProcessList.PERSISTENT_PROC_ADJ; // -800
        app.curSchedGroup = ProcessList.SCHED_GROUP_TOP_APP;
        return app.curAdj;
    }
    // 其餘普通應用程式繼續走原生降級邏輯...
}

同時,在該 App 的 AndroidManifest.xml 中宣告:

<!-- 賦予系統核心常駐屬性 -->
<application
    android:name=".FaceGateApp"
    android:persistent="true"
    android:process=":core"
    ... >

透過這兩組外科手術,Android 系統只要開機,就會把你的主程式視同 system_server 的心腹大將。只要 Android 系統還活著,你的辨識演算法就永遠不會被殺。


3. PSI(Pressure Stall Information):微秒級的脈搏監聽

在 Android 9 以前,LMKD 依靠 Linux 核心的 vmpressure 機制(基於記憶體頁回收掃描比率)來決定何時開殺。這套機制在低階板子上極其遲鈍,常常是記憶體已經崩潰了才收到通知。

Android 10 引入了革命性的 PSI(Pressure Stall Information)。
PSI 不看剩餘記憶體有多小,它看的是**「因為硬體資源不足,導致 CPU、記憶體執行緒被卡死暫停(Stall)的時間佔比」**。

在 /dev/pressure/memory 中,核心暴露了監控介面:

some avg10=2.34 avg60=1.12 avg300=0.54 total=3456789
full avg10=0.89 avg60=0.45 avg300=0.12 total=123456
  • some:部分執行緒因為記憶體等待被卡住的時間佔比。
  • full:所有執行緒全部停擺、全機癱瘓的時間佔比。

在我們的門禁機系統裡,我們在開機腳本中調校了 LMKD 的 PSI 觸發閾值:

# /vendor/etc/init/hw/init.qcom.rc
# 當 1000ms 窗口內,全機執行緒因為記憶體卡死累積超過 70ms,立即啟動一級清理
setprop ro.lmk.psi_partial_stall_ms 70
# 當卡死超過 150ms,啟動無情斬殺,清理背景行程
setprop ro.lmk.psi_complete_stall_ms 150
# 屠殺冷卻時間設定為 250ms,防止反覆震盪
setprop ro.lmk.kill_timeout_ms 250
# 優先斬殺佔用最大記憶體的行程
setprop ro.lmk.kill_heaviest_task true

透過 PSI,系統在記憶體真正斷氣前 100 毫秒就能感應到壓力風暴,在背景悄悄把無用的快取清空,人臉辨識的幀率連抖都不會抖一下。


4. 戰場實錄與避坑心法

這套 LMKD 控場機制上線後的表現堪稱完美:

  1. 零被殺紀錄:在 72 小時極限壓測(連續 5,000 次臉部辨識、模擬網路抖動連發日誌)中,主程式 PID 從未變動過一次。
  2. 炮灰優先奉獻:當記憶體水位逼近死線時,PSI 精準觸發,LMKD 優先清理的是系統殘留的 Webview 快取與設定介面行程,為相機管線換取寶貴生存空間。
  3. 拒絕盲目設 -1000:不要直接在啟動腳本寫死 echo -1000 > /proc/$PID/oom_score_adj。一旦主程式發生不可預期的 Deadlock,-1000 會導致 Linux 核心寧可全機重開機也不殺你。鎖在 -800(PERSISTENT)是最穩健的工業邊界。

防禦工事做好,主程式拿到了免死金牌。

但別高興得太早——外面的死神不會殺你,並不代表你不會在內部「自我了斷」。下一篇,我們把手術刀切向 JVM Heap 內部:ART GC(垃圾回收器)。看看在每秒 30 幀的相機狂暴轟炸下,頻繁的物件分配是如何引發 GC Thrashing,讓你的系統自己把自己勒死。

感謝觀看,收工。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *