fork는 anonymous page보다 rmap 관계를 먼저 복제합니다

fork가 모든 user page를 즉시 복사하지 않는다는 설명만으로는 CoW의 뒷부분이 빠집니다. parent의 folio를 child에서도 찾을 수 있어야 하고, child가 이후 만드는 새 folio는 별도 계보에 연결해야 합니다. anon_vma_clone()과 anon_vma_fork()가 이 비대칭 관계를 준비합니다.

clone의 대상은 contents가 아니라 AVC입니다

source VMA에 연결된 AVC마다 destination에도 연결을 만듭니다. split한 VMA라면 기존 anonymous backing을 계속 찾아야 하며, fork한 VMA라면 inherited folio를 parent ancestry에서 찾아야 합니다. source의 active anon_vma 하나만 복사하면 이미 물려받은 ancestor folio를 찾는 관계가 사라질 수 있습니다.

여러 연결을 복제하면서 root tree가 달라지면 이전 root lock을 놓고 새 lock을 얻는 관리가 필요합니다. 잠들 수 있는 allocation을 lock 아래 강행하지 않도록 빠른 allocation 실패 뒤 lock을 풀고 재시도하는 경로도 있습니다. 중간 실패에서는 이미 만든 destination 연결을 unlink_anon_vmas()로 되돌립니다.

child의 새 folio를 위한 active 대상

parent에 anon_vma가 없으면 새로 복제할 관계도 없습니다. 있다면 inherited AVC들을 clone한 뒤 child가 새 anonymous folio를 연결할 active anon_vma를 정합니다. 새 object를 만들면 parent pointer는 기존 active 대상에, root는 공통 계보의 root에 맞추고 root reference를 확보합니다.

기존 shared folio는 이전 anon_vma에 남아 parent·child 모두의 후보를 찾습니다. child가 CoW한 새 folio는 child active 대상에 연결되어 child 후보를 찾습니다. parent의 tree가 child VMA를 계속 포함해도 child PTE가 이미 다른 PFN을 가리킬 수 있다는 사실은 이후 walk에서 확인합니다.

inactive 계보 객체의 재사용

반복 fork와 빠른 process 종료는 active VMA가 없지만 ancestry 때문에 살아 있는 객체를 남길 수 있습니다. clone은 active count가 0이고 child count 조건이 단순한 object를 child active 대상으로 재사용할 수 있습니다. 모든 fork마다 새 계층을 덧붙여 ancestry가 불필요하게 길어지는 것을 제한하는 장치입니다.

이 재사용은 살아 있는 parent의 active object를 임의로 child 소유로 바꾸는 것이 아닙니다. object reference, parent child count와 active VMA count가 서로 다른 조건인 이유입니다. 현재 객체의 주소가 존재한다는 사실만으로 재사용할 자격이 생기지는 않습니다.

split과 merge에 같은 연결 machinery가 필요한 이유

split은 새 VMA에서 기존 folio를 찾도록 AVC를 clone합니다. merge는 합칠 양쪽의 active 계보가 compatible해야 하며, 흡수한 VMA의 중복 연결을 정리할 수 있어야 합니다. 양쪽 active anon_vma가 다른데 주소와 protection만 같다는 이유로 합치면 새 folio의 연결 대상을 하나로 정할 수 없습니다.

경계를 바꾸기 전 interval node를 빼고, start·end·pgoff를 바꾼 뒤 다시 넣습니다. folio마다 연결을 옮기지 않는 대신 중간 index를 정확히 유지하는 비용을 지불하는 것입니다. 이 관점에서 보면 fork ancestry와 VMA split이 별개의 예외가 아니라 같은 metadata 설계의 두 사용처라는 점이 보입니다.

Reverse iteration과 실패 unwind를 전체 함수에서 읽습니다

아래는 official Linux v6.0 source의 전체 function입니다. 이 historical tag의 caller와 counter 규칙을 설명하는 코드이며, 책 전체의 exact tag 또는 v6.18 caller layout과 동일하다는 보장은 아닙니다. Kernel context에 의존하며 새 build·runtime 검증을 수행하지 않았습니다. 원래 notice와 COPYING, GPL-2.0를 함께 보존했습니다.

AVC는 list 앞에 추가되므로 clone은 source를 역순으로 읽어 destination의 순서를 유지합니다. GFP_NOWAIT | __GFP_NOWARN allocation이 실패하면 root lock을 먼저 놓고 잠들 수 있는 allocation을 합니다. 실패 label에서 dst->anon_vma = NULL을 먼저 수행하는 이유는 이후 unlink가 active count를 잘못 줄이지 않게 하기 위해서입니다.

