Skip to main content

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

《殺的不是 Process,是我的幹話時間》

【你以為關掉幾個沒用的 system service 就解決了?】

Android 本來就有一套「快爆了我來殺人」的內建機制,名為:

Low-Memory Killer Daemon

我們把這幾個字分開來看, Low- Memory ,Killer,Daemon

Low-Memory 低記憶體,首先我們要定義,啥米是低記憶體,怎樣的情況叫做低記憶體,要多低才算,有沒有高效的訊號可以知道現在的狀況,如果有,那能即時監測嗎?會耗資源嗎?

Killer 殺手,殺手要殺誰,也就是要 kill process,那誰該被殺,有沒有一個明確指標,如果殺一個不夠,那要殺幾個?有沒有順序?怎麼殺?爲何要殺?

Daemon,守護神、惡魔?其實這個沒必要去深究,我只是想要分享鳥哥說的

你不必去區分什麼是 daemon 與 service !事實上,你可以將這兩者視為相同!因為達成某個服務是需要一支 daemon 在背景中運作, 沒有這支 daemon 就不會有 service !所以不需要分的太清楚啦!

上面幾個問題,都是希望得到一個效果,就是沒有記憶體的時後,我們要從哪挖出一些記憶體可以用?


一般手機怎麼處理記憶體?

簡單講:盡量不殺,背景多留點,切 App 順一點,用戶體驗比較好。

但記得我們前面提到的,任何的調教都需要基於使用情境,我們記憶體不夠,但我們不是手機,我們是一台人臉辨識機,我們只需要跑這個前台的程式,不用切換 APP,

上面這段是一個重點,但我先回到 LMKD 的第一個重點 “監控記憶體資源”,兩個要求

  1. 正確,降低誤報
  2. 即時但不吃資源

第二個重點是“決定殺誰?”

  1. 配置彈性
  2. 更適合 Android 裝置

我這邊稍微帶點歷史,因為在我過去大部分的專案都有看到這兩種相似的記憶體管理工具,甚至我在處理android 10的時候還有看到,讓我非常混亂,最早我以為是同一時間只會有一種,但其實常常有併用的情況

  1. In-kernel Low Memory Killer
  2. Userspace LMKd(Low Memory Killer daemon)

眾所週知,Android base on Linux,那這種記憶體不足的問題,怎麼可能Linux沒有專門處理的單位,甚至名稱相當直白

  • 0. OOM Killer(Out-of-Memory Killer)

那為何 android 卻不繼續沿用呢?

簡單來說 手機等 Edge device 通常記憶體都相對少很多,更何況早期甚至都不到 512 mb ,而 OOM Killer 的監控記憶體資源是直接根據甚於多少可用記憶體,啟動,這樣的方式會讓低記憶體裝置來不及反應,基本上就會進入崩潰了,這個基本上就不符合第二點 『即時』,加上對於要 kill 的對象配置上也不夠彈性,是採用通用的 oom_score ,這個分數是考慮系統穩定度為主的,並非根據使用者體驗。

https://blog.csdn.net/u010278923/article/details/105688107
oom_score = (RSS + Page tables + Swap)/ physical + Swap

基本上誰吃得得多就殺誰

前傳終於結束了,OOM killer 明顯的不足, Android 最早就設計出了 In-kernel LMK,首先想改善的就是 oom_score 因此提出  oom_adj_score 值較低的程式具有較高的優先順序,並且不太可能被終止,並提出了前景、可見、服務、背景、空 五種情況更適合 Edge device 的使用這情境,在配置上也提出 minfree 方便修改優先級

但是這個版本還是有許多先天限制

  1. 低 RAM 裝置的效能問題
    • 即使在低記憶體裝置上進行積極的 LMK 設定調整,當處理包含大型檔案支援的有效頁面快取的工作負載時,系統仍可能表現不佳,導致效能下降且無法有效終止進程。​(不符合即時且準確要件,而且搞超久)
  2. 設計僵硬導致客製化需求
    • 由於 LMK 的設計較為僵硬,裝置製造商通常需要自訂驅動程式以適應特定的硬體和軟體配置,增加了開發和維護的複雜性。​(不符合彈性配置)
  3. 與 Slab Shrinker API 的整合效率低下
    • LMK 驅動程式連結至 Slab Shrinker API,但該 API 並非設計用於執行如搜尋和終止進程等重度作業,這可能導致 vmscan 程序的速度變慢,影響整體系統效能。(不符合即時且準確要件)

