3.1 概述
- 虚拟地址空间:每个进程各自有自己的逻辑地址空间。在IA-32系统上,进程寻址范围是
0 ~ 4GB。地址空间被典型划分为3GB的用户空间和1GB的内核空间
- 管理物理内存方式
UMA(一致内存访问,uniform memory access):将可用内存以连续方式组织起来。SMP系统每个处理器访问个内存区都一样快(结构对称)NUMA(非一致内存访问, non-uniform memory access):仅用于多处理器计算机,各个CPU有各自的本地内存。CPU可以访问其它CPU的内存,但是比访问本地内存慢(结构非对称)
【注】书中重点考虑UMA系统的平坦内存模型FLATMEM,不考虑CONFIG_NUMA宏
# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 28 29 30 31 32 33 34 35 36 37 38 39 40 41
node 0 size: 79946 MB
node 0 free: 14940 MB
node 1 cpus: 14 15 16 17 18 19 20 21 22 23 24 25 26 27 42 43 44 45 46 47 48 49 50 51 52 53 54 55
node 1 size: 80597 MB
node 1 free: 55633 MB
node distances:
node 0 1
0: 10 21
1: 21 10
3.2 (N)UMA模型中的内存组织
3.2.1 术语定义
- 结点:内核将物理内存划分为结点,每个结点关联到系统的一个处理器。内核中有
pg_data_t的实例。- NUMA系统:各个内存结点保存在一个单链表中,供内核遍历。内核按照结点距离远近,优先遍历当前运行CPU相关联的结点
- 每个结点都提供了一个备用列表
struct zonelist,列表项越靠后越不适合分配
- 每个结点都提供了一个备用列表
- UMA系统:NUMA系统的特例,只有一个结点(图3-3的内存结点减少为一个),其余不变
- NUMA系统:各个内存结点保存在一个单链表中,供内核遍历。内核按照结点距离远近,优先遍历当前运行CPU相关联的结点
- 内存域:IA-32系统上,虚拟地址空间的3G ~ 4G属于内核空间,其中3G ~ 3G+896M直接映射物理地址的0~896M
- Linux把每个内存结点的物理内存划分为3个管理区
zone_type:- ZONE_DMA:0~16M,专门供I/O设备的DMA使用
- ZONE_NORMAL:16~896M,内核可以直接使用该区域的物理页面
- ZONE_HIGHMEM:896~1024M,属于高端内存,由于线性地址空间不足物理页面需要动态映射,所以不能由内核直接访问(64位系统地址空间充足,取消此区域)
- 各个内存域关联一个数组,组织物理内存页(即页帧)。对每个页帧,内核分配一个
struct page实例及所需的管理数据
- Linux把每个内存结点的物理内存划分为3个管理区
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA, //DMA设备使用,32位计算机该区域为16MB
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32, //32位计算机该区域为空,64位系统上两种DMA才有区别
#endif
ZONE_NORMAL, //普通内存域,所有体系上都存在,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM, //高端内存
#endif
ZONE_MOVABLE, //伪内存域,防止物理内存碎片的机制用到,见3.5.2节
MAX_NR_ZONES
};
3.2.2 数据结构
1. 结点管理:pg_data_t定义了内存结点的基本元素,包括:内存域、页帧、结点编号、交换守护进程等
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; //结点中各内存域的数据结构
struct zonelist node_zonelists[MAX_ZONELISTS]; //备用列表,当前结点没有可用空间时,在备用结点分配内存
int nr_zones; //内存域数目
#ifdef CONFIG_FLAT_NODE_MEM_MAP
struct page *node_mem_map; //指向结点的所有物理内存页(页帧)
#endif
struct bootmem_data *bdata; //用于内存管理子系统初始化的自举,自举内存分配器数据结构的实例
......
unsigned long node_start_pfn; //NUMA结点第一个页帧的逻辑编号,编号全局唯一。UMA中总是0
unsigned long node_present_pages; //结点中页帧总数
unsigned long node_spanned_pages; //结点含多少个页帧(中间可能有空洞,空洞不对应真正的页帧)
int node_id; //全局结点ID,从0开始编号
wait_queue_head_t kswapd_wait; //交换守护进程的等待队列
struct task_struct *kswapd; //交换守护进程的task_struct
int kswapd_max_order; //用于页交换子系统的实现,定义需要释放区域的长度
} pg_data_t;
- 结点状态管理:内核维护位图,记录各个结点的状态信息:结点是否有普通内存域、是否有高端内存域等等
- 在
/sys/devices/system/node/可以看到节点的详细信息 - 使用
node_set_state()和node_clear_state()设置或清除比特位
- 在
enum node_states {
N_POSSIBLE, /* 结点在某个时刻可能变为online */
N_ONLINE, /* 结点是online的 */
N_NORMAL_MEMORY, /* 节点有普通内存域,没有高端内存域 */
#ifdef CONFIG_HIGHMEM
N_HIGH_MEMORY, /* 节点有普通或高端内存域 */
#else
N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
N_CPU, /* 节点有一个或多个CPU */
NR_NODE_STATES
};
2. 内存域:内核使用struct zone描述内存域
- 由
ZONE_PADDING分隔为几个部分:页分配器访问的字段、页面收回扫描程序访问的字段、很少使用或大多数情况只读的字段
【注】这个结构体访问非常频繁。为了提高执行的性能,用两个自旋锁zone->lock和zone->lru_lock对齐到不同cache line中
struct zone {
//影响页交换的行为:内存不足则换到硬盘上
//空闲页多于pages_high,内存域状况理想
//空闲页低于pages_low,页换出到硬盘;
//空闲页低于pages_min,页回收压力大,即内存域急需空闲页。
unsigned long pages_min, pages_low, pages_high;
//为各个内存域预留若干页,用于无论如何不能失败的关键性内存分配
unsigned long lowmem_reserve[MAX_NR_ZONES];
//用于实现每个CPU的热页列表和冷页列表
struct per_cpu_pageset pageset[NR_CPUS];
spinlock_t lock; //自旋锁
struct free_area free_area[MAX_ORDER]; //用于实现伙伴系统,3.5.5节
ZONE_PADDING(_pad1_)
spinlock_t lru_lock;
struct list_head active_list; //活动页的集合
struct list_head inactive_list; //不活动页的集合
unsigned long nr_scan_active; //指定在回收内存时,需要扫描的活动页的数目
unsigned long nr_scan_inactive; //指定在回收内存时,需要扫描的不活动页的数目
unsigned long pages_scanned; //上次换出一页以来,有多少页未能成功扫描
//内存域当前状态,允许使用以下标志
//ZONE_ALL_UNRECLAIMABLE:内存试图页面回收,但是所有的页都已经“钉”住而无法回收(如mlock系统调用)
//ZONE_RECLAIM_LOCKED:防止多个CPU并发回收
//ZONE_OOM_LOCKED:进程OOM消耗大量内存,内核杀死该进程,此时要防止多个CPU同时回收内存
unsigned long flags;
//维护有关该内存域的统计信息,见17.7.1节。用zone_page_state读取信息
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
//存储了上一次扫描操作扫描该内存域的优先级
int prev_priority;
ZONE_PADDING(_pad2_)
//以下3个字段实现了一个等待队列,等待某一页变为可用的进程使用
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
//建立内存域和父结点的关联
struct pglist_data *zone_pgdat;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn; //内存域第一个页帧的索引
unsigned long spanned_pages; //较少使用,内存域中页的总数,包括空洞
unsigned long present_pages; //较少使用,实际可用的页的数目,不包括空洞,通常与spanned_pages相同
const char *name; //较少使用,保存内存域的惯用名称:Normal、DMA、HighMem
} ____cacheline_internodealigned_in_smp;
3. 内存阈值(watermark,也称水位)的初始化
- 在内核启动或内存热插拔时,会重新计算内存阈值
- 内存阈值:衡量内存的使用情况,有一个专门的内核线程
kswapd0定期回收内存。系统内存越大,则阈值越大- 空闲页多于
pages_high,内存域状况理想 - 空闲页低于
pages_low,页需要换出到硬盘 - 空闲页低于
pages_min,页回收压力大,即内存域急需空闲页
- 空闲页多于
zone->pages_min、zone->pages_low、zone->pages_high由setup_per_zone_pages_min()初始化,pages_min可以通过/proc/sys/vm/min_free_kbytes内核选项设置(>=128KB,<=64MB)。- 初始的典型值计算方式:
- ,其中
lowmem_kbytes可以认为是系统内存大小
- ,其中
zone->lowmem_reserve: kernel在分配内存时,可能会涉及到多个zone,优先尝试从自己的zone分配,如果失败就会尝试低地址的zone。每个zone需要自己预留内存- lowmem_reserve由
setup_per_zone_lowmem_reserve()初始化,具体算法是:- 比率由内核参数
/proc/sys/vm/lowmem_reserve_ratio指定,默认:DMA是256,NORMAL是256,HIGHMEM是32 (在Linux 2.6.24上面是256 32 32) - ratio=256表示有1/256的页帧被保护;如果ratio=1表示有100%的页帧被保护
- lowmem_reserve由
# 4.14内核版本的预留内存分布
$ cat /proc/sys/vm/lowmem_reserve_ratio
256 256 32
- 我们可以通过
/proc/zoneinfo查看具体的内存域信息
# 1GB内存32位虚拟机的内存域分布(2.6.24内核版本)
# cat /proc/zoneinfo
Node 0, zone DMA
pages free 2380
min 17
low 21
high 25
scanned 0 (a: 0 i: 0)
spanned 4096
present 4064
protection: (0, 873, 1000, 1000)
......
Node 0, zone Normal
pages free 220882
min 936
low 1170
high 1404
scanned 0 (a: 0 i: 0)
spanned 225280
present 223520
protection: (0, 0, 1015, 1015)
......
Node 0, zone HighMem
pages free 32520
min 32
low 66
high 100
scanned 0 (a: 0 i: 0)
spanned 32766
present 32511
protection: (0, 0, 0, 0)
......
# 系统总预留的物理内存(KB) ~= (dma.min + dma32.min + normal.min) * 4
# cat /proc/sys/vm/min_free_kbytes
3816
【说明】
- 物理内存分三个zone:DMA、NORMAL、HIGHMEM
- zone[DMA]管理4096个页帧,也就是16MB内存
- zone[NORMAL]管理225280个页帧,也就是约880MB内存
- zone[HIGHMEM]管理32766个页帧,也就是约128MB内存
- 总计1024MB内存
- spanned:表示内存域的总页帧数,包括空洞
- present:表示内存域的可用页帧数,不包括空洞
- managed:内存域中被伙伴系统管理的页帧数
- protection:每个区域保留的不能被用户空间分配的页面数目
- 预留内存计算 (在内核2.6.24源码是使用present,而内核4.14使用managed)
- protection[dma, normal] = (normal.present) / 256 = 873
- protection[dma, high] = (normal.present + high.present) / 256 = 1000
- 也就是说,如果normal内存不够,想在dma中尝试申请page时,此时dma.free > dma.high + protection[dma,normal],所以可以分配;否则不能分配(代码参考:3.5.5节
zone_watermark_ok())
【注】内核参数具体介绍可以看这里:www.kernel.org/doc/Documen…
4. 冷热页
- 热页:页加入到高速缓存;冷页:不在高速缓存中 (在2.6.25版本将冷热页合并为一个)
- 冷热页分配器(hot-n-cold allocator):根据阈值,控制页什么时候被加载进高速缓存中
struct zone
//用于实现每个CPU的热页列表和冷页列表
struct per_cpu_pageset pageset[NR_CPUS];
};
struct per_cpu_pageset {
struct per_cpu_pages pcp[2]; /* 0: hot. 1: cold */
......
} ____cacheline_aligned_in_smp;
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 */
struct list_head list; /* the list of pages */
};
5. 页帧
- 页帧:内存分配的最小单位,IA-32系统标准页长度为4KB。每个页会创建一个
struct page实例- 注意实例中结构体尽可能小,利用
union
- 注意实例中结构体尽可能小,利用
struct page {
unsigned long flags; /* 用于描述页的属性,原子标志,有些情况下会异步更新 */
atomic_t _count; /* 引用计数,内核中引用该页的次数。其值为0可以删除 */
union {
atomic_t _mapcount; /* 页表中有多少项指向该页
* 用于表示页是否已经映射,还用于限制逆向映射搜索。
*/
unsigned int inuse; /* 用于SLUB分配器:对象的数目 */
};
union {
struct {
unsigned long private; /* 由映射私有,不透明数据:
* 如果设置了PagePrivate,通常用于buffer_heads;
* 如果设置了PageSwapCache,则用于swp_entry_t;
* 如果设置了PG_buddy,则用于表示伙伴系统中的阶。
*/
struct address_space *mapping; /* mapping指定了页帧所在的地址空间。index是页帧映射内部的偏移量
* 如果最低位为0,则指向inode
* address_space,或为NULL。
* 如果页映射为匿名内存,最低位置位,
* 而且该指针指向anon_vma对象:
* 参见下文的PAGE_MAPPING_ANON。
*/
};
...
struct kmem_cache *slab; /* 用于SLUB分配器:指向slab的指针 */
struct page *first_page; /* 用于复合页的尾页,指向首页 */
};
union {
pgoff_t index; /* 在映射内的偏移量 */
void *freelist; /* SLUB: freelist req. slab lock */
};
struct list_head lru; /* 换出页列表,例如由zone->lru_lock保护的active_list!*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* 内核虚拟地址(如果没有映射则为NULL,即高端内存) */
#endif /* WANT_PAGE_VIRTUAL */
};
- 重要的页标志
page->flags。由一些原子操作设置相应比特位PG_locked:页是否锁定PG_error:该页的IO操作期间发生错误PG_referenced和PG_active:控制页的活跃次数,用于页交换PG_uptodate:页的数据已经从块设备读取,期间没有出错PG_dirty:页内容发生改变PG_lru:用于实现页面回收和切换PG_highmem:页在高端内存中,无法持久映射到内核内存中PG_private:page的private成员非空时设置PG_writeback:页内容正在向块设备回写PG_slab:页是slab分配器的一部分PG_swapcache:页处于交换缓存。此时private包含一个类型为swap_entry_t的项PG_reclaim:内核决定回收某个特定的页后,设置PG_reclaim标志通知PG_buddy:页空闲且包含在伙伴系统的列表中PG_compound:页属于一个更大的复合页,复合页由多个毗连的普通页组成
3.3 页表
3.3.1 页表回顾(第一章)
- 页:虚拟地址空间和物理内存被内核划分为很多等长部分。页一般专指虚拟地址空间的页
- 页帧:物理内存页常称作页帧
- 页表:虚拟地址空间映射到物理地址空间的数据结构。为节省内存通常分解为多级页表
3.3.2 数据结构和操作
1. 内存地址的分解
- 虚拟内存地址分为5个部分:PGD(全局页目录)、PUD(上层页目录)、PMD(中间页目录)、PTE(页表)、Offset(偏移) 。PGD寻址512个PUD,以此类推。
- X86 32位(二级页表):PGD=31~22、PTE=21~12、Offset=11~0
- X86 32位PAE模式(三级页表):PGD=31~30、PMD=29~21、PTE=20~12、Offset=11~0
- X86 64位(四级页表):PGD=47~39、PUD=38~30、PMD=29~21、PTE=20~12、Offset=11~0
2. 页表的格式
- 以AMD64体系结构为例,根据intel手册,页表项的结构如下:
- 因此,在Linux内核中,页表项定义为一个64 bit的struct
- 注:定义为struct目的是只允许相关辅助函数处理
typedef struct { unsigned long pte; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pud; } pud_t;
typedef struct { unsigned long pgd; } pgd_t;
- 页表项的位信息示例:
#define _PAGE_PRESENT 0x001 //页是否存在于内存中,即是否被交换到磁盘上了
#define _PAGE_RW 0x002 //允许读写或禁止读写
#define _PAGE_USER 0x004 //允许用户空间代码访问该页
#define _PAGE_PWT 0x008 //Page-level write-through
#define _PAGE_PCD 0x010 //Page-level cache disable
#define _PAGE_ACCESSED 0x020 //CPU每次访问页时,会设置该位
#define _PAGE_DIRTY 0x040 //页的内容是否被修改过
#define _PAGE_FILE 0x040 //页表项属于非线性映射
#define _PAGE_PSE 0x080 /* 2MB page */
#define _PAGE_PROTNONE 0x080 /* If not present */
#define _PAGE_GLOBAL 0x100 /* Global TLB entry */
- 以
pte_t为例,内核定义了一系列辅助函数操作页表项,用于比特位的置位:
//地址对齐到页边界(地址舍入到下一页的起始处,一定是页大小的倍数)
#define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1) & PAGE_MASK)
//读取页表项
#define pte_val(x) ((x).pte)
//设置页表项
#define __pte(x) ((pte_t) { (x) } )
//从内存指针和页表项获得下一级页表的地址
#define pte_index(address) (((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
//删除页表项,通常置0
#define pte_clear(mm,addr,xp) do { set_pte_at(mm, addr, xp, __pte(0)); } while (0)
//检查的页是否在内存中
#define pte_present(x) (pte_val(x) & (_PAGE_PRESENT | _PAGE_PROTNONE))
static inline void set_pte(pte_t *dst, pte_t val) {
pte_val(*dst) = pte_val(val);
}
//检查的页是否设置了DIRTY、ACCESSED、RW位
static inline int pte_dirty(pte_t pte) { return pte_val(pte) & _PAGE_DIRTY; }
static inline int pte_young(pte_t pte) { return pte_val(pte) & _PAGE_ACCESSED; }
static inline int pte_write(pte_t pte) { return pte_val(pte) & _PAGE_RW; }
//为页设置DIRTY、ACCESSED、RW位
static inline pte_t pte_mkdirty(pte_t pte) { set_pte(&pte, __pte(pte_val(pte) | _PAGE_DIRTY)); return pte; }
static inline pte_t pte_mkyoung(pte_t pte) { set_pte(&pte, __pte(pte_val(pte) | _PAGE_ACCESSED)); return pte; }
static inline pte_t pte_mkwrite(pte_t pte) { set_pte(&pte, __pte(pte_val(pte) | _PAGE_RW)); return pte; }
- 物理地址和虚拟地址的转换
- 内核源码中定义了一系列的转换函数
- 命名规范:page(页)、pfn(页帧)、virt(虚拟地址)、phys(物理地址)
- 转换规则
//内核空间(非高端内存域)的虚拟地址到物理地址的线性映射。例如0xC0100000映射到0x00100000物理地址
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
//物理地址和虚拟地址互转
#define __virt_to_phys(x) (((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET))
#define __phys_to_virt(x) ((unsignedlong)((x) - PHYS_OFFSET + PAGE_OFFSET))
//页帧到物理页的映射。mem_map是page结构体数组基址,加上物理页帧距离起始页帧的偏移即当前对应的虚拟页地址
#define pfn_to_page(pfn) (mem_map + ((pfn) - PHYS_PFN_OFFSET))
#define page_to_pfn(page) ((unsigned long)((page) - mem_map) + PHYS_PFN_OFFSET)
//内核虚拟地址和页帧互转
#define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT)
#define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT)
//内核虚拟地址和页互转
#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
//页帧、物理地址互转
#define PFN_ALIGN(x) (((unsigned long)(x) + (PAGE_SIZE - 1)) & PAGE_MASK)
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
#define PFN_PHYS(x) ((x) << PAGE_SHIFT)