<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>wendaining</title>
    <link>https://blog.wendain.ing/</link>
    <description>wendaining</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, wendaining</copyright>
    <lastBuildDate>Sat, 22 Aug 2026 17:43:01 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <atom:link href="https://blog.wendain.ing/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>给博客换个入口：Cloudflare Pages 主站与 GitHub Pages 镜像</title>
      <link>https://blog.wendain.ing/2026/08/22/blog-migration-to-cloudflare-pages/</link>
      <description>把博客的正式入口迁移到 Cloudflare Pages，同时保留 GitHub Pages 作为独立镜像；原有的 Blog-Source 和 GitHub Actions 发布流程继续使用。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%8D%9A%E5%AE%A2/">博客</category>
      <category domain="https://blog.wendain.ing/tags/%E5%8D%9A%E5%AE%A2/">博客</category>
      <category domain="https://blog.wendain.ing/tags/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/Cloudflare/">Cloudflare</category>
      <category domain="https://blog.wendain.ing/tags/GitHub-Actions/">GitHub Actions</category>
      <pubDate>Sat, 22 Aug 2026 17:43:01 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="TL-DR">TL;DR</h2><p>将博客的正式入口从 <code>blog.wendaining.top</code> 换成了 <code>blog.wendain.ing</code>，实际内容改由 Cloudflare Pages 托管。同时保留 <code>wendaining.github.io</code>，仅仅指向 GitHub Pages，使之继续作为一份可以直接访问的镜像，同时也是适应原来的工作流。</p><p>原来的工作流不变：文章等内容仍然放在 <code>Blog-Source</code> 分支。每次向这个分支提交，GitHub Actions 和 Cloudflare Pages 会分别构建并发布同一份 Hexo 博客。</p><h2 id="为什么要迁移">为什么要迁移</h2><p>最主要的原因是买了新域名一直没用（这个确实很cool啊x</p><p>之前博客完全托管在 GitHub Pages 上，域名 <code>blog.wendaining.top</code> 也直接指向 <code>wendaining.github.io</code>。GitHub 在部分网络环境下访问并不稳定。</p><p>所以这次没有放弃 GitHub Pages，而是增加 Cloudflare Pages 作为正式站点：日常访问默认落到 Cloudflare，GitHub Pages 则保留为备用镜像。</p><p>我之前写过的<a href="/2026/04/27/hexo-workflow-based-on-github-actions/">基于 GitHub Actions 部署 Hexo 博客的 workflow</a>仍然继续工作。</p><h2 id="迁移过程中做了什么">迁移过程中做了什么</h2><h3 id="建立-Cloudflare-Pages">建立 Cloudflare Pages</h3><p>Cloudflare Pages 直接连接同一个 GitHub 仓库，并监听 <code>Blog-Source</code> 分支。构建配置和 GitHub Actions 保持一致：</p><ul><li>Node.js 22；</li><li>执行 <code>npm ci &amp;&amp; npx hexo generate</code>；</li><li>发布 <code>public</code> 目录。</li></ul><p>这样一来，一次提交会触发两条彼此独立的流水线：GitHub Actions 把静态文件发布到 <code>main</code>，Cloudflare Pages 则自行构建并发布到 Cloudflare。</p><h3 id="重新整理域名">重新整理域名</h3><p>现在的域名关系如下：</p><ul><li><code>blog.wendain.ing</code>：正式域名，由 Cloudflare Pages 提供内容；</li><li><code>blog.wendaining.top</code>：永久重定向到新域名，文章路径和查询参数都会保留；</li><li><code>wendaining-blog.pages.dev</code>：同样永久重定向到正式域名；</li><li><code>wendaining.github.io</code>：保留为可直接访问的 GitHub Pages 镜像。</li></ul><h2 id="现在的架构">现在的架构</h2><pre><code class=" mermaid">flowchart TD    A["向 Blog-Source 提交文章或配置"]    B["GitHub Actions 构建"]    C["Cloudflare Pages 构建"]    D["main 分支：生成后的静态文件"]    E["wendaining.github.io&lt;br/&gt;GitHub Pages 镜像"]    F["blog.wendain.ing&lt;br/&gt;Cloudflare Pages 正式站"]    G["blog.wendaining.top"]    H["wendaining-blog.pages.dev"]    A --&gt; B    A --&gt; C    B --&gt; D    D --&gt; E    C --&gt; F    G --&gt;|301| F    H --&gt;|301| F</code></pre><h2 id="产生的影响">产生的影响</h2><h3 id="日常访问统一到了-Cloudflare">日常访问统一到了 Cloudflare</h3><p>从旧域名进入时，Cloudflare 会在边缘直接返回 301，然后浏览器访问 <code>blog.wendain.ing</code>。旧域名不再加载完整博客页面，也不会先绕到 GitHub Pages 再跳一次。</p><p>搜索引擎相关的站点 URL、Open Graph 和 RSS/Atom 元数据也已经改成新域名。</p><h3 id="发布流程基本没有变化">发布流程基本没有变化</h3><p>写文章时仍然只需要修改 <code>Blog-Source</code>。区别只是以前一次提交只有 GitHub Actions 构建，现在 Cloudflare Pages 也会同时构建。两边是独立流水线，因此更新时可能有几十秒的先后差异；只要两次构建都成功，最终内容就是一致的。</p><h2 id="最后">最后</h2><p>目前这个结构比较舒服：正常访问走 Cloudflare，GitHub Pages 不再承担主站流量，但仍然保留着一份随时可以访问和回退的静态博客。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Gorse Learning</title>
      <link>https://blog.wendain.ing/2026/08/22/gorse-learning/</link>
      <description>学习 Gorse 这个经典的 Golang 推荐系统项目。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://blog.wendain.ing/tags/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/Golang/">Golang</category>
      <pubDate>Sat, 22 Aug 2026 15:18:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><p>参考的资料：</p><p><a href="https://www.cnblogs.com/wanber/p/19451225">Gorse 推荐系统入门：从零到一构建推荐引擎 - 技术漫游 - 博客园</a></p><p><a href="https://deepwiki.com/gorse-io/gorse/1-overview">gorse-io/gorse | DeepWiki</a></p><p><a href="https://gorse.io/zh/">主页 | Gorse</a></p></blockquote><h2 id="入门">入门</h2><h3 id="什么是推荐系统">什么是推荐系统</h3><p>三个要素：</p><ul><li>记录行为</li><li>理解兴趣</li><li>预测可能喜欢</li></ul><h3 id="启动与使用">启动与使用</h3><p>直接使用 <code>docker</code> 启动（参阅官方文档）。</p><p>基于 Web 的，直接访问 <code>localhost:8088</code> 可以浏览。</p><p>使用 <code>curl</code> 进行插入数据、创建物品、插入反馈、获取推荐等。</p><h3 id="核心工作原理的概述">核心工作原理的概述</h3><p>四个核心概念：</p><ul><li>用户<ul><li>基础信息：ID、标签等（如年龄、性别）</li><li>行为历史：浏览、点击、购买...</li></ul></li><li>物品<ul><li>基础信息：ID、标签</li><li>统计数据：热度、评分</li></ul></li><li>反馈<ul><li>用户+物品+类型+时间</li></ul></li><li>推荐<ul><li>根据历史行为预测用户可能喜欢的物品</li></ul></li></ul><p>流程图：</p><ol><li>用户产生行为</li><li>系统记录反馈</li><li>模型定期执行分析、训练</li><li>生成推荐</li><li>用户看到推荐</li><li>循环迭代，回到 1</li></ol><p>Gorse 的推荐策略：<strong>多源融合</strong>。</p><ol><li><strong>协同过滤</strong>：找到相似用户，推荐他们喜欢的物品</li><li><strong>物品相似</strong>：推荐和用户历史物品相似的其他物品</li><li><strong>热门推荐</strong>：推荐最热门的物品</li><li><strong>最新推荐</strong>：字面意思</li></ol><p>所谓融合策略：推荐结果 = 30% 协同过滤 +  30% 物品相似 +  20% 热门推荐 +  20% 最新推荐</p><h3 id="Gorse-的架构设计">Gorse 的架构设计</h3><p>三层架构：</p><pre><code class=" mermaid">flowchart TD    A["用户 / 应用&lt;br/&gt;Web · App · 小程序"]    B["Server 节点&lt;br/&gt;RESTful API + 实时推荐"]    C["Master 节点&lt;br/&gt;模型训练 + 任务调度 + Dashboard"]    D["Worker 节点（多个）&lt;br/&gt;离线计算 + 批量推荐"]    E["存储层&lt;br/&gt;MySQL + Redis&lt;br/&gt;用户数据 + 物品数据 + 推荐缓存"]    A --&gt;|HTTP / HTTPS| B    B --&gt;|gRPC| C    C --&gt;|gRPC| D    D --&gt; E</code></pre><div class="note note-info"><p>关于 <strong>gRPC</strong></p><p><strong>RPC</strong> = Remote Procedure Call，远程过程调用。核心思想就是「像调用本地函数一样调用另一台机器上的函数」。</p><p><strong>gRPC</strong> = Google 开源的一套 RPC 框架。</p><p><strong>Protobuf</strong> = gRPC 通常使用的数据描述和序列化格式。你会写一个 <code>.proto</code> 文件定义「有哪些函数、参数是什么、返回值是什么」。</p><p>比如 Gorse 架构里：</p><div class="code-wrapper"><pre><code class="hljs crmsh">Server  ──gRPC──&gt;  <span class="hljs-keyword">Master</span><span class="hljs-title">Master</span>  ──gRPC──&gt;  Worker</code></pre></div><p>假设 Master 提供一个函数：</p><div class="code-wrapper"><pre><code class="hljs stylus"><span class="hljs-function"><span class="hljs-title">GetModel</span><span class="hljs-params">(name string)</span></span> Model</code></pre></div><p>Server 想调用它。但问题是：<strong>Master 和 Server 是两个独立进程，甚至可能运行在不同机器上</strong>，Server 显然不能直接：</p><div class="code-wrapper"><pre><code class="hljs crmsh">model := <span class="hljs-literal">master</span>.GetModel(<span class="hljs-string">"ranking"</span>)</code></pre></div><p>gRPC 做的事情，就是让这种<strong>远程调用看起来很像普通函数调用</strong>：</p><div class="code-wrapper"><pre><code class="hljs autohotkey"><span class="hljs-built_in">model,</span> err := client.GetModel(ctx, request)</code></pre></div><p>实际上背后发生的是：</p><div class="code-wrapper"><pre><code class="hljs text">Server  │  │ 调用 GetModel(...)  ↓gRPC Client  │  │ 序列化成 Protobuf  │ 通过 HTTP/2 发送  ↓网络  ↓Master 上的 gRPC Server  │  │ 反序列化  ↓真正执行 GetModel(...)</code></pre></div></div><h4 id="Master-节点">Master 节点</h4><p>可以理解为 Gorse 架构的大脑。</p><ul><li>模型训练</li><li>AutoML： Automated Machine Learning（自动机器学习）</li><li>任务调度，触发 Worker</li><li>Dashboard：监控、数据管理</li></ul><h4 id="Worker-节点">Worker 节点</h4><p>理解为 Gorse 架构的手脚。</p><ul><li>批量推荐：为每个用户生成推荐列表</li><li>相似度计算：计算物品之间的相似，计算用户之间的相似度</li><li>水平扩展：启动多个 Worker，负载均衡</li></ul><h4 id="Server-节点">Server 节点</h4><p>理解为「嘴巴」。</p><ul><li>提供 RESTful API</li><li>实时推荐</li><li>在线更新</li></ul><h4 id="综合">综合</h4><pre><code class=" mermaid">flowchart LR    A["用户行为"] --&gt; B["Server"]    B --&gt; C["DataStore&lt;br/&gt;MySQL"]    C --&gt; D["Master&lt;br/&gt;定期加载数据"]    D --&gt; E["训练模型"]    E --&gt; F["Worker&lt;br/&gt;计算推荐"]    F --&gt; G["CacheStore&lt;br/&gt;Redis"]    G --&gt; H["Server"]    H --&gt; I["返回用户"]    %% 局部调整布局    subgraph Train[" "]        direction TB        D --&gt; E    end    subgraph Recommend[" "]        direction TB        F --&gt; G    end</code></pre><h2 id="理解-Gorse-的管道（pipeline）">理解 Gorse 的管道（pipeline）</h2><p><a href="https://gorse.io/zh/docs/concepts/pipeline.html">管道 | Gorse</a></p><img src="https://image.wendaining.top/2543ba0dadfa669975c7cf48b4c197e7.png" style="zoom: 80%;"><ul><li>数据源输入输入层之后，传入检索层</li><li>检索层由多个推荐器构成，为用户生成候选物品</li><li>排序层合并来自不同推荐器的所有输出，删除用户已经看过的物品（已读物品），并根据用户与剩余物品互动的可能性对其进行评分</li></ul><p>默认的管道是只推荐最新物品。</p><h3 id="管道中的缓存">管道中的缓存</h3><p>以下中间结果被缓存并定期更新：</p><ul><li>用户到用户推荐器的用户邻居。<ul><li>简单来说，用户 A 的相似用户是 B C D，那么这个结果会被缓存</li></ul></li><li>物品到物品推荐器的物品邻居。<ul><li>和上面同理，相似物品缓存</li></ul></li><li>非个性化推荐器的结果。<ul><li>和具体用户没有太大关系的内容，比如说最新榜、热门榜</li></ul></li><li>每个用户的排序器输出<ul><li>就是最后排序器输出的结果</li></ul></li></ul><h3 id="Gorse-工作原理">Gorse 工作原理</h3><p>管道以<strong>分布式</strong>方式执行。Gorse 中有三种类型的节点：主节点、工作节点和服务器节点。也就是上面说的 Master Worker Server，不再赘述。</p><h2 id="深入-Gorse-推荐系统：数据结构与存储层设计剖析">深入 Gorse 推荐系统：数据结构与存储层设计剖析</h2><p><a href="https://www.cnblogs.com/wanber/p/19451343">深入 Gorse 推荐系统：数据结构与存储层设计剖析 - 技术漫游 - 博客园</a></p><p>我感觉也是这个作者的 AI 文章自己洗了一遍...总之大概看看吧。</p><h3 id="字符串-索引的映射">字符串-&gt;索引的映射</h3><p>显然我们会有大量的 JSON（也可以是 Go 里面的 <code>map[string][]</code>），里面都是字符串作为 key。</p><p>字符串作为 key，效率很低（主要是哈希比较带来的开销是 $O(len(string))$</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab9 mmap - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/08/19/xv6-lab9-mmap/</link>
      <description>xv6 的第九个 lab，实现 mmap 机制，十分综合，包括了页表机制和文件系统的结合。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Wed, 19 Aug 2026 00:45:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<div class="note note-primary"><p>感觉最难的 lab...起码是代码量最大的。</p></div><h2 id="阅读">阅读</h2><p>事实上这就是 Lab5 COW 里面提到的 <a href="https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec08-page-faults-frans/8.1-page-fault-basics">8.1 Page Fault Basics | MIT6.S081</a> 这一章节里面所涉及的内容（一个小节）。所以需要读的内容也不多，只是一种机制。</p><p>memory mapped files 的核心思想是将完整或部分文件加载到内存中，从而直接使用内存的 <code>load</code> <code>store</code>  来操控文件，减少了磁盘 I/O。</p><p>需要实现的接口（来自 <code>man 2 mmap</code>）：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span> *<span class="hljs-title function_">mmap</span><span class="hljs-params">(<span class="hljs-type">void</span> *addr, <span class="hljs-type">size_t</span> len, <span class="hljs-type">int</span> prot, <span class="hljs-type">int</span> flags,</span><span class="hljs-params">           <span class="hljs-type">int</span> fd, <span class="hljs-type">off_t</span> offset)</span>;           <span class="hljs-type">int</span> <span class="hljs-title function_">munmap</span><span class="hljs-params">(<span class="hljs-type">void</span> *addr, <span class="hljs-type">size_t</span> len)</span>;</code></pre></div><p>这个 lab 不要求完全实现 POSIX 里面的规定，可以做出如下简化问题的假设：</p><p>对于 <code>mmap()</code>：</p><ul><li><p><code>addr</code> 总是 <code>0</code>，表示由 kernel 决定映射到哪个虚拟地址。</p></li><li><p><code>mmap</code> 成功时返回映射地址，失败时返回 <code>0xffffffffffffffff</code>。</p></li><li><p><code>len</code> 表示要映射的字节数，它可能和文件长度不同。</p></li><li><p><code>prot</code> 表示这段内存是否可读、可写和/或可执行。这个 Lab 中只需要处理 <code>PROT_READ</code>、<code>PROT_WRITE</code> 或两者组合。</p></li><li><p><code>flags</code> 只会是 <code>MAP_SHARED</code> 或 <code>MAP_PRIVATE</code>。</p></li><li><p><code>MAP_SHARED</code> 表示对映射内存的修改应该写回文件。</p></li><li><p><code>MAP_PRIVATE</code> 表示修改不应该写回文件。</p></li><li><p><code>fd</code> 是待映射文件的已打开 file descriptor。</p></li><li><p><code>offset</code> 可以假设总是 <code>0</code>，即总是从文件开头开始映射。</p></li><li><p>关于<strong>懒分配</strong>：这里采用的是<strong>懒分配模式</strong>。也就是说，<code>mmap()</code> 本身不应该分物理内存，也不应该读取文件。只有当 page fault 发生时，再在 <code>usertrap()</code> 或它调用的 page fault 处理代码里完成真正的分配和文件读取。</p><div class="note note-info"><p><strong>回忆 cow lab</strong></p></div></li><li><p>如果两个进程映射同一个 <code>MAP_SHARED</code> 文件，这个 Lab 允许它们 <strong>不共享同一个 physical page</strong>。</p></li></ul><p>关于 <code>munmap()</code>：</p><ul><li>删除指定范围内的 mmap 映射。</li><li>如果进程已经修改过这段内存，并且对应的是 <code>MAP_SHARED</code> 映射，那么在解除映射之前，修改应该先写回文件。</li><li>本来可能是只解除整个 mmap 映射中的一部分（从中间挖洞）（回忆拆 super page），但是这个 lab 是要么开头要么结尾要么整个，总之不会只在中间。</li></ul><h2 id="实现">实现</h2><p>依旧以 hints 为线索。</p><h3 id="建立系统调用">建立系统调用</h3><p>还是老一套流程。</p><h3 id="为每个进程记录-mmap-区域">为每个进程记录 mmap 区域</h3><p>建立每进程的 <code>struct vma</code>，即 Virtual Memory Area，虚拟内存区域，记录 <code>mmap()</code> 创建的虚拟地址范围信息。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/proc.h</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> {</span>  <span class="hljs-type">int</span> valid;  uint64 addr;  uint64 len;  uint64 offset;  <span class="hljs-type">int</span> prot;  <span class="hljs-type">int</span> flags;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span>;</span>};</code></pre></div><blockquote><p><code>offset</code> <code>len</code> 不使用和原来签名一样的类型。</p><p>原因是 <code>size_t</code> 和 <code>off_t</code> 属于 mmap 接口类型，目前定义在 <code>defs.h</code>；但很多源文件会先包含 <code>proc.h</code>、后包含 <code>defs.h</code>，导致解析 <code>struct vma</code> 时还不认识它们。</p><p><code>uint64</code> 定义在更基础的 <code>types.h</code> 中，而且在 xv6 里：</p><ul><li><code>size_t</code> 实际就是 64 位无符号整数；</li><li>本实验的 <code>offset</code> 固定为 0，不需要 <code>off_t</code> 的有符号语义；</li><li>虚拟地址、长度和文件偏移本身也适合用 <code>uint64</code> 保存。</li></ul><p>因此，用户接口和 <code>sys_mmap()</code> 参数仍按照 man page 使用：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">size_t</span> len;<span class="hljs-type">off_t</span> offset;</code></pre></div><p>而 VMA 是内核内部数据结构，用 <code>uint64</code> 保存转换后的数值即可。两层不要求使用完全相同的类型名称，只需数值能正确表示。</p></blockquote><p>然后，由于 xv6 kernel 中没有 variable-size kernel allocator，直接声明一个固定大小（16）的 VMA 数组，需要时从里面分配。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/def.h</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> NVMA 16</span></code></pre></div><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/proc.c</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> <span class="hljs-title">vma</span>[<span class="hljs-title">NVMA</span>];</span></code></pre></div><h3 id="实现-mmap">实现 <code>mmap()</code></h3><p>首先要理解 mmap 的映射区域到底位于虚拟地址空间的哪些部分：</p><p>地址空间可以大致理解为：</p><div class="code-wrapper"><pre><code class="hljs text">低地址┌──────────────────────┐│ 程序代码、数据、堆      │├──────────────────────┤ ← 原来的 p-&gt;sz┤│ 尚未使用的地址空间      │├──────────────────────┤ ← limit / TRAPFRAME│ TRAPFRAME            │├──────────────────────┤│ TRAMPOLINE           │└──────────────────────┘ ← MAXVA高地址</code></pre></div><p>mmap 区域不能覆盖最上面的两个特殊页面。</p><p>一开始（以及读到的知乎文章 <a href="https://www.zhihu.com/column/c_1633749695578210304">MIT XV6 操作系统 实验全解 - 知乎</a> ）里面的实现是这样的：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 69 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 69 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_mmap</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">void</span> *addr;  <span class="hljs-type">size_t</span> len;  <span class="hljs-type">int</span> prot, flags, fd;  <span class="hljs-type">off_t</span> offset;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  argaddr(<span class="hljs-number">0</span>, (uint64 *)&amp;addr);  argaddr(<span class="hljs-number">1</span>, &amp;len);  argint(<span class="hljs-number">2</span>, &amp;prot);  argint(<span class="hljs-number">3</span>, &amp;flags);  argaddr(<span class="hljs-number">5</span>, (uint64 *)&amp;offset);  <span class="hljs-comment">// This lab only supports kernel-selected addresses and mappings that</span>  <span class="hljs-comment">// start at the beginning of a regular file.</span>  <span class="hljs-keyword">if</span>(addr != <span class="hljs-number">0</span> || len == <span class="hljs-number">0</span> || offset != <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>((prot &amp; ~(PROT_READ | PROT_WRITE)) != <span class="hljs-number">0</span> ||     (prot &amp; (PROT_READ | PROT_WRITE)) == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(flags != MAP_SHARED &amp;&amp; flags != MAP_PRIVATE) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// 将 fd 对应的文件对应到 struct file *f 上</span>  <span class="hljs-keyword">if</span>(argfd(<span class="hljs-number">4</span>, &amp;fd, &amp;f) &lt; <span class="hljs-number">0</span> || f-&gt;type != FD_INODE || !f-&gt;readable) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(flags == MAP_SHARED &amp;&amp; (prot &amp; PROT_WRITE) &amp;&amp; !f-&gt;writable) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// 为 mmap 选择一段虚拟地址进行映射，</span>  <span class="hljs-comment">// p-&gt;sz 是目前的进程能用到的最高的地址空间，将新映射放在它后面</span>  addr = (<span class="hljs-type">void</span> *)PGROUNDUP(p-&gt;sz);  uint64 limit = MAXVA - <span class="hljs-number">2</span> * PGSIZE;  <span class="hljs-comment">// leave TRAPFRAME/TRAMPOLINE untouched</span>  <span class="hljs-keyword">if</span>((uint64)addr &gt;= limit || len &gt; limit - (uint64)addr) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// 长度round up到最近的页面字节数</span>  <span class="hljs-type">size_t</span> maplen = PGROUNDUP(len);  <span class="hljs-type">int</span> idx;  <span class="hljs-keyword">for</span>(idx = <span class="hljs-number">0</span>; idx &lt; NVMA; idx++){    <span class="hljs-keyword">if</span>(!p-&gt;vma[idx].valid) {      <span class="hljs-keyword">break</span>;    }  }  <span class="hljs-keyword">if</span>(idx == NVMA) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">vma</span> =</span> &amp;p-&gt;vma[idx];  vma-&gt;valid = <span class="hljs-number">1</span>;  vma-&gt;addr = (uint64)addr;  vma-&gt;len = len;  vma-&gt;permissions = prot;  vma-&gt;offset = offset;  vma-&gt;prot = prot;  vma-&gt;flags = flags;  vma-&gt;f = filedup(f);  p-&gt;sz = (uint64)addr + maplen;  <span class="hljs-keyword">return</span> (uint64)addr;}</code></pre></div></details><p><code>vma</code> 如果按照答主的实现，从 <code>p-&gt;sz</code> 之后一路往上分配，在 2025 版的 lab （这个答主是 Fall 2020 版本）里面测试会不通过，然后大致理由是：</p><ul><li>由于 <code>mmap</code> 和 <code>munmap</code> 的触发时机不同以及目前寻找 vma 都是下标分配，事实上每个 <code>vma</code> 的 <code>addr</code> 及其下标会不存在正相关的关系，而且事实上难以管理 <code>p-&gt;sz</code> ，只能任凭其增长，无法缩小。</li><li>那么，假设我们 <code>munmap</code> 了某个 <code>vma</code>，会产生类似内部碎片的东西；</li><li>测试数据里面，这一块已经被 <code>munmap</code> 的部分，如果再触发 page fault，由于其满足 <code>va &lt; p-&gt;sz</code>，并且并不处于 <code>vma</code> 的范围内，会被判定为由 <code>sbrk()</code> 产生的 lazy allocation 页，再度分配实际的物理页框；</li><li>但是，测试数据是：解除某页的映射之后再度读取，预期触发非法访问杀死进程。这里显然不满足了</li></ul><p>所以我们实际上应该换一种分配 <code>vma</code> 的方式，我想到了从 <code>MAXVA - 2*PGSIZE</code> 也就是蹦床页往下两页开始<strong>倒着分配</strong>。</p><p>在 <code>struct proc</code> 里面添加一个 <code>uint64 mmap_top;</code>，表示下一次分配 <code>vma</code> 的起始处。因为虚拟地址近乎是无穷大的，这样不会对 OS 产生影响。</p><div class="note note-info"><p>但是其实我也不好说。因为 vma 一直执行，<code>p-&gt;mmaptop</code> 只会单调增长。</p><p>Linux 的实现里面，OS 会去找空余的碎片。但是这里实现确实有点麻烦了。</p></div><p>所以大致的实现是这样的：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 76 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 76 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_mmap</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">void</span> *addr;  <span class="hljs-type">size_t</span> len;  <span class="hljs-type">int</span> prot, flags, fd;  <span class="hljs-type">off_t</span> offset;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  argaddr(<span class="hljs-number">0</span>, (uint64 *)&amp;addr);  argaddr(<span class="hljs-number">1</span>, &amp;len);  argint(<span class="hljs-number">2</span>, &amp;prot);  argint(<span class="hljs-number">3</span>, &amp;flags);  argaddr(<span class="hljs-number">5</span>, (uint64 *)&amp;offset);  <span class="hljs-comment">// This lab only supports kernel-selected addresses and mappings that</span>  <span class="hljs-comment">// start at the beginning of a regular file.</span>  <span class="hljs-keyword">if</span>(addr != <span class="hljs-number">0</span> || len == <span class="hljs-number">0</span> || offset != <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>((prot &amp; ~(PROT_READ | PROT_WRITE)) != <span class="hljs-number">0</span> ||     (prot &amp; (PROT_READ | PROT_WRITE)) == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(flags != MAP_SHARED &amp;&amp; flags != MAP_PRIVATE) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// 将 fd 对应的文件对应到 struct file *f 上</span>  <span class="hljs-keyword">if</span>(argfd(<span class="hljs-number">4</span>, &amp;fd, &amp;f) &lt; <span class="hljs-number">0</span> || f-&gt;type != FD_INODE || !f-&gt;readable) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(flags == MAP_SHARED &amp;&amp; (prot &amp; PROT_WRITE) &amp;&amp; !f-&gt;writable) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// 为了防止从 p-&gt;sz 开始分配，结果因为创建/销毁 vma 的顺序不同，导致 p-&gt;sz 没有可能正确回收的问题</span>  <span class="hljs-comment">// 不使用 p-&gt;sz 开始分配的方法，而是从顶上开始倒着分配</span>  <span class="hljs-comment">// 因为虚拟地址近乎是无穷大的，这样不会对 OS 产生影响</span>  <span class="hljs-comment">// #define MMAPTOP (MAXVA - 2 * PGSIZE)</span>  <span class="hljs-keyword">if</span>(len &gt; MMAPTOP) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-type">size_t</span> maplen = PGROUNDUP(len);  <span class="hljs-keyword">if</span>(maplen &gt; p-&gt;mmap_top) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  uint64 mapaddr = p-&gt;mmap_top - maplen;  <span class="hljs-keyword">if</span>(mapaddr &lt; PGROUNDUP(p-&gt;sz)) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  addr = (<span class="hljs-type">void</span> *)mapaddr;  <span class="hljs-type">int</span> idx;  <span class="hljs-keyword">for</span>(idx = <span class="hljs-number">0</span>; idx &lt; NVMA; idx++){    <span class="hljs-keyword">if</span>(!p-&gt;vma[idx].valid) {      <span class="hljs-keyword">break</span>;    }  }  <span class="hljs-keyword">if</span>(idx == NVMA) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">vma</span> =</span> &amp;p-&gt;vma[idx];  vma-&gt;valid = <span class="hljs-number">1</span>;  vma-&gt;addr = (uint64)addr;  vma-&gt;len = maplen;  vma-&gt;offset = offset;  vma-&gt;prot = prot;  vma-&gt;flags = flags;  vma-&gt;f = filedup(f);  p-&gt;mmap_top = (uint64)addr;  <span class="hljs-keyword">return</span> (uint64)addr;}</code></pre></div></details><p>务必注意，<code>mmap()</code> 里面没有真的分配页，是要落实到 page fault 发生时才真的分配的。</p><p>这里就只是，记录下来这个文件（通过传入 <code>fd</code>）应该映射到这个特定的 VMA 这里。</p><h3 id="处理-mmap-page-fault">处理 mmap page fault</h3><p>添加代码，让 mmap region 中发生 page fault 时：</p><ol><li>分配一个物理页；</li><li>从文件中读取相关的 <code>PGSIZE</code> 到该页；</li><li>把该页映射进用户地址空间。</li></ol><p>使用 <code>readi()</code> 读取文件。</p><div class="note note-warning"><p><code>readi()</code> 的作用是：从某个 inode 表示的文件中，从指定偏移开始读取若干字节，复制到指定内存地址。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">readi</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *ip, <span class="hljs-type">int</span> user_dst, uint64 dst, uint off, uint n)</span>;</code></pre></div><p>参数含义：</p><ul><li><p>ip：要读取的文件对应的 inode，例如 vma-&gt;f-&gt;ip。</p></li><li><p>user_dst：目标地址类型。</p><ul><li>1：dst 是用户虚拟地址。</li><li>0：dst 是内核地址。</li></ul></li><li><p>dst：数据复制到哪里。</p></li><li><p>off：从文件的哪个字节开始读。</p></li><li><p>n：最多读取多少字节。</p></li><li><p>返回值：实际读到的字节数，失败可能返回 -1。</p></li></ul><p>例如 <code> readi(ip, 0, (uint64)mem, 4096, 4096);</code></p><p>表示从文件偏移 4096 处开始读取 4096 字节，写入内核地址 mem。</p></div><p>根据 cow lab 的经验，这里应该是修改 <code>vmfault()</code> 函数。</p><p>代码：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 65 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 65 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">vmfault</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va, <span class="hljs-type">int</span> read)</span>{  uint64 mem;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  <span class="hljs-keyword">if</span>(va &gt;= MAXVA) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  va = PGROUNDDOWN(va);  <span class="hljs-keyword">if</span>(ismapped(pagetable, va)) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-type">int</span> index = ismmaped(p, va);  <span class="hljs-keyword">if</span>(index != <span class="hljs-number">-1</span>) {    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">vma</span> =</span> &amp;p-&gt;vma[index];    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span> =</span> vma-&gt;f-&gt;ip;    <span class="hljs-comment">// mem 是 kalloc() 返回的内核地址</span>    <span class="hljs-keyword">if</span>((mem = (uint64)kalloc()) == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;    }    <span class="hljs-built_in">memset</span>((<span class="hljs-type">void</span> *)mem, <span class="hljs-number">0</span>, PGSIZE);    ilock(ip);    <span class="hljs-comment">// va - vma-&gt;addr = 当前产生 page fault 的页面与 vma 开始地址的距离</span>    <span class="hljs-comment">// 看似没保证页对齐，但是 vma-&gt;offset 永远是 0，而前两者已经页对齐，所以无妨</span>    uint64 fileoff = (va - vma-&gt;addr) + vma-&gt;offset;    <span class="hljs-keyword">if</span>(readi(ip, <span class="hljs-number">0</span>, mem, fileoff, PGSIZE) == <span class="hljs-number">-1</span>) {      iunlock(ip);      kfree((<span class="hljs-type">void</span> *)mem);      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;    }    <span class="hljs-type">int</span> flags = PTE_U;    <span class="hljs-keyword">if</span>(vma-&gt;prot &amp; PROT_READ) {      flags |= PTE_R;    }    <span class="hljs-keyword">if</span>(vma-&gt;prot &amp; PROT_WRITE) {      flags |= PTE_R | PTE_W;    }    <span class="hljs-keyword">if</span>(mappages(pagetable, va, PGSIZE, mem, flags) != <span class="hljs-number">0</span>) {      iunlock(ip);      kfree((<span class="hljs-type">void</span> *)mem);      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;    }    iunlock(ip);    <span class="hljs-keyword">return</span> mem;  }  <span class="hljs-comment">// Only addresses below p-&gt;sz belong to the ordinary lazy-allocation area.</span>  <span class="hljs-comment">// A high address removed by munmap must remain invalid.</span>  <span class="hljs-keyword">if</span>(va &gt;= p-&gt;sz) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  mem = (uint64) kalloc();  <span class="hljs-keyword">if</span>(mem == <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-built_in">memset</span>((<span class="hljs-type">void</span> *) mem, <span class="hljs-number">0</span>, PGSIZE);  <span class="hljs-keyword">if</span> (mappages(p-&gt;pagetable, va, PGSIZE, mem, PTE_W|PTE_U|PTE_R) != <span class="hljs-number">0</span>) {    kfree((<span class="hljs-type">void</span> *)mem);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">return</span> mem;}</code></pre></div></details><p>这里面比较麻烦的点在于这个 <code>fileoff</code>，但其实就是计算偏移。</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 1097 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">假设 vma-&gt;addr = 0x4000 vma-&gt;offset = 0 fault va = 0x6123 先把 fault 地址按页向下对...</span></summary><blockquote><p>假设</p><div class="code-wrapper"><pre><code class="hljs text">vma-&gt;addr   = 0x4000vma-&gt;offset = 0fault va    = 0x6123</code></pre></div><p>先把 fault 地址按页向下对齐：</p><div class="code-wrapper"><pre><code class="hljs text">va = PGROUNDDOWN(0x6123) = 0x6000</code></pre></div><p>虚拟地址区域：</p><div class="code-wrapper"><pre><code class="hljs text">VMA 虚拟地址空间0x4000              0x5000              0x6000              0x7000  │                   │                   │                   │  ▼                   ▼                   ▼                   ▼  ┌───────────────────┬───────────────────┬───────────────────┐  │ VMA 第 0 页       │ VMA 第 1 页       │ VMA 第 2 页       │  └───────────────────┴───────────────────┴───────────────────┘                                            ▲                                            │                                    fault va = 0x6123                                    所在页 = 0x6000</code></pre></div><p>计算它距离 VMA 开头有多远：</p><div class="code-wrapper"><pre><code class="hljs text">va - vma-&gt;addr= 0x6000 - 0x4000= 0x2000</code></pre></div><p>因此：</p><div class="code-wrapper"><pre><code class="hljs c">fileoff = (va - vma-&gt;addr) + vma-&gt;offset;</code></pre></div><p>得到：</p><div class="code-wrapper"><pre><code class="hljs text">fileoff = 0x2000 + 0 = 0x2000</code></pre></div><p>文件内容：</p><div class="code-wrapper"><pre><code class="hljs text">文件字节偏移0x0000              0x1000              0x2000              0x3000  │                   │                   │                   │  ▼                   ▼                   ▼                   ▼  ┌───────────────────┬───────────────────┬───────────────────┐  │ 文件第 0 页       │ 文件第 1 页       │ 文件第 2 页       │  └───────────────────┴───────────────────┴───────────────────┘                                            ▲                                            │                                      fileoff = 0x2000</code></pre></div><p>最终对应关系：</p><div class="code-wrapper"><pre><code class="hljs text">虚拟 mmap 区域                         文件0x4000  VMA 第 0 页  ───────────────▶  offset 0x00000x5000  VMA 第 1 页  ───────────────▶  offset 0x10000x6000  VMA 第 2 页  ───────────────▶  offset 0x2000</code></pre></div><p>因此下面这句：</p><div class="code-wrapper"><pre><code class="hljs c">readi(ip, <span class="hljs-number">0</span>, mem, fileoff, PGSIZE);</code></pre></div><p>表示：</p><div class="code-wrapper"><pre><code class="hljs text">文件 [0x2000, 0x3000)          │          │ readi()          ▼新分配的物理页 mem          │          │ mappages()          ▼用户虚拟地址 [0x6000, 0x7000)</code></pre></div><p>一句话总结：<code>fileoff</code> 用来确定“发生 page fault 的这个虚拟页面，对应文件中的哪一页数据”。</p></blockquote></details><p>另外，注意 <code>flags</code>。</p><p>另外：</p><img src="https://image.wendaining.top/0f174ae0-f325-4fea-aae5-67ce6a1350f1.png" alt="在 vm.c 里面遇到的情况，需要#include &quot;file.h&quot;" style="zoom:50%;"><h3 id="实现-munmap">实现 <code>munmap()</code></h3><p>最难的一个小题。</p><p>系统调用的函数签名：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">munmap</span><span class="hljs-params">(<span class="hljs-type">void</span> *addr, <span class="hljs-type">size_t</span> len)</span>;</code></pre></div><p><code>addr</code> 和 <code>len</code> 都是虚拟地址（虚拟地址空间的 VMA 部分）。</p><p>重点在于不需要处理部分挖洞的情况。也就是，只需要考虑这样的情况：</p><img src="https://image.wendaining.top/abd7009c41783e4f13f13a4a26a3e8df.jpg" style="zoom:50%;"><p>spec 说得不是很详细，但是我认为可以跨 VMA 处理，同时因为 VMA 是一直往前分配的，也就是这样：</p><img src="https://image.wendaining.top/3c4029fb0ec63906b2c81abe14dc14cc.jpg" style="zoom:50%;"><div class="note note-info"><p>这个图比较清晰，就是对于一个 <code>vma</code> 而言：</p><ul><li>最左端，是 <code>vma-&gt;addr</code></li><li>整段的长度，是 <code>vma-&gt;len</code></li><li>其余的不重要，<code>offset</code> 本题都默认是 <code>0</code></li></ul></div><p>这样的情况只是可能存在。</p><p>于是我就考虑，从 <code>addr</code> 开始，计算需要释放的字节范围窗口，然后一边释放一边缩小窗口，每次循环，都遍历所有的 <code>vma</code> 进行扫描，按照 <code>addr</code> 所在的地方来进行判断。（但是每轮循环只处理一个，扫到了就处理这个）。</p><p>得到了之后，通过不断的比较，确定是哪种情况（头 or 尾），然后再确定具体而言需要处理的范围。</p><p>之后，在这个范围内，一页一页地处理：</p><ul><li><p>如果 <code>vma-&gt;flags == MAP_SHARED</code>，那么就需要执行写回的操作</p><ul><li><p>计算 <code>fileoff</code>，也就是文件内的偏移量。因为是要写回文件，但是文件内部的调整是通过 <code>inode</code> 结构体的偏移量实现的，这里需要手动指定</p><div class="note note-info"><p>为什么不能完全处理，因为解映射可以是只解除一部分的。</p></div></li><li><p>写回文件，具体写多少也是个问题。默认来说，我们一次是写一页（因为是一页一页地处理）。但是，有时候末尾可能有不足一页的部分，这个时候 <code>write_len</code> 就变成了最后剩下的长度。</p><div class="note note-warning"><p>上面的部分需要获取 <code>ip-&gt;size</code>，就算是读取也需要上 inode 锁。</p></div></li><li><p>最后，写回。 spec 里面说，要参考 <code>filewrite()</code>，但是这里有问题，因为 <code>filewrite()</code> 的文件内偏移量无法自己指定。具体而言见下面的部分，总之写一个 <code>filewriteat()</code> 辅助函数，可以指定偏移量。</p></li><li><p>最后，如果正好是释放了一整个 <code>vma</code>，那么还需要减少文件的 ref（<code>fileclose(f)</code>），再清空掉这个 <code>vma</code>。</p></li></ul></li><li><p>最后记得修改窗口。</p></li></ul><p>最核心的逻辑，因为会发现后面的 <code>kexit()</code> 的修改，需要做和 <code>munmap()</code> 一样的事情，所以提取出来，作为一个传参的函数，放在 <code>vm.c</code> 里面。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/sysfile.c</span>uint64<span class="hljs-title function_">sys_munmap</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  uint64 addr;  <span class="hljs-type">size_t</span> len;  argaddr(<span class="hljs-number">0</span>, &amp;addr);  argaddr(<span class="hljs-number">1</span>, &amp;len);  <span class="hljs-keyword">return</span> vmaunmap(myproc(), addr, len);}</code></pre></div><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 80 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 80 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/vm.c</span><span class="hljs-type">int</span><span class="hljs-title function_">vmaunmap</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> proc *p, uint64 addr, uint64 len)</span>{  addr = PGROUNDDOWN(addr);  len = PGROUNDUP(len);  <span class="hljs-keyword">if</span>(len == <span class="hljs-number">0</span> || addr + len &lt; addr) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">while</span>(len &gt; <span class="hljs-number">0</span>) {    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">vma</span> =</span> <span class="hljs-number">0</span>;    <span class="hljs-comment">// Re-scan because VMA slot order need not match virtual-address order.</span>    <span class="hljs-keyword">for</span>(<span class="hljs-type">int</span> idx = <span class="hljs-number">0</span>; idx &lt; NVMA; idx++) {      <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">cand</span> =</span> &amp;p-&gt;vma[idx];      <span class="hljs-keyword">if</span>(cand-&gt;valid &amp;&amp; addr &gt;= cand-&gt;addr &amp;&amp;         addr - cand-&gt;addr &lt; cand-&gt;len) {        vma = cand;        <span class="hljs-keyword">break</span>;      }    }    <span class="hljs-keyword">if</span>(vma == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span> =</span> vma-&gt;f;    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span> =</span> f-&gt;ip;    uint64 vma_end = vma-&gt;addr + vma-&gt;len;    uint64 unmap_end = addr + len;    uint64 free_end = unmap_end &lt; vma_end ? unmap_end : vma_end;    uint64 free_len = free_end - addr;    <span class="hljs-keyword">for</span>(uint64 pageva = addr; pageva &lt; free_end; pageva += PGSIZE) {      <span class="hljs-type">pte_t</span> *pte = walk(p-&gt;pagetable, pageva, <span class="hljs-number">0</span>);      <span class="hljs-keyword">if</span>(pte == <span class="hljs-number">0</span> || (*pte &amp; PTE_V) == <span class="hljs-number">0</span>) {        <span class="hljs-keyword">continue</span>;      }      <span class="hljs-keyword">if</span>(vma-&gt;flags == MAP_SHARED &amp;&amp; (vma-&gt;prot &amp; PROT_WRITE)) {        uint64 pa = PTE2PA(*pte);        uint64 fileoff = vma-&gt;offset + (pageva - vma-&gt;addr);        uint write_len = PGSIZE;        ilock(ip);        <span class="hljs-keyword">if</span>(fileoff &gt;= ip-&gt;size) {          write_len = <span class="hljs-number">0</span>;        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(write_len &gt; ip-&gt;size - fileoff) {          write_len = ip-&gt;size - fileoff;        }        iunlock(ip);        <span class="hljs-keyword">if</span>(write_len &gt; <span class="hljs-number">0</span> &amp;&amp;           filewriteat(f, <span class="hljs-number">0</span>, pa, fileoff, write_len) != write_len) {          <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;        }      }      uvmunmap(p-&gt;pagetable, pageva, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>);    }    <span class="hljs-keyword">if</span>(addr == vma-&gt;addr &amp;&amp; free_end == vma_end) {      fileclose(f);      <span class="hljs-built_in">memset</span>(vma, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(*vma));    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(addr == vma-&gt;addr) {      vma-&gt;addr += free_len;      vma-&gt;len -= free_len;      vma-&gt;offset += free_len;    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(free_end == vma_end) {      vma-&gt;len -= free_len;    } <span class="hljs-keyword">else</span> {      panic(<span class="hljs-string">"vmaunmap: hole"</span>);    }    addr = free_end;    len -= free_len;  }  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div></details><h4 id="为什么需要-filewriteat">为什么需要 <code>filewriteat()</code></h4><p>这里的问题主要有两个：<strong>文件内的偏移量</strong>和<strong>源地址的类型</strong>。</p><p>首先看 <code>filewrite()</code> 最关键的部分：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span> ((r = writei(f-&gt;ip, <span class="hljs-number">1</span>, addr + i, f-&gt;off, n1)) &gt; <span class="hljs-number">0</span>) {  f-&gt;off += r;}</code></pre></div><p><code>filewrite()</code> 没有接收文件偏移量的参数，它使用的是 <code>struct file</code> 内部的 <code>f-&gt;off</code>。每写入一段数据，还会自动把 <code>f-&gt;off</code> 往后推进。这对普通的 <code>write()</code> 是正确的，因为普通文件读写本来就需要维护一个当前读写位置。</p><p>但 mmap 写回文件时，文件位置不能由 <code>f-&gt;off</code> 决定，而是由这张页在 VMA 内的位置决定：</p><div class="code-wrapper"><pre><code class="hljs c">fileoff = vma-&gt;offset + (pageva - vma-&gt;addr);</code></pre></div><p>例如，假设映射从文件偏移 <code>0</code> 开始，现在要解映射 VMA 的第 2 页：</p><div class="code-wrapper"><pre><code class="hljs text">VMA 第 0 页  ────▶  文件 offset 0VMA 第 1 页  ────▶  文件 offset 4096VMA 第 2 页  ────▶  文件 offset 8192</code></pre></div><p>这时必须把该页写到文件的 <code>8192</code> 处。但 <code>f-&gt;off</code> 可能是 <code>0</code>，也可能已经被普通的 <code>read()</code> / <code>write()</code> 改到了其他位置。而且 <code>filedup()</code> 只是增加原来 <code>struct file</code> 的引用计数，VMA 和 fd 指向的仍然是同一个 <code>struct file</code>，所以不能假设 <code>f-&gt;off</code> 始终等于 mmap 需要的位置。</p><div class="note note-warning"><p>即使在 <code>munmap()</code> 时直接把整个 VMA 写回，也没有解决这个问题。<code>filewrite()</code> 仍然会从 <code>f-&gt;off</code> 开始写，而不是从 <code>vma-&gt;offset</code> 开始写。另外，<code>munmap()</code> 可能只解除 VMA 的一部分，映射长度也可能大于文件长度，所以仍然需要明确指定这次要写回的文件区间。</p></div><p>于是把 <code>filewrite()</code> 中写 inode 文件的逻辑提取成 <code>filewriteat()</code>：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">filewriteat</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> file *f, <span class="hljs-type">int</span> user_src, uint64 src, uint64 off, <span class="hljs-type">int</span> n)</span>;</code></pre></div><p>最后的调用是：</p><div class="code-wrapper"><pre><code class="hljs c">filewriteat(f, <span class="hljs-number">0</span>, pa, fileoff, write_len);</code></pre></div><p>其中：</p><ul><li><p><code>0</code> 表示是 <code>pa</code> 不是 <code>va</code>；</p><div class="note note-info"><p>其实这里（<code>vmunmap()</code>）传 <code>va</code> 也可以（就是循环里面的 <code>pageva</code>），但是都一样。</p></div></li><li><p><code>pa</code> 是当前需要写回的物理页；</p></li><li><p><code>fileoff</code> 是这张页在文件内的正确位置；</p></li><li><p><code>write_len</code> 是这次实际允许写入的长度。</p></li></ul><p><code>filewriteat()</code> 仍然保留 <code>filewrite()</code> 里面的分批写入、文件系统事务和 inode 加锁逻辑，但它使用显式传入的 <code>off</code>，并且不读取、不修改 <code>f-&gt;off</code>。</p><h3 id="修改-kexit">修改 <code>kexit()</code></h3><p>就是释放所有 VMA，和 <code>munmap()</code> 一样。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// munmap all mmap regions</span><span class="hljs-keyword">for</span>(<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; NVMA; i++) {    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span> *<span class="hljs-title">vma</span> =</span> &amp;p-&gt;vma[i];    <span class="hljs-keyword">if</span>(vma-&gt;valid) {      <span class="hljs-keyword">if</span>(vmaunmap(p, vma-&gt;addr, vma-&gt;len) &lt; <span class="hljs-number">0</span>) {        panic(<span class="hljs-string">"kexit: vmaunmap"</span>);      }    }}</code></pre></div><h3 id="修改-kfork">修改 <code>kfork()</code></h3><p>也就是复制一下，和复制 <code>trapframe</code> 没什么本质区别。hints 也提示了需要给文件引用 +1，照做就行了。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; NVMA; ++i) {    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span>* <span class="hljs-title">vma</span> =</span> &amp;p-&gt;vma[i];    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">vma</span>* <span class="hljs-title">nvma</span> =</span> &amp;np-&gt;vma[i];    *nvma = *vma;    <span class="hljs-keyword">if</span> (nvma-&gt;valid) {      filedup(nvma-&gt;f);    }}</code></pre></div><h3 id="关于-challenges">关于 challenges</h3><p>其实我觉得这几个都挺有意思的，但是有点累了，在这里如果之后有兴趣，回来实现一下。</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 1546 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">If two processes have the same file mmap-ed (as in the fork tests), shar...</span></summary><blockquote><ul><li>If two processes have the same file mmap-ed (as in the fork tests), share their physical pages. You will need reference counts on physical pages.</li><li>Your solution probably allocates a new physical page for each page read from the mmap-ed file, even though the data is also in kernel memory in the buffer cache. Modify your implementation to use that physical memory, instead of allocating a new page. This requires that file blocks be the same size as pages (set <code>BSIZE</code> to 4096). You will need to pin mmap-ed blocks into the buffer cache. You will need worry about reference counts. One benefit of fixing this double-caching is that it also helps make <code>read()</code> and <code>write()</code> consistent with <code>mmap</code>. That is, if some <code>mmap</code>ed file data is modified through the memory mapping, <code>read</code> should return those modifications, and likewise, if an application calls <code>write</code>, the write should appear in any active memory mappings of that file. You might find it interesting to read the paper on <a href="https://www.usenix.org/legacy/publications/library/proceedings/usenix2000/freenix/full_papers/silvers/silvers.pdf">the unified buffer cache</a>.</li><li>Remove redundancy between your implementation for lazy allocation and your implementation of mmap-ed files. (Hint: create a VMA for the lazy allocation area.)</li><li>Modify <code>exec</code> to use a VMA for different sections of the binary so that you get on-demand-paged executables. This will make starting programs faster, because <code>exec</code> will not have to read any data from the file system.</li><li>Implement page-out and page-in: have the kernel move some parts of processes to disk when physical memory is low. Then, page in the paged-out memory when the process references it.</li></ul></blockquote></details><p>写完会发现自己对于 xv6 的日志以及事务系统好像就没什么了解，只有在 file system 的 lab 里面简单操作了一下诸如 <code>bread()</code> <code>brelse()</code> 这样的简单 API，但是没有像 <code>inode</code> 这样深入了解，这也是个 TODO 吧。</p><div class="note note-info"><p>不过似乎好好读读书（xv6 book）就行了。</p></div><p><img src="https://image.wendaining.top/image-20260820193745319.png" alt=""></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab8 File system - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/08/18/xv6-lab8-file-system/</link>
      <description>xv6 的第八个 lab，和文件系统有关。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Tue, 18 Aug 2026 18:50:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读">阅读</h2><p>简单读一下书，让 GPT 翻译并提炼重点，然后读对应的部分。</p><p>xv6 将文件系统分为 7 层：</p><ul><li><strong>Disk layer</strong>：真正向 VirtIO disk 读写 block。</li><li><strong>Buffer cache</strong>：把磁盘 block 缓存在内存，并保证一个 block 同一时间只有一个线程修改。</li><li><strong>Logging</strong>：把多个 block 的修改组成 transaction，保证 crash 后要么全部生效，要么全部不生效。</li><li><strong>Inode</strong>：把一个文件表示成 inode + 若干 data block。</li><li><strong>Directory</strong>：目录其实是一种特殊文件，内容是一系列 <code>name -&gt; inode number</code>。</li><li><strong>Pathname</strong>：解析 <code>/a/b/c</code>。</li><li><strong>File descriptor</strong>：最终向用户提供 <code>open/read/write/...</code> 这样的统一接口。</li></ul><p>所谓「crash consistency 崩溃一致性」：崩溃的时候，修改 inode，记录日志等工作完成到一半，导致不一致。xv6 的实现方式是日志，不直接修改正式文件系统，先把这次 transaction 的修改写进 log。</p><p>关于日志的设计，位于磁盘的特定区域。</p><p>inode 分磁盘上的 <code>struct dinode</code> 和内存里的 <code>struct inode</code>。内存 inode 只是磁盘 inode 的缓存副本</p><p>磁盘 inode 的重要字段：</p><div class="code-wrapper"><pre><code class="hljs ada"><span class="hljs-keyword">type</span><span class="hljs-type">nlink</span><span class="hljs-type"></span>sizeaddrs[]</code></pre></div><p>含义：</p><ul><li><code>type</code>：普通文件、目录、device……</li><li><code>nlink</code>：有多少 directory entry 指向它。</li><li><code>size</code>：文件大小。</li><li><code>addrs[]</code>：文件数据所在的 <strong>disk block number</strong>。</li></ul><p>inode number（inum）就是 inode 在磁盘 inode 区域中的编号。</p><p>内存中的 <code>struct inode</code> 额外拥有：</p><div class="code-wrapper"><pre><code class="hljs csharp"><span class="hljs-keyword">ref</span><span class="hljs-keyword">lock</span>valid...</code></pre></div><p><code>ref</code> 表示当前 kernel 中有多少 C 指针正在引用这个 inode。</p><p>然后有区分 direct blocks 和 indirect 的，也就是一级索引和二级索引：</p><img src="https://image.wendaining.top/image-20260818200521912.png" style="zoom:50%;"><p>然后查询 inode 和目录也是一堆 API，比如对于一个完整的路径，依据分隔符进行分割然后递归查询逐层的 inode 的 <code>namex()</code> 。</p><h2 id="Large-files">Large files</h2><p>根据 hints 来：</p><h3 id="分析-bmap-函数">分析 <code>bmap()</code> 函数</h3><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 43 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 43 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Return the disk block address of the nth block in inode ip.</span><span class="hljs-comment">// If there is no such block, bmap allocates one.</span><span class="hljs-comment">// returns 0 if out of disk space.</span><span class="hljs-type">static</span> uint<span class="hljs-title function_">bmap</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *ip, uint bn)</span>{  uint addr, *a;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">buf</span> *<span class="hljs-title">bp</span>;</span>  <span class="hljs-keyword">if</span>(bn &lt; NDIRECT){    <span class="hljs-keyword">if</span>((addr = ip-&gt;addrs[bn]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr == <span class="hljs-number">0</span>)        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      ip-&gt;addrs[bn] = addr;    }    <span class="hljs-keyword">return</span> addr;  }  bn -= NDIRECT;  <span class="hljs-keyword">if</span>(bn &lt; NINDIRECT){    <span class="hljs-comment">// Load indirect block, allocating if necessary.</span>    <span class="hljs-keyword">if</span>((addr = ip-&gt;addrs[NDIRECT]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr == <span class="hljs-number">0</span>)        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      ip-&gt;addrs[NDIRECT] = addr;    }    bp = bread(ip-&gt;dev, addr);    a = (uint*)bp-&gt;data;    <span class="hljs-keyword">if</span>((addr = a[bn]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr){        a[bn] = addr;        log_write(bp);      }    }    brelse(bp);    <span class="hljs-keyword">return</span> addr;  }  panic(<span class="hljs-string">"bmap: out of range"</span>);}</code></pre></div></details><ul><li>「阅读」部分的图 10.3 十分十分重要。</li><li><code>bn</code> 是 logical block number，即相对于文件开头而言，这个 block 是文件中的第几个 block。返回的是实际的 disk block number。</li><li>若 <code>bn &lt; NDIRECT</code>，根据图片所示，直接映射到 <code>ip-&gt;addrs[bn]</code> 处的数据块。<ul><li>但是实际实现是先检查是否存在，如果不存在还得 <code>balloc()</code> 分配一个出来。</li></ul></li><li>若 <code>bn &gt; NDIRECT</code>：<ol><li>先 <code>bn -= NDIRECT</code>，这一步是为了把原本在整个文件范围内的逻辑块号，转变为一级间接块数组内的下标<ul><li>事实上看图也能看出来进入一级块之后就又是 address 1 ~ 256 了，不过实际上应该是 0 ~ 255。</li></ul></li><li>检查 <code>bn &lt; NINDIRECT</code>，事实上就是检查之前的 <code>bn</code> 在不在最大范围内（ <code>MAXFILE = NDIRECT + NINDIRECT</code>)</li><li>检查通过后，检查间接块的存在性，没有就分配</li><li>然后，首先通过 <code>bread()</code> 获取磁盘上的这个 block 的间接块的 <code>struct buf</code>，获取里面的数据。<ol><li>如果数据为空，继续分配，不空就直接获得地址，可以直接返回了（不过是代码的写法上没这么写，逻辑等价）</li><li>确认非空之后，给 <code>a[bn]</code> 里面映射上刚分配好的块，再记录日志</li><li>最后释放掉，然后返回这个地址</li></ol></li></ol></li></ul><h3 id="修改宏定义">修改宏定义</h3><p>目前 xv6 的文件最多只能有 268 blocks，分析一下原因，是因为规定了 <code>#define NDIRECT 12</code>，并且 <code>dinode</code>  和 <code>inode</code> 里面有 <code>uint addrs[NDIRECT+1];</code>。</p><p>也就是，12 个直接块号，以及 1 个一级间接块号。</p><p>又规定 <code>BSIZE 1024</code>（块的字节数），<code>sizeof(uint) = 4</code> ，故一个间接块号至多存 256 个 block number，故 <code>MAXFILE = 12 + 256 = 268 blocks</code>。</p><p>这个 lab 需要实现 doubly-indirect block，变成了 $256^2 + 256 + 11 = 65803 \text{blocks}$</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> NDIRECT 11</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> NINDIRECT (BSIZE / sizeof(uint))</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> NINDIRECT2 (NINDIRECT * NINDIRECT)</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> MAXFILE (NDIRECT + NINDIRECT + NINDIRECT2)</span></code></pre></div><p>然后，修改 <code>struct inode</code> 和 <code>struct dinode</code> 字段：</p><div class="code-wrapper"><pre><code class="hljs c">uint addrs[NDIRECT+<span class="hljs-number">2</span>];</code></pre></div><h3 id="思考清楚计算方式">思考清楚计算方式</h3><blockquote><p>思考：给定一个文件的 logical block number，应该怎样计算它在 doubly-indirect block 中的位置，也就是先选第几个 singly-indirect block，再选其中第几个 data block。</p></blockquote><p>这边偷一张知乎上的图，画的很好：</p><img src="https://image.wendaining.top/image-20260818210059320.png" style="zoom:50%;"><p>具体而言，如果 <code>bn</code> 是位于二级索引的位置：</p><ul><li><p>首先是需要先减去前面两个部分（其实计算的过程中逐步减掉了）</p></li><li><p>然后，2nd indirect 里面的每一个 address，都映射到了另一个 1st indirect，也就是另外的 256 个实际的 data block</p></li><li><p>那么，举个例子，bn = 45162（<em>随便打的符合要求的数字</em>）</p><ul><li>经过一级和二级的判断，bn = 45162 - 11 - 256 = 44895</li><li>然后，整除 256，得到 175，也就是位于第 175 个一级索引</li><li>然后，44895 % 256 = 95，也就是位于这个一级索引指向的第 95 个 data block</li></ul></li></ul><p>这就是计算方式。</p><h3 id="实现对-bmap-的修改">实现对 <code>bmap()</code> 的修改</h3><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 81 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 81 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">static</span> uint<span class="hljs-title function_">bmap</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *ip, uint bn)</span>{  uint addr, *a;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">buf</span> *<span class="hljs-title">bp</span>;</span>  <span class="hljs-keyword">if</span>(bn &lt; NDIRECT){    <span class="hljs-keyword">if</span>((addr = ip-&gt;addrs[bn]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr == <span class="hljs-number">0</span>)        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      ip-&gt;addrs[bn] = addr;    }    <span class="hljs-keyword">return</span> addr;  }  bn -= NDIRECT;  <span class="hljs-keyword">if</span>(bn &lt; NINDIRECT){    <span class="hljs-comment">// Load indirect block, allocating if necessary.</span>    <span class="hljs-keyword">if</span>((addr = ip-&gt;addrs[NDIRECT]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr == <span class="hljs-number">0</span>)        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      ip-&gt;addrs[NDIRECT] = addr;    }    bp = bread(ip-&gt;dev, addr);    a = (uint*)bp-&gt;data;    <span class="hljs-keyword">if</span>((addr = a[bn]) == <span class="hljs-number">0</span>){      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span>(addr){        a[bn] = addr;        log_write(bp);      }    }    brelse(bp);    <span class="hljs-keyword">return</span> addr;  }  <span class="hljs-comment">// 以下为实现代码</span>  bn -= NINDIRECT;  <span class="hljs-keyword">if</span>(bn &lt; NINDIRECT2) {    <span class="hljs-comment">// 这里的 addr 是二级间接块的地址</span>    <span class="hljs-keyword">if</span> ((addr = ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>]) == <span class="hljs-number">0</span>) {      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span> (addr == <span class="hljs-number">0</span>) {        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      }      ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>] = addr;    }    <span class="hljs-comment">// 这里的 bp 是二级间接块的实际 struct buf</span>    bp = bread(ip-&gt;dev, addr);    a = (uint*)bp-&gt;data;    <span class="hljs-comment">// 这里的 addr 是二级间接块指向的一级间接块的地址</span>    <span class="hljs-keyword">if</span> ((addr = a[bn / NINDIRECT]) == <span class="hljs-number">0</span>) {      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span> (addr == <span class="hljs-number">0</span>) {        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      }      a[bn / NINDIRECT] = addr;      <span class="hljs-comment">// 记得任何对于实际 buf 的修改都要落日志，维护崩溃一致性</span>      log_write(bp);    }    brelse(bp);    <span class="hljs-comment">// 这里的 bp 是一级间接块的实际的 struct buf</span>    bp = bread(ip-&gt;dev, addr);    a = (uint*)bp-&gt;data;    <span class="hljs-comment">// 这里的 addr 是最后实际的数据的地址</span>    <span class="hljs-keyword">if</span> ((addr = a[bn % NINDIRECT]) == <span class="hljs-number">0</span>) {      addr = balloc(ip-&gt;dev);      <span class="hljs-keyword">if</span> (addr == <span class="hljs-number">0</span>) {        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;      }      a[bn % NINDIRECT] = addr;      log_write(bp);    }    brelse(bp);    <span class="hljs-keyword">return</span> addr;  }  panic(<span class="hljs-string">"bmap: out of range"</span>);}</code></pre></div></details><p>其实很多地方是重复利用了一个变量，比如 <code>bp</code> 和 <code>a</code> 和 <code>addr</code>，这是因为原本的代码就是这样的重复利用，虽然不清晰，不过为了保持代码风格一致，就这样吧。</p><p>记得对每一个通过 <code>bread()</code> 读取的 block，都不要忘记最终调用 <code>brelse()</code>。</p><h3 id="修改-itrunc">修改 <code>itrunc()</code></h3><blockquote><p>不要忘记修改 <code>itrunc()</code>，确保它能够释放文件的所有 block，包括 double-indirect block 及其下层 block。</p></blockquote><p>就像改了 <code>kalloc()</code> 加一级，那也肯定是要修改 <code>kfree()</code> 的。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 49 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 49 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">itrunc</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> inode *ip)</span>{  <span class="hljs-type">int</span> i, j, k;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">buf</span> *<span class="hljs-title">bp</span>;</span>  uint *a;  <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; i &lt; NDIRECT; i++){    <span class="hljs-keyword">if</span>(ip-&gt;addrs[i]){      bfree(ip-&gt;dev, ip-&gt;addrs[i]);      ip-&gt;addrs[i] = <span class="hljs-number">0</span>;    }  }  <span class="hljs-keyword">if</span>(ip-&gt;addrs[NDIRECT]){    bp = bread(ip-&gt;dev, ip-&gt;addrs[NDIRECT]);    a = (uint*)bp-&gt;data;    <span class="hljs-keyword">for</span>(j = <span class="hljs-number">0</span>; j &lt; NINDIRECT; j++){      <span class="hljs-keyword">if</span>(a[j])        bfree(ip-&gt;dev, a[j]);    }    brelse(bp);    bfree(ip-&gt;dev, ip-&gt;addrs[NDIRECT]);    ip-&gt;addrs[NDIRECT] = <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">if</span>(ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>]) {    bp = bread(ip-&gt;dev, ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>]);    a = (uint*)bp-&gt;data;    <span class="hljs-keyword">for</span>(j = <span class="hljs-number">0</span>; j &lt; NINDIRECT; j++) {      uint in1_addr;      <span class="hljs-keyword">if</span> ((in1_addr = a[j]) != <span class="hljs-number">0</span>) {        <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">buf</span> *<span class="hljs-title">in1_bp</span> =</span> bread(ip-&gt;dev, in1_addr);        uint *in1_a = (uint*)in1_bp-&gt;data;        <span class="hljs-keyword">for</span>(k = <span class="hljs-number">0</span>; k &lt; NINDIRECT; k++) {          bfree(ip-&gt;dev, in1_a[k]);        }        brelse(in1_bp);        bfree(ip-&gt;dev, in1_addr);      }    }    brelse(bp);    bfree(ip-&gt;dev, ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>]);    ip-&gt;addrs[NDIRECT + <span class="hljs-number">1</span>] = <span class="hljs-number">0</span>;  }  ip-&gt;size = <span class="hljs-number">0</span>;  iupdate(ip);}</code></pre></div></details><h2 id="Symbolic-links">Symbolic links</h2><p>给 xv6 添加符号链接 / 软链接功能，也就是实现系统调用 <code>symlink</code>。</p><p>复习一下，所谓的软链接，就是一个记录目标路径名的 alias。这么理解区别：</p><div class="code-wrapper"><pre><code class="hljs text">hard link：name → inodesymbolic link：name → symlink inode → pathname → inode</code></pre></div><p>这个 lab 里面不需要处理指向目录的软链接。</p><p>以 hints 为线索：</p><h3 id="添加和系统调用相关的基础配置">添加和系统调用相关的基础配置</h3><p>为 <code>symlink</code> 创建一个新的 system call number：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/syscall.h</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> SYS_symlink 22</span></code></pre></div><p>在 <code>kernel/sysfile.c</code> 里面实现空的 <code>sys_symlink()</code>：</p><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_symlink</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  }</code></pre></div><p>在 <code>kernel/syscall.c</code>l 下面声明、加入系统调用的数组：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">extern</span> uint64 <span class="hljs-title function_">sys_symlink</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>;<span class="hljs-type">static</span> <span class="hljs-title function_">uint64</span> <span class="hljs-params">(*syscalls[])</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> = {[SYS_fork]    sys_fork,<span class="hljs-comment">// ...</span>[SYS_symlink] sys_symlink,};</code></pre></div><p>在 <code>user/usys.pl</code> 里面加入对应的 entry：</p><div class="code-wrapper"><pre><code class="hljs perl">entry(<span class="hljs-string">"symlink"</span>);</code></pre></div><p>在 <code>user/user.h</code> 里面加入声明：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">symlink</span><span class="hljs-params">(<span class="hljs-type">char</span> *target, <span class="hljs-type">char</span> *path)</span>;</code></pre></div><div class="note note-success"><p>为什么知道具体的返回值？<strong>RTFM</strong></p></div><h3 id="加入新的标识文件类型">加入新的标识文件类型</h3><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/stat.h</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> T_DIR     1   <span class="hljs-comment">// Directory</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> T_FILE    2   <span class="hljs-comment">// File</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> T_DEVICE  3   <span class="hljs-comment">// Device</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> T_SYMLINK 4   <span class="hljs-comment">// soft link</span></span></code></pre></div><h3 id="添加新的-flag">添加新的 flag</h3><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_RDONLY   0x000</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_WRONLY   0x001</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_RDWR     0x002</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_CREATE   0x200</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_TRUNC    0x400</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> O_NOFOLLOW 0x20000</span></code></pre></div><p>这主要是给 <code>open</code> 用的（RTFM 得知这个 flag 是这个数）</p><div class="note note-info"><p>不过其实自己实现一个也可以，只要遵循传给 <code>open()</code> 的 flags 会用 bitwise OR 组合，所以新 flag 不能和任何已有 flag 的 bit 重叠的原则即可。</p></div><p>根据 <code>man</code>：</p><blockquote><p>If the trailing component (i.e., basename) of pathname is a symbolic link, then the open fails, with the error ELOOP</p></blockquote><h3 id="实现-symlink">实现 <code>symlink()</code></h3><p>在 <code>path</code> 创建新的 symbolic link 并让它指向 <code>target</code>，注意 <code>target</code> 不需要真实存在。</p><p>这里我一开始搞不懂这些 API 和概念（读书不仔细），然后我就考虑读一下 <code>sys_link()</code> 的实现：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 49 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 49 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Create the path new as a link to the same inode as old.</span>uint64<span class="hljs-title function_">sys_link</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> name[DIRSIZ], new[MAXPATH], old[MAXPATH];  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">dp</span>, *<span class="hljs-title">ip</span>;</span>  <span class="hljs-keyword">if</span>(argstr(<span class="hljs-number">0</span>, old, MAXPATH) &lt; <span class="hljs-number">0</span> || argstr(<span class="hljs-number">1</span>, new, MAXPATH) &lt; <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  begin_op();  <span class="hljs-keyword">if</span>((ip = namei(old)) == <span class="hljs-number">0</span>){    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  ilock(ip);  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DIR){    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  ip-&gt;nlink++;  iupdate(ip);  iunlock(ip);  <span class="hljs-keyword">if</span>((dp = nameiparent(new, name)) == <span class="hljs-number">0</span>)    <span class="hljs-keyword">goto</span> bad;  ilock(dp);  <span class="hljs-keyword">if</span>(dp-&gt;dev != ip-&gt;dev || dirlink(dp, name, ip-&gt;inum) &lt; <span class="hljs-number">0</span>){    iunlockput(dp);    <span class="hljs-keyword">goto</span> bad;  }  iunlockput(dp);  iput(ip);  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;bad:  ilock(ip);  ip-&gt;nlink--;  iupdate(ip);  iunlockput(ip);  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div></details><p>梳理出一些要点：</p><ol><li><code>begin_op()</code> 和 <code>end_op()</code> 开启文件系统的事务，保证 ACID</li><li><code>namei()</code> 返回路径对应的文件的 <code>inode</code> 结构体指针</li><li>得到了 <code>inode</code> 指针之后，如果想做修改，还需要使用 <code>ilock()</code> 进行加锁，再使用 <code>iunlock()</code> 解锁，修改之后需要使用 <code>iupdate</code> 进行落盘，把内存 <code>inode</code> 写回磁盘上的 <code>struct dinode</code></li></ol><p>但其实只能知道这些，然后读书（其实是问 LLM 要点）可以得到这些额外的信息：</p><ol><li><code>create()</code> 给出的 inode 是直接带锁的，不需要自己加锁</li><li>软链接是通过创建一个类型为 <code>T_SYMLINK</code> 的 inode，里面的 data block（实际是 <code>inode</code> 的 <code>addr</code> 所指向的 <code>buf</code> （通过 <code>bmap()</code> 求得）的 <code>uchar data[BSIZE];</code>） 是 <code>target</code> 来实现的。</li><li>因此，如果想读写，是有对应的 API 的，也就是<code>readi()</code> 和  <code>writei()</code>，这个的用法直接 Ctrl + F 一下就能看懂了。</li></ol><p>但是实际上理解也有偏颇，还需要读 <code>create()</code>，这里直接给出 LLM 的纠错版：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 1821 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">为什么 namei() 的结果会泄漏？ namei() 返回的不只是一个普通指针，它还替调用者持有了一个 inode 引用。 可以想象成： st...</span></summary><blockquote><p><strong>为什么 <code>namei()</code> 的结果会泄漏？</strong></p><p><code>namei()</code> 返回的不只是一个普通指针，它还替调用者持有了一个 inode 引用。</p><p>可以想象成：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span> =</span> namei(path);<span class="hljs-comment">// ip-&gt;ref 被加了一次</span></code></pre></div><p>调用者用完后必须：</p><div class="code-wrapper"><pre><code class="hljs c">iput(ip);</code></pre></div><p>否则引用计数永远不会减回来。</p><p>你原来写的是：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(namei(path) != <span class="hljs-number">0</span>){  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div><p>如果文件存在，<code>namei()</code> 返回了一个带引用的 inode，但你没有把返回值保存起来：</p><div class="code-wrapper"><pre><code class="hljs text">namei() 获得引用       ↓返回 inode 指针       ↓指针被直接丢弃       ↓无法再调用 iput()       ↓引用泄漏</code></pre></div><p>它类似：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(<span class="hljs-built_in">malloc</span>(<span class="hljs-number">100</span>) != <span class="hljs-number">0</span>)  <span class="hljs-keyword">return</span>;</code></pre></div><p>内存申请成功了，但指针直接丢掉，因此没办法 <code>free()</code>。</p><p>如果真的要这样检查，至少应该：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">existing</span> =</span> namei(path);<span class="hljs-keyword">if</span>(existing != <span class="hljs-number">0</span>){  iput(existing);  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div><p>不过在 <code>sys_symlink()</code> 里，这个检查应该直接删除，因为 <code>create()</code> 已经会在父目录锁的保护下检查路径是否存在。</p><p>提前 <code>namei()</code> 再 <code>create()</code> 还有并发问题：</p><div class="code-wrapper"><pre><code class="hljs text">进程 A：namei(path) 发现不存在进程 B：创建 path进程 A：create(path)</code></pre></div><p>真正可靠的“检查并创建”必须在 <code>create()</code> 内部完成。</p><p><strong><code>iunlock()</code> 和 <code>iunlockput()</code> 的区别</strong></p><p>它们控制两个不同的东西：</p><div class="code-wrapper"><pre><code class="hljs c">iunlock(ip);</code></pre></div><p>只释放 inode 锁，但仍然保留引用：</p><div class="code-wrapper"><pre><code class="hljs text">锁：释放引用：保留ip：以后还能继续使用</code></pre></div><p>而：</p><div class="code-wrapper"><pre><code class="hljs c">iunlockput(ip);</code></pre></div><p>等价于：</p><div class="code-wrapper"><pre><code class="hljs c">iunlock(ip);iput(ip);</code></pre></div><p>结果是：</p><div class="code-wrapper"><pre><code class="hljs text">锁：释放引用：也释放ip：以后不能继续使用</code></pre></div><p>完整状态变化可以这样看：</p><div class="code-wrapper"><pre><code class="hljs c">ip = namei(path);</code></pre></div><p>此时：</p><div class="code-wrapper"><pre><code class="hljs text">ref = +1locked = no</code></pre></div><p>然后：</p><div class="code-wrapper"><pre><code class="hljs c">ilock(ip);</code></pre></div><p>变成：</p><div class="code-wrapper"><pre><code class="hljs text">ref = +1locked = yes</code></pre></div><p>如果调用：</p><div class="code-wrapper"><pre><code class="hljs c">iunlock(ip);</code></pre></div><p>变成：</p><div class="code-wrapper"><pre><code class="hljs text">ref = +1locked = no</code></pre></div><p>还可以再次使用 <code>ip</code>。</p><p>如果调用：</p><div class="code-wrapper"><pre><code class="hljs c">iunlockput(ip);</code></pre></div><p>变成：</p><div class="code-wrapper"><pre><code class="hljs text">ref = 释放locked = no</code></pre></div><p>不能再使用 <code>ip</code>。</p><p><strong>为什么 <code>link()</code> 里使用 <code>iunlock()</code>？</strong></p><p><code>sys_link()</code> 的逻辑大致是：</p><div class="code-wrapper"><pre><code class="hljs c">ip = namei(old);  <span class="hljs-comment">// 获得 old inode 的引用</span>ilock(ip);ip-&gt;nlink++;iupdate(ip);iunlock(ip);      <span class="hljs-comment">// 只解锁，仍保留引用</span></code></pre></div><p>它此时还没有用完 <code>ip</code>。后面还需要：</p><div class="code-wrapper"><pre><code class="hljs c">dp = nameiparent(new, name);ilock(dp);dirlink(dp, name, ip-&gt;inum);</code></pre></div><p>所以必须保留 <code>ip</code> 的引用。</p><p>但又没必要继续锁住 <code>ip</code>，而且持有它的锁再做路径查找、获取父目录锁，会增加死锁风险。因此采用：</p><div class="code-wrapper"><pre><code class="hljs c">iunlock(ip);</code></pre></div><p>也就是：</p><blockquote><p>我暂时不需要锁住它，但这个 inode 还没用完，不能释放引用。</p></blockquote><p>成功完成新目录项之后才：</p><div class="code-wrapper"><pre><code class="hljs c">iput(ip);</code></pre></div><p>如果中间失败，还需要重新锁定它，回滚 <code>nlink</code>：</p><div class="code-wrapper"><pre><code class="hljs c">ilock(ip);ip-&gt;nlink--;iupdate(ip);iunlockput(ip);</code></pre></div><p>因此 <code>link()</code> 的生命周期是：</p><div class="code-wrapper"><pre><code class="hljs text">namei(old)         获得引用    ↓ilock(ip)          加锁    ↓修改 nlink    ↓iunlock(ip)        解锁，但保留引用    ↓创建新目录项，期间仍需要 ip-&gt;inum    ↓iput(ip)           最后释放引用</code></pre></div><p>大部分地方使用 <code>iunlockput()</code>，是因为那些地方解锁以后就再也不需要这个 inode 了。</p><p>另外两个典型的 <code>iunlock()</code> 场景：</p><ul><li><code>open()</code>：引用转交给 <code>f-&gt;ip</code>，等 <code>close()</code> 时释放。</li><li><code>chdir()</code>：引用转交给 <code>proc-&gt;cwd</code>。</li></ul><p>判断方式就是：</p><div class="code-wrapper"><pre><code class="hljs text">解锁后还需要这个 inode 或要把引用交给别人？    是 → iunlock()    否 → iunlockput()</code></pre></div></blockquote></details><p>上面的误会也导致了我开始的错误实现（包括 <code>open()</code>）</p><p>因此给出 <code>symlink()</code> 的实现：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 37 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 37 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_symlink</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> path[MAXPATH], target[MAXPATH];  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span>;</span>  <span class="hljs-keyword">if</span>(argstr(<span class="hljs-number">1</span>, path, MAXPATH) &lt; <span class="hljs-number">0</span> || argstr(<span class="hljs-number">0</span>, target, MAXPATH) &lt; <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  begin_op();  <span class="hljs-comment">// 这段代码不需要，`create()` 自己会检查目标路径是否已存在</span>  <span class="hljs-comment">// if (namei(path) != 0) {</span>  <span class="hljs-comment">//   end_op();</span>  <span class="hljs-comment">//   return -1;</span>  <span class="hljs-comment">// }</span>  ip = create(path, T_SYMLINK, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);  <span class="hljs-keyword">if</span> (ip == <span class="hljs-number">0</span>) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }    <span class="hljs-type">int</span> len = <span class="hljs-built_in">strlen</span>(target) + <span class="hljs-number">1</span>;  <span class="hljs-keyword">if</span> (writei(ip, <span class="hljs-number">0</span>, (uint64)target, <span class="hljs-number">0</span>, len) != len) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// iupdate(ip); 不需要，writei() 有</span>  iunlockput(ip);  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div></details><h3 id="修改-open">修改 <code>open()</code></h3><p>因为如果 <code>open()</code> 传入的 <code>path</code> 实际上只是一个符号链接，肯定不能直接返回符号链接的 <code>inode</code> 对应的 <code>fd</code>，而应该返回其指向的 <code>target</code> 所对应的。这里加上特殊处理即可。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 96 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 96 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_open</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> path[MAXPATH];  <span class="hljs-type">int</span> fd, omode;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span>;</span>  <span class="hljs-type">int</span> n;  argint(<span class="hljs-number">1</span>, &amp;omode);  <span class="hljs-keyword">if</span>((n = argstr(<span class="hljs-number">0</span>, path, MAXPATH)) &lt; <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  begin_op();  <span class="hljs-keyword">if</span>(omode &amp; O_CREATE){    ip = create(path, T_FILE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);    <span class="hljs-keyword">if</span>(ip == <span class="hljs-number">0</span>){      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  } <span class="hljs-keyword">else</span> {    <span class="hljs-keyword">if</span>((ip = namei(path)) == <span class="hljs-number">0</span>){      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    ilock(ip);    <span class="hljs-comment">// 目录检查放在展开之后</span>  }  <span class="hljs-comment">// ========</span>  <span class="hljs-type">int</span> depth = <span class="hljs-number">0</span>;  <span class="hljs-keyword">while</span> (ip-&gt;type == T_SYMLINK &amp;&amp; omode != O_NOFOLLOW) {    <span class="hljs-type">char</span> target[MAXPATH];    <span class="hljs-keyword">if</span> (readi(ip, <span class="hljs-number">0</span>, (uint64)target, <span class="hljs-number">0</span>, MAXPATH) &lt;= <span class="hljs-number">0</span>) {      iunlockput(ip);      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    iunlockput(ip);    <span class="hljs-keyword">if</span> ((ip = namei(target)) == <span class="hljs-number">0</span>) {      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    ilock(ip);    depth++;    <span class="hljs-keyword">if</span> (depth &gt; <span class="hljs-number">10</span>) {      iunlockput(ip);      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  }  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DIR &amp;&amp; omode != O_RDONLY){    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-comment">// ========</span>  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DEVICE &amp;&amp; (ip-&gt;major &lt; <span class="hljs-number">0</span> || ip-&gt;major &gt;= NDEV)){    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>((f = filealloc()) == <span class="hljs-number">0</span> || (fd = fdalloc(f)) &lt; <span class="hljs-number">0</span>){    <span class="hljs-keyword">if</span>(f)      fileclose(f);    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DEVICE){    f-&gt;type = FD_DEVICE;    f-&gt;major = ip-&gt;major;  } <span class="hljs-keyword">else</span> {    f-&gt;type = FD_INODE;    f-&gt;off = <span class="hljs-number">0</span>;  }  f-&gt;ip = ip;  f-&gt;readable = !(omode &amp; O_WRONLY);  f-&gt;writable = (omode &amp; O_WRONLY) || (omode &amp; O_RDWR);  <span class="hljs-keyword">if</span>((omode &amp; O_TRUNC) &amp;&amp; ip-&gt;type == T_FILE){    itrunc(ip);  }    iunlock(ip);  end_op();  <span class="hljs-keyword">return</span> fd;}</code></pre></div></details><p>这里可能存在的问题是不知道该在哪里插入，我的建议是分析一下 xv6 一共有的这些 flags 分别对应的情况，然后照着分支去分析</p><p><img src="https://image.wendaining.top/image-20260819004054132.png" alt="通过所有测试"></p><h3 id="另：一开始的错误实现">另：一开始的错误实现</h3><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 133 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 133 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_symlink</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> path[MAXPATH], target[MAXPATH];  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span>;</span>  <span class="hljs-keyword">if</span>(argstr(<span class="hljs-number">0</span>, path, MAXPATH) &lt; <span class="hljs-number">0</span> || argstr(<span class="hljs-number">1</span>, target, MAXPATH) &lt; <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  begin_op();  <span class="hljs-keyword">if</span> (namei(path) != <span class="hljs-number">0</span>) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  ip = create(path, T_SYMLINK, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);  <span class="hljs-keyword">if</span> (ip == <span class="hljs-number">0</span>) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }    <span class="hljs-keyword">if</span> (writei(ip, <span class="hljs-number">0</span>, (uint64)target, <span class="hljs-number">0</span>, MAXPATH) &lt; <span class="hljs-number">0</span>) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  iupdate(ip);  iunlock(ip);  end_op();  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}uint64<span class="hljs-title function_">sys_open</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> path[MAXPATH];  <span class="hljs-type">int</span> fd, omode;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">f</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">inode</span> *<span class="hljs-title">ip</span>;</span>  <span class="hljs-type">int</span> n;  argint(<span class="hljs-number">1</span>, &amp;omode);  <span class="hljs-keyword">if</span>((n = argstr(<span class="hljs-number">0</span>, path, MAXPATH)) &lt; <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  begin_op();  <span class="hljs-keyword">if</span>(omode &amp; O_CREATE){    ip = create(path, T_FILE, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);    <span class="hljs-keyword">if</span>(ip == <span class="hljs-number">0</span>){      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  } <span class="hljs-keyword">else</span> {    <span class="hljs-keyword">if</span>((ip = namei(path)) == <span class="hljs-number">0</span>){      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    ilock(ip);    <span class="hljs-keyword">if</span>(ip-&gt;type == T_DIR &amp;&amp; omode != O_RDONLY){      iunlockput(ip);      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  }  <span class="hljs-comment">// ========</span>  ip = namei(path);  <span class="hljs-keyword">if</span> (ip == <span class="hljs-number">0</span>) {    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  ilock(ip);  <span class="hljs-type">int</span> depth = <span class="hljs-number">0</span>;  <span class="hljs-keyword">while</span> (ip-&gt;type == T_SYMLINK &amp;&amp; omode != O_NOFOLLOW) {    <span class="hljs-type">char</span> target[MAXPATH];    <span class="hljs-keyword">if</span> (readi(ip, <span class="hljs-number">0</span>, (uint64)target, <span class="hljs-number">0</span>, MAXPATH) &lt; <span class="hljs-number">0</span>) {      iunlockput(ip);      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    iunlockput(ip);    <span class="hljs-keyword">if</span> ((ip = namei(target)) == <span class="hljs-number">0</span>) {      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }    ilock(ip);    depth++;    <span class="hljs-keyword">if</span> (depth &gt; <span class="hljs-number">10</span>) {      iunlockput(ip);      end_op();      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  }  <span class="hljs-comment">// ========</span>  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DEVICE &amp;&amp; (ip-&gt;major &lt; <span class="hljs-number">0</span> || ip-&gt;major &gt;= NDEV)){    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>((f = filealloc()) == <span class="hljs-number">0</span> || (fd = fdalloc(f)) &lt; <span class="hljs-number">0</span>){    <span class="hljs-keyword">if</span>(f)      fileclose(f);    iunlockput(ip);    end_op();    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(ip-&gt;type == T_DEVICE){    f-&gt;type = FD_DEVICE;    f-&gt;major = ip-&gt;major;  } <span class="hljs-keyword">else</span> {    f-&gt;type = FD_INODE;    f-&gt;off = <span class="hljs-number">0</span>;  }  f-&gt;ip = ip;  f-&gt;readable = !(omode &amp; O_WRONLY);  f-&gt;writable = (omode &amp; O_WRONLY) || (omode &amp; O_RDWR);  <span class="hljs-keyword">if</span>((omode &amp; O_TRUNC) &amp;&amp; ip-&gt;type == T_FILE){    itrunc(ip);  }  iunlock(ip);  end_op();  <span class="hljs-keyword">return</span> fd;}</code></pre></div></details>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab7 locks - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/08/17/xv6-lab7-locks/</link>
      <description>xv6 的第七个 lab，实现关于锁的代码，主要是将大锁拆小（多 CPU），以及读者写者锁的实现。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Mon, 17 Aug 2026 19:09:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读">阅读</h2><p>简单读一下书吧，让 GPT 翻译并提炼重点，然后读对应的部分。</p><p>并发的来源：</p><ul><li>多核 CPU</li><li>线程切换</li><li>中断</li></ul><p>回顾概念：</p><ul><li>竞态条件</li><li>临界区</li></ul><p>减少锁的竞争：这个 ostep 也有阐述。给整个数据结构加一把大锁固然能解决问题，但是效率低。</p><p>关于锁的源码：<code>kernel/spinlock.h</code> <code>kernel/spinlock.c</code></p><p>关于 spinlock 自旋锁</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> {</span>  uint locked; <span class="hljs-comment">// 0 表示没被持有</span>  <span class="hljs-type">char</span> *name;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">cpu</span> *<span class="hljs-title">cpu</span>;</span>};</code></pre></div><p>关于得锁，RISC-V 提供原子指令 <code>amoswap</code>，原子地完成读取内存-寄存器写入内存-返回旧值，也就是 <code>acquire()</code> 的实现。释放锁是类似的思想和原理。</p><p>Lab 的重点是对 <code>kalloc()</code> 进行优化，把锁的颗粒度拆细。</p><p>需要防止死锁。</p><p>xv6 的实现里面，只要有任何自旋锁被持有，就关闭 CPU 中断。</p><p><code>acquire()</code> 内部调用：</p><div class="code-wrapper"><pre><code class="hljs c">push_off();</code></pre></div><p>关闭中断。</p><p><code>release()</code> 最后调用：</p><div class="code-wrapper"><pre><code class="hljs c">pop_off();</code></pre></div><p>恢复中断。</p><p>关于 sleeplock：因为 spinlock 不能中断，才引入。</p><p>使用 <code>acquiresleep()</code> 让出 CPU，内部使用一个 spinlock 进行保护，之后可以 wakeup。</p><h2 id="Memory-allocator">Memory allocator</h2><p>大致是有一个 <code>kalloctest.c</code>，里面会高强度进行锁竞争（反复调用 <code>kalloc() free()</code>）。</p><p>根本原因是<code>kalloc()</code> 只有一个 free list，并且这个 free list 由一个全局锁保护。</p><p>减少锁竞争的基本思路是重新设计内存分配器，<strong>因此本 lab 的任务是为每个 CPU 单独维护一个 free list，并给每个 list 配一个自己的锁</strong>。</p><p>所以这个 lab 必须在有多核 CPU 的机器上做。</p><p>一个主要难点是某个 CPU 的 free list 已经空了，但另一个 CPU 的 free list 中还有空闲内存，这个时候，缺少内存的 CPU 需要去其他 CPU 的 free list 里面获取一些空闲页面，也就是所谓的 <strong>stealing</strong>。</p><p>现在着手实现：</p><p>首先是需要修改 <code>kmem</code>，就是原来空闲链表的结构体（一个 freelist 里面带一个锁）：</p><p>原来：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">lock</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">freelist</span>;</span>} kmem;</code></pre></div><p>修改成支持多 CPU 的形式：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">lock</span>[<span class="hljs-title">NCPU</span>];</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">freelist</span>[<span class="hljs-title">NCPU</span>];</span>} kmem;</code></pre></div><p>然后修改 <code>kinit()</code> 等，总之要使用到当前运行的 CPU。</p><p>这里首先需要知道获取当前运行的 CPU 的 API，这里 hint 3 也有说，不过还是问 LLM ：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 2091 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">核心 API 只有三个：cpuid()、push_off()、pop_off()。 cpuid() 做什么 kernel/proc.c 中： i...</span></summary><blockquote><p>核心 API 只有三个：<code>cpuid()</code>、<code>push_off()</code>、<code>pop_off()</code>。</p><ol><li><code>cpuid()</code> 做什么</li></ol><p><a href="/home/wendaining/alles/xv6-labs-2025/kernel/proc.c:64">kernel/proc.c</a> 中：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">cpuid</span><span class="hljs-params">()</span>{  <span class="hljs-type">int</span> id = r_tp();  <span class="hljs-keyword">return</span> id;}</code></pre></div><p>每个 CPU 都有一个编号，范围是：</p><div class="code-wrapper"><pre><code class="hljs text">0 到 NCPU - 1</code></pre></div><p><code>cpuid()</code> 从 RISC-V 的 <code>tp</code> 寄存器读取这个编号。因此，如果当前代码运行在 CPU 3 上：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> id = cpuid();</code></pre></div><p>得到的 <code>id</code> 就是 <code>3</code>，之后可以访问：</p><div class="code-wrapper"><pre><code class="hljs c">kmem.lock[id]kmem.freelist[id]</code></pre></div><ol start="2"><li>为什么不能直接调用 <code>cpuid()</code></li></ol><p>问题在于：进程可能被调度到另一个 CPU。</p><p>设想下面的情况：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> id = cpuid();  <span class="hljs-comment">// 此时得到 2</span><span class="hljs-comment">// 这里发生时钟中断和重新调度</span><span class="hljs-comment">// 进程后来在 CPU 5 上继续运行</span></code></pre></div><p>此时变量 <code>id</code> 还是 <code>2</code>，但代码已经运行在 CPU 5 上了。“当前 CPU”与保存的编号不再一致。</p><p>因此，<code>cpuid()</code> 的使用约定是：<strong>读取和使用 CPU 编号期间必须关闭中断。</strong></p><p>源码注释也明确写了这一点：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Must be called with interrupts disabled,</span><span class="hljs-comment">// to prevent race with process being moved</span><span class="hljs-comment">// to a different CPU.</span></code></pre></div><ol start="3"><li><code>push_off()</code> 和 <code>pop_off()</code></li></ol><p>安全的基本形式是：</p><div class="code-wrapper"><pre><code class="hljs c">push_off();<span class="hljs-type">int</span> id = cpuid();<span class="hljs-comment">// 在这里使用 id</span>pop_off();</code></pre></div><ul><li><code>push_off()</code>：关闭当前 CPU 的中断。</li><li><code>pop_off()</code>：恢复进入这段代码之前的中断状态。</li></ul><p>这段时间不会因为时钟中断被抢占，所以当前执行流不会突然迁移到其他 CPU。</p><p>不建议直接使用 <code>intr_off()</code> 和 <code>intr_on()</code>，因为它们不能正确处理嵌套。比如自旋锁的 <code>acquire()</code> 本身也会调用 <code>push_off()</code>。</p><p><code>push_off()</code>/<code>pop_off()</code> 会记录嵌套层数，因此下面这样是合法的：</p><div class="code-wrapper"><pre><code class="hljs c">push_off();                 <span class="hljs-comment">// 第 1 层</span><span class="hljs-type">int</span> id = cpuid();acquire(&amp;kmem.lock[id]);    <span class="hljs-comment">// acquire 内部进入第 2 层</span><span class="hljs-comment">// 修改 freelist</span>release(&amp;kmem.lock[id]);    <span class="hljs-comment">// 退出第 2 层</span>pop_off();                  <span class="hljs-comment">// 退出第 1 层</span></code></pre></div><p>在最外层 <code>pop_off()</code> 之前，中断仍然保持关闭。</p><ol start="4"><li>为什么不能取得编号后马上 <code>pop_off()</code></li></ol><p>下面这种写法不理想：</p><div class="code-wrapper"><pre><code class="hljs c">push_off();<span class="hljs-type">int</span> id = cpuid();pop_off();acquire(&amp;kmem.lock[id]);</code></pre></div><p>因为 <code>pop_off()</code> 之后，代码可能迁移到另一个 CPU，然后操作的就不再是“当前 CPU 的 freelist”。</p><p>所以在 <code>kfree()</code> 中，应该把中断关闭范围覆盖到对应 freelist 操作结束：</p><div class="code-wrapper"><pre><code class="hljs c">push_off();<span class="hljs-type">int</span> id = cpuid();acquire(&amp;kmem.lock[id]);<span class="hljs-comment">// 把 r 插入 kmem.freelist[id]</span>release(&amp;kmem.lock[id]);pop_off();</code></pre></div><p>这基本就是你下一步需要完成的内容。</p><ol start="5"><li><code>freerange()</code> 为什么会把所有页面交给一个 CPU</li></ol><p>启动流程大致是：</p><div class="code-wrapper"><pre><code class="hljs text">启动 CPU  → kinit()    → 初始化所有 kmem 锁    → freerange()      → 对每个物理页调用 kfree()</code></pre></div><p><code>freerange()</code> 本身不需要知道 CPU 编号：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">for</span>(; p + PGSIZE &lt;= (<span class="hljs-type">char</span>*)pa_end; p += PGSIZE)  kfree(p);</code></pre></div><p>真正选择 freelist 的是 <code>kfree()</code>。</p><p>启动时，<code>kinit()</code> 只在启动 CPU 上执行，通常就是 CPU 0。因此，每次 <code>kfree(p)</code> 取得的 CPU 编号都是 0，最终效果是：</p><div class="code-wrapper"><pre><code class="hljs text">kmem.freelist[0]：拥有全部初始空闲页kmem.freelist[1]：空kmem.freelist[2]：空...</code></pre></div><p>这就是 hint 所说的：</p><blockquote><p>让 <code>freerange()</code> 把所有空闲内存先交给当前正在运行 <code>freerange()</code> 的 CPU。</p></blockquote><p>这不是要求修改 <code>freerange()</code>，而是要求 <code>kfree()</code> 根据当前 CPU 选择 freelist。之后其他 CPU 第一次分配内存时发现自己的 freelist 为空，再通过 stealing 从 CPU 0 获取页面。</p><p>你现在可以先只改 <code>kfree()</code>，暂时不管 <code>kalloc()</code> 和 stealing。改动目标就是把原来的：</p><div class="code-wrapper"><pre><code class="hljs c">kmem.lockkmem.freelist</code></pre></div><p>换成由安全取得的 <code>id</code> 索引的：</p><div class="code-wrapper"><pre><code class="hljs c">kmem.lock[id]kmem.freelist[id]</code></pre></div></blockquote></details><p>所以 <code>kfree()</code> 和 <code>kalloc()</code> 这样改：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 37 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 37 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">kfree</span><span class="hljs-params">(<span class="hljs-type">void</span> *pa)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  <span class="hljs-keyword">if</span>(((uint64)pa % PGSIZE) != <span class="hljs-number">0</span> || (<span class="hljs-type">char</span>*)pa &lt; end || (uint64)pa &gt;= PHYSTOP)    panic(<span class="hljs-string">"kfree"</span>);  <span class="hljs-comment">// Fill with junk to catch dangling refs.</span>  <span class="hljs-built_in">memset</span>(pa, <span class="hljs-number">1</span>, PGSIZE);  r = (<span class="hljs-keyword">struct</span> run*)pa;  push_off();  <span class="hljs-type">int</span> id = cpuid();  acquire(&amp;kmem.lock[id]);  r-&gt;next = kmem.freelist[id];  kmem.freelist[id] = r;  release(&amp;kmem.lock[id]);  pop_off();}<span class="hljs-type">void</span> *<span class="hljs-title function_">kalloc</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  push_off();  <span class="hljs-type">int</span> id = cpuid();  acquire(&amp;kmem.lock[id]);  r = kmem.freelist;  <span class="hljs-keyword">if</span>(r)    kmem.freelist[id] = r-&gt;next;  release(&amp;kmem.lock[id]);  pop_off();  <span class="hljs-keyword">if</span>(r)    <span class="hljs-built_in">memset</span>((<span class="hljs-type">char</span>*)r, <span class="hljs-number">5</span>, PGSIZE); <span class="hljs-comment">// fill with junk</span>  <span class="hljs-keyword">return</span> (<span class="hljs-type">void</span>*)r;}</code></pre></div></details><p>但是这样还没有实现 stealing。</p><p>修改 <code>kalloc()</code>：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 33 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 33 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span> *<span class="hljs-title function_">kalloc</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  push_off();  <span class="hljs-type">int</span> id = cpuid();  acquire(&amp;kmem.lock[id]);  r = kmem.freelist[id];  <span class="hljs-keyword">if</span>(r)    kmem.freelist[id] = r-&gt;next;  release(&amp;kmem.lock[id]);  <span class="hljs-keyword">if</span> (r == <span class="hljs-number">0</span>) {    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; NCPU; ++i) {      <span class="hljs-comment">// 找到序号最小的有空余页的空闲列表</span>      <span class="hljs-keyword">if</span> (i == id) {        <span class="hljs-keyword">continue</span>;      }      acquire(&amp;kmem.lock[i]);      <span class="hljs-keyword">if</span> (kmem.freelist[i]) {        r = kmem.freelist[i];        kmem.freelist[i] = r-&gt;next;      }      release(&amp;kmem.lock[i]);      <span class="hljs-keyword">if</span> (r) {        <span class="hljs-keyword">break</span>;      }    }  }  pop_off();  <span class="hljs-keyword">if</span>(r)    <span class="hljs-built_in">memset</span>((<span class="hljs-type">char</span>*)r, <span class="hljs-number">5</span>, PGSIZE); <span class="hljs-comment">// fill with junk</span>  <span class="hljs-keyword">return</span> (<span class="hljs-type">void</span>*)r;}</code></pre></div></details><p>然后可以通过所有测试。</p><h2 id="Read-write-lock">Read-write lock</h2><p>考虑 xv6 中的：</p><div class="code-wrapper"><pre><code class="hljs c">sys_pause()sys_uptime()</code></pre></div><p>这两个函数都会读取全局变量 <code>ticks</code>。</p><p>但是 <code>ticks</code> 会同时被 <code>clockintr()</code> 更新，所以两个函数读取 <code>ticks</code> 之前会获取 <code>tickslock</code>。但是其实只需要持写锁就行了，完全没必要设读相关的锁。这个 lab 就是区分读者和写者。</p><p>复习一下 ostep 的内容，读写者问题里面，规则是：</p><ul><li>同一时间最多只能有 <strong>一个 writer</strong>；</li><li>存在 writer 时，不能存在 reader；</li><li>如果没有 writer，则可以同时存在 <strong>多个 reader</strong>。</li><li>为了防止 reader 一直读导致 writer 饿死，设置 writer priority，即一旦有 writer 开始尝试获取锁，那么之后到来的 reader 必须等待，直到这个 writer 成功获得锁并释放它。</li></ul><p>read-write lock API (in <code>kernel/defs.h</code>)：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span> <span class="hljs-title function_">initrwlock</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock*)</span>;<span class="hljs-type">void</span> <span class="hljs-title function_">read_acquire</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock*)</span>;<span class="hljs-type">void</span> <span class="hljs-title function_">read_release</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock*)</span>;<span class="hljs-type">void</span> <span class="hljs-title function_">write_acquire</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock*)</span>;<span class="hljs-type">void</span> <span class="hljs-title function_">write_release</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock*)</span>;</code></pre></div><p>本 lab 需要补全 <code>kernel/spinlock.c</code> 中 read-write spinlock API 对应的 stub 函数，以及修改  <code>kernel/spinlock.h</code> 中的 <code>struct rwspinlock</code> 定义。</p><p>hints 说要读 <code>sys_rwlktest()</code> 函数了解测试用例，其实大概就是一直验证读和写（上述的四条规则）。</p><p>从测试可以反推，这个锁至少需要记录：</p><ul><li>读者数</li><li>当前是否有 active reader</li><li>当前 waiting writer 的数量</li></ul><p>同时，<strong>需要保证状态的转换都是原子的</strong>。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Reader-writer lock.</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">rwspinlock</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">l</span>;</span> <span class="hljs-comment">// state lock</span>  <span class="hljs-type">int</span> readers;  <span class="hljs-type">int</span> waiting_writers;  <span class="hljs-type">int</span> is_writer_active;};</code></pre></div><p>hints 说：</p><blockquote><p>如果你什么都不修改，直接在 xv6 中运行 <code>rwlktest</code>，内核会打印：</p><div class="code-wrapper"><pre><code class="hljs text">panic: acquire</code></pre></div><p>原因是原有 spinlock 的实现只允许某一时刻由一个 thread 持有锁。</p><p>你需要替换：</p><div class="code-wrapper"><pre><code class="hljs text">write_acquire_innerwrite_release_innerread_acquire_innerread_release_inner</code></pre></div><p>中对 <code>acquire()</code> 和 <code>release()</code> 的调用，改为你自己的锁实现。</p></blockquote><p>实现的部分：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 60 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 60 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">read_acquire_inner</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock *rwlk)</span>{  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {    acquire(&amp;rwlk-&gt;l);    <span class="hljs-keyword">if</span> (rwlk-&gt;is_writer_active == <span class="hljs-number">0</span> &amp;&amp; rwlk-&gt;waiting_writers == <span class="hljs-number">0</span>) {      rwlk-&gt;readers++;      release(&amp;rwlk-&gt;l);      <span class="hljs-keyword">return</span>;    }    release(&amp;rwlk-&gt;l);  }}<span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">read_release_inner</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock *rwlk)</span>{  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {    acquire(&amp;rwlk-&gt;l);    <span class="hljs-keyword">if</span> (rwlk-&gt;readers &gt; <span class="hljs-number">0</span>) {      rwlk-&gt;readers--;      release(&amp;rwlk-&gt;l);      <span class="hljs-keyword">return</span>;    }    release(&amp;rwlk-&gt;l);  }}<span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">write_acquire_inner</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock *rwlk)</span>{  <span class="hljs-comment">// 首先这个肯定不能放到循环里面，其次这个务必要加锁</span>  acquire(&amp;rwlk-&gt;l);  rwlk-&gt;waiting_writers++;  release(&amp;rwlk-&gt;l);  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {    acquire(&amp;rwlk-&gt;l);    <span class="hljs-keyword">if</span> (rwlk-&gt;readers == <span class="hljs-number">0</span> &amp;&amp; rwlk-&gt;is_writer_active == <span class="hljs-number">0</span>) {      rwlk-&gt;waiting_writers--;      rwlk-&gt;is_writer_active = <span class="hljs-number">1</span>;      release(&amp;rwlk-&gt;l);      <span class="hljs-keyword">return</span>;    }    release(&amp;rwlk-&gt;l);  }}<span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">write_release_inner</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> rwspinlock *rwlk)</span>{  <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span>) {    acquire(&amp;rwlk-&gt;l);    <span class="hljs-keyword">if</span> (rwlk-&gt;is_writer_active) {      rwlk-&gt;is_writer_active = <span class="hljs-number">0</span>;      release(&amp;rwlk-&gt;l);      <span class="hljs-keyword">return</span>;    }    release(&amp;rwlk-&gt;l);  }}</code></pre></div></details><p>2 个注意点：</p><ol><li>务必要加循环，因为是一直去尝试获得，考虑到这是自旋锁<ul><li>我开始是读了 ostep 的实现读写锁的部分没看到，我后面才意识到人家是用的信号量机制，而信号量机制底层还是读写锁。我们实现的这些数据，其实就有点像是实现了一个信号量机制。</li></ul></li><li><code>write_acquire_inner(*struct* rwspinlock **rwlk*)</code> 函数，<code>waiting_writers</code> 变量的添加，肯定是要放在循环外面并且是函数开头的，因为这表示了开始等待，而自旋空转就是等待的过程。其次显然要加锁，这里开始忘记加了，test8过不去。</li></ol><p><img src="https://image.wendaining.top/image-20260818183344272.png" alt="通过所有测试"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab6 network driver - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/08/10/xv6-lab6-network-driver/</link>
      <description>xv6 的第六个 lab，写一个网络驱动。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Mon, 10 Aug 2026 11:09:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读">阅读</h2><p>中断 Interrupt：硬件需要得到 OS 的关注，产生一个中断，过程与系统调用类似。但是不同点：</p><ul><li>异步，与当前 CPU 运行的进程无关</li><li>并行，设备与 CPU 并行运行</li><li>需要驱动程序</li></ul><p>许多设备驱动都会在两个不同的上下文中执行代码：</p><ul><li><strong>top half（上半部）</strong>：运行在某个进程的内核线程中。</li><li><strong>bottom half（下半部）</strong>：在发生中断时执行，也就是中断处理程序。</li></ul><h2 id="Part-One：NIC">Part One：NIC</h2><p>网络栈已经准备好了一个装有完整数据包的内存 buffer（存放数据包 packets 的），我们需要做的是补全两个函数。</p><p>一些陌生名词的解释：</p><ul><li><p>NIC Network Interface Card 网卡，E1000 也是网卡的一种</p></li><li><p>DMA Direct Memory Access 直接内存访问：</p><ul><li>一般而言，硬件不能直接操纵内核内存</li><li>但是配置好 DMA 之后，E1000 可以直接读写指定的内存，也就是 buffer</li><li>这样，CPU 就不需要自己搬运数据</li></ul></li><li><p>Descriptor 描述符，主要是告诉网卡，数据包的位置、长度、以及别的信息，代码里面是 <code>tx_desc</code></p>  <div class="code-wrapper"><pre><code class="hljs text">descriptor├── addr：buffer 的内存地址├── length：数据包长度├── status：网卡是否处理完成└── cmd：要求网卡执行什么操作</code></pre></div><ul><li>TX descriptor：描述「需要发送的数据包」。</li><li>RX descriptor：描述「用来接收数据包的空 buffer」。</li></ul></li><li><p>Descriptor Ring 因为描述符的数量不够，所以是一个回环，题目里面是 0~15</p></li><li><p>Control register，驱动通过控制寄存器来通知 E1000 相关的操作，具体而言，是 <code>regs[]</code></p></li><li><p>TDT Transmit Descriptor Tail，可以理解为「TX ring 中，驱动下一次应该填写哪个 descriptor」</p></li><li><p>RDT Receive Descriptor Tail，「驱动已经处理完并重新交还给 E1000 的最后一个 RX descriptor」</p></li><li><p>DD Descriptor Done，descriptor  的 <code>status</code> 字段中的一个标志位</p><ul><li>TX 的 DD 表示 E1000 已经把这个 descriptor 对应的数据包发送完，也不再读取它的 buffer</li><li>RX 的 DD 表示 E1000 已经收到一个数据包，并将它完整写入这个 descriptor 指向的 buffer</li></ul></li><li><p>EOP End Of Packet 数据包的结尾</p></li><li><p>RS Report Status</p></li><li><p><code>net_rx()</code> xv6 网络栈接收数据包的入口</p></li></ul><p>首先是补全 <code>e1000_transmit()</code>：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 26 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 26 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">e1000_transmit</span><span class="hljs-params">(<span class="hljs-type">char</span> *buf, <span class="hljs-type">int</span> len)</span>{  acquire(&amp;e1000_lock);  uint32 index = regs[E1000_TDT];  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">tx_desc</span> *<span class="hljs-title">desc</span> =</span> &amp;tx_ring[index];  <span class="hljs-keyword">if</span>((desc-&gt;status &amp; E1000_TXD_STAT_DD) == <span class="hljs-number">0</span>){    release(&amp;e1000_lock);    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-keyword">if</span>(desc-&gt;addr != <span class="hljs-number">0</span>)    kfree((<span class="hljs-type">void</span> *)desc-&gt;addr);  desc-&gt;addr = (uint64)buf;  desc-&gt;length = len;  desc-&gt;cmd = E1000_TXD_CMD_EOP | E1000_TXD_CMD_RS;  desc-&gt;status = <span class="hljs-number">0</span>;  regs[E1000_TDT] = (index + <span class="hljs-number">1</span>) % TX_RING_SIZE;  release(&amp;e1000_lock);  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div></details><p>根据上面的概念和 hints，即可完成。</p><p>然后是 <code>e1000_recv()</code>，同理完成即可。</p><h2 id="Part-Two-UDP-Receive">Part Two: UDP Receive</h2><p>这个 part 里面，spec 给得更加详细了。照着读就行，甚至不需要读别的手册。</p><p>照着 hints 一步一步来：</p><h3 id="1-创建数据结构">1. 创建数据结构</h3><blockquote><p>Create a struct to keep track of bound ports and the packets in their queues.</p></blockquote><p>定义两个：</p><ul><li><code>udp_packet</code> 表示队列中的一个数据包</li><li><code>udp_port</code> 表示一个已经绑定的端口，同时记录下一个端口，从而形成链表，依此可以检查端口是否分配</li></ul><p>使用端口对象池分配绑定端口：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> UDP_PORTS_PER_PAGE 12</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">udp_port_page</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">udp_port</span> <span class="hljs-title">ports</span>[<span class="hljs-title">UDP_PORTS_PER_PAGE</span>];</span>  <span class="hljs-type">int</span> used;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">udp_port_page</span> *<span class="hljs-title">next</span>;</span>};</code></pre></div><p><code>sys_bind()</code> 的实现：检查传入端口号，然后检查是否绑定，没有就分配，然后加入 ports 链表。</p><p><code>ip_rx()</code> 的实现：检查收到的包，然后找对应的端口。</p><p><code>sys_recv()</code> 的实现：根据hint跑即可。</p><p>另：有个试着获得 mit pdos 官网 DNS 的测试，如果开着梯子的一些特殊设置似乎过不去，然后我就试着关掉梯子 / 系统代理 / TUN 连环关掉测试...最后得到的结果是：打开系统代理，关闭 TUN 即可。</p><p>记得修改代理之后，还需要重启 WSL 才能应用。</p><p><img src="https://image.wendaining.top/5a227a70-80ad-4be2-b7ce-a88cd4cd0981.png" alt="通过所有测试"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab5 Copy on-write - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/08/08/xv6-lab5-copy-on-write/</link>
      <description>xv6 的第五个 lab，理解并实现 Copy on-write 写时复制机制。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Sat, 08 Aug 2026 11:01:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读">阅读</h2><p>这个 lab 的 guidance 罕见地没有给出任何 Reading xv6 book 的指示。但是我还是看一看课程的文档：<a href="https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec08-page-faults-frans/8.1-page-fault-basics">8.1 Page Fault Basics | MIT6.S081</a></p><p>这一章节事实上涵盖了原本有的 Lazy Allocation Lab, Copy on-write Lab, 以及 mmap lab。</p><p>这里只简单概述一下 Copy on-write 的思想：</p><p>一个进程 fork 之后，创建的是父进程的一个完整拷贝。在目前的实现中，是直接复制物理空间；而如果 fork 之后还立刻执行 exec，则这个地址空间还会被丢弃，很浪费。</p><p>cow 的思想就是子进程和父进程的虚拟地址空间映射到同一个物理地址，但是 PTE 设置为只读。如果有写操作发生（无论是父还是子进程执行的写），则触发 page fault，拷贝相应的页，PTE 设置为可读写，再继续执行。</p><p>将一个页标记为 cow 页，使用的是 PTE 的最后一个 bit。</p><p>同时，由于之前的实现中不存在一个物理地址对应多个虚拟地址这样的映射，导致进程结束时的释放也会存在问题。解决方案是对物理页框添加引用计数。</p><h2 id="Implement-copy-on-write-fork">Implement copy-on-write fork</h2><p>其实 lab 这里对于什么是 copy on-write 的说明都很清晰了，比课程文档还清晰易懂一点。</p><p>根据提供的步骤，一步一步来即可，不过需要先实现一些基础设施（也就是 hint 的内容）：</p><h3 id="为-PTE-记录-COW-映射">为 PTE 记录 COW 映射</h3><p>在 <code>riscv.h</code> 里面</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> PTE_COW (1L &lt;&lt; 8)</span></code></pre></div><p>之后改 flags 的时候里面随便改。</p><h3 id="修改-uvmcopy">修改 <code>uvmcopy()</code></h3><p>这个函数原本的作用是在创建子进程时，把父进程的页表完全拷贝进新进程（在 Page table Lab 里面分析得很清楚了）。</p><p>这里需要：不要再为子进程分配新的物理页面并复制数据，改成<strong>直接把父进程的物理页面映射进子进程页表</strong>，并且<strong>将父进程和子进程的页中的 <code>PTE_W</code> 都清空</strong>。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 29 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 29 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">uvmcopy</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> old, <span class="hljs-type">pagetable_t</span> new, uint64 sz)</span>{  <span class="hljs-type">pte_t</span> *pte;  uint64 pa, i;  uint flags;  <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; i &lt; sz; i += PGSIZE){    <span class="hljs-keyword">if</span>((pte = walk(old, i, <span class="hljs-number">0</span>)) == <span class="hljs-number">0</span>)      <span class="hljs-keyword">continue</span>;   <span class="hljs-comment">// page table entry hasn't been allocated</span>    <span class="hljs-keyword">if</span>((*pte &amp; PTE_V) == <span class="hljs-number">0</span>)      <span class="hljs-keyword">continue</span>;   <span class="hljs-comment">// physical page hasn't been allocated</span>    pa = PTE2PA(*pte);    flags = PTE_FLAGS(*pte);    <span class="hljs-keyword">if</span>(flags &amp; PTE_W) {      flags &amp;= ~PTE_W;      flags |= PTE_COW;      *pte = PA2PTE(pa) | flags;    }    <span class="hljs-keyword">if</span>(mappages(new, i, PGSIZE, pa, flags) != <span class="hljs-number">0</span>)      <span class="hljs-keyword">goto</span> err;  }  <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span> 是否要刷新 TLB？留待之后分析</span>  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; err:  uvmunmap(new, <span class="hljs-number">0</span>, i / PGSIZE, <span class="hljs-number">1</span>);  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div></details><h3 id="修改-vmfault-处理-COW-page-fault">修改 <code>vmfault()</code> 处理 COW page fault</h3><p>关于 <code>vmfault()</code> 函数的作用，参考 Page table Lab 里面就有说明，主要是和 Lazy Alloaction 相关的。</p><p>目前的 <code>vmfault()</code> 函数，<code>read</code> 参数是没有用到的，我猜测作用是区分不同 page fault 的类型，询问 LLM 得到的答案：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 666 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">vmfault() 的 read 参数用于区分这次 page fault 是由“读取”还是“写入”引起的。 在 trap.c 中： vmfaul...</span></summary><blockquote><p><code>vmfault()</code> 的 <code>read</code> 参数用于区分这次 page fault 是由“读取”还是“写入”引起的。</p><p>在 <code>trap.c</code> 中：</p><div class="code-wrapper"><pre><code class="hljs c">vmfault(p-&gt;pagetable, r_stval(),        (r_scause() == <span class="hljs-number">13</span>) ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>)</code></pre></div><p>对应关系是：</p><table><thead><tr><th><code>scause</code></th><th>含义</th><th style="text-align:right"><code>read</code></th></tr></thead><tbody><tr><td>13</td><td>Load page fault，读取失败</td><td style="text-align:right">1</td></tr><tr><td>15</td><td>Store page fault，写入失败</td><td style="text-align:right">0</td></tr></tbody></table><p>你当前的 <code>vmfault()</code> 只处理 lazy allocation：只要页面未映射，就分配一个零页。读取和写入的处理相同，所以 <code>read</code> 目前没有被使用。</p><p>到 COW 阶段，它就有意义了：</p><div class="code-wrapper"><pre><code class="hljs text">read == 0，并且页面带 PTE_COW    → 这是写 COW 页面    → 分配新页、复制内容、恢复 PTE_Wread == 1    → 不能当作 COW 写入处理</code></pre></div><p>因为 COW 页面只清除了 <code>PTE_W</code>，仍然保留 <code>PTE_R</code>：</p><div class="code-wrapper"><pre><code class="hljs text">读取 COW 页面：正常，不应 fault写入 COW 页面：产生 store page fault</code></pre></div><p>因此，如果一个已经映射的页面发生读取 fault，通常说明它不是合法的 COW 写入，不能把它复制成可写页面。</p><p>另外，<code>copyout()</code> 是内核向用户空间写数据，所以之后调用 COW 处理逻辑时，应按照写操作处理，也就是相当于：</p><div class="code-wrapper"><pre><code class="hljs c">read = <span class="hljs-number">0</span>;</code></pre></div><p>总结：<code>read</code> 当前在 lazy allocation 中暂时没用，是为区分读写 page fault、特别是后面的 COW 写 fault 准备的。</p></blockquote></details><p>然后我大致有思路但是有点懒得自己手敲（实则是不太会），喂给 LLM 这样的 prompt：</p><blockquote><p>关于修改 vmfault 的这步，我大概是有个思路了：首先，如果是read=1，就和原来的逻辑一模一样去进行处理；如果是read=0，那么首先检查这个页面，检查页面的方式是walk() 一下找到 pte 然后检查 flags，然后 kalloc 分配一个新的物理页面，修改原来pte的flags（主要是write部分，顺便去掉cow），然后复制，最后让 pte 指向这个新的页面。因为只有原本设置为可写的页面才会被打上cow的标记，所以不需要做是否可写的判定。但是我脑子里有点难以组织这个逻辑如何和现有的懒分配的逻辑进行耦合，所以请你直接帮我修改一下这个代码，顺便讲解一下。</p></blockquote><p>代码实现：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 37 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 37 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">vmfault</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va, <span class="hljs-type">int</span> read)</span>{  uint64 mem, pa;  uint flags;  <span class="hljs-type">pte_t</span> *pte;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  <span class="hljs-keyword">if</span> (va &gt;= p-&gt;sz)    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  va = PGROUNDDOWN(va);  <span class="hljs-keyword">if</span>(ismapped(pagetable, va)) {    pte = walk(pagetable, va, <span class="hljs-number">0</span>);    <span class="hljs-keyword">if</span>(read || (*pte &amp; PTE_U) == <span class="hljs-number">0</span> || (*pte &amp; PTE_COW) == <span class="hljs-number">0</span>)      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;    pa = PTE2PA(*pte);    flags = PTE_FLAGS(*pte);    mem = (uint64)kalloc();    <span class="hljs-keyword">if</span>(mem == <span class="hljs-number">0</span>)      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;    memmove((<span class="hljs-type">void</span> *)mem, (<span class="hljs-type">void</span> *)pa, PGSIZE);    flags = (flags | PTE_W) &amp; ~PTE_COW;    *pte = PA2PTE(mem) | flags;    <span class="hljs-keyword">return</span> mem;  }  mem = (uint64) kalloc();  <span class="hljs-keyword">if</span>(mem == <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  <span class="hljs-built_in">memset</span>((<span class="hljs-type">void</span> *) mem, <span class="hljs-number">0</span>, PGSIZE);  <span class="hljs-keyword">if</span> (mappages(p-&gt;pagetable, va, PGSIZE, mem, PTE_W|PTE_U|PTE_R) != <span class="hljs-number">0</span>) {    kfree((<span class="hljs-type">void</span> *)mem);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">return</span> mem;}</code></pre></div></details><p>几个核心点：</p><ul><li><div class="note note-success"><p><code>va &gt;= p-&gt;sz</code> 和 <code>ismapped()</code> 检查的是两个完全不同的问题：</p><ul><li><code>va &gt;= p-&gt;sz</code>：这个虚拟地址在不在进程合法的地址范围内？</li><li><code>ismapped()</code>：这个合法虚拟地址目前有没有对应的物理页面？</li></ul></div></li><li><p>原本的懒分配逻辑是，检查是否 <code>ismapped()</code>，如果有映射说明是存在对应的物理页面的，无视。但是目前需要耦合 COW 的逻辑，而且 COW 页一定是存在对应的物理页的（考虑到 <code>uvmcopy</code> 的逻辑就是建立虚拟页对实际存在的物理页的映射，循环使用了 <code>mappages()</code> ），因此，<strong>这里 COW 的逻辑，就全部落在了原本直接 <code>return 0</code> 的 <code>ismapped</code> 部分</strong>。</p></li><li><p>这里继续检查，如果是只读，或者不是 COW 页，或者 <code>(*pte &amp; PTE_U) == 0</code> 即用户模式下不允许访问此页（如 trampoline 页等）也就直接返回掉。</p></li><li><p>然后，获取这个页对应的所有信息，<strong>和我之前给的 prompt 不同，首先复制旧页面，然后计算新 flags，最后才替换 PTE，这是为了防止 <code>kalloc()</code> 失败，原来的有效映射也可能被破坏</strong>。</p></li><li><div class="note note-primary"><p>所谓的 flags <strong>只有虚拟页也就是 pte 才有，物理页是没有的</strong>！</p></div></li><li><p>之后 COW 部分的逻辑就结束了，其余的丢给懒分配的逻辑。</p></li></ul><h3 id="实现物理页面引用计数">实现物理页面引用计数</h3><p>必须确保每个物理页面只在最后一个 PTE 引用消失后才被释放。</p><p>我开始认为，维护引用计数的规则：</p><ul><li><code>kalloc()</code> 分配页面时，初始化置 1</li><li><code>fork()</code> 让子进程共享物理页面时，引用计数 +1，虽然说是 <code>fork()</code>，但是实际上是在 <code>uvmcopy()</code> 里面实现，和下面的 <code>uvmunmap()</code> 对应。</li><li>某个进程从页表中移除此页面（<code>uvmunmap()</code>）时，引用计数 -1。</li><li><code>kfree()</code> 的逻辑：只有当引用计数为 0 时，才能放回空闲列表。</li><li>所谓的物理地址，也就是 <code>pa</code>。由于物理页框地址总是 <code>PGSIZE</code> 对齐的，可以使用 <code>pa / PGSIZE</code> 作为数组的下标，记录某个页的引用计数（实际上，这个数字就是第几个物理页框）。数组的大小，就是利用 <code>PHYSTOP</code> 就行了。</li><li>这里务必需要注意一点，这个数组不能随意修改，最好是加一把锁，然后通过设置好的函数 <code>krefinc()</code> <code>krefdec()</code> 进行加减访问，防止并发冲突。</li></ul><p>但是实际上存在几个问题：</p><ul><li><code>krefdec()</code> 其实不应该存在，而是把引用数的削减全部放在 <code>kfree()</code> 里面。因为解除虚拟页和物理页的引用的地方远远不止 <code>uvmunmap()</code> 一处。比如 <code>vmfault()</code> 就直接把旧的物理页改成了新的物理页，相当于是解引用，但是没有经过 <code>uvmunmap()</code>。但是无论如何，这里都相当于是减少了引用计数。<ul><li><strong>因此，这里需要修改 <code>kfree()</code> 的语义</strong>。应当是，先负责引用计数 -1，然后检查，如果真的减到 0 了，才执行释放（放回空闲列表中）。</li></ul></li><li><code>int phy_ref[PHYSTOP / PGSIZE];</code>  比较浪费，不如用 <code>int phy_ref[(PHYSTOP - KERNBASE) / PGSIZE]</code>，然后写一个 <code>index</code> 的映射函数。</li><li><code>freerange()</code> 需要特殊处理，因为 <code>freerange()</code> 开始的时候页面的初始计数都是 0，但是 <code>kfree()</code> 会搞成 -1，所以先赋值为 1</li></ul><p>摘一些核心的代码：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 71 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 71 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> NPAGE ((PHYSTOP - KERNBASE) / PGSIZE)</span><span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">lock</span>;</span>  <span class="hljs-type">int</span> count[NPAGE];} kref;<span class="hljs-type">static</span> <span class="hljs-type">int</span><span class="hljs-title function_">paindex</span><span class="hljs-params">(uint64 pa)</span>{  <span class="hljs-keyword">return</span> (pa - KERNBASE) / PGSIZE;}<span class="hljs-type">void</span><span class="hljs-title function_">freerange</span><span class="hljs-params">(<span class="hljs-type">void</span> *pa_start, <span class="hljs-type">void</span> *pa_end)</span>{  <span class="hljs-type">char</span> *p;  p = (<span class="hljs-type">char</span>*)PGROUNDUP((uint64)pa_start);  <span class="hljs-keyword">for</span>(; p + PGSIZE &lt;= (<span class="hljs-type">char</span>*)pa_end; p += PGSIZE){    <span class="hljs-comment">// kfree() drops a reference, so give each boot-time page one</span>    <span class="hljs-comment">// reference before adding it to the free list.</span>    acquire(&amp;kref.lock);    kref.count[paindex((uint64)p)] = <span class="hljs-number">1</span>;    release(&amp;kref.lock);    kfree(p);  }}<span class="hljs-type">void</span><span class="hljs-title function_">kfree</span><span class="hljs-params">(<span class="hljs-type">void</span> *pa)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  <span class="hljs-keyword">if</span>(((uint64)pa % PGSIZE) != <span class="hljs-number">0</span> || (<span class="hljs-type">char</span>*)pa &lt; end || (uint64)pa &gt;= PHYSTOP)    panic(<span class="hljs-string">"kfree"</span>);  <span class="hljs-type">int</span> index = paindex((uint64)pa);  acquire(&amp;kref.lock);  <span class="hljs-keyword">if</span>(kref.count[index] &lt; <span class="hljs-number">1</span>)    panic(<span class="hljs-string">"kfree: no reference"</span>);  kref.count[index]--;  <span class="hljs-keyword">if</span>(kref.count[index] &gt; <span class="hljs-number">0</span>){    release(&amp;kref.lock);    <span class="hljs-keyword">return</span>;  }  release(&amp;kref.lock);  <span class="hljs-comment">// Fill with junk to catch dangling refs.</span>  <span class="hljs-built_in">memset</span>(pa, <span class="hljs-number">1</span>, PGSIZE);  r = (<span class="hljs-keyword">struct</span> run*)pa;  acquire(&amp;kmem.lock);  r-&gt;next = kmem.freelist;  kmem.freelist = r;  release(&amp;kmem.lock);}<span class="hljs-type">void</span><span class="hljs-title function_">krefinc</span><span class="hljs-params">(uint64 pa)</span>{  <span class="hljs-keyword">if</span>((pa % PGSIZE) != <span class="hljs-number">0</span> || (<span class="hljs-type">char</span>*)pa &lt; end || pa &gt;= PHYSTOP)    panic(<span class="hljs-string">"krefinc"</span>);  <span class="hljs-type">int</span> index = paindex(pa);  acquire(&amp;kref.lock);  <span class="hljs-keyword">if</span>(kref.count[index] &lt; <span class="hljs-number">1</span>)    panic(<span class="hljs-string">"krefinc: free page"</span>);  kref.count[index]++;  release(&amp;kref.lock);}</code></pre></div></details><h3 id="修改-copyout">修改 <code>copyout()</code></h3><p>首先解析一下 <code>copyout()</code> 是什么：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Copy from kernel to user.</span><span class="hljs-comment">// Copy len bytes from src to virtual address dstva in a given page table.</span><span class="hljs-comment">// Return 0 on success, -1 on error.</span><span class="hljs-type">int</span> <span class="hljs-title function_">copyout</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 dstva, <span class="hljs-type">char</span> *src, uint64 len)</span></code></pre></div><p>从这个函数签名可以看出，作用是把内核空间中的数据，安全地复制到某个进程的用户虚拟地址中。</p><p>目前的 <code>copyout()</code> 的逻辑，有关于 lazy allocation 的处理逻辑：</p><div class="code-wrapper"><pre><code class="hljs c">pa0 = walkaddr(pagetable, va0);<span class="hljs-keyword">if</span>(pa0 == <span class="hljs-number">0</span>) {  <span class="hljs-keyword">if</span>((pa0 = vmfault(pagetable, va0, <span class="hljs-number">0</span>)) == <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }}</code></pre></div><p>如果这个用户地址尚未映射，当前代码会尝试通过 <code>vmfault()</code> 分配页面。</p><p>但是目前还无法正确处理 COW 逻辑，因为 COW 页没有 <code>PTE_W</code>，但是实际上是可写的，只是要模拟一次写 pagefault，其实就是调用一次 <code>vmfault()</code>即可。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 47 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 47 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">copyout</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 dstva, <span class="hljs-type">char</span> *src, uint64 len)</span>{  uint64 n, va0, pa0;  <span class="hljs-type">pte_t</span> *pte;  <span class="hljs-type">int</span> needfault;  <span class="hljs-keyword">while</span>(len &gt; <span class="hljs-number">0</span>){    va0 = PGROUNDDOWN(dstva);    <span class="hljs-keyword">if</span>(va0 &gt;= MAXVA) {      <span class="hljs-keyword">goto</span> err;    }    needfault = <span class="hljs-number">0</span>;    pa0 = walkaddr(pagetable, va0);    <span class="hljs-keyword">if</span>(pa0 == <span class="hljs-number">0</span>){      needfault = <span class="hljs-number">1</span>;    } <span class="hljs-keyword">else</span> {      pte = walk(pagetable, va0, <span class="hljs-number">0</span>);      <span class="hljs-keyword">if</span>((*pte &amp; PTE_W) == <span class="hljs-number">0</span>){        <span class="hljs-keyword">if</span>((*pte &amp; PTE_COW) == <span class="hljs-number">0</span>) {          <span class="hljs-keyword">goto</span> err;        }        needfault = <span class="hljs-number">1</span>;      }    }<span class="hljs-comment">// 这里运用了布尔语句的短路特性</span>    <span class="hljs-keyword">if</span>(needfault &amp;&amp; (pa0 = vmfault(pagetable, va0, <span class="hljs-number">0</span>)) == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">goto</span> err;    }          n = PGSIZE - (dstva - va0);    <span class="hljs-keyword">if</span>(n &gt; len) {      n = len;    }    memmove((<span class="hljs-type">void</span> *)(pa0 + (dstva - va0)), src, n);    len -= n;    src += n;    dstva = va0 + PGSIZE;  }  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; err:  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div></details><p>通过所有测试：</p><img src="https://image.wendaining.top/image-20260809121444034.png" style="zoom:50%;">]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab4 Traps - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/07/30/xv6-lab4-traps/</link>
      <description>xv6 的第四个 lab，涉及到不少汇编源码和底层 risc-v 硬件交互的地方</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Thu, 30 Jul 2026 17:51:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<div class="note note-primary"><p><strong>做这个 lab 中的一些感触</strong>：最好还是选择 Fall 2020 / Fall 2021 版本的，别选 2025 的。因为中文互联网上关于这门课的资料集中于前两个版本，各种源码分析也是。</p></div><h2 id="阅读">阅读</h2><p>本来是看录课的，但是发现自己看不太进去，遂又转向读文档。</p><p>归纳一下比较重要的机制：</p><h3 id="mode">mode</h3><p>supervisor mode 相比 user mode 只有两个特殊权限：</p><ul><li>可以读写控制寄存器</li><li>可以使用 <code>PTE_U</code> 标志位为 0 的 PTE。当 <code>PTE_U</code> 标志位为1的时候，<strong>只有</strong>用户代码可以使用这个页表；如果这个标志位为 0，则只有 supervisor mode 可以使用这个页表</li></ul><p><strong>没有的权限/设定</strong>：</p><ul><li>不能读写任意物理地址，像普通的用户代码一样，也需要通过 page table 来访问内存</li><li>trap 机制不控制 kernel 查看寄存器的内容，只是保存</li></ul><h3 id="一些重要的寄存器">一些重要的寄存器</h3><ul><li>如SATP（Supervisor Address Translation and Protection）寄存器，它包含了指向page table的物理内存地址（详见4.3）。</li><li>如STVEC（Supervisor Trap Vector Base Address Register）寄存器，它指向了内核中处理trap的指令的起始地址。</li><li>SEPC（Supervisor Exception Program Counter）寄存器，在trap的过程中保存程序计数器的值。</li></ul><h3 id="ecall"><code>ecall</code></h3><p><code>ecall</code> 是 RISC-V 提供的一个系统指令，执行：</p><ul><li>将代码从 user mode 改到 supervisor mode</li><li>将程序计数器的值保存在了 SEPC 寄存器</li><li>跳转到 STVEC 寄存器指向的指令（所谓的跳转就是把 PC 指向这里）</li></ul><p>对于 xv6 而言，STVEC 就是指向 trampoline page，包含 trap 处理代码。</p><blockquote><p>ecall并不会切换page table，这是ecall指令的一个非常重要的特点。所以这意味着，trap处理代码必须存在于每一个user page table中。因为ecall并不会切换page table，我们需要在user page table中的某个地方来执行最初的内核代码。而这个trampoline page，是由内核小心的映射到每一个user page table中，以使得当我们仍然在使用user page table时，内核在一个地方能够执行trap机制的最开始的一些指令。</p></blockquote><p><strong><code>ecall</code> 执行的东西很少</strong>，这是 RISC 的体现。</p><h3 id="uservec"><code>uservec</code></h3><p>用于保存用户寄存器的汇编函数。</p><p>基本来说，就是每个进程都有一个 <code>trapframe</code>结构体，对应每个 user page table 有一个 trapframe page（在 syscall lab 中见过了），把所有寄存器定义了一遍。汇编的函数实现也就是一堆 <code>sd ra, 40(a0)</code> 的存储。</p><p>但是在这之前，还需要做一件事情：</p><ul><li>在进入到 user space 之前，内核会将 trapframe page 的地址 0x3fffffe000 保存在 SSCRATCH 这个寄存器中</li><li><code>uservec</code> 的第一行，执行 <code>csrrw a0, sscratch, a0</code>，意思是交换两个寄存器的值</li></ul><div class="note note-info"><p>但是这好像是 2020 年的版本，新版已经是 <code>csrw sscratch, a0</code> <code>li a0, TRAPFRAME</code> 了。</p><img src="https://image.wendaining.top/image-20260731010850344.png" alt="大概的区别" style="zoom:67%;"></div><div class="note note-primary"><p><strong>这里寄存器交换的意义（主要是 a0 的意义）</strong>：</p><p>a0 指向 trapframe，后续的汇编代码采用 a0 作为基址寄存器。</p></div><p>然后：</p><ol><li>将 kernel 的栈顶指针加载到寄存器 sp 寄存器中</li><li>将 kernel 的 hartid （CPU 核编号）加载到 tp 寄存器中</li><li>将 <code>usertrap()</code> 函数的地址加载到 t0 寄存器中（打印出来是<code>0x800027a0</code>，属于虚拟地址空间中的 kernel text 区域）</li><li>将 kernel page table 的指针加载到 t1 寄存器中</li><li>切换 page table，将 kernel page table 的指针加载到 satp 寄存器中</li><li>通过 jump 指令跳转到函数：<code>usertrap()</code> ，这是内核的C代码</li></ol><p>总结一下做了什么：</p><ol><li>保存用户寄存器数据</li><li>为内核代码设置好堆栈空间</li><li>切换页表</li></ol><h3 id="usertrap"><code>usertrap</code></h3><p>细节略了，太多了，根本记不住。</p><p>usertrap 负责判断<strong>触发 trap 的原因</strong>，并执行响应的处理：</p><ul><li>若是 syscall，则调用对应的系统调用</li><li>若是设备中断，则跳转到响应的处理代码</li><li>若是 page fault，则杀死进程</li><li>最后执行 <code>usertrapret()</code> 函数</li></ul><h3 id="一些修改">一些修改</h3><p>从后面开始的部分，2025 的版本和 2020 的不一样。</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 546 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">新版 xv6 已经没有 usertrapret() 了，但功能没有消失，只是被重命名并重新组织了。 官方在 2025 年 10 月将： user...</span></summary><blockquote><p><strong>新版 xv6 已经没有 <code>usertrapret()</code> 了，但功能没有消失，只是被重命名并重新组织了。</strong></p><p>官方在 <strong>2025 年 10 月</strong>将：</p><div class="code-wrapper"><pre><code class="hljs c">usertrapret()</code></pre></div><p>改成了：</p><div class="code-wrapper"><pre><code class="hljs c">prepare_return()</code></pre></div><p>(<a href="https://github.com/mit-pdos/xv6-riscv/commits?utm_source=chatgpt.com">GitHub</a>)</p><p>现在的返回路径是：</p><div class="code-wrapper"><pre><code class="hljs text">usertrap()    ├─ prepare_return()    └─ return 用户页表的 satp           ↓trampoline.S: userret    ├─ 切换到用户页表    ├─ 恢复用户寄存器    └─ sret 返回用户态</code></pre></div><p>其中 <code>prepare_return()</code> 负责旧版 <code>usertrapret()</code> 的大部分准备工作：</p><ul><li>设置 <code>stvec = uservec</code></li><li>填充 trapframe 中的内核信息</li><li>设置 <code>sstatus</code></li><li>设置 <code>sepc</code></li></ul><p>随后 <code>usertrap()</code> 返回，汇编代码直接进入 <code>userret</code>。当前源码中已经搜索不到 <code>usertrapret</code>。(<a href="https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/trap.c">GitHub</a>)</p><p>所以看 2020 课程时，可以直接这样对应：</p><div class="code-wrapper"><pre><code class="hljs text">旧版 usertrapret()≈ 新版 prepare_return() + usertrap 返回 satp + userret</code></pre></div><p>你上传的 2025 版 xv6 book 也已经改为讲解 <code>prepare_return()</code>。</p></blockquote></details><h2 id="RISC-V-assembly">RISC-V assembly</h2><p>一系列不是很难的题目，就直接把答案放出来了：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 TEXT · 33 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">TEXT · 33 行</span></summary><div class="code-wrapper"><pre><code class="hljs txt">Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?RISC-V 使用 a0～a7 传递前八个函数参数。在 main 对 printf 的调用中，数值 13 位于 a2 寄存器中。Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)没有调用 f 和 g 函数，编译器内联了 f(8) + 1 的表达式为 12，表现为汇编代码 li a1,12At what address is the function printf located?根据 30:6c6000ef          jal6f6 &lt;printf&gt; 这一行，位于 6f6What value is in the register ra just after the jalr to printf in main?根据 RISC-V 文档，`jal` 跳转到目标函数时，会把下一条指令的地址保存到 ra，作为目标函数执行完毕之后，跳转到的地址，所以是 0x30 + 4 = 0x34Run the following code.unsigned int i = 0x00646c72;printf("H%x Wo%s", 57616, (char *) &amp;i);      What is the output? Here's an ASCII table that maps bytes to characters.The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?输出是 HE110 World$。%x 把 57616 按 16 进制输出则得到 0xe110，然后对于 %s 由于 RISC-V 是小端序，则 i = 0x00646c72，转换成 4 字节无符号整数之后从小到大依次为 r l d 0。如果是大端序则设置 i = 0x726c6400Here's a description of little- and big-endian and a more whimsical description.In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?printf("x=%d y=%d", 3);读到的会是 a2 寄存器里面残存的值，属于 UB 行为。</code></pre></div></details><h2 id="Backtrace">Backtrace</h2><p>需要沿着内核栈中的栈指针向上遍历，打印当前函数调用链中的返回地址。</p><p>考虑这个核心原理：</p><div class="code-wrapper"><pre><code class="hljs text">当前 fp  fp - 8   → 当前栈帧保存的返回地址  fp - 16  → 调用者的 fp</code></pre></div><p>那么，这样不断循环就可以了。</p><p>hint 1：</p><blockquote><p>Add the prototype for your <code>backtrace()</code> to <code>kernel/defs.h</code> so that you can invoke <code>backtrace</code> in <code>sys_pause</code>.</p></blockquote><p>没什么好说的，照做。</p><p>hint 2：</p><blockquote><p>The GCC compiler stores the frame pointer of the currently executing function in the register <code>s0</code>. In the section marked by #ifndef <strong>ASSEMBLER</strong> ... #endif, add the following function to <code>kernel/riscv.h</code>:</p><div class="code-wrapper"><pre><code class="hljs stylus">static inline uint64<span class="hljs-function"><span class="hljs-title">r_fp</span><span class="hljs-params">()</span></span>{  uint64 <span class="hljs-attribute">x</span>;  asm <span class="hljs-built_in">volatile</span>(<span class="hljs-string">"mv %0, s0"</span> : <span class="hljs-string">"=r"</span> (<span class="hljs-attribute">x</span>) );  return <span class="hljs-attribute">x</span>;}</code></pre></div><p>and call this function in <code>backtrace</code> to read the current frame pointer. <code>r_fp()</code> uses <a href="https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html">in-line assembly</a> to read <code>s0</code>.</p></blockquote><p>这也没啥好说的。照做。</p><p>hint 3：</p><p>就是所说的那个核心原理。</p><p>具体到代码上的处理：</p><div class="code-wrapper"><pre><code class="hljs c">uint64 ra = *(uint64 *)(fp - <span class="hljs-number">8</span>);fp = *(uint64 *)(fp - <span class="hljs-number">16</span>);</code></pre></div><p>hint 4：</p><blockquote><p>Your <code>backtrace()</code> will need a way to recognize that it has seen the last stack frame, and should stop. A useful fact is that the memory allocated for each kernel stack consists of a single page-aligned page, so that all the stack frames for a given stack are on the same page. You can use <code>PGROUNDDOWN(fp)</code> (see <code>kernel/riscv.h</code>) to identify the page that a frame pointer refers to.</p></blockquote><p>意思就是，我们是需要循环的，而循环的终止条件就是 <code>fp</code> 仍然有效，而 <code>fp</code> 的有效性就是看页面是否有效。</p><p>首先取得当前 <code>fp</code> 所在页面的起始地址：</p><div class="code-wrapper"><pre><code class="hljs c">uint64 stack_bottom = PGROUNDDOWN(fp);</code></pre></div><p>页面结束地址：</p><div class="code-wrapper"><pre><code class="hljs c">uint64 stack_top = stack_bottom + PGSIZE;</code></pre></div><p>只要 <code>fp</code> 还在这个页面内，就可以继续遍历：</p><div class="code-wrapper"><pre><code class="hljs c">fp &gt;= stack_bottom + <span class="hljs-number">16</span> &amp;&amp; fp &lt; stack_top</code></pre></div><p>别的没什么好说的，照做即可。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 20 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 20 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">backtrace</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"backtrace:\n"</span>);  uint64 fp = r_fp();  <span class="hljs-comment">// 当前内核栈所在页面的范围</span>  uint64 stack_bottom = PGROUNDDOWN(fp);  uint64 stack_top = stack_bottom + PGSIZE;  <span class="hljs-keyword">while</span>(fp &gt;= stack_bottom + <span class="hljs-number">16</span> &amp;&amp; fp &lt; stack_top){    <span class="hljs-comment">// 当前函数返回到调用者后的地址</span>    uint64 ra = *(uint64 *)(fp - <span class="hljs-number">8</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%p\n"</span>, (<span class="hljs-type">void</span>*)ra);    <span class="hljs-comment">// 切换到调用者的栈帧</span>    fp = *(uint64 *)(fp - <span class="hljs-number">16</span>);  }}</code></pre></div></details><h2 id="Alarm">Alarm</h2><p>按照 hint 照做。</p><p>hint 1 2 3 没什么好说的，唯一值得注意的是，<code>sys_sigalarm</code> 和 <code>sys_sigreturn</code> 函数签名我放在了 <code>sysproc.c</code> 中。</p><p>hint 4 5，<code>proc.h</code> 这里记录地址，是直接写 <code>uint64</code> 的，后面强转成指针即可。</p><p>hint 6 7，本质是一个点，其实只看 hint7 的指示就可以了。</p><p>hint 8 9，照着说明去做。</p><ul><li>hint 8 的意思是，判断 alarm 是否有效，应该使用 <code>p-&gt;alarm_interval &gt; 0</code></li><li>hint 9 的意思是，alarm 到期时，执行 <code>p-&gt;trapframe-&gt;epc = p-&gt;alarm_handler;</code> 以改变跳回之后的程序计数器。</li></ul><p>hint 10 之后的内容，是这题的重头戏。</p><p>hint 11 问，应该保存哪些寄存器？并且提示有很多。答案是应该保存所有的用户寄存器。因为执行 handler 是可能改变很多寄存器的内容的。最简单的方案，是保存整个 <code>trapframe</code>。</p><p>因而，hint 12 就是解决这一点的。在 <code>struct proc</code> 中加一个 <code>struct trapframe alarm_trapframe</code>：</p><ul><li><code>p-&gt;trapframe</code>：当前正在使用的用户现场，会随着 handler 和系统调用变化</li><li><code>p-&gt;alarm_trapframe</code>：alarm 发生前原程序现场的固定快照</li></ul><p>解决方案，就是：</p><div class="code-wrapper"><pre><code class="hljs c">memmove(&amp;p-&gt;alarm_trapframe, p-&gt;trapframe, <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> trapframe));p-&gt;trapframe-&gt;epc = p-&gt;alarm_handler;</code></pre></div><div class="note note-success"><p><code>memmove()</code> 是后面的参数复制到前面。</p></div><p>hint 13，主要是解决防止 handler 重入的问题。具体而言：handler 运行期间，显然可能重新发生定时器中断，若再次触发 alarm，则原来的 handler 递归进入自己，导致 <code>alarm_trapframe</code> 被执行 handler 过程中的 trapframe 覆盖，<code>sigreturn()</code> 永远无法找到被中断的源程序。</p><p>解决方案是加一个 flag <code>int alarm_handling;</code>，在 <code>allocproc</code> 中初始化为 0，只有满足 <code>p-&gt;alarm_interval &gt; 0 &amp;&amp; !p-&gt;alarm_handling</code> 时才能累计触发 alarm，并且触发时置 <code>p-&gt;alarm_ticks = 0;</code> 和 <code>p-&gt;alarm_handling = 1;</code>。</p><p>最后是 hint14，<code>sys_sigreturn()</code> 将备份恢复到当前 trapframe，也就是：</p><div class="code-wrapper"><pre><code class="hljs c">memmove(p-&gt;trapframe,        &amp;p-&gt;alarm_trapframe,        <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> trapframe));</code></pre></div><div class="note note-info"><p><code>sigreturn()</code> 是 handler 的最后一步，用来告诉内核 handler 执行结束了，请把我送回原来的程序。</p></div><p>但是 <code>a0</code> 特殊，因为 <code>a0</code> 被认为是返回值。而阅读 <code>syscall()</code> 的代码，会发现执行系统调用的方式是：</p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;trapframe-&gt;a0 = syscalls[num]();</code></pre></div><p>即，必须要保存好原始的 <code>a0</code> 并作为 <code>sigreturn()</code> 的返回值。</p><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_sigreturn</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  uint64 old_a0 = p-&gt;alarm_trapframe.a0;  memmove(p-&gt;trapframe, &amp;p-&gt;alarm_trapframe, <span class="hljs-keyword">sizeof</span>(<span class="hljs-keyword">struct</span> trapframe));  p-&gt;alarm_handling = <span class="hljs-number">0</span>;  <span class="hljs-keyword">return</span> old_a0;}</code></pre></div><p>运行 <code>make grade</code>：</p><img src="https://image.wendaining.top/image-20260808105800805.png" style="zoom:33%;">]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab3 Page tables - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/07/26/xv6-lab3-page-tables/</link>
      <description>xv6 的第三个 lab，对整个页表机制进行学习。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Sun, 26 Jul 2026 23:09:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读-xv6-book">阅读 xv6 book</h2><p>找到了两个很好的中文翻译的版本：</p><ul><li><a href="https://xv6.dgs.zone/">课程介绍 · 6.S081 All-In-One</a></li><li><a href="https://github.com/HelloYJohn/xv6-riscv-book-zh-cn">HelloYJohn/xv6-riscv-book-zh-cn: translation of xv6-riscv-book</a></li></ul><p>那么就试着速览一下第三章吧。</p><ul><li>我发现我还是读不进去书，不如直接开始做 lab 然后学吧。</li></ul><h2 id="Inspect-a-user-process-page-table">Inspect a user-process page table</h2><p>不需要写代码，主要是 inspect 一下。</p><p>运行 <code>pgtbltest</code>：</p><img src="https://image.wendaining.top/image-20260727140333426.png" style="zoom:50%;"><p>打印出的是此进程的 first 10 and last 10 PTE。</p><p>分析一下，首先根据 xv6 book，一个 PTE 的结构如图：</p><img src="https://image.wendaining.top/image-20260727151216229.png" style="zoom:50%;"><p>则：</p><ul><li><code>va</code>：进程看到的虚拟页地址</li><li><code>pte</code>：完整页表项</li><li><code>pa</code>：它映射到的物理页地址</li><li><code>perm</code>：PTE 低 10 位的标志位</li></ul><p>因为一页是 4096 Bytes 即 0x1000 Bytes，故可以见到 va 分别为 <code>0x0 0x1000 0x2000...</code>。</p><p>逐个解析每个 PTE 的 perm：</p><table><thead><tr><th>虚拟地址</th><th><code>perm</code></th><th>解释</th></tr></thead><tbody><tr><td><code>0x0</code></td><td><code>0x5B</code></td><td><code>V R X U A</code></td></tr><tr><td><code>0x1000</code></td><td><code>0x5B</code></td><td><code>V R X U A</code></td></tr><tr><td><code>0x2000</code></td><td><code>0x17</code></td><td><code>V R W U</code></td></tr><tr><td><code>0x3000</code></td><td><code>0x07</code></td><td><code>V R W</code>，但没有 <code>U</code></td></tr><tr><td><code>0x4000</code></td><td><code>0xD7</code></td><td><code>V R W U A D</code></td></tr><tr><td><code>0x5000</code>～<code>0x9000</code></td><td><code>0</code></td><td>不存在有效映射</td></tr><tr><td><code>0x3FFFFF6000</code>～<code>0x3FFFFFD000</code></td><td><code>0</code></td><td>不存在有效映射</td></tr><tr><td><code>0x3FFFFFE000</code></td><td><code>0xC7</code></td><td><code>V R W A D</code>，没有 <code>U</code></td></tr><tr><td><code>0x3FFFFFF000</code></td><td><code>0x4B</code></td><td><code>V R X A</code>，没有 <code>U</code></td></tr></tbody></table><p>再考虑图 3.4：</p><img src="https://image.wendaining.top/image-20260727152642862.png" style="zoom:50%;"><p>从低位开始分析 <code>va 0x0</code>、<code>va 0x1000</code> 都是 <code>perm 0x5B = R-XU</code>，即：可读、可执行、用户可访问，不可写。那么显然，是 <code>text</code> 部分。</p><p>再分析 <code>va 0x2000</code>，<code>perm 0x17 = R-WU</code> 对应可读、可写、用户可访问，不可执行，对应 <code>data</code> <code>R-WU</code>。</p><p>之后分析也同理，对着看就行。而且很显然，最高的两页分别就是 trampoline 和 trapframe。</p><h2 id="Speed-up-system-calls">Speed up system calls</h2><p>这道题虽然说是 easy，但是我还是有点「面向答案学习」了（指不会做，问 Agent 然后看答案的）。</p><p>梳理一下需求：<code>kernel/memlayout.h</code> 里面定义了一个这样的虚拟地址（va）：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> USYSCALL (TRAPFRAME - PGSIZE)</span></code></pre></div><p>需求是：在这个页面处定义一个这样的 struct：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">usyscall</span> {</span>  <span class="hljs-type">int</span> pid;  <span class="hljs-comment">// Process ID</span>};</code></pre></div><p>将其初始化为当前进程的 PID，以此消除了调用 <code>getpid()</code> 系统调用（陷入内核态）的必要，用户态就可直接从内存中读取 pid。</p><p>hint 提示我们：</p><blockquote><ul><li>Choose permission bits that allow userspace to only read the page.选择允许用户空间仅读取该页面的权限位。</li><li>There are a few things that need to be done over the lifecycle of a new page. For inspiration, understand the trapframe handling in <code>kernel/proc.c</code>.在新页面的生命周期中，有几件事情需要处理。作为参考，请理解 <code>kernel/proc.c</code> 中的 trapframe 处理。</li></ul></blockquote><p>简单梳理一下的意思就是，参考图 3.4，我们有一个 trapframe 的 va，现在我们在 trapframe 的<strong>正下方</strong>，又加了一个 usyscall。对于 usyscall 的实现，可以完全参考在 <code>kernel/proc.c</code> 中对于 trapframe 的处理。</p><p>那么，首先需要在 <code>kernel/proc.h</code> ，参考 <code>trapframe</code> 给 <code>struct proc</code> 加一个 <code>struct usyscall *usyscall</code>：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">trapframe</span> *<span class="hljs-title">trapframe</span>;</span> <span class="hljs-comment">// data page for trampoline.S</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">usyscall</span> *<span class="hljs-title">usyscall</span>;</span>   <span class="hljs-comment">// USYSCALL page, shared read-only with user</span></code></pre></div><p>然后，在 <code>kernel/proc.c</code> 中，Ctrl + F 搜索 trapframe，然后依次参考 trapframe 的实现，来仿照着实现 usyscall 的部分。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// static struct proc* allocproc(void)</span> <span class="hljs-comment">// Allocate a trapframe page.</span>  <span class="hljs-keyword">if</span>((p-&gt;trapframe = (<span class="hljs-keyword">struct</span> trapframe *)kalloc()) == <span class="hljs-number">0</span>){    freeproc(p);    release(&amp;p-&gt;lock);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">if</span>((p-&gt;usyscall = (<span class="hljs-keyword">struct</span> usyscall *)kalloc()) == <span class="hljs-number">0</span>) {    freeproc(p);    release(&amp;p-&gt;lock);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  p-&gt;usyscall-&gt;pid = p-&gt;pid;</code></pre></div><p>因为这里实际上是一个每进程且进程创建之后就不再改变的量，所以 <code>pid</code> 的逻辑在这里就已经实现完毕了。</p><p>接下来是：这个是纯粹模仿。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">freeproc</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> proc *p)</span>{  <span class="hljs-keyword">if</span>(p-&gt;trapframe)    kfree((<span class="hljs-type">void</span>*)p-&gt;trapframe);  p-&gt;trapframe = <span class="hljs-number">0</span>;  <span class="hljs-keyword">if</span>(p-&gt;usyscall)    kfree((<span class="hljs-type">void</span> *)p-&gt;usyscall);  p-&gt;usyscall = <span class="hljs-number">0</span>;  ...}</code></pre></div><p>这里是一个比较关键的地方：创建页表。</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 41 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 41 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Create a user page table for a given process, with no user memory,</span><span class="hljs-comment">// but with trampoline and trapframe pages.</span><span class="hljs-type">pagetable_t</span><span class="hljs-title function_">proc_pagetable</span><span class="hljs-params">(<span class="hljs-keyword">struct</span> proc *p)</span>{  <span class="hljs-type">pagetable_t</span> pagetable;  <span class="hljs-comment">// An empty page table.</span>  pagetable = uvmcreate();  <span class="hljs-keyword">if</span>(pagetable == <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  <span class="hljs-comment">// map the trampoline code (for system call return)</span>  <span class="hljs-comment">// at the highest user virtual address.</span>  <span class="hljs-comment">// only the supervisor uses it, on the way</span>  <span class="hljs-comment">// to/from user space, so not PTE_U.</span>  <span class="hljs-keyword">if</span>(mappages(pagetable, TRAMPOLINE, PGSIZE,              (uint64)trampoline, PTE_R | PTE_X) &lt; <span class="hljs-number">0</span>){    uvmfree(pagetable, <span class="hljs-number">0</span>);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-comment">// map the trapframe page just below the trampoline page, for</span>  <span class="hljs-comment">// trampoline.S.</span>  <span class="hljs-keyword">if</span>(mappages(pagetable, TRAPFRAME, PGSIZE,              (uint64)(p-&gt;trapframe), PTE_R | PTE_W) &lt; <span class="hljs-number">0</span>){    uvmunmap(pagetable, TRAMPOLINE, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);    uvmfree(pagetable, <span class="hljs-number">0</span>);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">if</span>(mappages(pagetable, USYSCALL, PGSIZE,              (uint64)(p-&gt;usyscall), PTE_R | PTE_U) &lt; <span class="hljs-number">0</span>){    uvmunmap(pagetable, TRAPFRAME, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);    uvmunmap(pagetable, TRAMPOLINE, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>); <span class="hljs-comment">// !!!</span>    uvmfree(pagetable, <span class="hljs-number">0</span>);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">return</span> pagetable;}</code></pre></div></details><p><code>mappages()</code> 是建立 va 到 实际物理地址的映射的，顺便规定好 perm，这里按照原文的说法，允许用户态读。</p><p>重点是这个清理步骤：</p><div class="code-wrapper"><pre><code class="hljs c">uvmunmap(pagetable, TRAPFRAME, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);uvmunmap(pagetable, TRAMPOLINE, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>); <span class="hljs-comment">// !!!</span>uvmfree(pagetable, <span class="hljs-number">0</span>);</code></pre></div><p>实际上的意思是 (summarized by LLM)：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 654 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">先看你的 bug 你的 proc_pagetable 中 USYSCALL 的错误处理（第 214-219 行）： if(mappages(pa...</span></summary><blockquote><p>先看你的 bug</p><p>你的 <code>proc_pagetable</code> 中 USYSCALL 的错误处理（第 214-219 行）：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(mappages(pagetable, USYSCALL, PGSIZE,            (uint64)(p-&gt;usyscall), PTE_R | PTE_U) &lt; <span class="hljs-number">0</span>){    uvmunmap(pagetable, TRAPFRAME, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);   <span class="hljs-comment">// 只 unmap 了 TRAPFRAME</span>    uvmfree(pagetable, <span class="hljs-number">0</span>);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div><p><strong>漏了 <code>uvmunmap(pagetable, TRAMPOLINE, 1, 0)</code>。</strong> 对比一下 TRAPFRAME 失败时的错误处理（第 207-211 行）——它 unmap 了 TRAMPOLINE，你这里也应该 unmap TRAMPOLINE + TRAPFRAME 两个。</p><p><code>proc_pagetable</code> 里的错误回滚</p><p>想象你在搭积木，<strong>从顶部往下搭</strong>（因为 TRAMPOLINE 在最顶上）：</p><div class="code-wrapper"><pre><code class="hljs nginx"><span class="hljs-attribute">TRAMPOLINE</span>  ✓ 已搭好TRAPFRAME   ✓ 已搭好USYSCALL    ✗ 搭这个的时候失败了！</code></pre></div><p>此时必须把<strong>成功搭上去的</strong>全拆掉，而且是<strong>逆序</strong>拆——你不能留一个残废的页表给 <code>freeproc</code> 用。<code>freeproc</code> 会假设页表结构是完好无损的。这就是为什么：</p><ul><li>TRAPFRAME 失败 → 回滚 TRAMPOLINE（只成功了一个）</li><li>USYSCALL 失败 → 回滚 TRAMPOLINE <strong>和</strong> TRAPFRAME（成功了两个）</li></ul></blockquote></details><p>之后，检查到 <code>kfork</code> 中对于 <code>trapframe</code> 的处理：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// copy saved user registers.</span>*(np-&gt;trapframe) = *(p-&gt;trapframe);<span class="hljs-comment">// Cause fork to return 0 in the child.</span>np-&gt;trapframe-&gt;a0 = <span class="hljs-number">0</span>;</code></pre></div><p>我们容易注意到，事实上 trapframe 的逻辑，是在这里实现的（继承父进程的）。但是，对于 <code>usyscall</code> ，因为 PID 进程创建之后即保持不变，所以其实不需要管这个。</p><p>至此，本题实现完成。</p><div class="note note-info"><p>话说回来，trapframe 的逻辑是在哪里实现的呢？</p></div><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 2081 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">trapframe 和 C 语言函数调用栈帧（call stack frame）是完全不同的两件事。这里的 "frame" 是指 "一份保存下来...</span></summary><blockquote><p>trapframe 和 C 语言函数调用栈帧（call stack frame）是<strong>完全不同的两件事</strong>。这里的 "frame" 是指 "一份保存下来的 CPU 状态快照"。</p><p>它的完整逻辑分布在<strong>三个文件</strong>中：</p><hr><ol><li>数据结构定义 — <code>kernel/proc.h</code> 第 43-80 行</li></ol><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">trapframe</span> {</span>  <span class="hljs-comment">/*   0 */</span> uint64 kernel_satp;   <span class="hljs-comment">// kernel page table</span>  <span class="hljs-comment">/*   8 */</span> uint64 kernel_sp;     <span class="hljs-comment">// top of process's kernel stack</span>  <span class="hljs-comment">/*  16 */</span> uint64 kernel_trap;   <span class="hljs-comment">// usertrap()</span>  <span class="hljs-comment">/*  24 */</span> uint64 epc;           <span class="hljs-comment">// saved user program counter</span>  <span class="hljs-comment">/*  32 */</span> uint64 kernel_hartid; <span class="hljs-comment">// saved kernel tp</span>  <span class="hljs-comment">/*  40 */</span> uint64 ra;  <span class="hljs-comment">/*  48 */</span> uint64 sp;  <span class="hljs-comment">/*  56 */</span> uint64 gp;  ...  <span class="hljs-comment">/* 280 */</span> uint64 t6;};</code></pre></div><p>注意每个字段旁边的注释数字——那是<strong>字节偏移量</strong>。这就是为什么 <code>trampoline.S</code> 里会出现 <code>sd ra, 40(a0)</code> 这种写法：<code>ra</code> 寄存器在 trapframe 结构体中偏移量就是 40。</p><hr><ol start="2"><li>汇编层：寄存器的物理保存/恢复 — <code>kernel/trampoline.S</code></li></ol><p>这是整个机制的核心，分两段：</p><p><strong>进入内核时（<code>uservec</code>，第 22 行）：</strong></p><div class="code-wrapper"><pre><code class="hljs scss">用户态发生 trap  → 硬件切换到内核态，跳转到 uservec  → 把所有用户寄存器 sd (store) 到 TRAPFRAME 地址处     sd ra, <span class="hljs-number">40</span>(a0)    <span class="hljs-comment">// 保存到 trapframe-&gt;ra (偏移40)</span>     sd sp, <span class="hljs-number">48</span>(a0)    <span class="hljs-comment">// 保存到 trapframe-&gt;sp (偏移48)</span>     ...  → 从 trapframe 加载内核上下文     ld sp, <span class="hljs-number">8</span>(a0)     <span class="hljs-comment">// 加载 kernel stack</span>     ld t0, <span class="hljs-number">16</span>(a0)    <span class="hljs-comment">// 加载 usertrap() 地址</span>     ld t1, <span class="hljs-number">0</span>(a0)     <span class="hljs-comment">// 加载 kernel page table</span>  → 切换页表  → jalr t0           <span class="hljs-comment">// 跳转到 C 代码 usertrap()</span></code></pre></div><p><strong>返回用户态时（<code>userret</code>，第 101 行）：</strong></p><div class="code-wrapper"><pre><code class="hljs scss">C 代码 <span class="hljs-built_in">usertrap</span>() 返回后  → 切换回用户页表  → 从 TRAPFRAME 恢复所有用户寄存器     ld ra, <span class="hljs-number">40</span>(a0)     ld sp, <span class="hljs-number">48</span>(a0)     ...  → sret             <span class="hljs-comment">// 硬件切回用户态</span></code></pre></div><hr><ol start="3"><li>C 层：填入内核侧信息 — <code>kernel/trap.c</code></li></ol><p><strong>进入时（<code>usertrap</code>，第 38 行）：</strong></p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;trapframe-&gt;epc = r_sepc();   <span class="hljs-comment">// 保存用户被中断时的 PC</span><span class="hljs-comment">// ... 处理系统调用、中断、缺页异常 ...</span></code></pre></div><p><strong>返回前（<code>prepare_return</code>，第 100 行）：</strong></p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;trapframe-&gt;kernel_satp = r_satp();         <span class="hljs-comment">// 内核页表地址</span>p-&gt;trapframe-&gt;kernel_sp = p-&gt;kstack + PGSIZE; <span class="hljs-comment">// 内核栈顶</span>p-&gt;trapframe-&gt;kernel_trap = (uint64)usertrap; <span class="hljs-comment">// 下次 trap 跳到哪</span>p-&gt;trapframe-&gt;kernel_hartid = r_tp();         <span class="hljs-comment">// CPU 编号</span></code></pre></div><p>这样下次进程再 trap 时，<code>trampoline.S</code> 里的 <code>uservec</code> 就能从 trapframe 里读取到正确的内核栈、内核页表、和 C 处理函数的地址。</p><hr><p>完整数据流</p><div class="code-wrapper"><pre><code class="hljs stylus">用户态运行中    │    ▼ (trap: 系统调用/中断/异常)uservec (trampoline.S)    │  保存用户寄存器 ──→ trapframe<span class="hljs-selector-class">.ra</span>, <span class="hljs-selector-class">.sp</span>, <span class="hljs-selector-class">.a0</span>...    │  加载内核上下文 ←── trapframe<span class="hljs-selector-class">.kernel_sp</span>, <span class="hljs-selector-class">.kernel_satp</span>, <span class="hljs-selector-class">.kernel_trap</span>    ▼<span class="hljs-function"><span class="hljs-title">usertrap</span><span class="hljs-params">()</span></span> (trap.c)    │  处理 trap 逻辑    │<span class="hljs-function"><span class="hljs-title">prepare_return</span><span class="hljs-params">()</span></span> (trap.c)    │  写入内核上下文 ──→ trapframe<span class="hljs-selector-class">.kernel_sp</span>, <span class="hljs-selector-class">.kernel_satp</span>...    │userret (trampoline.S)    │  恢复用户寄存器 ←── trapframe<span class="hljs-selector-class">.ra</span>, <span class="hljs-selector-class">.sp</span>, <span class="hljs-selector-class">.a0</span>...    ▼用户态继续运行</code></pre></div><p>所以 trapframe 本质上是一个<strong>交换区</strong>：</p><ul><li>进内核时：用户寄存器卸货到这里，内核上下文从这里装货</li><li>出内核时：内核上下文卸货到这里，用户寄存器从这里装货</li></ul><p>它和 C 语言的函数调用栈（那个是编译器自动生成的 prologue/epilogue，靠 <code>sp</code> 和 <code>fp</code> 操作）完全不是一回事。这里的每一个 <code>sd</code>/<code>ld</code> 都是手工写的汇编。</p></blockquote></details><h2 id="Print-a-page-table">Print a page table</h2><p>虽然说是 easy，但是我一边拷打 AI 一边自己想，写了差不多有 5 个多小时...</p><p>大概归纳一下吧，首先，我发现我对多级页表不熟，然后简单手写整理了一下：</p><img src="https://image.wendaining.top/3c37e1f880d2b030accf9e4241addadd.jpg" alt="字好丑..." style="zoom:50%;"><p>hint 里面说：参考 <code>freewalk</code> 函数：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 20 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 20 行</span></summary><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-comment">// Recursively free page-table pages.</span><span class="hljs-comment">// All leaf mappings must already have been removed.</span><span class="hljs-type">void</span><span class="hljs-title function_">freewalk</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable)</span>{  <span class="hljs-comment">// there are 2^9 = 512 PTEs in a page table.</span>  <span class="hljs-keyword">for</span>(<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">512</span>; i++){    <span class="hljs-type">pte_t</span> pte = pagetable[i];    <span class="hljs-keyword">if</span>((pte &amp; PTE_V) &amp;&amp; (pte &amp; (PTE_R|PTE_W|PTE_X)) == <span class="hljs-number">0</span>){      <span class="hljs-comment">// this PTE points to a lower-level page table.</span>      uint64 child = PTE2PA(pte);      freewalk((<span class="hljs-type">pagetable_t</span>)child);      pagetable[i] = <span class="hljs-number">0</span>;    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(pte &amp; PTE_V){      <span class="hljs-comment">// backtrace();</span>      panic(<span class="hljs-string">"freewalk: leaf"</span>);    }  }  kfree((<span class="hljs-type">void</span>*)pagetable);}</code></pre></div></details><p>大致模仿其框架就可以实现了。</p><p>有一个疑惑点：为什么判定一个 PTE 是一个指向下一层page table的 PTE 的条件是：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// 此条件等于该pte不是终末层, 而是指向下一层</span>(pte &amp; (PTE_R|PTE_W|PTE_X)) == <span class="hljs-number">0</span></code></pre></div><p>其实是 RISC-V 的规定，指向下一层页表的 PTE（非叶子 PTE），其 R、W、X 位必须全为 0。只有指向最终数据页的 PTE（叶子 PTE），才允许设置 R/W/X。</p><p>分析源码也可得到印证：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta"># kernel/vm.c mappages()</span><span class="hljs-keyword">if</span>(*pte &amp; PTE_V)  panic(<span class="hljs-string">"mappages: remap"</span>);*pte = PA2PTE(pa) | perm | PTE_V;</code></pre></div><p>这里传入的 perm，保证了一定包含 R W X 其一，否则 panic。</p><p>然后开始实现 <code>vmprint</code>，因为参数锁死了，所以需要设置一个辅助函数来实现递归遍历：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 26 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 26 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">vmprint_recur</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, <span class="hljs-type">int</span> level, uint64 va)</span>{  <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">512</span>; ++i) {    <span class="hljs-type">pte_t</span> pte = pagetable[i];    <span class="hljs-keyword">if</span> (pte &amp; PTE_V) { <span class="hljs-comment">// is a valid PTE</span>      <span class="hljs-comment">// 这里记得强转 i</span>      uint64 newva = va | ((uint64)i &lt;&lt; PXSHIFT(level));      <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = <span class="hljs-number">0</span>; j &lt;= <span class="hljs-number">2</span> - level; j++) {        <span class="hljs-built_in">printf</span>(<span class="hljs-string">" .."</span>);      }      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%p: pte %p pa %p\n"</span>, (<span class="hljs-type">void</span> *)newva, (<span class="hljs-type">void</span> *)pte, (<span class="hljs-type">void</span> *)PTE2PA(pte));      <span class="hljs-keyword">if</span> ((pte &amp; (PTE_R|PTE_W|PTE_X)) == <span class="hljs-number">0</span>) {        vmprint_recur((<span class="hljs-type">pagetable_t</span>)PTE2PA(pte), level - <span class="hljs-number">1</span>, newva);      }    }  }}<span class="hljs-type">void</span><span class="hljs-title function_">vmprint</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable)</span>{  <span class="hljs-built_in">printf</span>(<span class="hljs-string">"page table %p\n"</span>, pagetable);  <span class="hljs-comment">// ！！从 2 开始 而不是从 1 开始！！</span>  vmprint_recur(pagetable, <span class="hljs-number">2</span>, <span class="hljs-number">0</span>);}</code></pre></div></details><p>别的都还好，主要是这个<strong>计算 <code>newva</code></strong>，着重说明一下：</p><p><strong><code>pagetable[i]</code> 里的 <code>i</code> 和 VA 是线性对应的</strong>，我开始以为，像 <code>for(int i=0;i&lt;512;i++) pagetable[i]</code> 这样的遍历得到的是毫无逻辑杂乱无章的，但是实际上，考虑多级页表的原理，这个 <code>i</code> 就是对应虚拟地址 va 的第 <code>i</code> 段。更具体地说：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 1455 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">真正的原理是：每个页表项代表一段虚拟地址范围。newva 是在计算当前 PTE 所代表范围的起始虚拟地址。 先不要想位运算，先想“切分地址空间”...</span></summary><blockquote><p>真正的原理是：每个页表项代表一段虚拟地址范围。<code>newva</code> 是在计算当前 PTE 所代表范围的起始虚拟地址。</p><p>先不要想位运算，先想“切分地址空间”</p><p>每张页表有 512 个 PTE，因此它会把当前负责的地址范围平均切成 512 份。</p><p><code>level = 2</code>时：</p><p>根页表管理整个 Sv39 地址空间。</p><p>它的每个 PTE 负责 <code>1 GiB</code>（$\frac{2^{39}}{2^9}=2^{30}bit=1GiB$）</p><p>所以根页表：</p><div class="code-wrapper"><pre><code class="hljs text">pagetable[0] → 从 0 GiB 开始pagetable[1] → 从 1 GiB 开始pagetable[2] → 从 2 GiB 开始...</code></pre></div><p>假设当前：</p><div class="code-wrapper"><pre><code class="hljs c">i = <span class="hljs-number">3</span>;</code></pre></div><p>那么第 3 项所代表范围的起点就是：</p><div class="code-wrapper"><pre><code class="hljs text">3 × 1 GiB</code></pre></div><p>即：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-number">3</span> &lt;&lt; <span class="hljs-number">30</span></code></pre></div><p>因为：</p><div class="code-wrapper"><pre><code class="hljs text">1 GiB = 2^30</code></pre></div><hr><p><code>level = 1</code></p><p>进入下一张页表后，它把刚才的 <code>1 GiB</code> 再切成 512 份。</p><p>每项负责：</p><div class="code-wrapper"><pre><code class="hljs text">1 GiB / 512 = 2 MiB</code></pre></div><p>所以此时第 <code>i</code> 项产生的偏移是：</p><div class="code-wrapper"><pre><code class="hljs text">i × 2 MiB</code></pre></div><p>而：</p><div class="code-wrapper"><pre><code class="hljs text">2 MiB = 2^21</code></pre></div><p>因此：</p><div class="code-wrapper"><pre><code class="hljs c">i &lt;&lt; <span class="hljs-number">21</span></code></pre></div><hr><p><code>level = 0</code></p><p>最后一级页表又把 <code>2 MiB</code> 切成 512 份。</p><p>每项负责：</p><div class="code-wrapper"><pre><code class="hljs text">2 MiB / 512 = 4 KiB</code></pre></div><p>因此第 <code>i</code> 项产生的偏移：</p><div class="code-wrapper"><pre><code class="hljs text">i × 4 KiB</code></pre></div><p>而：</p><div class="code-wrapper"><pre><code class="hljs text">4 KiB = 2^12</code></pre></div><p>所以：</p><div class="code-wrapper"><pre><code class="hljs c">i &lt;&lt; <span class="hljs-number">12</span></code></pre></div><hr><p><code>PXSHIFT(level)</code> 的根本含义</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> PXSHIFT(level) (12 + 9 * level)</span></code></pre></div><p>它计算的其实是：<strong>当前层一个 PTE 所代表区域的大小是 <code>2^PXSHIFT(level)</code> 字节。</strong></p><p>因为：</p><div class="code-wrapper"><pre><code class="hljs text">level 0：一个 PTE 管 4 KiB  = 2^12level 1：一个 PTE 管 2 MiB  = 2^21level 2：一个 PTE 管 1 GiB  = 2^30</code></pre></div><p>所以：</p><div class="code-wrapper"><pre><code class="hljs c">(uint64)i &lt;&lt; PXSHIFT(level)</code></pre></div><p>等价于：</p><div class="code-wrapper"><pre><code class="hljs c">i * 当前层每个 PTE 负责的字节数</code></pre></div><hr><p>完整例子</p><p>假设遍历路径是：</p><div class="code-wrapper"><pre><code class="hljs text">根页表[1]  → 中间页表[3]    → 最低页表[5]</code></pre></div><p>初始：</p><div class="code-wrapper"><pre><code class="hljs c">va = <span class="hljs-number">0</span>;</code></pre></div><p>根页表第 1 项</p><p>每项管 <code>1 GiB</code>：</p><div class="code-wrapper"><pre><code class="hljs c">newva = <span class="hljs-number">0</span> + <span class="hljs-number">1</span> * <span class="hljs-number">1</span> GiB;      = <span class="hljs-number">0x40000000</span>;</code></pre></div><p>现在已经确定：地址位于第一个 <code>1 GiB</code> 区域。</p><p>下一层第 3 项</p><p>每项管 <code>2 MiB</code>：</p><div class="code-wrapper"><pre><code class="hljs c">newva = <span class="hljs-number">0x40000000</span> + <span class="hljs-number">3</span> * <span class="hljs-number">2</span> MiB;      = <span class="hljs-number">0x40000000</span> + <span class="hljs-number">0x600000</span>;      = <span class="hljs-number">0x40600000</span>;</code></pre></div><p>最后一层第 5 项</p><p>每项管 <code>4 KiB</code>：</p><div class="code-wrapper"><pre><code class="hljs c">newva = <span class="hljs-number">0x40600000</span> + <span class="hljs-number">5</span> * <span class="hljs-number">4</span> KiB;      = <span class="hljs-number">0x40600000</span> + <span class="hljs-number">0x5000</span>;      = <span class="hljs-number">0x40605000</span>;</code></pre></div><p>所以这条页表路径对应的虚拟页是：</p><div class="code-wrapper"><pre><code class="hljs text">0x40605000</code></pre></div><p>为什么代码写 <code>|</code> 而不是 <code>+</code></p><div class="code-wrapper"><pre><code class="hljs c">newva = va | ((uint64)i &lt;&lt; PXSHIFT(level));</code></pre></div><p>可以先把它理解为：</p><div class="code-wrapper"><pre><code class="hljs c">newva = va + i * 当前层每项负责的大小;</code></pre></div><p>在这里，<code>|</code> 和 <code>+</code> 结果相同，因为每层使用不同的地址位，不会重叠。</p><p>所以这句代码最直白的翻译是：</p><div class="code-wrapper"><pre><code class="hljs c">当前区域的起点+当前页表第 i 项在区域内的偏移=当前 PTE 所代表区域的起点</code></pre></div><p><code>level</code> 不是用来计算 <code>i</code> 的。它只决定：</p><div class="code-wrapper"><pre><code class="hljs text">当前页表的每个 PTE 到底代表 4 KiB、2 MiB，还是 1 GiB。</code></pre></div></blockquote></details><p>调用递归函数的时候，传入的参数也是个问题；<strong>以及最重要的，如何计算虚拟地址 va</strong>？这两个问题放在一起说明。</p><p>首先，务必要明晰这个模型：</p><div class="code-wrapper"><pre><code class="hljs text">[ VPN2 ][ VPN1 ][ VPN0 ][ offset ]                              ↑ 12 位</code></pre></div><p>而且通过阅读 <code>walk</code> 函数，我们也容易注意到，<strong>level 的取值只有 0 1 2，而且是越开始越高</strong>，所以，开始传入的 <code>level</code> 肯定是2。</p><div class="note note-info"><p>举个例子，也可以看宏：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> PXMASK          0x1FF <span class="hljs-comment">// 低9位是1</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> PXSHIFT(level)  (PGSHIFT+(9*(level)))</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> PX(level, va) ((((uint64) (va)) &gt;&gt; PXSHIFT(level)) &amp; PXMASK)</span></code></pre></div><p>如果要提取 <code>VPN[0]</code>，那么就是要计算 <code>(va &gt;&gt; 12) &amp; 0x1FF</code></p><p>右移 12 位的含义是：<strong>丢掉页内偏移，把第 12～20 位移动到最低位</strong>，然后和 <code>0b111111111</code> 进行与运算，自然就得到了那一块是什么。</p></div><p>现在，再考虑 <code>newva</code> 是怎么计算出来的（<code>uint64 newva = va | ((uint64)i &lt;&lt; PXSHIFT(*level*));</code>），就很清晰了，就是在上一次的基础上，累计这一层的偏移量。</p><div class="note note-primary"><p>每次遍历都是<strong>每页表</strong>的。</p></div><div class="note note-success"><p><strong>这里记得强转 i</strong>。</p></div><p>总之，感觉我对概念的理解实在是太差了，有点难办呢。花了 5 个小时去解决这么一个 easy 的题目确实有点挫败感了，不过也没办法，继续写下去吧。</p><h2 id="Use-superpages">Use superpages</h2><p>看到这个 Hard 的题目，有点慌，所以去网上搜了搜这些知识的解析，结果意外发现一些不错的资料：</p><ul><li>MIT6.s081/6.828 lectrue04：page tables 以及  Lab3 心得 - 逆风的大船的文章 - 知乎 <a href="https://zhuanlan.zhihu.com/p/651171058">https://zhuanlan.zhihu.com/p/651171058</a><ul><li>这个博主的手写笔记极为详细啊，很有用</li></ul></li><li><a href="https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081">https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081</a><ul><li>每节课的「文字版录播」，我觉得比 xv6 book 好多了，而且同学们问的问题也不错。</li></ul></li></ul><p>简单摘录一些：</p><ul><li><p><code>kvminithart</code> 函数，很重要：</p><blockquote><p>这个函数首先设置了SATP寄存器，kernel_pagetable变量来自于kvminit第一行。所以这里实际上是内核告诉MMU来使用刚刚设置好的page table。当这里这条指令执行之后，下一个指令的地址会发生什么？</p><p>在这条指令之前，还不存在可用的page table，所以也就不存在地址翻译。执行完这条指令之后，程序计数器（Program Counter）增加了4。而之后的下一条指令被执行时，<strong>程序计数器会被内存中的page table翻译</strong>。</p><p>所以这条指令的执行时刻是一个非常重要的时刻。因为整个地址翻译从这条指令之后开始生效，之后的每一个使用的内存地址都可能对应到与之不同的物理内存地址。因为在这条指令之前，我们使用的都是物理内存地址，这条指令之后page table开始生效，所有的内存地址都变成了另一个含义，也就是虚拟内存地址。</p></blockquote></li><li><p>为什么通过3级page table会比一个超大的page table更好呢？</p><blockquote><p>Frans教授：这是个好问题，这的原因是，3级page table中，大量的PTE都可以不存储。比如，对于最高级的page table里面，如果一个PTE为空，那么你就完全不用创建它对应的中间级和最底层page table，以及里面的PTE。所以，这就是像是在整个虚拟地址空间中的一大段地址完全不需要有映射一样。</p></blockquote></li><li><p><code>proc_mapstacks()</code>函数比较有趣，参考 <a href="https://www.cnblogs.com/looking-for-zihuatanejo/p/17629873.html">MIT6.s081/6.828 lectrue4：page tables 以及 Lab3 心得 - byFMH - 博客园</a> 的部分阅读</p></li><li><p>pageguard 的作用是什么？</p><blockquote><p>保护页是一页<strong>故意禁止访问的虚拟内存</strong>，放在栈旁边，用来检测栈溢出。</p><p>例如内核栈：</p><div class="code-wrapper"><pre><code class="hljs cpp">kernel stackguard page    ← 不映射kernel stackguard page</code></pre></div><p>如果内核栈不断向下增长，越过边界访问保护页，就会触发 page fault，最终使内核 <code>panic</code>。</p><p>这样比栈悄悄覆盖其他内核数据要好：<strong>尽早暴露错误，而不是继续带着损坏的数据运行。</strong></p><p>xv6 中有两种：</p><ul><li><strong>内核栈保护页</strong>：PTE 无效，内核访问也会 fault。</li><li><strong>用户栈保护页</strong>：清除 <code>PTE_U</code>，用户态不能访问，栈溢出时进程会 fault。</li></ul></blockquote></li><li><p>理解这一节学生问的问题很重要：<a href="https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec04-page-tables-frans/4.5-kernel-page-table">4.5 Kernel Page Table | MIT6.S081</a></p><p>内核页表也存在对用户进程的映射，而且对于内核而言，va == pa，是相同的。</p></li></ul><p>总之，把这些内容读完了之后（虽然还是有点混乱），再开始实现这个 Lab：</p><p>目的是实现 superpage：</p><ul><li>RISC-V 默认页面大小为 4KB</li><li>这里需要实现大小为 2MB 的页</li></ul><p>阅读 hint：</p><blockquote><p>阅读 <code>user/pgtbltest.c</code> 中的 <code>superpg_fork</code> 和 <code>superpg_free</code> 。</p></blockquote><p>自己看有点费劲，让 Agent 代劳：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 5750 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">关键背景定义 宏/函数 值/作用 SUPERPGSIZE 2MB (2,097,152 字节) — 一个 superpage 的大小 SZ 8...</span></summary><blockquote><p>关键背景定义</p><table><thead><tr><th>宏/函数</th><th>值/作用</th></tr></thead><tbody><tr><td><code>SUPERPGSIZE</code></td><td><strong>2MB</strong> (2,097,152 字节) — 一个 superpage 的大小</td></tr><tr><td><code>SZ</code></td><td><code>8 * SUPERPGSIZE</code> = <strong>16MB</strong></td></tr><tr><td><code>SUPERPGROUNDUP(a)</code></td><td>将地址 <strong>向上</strong> 对齐到 2MB 边界</td></tr><tr><td><code>SUPERPGROUNDDOWN(a)</code></td><td>将地址 <strong>向下</strong> 对齐到 2MB 边界</td></tr><tr><td><code>pgpte(void *va)</code></td><td>系统调用，返回当前进程页表中 <code>va</code> 对应的 PTE</td></tr><tr><td><code>SBRK_ERROR</code></td><td><code>(char *)-1</code>，sbrk 失败的返回值</td></tr></tbody></table><hr><p><code>superpg_fork()</code> — 测试 fork 是否正确复制 superpage</p><p>这个函数分 <strong>两个阶段</strong>：</p><p>第一阶段（第 138-156 行）：验证 fork 能正确复制 superpage</p><div class="code-wrapper"><pre><code class="hljs isbl"><span class="hljs-function"><span class="hljs-title">sbrk</span>(<span class="hljs-variable">SZ</span>=<span class="hljs-number">16</span><span class="hljs-variable">MB</span>)</span>    │    ▼┌─────────────────────────────┐│  <span class="hljs-number">8</span> 个 <span class="hljs-number">2</span><span class="hljs-variable">MB</span> <span class="hljs-variable">superpage</span>         │  ← 父进程地址空间│  (共 <span class="hljs-number">16</span><span class="hljs-variable">MB</span>)                  │└─────────────────────────────┘    │    │ <span class="hljs-function"><span class="hljs-title">supercheck</span>(<span class="hljs-variable">end</span>) — 父进程验证 <span class="hljs-variable">superpage</span> 存在</span><span class="hljs-function">    │</span><span class="hljs-function">    │ <span class="hljs-title">fork</span>()</span>    │    ├── 父进程: <span class="hljs-function"><span class="hljs-title">wait</span>()</span>    │    └── 子进程: <span class="hljs-function"><span class="hljs-title">supercheck</span>(<span class="hljs-variable">end</span>) — 子进程也必须看到相同的 <span class="hljs-variable">superpage</span></span><span class="hljs-function">                  <span class="hljs-title"><span class="hljs-built_in">exit</span></span>(<span class="hljs-number">0</span>)</span></code></pre></div><p><strong>核心逻辑</strong>：如果 fork 正确复制了页表（包括 superpage 映射），那么子进程的地址空间中，相同的虚拟地址范围也应该有 superpage 映射，PTE 属性应该完全一致。</p><p>第二阶段（第 159-173 行）：验证释放后 fork 的子进程不能访问已释放内存</p><div class="code-wrapper"><pre><code class="hljs isbl"><span class="hljs-function"><span class="hljs-title">sbrk</span>(-<span class="hljs-number">16</span><span class="hljs-variable">MB</span>)  — 释放所有内存</span><span class="hljs-function">    │</span><span class="hljs-function">    │ <span class="hljs-title">fork</span>()</span>    │    ├── 父进程: <span class="hljs-function"><span class="hljs-title">wait</span>(&amp;<span class="hljs-variable">status</span>)</span>    │           如果 <span class="hljs-variable">status</span> == <span class="hljs-number">0</span>（子进程正常退出），说明子进程成功访问了    │           已释放的内存 → 测试失败！    │    └── 子进程: *(<span class="hljs-variable">end</span> + <span class="hljs-number">1</span>) = <span class="hljs-string">'9'</span>  — 尝试写入已被父进程释放的内存                 如果内核正确工作 → 页错误 → 内核杀死子进程 (<span class="hljs-variable">status</span> ≠ <span class="hljs-number">0</span>)</code></pre></div><p>这是一个 <strong>安全测试</strong>：确保进程释放内存后，fork 出的子进程不能访问那块已经被释放的内存。</p><p><code>supercheck()</code> 辅助函数（第 94-128 行）</p><p>这个函数验证一块以 <code>end</code> 结束的内存区域是否使用了 superpage：</p><ol><li><strong>第 98-105 行</strong>：检查 <code>end</code> 到下一个 2MB 对齐边界之间的普通页面（非 superpage 部分），确保每个都有有效的 PTE</li><li><strong>第 107-118 行</strong>：检查对齐边界之后的 <strong>512 个 PGSIZE 页面</strong>（即一个 2MB superpage），验证：<ul><li>所有 PTE <strong>非零</strong>（映射存在）</li><li><strong>所有 PTE 值完全相同</strong>（这是 superpage 的关键特征 — 一个 L2 级别的 PTE 覆盖整个 2MB 区域）</li><li>PTE 有 <code>PTE_V | PTE_R | PTE_W</code> 标志</li></ul></li><li><strong>第 120-127 行</strong>：对 superpage 区域进行读写测试，验证数据正确性</li></ol><hr><p>好的，让我用更直观的方式一步步拆解 <code>superpg_free</code> 的逻辑。核心困惑可能在于：<strong>sbrk 操作的对象是什么，以及 SUPERPGROUNDDOWN 对齐到底在做什么</strong>。</p><hr><p>前置知识</p><ul><li><strong><code>sbrk(n)</code></strong>：将进程的堆（heap）扩大/缩小 <code>n</code> 字节，返回<strong>旧的堆顶地址</strong></li><li><strong><code>sbrk(0)</code></strong>：不改变堆大小，只返回<strong>当前堆顶地址</strong>（用来查询堆顶在哪）</li><li><strong>堆是连续增长的</strong>，从低地址向高地址。<code>sbrk(正数)</code> = 堆向上增长，<code>sbrk(负数)</code> = 堆向下收缩</li></ul><p>堆的内存布局大概是：</p><div class="code-wrapper"><pre><code class="hljs lasso">低地址├── code / <span class="hljs-built_in">data</span>├── heap（已分配）  ← sbrk 管理的区域├── <span class="hljs-params">...</span> 未分配 <span class="hljs-params">...</span>└── <span class="hljs-built_in">stack</span>高地址</code></pre></div><hr><p>逐行拆解 <code>superpg_free</code></p><p>第 1 步：分配 16MB（第 185-188 行）</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">char</span> *end = sbrk(SZ);  <span class="hljs-comment">// SZ = 8 * SUPERPGSIZE = 16MB</span></code></pre></div><p>此时 <code>end</code> = <strong>分配前的旧堆顶</strong>（也是新分配区域的起始地址）。画成图：</p><div class="code-wrapper"><pre><code class="hljs ada">                    <span class="hljs-keyword">end</span>（旧堆顶）                    │                    ▼┌───────────────────┼────────────────────────────────┐│   原来的 heap      │     新分配的 <span class="hljs-number">16</span>MB               ││                   │   = <span class="hljs-number">8</span> 个 <span class="hljs-number">2</span>MB superpage          │└───────────────────┼────────────────────────────────┘                    ▲                                ▲                    │                                │                  <span class="hljs-keyword">end</span>                        <span class="hljs-keyword">end</span> + <span class="hljs-number">16</span>MB                                            = 新堆顶</code></pre></div><p>此时 <code>sbrk(0)</code> 会返回 <code>end + 16MB</code>。</p><hr><p>第 2 步：释放 "超出一个 superpage 边界" 的部分（第 190-193 行）</p><p>这是最关键也最容易迷惑的部分。让我画出来：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">char</span> *a = sbrk(<span class="hljs-number">0</span>);                      <span class="hljs-comment">// a = 当前堆顶</span>uint64 s = SUPERPGROUNDDOWN((uint64)a); <span class="hljs-comment">// s = 将堆顶向下对齐到 2MB 边界</span>sbrk(-((uint64)a - s));                 <span class="hljs-comment">// 释放 a-s 字节</span>a = sbrk(<span class="hljs-number">0</span>);                            <span class="hljs-comment">// a = 新堆顶（现在对齐到 2MB 了）</span></code></pre></div><p><strong>为什么需要这一步？</strong> 因为 <code>sbrk(16MB)</code> 分配的 16MB 是恰好 8 个 superpage，但如果堆在分配之前不是 2MB 对齐的，那这 16MB 的<strong>结束位置</strong>也不会是 2MB 对齐的。后面测试需要堆顶恰好对齐到 2MB 边界，所以这一步把"超出去"的碎片裁掉。</p><p>画成具体例子（假设堆顶值不对齐）：</p><div class="code-wrapper"><pre><code class="hljs excel">假设堆顶 a = <span class="hljs-number">0</span>x10_300_000（不对齐到 <span class="hljs-number">2</span>MB）SUPERPGROUNDDOWN(<span class="hljs-number">0</span>x10_300_000)= 向下对齐到 <span class="hljs-number">2</span>MB= <span class="hljs-number">0</span>x10_200_000  ← 这个就是 s差值 = a - s = <span class="hljs-number">0</span>x10_300_000 - <span class="hljs-number">0</span>x10_200_000 = <span class="hljs-number">0</span>x100_000 = <span class="hljs-number">1</span>MBsbrk(-<span class="hljs-number">1</span>MB) = 释放 <span class="hljs-number">1</span>MB，堆顶回退到 <span class="hljs-number">0</span>x10_200_000</code></pre></div><p>画成图：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 ABNF · 19 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">ABNF · 19 行</span></summary><div class="code-wrapper"><pre><code class="hljs abnf">释放前：┌────────────────────────────────────────┬─────────────┐│         完整的 superpage 们             │  碎片部分    ││         每个 <span class="hljs-number">2</span>MB                        │  (&lt; <span class="hljs-number">2</span>MB)    │└────────────────────────────────────────┴─────────────┘                                         ▲             ▲                                         │             │                                    s <span class="hljs-operator">=</span> <span class="hljs-number">2</span>MB边界    a <span class="hljs-operator">=</span> 堆顶                                    (对齐)         (不对齐)释放 sbrk(-(a-s)) 后：┌────────────────────────────────────────┐│         完整的 superpage 们             ││         每个 <span class="hljs-number">2</span>MB                        │└────────────────────────────────────────┘                                         ▲                                         │                                    a <span class="hljs-operator">=</span> 新堆顶 <span class="hljs-operator">=</span> s                                    (恰好对齐 <span class="hljs-number">2</span>MB)</code></pre></div></details><p><strong>简化理解</strong>：这一步就是"裁边"，确保堆顶恰好落在 2MB 的边界上。此时 <code>sbrk(0)</code> 返回的地址是 2MB 对齐的。</p><hr><p>第 3 步：验证最后两个 4KB 页属于同一个 superpage（第 195-199 行）</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">pte_t</span> pte1 = (<span class="hljs-type">pte_t</span>) pgpte((<span class="hljs-type">void</span> *)(a - PGSIZE));      <span class="hljs-comment">// 倒数第 1 页的 PTE</span><span class="hljs-type">pte_t</span> pte2 = (<span class="hljs-type">pte_t</span>) pgpte((<span class="hljs-type">void</span> *)(a - <span class="hljs-number">2</span>*PGSIZE));     <span class="hljs-comment">// 倒数第 2 页的 PTE</span><span class="hljs-keyword">if</span> (pte1 != pte2) {    err(<span class="hljs-string">"not a super page"</span>);}</code></pre></div><div class="code-wrapper"><pre><code class="hljs livecodeserver">堆顶 <span class="hljs-keyword">a</span>（<span class="hljs-number">2</span>MB 对齐）││  <span class="hljs-keyword">a</span> - <span class="hljs-number">1</span>*PGSIZE  ← 最后一个 <span class="hljs-number">4</span>KB 页  → pte1│  <span class="hljs-keyword">a</span> - <span class="hljs-number">2</span>*PGSIZE  ← 倒数第 <span class="hljs-number">2</span> 个 <span class="hljs-number">4</span>KB 页 → pte2│  ...│  <span class="hljs-keyword">a</span> - <span class="hljs-number">512</span>*PGSIZE (= <span class="hljs-keyword">a</span> - <span class="hljs-number">2</span>MB) ← superpage 的起始地址│▼</code></pre></div><p><strong>如果 pte1 == pte2</strong>，说明这两个 4KB 页面共享同一个 2MB 级别的 PTE → 它们属于同一个 superpage ✓</p><p>（如果它们是不相关的普通 4KB 页面，PTE 值会不同，因为物理地址不同。）</p><hr><p>第 4 步：写入数据（第 201-203 行）</p><div class="code-wrapper"><pre><code class="hljs c">*(a - PGSIZE + <span class="hljs-number">1</span>) = <span class="hljs-string">'8'</span>;      <span class="hljs-comment">// 倒数第 1 页</span>*(a - <span class="hljs-number">2</span>*PGSIZE + <span class="hljs-number">1</span>) = <span class="hljs-string">'9'</span>;    <span class="hljs-comment">// 倒数第 2 页</span></code></pre></div><div class="code-wrapper"><pre><code class="hljs lasso">┌──────────────────────────────┬───────────┬───────────┐│   剩余 superpage 部分         │ 倒数第<span class="hljs-number">2</span>页  │ 倒数第<span class="hljs-number">1</span>页  ││   <span class="hljs-params">...</span>                        │ 写 <span class="hljs-string">'9'</span>    │ 写 <span class="hljs-string">'8'</span>    │└──────────────────────────────┴───────────┴───────────┘                                              ▲                                              │                                          a = 堆顶</code></pre></div><hr><p>第 5 步：释放最后 4KB → 触发 superpage 拆分！（第 205-207 行）</p><div class="code-wrapper"><pre><code class="hljs c">sbrk(-PGSIZE);   <span class="hljs-comment">// 释放最后 4KB</span>a = sbrk(<span class="hljs-number">0</span>);     <span class="hljs-comment">// 新堆顶</span></code></pre></div><div class="code-wrapper"><pre><code class="hljs livecodeserver">释放前：┌──────────────────────────────┬───────────┬───────────┐│   一个 <span class="hljs-number">2</span>MB superpage          │   <span class="hljs-string">'9'</span>     │   <span class="hljs-string">'8'</span>     ││   (<span class="hljs-number">512</span>个<span class="hljs-number">4</span>KB页，共享<span class="hljs-number">1</span>个PTE)     │           │           │└──────────────────────────────┴───────────┴───────────┘                                              ▲                                              │                                         旧堆顶 <span class="hljs-keyword">a</span>释放 sbrk(-PGSIZE) 后：┌──────────────────────────────┬───────────┐│   被拆分为 <span class="hljs-number">510</span> 个独立 <span class="hljs-number">4</span>KB 页  │   <span class="hljs-string">'9'</span>     │  释放掉的 <span class="hljs-string">'8'</span>│   (不再是 <span class="hljs-number">1</span> 个 superpage)     │  保留     │  ← 应该不可访问└──────────────────────────────┴───────────┘                               ▲                               │                          新堆顶 <span class="hljs-keyword">a</span></code></pre></div><p><strong>这是核心测试点</strong>：当 <code>sbrk(-PGSIZE)</code> 只释放 superpage 的最后一个 4KB 页时，内核<strong>必须</strong>将原来的 2MB superpage 拆分为 511 个独立的 4KB 普通页 + 释放掉最后 1 页。不能简单地释放整个 2MB。</p><hr><p>第 6 步：验证拆分后数据不丢失（第 209-211 行）</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (*(a - PGSIZE + <span class="hljs-number">1</span>) != <span class="hljs-string">'9'</span>) {    err(<span class="hljs-string">"lost content after freeing part of super page"</span>);}</code></pre></div><p>释放后 <code>a</code> 是新堆顶。<code>a - PGSIZE</code> 现在是最后一个<strong>仍有效的</strong> 4KB 页（之前写的 <code>'9'</code>），数据必须还在。</p><hr><p>第 7 步：fork 验证隔离性（第 213-230 行）</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span> ((pid = fork()) &lt; <span class="hljs-number">0</span>) {    ...} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (pid == <span class="hljs-number">0</span>) {    <span class="hljs-comment">// 子进程尝试访问 a 处的内存（已被父进程释放）</span>    <span class="hljs-keyword">if</span> (*(a + <span class="hljs-number">1</span>) == <span class="hljs-string">'9'</span>) {        <span class="hljs-built_in">exit</span>(<span class="hljs-number">0</span>);  <span class="hljs-comment">// 如果能读到 → 安全漏洞！</span>    }}</code></pre></div><ul><li>父进程释放了 <code>a</code> 处开始的 4KB 页</li><li>子进程 fork 时，这页<strong>不应该</strong>出现在子进程的地址空间中</li><li>如果子进程访问 <code>*(a + 1)</code> 成功且正常退出 → <strong>测试失败</strong>（隔离性被破坏）</li><li>正确行为：子进程页错误 → 被内核杀死 → 父进程看到 <code>status ≠ 0</code> → <strong>测试通过</strong></li></ul><hr><p>第 8-9 步：验证 PTE 清理 + 逐页释放（第 232-244 行）</p><div class="code-wrapper"><pre><code class="hljs c">pte1 = (<span class="hljs-type">pte_t</span>)pgpte((<span class="hljs-type">void</span> *)a);<span class="hljs-keyword">if</span> (pte1 != <span class="hljs-number">0</span>) {    err(<span class="hljs-string">"pte for freed memory is valid"</span>);  <span class="hljs-comment">// 已释放页的 PTE 必须为 0</span>}s = SUPERPGROUNDDOWN((uint64)a);  <span class="hljs-comment">// 找到这个（已拆分）superpage 的起始地址</span><span class="hljs-keyword">for</span> (; (uint64)a &gt; s; a -= PGSIZE) {    a = sbrk(-PGSIZE);           <span class="hljs-comment">// 逐页释放</span>    pte1 = (<span class="hljs-type">pte_t</span>)pgpte(sbrk(<span class="hljs-number">0</span>));    <span class="hljs-keyword">if</span> (pte1 != <span class="hljs-number">0</span>) {        err(<span class="hljs-string">"page hasn't been freed"</span>);  <span class="hljs-comment">// 每释放一页，其 PTE 必须变 0</span>    }}</code></pre></div><p>循环的意思是：从当前堆顶开始，一页一页往下释放，每释放一页就检查该页的 PTE 是否已经变为 0。一直释放到这个 2MB 区域的起始边界 <code>s</code>。</p><hr><p>整体逻辑总结</p><p><code>superpg_free</code> 的测试链路：</p><div class="code-wrapper"><pre><code class="hljs abnf">分配 <span class="hljs-number">16</span>MB  → 裁边让堆顶对齐 <span class="hljs-number">2</span>MB    → 确认最后两页属于同一个 superpage      → 写入测试数据        → 只释放最后 <span class="hljs-number">4</span>KB（迫使 superpage 拆分）          → 验证：剩余数据完好 ✓          → 验证：子进程不能访问释放页 ✓          → 验证：释放页 PTE <span class="hljs-operator">=</span> <span class="hljs-number">0</span> ✓          → 逐页释放剩余页，验证每页 PTE 归零 ✓</code></pre></div><p>一句话概括：<strong>验证当只释放 superpage 的一部分时，内核能否正确地将 superpage "打散"为普通页面，同时保持未释放部分的数据完整和进程间隔离。</strong></p><hr><p>两个测试的关系</p><table><thead><tr><th>测试</th><th>验证内容</th></tr></thead><tbody><tr><td><code>superpg_fork</code></td><td>superpage 在 <strong>fork 时能否完整复制</strong>，以及释放后子进程<strong>不能访问已释放内存</strong></td></tr><tr><td><code>superpg_free</code></td><td>superpage <strong>部分释放时能否正确拆分</strong>为普通页，数据完整性，以及释放后的<strong>隔离性</strong></td></tr></tbody></table><p>两者共同覆盖了 xv6 中 superpage（2MB 大页）支持的核心正确性：<strong>创建、复制、拆分、释放、隔离</strong>。</p></blockquote></details><p>再看下一个 hint：</p><blockquote><p>一个好的起点是 <code>kernel/sysproc.c</code> 中的 <code>sys_sbrk</code> ，它由 <code>sbrk</code> 系统调用调用。跟踪代码路径到 <code>growproc</code> 函数，该函数为 <code>sbrk</code> 立即分配内存。</p></blockquote><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 26 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 26 行</span></summary><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-comment">// kernel/sysproc.c</span>uint64<span class="hljs-title function_">sys_sbrk</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  uint64 addr;  <span class="hljs-type">int</span> t;  <span class="hljs-type">int</span> n;  argint(<span class="hljs-number">0</span>, &amp;n); <span class="hljs-comment">// 第 0 个参数：要增长/缩减的字节数</span>  argint(<span class="hljs-number">1</span>, &amp;t); <span class="hljs-comment">// 第 1 个参数：分配策略 (SBRK_EAGER=1 或 SBRK_LAZY=2)</span>  addr = myproc()-&gt;sz; <span class="hljs-comment">// 保存旧堆顶地址，作为返回值</span>  <span class="hljs-keyword">if</span>(t == SBRK_EAGER || n &lt; <span class="hljs-number">0</span>) {    <span class="hljs-keyword">if</span>(growproc(n) &lt; <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  } <span class="hljs-keyword">else</span> {    <span class="hljs-comment">// Lazily allocate memory for this process: increase its memory</span>    <span class="hljs-comment">// size but don't allocate memory. If the processes uses the</span>    <span class="hljs-comment">// memory, vmfault() will allocate it.</span>    <span class="hljs-keyword">if</span>(addr + n &lt; addr)      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    myproc()-&gt;sz += n;  }  <span class="hljs-keyword">return</span> addr;}</code></pre></div></details><p>这里可能会误以为 <code>sbrk()</code> 有两个参数，但实际上是用户态有两个调用入口：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// user/ulib.c</span><span class="hljs-type">char</span>* <span class="hljs-title function_">sbrk</span><span class="hljs-params">(<span class="hljs-type">int</span> n)</span>     { <span class="hljs-keyword">return</span> sys_sbrk(n, SBRK_EAGER); }  <span class="hljs-comment">// 立即分配</span><span class="hljs-type">char</span>* <span class="hljs-title function_">sbrklazy</span><span class="hljs-params">(<span class="hljs-type">int</span> n)</span>  { <span class="hljs-keyword">return</span> sys_sbrk(n, SBRK_LAZY);  }  <span class="hljs-comment">// 惰性分配</span></code></pre></div><p><strong>立即分配</strong>：调用 <code>growproc(n)</code> 立即调配物理内存：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 20 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 20 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/proc.c</span><span class="hljs-comment">// Shrink user memory by n bytes.</span><span class="hljs-comment">// Return 0 on success, -1 on failure.</span><span class="hljs-type">int</span><span class="hljs-title function_">growproc</span><span class="hljs-params">(<span class="hljs-type">int</span> n)</span>{  uint64 sz;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  sz = p-&gt;sz;  <span class="hljs-keyword">if</span>(n &gt; <span class="hljs-number">0</span>){    <span class="hljs-keyword">if</span>((sz = uvmalloc(p-&gt;pagetable, sz, sz + n, PTE_W)) == <span class="hljs-number">0</span>) {      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    }  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(n &lt; <span class="hljs-number">0</span>){    sz = uvmdealloc(p-&gt;pagetable, sz, sz + n);  }  p-&gt;sz = sz;  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div></details><ul><li>n &gt; 0 则分配物理页+建立页表的映射（在 <code>uvmalloc</code> 函数里面建立）</li><li>n &lt; 0 则立即释放物理页</li></ul><p>这里注意，n &lt; 0 时则<strong>无视懒分配策略</strong>。</p><p><strong>懒分配</strong>：</p><p>只改 <code>p-&gt;sz</code>，不分配物理内存，实际分配推迟到 page fault 发生时："<code>vmfault()</code>  will allocate it."：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 27 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 27 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// kernel/vm.c</span><span class="hljs-comment">// allocate and map user memory if process is referencing a page</span><span class="hljs-comment">// that was lazily allocated in sys_sbrk().</span><span class="hljs-comment">// returns 0 if va is invalid or already mapped, or if</span><span class="hljs-comment">// out of physical memory, and physical address if successful.</span>uint64<span class="hljs-title function_">vmfault</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va, <span class="hljs-type">int</span> read)</span>{  uint64 mem;  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();    <span class="hljs-keyword">if</span> (va &gt;= p-&gt;sz)    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  va = PGROUNDDOWN(va);  <span class="hljs-keyword">if</span>(ismapped(pagetable, va)) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  mem = (uint64) kalloc();  <span class="hljs-keyword">if</span>(mem == <span class="hljs-number">0</span>)    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  <span class="hljs-built_in">memset</span>((<span class="hljs-type">void</span> *) mem, <span class="hljs-number">0</span>, PGSIZE);  <span class="hljs-keyword">if</span> (mappages(p-&gt;pagetable, va, PGSIZE, mem, PTE_W|PTE_U|PTE_R) != <span class="hljs-number">0</span>) {    kfree((<span class="hljs-type">void</span> *)mem);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">return</span> mem;}</code></pre></div></details><p>核心就是：在 <code>va &lt; p-&gt;sz</code> （合法但未分配）时，分配内存。</p><div class="note note-info"><p>为什么 <code>myproc()-&gt;sz</code> 是旧堆顶地址？</p><p>本质还是 xv6 的<strong>用户</strong>进程的内存布局：</p><blockquote><p>[!tip]</p><p>这里肯定是用户进程，因为 <code>sbrk</code> 是用户态的时候调用的 syscall</p></blockquote><div class="code-wrapper"><pre><code class="hljs text">0x0├── text (代码)├── data (数据)├── guard page (保护页)├── heap (堆) ← sbrk 管理，向上增长│   ...│   └── 堆顶 = p-&gt;sz││   (未分配)│└── MAXVA    └── trampoline / trapframe</code></pre></div></div><div class="note note-info"><p>解析一下 <code>ismapped</code> 函数：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">ismapped</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va)</span> {  <span class="hljs-type">pte_t</span> *pte = walk(pagetable, va, <span class="hljs-number">0</span>);  <span class="hljs-keyword">if</span> (pte == <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;  }  <span class="hljs-keyword">if</span> (*pte &amp; PTE_V){    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;  }  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div><p>首先理解 <code>walk(pagetable, va, 0)</code> 的返回值：非零指针则为存在，0（NULL）则为不存在。</p><p>那么，如果是空指针肯定要首先避免掉，不然后面会出异常。</p><p>然后，只有 <code>*pte &amp; PTE_V</code> 才符合。</p></div><p>再看下一个 hint：</p><blockquote><p>你的内核需要能够分配和释放 2MB 的区域。修改 <code>kalloc.c</code> 以预留一些两兆字节的物理内存区域，并创建 <code>superalloc()</code> 和 <code>superfree()</code> 函数。你只需要少量两兆字节的内存块。</p></blockquote><p>什么叫「<em><strong>预留一些内存区域</strong></em>」？我们可以阅读源码得到答案。</p><p>首先，看到 <code>main.c</code>：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 37 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 37 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// start() jumps here in supervisor mode on all CPUs.</span><span class="hljs-type">void</span><span class="hljs-title function_">main</span><span class="hljs-params">()</span>{  <span class="hljs-keyword">if</span>(cpuid() == <span class="hljs-number">0</span>){    consoleinit();    printfinit();    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\n"</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"xv6 kernel is booting\n"</span>);    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\n"</span>);    kinit();         <span class="hljs-comment">// physical page allocator</span>    kvminit();       <span class="hljs-comment">// create kernel page table</span>    kvminithart();   <span class="hljs-comment">// turn on paging</span>    procinit();      <span class="hljs-comment">// process table</span>    trapinit();      <span class="hljs-comment">// trap vectors</span>    trapinithart();  <span class="hljs-comment">// install kernel trap vector</span>    plicinit();      <span class="hljs-comment">// set up interrupt controller</span>    plicinithart();  <span class="hljs-comment">// ask PLIC for device interrupts</span>    binit();         <span class="hljs-comment">// buffer cache</span>    iinit();         <span class="hljs-comment">// inode table</span>    fileinit();      <span class="hljs-comment">// file table</span>    virtio_disk_init(); <span class="hljs-comment">// emulated hard disk</span>    userinit();      <span class="hljs-comment">// first user process</span>    __sync_synchronize();    started = <span class="hljs-number">1</span>;  } <span class="hljs-keyword">else</span> {    <span class="hljs-keyword">while</span>(started == <span class="hljs-number">0</span>)      ;    __sync_synchronize();    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"hart %d starting\n"</span>, cpuid());    kvminithart();    <span class="hljs-comment">// turn on paging</span>    trapinithart();   <span class="hljs-comment">// install kernel trap vector</span>    plicinithart();   <span class="hljs-comment">// ask PLIC for device interrupts</span>  }  scheduler();        }</code></pre></div></details><p>注意到，这里 <code>kinit(); // physical page allocator</code> ，一个定义在 <code>kalloc.c</code> 里面的函数，起到了分配物理页的作用，其实也就是分配了空闲列表。</p><div class="note note-info"><p>这个在启动 xv6 的部分也有提到。</p></div><p>再去看到 <code>kalloc.c</code>：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 18 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 18 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">extern</span> <span class="hljs-type">char</span> end[]; <span class="hljs-comment">// first address after kernel.</span>                   <span class="hljs-comment">// defined by kernel.ld.</span><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">next</span>;</span>};<span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">lock</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">freelist</span>;</span>} kmem;<span class="hljs-type">void</span><span class="hljs-title function_">kinit</span><span class="hljs-params">()</span>{  initlock(&amp;kmem.lock, <span class="hljs-string">"kmem"</span>);  freerange(end, (<span class="hljs-type">void</span>*)PHYSTOP);}</code></pre></div></details><p><code>kmem</code> 就是一个带锁的空闲列表，看到 <code>kinit</code>，暂时忽略锁的部分（后续章节再涉及），发现调用了 <code>freerange</code> 函数，那么再看：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">freerange</span><span class="hljs-params">(<span class="hljs-type">void</span> *pa_start, <span class="hljs-type">void</span> *pa_end)</span>{  <span class="hljs-type">char</span> *p;  p = (<span class="hljs-type">char</span>*)PGROUNDUP((uint64)pa_start);  <span class="hljs-keyword">for</span>(; p + PGSIZE &lt;= (<span class="hljs-type">char</span>*)pa_end; p += PGSIZE)    kfree(p);}</code></pre></div><p>这里 <code>PGROUNDUP</code> 宏的作用是向上匀，保证开始的地址是 <code>PGSIZE</code> 的整数倍。</p><p>可以发现，是从新的开始处开始，直到 <code>pa_end</code>，一页一页地调用 <code>kfree(p)</code> 来释放。</p><div class="note note-info"><p>为什么要定义成 <code>char*</code>？这和字符串没有任何关系。用 <code>char *</code> 纯粹是因为 <strong><code>char</code> 在 C 语言里就是 1 字节的别名</strong>。</p><p><strong>核心原因：指针算术</strong></p><p>当你写 <code>p + 1</code> 时，实际加的字节数取决于指针的类型：</p><div class="code-wrapper"><pre><code class="hljs apache"><span class="hljs-attribute">char</span>*   p + <span class="hljs-number">1</span>  →  地址 + <span class="hljs-number">1</span> 字节   ✓ 精细到每个字节<span class="hljs-attribute">int</span>*    p + <span class="hljs-number">1</span>  →  地址 + <span class="hljs-number">4</span> 字节   ✗ 太粗糙了<span class="hljs-attribute">uint64</span>* p + <span class="hljs-number">1</span>  →  地址 + <span class="hljs-number">8</span> 字节   ✗ 太粗糙了</code></pre></div><p>操作物理内存地址时，需要<strong>逐字节</strong>级别的控制（比如对齐到某个地址、跳过某个字节数）。<code>char</code> 的 <code>sizeof</code> 保证为 1，所以 <code>char *</code> 是唯一的、自然的、"步长为 1" 的指针类型。</p><p><code>void *</code> 不行吗？不行——标准 C 不允许对 <code>void *</code> 做算术运算（<code>p + PGSIZE</code> 会编译报错）。</p><hr><p>看具体代码：这里的 <code>p += PGSIZE</code> 就是在做<strong>字节级的地址偏移</strong>：</p><ul><li><code>PGSIZE = 4096</code></li><li><code>p</code> 是 <code>char *</code>，所以 <code>p += 4096</code> 就是让地址前进 4096 <strong>字节</strong>（正好一页）</li></ul><p>如果 <code>p</code> 是 <code>uint64 *</code>，那 <code>p += 4096</code> 会让地址前进 <code>4096 × 8 = 32768</code> 字节，直接废了。</p></div><p>那么我们再看  <code>kfree()</code>：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 22 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 22 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Free the page of physical memory pointed at by pa,</span><span class="hljs-comment">// which normally should have been returned by a</span><span class="hljs-comment">// call to kalloc().  (The exception is when</span><span class="hljs-comment">// initializing the allocator; see kinit above.)</span><span class="hljs-type">void</span><span class="hljs-title function_">kfree</span><span class="hljs-params">(<span class="hljs-type">void</span> *pa)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  <span class="hljs-keyword">if</span>(((uint64)pa % PGSIZE) != <span class="hljs-number">0</span> || (<span class="hljs-type">char</span>*)pa &lt; end || (uint64)pa &gt;= PHYSTOP)    panic(<span class="hljs-string">"kfree"</span>);  <span class="hljs-comment">// Fill with junk to catch dangling refs.</span>  <span class="hljs-built_in">memset</span>(pa, <span class="hljs-number">1</span>, PGSIZE);  r = (<span class="hljs-keyword">struct</span> run*)pa;  acquire(&amp;kmem.lock);  r-&gt;next = kmem.freelist;  kmem.freelist = r;  release(&amp;kmem.lock);}</code></pre></div></details><p>首先是一个合理性校验，虽然读 OSTEP 的时候说，这一块主要是硬件 MMU 实现的，不知道为什么这里在 OS 代码层面实现了。</p><p>然后是把这一块内存填满垃圾。</p><p>最后，这一块就是保证原本的 <code>r-&gt;next</code> 不丢失，从而释放掉原本的 <code>pa</code> 处的页。</p><div class="note note-warning"><p>...太久没手写代码，这个地方都思考了一会儿。得写点 Leetcode 的链表题了。</p></div><p>干脆读完，看 <code>kalloc</code> 函数：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 18 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 18 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Allocate one 4096-byte page of physical memory.</span><span class="hljs-comment">// Returns a pointer that the kernel can use.</span><span class="hljs-comment">// Returns 0 if the memory cannot be allocated.</span><span class="hljs-type">void</span> *<span class="hljs-title function_">kalloc</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">r</span>;</span>  acquire(&amp;kmem.lock);  r = kmem.freelist;  <span class="hljs-keyword">if</span>(r)    kmem.freelist = r-&gt;next;  release(&amp;kmem.lock);  <span class="hljs-keyword">if</span>(r)    <span class="hljs-built_in">memset</span>((<span class="hljs-type">char</span>*)r, <span class="hljs-number">5</span>, PGSIZE); <span class="hljs-comment">// fill with junk</span>  <span class="hljs-keyword">return</span> (<span class="hljs-type">void</span>*)r;}</code></pre></div></details><p>别的没什么，这里我开始有个疑惑：这里明明是分配的栈上的指针 <code>r</code>，函数结束之后不应该就消失了吗？</p><p>但是实际上，<code>r</code> 存的是地址，不是内存本身。这个指针变量确实在栈上，函数返回后它作为局部变量就销毁了。</p><p>总之，读完这部分代码之后，再去理解「<em><strong>预留</strong></em>」：实际上就是 <code>freerange</code> 这一块：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">for</span>(; p + PGSIZE &lt;= (<span class="hljs-type">char</span>*)pa_end; p += PGSIZE)</code></pre></div><p>每次只按照 <code>PGSIZE</code> 为单位进行分配，那当然就没有 superpage 的份了。</p><p>那么我们动手开始实现：</p><p>仿照 4KB 页的 <code>freelist</code> 照抄一个：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">spinlock</span> <span class="hljs-title">lock</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">run</span> *<span class="hljs-title">freelist</span>;</span>} ksupermem;</code></pre></div><p>顺便，在 <code>kernel/riscv.h</code> 里面定义一下 hint 中所说的 <code>a handful of</code> 具体是多少，考虑到 xv6 的内存没多大，姑且设置成 8 个：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">define</span> SUPERPGAMOUNT 16</span></code></pre></div><p>然后，逻辑基本仿照即可。</p><p>这里留一个疑问，设置 <code>cnt_superpage</code> 是在 <code>superfree</code> 还是 <code>superalloc</code>？之后解决。</p><p>再看下两个 hint（一块实现）：</p><blockquote><p>支持大页的进程在 fork 时需分配大页，在退出时释放大页；你需要修改 <code>uvmcopy()</code> 和 <code>uvmunmap()</code> 的相关逻辑。</p><p>当 <code>sbrk</code> 部分释放一个超级页（例如，释放一个超级页的最后 4096 字节）时，你需要将该超级页“降级”为普通页。</p></blockquote><p>读一下代码：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 27 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 27 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">void</span><span class="hljs-title function_">uvmunmap</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va, uint64 npages, <span class="hljs-type">int</span> do_free)</span>{  uint64 a;  <span class="hljs-type">pte_t</span> *pte;  <span class="hljs-type">int</span> sz = PGSIZE;                    <span class="hljs-comment">// 每次跳 4KB（后面 superpage 需要改这里）</span>  <span class="hljs-keyword">if</span>((va % PGSIZE) != <span class="hljs-number">0</span>)    panic(<span class="hljs-string">"uvmunmap: not aligned"</span>);  <span class="hljs-keyword">for</span>(a = va; a &lt; va + npages*PGSIZE; a += sz){    <span class="hljs-keyword">if</span>((pte = walk(pagetable, a, <span class="hljs-number">0</span>)) == <span class="hljs-number">0</span>)   <span class="hljs-comment">// ① 找 PTE</span>      <span class="hljs-keyword">continue</span>;                              <span class="hljs-comment">//    没找到 → 跳过（允许不存在）</span>    <span class="hljs-keyword">if</span>((*pte &amp; PTE_V) == <span class="hljs-number">0</span>)                  <span class="hljs-comment">// ② V 位没置</span>      <span class="hljs-keyword">continue</span>;                              <span class="hljs-comment">//    已失效 → 跳过</span>    sz = PGSIZE;                             <span class="hljs-comment">// ③ 重置 sz（后面 superpage 需要改这里）</span>    <span class="hljs-keyword">if</span>(PTE_FLAGS(*pte) == PTE_V)      panic(<span class="hljs-string">"uvmunmap: not a leaf"</span>);         <span class="hljs-comment">// ④ 必须是叶节点</span>    <span class="hljs-keyword">if</span>(do_free){      uint64 pa = PTE2PA(*pte);      kfree((<span class="hljs-type">void</span>*)pa);                      <span class="hljs-comment">// ⑤ 释放物理页</span>    }    *pte = <span class="hljs-number">0</span>;                                <span class="hljs-comment">// ⑥ 清空 PTE</span>  }}</code></pre></div></details><div class="note note-info"><p><strong>注意这里的 <code>do_free</code> 参数</strong>：</p><ul><li>如果是普通用户内存页：<ul><li>物理页由 <code>uvmalloc</code> / <code>kalloc</code> 分配</li><li>解除映射时必须同时释放物理内存</li><li><code>do_free = 1</code></li></ul></li><li>如果是特殊页 (<code>trampoline</code> / <code>trapframe</code> / <code>usyscall</code>)<ul><li>物理页被多个进程共享</li><li>或者物理页的指针存在 proc 结构体里，单独管理</li><li>物理内存由别处负责释放</li><li><code>do_free = 0</code></li></ul></li></ul><p>看例子：<code>kernel/proc.c</code> 里面：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// 场景：创建页表，先 map 了 trampoline，然后 map trapframe 失败了</span><span class="hljs-keyword">if</span>(mappages(pagetable, TRAPFRAME, ...) &lt; <span class="hljs-number">0</span>){    uvmunmap(pagetable, TRAMPOLINE, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);   <span class="hljs-comment">// ← do_free=0</span>    uvmfree(pagetable, <span class="hljs-number">0</span>);    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div><p>这里就是 <code>do_free = 0</code>，因为 <strong>trampoline</strong> 的物理页不能释放。</p></div><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 34 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 34 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">uvmcopy</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> old, <span class="hljs-type">pagetable_t</span> new, uint64 sz)</span>{  <span class="hljs-type">pte_t</span> *pte;  uint64 pa, i;  uint flags;  <span class="hljs-type">char</span> *mem;  <span class="hljs-type">int</span> szinc = PGSIZE;              <span class="hljs-comment">// 步长（后面 superpage 需要改这里）</span>  <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; i &lt; sz; i += szinc){    <span class="hljs-keyword">if</span>((pte = walk(old, i, <span class="hljs-number">0</span>)) == <span class="hljs-number">0</span>)    <span class="hljs-comment">// ① 在父进程页表中找 i 对应的 PTE</span>      <span class="hljs-keyword">continue</span>;                          <span class="hljs-comment">//    没有 → 跳过</span>    <span class="hljs-keyword">if</span>((*pte &amp; PTE_V) == <span class="hljs-number">0</span>)             <span class="hljs-comment">// ② PTE 无效</span>      <span class="hljs-keyword">continue</span>;                          <span class="hljs-comment">//    跳过</span>    szinc = PGSIZE;                      <span class="hljs-comment">// ③ 重置步长</span>    pa = PTE2PA(*pte);                   <span class="hljs-comment">// ④ 提取物理地址</span>    flags = PTE_FLAGS(*pte);             <span class="hljs-comment">// ⑤ 提取权限位</span>    <span class="hljs-keyword">if</span>((mem = kalloc()) == <span class="hljs-number">0</span>)            <span class="hljs-comment">// ⑥ 分配新物理页</span>      <span class="hljs-keyword">goto</span> err;    memmove(mem, (<span class="hljs-type">char</span>*)pa, PGSIZE);     <span class="hljs-comment">// ⑦ 把父进程物理页内容拷贝过来</span>    <span class="hljs-keyword">if</span>(mappages(new, i, PGSIZE, (uint64)mem, flags) != <span class="hljs-number">0</span>){  <span class="hljs-comment">// ⑧ 映射到子进程页表</span>      kfree(mem);      <span class="hljs-keyword">goto</span> err;    }  }  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>; err:  uvmunmap(new, <span class="hljs-number">0</span>, i / PGSIZE, <span class="hljs-number">1</span>);      <span class="hljs-comment">// 失败回滚：释放已分配的所有页</span>  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div></details><p>现在两个函数都<strong>硬编码了 4KB 粒度</strong>（<code>szinc = PGSIZE</code>，<code>sz = PGSIZE</code>）。支持 superpage 后，<code>walk</code> 可能返回的是一个 L2 级别的 leaf PTE（代表 2MB 大页），所以才需要修改。</p><p>但是，这里有个问题：如何判断一个 PTE 是不是 superpage？LLM 的给的说法是，如果在 L1 层就发现这个 PTE 是 leaf page（L2 -&gt; L1 -&gt; L0），那么就是 superpage。</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// return the page size of a specific pte</span><span class="hljs-type">int</span><span class="hljs-title function_">pagesize</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va)</span>{  <span class="hljs-type">pte_t</span> *pte;  pte = &amp;pagetable[PX(<span class="hljs-number">2</span>, va)];  <span class="hljs-keyword">if</span> ((*pte &amp; PTE_V) == <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> PGSIZE;  }  <span class="hljs-type">pagetable_t</span> l1 = (<span class="hljs-type">pagetable_t</span>)PTE2PA(*pte);  pte = &amp;l1[PX(<span class="hljs-number">1</span>, va)];  <span class="hljs-keyword">if</span> ((*pte &amp; PTE_V) &amp;&amp; PTE_LEAF(*pte)) {    <span class="hljs-keyword">return</span> SUPERPGSIZE;  }  <span class="hljs-keyword">return</span> PGSIZE;}</code></pre></div><p>然后，接下来这一部分写了我快两天...</p><p>先分析一下 <code>uvmunmap</code>，作用是解除 <code>[va, va + npages*PGSIZE)</code> 的映射。这个 PTE 可能是 4KB 页也可能是 2MB superpage。</p><p>对于 4KB 页正常处理，对于 superpage：</p><ul><li><p>首先判断其是否是 superpage，方法就是 <code>if(pagesize(*pagetable*, a) == SUPERPGSIZE)</code></p></li><li><p>找到一些关键的参数：</p>  <div class="code-wrapper"><pre><code class="hljs c">uint64 sp_start = ((a % SUPERPGSIZE) == <span class="hljs-number">0</span>) ? a : SUPERPGROUNDDOWN(a); <span class="hljs-comment">// 这里注意一下，这个宏有问题</span>uint64 sp_end = sp_start + SUPERPGSIZE;uint64 unmap_end = va + npages * PGSIZE;</code></pre></div></li><li><p>然后，分三种情况：</p><ul><li><p>整块释放：<code>a == sp_start &amp;&amp; sp_end &lt;= unmap_end</code>，则就直接调用 <code>superfree</code> 即可</p></li><li><p>部分释放：这个地方就需要考虑把当前 superpage 给降级成一个或多个 4KB 页，这里实现一个函数 <code>split_superpage</code>：</p>  <details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 40 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 40 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Split a 2MB superpage into 512 individual 4KB pages.</span><span class="hljs-comment">// Pages within [unmap_start, unmap_end) are left unmapped (L0[j]=0).</span><span class="hljs-comment">// All other pages within the superpage are allocated new 4KB pages</span><span class="hljs-comment">// with data copied from the original 2MB block.</span><span class="hljs-comment">// The original 2MB physical block is freed.</span><span class="hljs-type">static</span> <span class="hljs-type">void</span><span class="hljs-title function_">split_superpage</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 sp_start,</span><span class="hljs-params">                uint64 unmap_start, uint64 unmap_end)</span>{  <span class="hljs-keyword">if</span> (pagesize(pagetable, sp_start) != SUPERPGSIZE) {    panic(<span class="hljs-string">"split_sp: not superpage"</span>);  }  uint64 sp_end = sp_start + SUPERPGSIZE;  unmap_end = unmap_end &lt; sp_end ? unmap_end : sp_end;  <span class="hljs-type">pte_t</span> *l2_pte = &amp;pagetable[PX(<span class="hljs-number">2</span>, sp_start)];  <span class="hljs-type">pagetable_t</span> l1 = (<span class="hljs-type">pagetable_t</span>)PTE2PA(*l2_pte);  <span class="hljs-type">pte_t</span> *sp_pte = &amp;l1[PX(<span class="hljs-number">1</span>, sp_start)];  uint64 sp_pa = PTE2PA(*sp_pte);  <span class="hljs-type">int</span> flags = PTE_FLAGS(*sp_pte);  <span class="hljs-type">pagetable_t</span> l0 = (<span class="hljs-type">pagetable_t</span>)kalloc();  <span class="hljs-keyword">if</span> (l0 == <span class="hljs-number">0</span>) {    panic(<span class="hljs-string">"split_sp: kalloc l0"</span>);  }  <span class="hljs-built_in">memset</span>((<span class="hljs-type">void</span>*)l0, <span class="hljs-number">0</span>, PGSIZE);  <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">512</span>; ++i) {    uint64 va = sp_start + i * PGSIZE;    <span class="hljs-keyword">if</span> (va &gt;= unmap_start &amp;&amp; va &lt; unmap_end) {      <span class="hljs-keyword">continue</span>;    }    uint64 new_page = (uint64) kalloc();    <span class="hljs-keyword">if</span> (new_page == <span class="hljs-number">0</span>) {      panic(<span class="hljs-string">"split_sp: kalloc page"</span>);    }    memmove((<span class="hljs-type">void</span>*)new_page, (<span class="hljs-type">void</span>*)(sp_pa + i * PGSIZE), PGSIZE);    l0[i] = PA2PTE(new_page) | flags | PTE_V;  }  *sp_pte = PA2PTE(l0) | PTE_V;  superfree((<span class="hljs-type">void</span>*)sp_pa);}</code></pre></div></details><p>大致的思路就是原本是 L2 指向的 L1 后面直接指向一个 superpage，现在需要把这个 superpage 拆散成若干普通页，这些普通页记录在一个 L0 页表里面，最后把原本 L1 指向的 PTE （就是 superpage 的物理地址）直接修改为重新分配到的 L0 的位置</p></li></ul></li></ul><p>然后是修改 <code>uvmcopy</code>：</p><p>原本的函数只是用 <code>mappages</code> 来建立虚拟地址到物理地址的映射，且只使用 4KB 页。为了支持 superpage，在 <code>uvmcopy</code> 中，如果原本的进程处的页是一个 superpage，子进程这里也应该是一个 superpage，所以我们必须要先创建一个 <code>map_superpage</code>：</p><p>关于 <code>map_superpage</code>，无非就是 <code>split_superpage</code> 的<strong>逆操作</strong>：在子进程页表里<strong>新建</strong>一个 L1 叶子 PTE。</p><ul><li>首先取到 L1 页表，如果不存在自行创建（为什么 <code>mappages</code> 不需要「不存在则自行创建」这一步？因为在 <code>walk</code> 中解决了）</li><li>然后，在 L1 页表的 PTE，原本该指向 L0 页表的 pte 的位置处，改为 <code>PA2PTE(*pa*) | *perm* | PTE_V</code></li><li>为什么这是一个 superpage？因为通过设置 flags，在 L1 层的 leaf page 当然就是一个 superpage。</li><li>实现完这个之后，修改 <code>uvmcopy</code> 其实就照抄 4KB 的逻辑差不多了。</li></ul><div class="note note-info"><p>可以看到，和 <code>mappages</code> 相比，其实就是去掉了循环，然后提前一层实现了映射，顺便处理了原本该由 <code>walk</code> 函数处理的部分逻辑。</p></div><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 23 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 23 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// Create a 2MB superpage mapping at L1.</span><span class="hljs-comment">// va and pa must be 2MB-aligned. Returns 0 on success, -1 on failure.</span><span class="hljs-type">static</span> <span class="hljs-type">int</span><span class="hljs-title function_">map_superpage</span><span class="hljs-params">(<span class="hljs-type">pagetable_t</span> pagetable, uint64 va, uint64 pa, <span class="hljs-type">int</span> perm)</span>{  <span class="hljs-comment">// Ensure the L1 page table exists (L2 already points to it)</span>  <span class="hljs-type">pte_t</span> *l2 = &amp;pagetable[PX(<span class="hljs-number">2</span>, va)];  <span class="hljs-type">pagetable_t</span> l1;  <span class="hljs-keyword">if</span>(*l2 &amp; PTE_V){    l1 = (<span class="hljs-type">pagetable_t</span>)PTE2PA(*l2);  } <span class="hljs-keyword">else</span> {    <span class="hljs-keyword">if</span>((l1 = (<span class="hljs-type">pagetable_t</span>)kalloc()) == <span class="hljs-number">0</span>)      <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;    <span class="hljs-built_in">memset</span>(l1, <span class="hljs-number">0</span>, PGSIZE);    *l2 = PA2PTE(l1) | PTE_V;  }  <span class="hljs-type">pte_t</span> *l1_pte = &amp;l1[PX(<span class="hljs-number">1</span>, va)];  <span class="hljs-keyword">if</span>(*l1_pte &amp; PTE_V)    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  <span class="hljs-comment">// already mapped</span>  *l1_pte = PA2PTE(pa) | perm | PTE_V;  <span class="hljs-comment">// R/W/X bits → hardware sees a leaf</span>  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div></details><p>最后，还需要改 <code>uvmalloc</code> 函数，因为目前还没有处理关于超级页的分配的逻辑。</p><div class="note note-info"><p>这个其实自己没想到因为 hint 里面没有，还是对 <code>vm.c</code> 的呼叫依赖关系太不敏感了。</p></div><p>这里也不麻烦，如果剩余的范围有 2MB 并且地址是对齐的，就尝试调用 <code>superalloc</code> 分配一个，这里的逻辑和下面调用 <code>kalloc</code> 以分配正常页的逻辑没什么区别。</p><p>至此就完成了，测试的时候，先 <code>make qemu</code>，再输入 <code>pgtbltest</code>。</p><div class="note note-warning"><p>后记一下的话，首先我觉得应该实现一个 <code>superwalk</code> 函数，然后在这里面判断一个 PTE 是否对应一个 superpage，顺便还能简化 <code>map_superpage</code> 的逻辑。</p></div><img src="https://image.wendaining.top/8602f1d7-882e-499d-bf5a-61d8b8ac8e89.png" alt="通过所有测试" style="zoom:67%;">]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab2 System calls - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/07/26/xv6-lab2-system-calls/</link>
      <description>xv6 的第二个 lab，熟悉内核启动的流程，以及一个系统调用从用户态被调用之后，究竟发生了什么的一个全过程。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Sun, 26 Jul 2026 01:50:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="阅读-xv6-book">阅读 xv6 book</h2><p>操作系统主要解决三个要求：</p><ul><li><strong>multiplexing</strong>：多个程序共享 CPU、内存等资源；</li><li><strong>isolation</strong>：一个程序不能破坏内核或其他程序；</li><li><strong>interaction</strong>：程序仍然需要通过管道等机制进行受控通信。</li></ul><p>因此出现了这些抽象：</p><table><thead><tr><th>物理资源</th><th>提供给程序的抽象</th></tr></thead><tbody><tr><td>CPU</td><td>进程、线程</td></tr><tr><td>物理内存</td><td>地址空间</td></tr><tr><td>磁盘</td><td>文件系统</td></tr><tr><td>进程间通信</td><td>管道、文件描述符</td></tr></tbody></table><p>RISC-V 的特权级：</p><ul><li><strong>machine mode</strong>：启动阶段使用，权限最高；</li><li><strong>supervisor mode</strong>：xv6 内核运行于此；</li><li><strong>user mode</strong>：普通应用运行于此。</li></ul><p>需要内核服务时，在 user mode 执行 <code>ecall</code> 陷入内核态。</p><p><strong>宏内核</strong>：</p><ul><li>文件系统、进程管理、内存管理、驱动等都运行在内核态。</li><li>优点是调用直接、模块协作方便、性能较好；缺点是内核庞大，任何严重 bug 都可能让整个系统崩溃。</li><li>xv6 是宏内核，所有核心模块都在 <code>kernel/</code> 中直接互相调用。</li></ul><p><strong>微内核</strong>：</p><ul><li>内核只保留调度、地址空间、IPC 等基本机制；文件系统等作为用户态服务器运行，通过消息通信</li><li>优点是内核更小、更容易隔离和验证；缺点是跨进程通信会增加实现和性能开销。</li></ul><p><strong>xv6 源码的组织</strong>：</p><table><thead><tr><th>领域</th><th>主要文件</th></tr></thead><tbody><tr><td>启动</td><td><code>entry.S</code>、<code>start.c</code>、<code>main.c</code></td></tr><tr><td>进程与调度</td><td><code>proc.c</code>、<code>swtch.S</code></td></tr><tr><td>系统调用</td><td><code>syscall.c</code>、<code>sysproc.c</code>、<code>sysfile.c</code></td></tr><tr><td>trap</td><td><code>trampoline.S</code>、<code>kernelvec.S</code>、<code>trap.c</code></td></tr><tr><td>虚拟内存</td><td><code>vm.c</code></td></tr><tr><td>物理内存</td><td><code>kalloc.c</code></td></tr><tr><td>文件系统</td><td><code>fs.c</code>、<code>file.c</code>、<code>log.c</code>、<code>bio.c</code></td></tr><tr><td>设备</td><td><code>console.c</code>、<code>uart.c</code>、<code>virtio_disk.c</code></td></tr></tbody></table><p><strong>xv6 中的进程</strong>：</p><p>每个进程在内核中有一个 <code>struct proc</code>，其中重要内容包括：</p><ul><li><code>pagetable</code>：用户地址空间；</li><li><code>kstack</code>：进入内核后使用的内核栈；</li><li><code>trapframe</code>：保存用户寄存器；</li><li><code>context</code>：内核线程上下文；</li><li><code>state</code>：运行、可运行、睡眠等状态。</li></ul><p>进程有两套栈：</p><ul><li>用户态运行：使用 user stack</li><li>进入内核：使用该进程自己的 kernel stack</li></ul><p>用户栈即使损坏，也不应该影响内核栈。xv6 中一个进程只有一个线程，现代系统通常允许一个进程拥有多个线程。</p><p><strong>调用一个系统调用的过程的例子</strong>：</p><p>调用 <code>write(2, "$", 2);</code>：</p><div class="code-wrapper"><pre><code class="hljs text">用户 C 代码调用 write()        ↓user/usys.S 中的 write 标签 （这一步是通过链接实现的）        ↓a7 = SYS_write        ↓ecall        ↓uservec → usertrap → syscall()        ↓syscalls[SYS_write]        ↓sys_write()        ↓返回值写入 trapframe-&gt;a0        ↓恢复用户寄存器，回到 write() 后面</code></pre></div><p>RISC-V 调用约定中：</p><ul><li><code>a0</code>、<code>a1</code>、<code>a2</code>……保存函数参数；</li><li><code>a7</code> 保存系统调用编号；</li><li><code>a0</code> 同时用来保存返回值。</li></ul><p><strong>系统调用中汇编代码的联系方式</strong>：</p><p>user.h 中的声明 + usys.S 中的汇编包装函数 + 链接器将两者对应起来</p><h3 id="xv6-的启动">xv6 的启动</h3><p>这一块是我写 lab 3 的时候感觉没弄懂的，所以回来补一下。</p><p>参考的资料：</p><ul><li><a href="https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec03-os-organization-and-system-calls/3.9-xv6-qi-dong-guo-cheng">3.9 XV6 启动过程 | MIT6.S081</a></li><li><a href="https://zhuanlan.zhihu.com/p/651170875">MIT6.s081/6.828 lectrue02：OS design 以及 Lab2 心得 - 逆风的大船的文章 - 知乎</a></li><li><a href="https://copicomi.github.io/posts/%E9%87%8D%E8%AF%BB-xv6/i/">重读 xv6（I） | Anri’s blog</a></li></ul><h4 id="QEMU">QEMU</h4><p>首先我们需要理解什么是 QEMU。</p><p>应该把 QEMU 想象为一个真正的，基于 RISC-V 的计算机对待。</p><p>但是，直观上来看，QEMU 是一个开源 C 语言程序，内部的核心就是一个 <code>while(1)</code> 循环，执行：</p><ul><li>读取 RISC-V 指令</li><li>解析，找到对应的 op code</li><li>在软件中执行相应的指令。</li></ul><h4 id="xv6-启动流程">xv6 启动流程</h4><p>RISC-V 有三种 CPU 执行指令的模式：<strong>机器模式、内核模式、用户模式</strong>。</p><p>启动流程：</p><ol><li>机器启动，将 kernel 加载到内存</li><li>运行 <code>entry.S</code>，为每个 CPU 设置运行栈，此时为 <em><strong>machine mode</strong></em></li><li>运行 <code>start</code>，进行一些初始配置，切换为 <em><strong>kernel mode</strong></em></li><li>运行 <code>main</code>，初始化各个模块，创建首个进程，切换为 <em><strong>user mode</strong></em></li><li>运行 <code>init</code>，设置标准 I/O，启动 <code>shell</code>，开始提供服务</li></ol><p><code>start</code> ：</p><ul><li><strong>时钟中断</strong>，用于虚拟化 CPU，切换进程</li><li><strong>页表映射</strong>，用于虚拟化内存地址</li></ul><p><code>main</code> ：</p><ul><li><strong>内存池、分页</strong>，用于分配用户空间，虚拟化内存</li><li><strong>trap 中断</strong>，用于响应异常，实现系统调用</li><li><strong>进程管理</strong>，用于分配资源，管理进程</li><li><strong>文件系统</strong>，用于控制文件读写</li><li><strong>PLIC、设备驱动、磁盘</strong></li></ul><p>实际远比这些复杂，之后会单开一个 blog 文章解析一下此过程，TODO。</p><ul><li>找到一个很不错的 b 站视频：<a href="https://www.bilibili.com/video/BV1PL411N7u9/?share_source=copy_web&amp;vd_source=60b44c8f21723cac51d65c96f7acfb00">【04(1)-xv6操作系统-系统启动-1（bootblock）】 </a>，之后评鉴一下。</li></ul><h2 id="Using-gdb">Using gdb</h2><p>一系列练习使用 gdb 的回答练习。</p><p>这一段说实话有点无聊，因为我之前没有很好的，关于使用 gdb debug 的经历。所以我大部分是让 LLM 指导我做的。</p><img src="https://image.wendaining.top/image-20260726163250240.png" alt="belike" style="zoom:33%;"><p>答题：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 TEXT · 30 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">TEXT · 30 行</span></summary><div class="code-wrapper"><pre><code class="hljs txt">Looking at the backtrace output, which function called syscall?usertrap() at kernel/trap.c:68What is the value of p-&gt;trapframe-&gt;a7 and what does that value represent? (Hint: look at user/init.c, the first user program xv6 starts, and its compiled assembly user/init.asm.)$4 = 0xf, 当前进程 p 进入内核时，保存下来的用户态寄存器 a7 的值。0xf = 15，而 SYS_OPEN = 15，所以代表 open 系统调用。What was the previous mode that the CPU was in?用户态Write down the assembly instruction the kernel is panicing at. Which register corresponds to the variable num?得到：scause=0xd sepc=0x80001cf8 stval=0x0panic: kerneltrap再得到：4410:    80001cf8:      00002683                lw      a3,0(zero) # 0 &lt;_entry-0x80000000&gt;a3 对应 numWhy does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? Is that confirmed by the value in scause above?代码 *(int *)0 要求 CPU 从虚拟地址 0 读取数据，但内核页表没有映射地址 0，于是发生 page fault。What is the name of the process that was running when the kernel paniced? What is its process id (pid)?(gdb)p p-&gt;name$1 "init",\000'&lt;repeats 11 times&gt;(gdb)p/d p-&gt;pid$2=1</code></pre></div></details><p>一个疑惑：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 1538 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">scause=0xd sepc=0x80001cf8 stval=0x0 panic: kerneltrap 这个分别代表什么，是怎么知道的？...</span></summary><blockquote><p>scause=0xd sepc=0x80001cf8 stval=0x0 panic: kerneltrap 这个分别代表什么，是怎么知道的？</p><p>把这三项理解成一次异常的“现场报告”：</p><div class="code-wrapper"><pre><code class="hljs text">scause = 为什么出错sepc   = 哪条指令出错stval  = 访问什么地址时出错</code></pre></div><p>你这里是：</p><div class="code-wrapper"><pre><code class="hljs text">scause=0xd sepc=0x80001cf8 stval=0x0panic: kerneltrap</code></pre></div><p><code>scause = 0xd</code></p><p><code>scause</code> 表示 trap 的原因。</p><div class="code-wrapper"><pre><code class="hljs text">0xd = 十进制 13 = Load page fault</code></pre></div><p>也就是：</p><blockquote><p>CPU 在<strong>读取内存</strong>时发生了页错误。</p></blockquote><p>这与故意加入的代码完全对应：</p><div class="code-wrapper"><pre><code class="hljs c">num = *(<span class="hljs-type">int</span> *)<span class="hljs-number">0</span>;</code></pre></div><p>它试图从内存地址 <code>0</code> 读取一个 <code>int</code>。RISC-V 规定异常编号 13 表示 load page fault。(<a href="https://docs.riscv.org/reference/isa/_attachments/riscv-privileged.pdf?utm_source=chatgpt.com">RISC-V Documentation</a>)</p><p><code>stval = 0x0</code></p><p>对于 page fault，<code>stval</code> 通常记录导致错误的虚拟地址。</p><p>这里是：</p><div class="code-wrapper"><pre><code class="hljs text">stval = 0x0</code></pre></div><p>所以可以确定：</p><blockquote><p>出错的内存访问目标是虚拟地址 <code>0</code>。</p></blockquote><p>也就是代码中的：</p><div class="code-wrapper"><pre><code class="hljs c">*(<span class="hljs-type">int</span> *)<span class="hljs-number">0</span>        ^</code></pre></div><p>内核页表没有映射地址 <code>0</code>，因此读取失败。Lab 正是让你通过 <code>scause=0xd</code> 和 <code>stval=0</code> 判断这是空地址读取造成的 page fault。(<a href="https://pdos.csail.mit.edu/6.828/2024/labs/syscall.html">MIT CSAIL</a>)</p><p><code>sepc = 0x80001cf8</code></p><p><code>sepc</code> 保存发生异常时正在执行的指令地址。</p><p>这里表示：</p><blockquote><p>位于内核地址 <code>0x80001cf8</code> 的那条机器指令触发了异常。</p></blockquote><p>因此 Lab 让你拿这个地址去查编译后的汇编：</p><div class="code-wrapper"><pre><code class="hljs bash">grep -n -B 4 -A 4 <span class="hljs-string">"80001cf8"</span> kernel/kernel.asm</code></pre></div><p>你应该会找到类似：</p><div class="code-wrapper"><pre><code class="hljs asm">80001cf8:  ...    lw a?,0(zero)</code></pre></div><p>其中：</p><div class="code-wrapper"><pre><code class="hljs asm">lw a?, 0(zero)</code></pre></div><p>意思是从地址：</p><div class="code-wrapper"><pre><code class="hljs text">zero + 0 = 0</code></pre></div><p>读取数据。Lab 明确要求用打印出的 <code>sepc</code> 在 <code>kernel/kernel.asm</code> 中定位故障指令。(<a href="https://pdos.csail.mit.edu/6.828/2024/labs/syscall.html">MIT CSAIL</a>)</p><p>也可以在 GDB 中直接停在这里：</p><div class="code-wrapper"><pre><code class="hljs gdb">b *0x80001cf8cx/i $pc</code></pre></div><p><code>panic: kerneltrap</code></p><p>异常发生时，CPU 正在执行内核代码，所以它进入：</p><div class="code-wrapper"><pre><code class="hljs c">kerneltrap()</code></pre></div><p><code>kerneltrap()</code> 会尝试判断是不是正常的设备或时钟中断。这个 page fault 不是可正常处理的设备中断，于是 xv6 打印三个寄存器并执行：</p><div class="code-wrapper"><pre><code class="hljs c">panic(<span class="hljs-string">"kerneltrap"</span>);</code></pre></div><p>所以整个推理过程是：</p><div class="code-wrapper"><pre><code class="hljs text">scause = 0xd→ 是读取内存产生的 page faultstval = 0→ 读取的是虚拟地址 0sepc = 0x80001cf8→ 去 kernel/kernel.asm 找这个地址的指令最终定位→ num = *(int *)0 导致内核读取未映射地址 0</code></pre></div><p>你在答案文件中可以写：</p><div class="code-wrapper"><pre><code class="hljs text">scause 0xd indicates a load page fault.stval 0x0 shows that the faulting virtual address was zero.sepc 0x80001cf8 is the address of the faulting instruction, which canbe located in kernel/kernel.asm. The kernel crashes because it triesto load data from unmapped virtual address 0.</code></pre></div></blockquote></details><h2 id="Sandbox-a-Command">Sandbox a Command</h2><p>这感觉主要就是建立认知和 insight，但是感觉文档给的引导不足，或者是我太菜了（大概是这样。）</p><ul><li>二遍：其实 xv6 book 里面写的很清楚，但是我读书不仔细。不过也无所谓，写完也清楚机制了。</li></ul><p>一个问题：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 2243 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">struct proc 是什么？sysproc.c 是干嘛的？ kfork kexit 这些都是啥？和 fork 系统调用区别是什么？话说回来这...</span></summary><blockquote><ol><li>struct proc 是什么？sysproc.c 是干嘛的？</li><li>kfork kexit 这些都是啥？和 fork 系统调用区别是什么？话说回来这就是fork的实现的地方吗？那是如何指向的？感觉我脑子里一团乱</li></ol><p><strong>Answer</strong>：</p><p><code>struct proc</code> 是什么？</p><p><code>struct proc</code> 定义在 <code>kernel/proc.h</code>，表示内核眼中的一个进程。</p><p>可以把它理解为：<strong>进程对应的一份内核记录</strong></p><p>里面保存这个进程的各种信息，例如：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> {</span>  <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">procstate</span> <span class="hljs-title">state</span>;</span>        <span class="hljs-comment">// 进程状态</span>  <span class="hljs-type">int</span> pid;                     <span class="hljs-comment">// 进程号</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">parent</span>;</span>         <span class="hljs-comment">// 父进程</span>  <span class="hljs-type">pagetable_t</span> pagetable;       <span class="hljs-comment">// 用户页表</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">trapframe</span> *<span class="hljs-title">trapframe</span>;</span> <span class="hljs-comment">// 用户寄存器</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">context</span> <span class="hljs-title">context</span>;</span>      <span class="hljs-comment">// 内核调度所需的寄存器</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">file</span> *<span class="hljs-title">ofile</span>[<span class="hljs-title">NOFILE</span>];</span>  <span class="hljs-comment">// 打开的文件</span>  <span class="hljs-type">char</span> name[<span class="hljs-number">16</span>];               <span class="hljs-comment">// 进程名称</span>};</code></pre></div><p>因此：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();</code></pre></div><p>意思是：<strong>取得当前正在运行进程的 struct proc</strong></p><p>之后：</p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;pidp-&gt;trapframep-&gt;state</code></pre></div><p>就是读取当前进程的信息。</p><p><code>struct proc</code> 只是数据结构。对进程进行创建、调度、退出等操作的代码，主要在 <code>kernel/proc.c</code>。</p><hr><p><code>sysproc.c</code> 是干什么的？</p><p><code>kernel/sysproc.c</code> 放的是与进程有关的系统调用入口，例如：</p><div class="code-wrapper"><pre><code class="hljs c">sys_fork()sys_wait()sys_getpid()sys_kill()</code></pre></div><p>这些 <code>sys_xxx()</code> 函数负责接收系统调用，然后调用内核内部的实现。</p><p>例如：</p><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_fork</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-keyword">return</span> kfork();}</code></pre></div><p>这里：</p><ul><li><code>sys_fork()</code>：系统调用入口</li><li><code>kfork()</code>：真正创建进程的内核函数</li></ul><p><code>sysproc.c</code> 可以理解为系统调用层和进程管理代码之间的连接层。</p><hr><p><code>fork()</code> 和 <code>kfork()</code> 有什么区别？</p><p>用户程序调用的是：</p><div class="code-wrapper"><pre><code class="hljs c">fork();</code></pre></div><p>内核真正实现进程创建的是：</p><div class="code-wrapper"><pre><code class="hljs c">kfork();</code></pre></div><p>完整路径是：</p><div class="code-wrapper"><pre><code class="hljs text">用户程序 fork()    ↓user/usys.S 中的 fork 汇编入口    ↓a7 = SYS_forkecall    ↓usertrap()    ↓syscall()    ↓syscalls[SYS_fork]    ↓sys_fork()    ↓kfork()</code></pre></div><hr><p><code>fork()</code> 如何指向 <code>sys_fork()</code>？</p><p>用户态的 <code>fork()</code> 汇编入口大致是：</p><div class="code-wrapper"><pre><code class="hljs asm">fork:  li a7, SYS_fork  ecall  ret</code></pre></div><p>它没有直接调用 <code>sys_fork()</code>，而是：</p><div class="code-wrapper"><pre><code class="hljs text">把系统调用编号 SYS_fork 放进 a7然后执行 ecall 进入内核</code></pre></div><p>内核中的 <code>syscall()</code> 读取这个编号：</p><div class="code-wrapper"><pre><code class="hljs c">num = p-&gt;trapframe-&gt;a7;</code></pre></div><p>然后查询系统调用表：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">static</span> <span class="hljs-title function_">uint64</span> <span class="hljs-params">(*syscalls[])</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> = {  [SYS_fork] sys_fork,  [SYS_exit] sys_exit,  [SYS_wait] sys_wait,};</code></pre></div><p>这一项：</p><div class="code-wrapper"><pre><code class="hljs c">[SYS_fork] sys_fork</code></pre></div><p>建立了 <code>SYS_fork</code> 这个编号 → <code>sys_fork()</code> 函数</p><p>之后：</p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;trapframe-&gt;a0 = syscalls[num]();</code></pre></div><p>对于 <code>fork</code> 来说，相当于：</p><div class="code-wrapper"><pre><code class="hljs c">p-&gt;trapframe-&gt;a0 = sys_fork();</code></pre></div><hr><p><code>kfork()</code> 是不是 <code>fork</code> 真正实现的地方？</p><p>是。</p><p><code>kfork()</code> 位于：</p><div class="code-wrapper"><pre><code class="hljs text">kernel/proc.c</code></pre></div><p>它主要完成：</p><ol><li>分配一个新的 struct proc</li><li>复制父进程的用户内存</li><li>复制父进程的 trapframe</li><li>复制打开的文件</li><li>设置父子关系</li><li>将子进程设置为 RUNNABLE</li></ol><p>简化后的结构是：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 19 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 19 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span><span class="hljs-title function_">kfork</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">p</span> =</span> myproc();  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">proc</span> *<span class="hljs-title">np</span>;</span>  np = allocproc();  uvmcopy(p-&gt;pagetable, np-&gt;pagetable, p-&gt;sz);  *(np-&gt;trapframe) = *(p-&gt;trapframe);  np-&gt;trapframe-&gt;a0 = <span class="hljs-number">0</span>;  np-&gt;parent = p;  np-&gt;state = RUNNABLE;  <span class="hljs-keyword">return</span> np-&gt;pid;}</code></pre></div></details><p>因此最需要记住的是：</p><div class="code-wrapper"><pre><code class="hljs text">fork()→ ecall→ syscall()→ sys_fork()→ kfork()</code></pre></div><p>相关文件关系：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 TEXT · 20 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">TEXT · 20 行</span></summary><div class="code-wrapper"><pre><code class="hljs text">user/user.h    fork() 的 C 声明user/usys.pl    生成 fork 的用户态汇编入口kernel/syscall.h    定义 SYS_fork 编号kernel/syscall.c    SYS_fork → sys_forkkernel/sysproc.c    sys_fork() 调用 kfork()kernel/proc.c    kfork() 真正创建进程kernel/proc.h    定义 struct proc</code></pre></div></details></blockquote></details><p>照着 hint 一步一步实现，然后有一步需要注意：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (p-&gt;syscall_mask &amp; (<span class="hljs-number">1</span> &lt;&lt; num)) {  p-&gt;trapframe-&gt;a0 = <span class="hljs-number">-1</span>;} <span class="hljs-keyword">else</span> {  p-&gt;trapframe-&gt;a0 = syscalls[num]();}</code></pre></div><p>我开始写的是：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span> (p-&gt;syscall_mask &amp; (<span class="hljs-number">1</span> &lt;&lt; num)) {  prinf(...) <span class="hljs-comment">// 一段错误日志</span>}</code></pre></div><p>系统调用返回用户态时，用户程序会把 <code>a0</code> 当作返回值。此时 <code>a0</code> 原本保存的通常还是系统调用的第一个参数。如果只打印日志，不修改 <code>a0</code>，返回用户态后，<code>open()</code> 可能把 <code>"README"</code> 的地址当作返回值，而不是得到失败的 <code>-1</code>。</p><p>总结一下，跟着 hint 一步一步走，我们究竟实现了什么：</p><h3 id="第一步">第一步</h3><blockquote><p>Add <code>$U/_sandbox</code> to UPROGS in Makefile将 <code>$U/_sandbox</code> 添加到 Makefile 中的 UPROGS 中</p></blockquote><p>没什么好说的，这相当于告知哪些用户程序需要被编译，并放进 xv6 的文件系统。</p><h3 id="第二步">第二步</h3><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 935 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">Run make qemu and you will see that the compiler cannot compile user/san...</span></summary><blockquote><p>Run make qemu and you will see that the compiler cannot compile <code>user/sandbox.c</code>, because the user-space stubs for the <code>interpose</code> system call don't exist yet: add a prototype for <code>interpose</code> to <code>user/user.h</code>, a stub to <code>user/usys.pl</code>, and a syscall number to <code>kernel/syscall.h</code>. The Makefile invokes the perl script <code>user/usys.pl</code>, which produces <code>user/usys.S</code>, the actual system call stubs, which use the RISC-V <code>ecall</code> instruction to transition to the kernel. Once you fix the compilation issues, run sandbox 32768 - cat README in the xv6 shell; it will fail because you haven't implemented the system call in the kernel yet.运行 make qemu ，你会发现编译器无法编译 <code>user/sandbox.c</code> ，因为 <code>interpose</code> 系统调用的用户空间存根尚不存在：在 <code>user/user.h</code> 中添加 <code>interpose</code> 的原型，在 <code>user/usys.pl</code> 中添加存根，并在 <code>kernel/syscall.h</code> 中添加系统调用号。Makefile 调用 Perl 脚本 <code>user/usys.pl</code> ，它会生成 <code>user/usys.S</code> ，即实际的系统调用存根，这些存根使用 RISC-V 的 <code>ecall</code> 指令来转换到内核。一旦你修复了编译问题，在 xv6 shell 中运行 sandbox 32768 - cat README ；它会失败，因为你尚未在内核中实现该系统调用。</p></blockquote></details><p>在 <code>user/user.h</code> 里提供一个用户的接口，只specify函数原型签名即可：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> <span class="hljs-title function_">interpose</span><span class="hljs-params">(<span class="hljs-type">int</span>, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *)</span>;</code></pre></div><p>在 <code>kernel/syscall.h</code> 里面添加系统调用号：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta"># kernel/syscall.h</span><span class="hljs-meta">#<span class="hljs-keyword">define</span> SYS_interpose 22</span></code></pre></div><p>然后，修改 <code>user/usys.pl</code>：</p><p>这里的 <code>pl</code> 后缀是 Perl 脚本，类似于这样：</p><div class="code-wrapper"><pre><code class="hljs perl"><span class="hljs-comment"># user/usys.pl</span>...省略...entry(<span class="hljs-string">"getpid"</span>);entry(<span class="hljs-string">"sbrk"</span>);entry(<span class="hljs-string">"pause"</span>);entry(<span class="hljs-string">"uptime"</span>);entry(<span class="hljs-string">"interpose"</span>);</code></pre></div><p>这究竟有什么用呢？<code>make</code> 会生成如这样的 stub：</p><div class="code-wrapper"><pre><code class="hljs assembly">.global interposeinterpose:  li a7, SYS_interpose  ecall  ret</code></pre></div><div class="note note-info"><p><code>stub</code> 在这里可以理解成：一小段“中转代码”或“占位入口”，自己不完成真正功能，只负责把调用转交到别处。</p><p>在 xv6 的系统调用里，<code>fork</code>、<code>sbrk</code>、<code>open</code> 这些用户态函数就是 syscall stub。</p><p>例如 <code>sbrk</code> 的 stub 大致是：</p><div class="code-wrapper"><pre><code class="hljs asm">sbrk:  li a7, SYS_sbrk  ecall  ret</code></pre></div><p>它做的事情很少：</p><div class="code-wrapper"><pre><code class="hljs text">把系统调用编号 SYS_sbrk 放进 a7执行 ecall 进入内核内核处理完后返回</code></pre></div><p>它自己并不负责分配内存。真正的工作由内核中的：</p><div class="code-wrapper"><pre><code class="hljs c">sys_sbrk()</code></pre></div><p>以及更底层的：</p><div class="code-wrapper"><pre><code class="hljs c">growproc()uvmalloc()kalloc()</code></pre></div><p>完成。</p><p>所以这条路径可以写成：</p><div class="code-wrapper"><pre><code class="hljs text">用户程序调用 sbrk()        ↓用户态 syscall stub        ↓ecall        ↓内核 syscall()        ↓sys_sbrk()</code></pre></div><p>为什么叫 <code>stub</code>，而不直接叫“实现”？</p><p>因为它非常薄，只是一个接口外壳：</p><div class="code-wrapper"><pre><code class="hljs text">stub：负责转交请求implementation：负责真正完成工作</code></pre></div><p>在 xv6 中：</p><div class="code-wrapper"><pre><code class="hljs text">user/user.h 中的声明    告诉 C 编译器函数怎么调用user/usys.pl 生成的 stub    把调用转换成 ecallkernel/sys_xxx()    真正接收和处理系统调用</code></pre></div><p>因此文档中的 <code>user-space stubs</code>，就是位于用户态、负责将普通函数调用转换成系统调用的一小段汇编入口。</p></div><h3 id="第三步">第三步</h3><blockquote><p>Add a <code>sys_interpose()</code> function in <code>kernel/sysproc.c</code> that implements the new system call by recording the mask argument in a new field in the <code>proc</code> structure (see <code>kernel/proc.h</code>). The functions to retrieve system call arguments from user space are in <code>kernel/syscall.c</code>, and you can see examples of their use in <code>kernel/sysproc.c</code>. Add your new <code>sys_interpose</code> to the <code>syscalls</code> array in <code>kernel/syscall.c</code>.在 <code>kernel/sysproc.c</code> 中添加一个 <code>sys_interpose()</code> 函数，通过将掩码参数记录在 <code>proc</code> 结构（见 <code>kernel/proc.h</code> ）的新字段中，来实现新的系统调用。从用户空间检索系统调用参数的函数位于 <code>kernel/syscall.c</code> 中，你可以在 <code>kernel/sysproc.c</code> 中看到它们的使用示例。将新的 <code>sys_interpose</code> 添加到 <code>kernel/syscall.c</code> 的 <code>syscalls</code> 数组中。</p></blockquote><p>修改 <code>struct proc</code>：</p><p>容易见到这个限制的掩码是一个<strong>每进程变量</strong>，因此要放在 <code>struct proc</code> 里面。</p><p>关于这个结构体是什么，回去看 read xv6 book 部分的「xv6 中的进程」。</p><p>于是，进入 <code>kernel/proc.h</code>，加入一个 <code>int syscall_mask</code> 字段。</p><p>修改 <code>kernel/syscall.c</code>：</p><p>首先在其他系统调用的声明附近加上</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">extern</span> uint64 <span class="hljs-title function_">sys_interpose</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>;</code></pre></div><p>然后找到系统调用表，把 <code>sys_interpose</code> 加入：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-comment">// An array mapping syscall numbers from syscall.h</span><span class="hljs-comment">// to the function that handles the system call.</span><span class="hljs-type">static</span> <span class="hljs-title function_">uint64</span> <span class="hljs-params">(*syscalls[])</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> = {<span class="hljs-comment">// ... 省略</span>[SYS_close]   sys_close,[SYS_interpose] sys_interpose,};</code></pre></div><p>之后写 <code>sys_call()</code> 函数的时候，会理解这一点。</p><p>然后，实现 <code>sys_interpose</code>：</p><p>在 <code>kernel/sysproc.c</code> 里面：</p><div class="note note-info"><p>关于这个文件是什么，参考 xv6 book：<img src="https://image.wendaining.top/image-20260726225618978.png" style="zoom:50%;"></p></div><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_interpose</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">int</span> mask; <span class="hljs-comment">// // 在内核栈中准备一个变量</span>  argint(<span class="hljs-number">0</span>, &amp;mask); <span class="hljs-comment">// // 取得用户传入的第 0 个参数，存入 mask</span>  myproc()-&gt;syscall_mask = mask; <span class="hljs-comment">// 把掩码保存到当前进程</span>  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}</code></pre></div><p>可能会疑惑，这看着根本没实现任何东西啊？<strong>我的理解是，这里只是起到了一个从用户态传参的作用</strong>。</p><h3 id="第四步">第四步</h3><blockquote><p>Modify <code>kfork()</code> (see <code>kernel/proc.c</code>) to copy the mask from the parent to the child process.修改 <code>kfork()</code> （见 <code>kernel/proc.c</code> ），将掩码从父进程复制到子进程。</p></blockquote><p>因为子进程的掩码要继承父进程的。</p><p>也很简单，在 <code>kfork()</code> 函数里面加入一个这个语句：</p><div class="code-wrapper"><pre><code class="hljs c">np-&gt;syscall_mask = p-&gt;syscall_mask;</code></pre></div><p>即可。</p><h3 id="第五步">第五步</h3><blockquote><p>Modify the <code>syscall()</code> function in <code>kernel/syscall.c</code> to check if the system call must be rejected.修改 <code>kernel/syscall.c</code> 中的 <code>syscall()</code> 函数，以检查系统调用是否必须被拒绝。</p></blockquote><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(num &gt; <span class="hljs-number">0</span> &amp;&amp; num &lt; NELEM(syscalls) &amp;&amp; syscalls[num]) {  <span class="hljs-keyword">if</span>(p-&gt;syscall_mask &amp; (<span class="hljs-number">1</span> &lt;&lt; num)) {    p-&gt;trapframe-&gt;a0 = <span class="hljs-number">-1</span>;  } <span class="hljs-keyword">else</span> {    p-&gt;trapframe-&gt;a0 = syscalls[num]();  }} <span class="hljs-keyword">else</span> {  printk(<span class="hljs-string">"%d %s: unknown sys call %d\n"</span>,         p-&gt;pid, p-&gt;name, num);  p-&gt;trapframe-&gt;a0 = <span class="hljs-number">-1</span>;}</code></pre></div><p><strong>这里才是真正实现 interpose 逻辑的地方</strong>。</p><p>其实我觉得这样的耦合，怪怪的。因为实现某个机制，居然要把它实现在一个调用 syscall 的通用函数里面耦合起来，感觉不太好，不过也没想到什么更好的实现机制了。</p><h3 id="从头到尾捋一遍思路">从头到尾捋一遍思路</h3><p>用户态调用 <code>interpose</code>：</p><ul><li>用户调用我们实现的系统调用接口 <code>int interpose(int, const char*)</code></li><li>这个接口的实现由 perl 脚本生成的汇编语言实现，把 <code>SYS_interpose</code> 的代号放入 <code>a7</code> 寄存器，由 ecall 硬件支持用户态转入内核态，用户态的函数栈帧存入 <code>myproc()-&gt;trapframe</code>（xv6 book 4.2 中详细讲解了）</li><li>控制转入 xv6 提供的通用接口 <code>void syscall(void)</code>，通过<code>int num; num = p-&gt;trapframe-&gt;a7;</code> 来获得需要调用的 syscall，然后从 <code>uint64 (*syscalls[])(void)</code> 这个函数指针的数组跳转到具体的函数调用，返回值放入 <code>a0</code> 寄存器</li><li>从 ecall 中退出，ret 使得用户侧返回。</li></ul><h2 id="Sandbox-with-allowed-pathnames">Sandbox with allowed pathnames</h2><p>添加一个对于 <code>open</code> 和 <code>exec</code> 检查路径的功能。</p><p>阅读 hint，有这两步：</p><blockquote><p>Some hints:</p><ul><li>Modify <code>sys_interpose()</code> to remember the allowed pathname. <code>argstr</code> will be handy to retrieve the pathname. You can declare a buffer of size <code>MAXPATH</code> in the <code>proc</code> struct.</li><li>If <code>open</code> or <code>exec</code> are masked, check if the pathname matches the allowed pathname. If so, allow the execution of those system calls.</li></ul></blockquote><p>第一个很简单，照着做就行了。因为和上一题里面做的东西基本一样。</p><p>第二个，我遇到了一些困难：</p><p>我问语言模型：</p><details class="collapsible-block collapsible-block--quote"><summary class="collapsible-block__summary" title="引用 · 921 字"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">引用</span><span class="collapsible-block__meta">我在完成这个 Sandbox with allowed pathnames， 我卡在了这个，如何判断 open 和 exec 的路径是否符合这一...</span></summary><blockquote><p>我在完成这个 Sandbox with allowed pathnames，</p><p>我卡在了这个，如何判断 open 和 exec 的路径是否符合这一步。我阅读源码，读不出来，比如我尝试阅读 exec 的实现：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 42 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 42 行</span></summary><div class="code-wrapper"><pre><code class="hljs c">uint64<span class="hljs-title function_">sys_exec</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span>{  <span class="hljs-type">char</span> path[MAXPATH], *argv[MAXARG];  <span class="hljs-type">int</span> i;  uint64 uargv, uarg;  argaddr(<span class="hljs-number">1</span>, &amp;uargv);  <span class="hljs-keyword">if</span>(argstr(<span class="hljs-number">0</span>, path, MAXPATH) &lt; <span class="hljs-number">0</span>) {    <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;  }  <span class="hljs-built_in">memset</span>(argv, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(argv));  <span class="hljs-keyword">for</span>(i=<span class="hljs-number">0</span>;; i++){    <span class="hljs-keyword">if</span>(i &gt;= NELEM(argv)){      <span class="hljs-keyword">goto</span> bad;    }    <span class="hljs-keyword">if</span>(fetchaddr(uargv+<span class="hljs-keyword">sizeof</span>(uint64)*i, (uint64*)&amp;uarg) &lt; <span class="hljs-number">0</span>){      <span class="hljs-keyword">goto</span> bad;    }    <span class="hljs-keyword">if</span>(uarg == <span class="hljs-number">0</span>){      argv[i] = <span class="hljs-number">0</span>;      <span class="hljs-keyword">break</span>;    }    argv[i] = kalloc();    <span class="hljs-keyword">if</span>(argv[i] == <span class="hljs-number">0</span>)      <span class="hljs-keyword">goto</span> bad;    <span class="hljs-keyword">if</span>(fetchstr(uarg, argv[i], PGSIZE) &lt; <span class="hljs-number">0</span>)      <span class="hljs-keyword">goto</span> bad;  }  <span class="hljs-type">int</span> ret = kexec(path, argv);  <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; i &lt; NELEM(argv) &amp;&amp; argv[i] != <span class="hljs-number">0</span>; i++)    kfree(argv[i]);  <span class="hljs-keyword">return</span> ret; bad:  <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; i &lt; NELEM(argv) &amp;&amp; argv[i] != <span class="hljs-number">0</span>; i++)    kfree(argv[i]);  <span class="hljs-keyword">return</span> <span class="hljs-number">-1</span>;}</code></pre></div></details><p>然后我发现，我找不到类似于实现 interpose 的时候那样的，具体的如何获得参数的部分。具体而言，当我阅读到这一行的时候：</p><p><code>if((n = argstr(0, path, MAXPATH)) &lt; 0)</code></p><p>我不是很清楚这个0是什么，我觉得是 stdin？我尝试一步一步读代码，但是好像有点太多了，读不过来。总之，我希望你能简明扼要给我串起来，回答不必过长</p></blockquote></details><p>得到的答案，总结一下：</p><ul><li><p>这里的 <code>0</code> <strong>不是 stdin</strong>，而是：读取系统调用的第 0 个参数，也就是第一个参数。</p></li><li><p><strong>a0, a1, ...</strong> 分别对应系统调用的第0，第1...个参数。</p></li><li><p>而这两个系统调用的函数形式（看源码就可以知道了，<code>user.h</code>，都是：</p>  <div class="code-wrapper"><pre><code class="hljs C">open(path, mode);exec(path, argv);</code></pre></div></li><li><p>因此，在 <code>syscall()</code>里面，使用 <code>argstr(0, pathm MAXPATH)</code> 就可以获得参数了。</p></li></ul><img src="https://image.wendaining.top/image-20260726203540808.png" alt="顺利通过" style="zoom:33%;"><h2 id="Attack-xv6">Attack xv6</h2><p>背景是，<code>uvmalloc</code> 和 <code>kalloc</code> 里面，省略了 <code>memset</code> 的几个语句，导致新分配的内存保留了其先前使用的内容。</p><p>然后运行这么一个程序：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 18 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 18 行</span></summary><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-meta">#<span class="hljs-keyword">define</span> DATASIZE (8*4096)</span><span class="hljs-type">char</span> data[DATASIZE];<span class="hljs-type">int</span><span class="hljs-title function_">main</span><span class="hljs-params">(<span class="hljs-type">int</span> argc, <span class="hljs-type">char</span> *argv[])</span>{  <span class="hljs-keyword">if</span>(argc != <span class="hljs-number">2</span>){    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Usage: secret the-secret\n"</span>);    <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);  }  <span class="hljs-built_in">strcpy</span>(data, <span class="hljs-string">"This may help."</span>);  <span class="hljs-built_in">strcpy</span>(data + <span class="hljs-number">16</span>, argv[<span class="hljs-number">1</span>]);  <span class="hljs-built_in">exit</span>(<span class="hljs-number">0</span>);}</code></pre></div></details><p>编写 <code>attack.c</code> 以得到 <code>secret.c</code> 的这个 secret</p><p>具体做法需要用到一个系统调用 <code>sbrk</code>：向内核申请扩大当前进程的用户内存，返回值是指向新增加的这段内存的起始位置。</p><p>在 xv6 中通常声明为：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">char</span> *<span class="hljs-title function_">sbrk</span><span class="hljs-params">(<span class="hljs-type">int</span> n)</span>;</code></pre></div><p>那么解决方案就很简单了：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 22 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 22 行</span></summary><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-type">int</span><span class="hljs-title function_">main</span><span class="hljs-params">(<span class="hljs-type">int</span> argc, <span class="hljs-type">char</span> *argv[])</span>{  <span class="hljs-comment">// Your code here.</span>  <span class="hljs-type">char</span> *p = sbrk(<span class="hljs-number">8</span>*<span class="hljs-number">4096</span>);  <span class="hljs-keyword">if</span> (p == (<span class="hljs-type">char</span>*)<span class="hljs-number">-1</span>) {    <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);  }  <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">8</span>*<span class="hljs-number">4096</span> - <span class="hljs-number">16</span>; i++) {    <span class="hljs-keyword">if</span> (p[i] ==<span class="hljs-string">'T'</span>) {      <span class="hljs-type">char</span> tmp[<span class="hljs-number">15</span>];      <span class="hljs-built_in">memcpy</span>(tmp, p + i, <span class="hljs-number">15</span>);      <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">strcmp</span>(tmp, <span class="hljs-string">"This may help."</span>)) {        <span class="hljs-keyword">for</span> (<span class="hljs-type">char</span> *c = p + i + <span class="hljs-number">16</span>; *c; c++) {          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%c"</span>, *c);        }        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\n"</span>);      }    }  }  <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);}</code></pre></div></details><h2 id="总结和碎碎念">总结和碎碎念</h2><img src="https://image.wendaining.top/image-20260726221754957.png" alt="完成lab2" style="zoom: 67%;"><p>xv6 book 找不到比较新版的汉化，所以基本是直接丢给 LLM 让他总结一下我随便看看就上手写 lab 了。结果发现，写 lab 的时候遇到的基本上所有问题，在书里面都有解答，但是我开始写的时候就是一头雾水，有点难绷。</p><p>但是仔细想想，感觉我就算仔细读，也读不进去，反倒是我现在从开始的一头雾水，到一边写一边弄懂，可能这样对我而言效果更好一点。或许，现在可以去重新读一下这一部分的书？不过我现在有点懒，所以作罢吧。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>xv6 Lab1 Utilities - MIT 6.1810 Fall 2025 Operating System</title>
      <link>https://blog.wendain.ing/2026/07/24/xv6-lab1-utilities/</link>
      <description>xv6 的第一个 lab，主要是一个 make hands dirty 的过程，熟悉一下开发环境，以及熟悉一些基础的系统调用</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/xv6/">xv6</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%AC%E5%BC%80%E8%AF%BE/">公开课</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/tags/xv6/">xv6</category>
      <pubDate>Fri, 24 Jul 2026 13:10:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="read-xv6-book">read xv6 book</h2><p>阅读 xv6 book：</p><blockquote><p>shell 是一个普通的程序，它读取用户的命令并执行它们。shell是一个用户程序，而不是内核的一部分，这一事实说明了系统调用接口的强大功能：shell没有什么特别的。这也意味着外壳易于更换；因此，现代Unix系统有多种shell可供选择，每种都有自己的用户界面和脚本功能。</p></blockquote><p>xv6 提供的 system call：</p><img src="https://image.wendaining.top/image-20260724142812369.png" style="zoom:50%;"><p><code>cd</code> 必须是 shell 的内置命令，因为：</p><p>假设 <code>cd</code> 是普通程序，Shell 会这样运行它：</p><div class="code-wrapper"><pre><code class="hljs text">Shell  │ fork  ▼子进程执行 cd</code></pre></div><p>子进程执行：</p><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-built_in">chdir</span>(<span class="hljs-string">"/home"</span>);</code></pre></div><p>改变的只是子进程自己的工作目录。随后子进程退出，原来的 Shell 目录完全没变。</p><p>因此 <code>cd</code> 必须由 Shell 自己执行：</p><div class="code-wrapper"><pre><code class="hljs text">Shell 进程直接调用 chdir()</code></pre></div><p>这样才能真正改变后续命令使用的工作目录。</p><p>这是 1.4 中值得重点理解的例子，因为它再次体现：<strong><code>fork</code> 后，父子进程的进程状态彼此独立。</strong></p><p>关于 <code>ping pong</code>：</p><blockquote><p>Write a program that uses UNIX system calls to “ping-pong” a byte between two processes over a pair of pipes, one for each direction. Measure the program’s performance, in exchanges per second.</p></blockquote><p>代码：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 193 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 193 行</span></summary><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdlib.h&gt;</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;time.h&gt;</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;unistd.h&gt;</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> N_EXCHANGES 100000</span><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">(<span class="hljs-type">int</span> argc, <span class="hljs-type">char</span>** argv)</span> {    <span class="hljs-type">int</span> pipe_child_to_parent[<span class="hljs-number">2</span>];   <span class="hljs-comment">// pipe_child_to_parent[0]=读端, [1]=写端</span>    <span class="hljs-type">int</span> pipe_parent_to_child[<span class="hljs-number">2</span>];   <span class="hljs-comment">// pipe_parent_to_child[0]=读端,  [1]=写端</span>    <span class="hljs-type">char</span> b = <span class="hljs-string">'a'</span>;    <span class="hljs-keyword">if</span> (pipe(pipe_child_to_parent) &lt; <span class="hljs-number">0</span> || pipe(pipe_parent_to_child) &lt; <span class="hljs-number">0</span>) {        perror(<span class="hljs-string">"Pipe error."</span>);        <span class="hljs-comment">// *** 问题：pipe 失败了但没有 exit(1)，程序会带着无效 fd 继续执行。</span>        <span class="hljs-comment">// 后面所有 read/write 都会操作 fd=-1，虽然不会崩溃但全部静默失败。</span>        <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);    }    <span class="hljs-type">int</span> pid = fork();    <span class="hljs-type">time_t</span> start, end;    <span class="hljs-keyword">if</span> (pid &lt; <span class="hljs-number">0</span>) {        perror(<span class="hljs-string">"fork error"</span>);        <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);        <span class="hljs-comment">// *** 问题：fork 失败后没有 exit(1)，只有 perror。</span>        <span class="hljs-comment">// 程序会继续往下执行，进入 pid&gt;0 或 pid==0 的未知分支。</span>    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (pid == <span class="hljs-number">0</span>) {        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//  关键概念：fork 之后谁持有哪些 fd？</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// pipe() 创建的管道是内核对象。fd 只是"指向这个内核对象的编号"。</span>        <span class="hljs-comment">// fork() 会复制整个 fd 表，所以父子各自拥有独立的 fd 编号，但都</span>        <span class="hljs-comment">// 指向同一个内核管道。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 创建了两条管道后：</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   pipe_parent_to_child[0] = 3  (读端)    父→子，父写子读</span>        <span class="hljs-comment">//   pipe_parent_to_child[1] = 4  (写端)</span>        <span class="hljs-comment">//   pipe_child_to_parent[0] = 5  (读端)    子→父，子写父读</span>        <span class="hljs-comment">//   pipe_child_to_parent[1] = 6  (写端)</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// fork 之后，父子进程各有一份 fd 3,4,5,6，都指向同两个内核管道。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 现在子进程的任务是：</span>        <span class="hljs-comment">//   - 从 pipe_parent_to_child 的读端读取父发来的数据 → 用 fd 3</span>        <span class="hljs-comment">//   - 向 pipe_child_to_parent 的写端把数据回给父   → 用 fd 6</span>        <span class="hljs-comment">// 所以子进程只用到 fd 3 和 fd 6。fd 4 和 fd 5 完全不用。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//  什么时候需要 close？——两条铁律</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 铁律 1：一个进程如果不打算在某条管道上"写"，就必须把自己持有</span>        <span class="hljs-comment">//         的该管道**所有写端 fd** 全部关掉。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 铁律 2：一个进程如果不打算在某条管道上"读"，就必须把自己持有</span>        <span class="hljs-comment">//         的该管道**所有读端 fd** 全部关掉。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//  为什么不关就会出事？——具体推演</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 以 pipe_parent_to_child 这条管道为例：</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   fork 之后：父进程有 fd{3,4}，子进程也有 fd{3,4}</span>        <span class="hljs-comment">//   （3=读端，4=写端）</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   父进程会写这条管道（用 fd 4），子进程会读这条管道（用 fd 3）。</span>        <span class="hljs-comment">//   但子进程的 fd 4（写端）他从来不写，如果没关 ——</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   那会发生什么？</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   假设程序没有固定循环次数，而是"读到 EOF 才停"：</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//     父进程：写了 10 次 → close(fd4) → 结束</span>        <span class="hljs-comment">//     子进程：while (read(fd3, ...) &gt; 0) { ... }</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   问题是：子进程 read 的时候，内核看了一眼 pipe_parent_to_child</span>        <span class="hljs-comment">//   这个管道——"嗯...父进程关了写端，但子进程的 fd4（写端）还开着！</span>        <span class="hljs-comment">//   可能还有数据要来，我再等等..."——于是 read() 永远阻塞，子进程</span>        <span class="hljs-comment">//   永远不会从 while 循环里出来。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   这就是"忘了 close 导致死锁"的经典场景。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">//   你的代码靠固定循环次数 + 两个进程最后都 exit() 避免了这个问题，</span>        <span class="hljs-comment">//   但这是危险的——将来只要有一个读操作依赖 EOF 来判断结束，就会死锁。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//  在哪里 close？——越早越好</span>        <span class="hljs-comment">// ============================================================</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 一旦你确定"这个进程不需要这个 fd 了"，就立刻 close。</span>        <span class="hljs-comment">// 对于 ping-pong 程序，在进入循环之前就应该关掉：</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// ==================== 子进程：进入循环前应 close ====================</span>        <span class="hljs-comment">// *** 缺失：close(pipe_parent_to_child[1]);</span>        <span class="hljs-comment">// 原因：子进程永远不会向 pipe_parent_to_child 这条管道写数据。</span>        <span class="hljs-comment">// 写端 fd[1] 只有父进程在用。子进程留着写端=告诉内核"我也可能</span>        <span class="hljs-comment">// 写哦"，会导致铁律 1 被违反。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// *** 缺失：close(pipe_child_to_parent[0]);</span>        <span class="hljs-comment">// 原因：子进程永远不会从 pipe_child_to_parent 这条管道读数据。</span>        <span class="hljs-comment">// 读端 fd[0] 只有父进程在用。不关也会违反铁律 2。</span>        <span class="hljs-comment">// close 先关掉不用的 fd（铁律！）</span>        close(pipe_parent_to_child[<span class="hljs-number">1</span>]);  <span class="hljs-comment">// 子进程不写 parent→child，关写端</span>        close(pipe_child_to_parent[<span class="hljs-number">0</span>]);  <span class="hljs-comment">// 子进程不读 child→parent，关读端</span>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; N_EXCHANGES; ++i) {            <span class="hljs-comment">// read 返回实际读到的字节数；期望读到 1 字节</span>            <span class="hljs-type">int</span> rt = read(pipe_parent_to_child[<span class="hljs-number">0</span>], &amp;b, <span class="hljs-number">1</span>);            <span class="hljs-keyword">if</span> (rt != <span class="hljs-number">1</span>) {                <span class="hljs-comment">// 读到 0 = 对方关了写端（EOF），-1 = 出错</span>                perror(<span class="hljs-string">"child read"</span>);                <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);            }            rt = write(pipe_child_to_parent[<span class="hljs-number">1</span>], &amp;b, <span class="hljs-number">1</span>);            <span class="hljs-keyword">if</span> (rt != <span class="hljs-number">1</span>) {                perror(<span class="hljs-string">"child write"</span>);                <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);            }        }        <span class="hljs-built_in">exit</span>(<span class="hljs-number">0</span>);    } <span class="hljs-keyword">else</span> {        <span class="hljs-comment">// ==================== 父进程 ====================</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 父进程的任务：</span>        <span class="hljs-comment">//   - 向 pipe_parent_to_child 写入数据         → 用 fd[1]（写端）</span>        <span class="hljs-comment">//   - 从 pipe_child_to_parent 读取子进程回传  → 用 fd[0]（读端）</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 不用的 fd（进入循环前必须关）：</span>        <span class="hljs-comment">//   pipe_parent_to_child[0]（读端）→ 父进程永远不会从这里读</span>        <span class="hljs-comment">//   pipe_child_to_parent[1]（写端）→ 父进程永远不会往这里写</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 不关的话：子进程将来如果想通过 read 返回 0（EOF）来判断</span>        <span class="hljs-comment">// "父进程写完了"，会因为父进程留着写端而永远等不到 EOF。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 另外，你当前在第 143 行 close 的是 pipe_parent_to_child[1] 和</span>        <span class="hljs-comment">// pipe_child_to_parent[0]——这两个是父进程在循环中**正在使用**的</span>        <span class="hljs-comment">// 写端和读端。它们应该最后关（循环结束后）。</span>        <span class="hljs-comment">// 而本该先关的 pipe_parent_to_child[0] 和 pipe_child_to_parent[1]</span>        <span class="hljs-comment">// 却完全没有被 close。顺序完全搞反了。</span>        <span class="hljs-comment">//</span>        <span class="hljs-comment">// 正确的 close 布局：</span>        <span class="hljs-comment">//   close(pipe_parent_to_child[0]);   // ← 循环前：关掉不用的读端</span>        <span class="hljs-comment">//   close(pipe_child_to_parent[1]);   // ← 循环前：关掉不用的写端</span>        <span class="hljs-comment">//   for (...) { write(...); read(...); }  // ← 用自己需要的 fd</span>        <span class="hljs-comment">//   close(pipe_parent_to_child[1]);   // ← 循环后：用完再关</span>        <span class="hljs-comment">//   close(pipe_child_to_parent[0]);   // ← 循环后：用完再关</span>        <span class="hljs-comment">// *** 缺失：close(pipe_parent_to_child[0]);</span>        <span class="hljs-comment">// *** 缺失：close(pipe_child_to_parent[1]);</span>        start = time(<span class="hljs-literal">NULL</span>);        <span class="hljs-comment">// 父进程先关掉自己不用的 fd</span>        close(pipe_child_to_parent[<span class="hljs-number">1</span>]);  <span class="hljs-comment">// 父进程不写 child→parent，关写端</span>        close(pipe_parent_to_child[<span class="hljs-number">0</span>]);  <span class="hljs-comment">// 父进程不读 parent→child，关读端</span>        <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; N_EXCHANGES; ++i) {            <span class="hljs-comment">// *** 关键：父进程先 write（发起 ping），再 read（等 pong）！</span>            <span class="hljs-comment">// 子进程那边是先 read 后 write，两边必须配对，</span>            <span class="hljs-comment">// 否则同时 read 就死锁。</span>            <span class="hljs-type">int</span> rt = write(pipe_parent_to_child[<span class="hljs-number">1</span>], &amp;b, <span class="hljs-number">1</span>);            <span class="hljs-keyword">if</span> (rt != <span class="hljs-number">1</span>) {                perror(<span class="hljs-string">"parent write"</span>);                <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);            }            rt = read(pipe_child_to_parent[<span class="hljs-number">0</span>], &amp;b, <span class="hljs-number">1</span>);            <span class="hljs-keyword">if</span> (rt != <span class="hljs-number">1</span>) {                perror(<span class="hljs-string">"parent read"</span>);                <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);            }        }        end = time(<span class="hljs-literal">NULL</span>);        <span class="hljs-type">double</span> elapsed = difftime(end, start);        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"每秒交换次数：%.2f\n"</span>, N_EXCHANGES / elapsed);        <span class="hljs-built_in">exit</span>(<span class="hljs-number">0</span>);    }}<span class="hljs-comment">// 总结你的主要问题：</span><span class="hljs-comment">//</span><span class="hljs-comment">// 1. 【致命】父进程把读端和写端搞反了。</span><span class="hljs-comment">//    记住口诀：pipe(fd) 之后，fd[0] 永远用来读，fd[1] 永远用来写。</span><span class="hljs-comment">//    你父进程里 read(fd[1]) 和 write(fd[0]) 都反了。</span><span class="hljs-comment">//</span><span class="hljs-comment">// 2. 【重要】缺少 close 不用的 fd。</span><span class="hljs-comment">//    在进入循环之前，每个进程应该关掉自己不需要的那两个 fd。</span><span class="hljs-comment">//</span><span class="hljs-comment">// 3. 【次要】pipe() 和 fork() 的错误处理不完整，失败后没有 exit。</span><span class="hljs-comment">//</span><span class="hljs-comment">// 4. 【次要】read/write 没有检查返回值，失败时静默将继续。</span></code></pre></div></details><h2 id="Boot-xv6">Boot xv6</h2><p>根据 lab tools page 配置好在 WSL 下运行 xv6 的环境。</p><p>克隆好相应的代码。</p><p>输入 <code>make qemu</code> 以构建并运行 xv6。</p><p>查看进程：<strong>不是</strong> <code>ps</code>，是 <code>Ctrl + P</code></p><p>退出 qemu，输入： <code>Ctrl-a x</code></p><h2 id="sleep">sleep</h2><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/types.h"</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/stat.h"</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/fcntl.h"</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"user/user.h"</span></span><span class="hljs-type">int</span> <span class="hljs-title function_">main</span><span class="hljs-params">(<span class="hljs-type">int</span> argc, <span class="hljs-type">char</span>* argv[])</span> {  <span class="hljs-keyword">if</span> (argc != <span class="hljs-number">2</span>) {    <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"Usage: sleep time...\n"</span>);    <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);  }  <span class="hljs-type">int</span> ret = pause(atoi(argv[<span class="hljs-number">1</span>]));  <span class="hljs-built_in">exit</span>(ret);}</code></pre></div><p>不过留下的疑问：</p><ol><li>system call，在<code>user.h</code> 文件里面，在 vscode 里面点击「查看定义」，溯回不到汇编源码，那么，汇编源码是如何和 C 语言头文件结合的？</li><li><code>pause</code> 的返回值，光看汇编也看不出来是什么</li></ol><p>答案：脚本生成 + 链接器符号解析。日后再深究吧。</p><p>感觉是我没读 csapp 的缘故？有点一头雾水，说实话。没有一个高屋建瓴的架构视角的感觉有点不爽，毕竟在 LLM 时代，这个比较重要。</p><p><strong>记得把文件加入 Makefile，不然重新编译内核也没用</strong>。</p><h2 id="sixfive">sixfive</h2><p>这里重点是注意参数可以接多个。</p><h2 id="memdump">memdump</h2><p>这里注意 <code>s</code> 的实现：</p><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-keyword">case</span> <span class="hljs-string">'s'</span>:   <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%s\n"</span>, *(<span class="hljs-type">char</span>**)d);   d += <span class="hljs-number">8</span>;   <span class="hljs-keyword">break</span>;</code></pre></div><p>先取出指针的指针，再解引用所指向的内容。</p><h2 id="find">find</h2><p>一个意外的 C 语言语法是：<code>const</code> 的指针可以执行运算，只是不能修改指向的内容。</p><p>换句话理解：<code>const</code> 修饰的是指向的内容，不是指针本身。</p><h2 id="find-with-exec">find with -exec</h2><p>先上代码：</p><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 C · 99 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">C · 99 行</span></summary><div class="code-wrapper"><pre><code class="hljs C"><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/types.h"</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/fcntl.h"</span> <span class="hljs-comment">// O_RDONLY</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/fs.h"</span>    <span class="hljs-comment">// struct dirent, DIRSIZ</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/stat.h"</span>  <span class="hljs-comment">// struct stat, T_DIR, T_FILE</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"kernel/param.h"</span></span><span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">"user/user.h"</span> <span class="hljs-comment">// open, read, close, fstat, printf...</span></span><span class="hljs-meta">#<span class="hljs-keyword">define</span> NULL 0</span><span class="hljs-type">void</span><span class="hljs-title function_">check_path</span><span class="hljs-params">(<span class="hljs-type">const</span> <span class="hljs-type">char</span> *path, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *name, <span class="hljs-type">char</span> **cmd)</span>;<span class="hljs-type">int</span><span class="hljs-title function_">main</span><span class="hljs-params">(<span class="hljs-type">int</span> argc, <span class="hljs-type">char</span> *argv[])</span>{  <span class="hljs-keyword">if</span>(argc &lt; <span class="hljs-number">3</span>) {    <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"Usage: find [path] [name] [-exec command ...]\n"</span>);    <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);  }  <span class="hljs-type">const</span> <span class="hljs-type">char</span> *path = argv[<span class="hljs-number">1</span>];  <span class="hljs-type">const</span> <span class="hljs-type">char</span> *name = argv[<span class="hljs-number">2</span>];  <span class="hljs-type">char</span> **cmd = <span class="hljs-literal">NULL</span>;  <span class="hljs-keyword">if</span>(argc &gt; <span class="hljs-number">3</span>) {    <span class="hljs-keyword">if</span>(<span class="hljs-built_in">strcmp</span>(argv[<span class="hljs-number">3</span>], <span class="hljs-string">"-exec"</span>) != <span class="hljs-number">0</span>) {      <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"The third argument should be -exec.\n"</span>);      <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);    }    <span class="hljs-keyword">if</span>(argc == <span class="hljs-number">4</span>) {      <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"No command was provided after -exec.\n"</span>);      <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);    }    cmd = argv + <span class="hljs-number">4</span>;  }  check_path(path, name, cmd);  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}<span class="hljs-type">void</span><span class="hljs-title function_">check_path</span><span class="hljs-params">(<span class="hljs-type">const</span> <span class="hljs-type">char</span> *path, <span class="hljs-type">const</span> <span class="hljs-type">char</span> *name, <span class="hljs-type">char</span> **cmd)</span>{  <span class="hljs-type">int</span> fd = open(path, O_RDONLY);  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">stat</span> <span class="hljs-title">st</span>;</span>  <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">dirent</span> <span class="hljs-title">de</span>;</span>  <span class="hljs-keyword">if</span>(fd &lt; <span class="hljs-number">0</span>) {    <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"Can not open the path.\n"</span>);    <span class="hljs-keyword">return</span>;  }  fstat(fd, &amp;st);  <span class="hljs-keyword">if</span>(st.type == T_DIR) {    <span class="hljs-keyword">while</span>(read(fd, &amp;de, <span class="hljs-keyword">sizeof</span>(de)) == <span class="hljs-keyword">sizeof</span>(de)) {      <span class="hljs-keyword">if</span>(!<span class="hljs-built_in">strcmp</span>(de.name, <span class="hljs-string">"."</span>) || !<span class="hljs-built_in">strcmp</span>(de.name, <span class="hljs-string">".."</span>) || !de.inum) {        <span class="hljs-keyword">continue</span>;      }      <span class="hljs-type">char</span> buf[<span class="hljs-number">512</span>];      <span class="hljs-built_in">strcpy</span>(buf, path);      <span class="hljs-type">char</span> *p = buf + <span class="hljs-built_in">strlen</span>(buf); <span class="hljs-comment">// p 指向 buf 末尾的 '\0'</span>      *p++ = <span class="hljs-string">'/'</span>;      memmove(p, de.name, DIRSIZ);      p[DIRSIZ] = <span class="hljs-number">0</span>;      check_path(buf, name, cmd);    }  } <span class="hljs-keyword">else</span> {    <span class="hljs-comment">// 当前 path 即为完整路径，判断最后文件名是否符合即可</span>    <span class="hljs-type">const</span> <span class="hljs-type">char</span> *p;    <span class="hljs-keyword">for</span>(p = path + <span class="hljs-built_in">strlen</span>(path); *p != <span class="hljs-string">'/'</span> &amp;&amp; p &gt; path; p--)      ;    p++;    <span class="hljs-keyword">if</span>(!<span class="hljs-built_in">strcmp</span>(p, name)) {      <span class="hljs-comment">// printf("%s\n", path);</span>      <span class="hljs-keyword">if</span>(!cmd) {        <span class="hljs-built_in">printf</span>(<span class="hljs-string">"%s\n"</span>, path);      } <span class="hljs-keyword">else</span> {        <span class="hljs-type">int</span> rc = fork();        <span class="hljs-keyword">if</span>(rc &lt; <span class="hljs-number">0</span>) {          <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"fork failed.\n "</span>);          <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(rc == <span class="hljs-number">0</span>) {          <span class="hljs-type">char</span> *new_cmd[MAXARG];          <span class="hljs-type">int</span> i;          <span class="hljs-keyword">for</span>(i = <span class="hljs-number">0</span>; cmd[i]; i++) {            new_cmd[i] = cmd[i];          }          new_cmd[i] = (<span class="hljs-type">char</span>*)path;          new_cmd[i+<span class="hljs-number">1</span>] = <span class="hljs-number">0</span>;          exec(new_cmd[<span class="hljs-number">0</span>], new_cmd);          <span class="hljs-built_in">fprintf</span>(<span class="hljs-number">2</span>, <span class="hljs-string">"exec failed.\n"</span>);          <span class="hljs-built_in">exit</span>(<span class="hljs-number">1</span>);        } <span class="hljs-keyword">else</span> {          wait(<span class="hljs-literal">NULL</span>);        }      }    }  }  close(fd);}</code></pre></div></details><p>主要遇到的问题：</p><ol><li><p><code>cmd</code> 的逻辑这一块，太久没手写了，如何写得优雅卡了一会儿，不过其实感觉这个让语言模型代劳完全没问题，毕竟和 OS 没什么关系...试着把握好度吧。</p></li><li><p>忘记处理参数了，开始直接写的是 <code>exec(cmd[0], cmd)</code>，没把 <code>path</code> 加进去。</p></li><li><p><code>de.inum == 0</code> 的判断很重要，这里其实照抄 <code>ls.c</code> 就行，不过解释一下原因：</p><div class="note note-success"><p>把目录理解成一本<strong>有"已删除"标记的笔记本</strong>。</p><p>文件系统删除文件时，并不会把目录里那条记录擦掉，也不会把后面的记录往前挪——太费劲了。它只做一件事：<strong>把 <code>inum</code> 改成 0</strong>。</p><div class="code-wrapper"><pre><code class="hljs routeros">删除前：                      删除后：┌────────┬──────────────┐    ┌────────┬──────────────┐│ <span class="hljs-attribute">inum</span>=5 │ <span class="hljs-attribute">name</span>=<span class="hljs-string">"cat"</span>   │    │ <span class="hljs-attribute">inum</span>=0 │ <span class="hljs-attribute">name</span>=<span class="hljs-string">"cat"</span>   │  ← 名字还在！├────────┼──────────────┤    ├────────┼──────────────┤│ <span class="hljs-attribute">inum</span>=7 │ <span class="hljs-attribute">name</span>=<span class="hljs-string">"ls"</span>    │    │ <span class="hljs-attribute">inum</span>=7 │ <span class="hljs-attribute">name</span>=<span class="hljs-string">"ls"</span>    │├────────┼──────────────┤    ├────────┼──────────────┤│ <span class="hljs-attribute">inum</span>=0 │ name(垃圾)    │    │ <span class="hljs-attribute">inum</span>=0 │ name(垃圾)    │└────────┴──────────────┘    └────────┴──────────────┘</code></pre></div><p>那你遍历目录时会发生什么？</p><div class="code-wrapper"><pre><code class="hljs stata"><span class="hljs-keyword">read</span>(fd, &amp;<span class="hljs-keyword">de</span>, sizeof(<span class="hljs-keyword">de</span>)) → <span class="hljs-keyword">de</span>.inum=0, <span class="hljs-keyword">de</span>.name=<span class="hljs-string">"cat"</span></code></pre></div><p>如果不检查 <code>de.inum == 0</code>，你会拿着 <code>"cat"</code> 这个名字去 <code>open("cat")</code>——<strong>但 cat 早就不存在了！</strong></p><p>更要命的是，空槽位里的 <code>name</code> 可能残留着任意旧数据。它可能恰好是一个有效的名字，导致你去 open 一个不该 open 的东西；或者更糟——在你这个递归场景下——引发预料之外的循环。</p><p><code>ls.c</code> 第 61 行做的就是这件事：</p><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-keyword">if</span>(de.inum == <span class="hljs-number">0</span>)        <span class="hljs-comment">// 空座位，跳过</span>    <span class="hljs-keyword">continue</span>;</code></pre></div></div></li></ol><p>Lab1 完成：</p><img src="https://image.wendaining.top/b32cc708002083e19828754279c977bf.png" style="zoom:33%;">]]>
      </content:encoded>
    </item>
    <item>
      <title>Cloudflare 文档阅读</title>
      <link>https://blog.wendain.ing/2026/07/17/cloudflare-docs-reading-note/</link>
      <description>虽然不知道读这种服务性质的内容的文档干嘛，不过似乎 cf 的这个基础文档里面会讲很多有关的计网知识，所以看一看也没什么坏处。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%A8%E6%A0%88/">全栈</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/">计算机网络</category>
      <category domain="https://blog.wendain.ing/tags/%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3/">官方文档</category>
      <pubDate>Fri, 17 Jul 2026 12:10:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="序">序</h2><p><a href="https://developers.cloudflare.com/fundamentals/">Cloudflare Fundamentals docs</a></p><p>虽然不知道读这种服务性质的内容的文档干嘛，不过似乎 cf 的这个基础文档里面会讲很多有关的计网知识，所以看一看也没什么坏处。</p><p>文档只提供英文版，阅读文档的时候用的是 DeepSeek V4 Flash +沉浸式翻译插件翻译的版本。</p><p>另：cf 还有这么一个网站：<a href="https://www.cloudflare.com/learning/">Learning Center | Cloudflare</a></p><p>里面讲解更多实际的网络相关的知识，之后可以加以补充。</p><div class="note note-info"><p>Cloudflare 的产品矩阵是真的庞大...</p><img src="https://image.wendaining.top/image-20260717122014044.png" alt="Products" style="zoom:25%;"><img src="https://image.wendaining.top/image-20260717122138539.png" alt="Solutions" style="zoom:25%;"><img src="https://image.wendaining.top/image-20260717122159010.png" alt="Resources" style="zoom:25%;"></div><h2 id="Overview">Overview</h2><p>Cloudflare 自称是<strong>连接云网络</strong>，这个似乎是 cf 自创的一个概念，详情见 <a href="https://blog.cloudflare.com/welcome-to-connectivity-cloud/#cloudflares-connectivity-cloud">Welcome to connectivity cloud: the modern way to connect and protect your clouds, networks, applications and users | The Cloudflare Blog</a>。</p><blockquote><p>连接云是一种新方法，用于提供企业保护和连接数字环境所需的众多服务。它是一个统一的、智能的、可编程的云原生服务平台，能够实现所有网络（企业网络和互联网）、云环境、应用程序和用户之间的任意互联。它包括大量的安全、性能和开发服务——并非旨在全面取代现有的一切，而是能够根据需要灵活部署，并将许多关键服务整合到单一平台上。</p></blockquote><h2 id="Concepts">Concepts</h2><h3 id="cf-作为-DNS-提供商">cf 作为 DNS 提供商</h3><p>TODO：<a href="https://www.cloudflare.com/learning/dns/what-is-dns/">What is DNS? | Learning Center</a></p><p>cf 这个 learning center 的配套文档也太多了...感觉这辈子都看不完，只能在这画个饼了。</p><p>不过 DNS 的基础概念还是知道的，这里不写了。很多细节是 TODO，后面估计单独拆出来记。</p><h3 id="cf-作为反向代理">cf 作为反向代理</h3><p>所谓反向代理：一组位于 Web 服务器前方的服务器网络。</p><ul><li>它们要么将请求转发给这些 Web 服务器，</li><li>要么代表 Web 服务器处理请求。</li></ul><p>使用 cf 进行反向代理的优势：</p><ul><li>负载均衡</li><li>缓存</li><li>防攻击</li><li>SSL 加密</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Docker Note</title>
      <link>https://blog.wendain.ing/2026/07/17/docker-note/</link>
      <description>Docker——学过，看过，用过，都很零散，这次决定系统梳理一遍。学习主线使用《Docker 从入门到实践》这本书，辅可能会有读到的文档和看的教学视频的内容加以补充。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%A8%E6%A0%88/">全栈</category>
      <category domain="https://blog.wendain.ing/tags/Docker/">Docker</category>
      <category domain="https://blog.wendain.ing/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/">中间件</category>
      <pubDate>Fri, 17 Jul 2026 12:10:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="序">序</h2><p>Docker——学过，看过，用过，都很零散，这次决定系统梳理一遍。</p><p>梳理一下我不太熟悉的东西：</p><ul><li>Docker Compose</li><li>Docker 的网络相关</li></ul><p>这次着重补一下这两块。</p><p>学习主线使用《<a href="https://yeasy.gitbook.io/docker_practice">Docker 从入门到实践</a>》这本书，辅可能会有读到的文档和看的教学视频的内容加以补充。</p><h2 id="Docker-简介">Docker 简介</h2><ul><li>轻量级的虚拟化技术</li><li>将应用程序及其依赖环境可以被打包成一个标准化的单元，在满足架构、内核能力和外部依赖前提的环境中高度一致地运行。</li></ul><h3 id="和虚拟机的差别">和虚拟机的差别</h3><p>传统虚拟机：</p><ul><li>虚拟出一套完整的硬件，在其之上运行一个完整的 OS</li></ul><p>Docker 容器：</p><ul><li>容器运行与<strong>宿主的内核</strong>，容器没有自己的内核，没有对于硬件的虚拟</li></ul><table><thead><tr><th>特性</th><th>Docker 容器</th><th>传统虚拟机</th></tr></thead><tbody><tr><td><strong>启动速度</strong></td><td>秒级</td><td>分钟级</td></tr><tr><td><strong>资源占用</strong></td><td>MB 级别</td><td>GB 级别</td></tr><tr><td><strong>性能</strong></td><td>接近原生</td><td>有明显损耗</td></tr><tr><td><strong>隔离级别</strong></td><td>进程级隔离</td><td>完全隔离</td></tr><tr><td><strong>单机数量</strong></td><td>可运行上千个</td><td>通常几十个</td></tr></tbody></table><h3 id="Docker-的优势">Docker 的优势</h3><ul><li>一次构建，到处运行</li><li>环境一致性</li><li>启动速度快<ul><li>对 CI/CD，弹性扩容很好</li></ul></li><li>资源效率相比虚拟机高</li><li>持续交付和部署<ul><li>契合 DevOps 的工作流程：代码提交、</li></ul></li><li>轻松迁移</li><li>微服务架构的基石</li></ul><h2 id="Docker-概念">Docker 概念</h2><h3 id="镜像">镜像</h3><ul><li><p>只读的<strong>模板</strong>，包含运行应用所需要的一切。</p><div class="note note-primary"><p>镜像<strong>只读，不包含动态数据，构建之后内容不改变</strong>。</p></div></li><li><p>一个镜像可以创建多个容器，而镜像本身保持不变。</p></li></ul><h4 id="镜像与-OS-的关系">镜像与 OS 的关系</h4><p>OS 分为<strong>内核</strong>和<strong>用户空间</strong>：</p><pre><code class=" mermaid">flowchart TD    subgraph UserSpace ["用户空间"]        direction TB        App["应用程序、工具、库、配置文件...&lt;br/&gt;（这部分被打包成 Docker 镜像）"]    end    subgraph KernelSpace ["Linux 内核"]        direction TB        Kernel["容器共享宿主机的内核"]    end    UserSpace --- KernelSpace</code></pre><p>Docker 容器：本质是一个自己的 <code>root</code> 文件系统，挂载在 OS 的内核之下运行，<strong>不包含自己的内核</strong>。</p><h4 id="分层存储">分层存储</h4><p>分成很多层，每一层都是基于上一层运行的。</p><p>体现在 <code>Dockerfile</code> 的不同的行中。</p><p>举一个<strong>陷阱式</strong>的例子来理解：</p><div class="code-wrapper"><pre><code class="hljs dockerfile"><span class="hljs-comment">## 错误示范 ❌</span><span class="hljs-keyword">FROM</span> ubuntu:<span class="hljs-number">24.04</span><span class="hljs-keyword">RUN</span><span class="language-bash"> apt-get update</span><span class="hljs-keyword">RUN</span><span class="language-bash"> apt-get install -y build-essential  <span class="hljs-comment"># 安装编译工具（约 200MB）</span></span><span class="hljs-keyword">RUN</span><span class="language-bash"> make &amp;&amp; make install                  <span class="hljs-comment"># 编译应用</span></span><span class="hljs-keyword">RUN</span><span class="language-bash"> apt-get remove build-essential        <span class="hljs-comment"># 试图删除编译工具</span></span><span class="hljs-comment">## 结果：镜像仍然包含 200MB 的编译工具！</span></code></pre></div><div class="code-wrapper"><pre><code class="hljs dockerfile"><span class="hljs-comment">## 正确做法 ✅</span><span class="hljs-keyword">FROM</span> ubuntu:<span class="hljs-number">24.04</span><span class="hljs-keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; \</span><span class="language-bash">    apt-get install -y build-essential &amp;&amp; \</span><span class="language-bash">    make &amp;&amp; make install &amp;&amp; \</span><span class="language-bash">    apt-get remove -y build-essential &amp;&amp; \</span><span class="language-bash">    apt-get autoremove -y &amp;&amp; \</span><span class="language-bash">    <span class="hljs-built_in">rm</span> -rf /var/lib/apt/lists/*</span><span class="hljs-comment">## 在同一层完成安装、使用、清理</span></code></pre></div><h4 id="镜像的标识">镜像的标识</h4><p>多种标识方式</p><ul><li><p><strong>镜像名称和标签</strong>：</p><ul><li>格式：<code>[仓库地址/]仓库名[:标签]</code>，但也有缩写形式，举例：</li><li>完整格式 <code>registry.example.com/myproject/myapp:v1.2.3</code></li><li>简写，默认仓库地址为官方的 Docker Hub：<code>nginx:1.28</code></li><li>省略标签，<code>nginx</code>，等价于 <code>nginx:latest</code></li></ul></li><li><p><strong>镜像 ID</strong></p>  <div class="code-wrapper"><pre><code class="hljs bash">$ docker imagesREPOSITORY   TAG       IMAGE ID       CREATED        SIZEnginx        latest    a6bd71f48f68   2 weeks ago    187MBubuntu       24.04     ca2b0f26964c   3 weeks ago    78.1MB</code></pre></div><p>此处的 <code>IMAGE ID</code> 即为。</p></li><li><p><strong>摘要</strong></p><ul><li>digest</li><li>镜像内容的唯一标识</li><li><strong>推荐使用这个代替标签</strong></li></ul></li></ul><h3 id="容器">容器</h3><ul><li><strong>容器是镜像的运行实例。</strong></li><li>如果把镜像比作程序，那么容器就是进程。</li><li>用 OOP 术语来说：<strong>镜像是类 (Class)，容器是对象 (Instance)</strong>。</li></ul><p>容器的特征：</p><ul><li>一个镜像可以创建多个容器</li><li>每个容器相互独立，互不影响</li><li>容器可以被创建、启动、停止、删除、暂停</li></ul><h4 id="容器的本质">容器的本质</h4><p><strong>容器的本质是一个特殊的进程</strong>。但是与一般的进程相比，有如下特质：</p><ul><li><strong>独立的进程空间</strong>：容器看不到宿主机上的其他进程。</li><li><strong>独立网络环境</strong>：在默认网络模式下，容器通常拥有独立的网络命名空间，并可分配独立 IP；使用 <code>host</code> 或 <code>container:</code> 等模式时则例外。</li><li><strong>独立文件系统</strong>：容器拥有独立的 root 目录。</li><li><strong>独立的用户空间</strong>：鉴于我不是很理解 Linux 的用户空间，这里暂且 TODO。</li></ul><h4 id="容器的存储层">容器的存储层</h4><p><strong>镜像层+容器层</strong>：假设容器基于的镜像有 $N$ 层，那么容器就是构建一个第 $N+1$ 层，可写，作为容器存储层。</p><p><strong>Copy-on-Write 写时复制</strong>：</p><p>当容器需要修改镜像层中的文件时：</p><ol><li>Docker 将该文件 <strong>复制</strong> 到容器存储层</li><li>在容器层中进行修改</li><li>原始镜像层保持不变</li></ol><p><strong>容器存储层的生命周期</strong>：和容器绑定，删除容器同时删除容器存储层。</p><p><strong>数据持久化范式</strong>：Docker 的最佳实践认为容器存储层应该保持<strong>无状态性</strong>，如果需要保留，应当使用数据卷 / 绑定挂载。</p><h4 id="容器的生命周期">容器的生命周期</h4><p>存在的状态：</p><ul><li>Created</li><li>Running</li><li>Paused</li><li>Stopped</li><li>Deleted</li></ul><h4 id="容器和进程的关系">容器和进程的关系</h4><p>容器的生命周期 = 主进程 (PID 1) 的生命周期</p><p>运行：</p><div class="code-wrapper"><pre><code class="hljs bash">docker run ubuntu</code></pre></div><p>容器会立刻退出：默认启动的程序没有持续工作，PID 1 很快结束了。使用：</p><div class="code-wrapper"><pre><code class="hljs bash">docker run -it ubuntu bash</code></pre></div><p>则 <code>bash</code> 成为 PID 1；只要你不退出这个 shell，容器就会继续运行。</p><h4 id="容器的隔离是如何实现的">容器的隔离是如何实现的</h4><p>使用 Linux 的 Namespace 机制。</p><h3 id="Docker-Registry">Docker Registry</h3><h4 id="核心概念">核心概念</h4><p><strong>Docker Registry 是存储和分发 Docker 镜像的服务，类似于代码的 GitHub 或包管理的 npm。</strong></p><p>Docker Registry 中可以包含多个 Repository，每个 Repository 可以包含多个 Tag。</p><p>这也就是为什么，镜像的完整名称是 <code>[registry 地址/][用户名/]仓库名[:标签]</code>。</p><div class="note note-info"><p>如果不指定 Registry 地址，默认使用 Docker Hub。如果不指定标签，默认使用 <code>latest</code>。</p></div><h4 id="公共-Registry-服务">公共 Registry 服务</h4><p><a href="https://hub.docker.com/">Docker Hub</a> 是最大的公共 Registry，也是 Docker 的默认 Registry。</p><ul><li>免费账户可以创建公开仓库</li><li>免费个人账户可创建 1 个私有仓库；更高套餐支持更多私有仓库</li></ul><p>还有一些镜像源比如 Github Container Registry，Google 的，阿里巴巴的，腾讯的等。</p><p>可以配置<strong>镜像加速器</strong>来加速：</p><div class="code-wrapper"><pre><code class="hljs json"><span class="hljs-comment">// /etc/docker/daemon.json</span><span class="hljs-punctuation">{</span>  <span class="hljs-attr">"registry-mirrors"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>    <span class="hljs-string">"https://your-accelerator-url"</span>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">}</span></code></pre></div><h4 id="私有-Registry">私有 Registry</h4><p>云厂商有提供服务，可以自己看，比如阿里云 ACR、腾讯云 TCR、AWS ECR 等。</p><p>有时需要使用 <code>docker login</code> 来登录到 Docker Registry。</p><h4 id="镜像的推送和拉取">镜像的推送和拉取</h4><div class="code-wrapper"><pre><code class="hljs plaintext">开发者机器                    Registry                    生产服务器     │                           │                             │     │  docker build             │                             │     │  构建镜像                  │                             │     │                           │                             │     │  docker push ─────────────▶                             │     │  推送镜像                  │  存储镜像                   │     │                           │                             │     │                           │  ◀───────────── docker pull │     │                           │                  拉取镜像    │     │                           │                             │     │                           │                  docker run │     │                           │                  运行容器    │</code></pre></div><h2 id="使用镜像">使用镜像</h2><h3 id="获取镜像">获取镜像</h3><details class="collapsible-block collapsible-block--code"><summary class="collapsible-block__summary" title="代码 BASH · 23 行"><span class="collapsible-block__chevron" aria-hidden="true"></span><span class="collapsible-block__label">代码</span><span class="collapsible-block__meta">BASH · 23 行</span></summary><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 完整格式</span>$ docker pull docker.io/library/ubuntu:24.04<span class="hljs-comment">## 省略 Registry（默认 Docker Hub）</span>$ docker pull library/ubuntu:24.04<span class="hljs-comment">## 省略 library（官方镜像）</span>$ docker pull ubuntu:24.04<span class="hljs-comment">## 省略标签（默认 latest）</span>$ docker pull ubuntu<span class="hljs-comment">## 拉取第三方镜像</span>$ docker pull bitnami/redis:latest<span class="hljs-comment">## 从其他 Registry 拉取</span>$ docker pull ghcr.io/username/myapp:v1.0</code></pre></div></details><h4 id="下载内容解析">下载内容解析</h4><div class="code-wrapper"><pre><code class="hljs bash">$ docker pull ubuntu:24.0424.04: Pulling from library/ubuntu92dc2a97ff99: Pull completebe13a9d27eb8: Pull completec8299583700a: Pull completeDigest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26Status: Downloaded newer image <span class="hljs-keyword">for</span> ubuntu:24.04docker.io/library/ubuntu:24.04</code></pre></div><table><thead><tr><th>输出内容</th><th>说明</th></tr></thead><tbody><tr><td><code>Pulling from library/ubuntu</code></td><td>正在从官方 ubuntu 仓库拉取</td></tr><tr><td><code>92dc2a97ff99: Pull complete</code></td><td>各层的下载状态 (显示层 ID 前 12 位)</td></tr><tr><td><code>Digest: sha256:...</code></td><td>镜像内容的唯一摘要</td></tr><tr><td><code>docker.io/library/ubuntu:24.04</code></td><td>镜像的完整名称</td></tr></tbody></table><p>可以看到镜像是<strong>分层下载</strong>的。</p><div class="note note-success"><p><strong>对于 <code>pull</code></strong>：</p><ul><li><code>--quiet -q</code> 可以静默安装</li><li><code>--platform</code> 可以指定平台架构</li></ul></div><h4 id="关于摘要">关于摘要</h4><p><strong>查看镜像摘要</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker images --digests ubuntuREPOSITORY   TAG     DIGEST                                                                    IMAGE IDubuntu       24.04   sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26   ca2b0f26964c</code></pre></div><p>然后<strong>使用摘要拉取</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26</code></pre></div><div class="note note-success"><p>生产环境使用摘要而非标签，因为标签可能被覆盖，摘要则是不可变的。</p></div><h4 id="磁盘空间不足的解决方案">磁盘空间不足的解决方案</h4><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 清理未使用的镜像</span>$ docker image prune<span class="hljs-comment">## 清理所有未使用资源</span>$ docker system prune</code></pre></div><h3 id="查看镜像">查看镜像</h3><h4 id="基本用法">基本用法</h4><div class="code-wrapper"><pre><code class="hljs bash">$ docker imagesREPOSITORY   TAG       IMAGE ID       CREATED        SIZEredis        latest    5f515359c7f8   5 days ago     183MBnginx        latest    05a60462f8ba   5 days ago     181MBubuntu       24.04     329ed837d508   3 days ago     78MBubuntu       noble     329ed837d508   3 days ago     78MB</code></pre></div><div class="note note-info"><p>关于 <code>ubuntu:24.04</code> 和 <code>ubuntu:noble</code>：</p><p><code>ubuntu:24.04</code> 是具体版本号，<code>ubuntu:noble</code> 是发布代号。</p><p>拥有相同的 <code>IMAGE ID</code>，是同一个镜像的不同标签，只占用一份存储空间。</p></div><div class="note note-success"><p>其实基本命令是 <code>docker image</code>，而 <code>docker images</code> 是 <code>docker image ls</code> 的 alias。</p></div><h4 id="理解镜像的大小">理解镜像的大小</h4><p><strong>本地大小和 Docker Hub 显示的大小</strong>：</p><ul><li>前者是本地解压后的实际大小</li><li>后者是压缩后的网络传输大小</li></ul><p><strong>实际磁盘占用</strong>：</p><ul><li>由于镜像分层存储，不同的镜像共享不同的层</li><li>故 $\sum{\text{size}} \gt \text{实际磁盘占用}$。</li></ul><p><strong>如何查看实际空间的占用</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker system <span class="hljs-built_in">df</span>TYPE            TOTAL   ACTIVE   SIZE      RECLAIMABLEImages          15      3        2.5GB     1.8GB (72%)Containers      5       2        100MB     80MB (80%)Local Volumes   8       2        500MB     400MB (80%)Build Cache     0       0        0B        0B</code></pre></div><h4 id="查找特定的镜像（过滤镜像）">查找特定的镜像（过滤镜像）</h4><p><strong>按镜像名过滤</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker images ubuntu$ docker images ubuntu:24.04</code></pre></div><p><strong>使用过滤器</strong>：</p><p><code>--filter -f</code></p><p>有一系列的过滤条件，这个感觉记了也记不住，知道就好。</p><h4 id="虚悬镜像-dangling-images">虚悬镜像 dangling images</h4><p>仓库名和标签都显示为 <code>&lt;none&gt;</code> 的镜像。</p><p><strong>产生原因</strong>：</p><ul><li><strong>镜像重新构建</strong>：新镜像使用了旧镜像的标签，旧镜像标签被移除</li><li><strong>docker pull 更新</strong>：拉取更新版本时，旧版本失去标签（<code>latest</code>）</li></ul><p><strong>处理方式</strong>：</p><ul><li>列出：<code>docker images -f dangling=true</code></li><li>删除：<code>docker image prune</code></li></ul><h4 id="中间层镜像">中间层镜像</h4><p><code>docker images</code> 只列出顶层镜像，为了查看中间镜像，使用 <code>docker images -a</code>。</p><div class="note note-primary"><p><strong>永远不需要手动删除中间层镜像</strong>。</p></div><h4 id="格式化输出">格式化输出</h4><p>主要是命令之间的搭配使用。</p><p><strong>只输出 ID</strong>：<code>docker images -q</code></p><p>举例：删除所有 redis 镜像：<code>docker rmi $(docker images -q redis)</code></p><div class="note note-info"><p>不过其实我觉得个人开发而言，用 Docker Desktop GUI 管理更方便。</p><p>对于 Agent 而言，这些东西自然语言阐述即可。</p></div><p><strong>显示摘要</strong>：<code>docker images --digests</code></p><h3 id="删除镜像">删除镜像</h3><h4 id="基本用法-2">基本用法</h4><div class="code-wrapper"><pre><code class="hljs bash">$ docker image <span class="hljs-built_in">rm</span> [选项] &lt;镜像1&gt; [&lt;镜像2&gt; ...]</code></pre></div><div class="note note-success"><p><code>docker rmi</code> 是 <code>docker image rm</code> 的简写，两者等效。</p></div><h4 id="镜像标识方式">镜像标识方式</h4><ul><li>短 ID（可以标识的前几位）</li><li>完整 ID</li><li>镜像名:标签</li><li>镜像摘要<ul><li>最精确，适合 CI/CD 场景</li></ul></li></ul><h4 id="删除镜像的输出信息">删除镜像的输出信息</h4><div class="code-wrapper"><pre><code class="hljs bash">$ docker rmi redis:alpineUntagged: redis:alpineUntagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86dDeleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899bDeleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23</code></pre></div><ul><li><strong>Untagged</strong>：移除镜像的标签</li><li><strong>Deleted</strong>：删除镜像的存储层</li></ul><p>删除流程：</p><p>先检查指向该镜像的标签并逐个 untag，然后再逐层删除镜像，被使用则保留，不被使用则删除。</p><h4 id="批量删除">批量删除</h4><p>这里大概自然语言描述一下能做到哪些吧：</p><ul><li>删除所有虚悬镜像，<code>docker image prune</code></li><li>删除一段时间内未使用的镜像：<code>docker image prune -a --filter "until=24h"</code></li><li>按照条件删除，比如：<ul><li>删除所有 redis 镜像</li><li>删除某个时间点之前的镜像</li><li>删除某个版本号之前的镜像</li></ul></li></ul><h4 id="删除失败的原因">删除失败的原因</h4><ul><li>存在容器依赖这个镜像</li><li>这个镜像作为中间层被其他镜像依赖</li><li>多个标签指向同一个镜像，比如 ubuntu 的 <code>24.04</code> 和 <code>latest</code> 同时指向同一个 <code>IMAGE ID</code></li></ul><h4 id="清理策略">清理策略</h4><p><strong>开发环境</strong>：</p><ul><li>定期清理 dangling images</li><li>清理未使用的资源 <code>docker system prune -a</code></li></ul><p><strong>CI/CD</strong>：</p><ul><li>只保留最近使用过的镜像（即一键删除一段时间内未使用过的镜像）</li></ul><p><strong>查看空间占用</strong>：</p><ul><li><code>docker system df</code></li></ul><h3 id="使用-docker-commit">使用 <code>docker commit</code></h3><p>把某个容器当前在文件系统上的改动，固化成一个新的 Docker 镜像。</p><p>命令格式：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker commit [OPTIONS] 容器名或容器ID 新镜像名[:标签]</code></pre></div><p>举个例子，进入容器之后，修改了里面文件系统的一些内容，</p><p>执行</p><div class="code-wrapper"><pre><code class="hljs bash">docker commit dev my-ubuntu:v1</code></pre></div><p>实际上做的：</p><p>原镜像的只读层 + dev 容器的可写层 -&gt; 转化成新的只读镜像层 -&gt; 新的镜像</p><p>原理是让新镜像继续复用原来的镜像层，再增加一层，用来保存这个容器相对于原镜像产生的文件系统变化。</p><h4 id="缺点">缺点</h4><p>使用 <code>docker commit</code> 定制的镜像存在如下问题：</p><ul><li>执行命令会连着根式地修改一大堆无关的文件，可以通过 <code>docker diff webserver</code> 看出；</li><li>对镜像的操作都是黑箱操作；</li><li>层数会膨胀</li></ul><h3 id="使用-Dockerfile-定制镜像">使用 <code>Dockerfile</code> 定制镜像</h3><p>解决上面所说的问题。</p><p><code>Dockerfile</code> 是一个包含了一条条的<strong>指令</strong>的文本文件：</p><ul><li>会修改文件系统的指令通常会创建新层；</li><li>而 <code>LABEL</code>、<code>CMD</code> 这类只修改镜像元数据的指令，则不会新增文件系统层。</li></ul><h4 id="创建-Dockerfile">创建 <code>Dockerfile</code></h4><p>比较详细的命令后面单开一节介绍。</p><div class="note note-info"><p>不过我觉得也没什么必要，知道基本概念最重要。</p></div><p><strong><code>FROM</code> 指定基础镜像</strong>：</p><p>所谓定制镜像，是以一个镜像为基础，在其上进行定制。所以需要 <code>FROM</code> 指定的基础镜像。</p><p>除了选择现有镜像为基础镜像外，Docker 还存在一个特殊的镜像，名为 <code>scratch</code>。这个镜像是虚拟的概念，并不实际存在，它表示一个空白的镜像。</p><div class="code-wrapper"><pre><code class="hljs docker"><span class="hljs-keyword">FROM</span> scratch...</code></pre></div><p>如果以 <code>scratch</code> 为基础镜像的话，意味着你不以任何镜像为基础，接下来所写的指令将作为镜像第一层开始存在。</p><p>不以任何系统为基础，直接将可执行文件复制进镜像的做法并不罕见，对于 Linux 下静态编译的程序来说，并不需要有操作系统提供运行时支持，所需的一切库都已经在可执行文件里了，因此直接 <code>FROM scratch</code> 会让镜像体积更加小巧。使用 <a href="https://golang.google.cn/">Go 语言</a>开发的应用很多会使用这种方式来制作镜像，这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。</p><p><strong><code>RUN</code> 执行命令行命令</strong>：</p><ul><li>shell 格式：<code>RUN &lt;命令&gt;</code></li><li>exec 格式：<code>RUN ["可执行文件", "参数1", "参数2"]</code></li></ul><div class="note note-warning"><p>每一个 <code>RUN</code> 指令都会产生一个新的镜像层。为了减少镜像体积和层数，我们通常会将多个命令合并到一个 <code>RUN</code> 指令中执行。</p></div><h4 id="构建镜像">构建镜像</h4><div class="code-wrapper"><pre><code class="hljs bash">docker build [选项] &lt;上下文路径/URL/-&gt;</code></pre></div><p>执行之后的输出，含义还挺明显的，没什么必要单独扯出来记。</p><h4 id="镜像构建上下文">镜像构建上下文</h4><p>关于什么是<strong>上下文路径</strong>：构建器可以访问到的文件集合。</p><p>比如 <code>Dockerfile</code> 里面会写 <code>COPY</code> 和 <code>RUN</code> 指令，假设有这样的：</p><div class="code-wrapper"><pre><code class="hljs dockerfile"><span class="hljs-keyword">COPY</span><span class="language-bash"> ./package.json /app/</span></code></pre></div><p>这里复制的就是<strong>上下文</strong>目录下的 <code>package.json</code>。</p><p>而假设写成：</p><div class="code-wrapper"><pre><code class="hljs dockerfile"><span class="hljs-keyword">COPY</span><span class="language-bash"> ../package.json /app</span></code></pre></div><p>这里的 <code>..</code> 属于<strong>越界</strong>，构建器都无法读取上下文之外的宿主机文件。</p><p>由此可见，<strong>上下文，和 <code>Dockerfile</code> 所在的目录，没有关系。</strong>假设把上下文直接指定成 <code>/</code> ，理论是可行的，不过 BuildKit 的可见上下文会过大导致构建缓慢甚至失败。</p><p>而所谓的 <code>.dockerignore</code> 文件（通常置于项目根目录下，和 <code>Dockerfile</code> 平级，采用和 <code>.gitignore</code> 一样的 Glob 语法），就是为了忽略掉在构建时不希望传给 Docker 引擎的文件。</p><h4 id="其余-docker-build-的用法">其余 <code>docker build</code> 的用法</h4><ul><li>从 Git Repo 中构建</li><li>用给定的压缩包构建</li><li>从标准输入中读取 Dockerfile 进行构建 <code>cat Dockerfile | docker build -</code></li></ul><h2 id="操作容器">操作容器</h2><h3 id="启动">启动</h3><p>启动容器有两种方式：</p><ul><li><strong>新建并启动</strong>：基于镜像创建新容器 <code>docker run</code></li><li><strong>重新启动</strong>：将已终止的容器重新运行 <code>docker start</code></li></ul><p>由于 Docker 容器非常轻量，实际使用中常常是随时删除和新建容器，而不是反复重启同一个容器。</p><h4 id="新建并启动">新建并启动</h4><div class="code-wrapper"><pre><code class="hljs bash">docker run [选项] 镜像 [命令] [参数...]</code></pre></div><p><strong>交互式容器</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker run -it ubuntu:24.04 /bin/bashroot@af8bae53bdd3:/#</code></pre></div><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>-i</code></td><td>保持标准输入 (stdin) 打开，允许输入</td></tr><tr><td><code>-t</code></td><td>分配伪终端 (pseudo-TTY)，提供终端界面</td></tr><tr><td><code>-it</code></td><td>两者组合使用，获得交互式终端</td></tr></tbody></table><h4 id="启动选项">启动选项</h4><table><thead><tr><th>选项</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td><code>-d</code></td><td>后台运行 (detach)</td><td><code>docker run -d nginx:latest</code></td></tr><tr><td><code>-it</code></td><td>交互式终端</td><td><code>docker run -it ubuntu:24.04 bash</code></td></tr><tr><td><code>--name</code></td><td>指定容器名称</td><td><code>docker run --name myapp nginx:latest</code></td></tr><tr><td><code>--rm</code></td><td>退出后自动删除容器</td><td><code>docker run --rm ubuntu:24.04 echo hi</code></td></tr></tbody></table><p><strong>端口映射</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 将容器的 80 端口映射到宿主机的 8080 端口</span>$ docker run -d -p 8080:80 nginx:latest<span class="hljs-comment">## 只绑定到 localhost</span>$ docker run -d -p 127.0.0.1:8080:80 nginx:latest</code></pre></div><p>映射端口这一点很重要，比如像 nginx 等服务，不暴露出端口，外部根本无法访问。</p><div class="note note-success"><p>网络相关的东西，等到网络专题再细究吧。</p></div><p><strong>数据卷挂载</strong></p><p>略，懒得记了</p><p><strong>环境变量</strong></p><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 设置单个环境变量</span>$ docker run -e MYSQL_ROOT_PASSWORD=secret mysql<span class="hljs-comment">## 从文件加载环境变量</span>$ docker run --env-file .<span class="hljs-built_in">env</span> myapp</code></pre></div><p>这个倒是，容器有时候是需要诸如 <code>.env</code> 文件里面的环境变量的，假设 <code>.dockerignore</code> 了之后。</p><p>但是感觉把 <code>.env</code> 文件 docker ignore 掉的意义也不大，为什么要这样做呢？</p><p>Answer from ChatGPT，我归纳一下：</p><p>首先，肯定不能把 <code>.env</code> 放进镜像中啊（回忆一下本节的标题「<strong>新建并启动</strong>」），因为镜像是要<strong>分发</strong>的，分发出去，直接访问里面的 <code>.env</code> 文件不完蛋了，所以肯定要忽略掉。</p><p>其次，这样做的话，镜像和运行环境就解耦合了。</p><p><strong>资源限制</strong>：</p><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 限制内存</span>$ docker run -m 512m nginx:latest<span class="hljs-comment">## 限制 CPU</span>$ docker run --cpus=1.5 nginx:latest</code></pre></div><h4 id="重新启动容器">重新启动容器</h4><p>使用 <code>docker start</code>，后面跟上容器名。</p><p>获取容器名，使用 <code>docker ps -a</code>。</p><h3 id="守护态运行">守护态运行</h3><p>当在终端运行一个程序时，有两种模式：</p><ul><li><strong>前台运行</strong>：程序占用当前终端，输出直接显示，关闭终端程序就停止</li><li><strong>后台运行</strong>：程序在后台执行，不占用终端，终端关闭也不影响程序</li></ul><p>Docker 容器默认是 <strong>前台运行</strong> 的。使用 <code>-d</code> (detach) 参数可以让容器在后台运行。</p><h4 id="理解为什么容器会立即退出">理解为什么容器会立即退出</h4><p><strong>核心原理</strong>：容器的生命周期与主进程绑定。</p><p>举个例子，即使使用了 <code>-d</code> 参数运行：</p><div class="code-wrapper"><pre><code class="hljs bash">$ docker run -d ubuntu:24.04</code></pre></div><p>再使用 <code>docker ps</code> 也看不见容器在运行，原因：</p><ol><li>容器启动</li><li>没有指定命令，默认执行 <code>/bin/bash</code></li><li>但没有交互式终端 (没有 <code>-it</code> 参数)，bash 发现没有输入源</li><li>bash 立即退出</li><li>主进程退出，容器停止</li></ol><div class="note note-primary"><p><strong><code>-d</code> 参数是让容器 “在后台运行”，能运行多久取决于主进程</strong></p></div><h4 id="查看容器">查看容器</h4><div class="code-wrapper"><pre><code class="hljs bash">$ docker container <span class="hljs-built_in">ls</span>$ docker ps<span class="hljs-comment"># 前者是后者的简写</span><span class="hljs-comment"># 查看日志</span>$ docker container logs 77b2dc01fe0f<span class="hljs-comment"># 实时查看日志</span>$ docker container logs -f 77b2dc01fe0f</code></pre></div><div class="note note-success"><p>对于一次性任务，使用 <code>--rm</code> 参数让容器退出后自动删除。</p></div><h3 id="终止">终止</h3><p><code>docker stop</code> 优雅终止</p><p><code>docker kill</code> 直接终止</p><h3 id="进入容器">进入容器</h3><p><code>-d</code> 启动之后，有时候需要进入容器进行操作。</p><div class="code-wrapper"><pre><code class="hljs bash"><span class="hljs-comment">## 进入容器并启动交互式 shell</span>$ docker <span class="hljs-built_in">exec</span> -it 容器名 /bin/bash<span class="hljs-comment">## 或使用 sh（适用于 Alpine 等精简镜像）</span>$ docker <span class="hljs-built_in">exec</span> -it 容器名 /bin/sh</code></pre></div><h3 id="导入和导出">导入和导出</h3><p><code>docker export</code> <code>docker import</code></p><h3 id="删除">删除</h3><p><code>docker rm</code></p><div class="note note-info"><p><code>docker rm</code> 是 <code>docker container rm</code> 的简写，两者等效。</p></div><h2 id="Dockerfile-指令详解">Dockerfile 指令详解</h2><h2 id="数据管理">数据管理</h2><h3 id="数据卷">数据卷</h3><p>首先需要 <code>$ docker volume create my-vol</code> 创建一个数据卷，然后新建容器的时候，使用 <code>--mount</code> 或者 <code>-v</code> 挂载数据卷。</p><h3 id="挂载主机目录">挂载主机目录</h3><div class="note note-info"><p><strong>数据卷（volume）</strong>：由 Docker 管理的存储。你只需要指定卷名，Docker 负责在主机上创建、保存和定位实际数据目录。</p><div class="code-wrapper"><pre><code class="hljs bash">docker run -v mydata:/app/data nginx<span class="hljs-comment"># 名为 mydata 的数据卷，映射到容器的 /app/data 目录</span></code></pre></div><p><strong>挂载主机目录（bind mount）</strong>：把你指定的主机目录直接映射到容器中。</p><div class="code-wrapper"><pre><code class="hljs bash">docker run -v /home/user/data:/app/data nginx</code></pre></div><p>核心区别：</p><ul><li><strong>volume</strong>：你管卷名，Docker 管实际存放位置，适合数据库等持久化数据。</li><li><strong>bind mount</strong>：你自己管理主机路径，适合开发时挂载代码、配置文件。</li><li><strong>可移植性</strong>：volume 不依赖具体主机路径；bind mount 依赖主机目录结构。</li></ul></div><h2 id="网络配置">网络配置</h2>]]>
      </content:encoded>
    </item>
    <item>
      <title>数据库系统原理课程的一些经验</title>
      <link>https://blog.wendain.ing/2026/07/04/db-course-experience/</link>
      <description>
        <![CDATA[<h2 id="前言">前言</h2>
<p>刚刚（2026-07-04 22:55）数据库出了个分，</p>]]>
      </description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%95%B0%E6%8D%AE%E5%BA%93/">数据库</category>
      <category domain="https://blog.wendain.ing/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/">数据库</category>
      <category domain="https://blog.wendain.ing/tags/%E7%BB%8F%E9%AA%8C/">经验</category>
      <pubDate>Sat, 04 Jul 2026 22:55:56 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="前言">前言</h2><p>刚刚（2026-07-04 22:55）数据库出了个分，</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>操作系统 26春 期末考试</title>
      <link>https://blog.wendain.ing/2026/07/01/os-sp26-final/</link>
      <description>回忆版本</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/">操作系统</category>
      <category domain="https://blog.wendain.ing/tags/%E5%BE%80%E5%B9%B4%E5%8D%B7/">往年卷</category>
      <category domain="https://blog.wendain.ing/tags/OS/">OS</category>
      <pubDate>Wed, 01 Jul 2026 14:07:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="1-单选-1-20">1. 单选 1%*20</h2><p>我感觉很多王道原题</p><h2 id="2-填空-1-10">2. 填空 1%*10</h2><p>操作系统是管理（）的软件</p><p>共享变量属于（）资源</p><p>磁盘性能评估时间的三个指标（）（）（）</p><h2 id="3-判断-1-10">3. 判断 1%*10</h2><p>忘了有什么了</p><h2 id="4-简答-5-4">4. 简答 5%*4</h2><p>非抢占式和抢占式调度哪个更消耗系统资源，给出原因</p><p>解释死锁的发生原因以及列出四个必要条件</p><p>简要分析，对比页式和段式存储</p><p>简述什么是文件的物理结构，列举出来，哪个适合大文件</p><h2 id="5-综合题-7-10-10-7-6">5. 综合题 7+10+10+7+6</h2><h3 id="1">1.</h3><p>一个 PV 信号量设计题</p><p>公交车司机-售票员</p><p>开车-关门开门-售票</p><p>原题感觉表述也不是很清楚</p><h3 id="2">2.</h3><div class="code-wrapper"><pre><code class="hljs c"><span class="hljs-type">int</span> A[<span class="hljs-number">100</span>][<span class="hljs-number">100</span>];<span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) {    <span class="hljs-keyword">for</span> (<span class="hljs-type">int</span> j = <span class="hljs-number">0</span>; j &lt; <span class="hljs-number">100</span>; j++) {        A[i][j] = <span class="hljs-number">0</span>;    }}</code></pre></div><p>一页最多装 200 个 <code>int</code>，数组行优先存储，主存最多 3 页，开始的时候存 <code>i</code> <code>j</code> 的页已在主存中，采用 LRU 置换算法</p><p>分析发生缺页的次数，以及执行完毕之后哪两个页会在主存中，对应数组的哪个部分</p><h3 id="3">3.</h3><p>一个访问序列（我记得是 13 条），共 7 页，主存最多装 4 页，分别用 LRU 和 FIFO 分析缺页率</p><h3 id="4">4.</h3><p>给出索引项大小 4B 和磁盘块大小 2K，直接索引 8 个，一级索引二级索引各两个，算最大文件大小</p><h3 id="5">5.</h3><p>给出一个磁盘访问序列，用 FCFS 和最近优先计算用时，磁道移动一个耗时 3ms</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>为什么我不推荐入学之后转专业？</title>
      <link>https://blog.wendain.ing/2026/06/29/why-you-shouldnt-transfer-major-if-you-want-to-study-cs/</link>
      <description>一个转专业的回忆录</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%9D%82%E8%B0%88%E4%B8%8E%E9%9A%8F%E6%83%B3/">杂谈与随想</category>
      <category domain="https://blog.wendain.ing/tags/%E6%9D%82%E8%B0%88/">杂谈</category>
      <category domain="https://blog.wendain.ing/tags/%E8%BD%AC%E4%B8%93%E4%B8%9A/">转专业</category>
      <pubDate>Mon, 29 Jun 2026 14:07:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近高考完，很多应届高考生发愁志愿填报，这里我做一个劝退。</p><p>稍微有点标题党了，注意以下的限制条件：</p><ul><li><strong>想去计算机类专业</strong>（电子信息类或许也能算？我不清楚，自行判断）；</li><li>分数是中九分段，有点比上不足比下有余，举例就是在我高考的江西省可以去厦大、天大、南开这样的 985 挑一挑专业，而武大、华科、同济、西交基本都是擦边进<ul><li>其实我估计也适用于别的一些分段，但是还是对我这个情况最有价值；</li><li>没有说前面分数线较低的学校不好的意思，只是确实在江西省分数偏低一点。</li></ul></li></ul><p>个人情况是 2024 年江西高考，发挥稍微炸了一点，638分，全省排名 2100 左右。高考完心情非常之差，志愿可以说是随便乱填，思考时间不超过 30 分钟，做好了滑档复读的准备，只填了三所学校：清华、同济、北邮，最后被同济中外合办专业录取，大一下转入计算机学院。</p><p>现在回顾起来看，其实比较后悔当时没去天大、南开、北邮这样的学校的计算机专业一步就位的。</p><blockquote><p>不过我本身就没思考</p></blockquote><div class="note note-info"><p>顺便回忆一下当时的心路历程：我想的是随便报，录到哪都行，反正可以转专业，转不出就退学复读。</p><p>结果没想到转专业里面转成了真的只是最小的一步，，</p></div><p>所以也写篇文章劝退一下想和我一样做的应届高考生。</p><p>不推荐这样做的原因有三个：</p><h2 id="1-这几所学校档次没有本质区别">1. 这几所学校档次没有本质区别</h2><p>高考生最难摒弃的就是<strong>亏分思维</strong>，觉得投档分数线差了这么几百名，仿佛这几所学校就有了天壤之别。但是我相信任何一位就读于这些中九，乃至于再往下一档的 985 / 211 的计算机系学生，<strong>都不会认为这些学校就业有什么区别</strong>（如果有同学能在评论区多现身说法一下就好了）。如果是硕博毕业去投算法岗，大部分情况看你硕博期间的产出；如果是投开发岗，这样的学历足够你进面试，进面之后众生平等，看个人发挥。</p><p>而事实上「<strong>亏分</strong>」从绝对意义上也是伪命题：某几所高校在我省三四年前还是 2000 名就能去，如今没有 600 名想都别想；而也有几所高校投档线退步略大。<strong>请你思考一下，难道真的是这些学校在这么短短几年内实力发生了质变吗</strong>？无非是招生的手段先进了一些，或者是该校的强势专业这两年火起来了罢了。而你入学到本科毕业乃至硕博毕业，有 4~10 年左右的时间，这些学校的投档线发生洗牌的概率很大，<strong>你到时候会作何感想呢</strong>？实际上入校半年多就会觉得所谓的「亏分」是纯粹瞎扯了。</p><h2 id="2-转专业，不止是转专业">2. 转专业，不止是转专业</h2><p>这一点很少有人提到，大部分人只会提转专业转过去很难，不过其实我觉得对于高考失利的人来说转走的概率还是挺大的，这里暂且不提了，<strong>不过还是要考虑清楚失败的风险</strong>。</p><p>转专业的一年，你的时间精力基本都要花费在学本专业的课程上。如果本专业课业轻松，那算还好；但是如果和我一样，课时算是全校最拉满的那一档的话，你基本不会有什么自由时间探索，<strong>而这一点非常非常非常非常重要</strong>，我觉得给高考生怎么强调都不为过。</p><img src="https://image.wendaining.top/image-20260629133443052.png" alt="大一下学期的课表" style="zoom:50%;"><p>如果你非常确定自己想学计算机，那「<strong>能不能一开始就在计算机类专业</strong>」本身就是一个<strong>很重要的优势</strong>。因为大学前两年并不是可以随便浪费的缓冲期，尤其对计算机专业来说，很多事情都是越早开始越好：做项目、找实习、进组、尝试科研方向，等等等等。</p><p>等你真的转入了之后，你会发现转专业的一年可以算是半浪费了，你没有学任何意向的专业的知识这会导致：</p><ul><li>现在计算机越来越卷，越早开始的时间收益是<strong>非常非常大的</strong>。见到太多大学四年除了 GPA 一无所有，保个本校水几篇论文接着干开发，或者等到大三才意识到自己适合什么不适合什么，<strong>所付出的时间成本和沉没成本会让你十分痛苦</strong>；</li><li>试想如果你大一没有转专业的压力，可以去尝试诸如科研 / 做项目 等等，找到自己的兴趣点，然后大二就可以开始 all in 某个方向了。小红书随便一刷就能刷到一大把的大二大厂实习、大二发论文，时间非常宝贵，并非危言耸听；</li><li>转完了还要继续补课，我只能说沉没成本进一步加剧了。很多转专业的同学（这里只说我在同济看到的）的路径是：大一卷转专业，大二补一大堆课，大三发现完蛋了，自己手上什么都拿不出来。</li></ul><h2 id="3-摒弃上课思维">3. 摒弃上课思维</h2><p>对于计算机类专业，学校的课 90% 是<strong>一点用也没有</strong>（不能说课本身没用，只能说教学让学到的和实际有用的完全正交）。它们和就业、科研、项目能力之间往往没什么关系。真正决定你后续竞争力的，很多时候是课堂之外的探索、实践和自学能力。详情可以看这篇博客 <a href="https://blog.lyc8503.net/post/4-years-at-nju/">在南京大学的四年 - 软件工程与纸上谈兵</a>，作者是南京大学软件学院毕业生，华五尚且如此，所以别对国内高校教学质量抱有什么很高的期待。</p><p>因而，课越少是越好的，<strong>但是对于转专业的人而言，不可避免要修一堆课</strong>，<strong>纯粹的浪费时间</strong>。这里也建议高考生别被什么优秀师资，能上很多看着很有用的课的招生幌子给骗了，<strong>本科专业基本就是课越少，自由时间越多越好</strong>。</p><p>有很多自由可支配的时间是<strong>非常宝贵的</strong>，无论是实习、进组打工、或者只是放松，都比学一堆化石般的课程要有用得多。</p><h2 id="总结">总结</h2><p>Generated by ChatGPT:</p><blockquote><p>如果你明确想去计算机，并且有机会在同档或略低一档学校一步到位，那就尽量不要为了一个看起来更好听的学校名字去赌转专业。转专业不是不能成功，而是它远比高考生想象中更消耗时间、更消耗心态，也更容易让你错过大学前两年最宝贵的试错窗口。</p></blockquote>]]>
      </content:encoded>
    </item>
    <item>
      <title>算法设计与分析 26春 期末考试</title>
      <link>https://blog.wendain.ing/2026/06/25/algorithm-sp26-final/</link>
      <description>同样是考完一个小时内的回忆，但是多选题几乎也想不起来了，希望能有用吧。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90/">算法设计与分析</category>
      <category domain="https://blog.wendain.ing/tags/%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90/">算法设计与分析</category>
      <category domain="https://blog.wendain.ing/tags/%E5%BE%80%E5%B9%B4%E9%A2%98/">往年题</category>
      <pubDate>Thu, 25 Jun 2026 13:37:17 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="不定项选择-15-2">不定项选择 15*2%</h2><blockquote><p>少选多选漏选错选均不得分</p></blockquote><p>重点：排序的稳定性，属于什么解决方法</p><p>什么问题属于什么解决方法（比如合并排序属于分治）</p><div class="note note-primary"><p>题目我想不起来了，但是你可以做的：给 LLM 发送这段 Prompt：</p><blockquote><p>这是我们这门课的考试的题型：</p><blockquote><p>算法课考试题型</p><p>第一大题：不定项选择题，15题，每题2分,共30分。四个选项，可以选1~4个，少选多选漏选错选均不得分</p><p>第二大题：简答题，4题，共28分。</p><p>第三大题：算法应用题,4题,共42分。</p><p>复习与答题要点</p><p>简答题务必扣住题目核心,只写关键词,不要东拉西扯、不要靠堆字数凑分,达不到核心就赶紧做其他题。</p><p>算法应用题若要求写算法思想,题目没指定形式时可自由发挥(伪代码、流程图、自然语言、代码均可);若题目明确要求某种形式,就按要求来。</p></blockquote><p>请你先帮我生成30个不定项选择，不定项选择大部分以考察概念为主。我要自己练习。我希望最好是交互式的，如果不能是，那也可以是先给我题目和答案分离。</p></blockquote><p>再加之以课件，可以生成和考试的不定项<strong>很相似</strong>的多选（本人亲测）。</p></div><h2 id="简答题-28">简答题 28%</h2><h3 id="1">1</h3><ol><li>简述分治法和减治法的思想</li><li>各举两个例子</li></ol><h3 id="2">2</h3><ol><li>一个问题能用动态规划求解，需要满足哪两个条件？</li><li>说明这两个条件</li></ol><h3 id="3">3</h3><p>考虑经典的找零问题，有<code>[1, 3, 4]</code>这三种面额，需要找<code>6</code>元，最少的方案：</p><ol><li>使用贪心求解，说明贪心的思路，给出结果</li><li>给出正确的结果</li><li>解释为什么贪心无法得到正确结果</li></ol><h3 id="4">4</h3><ol><li>阐述分支限界法和回溯法的区别</li><li>各举一例使用这两种方法能解决的问题</li><li>解释为什么分支界限法通常效率较高</li></ol><h2 id="算法题-42">算法题 42%</h2><blockquote><p>给的数据记不太清楚了，让 AI 编的，题目的意思都是对的。</p></blockquote><h3 id="1-8">1 8%</h3><p>司机开车，一路上有<code>n</code>个收费站，抵达第<code>i</code>个收费站，需要收费<code>cost[i]</code>。司机一次最多往前开 1 或 2 个收费站。设计一个动态规划算法求解到达第<code>n</code>个收费站的最低花费。</p><h3 id="2-14">2 14%</h3><p>某公司有若干个独立项目需要完成。每个项目完成后可以获得一定奖金，但项目必须在其截止时间之前或当天完成，才能获得对应奖金。已知每个项目都需要连续工作 <strong>1 天</strong> 才能完成，并且每天最多只能完成 <strong>1 个项目</strong>。如果某个项目未能在其截止时间前完成，则不能获得该项目奖金。</p><p>现有 7 个项目，其截止时间和奖金如下表所示：</p><table><thead><tr><th>项目</th><th>截止时间 (d_i)</th><th>奖金 (p_i)</th></tr></thead><tbody><tr><td>A</td><td>1</td><td>35</td></tr><tr><td>B</td><td>2</td><td>30</td></tr><tr><td>C</td><td>2</td><td>25</td></tr><tr><td>D</td><td>1</td><td>20</td></tr><tr><td>E</td><td>3</td><td>45</td></tr><tr><td>F</td><td>3</td><td>15</td></tr><tr><td>G</td><td>2</td><td>40</td></tr></tbody></table><p>请完成以下问题：</p><p><strong>（1）建立数学模型。</strong>将该问题抽象为一个优化问题，定义必要的变量，并说明约束条件是什么、优化目标是什么。</p><p><strong>（2）设计高效算法。</strong>请设计一个高效算法来求解该问题，使得在满足截止时间限制的前提下，获得的总奖金最大。要求说明算法的基本思想和具体步骤。</p><p><strong>（3）用所设计算法求解上述实例。</strong>请按照第（2）问中的算法，对表中 7 个项目进行调度，写出每一步的选择过程，最终给出应完成的项目顺序以及可获得的最大奖金。</p><p><strong>（4）分析算法复杂度。</strong>请分析所设计算法的时间复杂度。</p><h3 id="3-10">3 10%</h3><p><strong>题目：正整数序列的逆序数问题</strong></p><p>给定一个长度为 $n$ 的正整数序列：$$A = (a_1, a_2, \dots, a_n)$$若存在一对下标 $(i, j)$，满足：</p><p>$1 \leq i &lt; j \leq n$ 且 $a_i &gt; a_j$</p><p>则称 $(a_i, a_j)$ 是序列中的一个<strong>逆序对</strong>。序列中所有逆序对的总数称为该序列的<strong>逆序数</strong>。</p><p>例如，对于序列：</p><p>$A = (7, 3, 5, 2, 6, 1)$</p><p>其中存在若干逆序对，如 $(7,3)、(7,5)、(3,2)、(6,1)$ 等。要求计算该序列的逆序数。</p><p>请完成以下问题：</p><p><strong>（1）蛮力法及其复杂度分析。</strong>说明如何用蛮力法求给定正整数序列的逆序数，并分析该方法的时间复杂度。</p><p><strong>（2）设计高效算法。</strong>请设计一个比蛮力法更高效的算法来求解该问题。要求说明算法的基本思想和具体步骤。</p><p><strong>（3）递推式与复杂度分析。</strong>根据第（2）问所设计的算法，写出其时间复杂度的递推式，并求解该递推式，得到算法的渐进时间复杂度。</p><h3 id="4-10">4 10%</h3><p>某企业计划从 6 个备选研发项目中选择若干个进行投资。每个项目一旦选择，就必须完整投入所需预算，不能只投入其中一部分；每个项目最多只能选择一次。企业本年度可用于研发项目的总预算为 20 万元。</p><p>各项目所需预算和预计产出价值如下表：</p><table><thead><tr><th>项目</th><th>所需预算</th><th>预计产出价值</th></tr></thead><tbody><tr><td>A</td><td>7</td><td>49</td></tr><tr><td>B</td><td>4</td><td>40</td></tr><tr><td>C</td><td>8</td><td>40</td></tr><tr><td>D</td><td>5</td><td>45</td></tr><tr><td>E</td><td>3</td><td>18</td></tr><tr><td>F</td><td>6</td><td>48</td></tr></tbody></table><p>要求在总预算不超过 20 的前提下，选择若干项目，使得总预计产出价值最大。</p><p>（1）使用动态规划算法求解该问题的最大产出价值。要求给出状态表示、状态转移方程和边界条件</p><p>（2）使用优先队列式分支限界法求解该问题。要求自行设计合适的结点优先级函数或限界函数，并说明该函数为什么可以作为搜索时的上界。每个结点应至少包含：当前考虑到的项目编号、当前已用预算、当前已获得产出价值、当前价值上界以及已选择项目的情况。</p><p>在搜索过程中，用最大优先队列保存活结点，每次选择优先级最高的结点作为下一个扩展结点。请写出主要搜索过程，说明哪些结点可以被剪枝，并最终给出最优项目集合和最大产出价值。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>计算机系统结构（420563）25-26第二学期期末考试</title>
      <link>https://blog.wendain.ing/2026/06/22/ca-sp26-final/</link>
      <description>考完一个小时内的回忆，但是很多（尤其是判断题）也想不起来了，希望能有用吧。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/">课程笔记</category>
      <category domain="https://blog.wendain.ing/categories/%E8%AF%BE%E7%A8%8B%E7%AC%94%E8%AE%B0/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%BB%93%E6%9E%84/">计算机系统结构</category>
      <category domain="https://blog.wendain.ing/tags/%E5%BE%80%E5%B9%B4%E9%A2%98/">往年题</category>
      <category domain="https://blog.wendain.ing/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F%E7%BB%93%E6%9E%84/">计算机系统结构</category>
      <pubDate>Mon, 22 Jun 2026 13:44:17 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><p>update: 本人喜提一个良😃</p></blockquote><h2 id="名词解释-5-3">名词解释 (5*3%)</h2><p>什么是指令的静态调度</p><p>流水线的效率</p><p>Cache 的冲突不命中</p><p>动态流水线</p><p>异步 I/O</p><h2 id="判断题-15-1">判断题 (15*1%)</h2><blockquote><p>这下面很多判错的就是题干里一些地方写错了，但是我不记得写错的地方是什么了，所以很多是直接给出正确的命题。</p></blockquote><p>计算机系统的多级层次结构： L1微程序机器级 L2机器语言（传统机器级） L3操作系统虚拟机 L4汇编语言虚拟机 L5高级语言虚拟机 L6应用语言虚拟机（题目少了其中一两个问你对不对）</p><p>Amdahl 定律指出：：当对一个系统中的某个部件进行改进后，所能获得的整个系统性能的提高，受限于 该部件的执行时间占总执行时间的百分比</p><p>计算机系统结构：由程序员看到的计算机属性，即<strong>概念性结构与功能特性</strong>（）</p><p>什么是广义系统结构（设计的三个方面）：指令集结构、组成、硬件（）</p><p>CISC 比 RISC 好（）</p><p>解决流水线瓶颈问题：细分瓶颈段、重复设置瓶颈段（题目这两个有一个写错了，答案应该是 F，但是写错的是什么我忘记了）</p><h2 id="填空-一空一分-15-1">填空 (一空一分 15*1%)</h2><p>三种提升并行度的方法（）（）（）</p><p>以程序执行的视角划分5种并行级别，从低到高（）（）（）（）（）</p><p>根据是否存在反馈回路，流水线可以分为（）和（）</p><p>相关分为（）（）和名相关</p><p>IO设备是协调外设和（）</p><h2 id="大题-55">大题 (55%)</h2><h3 id="1-10">1 (10%)</h3><p>什么是多处理机的 Cache 一致性？解释目录协议与监听协议？</p><h3 id="2-10">2 (10%)</h3><p>一个系统，可改进比例 50%，部件加速比 10，求系统的加速比。</p><h3 id="3-10">3 (10%)</h3><p>二级Cache平均访存公式，设计二级Cache需要权衡的两个因素</p><h3 id="4-13">4 (13%)</h3><p>某浮点处理器采用 <strong>Tomasulo 动态调度算法</strong> 执行如下 6 条浮点指令。指令序列如下：</p><div class="code-wrapper"><pre><code class="hljs asm">I1: L.D    F6, 34(R2)I2: L.D    F2, 45(R3)I3: MUL.D  F0, F2, F4I4: SUB.D  F8, F6, F2I5: DIV.D  F10, F0, F6I6: ADD.D  F6, F8, F2</code></pre></div><p>处理器中设置有如下功能部件与保留站：</p><table><thead><tr><th>功能部件类型</th><th>保留站 / 缓冲站数量</th></tr></thead><tbody><tr><td>Load 缓冲站</td><td>2 个，记为 Load1、Load2</td></tr><tr><td>加减法保留站</td><td>3 个，记为 Add1、Add2、Add3</td></tr><tr><td>乘除法保留站</td><td>2 个，记为 Mult1、Mult2</td></tr></tbody></table><p>采用的状态表包括：</p><ol><li><strong>指令状态表</strong>：记录每条指令的流出、执行、写结果状态；</li><li><strong>保留站状态表</strong>：记录各保留站的 Busy、Op、Vj、Vk、Qj、Qk、A 等字段；</li><li><strong>寄存器状态表</strong>：记录各浮点寄存器的 Qi 字段。</li></ol><hr><h3 id="（1）">（1）</h3><p>假设上述指令按顺序流出，采用 Tomasulo 算法进行动态调度。</p><p>当 <strong>第 1 条指令 <code>L.D F6, 34(R2)</code> 刚完成写结果</strong> 时，分别给出此时的：</p><ul><li>指令状态表；</li><li>保留站状态表；</li><li>寄存器状态表。</li></ul><hr><h3 id="（2）">（2）</h3><p>若各类功能部件的执行时间如下：</p><table><thead><tr><th>指令类型</th><th>执行所需时钟周期</th></tr></thead><tbody><tr><td>Load</td><td>1 个时钟周期</td></tr><tr><td>ADD / SUB</td><td>2 个时钟周期</td></tr><tr><td>MUL</td><td>10 个时钟周期</td></tr><tr><td>DIV</td><td>40 个时钟周期</td></tr></tbody></table><p>在上述条件下，继续采用 Tomasulo 算法执行该指令序列。</p><p>要求画出或填写完整的：</p><ul><li>指令状态表；</li><li>保留站状态表；</li><li>寄存器状态表。</li></ul><h3 id="5-12">5 (12%)</h3><p>有一动态多功能流水线由 6 个功能段组成，如图所示。</p><pre><code class=" mermaid">flowchart LR    I1(( )) --&gt; S1[S1]    I2(( )) --&gt; S1    S1 --&gt; S2[S2]    S2 --&gt; S3[S3]    S1 --&gt;|乘法| S4[S4]    S4 --&gt; S5[S5]    S5 --&gt; S6[S6]    S3 --&gt;|加法| S6    S6 --&gt; O1(( ))    S6 --&gt; O2(( ))    classDef stage fill:#f8f3ef,stroke:#8b4a4a,stroke-width:2px,color:#111;    class S1,S2,S3,S4,S5,S6 stage;</code></pre><p>其中，S1、S4、S5、S6 组成乘法流水线，S1、S2、S3、S6 组成加法流水线，各个功能段时间均为 50 ns。假设该流水线的输出结果可以直接返回输入端，而且设置有足够的缓冲寄存器，并以最快的方式用该流水线计算：$$\sum_{i=1}^{5} x_i y_i z_i$$</p><h4 id="1">(1)</h4><p>画出时空图</p><h4 id="2">(2)</h4><p>计算其效率、加速比、吞吐率</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Redis Note</title>
      <link>https://blog.wendain.ing/2026/06/17/redis-note/</link>
      <description>
        <![CDATA[<h2 id="基础篇">基础篇</h2>
<p>没什么好记的，随便记点。</p>]]>
      </description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/">技术笔记</category>
      <category domain="https://blog.wendain.ing/tags/%E5%85%A8%E6%A0%88/">全栈</category>
      <category domain="https://blog.wendain.ing/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/">中间件</category>
      <category domain="https://blog.wendain.ing/tags/Redis/">Redis</category>
      <category domain="https://blog.wendain.ing/tags/NoSQL/">NoSQL</category>
      <pubDate>Wed, 17 Jun 2026 17:10:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="基础篇">基础篇</h2><p>没什么好记的，随便记点。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>速成技术岗实习的一些方法论阅读和汇总</title>
      <link>https://blog.wendain.ing/2026/06/15/accelerated-path-to-tech-internship/</link>
      <description>阅读到的一些速成实习的方法论和经验分享的汇总，可能会融入一些自己的思考。</description>
      <author>wendaining</author>
      <category domain="https://blog.wendain.ing/categories/%E6%9D%82%E8%B0%88%E4%B8%8E%E9%9A%8F%E6%83%B3/">杂谈与随想</category>
      <category domain="https://blog.wendain.ing/tags/%E6%9D%82%E8%B0%88/">杂谈</category>
      <category domain="https://blog.wendain.ing/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/">方法论</category>
      <pubDate>Mon, 15 Jun 2026 19:20:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="前言">前言</h2><p><strong>如何正视自己被浪费的时间？</strong>其实这个问题还挺难回答的。</p><p>总之开始行动可能就不会有这么多焦虑了。</p><p>下面是阅读到的一些速成实习的方法论和经验分享的汇总，可能会融入一些自己的思考。</p><h2 id="Nix-的两篇飞书文章">Nix 的两篇飞书文章</h2><p><a href="https://my.feishu.cn/wiki/RI2CwouC5i6FhJkbvRzcxwscnjh">‍‍﻿‌﻿‬﻿﻿⁠﻿⁠‬﻿⁠‬‌﻿﻿⁠﻿﻿‬‬基于混子导向的速成实习路线 - 飞书云文档</a></p><p><a href="https://my.feishu.cn/wiki/LQv1w1jH3iw5Wvkq8i7chrthnPS">‌﻿﻿‍﻿⁠‌⁠‍‬‬‬‌﻿⁠⁠‍﻿‬⁠‍‌‍‬‍﻿‍﻿‬⁠﻿‌⁠基于混子导向的速成实习实践攻略（Java） - 飞书云文档</a></p><p>总结一下：</p><ul><li>海投，越早越好，别怕面试，面试 = 普通的随堂小测</li><li>项目要以「解决了什么问题」为导向去写，吸引面试官</li><li>保证自己对项目的熟悉，或者是对面试的掌控度，也就是别贸然写自己不熟悉的东西</li></ul>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
