Linux系统编程——进程地址空间
32位计算机有32位的地址和数据总线,2^32 种信号组合、寻址空间;每个地址对应 1 个字节byte 的内存空间(计算机存储的基本单位),理论上最大内存容量为 4294967296 byte = 4 GB
进程运行时,内核会为该进程提供一个 虚拟内存视图 —— 进程地址空间
进程地址空间中的地址 称为 线性地址 or 虚拟地址;进程地址空间 让每个进程认为 自己独占了 整个连续的 内存空间;实际上 进程地址空间中的 虚拟地址 是 通过 页表 映射到 不连续的 物理内存中。
mm_struct
进程地址空间,本质是内核的一个 数据结构对象,类似于 PCB,进程地址空间也是 需要被操作系统内核进行管理的;在 linux 系统中,描述 进程地址空间 的数据结构为 mm_struct,也是将结构体指针声明在进程的 PCB —— task_struct 中,进行管理。
进程地址空间 / 程序地址空间 / 虚拟地址空间 存在的意义:
1、让所有进程以统一的视角看待内存,让无序的 物理内存 变为 有序的 虚拟内存
2、在进程需要访问 物理内存时,增加一个中间过程:可以对 寻址请求 进行审查,一旦存在异常访问 可以直接拦截;使该请求不会到达物理内存,对 物理内存 进行保护;
3、进程地址空间 和 页表 的存在,将 进程管理模块 与 内存管理模块 解耦!
页表
进程处于运行态 正在被 CPU 调度时,进程的 PCB--task_struct 被创建,进程地址空间被创建并进行维护;另外,内核会为该进程维护一张 页表 结构,页表是进程地址空间的虚拟地址 到 物理内存地址的 映射;
进程的 页表 属于进程的 硬件上下文数据(当该进程被从 CPU 剥离时,会带走上下文数据);该进程运行时,CPU 中的 cr3 寄存器会保存当前进程的 页表 起始地址(需要高频读写的数据放在寄存器),且指向该页表的地址属于 物理地址 ,CPU 通过该物理地址找到对应进程的 页表。
页表的标志位
页表的每个地址还有标志位:标志该地址 可读 还是 可写,因为 物理内存 中没有可读可写的概念,想写就能写,所以通过在 页表中为相应地址添加标志位,来表明空间的可读可写属性;所以为什么 进程地址空间 中的代码区、字符常量区 等内存区域就算是只读的,那么它地址处的数据是如何被写入的? —— 因为内核 在对应物理地址写入数据后,修改 页表中 该地址处的属性 为 可读,那么对上层来说,这块空间就是只读的!
缺页中断
操作系统对数据加载到内存中的方式一般是:惰性加载
在页表中,还有另外一个标志位:标志着该进程 对应的该地址处的 代码或者数据 是否已经被加载到了内存中:
如果没有加载到 内存,就没有开辟空间,也就没有对应的物理地址;
如果这个进程的页表中的 所有代码和数据 都没有被加载到内存中,那就说明:该进程被 挂起了(进程挂起 参考 之前的 blog)
如果此时要访问 / 修改 没有被加载到内存中的代码或数据,那么会触发 缺页中断,发生写时拷贝,操作系统 为该进程被访问的数据 在 物理内存中 重新开辟空间,并修改被访问数据的 虚拟地址与物理地址 的映射关系,虚拟地址是不变的。
进程的独立性
每个进程都有 自己的 PCB 与 硬件上下文数据,可能父子进程 代码或数据 的 虚拟地址 相同,但物理内存 中一定是不同的;进程地址空间通过 将 无序的物理内存使用 映射到 有序的 虚拟内存,实现了 以 统一的视角 看待进程。