Linux內存管理 (21)OOM【轉】 Linux內存管理專題

轉自:https://www.cnblogs.com/arnoldlu/p/8567559.html

專題:Linux內存管理專題

關鍵詞:OOM、oom_adj、oom_score、badness。

 

Linux內核爲了提高內存的使用效率採用過度分配內存(over-commit memory)的辦法,造成物理內存過度緊張進而觸發OOM機制來殺死一些進程回收內存。

該機制會監控那些佔用內存過大,尤其是瞬間很快消耗大量內存的進程,爲了防止內存耗盡會把該進程殺掉。

Linux在內存分配路徑上會對內存餘量做檢查,(1)如果檢查到內存不足,則觸發OOM機制。(2)OOM首先會對系統所有進程(出init和內核線程等特殊進程)進行打分,並選出最bad的進程;然後殺死該進程。(3)同時會觸發內核oom_reaper進行內存收割。(4)同時內核還提供了sysfs接口系統OOM行爲,以及進程OOM行爲。然後借用一個示例來分析OOM時內存狀態。

1. 關於OOM

內核檢測到系統內存不足,在內存分配路徑上觸發out_of_memory(),然後調用select_bad_process()選擇一個'bad'進程oom_kill_process()殺掉,判斷和選擇一個‘bad'進程的過程由oom_badness()決定。

Linux下每個進程都有自己的OOM權重,在/proc/<pid>/oom_adj裏面,範圍是-17到+15,取值越高,越容易被殺掉。

下面從幾個方便來分析OOM:

2. OOM觸發路徑

在內存分配路徑上,當內存不足的時候會觸發kswapd、或者內存規整,極端情況會觸發OOM,來獲取更多內存。

在內存回收失敗之後,__alloc_pages_may_oom是OOM的入口,但是主要工作在out_of_memory中進行處理。

由於Linux內存都是以頁爲單位,所以__alloc_pages_nodemask是必經之處。

複製代碼
alloc_pages
  ->_alloc_pages
    ->__alloc_pages_nodemask
      ->__alloc_pages_slowpath-------------------------此時已經說明內存不夠,會觸發一些內存回收、內存規整機制,極端情況觸發OOM。
        ->__alloc_pages_may_oom -----------------------進入OOM的開始,包括一些檢查動作。
->out_of_memory------------------------------OOM的核心
->select_bad_process-----------------------選擇最'bad'進程
->oom_scan_process_thread ->oom_badness----------------------------計算當前進程有多'badness'
->oom_kill_process-------------------------殺死選中的進程
複製代碼

還有一種情況是do_page_fault(),如果產生VM_FAULT_OOM錯誤,就進入pagefault_out_of_memory()。

複製代碼
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long write,
                              unsigned long mmu_meh)
{
...
good_area:
...
        fault = handle_mm_fault(vma, address, write ? FAULT_FLAG_WRITE : 0);
        if (unlikely(fault & VM_FAULT_ERROR)) {
                if (fault & VM_FAULT_OOM)-------------------------------------------handle_mm_fault()時產生VM_FAULT_OOM錯誤,進入out_of_memory處理。
                        goto out_of_memory;
                else if (fault & VM_FAULT_SIGBUS)
                        goto do_sigbus;
                else if (fault & VM_FAULT_SIGSEGV)
                        goto bad_area;
                BUG();
        }
        if (fault & VM_FAULT_MAJOR)
                tsk->maj_flt++;
        else
                tsk->min_flt++;

        up_read(&mm->mmap_sem);
        return;
...
out_of_memory:
        pagefault_out_of_memory();
        return;
...
}


void pagefault_out_of_memory(void)
{
    struct oom_control oc = {
        .zonelist = NULL,
        .nodemask = NULL,
        .memcg = NULL,
        .gfp_mask = 0,
        .order = 0,------------------------------------------------------------------單個頁面情況。
    };
...
    out_of_memory(&oc);
}
複製代碼

3. 影響OOM的內核參數

參照Linux內存管理 (23)內存sysfs節點和工具的OOM章節。 

4. OOM代碼分析

4.1 OOM數據結構

OOM的核心數據結構是struct oom_control,在include/linux/oom.h中。

複製代碼
struct oom_control {
    /* Used to determine cpuset */
    struct zonelist *zonelist;

    /* Used to determine mempolicy */
    nodemask_t *nodemask;

    /* Memory cgroup in which oom is invoked, or NULL for global oom */
    struct mem_cgroup *memcg;

    /* Used to determine cpuset and node locality requirement */
    const gfp_t gfp_mask;----------------------------------發生異常時頁面分配掩碼。

    /*
     * order == -1 means the oom kill is required by sysrq, otherwise only
     * for display purposes.
     */
    const int order;---------------------------------------發生異常時申請頁面order大小。

