psi 跟Android記憶體最佳化

yooooooo發表於2024-11-29

概述

lowmemorykiller的作用就是當記憶體比較緊張的時候去及時殺掉一些對使用者來說不那麼重要的程序,回收記憶體,保證手機的正常執行。

安卓平臺lowmemorykiller機制演進可以描述為:從早期的Kernel space Lowmemorykiller 到 UserSpace Lowmemorykiller (監聽vmpressure),再到UserSpace Lowmemorykiller (監聽PSI)。

核心空間LMK

Kernel LMK相關概念

  • /sys/module/lowmemorykiller/parameters/minfree:

18432,23040,27648,32256,55296,80640

數字代表一個記憶體級別

當手機記憶體低於80640時,就去殺掉優先順序906以及以上級別的程序,當記憶體低於55296時,就去殺掉優先順序900以及以上的程序.

  • proc/pid/oom_adj: 代表當前程序的優先順序,這個優先順序是kernel中的程序優先順序,只讀檔案。
  • /proc/pid/oom_score_adj: 上層優先順序,跟ProcessList中的優先順序對應,就是讓使用者空間調節oom_score的介面。(就是上面的odj值)

AMS: 安卓系統框架層的ActivityManagerService

Lmkd: 安卓使用者態的userspace lowmemory killer daemon

Lowmemorykiller 驅動程式允許使用者空間指定一組記憶體閾值,當記憶體使用達到這些閾值時,具有不同 oom_score_adj 值的程序將被終止。

實踐應用舉例:

有時在解Android系統連續啟動app效能問題時,就要調整上面的lmk水線,即minfree和adj值。因為Android系統現在隨著多媒體這些耗記憶體資源的功能日益強大,

所以為了在有限的記憶體世界裡面,滿足app啟動效能,就不得不這樣調整lmk水線,來靈活幹掉後臺無掛緊要程序,來釋放記憶體。

AMS與lmkd透過socket通訊

主要分為三種command,每種command代表一種資料控制方式:

  • LMK_TARGET:更新/sys/module/lowmemorykiller/parameters/中的minfree以及adj
  • LMK_PROCPRIO:更新指定程序的優先順序,也就是oom_score_adj
  • LMK_PROCREMOVE:移除程序。

Lowmemorykiller init

初始化lowmemorykiller,註冊shrinker,當空閒記憶體頁面不足時, 核心程序kswpd會呼叫註冊的shrink回撥函式來回收頁面。

回收記憶體流程時會被呼叫,lowmem_scan掛到了register_shrinker裡,shrink_slab_node裡會scan_objects

呼叫棧:

• [<c082a824>] (lowmem_scan) from [<c01e0ba4>]
(shrink_slab_node+0x204/0x3d0)
• [<c01e0ba4>] (shrink_slab_node) from [<c01e12b8>]
(shrink_slab+0x70/0xe4)
• [<c01e12b8>] (shrink_slab) from [<c01e3ba8>]
(try_to_free_pages+0x3c0/0x74c)
• [<c01e3ba8>] (try_to_free_pages) from [<c01d9188>]
(__alloc_pages_nodemask+0x578/0x92c)

lowmem_scan首先看看能不能找到oom_score_adj,如果找不到就認為記憶體充足不殺程序

  • 判斷記憶體是否充足的條件就是other_free和other_file兩個都必須同時小於lowmem_minfree中的使用者設定值,other_free基本上是free pages,other_file基本上是file pages,兩者可以分別看成MemFree和Cached大小
  • 遍歷所有的程序for_each_process,查詢 oom_score_adj要比min_score_adj大且rss最大的程序,透過send_sig(SIGKILL, selected, 0)殺掉。

Issues with lowmemorykiller kernel driver

1:Hooks into slab shrinker API that was not designed for this purpose. Shrinkers are supposed to quickly drop unused caches and exit in order to avoid slowing down the vmscan process.

Workload that lowmemorykiller performs includes searching for heavy processes and killing them, which are not quick operations。

2:還有個重要的缺陷: 核心是做機制工作的,再實現一些策略工作會比較臃腫。所以儘量剝離一些本來屬於策略上的東西(比如上面的依據程序rss大小來選擇殺哪些程序),分給Android 上層程式碼去做。

因為上層程式碼離業務層很近,更能根據業務需求,來做適合Android場景的low memory killer.

ULMK ‐VMPRESSURE

這個lowmemorykiller實現架構解決了上面lowmemorykiller kernel driver實現的不足。

