Linux mem 2.1 Page 页帧管理详解

1. Page

系统通常把物理内存划分成4k大小的页帧page frame,每个页帧使用一个struct page的数据结构来进行管理,如果这个地址需要被访问还要映射页表PTE(pgd→p4d→pud→pmd→pte)。

理解page framestruct pagePTE在不同场景下的配置和关系,是理解内存管理的关键。

1.1 struct page定义

/*
 * Each physical page in the system has a struct page associated with
 * it to keep track of whatever it is we are using the page for at the
 * moment. Note that we have no way to track which tasks are using
 * a page, though if it is a pagecache page, rmap structures can tell us
 * who is mapping it.
 * 系统中的每个物理页面都有一个与之关联的struct page,以跟踪我们目前使用该页面的目的。请注意,我们无法跟踪哪些任务正在使用一个页面,但是如果它是一个pagecache页面,rmap结构可以告诉我们是谁在映射它。
 *
 * The objects in struct page are organized in double word blocks in
 * order to allows us to use atomic double word operations on portions
 * of struct page. That is currently only used by slub but the arrangement
 * allows the use of atomic double word operations on the flags/mapping
 * and lru list pointers also.
 * 结构页中的对象被组织成双字块,以便我们可以在结构页的部分使用原子双字操作。目前只被slub使用,但是这种安排允许在flags/mapping和lru列表指针上使用原子双字操作。
 */
struct page {
	/* First double word block -------------------------------------------------------------- */
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously 原子标记,其中一些可能异步更新 */
	union {
		struct address_space *mapping;	/* If low bit clear, points to
						 * inode address_space, or NULL.
						 * If page mapped as anonymous
						 * memory, low bit is set, and
						 * it points to anon_vma object
						 * or KSM private structure. See
						 * PAGE_MAPPING_ANON and
						 * PAGE_MAPPING_KSM.
                         * 如果低bit清零,则指向inode address_space,或NULL。
                         * 如果页面映射为匿名内存,低bit置位,并指向anon_vma对象或KSM私有结构。查看PAGE_MAPPING_ANON和PAGE_MAPPING_KSM。
						 */
		void *s_mem;			/* slab first object,slab中的第一个对象 */
		atomic_t compound_mapcount;	/* first tail page */
		/* page_deferred_list().next	 -- second tail page */
	};

	/* Second double word -------------------------------------------------------------- */
	union {
		pgoff_t index;		/* Our offset within mapping. 我们在文件mapping中的offset */
		void *freelist;		/* sl[aou]b first free object,sl[aou]b中第一个free的对象 */
		/* page_deferred_list().prev	-- second tail page */
	};

	union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \
	defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
		/* Used for cmpxchg_double in slub */
		unsigned long counters;
#else
		/*
		 * Keep _refcount separate from slub cmpxchg_double data.
		 * As the rest of the double word is protected by slab_lock
		 * but _refcount is not.
         * 将_refcount与slub cmpxchg_double数据分开。 因为双字的其余部分受slab_lock保护,但_refcount不受保护。
		 */
		unsigned counters;
#endif
		struct {

			union {
				/*
				 * Count of ptes mapped in mms, to show when
				 * page is mapped & limit reverse map searches.
				 * pte映射在mms中的次数,以显示页面何时被映射并限制反向映射搜索。
                 *
				 * Extra information about page type may be
				 * stored here for pages that are never mapped,
				 * in which case the value MUST BE <= -2.
				 * See page-flags.h for more details.
                 * 对于从未映射过的页面,可能会在此处存储有关页面类型的其他信息,在这种情况下,该值必须为<= -2。 有关更多详细信息,请参见page-flags.h。
				 */
				atomic_t _mapcount;

