深入理解Linux 内核 内存管理(上)

目录

页框管理

页描述符

UMA和NUMA

内存管理区

保留的页框池

分区页框分配器

请求和释放页框

释放页框

高端内存页框的内核映射

伙伴系统算法

连续页框块释放:

1.数据结构

2.分配块

3.释放块


RAM中,剩下的自由部分被称为动态内存!因为这不仅是内进程所需要的宝贵资源,也是内核执行本身所需要的宝贵资源!实际上整个系统的性能都高度取决于如何有效的管理动态内存!因此内存管理是操作系统中非常重要的话题之一!

页框管理

IntelPentinum处理器可采用两种不同的页框大小:4KB,4MB(如PAE被激活,则为2MB)。Linux采用4KB页框大小作为标准的内存分配单元。

  1. 由分页单元引发的缺页异常很容易得到解释,或由于请求的页存在但不允许进程对其访问,或由于请求的页不存在。第二种情况下,内存分配器必须找到一个4KB的空闲页框,并将其分配给进程。

  2. 虽然4KB,4MB都是磁盘块大小的倍数,但绝大多数情况下,当主存和磁盘之间传输小块数据时更高效。

页描述符

内核必须记录每个页框当前的状态。内核必须能区分哪些页框包含的是属于进程的页,哪些页框包含的是内核代码或内核数据。类似地,内核还必须能确定动态内存中的页框是否空闲。页框的状态信息保存在一个类型为page的页描述符中,其中的字段如表所示:

类型名字说明
unsigned longflags一组标志。对页框所在的管理区进行编号。
atomic_t_count页框的引用计数器
atomic_t_mapcount页框中的页表项数量(没有则为-1)
unsigned longprivate可用于正使用页的内核成分
struct address_space*mapping当页被插入页高速缓存时使用。或当页属于匿名区时使用。
unsigned longindex作为不同的含义被几种内核成分使用。
struct list_headlru包含页的最近最少使用双向链表的指针

所有的页描述符存放在mem_map数组中。mem_map所需要的空间略小于整个RAM1%

virt_to_page(addr)宏产生线性地址addr对应的页描述符地址。 pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。

上述转换可行是因为内核知道页描述符数组起始线性地址,通过线性地址得到物理地址,通过物理地址得到页框在数组索引。进而定位到页描述符地址

让我们较详细地描述以下两个字段:

_count页的引用计数器。如字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身。如该字段值大于或等于0,则说明页框被分配给了一个或多个进程,或用于存放一些内核数据结构。page_count返回__count1后的值,即该页的使用者的数目。 flags:包含多达32个用来描述页框状态的标志。对每个PG_xyz标志,内核都定义了操纵其值的一些宏。通常,PageXyz宏返回标志的值,SetPageXyzClearPageXyz宏分别设置和清除相应的位。

下表正是常见的页框状态的标签:可以一览:

标志名含义
PG_locked页被锁住。如,在磁盘I/O操作中涉及的页
PG_error在传输页时发生I/O错误
PG_referenced刚刚访问过的页
PG_uptodate在完成读操作后置位,除非发生磁盘I/O错误
PG_dirty页已经被修改
PG_lru页在活动或非活动页链表中
PG_active页在活动页链表中
PG_highmem页框属于ZONE_HIGHMEM管理区
PG_checked由一些文件系统使用的标志
PG_arch_1在80x86体系结构上没有使用
PG_reserved页框留给内核代码或没有使用
PG_private页描述符的private字段存放了有意义的数据
PG_writeback正使用writeback方法将页写到磁盘上
PG_nosave系统挂起、唤醒时使用
PG_compound通过扩展分页机制处理页框
PG_swapcache页属于对换高速缓存
PG_mappedtodisk页框中的所有数据对应于磁盘上分配的块
PG_reclaim为回收内存对页已经做了写入磁盘的标记
PG_nosave_free系统挂起、恢复时使用

UMA和NUMA

习惯上,认为计算机内存是一种均匀,共享的资源。在忽略硬件高速缓存作用的情况下,期望不管内存单元处于何处,CPU处于何处,CPU对内存单元的访问都需相同的时间(UMA)。可惜,这些假设在某些体系结构上并不总是成立。如,对某些多处理器AlphaMIPS计算机,就不成立。Linux2.6支持非一致内存访问模型,在这种模型中,给定CPU对不同内存单元的访问时间可能不一样。系统的物理内存被划分为几个节点(node)。节点既封装了内存资源,也封装了CPU资源。在一个单独的节点内,任一给定CPU访问页面所需的时间都是相同的。然而, 对不同CPU,这个时间可能就不同。这就是NUMA的特性!

对每个CPU而言,内核都试图把耗时节点的访问次数减到最少,这就要小心地选择CPU最常引用的内核数据结构的存放位置。每个节点中的物理内存又可分为几个管理区。每个节点都有一个类型为pg_data_t的描述符。

类型名字说明
struct zone[]node_zones节点中管理区描述符的数组
struct zonelist[]node_zonelists页分配器使用的zonelist数据结构的数组
intnr_zones节点中管理区的个数
struct page*node_mem_map节点中页描述符的数组
struct bootmem_data*bdata用在内核初始化阶段
unsigned longnode_start_pfn节点中第一个页框的下标
unsigned longnode_present_pages内存节点的大小,不包含洞(以页框为单位)
unsigned longnode_spanned_pages节点的大小,包括洞(以页框为单位)
intnode_id节点标识符
pg_data_t*pgdat_next节点内存链表中的下一项
wait_queue_head_tkswapd_waitkswapd页换出守护进程使用的等待队列
struct task_struct*kswapd指针指向kswapd内核线程的进程描述符
intkswapd_max_orderkswapd将要创建的空闲块大小取对数的值

同样,我们只关注80x86IBM兼容PC使用一致内存访问模型,因此,并不真正需要NUMA的支持。然而,即使NUMA的支持没有编译进内核,Linux还是使用节点。不过,这是一个单独的节点,它包含了系统中所有的物理内存。此时,pgdat_list指向一个链表,此链表是由一个元素组成的。这个元素就是节点0描述符,它被存放在config_page_data。在80x86结构中,把物理内存分组在一个单独的节点中可能显得没用处;但这种方式有助于内核代码的处理具有可移植性。

