<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>虚拟内存 on 大飞的博客</title>
    <link>https://www.dafei.me/tags/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/</link>
    <description>Recent content in 虚拟内存 on 大飞的博客</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Wed, 03 Jun 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.dafei.me/tags/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>自制 x86-64 内核（46）：mmap 文件映射</title>
      <link>https://www.dafei.me/posts/os-46-mmap/</link>
      <pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://www.dafei.me/posts/os-46-mmap/</guid>
      <description>&lt;p&gt;上一章实现了 TCP 服务端，这章来实现一个经典的内存管理功能：&lt;strong&gt;mmap 文件映射&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;什么是-mmap-文件映射&#34;&gt;什么是 mmap 文件映射&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt; 把文件的一段内容映射到进程的虚拟地址空间。映射后可以像读内存一样读文件——不需要 &lt;code&gt;read()&lt;/code&gt; 系统调用，直接解引用指针。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; fd &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;open&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/bin/busybox&amp;#34;&lt;/span&gt;, O_RDONLY);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;p &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mmap&lt;/span&gt;(NULL, &lt;span style=&#34;color:#ae81ff&#34;&gt;4096&lt;/span&gt;, PROT_READ, MAP_PRIVATE, fd, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// p[0..3] 就是文件的前 4 字节（ELF magic: 7f 45 4c 46）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;关键设计vma-和-lazy-分配&#34;&gt;关键设计：VMA 和 Lazy 分配&lt;/h2&gt;
&lt;p&gt;mmap 不会立刻分配物理内存，而是&lt;strong&gt;登记一个 VMA（虚拟内存区域）&lt;/strong&gt;，等到进程真正访问这段地址时，才通过&lt;strong&gt;缺页异常&lt;/strong&gt;按需读取文件内容并建立页表映射。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mmap(fd) → 注册 VMA（vaddr, len, inum, offset） → 返回虚拟地址
                        ↓ 进程访问该地址
              #PF not-present → 找到 VMA → 分配物理页
                              → ext2_read(inum, offset, page)
                              → 建立页表 → 重试指令
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这就是操作系统课上说的&amp;quot;按需分页&amp;quot;（demand paging）。&lt;/p&gt;
&lt;h2 id=&#34;实现步骤&#34;&gt;实现步骤&lt;/h2&gt;
&lt;h3 id=&#34;1-定义-vma-结构&#34;&gt;1. 定义 VMA 结构&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;process_t&lt;/code&gt; 中加入 VMA 数组：&lt;/p&gt;</description>
    </item>
    <item>
      <title>从零写OS（十八）：fork 与 Copy-on-Write</title>
      <link>https://www.dafei.me/posts/os-18-fork-cow/</link>
      <pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate>
      <guid>https://www.dafei.me/posts/os-18-fork-cow/</guid>
      <description>&lt;p&gt;上一章每个进程有了自己的地址空间。这一章实现 &lt;code&gt;fork()&lt;/code&gt;——创建一个子进程，继承父进程的全部内存。&lt;/p&gt;
&lt;h2 id=&#34;最朴素的-fork-实现&#34;&gt;最朴素的 fork 实现&lt;/h2&gt;
&lt;p&gt;fork 的语义是&amp;quot;完整复制当前进程&amp;quot;。最直接的做法：遍历父进程页表，找到每一个物理页，分配新页，复制 4096 字节内容，给子进程建新映射。&lt;/p&gt;
&lt;p&gt;能用，但很浪费。大多数 &lt;code&gt;fork()&lt;/code&gt; 之后会紧接着 &lt;code&gt;exec()&lt;/code&gt;——旧内存根本用不上，全拷了白拷。进程堆如果有几十 MB，每次 fork 都要等好几毫秒。&lt;/p&gt;
&lt;h2 id=&#34;copy-on-write先共享写了再分&#34;&gt;Copy-on-Write：先共享，写了再分&lt;/h2&gt;
&lt;p&gt;CoW 的思路是：&lt;strong&gt;fork 时不复制，让父子共享同一批物理页；等到谁要写，再给他一份新的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实现上分两步：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：fork 时打标记&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;遍历父进程用户页表，对每一页做两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;清掉 WRITABLE 位，变成只读&lt;/li&gt;
&lt;li&gt;打上 PAGE_COW 标志（借用 x86 页表的 bit 9，这一位 CPU 不使用，留给软件自定义）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;子进程页表复制同一个物理页地址，同样只读 + CoW。&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;fork 后：
  父进程 → PT → 物理页 0xA000  (只读, CoW)
                    ↑ 共享
  子进程 → PT → 物理页 0xA000  (只读, CoW)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;注意一个关键细节：&lt;strong&gt;PDPT/PD/PT 这三级结构页必须为子进程单独分配&lt;/strong&gt;。如果父子共用同一棵结构树，后续 &lt;code&gt;vmm_map_page&lt;/code&gt; 修改子进程时会把父进程的 PT 一起改掉，隔离失效。数据页可以共享，结构页不能。&lt;/p&gt;
