<?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);
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