内存管理区

在一个理想的计算机体系结构中,一个页框就是一个内存存储单元,可用于任何事情:存放内核数据和用户数据,缓冲磁盘数据等等。任何种类的数据页都可存放在任何页框中。但实际的计算机体系结构有硬件的制约,这限制了页框可使用的方式。尤其是,Linux内核必须处理80x86体系结构的两种硬件约束:

  1. ISA总线的直接内存存取(DMA)处理器有一个严格的限制:它们只能对RAM的前16MB寻址、

  2. 在具有大容量RAM的现代32位计算机中,CPU不能直接访问所有的物理内存,因为线性地址空间太小。

为应对这两种限制,Linux2.6把每个内存节点的物理内存划分为三个管理区。在80x86UMA体系结构中的管理区为:

ZONE_DMA:包含低于16MB的内存页框 ZONE_NORMAL:包含高于16MB且低于896MB的内存页框 ZONE_HIGHMEM:包含从896MB开始高于896MB的内存页框

ZONE_DMA区包含的页框可由老式基于ISA的设备通过DMA使用。ZONE_DMAZONE_NORMAL区包含内存的"常规"页框,通过把它们直接映射到线性地址空间的第4个GB,内核就可直接进行访问。相反,ZONE_HIGHMEM区包含的内存页不能由内核直接访问,尽管它们页线性地映射到了线性地址空间的第4个GB。在64位体系结构上,ZONE_HIGHMEM区总是空的。

每个内存管理区都有自己的描述符。

类型名称说明
unsigned longfree_pages管理区中空闲页的数目
unsigned longpages_min管理区中保留页的数目
unsigned longpages_low回收页框使用的下界;同时也被管理区分配器作为阀值使用
unsigned longpages_high回收页框使用的上届;同时也被管理区分配器作为阀值使用
unsigned long[]lowmem_reserve指明在处理内存不足的临界情况下每个管理区必须保留的页框数目
struct per_cpu_pageset[]pageset用于实现单一页框的特殊高速缓存
spinlock_tlock保护该描述符的自旋锁
struct free_area[]free_area标识出管理区的空闲页框块
spinlock_tlru_lock活动以及非活动链表使用的自旋锁
struct list_headactive_list管理区中的活动页链表
struct list_headinactive_list管理区中的非活动页链表
unsigned longnr_scan_active回收内存时需扫描的活动页数目
unsigned longnr_scan_inactive回收内存时需扫描的非活动页数目
unsigned longnr_active管理区的活动链表上的页数目
unsigned longnr_inactive管理区的非活动链表上的页数目
unsigned longpages_scanned管理区内回收页框时使用的计数器
intall_unreclaimable在管理区中填满不可回收页时此标志被置位
inttemp_priority临时管理区的优先级
intprev_priority管理区优先级,范围在12和0之间
wait_queue_head_t*wait_table进等待队列的散列表,这些进程正在等待管理区中的某页
unsigned longwait_table_size等待队列散列表的大小
unsigned longwait_table_bits等待队列散列表数组大小,值为2^order
struct pglist_data*zone_pgdat内存节点
struct page*zone_mem_map指向管理区的第一个页描述符的指针
unsigned longzone_start_pfn管理区第一个页框的下标
unsigned longspanned_pages以页为单位的管理区的总大小,包括洞
unsigned longpresent_pages以页为单位的管理区的总大小,不包括洞
char*name指针指向管理区的传统名称:“DMA”,“NORMAL”,“HighMem”

每个页描述符都有到内存节点和节点内管理区的链接。为节省空间,这些链接被编码成索引存放在flags字段的高位。实际上,刻画页框的标志的数目是有限的。保留flags字段的最高位来编码特定内存节点和管理区号总是可能的。page_zone接收一个页描述符的地址作为它的参数,它读取页描述符中flags字段的最高位,然后通过查看zone_table数组来确定相应管理区描述符的地址。在启动时用所有内存节点的所有管理区描述符的地址初始化这个数组。

当内核调一个内存分配函数时,必须指明请求页框所在的管理区。内核通常指明它愿意使用哪个管理区。为了在内存分配请求中指定首选管理区,内核使用zonelist数据结构,这就是管理区描述符指针数组。

保留的页框池

可用两种不同的方法来满足内存分配请求。如有足够的空闲内存可用,请求就会被立刻满足。否则,必须回收一些内存,且将发出请求的内核控制路径阻塞,直到内存被释放。(NUMA下默认策略是本地节点内内存不足,从本地节点回收内存后再次尝试分配。若选择本地节点内存不足时,优先查看其他节点是否存在足量内存时,若存在从其他节点完成剩余部分分配的方案,在应用需要大内存场景下可能更高效)

当请求内存时,一些内核控制路径不能被阻塞。比如,这种情况发生在处理中断或执行临界区内的代码时。此时,一条内核控制路径应产生原子内存分配请求。原子请求从不被阻塞:如没有足够的空闲页,则仅仅是分配失败而已。尽管无法保证一个原子内存分配请求决不失败,但内核会设法尽量减少这种不幸事件发生的可能性。为做到这一点,内核为原子内存分配请求保留了一个页框池,只有在内存不足时才使用。

保留内存的数量(以KB为单位)存放在min_free_kbytes中。它的初始值在内核初始化时设置,并取决于直接映射到内核线性地址空间的第4个GB的物理内存的数量。即,取决于包含在ZONE_DMAZONE_NORMAL内存管理区内的页框数目。这是其公式:

$$
保留池的大小=\sqrt{16*直接映射内存}(KB)
$$

min_free_kbytes的初始值不能小于128也不能大于65536

ZONE_DMAZONE_NORMAL内存管理区将一定数量的页框贡献给保留内存,这个数目与两个管理区的相对大小成比例。 例,如ZONE_NORMAL管理区比ZONE_DMA8倍,则页框的7/8ZONE_NORMAL获得。1/8ZONE_DMA获得。

