文章目录
- 背景
- 进程地址空间
- 分页和虚拟地址空间
- 写时拷贝
背景
研究背景:我们在之前通过fork函数创建子进程的时候,我们发现fork的返回值有两个,且值不相同但地址确实相同的,我们知道在物理空间上这种情况是不可能存在的,同一个地址的变量怎么会有两个值呢?
把原来的代码再拿来感受一下:
上述代码,返回值有两个,因为分别为父子进程的返回值,且父子进程共用同一份代码和数据,这个我们在之前就了解过了,没什么问题,再来看下面的代码,稍加改动
结果输出不同,因为我们这里把子进程的g_val改为了100,且在修改的过程中发生了写实拷贝,新开辟了一块空间给子进程存储了这个数据,这个我们似乎也可以理解,那么又要怎么解释他们的地址相同呢?通过上述例子我们可以得出以下结论:
这个地址一定不是真正的地址(物理地址),在Linux下这种地址叫做虚拟地址,我们在C/C++语言中所看到的地址也全部都是虚拟地址,物理地址用户一概看不到,全部都由OS统一进行管理。
因此OS必须负责把虚拟地址转换为物理地址
进程地址空间
程序地址空间其实是一种不准确的表达,准确的应该说成进程地址空间,那么该如何理解?
此时可能你还会有些疑问,为什么要有虚拟地址?直接使用物理地址不是更简单吗?
我们在语言层面上经常会听到静态区,常量区,栈区,堆区等概念,现在我们先来对地址空间进行一个划分。
#include<stdio.h>
#include<stdlib.h>
int g_unval;//未初始化
int g_val = 100;//初始化
int main(int argc,char *argv[],char *env[])
{printf("code addr: %p\n",main);//代码区起始地址const char* p1 = "hello world";//p1是指针变量(栈区),p指向字符常量h(字符常量区)printf("read only : %p\n",p1);printf("global val: %p\n",&g_val);printf("global uninit val: %p\n",&g_unval);char *p2 = (char *)malloc(10);printf("heap addr: %p\n", p2);char *p3 = (char *)malloc(10);printf("heap addr: %p\n", p3);printf("stack addr: %p\n",&p1);//p1先定义先入栈printf("stack addr: %p\n",&p2);printf("args addr %p\n",argv[0]);//命令行参数printf("args addr %p\n",argv[argc-1]);printf("env addr: %p\n",env[0]);//环境变量return 0;
}
main函数开始地址空间由低向高增长,且栈区地址由高向低增长,堆区地址由低向高增长
1.进程地址空间不是内存
2.进程地址空间,会在进程的整个生命周期内一直存在,直到进程退出
分页和虚拟地址空间
我们来解释一下为什么需要虚拟地址空间?为什么不能让进程直接访问物理内存呢?
保护物理内存不受任何进程内地址的直接访问,在虚拟地址到物理地址的转化过程中方便进行合法性检验。
比如野指针导致物理内存中的数据被修改,即使操作系统让该段内存是可读的,也会有风险,比如密码,可能会被别人读取到。
那么虚拟地址空间是如何解决进程直接访问物理内存可能会出现的问题呢?
每一个进程都有它对应的task_struct,地址空间,页表,页表中有虚拟地址和物理内存的映射关系,有了页表的存在,就有了虚拟地址到物理地址的对应转化关系,这个转化过程由操作系统完成,同时也可以帮助系统进行合法性检测。
我们在写代码时经常会出现指针越界,那么指针越界就一定会出现错误吗?
不一定,
1.越界可能它还是在自己的合法区域,比如他本来指向的就是栈区,越界后他依然指向栈区,编译器的检查机制就会认为它是合法的,当你本来是指向数据区,结果指针后来指向了字符常量区,编译器就会根据mm_struct里面的struct,end区间来判断你有没有越界,此时发现你越界了就会报错
2.页表也有一种权限管理,当你对数据区进行映射时,数据区是可以读写的,相应的在页表映射关系就是可读可写的,但是当你对代码区和字符常量区进行映射时,因为这两个区域只是可读的,相应的在也表中的映射关系中的权限就是可读的,如果你对这个区域进行了写,通过页表中的权限管理,操作系统就会直接将这个进程杀掉。
所以,进程地址空间的存在也使得可以通过start和end以及页表的管理权限来判断指针是否具有合法性
虚拟地址空间的存在使得所有的进程以统一的视角去看待内存,然后OS会把虚拟地址空间中的虚拟地址映射到真实的物理地址上去
因为地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本
子进程继承于父进程,数据段只是可读的,当有一方想要写入时,OS会通过一系列的管理机制,让它进行写实拷贝,在页表中重新去映射对应的关系,从而做到而不去修改原来的数据,此处还有一些疑问,OS是怎么知道哪些数据是可以进行写实拷贝的呢?这个问题比较复杂,以后再来说