				unsigned int active;		/* SLAB */
				struct {			/* SLUB */
					unsigned inuse:16;
					unsigned objects:15;
					unsigned frozen:1;
				};
				int units;			/* SLOB */
			};
			/*
			 * Usage count, *USE WRAPPER FUNCTION* when manual
			 * accounting. See page_ref.h
			 */
			atomic_t _refcount;
		};
	};

	/*
	 * Third double word block -------------------------------------------------------------- 
	 *
	 * WARNING: bit 0 of the first word encode PageTail(). That means
	 * the rest users of the storage space MUST NOT use the bit to
	 * avoid collision and false-positive PageTail().
     * 警告:第一个word的bit0编码PageTail()。 这意味着其余存储空间的用户务必不要使用该位来避免冲突和错误的PageTail()。
	 */
	union {
		struct list_head lru;	/* Pageout list, eg. active_list,page链表例如active list
					 * protected by zone_lru_lock !
					 * Can be used as a generic list
					 * by the page owner.
					 */
		struct dev_pagemap *pgmap; /* ZONE_DEVICE pages are never on an
					    * lru or handled by a slab
					    * allocator, this points to the
					    * hosting device page map.
                        * ZONE_DEVICE页面从不在lru上,也不由slab分配器处理,这指向主机设备页面映射。
					    */
		struct {		/* slub per cpu partial pages,slub percpu部分的页面 */
			struct page *next;	/* Next partial slab */
#ifdef CONFIG_64BIT
			int pages;	/* Nr of partial slabs left */
			int pobjects;	/* Approximate # of objects */
#else
			short int pages;
			short int pobjects;
#endif
		};

		struct rcu_head rcu_head;	/* Used by SLAB 通过RCU销毁时由SLAB使用
						 * when destroying via RCU
						 */
		/* Tail pages of compound page,复合页的尾页 */
		struct {
			unsigned long compound_head; /* If bit zero is set */

			/* First tail page only */
#ifdef CONFIG_64BIT
			/*
			 * On 64 bit system we have enough space in struct page
			 * to encode compound_dtor and compound_order with
			 * unsigned int. It can help compiler generate better or
			 * smaller code on some archtectures.
			 */
			unsigned int compound_dtor;
			unsigned int compound_order;
#else
			unsigned short int compound_dtor;
			unsigned short int compound_order;
#endif
		};

#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS
		struct {
			unsigned long __pad;	/* do not overlay pmd_huge_pte
						 * with compound_head to avoid
						 * possible bit 0 collision.
						 */
			pgtable_t pmd_huge_pte; /* protected by page->ptl */
		};
#endif
	};

	/* Remainder is not double word aligned ----------------------------------------------  */
	union {
		unsigned long private;		/* Mapping-private opaque data:
					 	 * usually used for buffer_heads
						 * if PagePrivate set; used for
						 * swp_entry_t if PageSwapCache;
						 * indicates order in the buddy
						 * system if PG_buddy is set.
                         * 映射私有不透明数据:
                         * 如果设置了PagePrivate,通常用于buffer_heads。
                         * 如果PageSwapCache,则用于swp_entry_t;
                         * 如果设置了PG_buddy,则指示伙伴系统中的order。
						 */
#if USE_SPLIT_PTE_PTLOCKS
#if ALLOC_SPLIT_PTLOCKS
		spinlock_t *ptl;
#else
		spinlock_t ptl;
#endif
#endif
		struct kmem_cache *slab_cache;	/* SL[AU]B: Pointer to slab */
	};

#ifdef CONFIG_MEMCG
	struct mem_cgroup *mem_cgroup;
#endif

	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
     * 在所有RAM都映射到内核地址空间的机器上,我们可以简单地计算虚拟地址。
     * 在具有高内存的机器上,某些内存会动态映射到内核虚拟内存,因此我们需要一个位置来存储该地址。 请注意,该字段在x86 ...上可能是16位。
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
	int _last_cpupid;
#endif
}
Member Descript
.flags flags本来是用来表示page的状态的,后面在高位又塞入了多个index值:
bit function descript
63 - ↓ section 用于稀疏内存模型(SPARSEMEM)中的section index
node NUMA节点号, 标识该page属于哪一个节点
zone 内存域编号,标识该page属于哪一个zone
0 - ↑ flag 用来表示page的状态,每个bit表示一种状态。
.mapping
cond function descript
mapping = 0 swap cache page属于交换高速缓存页,page在swap out/in的过程当中,还没有swap完成。
swap地址swp_entry_t保存在`.private`成员当中
(mapping != 0) && (bit[0] = 0) file cache page属于页缓存或文件映射,mapping指向文件的地址空间address_space。
(mapping != 0) && (bit[0] != 0) anon or KSM 如果是匿名page,mapping指向anon_vma,anon_vma = (struct anon_vma *)(mapping - PAGE_MAPPING_ANON)
如果是KSM page,mapping指向KSM private structure
.index page在地址空间radix树page_tree中的对象索引号即页号, 表示该页在vm_file中的偏移页数,
._mapcount 被页表映射的次数,也就是说该page同时被多少个进程共享。
初始值为-1,如果只被一个进程的页表映射了,该值为0。
如果该page处于伙伴系统中,该值为PAGE_BUDDY_MAPCOUNT_VALUE(-128),内核通过判断该值是否为PAGE_BUDDY_MAPCOUNT_VALUE来确定该page是否属于伙伴系统。
._refcount 引用计数,表示内核中引用该page的次数。如果要操作该page, 引用计数会+1, 操作完成-1。
当该值为0时, 表示没有引用该page的位置,所以该page可以被解除映射,这往往在内存回收时是有用的
.lru 链表头,用于在各种链表上维护该页, 以便于按页将不同类别分组。
主要有3个用途:
对SLAB来说,通过lru将其链接到SLAB的partial、free等链表中;
在伙伴系统中,即当页空闲时,lru指向伙伴系统链表中相邻元素。
被分配以后,成为匿名内存或者文件内存,lru链接进page回收链表LRU的active_list或者inactive_list。
.private 映射私有不透明数据:
如果设置了PagePrivate,通常用于buffer_heads。
如果PageSwapCache,则用于swp_entry_t;
如果设置了PG_buddy,则指示伙伴系统中的order。

