目录
进程地址空间
看一个现象
写时拷贝
虚拟地址的认识
区域划分
程序加载的过程
编辑
为什么会有虚拟地址空间?
缺页中断的理解
补充的理解
进程地址空间
也叫虚拟地址空间
虚拟地址空间不是物理内存,每一个take_struct都要对应一个虚拟地址空间
虚拟地址空间的宽度对应一个字节
在32位机器——2^32个地址=4GB
在32位机器——2^64个地址=8GB
以32位机器为例子
低地址到高地址
上图是一个进程地址空间——虚拟地址空间
在用户方面用的是0-3GB的内存,剩下的都是内核的
如果用户想访问所谓的初始化、未初始化的代码、栈、堆等,都可以通过拿到对应的地址进行访问。
可以看到虚拟地址通过页表的映射对应了物理地址
问题:平常写代码的时候&,打印的时候是访问虚拟地址还是物理地址?——虚拟地址
看一个现象
#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; }结果
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)
区域划分
把虚拟地址空间分成几个固定的“区域”,每个区域放不同类型的数据。
只需要确定开始和结束的位置
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:内存不够用
物理空间可能小于进程的大小
没有虚拟内存,程序不能超过物理内存大小。
从虚拟地址空间角度理解野指针
野指针指向的虚拟地址,要么没有映射,要么映射到了不该访问的地方。——他映射的物理地址被释放掉了,就没有了地址。
从虚拟内存角度理解一行代码
char str* ="hello world"; *str='A';这个代码如果运行会直接崩溃,为什么?
因为str指向的内容是放在了字符常量区的,当运行的时候,字符常量区的虚拟地址映射到物理内存的时候,由于str不能被修改,会发送权限拦截。
完整的理解
1. 程序编译时,"hello world" 被放入只读数据段 2. 加载时,OS 把该区域映射为"只读" 3. 执行 *str = 'A' - CPU 用 str 的虚拟地址查页表 - 页表显示:该区域权限是"只读" - CPU 触发写保护异常 4. OS 接管 - 检查:这是只读区域,不允许写 - 向进程发送 SIGSEGV 信号 5. 进程收到 SIGSEGV - 默认行为:终止进程 - 终端显示:Segmentation fault (core dumped)缺页中断的理解
CPU 访问虚拟地址时,页表里找不到映射,触发异常让 OS 处理。OS 分配物理内存,填充页表,然后程序重新执行。缺页中断是虚拟内存“延迟分配”机制的核心,正常情况下程序感知不到,只有非法访问才会变成段错误。
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的区域,除了堆区,其他区域其实也是一样的