基本工作思路:去掉了以前核心態殺程序的策略部分實現,只監聽kernel的vmpressure events, 然後由lmkd根據程序adj以及記憶體level來決定殺哪些程序。

和以前的lmkd行為相比有個變化:

  • lmk僅根據oom_score_adj值的大小選擇殺程序,而不會考慮程序本身佔用記憶體的大小
  • native程序的oom_score_adj的值由rc檔案設定或者繼承自父程序,一般都是靜態的,不會變化. native程序的oom_score_adj都小於0,其中很多重要的系統支撐程序的oom_score_adj值為 -1000,oom killer都殺不了

lmk預設只管理oom_score_adj大於等於0的程序,所以只能殺死apk程序。

由這個 變化可以看出:lmkd殺程序的選擇策略已經跟核心慢慢脫離關係了,完全是使用者態決定了。

具體:

  1. AMS 與 Lmkd 透過soket通訊。

  2. Lmkd 與 kernel的Memcg通訊

    a. kernel 會向 lmkd 報告memory pressure event
    b. Lmkd 會根據kernel報告的critical memory pressure 或medium memorypressure 殺app
    c. 根據程序的adj以及minfree來選擇app殺掉

Lmkd 監聽vmpressure

vmpressure本身就定義了low, medium, critical三類記憶體壓力狀態,

vmpressure本身就定義了low, medium, critical三類記憶體壓力狀態,
LMKD 的初始化流程:

‐> init_mp_common(low,medium,critical)
open(MEMCG_SYSFS_PATH "memory.pressure_level", O_RDONLY | O_CLOEXEC);
evctlfd = open(MEMCG_SYSFS_PATH "cgroup.event_control", O_WRONLY | O_CLOEXEC);
write(evctlfd, buf, strlen(buf) + 1)
‐> memcg_write_event_control
event‐>register_event = vmpressure_register_event;
event‐>register_event(memcg, event‐>eventfd, buf);

Lmkd 如何處理vmpressure

對於 memory pressure 事件,處理函式是 mp_event_common,傳遞給他的 data 是memory

pressure level
Lmkd::init
‐>init_mp_common
vmpressure_hinfo[level_idx].data = level_idx;
vmpressure_hinfo[level_idx].handler = mp_event_common;
epev.data.ptr = (void *)&vmpressure_hinfo[level_idx];
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, evfd, &epev);

在未開啟use_minfree_levels的情況下,需要結合mp level以及swap 分割槽空餘大小來決定是否kill app,

kill app,
‐>!use_minfree_levels
if (mi.field.nr_free_pages < low_pressure_mem.max_nr_free_pages) {
pages_to_free = low_pressure_mem.max_nr_free_pages ‐
mi.field.nr_free_pages; }
if (level < VMPRESS_LEVEL_CRITICAL &&
mi.field.free_swap > low_pressure_mem.max_nr_free_pages)
return;
min_score_adj = level_oomadj[level];
find_and_kill_processes(min_score_adj, 0);

Issues with vmpressure for memory pressure detection

1:Reflects current reclaim efficiency rather than memory pressure level

從小米的psi工作文件裡面的Documentation/cgroup-v1/memory.txt裡面的vmpressure描述,能夠看出來這個確實是反映的是current reclaim efficiency。

2:Difficult to tune because of no direct link between reclaim efficiency and its effects on user experience。

這一點很重要,Android上的lowmemory killer的運用,主要是解決使用者操作手機卡頓,改善使用者體驗的。但是這個卡頓和記憶體回收效率並無直接關係,需要呼喚psi了。

3:Tightly coupled with vmscan implementation, changes in vmscan

mechanisms may result in behavior change

4:In testing, highly depends on the system memory size and particular

workload.

ULMK‐‐PSI

PSI improvements

  • More accurate pressure detection compared to vmpressure (2‐10x fewer false positives)

google在Android Q上已經基於PSI改進了lmkd,可以更加及時地檢測到記憶體過載和進行回收.根據google的記憶體壓力測試,新機制的檢測準確率超過上面的vmpressure的十倍。

  • Thresholds are configurable making tuning possible
  • PSI signals are rate‐limited and userspace can decide how often to poll after the first signal
  • Supports unlimited number of triggers

PSI解釋

PSI將各個任務延遲彙總為資源壓力指標,這些指標反映工作負載執行狀況和資源利用率方面的問題。

基準productivity:可以在CPU上執行任務的時間。

