Contents
  1. 1. 非连续内存区的线性地址
  2. 2. 非连续内存区的描述符
  3. 3. 分配非连续内存区
  4. 4. 释放函数

摘要:把一块存放slab结构的内存区映射到一组连续的页框是最好的选择,这样会充分利用高速缓存并获得较低的平均访问时间。不过,上面的方式主要是针对那些使用非常频繁的内核数据结构——如task_struct、inode来设计的。如果对内存区的请求不是很频繁,那么,通过连续的线性地址,而不是物理地址来访问非连续的物理页框这样一种分配模式就会很有意义了。这种模式的主要优点是避免了外碎片,而缺点是必须打乱内核页表。此外,非连续内存区的大小必须是4096 的倍数。Linux 在几个方面使用非连续内存区:为活动的交换区分配数据结构,为模块分配空间,或者给某些I/O 驱动程序分配缓冲区等。此外,非连续内存区还提供了另一种使用高端内存页框的方法。

非连续内存区的线性地址

要查找线性地址的一个空闲区,我们可以从PAGE_OFFSET开始查找(通常为0xc0000000,即第4 个GB 的起始地址)。下图让我们回忆了如何使用第4个GB 的线性地址:

回忆一下:

(1)内存区的开始部分包含的是对前896MB RAM 进行映射的线性地址。直接映射的物理内存末尾所对应的线性地址保存在high_memory全局变量中。当物理内存小于896MB,则线性地址0xc0000000以后的896MB与其一一对应;当物理内存大于896MB而小于4GB时,只直接映射前896MB的地址到0xc0000000以后的线性空间,然后把线性空间的其他部分与896MB和4GB物理空间映射起来,称为动态重映射,这是本博的重点;当物理内存大于4GB,则需要考虑PAE的情况,其他的东东没什么区别,我们不做过多的回忆了。

(2)内核的页表由内核页全局目录变量swapper_pg_dir维护;pagetable_init()建立内核页表项。

(3)内存区的结尾部分包含的是固定映射的线性地址,主要用于存放一些常量线性地址,具体查看“高端内存映射”博文。

(4)从PKMAP_BASE 开始,我们查找用于高端内存页框的永久内核映射的线性地址,具体查看“高端内存映射 ”博文。

(5)其余的线性地址可以用于非连续内存区。在物理内存映射的末尾与第一个内存区之间插入一个大小为8MB(宏VMALLOC_OFFSET)的安全区,目的是为了“捕获”对内存的越界访问。出于同样的理由,插入其他4KB 大小的安全区来隔离非连续的内存区。

非连续内存区的描述符

每个非连续内存区都对应着一个类型为vm_struct 的描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct vm_struct {
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
unsigned int nr_pages;
unsigned long phys_addr;
struct vm_struct *next;
};

介绍下它的字段:

void * addr 内存区内第一个内存单元的线性地址(首址)

unsigned long size 内存区的大小加4096(内存区之间的安全区间的大小)

unsigned long flags 非连续内存区映射的内存的类型

struct page ** pages 指向nr_pages数组的指针,该数组由指向页描述符的指针组成

unsigned int nr_pages 内存区填充的页的个数

unsigned long phys_addr 该字段设为0,除非内存已被创建来映射一个硬件设备的I/O 共享内存

struct vm_struct * next 指向下一个vm_struct结构的指针

分配非连续内存区

vmalloc()函数给内核分配一个非连续内存区。参数size表示所请求内存区的大小。如果这个函数能够满足请求,就返回新内存区的起始地址;否则,返回一个NULL 指针(mm/ vmalloc.c)

其工作方式类似于kmalloc(),只不过vmalloc()分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是,这并不保证它们在物理RAM中也连续。kmalloc()函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。vmalloc()函数只确保页在虚拟地址空间内是连续的。vmalloc()仅在不得已时才会使用——一般是在为了获得大块内存时,例如,当模块被动态插入到内核中时,就把模块装载到由vmalloc()分配的内存上。

伙伴关系也好、slab技术也好,从内存管理理论角度而言目的基本是一致的,它们都是为了防止“分片”,不过分片又分为外部分片和内部分片之说,所谓内部分片是说系统为了满足一小段内存区(连续)的需要,不得不分配了一大区域连续内存给它,从而造成了空间浪费;外部分片是指系统虽有足够的内存,但却是分散的碎片,无法满足对大块“连续内存”的需求。无论何种分片都是系统有效利用内存的障碍。slab分配器使得一个页面内包含的众多小块内存可独立被分配使用,避免了内部分片,节约了空闲内存。伙伴关系把内存块按大小分组管理,一定程度上减轻了外部分片的危害,因为页框分配不在盲目,而是按照大小依次有序进行,不过伙伴关系只是减轻了外部分片,但并未彻底消除。你自己比划一下多次分配页面后,空闲内存的剩余情况吧。

所以避免外部分片的最终思路还是落到了如何利用不连续的内存块组合成“看起来很大的内存块”——这里的情况很类似于用户空间分配虚拟内存,内存逻辑上连续,其实映射到并不一定连续的物理内存上。Linux内核借用了这个技术,允许内核程序在内核地址空间中分配虚拟地址,同样也利用页表(内核页表)将虚拟地址映射到分散的内存页上。以此完美地解决了内核内存使用中的外部分片问题。内核提供vmalloc函数分配内核虚拟内存,该函数不同于kmalloc,它可以分配较Kmalloc大得多的内存空间(可远大于128K,但必须是页大小的倍数),但相比Kmalloc来说,Vmalloc需要对内核虚拟地址进行重映射,必须更新内核页表,因此分配效率上要低一些(用空间换时间)。

释放函数

vfree()函数释放vmalloc()或vmalloc_32()创建的非连续内存区,而vunmap()函数释放vmap()创建的内存区。两个函数都使用同一个参数 —— 将要释放的内存区的起始线性地址address;它们都依赖于__vunmap()函数来做实质性的工作。

__vunmap()函数接收两个参数:将要释放的内存区的起始地址的地址addr,以及标志deallocate_pages,如果被映射到内存区内的页框应当被释放到分区页框分配器(调用vfree())中,那么这个标志被置位,否则被清除(vunmap()被调用)。该函数执行以下操作:

  1. 调用remove_vm_area()函数得到vm_struct 描述符的地址area,并清除非连续内存区中的线性地址对应的内核的页表项。

  2. 如果deallocate_pages 被置位,函数扫描指向页描述符的area->pages指针数组;对于数组的每一个元素,调用__free_page()函数释放页框到分区页框分配器。此外,执行kfree(area->pages)来释放数组本身。

  3. 调用kfree(area)来释放vm_struct 描述符。



本文章参考自《linux内核设计与实现》、slab分配器非连续内存区

Contents
  1. 1. 非连续内存区的线性地址
  2. 2. 非连续内存区的描述符
  3. 3. 分配非连续内存区
  4. 4. 释放函数