OOM killer过程分析

分析oomkiller

什么是oomkiller

OOM全称 Out-of-Memory,也就是操作系统的可利用的内存已经不足了,没法再分配新的内存出来给进程,导致系统没法继续工作,如果不紧急处理,最终的结果必定是系统关机,系统上的所有进程将被杀死。因此OS为了保证内核系统层面的稳定运行,就会根据一定算法规则,选出**最应该优先被杀死的进程(理论上就是最占内存空间的那个进程)**进行杀死,杀死之后系统就腾出了大量的内存空间,系统的生命将得以延续,继续稳定运行,而这个机制就是OOM Killer机制。

被杀死的是哪个进程

那为什么不杀死肇事者进程,而是去杀死无辜的进程呢?(我们姑且称引发这种现象的为肇事者进程)

我们在上帝视角来看,因为肇事者进程而导致内存可能死机,那肯定要去杀死肇事者进程啊。但是操作系统是无法分辨谁是肇事者进程的。

例如,A进程疯狂的分配内存,导致可用的内存都用完了,此时B进程来了,说我也要分配内存空间,那这内存空间都用完了,B进程肯定分配内存空间失败,它因为分配一直不成功,就一直抛出异常,那挂的还是B进程。

在linux源码中有一段话

* out_of_memory - kill the "best" process when we run out of memory
 * If we run out of memory, we have the choice between either
 * killing a random task (bad), letting the system crash (worse)
 * OR try to be smart about which process to kill. Note that we
 * don't have to be perfect here, we just have to be good.

”当我们内存不足时,我们有两种处理方案:随机杀死一个任务,这可能导致系统崩溃;或者尝试有策略地选择值得杀死的任务。我们没有必要做到最好,但我们只需要尽力把事情做好。“

源码分析

分配内存错误,那我们就从内存分配函数开始

alloc_pages()

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
    return __alloc_pages(gfp_mask, order, numa_node_id(), NULL);
}

__alloc_pages(gfp_mask, order, numa_node_id(), NULL)

  • alloc_pages() 实际上是对 __alloc_pages() 的调用。
  • numa_node_id() 返回当前 CPU 所在的 NUMA 节点编号。如果系统支持 NUMA,那么页面将优先从该节点分配。

分配页面数(order

alloc_pages() 中的 order 参数决定了分配的页面数。页面数是 2^order,即:

  • order = 0:分配 1 个页面。
  • order = 1:分配 2 个连续页面。
  • order = 2:分配 4 个连续页面。
  • 依此类推…

那主要还是看__alloc_pages()

__alloc_pages()

struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid, nodemask_t *nodemask)
{
    struct page *page;
    struct alloc_context ac = { };

    // 为分配上下文做准备,包括 NUMA 和内存域信息
    if (unlikely(!gfp_ok(gfp)))
        return NULL;

    // 初始化分配上下文
    gfp = clear_gfp_flags(gfp, __GFP_ATOMIC | __GFP_HIGHMEM);

    // 准备内存分配的上下文,获取分配目标区域、策略等信息
    if (unlikely(!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac)))
        return NULL;

retry_cpuset:
    // 从内存区域中尝试分配页面
    page = get_page_from_freelist(gfp, order, &ac);

    // 如果分配失败,尝试回收内存或调用 OOM 处理
    if (!page) {
        page = __alloc_pages_slowpath(gfp, order, &ac);
    }

    return page;
}

prepare_alloc_pages():该函数初始化内存分配上下文(alloc_context),确定要分配的内存区域,并检查系统状态是否允许分配内存。

get_page_from_freelist(gfp, order, &ac):这个函数尝试从空闲页列表中获取页面。如果成功,它会返回页面指针;如果失败,则进入下一步。

__alloc_pages_slowpath(gfp, order, &ac):如果从空闲列表中无法分配页面,则进入 “慢路径”。这个路径会尝试执行内存回收、可能会触发 OOM (Out Of Memory) 处理器来回收内存或终止进程。

__alloc_pages() 无法在快速路径中分配到内存页面时(通常是由于系统内存不足),会进入这个“慢路径”进行额外的操作,例如内存回收、触发 OOM(Out Of Memory)处理程序等,来尝试分配页面。

看到__alloc_pages_slowpath(gfp, order, &ac)这个函数

static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
						struct alloc_context *ac)
{
	bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
	const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
	struct page *page = NULL;
	unsigned int alloc_flags;
	unsigned long did_some_progress;
	enum compact_priority compact_priority;
	enum compact_result compact_result;
	int compaction_retries;
	int no_progress_loops;
	unsigned int cpuset_mems_cookie;
	int reserve_flags;