好啦,我扯到這邊也是有點累了,In-kernel LMK 比起OOM killer有明顯的進步,但還是不夠,不符合上面提到的重點 1. 正確且即時(vmscan 搭配 slab shrinker 不夠力),以及不夠彈性的配置(因為還是依賴幾乎寫死的kernel driver)

Userspace LMKD 登場:策略與機制分家

既然 Kernel 笨又難調,那我們就讓 User Space 來決定策略,Kernel 專心執行指令。這就是 Userspace LMKD 的誕生。

但等等,要讓 User Space 掌控狀況,總要有辦法得知「現在壓力多大」吧?

LMKD 對此又分成兩個階段,都符合 User space 能讀取到的這個條件

  1. Vmpressure 是 Linux kernel 提供的一種 內部通知機制,讓用戶空間(userspace)能即時知道目前記憶體是否「吃緊」,但它不是透過觀察「free memory」這種靜態指標,而是透過觀察「kernel 在 reclaim 頁面時發出的 event」來推估。
  • PSI(Pressure Stall Information)透過追蹤任務因資源不足(psi_memstall_enterpsi_memstall_leave 函數記錄的記憶體停滯時間最初以每個 CPU 的計數器形式維護 )而被延遲的時間,提供了更準確的壓力指標。這些延遲直接影響使用者體驗,因此 PSI 能夠更真實地反映系統的壓力狀況。
  • 提供了 somefull 兩種壓力指標:
    • some:表示至少有一個任務因資源不足而被延遲的時間比例。
    • full:表示所有非閒置任務同時因資源不足而被延遲的時間比例

各位有沒有感覺明顯 Vmpressure 不夠精準,有點事後諸葛的方式來反推記憶體壓力,肯定會有延遲,卻是早期 android 先導入的對象,沒錯,我也覺的很奇怪,但原因好像蠻簡單的

就是

vmpressure 是從 Linux kernel 3.14 就開始有的

PSI(Pressure Stall Information)則是 kernel 4.20 才被引入

對當時的使用場景也已足夠

  • 早期 Android 追求的是多工體驗(切 app、快回應),因此只要知道「記憶體壓力來了」就夠用。
  • vmpressure 能在 page reclaim 無效時發出「快爆了」的通知,夠應付早期「殺後台 app 保前台 smooth」的需求。

到了這邊,是不是覺得講的太多,整個故事線都提完了

記住 我們是希望打造出適合我們的 AI 人臉辨識裝置,而不是 Android 裝置所追求的 快速切換,絲滑操作唯一重要的「前景應用程式」就是您開發的人臉辨識程式。其他所有系統服務、背景程序,只要不是維持人臉辨識核心功能所必需的,都應該被視為可犧牲的資源。我們的 LMKD 調校目標是:

  • 確保人臉辨識應用程式(作為前景程序)在任何情況下都不會被 LMKD 終止。
  • 在記憶體壓力升高時,LMKD 應優先且積極地終止與人臉辨識無關的系統服務或背景程序。
  • 利用 PSI 提供的精確記憶體壓力資訊,避免不必要的終止,同時確保在必要時能及時介入。

# Set device as low-RAM
ro.config.low_ram=true

# Use PSI for memory pressure detection (requires kernel support)
ro.lmk.use_psi=true
ro.lmk.use_minfree_levels=false

# Adjust PSI thresholds (start with defaults for low-RAM, may need tuning)
ro.lmk.psi_partial_stall_ms=200
ro.lmk.psi_complete_stall_ms=700
這些數字可能需要降低,但我確實最後定調在這兩數字,越低的話,可以讓 LMKD 提早開始處理記憶體不足的問題,但是太低可能造成系統不段開關 process ,反而更糟

# Adjust oom_adj thresholds for killing (protect foreground, aggressively kill others)
# Target cached apps (oom_adj 900-1000) at low pressure 預設值 1001(禁用)表示在低壓力下不終止任何程序
ro.lmk.low=900
# Target services (oom_adj 500-800) at medium pressure
ro.lmk.medium=500
# Critical pressure allows killing any process (oom_adj 0), last resort
ro.lmk.critical=0
前景應用程式的 oom_adj 是 0,這是最後一道防線。保持為 0 是必要的,但我們的目標是透過積極終止高 oom_adj 程序來避免達到這個狀態。

# Kill the heaviest eligible task to free memory quickly
ro.lmk.kill_heaviest_task=true