    /* Used by oom implementation, do not set */
    unsigned long totalpages;
    struct task_struct *chosen;----------------------------OOM選中的當前進程結構。
    unsigned long chosen_points;---------------------------OOM對進程評分的最高分。
};
複製代碼

4.2 OOM觸發路徑

__alloc_pages_may_oom是內存分配路徑上的OOM入口,在進入OOM之前還會檢查一些特殊情況。

複製代碼
static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
    const struct alloc_context *ac, unsigned long *did_some_progress)
{
    struct oom_control oc = {---------------------------------------------------------OOM控制參數。
        .zonelist = ac->zonelist,
        .nodemask = ac->nodemask,
        .memcg = NULL,
        .gfp_mask = gfp_mask,
        .order = order,
    };
    struct page *page;

    *did_some_progress = 0;

    /*
     * Acquire the oom lock.  If that fails, somebody else is
     * making progress for us.
     */
    if (!mutex_trylock(&oom_lock)) {
        *did_some_progress = 1;
        schedule_timeout_uninterruptible(1);
        return NULL;
    }

    page = get_page_from_freelist(gfp_mask | __GFP_HARDWALL, order,
                    ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);-----------------------------再次使用高水位檢查一次,是否需要啓動OOM流程。
    if (page)
        goto out;

    if (!(gfp_mask & __GFP_NOFAIL)) {----------------------------------------------__GFP_NOFAIL是不允許內存申請失敗的情況,下面都是允許失敗的處理。
        /* Coredumps can quickly deplete all memory reserves */
        if (current->flags & PF_DUMPCORE)
            goto out;
        /* The OOM killer will not help higher order allocs */
        if (order > PAGE_ALLOC_COSTLY_ORDER)---------------------------------------order超過3的申請失敗,不會啓動OOM回收。
            goto out;
        /* The OOM killer does not needlessly kill tasks for lowmem */
        if (ac->high_zoneidx < ZONE_NORMAL)
            goto out;
        if (pm_suspended_storage())
            goto out;

        /* The OOM killer may not free memory on a specific node */
        if (gfp_mask & __GFP_THISNODE)
            goto out;
    }
    /* Exhausted what can be done so it's blamo time */
    if (out_of_memory(&oc) || WARN_ON_ONCE(gfp_mask & __GFP_NOFAIL)) {-------------經過上面各種情況,任然需要進行OOM處理。調用out_of_memory()。
        *did_some_progress = 1;

        if (gfp_mask & __GFP_NOFAIL) {
            page = get_page_from_freelist(gfp_mask, order,
                    ALLOC_NO_WATERMARKS|ALLOC_CPUSET, ac);-------------------------對於__GFP_NOFAIL的分配情況,降低分配條件從ALLOC_WMARK_HIGH|ALLOC_CPUSET降低到ALLOC_NO_WATERMARKS|ALLOC_CPUSET。
            /*
             * fallback to ignore cpuset restriction if our nodes
             * are depleted
             */
            if (!page)
                page = get_page_from_freelist(gfp_mask, order,
                    ALLOC_NO_WATERMARKS, ac);--------------------------------------如果還是分配失敗,再次降低分配標準,從ALLOC_NO_WATERMARKS|ALLOC_CPUSET降低到ALLOC_NO_WATERMARKS。真的是爲了成功,節操越來越低啊。
        }
    }
out:
    mutex_unlock(&oom_lock);
    return page;
}
複製代碼

4.3 OOM處理:對進程打分以及殺死最高評分進程

out_of_memory函數是OOM機制的核心,他可以分爲兩部分。一是調挑選最’bad‘的進程,二是殺死它。

