打破认知!Linux 进程地址空间不是 “真实内存”?底层揭秘
今天,我要告诉大家的是:实际物理内存是连续的存储单元,由操作系统内核统一管理,没有 “代码段、堆、栈” 这样的分区布局。这只是为进程设计的虚拟的地址空间,为什么?后面讲。
一、什么是进程地址空间?
1.1、虚拟地址
上面说的大家肯定都一头雾水,下面我们通过一段代码来让大家实际感受一下:


我们定义了一个全局变量g_val,子进程中修改变量值,通过打印的结果,我们发现:g_val在子进程和父进程中的地址一模一样,但结果却不同。这就说明:这里打印出来的地址一定不是物理地址,而且此时子进程和父进程各自有一个g_val。
这个地址就是虚拟地址!!!
1.2、进程地址空间
进程地址空间也叫虚拟地址空间,其实是操作系统为每个进程分配的一个独立的、连续的虚拟内存区域。这个虚拟地址空间与物理内存并不存在一一对应的关系,而是通过内存管理单元(MMU)和页表机制实现动态映射。

以32为机器为例,这段虚拟内存区域的寻址能力为2^32字节(4GB),不等同于进程地址空间占4GB内存,其中有1GB为内核空间(操作系统内核运行的特权内存区域,拥有最高权限,可直接访问硬件资源和执行敏感操作(如内存管理、进程调度、设备驱动等)),剩下3GB为用户空间(普通应用程序运行的非特权内存区域,权限受限,无法直接访问硬件或内核资源)。
同时,用户空间又被细分为了上图所示的区域。
1.3、进程地址空间的管理
在Linux中,为了方便管理进程地址空间,操作系统在底层的 task_struct 内部存在着⼀个结构 体指针,该结构体指针的类型是 mm_struct 结构体,该结构体中存在着⼀些变量用于存储指定区域的起始值和终止值。这些值本质也是地址值,但是这个地址并不是实 际的物理内存的地址,而是通过映射后的虚拟地址。
源码如下:
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的大小*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}

那既然每一个进程都会有自己独立的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct 组织起来的!虚拟空间的组织方式有两种:
1. 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
2. 当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树。
linux内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚 拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型 的虚拟内存区域。上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进 程快速访问。
struct vm_area_struct
{
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红黑树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //文件映射偏移量
struct file * vm_file; //映射的文件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

二、有什么用?
2.1、分页与数据独立
虚拟地址空间是怎么通过虚拟地址映射到物理内存的呢?
操作系统创建进程PCB时还会创建一个叫页表的东西,页表就可以用来实现虚拟地址到物理地址的映射。起初,页表中存储变量的虚拟地址,当进程执行,操作系统给变量分配了物理内存后,操作系统就会把内存地址填到页表,与虚拟地址建立联系。
下面我们通过解释上面提到的全局变量g_val在子进程中修改为什么不会影响和父进程中g_val的值来理解:
当父进程创建子进程后,会将父进程的所有数据(PCB,进程地址空间,页表)都拷贝一份给子进程,所以,子进程拥有和父进程相同的虚拟地址。

当子进程修改g_val的值:操作系统就会给子进程在内存中另外找一块空间存放g_val 的值,同时将内存地址重新填入页表(覆盖原来的地址)。

这样就是实现了进程的独立,主也是进程具有独立性的原因。
三、总结:为什么?
1. 让进程的内存使用从 “无序” 变 “有序”
进程地址空间(虚拟地址空间)会给进程的内存划分规整的逻辑区域(如正文代码段、数据段、堆、栈等),让进程的代码、变量、数据等内容 “按规则存放”—— 避免了进程内存使用的混乱,同时也让进程自身能清晰管理不同类型的内存资源(比如知道代码段是只读的、堆是动态分配的)。
2. 保护物理内存,拦截非法操作
进程使用的是 “虚拟地址”,访问内存时需要通过内核的地址转换(虚拟→物理) 流程:
这个过程中,内核会对地址的 “合法性、权限” 做判定(比如虚拟地址是否在进程地址空间范围内、操作是否符合区域权限);
像例子中 char *str = "helloworld"; str = "H"; 会崩溃,就是因为字符串常量区在虚拟地址空间中被标记为 “只读”,地址转换时内核检测到 “写入只读区域” 的非法操作,直接拦截(避免破坏物理内存中的数据);
同时,也能防止进程访问不属于自己的物理内存(比如其他进程的内存),实现了进程间的内存隔离。
野指针:当内核进行虚拟地址-> 物理地址 转化时,该虚拟地址并没有映射具体的物理地址,所以造成了野指针。
3. 让 “进程管理” 与 “内存管理” 解耦合
进程只需要和 “虚拟地址空间” 交互(感知不到物理内存的细节),而物理内存的分配、回收、映射等工作由内核独立负责:
进程不用关心物理内存是否连续、实际地址是什么,只需要使用虚拟地址;
内核可以更灵活地管理物理内存(比如用 “写时复制”“内存交换” 等技术),同时不影响进程的内存使用逻辑 —— 实现了进程管理和内存管理的分层,降低了系统的复杂度。
————————————————
版权声明:本文为CSDN博主「Sunday不上发条」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Miun123/article/details/156114372
云服务器爆款直降90%
新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!