管理区描述符的pages_min存储了管理区内保留页框的数目。这个字段和pages_low,pages_high一起还在页框回收算法中起作用。pages_low总是设为pages_min的值的5/4pages_high总是被设为pages_min3/2

分区页框分配器

分区页框分配器被称作分区页框分配器的内核子系统,处理对连续页框组的内存分配请求。它的主要组成如下:

管理区分配器下分给ZONE_DMA内存管理区,ZONE_NORMAL内存管理区,ZONE_HIGHMEM内存管理区。为了更加很合适的分配内存,每一个个管理区又有伙伴系统和Per-CPU页框高速缓存

其中,名为"管理区分配器"部分接受动态内存分配和释放的请求。在请求分配的情况下,该部分搜索一个能满足所请求的一组连续页框内存的管理区。在每个管理区内,页框被名为"伙伴系统"的部分来处理。为达到更好的系统性能,一小部分页框保留在高速缓存中用于快速地满足对单个页框的分配请求。

请求和释放页框

API说明
alloc_pages(gfp_mask, order)用这个函数请求2^order个连续的页框。它返回第一个所分配页框描述符的地址,或,如分配失败,则返回NULL
alloc_page(gfp_mask)用于获得一个单独页框的宏;扩展为:alloc_pages(gfp_mask, 0)
__get_free_pages(gfp_mask, order)类似alloc_pages,返回第一个所分配页的线性地址。
__get_free_page(gfp_mask)用于获得一个单独页框的宏;扩展为:__get_free_pages(gfp_mask, 0)
get_zeroed_page(gfp_mask)获取填满0的页框;它调用:alloc_pages(gfp_mask |__GFP_ZERO, 0);
__get_dma_pages(gfp_mask, order)获得适用于DMA的页框,它扩展为:__get_free_pages(gfp_mask |__GFP_DMA, order);

参数gfp_mask是一组标志,指明了如何寻找空闲的页框。能在gfp_mask中使用的标志如下:

标志说明
__GFP_DMA所请求的页框必须处于ZONE_DMA管理区。等价于GFP_DMA
__GFP_HIGHMEM所请求的页框处于ZONE_HIGHMEM管理区。
__GFP_WAIT允许内核对等待空闲页框的当前进程进行阻塞
__GFP_HIGH允许内核访问保留的页框池
__GFP_IO允许内核在低端内存页上执行I/O传输以释放页框
__GFP_FS如清0,则不允许内核执行依赖文件系统的操作
__GFP_COLD所请求的页框可能为"冷的"
__GFP_NOWARN一次内存分配失败将不会产生警告信息
__GFP_REPEAT内核重试内存分配直到成功
__GFP_NOFAIL与__GFP_REPEAT相同
__GFP_NORETRY一次内存分配失败后不再重试
__GFP_NO_GROWslab分配器不允许增大slab高速缓存
__GFP_COMP属于扩展页的页框
__GFP_ZERO任何返回的页框必须被填满0

实际上,Linux使用预定义标志值的组合。

组名相应标志
GFP_ATOMIC__GFP_HIGH
GFP_NOIO__GFP_WAIT
GFP_NOFS__GFP_WAIT
GFP_KERNEL__GFP_WAIT
GFP_USER__GFP_WAIT
GFP_HIGHUSER__GFP_WAIT

__GFP_DMA和__GFP_HIGHMEM被称作管理区修饰符;它们标示寻找空闲页框时内核所搜索的管理区。contig_page_data节点描述符的node_zonelists是一个管理区描述符链表的数组,它代表后备管理区:对管理区修饰符的每一个设置,相应的链表包含的内存管理区能在原来的管理区缺少页框的情况下被用于满足内存分配请求。在80x86 UMA体系结构中,后备管理区如下:

  1. __GFP_DMA被置位,则只能从ZONE_DMA内存管理区获取页框

  2. __GFP_HIGHMEM没被置位,按优先次序从ZONE_NORMALZONE_DMA内存管理区获取页框

  3. __GFP_HIGHMEM被置位,按优先次序从ZONE_HIGHMEMZONE_NORMALZONE_DMA内存管理区获取页框

释放页框

下面4个函数和宏中的任一个都可释放页框

API说明
__free_pages(page, order)先检查page指向的页描述符;如该页框未被保留,就把描述符的count字段减1。如count变为0,就假定从与page对应的页框开始的2^order个连续页框不再被使用。此时,函数释放页框。
free_pages(addr, order)类似于__free_pages(page, order),它接收的参数为要释放的第一个页框的线性地址addr
__free_page(page)释放page所指描述符对应的页框;扩展为:__free_pages(page, 0)
free_page(addr)释放线性地址为addr的页框;扩展为:free_pages(addr, 0)

高端内存页框的内核映射

与直接映射的物理内存末端,高端内存的始端所对应的线性地址存放在high_memory变量。被设置为896MB896MB边界以上的页框并不会采用直接映射方式对应到内核线性地址空间的第4个GB中相应位置,因此,内核不能直接访问它们。意味着,返回所分配页框线性地址的页分配器函数不适用于高端内存。即不适用于ZONE_HIGHMEM内存管理区内的页框。如,假定内核调__get_free_pages(GFP_HIGHMEM, 0)在高端内存分配一个页框,如分配器在高端内存确实分配了一个页框,则__get_free_pages不能返回它的线性地址。依次类推,内核不能使用这个页框;甚至更坏情况下,也不能释放该页框。

在64位硬件平台上不存在这个问题,因为可使用的线性地址空间远大于能按照的RAM大小。简言之,这些体系结构的ZONE_HIGHMEM管理区总是空的。但在32位平台上,如80x86体系结构,Linux设计者不得不找到某种方法来允许内核使用所有可使用的RAM,达到PAE所支持的64GB。

