mm_struct의 field와 실패 경로를 함께 읽기

mm_struct를 address-space의 모든 상태가 들어 있는 큰 구조체라고만 생각하면, field가 추가될수록 이해가 어려워집니다. 먼저 어떤 field가 mapping의 모양을 기술하는지, 어떤 field가 수명을 세는지, 어떤 field가 subsystem의 참여 상태를 담는지 나누어야 합니다. 특히 초기화 도중 실패했을 때 어디까지 준비되었는지를 추적하면 이 구분이 실제 동작으로 이어집니다.

이 글의 구현 기준은 official Linux v6.18입니다. historical list·red-black tree·task-local RSS를 사용하던 설명과 current Maple Tree·per-CPU RSS를 한 구조체로 합치지 않습니다.

구조체 전체에서 보호 범위를 찾습니다

아래는 v6.18 include/linux/mm_types.h의 mm_struct 전체 정의입니다. configuration 분기와 주석을 남겼습니다. kernel 내부 의존성이 있는 GPL code이며 원래 SPDX, source SHA·commit은 동반 파일의 provenance에, license 본문은 code/GPL-2.0.txt에 있습니다.

v6_18_mm_struct.c

/* SPDX-License-Identifier: GPL-2.0 */
/*
 * Official Linux v6.18: include/linux/mm_types.h
 * Complete struct; source lines 944-1237.
 * Body unchanged. Kernel dependencies and configuration are required.
 * Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
 */

struct mm_struct {
	struct {
		/*
		 * Fields which are often written to are placed in a separate
		 * cache line.
		 */
		struct {
			/**
			 * @mm_count: The number of references to &struct
			 * mm_struct (@mm_users count as 1).
			 *
			 * Use mmgrab()/mmdrop() to modify. When this drops to
			 * 0, the &struct mm_struct is freed.
			 */
			atomic_t mm_count;
		} ____cacheline_aligned_in_smp;

		struct maple_tree mm_mt;

		unsigned long mmap_base;	/* base of mmap area */
		unsigned long mmap_legacy_base;	/* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
		/* Base addresses for compatible mmap() */
		unsigned long mmap_compat_base;
		unsigned long mmap_compat_legacy_base;
#endif
		unsigned long task_size;	/* size of task vm space */
		pgd_t * pgd;

#ifdef CONFIG_MEMBARRIER
		/**
		 * @membarrier_state: Flags controlling membarrier behavior.
		 *
		 * This field is close to @pgd to hopefully fit in the same
		 * cache-line, which needs to be touched by switch_mm().
		 */
		atomic_t membarrier_state;
#endif

		/**
		 * @mm_users: The number of users including userspace.
		 *
		 * Use mmget()/mmget_not_zero()/mmput() to modify. When this
		 * drops to 0 (i.e. when the task exits and there are no other
		 * temporary reference holders), we also release a reference on
		 * @mm_count (which may then free the &struct mm_struct if
		 * @mm_count also drops to 0).
		 */
		atomic_t mm_users;

#ifdef CONFIG_SCHED_MM_CID
		/**
		 * @pcpu_cid: Per-cpu current cid.
		 *
		 * Keep track of the currently allocated mm_cid for each cpu.
		 * The per-cpu mm_cid values are serialized by their respective
		 * runqueue locks.
		 */
		struct mm_cid __percpu *pcpu_cid;
		/*
		 * @mm_cid_next_scan: Next mm_cid scan (in jiffies).
		 *
		 * When the next mm_cid scan is due (in jiffies).
		 */
		unsigned long mm_cid_next_scan;
		/**
		 * @nr_cpus_allowed: Number of CPUs allowed for mm.
		 *
		 * Number of CPUs allowed in the union of all mm's
		 * threads allowed CPUs.
		 */
		unsigned int nr_cpus_allowed;
		/**
		 * @max_nr_cid: Maximum number of allowed concurrency
		 *              IDs allocated.
		 *
		 * Track the highest number of allowed concurrency IDs
		 * allocated for the mm.
		 */
		atomic_t max_nr_cid;
		/**
		 * @cpus_allowed_lock: Lock protecting mm cpus_allowed.
		 *
		 * Provide mutual exclusion for mm cpus_allowed and
		 * mm nr_cpus_allowed updates.
		 */
		raw_spinlock_t cpus_allowed_lock;
#endif
#ifdef CONFIG_MMU
		atomic_long_t pgtables_bytes;	/* size of all page tables */
#endif
		int map_count;			/* number of VMAs */

