0.地址空间
#include<stdio.h>
#include<stdlib.h>
int g_val_1;
int g_val_2=100;
int main()
{
printf("code addr: %p\n", main);
const char *str = "hello bit";
printf("read only string addr: %p\n", str);
printf("init global value addr: %p\n", &g_val_2);
printf("uninit global value addr: %p\n", &g_val_1);
char *mem = (char*)malloc(100);
char *mem1 = (char*)malloc(100);
char *mem2 = (char*)malloc(100);
printf("heap addr: %p\n", mem);
printf("heap addr: %p\n", mem1);
printf("heap addr: %p\n", mem2);
printf("stack addr: %p\n", &str);
printf("stack addr: %p\n", &mem);
static int a = 0;
int b;
int c;
printf("static int a = stack addr: %p\n", &a);
printf("int b = stack addr: %p\n", &b);
printf("int c = stack addr: %p\n", &c);
for(; argv[i]; i++)
printf("argv[%d]: %p\n", i, argv[i]);
for(i=0; env[i]; i++)
printf("env[%d]: %p\n", i, env[i]);
return 0;
}
代码运行的结果如下:
由上面的代码及代码运行的结果,可以看出,
- ①从低地址从高地址,依次正文代码区,字符常量区,初始化全局变量区,未初始化全局变量区,堆区,栈区、命令行参数、环境变量;
- ②堆区地址向上增长,栈区地址向下增长;
- ③static修饰的局部变量编译的时候被放到了全局变量区,而非static修饰的局部变量被放到了栈区。
我们可以得到以下的地址空间布局图如下:
1.什么是程序地址空间
看到地址空间布局图,我们就真正理解了内存的地址(即“程序的地址空间”
)了吗?它是我们理解的内存真实的地址(物理地址)了吗?
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id == 0)
{
int cnt = 5;
// 子进程
while(1)
{
printf("i am child, pid : %d, ppid : %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
if(cnt) cnt--;
else {
g_val=200;
printf("子进程change g_val : 100->200\n");
cnt--;
}
}
}
else
{
// 父进程
while(1)
{
printf("i am parent, pid : %d, ppid : %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
代码运行的结果如下:
由上面的代码及代码运行的结果,根据我们在fork()
学习的知识,父进程和子进程在刚开始的时候数据和代码共享,但为了保持进程间的独立性,g_val的值(数据)
在发生改变的时候,会发生写时拷贝,操作系统会为子进程的变量g_val
开辟新的空间存放修改的值,新的地址与原来的地址应该不一样了,但我们发现,父子进程,输出地址是一致的,但是变量内容不一样!这怎么可能呢?
据此我们可以的出以下结论:
- ①变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但地址值是一样的,说明该地址绝对不是
物理地址
!- ②在
Linux
地址下,这种地址叫做虚拟地址
,我们在用C/C++语言所看到的地址,全部都是虚拟地址
!物理地址
,用户一概看不到,由OS统一管理- ③
OS
必须负责将虚拟地址
转化成物理地址
1.1 进程地址空间
所以之前说‘程序的地址空间’
是不准确的,准确的应该说成 进程地址空间
,那该如何理解呢?看图:
进程看到的是它自己的虚拟地址空间,在操作系统的参与下,通过“页表”
与物理地址对应的物理内存进行关联起来,进程便可以将自己的数据存放到物理内存中并进行访问。那什么是页表
呢?它是一种叫做 KV 式的映射关系
,在页表中,左侧是我们对应的虚拟地址,右侧是物理地址。
PCB中的mm_struc
t的结构体:
struct mm_struct
{
//正文代码区
long code_start;
long code_end;
//字符常量区
long readonly_strat;
long readonly_end;
//初始化全局变量区
long init_start;
long init_end;
//未初始化全局变量区
long unint_start;
long unint_start;
//堆区
long heap_start;
long heap_end;
//栈区
long stack_start;
long stack_end;
};
总结:
①所谓进程地址空间,本质是描述一个进程可视范围的大小,地址空间内一定要存在各种区域的划分,对线性地址进行start/end
;
②地址空间本质是内核的一个数据结构对象,类似PCB一样,地址空间也是要被操作系统管理的:先描述,在组织
;
③在task_struct
结构体(PCB
)中,有一个mm_struc
t的结构体,该结构体中定义了start/end
变量对进程空间各区域进行划分管理;
2.程序地址空间的意义是什么
例子①问题是我们为什么要用地址空间啊?正是因为每个进程都认为自己具有了 4GB 的地址空间,每一个进程可不可以叫做用自己对应的自己的代码和各自用自己的已初始化数据和,那么在整个内存布局上是可以做到完全一样的。父进程代码,比如说数据,那么那代码比如从
0X123456
开始,子进程它也可以代码从0X123456
开始,那么我们就可以统一的视角来看待,那么对应内存,而不用再让父子进程还特殊性的去记录自己的代码和数据,在物理内存什么地方不用记住了,父子进程可以以统一的视角看待内存。
结论①: 进程可以以统一的视角看待进程。
例子② 那么当你小的时候呢?过年的时候,同学们都会有你的爸爸妈妈、爷爷奶奶,你的二爸三舅四大爷之类的给你放各种各样的压岁钱,然后你拿着你的钱就能直接去对应的商店去买东西。可是因为你太小了,所以你经常会买到你干脆自己压根就不需要的东西,甚至可能会被这个那么无良商家给骗了,那么反正就是乱花钱的情况比较严重。所以有一天那么你自己好不容易收了,比如说你当前收了500 块钱的压岁钱,然后你的妈妈就有一天晚上找到了你,说"小王,哎,这样,你把你的压岁钱让妈妈给你拿着,你以后买什么东西了?你给妈妈讲”,比如说你要买,如果买个饼要花10 块钱,妈妈就给你 10 块钱,你再去买啊。你妈妈说:“没什么区别,你要买什么东西照样还是买,只不过妈妈给你管一下就行”。
以前一边是你,一边是商店的老板,你们两个是可以直接交易的,现在你没买什么东西都得先找你妈,然后你妈妈要同意之后,然后你才去找这个老板。有一天你给老妈说你想买一个那个文具盒,你妈我想花20 块钱买个文具盒,你妈妈说行啊,因为她觉得你买文具盒是办正事。有一天你又找你妈,你说妈妈我想买个小的游戏机,大概 50块钱。你妈妈瞪了你一眼说,买什么游戏机?你现在不是还在上学吗?掏什么钱呢?直接拒绝了你同学们,所以在这个过程当中,你们妈存在的意义是什么呢?同学们,那么你妈妈存在的意义是不是就可以让你那么不再犯错?在花钱这件事情上不要过多的去犯错。那么你们妈可以针对你要花的钱进行判定,如果你要花的钱是一个非法的钱,她可以提前拦截你,所以在我们所对应的那么软件当中,你自己直接访问,你可能会犯错,但有了你妈妈的存在,那么这个犯错概率特别小了。我们在进程在访问内存的时候也是同样的道理!
结论②: 增加进程的虚拟地址空间可以让我们访问的时候,增加一个转化过程,在这个转化过程中可以对我们的寻址请求进行审查,一旦异常访问,直接拦截,该请求不会到达物理内存,起到了保护物理内存的作用!
例子 ③如果进程能直接访问物理内存,可能会修改别的进程正在使用的数据,而且当访问物理内存发生错误时,会直接导致进程崩溃;但如果进程使用了地址空间,只需要往自己的对应的区域存放数据,虚拟地址填到页表中,至于如何放到物理内存中,由操作系统完成,至于会不会影响到别的进程的数据、越界访问等情况,进程不需要关心,物理内存的错误,也不会立马导致进程立马崩溃
结论③: 因为有了进程地址空间和页表的存在,将进程管理模块和内存管理模块进行了解耦合。
3.页表的作用
①正文代码区、字符常量区是只读的,是怎么做到的?
在页表中由一个标志位,如果该标志位为rw
的,可以对物理内存的内容进行修改,如对数据区映射的内容进行读写;如果该标志位为r
,可以对物理内存的内容只能读取,如正文代码区、字符常量区映射的内容只读不写。
②程序是被全部加载到内存中的吗?
在页表中,通常有一个标志位用来指示一个页面是否已经被加载到物理内存中。这个标志位通常被称为“存在位”(Present Bit)或“有效位”(Valid Bit)
。当存在位设置为1
时,表示页表项指向的页面已经在物理内存中;当设置为0
时,表示页面尚未加载到物理内存中。
当我们在玩大型游戏(几十GB)的时候,而我们的内存一般都是4GB,即使把全部内存给到这个进程也不可能满足(当然这也不可能,因为还有其他进程被运行),而且CPU
内还有进程切换,但是我们还是能正常玩游戏!这是因为游戏进程在运行的时候,只需要先运行部分资源,就可以运行起来,等需要的时候,再把其他资源加载进来!内存存在预加载原理,先把进程的资源加载进来,可以把运行运行起来,在“惰性加载”原理,进程找虚拟地址对应的物理内容,发现物理地址为空,且存在位为0
,进程会暂时中断,操作系统便会把硬盘的资源加载到物理内存中,便把对应的物理地址放到页表中,这就是 缺页中断
过程,CPU
便可以运行该游戏进程!
总结:
- ①页表中有读写的标志位,与程序地址空间相辅相成,正是有了标志位,操作系统才能对进程越界等异常请求进行拦截。
- ②页表中有存在位,可以让进程先预加载部分资源,当需要使用到剩下进程的资源,触发缺页中断,操作系统才把硬盘部分的资源加载到物理内存,缓解了物理内存资源紧张(惰性加载)。
- ③让不同的进程可以使用相同的虚拟地址(以统一的视角),但可以维护进程各自的资源,维护了进程的独立性!
- ④进程被创建的时候,先创建内核的数据结构,把页表的地址给到CPU的
cr3
寄存器中,CPU通过页表的映射关系把进程物理内存资源交互到CPU中运行。 - ⑤进程=内核数据结构{task_struct&&mm_struct&&页表}+程序的代码和数据。