複製代碼
bool out_of_memory(struct oom_control *oc)
{
    unsigned long freed = 0;
    enum oom_constraint constraint = CONSTRAINT_NONE;

    if (oom_killer_disabled)----------------------------------------------------在freeze_processes會將其置位,即禁止OOM;在thaw_processes會將其清零,即打開OOM。所以,如果在凍結過程,不允許OOM。
        return false;

    if (!is_memcg_oom(oc)) {
        blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
        if (freed > 0)
            /* Got some memory back in the last second. */
            return true;
    }

    if (task_will_free_mem(current)) {----------------------------------------如果當前進程正因爲各種原因將要退出,或者釋放內存,將當前進程作爲OOM候選者,然後喚醒OOM reaper去收割進而釋放內存。
        mark_oom_victim(current);
        wake_oom_reaper(current);
        return true;---------------------當前進程由於自身原因將要推出,OOM則將其標註爲TIF_MEMDIE狀態;然後喚醒OOM Reaper去處理。不需要經過下面的打分和殺死流程。
    }

    if (oc->gfp_mask && !(oc->gfp_mask & (__GFP_FS|__GFP_NOFAIL)))-----------如果內存申請掩碼包括__GFP_DS或__GFP_NOFAIL,則不進行OOM收割。
        return true;

    constraint = constrained_alloc(oc);--------------------------------------未定義CONFIG_NUMA返回CONSTRAINT_NONE。
    if (constraint != CONSTRAINT_MEMORY_POLICY)
        oc->nodemask = NULL;
    check_panic_on_oom(oc, constraint);--------------------------------------檢查sysctl_panic_on_oom設置,以及是否由sysrq觸發,來決定是否觸發panic。

    if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&--------------如果設置了sysctl_oom_kill_allocating_task,那麼當內存耗盡時,會把當前申請內存分配的進程殺掉。
        current->mm && !oom_unkillable_task(current, NULL, oc->nodemask) &&
        current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
        get_task_struct(current);
        oc->chosen = current;
        oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");
        return true;
    }

    select_bad_process(oc);-------------------------------------------------遍歷所有進程,進程下的線程,查找合適的候選進程。即得分最高的候選進程。
    /* Found nothing?!?! Either we hang forever, or we panic. */
    if (!oc->chosen && !is_sysrq_oom(oc) && !is_memcg_oom(oc)) {------------如果沒有合適候選進程,並且OOM不是由sysrq觸發的,進入panic。
        dump_header(oc, NULL);
        panic("Out of memory and no killable processes...\n");
    }
    if (oc->chosen && oc->chosen != (void *)-1UL) {
        oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" :
                 "Memory cgroup out of memory");----------------------------殺死選中的進程。
        schedule_timeout_killable(1);
    }
    return !!oc->chosen;
}
複製代碼

select_bad_process()通過oom_evaluate_task()來評估每個進程的得分,對於進程1、內核線程、得分低的進程直接跳過。

複製代碼
static void select_bad_process(struct oom_control *oc)
{
    if (is_memcg_oom(oc))
        mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
    else {
        struct task_struct *p;

        rcu_read_lock();
        for_each_process(p)----------------------------------------------遍歷系統範圍內所有進程線程。
            if (oom_evaluate_task(p, oc))
                break;
        rcu_read_unlock();
    }

    oc->chosen_points = oc->chosen_points * 1000 / oc->totalpages;
}

static int oom_evaluate_task(struct task_struct *task, void *arg)
{
    struct oom_control *oc = arg;
    unsigned long points;

    if (oom_unkillable_task(task, NULL, oc->nodemask))-------------------進程1以及內核線程等等不能被kill的線程跳過。
        goto next;

    if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
        if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
            goto next;
        goto abort;
    }

    if (oom_task_origin(task)) {
        points = ULONG_MAX;
        goto select;
    }

    points = oom_badness(task, NULL, oc->nodemask, oc->totalpages);------對進程task進行打分。
    if (!points || points < oc->chosen_points)---------------------------這裏保證只取最高分的進程,所以分數最高者被選中。其他情況則直接跳過。
        goto next;

    /* Prefer thread group leaders for display purposes */
    if (points == oc->chosen_points && thread_group_leader(oc->chosen))
        goto next;
select:
    if (oc->chosen)
        put_task_struct(oc->chosen);
    get_task_struct(task);
    oc->chosen = task;--------------------------------------------------更新OOM選中的進程和當前最高分。
    oc->chosen_points = points;
next:
    return 0;
abort:
    if (oc->chosen)
        put_task_struct(oc->chosen);
    oc->chosen = (void *)-1UL;
    return 1;
}
複製代碼

在oom_badness()中計算當前進程的得分,返回選中進程的結構體,以及進程得分ppoints。

oom_badness()是給進程打分的函數,可以說是核心中的核心。最終結果受oom_score_adj和當前進程內存使用量綜合影響。

  • oom_score_adj爲OOM_SCORE_ADJ_MIN的進程不參加評選。進程的oom_score_adj值在/proc/xxx/oom_score_adj中。
  • mm->flags爲MMF_OOM_SKIP的進程不參加評選。
  • 處於vfork()中的進程不參加評選。
  • 進程的得分取決於其消耗的RSS部分內存(文件映射內存MM_FILEPAGES、匿名映射內存MM_ANONPAGES、shmem內存MM_SHMEMPAGES)、匿名交換內存MM_SWAPENTS、PTE頁表所佔內存、PMD頁表所佔內存。
  • 具有root權限的進程只取其97%的得分參加評選。

