本片博客将重点回答三个问题
什么是地址空间?
地址空间是如何设计的?
为什么要有地址空间?
程序地址空间排布图
在32位下,一个进程的地址空间,取值范围是0x0000 0000~ 0xFFFF FFFF
回答三个问题之前我们先来证明地址空间排布是按如图所布局的
各个区空间地址验证代码
#include <unistd.h>
#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);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
// 堆区,指针变量本质是变量,也要开辟空间,不过放的内容是地址
char *heap_mem = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem);
// 栈区,函数内定义的变量都是在栈上开辟空间
printf("stack addr: %p\n", &heap_mem);
int i = 0;
for (i = 0; i < argc; i++)
{
// 命令行参数地址
printf("argv[%d]: %p\n", i, argv[i]);
}
int j = 0;
for (j = 0; env[j]; j++)
{
printf("env[%d]: %p\n", j, env[j]);
}
return 0;
}
运行结果
堆、栈之间的两个箭头表示
栈向地址减小的方向增长
堆向地址增大的方向增长
证明方法也很简单
运行结果也证明确实是这样
我们会发现堆区之间差了20字节
我们平时申请空间,系统会多给你一些空间
多出的空间用来记录你堆的属性信息
所以平时我们free空间,只要传起始地址
剩下的系统知道要free多长的空间
我们在系统部分要记的两个口诀
1、先描述在组织
2、堆、栈相对而生
static 修饰局部变量,本质就是将该变量开辟在全局区域
所有的字面常量将来都是要映编码进代码的
在正文代码上其实有一小段是字符常量区
什么是地址空间以及是如何设计的
我们平时打印各种地址其实就是进程打印
,程序运行之后打印的
在解释什么是地址空间之前,我们先来讲一个故事
有一个富豪,他有5亿元家产
他有3个私生子,彼此并不知道对方存在
3个私生子分别叫张三、李四、王五
富豪为了鼓励3个儿子
对张三说你好好念书将来5亿就是你的了
对另外两个儿子也说了同样的话
因为不知道彼此存在
对于这三个儿子,他们都认为是5亿继承人
富豪给他们每一个儿子画了一个大饼
有一天,张三对他爸说要1千买学习资料
李四说我成年了想买一辆两百万的跑车
王五说我创业需要50万
富豪都给了他们需要的钱
只要他们要钱富豪都会给
有一天张三说要1亿,富豪说要这么多干嘛
拒绝了张三,即使被拒绝了张三依旧认为自己是5亿的继承人
我们站在上帝视角知道即使富豪过世了
这三个儿子不可能都拥有5亿
他们每个儿子可以断断续续的要钱
但永远要不到5亿,却依然坚信自己以后能拥有这5亿
对应关系
富豪 ---- 操作系统
儿子 ---- 进程
富豪画的饼 ---- 地址空间
在内存中的地址空间本质是一种数据结构
将来要和一个特定的进程关联起来
以前直接访问物理内存,如果有野指针的问题
可能直接访问到其他进程
内存本身是随时可以被读写
所以在老式的程序里面野指针是会直接改了其他进程的东西
结论:直接使用物理内存不安全
现代计算机的解决方式
每个进程有自己的PCB
操作系统给每个进程一个虚拟的地址空间
通过映射机制映射到物理内存
我们可能会有疑问,最终还是会访问物理内存
万一虚拟地址是一个非法地址呢
其实映射机制有一个检查机制,万一是非法地址
可以不让你映射
虚拟地址空间究竟是什么?
每个进程都要有地址空间
就好比操作系统要给每个进程画个饼
操作系统要给每个饼做管理
在内存中的地址空间本质是一种内核数据结构
它里面至少有各个区域的划分
我们把如图结构称为地址空间
区域空间并不是死的,会有一定的变化
所谓的范围变化,本质是对start 或end 标记值 + - 特定的范围即可
所以一个地址为什么有两个值
到这里就可以回答这个问题了
刚开始创建时只有父进程
然后创建子进程,子进程会继承父进程的属性
所以子进程的页表、地址空间和父进程一样
当子进程尝试修改变量值时
因为要保证进程的独立性
操作系统会重新为子进程,开辟一份物理内存
并修改子进程页表的映射关系
但是虚拟地址并不受影响,还是一样的地址
但映射到物理内存的不同区域
看到的值便不一样
这种策略就叫作写时拷贝
为什么要存在地址空间
- 保护物理内存
凡是非法的访问或者映射,
os都会识别到,并且终止你这个进程
因为地址空间和页表是os创建并维护的
也就意味着凡是想使用地址空间和页表
进行映射,也一定要在OS的监管下进行访问
- OS耦合度更低
因为有地址空间的存在
因为有页表映射的存在
我们的物理内存就可以
对未来的数据进行任意位置的加载
物理内存的分配就可以和
进程的管理互不关联
从而使内存管理模块和进程管理模块
完成解耦合
我们在C、C++语言上new、malloc空间时
本质是在虚拟地址空间申请的
因为有地址空间的存在,所以上层申请空间
物理内存可以甚至一个字节都不给你
当你真正访问物理地址时,才执行
内存相关算法,帮你申请内存,构建
页表映射关系,这样空间使用率为100%
以此提高整机效率
- 保证进程的独立性
因为有地址空间的存在,每一个进程
都认为自己拥有4GB的空间(32)
并且各个区域是有序的,进而
可以通过页表映射到不同的区域
来实现进程的独立性,每一个进程
不知道,也不需要知道其他进程的存在
重新理解什么是挂起?
加载的本质就是创建进程,但并不是
非得把所有程序的代码和数据加载到
内存中,并创建内核数据结构建立映射关系
在极端情况下,只有内核结构被创建
此时就叫新建状态
理论上,可以实现对程序的分批加载
既然可以分批加载,自然可以分批换出
一个进程短时间不会被执行,比如阻塞
而使进程的数据和代码被换出就叫挂起
页表不仅仅映射物理内存
磁盘位置也可以映射
所以当代码挂起时,不用把数据
刷新到磁盘里。只要把空间直接释放掉,
在页表重新填上磁盘当中代码和数据的
位置,就可以完成一次基本的挂起
扩展知识
在vim中注释
Ctrl + v 进入视图模式(V-BLOCK)
hjkl 选中需要注释代码
输入大写的i,左下角出现INSERT
输入 // ,再按esc 自动注释选中的代码
取消注释还是上面的操作
选中需要注释的代码,按d删除
✨✨✨✨✨
本篇博客完,感谢阅读🌹🌹🌹
如有错误之处可评论指出,博主会耐心听取每条意见