4. 虚拟内存区域VMA:VMA结构体,VMA操作函数
大家好,我是你们的老朋友。今天我们来聊聊虚拟内存区域,也就是VMA。说实话,VMA这个概念,我当年刚接触内核时觉得挺抽象的。后来在项目中调一个内存泄漏的bug,折腾了三天,最后发现是VMA操作没处理好。从那以后,我对VMA就格外上心了。
你想想看,一个进程的地址空间,不是一整块铁板。它被划分成很多段:代码段、数据段、堆、栈、还有mmap映射的区域。每一段,内核就用一个VMA结构体来管理。说白了,VMA就是进程地址空间的“户口本”,记录着每块内存区域的起止地址、权限、标志位等信息。
4.1 VMA结构体:内存区域的身份证
我们先来看看VMA长什么样。在include/linux/mm_types.h里,struct vm_area_struct就是它的真身。我挑几个关键字段说说:
struct vm_area_struct {
unsigned long vm_start; // 起始地址
unsigned long vm_end; // 结束地址
struct mm_struct *vm_mm; // 所属的进程mm结构
unsigned long vm_flags; // 权限标志位
struct rb_node vm_rb; // 红黑树节点
struct list_head anon_vma_chain; // 匿名页链
const struct vm_operations_struct *vm_ops; // 操作函数集
unsigned long vm_pgoff; // 文件映射的偏移
struct file *vm_file; // 映射的文件
void *vm_private_data; // 私有数据
};
嗯,这里要注意几个点:
- vm_start和vm_end:这是VMA的边界。内核保证它们按页对齐,且vm_start < vm_end。
- vm_flags:权限位,比如VM_READ、VM_WRITE、VM_EXEC、VM_SHARED。我在项目中遇到过,有人mmap时忘了设VM_SHARED,结果父子进程各自为政,数据不同步,排查了好久。
- vm_ops:这个很重要。它是一组回调函数,比如open、close、fault、page_mkwrite。缺页异常时,内核会调用vm_ops->fault来加载页面。
核心要点:每个VMA代表一段连续、同属性的虚拟地址空间。内核用红黑树管理所有VMA,查找效率是O(log n)。
4.2 VMA操作函数:增删改查的艺术
内核提供了一套API来操作VMA。我个人最常用的两个是find_vma和insert_vm_struct。咱们一个一个说。
4.2.1 find_vma:快速定位内存区域
这个函数用来查找给定地址属于哪个VMA。原型很简单:
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
它返回第一个满足vma->vm_end > addr的VMA。注意,不是vm_start <= addr,而是vm_end > addr。为什么?因为内核要处理地址刚好落在两个VMA之间的情况。如果addr在空隙里,返回的是下一个VMA。
我曾经在写一个内存监控模块时,用find_vma遍历进程的所有VMA。结果发现,如果addr正好等于某个VMA的vm_end,返回的是下一个VMA。当时没注意,导致统计漏了一块区域。嗯,这个坑我踩过,你们记住就好。
使用技巧:调用find_vma前,最好先拿mmap_lock(读锁)。虽然函数内部会检查,但外部加锁能保证VMA链表不被并发修改。
4.2.2 insert_vm_struct:插入新的内存区域
当进程调用mmap、brk、mremap时,内核需要创建新的VMA并插入到红黑树中。insert_vm_struct就是干这个活的。
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma);
调用前,你得先初始化好VMA的各个字段,特别是vm_start、vm_end、vm_flags。内核会检查新VMA是否与现有区域重叠。如果重叠,返回-ENOMEM。
我记得有一次,我在写一个内核模块,想通过insert_vm_struct直接插入一个VMA。结果忘了设置vm_mm字段,内核直接panic了。从那以后,我每次插入前都会用memset(vma, 0, sizeof(*vma))清空,再逐个字段赋值。
警告:不要直接调用insert_vm_struct来创建用户空间的VMA。用户空间的VMA应该通过系统调用(如mmap)来创建。内核模块直接插入VMA,可能会导致进程地址空间混乱,甚至引发安全漏洞。
4.3 VMA的遍历与查找:红黑树与链表
内核用两种数据结构管理VMA:红黑树和双向链表。红黑树用于快速查找,链表用于遍历。
遍历所有VMA,可以用mmap->mmap链表:
struct vm_area_struct *vma;
for (vma = mm->mmap; vma; vma = vma->vm_next) {
// 处理每个VMA
}
但如果你要查找某个地址,用链表遍历就太慢了。这时候红黑树就派上用场了。find_vma内部就是走红黑树查找,效率高得多。
我建议,在写内核模块时,能用find_vma就别自己遍历。除非你要统计所有VMA的信息,那才用链表遍历。
4.4 实战:写一个VMA遍历函数
光说不练假把式。咱们写一个简单的函数,打印当前进程的所有VMA信息:
void dump_vma_info(void) {
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma;
if (!mm)
return;
mmap_read_lock(mm);
for (vma = mm->mmap; vma; vma = vma->vm_next) {
printk("VMA: 0x%lx-0x%lx flags=0x%lx\n",
vma->vm_start, vma->vm_end, vma->vm_flags);
}
mmap_read_unlock(mm);
}
这个函数很简单,但很实用。我在调试内存泄漏时,就靠它打印所有VMA,然后对比正常进程的VMA分布,找出异常区域。
经验之谈:VMA的数量不是越多越好。一个进程如果有成千上万个VMA,说明内存碎片化严重,或者有频繁的mmap/munmap操作。这时候,查找和遍历的开销都会变大。我曾经优化过一个数据库进程,把VMA数量从5000降到200,性能提升了15%。
4.5 总结
好了,今天的内容就到这里。我们聊了VMA的结构体、查找函数find_vma、插入函数insert_vm_struct,还有遍历的方法。VMA是内存管理的基石,理解它,你就能看懂进程地址空间的布局。
下一章,我们会深入缺页异常处理,看看VMA的vm_ops是怎么被调用的。到时候,你会发现VMA的fault函数才是真正的重头戏。
记住,写内核代码,多一分谨慎,少一分panic。咱们下期见。