【Linux】虚拟地址空间
前言:
上文我们讲到了Linux中的环境变量【Linux】环境变量-CSDN博客
初识空间分布
下面是虚拟地址空间的大致分布图:
我们可以先通过代码来验证是否正确:
#include #include #include int g_unval;int g_val = 100;int main(int argc, char* argv[], char* env[]){ const char* str = \"helloworld\"; printf(\"code addr: %p\\n\", main); printf(\"init global addr: %p\\n\", &g_val); printf(\"uninit global addr: %p\\n\", &g_unval); static int test = 10; char* heap_mem = (char*)malloc(10); char* heap_mem1 = (char*)malloc(10); char* heap_mem2 = (char*)malloc(10); char* heap_mem3 = (char*)malloc(10); printf(\"heap addr: %p\\n\", heap_mem); printf(\"heap addr: %p\\n\", heap_mem1); printf(\"heap addr: %p\\n\", heap_mem2); printf(\"heap addr: %p\\n\", heap_mem3); printf(\"test static addr: %p\\n\", &test); printf(\"stack addr: %p\\n\", &heap_mem); printf(\"stack addr: %p\\n\", &heap_mem1); printf(\"stack addr: %p\\n\", &heap_mem2); printf(\"stack addr: %p\\n\", &heap_mem3); printf(\"read only string addr: %p\\n\", str); for (int i = 0; i < argc; i++) { printf(\"argv[%d]: %p\\n\", i, argv[i]); } for (int i = 0; env[i]; i++) { printf(\"env[%d]: %p\\n\", i, env[i]); } return 0;}
hyc@hcss-ecs-4ce7:~/linux/虚拟地址$ ./testcode addr: 0x55da1c103189init global addr: 0x55da1c106010uninit global addr: 0x55da1c10601cheap addr: 0x55da1d1f06b0heap addr: 0x55da1d1f06d0heap addr: 0x55da1d1f06f0heap addr: 0x55da1d1f0710test static addr: 0x55da1c106014stack addr: 0x7ffcf95ad350stack addr: 0x7ffcf95ad358stack addr: 0x7ffcf95ad360stack addr: 0x7ffcf95ad368read only string addr: 0x55da1c104004argv[0]: 0x7ffcf95ad71fenv[0]: 0x7ffcf95ad726env[1]: 0x7ffcf95ad736env[2]: 0x7ffcf95ad744env[3]: 0x7ffcf95ad75fenv[4]: 0x7ffcf95ad780env[5]: 0x7ffcf95ad78cenv[6]: 0x7ffcf95ad7a1env[7]: 0x7ffcf95ad7b0env[8]: 0x7ffcf95ad7bfenv[9]: 0x7ffcf95ad7d0env[10]: 0x7ffcf95addbfenv[11]: 0x7ffcf95addf5env[12]: 0x7ffcf95ade17env[13]: 0x7ffcf95ade2eenv[14]: 0x7ffcf95ade39env[15]: 0x7ffcf95ade59env[16]: 0x7ffcf95ade62env[17]: 0x7ffcf95ade6aenv[18]: 0x7ffcf95ade7eenv[19]: 0x7ffcf95ade9aenv[20]: 0x7ffcf95adebeenv[21]: 0x7ffcf95adecfenv[22]: 0x7ffcf95adf10env[23]: 0x7ffcf95adf78env[24]: 0x7ffcf95adfabenv[25]: 0x7ffcf95adfbeenv[26]: 0x7ffcf95adfd1env[27]: 0x7ffcf95adfe8
虚拟地址
打印不同进程下的相同变量的地址与值:
#include #include int g_val = 0;int main(){ pid_t id = fork(); if (id < 0) { perror(\"fork\"); return 0; } else if(id == 0) { printf(\"child[%d]: %d : %p\\n\", getpid(), g_val, &g_val); } else { printf(\"parent[%d]: %d : %p\\n\", getpid(), g_val, &g_val); } sleep(1); return 0;}
我们可以看到父子进程的输出结果都是一样的,很好理解没有问题。
但如果子进程对变量进行修改会发生什么?
#include #include int g_val = 0;int main(){ pid_t id = fork(); if (id < 0) { perror(\"fork\"); return 0; } else if (id == 0) { g_val=100; printf(\"child[%d]: %d : %p\\n\", getpid(), g_val, &g_val); } else { printf(\"parent[%d]: %d : %p\\n\", getpid(), g_val, &g_val); } sleep(1); return 0;}
这时我们就会发现输出变量的地址是一模一样的,但是变量的值却不一样!这就很奇怪了。
总结:
相同的地址值却不一样,说明我们所打印出来的绝对不可能是真实的地址。而这就是我们说所的虚拟地址。
我们在使用C/C++看到的地址,全部都是虚拟地址!物理地址,用户一概都看不到,由OS统一管理。
换言之,OS负责讲虚拟地址转化为物理地址。
虚拟地址空间
结论一:
1.一个进程,对应拥有一个虚拟地址空间。
2.一个进程,也对应拥有一个页表。页表的作用是建立虚拟地址与物理地址的映射关系,用于虚拟地址与物理地址的转化。
3.虚拟地址空间单位为:1字节,既一个地址指向的空间大小为1字节。32位机器有2^32个地址,64位机器有2^64个地址。32位机器的虚拟空间大小就有4G(2^32*1字节)其中3G是用户可用空间,1G是内核空间(暂时不用管)
如果我们创建一个子进程,我们知道子进程的task_struct是拷贝父进程的,而子进程与父进程共享代码与数据(没有新继承加载进来的情况下)这也就意味了子进程的页表映射关系与父进程一模一样。
但如果子进程要修改数据呢?会发生什么?
结论二:
子进程修改数据会发生写时拷贝:在物理空间上重新开辟,并在页表中重新建立映射关系。注意:虚拟地址并没有改变,仅仅改变了物理地址!!!
这也就解释了最开始的代码中,为什么明明打印出来的地址相同,但是值却不同。
写时拷贝的意义
有的同学可以困惑,为什么不创建进程的时间直接拷贝一份呢?
1.减少了创建进程的时间
如果创建时就直接拷贝,会增加进程创建的时间:并不会进程中每一个变量都会修改
2.减少了内存的浪费
对于不需要修改的变量,依旧拷贝就会产生冗余数据,浪费内存空间
虚拟地址空间的本质
mm_struct
虚拟地址空间的本身其实就是一个结构体!(mm_struct)
我们知道虚拟空间中有许多区域划分,而如果确定区域的位置呢?只需要知道各个区域的起始位置以及结束位置就可以了。所以mm_struct的主要成员变量是各个区域的位置信息。
想要对区域进行调整,也只需要修改对应区域的start、end即可。
struct mm_struct{ unsigned long start_code,end_code; unsigned long start_data,end_data; ......}
我们说一个进程对应一个虚拟地址空间,既对应一个mm_struct。操作系统中有多个进程,必然也有多个mm_struct。那么就如何管理的呢?
1.当数量较少时,采用单链表管理
2.当数量较多时,采用红黑数管理
vm_area_struct
mm_struct中还有一重要成员:vm_area_struct(存放其指针)
vm_area_struct用来表示一个独立的虚拟内存区域,由于每个不同的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表示不同类型的虚拟内存区域,方便进程快速访问。
为什么要有虚拟地址
1.让无序的物理地址,变为有序
2.在地址的转化过程中,可以对地址以及操作进行判定是否合法,进而保护了物理内存
例如:char* str=\"abc\"; *str=\"a\",为什么会报错?因为在查找页表时被拦截了!
3.让进程管理和内存管理,进行一定程度的解耦合
不让虚拟地址与物理地址有强相关性
补充:
1.我们可以不加载代码和数据,只有task_struct、mm_struct、页表
2.再次理解挂起:保留虚拟地址空间以及页表中的虚拟地址,清空页表中的物理地址,将物理地址空间唤入到磁盘的swap分区中