目录
进程地址空间
验证内存空间布局
学习 C 语言时,可能对“内存空间布局”有所了解:

接下来写几行代码来验证:
#include <stdio.h>
#include <stdlib.h>
int g_val_1;
int g_val_2 = 10;
int main()
{
printf("code addr: %p\n", main); // 代码区地址
const char* str = "Hello world";
printf("read only string addr: %p\n", str);
printf("init global value addr: %p\n", &g_val_2); // 已初始化全局变量地址
printf("uninit global value addr: %p\n", &g_val_2); // 未初始化全局变量地址
char* mem = (char*)malloc(100);
printf("heap addr: %p\n", mem); // 堆地址
printf("stack addr: %p\n", &str); // 栈地址
return 0;
}
输出结果:
code addr: 0x40057d
read only string addr: 0x4006bf
init global value addr: 0x60103c
uninit global value addr: 0x60103c
heap addr: 0x1b57010
stack addr: 0x7ffd396919e0
通过比较地址的大小,发现确实符合上图所示的内存空间布局。
我们还知道,栈区向内存地址减小的方向生长,堆区向内存地址增大的方向生长,继续写代码验证
int main()
{
int a;
int b;
int c;
int* p1 = (int*)malloc(100);
int* p2 = (int*)malloc(100);
int* p3 = (int*)malloc(100);
printf("a addr: %p\n", &a);
printf("b addr: %p\n", &b);
printf("c addr: %p\n", &c);
printf("p1 addr: %p\n", p1);
printf("p2 addr: %p\n", p2);
printf("p3 addr: %p\n", p3);
return 0;
}
输出结果:
a addr: 0x7fff67504814
b addr: 0x7fff67504810
c addr: 0x7fff6750480c
p1 addr: 0x1ce1010
p2 addr: 0x1ce1080
p3 addr: 0x1ce10f0
从结果来看,确实如此
我们还知道,static 变量不会随着函数栈帧的销毁而被释放
int g_val_2 = 10;
int main()
{
static int a;
printf("a addr: %p\n", &a);
printf("g_val_2 addr: %p\n", &g_val_2);
return 0;
}
输出结果:
a addr: 0x60103c
g_val_2 addr: 0x601034
发现 static 变量在内存的存储地址与全局变量的存储地址非常近,static 修饰的变量,编译时已经被编译到全局数据区。
问题引入
运行以下代码观察结果:
#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
我们发现,父子进程的全局变量,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在 Linux 地址下,这种地址叫做 虚拟地址(线性地址)
- 我们在用 C/C++ 、java 等任何编程语言(包括汇编语言,甚至二进制程序)所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
- OS必须负责将 虚拟地址 转化成 物理地址 。
物理地址和虚拟地址

