时间轴

2025-11-20

init

关于地址

地址空间不隔离

1.恶意的进程可以随意修改别的进程的内存

2.内存使用的效率很低,如当内存短缺时需要把进程中的所有数据交换到交换分区

早期内存的使用方法

  • 分段(主要思想:将虚拟地址空间与物理地址空间做一一的映射,程序运行需要全部装载到内存中)
  • 分页(主要思想:程序运行时,需要哪个页面才分配哪个页面,即按需分配,长期不需要的可以交换到磁盘中)

地址概念:

  • 逻辑地址(Intel 专用术语,产生的和段相关的偏移地址那个部分)

  • 线性地址(Intel 专用术语,逻辑地址转换到物理地址的一个中间层,在分段中,逻辑地址是段偏移地址,再加上基地址即变成线性地址)

  • 虚拟地址 (逻辑地址和线性地址统称虚拟地址)

  • 物理地址(CPU 通过外部总线访问物理内存需要的地址)

  • 进程地址空间

内存管理硬件结构图

TLB也称快表,缓存了页表转换的页表项

内存管理概述

Linux 内存管理模块图

从进程角度看内存管理:

进程角度看内存管理

  1. 典型的 32 位 Linux 系统虚拟地址空间布局:

    • 用户空间(0-3G)
      • 代码段 & 数据段:存放程序的可执行指令和初始化数据。
      • 堆空间:用于动态内存分配(如 malloc),向高地址方向增长。
      • mmap 空间:用于文件映射、共享内存等,向低地址方向增长。
      • 栈空间:用于函数调用和局部变量,向低地址方向增长。
    • 内核空间(3G-4G)
      • 所有进程共享这部分地址空间,用于内核代码、数据和临时映射。

    虚拟地址内存布局

  2. 物理内存映射

    • 线性映射:内核将低端物理内存(如 ZONE_NORMAL)直接、线性地映射到内核地址空间的固定区域,访问速度快。

    • 高端映射:对于高端物理内存(如 ZONE_HIGHMEM),内核需要时才将其临时映射到内核地址空间的高端部分。

  3. 进程层面:task_structmm_struct

    • task_struct:每个进程在内核中都由一个 task_struct 结构体描述,它包含了进程的所有信息,其中的 mm 成员指向了进程的内存描述符。

    • mm_struct:这是进程的内存描述符,是管理进程虚拟地址空间的核心数据结构。它主要包含两个关键成员:

      • mmap:指向进程的虚拟内存区域(VMA)链表或红黑树的根。
      • pgd:指向进程页全局目录(Page Global Directory),是页表的顶级入口。
  4. 虚拟内存区域(VMA)管理

    • VMA(Virtual Memory Area):内核用 vm_area_struct 结构体来描述一段连续的虚拟地址空间。每个 VMA 代表一段具有相同属性(如可读、可写、可执行)的虚拟内存,例如代码段、数据段、堆、栈、mmap 映射区等。

    • mm_struct 通过 mmap 成员将所有 VMA 组织起来,方便内核进行查找、管理和操作。

  5. mem_map[] 数组:内核用一个全局的 mem_map 数组来管理所有物理内存页,数组中的每一个元素都是一个 struct page 结构体,代表一个物理页帧。

  6. ZONE 管理:物理内存被划分为不同的内存管理区(ZONE),例如 ZONE_DMAZONE_NORMALZONE_HIGHMEM 等,每个区有不同的用途和管理方式。

内存分配器简图

常用数据结构

linux 内存管理常用数据结构关系

数据结构 主要定义文件 补充说明
mm_struct include/linux/mm_types.h 也会在 include/linux/mm.h 中被广泛引用和操作
vm_area_struct (VMA) include/linux/mm_types.h 描述一段连续的虚拟内存区域
struct page include/linux/mm_types.h 早期内核版本中定义在 include/linux/mm.h,新版本移至 mm_types.h
struct zone include/linux/mmzone.h 描述物理内存管理区(如 DMA、NORMAL、HIGHMEM)
struct pglist_data (pgdata) include/linux/mmzone.h NUMA 节点的内存描述符,管理该节点下的所有 zone
mem_map[] mm/memory.c 全局物理页数组,其地址由内核初始化时确定,每个 NUMA 节点也有自己的 node_mem_map
pte_t / 页表项 arch/arm64/include/asm/pgtable-types.h 页表项类型与页表操作是架构相关的

struct mm_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// include/linux/mm_types.h
struct mm_struct {
struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
u64 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
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 */
unsigned long highest_vm_end; /* highest vma end address */
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;

/**
* @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;

#ifdef CONFIG_MMU
atomic_long_t pgtables_bytes; /* PTE page table pages */
#endif
int map_count; /* number of VMAs */

spinlock_t page_table_lock; /* Protects page tables and some
* counters
*/
/*
* With some kernel config, the current mmap_lock's offset
* inside 'mm_struct' is at 0x120, 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
*/


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 */
unsigned long 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 */

/*
* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
struct mm_rss_stat rss_stat;

struct linux_binfmt *binfmt;

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

unsigned long flags; /* Must use atomic bitops to access */

struct core_state *core_state; /* coredumping support */

#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) && !USE_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 the PTEs will be marked
* pte_numa. NUMA hinting faults will gather statistics and
* migrate pages to new nodes if necessary.
*/
unsigned long numa_next_scan;

/* Restart point for scanning and setting pte_numa */
unsigned long numa_scan_offset;

/* numa_scan_seq prevents two threads setting pte_numa */
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 or PROT_NUMA mapped page.
*/
atomic_t tlb_flush_pending;
#ifdef CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH
/* See flush_tlb_batched_pending() */
bool tlb_flush_batched;
#endif
struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
atomic_long_t hugetlb_usage;
#endif
struct work_struct async_put_work;

#ifdef CONFIG_IOMMU_SUPPORT
u32 pasid;
#endif
} __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[];
};

struct vm_area_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// include/linux/mm_types.h
/*
* This struct describes a virtual memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */

unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;

struct rb_node vm_rb;

/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;

/* Second cache line starts here. */

struct mm_struct *vm_mm; /* The address space we belong to. */

/*
* Access permissions of this VMA.
* See vmf_insert_mixed_prot() for discussion.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags; /* Flags, see mm.h. */

/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;

/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_lock &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */

#ifdef CONFIG_SWAP
atomic_long_t swap_readahead_info;
#endif
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

struct page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
// include/linux/mm_types.h

