分析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_progress
为 0
(即之前的内存回收没有进展),也会返回 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个点:
adj == OOM_SCORE_ADJ_MIN
时,说明该进程已被设置为不可被杀死进程,返回的得分将无限低(LONG_MIN)。points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + mm_pgtables_bytes(p->mm) / PAGE_SIZE;
分数公式,分数是由这三部分计算打出:进程所占用的内存中的空间、SWAP所占用的空间、page cache里所占用的空间 ;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 激活