	/*
	 * We also sanity check to catch abuse of atomic reserves being used by
	 * callers that are not in atomic context.
	 */
	if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
				(__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
		gfp_mask &= ~__GFP_ATOMIC;

retry_cpuset:
	compaction_retries = 0;
	no_progress_loops = 0;
	compact_priority = DEF_COMPACT_PRIORITY;
	cpuset_mems_cookie = read_mems_allowed_begin();

	/*
	 * The fast path uses conservative alloc_flags to succeed only until
	 * kswapd needs to be woken up, and to avoid the cost of setting up
	 * alloc_flags precisely. So we do that now.
	 */
	alloc_flags = gfp_to_alloc_flags(gfp_mask);

	/*
	 * We need to recalculate the starting point for the zonelist iterator
	 * because we might have used different nodemask in the fast path, or
	 * there was a cpuset modification and we are retrying - otherwise we
	 * could end up iterating over non-eligible zones endlessly.
	 */
	ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->highest_zoneidx, ac->nodemask);
	if (!ac->preferred_zoneref->zone)
		goto nopage;

	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);

	/*
	 * The adjusted alloc_flags might result in immediate success, so try
	 * that first
	 */
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/*
	 * For costly allocations, try direct compaction first, as it's likely
	 * that we have enough base pages and don't need to reclaim. For non-
	 * movable high-order allocations, do that as well, as compaction will
	 * try prevent permanent fragmentation by migrating from blocks of the
	 * same migratetype.
	 * Don't try this for allocations that are allowed to ignore
	 * watermarks, as the ALLOC_NO_WATERMARKS attempt didn't yet happen.
	 */
	if (can_direct_reclaim &&
			(costly_order ||
			   (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
			&& !gfp_pfmemalloc_allowed(gfp_mask)) {
		page = __alloc_pages_direct_compact(gfp_mask, order,
						alloc_flags, ac,
						INIT_COMPACT_PRIORITY,
						&compact_result);
		if (page)
			goto got_pg;

		/*
		 * Checks for costly allocations with __GFP_NORETRY, which
		 * includes some THP page fault allocations
		 */
		if (costly_order && (gfp_mask & __GFP_NORETRY)) {
			/*
			 * If allocating entire pageblock(s) and compaction
			 * failed because all zones are below low watermarks
			 * or is prohibited because it recently failed at this
			 * order, fail immediately unless the allocator has
			 * requested compaction and reclaim retry.
			 *
			 * Reclaim is
			 *  - potentially very expensive because zones are far
			 *    below their low watermarks or this is part of very
			 *    bursty high order allocations,
			 *  - not guaranteed to help because isolate_freepages()
			 *    may not iterate over freed pages as part of its
			 *    linear scan, and
			 *  - unlikely to make entire pageblocks free on its
			 *    own.
			 */
			if (compact_result == COMPACT_SKIPPED ||
			    compact_result == COMPACT_DEFERRED)
				goto nopage;

			/*
			 * Looks like reclaim/compaction is worth trying, but
			 * sync compaction could be very expensive, so keep
			 * using async compaction.
			 */
			compact_priority = INIT_COMPACT_PRIORITY;
		}
	}

retry:
	/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);

	reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
	if (reserve_flags)
		alloc_flags = current_alloc_flags(gfp_mask, reserve_flags);

	/*
	 * Reset the nodemask and zonelist iterators if memory policies can be
	 * ignored. These allocations are high priority and system rather than
	 * user oriented.
	 */
	if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
		ac->nodemask = NULL;
		ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->highest_zoneidx, ac->nodemask);
	}

	/* Attempt with potentially adjusted zonelist and alloc_flags */
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/* Caller is not willing to reclaim, we can't balance anything */
	if (!can_direct_reclaim)
		goto nopage;

	/* Avoid recursion of direct reclaim */
	if (current->flags & PF_MEMALLOC)
		goto nopage;

	/* Try direct reclaim and then allocating */
	page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
							&did_some_progress);
	if (page)
		goto got_pg;

	/* Try direct compaction and then allocating */
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					compact_priority, &compact_result);
	if (page)
		goto got_pg;

	/* Do not loop if specifically requested */
	if (gfp_mask & __GFP_NORETRY)
		goto nopage;

	/*
	 * Do not retry costly high order allocations unless they are
	 * __GFP_RETRY_MAYFAIL
	 */
	if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
		goto nopage;

	if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
				 did_some_progress > 0, &no_progress_loops))
		goto retry;

	/*
	 * It doesn't make any sense to retry for the compaction if the order-0
	 * reclaim is not able to make any progress because the current
	 * implementation of the compaction depends on the sufficient amount
	 * of free memory (see __compaction_suitable)
	 */
	if (did_some_progress > 0 &&
			should_compact_retry(ac, order, alloc_flags,
				compact_result, &compact_priority,
				&compaction_retries))
		goto retry;


	/* Deal with possible cpuset update races before we start OOM killing */
	if (check_retry_cpuset(cpuset_mems_cookie, ac))
		goto retry_cpuset;

	/* Reclaim has failed us, start killing things */
	page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
	if (page)
		goto got_pg;

	/* Avoid allocations with no watermarks from looping endlessly */
	if (tsk_is_oom_victim(current) &&
	    (alloc_flags & ALLOC_OOM ||
	     (gfp_mask & __GFP_NOMEMALLOC)))
		goto nopage;

	/* Retry as long as the OOM killer is making progress */
	if (did_some_progress) {
		no_progress_loops = 0;
		goto retry;
	}