/*
* 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.
*
* If you allocate the page using alloc_pages(), you can use some of the
* space in struct page for your own purposes. The five words in the main
* union are available, except for bit 0 of the first word which must be
* kept clear. Many users use this word to store a pointer to an object
* which is guaranteed to be aligned. If you use the same storage as
* page->mapping, you must restore it to NULL before freeing the page.
*
* If your page will not be mapped to userspace, you can also use the four
* bytes in the mapcount union, but you must call page_mapcount_reset()
* before freeing it.
*
* If you want to use the refcount field, it must be used in such a way
* that other CPUs temporarily incrementing and then decrementing the
* refcount does not cause problems. On receiving the page from
* alloc_pages(), the refcount will be positive.
*
* If you allocate pages of order > 0, you can use some of the fields
* in each subpage, but you may need to restore some of their values
* afterwards.
*
* SLUB uses cmpxchg_double() to atomically update its freelist and
* counters. That requires that freelist & counters be adjacent and
* double-word aligned. We align all struct pages to double-word
* boundaries, and ensure that 'freelist' is aligned within the
* struct.
*/
#ifdef CONFIG_HAVE_ALIGNED_STRUCT_PAGE
#define _struct_page_alignment __aligned(2 * sizeof(unsigned long))
#else
#define _struct_page_alignment
#endif

