前言
程序地址空间
在之前,我们学习C/C++
时,多多少少都看过这样的一张图
我们现在通过下面这一段代码看一下:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
static int test = 10;
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}

通过观察我们可以发现,我们程序内的地址确实是符合图片中的地址区域划分的。
补充:
- 在
32
为机器下程序地址空间的大小为2^32
字节,也就是4GB
。
虚拟地址
这里我们思考一个问题,程序地址空间是内存吗?
我们先来看一下代码:
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
int x = 0;
int id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
//子进程
while(1){
printf("子进程, x = %-3d, &x = %p\n",x,&x);
sleep(1);
x+=10;
}
}
else{
//父进程
while(1){
printf("父进程, x = %-3d, &x = %p\n",x,&x);
sleep(1);
}
}
return 0;
}
这里我们让父子进程同时执行,然后子进程每次执行x+=10
;父子进程都输出x
的值和x
的地址。

我们惊奇的发现,父子进程输出的
x
的值不一样,但是x
的地址却是一样的!!!所以很显然,这里的程序地址空间不是内存。(如果是内存,同一个地址为什么会输出不同的值?)
- 这里变量内容不一样,所以我们父子进程输出的一定不是同一个变量
- 地址一样,所以这里的地址一定不是指的内存(物理地址)
- 在
Linux
中,这种地址称为虚拟地址
所以,我们之前在代码中使用&
操作,取到的都是虚拟地址;
而物理地址,我们是看不到的,它由操作系统管理起来。
而我们的操作系统OS
可以通过虚拟地址找到对应的物理地址。
进程地址空间
其实,我们之前的程序地址空间
,它是从语言层面去理解的;它本质上是进程地址空间
。
简单来说,进程地址空间就是操作系统为每一个进程分配额的一个
虚拟内存视图
,它让每一个进程都以为自己独占了整个计算机的内存资源。
用现在通俗的话来说,进程地址空间就是操作系统给每一个进程地址空间画的大饼,给每一个进程说,内存资源都是你的,让它以为它自己占用了所有的内存资源。
- 一个进程,一个进程地址空间
- 一个进程,一套页表
我们知道在内存中存在非常多的进程,那么多进程,每一个进程都有一个进程地址空间;如此多的进程地址空间,操作系统是不是也要将其管理起来呢?
答案是的,管理这些进程地址空间:先描述、后组织
在Linux
内核中,进程地址空间本质上就是一个结构体对象mm_struct
;
那也就是说,在进程当中还存在一个进程地址空间,我们在语言层面用到的地址都是虚拟地址,而操作系统可以通过虚拟地址,找到对应的物理地址。

页表
在上面描述中,提到了一个进程,一套页表;那页表指的是什么呢?
我们知道,操作系统要能够通过虚拟地址找到对应的物理地址,那如何去找呢?(就比如父子进程中x
的虚拟地址是一样的,但值不一样就说明找到的物理地址不一样)
页表本质上就是虚拟地址
和物理地址
的映射关系,它里面存储了虚拟地址和其对应的物理地址(除此之外,还存储了其他内容,例如权限)
那对于不同的进程,相同的虚拟地址能够找到不同的物理地址,也就是说两个进程中的虚拟地址和物理地址对应关系是不一样的。
所以,一个进程,一套页表
写时拷贝
了解了进程地址空间和页表之后,我们再回过头看上面的问题:父子进程x
的值不相同,但虚拟地址是相同的
我们知道一个进程一个进程地址空间,一个进程对应一套页表;
在父进程创建子进程时,子进程也会将父进程的task_struct
大部分数据拷贝到子进程的task_atruct
(部分数据会修改);也会将父进程的进程地址空间连同页表进行浅拷贝;这样父子进程就公共代码和数据
而当我们子进程进行修改数据时,(操作系统不让子进程在原数据上修改,而是给子进程开辟一块新的内存,然后将子进程页表的对应关系修改了,也就是将页表中对应的物理内存修改为新开辟的这一块空间)。
那这样,父进程在访问时,操作系统通过页表找到对应的物理地址与子进程在访问时操作系统通过页表找到对应的物理地址就不相同了

管理进程地址空间
我们知道了每一个进程都有对应的进程地址空间,那操作系统是如何将进程地址空间管理起来的呢?
在Linux
下,描述进程地址空间所有信息的结构体是mm_struct
;每一个进程都有一个mm_struct
结构;在进程的task_struct
中都存在执行进程对应mm_struct
的指针。
理解区域划分
我们观察进程地址空间的图,我们可以发现有正文代码、初始化数据、未初始化数据、堆、栈等非常多的区域;那操作系统是如何价格进程地址空间划分成如此多的区域呢?
这里就好比我们小时候,与同桌之间画的
三八线
;它将桌子划分成了两个区域,站在计算机的角度去理解就是:整个桌子的区域
[0,99]
,三八线
将桌子划分成了两个区域[0,49]
和[50,99]
;你的区域是[0,49]
,你同桌的区域是[50,99]
;我们只需要知道你区域的起始位置和结束位置,同桌的起始位置和结束位置,就可以将桌子划分;
在这里也是一样,我们是不是只需要知道每一个区域的起始位置和终止位置,那就可以很好的将进程地址空间划分成非常多的区域。
类似于下图这样:

在Linux
内核中,我们可以查看mm_struct
的结构:
c
struct mm_struct
{
//...
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
//...
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
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;
}
通过查看我们可以发现:在其中还存在一个rb_root
和struct vm_area_struct*
类型的mmap
:
- 当虚拟区间少时采用单链表,由
mmap
指针指向这个链表。 - 当虚拟区间多时采用红黑树管理,由
mm_rt
指向这颗树。
虚拟区间
我们在malloc
时,在堆上开辟空间,那堆区不应该只有一个吧?那也就是不止存在一个虚拟空间起始位置吧?
所以在Linux
操作系统中还存在vm_area_struct
,它表示一个独立的虚拟内存区域,因为每个不同的虚拟内存区域功能和内部机制都不相同,所以要使用多个vm_area_struct
来区分不同类型的虚拟内存区域。
上面意思呢?
简单来说就是,将进程地址空间中
mm_struct
是每一个区域再进行细致划分,划分成一个个虚拟区,然后将虚拟区间管理起来;在
mm_struct
内核源代码中,存在的mmap
和mm_rb
就是管理虚拟区的。
那操作系统管理进程地址空间就可以形象化为下面图片所示:


当然在mm_struct
中也存在每一个区域的开始和结束位置,而vm_area_struct
中表示的是更加细致的区域划分。
进程地址空间的作用
- 将无序的地址,变成有序的
这个就非常好理解了,物理内存空间它不一定是连续的,通过进程地址空间的虚拟地址,将这些无序的地址转化成有序的地址;然后将虚拟地址提供为上层用户使用
- 地址转化的过程中,对我们的地址进行合法性判定,从而保护物理内存
当我们进行野指针访问时,操作系统在页表中查询,没有查询到,操作系统就直接告诉我们野指针访问,从而保护物理内存
还用一种就是当我们修改只有只读权限的内容时,操作系统在页表中查询到以后,发现我们只具有只读权限,而我们要进行写入数据,操作需要就会报错;
这里写时拷贝也是如此,当我们子进程要修改数据时,操作系统会发现子进程只具有只读权限,就会报错,然后给子进程开辟新的空间,重新映射页表,然后修改权限为可写可读。
本篇文章到这里就结束了,感谢支持
简单总结:
- 理解进程地址空间是什么
- 理解写时拷贝
- 区域划分和虚拟区间