个人主页:chian-ocean
文章专栏-Linux
前言:
进程地址空间是操作系统为每个进程提供的一块独立的虚拟内存空间。每个进程的地址空间是独立的,确保了一个进程的运行不会直接影响其他进程的内存空间。

进程地址空间
进程地址空间是操作系统为每个进程分配的一块独立的、连续的虚拟内存地址范围。每个进程在运行时使用自己的地址空间来管理程序的代码、数据、堆栈等内存部分。通过这种机制,操作系统实现了进程之间的内存隔离和资源保护。
核心概念
- 进程地址空间基于虚拟内存机制实现。虚拟内存是操作系统为每个进程提供的一个逻辑内存空间,独立于实际的物理内存。
- 进程访问的地址是虚拟地址,通过内存管理单元(MMU)和操作系统,将虚拟地址映射到物理内存。
- 每个进程有独立的地址空间,一个进程无法直接访问另一个进程的地址空间。
- 即使多个进程使用相同的虚拟地址,这些地址也映射到不同的物理内存区域。
- 地址空间中的很多部分在进程运行时按需分配,比如堆空间和栈空间,操作系统动态管理这些区域。
- 地址空间被划分为不同的段(如代码段、数据段、堆和栈)。
- 使用分页机制,将这些段划分为固定大小的页,以便更高效地管理内存。

进程地址空间的组成
进程地址空间的组成通常包括以下部分:

#include<stdio.h>
#include<stdlib.h>
int g_val_1;
int g_val_2 = 20;
int main()
{
const char *str = "hello linux";
char *p1 = (char*)malloc(100);
char *p2 = (char*)malloc(100);
int a = 1;
int b = 2;
//代码区
printf("code addr : %p\\n",main);
//字符常量区
printf("read only string addr : %p\\n",str);
//已初始化全局数据区(数据段)
printf("init global value addr : %p\\n",&g_val_1);
//未初始化全局数据区
printf("uninit global value addr : %p\\n",&g_val_2);
//堆区
printf("heap addr : %p\\n",p1);
printf("heap addr : %p\\n",p2);
//栈区
printf("stsck addr : %p\\n",&a);
printf("stsck addr : %p\\n",&b);
return 0;
}
代码解析
程序内存区域的划分
- main 函数所在的地址属于代码段,存放程序的可执行指令。
- 打印 main 函数的地址,展示代码段所在位置。
- 字符串常量 "hello linux" 被存储在只读常量区。
- 打印字符串的地址,展示它在内存中的存储位置。
- 已初始化的全局变量(g_val_2)存放在已初始化全局数据区。
- 未初始化的全局变量(g_val_1)存放在BSS 段,程序运行时会自动将其初始化为零。
- 使用 malloc 动态分配的内存存储在堆区,堆区的内存地址通常随着分配逐渐向高地址增长。
- 打印 p1 和 p2 的地址,展示它们在堆区的存储位置。
- 局部变量(如 a 和 b)存放在栈区,栈的内存地址通常随着分配逐渐向低地址增长(栈是向下生长的)。
- 打印 a 和 b 的地址,展示栈的内存布局。
示例输出
假设程序运行在 64 位系统上,输出可能类似如下(具体地址会因运行环境而异):
code addr : 0x4005d6 // 代码段地址
read only string addr : 0x600e10 // 字符常量区地址
init global value addr : 0x601044 // 已初始化全局变量地址(数据段)
uninit global value addr : 0x601040 // 未初始化全局变量地址(BSS 段)
heap addr : 0x7f98c00008c0 // 堆区地址
heap addr : 0x7f98c00008f0 // 堆区地址(连续分配)
stsck addr : 0x7fff5d6b4cfc // 栈区地址
stsck addr : 0x7fff5d6b4cf8 // 栈区地址
关键点解析
- 打印 main 函数的地址,说明它位于代码段中。
- str 指向字符串常量 "hello linux",存储在只读区域(防止被修改)。
- g_val_2 是已初始化的全局变量,存储在数据段中。
- g_val_1 是未初始化的全局变量,存储在 BSS 段中,并在运行时自动初始化为 0。
- malloc 动态分配的内存位于堆区,连续调用 malloc 会分配到地址更高的堆区位置。
- 栈区存储局部变量,变量 a 和 b 的地址说明栈是从高地址向低地址分配的。
典型进程地址空间布局
| 内核空间 | 位于地址空间的最高部分,供操作系统内核使用,用户进程不能直接访问。 |
| 用户空间 | 用户进程可访问的地址空间,通常由以下部分组成: |
| 栈区(Stack) | – 存储局部变量、函数调用信息(如返回地址、参数等)。 – 栈从高地址向低地址生长。 |
| 堆区(Heap) | – 存储动态分配的内存(如 malloc、new)。 – 堆从低地址向高地址生长。 |
| BSS 段 | – 存储未初始化的全局变量和静态变量。 – 在程序启动时自动初始化为 0。 |
| 数据段(Data) | – 存储已初始化的全局变量和静态变量。 |
| 代码段(Text) | – 存储程序的可执行代码(只读区域)。 |
| 只读常量区 | – 存储字符串常量、只读数据等。 |
虚拟内存和物理内存(实际内存)的交互
虚拟内存和物理内存之间通过地址映射机制联系在一起。这个映射是通过**页表(Page Table)**完成的。
地址转换过程:
- 程序访问的地址,通常分为多个逻辑段(代码段、堆、栈等)。
- 页表保存虚拟地址到物理地址的映射关系。
- 每个进程都有一个独立的页表,由操作系统维护。
- 通过页表,虚拟地址被转换为物理地址,从而定位到物理内存中的数据。
虚拟内存与物理内存的差异
| 定义 | 操作系统提供的逻辑地址空间 | 计算机硬件提供的实际内存 |
| 容量 | 可以比物理内存大,依赖磁盘扩展 | 受限于实际 RAM 的大小 |
| 性能 | 速度较慢,部分数据存放在磁盘 | 速度快,直接存储在 RAM 中 |
| 隔离 | 每个进程的虚拟地址空间独立 | 共享物理内存,需要通过虚拟内存管理 |
| 保护 | 防止进程之间相互访问或破坏 | 无保护,需依赖操作系统管理 |
页表 (Page Table)
页表是操作系统在实现虚拟内存(Virtual Memory)**时的重要数据结构,用于将**虚拟地址(Virtual Address)**映射到**物理地址(Physical Address)。
在分页内存管理中,虚拟地址被分为多个固定大小的块,称为页(Page);物理内存也被分为同样大小的块,称为页框(Page Frame)。页表记录了每个虚拟页与物理页框之间的对应关系。
页表的作用
- 地址转换:将虚拟地址映射为物理地址。
- 内存隔离:为每个进程提供独立的地址空间,不同进程的页表互相独立。
- 内存保护:通过页表中的属性位(如读/写权限)限制对内存的非法访问。
页表的结构
页表的结构
页表是一张映射表,其中每一项称为页表项(Page Table Entry, PTE),记录虚拟页与物理页框之间的映射关系。
页表项 (PTE) 的内容
一个页表项通常包含以下信息:
页框号(Frame Number):
- 表示虚拟页映射到的物理页框。
有效位(Valid Bit):
- 指示该虚拟页是否有效(是否在物理内存中)。
读/写权限位(R/W Bit):
- 表示该页是否可读、写或执行。在物理内存中是无法实现字符常量区在内存中不更改的。由于页表权限位的存在,使我们不能访问此常量区。
#include<stdio.h>
int main()
{
char *str = "hello bit";
*str = 'H';
return 0;
}
- 示例中,页表中权限位的存在不可更改。
存在位(Present Bit):
- 表示该页是否已被加载到物理内存。
- 如果没有被预加载到内存会触发缺页中断
页号(Page Number):
- 表示虚拟页的编号。
其他控制信息:
- 如缓存禁用位、页面脏位(是否被修改过)、访问位等。
示例图:
~
- 示例中,页表中权限位的存在不可更改。
存在位(Present Bit):
- 表示该页是否已被加载到物理内存。
- 如果没有被预加载到内存会触发缺页中断
页号(Page Number):
- 表示虚拟页的编号。
其他控制信息:
- 如缓存禁用位、页面脏位(是否被修改过)、访问位等。
示例图:



