前言:
上文我们讲到了Linux中的环境变量【Linux】环境变量-CSDN博客
本文我们来讲一下Linux中的虚拟地址空间
初识空间分布
下面是虚拟地址空间的大致分布图:
我们可以先通过代码来验证是否正确:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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/虚拟地址$ ./test
code addr: 0x55da1c103189
init global addr: 0x55da1c106010
uninit global addr: 0x55da1c10601c
heap addr: 0x55da1d1f06b0
heap addr: 0x55da1d1f06d0
heap addr: 0x55da1d1f06f0
heap addr: 0x55da1d1f0710
test static addr: 0x55da1c106014
stack addr: 0x7ffcf95ad350
stack addr: 0x7ffcf95ad358
stack addr: 0x7ffcf95ad360
stack addr: 0x7ffcf95ad368
read only string addr: 0x55da1c104004
argv[0]: 0x7ffcf95ad71f
env[0]: 0x7ffcf95ad726
env[1]: 0x7ffcf95ad736
env[2]: 0x7ffcf95ad744
env[3]: 0x7ffcf95ad75f
env[4]: 0x7ffcf95ad780
env[5]: 0x7ffcf95ad78c
env[6]: 0x7ffcf95ad7a1
env[7]: 0x7ffcf95ad7b0
env[8]: 0x7ffcf95ad7bf
env[9]: 0x7ffcf95ad7d0
env[10]: 0x7ffcf95addbf
env[11]: 0x7ffcf95addf5
env[12]: 0x7ffcf95ade17
env[13]: 0x7ffcf95ade2e
env[14]: 0x7ffcf95ade39
env[15]: 0x7ffcf95ade59
env[16]: 0x7ffcf95ade62
env[17]: 0x7ffcf95ade6a
env[18]: 0x7ffcf95ade7e
env[19]: 0x7ffcf95ade9a
env[20]: 0x7ffcf95adebe
env[21]: 0x7ffcf95adecf
env[22]: 0x7ffcf95adf10
env[23]: 0x7ffcf95adf78
env[24]: 0x7ffcf95adfab
env[25]: 0x7ffcf95adfbe
env[26]: 0x7ffcf95adfd1
env[27]: 0x7ffcf95adfe8
虚拟地址
打印不同进程下的相同变量的地址与值:
#include <stdio.h>
#include <unistd.h>
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 <stdio.h>
#include <unistd.h>
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分区中