进程的内存映像,只读区,可读写区,堆,共享库,栈详解
我们把一个进程的虚拟地址空间想象成一栋专门为它建造的摩天大楼。
- 虚拟地址空间:这栋摩天大楼的设计图纸。在32位系统上,图纸上规划了从0层到大约40亿层(4GB)的巨大空间。每个进程都拿到一份一模一样的图纸,它们都以为自己独占了整栋大楼。
- 物理内存:现实世界中真正可用的建筑材料。所有进程的“大楼”实际上都是用这些有限的材料搭建的。
摩天大楼的楼层规划(从低地址到高地址)
这栋4GB高的大楼,被严格地分成了两个大区:用户区(低3GB) 和 内核区(高1GB)。
内核区 (Kernel Space) - “顶层豪华总统套房” (高地址区域)
- 位置:大楼的最顶层,大约从3GB到4GB的位置。
- 住户:操作系统内核。
- 存放内容:
- 进程的“身份证” (PCB):每个租户(进程)的个人档案都存放在这里,由大楼管理员(OS)统一保管。
- 大楼的“管理规定” (OS代码):比如“租户调度规则”、“安保条例”等,都存放在这里。
- 大楼的“总地图” (页表):记录了每个租户的“设计图纸”是如何映射到“现实建材”上的。
- 权限:戒备森严。普通租户(用户进程)绝对禁止入内。只有在需要请求管理员服务时(通过系统调用),才能在管理员的陪同下进入一小部分指定区域。
用户区 (User Space) - “租户的私人生活空间” (低地址区域)
这是分配给每个进程自由使用的区域。它也被精心划分成了几个功能区。
1. 只读区(代码段 .text
+ 只读数据段 .rodata
) - “图书馆和展览馆”
- 位置:用户区的最底层。
- 存放内容:
- 代码段 (
.text
): 你写的C语言代码被编译成的机器指令(程序的“说明书”),就存放在这里。这些指令是规定好的,运行时不能被修改。 - 只读数据段 (
.rodata
): 存放常量。比如C语言里用const
关键字定义的全局变量,或者字符串字面量(如"Hello, World!"
)。它们就像展览馆里的展品,只能看,不能摸(不能修改)。
- 代码段 (
- 宏定义 vs. const常量:
#define PI 3.14
:这只是一个“文本替换”的宏。在预编译阶段,代码里所有的PI
都会被直接换成3.14
。它不会在内存中单独占据一个格子,而是融入了指令之中。const float pi = 3.14;
:这定义了一个真正的常变量。系统会为pi
在只读数据区分配一个格子,并把3.14存进去。你可以获取它的地址,但不能修改它的值。
2. 可读写数据区(数据段 .data
+ .bss
段) - “储藏室”
- 位置:只读区的上方。
- 存放内容:那些在程序整个生命周期内都存在的、且可以被修改的变量。
- 数据段 (
.data
): 存放已初始化的全局变量和静态变量。比如int global_var = 10;
。程序启动时,这个10
就会被加载到这里。 - BSS段 (
.bss
): 存放未初始化的全局变量和静态变量。比如static int static_var;
。为了节省可执行文件的大小,这些变量的值(默认为0)不会被存在文件里,只是记录了需要多大的空间。程序启动时,操作系统会在这里清出一片内存,并全部填充为0。
- 数据段 (
- 特点:这片区域的大小在程序加载时就固定了,运行时不会改变。
3. 堆 (Heap) - “可自由搭建的乐高区”
- 位置:数据区的上方。
- 生长方向:从低地址向高地址增长。
- 存放内容:由程序员手动管理的内存。在C语言中,通过
malloc()
申请的内存就在堆上;通过free()
释放。 - 特点:大小动态变化。你需要多少,就申请多少。忘了释放,就会造成内存泄漏。堆是程序中最灵活、也最容易出问题的地方。
4. 共享库 (Shared Libraries) - “公共设施层”
- 位置:在堆和栈之间的一大片区域。
- 存放内容:存放一些系统提供的、多个进程可以共享的库函数代码。比如
printf
、scanf
等函数的机器指令。 - 特点:为了节省内存,这些公共的库函数在物理内存中可能只存在一份,但被映射到了多个进程的虚拟地址空间中。
5. 栈 (Stack) - “临时工作间”
- 位置:用户区的最顶层。
- 生长方向:从高地址向低地址增长。这种设计可以使堆和栈有最大的发展空间,只要它们不“撞车”就行。
- 存放内容:与函数调用相关的一切。
- 局部变量:在函数内部定义的变量。
- 函数参数:调用函数时传递的参数。
- 返回地址:函数执行完后,应该跳回到哪里继续执行。
- 特点:自动管理。调用一个函数时,系统会自动在栈顶“压入”一块内存(称为一个栈帧)来存放这个函数相关的所有数据。当函数返回时,这块内存会被自动“弹出”并销毁。如果函数调用嵌套太深(比如无限递归),栈会不停地向低地址增长,最终可能会用尽所有空间,导致栈溢出 (Stack Overflow)。
必会题与详解
题目:给定以下C语言代码,请指出变量g_var
, g_uninit_var
, PI
, c_var
, s_var
, l_var
以及 p_var
所指向的内存,分别存储在进程内存映像的哪个区域?
#include <stdio.h>
#include <stdlib.h>#define PI 3.14159const int c_var = 10;
int g_var = 20;
int g_uninit_var;void func() {int l_var = 40;printf("Local variable: %d\n", l_var);
}int main() {static int s_var = 30;int *p_var = (int*)malloc(sizeof(int));*p_var = 50;func();free(p_var);return 0;
}
答案详解:
g_var
(全局已初始化变量):存储在 数据段 (.data
)。它在程序整个生命周期都存在,并且有初始值。g_uninit_var
(全局未初始化变量):存储在 BSS段 (.bss
)。它也在整个程序生命周期存在,但没有初始值,程序加载时会被初始化为0。PI
(宏定义常量):不存储在内存的任何区域。它是一个宏,在预编译阶段,代码中所有出现PI
的地方都会被直接替换为文本3.14159
。它最终会成为CPU指令的一部分(比如mov rax, 3.14159
)。c_var
(const全局变量):存储在 只读数据段 (.rodata
)。它是一个常量,程序运行时不能被修改。s_var
(静态局部变量):存储在 数据段 (.data
)。虽然它定义在main
函数内部,但static
关键字改变了它的生命周期,使其与程序的生命周期一样长。因为它有初始值,所以存放在.data
段。l_var
(局部变量):存储在 栈 (Stack) 上。它是func
函数的局部变量,当func
被调用时,l_var
在func
的栈帧中被创建;当func
返回时,它随着栈帧的销毁而被释放。p_var
所指向的内存:变量p_var
本身是一个指针,它是一个局部变量,所以p_var
这个指针变量本身存储在栈 (Stack) 上。但是,它所指向的那块通过malloc
分配的、大小为sizeof(int)
的内存,是存储在 堆 (Heap) 上的。