nopage:
	/* Deal with possible cpuset update races before we fail */
	if (check_retry_cpuset(cpuset_mems_cookie, ac))
		goto retry_cpuset;

	/*
	 * Make sure that __GFP_NOFAIL request doesn't leak out and make sure
	 * we always retry
	 */
	if (gfp_mask & __GFP_NOFAIL) {
		/*
		 * All existing users of the __GFP_NOFAIL are blockable, so warn
		 * of any new users that actually require GFP_NOWAIT
		 */
		if (WARN_ON_ONCE(!can_direct_reclaim))
			goto fail;

		/*
		 * PF_MEMALLOC request from this context is rather bizarre
		 * because we cannot reclaim anything and only can loop waiting
		 * for somebody to do a work for us
		 */
		WARN_ON_ONCE(current->flags & PF_MEMALLOC);

		/*
		 * non failing costly orders are a hard requirement which we
		 * are not prepared for much so let's warn about these users
		 * so that we can identify them and convert them to something
		 * else.
		 */
		WARN_ON_ONCE(order > PAGE_ALLOC_COSTLY_ORDER);

		/*
		 * Help non-failing allocations by giving them access to memory
		 * reserves but do not use ALLOC_NO_WATERMARKS because this
		 * could deplete whole memory reserves which would just make
		 * the situation worse
		 */
		page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
		if (page)
			goto got_pg;

		cond_resched();
		goto retry;
	}
fail:
	warn_alloc(gfp_mask, ac->nodemask,
			"page allocation failure: order:%u", order);
got_pg:
	return page;
}

我们只分析代码中关于oom的部分

OOM 触发点:当页面分配失败且内存不足时,__alloc_pages_slowpath() 调用 __alloc_pages_may_oom() 尝试触发 OOM killer。

page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
if (page)
    goto got_pg;

__alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress):这个函数负责处理 OOM 的逻辑。如果之前的内存回收没有取得进展(did_some_progress),那么 OOM killer 会被触发,选择杀掉某些进程来释放内存。

如果 __alloc_pages_may_oom() 成功杀掉了进程并释放了内存,它可能会返回一个可用的页面指针,并跳转到 got_pg

OOM 处理机制:通过终止内存占用大的进程来释放内存,确保系统可以继续分配页面。

OOM触发条件检测tsk_is_oom_victim(current):检查当前任务是否已经是 OOM killer 的受害者。如果是,则无需重复进行 OOM 处理,这样可以避免反复进入 OOM 逻辑。

if (tsk_is_oom_victim(current) &&
    (alloc_flags & ALLOC_OOM ||
    (gfp_mask & __GFP_NOMEMALLOC)))
    goto nopage;

当当前任务已经被标记为 OOM 受害者或分配标志允许无水位分配时(ALLOC_NO_WATERMARKS),跳过 OOM 处理,直接进入分配失败逻辑。

进展监控与重试:OOM 处理后,系统会根据内存释放的进展决定是否重试页面分配,以避免死循环和系统崩溃。

即使 OOM killer 触发后没有马上成功分配到页面,系统会继续监控进展,并在有进展时重试分配。

if (did_some_progress) {
    no_progress_loops = 0;
    goto retry;
}

did_some_progress 用于跟踪回收或 OOM 处理过程中是否有进展。如果 OOM 处理取得进展,则重置进度循环计数并重试分配。这避免了无效的死循环尝试,同时给 OOM 处理留有足够时间来释放内存。

__alloc_pages_slowpath() 进入慢路径尝试分配页面时,首先尝试内存压缩(compaction)、内存回收(reclaim)等手段。

如果这些手段失败并且内存不足,进入 OOM 处理逻辑,调用 __alloc_pages_may_oom()

