目录
进程地址空间
也叫虚拟地址空间
虚拟地址空间不是物理内存,每一个take_struct都要对应一个虚拟地址空间
虚拟地址空间的宽度对应一个字节
在32位机器------2^32个地址=4GB
在32位机器------2^64个地址=8GB
以32位机器为例子
低地址到高地址

上图是一个进程地址空间------虚拟地址空间
在用户方面用的是0-3GB的内存,剩下的都是内核的
如果用户想访问所谓的初始化、未初始化的代码、栈、堆等,都可以通过拿到对应的地址进行访问。

可以看到虚拟地址通过页表的映射对应了物理地址
问题:平常写代码的时候&,打印的时候是访问虚拟地址还是物理地址?------虚拟地址
看一个现象
cpp
#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
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
结果
bash
hzx@VM-0-12-ubuntu:~$ gcc fork_addr.c -o fork_addr
hzx@VM-0-12-ubuntu:~$ ./fork_addr
child[12345]: 0 : 0x60104c
parent[12344]: 0 : 0x60104c
关键现象:
-
子进程和父进程打印的
g_val值都是 0 -
子进程和父进程打印的
&g_val地址完全一样 (0x60104c)
原因:虽然g_val是不同进程的,但是他们的地址是一样的,当然这也不能说明访问的是虚拟地址还是物理地址。------下文慢慢会深挖,只需要一个现象------访问地址一样。
当创建子进程的时候
会拷贝父进程的页表、take_strcut、和虚拟地址------浅拷贝。
因此父进程和子进程的g_val虚拟地址和物理地址会一样,当我们&访问内存的时候地址就是一样的。

后来,由于各种的需求子进程的g_val需要被修改,会怎么变化?
因为进程具有独立性,就可以猜想到,父进程和子进程的g_val不会相互影响。------怎么变化?------写时拷贝
写时拷贝
当子进程的g_val被修改的时候,操作系统会第一时间知道,然后会在物理内存处增加一个内存空间存放子进程修改的值,把修改后g_val的值放到新的物理内存空间,但是他们的虚拟地址都是一样的------因此平常写代码&,访问的是虚拟地址。

虚拟地址的认识
因此,也有了虚拟地址的一个大概的认识:
虚拟地址是操作系统给每个进程发的"假地址"。进程以为自己独占整个内存,实际上 OS 通过页表把它映射到真实的物理内存。& 取到的地址是虚拟地址,父子进程虚拟地址相同,但通过各自的页表映射到不同的物理内存。
虚拟地址空间也要被管理------先描述再组织------数据结构(strcut mm_struct)
区域划分
把虚拟地址空间分成几个固定的"区域",每个区域放不同类型的数据。
只需要确定开始和结束的位置
cpp
struct mm_struct {
// 1. 区域边界(最核心)
unsigned long start_code, end_code; // 代码段起止
unsigned long start_data, end_data; // 数据段起止
unsigned long start_brk, brk; // 堆起止
unsigned long start_stack; // 栈起始
// 2. 参数和环境变量
unsigned long arg_start, arg_end; // 命令行参数
unsigned long env_start, env_end; // 环境变量
// 3. 组织所有区域
struct vm_area_struct *mmap; // 区域链表头
struct rb_root mm_rb; // 区域红黑树
// 4. 页表
pgd_t *pgd; // 页全局目录
// 5. 其他
unsigned long task_size; // 地址空间大小
atomic_t mm_users; // 使用该地址空间的进程数
atomic_t mm_count; // 引用计数
// ...
};
每一个位置的都有对应的地址,那么这个就是统一编制。

程序加载的过程
程序加载时,OS 只建立虚拟地址空间和页表(页表项为空),物理内存不分配。程序执行时,访问虚拟地址触发缺页中断,OS 才分配物理内存、读取文件内容、填充页表。这就是"延迟分配"------虚拟地址先确定,物理内存用到才分配。
那么这个时候,建立虚拟地址空间的过程就是调整区域的划分把上面结构体的_end,start的数字进行调整。
take_strct
为什么会有虚拟地址空间?
问题 1:进程之间互相踩踏
进程A和进程B都想用同一个地址,数据会被覆盖。
问题 2:安全风险
程序可以直接访问内核数据,造成系统崩溃、内存泄漏。
没有保护,任何程序都能访问任何内存。------系统会对你地址的访问和操作进行合法判定
问题 3:地址不确定
每次运行地址都不一样,程序无法预知自己的数据在哪。
问题 4:内存不够用
物理空间可能小于进程的大小
没有虚拟内存,程序不能超过物理内存大小。
从虚拟地址空间角度理解野指针
野指针指向的虚拟地址,要么没有映射,要么映射到了不该访问的地方。------他映射的物理地址被释放掉了,就没有了地址。
从虚拟内存角度理解一行代码
cpp
char str* ="hello world";
*str='A';
这个代码如果运行会直接崩溃,为什么?
因为str指向的内容是放在了字符常量区的,当运行的时候,字符常量区的虚拟地址映射到物理内存的时候,由于str不能被修改,会发送权限拦截。
完整的理解
cpp
1. 程序编译时,"hello world" 被放入只读数据段
2. 加载时,OS 把该区域映射为"只读"
3. 执行 *str = 'A'
- CPU 用 str 的虚拟地址查页表
- 页表显示:该区域权限是"只读"
- CPU 触发写保护异常
4. OS 接管
- 检查:这是只读区域,不允许写
- 向进程发送 SIGSEGV 信号
5. 进程收到 SIGSEGV
- 默认行为:终止进程
- 终端显示:Segmentation fault (core dumped)
缺页中断的理解
CPU 访问虚拟地址时,页表里找不到映射,触发异常让 OS 处理。OS 分配物理内存,填充页表,然后程序重新执行。缺页中断是虚拟内存"延迟分配"机制的核心,正常情况下程序感知不到,只有非法访问才会变成段错误。
cpp
1. 程序访问虚拟地址 0x4005d6
2. CPU 查页表
3. 页表项为空(或无效)
4. CPU 触发缺页中断
5. OS 接管:
a. 检查这个地址是否合法
b. 分配一块物理内存
c. 从磁盘读取数据(如果是文件映射)
d. 填充页表:0x4005d6 → 物理地址 X
e. 标记权限(只读/可写)
6. 返回程序,重新执行刚才的指令
7. 这次页表有映射了,访问成功
因此还有一个原因是为了让进程管理和内存管理进行解耦合
补充的理解
我们可以不加载数据,只有take_struct、mm_struct。
创建进程先有内核数据结构,才陆陆续续加载代码和数据。
进程挂起的时候,OS会查找页表对应的地址,把页表清空,再把代码和数据放到磁盘。
栈不是不只有一个吗,怎么做到malloc的时候是随机的?
mm_struct有多个vm_area_struct,每个代表一个malloc的区域,除了堆区,其他区域其实也是一样的

