【Linux】程序地址空间(是什么?为什么?)

📅 发布时间:2026/7/11 14:56:21 👁️ 浏览次数:
【Linux】程序地址空间(是什么?为什么?)
一、程序地址空间回顾我们在学习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: 0x400416 init global addr: 0x403020 uninit global addr: 0x40302c heap addr: 0x171e96b0 heap addr: 0x171e96d0 heap addr: 0x171e96f0 heap addr: 0x171e9710 test static addr: 0x403024 stack addr: 0x7ffc76d2b388 stack addr: 0x7ffc76d2b380 stack addr: 0x7ffc76d2b378 stack addr: 0x7ffc76d2b370 read only string addr: 0x40185c argv[0]: 0x7ffc76d2d580 env[0]: 0x7ffc76d2d587 env[1]: 0x7ffc76d2d597 env[2]: 0x7ffc76d2d5ae env[3]: 0x7ffc76d2d5ca env[4]: 0x7ffc76d2d5d8 env[5]: 0x7ffc76d2d5ef env[6]: 0x7ffc76d2d5fb env[7]: 0x7ffc76d2d610 env[8]: 0x7ffc76d2d61f env[9]: 0x7ffc76d2d62e env[10]: 0x7ffc76d2d63f env[11]: 0x7ffc76d2dd58 env[12]: 0x7ffc76d2dd87 env[13]: 0x7ffc76d2dd9e env[14]: 0x7ffc76d2dda9 env[15]: 0x7ffc76d2ddcc env[16]: 0x7ffc76d2ddd5 env[17]: 0x7ffc76d2ddfd env[18]: 0x7ffc76d2de05 env[19]: 0x7ffc76d2de1a env[20]: 0x7ffc76d2de36 env[21]: 0x7ffc76d2de58 env[22]: 0x7ffc76d2de71 env[23]: 0x7ffc76d2dee6 env[24]: 0x7ffc76d2df19 env[25]: 0x7ffc76d2df33 env[26]: 0x7ffc76d2df46 env[27]: 0x7ffc76d2df57 env[28]: 0x7ffc76d2dfe8二、虚拟地址我们之前在进程概念的学习中创建过子进程那我们刚好可以观察一下当子进程修改某一共享变量时父子进程读取到的该变量的值是否会发生改变该变量的地址又呈现出什么样的内容#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 }我们发现输出出来的变量值和地址是⼀模⼀样的很好理解呀因为⼦进程按照⽗进程为模版⽗ ⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:int main() { //子进程修改value值的情况 pid_t idfork(); if(id0) { perror(fork()); } else if(id0) { g_val100; printf(child[%d]:%d:%p\n,getpid(),g_val,g_val); } else { printf(parent[%d]:%d:%p\n,getpid(),g_val,g_val); } return 0; }我们发现⽗⼦进程输出地址是⼀致的但是变量内容不⼀样变量内容不一样说明父子进程输出的变量一定不是同一个变量但地址是一样的说明此地址一定不是物理地址在Linux地址下这种地址是虚拟地址。我们在⽤C/C语⾔所看到的地址全部都是虚拟地址物理地址⽤⼾⼀概看不到由OS统⼀ 管理OS必须负责将 虚拟地址转化成 物理地址。三、进程地址空间所以我们意识到开头最先看的图片不是物理内存分布图而是进程空间分布图。3.1分页、虚拟地址空间这个图足矣说明问题同一个变量地址相同其实都是虚拟地址相同内容不同其实是因为被映射到不同的物理地址。虚拟地址空间是个结构体变量页表承担着虚拟地址和物理地址映射的作用3.2写时拷贝这里还有个写时拷贝的现象子进程继承了父进程的g_val变量如果子进程不修改这个变量值那么它们共享这个变量g_val在同一个物理位置如果子进程要改变变量的值这时会在物理内存上拷贝一份g_val变量供子进程使用这就是写时拷贝现象通常⽗⼦代码共享⽗⼦再不写⼊时数据也是共享的当任意⼀⽅试图写⼊便以写时拷⻉的方式各⾃⼀份副本。如图因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证!​ 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率。四、虚拟内存管理linux下进程的地址空间的所有信息的结构体是mm_struct内存描述符每个进程只有一个mm_struct在每个进程的task_struct结构中有一个指向mm_struct的结构体指针。倘若我们遇到堆区的动态开辟空间呢可能一个程序在堆区上会malloc很多次地址不同区域不同会被分成很多份这时候一个start和end指针能管理过来吗其实也无需担心linux内核会使用vm_area_struct结构来表示一个独立的虚拟内存区域VMA再采用单链表或者红黑树的方式vm_area_struct结构来连接各个VMA方便进程快速访问。五、为什么要有虚拟地址空间呢1.要保护物理内存如果每个进程都可以访问任意的内存空间这也就意味着任意进程都可去读写系统相关内存区域这是高危的行为2.因为有地址空间的存在和页表的映射的存在我们的物理内存中可以对未来的数据进行任意位置的加载物理内存的分配和进程的管理就没有关系了进程管理模块和内存管理模块就完成了解耦合。注所以我们之前在new和malloc空间时其实是在虚拟地址空间上申请的物理内存可以甚至一个字节都不给你而当你真正需要对物理地址空间进行访问时才执行相关的管理算法来帮你申请内存构建页表映射关系做到延时分配而这个操作是由操作系统自动完成的用户0感知缺页中断时其实就是在动态内存分配3.因为有页表的映射存在程序在物理内存中理论上可以在任意位置加载它可以将地址空间上的虚拟地址和物理地址进行映射在进程视角所有的内存分布都可以是有序的这里再补充一个进程挂起的小知识进程挂起其实就是找到页表把页表右侧的物理地址清除然后把代码数据换出到磁盘的指定区域。