page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);

那我们接下来看_alloc_pages_may_oom()

_alloc_pages_may_oom()

static struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
		      const struct alloc_context *ac, unsigned long *did_some_progress)
{
	// 初始化 OOM 控制结构体,用于控制 OOM 处理过程
	struct oom_control oc = {
		.zonelist = ac->zonelist,  // 分配使用的 zonelist
		.nodemask = ac->nodemask,  // 节点掩码(如果有 NUMA 限制)
		.gfp_mask = gfp_mask,      // 内存分配标志
		.order = order,            // 请求的页面数量(2^order 个页面)
		/* 确保分配是阻塞的或不使用内存保留区 */
		.may_throttle = gfp_mask & __GFP_DIRECT_RECLAIM,
	};

	/*
	 * 获取 OOM 锁并开始 OOM 处理。
	 * 如果触发了 OOM killer(通过 out_of_memory() 返回 true),
	 * 或者没有任何进展(did_some_progress 为 0),则返回 NULL。
	 */
	if (out_of_memory(&oc) || !*did_some_progress) {
		// 标记分配过程取得了一些进展
		*did_some_progress = 1;
		return NULL;  // 返回 NULL,表示内存分配失败,需要触发 OOM
	}

	/*
	 * 如果 OOM killer 已经杀死了某些进程并释放了一些内存,
	 * 再次尝试通过快速路径分配页面。
	 */
	return get_page_from_freelist(gfp_mask, order, gfp_to_alloc_flags(gfp_mask), ac);
}


核心逻辑在这里。调用 out_of_memory(&oc) 来启动 OOM 处理。如果内存非常紧张,内核会触发 OOM 处理机制。

out_of_memory() 返回 true 表示已经执行了 OOM 杀手来终止进程以释放内存。

如果 OOM 处理未能释放足够的内存并且 did_some_progress0(即之前的内存回收没有进展),也会返回 NULL,表示无法分配内存。

如果 OOM 处理杀死了一些进程并释放了内存,函数会调用 get_page_from_freelist() 再次尝试从空闲列表中分配页面。

get_page_from_freelist() 是用于分配页面的函数,它尝试从内存空闲区域中获取请求的页面。

进展标志 did_some_progress

  • 如果在调用 OOM 处理之前内存回收没有进展,did_some_progress 被设置为 1,以避免陷入无限循环。
  • did_some_progress 用来记录之前是否通过回收机制成功释放了一些内存。如果 OOM 杀手已经启动,且没有任何进展,系统会放弃当前分配尝试。

触发 OOM 的条件

  • 无法分配页面:当内存回收和压缩机制都失败时,系统无法满足分配请求,就会触发 OOM 处理。
  • 允许 OOM 处理__alloc_pages_may_oom() 只有在 GFP 标志允许直接内存回收(__GFP_DIRECT_RECLAIM)的情况下才会触发 OOM。
  • OOM 处理的结果:如果 out_of_memory() 成功终止了一个进程并释放了足够的内存,系统会再次尝试内存分配。

我们接着看out_of_memory()这个函数