所以進程得分points=process_pages + oom_score_adj*totalpages/1000;如果是root權限的進程points=process_pages*0.97 + oom_score_adj*totalpages/1000。

  • 在oom_score_adj都爲0(默認值)的情況下,最終得分跟進程自身消耗的內存有關;消耗的內存越大越容易被Kill。
  • oom_score_adj每降低1,可以多獲得系統內存資源的1/1000使用量。反之,每增加1,則少獲得系統內存資源1/1000使用量。
複製代碼
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{
    long points;
    long adj;

    if (oom_unkillable_task(p, memcg, nodemask))-------------------------------如果進程不可被殺,直接跳過。
        return 0;

    p = find_lock_task_mm(p);------------找到進程p,並使用task_lock()鎖上。
    if (!p)
        return 0;

    /*
     * Do not even consider tasks which are explicitly marked oom
     * unkillable or have been already oom reaped or the are in
     * the middle of vfork
     */
    adj = (long)p->signal->oom_score_adj;--------------------------------------獲取當前進程的oom_score_adj參數。
    if (adj == OOM_SCORE_ADJ_MIN ||
            test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
            in_vfork(p)) {
        task_unlock(p);
        return 0;--------------------------------------------------------------如果當前進程oom_score_adj爲OOM_SCORE_ADJ_MIN的話,就返回0.等於告訴OOM,此進程不參數'bad'評比。
    }

    /*
     * The baseline for the badness score is the proportion of RAM that each
     * task's rss, pagetable and swap space use.
     */
    points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
        atomic_long_read(&p->mm->nr_ptes) + mm_nr_pmds(p->mm);-----------------可以看出points綜合了內存佔用情況,包括RSS部分、swap file或者swap device佔用內存、以及頁表佔用內存。
    task_unlock(p);

    /*
     * Root processes get 3% bonus, just like the __vm_enough_memory()
     * implementation used by LSMs.
     */
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))------------------------------如果是root用戶,增加3%的使用特權。
        points -= (points * 3) / 100;

    /* Normalize to oom_score_adj units */
    adj *= totalpages / 1000;--------------------------------------------------這裏可以看出oom_score_adj對最終分數的影響,如果oom_score_adj小於0,則最終points就會變小,進程更加不會被選中。
    points += adj;-------------------------------------------------------------將歸一化後的adj和points求和,作爲當前進程的分數。

    /*
     * Never return 0 for an eligible task regardless of the root bonus and
     * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
     */
    return points > 0 ? points : 1;
}
複製代碼

oom_kill_process()用於殺死最高分的進程,包括進程下的線程。

複製代碼
static void oom_kill_process(struct oom_control *oc, const char *message)
{
    struct task_struct *p = oc->chosen;
    unsigned int points = oc->chosen_points;
    struct task_struct *victim = p;
    struct task_struct *child;
    struct task_struct *t;
    struct mm_struct *mm;
    unsigned int victim_points = 0;
    static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL,
                          DEFAULT_RATELIMIT_BURST);
    bool can_oom_reap = true;

    task_lock(p);
    if (task_will_free_mem(p)) {------------------------------------------對於非coredump正處於退出狀態的線程,標註TIF_MEMDIE並喚醒reaper線程進行收割,然後退出。
        mark_oom_victim(p);
        wake_oom_reaper(p);
        task_unlock(p);
        put_task_struct(p);
        return;
    }
    task_unlock(p);

    if (__ratelimit(&oom_rs))
        dump_header(oc, p);----------------------------------------------在kill進程之前,將系統棧信息、內存信息、所有進程的內存消耗情況打印。

5.    pr_err("%s: Kill process %d (%s) score %u or sacrifice child\n",
        message, task_pid_nr(p), p->comm, points);-----------------------輸出將要kill掉的進程名、pid、score。

    read_lock(&tasklist_lock);
    for_each_thread(p, t) {-----------------------------------------------遍歷進程下的線程
        list_for_each_entry(child, &t->children, sibling) {
            unsigned int child_points;

            if (process_shares_mm(child, p->mm))--------如果子進程有自己單獨的mm內存空間,則可以被選中代提父進程被kill。
                continue;
            /*
             * oom_badness() returns 0 if the thread is unkillable
             */
            child_points = oom_badness(child,
                oc->memcg, oc->nodemask, oc->totalpages);-----------------計算子線程的得分情況
            if (child_points > victim_points) {---------------------------將得分最高者計爲victim,得分爲victim_points。
                put_task_struct(victim);
                victim = child;-------------------------------------------確保victim是p子進程中得分最高者,代提p受死。
                victim_points = child_points;
                get_task_struct(victim);
            }
        }
    }
    read_unlock(&tasklist_lock);

    p = find_lock_task_mm(victim);
    if (!p) {
        put_task_struct(victim);
        return;
    } else if (victim != p) {
        get_task_struct(p);
        put_task_struct(victim);
        victim = p;
    }

    /* Get a reference to safely compare mm after task_unlock(victim) */
    mm = victim->mm;
    atomic_inc(&mm->mm_count);

    do_send_sig_info(SIGKILL, SEND_SIG_FORCED, victim, true);--------------發送SIGKILL信號給victim進程。
    mark_oom_victim(victim);-----------------------------------------------標註TIF_MEMDIE是因爲OOM被殺死。
