程序地址空间
C/C++地址空间!
在先认识程序地址空间前我们要先认识一下C/C++的地址空间!
这就是C/C++的地址空间的结构!
不过这个地址空间究竟是什么?——是内存吗?
==答案是错误的!这个地址空间其实不是内存!==
我们可以看一下
int main()
{
pid_t id = fork();
if(id < 0 )
{
printf(fork error\n);
}
else if( id == 0 )
{
int cnt = 0;
while(1)
{
printf(i am child ,pid = %d,ppid = %d,global_value = %d ,&global_valueo=%p\n,getpid(),getppid(),global_value,&global_value);
cnt++;
if(cnt ==10) {
global_value = 300;
printf(child had change global_value!\n);
}
sleep(1);
}
}
else
{
while(1)
{
printf(i am father ,pid = %d,ppid = %d,global_value = %d ,&global_valueo=%p\n,getpid(),getppid(),global_value,&global_value);
sleep(2);
}
}
return 0;
}
fork的时候会生成一个子进程!因为进程要被系统管理!==所以子进程就要去拷贝父进程的内核数据结构(PCB)!和复制父进程的代码!==
==怎么回事呢?为什么明明是同一个地址的变量却拥有两个值呢?多进程在读取同一个地址的时候怎么会出现不同的结果呢?==——如果值不同地址不同我们或许可以理解!因为进程之间是互相独立的!数据也应该是独立的!但是地址相同是怎么回事?
但是我们可以从这个现象中反推一个结论!——==这里打印出来的地址!绝对不是一个真实的物理地址!==
==我们也可以推出我们学的语言级别的基本地址(指针),也绝对不是对应的物理地址!==
我们以前看到的所有地址其实是一个虚拟地址!(又叫线性地址,linux下又叫逻辑地址!)
==所以C++的地址空间其实不是内存!我们是将其称之为虚拟地址空间!——也叫进程地址空间==
进程地址空间的概念
首先我们要对进程地址空间有个感性的认知!
我们要明白
==每一个的进程都认为自己是独占所有的系统资源的!==
为了能更好的认识这个概念!我们下面举个例子
有一个大富翁他资产100亿,他有很多的孩子,但是孩子之间都是彼此互不相认的,认为自己是独生子,而且孩子都有自己的工作!然后呢大富翁告诉每个孩子,只要他好好的工作,那么他的遗产就去全部给他(相当于给每个人都画了一张大饼),所以每个后代都认为自己将独占大富翁的所有家产!
sun1,sun2,sun3都会因为各种原因向大富翁要钱!大富翁求他们的要求都是有求必应,但是可以肯定的是sun1,sun2,sun3肯定不会一次性一次性要100亿资产,只会小部分小部分的要
而这个我们将其换成操作系统
大富翁就是操作系统!100亿资产就是内存!
每个后代都是进程!——我们对系统要资源的时候,系统也都是有求必应
而且我们申请内存大小的时候也不会去一次性要所有的内存,我们只会一小部分一小部分的申请!
而大富翁给每个后代画的大饼就是——我们所说的==进程地址空间!==!——放在语言级别的就是虚拟地址空间!
==也可以说进程地址空间就是操作系统给每个进程画的大饼!==
这种设计思想是来自现实
我们现实中也常常被画“大饼”,例如我们将钱放入银行,其实银行会降我们的钱拿去投资,放贷什么的之类的,我们账户上的数字就是银行给我们的大饼,因为我们大概率是不会一次性拿出所有的钱的!
所以银行最害怕的就是很多人一起取钱,因为这样子银行的实际资金就不够了!
那么如何画饼呢?
画饼的本质就是在大脑中构建一个蓝图——这就相当于==数据结构的对象!==
像是公司老板给某个员工画饼,说是你达成了什么,他就在什么时候给你什么奖励!
每画一个饼就相当于多一个对象!像是这样
但是如果画饼画太多!给每个人不同的饼也不好记!所以饼也要管理!
假如操作系统内有 500个进程!每个进程都有不同的进程地址空间!进程本身要被管理!
虚拟地址空间也是要被管理的!——操作系统通过先描述后组织来管理!
==所以地址空间的本质就是——内核的一种数据结构!mm_struct!==
mm_struct的成员
以32位计算机为例
首先我们要知道几件事
- 地址空间描述的基本空间大小是字节!
- 32为下 有2^32^个地址!
- 每个进程地址空间所占的空间范围为 1字节 ——4GB
- 每一个字节的都有唯一的地址对应!
所以mm_struct的成员我们就可以如下表示
struct mm_struct
{
uin32_t code_start,code_end;//代码区
uin32_t data_start,data_end;//数据区
uin32_t heap_start,heap_end;//堆区
uin32_t stack_start,stack_end;//栈区
//.....
}
所以当给进程创建进程地址空间的时候实际上是在干嘛呢?
当一个进程开始的的时候,就会创建一个内核数据结构!系统会去申请一个空间(相当于malloc)!
然后开始去在这个数据结构里面去划分区域
==所以这就是为什么每一个进程都认为自己独占所有的资源!因为虚拟地址空间就是默认(例如32位计算机)有2^32^个地址!==
==而这2^32^的每一个地址都是虚拟地址!==
如何管理内存地址空间!
进程存在必然会存在一个内核数据结构(PCB)
而这个内核数据结构里面就存在一个指向struct_mm的指针!
即使通过这个task_struct来指向进程地址空间来管理的!
linux内核PCB里面的指针
进程地址空间和物理空间的对应!
我们已经知道了进程地址空间实际上不是真正的内存!而是一个数据结构!
那么我们数据是如何存在内存上面的呢?
我们的首先都知道,执行程序要加载首先要加载到磁盘上面!
进程占内存一定的大小,占用的这一部分==内存区域==一定存在起始地址和结束地址!
==这个地址都是真实的物理地址!==
可是虚拟地址空间和真实物理地址是不同的!那么怎么让进程找到存在于内存的实际的代码呢?
==将虚拟地址空间和真实物理空间连接起来的就是——页表==
为什么要有进程地址空间?
为什么不让进程直接访问物理空间?而是先访问虚拟地址空间再访问物理空间?绕了一大圈
-
==直接读取物理内存不利于数据安全!==——例如存在一个恶意进程,如果可以直接读取物理内存,只要把整个内存扫一遍,那么就可以读取到内存中的任意数据像是账号密码之类的!
-
如何进程直接访问物理内存那么一旦出现进程非法越界了该怎么办?
==一旦我们对于其他的进程数据进行读写会很大概率会导致其他进程出错!==,如果存在了虚拟地址空间!当进程拿着一个非法的虚拟地址去页表进行读取!发现没有对应的物理地址的时候!==那么页表就可以直接建拦截!==(页表不仅仅是保存地址还有其他的功能),不允许访问物理地址!!——==所以因为有页表的存在,那么每个进程都只会映射到合法的内存中!==这样就可以不用担心物理内存被随意的读写了!像是老式计算机没有页表保护一旦出现越界访问可能就会导致整个系统崩溃!
-
==地址空间的存在,可以方便的进行进程和进程的数据代码的解耦,保质量进程独立性这样的特征!==
-
让进程进程以统一的视角来看待进程对应的代码和数据等个个区域方便使用!方便编译器也已统一的视角来进行编译代码!
一个负责编译,一个负责使用所以!编译完可以立刻使用!
重新认识进程地址空间!
我们上面说了那么多都是在内存里面进行谈论的!
那么有个问题==在还没有加载到内存里面的时候可执行程序里面存不存在地址呢?==
在我们编程写C语言代码的时候想要将其转换为可执行程序要经历——预处理——汇编——编译——链接——可执行程序!
我们可以看一下反汇编后的结果!我们看到了什么?==没错是地址!==
也就是说在形成可执行程序之前!就已经存在地址了!
==我们以前在编译器上经常能看见以32位,64位编译代码!现在我们也可以知道了!其实本质就是使用的虚拟地址的编址方式不同!==