linux内核--内核内存管理

时间:2024-01-14 11:26:38

如题目所示,为什么要称作“内核内存管理”,因为内核所需要的内存和用户态所需要的内存,这两者在管理上是不一样的。

这篇文章描述内核的内存管理,用户态的内存管理在以后的文章中讲述。

首先简单的说明一下下面的描述所需要的基础知识:

1,以下描述适用于32位系统

2,32位系统的线性地址(或称为逻辑地址,下面统称为线性地址)0-4G,其中的3G-4G的地址空间由内核使用。宏PAGE_OFFSET 为0xC0000000(3G),也是内核空间和用户空间的分界。但是linux内核并没有把整个1G空间用于线性映射,而只能映射最多896M物理内存,预留了最高端的128M虚拟地址空间给IO设备和其他用途。所以,当系统物理内存较大时,超过896M的内存区域,内核就无法直接通过线性映射直接访问了。这部分内存被称作high memory。

3,high memory和low memory一样,都是参与内核的物理内存分配,都可以被映射到内核地址空间,也都可以被映射到用户地址空间。

4,物理内存<896M时,没有high memory,因为所有的内存都被kernel直接映射了。

5,64位系统下不会有high memory,因为64位虚拟地址空间非常大(分给kernel的也很大),完全能够直接映射全部物理内存。

6,内存的管理是页框为单位的,典型的X86下,不开启PAE时,页框的大小为4K

7,linux中内存的管理是分区(Zone)的,在X86体系结构中,分为ZONE_DMA(低于16M的页框),ZONE_NORMAL(16-896M页框),ZONE_HIGHMEM(大于等于896M页框)。

下面附录一张PAGE_OFFSET后的线性地址区间表,以供参考。

linux内核--内核内存管理

下面步入正题:

一,大尺度内存管理--伙伴系统算法

我们这篇文章描述的内核的内存策略,内核的首要出发点就是“性能”,因此内核申请内存时,最好是使用物理上连续的页框,原因有二:

1,在某些情况下,连续的页框确实是必要的,典型的例子是DMA,因为DMA忽略分页策略。

2,如果频繁的修改页表会导致频繁的刷新TLB,而连续页框分配可以减轻这点。

算法描述

把所有的空闲页框分为11个组,每组中的节点都以双向循环链表的形式链接起来,这11个组中的每一个节点的页框数分别为:1,2,4,8,16,32,64,128,256,512,1024。每个节点的页框都是连续的。

需要需要请求256个页框的节点(即1MB),算法先在256个页框的链表中检查是否有一个空闲的块,如果没有的话,算法会查找一个更大的节点,如果在512个页框的链表中找到了一个这样的节点,那么就把这个512个页框的节点分成两半,一半满足需求,另一半插入到256个页框的链表中。如果找到1024个页框的链表中也没满足需求,那么返回失败。

算法的逆过程就是释放的过程,算法会试图将大小为b的一对空闲节点(伙伴)合并成一个大小为2b的节点,这也是“伙伴”算法命名的由来。

算法使用的数据结构:

每个ZONE的结构体中,包含了一个struct free_area[] free_area的一个数组,这个数组有11个元素,第K个元素标识大小为2K个页框的链表,free_area中有个free_list字段是双向循环链表的头,free_area中有个nr_free字段代表链表中节点的个数。指向链表中相邻元素的指针放在页描述符的lru字段中。

每CPU页框高速缓存

内核经常请求和释放单个页框,为了提升系统性能,每个ZONE定义了一个“每CPU”页框高速缓存。

即struct per_cpu_pageset[] pageset数组,数组的大小为CPU的个数。数组中的每个元素由两个per_cpu_pages组成,一个留给热高速缓存,一个留给冷高速缓存。

struct per_cpu_pages { int count; int low; int high; int batch; struct list_head list}

内核通过high和low两个标识来管理缓存的大小,当页框的个数小于low,内核通过伙伴系统分配batch个页框来补充,释放的过程类似。

伙伴系统的算法其实还很复杂,例如当空闲页框较少时需要按一定的规则回收页框等,调用伙伴系统的路径是否在中断处理程序中也会有不同的对待,这些细节就不一一列举。

二,SLAB分配器

伙伴系统算法采用页框作为基本的内在区,这适合于大块的内存请求,那我们怎么处理对小内存区的请求呢,比如说几十或几百个字节?这就是我们下面要说的算法。

linux内核--内核内存管理

slab分配算法基本就包含在上图中了,首先声明几个概念:

对象:就是上面所说到的小的内存区,例如进程描述符,它所占用的内存区可以称为对象。

slab:可以理解为通过伙伴算法所申请的一个或几个连续的页框。它可以容纳N个对象。

高速缓存:每个高速缓存包含了相同类型的对象,高速缓存相当于一个对象的容器。高速缓存直接容纳的是slab,slab又容纳了对象。

普通或专用高速缓存:专用的高速缓存可以理解为专门存储一类对象,例如进程描述符,的高速缓存。而对于普通高速缓存,例如,某个高速缓存里面的对象都是100字节大小,那么这个普通高速缓存就可以存储所以小于100字节大小的对象。在系统初始化时,就创建了26(分为13对)个普通高速缓存,它们的大小呈现几何分布,分别为32,64,128,256... 131072。对于每一对,一个适用于ISA DMA分配,另一个用于常规分配。

1,算法简介

如上图所示,每一类高速缓存结构体中有三个字段,每个字段就是一个双向链表的头,分别是全满的slab,部分满的slab,空的slab。分别对应图中的三个圆角矩形。每一个双向链表把slab串起来,因为slab可以动态的增加或减少。

2,给高速缓存分配和释放slab

如果高速缓存为空,就调用伙伴系统分配一个slab,然后将构造方法(如果定义了的话)应用到新的slab所包含的所有的对象上。

每个高速缓存结构体都有两个字段void * ctor,dtor;分别表示这类对象的构造和析构方法。

将这个新的slab放到对应的双向链表中,并更新相关计数器。

释放的过程类似,使用析构方法释放所有的对象,并释放页框给伙伴系统。

细节(不在算法范围之内):给定一个页框,内核必须确定它是否被 slab分配器使用,并得到相应高速缓存和slab描述符的地址,这是通过页描述符的lru字段的next和pre子字段来实现,因为它分别存储了调整缓存描述符和slab描述符。反之,通过slab分配器也可以获取它占用的页框,这是通过高速缓存描述符的gforder字段(slab大小)和slab描述符的s_mem字段来实现。

3,本地高速缓存

为了减少多个CPU竞争slab分配器,每个高速缓存描述符都有一个array字段,它是一个数组,数组的大小是CPU的个数。它非常类似于上面所说的“每CPU页框高速缓存”。