【Linux】进程地址空间
Linux 进程地址空间(32位为例)
进程地址空间是操作系统为每个运行中的进程分配的专属虚拟内存疆域,是一个结构体对象。如同为进程构建的“独立王国”:
1️⃣疆域范围:从低地址(如0x00400000)到高地址(如0xFFFFFFFF),覆盖完整的虚拟地址范围。
2️⃣分区治理:不同内存区域承担特定功能,严格隔离(如图中所见分区结构)。
3️⃣虚实映射:看似连续的“领土”,实际由操作系统通过页表+MMU硬件动态映射到物理内存(或磁盘)。
命令行参数与环境变量在栈区之上
在Window系统中,内存的分布并不是这个样子的,但是大差不差
一.前置知识:系统的32位与64位
32位与64位指的是CPU一次处理数据的宽度与寻址的范围.
32位CPU:CPU可以一次处理32bit(4字节)的数据范围,以及寻址232byte(4GB)的范围.
64位CPU:CPU可以一次处理64bit(8字节)的数据范围,以及寻址264byte(18.4亿GB)的范围.
二.地址空间划分的基本验证
可以观察到各个变量的地址的值,可以发现它们的大小是遵从上面的地址空间划分图的
三.地址空间中栈区、堆区的增长验证
可以发现,随着堆上内存的申请,堆的地址是向上增长的;随着栈上变量的创建,变量的地址是减小的
四.程序中的虚拟地址
我们可以发现,当子程序修改全局变量的时候,父进程对于同一地址的全局变量的值是没有变化的,其实也就是说,父进程与子进程的全局变量的地址是假的,不是真实的物理地址,其实就是虚拟地址,那么进程如何通过虚拟的地址找到物理地址的呢?下面讲解
五.页表
知识补充:虚拟内存≠实际内存
- 虚拟地址空间:32 位系统中进程认为有
0x00000000~0xFFFFFFFF(4GB)地址可用.- 物理内存:计算机实际安装的物理内存可能只有 1GB、2GB,远小于 4GB.
操作系统通过虚拟内存机制(页表映射 + 换页),让进程误以为自己独占整个 4GB 空间.
对上,总的来说,在32位系统中,每一个进程都有4G的进程地址空间是系统给进程画的大饼,实际内存的申请是否合理由计算机判定
在Linux内核中,task_struct是描述一个进程或线程的核心数据结构,而其中的mm_struct *mm成员是一个指向进程内存管理描述符的指针.其中 mm_struct 是用来管理用户内存空间分布的结构体,其中包含了虚拟内存空间、页表、内存映射等信息,下面我们展示一张图,具体说明页表的作用.
我们可以发现,我们的进程中的数据都有一份虚拟地址,进程可以通过这一份虚拟地址去页表中查找,找到对应的数据存放的物理地址.其次,子进程被父进程创建时,子进程会复制一份父进程的页表,也就是说,子进程中的变量与父进程指向的是同一的物理地址.下面,解释一下4.程序中的虚拟地址中的父子进程同一个g_val有两个值的原因.
#include<stdio.h>#include<unistd.h>intglobol_value=100;intmain(){pid_tid=fork();intcount=5;if(id==0){//子进程while(1){printf("I am child,pid = %d,ppid = %d,&global = %p,global_value = %d\n",getpid(),getppid(),&globol_value,globol_value);if(count==0)globol_value=200;elsecount--;sleep(1);}}elseif(id>0){//父进程while(1){printf("I am father,pid = %d,ppid = %d,&global = %p,global_value = %d\n",getpid(),getppid(),&globol_value,globol_value);sleep(1);}}return0;}缺页中断:写时复制(Copy-on-Write, COW),当一个进程创建子进程时(例如 Linux 中的 fork()),为了效率,父子进程一开始会共享所有的内存页面,但这些页面会被标记为“只读”。如果父进程或子进程中任何一个试图写入这些共享页面,就会触发一个缺页中断。此时,操作系统会为写入方复制一份该页面的私有副本,让它去修改这个副本,从而保证了父子进程内存空间的独立性。
所以,当我们的父进程创建了一个子进程之后,我们的子进程就会继承我们父进程的页表,此时我们修改子进程数据,子进程会发生页表中断,重新找到一个新的位置存放新的数据,此时在页表中只有物理地址被修改,虚拟地址没有被修改,所以这就是同一个地址存储了不同的值的原因.
题外话:
页表是在CPU中cr3寄存器中保存的
当我们的程序编译之后,我们的变量将是不存在的,会全部转换为地址(虚拟地址)
现代所有的语言都无法看到物理地址
六.内核空间是什么?
用户态(User Mode)和内核态(Kernel Mode)是操作系统中进程运行的两种不同权限级别,它们的本质区别在于对系统资源的访问权限和执行指令的能力。
- 用户态:
指进程在执行普通应用程序代码时所处的状态。在用户态下,进程只能访问受限的内存空间,不能直接操作硬件,也不能执行特权指令(如I/O操作、管理内存等)。用户态的程序如果需要访问硬件或操作系统资源,必须通过“系统调用”请求操作系统帮忙完成。
- 内核态:
指进程在执行操作系统内核代码时所处的状态。在内核态下,进程拥有最高权限,可以访问所有硬件资源、内存空间,并能执行所有CPU指令。操作系统内核本身和驱动程序等都运行在内核态。
内核空间存储着内核代码的虚拟地址,同时内核空间也会有一个内核空间页表,将虚拟的地址转化为物理地址,给进程找到内核代码(调用系统接口时)!
50个进程会有50份页表,但是一个操作系统的内核页表只有一份!所有的进程通过这个页表转换后都可以找到内核代码去执行,比如调用Printf函数!
当代码读取到printf时,进程从用户态转变成内核态,通过页表寻找我们的内核代码,执行printf函数的代码,随后再转换成用户态继续执行原来的代码!
七、虚拟地址到物理地址的转化
我们在上面说过了,在进程中我们能看到的都是虚拟地址,我们需要通过页表来进行虚拟地址到物理地址的转化,在32位系统下,地址的大小为32bit(4byte),下面我们看图:
1. 虚拟地址的构成
从图中可以看到,32位虚拟地址被分解为三个部分:
- 高10位:用于索引页目录
- 中间10位:用于索引二级页表
- 低12位:作为页内偏移量
具体分解:32 = 10 + 10 + 12
2. 转换流程详解
第一步:页目录查找
- 使用虚拟地址的高10位作为索引
- 在页目录中找到对应的页目录表项
- 页目录有1024个条目(210)
- 每个页目录表项包含一个指向二级页表的起始地址
第二步:二级页表查找
- 使用虚拟地址的中间10位作为索引
- 在二级页表中找到对应的页表表项
- 每个二级页表也有1024个条目(210)
- 每个页表表项包含一个指向物理内存中页框起始地址
第三步:物理地址计算
- 从页表表项中获取页框的起始地址
- 将虚拟地址的低12位偏移量直接加到页框起始地址上
- 得到最终的物理地址
3. 具体示例
0000 0000 0000 0000 0000 0000 0000 0000
转换过程:
- 高10位
A→ 索引页目录 - 中间10位
B→ 索引二级页表 - 低12位
C→ 页内偏移量
最终物理地址 = 页框起始地址 + 偏移量
例如:0x11223300 + 偏移量 = 物理地址
4. 关键特点
内存效率:
- 二级页表"大部分情况都是不全的"
- 操作系统只为实际使用的虚拟地址空间分配页表
- 节省了大量内存空间
覆盖范围:
- 每个页目录条目可以映射1MB的虚拟地址空间
- 计算:4KB × 1024 = 1MB
页大小:
- 每个页框大小为4KB(212字节)
- 偏移量范围:[0, 212-1]
11223300 + 偏移量 = 物理地址`
4. 关键特点
内存效率:
- 二级页表"大部分情况都是不全的"
- 操作系统只为实际使用的虚拟地址空间分配页表
- 节省了大量内存空间
覆盖范围:
- 每个页目录条目可以映射1MB的虚拟地址空间
- 计算:4KB × 1024 = 1MB
页大小:
- 每个页框大小为4KB(212字节)
- 偏移量范围:[0, 212-1]