采用的方法如下:

  1. 高端内存页框的分配只能通过alloc_pages和它的快捷函数alloc_page。这些函数不返回第一个被分配页框的线性地址,因为如该页框属于高端内存,则这样的线性地址根本不存在。这些函数返回第一个被分配页框的页描述符的线性地址。这些线性地址总是存在的,因为所有页描述符被分配在低端内存,它们在内核初始化阶段完成后就不会改变。

  2. 没有线性地址的高端内存中的页框不能被内核访问。故,内核线性地址空间的最后128MB中的一部分专门用于映射高端内存页框。这种映射是暂时的。通过重复使用线性地址,使得整个高端内存能在不同的时间被访问。

内核可采用三种不同的机制将页框映射到高端内存(线性地址):分别叫永久内核映射,临时内核映射,非连续内存分配。 建立永久内核映射可能阻塞当前进程;这发生在空闲页表项不存在时,即在高端内存上没有页表项可用作页框的"窗口"时。永久内核映射不能用于中断处理程序和可延迟函数。 建立临时内核映射不会要求阻塞当前进程;它的缺点是只有很少的临时内核映射可同时建立起来。使用临时内核映射的内核控制路径必须保证当前没其他的内核控制路径在使用同样的映射。意味着内核控制路径永不能被阻塞,否则其他内核控制路径很可能使用同一个窗口来映射其他的高端内存页。永久内核映射在64位体系下高端内存区不存在,自然也无永久内核映射

永久内核映射允许内核建立高端页框到内核地址空间(线性地址)的长期映射。它们使用主内核页表中一个专门的页表。地址存放在pkmap_page_table。页表中的表项数由LAST_PKMAP宏产生。页表照样含512或1024项,这取决于PAE是否被激活;因此,内核一次最多访问2MB或4MB的高端内存。该页表映射的线性地址从PKMAP_BASE开始。pkmap_count数组包含LAST_PKMAP个计数器,pkmap_page_table页表中的每一项都有一个。

  • 计数器为0:对应的页表项没映射任何高端内存页框,且是可用的

  • 计数器为1:对应的页表项没映射任何高端内存页框,但它不能使用,因为自从它最后一次使用以来,其相应的TLB表项还未被刷新。

  • 计数器为n:相应的页表项映射一个高端内存页框,意味着正好有n-1个内核成分在使用这个页框。

为记录高端内存页框与永久内核映射包含的线性地址之间的关系,内核使用了page_address_htable散列表。该表包含一个page_address_map数据结构,用于为高端内存中的每一页框进行当前映射,该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。

  1. page_address–传入页框描述符线性地址,返回对应页框的线性地址 page_address返回页框(物理地址)对应的线性地址,如页框在高端内存(线性地址)中且没被映射,则返回NULL。这个函数接受一个页描述符指针page(描述一个页框)作为参数,区分以下两种情况: (1).如页框不在高端内存(PG_highmem为0),则采用直接映射。则线性地址总是存在且是通过计算页框下标,然后将其转换成物理地址,最后根据相应的物理地址得到线性地址。 (2).如页框在高端内存(PG_highmem为1),该函数就到page_address_htable散列表中查找。如在散列表中找到页框,page_address就返回它的线性地址,否则返回NULL

  1. kmap–建立永久内核映射。

void* kmap(struct page* page)
{if(!PageHighMem(page))return page_address(page);return kmap_high(page); // 如页框确实属于高端内存,则调kmap_high
}
void *kamp_high(struct page* page)
{unsigned long vaddr;spin_lock(&kmap_lock);// 永久内核映射对所有处理器可见。防止多核并发,需加锁保护。vaddr = (unsigned long)page_address(page);// 查找哈希表if(!vaddr)vaddr = map_new_virtual(page);// 向哈希表插入,并返回线性地址pkmap_count[(vaddr-PKMAP_BASE) >> PAGE_SHIFT]++;// 通过线性地址找到索引spin_unlock(&kmap_lock);return (void*)vaddr;
}

中断处理程序和可延迟函数不能调kmapkmap_high通过调page_address检查页框是否已经被映射。如不是,调map_new_virtual把页框的物理地址插入到pkmap_page_table的一个项,并在page_address_htable中加入一个元素。然后,kmap_high使页框的线性地址所对应的计数器加1来将调用该函数的新内核成分考虑在内。最后,kmap_high释放kmap_lock并返回对该页框进行映射的线性地址。

  1. map_new_virtual–完成页表注册,完成哈希表注册 本质上执行两个嵌套循环:

for(;;)
{int count;DECLARE_WAITQUEUE(wait, current);for(count = LAST_PKMAP; count > 0; --count)// 遍历所有表项{last_pkmap_nr = (last_pkmap_nr+1)&(LAST_PKMAP-1);// 后半部分搜索没找到可用表项时。先刷新,再从开始位置再搜索一遍if(!last_pkmap_nr){flush_all_zero_pkmaps();// 将使用者不存在的槽位清理腾出多余位置count = LAST_PKMAP;}// 找到可用槽位if(!pkmap_count[last_pkmap_nr]){unsigned long vaddr = PKMAP_BASE+(last_pkmap_nr<<PAGE_SHIFT);// 计算此位置对应线性地址set_pte(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page/*页框物理地址*/, __pgprot(0x63)));// 设置页表。完成页表注册。pkmap_count[last_pkmap_nr] = 1;// 表示页表映射建立了。但此页表项映射的页框并没有使用者。set_page_address(page, (void*)vaddr);// 哈希表注册return vaddr;// 返回线性地址}}// 执行到这里,表示后半部分没搜索到可用表项,且刷新从头搜依然没搜到current->state = TASK_UNINTERRUPTIBLE;add_wait_queue(&pkmap_map_wait, &wait);// 向完成队列加入新的等待项spin_unlock(&kmap_lock);// 放弃cpu之前先释放锁。schedule();// 主动放弃cpu,让内核选择另一进程运行。// 走到这里,一定是其他进程腾出表项后,发现有人在等待空闲表项。所以,让等待者变为就绪,将进程重新加入cpu的可调度队列。// 某次调度,等待者被调度恢复后继续执行这里remove_wait_queue(&pkmap_map_wait, &wait);// 将自己从等待队列移除spin_lock(&kmap_lock);// 重新加锁if(page_address(page)) // 再次尝试页表注册,哈希表注册前,先检查,是否其他内核线程已经完成了注册工作。return (unsigned long)page_address(page);// 其他内核线程已经完成注册后,可以直接返回。
}   

