前言
在我最开始写博客的时候,第一篇就介绍了虚拟地址空间,但当时受限于自己的水平,并且很多内容当时还没有介绍,所以当时写的比较简单,这篇文章算是还愿了吧。
/proc/{pid}/maps
linux的根目录下有很多目录,比如/etc通常是存放参数文件的,/bin通常存放二进制的可执行程序,/lib通常存放库文件,/include存放头文件,/proc存放的则是进程相关的文件。对每个正在运行的文件,/proc下都有一个文件名为该进程pid号的文件夹,例如有一个pid为10086的进程正在运行,那么就存在/proc/10086这个路径,路径下会有一个maps文件,这个文件展示了当前进程的虚拟地址空间,如下图:
文件的每列的含义如下:
第一列 | 第二列 | 第三列 | 第四列 | 第五列 | 第六列 | 第七列 |
---|---|---|---|---|---|---|
段起始地址 | 段结束地址 | 段属性 | 段在文件中的偏移 | 映射的i-node号 | 映射文件所属的设备号 | 映射的文件名 |
其中,段属性中,rwx分别表示读、写和执行,p和s公共一位,p为私有,而s为共享。如果是匿名映射,则第四、五、六列均为0。上面的文件中,我们可以清楚地看到堆区、栈区和链接的动态库,最上面五行是代码段、bss段、rodata段、data段等。通过maps,我们可以验证之前总结过的普通变量、指针变量、static变量等的存储位置。
#include <unistd.h>
#include <iostream>
using namespace std;
static int c = 10;
int main(){
cout << getpid() <<endl;
int a = 10;
int *b = new int(20);
cout << "a:" << &a << ",b:" << b << ",c:" << &c << endl;
delete(b);
return 0;
}
先获取进程pid号的目的是找到maps文件所在的路径,通过gdb运行程序,使程序停止在delete(b)
,程序打印信息如下:
打开路径为/proc/32394/maps的文件如下:
显然普通变量a在[stack]中,b在[heap]中,c在第5行的[data]段中,这说明我们之前的讨论是正确的,有兴趣的话还可以通过maps验证之前其他关于储存位置的讨论。
VMA
虚拟内存的管理在内核代码中是通过mm_struct来完成的,mm_struct被记录在task_struct中,是task_struct的成员之一,mm_struct中包含很多vm_area,简称为VMA,每个VMA就代表一个段,上面的例子中包含40个段,也就是包含40个VMA,VMA的具体结构如下:
struct vm_area_struct {
unsigned long vm_start; //起始地址
unsigned long vm_end; //结束地址
struct vm_area_struct* vm_next, * vm_prev; //VMA通过双向链表串在一起
struct rb_node vm_rb; //为了便于查找,VMA同时放置在红黑树中
unsigned long rb_subtree_gap; //红黑树中,该节点左右子树与该节点的最大间隙
struct mm_struct* vm_mm; //属于哪个mm_struct
pgprot_t vm_page_prot; //VMA的访问权限
unsigned long vm_flags; //VMA标志位
struct {
struct rb_node rb
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma* anon_vma; //指向匿名域的指针
const struct vm_operations_struct* vm_ops; //VMA操作函数
unsigned long vm_pgoff; //文件映射的偏移量
struct file* vm_file; //被映射的文件
void* vm_private_data;
};
有时程序会映射一段共享内存到虚拟地址空间中,此时在mm_struct中,增加了一个VMA,用来表示映射的共享内存,但此时并没有分配物理内存,当对页面进行操作时,通过缺页中断分配物理内存。
malloc的内存分配
malloc函数是c语言提供的库函数,库函数是不能直接操作内存的,需要通过系统调用进入内核态操作,malloc的系统调用由glibc提供,包括brk系统调用和mmap系统调用两种:
- 当申请的内存空间小于128k时,使用brk系统调用,brk系统调用仅移动_edata指针,表示增加堆上使用的内存,基于写时复制,此时也未分配物理内存,对内存进行操作时才分配物理内存。
- 当申请的内存空间大于128k时,使用mmap系统调用,mmap系统调用会在堆和动态库链接的中间分配一段内存,而不是在原来的heap上分配。
- 下图假设内存使用了三次malloc,两次使用brk系统调用,一次使用mmap系统调用,此时如果代码中
free
brk系统调用第一次分配的内存,则不能马上释放,需要等待brk第二次分配的内存释放后才能释放,当然虽然不能马上释放,但这段内存可以被程序复用。但mmap分配的内存没有这个限制,可以直接被释放。
我们通过下面的代码来验证这里的观点:
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
cout << getpid() <<endl;
void *a = malloc(256*1024);
int *b = new int(20);
cout << "a:" << a << ",b:" << b << endl;
delete(b);
free(a);
return 0;
}
程序打印结果:
打开对应的maps文件:
显然a分配在heap和/usr/lib/libc-2.33.so之间的匿名映射中,而b在[heap]中,验证成功。