out_of_memory()
bool out_of_memory(struct oom_control *oc)
{
    unsigned long freed = 0;  // 跟踪通过通知器链释放的内存量

    // 如果 OOM killer 被禁用,直接返回 false
    if (oom_killer_disabled)
        return false;

    // 检查是否为 memory cgroup(memcg) OOM
    if (!is_memcg_oom(oc)) {
        // 调用 OOM 通知器链,尝试释放内存
        blocking_notifier_call_chain(&oom_notify_list, 0, &freed);
        if (freed > 0)
            // 如果通过通知器链释放了一些内存,返回 true
            return true;
    }

    /*
     * 如果当前进程有 pending 的 SIGKILL 信号或正在退出,则标记它为
     * OOM victim(即被选中的进程)。这是为了让它快速退出,并释放内存。
     */
    if (task_will_free_mem(current)) {
        mark_oom_victim(current);  // 标记当前进程为 OOM victim
        wake_oom_reaper(current);  // 唤醒 OOM reaper 线程,确保尽快回收内存
        return true;  // 返回 true,表明内存将很快被释放
    }

    /*
     * 如果分配是 IO-less reclaim(没有 IO 相关的内存回收),或者分配的 gfp_mask
     * 不允许使用文件系统进行回收(即没有 __GFP_FS),则不触发 OOM killer,
     * 但 mem_cgroup_oom() 仍需要触发 OOM killer 即使它是 GFP_NOFS 分配。
     */
    if (oc->gfp_mask && !(oc->gfp_mask & __GFP_FS) && !is_memcg_oom(oc))
        return true;  // 不触发 OOM killer,允许继续尝试分配内存

    /*
     * 检查分配是否受限于 NUMA(非统一内存访问)或 memory cgroup 的约束条件,
     * 如果分配受限,这些情况可能需要不同的处理。
     */
    oc->constraint = constrained_alloc(oc);
    if (oc->constraint != CONSTRAINT_MEMORY_POLICY)
        oc->nodemask = NULL;  // 如果不是内存策略限制,则取消节点掩码

    // 检查是否应该触发内核 panic(基于 OOM 配置)
    check_panic_on_oom(oc);

    /*
     * 如果启用了 sysctl_oom_kill_allocating_task,并且当前进程可以被 OOM killer 终止,
     * 那么选择当前正在分配内存的进程作为被杀死的目标。这样做的目的是快速回收内存。
     */
    if (!is_memcg_oom(oc) && sysctl_oom_kill_allocating_task &&
        current->mm && !oom_unkillable_task(current) &&
        oom_cpuset_eligible(current, oc) &&
        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;  // 成功选择并终止了进程
    }

    // 启动 OOM 选择器来挑选要杀死的进程
    select_bad_process(oc);

    // 如果没有找到可以杀死的进程
    if (!oc->chosen) {
        dump_header(oc, NULL);  // 打印内存状态头信息,帮助诊断 OOM 状况
        pr_warn("Out of memory and no killable processes...\n");
        
        /*
         * 如果当前 OOM 是由系统级分配引起的,并且找不到可杀的进程,系统将进入死锁状态,
         * 因此应触发 panic,避免进入内存分配的死循环。
         */
        if (!is_sysrq_oom(oc) && !is_memcg_oom(oc))
            panic("System is deadlocked on memory\n");  // 触发 panic
    }

    /*
     * 如果找到了一个要杀死的进程,且不是特殊标记的情况(即 chosen 不是 -1),
     * 则终止该进程。
     */
    if (oc->chosen && oc->chosen != (void *)-1UL)
        oom_kill_process(oc, !is_memcg_oom(oc) ? "Out of memory" :
                         "Memory cgroup out of memory");

    return !!oc->chosen;  // 返回是否成功选择了要杀死的进程
}

负责判断是否需要启动 OOM killer,并根据当前系统的内存状态和分配上下文选择合适的进程进行终止。

该函数会首先检查是否有其他回收手段(如通知器链)可以释放内存,如果不行,再通过 OOM killer 来终止进程。

通过 select_bad_process(),系统会找到一个合适的“坏进程”,并通过 oom_kill_process() 将其终止,释放内存。

我们已经分析到了什么时候触发OOM killer,下面我们终于到了分析操作系统当触发OOM killer是到底会杀死哪个进程了。

select_bad_process()

static void select_bad_process(struct oom_control *oc)
{
	// 初始化 chosen_points 为 LONG_MIN,这个值表示目前还没有选择到合适的进程
	oc->chosen_points = LONG_MIN;

	// 检查是否是 memory cgroup OOM
	if (is_memcg_oom(oc)) {
		// 如果是 memcg OOM,则遍历该 memory cgroup 的所有进程,并使用 oom_evaluate_task 来评估每个进程
		mem_cgroup_scan_tasks(oc->memcg, oom_evaluate_task, oc);
	} else {
		struct task_struct *p;

		// 使用 RCU 读锁保护任务列表的遍历
		rcu_read_lock();

		// 遍历系统中的所有进程
		for_each_process(p) {
			// 对每个进程调用 oom_evaluate_task,评估该进程是否应该被选中为 OOM victim
			// 如果找到合适的进程,则终止遍历
			if (oom_evaluate_task(p, oc))
				break;
		}

		// 释放 RCU 读锁
		rcu_read_unlock();
	}
}

负责在 OOM 事件发生时,从所有进程或特定的 memory cgroup 中选择一个合适的进程进行终止。

选择策略:通过 oom_evaluate_task() 函数来评估每个进程的适合性,优先选择消耗大量内存且对系统影响较小的进程。

系统级与 cgroup:该函数可以根据是否是 memory cgroup OOM 来选择合适的进程。如果是 cgroup OOM,它会在特定 cgroup 内的进程中选择 OOM victim;如果是系统级 OOM,它会在整个系统中选择。

这个函数是通过调用oom_evaluate_task() 函数来评估每个进程的适合性,那接下来我们就看oom_evaluate_task() 函数。

oom_evaluate_task()

主要用于于在 OOM(Out of Memory)事件发生时评估每个进程的“坏度”,并根据条件选择合适的进程作为 OOM victim(即将被终止的进程)。该函数通过对每个进程的评估,判断是否应该终止它以释放内存资源。