struct page {
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
/*
* Five words (20/40 bytes) are available in this union.
* WARNING: bit 0 of the first word is used for PageTail(). That
* means the other users of this union MUST NOT use the bit to
* avoid collision and false-positive PageTail().
*/
union {
struct { /* Page cache and anonymous pages */
/**
* @lru: Pageout list, eg. active_list protected by
* lruvec->lru_lock. Sometimes used as a generic list
* by the page owner.
*/
struct list_head lru;
/* See page-flags.h for PAGE_MAPPING_FLAGS */
struct address_space *mapping;
pgoff_t index; /* Our offset within mapping. */
/**
* @private: Mapping-private opaque data.
* Usually used for buffer_heads if PagePrivate.
* Used for swp_entry_t if PageSwapCache.
* Indicates order in the buddy system if PageBuddy.
*/
unsigned long private;
};
struct { /* page_pool used by netstack */
/**
* @pp_magic: magic value to avoid recycling non
* page_pool allocated pages.
*/
unsigned long pp_magic;
struct page_pool *pp;
unsigned long _pp_mapping_pad;
unsigned long dma_addr;
union {
/**
* dma_addr_upper: might require a 64-bit
* value on 32-bit architectures.
*/
unsigned long dma_addr_upper;
/**
* For frag page support, not supported in
* 32-bit architectures with 64-bit DMA.
*/
atomic_long_t pp_frag_count;
};
};
struct { /* slab, slob and slub */
union {
struct list_head slab_list;
struct { /* Partial pages */
struct page *next;
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache *slab_cache; /* not slob */
/* Double-word boundary */
void *freelist; /* first free object */
union {
void *s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct { /* Tail pages of compound page */
unsigned long compound_head; /* Bit zero is set */

/* First tail page only */
unsigned char compound_dtor;
unsigned char compound_order;
atomic_t compound_mapcount;
unsigned int compound_nr; /* 1 << compound_order */
};
struct { /* Second tail page of compound page */
unsigned long _compound_pad_1; /* compound_head */
atomic_t hpage_pinned_refcount;
/* For both global and memcg */
struct list_head deferred_list;
};
struct { /* Page table pages */
unsigned long _pt_pad_1; /* compound_head */
pgtable_t pmd_huge_pte; /* protected by page->ptl */
unsigned long _pt_pad_2; /* mapping */
union {
struct mm_struct *pt_mm; /* x86 pgds only */
atomic_t pt_frag_refcount; /* powerpc */
};
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
};
struct { /* ZONE_DEVICE pages */
/** @pgmap: Points to the hosting device page map. */
struct dev_pagemap *pgmap;
void *zone_device_data;
/*
* ZONE_DEVICE private pages are counted as being
* mapped so the next 3 words hold the mapping, index,
* and private fields from the source anonymous or
* page cache page while the page is migrated to device
* private memory.
* ZONE_DEVICE MEMORY_DEVICE_FS_DAX pages also
* use the mapping, index, and private fields when
* pmem backed DAX files are mapped.
*/
};

/** @rcu_head: You can use this to free a page by RCU. */
struct rcu_head rcu_head;
};

union { /* This union is 4 bytes in size. */
/*
* If the page can be mapped to userspace, encodes the number
* of times this page is referenced by a page table.
*/
atomic_t _mapcount;

/*
* If the page is neither PageSlab nor mappable to userspace,
* the value stored here may help determine what this page
* is used for. See page-flags.h for a list of page types
* which are currently stored here.
*/
unsigned int page_type;

unsigned int active; /* SLAB */
int units; /* SLOB */
};

/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
atomic_t _refcount;

#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#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 ... ;)
*
* 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
} _struct_page_alignment;

struct zone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// include/linux/mmzone.h
struct zone {
/* Read-mostly fields */

/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long _watermark[NR_WMARK];
unsigned long watermark_boost;

unsigned long nr_reserved_highatomic;

/*
* We don't know if the memory that we're going to allocate will be
* freeable or/and it will be released eventually, so to avoid totally
* wasting several GB of ram we must reserve some of the lower zone
* memory (otherwise we risk to run OOM on the lower zones despite
* there being tons of freeable ram on the higher zones). This array is
* recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
* changes.
*/
long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
int node;
#endif
struct pglist_data *zone_pgdat;
struct per_cpu_pages __percpu *per_cpu_pageset;
struct per_cpu_zonestat __percpu *per_cpu_zonestats;
/*
* the high and batch values are copied to individual pagesets for
* faster access
*/
int pageset_high;
int pageset_batch;

#ifndef CONFIG_SPARSEMEM
/*
* Flags for a pageblock_nr_pages block. See pageblock-flags.h.
* In SPARSEMEM, this map is stored in struct mem_section
*/
unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;

/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* present_early_pages is present pages existing within the zone
* located on memory available since early boot, excluding hotplugged
* memory.
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* cma pages is present pages that are assigned for CMA use
* (MIGRATE_CMA).
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*
* Locking rules:
*
* zone_start_pfn and spanned_pages are protected by span_seqlock.
* It is a seqlock because it has to be read outside of zone->lock,
* and it is done in the main allocator path. But, it is written
* quite infrequently.
*
* The span_seq lock is declared along with zone->lock because it is
* frequently read in proximity to zone->lock. It's good to
* give them a chance of being in the same cacheline.
*
* Write access to present_pages at runtime should be protected by
* mem_hotplug_begin/end(). Any reader who can't tolerant drift of
* present_pages should get_online_mems() to get a stable value.
*/
atomic_long_t managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
#if defined(CONFIG_MEMORY_HOTPLUG)
unsigned long present_early_pages;
#endif
#ifdef CONFIG_CMA
unsigned long cma_pages;
#endif

const char *name;

#ifdef CONFIG_MEMORY_ISOLATION
/*
* Number of isolated pageblock. It is used to solve incorrect
* freepage counting problem due to racy retrieving migratetype
* of pageblock. Protected by zone->lock.
*/
unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
/* see spanned/present_pages for more description */
seqlock_t span_seqlock;
#endif

int initialized;

/* Write-intensive fields used from the page allocator */
ZONE_PADDING(_pad1_)

/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];

/* zone flags, see below */
unsigned long flags;

/* Primarily protects free_area */
spinlock_t lock;

/* Write-intensive fields used by compaction and vmstats. */
ZONE_PADDING(_pad2_)

/*
* When free pages are below this point, additional steps are taken
* when reading the number of free pages to avoid per-cpu counter
* drift allowing watermarks to be breached
*/
unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* pfn where compaction free scanner should start */
unsigned long compact_cached_free_pfn;
/* pfn where compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
unsigned long compact_init_migrate_pfn;
unsigned long compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
/*
* On compaction failure, 1<<compact_defer_shift compactions
* are skipped before trying again. The number attempted since
* last failure is tracked with compact_considered.
* compact_order_failed is the minimum compaction failed order.
*/
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
/* Set to true when the PG_migrate_skip bits should be cleared */
bool compact_blockskip_flush;
#endif

bool contiguous;

ZONE_PADDING(_pad3_)
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
atomic_long_t vm_numa_event[NR_VM_NUMA_EVENT_ITEMS];
} ____cacheline_internodealigned_in_smp;

struct pglist_data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// include/linux/mmzone.h
/*
* On NUMA machines, each NUMA node would have a pg_data_t to describe
* it's memory layout. On UMA machines there is a single pglist_data which
* describes the whole memory.
*
* Memory statistics and page replacement data structures are maintained on a
* per-zone basis.
*/
typedef struct pglist_data {
/*
* node_zones contains just the zones for THIS node. Not all of the
* zones may be populated, but it is the full list. It is referenced by
* this node's node_zonelists as well as other node's node_zonelists.
*/
struct zone node_zones[MAX_NR_ZONES];

/*
* node_zonelists contains references to all zones in all nodes.
* Generally the first zones will be references to this node's
* node_zones.
*/
struct zonelist node_zonelists[MAX_ZONELISTS];

int nr_zones; /* number of populated zones in this node */
#ifdef CONFIG_FLATMEM /* means !SPARSEMEM */
struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
/*
* Must be held any time you expect node_start_pfn,
* node_present_pages, node_spanned_pages or nr_zones to stay constant.
* Also synchronizes pgdat->first_deferred_pfn during deferred page
* init.
*
* pgdat_resize_lock() and pgdat_resize_unlock() are provided to
* manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG
* or CONFIG_DEFERRED_STRUCT_PAGE_INIT.
*
* Nests above zone->lock and zone->span_seqlock
*/
spinlock_t node_size_lock;
#endif
unsigned long node_start_pfn;
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page
range, including holes */
int node_id;
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd; /* Protected by
mem_hotplug_begin/end() */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;

int kswapd_failures; /* Number of 'reclaimed == 0' runs */

#ifdef CONFIG_COMPACTION
int kcompactd_max_order;
enum zone_type kcompactd_highest_zoneidx;
wait_queue_head_t kcompactd_wait;
struct task_struct *kcompactd;
bool proactive_compact_trigger;
#endif
/*
* This is a per-node reserve of pages that are not available
* to userspace allocations.
*/
unsigned long totalreserve_pages;

#ifdef CONFIG_NUMA
/*
* node reclaim becomes active if more unmapped pages exist.
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */

/* Write-intensive fields used by page reclaim */
ZONE_PADDING(_pad1_)

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
/*
* If memory initialisation on large machines is deferred then this
* is the first PFN that needs to be initialised.
*/
unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif

/* Fields commonly accessed by the page reclaim scanner */

/*
* NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
*
* Use mem_cgroup_lruvec() to look up lruvecs.
*/
struct lruvec __lruvec;

unsigned long flags;

ZONE_PADDING(_pad2_)

/* Per-node vmstats */
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

mem_map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// mm/memory.c
#ifndef CONFIG_NUMA
unsigned long max_mapnr;
EXPORT_SYMBOL(max_mapnr);

struct page *mem_map;
EXPORT_SYMBOL(mem_map);
#endif

/*
* A number of key systems in x86 including ioremap() rely on the assumption
* that high_memory defines the upper bound on direct map memory, then end
* of ZONE_NORMAL. Under CONFIG_DISCONTIG this means that max_low_pfn and
* highstart_pfn must be the same; there must be no gap between ZONE_NORMAL
* and ZONE_HIGHMEM.
*/
void *high_memory;
EXPORT_SYMBOL(high_memory);

Linux 物理内存的初始化

  1. 系统启动时,ARM Linux 内核如何知道系统中有多大的内存空间
  2. 在 32 bit Linux内核中,用户空间和内核空间的比例通常是3:1,可以修改成2:2吗?
  3. 物理内存页面如何添加到伙伴系统中,是一页一页添加还是以2的几次幂来添加的?

DDR 简介

  • Bank

DDR Bank

DDR

页表映射

arm32

  • 支持四级映射
    • 全局目录项 PGDPage Global Directory
    • 上级目录项 PUDPage Upper Directory
    • 中间目录项 PMDPage Middle Directory
    • 页表项 (Page Table

Linux内核最早是基于x86做的,因此内核代码中常用上面这个术语来表示一级页表,二级页表等

虚拟地址到物理地址的转换

虚拟地址到物理地址的转换

一级页表表项

一级页表表项

  • Fault
    • 错误的表项
  • Page table
    • 一级页表的表项
    • bit[0~1]:用来表示这个页表项是一级页表还是段映射的表项
    • PXN:PL1是否可以执行这段代码,为0表示可执行,为1表示不可执行
    • NS: non security bit表示安全扩展的比特位
    • Domain: 指明所属的域,Linux只用到了3个域
    • bits[31:10] Page table base address 指向二级页表的基地址

Linux关于页表定义的一些宏

  • Section
    • 段映射的段表项

二级页表表项

二级页表表项

  • bit0 禁止执行标志,1表示禁止执行,0表示可执行
  • bit1 区分大页还是小页
  • C/B bit: 内存区域属性
  • TEX[2:0]:内存区域属性
  • AP[1:0]: 访问权限
  • S: 是否可共享
  • nG: 用于TLB

arm64

ARMv8-A架构

  • 支持48根地址线
  • 访问区域分成两部分(内核区和用户空间区),每个区域256TB
  • 支持4KB,16KB和64KB的页,支持3级或者4级映射

ARMv8有两个页表基地址,一个用于用户区一个用于内核区,通过bit63来选择

4KB大小页+4级映射

4KB大小页+4级映射

TODO

  1. 总结页表相关操作的宏
  2. start_kernel()->setup_arch()->paging_init()->map_lowmem()

static void __init create_mapping(struct map_desc *md)

kernel image区域建立页表映射,线性映射区域建立页表映射

研究如何建立内核页表的映射

  1. int remap_pfn_range() 研究如何由vma和虚拟地址以及物理地址pfn这些信息去建立页表映射的
  2. static int do_anonymous_page 研究这个缺页中断函数来观察:知道了vma和虚拟地址vaddr以及页表项如何来设置用户进程的页表

x86和arm32页表对比

ARM32的实现方式:平行页表

两套页表,一套为了迎合硬件,一套为了迎合Linux内核

ARM32的实现方式:平行页表

页面分配机制

伙伴系统

  • 伙伴算法是按2的幂次方大小进行分配内存块,这些内存被称为伙伴
  • 何为伙伴
    • 两个块大小相同
    • 两个块地址连续
    • 两个块必须是同一个大块分离出来的

伙伴系统

迁移类型

  • 为了解决碎片化的问题,在伙伴系统中引入了迁移类型
    • MIGRATE_UNMOVABLE: 在内存中有固定位置,不能随意移动,比如内核分配的内存
    • MIGRATE_MOVABLE: 可以随意移动,用户态app分配内存
    • MIGRATE_RECLAIMABLE: 不能移动可以删除回收,比如文件映射
  • 内存碎片化的产生

内存碎片化的产生

page3是被内核自身分配的函数使用了,比如alloc_page(GPF_KERNEL),所以它属于不可移动的页面,尽管其他页面都是空闲页面,page0到page7不能合成一块大的内存块

页面分配和分配掩码

页面核心分配函数

1
2
3
struct page *alloc_pages(gfp_t gfp_mask,unsigned int order)
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
alloc_page(gfp_mask)

gfp_mask标志位

gfp_mask标志位

gfp_mask标志位

  • 页面分配器是基于zone来设计的,因此页面的分配有必要确定哪些zone可以用作本次页面分配,系统会优先使用ZONE_HIGHMEM,然后才是ZONE_NORMAL
  • 页面分配的时候也需要知道当前分配是要从哪个迁移类型中分配内存

zone水位

zone水位

Linux内核中定义:

1
2
3
4
5
6
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};

页面释放函数

1
2
3
void free_pages(unsigned long addr,unsigned int order)
__free_page(page)
free_page(addr)

per-cpu高速页面缓存

  • 内核经常请求和释放单个页框,比如网卡驱动等等

  • 页面分配器分配和释放页面的时候需要申请一把锁: zone->lock

    • 为了提高单个页框的申请和释放效率,内核建立了per-cpu页面高速缓存池
    • 其中存放了若干预先分配好的页框
  • 当请求单个页框时,直接从本地cpu的页框高速缓存池中取出页框

    • 不必申请锁
    • 不必进行复杂的页框分配操作

(体现了预先建立缓存池的优势,而且是每个CPU有一个独立的缓存)

per-CPU数据结构

  • zone中有一个成员pageset字段指向per-cpu的高速缓存
  • struct per_cpu_pages数据结构
1
2
3
4
5
6
7
8
struct per_cpu_pages {
int count; /* number of pages in the list */
int high; /* high watermark, emptying needed */
int batch; /* chunk size for buddy add/remove */

/* Lists of pages, one per migrate type stored on the pcp-lists */
struct list_head lists[MIGRATE_PCPTYPES];
};

slab机制

当在内核中需要分配几十个字节的小块内存的时候,怎么办

slab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*
* Definitions unique to the original Linux SLAB allocator.
*/

struct kmem_cache {
struct array_cache __percpu *cpu_cache;

/* 1) Cache tunables. Protected by slab_mutex */
unsigned int batchcount;
unsigned int limit;
unsigned int shared;

unsigned int size;
struct reciprocal_value reciprocal_buffer_size;
/* 2) touched by every alloc & free from the backend */

unsigned int flags; /* constant flags */
unsigned int num; /* # of objs per slab */

/* 3) cache_grow/shrink */
/* order of pgs per slab (2^n) */
unsigned int gfporder;

/* force GFP flags, e.g. GFP_DMA */
gfp_t allocflags;

size_t colour; /* cache colouring range */
unsigned int colour_off; /* colour offset */
struct kmem_cache *freelist_cache;
unsigned int freelist_size;

/* constructor func */
void (*ctor)(void *obj);

/* 4) cache creation/removal */
const char *name;
struct list_head list;
int refcount;
int object_size;
int align;

/* 5) statistics */
#ifdef CONFIG_DEBUG_SLAB
unsigned long num_active;
unsigned long num_allocations;
unsigned long high_mark;
unsigned long grown;
unsigned long reaped;
unsigned long errors;
unsigned long max_freeable;
unsigned long node_allocs;
unsigned long node_frees;
unsigned long node_overflow;
atomic_t allochit;
atomic_t allocmiss;
atomic_t freehit;
atomic_t freemiss;

/*
* If debugging is enabled, then the allocator can add additional
* fields and/or padding to every object. size contains the total
* object size including these internal fields, the following two
* variables contain the offset to the user object and its size.
*/
int obj_offset;
#endif /* CONFIG_DEBUG_SLAB */
#ifdef CONFIG_MEMCG_KMEM
struct memcg_cache_params memcg_params;
#endif

struct kmem_cache_node *node[MAX_NUMNODES];
};

  • array_cache:这里是指的per-CPU的对象缓存池,每个CPU一个
    • 用来建立本地对象缓存池,每个CPU一个,有利于:
      • 让对象尽可能的使用同一个CPU上的cache,有利于提高效率
      • 不需要额外的自旋锁,避免锁的争用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/*
* struct array_cache
*
* Purpose:
* - LIFO ordering, to hand out cache-warm objects from _alloc
* - reduce the number of linked list operations
* - reduce spinlock operations
*
* The limit is stored in the per-cpu structure to reduce the data cache
* footprint.
*
*/
struct array_cache {
unsigned int avail;
unsigned int limit;
unsigned int batchcount;
unsigned int touched;
// entry用来保存空闲对象的实体
void *entry[]; /*
* Must have this definition in here for the proper
* alignment of array_cache. Also simplifies accessing
* the entries.
*
* Entries should not be directly dereferenced as
* entries belonging to slabs marked pfmemalloc will
* have the lower bits set SLAB_OBJ_PFMEMALLOC
*/
};


#ifndef CONFIG_SLOB
/*
* The slab lists for all objects.
*/
struct kmem_cache_node {
spinlock_t list_lock;

#ifdef CONFIG_SLAB
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif

#ifdef CONFIG_SLUB
unsigned long nr_partial;
struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif

};
  • batchcount:表示当本地的对象缓存池为空时,需要从共享缓存池中获取batchcount个对象到本地缓存池
  • limit: 当本地对象缓存池空闲数目大于limit时,需要释放一些对象
  • shared: 用于多核系统
  • size: 表示对象的长度
  • flags: 对象分配掩码
  • num: 一个slab中可以有多少个对象
  • gfporder: 一个slab占用多个2的order次方的物理页面
  • colour: 表示一个slab的有多个cache line用于着色
  • freelist_size:每个对象要占用1字节来存放freelist
  • name: 描述符的名称
  • object_size:对象实际大小
  • align: 对齐的长度

一个slab的组成

一个slab的组成

slab运行机制

slab运行机制

slab的回收

  • 如果一个slab描述符中有很多空闲对象,那么系统是否要回收一些空闲的缓存对象从而释放内存归还系统呢?
    • 使用kmem_cache_free释放一个对象,当发现本地和共享对象缓冲池中的空闲对象数目ac->avail大于等于缓冲池的极限值ac->limit时,系统会主动释放batchcount个对象
    • slab系统还注册了一个定时器,定时去扫描所有的slab描述符,回收一部分空闲对象

kmalloc函数

  • kmalloc()函数的核心是slab机制
  • 按照内存块的2的order来创建多个slab描述符
    • void *kmalloc(size_t size, gfp_t flags)
    • void kfree(const void*)

vmalloc

虚拟地址空间

vmalloc区域

vmalloc接口函数

1
2
void *vmalloc(unsigned long size);
void vfree(const void *addr);

vmalloc分配过程中是可以睡眠的,因此不能用在中断上下文中

VMA操作

VMA操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */

unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;

struct rb_node vm_rb;

/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;

/* Second cache line starts here. */

struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */

/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;

/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};

vma管理

1
2
3
4
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr);
struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,
struct vm_area_struct **pprev);

find_vma

1
2
3
4
5
6
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
struct vm_area_struct *vma_merge(struct mm_struct *mm,
struct vm_area_struct *prev, unsigned long addr,
unsigned long end, unsigned long vm_flags,
struct anon_vma *anon_vma, struct file *file,
pgoff_t pgoff, struct mempolicy *policy)

vma_merge

malloc

