LWN: Android記憶體管理
Android memory management
ByJonathan Corbet, May 1, 2019, LSFMM
Android系統的設計核心目標就是要能夠讓使用者體驗到絲滑一般的響應速度,哪怕是CPU和記憶體很有限的場景,體驗也不能下降的太離譜。為了達成這個目標,就引入了很多的技術細節,包括定期進行low-memory process killer(低空閒記憶體時殺程序)這個其他系統下沒有的功能。在2019 Linux Storage, Filesystem, and Memory-Management Summit上,Suren Baghdasaryan介紹了Android為了讓UI互動程序(interactive processes)能有足夠的記憶體,碰到了哪些問題。
Baghdasaryan一開始就介紹了最近新加的pressure-stall information (https://lwn.net/Articles/759781/) 功能。這個功能起初不是Android開發的,不過看起來非常有效。它能讓Android runtime拿到更精確的記憶體壓力資訊,也就能用來更好的管理執行的那些程序。總的來說,Android memory management的目標是能讓interactive process儘可能的正常執行,與此同時也減少不必要的out-of-memory kill事件(也就是希望能少kill程序)。
Android的low-memory killer daemon (LMKD) 負責來實現這些功能。除了pressure-stall information(記憶體卡頓壓力資訊),近期還有其他一些功能能讓LMKD更有效。例如pidfd(LWN:給程序發signal的時候PID已經被別的程序佔用了,怎麼辦?! )就是很有用的技術,還有正在加入的功能可以輪詢這些descriptor來檢測死亡程序的也會很有用處。但是在回收記憶體的時候,還是有不少問題。例如cgroup,這個功能在其他很多地方都很有幫助,但是在LMKD裡面,卻事實上把kernel的LRU(least-recently-used,最近使用過)list給分成了很多的小list,這樣記憶體回收實際上更難了。這個session討論的核心問題就是怎樣能儘快從LMKD殺死的程序那裡把記憶體回收出來。記憶體回收(reclaim)有時候可能會耗費很長時間,甚至是無法預期什麼時候才能回收到可用記憶體,這樣LMKD就很難使用了,它甚至會很快開始繼續kill其他本不用被kill的程序。Baghdasaryan的opportunistic reclaim patches (LWN:儘快釋放被殺死的程序記憶體 )就是希望能改善這個情況。它能夠馬上把被kill的程序(目標程序)的記憶體剝奪出來,避免目標程序自己出於某些原因只在緩慢的釋放資源。快速回收記憶體就能讓LMKD更好的預測是否需要kill更多程序。
第一版的patch是基於OOM reaper程式碼的,但他認為可能不是最終版的實現方式。不過要想實現出最終版本,需要先回答好幾個問題。第一,從被kill的程序哪裡剝奪記憶體,應該在哪裡來做?一個方案是讓發出SIGKILL訊號的程序來負責(在kernel space)回收這部分記憶體。這個方案有很多好處:很簡單能實現,CPU time會算在發起kill的程序上,記憶體回收時能夠自動做好程序優先順序繼承,也能讓user-space更好的控制記憶體回收的可預期性,但是從超大程序(large processes)來回收記憶體可能存在一些問題。
第二個方案就是找一些kernel thread來做這個事情,這樣能讓API很簡單,也能做到很容易擴充套件(針對那些超大程序)。但是呢,user space就沒法控制這些記憶體回收了。
Rik van Riel注意到這裡有一個硬體配置問題,某些時候,移動裝置會執行的非常快,也擁有足夠的記憶體,這樣根本也就不需要記憶體回收。這種場景下,增加一個新的API來加速記憶體回收可能本身就是一個錯誤的行為。Michal Hocko卻認為這裡的真正問題在於被block的程序(它們因此沒法做自己的cleanup工作來釋放記憶體),而不在於這些硬體配置是否高階。Johannes Weiner提出,針對那些硬體資源有限的環境,也可以通過自動把推出的程序遷移到系統裡最快的CPU上去做。其他的資源限制就不會對這個正在退出的程序造成阻礙了,它們也就能儘快讓出資源,因此他認為這裡可能真正應該做的是調整CPU的分配。其他還有一些人擔心這個方案會導致功耗過高的問題,不過其實這種場景下一般是有interactive process正在進行互動,這樣一般都是會有一個快速的CPU(例如ARM裡的big cores)正在執行的。
Hocko回答道,這裡用到的功耗並不是一個問題,但是程序隔離可能是個問題。如果一個程序被繫結到一個slow CPU上,那麼把它搬到fast CPU上去很可能會破壞cgroup之前規定好的程序隔離的規則,進而影響到當前正在執行的interactive processes互動程序。如果真的希望讓一個正在退出的程序遷移去一個fast CPU上去做自己的清理釋放工作,那就應該是讓user space來發起這個遷移,把它移到另一個cgroup裡去,然後再kill。
Mel Gorman提出,這裡會有好幾個問題,一個是可能沒有足夠的CPU time來讓這個清理釋放工作儘快完成;另一個問題是程序空間現在已經膨脹的太大了,哪怕是最快的CPU也很難立刻把清理釋放工作即可完成。他認為最好的方案很簡單,至少是kernel這部分很簡單:正在退出的程序應該遷移到一個快速CPU上(當然前提條件是CPU mask允許它遷過去),剩下的工作就是user space該解決的了,它應該把程序事先遷移安排好,如果它真想盡快回收記憶體的話。kernel不應該做更多的事情了,否則肯定會違背某些應用場景下程序隔離的需求。
Matthew Wilcox回到最開始的方案,他覺得在發起kill的程序那裡去回收記憶體,就能繞過這些問題了。Gorman回答道,按那樣去實現記憶體回收的話肯定能減少CPU之間的inter-processor interrupt終端,因為被kill的程序的記憶體不用被兩個CPU都來touch了(那樣效能肯定會不好)。也有人擔心如果讓發起kill的程序來負責回收的話,kill()系統呼叫就可能會變成一個blocking operation(阻塞操作)了,沒人知道kill()多久之後才會返回,這樣發起kill()的程序會丈二和尚摸不著頭腦。
一個最終方案可能就是利用madvise(MADV_DONTNEED)類似的操作,來允許某個程序可以強制回收另一個程序的記憶體(至少是部分)。Gorman有點擔心這個操作可能會被利用來做惡作劇,就像常說的”程序是否能用ptrace()來強制控制目標程序?“。不過這個API可能會有個好處就是能用在多執行緒的環境裡,每個執行緒可以釋放一部分地址空間。這樣就能比較容易的把釋放記憶體的工作並行化。在討論的最後,還有個建議,每當對一個pidfd(process file descriptor)來呼叫fadvise()或者truncate()的時候,就能夠做到回收目標程序的記憶體。不過這個idea沒有更多時間展開討論了。