0.引入
-
- 此处的地址空间不是内存
- 几乎所有的语言,如果它有“地址”的概念,这个地址一定不是物理地址,而是虚拟地址。
- static修饰局部变量,本质是将该变量开辟在全局区域
- 以上结论,默认只在Linux有效
一、 字面常量
//在c中能直接编译
10;
'a';
"helloworld";
- 字面常量会加入正文代码区
- 但有时候常量区和代码区会分开
二、用户空间VS内核空间
- 在32位下,一个进程的地址空间,取值范围是0x0000 0000~0xFFFF FFFF
- [0,3GB]:用户空间,按照[[3.Linux进程概念#五、程序地址空间]]的排列方式
- [3GB,4GB]:内核空间
- 属于操作系统的范畴
三、 地址空间
- 故事引入:操作系统是个父亲的角色,给各个进程(孩子)分配不同的地址空间(画饼)
1. 地址空间是什么?
- 先描述,在组织:
- 内核中的地址空间,本质将来也一定是一种数据结构,将来一定要和一个特定的进程关联起来–>地址空间时一种内核数据结构,它里面至少要有:各个区域的划分
2. 为什么有地址空间?
-
每个进程都私有一份地址空间和页表(用户级)
- 只要保证每一个进程的页表映射的是物理内存的不同区域,就能做到进程之间不会互相干扰,保证进程的独立性
-
① 因为地址空间和页表是OS创建并维护的,意味着凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来进行访问
- 内存本身是随时可以被读写的,所以若直接使用物理内存时,地址写错了的话,就会导致原本想要修改的进程没有被修改,并导致其他进程被修改影响了 --> 特别不安全
- 于是就产生了虚拟地址,虚拟地址会通过某些映射机制,实现虚拟地址转换成物理地址的过程。并且凡是非法的访问或者映射,OS都会识别到,并且终止该进程(–>有效地保护了物理内存中所有合法数据,包含各个进程及内核的相关有效数据)
-
②因为有地址空间和页表映射的存在,物理内存中就可以对未来的数据进行任意位置的加载。
- 内存管理模块和进程管理模块完成解耦合:物理内存的分配就可以和进程d的管理做到没有关系
- 故,在C/C++上new、malloc空间的时候,本质是在虚拟地址空间申请的(因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给,直到当你真正进行对物理地址空间访问时,才执行内存的相关管理算法:申请内存,构建页表映射关系–> 是由操作系统自动完成,用户、进程完全0感知)
- 即:延迟分配策略,以提高整机的效率(几乎内存的有效使用为100%)
-
③地址空间+页表的存在,每个进程都认为自己拥有4GB空间(32位),并且各个区域内存分布有序化,进而可以通过页表映射到不同区域来实现进程的独立性
- 每个进程不知道,也不需要知道其他进程的存在
- 因为在物理内存中,理论上可以任意位置加载,故物理内存中几乎所有的数据和代码在内存中是乱序的
- 但因为页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射,所以在进程视角所有的内存分布都可以是有序的
3. 区域划分
-
本质是在一个范围里定义出start和end
-
范围变化:本质其实是对start或者end标记值+/-特定的范围
四、为何父子进程能地址相同,但值不同呢?
1.写时拷贝
- 当子进程要对变量进行修改时,创建出一块新空间,它和父进程虽同属一个虚拟地址,但出于页表在物理内存的不同映射,父子进程就有了不同的值
2. return的本质:对id进行写入
- 发生了写时拷贝,所以父子进程各自其实在物理内存中有属于自己的变量空间,只不过在用户层用同一个变量(虚拟地址)来标识了
五、 拓展:深入理解虚拟地址(先记,后续理解)
-
当程序在编译时(形成可执行程序时,没有被加载到内存时候),程序内部其实已经有地址了。
- 可执行程序其实编译的时候,内部已经有地址了
- 地址空间不要仅仅理解成为时OS内部要遵守的,其实编译器也要遵守,即编译器编译代码时,就已经给我们形成了各个区域代码区、数据区……并且,采用和Linux内核中一样的编址方式,给每一个变量、每一行代码进行了编址,故程序在编译的时候,每一个字段早已经具有了一个虚拟地址。
- 程序内部的地址,依旧用的是编译器编译好的虚拟地址,当程序加载到内存的时候,每行代码、每个变量便具有了一个物理地址(外部的)
- 当CPU读到指令的时候,指令内部也有地址(也是虚拟的)
- CPU拿到的永远都是虚拟地址
-
地址空间和页表,最开始的时候,数据是从哪里来的呢?
- ①地址空间:
- 区域:编译好后,最开始的时候把代码的起始地址和最后地址来填充到正文部分来填入初始化和未初始化的start和end当中,堆区和栈区没有,就设为0或设定为初始值([[3.Linux进程概念#区域划分]])
- ②可执行函数每一个变量、每一个函数都有地址(是编译器给的),同样也一定被加载到了物理内存中
- 建立映射:虚拟地址填充页表的左侧;每个变量加载到物理内存后产生的物理地址,填充到页表的右侧
- ①地址空间:
-
正式访问过程:
- 每个变量可以通过变量名的方式在上层被用户访问(变量名其实也是地址)
- CPU通过上层的函数名(fun())等的入口地址(0x1),再通过页表映射找到物理内存中的数据,并读回CPU(此时CPU读到的指令内部,使用的地址是虚拟地址)。CPU拿到地址后,再去正文里查列表,就找到了下一条函数的地址了,继续读就可以开始调用函数了。
-
在编译器眼中,虚拟地址其实是逻辑地址,但在Linux下逻辑地址/线性地址/虚拟地址是一样的,因为Linux认为所有的起始地址都是从0开始的
- (市面上博客的说法大部分都是错的,不值得看)
加强理解:虚拟地址和物理地址
- 虚拟地址相当于学生的学号,而物理地址好比学生桌子上的桌号,喊某名同学的学号时,对应的桌号和学号可以是不同的