  • malloc 全称是memory allocation,中午名动态分配,用户空间主要用来分配虚拟内存的API

  • calloc 在动态分配完内存后,自动初始化改内存空间为0,而malloc不初始化,里面的数据是随机的垃圾数据

  • realloc 修改一个原先已经分配的内存块的大小,可以使一块内存的扩大或缩小

  • malloc是c库封装的API,最终调用brk系统调用

  • glibc里维护了一个小仓库,malloc函数的实现为用户进程维护了一个本地小仓库,当该进程需要使用更多的内存时就向这个小仓库要货,小仓库存量不足时就通过该代理商brk向内核批发

  • malloc()想象成零售,那么brk就是代理商

  • brk系统调用

1
SYSCALL_DEFINE1(brk, unsigned long, brk)

maclloc/brk分配虚拟内存的地方

malloc调用

  • 上图在进程的地址空间堆空间中查找一段空闲空间,然后创建一个VMA,然后就返回
  • 按需分配(on-demand page)
    • malloc分配的内存可能长时间不使用,所以进程分配内存马上就分配物理空间其实不是一个好办法
    • 解决办法: 缺页中断,当进程真的需要访问这些虚拟页面的时候,情非得已才去创建物理内存

mlock系统调用

  • 系统调用mlock允许程序在物理内存上锁住它的部分或全部地址空间,调用mlock会马上为虚拟内存分配物理内存
  • 将阻止Linux将这个内存页调度到交换空间(swap space)
  • 在brk函数实现中,最后返回虚拟内存地址的时候,会去检查一个变量VM_LOCKED,这个VM_LOCKED通常从mlock系统调用中设置而来
    • 如果有,那么需要调用mm_populate()马上分配物理内存并建立映射
    • 一般情况是:一直将分配物理页面的工作推延到用户进程需要访问这些虚拟页面时,发生了缺页中断才会分配物理内存,并和虚拟地址建立映射关系

mlock系统调用

get_user_pages()函数