6.  pr_err("Killed process %d (%s) total-vm:%lukB, anon-rss:%lukB, file-rss:%lukB, shmem-rss:%lukB\n",
        task_pid_nr(victim), victim->comm, K(victim->mm->total_vm),
        K(get_mm_counter(victim->mm, MM_ANONPAGES)),
        K(get_mm_counter(victim->mm, MM_FILEPAGES)),
        K(get_mm_counter(victim->mm, MM_SHMEMPAGES)));---------------------被kill進程的內存信息。
    task_unlock(victim);

    rcu_read_lock();
    for_each_process(p) {--------------------------------------------------繼續處理共享內存的相關線程
        if (!process_shares_mm(p, mm))
            continue;
        if (same_thread_group(p, victim))
            continue;
        if (is_global_init(p)) {
            can_oom_reap = false;
            set_bit(MMF_OOM_SKIP, &mm->flags);
            pr_info("oom killer %d (%s) has mm pinned by %d (%s)\n",
                    task_pid_nr(victim), victim->comm,
                    task_pid_nr(p), p->comm);
            continue;
        }

        if (unlikely(p->flags & PF_KTHREAD))-----------------------------內核線程跳過。
            continue;
        do_send_sig_info(SIGKILL, SEND_SIG_FORCED, p, true);
    }
    rcu_read_unlock();

    if (can_oom_reap)
        wake_oom_reaper(victim);----------------------------------------喚醒OOM Reaper內核線程收割。

    mmdrop(mm);---------------------------------------------------------釋放mm空間的內存。包括申請的頁面、mm結構體等。
    put_task_struct(victim);--------------------------------------------釋放task_struct佔用的內存空間,包括cgroup等等。
}
複製代碼

 dump_header()有助於發現OOM現場,找出OOM原因。

複製代碼
static void dump_header(struct oom_control *oc, struct task_struct *p)
{
    nodemask_t *nm = (oc->nodemask) ? oc->nodemask : &cpuset_current_mems_allowed;

1. pr_warn("%s invoked oom-killer: gfp_mask=%#x(%pGg), nodemask=%*pbl, order=%d, oom_score_adj=%hd\n",
        current->comm, oc->gfp_mask, &oc->gfp_mask,
        nodemask_pr_args(nm), oc->order,
        current->signal->oom_score_adj);--------------------------------顯示在哪個進程中觸發了OOM。
    if (!IS_ENABLED(CONFIG_COMPACTION) && oc->order)
        pr_warn("COMPACTION is disabled!!!\n");

    cpuset_print_current_mems_allowed();
2.      dump_stack();-------------------------------------------------輸出當前現場的棧信息。
    if (oc->memcg)
        mem_cgroup_print_oom_info(oc->memcg, p);
    else
3.       show_mem(SHOW_MEM_FILTER_NODES);------------------------------輸出整個系統的內存使用情況。
    if (sysctl_oom_dump_tasks)
4.      dump_tasks(oc->memcg, oc->nodemask);--------------------------顯示系統所有進程的內存使用情況。
}

static void dump_tasks(struct mem_cgroup *memcg, const nodemask_t *nodemask)
{
    struct task_struct *p;
    struct task_struct *task;

    pr_info("[ pid ]   uid  tgid total_vm      rss nr_ptes nr_pmds swapents oom_score_adj name\n");
    rcu_read_lock();
    for_each_process(p) {
        if (oom_unkillable_task(p, memcg, nodemask))-------------------不可被kill的進程不顯示。
            continue;

        task = find_lock_task_mm(p);-----------------------------------內核線程等沒有自己的mm,也無法被kill,所以不顯示。
        if (!task) {
            continue;
        }

        pr_info("[%5d] %5d %5d %8lu %8lu %7ld %7ld %8lu         %5hd %s\n",
            task->pid, from_kuid(&init_user_ns, task_uid(task)),
            task->tgid, task->mm->total_vm, get_mm_rss(task->mm),
            atomic_long_read(&task->mm->nr_ptes),
            mm_nr_pmds(task->mm),
            get_mm_counter(task->mm, MM_SWAPENTS),
            task->signal->oom_score_adj, task->comm);-----------------total_vm和rss單位都是頁。
        task_unlock(task);
    }
    rcu_read_unlock();
}
複製代碼

