OOM reaper는 어느 VMA를 비울 수 있을까요?

OOM log에 reaped가 보이더라도 process의 모든 RSS와 kernel 자원이 즉시 반환된 것은 아닙니다. reaper는 제한된 lock과 비차단 protocol로 가능한 mapping을 비우는 도구입니다. 대상 VMA의 조건과 physical folio가 실제로 사라지는 조건을 나눠 살펴보겠습니다.

먼저 contents가 불안정해졌음을 표시합니다

__oom_reap_task_mm()는 mm에 MMF_UNSTABLE을 설정합니다. reaping이 시작된 private memory를 나중 fault에서 정상 zero-filled allocation으로 되살리면 원래 내용을 복구한 것처럼 보일 수 있습니다. 관련 fault 경로의 check_stable_address_space()는 이런 상태에서 VM_FAULT_SIGBUS를 반환합니다.

이 flag는 모든 kernel 접근을 단 하나의 검사로 보호하는 만능 barrier가 아닙니다. user access가 fault를 거치는 방식과 각 memory-management 경로의 protocol을 함께 보아야 합니다. descriptor가 살아 있다고 내용도 안정적이라는 가정은 사용할 수 없습니다.

VMA filter의 순서가 중요합니다

v6.18에서는 Maple Tree를 높은 주소에서 낮은 주소로 순회합니다. 정상 exit_mmap()의 낮은 주소부터 시작하는 unmap과 반대쪽에서 진행하여 같은 page-table lock을 두고 경쟁하는 일을 줄이려는 것입니다. 먼저 VM_HUGETLB와 VM_PFNMAP을 제외합니다. 전자는 전용 hugepage 관리이고 후자는 일반 RAM folio ownership으로 다룰 수 없는 mapping일 수 있습니다. anonymous THP 전체를 hugetlb와 같은 예외로 묶으면 안 됩니다.

그다음 조건은 vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED)입니다. anonymous이거나 shared가 아닌 mapping입니다. 따라서 private file mapping도 대상에 들어갈 수 있습니다. 'anonymous page만 reap한다'는 요약은 이 조건을 충분히 설명하지 못합니다.

VMA filter 통과와 physical free 가능성을 분리한 표입니다. private file에는 file cache와 CoW anon 내용이 섞일 수 있고, pin이나 다른 mapping 때문에 PTE 제거 뒤에도 folio가 남을 수 있습니다.
VMA filter 통과와 physical free 가능성을 분리한 표입니다. private file에는 file cache와 CoW anon 내용이 섞일 수 있고, pin이나 다른 mapping 때문에 PTE 제거 뒤에도 folio가 남을 수 있습니다.

ordinary shared file과 shared shmem은 이 filter에서 제외됩니다. private file은 file-cache reference가 남을 수도 있고 CoW로 생긴 anon page가 해제될 수도 있습니다. VMA의 file-backed 여부 하나만으로 그 안의 모든 PTE가 같은 종류의 folio를 가진다고 가정해서는 안 됩니다.

Notifier가 막으면 일부만 처리할 수도 있습니다

각 eligible VMA에 대해 MMU notifier range와 mmu_gather를 준비합니다. mmu_notifier_invalidate_range_start_nonblock()이 실패하면 무기한 기다리지 않고 그 range를 건너뛰며 전체 결과를 실패로 남깁니다. 다른 VMA는 계속 처리할 수 있으므로 false는 아무것도 하지 않았다는 뜻만은 아닙니다.

성공한 range는 unmap_page_range()로 PTE를 제거하고 notifier end와 tlb_finish_mmu()로 정리합니다. stale TLB translation이 memory 재사용 이후까지 남지 않도록 하는 protocol입니다. folio를 free할 수 있는지와 별개로 CPU와 secondary MMU의 관찰을 맞춰야 합니다.

이후 caller는 일부 처리가 남았다면 제한된 retry를 할 수 있습니다. VMA filter를 통과했다는 사실, mmap read lock을 얻었다는 사실, notifier가 허용했다는 사실, 실제 folio가 free되었다는 사실은 네 가지 다른 조건입니다.

mlock과 pin은 같은 보호가 아닙니다

VM_LOCKED는 일반 pressure reclaim에서 중요하지만 이 reaper filter의 단독 제외 조건은 아닙니다. dying address space를 비우는 작업은 일반 LRU eviction과 목적이 다릅니다. mlock을 OOM reaper에 대한 절대 보존 기능으로 사용할 수 없습니다.

반면 FOLL_PIN 같은 외부 reference는 PTE를 지워도 physical folio를 계속 붙잡을 수 있습니다. reaper는 VMA를 순회하며 user mapping을 제거하지 외부 subsystem의 DMA pin을 강제로 몰수하지 않습니다. file cache나 다른 address space의 mapping도 folio 수명에 영향을 줍니다.