更进一步的详细解释如下:
a.如果设置了PG_private标志,则private字段指向struct buffer_head
b.如果设置了PG_compound,则指向struct page
c.如果设置了PG_swapcache标志,private存储了该page在交换分区中对应的位置信息swp_entry_t。
d.如果_mapcount = PAGE_BUDDY_MAPCOUNT_VALUE,说明该page位于伙伴系统,private存储该伙伴的order
.virtual 在内核虚拟地址比较少的机器上,如果分配了一个高内存的page,虽然得到了page结构,但是如果要访问page frame空间需要先进行虚拟地址映射才能访问。
在具有高内存的机器上,某些内存会动态映射到内核虚拟内存,因此我们需要一个位置来存储该地址。 请注意,该字段在x86 ...上可能是16位。
在所有RAM都映射到内核地址空间的机器上,我们可以简单地计算虚拟地址。
.s_mem slab第一个对象
.freelist 所在sl[aou]b的第一个free对象
.inuse 在当前slub中,已经分配的slub对象数量。
.objects 在当前slub中,总的slub对象数量。
.frozen 表示只能由活动CPU分配slub,其他CPU只能向其释放slub。
.slab_cache 指向本页关联的SLAB描述符。

以下是一些关键成员的详细说明:

  • .flags

.flags成员中不仅仅存放了标志,还存放了node、zone、稀疏模式的section等信息:

在这里插入图片描述

flags布局相关的宏定义:

linux-source-4.15.0\include\linux\mm.h:

/*
 * The zone field is never updated after free_area_init_core()
 * sets it, so none of the operations on it need to be atomic.
 */

/* Page flags: | [SECTION] | [NODE] | ZONE | [LAST_CPUPID] | ... | FLAGS | */
#define SECTIONS_PGOFF		((sizeof(unsigned long)*8) - SECTIONS_WIDTH)
#define NODES_PGOFF		(SECTIONS_PGOFF - NODES_WIDTH)
#define ZONES_PGOFF		(NODES_PGOFF - ZONES_WIDTH)
#define LAST_CPUPID_PGOFF	(ZONES_PGOFF - LAST_CPUPID_WIDTH)

linux-source-4.15.0\include\linux\page-flags-layout.h:

/*
 * page->flags layout:
 *
 * There are five possibilities for how page->flags get laid out.  The first
 * pair is for the normal case without sparsemem. The second pair is for
 * sparsemem when there is plenty of space for node and section information.
 * The last is when there is insufficient space in page->flags and a separate
 * lookup is necessary.
 *
 * No sparsemem or sparsemem vmemmap: |       NODE     | ZONE |             ... | FLAGS |
 *      " plus space for last_cpupid: |       NODE     | ZONE | LAST_CPUPID ... | FLAGS |
 * classic sparse with space for node:| SECTION | NODE | ZONE |             ... | FLAGS |
 *      " plus space for last_cpupid: | SECTION | NODE | ZONE | LAST_CPUPID ... | FLAGS |
 * classic sparse no space for node:  | SECTION |     ZONE    | ... | FLAGS |
 */

FLAGS部分相关的宏定义:

linux-source-4.15.0\include\linux\page-flags.h:

enum pageflags {
	PG_locked,		/* Page is locked. Don't touch. */
	PG_error,
	PG_referenced,
	PG_uptodate,
	PG_dirty,
	PG_lru,
	PG_active,
	PG_waiters,		/* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
	PG_slab,
	PG_owner_priv_1,	/* Owner use. If pagecache, fs may use*/
	PG_arch_1,
	PG_reserved,
	PG_private,		/* If pagecache, has fs-private data */
	PG_private_2,		/* If pagecache, has fs aux data */
	PG_writeback,		/* Page is under writeback */
	PG_head,		/* A head page */
	PG_mappedtodisk,	/* Has blocks allocated on-disk */
	PG_reclaim,		/* To be reclaimed asap */
	PG_swapbacked,		/* Page is backed by RAM/swap */
	PG_unevictable,		/* Page is "unevictable"  */
#ifdef CONFIG_MMU
	PG_mlocked,		/* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
	PG_uncached,		/* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
	PG_hwpoison,		/* hardware poisoned page. Don't touch */
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
	PG_young,
	PG_idle,
#endif
	__NR_PAGEFLAGS,

