Android LowMemoryKiller原理分析
frameworks/base/services/core/java/com/android/server/am/ProcessList.java platform/system/core/lmkd/lmkd.c kernel/common/drivers/staging/Android/lowmemorykiller.c
一. 概述
Android的設計理念之一,便是應用程序退出,但進程還會繼續存在系統以便再次啟動時提高響應時間. 這樣的設計會帶來一個問題, 每個進程都有自己獨立的內存地址空間,隨著應用打開數量的增多,系統已使用的內存越來越大,就很有可能導致系統內存不足, 那么需要一個能管理所有進程,根據一定策略來釋放進程的策略,這便有了 lmk ,全稱為LowMemoryKiller(低內存殺手),lmkd來決定什么時間殺掉什么進程.
Android基于Linux的系統,其實Linux有類似的內存管理策略——OOM killer,全稱(Out Of Memory Killer), OOM的策略更多的是用于分配內存不足時觸發,將得分最高的進程殺掉。而 lmk 則會每隔一段時間檢查一次,當系統剩余可用內存較低時,便會觸發殺進程的策略,根據不同的剩余內存檔位來來選擇殺不同優先級的進程,而不是等到OOM時再來殺進程,真正OOM時系統可能已經處于異常狀態,系統更希望的是未雨綢繆,在內存很低時來殺掉一些優先級較低的進程來保障后續操作的順利進行。
二. framework層
位于 ProcessList.java 中定義了3種命令類型,這些文件的定義必須跟 lmkd.c 定義完全一致,格式分別如下:
LMK_TARGET <minfree> <minkillprio> ... (up to 6 pairs) LMK_PROCPRIO <pid> <prio> LMK_PROCREMOVE <pid>
| 功能 | 命令 | 對應方法 | 觸發時機 |
|---|---|---|---|
| 更新oom_adj | LMK_TARGET | updateOomLevels | AMS.updateConfiguration |
| 設置進程adj | LMK_PROCPRIO | setOomAdj | AMS.applyOomAdjLocked |
| 移除進程 | LMK_PROCREMOVE | remove | AMS.handleAppDiedLocked/cleanUpApplicationRecordLocked |
在前面文章 Android進程調度之adj算法 中有講到 AMS.applyOomAdjLocked ,接下來以這個過程為主線開始分析。
2.1 AMS.applyOomAdjLocked
private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now,
long nowElapsed) {
...
if (app.curAdj != app.setAdj) {
//【見小節2.2】
ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
app.setAdj = app.curAdj;
}
...
}
2.2 PL.setOomAdj
public static final void setOomAdj(int pid, int uid, int amt) {
//當adj=16,則直接返回
if (amt == UNKNOWN_ADJ)
return;
long start = SystemClock.elapsedRealtime();
ByteBuffer buf = ByteBuffer.allocate(4 * 4);
buf.putInt(LMK_PROCPRIO);
buf.putInt(pid);
buf.putInt(uid);
buf.putInt(amt);
//將16Byte字節寫入socket【見小節2.3】
writeLmkd(buf);
long now = SystemClock.elapsedRealtime();
if ((now-start) > 250) {
Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid
+ " = " + amt);
}
}</pre>
buf大小為16個字節,依次寫入LMK_PROCPRIO(命令類型), pid(進程pid), uid(進程uid), amt(目標adj),將這些字節通過socket發送給lmkd.
2.3 PL.writeLmkd
private static void writeLmkd(ByteBuffer buf) {
//當socket打開失敗會嘗試3次
for (int i = 0; i < 3; i++) {
if (sLmkdSocket == null) {
//打開socket 【見小節2.4】
if (openLmkdSocket() == false) {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
}
continue;
}
}
try {
//將buf信息寫入lmkd socket
sLmkdOutputStream.write(buf.array(), 0, buf.position());
return;
} catch (IOException ex) {
try {
sLmkdSocket.close();
} catch (IOException ex2) {
}
sLmkdSocket = null;
}
}
}
- 當sLmkdSocket為空,并且打開失敗,重新執行該操作;
- 當sLmkdOutputStream寫入buf信息失敗,則會關閉sLmkdSocket,重新執行該操作;
這個重新執行操作最多3次,如果3次后還失敗,則writeLmkd操作會直接結束。嘗試3次,則不管結果如何都將退出該操作,可見writeLmkd寫入操作還有可能失敗的。
2.4 PL.openLmkdSocket
private static boolean openLmkdSocket() {
try {
sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
//與遠程lmkd守護進程建立socket連接
sLmkdSocket.connect(
new LocalSocketAddress("lmkd",
LocalSocketAddress.Namespace.RESERVED));
sLmkdOutputStream = sLmkdSocket.getOutputStream();
} catch (IOException ex) {
Slog.w(TAG, "lowmemorykiller daemon socket open failed");
sLmkdSocket = null;
return false;
}
return true;
}
sLmkdSocket采用的是SOCK_SEQPACKET,這是類型的socket能提供順序確定的,可靠的,雙向基于連接的socket endpoint,與類型SOCK_STREAM很相似,唯一不同的是SEQPACKET保留消息的邊界,而SOCK_STREAM是基于字節流,并不會記錄邊界。
舉例:本地通過write()系統調用向遠程先后發送兩組數據:一組4字節,一組8字節;對于SOCK_SEQPACKET類型通過read()能獲知這是兩組數據以及大小,而對于SOCK_STREAM類型,通過read()一次性讀取到12個字節,并不知道數據包的邊界情況。
常見的數據類型還有SOCK_DGRAM,提供數據報形式,用于udp這樣不可靠的通信過程。
再回到openLmkdSocket()方法,該方法是打開一個名為 lmkd 的socket,類型為LocalSocket.SOCKET_SEQPACKET,這只是一個封裝,真實類型就是SOCK_SEQPACKET。先跟遠程lmkd守護進程建立連接,再向其通過write()將數據寫入該socket,再接下來進入lmkd過程。
三. lmkd
lmkd是由init進程,通過解析init.rc文件來啟動的lmkd守護進程,lmkd會創建名為 lmkd 的socket,節點位于 /dev/socket/lmkd ,該socket用于跟上層framework交互。
service lmkd /system/bin/lmkd
class core
critical
socket lmkd seqpacket 0660 system system
writepid /dev/cpuset/system-background/tasks
lmkd啟動后,接下里的操作都在 platform/system/core/lmkd/lmkd.c 文件,首先進入main()方法
3.1 main
int main(int argc __unused, char **argv __unused) {
struct sched_param param = {
.sched_priority = 1,
};
mlockall(MCL_FUTURE);
sched_setscheduler(0, SCHED_FIFO, ¶m);
//初始化【見小節3.2】
if (!init())
mainloop(); //成功后進入loop [見小節3.3]
ALOGI("exiting");
return 0;
}
3.2 init
static int init(void) {
struct epoll_event epev;
int i;
int ret;
page_k = sysconf(_SC_PAGESIZE);
if (page_k == -1)
page_k = PAGE_SIZE;
page_k /= 1024;
//創建epoll監聽文件句柄
epollfd = epoll_create(MAX_EPOLL_EVENTS);
//獲取lmkd控制描述符
ctrl_lfd = android_get_control_socket("lmkd");
//監聽lmkd socket
ret = listen(ctrl_lfd, 1);
epev.events = EPOLLIN;
epev.data.ptr = (void *)ctrl_connect_handler;
//將文件句柄ctrl_lfd,加入epoll句柄
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_lfd, &epev) == -1) {
return -1;
}
maxevents++;
//該路徑是否具有可寫的權限
use_inkernel_interface = !access(INKERNEL_MINFREE_PATH, W_OK);
if (use_inkernel_interface) {
ALOGI("Using in-kernel low memory killer interface");
} else {
ret = init_mp(MEMPRESSURE_WATCH_LEVEL, (void *)&mp_event);
if (ret)
ALOGE("Kernel does not support memory pressure events or in-kernel low memory killer");
}
for (i = 0; i <= ADJTOSLOT(OOM_SCORE_ADJ_MAX); i++) {
procadjslot_list[i].next = &procadjslot_list[i];
procadjslot_list[i].prev = &procadjslot_list[i];
}
return 0;
}</pre>
這里,通過檢驗/sys/module/lowmemorykiller/parameters/minfree節點是否具有可寫權限來判斷是否使用kernel接口來管理lmk事件。默認該節點是具有系統可寫的權限,也就意味著 use_inkernel_interface =1.
3.3 mainloop
static void mainloop(void) {
while (1) {
struct epoll_event events[maxevents];
int nevents;
int i;
ctrl_dfd_reopened = 0;
//等待epollfd上的事件
nevents = epoll_wait(epollfd, events, maxevents, -1);
if (nevents == -1) {
if (errno == EINTR)
continue;
continue;
}
for (i = 0; i < nevents; ++i) {
if (events[i].events & EPOLLERR)
ALOGD("EPOLLERR on event #%d", i);
// 當事件到來,則調用ctrl_connect_handler方法 【見小節3.4】
if (events[i].data.ptr)
(*(void (*)(uint32_t))events[i].data.ptr)(events[i].events);
}
}
}</pre>
主循環調用epoll_wait(),等待epollfd上的事件,當接收到中斷或者不存在事件,則執行continue操作。當事件到來,則 調用的ctrl_connect_handler方法,該方法是由init()過程中設定的方法。
3.4 ctrl_connect_handler
static void ctrl_connect_handler(uint32_t events __unused) {
struct epoll_event epev;
if (ctrl_dfd >= 0) {
ctrl_data_close();
ctrl_dfd_reopened = 1;
}
ctrl_dfd = accept(ctrl_lfd, NULL, NULL);
if (ctrl_dfd < 0) {
ALOGE("lmkd control socket accept failed; errno=%d", errno);
return;
}
ALOGI("ActivityManager connected");
maxevents++;
epev.events = EPOLLIN;
epev.data.ptr = (void *)ctrl_data_handler;
//將ctrl_lfd添加到epollfd
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_dfd, &epev) == -1) {
ALOGE("epoll_ctl for data connection socket failed; errno=%d", errno);
ctrl_data_close();
return;
}
}</pre>
當事件觸發,則調用ctrl_data_handler
3.5 ctrl_data_handler
static void ctrl_data_handler(uint32_t events) {
if (events & EPOLLHUP) {
//ActivityManager 連接已斷開
if (!ctrl_dfd_reopened)
ctrl_data_close();
} else if (events & EPOLLIN) {
//[見小節3.6]
ctrl_command_handler();
}
}
3.6 ctrl_command_handler
static void ctrl_command_handler(void) {
int ibuf[CTRL_PACKET_MAX / sizeof(int)];
int len;
int cmd = -1;
int nargs;
int targets;
len = ctrl_data_read((char *)ibuf, CTRL_PACKET_MAX);
if (len <= 0)
return;
nargs = len / sizeof(int) - 1;
if (nargs < 0)
goto wronglen;
//將網絡字節順序轉換為主機字節順序
cmd = ntohl(ibuf[0]);
switch(cmd) {
case LMK_TARGET:
targets = nargs / 2;
if (nargs & 0x1 || targets > (int)ARRAY_SIZE(lowmem_adj))
goto wronglen;
cmd_target(targets, &ibuf[1]);
break;
case LMK_PROCPRIO:
if (nargs != 3)
goto wronglen;
//設置進程adj【見小節3.7】
cmd_procprio(ntohl(ibuf[1]), ntohl(ibuf[2]), ntohl(ibuf[3]));
break;
case LMK_PROCREMOVE:
if (nargs != 1)
goto wronglen;
cmd_procremove(ntohl(ibuf[1]));
break;
default:
ALOGE("Received unknown command code %d", cmd);
return;
}
return;
wronglen:
ALOGE("Wrong control socket read length cmd=%d len=%d", cmd, len);
}
CTRL_PACKET_MAX 大小等于 (sizeof(int) * (MAX_TARGETS * 2 + 1));而MAX_TARGETS=6,對于sizeof(int)=4的系統,則 CTRL_PACKET_MAX =52。 獲取framework傳遞過來的buf數據后,根據3種不同的命令,進入不同的分支。 接下來,繼續以前面傳遞過來的 LMK_PROCPRIO 命令來往下講解,進入 cmd_procprio 過程。
3.7 cmd_procprio
static void cmd_procprio(int pid, int uid, int oomadj) {
struct proc *procp;
char path[80];
char val[20];
...
snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
snprintf(val, sizeof(val), "%d", oomadj);
//向節點/proc/<pid>/oom_score_adj寫入oomadj
writefilestring(path, val);
//當使用kernel方式則直接返回
if (use_inkernel_interface)
return;
procp = pid_lookup(pid);
if (!procp) {
procp = malloc(sizeof(struct proc));
if (!procp) {
// Oh, the irony. May need to rebuild our state.
return;
}
procp->pid = pid;
procp->uid = uid;
procp->oomadj = oomadj;
proc_insert(procp);
} else {
proc_unslot(procp);
procp->oomadj = oomadj;
proc_slot(procp);
}
}</pre>
向節點``/proc/ /oom_score_adj`寫入oomadj。由于use_inkernel_interface=1,那么再接下里需要看看kernel的情況
3.8 小節
use_inkernel_interface該值后續應該會逐漸采用用戶空間策略。不過目前仍為 use_inkernel_interface=1則有:
- LMK_PROCPRIO: 向 /proc/<pid>/oom_score_adj 寫入oomadj,則直接返回;
- LMK_PROCREMOVE:不做任何事,直接返回;
- LMK_TARGET:分別向 /sys/module/lowmemorykiller/parameters 目錄下的 minfree 和 adj 節點寫入相應信息;
四. Kernel層
lowmemorykiller driver位于 drivers/staging/Android/lowmemorykiller.c
4.1 lowmemorykiller初始化
static struct shrinker lowmem_shrinker = {
.scan_objects = lowmem_scan,
.count_objects = lowmem_count,
.seeks = DEFAULT_SEEKS * 16
};
static int __init lowmem_init(void)
{
register_shrinker(&lowmem_shrinker);
return 0;
}
static void __exit lowmem_exit(void)
{
unregister_shrinker(&lowmem_shrinker);
}
module_init(lowmem_init);
module_exit(lowmem_exit);</pre>
通過register_shrinker和unregister_shrinker分別用于初始化和退出。
4.2 shrinker
LMK驅動通過注冊shrinker來實現的,shrinker是linux kernel標準的回收內存page的機制,由內核線程kswapd負責監控。
當內存不足時kswapd線程會遍歷一張shrinker鏈表,并回調已注冊的shrinker函數來回收內存page,kswapd還會周期性喚醒來執行內存操作。每個zone維護active_list和inactive_list鏈表,內核根據頁面活動狀態將page在這兩個鏈表之間移動,最終通過shrink_slab和shrink_zone來回收內存頁,有興趣想進一步了解linux內存回收機制,可自行研究,這里再回到LowMemoryKiller的過程分析。
4.3 lowmem_count
static unsigned long lowmem_count(struct shrinker *s,
struct shrink_control *sc)
{
return global_page_state(NR_ACTIVE_ANON) +
global_page_state(NR_ACTIVE_FILE) +
global_page_state(NR_INACTIVE_ANON) +
global_page_state(NR_INACTIVE_FILE);
}
ANON代表匿名映射,沒有后備存儲器;FILE代表文件映射; 內存計算公式= 活動匿名內存 + 活動文件內存 + 不活動匿名內存 + 不活動文件內存
4.4 lowmem_scan
當觸發lmkd,則先殺oom_adj最大的進程, 當oom_adj相等時,則選擇oom_score_adj最大的進程.
static unsigned long lowmem_scan(struct shrinker s, struct shrink_control sc)
{
struct task_struct tsk;
struct task_struct selected = NULL;
unsigned long rem = 0;
int tasksize;
int i;
short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
int minfree = 0;
int selected_tasksize = 0;
short selected_oom_score_adj;
int array_size = ARRAY_SIZE(lowmem_adj);
//獲取當前剩余內存大小
int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
int other_file = global_page_state(NR_FILE_PAGES) -
global_page_state(NR_SHMEM) -
total_swapcache_pages();
//獲取數組大小
if (lowmem_adj_size < array_size)
array_size = lowmem_adj_size;
if (lowmem_minfree_size < array_size)
array_size = lowmem_minfree_size;
//遍歷lowmem_minfree數組找出相應的最小adj值
for (i = 0; i < array_size; i++) {
minfree = lowmem_minfree[i];
if (other_free < minfree && other_file < minfree) {
min_score_adj = lowmem_adj[i];
break;
}
}
if (min_score_adj == OOM_SCORE_ADJ_MAX + 1) {
return 0;
}
selected_oom_score_adj = min_score_adj;
rcu_read_lock();
for_each_process(tsk) {
struct task_struct *p;
short oom_score_adj;
if (tsk->flags & PF_KTHREAD)
continue;
p = find_lock_task_mm(tsk);
if (!p)
continue;
if (test_tsk_thread_flag(p, TIF_MEMDIE) &&
time_before_eq(jiffies, lowmem_deathpending_timeout)) {
task_unlock(p);
rcu_read_unlock();
return 0;
}
oom_score_adj = p->signal->oom_score_adj;
//小于目標adj的進程,則忽略
if (oom_score_adj < min_score_adj) {
task_unlock(p);
continue;
}
//獲取的是進程的Resident Set Size,也就是進程獨占內存 + 共享庫大小。
tasksize = get_mm_rss(p->mm);
task_unlock(p);
if (tasksize <= 0)
continue;
//算法關鍵,選擇oom_score_adj最大的進程中,并且rss內存最大的進程.
if (selected) {
if (oom_score_adj < selected_oom_score_adj)
continue;
if (oom_score_adj == selected_oom_score_adj &&
tasksize <= selected_tasksize)
continue;
}
selected = p;
selected_tasksize = tasksize;
selected_oom_score_adj = oom_score_adj;
lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n",
p->comm, p->pid, oom_score_adj, tasksize);
}
if (selected) {
long cache_size = other_file * (long)(PAGE_SIZE / 1024);
long cache_limit = minfree * (long)(PAGE_SIZE / 1024);
long free = other_free * (long)(PAGE_SIZE / 1024);
lowmem_deathpending_timeout = jiffies + HZ;
set_tsk_thread_flag(selected, TIF_MEMDIE);
//向選中的目標進程發送signal 9來殺掉目標進程
send_sig(SIGKILL, selected, 0);
rem += selected_tasksize;
}
rcu_read_unlock();
return rem;
}</pre>
- 選擇oom_score_adj最大的進程中,并且rss內存最大的進程作為選中要殺的進程。
- 殺進程方式: send_sig(SIGKILL, selected, 0) `向選中的目標進程發送signal 9來殺掉目標進程。
另外,lowmem_minfree[]和lowmem_adj[]數組大小個數為6,通過如下兩條命令:
module_param_named(debug_level, lowmem_debug_level, uint, S_IRUGO | S_IWUSR); module_param_array_named(adj, lowmem_adj, short, &lowmem_adj_size, S_IRUGO | S_IWUSR);
當如下節點數據發送變化時,會通過修改lowmem_minfree[]和lowmem_adj[]數組:
/sys/module/lowmemorykiller/parameters/minfree /sys/module/lowmemorykiller/parameters/adj
五、總結
本文主要從frameworks的ProcessList.java調整adj,通過socket通信將事件發送給native的守護進程lmkd;lmkd再根據具體的命令來執行相應操作,其主要功能 更新進程的oom_score_adj值以及lowmemorykiller驅動的parameters(包括minfree和adj);
最后講到了lowmemorykiller驅動,通過注冊shrinker,借助linux標準的內存回收機制,根據當前系統可用內存以及parameters配置參數(adj,minfree)來選取合適的selected_oom_score_adj,再從所有進程中選擇adj大于該目標值的并且占用rss內存最大的進程,將其殺掉,從而釋放出內存。
5.1 lmkd參數:
- oom_adj :代表進程的優先級, 數值越大,優先級越低,越容易被殺. 取值范圍[-16, 15]
- oom_score_adj : 取值范圍[-1000, 1000]
- oom_score:lmk策略中貌似并沒有看到使用的地方,這個應該是oom才會使用。
想查看某個進程的上述3值,只需要知道pid,查看以下幾個節點:
/proc/<pid>/oom_adj /proc/<pid>/oom_score_adj /proc/<pid>/oom_score
對于oom_adj與oom_score_adj有一定的映射關系:
- 當oom_adj = 15, 則oom_score_adj=1000;
- 當oom_adj < 15, 則oom_score_adj= oom_adj * 1000/17;
5.2 driver參數
/sys/module/lowmemorykiller/parameters/minfree (代表page個數) /sys/module/lowmemorykiller/parameters/adj (代表oom_score_adj)
例如:將 1,6 寫入節點/sys/module/lowmemorykiller/parameters/adj,將 1024,8192 寫入節點/sys/module/lowmemorykiller/parameters/minfree。策略:當系統可用內存低于 8192 個pages時,則會殺掉oom_score_adj>= 6 的進程;當系統可用內存低于 1024 個pages時,則會殺掉oom_score_adj>= 1 的進程。
來自:http://gityuan.com/2016/09/17/android-lowmemorykiller/