  • 一个很重要分配物理内存的接口函数,有很多驱动程序使用这个API来为用户态程序分配物理内存

get_user_pages函数

follow_page()函数

  • 返回用户进程地址空间VMA中已有映射过的normal mapping页面的struct page数据结构

vm_normal_page()函数

  • 该函数把page页面分为两个阵营,一个是normal page,另一个是special page
    • normal page通常指正常mapping的页面,比如匿名页面,page cache和共享内存页面
    • special page通常指不正常mapping的页面,这些页面不希望参与内存管理的回收或者合并的功能,例如映射如下特性的页面:
      • VM_IO : 为I/O设备映射内存
      • VM_PEN_MAP : 纯PFN映射
      • VM_MIXEDMAP : 固定映射

总结

  • malloc函数其实是为用户空间分配进程地址空间,用内核术语来说就是分配一块VMA,相当于一个空的纸箱子,那么什么时候往箱子里装东西?两种方式:
    • 一种是到了真正使用箱子的时候才往里面装东西
    • 另一种是分配箱子的时候就装了你想要的东西
  • 两个进程的malloc分配的虚拟地址是一样的,会打架吗?
    • 每个用户进程有自己的一份页表,每个进程有一个mm_struct数据结构,包含一个属于进程自己的页表,一个管理VMA的红黑树和链表
    • 即使进程A和进程B使用malloc分配内存返回的相同的虚拟地址,但其实他们是两个不同的VMA,分别被不同的两套页表来管理

malloc

mmap

1
2
3
#include <sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void *addr,size_t length);
  • addr : 用于指定映射到进程地址空间的起始地址,为了应用程序的可移植性,一般设置为NULL,让内核选择一个合适的地址
  • length: 表示映射到进程地址空间的大小
  • prot : 用于设置内存映射区域的读写属性
    • PROT_EXEC:表示映射的页面是可以执行的
    • PROT_READ:表示映射的页面是可以读取的
    • PROT_WRITE:表示映射的页面是可以写入的
    • PROT_NONE: 表示映射的页面是不可访问的
  • flags:用于设置内存映射的属性,例如共享映射,私有映射等
    • MAP_SHARED: 创建一个共享映射的区域,多个进程可以通过共享映射的方式来映射一个文件,这样其他进程也可以看到映射内容的改变,修改后的内容会同步到磁盘文件中
    • MAP_PRIVATE: 创建一个私有的写时复制的映射,多个进程可以通过私有映射的方式来映射一个文件,这样其他进程不会看到映射内容的改变,修改后的内容也不会同步到磁盘文件里
    • MAP_ANONYMOUS: 创建一个匿名映射,即没有关联到文件的映射
    • MAP_FIXED: 使用参数addr创建映射,如果在内核中无法映射指定的地址addr,那么mmap会返回失败,参数addr要求按页对齐。如果addr和length指定的进程地址空间和已有的VMA区域重叠,那么内核会调用do_munmap()函数把这段重叠的区域销毁,然后重新映射新的内容
    • MAP_POPULATE: 对于文件映射来说,会提前预读文件内容到映射区域,该特性只支持私用映射。
  • fd: 表示这是一个文件映射,fd是打开的文件句柄
  • offset: 在文件映射时,表示文件偏移量

文件映射和匿名映射