内循环中,函数扫描pkmap_count中所有计数器直到找到一个空值。当在pkmap_count中找到一个未使用项时,大的if代码块运行。这段代码确定该项对应的线性地址,为它在pkmap_page_table页表中创建一个项,将count置1,调set_page_address插入一个新元素到page_address_htable散列表,返回线性地址。

搜索从上次因调map_new_virtual而跳出的地方开始。在pkmap_count中搜索完最后一个计数器尚未找到空闲槽位时,又从下标为0计数器开始搜索。继续之前,map_new_virtualflush_all_zero_pkmaps开始寻址计数器为1的另一趟扫描。每个值为1的计数器都表示在pkmap_page_table中表项是空闲的,但不能使用,因为相应的TLB表项还没被刷新。flush_all_zero_pkmaps把它们的计数器重置为0,删除page_address_htable散列表中对应的元素,并对pkmap_page_table里的所有项上进行TLB刷新。

如内循环在pkmap_count中没找到空的计数器,map_new_virtual就阻塞当前进程,直到某个进程释放了pkmap_page_table页表中的一个表项,通过把current插入到pkmap_map_wait等待队列,把current设置为TASK_UNINTERRUPTIBLE,并调schedule放弃CPU来达到此目的。一旦进程被唤醒,函数就调page_address检查是否存在另一个进程已映射了该页。如还没其他进程映射该页,则内循环重新开始。

  1. kunmap撤销先前由kmap建立的永久内核映射。如页确实在高端内存中,则调kunmap_high。

void kunmap_high(struct page* page)
{spin_lock(&kmap_lock);// 这是检测此高端内存内页框释放后,此页框占据的页表表项是否没了使用者,进而可被清理后复用(用来服务于另一个页框)if((--pkmap_count[((unsigned long)page_address(page)-PKMAP_BASE)>>PAGE_SHIFT]) == 1){if(waitqueue_active(&pkmap_map_wait))// 检测等待队列上是否有等待对象wake_up(&pkmap_map_wait);//唤醒首个等待对象spin_unlock(&kmap_lock);}
}

上述括号内的表达式从页的线性地址计算出pkmap_count数组的索引。计数器被减1并与1相比。匹配成功表明没进程在使用页了。函数最终能唤醒由map_new_virtual添加在等待队列中的进程。

在高端内存的任一页框都可通过一个"窗口"映射到内核地址空间。留给临时内核映射的窗口数是非常少的。

每个CPU有它自己的包含13个窗口的集合,它们用enum km_type数据结构表示。该数据结构中定义的每个符号,如KM_BOUNCE_READKM_USER0KM_PTE0,标识了窗口的线性地址。内核必须确保同一窗口永不会被两个不同的控制路径同时使用。故,km_type中的每个符号只能由一种内核成分使用,并以该成分命名。最后一个符号KM_TYPE_NR本身并不表示一个线性地址,但由每个CPU用来产生不同的可用窗口数。

km_type中的每个符号(除了最后一个)都是固定映射的线性地址的一个下标。enum fixed_address数据结构包含符号FIX_KMAP_BEGINFIX_KMAP_END;把后者的值赋成下标FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1。这种方式下,系统中的每个CPU都有KM_TYPE_NR个固定映射的线性地址。此外,内核用fix_to_virt(FIX_KMAP_BEGIN)线性地址对应的页表项的地址初始化kmap_pte变量。

1.kmap_atomic–建立临时内核映射。

void* kmap_atomic(struct page* page, enum km_type type)
{enum fixed_address idx;unsigned long vaddr;current_thread_info()->preempt_count++;// 这样就禁止了内核抢占if(!PageHighMem(page))return page_address(page);idx = type + KM_TYPE_NR * smp_processor_id();// 取得正确索引vaddr = fix_to_virt(FIX_KMAP_BEGIN+idx);// 取得对应线性地址set_pte(kmap_pte-idx/* pte表项地址 */, mk_pte(page/* 页框描述符线性地址 */, 0x63));// 页表注册__flush_tlb_single(vaddr);// TLB刷新return (void*)vaddr;
}

type参数和CPU标识符指定必须用哪个固定映射的线性地址映射请求页。如页框不属于高端内存,则该函数返回页框的线性地址;否则,用页的物理地址及Present,Accessed,Read/WriteDirty位建立该固定映射的线性地址对应的页表项。最后,该函数刷新适当的TLB项并返回线性地址。

2.kunmap_atomic–撤销临时内核映射。 在80x86结构中,这个函数减少当前进程的preempt_count。因此,如在请求临时内核映射之前能抢占内核控制路径, 则在同一个映射被撤销后可再次抢占。此外,kunmap_atomic检查当前进程的TIF_NEED_RESCHED标志是否被置位。如是,就调schedule

伙伴系统算法

内核应为分配一组连续的页框建立一种健壮,高效的分配策略。频繁地请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。

本质上,避免外碎片的方法有两种:

  1. 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。

  2. 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而分割大的空闲块。

基于以下三种原因,内核首选第二种方法:

  1. 某些情况下,连续的页框确实是必要的。因为仅连续的线性地址不足以满足请求。典型例子就是给DMA处理器分配缓存区的内存请求。因为当在一次单独的I/O操作中传送几个磁盘扇区的数据时,DMA忽略分页机制而直接访问地址总线(直接采用物理地址),故,所请求的缓冲区必须位于连续的页框中。

  2. 即使连续页框的分配并不是很必要,但它在保持内核页表不变方面所起的作用也不容忽视。在内核页表中,只需要为这些连续的页框创建一个条目,而不是为每个页框创建一个单独的条目。这可以减少内核页表的大小,并降低内存管理的开销。在查找页表时,操作系统只需要查找一个条目,而不是多个条目。操作系统只需要查找一个页表条目,就可以确定该虚拟地址对应的物理地址。连续页框的分配可以使得内存块更加连续和紧凑。这有助于提高内存利用率,因为操作系统可以更有效地管理和调度内存。修改页表会怎样?频繁修改页表势必导致平均访问内存次数增加,因为这会使CPU频繁刷新TLB(TLB不命中率提高)的内容。

  3. 内核通过4MB的页可访问大块连续的物理内存。这样减少了TLB失效率(TLB命中率提高),提高了访问内存的平均速度。