		spinlock_t page_table_lock; /* Protects page tables and some
					     * counters
					     */
		/*
		 * Typically the current mmap_lock's offset is 56 bytes from
		 * the last cacheline boundary, which is very optimal, as
		 * its two hot fields 'count' and 'owner' sit in 2 different
		 * cachelines, and when mmap_lock is highly contended, both
		 * of the 2 fields will be accessed frequently, current layout
		 * will help to reduce cache bouncing.
		 *
		 * So please be careful with adding new fields before
		 * mmap_lock, which can easily push the 2 fields into one
		 * cacheline.
		 */
		struct rw_semaphore mmap_lock;

		struct list_head mmlist; /* List of maybe swapped mm's.	These
					  * are globally strung together off
					  * init_mm.mmlist, and are protected
					  * by mmlist_lock
					  */
#ifdef CONFIG_PER_VMA_LOCK
		struct rcuwait vma_writer_wait;
		/*
		 * This field has lock-like semantics, meaning it is sometimes
		 * accessed with ACQUIRE/RELEASE semantics.
		 * Roughly speaking, incrementing the sequence number is
		 * equivalent to releasing locks on VMAs; reading the sequence
		 * number can be part of taking a read lock on a VMA.
		 * Incremented every time mmap_lock is write-locked/unlocked.
		 * Initialized to 0, therefore odd values indicate mmap_lock
		 * is write-locked and even values that it's released.
		 *
		 * Can be modified under write mmap_lock using RELEASE
		 * semantics.
		 * Can be read with no other protection when holding write
		 * mmap_lock.
		 * Can be read with ACQUIRE semantics if not holding write
		 * mmap_lock.
		 */
		seqcount_t mm_lock_seq;
#endif
#ifdef CONFIG_FUTEX_PRIVATE_HASH
		struct mutex			futex_hash_lock;
		struct futex_private_hash	__rcu *futex_phash;
		struct futex_private_hash	*futex_phash_new;
		/* futex-ref */
		unsigned long			futex_batches;
		struct rcu_head			futex_rcu;
		atomic_long_t			futex_atomic;
		unsigned int			__percpu *futex_ref;
#endif

		unsigned long hiwater_rss; /* High-watermark of RSS usage */
		unsigned long hiwater_vm;  /* High-water virtual memory usage */

		unsigned long total_vm;	   /* Total pages mapped */
		unsigned long locked_vm;   /* Pages that have PG_mlocked set */
		atomic64_t    pinned_vm;   /* Refcount permanently increased */
		unsigned long data_vm;	   /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
		unsigned long exec_vm;	   /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
		unsigned long stack_vm;	   /* VM_STACK */
		vm_flags_t def_flags;

		/**
		 * @write_protect_seq: Locked when any thread is write
		 * protecting pages mapped by this mm to enforce a later COW,
		 * for instance during page table copying for fork().
		 */
		seqcount_t write_protect_seq;

		spinlock_t arg_lock; /* protect the below fields */

		unsigned long start_code, end_code, start_data, end_data;
		unsigned long start_brk, brk, start_stack;
		unsigned long arg_start, arg_end, env_start, env_end;

		unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

#ifdef CONFIG_ARCH_HAS_ELF_CORE_EFLAGS
		/* the ABI-related flags from the ELF header. Used for core dump */
		unsigned long saved_e_flags;
#endif

		struct percpu_counter rss_stat[NR_MM_COUNTERS];

		struct linux_binfmt *binfmt;

		/* Architecture-specific MM context */
		mm_context_t context;

		mm_flags_t flags; /* Must use mm_flags_* hlpers to access */

#ifdef CONFIG_AIO
		spinlock_t			ioctx_lock;
		struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
		/*
		 * "owner" points to a task that is regarded as the canonical
		 * user/owner of this mm. All of the following must be true in
		 * order for it to be changed:
		 *
		 * current == mm->owner
		 * current->mm != mm
		 * new_owner->mm == mm
		 * new_owner->alloc_lock is held
		 */
		struct task_struct __rcu *owner;
#endif
		struct user_namespace *user_ns;

		/* store ref to file /proc/<pid>/exe symlink points to */
		struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
		struct mmu_notifier_subscriptions *notifier_subscriptions;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !defined(CONFIG_SPLIT_PMD_PTLOCKS)
		pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_NUMA_BALANCING
		/*
		 * numa_next_scan is the next time that PTEs will be remapped
		 * PROT_NONE to trigger NUMA hinting faults; such faults gather
		 * statistics and migrate pages to new nodes if necessary.
		 */
		unsigned long numa_next_scan;

		/* Restart point for scanning and remapping PTEs. */
		unsigned long numa_scan_offset;

		/* numa_scan_seq prevents two threads remapping PTEs. */
		int numa_scan_seq;
#endif
		/*
		 * An operation with batched TLB flushing is going on. Anything
		 * that can move process memory needs to flush the TLB when
		 * moving a PROT_NONE mapped page.
		 */
		atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
		/* See flush_tlb_batched_pending() */
		atomic_t tlb_flush_batched;
#endif
		struct uprobes_state uprobes_state;
#ifdef CONFIG_PREEMPT_RT
		struct rcu_head delayed_drop;
#endif
#ifdef CONFIG_HUGETLB_PAGE
		atomic_long_t hugetlb_usage;
#endif
		struct work_struct async_put_work;

#ifdef CONFIG_IOMMU_MM_DATA
		struct iommu_mm_data *iommu_mm;
#endif
#ifdef CONFIG_KSM
		/*
		 * Represent how many pages of this process are involved in KSM
		 * merging (not including ksm_zero_pages).
		 */
		unsigned long ksm_merging_pages;
		/*
		 * Represent how many pages are checked for ksm merging
		 * including merged and not merged.
		 */
		unsigned long ksm_rmap_items;
		/*
		 * Represent how many empty pages are merged with kernel zero
		 * pages when enabling KSM use_zero_pages.
		 */
		atomic_long_t ksm_zero_pages;
#endif /* CONFIG_KSM */
#ifdef CONFIG_LRU_GEN_WALKS_MMU
		struct {
			/* this mm_struct is on lru_gen_mm_list */
			struct list_head list;
			/*
			 * Set when switching to this mm_struct, as a hint of
			 * whether it has been used since the last time per-node
			 * page table walkers cleared the corresponding bits.
			 */
			unsigned long bitmap;
#ifdef CONFIG_MEMCG
			/* points to the memcg of "owner" above */
			struct mem_cgroup *memcg;
#endif
		} lru_gen;
#endif /* CONFIG_LRU_GEN_WALKS_MMU */
#ifdef CONFIG_MM_ID
		mm_id_t mm_id;
#endif /* CONFIG_MM_ID */
	} __randomize_layout;

	/*
	 * The mm_cpumask needs to be at the end of mm_struct, because it
	 * is dynamically sized based on nr_cpu_ids.
	 */
	unsigned long cpu_bitmap[];
};

mm_mt, map_count, mmap_base, task_size는 각각 VMA range index, VMA 수, 배치 기준, user address-space 한계와 관련됩니다. total_vm은 page 단위의 virtual 양이며 pgtables_bytes와 단위부터 다릅니다. page table 하나를 설치하거나 없앨 때의 mm_inc_nr_ptes()·mm_dec_nr_ptes() 같은 helper는 table metadata 비용을 조율합니다. data folio가 아직 없는 sparse mapping에서도 이 비용은 발생할 수 있습니다.

historical get_unmapped_area function pointer는 새 mapping의 위치 검색을 선택하던 hook입니다. file의 file_operations.get_unmapped_area가 제공하는 검색과 mm의 기본 검색을 구별해야 합니다. 이름이 비슷한 VMA fault callback은 이 배치 선택의 소유자가 아닙니다. current 구조체에 없어진 field를 설명하기 위해 현재 구조체에 가상의 field를 끼워 넣을 필요는 없습니다.

mm_count를 별도 cache line에 놓고 membarrier_state를 pgd 근처에 두는 것은 의미상의 소유권과 다른 최적화입니다. mmap_lock 앞의 field 배치도 중요합니다. current 주석은 두 hot field인 rwsem의 count와 owner가 다른 cache line에 놓이도록 하는 효과를 설명합니다. historical 설명의 특정 byte offset을 ABI나 모든 configuration의 고정 offset으로 인용해서는 안 됩니다. 구조체 앞에 작은 field 하나를 추가해도 경합 때의 cache bouncing이 달라질 수 있습니다.

