linux内核笔记(五)通用内存的表示
通用内存的表示
基础概念
内存管理涉及的领域
内存中的物理内存页的管理;
分配大块内存的伙伴系统;
分配较小块内存的slab、slub和slob分配器;
分配非连续内存块的vmalloc 机制;
进程的地址空间。
内存地址空间、32位系统的寻址限制
Linux内核将地址空间划分为两个部分。底部比较大的部分用于用户进程,顶部则专用于内核。虽然(在两个用户进程之间的)上下文切换期间会改变下半部分,但虚拟地址空间的内核部分总是保持不变。在IA-32系统上,地址空间在用户进程和内核之间划分的典型比例为3∶1。
如果物理内存比可以映射到内核地址空间中的数量要多,那么内核必须借助于高端内存(highmem)方法来管理超出内存。在IA-32系统上,可以直接管理的物理内存数量不超过896 MiB。超过该值(直到最大4 GiB为止)的内存只能通过高端内存寻址。在64位计算机上,由于可用的地址空间非常巨大 $2^{64}= 16EiB$,因此不需要高端内存模式。
UMA 和 NUMA
在管理物理内存的方式上,有两种类型的计算机:
- UMA计算机(一致内存访问 ,uniform memory access)将可用内存以连续方式组织起来(可能有小的缺口)。SMP系统中的每个处理器访问各个内存区都是同样快。
- NUMA计算机(非一致内存访问 ,non-uniform memory access)多处理器计算机。系统的各个CPU都有本地内存,可支持特别快速的访问。各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问(比访问本地内存慢些)。
物理模型对应的内存结构
内核对UMA和非NUMA使用相同的数据结构,因此针对各种不同形式的内存布局,各个算法几乎没有什么差别。在UMA系统上,只使用一个NUMA结点来管理整个系统内存。而内存管理的其他部分则相信它们是在处理一个伪NUMA系统。
结点
内存划分为结点的虚拟结构,每个结点关联到系统中的一个处理器。
结点的数据结构
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; //结点中各内存域
struct zonelist node_zonelists[MAX_ZONELISTS]; //备用结点及其内存域的列表(在当前结点没有可用空间时,在备用结点分配内存)
int nr_zones; // 结点中可用内存域的数量
struct page *node_mem_map; // 指向page实例数组的指针,用于描述结点的所有物理内存页
unsigned long node_start_pfn; // 该结点第一个页帧的逻辑编号。系统中所有 结点的页帧是依次编号的,每个页帧的号码都是全局唯一的
unsigned long node_present_pages; // 结点中可用页帧的数量
unsigned long node_spanned_pages; // 结点中物理内存的总量(包含物理内存洞)
int node_id; // 全局结点ID。系统中的NUMA结点都从0开始编号。
wait_queue_head_t kswapd_wait; // 是交换守护进程(swap daemon)的等待队列,在将页帧换出结点时会用到
struct task_struct *kswapd; //指向负责该结点的交换守护进程的task_struct
int kswapd_max_order; //用于页交换子系统的实现,用来定义需要释放的区域的长度
} pg_data_t;
- 物理内存洞:物理内存中,某些页帧被保留,不能被分配。这些页帧称为洞。
- 硬件保留区域:某些硬件设备(如显卡、BIOS、PCI设备等)需要使用特定的物理地址空间。这些设备保留了部分物理地址,导致这部分地址不能用于实际的物理内存。
- 内存映射 I/O(MMIO): 内存映射 I/O 是指将设备的寄存器映射到系统的地址空间,以便 CPU 可以通过内存地址访问设备。这些映射区域会占用部分物理地址空间,导致实际的内存不能使用这些地址。
- 系统保留区域:系统引导过程中,BIOS 或 EFI 可能会保留部分内存用于特定用途,例如存储系统表或内存映射表。这些保留区域也会形成内存洞。
结点的状态管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum node_states {
N_POSSIBLE, // 结点在某个时候可能变为联机
N_ONLINE, // 结点是联机的
N_CPU, //结点有一个或多个CPU
/* 以上三个状态适用于cpu与内存的热插拨*/
N_NORMAL_MEMORY, // 结点有普通内存域
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, //结点有普通或高端内存域
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
NR_NODE_STATES
};
//设置和清除node的状态函数
static inline void node_set_state(int node, enum node_states state)
static inline void node_clear_state(int node, enum node_states state)
内存域
内存域可以是普通内存、高端内存、DMA 内存等,每个内存域都属于一个特定的结点。
内存域的数据结构
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
struct zone {
unsigned long watermark[NR_WMARK]; //内存域水印,通过*_wmark_pages(zone) 宏访问
unsigned long percpu_drift_mark; //当节点可用页面低于此点时,在读取可用页面数时会采取其他步骤,以避免每个cpu计数器偏移导致水印被破坏
unsigned long lowmem_reserve[MAX_NR_ZONES]; // 储备的低端内存域,用于一些无论如何都不能失败的关键性内存分配
struct per_cpu_pageset __percpu *pageset; // 每个cpu的内存页帧集合 被__percpu修饰,每个cpu会获得此变量的副本
spinlock_t lock;
int all_unreclaimable; // 不可回收的页数
//#ifdef CONFIG_MEMORY_HOTPLUG //如果启用了内存热插拔功能
seqlock_t span_seqlock; //见zone_start_pfn下面的注释
//#endif
struct free_area free_area[MAX_ORDER]; //用于实现伙伴系统。每个数组元素都表示某种固定长度的一些连续内存区。对于包含在每个区域中的空闲内存页的管理,free_area 是一个起点
ZONE_PADDING(_pad1_)
/*
由ZONE_PADDING 将该结构分隔为几个部分。这是因为内核对此结构的访问非常频繁。在多处理器系统上,通常会有不同的CPU试图同时访问结构成员。因此使用锁防止它们彼此干扰,避免错误和不一致。
如果数据保存在CPU高速缓存中,那么会处理得更快速。高速缓存分为行,每一行负责不同的内存区。内核使用ZONE_PADDING宏生成填充字段添加到此结构中,以确保每个自旋锁都处于自身的缓存行中。还使用了编译器关键字__cacheline_maxaligned_in_smp ,用以实现最优的高速缓存对齐方式。
*/
/* 页面回收扫描程序通常访问的字段 */
spinlock_t lru_lock;
struct zone_lru {
struct list_head list;
} lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;//内存域的扫描统计信息 扫描的统计次数越多 缓存越有价值
unsigned long pages_scanned; //上一次回收以来扫描过的页
unsigned long flags; //内存域回收标志 见下面枚举zone_flags_t
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; //维护了大量有关该内存域的统计信息
unsigned int inactive_ratio; //此内存域的lru 活动与不活动页面的比例
ZONE_PADDING(_pad2_)
/* 很少使用 或 大部分只用于读取的字段*/
/* 以下三个字段维护了一个等待内存页的进程队列,当内存页可用,系统将唤醒等待队列的进程使其运行 */
wait_queue_head_t * wait_table; // 指向hash_table的数组
unsigned long wait_table_hash_nr_entries; //wait_table的数组大小
unsigned long wait_table_bits; // (1 << wait_table_bits) == wait_table_size
/* 支持非连续内存的字段. */
struct pglist_data *zone_pgdat; //指向该内存域所属的pg_data_t内存结点结构
unsigned long zone_start_pfn; //指向内存域第一个页帧的索引 zone_start_pfn == zone_start_paddr >> PAGE_SHIFT
/*
zone_start_pfn, spanned_pages and present_pages 被 span_seqlock 保护(这是个顺序锁),由于填充的原因,它没有被zone->lock所保护。span_seqlock与zone->lock 一起声明是因为它们经常被同时访问,放在同一个缓存行中提高命中率。
*/
unsigned long spanned_pages; //内存域所包含的页帧总数(包含空洞)
unsigned long present_pages; //内存数量(除去空洞)
const char *name; //内存域的名字 基本不使用
} ____cacheline_internodealigned_in_smp;
typedef enum {
ZONE_RECLAIM_LOCKED, // 表示当前内存区域正在进行回收操作,防止并发回收
ZONE_OOM_LOCKED, // 表示当前内存区域正处于“内存不足”的处理状态。内核在检测到严重的内存不足情况时,会触发OOM 杀手(OOM killer)来释放内存,防止在 OOM 处理期间进行新的内存分配或回收操作
ZONE_CONGESTED, //表示当前内存区域处于拥塞状态。拥塞状态通常意味着该区域内的页帧回收或I/O操作速度较慢,导致内存分配的效率降低。
} zone_flags_t;
//设置和清除zone的状态函数
static inline void zone_set_flag(struct zone *zone, zone_flags_t flag)
static inline int zone_test_and_set_flag(struct zone *zone, zone_flags_t flag)
static inline void zone_clear_flag(struct zone *zone, zone_flags_t flag)
内存域中的冷热页
zone->pageset
是CPU冷热页的实现,热页
代表页已经加载到CPU高速缓存,与在内存中的页相比,其数据能够更快地访问。冷页
则不在高速缓存中。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。因此pageset
字段使用了__percpu
修饰,每个CPU会获得此变量的副本。
尽管zone归属于某个pg_data_t从而归属于某个特定的CPU,但其他CPU的高速缓存仍然可以包含该内存域中的页。每个处理器都可以访问系统中所有的页,尽管速度不同。因此,特定于内存域的数据结构不仅要考虑到所属结点相关的CPU,还必须照顾到系统中其他的CPU。
1
2
3
4
5
6
7
8
9
struct per_cpu_pageset {
struct per_cpu_pages pcp;
};
struct per_cpu_pages {
int count; // 该链表中物理页的个数
int high; // 链表中的物理页个数超过该数值,会将部分页返还给zone buddy系统
int batch; // 每次返还给buddy系统的物理页的个数 */
struct list_head lists[MIGRATE_PCPTYPES]; // 对于不同的迁移类型的页帧有不同的链表(对内存碎片的优化),其中热页在链表头部,冷页存放在链表尾部。
};
页帧
页帧是内存的物理页,是代表系统内存的最小单位,每个物理页都有一个struct page结构体与之对应, 其中记录了该物理页的许多状态:该页中是否保存着数据、该页是否可回收、该页在页表中是否被映射等等。 由于页的数目巨大,因此对page结构的小改动,也可能导致保存所有page实例所需的物理内存暴涨。所有的page都存放在一个全局数组中(mem_map)
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
struct page {
unsigned long flags; // 页帧的原子标志,有些情况下会异步更新 flags可被设置成page-flags.h文件中的pageflags枚举
atomic_t _count; // 页帧的引用计数,表示该页帧被多少个进程映射到内存中
union {
atomic_t _mapcount; // 页帧的映射计数,表示该页帧被多少个进程映射到内存中
struct { // 用于slub分配器 对象的分配数
u16 inuse;
u16 objects;
};
};
union {
struct {
unsigned long private; // 页帧的私有数据,用于slub分配器
struct address_space *mapping; // 页帧的映射对象 (根据位值确定是否匿名页)
};
struct kmem_cache *slab; // 用于slub分配器,指向slab
struct page *first_page; // 用于复合页的尾页,指向首页
};
union {
pgoff_t index; // 在映射的虚拟空间(vma_area)内的偏移;
void *freelist; // sl[aou]b 第一个空闲页
};
struct list_head lru; // 用于在各种链表上维护该页,以便将页按不同类别分组,比如活动和不活动页,受zone->lru_lock保护。
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; // 用于高端内存区域中的页 存储该页的虚拟地址
#endif
小结
每个CPU关联到内核中的结点,结点下管理了不同状态的内存域,内存域中按照冷热页和迁移类型的分类,管理不同状态的页帧。
页表
页表用于建立用户进程的虚拟地址空间和系统物理内存(内存、页帧)之间的关联。上文主要描述了内存的结构层次(结点、内存域、page页帧),其中包含的页帧的数量和状态(使用中或空闲)。页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区。该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现(几个进程同时共享的内存),还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。
内核内存管理总是使用四级页表,而不管底层处理器是否如此。IA-32系统是不支持四级页表的,该体构只使用两级分页系统(不使用PAE扩展)。因此,第三和第四级页表必须由特定于体系结构的代码模拟。
内存地址与页表的关系
根据四级页表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置)
将虚拟地址与各项的位掩码按位与,即可提取出各项的分量
页表在内核中的表示
由于不同体系架构的差异,内核提供一个抽象的页表结构与相应的辅助函数,每种体系结构需要提供对应的实现。
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
/** 各页表层级的结构 **/
typedef struct {
unsigned long pte;
} pte_t;
typedef struct {
unsigned long pmd[16];
} pmd_t;
typedef struct {
unsigned long pgd;
} pgd_t;
typedef struct {
unsigned long pgprot;
} pgprot_t;
typedef struct page *pgtable_t;
/** 地址与结构互相转换的宏 **/
#define pte_val(x) ((x).pte)
#define pmd_val(x) ((&x)->pmd[0])
#define pgd_val(x) ((x).pgd)
#define pgprot_val(x) ((x).pgprot)
#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x) ((pgprot_t) { (x) } )
pte_t页表中的指针不仅指向了内存位置,在不同的体系结构下还包含了相应的附加信息(在多余的比特位中),下列表格提供比较常见的内存页的函数与。
函数 | 描述 |
pte_present | 页是否在内存中(是否被换出) |
pte_read | 从用户空间可以读取该页吗 |
pte_write | 可以写入到该页吗 |
…… | …… |
mk_pte | 创建页表项 |
pte_page | 获得页表项描述的页对应的page实例地址 |
pgd/…/pte_alloc | 分配并初始化一个对应项的内存 |
pgd/…/pte_free | 释放对应项占据的内存 |
set_pgd/…/pte | 设置对应项中的值 |