Linux进程虚拟地址空间:从实验到内核结构
前言每一个程序都以为自己独占一段内存空间这个错觉是谁给的同时为什么创建子进程之后父子进程打印出相同的地址值但是值又是不相同的。这些问题就要通过今天的学习便可一一破解。一、进程地址空间1、进程地址空间图2、虚拟地址空间的直接证明int main(){int val0;pid_t idfork();if(id0){val100;printf(child[%d], val:%d val:%p\n,getpid(),val,val);}else if(id0){sleep(1);printf(parent[%d], val:%d val:%p\n,getpid(),val,val);}return 0;}可以看到的是父子进程地址一样但是值却不一样的情况那么也就证明了这个地址是虚拟地址而并不是真实的物理地址并且拥有各自的地址通过某种方式去映射到了同一个虚拟地址空间。二、进程虚拟地址空间全景图1、代码验证通过上边的图片可以看到虚拟地址空间的全景图此时我们通过程序将地址打印出来看看地址的分布情况。#include stdio.h#include stdlib.hint g_unval; // 未初始化全局变量 - BSSint 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;}jilu可以得到这个地址与上边的实景图大致相符。2、深入问题为什么栈和堆要相向而生通过图片可以看到堆和栈中间有一个共享区此时的话栈向下生长堆向上生长此时就可以最大化的共享中间区域并且可以动态的调整。也就是说最大的利用空间。三、虚拟地址和物理地址页表和映射1、简述虚拟地址的由来程序编译链接时会为代码段数据段等分配逻辑地址。当程序被加载到内存后这个逻辑地址就是虚拟地址。当CPU处理一个虚拟地址时1将虚拟地址拆分成虚拟页号(高位)以及页内偏移(低位)。2拿着虚拟页号当作数组下标去页表里找到对应的记录。3检查权限如果有效位为0则触发缺页中断则操作系统会去磁盘里获取数据4得到的记录与页内偏移量拼接后就是所得到的物理地址。2、缺页中断按需分配的核心当进程调用malloc时只是将堆顶上的指针向高处移动了而已并没有给malloc扩容分配新的物理页当真正写入访问的时候才会去分配新的物理页并建立映射。指令 : pamp -x PID -这个指令可以拆开每一段看堆栈的大小。验证惰性分配实验int main(){char *pmalloc(100*1024*1024);printf(p%p \n,p);getchar();for(int i0;i100*1024*1024;i4096){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。只有真正访问写入时才会申请物理空间。程序视角下内存是连续的但是物理空间可能散布各处。后续更新进程控制