  • 匿名映射: 没有映射对应的相关文件,这种映射的内存区域的内容会被初始化为0
  • 文件映射:映射和实际文件相关联,通常是把文件的内容映射到地址空间,这样应用程序可以像操作系统进程空间一样读写文件

私有映射和共享映射

  • 私有映射: 创建的映射只能自己看到,其他的进程不能看到映射内容的改变
  • 共享映射:创建的映射其他进程可以看到映射内容的改变,如果以共享的方式打开一个文件的话,修改后的内容会同步到磁盘文件中

私有映射和共享

mmap

缺页中断

  • 分页机制

    • MMU
    • 页表项中的PTE_PRESENT比特位

    PTE页表项

  • ARM32的MMU中有如下两个与存储访问失效相关的寄存器

    • 失效状态寄存器(Data Fault Status Register, FSR)
    • 失效地址寄存器(Data Fault Address Register, FAR)
  • ARMv7汇编处理流程为 vectors_start -> vector_dabt -> datb_user / dabt_svc -> dabt_helpper -> v7_early_abort

  • struct fsr_info数据结构用于描述一条失效状态对应的处理方案

  • fs_info[]数组列出了常见的地址失效处理方案

fs_info数组

find_vma

  • 问题1:什么是匿名页面?

    • 在Linux内核中没有关联到文件映射的页面称为匿名页面(Anonymous Page,简称anon page)。比如malloc分配的页面
  • 问题2:为什么需要匿名页面的缺页中断?

    • 如果没有匿名页面缺页中断,那么你在用户空间使用malloc()这个API来分配虚拟内存,那么在内核中都需要实实在在的为其分配物理内存。主要的问题就是浪费,因为很多时候应用程序分配虚拟内存并不会马上使用,所以没有必要马上就满足应用程序的需求
  • 问题3:匿名页面缺页中断发生的条件是什么?

    • 当pte页表项中PRESENT没有置位
    • pte内容为空
    • 没有指定vma->vm_ops->fault()函数指针

    满足上述三个条件我们就判断现在是匿名页面缺页中断了,处理函数是:do_anonymous_page

do_anonymous_page

  • 问题1:什么是文件映射?
    • 和文件相连的映射,其实就是文件映射,这个是和匿名映射相对应的。把文件的内容映射到进程地址空间里面
  • 问题2:哪些类型的映射会产生文件映射的缺页中断?
    • 对应的文件映射主要是普通文件的映射,比如视频播放器读取一个视频文件
    • 还有一种情况就是,设备驱动通过mmap把设备DMA buffer映射到进程地址空间里
  • 在handle_pte_fault()函数里怎么判断是文件映射
    • 当页面不在内存
    • 页表项内容为空
    • VMA定义了fault方法函数(vma->vm_ops->fault())

写缺页中断

  • 写缺页中断是最复杂的情况,也是最常用的情形
  • 写时复制技术(COW)
    • 在fork子进程的时候,内核不需要复制父进程的整个用户空间内容给予子进程而是让父子进程共享父进程的地址空间,这样就省去了非常耗时的拷贝动作,而仅仅是拷贝页表即可
    • 当父子进程有一方需要写入的时候,数据才会被复制,从而父子进程才会拥有各自的副本。所以,这里的do-wp-page函数就是处理这种共享问题
  • do_wp_page()的判断条件是什么
    • 当PTE_PRESETN位 置位 了然后缺页处理flags标志位是显示写保护错误(这个写保护错误,通常硬件是会报告的)

缺页中断

  • 总的来说,这个过程相当复杂,需要处理很多种情况:
    • normal mapping页面和special mapping页面
    • 纯粹的匿名页面,这个名词是编造的,也就是剔除了KSM的匿名页面
    • 单身匿名页面,这个名词也是编造的,就是只有一个进程的虚拟页面映射了这个匿名页面,即mapcount=0
    • 非单身匿名页面
    • 可写共享匿名页面,这个通常指的是具有可写属性,并且是share的页面,通常是page cache

总结

  • 缺页中断发生后,根据pte页表项的PRESETN位,pte内容是否为空(pte_none()宏)以及是否文件映射等条件,相应的处理函数如下:
    • 匿名页面缺页中断 do_anonymous_page()
      • 判断条件
        • pte页表项中PRESENT没有置位
        • pte内容为空
        • 没有指定vma->vm_ops->fault()函数指针
      • 应用场合: malloc()分配内存
    • 文件映射缺页中断do_fault()
      • 判断条件
        • pte页表中的PRESENT没有置位
        • pte内容为空且制定了vma->vm_ops->fault()函数指针
        • do_fault()属于在文件映射中发生的缺页中断的情况
        • 如果仅发生读错误,那么调用do_read_fault()函数去读取这个页面
        • 如果在私有映射VMA中发生写保护错误,那么发生写时复制,新分配一个页面new_page,旧页面的内容要复制到新页面中,利用新页面生成一个PTE entry并设置到硬件页表项中,这就是所谓的写时复制COW
        • 如果写保护错误发生在共享映射VMA中,那么就产生了脏页,调用系统的回写机制来回写这个脏页
      • 应用场景:
        • 使用mmap读文件内容,例如驱动中使用mmap映射设备内存到用户空间
        • 动态库映射,例如不同的进程可以通过文件映射来共享一个动态库
    • swap缺页中断do_swap_page()
    • 写时复制COW缺页中断do_wp_page()
      • do_wp_page()最终处理有两种情况
        • resue复用old_page: 单身匿名页面和可写的共享页面
        • gotten写时复制:非单身匿名页面,只读或者非共享的文件映射页面
      • 判断条件:pte页表项中的PRESENT置位了且发生写错误缺页中断
      • 应用场景:fork, 父进程fork子进程,父子进程都共享父进程的匿名页面,当其中一方需要修改内容时,COW便会发生

pages数据结构

当开启了MMU之后,CPU访问内存的最小单位为页(Page)

pages数据结构