anon_vma_clone.c

/*
 * mm/rmap.c - physical to virtual reverse mappings
 *
 * Copyright 2001, Rik van Riel <riel@conectiva.com.br>
 * Released under the General Public License (GPL).
 *
 * Simple, low overhead reverse mapping scheme.
 * Please try to keep this thing as modular as possible.
 *
 * Provides methods for unmapping each kind of mapped page:
 * the anon methods track anonymous pages, and
 * the file methods track pages belonging to an inode.
 *
 * Original design by Rik van Riel <riel@conectiva.com.br> 2001
 * File methods by Dave McCracken <dmccr@us.ibm.com> 2003, 2004
 * Anonymous methods by Andrea Arcangeli <andrea@suse.de> 2004
 * Contributions by Hugh Dickins 2003, 2004
 */

/*
 * Lock ordering in mm:
 *
 * inode->i_rwsem	(while writing or truncating, not reading or faulting)
 *   mm->mmap_lock
 *     mapping->invalidate_lock (in filemap_fault)
 *       page->flags PG_locked (lock_page)   * (see hugetlbfs below)
 *         hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share)
 *           mapping->i_mmap_rwsem
 *             hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             anon_vma->rwsem
 *               mm->page_table_lock or pte_lock
 *                 swap_lock (in swap_duplicate, swap_info_get)
 *                   mmlist_lock (in mmput, drain_mmlist and others)
 *                   mapping->private_lock (in block_dirty_folio)
 *                     folio_lock_memcg move_lock (in block_dirty_folio)
 *                       i_pages lock (widely used)
 *                         lruvec->lru_lock (in folio_lruvec_lock_irq)
 *                   inode->i_lock (in set_page_dirty's __mark_inode_dirty)
 *                   bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty)
 *                     sb_lock (within inode_lock in fs/fs-writeback.c)
 *                     i_pages lock (widely used, in set_page_dirty,
 *                               in arch-dependent flush_dcache_mmap_lock,
 *                               within bdi.wb->list_lock in __sync_single_inode)
 *
 * anon_vma->rwsem,mapping->i_mmap_rwsem   (memory_failure, collect_procs_anon)
 *   ->tasklist_lock
 *     pte map lock
 *
 * * hugetlbfs PageHuge() pages take locks in this order:
 *         mapping->i_mmap_rwsem
 *           hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             page->flags PG_locked (lock_page)
 */

/* Upstream Linux v6.0: mm/rmap.c:279-326
 * Exact source lines, not a restored book listing or standalone program.
 */
int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src)
{
	struct anon_vma_chain *avc, *pavc;
	struct anon_vma *root = NULL;

	list_for_each_entry_reverse(pavc, &src->anon_vma_chain, same_vma) {
		struct anon_vma *anon_vma;

		avc = anon_vma_chain_alloc(GFP_NOWAIT | __GFP_NOWARN);
		if (unlikely(!avc)) {
			unlock_anon_vma_root(root);
			root = NULL;
			avc = anon_vma_chain_alloc(GFP_KERNEL);
			if (!avc)
				goto enomem_failure;
		}
		anon_vma = pavc->anon_vma;
		root = lock_anon_vma_root(root, anon_vma);
		anon_vma_chain_link(dst, avc, anon_vma);

		/*
		 * Reuse existing anon_vma if it has no vma and only one
		 * anon_vma child.
		 *
		 * Root anon_vma is never reused:
		 * it has self-parent reference and at least one child.
		 */
		if (!dst->anon_vma && src->anon_vma &&
		    anon_vma->num_children < 2 &&
		    anon_vma->num_active_vmas == 0)
			dst->anon_vma = anon_vma;
	}
	if (dst->anon_vma)
		dst->anon_vma->num_active_vmas++;
	unlock_anon_vma_root(root);
	return 0;

 enomem_failure:
	/*
	 * dst->anon_vma is dropped here otherwise its degree can be incorrectly
	 * decremented in unlink_anon_vmas().
	 * We can safely do this because callers of anon_vma_clone() don't care
	 * about dst->anon_vma if anon_vma_clone() failed.
	 */
	dst->anon_vma = NULL;
	unlink_anon_vmas(dst);
	return -ENOMEM;
}

num_children < 2와 num_active_vmas == 0을 둘 다 읽어야 재사용 대상을 이해할 수 있습니다. Root는 self-parent 관계와 살아 있는 child 때문에 이 조건으로 무제한 재사용되는 대상이 아닙니다. Clone 성공 뒤에도 새 active anon_vma가 필요할 수 있으므로 fork 함수가 다음 단계를 맡습니다.

anon_vma_fork.c