4.4 OOM Reaper

內核創建oom_reaper內核線程,用於快速回收OOM選中的victim進程所使用的匿名內存、非VM_SHARED內存、swapped out內存等。

4.4.1 創建oom_reaper內核線程

oom_init()創建oom_reaper內核線程後即進入睡眠狀態,等待產生OOM之後喚醒,進行收割。

複製代碼
subsys_initcall(oom_init)

static struct task_struct *oom_reaper_th;------------oom_reaper內核線程的task_struct指針。
static DECLARE_WAIT_QUEUE_HEAD(oom_reaper_wait);-----OOM Reaper等待隊列,oom_reaper線程在此等待,當有OOM產生的時候喚醒等待隊列,並從oom_reaper_list中獲取待收割進程結構體。
static struct task_struct *oom_reaper_list;----------待收割的進程。
static DEFINE_SPINLOCK(oom_reaper_lock);
static int __init oom_init(void)
{
    oom_reaper_th = kthread_run(oom_reaper, NULL, "oom_reaper");-----創建oom_reaper內核線程。
    if (IS_ERR(oom_reaper_th)) {
        pr_err("Unable to start OOM reaper %ld. Continuing regardless\n",
                PTR_ERR(oom_reaper_th));
        oom_reaper_th = NULL;
    }
    return 0;
}

static int oom_reaper(void *unused)
{
    while (true) {
        struct task_struct *tsk = NULL;

        wait_event_freezable(oom_reaper_wait, oom_reaper_list != NULL);----oom_reaper在此睡眠,直到有OOM產生並且通過wake_oom_reaper()喚醒。
        spin_lock(&oom_reaper_lock);
        if (oom_reaper_list != NULL) {
            tsk = oom_reaper_list;
            oom_reaper_list = tsk->oom_reaper_list;
        }
        spin_unlock(&oom_reaper_lock);

        if (tsk)
            oom_reap_task(tsk);-----------------------收割OOM選中的最bad進程,從流程看oom_reaper每次只能收割一個線程。
    }

    return 0;
}
複製代碼

4.4.2 收割進程

複製代碼
#define MAX_OOM_REAP_RETRIES 10
static void oom_reap_task(struct task_struct *tsk)
{
    int attempts = 0;
    struct mm_struct *mm = tsk->signal->oom_mm;

    /* Retry the down_read_trylock(mmap_sem) a few times */
    while (attempts++ < MAX_OOM_REAP_RETRIES && !__oom_reap_task_mm(tsk, mm))
        schedule_timeout_idle(HZ/10);-------最多重試10次,每次間隔100ms,進行收割。

    if (attempts <= MAX_OOM_REAP_RETRIES)
        goto done;

    pr_info("oom_reaper: unable to reap pid:%d (%s)\n",
        task_pid_nr(tsk), tsk->comm);-------收割失敗,顯示系統hold住的lock信息。
    debug_show_all_locks();

done:
    tsk->oom_reaper_list = NULL;
    set_bit(MMF_OOM_SKIP, &mm->flags);
    put_task_struct(tsk);
}

static bool __oom_reap_task_mm(struct task_struct *tsk, struct mm_struct *mm)
{
    struct mmu_gather tlb;
    struct vm_area_struct *vma;
    struct zap_details details = {.check_swap_entries = true,
                      .ignore_dirty = true};
    bool ret = true;

    mutex_lock(&oom_lock);

    if (!down_read_trylock(&mm->mmap_sem)) {
        ret = false;
        goto unlock_oom;
    }

    if (mm_has_notifiers(mm)) {
        up_read(&mm->mmap_sem);
        schedule_timeout_idle(HZ);
        goto unlock_oom;
    }

    if (!mmget_not_zero(mm)) {
        up_read(&mm->mmap_sem);
        goto unlock_oom;
    }

    set_bit(MMF_UNSTABLE, &mm->flags);

    tlb_gather_mmu(&tlb, mm, 0, -1);
    for (vma = mm->mmap ; vma; vma = vma->vm_next) {
        if (is_vm_hugetlb_page(vma))------跳過hugetlb類型頁面。
            continue;

        if (vma->vm_flags & VM_LOCKED)----跳過VM_LOCKED的vma區域。
            continue;

        if (vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED))
            unmap_page_range(&tlb, vma, vma->vm_start, vma->vm_end,
                     &details);----------釋放匿名、非VM_SHARED類型頁面。
    }
    tlb_finish_mmu(&tlb, 0, -1);
    pr_info("oom_reaper: reaped process %d (%s), now anon-rss:%lukB, file-rss:%lukB, shmem-rss:%lukB\n",
            task_pid_nr(tsk), tsk->comm,
            K(get_mm_counter(mm, MM_ANONPAGES)),
            K(get_mm_counter(mm, MM_FILEPAGES)),
            K(get_mm_counter(mm, MM_SHMEMPAGES)));----------------顯示經過oom_reaper之後的victim進程所佔用的內存信息。
    up_read(&mm->mmap_sem);

    mmput_async(mm);
