前言
每一个程序都以为自己独占一段内存空间,这个错觉是谁给的,同时,为什么创建子进程之后父子进程打印出相同的地址值但是值又是不相同的。这些问题就要通过今天的学习便可一一破解。
一、进程地址空间
1、进程地址空间图
2、虚拟地址空间的直接证明
int main()
{
int val=0;
pid_t id=fork();
if(id==0)
{
val=100;
printf("child[%d], val:%d &val:%p\n",getpid(),val,&val);
}
else if(id>0)
{
sleep(1);
printf("parent[%d], val:%d &val:%p\n",getpid(),val,&val);
}
return 0;
}
可以看到的是父子进程地址一样但是值却不一样的情况,那么也就证明了这个地址是虚拟地址而并不是真实的物理地址,并且拥有各自的地址,通过某种方式去映射到了同一个虚拟地址空间。
二、进程虚拟地址空间全景图
1、代码验证
通过上边的图片可以看到虚拟地址空间的全景图,此时我们通过程序将地址打印出来,看看地址的分布情况。
#include <stdio.h>
#include <stdlib.h>
int g_unval; // 未初始化全局变量 -> BSS
int g_val = 100; // 初始化全局变量 -> 数据段
int main(int argc, char *argv[], char *env[])
{
const char *str = "helloworld"; // 字符串常量 -> 只读数据区(文本段附近)
static int test = 10; // 初始化静态变量 -> 数据段
char *heap1 = malloc(10); // 堆
char *heap2 = malloc(10);
int stack_var; // 栈(注意取地址方式)
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
printf("static addr: %p\n", &test);
printf("string literal addr: %p\n", str);
printf("heap1 addr: %p\n", heap1);
printf("heap2 addr: %p\n", heap2);
printf("stack var addr: %p\n", &stack_var);
printf("argv[0]: %p\n", argv[0]);
printf("env[0]: %p\n", env[0]);
return 0;
}ji'lu
可以得到这个地址与上边的实景图大致相符。
2、深入问题:为什么栈和堆要相向而生
通过图片可以看到,堆和栈中间有一个共享区,此时的话栈向下生长,堆向上生长,此时就可以最大化的共享中间区域,并且可以动态的调整。也就是说最大的利用空间。
三、虚拟地址和物理地址:页表和映射
1、简述虚拟地址的由来,程序编译链接时,会为代码段,数据段等分配逻辑地址。当程序被加载到内存后,这个逻辑地址就是虚拟地址。
当CPU处理一个虚拟地址时
(1)将虚拟地址拆分成虚拟页号(高位)以及页内偏移(低位)。
(2)拿着虚拟页号当作数组下标,去页表里找到对应的记录。
(3)检查权限,如果有效位为0则触发缺页中断,则操作系统会去磁盘里获取数据
(4)得到的记录与页内偏移量拼接后就是所得到的物理地址。
2、缺页中断:按需分配的核心
当进程调用malloc时,只是将堆顶上的指针向高处移动了而已,并没有给malloc扩容分配新的物理页,当真正写入访问的时候才会去分配新的物理页并建立映射。
指令 : pamp -x PID <-这个指令可以拆开每一段,看堆栈的大小。
验证惰性分配实验:
int main()
{
char *p=malloc(100*1024*1024);
printf("p=%p \n",p);
getchar();
for(int i=0;i<100*1024*1024;i+=4096)
{
p[i]=0;
}
printf("accessed\n");
getchar();
return 0;
}
观察现象(注:kbytes是虚拟内存大小 RSS是物理内存)
当我们执行代码是第一个getchar之后的运行结果可以得到第一张图,可以发现虚拟内存大小是100mb,当执行第二个getchar之后得到的运行结果可以得到第二张图,可以发现虚拟内存大小和物理内存大小相等,也就是说当写入访问的时候才会去分配物理内存。
四、内核如何描述和管理地址空间
1、task_struct(PCB)与mm_struct
前文得知,进程中都会存在属于自己的PCB,这个PCB内部有一个指针指向mm_struct也就是内存描述符。
struct task_struct {
// ...
struct mm_struct *mm; // 用户空间内存描述符
struct mm_struct *active_mm; // 内核线程借用
// ...
};
mm_struct定义了用户地址空间布局,是管理虚拟内存的。但准确的来说不是虚拟内存本身
struct mm_struct {
// ...
unsigned long start_code, end_code; // 代码段起止
unsigned long start_data, end_data; // 数据段起止
unsigned long start_brk, brk; // 堆起止(brk是当前堆顶)
unsigned long start_stack; // 栈起始
unsigned long arg_start, arg_end; // 命令行参数区
unsigned long env_start, env_end; // 环境变量区
// ...
};
2、vm_area_struct每个区域都是独立个体
一个进程的地址空间可能包含多个虚拟内存区域(VMA),例如:
- 代码段一个VMA
- 数据段一个VMA
- 堆一个VMA
- 栈一个VMA
- 每个动态库一个VMA
内核用vm_area_struct来描述每个VMA
五、为什么要存在虚拟内存
1、安全
如果没有虚拟内存的话可能进程A通过指针去访问修改进程B的数据,如果存在虚拟内存的话每一个进程都只能访问自己的虚拟内存中页表所映射的物理空间。
2、效率
程序运行不需要把所有的数据放入内存之中,用到哪个就放哪个,甚至内存紧张时可以通过swap交换到磁盘中以此缓解内存紧张。
3、地址确定性
程序只需要知道虚拟内存地址就可以通过映射去访问真实的物理内存。
4、解耦与灵活性
malloc申请虚拟内存空间,物理内存只要不访问就可以是0。
只有真正访问写入时才会申请物理空间。
程序视角下,内存是连续的,但是物理空间可能散布各处。
后续
更新进程控制