/*
 * mm/rmap.c - physical to virtual reverse mappings
 *
 * Copyright 2001, Rik van Riel <riel@conectiva.com.br>
 * Released under the General Public License (GPL).
 *
 * Simple, low overhead reverse mapping scheme.
 * Please try to keep this thing as modular as possible.
 *
 * Provides methods for unmapping each kind of mapped page:
 * the anon methods track anonymous pages, and
 * the file methods track pages belonging to an inode.
 *
 * Original design by Rik van Riel <riel@conectiva.com.br> 2001
 * File methods by Dave McCracken <dmccr@us.ibm.com> 2003, 2004
 * Anonymous methods by Andrea Arcangeli <andrea@suse.de> 2004
 * Contributions by Hugh Dickins 2003, 2004
 */

/*
 * Lock ordering in mm:
 *
 * inode->i_rwsem	(while writing or truncating, not reading or faulting)
 *   mm->mmap_lock
 *     mapping->invalidate_lock (in filemap_fault)
 *       page->flags PG_locked (lock_page)   * (see hugetlbfs below)
 *         hugetlbfs_i_mmap_rwsem_key (in huge_pmd_share)
 *           mapping->i_mmap_rwsem
 *             hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             anon_vma->rwsem
 *               mm->page_table_lock or pte_lock
 *                 swap_lock (in swap_duplicate, swap_info_get)
 *                   mmlist_lock (in mmput, drain_mmlist and others)
 *                   mapping->private_lock (in block_dirty_folio)
 *                     folio_lock_memcg move_lock (in block_dirty_folio)
 *                       i_pages lock (widely used)
 *                         lruvec->lru_lock (in folio_lruvec_lock_irq)
 *                   inode->i_lock (in set_page_dirty's __mark_inode_dirty)
 *                   bdi.wb->list_lock (in set_page_dirty's __mark_inode_dirty)
 *                     sb_lock (within inode_lock in fs/fs-writeback.c)
 *                     i_pages lock (widely used, in set_page_dirty,
 *                               in arch-dependent flush_dcache_mmap_lock,
 *                               within bdi.wb->list_lock in __sync_single_inode)
 *
 * anon_vma->rwsem,mapping->i_mmap_rwsem   (memory_failure, collect_procs_anon)
 *   ->tasklist_lock
 *     pte map lock
 *
 * * hugetlbfs PageHuge() pages take locks in this order:
 *         mapping->i_mmap_rwsem
 *           hugetlb_fault_mutex (hugetlbfs specific page fault mutex)
 *             page->flags PG_locked (lock_page)
 */

/* Upstream Linux v6.0: mm/rmap.c:333-393
 * Exact source lines, not a restored book listing or standalone program.
 */
int anon_vma_fork(struct vm_area_struct *vma, struct vm_area_struct *pvma)
{
	struct anon_vma_chain *avc;
	struct anon_vma *anon_vma;
	int error;

	/* Don't bother if the parent process has no anon_vma here. */
	if (!pvma->anon_vma)
		return 0;

	/* Drop inherited anon_vma, we'll reuse existing or allocate new. */
	vma->anon_vma = NULL;

	/*
	 * First, attach the new VMA to the parent VMA's anon_vmas,
	 * so rmap can find non-COWed pages in child processes.
	 */
	error = anon_vma_clone(vma, pvma);
	if (error)
		return error;

	/* An existing anon_vma has been reused, all done then. */
	if (vma->anon_vma)
		return 0;

	/* Then add our own anon_vma. */
	anon_vma = anon_vma_alloc();
	if (!anon_vma)
		goto out_error;
	anon_vma->num_active_vmas++;
	avc = anon_vma_chain_alloc(GFP_KERNEL);
	if (!avc)
		goto out_error_free_anon_vma;

	/*
	 * The root anon_vma's rwsem is the lock actually used when we
	 * lock any of the anon_vmas in this anon_vma tree.
	 */
	anon_vma->root = pvma->anon_vma->root;
	anon_vma->parent = pvma->anon_vma;
	/*
	 * With refcounts, an anon_vma can stay around longer than the
	 * process it belongs to. The root anon_vma needs to be pinned until
	 * this anon_vma is freed, because the lock lives in the root.
	 */
	get_anon_vma(anon_vma->root);
	/* Mark this anon_vma as the one where our new (COWed) pages go. */
	vma->anon_vma = anon_vma;
	anon_vma_lock_write(anon_vma);
	anon_vma_chain_link(vma, avc, anon_vma);
	anon_vma->parent->num_children++;
	anon_vma_unlock_write(anon_vma);

	return 0;

 out_error_free_anon_vma:
	put_anon_vma(anon_vma);
 out_error:
	unlink_anon_vmas(vma);
	return -ENOMEM;
}

