OOM reaper는 왜 잠깐 기다린 뒤 trylock할까요?

SIGKILL을 받은 process는 정상 exit를 통해 memory와 다른 자원을 정리할 수 있습니다. OOM reaper가 즉시 모든 mapping을 비우면 robust futex 등 exit에 필요한 user 내용까지 먼저 사라질 수 있습니다. 반대로 task가 응답하지 않는다면 memory pressure를 오래 방치할 수도 없습니다.

Queue에 넣기 전에 정상 exit에 기회를 줍니다

CONFIG_MMU 경로의 oom_init()은 전용 reaper thread를 준비합니다. queue_oom_reaper()는 mm의 MMF_OOM_REAP_QUEUED를 test-and-set해 같은 mm의 중복 예약을 막습니다. 새 예약이면 task reference를 얻고 timer를 설정합니다.

v6.18의 OOM_REAPER_DELAY는 2 * HZ입니다. 이것은 정상 exit를 기다려 보는 heuristic이지 OOM 처리의 전체 완료 deadline이 아닙니다. timer가 만료되어야 queue에 실제 연결되는 구조이므로 예약과 thread의 작업 시작도 같은 시점이 아닙니다.

Task reference와 mm reference의 주인은 다릅니다

selection이 얻은 task reference는 kill 경로가 사용한 뒤 반환합니다. reaper 예약은 자신의 task reference를 별도로 얻습니다. signal object의 oom_mm는 mmgrab()으로 mm descriptor를 붙잡고, signal lifetime이 끝날 때 그 보유분을 정리합니다. 하나의 refcount가 모두를 대신하지 않습니다.

kill 경로가 shared-mm 비교를 위해 임시로 얻은 mmgrab()은 __oom_kill_process() 끝의 mmdrop()과 대응합니다. 반면 signal->oom_mm에 최초 등록하며 얻은 보유분은 더 오래 남아 v6.18의 free_signal_struct()에서 mmdrop_async()로 정리됩니다. queue용 task reference가 signal object를, signal object가 이 mm descriptor를 유지하므로 timer와 reaper가 나중에 안전하게 참조할 수 있습니다.

이 구조에서 task는 이미 task->mm를 놓았을 수 있지만 reaper는 signal의 oom_mm를 확인할 수 있습니다. descriptor가 살아 있다는 사실도 그 안의 mapping과 physical contents가 그대로 살아 있다는 뜻은 아닙니다. mm_count와 mm_users를 분리해야 하는 이유입니다.

victim marking·timer·reaper와 mm descriptor lifetime의 관계를 단순화한 독립 도식입니다. PTE를 지워도 외부 reference가 folio를 계속 유지할 수 있습니다.
victim marking·timer·reaper와 mm descriptor lifetime의 관계를 단순화한 독립 도식입니다. PTE를 지워도 외부 reference가 folio를 계속 유지할 수 있습니다.

Timer callback은 끝난 mm를 queue에 넣지 않습니다

wake_oom_reaper()는 먼저 SKIP을 확인합니다. 정상 exit가 exit_mmap()의 해당 단계까지 진행했다면 reaper의 추가 작업이 필요 없거나 안전하지 않을 수 있습니다. 이 경우 예약이 보유한 task reference를 내리고 끝냅니다.

그렇지 않으면 oom_reaper_lock 아래 list head에 task를 연결하고 wait queue를 깨웁니다. head에 넣고 head에서 꺼내므로 엄격한 FIFO 처리 순서를 약속하는 queue는 아닙니다. spinlock은 이 연결을 보호하며 실제 VMA unmap 전체를 감싼 lock이 아닙니다.

mmap_lock을 무기한 기다리지 않습니다

reaper thread는 freezable wait queue에서 작업을 기다리고, 하나를 꺼내 oom_reap_task()로 처리합니다. oom_reap_task_mm()은 mmap_read_trylock()을 사용합니다. write-side holder가 memory를 기다리며 멈춰 있다면 reaper까지 blocking으로 묶는 것은 문제를 키울 수 있기 때문입니다.

lock을 얻은 뒤 SKIP을 다시 확인합니다. 바깥 timer에서 한 번 확인했더라도 그 사이 exit_mmap()이 진행할 수 있습니다. mmap write-side teardown과 serializing하는 read lock 아래 재검사해야 그 변경을 올바르게 봅니다.

Retry 뒤에는 성공하지 못해도 SKIP일 수 있습니다

v6.18 reaper는 최대 10번 시도하고 실패 후 HZ/10 간격으로 대기할 수 있습니다. 잠드는 task는 victim이 아니라 reaper입니다. scheduler 지연·lock 경쟁·notifier 실패까지 있으므로 '정확히 3초 후 모든 RAM 반환' 같은 시간을 계산할 수 없습니다.