Linux采用著名的伙伴系统算法来解决外碎片。它把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512,1024个连续的页框的块的集合。对1024个页框的最大请求对应着4MB大小的连续RAM块。伙伴系统保证每个块的第一个页框的物理地址是该块大小的整数倍。例如,大小为16个页框的块,其起始地址是16 \times 2^{12}的倍数

通过举例来说明算法的工作原理:连续页框块申请–假设申请256个连续页框

  • 先在256个页框的链表中检查是否有一个空闲块。如存在,分配此块。

  • 如没有,算法会查找下一个更大的页块。即在512个页框的链表中找一个空闲块。 如存在,内核把空闲块分为两部分。一半用作满足请求。另一半作为新块插入到256个页框的链表。

  • 如在512个页框的块链表没找到空闲块,就继续在1024找。 如找到,内核把1024个页框块的划分为一个256个页框的块用于满足需求。剩余部分划分为一个256页框的新快,一个512页框的新快分别插入对应的链表。

  • 如1024页框链表还没找到,算法就放弃并发出错误信号。(意味着连续页框分配最大只能一次分配4MB内存)、

连续页框块释放:

连续页框块释放时,内核会检查释放块是否可以现有空闲块合并成更大的空闲块。允许合并时,将参与合并的空闲块从链表移除,组成一个新块。对新的块持续如此迭代,直到迭代到无法合并时,块加入链表。

合并成立条件:

  • 两个块有相同的大小,记作b

  • 它们的物理地址是连续的。

  • 第一个块的第一个页框的物理地址是2 \times b \times 2^{12}的倍数。

1.数据结构

Linux 2.6为每个管理区使用不同的伙伴系统。因此,在80x86结构中,有三种伙伴系统:

  • 第一种处理适合ISA DMA的页框。

  • 第二种处理"常规"页框。

  • 第三种处理高端内存页框。

每个伙伴系统使用的主要数据结构如下:

  1. 前面介绍过的mem_map数组。实际上,每个管理区都关系到mem_map元素的子集。子集中的第一个元素和元素的个数分别由管理区描述符的zone_mem_mapsize字段指定。

  2. 包含有11个元素,元素类型为free_area的一个数组,每个元素对应一种特定块大小的链表。该数组存放在管理区描述符的free_area字段中。

考虑管理区描述符中free_area数组的第k个元素,它标识所有大小为2^k个页框的空闲块。这个元素的free_list字段是双向循环链表的头,这个双向循环链表集中了大小为2^k个页框的空闲块对应的页描述符。更精确地说,是空闲块中起始页框的页描述符;指向链表中相邻元素的指针存放在页描述符的lru字段中。除了链表头外,free_area数组的第k个元素同样包含字段nr_free,它指定了大小为2^k个页框的空闲块的个数。如没大小为2^k个页框的空闲块,则nr_free等于0free_list为空。

一个大小为2^k个页框的空闲块的第一个页框的描述符的private字段存放了块的order,即k。正是由于此字段,页块被释放时,内核可确定这个块的伙伴是否也空闲。如是,它可以把两个块结合成大小为2^{k + 1}页框的新块。

2.分配块

__rmqueue–用来在管理区找到一个空闲块

  • 参数:管理区描述符地址,orderorder表示请求的空闲页块大小的对数值。

  • 返回值:如页框被成功分配,__rmqueue就返回第一个被分配页框的页描述符。否则, 返回NULL。

__rmqueue假设调用者已经禁止了本地中断,并获得了保护伙伴系统数据结构的zone->lock自旋锁。 从所请求order的链表开始,它扫描每个可用块链表进行循环搜索,如需要搜索更大的order,就继续搜索。

struct free_area* area;
unsigned int current_order;
for(current_order = order; current_order < 11; ++current_order)
{area = zone->free_area + current_order;if(!list_empty(&area->free_list))goto block_found;
}
return NULL;

如直到循环结束还没找到合适的空闲块,则__rmqueue就返回NULL。否则,找到了一个合适的空闲块,这种情况下,从链表中删除它的第一个页框描述符,并减少管理区描述符中的free_pages的值。

block_found:// 1.定位到链表首个有效元素// 2.链表首个有效元素是一个struct page对象的lru字段。// 3.从lru字段地址导出隶属的struct page对象起始地址page = list_entry(area->free_list.next, struct page, lru);// 从隶属的双向链表中移除该节点list_del(&page->lru);// 清理page的private字段ClearPagePrivate(page);// 暂时被设置为0page->private = 0;// 更新有效块数area->nr_free--;// 更新隶属管理区内空闲页框数zone->free_pages -= 1UL << order;

当为了满足2^h个页框的请求而有必要使用2^k个页框的块时(h<k), 程序就分配前面的2^h个页框,把后面2^k - 2^h个页框循环再分配给free_area链表中下标在hk之间的元素:

// 这是获得得到块尺寸
size = 1 << curr_order;
while(curr_order > order)
{// 规模小一级空闲块area--;// 规模curr_order--;// 页数size >>= 1;// page是分配出去的块的首个页框。page+size得到剩余可放入当前规模块链表的起始页框buddy = page + size;// 将该页框放入当前规模块链表list_add(&buddy->lru, &area->free_list);// 规模块中可用块数量更新area->nr_free++;// 设置该page的private以记录其隶属的块的规模buddy->private = curr_order;// 设置page的标志。来表示其private字段有效。SetPagePrivate(buddy);
}
return page;// 被分配出去的块的首个page的private字段无效