	/* Filesystems */
	PG_checked = PG_owner_priv_1,

	/* SwapBacked */
	PG_swapcache = PG_owner_priv_1,	/* Swap page: swp_entry_t in private */

	/* Two page bits are conscripted by FS-Cache to maintain local caching
	 * state.  These bits are set on pages belonging to the netfs's inodes
	 * when those inodes are being locally cached.
	 */
	PG_fscache = PG_private_2,	/* page backed by cache */

	/* XEN */
	/* Pinned in Xen as a read-only pagetable page. */
	PG_pinned = PG_owner_priv_1,
	/* Pinned as part of domain save (see xen_mm_pin_all()). */
	PG_savepinned = PG_dirty,
	/* Has a grant mapping of another (foreign) domain's page. */
	PG_foreign = PG_owner_priv_1,

	/* SLOB */
	PG_slob_free = PG_private,

	/* Compound pages. Stored in first tail page's flags */
	PG_double_map = PG_private_2,

	/* non-lru isolated movable page */
	PG_isolated = PG_reclaim,
};
name descript
PG_locked 指定了页是否被锁定。 如果该比特未被置位, 说明有使用者正在操作该page, 则内核的其他部分不允许访问该页,防止内存管理出现竞态条件。
PG_error 如果涉及该page的I/O操作发生了错误,则该位被设置。
PG_referenced 表示page刚刚被访问过。
PG_uptodate 表示page的数据已经与后备存储器是同步的。即页的数据已经从块设备读取,且没有出错,数据是最新的。
PG_dirty 与后备存储器中的数据相比,该page的内容已经被修改。 出于性能能的考虑,页并不在每次改变后立即回写, 因此内核需要使用该标识来表明页面中的数据已经改变, 应该在稍后刷出。
PG_lru 表示该page处于LRU链表上,这有助于实现页面的回收和切换。内核使用两个最近最少使用(least recently used-LRU)链表来区别活动和不活动页. 如果页在其中一个链表中, 则该位被设置。
PG_active page处于inactive LRU链表。PG_active和PG_referenced一起控制该page的活跃程度,这在内存回收时将会非常有用,当位于LRU active_list链表上的页面该位被设置, 并在页面移除时清除该位, 它标记了页面是否处于活动状态。
PG_waiters 页面有等待者,请检查其等待队列。本标志必须是bit7,并且与“ PG_locked”在同一字节中。
PG_slab 该page属于slab分配器
PG_owner_priv_1 -
PG_arch_1 特定体系结构的页面状态位。一般的代码保证当页面首次进入页面缓存时,该位将被清除。
PG_reserved 保留给内核或没有使用的页。设置该标志,防止该page被交换到swap。
PG_private pagecache下如果设置了PG_private标志,则private字段指向struct buffer_head
PG_private_2 If pagecache, has fs aux data
PG_writeback page中的数据正在被回写到后备存储器
PG_head A head page
PG_mappedtodisk 表示page中的数据在后备存储器中有对应
PG_reclaim 表示该page要被回收。当PFRA决定要回收某个page后,需要设置该标志。
PG_swapbacked 该page的后备存储器是swap
PG_unevictable 该page被锁住,不能交换,并会出现在LRU_UNEVICTABLE链表中。它包括的几种page:ramdisk或ramfs使用的页, shm_locked、mlock锁定的页
PG_mlocked 该page在vma中被锁定,一般是通过系统调用mlock()锁定了一段内存
PG_uncached page被映射成uncached
PG_hwpoison 硬件毒药page,不要碰它
PG_young -
PG_idle -

系统定义了一批专门操作flags的函数,如PageReserved()、PageDirty()、PageWriteback():

include\linux\page-flags.h:

#define TESTPAGEFLAG(uname, lname, policy)				\
static __always_inline int Page##uname(struct page *page)		\
	{ return test_bit(PG_##lname, &policy(page, 0)->flags); }

#define PAGEFLAG(uname, lname, policy)					\
	TESTPAGEFLAG(uname, lname, policy)				\
	SETPAGEFLAG(uname, lname, policy)				\
	CLEARPAGEFLAG(uname, lname, policy)

PAGEFLAG(Dirty, dirty, PF_HEAD) TESTSCFLAG(Dirty, dirty, PF_HEAD)
	__CLEARPAGEFLAG(Dirty, dirty, PF_HEAD)
  • .virtual

在某些架构中,分配的高端内存page初始状态是没有内核虚拟地址映射的,需要使用kmap()手动进行映射。把映射后的内核虚拟地址保存到.virtual成员或者一个全局hash链表中,这样后续相同的page的访问可以直接使用这个内核虚拟地址。

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}

↓

page_address()

1.2 struct page的初始化

在系统初始化的时候,所有struct page结构被初始化成:

native_pagetable_init() → paging_init() → zone_sizes_init() → free_area_init_nodes() → free_area_init_node() → free_area_init_core() → memmap_init() → memmap_init_zone() → memmap_init_zone() → __init_single_page():

static void __meminit __init_single_page(struct page *page, unsigned long pfn,
				unsigned long zone, int nid, bool zero)
{
    /* (1) 整个结构体清零 */
	if (zero)
		mm_zero_struct_page(page);
    /* (2) .flags成员中的:zone、nid、section赋值 */
	set_page_links(page, zone, nid, pfn);
    /* (3) ._refcount成员赋值1 */
	init_page_count(page);
    /* (4) ._mapcount成员赋值-1 */
	page_mapcount_reset(page);
    /* (5) .flags成员中的cpuid赋值 */
	page_cpupid_reset_last(page);

	INIT_LIST_HEAD(&page->lru);
#ifdef WANT_PAGE_VIRTUAL
	/* The shift won't overflow because ZONE_NORMAL is below 4G. */
	if (!is_highmem_idx(zone))
		set_page_address(page, __va(pfn << PAGE_SHIFT));
#endif
}

1.3 struct page的存放位置

一个struct page结构体在ubuntu18.04 x84_64下的大小为64字节。这样算下来,管理一个4k大小的物理内存成本还是挺高的:64byte(struct page) + 8byte(pte) + pgd/p4d/pud/pmd。

所以需要精细的管理存储struct page结构体的内存,为此内核发展出了四种管理模式:

Config Mode Descript
CONFIG_FLATMEM 内存物理地址空间连续 用连续的空间来管理`struct page`,单数组计算非常简单,但是在物理地址有空洞的场景下比较浪费内存。
CONFIG_DISCONTIGMEM 内存物理地址空间不连续,有空洞 使用多个数组来管理多块内存的`struct page`,避开了空洞的开销。
CONFIG_SPARSEMEM 不连续+hotplug 为了解决DISCONTIGMEM模式不支持内存hotplug而生的,两级数组(section+page),可以动态的创建和释放。
这种模式下为了应对page查找section,把section index存储在page->flags中,这样会存在bit不够用的风险。
CONFIG_SPARSEMEM_VMEMMAP SPARSEMEM的优化 为了解决SPARSEMEM模式下,flags中section bit不够用的问题
按照完整的物理内存连续地址(包括空洞)来分配`struct page`的存储虚拟空间,但是空洞处不分配实际的物理内存,这样在计算时可以做到和FLATMEM模式一样。

有一篇文章详细讲述了这几种模式,可以参考Linux内存模型

可以根据页帧号pfn(page frame number)找到对应的struct page指针,也可以根据struct page指针反查到对应的pfn。

linux-source-4.15.0\include\asm-generic\memory_model.h:

/*
 * supports 3 memory models.
 */
#if defined(CONFIG_FLATMEM)

#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)
#elif defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn)			\
({	unsigned long __pfn = (pfn);		\
	unsigned long __nid = arch_pfn_to_nid(__pfn);  \
	NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})

#define __page_to_pfn(pg)						\
({	const struct page *__pg = (pg);					\
	struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));	\
	(unsigned long)(__pg - __pgdat->node_mem_map) +			\
	 __pgdat->node_start_pfn;					\
})

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

#elif defined(CONFIG_SPARSEMEM)
/*
 * Note: section's mem_map is encoded to reflect its start_pfn.
 * section[i].section_mem_map == mem_map's address - start_pfn;
 */
#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */

/*
 * Convert a physical address to a Page Frame Number and back
 */
#define	__phys_to_pfn(paddr)	PHYS_PFN(paddr)
#define	__pfn_to_phys(pfn)	PFN_PHYS(pfn)

#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page

CONFIG_FLATMEM模式下,mem_map[]的初始化:

native_pagetable_init() → paging_init() → zone_sizes_init() → free_area_init_nodes() → free_area_init_node() → alloc_node_mem_map():