성공하거나 이미 SKIP이면 마치고, 재시도를 소진하면 진단 정보를 남길 수 있습니다. 어느 경우든 후속 OOM이 같은 mm에 무의미하게 매달리지 않도록 SKIP을 설정하고 list 연결과 queue-held task reference를 정리합니다.

따라서 SKIP은 전체 memory 해제의 영수증이 아닙니다. 이미 reaped했을 수도 있고, lock이나 다른 조건 때문에 더 이상 이 경로에서 유망하지 않을 수도 있습니다. 이후 실제 exit와 다른 reference의 해제가 남아 있을 수 있습니다.

Timer에서 worker로 넘기는 전체 code

다음은 CONFIG_MMU가 있는 Linux v6.18 경로의 state declaration, 초기화, delay와 queue·wake 함수입니다. CONFIG_MMU가 없으면 같은 queue 함수는 비어 있는 stub이므로 이 동작을 모든 configuration에 적용하지 않습니다. upstream notice와 GPL-2.0을 유지했습니다.

v6_18_reaper_state_declarations.h

// 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#L510
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static struct task_struct *oom_reaper_th;
static DECLARE_WAIT_QUEUE_HEAD(oom_reaper_wait);
static struct task_struct *oom_reaper_list;
static DEFINE_SPINLOCK(oom_reaper_lock);

v6_18_oom_init.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#L741
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static int __init oom_init(void)
{
	oom_reaper_th = kthread_run(oom_reaper, NULL, "oom_reaper");
#ifdef CONFIG_SYSCTL
	register_sysctl_init("vm", vm_oom_kill_table);
#endif
	return 0;
}

v6_18_reaper_delay.h

// 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#L700
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

#define OOM_REAPER_DELAY (2*HZ)

v6_18_queue_oom_reaper.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#L701
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static void queue_oom_reaper(struct task_struct *tsk)
{
	/* mm is already queued? */
	if (mm_flags_test_and_set(MMF_OOM_REAP_QUEUED, tsk->signal->oom_mm))
		return;

	get_task_struct(tsk);
	timer_setup(&tsk->oom_reaper_timer, wake_oom_reaper, 0);
	tsk->oom_reaper_timer.expires = jiffies + OOM_REAPER_DELAY;
	add_timer(&tsk->oom_reaper_timer);
}

v6_18_wake_oom_reaper.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#L671
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static void wake_oom_reaper(struct timer_list *timer)
{
	struct task_struct *tsk = container_of(timer, struct task_struct,
			oom_reaper_timer);
	struct mm_struct *mm = tsk->signal->oom_mm;
	unsigned long flags;

	/* The victim managed to terminate on its own - see exit_mmap */
	if (mm_flags_test(MMF_OOM_SKIP, mm)) {
		put_task_struct(tsk);
		return;
	}

	spin_lock_irqsave(&oom_reaper_lock, flags);
	tsk->oom_reaper_list = oom_reaper_list;
	oom_reaper_list = tsk;
	spin_unlock_irqrestore(&oom_reaper_lock, flags);
	trace_wake_reaper(tsk->pid);
	wake_up(&oom_reaper_wait);
}

예약 flag는 mm에 있고 timer는 task에 있습니다. 같은 mm가 이미 queued라면 task reference를 새로 얻지 않습니다. 새 예약만 get을 수행하므로 callback의 SKIP early return과 실제 worker 종료 중 정확히 한 경로가 그 queue reference를 put해야 합니다. timer callback이 list에 넘긴 reference를 바로 put하면 worker가 사용할 task의 수명이 사라질 수 있습니다.

oom_reaper_lock은 head push·pop 구간을 보호합니다. VMA를 비우는 동안 계속 잡고 있으면 안 됩니다. 아래 worker는 lock 안에서 task 하나를 꺼내고 lock 밖에서 긴 reap을 수행합니다. freezable wait는 list 조건이 이미 참이면 매번 새 wakeup을 기다리는 계약이 아닙니다.

v6_18_oom_reaper.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#L649
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static int oom_reaper(void *unused)
{
	set_freezable();

	while (true) {
		struct task_struct *tsk = NULL;

		wait_event_freezable(oom_reaper_wait, oom_reaper_list != NULL);
		spin_lock_irq(&oom_reaper_lock);
		if (oom_reaper_list != NULL) {
			tsk = oom_reaper_list;
			oom_reaper_list = tsk->oom_reaper_list;
		}
		spin_unlock_irq(&oom_reaper_lock);

		if (tsk)
			oom_reap_task(tsk);
	}

	return 0;
}

v6_18_reaper_retry_limit.h

// 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#L617
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

#define MAX_OOM_REAP_RETRIES 10

v6_18_oom_reap_task.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#L618
 * Complete upstream scope, not an elided book-code reconstruction.
 * See GPL-2.0.txt. Not a standalone translation unit.
 */

