目录

Linux进程地址空间

目录

[Linux]进程地址空间

该图是虚拟地址:

https://i-blog.csdnimg.cn/direct/df53dc4995dc4bdba381a99c0640cdda.png

#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,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
		g_val=100;
		printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}else{ //parent
		sleep(3);
		printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
	}
sleep(1);
return 0;
}
// 结果
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

变量内容不一样,所以父子进程输出的变量绝对不是同一个变量

但地址值是一样的,说明,该地址绝对不是物理地址!

在Linux地址下,这种地址叫做虚拟地址

我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

在操作系统中,子进程和父进程的虚拟地址空间在创建时是相同的,但它们的物理内存映射可能不同,具体取决于操作系统的内存管理机制。

1、fork() 创建子进程时的行为

当父进程通过 fork() 创建子进程时,子进程会复制父进程的虚拟地址空间:

  1. 虚拟地址相同:子进程的代码段、数据段、堆栈等逻辑地址布局与父进程完全一致。
  2. 物理内存分离:初始时,父子进程的虚拟地址映射到相同的物理内存页,但这些页会被标记为只读(写时复制的优化)。

假设父进程有一个变量 int x = 10,地址为 0x1000:

子进程的 x 也会看到地址 0x1000,但物理内存此时是共享的。

当父进程或子进程尝试修改 x 时,操作系统会触发写时复制,为修改者分配新的物理页,此时两者的物理内存分离,但虚拟地址仍相同。

2、写时拷贝

  1. 核心思想

    共享而非复制:初始时,多个进程(或对象)共享同一份物理内存数据。

    按需拷贝:只有当某个进程尝试写入共享内存时,系统才会真正复制该内存区域,并为写入者分配独立的物理内存副本。

    目的:避免无意义的物理内存复制,提升性能。

    触发条件:当父进程或子进程尝试写入共享内存页时。

    结果:修改者获得独立的物理页,虚拟地址保持不变。

  2. 典型场景:fork() 创建子进程

    传统方式:fork() 直接复制父进程全部内存到子进程,导致大量内存拷贝(即使数据未被修改)。

COW 优化:

共享阶段:fork() 后,父子进程共享所有内存页,但将内存页标记为只读。

触发拷贝:当任一进程尝试写入共享页时,触发页错误(Page Fault)。

内核介入:操作系统检测到写操作,分配新的物理页,复制原页内容到新页,并修改进程的页表映射。

完成写入:进程继续执行写入操作,但此时操作的是独立的物理页。

https://i-blog.csdnimg.cn/direct/48ea7784eb6e4f9c9080fdf593b2c4a1.png

  1. 虚拟地址空间的独立性

    进程隔离:每个进程的虚拟地址空间是操作系统分配的独立逻辑视图,彼此隔离。

    物理内存映射不同:即使虚拟地址相同,实际物理内存可能完全不同(如父子进程修改共享页后)。

创建进程,本质是系统多了一个进程,因此需要管理进程

https://i-blog.csdnimg.cn/direct/fdc1377b5c6542e58a39b9bd92c9854a.png

同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址

Linux为什么要有地址空间?Linux 进程地址空间的设计是操作系统内存管理的核心机制,其核心目标是为进程提供内存保护、隔离和抽象,同时确保系统稳定性和资源的高效利用。

1、内存保护:杜绝系统级越界问题

虚拟地址空间的隔离性:

每个进程拥有独立的虚拟地址空间,通过页表映射到物理内存。进程只能访问其虚拟地址范围内被明确分配的内存区域。

页表的权限控制:

页表中每个内存页标记了读(R)、写(W)、执行(X)权限。例如:

代码段(.text)标记为 R-X(可读、可执行,不可写),防止恶意代码篡改程序逻辑。

数据段(.data)标记为 RW-(可读、可写,不可执行),防止数据段被当作代码执行(防范缓冲区溢出攻击)。

非法访问示例:

若进程尝试通过野指针写入未分配的地址(如 (int )0xdeadbeef = 42),页表中无对应物理页映射,触发段错误(Segmentation Fault),操作系统直接终止进程。

内核空间的保护:

内核内存(如 0xC0000000 以上的高地址空间)在所有进程的虚拟地址中共享,但用户态进程无权访问。任何用户态程序尝试访问内核地址会触发权限异常,确保内核安全。

2、内存抽象:进程视角的“独占内存”

(1) 一致的虚拟空间布局

所有进程的虚拟地址空间范围相同:

例如,在 32 位系统中,每个进程“认为”自己拥有完整的 0x00000000 到 0xFFFFFFFF 地址空间,包含代码段、数据段、堆、栈等区域。

实际物理内存映射不同:

进程 A 的栈地址 0x7ffffff0000 可能映射到物理页 X,而进程 B 的同栈地址映射到物理页 Y。

这种抽象让程序无需关心物理内存的实际分配,简化开发。

(2) 进程的“独占内存”假象

独立的内存视图:

进程认为自己是系统中唯一运行的实体,所有内存区域(代码、堆、栈)均为自己独占。

实际资源共享:

多个进程的代码段可能通过写时拷贝(COW)共享同一物理内存(如 fork() 后的父子进程)。

动态链接库(如 libc.so)被加载到固定虚拟地址,多个进程共享同一物理内存副本。

3、进程独立性:解耦调度与内存管理

(1) 内存管理与进程调度的分离

虚拟地址空间使物理内存管理透明化:

进程调度器只需关注 CPU 时间片的分配,无需关心进程内存的物理位置。

内存管理器通过页表动态分配物理页,甚至可以换出(Swap Out)不活跃进程的内存到磁盘,而不影响进程的虚拟地址视图。

(2) 灵活的内存分配

按需分配物理内存:

进程申请内存(如 malloc())时,操作系统仅分配虚拟地址,物理内存的分配延迟到实际写入时(通过 COW 或缺页中断)。

示例:

进程调用 malloc(1GB),系统立即分配虚拟地址范围,但物理内存可能仅在进程实际写入时逐步分配。

https://i-blog.csdnimg.cn/direct/38f69e8170d04400bad20e2edf040467.png