static void __ref alloc_node_mem_map(struct pglist_data *pgdat)
{
	unsigned long __maybe_unused start = 0;
	unsigned long __maybe_unused offset = 0;

	/* Skip empty nodes */
	if (!pgdat->node_spanned_pages)
		return;

	start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
	offset = pgdat->node_start_pfn - start;
	/* ia64 gets its own node_mem_map, before this, without bootmem */
	if (!pgdat->node_mem_map) {
		unsigned long size, end;
		struct page *map;

		/*
		 * The zone's endpoints aren't required to be MAX_ORDER
		 * aligned but the node_mem_map endpoints must be in order
		 * for the buddy allocator to function correctly.
		 */
		end = pgdat_end_pfn(pgdat);
		end = ALIGN(end, MAX_ORDER_NR_PAGES);
		size =  (end - start) * sizeof(struct page);
        /* (1) 分配空间 */
		map = alloc_remap(pgdat->node_id, size);
		if (!map)
			map = memblock_virt_alloc_node_nopanic(size,
							       pgdat->node_id);
		pgdat->node_mem_map = map + offset;
	}
	pr_debug("%s: node %d, pgdat %08lx, node_mem_map %08lx\n",
				__func__, pgdat->node_id, (unsigned long)pgdat,
				(unsigned long)pgdat->node_mem_map);
#ifndef CONFIG_NEED_MULTIPLE_NODES
	/*
	 * With no DISCONTIG, the global mem_map is just set as node 0's
	 */
	if (pgdat == NODE_DATA(0)) {
        /* (2) 把空间赋值给mem_map */
		mem_map = NODE_DATA(0)->node_mem_map;
#if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM)
		if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
			mem_map -= offset;
#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */
	}
#endif
}

1.4 page frame的物理地址和虚拟地址

在x86_64等64位架构中,因为内核虚拟空间较大,所以直接把所有物理内存平板映射到内核虚拟地址当中,物理地址内核虚拟地址仅仅一个PAGE_OFFSET的偏移:

内核虚拟地址 = PAGE_OFFSET + 物理地址

相应的物理地址内核虚拟地址转换宏的定义为:

linux-source-4.15.0\arch\x86\include\asm\page.h:

#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x)		__phys_addr((unsigned long)(x))

所以我们分配得到一个struct page以后,我们就能得到pfn,再根据pfn计算得到物理地址,再根据物理地址得到内核虚拟地址:

// page → pfn → page frame physical address  → page frame kernel virtual address:

#define page_to_virt(page)	pfn_to_virt(page_to_pfn(page))

#  define page_to_pfn(page) ((page) - mem_map)

#define pfn_to_virt(pfn)	__va((pfn) << PAGE_SHIFT)

对于有高端内存的架构,因为内核虚拟地址空间不够,部分的内存是没有办法全部映射到内核虚拟地址当中的。分配了这种高端内存(__GFP_HIGHMEM)的page以后,并不得到page frame的虚拟地址,内核访问之前需要调用kmap()进行映射。

2. page fault

论到page的应用,除了buddy和slub静态分配时的链接关系,最复杂的还是各种组合场景下的处理。

1、内核态的内存分配和映射比较简单,一旦建立一般是系统不会再动态操作。

