1. 什么是进程地址空间(what)
在我们之前的博文中,画过很多次这个图,我们当时说的是内存中的分布情况,但是实际上它并不是所谓内存上的东西,它有一个自己的名字叫做进程地址空间。
首先来一段代码感受一下:
#include <stdio.h>
#include <unistd.h>
int global_value = 100;
int main()
{
printf("global_value=%d\n", global_value);
printf("--------------创建子进程-------------------\n");
pid_t id = fork();
if(id > 0)
{
//parent
sleep(3);//父进程首先等待3秒
printf("parent->pid:%d, global_value=%d, &global_value=%p\n", getpid(), global_value, &global_value);
}
else if(id == 0)
{
//child
global_value = 200;
printf(" child->pid:%d, global_value=%d, &global_value=%p\n", getpid(), global_value, &global_value);
}
}
可以看到首先打印的是创建子进程前的global_value的值是100,没有问题,然后创建子进程后,父进程sleep,子进程将global_value修改成200然后打印,3秒后父进程打印global_value,这里父子进程的global_value的值不一样,为什么呢?这个原因我们可以用进程的独立性来解释。再往后看,父子进程的global_value的地址是相同的!!!
但是多进程在读取同一个地址的时候怎么可能出现不同的结果,这个现象说明了这里的地址并不是真正的地址
这里的地址是虚拟地址(线性地址),在Linux内也叫逻辑地址
实际上,操作系统为每一个进程都创建了一个虚拟的进程地址空间,然后通过页表结构将虚拟进程地址空间和物理内存联系起来(映射)。我们在使用内存的时候,获取到的地址只能是虚拟地址,当在访问虚拟地址的时候,操作系统会根据虚拟地址通过页表找到对应的物理内存,从而获取到对应的数据
2. 为什么要有进程地址空间(why)
上文我们了解了进程地址空间,那么为什么要搞出来这样一个概念呢?
- 进程地址空间保证了数据的安全
- 地址空间的存在保证了进程的独立性
- 让进程以统一的视角,看待进程对应的代码和数据各个区域,方便编译器也以统一的视角来进行编译代码
3. 进程地址空间是怎么处理的(how)
根据上文中的知识,我们知道:OS会为每一个进程创建一个进程地址空间;但是OS内部存在着很多进程,所以为了保证这些进程都能够正常运行,OS需要将这些进程进行管理。
那么如何管理呢?看过博主之前文章的童鞋们应该能够轻松拿捏这个问题:管理的本质就是先描述,再组织。
所以首先对这个进程地址空间进行描述,我们可以把进程地址空间抽象成一个线性的数组,然后通过描述每一块的起始地址和结束地址来维护一这块空间。也就是下图的这种形式
在Linux源码里面我们能够找到一个叫做mm_struct
的结构体,这个结构体就是用来描述进程地址空间的结构体。先描述
同时,在我们之前提到的进程的描述结构体task_struct
中能够找到一个mm_struct类型的指针mm
,用于管理这个进程地址空间。再组织
每个进程被创建时,其对应的进程控制块(task_struct)和进程地址空间(mm_struct)也会随之被创建。而操作系统可以通过进程的task_struct找到其mm_struct。根据上文中提到的进程地址空间的描述模型,我们想要调整其中每一块的大小,本质上,调整task_struct->mm
指向的相应的块的起始和结束的数值即可。
补充:今天讲的进程地址空间其实只将了一部分,其中还有很多比较复杂的细节我们没有涉及,比如页表分级、缺页、命中等等,这部分内容我们会在后面学习文件系统以及多线程的时候慢慢补充。
本节完…