static int oom_evaluate_task(struct task_struct *task, void *arg)
{
	struct oom_control *oc = arg;  // 将传递的参数转换为 oom_control 结构
	long points;

	// 检查进程是否不能被杀死(如内核进程、OOM 免疫进程等),如果不能杀死则跳过该进程
	if (oom_unkillable_task(task))
		goto next;

	// 检查当前进程是否在分配内存时符合 cpuset 的限制,如果不符合,则跳过
	if (!is_memcg_oom(oc) && !oom_cpuset_eligible(task, oc))
		goto next;

	/*
	 * 如果当前任务已经是 OOM victim 并且可以使用系统的内存保留区,
	 * 那么不再选择其他任务,除非该任务设置了 MMF_OOM_SKIP 标志,
	 * 表示它可能不会释放内存。
	 */
	if (!is_sysrq_oom(oc) && tsk_is_oom_victim(task)) {
		// 如果进程标记为 MMF_OOM_SKIP,表示可能不会释放内存,跳过该进程
		if (test_bit(MMF_OOM_SKIP, &task->signal->oom_mm->flags))
			goto next;
		// 如果进程已经是 OOM victim,直接终止选择,避免多次选择同一进程
		goto abort;
	}

	/*
	 * 如果进程是导致 OOM 事件发生的进程,并且已标记为优先被杀死,
	 * 那么立即选择该进程作为 OOM victim。
	 */
	if (oom_task_origin(task)) {
		points = LONG_MAX;  // 给该任务分配最高的“坏度”评分
		goto select;  // 直接选择该任务为 OOM victim
	}

	// 计算该进程的坏度得分,得分越高表示越有可能被选为 OOM victim
	points = oom_badness(task, oc->totalpages);
	
	// 如果进程的坏度得分为 LONG_MIN,或者得分小于已选中的进程,则跳过该进程
	if (points == LONG_MIN || points < oc->chosen_points)
		goto next;

select:
	// 如果已经选择了一个进程,释放该进程的引用
	if (oc->chosen)
		put_task_struct(oc->chosen);
	// 增加当前任务的引用计数,防止该任务在处理期间被销毁
	get_task_struct(task);
	// 选择当前任务为 OOM victim
	oc->chosen = task;
	oc->chosen_points = points;  // 记录当前任务的坏度得分

next:
	// 继续评估下一个任务
	return 0;

abort:
	// 如果已经选择了一个进程,但现在要终止选择,将其引用释放
	if (oc->chosen)
		put_task_struct(oc->chosen);
	// 将 oc->chosen 设为 -1UL,表示无法再继续选择其他任务
	oc->chosen = (void *)-1UL;
	return 1;  // 中止任务评估
}

在 OOM 事件发生时,评估每个进程是否应该被终止。它根据进程的内存使用、优先级等因素打分,并选择最合适的 OOM victim。

任务筛选机制:该函数会优先跳过系统关键任务或不可被杀死的任务,并确保在评估任务时不会重复选择已经标记为 OOM victim 的任务。

评分系统:使用 oom_badness() 函数对任务进行评分,分数高的任务更有可能被选为 OOM victim,并最终被终止以释放内存。

那接下来我们看看评分函数oom_badness()

oom_badness()

long oom_badness(struct task_struct *p, unsigned long totalpages)
{
	long points;
	long adj;

	// 检查任务是否不可被杀死(如内核任务、系统关键任务等)
	if (oom_unkillable_task(p))
		return LONG_MIN;  // 如果任务不能被杀死,返回最小值表示不考虑该任务

	// 获取并锁定任务的内存描述符(mm_struct),如果没有,则跳过
	p = find_lock_task_mm(p);
	if (!p)
		return LONG_MIN;  // 如果任务没有内存描述符,则返回最小值

	/*
	 * 检查进程是否被标记为 OOM 免疫,或已被 OOM reaper 处理过,
	 * 或者是否正在进行 vfork 操作。如果是,跳过该任务。
	 */
	adj = (long)p->signal->oom_score_adj;  // 获取进程的 OOM 优先级调整值
	if (adj == OOM_SCORE_ADJ_MIN ||  // 如果进程的 OOM 调整分数是最低的,跳过
			test_bit(MMF_OOM_SKIP, &p->mm->flags) ||  // 如果进程已被 OOM 跳过,跳过
			in_vfork(p)) {  // 如果进程正在 vfork,跳过
		task_unlock(p);
		return LONG_MIN;  // 跳过该任务
	}

	/*
	 * 基于任务的内存使用量计算基础分数,包括 RSS(驻留集大小)、页面表项大小
	 * 和 swap 空间使用情况。它们与 RAM 使用成比例。
	 */
	points = get_mm_rss(p->mm) +  // 获取任务使用的物理内存页数(RSS)
		get_mm_counter(p->mm, MM_SWAPENTS) +  // 获取任务的交换空间使用量
		mm_pgtables_bytes(p->mm) / PAGE_SIZE;  // 获取任务的页面表大小并换算成页
	task_unlock(p);  // 释放任务的锁

	/* 根据进程的 oom_score_adj 值进行调整,将其规范化到 totalpages/1000 单位 */
	adj *= totalpages / 1000;  // 将调整值归一化到物理内存的比例
	points += adj;  // 将调整值加入到得分中

	// 返回计算后的分数,分数越高的进程越有可能被 OOM killer 终止
	return points;
}

