mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-11-01 19:53:43 +00:00
Update 计算机操作系统 - 内存管理.md
补充缺页异常的处理流程和进程虚拟地址空间的布局,增加配图和实测附录。
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 247 KiB |
+92
-1
@@ -14,6 +14,9 @@
|
||||
* [段页式](#段页式)
|
||||
* [分页与分段的比较](#分页与分段的比较)
|
||||
* [附录:实测](#附录实测)
|
||||
* [A1 页面置换模拟](#a1-页面置换模拟)
|
||||
* [A2 缺页次数](#a2-缺页次数)
|
||||
* [A3 进程的地址空间布局](#a3-进程的地址空间布局)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -42,6 +45,18 @@
|
||||
|
||||
64 位系统的地址空间很大,如果用一张页表覆盖全部地址,页表本身就会非常大,因此使用多级页表,只为用到的地址分配下一级页表。例如 x86-64 通常使用 4 级页表,48 位虚拟地址分为 4 个 9 位的页表索引和 12 位的页内偏移。
|
||||
|
||||
MMU 查页表时发现页不在内存中,或者这次访问违反了页的权限(例如写只读页),就会触发缺页异常(Page Fault),由操作系统处理:
|
||||
|
||||
1. 保存现场,取得引发缺页的虚拟地址;
|
||||
2. 检查地址是否合法,地址不属于进程的任何区域或者权限不符(例如写代码段)时,向进程发送 SIGSEGV 信号,也就是常见的段错误;
|
||||
3. 按页的来源准备页框:第一次访问的堆、栈等匿名页分配一个页框并清零;代码和 mmap 映射的文件页先在页缓存中找,找到就直接映射,找不到才分配页框并读磁盘;换出到交换区的页从交换区读回;fork 之后写共享的页时复制一份(写时复制);
|
||||
4. 需要页框而内存不足时,先用页面置换算法换出一页,被修改过的页要先写回磁盘;
|
||||
5. 更新页表项,回到用户态重新执行引发缺页的那条指令。
|
||||
|
||||
需要读磁盘的缺页称为主要缺页(Major Fault),进程要阻塞到 I/O 完成,期间 CPU 去运行其它进程;不需要 I/O 的称为次要缺页(Minor Fault),只需分配、清零或复制页框并修改页表。缺页异常属于异常,在执行某条指令的过程中同步产生,处理完后重新执行这条指令;外部中断由设备异步产生,处理完后继续执行下一条指令,见[中断分类](计算机操作系统%20-%20概述.md#中断分类)。
|
||||
|
||||
例如用 mmap 申请 256 MB 的匿名内存后,进程的常驻内存(RSS)几乎不变;逐页写入一遍,触发 65536 次次要缺页(256 MB / 4 KB),RSS 增加到 257 MB,再写一遍就不再缺页。fork 出的子进程把这些页再写一遍,又触发 65536 次缺页,每次复制一页,父进程中的值不受影响(Ubuntu 24.04 实测,见[附录 A2](#a2-缺页次数))。这也说明申请内存只是分配了虚拟地址,真正用到时才占用物理内存。
|
||||
|
||||
## 页面置换算法
|
||||
|
||||
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
|
||||
@@ -62,7 +77,7 @@
|
||||
7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
|
||||
```
|
||||
|
||||
开始运行时,先将 7、0、1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。按这个规则走完整个序列共缺页 9 次;同样的序列和 3 个物理块,LRU 缺页 12 次,FIFO 缺页 15 次(逐步模拟,见[附录:实测](#附录实测))。
|
||||
开始运行时,先将 7、0、1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。按这个规则走完整个序列共缺页 9 次;同样的序列和 3 个物理块,LRU 缺页 12 次,FIFO 缺页 15 次(逐步模拟,见[附录 A1](#a1-页面置换模拟))。
|
||||
|
||||
### 2. 最近最久未使用
|
||||
|
||||
@@ -134,8 +149,19 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问
|
||||
|
||||
需要注意的是,x86-64 的 64 位模式下分段基本不再使用,代码段、数据段的基址都按 0 处理(FS、GS 除外,用于线程局部存储),Linux 等系统只用分页来管理内存。
|
||||
|
||||
虽然不用分段,Linux 仍然把进程的地址空间划分成代码、数据、堆、栈等区域,每个区域是一段连续的虚拟地址,区域之间的大片空白不分配页框。下图是 Ubuntu 上一个小程序打印出的各部分地址(实测,见[附录 A3](#a3-进程的地址空间布局)):
|
||||
|
||||
<div align="center"> <img src="pics/proc-vm-layout.png" width="740px"/> </div><br>
|
||||
|
||||
- 代码段、数据段和 BSS 在低地址,大小在程序加载时就确定了,BSS 存放未初始化的全局变量,加载时清零;
|
||||
- 堆从低地址向高地址增长,malloc 申请小块内存时通过 brk 扩大堆,大块内存(glibc 的阈值初始为 128 KB)改用 mmap 单独映射;
|
||||
- 栈从高地址向低地址增长,Ubuntu 默认最大 8 MB(ulimit -s);
|
||||
- 共享库、mmap 映射的文件和大块内存放在堆和栈之间的内存映射区。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
### A1 页面置换模拟
|
||||
|
||||
用 Python 逐步模拟 3 个物理块下的页面置换,引用序列为 OPT 一节中的例子。Clock 的结果与新装入页面的 R 位取 0 还是 1、指针从哪里开始有关,这里装入时置 1:
|
||||
|
||||
```text
|
||||
@@ -146,8 +172,73 @@ FIFO 缺页 15 次
|
||||
Clock 缺页 14 次
|
||||
```
|
||||
|
||||
### A2 缺页次数
|
||||
|
||||
Docker Desktop 中的 Ubuntu 24.04 容器(Apple M1 Pro,aarch64,内核 6.10.14-linuxkit,页大小 4 KB),gcc 13.3。用 mmap 申请 256 MB 匿名内存,用 getrusage 的 ru_minflt、ru_majflt 统计缺页次数,从 /proc/self/status 读 VmRSS:
|
||||
|
||||
```c
|
||||
char *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
madvise(p, size, MADV_NOHUGEPAGE);
|
||||
for (size_t i = 0; i < size; i += page) p[i] = 1; // 逐页写入
|
||||
for (size_t i = 0; i < size; i += page) p[i] = 2; // 再写一遍
|
||||
if (fork() == 0) {
|
||||
for (size_t i = 0; i < size; i += page) p[i] = 3; // 子进程写入,触发写时复制
|
||||
_exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
页大小 4096 字节,共 65536 页,MADV_NOHUGEPAGE
|
||||
mmap 之后: 次要缺页 +0,RSS 1 MB
|
||||
逐页写入后:次要缺页 +65536,主要缺页 0,RSS 257 MB
|
||||
再写一遍: 次要缺页 +0
|
||||
子进程逐页写入(写时复制):次要缺页 +65536
|
||||
父进程中的值仍为 2
|
||||
```
|
||||
|
||||
容器所在虚拟机的透明大页设置为 always,不调用 madvise 时内核用 2 MB 的大页映射,逐页写入只触发 128 次缺页(256 MB / 2 MB);Ubuntu 默认的设置是 madvise,不主动申请时使用 4 KB 的页,与上面的结果一致。
|
||||
|
||||
### A3 进程的地址空间布局
|
||||
|
||||
同一个容器,gcc 13.3 编译(默认生成位置无关的可执行文件),开启地址空间布局随机化,打印各部分的地址和 /proc/self/maps(节选):
|
||||
|
||||
```c
|
||||
int inited = 1024; // 数据段
|
||||
int uninited; // BSS
|
||||
int main(void) {
|
||||
int local = 0; // 栈
|
||||
char *small = malloc(64); // 堆
|
||||
char *big = malloc(1 << 20); // 超过 mmap 阈值,单独映射
|
||||
printf("代码 main %p\n", (void *)main);
|
||||
// ……依次打印 &inited、&uninited、small、big、printf、&local,再输出 /proc/self/maps
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
代码 main 0xaaaaafbc0a18
|
||||
数据 inited 0xaaaaafbe0010
|
||||
BSS uninited 0xaaaaafbe0018
|
||||
堆 small 0xaaaacdcad2a0
|
||||
映射 big 0xffff9928f010
|
||||
共享库 printf 0xffff993e0590
|
||||
栈 local 0xffffcdcad8fc
|
||||
aaaaafbc0000-aaaaafbc1000 r-xp 00000000 00:40 841313 /tmp/layout
|
||||
aaaaafbe0000-aaaaafbe1000 rw-p 00010000 00:40 841313 /tmp/layout
|
||||
aaaacdcad000-aaaacdcce000 rw-p 00000000 00:00 0 [heap]
|
||||
ffff9928f000-ffff99390000 rw-p 00000000 00:00 0
|
||||
ffff99390000-ffff9952a000 r-xp 00000000 00:40 831743 /usr/lib/aarch64-linux-gnu/libc.so.6
|
||||
ffffcdc8d000-ffffcdcae000 rw-p 00000000 00:00 0 [stack]
|
||||
$ ulimit -s
|
||||
8192
|
||||
```
|
||||
|
||||
再运行一次,各区域的起始地址都变了(例如 main 为 0xaaaabc070a18、local 为 0xffffd6a9311c),上下顺序不变。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014.
|
||||
- Silberschatz A, Galvin P B, Gagne G. Operating System Concepts [M]. Wiley, 2018.
|
||||
- [Page Tables — The Linux Kernel documentation](https://docs.kernel.org/mm/page_tables.html)
|
||||
- [getrusage(2) — Linux manual page](https://man7.org/linux/man-pages/man2/getrusage.2.html)
|
||||
- [proc_pid_maps(5) — Linux manual page](https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html)
|
||||
- [mallopt(3) — Linux manual page](https://man7.org/linux/man-pages/man3/mallopt.3.html)
|
||||
|
||||
Reference in New Issue
Block a user