虚拟内存的管理
32位处理器的寻址能力为2^32B,即4GB大小的地址空间,这部分空间称为虚拟地址空间。使用MMU作虚拟地址空间到物理地址空间的映射。操作系统必须建立页表,来支持MMU执行此操作。
linux内核将4GB的虚拟地址空间分为两大块:
- 顶部的1GB空间给内核使用,称为内核空间
- 底部的3GB空间给用户空间使用,称为用户空间。
(PAGE_OFFSET宏即为虚拟地址空间中内核部分的起始地址)
内核虚拟地址空间的构成
全局变量high_memory是内核空间中的一个分界点,high_memory以下,为物理内存直接映射区域
以上,有一段空洞,不做映射,如果访问到这块区域,会产生缺页异常,以做内存访问的保护。
再往上,VMALLOC_START是vmalloc区的开始,直到VMALLOC_END,用于虚拟内存的分配。
在最后的特殊映射区之前,还有一段空洞,目的也是一样的。
vmalloc和vfree
vmalloc函数也是内核模块会使用到的一个内存分配函数,他的特点是,分配的虚拟地址空间是连续的,但是物理地址空间可能是不连续的。vmalloc主要就是对上面图中的vmalloc区操作,返回的地址也就来自于这个区域。
驱动程序,不鼓励使用vmalloc:
- vmalloc的实现机制,决定了它的使用效率没有kmalloc这样的函数高。
- 一些体系结构上,例如x86,因为物理内存通常都比较大,这使得vmalloc区域相对变得很小,vmalloc的调用失败的几率也就变得特别大。
- vmalloc分配处的地址空间在物理上并不能保证是连续的,对于一些对物理地址空间连续性要求高的场景,如DMA,就不是那么友好。
在一些获得连续物理内存空间的可能性不是很大的情况下,可以使用vmalloc来用不连续的物理页面在虚拟地址空间上组装出一块连续的内存区域。内核模块就使用了这个接口,为模块的ELF文件数据分配空间。因为模块可以随时被加载到系统中,如果系统运行了很长的一段时间,而模块的ELF又比较大,使用kmalloc很容易拿不到连续的物理内存页面。所以使用了vmalloc来为模块分配空间。
vmalloc
void *vmalloc(unsigned long size);
vmalloc函数实现的原理大致分为三个步骤:
- vmalloc区域分配一段连续的虚拟内存区域
- 通过buddy组件分配物理页
- 通过在页表中建立页表项,将1中分配的虚拟内存映射到2中获取的物理页上
实际代码实现中,步骤一采用了红黑树来解决vmalloc区动态虚拟内存块的分配与释放,每一个分配出的虚拟内存块,都用vm_struct来描述:
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;
void *caller;
};
next成员用于将所有已分配的vm_struct对象构成链表。链表头为全局变量struct vm_struct *vmlist
addr是对应虚拟内存块的起始地址,页对齐。
size为虚拟内存块的大小,是页面大小的整数倍。
flags是表示当前虚拟内存块的映射特性的标志。
- VM_ALLOC标志:当前虚拟内存块是给vmalloc函数使用的,映射的是实际的物理内存RAM
- VM_IOREMAP标志:当前虚拟内存块是给ioremap相关函数使用的,映射的是I/O空间地址,也就是设备内存。
pages成员是被映射的物理内存页面所形成的数组首地址。
nr_pages表示物理页的数量
phys_addr多在ioremap函数中使用,表示映射的I/O空间起始地址,页对齐。
此过程的第一步,即 vmalloc区域分配一段连续的虚拟内存区域,通常会将传入vmalloc的size做一次页对齐:
size = PAGE_ALIGN(size);
然后会追加一个页面的大小:
size += PAGE_SIZE;
这个操作也是和上面空洞的存在原因是一致的,防止越界访问。这个追加的页面对应的物理内存页面是空的,不会让buddy组件去往上面提交实际物理页面。
此过程的第二步,内核通过buddy组件分配物理页时,使用GFP_KERNEL | __GFP_HIGHMEM.
GFP_KERNEL意味着vmalloc可能在执行时睡眠,所以不能用在中断上下文。
__GFP_HIGHMEM告诉伙伴系统,在ZONE_HIGHMEM中查找空闲页。(ZONE_NORMAL十分宝贵,留给kmalloc)
这里vmalloc分配物理页面时,使用的是alloc_pages_node函数,也即总是分配单个页面的形式,最后MMU页表分别对它们作映射,到一个集中连续的虚拟地址空间上。
此过程的第三步,建立映射,没有什么好说的,细节是最后一个追加的页面,不要做映射。
vmalloc最后形成的结果:
ioremap
void __iomem *ioremap(unsigned long phys_addr, size_t size);
# define __iomem __attribute__((noderef, address_space(2)))
__iomem是编译器属性,提醒调用者返回的是IO类型的地址。
ioremap及其变种函数,用来将vmalloc区的某段虚拟地址内存块映射到IO空间,其实现步骤几乎和vmalloc的三部曲一致,只是ioremap无需步骤二:通过buddy组件分配物理页。因为ioremap要映射的目标是IO地址空间,不是物理内存。
对于ioremap返回地址的操作
因为I/O空间在不同体系结构上有不同的解释,IA32上有独立与内存访问指令之外的IO访问指令:in 和 out
arm架构没有这种,所以在函数返回地址的使用上,有需要注意的地方。假设返回的是pVaddr:
有专门的IO访问指令的结构:
*pVaddr=0x1234
是错误地,必须使用readw(pWaddr),后者使用了inw指令,即in指令的变体。
对于没有专门的IO访问指令的结构:
*pVaddr=0x1234
是正确的,无需使用readw。
如果要写出可移植的代码,应该统一使用readb/writeb这样的宏,这些宏在不同的平台上,会展开成架构相关的代码。
iounmap
被映射的IO空间不再使用,应该使用iounmap函数来做相关的清除工作,iounmap函数做ioremap的反操作。将vmalloc区分配的虚拟内存还给vmalloc区,清除页表项解除映射。