  • flag标志:PG_*标志位
    • 定义在include/linux/page-flags.h

flag标志

arm vexpress平台

arm vexpress平台 page->flags布局

  • _count字段
    • 当_count的值为0时,表示该page页面为空闲或即将要被释放的页面
    • 当_count的值为0时,表示该page页面已经被分配且内核正在使用,暂时不会被释放
1
2
3
4
static inline void get_page(struct page *page);
void put_page(struct page *page);
#define page_cache_get(page) get_page(page)
#define page_cache_release(page) put_page(page)
  • _mapcount字段

    _mapcount引用技术表示这个页面被进程映射的个数,即已经映射了多少个用户pte页表。在32位Linux内核中,每个用户进程都拥有3GB的虚拟空间和一份独立的页表,所以有可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP反向映射系统就是利用这个特性来实现的。_mapcount引用计数主要用于RMAP反向映射系统中。

    • _mapcount==-1 表示没有pte映射到页面
    • _mapcount==-1 表示只有父进程映射到了页面。匿名页面刚分配时,_mapcount引用计数初始化为0

使用:

  • 内核代码不会直接去检查count和mapcount的个数,而是采用两个宏

  • static inline int page_mapcount(struct page *page)

  • static inline int page_count(struct page *page)

  • mmaping 字段

    • 当这个页面用于文件缓存即page cache时候,mapping指向和这个page cache相关联的address_space对象,这个address_space对象是属于内存对象(比如索引节点的页面集合)
    • 当这个页面用于匿名页面(Anonymous Page)的时候,mapping指向一个anon_vma数据结构,主要是用于反向映射(Reverse Mapping)
  • lru字段

    • 用在页面回收的LRU链表算法中,LRU链表算法定义了多个链表
    • 用来把一个slab添加到slab满链表,slab空闲链表和slab部分链表中
  • virtual字段

    • 一个指向页所对应的虚拟地址的指针
  • struct page的意义

    • 内核知道当前这个页面的状态(通过flags字段)
    • 内核需要知道一个页面是否空闲即有没有分配出去,有多少个进程或者内存路径使用了这个页面(使用count和mapcount引用计数)
    • 内核知道了谁在使用了这个页面,比如使用者是通过用户空间进程的匿名页面还是page cache?(通过mapping字段)
    • 内核知道这个页面是否被slab机制使用(通过lru,s_mem等字段)
    • 内核知道这个页面是否线性映射(通过virtual字段)

物理内存与mem_map数组对应关系

  • 页面锁PG_Locked
    • struct page数据结构成员flags定义了一个标志位PG_locked,内核通常利用PG_locked来设置一个页面锁
      • lock_page()函数用于申请页面锁,如果页面锁被其他进程占用了,那么就会睡眠等待
      • trylock_page()如果返回false表示获取锁失败,返回true表示获取锁成功
      • trylock_page()不会去睡眠等待,只是用来做一下判断

RMAP 反向映射机制

  • 正向映射

    • 从虚拟地址到物理地址,沿着MMU硬件的脚步

    RMAP反向映射

  • 当物理内存短缺时?

    • 虚拟内存常常大于物理内存
    • 把暂时不用的物理内存swap到交换分区
  • 如何判断哪些物理内存暂时不用?

    • LRU算法
    • 第二次机会法
  • Linux 2.4内核的做法

    • 遍历所有进程的VMA来确定某个物理页面映射的pte
  • 反向映射的设计目标:

反向映射的设计目标

反向映射用到的数据结构:

反向映射用到的数据结构

RMAP四部曲(以Linux 2.6.11为例)

  1. 父进程分配匿名页面

    1. do_anonymous_page()->page_add_anon_rmap()
    2. page_add_anon_rmap()
      1. page->mapping指向VMA的anon_vma数据结构
      2. 计算page->index
  2. 父进程创建子进程

    1. do_fork()->copy_mm()->dump_mm(),把父进程所有的VMA复制到子进程对应的VMA中
    2. anon_vma_link(): 把子进程的VMA加入到父进程的vma->anon_vma->head链表中
  3. 子进程发生COW

    1. 当父子进程共享匿名页面,子进程的VMA发生COW

    缺页中断->handle_pte_fault()->do_wp_page()->分配一个新的匿名页面->page_add_anon_rmap()

    新分配的匿名页面page->mapping指向父进程vma->anon_vma

  4. RMAP应用

    1. 页面回收:断开所有映射的用户PTE才能回收页面
    2. 页面迁移:断开所有映射的用户PTE

RMAP应用

缺陷:

缺陷

优化,降低锁的粒度

优化,降低锁的粒度

  • 反向映射总结
    • 提高页面回收效率
    • 消耗一定内存空间,典型的以空间换时间
    • 连接虚拟内存管理和物理内存管理的一个桥梁

页面回收

  • 页面置换(page replacement)

  • 页面回收(page reclaim)

  • 页面置换其他例子

    • 处理器高速缓存cache
    • web服务器
  • 页面回收算法介绍

    • 最优页面置换算法
    • 先进先出页面置换算法
    • 最近未使用页面置换算法
    • LRU算法(最近最少使用页面置换算法Least Recently Used)
      • 利用局部性原理
      • 维护链表
      • 新页面加入到链表头部
      • 每当链表中的页面被访问,该页面加入到链表头部
      • 每当需要页面置换的时候,从链表尾部取页面
    • 第二次机会法
      • 给页面第二次可以被访问的机会
    • 时钟页面置换算法
    • 工作集算法
  • Linux内核中使用的页面回收算法

    • LRU链表
      • 不活跃匿名页面链表 LRU_INACTIVE_ANON
      • 活跃匿名页面链表LRU_ACTIVE_ANON
      • 不活跃文件映射页面链表LRU_INACTIVE_FILE
      • 活跃文件映射页面链表LRU_ACTIVE_FILE
      • 不可回收页面链表LRU_UNEVICTABLE

LRU

  • LRU基于zone实现的。每个zone有一套完整的LRU链表

  • 第二次机会法