unlock_oom:
    mutex_unlock(&oom_lock);
    return ret;
}
複製代碼

5. OOM實例解析

對照一個OOM實例並解析如下:

複製代碼
1. [19174.926798] copy invoked oom-killer: gfp_mask=0x24200c8(GFP_USER|__GFP_MOVABLE), nodemask=0, order=0, oom_score_adj=0--------參考dump_header(),輸出OOM產生現場線程信息,包括分配掩碼、OOM信息。
2. [19174.937586] CPU: 0 PID: 163 Comm: copy Not tainted 4.9.56 #1---------參考show_stack(),顯示棧信息。可以看出OOM現場的調用信息,這裏可以看出是CMA分配出發了OOM。
[19174.943274] 
Call Trace:
[<802f63c2>] dump_stack+0x1e/0x3c
[<80132224>] dump_header.isra.6+0x84/0x1a0
[<800f2d68>] oom_kill_process+0x23c/0x49c
[<800f32fc>] out_of_memory+0xb0/0x3a0
[<800f7834>] __alloc_pages_nodemask+0xa84/0xb5c
[<801306b8>] alloc_migrate_target+0x34/0x6c
[<8012f30c>] migrate_pages+0x108/0xbe4
[<800f8a0c>] alloc_contig_range+0x188/0x378
[<80130c54>] cma_alloc+0x100/0x220
[<80388fe2>] dma_alloc_from_contiguous+0x2e/0x48
[<8037bb30>] xxxxx_dma_alloc_coherent+0x48/0xdc
[<8037be8c>] mem_zone_ioctl+0xf0/0x198
[<80148cec>] do_vfs_ioctl+0x84/0x70c
[<80149408>] SyS_ioctl+0x94/0xb8
[<8004a246>] csky_systemcall+0x96/0xe0
3. [19175.001223] Mem-Info:------------參考show_mem(),輸出系統內存詳細使用情況。這裏可以看出free=592很少,active_anon和shmem非常大。
[19175.003535] active_anon:99682 inactive_anon:12 isolated_anon:1----------顯示當前系統所有node不同類型頁面的統計信息,單位是頁面。
[19175.003535]  active_file:55 inactive_file:75 isolated_file:0
[19175.003535]  unevictable:0 dirty:0 writeback:0 unstable:0
[19175.003535]  slab_reclaimable:886 slab_unreclaimable:652
[19175.003535]  mapped:2 shmem:91862 pagetables:118 bounce:0
[19175.003535]  free:592 free_pcp:61 free_cma:0
[19175.035394] Node 0 active_anon:398728kB inactive_anon:48kB active_file:220kB inactive_file:300kB unevictable:0kB isolated(anon):4kB isolated(file):0kB mapped:8kB dirty:0kB writeback:0kB shmem:367448kB writeback_tmp:0kB unstable:0kB pages_scanned:2515 all_unreclaimable? yes
--------------------顯示單個節點的內存統計信息,單位是kB。 [19175.059602] Normal free:2368kB min:2444kB low:3052kB high:3660kB active_anon:398728kB inactive_anon:48kB active_file:220kB inactive_file:300kB unevictable:0kB writepending:0kB present:1048572kB managed:734584kB mlocked:0kB slab_reclaimable:3544kB slab_unreclaimable:2608kB kernel_stack:624kB pagetables:472kB bounce:0kB free_pcp:244kB local_pcp:244kB free_cma:0kB
--------------------顯示某一zone下內存統計信息,單位是kB。 [19175.091602] lowmem_reserve[]: 0 0 0 [19175.095144] Normal: 21*4kB (MHI) 14*8kB (MHI) 13*16kB (HI) 2*32kB (HI) 4*64kB (MI) 2*128kB (MH) 0*256kB 2*512kB (HI) 1*1024kB (H) 1*2048kB (I) 0*4096kB = 5076kB
--------------------顯示某一zone的按頁面可遷移類型劃分頁面大小,單位是KB。M表示Movable,U表示Unmovable,E表示rEclaimable,H表示Highatomic,C表示CMA,I表示Isolate。 91996 total pagecache pages [19175.112370] 262143 pages RAM [19175.115254] 0 pages HighMem/MovableOnly [19175.119106] 78497 pages reserved [19175.122350] 90112 pages cma reserved 4. [19175.125942] [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name-----------參考dump_tasks(),輸出系統可被kill的進程內存使用情況。 [19175.134514] [ 135] 0 135 1042 75 4 0 0 -1000 sshd [19175.143070] [ 146] 0 146 597 141 3 0 0 0 autologin [19175.152057] [ 147] 0 147 608 152 4 0 0 0 sh [19175.160434] [ 161] 0 161 109778 7328 104 0 0 0 xxxxx 5. [19175.169068] Out of memory: Kill process 161 (xxxxx) score 39 or sacrifice child---------------因爲OOM待kill的進程信息。 6. [19175.176439] Killed process 161 (xxxxx) total-vm:439112kB, anon-rss:29304kB, file-rss:8kB, shmem-rss:0kB----------已經發送信號SIGKILL強制退出的進程信息。
複製代碼

通過上面的信息可以知道是哪個進程、OOM現場、哪些內存消耗太多。

這裏需要重點查看系統的active_anon和shmem爲什麼如此大,造成了OOM。

6. 對OOM的配置

6.1 系統OOM配置

/proc/sys/vm/oom_dump_tasks:如果設置,則dump_tasks()顯示當前系統所有進程內存使用狀態。
/proc/sys/vm/oom_kill_allocating_task:如果設置了sysctl_oom_kill_allocating_task,在當前進程滿足一定條件下,優先選擇當前進程作爲OOM殺死對象。

複製代碼
bool out_of_memory(struct oom_control *oc)
{
...
    if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
        current->mm && !oom_unkillable_task(current, NULL, oc->nodemask) &&
        current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
        get_task_struct(current);
        oc->chosen = current;
        oom_kill_process(oc, "Out of memory (oom_kill_allocating_task)");
        return true;
    }
...
}
複製代碼