Userfaultfd와 실제 exit도 별도로 남습니다

userfaultfd가 있다는 이유만으로 VMA 전체가 이 filter에서 제외되지는 않습니다. 관련 missing-fault가 unstable private memory를 정상 allocation으로 복구하려 할 때 fault 경로의 stable-address-space 검사가 영향을 줍니다. handler의 응답을 기다리는 일반 fault protocol과 OOM reaper의 nonblocking unmap을 하나로 섞어서는 안 됩니다.

OOM 뒤의 결과를 읽을 때에는 SIGKILL 전송, 정상 exit, reaper가 성공한 range, notifier나 lock 때문에 생략된 range, 최종 reference 반환을 분리해야 합니다. reaper의 성공은 정한 범위에서 가능한 unmap을 수행했다는 뜻이며, system pressure가 반드시 해소되었다거나 process의 모든 resource가 사라졌다는 보장은 아닙니다.

전체 unmap 함수에서 filter와 notifier를 같이 읽습니다

다음은 official Linux v6.0과 v6.18의 전체 reap 함수입니다. linked-list VMA 순회와 reverse Maple Tree 순회를 나누어 비교하실 수 있습니다. 둘 다 upstream notice와 GPL-2.0을 유지했습니다.

v6_0___oom_reap_task_mm.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/oom_kill.c
 * 
 *  Copyright (C)  1998,2000  Rik van Riel
 *	Thanks go out to Claus Fischer for some serious inspiration and
 *	for goading me into coding this file...
 *  Copyright (C)  2010  Google, Inc.
 *	Rewritten by David Rientjes
 *
 *  The routines in this file are used to kill a process when
 *  we're seriously out of memory. This gets called from __alloc_pages()
 *  in mm/page_alloc.c when we really run out of memory.
 *
 *  Since we won't call these routines often (on a well-configured
 *  machine) this file will double as a 'coding guide' and a signpost
 *  for newbie kernel hackers. It features several pointers to major
 *  kernel subsystems and hints as to where to find out what things do.
 */

/*
 * Official Linux v6.0: mm/oom_kill.c
 * https://github.com/torvalds/linux/blob/v6.0/mm/oom_kill.c#L512
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

bool __oom_reap_task_mm(struct mm_struct *mm)
{
	struct vm_area_struct *vma;
	bool ret = true;

	/*
	 * Tell all users of get_user/copy_from_user etc... that the content
	 * is no longer stable. No barriers really needed because unmapping
	 * should imply barriers already and the reader would hit a page fault
	 * if it stumbled over a reaped memory.
	 */
	set_bit(MMF_UNSTABLE, &mm->flags);

	for (vma = mm->mmap ; vma; vma = vma->vm_next) {
		if (vma->vm_flags & (VM_HUGETLB|VM_PFNMAP))
			continue;

		/*
		 * Only anonymous pages have a good chance to be dropped
		 * without additional steps which we cannot afford as we
		 * are OOM already.
		 *
		 * We do not even care about fs backed pages because all
		 * which are reclaimable have already been reclaimed and
		 * we do not want to block exit_mmap by keeping mm ref
		 * count elevated without a good reason.
		 */
		if (vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED)) {
			struct mmu_notifier_range range;
			struct mmu_gather tlb;

			mmu_notifier_range_init(&range, MMU_NOTIFY_UNMAP, 0,
						vma, mm, vma->vm_start,
						vma->vm_end);
			tlb_gather_mmu(&tlb, mm);
			if (mmu_notifier_invalidate_range_start_nonblock(&range)) {
				tlb_finish_mmu(&tlb);
				ret = false;
				continue;
			}
			unmap_page_range(&tlb, vma, range.start, range.end, NULL);
			mmu_notifier_invalidate_range_end(&range);
			tlb_finish_mmu(&tlb);
		}
	}

	return ret;
}

v6_18___oom_reap_task_mm.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/oom_kill.c
 *
 *  Copyright (C)  1998,2000  Rik van Riel
 *	Thanks go out to Claus Fischer for some serious inspiration and
 *	for goading me into coding this file...
 *  Copyright (C)  2010  Google, Inc.
 *	Rewritten by David Rientjes
 *
 *  The routines in this file are used to kill a process when
 *  we're seriously out of memory. This gets called from __alloc_pages()
 *  in mm/page_alloc.c when we really run out of memory.
 *
 *  Since we won't call these routines often (on a well-configured
 *  machine) this file will double as a 'coding guide' and a signpost
 *  for newbie kernel hackers. It features several pointers to major
 *  kernel subsystems and hints as to where to find out what things do.
 */

