现代操作系统 · 第三章《内存管理》
·
<?php
/**
* ============================================================
* 现代操作系统 · 第三章《内存管理》大白话 + 代码例子
* ============================================================
* 作用:54 个内存管理概念,每条 = 大白话 + 一段 PHP 代码示例。
* 说明:内存管理多为算法与机制,几乎全部用 PHP 代码模拟其思想。
* 运行:php memory.php 全部
* php memory.php 132 看第 132 条(LRU)
* php memory.php 页表 关键词搜索
* ============================================================
*/
declare(strict_types=1);
final class Topic
{
public function __construct(
public readonly int $no,
public readonly string $title,
public readonly string $plain,
public readonly string $code
) {}
}
final class MemoryBook
{
/** @var Topic[] */
private array $topics = [];
public function __construct()
{
$this->load();
}
private function load(): void
{
$data = [
106 => ['无存储器抽象',
'早期没有抽象,程序直接用物理地址,一次只能跑一个,多了就互相踩内存。',
<<<'CODE'
// 程序直接写物理地址, 没有任何隔离
$physicalMem[1000] = "程序A的数据";
$physicalMem[1000] = "程序B的数据"; // 直接覆盖A! 互相踩
CODE],
107 => ['一种存储器抽象:地址空间',
'给每个程序一套自己的“虚拟地址”,由系统映射到真实物理内存,互不干扰。',
<<<'CODE'
function toPhysical($pid, $virtual, $base) {
return $base[$pid] + $virtual; // 虚拟地址 + 基址 = 物理地址
}
echo toPhysical('A', 100, ['A'=>5000, 'B'=>9000]); // 5100
CODE],
108 => ['地址空间的概念',
'一个进程能用的全部地址范围,用基址+界限保证不越界。',
<<<'CODE'
$base = 5000; $limit = 1000; // 起点 + 长度
function access($addr, $base, $limit) {
if ($addr >= $limit) die("越界!");
return $base + $addr; // 重定位到物理地址
}
CODE],
109 => ['交换技术',
'内存不够时,把整个进程暂时挪到硬盘,需要时再换回来。',
<<<'CODE'
function swapOut($proc, &$mem, &$disk) {
$disk[$proc] = $mem[$proc]; // 整个进程搬到硬盘
unset($mem[$proc]); // 腾出内存
}
// 需要时 swapIn 再搬回内存
CODE],
110 => ['空闲内存管理',
'系统得记住哪些内存空着、哪些被占,两大记法:位图和链表。',
<<<'CODE'
// 两种记账方式
$bitmap = [1,1,0,0,1,0]; // 1=占用 0=空闲
$list = [['free',0,2], ['used',2,3]]; // 段链表
CODE],
111 => ['位图存储管理',
'把内存切成小块,每块用 1 个比特记占用还是空闲。找空间要扫一长串 0。',
<<<'CODE'
$bitmap = [1,1,0,0,0,1]; // 每位代表一块
function findFree($bitmap, $need) { // 找连续 need 个 0
$run = 0;
foreach ($bitmap as $i => $b) {
$run = $b ? 0 : $run + 1;
if ($run == $need) return $i - $need + 1;
}
return -1;
}
echo findFree($bitmap, 3); // 2
CODE],
112 => ['链表存储管理',
'用链表把内存按“占用/空闲”分段串起来,分配时按首次适配等策略找。',
<<<'CODE'
$segments = [
['hole', 0, 100], // 空洞: 起址0, 长100
['proc', 100, 50],
['hole', 150, 200],
];
// 首次适配: 从头找第一个够大的 hole
foreach ($segments as $s)
if ($s[0]=='hole' && $s[2] >= 80) { echo "用 {$s[1]}\n"; break; }
CODE],
113 => ['虚拟内存',
'让程序用的地址比真实内存还大,用不到的部分先放硬盘,用时再调进来。',
<<<'CODE'
$virtualSize = 16; // 程序以为有16块
$physicalSize = 4; // 实际只有4块物理内存
// 多出来的12块暂存硬盘, 按需调入
echo "虚拟$virtualSize 块 / 物理$physicalSize 块\n";
CODE],
114 => ['分页',
'把虚拟内存切成固定大小的“页”,物理内存切成同样大的“页框”,按页搬运。',
<<<'CODE'
define('PAGE_SIZE', 4096); // 每页 4KB
function split($virtualAddr) {
return [
'page' => intdiv($virtualAddr, PAGE_SIZE), // 第几页
'offset' => $virtualAddr % PAGE_SIZE, // 页内偏移
];
}
print_r(split(9000)); // page=2, offset=808
CODE],
115 => ['页表',
'一张对照表,记录每个虚拟页对应哪个物理页框。',
<<<'CODE'
$pageTable = [ // 虚拟页 => 物理页框
0 => 5,
1 => 2,
2 => 7,
];
$frame = $pageTable[2]; // 虚拟页2 -> 页框7
CODE],
116 => ['页面',
'虚拟地址空间里切出来的固定大小的块,是搬运的最小单位。',
<<<'CODE'
$page = [
'number' => 2,
'size' => 4096,
'data' => str_repeat('x', 4096),
];
CODE],
117 => ['页框',
'物理内存里和页同样大小的格子,页就装进页框里。',
<<<'CODE'
$frames = [0=>null, 1=>null, 2=>null, 3=>null]; // 4个空页框
$frames[2] = ['page'=>5]; // 把虚拟页5装进2号页框
CODE],
118 => ['缺页中断',
'访问的页不在内存里,硬件触发中断,由系统从硬盘把它调进来。',
<<<'CODE'
function access($page, $pageTable, &$mem, $disk) {
if (!isset($pageTable[$page])) { // 不在内存 -> 缺页
echo "缺页中断! 从硬盘调入页$page\n";
$mem[] = $disk[$page]; // 调入
}
}
CODE],
119 => ['页表项的结构',
'每个页表项除了页框号,还有在不在内存、改没改、能不能写等标志位。',
<<<'CODE'
$entry = [
'frame' => 7, // 页框号
'present' => 1, // 在内存吗
'modified' => 0, // 脏页吗(改过吗)
'referenced' => 1, // 被访问过吗
'protect' => 'rw', // 读写权限
];
CODE],
120 => ['加速分页过程',
'每次访存都查页表太慢,于是用硬件缓存(TLB)和多级表来提速。',
<<<'CODE'
// 两大提速手段
$tlb = []; // 1) TLB 缓存常用映射
function fastLookup($page, &$tlb, $pageTable) {
return $tlb[$page] ?? ($tlb[$page] = $pageTable[$page]);
}
CODE],
121 => ['转换检测缓冲区 TLB',
'放在 CPU 里的小缓存,存最近用过的“页→页框”映射,命中就不查页表了。',
<<<'CODE'
$tlb = [2 => 7]; // 最近: 页2->框7
function translate($page, &$tlb, $pageTable) {
if (isset($tlb[$page])) return $tlb[$page]; // TLB命中, 极快
return $tlb[$page] = $pageTable[$page]; // 未命中, 查表并缓存
}
CODE],
122 => ['软件 TLB 管理',
'TLB 没命中时,不靠硬件而是让操作系统用软件去查页表、填 TLB。',
<<<'CODE'
function tlbMissHandler($page, &$tlb, $pageTable) {
// 由 OS 软件处理未命中(RISC 常见做法)
$tlb[$page] = $pageTable[$page]; // 软件填回 TLB
}
CODE],
123 => ['大内存的页表',
'地址空间巨大时页表会大到内存装不下,需要更省空间的结构。',
<<<'CODE'
// 64位地址 + 4KB页 = 页表项天文数字, 不能整张放内存
$bits = 64; $pageBits = 12;
$entries = 2 ** ($bits - $pageBits); // 项数大到离谱
echo "需要 $entries 个页表项\n"; // 所以要多级/倒排
CODE],
124 => ['多级页表',
'把页表拆成几层,只为真正用到的部分建下级表,大幅省空间。',
<<<'CODE'
function split($addr) {
return [
'dir' => ($addr >> 22) & 0x3FF, // 一级索引
'page' => ($addr >> 12) & 0x3FF, // 二级索引
'offset' => $addr & 0xFFF, // 页内偏移
];
}
print_r(split(0x00403004));
CODE],
125 => ['倒排页表',
'不按虚拟页建表,而按物理页框建表(框少表小),靠哈希查找。',
<<<'CODE'
// 按页框建表: 框号 => (进程, 虚拟页)
$inverted = [
5 => ['pid'=>1, 'vpage'=>2],
7 => ['pid'=>1, 'vpage'=>9],
];
// 查找用哈希: 给(pid,vpage)反查框号
CODE],
126 => ['页面置换算法',
'内存满了要调新页进来,得挑一个旧页踢出去,挑谁就是置换算法。',
<<<'CODE'
function needReplace($frames, $maxFrames) {
return count($frames) >= $maxFrames; // 满了就得踢一个
}
// 踢谁? -> FIFO / LRU / 时钟 ... 见下文
CODE],
127 => ['最优页面置换算法',
'踢掉“将来最久才会用到”的页,理论最好但需预知未来,无法实现,只作标杆。',
<<<'CODE'
function optimal($frames, $future) { // future=将来访问序列
$farthest = -1; $victim = null;
foreach ($frames as $p) {
$next = array_search($p, $future);
if ($next === false) return $p; // 将来不再用, 直接踢
if ($next > $farthest) { $farthest=$next; $victim=$p; }
}
return $victim; // 踢最晚才用到的
}
CODE],
128 => ['最近未使用算法 NRU',
'按“最近访问过吗 + 改过吗”分四类,优先踢两者都为 0 的页。',
<<<'CODE'
function nru($pages) { // 每页 [R, M]
foreach ([[0,0],[0,1],[1,0],[1,1]] as $class)
foreach ($pages as $p => $rm)
if ($rm == $class) return $p; // 从最低类挑一个踢
}
echo nru([0=>[1,1], 1=>[0,0], 2=>[1,0]]); // 1
CODE],
129 => ['先进先出算法 FIFO',
'谁最早进内存就先踢谁,简单但可能踢掉正在频繁用的页。',
<<<'CODE'
$queue = new SplQueue();
function fifoReplace($queue, $newPage) {
$victim = $queue->dequeue(); // 踢最老的
$queue->enqueue($newPage); // 新页入队尾
return $victim;
}
CODE],
130 => ['第二次机会算法',
'FIFO 改良:队头若最近被访问过(R=1),给它一次机会移到队尾并清 R。',
<<<'CODE'
function secondChance(&$queue) {
while (true) {
$p = array_shift($queue);
if ($p['R'] == 0) return $p; // R=0, 踢它
$p['R'] = 0; // R=1, 清零给机会
$queue[] = $p; // 移到队尾
}
}
CODE],
131 => ['时钟页面置换算法',
'把页排成一个环,指针像时钟一样转,R=1 就清零放过,R=0 就踢。',
<<<'CODE'
function clock(&$pages, &$hand) {
while (true) {
if ($pages[$hand]['R'] == 0) {
$victim = $hand;
$hand = ($hand + 1) % count($pages);
return $victim; // 踢它
}
$pages[$hand]['R'] = 0; // 给机会
$hand = ($hand + 1) % count($pages);
}
}
CODE],
132 => ['最近最少使用算法 LRU',
'踢掉“最久没被用过”的页,效果好,但要精确记录访问顺序,开销大。',
<<<'CODE'
function lruAccess(&$order, $page) {
if (($k = array_search($page, $order)) !== false)
unset($order[$k]); // 用过的移到最新
$order[] = $page; // 末尾=最近用
return array_values($order);
}
// 置换时踢 $order[0] (最久没用)
CODE],
133 => ['用软件模拟 LRU',
'硬件难精确实现 LRU,用每页一个计数器或移位寄存器近似。',
<<<'CODE'
// NFU: 每次时钟把 R 位加到计数器上
function nfu(&$counters, $R) {
foreach ($R as $page => $r)
$counters[$page] += $r; // 用得多计数大
}
// 置换时踢计数器最小的
CODE],
134 => ['老化算法',
'NFU 改良:计数器每次先右移再把 R 加到最高位,让近期访问权重更大。',
<<<'CODE'
function aging(&$counters, $R) {
foreach ($counters as $page => &$c) {
$c >>= 1; // 先右移(老化)
if ($R[$page]) $c |= 0b10000000; // R加到最高位
}
}
// 踢计数器最小者
CODE],
135 => ['工作集页面置换算法',
'只把进程“当前常用的一组页(工作集)”留在内存,其余踢掉,减少缺页。',
<<<'CODE'
function workingSet($accessTimes, $now, $tau) {
$ws = [];
foreach ($accessTimes as $page => $t)
if ($now - $t <= $tau) $ws[] = $page; // tau时间内用过的=工作集
return $ws; // 不在工作集的可踢
}
CODE],
136 => ['工作集时钟算法',
'把工作集思想装进时钟算法:转指针时综合 R 位和访问时间决定踢谁。',
<<<'CODE'
function wsClock(&$pages, &$hand, $now, $tau) {
$p = &$pages[$hand];
if ($p['R']) { $p['R'] = 0; $p['time'] = $now; } // 用过, 更新
elseif ($now - $p['time'] > $tau && !$p['M'])
return $hand; // 老且干净, 踢
$hand = ($hand + 1) % count($pages);
}
CODE],
137 => ['页面置换算法小结',
'各算法权衡:最优最好但不可行,LRU 接近最优但贵,老化/时钟实用折中。',
<<<'CODE'
$summary = [
'Optimal' => '最好, 不可实现(标杆)',
'FIFO' => '简单, 可能踢错',
'LRU' => '接近最优, 开销大',
'Aging' => '近似LRU, 实用',
'WSClock' => '综合最强, 常用',
];
CODE],
138 => ['局部性原理',
'程序总在一小段时间里反复访问相邻的少数页,这是分页能高效的根基。',
<<<'CODE'
// 访问序列常扎堆在邻近地址(局部性)
$accesses = [100, 101, 102, 100, 103, 101]; // 都挤在100附近
// 所以只缓存这一小撮页, 命中率就很高
CODE],
139 => ['分页系统中的设计问题',
'设计要权衡:页多大、内存怎么分配、何时换页、装多少进程合适。',
<<<'CODE'
$design = [
'pageSize' => 4096, // 页大小
'allocation' => 'global', // 局部还是全局分配
'fetchPolicy' => 'demand', // 按需调页
];
CODE],
140 => ['局部分配与全局分配策略',
'局部:只在本进程的页框里挑踢谁;全局:可踢任意进程的页。全局更灵活。',
<<<'CODE'
function chooseVictim($scope, $myFrames, $allFrames) {
return $scope == 'local'
? pickFrom($myFrames) // 只动自己的页
: pickFrom($allFrames); // 可动所有进程的页
}
CODE],
141 => ['负载控制',
'若所有进程都缺页(抖动),就把部分进程整个换出,减轻内存压力。',
<<<'CODE'
function loadControl(&$active, &$suspended, $pageFaultRate) {
if ($pageFaultRate > 0.9) // 缺页太高=抖动
$suspended[] = array_pop($active); // 暂停一个进程
}
CODE],
142 => ['页面大小',
'页小则内部碎片少但页表大;页大则页表小但浪费多,需折中。',
<<<'CODE'
function overhead($pageSize, $procSize, $entrySize) {
$tableSize = ($procSize / $pageSize) * $entrySize; // 页表开销
$internalFrag = $pageSize / 2; // 内部碎片
return $tableSize + $internalFrag; // 总开销, 求最小
}
CODE],
143 => ['分离的指令空间和数据空间',
'把代码和数据分成两套独立地址空间,翻倍可用空间也便于保护。',
<<<'CODE'
$ISpace = ['pageTable' => [], 'type' => '指令(只读)'];
$DSpace = ['pageTable' => [], 'type' => '数据(可写)'];
// 同一虚拟地址在I空间和D空间指向不同物理页
CODE],
144 => ['共享页面',
'多个进程映射到同一物理页,共用代码省内存(改了要写时复制)。',
<<<'CODE'
$frame = 7; // 同一物理页框
$procA_table[3] = $frame; // A的页3指向它
$procB_table[8] = $frame; // B的页8也指向它 -> 共享
CODE],
145 => ['共享库',
'像 libc 这类公共库,内存里只放一份,所有用它的进程共享。',
<<<'CODE'
$loadedLibs = [];
function loadLib($name, &$loadedLibs) {
if (isset($loadedLibs[$name]))
return $loadedLibs[$name]; // 已加载, 直接共享同一份
return $loadedLibs[$name] = "加载 $name 到内存";
}
CODE],
146 => ['内存映射文件',
'把文件直接映射进进程地址空间,读写内存就等于读写文件,免去 read/write。',
<<<'CODE'
// 把文件映射成内存数组, 改内存=改文件
$mmap = str_split(file_get_contents('a.txt'));
$mmap[0] = 'X'; // 改内存
file_put_contents('a.txt', implode('', $mmap)); // 回写
CODE],
147 => ['清除策略',
'后台“分页守护进程”定期把脏页提前写盘,保证总有干净页框可用。',
<<<'CODE'
function pagingDaemon(&$frames) {
foreach ($frames as &$f)
if ($f['M']) { writeToDisk($f); $f['M'] = 0; } // 脏页提前刷盘
}
// 这样缺页时随手就有干净页框可踢
CODE],
148 => ['虚拟内存接口',
'系统给程序提供控制虚拟内存的接口,如共享、映射、命名内存区。',
<<<'CODE'
// 程序可主动控制映射(类似 mmap/munmap)
$region = vm_map(name: 'shared1', size: 4096);
vm_share($region, withProcess: 'B'); // 与B共享这块区
vm_unmap($region);
CODE],
149 => ['与分页有关的工作',
'系统需在进程创建、执行、缺页、终止四个时机做对应分页管理。',
<<<'CODE'
$pagingWork = [
'create' => '建页表, 分配交换区',
'execute' => '复位TLB, 调入部分页',
'pagefault' => '定位缺页, 调入',
'terminate' => '回收页表和页框',
];
CODE],
150 => ['缺页中断处理',
'缺页发生后的标准流程:陷入内核→找页→挑页框→调入→更新页表→重试指令。',
<<<'CODE'
function handlePageFault($page, &$mem, $disk, $pageTable) {
$frame = findFreeFrame($mem) ?? evictPage($mem); // 1.找/腾页框
$mem[$frame] = $disk[$page]; // 2.调入
$pageTable[$page] = ['frame'=>$frame, 'present'=>1];// 3.更新页表
return 'RETRY'; // 4.重执行原指令
}
CODE],
151 => ['指令备份',
'一条指令执行到一半缺页,要能把它“撤回”从头重做,靠记录指令起点。',
<<<'CODE'
$instructionStart = $pc; // 执行前记下起点
try {
execute($instruction);
} catch (PageFault $e) {
$pc = $instructionStart; // 缺页则回退, 调页后重做
}
CODE],
152 => ['锁定内存中的页面',
'正在做 I/O 的页要“钉住”,防止被换出去导致数据写错地方。',
<<<'CODE'
function pinPage(&$frame) { $frame['locked'] = true; } // 钉住
function canEvict($frame) { return empty($frame['locked']); } // 锁住的不能踢
CODE],
153 => ['后备存储',
'硬盘上专门留一块“交换区”,存放被换出的页。',
<<<'CODE'
$swapArea = []; // 硬盘上的交换分区
function evictTo(&$swapArea, $page, $data) {
$swapArea[$page] = $data; // 换出的页存这里
}
CODE],
154 => ['策略与机制的分离',
'把“怎么搬页”(机制,在内核)和“该搬哪页”(策略,可在用户态)分开,更灵活。',
<<<'CODE'
// 机制: 内核负责实际搬运
function mechanism_movePage($page, $frame) { /* 内核搬运 */ }
// 策略: 外部决定搬哪个(可换不同算法)
function policy_whichVictim($pages) { return lru($pages); }
CODE],
155 => ['分段',
'按程序的逻辑结构(代码段、栈段、堆段)分成可独立增长的段,而非一刀切。',
<<<'CODE'
$segments = [
0 => ['name'=>'代码', 'base'=>0, 'limit'=>2000],
1 => ['name'=>'数据', 'base'=>4000, 'limit'=>1000],
2 => ['name'=>'栈', 'base'=>8000, 'limit'=>500],
];
// 地址 = (段号, 段内偏移)
CODE],
156 => ['纯分段的实现',
'段长不一,分配回收会在内存留下大小不一的空洞(外部碎片),需整理。',
<<<'CODE'
function segAddr($seg, $offset, $segTable) {
$s = $segTable[$seg];
if ($offset >= $s['limit']) die("段越界");
return $s['base'] + $offset; // 段基址 + 偏移
}
// 长期运行会产生外部碎片, 需紧缩(compaction)
CODE],
157 => ['分段与分页结合',
'先把程序分段(逻辑清晰),再把每段分页(消除外部碎片),两者优点兼得。',
<<<'CODE'
function translate($seg, $page, $offset, $segTable) {
$pageTable = $segTable[$seg]['pageTable']; // 每段有自己的页表
$frame = $pageTable[$page];
return $frame * PAGE_SIZE + $offset;
}
CODE],
158 => ['MULTICS 的分段与分页',
'经典系统:每个进程有大段表,每段再分页,靠描述符 + TLB 加速。',
<<<'CODE'
$multics = [
'segmentTable' => [ // 段号 -> 段描述符
0 => ['pageTable'=>[/*该段的页*/], 'protect'=>'rw'],
],
'tlb' => [], // 缓存(段,页)->框
];
CODE],
159 => ['Intel x86 的分段与分页',
'x86 用段选择子+GDT/LDT 做分段,再叠加多级页表分页(现代系统多把段做扁平)。',
<<<'CODE'
// 逻辑地址 -> (分段) -> 线性地址 -> (分页) -> 物理地址
function x86($selector, $offset, $gdt, $pageDir) {
$linear = $gdt[$selector]['base'] + $offset; // 分段
return paging($linear, $pageDir); // 再分页
}
CODE],
];
foreach ($data as $no => [$title, $plain, $code]) {
$this->topics[$no] = new Topic($no, $title, $plain, $code);
}
}
public function renderAll(): void
{
$this->printHeader('现代操作系统 · 第三章《内存管理》大白话 + 代码例子');
foreach ($this->topics as $t) $this->printTopic($t);
$this->printFooter(count($this->topics));
}
public function renderOne(int $no): void
{
if (!isset($this->topics[$no])) {
echo "⚠️ 没有第 {$no} 条(本章 106~159)\n";
return;
}
$this->printHeader("第 {$no} 条");
$this->printTopic($this->topics[$no]);
}
public function search(string $kw): void
{
$hit = array_filter(
$this->topics,
fn(Topic $t) => mb_strpos($t->title, $kw) !== false
|| mb_strpos($t->plain, $kw) !== false
|| mb_strpos($t->code, $kw) !== false
);
if (!$hit) { echo "🔍 没找到包含“{$kw}”的条目\n"; return; }
$this->printHeader("搜索“{$kw}”,命中 " . count($hit) . " 条");
foreach ($hit as $t) $this->printTopic($t);
}
private function printHeader(string $title): void
{
echo "\n" . str_repeat('=', 62) . "\n {$title}\n" . str_repeat('=', 62) . "\n";
}
private function printTopic(Topic $t): void
{
printf("\n【%03d】%s\n", $t->no, $t->title);
echo " ▷ 大白话:" . wordwrap_cn($t->plain, 36, "\n ") . "\n";
echo " ▶ 代码例子:\n";
foreach (explode("\n", $t->code) as $line) {
echo " | " . $line . "\n";
}
}
private function printFooter(int $count): void
{
echo "\n" . str_repeat('-', 62) . "\n";
echo " 共 {$count} 条,每条含“大白话 + PHP代码例子”。\n";
echo str_repeat('-', 62) . "\n";
}
}
/** 中文换行 */
function wordwrap_cn(string $text, int $width, string $break): string
{
$len = mb_strlen($text);
if ($len <= $width) return $text;
$out = '';
for ($i = 0; $i < $len; $i += $width) {
$out .= mb_substr($text, $i, $width);
if ($i + $width < $len) $out .= $break;
}
return $out;
}
// ===================== 入口 =====================
$book = new MemoryBook();
$arg = $argv[1] ?? null;
if ($arg === null) $book->renderAll();
elseif (ctype_digit($arg)) $book->renderOne((int) $arg);
else $book->search($arg);
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)