因为__rmqueue已经找到了合适的空闲块,所以它返回所分配的第一个页框对应的页描述符的地址page。 上述分配过程看,每次分配页框会被规整到2的幂次后再执行页框分配(造成分配时内部碎片,牺牲容量,换取性能优化)。

3.释放块

__free_pages_bulk–按伙伴系统的策略释放页框

参数:

  • page:被释放块中所包含的第一个页框描述符的地址

  • zone:管理区描述符的地址

  • order:块大小的对数

函数假设调用者已禁止本地中断(防止外部中断打断执行流程)并获得了保护伙伴系统数据结构的zone->lock(防止其他处理器打断执行流程)自旋锁。__free_pages_bulk先声明和初始化一些局部变量:

struct page* base = zone->zone_mem_map;
unsigned long buddy_idx, page_idx = page - base;
struct page* buddy, *coalesced;
int order_size = 1 << order;// 页数

page_idx包含块中第一个页框的下标,这是相对于管理区中第一个页框而言的。order_size用于增加管理区中空闲页框的计数器:

zone->free_pages += order_size;

现在函数开始执行循环,最多循环(10-order)次,每次都尽量把一个块和它的伙伴进行合并。函数以最小块开始,然后向上移动到顶部:

while(order < 10)
{// order是当前规模// 这里的意思是将page_idx的二进制下第order位取反。// 若此位之前是1,buddy_idx此位是0。这样取得前一个buddy。因为只有前一个buddy才能作为合并后buddy的起始部分。对齐要求。// 若此位之前是0,buddy_idx此位是1。这样取得后一个buddy。此时只有自己才能作为合并后buddy的起始部分。对齐要求。buddy_idx = page_idx ^ (1 << order);buddy = base + buddy_idx;// 验证此page是否符合作为规模为order的buddy块首个page的条件if(!page_is_buddy(buddy, order))break;list_del(&buddy->lru);// 将此buddy块从隶属的双向链表移除zone->free_area[order].nr_free--;// 更新本来隶属的规模中可有块数量// 清理此块首个page的privateClearPagePrivate(buddy);buddy->private = 0;// 确定合并块的首个page的索引。// page_idx的二进制下第order位// 若此位之前是1,buddy_idx此位是0。// 这样合并后块内首个page索引,取buddy_idx// 若此位之前是0,buddy_idx此位是1。// 这样合并后块内首个page索引,取page_idx// page_idx &= buddy_idx得到的结果其余位和page_idx一致。但第order位固定为0。符合上述要求。 page_idx &= buddy_idx;// 这样我们得到了规模为order+1的块及块内首个page。继续迭代。order++;
}

在循环体内,函数寻找块的下标buddy_idx,它是拥有page_idx页描述符下标的块的伙伴。结果这个下标可被简单地如下计算:

buddy_idx = page_idx ^ (1 << order);

实际上,使用(1<<order)掩码的异或转换page_idxorder位的值。 因此,如这个位原先是0buddy_idx就等于page_idx+order_size;如这个位原先是1buddy_idx就等于page_idx - order_size。 一旦知道了伙伴块下标,就可通过下式很容易获得伙伴块的页描述符:

buddy = base + buddy_idx;

现在调page_is_buddy来检查buddy是否描述了大小为order_size的空闲页框块的第一个页。

int page_is_buddy(struct page* page, int order)
{if(PagePrivate(buddy) && page->private == order && !PageReserved(buddy) && page_count(page) == 0)return 1;return 0;
}

buddy的第一个页必须为空闲(_count等于-1),它必须属于动态内存,它的private字段必须有意义,最后private字段必须存放将要被释放的块的order。如所有这些条件都符合,伙伴块就被释放,且函数将它从以order排序的空闲块链表上删除,并再执行一次循环以寻找两倍大小的伙伴块。如page_is_buddy中至少有一个条件没被满足,则该函数跳出循环,因为获得的空闲块不能再和其他空闲块合并。函数将它插入适当的链表并以块大小的order更新第一个页框的private

// 得到最终合并块的首个page
coalesced = base + page_idx;
// 设置其private
coalesced->private = order;
SetPagePrivate(coalesced);
// 将page加入对应规模块的双向链表
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;// 更新对应规模内有效块数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/6432.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 Python 异步协程中使用同步队列

在 Python 异步协程中使用同步队列 使用 Python asyncio 进行异步编程时&#xff0c;如果需要在协程间交互数据可以使用异步队列 asyncio.Queue。但 asyncio.Queue 不是线程安全的&#xff0c;如果需要在不同线程间的异步程序之间或者不同线程的异步程序和同步程序间交换数据&a…

c++实战篇(三) ——对socket通讯服务端与客户端的封装

前言 在前面几篇文章中我们介绍过一些有关于网络编程的相关知识,我们介绍了在tcp传输数据的时候发送缓冲区与接收缓冲区&#xff0c;我们介绍了在tcp发送与接收的过程中可能出现的分包与粘包的问题&#xff1a; c理论篇(一) ——浅谈tcp缓存与tcp的分包与粘包 我们介绍了在网络…

MongoDB与Elasticsearch特性及知识点对比

仅作为技术选型和面试参考。对比记忆更佳。 目录 1.数据模型 2.索引机制 3. 查询性能 4.分布式架构 5.数据一致性 6.文档大小限制 7.存储引擎 8.数据压缩 10.实时搜索 11.安全性 12.版本控制 13 事务支持 14.地理空间搜索 15.多租户支持 16 运维复杂度 17,监…

直播素材安卓情侣飞行棋v2.22 仿dofm 支持自定义模式—可用直播素材

一个情侣间增进友谊的小游戏非常好玩&#xff0c;适合男孩女孩之间增进感情&#xff01;快和你暗恋的女孩一块玩吧&#xff0c;极速升温 永久免费&#xff01;解锁激活码内容全部畅玩&#xff01;全网最强超级给力&#xff01;真人说书音频 网盘自动获取 链接&#xff1a;http…

