凡铁游戏提供最新游戏下载和手游攻略!

深入解析Linux内核源码6Linux内存管理(二)

发布时间:2024-10-21浏览:13

———— 零音学院

6.3 内存的分配和回收

内存初始化完成后,内核映像(内核代码和数据)常驻于内存中。从现在开始,使用

用户程序的执行和结束需要不断的分配和释放物理页。内核应该分配一组连续的页面

并建立稳定、高效的分销策略。为此,必须解决一个更重要的内存管理问题,那就是外部碎片

胶片问题。频繁地请求和释放一组连续的不同大小的页面将不可避免地导致许多分配的内存块被分散。

小块的空闲页面。出现的问题是,即使这些小块的空闲页面加起来足以满足请求

页,但分配一个大的连续页可能根本不够。 Linux采用了著名的Buddy

系统算法解决外部碎片问题。

但请注意,在Linux中,CPU不能通过物理地址访问存储空间,而必须使用虚拟地址;

因此,对于内存页的管理,通常是先在虚拟内存空间中分配一段虚拟内存区间,然后根据需要进行分配。

该间隔分配相应的物理页并建立映射。也就是说先分配虚拟内存区间,再先分配物理页。

分配稍后再说,不过为了继续上一节的内容,我们先介绍内存的分配和回收,然后再介绍用户的

进程虚拟存储区间的建立。

6.3.1 伙伴算法

1原则

Linux的伙伴算法将所有空闲页面分为10个块组,每个组中块的大小是2页的幂。

例如,组0 中的块的大小全部为20(1 页),组1 中的块的大小全部为21(2 页),

第9 组中的块大小均为29(512 页)。即每组中的块的大小是相同的,这也是

大小的块形成一个链接列表。

我们通过一个简单的例子来说明该算法的工作原理。

假设需要分配的块大小为128页(多个页组成的块称为页块)。应该

该算法首先搜索一个块大小为128页的链表,看是否有这样的空闲块。如果有就直接除掉

如果没有,算法会寻找下一个更大的块,具体来说,是一个块大小为256 页的链表

查找中的空闲块。如果存在这样的空闲块,内核会将这256 个页分成两等份,并分配一个

Out,另一个副本被插入到链表中,块大小为128页。如果在块大小为256页的链表中也

如果没有找到空闲页块,则搜索将继续到更大的块,即512 页的块。如果存在这样的块,内核会以

从512页的块中分支出128页来满足请求,然后从384页中取出256页插入到块中

在大小为256 页的链接列表中。然后将剩余的128 页插入到块大小为128 页的链表中。

如果512 页链表中没有空闲块,算法将放弃分配并发出错误信号。

上述过程的逆过程就是区块的释放过程,这也是该算法名称的由来。满足以下条件的两人

块称为伙伴:

(1) 两个块大小相同;

(2)两个块的物理地址是连续的。

伙伴算法将满足上述条件的两个块合并为一个块。该算法是一种迭代算法。如果合并后的块

也可以与相邻的块合并,然后算法继续合并。

2数据结构

在6.2.6节介绍的管理区数据结构struct zone_struct中,涉及到自由区数据结构。

结构:

free_area_t free_area[MAX_ORDER];

我们再次对free_area_t进行更详细的描述。

#difine MAX_ORDER 10 type struct free_area_struct { struct list_head free_list unsigned int *map } free_area_t 其中list_head字段是一般的双向链表结构,链表中元素的类型将为mem_map_t(即

struct 页面结构)。 Map 字段指向一个位图,其大小取决于可用页面的数量。空闲区域项目

位图的每一位描述了两个大小为2k 页的伙伴块的状态。如果位图的某个位为0,则表

指示一对同级块中的任意两个是空闲的,或者两者都已分配。如果为1,则必须已分配1 个块。什么时候

当兄弟块空闲时,内核将它们视为大小为2k+1 的单个块。图6.9 数据结构

结构示意图。

在图6.9中,free_aea数组的元素0包含一个空闲页(页号0);而元素2 包含

包含两个空闲页块,大小为4 页。第一个页块以数字4 开头,第二个页块从数字4 开始

起始编号是56。

我们提到,当需要分配多个内存页时,用于DMA的内存页必须是连续的。实际上

为了便于管理,从伙伴算法可以看出,只要请求的块大小不超过512页(2KB),内部

核心尝试分配连续的页面。

6.3.2 物理页的分配和释放

当进程请求分配连续的物理页时,它可以通过调用alloc_pages() 来实现。 Linux 2.4

版本中有两个alloc_pages(),一个在mm/numa.c,另一个在mm/page_alloc,c,编译

该选择是根据定义的条件选项CONFIG_DISCONTIGMEM 进行的。

1非统一内存架构(NUMA) 中的页面分配

CONFIG_DISCONTIGMEM条件编译的意思是“不连续的存储空间”。 Linux 将不连续的存储空间视为

该空间也被归类为非均匀内存架构(NUMA)。这是因为不连续存储空间本质上是广义的

NUMA,因为这意味着最低物理地址和最高物理地址之间有一个空洞,有空洞的空间当然是

“不一致”。因此,地址不连续的物理空间必须像不同结构的物理空间一样被划分为多个段。

连续且均匀的“节点”。因此,在存储结构不连续的系统中,每个模块都有多个节点,因此

有一个pg_data_t数据结构队列。我们首先看一下mm/numa.c中的alloc_page()函数:

/* * 这可以改进。目前,尝试进行循环,而* 应该从当前节点开始进行集中循环搜索。 */struct page * _alloc_pages(unsigned int gfp_mask, unsigned int order){ struct page *ret=0; pg_data_t *start, *temp;#ifndef CONFIG_NUMA 无符号长标志; static pg_data_t *next=0;#endif if (order=MAX_ORDER) return NULL;#ifdef CONFIG_NUMA temp=NODE_DATA(numa_node_id());#else spin_lock_irqsave(node_lock, flags); if (!next) next=pgdat_list;温度=下一个;下一个=下一个节点_下一个; spin_unlock_irqrestore (node_lock, 标志); #endif开始=临时; while (temp) { if ((ret=alloc_pages_pgdat(temp , gfp_mask, order))) return (ret); } temp=temp-node_next; } 临时=pgdat_list; while (temp !=start) { if ((ret=alloc_pages_pgdat (temp, gfp_mask, order))) return (ret) ; temp=temp-node_next;返回(0);该函数的说明如下。

该函数有两个参数。 gfp_mask表示使用哪种分配策略。参数order代表所需物理块的大小

小,可以是1、2、3 最多2MAX_ORDER-1。

如果定义了CONFIG_NUMA,即在有NUMA结构的系统中,可以通过NUMA_DATA()宏找到

将CPU所在节点的pg_data_t数据结构进行排队,存放在临时变量temp中。

如果在不连续的UMA结构中,存在pg_data_t数据结构pgdat_list、pgdat_list的队列

这是队列的头。因为队列通常是关键资源,当对队列执行两个以上操作时

锁定。

依次从各个节点开始分配,平衡各个节点的负载。函数中有两个循环,其形式基本上是

同样的,也就是节点队列基本扫描两次,直到某个节点分配成功,然后跳出循环。

否则,完全失败并返回0。对于每个节点,调用alloc_pages_pgdat()函数尝试分配

所需的页面。

2统一内存架构(UMA) 中的页面分配

连续空间UMA结构体的alloc_page()定义在include/linux/mm.h中:

#ifndef CONFIG_DISCONTIGMEM static inline struct page * alloc_pages(unsigned int gfp_mask, unsigned int order){ /* * 被编译器优化掉。 */if (order=MAX_ORDER) return NULL; return __alloc_pages(gfp_mask, order, contig_page_data.node_zonelists+ (gfp_mask GFP_ZONEMASK));从该函数的定义可以看出,alloc_page()是_alloc_pages()的封装函数,而

_alloc_pages()是伙伴算法的核心。该函数定义在mm/page_alloc.c中。我们首先

简要描述了功能。

_alloc_pages()在管理区域列表zonelist中依次搜索各个区域,找到符合要求的区域。

然后使用伙伴算法从该区域分配给定大小(2阶)的页块。如果所有地区都没有足够的

如果该页空闲,则调用swapper或bdflush内核线程将脏页写入磁盘以释放一些页。

__alloc_pages() 和虚拟内存(简称VM)代码之间有一些复杂的接口(稍后会详细介绍)

描述)。每个区域都会跟踪刚刚映射到进程虚拟机的页面。映射的页面可能只是

已标记,但未实际分配。因为根据虚拟存储的分配原则,必须尽可能多地分配物理页。

量被推迟到不能再推迟为止,即进程的代码或数据必须加载到内存中时,才真正

分配物理页。

明确了页面分配的基本原理后,我们详细分析其代码如下:

/* * 这是分区伙伴分配器:的“心脏” */struct page * __alloc_pages(unsigned int gfp_mask, unsigned int order, zonelist_t *zonelist) { unsigned long min; zone_t **区域,*类区域; struct page * page ;int 已释放;区域=区域列表区域;类区=*区;最小=1UL 订单; for (;) { zone_t *z=*(zone++); if (!z) 中断; min +=z-pages_low; if (z-free_pages min) { page=rmqueue(z, order); if (page) 返回页面;这是分配策略中指定的所有页面管理区域的循环。在循环中,依次检查每个区域的可用空间。

总页数。如果总量仍然大于“最低水位线”与请求页数之和,则调用rmqueue() 尝试

图片是从这个区域分配的。如果分配成功,则返回一个页结构指针,指向页块中的第一页。

起始地址。

类区-need_balance=1; mb();如果(waitqueue_active(kswapd_wait))wake_up_interruptible(kswapd_wait);如果发现管理区的空闲页总量已经下降到最低点,则需要重新平衡zone_t结构

flag(need_balance)设置为1,如果内核线程kswapd正在等待队列中休眠,则将其唤醒,

让它回收一些页面来使用(可以看到,need_balance是和kswapd结合使用的)。

区域=区域列表区域;最小=1UL 订单; for (;) { unsigned long local_min; zone_t *z=*(zone++); if (!z) 中断; local_min=z-pages_min; if (!(gfp_mask __GFP_WAIT) ) local_min=2; min +=local_min; if (z-free_pages min) { page=rmqueue(z, order); if (page) 返回页面;如果给定的分配策略中的所有页面管理区域都分配失败,那么我们就要把原来的“最低水位”移至

向下调整(除以4),然后看是否满足要求(z-free_pages min)。如果满足要求,则调用rmqueue

() 进行分配。

/* 这里我们处于内存不足的慢速路径*/rebalance: if (current-flags (PF_MEMALLOC | PF_MEMDIE)) { zone=zonelist-zones; for (;) { zone_t *z=* (zone++); if (!z) 中断; page=rmqueue(z, order); if (page) 返回页面;返回NULL;如果分配不成功,则取决于请求分配内存页的进程类型。其中PF_MEMALLOC

PF_MEMDIE是进程的task_struct结构中flags字段的值。对于正在分配页面的进程(例如

kswapd内核线程),其PF_MEMALLOC值为1(一般进程的该标志为0),并且为内存

因溢出而被终止的进程会将其PF_MEMDIE 设置为1。无论哪种情况,都意味着必须将该页分配给该进程

(想想为什么)。那么,继续执行任务吧。

/* 原子分配- 我们无法平衡任何东西*/if (! (gfp_mask __GFP_WAIT)) return NULL;如果请求分配页面的进程无法等待或重新调度,则必须等待直到页面被分配。

“空手而归”。

page=Balance_classzone (classzone, gfp_mask, order, freed); if (page) 返回页面;如果经过多次努力,必须获取页面的进程(如kswapd)还没有分配页面,则必须调用它

Balance_classzone()函数释放当前进程占用的本地页面。如果发布成功则返回

页结构指针,指向页块中第一页的起始地址。

区域=区域列表区域;最小=1UL 订单; for (;) { zone_t *z=* (zone++); if (!z) 中断; min +=z-pages_min; if (z-free_pages min) { page=rmqueue(z, order); if (page) 返回页面; } } 继续分配。

/* 不要让大订单分配循环* if (order 3) return NULL; /* kswapd 的收益,然后重试*/current-policy |=SCHED_YIELD; __set_current_state(TASK_RUNNING);日程();转到重新平衡; }在这个函数中,频繁调用rmqueue()函数。我们来仔细看看这个函数的内容。

(1)rmqueue()函数

该函数尝试从页管理区域分配几个连续的内存页。这是最基本的分配操作,其中有

正文代码如下:

静态结构页*rmqueue(zone_t *zone, unsigned int order){free_area_t *area=zone-free_area + order;unsigned int curr_order=order;struct list_head *head, *curr;unsigned long flags;struct page *page;spin_lock_irqsave( zone-lock, flags);do {head=area-free_list;curr=memlist_next(head);if (curr !=head) {unsigned int index;page=memlist_entry(curr, struct page, list);if (BAD_RANGE(区域、页面))BUG(); memlist_del(当前);索引=页-zone-zone_mem_map; if (curr_order !=MAX_ORDER-1) MARK_USED (索引, curr_order, 区域); zone-free_pages -=1UL 订单;页面=展开(区域,页面,索引,顺序,curr_order,区域); spin_unlock_irqrestore(区域锁定,标志); set_page_count(页, 1); if (BAD_RANGE(区域,页面))BUG(); if (PageLRU(页) ) BUG(); if (PageActive(页面)) BUG();返回页面;} curr_order++;面积++; while (curr_order MAX_ORDER); spin_unlock_irqrestore(区域锁定,标志);返回空值;该函数的解释如下。

参数zone指向要分配页的管理区,order表示需要分配的页数为2 order。

do循环从free_area数组的第序元素开始,扫描组成页面结构的每个元素。

双向循环空闲队列。如果找到合适的页块,则将其从队列中删除。删除过程不允许其他

进程和其他处理器会发生中断。所以使用spin_lock_irqsave() 来锁定这个循环。

首先在完全满足大小要求的队列中进行分配。其中memlist_entry(curr, 结构页,

list) 来获取空闲块的第一页的地址。如果这个地址是无效地址,就会陷入BUG()。如果

有效,memlist_del(curr) 从队列中删除分配的页块。如果分配了页块,则

要在frea_area 中标记位图,可以通过调用MARK_USED() 宏来完成。

如果分配后还有剩余块,则通过expand()获取分配的页块,并将剩余块链入适当的页块

当处于空闲队列时。

如果当前空闲队列中没有空闲块,则会从一个更大的空闲块队列中查找。

(2)expand()函数

该函数的源码如下。

静态内联结构体页* 扩展(zone_t *zone, 结构体页*page, unsigned long index, int low, int high, free_area_t * area) { unsigned long size=1 high; while (high low) { if (BAD_RANGE (区域, 页面)) BUG();区域- ;高的- ;大小=1; memlist_add_head((页)-列表,(区域)-free_list); MARK_USED(指数, 最高点, 面积);索引+=大小;页+=大小; } if (BAD_RANGE(区域,页面))BUG();返回页面;该函数解释如下。

参数zone指向分配的页块所在的管理区域; page指向分配的页块;索引已分配

该页在mem_map中的索引; low 表示所需的页块大小为2 low,high 表示从空闲队列中重新分配该页。

实际分配的页块大小为2高; area是一个free_area_struct结构,指向实际分配的页块。

从上面的介绍可以知道,返回给请求者的块大小为2low page,剩余的page放入merge

适当的空闲队列,并相应地修改伙伴系统的位图。例如,假设我们需要一个2 页的块,

然而,我们必须从顺序3(8 页)的空闲队列中进行分配,假设我们碰巧选择

使用第800 页作为页面块的底部。在我们的示例中,这些参数值为:

页==mem_map+800

指数==800

低==1

高==3

area==zone-free_area+high (即frea_area数组中索引为3的元素)

首先将size初始化为分配块中的页数(例如,size=13==8)

while 循环执行循环搜索。每次循环时将大小减少一半。如果我们从空闲队列中分配一个

block 与请求的大小匹配,然后low=high,完全跳出循环并返回分配的页块。

如果分配的物理块位于大于所需块大小(即2 high2low)的空闲块中,则该空闲块为

将其分成两半(即area--;high--;size=1),然后调用memlist_add_head()进行分配

页块被添加到下一个较低级别的空闲队列中(物理块减半),准备好

从剩下的一半空闲块中重新进 行分配,并调用 MARK_USED()设置位图。 在上面的例子中,第 1 次循环,我们从页面 800 开始,把页面大小为 4(即 2high--)的块 其首地址插入到 frea_area[2]中的空闲队列;因为 low 面 804 开始,把页面大小为 2 的块插入到 frea_area[1]中的空闲队列,此时,page=806, high=low=1,退出循环,我们给调用者返回从 806 页面开始的一个 2 页面块。 从这个例子可以看出,这是一种巧妙的分配算法。 3.释放页面 从上面的介绍可以看出,页面块的分配必然导致内存的碎片化,而页面块的释放则可以 将页面块重新组合成大的页面块。页面的释放函数为__free_pages(page struct *page, unsigned long order) ,该函数从给定的页面开始,释放的页面块大小为 2order。原函数为: void __free_pages(page struct *page, unsigned long order){ if (!PageReserved(page) && put_page_testzero(page)) __free_pages_ok(page, order);}其中比较巧妙的部分就是调用 put_page_testzero()宏,该函数把页面的引用计数减 1, 如果减 1 后引用计数为 0,则该函数返回 1。因此,如果调用者不是该页面的最后一个用户, 那么,这个页面实际上就不会被释放。另外要说明的是不可释放保留页 PageReserved,这是 通过 PageReserved()宏进行检查的。 如果调用者是该页面的最后一个用户,则__free_pages() 再调用 __free_pages_ok()。 __free_pages_ok()才是对页面块进行释放的实际函数,该函数把释放的页面块链入空闲链 表,并对伙伴系统的位图进行管理,必要时合并伙伴块。这实际上是 expand()函数的反操作, 我们对此不再进行详细的讨论。

6.3.3 Slab 分配机制

采用伙伴算法分配内存时,每次至少分配一个页面。但当请求分配的内存大小为几十个 字节或几百个字节时应该如何处理?如何在一个页面中分配小的内存区,小内存区的分配所 产生的内碎片又如何解决? Linux 2.0 采用的解决办法是建立了 13 个空闲区链表,它们的大小从 32 字节到 132056 字节。从 Linux 2.2 开始,MM 的开发者采用了一种叫做 Slab 的分配模式,该模式早在 1994 年就被开发出来,用于 Sun Microsystem Solaris 2.4 操作系统中。Slab 的提出主要是基于 以下考虑。 内核对内存区的分配取决于所存放数据的类型。例如,当给用户态进程分配页面时,内 核调用 get_free_page()函数,并用 0 填充这个页面。而给内核的数据结构分配页面时,事 情没有这么简单,例如,要对数据结构所在的内存进行初始化、在不用时要收回它们所占用 的内存。因此,Slab 中引入了对象这个概念,所谓对象就是存放一组数据结构的内存区,其 方法就是构造或析构函数,构造函数用于初始化数据结构所在的内存区,而析构函数收回相 应的内存区。但为了便于理解,你也可以把对象直接看作内核的数据结构。为了避免重复初 始化对象,Slab 分配模式并不丢弃已分配的对象,而是释放但把它们依然保留在内存中。当 以后又要请求分配同一对象时,就可以从内存获取而不用进行初始化,这是在 Solaris 中引 入 Slab 的基本思想。 实际上,Linux 中对 Slab 分配模式有所改进,它对内存区的处理并不需要进行初始化或 回收。出于效率的考虑,Linux 并不调用对象的构造或析构函数,而是把指向这两个函数的 指针都置为空。Linux 中引入 Slab 的主要目的是为了减少对伙伴算法的调用次数。 实际上,内核经常反复使用某一内存区。例如,只要内核创建一个新的进程,就要为该 进程相关的数据结构(task_struct、打开文件对象等)分配内存区。当进程结束时,收回这 些内存区。因为进程的创建和撤销非常频繁,因此,Linux 的早期版本把大量的时间花费在 反复分配或回收这些内存区上。从 Linux 2.2 开始,把那些频繁使用的页面保存在高速缓存 中并重新使用。 可以根据对内存区的使用频率来对它分类。对于预期频繁使用的内存区,可以创建一组 特定大小的专用缓冲区进行处理,以避免内碎片的产生。对于较少使用的内存区,可以创建 一组通用缓冲区(如 Linux 2.0 中所使用的 2 的幂次方)来处理,即使这种处理模式产生碎 片,也对整个系统的性能影响不大。 硬件高速缓存的使用,又为尽量减少对伙伴算法的调用提供了另一个理由,因为对伙伴 算法的每次调用都会“弄脏”硬件高速缓存,因此,这就增加了对内存的平均访问次数。 Slab 分配模式把对象分组放进缓冲区(尽管英文中使用了 Cache 这个词,但实际上指的 是内存中的区域,而不是指硬件高速缓存)。因为缓冲区的组织和管理与硬件高速缓存的命中 率密切相关,因此,Slab 缓冲区并非由各个对象直接构成,而是由一连串的“大块(Slab)” 构成,而每个大块中则包含了若干个同种类型的对象,这些对象或已被分配,或空闲,如图 6.10 所示。一般而言,对象分两种,一种是大对象,一种是小对象。所谓小对象,是指在一 个页面中可以容纳下好几个对象的那种。例如,一个 inode 结构大约占 300 多个字节,因此, 一个页面中可以容纳 8 个以上的 inode 结构,因此,inode 结构就为小对象。Linux 内核中把 小于 512 字节的对象叫做小对象。 实际上,缓冲区就是主存中的一片区域,把这片区域划分为多个块,每块就是一个 Slab, 每个 Slab 由一个或多个页面组成,每个 Slab 中存放的就是对象。 因为 Slab 分配模式的实现比较复杂,我们不准备对其进行详细的分析,只对主要内容 给予描述。 1.Slab 的数据结构 Slab 分配模式有两个主要的数据结构,一个是描述缓冲区的结构 kmem_cache_t,一个 是描述 Slab 的结构 kmem_slab_t,下面对这两个结构给予简要讨论。 (1)Slab Slab 是 Slab 管理模式中最基本的结构。它由一组连续的物理页面组成,对象就被顺序 放在这些页面中。其数据结构在 mm/slab.c 中定义如下: /* * slab_t * * Manages the objs in a slab. Placed either at the beginning of mem allocated * for a slab, or allocated from an general cache. * Slabs are chained into three list: fully used, partial, fully free slabs. */ typedef struct slab_s { struct list_head list; unsigned long colouroff; void *s_mem; /* including colour offset */ unsigned int inuse; /* num of objs active in slab */ kmem_bufctl_t free; } slab_t;这里的链表用来将前一个 Slab 和后一个 Slab 链接起来形成一个双向链表,colouroff 为该 Slab 上着色区的大小,指针 s_mem 指向对象区的起点,inuse 是 Slab 中所分配对象的 个数。最后,free 的值指明了空闲对象链中的第一个对象,kmem_bufctl_t 其实是一个整数。 Slab 结构的示意图如图 6.11 所示。 对于小对象,就把 Slab 的描述结构 slab_t 放在该 Slab 中;对于大对象,则把 Slab 结 构游离出来,集中存放。关于 Slab 中的着色区再给予具体描述。 每个 Slab 的首部都有一个小小的区域是不用的,称为“着色区(Coloring Area)”。 着色区的大小使 Slab 中的每个对象的起始地址都按高速缓存中的“缓存行(Cache Line)” 大小进行对齐(80386 的一级高速缓存行大小为 16 字节,Pentium 为 32 字节)。因为 Slab 是由 1 个页面或多个页面(最多为 32)组成,因此,每个 Slab 都是从一个页面边界开始的, 它自然按高速缓存的缓冲行对齐。但是,Slab 中的对象大小不确定,设置着色区的目的就是

将 Slab 中第一个对象的起始地址往后推到与缓冲行对齐的位置。因为一个缓冲区中有多个 Slab,因此,应该把每个缓冲区中的各个 Slab 着色区的大小尽量安排成不同的大小,这样可 以使得在不同的 Slab 中,处于同一相对位置的对象,让它们在高速缓存中的起始地址相互错 开,这样就可以改善高速缓存的存取效率。 每个 Slab 上最后一个对象以后也有个小小的废料区是不用的,这是对着色区大小的补 偿,其大小取决于着色区的大小,以及 Slab 与其每个对象的相对大小。但该区域与着色区的 总和对于同一种对象的各个 Slab 是个常数。 每个对象的大小基本上是所需数据结构的大小。只有当数据结构的大小不与高速缓存中 的缓冲行对齐时,才增加若干字节使其对齐。所以,一个 Slab 上的所有对象的起始地址都必 然是按高速缓存中的缓冲行对齐的。 (2)缓冲区 每个缓冲区管理着一个 Slab 链表,Slab 按序分为 3 组。第 1 组是全满的 Slab(没有空 闲的对象),第 2 组 Slab 中只有部分对象被分配,部分对象还空闲,最后一组 Slab 中的对象 全部空闲。只所以这样分组,是为了对 Slab 进行有效的管理。每个缓冲区还有一个轮转锁 (Spinlock),在对链表进行修改时用这个轮转锁进行同步。类型 kmem_cache_s 在 mm/slab.c 中定义如下: struct kmem_cache_s {/* 1) each alloc & free */ /* full, partial first, then free */ struct list_head slabs_full; struct list_head slabs_partial; struct list_head slabs_free; unsigned int objsize; unsigned int flags; /* constant flags */ unsigned int num; /* # of objs per slab */ spinlock_t spinlock;#ifdef CONFIG_SMP unsigned int batchcount;#endif/* 2) slab additions /removals */ /* order of pgs per slab (2^n) */ unsigned int gfporder; /* force GFP flags, e.g. GFP_DMA */ unsigned int gfpflags; size_t colour; /* cache colouring range */ unsigned int colour_off; /* colour offset */ unsigned int colour_next; /* cache colouring */ kmem_cache_t *slabp_cache; unsigned int growing; unsigned int dflags; /* dynamic flags */ /* constructor func */ void (*ctor)(void *, kmem_cache_t *, unsigned long); /* de-constructor func */ void (*dtor)(void *, kmem_cache_t *, unsigned long); unsigned long failures;/* 3) cache creation/removal */ char name[CACHE_NAMELEN]; struct list_head next;#ifdef CONFIG_SMP/* 4) per-cpu data */ cpucache_t *cpudata[NR_CPUS];#endif…..};然后定义了 kmem_cache_t,并给部分域赋予了初值: static kmem_cache_t cache_cache = { slabs_full: LIST_HEAD_INIT(cache_cache.slabs_full), slabs_partial: LIST_HEAD_INIT(cache_cache.slabs_partial), slabs_free: LIST_HEAD_INIT(cache_cache.slabs_free),objsize: sizeof(kmem_cache_t), flags: SLAB_NO_REAP, spinlock: SPIN_LOCK_UNLOCKED, colour_off: L1_CACHE_BYTES, name: "kmem_cache",};对该结构说明如下。 该结构中有 3 个队列 slabs_full、slabs_partial 以及 slabs_free,分别指向满 Slab、 半满 Slab 和空闲 Slab,另一个队列 next 则把所有的专用缓冲区链成一个链表。 除了这些队列和指针外,该结构中还有一些重要的域:objsize 是原始的数据结构的大 小,这里初始化为 kmem_cache_t 的大小;num 表示每个 Slab 上有几个缓冲区;gfporder 则 表示每个 Slab 大小的对数,即每个 Slab 由 2 gfporder个页面构成。 如前所述,着色区的使用是为了使同一缓冲区中不同 Slab 上的对象区的起始地址相互 错开,这样有利于改善高速缓存的效率。colour_off 表示颜色的偏移量,colour 表示颜色的 数量;一个缓冲区中颜色的数量取决于 Slab 中对象的个数、剩余空间以及高速缓存行的大小。 所以,对每个缓冲区都要计算它的颜色数量,这个数量就保存在 colour 中,而下一个 Slab 将要使用的颜色则保存在 colour_next 中。当 colour_next 达到最大值时,就又从 0 开始。 着色区的大小可以根据(colour_off×colour)算得。例如,如果 colour 为 5,colour_off 为 8,则第一个 Slab 的颜色将为 0,Slab 中第一个对象区的起始地址(相对)为 0,下一个 Slab 中第一个对象区的起始地址为 8,再下一个为 16,24,32,0……等。 cache_cache 变量实际上就是缓冲区结构的头指针。 由此可以看出,缓冲区结构 kmem_cache_t 相当于 Slab 的总控结构,缓冲区结构与 Slab 结构之间的关系如图 6.12 所示。 在图 6.12 中,深灰色表示全满的 Slab,浅灰色表示含有空闲对象的 Slab,而无色表示 空的 Slab。缓冲区结构之间形成一个单向链表,Slab 结构之间形成一个双向链表。另外,缓 冲区结构还有分别指向满、半满、空闲 Slab 结构的指针。 2.专用缓冲区的建立和撤销 专用缓冲区是通过 kmem_cache_create()函数建立的,函数原型为: kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset, unsigned long c_flags, void (*ctor) (void *objp, kmem_cache_t *cachep, unsigned long flags), void (*dtor) (void *objp, kmem_cache_t *cachep, unsigned long flags)) 对其参数说明如下。 (1)name: 缓冲区名 ( 19 个字符)。 (2)size: 对象大小。 (3)offset :所请求的着色偏移量。 (4)c_flags :对缓冲区的设置标志。 • SLAB_HWCACHE_ALIGN:表示与第一个高速缓存中的缓冲行边界(16 或 32 字节)对齐。 • SLAB_NO_REAP:不允许系统回收内存。 • SLAB_CACHE_DMA:表示 Slab 使用的是 DMA 内存。 (5)ctor :构造函数(一般都为 NULL)。 (6)dtor :析构函数(一般都为 NULL)。 (7)objp :指向对象的指针。 (8)cachep :指向缓冲区。 对专用缓冲区的创建过程简述如下。 kmem_cache_create()函数要进行一系列的计算,以确定最佳的 Slab 构成。包括:每 个 Slab 由几个页面组成,划分为多少个对象;Slab 的描述结构 slab_t 应该放在 Slab 的外 面还是放在 Slab 的尾部;还有“颜色”的数量等等。并根据调用参数和计算结果设置 kmem_cache_t 结构中的各个域,包括两个函数指针 ctor 和 dtor。最后,将 kmem_cache_t 结构插入到 cache_cache 的 next 队列中。 但请注意,函数 kmem_cache_create()所创建的缓冲区中还没有包含任何 Slab,因此, 也没有空闲的对象。只有以下两个条件都为真时,才给缓冲区分配 Slab: (1)已发出一个分配新对象的请求; (2)缓冲区不包含任何空闲对象。 当这两个条件都成立时,Slab 分配模式就调用 kmem_cache_grow()函数给缓冲区分配 一个新的 Slab。其中,该函数调用 kmem_gatepages()从伙伴系统获得一组页面;然后又调用 kmem_cache_slabgmt()获得一个新的 Slab结构;还要调用 kmem_cache_init_objs()为新 Slab 中的所有对象申请构造方法(如果定义的话);最后,调用 kmem_slab_link_end()把这个 Slab 结构插入到缓冲区中 Slab 链表的末尾。 Slab 分配模式的最大好处就是给频繁使用的数据结构建立专用缓冲区。但到目前的版本 为止,Linux 内核中多数专用缓冲区的建立都用 NULL 作为构造函数的指针,例如,为虚存区 间结构 vm_area_struct 建立的专用缓冲区 vm_area_cachep: vm_area_cachep = kmem_cache_create("vm_area_struct", sizeof(struct vm_area_struct), 0, SLAB_HWCACHE_ALIGN, NULL, NULL); 就把构造和析构函数的指针置为 NULL,也就是说,内核并没有充分利用 Slab 管理机制 所提供的好处。为了说明如何利用专用缓冲区,我们从内核代码中选取一个构造函数不为空 的简单例子,这个例子与网络子系统有关,在 net/core/buff.c 中定义: void __init skb_init(void){ int i; skbuff_head_cache = kmem_cache_create("skbuff_head_cache", sizeof(struct sk_buff), 0, SLAB_HWCACHE_ALIGN, skb_headerinit, NULL); if (!skbuff_head_cache) panic("cannot create skbuff cache"); for (i=0; i从代码中可以看出,skb_init()调用 kmem_cache_create()为网络子系统建立一个 sk_buff 数据结构的专用缓冲区,其名称为“skbuff_head_cache”( 你可以通过读取/ proc/slabinfo/文件得到所有缓冲区的名字)。调用参数 offset 为 0,表示第一个对象在 Slab 中的位移并无特殊要求。但是参数 flags 为 SLAB_HWCACHE_ALIGN,表示 Slab 中的对象要与 高速缓存中的缓冲行边界对齐。对象的构造函数为 skb_headerinit(),而析构函数为空, 也就是说,在释放一个 Slab 时无需对各个缓冲区进行特殊的处理。 当从内核卸载一个模块时,同时应当撤销为这个模块中的数据结构所建立的缓冲区,这 是通过调用 kmem_cache_destroy()函数来完成的。从 Linux 2.4.16 内核代码中进行查找可 知,对这个函数的调用非常少。 3.通用缓冲区 在内核中初始化开销不大的数据结构可以合用一个通用的缓冲区。通用缓冲区非常类似 于物理页面分配中的大小分区,最小的为 32,然后依次为 64、128、……直至 128KB(即 32 个页面),但是,对通用缓冲区的管理又采用的是 Slab 方式。从通用缓冲区中分配和释放缓 冲区的函数为: void *kmalloc(size_t size, int flags); Void kree(const void *objp); 因此,当一个数据结构的使用根本不频繁时,或其大小不足一个页面时,就没有必要给 其分配专用缓冲区,而应该调用 kmallo()进行分配。如果数据结构的大小接近一个页面,则 干脆通过 alloc_page()为之分配一个页面。 事实上,在内核中,尤其是驱动程序中,有大量的数据结构仅仅是一次性使用,而且所 占内存只有几十个字节,因此,一般情况下调用 kmallo()给内核数据结构分配内存就足够了。 另外,因为,在 Linux 2.0 以前的版本一般都调用 kmallo()给内核数据结构分配内存,因此, 调用该函数的一个优点是(让你开发的驱动程序)能保持向后兼容。 6.3.4 内核空间非连续内存区的管理 我们说,任何时候,CPU 访问的都是虚拟内存,那么,在你编写驱动程序,或者编写模 块时,Linux 给你分配什么样的内存?它处于 4GB 空间的什么位置?这就是我们要讨论的非 连续内存。 首先,非连续内存处于 3GB 到 4GB 之间,也就是处于内核空间,如图 6.13 所示。 图 6.13 中,PAGE_OFFSET 为 3GB,high_memory 为保存物理地址最高值的变量, VMALLOC_START 为非连续区的的起始地址,定义于 include/i386/pgtable.h 中: #define VMALLOC_OFFSET (8*1024*1024) #define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & \~ (VMALLOC_OFFSET-1)) 在物理地址的末尾与第一个内存区之间插入了一个 8MB(VMALLOC_OFFSET)的区间,这 是一个安全区,目的是为了“捕获”对非连续区的非法访问。出于同样的理由,在其他非连 续的内存区之间也插入了 4KB 大小的安全区。每个非连续内存区的大小都是 4096 的倍数。 1.非连续区的数据结构 描述非连续区的数据结构为 struct vm_struct,定义于 include/linux/vmalloc.h 中: struct vm_struct { unsigned long flags; void * addr; unsigned long size; struct vm_struct * next;};struct vm_struct * vmlist; 非连续区组成一个单链表,链表第一个元素的地址存放在变量 vmlist 中。Addr 域是内 存区的起始地址;size 是内存区的大小加 4096(安全区的大小)。 2.创建一个非连续区的结构 函数 get_vm_area()创建一个新的非连续区结构,其代码在 mm/vmalloc.c 中: struct vm_struct * get_vm_area(unsigned long size, unsigned long flags) { unsigned long addr; struct vm_struct **p, *tmp, *area; area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL); if (!area) return NULL; size += PAGE_SIZE; addr = VMALLOC_START; write_lock(&vmlist_lock); for (p = &vmlist; (tmp = *p) ; p = &tmp->next) { if ((size + addr)< addr) goto out; if (size + addr<= (unsigned long) tmp->addr) break; addr = tmp->size + (unsigned long) tmp->addr; if (addr >VMALLOC_END-size) goto out; } area->flags = flags; area->addr = (void *)addr; area->size = size; area->next = *p; *p = area; write_unlock(&vmlist_lock); return area;out: write_unlock(&vmlist_lock); kfree(area); return NULL; }这个函数比较简单,就是在单链表中插入一个元素。其中调用了 kmalloc()和 kfree() 函数,分别用来为 vm_struct 结构分配内存和释放所分配的内存。 3.分配非连续内存区 vmalloc()函数给内核分配一个非连续的内存区,在/include/linux/vmalloc.h 中定 义如下: static inline void * vmalloc (unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); }vmalloc()最终调用的是__vmalloc()函数,该函数的代码在 mm/vmalloc.c 中: void * __vmalloc (unsigned long size, int gfp_mask, pgprot_t prot) { void * addr; struct vm_struct *area; size = PAGE_ALIGN(size); if (!size || (size >>PAGE_SHIFT) >num_physpages) { BUG(); return NULL; } area = get_vm_area(size, VM_ALLOC); if (!area) return NULL; addr = area->addr; if (vmalloc_area_pages(VMALLOC_VMADDR(addr), size, gfp_mask, prot)) { vfree(addr); return NULL; } return addr; }函数首先把 size 参数取整为页面大小(4096)的一个倍数,也就是按页的大小进行对 齐,然后进行有效性检查,如果有大小合适的可用内存,就调用 get_vm_area()获得一个 内存区的结构。但真正的内存区还没有获得,函数 vmalloc_area_pages()真正进行非连续 内存区的分配: inline int vmalloc_area_pages (unsigned long address, unsigned long size, int gfp_mask, pgprot_t prot) { pgd_t * dir; unsigned long end = address + size; int ret; dir = pgd_offset_k(address); spin_lock(&init_mm.page_table_lock); do { pmd_t *pmd; pmd = pmd_alloc(&init_mm, dir, address); ret = -ENOMEM; if (!pmd) break; ret = -ENOMEM; if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) break; address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; ret = 0; } while (address && (address< end));spin_unlock(&init_mm.page_table_lock); return ret; }该函数有两个主要的参数,address 表示内存区的起始地址,size 表示内存区的大小。 内存区的末尾地址赋给了局部变量 end。其中还调用了几个主要的函数或宏。 (1)pgd_offset_k()宏导出这个内存区起始地址在页目录中的目录项。 (2)pmd_alloc()为新的内存区创建一个中间页目录。 (3)alloc_area_pmd()为新的中间页目录分配所有相关的页表,并更新页的总目录; 该函数调用 pte_alloc_kernel()函数来分配一个新的页表,之后再调用 alloc_area_pte() 为页表项分配具体的物理页面。 (4)从 vmalloc_area_pages()函数可以看出,该函数实际建立起了非连续内存区到物 理页面的映射。 4.kmalloc()与 vmalloc()的区别 kmalloc()与 vmalloc() 都是在内核代码中提供给其他子系统用来分配内存的函数,但 二者有何区别? 从前面的介绍已经看出,这两个函数所分配的内存都处于内核空间,即从 3GB~4GB;但位 置不同,kmalloc()分配的内存处于 3GB~high_memory 之间,而 vmalloc()分配的内存在 VMALLOC_START~4GB 之间,也就是非连续内存区。一般情况下在驱动程序中都是调用 kmalloc() 来给数据结构分配内存,而 vmalloc()用在为活动的交换区分配数据结构,为某些 I/O 驱动程 序分配缓冲区,或为模块分配空间,例如在 include/asm-i386/module.h 中定义了如下语句: #define module_map(x) vmalloc(x) 其含义就是把模块映射到非连续的内存区。 与 kmalloc()和 vmalloc()相对应,两个释放内存的函数为 kfree()和 vfree()。

用户评论

顶个蘑菇闯天下i

这篇文章写得真好!我一直在想怎么理解 Linux 内存管理,尤其是虚拟内存机制,你的解释非常透彻,一下子就让我明白了。希望以后还有更多关于 Linux 内核源码的解析

    有10位网友表示赞同!

断秋风

内核源代码真的太难懂了,感觉自己需要花很多时间才能啃下来。不过这篇文章分析得很细致,把重点都讲清楚了,我开始有了一点信心!

    有12位网友表示赞同!

oО清风挽发oО

内存管理这个部分确实比较抽象,看了你的解释之后感觉好多了。最想了解更多的是Slab 机制和页面分配策略的具体实现方式,有机会再做深入讲解吗?

    有10位网友表示赞同!

雪花ミ飞舞

虚拟内存简直是大神级别的设计!你把这篇文章写得通俗易懂,我仿佛就明白了这个机制的奥秘! kudos!

    有5位网友表示赞同!

陌颜幽梦

说实话,我是个新手,对于内核源码还是一知半解。 虽然这篇分析文章内容很专业,但我觉得有些地方还是过于深入导致难度比较大,希望以后可以放慢节奏讲通俗易懂些

    有7位网友表示赞同!

将妓就计

我读了不少关于 Linux 内存管理的文档,但这篇文章写的角度不同,更注重源码级别的细节分析,很有启发!

    有16位网友表示赞同!

江山策

文章里提到了很多内核函数和结构体,有没有推荐一些学习这些内容的资源呢? 比如说书籍或者教程?

    有11位网友表示赞同!

珠穆郎马疯@

感觉这篇文章对 Linux 内存管理做了比较全面的梳理,但是没有多少代码示例,看个例子更容易理解啊!希望以后可以结合具体的代码片段讲解!

    有12位网友表示赞同!

留我一人

最近在研究内存泄漏的问题,这篇分析的文章正好让我了解了内核内存管理机制的细节,对解决实际问题很有帮助!

    有14位网友表示赞同!

回忆未来

Linux 内核源代码确实很复杂,很多实现细节都让人难以理解。希望作者能继续写一些深入分析的文章,帮助我们更全面地认识 Linux 内核!

    有13位网友表示赞同!

有一种中毒叫上瘾成咆哮i

这篇文章写的很好,非常清晰易懂,我很喜欢这种把理论知识和实际应用结合的讲解方式!

    有5位网友表示赞同!

花开丶若相惜

我有个疑问,关于内存页回收机制,您能详细解释一下吗?

    有18位网友表示赞同!

月下独酌

虚拟内存这个概念真的太棒了!它将实物理内存的使用范围拓展到了一个新的维度,作者分析这个概念得非常好。

    有18位网友表示赞同!

男神大妈

这篇文章内容很全面,但感觉对新手来说难度还是有点大,对于一些复杂的概念解释不够到位,建议可以加上一些入门级别的讲解。

    有8位网友表示赞同!

娇眉恨

学习 Linux 内存管理需要不断地实践和探索,看了这篇分析文章之后,我对这个系统有了更深入的理解!

    有18位网友表示赞同!

弃我者亡

作为一名开发人员,对内核源码的理解非常重要,这篇文章帮我明白了很多关于内存管理的关键点!

    有11位网友表示赞同!

麝香味

内存碎片化是如何处理的呢? 有没有什么方法可以避免内存碎片化问题加剧?

    有11位网友表示赞同!

热点资讯