같은 객체에도 서로 다른 lock이 필요합니다

mmap_lock은 VMA metadata의 보호를, page_table_lock과 split PTL은 page-table 변경을 맡습니다. mmlist는 잠재적인 swap 관련 mm들을 연결하며 init_mm.mmlist가 list head입니다. 이 list는 별도의 mmlist_lock을 따릅니다. mm의 field라는 이유로 mmap read lock만 잡고 모든 list를 변경할 수는 없습니다.

arg_lock은 실행 이미지와 argument/environment 경계를 바꾸는 경로의 보호에 관계됩니다. start_code·end_code, start_brk·brk, argument와 environment의 start/end는 현재 VMA index와 별도의 bookkeeping입니다. user가 이 경계를 관측한다고 그 범위가 현재 모두 present라는 뜻은 아닙니다. saved_auxv도 /proc/PID/auxv를 위한 저장 상태입니다.

write_protect_seq는 fork에서 CoW를 준비하는 write-protection과 GUP observer 사이의 조율입니다. 반면 per-VMA lock 구성의 mm_lock_seq는 mmap writer와 VMA reader의 관계를 나타냅니다. 이름에 sequence가 들어간다는 공통점만으로 같은 retry 규칙을 적용하면 안 됩니다. tlb_flush_pending 역시 batched invalidation 진행을 나타내며, lock을 놓았다는 사실과 CPU translation이 이미 갱신되었다는 사실을 분리합니다.

cpu_bitmap[]는 mm_cpumask()를 통해 이 mm의 translation 이용 CPU를 추적합니다. 동적 mm에서는 nr_cpu_ids에 맞게 추가 공간을 할당하므로 flexible array가 마지막에 있어야 합니다. task의 CPU affinity나 현재 실행 CPU 하나를 나타내는 bitmap은 아닙니다. scheduler의 concurrency ID 관련 nr_cpus_allowed와도 별개의 자료입니다.

owner는 유일한 이용자가 아닙니다

CONFIG_MEMCG의 owner는 해당 mm의 canonical owner task를 추적합니다. 여러 task가 같은 mm을 이용할 수 있으므로 독점 소유권 pointer로 읽지 않습니다. 구조체 주석은 owner 변경 조건을 구체적으로 적습니다. 변경하는 current가 기존 owner이고, current는 그 mm을 더 이상 자기 mm으로 쓰지 않으며, 새 owner의 mm이 대상 mm이고, 새 owner의 alloc_lock이 잡혀 있어야 합니다. 단순한 pointer 대입으로 다른 task에 넘기는 계약이 아닙니다.

exe_file은 /proc/PID/exe가 가리키는 file reference이고 user_ns는 namespace 수명과 연결됩니다. mmu notifier, AIO, uprobes, hugetlb, NUMA balancing, KSM과 MGLRU의 field는 각 subsystem과의 연결 상태를 담습니다. 이들이 구조체 전체 정의에 보인다고 모든 configuration에 동시에 존재하거나 하나의 lock으로 보호되는 것은 아닙니다. 예를 들어 NUMA scan offset과 sequence는 hinting fault를 준비하는 순회 상태이며, 실제 physical node의 소유권 자체가 아닙니다.

초기화는 자원을 획득하는 과정입니다

아래는 v6.18 kernel/fork.c의 lock 초기화와 mm 초기화 전체입니다.

v6_18_mmap_init_lock.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
 * Complete function; source lines 1021-1028.
 * Body unchanged. Kernel dependencies and configuration are required.
 * Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
 */

static void mmap_init_lock(struct mm_struct *mm)
{
	init_rwsem(&mm->mmap_lock);
	mm_lock_seqcount_init(mm);
#ifdef CONFIG_PER_VMA_LOCK
	rcuwait_init(&mm->vma_writer_wait);
#endif
}

v6_18_mm_init.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
 * Complete function; source lines 1030-1107.
 * Body unchanged. Kernel dependencies and configuration are required.
 * Source URL, SHA, commit, and license: provenance.json; GPL-2.0.txt.
 */