    • LRU算法缺点:没有考虑该页面的频繁使用的情况,页面依然会被踢出LRU链表
    • 第二次机会法:
      • 设置了一个访问状态位(硬件控制的比特位)
      • 要检查页面的访问位
        • 如果是0,立刻踢出LRU链表
        • 如果是1,表示这期间又被访问过了,这时清除这个访问状态位,然后把这个页面重新添加到链表头,和新加入的页面一样
    • Linux内核使用如下几个状态位来实现第二次机会法:
      • PTE_YONG 硬件比特位,当一个页面被访问过,硬件会自动设置这个比特位
      • PG_active 软件比特位
      • PG_referenced 软件比特位

Linux页面回收框图

Linux页面回收框图

水位图

匿名页面回收过程:
匿名页面回收过程

page cache的页面回收流程

page cache的页面回收流程

LRU链表的迁移

  • LRU链表可以在活跃链表和不活跃链表中迁移
  • 主要使用的软硬件比特位
    • PTE_YONG: 硬件比特位,当一个页面被访问过,硬件会自动设置这个比特位
    • PG_active: 软件比特位
    • PG_referenced: 软件比特位
  • 主要辅助函数
    • mark_page_accessed(): 主要是用来标记页面已经被访问了,然后设置PG_active和PG_referenced
    • page_referenced():判断page是否被访问引用过,返回的访问引用pte的个数,利用反向映射RMAP系统来统计访问引用pte的个数
    • page_check_references()

kswapd内核线程

  • 负责在内存不足的时候异步回收页面
  • 每个NUMA内存节点创建一个kswapd内核线程
  • alloc_pages()在低水位(ALLOC_WMARK_LOW)中无法分配出内存,这时分配内存函数会调用wakeup_kswapd()来唤醒kswapd内核线程

kswapd内核线程

  • 当zone处于高水位时会让kswapd睡眠

页面回收详细流程图

页面回收详细流程图

匿名页面的生命周期

  • maclloc/mmap接口分配的内存->do_anonymous_page()
  • 写时复制:当缺页中断出现写保护错误时,新分配的页面是匿名页面
  • do_swap_page() 从swap分区读回数据时会新分配匿名页面
  • 迁移页面

分配

  • do_anonymous_page()分配一个匿名页面anon_page为例子,anon_page刚分配的状态如下:
    • page->_count = 1
    • page->_mapcount=0
    • 设置PG_swapbacked标志位
    • 加入LRU_ACTIVE_ANON链表里
    • page->mapping指向VMA中的anon_vma数据结构

使用

  • 匿名页面在缺页中分配完成之后,就建立了进程虚拟地址空间VMA和物理页面的映射关系,用户进程访问虚拟地址即访问到匿名页面的内容

换出

  • 活跃链表->不活跃链表

  • 第一次扫描不活跃链表

    • add_to_swap()函数会为该页分配swap分区空间

    add_to_swap

    • try_to_unmap()

    try_to_unmap

    • pageout()

pageout

  • 第二次扫描不活跃链表

    • 假设第二次扫描不活跃链表时,该页写入swap分区已经完成。Block layer层的回调函数end_swap_bio_write()->end_page_writeback()会完成如下动作:

      • 清除PG_writeback标志位
      • 唤醒等待在该页PG_writeback的线程,见wake_up_page(page,PG_writeback)函数
    • shrink_page_list()->__remove_mapping()函数作用如下

      • page_freeze_refs(page,2)判断当前page->_count是否为2,并且将该计数设置为0
      • 清PG_swapcache标志位
      • 清PG_locked标志位

      __remove_mapping

最后把page加入free_page链表中,释放该页,因此该anon_page页的状态是页面的内容已经写入swap分区,实际物理页面已经释放

匿名页面的换入

  • 匿名页面被换出到swap分区后,如果应用程序需要读写这个页面,缺页中断发生,因为pte中的present比特位显示该页不在内存中,但pte表项不为空,说明该页在swap分区中,因此调用do_swap_page()函数重新读入该页的内容

匿名页面的释放

  • 当用户进程关闭或者退出时,会扫描这个用户进程所有的VMAs,并会青醋这些VMAs,如果符合释放标准,相关页面会被释放

页面的释放

页面迁移

NUMA:Non-Uniform Memory Access 非统一内存访问

  • 每个处理器有自己的本地内存(Node Local Memory)

  • 也可以访问其他处理器的内存(Remote Memory),但速度更慢。

  • 常见于多路服务器、大型多核系统

NUMA System

UMA:Uniform Memory Access 统一内存访问

  • 所有处理器共享一块物理内存

  • 访问速度一致,也就是 访问任意内存的延迟是相同的

  • 常见于小型多核系统对称多处理器系统(SMP)

libnuma

libnuma

迁移一个进程的所有页面从一个内存结点到另外一个内存结点

pid 进程的PID

maxnode: node的最大数量

old_nodes: 指向nodes的一个bit mask,这是进程所在的nodes

new_nodes: 指向要迁移目的地的一个node mask

move_pages

迁移进程的部分页面到新的内存结点

migrae_pages

migrate_pages

页面迁移核心函数

migrate_mode

MIGRATE_ASYNC 异步模式,不会阻塞

MIGRATE_SYNC 同步模式,过程会阻塞

migrate_reason

页面迁移的流程

页面迁移的流程

内存规整

memory compaction

内存碎片换产生的原因

  • Linux内核的物理内存是由伙伴系统来管理的
  • 伙伴系统由11个2的order次方的链表组成,order从0~10
  • 伙伴系统有切蛋糕的习惯
  • 伙伴系统可以有神奇的修复功能

内存碎片

memory compaction

KSM

KSM

如何使用KSM

  • 使能KSM:
    • madvise(addr,length,MADV_MERGEA_BLE)
  • 关闭KSM:
    • madvise(addr,length,MADV_UNMERGEABLE)
  • android的bionic库中的mmap函数默认打开了KSM

KSM

KSM的统计计数

  • run : 这个结点写1,表示启动ksmd内核线程,写0表示要停止ksmd内核线程
  • pages_to_scan:单次扫描的页数,ksmd内核线程每次被唤醒之后,会扫描多少个页面
  • sleep_millisecs: ksmd在下一次扫描前睡眠多长时间,单位毫秒
  • pages_shared: 共享的页面数,如果1000个页面均为同一个内容,然后合并成一个页面,这里pages_shared等于1
  • pages_sharing: 可共享的页面数,如果1000个页面合并成一个页面,那么pages-sharing就是1000
  • pages_unshared:当前没有合并的页面数量,通常是unstable页面的数量
  • full_scans:从头到尾扫描的次数