2、用户态的内存管理就相当复杂。最基础的内存页有两种匿名页(anon page)和文件缓存页(page cache)。需要在此基础上处理以下场景:

  • 首次访问,Lazy分配。因为用户态内存分配采取延迟分配策略,初始分配后都只是分配了vma虚拟地址,并没有分配实际的物理内存页面(page frame)。除非指定了VM_LOCKED,它会立即分配内存。
    (1) 初始状态。文件缓存页的vma中记录了其实虚拟地址和长度,以及与之关联的文件句柄和offset;匿名页的vma中只记录了虚拟地址和长度,至于内存它只需要内容全0的页帧即可。
    (2) 初次访问。因为物理内存和地址映射都不存在,第一次虚拟地址访问会触发page fault,在缺页异常处理中分配物理内存页帧、建立地址映射、拷贝需要的内容。
    (3) 匿名页面page fault处理。
    (3.1) 读写匿名页面。首先分配一个物理页帧(page frame)并且清零,然后创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_anonymous_page()
    (4) 文件缓存页page fault处理。文件缓存页在还分为共享内存私有内存:共享内存使用MAP_SHARED进行的mmap(),对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享;私有内存使用MAP_PRIVATE进行的mmap(),对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。所以总共分为以下4种情况:
    (4.1) 私有文件缓存页。首先分配一个物理页帧,然后把该page加入到文件缓存树中并且读入相应的文件内容,然后创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_fault()→do_read_fault()
    (4.2) 共享文件缓存页。为了支持对共享内存的写操作能刷入内存,需要把pte的dirty反映到page cache dirty,在mmap()的时候会对pte属性进行降级去掉写属性,变成只读。首先分配一个物理页帧,然后把该page加入到文件缓存树中并且读入相应的文件内容,然后创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来。这时页面设置为只读,下次的写操作会触发page fault。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_fault()→do_read_fault()
    (4.3) 私有文件缓存页。在缺页异常处理中,首先分配一个物理页帧,然后把对应文件缓存树page中的内容拷贝道新page当中,然后创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_fault()→do_cow_fault()
    (4.4) 共享文件缓存页。在缺页异常处理中,首先分配一个物理页帧,然后把该page加入到文件缓存树中并且读入相应的文件内容,然后创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来,并把page设置成dirty。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_fault()→do_shared_fault()

  • 后续访问,writenotify。经过首次访问以后,匿名页(anon page)和文件缓存页(page cache)的物理页面和映射关系都已经建立起来,可以正常的访问不会再发生page fault异常。但是有一种情况例外就是共享文件缓存页的写操作。
    (1) 共享文件缓存页降级为只读。为了能在写操作的时候把page设置为dirty,在第一次读操作触发page fault或者系统回刷dirty page以后,把共享文件缓存页降级成只读。
    (2) 共享文件缓存页。降级后的第一次写操作会触发没有访问权限的page fault。在缺页异常处理中,把页面恢复成可写,并且设置page的dirty标志,以通知系统回刷脏页到文件系统中。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_wp_page()
    (3) 后续写。在把页面恢复成可写以后,后续的写操作不会触发page fault了。当系统把脏页回写,并且清除掉dirty标志,继续把页面降级成只读。形成周而复始的循环。

  • 内存回收。系统内存不够用时需要回收,只会回收用户态内存。
    (1) 回收前。内存都加入到回收链表lru当中,匿名页加入到LRU_INACTIVE_ANON和LRU_ACTIVE_ANON链表,文件缓存页加入到LRU_INACTIVE_FILE和LRU_ACTIVE_FILE当中。由PG_referenced和PG_active来控制inactive和active状态的迁移。
    (2) 回收处理。匿名页会被swap出,内存页帧(page frame)被系统回收,地址映射不会被回收PTE用来记录swap信息(P标志为0,物理地址改为保存swp_entry_t);而文件缓存页会被直接丢弃,地址映射PTE也会被释放,虚拟地址和文件的对应关系有vma记录。
    (3) 回收后。访问已经被回收的虚拟地址,由于物理页面已经不存在,会发生page fault,需要在缺页异常处理中重新构建内存:分配新的物理页面(page frame),从backer中读取内容到新页面,建立起新页面和原虚拟地址之间的映射关系。
    (4) 读写匿名页面。在缺页异常处理中,首先根据PTE找到swp_entry_t,然后分配一个swap cache page内存页来读取swap中的原内容,读完以后把这个page创建映射关系(pgd→p4d→pud→pmd→pte)把虚拟地址和页帧的物理地址连续起来。相应的调用链为:__handle_mm_fault()→handle_pte_fault()→do_swap_page()
    (5) 读写文件缓存页。因为所有文件缓存页的物理页面和映射结构都已经丢弃,所以所有的访问和首次访问一样。

  • 写时复制COW(Copy On Write)。为了节约物理内存,减少进程创建时资源和时间的消耗,父进程在调用 fork () 生成子进程时,子进程与父进程会共享同一内存区。只有当其中一进程进行写操作时,系统才会为其另外分配内存页面。这就是写时复制机制 (copy on write) 的意思。
    (1) 支持写时复制的页面。并不是所有的页面都需要写时复制,只有私有+可写的页面,在写入时才需要触发COW。这种基本就是存储数据用的匿名页面。
    (2) 进程创建。在复制mm时,碰到私有+可写的页面,复制映射结构pte,将父进程和子进程这部分地址都去掉写权限,设置成只读。
    (3) 页面。当父子进程任一对页面进行写操作,会触发page fault,在缺页异常处理中分配新的页面,拷贝原页面内容到新页面,并且更新pte页面中的物理地址。__handle_mm_fault()→handle_pte_fault()→do_wp_page()

  • KSM(Kernel Samepage Merging)
    在内核中还有一种节约内存的方法,把相同内容的页面合并成同一个COW页面。如果有写操作发生,又分裂成多个页面。
    在这里插入图片描述
    struct page数据结构在不同场景下数据成员的含义:
    在这里插入图片描述

本来是准备详细分析一下page fault的详细流程的,但是发现一篇神文Linux内存管理之page fault处理,让我觉得分析得已经出神入化了。忍不住搬过来:

Read the fucking source code! --By 鲁迅
A picture is worth a thousand words. --By 高尔基

2.1 概述

在这里插入图片描述

2.2 do_page_fault()

在这里插入图片描述

2.3 handle_mm_fault()

在这里插入图片描述

2.4 do_fault()

do_fault函数用于处理文件页异常,包括以下三种情况:

  • 读文件页错误;
  • 写私有文件页错误;
  • 写共享文件页错误;
    在这里插入图片描述

2.5 do_anonymous_page()

匿名页的缺页异常处理调用本函数,在以下情况下会触发:

  • malloc/mmap分配了进程地址空间区域,但是没有进行映射处理,在首次访问时触发;
  • 用户栈不够的情况下,进行栈区的扩大处理;
    在这里插入图片描述

2.6 do_swap_page()

如果访问Swap页面出错(页面不在内存中),则从Swap cache或Swap文件中读取该页面。
在这里插入图片描述

2.7 do_wp_page()

do_wp_page函数用于处理写时复制(copy on write),会在以下三种情况处理:

  • 创建子进程时,父子进程会以只读方式共享私有的匿名页和文件页,当试图写的时候,触发页错误异常,从而复制物理页,并创建映射;
  • 进程创建私有文件映射,读访问后触发异常,将文件页读入到page cache中,并以只读模式创建映射,之后发生写访问后,触发COW;
  • writenotify功能,把共享文件映射降级成只读。
    在这里插入图片描述

关键的复制工作是由wp_page_copy完成的:
在这里插入图片描述

3. COW

在进程创建时针对私有+可写的页面,对页面进行降级成只读。

sys_fork()→do_fork()→copy_process()→copy_mm()→dup_mm()→dup_mmap()→copy_page_range()→copy_pud_range()→copy_pmd_range()→copy_pte_range()→copy_one_pte():

static inline unsigned long
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
		unsigned long addr, int *rss)
{

	/*
	 * If it's a COW mapping, write protect it both
	 * in the parent and the child
	 */
	/* 如果拷贝的是`私有+可写`的页面,对页面进行降级成只读。 */
	if (is_cow_mapping(vm_flags)) {
		ptep_set_wrprotect(src_mm, addr, src_pte);
		pte = pte_wrprotect(pte);
	}

}

static inline bool is_cow_mapping(vm_flags_t flags)
{
	return (flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE;
}

4. Swap

在系统进程内存回收时,用户态匿名内存不能被直接释放,它只能交换(swap)出去。

4.1 swap out

内核在回收anonymous pages时会把它们的内容复制到一个swap area的某个slot中保存起来,这个过程叫做swap out,对应的执行函数是add_to_swap()。

  • 首先需要调用get_swap_page()函数从swap area中分配空余的slot,然后增加swap cache(交换缓存)对准备swap out的页面的指向,并标记这个页面的状态为"dirty"。
    在这里插入图片描述

  • 等到调用swap_writepage(),才会执行真正的I/O操作,将页面的内容写入外部的swap area,然后清除swap cache对页面的指向,释放页面所占的内存。此时PTE的内容指向swp_entry_t。
    在这里插入图片描述

4.2 swap in

内核在将一个页面换入内存后,会增加一个swap cache对这个页面的指向。当进程试图换入一个页面时,会首先从swap cache中查找,如果找到了,则填写对应的PTE,将对外部swap slot的指向转化为对这个内存页面的指向。
在这里插入图片描述

4.3 swap in/swap out

swap cache的作用还体现在一个进程试图swap in一个正在被swap out的页面时。比如现在一个page frame的内容正在被写入磁盘,此时进程B又试图访问这个页面,那么page fault handler将根据swap cache对该页面的指向,找到其物理地址,填入进程B对应的PTE中。这就是为什么在上文介绍的swap out的过程中也需要swap cache的原因。
在这里插入图片描述

参考资料:

1、Linux中的内存回收[一]
2、Linux内核内存回收逻辑和算法(LRU)
3、内存管理—页框回收
4、linux kernel内存回收机制
5、Linux内存回收之LRU链表和第二次机会法
6、Linux内存模型
7、内存管理-页面(page)
8、page 数据结构
9、Linux中的Anonymous Pages和Swap [一]
10、Linux中的Anonymous Pages和Swap [二]
11、逆向映射的演进
12、Copy On Write机制了解一下
13、vmalloc不连续内存管理
14、深入了解Linux - COW写时拷贝实现原理
15、内存在父子进程间的共享时间及范围
16、Linux内存管理之page fault处理
17、Android匿名共享内存(Ashmem)原理

猜你喜欢

转载自blog.csdn.net/pwl999/article/details/109539348
今日推荐