一个进程由 PCB 和它的代码和数据组成,其中“代码和数据” 就是进程的地址空间,进程的 PCB 会有一个指针指向这块空间。在进程的地址空间和物理内存之间,还有一个叫“页表”的结构,它是一种 key-value 结构,存储了虚拟地址和物理地址的映射关系。在子进程继承父进程的代码和数据的同时,父进程的进程的地址空间、页表会拷贝给子进程,但这种拷贝只是浅拷贝,子进程的页表的物理地址和虚拟地址与父进程完全相同。当子进程要修改从父进程继承的某个变量时,操作系统发现该变量的物理地址已被占用,所以会另外寻找一块空间,存储这个变量,这个过程叫写时拷贝,由操作系统自动完成。操作系统在完成写时拷贝时,不会修改任何虚拟地址,所以子进程继承父进程的变量的虚拟地址不会改变。
上面的描述十分粗浅,还有一些细节问题:
1、地址空间究竟是什么?
在 32 位计算机中,有 32 位的地址和数据总线,每一根线可以表示 0 或 1 两种状态(实际是向设备地址寄存器充放电的过程,高电平表示 1,低电平表示 0)。32 根就可以表示 32 位 bit 位,把这 32 位 bit 位组合的每个状态映射到一个字节,就能表示 2 ^32 byte = 4GB,地址空间就是指可以表示和访问地址范围,在 32 位的机器就是 [0,2 ^32] 。而进程地址空间就是指一个进程可以访问的地址范围。不同的进程的地址空间(用 start 和 end 表示)划分到不同的地方,而每个进程的地址空间又可以被划分成:代码区、全局数据区......。地址空间本质是操作系统内核的一个数据结构对象,类型 PCB 一样,也要被操作系统管理,进程的 PCB 可以指向该数据结构对象,
2、进程地址空间是什么?
进程地址空间就是每个进程都认为自己独自占有所有内存资源,即使某个进程申请使用所有内存资源,被操作系统拒绝后,仍然如此认为。因为一个进程不可能真的独自占有所有内存资源,即使想,操作系统也不允许。进程地址空间本质是操作系统内核的一个数据结构对象 (struct mm_struct),类型 PCB 一样,也要被操作系统管理,进程的 PCB 可以指向该数据结构对象,该数据结构对象内记录了代码区、全局数据区......等等区域的开始和结束的地址

3、为什么要有页表存储虚拟内存和物理内存的映射关系?
在进程和内存之间,增加了虚拟内存这一间接性,这样做是为了
1、让进程以统一的视角看待内存。如果没有虚拟内存,进程直接访问内存,那么进程就必须在自己的 PCB 里记录自己的代码、数据在物理内存的地址,增加了冗余,并且进程的代码和数据加载到内存的不同位置,这些位置可能完全是混乱的,而如果有了进程地址空间,每个进程都认为内存空间布局是:从低地址到高地址依次为:代码区、字符常量区...... 然后通过页表映射到物理内存,根本不用关心代码和数据在物理内存是否有序。
2、避免进程越界或越权操作。如果进程想访问内存的某个资源,但该资源是其他进程的资源,或者该资源是只读的现在你要修改(在页表内,不仅存储了虚拟地址和物理地址的映射关系,还存储了该地址的访问权限),这些操作就会被提前拦截。(应该注意,物理内存是没有权限的概念的,可以在物理内存的任何地址读或写)。现在,我们不仅可以就语法层面说明进程的代码和字符常量是只读的,还可以就操作系统的层面解释为什么:因为代码区和字符常量区的虚拟内存地址在页表都标记为只读。
3、因为有进程地址空间和页表的存在,可以将进程管理模块和内存管理模块进行解耦合
补充知识:
在 CPU 内,有 cr3 寄存器,它保存了进程对应的页表的起始地址,当进程不在 CPU 上运行时,进程会将 cr3 寄存器的内容打包带走,下一次运行时,再将该值赋值给 cr3 寄存器(页表的起始地址本质属于进程的硬件上下文)
某个游戏在磁盘中占几十个G,但内存只有 4G,为什么该游戏仍然可以在操作系统上运行?我们先建立一个共识:现代操作系统,几乎不做任何浪费时间和空间的事情。操作系统可以实现对大文件的分批加载,但是仍然有问题:加载了 500M 代码和数据,实际只有 5M 的代码和数据在运行,浪费了内存 495M 的空间,为了解决这个问题,操作系统实现了惰性加载的方式:在进程的页表,还有一个数据项,该数据项记录了虚拟内存的代码和数据是否被加载到了内存。现在,进程要执行某行代码,在页表中如果记录了它已经被加载到了内存,那么根据页表的物理地址直接访问内存,如果没有加载到内存,会触发缺页中断,进程会去磁盘找到对应的代码,加载到内存,在页表里填入对应的物理内存的地址,然后再执行。
现在,对于进程是什么有了更全面的认识:
进程 = 内核数据结构(task_struct && mm_struct && 页表)+ 代码和数据

2020

被折叠的 条评论
为什么被折叠?



