一、基本概念
1.逻辑地址:包含在机器语言指令中的用来指定一个操作数或者一条指令的地址。每一个逻辑地址由一个段和偏移量组成,偏移量指明了从段开始的地方到实际地址的距离。
2.线性地址(又叫虚拟地址):是一个32位无符号整数,可以用来表示高达4GB的地址,也即是说,高达4294967296个内存单元。线性地址通常用十六进制进行表示,范围为0x00000000到0xffffffff
3.物理地址:用于芯片级内存单元寻址。他们是与微处理器的地址引脚发送到的内存总线的电信号相对应。由32或64为无符号整型表示。
4.内存管理单元(MMU):通过一种称为分段单元的硬件电路将一个逻辑地址转换成线性地址;接着,第二个称为分页单元的硬件电路将线性地址转换成物理地址。
5.段选择符:逻辑地址是由段选择符和一个指定的段内相对地址偏移量。段选择符是一个16位长的字段。偏移量32位。
段选择符字段含义
索引号(index):所对应的段描述符处于GDT或LDT中的索引。
TI:TI=0表示对应段描述符保存在GDT(全局描述符表)中,TI=1表示对应的段描述符保存在LDT(局部描述符表)中。
RPL:当此对应的段选择符装入cs寄存器时,设置CPU当前的特权级的值为RPL,也就是cs寄存器中的RPL就是CPL。
6.段描述符:为了加速逻辑地址与线性地址的转换,x86_64处理器引入了一种附加的非编程的寄存器,供6个可编程的段寄存器使用,此非编程寄存器含8字节的段描述符,由相应的段寄存器中的段选择符指定,每当一个段选择符被装入段寄存器,相应的段描述符由内存装入到相应的非编程寄存器,这样就不用每次访问段都要跑到内存中的段描述符表中获取。
段选择符和段描述符
7.段寄存器:为了方便的找到段选择符,提供了段寄存器,他的唯一的目的是存放段选择符。cs,ss,ds,es,fs,gs程序可以把同一个寄存器用于不同的用途,方法就是先将值保存在内存中,用完后恢复。
cs:代码段寄存器,指向包含程序制定的段。还有一个重要的功能:它包含二位的字段,用以指明cpu的当前权限级别,值为 0表示最高优先级,而值为3代表最低优先级。linux只能用0级和3级,分别表示内核态和用户态。
ss:栈段寄存器,指向包含当前程序栈的段。
ds:数据段寄存器,指向包含任意的数据段。
其他三个用于一般用途。可指向任意的数据段。
8.全局描述符表(GDT)与局部描述符表(LDT)
全局描述符表和局部描述符表保存的都是段描述符,记住要把段描述符和段选择符区别开来,保存在寄存器中的是段选择符,这个段选择符会到描述符表中获取对应的段描述符,然后将段描述符保存到对应寄存器的非编程寄存器中。
系统中每个CPU有属于自己的一个全局描述符表(GDT),其所在内存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小为64K,一共可保存8192个段描述符,不过第一个一般都会置空,也就是能保存8191个段描述符。第一个置空的原因是防止加电后段寄存器未经初始化就进入保护模式而使用GDT。
而对于局部描述符表,CPU设定是每个进程可以创建属于自己的局部描述符表(LDT),当前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不过大多数用户态的liunx程序都不使用局部描述符表,所以linux内核只定义了一个缺省的LDT供大多数进程共享。描述这个局部描述符表的局部描述符表描述符保存在GDT中。
TLS段描述符:中文名字是局部线程存储段,这个会允许线程拥有自己的段,不过一般程序不经常会用到的,系统调用set_thread_area()与get_thread_area()为当前进程创建和撤销一个TLS段。
TSS段描述符:叫做任务状态段,这个描述符非常重要,每个处理器包含一个自己的tss段,这个tss段中的主要数据是一个tss_struct结构体,linux会将所有CPU的tss_struct结构体以init_tss数组的形式保存起来,这个tss_struct结构体中保存的是当前运行进程的内核态堆栈栈顶地址和当前进程的IO许可权限位。当进程切换时就会设置CPU的tss_struct结构体,CPU就可以从tss_struct中获取当前进程的内核栈和IO许可权限。
kernel code,kernel data,user code,user data:分别是内核代码段描述符,内核数据段描述符,用户代码段描述符,用户数据段描述符,不同的进程会使用同一个用户代码段/数据段描述符。
9.段描述符
BASE(32位):段首地址的线性地址。
G:为0代表此段长度以字节为单位,为1代表此段长度以4K为单位。
LIMIT(20位):此最后一个地址的偏移量,也相当于长度,G=0,段大小在1~1MB,G=1,段大小为4KB~4GB。
S:为0表示是系统段,否则为代码段或数据段。
Type:描述段的类型和存取权限。
DPL:描述符特权级,表示访问这个段CPU要求的最小优先级(保存在cs寄存器的CPL特权级),当DPL为0时,只有CPL为0才能访问,DPL为3时,CPL为0为3都可以访问这个段。
P:表示此段是否被交换到磁盘,总是置为1,因为linux不会把一个段都交换到磁盘中。
D或B:如果段的LIMIT是32位长,则置1,如果是16位长,置0。(详见intel手册)
AVL:忽略。
数据段描述符:
表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核数据段属于数据段描述符,并不属于系统段描述符。
代码段描述符:
表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核代码段属于代码段描述符,并不属于系统段描述符。
系统段描述符:
此描述符代表一个系统段,Type的值代表了是哪一种系统段,S标志位为0。其中以下两种都是系统段
局部描述符表描述符(LDTD,系统段描述符的一种):
此种描述符代表一个包含有LDT的段,它只能保存在GDT中,相应的Type为2,S为0。
任务状态段描述符(TSSD,系统段描述符的一种):
这个描述符代表一个任务状态段(TSS),这个段用于保存部分处理器寄存器的内容(内核态栈地址和IO许可权限位),它只保存在GDT中,根据相应的进程是否正在CPU上运行,其Type字段的值分别为11或9.这个描述符S标志为0。
在所有段描述符中可能大家最关心的就是内核代码段描述符和内核数据段描述符以及用户代码段描述符和用户数据段描述符了,这里也具体说说这几个描述符,它们的构成如下:
可以看出来它们的S都是1,都是非系统段,注意并不是内核用的段就是系统段,这里的系统段的区分不是我们用户态和内核态的这种划分。所有的用户进程都是使用同一个用户代码段描述符和用户数据段描述符,它们是__USER_CS和__USER_DS,也就是每个进程处于用户态时,它们的CS寄存器和DS寄存器中的值是相同的。当任何进程或者中断异常进入内核后,都是使用相同的内核代码段描述符和内核数据段描述符,它们是__KERNEL_CS和__KERNEL_DS。这里要明确记得,内核数据段实际上就是内核态堆栈段。
还可以看出这几个段的BASE都是0x00000000,LIMIT都是0xfffff,并且G为1。也就是说,用户代码段,用户数据段,内核代码段,内核数据段这四个段它们的寻址地址都是0x00000000~0xffffffff。也就是地址0到4G的大小。这也形成了为什么所有进程都可以使用同一个用户代码段和用户数据段的条件。并且很清楚地可以看出,内核代码段和内核数据段都需要CPL为0时才能访问,而用户代码段和用户数据段在CPL为0或者3时都可以访问。
再看看这4个段描述符对应的段选择符:
可以看出来,它们的TI为0,表示都保存在全局段描述符表中。可能看到这里大家会有个疑问,既然用户段的RPL为3,那怎么去访问DPL为0的内核段呢,这就是linux精明的地方,它就是禁止用户态访问内核态的数据,但是内核为用户态开了两个小门,然用户态能够通过这两个小门进入到内核态中,这两个小门就是系统调用与中断和异常。
二、内存分段和分页
1. linux内核使用页式内存管理,应用程序给出的内存地址是逻辑地址,它需要经过若干级页表一级一级的变换(分段、分页),才变成真正的物理地址。大概流程如下:
2. 分段
逻辑地址如何转换成物理地址?分段单元执行以下操作:
1) 先检查描述符的TI字段,判断段描述符是在GDT中(分段单元从gdtr寄存器中得到线性基地址)还是在LDT中(从ldtr中得到线性基地址)?
2) 从段选择符的index字段计算段描述符的地址,index乘以8(一个段描述符的大小),这个结果与gdtr或ldtr寄存器中的值相加,得到段描述符的base字段。
3) 把逻辑地址的偏移与段描述符base字段的值相加就得到线性地址。
图示:
线性地址=index*8+gdtr(ldtr)+offset
由于linux使用的分段机制有限,可以认为,linux下的逻辑地址=线性地址。也就是,我们编码使用的是线性地址,之后只需要经过一个分页机制把线性地址转为物理地址了。所以重要的说明一下linux的分页机制。
3. 分页
linux采用四级分页模型,这四种页表是:页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)、页表(PTE)。这里的所有页全局目录、页上级目录、页中间目录、页表,它们的大小都是一个页。linux下各个硬件上并不一定都是使用四级目录的,当使用于没有启动物理地址扩展(PAE)的32位系统上时,只使用二级页表,linux会把页上级目录和页中间目录置空。而在启用了物理地址扩展的32位系统上时,linux使用的是三级页表,页上级目录被置空。而在64位系统上,linux根据硬件的情况会选择三级页表或者四级页表。这个整个由线性地址转换到物理地址的过程,是由CPU自动进行的。
每个进程都有它自己的页全局目录,当进程运行时,系统会将该进程的页全局目录基地址保存到cr3寄存器中;而当进程被换出时,会将这个cr3保存的页全局目录地址保存到进程描述符中。还有一个cr2寄存器,用于缺页异常处理的。当进程运行时,它使用的是它自己的一套页表,当它通过系统调用或陷入内核态时,使用的是内核页表,实际上,对于所有的进程页表来说,它们的线性地址0xC0000000以上所涉及到的页表都是主内核页全局目录(保存在init_mm.pgd),它们的内容等于主内核页全局目录的相应表项,这样就实现了所有进程的进程空间相互隔离,但是内核空间相互共享的情况。当某个进程修改了内核页表的一些映射情况后,系统只会相应的修改主内核页全局目录中的表项(只能修改高端内存中非连续内存区的映射),当其他进程访问这些线性地址时,会出现缺页异常,然后修改该进程的页表项重新映射该地址。
因为说到每个进程都有它自己的页全局目录,如果有100个进程,内存中就要保存100个进程的整个页表集,看起来会耗费相当多的内存。实际上,只有进程使用到的情况下系统才会分配给进程一条路径,比如我们要求访问一个线性地址,但是这个地址可能对应的页上级目录、页中间目录、页表和页都不存在的,这时系统会产生一个缺页异常,在缺页异常处理中再给进程的这个线性地址分配页上级目录、页中间目录、页表和页所需的物理页框。
4.地址空间
一个线性地址经过分页机制转为一个对应的物理地址,我们称之为映射。
在linux系统中分两个地址空间,一个是进程地址空间,一个是内核地址空间。对于每个进程来说,他们都有自己的大小为3G的进程地址空间,这些进程地址空间是相互隔离的,也就是进程A的0x00000001线性地
址址和进程B的0x00000001线性地址并不是同一个地址,进程A也不能通过自己的进程空间直接访问进程B的进程地址空间。而当线性地址大于3G时(也就是0xC0000000),这里的线性地址属于内核空间,内核地址空间的大小为1G,地址从0xC0000000到0xFFFFFFFF。在内核地址空间中,内核会把前896MB的线性地址直接与物理地址的前896MB进行映射,也就是说,内核地址空间的线性地址0xC0000001所对应的物理地址为0x00000001,它们之间相差一个0xC0000000。
linux内核会将物理内存分为3个管理区,分别是:
ZONE_DMA:包含0MB~16MB之间的内存页框,可以由老式基于ISA的设备通过DMA使用,直接映射到内核的地址空间。
ZONE_NORMAL:包含16MB~896MB之间的内存页框,常规页框,直接映射到内核的地址空间。
ZONE_HIGHMEM:包含896MB以上的内存页框,不进行直接映射,可以通过永久映射和临时映射进行这部分内存页框的访问。
内存地址映射关系图
对于ZONE_DMA和ZONE_NORMAL这两个管理区,内核地址都是进行直接映射,只有ZONE_HIGHMEM管理区系统在默认情况下是不进行直接映射的,只有在需要使用的时候进行映射(临时映射或者永久映射)。
linux内核空间的另外128M是干吗用的呢?实际上内核会使用这些线性地址实现非连续性内存分配和固定映射的线性地址;非连续内存分配仅仅是动态的分配和释放内存,固定映射是不同于那896M的直接映射, 他是以任意方式建立的一种映射关系,内核使用固定映射的线性地址来代替指针变量,因为这些指针变量的值从来不会变化。
内核地址空间的映射关系:
1)内核线性地址空间范围:3GB-4GB (0xc0000000-0xffffffff)
2)内核线性地址空间[3GB,3GB+896MB]-----(线性映射)----------物理地址空间[0,896M]
3)内核线性地址空间[3GB+896MB,4GB]用来实现“非连续内存分配”和“固定映射”
固定映射的特点总结:
1)固定映射的线性地址以“任意方式”(与前896MB线性映射方式相比)映射任何物理地址空间
2)固定映射使用的线性地址位于线性地址第4个GB末端
3)每个固定映射的线性地址都由定义在enum fixed_addresses数据结构中的整型索引来表示
几个固定映射相关的函数:
fix_to_virt(idx)函数:计算从给定索引开始的常量线性空间(获取用来进行固定映射的常量线性地址)
set_fixmap(idx,phys)函数:将fix_to_virt(idx)返回的线性地址对应的一个页表项初始化为物理地址phys
set_fixmap(idx,phys)函数:同上,并且将页表项的PCD标志置位,表示当访问这个页框中的数据时禁用硬件高速缓存
clear_fixmap(idx)函数:撤销固定映射线性地址idx与物理地址之间的连接
三、内存管理框架
内存管理各个数据结构之间关系
主要结构体说明
1.pglist_data结构体:numa机器中,每个numa节点都有一个pglist_data结构体来描述他的内存分布;
/* 内存结点描述符,所有的结点描述符保存在 struct pglist_data *node_data[MAX_NUMNODES] 中 */
typedef struct pglist_data {
/* 管理区描述符的数组 */
struct zone node_zones[MAX_NR_ZONES];
/* 页分配器使用的zonelist数据结构的数组,将所有结点的管理区按一定的关联链接成一个链表,分配内存时会按照此链表的顺序进行分配 */
struct zonelist node_zonelists[MAX_ZONELISTS];
/* 结点中管理区的个数 */
int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
/* 结点中页描述符的数组,包含了此结点中所有页框描述符,实际分配是是一个指针数组 */
struct page *node_mem_map;
#ifdef CONFIG_MEMCG
/* 用于资源限制机制 */
struct page_cgroup *node_page_cgroup;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
/* 用在内核初始化阶段 */
struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
/* 自旋锁 */
spinlock_t node_size_lock;
#endif
/* 结点中第一个页框的下标,在numa系统中,页框会有两个序号,所有页框的一个序号,还有就是在此结点中的一个序号
* 比如结点2中的页框1,它在结点2中的序号是1,但是在所有页框中的序号是1001,这个变量就是保存这个结点首页框的序号1000,用于方便转换
*/
unsigned long node_start_pfn;
/* 内存结点的大小,不包括洞(以页框为单位) */
unsigned long node_present_pages;
/* 结点的大小,包括洞(以页框为单位) */
unsigned long node_spanned_pages;
/* 结点标识符 */
int node_id;
/* kswaped页换出守护进程使用的等待队列 */
wait_queue_head_t kswapd_wait;
wait_queue_head_t pfmemalloc_wait;
/* 指针指向kswapd内核线程的进程描述符 */
struct task_struct *kswapd; /* Protected by
mem_hotplug_begin/end() */
/* kswapd将要创建的空闲块大小取对数的值 */
int kswapd_max_order;
enum zone_type classzone_idx;
#ifdef CONFIG_NUMA_BALANCING
/* 以下用于NUMA的负载均衡 */
/* Lock serializing the migrate rate limiting window */
spinlock_t numabalancing_migrate_lock;
/* Rate limiting time interval */
unsigned long numabalancing_migrate_next_window;
/* Number of pages migrated during the rate limiting time interval */
unsigned long numabalancing_migrate_nr_pages;
#endif
} pg_data_t;
系统中所有的结点描述符pg_data_t都保存在node_data[MAX_NUMNODES]这个数组中。在pg_data_t这个结点描述符中,node_zones数组中保存了这个结点中所有的管理区描述符,虽然系统将物理内存分为三个区,但是在逻辑上,系统分为了四个管理区,多出的一个是ZONE_MOVABLE,这个区是一个虚拟的管理区,它并没有对应于内存的某个区域,它的主要目的就是为了避免内存碎片化,它的内存要么全部来自ZONE_HIGHMEM区,要么全部来自ZONE_NORMAL区。这些在初始化函数中将会看到。
每个结点都有一个内核线程kswapd,它的作用就是将进程或内核持有的,但是不常用的页交换到磁盘上,以腾出更多可用内存。
2.zone结构体
/* 内存管理区描述符 */
struct zone {
/* Read-mostly fields */
/* zone watermarks, access with *_wmark_pages(zone) macros */
/* 包括pages_min,pages_low,pages_high
* pages_min: 管理区中保留页的数目
* pages_low: 回收页框使用的下界,同时也被管理区分配器作为阀值使用,一般这个数字是pages_min的5/4
* pages_high: 回收页框使用的上界,同时也被管理区分配器作为阀值使用,一般这个数字是pages_min的3/2
*/
unsigned long watermark[NR_WMARK];
/* 指明在处理内存不足的临界情况下管理区必须保留的页框数目,同时也用于在中断或临界区发出的原子内存分配请求(就是禁止阻塞的内存分配请求) */
long lowmem_reserve[MAX_NR_ZONES];
#ifdef CONFIG_NUMA
int node;
#endif
/*
* The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
* this zone's LRU. Maintained by the pageout code.
*/
unsigned int inactive_ratio;
/* 指向此管理区属于的结点 */
struct pglist_data *zone_pgdat;
/* 实现每CPU页框高速缓存,里面包含每个CPU的单页框的链表 */
struct per_cpu_pageset __percpu *pageset;
/*
* This is a per-zone reserve of pages that should not be
* considered dirtyable memory.
*/
unsigned long dirty_balance_reserve;
#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;//位表,每4个bits用于标识一个pageblock_order 的迁移类型
#endif /* CONFIG_SPARSEMEM */
#ifdef CONFIG_NUMA
/*
* zone reclaim becomes active if more unmapped pages exist.
*/
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
/* 管理区第一个页框下标 */
unsigned long zone_start_pfn;
/* 所有正常情况下可用的页,总页数(不包括洞)减去保留的页数 */
unsigned long managed_pages;
/* 管理区总大小(页为单位),包括洞 */
unsigned long spanned_pages;
/* 管理区总大小(页为单位),不包括洞 */
unsigned long present_pages;
/* 指向管理区的传统名称,"DMA" "NORMAL" "HighMem" */
const char *name;
/* 对应于伙伴系统中MIGRATE_RESEVE链的页块的数量 */
int nr_migrate_reserve_block;
#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
/* 进程等待队列的hash表,这些进程在等待管理区中的某页 */
wait_queue_head_t *wait_table;
/* 等待队列散列表的大小 */
unsigned long wait_table_hash_nr_entries;
/* 等待队列散列表数组大小 */
unsigned long wait_table_bits;
ZONE_PADDING(_pad1_)
/* Write-intensive fields used from the page allocator */
/* 保护该描述符的自旋锁 */
spinlock_t lock;
/* free areas of different sizes */
/* 标识出管理区中的空闲页框块,用于伙伴系统 */
/* MAX_ORDER为11,分别代表包含大小为1,2,4,8,16,32,64,128,256,512,1024个连续页框的链表 */
struct free_area free_area[MAX_ORDER];
/* zone flags, see below */
/* 管理区标识 */
unsigned long flags;
ZONE_PADDING(_pad2_)
/* Fields commonly accessed by the page reclaim scanner */
/* 活动及非活动链表使用的自旋锁 */
spinlock_t lru_lock;
struct lruvec lruvec;
/* Evictions & activations on the inactive file list */
atomic_long_t inactive_age;
/*
* 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 async and sync compaction migration scanner should start */
unsigned long compact_cached_migrate_pfn[2];
#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.
*/
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
ZONE_PADDING(_pad3_)
/* 管理区的一些统计数据 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];//节点中各种类型页统计
} ____cacheline_internodealigned_in_smp;
此管理区描述符中的实际把所有属于该管理区的页框保存在两个地方:struct free_area free_area[MAX_ORDER]和struct per_cpu_pageset __percpu * pageset。free_area是这个管理区的伙伴系统,而pageset是这个区的每CPU页框高速缓存。对管理区的理解需要结合伙伴系统和每CPU页框高速缓存
3.管理区页框分配器(管理所有物理内存页框)
ZONE_NORMAL和ZONE_DMA的地址直接映射到了内核地址空间,但是也不代表内核的代码可以随心所欲的通过线性地址直接访问物理地址。内核通过一个管理区页框分配器管理着物理内存上所有的页框,在管理区分配器里的核心系统就是伙伴系统和每CPU页框高速缓存(不是硬件上的高速缓存,只是名称一样)。在linux系统中,管理区页框分配器管理着所有物理内存,无论你是内核还是进程,需要将一些内存占为己有时,都需要请求管理区页框分配器,这时才会分配给你应该获得的物理内存页框。当你所拥有的页框不再使用时,你必须释放这些页框,让这些页框回到管理区页框分配器当中。特别的,对于高端内存,即使从管理区页框分配器中获得了相应的页框,我们还需要进行映射才能够使用。
有时候目标管理区不一定有足够的页框去满足分配,这时候系统会从另外两个管理区中获取要求的页框,但这是按照一定规则去执行的,如下:
如果要求从DMA区中获取,就只能从ZONE_DMA区中获取。
如果没有规定从哪个区获取,就按照顺序从 ZONE_NORMAL -> ZONE_DMA 获取。
如果规定从HIGHMEM区获取,就按照顺序从 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 获取。
注意系统是不允许在一次分配中从不同的两个管理区获取页框的,并且当请求多个页框时,从伙伴系统中分配给目标的页框是连续的,并且请求的页数必须是2的次方个数。
管理区分配器主要做的事情就是将页框通过伙伴系统或者每CPU页框高速缓存分配出去,这里涉及到三个结构,页描述符,伙伴系统,每CPU高速缓存。
我们先说说页描述符,页描述符实际上并不专属于描述页框,它还用于描述一个SLAB分配器和SLUB分配器,这个之后再说,我们先说关于页的:
4.page描述符
/* 页描述符,描述一个页框,也会用于描述一个SLAB,相当于同时是页描述符,也是SLAB描述符 */
struct page {
/* First double word block */
/* 用于页描述符,一组标志(如PG_locked、PG_error),也对页框所在的管理区和node进行编号 */
unsigned long flags; /* Atomic flags, some possibly
* updated asynchronously */
union {
/* 用于页描述符,当页被插入页高速缓存中时使用,或者当页属于匿名区时使用 */
struct address_space *mapping;
/* 用于SLAB描述符,用于执行第一个对象的地址 */
void *s_mem; /* slab first object */
};
/* Second double word */
struct {
union {
/* 作为不同的含义被几种内核成分使用。例如,它在页磁盘映像或匿名区中标识存放在页框中的数据的位置,或者它存放一个换出页标识符 */
pgoff_t index; /* Our offset within mapping. */
/* 用于SLAB描述符,指向第一个空闲对象地址 */
void *freelist;
/* 当管理区页框分配器压力过大时,设置这个标志就确保这个页框专门用于系统释放其他页框时使用 */
bool pfmemalloc;
};
union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
/* SLUB使用 */
unsigned long counters;
#else
/* SLUB使用 */
unsigned counters;
#endif
struct {
union {
/* 页框中的页表项计数,如果没有为-1,如果为PAGE_BUDDY_MAPCOUNT_VALUE(-128),说明此页及其后的一共2的private次方个数页框处于伙伴系统中,正在使用时应该是0 */
atomic_t _mapcount;
struct { /* SLUB使用 */
unsigned inuse:16;//已经使用的object
unsigned objects:15;//slab中的object数
unsigned frozen:1;//为1表明slab位于s->cpu_slab中;为0表明slab位于s->node中或者没有空闲object的slab
};
int units; /* SLOB */
};
/* 页框的引用计数,如果为-1,则此页框空闲,并可分配给任一进程或内核;如果大于或等于0,则说明页框被分配给了一个或多个进程,或用于存放内核数据。page_count()返回_count加1的值,也就是该页的使用者数目 */
atomic_t _count; /* Usage count, see below. */
};
/* 用于SLAB描述符 */
unsigned int active; /* SLAB */
};
};
/* Third double word block */
union {
/* 包含到页的最近最少使用(LRU)双向链表的指针,用于插入伙伴系统的空闲链表中,只有块中头页框要被插入 */
struct list_head lru;
/* SLAB使用 */
struct { /* slub per cpu partial pages */
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 slab *slab_page; /* slab fields */
struct rcu_head rcu_head;
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS
pgtable_t pmd_huge_pte; /* protected by page->ptl */
#endif
};
/* Remainder is not double word aligned */
union {
/* 可用于正在使用页的内核成分(例如: 在缓冲页的情况下它是一个缓冲器头指针,如果页是空闲的,则该字段由伙伴系统使用,在给伙伴系统使用时,表明的是块的2的次方数,只有块的第一个页框会使用) */
unsigned long private;
#if USE_SPLIT_PTE_PTLOCKS
#if ALLOC_SPLIT_PTLOCKS
spinlock_t *ptl;
#else
spinlock_t ptl;
#endif
#endif
/* SLAB描述符使用,指向SLAB的高速缓存 */
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
struct page *first_page; /* Compound tail pages */
};
#if defined(WANT_PAGE_VIRTUAL)
/* 此页框第一个物理地址对应的线性地址,如果是没有映射的高端内存的页框,则为空 */
void *virtual;
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
unsigned long debug_flags; /* Use atomic bitops on this */
#endif
#ifdef CONFIG_KMEMCHECK
void *shadow;
#endif
#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
int _last_cpupid;
#endif
}
在struct page描述一个页框时,我们比较关注的成员变量有unsigned long flags、struct list_head lru和atomic_t _count。
flags:包含有很多信息,包括此页框属于的node结点号,此页框属于的zone号和此页框的属性。
lru:用于将此页描述符放入相应的链表,比如伙伴系统或者每CPU页框高速缓存。
_count:代表页框的引用计数,-1代表此页框空闲,大于0代表此页框分配给了多少个进程使用(共享)。
linux为了防止内存中产生过多的碎片,一般把页的类型分为三种:
不可移动页:在内存中有固定位置,不能移动到其他地方。内核中使用的页大部分是属于这种类型。
可回收页:不能直接移动,但可以删除,页中的内容可以从某些源中重新生成。例如,页内容是映射到文件数据的页就属于这种类型。对于这种类型,在内存短缺(分配失败)时,会发起内存回收,将这类型页 进行回写释放。
可移动页:可随意移动,用户空间的进程使用的没有映射具体磁盘文件的页就属于这种类型(比如堆、栈、shmem共享内存、匿名mmap共享内存),它们是通过进程页表映射的,把这些页复制到新位置时,只要更新进程页表就可以了。一般这些页是从高端内存管理区获取。
5、伙伴系统
伙伴系统的主要作用就是减少物理内存的外部碎片(SLAB/SLUB减少页框的内部碎片),它实际上是一个struct free_area的数组,数组长度是MAX_ORDER,也就是11,代表着每个数组元素中链表上保存的连续页框长度是2的order次方。free_area[0]中链表保存的是长度为1的页框,free_area[1]中链表上保存的是物理上连续的2个页框的首页框链表,free_area[2]中链表上保存的是物理上连续4个页框的首页框链表,free_area[10]中链表上保存的是物理上连续1024个页框的首页框链表,所以整个伙伴系统中将管理区中的页框分为连续的1,2,4,8,16,32,64,128,256,512,1024页框放入不同链表中保存起来。而因为伙伴系统中每个链表保存的页框都是连续的,所以只有第一个页框会加入到链表中,因为有order,也可以知道此页框之后的多少个页框是属于这一小块连续页框的。当需要在普通内存区申请4个页框大小的内存时,系统会到普通内存管理区的伙伴系统中的free_area[2]中的第一个链表结点,这个结点的页框及其之后3个页框都是空闲的,然后把首页框返回给申请者。
/* 伙伴系统的一个块,描述1,2,4,8,16,32,64,128,256,512或1024个连续页框的块 */
struct free_area {
/* 指向这个块中所有空闲小块的第一个页描述符,这些小块会按照MIGRATE_TYPES类型存放在不同指针里 */
struct list_head free_list[MIGRATE_TYPES];
/* 空闲小块的个数 */
unsigned long nr_free;
};
//在伙伴系统中,因为页的分类关系,在每种长度相同的连续页框中又会分出多个不同类型的链表,如下,
enum {
MIGRATE_UNMOVABLE, /* 不可移动页 */
MIGRATE_RECLAIMABLE, /* 可回收页 */
MIGRATE_MOVABLE, /* 可移动页 */
MIGRATE_PCPTYPES, /* 用来表示每CPU页框高速缓存的数据结构中的链表的可移动类型数目 */
MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
MIGRATE_ISOLATE, /* 不能从这个链表分配页框,因为这个链表专门用于NUMA结点移动物理内存页,将物理内存页移动到使用这个页最频繁的CPU */
#endif
MIGRATE_TYPES
};
在从伙伴系统中申请页框时,有可能会遇到一种情况,就是当前需求的连续页框链表上没有可用的空闲页框,这时后,伙伴系统会从下一级获取一个连续长度的页框块,将其拆分放入这级列表;当然在拥有者释放连续页框时伙伴系统也会适当地进行连续页框的合并,并放入下一级中。比如:我需要申请4个页框,但是长度为4个连续页框块链表没有空闲的页框块,伙伴系统会从连续8个页框块的链表获取一个,并将其拆分为两个连续4个页框块,放入连续4个页框块的链表中。释放时道理也一样,会检查释放的这几个页框的之前和之后的物理页框是否空闲,并且能否组成下一级长度的块。
5.每CPU页框高速缓存
每CPU页框高速缓存也是一个分配器,配合着伙伴系统进行使用,这个分配器是专门用于分配单个页框的,它维护一个单页框的双向链表,为什么需要这个分配器,因为每个CPU都有自己的硬件高速缓存,当对一个页进行读取写入时,首先会把这个页装入硬件高速缓存,而如果进程对这个处于硬件高速缓存的页进行操作后立即释放掉,这个页有可能还保存在硬件高速缓存中,这样我另一个进程需要请求一个页并立即写入数据的话,分配器将这个处于硬件高速缓存中的页分配给它,系统效率会大大增加。
在每CPU页框高速缓存中用一个链表来维护一个单页框的双向链表,每个CPU都有自己的链表(因为每个CPU有自己的硬件高速缓存),那些比较可能处于硬件高速缓存中的页被称为“热页”,比较不可能处于硬件高速缓存中的页称为“冷页”。其实系统判断是否为热页还是冷页很简单,越最近释放的页就比较可能是热页,所以在双向链表中,从链表头插入可能是热页的单页框,在链表尾插入可能是冷页的单页框。分配时热页就从链表头获取,冷页就从链表尾获取。
在每CPU页框高速缓存中也可能会遇到没有空闲的页框(被分配完了),这时候每CPU页框高速缓存会从伙伴系统中拿出页框放入每CPU页框高速缓存中,相反,如果每CPU页框高速缓存中页框过多,也会将一些页框放回伙伴系统。
在内核中使用struct per_cpu_pageset结构描述一个每CPU页框高速缓存,其中的struct per_cpu_pages是核心结构体,如下:
* 描述一个CPU页框高速缓存 */
struct per_cpu_pageset {
/* 高速缓存页框结构 */
struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
s8 expire;
#endif
#ifdef CONFIG_SMP
s8 stat_threshold;
s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};
struct per_cpu_pages {
/* 当前CPU高速缓存中页框个数 */
int count; /* number of pages in the list */
/* 上界,当此CPU高速缓存中页框个数大于high,则会将batch个页框放回伙伴系统 */
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];
};
参考:《深入理解liuux内核》(第三版)