Parent가 아직 anon_vma를 갖지 않는 경우에는 즉시 반환합니다. 반면 새 child object를 만들었다면 root reference를 추가하고, parent child count를 root lock 아래에서 올립니다. AVC allocation 실패와 anon_vma allocation 실패는 서로 다른 지점에서 시작하지만 이미 clone한 관계까지 unwind해야 하므로 최종적으로 unlink_anon_vmas()에 도달합니다.

Interval 변경은 remove·수정·insert의 순서입니다

VMA를 옮기거나 split하면서 vm_start, vm_end, vm_pgoff를 수정할 때, 예전 key로 들어 있는 interval node를 그대로 둔 채 field만 바꾸면 tree의 순서와 augmentation이 깨질 수 있습니다. 아래 두 helper는 이 변경의 앞뒤에 사용됩니다. 함수 자체가 lock을 획득하는 것은 아니므로 caller의 배타적 보호가 전제입니다.

여기서 배타적 보호는 v6.0 기준으로 mmap_lock의 write-side와 root anon_vma->rwsem의 write-side를 함께 유지한다는 뜻입니다. VMA field를 바꾸는 구간만이 아니라 기존 interval의 remove부터 field 변경, 새 interval의 insert까지 전체를 보호해야 합니다. mmap_lock read-side만 잡거나 tree를 다시 넣기 전에 root lock을 놓으면 이 계약을 충족하지 않습니다. v6.0 interval 갱신의 caller 계약

anon_vma_interval_tree_pre_update_vma.c

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

/* Upstream Linux v6.0: mm/mmap.c:470-477
 * Exact source lines, not a restored book listing or standalone program.
 */
static inline void
anon_vma_interval_tree_pre_update_vma(struct vm_area_struct *vma)
{
	struct anon_vma_chain *avc;

	list_for_each_entry(avc, &vma->anon_vma_chain, same_vma)
		anon_vma_interval_tree_remove(avc, &avc->anon_vma->rb_root);
}

anon_vma_interval_tree_post_update_vma.c

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

/* Upstream Linux v6.0: mm/mmap.c:479-486
 * Exact source lines, not a restored book listing or standalone program.
 */
static inline void
anon_vma_interval_tree_post_update_vma(struct vm_area_struct *vma)
{
	struct anon_vma_chain *avc;

	list_for_each_entry(avc, &vma->anon_vma_chain, same_vma)
		anon_vma_interval_tree_insert(avc, &avc->anon_vma->rb_root);
}

VMA merge의 anonymous 조건과 흡수되는 VMA의 정리도 나눠 보겠습니다. 두 active pointer가 모두 있다면 같은 객체여야 합니다. 한쪽 pointer가 없을 때의 singular-list 검사와 vma == NULL 예외를 빼고 단순 equality 함수라고 설명하면 호출별 의미를 잃습니다.

is_mergeable_anon_vma.c

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

/* Upstream Linux v6.0: mm/mmap.c:1015-1027
 * Exact source lines, not a restored book listing or standalone program.
 */
static inline int is_mergeable_anon_vma(struct anon_vma *anon_vma1,
					struct anon_vma *anon_vma2,
					struct vm_area_struct *vma)
{
	/*
	 * The list_is_singular() test is to avoid merging VMA cloned from
	 * parents. This can improve scalability caused by anon_vma lock.
	 */
	if ((!anon_vma1 || !anon_vma2) && (!vma ||
		list_is_singular(&vma->anon_vma_chain)))
		return 1;
	return anon_vma1 == anon_vma2;
}

anon_vma_merge.c

/* SPDX-License-Identifier: GPL-2.0 */

/* Upstream Linux v6.0: include/linux/rmap.h:162-167
 * Exact source lines, not a restored book listing or standalone program.
 */
static inline void anon_vma_merge(struct vm_area_struct *vma,
				  struct vm_area_struct *next)
{
	VM_BUG_ON_VMA(vma->anon_vma != next->anon_vma, vma);
	unlink_anon_vmas(next);
}

출처: v6.0 mm/rmap.c, v6.0 mm/mmap.c, v6.0 include/linux/rmap.h. Kernel source 범위에는 생략 부호를 넣지 않았으며, caller 전체를 수록하거나 semantic 검증을 완료했다는 뜻은 아닙니다.

참고 자료

Lorenzo Stoakes, The Linux Memory Manager, Chapter 7 §7.0.8–§7.0.11. Linux v6.18 mm/rmap.c의 clone·fork·unlink, mm/vma.c의 interval 갱신을 참고하실 수 있습니다.