前言
你可能在C语言的学习中看过C程序的地址空间,但是今天以后它应该叫做进程地址空间。 1,你最熟悉的应该是栈和堆,当时你可能只了解了下面的部分,实际上上面的部分是内核区域。 2,还有一点要注意的是,代码段并不是从0000。。。开始的,虽然图上是这样画的。 3,这张图清晰的表明了C程序运行的数据分配。你可能被告知上面的是内存的划分,但是接下来的讨论可能会让你大开眼界(相信我,你会的)。
地址空间
首先要说明的是地址空间这个概念,什么是地址空间呢?一块地址空间每个部分都有被使用吗?不是的,地址空间只是划分出一段范围,表示所有的程序只能在这段空间内活动,这并不表示每个部分都有被使用。
进程地址空间不是物理内存 + 矛盾的栗子
下面的代码可能让人难以理解:
上面的栗子的作用是父进程fork了一个子进程,它们都有一个val,父进程输出val之前先睡3秒,保证父进程在子进程后面执行。此时val已经被改变,我们来看看这个进程的结果。 可以看到一个奇怪的现象:父子进程的val是不同的,但是它们的地址相同。进程地址空间是内存吗?如果是物理内存,那么一块空间不可能在同一时间有不同的值!!所以进程地址空间不是内存。进程地址空间只是夹在进程的PCB和物理内存中间的虚拟的东西。
task_struct中夹着的东西就是进程地址空间,每一个task_struct都配一个进程地址空间,而进程地址空间通过某种映射关系与物理内存发生联系。现在我们来解决这个程序的疑惑。一开始的时候父进程在它的进程地址空间给val分配了一块空间,然后fork之后,子进程也会复制一份跟父进程类似的进程地址空间,而它自然也把val在进程地址空间的位置复制过来了,所以父子进程的val地址相同。刚复制过来的时候,由于子没有修改数据内容,所以操作系统就把父子的val映射到同一块物理内存。但是当val变成1000时,由于进程有独立性,它是数据独有,所以为了保证父子进程互不影响,操作系统会在物理内存中另外找一份空间给子进程val,注意,这里子进程的task_struct和他的虚拟地址的映射不会改变!!所以这就造成了一种现象:地址相同,值不同。
为什么需要进程地址空间?
1,如果没有进程地址空间,那么你直接操作的就是物理内存,那么你的失误操作就可能影响到内存的安全,或者影响到别的进程。比如你使用了野指针,或者你的数组越界,访问到别的进程的空间,那就完了。而增加了进程地址空间,由用户直接访问内存变成由操作系统去访问内存,操作系统就会直接pass掉对内存有害的操作。 2,考虑这样一种情况,内存一共4G, A进程2G,B进程1G,此时还剩下1G,但是A和B进程不是连续存放的,导致剩下的空间不是连续的,如果此时来了一个1G的进程想要连续的空间(比如开辟数组),此时虽然你的内存足够,但是无法使用。这就造成了内存的浪费。如果使用虚拟地址空间,那么从pcb的角度来看,虚拟地址空间是连续的,你就可以随便使用,但是你可以改变映射关系,使得连续的虚拟空间映射到不连续的物理内存,充分利用内存的空间。 3,上面就是我们需要进程地址空间的原因之二。这个进程地址空间的设计有点类似shell的思想,封装一层,在C++中这种思想比比皆是。
进程地址空间如何映射到物理内存?
这里就要介绍一种叫做页表的东西,进程地址空间实际上是通过页表映射到物理内存上的。页表中记录了虚拟内存和物理内存的地址。
页表对内存的权限管理
这里就要问道几个问题了,既然虚拟地址空间会保护内存,那么我们编写C/C++代码时为什么还有注意野指针,越界之类的问题?答案是,虽然一定程度上保护了内存,但是还是可能映射到非法的物理内存上。还有我们常说的代码时可读不可写的,string时可读的,这些的权限限制是如何搞定的呢?没错,页表的又一功能就是限制访问内存的权限:保证只读的数据只有读权限,只写的数据只有写权限。而且如果你使用野指针或者非法数据,由可能系统根本不会帮助你映射到页表上,即使映射到页表上,映射到了非法的内存,你也不对这块内存具有访问权限!!! 所以可以解释一种现象,有的时候你写非法代码系统竟然允许,有的时候会崩溃。因为允许的时候,你映射的非法内存的写权限和读权限被允许,崩溃的时候则是权限不够,被系统pass掉了。而所谓的只读,只写,就是页表规定的。
管理进程地址空间
系统中一般都有多个进程,也就会有多个进程地址空间,那么这些数据需要被管理。怎么管理呢?当然是先描述,再组织。上面说过进程地址空间不是内存,它实际上也是结构体!! 名字叫做struct mm_struct。而且我们知道进程地址空间是分段的,堆区,栈区等等。那么体现在结构体中是怎样的呢?在linux中mm_struct的源码中,是按照范围划分的。比如一段进程地址空间大小为100,那么我规定好代码段的begin和end就好了。比如我规定begin是0,end是50,那么代码段的范围就是[0,50].
mm_struct{
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
}
可以看到源码中的描述方式就是使用整数描述了各段的开始和结束,这样就划分出不同的区域供使用。申请空间的本质是向内存索要空间,得到物理内存,然后在找到未使用的虚拟地址,建立映射关系,返回虚拟地址即可。 这就像大学里上课,老师就是pcb,学生在教室里可以随便坐,但是每次老师点名用的名单不变,名单就相当于进程地址空间,而物理内存就是教室。老师并不关系你坐在哪,我只要知道名单就够了,然后按照顺序点名。
其他的进程信息
内存指针:pcb中的内存指针可以理解为指向内存中该进程代码的指针,帮助pcb找到代码。 程序计数器:在pcb中,主要用来记录下一条指令的地址。 上下文数据:一个进程在cpu中运行时,会产生大量临时数据,当这个进程的时间片结束或者资源被抢占时,需要存储临时数据,这些上下文数据就存在pcb中。
总结
进程就是task_struct + 程序的数据&&代码 + mm_struct和页表。task_struct中包含了进程的各种信息,当进程被加载到内存中,它的task_struct被创建出来加入操作系统的管理队列,然后分配内存的时候mm_struct和页表也被创建,方便和安全的对内存管理。
|