/*
 * Official Linux v6.18: mm/oom_kill.c
 * https://github.com/torvalds/linux/blob/v6.18/mm/oom_kill.c#L515
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static bool __oom_reap_task_mm(struct mm_struct *mm)
{
	struct vm_area_struct *vma;
	bool ret = true;
	MA_STATE(mas, &mm->mm_mt, ULONG_MAX, ULONG_MAX);

	/*
	 * Tell all users of get_user/copy_from_user etc... that the content
	 * is no longer stable. No barriers really needed because unmapping
	 * should imply barriers already and the reader would hit a page fault
	 * if it stumbled over a reaped memory.
	 */
	mm_flags_set(MMF_UNSTABLE, mm);

	/*
	 * It might start racing with the dying task and compete for shared
	 * resources - e.g. page table lock contention has been observed.
	 * Reduce those races by reaping the oom victim from the other end
	 * of the address space.
	 */
	mas_for_each_rev(&mas, vma, 0) {
		if (vma->vm_flags & (VM_HUGETLB|VM_PFNMAP))
			continue;

		/*
		 * Only anonymous pages have a good chance to be dropped
		 * without additional steps which we cannot afford as we
		 * are OOM already.
		 *
		 * We do not even care about fs backed pages because all
		 * which are reclaimable have already been reclaimed and
		 * we do not want to block exit_mmap by keeping mm ref
		 * count elevated without a good reason.
		 */
		if (vma_is_anonymous(vma) || !(vma->vm_flags & VM_SHARED)) {
			struct mmu_notifier_range range;
			struct mmu_gather tlb;

			mmu_notifier_range_init(&range, MMU_NOTIFY_UNMAP, 0,
						mm, vma->vm_start,
						vma->vm_end);
			tlb_gather_mmu(&tlb, mm);
			if (mmu_notifier_invalidate_range_start_nonblock(&range)) {
				tlb_finish_mmu(&tlb);
				ret = false;
				continue;
			}
			unmap_page_range(&tlb, vma, range.start, range.end, NULL);
			mmu_notifier_invalidate_range_end(&range);
			tlb_finish_mmu(&tlb);
		}
	}

	return ret;
}

고전 구현의 ascending list 순서를 v6.18에 그대로 옮기면 안 됩니다. v6.18은 높은 주소 쪽에서 시작해 정상 exit와 page-table lock을 두고 충돌할 가능성을 줄입니다. 하지만 순서를 바꾼 것이 exclusive ownership이나 모든 range의 성공을 보장하는 것은 아닙니다.

각 eligible range는 notifier를 nonblocking으로 시작합니다. 거절되면 이미 준비한 mmu_gather는 finish하되, 성공하지 않은 start에 대응하는 notifier end나 PTE unmap은 수행하지 않습니다. 전체 ret는 false로 남기고 다른 VMA로 계속 갑니다. 성공한 range만 unmap·notifier end·TLB finish를 순서대로 처리합니다. 반환값 true도 filter에서 제외된 VMA까지 비웠다는 뜻은 아닙니다.

Private file의 unmap을 file writeback으로 오해하지 않습니다

private file VMA에는 원래 file-cache page와 write fault로 생긴 CoW anon page가 함께 있을 수 있습니다. reaper는 해당 VMA의 user mapping을 비웁니다. file-cache page의 cache reference를 직접 제거하거나 dirty file data의 writeback까지 완료시키는 함수는 아닙니다. 반대로 CoW anon page도 다른 mapping·pin이 남으면 physical free가 미뤄질 수 있습니다.

anonymous THP는 VM_HUGETLB의 전용 pool huge page와 다릅니다. 일반 anonymous 조건을 만족하면 대상이 될 수 있습니다. VM_LOCKED나 userfaultfd 등록만으로 이 filter에서 제외되지 않는다는 점도 normal reclaim의 mlock 조건과 분리해서 보셔야 합니다.

v6_18_check_stable_address_space.c

/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __INCLUDE_LINUX_OOM_H
#define __INCLUDE_LINUX_OOM_H

/*
 * Official Linux v6.18: include/linux/oom.h
 * https://github.com/torvalds/linux/blob/v6.18/include/linux/oom.h#L92
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static inline vm_fault_t check_stable_address_space(struct mm_struct *mm)
{
	if (unlikely(mm_flags_test(MMF_UNSTABLE, mm)))
		return VM_FAULT_SIGBUS;
	return 0;
}

이 helper는 unstable mm의 관련 fault에 SIGBUS 결과를 돌려줍니다. 모든 fault·user-access 경로를 하나로 바꾸는 명령이 아니라, 해당 helper를 사용하는 allocation·fault protocol의 검사입니다. reaper가 PTE를 제거했다는 사실과 이후 정상 exit, page-table free, 외부 reference의 해제가 끝났다는 사실을 따로 보아야 합니다.

참고 자료