&lt;p&gt;另一个细节：改完父进程页表后必须刷新 TLB。否则 CPU 缓存里还是旧的可写映射，父进程写该页不会触发 fault，CoW 形同虚设。&lt;/p&gt;</description>
    </item>
    <item>
      <title>从零写OS（十七）：每个进程有自己的地址空间</title>
      <link>https://www.dafei.me/posts/os-17-address-space/</link>
      <pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate>
      <guid>https://www.dafei.me/posts/os-17-address-space/</guid>
      <description>&lt;p&gt;前几章的进程共享同一张页表——所有进程看到的是同一片内存。这意味着进程 A 知道进程 B 的地址，就能直接读写它的数据。一个 bug 就能破坏整个系统。&lt;/p&gt;
&lt;p&gt;这一章解决这个问题：&lt;strong&gt;给每个进程一张自己的页表，互相看不见彼此的内存&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;切换页表就是切换世界&#34;&gt;切换页表就是切换世界&lt;/h2&gt;
&lt;p&gt;x86-64 的虚拟地址翻译规则写在页表里，页表的根地址放在 &lt;strong&gt;CR3 寄存器&lt;/strong&gt;里。&lt;/p&gt;
&lt;p&gt;这意味着：写 CR3 就是切换地址空间。进程 A 跑的时候 CR3 指向 A 的页表，进程 B 跑的时候 CR3 指向 B 的页表。同一个虚拟地址 &lt;code&gt;0x400000&lt;/code&gt;，在 A 里翻译到物理页 X，在 B 里翻译到物理页 Y——两边完全隔离，互不干扰。&lt;/p&gt;
&lt;p&gt;切换进程时，只需要一行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;vmm_switch&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;uint64_t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pml4) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    __asm__ &lt;span style=&#34;color:#66d9ef&#34;&gt;volatile&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mov %0, %%cr3&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;::&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;r&amp;#34;&lt;/span&gt;((&lt;span style=&#34;color:#66d9ef&#34;&gt;uint64_t&lt;/span&gt;)pml4) &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;硬件帮我们做了全部翻译工作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;创建新页表&#34;&gt;创建新页表&lt;/h2&gt;
&lt;p&gt;每个进程需要自己的 PML4。但不能从空白开始——内核代码的映射必须保留，否则切过去之后 CPU 取不到内核指令，立刻 Page Fault。&lt;/p&gt;
&lt;p&gt;做法：把内核的 &lt;code&gt;kernel_pml4&lt;/code&gt; 整张复制一份作为起点，然后再往里加用户空间的映射。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;uint64_t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;vmm_create_page_table&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;uint64_t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;new_pml4 &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;uint64_t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;)&lt;span style=&#34;color:#a6e22e&#34;&gt;pmm_alloc&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; i &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;512&lt;/span&gt;; i&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        new_pml4[i] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; kernel_pml4[i];  &lt;span style=&#34;color:#75715e&#34;&gt;// 继承内核映射
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; new_pml4;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;往指定页表里建映射&#34;&gt;往指定页表里建映射&lt;/h2&gt;
&lt;p&gt;之前的 &lt;code&gt;map_page&lt;/code&gt; 只能操作当前 CR3 指向的页表。现在需要给进程建映射，但不想先切换过去，所以新接口接受一个显式的 &lt;code&gt;pml4&lt;/code&gt; 参数：&lt;/p&gt;</description>
    </item>
    <item>
      <title>从零写OS（七）：虚拟内存，给每个程序一个假的地址空间</title>
      <link>https://www.dafei.me/posts/os-07-vmm/</link>
      <pubDate>Wed, 06 May 2026 07:00:00 +0000</pubDate>
      <guid>https://www.dafei.me/posts/os-07-vmm/</guid>
      <description>&lt;p&gt;上一章内核能分配物理页了，但用的全是物理地址。&lt;/p&gt;
&lt;p&gt;物理地址有个问题：全局唯一，谁都能访问。进程 A 如果知道进程 B 的物理地址，直接就能读写它的数据。这不行。&lt;/p&gt;
&lt;p&gt;解决方案是&lt;strong&gt;虚拟内存&lt;/strong&gt;：每个程序看到的地址都是&amp;quot;假的&amp;quot;，CPU 访问时由硬件自动翻译成真实的物理地址。程序互相隔离，谁也看不见谁。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;地址翻译的硬件机制四级页表&#34;&gt;地址翻译的硬件机制：四级页表&lt;/h2&gt;
&lt;p&gt;x86-64 的地址翻译靠 &lt;strong&gt;MMU&lt;/strong&gt;（Memory Management Unit，内存管理单元，CPU 内部硬件，负责把虚拟地址翻译成物理地址）完成，翻译规则写在页表里，页表的根地址放在 CR3 寄存器里。&lt;/p&gt;
&lt;p&gt;一个 64 位虚拟地址被这样拆开：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;63        48 47    39 38    30 29    21 20    12 11       0
[  符号扩展  | PML4_IDX | PDPT_IDX | PD_IDX | PT_IDX | 页内偏移 ]
               9 bit      9 bit      9 bit    9 bit    12 bit
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;翻译过程是四级查表：&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;CR3 → PML4[PML4_IDX] → PDPT[PDPT_IDX] → PD[PD_IDX] → PT[PT_IDX] → 物理页帧
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每级页表是一个 512 项的数组，每项 8 字节，刚好占满一个 4KB 页。每项的低 12 位是 flags，高位是下一级页表（或最终物理页）的地址。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