壓力:表示由於資源爭用而無法執行任務的時間量。

productivity的概念包括兩個部分:workload和CPU。 為了衡量壓力對兩者的影響,我們定義了兩個

資源的爭用狀態:SOME和FULL。

  • SOME: Time percentage due to the stalling of a few tasks caused by lack of a specific kind of resource.
  • FULL: Time percentage due to the stalling of all tasks caused by lack of a specific kind of resource.

Psi旨在提供可供使用者配置的低延遲的短期壓力監測機制。 在使用者定義的時間視窗內度量延遲高於使用者定義的閾值時通知使用者。

時間視窗和閾值都以usecs表示,可以同時監視具有不同閾值和窗大小的多個psi資源,PSI監測的資源包括:memory,IO以及cpu

PSI如何監控資源壓力

使用者需要註冊trigger,當資源壓力超過門限值時透過poll()通知使用者。

  • Trigger描述的是特定時間視窗內的最大累計停頓時間,例如 任何500ms視窗內100ms的總停頓時間生成喚醒事件
  • 要註冊trigger,使用者必須在proc / pressure /下開啟psi介面檔案,表示要監視的資源,並寫入所需的閾值和時間視窗。例如,將“some 150000 1000000”寫入/ proc / pressure / memory,將為在1秒時間視窗內測量的部分記憶體停頓150ms閾值。
  • PSI監視器僅在系統進入受監測的psi度量標準的卡頓狀態時啟用, 並在退出卡頓狀態時停用。

系統記錄memory stall狀態

系統透過psi_memstall_enter以及psi_memstall_leave記錄memory stall 狀態,

在下面幾個記憶體相關的檔案將對memstall 狀態進行記錄

mm/compaction.c
mm/filemap.c, mm/page_alloc.c mm/vmscan.c

Psi 更新trigger

‐>psi_memstall_enter/psi_memstall_leave
‐>psi_task_change
‐>psi_schedule_poll_work
‐>psi_poll_work
‐>collect_percpu_times
‐>update_triggers
growth = window_update(&t‐>win, now, total[t‐>state]);
if (growth < t‐>threshold)   //比較視窗時間內的stall時間是否> threshold
continue;
if (cmpxchg(&t‐>event, 0, 1) == 0)
wake_up_interruptible(&t‐>event_wait);  //產生event,喚醒等待佇列程序

通知事件

psi_fop_poll此函式是使用者態發起poll()系統呼叫後,會有psi此函式對接,檢查在此poll之前的時間內是否有事件發生,如果有則設定相應事件signal,如果無則透過poll_wait()讓其等待。所以使用者每次監聽io/mem/cpu的任一檔案,都會引發此對接函式的呼叫,根據已有的trigger判斷事件監聽情況。

Lmkd 處理PSI通知

處理函式依然是mp_event_common

mp_event_common
‐>zone_watermarks_ok
‐>file_cache_to_adj
‐>find_and_kill_process

不再使用use_minfree_levels, 透過zone 水線判斷是否需要kill process, 若需要kill則透過file_cache_to_adj來獲取adj.

Android現有ulmk+psi的不足

有個問題,psi的初衷跟我的單執行緒工作效能解析初衷是一樣的,想衡量單執行緒在cpu,mem和io這三大塊資源的等待延遲時間。
所以很好,以後我的單執行緒工作效能解析,進一步地發展就是跟psi靠攏吧。

但是psi上傳的事件資訊,很難看出來是哪個task延遲的,這樣不好吧,那還不如做成mem cgroup分組後,這樣最起碼知道不同的mem cgroup的psi值狀況。

例如知道前臺group裡面有psi記憶體壓力導致該group內的task卡頓時,就去有選擇地殺掉後臺group裡面的程序。

psi上傳資訊為什麼不帶上task的pid一點分析:

這是個需要調查的問題點,如果真帶上的話,哇,那就完美了,我的單執行緒工作效能解析以後就有救了,可以利用psi資訊,就知道解壓縮執行緒的工作效能狀況了。
不過要想做到這個,還得加上mem psi的具體卡在什麼地方的資訊(mem reclaim, mem thrashing, mem compact or others)。
這樣還不如用那個kernel裡面tools目錄下面的get_task_delay工具了。

所以最後總結:psi這樣設計上報使用者態介面資訊,是有依據的,只是上報下統計資料資訊,不想上報記憶體壓力各階段耗時,因為上報資訊多了,就會對核心效能產生干擾了

相關文章