static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p,
	struct user_namespace *user_ns)
{
	mt_init_flags(&mm->mm_mt, MM_MT_FLAGS);
	mt_set_external_lock(&mm->mm_mt, &mm->mmap_lock);
	atomic_set(&mm->mm_users, 1);
	atomic_set(&mm->mm_count, 1);
	seqcount_init(&mm->write_protect_seq);
	mmap_init_lock(mm);
	INIT_LIST_HEAD(&mm->mmlist);
	mm_pgtables_bytes_init(mm);
	mm->map_count = 0;
	mm->locked_vm = 0;
	atomic64_set(&mm->pinned_vm, 0);
	memset(&mm->rss_stat, 0, sizeof(mm->rss_stat));
	spin_lock_init(&mm->page_table_lock);
	spin_lock_init(&mm->arg_lock);
	mm_init_cpumask(mm);
	mm_init_aio(mm);
	mm_init_owner(mm, p);
	mm_pasid_init(mm);
	RCU_INIT_POINTER(mm->exe_file, NULL);
	mmu_notifier_subscriptions_init(mm);
	init_tlb_flush_pending(mm);
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !defined(CONFIG_SPLIT_PMD_PTLOCKS)
	mm->pmd_huge_pte = NULL;
#endif
	mm_init_uprobes_state(mm);
	hugetlb_count_init(mm);

	mm_flags_clear_all(mm);
	if (current->mm) {
		unsigned long flags = __mm_flags_get_word(current->mm);

		__mm_flags_set_word(mm, mmf_init_legacy_flags(flags));
		mm->def_flags = current->mm->def_flags & VM_INIT_DEF_MASK;
	} else {
		__mm_flags_set_word(mm, default_dump_filter);
		mm->def_flags = 0;
	}

	if (futex_mm_init(mm))
		goto fail_mm_init;

	if (mm_alloc_pgd(mm))
		goto fail_nopgd;

	if (mm_alloc_id(mm))
		goto fail_noid;

	if (init_new_context(p, mm))
		goto fail_nocontext;

	if (mm_alloc_cid(mm, p))
		goto fail_cid;

	if (percpu_counter_init_many(mm->rss_stat, 0, GFP_KERNEL_ACCOUNT,
				     NR_MM_COUNTERS))
		goto fail_pcpu;

	mm->user_ns = get_user_ns(user_ns);
	lru_gen_init_mm(mm);
	return mm;

fail_pcpu:
	mm_destroy_cid(mm);
fail_cid:
	destroy_context(mm);
fail_nocontext:
	mm_free_id(mm);
fail_noid:
	mm_free_pgd(mm);
fail_nopgd:
	futex_hash_free(mm);
fail_mm_init:
	free_mm(mm);
	return NULL;
}

새 mm의 mm_users = 1, mm_count = 1은 user 이용 관계 전체가 object reference 하나를 갖는 모델과 맞습니다. mm_mt는 mmap lock을 external writer lock으로 사용하도록 연결합니다. def_flags는 부모의 값을 무조건 모두 복사하지 않고 VM_INIT_DEF_MASK로 거르며, mm flags도 초기화용 변환을 거칩니다. 새로운 주소 공간의 policy가 raw 구조체 memcpy만으로 완성되는 것은 아닙니다.

실패는 PGD에서만 발생하지 않습니다. futex 상태, PGD, mm ID, architecture context, scheduler CID, per-CPU RSS counter 준비가 차례로 실패할 수 있습니다. 예를 들어 RSS counter 초기화가 실패하면 이미 만든 CID와 architecture context를 내려놓고 mm ID, PGD, futex 자원과 mm 객체를 정리합니다. architecture context 생성 자체가 실패했을 때에는 성공하지 않은 context를 같은 label에서 파괴하지 않습니다. 각 label이 '직전 단계까지 성공한 상태'를 나타내기 때문입니다.

성공 뒤에는 namespace reference를 얻고 MGLRU 관련 mm 상태를 초기화하여 반환합니다. 이것은 아직 user VMA와 data를 모두 채웠다는 뜻이 아닙니다. fork는 이 mm을 토대로 VMA와 user mapping을 준비하고, exec는 새 이미지의 mapping을 따로 구성합니다. metadata 초기화, mapping 복제, demand fault를 분리해야 allocation 실패와 늦은 fault 실패를 올바르게 설명할 수 있습니다.

참고 자료