前言: 在现代操作系统中,进程的内存管理至关重要。操作系统通过虚拟地址空间来隔离不同进程的内存,确保它们不互相干扰,同时也能够高效地管理有限的物理内存资源。在本文中,我们将详细探讨虚拟地址空间、进程地址空间的概念,并通过具体代码示例帮助理解。
当我们在学习编程语言(如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;
}code addr: 0x40055d
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x1791010
heap addr: 0x1791030
heap addr: 0x1791050
heap addr: 0x1791070
test static addr: 0x601038
stack addr: 0x7ffd0f9a4368
stack addr: 0x7ffd0f9a4360
stack addr: 0x7ffd0f9a4358
stack addr: 0x7ffd0f9a4350
read only string addr: 0x400800
argv[0]: 0x7ffd0f9a4811
env[0]: 0x7ffd0f9a4819
env[1]: 0x7ffd0f9a482e
env[2]: 0x7ffd0f9a4845
env[3]: 0x7ffd0f9a4850
env[4]: 0x7ffd0f9a4860
env[5]: 0x7ffd0f9a486e
env[6]: 0x7ffd0f9a4892
env[7]: 0x7ffd0f9a48a5
env[8]: 0x7ffd0f9a48ae
env[9]: 0x7ffd0f9a48f1
env[10]: 0x7ffd0f9a4e8d
env[11]: 0x7ffd0f9a4ea6
env[12]: 0x7ffd0f9a4f00
env[13]: 0x7ffd0f9a4f13
env[14]: 0x7ffd0f9a4f24
env[15]: 0x7ffd0f9a4f3b
env[16]: 0x7ffd0f9a4f43
env[17]: 0x7ffd0f9a4f52
env[18]: 0x7ffd0f9a4f5e
env[19]: 0x7ffd0f9a4f93
env[20]: 0x7ffd0f9a4fb6
env[21]: 0x7ffd0f9a4fd5
env[22]: 0x7ffd0f9a4fdf通过这些输出,可以看到不同内存区域(如代码段、数据段、堆、栈和只读数据段)的地址分布和特点。堆的地址通常递增,栈的地址则通常递减,静态变量和全局变量则存储在数据段。
在进程中,每个变量和内存段都有一个虚拟地址。虚拟地址并不直接对应物理内存中的地址,而是通过操作系统的地址映射机制转化为物理地址。
虚拟地址示例:进程间的地址映射 通过fork()函数,我们可以创建一个子进程。子进程会复制父进程的内存空间,但由于虚拟地址的存在,它们看到的地址相同,但物理地址不同。这是因为操作系统为每个进程分配了独立的虚拟地址空间。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,⽗⼦并没有对变量进⾏进⾏任何修改。
可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0)
{ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:
OS必须负责将 虚拟地址 转化成 物理地址 。
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:

上⾯的图就⾜矣说明问题,同⼀个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
每个进程在操作系统中都有自己的地址空间,这个地址空间由多个虚拟内存区域组成。操作系统通过mm_struct结构来管理进程的虚拟内存空间。
mm_struct包含以下关键字段:
以下是mm_struct的结构定义:
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
unsigned long task_size;
unsigned long start_code, end_code;
unsigned long start_data, end_data;
unsigned long start_brk, brk;
unsigned long start_stack;
unsigned long arg_start, arg_end;
unsigned long env_start, env_end;
};可以说,mm_struct结构是对整个⽤⼾空间的描述。每⼀个进程都会有⾃⼰独⽴的mm_struct,这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。先来看看由task_struct到mm_struct,进程的地址空间的分布情况:

定位mm_struct⽂件所在位置和task_struct所在路径是⼀样的,不过他们所在⽂件是不⼀样的,mm_struct所在的⽂件是mm_types.h。
那既然每⼀个进程都会有⾃⼰独⽴的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的!虚拟空间的组织⽅式有两种:
vm_area_struct是描述虚拟内存区域(VMA)的结构。每个VMA表示进程的一个内存区域,如代码段、数据段、堆等。


这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
下面一个小故事来进行描述:
在一个古老的计算机世界里,有两位程序A和B,它们都急切地等待着能够在内存中找到属于自己的位置。那时,计算机的内存就像一个巨大的仓库,程序们必须在这个仓库中找到合适的空间才能开始工作。 这台计算机的内存总共只有128MB,程序A和程序B各自需要自己的地方。程序A是一个小巧的任务,它只需要10MB的内存,而程序B则需要一个庞大的空间,要求高达110MB的内存。为了让它们顺利运行,操作系统就像是一位聪明的管理员,开始安排它们的宿位。 首先,管理员决定将仓库的前10MB腾给程序A,让它可以在这里忙碌。程序A在自己的空间里工作得非常高效,没有被其他程序打扰。接着,剩余的118MB空间就成了程序B的家,操作系统小心翼翼地为程序B分配了110MB的内存。程序B也开始了它的工作,虽然它需要的空间更大,但由于操作系统的精心安排,程序B能够在剩余的空间中毫不费力地运行。 两个程序在这台计算机的内存中各自找到了合适的位置,操作系统巧妙地管理着内存,确保它们都能顺利地完成自己的任务。就这样,两个程序在各自的空间里忙碌着,计算机的内存得到了高效的利用,而它们也完成了自己的使命。

这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。
存在这么多问题,有了虚拟地址空间和分⻚机制就能解决了吗?当然!