深入理解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);
    // 暂时被设置为0
    page->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的private
    ClearPagePrivate(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.mfbz.cn/a/592120.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mac M2 本地下载 Xinference

想要在Mac M2 上部署一个本地的模型。看到了Xinference 这个工具 一、Xorbits Inference 是什么 Xorbits Inference&#xff08;Xinference&#xff09;是一个性能强大且功能全面的分布式推理框架。可用于大语言模型&#xff08;LLM&#xff09;&#xff0c;语音识别模型&…

激动,五四青年节,拿下YashanDB认证YCP

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中国DBA联盟(ACDU)成员&#xff0c;10余年DBA工作经验&#xff0c; Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主&#xff0c;全网粉丝10万 擅长主流Oracle、My…

中间件之搜索和数据分析组件Elasticsearch

一、概述 1.1介绍 The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash&#xff08;也称为 ELK Stack&#xff09;。 能够安全可靠地获取任何来源、任何格式的数据&#xff0c;然后实时地对数据进行搜索、分析和可视 化。Elaticsearch&#xff0c;简称为 ES&a…

CUDA和显卡驱动

1.安装显卡驱动 https://www.nvidia.com/download/index.aspx?langen-us 由于我的显卡是RTX4060&#xff0c;因此先选择RTX40系列&#xff0c;然后选择RTX4060&#xff0c;进行安装 2.查看显卡对应的CUDA CUDA安装地址&#xff1a;https://developer.nvidia.com/cuda-toolk…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-12-蜂鸣器

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

复旦微JFM7VX690计算后IO接口模块,用于雷达信号处理、数据处理等需要高速密集计算的应用场景

计算后IO接口模块 1 介绍 1.1 产品概述 计算后IO接口模块主要由复旦微JFM7VX690型FPGA、国产以太网收发器YT8521、国产BMC芯片GD32F450、国产CPLD芯片EF2L45BG256B、国产内存颗粒等主要芯片组成&#xff0c;采用标准6U VPX尺寸设计。 本计算后IO接口模块主要用于雷达信号处…

QT+串口调试助手+基本版

一、创建串口调试助手UI界面 1、首先生成串口连接必要参数界面&#xff0c;删除关闭串口控件 2、给参数下拉框添加常见的选项&#xff0c;删除关闭串口控件 3、将串口调试助手参数界面布局整齐&#xff0c;删除关闭串口控件 4、更改控件名字&#xff0c;方便后续编程&#xff…

第五篇:通信脉络:探索计算机外设与总线体系的精髓

通信脉络&#xff1a;探索计算机外设与总线体系的精髓 1 引言 在这个技术日新月异的时代&#xff0c;理解计算机系统的基本构成要素 —— 总线和外设 —— 对于每个从事技术工作的人来说都是至关重要的。这些组件不仅是计算机通信的基石&#xff0c;也直接影响着系统的性能、效…

Universal Thresholdizer:将多种密码学原语门限化

参考文献&#xff1a; [LS90] Lapidot D, Shamir A. Publicly verifiable non-interactive zero-knowledge proofs[C]//Advances in Cryptology-CRYPTO’90: Proceedings 10. Springer Berlin Heidelberg, 1991: 353-365.[Shoup00] Shoup V. Practical threshold signatures[C…

关于MS-DOS时代的回忆

目录 一、MS-DOS是什么&#xff1f; 二、MS-DOS的主要功能有哪些&#xff1f; 三、MS-DOS的怎么运行的&#xff1f; 四、微软开源MS-DOS源代码 五、高手与漂亮女同学 一、MS-DOS是什么&#xff1f; MS-DOS&#xff08;Microsoft Disk Operating System&#xff09;是微软公…

多线程与信号量简介

信号量与 PV 操作 计算机中信号量的本质是整数&#xff0c;数值表示可用的资源数量 P 操作 (Passeren > 通过, 原子操作) 若信号量 0&#xff0c;当前任务阻塞 (进入信号量等待队列)若信号量 > 0&#xff0c;则&#xff1a;将信号量数值减一&#xff0c;当前任务继续执…

USP技术提升大语言模型的零样本学习能力

大语言模型&#xff08;LLMs&#xff09;在零样本和少样本学习能力上取得了显著进展&#xff0c;这通常通过上下文学习&#xff08;in-context learning, ICL&#xff09;和提示&#xff08;prompting&#xff09;来实现。然而&#xff0c;零样本性能通常较弱&#xff0c;因为缺…

KMP算法--C语言实现

#include <stdio.h> #include <assert.h> #include <string.h> #include <stdlib.h>void GetNext(char* sub, int next[]) {int lenSub strlen(sub);next[0] -1; // 初始第一个为 -1 第二个为 0next[1] 0;int i 2;int k 0;while (i < lenSub){…

探究Android的多分辨率支持以及各种类型图标尺寸大小

术语和概念 屏幕尺寸 屏幕的物理尺寸&#xff0c;以屏幕的对角线长度作为依据&#xff08;比如 2.8寸&#xff0c; 3.5寸&#xff09;。 简而言之&#xff0c; Android把所有的屏幕尺寸简化为三大类&#xff1a;大&#xff0c;正常&#xff0c;和小。 程序可以针对这三种尺寸…

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例 引言1. UniMRCP和UmcFramework简介2. 准备工作3. unimrcpclient.xml配置文件3.1 定义SIP设置3.2 定义MRCP会话配置文件 4. C代码示例5. 测试和验证6. 故障排查7. 结论8. 参考文献 引言 在多媒体通信…

Vue单页面应用和多页面应用的区别

概念&#xff1a; SPA单页面应用&#xff08;SinglePage Web Application&#xff09;&#xff0c;指只有一个主页面的应用&#xff0c;一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面&#xff0c;对每一个功能模块组件化。单页应用跳转&#xff0c;就是切换…

STM32标准库编译流程

导入库函数 在ST官方固件库中找到STM32F10x_StdPeriph_Lib_V3.5.0.zip文件&#xff0c;解压&#xff0c;打开Libraries,接着打开STM32F10x_StdPeriph_Driver文件夹&#xff0c;继续点击src&#xff0c;看到库函数源文件&#xff1a; 将其复制到keil建立的工程的文件中&#xf…

JAVA系列 小白入门参考资料 接口

目录 接口 接口的概念 语法 接口使用 接口实现用例 接口特性 实现多个接口和实现用例 接口间的继承 接口 接口的概念 在现实生活中&#xff0c;接口的例子比比皆是&#xff0c;比如&#xff1a;笔记本上的 USB 口&#xff0c;电源插座等。 电脑的 USB 口上&am…

在视频中使用时间卷积和半监督训练进行三维人体姿态估计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;在视频中使用时间卷积和半监督训练进行三维人体姿态估计1、文献摘要2、提出方法2.1、时间扩张卷积模型2.2、半监督方法2.3、与传统…

【错题集-编程题】十字爆破(预处理 + 模拟)

牛客对于题目链接&#xff1a;十字爆破 (nowcoder.com) 一、分析题目 暴力模拟会超时。 预处理&#xff0c;先把每一行以及每一列的和存起来。 模拟即可&#xff0c;但是由于数据量过⼤&#xff0c;我们可以提前把每⼀⾏以及每⼀列的和存起来&#xff0c;⽅便统计总和。 二、代…
最新文章