跟上面的oom_evaluate_task()函数一样先检查是否被标记为不可被 OOM killer 杀死。

目的是计算每个进程在系统 OOM 事件中的“坏度”得分,决定哪些进程占用的资源最多,从而优先选择它们作为 OOM victim 以释放内存。

它根据进程的内存使用量(RSS、交换空间、页表大小)来计算基础得分,并根据 oom_score_adj 进行调整。

该函数确保不会选择不可杀死的进程,并根据任务的实际内存使用量进行合理评估。

这个函数只是计算基础得分,算出之后再根据 oom_score_adj 进行调整。不得不说,操作系统还是很严谨的。

Oom killer通过这个oom_badness函数进行打分,返回值是根据一定策略给进程打的分数,后续oom killer根据该分数高低选择出最该杀死的那个进程(分数越高越优先杀死),这里需要注意3个点:

  1. adj == OOM_SCORE_ADJ_MIN时,说明该进程已被设置为不可被杀死进程,返回的得分将无限低(LONG_MIN)。
  2. points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + mm_pgtables_bytes(p->mm) / PAGE_SIZE;分数公式,分数是由这三部分计算打出:进程所占用的内存中的空间、SWAP所占用的空间、page cache里所占用的空间 ;
  3. adj *= totalpages / 1000; points += adj; 分数另一部分的构成是这个oom_score_adj,这个是配置在内核文件的,范围是-1000~1000,默认是0,所以oom_badness先把该分数归一化,再做加法。

我们可以在内核的 include/linux/sched/signal.h 头文件中看到,它是 signal_struct 结构体中的一个字段。

struct signal_struct {
    ...
    int oom_score_adj;          // OOM 优先级调整值
    int oom_score_adj_min;      // 调整值的最小范围(通常是 -1000)
    ...
};

signal_struct 结构体用于表示与进程的信号处理相关的信息。

好了,通过这个函数我们就可以知道得分越低的进程会优先被杀死,不一定是触发OOM killer的进程。

计算完之后就会调用oom_kill_process()进行杀死进程

oom_kill_process()

static void oom_kill_process(struct oom_control *oc, const char *message)
{
	struct task_struct *victim = oc->chosen;  // 获取被选中的 OOM victim 进程
	struct mem_cgroup *oom_group;  // 指向 OOM 相关的 memory cgroup
	static DEFINE_RATELIMIT_STATE(oom_rs, DEFAULT_RATELIMIT_INTERVAL,
					      DEFAULT_RATELIMIT_BURST);  // 定义速率限制器

	/*
	 * 如果进程已经在退出过程中,不需要再次杀死它。
	 * 只需给它访问系统的内存保留区,以便让它能够快速结束并释放内存。
	 */
	task_lock(victim);
	if (task_will_free_mem(victim)) {  // 检查进程是否会主动释放内存(如正在退出)
		mark_oom_victim(victim);  // 将进程标记为 OOM victim,允许其访问内存保留区
		wake_oom_reaper(victim);  // 唤醒 OOM reaper 线程,确保进程快速释放内存
		task_unlock(victim);  // 释放进程锁
		put_task_struct(victim);  // 释放任务的引用计数
		return;  // 不再进一步处理该进程
	}
	task_unlock(victim);  // 释放进程锁

	// 使用速率限制机制,控制 OOM 日志输出的频率,避免大量日志淹没系统
	if (__ratelimit(&oom_rs))
		dump_header(oc, victim);  // 打印 OOM 事件的头信息,包含 victim 的信息

	/*
	 * 检查是否需要杀死整个 memory cgroup 或其父 cgroup。
	 * 在实际终止进程之前先进行这一步操作。
	 */
	oom_group = mem_cgroup_get_oom_group(victim, oc->memcg);  // 获取需要杀死的 memory cgroup

	// 实际终止进程
	__oom_kill_process(victim, message);

	/*
	 * 如果必要,杀死整个 memory cgroup 中的所有进程。
	 * 如果 memory cgroup 被标记为 OOM,则杀死 cgroup 内的所有任务。
	 */
	if (oom_group) {
		mem_cgroup_print_oom_group(oom_group);  // 打印 memory cgroup 中的信息
		mem_cgroup_scan_tasks(oom_group, oom_kill_memcg_member, (void*)message);  // 遍历并杀死 cgroup 中的所有进程
		mem_cgroup_put(oom_group);  // 释放对 cgroup 的引用
	}
}

