1 内存中不连续的页的分配
根据上文的讲述, 我们知道物理上连续的映射对内核是最好的, 但并不总能成功地使用. 在分配一大块内存时, 可能竭尽全力也无法找到连续的内存块.
在用户空间中这不是问题,因为普通进程设计为使用处理器的分页机制, 当然这会降低速度并占用TLB.
在内核中也可以使用同样的技术. 内核分配了其内核虚拟地址空间的一部分, 用于建立连续映射.
在IA-32系统中, 前16M划分给DMA区域, 后面一直到第896M作为NORMAL直接映射区, 紧随直接映射的前896MB物理内存,在插入的8MB安全隙之后, 是一个用于管理不连续内存的区域. 这一段具有线性地址空间的所有性质. 分配到其中的页可能位于物理内存中的任何地方. 通过修改负责该区域的内核页表, 即可做到这一点.
Persistent mappings和Fixmaps地址空间都比较小, 这里我们忽略它们, 这样只剩下直接地址映射和VMALLOC区, 这个划分应该是平衡两个需求的结果
尽量增加DMA和Normal区大小,也就是直接映射地址空间大小,当前主流平台的内存,基本上都超过了512MB,很多都是标配1GB内存,因此注定有一部分内存无法进行线性映射。
- 保留一定数量的VMALLOC大小,这个值是应用平台特定的,如果应用平台某个驱动需要用vmalloc分配很大的地址空间,那么最好通过在kernel参数中指定vmalloc大小的方法,预留较多的vmalloc地址空间。
并不是Highmem没有或者越少越好,这个是我的个人理解,理由如下:高端内存就像个垃圾桶和缓冲区,防止来自用户空间或者vmalloc的映射破坏Normal zone和DMA zone的连续性,使得它们碎片化。当这个垃圾桶较大时,那么污染Normal 和DMA的机会自然就小了。
通过这种方式, 将内核的内核虚拟地址空间划分为几个不同的区域
下面的图是VMALLOC地址空间内部划分情况
2 用vmalloc分配内存
vmalloc是一个接口函数, 内核代码使用它来分配在虚拟内存中连续但在物理内存中不一定连续的内存
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L70
void *vmalloc(unsigned long size);
该函数只需要一个参数, 用于指定所需内存区的长度, 与此前讨论的函数不同, 其长度单位不是页而是字节, 这在用户空间程序设计中是很普遍的.
使用vmalloc的最著名的实例是内核对模块的实现. 因为模块可能在任何时候加载, 如果模块数据比较多, 那么无法保证有足够的连续内存可用, 特别是在系统已经运行了比较长时间的情况下.
如果能够用小块内存拼接出足够的内存, 那么使用vmalloc可以规避该问题
内核中还有大约400处地方调用了vmalloc, 特别是在设备和声音驱动程序中.
因为用于vmalloc的内存页总是必须映射在内核地址空间中, 因此使用ZONE_HIGHMEM
内存域的页要优于其他内存域. 这使得内核可以节省更宝贵的较低端内存域, 而又不会带来额外的坏处. 因此, vmalloc
等映射函数是内核出于自身的目的(并非因为用户空间应用程序)使用高端内存页的少数情形之一.
所有有关vmalloc的数据结构和API结构声明在include/linux/vmalloc.h
声明头文件 | NON-MMU实现 | MMU实现 |
---|---|---|
include/linux/vmalloc.h | mm/nommu.c | mm/vmalloc.c |
2.1 数据结构
内核在管理虚拟内存中的vmalloc
区域时, 内核必须跟踪哪些子区域被使用、哪些是空闲的. 为此定义了一个数据结构vm_struct
, 将所有使用的部分保存在一个链表中. 该结构提的定义在include/linux/vmalloc.h?v=4.7, line 32
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L32
struct vm_struct {
struct vm_struct *next;
void *addr;
unsigned long size;
unsigned long flags;
struct page **pages;
unsigned int nr_pages;
phys_addr_t phys_addr;
const void *caller;
};
注意, 内核使用了一个重要的数据结构称之为vm_area_struct, 以管理用户空间进程的虚拟地址空间内容. 尽管名称和目的都是类似的, 虽然二者都是做虚拟地址空间映射的, 但不能混淆这两个结构。
- 前者是内核虚拟地址空间映射,而后者则是应用进程虚拟地址空间映射。
- 前者不会产生page fault,而后者一般不会提前分配页面,只有当访问的时候,产生page fault来分配页面。
对于每个用vmalloc分配的子区域, 都对应于内核内存中的一个该结构实例. 该结构各个成员的语义如下
字段 | 描述 |
---|---|
next | 使得内核可以将vmalloc区域中的所有子区域保存在一个单链表上 |
addr | 定义了分配的子区域在虚拟地址空间中的起始地址。size表示该子区域的长度. 可以根据该信息来勾画出vmalloc区域的完整分配方案 |
flags | 存储了与该内存区关联的标志集合, 这几乎是不可避免的. 它只用于指定内存区类型 |
pages | 是一个指针,指向page指针的数组。每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 |
nr_pages | 指定pages中数组项的数目,即涉及的内存页数目 |
phys_addr | 仅当用ioremap映射了由物理地址描述的物理内存区域时才需要。该信息保存在phys_addr中 |
caller |
其中flags只用于指定内存区类型, 所有可能的flag标识以宏的形式定义在include/linux/vmalloc.h?v=4.7, line 14
// http://lxr.free-electrons.com/source/include/linux/vmalloc.h?v=4.7#L14
/* bits in flags of vmalloc's vm_struct below */
#define VM_IOREMAP 0x00000001 /* ioremap() and friends */
#define VM_ALLOC 0x00000002 /* vmalloc() */
#define VM_MAP 0x00000004 /* vmap()ed pages */
#define VM_USERMAP 0x00000008 /* suitable for remap_vmalloc_range */
#define VM_UNINITIALIZED 0x00000020 /* vm_struct is not fully initialized */
#define VM_NO_GUARD 0x00000040 /* don't add guard page */
#define VM_KASAN 0x00000080 /* has allocated kasan shadow memory */
/* bits