除了這個部分,還有一個地方 Framework 層可以下手看看 ActivityManagerServiceActivityManagerConstants

frameworks_base_services/java/com/android/server/am
/ProcessList.java
CUR_TRIM_EMPTY_PROCESSES
 = computeTrimEmptyApps(rawMaxEmptyProcesses);
CUR_TRIM_CACHED_PROCESSES =
               computeTrimCachedApps(rawMaxEmptyProcesses, MAX_CACHED_PROCESSES);
MAX_CACHED_PROCESSES : 32->3

/**
    * Return the maximum pss size in kb that we consider a process acceptable to
    * restore from its cached state for running in the background when RAM is low.
    */
   long getCachedRestoreThresholdKb() {
       // heaton
       Slog.w(TAG, "getCachedRestoreThresholdKb "+mCachedRestoreLevel);
       return mCachedRestoreLevel/10;
   }
保留1/10 的一班cache memory.

MAX_CACHED_PROCESSES 限制系統在記憶體充足時最多保留多少快取程序的數量 ,不需要保留大量快取程序來實現快速多工切換

getCachedRestoreThresholdKb 降低為原本的1/10會使得記憶體使用量相對較高的快取程序更容易被系統清理

這兩個參數實際上使用起來非常有效,很快就可以看到數據的變化,但是直接調整這些數據有可能會跟 low mem的部分參數起到衝突,以及明顯沒有 cache process 可能帶來的問題,也就是當真的有用到的 process 可能因為激烈的 kill 及 重新啟動造成 CPU loading 提高,例如,某些看似不重要的背景服務可能實際上為相機串流或 AI 推理提供底層支援。所以為何前面的章節會先要求 學會 “評估

這些調校都需要在您的裝置上進行仔細的測試和監控,以確保在積極釋放記憶體的同時,不會影響到核心的人臉辨識應用程式的穩定運行。

多個小故事,針對 LMKD,結論來說我們會希望是 Userspace LMKD + PSI .當初使用 Android 10 經過一堆調整後,還是發現過一些非預期的情況,就是不知道為何刪掉預想外的 background , 我一直以為是 LMKD 搞錯參數,後來看者看者 Log 才發現 lowmemorykiller 怎麼也在運行,這才突破我固定的思考模式,原來是有可能一套系統中有兩套記憶體管理機制在運行,極有可能當時在porting 時沒有注意到

adb shell ps -A | grep lmkd 
輸出中包含 lmkd 相關的行,表示 LMKD 正在運行
adb shell getprop ro.lmk.use_psi 
true,表示 LMKD 被配置為使用 PSI 進行記憶體壓力偵測 。
false 或沒有輸出,則可能表示使用 vmpressure 或傳統模式。

adb shell logcat | grep lowmemorykiller
adb shell logcat | grep lmkd
Using psi monitors for memory pressure detection :這明確表示 LMKD 正在使用 PSI。 
Using vmpressure for memory pressure detection :表示使用較舊的 vmpressure 機制。  
adb shell ls /proc/pressure/
如果輸出包含 cpu, io, memory 等檔案,表示核心支援 PSI.

底下這段設置可以試試看,確保我們要使用的 lmkd 模式
CONFIG_ANDROID_LOW_MEMORY_KILLER=n 禁用傳統的核心內建低記憶體終止器 (in-kernel LMK) 驅動程式
CONFIG_MEMCG=y 啟用了 Linux 核心的記憶體控制群組 (Memory Cgroups) 功能
CONFIG_MEMCG_SWAP=y 啟用了記憶體 cgroup 的交換 (Swap) 帳戶功能

最後展示一下結果

before

Total RAM: 1,909,104K (status normal)
 Free RAM:   456,339K (  140,995K cached pss +   251,112K cached kernel +    64,232K free)
 Used RAM: 1,503,496K (1,301,496K used pss +   202,000K kernel)
 Lost RAM:   125,250K
     ZRAM:    44,240K physical used for   225,168K in swap (  524,284K total swap)
      KSM:    26,640K saved from shared       828K              20,964K unshared;     6,096K


final

Total RAM: 1,909,104K (status normal)
 Free RAM: 1,135,724K (   56,072K cached pss +   609,924K cached kernel +   469,728K free)
 Used RAM:   804,931K (  641,123K used pss +   163,808K kernel)
 Lost RAM:   137,107K
     ZRAM:    39,672K physical used for   212,468K in swap (  262,140K total swap)
      KSM:     2,872K saved from shared        72K              14,204K unshared;       572K volatile