/proc/sys/vm/panic_on_oom:在發生OOM的時候是否進行panic。

複製代碼
static void check_panic_on_oom(struct oom_control *oc,
                   enum oom_constraint constraint)
{
    if (likely(!sysctl_panic_on_oom))-------------panic_on_oom爲0表示發生OOM的時候不進行panic()。
        return;
    if (sysctl_panic_on_oom != 2) {---------------panic_on_oom爲非2,是否進入panic還需要根據constraint來決定。
        /*
         * panic_on_oom == 1 only affects CONSTRAINT_NONE, the kernel
         * does not panic for cpuset, mempolicy, or memcg allocation
         * failures.
         */
        if (constraint != CONSTRAINT_NONE)
            return;
    }
    /* Do not panic for oom kills triggered by sysrq */
    if (is_sysrq_oom(oc))return;
    dump_header(oc, NULL);
    panic("Out of memory: %s panic_on_oom is enabled\n",
        sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");--panic_on_oom爲2,則強制進行panic()。
} 
複製代碼

6.2 進程OOM配置

/proc/xxx/oom_adj:可讀寫,範圍是-17~15。oom_adj是oom_score_adj經過換算能得到。

複製代碼
static ssize_t oom_adj_read(struct file *file, char __user *buf, size_t count,
                loff_t *ppos)
{
    struct task_struct *task = get_proc_task(file_inode(file));
    char buffer[PROC_NUMBUF];
    int oom_adj = OOM_ADJUST_MIN;
    size_t len;

    if (!task)
        return -ESRCH;
    if (task->signal->oom_score_adj == OOM_SCORE_ADJ_MAX)
        oom_adj = OOM_ADJUST_MAX;
    else
        oom_adj = (task->signal->oom_score_adj * -OOM_DISABLE) /
              OOM_SCORE_ADJ_MAX;
    put_task_struct(task);
    len = snprintf(buffer, sizeof(buffer), "%d\n", oom_adj);
    return simple_read_from_buffer(buf, count, ppos, buffer, len);
}
複製代碼

/proc/xxx/oom_score:只讀。經過oom_badness()計算得出對當前進程消耗頁面數目,然後相對於totalpages歸一化到1000。

複製代碼
static int proc_oom_score(struct seq_file *m, struct pid_namespace *ns,
              struct pid *pid, struct task_struct *task)
{
    unsigned long totalpages = totalram_pages + total_swap_pages;
    unsigned long points = 0;

    points = oom_badness(task, NULL, NULL, totalpages) *
                    1000 / totalpages;
    seq_printf(m, "%lu\n", points);

    return 0;
}
複製代碼

/proc/xxx/oom_score_adj:可讀寫,範圍是-1000~1000。內核中用於計算進程badness points參數。

 

相關閱讀:《Linux內核OOM Killer機制》、《Linux OOM機制介紹》、《Linux內核OOM機制的詳細分析》、《Linux內核OOM機制分析》 

聯繫方式:[email protected]
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章