在 OOM 事件发生时,内核通过这个函数选择并终止一个进程,同时检查是否需要终止整个 memory cgroup。

关键功能

  • 检查进程是否正在退出并避免重复终止;
  • 使用速率限制输出日志;
  • 如果需要,处理 memory cgroup 相关的进程清理;
  • 发送 SIGKILL 信号,确保进程被强制终止;
  • 唤醒 OOM reaper 来加速内存回收。

相关函数

mark_oom_victim():标记进程为 OOM victim,使其能够获得对内存的访问权,确保它能被终止。

wake_oom_reaper():唤醒 OOM reaper 线程,清理被杀死进程的内存,避免系统陷入内存不足的死循环。

do_send_sig_info():发送 SIGKILL 信号给被选中的进程,强制终止它。

总结

从内存分配函数alloc_pages() 一直到进程杀死函数oom_kill_process(),我们知道了,从内存分配到触发oomkiller,再到选择函数,这个过程其实是很严谨的一个过程,当触发oomkiller之后,操作系统并不会随便选择一个进程进行杀死,通过对系统中的进程进行筛选,打分之后再选择一个进程进行杀死。

Linux提供了一个策略,可以让用户通过填写oom_score_adj文件来影响oom killer的选择,当你填-1000时,则表示该进程将不会被杀死,但如果你填写的是非-1000,那这个进程还是会参与打分,但会受到oom_score_adj的影响,比如oom_score_adj你填了-3,当你的进程消耗内存很大时,同样大概率会被杀死。还有一个值得注意的是,oom killer选择策略,只受进程占用内存和oom_score_adj的影响,至于该进程是否是短时间内快速吃掉大量内存,oom killer并不关心

系统也希望有一个策略能完美地选出该杀的进程,但现实上却没有这么优秀的算法,跑在OS上的进程成千上万,我们怎么能这么有把握选出的进程必然是正确的,只是选出一个比较合适的而已,误杀在所难免。

避免oomkiller的方案

避免oom killer的方案

1. 直接修改/proc//oom_score_adj文件,将其置为-1000

以前是通过/proc//oom_score来控制的,但近年来新版linux已经使用oom_score_adj来代替旧版的oom_score,

2. 直接关闭oom-killer

# echo "0" > /proc/sys/vm/oom-kill 关闭 # echo "1″ > /proc/sys/vm/oom-kill 激活
Android内存机制在不同版本之间有着较大的演变,其中最主要的就是内存管理策略的变化。在早期的Android版本中,内存管理采用的是进程优先级的方式,即根据进程的重要性和消耗的资源量来确定内存使用的权重,但这种方式容易导致内存不足的情况,进而导致应用程序崩溃。 后来,Google在Android 2.0中引入了LowMemoryKiller机制,通过监控系统内存使用情况,当内存不足时自动清理不必要的进程,以释放内存资源。LowMemoryKiller机制的实现是通过kernel的oom-killer机制来实现的,当系统内存不足时,通过oom_adj值来判断哪些进程可以杀掉,以释放内存资源。在Android 2.x中,LowMemoryKiller机制主要依赖于进程oom_adj值的设置,以及进程的重要性和消耗的资源量来判断哪些进程可以被杀掉。 随着Android版本的不断升级,Google也对LowMemoryKiller机制进行了多次优化,主要包括: 1. Android 3.0中引入了memcg机制,通过将进程的内存资源划分为多个cgroup,实现对内存资源的精细化管理。 2. Android 4.0中引入了Lmkd机制,通过对进程的内存资源进行动态调整,以更加精准地释放内存资源。 3. Android 4.4中引入了Zram机制,通过将一部分物理内存作为压缩内存使用,提高内存使用效率。 4. Android 6.0中引入了Doze机制,通过限制应用程序的后台运行,以降低系统内存负载。 总的来说,Android的内存管理机制是不断演变和优化的过程,不断追求更加高效和精细化的内存管理方式,以保证系统的稳定性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值