static void oom_reap_task(struct task_struct *tsk)
{
	int attempts = 0;
	struct mm_struct *mm = tsk->signal->oom_mm;

	/* Retry the mmap_read_trylock(mm) a few times */
	while (attempts++ < MAX_OOM_REAP_RETRIES && !oom_reap_task_mm(tsk, mm))
		schedule_timeout_idle(HZ/10);

	if (attempts <= MAX_OOM_REAP_RETRIES ||
	    mm_flags_test(MMF_OOM_SKIP, mm))
		goto done;

	pr_info("oom_reaper: unable to reap pid:%d (%s)\n",
		task_pid_nr(tsk), tsk->comm);
	sched_show_task(tsk);
	debug_show_all_locks();

done:
	tsk->oom_reaper_list = NULL;

	/*
	 * Hide this mm from OOM killer because it has been either reaped or
	 * somebody can't call mmap_write_unlock(mm).
	 */
	mm_flags_set(MMF_OOM_SKIP, mm);

	/* Drop a reference taken by queue_oom_reaper */
	put_task_struct(tsk);
}

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#L577
 * 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 task_struct *tsk, struct mm_struct *mm)
{
	bool ret = true;

	if (!mmap_read_trylock(mm)) {
		trace_skip_task_reaping(tsk->pid);
		return false;
	}

	/*
	 * MMF_OOM_SKIP is set by exit_mmap when the OOM reaper can't
	 * work on the mm anymore. The check for MMF_OOM_SKIP must run
	 * under mmap_lock for reading because it serializes against the
	 * mmap_write_lock();mmap_write_unlock() cycle in exit_mmap().
	 */
	if (mm_flags_test(MMF_OOM_SKIP, mm)) {
		trace_skip_task_reaping(tsk->pid);
		goto out_unlock;
	}

	trace_start_task_reaping(tsk->pid);

	/* failed to reap part of the address space. Try again later */
	ret = __oom_reap_task_mm(mm);
	if (!ret)
		goto out_finish;

	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)));
out_finish:
	trace_finish_task_reaping(tsk->pid);
out_unlock:
	mmap_read_unlock(mm);

	return ret;
}

retry loop의 post-increment 때문에 진단에서 attempts가 한 번 더 증가한 값을 볼 수 있지만, 실제 helper 호출은 최대 10번입니다. 실패한 시도 뒤 잠드는 주체는 reaper이고 성공한 호출마다 무조건 0.1초를 추가하는 것은 아닙니다. SKIP을 read lock 안에서 확인해 이미 exit가 진행한 mm를 다시 비우지 않으며, notifier가 일부 범위를 거절하면 false를 받아 제한적으로 재시도합니다.

exit_mmap의 고전 경로와 현재 경로

v6.0은 OOM victim mm이면 정상 exit 초기에 __oom_reap_task_mm()를 직접 호출하고 SKIP을 설정한 뒤 mmap write lock cycle로 reaper와 순서를 맞춥니다. 아래 전체 함수에서 notifier release가 앞서는 점도 중요합니다.

v6_0_exit_mmap.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code	<alan@lxorguk.ukuu.org.uk>
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

void exit_mmap(struct mm_struct *mm)
{
	struct mmu_gather tlb;
	struct vm_area_struct *vma;
	unsigned long nr_accounted = 0;

	/* mm's last user has gone, and its about to be pulled down */
	mmu_notifier_release(mm);

	if (unlikely(mm_is_oom_victim(mm))) {
		/*
		 * Manually reap the mm to free as much memory as possible.
		 * Then, as the oom reaper does, set MMF_OOM_SKIP to disregard
		 * this mm from further consideration.  Taking mm->mmap_lock for
		 * write after setting MMF_OOM_SKIP will guarantee that the oom
		 * reaper will not run on this mm again after mmap_lock is
		 * dropped.
		 *
		 * Nothing can be holding mm->mmap_lock here and the above call
		 * to mmu_notifier_release(mm) ensures mmu notifier callbacks in
		 * __oom_reap_task_mm() will not block.
		 */
		(void)__oom_reap_task_mm(mm);
		set_bit(MMF_OOM_SKIP, &mm->flags);
	}

	mmap_write_lock(mm);
	arch_exit_mmap(mm);

	vma = mm->mmap;
	if (!vma) {
		/* Can happen if dup_mmap() received an OOM */
		mmap_write_unlock(mm);
		return;
	}

	lru_add_drain();
	flush_cache_mm(mm);
	tlb_gather_mmu_fullmm(&tlb, mm);
	/* update_hiwater_rss(mm) here? but nobody should be looking */
	/* Use -1 here to ensure all VMAs in the mm are unmapped */
	unmap_vmas(&tlb, vma, 0, -1);
	free_pgtables(&tlb, vma, FIRST_USER_ADDRESS, USER_PGTABLES_CEILING);
	tlb_finish_mmu(&tlb);

	/* Walk the list again, actually closing and freeing it. */
	while (vma) {
		if (vma->vm_flags & VM_ACCOUNT)
			nr_accounted += vma_pages(vma);
		vma = remove_vma(vma);
		cond_resched();
	}
	mm->mmap = NULL;
	mmap_write_unlock(mm);
	vm_unacct_memory(nr_accounted);
}