重要综述!全文翻译!宫鹏教授、陈镜明教授、梁顺林教授等《Nature Climate Change》!

2013年&#xff0c;由宫鹏教授、陈镜明教授和梁顺林教授等联合发表了一篇《Nature Climate Change》综述文章&#xff0c;其主题是卫星遥感在全球变化中的作用研究。&#xff08;已被引510次&#xff0c;来源谷歌学术&#xff09;。 卫星遥感方式对于气象问题、大气、陆地和海洋…

【机器学习】集成方法---Boosting之AdaBoost

一、Boosting的介绍 1.1 集成学习的概念 1.1.1集成学习的定义 集成学习是一种通过组合多个学习器来完成学习任务的机器学习方法。它通过将多个单一模型&#xff08;也称为“基学习器”或“弱学习器”&#xff09;的输出结果进行集成&#xff0c;以获得比单一模型更好的泛化性…

【中断】【ARM64】学习总结

optee中的异常向量表解读–中断处理解读 https://mp.weixin.qq.com/s/gBsy4YDYTHGRsy2zcVr6Vg

操作系统:磁盘交换空间

什么是磁盘交换空间? 磁盘交换空间(swap space)是在磁盘上预留出来的一块区域&#xff0c;用作补充系统物理内存&#xff08;RAM&#xff09;的一种方式。当系统的物理内存不足以存储当前所有活动进程所需的数据时&#xff0c;操作系统会将一部分暂时不用或使用较少的内存数据…

windows ubuntu sed,awk,grep篇:13.其他 awk 命令

目录 85. 使用 printf 格式化输出 86. awk 内置数值函数 87. 随机数生成器 88. 常用字符串函数 89. GAWK/NAWK 的字符串函数 90. GAWK 字符串函数 91.处理参数(ARGC,ARGV,ARGIND) 92. OFMT 93. GAWK 内置的环境变量 94. pgawk – awk 运行分析器 95. 位操作 96.用户…

Linux搭建sqlilabs靶场

提前准备&#xff1a; 文章中所使用到的Linux系统&#xff1a;Ubantu20.4sqlilabs靶场下载地址&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. 一. 安装phpstudy phpstudy安装命令&#xff1a;wget -O install.sh h…

python 的继承、封装和多态

1. 继承&#xff08;Inheritance&#xff09; 继承是面向对象编程中的一个重要概念&#xff0c;它允许一个类&#xff08;子类&#xff09;继承另一个类&#xff08;父类&#xff09;的属性和方法。子类可以重用父类的代码&#xff0c;同时也可以扩展或修改父类的行为。 常用…

托普利兹矩阵(T矩阵)及其应用(Matlab demo测试)

托普利兹矩阵&#xff08;T矩阵&#xff09;及其应用&#xff08;Matlab demo测试&#xff09; 1. 概念2. Matlab简单测试2.1 生成测试2.2 基本性质及原理2.3 性质验证 3. 其他应用总结3.1 其他性质3.2 文献阅读看到的 参考资料 1. 概念 托普利兹矩阵&#xff0c;简称为T型矩阵…

H3C MSTP 实验

H3C MSTP 实验 实验拓扑 ​​ 实验需求 所有交换机上创建 Vlan10&#xff0c;Vlan20&#xff0c;Vlan30 和 Vlan40所有交换机之间的端口配置为 Trunk&#xff0c;并放行相关 VLAN按照图示分区域配置 MSTP&#xff0c;并配置主备根网桥 实验步骤 VLAN基础配置&#xff08;…

力扣面试150 简化路径 栈 模拟

Problem: 71. 简化路径 思路 &#x1f469;‍&#x1f3eb; 三叶题解 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) Code class Solution {public String simplifyPath(String path){ArrayDeque<String> d new ArrayDeque<>();…

2022 亚马逊云科技中国峰会,对话开发者论坛

目录 前言 最近整理资料发现还有一些前 2 年的内容没发出来&#xff0c;故补发记录&#xff0c;每年都有新的感悟。 开发者论坛 1. 你认为什么是开发者社区&#xff0c;如何定义一个成功的开发者社区&#xff1f; 我认为可以把开发者社区看成一个 “产品” 来对待&#xff…

【RAG 论文】GenRead:“generate-read“ 可能比 “retrieve-read“ 更有效

论文&#xff1a;Generate rather than Retrieve: Large Language Models are Strong Context Generators ⭐⭐⭐⭐ ICLR 2023 Code: github.com/wyu97/GenRead 一、论文速读 该工作发现&#xff1a;由 LLM 生成的文档中&#xff0c;往往比 retrieved documents 更可能包含正确…

C++校招八股

c类的访问权限与继承方式 公有成员在任何地方都可以被访问&#xff0c;包括类的外部和派生类。受保护成员在类的内部和派生类中可以被访问&#xff0c;但在类的外部不可访问。 私有成员只能在类的内部访问&#xff0c;包括类的成员函数和友元函数&#xff0c;不允许在类的外部…

一步一步写线程之十一线程池应用内存池

一、内存池 内存池&#xff0c;非常好理解&#xff0c;就是存储内存的一个池子&#xff08;Pool&#xff09;&#xff0c;一般来说&#xff0c;都是使用各种容器或者自己实现的类似容器的内存管理类。内存池其实就是为了解决两个主要问题&#xff0c;一个是内存反复分配回收的…

关于“泼辣”DB 你应该知道的几件事

PolarDB PolarDB for PostgreSQL&#xff08;以下简称 PolarDB&#xff09;是一款阿里云自主研发的企业级数据库产品&#xff0c;采用计算存储分离架构&#xff0c;100% 兼容 PostgreSQL。 PolarDB 的存储与计算能力均可横向扩展&#xff0c;具有高可靠、高可用、弹性扩展等企…

文件(夹)批量重命名数字、字母、日期、中文数字大写小写

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 目标是重命名下面5个文件&#xff08;也可以是文件夹等&#xff0c;任意&#xff09;&#xff0c;从大写中文数字“贰”开始 打开工具&#xff0c;找到“文…