vmalloc原理与实现
在 Linux 系统中的每个进程都有独立 4GB 内存空间,而 Linux 把这 4GB 内存空间划分为用户内存空间(0 ~ 3GB
)和内核内存空间(3GB ~ 4GB
),而内核内存空间由划分为直接内存映射区和动态内存映射区(vmalloc区)。
直接内存映射区从 3GB
开始到 3GB+896MB
处结束,直接内存映射区的特点就是物理地址与虚拟地址的关系为:虚拟地址 = 物理地址 + 3GB
。而动态内存映射区不能通过这种简单的关系关联,而是需要访问动态内存映射区时,由内核动态申请物理内存并且映射到动态内存映射区中。下图是动态内存映射区在内存空间的位置:
为什么需要vmalloc区
由于直接内存映射区(3GB ~ 3GB+896MB
)是直接映射到物理地址(0 ~ 896MB
)的,所以内核不能通过直接内存映射区使用到超过 896MB 之外的物理内存。这时候就需要提供一个机制能够让内核使用 896MB 之外的物理内存,所以 Linux 就实现了一个 vmalloc 机制。vmalloc 机制的目的是在内核内存空间提供一个内存区,能够让这个内存区映射到 896MB 之外的物理内存。如下图:
那么什么时候使用 vmalloc 呢?一般来说,如果要申请大块的内存就可以用vmalloc。
vmalloc实现
可以通过 vmalloc()
函数向内核申请一块内存,其原型如下:
void * vmalloc(unsigned long size);
参数 size
表示要申请的内存块大小。
我们看看看 vmalloc()
函数的实现,代码如下:
static inline void * vmalloc(unsigned long size)
{
return __vmalloc(size, GFP_KERNEL|__GFP_HIGHMEM, PAGE_KERNEL);
}
从上面代码可以看出,vmalloc()
函数直接调用了 __vmalloc()
函数,而 __vmalloc()
函数的实现如下:
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;
}
__vmalloc()
函数主要工作有两点:
调用
get_vm_area()
函数申请一个合法的虚拟内存地址。调用
vmalloc_area_pages()
函数把虚拟内存地址映射到物理内存地址。
接下来,我们看看 get_vm_area()
函数的实现,代码如下:
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;
}
get_vm_area()
函数比较简单,首先申请一个类型为 vm_struct
的结构 area
用于保存申请到的虚拟内存地址。然后查找可用的虚拟内存地址,如果找到,就把虚拟内存到虚拟内存地址保存到 area
变量中。最后把 area
连接到 vmalloc
虚拟内存地址管理链表 vmlist
中。vmlist
链表最终结果如下图:
申请到虚拟内存地址后,__vmalloc()
函数会调用 vmalloc_area_pages()
函数来对虚拟内存地址与物理内存地址进行映射。
我们知道,映射过程就是对进程的 页表
进行映射。但每个进程都有一个独立 页表
(内核线程除外),并且我们知道内核空间是所有进程共享的,那么就有个问题:如果只映射当前进程 页表
的内核空间,那么怎么同步到其他进程的内核空间呢?
为了解决内核空间同步问题,Linux 并不是直接对当前进程的内核空间映射的,而是对 init
进程的内核空间(init_mm
)进行映射,我们来看看 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); // 获取 address 地址在 init 进程对应的页目录项
spin_lock(&init_mm.page_table_lock); // 对 init_mm 上锁
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;
}
从上面代码可以看出,vmalloc_area_pages()
函数映射的主体是 init
进程的内存空间。因为映射的 init
进程的内存空间,所以当前进程访问 vmalloc()
函数申请的内存时,由于没有对虚拟内存进行映射,所以会发生 缺页异常
而触发内核调用 do_page_fault()
函数来修复。我们看看 do_page_fault()
函数对 vmalloc()
申请的内存异常处理:
void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
...
__asm__("movl %%cr2,%0":"=r" (address)); // 获取出错的虚拟地址
...
if (address >= TASK_SIZE && !(error_code & 5))
goto vmalloc_fault;
...
vmalloc_fault:
{
int offset = __pgd_offset(address);
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
pte_t *pte_k;
asm("movl %%cr3,%0":"=r" (pgd));
pgd = offset + (pgd_t *)__va(pgd);
pgd_k = init_mm.pgd + offset;
if (!pgd_present(*pgd_k))
goto no_context;
set_pgd(pgd, *pgd_k);
pmd = pmd_offset(pgd, address);
pmd_k = pmd_offset(pgd_k, address);
if (!pmd_present(*pmd_k))
goto no_context;
set_pmd(pmd, *pmd_k);
pte_k = pte_offset(pmd_k, address);
if (!pte_present(*pte_k))
goto no_context;
return;
}
}
上面的代码就是当进程访问 vmalloc()
函数申请到的内存时,发生 缺页异常
而进行的异常修复,主要的修复过程就是把 init
进程的 页表项
复制到当前进程的 页表项
中,这样就可以实现所有进程的内核内存地址空间同步。