v6.18은 같은 수동 reap branch가 아닙니다. notifier release 뒤 read lock 아래 정상 unmap_vmas()를 수행하고 read lock을 내린 뒤 SKIP을 설정합니다. 이어 write lock을 얻어 Maple Tree에서 분리하고 page table·VMA를 정리합니다. reaper의 read-side SKIP 재검사가 이 write-side cycle과 직렬화됩니다. VMA가 없는 early path까지 포함해 전체 함수를 읽어야 합니다.

v6_18_exit_mmap.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/mmap.c
 *
 * Written by obz.
 *
 * Address space accounting code	<alan@lxorguk.ukuu.org.uk>
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

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

void exit_mmap(struct mm_struct *mm)
{
	struct mmu_gather tlb;
	struct vm_area_struct *vma;
	unsigned long nr_accounted = 0;
	VMA_ITERATOR(vmi, mm, 0);
	int count = 0;

	/* mm's last user has gone, and its about to be pulled down */
	mmu_notifier_release(mm);

	mmap_read_lock(mm);
	arch_exit_mmap(mm);

	vma = vma_next(&vmi);
	if (!vma || unlikely(xa_is_zero(vma))) {
		/* Can happen if dup_mmap() received an OOM */
		mmap_read_unlock(mm);
		mmap_write_lock(mm);
		goto destroy;
	}

	flush_cache_mm(mm);
	tlb_gather_mmu_fullmm(&tlb, mm);
	/* update_hiwater_rss(mm) here? but nobody should be looking */
	/* Use ULONG_MAX here to ensure all VMAs in the mm are unmapped */
	unmap_vmas(&tlb, &vmi.mas, vma, 0, ULONG_MAX, ULONG_MAX, false);
	mmap_read_unlock(mm);

	/*
	 * Set MMF_OOM_SKIP to hide this task from the oom killer/reaper
	 * because the memory has been already freed.
	 */
	mm_flags_set(MMF_OOM_SKIP, mm);
	mmap_write_lock(mm);
	mt_clear_in_rcu(&mm->mm_mt);
	vma_iter_set(&vmi, vma->vm_end);
	free_pgtables(&tlb, &vmi.mas, vma, FIRST_USER_ADDRESS,
		      USER_PGTABLES_CEILING, true);
	tlb_finish_mmu(&tlb);

	/*
	 * Walk the list again, actually closing and freeing it, with preemption
	 * enabled, without holding any MM locks besides the unreachable
	 * mmap_write_lock.
	 */
	vma_iter_set(&vmi, vma->vm_end);
	do {
		if (vma->vm_flags & VM_ACCOUNT)
			nr_accounted += vma_pages(vma);
		vma_mark_detached(vma);
		remove_vma(vma);
		count++;
		cond_resched();
		vma = vma_next(&vmi);
	} while (vma && likely(!xa_is_zero(vma)));

	BUG_ON(count != mm->map_count);

	trace_exit_mmap(mm);
destroy:
	__mt_destroy(&mm->mm_mt);
	mmap_write_unlock(mm);
	vm_unacct_memory(nr_accounted);
}

v6_18_free_signal_struct.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/kernel/fork.c
 *
 *  Copyright (C) 1991, 1992  Linus Torvalds
 */

/*
 *  'fork.c' contains the help-routines for the 'fork' system call
 * (see also entry.S and others).
 * Fork is rather simple, once you get the hang of it, but the memory
 * management can be a bitch. See 'mm/memory.c': 'copy_page_range()'
 */

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

static inline void free_signal_struct(struct signal_struct *sig)
{
	taskstats_tgid_free(sig);
	sched_autogroup_exit(sig);
	/*
	 * __mmdrop is not safe to call from softirq context on x86 due to
	 * pgd_dtor so postpone it to the async context
	 */
	if (sig->oom_mm)
		mmdrop_async(sig->oom_mm);
	kmem_cache_free(signal_cachep, sig);
}

마지막 code는 signal-owned oom_mm의 보유분을 돌려줍니다. x86에서는 softirq context에서 곧바로 __mmdrop을 수행하기 안전하지 않은 이유가 있어 async drop을 사용합니다. 이는 reaper의 queue task put이나 kill 경로의 임시 mmdrop과 다른 보유분입니다. mm descriptor 보존을 mm의 모든 PTE 보존과 혼동하지 않아야 이 정리가 맞습니다.

참고 자료