目录
注:学习背景为32位平台
一、程序地址空间回顾
在之前学习C/C++时,可以知道C/C++程序在运行时内存会被划分为如下几块区域:
在学习了进程的一些基本概念后,执行下面的代码:
#include <stdio.h>
#include <unistd.h>
int global_val = 100;
int main()
{
// 创建一个子进程
pid_t id = fork();
if (id < 0)
{
// 子进程创建失败,不玩了
printf("fork() error!\n");
return 1;
}
else if (id == 0)
{
int n = 0;
while (1)
{
printf("子进程:pid=%d, ppid=%d, global_val=%d, &global_val=%p\n", getpid(), getppid(), global_val, &global_val);
sleep(1);
if (n == 10)
{
global_val = 300;
printf("----------子进程的global_val修改为:%d----------\n", global_val);
}
++n;
}
}
else
{
while (1)
{
printf("父进程:pid=%d, ppid=%d, global_val=%d, &global_val=%p\n", getpid(), getppid(), global_val, &global_val);
sleep(2);
}
}
return 0;
}
代码运行结果如下:
在子进程的global_val被修改之前,子进程和父进程global_val有相同的值和地址,这很好理解,因为子进程在被创建时,会继承父进程的一些属性。
在子进程的global_val被修改之后,子进程和父进程global_val拥有不同的值,这也是可以理解的,因为两个进程的数据是独立的,但是最难理解的是,为什么同样的地址,取出来的是不同的值呢?
答案是:因为值不同,所以地址一定是不同的;
所以给出的地址是一个“假地址”,我们称之为“虚拟地址”;
操作系统负责将真实的物理地址和“虚拟地址”做出转化。
二、进程地址空间
用“程序地址空间”来描述会有一些不准确,更准确的是“进程地址空间”,因为只有程序在运行起来后,才会被分配内存,拥有自己的地址空间。
每一个程序运行起来,操作系统都会创建一个PCB(进程控制块)结构体来存储一些进程的基本信息做统一管理;每一个进程都有自己的地址空间,同样的,操作系统为了管理这些虚拟地址空间,也会创建一个结构体来管理这些虚拟地址空间。
在学习进程地址空间之前,要理解一些概念:
1、进程会认为它自己是独占所有地址空间的(实际上并不是);
2、地址空间的基本单位是字节Byte;
3、地址具有唯一性,即每个地址都标识唯一的一块空间;
4、在32位操作系统下,有32根地址线,可以表示0x00000000-0xFFFFFFFF共2^32次方个地址,也就是共有2^32 Byte = 2^22 KB = 2^12 MB = 2^2 GB,也就是4GB的空间可以被标识。
也就是说每一个进程都认为自己独享4GB的空间,结合本文最开始程序地址空间的划分,可以定义如下结构体:
struct mm_struct {
// 声明正文代码的虚拟地址起止空间
uint32_t code_start, code_end;
// 声明堆的虚拟地址起止空间
uint32_t heap_start, heap_end;
// 声明栈的虚拟地址起止空间
uint32_t stack_start, stack_end;
// ......
};
// 定义某个进程的虚拟地址空间
struct mm_struct process_A = {
0x10000000, 0x1FFFFFFF, // 正文代码的虚拟地址空间
0x22222222, 0x3FFFFFFF, // 堆的虚拟地址空间
0x55555555, 0x6FFFFFFF, // 栈的虚拟地址空间
// ......
};
这样,每一个进程都有自己的PCB,都有自己的虚拟地址空间,可以映射到物理内存,因为程序在运行过程中,通过malloc或者new向操作系统申请的空间不会太大(如果太大可以操作系统可以拒绝),所以虽然每个进程都认为自己独享4GB的内存空间,但它们实际使用的比这小很多。
同时,在虚拟地址到物理地址的映射过程中,由一个叫做页表的东西来控制(文件操作会详细解释)。每一个进程都有自己独立的页表。
三、为什么要有进程地址空间
1、安全性
如果让进程直接访问物理内存,如果越界、非法访问,会导致其他进程的代码被破坏、信息被泄漏;当进程需要访问内存的时候,先在进程地址空间访问,进程地址空间将请求提交给页表,页表判断如果是非法访问,就会拒绝请求。从而保证了其他程序的安全。
2、进程独立性
在本文最初的代码中,父进程创建了子进程后,global_value作为全局变量,是被父子进程共享的,也就是两个进程的global_value实际上是同一块物理内存空间。
但是在子进程要修改全局变量时,为了保证进程数据的独立性,操作系统就会把全局变量拷贝一份到一个新的物理地址空间,并修改页表,让虚拟地址与物理地址重新对应,然后再做修改,这种方式称为写时拷贝。
3、统一性
我们写好的C/C++程序,在经过编译预处理、编译、汇编、链接,最后生成可执行程序。那么,生成的可执行程序中一定是存在地址的!
举个例子,在链接的过程中,我们在代码中调用系统的库函数,就会转换成对应的函数的地址。
而这个地址,很明显不可能是物理地址,所以它一定就是虚拟地址。
所以,虚拟地址的存在,可以让编译器以统一的视角来编译代码。