<?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>CodeTalks</title>
    <link>https://blog.howardlau.me/</link>
    <description>记录想法</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, howardlau</copyright>
    <lastBuildDate>Sun, 16 Mar 2025 14:39:31 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <image>
      <url>https://blog.howardlau.me/img/favicon.jpg</url>
      <title>CodeTalks</title>
      <link>https://blog.howardlau.me/</link>
    </image>
    <atom:link href="https://blog.howardlau.me/feed/index.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>DeepSeek 3FS 源码解读——客户端篇</title>
      <link>https://blog.howardlau.me/programming/deepseek-3fs-code-reading-client.html</link>
      <description>
        <![CDATA[<p>文件系统客户端作为应用或者用户访问文件系统的入口，其实现方式决定了用户最终实际能体验到的性能上限，客户端面对的情况更为复杂，代码量也不容小觑。为了充分发挥后端性能，3FS 的客户端部分经过精心设计和实现。</p>
<p>在了解 3FS]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sun, 16 Mar 2025 01:26:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>文件系统客户端作为应用或者用户访问文件系统的入口，其实现方式决定了用户最终实际能体验到的性能上限，客户端面对的情况更为复杂，代码量也不容小觑。为了充分发挥后端性能，3FS 的客户端部分经过精心设计和实现。</p><p>在了解 3FS 实现前，先介绍下并行文件系统的客户端实现方式。客户端根据实现方式和访问协议的不同，大致可以分为以下几类：</p><ul><li><strong>内核模块</strong>：将文件系统的访问逻辑实现为 POSIX 标准内核模块，用户只需要在使用时通过 <code>modprobe</code> 命令将客户端内核模块加载到内核中，然后通过 <code>mount</code> 指令指定选项或者配置文件挂载到 VFS 系统中，即可像使用本地文件系统一样访问远端的网络并行文件系统，常用的 <code>ls</code> 和 <code>vim</code> 等工具也可以直接访问文件系统。例如 Ceph、BeeGFS、Lustre 等文件系统使用了这种方式。如果文件系统支持 NFS 或者 SMB 协议，也可以归类为这种。</li><li><strong>SDK</strong>：文件系统的访问逻辑以单独的 SDK 提供，用户程序需要完全修改源代码并编译链接 SDK，显式使用 SDK 的 API 来访问文件系统，其他常用的程序例如 <code>ls</code> 无法直接访问，需要通过专用命令行工具或客户端。例如 HDFS、盘古等文件系统使用了这种方式。</li><li><strong>动态库&#x2F;系统调用拦截</strong>：通过 <code>LD_PRELOAD</code> 或者修改加载器的方式在应用启动加载客户端动态库，抢占 <code>open</code>、<code>read</code> 等函数符号（原始的符号也会保存到另外的变量中），在应用调用 <code>read</code> 等函数时，会调用到文件系统的动态库实现的函数中，从而实现访问远端网络文件系统，适合无法修改源代码的应用程序使用。例如 Intel DAOS 或者一些学术研究文件系统使用了这种方式。 </li><li><strong>FUSE</strong>：将文件系统的访问逻辑实现到用户态守护进程，并通过内核 <code>fuse</code> 模块将文件系统以及 <code>mount</code> 指令挂载到内核 VFS 中，和内核模块文件系统客户端有同样的使用效果。例如 OSSFS、BeeGFS FUSE、Ceph FUSE、SeaweedFS 等文件系统使用了这种方式。</li></ul><p><a href="/programming/deepseek-3fs-code-reading-client/Pasted-image-20250315154405.png" data-fancybox="gallery" data-caption=""><img src="/programming/deepseek-3fs-code-reading-client/Pasted-image-20250315154405.png"></a></p><p>以上几种客户端实现方式从使用兼容性、开发难度、访问性能等各有优劣：</p><table><thead><tr><th>实现方式</th><th>兼容性</th><th>开发难度</th><th>性能</th></tr></thead><tbody><tr><td>内核模块</td><td>好，兼容 POSIX</td><td>高，内核态代码调试维护困难</td><td>中，需要经过 VFS 和一次内核态用户态内存拷贝</td></tr><tr><td>SDK</td><td>差，需要修改应用源码</td><td>低，提供文件系统原生 API 即可，开发复杂度交给用户了</td><td>高，和用户程序在同一地址空间，可以实现 0 拷贝</td></tr><tr><td>动态库&#x2F;系统调用拦截</td><td>中，不需要修改应用源码，但只适合动态加载 C 库的程序</td><td>中，需要穷举可能被调用的 C 接口，编译产物为动态库，加载时机难确定；不走挂载点，需要库实现区分目录挂载在什么文件系统下的逻辑</td><td>高，和用户程序在同一地址空间，可以实现 0 拷贝</td></tr><tr><td>FUSE</td><td>好，兼容 POSIX</td><td>低，和实现一个服务端程序差不多</td><td>低，需要经过 VFS 和两次内核态用户态内核拷贝</td></tr></tbody></table><p>3FS 混合使用了 FUSE 和 SDK 模式：为性能不敏感的应用程序提供了 FUSE 方式访问，对有能力修改源码的应用程序提供了 USRBIO（UserSpace Ring Based IO）的类 SDK 接入访问实现 User Space Kernel Bypass IO，但元数据操作仍然走 FUSE 接口。</p><h2 id="FUSE-实现"><a href="#FUSE-实现" class="headerlink" title="FUSE 实现"></a>FUSE 实现</h2><p>3FS FUSE 部分大量复用了 RPC 篇中提到的客户端实现，以使用协程操作完成 FUSE 守护进程和 3FS 服务端的通信，如果还不了解 3FS 的 RPC 实现原理，可以先阅读：<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-rpc.html">DeepSeek 3FS 源码解读——RPC 篇</a></p><p>而 FUSE 客户端部分只需要照着 FUSE 规范实现一个符合 POSIX 接口的用户态守护进程即可。简单来说守护进程只需要链接 libfuse，设置好初始化参数，通过 <code>fuse_session_mount</code> 将文件系统挂载到指定挂载点中，然后进入 <code>fuse_session_loop</code>（多线程是 <code>fuse_session_loop_mt</code>） FUSE 库便会负责接收来自内核的 IO 请求，然后调用初始化时指定的函数回调指针调用 3FS 的 FUSE 实现。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> fuse_lowlevel_ops hf3fs_oper = &#123;</span><br><span class="line">    <span class="comment">// 节选部分 POSIX 接口...</span></span><br><span class="line">    .mknod = hf3fs_mknod,</span><br><span class="line">    .mkdir = hf3fs_mkdir,</span><br><span class="line">    .unlink = hf3fs_unlink,</span><br><span class="line">    .rmdir = hf3fs_rmdir,</span><br><span class="line">    .symlink = hf3fs_symlink,</span><br><span class="line">    .rename = hf3fs_rename,</span><br><span class="line">    .link = hf3fs_link,</span><br><span class="line">    .open = hf3fs_open,</span><br><span class="line">    .read = hf3fs_read,</span><br><span class="line">    .write = hf3fs_write,</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 启动守护进程主循环</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">fuseMainLoop</span><span class="params">(<span class="type">const</span> String &amp;programName,</span></span></span><br><span class="line"><span class="params"><span class="function">                 <span class="type">bool</span> allowOther,</span></span></span><br><span class="line"><span class="params"><span class="function">                 <span class="type">const</span> String &amp;mountpoint,</span></span></span><br><span class="line"><span class="params"><span class="function">                 <span class="type">size_t</span> maxbufsize,</span></span></span><br><span class="line"><span class="params"><span class="function">                 <span class="type">const</span> String &amp;clusterId)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> &amp;d = <span class="built_in">getFuseClientsInstance</span>();</span><br><span class="line">  <span class="comment">// 就是 hf3fs_oper</span></span><br><span class="line">  <span class="type">const</span> <span class="keyword">auto</span> &amp;ops = <span class="built_in">getFuseOps</span>();</span><br><span class="line"></span><br><span class="line">  std::stack&lt;std::function&lt;<span class="type">void</span>()&gt;&gt; onStopHooks;</span><br><span class="line">  SCOPE_EXIT &#123;</span><br><span class="line">    <span class="keyword">while</span> (!onStopHooks.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">      onStopHooks.<span class="built_in">top</span>()();</span><br><span class="line">      onStopHooks.<span class="built_in">pop</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 解析命令行参数</span></span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">fuse_args</span> args = <span class="built_in">FUSE_ARGS_INIT</span>((<span class="type">int</span>)fuseArgsPtr.<span class="built_in">size</span>(), fuseArgsPtr.<span class="built_in">data</span>());</span><br><span class="line">  <span class="comment">// struct fuse_args args = FUSE_ARGS_INIT(argc, argv);</span></span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">fuse_cmdline_opts</span> opts;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">fuse_loop_config</span> *config = <span class="built_in">fuse_loop_cfg_create</span>();</span><br><span class="line">  SCOPE_EXIT &#123; <span class="built_in">fuse_loop_cfg_destroy</span>(config); &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">fuse_parse_cmdline</span>(&amp;args, &amp;opts) != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 初始化 FUSE 会话，设置 POSIX 操作对应的实现</span></span><br><span class="line">  d.se = <span class="built_in">fuse_session_new</span>(&amp;args, &amp;ops, <span class="built_in">sizeof</span>(ops), <span class="literal">NULL</span>);</span><br><span class="line">  <span class="keyword">if</span> (d.se == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  onStopHooks.<span class="built_in">push</span>([&amp;] &#123; <span class="built_in">fuse_session_destroy</span>(d.se); &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">fuse_set_signal_handlers</span>(d.se) != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  onStopHooks.<span class="built_in">push</span>([&amp;] &#123; <span class="built_in">fuse_remove_signal_handlers</span>(d.se); &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">fuse_session_mount</span>(d.se, opts.mountpoint) != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 进程结束回调，unmount 文件系统</span></span><br><span class="line">  onStopHooks.<span class="built_in">push</span>([&amp;] &#123; <span class="built_in">fuse_session_unmount</span>(d.se); &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="type">int</span> ret = <span class="number">-1</span>;</span><br><span class="line">  <span class="comment">// 主循环</span></span><br><span class="line">  <span class="keyword">if</span> (opts.singlethread) &#123;</span><br><span class="line">    ret = <span class="built_in">fuse_session_loop</span>(d.se);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="built_in">fuse_loop_cfg_set_clone_fd</span>(config, opts.clone_fd);</span><br><span class="line">    <span class="built_in">fuse_loop_cfg_set_idle_threads</span>(config, d.maxIdleThreads);</span><br><span class="line">    <span class="built_in">fuse_loop_cfg_set_max_threads</span>(config, d.maxThreads);</span><br><span class="line">    ret = <span class="built_in">fuse_session_loop_mt</span>(d.se, config);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> ret ? <span class="number">1</span> : <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>值得一提的是 3FS 的 FUSE 守护进程同样使用了 C++ 20 协程来完成网络通信。然而 FUSE 本身是 C 库，也不支持异步接口，因此这里存在一个协程到同步操作的转换：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Awaitable&gt;</span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">withRequestInfo</span><span class="params">(<span class="type">fuse_req_t</span> req, Awaitable &amp;&amp;awaitable)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> guard = RequestInfo::<span class="built_in">set</span>(req);</span><br><span class="line">  <span class="keyword">return</span> folly::coro::<span class="built_in">blockingWait</span>(std::forward&lt;Awaitable&gt;(awaitable));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>FUSE 的请求在经过一系列检查和翻译成 3FS 请求后，会调用对应的协程函数，并使用 <code>folly::coro::blockingWait</code> 阻塞等待完成，例如打开文件的操作：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">hf3fs_open</span><span class="params">(<span class="type">fuse_req_t</span> req, <span class="type">fuse_ino_t</span> fino, <span class="keyword">struct</span> fuse_file_info *fi)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> userInfo = <span class="built_in">UserInfo</span>(flat::<span class="built_in">Uid</span>(<span class="built_in">fuse_req_ctx</span>(req)-&gt;uid), flat::<span class="built_in">Gid</span>(<span class="built_in">fuse_req_ctx</span>(req)-&gt;gid), d.fuseToken);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 判断是不是在只读挂载上打开可写文件了</span></span><br><span class="line">  <span class="keyword">if</span> ((fi-&gt;flags &amp; O_WRONLY) || (fi-&gt;flags &amp; O_RDWR) || (fi-&gt;flags &amp; O_CREAT) || (fi-&gt;flags &amp; O_EXCL) ||</span><br><span class="line">      (fi-&gt;flags &amp; O_TRUNC) || (fi-&gt;flags &amp; O_APPEND)) &#123;</span><br><span class="line">    <span class="keyword">if</span> (d.userConfig.<span class="built_in">getConfig</span>(userInfo).<span class="built_in">readonly</span>()) &#123;</span><br><span class="line">      <span class="built_in">fuse_reply_err</span>(req, EROFS);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> ino = <span class="built_in">real_ino</span>(fino);</span><br><span class="line">  <span class="keyword">auto</span> ptr = <span class="built_in">inodeOf</span>(*fi, ino);</span><br><span class="line"></span><br><span class="line">  Uuid session;</span><br><span class="line">  <span class="keyword">if</span> ((fi-&gt;flags &amp; O_ACCMODE) == O_WRONLY || (fi-&gt;flags &amp; O_ACCMODE) == O_RDWR) &#123;</span><br><span class="line">    session = meta::client::SessionId::<span class="built_in">random</span>();</span><br><span class="line">    <span class="comment">// 阻塞同步调用 MetaClient 的 RPC 协程函数</span></span><br><span class="line">    <span class="comment">// 这里的 ino 是 FUSE 联合 VFS 调用 Lookup 函数查到的</span></span><br><span class="line">    <span class="keyword">auto</span> res = <span class="built_in">withRequestInfo</span>(req, d.metaClient-&gt;<span class="built_in">open</span>(userInfo, ino, std::<span class="literal">nullopt</span>, session, fi-&gt;flags));</span><br><span class="line">    <span class="keyword">if</span> (res.<span class="built_in">hasError</span>()) &#123;</span><br><span class="line">      <span class="built_in">handle_error</span>(req, res);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// O_DIRECT open means direc io</span></span><br><span class="line">  <span class="comment">// read cached disabled &amp;&amp; not O_NONBLOCK open (for mmap) means direct io too</span></span><br><span class="line">  fi-&gt;direct_io =</span><br><span class="line">      (fi-&gt;flags &amp; O_DIRECT) || (!d.userConfig.<span class="built_in">getConfig</span>(userInfo).<span class="built_in">enable_read_cache</span>() &amp;&amp; !(fi-&gt;flags &amp; O_NONBLOCK))</span><br><span class="line">          ? <span class="number">1</span></span><br><span class="line">          : <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  fi-&gt;fh = (<span class="type">uintptr_t</span>)(<span class="keyword">new</span> FileHandle&#123;ptr, (<span class="type">bool</span>)(fi-&gt;flags &amp; O_DIRECT), session&#125;);</span><br><span class="line">  <span class="built_in">fuse_reply_open</span>(req, fi);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里跟读代码的话会发现其调用了 <code>MetaClient</code> 或者 <code>StorageMessenger</code>、<code>StorageClient</code> 来和 3FS 服务器通信，不过一层套一层最后还是调用了 <code>serde</code> 类完成 RPC 操作。还不了解 RPC 实现的读者可以参考阅读 <a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-rpc.html">DeepSeek 3FS 源码解读——RPC 篇</a>，只需要关注带 <code>Serde</code> 字样的类即可。客户端 RPC 和服务端 RPC 类似都是通过线程池完成的。简单梳理一下这里同步和异步的转换关系：</p><ol><li>客户端调用 POSIX 系统调用陷入内核态 VFS，此处用户线程阻塞</li><li>VFS 塞 IO 请求到 FUSE 队列中，唤醒阻塞在 fuse 循环中的守护进程的线程</li><li><code>libfuse</code> 从队列中取 IO 请求，执行 <code>ops</code> 对应的函数指针</li><li>函数指针执行 RPC 请求协程发起 meta 或 storage RPC call，此时 RPC 被调度到网络 IO 线程池，fuse 循环线程因 <code>blockingWait</code> 同步阻塞在 <code>co_await baton</code></li><li>收到响应后，fuse 循环线程被唤醒，处理 RPC 响应，并通过 <code>fuse_reply_*</code> 系列函数通知内核 FUSE 模块对应的 IO 请求已完成，写回结果</li><li>内核 FUSE 模块找到 IO 请求对应的用户线程，唤醒用户线程，用户线程恢复执行，处理 IO 结果</li></ol><p>RDMA 的 Buffer 由 FUSE Daemon 通过内存池分配。其他的具体的实现方法接口和逻辑细节都比较多，这里不深入解析了，对实现感兴趣的读者可以自行顺着 <code>FuseOps.cc</code> 中的若干 POSIX 接口开始跟读源码。另由于接入了 VFS 层，其实 FUSE 可以复用例如 Page Cache 等内核文件系统缓存机制或者 aio 等机制，当然也可以禁用。这里也比较复杂，也不再展开了。</p><p>另，一些复杂的元数据操作，例如 <code>rm -rf</code>，通过单独的一个 <code>symlink</code> 文件实现，具体分析可参考：<a href="https://mp.weixin.qq.com/s/X60PsEPeFsb-ZPKATMrWrA">https://mp.weixin.qq.com/s/X60PsEPeFsb-ZPKATMrWrA</a>（文中关于 Ior 的机制不太准确，请见下文）</p><h2 id="USRBIO-机制"><a href="#USRBIO-机制" class="headerlink" title="USRBIO 机制"></a>USRBIO 机制</h2><p>从上面的分析可以看到 FUSE 操作基本都会同步阻塞线程，且单次 IO 每方向涉及到 2 次内核用户态切换以及 1~2 次内核态和用户态的内存拷贝，性能一般。为了提升读写性能和延迟，3FS 提供了 USRBIO API，可以理解为 3FS SDK，对读写性能有要求，希望实现 Zero Copy 的应用程序可以通过接入 SDK 来提升读写性能。</p><p>通过环形 SQ CQ 队列 + Pinned Memory DMA 实现 Zero Context Switch + Zero Copy IO 是目前高性能 IO 软件常用的技术方案，例如 RDMA、DPDK、io_uring、SPDK 都采用了这种设计。USRBIO 就是在这种思想下设计出来的技术方案。</p><p>抽象地来说，Ring Based IO 涉及以下要素：</p><ol><li>工作线程和应用线程，工作线程负责处理从应用线程提交的 IO 请求，应用线程负责处理 IO 完成事件。</li><li>Submission Queue（SQ，提交队列）和 Completion Queue（CQ，完成队列），一对有界环形队列，队列中的元素分别为 SQE 和 CQE，分别用于描述 IO 请求和 IO 完成事件。工作线程轮询 SQ 获取 SQE，执行 IO 请求，读写 SQE 中指定的 Buffer，将完成状态 CQE 写入 CQ；用户线程将 IO 请求写入 SQ，并轮询 CQ 获取 CQE。 </li><li>Pinned Memory Buffers，工作线程和应用线程都可读写的共享内存。用于存放 IO 数据，在 IO 操作完成前不可读写、不可释放。IO 请求中包含的读写区域必须是这里面的内存区域。</li><li>可选：Submission Bell 和 Completion Bell，前者用于通知 IO 工作线程有新的 IO 任务，后者用于通知应用线程有新的 IO 完成事件。采用这种机制可以实现类似事件通知的机制，避免轮询消耗 CPU。</li></ol><p>具体到 3FS，它提出了 Ior 和 Iov 的抽象，Ior 就是一个 IO Ring，只包含一些管理用的元数据属性；而 Iov 是一块共享内存的抽象，既提供了 Ior 的 SQ、CQ 共享内存管理，也提供了 Zero Copy IO Buffer 管理，总体的实现框架和关系是：</p><p><a href="/programming/deepseek-3fs-code-reading-client/Pasted-image-20250316210459.png" data-fancybox="gallery" data-caption=""><img src="/programming/deepseek-3fs-code-reading-client/Pasted-image-20250316210459.png"></a></p><ol><li>用户程序可以创建多个不同优先级的 Ior，<strong>每个 Ior 要么只负责读操作，要么只负责写操作</strong>；每一个 Ior 的实际队列存放在一段 Ior-Iov 共享内存，也就是一组 SQ、CQ；以及多个 Non-Ior Iov 文件用作 IO Buffer（实际上也是 IPC 共享内存）<ul><li>Non-Ior Iov 文件用来作为读写数据的 pinned memory buffer，通过 UUID 索引<ul><li>用户提前规划好需要使用的总容量</li><li>文件创建之后 FUSE Daemon 将对应内存注册成 RDMA memory buffer，进而实现整个链路的零拷贝</li><li>Iov 带有 Size、NUMA Node ID 等属性</li></ul></li><li>Ior Iov 文件用来实现 IoRing<ul><li>用户提前规划好队列上界</li><li>在整个内存区域上抽象出了提交队列和完成队列，具体布局参考上图，代码定义参考 <code>src/fuse/IoRing.h</code> 中的 <code>IoRing</code> 类</li><li>SQE 间接引用了 RingSection 中的项描述 IO 的目的 Buffer，Userdata 即 <code>epoll</code> 中的 <code>ev.data.ptr</code> 或者 <code>liburing</code> 中的 <code>io_uring_sqe_set_data</code>，用来找回调&#x2F;协程抓手的</li><li>内存的尾部是提交完成队列的 IPC 信号量，FUSE Daemon 在处理完 IO 后通过这个信号量通知到用户进程，唤醒 <code>hf3fs_wait_for_ios</code> 线程</li></ul></li></ul></li><li>一个挂载点的所有 Ior 共享 3 个 submit IPC sem<ul><li>这三个 sem 作为 IO 提交事件的信号量（submit sem），每一个 sem 代表一个优先级，数字越小优先级越高</li><li>一旦某个 USRBIO 实例有 IO 需要提交，会通过这些信号量通知到 FUSE Daemon</li><li>USRBIO 处理完成 CQE 之后也通过这些信号量通知 FUSE Daemon CQ 有空余了</li></ul></li><li>所有的共享内存文件在挂载点 <code>3fs-virt/iovs/</code> 目录下均建有 symlink，指向 <code>/dev/shm</code> 下的对应文件</li></ol><p>篇幅限制，Ior 和 Iov 的具体管理逻辑这里先不贴代码分析了，感兴趣的读者可以参考阅读蚂蚁存储团队对 Ior 和 Iov 管理代码的阅读分析：<a href="https://mp.weixin.qq.com/s/sPkqOdVA3qBAUiMQltveoQ">https://mp.weixin.qq.com/s/sPkqOdVA3qBAUiMQltveoQ</a></p><h3 id="用户侧-USRBIO-SDK-分析"><a href="#用户侧-USRBIO-SDK-分析" class="headerlink" title="用户侧 USRBIO SDK 分析"></a>用户侧 USRBIO SDK 分析</h3><p>用户侧的 USRBIO SDK API 实现位于 <code>src/lib</code>。先看看官方使用示例：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;hf3fs_usrbio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">constexpr</span> <span class="type">uint64_t</span> NUM_IOS = <span class="number">1024</span>;</span><br><span class="line"><span class="type">constexpr</span> <span class="type">uint64_t</span> BLOCK_SIZE = (<span class="number">32</span> &lt;&lt; <span class="number">20</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">hf3fs_ior</span> <span class="title">ior</span>;</span></span><br><span class="line">    <span class="comment">// 对应 io_uring 的 io_uring_queue_init_params</span></span><br><span class="line">    <span class="comment">// 注意一个 Ior 要么只读要么只写，可以创建多个 Ior</span></span><br><span class="line">    hf3fs_iorcreate4(&amp;ior, <span class="string">&quot;/hf3fs/mount/point&quot;</span>, NUM_IOS, <span class="literal">true</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">-1</span>, <span class="number">0</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">hf3fs_iov</span> <span class="title">iov</span>;</span></span><br><span class="line">    <span class="comment">// 对应 io_uring 的 io_uring_register_buffers</span></span><br><span class="line">    <span class="comment">// 可以创建多个 Iov</span></span><br><span class="line">    hf3fs_iovcreate(&amp;iov, <span class="string">&quot;/hf3fs/mount/point&quot;</span>, NUM_IOS * BLOCK_SIZE, <span class="number">0</span>, <span class="number">-1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> fd = open(<span class="string">&quot;/hf3fs/mount/point/example.bin&quot;</span>, O_RDONLY);</span><br><span class="line">    <span class="comment">// 对应 io_uring 的 io_uring_register_files</span></span><br><span class="line">    hf3fs_reg_fd(fd, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; NUM_IOS; i++) &#123;</span><br><span class="line">        <span class="comment">// 对应 io_uring 的 io_uring_prep_&#123;read,write&#125;</span></span><br><span class="line">        <span class="comment">// 最后一个参数 nullptr 实际上就是用户自定义的上下文指针，会在 CQE 原封不动传回来</span></span><br><span class="line">        hf3fs_prep_io(&amp;ior, &amp;iov, <span class="literal">true</span>, iov.base + i * BLOCK_SIZE, fd, i * BLOCK_SIZE, BLOCK_SIZE, nullptr);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 下面三行对应 io_uring 的 io_uring_submit_and_wait</span></span><br><span class="line">    hf3fs_submit_ios(&amp;ior);</span><br><span class="line"></span><br><span class="line">    hf3fs_cqe cqes[NUM_IOS];</span><br><span class="line">    hf3fs_wait_for_ios(&amp;ior, cqes, NUM_IOS, NUM_IOS, nullptr);</span><br><span class="line">    <span class="comment">// 这里可以根据每个 CQE 中的 userdata 取回上下文，执行一些用户自定义的逻辑</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对应 io_uring 一系列释放资源操作</span></span><br><span class="line">    hf3fs_dereg_fd(fd);</span><br><span class="line">    close(fd);</span><br><span class="line">    hf3fs_iovdestroy(&amp;iov);</span><br><span class="line">    hf3fs_iordestroy(&amp;ior);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是，提交 IO 和等待 IO 完成可以不在同一个线程，但同时只能有一个线程提交 IO 和一个线程等待 IO 完成。</p><p>USRBIO SDK 主要分几个功能：</p><ol><li>Ring + Pinned Memory Buffer 管理</li><li>非 IO 关键的 Meta 操作卸载为 <code>ioctl</code> 调用</li><li>IO 关键（如 <code>open</code>、<code>close</code>）的 Meta 操作走 VFS FUSE</li><li>读写 IO 构造请求往 SQ 塞 SQE 通知 FUSE Daemon，并等 FUSE Daemon 通知从 CQ 取 CQE</li></ol><p>3FS 并没有实现 Polling Mode 的 Ring Based IO，而是通过 IPC 信号量来唤醒进程，这里可能是出于节省 CPU 和攒 Batch 考虑。要改成 Polling Mode 也不太困难，轮询队列的 head&#x2F;tail 就可以了。</p><h3 id="FUSE-Daemon-侧-USRBIO-分析"><a href="#FUSE-Daemon-侧-USRBIO-分析" class="headerlink" title="FUSE Daemon 侧 USRBIO 分析"></a>FUSE Daemon 侧 USRBIO 分析</h3><p>FUSE Daemon 承担了 IoRing 的工作线程任务，在 FUSE Daemon 启动时，会启动 IoRing Workers（带取消机制，详见 <a href="https://sf-zhou.github.io/coroutine/folly_coro_cancellation.html">https://sf-zhou.github.io/coroutine/folly_coro_cancellation.html</a>） 和 IoRing Watchers（用于接收信号量唤醒）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/fuse/FuseClients.cc</span></span><br><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">FuseClients::init</span><span class="params">(...)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">// M:N 调度协程到线程池中</span></span><br><span class="line">  <span class="keyword">auto</span> &amp;tp = client-&gt;<span class="built_in">tpg</span>().<span class="built_in">bgThreadPool</span>();</span><br><span class="line">  <span class="keyword">auto</span> coros = fuseConfig.<span class="built_in">batch_io_coros</span>();</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; coros; ++i) &#123;</span><br><span class="line">    <span class="keyword">auto</span> exec = &amp;tp.<span class="built_in">get</span>(i % tp.<span class="built_in">size</span>());</span><br><span class="line">    <span class="comment">// 此处可以在 Fuse 被关闭的时候结束执行中的请求</span></span><br><span class="line">    <span class="comment">// 取消机制原理详见：https://sf-zhou.github.io/coroutine/folly_coro_cancellation.html</span></span><br><span class="line">    <span class="built_in">co_withCancellation</span>(cancelIos.<span class="built_in">getToken</span>(), <span class="built_in">ioRingWorker</span>(i, coros)).<span class="built_in">scheduleOn</span>(exec).<span class="built_in">start</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 每个信号量一个 Watcher 线程</span></span><br><span class="line">  ioWatches.<span class="built_in">reserve</span>(<span class="number">3</span>);</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">3</span>; ++i) &#123;</span><br><span class="line">    ioWatches.<span class="built_in">emplace_back</span>(folly::<span class="built_in">partial</span>(&amp;FuseClients::watch, <span class="keyword">this</span>, i));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Watcher 实现比较简单，就是等信号量，然后从 IoRing 共享内存取一批 SQE，封装成一个 Job 塞入工作队列：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">FuseClients::watch</span><span class="params">(<span class="type">int</span> prio, std::stop_token stop)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">while</span> (!stop.<span class="built_in">stop_requested</span>()) &#123;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">timespec</span> ts;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">clock_gettime</span>(CLOCK_REALTIME, &amp;ts) &lt; <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> nsec = ts.tv_nsec + jitter.<span class="built_in">load</span>().<span class="built_in">count</span>();</span><br><span class="line">    ts.tv_nsec = nsec % <span class="number">1000000000</span>;</span><br><span class="line">    ts.tv_sec += nsec / <span class="number">1000000000</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">sem_timedwait</span>(iors.sems[prio].<span class="built_in">get</span>(), &amp;ts) &lt; <span class="number">0</span> &amp;&amp; errno == ETIMEDOUT) &#123;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> gotJobs = <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">      gotJobs = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">auto</span> n = iors.ioRings-&gt;slots.nextAvail.<span class="built_in">load</span>();</span><br><span class="line">      <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; ++i) &#123;</span><br><span class="line">        <span class="keyword">auto</span> ior = iors.ioRings-&gt;table[i].<span class="built_in">load</span>();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (ior &amp;&amp; ior-&gt;priority == prio) &#123;</span><br><span class="line">          <span class="keyword">auto</span> jobs = ior-&gt;<span class="built_in">jobsToProc</span>(config-&gt;<span class="built_in">max_jobs_per_ioring</span>());</span><br><span class="line">          <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;&amp;job : jobs) &#123;</span><br><span class="line">            gotJobs = <span class="literal">true</span>;</span><br><span class="line">            iojqs[prio]-&gt;<span class="built_in">enqueue</span>(std::<span class="built_in">move</span>(job));</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">while</span> (gotJobs);  <span class="comment">// loop till we found no more jobs and then block in the next iter</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工作协程从工作队列取 Job 完成一批批的 SQE 操作，取任务的逻辑比较复杂，这里简单总结下：</p><ol><li>根据自己的协程序号确定自己应该处理哪个优先级的 IO 请求，协程序号小的协程对应处理高优先级的任务，反之亦然</li><li>从自己优先级任务队列取任务执行；队列为空的话，低优先级的协程也可取高优先级的任务队列中的任务执行，但执行一次后下次就不会 Steal 任务避免自己队列饿死</li></ol><p>执行任务的起点是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTask&lt;Void&gt; <span class="title">FuseClients::ioRingWorker</span><span class="params">(<span class="type">int</span>, <span class="type">int</span>)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// For Loop, 从队列取任务, 一个 Job 可能处理多个 SQE！</span></span><br><span class="line">  job = <span class="keyword">co_await</span> iojqs[prio]-&gt;<span class="built_in">co_dequeue</span>();</span><br><span class="line">  <span class="keyword">co_await</span> job.ior-&gt;<span class="built_in">process</span>(job.sqeProcTail,</span><br><span class="line">                                  job.toProc,</span><br><span class="line">                                  *storageClient,</span><br><span class="line">                                  config-&gt;<span class="built_in">storage_io</span>(),</span><br><span class="line">                                  userConfig,</span><br><span class="line">                                  std::<span class="built_in">move</span>(lookupFiles),</span><br><span class="line">                                  std::<span class="built_in">move</span>(lookupBufs));</span><br><span class="line">  <span class="comment">// 如果高优队列满或是 Steal 的任务则唤醒对应 Ring</span></span><br><span class="line">  <span class="keyword">if</span> (iojqs[<span class="number">0</span>]-&gt;<span class="built_in">full</span>() || job.ior-&gt;priority != prio) &#123;</span><br><span class="line">          <span class="built_in">sem_post</span>(iors.sems[job.ior-&gt;priority].<span class="built_in">get</span>());  <span class="comment">// wake the watchers</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="comment">// 顺便检查当前 SQ 有没有新的任务，减少回到等待信号量的次数</span></span><br><span class="line">          <span class="keyword">auto</span> jobs = job.ior-&gt;<span class="built_in">jobsToProc</span>(<span class="number">1</span>);</span><br><span class="line">          <span class="keyword">if</span> (!jobs.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">            job = jobs.<span class="built_in">front</span>();</span><br><span class="line">            <span class="keyword">if</span> (!iojqs[<span class="number">0</span>]-&gt;<span class="built_in">try_enqueue</span>(job)) &#123;</span><br><span class="line">              <span class="keyword">continue</span>;</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行任务的逻辑也比较关键和复杂，这里也不贴代码直接总结下：</p><ol><li>调用 <code>lookupFiles</code> 获取这批 IO 涉及到的 Inode</li><li>调用 <code>lookupBufs</code> 获取这批 IO 涉及到的 Buffer Iov</li><li>根据 Ior 的读&#x2F;写类型，往 PIoV 中塞入构造好的读写请求</li><li>调用 PIoV 的 <code>executeRead</code> 或者 <code>executeWrite</code> 执行读写请求。对于读和写分别有不同的并行策略，详见后面分析</li><li>收集执行结果，构造 CQE，写入 Ior 的 CQ 中，Post cqeSem 唤醒用户进程</li></ol><h3 id="读写并行策略分析"><a href="#读写并行策略分析" class="headerlink" title="读写并行策略分析"></a>读写并行策略分析</h3><p>对于并行读写，它们在一些逻辑上是相同的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 并行读</span></span><br><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">StorageClientImpl::batchReadWithoutRetry</span><span class="params">(ClientRequestContext &amp;requestCtx,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                         <span class="type">const</span> std::vector&lt;ReadIO *&gt; &amp;readIOs,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                         <span class="type">const</span> flat::UserInfo &amp;userInfo,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                         <span class="type">const</span> ReadOptions &amp;options)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// collect target/chain infos</span></span><br><span class="line">  TargetSelectionOptions targetSelectionOptions = options.<span class="built_in">targetSelection</span>();</span><br><span class="line">  targetSelectionOptions.<span class="built_in">set_mode</span>(options.<span class="built_in">targetSelection</span>().<span class="built_in">mode</span>() == TargetSelectionMode::Default</span><br><span class="line">                                      ? TargetSelectionMode::LoadBalance</span><br><span class="line">                                      : options.<span class="built_in">targetSelection</span>().<span class="built_in">mode</span>());</span><br><span class="line"></span><br><span class="line">  std::shared_ptr&lt;hf3fs::client::RoutingInfo <span class="type">const</span>&gt; routingInfo = <span class="built_in">getCurrentRoutingInfo</span>();</span><br><span class="line">  <span class="keyword">auto</span> targetedOps = <span class="built_in">selectRoutingTargetForOps</span>(requestCtx, routingInfo, targetSelectionOptions, readIOs);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// select storage target for each IO and group by node id</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> batches = <span class="built_in">groupOpsByNodeId</span>(requestCtx,</span><br><span class="line">                                  targetedOps,</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">read</span>().<span class="built_in">max_batch_size</span>(),</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">read</span>().<span class="built_in">max_batch_bytes</span>(),</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">read</span>().<span class="built_in">random_shuffle_requests</span>());</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 并行写</span></span><br><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">StorageClientImpl::batchWriteWithoutRetry</span><span class="params">(ClientRequestContext &amp;requestCtx,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                          <span class="type">const</span> std::vector&lt;WriteIO *&gt; &amp;writeIOs,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                          <span class="type">const</span> flat::UserInfo &amp;userInfo,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                          <span class="type">const</span> WriteOptions &amp;options)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// collect target/chain infos</span></span><br><span class="line"></span><br><span class="line">  TargetSelectionOptions targetSelectionOptions = options.<span class="built_in">targetSelection</span>();</span><br><span class="line">  targetSelectionOptions.<span class="built_in">set_mode</span>(options.<span class="built_in">targetSelection</span>().<span class="built_in">mode</span>() == TargetSelectionMode::Default</span><br><span class="line">                                      ? TargetSelectionMode::HeadTarget</span><br><span class="line">                                      : options.<span class="built_in">targetSelection</span>().<span class="built_in">mode</span>());</span><br><span class="line"></span><br><span class="line">  std::shared_ptr&lt;hf3fs::client::RoutingInfo <span class="type">const</span>&gt; routingInfo = <span class="built_in">getCurrentRoutingInfo</span>();</span><br><span class="line">  <span class="keyword">auto</span> targetedIOs = <span class="built_in">selectRoutingTargetForOps</span>(requestCtx, routingInfo, targetSelectionOptions, writeIOs);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// select storage target for each IO and group by node id</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> batches = <span class="built_in">groupOpsByNodeId</span>(requestCtx,</span><br><span class="line">                                  targetedIOs,</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">write</span>().<span class="built_in">max_batch_size</span>(),</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">write</span>().<span class="built_in">max_batch_bytes</span>(),</span><br><span class="line">                                  config_.<span class="built_in">traffic_control</span>().<span class="built_in">write</span>().<span class="built_in">random_shuffle_requests</span>());</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>根据 IO 请求的目的 Chunk 和 Chain 决定 IO 请求应该发往哪个 Storage Server；写操作必须选复制链中的头节点；读操作可以按照各种负载均衡策略任意选择链中的节点，充分聚合利用各 Storage Server 的读带宽</li><li>根据 1 中的决策，将 IO 请求分别按照目的 Storage Server 聚合起来，并在每个 Server 内再进行一次分批聚合，将请求聚合成请求 Batch</li><li>根据配置文件串行执行&#x2F;并发执行 Batches，此时根据单个 Storage Server 并发收到的请求数和单个 Storage Client 总的并发发送请求数进行限流</li></ol><p>Batch 间可以根据配置文件串行或者调用 <code>collectAllRange</code> 并发执行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Op, <span class="keyword">typename</span> Ops = std::vector&lt;Op *&gt;&gt;</span><br><span class="line">CoTask&lt;<span class="type">void</span>&gt; <span class="built_in">processBatches</span>(<span class="type">const</span> std::vector&lt;std::pair&lt;NodeId, Ops&gt;&gt; &amp;batches, <span class="keyword">auto</span> &amp;&amp;func, <span class="type">bool</span> parallel) &#123;</span><br><span class="line">  std::vector&lt;CoTask&lt;<span class="type">bool</span>&gt;&gt; tasks;</span><br><span class="line">  <span class="keyword">if</span> (parallel) tasks.<span class="built_in">reserve</span>(batches.<span class="built_in">size</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">size_t</span> index = <span class="number">0</span>; index &lt; batches.<span class="built_in">size</span>(); index++) &#123;</span><br><span class="line">    <span class="type">const</span> <span class="keyword">auto</span> &amp;[nodeId, ops] = batches[index];</span><br><span class="line">    <span class="keyword">if</span> (!ops.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">      <span class="keyword">if</span> (parallel) &#123;</span><br><span class="line">        tasks.<span class="built_in">push_back</span>(<span class="built_in">func</span>(index, nodeId, ops));</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">co_await</span> <span class="built_in">func</span>(index, nodeId, ops);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (parallel) <span class="keyword">co_await</span> folly::coro::<span class="built_in">collectAllRange</span>(std::<span class="built_in">move</span>(tasks));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// StorageClientImpl::batchReadWithoutRetry</span></span><br><span class="line">  <span class="type">bool</span> parallelProcessing = config_.<span class="built_in">traffic_control</span>().<span class="built_in">read</span>().<span class="built_in">process_batches_in_parallel</span>();</span><br><span class="line">  <span class="keyword">co_await</span> <span class="built_in">processBatches</span>&lt;ReadIO&gt;(batches, sendReq, parallelProcessing);</span><br><span class="line"></span><br><span class="line"><span class="comment">// StorageClientImpl::batchWriteWithoutRetry</span></span><br><span class="line">  <span class="type">bool</span> parallelProcessing = config_.<span class="built_in">traffic_control</span>().<span class="built_in">write</span>().<span class="built_in">process_batches_in_parallel</span>();</span><br><span class="line">  <span class="keyword">co_await</span> <span class="built_in">processBatches</span>&lt;WriteIO&gt;(batches, sendReq, parallelProcessing);</span><br></pre></td></tr></table></figure><p>然而在具体执行读写操作时，它们实际发往 Storage Server 的请求会根据读写不同而有所不同。</p><h4 id="并行读策略分析"><a href="#并行读策略分析" class="headerlink" title="并行读策略分析"></a>并行读策略分析</h4><p>对于并行读请求，会将每一批的请求，组装成一个单独的 <code>BatchRead</code> 请求：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// 组装 BatchRead 请求</span></span><br><span class="line"><span class="keyword">template</span> &lt;&gt;</span><br><span class="line"><span class="keyword">typename</span> hf3fs::<span class="function">storage::BatchReadReq <span class="title">buildBatchRequest</span><span class="params">(<span class="type">const</span> ClientRequestContext &amp;requestCtx,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        <span class="type">const</span> ClientId &amp;clientId,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        std::<span class="type">atomic_uint64_t</span> &amp;nextRequestId,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        <span class="type">const</span> StorageClient::Config &amp;config,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        <span class="type">const</span> ReadOptions &amp;options,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        <span class="type">const</span> flat::UserInfo &amp;userInfo,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                        <span class="type">const</span> std::vector&lt;ReadIO *&gt; &amp;ops)</span> </span>&#123;</span><br><span class="line">  std::vector&lt;hf3fs::storage::ReadIO&gt; payloads;</span><br><span class="line">  payloads.<span class="built_in">reserve</span>(ops.<span class="built_in">size</span>());</span><br><span class="line">  <span class="type">size_t</span> requestedBytes = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">auto</span> requestTagSet = monitor::<span class="built_in">instanceTagSet</span>(<span class="string">&quot;batchRead&quot;</span>);</span><br><span class="line">  <span class="keyword">auto</span> tagged_bytes_per_operation = bytes_per_operation.<span class="built_in">getRecoderWithTag</span>(requestTagSet);</span><br><span class="line"></span><br><span class="line">  hf3fs::<span class="function">storage::RequestId <span class="title">requestId</span><span class="params">(nextRequestId.fetch_add(<span class="number">1</span>))</span></span>;</span><br><span class="line">  hf3fs::storage::MessageTag tag&#123;clientId, requestId&#125;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;op : ops) &#123;</span><br><span class="line">    hf3fs::storage::GlobalKey key&#123;op-&gt;routingTarget.<span class="built_in">getVersionedChainId</span>(), op-&gt;chunkId&#125;;</span><br><span class="line"></span><br><span class="line">    <span class="type">size_t</span> offset = op-&gt;data - op-&gt;buffer-&gt;<span class="built_in">data</span>();</span><br><span class="line">    <span class="keyword">auto</span> iobuf = op-&gt;buffer-&gt;<span class="built_in">subrange</span>(offset, op-&gt;length);</span><br><span class="line"></span><br><span class="line">    requestedBytes += op-&gt;length;</span><br><span class="line">    tagged_bytes_per_operation-&gt;<span class="built_in">addSample</span>(op-&gt;length);</span><br><span class="line"></span><br><span class="line">    op-&gt;requestId = requestId;</span><br><span class="line">    payloads.<span class="built_in">push_back</span>(&#123;op-&gt;offset, op-&gt;length, std::<span class="built_in">move</span>(key), iobuf.<span class="built_in">toRemoteBuf</span>()&#125;);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 设置一些 metrics 和请求 Flag</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> hf3fs::storage::BatchReadReq&#123;std::<span class="built_in">move</span>(payloads),</span><br><span class="line">                                      tag,</span><br><span class="line">                                      requestCtx.retryCount,</span><br><span class="line">                                      userInfo,</span><br><span class="line">                                      featureFlags,</span><br><span class="line">                                      checksumType,</span><br><span class="line">                                      requestCtx.debugFlags&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// StorageClientImpl::batchReadWithoutRetry 实际执行的请求逻辑 </span></span><br><span class="line"><span class="keyword">auto</span> batchReq = <span class="built_in">buildBatchRequest</span>&lt;ReadIO, BatchReadReq&gt;(requestCtx,</span><br><span class="line">                                                            clientId_,</span><br><span class="line">                                                            nextRequestId_,</span><br><span class="line">                                                            config_,</span><br><span class="line">                                                            options,</span><br><span class="line">                                                            userInfo,</span><br><span class="line">                                                            batchIOs);</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> response =</span><br><span class="line">        <span class="keyword">co_await</span> <span class="built_in">sendBatchRequest</span>&lt;ReadIO, BatchReadReq, BatchReadRsp, &amp;StorageMessenger::batchRead&gt;(messenger_,</span><br><span class="line">                                                                                                    requestCtx,</span><br><span class="line">                                                                                                    routingInfo,</span><br><span class="line">                                                                                                    nodeId,</span><br><span class="line">                                                                                                    batchReq,</span><br><span class="line">                                                                                                    batchIOs);</span><br></pre></td></tr></table></figure><p>就是简单地将一个 Batch 中的 ReadIO 组装到一个 <code>BatchRead</code> 请求中。</p><h4 id="并行写策略分析"><a href="#并行写策略分析" class="headerlink" title="并行写策略分析"></a>并行写策略分析</h4><p>写只有 Batch 间的并发，Batch 内串行执行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// StorageClientImpl::batchWriteWithoutRetry 中实际执行的逻辑</span></span><br><span class="line"><span class="keyword">if</span> (!<span class="built_in">allocateChannelsForOps</span>(chanAllocator_, batchIOs, <span class="literal">false</span> <span class="comment">/*reallocate*/</span>)) &#123;</span><br><span class="line">      <span class="built_in">XLOGF</span>(WARN,</span><br><span class="line">            <span class="string">&quot;Cannot allocate channel ids for &#123;&#125; write IOs, first IO &#123;&#125;&quot;</span>,</span><br><span class="line">            batchIOs.<span class="built_in">size</span>(),</span><br><span class="line">            fmt::<span class="built_in">ptr</span>(batchIOs.<span class="built_in">front</span>()));</span><br><span class="line">      <span class="built_in">setErrorCodeOfOps</span>(batchIOs, StorageClientCode::kResourceBusy);</span><br><span class="line">      <span class="keyword">co_return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// log the waiting time before communication starts</span></span><br><span class="line">    requestCtx.<span class="built_in">logWaitingTime</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> statusCode =</span><br><span class="line">        <span class="keyword">co_await</span> <span class="built_in">sendWriteRequestsSequentially</span>(requestCtx, batchIOs, routingInfo, nodeId, userInfo, options);</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>3FS 的客户端实现复杂程度不亚于其服务端，主要复杂在 FUSE 和 USRBIO 的 IPC、FUSE 本身对于 IO 请求的批处理逻辑。尽管从设计思想看客户端的实现思路比较简洁，但其中涉及到大量的跨进程、跨线程同步，以及各种提升性能的技巧，以及多个 Ior 和 Iov 的管理，实现细节非常繁琐。</p><p>个人感觉 FUSE Daemon 在此处起到了类似 Agent 的作用，收敛了网络线程避免创建太多 QP 和提供集中式元数据缓存减少 Meta RPC。未来有机会扩展成 P2P 的客户端数据缓存，支持 BatchRead 从另外的客户端内存中读取数据，进一步释放读性能。</p><p>另外，篇幅限制本文还有许多设计实现讨论未能涉及，例如还没提及 3FS 还对 USRBIO 封装了 Python 接口，还没有分析选择 Target 的详细策略等，对更多细节感兴趣的读者可以继续阅读蚂蚁存储团队的文章：<a href="https://mp.weixin.qq.com/s/sPkqOdVA3qBAUiMQltveoQ">https://mp.weixin.qq.com/s/sPkqOdVA3qBAUiMQltveoQ</a> ，他们也在其中讨论了客户端设计的考量和权衡</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>DeepSeek 3FS 源码解读——RPC 篇</title>
      <link>https://blog.howardlau.me/programming/deepseek-3fs-code-reading-rpc.html</link>
      <description>
        <![CDATA[<p>趁热打铁，这回我们看看 3FS 的 RPC 子系统是如何搭建的。粗略分析后，3FS 没有采用类似 Protocol Buffers 或者 Flatbuffers 等 IDL 语言，而是大量使用了 C++ 模板以及宏实现了一套简易的反射机制，并且使用 C++ 20]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 03 Mar 2025 09:10:04 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>趁热打铁，这回我们看看 3FS 的 RPC 子系统是如何搭建的。粗略分析后，3FS 没有采用类似 Protocol Buffers 或者 Flatbuffers 等 IDL 语言，而是大量使用了 C++ 模板以及宏实现了一套简易的反射机制，并且使用 C++ 20 Concepts 和编译时 constexpr 使一些模板逻辑更清晰易读，从而使开发者用宏就能在 C++ 源代码中定义出 RPC 的请求回复结构体，以及 RPC 服务接口定义。这一块又是相当能体现 DeepSeek “妖”的代码风格，如果说协程部分是现代 C++，那么 RPC 框架的代码就是<strong>后</strong>现代 C++，不禁让人感叹这是人能写出来的代码？？？</p><p>本文分析序列化工具和网络收发调用关系。还是请先在阅读本文前，阅读<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-coroutine.html">《DeepSeek 3FS 源码解读——协程&amp;RDMA篇》</a>大致了解 3FS 的网络处理流程。</p><p>RPC 框架主要逻辑位于 <code>src/common/serde</code> 目录下。</p><h2 id="结构体序列化与反序列化"><a href="#结构体序列化与反序列化" class="headerlink" title="结构体序列化与反序列化"></a>结构体序列化与反序列化</h2><p><code>src/common/serde/Serde.h</code> 中主要负责的是提供宏 <code>SERDE_STRUCT_FIELD(NAME, DEFAULT)</code> 用于定义 <code>struct</code> 中的字段，并自动通过反射注册字段，使得 RPC 框架能够自动进行序列化&#x2F;反序列化。这个头文件中杂糅了二进制&#x2F;JSON&#x2F;TOML的序列化反序列化的逻辑。</p><p>先研究下 <code>SERDE_STRUCT_FIELD</code>，挑一个展开后的类来研究一下，以 <code>UpdateIO</code> 前两个字段为例：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 展开 SERDE_STRUCT_FIELD(offset, uint32_t&#123;&#125;);</span></span><br><span class="line"><span class="comment">// 展开 SERDE_STRUCT_FIELD(length, uint32_t&#123;&#125;);</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:  </span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper;  </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Toffset</span> : std::type_identity&lt;<span class="keyword">decltype</span>(<span class="built_in">CollectField</span>(::hf3fs::refl::Rank&lt;&gt;&#123;&#125;))&gt; &#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">public</span>:  </span><br><span class="line">  std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(<span class="type">uint32_t</span>&#123;&#125;)&gt; offset = <span class="type">uint32_t</span>&#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">protected</span>:  </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="title">Toffset</span><span class="params">()</span> -&gt; ::hf3fs::thief::steal&lt;<span class="keyword">struct</span> Toffset, std::<span class="type">decay_t</span>&lt;<span class="title">decltype</span><span class="params">(*<span class="keyword">this</span>)</span>&gt;&gt;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="type">static</span> ::hf3fs::refl::Append_t&lt;  </span><br><span class="line">  <span class="keyword">typename</span> Toffset::type,  </span><br><span class="line">  <span class="keyword">decltype</span>((::hf3fs::serde::FieldInfo&lt;<span class="string">&quot;offset&quot;</span>, &amp;::hf3fs::thief::retrieve&lt;<span class="keyword">struct</span> Toffset&gt;::offset&gt;&#123;&#125;))&gt;</span><br><span class="line"><span class="built_in">CollectField</span>(::hf3fs::refl::Rank&lt;std::tuple_size_v&lt;<span class="keyword">typename</span> Toffset::type&gt; + <span class="number">1</span>&gt;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:  </span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper;  </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Tlength</span> : std::type_identity&lt;<span class="keyword">decltype</span>(<span class="built_in">CollectField</span>(::hf3fs::refl::Rank&lt;&gt;&#123;&#125;))&gt; &#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">public</span>:  </span><br><span class="line">  std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(<span class="type">uint32_t</span>&#123;&#125;)&gt; length = <span class="type">uint32_t</span>&#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">protected</span>:  </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="title">Tlength</span><span class="params">()</span> -&gt; ::hf3fs::thief::steal&lt;<span class="keyword">struct</span> Tlength, std::<span class="type">decay_t</span>&lt;<span class="title">decltype</span><span class="params">(*<span class="keyword">this</span>)</span>&gt;&gt;</span>;  </span><br><span class="line">  <span class="type">static</span> ::hf3fs::refl::Append_t&lt;  </span><br><span class="line">  <span class="keyword">typename</span> Tlength::type,  </span><br><span class="line">  <span class="keyword">decltype</span>((::hf3fs::serde::FieldInfo&lt;<span class="string">&quot;length&quot;</span>, &amp;::hf3fs ::thief::retrieve&lt;<span class="keyword">struct</span> Tlength&gt;::length&gt;&#123;&#125;))&gt;  </span><br><span class="line"><span class="built_in">CollectField</span>(::hf3fs::refl::Rank&lt;std::tuple_size_v&lt;<span class="keyword">typename</span> Tlength::type&gt; + <span class="number">1</span>&gt;)</span><br></pre></td></tr></table></figure><p>其中 <code>steal</code> 和 <code>retrieve</code> 定义如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">namespace</span> hf3fs::thief &#123;</span><br><span class="line"><span class="keyword">namespace</span> detail &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Tag&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Bridge</span> &#123;</span><br><span class="line">  <span class="function"><span class="keyword">friend</span> <span class="keyword">consteval</span> <span class="keyword">auto</span> <span class="title">ADL</span><span class="params">(Bridge&lt;Tag&gt;)</span></span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Tag, <span class="keyword">typename</span> Store&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">StealType</span> &#123;</span><br><span class="line">  <span class="function"><span class="keyword">friend</span> <span class="keyword">consteval</span> <span class="keyword">auto</span> <span class="title">ADL</span><span class="params">(Bridge&lt;Tag&gt;)</span> </span>&#123; <span class="keyword">return</span> std::type_identity&lt;Store&gt;&#123;&#125;; &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">&#125;  <span class="comment">// namespace detail</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Tag, <span class="keyword">typename</span> Store&gt;</span><br><span class="line"><span class="keyword">using</span> steal = <span class="keyword">decltype</span>(detail::StealType&lt;Tag, Store&gt;&#123;&#125;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Tag&gt;</span><br><span class="line"><span class="keyword">using</span> retrieve = <span class="keyword">typename</span> <span class="keyword">decltype</span>(<span class="built_in">ADL</span>(detail::Bridge&lt;Tag&gt;&#123;&#125;))::type;</span><br><span class="line"></span><br><span class="line">&#125;  <span class="comment">// namespace hf3fs::thief</span></span><br></pre></td></tr></table></figure><p>我承认这里我实在看不太懂了，只能大概说说原理。看模板就一个诀窍：人肉展开！<code>steal</code> 在这里会实例化为 <code>decltype(detail::StealType&lt;Toffset, UpdateIO&gt;{})</code>，那么也就实例化出了里面的 <code>ADL</code> 方法。而 <code>retrieve</code> 实例化为 <code>typename decltype(ADL(detail::Bridge&lt;struct Toffset&gt;{}))::type</code>，通过 ADL 方法会匹配到该标签类曾经绑定到的 <code>Store</code>，也就是 <code>UpdateIO</code>。</p><p>更新：经过请教多位网友，这里是一种名为 Friend Injection 的技巧，实现原理参考 <a href="https://zhuanlan.zhihu.com/p/646752343">https://zhuanlan.zhihu.com/p/646752343</a>；DeepSeek 研发博客也有记录使用这种技巧是为了在类内部获取当前类的名字方便宏展开，参考 <a href="https://sf-zhou.github.io/programming/cpp_static_reflection.html">https://sf-zhou.github.io/programming/cpp_static_reflection.html</a> 以及 <a href="https://github.com/MitalAshok/self_macro">https://github.com/MitalAshok/self_macro</a>。也可以用来反射对象的私有字段，原理利用了类模版显式实例化时可以访问私有字段。</p><p>阿里开源的 yalantinglibs 采用了更先进的反射技巧，ylt 的反射甚至都不需要宏。<a href="https://alibaba.github.io/yalantinglibs/zh/reflection/reflection_introduction.html">https://alibaba.github.io/yalantinglibs/zh/reflection/reflection_introduction.html</a>直接访问对象的所有字段，包括字段个数、字段名、字段值、字段索引等信息。</p><p><code>Append_t</code> 则负责将当前的 <code>field</code> 注册到参数列表中，形成一个新的重载，经过一系列的模板实例化和别名替换后，最终展开的形式其实是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span>:  </span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper;</span><br><span class="line">  <span class="comment">// Rank&lt;&gt; 就是 Rank&lt;64&gt; </span></span><br><span class="line">  <span class="comment">// 然后递归地 Rank&lt;64&gt; : Rank&lt;63&gt;，Rank&lt;63&gt; : Rank&lt;62&gt; 一路继承到 Rank&lt;0&gt;，终止递归</span></span><br><span class="line">  <span class="comment">// 由于 offset 是第一个字段，所以直接匹配到了默认的</span></span><br><span class="line">  <span class="comment">// [[maybe_unused]] static std::tuple&lt;&gt; CollectField(::hf3fs::refl::Rank&lt;0&gt;);</span></span><br><span class="line">  <span class="comment">// 此时展开为 </span></span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Toffset</span> : std::type_identity&lt;std::tuple&lt;&gt;&gt; &#123;&#125;;  </span><br><span class="line">  <span class="comment">// 所以也能看出 RPC 框架应该最多支持 64 个字段</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:  </span><br><span class="line">  <span class="type">uint32_t</span> offset = <span class="type">uint32_t</span>&#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">protected</span>:</span><br><span class="line">  <span class="comment">// 显式实例化 steal，实现 Friend Injection</span></span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="title">Toffset</span><span class="params">()</span> -&gt; detail::StealType&lt;Toffset, UpdateIO&gt;</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 注意此处将新的 Field 添加到 Tuple 中</span></span><br><span class="line">  <span class="comment">// 并且此时 CollectField 形参中变为 Rank&lt;1&gt;</span></span><br><span class="line">  <span class="comment">// 由于 Rank 是按照从大到小去匹配形参</span></span><br><span class="line">  <span class="comment">// 下一次 decltype(CollectField(::hf3fs::refl::Rank&lt;&gt;&#123;&#125;))</span></span><br><span class="line">  <span class="comment">// 就会匹配到此函数声明，从而 decltype 会获取到添加了 field 后</span></span><br><span class="line">  <span class="comment">// 的新的 tuple 类型</span></span><br><span class="line">  <span class="type">static</span> ::hf3fs::refl::Append_t&lt;  </span><br><span class="line">  std::tuple&lt;&gt;,  </span><br><span class="line">  ::hf3fs::serde::FieldInfo&lt;<span class="string">&quot;offset&quot;</span>, &amp;UpdateIO::offset&gt;</span><br><span class="line">  &gt;</span><br><span class="line"><span class="built_in">CollectField</span>(::hf3fs::refl::Rank&lt;<span class="number">1</span>&gt;);</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:  </span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper;  </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Tlength</span> : std::type_identity&lt;std::tuple&lt;::hf3fs::serde::FieldInfo&lt;&quot;offset&quot;, &amp;UpdateIO::offset&gt;&gt;&gt; &#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">public</span>:  </span><br><span class="line">  <span class="type">uint32_t</span> length = <span class="type">uint32_t</span>&#123;&#125;;  </span><br><span class="line">  </span><br><span class="line"><span class="keyword">protected</span>:  </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="title">Tlength</span><span class="params">()</span> -&gt; detail::StealType&lt;Tlength, UpdateIO&gt;</span>;  </span><br><span class="line">  <span class="type">static</span> ::hf3fs::refl::Append_t&lt;  </span><br><span class="line">  std::tuple&lt;::hf3fs::serde::FieldInfo&lt;&quot;offset&quot;, &amp;UpdateIO::offset&gt;&gt;,  </span><br><span class="line">  ::hf3fs::<span class="function">serde::FieldInfo&lt;&quot;length&quot;, &amp;UpdateIO::length&gt; </span></span><br><span class="line"><span class="function"><span class="title">CollectField</span><span class="params">(::hf3fs::refl::Rank&lt;<span class="number">2</span>&gt;)</span></span>;</span><br></pre></td></tr></table></figure><p>到这里注册 field 的逻辑我们就基本搞明白了，其实就是通过一系列的模板实例化，将 <code>FieldInfo</code> 通过声明新的 <code>CollectField</code> 重载不断添加到 <code>tuple</code> 中。</p><p>现在看看怎么获取一个类中注册了哪些 field，其实很简单，直接获取 Rank 最大的 <code>CollectField</code> 重载的返回值类型，就能拿到包含全部 <code>FieldInfo</code> 的 <code>tuple</code>，剩下的就是通过模板操作，遍历或者索引我们想要的 <code>tuple</code> 元素即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Helper</span> &#123;</span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="title">decltype</span><span class="params">(CollectField(refl::Rank&lt;&gt;&#123;&#125;))</span> <span class="title">getFieldInfo</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">  <span class="keyword">requires</span> <span class="keyword">requires</span> &#123;</span><br><span class="line">    &#123; T::<span class="built_in">CollectField</span>(refl::Rank&lt;&gt;&#123;&#125;) &#125; -&gt; is_specialization&lt;std::tuple&gt;;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="title">decltype</span><span class="params">(T::CollectField(refl::Rank&lt;&gt;&#123;&#125;))</span> <span class="title">getFieldInfo</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">  <span class="keyword">using</span> FieldInfoList = <span class="keyword">decltype</span>(<span class="built_in">getFieldInfo</span>&lt;T&gt;());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="keyword">auto</span> Size = std::tuple_size_v&lt;FieldInfoList&lt;T&gt;&gt;;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="type">size_t</span> I&gt;</span><br><span class="line">  <span class="keyword">using</span> FieldInfo = std::<span class="type">tuple_element_t</span>&lt;I, FieldInfoList&lt;T&gt;&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;SerdeType T&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">count</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> refl::Helper::Size&lt;T&gt;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;SerdeType T, <span class="type">size_t</span> Index&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">name</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> refl::Helper::FieldInfo&lt;T, Index&gt;::name;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;SerdeType T, <span class="type">size_t</span> Index&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">getter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> refl::Helper::FieldInfo&lt;T, Index&gt;::getter;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">count</span><span class="params">(<span class="keyword">auto</span> &amp;&amp;o)</span> </span>&#123; <span class="keyword">return</span> count&lt;std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(o)&gt;&gt;(); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="type">size_t</span> Index&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="title">name</span><span class="params">(<span class="keyword">auto</span> &amp;&amp;o)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> name&lt;std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(o)&gt;, Index&gt;();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="type">size_t</span> Index&gt;</span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> &amp;<span class="title">value</span><span class="params">(<span class="keyword">auto</span> &amp;&amp;o)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> o.*getter&lt;std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(o)&gt;, Index&gt;();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="type">size_t</span> I = <span class="number">0</span>&gt;</span><br><span class="line"><span class="keyword">constexpr</span> <span class="keyword">inline</span> <span class="keyword">auto</span> <span class="built_in">iterate</span>(<span class="keyword">auto</span> &amp;&amp;f, <span class="keyword">auto</span> &amp;&amp;o, <span class="keyword">auto</span> &amp;&amp;...args) &#123;</span><br><span class="line">  <span class="keyword">return</span> refl::Helper::iterate&lt;std::<span class="type">decay_t</span>&lt;<span class="keyword">decltype</span>(o)&gt;&gt;(</span><br><span class="line">      [&amp;](<span class="keyword">auto</span> type) &#123; <span class="keyword">return</span> <span class="built_in">f</span>(type.name, o.*type.getter, args...); &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>序列化的函数是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">O</span>&gt;</span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">void</span> <span class="title">serialize</span><span class="params">(<span class="keyword">auto</span> &amp;&amp;o, Out&lt;O&gt; &amp;out)</span></span></span><br></pre></td></tr></table></figure><p>里面通过一大堆的 <code>if constexpr (requires { o.serdeToReadable(); } &amp;&amp; !isBinaryOut)</code> 类似的语句，去尝试匹配传进来的第一个参数的对象类型应该调用什么方法序列化。如果没有 C++ 17&#x2F;20 的 <code>if constexpr</code> 那么将需要一大堆的 <code>std::enable_if</code> 通过 SFINAE 来编写相同的逻辑，那将会更加反人类。</p><p>对于一整个序列化类来说起点是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> <span class="built_in">constexpr</span> (SerdeType&lt;T&gt;) &#123;</span><br><span class="line">  <span class="keyword">auto</span> start = out.<span class="built_in">tableBegin</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(isBinaryOut)</span> </span>&#123;</span><br><span class="line">    refl::Helper::<span class="built_in">iterate</span>&lt;T, <span class="literal">true</span>&gt;([&amp;](<span class="keyword">auto</span> type) &#123; <span class="built_in">serialize</span>(o.*type.getter, out); &#125;,</span><br><span class="line">                                   [&amp;] &#123; out.<span class="built_in">tableEnd</span>(start), start = out.<span class="built_in">tableBegin</span>(<span class="literal">false</span>); &#125;);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    refl::Helper::<span class="built_in">iterate</span>&lt;T&gt;([&amp;](<span class="keyword">auto</span> type) &#123; out.<span class="built_in">key</span>(type.name), <span class="built_in">serialize</span>(o.*type.getter, out); &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  out.<span class="built_in">tableEnd</span>(start);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用了 <code>Helper::iterate</code> 去遍历每个 <code>FieldInfo</code> 然后调用序列化函数，递归地再次去匹配序列化函数。倒是还挺好理解，就是实现上其实也是使用了大量模板技巧，这里不贴代码了。</p><p>对于需要序列化&#x2F;反序列化的类型，需要使用模板特化的方式，定义带有序列化&#x2F;反序列化的工具类，例如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">SerdeMethod</span> &#123;</span><br><span class="line">  <span class="comment">// 真正的序列化反序列化</span></span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">serialize</span><span class="params">(<span class="type">const</span> T &amp;o, <span class="keyword">auto</span> &amp;out)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">serializeReadable</span><span class="params">(<span class="type">const</span> T &amp;o, <span class="keyword">auto</span> &amp;out)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">deserialize</span><span class="params">(T &amp;o, <span class="keyword">auto</span> &amp;in)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> Result&lt;T&gt; <span class="title">deserializeReadable</span><span class="params">(T &amp;o, <span class="keyword">auto</span> &amp;out)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 把类型序列化到别的中间类型</span></span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">serdeTo</span><span class="params">(<span class="type">const</span> T &amp;t)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">serdeToReadable</span><span class="params">(<span class="type">const</span> T &amp;o)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> Result&lt;T&gt; <span class="title">serdeFrom</span><span class="params">(<span class="keyword">auto</span>)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">  <span class="function"><span class="type">static</span> Result&lt;T&gt; <span class="title">serdeFromReadable</span><span class="params">(<span class="keyword">auto</span>)</span> </span>= <span class="keyword">delete</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;&gt;</span><br><span class="line"><span class="keyword">struct</span> ::hf3fs::serde::SerdeMethod&lt;::hf3fs::storage::ChunkId&gt; &#123;</span><br><span class="line">  <span class="function"><span class="type">static</span> std::string_view <span class="title">serdeTo</span><span class="params">(<span class="type">const</span> storage::ChunkId &amp;chunkId)</span> </span>&#123; <span class="keyword">return</span> chunkId.<span class="built_in">data</span>(); &#125;</span><br><span class="line">  <span class="function"><span class="type">static</span> Result&lt;storage::ChunkId&gt; <span class="title">serdeFrom</span><span class="params">(std::string_view str)</span> </span>&#123; <span class="keyword">return</span> storage::<span class="built_in">ChunkId</span>(str); &#125;</span><br><span class="line">  <span class="function"><span class="type">static</span> std::string <span class="title">serdeToReadable</span><span class="params">(<span class="type">const</span> storage::ChunkId &amp;chunkId)</span> </span>&#123; <span class="keyword">return</span> chunkId.<span class="built_in">describe</span>(); &#125;;</span><br><span class="line">  <span class="function"><span class="type">static</span> Result&lt;storage::ChunkId&gt; <span class="title">serdeFromReadable</span><span class="params">(std::string_view s)</span> </span>&#123; <span class="keyword">return</span> storage::ChunkId::<span class="built_in">fromString</span>(s); &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样在被遍历到的时候就能自动调用特化后的模板类去序列化反序列化了。</p><p>总之大概是这么回事，喜欢研究 C++ 模板的读者可以下载代码细细品读后现代 C++ 代码。</p><p>最终，在需要将请求发送到网络的时候，会调用下面的代码序列化到二进制 Buffer（<code>src/common/net/WriteItem.h</code>）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SerdeBuffer</span> &#123;</span><br><span class="line">  <span class="keyword">template</span> &lt;serde::SerdeType T, <span class="keyword">class</span> <span class="title class_">Allocator</span> = net::Allocator&lt;<span class="number">4</span>_KB, <span class="number">1024</span>, <span class="number">64</span> * <span class="number">1024</span>&gt;&gt;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">auto</span> <span class="built_in">create</span>(<span class="type">const</span> T &amp;packet, <span class="type">const</span> CoreRequestOptions &amp;options) &#123;</span><br><span class="line">    serde::Out&lt;DownwardBytes&lt;Allocator&gt;&gt; out;</span><br><span class="line">    <span class="comment">// 这里就将类型序列化到 Buffer 里了</span></span><br><span class="line">    serde::<span class="built_in">serialize</span>(packet, out);</span><br><span class="line">    MessageHeader header;</span><br><span class="line">    header.size = out.<span class="built_in">bytes</span>().<span class="built_in">size</span>();</span><br><span class="line">    out.<span class="built_in">bytes</span>().<span class="built_in">append</span>(&amp;header, <span class="built_in">sizeof</span>(header));</span><br><span class="line">    out.<span class="built_in">bytes</span>().<span class="built_in">reserve</span>(header.size + kMessageHeaderSize + <span class="built_in">sizeof</span>(SerdeBuffer));  <span class="comment">// for headroom.</span></span><br><span class="line"></span><br><span class="line">    <span class="type">uint32_t</span> offset;</span><br><span class="line">    <span class="type">uint32_t</span> capacity;</span><br><span class="line">    std::unique_ptr&lt;SerdeBuffer, Deleter&lt;Allocator&gt;&gt; ptr&#123;</span><br><span class="line">        <span class="built_in">reinterpret_cast</span>&lt;SerdeBuffer *&gt;(out.<span class="built_in">bytes</span>().<span class="built_in">release</span>(offset, capacity))&#125;;</span><br><span class="line">    ptr-&gt;headroom_ = offset;  <span class="comment">// size of headroom.</span></span><br><span class="line">    ptr-&gt;capacity_ = capacity;</span><br><span class="line">    <span class="comment">// ... </span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>调用的逻辑是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;IsBinaryOut T&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Out</span>&lt;T&gt; &#123;</span><br><span class="line">  <span class="comment">// 最终就是将 value 不断拷贝到 buffer 里，并处理嵌套 table</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>反序列化也是差不多的逻辑，调用起点是 <code>deserialize</code> 函数，从 <code>In&lt;T&gt;</code> 读 Buffer。反序列化还稍微复杂一些，不过分析的方法大同小异。</p><h2 id="RPC-服务"><a href="#RPC-服务" class="headerlink" title="RPC 服务"></a>RPC 服务</h2><h3 id="RPC-服务定义"><a href="#RPC-服务定义" class="headerlink" title="RPC 服务定义"></a>RPC 服务定义</h3><p><code>src/common/serde/Service.h</code> 提供了宏 <code>SERDE_SERVICE</code> 定义一个 RPC 服务，注册其名字和服务 ID（全局唯一），并提供宏 <code>SERDE_SERVICE_METHOD</code> 定义 RPC 服务方法，注册其名字和出参入参，以及方法 ID（服务内唯一）。客户端和服务器都要包含这个宏声明的服务以及其接口，从而能正确地调用 RPC 方法。</p><p><code>SERDE_SERVICE_METHOD</code> 首先是定义了调用的 Stub，然后还是和结构体序列化采用了类似的模板技巧，通过不断重载 <code>CollectField</code> 不停将方法元信息添加到 <code>tuple</code> 里。这里的 <code>FieldInfo</code> 会实例化为 <code>MethodInfo</code>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> SERDE_SERVICE_METHOD_REFL(NAME, ID, REQ, RSP)                                                 </span></span><br><span class="line"> <span class="keyword">private</span>:                                                                                             \</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">MethodId</span>##ID : std::type_identity&lt;REFL_NOW&gt; &#123;&#125;;                                              \</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">CollectField</span><span class="params">(::hf3fs::refl::Rank&lt;std::tuple_size_v&lt;<span class="keyword">typename</span> MethodId##ID::type&gt; + <span class="number">1</span>&gt;)</span> </span>&#123; \</span><br><span class="line">    <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(std::is_void_v&lt;T&gt;)</span> </span>&#123;                                                                \</span><br><span class="line">      <span class="keyword">return</span> ::hf3fs::refl::Append_t&lt;<span class="keyword">typename</span> MethodId##ID::type,                                     \</span><br><span class="line">                                     ::hf3fs::serde::MethodInfo&lt;#NAME, T, REQ, RSP, ID, <span class="literal">nullptr</span>&gt;&gt;&#123;&#125;;  \</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;                                                                                          \</span><br><span class="line">      <span class="keyword">return</span> ::hf3fs::refl::Append_t&lt;<span class="keyword">typename</span> MethodId##ID::type,                                     \</span><br><span class="line">                                     ::hf3fs::serde::MethodInfo&lt;#NAME, T, REQ, RSP, ID, &amp;T::NAME&gt;&gt;&#123;&#125;; \</span><br><span class="line">    &#125;                                                                                                 \</span><br><span class="line">  &#125;                                                                                                   \</span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper</span><br><span class="line"> <span class="keyword">private</span>:                                                                                             \</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">MethodId</span>##ID : std::type_identity&lt;REFL_NOW&gt; &#123;&#125;;                                              \</span><br><span class="line">  <span class="function"><span class="type">static</span> <span class="keyword">auto</span> <span class="title">CollectField</span><span class="params">(::hf3fs::refl::Rank&lt;std::tuple_size_v&lt;<span class="keyword">typename</span> MethodId##ID::type&gt; + <span class="number">1</span>&gt;)</span> </span>&#123; \</span><br><span class="line">    <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(std::is_void_v&lt;T&gt;)</span> </span>&#123;                                                                \</span><br><span class="line">      <span class="keyword">return</span> ::hf3fs::refl::Append_t&lt;<span class="keyword">typename</span> MethodId##ID::type,                                     \</span><br><span class="line">                                     ::hf3fs::serde::MethodInfo&lt;#NAME, T, REQ, RSP, ID, <span class="literal">nullptr</span>&gt;&gt;&#123;&#125;;  \</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;                                                                                          \</span><br><span class="line">      <span class="keyword">return</span> ::hf3fs::refl::Append_t&lt;<span class="keyword">typename</span> MethodId##ID::type,                                     \</span><br><span class="line">                                     ::hf3fs::serde::MethodInfo&lt;#NAME, T, REQ, RSP, ID, &amp;T::NAME&gt;&gt;&#123;&#125;; \</span><br><span class="line">    &#125;                                                                                                 \</span><br><span class="line">  &#125;                                                                                                   \</span><br><span class="line">  <span class="keyword">friend</span> <span class="keyword">struct</span> ::hf3fs::refl::Helper</span><br></pre></td></tr></table></figure><h3 id="RPC-网络实现"><a href="#RPC-网络实现" class="headerlink" title="RPC 网络实现"></a>RPC 网络实现</h3><p>RPC 请求通过 <code>Transport</code> 接收和发送，<code>ServiceGroup</code> 起到一个注册服务的作用，最终都是由 <code>Processor</code> 处理。<code>CallContext</code> 是服务端保存一次请求上下文的类，客户端侧是 <code>ClientCallContext</code>。</p><h4 id="RPC-服务端"><a href="#RPC-服务端" class="headerlink" title="RPC 服务端"></a>RPC 服务端</h4><p>服务器收请求的逻辑位于 <code>Transport::handleEvents</code>，最终会一路调用 <code>Transport::doRead</code> 到 <code>Socket::recv</code>，读取收到的缓冲区，解析 RPC 请求，然后包装成 <code>MessageWrapper</code> 发送到 <code>ioWorker_.processMsg</code>，然后调用 <code>processor_.processMsg</code> 进入真正的 Dispatch 和处理 <code>MessagePacket</code> 逻辑。<code>MessagePacket</code> 是通用的 RPC 请求包，包含请求 ID、服务 ID、方法 ID、PayLoad 等通用字段。</p><p>RPC 服务实现方，需要包含用 <code>SERDE_SERVICE</code> 声明的 RPC 服务接口。继承 <code>serde::ServiceWrapper</code> 类后，实现名字一样的 RPC 处理逻辑即可，不需要开发者自己处理网络请求解析了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">StorageService</span> : <span class="keyword">public</span> serde::ServiceWrapper&lt;StorageService, storage::StorageSerde&gt; &#123;</span><br><span class="line">   <span class="function">CoTryTask&lt;BatchReadRsp&gt; <span class="title">batchRead</span><span class="params">(serde::CallContext &amp;ctx, <span class="type">const</span> BatchReadReq &amp;req)</span></span>;</span><br><span class="line">   <span class="function">CoTryTask&lt;WriteRsp&gt; <span class="title">write</span><span class="params">(serde::CallContext &amp;ctx, <span class="type">const</span> BatchReadReq &amp;req)</span> </span></span><br><span class="line"><span class="function">&#125;</span>;</span><br></pre></td></tr></table></figure><p>最终在 Server 初始化时，会调用到 <code>addService</code> 注册服务：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">Service</span>&gt;</span><br><span class="line">  <span class="function">Result&lt;Void&gt; <span class="title">addService</span><span class="params">(std::unique_ptr&lt;Service&gt; &amp;&amp;obj, <span class="type">bool</span> isRDMA)</span> </span>&#123;</span><br><span class="line">    std::shared_ptr&lt;Service&gt; shared = std::<span class="built_in">move</span>(obj);</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> i = <span class="number">0u</span>; i &lt;= <span class="built_in">uint32_t</span>(isRDMA); ++i) &#123;</span><br><span class="line">      <span class="keyword">auto</span> &amp;service = services_[i][Service::kServiceID];</span><br><span class="line">      <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(service.object != <span class="literal">nullptr</span>)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">makeError</span>(StatusCode::kInvalidArg, fmt::format(<span class="string">&quot;redundant service id: &#123;&#125;&quot;</span>, Service::kServiceID));</span><br><span class="line">      &#125;</span><br><span class="line">      service.getter = &amp;MethodExtractor&lt;Service, CallContext, &amp;CallContext::invalidId&gt;::get;</span><br><span class="line">      service.object = shared.<span class="built_in">get</span>();</span><br><span class="line">      service.alive = std::<span class="built_in">shared_ptr</span>&lt;<span class="type">void</span> *&gt;(shared, <span class="literal">nullptr</span>);</span><br><span class="line">      <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(<span class="keyword">requires</span> &#123; Service&#123;&#125;.onError(Status::OK); &#125;)</span> </span>&#123;</span><br><span class="line">        service.onError = &amp;CallContext::customOnError&lt;Service, &amp;Service::onError&gt;;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>其中：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">service.getter = &amp;MethodExtractor&lt;Service, CallContext, &amp;CallContext::invalidId&gt;::get</span><br></pre></td></tr></table></figure><p>是根据方法 ID 分派 RPC 服务执行逻辑的关键。负责解析并处理请求的类是 <code>src/common/serde/Processor.h</code> 中的类：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Processor</span> &#123;</span><br><span class="line">  <span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">processSerdeRequest</span><span class="params">(IOBufPtr buf, serde::MessagePacket&lt;&gt; packet, TransportPtr tr)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// decrease the count in any case.</span></span><br><span class="line">    <span class="keyword">auto</span> guard = folly::<span class="built_in">makeGuard</span>([&amp;] &#123; flags_ -= kCountInc; &#125;);</span><br><span class="line"></span><br><span class="line">    (<span class="type">void</span>)buf;  <span class="comment">// keep alive.</span></span><br><span class="line">    <span class="keyword">auto</span> &amp;service = serdeServices_.<span class="built_in">getServiceById</span>(packet.serviceId, tr-&gt;<span class="built_in">isRDMA</span>());</span><br><span class="line">    <span class="function">serde::CallContext <span class="title">ctx</span><span class="params">(packet, std::move(tr), service)</span></span>;</span><br><span class="line">    <span class="keyword">if</span> (packet.<span class="built_in">useCompress</span>()) &#123;</span><br><span class="line">      ctx.<span class="built_in">responseOptions</span>().compression = &#123;config_.<span class="built_in">response_compression_level</span>(),</span><br><span class="line">                                           config_.<span class="built_in">response_compression_threshold</span>()&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">co_await</span> ctx.<span class="built_in">handle</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>CallContext</code> 中的 <code>handle</code> 便会调用 <code>MethodExtractor</code> 根据方法 ID 找到对应的方法，然后调用它：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CallContext</span> &#123;</span><br><span class="line">  <span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">handle</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> method = service_.<span class="built_in">getter</span>(packet_.methodId);</span><br><span class="line">    <span class="built_in">co_await</span> (<span class="keyword">this</span>-&gt;*method)();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">C</span>, <span class="keyword">auto</span> DEFAULT = <span class="literal">nullptr</span>&gt;</span><br><span class="line"><span class="keyword">class</span> MethodExtractor &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="type">static</span> <span class="keyword">auto</span> <span class="built_in">get</span>(<span class="type">uint16_t</span> id) &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> MethodExtractor ins;</span><br><span class="line">    <span class="keyword">return</span> id &lt;= ins.kMaxThreadId ? ins.table[id] : DEFAULT;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span>:</span><br><span class="line">  <span class="function"><span class="keyword">consteval</span> <span class="title">MethodExtractor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">uint16_t</span> i = <span class="number">0</span>; i &lt;= kMaxThreadId; ++i) &#123;</span><br><span class="line">      table[i] = <span class="built_in">calc</span>(i);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="type">size_t</span> I = <span class="number">0</span>&gt;</span><br><span class="line">  <span class="keyword">consteval</span> <span class="keyword">auto</span> <span class="built_in">calc</span>(<span class="type">uint16_t</span> id) &#123;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">constexpr</span> (I == std::tuple_size_v&lt;FieldInfoList&gt;) &#123;</span><br><span class="line">      <span class="keyword">return</span> DEFAULT;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">using</span> FieldInfo = std::<span class="type">tuple_element_t</span>&lt;I, FieldInfoList&gt;;</span><br><span class="line">      <span class="keyword">return</span> FieldInfo::id == id ? &amp;C::<span class="keyword">template</span> call&lt;FieldInfo&gt; : <span class="built_in">calc</span>&lt;I + <span class="number">1</span>&gt;(id);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">  <span class="keyword">using</span> FieldInfoList = refl::Helper::FieldInfoList&lt;T&gt;;</span><br><span class="line">  <span class="keyword">using</span> Method = <span class="keyword">decltype</span>(&amp;C::<span class="keyword">template</span> call&lt;std::<span class="type">tuple_element_t</span>&lt;<span class="number">0</span>, FieldInfoList&gt;&gt;);</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="keyword">auto</span> kMaxThreadId = MaxMethodId&lt;FieldInfoList&gt;;</span><br><span class="line">  std::array&lt;Method, kMaxThreadId + 1&gt; table;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这里 <code>MethodExtractor</code> 找到的其实是对应的 <code>FieldInfo</code> （实际上实例化为 <code>MethodInfo</code>）类型，然后实例化之后展开最终调用的是方法 ID 对应的 <code>&amp;C::template call&lt;MethodInfo&gt;</code> 。需要注意：<code>MethodInfo</code> 是完全在编译时就计算好的，<code>MethodExtractor</code> 也是在编译时就计算好每个方法 ID 对应的 <code>MethodInfo</code> 了，这时候 <code>table</code> 存的已经是函数指针了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;NameWrapper NAME, <span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">REQ</span>, <span class="keyword">class</span> <span class="title class_">RSP</span>, <span class="type">uint16_t</span> ID, <span class="keyword">auto</span> METHOD&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">MethodInfo</span> &#123;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="keyword">auto</span> nameWrapper = NAME;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> std::string_view name = NAME;</span><br><span class="line">  <span class="keyword">using</span> Object = T;</span><br><span class="line">  <span class="keyword">using</span> ReqType = REQ;</span><br><span class="line">  <span class="keyword">using</span> RspType = RSP;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="keyword">auto</span> id = ID;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="keyword">auto</span> method = METHOD;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 虽然叫 call</span></span><br><span class="line"><span class="comment">// 其实是被 RPC call 的一方会调用这个函数</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">F</span>&gt;</span><br><span class="line">  <span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">call</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// deserialize payload.</span></span><br><span class="line">    <span class="keyword">if</span> (packet_.timestamp) &#123;</span><br><span class="line">      packet_.timestamp-&gt;serverWaked = UtcClock::<span class="built_in">now</span>().<span class="built_in">time_since_epoch</span>().<span class="built_in">count</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">typename</span> F::ReqType req;</span><br><span class="line">    <span class="comment">// 反序列化</span></span><br><span class="line">    <span class="keyword">auto</span> deserializeResult = serde::<span class="built_in">deserialize</span>(req, packet_.payload);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(!deserializeResult)) &#123;</span><br><span class="line">      <span class="built_in">onDeserializeFailed</span>();</span><br><span class="line">      <span class="keyword">co_return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// call method.</span></span><br><span class="line">    <span class="comment">// 调用方法 ID 对应的服务实现</span></span><br><span class="line">    <span class="keyword">auto</span> obj = <span class="built_in">reinterpret_cast</span>&lt;<span class="keyword">typename</span> F::Object *&gt;(service_.object);</span><br><span class="line">    <span class="keyword">auto</span> result = <span class="keyword">co_await</span> folly::coro::<span class="built_in">co_awaitTry</span>((obj-&gt;*F::method)(*<span class="keyword">this</span>, req));</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(result.<span class="built_in">hasException</span>())) &#123;</span><br><span class="line">      <span class="built_in">XLOGF</span>(FATAL,</span><br><span class="line">            <span class="string">&quot;Processor has exception: &#123;&#125;, request &#123;&#125;:&#123;&#125; &#123;&#125;&quot;</span>,</span><br><span class="line">            result.<span class="built_in">exception</span>().<span class="built_in">what</span>(),</span><br><span class="line">            packet_.serviceId,</span><br><span class="line">            packet_.methodId,</span><br><span class="line">            serde::<span class="built_in">toJsonString</span>(req));</span><br><span class="line">      <span class="keyword">co_return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (packet_.timestamp) &#123;</span><br><span class="line">      packet_.timestamp-&gt;serverProcessed = UtcClock::<span class="built_in">now</span>().<span class="built_in">time_since_epoch</span>().<span class="built_in">count</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 返回结果</span></span><br><span class="line">    <span class="comment">// 调用 Transport::send</span></span><br><span class="line">    <span class="built_in">makeResponse</span>(result.<span class="built_in">value</span>());</span><br><span class="line">    <span class="keyword">co_return</span>;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>服务器的逻辑大致就是这样。</p><h4 id="RPC-客户端"><a href="#RPC-客户端" class="headerlink" title="RPC 客户端"></a>RPC 客户端</h4><p>客户端的 Stub 定义通过 <code>SERDE_SERVICE_METHOD_SENDER</code> 定义，就是把模板参数和函数名简单填进宏里调用 <code>ClientCallContext</code> 的 <code>call</code> 方法。</p><p>客户端逻辑主要是 <code>ClientCallContext</code> 实现，模板没有服务端那么套娃，直接就能看出调用的是这段代码，模板参数意义也很明确了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;NameWrapper kServiceName,</span><br><span class="line">            NameWrapper kMethodName,</span><br><span class="line">            <span class="keyword">class</span> <span class="title class_">Req</span>,</span><br><span class="line">            <span class="keyword">class</span> <span class="title class_">Rsp</span>,</span><br><span class="line">            <span class="type">uint16_t</span> ServiceID,</span><br><span class="line">            <span class="type">uint16_t</span> MethodID&gt;</span><br><span class="line">  <span class="function">CoTryTask&lt;Rsp&gt; <span class="title">call</span><span class="params">(<span class="type">const</span> Req &amp;req,</span></span></span><br><span class="line"><span class="params"><span class="function">                      <span class="type">const</span> net::UserRequestOptions *customOptions = <span class="literal">nullptr</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">                      Timestamp *timestamp = <span class="literal">nullptr</span>)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> options = *options_.<span class="built_in">load</span>(std::memory_order_acquire);</span><br><span class="line">    <span class="keyword">if</span> (customOptions != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">      options.<span class="built_in">merge</span>(*customOptions);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    net::Waiter::Item item;</span><br><span class="line">    <span class="type">uint64_t</span> uuid = net::Waiter::<span class="built_in">instance</span>().<span class="built_in">bind</span>(item);</span><br><span class="line"></span><br><span class="line">    Timestamp ts;</span><br><span class="line">    <span class="keyword">if</span> ((options.logLongRunningThreshold != <span class="number">0</span>_ms || options.reportMetrics) &amp;&amp; timestamp == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">      timestamp = &amp;ts;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    <span class="function">MessagePacket <span class="title">packet</span><span class="params">(req)</span></span>;</span><br><span class="line">    packet.uuid = uuid;</span><br><span class="line">    packet.serviceId = ServiceID;</span><br><span class="line">    packet.methodId = MethodID;</span><br><span class="line">    packet.flags = EssentialFlags::IsReq;</span><br><span class="line">    <span class="keyword">if</span> (options.compression) &#123;</span><br><span class="line">      packet.flags |= EssentialFlags::UseCompress;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (options.enableRDMAControl) &#123;</span><br><span class="line">      packet.flags |= EssentialFlags::ControlRDMA;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (timestamp != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">      packet.timestamp = Timestamp&#123;UtcClock::<span class="built_in">now</span>().<span class="built_in">time_since_epoch</span>().<span class="built_in">count</span>()&#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 序列化</span></span><br><span class="line">    <span class="keyword">auto</span> writeItem = net::WriteItem::<span class="built_in">createMessage</span>(packet, options);</span><br><span class="line">    writeItem-&gt;uuid = uuid;</span><br><span class="line">    <span class="keyword">auto</span> requestLength = writeItem-&gt;buf-&gt;<span class="built_in">length</span>();</span><br><span class="line">    <span class="comment">// 发出去</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(std::<span class="built_in">holds_alternative</span>&lt;net::IOWorker *&gt;(connectionSource_))) &#123;</span><br><span class="line">      std::<span class="built_in">get</span>&lt;net::IOWorker *&gt;(connectionSource_)-&gt;<span class="built_in">sendAsync</span>(destAddr_, net::<span class="built_in">WriteList</span>(std::<span class="built_in">move</span>(writeItem)));</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (std::<span class="built_in">holds_alternative</span>&lt;net::Transport *&gt;(connectionSource_)) &#123;</span><br><span class="line">      std::<span class="built_in">get</span>&lt;net::Transport *&gt;(connectionSource_)-&gt;<span class="built_in">send</span>(net::<span class="built_in">WriteList</span>(std::<span class="built_in">move</span>(writeItem)));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">co_return</span> <span class="built_in">MAKE_ERROR_F</span>(StatusCode::kFoundBug,</span><br><span class="line">                             <span class="string">&quot;Sync client call async method: service id &#123;&#125;, method id &#123;&#125;&quot;</span>,</span><br><span class="line">                             ServiceID,</span><br><span class="line">                             MethodID);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 看到我们的老朋友 Baton 了吗</span></span><br><span class="line">    net::Waiter::<span class="built_in">instance</span>().<span class="built_in">schedule</span>(uuid, options.timeout);</span><br><span class="line">    <span class="keyword">co_await</span> item.baton;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(!item.status)) &#123;</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Result&lt;Rsp&gt; rsp = <span class="built_in">makeError</span>(StatusCode::kUnknown);</span><br><span class="line">    <span class="keyword">auto</span> deserializeResult = serde::<span class="built_in">deserialize</span>(rsp, item.packet.payload);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(!deserializeResult)) &#123;</span><br><span class="line">      <span class="built_in">XLOGF</span>(ERR, <span class="string">&quot;deserialize rsp error: &#123;&#125;&quot;</span>, deserializeResult.<span class="built_in">error</span>());</span><br><span class="line">      <span class="keyword">if</span> (item.transport) &#123;</span><br><span class="line">        item.transport-&gt;<span class="built_in">invalidate</span>();</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="function"><span class="keyword">co_return</span> <span class="title">makeError</span><span class="params">(std::move(deserializeResult.error()))</span></span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">co_return</span> rsp;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>客户端先生成一个请求 UUID（64 位整数，方便好使不占地，也有去重的作用），然后根据要调用的方法，序列化出一个请求，发出去。这时候可不能占着线程不放，得通过 <code>co_await</code> 切出去。收到服务器响应后，<code>Waiter</code> 根据 UUID 再把对应的协程捞出来，<code>baton.post()</code> 一下继续调度执行，解析响应返回结果。</p><h3 id="动态协程池"><a href="#动态协程池" class="headerlink" title="动态协程池"></a>动态协程池</h3><p>3FS 的 RPC 框架还有一个功能就是可以根据方法分发到不同的协程池去运行逻辑，实现更精确地调度：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> DynamicCoroutinesPool &amp;<span class="title">getCoroutinesPool</span><span class="params">(<span class="type">uint16_t</span> methodId)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(config.<span class="built_in">use_coroutines_pool_read</span>()) &amp;&amp; methodId == StorageSerde&lt;&gt;::batchReadMethodId) &#123;</span><br><span class="line">    <span class="keyword">return</span> readPool;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(config.<span class="built_in">use_coroutines_pool_update</span>()) &amp;&amp;</span><br><span class="line">      (methodId == StorageSerde&lt;&gt;::writeMethodId || methodId == StorageSerde&lt;&gt;::updateMethodId)) &#123;</span><br><span class="line">    <span class="keyword">return</span> updatePool;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (methodId == StorageSerde&lt;&gt;::syncStartMethodId || methodId == StorageSerde&lt;&gt;::getAllChunkMetadataMethodId) &#123;</span><br><span class="line">    <span class="keyword">return</span> syncPool;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> defaultPool;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="BatchRead-请求调用分析"><a href="#BatchRead-请求调用分析" class="headerlink" title="BatchRead 请求调用分析"></a>BatchRead 请求调用分析</h2><p>以 BatchRead 为例，看 RPC 和 RDMA 单边怎么结合的：</p><ol><li>客户端发送 BatchRead 请求，指定一批要读的 Chunk 数据偏移长度请求列表，以及自己准备好的一堆接收区 Buffer</li><li>服务器收到请求，解包处理</li><li>服务器分配相应数量的 Local RDMA Buffer，调用 AIO 批量读到对应的 Local RDMA Buffer</li><li>如果客户端要求限流，服务器反向发起 RPC 请求客户端的 RDMAControl 服务，客户端根据自身负载在 RPC 处理逻辑中 wait 一段时间</li><li>限流放行返回 RPC 响应后，服务器批量 RDMA Write 到客户端的缓冲区列表对应的 Buffer 中</li><li>服务器 RDMA Write 成功后，返回 RPC 成功</li><li>客户端收到 RPC 响应，知道缓冲区数据已经就绪，美美处理缓冲区中的数据</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>3FS 的 RPC 框架由于大量使用了模板技巧和宏导致代码极其抽象后现代，可阅读性极差，编译时间和内存爆炸。不过通过人肉展开宏和模板递归后还是能找到一些蛛丝马迹的。总的来说就是用 C++ 宏和模板实现了一套带 RPC 功能的 Protobuf。如果你也想在代码里炫技一下反射或者类似的功能，这个代码还是值得学习一下的，就是后面读的人多少得痛苦一下。</p><p>读的我很心累，不想多写什么评价了。不知道 C++ 26 的反射能不能帮助简化下代码。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>DeepSeek 3FS 源码解读——磁盘 IO 篇</title>
      <link>https://blog.howardlau.me/programming/deepseek-3fs-code-reading-disk-io.html</link>
      <description>
        <![CDATA[<p>请先阅读：<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-coroutine.html">DeepSeek 3FS]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sun, 02 Mar 2025 09:21:10 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>请先阅读：<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-coroutine.html">DeepSeek 3FS 源码解读——协程篇</a></p><p>书接上回，作为一个文件系统，磁盘 IO 也是非常重要的一环。在大致浏览了源码后，个人发现 3FS 中，发起磁盘 IO 的可能是前台服务客户端读写的线程、负责链式复制的线程、也有可能是后台的垃圾回收线程。同时，和网络 IO 充分协程化不太一样，磁盘 IO 基本是由线程池来完成同步 IO 操作，而且读 Chunk 实际数据使用了 AIO 来异步读取，但是写数据则调用了同步 IO。这也就意味着 3FS 没有采用目前高性能存储系统使用的 Run To Completion 模型。因此，我们需要分析一下，磁盘的 IO 是怎样调度，又是怎样将同步 IO 或者 AIO 和网络协程结合起来的，以及协程调度过程中线程切换点。</p><h2 id="存储引擎"><a href="#存储引擎" class="headerlink" title="存储引擎"></a>存储引擎</h2><p>代码中一个比较吸引眼球的文件夹便是 <code>src/storage/chunk_engine</code> 中用 Rust 编写的 Chunk 存储引擎，简单地搜索后发现其中并没有包含 <code>async</code> 关键词，因此基本可以确定这部分的代码没有使用异步操作。另外，<code>StorageTarget</code> 中包含了这么一段代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">bool</span> <span class="title">useChunkEngine</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> targetConfig_.only_chunk_engine; &#125;</span><br></pre></td></tr></table></figure><p>简单搜索此函数的调用方，可以发现，实际上 3FS 实现了两套 Chunk 存储引擎，一套是 Rust、另一套是 C++ 中的 <code>ChunkStore</code>，并且 C++ 中的 Chunk 存储引擎，实现同样是基于同步阻塞的 IO 操作。</p><p>熟悉异步编程的开发者知道，在异步的协程中，是不可以调用同步阻塞的系统调用的，否则会造成协程所属的线程被阻塞，从而阻塞其他所有该线程上的协程。</p><p>那么，问题就变成了：3FS 在处理网络请求的协程中，是怎么做到对磁盘发起同步 IO 操作的？粗略分析后，个人认为 3FS 在写场景完全用了同步 IO；而读场景，Chunk 引擎同步读 Chunk 元数据后（在 <code>aioPrepareRead</code> 函数中），通过 <code>AioReadJob</code> 发起实际的异步读取 IO。</p><p>在 Rust 的设计文档中可以看到，在 Chunk 引擎中需要有两个后台线程来完成 Chunk 的一些后台管理：一个是 <code>allocate_thread</code> 负责预分配一些 <code>Group</code> 到内存中加速分配；而 <code>compact_thread</code> 负责整理离散的 Chunk 或者垃圾回收，尽可能将一些未利用满的 <code>Group</code> 中的数据重新整理，释放出 <code>active_group</code> 加速分配。对于这种多线程并发操作，利用 Rust 能很好地在编译期就检查出数据竞争问题，保证 Chunk 分配数据在内存中是正确的。</p><h3 id="写路径"><a href="#写路径" class="headerlink" title="写路径"></a>写路径</h3><p>继续分析下写路径，可以发现写路径的起点是 <code>StorageOperator::doUpdate</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTask&lt;IOResult&gt; <span class="title">StorageOperator::doUpdate</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    UpdateJob job;</span><br><span class="line">    <span class="keyword">co_await</span> updateWorker_.<span class="built_in">enqueue</span>(&amp;job);</span><br><span class="line">    <span class="keyword">co_await</span> job.<span class="built_in">complete</span>();</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">UpdateJob</span> &#123;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">setResult</span><span class="params">(Result&lt;<span class="type">uint32_t</span>&gt; result)</span> </span>&#123;</span><br><span class="line">    result_.lengthInfo = std::<span class="built_in">move</span>(result);</span><br><span class="line">    baton_.<span class="built_in">post</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">complete</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">co_await</span> baton_; &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这几行就是打通网络协程和磁盘 IO 的关键代码了，这里和网络 IO 一样，还是用了 <code>Baton</code> 类来挂起和唤醒协程。下面看看 <code>UpdateWorker</code> 的相关实现代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">enqueue</span><span class="params">(UpdateJob *job)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">assert</span>(job-&gt;<span class="built_in">target</span>()-&gt;<span class="built_in">diskIndex</span>() &lt; queueVec_.<span class="built_in">size</span>());</span><br><span class="line">    <span class="keyword">co_await</span> queueVec_[job-&gt;<span class="built_in">target</span>()-&gt;<span class="built_in">diskIndex</span>()]-&gt;<span class="built_in">co_enqueue</span>(job);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现就是将 IO 任务提交到了磁盘对应的处理队列中：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> U = T&gt;</span><br><span class="line">folly::coro::Task&lt;<span class="type">void</span>&gt; <span class="built_in">co_enqueue</span>(U &amp;&amp;item) &#123;</span><br><span class="line">    <span class="keyword">co_await</span> enqueueSemaphore_.<span class="built_in">co_wait</span>();</span><br><span class="line">    queue_.<span class="built_in">writeIfNotFull</span>(std::forward&lt;U&gt;(item));</span><br><span class="line">    dequeueSemaphore_.<span class="built_in">signal</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一个比较经典的有界阻塞队列实现，这里的队列 <code>queue_</code> 是一个 <code>MPMC</code> 多生产者多消费者有锁队列。因为已经通过信号量控制入队数量了，理论上这里不会发生阻塞。</p><p>在 <code>UpdateWorker</code> 中，启动了两个线程池用来处理 IO 任务，前台线程池用来处理任务队列，将其提交每个磁盘对应的后台 IO 线程：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">UpdateWorker::start</span><span class="params">(<span class="type">uint32_t</span> numberOfDisks)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (config_.<span class="built_in">num_threads</span>() &lt; numberOfDisks) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">makeError</span>(StatusCode::kInvalidConfig,</span><br><span class="line">                     fmt::format(<span class="string">&quot;too few update worker threads, &#123;&#125; &lt; &#123;&#125;&quot;</span>, config_.<span class="built_in">num_threads</span>(), numberOfDisks));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  queueVec_.<span class="built_in">reserve</span>(numberOfDisks);</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> i = <span class="number">0u</span>; i &lt; numberOfDisks; ++i) &#123;</span><br><span class="line">    queueVec_.<span class="built_in">emplace_back</span>(std::<span class="built_in">make_unique</span>&lt;Queue&gt;(config_.<span class="built_in">queue_size</span>()));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> i = <span class="number">0u</span>; i &lt; config_.<span class="built_in">num_threads</span>(); ++i) &#123;</span><br><span class="line">    executors_.<span class="built_in">add</span>([<span class="keyword">this</span>, i] &#123; <span class="built_in">run</span>(*queueVec_[i % queueVec_.<span class="built_in">size</span>()]); &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">UpdateWorker::run</span><span class="params">(Queue &amp;queue)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="keyword">auto</span> job = queue.<span class="built_in">dequeue</span>();</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(job == <span class="literal">nullptr</span>)) &#123;</span><br><span class="line">      <span class="built_in">XLOGF</span>(DBG, <span class="string">&quot;Storage worker &#123;&#125; stop...&quot;</span>, fmt::<span class="built_in">ptr</span>(<span class="keyword">this</span>));</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    job-&gt;<span class="built_in">target</span>()-&gt;<span class="built_in">updateChunk</span>(*job, bgExecutors_);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>updateChunk</code> 的实现中可以发现：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">StorageTarget::updateChunk</span><span class="params">(UpdateJob &amp;job, folly::CPUThreadPoolExecutor &amp;executor)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (job.<span class="built_in">type</span>() == UpdateType::COMMIT) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">useChunkEngine</span>()) &#123;</span><br><span class="line">      job.<span class="built_in">setResult</span>(ChunkEngine::<span class="built_in">commit</span>(*engine_, job, config_.<span class="built_in">kv_store</span>().<span class="built_in">sync_when_write</span>()));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      job.<span class="built_in">setResult</span>(ChunkReplica::<span class="built_in">commit</span>(chunkStore_, job));</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">auto</span> result =</span><br><span class="line">        <span class="built_in">useChunkEngine</span>() ? ChunkEngine::<span class="built_in">update</span>(*engine_, job) : ChunkReplica::<span class="built_in">update</span>(chunkStore_, job, executor);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(result.<span class="built_in">hasValue</span>())) &#123;</span><br><span class="line">        <span class="comment">// 一些 metrics 统计...</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="type">uint32_t</span> code = result.<span class="built_in">error</span>().<span class="built_in">code</span>();</span><br><span class="line">      <span class="keyword">switch</span> (code) &#123;</span><br><span class="line">        <span class="comment">// 同样是 metrics 统计...</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 注意这里设置了结果，也就 post 了 Baton，从而唤醒协程</span></span><br><span class="line">    <span class="comment">// 特别注意，post 是在 IO 线程中调用的，最终也就是在 IO 线程</span></span><br><span class="line">    <span class="comment">// 调用了 coroutine 的 resume 函数。</span></span><br><span class="line">    <span class="comment">// 但是 folly 会保证一个 Task 中的 co_await 点都能正确调度回原来的 Executor 执行</span></span><br><span class="line">    <span class="comment">/// Within the body of a Task&#x27;s coroutine, executor binding to the parent</span></span><br><span class="line">    <span class="comment">/// executor is maintained by implicitly transforming all &#x27;co_await expr&#x27;</span></span><br><span class="line">    <span class="comment">/// expressions into `co_await co_viaIfAsync(parentExecutor, expr)&#x27; to ensure</span></span><br><span class="line">    <span class="comment">/// that the coroutine always resumes on the parent&#x27;s executor.</span></span><br><span class="line">    job.<span class="built_in">setResult</span>(std::<span class="built_in">move</span>(result));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就是调用 Chunk 引擎落盘后，设置协程的结果并唤醒协程的逻辑了。</p><p>落盘相关的逻辑：</p><p>Rust（<code>src/storage/chunk_engine/src/alloc/chunk.rs</code>）：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">impl</span> <span class="title class_">Chunk</span> &#123;</span><br><span class="line">    <span class="comment">// 关注这个函数，里面主要是数据的对齐之类的操作，最后调用的是 `pwrite` 调用</span></span><br><span class="line">    <span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">safe_write</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">pwrite</span>(&amp;<span class="keyword">self</span>, pos: Position, <span class="keyword">mut</span> buf: &amp;[<span class="type">u8</span>], offset: <span class="type">u32</span>) <span class="punctuation">-&gt;</span> <span class="type">Result</span>&lt;()&gt; &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">aligned</span> = <span class="title function_ invoke__">is_aligned_io</span>(buf, offset);</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">offset</span> = pos.<span class="title function_ invoke__">offset</span>() + offset;</span><br><span class="line">    <span class="keyword">while</span> !buf.<span class="title function_ invoke__">is_empty</span>() &#123;</span><br><span class="line">        <span class="keyword">let</span> <span class="variable">fd</span> = <span class="keyword">if</span> aligned &amp;&amp; <span class="title function_ invoke__">is_aligned_len</span>(buf.<span class="title function_ invoke__">len</span>() <span class="keyword">as</span> <span class="type">u32</span>) &#123;</span><br><span class="line">            &amp;<span class="keyword">self</span>.direct_fd</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            &amp;<span class="keyword">self</span>.normal_fd</span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">match</span> fd.<span class="title function_ invoke__">write_at</span>(buf, offset.<span class="title function_ invoke__">into</span>()) &#123;</span><br><span class="line">            <span class="title function_ invoke__">Ok</span>(<span class="number">0</span>) =&gt; <span class="keyword">return</span> <span class="title function_ invoke__">Err</span>(Error::<span class="title function_ invoke__">IoError</span>(<span class="built_in">format!</span>(<span class="string">&quot;write &#123;:?&#125; return 0&quot;</span>, fd))),</span><br><span class="line">            <span class="title function_ invoke__">Ok</span>(n) =&gt; &#123;</span><br><span class="line">                buf = &amp;buf[n..];</span><br><span class="line">                offset += n <span class="keyword">as</span> <span class="type">u64</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="title function_ invoke__">Err</span>(e) =&gt; <span class="keyword">Self</span>::<span class="title function_ invoke__">handle_error</span>(e)?,</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="title function_ invoke__">Ok</span>(())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>C++（<code>src/storage/store/ChunkFileView.cc</code>）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Result&lt;<span class="type">uint32_t</span>&gt; <span class="title">ChunkFileView::write</span><span class="params">(<span class="type">const</span> <span class="type">uint8_t</span> *buf, <span class="type">size_t</span> size, <span class="type">size_t</span> offset, <span class="type">const</span> ChunkMetadata &amp;meta)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 同样是对齐操作等</span></span><br><span class="line">  <span class="function">ExponentialBackoffRetry <span class="title">retry</span><span class="params">(<span class="number">100</span>_ms, <span class="number">5</span>_s, <span class="number">30</span>_s)</span></span>;</span><br><span class="line">  <span class="keyword">while</span> (size &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="type">int</span> ret = ::<span class="built_in">pwrite</span>(fd, buf, size, offset);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(ret &gt; <span class="number">0</span>)) &#123;</span><br><span class="line">      w += ret;</span><br><span class="line">      buf += ret;</span><br><span class="line">      size -= ret;</span><br><span class="line">      offset += ret;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">auto</span> msg = fmt::format(<span class="string">&quot;write chunk file failed: fd &#123;&#125;, direct &#123;&#125;, buf &#123;&#125;, offset &#123;&#125;, size &#123;&#125;, ret &#123;&#125;, errno &#123;&#125;&quot;</span>,</span><br><span class="line">                             fd,</span><br><span class="line">                             fd == direct_,</span><br><span class="line">                             fmt::<span class="built_in">ptr</span>(buf),</span><br><span class="line">                             offset,</span><br><span class="line">                             size,</span><br><span class="line">                             ret,</span><br><span class="line">                             errno);</span><br><span class="line">      <span class="built_in">XLOG</span>(ERR, msg);</span><br><span class="line">      <span class="keyword">auto</span> waitTime = retry.<span class="built_in">getWaitTime</span>();</span><br><span class="line">      <span class="keyword">if</span> (waitTime.<span class="built_in">count</span>() == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">makeError</span>(StorageCode::kChunkWriteFailed, std::<span class="built_in">move</span>(msg));</span><br><span class="line">      &#125;</span><br><span class="line">      std::this_thread::<span class="built_in">sleep_for</span>(waitTime);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  recordGuard.<span class="built_in">succ</span>();</span><br><span class="line">  <span class="keyword">return</span> w;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="读路径"><a href="#读路径" class="headerlink" title="读路径"></a>读路径</h3><p>读取路径如上文所述，Chunk 引擎通过同步 IO 获取 Chunk 元数据，转换成需要实际发起 IO 操作的 <code>fd</code> 和偏移以及长度，最后提交到 AIO 或者 io_uring 异步读取：</p><p>具体来说，看代码在网络线程中构造了 <code>BatchReadJob</code>，然后将其通过 MPMC 队列提交到线程池中，关键逻辑已添加注释，请留意阅读：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTryTask&lt;BatchReadRsp&gt; <span class="title">StorageOperator::batchRead</span><span class="params">(ServiceRequestContext &amp;requestCtx,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                   <span class="type">const</span> BatchReadReq &amp;req,</span></span></span><br><span class="line"><span class="params"><span class="function">                                                   serde::CallContext &amp;ctx)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 一些 metrics 统计...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> snapshot = components_.targetMap.<span class="built_in">snapshot</span>();</span><br><span class="line">  <span class="keyword">auto</span> batchSize = req.payloads.<span class="built_in">size</span>();</span><br><span class="line">  <span class="comment">// 准备发起磁盘 BatchRead 工作</span></span><br><span class="line">  BatchReadRsp rsp;</span><br><span class="line">  rsp.results.<span class="built_in">resize</span>(batchSize);</span><br><span class="line">  <span class="function">BatchReadJob <span class="title">batch</span><span class="params">(req.payloads, rsp.results, req.checksumType)</span></span>;</span><br><span class="line">  <span class="comment">// 一些 metrics 统计...</span></span><br><span class="line"></span><br><span class="line">  <span class="type">size_t</span> totalLength = <span class="number">0</span>;</span><br><span class="line">  <span class="type">size_t</span> totalHeadLength = <span class="number">0</span>;</span><br><span class="line">  <span class="type">size_t</span> totalTailLength = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">for</span> (AioReadJobIterator <span class="built_in">it</span>(&amp;batch); it; it++) &#123;</span><br><span class="line">    <span class="comment">// get target for batch read, need check public and local state.</span></span><br><span class="line">    <span class="comment">// 纯内存操作</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 一些 metrics 统计...</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> prepareBufferRecordGuard = storageReadPrepareBuffer.<span class="built_in">record</span>();</span><br><span class="line">  <span class="keyword">auto</span> buffer = components_.rdmabufPool.<span class="built_in">get</span>();</span><br><span class="line">  <span class="keyword">for</span> (AioReadJobIterator <span class="built_in">it</span>(&amp;batch); it; it++) &#123;</span><br><span class="line">    <span class="comment">// 分配一下 RDMA Buffer</span></span><br><span class="line">  &#125;</span><br><span class="line">  prepareBufferRecordGuard.<span class="built_in">report</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">BITFLAGS_CONTAIN</span>(req.featureFlags, FeatureFlags::BYPASS_DISKIO)) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">auto</span> recordGuard = storageAioEnqueueRecoder.<span class="built_in">record</span>();</span><br><span class="line">    <span class="keyword">auto</span> splitSize = config_.<span class="built_in">batch_read_job_split_size</span>();</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">uint32_t</span> start = <span class="number">0</span>; start &lt; batchSize; start += splitSize) &#123;</span><br><span class="line">      <span class="comment">// 提交 ReadJob</span></span><br><span class="line">      <span class="comment">// 同样通过 MPMC 队列提交</span></span><br><span class="line">      <span class="keyword">co_await</span> components_.aioReadWorker.<span class="built_in">enqueue</span>(<span class="built_in">AioReadJobIterator</span>(&amp;batch, start, splitSize));</span><br><span class="line">    &#125;</span><br><span class="line">    recordGuard.<span class="built_in">report</span>(<span class="literal">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 一些 metrics 统计...</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 等待磁盘 BatchRead 完成</span></span><br><span class="line">  <span class="keyword">co_await</span> batch.<span class="built_in">complete</span>();</span><br><span class="line">  <span class="comment">// 执行到此处时，folly 会保证协程切换回 RDMA 线程继续执行</span></span><br><span class="line">  waitAioRecordGuard.<span class="built_in">report</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">BITFLAGS_CONTAIN</span>(req.featureFlags, FeatureFlags::SEND_DATA_INLINE)) &#123;</span><br><span class="line">    batch.<span class="built_in">copyToRespBuffer</span>(rsp.inlinebuf.data);</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!<span class="built_in">BITFLAGS_CONTAIN</span>(req.featureFlags, FeatureFlags::BYPASS_RDMAXMIT)) &#123;</span><br><span class="line">    <span class="keyword">auto</span> ibSocket = ctx.<span class="built_in">transport</span>()-&gt;<span class="built_in">ibSocket</span>();</span><br><span class="line">    <span class="comment">// 准备发起一批 RDMA Write 操作</span></span><br><span class="line">    <span class="keyword">auto</span> waitBatchRecordGuard = storageWaitBatchRecoder.<span class="built_in">record</span>();</span><br><span class="line">    <span class="keyword">auto</span> writeBatch = ctx.<span class="built_in">writeTransmission</span>();</span><br><span class="line">    batch.<span class="built_in">addBufferToBatch</span>(writeBatch);</span><br><span class="line">    waitBatchRecordGuard.<span class="built_in">report</span>(<span class="literal">true</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// RDMA 限流</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="comment">// RDMA 限流</span></span><br><span class="line">    <span class="keyword">if</span> (ctx.<span class="built_in">packet</span>().<span class="built_in">controlRDMA</span>() &amp;&amp; RDMATransmissionReqTimeout != <span class="number">0</span>_ms &amp;&amp; !applyTransmissionBeforeGettingSemaphore) &#123;</span><br><span class="line">      <span class="keyword">co_await</span> writeBatch.<span class="built_in">applyTransmission</span>(RDMATransmissionReqTimeout);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> waitPostRecordGuard = storageWaitPostRecoder.<span class="built_in">record</span>(ibdevTagSet);</span><br><span class="line">    <span class="comment">// 此时在 RDMA 线程执行 RDMA Write</span></span><br><span class="line">    <span class="keyword">auto</span> postResult = <span class="built_in">FAULT_INJECTION_POINT</span>(requestCtx.debugFlags.<span class="built_in">injectServerError</span>(),</span><br><span class="line">                                            <span class="built_in">makeError</span>(RPCCode::kRDMAPostFailed),</span><br><span class="line">                                            (<span class="keyword">co_await</span> writeBatch.<span class="built_in">post</span>()));</span><br><span class="line">    <span class="comment">// RDMA 成功后，还在 RDMA 线程中</span></span><br><span class="line">  &#125;</span><br><span class="line">  waitAioAndPostRecordGuard.<span class="built_in">report</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  recordGuard.<span class="built_in">succ</span>();</span><br><span class="line">  <span class="keyword">co_return</span> rsp;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述代码片段关键逻辑已添加注释，请留意阅读。继续分析下实际的 AIO 逻辑，这里是从队列出取出任务，根据提交的 ReadJob 构造 AIO 调用（或者 io_uring 调用）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">AioStatus::collect</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> recordGuard = ioCollectRecorder.<span class="built_in">record</span>();</span><br><span class="line">  <span class="keyword">while</span> (<span class="built_in">availableToSubmit</span>() &amp;&amp; iterator_) &#123;</span><br><span class="line">    <span class="keyword">auto</span> &amp;job = *iterator_++;</span><br><span class="line">    <span class="keyword">auto</span> result = job.<span class="built_in">state</span>().storageTarget-&gt;<span class="built_in">aioPrepareRead</span>(job);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(!result)) &#123;</span><br><span class="line">      job.<span class="built_in">setResult</span>(<span class="built_in">makeError</span>(std::<span class="built_in">move</span>(result.<span class="built_in">error</span>())));</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ++readyToSubmit_;</span><br><span class="line">    ++inflight_;</span><br><span class="line">    <span class="keyword">auto</span> iocb = availables_.<span class="built_in">back</span>();</span><br><span class="line">    availables_.<span class="built_in">pop_back</span>();</span><br><span class="line">    <span class="keyword">auto</span> &amp;state = job.<span class="built_in">state</span>();</span><br><span class="line">    job.<span class="built_in">resetStartTime</span>();</span><br><span class="line">    ::<span class="built_in">io_prep_pread</span>(iocb, state.readFd, state.localbuf.<span class="built_in">ptr</span>(), state.readLength, state.readOffset);</span><br><span class="line">    iocb-&gt;data = &amp;job;</span><br><span class="line">  &#125;</span><br><span class="line">  recordGuard.<span class="built_in">succ</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">IoUringStatus::collect</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> recordGuard = ioCollectRecorder.<span class="built_in">record</span>();</span><br><span class="line">  <span class="keyword">while</span> (<span class="built_in">availableToSubmit</span>() &amp;&amp; iterator_) &#123;</span><br><span class="line">    <span class="keyword">auto</span> &amp;job = *iterator_++;</span><br><span class="line">    <span class="keyword">auto</span> result = job.<span class="built_in">state</span>().storageTarget-&gt;<span class="built_in">aioPrepareRead</span>(job);</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(!result)) &#123;</span><br><span class="line">      job.<span class="built_in">setResult</span>(<span class="built_in">makeError</span>(std::<span class="built_in">move</span>(result.<span class="built_in">error</span>())));</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ++inflight_;</span><br><span class="line">    <span class="keyword">auto</span> &amp;state = job.<span class="built_in">state</span>();</span><br><span class="line"></span><br><span class="line">    job.<span class="built_in">resetStartTime</span>();</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">io_uring_sqe</span> *sqe = ::<span class="built_in">io_uring_get_sqe</span>(&amp;ring_);</span><br><span class="line">    <span class="built_in">assert</span>(sqe != <span class="literal">nullptr</span>);</span><br><span class="line">    ::<span class="built_in">io_uring_prep_read_fixed</span>(sqe,</span><br><span class="line">                               state.fdIndex.<span class="built_in">value_or</span>(state.readFd),</span><br><span class="line">                               state.localbuf.<span class="built_in">ptr</span>(),</span><br><span class="line">                               state.readLength,</span><br><span class="line">                               state.readOffset,</span><br><span class="line">                               state.bufferIndex);</span><br><span class="line">    <span class="keyword">if</span> (state.fdIndex) &#123;</span><br><span class="line">      sqe-&gt;flags |= IOSQE_FIXED_FILE;</span><br><span class="line">    &#125;</span><br><span class="line">    ::<span class="built_in">io_uring_sqe_set_data</span>(sqe, &amp;job);</span><br><span class="line">    submittingJobs_.<span class="built_in">push_back</span>(&amp;job);</span><br><span class="line">  &#125;</span><br><span class="line">  recordGuard.<span class="built_in">succ</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要特别注意这里的 <code>aioPrepareRead</code> 是同步调用。然后将构造好的请求塞入到系统调用，工作线程池也是很经典的 <code>submit</code> 和 <code>reap</code> 循环：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">AioReadWorker::start</span><span class="params">(<span class="type">const</span> std::vector&lt;<span class="type">int</span>&gt; &amp;fds, <span class="type">const</span> std::vector&lt;<span class="keyword">struct</span> iovec&gt; &amp;iovecs)</span> </span>&#123;</span><br><span class="line">  <span class="type">uint32_t</span> numThreads = config_.<span class="built_in">num_threads</span>();</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> i = <span class="number">0u</span>; i &lt; numThreads; ++i) &#123;</span><br><span class="line">    executors_.<span class="built_in">add</span>([&amp;]() &#123;</span><br><span class="line">      AioStatus aioStatus;</span><br><span class="line">      IoUringStatus ioUringStatus;</span><br><span class="line">      <span class="comment">// 初始化一下 AIO 或者 uring，开跑</span></span><br><span class="line">      <span class="built_in">run</span>(aioStatus, ioUringStatus);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; initialized_ != numThreads; ++i) &#123;</span><br><span class="line">    <span class="built_in">XLOGF_IF</span>(INFO, i % <span class="number">5</span> == <span class="number">0</span>, <span class="string">&quot;Waiting for AioReadWorker@&#123;&#125;::run start...&quot;</span>, fmt::<span class="built_in">ptr</span>(<span class="keyword">this</span>));</span><br><span class="line">    std::this_thread::<span class="built_in">sleep_for</span>(<span class="number">100</span>_ms);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">RETURN_AND_LOG_ON_ERROR</span>(*initResult_.<span class="built_in">lock</span>());</span><br><span class="line">  <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">AioReadWorker::run</span><span class="params">(AioStatus &amp;aioStatus, IoUringStatus &amp;ioUringStatus)</span> </span>&#123;</span><br><span class="line">  aioRunningThreadsCount.<span class="built_in">addSample</span>(<span class="number">1</span>);</span><br><span class="line">  <span class="keyword">auto</span> guard = folly::<span class="built_in">makeGuard</span>([] &#123; aioRunningThreadsCount.<span class="built_in">addSample</span>(<span class="number">-1</span>); &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">    <span class="comment">// 1. try to fetch a batch read job.</span></span><br><span class="line">    aioRunningThreadsCount.<span class="built_in">addSample</span>(<span class="number">-1</span>);</span><br><span class="line">    <span class="keyword">auto</span> it = queue_.<span class="built_in">dequeue</span>();  <span class="comment">// waiting.</span></span><br><span class="line">    aioRunningThreadsCount.<span class="built_in">addSample</span>(<span class="number">1</span>);</span><br><span class="line">    <span class="keyword">if</span> (it.<span class="built_in">isNull</span>()) &#123;</span><br><span class="line">      <span class="built_in">XLOGF</span>(DBG, <span class="string">&quot;Stop AioReadWorker &#123;&#125;...&quot;</span>, fmt::<span class="built_in">ptr</span>(<span class="keyword">this</span>));</span><br><span class="line">      <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    batchReadInQueueRecorder.<span class="built_in">addSample</span>(RelativeTime::<span class="built_in">now</span>() - it.<span class="built_in">startTime</span>());</span><br><span class="line">    it-&gt;<span class="built_in">batch</span>().<span class="built_in">resetStartTime</span>();</span><br><span class="line"></span><br><span class="line">    IoStatus &amp;status = config_.<span class="built_in">useIoUring</span>() ? <span class="built_in">static_cast</span>&lt;IoStatus &amp;&gt;(ioUringStatus) : aioStatus;</span><br><span class="line">    status.<span class="built_in">setAioReadJobIterator</span>(it);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">      <span class="comment">// 2. collect a batch of read jobs.</span></span><br><span class="line">      status.<span class="built_in">collect</span>();</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 3. submit a batch of read jobs.</span></span><br><span class="line">      status.<span class="built_in">submit</span>();</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 4. wait a batch of events.</span></span><br><span class="line">      <span class="keyword">while</span> (status.<span class="built_in">inflight</span>()) &#123;</span><br><span class="line">        status.<span class="built_in">reap</span>(config_.<span class="built_in">min_complete</span>());</span><br><span class="line">      &#125;;</span><br><span class="line">    &#125; <span class="keyword">while</span> (status.<span class="built_in">hasUnfinishedBatchReadJob</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里贴一下 AIO 的 <code>submit</code> 和 <code>reap</code> 代码，uring 也类似的逻辑，换成对应的系统调用就行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">AioStatus::submit</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="type">uint32_t</span> submitStartPoint = availables_.<span class="built_in">size</span>();</span><br><span class="line">  <span class="type">uint32_t</span> loopCnt = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">while</span> (readyToSubmit_) &#123;</span><br><span class="line">    ++loopCnt;</span><br><span class="line">    <span class="keyword">auto</span> recordGuard = ioSubmitRecorder.<span class="built_in">record</span>();</span><br><span class="line">    <span class="type">int</span> ret = ::<span class="built_in">io_submit</span>(aioContext_, readyToSubmit_, &amp;availables_[submitStartPoint]);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 一些错误处理...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">AioStatus::reap</span><span class="params">(<span class="type">uint32_t</span> minCompleteIn)</span> </span>&#123;</span><br><span class="line">  <span class="type">uint32_t</span> minComplete = std::<span class="built_in">min</span>(<span class="built_in">inflight</span>(), minCompleteIn);</span><br><span class="line">  <span class="keyword">auto</span> recordGuard = ioGetEventsRecorder.<span class="built_in">record</span>();</span><br><span class="line">  <span class="type">int</span> ret = ::<span class="built_in">io_getevents</span>(aioContext_, minComplete, <span class="built_in">inflight</span>(), events_.<span class="built_in">data</span>(), <span class="literal">nullptr</span>);</span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">LIKELY</span>(ret &gt;= <span class="number">0</span>)) &#123;</span><br><span class="line">    recordGuard.<span class="built_in">succ</span>();</span><br><span class="line">    ioGetEventsSize.<span class="built_in">addSample</span>(ret);</span><br><span class="line">    inflight_ -= ret;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; ret; ++i) &#123;</span><br><span class="line">      <span class="keyword">auto</span> &amp;event = events_[i];</span><br><span class="line">      availables_.<span class="built_in">push_back</span>(event.obj);</span><br><span class="line">      <span class="built_in">setReadJobResult</span>(event.data, event.res);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (ret == -EINTR) &#123;</span><br><span class="line">    <span class="built_in">XLOGF</span>(INFO, <span class="string">&quot;aio is interrupted by a signal handler&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="built_in">XLOGF</span>(ERR, <span class="string">&quot;aio io_getevents error: &#123;&#125;&quot;</span>, ret);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>setReadJobResult</code> 就是负责设置结果，增加 <code>BatchReadJob</code> 的计数器，做完了就唤醒协程，还是我们的老朋友 <code>Baton</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">AioReadJob::setResult</span><span class="params">(Result&lt;<span class="type">uint32_t</span>&gt; lengthInfo)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// 检查 Checksum，错误处理</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  result_.lengthInfo = std::<span class="built_in">move</span>(lengthInfo);</span><br><span class="line">  state_.chunkEngineJob.<span class="built_in">reset</span>();</span><br><span class="line">  batch_.<span class="built_in">finish</span>(<span class="keyword">this</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">BatchReadJob::finish</span><span class="params">(AioReadJob *job)</span> </span>&#123;</span><br><span class="line">  (<span class="type">void</span>)job;</span><br><span class="line">  <span class="keyword">if</span> (++finishedCount_ == jobs_.<span class="built_in">size</span>()) &#123;</span><br><span class="line">    batchReadLatency.<span class="built_in">addSample</span>(RelativeTime::<span class="built_in">now</span>() - <span class="built_in">startTime</span>());</span><br><span class="line">    baton_.<span class="built_in">post</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="后台-IO-线程"><a href="#后台-IO-线程" class="headerlink" title="后台 IO 线程"></a>后台 IO 线程</h3><p>这块设计文档说的挺明白的了，这里简单贴一下线程在哪启动的，Rust 引擎本身不会启动后台线程，反而是由 C++ 线程来驱动，线程中也全部是同步 IO 操作，感兴趣的读者顺藤摸瓜找一找具体的实现逻辑就好。个人理解这些线程是计算为主。</p><p>对应的类 <code>AllocateWorker</code>，代码位于 <code>src/storage/worker/AllocateWorker.cc</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">AllocateWorker::loop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">while</span> (!stopping_) &#123;</span><br><span class="line">    <span class="keyword">auto</span> lock = std::<span class="built_in">unique_lock</span>(mutex_);</span><br><span class="line">    <span class="keyword">if</span> (cond_.<span class="built_in">wait_for</span>(lock, <span class="number">100</span>_ms, [&amp;] &#123; <span class="keyword">return</span> stopping_.<span class="built_in">load</span>(); &#125;)) &#123;</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> minRemainGroups = config_.<span class="built_in">min_remain_groups</span>();</span><br><span class="line">    <span class="keyword">auto</span> maxRemainGroups = config_.<span class="built_in">max_remain_groups</span>();</span><br><span class="line">    <span class="keyword">auto</span> minRemainUltraGroups = config_.<span class="built_in">min_remain_ultra_groups</span>();</span><br><span class="line">    <span class="keyword">auto</span> maxRemainUltraGroups = config_.<span class="built_in">max_remain_ultra_groups</span>();</span><br><span class="line">    <span class="keyword">auto</span> maxReserved = config_.<span class="built_in">max_reserved_chunks</span>();</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;engine : components_.storageTargets.<span class="built_in">engines</span>()) &#123;</span><br><span class="line">      engine-&gt;<span class="built_in">allocate_groups</span>(minRemainGroups, maxRemainGroups, <span class="number">128</span>);</span><br><span class="line">      engine-&gt;<span class="built_in">allocate_ultra_groups</span>(minRemainUltraGroups, maxRemainUltraGroups, <span class="number">32</span>);</span><br><span class="line">      engine-&gt;<span class="built_in">compact_groups</span>(maxReserved);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">XLOGF</span>(INFO, <span class="string">&quot;AllocateWorker@&#123;&#125;::loop stopped&quot;</span>, fmt::<span class="built_in">ptr</span>(<span class="keyword">this</span>));</span><br><span class="line">  stopped_ = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将 <code>allocate_thread</code> 和 <code>compact_thread</code> 合二为一了。有个比较奇怪的地方是，就算一个 Target 没有用到 Rust 的 Chunk Engine，也会启动 Rust 的引擎，只是对应的目录下应该啥东西都没有，实际运行中也不会调用到 Rust 引擎，而是调用 C++ 的 Chunk 引擎，对应的 Rust 引擎直接空转：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">StorageTargets::init</span><span class="params">(CPUExecutorGroup &amp;executor)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  targetPaths_ = config_.<span class="built_in">target_paths</span>();</span><br><span class="line">std::vector&lt;folly::coro::TaskWithExecutor&lt;Result&lt;rust::Box&lt;chunk_engine::Engine&gt;&gt;&gt;&gt; tasks;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;path : targetPaths_) &#123;</span><br><span class="line">    <span class="keyword">auto</span> engine_path = path / <span class="string">&quot;engine&quot;</span>;</span><br><span class="line">    <span class="type">bool</span> create = !boost::filesystem::<span class="built_in">exists</span>(engine_path);</span><br><span class="line">    create |= config_.<span class="built_in">create_engine_path</span>();</span><br><span class="line">    tasks.<span class="built_in">push_back</span>(folly::coro::<span class="built_in">co_invoke</span>([engine_path, create]() -&gt; CoTryTask&lt;rust::Box&lt;chunk_engine::Engine&gt;&gt; &#123;</span><br><span class="line">                      std::string error;</span><br><span class="line">                      <span class="keyword">auto</span> engine = chunk_engine::<span class="built_in">create</span>(engine_path.<span class="built_in">c_str</span>(), create, <span class="built_in">sizeof</span>(ChainId), error);</span><br><span class="line">                      <span class="keyword">if</span> (!error.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">                        <span class="keyword">co_return</span> <span class="built_in">makeError</span>(StorageCode::kStorageStatFailed, std::<span class="built_in">move</span>(error));</span><br><span class="line">                      &#125;</span><br><span class="line">                      <span class="keyword">co_return</span> rust::Box&lt;chunk_engine::Engine&gt;::<span class="built_in">from_raw</span>(engine);</span><br><span class="line">                    &#125;).<span class="built_in">scheduleOn</span>(&amp;executor.<span class="built_in">pickNext</span>()));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> results = folly::coro::<span class="built_in">blockingWait</span>(folly::coro::<span class="built_in">collectAllRange</span>(std::<span class="built_in">move</span>(tasks)));</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;result : results) &#123;</span><br><span class="line">    <span class="built_in">RETURN_AND_LOG_ON_ERROR</span>(result);</span><br><span class="line">    engines_.<span class="built_in">push_back</span>(std::<span class="built_in">move</span>(result.<span class="built_in">value</span>()));</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>另外还会启动 <code>PunchHoleWorker</code>，应该就是 <code>compact_thread</code>，只对使用了 C++ 引擎的 StorageTarget 有作用，Rust 的垃圾回收在 <code>AllocateWorker</code> 里：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">StorageTarget</span> : <span class="keyword">public</span> enable_shared_from_this&lt;StorageTarget&gt; &#123;</span><br><span class="line">  <span class="function">Result&lt;<span class="type">bool</span>&gt; <span class="title">punchHole</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">useChunkEngine</span>()) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> chunkStore_.<span class="built_in">punchHole</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">PunchHoleWorker::loop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  RelativeTime lastPunchHoleTime = RelativeTime::<span class="built_in">now</span>();</span><br><span class="line"></span><br><span class="line">  <span class="type">bool</span> allTargetsRecycled = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">while</span> (!stopping_) &#123;</span><br><span class="line">    <span class="keyword">if</span> (allTargetsRecycled) &#123;</span><br><span class="line">      <span class="keyword">auto</span> lock = std::<span class="built_in">unique_lock</span>(mutex_);</span><br><span class="line">      <span class="keyword">if</span> (cond_.<span class="built_in">wait_for</span>(lock, <span class="number">1</span>_s, [&amp;] &#123; <span class="keyword">return</span> stopping_.<span class="built_in">load</span>(); &#125;)) &#123;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. recycle all targets.</span></span><br><span class="line">    <span class="keyword">if</span> (RelativeTime::<span class="built_in">now</span>() - lastPunchHoleTime &gt;= <span class="number">10</span>_s) &#123;</span><br><span class="line">      std::vector&lt;std::weak_ptr&lt;StorageTarget&gt;&gt; targets;</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="keyword">auto</span> targetMap = components_.targetMap.<span class="built_in">snapshot</span>();</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;[targetId, target] : targetMap-&gt;<span class="built_in">getTargets</span>()) &#123;</span><br><span class="line">          <span class="keyword">if</span> (target.<span class="built_in">unrecoverableOffline</span>()) &#123;</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">if</span> (target.localState != flat::LocalTargetState::OFFLINE &amp;&amp; target.storageTarget != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">            targets.<span class="built_in">push_back</span>(target.storageTarget);</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      allTargetsRecycled = <span class="literal">true</span>;</span><br><span class="line">      <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;weakTarget : targets) &#123;</span><br><span class="line">        <span class="keyword">if</span> (stopping_) &#123;</span><br><span class="line">          <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">auto</span> target = weakTarget.<span class="built_in">lock</span>();</span><br><span class="line">        <span class="keyword">if</span> (!target) &#123;</span><br><span class="line">          <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">bool</span> targetRecycled = <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span> i = <span class="number">0u</span>; i &lt; <span class="number">128u</span> &amp;&amp; !stopping_; ++i) &#123;</span><br><span class="line">          <span class="keyword">auto</span> result = target-&gt;<span class="built_in">punchHole</span>();</span><br><span class="line">          <span class="keyword">if</span> (result.<span class="built_in">hasError</span>()) &#123;</span><br><span class="line">            <span class="built_in">XLOGF</span>(ERR, <span class="string">&quot;recycle target &#123;&#125; failed: &#123;&#125;&quot;</span>, target-&gt;<span class="built_in">path</span>(), result.<span class="built_in">error</span>());</span><br><span class="line">            targetRecycled = <span class="literal">true</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">          &#125; <span class="keyword">else</span> <span class="keyword">if</span> (*result) &#123;</span><br><span class="line">            targetRecycled = <span class="literal">true</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        allTargetsRecycled &amp;= targetRecycled;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      lastPunchHoleTime = RelativeTime::<span class="built_in">now</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  stopped_ = <span class="literal">true</span>;</span><br><span class="line">  <span class="built_in">XLOGF</span>(INFO, <span class="string">&quot;PunchHoleWorker@&#123;&#125;::loop stopped&quot;</span>, fmt::<span class="built_in">ptr</span>(<span class="keyword">this</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>个人觉得后台线程这块还是挺混乱的，主要是线程功能职责混乱，还有多了一些空转的逻辑。还有 <code>PunchHoleWorker</code> 这里直接暴力重试 128 次调用 <code>punchHole</code>，不太符合 DeepSeek 对代码的极致追求啊，可能 C++ 不是主引擎。</p><p><a href="/programming/deepseek-3fs-code-reading-disk-io/download.jpeg" data-fancybox="gallery" data-caption=""><img src="/programming/deepseek-3fs-code-reading-disk-io/download.jpeg"></a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>3FS 的磁盘 IO 出人意料地采用了同步 IO 的模式，使用线程池去完成实际的 IO 操作。写操作通过 <code>UpdateWorker</code> 线程池完成，读操作使用了 AIO 异步读取实现 <code>AioReadWorker</code>（实际里面还实现了 <code>uring</code>），此时 <code>ChunkEngine</code> 或者 <code>ChunkStore</code> 被 Bypass，只起到提供读取的目标 <code>fd</code> 以及偏移和长度的作用，这也符合 3FS 所 claim 的对于读场景优化（对写场景看起来是真的没有做什么优化，甚至还加了锁将写操作串行化）。我们关心的对于网络协程和磁盘 IO 的交互，使用了有锁 MPMC 任务队列的方式将网络上收到的 IO 请求发送到 IO 线程池完成，最后也是通过 <code>Baton</code> 类来负责协程的挂起和唤醒。</p><p>这里的线程切换时机需要特别注意：从 RDMA 网络线程切换到 IO 线程，发生在 IO 工作线程取出 IO 任务这一刻；而 IO 任务完成后，虽然是在 IO 线程中执行的 <code>baton.post()</code>，但 folly 的 <code>Task</code> 通过 <code>await_transform</code> 变换保证 <code>co_await baton</code> 能正确地调度回 RDMA 线程的执行器执行后续流程，如果后面要提交网络 RDMA IO，实际上是切换回了 RDMA 线程去提交。所以 <code>co_await</code> 时如果不做特殊处理，其实是有可能发生线程切换的，如果是自己编写无栈协程库，这是编程中需要特别注意的一个点。关于 Folly 协程实现细节，可以参考：<a href="https://www.yinkuiwang.cn/2023/09/24/Folly%20coro%E5%AD%A6%E4%B9%A0/">https://www.yinkuiwang.cn/2023/09/24/Folly%20coro%E5%AD%A6%E4%B9%A0/</a></p><p>总的来说，个人看法是 3FS 在性能和开发复杂度之间进行了取舍，磁盘 IO 实现并没有 RDMA 网络那么精巧，实际上并没有在整个 IO 链路都采用纯异步的方式来编程。个人推测可能是因为其 Chunk 引擎使用了 RocksDB 来进行元数据持久化，Chunk 读写的关键路径上必定涉及 RocksDB IO 操作。但是，将 RocksDB 改造成全异步并不是一件简单的事，于是退而求其次将磁盘 IO 用同步阻塞的方式实现了。只要提交队列够深，或者磁盘 IO 的开销大于线程间协程的同步，那么总的来说是可以牺牲少量的性能来换取代码开发的大幅简化。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>
        <![CDATA[DeepSeek 3FS 源码解读——协程&RDMA篇]]>
      </title>
      <link>https://blog.howardlau.me/programming/deepseek-3fs-code-reading-coroutine.html</link>
      <description>
        <![CDATA[<p>重磅文件系统项目 3FS 在 DeepSeek 轰轰烈烈的开源周压轴登场，补齐了计算、网络以外的最后一块拼图——存储。和之前精巧的极致压榨性能或者提供巧妙算法的开源库不同，3FS]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 01 Mar 2025 06:51:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>重磅文件系统项目 3FS 在 DeepSeek 轰轰烈烈的开源周压轴登场，补齐了计算、网络以外的最后一块拼图——存储。和之前精巧的极致压榨性能或者提供巧妙算法的开源库不同，3FS 是完整的涉及多种节点、结合多种外部节点的高速并行文件系统，其代码结构清晰、模块间解耦程度高，充分利用了现代 C++ 的语言特性优化性能和代码可读性，还结合了 Rust 语言编写核心存储引擎，展现了 DeepSeek 工程师对复杂工程极强的驾驭能力。尽管已经提供了详尽的设计文档，其复杂程度对于想要阅读学习 3FS 项目的爱好者提出了不小的挑战。本文尝试一步步抽丝剥茧，在官方提供的设计文档之外，提供一些阅读源码的技巧和思路，希望能起到抛砖引玉的作用。</p><p>阅读完本文后，可以继续阅读<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-disk-io.html">DeepSeek 3FS 源码解读——磁盘 IO 篇</a></p><h2 id="协程基本思想"><a href="#协程基本思想" class="headerlink" title="协程基本思想"></a>协程基本思想</h2><p>现代网络和存储硬件基本已经全面拥抱了事件驱动或者完成事件驱动的异步编程范式。3FS 源码中最突出的一个特点便是大量使用了 C++ 20 提供的 <code>coroutines</code> 语言特性，使得程序能充分发挥异步性能的同时避免了大量的回调函数导致代码结构松散的问题。</p><p>在介绍 C++ 20 协程之前，我们先看看经典的异步编程是如何实现的。首先，异步编程最原始的 API 通常以回调的方式提供，总结起来为以下的伪代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">device</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 提交 IO 任务</span></span><br><span class="line">    <span class="function">error_code <span class="title">device::submit_io</span><span class="params">(io_request *req, function&lt;(io_request *, io_result)&gt; callback)</span> </span>&#123;</span><br><span class="line">        device_raw_io *raw_io;</span><br><span class="line">        raw_io-&gt;ctx = <span class="keyword">new</span> <span class="built_in">io_context</span>(req, callback);</span><br><span class="line">        raw_io-&gt;buf = req-&gt;buf;</span><br><span class="line">        raw_io-&gt;opcode = <span class="built_in">from_req_op</span>(req-&gt;op);</span><br><span class="line">        <span class="built_in">driver_submit_io</span>(device_ptr, raw_io);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 等待事件发生</span></span><br><span class="line">    <span class="function">error_code <span class="title">device::wait_event</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 读取事件/完成队列</span></span><br><span class="line">    <span class="function">error_code <span class="title">poll_events</span><span class="params">(vector&lt;event&gt; &amp;events)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">loop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="built_in">wait_event</span>());</span><br><span class="line">        vector&lt;event&gt; events;</span><br><span class="line">        <span class="built_in">poll_events</span>(events);</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;event : events) &#123;</span><br><span class="line">            <span class="comment">// 处理事件，调用回调</span></span><br><span class="line">            <span class="comment">// 回调中可能再次发起 IO 操作</span></span><br><span class="line">            io_context *ctx = (io_context*)event.data;</span><br><span class="line">            io_result res = <span class="built_in">io_result</span>(event.status);</span><br><span class="line">            ctx-&gt;<span class="built_in">callback</span>(ctx-&gt;req, res);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 事件循环</span></span><br><span class="line"><span class="function">jthread <span class="title">io_thread</span><span class="params">([]() &#123;</span></span></span><br><span class="line"><span class="params"><span class="function">    io_device-&gt;loop();</span></span></span><br><span class="line"><span class="params"><span class="function">&#125;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function">jthread <span class="title">net_thread</span><span class="params">([]() &#123;</span></span></span><br><span class="line"><span class="params"><span class="function">    net_device-&gt;loop();</span></span></span><br><span class="line"><span class="params"><span class="function">&#125;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 例如，读取磁盘上的数据，发送到网络上</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">demo</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    io_request *io_req, *net_req;</span><br><span class="line">    io_req-&gt;buf = <span class="keyword">new</span> buffer[<span class="number">1024</span>];</span><br><span class="line">    io_req-&gt;len = <span class="number">1024</span>;</span><br><span class="line">    io_req-&gt;fd = <span class="number">123</span>;</span><br><span class="line">    io_req-&gt;op = READ;</span><br><span class="line">    </span><br><span class="line">    io_device-&gt;<span class="built_in">submit_io</span>(req, [](io_request *io_req, io_result) &#123;</span><br><span class="line">       <span class="built_in">memcpy</span>(net_req-&gt;buf, io_req-&gt;buf, <span class="number">1024</span>);</span><br><span class="line">       net_req-&gt;peer = <span class="number">456</span>;</span><br><span class="line">       net_req-&gt;op = SEND;</span><br><span class="line">       net_device-&gt;<span class="built_in">submit_io</span>(req, [](io_request *net_req, io_result res) &#123;</span><br><span class="line">           <span class="keyword">if</span> (!res.<span class="built_in">ok</span>()) <span class="built_in">handle_error</span>();</span><br><span class="line">           <span class="built_in">print</span>(<span class="string">&quot;ok&quot;</span>);</span><br><span class="line">       &#125;)</span><br><span class="line">    &#125;);</span><br><span class="line">&#125; </span><br><span class="line"></span><br></pre></td></tr></table></figure><p>可以看到，如果一次请求处理涉及到多次异步操作，那么将会出现回调套回调的情况，并且使得生命周期变得非常难以管理。更不用说在 lambda 函数实现之前，需要手动分配上下文内存保存变量状态了。</p><p>为了解决异步编程回调地狱的问题，工程师提出了协程的解决方案，协程分为有栈协程和无栈协程两种。在 C++ 20 之前，工业界早已通过有栈协程实现了异步操作代码的同步化编写。有栈协程的基本实现原理是通过汇编指令或 <code>ucontext</code> 库保存恢复寄存器切换上下文的方式，将函数的栈从栈内存切换到堆内存中的空间，并将寄存器状态也保存到堆上使得协程发生切换时状态得以保留恢复。开发者只需要像写同步代码一样调用 <code>read</code> 和 <code>write</code> 等函数，由协程库通过动态库或者静态库 Hook 的方式劫持这些系统调用，并在其中实现有栈协程的调度。这里有一些细节问题，例如切换上下文的开销，以及协程栈空间分配浪费的问题；同时有时候开发者会意识不到协程的切换点，或者在写异步代码导致调用了线程级别的同步锁或者阻塞的系统调用，导致线程 Hang 死。</p><p>为了解决有栈协程的问题，C++ 20 实现了无栈协程方案，引入了 <code>co_await</code> 以及 <code>co_return</code> 等关键词，由开发者自己显式地指定异步操作的切换点。具体的 <code>coroutine</code> 细节网上资料已经有很多，这里提供一些简化版的代码示例和讲解来帮助快速入门。</p><p>使用无栈协程，上面的代码可以改造成：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">io_awaitable</span> &#123;</span><br><span class="line">    coroutine_handle handle;</span><br><span class="line">    io_request *req;</span><br><span class="line">    io_result res;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_resume</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_suspend</span><span class="params">(coroutine_handle&lt;&gt; h)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 挂起协程，发起 IO 操作</span></span><br><span class="line">        <span class="keyword">this</span>-&gt;handle = h;</span><br><span class="line">        device_raw_io *raw_io;</span><br><span class="line">        raw_io-&gt;ctx = <span class="keyword">this</span>;</span><br><span class="line">        raw_io-&gt;buf = req-&gt;buf;</span><br><span class="line">        raw_io-&gt;opcode = <span class="built_in">from_req_op</span>(req-&gt;op);</span><br><span class="line">        <span class="built_in">driver_submit_io</span>(device_ptr, raw_io);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">io_result <span class="title">await_resume</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 返回结果</span></span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">device</span> &#123;</span><br><span class="line">    <span class="comment">// 1. 提交 IO 任务</span></span><br><span class="line">    <span class="function">io_awaitable <span class="title">device::submit_io</span><span class="params">(io_request *req)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">io_awaitable</span>(req);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 2. 等待事件发生</span></span><br><span class="line">    <span class="function">error_code <span class="title">device::wait_event</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 读取事件/完成队列</span></span><br><span class="line">    <span class="function">error_code <span class="title">poll_events</span><span class="params">(vector&lt;event&gt; &amp;events)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">loop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="built_in">wait_event</span>());</span><br><span class="line">        vector&lt;event&gt; events;</span><br><span class="line">        <span class="built_in">poll_events</span>(events);</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">auto</span> &amp;event : events) &#123;</span><br><span class="line">            <span class="comment">// 处理事件，设置协程返回值</span></span><br><span class="line">            io_awaitable *awaitable = (io_awaitble*) event.ctx;</span><br><span class="line">            awaitable-&gt;res = <span class="built_in">io_result</span>(event.status);</span><br><span class="line">            <span class="comment">// 继续协程</span></span><br><span class="line">            awaitable-&gt;handle.<span class="built_in">resume</span>();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 事件循环</span></span><br><span class="line"><span class="function">jthread <span class="title">io_thread</span><span class="params">([]() &#123;</span></span></span><br><span class="line"><span class="params"><span class="function">    io_device-&gt;loop();</span></span></span><br><span class="line"><span class="params"><span class="function">&#125;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function">jthread <span class="title">net_thread</span><span class="params">([]() &#123;</span></span></span><br><span class="line"><span class="params"><span class="function">    net_device-&gt;loop();</span></span></span><br><span class="line"><span class="params"><span class="function">&#125;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 例如，读取磁盘上的数据，发送到网络上</span></span><br><span class="line">CoTask&lt;&gt; <span class="built_in">demo</span>() &#123;</span><br><span class="line">    io_request *io_req, *net_req;</span><br><span class="line">    io_req-&gt;buf = <span class="keyword">new</span> buffer[<span class="number">1024</span>];</span><br><span class="line">    io_req-&gt;len = <span class="number">1024</span>;</span><br><span class="line">    io_req-&gt;fd = <span class="number">123</span>;</span><br><span class="line">    io_req-&gt;op = READ;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">auto</span> res = <span class="keyword">co_await</span> io_device-&gt;<span class="built_in">submit_io</span>(req);</span><br><span class="line">    <span class="built_in">memcpy</span>(net_req-&gt;buf, io_req-&gt;buf, <span class="number">1024</span>);</span><br><span class="line">    net_req-&gt;peer = <span class="number">456</span>;</span><br><span class="line">    net_req-&gt;op = SEND;</span><br><span class="line">    res = <span class="keyword">co_await</span> net_device-&gt;<span class="built_in">submit_io</span>(req);</span><br><span class="line">    <span class="keyword">if</span> (!res.<span class="built_in">ok</span>()) <span class="built_in">handle_error</span>();</span><br><span class="line">    <span class="keyword">co_return</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">syncAwait</span>(<span class="built_in">demo</span>());</span><br></pre></td></tr></table></figure><p>可以看到，我们通过将异步操作封装成 <code>awaitable</code>，就可以享受到 C++ 20 带来的协程语言特性的便利。其中最为关键的部分，一个是 <code>await_suspend</code> 函数，这个函数是协程被挂起后，编译器将协程的一些状态和后续的代码执行逻辑封装到了 <code>coroutine_handle</code> 中，此变量指向了堆内存中的一片空间，保存了驱动后续协程运行的状态信息和代码地址，IO 操作的开发者只需要将此变量妥善的保管起来，便能在 IO 操作完成后调用此类上的方法继续协程的运行；另一个是 <code>co_await</code> 关键词，开发者通过此关键词，告诉编译器此处需要发生一次协程调度，编译器便会在此处插入一个暂停点，并将后续可能用到的变量等状态，打包为 <code>coroutine_handle</code>，传入到 <code>await_suspend</code> 函数中，以便后续调度执行。</p><p>无栈协程的好处是，开发者不再需要通过栈指针或者 <code>ucontext</code> 切换上下文的方式来保存和恢复自己的函数状态了，因为这些工作都由编译器自行分析函数中的变量生命周期后打包到 <code>coroutine_handle</code> 中了。而且实际上，这里就是一次简单的函数调用，展开后可以理解为如下伪代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">continuation2</span><span class="params">(ctx_type2 *ctx2)</span> </span>&#123;</span><br><span class="line">    io_result res = ctx2-&gt;ret;</span><br><span class="line">    <span class="keyword">if</span> (!res.<span class="built_in">ok</span>()) <span class="built_in">handle_error</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">continuation1</span><span class="params">(ctx_type1 *ctx1)</span> </span>&#123;</span><br><span class="line">    io_result res = ctx1-&gt;ret;</span><br><span class="line">    ctx_type2 *ctx2 = <span class="keyword">new</span> <span class="built_in">ctx2</span>();</span><br><span class="line">    <span class="built_in">store_vars</span>(ctx2, var2, ...);</span><br><span class="line">    io_awaitable a2 = io_device-&gt;<span class="built_in">submit_io</span>(net_req);</span><br><span class="line">    coroutine_handle h = <span class="built_in">from_ctx_and_cont</span>(ctx2, continuation2);</span><br><span class="line">    a<span class="number">2.</span><span class="built_in">await_suspend</span>(h);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">demo</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    ctx_type1 *ctx1 = <span class="keyword">new</span> <span class="built_in">ctx1</span>();</span><br><span class="line">    <span class="built_in">store_vars</span>(ctx1, var1, ...);</span><br><span class="line">    io_awaitable a1 = io_device-&gt;<span class="built_in">submit_io</span>(io_req);</span><br><span class="line">    coroutine_handle h = <span class="built_in">from_ctx_and_cont</span>(ctx1, continuation_1);</span><br><span class="line">    a<span class="number">1.</span><span class="built_in">await_suspend</span>(h);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以，C++ 20 的协程，我更倾向于认为它是一个语法糖。事实上，C++ 20 的协程也仅仅提供了语言层面的一些语法支持，其标准库中并没有包含协程运行必须的调度器等关键组件。另外，<code>await_suspend</code> 仅仅提供了挂起和继续协程的功能，在实际编程中，我们通常会将多个异步操作也逐层封装成一个函数，对于这种嵌套的协程执行，还需要开发者自行封装一个类来保存嵌套关系，从而能正确地执行，这也是为什么示例中的函数签名，使用了 <code>co_await</code> 关键词的函数返回值是一个 <code>CoTask</code> 类。这也导致了无栈协程具有极强的侵入性，一旦代码中的一处使用了无栈协程，那么调用此代码上路径所涉及到的所有函数，都必须修改为无栈协程的调用方式，插入 <code>co_await</code> 点。</p><h2 id="协程在-3FS-中的实现和应用"><a href="#协程在-3FS-中的实现和应用" class="headerlink" title="协程在 3FS 中的实现和应用"></a>协程在 3FS 中的实现和应用</h2><p>回到 3FS 的话题上，3FS 并没有选择自行编写协程库，甚至也没有使用 <code>await_suspend</code> 的方式来封装 IO（在代码库里是搜索不到这个关键词的），而是使用了 Facebook 开源的 <code>folly</code> 库，调用了其中实现的 Executor 等执行调度器驱动。由于 <code>folly</code> 本身已经将传统的网络 Socket 和 IO 等操作封装成为了协程函数，所以这里着重分析下 3FS 如何将 RDMA 操作封装成协程函数的。这里先建议读者自行下载 3FS 和其三方依赖库的代码后到本地后再继续参考阅读。</p><p>这里也提供下我的分析思路：对于封装底层的 IO 操作，一个常见的做法就是通过上述提到的 <code>await_suspend</code> 方法，来接收编译器挂起好的协程，但是没搜索到这个函数；所以换个思路，了解 RDMA 编程的开发者知道，发起 RDMA 操作调用 <code>ibv_post_send</code>，轮询并处理完成事件则是调用 <code>ibv_poll_cq</code>，搜索这两个函数顺藤摸瓜便能找到 3FS 是如何封装 RDMA 到协程中的了。</p><p>主要涉及到的代码文件是 <code>src/net/IBSocket.{h,cc}</code>，在 <code>IBSocket.cc</code> 中，我们找到了两处关键地方：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">IBSocket::rdmaPostWR</span><span class="params">(RDMAPostCtx &amp;ctx)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  wrs.<span class="built_in">rbegin</span>()-&gt;next = <span class="literal">nullptr</span>;</span><br><span class="line">  wrs.<span class="built_in">rbegin</span>()-&gt;wr_id = WRId::<span class="built_in">rdma</span>(&amp;ctx, <span class="literal">true</span>);</span><br><span class="line">  wrs.<span class="built_in">rbegin</span>()-&gt;send_flags |= IBV_SEND_SIGNALED;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  ibv_send_wr *bad = <span class="literal">nullptr</span>;</span><br><span class="line">  <span class="type">const</span> <span class="type">int</span> ret = <span class="built_in">ibv_post_send</span>(qp_.<span class="built_in">get</span>(), &amp;wrs[<span class="number">0</span>], &amp;bad);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>wr_id</code>，实际上就是一次 RDMA 操作对应的 Context，设备在完成事件中，将程序传入的值原封不动的返回，这样程序就能找到完成事件对应的 Context 是什么，从而找到 Context 中包含的协程，将其重新调度运行。这里的 <code>WRId</code> 使用了 <code>StampedPtr</code> 技巧，利用了 64 位机器中的虚拟地址指针只有低 48 位用于寻址的特点，在未使用的高 16 位中保存了一些元信息：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">  <span class="keyword">struct</span> <span class="title class_">WRId</span> : <span class="keyword">private</span> folly::StampedPtr&lt;<span class="type">void</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Base = folly::StampedPtr&lt;<span class="type">void</span>&gt;;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">send</span><span class="params">(<span class="type">uint32_t</span> signalCount)</span> </span>&#123; <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(signalCount, WRType::SEND); &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">recv</span><span class="params">(<span class="type">uint32_t</span> bufIndex)</span> </span>&#123; <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(bufIndex, WRType::RECV); &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">ack</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(<span class="literal">nullptr</span>, WRType::ACK); &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">rdma</span><span class="params">(RDMAPostCtx *ptr, <span class="type">bool</span> last)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(ptr, last ? WRType::RDMA_LAST : WRType::RDMA);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">close</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(<span class="literal">nullptr</span>, WRType::CLOSE); &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">check</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> WRId::<span class="built_in">pack</span>(<span class="literal">nullptr</span>, WRType::CHECK); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">pack</span><span class="params">(<span class="type">uint32_t</span> val, WRType type)</span> </span>&#123; <span class="keyword">return</span> Base::<span class="built_in">pack</span>((<span class="type">void</span> *)(<span class="type">uint64_t</span>)val, (<span class="type">uint16_t</span>)type); &#125;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">uint64_t</span> <span class="title">pack</span><span class="params">(<span class="type">void</span> *ptr, WRType type)</span> </span>&#123; <span class="keyword">return</span> Base::<span class="built_in">pack</span>(ptr, (<span class="type">uint16_t</span>)type); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">WRId</span>(<span class="type">uint64_t</span> raw)</span><br><span class="line">        : Base&#123;raw&#125; &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">WRType <span class="title">type</span><span class="params">()</span> <span class="type">const</span> </span>&#123; <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;WRType&gt;(<span class="built_in">stamp</span>()); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">uint32_t</span> <span class="title">sendSignalCount</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">      <span class="built_in">assert</span>(<span class="built_in">type</span>() == WRType::SEND);</span><br><span class="line">      <span class="keyword">return</span> (<span class="type">uint32_t</span>)(<span class="type">uint64_t</span>)<span class="built_in">ptr</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">uint32_t</span> <span class="title">recvBufIndex</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">      <span class="built_in">assert</span>(<span class="built_in">type</span>() == WRType::RECV);</span><br><span class="line">      <span class="keyword">return</span> (<span class="type">uint32_t</span>)(<span class="type">uint64_t</span>)<span class="built_in">ptr</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">RDMAPostCtx *<span class="title">rdmaPostCtx</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">      <span class="built_in">assert</span>(<span class="built_in">type</span>() == WRType::RDMA || <span class="built_in">type</span>() == WRType::RDMA_LAST);</span><br><span class="line">      <span class="keyword">return</span> (RDMAPostCtx *)<span class="built_in">ptr</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>继续顺藤摸瓜，分析一下 <code>RDMAPostCtx</code> 这个类，位于 <code>src/net/IBSocket.h</code> 中</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">RDMAPostCtx</span> &#123;</span><br><span class="line">    std::optional&lt;std::reference_wrapper&lt;folly::fibers::BatchSemaphore&gt;&gt; sem;</span><br><span class="line">    std::optional&lt;folly::fibers::BatchSemaphore::Waiter&gt; waiter;</span><br><span class="line"></span><br><span class="line">    ibv_wr_opcode opcode;</span><br><span class="line">    std::span&lt;<span class="type">const</span> RDMAReq&gt; reqs;</span><br><span class="line">    std::span&lt;RDMABuf&gt; localBufs;</span><br><span class="line">    <span class="type">size_t</span> bytes = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    folly::coro::Baton baton;</span><br><span class="line">    ibv_wc_status status = IBV_WC_SUCCESS;</span><br><span class="line">    std::chrono::steady_clock::time_point postBegin;</span><br><span class="line">    std::chrono::steady_clock::time_point postEnd;</span><br><span class="line"></span><br><span class="line">    <span class="function">CoTask&lt;<span class="type">void</span>&gt; <span class="title">waitSem</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      <span class="keyword">if</span> (waiter.<span class="built_in">has_value</span>()) &#123;</span><br><span class="line">        <span class="keyword">co_await</span> waiter-&gt;baton;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">setError</span><span class="params">(ibv_wc_status error)</span> </span>&#123;</span><br><span class="line">      <span class="keyword">if</span> (status == IBV_WC_SUCCESS) &#123;</span><br><span class="line">        status = error;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    __attribute__((<span class="built_in">no_sanitize</span>(<span class="string">&quot;thread&quot;</span>))) <span class="function"><span class="type">void</span> <span class="title">finish</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      postEnd = std::chrono::steady_clock::<span class="built_in">now</span>();</span><br><span class="line">      baton.<span class="built_in">post</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个类和之前示例中的 <code>io_awaitable</code> 类似，保存了一些 IO 请求的信息，并且预留了 <code>status</code> 字段保存返回结果，而其中的 <code>finish()</code> 函数，感觉便是调度协程运行的关键函数，尝试分析下调用方，发现位于 <code>IBSocket::cqPoll</code> 函数中。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">IBSocket::cqPoll</span><span class="params">(Events &amp;events)</span> </span>&#123;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="type">int</span> kPollCQBatch = <span class="number">16</span>;</span><br><span class="line">  std::array&lt;ibv_wc, kPollCQBatch&gt; wcArr;</span><br><span class="line"></span><br><span class="line">  <span class="type">int</span> ret = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">while</span> ((ret = <span class="built_in">ibv_poll_cq</span>(cq_.<span class="built_in">get</span>(), kPollCQBatch, &amp;wcArr[<span class="number">0</span>])) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">IBDBG</span>(<span class="string">&quot;IBSocket &#123;&#125; get &#123;&#125; WCs&quot;</span>, <span class="built_in">describe</span>(), ret);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; ret; i++) &#123;</span><br><span class="line">      <span class="type">const</span> ibv_wc &amp;wc = wcArr[i];</span><br><span class="line">      <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(wc.status != IBV_WC_SUCCESS)) &#123;</span><br><span class="line">        <span class="built_in">wcError</span>(wc);</span><br><span class="line">        state_ = State::ERROR;</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">UNLIKELY</span>(<span class="built_in">wcSuccess</span>(wc, events) != <span class="number">0</span>)) &#123;</span><br><span class="line">        state_ = State::ERROR;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// break out of the loop if the CQ is empty</span></span><br><span class="line">    <span class="keyword">if</span> (ret &lt; kPollCQBatch) <span class="keyword">break</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>wcSuccess</code> 最终会调用的是此代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">IBSocket::onRDMAFinished</span><span class="params">(<span class="type">const</span> ibv_wc &amp;wc, Events &amp;)</span> </span>&#123;</span><br><span class="line">  <span class="function">WRId <span class="title">wr</span><span class="params">(wc.wr_id)</span></span>;</span><br><span class="line">  wr.<span class="built_in">rdmaPostCtx</span>()-&gt;<span class="built_in">finish</span>();</span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么这里就能确定，<code>finish</code> 是在处理完成事件时被调用，从而唤醒协程了。我们继续分析下 <code>Baton</code> 类的实现原理（<code>folly/experimental/coro/Baton.h</code>），代码不多，直接贴过来了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/// A baton is a synchronisation primitive for coroutines that allows a</span></span><br><span class="line"><span class="comment">/// coroutine to co_await the baton and suspend until the baton is posted by</span></span><br><span class="line"><span class="comment">/// some thread via a call to .post().</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// This primitive is typically used in the construction of larger library types</span></span><br><span class="line"><span class="comment">/// rather than directly in user code.</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// As a primitive, this is not cancellation-aware.</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// The Baton supports being awaited by multiple coroutines at a time. If the</span></span><br><span class="line"><span class="comment">/// baton is not ready at the time it is awaited then an awaiting coroutine</span></span><br><span class="line"><span class="comment">/// suspends. All suspended coroutines waiting for the baton to be posted will</span></span><br><span class="line"><span class="comment">/// be resumed when some thread next calls .post().</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">/// Example usage:</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">///   folly::coro::Baton baton;</span></span><br><span class="line"><span class="comment">///   std::string sharedValue;</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">///   folly::coro::Task&lt;void&gt; consumer()</span></span><br><span class="line"><span class="comment">///   &#123;</span></span><br><span class="line"><span class="comment">///     // Wait until the baton is posted.</span></span><br><span class="line"><span class="comment">///     co_await baton;</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">///     // Now safe to read shared state.</span></span><br><span class="line"><span class="comment">///     std::cout &lt;&lt; sharedValue &lt;&lt; std::cout;</span></span><br><span class="line"><span class="comment">///   &#125;</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">///   void producer()</span></span><br><span class="line"><span class="comment">///   &#123;</span></span><br><span class="line"><span class="comment">///     // Write to shared state</span></span><br><span class="line"><span class="comment">///     sharedValue = &quot;some result&quot;;</span></span><br><span class="line"><span class="comment">///</span></span><br><span class="line"><span class="comment">///     // Publish the value by &#x27;posting&#x27; the baton.</span></span><br><span class="line"><span class="comment">///     // This will resume the consumer if it was currently suspended.</span></span><br><span class="line"><span class="comment">///     baton.post();</span></span><br><span class="line"><span class="comment">///   &#125;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Baton</span> &#123;</span><br><span class="line"> <span class="keyword">public</span>:</span><br><span class="line">  <span class="keyword">class</span> <span class="title class_">WaitOperation</span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/// Initialise the Baton to either the signalled or non-signalled state.</span></span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">Baton</span><span class="params">(<span class="type">bool</span> initiallySignalled = <span class="literal">false</span>)</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  ~<span class="built_in">Baton</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">/// Query whether the Baton is currently in the signalled state.</span></span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">ready</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/// Asynchronously wait for the Baton to enter the signalled state.</span></span><br><span class="line">  <span class="comment">///</span></span><br><span class="line">  <span class="comment">/// The returned object must be co_awaited from a coroutine. If the Baton</span></span><br><span class="line">  <span class="comment">/// is already signalled then the awaiting coroutine will continue without</span></span><br><span class="line">  <span class="comment">/// suspending. Otherwise, if the Baton is not yet signalled then the</span></span><br><span class="line">  <span class="comment">/// awaiting coroutine will suspend execution and will be resumed when some</span></span><br><span class="line">  <span class="comment">/// thread later calls post().</span></span><br><span class="line">  [[nodiscard]] <span class="function">WaitOperation <span class="keyword">operator</span> <span class="title">co_await</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/// Set the Baton to the signalled state if it is not already signalled.</span></span><br><span class="line">  <span class="comment">///</span></span><br><span class="line">  <span class="comment">/// This will resume any coroutines that are currently suspended waiting</span></span><br><span class="line">  <span class="comment">/// for the Baton inside &#x27;co_await baton&#x27;.</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">post</span><span class="params">()</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/// Atomically reset the baton back to the non-signalled state.</span></span><br><span class="line">  <span class="comment">///</span></span><br><span class="line">  <span class="comment">/// This is a no-op if the baton was already in the non-signalled state.</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">reset</span><span class="params">()</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">class</span> <span class="title class_">WaitOperation</span> &#123;</span><br><span class="line">   <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="keyword">explicit</span> <span class="title">WaitOperation</span><span class="params">(<span class="type">const</span> Baton&amp; baton)</span> <span class="keyword">noexcept</span> : baton_(baton) &#123;</span>&#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> baton_.<span class="built_in">ready</span>(); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_suspend</span><span class="params">(coroutine_handle&lt;&gt; awaitingCoroutine)</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">      awaitingCoroutine_ = awaitingCoroutine;</span><br><span class="line">      <span class="keyword">return</span> baton_.<span class="built_in">waitImpl</span>(<span class="keyword">this</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_resume</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">protected</span>:</span><br><span class="line">    <span class="keyword">friend</span> <span class="keyword">class</span> <span class="title class_">Baton</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> Baton&amp; baton_;</span><br><span class="line">    coroutine_handle&lt;&gt; awaitingCoroutine_;</span><br><span class="line">    WaitOperation* next_;</span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span>:</span><br><span class="line">  <span class="comment">// Try to register the awaiter as</span></span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">waitImpl</span><span class="params">(WaitOperation* awaiter)</span> <span class="type">const</span> <span class="keyword">noexcept</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// this  - Baton is in the signalled/posted state.</span></span><br><span class="line">  <span class="comment">// other - Baton is not signalled/posted and this is a pointer to the head</span></span><br><span class="line">  <span class="comment">//         of a potentially empty linked-list of Awaiter nodes that were</span></span><br><span class="line">  <span class="comment">//         waiting for the baton to become signalled.</span></span><br><span class="line">  <span class="keyword">mutable</span> std::atomic&lt;<span class="type">void</span>*&gt; state_;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="title">Baton::Baton</span><span class="params">(<span class="type">bool</span> initiallySignalled)</span> <span class="keyword">noexcept</span></span></span><br><span class="line"><span class="function">    : state_(initiallySignalled ? static_cast&lt;void*&gt;(this) : nullptr) &#123;</span>&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">bool</span> <span class="title">Baton::ready</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> state_.<span class="built_in">load</span>(std::memory_order_acquire) ==</span><br><span class="line">      <span class="built_in">static_cast</span>&lt;<span class="type">const</span> <span class="type">void</span>*&gt;(<span class="keyword">this</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">inline</span> Baton::WaitOperation Baton::<span class="keyword">operator</span> <span class="title">co_await</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> Baton::WaitOperation&#123;*<span class="keyword">this</span>&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="type">void</span> <span class="title">Baton::reset</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">  <span class="comment">// Transition from &#x27;signalled&#x27; (ie. &#x27;this&#x27;) to not-signalled (ie. nullptr).</span></span><br><span class="line">  <span class="type">void</span>* oldState = <span class="keyword">this</span>;</span><br><span class="line">  (<span class="type">void</span>)state_.<span class="built_in">compare_exchange_strong</span>(</span><br><span class="line">      oldState, <span class="literal">nullptr</span>, std::memory_order_acq_rel, std::memory_order_relaxed);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">Baton::post</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">  <span class="type">void</span>* <span class="type">const</span> signalledState = <span class="built_in">static_cast</span>&lt;<span class="type">void</span>*&gt;(<span class="keyword">this</span>);</span><br><span class="line">  <span class="type">void</span>* oldValue = state_.<span class="built_in">exchange</span>(signalledState, std::memory_order_acq_rel);</span><br><span class="line">  <span class="keyword">if</span> (oldValue != signalledState) &#123;</span><br><span class="line">    <span class="comment">// We are the first thread to set the state to signalled and there is</span></span><br><span class="line">    <span class="comment">// a waiting coroutine. We are responsible for resuming it.</span></span><br><span class="line">    WaitOperation* awaiter = <span class="built_in">static_cast</span>&lt;WaitOperation*&gt;(oldValue);</span><br><span class="line">    <span class="keyword">while</span> (awaiter != <span class="literal">nullptr</span>) &#123;</span><br><span class="line">      std::<span class="built_in">exchange</span>(awaiter, awaiter-&gt;next_)-&gt;awaitingCoroutine_.<span class="built_in">resume</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Baton::waitImpl</span><span class="params">(WaitOperation* awaiter)</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">  <span class="comment">// Try to push the awaiter onto the front of the queue of waiters.</span></span><br><span class="line">  <span class="type">const</span> <span class="keyword">auto</span> signalledState = <span class="built_in">static_cast</span>&lt;<span class="type">const</span> <span class="type">void</span>*&gt;(<span class="keyword">this</span>);</span><br><span class="line">  <span class="type">void</span>* oldValue = state_.<span class="built_in">load</span>(std::memory_order_acquire);</span><br><span class="line">  <span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (oldValue == signalledState) &#123;</span><br><span class="line">      <span class="comment">// Already in the signalled state, don&#x27;t enqueue it.</span></span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    awaiter-&gt;next_ = <span class="built_in">static_cast</span>&lt;WaitOperation*&gt;(oldValue);</span><br><span class="line">  &#125; <span class="keyword">while</span> (!folly::<span class="built_in">atomic_compare_exchange_weak_explicit</span>(</span><br><span class="line">      &amp;state_,</span><br><span class="line">      &amp;oldValue,</span><br><span class="line">      awaiter,</span><br><span class="line">      std::memory_order_release,</span><br><span class="line">      std::memory_order_acquire));</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>查看 <code>Baton</code> 的实现，可以发现它确实使用了 <code>await_suspend</code> 将 <code>coroutine_handle</code> 保存起来了，并且在有人调用 <code>post</code> 的时候，通过调用 <code>coroutine_handle</code> 的 <code>resume</code> 方法将其唤醒执行。</p><p>那么，什么时候发生的挂起呢？我们搜索 <code>rdmaPostWR</code> 的调用方，找到了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">IBSocket::rdmaPost</span><span class="params">(RDMAPostCtx &amp;ctx)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">IBDBG</span>(<span class="string">&quot;IBSocket &#123;&#125; postRdma: opcode &#123;&#125;, &#123;&#125; reqs&quot;</span>, <span class="built_in">describe</span>(), magic_enum::<span class="built_in">enum_name</span>(ctx.opcode), ctx.reqs.<span class="built_in">size</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (ctx.waiter.<span class="built_in">has_value</span>()) &#123;</span><br><span class="line">    <span class="keyword">co_await</span> ctx.waiter-&gt;baton;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">auto</span> guard = folly::<span class="built_in">makeGuard</span>([&amp;]() &#123; rdmaSem_.<span class="built_in">signal</span>(ctx.reqs.<span class="built_in">size</span>()); &#125;);</span><br><span class="line">  <span class="built_in">CO_RETURN_ON_ERROR</span>(<span class="built_in">checkState</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">auto</span> ret = <span class="built_in">rdmaPostWR</span>(ctx); <span class="built_in">UNLIKELY</span>(ret != <span class="number">0</span>)) &#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="title">makeError</span><span class="params">(RPCCode::kRDMAPostFailed)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">co_await</span> ctx.baton;</span><br><span class="line">  <span class="keyword">if</span> (ctx.status != IBV_WC_SUCCESS) &#123;</span><br><span class="line">    <span class="built_in">XLOGF</span>(DBG, <span class="string">&quot;IBSocket &#123;&#125; RDMA failed, error &#123;&#125;&quot;</span>, <span class="built_in">describe</span>(), <span class="built_in">ibv_wc_status_str</span>(ctx.status));</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="title">makeError</span><span class="params">(RPCCode::kRDMAError)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">co_return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>继续顺藤摸瓜，可以找到一些 RDMA 操作原语了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">IBSocket</span> &#123;</span><br><span class="line">  <span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">rdmaRead</span><span class="params">(<span class="type">const</span> RDMARemoteBuf &amp;remoteBuf, RDMABuf &amp;localBuf)</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="keyword">co_await</span> <span class="title">rdmaRead</span><span class="params">(remoteBuf, std::span(&amp;localBuf, <span class="number">1</span>))</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">rdmaRead</span><span class="params">(<span class="type">const</span> RDMARemoteBuf &amp;remoteBuf, std::span&lt;RDMABuf&gt; localBufs)</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="keyword">co_await</span> <span class="title">rdma</span><span class="params">(IBV_WR_RDMA_READ, remoteBuf, localBufs)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">rdmaWrite</span><span class="params">(<span class="type">const</span> RDMARemoteBuf &amp;remoteBuf, RDMABuf &amp;localBuf)</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="keyword">co_await</span> <span class="title">rdmaWrite</span><span class="params">(remoteBuf, std::span(&amp;localBuf, <span class="number">1</span>))</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">rdmaWrite</span><span class="params">(<span class="type">const</span> RDMARemoteBuf &amp;remoteBuf, std::span&lt;RDMABuf&gt; localBufs)</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="keyword">co_await</span> <span class="title">rdma</span><span class="params">(IBV_WR_RDMA_WRITE, remoteBuf, localBufs)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RDMAReqBatch</span> &#123;</span><br><span class="line">  <span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">post</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">co_return</span> <span class="keyword">co_await</span> socket_-&gt;<span class="built_in">rdmaBatch</span>(opcode_, reqs_, localBufs_, waitLatency_, transferLatency_);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里已经到达了上层应用调用 RDMA 操作的边界处，上层应用通过 <code>co_await rdmaWrite()</code> 等方式去执行异步的 RDMA 操作，在 RDMA 操作结束前协程会被挂起，在完成后被调度执行。</p><p>这里还有一个 <code>CoTryTask</code> 还没有介绍，实际上它是 <code>folly::coro::Task</code> 一个简单的类型别名封装，它就是示例代码后面提到的，用于嵌套协程执行的类。它的关键逻辑是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Task</span> &#123;</span><br><span class="line">  <span class="keyword">class</span> <span class="title class_">Awaiter</span> &#123;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">template</span> &lt;<span class="keyword">typename</span> Promise&gt;</span><br><span class="line">    <span class="function">FOLLY_NOINLINE <span class="keyword">auto</span> <span class="title">await_suspend</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">        coroutine_handle&lt;Promise&gt; continuation)</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">      <span class="built_in">DCHECK</span>(coro_);</span><br><span class="line">      <span class="keyword">auto</span>&amp; promise = coro_.<span class="built_in">promise</span>();</span><br><span class="line"></span><br><span class="line">      promise.continuation_ = continuation;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">auto</span>&amp; calleeFrame = promise.<span class="built_in">getAsyncFrame</span>();</span><br><span class="line">      calleeFrame.<span class="built_in">setReturnAddress</span>();</span><br><span class="line"></span><br><span class="line">      <span class="function"><span class="keyword">if</span> <span class="title">constexpr</span> <span class="params">(detail::promiseHasAsyncFrame_v&lt;Promise&gt;)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">auto</span>&amp; callerFrame = continuation.<span class="built_in">promise</span>().<span class="built_in">getAsyncFrame</span>();</span><br><span class="line">        folly::<span class="built_in">pushAsyncStackFrameCallerCallee</span>(callerFrame, calleeFrame);</span><br><span class="line">        <span class="keyword">return</span> coro_;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        folly::<span class="built_in">resumeCoroutineWithNewAsyncStackRoot</span>(coro_);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">T <span class="title">await_resume</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      <span class="built_in">DCHECK</span>(coro_);</span><br><span class="line">      SCOPE_EXIT &#123; std::<span class="built_in">exchange</span>(coro_, &#123;&#125;).<span class="built_in">destroy</span>(); &#125;;</span><br><span class="line">      <span class="keyword">return</span> std::<span class="built_in">move</span>(coro_.<span class="built_in">promise</span>().<span class="built_in">result</span>()).<span class="built_in">value</span>();</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>最关键的地方在于保存好 <code>continuation</code> 点，用于正确执行被嵌套的协程。这里还值得一提的是，<code>folly</code> 中通过 <code>AsyncFrame</code> 来保存协程的调用关系，从而能在调试时展开协程调用链帮助恢复现场。当然，这会引入额外开销，如果是自己编写协程库，可以去掉这一步。其他关于协程嵌套封装的逻辑，网络上介绍 C++ 20 协程的资料也都会详细讲解，这里不再展开了。</p><p>至此，我们已经梳理清楚了示例中的 IO 异步操作 Demo 逻辑，是如何在 3FS 中的 RDMA 操作实战的了。事实上，协程调度不仅仅局限于 IO 异步操作，在一些需要发生自定义的调度逻辑时，我们同样可以通过 <code>co_await</code> 灵活地挂起或调度协程的执行。</p><p>留意到，在 <code>IBSocket::rdmaPost</code> 中，有一段这样的逻辑：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">IBSocket::rdmaPost</span><span class="params">(RDMAPostCtx &amp;ctx)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (ctx.waiter.<span class="built_in">has_value</span>()) &#123;</span><br><span class="line">    <span class="keyword">co_await</span> ctx.waiter-&gt;baton;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个 <code>waiter</code> 变量，是一个 <code>folly::fibers::BatchSemaphore::Waiter</code> 类，可以理解为是一个信号量等待者的实现，用于限制并发数，在令牌不足的时候，负责调用信号量的 <code>wait</code> 函数挂起协程：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">SemaphoreBase::waitSlow</span><span class="params">(Waiter&amp; waiter, <span class="type">int64_t</span> tokens)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// Slow path, create a baton and acquire a mutex to update the wait list</span></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">auto</span> waitListLock = waitList_.<span class="built_in">wlock</span>();</span><br><span class="line">    <span class="keyword">auto</span>&amp; waitList = *waitListLock;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">auto</span> testVal = tokens_.<span class="built_in">load</span>(std::memory_order_acquire);</span><br><span class="line">    <span class="keyword">if</span> (testVal &gt;= tokens) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// prepare baton and add to queue</span></span><br><span class="line">    waitList.<span class="built_in">push_back</span>(waiter);</span><br><span class="line">    <span class="built_in">assert</span>(!waitList.<span class="built_in">empty</span>());</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// Signal to caller that we managed to push a waiter</span></span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>并在令牌重新充足后，通过 <code>baton.post()</code> 调度执行被挂起的协程。</p><p>信号量是 <code>rdmaSem_</code>，相关的 PV 操作位于：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">IBSocket::rdmaBatch</span><span class="params">(...)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="comment">/* setup post info and wait on semaphore */</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i &lt; numPosts; i++) &#123;</span><br><span class="line">    <span class="keyword">auto</span> &amp;post = posts[i];</span><br><span class="line">    post.opcode = opcode;</span><br><span class="line">    post.reqs = reqs.<span class="built_in">subspan</span>(i * wrsPerPost, std::<span class="built_in">min</span>(reqs.<span class="built_in">size</span>() - i * wrsPerPost, wrsPerPost));</span><br><span class="line">    post.localBufs = localBufs;</span><br><span class="line">    post.waiter.<span class="built_in">emplace</span>(post.reqs.<span class="built_in">size</span>());</span><br><span class="line">    <span class="comment">// 尝试获取信号量</span></span><br><span class="line">    <span class="keyword">if</span> (rdmaSem_.<span class="built_in">try_wait</span>(post.waiter.<span class="built_in">value</span>(), post.reqs.<span class="built_in">size</span>())) &#123;</span><br><span class="line">      <span class="comment">// 获取成功了就不必等待</span></span><br><span class="line">      post.waiter = std::<span class="literal">nullopt</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">CoTryTask&lt;<span class="type">void</span>&gt; <span class="title">IBSocket::rdmaPost</span><span class="params">(RDMAPostCtx &amp;ctx)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">IBDBG</span>(<span class="string">&quot;IBSocket &#123;&#125; postRdma: opcode &#123;&#125;, &#123;&#125; reqs&quot;</span>, <span class="built_in">describe</span>(), magic_enum::<span class="built_in">enum_name</span>(ctx.opcode), ctx.reqs.<span class="built_in">size</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (ctx.waiter.<span class="built_in">has_value</span>()) &#123;</span><br><span class="line">    <span class="keyword">co_await</span> ctx.waiter-&gt;baton;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 释放信号量，通过 RAII 实现在此函数执行完后调用</span></span><br><span class="line">  <span class="keyword">auto</span> guard = folly::<span class="built_in">makeGuard</span>([&amp;]() &#123; rdmaSem_.<span class="built_in">signal</span>(ctx.reqs.<span class="built_in">size</span>()); &#125;);</span><br><span class="line">  <span class="built_in">CO_RETURN_ON_ERROR</span>(<span class="built_in">checkState</span>());</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">auto</span> ret = <span class="built_in">rdmaPostWR</span>(ctx); <span class="built_in">UNLIKELY</span>(ret != <span class="number">0</span>)) &#123;</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="title">makeError</span><span class="params">(RPCCode::kRDMAPostFailed)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">co_await</span> ctx.baton;</span><br><span class="line">  <span class="keyword">if</span> (ctx.status != IBV_WC_SUCCESS) &#123;</span><br><span class="line">    <span class="built_in">XLOGF</span>(DBG, <span class="string">&quot;IBSocket &#123;&#125; RDMA failed, error &#123;&#125;&quot;</span>, <span class="built_in">describe</span>(), <span class="built_in">ibv_wc_status_str</span>(ctx.status));</span><br><span class="line">    <span class="function"><span class="keyword">co_return</span> <span class="title">makeError</span><span class="params">(RPCCode::kRDMAError)</span></span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">co_return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此外，在 <code>RDMATransmissionLimiter</code> 类中也能看到类似的思想。</p><h2 id="事件驱动"><a href="#事件驱动" class="headerlink" title="事件驱动"></a>事件驱动</h2><p>上面介绍的是一些离散的协程操作，我们现在尝试分析一下 3FS 是如何驱动事件循环让这些协程顺利地调度起来的。既然是异步操作，那么是免不了事件循环的，我们还是从 <code>ibv_poll_cq</code> 这个函数出发，找到调用它的事件循环逻辑。</p><p>可以从代码看到，有个函数 <code>IBSocket::poll</code> 是很明显的轮询事件的函数，调用它的是 <code>Transport::handleEvents</code>，再往上寻找，可以发现有一个 <code>EventLoop::loop</code> 函数调用了它，这里就是非常经典的基于 <code>epoll</code> 的事件循环了。</p><p>那么只需要分析，什么事件触发了 RDMA 的 <code>handleEvents</code> 即可。了解 RDMA 编程的开发者可能知道，RDMA 只规定了 Busy Loop 轮询完成事件的方式。这种方式延迟最低，但是无法很好地和其他事件循环方式（如 <code>epoll</code>）融合起来，要么将其他事件循环也改成 Busy Loop，要么单开一个线程专门轮询 RDMA，这两种方式各有弊端。为此，ibverbs 提供了一个完成事件通知通道，即 <code>struct ibv_comp_channel</code>，里面含有一个 <code>fd</code>，可以被加入到 <code>epoll</code> 等事件循环中，然后在完成队列有事件时，这个 <code>fd</code> 会被触发可读事件。所以，开发者只需要在这个 <code>fd</code> 被触发读事件时，轮询 RDMA 完成队列即可。具体到 3FS，就是 <code>IOWorker::addIBSocket</code> 这个函数将 RDMA 的 <code>fd</code> 添加到事件循环中的。代码的逻辑是： <code>Transport</code> 作为一个 <code>EventHandler</code> 的派生类，里面包含了 <code>IBSocket</code>，在 <code>EventLoop::add</code> 时，调用了 <code>EventHandler::fd()</code>（此时实际调用 <code>Transport::fd() { return socket_-&gt;fd(); }</code>），从而将 RDMA 的 <code>fd</code> 添加到事件循环中的。</p><p>而 <code>EventLoop::start</code> 就是负责拉起一个线程，循环执行 <code>EventLoop::loop</code> 函数。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Result&lt;Void&gt; <span class="title">EventLoop::start</span><span class="params">(<span class="type">const</span> std::string &amp;threadName)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  thread_ = std::<span class="built_in">jthread</span>(&amp;EventLoop::loop, <span class="keyword">this</span>);</span><br><span class="line">  folly::<span class="built_in">setThreadName</span>(thread_.<span class="built_in">get_id</span>(), threadName);</span><br><span class="line">  <span class="keyword">return</span> Void&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此，网络协程部分链路打通。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简单地介绍了 C++ 20 协程引入的历史和基本思想，为读者提供了分析 C++ 20 协程代码的一些思路，并应用这些思路分析 3FS 中将 RDMA 封装为协程操作的逻辑，以及分析了一些 3FS 中对非 IO 操作的协程操作使用，最后再介绍了事件循环如何驱动所有逻辑的执行，希望能给大家阅读源码提供一些思路和技巧。</p><p>继续阅读：<a href="https://blog.howardlau.me/programming/deepseek-3fs-code-reading-disk-io.html">DeepSeek 3FS 源码解读——磁盘 IO 篇</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>分布式存储入门学习路线</title>
      <link>https://blog.howardlau.me/programming/learning-distributed-storage.html</link>
      <description>
        <![CDATA[<p>分布式存储一方面是从传统的单机存储发展而来，解决单机存储的性能和容量瓶颈，另一方面它最终还是由单机存储引擎通过网络组合成一个大型分布式系统。在此基础之上还需要包装接口，对外提供对象存储、块存储、文件存储、并行存储多种多样的产品形态方便用户使用。</p>
<p>所以学习的话可]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 17 Jan 2025 06:50:01 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>分布式存储一方面是从传统的单机存储发展而来，解决单机存储的性能和容量瓶颈，另一方面它最终还是由单机存储引擎通过网络组合成一个大型分布式系统。在此基础之上还需要包装接口，对外提供对象存储、块存储、文件存储、并行存储多种多样的产品形态方便用户使用。</p><p>所以学习的话可以先从基本的操作系统方面出发，了解单机存储系统上的 IO 栈，先尝试用 QEMU 搭建一个 Linux 内核的调试环境，用 gdb 尝试去打断点，了解一次 read&#x2F;write 系统调用的时候，操作系统是怎么处理一个 IO 请求的。比如了解 ext4 等文件系统的数据在内存和磁盘上是通过什么数据结构组织、索引和缓存的（LSM、B 树、哈希表等），OS 又是最终如何通过硬件驱动、DMA、MMIO、中断等方式去真正地发起读写磁盘（块设备）的命令和接收执行结果等。阿里云盘古的大佬写了系列长文详解：<a href="https://blog.51cto.com/alanwu/category8.html">一个 IO 的传奇一生</a>。有时间的话可以参考 <a href="https://linux-kernel-labs.github.io/refs/heads/master/">https://linux-kernel-labs.github.io/refs/heads/master/</a> 里相关的 Lab（块设备、文件系统），去动手实践一下，印象会更深刻。另外还需要关注一下 SSD、HDD、ZNS SSD、SMR HDD 之类的硬件特点，针对不同的特点又有什么不同的文件系统设计。</p><p>了解了单机的文件系统、块设备的原理之后，分布式存储其实就是想办法将整个 IO 栈拆开，放到不同的机器上去执行，用网络将不同抽象层粘合起来，并且根据实际的功能性能需求修改实现。分布式存储通常需要在读写性能、可用性、可靠性、扩展性、成本之间做取舍。这时候可以了解一下曾经学术界和工业界都提出过什么比较知名的分布式存储系统架构，阅读相关文档和论文，对比一下他们的产品形态（提供的 API）、优缺点和实现原理，开源的系统也可以尝试阅读代码。例如，高性能计算方向的 Lustre、BeeGFS、Intel DAOS，往往是兼容 POSIX 协议和支持 MPI-IO，性能高；大数据时代大名鼎鼎的 Google File System 及其开源实现 HDFS，聚焦于基于大量廉价硬件构建高可用的存储系统，牺牲 POSIX 语义，提供专用 API；云计算底层的 Windows Azure Storage、阿里云盘古，为对象存储、块存储等产品形态提供底层的存储技术底座；还有比较通用的比较经典的 Ceph、GlusterFS、GPFS 等。</p><p>还可以阅读一下<a href="https://book.douban.com/subject/26325526/">《大话存储》</a>这本书，里面涉及到了单机和分布式存储的知识，了解更多的存储技术和发展历史。</p><p>对单机和分布式存储的架构都有了一个大致的了解之后，就可以继续深入研究感兴趣的细节了。一方面可以从存储技术本身出发，比如针对数据可靠性，通常采用副本或者擦除码的方式做冗余，那么就可以研究一下副本之间的一致性用什么算法保证（链式复制、Quorum、Raft、Paxos 等），擦除码有什么空间上和时间上更优的算法（AZC 等）。对于网络互联，现在也出现了很多新型硬件和技术，比如 RDMA（Infiniband、RoCE）、SmartNIC（基于 FPGA、NPU 或者 ARM）、CXL、NVMe-oF 等，可以研究一下基于这些新的网络技术，怎么实现比传统的 TCP&#x2F;IP 更高效的性能。存储硬件也出现了 PMEM（虽然凉了）、NVMe 之类的超低延迟超高性能的存储设备，对于软件栈提出了很高的要求，传统的 IO 和网络栈太深，内存拷贝、上下文切换太多，已经不适应高性能的硬件，就可以研究一下使用 DPDK、SPDK 等用户态的高性能框架去构建单机上的存储引擎，同时单机引擎也会一定程度反过来影响分布式存储的架构。</p><p>也可以从应用方面出发，去了解不同的应用负载对存储系统提出了什么要求，比如现在很火的 AI 大模型，也涉及到数据集的存储和模型权重（Checkpoint）的存储，那么可能就涉及到怎么高效大带宽地从 GPU 读写数据，使得 IO 尽可能不影响模型的训练过程，甚至必要时牺牲一定的可靠性或者一致性进一步压榨性能。对于虚拟化负载（超融合，比如 PVE+Ceph），就可以研究一下怎么通过内核模块等把分布式存储虚拟化成 VM 里的块设备，提供通用性，保障安全性和性能等。另外像 Docker、K8s 之类的容器和调度技术的发展，也催生了一些分布式存储生态上的需求，比如 CSI 存储插件等。</p><p>之后如果想继续探索分布式存储前沿技术，那么就可以多读论文，尝试去做相关方向的实验室学习；或者想投身工业界，就尝试磨炼好工程技术，去微软、AWS、阿里、华为、SmartX 等存储相关的公司实习或者工作，了解商用的分布式存储系统及其生态是怎么构建和交付的。</p><p>分布式存储这块涉及到的技术比较多和杂，随着新硬件新技术新应用的出现还会继续演进出不同的架构和产品形态，我在这里也是抛砖引玉，泛泛列举了一些常见的技术，更深入的学习还需要多阅读论文多动手实践。</p><p>——<a href="https://www.zhihu.com/question/630875908/answer/3298667270">发自我的知乎</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>系统研究者福利——CloudLab 免费裸金属集群使用入门</title>
      <link>https://blog.howardlau.me/programming/using-cloudlab.html</link>
      <description>
        <![CDATA[<h2 id="什么是-CloudLab"><a href="#什么是-CloudLab" class="headerlink" title="什么是 CloudLab"></a>什么是 CloudLab</h2><p>OSDI、FAST 或者 SOSP]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 25 Sep 2023 01:42:27 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="什么是-CloudLab"><a href="#什么是-CloudLab" class="headerlink" title="什么是 CloudLab"></a>什么是 CloudLab</h2><p>OSDI、FAST 或者 SOSP 顶会文章里经常能看到作者是在 <a href="https://cloudlab.us/">CloudLab</a> 上做实验的。这是美国的大学和科研机构联合搭建的裸金属服务器集群，对科研工作者是免费开放使用的。和普通的云服务器不同的是，在上面申请的节点都是裸金属的，没有任何虚拟化，研究人员可以直接以 root 权限操控物理硬件。而且在 <a href="https://docs.cloudlab.us/hardware.html">CloudLab 机型文档</a>可以看到，它提供的硬件种类多样，有带 GPU 的也有带 FPGA 的机型，甚至还有带 BlueField 智能网卡的机器，方便科研人员探索不同的计算机系统架构。每次申请新的节点时都会恢复机器上硬件和软件的配置，所以也不用担心研究过程会对其他人造成影响。如果自己不小心搞砸了配置，也可以直接重新创建节点来恢复初始实验环境。</p><p>由于 CloudLab 提供了统一的硬件配置，所以在上面提供可复现的完整系统也很方便。其他研究人员可以直接在网站上用相同的机型配置创建相同的实验集群，然后使用预设好的磁盘镜像或者脚本就能方便地复现实验了。这样也不需要自己提供服务器让别人连进来做实验了。</p><p>如果你们实验室也有裸金属服务器想像 CloudLab 一样管理，可以参考我之前发布的<a href="https://blog.howardlau.me/programming/openstack-ironic-baremetal.html">OpenStack Ironic 裸金属服务器集群搭建指南</a>用 OpenStack 搭建自己的集群。</p><h2 id="注册使用-CloudLab"><a href="#注册使用-CloudLab" class="headerlink" title="注册使用 CloudLab"></a>注册使用 CloudLab</h2><p>使用 CloudLab 之前需要<a href="https://cloudlab.us/signup.php">注册账号</a>。在 Personal Information 填个人信息，上传 SSH 秘钥方便之后连接服务器，然后在 Project Information 填入需要加入&#x2F;创建 Project 的信息。建议使用 edu 邮箱。</p><p>CloudLab 不是自由注册的，而是需要加入 Project。每个 Project 都有自己的管理员，需要管理员审批之后账号才能生效。如果你所在的实验室已经有人是 Project 的管理员，那么选择 Join Existing Project 然后填上 Project 的名称，通知管理员审批通过即可。</p><p>如果还没有 Project，则选择 Start New Project 创建新的 Project，之后会有 CloudLab 的管理员来审核你的注册请求。ID 填 Project 的名称，以后别人就可以用这个 ID 加入你的 Project。Title 则简单描述你的 Project，比如实验室的名称，或者研究的课题。URL 可以填实验室组织的主页，或者 Github 地址都可以。Description 则是需要详细描述你的 Project 的用途，这里可以简单介绍下你的研究方向或者工作，打算用 CloudLab 做什么研究等等。</p><p>第一个创建 Project 的账户默认就是管理员，可以审批其他加入 Project 的请求。CloudLab 建议是教授或者长期在职的人来创建 Project，否则可能不会通过创建 Project 的申请。</p><h2 id="开始使用-CloudLab"><a href="#开始使用-CloudLab" class="headerlink" title="开始使用 CloudLab"></a>开始使用 CloudLab</h2><p>账号注册并审核通过后就可以登录并使用 CloudLab 的硬件资源了。需要注意的是 CloudLab 不太适合炼丹之类的应用，更多地还是适合系统方向的研究者使用。而且由于 CloudLab 提供了对底层硬件的控制权，有可能你的操作会波及到同一个交换机或者集群的其他用户（比如不小心搞挂了网络或者发起了 DDoS），如果造成严重后果的话会封号。同时 CloudLab 也不能拿来运行 Web 服务或者挖矿等，违规者也会封号。</p><p><a href="/programming/using-cloudlab/cloudlab-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-1.png"></a></p><p>CloudLab 上申请节点是通过创建 Experiment 来实现的。在<a href="https://www.cloudlab.us/instantiate.php">新建实验</a>界面可以选择需要使用的 Profile。Profile 可以理解为对集群的一系列预创建的配置，比如网络拓扑，硬件配置等，想要快速体验的话可以选默认的 small-lan 配置文件，这个是数个节点连接到同一个局域网的配置。</p><p><a href="/programming/using-cloudlab/cloudlab-2.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-2.png"></a></p><p>不同 Profile 有不同可以配置的参数。small-lan 提供了一些基本参数，例如节点数量，系统镜像，指定物理节点机型等。<a href="/programming/using-cloudlab/cloudlab-3.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-3.png"></a>图里是启动 1 个节点，镜像使用 CentOS 8 Stream，并且指定使用 c6525-25g 这个机型。可用机型在 <a href="https://docs.cloudlab.us/hardware.html">CloudLab 机型文档</a>可以查到，也可以<a href="https://www.cloudlab.us/portal-hardware.php">在硬件选择器页面快速选择</a>。由于集群资源有限，可能你指定的节点类型不一定有空闲机器，需要到<a href="https://www.cloudlab.us/resinfo.php">集群资源状态</a>页面查询你想要的机器有没有空闲节点。如果当前时间没有，可以使用<a href="https://www.cloudlab.us/resgroup.php">预约</a>的功能来预留资源。</p><p><a href="/programming/using-cloudlab/cloudlab-4.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-4.png"></a>这里可以填实验的名称，还有针对单独的节点定制一些参数等。确认无误之后就可以点下一步开始实验了。<a href="/programming/using-cloudlab/cloudlab-5.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-5.png"></a></p><p>实验可以在指定时间之后开始（通常是配合预约资源功能使用）也可以马上开始，实验时长默认最长是 16 小时，如果需要更长需要在实验开始之后申请延长。</p><p>之后就会开始实验，分配节点，初始化机器。</p><p><a href="/programming/using-cloudlab/cloudlab-6.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-6.png"></a></p><p>初始化的时间会比较长，一般在 5~10 分钟。启动的时候可以进入节点的 Console 查看启动进程：<a href="/programming/using-cloudlab/cloudlab-7.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-7.png"></a></p><p><a href="/programming/using-cloudlab/cloudlab-8.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-8.png"></a></p><p>如果有别的需要可以在 Console 界面和启动过程交互。</p><p><a href="/programming/using-cloudlab/cloudlab-9.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-9.png"></a></p><p>节点启动完成之后可以直接在 Console 登录到 root 账户操作，或者使用列表里的 ssh 命令连接到服务器。sudo 默认是免密的。之后就可以自由的进行实验了。</p><p>实验在到期之后就会默认释放所有机器，并删除磁盘上的内容，所以如果有代码或者实验结果需要保存的，需要自行上传到云服务或者用 scp 保存下来。在 <code>/proj</code> 目录下也有网络挂载的持久化共享存储可以使用。</p><p>节点默认是连通外网的，下载 Github 或者装 RPM 什么的都飞快。装好软件包之后，为了节省时间，可以创建磁盘镜像，下次启动的时候就可以用这个磁盘镜像快速拉起相同的环境了：</p><p><a href="/programming/using-cloudlab/cloudlab-10.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-10.png"></a></p><p>创建磁盘镜像之后可以在镜像列表查到自己创建的所有镜像，并且会有一个 URN 全局资源标识符（类似 <code>urn:publicid:IDN+utah.cloudlab.us+image+xxxx-PG0:lustre-2.15.3</code>）</p><p><a href="/programming/using-cloudlab/cloudlab-11.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-11.png"></a></p><p>在创建实验的时候，我们可以 Copy Profile，然后修改 small-lan 这个 Profile 的代码，把自己的镜像的 URN 添加进去，之后就能在创建实验的时候使用了：</p><p><a href="/programming/using-cloudlab/cloudlab-12.png" data-fancybox="gallery" data-caption=""><img src="/programming/using-cloudlab/cloudlab-12.png"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>2023 阿里云暑期实习总结</title>
      <link>https://blog.howardlau.me/programming/2023-alibaba-cloud-oss-internship.html</link>
      <description>
        <![CDATA[<p><a href="/programming/2023-alibaba-cloud-oss-internship/IMG_20230607_090230.jpg" data-fancybox="gallery" data-caption=""><img]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 30 Aug 2023 23:48:33 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="/programming/2023-alibaba-cloud-oss-internship/IMG_20230607_090230.jpg" data-fancybox="gallery" data-caption=""><img src="/programming/2023-alibaba-cloud-oss-internship/IMG_20230607_090230.jpg"></a></p><p>2022 年秋招的凌冽寒风刮到了 2023 年的春招实习，经过漫长又折磨的面试流程，终于有幸得到了珍贵的阿里云 OSS 对象存储实习 offer。或许是因为阿里系的集团拆分战略调整，尽管在三月底就完成了面试流程，主管也在四月初给了明确的通过，意向书还是拖到了五月中旬才发出，等入职已经是五月底了。</p><p>当初煎熬的等待过程中，四月中还拿了字节的 SDN 部门实习 offer 以及 RisingWave 的存储引擎实习 offer，对我来说都是很大的诱惑。但最后还是对这个能跟 AWS S3 掰手腕的系统的技术架构更感兴趣，所以还是选择了这个老牌产品。</p><p>在学校的时候也仔细阅读过阿里云在 FAST ‘23 上发表的介绍盘古 2.0 的文章，也曾经投递过盘古部门，不过由于个人感觉盘古过于底层，可能和在学校的科研项目也比较接近，加上论文也已经把架构介绍得很完整了，所以选择了更贴近业务，公开资料也更少的 OSS 部门。</p><p>当然，进来之前也担忧 OSS 作为一个已经沉淀了十多年的成熟产品，新人会不会没有什么可以做的新东西。等真正开始实习并且大致了解 OSS 背后技术原理以及当前的用户需求后，发现其实我的担忧是多余的。哪怕是 OSS 最基本的桶管理以及对象存储以及读取的功能，面对爆发增长的数据量和数据性能需求，也会暴露出老架构的局限性，这里面就有很多性能优化的点可做；而在数据存储之上，有各种各样的增值功能（大部分对标 S3），例如跨区域复制、数据湖生态、HDFS 生态、对象函数计算等功能开发和优化也是还有很多问题等待解决；多种多样的存储介质，磁带、ZNS SSD、SMR HDD 等让存储成本有进一步优化的空间，也带来了软件层面的适配问题；对于专有云、私有云的部署场景也催生了针对小规模硬件集群的软件架构调整需求。所以，十几年的沉淀并不会掣肘开发者，其沉淀的首屈一指的数据规模、五花八门的用户需求反而是 OSS 生命力所在。</p><p>虽然有许多面向用户的需求，但和淘宝之类的系统并不一样，OSS 的需求是可以长期保留的，而不是像购物节大促一样生命周期极短，并且许多需求也具有相当的技术挑战性；而盘古等底层技术又过于接近硬件，脱离了需求后演进较慢，接口变化也不大，更多的时间在性能优化和硬件适配上；EBS、NAS 等则并不提供公网服务，和虚拟机 ECS 服务捆绑得更紧密。另一方面来说，OSS 作为低成本大容量的存储，往往是数据的最终流向。例如虚拟机或者块存储的快照、各种日志的归档、各种媒体文件的存储，即使是现在最火的 AI，数据集和模型权重最终也是会存放在对象存储中长期存储。所以，在可见的未来，对象存储仍有顽强的生命力。</p><p>就和 ChatGPT 极简的用户界面一样，OSS 只需要通过简单的 HTTP 请求就能读写数据，不需要格式化、不需要预分配空间、随存随取、容量无限、成本低；而在最简单的用户交互背后，往往是最复杂的技术在支撑。由于使用了廉价的硬件，如何在不可靠、低性能的硬件上构建出可靠、高效的系统变为一个非常有挑战性的问题。例如，硬盘可能随时达到读写寿命而下架，需要多副本保存数据来保证数据的可用性。但是多副本一方面会浪费空间，另一方面也会影响读写性能，这就需要设计高效的高可用机制，既能够满足数据安全性，又能提供不错的数据读写性能。另外，也需要设计有效的监控系统，及时发现并自动化处理硬件故障。</p><p>由于 OSS 也是直接提供公网服务，这里面也有很多网络安全相关的技术沉淀。和通用的安全防护不一样，OSS 的安全防护有更多结合业务的功能。例如，安全防护可以只针对某个被攻击的 Bucket 生效。</p><p>而对象存储的无限容量，实际上背后是有多套集群支撑的。一方面，机器过保等情况需要大批量下架替换服务器的情况下，需要将原有数据迁移到其他的集群；另一方面加入的新机器也可以参与到原有工作负载的平衡上。对于这些内部迁移的任务，也需要复杂的一致性算法设计来保证用户能够读写到正确的数据版本。</p><p>更多的技术细节可以参考<a href="https://www.doit.com.cn/p/492404.html">《打造具备极致容灾能力的对象存储》</a> 这篇技术分享文章。</p><p>接下来谈谈一些实习的感受吧。我分到的团队是服务层的跨区域复制小组，工作更多的是用 C++ 开发上层的业务逻辑。作为实习生，工作也是以完成小的功能需求、性能优化和修复一些 Bug 为主。跨区域复制牵扯到 Bucket 和 Object 的管理、网络、计算资源的调度，任务 SLA 保障和容错等，开发过程中需要利用到很多数据的底层组织结构还有 KV 存储的原理相关的知识。所以尽管不是做底层的 KV 存储工作，和一般的业务 CRUD 也有所区别。总体来说在整个产品处于一个中后台的位置，能接触到的技术面还是很广的，在工作的过程中慢慢就了解了 OSS 整体的业务和核心技术了，实习体验非常好。</p><p>团队的氛围还是比较融洽和纯粹的，大家的心思都是放在如何把产品做好以及打磨技术上，并不会有网上传言的 PUA 等现象。而我认识的一些在其他团队实习的朋友的体感则并不是那么好。在技术上，也有丰富和详细的技术文档沉淀，新人能快速地了解系统的整体架构和演进历程及其背后的一些设计过程等。由于是基础存储产品，对于代码质量的要求也是十分严格的，Code Review 和测试覆盖率等工程实践都执行得非常贯彻，这也是 OSS 系统稳定性的根基。和在学校写程序或者做项目随便手工测试不同，这边的测试都需要以代码形式固化下来，同时会有严格的回归测试保证新加入的代码不会影响旧的功能。对于新手或者实习生，就会出现开发功能可能只需要一两天，然后花上一两周甚至更长来 CR 还有补全测试。不过，对于代码正确性的保证还只是依赖于 CR 和测试覆盖率，导致线上还是会有一些 bug 的出现，和 S3 采用的形式化验证还有一些先进性上的距离。实习生的权限也很少，看不了内网的技术社区，有点可惜。不过短短几个月能把自己组内的技术细节吃透也已经受益匪浅了。</p><p>工作之余的话，组里偶尔会有聚餐活动，也有每周固定的羽毛球活动，下班时间也基本不谈工作（线上出问题需要 oncall 除外）。阿里云总部现在搬到了云谷园区，是真正意义上的荒郊野岭，周围只有工地、农田和山，也没有地铁，交通不是很便利。吃饭方面，食堂的话中规中矩，菜式还是挺多样的，但味道就确实一般般，价格在 20 元一顿左右；也有 KFC 或者和府捞面等商家入驻，价格就贵一些；园区太荒，也点不到什么外卖；聚餐也基本上要开车去几公里外才有繁华一点的商圈或者饭店。饮料店的话有古茗、星巴克、瑞幸、manner，虽然茶水间没有免费咖啡机，自费买咖啡提提神也是可以的。晚饭时间有 20 元餐券，可以去食堂吃，也可以拿面包牛奶走人，过时作废，不能累计。夜宵则是有 10 元餐券，同样是过时作废。总的来说，能满足大部分吃喝需求，不过选择不多。不知道以后那边会不会繁华起来。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 C++ 20 协程封装 UCX</title>
      <link>https://blog.howardlau.me/programming/ucx-with-cpp-20-coroutines.html</link>
      <description>
        <![CDATA[<p>@王润基 在 <a href="https://zhuanlan.zhihu.com/p/397199431">这篇文章</a> 里介绍了如何使用 Async Rust 封装 UCX 通信库。在那篇文章中，rjgg 已经介绍过 UCX 的机制，这里就不再重复。和 Rust]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 20 Aug 2022 01:45:08 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>@王润基 在 <a href="https://zhuanlan.zhihu.com/p/397199431">这篇文章</a> 里介绍了如何使用 Async Rust 封装 UCX 通信库。在那篇文章中，rjgg 已经介绍过 UCX 的机制，这里就不再重复。和 Rust 相提并论的 C++ 20 中也提供了 <code>co_await</code> 等异步编程语法关键词，我们可不可以也用它来封装异步通信、降低编程复杂度，而性能开销又有多大呢？答案是：可以，而且是零开销封装！</p><p>和 Rust 的异步 Future 需要一个 Runtime 负责 poll 不同，C++ 的协程仅仅是帮助编译器进行代码变换的一个语法，也就是说 C++ 的协程实际上是不需要借助类似 Tokio 等通用运行时来驱动协程运行，而是可以完全由用户决定运行的时机。正如<a href="https://blog.howardlau.me/programming/writing-rdma-with-cpp20-coroutines.html">使用 C++ 20 封装 RDMA 操作</a>中提到的，我们只需要开启一个轮询线程，不断地轮询事件的发生，然后调用预先定义好的回调函数，就能让协程运行起来了。</p><p>具体到 UCX，这个驱动协程运行的函数就是 <code>ucp_worker_progress</code> ，通过不断调用这个函数，UCX 会检查 IO 事件是否完成，并调用我们发起请求时注册的回调函数。我们只需要将编译器提供的继续运行被暂停协程的函数地址保存到上下文中，并且在回调函数中调用继续运行的函数，就能驱动协程的运行了。UCX 中提供了一系列 <code>*_nbx</code> 函数用来发起异步操作。这些函数调用之后不会阻塞，如果操作没有完成，就返回一个地址，我们后续可以通过轮询这个地址的状态，来检查我们的操作是否完成。也就是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ucp_request_param_t</span> param = &#123;&#125;;</span><br><span class="line"><span class="comment">// ... </span></span><br><span class="line"><span class="keyword">auto</span> request = ucp_*_nbx(..., &amp;param); </span><br><span class="line"><span class="keyword">while</span> (<span class="built_in">ucp_request_check_status</span>(request) != UCS_OK) </span><br><span class="line">  <span class="built_in">ucp_worker_progress</span>(worker);</span><br></pre></td></tr></table></figure><p>但很显然，如果我们使用这种阻塞的方法去轮询的话，并发度会很低，效率肯定不高。为了充分发挥 UCX 的异步性能，我们需要想办法把 C++ 提供的协程语法和回调机制结合起来。正如<a href="https://zhuanlan.zhihu.com/p/553140760">使用 C++ 20 封装 RDMA 操作</a>中提到的，所有的异步操作的最终实现形态就是 <code>awaitable</code>，那么实现思路就很清晰了</p><ul><li>在构造函数中，我们保存此次操作需要的参数。</li><li>在 <code>await_ready</code> 中发起操作，将当前 <code>awaitable</code> 指针保存到请求上下文。如果操作立刻完成了，那就可以节省一次打包状态的开销。</li><li>在 <code>await_suspend</code> 中，我们简单的将 <code>coroutine_handle</code> 保存到 <code>awaitable</code> 中。</li><li>在 <code>await_resume</code> 中，将操作的结果返回。对于 <code>stream_recv</code>，需要返回接收的字节数。对于 <code>tag_recv</code> 需要返回接收的字节数和发送方的 tag。</li></ul><p>另一个需要注意的地方是，每次需要发起操作的时候都需要构造 <code>awaitable</code> 并调用相关函数，也就是 <code>awaitable</code> 处于热点路径上，我们必须尽可能减小 <code>awaitable</code> 的尺寸，减少代码分支，并且在传参的时候尽量避免开销大的复制操作（例如 <code>shared_ptr</code> 的复制，以牺牲安全性为代价换取性能）。</p><p>最终封装完成的代码见：<a href="https://github.com/howardlau1999/ucxpp">https://github.com/howardlau1999/ucxpp</a> </p><p>目前支持的功能有：</p><ul><li>UCP：Tag&#x2F;Stream Send&#x2F;Recv、RMO</li></ul><h2 id="性能测试"><a href="#性能测试" class="headerlink" title="性能测试"></a>性能测试</h2><p>实现完成后，我们来看看 UCX++ 的性能如何。使用协程，可以很方便地就写出<a href="https://github.com/howardlau1999/ucxpp/blob/master/examples/perftest.cc">高并发的测试程序</a>。</p><p>测试使用了两台物理服务器进行，它们的软硬件配置相同。测试环境详情：</p><ul><li>OS: Ubuntu 22.04 (5.15.0-46-generic)</li><li>CPU: Intel(R) Xeon(R) Gold 6230N CPU (1 socket, 20 physical cores, 40 logical cores)</li><li>RAM: 192 GB DDR4 2666</li><li>HCA: Mellanox ConnectX-4 Infiniband 56G</li><li>Compiler: gcc (Ubuntu 11.2.0-19ubuntu1)</li><li>UCX: v1.13.0 (<code>./contrib/configure-release</code>)</li><li>Infiniband Driver: MLNX_OFED_LINUX-5.6-2.0.9.0</li></ul><p>这里就简单测试一下小包和大包的性能。Baseline 就是 UCX 附带的 <code>ucx_perftest</code>，这里运行的是 <code>tag_bw</code> 测试，测试方法是客户端不停向服务端发起 <code>tag_send</code> 请求，是单向的测试。对于 8B 和 256B 的测试，并发数为 1，对于单次发送 4K 和 64K 的测试，并发数为 32。测试程序全部都是单线程的，通过 <code>taskset</code> 以及内核的 <code>isolcpus</code> 选项单独隔离一个物理核进行测试。</p><p><a href="/programming/ucx-with-cpp-20-coroutines/plot_8.png" data-fancybox="gallery" data-caption=""><img src="/programming/ucx-with-cpp-20-coroutines/plot_8.png"></a></p><p><a href="/programming/ucx-with-cpp-20-coroutines/plot_256.png" data-fancybox="gallery" data-caption=""><img src="/programming/ucx-with-cpp-20-coroutines/plot_256.png"></a></p><p><a href="/programming/ucx-with-cpp-20-coroutines/plot_4096.png" data-fancybox="gallery" data-caption=""><img src="/programming/ucx-with-cpp-20-coroutines/plot_4096.png"></a></p><p><a href="/programming/ucx-with-cpp-20-coroutines/plot_65536.png" data-fancybox="gallery" data-caption=""><img src="/programming/ucx-with-cpp-20-coroutines/plot_65536.png"></a></p><p>可以看到基本上 IOPS 差距为 1% 以内，这其中还有计时方法的影响，所以可以放心使用 UCX++ 而不用担心性能问题。</p><p><del>重铸 C++ 荣光，我辈义不容辞！</del></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 C++20 协程实现 RDMA 操作</title>
      <link>https://blog.howardlau.me/programming/writing-rdma-with-cpp20-coroutines.html</link>
      <description>
        <![CDATA[<p>C++ 20 中的协程非常适合封装异步操作，可以像 JavaScript 或者 Rust]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 12 Aug 2022 02:09:24 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>C++ 20 中的协程非常适合封装异步操作，可以像 JavaScript 或者 Rust 那样按照顺序的方法去编写异步代码。没有协程的时候，异步操作往往是通过回调函数的方式来实现的。这就要求我们手动将程序的状态保存起来，然后在回调操作的时候重新恢复之前的执行状态。对于一些简单的操作而言，手动保存状态也可以接受，但是这不 scalable。当一个函数涉及到很多异步操作的话，手动管理保存状态就会显得很繁琐。此外，由于操作被拆分成多个回调函数，在编写代码的时候逻辑会显得比较零散，既不方便编写，也不方便阅读。</p><p>协程实际上就是将状态的打包和回调函数的编写交给编译器来实现，这样程序员就可以用顺序而且紧凑的方法去编写异步代码了。C++ 20 中的协程比起其他语言的协程实现要复杂许多，而且不像其他语言有一个比较广泛使用的运行时库。换句话说，如果你想使用 C++ 20 的协程，需要按照你自己的应用需求去编写一个驱动协程运行的运行时。这个运行时做的事情并不复杂，只需要调用编译器打包好的回调函数就可以了，理论上使用什么作为运行时都无所谓。可以是 <code>epoll</code> 事件循环，可以是一个线程池，也可以是 RDMA。</p><p>本文提供了一种基于 <code>epoll</code> 和 <code>ibverbs</code> 编写的 RDMA 操作库实现思路，将复杂的 RDMA 操作封装成基于协程的函数，大大提升了程序的可读性。其中 <code>epoll</code> 方面的作用类似 <code>rdmacm</code>，是在 Queue Pair（类似 TCP Socket）建立过程中使用 TCP 交换 QP 信息的。</p><p>完整代码见：<a href="https://github.com/howardlau1999/rdmapp">https://github.com/howardlau1999/rdmapp</a></p><h2 id="C-20-协程入门"><a href="#C-20-协程入门" class="headerlink" title="C++ 20 协程入门"></a>C++ 20 协程入门</h2><p>网上关于 C++ 20 的协程资料比较多而且杂，但是都比较专注于某一个点的细节解析，比较少结合实际场合讲解的。正如前文所说，C++ 的协程实际上是编译器对我们的程序做的一种变换。我们使用协程语法，更像是在使用一种非常高级的宏，告诉编译器应该如何变换。例如，使用协程编写 TCP 服务器程序，伪代码可以理解为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">handle_connection</span><span class="params">(tcp_connection conn)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">    <span class="type">char</span> buffer[<span class="number">4096</span>];</span><br><span class="line">    <span class="type">int</span> n = <span class="keyword">co_await</span> conn.<span class="built_in">recv</span>(buffer, <span class="number">4096</span>);</span><br><span class="line">    <span class="keyword">if</span> (n == <span class="number">0</span>) <span class="keyword">co_return</span>; <span class="comment">// Connection closed</span></span><br><span class="line">    <span class="comment">// Process buffer... </span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>recv</code> 函数签名是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">tcp_connection::recv_awaitable <span class="title">tcp_connection::recv</span><span class="params">(<span class="type">char</span> *buffer, <span class="type">size_t</span> length)</span> </span></span><br></pre></td></tr></table></figure><p>那么对于 <code>co_await</code> 那一行，实际上编译器会将程序转换为以下一系列操作：</p><ol><li>调用 <code>recv</code> 函数，这个函数返回一个类 <code>awaitable</code>。一个 <code>awaitable</code> 可以理解为一个 <code>promise</code>，也就是一个可能完成了，也可能没有完成的异步操作。一个 <code>awaitable</code> 无需继承任何类，只需要实现以下三种方法：</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">recv_awaitable</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="comment">// 这里的返回值类型是什么后面会讲到</span></span><br><span class="line">  ??? <span class="built_in">await_suspend</span>(std::coroutine_handle&lt;&gt; h);</span><br><span class="line">  <span class="function"><span class="type">int</span> <span class="title">await_resume</span><span class="params">()</span></span>; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ol start="3"><li>然后，调用 <code>awaitable</code> 的 <code>await_ready</code> 函数，检查这个异步操作完成了没有。如果返回 <code>true</code>，就不用进行一系列复杂的打包操作了，继续执行。</li><li>重点来了，如果 <code>await_ready</code> 返回 <code>false</code>，也就是这个异步操作还没完成（例子里就是 TCP 还没数据可读），那么编译器会将函数还没执行完成的部分以及需要使用的状态（例如 buffer 和一些局部变量）打包成一个 <code>coroutine_handle</code>，我们不需要关心里面具体是怎么实现的，只需要知道，这个 <code>coroutine_handle</code>，有一个 <code>resume</code> 方法，用来执行函数还没执行完的部分，以及 <code>done</code> 方法，检查还有没有需要执行的部分。所以很简单，无论我们使用的库是什么，只需要在事件处理函数去调用 <code>resume</code> 方法就可以了！</li><li>那么这个 <code>coroutine_handle</code> 要怎么获取到？答案就是 <code>await_suspend</code> 方法，它的签名可以是：</li></ol> <figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; h)</span></span>;</span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; h)</span></span>;</span><br><span class="line">std::coroutine_handle&lt;&gt; <span class="built_in">await_suspend</span>(std::coroutine_handle&lt;&gt; h);</span><br></pre></td></tr></table></figure><p>第一次看到这些签名，一个疑惑就是为什么可以有参数相同返回值不一样的签名？还有一个疑惑就是 <code>coroutine_handle&lt;&gt;</code> 是什么意思？</p><p>对于第一个疑问，答案就是，这几个签名只能选择一种实现在 <code>awaitable</code> 里。编译器在编译的过程中，是可以分辨出不同返回值的签名的。根据返回值类型的不同，它们对于后续协程运行过程也有不同的影响。</p><p>对于第二个疑问，这其实是 C++ 中的一种编程技巧：类型擦除。这是因为实际上每一个协程的”剩余部分“都是不同的类型，而我们又不想用虚函数这种对性能有影响的方法，那么就可以将具体的类型给”擦除“掉，变成同一种类型，从而也可以使用统一的接口。</p><p>回到 <code>await_suspend</code> 的三幅面孔，不同的返回类型的作用是：</p><ul><li>对于 <code>void</code> 类型，当前函数（例子是 <code>recv</code>）的执行就到此为止了，直到下次调用 <code>h.resume()</code> 方法才会继续执行</li><li>对于 <code>bool</code> 类型，含义和 <code>await_ready</code> 是相反的，如果返回了 <code>false</code>，函数就不会暂停，会继续执行，适合调用 API 失败的时候使用，否则就和 <code>void</code> 一样，暂停执行</li><li>对于 <code>std::coroutine_handle&lt;&gt;</code> 类型，会调用这个返回了的协程类型的 <code>resume</code> 方法</li></ul><p>那么我们需要在 <code>await_suspend</code> 方法里做的事就很清楚了，也就是调用其他库，把调用 <code>h.resume()</code> 作为回调函数注册到事件处理函数中。</p><p>最后，无论这个协程是在 <code>await_ready</code> 返回 <code>true</code> 后继续执行，又或者是 <code>await_suspend</code> 之后被继续执行了，都会在继续执行的一开始调用 <code>await_resume</code> 函数，并且把它的返回值作为整个 <code>co_await</code> 的返回值。</p><p>梳理下来，<code>recv_awaitable</code> 的实现思路就很清楚了：在 <code>await_ready</code> 里我们先试着 <code>recv</code> 一下，返回 <code>EAGAIN</code> 或者 <code>EWOULDBLOCK</code> 的话我们就返回 <code>false</code>，让编译器把协程抓手传递给我们，我们在 <code>await_suspend</code> 里把相关的 fd 和这个抓手的地址注册到 epoll 里。最后，在 <code>await_resume</code> 里，检查我们需不需要再 <code>recv</code> 一次，再把 <code>recv</code> 返回值返回即可。</p><p>伪代码就是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="type">int</span> epfd;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">recv_awaitable</span> &#123;</span><br><span class="line">  <span class="type">int</span> fd_;</span><br><span class="line">  <span class="type">int</span> n_;</span><br><span class="line">  <span class="type">char</span> *buffer_;</span><br><span class="line">  <span class="type">size_t</span> length_;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="built_in">recv_awaitable</span>(<span class="type">int</span> fd, <span class="type">char</span> *buffer, <span class="type">size_t</span> length) : <span class="built_in">fd_</span>(fd), <span class="built_in">buffer_</span>(buffer), <span class="built_in">length_</span>(length) &#123;&#125;</span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    n_ = ::<span class="built_in">read</span>(fd_, buffer_, length_);</span><br><span class="line">    <span class="keyword">return</span> n_ &gt;= <span class="number">0</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; h)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">epoll_event</span> event;</span><br><span class="line">    event.events = EPOLLIN;</span><br><span class="line">    event.data.ptr = h.<span class="built_in">address</span>();</span><br><span class="line">    ::<span class="built_in">epoll_ctl</span>(epfd, EPOLL_CTL_ADD, fd_, &amp;event);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function"><span class="type">int</span> <span class="title">await_resume</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (n_ &lt; <span class="number">0</span>) &#123;</span><br><span class="line">      ::<span class="built_in">epoll_ctl</span>(epfd, EPOLL_CTL_DEL, fd_, <span class="literal">nullptr</span>);</span><br><span class="line">      n_ = ::<span class="built_in">read</span>(fd_, buffer_, length_);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> n_;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>而在事件循环中，我们像往常一样，不停 <code>epoll_wait</code>，然后调用事件的回调函数即可：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (;;) &#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">epoll_event</span> event;</span><br><span class="line">  ::<span class="built_in">epoll_wait</span>(epfd, &amp;event, <span class="number">1</span>);</span><br><span class="line">  <span class="keyword">auto</span> h = std::coroutine_handle&lt;&gt;::<span class="built_in">from_address</span>(event.data.ptr);</span><br><span class="line">  h.<span class="built_in">resume</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，我们只需要对代码进行一点点封装，就可以享受到使用协程编程的便利了。封装的方法就是将以前的 <code>epoll_ctl</code> 等系统调用封装成 <code>awaitable</code> 即可，这样经验也能轻松迁移。</p><p>当然，还有一个大问题没有解决，就是 <code>handle_connection</code> 这种调用了 <code>co_await</code> 的函数应该返回什么？我们也没有像普通函数一样使用 <code>return</code>，而是使用了 <code>co_return</code>，这又会带来什么不同？</p><p>首先，既然它能被 <code>co_await</code>，那么这个函数的返回值一定也需要提供上面说的 <code>awaitable</code> 的三个接口，否则编译器无法完成变换。而且，它也可以被“暂停执行”，也会被编译器打包成 <code>std::coroutine_handle&lt;&gt;</code>，同时，由于函数有返回值，也可能抛出异常，我们还需要更多接口去处理。</p><p>对于这种情况，我们需要返回一种 <code>task</code> 类型，它同样不需要继承任何类，需要实现的接口有：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">task</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">promise_type</span> &#123;</span><br><span class="line">    <span class="function">task&lt;T&gt; <span class="title">get_return_object</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> std::coroutine_handle::<span class="built_in">from_promise</span>(*<span class="keyword">this</span>); &#125;</span><br><span class="line">    <span class="function">awaitable <span class="title">initial_suspend</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function">awaitable <span class="title">final_suspend</span><span class="params">()</span></span>;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">return_value</span><span class="params">(T &amp;&amp;value)</span></span>;</span><br><span class="line">    <span class="comment">// 或者 </span></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">return_void</span><span class="params">()</span></span>;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="function">task_awaitable <span class="keyword">operator</span> <span class="title">co_await</span><span class="params">()</span></span>;</span><br><span class="line">  <span class="built_in">task</span>(std::coroutine_handle&lt;promise_type&gt; h) : <span class="built_in">h_</span>(h) &#123;&#125;</span><br><span class="line">  ~<span class="built_in">task</span>() &#123; h_.<span class="built_in">destroy</span>(); &#125;</span><br><span class="line">  std::coroutine_handle&lt;promise_type&gt; h_;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>其中一定要包含一个类型 <code>promise_type</code>，编译器才能做变换，变换后的结果大概是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    promise-type promise promise-constructor-arguments ;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">co_await</span> promise.<span class="built_in">initial_suspend</span>() ;</span><br><span class="line">        function-body</span><br><span class="line">    &#125; <span class="built_in">catch</span> ( ... ) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!initial-await-resume-called)</span><br><span class="line">            <span class="keyword">throw</span> ;</span><br><span class="line">        promise.<span class="built_in">unhandled_exception</span>() ;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="keyword">final</span>-suspend :</span><br><span class="line">    <span class="keyword">co_await</span> promise.<span class="built_in">final_suspend</span>() ;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>initial_suspend</code> 的作用是允许函数在开始执行之前先用一个 <code>awaitable</code> 暂停一下，比如你想先创建好任务，再一起运行。而 <code>return_value</code> 和 <code>return_void</code> 就如字面意思，在 <code>co_return</code> 的时候会调用。在返回前，还会再调用 <code>final_suspend</code>，也可以用 <code>awaitable</code> 暂停执行。这是为了可以做一些收尾工作，同时也可以嵌套调用协程。</p><p>这里的 <code>task_awaitable</code> 实现接口虽然相同，但是具体实现则不太一样。首先 <code>await_ready</code> 直接检查 <code>h_.done()</code>，而 <code>await_suspend</code> 则需要把这个 <code>h_</code> 保存到某个位置，我们可以给 <code>promise_type</code> 添加一个成员变量 <code>continuation_</code> 来保存，然后在 <code>final_suspend</code> 的时候返回出去，继续嵌套的协程。<code>await_resume</code> 则不需要实现什么。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">task</span> &#123;</span><br><span class="line">  <span class="keyword">class</span> <span class="title class_">task_awaitable</span> &#123;</span><br><span class="line">    std::coroutine_handle&lt;task&lt;T&gt;&gt; h_;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> h_.<span class="built_in">done</span>(); &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; suspended)</span> </span>&#123;</span><br><span class="line">      h_.<span class="built_in">promise</span>().continuation_ = suspended;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_resume</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>而这个 <code>continuation_</code> 可以在 <code>final_suspend</code> 的时候返回出去，让它能够继续执行：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">promise_type</span> &#123;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">  <span class="function"><span class="keyword">auto</span> <span class="title">final_suspend</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">awaiter</span> &#123;</span><br><span class="line">      <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line">      std::coroutine_handle&lt;&gt;</span><br><span class="line">      <span class="built_in">await_suspend</span>(CoroutineHandle suspended) <span class="keyword">noexcept</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (suspended.<span class="built_in">promise</span>().continuation_) &#123;</span><br><span class="line">          <span class="keyword">return</span> suspended.<span class="built_in">promise</span>().continuation_;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="keyword">return</span> std::<span class="built_in">noop_coroutine</span>();</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      <span class="function"><span class="type">void</span> <span class="title">await_resume</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;&#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">return</span> awaiter&#123;&#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这样，完整可用的协程运行时就编写完成了。代码量其实并不大，主要难度在于理解编译器是如何变换我们的程序，一旦理解后就可以轻松自如地编写协程程序了！</p><h2 id="RDMA"><a href="#RDMA" class="headerlink" title="RDMA"></a>RDMA</h2><p>RDMA 编程也和 <code>epoll</code> 类似，有一个循环不停地获取“完成事件”，这个完成事件中同样可以携带一个指针，利用这个指针，就可以完成我们的回调操作了，需要注意的是一般我们不在 poll 线程直接执行回调，而是放到别的线程去做，避免影响 poll：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (;;) &#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">ibv_wc</span> wc;</span><br><span class="line">  <span class="keyword">if</span> (<span class="keyword">auto</span> n = ::<span class="built_in">ibv_poll_cq</span>(cq, &amp;wc, <span class="number">1</span>); n &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">auto</span> cb = <span class="built_in">reinterpret_cast</span>&lt;callback_ptr&gt;(wc.wr_id);</span><br><span class="line">    executor-&gt;<span class="built_in">run</span>(cb);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而 <code>recv_awaitable</code> 则和上面的 <code>epoll</code> 实现思路大同小异，在 <code>await_suspend</code> 函数中，发布一个 <code>recv</code> 操作（<code>send</code> 同理），把回调地址传到 <code>wr_id</code> 里就可以了。 </p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> qp::send_awaitable::<span class="built_in">await_suspend</span>(std::coroutine_handle&lt;&gt; h) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">auto</span> callback = executor::<span class="built_in">make_callback</span>([h, <span class="keyword">this</span>](<span class="keyword">struct</span> ibv_wc <span class="type">const</span> &amp;wc) &#123;</span><br><span class="line">    wc_ = wc;</span><br><span class="line">    h.<span class="built_in">resume</span>();</span><br><span class="line">  &#125;);</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  send_wr.wr_id = <span class="built_in">reinterpret_cast</span>&lt;<span class="type">uint64_t</span>&gt;(callback);</span><br><span class="line">  qp_-&gt;<span class="built_in">post_send</span>(send_wr, bad_send_wr);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意回调函数的生命周期起码要在回调结束之后才能结束。</p><h2 id="扩展"><a href="#扩展" class="headerlink" title="扩展"></a>扩展</h2><p>如果想把协程放到后台去运行（类似 <code>tokio::spawn</code> 或者 <code>thread::detach</code> ），我们可以利用 <code>std::promise</code> 和 <code>std::future</code>，在协程运行结束时通过 <code>promise</code> 来设置值，调用方则使用 <code>future</code> 来等待执行完成或者获取返回值。而为了避免 <code>task</code> 被提前销毁，我们需要将其移动到堆上：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">detach</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> detached_task = <span class="keyword">new</span> <span class="built_in">task</span>&lt;<span class="type">void</span>&gt;(std::<span class="built_in">move</span>(*<span class="keyword">this</span>));</span><br><span class="line">  h_.<span class="built_in">promise</span>().<span class="built_in">set_detached_task</span>(detached_task);</span><br><span class="line">  detached_ = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于已经被放到后台的任务，我们在 <code>~task</code> 里就不销毁 <code>coroutine_handle</code> 了，而是在 <code>final_suspend</code> 后去注意释放 <code>task</code> 的内存即可。由于一旦注册回调之后，协程就会在事件循环的驱动下不断执行，所以我们也不需要轮询 <code>task</code> 的状态，如果想在完成后收到通知，用 <code>std::future</code> 就足够了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 libav 实现四人同屏直播效果</title>
      <link>https://blog.howardlau.me/programming/libav-multi-stream-broadcasting.html</link>
      <description>
        <![CDATA[<p>前几天加的一个学校原神群里想趁原神躲猫猫活动之前在群里举办一个躲猫猫大赛，一开始是想大家简单各自比一比，然后记录分数就好。不过这样就太没意思了，躲猫猫活动里经常有人有骚操作，如果能够直播就好了。而且躲猫猫是四个人玩，观众如果能看到猎手和游侠的位置，就更有意思了。按理来说，如]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 08 Aug 2022 04:16:46 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>前几天加的一个学校原神群里想趁原神躲猫猫活动之前在群里举办一个躲猫猫大赛，一开始是想大家简单各自比一比，然后记录分数就好。不过这样就太没意思了，躲猫猫活动里经常有人有骚操作，如果能够直播就好了。而且躲猫猫是四个人玩，观众如果能看到猎手和游侠的位置，就更有意思了。按理来说，如果大家都在校园网内，那么直播是很简单的，我只需要在我的电脑上开一个 OBS，然后让大家推流到我的电脑，我再推流合成后的画面，但是此时已经是寒假，很多人已经回家了，所以这个办法马上就被否决了。大家此时又想到了用会议软件来实现多人同屏，可是目前国内常用的飞书还有腾讯会议都不支持多人同时共享屏幕，而 Zoom 虽然支持，但并不是每个人都能方便地连接上 Zoom。因此，最终只能选择开一个云服务器，然后让云服务器接收大家的推流之后再推到 B 站即可。按理来说，我只需要开一个 Windows 的服务器，在上面安装一个 OBS，再安装一个 RTMP 服务器就大功告成了。可是作为程序员怎么能不用 Linux！但是我之前并没有音视频处理的经验，也想借这个机会学习一下音视频编程的知识，于是，我开始了研究在 Linux 下进行音视频处理的踩坑之路……</p><p><a href="/programming/libav-multi-stream-broadcasting/1659960797630.png" data-fancybox="gallery" data-caption=""><img src="/programming/libav-multi-stream-broadcasting/1659960797630.png"></a></p><p>最终的效果：<a href="https://www.bilibili.com/video/BV1vT4y1y7ve">https://www.bilibili.com/video/BV1vT4y1y7ve</a></p><p>完整代码：<a href="https://github.com/howardlau1999/live/blob/main/live.cpp">https://github.com/howardlau1999/live/blob/main/live.cpp</a> </p><p>总结一下需求就是，需要实现四人直播同屏，且四个人是异地的，都从公网推流，并且希望延迟尽可能低。这个看上去很简单的需求其实有很多细节需要注意处理。</p><p>当然，在 Linux 下处理音视频也是有现成的工具的，那就是 ffmpeg。经过一番简单的搜索，发现使用 ffmpeg 命令行工具就可以实现需求，只需要使用一个稍微复杂的滤镜即可。但是经过测试之后发现，这个方法虽然简单，但是稳定性却很差，如果四个流其中一个发生卡顿，会造成整个画面都会卡顿，而且经常出现时空回溯的现象。也就是说，ffmpeg 命令行工具是要等所有的输入都有响应之后才会处理下一帧。考虑到大家都是公网推流，且大家的网络以及设备高度不可控，这样的稳定性是无法接受的。所以，一个很自然的想法就是在拉流解码端和编码推流端加入一个中间层，来缓冲数据流不同步的问题。我开始研究能不能使用一些命令行工具组合来提高稳定性。首先想到的就是 mkfifo 命名管道，先用四个拉流的程序拉取每个人的直播流并解码，然后往四个命名管道写入数据，然后编写一个程序从四个管道读取数据并缓存，然后写入到新的命名管道中，如果某个拉流管道没有新的数据，就不断写入缓存中的数据。负责整合画面的推流程序则不断从管道中读取数据。程序很简单，很快就编写完毕了，但实际测试发现并没有什么用，画面反而卡顿得更厉害了，这个简单的思路行不通。而且，我们无法知道写入管道里的数据是不是刚好是一帧数据。所以，最后不得不采用了重量级的解决办法——自己调用 ffmpeg 的库函数写一个拉流推流程序。</p><p>ffmpeg 这个命令行工具实际上是 libavformat 等库函数的封装，所以，我们首先得安装 libavformat 等库。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install libavformat-dev libavdevice-dev libavcodec-dev libavutil-dev libavfilter-dev libswscale-dev</span><br></pre></td></tr></table></figure><p>平时我们见到的 mp4、flv 等文件实际上是一个“容器”，也就是说它只负责将已经编码好的视频和音频合并起来，而不负责编码，关于封装格式的处理，都是由 libavformat 处理的。而具体到音视频的编解码则是由 libavcodec 来处理，例如，我们网上的直播视频通常采用的是 H.264 编码，而音频则是使用 AAC 进行编码。当视频被解码之后得到的原始数据则可以使用 libavfilter 进行一些处理，比如缩放、旋转、叠加文字等等。</p><p>关于音视频部分的编解码，其实没有什么好说的，直接传入自己需要的参数给编码器，然后使用函数从编解码器获取需要的数据即可。</p><p>在新版本的 ffmpeg&#x2F;libav 库中，基本上音视频数据都被抽象成 <code>packet</code> 和 <code>frame</code> 这两个数据结构了。可以认为 <code>packet</code> 对应的是原始数据，也就是编码后的数据，而 <code>frame</code> 则是解码后的数据。无论是将 RAW 的数据用 avcodec 编码成 H.264 或者 AAC，还是将编码好的 H.264 和 AAC 用 avformat 封装成 FLV 格式，都是统一采用的 <code>frame</code> 和 <code>packet</code> 来进行数据的传输。这里需要注意的是，一些函数会帮你释放 <code>frame</code> 和 <code>packet</code>，一些则会拷贝数据，在开发的时候，建议先不要写 <code>free</code> 和 <code>unref</code> 的函数，也就是任他内存泄漏，等跑通了之后，再去仔细地添加释放函数。</p><p>在解码之后，将 <code>frame</code> 传给 <code>avfilter</code> 由它来合并画面之后，将输出的 <code>frame</code> 重新编码为 <code>packet</code> 推给 B 站 RTMP 服务器。</p><h2 id="服务器搭建"><a href="#服务器搭建" class="headerlink" title="服务器搭建"></a>服务器搭建</h2><p>服务器除了视频合成程序以外，还需要启动一个 RTMP 服务器来接收客户端的推流，然后视频合成程序再从这个服务器进程分别把流拉下来解码处理，然后再推送给 B 站。RTMP 服务器可以使用 nginx，我这里直接使用了 OpenSRS，只需要一行命令就能启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> -it -p 1935:1935 -p 1985:1985 -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5 ./objs/srs -c conf/realtime.conf</span><br></pre></td></tr></table></figure><p>客户端推流，移动端使用的是 Larix Screencaster，体积很小而且没有广告，App Store 和 Google Play 都可以下载，电脑端就还是使用 OBS Studio。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>可以看到直接使用 ffmpeg&#x2F;libav 库来操作音视频编解码还是比较底层的，而且也比较繁琐，如果只是单纯的想合成画面之类的，可能使用 libobs 会更好一些，但是没有时间去研究了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>OpenStack Ironic 裸金属服务搭建</title>
      <link>https://blog.howardlau.me/programming/openstack-ironic-baremetal.html</link>
      <description>
        <![CDATA[<p>实验室有二十台 R740 服务器给大家用，由于涉及到内核以及硬件方面的实验，时间一长的话不同服务器上的环境就很混乱，运维管理也很麻烦，并且虚拟化不适合我们的场景。之前 OSDI 22 AE 提供了 <a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 19 Jul 2022 04:44:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>实验室有二十台 R740 服务器给大家用，由于涉及到内核以及硬件方面的实验，时间一长的话不同服务器上的环境就很混乱，运维管理也很麻烦，并且虚拟化不适合我们的场景。之前 OSDI 22 AE 提供了 <a href="https://cloudlab.us/">https://cloudlab.us</a> 的账号，这个网站上面就是像虚拟机一样使用物理机器，可以自由组网、选择操作系统镜像直接启动、给服务器做镜像等，体验相当不错。但是他们的系统并不开源，所以我想看看有没有办法用开源的软件像他们一样直接管理裸金属服务器。</p><p>OpenStack Ironic 就是基于 OpenStack 生态的裸金属计算服务，但是网上资料比较少，虽然官方文档已经很详细了，但是针对到自己特定的需求的话还是有不少坑需要解决。这篇文章记录了搭建 Ironic 的过程，记录一下参数的配置以及如何调整具体服务的思路，也是给 Ironic 生态添加一点资料吧。</p><h2 id="前提条件"><a href="#前提条件" class="headerlink" title="前提条件"></a>前提条件</h2><p>想要搭建 Ironic，服务器必须支持带外管理 (Out-of-band management)，也就是就算不运行操作系统也能对服务器的电源、BIOS 设置等进行调整。一般的商业服务器都提供了这种功能，例如 Dell 的 iDRAC。然后需要有一台服务器作为控制节点长期运行，并且能够连接远程管理接口，通过这个管理接口远程操作其他机器。控制节点由于需要设置网络等，需要有 root 权限。</p><p>元数据的话通过 HTTP API 提供，可以保存在其他机器，但是需要确保裸机的控制节点可以访问这个 API 服务器，裸机本身也需要访问。也可以 API 服务器和控制节点都在同一台服务器，按照自己需求确定拓扑。</p><h3 id="参考网络拓扑"><a href="#参考网络拓扑" class="headerlink" title="参考网络拓扑"></a>参考网络拓扑</h3><p><a href="/programming/openstack-ironic-baremetal/topo-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/openstack-ironic-baremetal/topo-1.png"></a></p><p>上图是一种可行的网络拓扑，其中 API 和 DB 服务器和普通的 Web 服务器一样，可以部署在任意的地方，只要裸机控制节点和裸金属服务器可以访问就行了，托管在云上也是 OK 的。裸机控制节点则建议提供 3 个不同的子网，一个用于访问 API 服务器，一个用于提供裸金属集群的业务网，一个用于连接管理接口的管理网。如果没有路由器，裸机控制节点也作为可以 NAT 网关，让裸金属集群访问 API。</p><p>当然如果嫌麻烦的话，业务网和其他网络合并，然后控制器和 API 跑在同一台服务器上也是可以的，后面进行 IP 地址分配的时候注意不要产生冲突即可。业务网 IP 是后面通过 DHCP 服务分配的。</p><h3 id="服务器配置"><a href="#服务器配置" class="headerlink" title="服务器配置"></a>服务器配置</h3><p>因为 OpenStack 服务很多，而且全部是用 Python 写的，外加需要跑一个 SQL 数据库和消息队列，所以 API 服务器尽量使用配置高一点的，否则会爆内存或者 API 超时。在我的小型部署环境里，API 服务器实测常驻内存会到 16G 以上，建议选择 32G 的内存，CPU 可以选择 8 核或者 16 核。控制节点同样需要跑不少服务，以计算为主，内存建议 16G 以上，CPU 8 核以上。</p><h2 id="服务运行总体流程"><a href="#服务运行总体流程" class="headerlink" title="服务运行总体流程"></a>服务运行总体流程</h2><p>Ironic 由于是直接操作物理服务器，所以整体的流程比虚拟机要复杂一点。这里先介绍一下一台裸金属服务器是怎么启动和关闭的，先对服务之间的互动有个大局上的把握，后面填配置的时候不至于两眼一黑。</p><p>部署 Ironic 涉及到的服务有：</p><ul><li>Keystone：鉴权服务</li><li>Nova：计算服务，包含 API Server、Scheduler，依赖 Placement</li><li>Placement：资源分配服务</li><li>Glance：镜像存储服务，用到的启动镜像、内核、RAMdisk 都存放在这</li><li>Neutron：网络服务，这个是最复杂的服务之一，除了 API Server，还有不同的 Agent 用来设置好网络</li><li>Ironic：包含 Neutron Agent 接收网络配置，绑定虚拟接口到服务器上，Conductor 负责接收 Nova 指令，完成服务器管理操作</li><li>Horizon：Web 管理界面，方便操作</li></ul><p>开关机的流程：</p><ol><li>用户通过 API&#x2F;Web 创建实例，指定裸金属服务器作为实例类型</li><li>Scheduler 调用 Placement 服务寻找可用的服务器，指定给 Compute 服务进行启动</li><li>Compute 服务标记占用服务器，调用 Ironic Conductor 开始启动过程</li><li>Conductor 完成 Neutron 网络等配置，尤其是 DHCP 服务</li><li>Conductor 从 Glance 镜像服务下载 Ironic Python Agent (IPA) 镜像，调用服务器管理接口从这个镜像启动<ul><li>这里的 IPA 镜像相当于一个 PE 预加载环境，就像平时安装操作系统那样启动一个纯内存的操作系统，然后通过这个操作系统完成后续的操作，所有硬件驱动必须打包在这个镜像里</li></ul></li><li>IPA 启动后，回调 Conductor，通知它下载用户镜像，然后 IPA 开始部署用户操作系统镜像<ul><li>这里的操作系统镜像分为全盘镜像和分区镜像，全盘镜像包含分区表等，分区镜像就是一个普通的虚拟磁盘</li><li>实际上就是把镜像 <code>dd</code> 到硬盘里</li></ul></li><li>Conductor 再次调用远程管理接口，将服务器引导到用户的操作系统启动，部署完成</li><li>用户删除实例后，Compute 服务调用 Conductor 进行清理操作</li><li>Conductor 调用服务器远程管理接口，重新引导到 IPA，清理磁盘</li><li>清理完成后，调用远程管理接口，关机</li></ol><p>以上这些服务还依赖于一些数据库用于持久化和消息队列用于 RPC 通信：</p><ul><li>MySQL</li><li>Memcache</li><li>RabbitMQ</li></ul><p>OpenStack 整个都是用 Python 实现的，所以还需要安装 Python 3.6 以上版本。</p><p>下面所有操作都在 Yoga 版本验证过，如果没有特别说明的话，就是安装在 API 服务节点。需要注意的是，API 服务如果通过包管理安装，有的是以 systemd 系统服务形式，直接启动 Python 程序提供服务，有的是安装到 Apache 2 作为 WSGI 服务。</p><h2 id="Keystone-鉴权服务"><a href="#Keystone-鉴权服务" class="headerlink" title="Keystone 鉴权服务"></a>Keystone 鉴权服务</h2><p>这个服务没什么坑，就是根据用户名密码检查权限，下发 Token 的 HTTP API 服务，直接按照官网文档安装就可以了。这个服务是其他所有服务的基础，必须最先安装好。如果有 LDAP 服务的话，建议先不接入，后续再使用其他 OpenID 服务实现统一鉴权。 这个是以系统服务形式安装的。</p><p><a href="https://docs.openstack.org/keystone/yoga/install/index.html">https://docs.openstack.org/keystone/yoga/install/index.html</a></p><h2 id="Placement-资源分配服务"><a href="#Placement-资源分配服务" class="headerlink" title="Placement 资源分配服务"></a>Placement 资源分配服务</h2><p>这个服务也没有什么坑，也直接按照官网教程操作，配置好 Keystone 服务地址和凭据即可。</p><p><a href="https://docs.openstack.org/placement/yoga/install/index.html#installation-packages">https://docs.openstack.org/placement/yoga/install/index.html#installation-packages</a></p><p>我在 CentOS 8 上默认是以 WSGI 方式安装的，要配置一下 Apache 2，打开访问权限，否则其他服务无法访问 API，对比以下内容修改你的配置文件就可以了：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">VirtualHost</span> *<span class="attr">:8778</span>&gt;</span></span><br><span class="line">  WSGIProcessGroup placement-api</span><br><span class="line">  WSGIApplicationGroup %&#123;GLOBAL&#125;</span><br><span class="line">  WSGIPassAuthorization On</span><br><span class="line">  WSGIDaemonProcess placement-api processes=3 threads=1 user=placement group=placement</span><br><span class="line">  WSGIScriptAlias / /usr/bin/placement-api</span><br><span class="line">  <span class="tag">&lt;<span class="name">IfVersion</span> &gt;</span>= 2.4&gt;</span><br><span class="line">    ErrorLogFormat &quot;%M&quot;</span><br><span class="line">  <span class="tag">&lt;/<span class="name">IfVersion</span>&gt;</span></span><br><span class="line">  ErrorLog /var/log/placement/placement-api.log</span><br><span class="line">  #SSLEngine On</span><br><span class="line">  #SSLCertificateFile ...</span><br><span class="line">  #SSLCertificateKeyFile ...</span><br><span class="line">  <span class="tag">&lt;<span class="name">Directory</span> /<span class="attr">usr</span>/<span class="attr">bin</span>&gt;</span></span><br><span class="line">    Require all denied</span><br><span class="line">    # 添加以下内容</span><br><span class="line">    <span class="tag">&lt;<span class="name">Files</span> &quot;<span class="attr">placement-api</span>&quot;&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">RequireAll</span>&gt;</span></span><br><span class="line">        Require all granted</span><br><span class="line">        Require not env blockAccess</span><br><span class="line">      <span class="tag">&lt;/<span class="name">RequireAll</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Files</span>&gt;</span></span><br><span class="line">    # 添加以上内容</span><br><span class="line">  <span class="tag">&lt;/<span class="name">Directory</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">VirtualHost</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Glance-镜像存储服务"><a href="#Glance-镜像存储服务" class="headerlink" title="Glance 镜像存储服务"></a>Glance 镜像存储服务</h2><p>这个也很简单，按照官网操作，需要注意存储镜像的路径选择一个硬盘空间足够的路径。这个默认也是安装成系统服务。</p><p><a href="https://docs.openstack.org/glance/yoga/install/index.html">https://docs.openstack.org/glance/yoga/install/index.html</a></p><h2 id="Horizon-Web-界面服务"><a href="#Horizon-Web-界面服务" class="headerlink" title="Horizon Web 界面服务"></a>Horizon Web 界面服务</h2><p>按照官网操作，并配置好和 Keystone 的 SSO 鉴权通信：</p><p><a href="https://docs.openstack.org/horizon/yoga/install/index.html">https://docs.openstack.org/horizon/yoga/install/index.html</a></p><p>CentOS 8 的包管理把这个安装成 WSGI，需要改 Apache 2 的配置文件，找到关于 OpenStack Dashboard 的，对比以下内容，添加好访问权限，否则访问会报 403</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">WSGIDaemonProcess dashboard</span><br><span class="line">WSGIProcessGroup dashboard</span><br><span class="line">WSGISocketPrefix run/wsgi</span><br><span class="line">WSGIApplicationGroup %&#123;GLOBAL&#125;</span><br><span class="line">WSGIScriptAlias /dashboard /usr/share/openstack-dashboard/openstack_dashboard/wsgi.py</span><br><span class="line">Alias /dashboard/static /usr/share/openstack-dashboard/static</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">Directory</span> /<span class="attr">usr</span>/<span class="attr">share</span>/<span class="attr">openstack-dashboard</span>/<span class="attr">openstack_dashboard</span>&gt;</span></span><br><span class="line">  Options All</span><br><span class="line">  AllowOverride All</span><br><span class="line">  Require all granted</span><br><span class="line"><span class="tag">&lt;/<span class="name">Directory</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">Directory</span> /<span class="attr">usr</span>/<span class="attr">share</span>/<span class="attr">openstack-dashboard</span>/<span class="attr">static</span>&gt;</span></span><br><span class="line">  Options All</span><br><span class="line">  AllowOverride All</span><br><span class="line">  Require all granted</span><br><span class="line"><span class="tag">&lt;/<span class="name">Directory</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="Neutron-网络服务"><a href="#Neutron-网络服务" class="headerlink" title="Neutron 网络服务"></a>Neutron 网络服务</h2><h2 id="API-服务"><a href="#API-服务" class="headerlink" title="API 服务"></a>API 服务</h2><p>这里裸机网络的配置和虚拟机的非常不同，先按照官网把 neutron-server 配置起来，是安装成系统服务的。网络插件按照 Ironic 的教程配，跳过 Configure networking options 和 Configure the metadata agent，不要配 linuxbridge 的部分，然后也不要配 Metadata Agent：</p><p><a href="https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html">https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html</a></p><p>Configure networking options 这一节，用下面 Ironic 的步骤代替，配置到 3. Restart the neutron-server service, to load the new configuration. 服务器部分就算配完了了，后面的步骤是配置裸机控制服务的</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-networking.html">https://docs.openstack.org/ironic/yoga/install/configure-networking.html</a></p><p>配好 ml2 服务器部分之后，回到 <a href="https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html">https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html</a> ，把 Finalize installation 配完</p><h2 id="裸机控制服务"><a href="#裸机控制服务" class="headerlink" title="裸机控制服务"></a>裸机控制服务</h2><p>需要在裸机的控制节点安装 Open vSwitch，安装之后不要用 Unix 套接字启动 Open vSwitch 的 server，要监听 TCP 的 6640 端口，因为 neutron 是用 TCP 去连接的。还有通过 <code>ovs-vsctl br-add</code> 添加的网桥默认是 down 的，不要急着把物理端口加进去，否则会断网，先用 <code>ip link set dev br-xxx up</code> 把它拉起来，然后设置好 IP，再把物理端口加进去。配置好后，启动 neutron-openvswitch-agent 和 neutron-dhcp-agent，这两个服务都需要用到 sudo 权限，需要提前给运行服务的用户配置好免密 sudo，否则服务会起不来</p><p>在裸机控制节点，配置 Ironic 的网络 agent，从 4. Create and edit <code>/etc/neutron/plugins/ml2/ironic_neutron_agent.ini</code> and add the required configuration. 开始配，做到执行 <code>ovs-vsctl show</code>，后面的命令是在 API 节点运行的。注意创建 subnet 的时候，如果你是<strong>用域名访问的 API 服务器，需要顺便填一下 DNS</strong>，否则后面裸金属启动的时候会因为没有办法通知 OpenStack 而导致整个启动过程卡住。</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-networking.html">https://docs.openstack.org/ironic/yoga/install/configure-networking.html</a></p><p>启动 neutron-openvswitch-agent 如果报了 <code>ModuleNotFoundError</code>，需要编辑一下报错的包名下面的 <code>__init__.py</code>，在文件最后 import 一下报错的模块名。</p><p>接着配置一下 Metadata Agent，这个服务是用来给 cloud-init 提供初始化元数据的，按照 Configure the metadata agent 和 Configure the Compute service to use the Networking service 做，nova 的部分先跳过不做，后面装好 nova 了再把配置填上就行：</p><p><a href="https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html">https://docs.openstack.org/neutron/yoga/install/controller-install-rdo.html</a></p><p>最后安装配置 DHCP Agent，看 DHCP agent setup: OVS plug-in 一节，这个是为了 PXE 启动和动态 IP 分配，需要在机器上安装 iproute2（ip 命令） dnsmasq dhcp_release haproxy：</p><p><a href="https://docs.openstack.org/neutron/yoga/admin/archives/config-agents.html#dhcp-agent-setup-ovs-plug-in">https://docs.openstack.org/neutron/yoga/admin/archives/config-agents.html#dhcp-agent-setup-ovs-plug-in</a></p><p>最后，务必确认防火墙是否放行了 DHCP 的 UDP 端口，至少需要裸金属服务器是可以访问的。 </p><h2 id="Nova-计算服务"><a href="#Nova-计算服务" class="headerlink" title="Nova 计算服务"></a>Nova 计算服务</h2><h3 id="API-服务-1"><a href="#API-服务-1" class="headerlink" title="API 服务"></a>API 服务</h3><p>官网的教程是安装虚拟机的，Ironic 安装的话需要按照 Ironic 的教程来，控制节点只需要启动 openstack-nova-api、openstack-nova-conductor 和 openstack-nova-scheduler 就行了，openstack-nova-novncproxy 用不到，不需要启动，也不需要配置。</p><p><a href="https://docs.openstack.org/nova/yoga/install/controller-install.html">https://docs.openstack.org/nova/yoga/install/controller-install.html</a></p><p>上面安装好 API 服务之后，配置按照下面这个链接填：</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-compute.html">https://docs.openstack.org/ironic/yoga/install/configure-compute.html</a></p><h3 id="裸机控制服务-1"><a href="#裸机控制服务-1" class="headerlink" title="裸机控制服务"></a>裸机控制服务</h3><p>感觉可以装在 API 服务器上，不过我不太确定，还是安装在了裸机控制节点上。</p><p>首先参考 <a href="https://docs.openstack.org/nova/yoga/install/compute-install.html">https://docs.openstack.org/nova/yoga/install/compute-install.html</a> 安装 compute 服务，但是配置的话按照下面这个链接填：</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-compute.html">https://docs.openstack.org/ironic/yoga/install/configure-compute.html</a></p><p>可以偷懒直接用 API 服务器的配置文件。</p><h2 id="Ironic-裸金属服务"><a href="#Ironic-裸金属服务" class="headerlink" title="Ironic 裸金属服务"></a>Ironic 裸金属服务</h2><h3 id="API-服务-2"><a href="#API-服务-2" class="headerlink" title="API 服务"></a>API 服务</h3><p>按照官网配置即可。只需要装 openstack-ironic-api，可以装成系统服务，也可以装成 WSGI：</p><p><a href="https://docs.openstack.org/ironic/yoga/install/install.html">https://docs.openstack.org/ironic/yoga/install/install.html</a></p><h3 id="裸机控制服务-2"><a href="#裸机控制服务-2" class="headerlink" title="裸机控制服务"></a>裸机控制服务</h3><p>在裸机控制节点装 openstack-ironic-conductor，主要是那一堆 driver 比较麻烦，但是官网有给了示例配置，直接复制粘贴没有什么大问题。</p><p><a href="https://docs.openstack.org/ironic/yoga/install/setup-drivers.html">https://docs.openstack.org/ironic/yoga/install/setup-drivers.html</a></p><p>装好之后，用下面的命令检查 conductor 是不是注册成功了，<strong>务必保证两台服务器时间同步</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">baremetal driver list</span><br></pre></td></tr></table></figure><p>如果是空的，说明没注册成功，检查一下 API 服务器的防火墙是不是拦截了消息队列或者 API 服务的端口，也有可能是两台机器时间不同步。</p><h3 id="配置-PXE-服务器"><a href="#配置-PXE-服务器" class="headerlink" title="配置 PXE 服务器"></a>配置 PXE 服务器</h3><p>也是按照官网操作就行，可以直接下载需要的 deb、rpm 包等把引导文件解压出来，放到指定目录就可以了。</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-pxe.html">https://docs.openstack.org/ironic/yoga/install/configure-pxe.html</a></p><p>需要注意防火墙是否放行 TFTP 的 UDP 端口和 HTTP 服务器的 TCP 端口。</p><h2 id="注册裸机节点"><a href="#注册裸机节点" class="headerlink" title="注册裸机节点"></a>注册裸机节点</h2><h3 id="创建镜像"><a href="#创建镜像" class="headerlink" title="创建镜像"></a>创建镜像</h3><p>按照官网文档操作就行。</p><p><a href="https://docs.openstack.org/ironic/yoga/install/configure-glance-images.html">https://docs.openstack.org/ironic/yoga/install/configure-glance-images.html</a></p><p>一些参考命令：</p><h4 id="通用软件包"><a href="#通用软件包" class="headerlink" title="通用软件包"></a>通用软件包</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> COMMON_ELEMENTS=<span class="string">&quot;cloud-init cloud-init-datasources mellanox growroot devuser dynamic-login baremetal dhcp-all-interfaces grub2&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 下面这个不设置的话，会导致 cloud-init 不启动，密钥之类的注入不了</span></span><br><span class="line"><span class="built_in">export</span> DIB_CLOUD_INIT_DATASOURCES=<span class="string">&quot;Ec2, ConfigDrive, OpenStack&quot;</span></span><br></pre></td></tr></table></figure><h4 id="CentOS-8-Stream"><a href="#CentOS-8-Stream" class="headerlink" title="CentOS 8 Stream"></a>CentOS 8 Stream</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> DIB_RELEASE=8-stream</span><br><span class="line"><span class="built_in">export</span> DIB_DISTRIBUTION_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/centos/ </span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_USERNAME=username</span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PWDLESS_SUDO=<span class="literal">true</span></span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PASSWORD=<span class="string">&quot;password&quot;</span></span><br><span class="line">disk-image-create centos <span class="variable">$COMMON_ELEMENTS</span> --ramdisk dracut-ramdisk -o centos-8-stream</span><br></pre></td></tr></table></figure><h4 id="Debian-11"><a href="#Debian-11" class="headerlink" title="Debian 11"></a>Debian 11</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> DIB_RELEASE=bullseye </span><br><span class="line"><span class="built_in">export</span> DIB_DISTRIBUTION_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/debian/</span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_USERNAME=username</span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PWDLESS_SUDO=<span class="literal">true</span></span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PASSWORD=<span class="string">&quot;password&quot;</span></span><br><span class="line">disk-image-create debian<span class="variable">$COMMON_ELEMENTS</span> -o debian-11</span><br></pre></td></tr></table></figure><h4 id="Fedora-36"><a href="#Fedora-36" class="headerlink" title="Fedora 36"></a>Fedora 36</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> DIB_RELEASE=36</span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_USERNAME=username</span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PWDLESS_SUDO=<span class="literal">true</span></span><br><span class="line"><span class="built_in">export</span> DIB_DEV_USER_PASSWORD=<span class="string">&quot;password&quot;</span></span><br><span class="line"><span class="built_in">export</span> DIB_DISTRIBUTION_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/fedora/</span><br><span class="line">disk-image-create fedora <span class="variable">$COMMON_ELEMENTS</span> -o fedora-36</span><br></pre></td></tr></table></figure><h4 id="从虚拟机镜像转换"><a href="#从虚拟机镜像转换" class="headerlink" title="从虚拟机镜像转换"></a>从虚拟机镜像转换</h4><p>先像平时一样把虚拟机安装好，然后装好硬件需要的驱动，打开所有网络接口的 DHCP 功能，安装 <code>cloud-init</code> 包和其他需要的软件，把 vmlinuz 和 initrd 拷贝出来，导出虚拟磁盘之后用 <code>qemu-img</code> 转换 <code>qcow2</code>。 </p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">qemu-img convert -O qcow2 VMware-machine-disk1.vmdk VMware-machine-disk1.qcow2</span><br></pre></td></tr></table></figure><h4 id="上传镜像"><a href="#上传镜像" class="headerlink" title="上传镜像"></a>上传镜像</h4><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> OS <span class="keyword">in</span> centos-8-stream fedora-36 debian-11; <span class="keyword">do</span></span><br><span class="line">    openstack image create<span class="variable">$&#123;OS&#125;</span>-kernel --public --disk-format aki --container-format aki --file <span class="variable">$&#123;OS&#125;</span>.vmlinuz </span><br><span class="line">    openstack image create<span class="variable">$&#123;OS&#125;</span>-initrd --public --disk-format ari --container-format ari --file <span class="variable">$&#123;OS&#125;</span>.initrd</span><br><span class="line">    openstack image create<span class="variable">$&#123;OS&#125;</span> --public --disk-format qcow2 --container-format bare --property kernel\_id=$(openstack image show -c <span class="built_in">id</span> -f value<span class="variable">$&#123;OS&#125;</span>-kernel) --property ramdisk\_id=$(openstack image show -c <span class="built_in">id</span> -f value<span class="variable">$&#123;OS&#125;</span>-initrd) --file <span class="variable">$&#123;OS&#125;</span>.qcow2</span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="注册裸机"><a href="#注册裸机" class="headerlink" title="注册裸机"></a>注册裸机</h3><p><strong>如果是第一台机器</strong>，需要先创建 Flavor （也就是“机型”）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> RAM_MB=191736</span><br><span class="line"><span class="built_in">export</span> VCPU=40</span><br><span class="line"><span class="built_in">export</span> DISK_GB=500</span><br><span class="line"><span class="comment"># 上面的信息不重要，不影响调度</span></span><br><span class="line"><span class="built_in">export</span> FLAVOR_NAME=baremetal</span><br><span class="line"><span class="built_in">export</span> RESOURCE_NAME=CUSTOM_$(<span class="built_in">echo</span> <span class="variable">$FLAVOR_NAME</span> | <span class="built_in">tr</span> <span class="string">&#x27;.:-&#x27;</span> <span class="string">&#x27;___&#x27;</span> | <span class="built_in">tr</span> <span class="string">&#x27;[:lower:]&#x27;</span> <span class="string">&#x27;[:upper:]&#x27;</span>)</span><br><span class="line">openstack flavor create --ram<span class="variable">$RAM</span>\_MB --vcpus <span class="variable">$VCPU</span> --disk<span class="variable">$DISK</span>\_GB <span class="variable">$FLAVOR_NAME</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 必须设置下面全部选项，否则无法调度</span></span><br><span class="line">openstack flavor <span class="built_in">set</span> --property resources:<span class="variable">$RESOURCE</span>\_NAME=1 <span class="variable">$FLAVOR_NAME</span></span><br><span class="line">openstack flavor <span class="built_in">set</span> --property resources:VCPU=0<span class="variable">$FLAVOR_NAME</span></span><br><span class="line">openstack flavor <span class="built_in">set</span> --property resources:MEMORY_MB=0 <span class="variable">$FLAVOR_NAME</span></span><br><span class="line">openstack flavor <span class="built_in">set</span> --property resources:DISK_GB=0<span class="variable">$FLAVOR_NAME</span></span><br></pre></td></tr></table></figure><p>配置好之后，使用 OpenStack 命令行创建一个裸金属服务器，注意 <code>--resource-class</code> 要和上面 flavor 对应：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> SERVER_NAME=bm-server</span><br><span class="line"><span class="built_in">export</span> DRIVER=idrac</span><br><span class="line">baremetal node create --driver <span class="variable">$SERVER</span>\_NAME --name<span class="variable">$SERVER_NAME</span> --resource-class <span class="variable">$FLAVOR_NAME</span></span><br></pre></td></tr></table></figure><p>这时候可以对名称之类的信息修改，然后，指定远程管理 driver 所需要的信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> IPA_KERNEL_UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span><br><span class="line"><span class="built_in">export</span> IPA_INITRD_UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</span><br><span class="line"><span class="built_in">export</span> DRAC_ADDRESS=1.2.3.4</span><br><span class="line"><span class="built_in">export</span> DRAC_USERNAME=root</span><br><span class="line"><span class="built_in">export</span> DRAC_PASSWORD=123456</span><br><span class="line"><span class="built_in">export</span> REDFISH_ADDRESS=https://<span class="variable">$DRAC_ADDRESS</span>/redfish/v1</span><br><span class="line"><span class="built_in">export</span> REDFISH_USERNAME=root</span><br><span class="line"><span class="built_in">export</span> REDFISH_PASSWORD=123456</span><br><span class="line">baremetal node <span class="built_in">set</span> <span class="variable">$SERVER_NAME</span> \</span><br><span class="line">  --driver-info deploy_kernel=<span class="variable">$IPA_KERNEL_UUID</span> \</span><br><span class="line">  --driver-info deploy_ramdisk=<span class="variable">$IPA_INITRD_UUID</span> \</span><br><span class="line">  --driver-info agent_verify_ca=False \</span><br><span class="line">  --driver-info drac_address=<span class="variable">$DRAC_ADDRESS</span> \</span><br><span class="line">  --driver-info drac_username=<span class="variable">$DRAC_USERNAME</span> \</span><br><span class="line">  --driver-info drac_password=<span class="variable">$DRAC_PASSWORD</span> \</span><br><span class="line">  --driver-info redfish_address=<span class="variable">$REDFISH_ADDRESS</span> \</span><br><span class="line">  --driver-info redfish_verify_ca=False \</span><br><span class="line">  --driver-info redfish_username=<span class="variable">$REDFISH_USERNAME</span> \</span><br><span class="line">  --driver-info redfish_password=<span class="variable">$REDFISH_PASSWORD</span></span><br></pre></td></tr></table></figure><p>设置之后，使用命令将节点转移到管理状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">baremetal node manage<span class="variable">$SERVER_NAME</span></span><br></pre></td></tr></table></figure><p>这时候，你可以手动用 <code>baremetal port create</code> 命令创建网卡 MAC 和节点 UUID 对应的关系，也可以用 <code>baremetal node inspect</code> 命令让 Ironic 自动调取远程管理接口来填充信息。</p><p>之后，运行 <code>baremetal node validate $SERVER\_NAME</code> 验证节点配置，没有问题之后，运行 <code>baremetal node provide$SERVER_NAME</code> ，节点就准备完成了。</p><p>添加完之后，一定延迟之后节点就可以被调度了，如果着急的话，可以用 <code>nova-manage cell_v2 discover_hosts</code> 扫描一次节点。</p><p>可以用下面的命令检查能不能被调度到，列表不为空就是可以：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">openstack allocation candidate list --resource <span class="variable">$RESOURCE_NAME</span>=<span class="string">&#x27;1&#x27;</span></span><br></pre></td></tr></table></figure><p>如果是空的话，检查一下 <code>baremetal node show$SERVER_NAME -c provision_state</code> 有没有问题。</p><h2 id="部署裸机系统"><a href="#部署裸机系统" class="headerlink" title="部署裸机系统"></a>部署裸机系统</h2><h3 id="Web"><a href="#Web" class="headerlink" title="Web"></a>Web</h3><p>直接点 Create Instance，按照向导填好资料就可以了。</p><h3 id="命令行"><a href="#命令行" class="headerlink" title="命令行"></a>命令行</h3><p>看文档： <a href="https://docs.openstack.org/ironic/yoga/user/deploy.html">https://docs.openstack.org/ironic/yoga/user/deploy.html</a> </p><h2 id="运维命令"><a href="#运维命令" class="headerlink" title="运维命令"></a>运维命令</h2><h3 id="BIOS-设置"><a href="#BIOS-设置" class="headerlink" title="BIOS 设置"></a>BIOS 设置</h3><p>看文档，因机器而异：<a href="https://docs.openstack.org/ironic/yoga/admin/bios.html">https://docs.openstack.org/ironic/yoga/admin/bios.html</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">baremetal node bios setting list <span class="variable">$SERVER_NAME</span></span><br></pre></td></tr></table></figure><h2 id="下架指令"><a href="#下架指令" class="headerlink" title="下架指令"></a>下架指令</h2><p>先 <code>baremetal node retire</code>：</p><p><a href="https://docs.openstack.org/ironic/yoga/admin/retirement.html">https://docs.openstack.org/ironic/yoga/admin/retirement.html</a></p><p>然后 <code>baremetal node delete</code>。</p><h2 id="救急命令"><a href="#救急命令" class="headerlink" title="救急命令"></a>救急命令</h2><p>有时候节点进入了错误的状态的话，就需要人工干预修改状态，具体参考 ironic 状态机文档：</p><p><a href="https://docs.openstack.org/ironic/yoga/user/states.html#state-machine-diagram">https://docs.openstack.org/ironic/yoga/user/states.html#state-machine-diagram</a></p><h2 id="常用-iDRAC-RACADM-命令"><a href="#常用-iDRAC-RACADM-命令" class="headerlink" title="常用 iDRAC RACADM 命令"></a>常用 iDRAC RACADM 命令</h2><h3 id="查询网卡-MAC-和-PCI-E-地址"><a href="#查询网卡-MAC-和-PCI-E-地址" class="headerlink" title="查询网卡 MAC 和 PCI-E 地址"></a>查询网卡 MAC 和 PCI-E 地址</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">racadm get nic.VndrConfigPage.1</span><br></pre></td></tr></table></figure><p>1 可以换成其他数字。</p><h3 id="查询网卡配置"><a href="#查询网卡配置" class="headerlink" title="查询网卡配置"></a>查询网卡配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">racadm get nic.niCconfig.1</span><br></pre></td></tr></table></figure><p>主要看 <code>LegacyBootProto</code> 是不是 <code>PXE</code> ，不是的话用 <code>set nic.niCconfig.1.LegacyBootProto PXE</code> 设置，不会马上生效，需要用 <code>jobqueue create NIC.Integrated.1-1-1 -r pwrcycle -s TIME_NOW</code> 命令创建配置任务，注意这个命令会重启 iDRAC，<strong>有失联风险</strong>。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>现代 CPU 微架构入门</title>
      <link>https://blog.howardlau.me/programming/processor-microarchitecture.html</link>
      <description>
        <![CDATA[<p>这篇文章是 Processor Microarchitecture An Implementation Perspective 的读书笔记。虽然这本书是 2011 年出版的，讲的东西都已经有点过时了，但是作为一个入门 CPU 微架构的材料还是不错的。对于程序员来说了解]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 24 May 2022 02:56:49 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>这篇文章是 Processor Microarchitecture An Implementation Perspective 的读书笔记。虽然这本书是 2011 年出版的，讲的东西都已经有点过时了，但是作为一个入门 CPU 微架构的材料还是不错的。对于程序员来说了解 CPU 的工作原理也有助于写出更高性能的代码，充分发挥硬件性能。</p><p>在大学本科里学习的计算机组成原理一般是顺序执行标量五级流水线的 CPU，但是在现代的高性能 CPU 中，在一个时钟周期里不同的执行部件可以有多条指令同时执行（例如同时执行两个整数加法）。另外，CPU 也有可能乱序执行程序的指令（在没有数据依赖的情况下）。而在不同的执行阶段中，还可能划分更小的流水线（例如浮点数操作）。所以，现在的 CPU 的流水线可能不止五层，而且是执行模型乱序多发射的。当然，在一些对性能要求不高，而对能耗和芯片面积比较敏感的平台上，CPU 的设计则可能会简化为三级流水线。</p><p>首先介绍一下 CPU 的分类维度，第一个是是否采用了流水线设计；第二个是乱序还是顺序执行，在乱序执行中，指令可以不按程序指定的顺序执行，减少阻塞，但对外表现的行为还是和顺序执行的处理器一样；第三个是标量和超标量处理器，标量处理器的 IPC 最多为 1，因为只有一套执行单元，而不是标量处理器的就是超标量处理器，IPC 可以大于 1，例如 VLIW 处理器；第四个是向量处理器，向量处理器可以使用一条向量指令处理多个元素的向量，也就是 SIMD，例如 Intel 的 AVX 指令就是 SIMD 指令；第五个是是否多核；第六个是是否多线程，多线程和多核的区别是，多核处理器中每一个核心的硬件资源相对独立不共享，而多线程中的线程通常共用大部分的硬件资源，比如 Intel 的超线程就是多线程，一个核心上有两个线程。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523112526.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523112526.png"></a></p><p>大体上讲，CPU 的流水线是这样工作的。首先，取指模块从内存中获取下一条需要执行的指令，然后译码模块将指令解码为相应的控制指令。在重命名阶段，CPU 将给需要执行的指令分配寄存器，并将指令分配到一条发射队列中。在发射阶段，CPU 检查发射队列中的指令的操作数是否已经准备好了（例如已经从内存中读取了数据），将已经就绪的指令发送到执行单元执行。执行完毕之后，则需要将操作数写回寄存器，在乱序 CPU 中，则是写回重排缓冲区（ROB）。最后，当一条指令可以提交后，就真正修改 CPU 的内部状态，完成一条指令。</p><h2 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h2><p>现在 CPU 的大多数访存操作都需要通过缓存进行（有一些 CPU 支持绕过缓存的指令），所以，有必要了解缓存的设计原理。</p><p>目前的 CPU 都通过虚拟地址来访问内存，在访问前需要进行地址转换。虚拟地址转换为物理地址的映射关系也是存储在内存中的，但由于地址转换是非常频繁的操作，所以 CPU 中有 TLB 缓存用来加速地址转换。TLB 使用虚拟地址索引，里面存放的都是页表项。</p><p>得到物理地址之后，就可以向数据缓存发起访问请求了。缓存一般是截取物理地址的一部分作为缓存行索引。在缓存中，一个缓存行索引可以对应一路或多路缓存行，具体数量称为关联度。一个索引对应一路缓存行的，称为直接映射；一个索引对应整个缓存的称为全相连；剩下的就称为 n 路组相连。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524114940.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524114940.png"></a></p><p>由于不同地址可能有相同的缓存行索引，所以地址除了页内偏移量以及缓存行索引的剩下的比特作为缓存行的标签，用来确定缓存是否有效。缓存的标签和数据访问可以并行执行，也可以先比较完标签再访问数据。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524171630.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524171630.png"></a></p><p>由于缓存处于关键路径上，为了满足现在 CPU 的高频率要求，缓存控制器本身也是流水线化的。在第一阶段，先计算出索引和标签位等，然后在第二阶段进行消歧义操作，第三阶段访问缓存，第四阶段根据偏移量从缓存行中取出相应的数据。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524171938.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524171938.png"></a></p><p>缓存也可以设计为先比较标签再访问数据。这样做虽然引入了额外的一个时钟周期的延迟，但是由于比较标签后可以只访问命中的那一路数据，可以减少能耗，同时缩短了关键路径，进一步提高时钟频率。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524172239.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524172239.png"></a></p><p>可以看出序列比较标签的缓存的流水线多了一级。对于乱序执行处理器来说，乱序执行可以掩盖多出来的一个周期，所以使用序列比较的缓存有利于频率提升。顺序处理器使用并行比较标签更为合理。</p><p>关联度越大的缓存产生冲突的几率越小，但是占用芯片面积更大，电路复杂，能耗更高，所以一般的缓存关联度不会太大，例如常见的有 12 路、16 路等。</p><p>缓存在未命中的时候需要访问下一级存储获取数据，简单的做法是阻塞这条指令，直到缓存获取到数据。但是这样对性能损失很大。在乱序处理器中，缓存允许在一条指令还没有获取到数据的时候，就执行其他不依赖这个数据的指令。另外，还有一个要求就是非阻塞，或者称为无锁的。无锁的缓存即使在有未命中的指令执行的时候，也允许处理器发射新的访存指令。为了跟踪还没有完成的访存请求，缓存中使用了未命中状态保持寄存器（MSHR）。另外，数据到达缓存的时候，会进入输入栈（现代 CPU 称为填充缓冲区 Fill Buffer），然后才写入到数据矩阵中。</p><p>对于无锁缓存来说，未命中的情况可以分为三类：</p><ul><li>初级未命中：发生的第一次未命中，此时缓存会向下一层存储请求数据。</li><li>次级未命中：还在请求数据的缓存块再一次发生未命中的情况。</li><li>结构阻塞未命中：由于硬件资源不足而无法处理的次级未命中。这种未命中会因为结构冒险而导致阻塞。</li></ul><p>MSHR 也有很多种组织方法。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524175806.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524175806.png"></a></p><p>上图是隐式寻址 MSHR，如果一个缓存块包含 N 个字，那么就有 N 个目的寄存器，并且还有一个寄存器保存块地址。发生初级未命中的时候，会设置这个地址寄存器以及相应字的目的寄存器。如果后续发生了次级未命中，就比较地址，并设置目的寄存器。MSHR 里保存了一个缓存块里的一个字的目的寄存器，以及一些格式信息，例如此次访存指令的位宽，是否需要符号扩展等。也就是说一个访存指令在块内的地址隐式地由目的寄存器的编号决定。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524180227.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524180227.png"></a></p><p>相对地就有显式寻址 MSHR。这种 MSHR 不要求目的寄存器的数量和块的大小相同，但是需要显式地在寄存器中编码一次访存请求的块内偏移。这样可以允许任意数量的未完成请求，和缓存块大小解耦。</p><p>还有一种缓存内 MSHR。因为在缓存块还没有读取完毕的时候缓存块是没有数据的，所以可以利用这个空间来保存 MSHR 信息，也不必使用额外的地址寄存器。这种设计要求缓存块额外带一个 transient 标记位，用来标记这个缓存块是不是还在获取。</p><p>现代 CPU 通常支持一个周期内发射 2 条访存指令，为了支持 CPU 的带宽需求，缓存需要提供多个读写端口。</p><p>一种简单的设计是，缓存真的提供两个读端口，也就是每个数据块都连接到两个读端口上。但是这样能耗高、延迟高、占用面积大，没有 CPU 采用这样的设计。</p><p>另一种方法是将标签和数据块复制两次，每个数据块只连接到一个读端口，这样可以降低延迟，但是在写入的时候需要同时写入两个数据块。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524181308.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524181308.png"></a></p><p>现代的 CPU 通常采用多 Bank 的设计，在一个周期内，不同 Bank 的请求可以并发执行，而同一个 Bank 的则会冲突，需要阻塞。每个 Bank 内都有独立的标签和数据矩阵，比较器等。这体现了分而治之的思想。</p><p>对于指令缓存来说，一般使用的是阻塞的缓存。数据缓存使用非阻塞是因为处理器可以发射多条不依赖数据的指令来掩盖延迟，但是取指的时候，之后的指令按照程序顺序隐式依赖与前面的指令。所以指令缓存发生未命中的时候，需要等之前的指令获取完毕后才能执行后面的指令。所以指令缓存就算实现为非阻塞的也没有多大用处。</p><p>由于并行比较标签可以节省一个周期的延迟，所以指令缓存可能更喜欢用这个。但是这也会导致时钟频率下降和能耗上升，需要仔细权衡 trade-off。</p><h2 id="取指"><a href="#取指" class="headerlink" title="取指"></a>取指</h2><p>在现代 CPU 中，取指单元一个周期可以取一条指令，也就是说取指单元需要每个周期都计算出下一条指令的地址。然而在如果是分支指令，在真正执行分支指令之前是没有办法知道下一个执行的地址是什么的。如果让取指单元空等直到执行完毕的话，那么会浪费许多 CPU 周期，造成性能下降。所以，现在的 CPU 都有一个分支目标缓冲区（BTB），用来预测下一个指令地址。由于程序里许多跳转也涉及函数的返回，一些 CPU 还有返回地址栈（RAS）。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523145906.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523145906.png"></a>一条简单的取指流水线可能包含四个阶段，第一个阶段是不同的分支预测器预测下一个地址，第二个阶段是根据预测器的结果确定取指地址，第三阶段则是从指令缓存中取指，第四阶段是将指令从缓存行中取出并驱动译码单元。虽然这样取指需要四个周期，但由于是流水线化的，实际上可以做到一个周期取一条指令，并且能够提供较高的时钟频率。</p><h2 id="译码"><a href="#译码" class="headerlink" title="译码"></a>译码</h2><p>译码单元和 ISA 有很大的关系，RISC 指令集通常是定长的，而且操作数编码在相对固定的位置，译码单元比较简单，并行度也比较高。而像 x86 这种变长的 CISC 指令集，译码单元则相当复杂，并且译码单元还需要在指令字节流里面切割出不同的指令。由于 CISC 指令太过复杂，实际上在 CPU 内部，一条 CISC 指令通常也是进一步译码为多个类似 RISC 的微指令。P6 的微指令长度为 118 位，可以从大小看出这个相比于 RISC 指令（32 位）来说，更像是译码之后的 RISC 指令。<a href="/programming/processor-microarchitecture/Pasted-image-20220523151924.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523151924.png"></a>在 Intel Core CPU 中，取指单元的指令会先放在预取缓冲区，然后指令长度译码器负责分割出多个指令，送入指令队列。译码器从指令队列取出指令并将指令译码为微指令，送入指令译码队列。对于一些简单的指令，例如寄存器-寄存器指令，只需要译码为一个微指令，而一些复杂的指令则需要译码为至多 4 个微指令，这样设计的好处是在大多数指令都是简单指令的情况下，可以节省一部分能耗而不牺牲译码带宽。对于一些极其复杂的指令，例如字符串相关的指令，则会停止正常的译码流水线，并将控制转移到 MSROM。这个 ROM 无非也是一个序列发生器，生成指令对应的微码流。</p><h2 id="分配"><a href="#分配" class="headerlink" title="分配"></a>分配</h2><p>在这个阶段中，CPU 主要完成两个操作：寄存器重命名和资源分配。其中寄存器重命名是为了解决命名冲突（读后写，写后写），而资源分配则是预留发射队列、重排缓冲区项、Load&#x2F;Store 队列等指令后续操作可能使用的硬件资源。如果资源不足，那么这条指令就需要等待到资源可用才可以分配。有时候不同的硬件功能单元（例如访存、浮点数、整数）会有各自独立的发射队列，在分配过程中，也有可能按照指令的类型分配到不同的发射队列中。</p><p>寄存器重命名一个出名的算法是 Tomasulo 算法，这个算法用执行单元来命名输出结果的寄存器，但有个缺点是它在指令执行完成之前都要占用保留站的资源。所以现在的处理器在指令发射之后就释放发射队列项，提高执行效率。</p><p>在汇编指令中的寄存器名称是<strong>逻辑寄存器</strong>，而因为寄存器重命名，一个逻辑寄存器可能在不同时候会对应不同的<strong>物理寄存器</strong>。而实现寄存器重命名有多种方法，例如使用重排缓冲区、使用重命名缓冲区以及使用合并寄存器堆的方法。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523153934.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523153934.png"></a>通过重排缓冲区重命名是一种重命名方法，指令执行的结果会写入到 ROB 中，在指令提交的时候，再将 ROB 中的值写入到寄存器堆相应的位置。使用这种方法的时候，一个逻辑寄存器的值可能会映射到 ROB 中，也可能是在寄存器堆中，在执行的时候需要判断究竟需要从哪里取指。</p><p>另一种方法是上面方法的一个小改进，也就是使用重命名缓冲区。由于许多指令其实不需要写寄存器，所以如果为这些指令分配重排缓冲区项的话会浪费一些资源。所以，在改进的方法中，只为那些需要写寄存器的指令分配一个重命名项，而在指令提交的时候同样将对应的值写入寄存器堆中即可。</p><p>上面两种方法的一个缺点就是一个值需要写入两次，一次写入到缓冲区，一次写入到寄存器堆，而合并寄存器堆则节省了一次写入，而且只需要从寄存器堆读，不需要判断是否在 ROB 中。这种方法的 CPU 内部有比逻辑寄存器数量多的物理寄存器，并且 CPU 会维护一个映射表，将逻辑寄存器映射到物理寄存器。另外，还有一个环形队列，用来维护当前空闲的寄存器列表。在一条指令需要写入结果的时候，从空闲寄存器堆中分配一个寄存器，没有空闲的寄存器则需要等待。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523155245.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523155245.png"></a></p><p>在指令提交的时候，只需要释放原来的物理寄存器，并更新映射表即可，不需要将值复制写入。</p><p>还有一个重要的问题需要解决的是指令在什么时候读寄存器。一种是在发射前读寄存器的值，并且在发射队列中的项包含了具体的值；另一种是在发射后再读寄存器，发射队列中的项只包含逻辑寄存器编号，这种方法只需要读一次，而且不需要将值一直复制下去。理论上读寄存器的方法和重命名方法是正交的，对于合并寄存器来说，两种读的方法没有什么区别，但是使用重排缓冲区的方法更适合在发射前读寄存器。这是因为如果一条指令在执行之前，它的一个操作数寄存器有指令提交写入了，那么提交的时候就需要找到所有使用这个寄存器的指令，并将其 ROB 指针修改为指向寄存器堆。这在硬件电路实现上非常麻烦，所以使用重排缓冲区或者重命名缓冲区的 CPU 一般都是在发射前读寄存器。</p><p>在发生分支预测错误或者异常的时候，已经分配了的指令需要重置，释放队列项等，并将修改的重命名表等恢复到原来的状态。</p><h2 id="发射"><a href="#发射" class="headerlink" title="发射"></a>发射</h2><p>在发射阶段，CPU 会检查发射队列中的指令的源操作数是否已经就绪，一旦操作数就绪，就会将指令发射到执行单元执行。一些 CPU 可能会给不同的执行单元分配不同的发射队列，而一些 CPU 所有的执行单元共用一条发射队列。正如前面所说，读取操作数有两种方法，一种是发射前读取，另一种是发射后读取。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523162455.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523162455.png"></a></p><p>发射前读取的发射单元中保存了指令所需的数据（data）和寄存器编号（id）。对于一些只使用一个寄存器的指令，指令对应的有效位会置零，而就绪位标识了一个操作数是否就绪，如果就绪的话，对应的数据项则保存了对应的值。</p><p>和发射单元相关的主要有三个事件，一是发射队列分配，二是指令唤醒，三是指令选择。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523164802.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523164802.png"></a>在分配阶段，分配单元会尝试在发射队列分配一个发射队列项，然后进行寄存器重命名操作。然后，分配单元读取寄存器和映射表，对于已经就绪的寄存器，直接读取值，没有就绪的就保留寄存器号，最后，分配单元将指令以及相关的信息写入发射队列中。</p><p>在一个指令执行完成的时候，会发送一个唤醒信号，包含重命名之后的寄存器 ID 以及相应的值。发射单元此时根据信号匹配发射队列中的指令的源操作数寄存器 ID，并将匹配的项的就绪位设置为 1。一旦一条指令的所有有效操作数都就绪了（指令被唤醒了），那么这个指令就可以考虑被选择单元选择发射执行了。需要注意的是信号只会发送一次，如果有指令还没进入发射单元的话将无法收到信号，此时需要修改寄存器映射表中的记录，将对应的寄存器设置为可用。另外，为了避免死锁，在分配器写入发射队列项之前，也需要检查是否有信号。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523182306.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523182306.png"></a></p><p>正如前面所说，唤醒信号是在一个指令产生了操作结果后发出的。如图所示，如果每个指令都等到执行完成之后才唤醒的话，那么就会造成唤醒前有三个周期在空等。为了进一步提高流水线的利用率，唤醒信号可以在指令选择阶段就发出，这样等到下一条指令准备开始执行的时候，上一条指令恰好执行完成，通过转发电路，可以直接获取操作结果，避免流水线气泡。如果指令选择和唤醒不能在同一个周期完成的话，就会带来很大的性能损失。</p><p>产生唤醒信号有多种办法。一种是在指令执行前三个周期的时候就发出信号，当然具体几个周期需要根据指令的类型确定，例如整数加法操作可能只需要一个周期，而整数乘法周期则需要多个周期完成。另一种办法是将就绪位实现为移位寄存器，在指令选择之后，根据指令的执行周期数，将移位寄存器对应位置的位设为 1，同时在每一个周期都进行移位操作。这样，在第 0 位被置为 1 的时候操作数就就绪了。需要注意的是这些方法只适合延迟已经确定的指令。对于访存指令，由于实际的延迟取决于是否命中 TLB、数据缓存等，需要等到地址计算完成并访存结束之后才能唤醒。一种优化是在缓存返回命中的时候就直接唤醒，在缓存未命中的时候则等到访存结束再唤醒。另一种优化则是预测执行，在缓存未命中的时候付出一定代价，加速缓存命中的执行速度。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523183505.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523183505.png"></a></p><p>指令选择单元检查一条指令的操作数是否就绪和执行单元是否空闲。例如，一个只有一个乘法器的 CPU 是无法同时执行两条乘法指令的。指令选择单元非常重要，正如前面所说，为了支持指令“背靠背”地执行，选择逻辑必须在一个周期内完成。为了提高性能，现在的 CPU 通常会实现不止一个指令选择器，而是将其拆分成多个仲裁器或调度器。例如，一个支持 4 发射的 CPU 可能会实现 2 个或 4 个仲裁器，不同的执行单元静态地绑定到一个仲裁器。指令重命名之后，就会根据指令类型发送到不同的仲裁器的发射队列中。当就绪的指令多于发射宽度的时候，调度器需要根据一定的算法确定不同指令的优先级。</p><p>当指令发射到执行单元开始执行之后，对应的发射队列项就已经可以安全地回收了。一个例外是如果这条指令是访存指令，并且处理器使用了预测唤醒的话，那么就要等到安全的唤醒之后才可以回收。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523190215.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523190215.png"></a></p><p>对于在发射后读取寄存器的设计来说，发射器就不需要保存寄存器的值了，但是在唤醒和执行之间多了一个读取寄存器的周期。另外，发射后读取需要寄存器堆有和发射宽度一样多的读端口，而发射前读取只需要寄存器堆有和机器宽度一样多的端口。有点反直觉的是发射宽度有时候会大于机器宽度。这是因为不同的发射队列绑定了不同的执行单元，一个 4 发射的整数执行单元和一个 4 发射的浮点数执行单元的发射宽度是 8。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523194553.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523194553.png"></a></p><p>由于读端口的数量会影响芯片面积以及功耗，所以在发射后读取的设计中需要想办法减少读端口的数量。Alpha 21264 的优化方法是将寄存器堆分为两个复制的堆，每个有减半数量的读端口。而处理器还可以大胆地使用比最坏情况更少的读端口。为了解决读端口不足的问题，调度器可以互相协商，如果发射的指令多于读端口，那么一些调度器就暂停发射指令。但是分布式的调度器本来就是为了降低延迟，采取这样主动协商的办法可能导致延迟上升。另一种被动的方法是调度器一律发射指令，在指令执行发现读端口不足的情况下，取消指令并重新发射。这种情况有可能导致活锁问题，所以需要调度器采用更复杂的调度算法。</p><p>以上的调度算法只适用于非访存操作的。访存操作的发射要更加复杂，在一个访存操作发射的时候，要确保它和其他的访存操作没有冲突。负责处理内存依赖的机制就叫做内存消歧义策略。不同的 CPU 可能会采用不同的策略，大概可以分为非预测和预测的消歧义策略。非预测的消歧义策略在确定一个内存操作不会和之前任何的内存操作产生依赖的时候才允许执行；而预测策略则是想办法预测一个内存操作是否和另一个正在执行的内存操作产生依赖。过于保守的策略会限制指令的并行度，而过于激进的策略则会导致恢复的机制非常复杂，预测失败的时候的能耗也会急剧上升。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523205130.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523205130.png"></a></p><p>非预测消歧义策略主要有全序、偏序、加载&#x2F;存储序。全序中，所有内存操作都是按照顺序执行的，目前已经没有乱序处理器会使用这种办法，因为这样会大大减少并行度。剩下的策略允许加载操作相对于存储操作乱序执行。加载&#x2F;存储序中，加载操作按照自己的顺序执行，存储操作按照自己的顺序执行，但是加载操作不需要等待之前的存储操作访存完毕。而在偏序中，加载操作可以乱序执行，只要之前的所有存储操作的内存地址已经计算完毕即可。</p><p>一旦存储操作的内存地址计算完毕，CPU 就可以进行内存消歧义了。所以一些 CPU 会将存储操作进一步划分为两个子任务，一个是计算地址，另一个是进行实际的写入操作。</p><p>在 AMD K6 处理器中，实现的是加载&#x2F;存储序。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523205728.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523205728.png"></a></p><p>在访存流水线中，加载队列用于按照程序顺序存放加载指令。指令在重命名之后插入到这个队列，直到它的操作数就绪并且到达了队列头部。地址生成器则是计算访存操作的地址。存储队列和加载队列类似，按照程序顺序存放存储指令，也是在重命名之后插入，并且等待操作数和到达队列头部。存储缓冲区则按程序顺序存放了存储操作，当一个操作变成 CPU 中最旧的指令的时候才会更新内存。值得注意的是，存储操作不需要等存储数据就绪就可以发射，也就是存储缓冲中的存储指令还有可能需要等待存储数据执行完成才能写入内存。</p><p>加载指令会将自己的地址和存储缓冲中的比自己旧的指令的地址做比较，同时如果正在计算地址的存储指令更旧，也会和它的地址做部分比较。之所以是部分比较是因为在比较的时候地址还没能计算完毕，所以如果一部分的位相同就认为是相同。最后加载指令检查调度器看看是否还没有更旧的存储指令没有计算出地址。如果一条加载指令和任何之前的存储指令地址相同，或者发射队列还有更旧的存储指令，整个加载流水线就需要暂停。虽然看起来加载需要按序执行有点多余，但是这是一种实现 x86 内存语义的简单方法，也就是存储需要按顺序可见，加载需要看起来像是按序执行的。Intel Core 处理器中，这个一致性要求也实现了，但是它允许预测的内存消歧义，也允许加载乱序执行，甚至允许在有存储没有计算出地址的情况下执行。</p><p>对于预测内存消歧义来说，加载指令不需要等待之前的存储指令计算出地址，但是需要特殊的硬件来识别出错误的预测并恢复执行。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523221459.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523221459.png"></a></p><p>图中可以看出多了一个等待表，内容是使用虚拟地址索引的 1024 个比特。当一个加载依赖于一个更早的存储指令的时候，会触发存储-加载陷阱，并相应地更新等待表，将加载指令地址相应的位置位。为了避免等待表里全部都是 1，每 16384 个周期等待表就会重置一次。</p><p>加载指令将计算好的地址写入到加载队列中，并且比较更新的加载指令的地址，如果有相同的，会触发加载-加载内存违例陷阱，这个陷阱会使得处理器从触发陷阱的指令处开始执行，如果没有需要触发陷阱，那么加载指令就继续访问内存和缓存。</p><p>在内存消歧义阶段，存储指令同样将自己的地址写入到存储队列。另外，它们还会检查是否有更新的加载指令的地址和自己的相同，如果有，就会触发存储-加载违例陷阱，并继续从加载指令开始执行。另外等待表也会更新，避免这种情况再次发生。需要注意的是没有存储-存储违例，因为存储只有在指令提交才生效，而指令提交本身就是按程序顺序的。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523223318.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523223318.png"></a></p><p>对于加载指令的数据消费者而言，一种保守的唤醒策略是在计算出缓存是否命中之后才发出唤醒的信号，这样会引入流水线气泡；另一种策略是预测唤醒，不管是否命中都发出唤醒信号，避免气泡。但是在缓存未命中的情况下，需要取消依赖指令的执行，并重新放入发射队列。如果发射队列这时候没有空闲位置，并且队列里的指令都依赖于这个被取消的指令，那么就会发生死锁。</p><p>死锁的解决办法也有多种，各有各的优劣势，一种办法是直接清空比被取消的指令更新的所有指令，然后重新开始执行。这种方法在预测错误很多的时候会造成性能的急剧下降。另一种方法是在确定一条指令不会被重新发射之前，不要清空对应的发射队列项。实现方法是每一个队列项都有一个发射位标记这个指令是否已经发射，如果已经发射了那指令选择逻辑就不会考虑这条指令。这种方法的性能损失比前面那种方法要少，但是需要很深的发射队列，但通常发射队列不会很长，如果队列里全是已发射而未完成的指令，同样会造成性能下降。因此，一些处理器选择另外实现一条重放队列，已经发射但还没有执行完毕的指令会先进入重放队列，在需要重发射的时候，调度器则给重放队列里的指令更高的优先级。</p><h2 id="执行"><a href="#执行" class="headerlink" title="执行"></a>执行</h2><p><a href="/programming/processor-microarchitecture/Pasted-image-20220523224441.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220523224441.png"></a></p><p>在执行阶段，指令结果被真正地计算出来。不同的指令有不同的复杂度，因此也有不同的延迟。现在的 CPU 一般会有多个不同功能的功能单元。例如常见的有 FPU 浮点计算单元，ALU 算术逻辑单元，AGU 地址生成单元以及 BRU 分支计算单元。数据缓存是访存操作中的重要组成部分，同时访存操作还包括了地址转换单元，将虚拟地址转换为物理地址。另一个重要的结构是旁路网络，它将源数据和计算结果在不同的功能单元传递。另外，如果一些有依赖的指令想背靠背地执行，也需要旁路网络提前转发操作结果。</p><p>ALU 执行的是简单的整数操作，例如加法、减法、位操作，而整数乘除法因为太过复杂，通常由单独的功能单元执行（IMUL、IDIV）。另外，一些处理器为了节省芯片面积和能耗，会利用浮点数单元来计算整数乘除法，也就是先将整数转换为浮点数之后进行运算，最后再转换为整数。</p><p>内存地址空间模型有线性模型和分段模型，线性模型对于程序来说内存就是一整段连续的内存地址，而分段模型就是将内存分为不同的段，在段内使用偏移量访问。x86 的分段模型是最复杂的地址模型之一，它的 AGU 输入有基址、偏移量、尺度和索引四个部分。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524094644.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524094644.png"></a></p><p>最终的地址为 Offset &#x3D; Base + (Index × Scale) + Displacement，另外 AGU 还需要检查地址是否越界。由于计算过程太复杂，可能无法在高时钟频率的条件下计算出来，所以 AGU 还可能分为多级流水线。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524094812.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524094812.png"></a></p><p>另一种办法是将访存操作拆分为更多的微码，但是这样会牺牲一定的发生宽度，而且访存指令就不能使用简单译码器了。</p><p>分支单元计算的是下一个需要执行的指令地址：</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524100128.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524100128.png"></a></p><p>一般分支有三个寻址模式：直接绝对寻址、直接相对寻址以及间接寻址。直接绝对寻址的指令直接包含了下一个 PC 的值，直接相对寻址则包含了相对当前 PC 的偏移量，间接寻址则指定一个存放了下一个 PC 的寄存器。</p><p>浮点计算单元一般比较复杂，占用芯片面积很大，内部也是有多级流水线的。浮点寄存器和通用寄存器通常是分开的寄存器堆。FPU 通常除了支持乘除法以外，还可以支持三角函数运算，求平方根等。x86 除了支持 IEEE 754 的 32 位和 64 位浮点数以外，还支持 80 位的浮点数操作。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524100637.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524100637.png"></a></p><p>SIMD 计算单元通常用于向量计算。在最早的 Cray 超算里，SIMD 实现为向量处理器操作，支持上百个元素的向量。而 Intel CPU 的 SIMD 功能最早是为了支持游戏和多媒体应用，这些应用的向量不会很大，只有 4 个元素或者多一点。由于设计理念的不同，通常将元素较多的操作称为向量操作，而元素较少的称为 SIMD 操作。</p><p>SIMD 单元内部同样有浮点计算单元等，而且每一个单元内部还会进一步划分 Lane。一个 Lane 对应的就是一个元素的操作。Lane 的数量可能小于 SIMD 操作的宽度，在这种情况下，一个 SIMD 操作可能需要多于一个时钟周期完成。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524101428.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524101428.png"></a>假如一个 Lane 宽度是 64 位，SSE 操作的位宽是 128 位，那么如果有两个 Lane，一次 SSE 操作就可以在一个周期内完成（中图），否则需要两个周期（下图）。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524101640.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524101640.png"></a></p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524101653.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524101653.png"></a></p><p>如果等到写回阶段执行的时候才读取数据，会造成流水线气泡，流水线越深的时候气泡带来的性能损失越严重。为此，现代 CPU 都实现了旁路网络，等到计算结果一出来，就直接转发给消费者。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524101836.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524101836.png"></a></p><p>这种设计需要添加额外的电路和多路选择器。有一些处理器为了减少设计复杂度和提高时钟频率，也会选择不实现旁路网络，引入流水线气泡开销，并通过乱序执行填充气泡。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524102542.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524102542.png"></a></p><p>没有旁路网络的 CPU 中，功能单元的输出直接连接到寄存器堆的写端口，寄存器堆的读端口连接到功能单元输入。在简单的旁路网络中，输出除了连接到写端口，还直接和寄存器的读端口一起，经过多路选择器，连接到功能单元的输入端，形成了结果总线。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524104005.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524104005.png"></a></p><p>在比较深的流水线里面，数据可以从不同阶段的生产者转发到不同阶段的消费者。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524104155.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524104155.png"></a></p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524104206.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524104206.png"></a></p><p>而对于顺序处理器，旁路网络的设计可能会非常复杂。在顺序处理器中，写回阶段必须等到流水线中最慢的功能单元执行完毕之后才可以执行。对于这种延迟写入结果的操作，就称为 Staging，存放结果的寄存器成为 Staging 寄存器。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524104652.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524104652.png"></a></p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524104734.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524104734.png"></a></p><p>现在的处理器有很多功能单元，如果每一个单元都和其他的连接，就会形成极其复杂的旁路网络，影响时钟频率的提升。所以，一般浮点数单元和 SIMD 单元、整数单元有各自的旁路网络，减少网络复杂度。另外，AGU 一般和整数单元有关，它们也会有旁路网络连接。</p><p>当然，为了进一步降低复杂度，提升时钟频率，可以采用分而治之的原则，将不同的电路聚合在一起，不同的聚合体之间相互独立。</p><p>例如，我们可以允许一个功能单元只能旁路自己的操作结果，虽然会引入流水线气泡，但是可以降低电路复杂度，提高时钟频率，最终提高性能。这体现了系统设计中的 trade-off 考虑。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524105212.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524105212.png"></a></p><p>另一种思路是将寄存器堆分为两个，每个只有一半的端口（Alpha 21264），如果一个结果在其中一个寄存器堆产生，在另一个寄存器堆旁路使用，那么就需要额外的时钟周期复制一次。两个寄存器堆的内容是一样的。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524105402.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524105402.png"></a></p><p>更激进的做法是两个寄存器堆中的内容并不相同，执行结果只会写入局部的寄存器堆，而不会广播到另一个寄存器堆。需要使用的时候通过额外的复制机制使用。一个集群中的指令只会竞争自己集群内部的资源。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524105646.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524105646.png"></a></p><p>通常这种设计也会涉及到分布式的发射队列，依赖于分配阶段的指令分配机制来调度依赖的指令到同一个集群中执行。但是这对分配算法有很高的要求，需要尽可能平衡两个集群中的指令分配，不要出现一个集群很忙而另一个集群空闲的状态。</p><h2 id="提交"><a href="#提交" class="headerlink" title="提交"></a>提交</h2><p>为了让乱序执行的指令最终看起来像是顺序执行的，处理器在最后还需要一个提交阶段，用来强制指令以程序顺序修改最终的寄存器状态。一个 CPU 有两个状态：架构上的状态以及预测的状态。预测的状态就是架构状态加上还在执行的指令修改的状态。预测的状态并不保证最终会落实到架构状态上，因为分支预测有可能出错，也有可能在执行过程中发生异常。如果一个 CPU 可以在异常发生的时候，保证异常指令后的所有指令都不会真正修改架构状态，那么就说这个 CPU 可以提供精确的异常。</p><p>架构状态包含了每一个逻辑寄存器的状态加上内存的状态。所以，存储操作必须等到指令提交的时候才可以修改内存，而正在执行的加载指令需要检查是否在同一个地址上有更早的存储指令还没有提交。</p><p>在 Intel P6 中，使用的是 ROB 重排缓冲区来暂时保存预测状态，RRF 退休寄存器堆保存了架构状态。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524110550.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524110550.png"></a></p><p>在分配阶段的时候，从重排缓冲区分配一项，而在提交的时候释放重排缓冲区项，并更新架构寄存器。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524110742.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524110742.png"></a></p><p>提交的时候，需要通知指令队列和发射队列中的指令，指示它们需要从退休寄存器堆中读取值，而不是去重排缓冲区读。正如前面所说，所有还没执行或者读取寄存器的指令都需要检查这次通知，重命名表也需要更新。</p><p>基于重排缓冲区的乱序执行最好是在发射前读取，否则会变得很复杂。而基于合并寄存器堆的可以使用发射后读取的方式。但是这种办法相比重排缓冲区的方法，需要更复杂的管理机制。它需要一个额外的列表来保存可用的物理寄存器，而且在它能确定一个物理寄存器不再需要之前，都不能释放这个物理寄存器。一般来说，一条指令 A 写入的物理寄存器需要在另一条更新的指令 B 写入相同的逻辑寄存器后才可以释放。</p><p><a href="/programming/processor-microarchitecture/Pasted-image-20220524111742.png" data-fancybox="gallery" data-caption=""><img src="/programming/processor-microarchitecture/Pasted-image-20220524111742.png"></a></p><p>对于预测执行出错的情况，需要清空流水线，并将 PC 重新设置为正确的值后开始执行。前端主要需要恢复分支预测表和 PC 寄存器，后端则需要恢复分配表、发射队列、重排缓冲区等等。对于 Intel Pentium 处理器来说，如果发生了预测错误，那么它就会等预测错误的那条分支指令以及这之前的所有指令都提交后，才开始恢复过程。恢复的过程只需要将所有的逻辑寄存器指向退休寄存器堆即可。</p><p>对于合并寄存器堆的 CPU 来说，一般不会等到分支指令提交之后才开始恢复过程。在执行的时候，CPU 会保存一个日志，记录重命名表的修改过程以及指令分配过的资源。需要恢复的时候，就根据日志恢复。日志项一般包含指令写入的逻辑寄存器以及分配给这条指令的物理寄存器或者分配给同一个逻辑寄存器的上一个物理寄存器编号。如果日志太长，那么恢复过程也会很长，所以一些处理器使用了检查点的方式截断日志。</p><p>异常一般是在提交的时候才处理，一方面我们需要保证触发异常的指令不是预测执行的，另一方面我们需要在异常发生的时候保证架构状态和异常指令之前所有指令执行完毕一样。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>天空计算——多云时代的分布式计算</title>
      <link>https://blog.howardlau.me/programming/sky-computing.html</link>
      <description>
        <![CDATA[<p>Berkeley 的 RISELab 最近又搞了个大新闻，在 <a href="https://arxiv.org/abs/2205.07147">The Sky Above The Clouds</a> 论文中提出了 Sky Computing]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 20 May 2022 06:59:03 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Berkeley 的 RISELab 最近又搞了个大新闻，在 <a href="https://arxiv.org/abs/2205.07147">The Sky Above The Clouds</a> 论文中提出了 Sky Computing 的概念。总的来讲，尽管目前市场上有许多云服务可供选择，但是不同云服务使用的 API 不同，数据在不同云服务之间迁移也很麻烦，需要用户自己处理数据的迁移和不同云服务 API 调用的编写。为了更方便地使用不同云服务厂商的优势服务，需要有一个中间层来连接不同的云服务，并向用户提供一个统一的 API 来描述任务，由中间层来负责在不同云之间调度服务。</p><p>举个例子，如果用户想使用脱敏的线上数据训练一个机器模型并部署，那么他可以先在 Azure 云上进行数据脱敏，然后使用 GCP 云的 TPU 快速低成本地训练模型，然后将训练好的模型传输到 AWS 云上使用 T4 提供线上服务。</p><p>这样做的好处一是可以让使用云服务更加简单，从而扩大云服务的市场；二是可以促进更加专门的云服务；第三是允许整合不同的计算资源；第四则是出于合规性的考虑，例如对于数据存放位置在不同国家地区可能有不同的要求。</p><p>但是这个中间层并不是要重新定义一个统一的 API，然后让不同云厂商去实现，相反，它是定义了一套兼容特性集合，实现了相应特性的云服务可以加入到 Sky 中，而用户则可以在任务中指定需要的特性，由中间层自动决定可以调用哪些云服务。作者也希望未来能够提供开源的测试用例，用来测试不同的云服务是否兼容某个特性。</p><table><thead><tr><th></th><th>分区多云</th><th>可移植多云</th><th>Sky</th></tr></thead><tbody><tr><td>在不同的云上运行相同的应用</td><td>否</td><td>是</td><td>是</td></tr><tr><td>对用户来说云是否透明的</td><td>否</td><td>否</td><td>是</td></tr><tr><td>统一 API（所有的云提供同样的 API）</td><td>否</td><td>是</td><td>否</td></tr><tr><td>提供不同级别的 API</td><td>是</td><td>否</td><td>是</td></tr></tbody></table><p>Sky 的目标并不是想要提供完整的兼容性在所有的云上运行所有的应用，而是提供一部分兼容性，使得一部分应用可以在一部分的云上运行。这是由于提供太强的兼容性会使得标准过于复杂而难以实现，并且可能阻碍创新，放宽兼容性的要求可以使得 Sky 更有实用性，也允许创新的出现。</p><p>Sky 希望用户感知不到云服务的存在，用户无需和云服务厂商打交道。虽然现在也有 Kubernetes 这种分布式集群管理调度软件，但是它没有办法让用户不关心云服务的细节，用户还是需要自己到云服务厂商购买服务并部署软件，而且它也不能自动打通不同云服务，用户想要在不同的云上运行应用还得自己手动迁移数据。Sky 则免除了这些烦恼，用户只需要指定任务的运行脚本等，然后让 Sky 自动完成即可。</p><p>Sky 并不是对未来的被动预测，而是号召大家一起来实现这个宏图，构建一个细粒度的双边市场，避免价格战或者公司勾结，并在初期推出“杀手应用”来推进 Sky 的使用。例如机器学习就是一个可能的杀手级应用，用户可以使用 Ray 配合 Sky 在不同的云上同时进行超参数搜索，而无需分别编写不同的云 API 脚本。</p><p>一开始 Sky 支持的任务可能相当有限，例如只支持使用 DAG 描述的批处理任务，同时支持的云特性也比较有限，可能只支持 GPU 等。这样做可以让开发者节省精力专注在常用的应用场景中，随着使用人群的扩大，再根据需求排定优先级开发其他的功能，也就是使用一种类似“敏捷开发”的方法来迭代系统。</p><p>而这个中间层重要的一个组件就是优化器，就和数据库中的优化器一样，负责将用户的输入根据不同云平台的成本以及迁移开销，转换为实际的执行计划，从而最小化用户的成本。虽然现在用户也可以自己按照一定的方法来计算，但是当任务变得复杂之后计算也会变得困难，同时云服务的市场价格可能随时变动，让计算机去计算更合适。优化器生成执行计划后，先由分配器在不同的云服务预留资源，如果预留失败，就让优化器重新生成计划，然后执行器在预留的资源中真正地执行任务。</p><p><a href="/programming/sky-computing/Pasted-image-20220520223301.png" data-fancybox="gallery" data-caption=""><img src="/programming/sky-computing/Pasted-image-20220520223301.png"></a></p><p>而在未来，作者展望云服务厂商之前可以达成对等协议，从而使得数据在对等的云服务之间传输不需要额外的费用，进一步促进 Sky 的发展，降低用户的使用成本。</p><p>总的来说，随着云计算的成熟，以及 Serverless 的成熟，能让用户能快速上手，不用纠结选择什么云服务的什么实例规格，只需要描述自己的任务就能轻松上云的 Sky，还是很有前景的，或许是分布式计算的又一个里程碑。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>系统设计中的端到端原则</title>
      <link>https://blog.howardlau.me/programming/end-to-end-arguments-in-system-design.html</link>
      <description>
        <![CDATA[<p>End-To-End Arguments in System Design]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 19 May 2022 08:42:09 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>End-To-End Arguments in System Design 提出在系统设计中，将一些功能放到底层实现可能是没有价值甚至多余的，相比起实现它们的成本而言。相反，只有终端的应用知道自己的具体需求是什么，无论怎么样都需要在终端的应用中实现相同的功能。在底层实现的功能往往只能起到提高性能的作用。</p><p>因为底层的抽象层往往对上层应用细节不了解，如果提供太多功能，那么所有使用到的应用都需要负担额外的开销，哪怕实际上它们并不需要相关的功能。而且，有时候底层实现的功能可能核应用的需求不符合，这时候应用反而需要想办法绕过或者修改底层，来实现自己的功能。哪怕底层的功能确实是应用所需要的，其提供的保证往往不足或者过多，应用仍然需要实现自己的一套功能。</p><p>例如，在文件传输中，哪怕底层采用了可靠的传输协议，在发送方读取文件或者接收方写入文件的过程中仍然有可能发生错误，导致文件传输出错。而这时候底层的协议并不知道文件传输的语义，也就不能对文件传输的全过程进行校验。所以，应用还是需要实现文件级别的校验和，从而确保文件传输的正确性。如果接收方发现校验和出错，那么可以简单地要求发送方重传。在这种情况下，底层网络是否可靠也不影响应用的正确性。但如果底层网络太过不可靠，那么可能导致无限的出错和重试，造成应用无法正常运行。此时，一个可靠的底层网络可以提高传输的成功率，减少重试次数，节省网络流量，提高系统的整体性能。</p><p>而贴近生活一点的例子，比如微信，即使底层使用了 TCP 的连接，我们仍然不能保证消息在发送到服务器之后服务器处理不出错，或者你的号没有被封，所以微信在应用层还是需要一套单独的确认机制来确认消息有没有发送成功。这时候的终端指的是双方的微信软件。但是即使发送成功了，你也没办法保证对面一定看到了消息，如果你想进一步确认，那么你就需要发一条“收到请回复”的消息，然后等对方回复。这时候的终端就是你和聊天对象。</p><p>可以看出，端到端的对象即使在同一个应用中，随着终端定义的不同，也会发生改变。再举一个例子就是语音通话。实时语音通话的时候，底层网络如果提供了可靠性，那么意味着会发生重传等情况，造成延迟越来越大。但是在语音通话的过程中，出现偶尔的破音、静音都是可以接受的，大不了没听清的人让对面再说一次。哪怕断线了，也可以重拨。这时候底层的可靠性就是完全多余的，这也是为什么现在的实时音视频协议喜欢用 UDP 协议而不是 TCP 协议。但是，假如不是实时音频通话，而是远程录音，那么如果发生丢包等情况，是没有办法让说话的人重新说一次的，而且由于是写入到录音文件中，实时性没有要求。在这种情况下，底层网络提供可靠性可以简化应用程序的设计，也提高了录音的质量。随着终端的改变，对于不同层次的功能设计要求可能也会发生巨大的改变。</p><p>而例如在操作系统中，以前的设计思想是操作系统内核包办一切，例如网络、存储等。但是随着硬件和应用的发展，也出现了类似 DPDK、SPDK 这种应用程序直接接管网络和存储硬件，自己从头实现网络存储栈的。也有类似 eBPF 等技术，修改内核的行为，来适应应用的需求以及提升性能。从另一方面看，这也是 Linux 实现的一个败笔，大家也喜欢在内核修修补补，最后内核变成了忒修斯之船。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用协程提高流水线利用率</title>
      <link>https://blog.howardlau.me/programming/improving-ilp-using-coroutines.html</link>
      <description>
        <![CDATA[<p>CppCon 2018: G. Nishanov “Nano-coroutines to the Rescue! (Using Coroutines TS, of Course)” 的笔记。</p>
<p><a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 12 May 2022 07:14:57 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>CppCon 2018: G. Nishanov “Nano-coroutines to the Rescue! (Using Coroutines TS, of Course)” 的笔记。</p><p><a href="https://zhuanlan.zhihu.com/p/512942308">无栈协程</a>切换的开销很小，几乎等于一次函数调用，而在 LLC Miss 的时候，访问内存通常需要 60 ns 以上的延迟，而如果访存命令后的命令都依赖于访问的数据，那么此时 CPU 流水线就被阻塞了，无法充分并行执行指令。而 CPU 通常提供了 prefetch 指令，<a href="https://zhuanlan.zhihu.com/p/511572414">内存系统也提供了一定程度的并行度</a>，可以先发出预取命令，然后切换到其他的请求执行，使得多个访存指令重叠，充分利用流水线。这时候协程也可以看成是<a href="https://zhuanlan.zhihu.com/p/513073896">软件实现的指令级并行调度技术</a>。而如果使用线程切换，那线程切换的开销会比请求内存的开销还大，所以不太合适。而无栈协程的低开销则可以完美用来重叠访存和计算。</p><p>总体的思路是，把 prefetch 当成一个类似 socket 编程中的异步 read 调用，发出 prefetch 指令之后就暂停当前的协程，切换到别的协程运行。在并发请求足够多的时候，同时可以发出足够多的 prefetch 指令，而且，大概率在切换到一个需要真正访问数据的协程的时候，数据已经在 Cache 中，此时访问延迟将大大降低。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> Iterator, <span class="keyword">typename</span> Value, <span class="keyword">typename</span> Found, <span class="keyword">typename</span> NotFound&gt;</span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">binary_search</span><span class="params">(Iterator first, Iterator last, Value val, Found on_found, NotFound on_not_found)</span> -&gt; root_task </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> len = last - first;</span><br><span class="line">  <span class="keyword">while</span> (len &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">auto</span> half = len / <span class="number">2</span>;</span><br><span class="line">    <span class="keyword">auto</span> middle = first + half;</span><br><span class="line">    <span class="keyword">auto</span> middle_key = <span class="keyword">co_await</span> <span class="built_in">prefetch</span>(*middle);</span><br><span class="line">    <span class="keyword">if</span> (middle_key &lt; val) &#123;</span><br><span class="line">      first = middle + <span class="number">1</span>;</span><br><span class="line">      len = len - half <span class="number">-1</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      len = half;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (middle_key == half) &#123;</span><br><span class="line">      <span class="function"><span class="keyword">co_return</span> <span class="title">on_found</span><span class="params">(val, middle)</span></span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function"><span class="keyword">co_return</span> <span class="title">on_not_found</span><span class="params">(val)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面是一个二分查找的例子，在数组很大的时候，可能不能完全放入 Cache 中，而且 middle 的访问模式接近于随机。这时候，如果并发请求很多（比如在一个数据库中的 Join 操作），那么就可以先发出 prefetch 指令，然后切换到另一个协程。而这个 prefetch 函数也很简单，只需要返回一个 Awaitable，在暂停协程的时候发出真正的 prefetch 指令，然后在协程恢复运行的时候发起读取内存的请求并返回值即可。 </p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">prefetch_awaitable</span> &#123;</span><br><span class="line">  T&amp; value;</span><br><span class="line">  <span class="built_in">prefetch_awaitable</span>(T&amp; value) : <span class="built_in">value</span>(value) &#123;&#125;</span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">auto</span> <span class="title">await_suspend</span><span class="params">(coroutine_handle&lt;&gt; h)</span> </span>&#123;</span><br><span class="line">    _mm_prefetch(<span class="built_in">static_cast</span>&lt;<span class="type">char</span> <span class="type">const</span> *&gt;(std::<span class="built_in">addressof</span>(value))), _MM_HINT_NTA);</span><br><span class="line">    scheduler.<span class="built_in">push_back</span>(h);</span><br><span class="line">    <span class="keyword">return</span> scheduler.<span class="built_in">pop_front</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function">T&amp; <span class="title">await_resume</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> value; &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后，对于一大堆的请求，我们生成很多协程，然后运行即可，当然，硬件能够支持的并发访存请求有限，我们需要限制并发数，否则可能导致 thrashing：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">parallel_lookup</span><span class="params">(std::vector&lt;<span class="type">int</span>&gt; <span class="type">const</span>&amp; arr, std::vector&lt;<span class="type">int</span>&gt; <span class="type">const</span>&amp; lookups, <span class="type">int</span> concurrency)</span> </span>&#123;</span><br><span class="line">  <span class="type">size_t</span> found = <span class="number">0</span>;</span><br><span class="line">  <span class="type">size_t</span> not_found = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">  <span class="function">throttler <span class="title">t</span><span class="params">(concurrency)</span></span>;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">auto</span> key : lookups) &#123;</span><br><span class="line">    t.<span class="built_in">spawn</span>(<span class="built_in">binary_search</span>(v.<span class="built_in">begin</span>(), v.<span class="built_in">end</span>(), key, [&amp;](<span class="keyword">auto</span>) &#123; ++found; &#125;), [&amp;](<span class="keyword">auto</span>) &#123; ++not_found; &#125;);</span><br><span class="line">  &#125;</span><br><span class="line">  t.<span class="built_in">join</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来还是需要解决怎么让这些协程运行起来的问题，也就是 throttler 怎么实现。总的来说就是在生成任务的时候查看并行的协程是否达到了限制，有的话就优先执行队列里面的，否则就加入任务队列。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">throttler</span> &#123;</span><br><span class="line">  <span class="type">size_t</span> limit;</span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">throttler</span><span class="params">(<span class="type">size_t</span> limit)</span> : limit(limit) &#123;</span>&#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">spawn</span><span class="params">(root_task t)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (limit == <span class="number">0</span>) &#123;</span><br><span class="line">      scheduler.<span class="built_in">pop_front</span>().<span class="built_in">resume</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">auto</span> h = t.<span class="built_in">set_owner</span>(<span class="keyword">this</span>);</span><br><span class="line">    scheduler.<span class="built_in">push_back</span>(h);</span><br><span class="line">    --limit;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">on_task_done</span><span class="params">()</span> </span>&#123; ++limit; &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">join</span><span class="params">()</span> </span>&#123; scheduler.<span class="built_in">run</span>(); &#125;</span><br><span class="line">  ~<span class="built_in">throttler</span>() &#123; <span class="built_in">join</span>(); &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>为了能够在运行结束的时候反馈给 throttler，协程的 task 类型需要提供接口设置对应的 throttler。而为了在生成大量协程的时候提高效率，可以使用自定义的内存分配器提高内存分配效率。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">root_task</span> &#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">promise_type</span> &#123;</span><br><span class="line">    throttler *owner = <span class="literal">nullptr</span>;</span><br><span class="line">    <span class="function">suspend_never <span class="title">final_suspend</span><span class="params">()</span> </span>&#123; owner-&gt;<span class="built_in">on_task_done</span>(); <span class="keyword">return</span> &#123;&#125;; &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> *<span class="keyword">operator</span> <span class="title">new</span><span class="params">(<span class="type">size_t</span> sz)</span> </span>&#123; <span class="keyword">return</span> allocator.<span class="built_in">alloc</span>(sz); &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="keyword">operator</span> <span class="title">delete</span><span class="params">(<span class="type">void</span> *p, <span class="type">size_t</span> sz)</span> </span>&#123; allocator.<span class="built_in">free</span>(p, sz); &#125;</span><br><span class="line">    <span class="comment">/* ... */</span></span><br><span class="line">  &#125;;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">auto</span> <span class="title">set_owner</span><span class="params">(throttler *owner)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">auto</span> result = h;</span><br><span class="line">    h.<span class="built_in">promise</span>().owner = owner;</span><br><span class="line">    h = <span class="literal">nullptr</span>;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">  &#125;</span><br><span class="line"> </span><br><span class="line">  ~<span class="built_in">root_task</span>() &#123; <span class="keyword">if</span> (h) h.<span class="built_in">destroy</span>(); &#125;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">  coroutine_handle&lt;promise_type&gt; h;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>上面的代码在演讲者的服务器上，每次查找平均只要 7.56 ns，达到了 1.46 的 IPC，效率非常高。他还和自己手写状态机的代码进行了对比，手写状态机的实现平均每次查找需要 10 ns。他觉得这可能是由于编译器在编译协程的时候，会先进行一次函数优化，然后再编译成状态机，在编译为状态机后，针对每一个状态再进行优化。而对于手写的状态机，编译器可能就比较难优化。所以，协程甚至有“Negative-cost Abstraction” 的效果。</p><p>这个技术有一个缺点，那就是并发度应该设置多少需要手动调整， 和硬件支持的并发度相关，需要通过不断测试调参数。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p><a href="https://www.youtube.com/watch?v=j9tlJAqMV7U">https://www.youtube.com/watch?v=j9tlJAqMV7U</a></p><p>视频里提到的两篇论文：</p><p><a href="https://dl.acm.org/doi/10.14778/3149193.3149202">Interleaving with coroutines: a practical approach for robust index joins (VLDB 2018)</a></p><p><a href="https://dl.acm.org/doi/10.14778/3236187.3236216">Exploiting coroutines to attack the “killer nanoseconds” (VLDB 2017)</a></p><p>VLDB 2021 上有个利用了相同思想的 CoroBase：<a href="https://github.com/sfu-dis/corobase">GitHub - sfu-dis&#x2F;corobase: Coroutine-Oriented Main-Memory Database Engine, VLDB 2021.</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>内存系统的实现细节</title>
      <link>https://blog.howardlau.me/programming/the-memory-system.html</link>
      <description>
        <![CDATA[<p>The Memory System: You Can’t Avoid It, You Can’t Ignore It, You Can’t Fake It 和 Innovations in Memory System 是 Synthesis Lectures on]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 11 May 2022 06:06:42 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>The Memory System: You Can’t Avoid It, You Can’t Ignore It, You Can’t Fake It 和 Innovations in Memory System 是 Synthesis Lectures on Computer Architecture 系列中关于内存的两本书。这篇文章总结了一些内存系统的细节。</p><p>内存条通常没有什么计算能力，只是作为内存控制器的一个从设备。内存控制器连接到主板上的内存通道（平时装机说的双通道就是内存通道），一个内存通道由地址&#x2F;命令线和数据线组成。地址&#x2F;命令线是单向的，从内存控制器到内存芯片，包含 a 位宽的地址线以及 c 位宽的命令线；数据线是双向的，包含 d 位宽的数据线。下面假设 a &#x3D; 17, c &#x3D; 5, d &#x3D; 64。</p><p>内存通道通常运行在比 CPU 低的频率，我们买内存的时候标注的 DDR 是 Double Data Rate 的意思，也就是在时钟的上升和下降沿都传输一位数据，也就是说在一个时钟周期内，一条数据线可以传输 2d 位数据。需要注意的是，DDR 2400 的内存其实时钟频率是 1200MHz，而且地址&#x2F;命令线运行的是单倍的数据传输率，也就是一个时钟周期只传输 a 位地址和 c 位命令。</p><p>为了支持这么高的通道频率，受物理电气的限制，内存通道的走线要比较短，而且负载不能太高。所以，一个内存通道上能连接的内存是有限的。高端服务器能插几十根内存条，也是因为主板上有很多的内存通道，而且不同的内存通道是由不同的内存控制器驱动的。</p><p>DDR 协议方便了内存厂商和 CPU 厂商，但是也成为了创新的一个阻力。DDR 的升级通常会带来更高的带宽以及更低的能耗，虽然 DDR5 已经面世，DDR6 还是有很大的不确定性。因为技术的升级往往会带来更高的错误率，所以新的 DDR 技术也会增加更多的保证可靠性的功能。</p><p><a href="/programming/the-memory-system/16519962141.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16519962141.png"></a><a href="/programming/the-memory-system/16519915541.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16519915541.png"></a><a href="/programming/the-memory-system/Pasted-image-20220508170216.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/Pasted-image-20220508170216.png"></a><a href="/programming/the-memory-system/16518408411.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16518408411.png"></a><a href="/programming/the-memory-system/16518413591.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16518413591.png"></a></p><p>我们买的内存条叫 DIMM，电路板正面和背面都有 DRAM 芯片。主板上的插槽则对应内存通道，一个内存通道可能有多个插槽。DIMM 上有 Rank，一个 Rank 上的 DRAM 芯片是同时操作的，同时向数据总线读写数据。如果单个 DRAM 芯片的数据线是 8 位的，那么就叫做 x8 芯片，4 位的就叫 x4 芯片。假如数据总线是 64 位的，那么一个 Rank 需要 8 个 x8 芯片组合，或者 16 个 x4 芯片，这样才能满足数据总线的位宽。一个 DIMM 上可以有多个 Rank，例如正面是一个 Rank，背面是另一个。</p><p>一个内存通道里的一条数据线只会连接到一个 Rank 上的一个 DRAM 芯片引脚。如果内存通道支持 4 个 Rank，那么数据线就要驱动到 4 个不同引脚。为了避免总线过载，Rank 数量不能太大。一个内存通道里的地址&#x2F;命令线则同时连接到了通道里所有 Rank 的每一个 DRAM 芯片上。因为负载太大，所以地址&#x2F;命令线并没有采用 DDR，否则会导致信号不稳定。有时候为了进一步减少负载，DIMM 上面可能也有一个缓冲芯片，接收地址&#x2F;命令后，再由缓冲芯片广播到每一个 DRAM 芯片上，这样每个地址&#x2F;命令线都只需要驱动所有 DIMM 上一个单独的芯片就可以了。</p><p>因为 DRAM 芯片电路相对来说还是比较慢的，从一个 Rank 中读取数据可能需要花费 40 ns，如果我们每次都等上一个内存请求完成之后再进行下一个内存请求，那么内存系统将会慢的难以接受。和 CPU 类似，内存系统也有流水线。当内存请求在地址&#x2F;控制线上发送之后，和这个请求相关的一个 Rank 内 DRAM 芯片就开始激活电路，读取数据，然后再将数据放到数据总线上发回。当一个 Rank 还在读取数据的时候，地址&#x2F;控制总线就可以向其他 Rank 同时发起请求了。这样，不同 Rank 就可以同时并行地读取数据了。当然，最终数据的发回还是串行的，因为它们都连接到了一条共享的数据总线上。</p><p>这三个流水线阶段时间非常不平衡，例如发送地址&#x2F;命令只需要 1 ns，读取数据则可能需要 35 ns，最终数据总线的传输需要 5 ns。虽然一个内存通道可以支持多个 Rank，但也并不会太多，例如常见的台式机上面只有双通道四插槽，那么其实每个通道只有 4 个 Rank。如果我们只利用 Rank 的并行，那么最多只能有 4 个并行的请求，还无法充分地利用流水线重叠请求。</p><p>所以，每一个 Rank 内还会划分成多个 Bank。假如一个 Rank 内再划分了 8 个 Bank，那么一个内存通道就有 32 个 Bank。每个 Bank 可以独立地处理请求，大大地增加了并行度，提高了数据总线的利用率。如果一个 Rank 由 8 个 DRAM 芯片组成，那么这个 Rank 里的每一个 Bank 都会横跨这 8 个芯片。每个 Bank 内部还会划分成子 Bank，一个子 Bank 指的是那个 Bank 的在每一个内存芯片上的每一部分。每个子 Bank 包含子 Array 和比特矩阵，这样做是为了减少数据查找的延迟以及互联的开销。一个子 Array 就是子 Bank 里比特矩阵<strong>矩阵</strong>的一行。每个比特矩阵其实就是 DRAM 单元组成的一个矩阵，每个 DRAM 单元存储 1 位数据。如果这个比特矩阵有 512 行 x 512 列，那么这个矩阵就有 256 Kb 容量。横跨一行的线叫字线，而在每一行里，每个 DRAM 单元连接的纵向的线叫位线。在每个矩阵下面都有负责处理位线信号的放大器。读取一行数据到放大器需要大概 13 ns，这也叫行激活，由 RAS 命令发起。</p><p>CPU 以一个 Cache Line 为单位向内存发起读写请求。常见的 Cache Line 大小是 64 字节。这 64 字节会分散在组成这个 Rank 和 Bank 的所有 DRAM 芯片上。如果一个 Rank 由 8 个 x8 的芯片组成，那意味着每个芯片贡献了 Cache Line 的 64 位，这 64 位又分散在了子 Bank 里的多个矩阵里。所以，一次 Cache Line 读取请求，可能会涉及到 64 个不同矩阵一行 512 位宽的数据的读取。所以，所有放大器加起来会保存 32 Kb（4 KB）的数据，一个 Bank 里保存有效数据的放大器就叫行缓冲。请求的 64 字节的 Cache Line 则从这 4 KB 的数据里截取出来，通过数据总线传输回内存通道。这时，每个矩阵负责 8 位。这个过程由 CAS 命令发起。可以看出，一次 Cache Line 的传输，虽然只需要读取 64 字节数据，但实际上激活了 4 KB 数据，这种情况就叫 “overfetch”，如果下一次 Cache Line 请求已经在行缓冲区中，那么就不用重新读取矩阵了，行缓冲充当了 DRAM 里的 Cache！</p><p>当数据读取完成后，需要 8 次 64 位的传输，每次 64 位传输里，8 个芯片里的每个负责其中不同的 8 位。因为我们使用的是 DDR，所以 8 次传输只需要 4 个内存时钟周期。</p><p>需要注意的是，一个 Bank 里同时只有一行能够被激活，在行里的数据可以读取之前，需要给 DRAM 单元充电，这个过程大概需要 13 ns，一旦充电完成，之前存放在行缓冲中的数据就丢失了。</p><p>因此，实际上内存请求还可以进一步细化为三类。第一类就是命中行缓冲的，这时候访问延迟最低，只需要 13 ns，也就是将缓冲中的数据传输到 DRAM 芯片的输出引脚的延迟；第二类是空的行访问，也就是行缓冲中没有数据，并且位线已经预充电好了，这时候需要 13 ns 来将数据传输到行缓冲，再需要 13 ns 传输到 DRAM 芯片的输出端口；第三类情况是，行缓冲中已经有数据了，而且需要访问不同的行，这时候需要 13 ns 给位线充电，13 ns 将数据传输到行缓冲，再需要 13 ns 传输到输出端口。内存控制器需要负责在合适的时机发起预充电命令，以增加第一、二类请求的可能性。</p><p>当 LLC 未命中的时候，内存访问延迟可能超过 100 ns，比如说有 60 ns 的时间，请求在内存控制器中排队，39 ns 的时间花在第三类请求的处理上，然后有 4 ns 的时间用于数据总线的传输。</p><p>看到这里，相信你也有点晕了，我也有点晕了，作者也觉得这确实太绕了。总的来讲，内存系统也是有自己的层次化结构的，从大到小分别是 DIMM、Rank、Bank、子 Bank、子 Array、比特矩阵。对于装机人员，只关心 DIMM；对于设计内存系统的工程师，则主要研究 Rank 和 Bank 带来的并行度；而微架构工程师，也就是负责内存的电路设计的人员，就主要关心子 Bank、子 Array、比特矩阵的具体物理组织方式。</p><p><a href="/programming/the-memory-system/16518414341.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16518414341.png"></a></p><p>CPU 发起的内存请求会先放到请求队列中，然后内存控制器翻译 CPU 请求，发送到 DRAM 队列，每个 DRAM 队列对应的是单独的内存通道。每个队列对应的是单独的内存控制器。为了增加带宽聚合在一起的内存通道通常共享同一个控制器和队列。至于物理上的 DRAM 队列是怎么实现的，通常会按 Rank 划分队列和按 Bank 划分队列。按 Bank 划分有两种方式，一种是 Bank i 的请求全部发往同一个队列，不管 Rank 是多少。另一种是每个 Bank 都有自己的队列。按 Bank 划分可能是现代计算机中限制每个内存通道的 Rank 数量的瓶颈。DRAM 队列里存放的命令包含通道号、Rank 号、行号和列号。</p><p>CPU 的命令和 DRAM 的命令都可能被乱序调度执行（而不是 FIFO）。调度器应该尽可能地保持一行是打开状态的，这样可以处理行缓冲命中的情况，这称为 Open-page 策略，对于局部性好的程序很友好。但是这个策略会导致在发生行缓冲冲突的时候有很大的延迟，所以如果程序的局部性不好，那么采取 Close-page 策略会更好，也就是在一行读完之后，就马上给位线充电，清空行缓冲。现在的内存控制器使用的是介于这两者之间的调度算法。</p><p>此外，虽然数据总线是双向的，但是每次切换方向的时候也会有一定的延迟，称为总线转向时间，大约需要 7.5 ns。所以，读和写都是分别分批处理的。读比较重要，总是第一时间处理，写的话有 Write Buffer，等 Buffer 快满了之后才切换总线方向，然后一大批地写。</p><p>为了提高行缓冲的命中率，调度器需要在每一个时钟周期都检查队列中的请求，使用 FR-FCFS 算法，尽可能选择已经充电好的请求处理，但这会导致一些线程优先级过高，需要另外的算法保证公平性。</p><p>虽然读内存的时候，CPU 是以 64 字节为单位取到缓存里，但是内存在读取数据返回给总线的时候，是需要多个时钟周期的，这些时钟周期也叫节拍。如果数据总线是 64 位（8 字节）的，那么就需要 8 拍才能把数据传输完。</p><p>DRAM 返回数据的时候，并不是直接发送到 CPU，而是先到内存控制器的缓冲区，然后再传送到 CPU。这样做的好处就是可以允许两个带宽不一致，CPU 的带宽和 DRAM 的带宽可以不同。在一些乱序的总线上，可以通过事务 ID 来区分不同的请求。</p><p><a href="/programming/the-memory-system/16519966411.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/16519966411.png"></a></p><p>为了减轻内存控制器以及总线的电流负载，一些 DIMM 上可能会带有额外的缓冲区芯片，RDIMM 只在地址&#x2F;命令线添加缓冲区，缓冲区将地址&#x2F;命令数据广播到 DRAM 芯片上，DRAM 芯片的数据引脚仍然直接连接到数据总线。而 LRDIMM 则是将数据引脚也接入到缓冲区，DDR 3 是接入到一个大的缓冲区，DDR 4 则是每个DRAM 芯片有自己的缓冲区。</p><p><a href="/programming/the-memory-system/Pasted-image-20220508161909.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/Pasted-image-20220508161909.png"></a></p><p><a href="/programming/the-memory-system/Pasted-image-20220506214357.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/Pasted-image-20220506214357.png"></a></p><p>实际的带宽最大值往往只有理论最大值的 65%~75%，随着内存带宽使用率上升，内存延迟也会上升。带宽利用率不同阶段的延迟上升程度不一样。</p><p>另外，由于内存控制器队列、硬件 Prefetcher、总线利用率、DIMM Rank 和 Bank 都可能影响内存访问的延迟，在使用理论模型分析内存访问延迟的时候，不能简单地认为内存访问延迟是一个常数，而是需要更加复杂的访问模型来计算，例如下面的伪代码就考虑了总线带宽，硬件 Prefetch 的影响。对于复杂的系统，必须使用更复杂的模型，否则结果将出现相当大的偏差，造成“Garbage in, garbage out”的结果。</p><p><a href="/programming/the-memory-system/Pasted-image-20220506220001.png" data-fancybox="gallery" data-caption=""><img src="/programming/the-memory-system/Pasted-image-20220506220001.png"></a></p><p>目前 DDR DRAM 的内存发展遇到了一定的瓶颈，HBM 等新技术也开始得到广泛应用。另外，为了解决 CPU 和内存之间的传输瓶颈，近数据处理也成为了热门话题，例如 In-Memory Computing。有的是利用了内存的物理电气规律来进行简单的逻辑运算，有的则是在内存芯片上安装额外的简单的处理器。目前这个领域也是方兴未艾，相关的编程语言等支持也还在研究之中。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>协程：Rust 与 C++ 20</title>
      <link>https://blog.howardlau.me/programming/coroutines-rust-cpp20.html</link>
      <description>
        <![CDATA[<p>这篇文章主要讲了当前流行的异步编程以及协程语法在 Rust 和 C++]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 11 May 2022 05:58:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>这篇文章主要讲了当前流行的异步编程以及协程语法在 Rust 和 C++ 的基本应用，怎么封装自己的协程，以及有哪些实现细节需要注意。</p><p>协程出现的时间其实很早，最早的操作系统运行程序就是使用协程的方式，这个时候操作系统和应用程序互相协作，处于平等的地位，如果应用程序不主动返回到操作系统，那么操作系统将永远没有机会执行其他操作。</p><p>协程其实是一个比较宽泛的概念，粗略地讲，普通函数调用后，所有的局部变量都会销毁，也无法保存状态，而协程在返回之后，会保持自己的状态，在下次调用的时候，可以根据这个状态从函数的中间继续运行。例如，编译器中的词法分析器（Lexer），在每次调用之后都会根据当前的分析状态，解析出下一个词法单元给语法分析器，在返回之后，会保存当前的位置信息提供给下一次调用。这个简单的例子也可以认为是一个协程：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">counter</span><span class="params">()</span> &#123;</span><br><span class="line">  <span class="type">static</span> <span class="type">int</span> count = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">return</span> count++;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过将 <code>count</code> 变量声明为 <code>static</code> 类型，在函数返回之后，<code>count</code> 的值不会被重置，我们可以多次调用 <code>counter</code> 函数，得到一个递增的整数序列。复杂一点来说，假如我们在编写 HTTP 服务器，那么我们可以写出一个这样的状态机函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">serve_http</span><span class="params">()</span> &#123;</span><br><span class="line">  <span class="type">static</span> <span class="type">int</span> step = <span class="number">0</span>;</span><br><span class="line">  <span class="type">static</span> <span class="type">int</span> fd = ...;</span><br><span class="line">  <span class="type">static</span> <span class="type">char</span> buffer[BUFFER];</span><br><span class="line">  <span class="type">int</span> n = <span class="number">0</span>;</span><br><span class="line">  <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (step) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">0</span>: <span class="comment">// Parse header</span></span><br><span class="line">      n = read(fd);</span><br><span class="line">      <span class="keyword">if</span> (n == EWOULDBLOCK) <span class="keyword">return</span>;</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">      step = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>: <span class="comment">// Parse body</span></span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> <span class="number">2</span>: <span class="comment">// Send response</span></span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，每次调用这个函数的时候，都会从 <code>static</code> 变量中读取之前保存的状态，看看目前应该是解析 Header 还是 Body，而如果发现还没有数据可以读的时候，就返回，让这个线程的别的函数有机会继续执行。</p><p>当然，如果使用 <code>static</code> 变量，那么同时只能处理一个请求，换个方法，我们可以先分配好保存状态所需的内存，然后在每次调用这个函数的时候，都将状态作为参数传入，这样我们就可以使用一个函数处理多个请求了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">client_state</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> step, fd;</span><br><span class="line">    <span class="type">char</span> buffer[BUFFER];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">serve_http</span><span class="params">(<span class="keyword">struct</span> client_state *state)</span> &#123;</span><br><span class="line">  <span class="comment">// Use state...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而主函数其实也非常简单，通过 <code>select</code>、<code>epoll</code> 等系统调用，在新的 I&#x2F;O 事件发生的时候获取对应的状态指针，然后调用协程函数就可以了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">    epoll_wait(...);</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">struct</span> epoll_event* p = head; p != <span class="literal">NULL</span>; p = p-&gt;next) &#123;</span><br><span class="line">      <span class="class"><span class="keyword">struct</span> <span class="title">state</span>* <span class="title">state</span> =</span> (<span class="keyword">struct</span> state *) p-&gt;data;</span><br><span class="line">      serve_http(state);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以其实协程并不是什么很复杂深奥和神秘的东西，它就是方便我们异步编程的一种编程方式而已。</p><p>上面这种只保存需要的状态的协程，使用状态机的方式实现具体逻辑的，叫做无栈协程。无栈协程优势在于性能好、占用空间少，只需要保存需要的状态，但是可以看出其编程十分复杂，和平时我们直接调用函数相比有很大差别。另外，如果在协程中想要继续调用协程，则需要状态的嵌套。例如，假如发送回复的时候，我们想调用一个单独的 <code>write_response</code> 函数，里面又分为写 Header 和写 Body 两个步骤，那么我们可以写：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">resp_state</span> &#123;</span> <span class="type">int</span> step, fd; <span class="type">int</span> finished; <span class="comment">/* ... */</span> &#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">write_response</span><span class="params">(<span class="keyword">struct</span> resp_state *state)</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (state-&gt;step) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么我们要怎么在 <code>serve_http</code> 里面调用这个函数呢？一种办法是，我们在 <code>client_state</code> 里面保存一个 <code>resp_state</code> 指针，然后，在状态处于发送回复的时候，我们就在状态机中，调用 <code>write_response</code> 函数，然后看看 <code>resp_state</code> 里面的 <code>finished</code> 是否为 <code>1</code>，是就说明发送回复的状态机也执行完了，可以进行下一步操作了，否则就直接返回，等待下一次 I&#x2F;O 事件。这种方法就是<strong>自顶向下</strong>的方法，每次都要从 <code>serve_http</code> 这个最上层的协程开始执行，并通过它来驱动子协程。</p><p>另一种办法，则需要我们对代码的整体结构做一些修改，在调用 <code>write_response</code> 之后，其实我们没必要每次都从 <code>serve_http</code> 开始往下执行，我们可以告诉主循环，让它下次收到 I&#x2F;O 事件的时候，直接调用 <code>write_response</code>。但这时候，<code>resp_state</code> 就需要保存指向 <code>client_state</code> 的指针，在自己执行完成之后，调用 <code>write_response</code>，并且重新设置相应的数据结构，在下次 I&#x2F;O 事件的时候，调用 <code>write_response</code>。</p><p>例如：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">connection</span> &#123;</span></span><br><span class="line">  (<span class="type">void</span> *f)(<span class="type">void</span> *state);</span><br><span class="line">  <span class="type">void</span> *state;  </span><br><span class="line">  <span class="type">int</span> fd;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">resp_state</span> &#123;</span> <span class="comment">/* ... */</span> (<span class="type">void</span> *parent)(<span class="type">void</span> *state); <span class="type">void</span> *parent_state; &#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">write_response</span><span class="params">(<span class="keyword">struct</span> resp_state *state)</span> &#123;</span><br><span class="line">  <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (state-&gt;step) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">0</span>:</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">      <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  state-&gt;parent(state-&gt;parent_state);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="comment">/* each event p*/</span>) &#123;</span><br><span class="line">      <span class="class"><span class="keyword">struct</span> <span class="title">connection</span> *<span class="title">c</span> =</span> (<span class="keyword">struct</span> connection *) p-&gt;data;</span><br><span class="line">      c-&gt;f(c-&gt;state);    </span><br><span class="line">    &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种方法就是<strong>自底向上</strong>的方法，每次继续执行的时候，都从最后一个被调用的协程开始执行，并且子协程会保存返回点，在执行完成后返回到父协程。这样，就节省了从顶级协程一路调用下来的开销。</p><p>当然，既然有无栈协程，那么就有有栈协程。有栈协程相比无栈协程，就简单得多了。它就是在协程需要主动返回的时候，将当前的调用栈保存到堆内存中，这样就实现了状态的保存。具体来说，其实就是将系统调度线程的逻辑实现到了用户态中。而执行函数的切换，也和操作系统中切换线程进程的逻辑一样，设置 CPU 相关的寄存器，尤其是栈指针，然后跳转到指定的函数地址就完成了切换。那么问题就在于应该跳转到哪？如果像操作系统一样，那么所有协程不管如何调用，只要遇到需要挂起的情况，就将控制权转移给一个中心的调度器，调度器再选择下一个需要执行的协程继续执行，这就是对称协程。而像上面那种，在需要挂起的时候，一路返回到一开始调用的函数的，就是非对称协程了。</p><p>上面其实就是协程的一些基本概念了，但是比实际场景的协程还缺少了一些东西。例如，协程要怎么返回值呢？这里同样有多种解决办法，在自底向上的模型中，可以使用一个共享的变量来交换信息，例如在 <code>client_state</code> 里面添加一个 <code>resp_ret</code> 变量，<code>write_response</code> 函数可以设置这个变量，然后在继续执行 <code>serve_http</code> 的时候，就可以通过读取这个变量获得返回值。而在自顶向下的模型中，也可以直接使用函数的返回值。</p><p>对于无栈协程，如果全部都手写状态机的话，其实都是重复的工作，而且容易出错。所以，现代语言通常都会加入对于协程的原生支持，由编译器来生成这些状态机代码。对于有栈协程，则一般是通过库函数的方式来实现的。</p><p>如果是无栈协程，那一般在代码里会有 <code>async</code> 或者 <code>await</code> 这样的关键词，<code>await</code> 就是用来告诉编译器划分状态机的位置。而有栈协程，在编写程序的时候感知不到，只是需要调用协程库提供的协程创建函数，在可能阻塞的地方，协程库都会拦截系统调用，转移控制权。</p><p>例如，像 JavaScript、Python、C++、Rust 这些使用了 <code>await</code> 关键词的，就是使用的无栈协程，而像 Go 语言，用户像普通函数一样调用例如 <code>read</code> 这样的函数，只是需要使用 <code>go</code> 关键词来创建协程，就是有栈协程了。</p><p>通常来讲，无栈协程比有栈协程开销更小，性能更高，而非对称协程和对称协程在功能上没有区别。</p><p>无栈协程在有 GC 的语言里实现比较简单，而在没有 GC 的语言，例如 C++ 和 Rust 中，就需要注意非常多的细节。</p><h2 id="C"><a href="#C" class="headerlink" title="C++"></a>C++</h2><p>C++ 中需要实现的东西：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Task</span> &#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">promise_type</span> &#123;</span><br><span class="line">    <span class="function">ReturnObject <span class="title">get_return_object</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> <span class="built_in">ReturnObject</span>(coroutine_handle&lt;promise_type&gt;::<span class="built_in">from_promise</span>(*<span class="keyword">this</span>)); &#125;</span><br><span class="line">    <span class="function">std::suspend_never <span class="title">initial_suspend</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> &#123;&#125;; &#125;</span><br><span class="line">    <span class="function">std::suspend_never <span class="title">final_suspend</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> &#123;&#125;; &#125;</span><br><span class="line">    <span class="function">std::suspend_always <span class="title">yield_value</span><span class="params">(<span class="type">unsigned</span> value)</span> </span>&#123;</span><br><span class="line">      value_ = value;</span><br><span class="line">      <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">unhandled_exception</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">return_value</span><span class="params">(T&amp;&amp; value)</span> </span>&#123;</span><br><span class="line">      value_ = value;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// void return_void() &#123;&#125;</span></span><br><span class="line">  &#125;;</span><br><span class="line">  std::coroutine_handle&lt;promise_type&gt; h_;</span><br><span class="line">  T value_;</span><br><span class="line">  <span class="built_in">ReturnObject</span>(std::coroutine_handle&lt;&gt; h) : <span class="built_in">h_</span>(h) &#123;&#125;;</span><br><span class="line">  <span class="keyword">operator</span> std::<span class="built_in">coroutine_handle</span>&lt;promise_type&gt;() <span class="type">const</span> &#123; <span class="keyword">return</span> h_; &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Awaiter</span> &#123;</span><br><span class="line">  std::coroutine_handle&lt;&gt; *hp_;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// optional</span></span><br><span class="line">  <span class="function">Awaiter <span class="keyword">operator</span> <span class="title">co_await</span><span class="params">()</span></span>;</span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line">  <span class="function"><span class="type">bool</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; h)</span> </span>&#123; *hp_ = h; <span class="keyword">return</span> <span class="literal">false</span>; &#125;</span><br><span class="line">  <span class="function">T <span class="title">await_resume</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123;&#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>一共九个方法需要实现，看的头都大了。先来看一个简单的例子感受一下上面的代码分别有什么用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function">Task <span class="title">counter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">unsigned</span> i = <span class="number">0</span>; i &lt; <span class="number">3</span>; ++i)</span><br><span class="line">    <span class="keyword">co_yield</span> i;</span><br><span class="line">  <span class="keyword">co_return</span> <span class="number">42</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> h = <span class="built_in">counter</span>().h_;</span><br><span class="line">  <span class="keyword">auto</span> &amp;promise = h.<span class="built_in">promise</span>();</span><br><span class="line">  <span class="keyword">while</span> (!h.<span class="built_in">done</span>()) &#123; <span class="comment">// Do NOT use while(h) (which checks h non-NULL)</span></span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;counter: &quot;</span> &lt;&lt; promise.value_ &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">h</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  h.<span class="built_in">destroy</span>();</span><br><span class="line">  <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个简单的计数器协程，每次运行都会递增值。前面的 <code>Task</code> 就代表一个协程，里面可以保存返回值，而 <code>promise_type</code> 就是协程对应的 Promise。Promise 和 Future 一般是成对出现的，Promise 用来<strong>写入</strong>一个未来的值，而 Future 则用来<strong>读取</strong>一个未来的值。所以，在上面的 <code>promise_type</code> 中，<code>yield_value</code> 和 <code>return_value</code> 就是用来写入值的方法，而我们可以使用 <code>get</code> 方法得到返回的值。一般来说，Future 的值可以有阻塞的和非阻塞的读取的方法，非阻塞就是无论 Promise 有没有已经写入值，都立刻返回，而阻塞的则是等到 Promise 写入值之后再返回。计数器在 Promise 中设置返回值，然后我们就可以在 Task 中读取这个值。C++ 标准说协程的代码相当于：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    promise-type promise promise-constructor-arguments ;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">co_await</span> promise.<span class="built_in">initial_suspend</span>() ;</span><br><span class="line">        function-body</span><br><span class="line">    &#125; <span class="built_in">catch</span> ( ... ) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!initial-await-resume-called)</span><br><span class="line">            <span class="keyword">throw</span> ;</span><br><span class="line">        promise.<span class="built_in">unhandled_exception</span>() ;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="keyword">final</span>-suspend :</span><br><span class="line">    <span class="keyword">co_await</span> promise.<span class="built_in">final_suspend</span>() ;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么 <code>co_await</code> 又是如何驱动协程的运行呢？再来看一个复杂一点的例子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;coroutine&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdexcept&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;thread&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">switch_to_new_thread</span><span class="params">(std::jthread&amp; out)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">awaitable</span> &#123;</span><br><span class="line">    std::jthread* p_out;</span><br><span class="line">    <span class="built_in">awaitable</span>(std::jthread* p_out) : <span class="built_in">p_out</span>(p_out) &#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">    &#125;</span><br><span class="line">    ~<span class="built_in">awaitable</span>() &#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">await_ready</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; h)</span> </span>&#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">      std::jthread&amp; out = *p_out;</span><br><span class="line">      <span class="keyword">if</span> (out.<span class="built_in">joinable</span>())</span><br><span class="line">        <span class="keyword">throw</span> std::<span class="built_in">runtime_error</span>(<span class="string">&quot;Output jthread parameter not empty&quot;</span>);</span><br><span class="line">      out = std::<span class="built_in">jthread</span>([h] &#123; h.<span class="built_in">resume</span>(); &#125;);</span><br><span class="line">      <span class="comment">// Potential undefined behavior: accessing potentially destroyed *this</span></span><br><span class="line">      <span class="comment">// std::cout &lt;&lt; &quot;New thread ID: &quot; &lt;&lt; p_out-&gt;get_id() &lt;&lt; &#x27;\n&#x27;;</span></span><br><span class="line">      std::cout &lt;&lt; <span class="string">&quot;New thread ID: &quot;</span> &lt;&lt; out.<span class="built_in">get_id</span>() &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;  <span class="comment">// this is OK</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">await_resume</span><span class="params">()</span> </span>&#123; std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl; &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="keyword">return</span> awaitable&#123;&amp;out&#125;;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">task</span> &#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">promise_type</span> &#123;</span><br><span class="line">    <span class="function">task <span class="title">get_return_object</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">      <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">std::suspend_never <span class="title">initial_suspend</span><span class="params">()</span> </span>&#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">      <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">std::suspend_never <span class="title">final_suspend</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">      std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">      <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">return_void</span><span class="params">()</span> </span>&#123; std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl; &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">unhandled_exception</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="built_in">task</span>() &#123;</span><br><span class="line">    std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">  &#125;</span><br><span class="line">  ~<span class="built_in">task</span>() &#123;</span><br><span class="line">    std::cout &lt;&lt; __FUNCTION__ &lt;&lt; std::endl;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function">task <span class="title">resuming_on_new_thread</span><span class="params">(std::jthread&amp; out)</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;Coroutine started on thread: &quot;</span> &lt;&lt; std::this_thread::<span class="built_in">get_id</span>()</span><br><span class="line">            &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">  <span class="function"><span class="keyword">co_await</span> <span class="title">switch_to_new_thread</span><span class="params">(out)</span></span>;</span><br><span class="line">  <span class="comment">// awaiter destroyed here</span></span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;Coroutine resumed on thread: &quot;</span> &lt;&lt; std::this_thread::<span class="built_in">get_id</span>()</span><br><span class="line">            &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  std::jthread out;</span><br><span class="line">  <span class="keyword">auto</span> t = <span class="built_in">resuming_on_new_thread</span>(out);</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;Main finished&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用支持 C++ 20 标准的编译器编译运行之后，可以得到以下的输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">task::promise_type::get_return_object</span><br><span class="line">task::task</span><br><span class="line">task::promise_type::initial_suspend</span><br><span class="line">Coroutine started on thread: 3804</span><br><span class="line">switch_to_new_thread::awaitable::awaitable</span><br><span class="line">switch_to_new_thread::awaitable::await_ready</span><br><span class="line">switch_to_new_thread::awaitable::await_suspend</span><br><span class="line">New thread ID: 2556</span><br><span class="line">Main finished</span><br><span class="line">task::~task</span><br><span class="line">switch_to_new_thread::awaitable::await_resume</span><br><span class="line">switch_to_new_thread::awaitable::~awaitable</span><br><span class="line">Coroutine resumed on thread: 2556</span><br><span class="line">task::promise_type::return_void</span><br><span class="line">task::promise_type::final_suspend</span><br></pre></td></tr></table></figure><p>这个例子里，<code>resuming_on_new_thread</code> 是一个协程，而 <code>switch_to_new_thread</code> 则是协程里 <code>co_await</code> 的一个函数，在 <code>co_await</code> 这个函数之后，协程剩余的部分就会在另一个线程中执行。</p><p><code>co_await</code> 这个是一个操作符，在调用之后，首先会调用函数，得到一个 <code>Awaitable</code>，然后，调用这个 <code>Awaitable</code> 的 <code>co_await</code> 操作符，获取一个 <code>awaiter</code>，然后执行 <code>awaiter</code> 的 <code>await_ready</code> ，判断是否需要让出控制权，如果需要的话，就调用 <code>await_suspend</code>，暂停执行，然后返回。在可以继续执行之后，会调用 <code>await_resume</code>，获取返回值。用伪代码来描述就是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> awaiter = awaitable.<span class="keyword">operator</span> <span class="built_in">co_await</span>();  </span><br><span class="line"><span class="keyword">if</span> (!awaiter.<span class="built_in">await_ready</span>()) &#123;  </span><br><span class="line">   awaiter.<span class="built_in">await_suspend</span>(current_coroutine_handle);  </span><br><span class="line">   <span class="keyword">continue</span> <span class="keyword">return</span>;  </span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">auto</span> value = awaiter.<span class="built_in">await_resume</span>();</span><br></pre></td></tr></table></figure><p>可以看到 <code>await_suspend</code> 这个方法，接受了一个 <code>coroutine_handle</code>，并且在新开的线程中执行了。这个 <code>coroutine_handle</code>，就是编译器打包好的，协程的状态以及还没有执行完成的部分，只要调用这个对象，就可以继续执行协程了。</p><p>协程能够运行起来，关键是 <code>await_suspend</code> 这个方法，这个方法需要负责设置回调之类的操作，并且在回调中，调用传进来的 Callable，这样协程才能继续运行，否则就卡住了。例如，在服务器程序中，调用 <code>await_suspend</code> 的时候，就将协程注册到 epoll 的 fd 列表里，然后设置在下一次 I&#x2F;O 发生的时候调用传入的 <code>coroutine_handle</code> 以继续执行暂停的协程。</p><p>非常奇怪的是，<code>await_suspend</code> 的返回值类型会影响这个函数实际的行为。如果这个函数返回的是 <code>void</code>，那么当前的协程（调用 <code>co_await</code> 的协程）会立即返回；如果这个协程返回的是 <code>bool</code>，那么在返回 <code>true</code> 的时候，当前协程会立即返回，而如果返回 <code>false</code> 表明当前协程可以继续执行，不会返回；而如果这个协程返回的同样是一个 <code>coroutine_handle</code>，那么就执行这个对象。</p><p>C++ 中如果想要嵌套调用协程，是需要自己在 <code>await_suspend</code> 里保存并调用 Callable 的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">task</span>&lt;T&gt;::promise_type &#123; </span><br><span class="line">  std::coroutine_handle&lt;&gt; continuation; </span><br><span class="line">  <span class="function"><span class="keyword">auto</span> <span class="title">final_suspend</span><span class="params">()</span> <span class="keyword">noexcept</span> </span>&#123; </span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">awaiter</span> &#123; </span><br><span class="line">      <span class="function"><span class="keyword">auto</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle suspended)</span> </span>&#123; </span><br><span class="line">        <span class="keyword">if</span> (suspended.<span class="built_in">promise</span>().continuation)  </span><br><span class="line">          <span class="keyword">return</span> suspended.<span class="built_in">promise</span>().continuation.<span class="built_in">resume</span>();</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">          <span class="keyword">return</span> std::noop_coroutine&#123;&#125;;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;; </span><br><span class="line">    <span class="keyword">return</span> awaiter&#123;&#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>final_suspend</code> 是看看当前协程在运行结束返回时，是否有需要返回的协程，有的话就返回到上一个协程，否则就啥都不做，结束执行。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> task&lt;T&gt;::<span class="function"><span class="keyword">operator</span> <span class="title">co_await</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">awaiter</span> &#123;</span><br><span class="line">    std::coroutine_handle&lt;promise_type&gt; handle;</span><br><span class="line">    <span class="function"><span class="keyword">auto</span> <span class="title">await_suspend</span><span class="params">(std::coroutine_handle&lt;&gt; suspended)</span> </span>&#123;</span><br><span class="line">      handle.<span class="built_in">promise</span>().continuation = suspended;</span><br><span class="line">      <span class="keyword">return</span> handle;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">  <span class="keyword">return</span> awaiter&#123;*_coro&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里的 <code>operator co_await</code> 函数则是为了能在一个 <code>task</code> 里面 <code>co_await foo()</code>，其中 <code>foo()</code> 返回的是另一个 <code>task</code>，可以看到这个函数做的东西很简单，就是设置了一下被调用的协程的返回协程为当前协程。</p><p>有了这些基础之后，只需要对系统的异步 API 进行简单的封装就可以使用协程编程了。例如 <code>co_await read()</code> 可以是在 <code>read</code> 的 <code>await_suspend</code> 中将连接的 fd 注册到 epoll fd 里，然后把协程的调用函数作为回调函数保存起来。</p><h2 id="Rust"><a href="#Rust" class="headerlink" title="Rust"></a>Rust</h2><p>Rust 则需要实现 <code>Future</code> 这个 trait：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Poll</span> &#123; <span class="title function_ invoke__">Ready</span>(T), Pending &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">trait</span> <span class="title class_">Future</span> &#123;</span><br><span class="line">    <span class="keyword">type</span> <span class="title class_">Output</span>;</span><br><span class="line">    <span class="keyword">fn</span> <span class="title function_">poll</span>(</span><br><span class="line">        <span class="comment">// Note the change from `&amp;mut self` to `Pin&lt;&amp;mut Self&gt;`:</span></span><br><span class="line">        <span class="keyword">self</span>: Pin&lt;&amp;<span class="keyword">mut</span> <span class="keyword">Self</span>&gt;,</span><br><span class="line">        <span class="comment">// and the change from `wake: fn()` to `cx: &amp;mut Context&lt;&#x27;_&gt;`:</span></span><br><span class="line">        cx: &amp;<span class="keyword">mut</span> Context&lt;<span class="symbol">&#x27;_</span>&gt;,</span><br><span class="line">    ) <span class="punctuation">-&gt;</span> Poll&lt;<span class="keyword">Self</span>::Output&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>只有一个方法：<code>poll()</code>，返回 <code>Poll::Pending</code> 表示协程还没有结束，这时候需要负责设置回调，获取 <code>Context</code> 里的 <code>waker</code>，保存起来，然后在回调处理函数中调用 <code>waker.wake()</code>，将协程重新加入调度队列中，而返回 <code>Poll::Ready(())</code> 表示协程完成了。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Context</span> &#123; </span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Context</span> &#123;</span><br><span class="line">  <span class="keyword">fn</span> <span class="title function_">waker</span>(&amp;<span class="keyword">self</span>) <span class="punctuation">-&gt;</span> &amp;Waker;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Waker</span> &#123; </span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Waker</span> &#123;</span><br><span class="line">  <span class="keyword">fn</span> <span class="title function_">wake</span>(<span class="keyword">self</span>);</span><br><span class="line">  <span class="keyword">fn</span> <span class="title function_">wake_by_ref</span>(&amp;<span class="keyword">self</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当前，<code>Context</code> 的唯一作用就是保存 <code>Waker</code> 对象，而 <code>Waker</code> 唯一作用就是用来 <code>wake</code>，至于协程所需要的其他信息，可以保存在 <code>Future</code> 对象中。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">pub</span> <span class="keyword">fn</span> <span class="title function_">waker_fn</span>&lt;F: <span class="title function_ invoke__">Fn</span>() + <span class="built_in">Send</span> + <span class="built_in">Sync</span> + <span class="symbol">&#x27;static</span>&gt;(f: F) <span class="punctuation">-&gt;</span> Waker &#123;</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">raw</span> = Arc::<span class="title function_ invoke__">into_raw</span>(Arc::<span class="title function_ invoke__">new</span>(f)) <span class="keyword">as</span> *<span class="title function_ invoke__">const</span> ();</span><br><span class="line">  <span class="keyword">let</span> <span class="variable">vtable</span> = &amp;Helper::&lt;F&gt;::VTABLE;</span><br><span class="line">  <span class="keyword">unsafe</span> &#123; Waker::<span class="title function_ invoke__">from_raw</span>(RawWaker::<span class="title function_ invoke__">new</span>(raw, vtable)) &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Helper</span>&lt;F&gt;(F);</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span>&lt;F: <span class="title function_ invoke__">Fn</span>() + <span class="built_in">Send</span> + <span class="built_in">Sync</span> + <span class="symbol">&#x27;static</span>&gt; Helper&lt;F&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> VTABLE: RawWakerVTable = RawWakerVTable::<span class="title function_ invoke__">new</span>(</span><br><span class="line">    <span class="keyword">Self</span>::clone_waker,</span><br><span class="line">    <span class="keyword">Self</span>::wake,</span><br><span class="line">    <span class="keyword">Self</span>::wake_by_ref,</span><br><span class="line">    <span class="keyword">Self</span>::drop_waker,</span><br><span class="line">  );</span><br><span class="line"></span><br><span class="line">  <span class="keyword">unsafe</span> <span class="keyword">fn</span> <span class="title function_">clone_waker</span>(ptr: *<span class="title function_ invoke__">const</span> ()) <span class="punctuation">-&gt;</span> RawWaker &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">arc</span> = ManuallyDrop::<span class="title function_ invoke__">new</span>(Arc::<span class="title function_ invoke__">from_raw</span>(ptr <span class="keyword">as</span> *<span class="keyword">const</span> F));</span><br><span class="line">    mem::forget(arc.<span class="title function_ invoke__">clone</span>());</span><br><span class="line">    RawWaker::<span class="title function_ invoke__">new</span>(ptr, &amp;<span class="keyword">Self</span>::VTABLE)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">unsafe</span> <span class="keyword">fn</span> <span class="title function_">wake</span>(ptr: *<span class="title function_ invoke__">const</span> ()) &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">arc</span> = Arc::<span class="title function_ invoke__">from_raw</span>(ptr <span class="keyword">as</span> *<span class="keyword">const</span> F);</span><br><span class="line">    (arc)();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">unsafe</span> <span class="keyword">fn</span> <span class="title function_">wake_by_ref</span>(ptr: *<span class="title function_ invoke__">const</span> ()) &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">arc</span> = ManuallyDrop::<span class="title function_ invoke__">new</span>(Arc::<span class="title function_ invoke__">from_raw</span>(ptr <span class="keyword">as</span> *<span class="keyword">const</span> F));</span><br><span class="line">    (arc)();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">unsafe</span> <span class="keyword">fn</span> <span class="title function_">drop_waker</span>(ptr: *<span class="title function_ invoke__">const</span> ()) &#123;</span><br><span class="line">      <span class="title function_ invoke__">drop</span>(Arc::<span class="title function_ invoke__">from_raw</span>(ptr <span class="keyword">as</span> *<span class="keyword">const</span> F));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>值得一提的是，<code>Waker</code> 中保存的是一个 <code>RawWaker</code> 对象，这个对象需要实现 <code>clone_waker</code>、<code>wake</code>、<code>wake_by_ref</code>、<code>drop_waker</code> 这四个方法。为了能够使 <code>Waker</code> 可以静态分配，这四个方法并不是通过实现 <code>trait</code> 的方式来实现的，而是通过手动构造虚函数表来实现的。上面就是一个简单的调用一个函数的 <code>RawWaker</code> 实现。</p><p>和 C++ 不同，Rust 中的协程，需要在函数声明前添加 <code>async</code> 关键词，而函数的返回类型并不是 Future，而是实际返回值的类型。</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">fn</span> <span class="title function_">write_header</span>(conn: Connection) <span class="punctuation">-&gt;</span> <span class="type">Result</span>&lt;<span class="type">usize</span>&gt;;</span><br></pre></td></tr></table></figure><h2 id="对比"><a href="#对比" class="headerlink" title="对比"></a>对比</h2><p>Rust 中嵌套调用协程是编译器帮忙生成的，不需要自己做额外的操作。函数的签名需要加特殊的关键词，返回值的类型是实际返回值的类型。</p><p>C++ 的接口显然更复杂，但是它的模型是“自底向上”的，只有当协程能够继续运行的时候才会调用继续函数。函数不需要加特殊的关键词，而是返回一个特殊的包装了返回值类型的协程类型。C++ 也使得你对执行过程有更精细的控制权。</p><p>而 Rust 接口简洁不少，基于轮询，模型是“自顶向下”的，需要有一个执行器负责调用 <code>Future</code> 的 <code>poll</code> 接口，整个状态机才能推进，也就是说，Rust 的协程有时可以被不必要地轮询。</p><p>另外，Rust 中的 Future 状态类型，在编译的时候就已经确定了，可以当成普通对象一样放在栈上，但也造成有些时候一些变量保存过久，浪费空间；而 C++ 中的 <code>coroutine_handle</code> 则被类型擦除了，编译的时候无法知道具体的类型，也无法知道状态的具体大小，只有在优化结束之后才能确定，这样导致了协程的状态必须分配堆内存，好处是 C++ 的协程，仅会保存需要的状态，比如一个变量只在前面部分使用了，就没有必要保存到后面了。</p><h2 id="事件循环逻辑"><a href="#事件循环逻辑" class="headerlink" title="事件循环逻辑"></a>事件循环逻辑</h2><p>前面提到，协程在暂停之后，需要有其他代码主动调用协程函数才能让协程继续运行。而所有的协程，通常都是在程序的主函数中，通过一个事件循环来不停调用协程函数来驱动它们的。也就是上面的 HTTP 服务例子里的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="comment">/* each event p */</span>) &#123;</span><br><span class="line">  p-&gt;data-&gt;state-&gt;event = p-&gt;event;</span><br><span class="line">  p-&gt;data-&gt;<span class="built_in">callback</span>(p-&gt;data-&gt;state);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当然，要想正确的设置回调函数，需要追踪每一个 fd，这也是为什么在使用异步编程库的时候，需要使用库提供的 TCP 函数和连接类型等，因为这样它们最终负责调用系统函数的时候才能区分不同的连接上的不同事件。</p><p>对于事件循环，也有多种模式。一种是，类似于 Go 语言和 tokio 运行时，不同的协程可以在不同的操作系统线程上执行，这样的执行模型就像操作系统一样了，不同的进程可以在不同的 CPU 核心上执行，而且在一些核心空闲的时候，可以从其他核心那里“窃取”任务执行，提高 CPU 的利用率。</p><p>而另一种执行模型是 Loop Per Core，也就是每个核心单独执行一个单线程的事件循环，这样做的好处是程序的局部性大大提升，而且避免了协程在多核之间迁移执行时的同步开销（例如任务队列需要加锁），协程使用的数据结构也不必考虑多核同步的问题。相比于多核模型，执行效率会更高。但是如果出现了任务负载不均衡的现象，那么这种模型的 CPU 利用率就会下降，此时性能就没有多核模型好了。</p><p>所以，使用哪一种执行模型还是取决于应用程序本身的负载特性，并没有说哪一种模型有绝对的优势。</p><h2 id="生命周期管理"><a href="#生命周期管理" class="headerlink" title="生命周期管理"></a>生命周期管理</h2><p>和多线程编程一样，在使用协程经常犯的一个错误就是对象的生命周期问题。因为协程可以被暂停，然后再在之后继续执行，如果协程保存了对一个对象的引用或者指针，那么必须确保这个对象在协程执行完成之前都一直存活，否则将发生内存错误。在 GC 语言中，这个问题不存在，编程非常简单。而在 C++ 中，可以使用智能指针 <code>unique_ptr</code> 或者 <code>shared_ptr</code> 等来管理对象的生命周期，在 Rust 中，编译器的 Ownership 模型可以帮助程序员发现生命周期问题。</p><h2 id="取消"><a href="#取消" class="headerlink" title="取消"></a>取消</h2><p>另一个复杂的问题就是怎么样取消协程。例如，在一个耗时的操作中，可能用户已经等得不耐烦了，取消了请求，也有可能超时了，需要终止请求防止过载。</p><p>协程的取消难点在于需要追踪一切涉及到的资源，并在取消的时候正确地释放。例如，在 Go 语言中，所有需要取消的协程，参数一定会带一个 <code>ctx context.Context</code>，并且在所有的异步操作中，同时 <code>select</code> <code>ctx.Done()</code>，在取消的之后，<code>ctx</code> 则会一路传播信号，通知涉及到的协程清理资源并退出。</p><p>而在 Rust 和 C++ 中则没有约定俗成的取消方法，但是原理也是类似的。在每一个 <code>await</code> 的调用的时候，都可能是这个协程的一个取消点。如果想要处理取消，那么我们可以在设置回调的时候，同时设置一个监听取消的回调，这样在取消信号发生的时候，协程就可以被唤醒，处理异常情况了。在 Rust 的 <code>tokio</code> 中提供了一个 <code>select!</code> 宏，用法和 Go 语言中的类似，原理就是轮流或者随机查询 <code>select!</code> 的每一个 Future，一旦其中一个 <code>Ready</code>，就执行对应的代码。</p><h2 id="锁"><a href="#锁" class="headerlink" title="锁"></a>锁</h2><p>在实际应用场景中，一个协程在被暂停之后，可能会在另一个线程被调度执行，而普通的操作系统的锁，有可能需要解锁的线程和加锁的线程是同一个，那么如果在协程中一个锁加锁的时间可能横跨多个 <code>await</code> 点的时候，就不能直接使用普通的锁了，而是要使用支持追踪异步调用信息的异步锁，这样才能在正确的线程释放，避免错误。当然，异步锁的开销比普通的锁的开销要更大，如果加锁的时间没有横跨 <code>await</code>，那么就不需要使用异步锁。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p><a href="https://www.jonathanmueller.dev/talk/accu2022/">https://www.jonathanmueller.dev/talk/accu2022/</a></p><p><a href="https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html">https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html</a></p><p><a href="https://lewissbaker.github.io/2017/09/25/coroutine-theory">https://lewissbaker.github.io/2017/09/25/coroutine-theory</a></p><p><a href="https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html">https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html</a></p><p><a href="https://github.com/madsys-dev/async-ucx">https://github.com/madsys-dev/async-ucx</a></p><p><a href="https://github.com/sekirio-rs/Kuro">https://github.com/sekirio-rs/Kuro</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Paxos 后传——小岛历史的延续</title>
      <link>https://blog.howardlau.me/programming/paxos-optimizations.html</link>
      <description>
        <![CDATA[<p>Paxos 本身以晦涩难懂而闻名，更不要说对它的优化了。为了破除大家心中的阴影，UCB 的几个大佬在 Journal of Systems Research 期刊上发表了 <a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 03 May 2022 02:05:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Paxos 本身以晦涩难懂而闻名，更不要说对它的优化了。为了破除大家心中的阴影，UCB 的几个大佬在 Journal of Systems Research 期刊上发表了 <a href="https://escholarship.org/uc/item/9w79h2jg">SoK: A Generalized Multi-Leader State Machine Replication Tutorial</a> 的论文。这篇文章也是我对这篇论文的阅读笔记了。</p><p><a href="https://mwhittaker.github.io/frankenpaxos/">https://mwhittaker.github.io/frankenpaxos/</a> 有不同 Paxos 算法的动画演示。</p><h2 id="概览"><a href="#概览" class="headerlink" title="概览"></a>概览</h2><p>自从 Lamport 老爷子提出 Paxos 算法以来，许多工程师和学者都投入到对这个算法的实现和改进的工作中。首先最经典的就是将单个 Paxos 算法扩展到分布式状态机复制的 MultiPaxos 还有尝试让 MultiPaxos 变得更加易懂、易实现，将其中细节问题讲清楚的 Raft 算法。他们的特点都是将上层应用的状态视为状态机，共识算法则负责决定这个状态机执行的日志。将状态变更组织成日志的好处是实现简单，但是显而易见的会有性能问题。例如在 Raft 中，日志不允许存在空洞，日志后面的命令需要等到日志前面的日志提交后自己才能提交。例如，两个用户分别更新自己的用户名，两个操作执行的顺序其实并无要求，谁先修改成功都可以，只要所有副本都保证最后的结果一致即可。而日志则强行为两个并发的操作规定了一个全序，将操作串行化了。尽管可以通过 Sharding 的方式来一定程度上缓解串行化执行的问题，但是在一个 Shard 内的操作仍然是有全序的。</p><p>Lamport 自己也意识到了这个问题，提出了泛化的 Paxos 算法，也就是 GPaxos。它的思想也很简单，既然日志强行施加了一个全序，那么我们想办法以一种偏序的方法表达操作即可。也就是说，现在需要共识的不是一个线性的日志了，而是一个可以有依赖关系的有向图。在这个有向图中（为什么允许有环在后面讲），顶点就是需要执行的操作，而如果一个操作 b 依赖另一个操作 a，那么就有一条从 b 指向 a 的有向边。很显然，我们只需要按照这个图的逆拓扑序执行操作就可以了，两个没有依赖关系的顶点可以并发执行。</p><p>但是，上面提到的算法，都有一个瓶颈：它们实际上都有一个 Leader 节点。Leader 节点负责所有的请求，而且比其他的节点要收发多得多的信息。尽管我们也可以用 Sharding 的方式来缓解 Leader 的热点问题，但是同样的，对于一个 Shard 内部 Leader 还是有可能成为瓶颈。所以大家就开始在泛化的 Paxos 算法基础上去研究多 Leader 的算法。</p><p>多 Leader 算法中，不同的节点互相都是平等的，客户端的请求可以随便发往任意一个节点处理，而且那个节点可以自己处理请求，而不是将请求转发到别的节点去执行。</p><p>消除了 Leader 节点的瓶颈问题之后，还有一个问题需要解决，那就是请求的延迟问题。在普通的 Paxos 算法中，需要 7 次 RPC 消息延迟才能提交一个操作，显然这个延迟有点太高了。Lamport 老爷子依然神机妙算，提出了 Fast Paxos 算法，在没有冲突的情况下只需要 4 次 RPC 消息延迟就可以提交一个操作。考虑到最理想的情况下也还需要 2 次 RPC 消息延迟才能提交客户端的操作，4 次相比于 7 次已经是一个很可观的改进了。</p><p>当然，在多 Leader 的情况下，冲突的可能性更大，而且 Fast Paxos 对于泛化的 Paxos 没有进一步说明，如何将这个快速提交的算法应用到多 Leader 的泛化的 Paxos 算法，就是更进一步的挑战了。</p><p>这篇论文从基本的 MultiPaxos 算法出发，按照上面说的思路一步步地去改进基本的算法，通过对比差异之处，来试图给读者一个更清晰的印象，让读者更好地理解不同的算法的优化点是怎么提出来的。文章里没有希腊字母，可以放心食用。</p><h2 id="基本-Paxos-算法"><a href="#基本-Paxos-算法" class="headerlink" title="基本 Paxos 算法"></a>基本 Paxos 算法</h2><p><a href="/programming/paxos-optimizations/16514772501.png" data-fancybox="gallery" data-caption="16514772501.png"><img src="/programming/paxos-optimizations/16514772501.png" alt="16514772501.png"></a>在基本的 Paxos 算法中，Phase 1 用来确认是否已经有值被大多数的 Acceptor 接受过，Phase 2 则用来通知 Acceptor 接受值。这个只是针对一个值的共识算法。如果将不同操作视为不同的值，那么就可以引出 MultiPaxos 算法了。我们都知道，假如需要容忍 $f$ 个进程发生故障，那么需要至少 $2f+1$ 个节点，这样才能保证故障前和故障后的大多数有交集，从而避免不一致的情况发生。</p><h3 id="MultiPaxos-算法"><a href="#MultiPaxos-算法" class="headerlink" title="MultiPaxos 算法"></a>MultiPaxos 算法</h3><p><a href="/programming/paxos-optimizations/16514771811.png" data-fancybox="gallery" data-caption="16514771811.png"><img src="/programming/paxos-optimizations/16514771811.png" alt="16514771811.png"></a>如上图所示，MultiPaxos 首先通过 Leader 选举算法在 $f+1$ 个 Proposer 中选出一个 Leader，选举完成后，Leader 对之前每一个日志项执行一次 Paxos 的 Phase 1，之后这个 Leader 负责所有客户端的交互。Leader 在收到客户端的操作命令之后，确定一个日志编号 $i$，直接执行 Phase 2 进行共识。在收到大多数的 Acceptor 的确认之后，Proposer 就可以确定这个日志项的值了，并将这个日志项发送到 Replica 中执行，Replica 执行完成后通知客户端。当然这里的 Proposer、Accpetor 和 Replica 并不是真的都是独立的进程，可以理解为他们是一个服务进程里的不同线程。可以看到 Leader 需要处理所有的客户端请求，而且 Leader 一共需要收发 7 次消息，而其他进程只需要最多收发 2 次消息，显然 Leader 会成为系统的瓶颈。另外，日志也应该是连续的，Replica 是以一个操作日志的前缀执行操作的，这就使得不同的操作串行化了。</p><h2 id="冲突图"><a href="#冲突图" class="headerlink" title="冲突图"></a>冲突图</h2><p><a href="/programming/paxos-optimizations/16514171961.png" data-fancybox="gallery" data-caption="16514171961.png"><img src="/programming/paxos-optimizations/16514171961.png" alt="16514171961.png"></a>实际上，真实的应用场景中，很多操作都是可以并发执行的，不关心执行顺序，而只有一些操作需要规定严格的依赖关系。为了能最大化执行效率，我们需要定义哪些操作之间是可以并发执行的，哪些是不可以的，也就是冲突的。从日志角度来看，如果两个操作交换了顺序执行，而不影响最终的状态的话，那么这两个操作就是不冲突的，否则就是冲突的。例如，对于不同变量的赋值是没有冲突的，可以并发执行，而那些依赖于赋值后变量的值的操作就是冲突的，需要规定顺序执行。例如在图中，<code>a=2</code> 和 <code>b=1</code> 这两个操作就是可交换的，不冲突的，可以并发执行。而 <code>b=a</code> 如果被交换到 <code>b=1</code> 之前执行，那么最终 b 的值会变为 1，和原来 b 的值为 2 结果不一样，所以这两个操作是冲突的，不能并发执行，因为并发执行不能保证执行的顺序，有可能导致不同副本上的执行结果不一致。</p><p><a href="/programming/paxos-optimizations/16514172061.png" data-fancybox="gallery" data-caption="16514172061.png"><img src="/programming/paxos-optimizations/16514172061.png" alt="16514172061.png"></a>根据上面冲突的定义，可以画出一个执行日志对应的冲突图，图中的顶点就是日志中的操作，而顶点之间的有向边则代表了冲突关系。显然，我们可以以冲突图的任意一个逆拓扑序去执行操作。也就是说，对于不冲突的操作，我们可以并发执行。而如果想要往这个图中添加新的操作，需要检查这个新的操作和已有的所有操作是否冲突，是的话就添加一条边，保证执行的顺序不会错乱。</p><p>形式化一点来讲，图中的顶点为 $v$，那么它所依赖的顶点的集合就是 $\text{deps}(v)$。</p><p>有了冲突图的定义之后，就可以将普通的 Paxos 算法扩展为泛化 Paxos 算法了。泛化的 Paxos 算法需要满足<strong>共识不变式</strong>以及<strong>依赖不变式</strong>。</p><p><strong>共识不变式</strong>很好理解，就是对于每一个顶点 $v$ ，最多只能够有一个值 $(x, \text{deps}(v))$，就如同 Raft 的日志中，一个日志项要么没有提交，一旦提交了所有节点都是同一个值。</p><p>而<strong>依赖不变式</strong>，则是形式化的描述了依赖图中的冲突关系。对于一个已经确定了值 $(x, \text{deps}(v_x))$ 的顶点 $v_x$ 和$(y, \text{deps}(v_y))$ 的顶点 $v_y$ ，如果 $x$ 和 $y$ 存在冲突，要么 $v_x \in \text{deps}(v_y)$ ，要么 $v_y \in \text{deps}(v_x)$，或者两者同时满足。前面两个都很好理解，因为节点新增的时间可能不同。但是为什么可以同时满足要等到后面实现细节才好理解。</p><p><a href="/programming/paxos-optimizations/16514770721.png" data-fancybox="gallery" data-caption="16514770721.png"><img src="/programming/paxos-optimizations/16514770721.png" alt="16514770721.png"></a>有了偏序定义之后，就可以开始对基本的 Paxos 的算法进行改进了。泛化的 Paxos 算法如上图所示，它和 MultiPaxos 最大的不同就是增加了 $2f+1$ 个 Dependency Service，这个就是用来计算依赖关系的节点。另外，在这个改进的算法中，不再有单一的一个 Leader，所有 Proposer 都可以处理客户端发送过来的操作命令。Acceptor 进行共识的是冲突图的顶点，而 Replica 执行的也不再是日志，而是上面提到的冲突图。相应地，处理客户端请求的步骤也变多了。这个算法处理请求的流程有 7 步：</p><ol><li>客户端发送请求 $x$ 到任意一个 Proposer；</li><li>Proposer 生成一个全局唯一的顶点 ID $v_x&#x3D;(p_i, m)$，其中 $p_i$ 是进程的标识符，$m$ 是这个进程里单调递增的一个序列号。然后，将 $v_x$ 以及 $x$ 发送到所有的 Dependency Service；</li><li>依赖服务计算 $\text{deps}(x)$ 发回给 Proposer；</li><li>收到至少 $f+1$ 个回复后，Proposer 针对 ID 为 $v_x$ 的顶点发起一次 Paxos 共识算法，值就是操作以及依赖的顶点 $(x, \text{deps}(v_x))$。这里可以直接发起 Accept 请求的原因是因为这个顶点是刚刚生成的全局唯一的 ID，此时应该还没有别的进程发起过 Accept 请求，所以 Proposer 可以安全地发起任何值；</li><li>Acceptor 发送回复；</li><li>当大多数确定了 $v_x$ 的值之后，Proposer 将确认的值 $(x, \text{deps}(v_x))$ 发送给 Replica 执行；</li><li>Replica 执行操作后，回复客户端。</li></ol><p>那么依赖服务如何计算一个操作的依赖呢？首先，每一个依赖服务节点 $d_i$ 都会保存一份无环的冲突图，当一个节点收到了 $x$ 和 $v_x$ 之后，如果检查发现冲突图中已经包含了 $v_x$，那么就不做任何操作，不改变冲突图，直接返回 $v_x$ 指向的所有节点的集合；否则，就将 $v_x$ 加入到冲突图，并检查所有其他的顶点 $v_y$，如果 $x$ 和 $y$ 冲突，就添加一条从 $v_x$ 指向 $v_y$ 的边。遍历完成后，同样返回 $v_x$ 指向的所有顶点的集合。</p><p>当 Proposer 收到至少 $f+1$ 个回复之后，取所有回复的<strong>并集</strong>作为顶点的依赖。例如，假设 $f&#x3D;1$，$d_1$ 的回复是 $\left\{v_w, v_y\right\}$，$d_2$ 的回复是 $\left\{v_w, v_z\right\}$，那么 $\text{deps}(x)&#x3D;\left\{v_w, v_y, v_z\right\}$。</p><p>经过这样的操作，就可以得到另外一个不变式：如果 $v_x$ 和 $v_y$ 中的操作 $x$ 和 $y$ 冲突了，并且依赖关系服务计算出了 $\text{deps}(x)$ 和 $\text{deps}(y)$，要么 $v_x \in \text{deps}(v_y)$ ，要么 $v_y \in \text{deps}(v_x)$，或者两者同时满足。这个和依赖不变式的不同在于，这个不变式的 $\text{deps}(x)$ 是从依赖服务返回的。</p><p><a href="/programming/paxos-optimizations/16514795521.png" data-fancybox="gallery" data-caption="16514795521.png"><img src="/programming/paxos-optimizations/16514795521.png" alt="16514795521.png"></a>但是，两个冲突的操作，可能是同时发起的，而不同的消息到达不同的依赖服务节点的顺序有可能不同，这就导致了在不同服务节点中的依赖关系可能不同。即使依赖服务维护的冲突图是无环的，Replica 中形成的冲突图也有可能有环。以上图为例，$x$ 和 $y$ 是两个冲突的操作，分别同时发送给了 $p_1$ 和 $p_2$。$p_1$ 发送 $x$ 和 $v_x$ 给依赖服务，同时 $p_2$ 发送 $y$ 和 $v_y$ 给依赖服务。由于网络问题，$d_1$ 和 $d_2$ 先收到了 $x$，然后接收到了 $y$，所以根据这两个节点计算得到 $\text{deps}(v_x)&#x3D;\emptyset$， $\text{deps}(v_y)&#x3D;\left\{v_x\right\}$。$d_3$ 则是先收到了 $y$ 再收到了 $x$，所以计算得到  $\text{deps}(v_x)&#x3D;\left\{v_y\right\}$， $\text{deps}(v_y)&#x3D;\emptyset$。之后，由于网络问题，$d_1$ 的回复丢失了，$p_1$ 和 $p_2$ 分别都收到了 $d_2$ 和 $d_3$ 的回复，根据上面的算法，因为已经收到了大多数的回复，他们各自对自己关心的节点的依赖取并集，然后向 Acceptor 发送对应的值。$p_1$ 认为 $v_x$ 的值是 $(x, \left\{v_y\right\})$，$p_2$ 认为 $v_y$ 的值是  $(y, \left\{v_x\right\})$。这两个值都能够顺利的被 Acceptor 接受并提交了，然后 Proposer 将确认后的值发送给 Replica。Replica 将 $v_x$ 和 $v_y$ 加入到自己的冲突图中，并在接收到两个操作之后执行操作，并返回操作结果给客户端。可以看到，Replica 中的冲突图成环了。对于这种情况，需要以一种确定的预先规定好的算法来打破循环。</p><p>引入依赖图和日志有一个同样的问题，如果一个操作的依赖中有顶点因为各种原因一直没能确定值（比如 Proposer 突然挂了），那么这个操作也永远执行不了。为了解决这种问题，可以设置一定的超时时间，如果一个操作等依赖执行等得太久了，就用一个空操作来取代原来的节点。空操作指的是一个对于状态机安全的，不会改变状态的操作。恢复的过程也很简单，只需要对于那个节点，以第 $r (r\gt 0)$ 轮从 Phase 1 执行一次 Paxos 算法即可。这时需要执行 Phase 1 的原因是，有可能上一个 Proposer 已经让大多数的 Acceptor 接受值了，这时候需要做的就是将没有完成的流程进行下去，不能提交空操作。通过执行一次完整的 Paxos 就完成了恢复，也让整个算法变得更简单。</p><p>当然，上面的这个算法为了提交一个操作，需要等 7 次消息的延迟，显然有点不能接受。为了提高性能，还需要想办法减少消息的数量。</p><h3 id="快速提交-Fast-Paxos"><a href="#快速提交-Fast-Paxos" class="headerlink" title="快速提交 Fast Paxos"></a>快速提交 Fast Paxos</h3><p>为了改进 Paxos 交换信息次数太多的缺点，Lamport 又提出了 Fast Paxos。这个算法和原始的 Paxos 算法一样，只是用来确定单个值的。</p><p>就像之前的 MultiPaxos 一样，如果很确定自己是第一个提出一个值的话，那么就可以安全地跳过第一阶段，直接进入第二阶段提交。类似地，在这个改进版的算法中，客户端不再和 Proposer 通信，而是乐观地直接向 Acceptor 发起 Accept 请求，如果 Acceptor 没有接受过更早的消息（包括第 0 轮），那么就接受这个请求，返回 ${\rm P{\small HASE}2B}\left &lt; 0, v \right &gt;$ 消息给 Leader。定义多数函数 $\text{maj}(n)&#x3D;\lceil \frac{n+1}{2} \rceil$，也就是求 $n$ 个人最少需要多少人算大多数。只有当 Leader 收到至少 $f+\text{maj}(f+1)$ 条 ${\rm P{\small HASE}2B}\left &lt; 0, v’ \right &gt;$ 回复且 $v’$ 相等之后，才能确定这个值为 $v’$。可见，为了能够安全提交一个值，需要的 Acceptor 比 $f+1$ 多。但随之而来的好处是，提交一个值只需要 4 次消息的延迟。</p><p><a href="/programming/paxos-optimizations/16514971281.png" data-fancybox="gallery" data-caption="16514971281.png"><img src="/programming/paxos-optimizations/16514971281.png" alt="16514971281.png"></a>现在来考虑一下出错的情况。如果是 Leader 挂了，那么其他的 Proposer 需要针对这个值从头进行一次 Paxos 算法。Proposer 和普通的 Paxos 算法一样，需要先确定一个轮数 $i$，并且用这个轮数发送 ${\rm P{\small HASE}1A}\left &lt; i \right &gt;$ 给至少 $f+1$ 个 Acceptor，收到至少 $f+1$ 个  ${\rm P{\small HASE}1B}\left &lt; i, vr, vv \right &gt;$ 消息之后，Proposer 计算 $k&#x3D;\max{\{vr\}}$ ，也就是和 Paxos 算法一样取最大的那个数。另外，记收到回复的 Acceptor 的集合为 $A$。如果 $k&#x3D;-1$，那说明还没有任何值被提交过，Proposer 可以自由地提交任意值；如果 $k\gt 0$，那么 Proposer 就要继续完成没有完成的共识，只能够提交 $k$ 对应的 $vv$ 。</p><p>当 $k&#x3D;0$ 的时候，说明有一些 Acceptor 收到过来自客户端的消息。此时，$v$ 有可能已经确定了某个值，也有可能还没有确定，需要分情况讨论。</p><p>如果 $\text{maj}(f+1)$ 个 Acceptor 已在第 0 轮的时候投票给了某个值 $v’$，那么这个值可能已经被决定了。如果 $A$ 集合之外的 $f$ 个进程也投票给了 $v’$，那么这时候就满足了 $f+\text{maj}(f+1)$ 的数量要求，可以安全地提交 $v’$ 了。如果没有哪个值得到了 $\text{maj}(f+1)$ 的票数，那么 Proposer 就可以断定在第 0 轮的时候没有决定一个值，此时它可以提出任意值。换句话说，如果一个值想要被快速提交，它不仅要得到大多数成员的认可，还要在大多数的大多数中得到认可，才能安全地提交。</p><p>确定了要提交哪个值之后，Proposer 就可以继续进行 Phase 2 的 Paxos，将值提交了。需要注意的是，想要快速提交一个值的话必须得到至少 $f+\text{maj}(f+1)$ 的票数，否则会出现脑裂的情况。假如 $f&#x3D;2$，Acceptor 分别为 $a_1, a_2, \dots, a_5$。如果一个值只需要 3 个 Acceptor 同意，那么假如按照上面恢复的过程，接手的 Proposer 使用 $i&#x3D;1$ 向 $a_3, a_4, a_5$ 发起 Phase 1 请求，此时 $a_3$ 回复它给 $x$ 投票了，$a_4$ 回复它给 $y$ 投票了，$a_5$ 说它没投票。注意它们都是第 0 轮。现在，因为 $a_1$ 和 $a_2$ 的状况未知，如果它们投票给了 $x$，根据我们的假设，满足了至少 $f+1$ 的票数，那么被选中的就是 $x$；但是它们也有可能投票给了 $y$，此时被选中的就是 $y$。Proposer 此时并不能决定提交哪个值才是正确的，算法就无法进行下去了。如果我们要求快速提交的时候需要 $f+\text{maj}(f+1)$ 的票数，那么这种情况就无法发生，要么有一个值得到了剩下的 $f+1$ 个成员的大多数的投票，此时可以断定这个值已经能够提交，要么剩下的 $f+1$ 个成员没有达成大多数的共识，那按照快速提交的规则，没有值能够安全提交，这时候就可以安全地提交任意一个值了。</p><p>快速提交发生冲突的情况也是类似的处理，要是没有哪个值能够达到要求的票数，那么就由 Proposer 发起普通的 Paxos 过程。</p><p>需要注意的是 $f+\text{maj}(f+1)$ 的限制只是为了安全的快速提交而设的。如果中途 Acceptor 挂了或是发生了别的情况，我们还是可以向剩下的至少 $f+1$ 个进程发起普通 Paxos 请求来提交值的。</p><p>这里还有一个可以优化的点，在冲突发生的时候，除了重新进行一次普通的 Paxos 算法，其实在冲突发生的时候 Leader 就已经能够收集到足够的信息了。在第 1 轮的 Paxos 算法中，${\rm P{\small HASE}1B}\left &lt; 1, 0, v’ \right &gt;$ 包含的信息和第 0 轮的 ${\rm P{\small HASE}2B}\left &lt; 0, v’ \right &gt;$ 信息是完全一样的。所以我们可以将 ${\rm P{\small HASE}2B}\left &lt; 0, v’ \right &gt;$ 看成是 ${\rm P{\small HASE}1B}\left &lt; 1, 0, v’ \right &gt;$。这样，第 1 轮的 Proposer 就可以直接跳到 Paxos 的 Phase 2 进行恢复了。</p><h3 id="不安全的快速泛化-Paxos"><a href="#不安全的快速泛化-Paxos" class="headerlink" title="不安全的快速泛化 Paxos"></a>不安全的快速泛化 Paxos</h3><p>了解了 Fast Paxos 算法后，我们可以依葫芦画瓢地改进普通的泛化的 Paxos 算法了。</p><p><a href="/programming/paxos-optimizations/Pasted-image-20220502220309.png" data-fancybox="gallery" data-caption="Pasted image 20220502220309.png"><img src="/programming/paxos-optimizations/Pasted-image-20220502220309.png" alt="Pasted image 20220502220309.png"></a></p><p>在改进的算法中，同样编号的角色都跑在同一个服务器上，这样同一个编号的不同角色之间的消息传递都是本地通信，不需要引入网络开销。</p><p>客户端还是需要通过任意一个 Proposer 来提交操作请求。Proposer 收到请求之后，一样是生成一个编号 $v_x$，并将操作 $x$ 和 $v_x$ 发送到依赖服务节点，和之前不同的是，依赖服务节点收到新的节点之后，并不是直接将 $\text{deps}(v_x)$ 发送回 Proposer，而是直接向自己本地的 Acceptor 发送 $(x, \text{deps}(v_x))$ 作为 $v_x$ 的值。Acceptor 和 Fast Paxos 中的 Acceptor 一样，当接收到值之后，按照规则投票或忽略，并将回复发送回 Proposer。同样的，Proposer 在收到 $f+\text{maj}(f+1)$ 个同样值的回复之后，就返回客户端操作成功，否则执行恢复流程。</p><p><a href="/programming/paxos-optimizations/16515006761.png" data-fancybox="gallery" data-caption="16515006761.png"><img src="/programming/paxos-optimizations/16515006761.png" alt="16515006761.png"></a></p><p>但是这个算法是不安全的。假设还是 $f&#x3D;2$ 的情景，$p_1$ 和 $p_5$ 分别同时收到了冲突的请求 $x$ 和 $y$，$p_1$ 将节点 $v_x$ 和 $x$ 发送给了 $d_1$ 和 $d_2$，它们由于不知道 $y$，计算得到 $\text{deps}(v_x)&#x3D;\emptyset$，并向 $a_1$ 和 $a_2$ 提出 $v_x$ 的值为 $(x, \emptyset)$。发给 $d_3, d_4, d_5$ 的消息被丢包了。类似地，$a_4$ 和 $a_5$ 提出 $v_y$ 的值为 $(y, \emptyset)$。$d_3$ 自始至终都没有收到任何消息，所以 $a_3$ 没有投票给任何值。这时候 $p_1$ 和 $p_5$ 都挂了，$p_2$ 负责恢复 $x$ 。它向 $a_1, a_2, a_3$ 发起了 Phase 1 的 Paxos 请求，这时候它收到了 $a_1$ 和 $a_2$ 的回复 $(x, \emptyset)$，因为 $\text{maj}(f+1)&#x3D;\text{maj}(3)&#x3D;2$，所以 $p_2$ 只能提交 $(x, \emptyset)$。类似地，$p_4$ 负责 $y$ 的恢复，并最终确定只能提交 $(y, \emptyset)$。这时候，Replica 分别收到 $(x, \emptyset)$ 和 $(y, \emptyset)$，所以在它们看来，$x$ 和 $y$ 不冲突，可以并发执行，就有可能导致不同 Replica 执行顺序不一致导致最终状态的不一致。</p><p>问题就出在，$\text{deps}(v_x)&#x3D;\emptyset$ 并没有得到大多数依赖服务的节点的共识。单独保证<strong>共识不变式</strong>很简单，单独保证<strong>依赖不变式</strong>也很简单，但是要同时确保两者就有点 tricky 了。在恢复的过程中，共识不变式强制要求 Proposer 提交一个特定的值，而依赖不变式则强制要求 Proposer <strong>不要</strong>提交那个值。这种两个不变式之间的矛盾，论文作者称之为<strong>基本矛盾</strong>。怎样在恢复过程中解决这个矛盾，是所有泛化的多 Leader 的 Paxos 算法最核心的挑战。EPaxos、Caesar 以及 Atlas 都是类似的，它们的进程组织是类似的，正常的执行流程也是类似的，最大的不同之处就在于它们是如何解决这个基本矛盾的。</p><p>解决这个基本矛盾的方法有两种，一种是避免矛盾，另一种则是解决矛盾。避免矛盾是通过调整 Quorum 的大小来避免矛盾的发生；而解决矛盾则更加复杂，需要在发现矛盾之后尝试解决它。</p><h3 id="避免矛盾"><a href="#避免矛盾" class="headerlink" title="避免矛盾"></a>避免矛盾</h3><p>在 Fast flexible paxos: Relaxing quorum intersection for fast paxos 这篇论文中，作者指出，只要满足以下两个条件，Fast Paxos 算法就是安全的：</p><ol><li>每一个 Phase 1 的 Quorum 集合（也就是大多数节点的集合）$Q$，和每一个 Phase 2 的 Quorum 集合 $Q’$ 相交。也就是 $Q \cap Q’\ne \emptyset$ 。</li><li>每一个 Phase 1 的 Quorum 集合 $Q$ 和每一<strong>对</strong>快速 Phase 2 的 Quorum 集合 $Q’, Q’’$ 相交，也就是 $Q\cap Q’ \cap Q’’\ne \emptyset$ 。</li></ol><p>那么，根据这个结果，只需要将不安全的快速泛化 Paxos 算法的快速提交需要的票数提高到 $f+(f+1)$ 就可以了。但是这意味着要想快速提交一个值得全体进程统一才可以。</p><p>和不安全的算法对比，在恢复阶段，当 $k&#x3D;0$ 时，只有当 Proposer 收到了 $f+1$ 个同样的 ${\rm P{\small HASE}1B}\left &lt; 1, 0, v’ \right &gt;$ 回复后，才是被强制提交 $v’$ 的，在其他情况下，Proposer 都可以选择任意的满足依赖不变式的值提交。这个算法用更大的进程集合为代价，完全避免了矛盾的发生。因为在 $f+1$ 个相同的回复后，说明 $\text{deps}(v_x)$ 已经被大多数节点接受了，此时就不会发生依赖不一致的现象了。</p><p>显然，如果每一次提交都要和所有进程通信，那么万一有一个进程比较慢，就会拖慢整个算法，如果有一个进程挂了，那么算法就无法继续进行了（虽然还是可以通过其他方法用基本 Paxos 算法执行）。无论是从性能的角度还是可用性的角度这个算法都不太实用，所以还需要进一步的改进。</p><h3 id="基本的-EPaxos-改进"><a href="#基本的-EPaxos-改进" class="headerlink" title="基本的 EPaxos 改进"></a>基本的 EPaxos 改进</h3><p><a href="/programming/paxos-optimizations/Pasted-image-20220502230155.png" data-fancybox="gallery" data-caption="Pasted image 20220502230155.png"><img src="/programming/paxos-optimizations/Pasted-image-20220502230155.png" alt="Pasted image 20220502230155.png"></a></p><p>通过对上面的算法进一步改进，可以将快速提交的票数要求降低为 $f+f$。</p><p>第一个改动是 Proposer $p_i$ 在收到请求之后不再广播请求到所有的依赖服务节点，而只在本地计算依赖。</p><p>第二个改动是，本地的 $d_i$ 像普通的算法一样先计算出依赖节点，然后由依赖服务负责将 $v_x, x$ 和 $\text{deps}(v_x)_i$ 广播给其他节点。当其他节点 $d_j$ 接收到之后，按照自己的依赖图计算 $\text{deps}(v_x)_j$，然后向 $a_j$ 提出 $(x, \text{deps}(v_x)_i \cup \text{deps}(v_x)_j)$ 。</p><p>Acceptor 还是像之前一样投票给提出的值。</p><p>最后一个改动是，当 Proposer 接收到 $f+f$ 票后（包括本地的 $a_i$）就认为这个值确定了，并将确定的值 $v$ 发给本地的 $a_i$，如果此时 $a_i$ 还处于第 0 轮，那么这个值就是真正地确定了。如果不是的话，就拒绝这次请求，回复错误信息给 $p_i$。这整个过程都只涉及本地进程，不需要网络通信。</p><p>在正常情况下，$v_x$ 就正式确定了，$a_i$ 将 $v$ 广播到所有 Acceptor，如果有 Acceptor 拒绝了，就执行一次恢复过程。</p><p>需要注意的是，上面所说的优化只在 $f\gt 1$ 的时候成立，在 $f&#x3D;1$ 的时候，所有的 Quorum 集合大小都是 $f+1&#x3D;f+f&#x3D;2$ ，不满足前面所说的两条定理，因此是不安全的。这时候，快速提交仍然需要 $f+f+1$ 票才能安全提交。</p><h3 id="Atlas-优化"><a href="#Atlas-优化" class="headerlink" title="Atlas 优化"></a>Atlas 优化</h3><p>Atlas 优化的思想是放宽一致投票的要求，并增加 Proposer 执行快速提交的可能性。它定义了 $\text{popular}(X_1, X_2, \dots, X_{2f+1})&#x3D;\left\{x|x 至少在 f+1 个集合中出现 \right\}$ ，其中 $X_i$ 是集合。当 Proposer 收到了来自 $2f+1$ 个 Acceptor 的回复后，如果 $\text{deps}(v_x)&#x3D;\text{popular}\left(\text{deps}(v_x)_1, \text{deps}(v_x)_2, \dots, \text{deps}(v_x)_{2f+1} \right)$，那么就执行快速提交，并且 $\text{deps}(v_x)&#x3D;\text{deps}(v_x)_1\cup \text{deps}(v_x)_2\cup \cdots\cup \text{deps}(v_x)_{2f+1}$。也就是说，Proposer 只有在每一个依赖服务计算出的依赖顶点 $v_y$ 也被大多数的依赖服务节点计算出来的时候，才执行快速提交。</p><h3 id="解决矛盾"><a href="#解决矛盾" class="headerlink" title="解决矛盾"></a>解决矛盾</h3><p>避免矛盾需要很多的节点才可以快速提交，所以并不是很实用。为了达到和 Fast Paxos 一样的 $f+\text{maj}(f+1)$ 的快速提交票数，还需要进一步优化算法，允许矛盾发生，并在发现矛盾的时候去解决矛盾。再回顾一下基本矛盾，在执行恢复的时候，共识不变式要求 Proposer 提出 $(x, \text{deps}(v_x))$，但同时，它无法保证 $\text{deps}(v_x)$ 是由大多数的依赖服务节点计算出来的，这就导致了它<strong>不能</strong>提交 $(x, \text{deps}(v_x))$。依赖避免算法通过增大 Quorum 大小，保证了 Proposer 提交的  $\text{deps}(v_x)$ 是由大多数的依赖服务节点计算出来的，这也导致了性能和可用性的下降。</p><p>矛盾解决算法则并不要求更多的 Quorum 节点，并且允许在恢复的时候出现这种不一致的现象，但通过更复杂的机制，要么确定 $\text{deps}(v_x)$ 没有被提交成功，要么确定 $\text{deps}(v_x)$ 已经被大多数节点接受，可以提交。矛盾解决算法更加抽象复杂，需要更多的逻辑推理。</p><p>这个算法需要引入一个剪枝依赖的概念。假如一个 Proposer $p$ 已经知道 $v_y$ 的值是 $(y, \text{deps}(v_y))$，对于依赖里的一个顶点 $v_x\in \text{deps}(v_y)$ ，就没有必要依赖 $v_y$ 了，因为它们的冲突关系已经在 $v_y$ 的值中表达出来了（回忆一下之前的依赖算法没有避免环的产生）。也就是说，对于互相冲突的操作 $x$ 和 $y$，要么 $v_x\in \text{deps}(v_y)$ 要么 $v_y\in \text{deps}(v_x)$ 。注意这时候两者同时成立的可能被排除了。</p><p>所以，我们可以剪枝掉一些不必要的依赖节点。假设 $v_x$ 的依赖是 $\text{deps}(v_x)$ ，那么可以计算出一个集合 $P\subseteq \text{deps}(v_x)$ ，对于 P 里面的元素 $v_y$ ，要么 $v_y$ 提交了一个空操作，要么 $v_y$ 提交的 $\text{deps}(v_y)$ 中包含了 $v_x$ 。最后，计算出 $\text{deps}(v_x)-P$ ，就是节点 $v_x$ 的剪枝后的依赖了。</p><p>对于这个剪枝后的依赖，提出一个新的<strong>剪枝依赖不变式</strong>，对于每一个节点 $v_x$，它的值要么提交了 $(\text{noop}, \emptyset)$，要么提交了 $(x, \text{deps}(v_x)-P)$，其中 $\text{deps}(v_x)-P$ 就是按照上面的算法计算出的剪枝后的依赖。</p><p>接下来，就开始修改之前的快速泛化 Paxos 算法了。第一个大的改动是，每一个 Acceptor 和 Replica 一样，也需要维护一个冲突图，当 Acceptor 收到消息确定 $v_x$ 提交了 $(x, \text{deps}(v_x))$ 之后，也按照算法，将 $v_x$ 加入到图中，并添加 $v_x$ 指向 $\text{deps}(v_x)$ 每一个顶点的边。其次，Proposer 执行的恢复算法要复杂得多。</p><p>具体来说，按照快速泛化 Paxos 算法，Proposer 收到至少 $f+1$ 个  ${\rm P{\small HASE}1B}\left &lt; i, vr, vv \right &gt;$ 消息（记发送消息的 Acceptor 集合为 $A$）之后，如果 $k&#x3D;0$ 且 $v’&#x3D;(x, \text{deps}(v_x))$ 获得了至少 $\text{maj}(f+1)$ 票，按照快速算法，Proposer 需要提交 $v’$。但按照泛化算法，却无法保证 $\text{deps}(v_x)$ 是原来大多数节点所求依赖的并集，所以这个值不能提交。这时候，冲突解决算法需要做一些额外的工作，来断定要么 $v’$ 并没有被提交，要么 $\text{deps}(v_x)$ 是 $v_x$ 的一个剪枝依赖集合。</p><p><a href="/programming/paxos-optimizations/Pasted-image-20220503155932.png" data-fancybox="gallery" data-caption="Pasted image 20220503155932.png"><img src="/programming/paxos-optimizations/Pasted-image-20220503155932.png" alt="Pasted image 20220503155932.png"></a></p><p>首先要做的是，Proposer 向发送消息的 $f+1$ 个依赖服务（和对应的 Acceptor 在同一个进程中）发送 $v_x$ 和 $x$，并将这些依赖服务返回的依赖集合求并集，保存到 $\text{deps}(v_x)_A$ 中，并且计算一个差集合 $Y&#x3D;\text{deps}(v_x)_A-\text{deps}(v_x)$，也就是用返回的依赖和 Acceptor 保存的依赖作差（因为 $\text{deps}(v_x)$ 是 $\text{maj}(f+1)$ 个 Acceptor 返回的，$\text{deps}(v_x)_A$ 是 $f+1$ 个节点返回的，前面的集合是后面集合的自己）。</p><p>然后，需要对这个集合开始剪枝操作。一开始 $P&#x3D;\emptyset$ ，对于 $Y$ 中的每一个顶点 $v_y$，如果还不确定 $v_y$ 是否已经提交值，那么就需要先对 $v_y$ 执行恢复操作。在确定了 $v_y$ 的提交值之后，根据提交的内容来执行进一步的操作。如果 $v_y$ 提交的是一个空操作，或者 $v_x\in \text{deps}(v_y)$，那么就直接剪枝，将 $v_y$ 添加到 $P$ 中。否则，则需要向 Acceptor 的大多数集合 $A’$ 确认 $v_x$ 的状态。如果有 Acceptor 返回 $v_x$ 已提交，那么就直接终止恢复过程。如果所有的 Acceptor 都没有认为 $v_x$ 已提交，那么 Proposer 就可以安全地提出任何一个满足依赖不变式的值 $v$，并向所有 Acceptor 发送 ${\rm P{\small HASE}2A}\left &lt; i, v \right &gt;$ 的消息。关于为什么这个时候就可以断定在第 0 轮中没有值被提交，可以提交任意值，需要更复杂的推理，放到后面讲。</p><p>循环结束之后，说明剪枝完成，此时 $\text{deps}(v_x)&#x3D;\text{deps}(v_x)_A-P$，也就是原来的 $v’$ 可以直接提交，这时候，就可以发送 ${\rm P{\small HASE}2A}\left &lt; i, v’ \right &gt;$，以及 $P$ 中每一个顶点的最终提交值给至少 $f+1$ 个 Acceptor 进行 $v_x$ 的提交了。</p><p>现在再回头看看，为什么上面说 Proposer 可以断定没有值被提交。首先，执行到这个分支的时候，我们知道 $v_y$ 不是空操作，而且 $v_x \notin \text{deps}(v_y)$ ，根据剪枝依赖不变式，$\text{deps}(v_y)&#x3D;\text{deps}(v_y)_D-P’$，其中 $\text{deps}(v_y)_D$ 是由至少 $f+1$ 个依赖服务节点 $D$ 计算得到的集合的并集。因为 $v_x\notin \text{deps}(v_y)_D-P’$，要么 $v_x$ 本来就不在依赖中 $v_x\notin \text{deps}(v_y)_D$，要么被剪枝了 $v_x\in P’$。</p><p>但是 $v_x$ 是不可能在 $P’$ 中的。因为此时节点已经达成了 $v_y$ 的 $\text{deps}(v_y)&#x3D;\text{deps}(v_y)_D-P’$ 共识，同时也达成了 $P’$ 中的所有顶点已经提交了的共识，根据相交定理，肯定至少有一个联系到的 Acceptor 确定 $v_x$ 已经提交。如果 $v_x$ 在 $P’$ 中，就产生了矛盾，因为这时候算法认为 $v_x$ 还没有提交。因此，$v_x\notin \text{deps}(v_y)_D$ 成立。所以，$D$ 中的所有节点都先处理了 $v_y$，再处理 $v_x$。又因为 $v_y\notin \text{deps}(v_x)$（因为 $\text{deps}(v_x)$ 是 $\text{deps}(v_x)_A$ 子集，如果 $v_y\in \text{deps}(v_x)$，那它就不可能出现在循环中），这时候就需要有快速提交的大多数达成共识 $v_x$ 在 $v_y$ 前处理，但已经有 $D$ 个节点达成了 $v_y$ 在 $v_x$ 前处理的共识，前面的共识是不可能被提交的。所以 $v’&#x3D;(x, \text{deps}(v_x))$ 肯定没有被提交，Proposer 可以任选一个值提交。</p><p>经过一番推理之后，我们得到了一个解决冲突的算法。但是这个算法有一个缺点：它有可能死锁。这是因为，有可能存在节点 $v_1, v_2, \dots, v_n$ 其中每一个 $v_i$ 都依赖 $v_{i+1 \mod n}$，那么在递归解决未提交的顶点的时候，就会死锁。</p><p>为了解决死锁问题，我们还需要修改算法，具体来说，Proposer 会在两个条件都满足的情况下，认为一个顶点 $v_x$ <strong>快速</strong>提交了 $v&#x3D;(x, \text{deps}(v_x))$。首先当然是这个值得到了来自快速提交所需的 $f+\text{maj}(f+1)$ 票，并且记投票了的 Acceptor 集合为 $F$，<strong>并且</strong>对于每一个 $v_y\in \text{deps}(v_x)$ ，都有 $f+1$ 个 Acceptor $A\subseteq F$，在投票的时候知道 $v_y$ 已经提交了。其次，在 Acceptor $a_i$ 发送  ${\rm P{\small HASE}2B}\left &lt; 0, v \right &gt;$ 回复（$v&#x3D;(x, \text{deps}(v_x))$）的时候，顺带发送 $\text{deps}(v_x)$ 中 $a_i$ 知道已经提交了的节点和它们的值。</p><p>最后，在恢复的过程中，要检测出是否发生了循环依赖。具体来说需要在上面的恢复算法中的第 10 行之后插入以下算法：</p><p><a href="/programming/paxos-optimizations/Pasted-image-20220503160912.png" data-fancybox="gallery" data-caption="Pasted image 20220503160912.png"><img src="/programming/paxos-optimizations/Pasted-image-20220503160912.png" alt="Pasted image 20220503160912.png"></a></p><p>在获得 $v’&#x3D;(x, \text{deps}(v_x))$ 的信息之后，首先计算出投票给了 $v’$ 的 Acceptor 集合 $M$，Proposer 检查 $\text{deps}(v_x)$ 是否存在一个 $M$ 中没有 Acceptor 知道是否已经提交值的顶点 $v_y$，存在的话，说明 $v’$ 不可能在第 0 轮中提交。如果 $v’$ 提交了，那么说明 $F$ 中的 Acceptor 投票给了 $v’$，同时存在 $A’\subseteq F$ 中的 Acceptor 知道 $v_y$ 已经提交了。由于 $A’\cap A\ne\emptyset$，但又没有 Acceptor $a$ 同时投票并且知道 $v_y$ 已经提交，这就产生了矛盾。因此，Proposer 可以提出任意值。</p><p>其次，Proposer 可能之前正在恢复 $v_z$ ，然后根据算法递归调用来恢复 $v_x$，如果此时 $v_z\in \text{deps}(v_x)$，那么 $M$ 中肯定有 Acceptor $a$ 已经知道 $v_z$ 被提交了，这时候就可以终止 $v_z$ 的恢复过程了。否则，$v_z \notin \text{deps}(v_x)$，那么每一个 $M$ 中的节点的依赖服务都是先处理 $v_x$ 后处理 $v_z$。又因为 $|M|\ge \text{maj}(f+1)$，所以肯定只有少于 $f+\text{maj}(f+1)$ 个节点有可能认为 $v_z$ 早于 $v_x$ 处理。如果 Proposer 在恢复 $v_z$ 的时候需要递归恢复 $v_x$，那么 $v_x\notin \text{deps}(v_z)$（要不然的话 $v_x$ 已经确定提交了，不需要恢复）。因此，如果 $v_z$ 想要带着 $v_x\notin \text{deps}(v_z)$ 快速提交，那么就需要至少有 $f+\text{maj}(f+1)$ 个节点认为 $v_z$ 早于 $v_x$ 处理，但这和之前的条件矛盾，所以 $v_z$ 不可能被快速提交，这时候就可以提出任意值了。</p><p>这样，通过对 Paxos 算法循序渐进地改进，我们就得到了一个可以解决冲突的，不会死锁的快速泛化 Paxos 算法了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><a href="/programming/paxos-optimizations/16513947231.png" data-fancybox="gallery" data-caption="16513947231.png"><img src="/programming/paxos-optimizations/16513947231.png" alt="16513947231.png"></a></p><p>作者根据 Leader 的数量、是否泛化、能否快速提交、怎样处理基本矛盾，大致给不同的 Paxos 变种分到不同类别中。</p><h3 id="真实算法"><a href="#真实算法" class="headerlink" title="真实算法"></a>真实算法</h3><p>作者为了更清楚地说明 Paxos 算法的改进过程，对一些细节忽略或者做了修改。EPaxos 使用了带基本 EPaxos 改进的快速算法，来减少快速提交所需的票数，同时也使用了最后所说的依赖剪枝以及避免死锁的递归恢复算法。Caesar 则进一步地改进 EPaxos 算法，和 Atlas 优化类似，Caesar 在快速提交的时候不需要 Acceptor 都投票给完全一样的值。此外 Caesar 没有使用递归恢复算法，而是使用了逻辑时间戳以及在协议中仔细加入 Barrier 来避免递归的恢复。</p><h3 id="和单-Leader-日志型算法的比较"><a href="#和单-Leader-日志型算法的比较" class="headerlink" title="和单 Leader 日志型算法的比较"></a>和单 Leader 日志型算法的比较</h3><p>相比于 MultiPaxos、Raft 以及 Viewstamped Replication 这些将所有的操作赋予全序形成线性的日志的算法，泛化的多 Leader Paxos 算法虽然更复杂，但也有性能和可用性上的优势。</p><p>首先，多 Leader 避免了单 Leader 成为瓶颈的问题，任何 Proposer 都可以接受处理命令，提高了吞吐量。</p><p>其次，多 Leader 算法能更好地应对故障。在 Raft 和 MultiPaxos 这些算法里，一旦 Leader 发生故障，那么吞吐量会立即降为 0，直到新的 Leader 被选举出来后才能恢复。像 EPaxos 这种算法，如果有 Leader 死机了，那么吞吐量虽然也会下降，但不会下降到零，其他 Leader 还是可以继续处理命令。故障的 Leader 恢复后，吞吐量也能回归正常。</p><p>第三，对于地理上分开的应用来说，多 Leader 算法可以达到更低的延迟。和单 Leader 对比，可以在不同的数据中心各放置一个 Leader，这样不同数据中心的客户端感受到的延迟都是一样的，整体的延迟也会更低。</p><p>最后，泛化的 Paxos 能降低操作之间依赖关系很少的应用的尾延迟。在 Raft 中，一个命令如果因为网络问题没能提交，那么后面所有的命令都会受到影响。任何一个命令的延迟执行，都会影响到串行化到它后面的命令的延迟。而泛化的 Paxos 算法中，即使一个命令延迟执行了，和它不冲突的命令都可以不受影响地并发执行，降低了尾延迟。</p><h3 id="更多相关工作"><a href="#更多相关工作" class="headerlink" title="更多相关工作"></a>更多相关工作</h3><p>SpecPaxos 和 NOPaxos 采取更加激进的推测执行（Speculative Execution），对 Fast Paxos 改进到最佳情况下只需要 2 次网络延迟就能完成操作。CURP 则进一步地扩展泛化性，允许可交换的命令以任何顺序执行。但是，随着提交延迟的降低，算法的复杂性也在上升，进一步影响了它们在工程中的应用。</p><p>Mencius 则是一个多 Leader 的非泛化的 Paxos 变种，其中不同的 MultiPaxos 日志项在不同的 Leader 中分块处理，同样地，一个日志项只有等到它之前的日志项执行完成后才能执行。为了同步日志，Mencius 中的 Leader 会执行 All-to-All 的广播操作。它的优点是相比起 EPaxos 等算法简单很多。这也说明了对 Paxos 算法优化的复杂性主要来自于泛化而不是多 Leader。当然多 Leader 也是会带来一定复杂性的。</p><h3 id="Paxos-之外"><a href="#Paxos-之外" class="headerlink" title="Paxos 之外"></a>Paxos 之外</h3><p>除了千奇百怪的 Paxos 变种，还有很多其他的分布式共识算法。例如 Chain Replication，不同的节点组成了一条复制链，写的时候往头节点写，然后沿着复制链复制日志，读的时候只从尾节点读。链复制算法比 MultiPaxos 能达到更高的吞吐量，因为负载在不同节点上更加均衡。</p><p>Scalog 则使用了一种复杂的批处理方式来复制日志。客户端并不直接将日志项发送到一个中心的 Leader，而是发送到批处理节点中的任意一个。按照一定时间间隔，批处理的每一批数据会被封存，并且分配一个 ID，然后这个 ID 被当做日志 ID，就像 MultiPaxos 的日志编号一样，发往一个状态机复制协议。</p><p>这两个算法也说明 Leader 带来的瓶颈可以不通过多 Leader 来解决。</p><p>而 PQR、Harmonia、CRAQ 这些算法则实现了对于读操作的优化，对于不涉及状态机修改的命令，都可以不通过 Leader 来执行（Raft 也有类似的 Follower Read 优化），写命令还是要通过 Leader 执行。比较有趣的是，对于泛化后的 Paxos 能不能也实现读优化还是一个值得研究的问题。</p><p>Paxos 算法通过<strong>限制未来，用大多数集合相交排除可能性</strong>达到了各个节点的一致性。它简单，但仍有许多值得深入研究的地方。这正是它最引人入胜的地方。从泛化性和一致性的拉扯也可以看出系统设计中的一些 trade-off，如何找到其中的 sweet point，也是计算机系统研究的乐趣之一。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Linux 内核模块调试方法</title>
      <link>https://blog.howardlau.me/programming/debugging-linux-kernel-modules.html</link>
      <description>
        <![CDATA[<p>调试内核模块和调试 Linux 程序本身差不多，都可以使用 gdb + qemu 来调试，也可以使用 Linux 内核自带的 kgdb 工具调试。不过因为内核模块是动态加载的，需要 <code>add-symbol-file</code>]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 18 Mar 2022 23:31:14 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>调试内核模块和调试 Linux 程序本身差不多，都可以使用 gdb + qemu 来调试，也可以使用 Linux 内核自带的 kgdb 工具调试。不过因为内核模块是动态加载的，需要 <code>add-symbol-file</code> 来指定模块的加载地址。</p><h2 id="使用-kgdb-调试"><a href="#使用-kgdb-调试" class="headerlink" title="使用 kgdb 调试"></a>使用 kgdb 调试</h2><p>需要使用到串口进行通信。可以在 qemu 启动系统的时候加入参数 <code>-serial tcp:localhost:4321,server,nowait</code>，将内核的串口设备映射到本机 tcp 连接上，然后编辑 <code>/etc/default/grub</code> 在系统启动参数里加入 <code>kgdboc=ttyS1,115200 nokalsr</code> ，并运行 <code>sudo update-grub</code> 之后重启虚拟机，启动 gdb 使用 <code>target remote localhost:4321</code> 连接到串口调试端口，之后，在虚拟机操作系统里，运行 <code>echo g &gt; /proc/sysrq-trigger</code> 触发断点，将控制权交给 gdb，使用 gdb 设置好断点后按 <code>c</code> 继续运行。后续想要将控制权交给 gdb 都需要使用上面的 echo 语句触发断点。需要注意的是，如果是给内核模块打断点，需要先通过 <code>cat /sys/module/&lt;模块名&gt;/sections/.text</code> 查看模块被加载到哪里了，然后再使用 <code>add-symbol-file /path/to/foo.ko &lt;输出的地址&gt;</code> 加载调试符号。另外，给内核模块打断点需要使用 <code>hbreak</code> 打硬件断点，否则 gdb 会提示无法插入断点。另外退出 gdb 之前记得删除所有断点，不然会导致客户机在下一次触发断点的时候卡死。</p><h2 id="使用-qemu-gdb-server-调试"><a href="#使用-qemu-gdb-server-调试" class="headerlink" title="使用 qemu gdb server 调试"></a>使用 qemu gdb server 调试</h2><p>qemu 本身也支持 gdb 连接到 qemu 程序对客户机进行调试，这种方法只需要在 qemu 启动的时候加入参数 <code>-gdb tcp::1234</code> 就可以了，后面直接启动 gdb 用 <code>target remote localhost:1234</code> 连接到 qemu 调试服务器，这时候按 Ctrl+C 可以直接取得控制权，进行打断点操作。如果要调试内核模块，需要在编译内核的时候以下的配置项都是关闭的（可以直接修改 <code>.config</code> 文件）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CONFIG_DEBUG_RODATA=n</span><br><span class="line">CONFIG_DEBUG_RODATA_TEST=n</span><br><span class="line">CONFIG_DEBUG_SET_MODULE_RONX=n</span><br></pre></td></tr></table></figure><p>然后参照上面的方法加载调试符号即可，可以直接使用 <code>break</code> 来设置断点，不需要用硬件断点。</p><p>如果需要访问内核模块的全局变量，还需要加载其他段的地址，可以用这个脚本快速获得所有地址的加载命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="built_in">cd</span> /sys/module/<span class="variable">$1</span>/sections</span><br><span class="line"><span class="built_in">echo</span> -n add-symbol-file<span class="variable">$2</span> `/bin/cat .text`</span><br><span class="line"><span class="keyword">for</span> section <span class="keyword">in</span> .[a-z]* *; <span class="keyword">do</span></span><br><span class="line">    <span class="keyword">if</span> [ <span class="variable">$section</span> != <span class="string">&quot;.text&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot; \&quot;</span></span><br><span class="line"><span class="string">echo -n &quot;</span> -s<span class="string">&quot;<span class="variable">$section</span> `/bin/cat <span class="variable">$section</span>`</span></span><br><span class="line"><span class="string">    fi</span></span><br><span class="line"><span class="string">done</span></span><br><span class="line"><span class="string">echo</span></span><br></pre></td></tr></table></figure><p>脚本第一个参数是模块名，第二个参数是模块 <code>.ko</code> 文件地址。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>托福备考经验分享</title>
      <link>https://blog.howardlau.me/university/prepare-for-toefl.html</link>
      <description>
        <![CDATA[<h2 id="tl-dr"><a href="#tl-dr" class="headerlink" title="tl; dr"></a>tl; dr</h2><ul>
<li>纯自学托福 108(R29+L30+S23+W26)]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <pubDate>Fri, 10 Dec 2021 09:27:56 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="tl-dr"><a href="#tl-dr" class="headerlink" title="tl; dr"></a>tl; dr</h2><ul><li>纯自学托福 108(R29+L30+S23+W26) ，大约零散准备三周，做了十套阅读、听力，两套写作，五套口语，写作和口语没有找人批改</li><li>本人四六级裸考分别 647、561，对照你的水平来预估准备时间</li><li>这只是一个语言考试，有应试技巧，有策略地分配你的备考时间</li><li>尽可能搜集免费的资料</li><li>上<a href="https://toefl.kmf.com/">考满分</a>做 TPO，务必限时</li><li>阅读或听力有加试题</li><li>听力是重中之重，听力能力会很大程度上影响你的成绩</li><li>会听，更要会记</li><li>口语写作要提前准备模板、例子和理由，但不能生搬硬套</li><li>有条件的要找人批改口语写作</li><li>写作避免犯低级错误，尤其是定冠词</li><li>提升打字速度</li><li>合理利用 TPO，但也别迷信</li></ul><h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>2021 年 10 月 30 日早上，我冒着蒙蒙细雨赶到考场考了人生中第一次托福，考到了 108(R29+L30+S23+W26) 的分数。虽然比不上 110+ 的大神，但这也是一个不错的成绩了。我并不是连续一段时间高强度集中准备的，而是零零散散地去准备，中途会有几天完全没有准备，加起来可能大约花了 3 周，如果按一天 2 小时左右的备考时间去计算。全过程我都没有花钱报班或者请老师辅导，我是自己搜集资料还有请教身边考过托福的同学的经验来准备的。如果你没有时间或者不想花钱，也想 DIY 准备托福考试，希望我的经验可以帮助到你。</p><p>先说说我个人的英语水平，我之前参加过的考试就是 2017 年 12 月的大学四级和 2018 年中的大学六级，分别是 647 分和 561 分，都是裸考参加，没有考口试。18 年上完英语课之后几乎就没有怎么提升过英语能力了，平时上课和做作业也都是中文环境，但是有阅读英语资料的习惯，所以我自己是认为除了阅读之外，考托福时候的英语水平可能还不如之前的水平。如果你自认为水平比我高，那你可以少花一点时间准备，否则你可能需要多花一点时间。</p><p>考试都是有应试技巧的。总体上讲，托福是一个测试你使用英语进行<strong>学术交流活动</strong>的<strong>语言能力测试</strong>。学术交流活动的意思是，它的主题会更偏向于学术内容，而且用语更正式，而不是生活化的内容或用语。语言能力测试的意思是，它对你的其他知识水平几乎没有要求，凡是出现学术名词，它一定会有简单易懂的解释来帮助你理解。而在口语写作部分，也只关注你输出的东西是否达到了语言和逻辑上面的要求，对于真实性没有要求，也就是可以“我有一个朋友”。</p><p>另外，建议先<a href="https://toefl.neea.cn/">报名（线下考试）</a>，然后根据你的考试时间去安排备考。花了钱才更有动力 :)</p><h2 id="考试形式与流程"><a href="#考试形式与流程" class="headerlink" title="考试形式与流程"></a>考试形式与流程</h2><p>从考试形式来说，托福 iBT 考试是<strong>机考</strong>。分为阅读、听力、口语、写作四个部分（Section），每个部分独立计时，而且一个部分完成之后就没有机会返回检查或修改了。考试的具体流程是：</p><h3 id="阅读"><a href="#阅读" class="headerlink" title="阅读"></a>阅读</h3><p>3 篇约 600 词的文章，每篇文章有 10 道选择题，有八道选择题是四选一的单选，可能考察文章里某个单词的意思，也有考察你对文章某一段落的理解等。有一道选择题是给你一个句子和文章中四个位置，需要你选择四个位置中最适合插入这个句子的一个。最后一道选择题是六个选项选三个，每个选项都是对文章某一段落的总结句，有些选项可能和文章有出入或者漏掉了重要的信息。</p><p>时限 60 分钟，这 60 分钟内你可以做上面的全部 30 道选择题，可以提前结束。除了六选三，做题的时候文章会一直显示在右边，六选三需要你按按钮切换。</p><h3 id="听力"><a href="#听力" class="headerlink" title="听力"></a>听力</h3><p>听力会进一步细分为两个部分，第一个部分有 1 段约三分钟的对话和 2 篇约五分钟的课堂录音，第二个部分有 1 段约三分钟的对话和 1 篇约五分钟的课堂录音。每段对话&#x2F;录音播放完成后，有 5~6 个问题，以四选一选择题为主，偶尔会有四选二甚至排序题，题型多样。听力播放的时候需要做笔记，并且播放完之后才会显示问题。题目会显示在屏幕上，也会念一遍，每个问题作答并确认之后就不能修改了，会直接显示下一道题。</p><p>在播放录音的时候计时会暂停，也就是播录音的时间不算作答时间。第一部分所有问题作答时间加起来不能超过 10 分钟，第二部分所有问题作答时间加起来不能超过 6.5 分钟。听力之后是一段 10 分钟的休息时间，你可以离开座位去洗手间还有吃东西。时间到之后并不会自动进入考试，而是需要监考员再次解锁考试软件。</p><h3 id="关于加试"><a href="#关于加试" class="headerlink" title="关于加试"></a>关于加试</h3><p>阅读和听力有可能会遇到加试题，是 ETS 用来测试新题的难度的，不算分，但是<strong>你不知道哪些是加试题</strong>。如果是在阅读部分加试，那么阅读就是 4 篇文章 40 题，限时 72 分钟。如果是在听力部分加试，那么第二部分会多一篇讲座，同样限时 10 分钟。</p><h3 id="口语"><a href="#口语" class="headerlink" title="口语"></a>口语</h3><p>口语一共四个问题（Task 1~4），其中 Task 1 为独立任务，一般是提问你对某个观点是否同意并说出理由，Task 2~4 为综合任务，都是需要你阅读一段材料（Task 4 没有）、听一段录音，然后根据阅读材料和录音作答。阅读材料在听力开始之后会消失，所以阅读材料的时候也要做笔记。Task 1 题目显示后有 15 秒准备时间，然后有 45 秒进行作答。</p><p>Task 2~3 阅读材料的时间约 50 秒，听力播完后，Task 2~3 有 30 秒准备时间，Task 4 有 20 秒准备时间，之后都有 60 秒进行作答。</p><h3 id="写作"><a href="#写作" class="headerlink" title="写作"></a>写作</h3><p>写作一共要写两篇。其中第一篇是综合写作，会先让你阅读一篇约 300 词的文章，然后播放一段录音，然后写一篇约 250 词的文章，在写作的时候文章会一直显示在旁边，所以可以不用做笔记。第二篇是独立写作，和独立口语比较像，需要写一篇不少于 300 词的文章发表你对某个观点的看法和理由。两篇作文写作时间都是 30 分钟，综合写作完成之后就不可以修改了，会直接跳转到独立写作。综合写作阅读文章的两分钟和听力的时间不算入作答时间。</p><h3 id="考试完成后"><a href="#考试完成后" class="headerlink" title="考试完成后"></a>考试完成后</h3><p>考完之后会显示阅读和听力的分数，右上角有两个选项，<strong>问你是否提交这次成绩</strong>，别选错了。如果不小心点了取消，也有后悔药，就是要多送点钱给 ETS 来恢复你的成绩。如果是线下考试，考完六天后就可以查成绩了。</p><p>如果你觉得考得不好，要再考一次，要间隔 7 天报名，也就是你最早也只能预约下周的。</p><h2 id="考试内容和应试技巧"><a href="#考试内容和应试技巧" class="headerlink" title="考试内容和应试技巧"></a>考试内容和应试技巧</h2><p>上面是考试的形式和流程，下面针对各个部分具体考试内容谈谈我的看法和应试技巧。</p><h3 id="输入部分"><a href="#输入部分" class="headerlink" title="输入部分"></a>输入部分</h3><p>输入部分的阅读和听力相对容易，全部都是选择题，不需要打字。</p><p>这部分准备起来比较容易，直接上网站做题，做完题就知道自己几斤几两了。但短时间内很难有大幅提升，基本是要你有比较好的平时积累，时间花太多的话性价比不高，按照自己的水平做题保持手感就可以了。</p><p>我个人考前大概做了 10 套 TPO，阅读和听力没有连着做。</p><h4 id="阅读-1"><a href="#阅读-1" class="headerlink" title="阅读"></a>阅读</h4><p>这个部分相信大家都熟能生巧了，托福的阅读大概会涉及天文、地理、生物、文化、历史等领域，如果你不熟悉某个领域，不要紧，文章一定会用通俗语言解释学术名词的。反过来讲，也没必要去特地准备背景知识，对你做题帮助不大。基于文章内容的题，按照你以前英语考试的习惯去做即可。如果题目有标注段落，那么只需要关注段落的内容就可以了。对于词汇题，我个人的建议是直接根据你背单词记的那个意思选，如果将选项放到文章里容易受到干扰项的影响。对于句子插入题，可以留意句子本身和上下文的<strong>连词</strong>和<strong>代词</strong>。比如你看到 Consequently 这个词，那么大概率是要插到最后面的。如果你看到这个句子里有代词，可以分析一下这个代词指代的是什么，可以排除一些空。对于六选三，个人建议用排除法，常见的错误有错误的推论，比如作者只是说可能，而选项则直接肯定了；因果倒置；细节出入等。</p><h4 id="听力-1"><a href="#听力-1" class="headerlink" title="听力"></a>听力</h4><p><strong>听力能力是托福考试的重中之重</strong>，从考试形式可以看出，<strong>四个部分有三个部分需要你听听力</strong>。所以<strong>如果你时间紧张，优先提升听力能力</strong>。如果你同时在背单词，<strong>务必打开背单词软件的发音功能</strong>，至少要对某个发音对应到什么单词有个大概印象。不用背单词软件的话，自己按照音标<strong>念出声音</strong>。</p><p>除了听懂，你还需要提升<strong>做笔记</strong>的能力。手写速度肯定跟不上录音的速度，所以你肯定没有办法将听到的所有东西记录下来。做笔记分为两个重点：<strong>记什么</strong>和<strong>怎么记</strong>。</p><p>首先我们需要解决<strong>记什么</strong>的问题，一段听力只会提 5~6 个问题，但是有一些题对应到原文可能只有一句话，一旦漏听就没办法做对，所以需要关注听力的<strong>每一句话</strong>。我个人的建议是，按照<strong>名词、动词、形容词</strong>的优先级顺序去记，能记多少记多少，如果涉及到<strong>逻辑</strong>，比如因果关系，也要记下关系和对应的事物。如果听力中出现<strong>语调的变化或者讨论的话题发生改变</strong>，可能是重点，有可能出题，需要打起精神记下接下来的内容。</p><p>然后需要解决<strong>怎么记</strong>的问题，笔记一定要<strong>快</strong>，什么快写什么，缩写快就写缩写，因果关系可以划→，也可以写“∵”和“∴”，如果你觉得写汉字快，也可以<strong>写汉字</strong>。笔记能自己看懂就行，但顺序一定不能乱。如果写错了，不要回头改，会浪费时间。如果你感觉来不及写了，不要写完，直接放弃，开始记当前听到的内容。<strong>不要为了记笔记漏听内容</strong>。如果你在线下考，用的是纸笔，有一个小技巧：用两条短边的中点的连线将纸分为两个小窄条，对折或者画一条直线都可以，然后先用左半边记，左半边用完后再用右半边，这样换行的时候能更快。</p><p>而关于听力内容本身，对话通常是<strong>校园生活场景</strong>，比如学生和教授讨论学术问题，或者学生需要和图书馆、住宿等沟通解决问题。这种一般需要关注一方<strong>遇到了哪些问题</strong>，另一方是<strong>给了什么建议、答复或解决办法</strong>，一方对另一方的回答<strong>态度是怎样的</strong>。</p><p>而课堂录音一般是教授解释某个学术名词，偶尔会穿插<strong>学生问答</strong>。涉及的领域和阅读基本一样，地理、天文、生物、文化、历史、艺术等。需要注意的是，听力的开头有时候只是引子，可能会话锋一转谈论其他话题。如果是科学类的，需要关注<strong>概念的解释、举例子、多方观点</strong>等。如果是文化、历史、艺术等，注意<strong>记录时间顺序（可以不记具体时间）、某个事件的背景、造成的结果、多方观点</strong>等**。**</p><p>具体到问题，每篇听力的第一题一定是问这个听力<strong>主要</strong>讲了什么，一定不会问什么事在哪年发生这种题。然后就是对细节的提问。有时候会重播听力中某个人的某句话，通常是有隐含意思（or 阴阳怪气），然后问你他为什么要说这句话，也就是问话外之音。</p><p>练习的时候，可以尝试开 1.2 倍速来练习，这样到实际考试的时候可以有更多的反应时间。</p><h3 id="输出部分"><a href="#输出部分" class="headerlink" title="输出部分"></a>输出部分</h3><p>阅读和听力考察完输入能力之后，就开始考察你的输出能力了。输出能力的重点就是，<strong>如何通过你的答案在有限的时间内展现你的语言能力</strong>。</p><p>这部分准备起来会比较困难，因为<strong>没有反馈</strong>，如果你对自己的水平比较自信，那么自己琢磨琢磨，看看范文好在哪，学一学。如果你水平不高，那么还是建议找老师去给你批改、反馈，帮你提升。但是口语写作是可以短时间通过“死记硬背”来获得不小的提升的，性价比更高。</p><p>输出部分有会有机器评分和人工评分，机器评分是按照一定规则去检查一些基础问题，例如口语是不是卡壳，写作是不是有拼错的单词或者语法错误等，人工评分则更关注内容一点（我猜的）。所以，输出部分优先保证准确度，然后再想办法提升内容的水平。</p><p>我个人的话口语写作都是没有找老师批改过的，考前也只练习过两篇写作。输出部分的话，精心去研究自己的短处，怎么将一篇文章、口语回答打磨好，比盲目练习很多篇更好。</p><h4 id="口语-1"><a href="#口语-1" class="headerlink" title="口语"></a>口语</h4><p>口语一般是中国人最不擅长的，所以我也花了比较长时间去准备。输出的部分都可以准备一些<strong>模板</strong>来凑时间、凑字数。</p><h5 id="独立口语"><a href="#独立口语" class="headerlink" title="独立口语"></a>独立口语</h5><p>独立口语的话题没有那么学术，更偏向于生活常识。例如，问你你觉得电子书是否会取代纸质书，或者是你喜欢去大超市还是小商店这种。偶尔会问一些比较高深的，你用中文可能都不怎么会回答，例如你觉得电视上的商业广告是否会对儿童造成不良影响。独立口语是先在屏幕上显示问题，这时候你就可以提前开始想想回答，实际上准备时间比 15 秒多一点。题目一般是：Do you agree or disagree? [某个观点] State your opinion and explain why?回答思路就是开篇表明态度，然后说理由，理由里最好有例子。理由和例子具体是什么都不要紧，和你的态度一致就行。<strong>不需要面面俱到</strong>，只需要说支持你观点的内容。我用的模板如下</p><blockquote><p>Personally speaking, I agree&#x2F;disagree that [把题目观点说一次]. I feel this way for several reasons. First, [理由一]. For example, [例子一]. Second, [理由二]. For instance, [例子二].</p></blockquote><p>当然考试的时候由于紧张，可能没有办法完全按照模板来，那么理由二可以不举例子，瞎扯淡凑够时间就可以了。</p><p>对于独立口语的部分的准备，我是先准备了几个理由，比如对身体好，节省时间等，然后将 TPO1~54 所有独立口语题看了一遍，针对每一个问题，我都会看看已经有的理由能不能套上去，不能就再想两个加到理由集合里面。基本上 TPO 涵盖的范围足够广了。准备好所有理由之后，针对每个理由再编几个例子。将理由和例子烂熟于心，考试的时候看情况是否能直接套。理由和例子最好不要背网上的，自己想的更好，答题的时候也不会有死记硬背的感觉。</p><h5 id="综合口语"><a href="#综合口语" class="headerlink" title="综合口语"></a>综合口语</h5><p>综合口语难度相对来说低一点，毕竟不需要自己想内容了，独立口语说得不好也没有关系，也不要在后面的题去想了，专心在综合口语部分拿多一点分。不同 Task 侧重点也不同，技巧也不同，下面分开讲。</p><p>Task 2 的阅读材料通常是一个公告或者建议信等，一般是对学校设施、政策等变动的通知或者提议，比如要翻修体育馆或者新建建筑之类的，然后材料一般会给出两个理由。这时候你可以在纸上记下变动和两个理由。然后录音的对话会有一个人表达他对这个阅读材料的看法，可能是赞成、反对或者两者兼之。他会分别点评两个理由并给出自己的看法，需要尽可能详细记他的看法，给分点重点在听力。</p><p>回答的时候，可以直接套模板。准备时间的时候，尽可能快的将整个内容过一遍。</p><blockquote><p>The letter&#x2F;announcement&#x2F;… suggests that [一句话总结变动] for several reasons. First, [一句话总结材料理由一]. Second, [一句话总结材料理由二]. The man&#x2F;woman agrees&#x2F;disagrees&#x2F;holds a mixed opinion this. First, [听力对理由一的看法]. Second, [听力对理由二的看法].</p></blockquote><p>如果还有时间，可以加个总结句，不加也没有关系，我考试的时候讲完还剩下十秒什么也没说。</p><p>Task 3 的阅读材料通常是对一个学术名词的解释，记笔记的时候记下来名词和一句话解释。听力中讲话的人会举例子去进一步解释这个学术名词。有时候举一个，有时候举两个。给分重点还是在听力的例子部分。回答的时候也是套模板。</p><blockquote><p>[学术名词] means [名词的一句话解释]. The lecturer elaborates on this by 1&#x2F;2 example(s). First, [例子一讲一遍]. Second, [例子二讲一遍]</p></blockquote><p>注意按照例子数量去分配作答时间。</p><p>Task 4 没有阅读材料，但内容和 Task 3 差不多，也是对一个学术名词先解释然后举例子。可以认为是把阅读材料放到听力里念一遍的 Task 3。给分重点同样在例子，模板如下。</p><blockquote><p>The lecturer explains that [名词] means [解释]. He&#x2F;She elaborates on this by 1&#x2F;2 example(s). First, [例子一讲一遍]. Second, [例子二讲一遍].</p></blockquote><p>口语都可以不需要总结句收尾。</p><h4 id="写作-1"><a href="#写作-1" class="headerlink" title="写作"></a>写作</h4><p>写作部分由于是使用电脑打字，打字速度不高的要练习一下。时间比较紧张，建议直接开写，毕竟电脑修改很方便。</p><p>写作部分的准备，在于精，不在于多。写完后一定要对照写的好的文章看自己的文章有什么问题，然后想办法修正问题。然后再看看文章的表达有什么提升的地方，学习范文的表达方法。</p><p>英语写作<strong>注重逻辑、简洁、连贯</strong>，文章里最好多点使用<strong>连词和过渡短语</strong>来体现你的逻辑，也确保了文章的连贯性。</p><h5 id="综合写作"><a href="#综合写作" class="headerlink" title="综合写作"></a>综合写作</h5><p>综合写作和综合口语一样，不需要自己想内容，给分重点在听力。阅读材料一般会提出一个观点或者方案，然后给出三个理由，听力则是一一反驳这三个理由，反驳的时候会举例子，也要记下来。一定要注意表达的多样化，同样的单词不要重复出现多次，尽可能用不同的同义词。<strong>在短短的 300 词文章里密集地展现你的语言能力</strong>。</p><p>写作就不给具体模板了，大概介绍下思路。</p><blockquote><p>开头第一段：一句话总结文章观点，然后使用过渡句提出听力里的人反对这篇文章</p><p>中间段，一个理由一段：开头先一句话总结文章的理由，然后使用过渡句提出听力的对这个理由的看法，然后写几句例子</p><p>综合写作不需要结尾段。</p></blockquote><h5 id="独立写作"><a href="#独立写作" class="headerlink" title="独立写作"></a>独立写作</h5><p>独立写作则和独立口语差不多，一般是给出两个观点，问你赞成哪一个，有时候是三个。准备方法也和口语差不多，<strong>想理由和例子备用</strong>即可。可以适当使用倒装句、强调句、虚拟语气、独立主格等高级句式，但是<strong>如果你不会用就别用了</strong>，用错了会弄巧成拙。同理，如果一个高级的单词或者词组你<strong>把握不准能不能用，那就别用</strong>。独立写作的<strong>表达尽可能多样化</strong>，不要整篇都是 sb. do sth. 的简单句子，也不要整得满篇都是长难句。<strong>考虑一下读你的文章的考官会是什么感受</strong>。尽可能句式多样化，同样一个意思尽可能用不同单词表达。<strong>在短短的 300 词文章里密集地展现你的语言能力</strong>。你的<strong>态度</strong>是什么<strong>不重要</strong>，文章也<strong>不需要面面俱到，文章逻辑自洽即可</strong>。</p><blockquote><p> 开头第一段：扯一两句时代社会大背景，过渡到反方观点，然后话锋一转，表达自己的观点</p><p>中间段：按照自己水平来，写不了长的就只写两段，和独立口语一样，只需要对自己的态度发表理由加例子，如果能扯淡，可以多加一段让步段，也就是点评一下反方观点，指出一个“虽然，但是”</p><p>结尾段：使用模板句收一下尾，重复一下你的观点</p></blockquote><p>关于独立写作，有的人说越长越好，我持保留意见，毕竟写作时间只有 30 分钟，写得越长就意味着写得着急，反而会增加犯错的概率。一篇短一点但是几乎没有错误的文章比一篇长一点但是错误比较多的文章要好。我考的时候只写了 340 词。更详细的模板等可以自己上网搜索相关资料，我在此就不重复了。</p><p>有时间的话，一定要检查文章有无低级错误，例如单词拼写、语法、<strong>定冠词</strong>（a, the）</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>做题网站：<a href="https://toefl.kmf.com/">https://toefl.kmf.com</a>，上面也有作文和口语的机改功能，2元&#x2F;篇，比较鸡肋</p><p>口语写作也可以上 B 站和油管等用托福、TOEFL 等关键词找一些视频参考，这里就不列举了。</p><p>P.S.：关于背不背单词的问题，我建议是背一背。我用的单词软件是扇贝。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>关于 Monorepo 的一点想法</title>
      <link>https://blog.howardlau.me/programming/thoughts-on-monorepo.html</link>
      <description>
        <![CDATA[<p>Google、Facebook 的单一代码仓库已闻名遐迩，不过在没有真正接触过实际的 Monorepo 项目之前，可能很难想象 Monorepo 是怎么工作的。我在学校里的时候也没太理解，直到去了微信实习才看到了真正的 Monorepo，当然他们不是全 BG]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sun, 08 Aug 2021 05:27:08 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>Google、Facebook 的单一代码仓库已闻名遐迩，不过在没有真正接触过实际的 Monorepo 项目之前，可能很难想象 Monorepo 是怎么工作的。我在学校里的时候也没太理解，直到去了微信实习才看到了真正的 Monorepo，当然他们不是全 BG 共享代码，实际上单个 repo 的规模要比 Google 那种全公司共享要小，使用也不是特殊的客户端，就是普通的 git，也没有使用 git-vfs 之类的，而是把代码全拉下来开发。不过，虽然只是 Google 真正的 Monorepo 的模仿，也能管中窥豹，大致理解 Monorepo 的特点。</p><p>最大的感受是，量变引起质变，当代码仓库量级大到一定程度时，工作的流程和平时的小仓库相比就完全不同了。</p><p>Monorepo 的优点是：</p><ul><li>透明的依赖管理，上游模块很容易知道被哪些下游模块依赖，修改后能一次性全测试</li><li>唯一的依赖版本，永远都是使用最新的依赖版本，避免了菱形依赖等麻烦的情况</li><li>代码共享，所有开发者都能看到所有的代码，方便搜索学习以及重用</li><li>方便的使用依赖，开发者只需要加入依赖的路径到自己的编译文件中就可以使用其他人的依赖，不需要另外引入包管理之类的</li><li>统一的构建系统，开发者无需操心怎么编译，一条命令就可以把二进制构建出来</li><li>方便进行大规模的重构</li></ul><p>缺点也很明显：</p><ul><li>管理成本高，Monorepo 需要大量开发自研工具链才能使用</li><li>引入外部依赖麻烦，这里的外部依赖是指开源项目等，一般需要适配好构建系统</li></ul><h2 id="代码版本管理系统"><a href="#代码版本管理系统" class="headerlink" title="代码版本管理系统"></a>代码版本管理系统</h2><p>目前最主流的代码版本管理系统 Git 对于大规模的代码仓库支持并不算太好，尽管可以用，但速度上比较慢。所以，Google 开发了 Piper，Facebook 开发了 Mecurial，来解决规模变大之后的效率问题。</p><h2 id="云端工作"><a href="#云端工作" class="headerlink" title="云端工作"></a>云端工作</h2><p>Monorepo 存储了大量代码，容量可能数以 TB 计，这种量级的代码哪怕是在内网拉取也得耗费数小时，而且对于笔记本之类开发者常用的设备来说，还是太大了。所以，Monorepo 基本上是不可能全部代码都下载到本地的。也就是说，平时的开发都要依赖于开发服务器，在服务器上写代码，在服务器上提交代码。</p><p>当然了，如果大家都是挂载到共享的文件系统工作，那么肯定会发生大量冲突，所以云端开发也并不是这个意思。一般是每个开发者有各自的云端虚拟机或者本地工作站，使用专用的客户端来访问代码。一般来说，这种客户端需要支持按需下载文件的功能，避免占用本地过多资源。当然，如果 Monorepo 的体积比较小，比如几十 GB，现代的服务器或者工作站都还是可以直接全部拉取下来的。</p><h2 id="编译系统的支持"><a href="#编译系统的支持" class="headerlink" title="编译系统的支持"></a>编译系统的支持</h2><p>Monorepo 的编译一般是使用单独的构建系统由专门的编译服务器集群负责构建，一来可以进行分布式编译，加速构建过程；二来可以全团队共享编译缓存，进一步加速。虽然个人开发服务器或者工作站也可以编译，但是基本上编译的时候什么都做不了了，而且还很慢。</p><p>Google 有 Blaze（开源版叫 Bazel），Facebook 有 Buck，微信有 Blade（ex-Googler 写的类似 Bazel 的工具），他们都没有用现成的 CMake 或者 Make 等，而是自己又开发了一套编译系统。针对 Monorepo 使用的编译系统一般都是用类似 Python 的 DSL（比如 Bazel 用了 Starlark），支持细粒度的 target 管理，能更好地利用编译缓存。同时，为了保证多次构建的产物完全一致，编译系统一般会使用“气密式环境”构建，也就是所有输入要通过哈希值确定版本，并且编译工具链也通过哈希确定版本，构建过程运行在沙盒环境里，例如使用容器或者虚拟机，这样就可以保证构建产物的一致性了。</p><p>当然，如果什么都是自己写的话，Monorepo 的构建系统就很好用，然而一旦需要引入没有使用这套构建系统的第三方项目，就需要手动编写构建文件适配，如果项目构建比较复杂，那么适配过程将很痛苦。同时，内部的项目需要开源的话，也需要编写例如 CMake 等其他人常用的构建文件。</p><p>编译系统是以代码仓库为基准进行编译的，在代码还没有合入代码仓库之前怎么编译呢？一种解决办法就是将本地修改过的文件打包发送到编译服务器，编译系统在编译前将修改临时应用到代码主干，再进行编译。</p><h2 id="智能提示"><a href="#智能提示" class="headerlink" title="智能提示"></a>智能提示</h2><p>Monorepo 因为体积过大，一些 IDE 或者编辑器插件的智能提示功能很可能分析不来，直接歇菜。一般来说可以用云 IDE 的方法，或者在编译服务器构建好索引下载下来，或者生成 Compilation Database，只让插件分析使用到的依赖，避免全库扫描。</p><p>Monorepo 还有一个重要的优势就是代码透明，这样开发者可以直接在整个代码库中搜索，比如一个 API，可以搜索它的实现，也可以搜索其他人是如何使用的，直接复制粘贴，提高编码效率。显然，基于文本的搜索准确率远远不够，而基于符号分析的搜索显然也在个人开发服务器上跑不动，所以，使用 Monorepo，还需要一个好用的 Code Search。Google 和微信都有这么一个 Code Search，可以基于符号搜索，搜索结果中的每一个符号都可以点击并进行 Cross Reference。当然，Code Search 怎么做好是很有技术含量的，首先搜索的响应要及时，其次代码变化得很频繁，如何及时更新索引也是一个技术挑战。Google 选择了部分增量索引加定时全量索引以及暴力搜索相结合得方式，同时复用了老本行 Google Search 的技术栈，给开发者提供了优质的代码搜索体验。</p><h2 id="CI-CD"><a href="#CI-CD" class="headerlink" title="CI&#x2F;CD"></a>CI&#x2F;CD</h2><p>对于 Monorepo，大家都在一个分支上提交代码，传统的一个 commit 触发一次全量的 CI&#x2F;CD 肯定是不行的。所以，CI&#x2F;CD 也需要支持分子路径来运行，或者手动触发运行。这种功能可以参考 GitLab CI 里的 <code>path</code> 参数，可以在检测到对应路径的文件发生改变之后才运行对应的 CI。</p><h2 id="代码权限管理"><a href="#代码权限管理" class="headerlink" title="代码权限管理"></a>代码权限管理</h2><p>因为是 Monorepo，所以代码全部公开，也意味着每个人都有权查看修改，查看代码一般不是什么大问题，毕竟 Monorepo 的目的就是共享代码，但是如果每个人都能修改代码那就乱套了。Google 采用的办法是使用 OWNERS 文件，对于子目录而言，只有 OWNERS 文件里的成员才可以批准修改。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 Ray Tune 进行分布式调参</title>
      <link>https://blog.howardlau.me/programming/distributed-hyperparameter-tuning-with-ray-tune.html</link>
      <description>
        <![CDATA[<p>之前学习了<a href="https://howardlau.me/programming/pytorch-distributed-data-parallel.html">怎么使用 PyTorch]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 26 May 2021 03:01:08 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>之前学习了<a href="https://howardlau.me/programming/pytorch-distributed-data-parallel.html">怎么使用 PyTorch 进行分布式训练</a>，不过，手动计算参数启动进程还是比较麻烦的。Berkeley 的 RISELab 开发了 <a href="https://ray.io/">Ray</a> 这个框架来简化分布式计算程序的编写。Ray 有以下几个优点：</p><ul><li>支持在多种云平台一键部署集群</li><li>支持 Autoscaling，可以节省成本</li><li>支持多种深度学习框架，比如 PyTorch、TensorFlow</li></ul><p>通过 Ray 统一的编程 API，写好的程序既可以在本地集群跑，本地集群资源紧张的时候也可以直接放到云端跑，不需要改代码，集群管理和调度都不需要我们自己操心。同时，Ray 还提供了调参库，等模型迭代成熟之后可以直接使用 Ray Tune 进行超参搜索，不需要自己写调参脚本了。</p><h2 id="安装-Ray"><a href="#安装-Ray" class="headerlink" title="安装 Ray"></a>安装 Ray</h2><p>安装 Ray 很简单，只需要在每台服务器都执行 </p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install -U ray <span class="string">&quot;ray[default]&quot;</span></span><br></pre></td></tr></table></figure><h2 id="启动-Ray"><a href="#启动-Ray" class="headerlink" title="启动 Ray"></a>启动 Ray</h2><p>Ray 采用了中心化的管理模型，分为头节点（Head Node）和工作节点（Worker Node）。头节点负责资源调度、数据分发以及用户交互等，工作节点会主动连接到头节点接受任务并执行。所以，在启动不同节点的时候需要的命令不一样。</p><h3 id="启动头节点"><a href="#启动头节点" class="headerlink" title="启动头节点"></a>启动头节点</h3><p>首先，任选一台服务器作为头节点，然后执行命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ray start --<span class="built_in">head</span> --node-ip-address 10.0.0.1 --port=6379 --redis-password=123 --dashboard-host=0.0.0.0</span><br></pre></td></tr></table></figure><p>其中，<code>--head</code> 表示这是一个头节点，<code>--node-ip-address</code> 应该换成机器的真实 IP，不可以使用 <code>127.0.0.1</code>，接下来的两个参数则是指定了 Redis（用于数据交换的 KV 内存数据库）的启动选项，选择一个没有被占用的端口即可，密码可以自选。<code>--dashboard-host</code> 则是指定了 Ray 的监控面板应该监听哪个地址，默认监听的是 <code>127.0.0.1</code>，一般来说我们都不会在服务器上面开桌面，填 <code>0.0.0.0</code> 可以让我们用服务器的任何一个 IP 访问监控界面。</p><p>执行命令之后应该可以看到以下输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Local node IP: 10.0.0.1</span><br><span class="line">2021-05-26 11:04:21,084 INFO services.py:1267 -- View the Ray dashboard at http://0.0.0.0:8265</span><br><span class="line"></span><br><span class="line">--------------------</span><br><span class="line">Ray runtime started.</span><br><span class="line">--------------------</span><br><span class="line"></span><br><span class="line">Next steps</span><br><span class="line">  To connect to this Ray runtime from another node, run</span><br><span class="line">    ray start --address=<span class="string">&#x27;10.0.0.1:6379&#x27;</span> --redis-password=<span class="string">&#x27;123&#x27;</span></span><br><span class="line">  </span><br><span class="line">  Alternatively, use the following Python code:</span><br><span class="line">    import ray</span><br><span class="line">    ray.init(address=<span class="string">&#x27;auto&#x27;</span>, _redis_password=<span class="string">&#x27;123&#x27;</span>)</span><br><span class="line">  </span><br><span class="line">  If connection fails, check your firewall settings and network configuration.</span><br><span class="line">  </span><br><span class="line">  To terminate the Ray runtime, run</span><br><span class="line">    ray stop</span><br></pre></td></tr></table></figure><h3 id="启动工作节点"><a href="#启动工作节点" class="headerlink" title="启动工作节点"></a>启动工作节点</h3><p>然后，在除了头节点以外的服务器上，复制粘贴执行上面输出的命令。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ray start --address=<span class="string">&#x27;10.0.0.1:6379&#x27;</span> --redis-password=<span class="string">&#x27;123&#x27;</span></span><br></pre></td></tr></table></figure><p>成功应该可以看到以下输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Local node IP: 10.0.0.2</span><br><span class="line"></span><br><span class="line">--------------------</span><br><span class="line">Ray runtime started.</span><br><span class="line">--------------------</span><br><span class="line"></span><br><span class="line">To terminate the Ray runtime, run</span><br><span class="line">  ray stop</span><br></pre></td></tr></table></figure><p>说明工作节点成功连接到头节点了。</p><h3 id="查看监控界面"><a href="#查看监控界面" class="headerlink" title="查看监控界面"></a>查看监控界面</h3><p>头节点的输出中的</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Local node IP: 10.0.0.1</span><br><span class="line">2021-05-26 11:04:21,084 INFO services.py:1267 -- View the Ray dashboard at http://0.0.0.0:8265</span><br></pre></td></tr></table></figure><p>显示了监控界面的监听端口，这里是 8265，将 <code>0.0.0.0</code> 换成你的服务器 IP，然后打开网址，就能看到 Ray 的监控界面了。</p><p><a href="/programming/distributed-hyperparameter-tuning-with-ray-tune/raydashboard.png" data-fancybox="gallery" data-caption=""><img src="/programming/distributed-hyperparameter-tuning-with-ray-tune/raydashboard.png"></a></p><p>监控界面主要显示的是机器的资源情况还有任务执行情况。</p><h2 id="用-Ray-Tune-自动搜索超参训练模型"><a href="#用-Ray-Tune-自动搜索超参训练模型" class="headerlink" title="用 Ray Tune 自动搜索超参训练模型"></a>用 Ray Tune 自动搜索超参训练模型</h2><h3 id="使用-Trainable-包装训练过程"><a href="#使用-Trainable-包装训练过程" class="headerlink" title="使用 Trainable 包装训练过程"></a>使用 Trainable 包装训练过程</h3><p>Ray Tune 提供了几种方法将训练过程包装起来。首先是最基础的 <code>tune.Trainable</code>，这个类要求我们实现四个方法：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> ray <span class="keyword">import</span> tune</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MNISTTrainable</span>(tune.Trainable):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">setup</span>(<span class="params">self, config</span>):</span><br><span class="line">        <span class="keyword">pass</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">pass</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">save_checkpoint</span>(<span class="params">self, checkpoint_dir</span>):</span><br><span class="line">        <span class="keyword">pass</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">load_checkpoint</span>(<span class="params">self, checkpoint_path</span>):</span><br><span class="line">        <span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>首先，在模型训练开始前，Ray 会调用 <code>setup</code> 函数，这个函数里，我们需要做初始化的工作，比如加载模型和数据集以及设置优化器等，另外，训练的超参数也会从 <code>config</code> 中传进来，在这个函数读取并保存到对象属性里即可。需要注意的是，如果涉及到数据集的下载等，需要注意多进程文件读写的同步。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> ray.tune.examples.mnist_pytorch <span class="keyword">import</span> (train, test, get_data_loaders,</span><br><span class="line">                                             ConvNet)</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">setup</span>(<span class="params">self, config</span>):</span><br><span class="line">    use_cuda = config.get(<span class="string">&quot;use_gpu&quot;</span>) <span class="keyword">and</span> torch.cuda.is_available()</span><br><span class="line">    <span class="variable language_">self</span>.device = torch.device(<span class="string">&quot;cuda&quot;</span> <span class="keyword">if</span> use_cuda <span class="keyword">else</span> <span class="string">&quot;cpu&quot;</span>)</span><br><span class="line">    <span class="variable language_">self</span>.train_loader, <span class="variable language_">self</span>.test_loader = get_data_loaders()</span><br><span class="line">    <span class="variable language_">self</span>.model = ConvNet().to(<span class="variable language_">self</span>.device)</span><br><span class="line">    <span class="variable language_">self</span>.optimizer = optim.SGD(</span><br><span class="line">        <span class="variable language_">self</span>.model.parameters(),</span><br><span class="line">        lr=config.get(<span class="string">&quot;lr&quot;</span>, <span class="number">0.01</span>),</span><br><span class="line">        momentum=config.get(<span class="string">&quot;momentum&quot;</span>, <span class="number">0.9</span>))</span><br></pre></td></tr></table></figure><p>然后，Ray 会循环调用 <code>step</code> 函数，在 <code>step</code> 的最后，应该返回一个字典，用来告诉 Ray 这次迭代的指标：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">    train(<span class="variable language_">self</span>.model, <span class="variable language_">self</span>.optimizer, <span class="variable language_">self</span>.train_loader, device=<span class="variable language_">self</span>.device)</span><br><span class="line">    acc = test(<span class="variable language_">self</span>.model, <span class="variable language_">self</span>.test_loader, <span class="variable language_">self</span>.device)</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;mean_accuracy&quot;</span>: acc&#125;</span><br></pre></td></tr></table></figure><p>后面两个参数则是为了恢复训练而设计的，<code>save_checkpoint</code> 函数由 Ray 定期调用，在这个函数里，应该将训练的中间状态保存到 <code>checkpoint_dir</code> 指定的位置，并且返回保存后的路径。注意这里只能保存一个文件，保存的时候可以用 tuple 的方式将模型参数和优化器状态保存到文件里。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">save_checkpoint</span>(<span class="params">self, checkpoint_dir</span>):</span><br><span class="line">    checkpoint_path = os.path.join(checkpoint_dir, <span class="string">&quot;checkpoint.bin&quot;</span>)</span><br><span class="line">    torch.save((<span class="variable language_">self</span>.model.state_dict(), <span class="variable language_">self</span>.optimizer.state_dict()), checkpoint_path)</span><br><span class="line">    <span class="keyword">return</span> checkpoint_path</span><br></pre></td></tr></table></figure><p><code>load_checkpoint</code> 则顾名思义，只需要加载传进来的 <code>checkpoint_path</code> 文件即可 ：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">load_checkpoint</span>(<span class="params">self, checkpoint_path</span>):</span><br><span class="line">    model, optimizer = torch.load(checkpoint_path)</span><br><span class="line">    <span class="variable language_">self</span>.model.load_state_dict(model)</span><br><span class="line">    <span class="variable language_">self</span>.optimizer.load_state_dict(optimizer)</span><br></pre></td></tr></table></figure><h4 id="调用-Ray-Tune-进行训练"><a href="#调用-Ray-Tune-进行训练" class="headerlink" title="调用 Ray Tune 进行训练"></a>调用 Ray Tune 进行训练</h4><p>实现了四个函数之后，就可以将我们的类交给 Ray Tune 进行调度运行，运行的方法非常简单。主函数只需要初始化 Ray 环境，然后调用 <code>tune.run</code> 即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> ray.tune.schedulers <span class="keyword">import</span> ASHAScheduler</span><br><span class="line"><span class="keyword">from</span> ray.tune <span class="keyword">import</span> CLIReporter</span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># 初始化 Ray 集群</span></span><br><span class="line">    ray.init(address=<span class="string">&#x27;auto&#x27;</span>)</span><br><span class="line">    <span class="comment"># 开始搜索超参</span></span><br><span class="line">    sched = ASHAScheduler(metric=<span class="string">&quot;mean_accuracy&quot;</span>, mode=<span class="string">&quot;max&quot;</span>)</span><br><span class="line">    reporter = CLIReporter()</span><br><span class="line">    analysis = tune.run(MNISTTrainable,</span><br><span class="line">                      scheduler=sched,</span><br><span class="line">                      progress_reporter=reporter,</span><br><span class="line">                      stop=&#123;<span class="string">&quot;mean_accuracy&quot;</span>: <span class="number">0.99</span>,</span><br><span class="line">                            <span class="string">&quot;training_iteration&quot;</span>: <span class="number">100</span>&#125;,</span><br><span class="line">                      resources_per_trial=&#123;<span class="string">&quot;cpu&quot;</span>: <span class="number">2</span>, <span class="string">&quot;gpu&quot;</span>: <span class="number">1</span>&#125;,</span><br><span class="line">                      num_samples=<span class="number">128</span>,</span><br><span class="line">                      config=&#123;<span class="string">&quot;lr&quot;</span>: tune.uniform(<span class="number">0.001</span>, <span class="number">1.0</span>),</span><br><span class="line">                              <span class="string">&quot;momentum&quot;</span>: tune.uniform(<span class="number">0.1</span>, <span class="number">0.9</span>),</span><br><span class="line">                              <span class="string">&quot;use_gpu&quot;</span>: <span class="literal">True</span>&#125;)</span><br><span class="line">    <span class="comment"># 保存实验结果</span></span><br><span class="line">    analysis.results_df.to_csv(<span class="string">&quot;result.csv&quot;</span>)</span><br><span class="line">    <span class="comment"># 获得最佳参数</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Best config is:&quot;</span>, analysis.get_best_config(metric=<span class="string">&quot;mean_accuracy&quot;</span>, mode=<span class="string">&quot;max&quot;</span>))</span><br></pre></td></tr></table></figure><p>其中，<code>scheduler</code> 参数表示超参搜索的策略，有一些调度器可以提前终止表现一般的实验，节省资源给其他实验，同时也节省了搜索时间。具体有哪些调度器可以参考<a href="https://docs.ray.io/en/master/tune/api_docs/schedulers.html">官方文档</a>。</p><p>而 <code>stop</code> 指定了停止的标准，一般需要设置 <code>training_iteration</code>，表示最多调用几次 <code>step</code> 函数，其他可以按照自己的需要设置，设置的值需要是 <code>step</code> 会返回的。</p><p><code>progress_reporter</code> 指定了实验进度如何反馈给用户，这里用了默认的 <code>CLIReporter</code> ，如果用 Jupyter Notebook，可以用 <code>JupyterNotebookReporter</code>。</p><p><code>resources_per_trial</code> 则指定了分配多少资源给每个实验，默认是 1 个 CPU 和 0 个 GPU，如果不指定 GPU 数量的话，将无法使用 GPU 进行训练（<code>CUDA_VISIBLE_DEVICES</code> 会被设置成空值，即使服务器有 GPU 程序也会不认）。因为上面的示例没有写多卡代码，所以每个实验直接使用 1 个 GPU 即可。Ray 会尽可能调度多的实验一起跑，假如你有 8 个 GPU，那么会有 8 个实验同时进行。</p><p><code>num_samples</code> 表示执行多少次实验，每一次实验，会将 <code>config</code> 里指定为随机分布的参数采样一遍。如果 <code>config</code> 里指定了 <code>grid_search</code>，那么同样的参数会被重复 <code>num_samples</code> 次。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 会运行 9 个实验</span></span><br><span class="line">tune.run(trainable, num_samples=<span class="number">1</span>, config=&#123;</span><br><span class="line">    <span class="string">&quot;x&quot;</span>: tune.grid_search([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]),</span><br><span class="line">    <span class="string">&quot;y&quot;</span>: tune.grid_search([a, b, c])&#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 会运行 18 个实验</span></span><br><span class="line">tune.run(trainable, num_samples=<span class="number">2</span>, config=&#123;</span><br><span class="line">    <span class="string">&quot;x&quot;</span>: tune.grid_search([<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>]),</span><br><span class="line">    <span class="string">&quot;y&quot;</span>: tune.grid_search([a, b, c])&#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>编写完主函数之后，保存脚本，<strong>在任意一个启动了 Ray 的服务器上</strong>运行你的脚本</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python main.py</span><br></pre></td></tr></table></figure><p>应该可以看到输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2021-05-26 11:41:09,280 INFO worker.py:640 -- Connecting to existing Ray cluster at address: 10.0.0.1:6379</span><br></pre></td></tr></table></figure><p>说明成功连接到了 Ray 集群，训练过程中，应该可以看到类似的进度报告：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">== Status ==</span><br><span class="line">Memory usage on this node: 19.0/250.9 GiB</span><br><span class="line">Using AsyncHyperBand: num_stopped=3</span><br><span class="line">Bracket: Iter 64.000: None | Iter 16.000: None | Iter 4.000: 0.1129 | Iter 1.000: 0.1129</span><br><span class="line">Resources requested: 20.0/112 CPUs, 16.0/16 GPUs, 0.0/689.53 GiB heap, 0.0/299.5 GiB objects (0.0/4.0 accelerator_type:V100)</span><br><span class="line">Current best trial: 358bb_00006 with mean_accuracy=0.1174 and parameters=&#123;<span class="string">&#x27;lr&#x27;</span>: 0.00032010745937302484, <span class="string">&#x27;momentum&#x27;</span>: 0.3077716737703313&#125;</span><br><span class="line">Result logdir: /home/ray/ray_results/TorchTrainable_2021-05-26_11-41-09</span><br><span class="line">Number of trials: 32/32 (25 PENDING, 4 RUNNING, 3 TERMINATED)</span><br><span class="line">+----------------------------+------------+--------------------+-------------+------------+--------+--------+------------------+---------------+---------+---------------+</span><br><span class="line">| Trial name                 | status     | loc                |          lr |   momentum |    acc |   iter |   total <span class="keyword">time</span> (s) |   num_samples |   epoch |   batch_count |</span><br><span class="line">|----------------------------+------------+--------------------+-------------+------------+--------+--------+------------------+---------------+---------+---------------|</span><br><span class="line">| TorchTrainable_358bb_00000 | RUNNING    | 89.72.32.13:12113  | 0.00874452  |   0.652566 | 0.0829 |      6 |         50.2268  |         10000 |       6 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00003 | RUNNING    | 89.72.32.24:283613 | 0.00380314  |   0.755093 | 0.1128 |      6 |         49.1289  |         10000 |       6 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00004 | RUNNING    | 89.72.32.55:441696 | 0.000573206 |   0.531036 | 0.113  |      4 |         31.9356  |         10000 |       4 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00006 | RUNNING    | 89.72.32.18:30118  | 0.000320107 |   0.307772 | 0.1174 |      3 |         23.9964  |         10000 |       3 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00007 | PENDING    |                    | 0.000578684 |   0.826577 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00008 | PENDING    |                    | 0.000104167 |   0.841208 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00009 | PENDING    |                    | 0.00792254  |   0.419486 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00010 | PENDING    |                    | 0.00238698  |   0.496603 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00011 | PENDING    |                    | 0.00342996  |   0.611557 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00012 | PENDING    |                    | 0.000662736 |   0.379733 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00013 | PENDING    |                    | 0.000326747 |   0.835042 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00014 | PENDING    |                    | 0.00234961  |   0.761008 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00015 | PENDING    |                    | 0.00748315  |   0.234968 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00016 | PENDING    |                    | 0.000121585 |   0.806935 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00017 | PENDING    |                    | 0.00011588  |   0.578034 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00018 | PENDING    |                    | 0.00218129  |   0.663069 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00019 | PENDING    |                    | 0.000373483 |   0.726266 |        |        |                  |               |         |               |</span><br><span class="line">| TorchTrainable_358bb_00001 | TERMINATED |                    | 0.000819226 |   0.293562 | 0.0594 |      1 |          8.50461 |         10000 |       1 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00002 | TERMINATED |                    | 0.00035852  |   0.701368 | 0.0722 |      1 |          8.65457 |         10000 |       1 |            40 |</span><br><span class="line">| TorchTrainable_358bb_00005 | TERMINATED |                    | 0.00253235  |   0.834568 | 0.0767 |      1 |          8.26594 |         10000 |       1 |            40 |</span><br><span class="line">+----------------------------+------------+--------------------+-------------+------------+--------+--------+------------------+---------------+---------+---------------+</span><br><span class="line">... 12 more trials not shown (12 PENDING)</span><br></pre></td></tr></table></figure><h3 id="使用-TrainingOperator-包装训练过程"><a href="#使用-TrainingOperator-包装训练过程" class="headerlink" title="使用 TrainingOperator 包装训练过程"></a>使用 TrainingOperator 包装训练过程</h3><p>可以看到使用 <code>tune.Trainable</code> 的话，需要自己设置模型训练的初始化过程，需要自己封装多卡训练代码或者混合精度训练，如果没有特殊需求，可以使用 <code>ray.util.sgd.torch</code> 提供的 <code>TrainingOperator</code>。使用 <code>TrainingOperator</code> 可以不用自己操心 <code>checkpoint</code> 以及是否使用多卡等细节。</p><p>使用 <code>TrainingOperator</code> 和 <code>Trainable</code> 类似，需要我们重载几个方法。必须重载的只有 <code>setup</code> 方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MNISTrainingOperator</span>(<span class="title class_ inherited__">TrainingOperator</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">setup</span>(<span class="params">self, config</span>):</span><br><span class="line">        train_loader, test_loader = get_data_loaders()</span><br><span class="line">        model = ConvNet()</span><br><span class="line">        optimizer = optim.SGD(model.parameters(), lr=config.get(<span class="string">&quot;lr&quot;</span>, <span class="number">1e-4</span>), momentum=config.get(<span class="string">&quot;momentum&quot;</span>, <span class="number">0.9</span>))</span><br><span class="line">        criterion = torch.nn.NLLLoss()</span><br><span class="line">        <span class="variable language_">self</span>.model, <span class="variable language_">self</span>.optimizer, <span class="variable language_">self</span>.criterion = <span class="variable language_">self</span>.register(models=model, optimizers=optimizer, criterion=criterion)</span><br><span class="line">        <span class="variable language_">self</span>.register_data(train_loader=train_loader, validation_loader=test_loader)</span><br></pre></td></tr></table></figure><p>在 <code>setup</code> 方法中，只需要做两件事：注册模型、注册数据加载器。而且，不用自己操心 DataParallel 之类的包装，也不需要操心 checkpoint 加载保存。甚至连训练循环都不需要自己写，当然大部分情况下还是需要自己重载的。</p><p>注册模型使用的是 <code>self.register</code> 方法，这个函数负责注册模型和优化器等，以便 Ray 分发模型参数。另外，如果需要自定义学习率的 scheduler，参数名是 <code>schedulers</code>。如果使用分布式训练，还可以使用 <code>ddp_args={&quot;find_unused_parameters&quot;: True}</code> 来自定义 ddp 参数。如果使用混合精度训练，可以使用 <code>apex_args={&quot;opt_level&quot;: &quot;O2&quot;}</code> 自定义 APEX 参数。</p><p>参数都可以传入数组，假如训练 GAN，<code>models</code> 就可以传 <code>[generator, discriminator]</code>。不过，假如 <code>models</code> 是数组，就必须重载 <code>train_epoch</code> 函数。</p><p>注册后，Ray 会自动使用 APEX 或者 DistributedDataParallel 包装模型，不需要我们自己包装。</p><p><code>register_data</code> 负责注册数据加载器，同样的，Ray 会自动使用 DistributedRandomSampler 帮我们包装，不需要自己操心细节。</p><p>默认的情况下，<code>TrainingOperator</code> 的训练代码大概如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">train_epoch</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="keyword">for</span> idx, batch <span class="keyword">in</span> <span class="built_in">enumerate</span>(<span class="variable language_">self</span>.train_loader):</span><br><span class="line">        batch_info = &#123;<span class="string">&quot;batch_idx&quot;</span>: idx, <span class="string">&quot;global_step&quot;</span>: global_step&#125;</span><br><span class="line">        metrics = <span class="variable language_">self</span>.train_batch(batch, batch_info)</span><br><span class="line">        meter.update(metrics)</span><br><span class="line">    <span class="keyword">return</span> meter.summary()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">train_batch</span>(<span class="params">self, batch, batch_info</span>):</span><br><span class="line">    *features, target = batch</span><br><span class="line">    features = [feature.to(<span class="variable language_">self</span>.device) <span class="keyword">for</span> feature <span class="keyword">in</span> features]</span><br><span class="line">    target = target.to(<span class="variable language_">self</span>.device)</span><br><span class="line">    output = model(*features)</span><br><span class="line">    loss = <span class="variable language_">self</span>.criterion(output, target)</span><br><span class="line">    <span class="keyword">return</span> &#123;<span class="string">&quot;train_loss&quot;</span>: loss, <span class="string">&quot;num_samples&quot;</span>: target.size(<span class="number">0</span>)&#125;</span><br></pre></td></tr></table></figure><p>当然，实际代码里会有其他的工作，比如更新 scheduler 还有一些统计工作。eval 的代码也大同小异，只是函数名字分别叫 <code>validate</code> 和 <code>validate_batch</code>。</p><p>如果要重写函数，记得 <code>train_epoch</code> 需要返回这个 epoch 的统计量字典。如果只重写 <code>train_batch</code> ，需要注意返回的字典一定要有 <code>num_samples</code>，代表这个 batch 有多少样本。</p><p>定义好 <code>TrainingOperator</code> 之后，用 <code>Trainer</code> 将 <code>TrainingOperator</code> 包装起来：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">trainer = TorchTrainer(</span><br><span class="line">        training_operator_cls=MNISTrainingOperator,</span><br><span class="line">        num_workers=<span class="number">8</span>,</span><br><span class="line">        num_cpus_per_worker=<span class="number">2</span>,</span><br><span class="line">        use_gpu=<span class="literal">True</span>,</span><br><span class="line">        use_fp16=<span class="literal">True</span>,</span><br><span class="line">        config=&#123;<span class="string">&quot;batch_size&quot;</span>: <span class="number">16</span>&#125;</span><br><span class="line">    )</span><br></pre></td></tr></table></figure><p>其中，<code>num_workers</code> 代表使用多少个进程训练，其他参数 <code>num_cpus_per_worker</code> 则顾名思义。如果有多个 worker，模型和加载器就会自动被 DistributedDataParallel 和 DistributedRandomSampler 包装，自动扩展到多卡训练。而 <code>use_fp16</code> 则指定是否使用 APEX 混合精度训练。</p><p>之后，只需要调用 <code>trainer.train()</code> 就能训练一个 epoch：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(num_epochs):</span><br><span class="line">    metrics = trainer.train()</span><br><span class="line">    val_metrics = trainer.validate()</span><br></pre></td></tr></table></figure><p><code>Trainer</code> 本身不是一个可以直接传到 Ray Tune 的 <code>Trainable</code>，要想得到 <code>Trainable</code>，只需要执行</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">trainable = TorchTrainer.as_trainable(</span><br><span class="line">        training_operator_cls=MNISTrainingOperator,</span><br><span class="line">        num_workers=<span class="number">8</span>,</span><br><span class="line">        num_cpus_per_worker=<span class="number">2</span>,</span><br><span class="line">        use_gpu=<span class="literal">True</span>,</span><br><span class="line">        use_fp16=<span class="literal">True</span>,</span><br><span class="line">        config=&#123;<span class="string">&quot;batch_size&quot;</span>: <span class="number">16</span>&#125;</span><br><span class="line">    )</span><br></pre></td></tr></table></figure><p>之后，参照上面的代码用 tune.run 执行这个 <code>Trainable</code> 就可以自动调参。</p><p>需要注意的是，使用这种方式调用 <code>tune.run</code> 的话，不需要也不可以指定 <code>resources_per_trial</code>，<code>trainer</code> 会有额外的方法自动计算所需要的资源传给 Ray Tune。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>PyTorch 多卡分布式训练</title>
      <link>https://blog.howardlau.me/programming/pytorch-distributed-data-parallel.html</link>
      <description>
        <![CDATA[<p>目前，PyTorch 官方已经准备逐渐放弃对 <code>DataParallel</code> 的支持。而且受限于 Python 的 GIL，<code>DataParallel</code> 采用的多线程模型不能充分发挥多核性能。PyTorch 官方建议大家使用]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 22 May 2021 05:49:54 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>目前，PyTorch 官方已经准备逐渐放弃对 <code>DataParallel</code> 的支持。而且受限于 Python 的 GIL，<code>DataParallel</code> 采用的多线程模型不能充分发挥多核性能。PyTorch 官方建议大家使用 <code>DistributedDataParallel</code>（简称 DDP），即使你只有一台服务器。这个类使用上会复杂一些，PyTorch 官方教程写的比较复杂，这个教程将步骤拆解一下，方便入门。</p><h2 id="DDP-原理"><a href="#DDP-原理" class="headerlink" title="DDP 原理"></a>DDP 原理</h2><p>DDP 的原理是开启多个进程，每个进程占用一张卡来进行训练。在训练开始之前，每个进程需要加载相同的模型权重，这样大家就有同样的起点。然后，训练过程中，每个进程单独加载不同的训练集的子集，进行前向传播并进行反向传播求解梯度。最后，所有进程开始互相通信，交换其他所有进程的梯度（All-Reduce Sum），并求平均。最后，每个进程都使用平均后的梯度更新自己的模型参数。由于每个进程更新的梯度都是一样的，所以在每次梯度更新后所有进程的模型参数都是一样的。</p><p>同步梯度的时候不像 DataParallel 那样在一张卡上计算更新梯度之后再将模型参数下发（Parameter Server 模型），这样不仅会造成某张卡负载更高，还有可能会因为传输过多碰到网络瓶颈。所以 DDP 采用了 Ring AllReduce 的方法，每张卡只和自己的邻居交换梯度。</p><p><a href="/programming/pytorch-distributed-data-parallel/pytorch-ddp-1.jpg" data-fancybox="gallery" data-caption="img"><img src="/programming/pytorch-distributed-data-parallel/pytorch-ddp-1.jpg" alt="img"></a></p><p><a href="/programming/pytorch-distributed-data-parallel/pytorch-ddp-2.jpg" data-fancybox="gallery" data-caption="img"><img src="/programming/pytorch-distributed-data-parallel/pytorch-ddp-2.jpg" alt="img"></a></p><p>比如 1 卡只会接收从 0 卡发来的梯度，只会发送梯度到 2 卡。同时，传输的时候会将数据分块，数据量为 D 的时候，N 个进程在最开始的 N-1 轮传输会发送和接收 D&#x2F;N 的数据量，然后后面再用 N-1 轮，每个进程将每一块平均好的数据按照环形发送到下一个进程，同样是 D&#x2F;N 的数据量。这样总共需要传输 2D*(N-1)&#x2F;N 的数据量。可以看出，数据传输总量基本和 N 无关，所以用 DDP 有良好的扩展性，有钱任性开几百个进程训练数据传输都不会成为瓶颈。</p><p>没错，DDP 的原理就是这么简单，下面看看怎么用。首先介绍几个 DDP 里会碰到的概念（括号里是后面会用到的环境变量名称）：</p><ul><li>World Size（<code>WORLD_SIZE</code>）：总共有多少个进程参与训练，如果你有两台服务器，每台服务器有四张卡，那么 World Size 就是 2 x 4 &#x3D; 8。</li><li>Rank（<code>RANK</code>）：标识一个进程的序号，从 0 开始。按照上面例子的配置，第一台机器上的 0, 1, 2, 3 卡对应的进程序号就是 0, 1, 2, 3，第二台机器上 0, 1, 2, 3 对应的进程序号就是 4, 5, 6, 7。需要确保每个进程的序号没有重复。其中 0 号进程为主进程，负责一些同步操作的通信。</li><li>Master Address（<code>MASTER_ADDR</code>）：标识主进程所在的机器 IP 或者主机名，例如 <code>10.0.0.1</code> 或者 <code>gpu-server1</code>，每一个进程都填一样的就可以了。假如你只有一台服务器的话，填 <code>127.0.0.1</code> 或者 <code>localhost</code> 也可以。</li><li>Master Port（<code>MASTER_PORT</code>）：标识主进程应该监听哪一个端口，随便选择一个没有被占用的端口就好了，比如 23333、10086。一样是每个进程都填一样的就好了。</li><li>Local Rank（<code>LOCAL_RANK</code>）：标识一个进程在本机上是第几个进程，不是必须的参数。可以简单理解为这个进程占用的是一台机器上的第几张卡。按照上面例子的配置，第一台机器上的 0, 1, 2, 3 卡对应的 Local Rank 就是 0, 1, 2, 3，第二台机器上 0, 1, 2, 3 对应的 Local Rank 就是 0, 1, 2, 3。可以看出，这个和 Rank 不同，是可以重复的，只用来标识这是一台机器的第几张卡。另外，假如你用了 <code>CUDA_VISIBLE_DEVICES</code> 的话，需要注意 Local Rank 就不一定和卡的序号相同了。比如你设定了 <code>CUDA_VISIBLE_DEVICES=2,3</code>，你的 Local Rank 分别应该设置成 0, 1。</li></ul><p>这里提一下，既然 Rank 已经能够唯一标识每个进程，为什么还要 Local Rank？主要是在操作文件的时候还有设定进程使用的卡的时候会用到。因为可能你的服务器之间没有共享的文件系统，那么像数据集下载、模型保存这种操作就需要每台机器自己进行同步了，Local Rank 就可以当成这台机器上的本地主进程了。</p><h2 id="代码实战"><a href="#代码实战" class="headerlink" title="代码实战"></a>代码实战</h2><p>为了简化代码，下面的代码片段默认你使用 DDP，但是一般来说，为了方便 debug，在开发阶段用一张卡就能调，需要你自己写判断是否使用分布式训练。比如加一个 <code>if</code> 判断没有传上面的环境变量就使用 <code>DataParallel</code> 或者单卡来训练。</p><h3 id="初始化进程组"><a href="#初始化进程组" class="headerlink" title="初始化进程组"></a>初始化进程组</h3><p>首先，你需要在你的脚本里一开始就告诉 PyTorch，你要进行分布式训练。在开始训练前（通常在 <code>parser.parse_args()</code> 之后），添加这么一行代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">torch.distributed.init_process_group(backend=<span class="string">&quot;nccl&quot;</span>)</span><br><span class="line">local_rank = <span class="built_in">int</span>(os.getenv(<span class="string">&quot;LOCAL_RANK&quot;</span>), -<span class="number">1</span>)</span><br></pre></td></tr></table></figure><p>默认情况下，PyTorch 会读取上面提到的环境变量，尝试连接到主进程进行通信。一般多 GPU 训练使用 <code>nccl</code> 后端即可。</p><h3 id="设置使用的卡"><a href="#设置使用的卡" class="headerlink" title="设置使用的卡"></a>设置使用的卡</h3><p>初始化进程组后，设定本进程使用哪一张卡：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">torch.cuda.set_device(local_rank)</span><br><span class="line">device = torch.device(<span class="string">&quot;cuda&quot;</span>, local_rank)</span><br></pre></td></tr></table></figure><h3 id="加载模型并转移到卡上"><a href="#加载模型并转移到卡上" class="headerlink" title="加载模型并转移到卡上"></a>加载模型并转移到卡上</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">model = torch.load(<span class="string">&quot;model.bin&quot;</span>, map_location=<span class="string">&quot;cpu&quot;</span>)</span><br><span class="line">model.to(device)</span><br></pre></td></tr></table></figure><p>这里使用 <code>map_location=&quot;cpu&quot;</code> 的原因是避免不小心将模型加载到同一张卡上，因为 PyTorch 在保存模型的时候会同时保存这个模型在哪个设备上，一般来说只有主进程会保存，所以如果直接加载的话会将模型复制几遍加载到主进程的卡上，分分钟爆显存。</p><h3 id="使用-DDP-包装模型"><a href="#使用-DDP-包装模型" class="headerlink" title="使用 DDP 包装模型"></a>使用 DDP 包装模型</h3><p>直接使用 <code>DistributedDataParallel</code> 类包装即可。需要注意的是，如果使用混合精度训练，需要先用 <code>amp</code> 包装模型，再使用 DDP 包装模型：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> fp16:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">from</span> apex.optimizers <span class="keyword">import</span> FusedAdam</span><br><span class="line">            <span class="keyword">from</span> apex <span class="keyword">import</span> amp</span><br><span class="line">        <span class="keyword">except</span> ImportError:</span><br><span class="line">            <span class="keyword">raise</span> ImportError(<span class="string">&quot;Please install apex from https://www.github.com/nvidia/apex to use distributed and fp16 training.&quot;</span>)</span><br><span class="line"></span><br><span class="line">        optimizer = FusedSGD(model.parameters(),</span><br><span class="line">                              lr=learning_rate)</span><br><span class="line">        model, optimizer = amp.initialize(</span><br><span class="line">            model,</span><br><span class="line">            optimizers=optimizer,</span><br><span class="line">            opt_level=fp16_opt_level,</span><br><span class="line">            keep_batchnorm_fp32=<span class="literal">False</span>,</span><br><span class="line">            loss_scale=<span class="string">&quot;dynamic&quot;</span> <span class="keyword">if</span> loss_scale == <span class="number">0</span> <span class="keyword">else</span> args.loss_scale,</span><br><span class="line">        )</span><br><span class="line">model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)</span><br></pre></td></tr></table></figure><h3 id="包装数据集采样器"><a href="#包装数据集采样器" class="headerlink" title="包装数据集采样器"></a>包装数据集采样器</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">train_dataset = TensorDataset() <span class="comment"># 每个进程加载一样的数据集</span></span><br><span class="line">train_sampler = DistributedSampler(train_dataset)</span><br><span class="line">train_dataloader = DataLoader(train_dataset, sampler=train_sampler, batch_size=batch_size)</span><br></pre></td></tr></table></figure><h3 id="训练"><a href="#训练" class="headerlink" title="训练"></a>训练</h3><p>这一部分没有什么特别的，按照通常做法进行前向传播，反向传播，优化器更新参数即可。需要注意的是，为了保证每个进程采样过程随机数种子一样，需要在每个 Epoch 前设置 Sampler 的 Epoch：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(num_train_epochs):</span><br><span class="line">    train_sampler.set_epoch(epoch)</span><br><span class="line">    <span class="keyword">for</span> x, y <span class="keyword">in</span> train_dataloader:</span><br><span class="line">        x = x.to(device)</span><br><span class="line">        y = y.to(device)</span><br><span class="line">        outputs = model(x)</span><br><span class="line">        loss = loss_fn(outputs, y)</span><br><span class="line">        loss.backward()</span><br><span class="line">        optimizer.step()</span><br></pre></td></tr></table></figure><p>这样就完成了多卡训练代码的编写，下面看看怎么启动分布式训练。</p><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><h4 id="多进程读写文件同步"><a href="#多进程读写文件同步" class="headerlink" title="多进程读写文件同步"></a>多进程读写文件同步</h4><p>如果你的代码有输出或者写文件的操作，需要特别注意进程之间不要同时写一个文件，否则会造成数据错乱。一般来说，可以指定 <code>local_rank == 0</code> 或者 <code>torch.distributed.get_rank() == 0</code> 的进程来写。</p><h4 id="判断是否分布式训练"><a href="#判断是否分布式训练" class="headerlink" title="判断是否分布式训练"></a>判断是否分布式训练</h4><p>可以使用 <code>torch.distributed.is_available() and torch.distributed.is_initialized()</code> 判断，不是多进程的情况下就可以不需要担心多进程文件读写同步的问题了。</p><h2 id="启动训练"><a href="#启动训练" class="headerlink" title="启动训练"></a>启动训练</h2><h3 id="手动启动"><a href="#手动启动" class="headerlink" title="手动启动"></a>手动启动</h3><p>这里还是假设你有两台服务器，每台四张卡，分别叫 <code>gpu-server1</code> 和 <code>gpu-server2</code>。</p><p>在 <code>gpu-server1</code> 上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MASTER_ADDR=gpu-server1</span><br><span class="line"><span class="built_in">export</span> MASTER_PORT=10086</span><br><span class="line"><span class="built_in">export</span> WORLD_SIZE=8</span><br><span class="line">RANK=0 LOCAL_RANK=0 python train.py</span><br><span class="line">RANK=1 LOCAL_RANK=1 python train.py</span><br><span class="line">RANK=2 LOCAL_RANK=2 python train.py</span><br><span class="line">RANK=3 LOCAL_RANK=3 python train.py</span><br></pre></td></tr></table></figure><p>在 <code>gpu-server2</code> 上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MASTER_ADDR=gpu-server1</span><br><span class="line"><span class="built_in">export</span> MASTER_PORT=10086</span><br><span class="line"><span class="built_in">export</span> WORLD_SIZE=8</span><br><span class="line">RANK=4 LOCAL_RANK=0 python train.py</span><br><span class="line">RANK=5 LOCAL_RANK=1 python train.py</span><br><span class="line">RANK=6 LOCAL_RANK=2 python train.py</span><br><span class="line">RANK=7 LOCAL_RANK=3 python train.py</span><br></pre></td></tr></table></figure><p>PyTorch 的分布式训练进程会等待所有进程都准备完毕后才会继续往下执行，所以可以手动执行上面的脚本。</p><h3 id="使用-PyTorch-启动工具启动"><a href="#使用-PyTorch-启动工具启动" class="headerlink" title="使用 PyTorch 启动工具启动"></a>使用 PyTorch 启动工具启动</h3><p>PyTorch 提供了一个 <code>torch.distributed.launch</code> 帮助我们启动进程。</p><p>在 <code>gpu-server1</code> 上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MASTER_ADDR=gpu-server1</span><br><span class="line"><span class="built_in">export</span> MASTER_PORT=10086</span><br><span class="line">python -m torch.distributed.launch --use_env --nproc_per_node=4 --nnodes=2 --node_rank=0 --master_addr=<span class="variable">$MASTER</span>\_ADDR --master\_port=<span class="variable">$MASTER_PORT</span> train.py</span><br></pre></td></tr></table></figure><p>在 <code>gpu-server2</code> 上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> MASTER_ADDR=gpu-server1</span><br><span class="line"><span class="built_in">export</span> MASTER_PORT=10086</span><br><span class="line">python -m torch.distributed.launch --use_env --nproc_per_node=4 --nnodes=2 --node_rank=1 --master_addr=<span class="variable">$MASTER</span>\_ADDR --master\_port=<span class="variable">$MASTER_PORT</span> train.py</span><br></pre></td></tr></table></figure><p>其中 <code>--nproc_per_node</code> 指的是你每个服务器上想启动多少个进程，一般来说每个服务器有几张 GPU 就填几，<code>--nnodes</code> 表示你有几台服务器，<code>--node_rank</code> 指的是当前启动的是第几台服务器，从 0 开始。<code>--use_env</code> 表示 Local Rank 用 <code>LOCAL_RANK</code> 这个环境变量传参，如果不加这个选项，会在你的训练脚本之后额外添加一个 <code>--local_rank 0</code> 的命令行参数。</p><p>这个工具其实就是帮你计算 <code>WORLD_SIZE = nproc_per_node * nnodes</code>，然后执行一个循环，启动本机进程</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> local_rank <span class="keyword">in</span> range(nproc_per_node)</span><br><span class="line">RANK = nproc_per_node * node_rank + local_rank</span><br><span class="line">    <span class="comment"># 设置环境变量，启动训练进程</span></span><br><span class="line">    subprocess.call(...)</span><br></pre></td></tr></table></figure><p>这个脚本适合你的每台服务器上都有一样数量的卡，如果不是的话需要自己手动计算 Rank 和 World Size 按照第一种方法启动。</p><h3 id="通过-Slurm-启动"><a href="#通过-Slurm-启动" class="headerlink" title="通过 Slurm 启动"></a>通过 Slurm 启动</h3><p>如果你的服务器集群用 Slurm 作为作业管理软件，可以参考下面的 SBATCH 脚本来启动 PyTorch DDP 分布式训练：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment">#SBATCH -N 2</span></span><br><span class="line"><span class="comment">#SBATCH --ntasks-per-node=1</span></span><br><span class="line"><span class="comment">#SBATCH -p gpu_v100</span></span><br><span class="line"><span class="comment">#SBATCH --output=joblog/R-%x.%j.out</span></span><br><span class="line"><span class="comment">#SBATCH --error=joblog/R-%x.%j.err</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Load anything you want</span></span><br><span class="line">module load cudnn/7.6.4-CUDA10.1</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> MASTER_ADDR=`/bin/hostname -s`</span><br><span class="line"><span class="comment"># 自动找一个空闲端口</span></span><br><span class="line"><span class="built_in">export</span> MASTER_PORT=`netstat -tan | awk <span class="string">&#x27;$1 == &quot;tcp&quot; &amp;amp;&amp;amp;$4 ~ /:/ &#123; port=$4; sub(/^[^:]+:/, &quot;&quot;, port); used[int(port)] = 1; &#125; END &#123; for (p = 10000; p &lt;= 65535; ++p) if (! (p in used)) &#123; print p; exit(0); &#125;; exit(1); &#125;&#x27;</span>`</span><br><span class="line"></span><br><span class="line">srun run.slurm.sh</span><br></pre></td></tr></table></figure><p>其中 <code>run.slurm.sh</code> 参考内容如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line">python -m torch.distributed.launch \</span><br><span class="line">  --nproc_per_node=4 \</span><br><span class="line">  --nnodes=<span class="variable">$&#123;SLURM_JOB_NUM_NODES&#125;</span> \</span><br><span class="line">  --node_rank=<span class="variable">$&#123;SLURM_NODEID&#125;</span> \</span><br><span class="line">  --master_addr=<span class="variable">$&#123;MASTER_ADDR&#125;</span> \</span><br><span class="line">  --master_port=<span class="variable">$&#123;MASTER_PORT&#125;</span> \</span><br><span class="line">  train.py</span><br></pre></td></tr></table></figure><p>这个其实就是用 <code>srun</code> 在每台服务器上执行启动脚本，脚本里传递 Slurm 的环境变量给启动工具。</p><p>完整代码可以到 <a href="https://github.com/howardlau1999/pytorch-ddp-template">https://github.com/howardlau1999/pytorch-ddp-template</a> 参考。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>CMU 15-445 Fall 2020 Labs 实现笔记</title>
      <link>https://blog.howardlau.me/programming/cmu-15-445-fall-2020-labs-implementation-notes.html</link>
      <description>
        <![CDATA[<p>之前在 YouTube 刷了一下数据库入门基础课程 CMU 15-445&#x2F;645 的 <a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 27 Mar 2021 06:14:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>之前在 YouTube 刷了一下数据库入门基础课程 CMU 15-445&#x2F;645 的 <a href="https://www.youtube.com/watch?v=oeYBdghaIjc&list=PLSE8ODhjZXjbohkNBWQs_otTrBTrjyohi">Lecture Videos</a>，感觉 Andy 是个很有个性也很有趣的人，课堂气氛很活跃，也将数据库的基础知识讲的深入浅出，例子生动。虽然之前大二也修读了数据库的相关课程，但是由于时间限制，并没有讲关于索引并发控制的问题（事务并发控制还是讲了的），也没有讲 Recovery 的知识（听师兄说有一年不仅没讲，期末考试还考了，开卷考考场现学 ARIES），倒是讲了很多 Funtional Dependency 相关的知识。所以看网课也主要看了课内没讲的东西。</p><p>当然，课程内容其实大同小异，精华部分还在课程配套的 Lab。当时我上课的课程项目并没有实现 RDBMS，而是小组实现了单线程版的 <a href="https://github.com/sysu-2019-dbms/fp-tree">FPTree</a> [1]，用了 libpmem 模拟 PMEM。那个学期课程特别多，还特别硬，所以用五一假期和队友爆肝五天就做完了，就完成了单线程版的，虽然后面完成多线程版有加分，但确实没精力做了。这部分约等于实现了一个 PMEM 上的 Buffer Pool Manager 以及加了一点细节加速叶子节点查找的 B+ Tree。</p><p>CMU 15-445 的 Lab 代码可以在 <a href="https://github.com/cmu-db/bustub">https://github.com/cmu-db/bustub</a> 下载，但是课程页面明确说明已经完成的代码不可以公开。据说这是 Andy Pavlo 花了钱请了实习生写了几个暑假的，他非常慷慨地开放源代码，让不是 CMU 的学生也能学习。每学期都有 4 个 Lab，其中 Buffer Pool Manager 和 Query Execution 是固定节目，Index 部分有 Linear Hash（2019）或者 B+ Tree（2020）。而 Lab 4 可能是 Logging Recovery（2019） 或者 Concurrency Control（2020）。GitHub 上的代码只提供了非常基本的测试，通过了这些测试不代表你的实现就是正确的。需要将代码提交到 Gradescope 上通过更多更严格的测试，Gradescope 的测试代码不会开放。这是为了鼓励学生自己编写测试用例，毕竟现实中的工程开发测试用例也要自己写的，自己的代码质量应该由自己保证。往年 Gradescope 测试都没有开放给非 CMU 学生，今年（2021）开放了测试，可以将自己完成的 Lab 上传上去进行更多测试。Access Code 是 5VX7JZ （<a href="https://github.com/cmu-db/bustub/issues/111">https://github.com/cmu-db/bustub/issues/111</a> ），开放到 2021-12-31。不知道明年会不会开放，我对 Logging Recovery 还挺感兴趣的。</p><p>言归正传，15-445 的 Lab 也是完形填空，初始代码提供了类的定义以及接口，学生可以按需要添加更多的接口或者成员，但是不能修改已有的接口（测试代码可能会用到）。从第一个 Lab 开始，都要求实现是 Thread-safe 的，需要对共享的数据结构加锁保护。实话说，个人觉得 bustub 的接口设计比较令人迷惑，我其实花了很多时间去理解接口设计的用意……</p><h2 id="Lab-1-Buffer-Pool-Manager"><a href="#Lab-1-Buffer-Pool-Manager" class="headerlink" title="Lab 1 Buffer Pool Manager"></a>Lab 1 Buffer Pool Manager</h2><p>第一个实验是 Buffer Pool Manager。需要实现 <code>LRUReplacer</code> 以及 <code>BufferPoolManager</code>。我们知道，数据库是以页为单位管理数据存储的，同时为了加速 IO，会将一部分页面缓存在内存中，等到内存不足的时候再将修改过的页面刷写回磁盘。而 Replacer 实现不同的缓存算法，负责在内存不足的时候，决定将哪些页面淘汰以便给新页面腾出空间。当然，如果有线程正在读写某个页面的话，这个页面是不可以被淘汰的。而 BufferPoolManager 则提供接口供线程获取&#x2F;释放某个页面，隐藏了磁盘操作（由 DiskManager 负责，课程代码已经提供好）以及页面元数据的细节，并在内存不足的时候调用 Replacer 算法决定淘汰页面并按需刷写回磁盘。</p><p>这里会接触两个概念，<code>frame</code> 和 <code>page</code>：<code>frame</code> 是内存中的空间，和页面大小一样，用来放置从磁盘读取的 <code>page</code> 的内容。一个 <code>page</code> 可以放在任意的空 <code>frame</code> 里，操作者无需关心 <code>page</code> 放在哪个 <code>frame</code> 里，而是由 Buffer Pool Manager 来管理映射关系。操作者只需要调用 <code>FetchPage(page_id)</code> 接口即可获取需要的 <code>page</code>。当 Buffer Pool Manager 需要新的 <code>frame</code> 来存放 <code>page</code> 的内容，会首先查询自己的 <code>freelist</code> 中是否有空闲的 <code>frame</code>，如果没有，再调用 Replacer 的 <code>Victim()</code> 接口决定淘汰某个 <code>frame</code>，并将 <code>frame</code> 的内容刷回 Disk，再将新的 <code>page</code> 读取到 <code>frame</code> 中。如果连 Replacer 都无法淘汰出 <code>frame</code>，那么就 OOM 了，直接返回错误。</p><p>这里需要实现的 <code>LRUReplacer</code> 和一般的 LRU 不大一样，只需要一个 <code>list</code> 即可。当然，为了加速操作，可以添加一个 <code>unordered_map</code> 将 <code>frame_id</code> 映射到 <code>list</code> 中的迭代器。</p><ul><li><code>Pin()</code> 接口代表一个 <code>frame</code> 有线程正在读写，不可以被淘汰，那么就直接将这个 <code>frame</code> 从 <code>list</code> 中移除。</li><li><code>UnPin()</code> 接口代表这个 <code>frame</code> 已经没有线程读写了，可以被淘汰，又因为这个 <code>frame</code> 最近被读写，所以直接插到 <code>list</code> 后面即可。如果一个 <code>frame</code> 被 <code>UnPin()</code> 两次，我们不需要做任何操作。</li><li><code>Victim()</code> 接口代表 Buffer Pool Manager 已经没有空闲的 <code>frame</code> 可以使用，需要淘汰出一个 <code>frame</code>。这时直接将队首的元素移除并返回即可，要是 <code>list</code> 为空，说明 Replacer 中的所有页面都正在被使用，需要返回错误。</li></ul><p>而 Buffer Pool Manager 顾名思义，首先要有一个 Pool，Pool 中就是 <code>frame</code> 了，在 Lab 里是 <code>pages_</code> 成员变量，可以看到它就是一个 <code>Page</code> 数组而已。而 <code>frame_id</code> 就是 <code>pages_</code> 数组的下标。初始的时候我们将所有的 <code>frame_id</code> 加入到 <code>free_list_</code> 中，并初始化 Replacer。Buffer Pool Manager 通过引用计数（<code>Page</code> 中的 <code>pin_count</code> 字段的方法跟踪一个 Frame 被多少线程引用。</p><ul><li><code>FetchPageImpl()</code> 负责根据 page id 读取一个页面，首先会检查这个页面是否已经在 Pool 中，有则直接返回，并将引用计数加 1。</li><li><code>UnpinPageImpl()</code> 说明一个线程不再需要这个页面，可以释放。这时候 Buffer Pool Manager 直接将引用计数减 1。如果为 0，说明已经没有线程使用这个页面了，调用 Replacer 的 <code>UnPin</code> 接口来允许这个 Frame 重新分配。</li></ul><p>修改页面元数据的时候，是不需要调用 <code>Page</code> 的 <code>WLatch</code> 方法的，<code>Page</code> 的 <code>RLatch</code> 和 <code>WLatch</code> 是用来保护页面本身的数据（也就是会实际落盘的数据）。</p><h2 id="Lab-2-B-Tree-Index"><a href="#Lab-2-B-Tree-Index" class="headerlink" title="Lab 2 B+ Tree Index"></a>Lab 2 B+ Tree Index</h2><p>第二个实验是 B+ 树索引，我个人觉得接口设计很有槽点，有些函数例如 <code>Split()</code> 用了模板，但是 Internal Tree Page 和 Leaf Tree Page 的同名接口参数都不一样，需要自己补接口来用上模板。而且 Lab 指导里用的是 <code>reinterpret_cast</code> 直接简单粗暴地将 Page 里的 data 解释成数据结构。一般来说都是需要用序列化&#x2F;反序列化来转换的。</p><p>这部分的代码量比较大，而且因为接口设计的问题，可能要花点时间来理解不同函数到底在做什么。每个 Page 存储了若干 Pair。根据 Internal 和 Leaf 的不同，Pair 的类型也不一样。</p><ul><li>Internal Tree Page 中，Pair 的 Key 就是 Key，而 Value 是 Page ID。Key[i] &lt;&#x3D; PageID[i] &lt; Key[i+1]，而 Key[0] 是无效的，可以认为 PageID[0] 存放了比 Key[1] 小的所有 Key。自然，PageID[-1] 存放了比 Key[-1] 大的所有 Key。</li><li>Leaf Tree Page 中，Pair 的 Key 也是 Key，而 Value 则是 RID（RID 即是 PageID+SlotID，用来定位一条记录在哪个 Page 的具体哪个 Offset）。而 Leaf 中的 K-V 则是一一对应的，RID[i] 就是 Key[i] 对应的记录。</li></ul><p>具体的数据结构实现也并不难，在插入删除的时候检查节点大小，如果溢出则进行分裂，不足则视兄弟节点情况进行合并或者重分配。在实现的时候注意维护好有序关系以及 KV 对应关系即可。</p><p>之前我只实现过单线程版的 B+ Tree，而 Lab 要求实现多线程，则需要加锁。当然，不能简单粗暴地直接对整棵树加锁，这样效率过低。我们可以用 Crab Locking 的方法来只对树的一部分加锁。这里的锁是 Latch，也就是一般常用的 (RW)Mutex。</p><ul><li>如果是读操作，那么从树的根节点开始加 RLatch，然后查找子节点，对子节点加 RLatch 成功后立即解锁父节点即可。</li><li>如果是插入操作，那么首先从树的根节点开始加 WLatch，并将加了 WLatch 的 Page ID 存入一个队列中（可以用 Transaction 的 <code>AddIntoPageSet</code> 操作。如果检查子节点没有满，说明插入后到这里就不会继续分裂了，就可以释放之前所有父节点加的锁了。需要注意的是，解锁顺序要和加锁顺序一致，否则可能会导致死锁的情况发生。如果子节点满了，那么有可能子节点会需要分裂，从而导致父节点也会需要更新，这时候就不能解锁父节点的锁。</li><li>如果是删除操作，相比插入操作会更复杂一些，加 WLatch 的规则是一样的，只是需要检查的条件变成了子节点是不是至少是半满的。而在合并操作后，可能需要删除某个页面，这时候需要用 <code>AddIntoDeletedPageSet</code> 添加需要删除的页面，然后在解锁完成之后删除。</li></ul><h2 id="Lab-3-Query-Execution"><a href="#Lab-3-Query-Execution" class="headerlink" title="Lab 3 Query Execution"></a>Lab 3 Query Execution</h2><p>第三个实验是查询执行。这一部分不需要考虑多线程的问题，而且也不需要我们解析 SQL、生成执行计划等，而是用代码手工构建执行计划来进行测试。这一部分比较简单，执行器采用了 Vocalno 模型，每个 Executor 只需要实现 <code>Next()</code> 接口和 <code>Init()</code> 接口即可。 <code>Init()</code> 接口则用于初始化执行器。<code>Next()</code> 接口返回 true 则代表这个执行器还有下一条记录需要返回；false 则代表执行器执行完了，没有更多记录了。简单的代码示意如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Tuple tuple;</span><br><span class="line">RID rid;</span><br><span class="line">vector&lt;Tuple&gt; result_set;</span><br><span class="line">executor.<span class="built_in">Init</span>();</span><br><span class="line"><span class="keyword">while</span> (exectuor.<span class="built_in">Next</span>(&amp;tuple, &amp;rid)) &#123;</span><br><span class="line">result_set.<span class="built_in">push_back</span>(tuple);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> result_set;</span><br></pre></td></tr></table></figure><p>Executor 是可以嵌套的，最上层的 Executor 就代表了整条 SQL 语句。</p><p>实现 Executor 的时候，涉及到更新的部分，例如插入删除等，要记得同时更新 Table 中的所有 Index。</p><h2 id="Lab-4-Concurrency-Control"><a href="#Lab-4-Concurrency-Control" class="headerlink" title="Lab 4 Concurrency Control"></a>Lab 4 Concurrency Control</h2><p>第四部分则是将查询执行更改为支持事务。Executor Context 中可以获取到事务指针。实验中，事务获取锁的粒度是 Tuple，也就是以 RID 为 Key 来查询锁的信息。在 Executor 执行过程中，需要通过 Lock Manager 来获取锁，如果发生锁冲突的情况则会 Block 住锁请求。</p><p>不同于操作系统的锁，Lock Manager 是需要实现死锁检测的功能的，因此，需要将所有锁的信息保存在数据结构中，并定时检测锁之间的依赖是否产生了环，如果有，则想办法打破这个环即可解除死锁状态。</p><p>在实验提供的框架中，对于每一个 RID，都有一个对应的 Lock Queue 存放了当前已申请（可能没有被允许）并且还没释放的锁，当一个事务调用 LockShared 申请 S 锁的时候，只需要检测这个锁的前面是否有 X 锁即可，如果有，则不能申请成功。而当一个事务调用 LockExclusive 申请 X 锁的时候，则检测前面是否已经没有锁在排队，如果有，则同样不能申请成功。而一个事务持有 S 锁，需要对 Tuple 进行写操作的话，需要升级为 X 锁（不能先解锁再加锁），这时候和 LockExclusive 同理，只是不仅需要检测前面的锁是否冲突，还要检测后面是不是有锁已经被 Granted（例如后面也是 S 锁）。</p><p>实验中，死锁检测算法采用了有向图判环的算法来检测是否有死锁。Lock Manager 会在启动的时候启动一个后台线程，定时执行死锁检测算法。而死锁检测首先需要根据当前锁的排队情况生成等待图。等待图的每一个节点 V 表示一个事务，一条有向边 V1-&gt;V2 则代表 V1 需要 V2 当前持有&#x2F;等待的锁。</p><p>生成等待图的算法也很简单，对于每一个 Queue 中的每一个锁请求，检测在它前面的锁是否和它冲突即可，冲突则在图中添加节点和边。只有 S 锁和 S 锁是兼容的，其他的情况都是不兼容的。构建好图之后，运行一次三色 DFS 算法判环即可，不过测试好像要求起点要根据事务 ID 从小到大排。</p><p>如果有环的话，需要选择一个事务终止，实验的策略是选择最年轻的事务，实际上就是 ID 最大的事务。</p><p>而 Executor 这边则需要在每个读取和修改 Tuple 的地方都添加申请锁的代码。需要注意的是，因为下层的 Executor 可能已经在这个事务中对 Tuple 加锁了，每次加锁前都需要从 Transaction 指针查询当前的 RID 是不是已经加了对应的锁。假如要申请 S 锁，那么已经申请了 S 锁和 X 锁都是可以的，如果要申请 X 锁，却发现已经申请了 S 锁，那就申请锁升级。</p><p>这一部分的测试感觉比较弱，个人觉得自己的 LockUpgrade 没有实现的很周全，一样通过了全部测试。</p><p>[1] Oukid, Ismail, Johan Lasperas, Anisoara Nica, Thomas Willhalm, and Wolfgang Lehner. “FPTree: A Hybrid SCM-DRAM Persistent and Concurrent B-Tree for Storage Class Memory.” In <em>Proceedings of the 2016 International Conference on Management of Data</em>, 371–86. San Francisco California USA: ACM, 2016. <a href="https://doi.org/10.1145/2882903.2915251">https://doi.org/10.1145/2882903.2915251</a>.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>MIT 6.824 2015 Paxos Lab 实现笔记</title>
      <link>https://blog.howardlau.me/programming/mit-6-824-2015-paxos-lab-implementation-notes.html</link>
      <description>
        <![CDATA[<blockquote>
<p>There is only one consensus protocol, and that’s <em>Paxos</em>. – Mike Burrows, author of Chubby</p>
</blockquote>
<p>在]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 27 Mar 2021 05:54:01 GMT</pubDate>
      <content:encoded>
        <![CDATA[<blockquote><p>There is only one consensus protocol, and that’s <em>Paxos</em>. – Mike Burrows, author of Chubby</p></blockquote><p>在 2015 年及以前，MIT 6.824 课程的一致性算法实验都是用的 Paxos 算法。实现了 Raft 之后觉得也有必要实现一下 Paxos 这个经典的算法。</p><p>首先 Paxos 算法最原始的论文有 Leslie Lamport 写的 The Part-time Parliment 和 Paxos Made Simple。前面一篇是 1990 年写的，而且不同于其他学术论文，这篇论文用讲故事的方法来描述 Paxos 算法，我其实很喜欢这篇论文。不过，因为里面出现了太多的希腊字母和单词，而且行文实在是太过独特，许多人纷纷表示看不懂。于是，Lamport 在 2001 年的一场学术会议上向其他学者口头讲了一遍 Paxos 算法，并在事后根据回忆用正常一点的英语写了第二篇更容易懂的论文。</p><p>其实 Paxos 的核心算法个人觉得比 Raft 简单，因为它只做了一件事：如何让多个参与者对<strong>一个值</strong>的取值达成一致。但是为什么大家觉得 Raft 更好懂而且更好实现呢？因为 Paxos 只用数学语言和定理描述了算法，并没有说明一些工程实现上的细节，而且也没有随论文发表源代码和测试，就算实现了也难以确保自己的实现是正确的。而 Raft 则在论文里详细地描述了每个参与者的数据结构，RPC 的定义以及详细流程，同时还给出了作者本人的参考实现，所以哪怕论文读不太懂，对着代码看也能帮助理解算法。</p><p>Paxos 算法分为 Propose、Accept、Learn 三个阶段，不区分 Leader、Follower 等，任何参与者都可以提出一个值（这会导致活锁 Live-Lock 的问题，优化实现通常还是会先选举出 Leader）。Paxos 的核心思想是，预测未来是很难的一件事，所以我们干脆限制未来的可能性就好。</p><p>Paxos 里提到了三个角色 Proposer、Acceptor、Learner，其实我们并不需要开很多个进程，这些角色实际上对应的是 RPC 过程。Proposer 是驱动 Paxos 算法的一个线程，会发送消息给 Acceptor 和 Learner。模仿 Raft 的风格，其实 Paxos 算法包含以下几个 RPC：Propose RPC、Accept RPC、Decided RPC。Acceptor 负责处理 Propose 消息和 Accept 消息，Learner 则处理 Decided 消息。</p><p>因为 Paxos 算法仅仅是确定一个值的算法，而实际应用场景中，我们需要确定一系列值，例如状态机的 Log。那么对于每一个 Log，我们都要运行一次 Paxos 算法（称为一个 Instance），所以我们还需要分配序列号用来标识我们确定的是哪个值，可以理解为是 Raft 里的 Log Index。Lab 里叫 Seq。每个进程都需要记录关于每个 Seq 的状态信息，包括见过最大的 Propose N，已经 Accept 的最大的 N 及其对应的值 V。对于提交多个日志的 Paxos 算法一般就叫做 Multi-Paxos 了。</p><ol><li>当上层应用需要提交一个 Log 的时候，需要提供给 Paxos 算法一个 Seq 和需要提交的值。然后，每个参与者（Proposer）需要雀帝那个一个比自己见过的当前 Seq 对应的最大的数字 N_max 更大的数 N，而且需要这个 N 不会被别人提出。实现上，我们可以根据参与者 ID 来分配号码，例如有 3 个参与者，0,3,6… 给 0 号进程使用，1,4,7… 给 1 号进程使用。确定数字 N 后，向所有进程（包括自己）发送 Propose(Seq, N) 消息。这个阶段用来确定谁可以在之后提交值。</li><li>其他进程收到 Propose(Seq, N) 消息后，查询对应的 Acceptor 状态，如果最大的 Propose N 不如收到的大，那就更新 Acceptor 状态，记录这个 N，并且将已经 Accept 的最大的 N 以及其对应的值 V 返回给对方。也就是回复 Promise(true, AcceptN, AcceptV)。这意味着 Acceptor 做出了承诺：未来不会接受任何比 N 更小的请求。如果收到的 N 小于承诺过的 N，那么将拒绝这次 Propose 请求，回复 Promise(false)。</li><li>等收到集群中超过半数的成功回复后，就可以进入 Accept 阶段，这个阶段用来确定最终提交的值。但是，Proposer 不一定可以提交上层应用提交的值，而是要先检查 Promise 回复中，最大的 AcceptN 是多少，如果所有的回复都表示没有 Accept 过值，那么 Proposer 才可以申请 Accept 自己的值；否则，Proposer 只能申请 Accept 最大的 AcceptN 对应的 AcceptV。</li><li>确定好提交的值后，向所有 Acceptor 发送 Accept(Seq, N, V) 请求。Acceptor 收到 Accept 请求后，检查 N 是否大于等于自己收到过的最大的 Propose N。如果是，那么值就被确定了，Acceptor 将这次的 N 记录到自己最大的 AcceptN 中，并将值保存到 AcceptV 里，返回 Accept(true)；否则拒绝这次 Accept 请求，返回 Accept(false)。</li><li>Proposer 收到超过半数的 Accept 成功回复后，就可以认为这个值已经成功提交了。这时候就可以给客户端返回成功的消息，并向集群广播 Decided(Seq, N) 信息。Learner 收到 Decided 信息后，直接将 Seq 对应的值保存在自己的日志里，并且将 Seq 的状态修改为 Decided。</li></ol><p>总结一下，Paxos 决定值的关键阶段是 Propose 和 Accept，Propose 是主动发起的请求，其他都是被动处理的：</p><ol><li>Propose 阶段用来寻找已经被确定的值，防止更旧的值被提交。</li><li>Accept 阶段确定值，一旦<strong>大多数</strong>都接受了某个值，那么<strong>未来所有的提交</strong>都将会是这个值。</li></ol><p>实现了基础的 Paxos 算法之后，就可以在这个基础上实现 KV 服务了，也就是 Lab 3 的下半部分。</p><p>实现 Paxos KV 的思路和 Raft KV 其实大同小异，只是 Log 的 Index 需要由 KV 服务器自己提供，个人实现的时候是取第一个日志空洞的位置。同时，何时将日志应用到状态机也需要应用自己处理。尽管日志可以乱序确定具体值，但是提交还是要按顺序。只要模仿 Raft 维护一个 apply 的 Index，然后按顺序查询 Paxos 日志，将确定的值应用到状态机即可。去重原理也是利用 Reqeust ID 进行去重即可。</p><p>至于确定日志状态，粗暴点的办法就是定时轮询，实验对性能没有要求，就选择这个简单的办法。</p><h2 id="Paxos-和-Raft"><a href="#Paxos-和-Raft" class="headerlink" title="Paxos 和 Raft"></a>Paxos 和 Raft</h2><p>可以看出，Paxos 并不像 Raft 那样提交某个日志的时候将前面连续的所有日志也一起提交，而是允许“空洞”的存在。但这也会带来一些麻烦，进程需要逐条查询缺失的日志，论文里提到的就是重新发起一次 Paxos 过程即可。</p><p>之前提到活锁的问题，Paxos 有可能发生以下的情况：</p><ol><li>A Propose 1 并且得到了大多数回复，准备 Accept。</li><li>B 这时 Propose 2，因为数字更大，Acceptor 更新自己的状态，得到大多数回复。</li><li>A 发送 Accept 1 请求，因为 1 &lt; 2，所以这次 Accept 被拒绝，重新发起 Propose 3，得到大多数回复。</li><li>B 发送 Accept 2 请求，同样 2 &lt; 3，Accept 被拒绝，B 重新发起 Propose 4，得到大多数回复。</li></ol><p>以此类推，虽然整个系统里没有人宕机，消息也正常收发，但是系统的整体状态却没能推进。针对这种情况的解决办法也很简单，确保只有一个 Proposer 能够提出 Propose 请求即可。至于怎么选举 Leader，Lamport 提出了一种简单的办法，那就是谁的 ID 大谁就当 Leader，并定期向其他进程发送心跳消息维持 Leader 身份。其他进程如果超过一定时间间隔都没有收到 Leader 心跳，那么就尝试将自己选举成 Leader。收到客户端请求之后，如果发现自己不是 Leader，就拒绝这次请求，并通知客户端 Leader 的地址。非 Leader 不可以发起 Propose 请求，也就不会向其他服务器发送信息。即使是同时出现两个 Leader 也不要紧，因为 Paxos 本来就允许多个进程同时发起 Propose 请求，只是效率更低。</p><p>另外，对于每一个日志项都需要发送两轮 RPC，一次 Propose 一次 Accept 才可以确定值，显得有些低效。能不能在满足某些条件的时候只通过 Accept 来确定值呢？例如，如果我们能猜到 Prepare 的结果是可以自由提出任何值，不就可以省略 Prepare，直接 Accept 我们需要的值吗？如果我们能确保 Leader 只能有唯一一个，那就不会有其他人主动发起 Propose 请求了。我们可以将 Leader 选举过程也当成一条特殊的日志写入状态机就好了，Paxos 会保证这条日志只会有唯一确定的取值。</p><p>当一个 Proposer 在 Leader Elect 过程中收到大多数 Accept 回复后，就可以确定自己是 Leader 了，之后都可以用自己的 Propose N 来提交日志了。Leader 当选之后需要补齐自己的日志空洞，对于未提交的日志，需要用自己的 Propose N 来重新提交一次。</p><p>所以其实 Multi-Paxos 和 Raft 很像，选主的时候都是谁数字大谁当选（Raft 是 Term，Paxos 是 Propose N）。而且，一旦一个日志项（Raft 叫 Log Entry，Paxos 叫 Proposal）被多数派接收，那么它就是<strong>可以</strong>提交（Committ）的，而且这个值将永远不会被修改。等日志被提交（Committed）之后，上层应用才能应用（Apply）。Raft 强制日志要按顺序 Commit 以及 Apply，但 Paxos 允许乱序 Commit 的特性，给了上层应用更大的灵活性可以乱序 Apply。所以理论上，Paxos 性能上限比 Raft 更高。</p><p>Raft 相对于 Paxos 而言做出了更多的限制：</p><ol><li>同步日志上，Raft 不允许日志出现空洞，也就是如果想同步一条日志，需要将这条日志之前的所有日志也一并同步，Paxos 则认为日志之间是独立的，可以单独地同步任何一条日志。</li><li>Leader Election 上，只有日志和半数以上的成员至少一样新，才可以当选 Leader，而 Paxos 则是任何一个成员都可以当选。</li><li>前面两个限制简化了 Leader Election 和日志提交的过程，只要 Leader 有最新的已被提交的日志，那么之前的日志都是已经提交的。至少有一个多数派有已经提交的日志。</li></ol><p>而 Raft 和 Paxos 保证一个值被多数派确定确定后不会被修改都是利用了同一个原理：<strong>任何多数派之间肯定有交集</strong>。这个交集就确保了这个值不会被修改。</p><p>所以，其实 Raft 反而比 Paxos 多了更多限制，更复杂，更不好懂。据说 MIT 6.824 第一年换用 Raft 作为 Programming Lab 的时候，TA 和学生都饱受折磨，错误地以为 Raft 至少和 Paxos 一样好写，但胜在 Paper 详细，Diego Ongaro 的博士论文基本就等于手把手教你实现 Raft 了，他本人也给出了 LogCabin 这个参考实现。Paxos 其实更简洁，限制更少，但可惜一开始的 Paper 写得过于文学性，后面的 Paper 也没有给伪代码或者代码参考等，让人难以将算法翻译成代码。看来酒香也怕巷子深，好的东西还是要用大众更容易接受的办法展示出来才能收获更多关注。</p><p>也难怪 Google Chubby 作者 Mike Burrows 说：“世界上只有一种共识算法，那就是 Paxos。”</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>MIT 6.824 Spring 2021 Raft Lab 实现笔记</title>
      <link>https://blog.howardlau.me/programming/mit-6-824-spring-2021-raft-lab-implementation-notes.html</link>
      <description>
        <![CDATA[<p>最近花时间做了一下 6.824 的 Raft Labs，总算把 Raft 亲自实现了一下，收获还是很大的，羡慕 MIT 学生。实验提供的初始代码可以通过 <code>git clone git://g.csail.mit.edu/6.824-golabs-2021]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 18 Mar 2021 04:47:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>最近花时间做了一下 6.824 的 Raft Labs，总算把 Raft 亲自实现了一下，收获还是很大的，羡慕 MIT 学生。实验提供的初始代码可以通过 <code>git clone git://g.csail.mit.edu/6.824-golabs-2021 6.824</code> 获取。Lab 指导资料可以在 <a href="https://pdos.csail.mit.edu/6.824/labs/lab-raft.html">https://pdos.csail.mit.edu/6.824/labs/lab-raft.html</a> （Lab 2） 和 <a href="https://pdos.csail.mit.edu/6.824/labs/lab-kvraft.html">https://pdos.csail.mit.edu/6.824/labs/lab-kvraft.html</a> （Lab 3）获取。Lab 4 是 Sharded KV Server 实验，是默认的 Final Project，约等于精简版的 TiKV 了，工作量比较大。不得不说分布式的程序调起来十分要命，首先是错误不一定 100% 出现，复现得碰运气，也基本用不上 Debugger，得在茫茫 Log 中大海捞针，分析不同进程到底以怎样的顺序执行了操作，发生了什么状态导致了 Bug，是对耐心和编程技巧的一次考验。再次羡慕 MIT 学生，拥有优秀的教学资源。</p><p>不过，Lab 2 和 Lab 3 都没有要求实现 Membership Change，这个其实也是一个很具有挑战性的功能，有空参考其他资料实现一下。</p><p>Raft 有两篇论文，一篇是精简版的发表在 ATC’14 的 [In Search of an Understandable Consensus Algorithm]<sup><a href="#fn-1" id="fnref-1">[1]</a></sup>，另一篇则是更加完善的 Diego Ongaro 的博士论文 [Consensus: Bridging Theory and Practice]<sup><a href="#fn-2" id="fnref-2">[2]</a></sup>。</p><p><a href="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/In-Search-of-an-Understandable-Consensus-Algorithm-Ongaro-Ousterhout-2014.png" data-fancybox="gallery" data-caption=""><img src="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/In-Search-of-an-Understandable-Consensus-Algorithm-Ongaro-Ousterhout-2014.png"></a></p><p>Raft 最最核心的算法只有 Figure 2 的两个 RPC：<code>AppendEntries</code> 和 <code>RequestVote</code>，非常简单明了，实现起来代码量也不大，大约几百行就可以完成 Leader Election 和 Log Replication 的功能。而且小论文里的 <code>Figure 2</code> 也详细地列出了不同状态下的服务器在收到不同 RPC 的情况下，应当做什么。只要严格按照论文中的要求实现代码，就能完成 Raft 的基本功能。</p><p>Raft 有几个基本性质：</p><ol><li>Election Safety：一个 Term 内最多只能有一个 Leader。</li><li>Leader Append-Only：Leader 只会往自己的 Log 中新增日志，不会删除、修改日志。</li><li>Log Matching：如果某两个 Log 有相同的 Index 和 Term，那么这个 Log 以及之前的所有 Log 都包含相同的数据。</li><li>Leader Completeness：如果一个日志已经提交（Commited），那么这个日志将会包含在所有未来的有着更高 Term 的 Leader 中。</li><li>State Machine Safety：如果一个日志已经应用（Applied）到状态机，那么不会有服务器在同一个 Index 应用不同的 Log。</li></ol><p>同时，还有几个实现时候的原则：</p><ol><li>Log 只会由 Leader 发送到 Follower，不可能反向传输。</li><li>状态机只能在一个操作被 Apply 之后，才能将这个操作应用到状态转移。</li><li>Log 应该按顺序 Apply，不应该出现空洞，例如，应用 Index 为 1 的 Log 之后如果应用 Index 为 3 的 Log 是非法的。</li></ol><p>论文里提到 Leader 除了在收到客户端请求之后需要发送 AppendEntries RPC 给 Follower 同步最新日志，还需要定时发送 Heartbeat 给 Follower 以维持 Leader 状态，又提到 Heartbeat 其实是 entries 为空的 AppendEntries RPC。一开始可能会觉得 Heartbeat 是一种特殊的消息类型，而且只需要发送 entries 为空就好，但其实如果 Leader 发现 Follower 有缺失的日志（通过检查 nextIndex），也是需要捎带在心跳包中的。不然如果只在 Leader 收到客户端请求之后才发送 AppendEntries RPC 补齐日志，那么有可能如果客户端长时间不进行任何操作，有一部分日志会永远都同步不到 Follower 上，也就无法提交这部分日志。说白了 Heartbeat 也是 AppendEntries RPC，只是它允许 entries 为空，而不是必须为空。在心跳期间如果发现 Follower 日志落后于 Leader，也是需要按照流程将缺失的 Log 发送到 Follower 的。另外，AppendEntries RPC 还有一个功能是收集不同 Follower 的日志同步信息，以便 Leader 推进 commitIndex，同时 Leader 也需要通过 AppendEntries RPC 中的 leaderCommit 来推进 Follower 提交日志。所以，哪怕日志为空，Heartbeat 也起到了推进 commitIndex 的作用，各 Server 才可以将日志不断 Apply 到状态机。所以，Heartbeat 并不是仅仅是维持 Leader Lease 一个作用，系统状态也需要依赖它进行整体推进。因此，Leader 不需要实现单独的 Heartbeat 方法，只需要按照 Heartbeat 间隔向各 Follower 按照完成流程发送 AppendEntries RPC 即可，只是不需要检查 lastLogIndex 是否大于等于 nextIndex。当然，可能某次客户端请求后发送的 AppendEntries RPC 还没返回，nextIndex 还没有推进，就到了 Heartbeat 时间，这种情况下可能会重复发送相同的 AppendEntries RPC，虽然对正确性没有影响，但是是一个可以优化以节省网络带宽的地方。</p><p>论文里虽然提到了收到了过期的 RPC 请求直接忽略并返回当前 Term 给请求方，但没提到收到过期的 RPC 回复应该如何处理。一种比较简单的方法就是，比较 RPC 请求时的 Term 和当前 Term，如果是过期的请求，那么直接忽略不处理即可。</p><p>论文里还提到 Follower 收到 AppendEntries RPC 时应当检查 prevLogIndex 和 prevLogTerm 是否匹配。但是并没有提到不匹配的时候 Leader 应当如何探测出匹配的 Log Index。一种粗暴的方法是 Leader 每次都将 nextIndex - 1，直到成功匹配为止。这种方法效率显然是很低的，需要大量的 RPC，假如网络延迟比较高，那么时间开销会非常大。当然，也可以考虑二分的方法来匹配，但是二分法可能会传输不必要的 Log Entries。</p><p>更好的办法是让 Follower 在发生不匹配的时候尝试找到最后一个匹配的 Log Index 并发送回 Leader，这样 Leader 可以一步到位知道日志最后匹配的位置，也避免了发送不必要的日志。但是论文里没有提到怎么检测不匹配的位置，又如何返回信息。课程 TA 的博客（<a href="https://thesquareplanet.com/blog/students-guide-to-raft/">https://thesquareplanet.com/blog/students-guide-to-raft/</a> ）提到了这么一种办法：</p><p>在 AppendEntries Reply 中额外返回 <code>conflictIndex</code> 和 <code>conflictTerm</code> 两个字段</p><ol><li>如果 prevLogIndex 超过了自己的 Log Index，那么直接返回 conflictIndex &#x3D; lastLogIndex + 1，conflictTerm &#x3D; None</li><li>如果 prevLogIndex 在自己 Log 中存在，但是 Term 不等于 prevLogTerm，说明之前有 Leader 同步了 Log，但没来得及提交就 Crash 了，那么 conflictTerm 设为自己 log 中 prevLogIndex 对应的日志的 Term，并且将 conflictIndex 设置为那个 Term 的第一个日志的 Index</li></ol><p>Leader 收到 AppendEntries 失败的时候，需要根据 <code>conflictIndex</code> 和 <code>conflictTerm</code> 两个字段来找到最后匹配的日志。</p><ol><li>首先找 conflictTerm 在自己日志中是否存在，存在的话将 nextIndex 设置为那个 Term 最后一个 Log 的 Index + 1</li><li>如果找不到，那么 nextIndex &#x3D; conflictIndex</li></ol><p>这样可以减少 RPC 次数，也避免了发送不必要的 Log Entries 节省网络带宽。</p><p>原始的算法中并没有提到 Leader 当选后需要提交空日志，因为作为通用的共识算法，空日志应当由上层应用提供，我们只需要提供一个回调函数接口或者通知机制即可。但是这样可能导致如果 Leader 当选后的 Term 和之前未提交的 Log Term 不一致的话，而后续客户端又没有提供新日志，那么这最后的日志将无法提交，又导致了状态无法推进的问题。尽管正确的解决办法是上层应用在得知新 Leader 当选后立刻提交一个空日志，以便将之前未提交的日志一并提交，但是 Lab 2 测试没有这样的行为。所以我们需要尽可能避免这种情况发生。</p><p>一种可行的方法是放宽 Leader 提交日志的条件，将 log[newCommitIndex].Term &#x3D;&#x3D; currentTerm 的限制放宽到 log[newCommitIndex].Term &gt;&#x3D; currentTerm - 1。另外还需要调整 Heartbeat Interval 和 Election Timeout（对的，哪怕是 Raft 也要调参 &#x3D; &#x3D;），尽可能避免 Term 跨度过大的情况发生，也就是新 Leader 当选后由于网络延迟或者丢包等问题没来得及提交日志又发生了 Election 的问题。个人尝试了 Heartbeat Interval &#x3D; 125ms 和 Election Timeout &#x3D; 350ms~550ms 可以稳定通过测试。</p><p>分布式以及多线程程序的测试，往往测试几次可能都无法暴露出 bug，需要大量长时间重复测试才可以更充分地说明你的程序没有 bug （有的 bug 可能需要跑几百次才会发生一次）。同时也需要保存好测试中的 log 输出，等测试发现了有 bug 发生，就可以用 log 来排查问题。否则又要跑几百次来复现 bug。</p><p>我个人测试用了助教提供的并行测试脚本 <a href="https://gist.github.com/jonhoo/f686cacb4b9fe716d5aa">https://gist.github.com/jonhoo/f686cacb4b9fe716d5aa</a> ，可以同时开多个 <code>go test</code> 进程来对算法进行测试。这是因为在低负载和高负载的情况下，进程的调度顺序可能有很大不同，高负载情况下往往更容易暴露出一些 Bug。当然并行数不能开太高，我设置的是 CPU 核数 x 2，如果太高的话，可能会发生某个测试跑太慢，实际运行时间太长（例如 2B 测试需要在 120s 内跑完）导致失败的问题（尽管可能程序并没有问题）。我以 CPU 核数 x 2 为并行数，跑了 2048 次测试，在我的 Ubuntu 20.04 虚拟机上一个测试大概需要 8 分钟的实际时间和 1m30s 的 CPU 时间。跑了整整一天之后看见没有 failed，终于觉得实现比较靠谱了。</p><p><a href="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/image-20210318165912174.png" data-fancybox="gallery" data-caption=""><img src="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/image-20210318165912174.png"></a></p><p>Raft 日志不能无限增长，所以后期需要实现 Snapshot，并对 Log 数组进行截断操作以节省内存使用，这时候就不能粗暴地用 Log Index 作为数组下标寻址。一种方法是用 map 存储 Log Index 和实际数组下标的关系，不过维护起来可能有些麻烦。由于 Log 是有序存储的，所以用二分查找也是不错的选择，实现也比较简单，不需要额外空间。个人用的是二分查找的办法。</p><p>Snapshot 中的 LastIncludedTerm 和 LastIncludedIndex 可以存储在 log[0]，这样就不需要额外的字段来存储 Snapshot 状态了，也简化了代码。</p><p>Snapshot 的实现也是直接按照论文实现即可，由于不用实现 Snapshot 的分段传输，所以没有什么难度。不过 Snapshot 的应用流程有一点绕，具体流程是：</p><ol><li>上层应用检测到 Raft Log 过大时，将自己当前状态机进行编码，并调用 <code>Snapshot</code> 函数将当前状态数据以及最后应用的 Index 传入 Raft 层，Raft 层截断自己的 Log 并将 Snapshot 数据持久化。</li><li>当 Follower 收到 <code>InstallSnapshot RPC</code> 后（一般是落后太多的情况下，例如宕机太久或者由于网络问题一直没有收到 AppendEntries），更新自己的 Log，并将 Snapshot 提交到 Apply Channel。</li><li>上层应用收到 Apply Channel 中的 Snapshot 时，调用 <code>CondInstallSnapshot</code> 检查 Snapshot 是否是最新的，能否被应用。</li><li>如果 <code>CondInstallSnapshot</code> 返回 true，说明 Snapshot 可以应用，上层根据自己的协议解析 Snapshot Data，应用到自己状态机即可。</li></ol><p>实现了 Raft 之后实现上层应用就比较简单了，只需要调用 <code>Start()</code> 接口并监听 Apply Channel，将状态机命令应用即可。Lab 3 就是基于 Lab 2 的 Raft 协议实现一个 Fault-tolerance KV Service。在小论文里篇幅限制没有详细说明 Client 的 RPC 过程，但是在博士论文里有详细说明 RPC 的过程。</p><p><a href="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/image-20210318203506954.png" data-fancybox="gallery" data-caption=""><img src="/programming/mit-6-824-spring-2021-raft-lab-implementation-notes/image-20210318203506954.png"></a></p><p>需要注意几点：</p><ol><li>所有的客户端请求只能由 Leader 处理，如果请求的服务器不是 Leader，或者在请求处理过程中失去了 Leader 的身份，需要返回错误告知 Client 当前 Server 不再是 Leader。一个优化的操作是同时在错误返回中告知当前 Leader 的地址。</li><li>新 Leader 选举出来的之后为了更快提交之前没有提交的日志，需要马上在当前任期提交一个空操作的日志，成功提交之后之前的日志也就一并提交了。我是通过一个回调函数来实现的。用 Channel 可能更好。</li><li>客户端初始并不知道哪一个 Server 是 Leader，可以简单地随机尝试一个。当 RPC 失败或者 Server 返回错误说明自己不是 Leader 的情况下，随机重试另外一个 Server，如果错误中包含了 Leader 地址，直接用 Leader 地址即可。由于实际中 Leader 一般不会频繁变化，Client 在收到一次成功的响应之后就可以将 Leader 缓存起来，下一次直接请求即可。当然，也可以实现 Server 负责转发请求到真正的 Leader，但是这种实现会麻烦一些，由客户端自行选择 Leader 会简化实现。</li><li>为了防止一个客户端命令重复提交，需要每个客户端拥有一个唯一的标识符。博士论文里是使用 <code>RegisterClient</code> RPC 来申请一个 Client ID，Lab 3 简单起见直接由客户端在启动的时候生成一个随机数作为 Client ID。同时，每一个 Client 发送请求的时候都应该携带唯一的 Request ID，可以简单采用递增的序列号来标识请求。Server 端收到请求的时候，首先检查 Client ID + Request ID 是否已经响应过，如果有缓存的话则不再进行 Raft 共识，直接将缓存的响应返回给客户端，达到去重效果。在提交操作到 Raft 层时，需要将 Client ID 与 Request ID 也作为 Op 的一部分提交，确保每一个 Member 都能得知相同的去重数据。在 Server Apply Command 的时候，直接构造好响应请求存入缓存中即可。</li><li>由于 Server 不可能无限制地缓存所有请求的回复，需要定期删除 Server 的响应缓存（Lab 3 测试中也会测试你的 Snapshot 大小是否超过限制）。需要注意的是每个 Server 淘汰缓存的策略应该是确定性并完全一致的，论文里提到可以采用 LRU 的方法。或者，Leader 提交 Op 的时候同时提交一个时间戳，每个 Server 根据时间戳来决定是否淘汰。也可以 Client 在发送请求的时候捎带上自己最后收到回复的 Request ID，Server 直接将这个 Request ID 以及之前的响应全部淘汰即可。Server 还应当记录每个 Client 的 ID 以及发送过的最大的 Request ID，如果发现一个请求的 Client ID 不存在，或者有着更小的 ID，但是缓存中没有找到，说明这个响应已经处理过，但是已经被淘汰了。这时候可以换回一个 ErrSessionExpired 告知客户端会话过期，如何处理这个错误由客户端决定，博士论文提到其实现是直接 Crash 客户端，这样就直接申请一个新的 Client ID 继续运行就好。</li><li>每一个请求在状态机 Apply 之前都不应该返回，可以用一个 Channel Block 住当前请求，并用 Raft 层返回的 Command Index 作为索引存放在 map 中。如果某个 Command Index 已经在等待，说明之前这个 Server 曾经当过 Leader，但是可能是某个 Minority 的 Leader，在与 Majority 恢复通信之后 Log 被截断（有更大的 Term 的 Log），之后又当选为 Leader，所以会出现同一个 Command Index 已经有等待的请求的情况发生，这时候应该先响应旧的请求 ErrWrongLeader，然后再将新的请求存入 map 中。Apply Log 的时候也是同理，如果发现实际应用的 Request ID 或者 Client ID 不匹配，说明之前的请求失败了，应该返回 ErrWrongLeader 让客户端重试一次。</li><li>如果状态机应用了 Snapshot，应该检查 Snapshot 之前的 Index 是不是有正在等待的客户端请求，在 Lab 实现里，只有 Follower 才会收到 Snapshot 信息，所以直接将所有正在等待的客户端请求响应 ErrWrongLeader 即可。</li></ol><p>踩过的一些坑：</p><p>不要在临界区做阻塞的操作。例如在 Lock 之后向一个阻塞的 Channel 发送消息或者进行同步 RPC。这样大概率会导致死锁。应该另起 goroutine 或者 Unlock 之后再做阻塞的操作（不知道用 select 将其转换为非阻塞操作是否合理）。</p><p>如果一个 Channel 需要 close，最好只有一个 goroutine 来对其进行写操作，而且读方不可以 close 这个 Channel，只能由写方来 close。这是由于向一个已经关闭的 channel 进行写操作会导致 panic，如果一个 goroutine close Channel 之后另一个进行写，就会导致 panic。但是由于第一条规则，我们也不能对 Channel 操作加锁，所以最好是将对 Channel 的写操作收敛到一个 goroutine 里。</p><div class="footnotes"><hr><ol><li id="fn-1">Ongaro, Diego, and John K. Ousterhout. “In Search of an Understandable Consensus Algorithm.” In *2014 USENIX Annual Technical Conference, USENIX ATC ’14, Philadelphia, PA, USA, June 19-20, 2014*, edited by Garth Gibson and Nickolai Zeldovich, 305–19. USENIX Association, 2014. https://www.usenix.org/conference/atc14/technical-sessions/presentation/ongaro. <a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">Ongaro, Diego. “Consensus: Bridging Theory and Practice,” n.d., 258. <a href="#fnref-2" class="footnote-backref">↩</a></li></ol></div>]]>
      </content:encoded>
    </item>
    <item>
      <title>论文阅读 | TransCoder AI 转译代码</title>
      <link>https://blog.howardlau.me/machine-learning/ai-transcoder.html</link>
      <description>
        <![CDATA[<p>个人感觉还是比较有意思的一个工作。</p>
<p>Paper：<a href="https://arxiv.org/abs/2006.03511">https://arxiv.org/abs/2006.03511</a></p>
<p>Code：<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Thu, 24 Dec 2020 01:53:34 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>个人感觉还是比较有意思的一个工作。</p><p>Paper：<a href="https://arxiv.org/abs/2006.03511">https://arxiv.org/abs/2006.03511</a></p><p>Code：<a href="https://github.com/facebookresearch/TransCoder">https://github.com/facebookresearch/TransCoder</a></p><p>Facebook 用 XLM 对代码进行预训练，用了无监督翻译的思想，达到 AI 自动翻译代码的效果。虽然理论上图灵完备的语言都能互相转换，而且像 TypeScript 之类的语言通常自带 Transcompiler 将自己编译到 JavaScript 等已经有解释器&#x2F;编译器的语言，但是这需要 Transcompiler 作者对源语言和目标语言都非常熟悉，而且需要编写大量规则。而完全手动翻译则更加不可取。所以尽管 AI 翻译的准确率比较低，但是用 AI + 人工调整的方式还是具有一定优势的。</p><p>但是问题在于代码几乎没有平行语料可以用，也就是很难找到大量的 C++ 到 Java 或者之类的对应的代码，所以最好是使用无监督翻译的思想。这篇论文假设代码都能映射到同一个隐空间，用了 XLM 的思想，所有语言对都用同一个模型翻译。</p><p><a href="/machine-learning/ai-transcoder/image-20201224173138779.png" data-fancybox="gallery" data-caption="image-20201224173138779"><img src="/machine-learning/ai-transcoder/image-20201224173138779.png" alt="image-20201224173138779"></a></p><p>这种思路关键在于语言之间需要有<strong>锚点</strong>（Anchor Point），否则模型很难对齐 Embedding。像英文-中文就比较难对齐，因为用的字母表都完全不同，而英语-法语就比较容易对齐。编程语言中的关键词、标识符通常都是用英文单词写的，所以同理也比较适合 XLM 对齐。（但是可能有拼音、ACM 风的变量名）</p><p><a href="/machine-learning/ai-transcoder/image-20201224165426562.png" data-fancybox="gallery" data-caption="image-20201224165426562"><img src="/machine-learning/ai-transcoder/image-20201224165426562.png" alt="image-20201224165426562"></a></p><p>模型训练主要分为三个步骤：</p><ol><li>首先用 XLM 训练一个 MLM</li><li>然后用 DAE 训练自己到自己的翻译模型（尽管源代码有被修改但是模型还能够恢复正确的代码）</li><li>最后就是经典的回译了</li></ol><p>然后论文提到以前的评价指标是 BLEU，这也是机器翻译常用的指标。但是对于程序来说，只要运行结果正确就可以了，变量名之类的可以完全不同。所以作者从 GeeksForGeeks 搜集了一些算法题，然后将不同版本的答案作为测试用的平行语料，最后就是用 OJ 的思想来对翻译后的代码进行正确性验证，随机生成不同样例，只要能 AC 就算翻译成功。</p><h3 id="实验部分"><a href="#实验部分" class="headerlink" title="实验部分"></a>实验部分</h3><p><a href="/machine-learning/ai-transcoder/image-20201224173003058.png" data-fancybox="gallery" data-caption="image-20201224173003058"><img src="/machine-learning/ai-transcoder/image-20201224173003058.png" alt="image-20201224173003058"></a></p><p>作者用了 Google Cloud 上的 Github Repos 数据集，具体如何下载可以到代码仓库的 README 查看。实验只做了 C++、Java、Python 的部分。刚好这三种语言比较流行，代码量比较大，而且有动态语言和静态语言，覆盖面比较广。测试的时候以函数为粒度进行测试。</p><p><a href="/machine-learning/ai-transcoder/image-20201224173027397.png" data-fancybox="gallery" data-caption="image-20201224173027397"><img src="/machine-learning/ai-transcoder/image-20201224173027397.png" alt="image-20201224173027397"></a></p><p>关于数据预处理，传统的机器翻译通常对所有语言都使用同一个 Tokenizer，这样可以尽可能减少词表大小，增加词汇重叠度。但是对于编程语言，行不通，因为 C++ 缩进是可以忽略的，但是 Python 是不可以的，而且像 <code>&amp;&amp;</code> 和 <code>||</code> 这种操作符 Python 也没有。所以不同的语言要使用不同的 Tokenizer，然后再用 BPE 来将 Token 分解为 Subword。</p><p><a href="/machine-learning/ai-transcoder/image-20201224173100370.png" data-fancybox="gallery" data-caption="image-20201224173100370"><img src="/machine-learning/ai-transcoder/image-20201224173100370.png" alt="image-20201224173100370"></a></p><p>实验结果可以看到，静态语言到静态语言的准确率还是很高的，而动态语言到静态语言相比之下就相当低了。这也很好解释，因为动态语言通常没有类型标注，翻译到 C++、Java 之类的比较困难。</p><p>作者还和 j2py 还有一个 Tangible Software 的商用的 C++ 到 Java 的翻译器作了比较。后面的翻译器能处理像 vector 到 List 之类的映射。</p><p><a href="/machine-learning/ai-transcoder/image-20201224172323236.png" data-fancybox="gallery" data-caption="image-20201224172323236"><img src="/machine-learning/ai-transcoder/image-20201224172323236.png" alt="image-20201224172323236"></a></p><p>可以看到准确率在 Greedy Search 情况下不如 Baseline，但是加了 Beam Search 的话准确率可以大幅提升，不过这是算 Beam Search 所有搜到的结果，只要有一个成功就算翻译成功。Top 1 则是取 Log Prob. 最大的，可以看到相比不取，掉了很多准确率，说明模型会把不正确的程序的概率计算成比较大的。</p><p><a href="/machine-learning/ai-transcoder/image-20201224173213356.png" data-fancybox="gallery" data-caption="image-20201224173213356"><img src="/machine-learning/ai-transcoder/image-20201224173213356.png" alt="image-20201224173213356"></a></p><p><a href="/machine-learning/ai-transcoder/image-20201224173411380.png" data-fancybox="gallery" data-caption="image-20201224173411380"><img src="/machine-learning/ai-transcoder/image-20201224173411380.png" alt="image-20201224173411380"></a></p><p><a href="/machine-learning/ai-transcoder/image-20201224173524037.png" data-fancybox="gallery" data-caption="image-20201224173524037"><img src="/machine-learning/ai-transcoder/image-20201224173524037.png" alt="image-20201224173524037"></a></p><p>这里是作者给出的几个成功的样例，可以看到模型能够学习到不同语言标准库和原始类型的映射关系。即便是 Python 到 Java，在一些情况下仍然能正确推导一些变量的类型。</p><p><a href="/machine-learning/ai-transcoder/image-20201224173625283.png" data-fancybox="gallery" data-caption="image-20201224173625283"><img src="/machine-learning/ai-transcoder/image-20201224173625283.png" alt="image-20201224173625283"></a></p><p>和 Baseline 比较，模型对于标准库函数映射更胜一筹。</p><p>奇怪的是，一些非常简单的函数反而会翻译错误：</p><p><a href="/machine-learning/ai-transcoder/image-20201224173706944.png" data-fancybox="gallery" data-caption="image-20201224173706944"><img src="/machine-learning/ai-transcoder/image-20201224173706944.png" alt="image-20201224173706944"></a></p><p>主要是不同语言语法和标准库函数一些细微的差别导致的。</p><h3 id="想法"><a href="#想法" class="headerlink" title="想法"></a>想法</h3><p>AI 转译主要是难以保证结果的稳定性而且不好 Debug，程序本来就是结构化的，可以写 Transcompiler 准确翻译。不过写 Transcompiler 需要编码大量规则，我觉得可以尝试从模型中抽取标准库函数和语法之类的对齐关系来辅助编写 Transcompiler。</p><p>而且例子给出的基本都是只用了标准库函数的代码，现实应用中通常会使用大量的第三方库或者框架，不知道模型对于这种场景是否能很好应对。</p><p>个人感觉比较新颖吧，但是实用性可能还有待考究。作者说可以训练一些从旧代码语言比如 COBOL 翻译到 Java 的模型，但是我感觉有可能旧代码的量不是很够训练。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>机器学习 | EM 算法</title>
      <link>https://blog.howardlau.me/machine-learning/em-algorithm.html</link>
      <description>
        <![CDATA[<h1 id="EM-算法实验"><a href="#EM-算法实验" class="headerlink" title="EM 算法实验"></a>EM 算法实验</h1><h2 id="最大似然估计"><a href="#最大似然估计" class="headerlink"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Fri, 13 Nov 2020 00:47:10 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h1 id="EM-算法实验"><a href="#EM-算法实验" class="headerlink" title="EM 算法实验"></a>EM 算法实验</h1><h2 id="最大似然估计"><a href="#最大似然估计" class="headerlink" title="最大似然估计"></a>最大似然估计</h2><p>在已知每次实验的硬币是 A 和 B 的情况下，我们可以直接使用最大似然估计求解 A、B 硬币正面出现的概率 $p_A$ 和 $p_B$。由于每次实验都是独立地抛十次，所以可以知道实验结果服从二项分布 $B(10, p_A)$ 和 $B(10,p_B)$。</p><p>设 A 硬币的第 $i$ 次实验正面朝上的次数为 $y_A^{(i)}$， B 硬币的第 $i$ 次实验正面朝上的次数为 $y_B^{(i)}$，分别进行了 $n_A$ 次实验和 $n_B$ 次实验，可以得到 A 的似然估计函数为：</p><p>$$L_A&#x3D;\prod^{n_A}_{i&#x3D;1}{10\choose y^{(i)}_A}p_A^{y^{(i)}_A}(1-p_A)^{10 - y^{(i)}_A}$$</p><p>B 的似然估计函数为：</p><p>$$L_B&#x3D;\prod^{n_B}_{i&#x3D;1}{10\choose y^{(i)}_B}p_B^{y^{(i)}_B}(1-p_B)^{10 - y^{(i)}_B}$$</p><p>分别取对数并对 $p_A$ 求导后得到：</p><p>$$\frac{\partial\ln L_A}{\partial p_A}&#x3D;\sum^{n_A}_{i&#x3D;1}(\frac{y_A^{(i)}}{p_A}-\frac{10 -y_A^{(i)}}{1 - p_A})$$</p><p>令导数为 0，得到：</p><p>$$p_A &#x3D; \frac{\sum_{i&#x3D;1}^{n_A}y_A^{(i)}}{\sum_{i&#x3D;1}^{n_A}y_A^{(i)}+\sum_{i&#x3D;1}^{n_A}(10-y_A^{(i)})}$$</p><p>同理可得：</p><p>$$p_B &#x3D; \frac{\sum_{i&#x3D;1}^{n_B}y_B^{(i)}}{\sum_{i&#x3D;1}^{n_B}y_B^{(i)}+\sum_{i&#x3D;1}^{n_B}(10-y_B^{(i)})}$$</p><p>代入数据，即求得：</p><p>$$p_A &#x3D; \frac{9+8+7}{9+8+7+1+2+3}&#x3D;\frac{24}{30}&#x3D;0.8\\p_B &#x3D; \frac{5+4}{5+4+5+6}&#x3D;\frac{9}{20}&#x3D;0.45$$</p><h2 id="EM-算法"><a href="#EM-算法" class="headerlink" title="EM 算法"></a>EM 算法</h2><p>而在不知道每次实验选取的是哪个硬币的时候，我们就没有办法直接对 A 和 B 进行最大似然估计。这时候就需要使用 EM 算法，通过引入一个隐变量 $Z$ 来对硬币概率建模，并规定 $Z\in \{0, 1\}$，$Z&#x3D;0$ 表示当前硬币是硬币 A，$Z&#x3D;1$ 表示当前硬币是硬币 B，并记 $P(Z&#x3D;0)&#x3D;\pi$。此时模型参数 $\theta&#x3D;(\pi,p_A,p_B)$，可以知道，$Y$ 与 $Z$ 是相关的，要求 $P(Y|\theta)$，就需要求 $P(Y, Z|\theta)$ 的边缘分布。</p><p>记 $z_i$ 和 $y_i$ 分别为第 $i$ 次实验选择的硬币（未知的）和正面朝上的次数，那么，对于所有实验而言，似然函数就变成了</p><p>$$P(Y|\theta)&#x3D;\prod^{n}_{i&#x3D;1}P(Y&#x3D;y_i|\theta)&#x3D;\prod^{n}_{i&#x3D;1}\sum_{z_i \in {0,1} }P(Y&#x3D;y_i,Z&#x3D;z_i|\theta)&#x3D;\prod_{i&#x3D;1}^{n}\sum_{z_i\in{0,1}}P(Z&#x3D;z_i|\theta)P(Y&#x3D;y_i|Z&#x3D;z_i,\theta)\\&#x3D;\prod_{i&#x3D;1}^{n}[\pi{10\choose y_i}\ p_A^{y_i}(1-p_A)^{10-y_i}+(1-\pi){10\choose y_i}\ p_B^{y_i}(1-p_B)^{10-y_i}]$$</p><p>而极大似然估计求参数即为：</p><p>$$\hat\theta&#x3D;\arg\max_\theta\ln P(Y|\theta)$$</p><p>这个问题很难求出解析解，所以需要使用 EM 迭代法求近似解。</p><p>因为 </p><p>$$\ln P(Y|\theta)&#x3D;\sum_{i&#x3D;1}^{n}\ln[\pi{10\choose y_i}\ p_A^{y_i}(1-p_A)^{10-y_i}+(1-\pi){10\choose y_i}\ p_B^{y_i}(1-p_B)^{10-y_i}]$$</p><p>含有“和的对数”，极难求解，通过 Jensen 不等式可得</p><p>$$\ln P(Y|\theta) &#x3D; \sum^{n}_{i&#x3D;1}\ln \sum_{z_i \in {0,1}}P(Y&#x3D;y_i,Z&#x3D;z_i|\theta)\\&#x3D;\sum^{n}_{i&#x3D;1}\ln \sum_{z_i \in {0,1}}Q_i(z_i,\theta)\frac{P(Y&#x3D;y_i,Z&#x3D;z_i|\theta)}{Q_i(z_i,\theta)}\\\ge\sum^{n}_{i&#x3D;1} \sum_{z_i \in {0,1}}Q_i(z_i,\theta)\ln \frac{P(Y&#x3D;y_i,Z&#x3D;z_i|\theta)}{Q_i(z_i,\theta)}&#x3D;Q(\theta;\theta)$$</p><p>而 </p><p>$$Q_i(z_i,\theta)&#x3D;P(Z&#x3D;z_i|Y&#x3D;y_i, \theta)&#x3D;\frac{P(Z&#x3D;z_i|\theta)P(Y&#x3D;y_i|Z&#x3D;z_i, \theta)}{P(Y&#x3D;y_i|\theta)}\\&#x3D;\frac{(1-z_i)\pi{10\choose y_i}\ p_A^{y_i}(1-p_A)^{10-y_i}+z_i(1-\pi){10\choose y_i}\ p_B^{y_i}(1-p_B)^{10-y_i}}{\pi{10\choose y_i}\ p_A^{y_i}(1-p_A)^{10-y_i}+(1-\pi){10\choose y_i}\ p_B^{y_i}(1-p_B)^{10-y_i}}$$</p><p>可以看出：$1-Q_i(0)&#x3D;Q_i(1)$</p><p>在 E 步，我们通过统计数据计算出每次实验的 $Q_i(z)$，然后固定参数 $\theta$，改变参数 $\theta’&#x3D;(\pi’,p_A’,p_B’)$，最大化下面的函数：</p><p>$$Q(\theta’;\theta)&#x3D;\sum_{i&#x3D;1}^{n}\sum_{z_i \in {0,1}}Q_i(z_i,\theta)\ln \frac{P(Y&#x3D;y_i,Z&#x3D;z_i|\theta’)}{Q_i(z_i,\theta)}\\&#x3D;\sum_{i&#x3D;1}^{n}Q_i(0,\theta)\ln \frac{\pi’{10\choose y_i}\ p_A’^{y_i}(1-p_A’)^{10-y_i}}{Q_i(0,\theta)} + \sum_{i&#x3D;1}^{n}Q_i(1,\theta)\ln  \frac{(1-\pi’){10\choose y_i}\ p_B’^{y_i}(1-p_B’)^{10-y_i}}{Q_i(1,\theta)}$$</p><p>对 $\pi$ 求偏导得：</p><p>$$\frac{\partial Q(\theta’; \theta)}{\partial\pi’}&#x3D;\frac{\sum_{i&#x3D;1}^nQ_i(0,\theta)}{\pi’}-\frac{\sum_{i&#x3D;1}^nQ_i(1,\theta)}{1-\pi’}$$</p><p>令导数为 0 得：</p><p>$$\pi’&#x3D;\frac{\sum_{i&#x3D;1}^{n}Q_i(0,\theta)}{n}$$</p><p>同理可得：</p><p>$$p_A’&#x3D;\frac{\sum_{i&#x3D;1}^{n}y_iQ_i(0,\theta)}{\sum_{i&#x3D;1}^{n}10Q_i(0,\theta)}\\p_B’&#x3D;\frac{\sum_{i&#x3D;1}^{n}y_iQ_i(1,\theta)}{\sum_{i&#x3D;1}^{n}10Q_i(1,\theta)}$$</p><p>这就是 M 步的更新公式。</p><p>EM 算法需要我们手动指定一个初值，然后开始迭代，迭代的终止条件为 $||\theta’-\theta|| &lt; \epsilon_1$ 或 $||Q(\theta’; \theta)-Q(\theta; \theta)|| &lt; \epsilon_2$</p><p>指定初值 $\pi&#x3D;0.5$，$p_A&#x3D;0.6$，$p_B&#x3D;0.5$ 的情况下，最终迭代过程如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Iteration 1 pi = 0.597 p_A = 0.713, p_B = 0.581</span><br><span class="line">Iteration 2 pi = 0.591 p_A = 0.733, p_B = 0.555</span><br><span class="line">Iteration 3 pi = 0.582 p_A = 0.752, p_B = 0.532</span><br><span class="line">Iteration 4 pi = 0.572 p_A = 0.767, p_B = 0.516</span><br><span class="line">Iteration 5 pi = 0.564 p_A = 0.777, p_B = 0.509</span><br><span class="line">Iteration 6 pi = 0.556 p_A = 0.783, p_B = 0.506</span><br><span class="line">Iteration 7 pi = 0.550 p_A = 0.786, p_B = 0.506</span><br><span class="line">Iteration 8 pi = 0.545 p_A = 0.788, p_B = 0.507</span><br><span class="line">Iteration 9 pi = 0.541 p_A = 0.789, p_B = 0.508</span><br><span class="line">Iteration 10 pi = 0.538 p_A = 0.790, p_B = 0.509</span><br><span class="line">Iteration 11 pi = 0.535 p_A = 0.791, p_B = 0.510</span><br><span class="line">Iteration 12 pi = 0.533 p_A = 0.791, p_B = 0.510</span><br><span class="line">Iteration 13 pi = 0.531 p_A = 0.792, p_B = 0.511</span><br><span class="line">Iteration 14 pi = 0.529 p_A = 0.792, p_B = 0.512</span><br><span class="line">Iteration 15 pi = 0.528 p_A = 0.792, p_B = 0.512</span><br><span class="line">Iteration 16 pi = 0.527 p_A = 0.792, p_B = 0.512</span><br><span class="line">no update <span class="keyword">in</span> params, stop iteration</span><br><span class="line">Result pi = 0.527 p_A = 0.792, p_B = 0.512</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 虚拟网络模型</title>
      <link>https://blog.howardlau.me/programming/kubernetes/kubernetes-networking-model.html</link>
      <description>
        <![CDATA[<p>在 K8s 的网络模型中，Pod 就像是一个个 Host，拥有独立的 IP，它们之间可以不需要经过 NAT 直接使用对方的 IP 进行通信。一般来说，在同一个机器上的 Pod，我们可以使用 Linux 网桥将不同 Network Namespace]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Sat, 28 Mar 2020 01:01:04 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在 K8s 的网络模型中，Pod 就像是一个个 Host，拥有独立的 IP，它们之间可以不需要经过 NAT 直接使用对方的 IP 进行通信。一般来说，在同一个机器上的 Pod，我们可以使用 Linux 网桥将不同 Network Namespace 的网卡桥接起来，从而使得它们好像处在同一个二层网络一样。但是到了多机的容器互联互通场景，网桥就行不通了。而其实，要使得网络互联互通，只有两种办法：<strong>路由</strong>和<strong>隧道</strong>。</p><p>其实，K8s 的虚拟网络，也可以算作一种 SDN（软件定义网络）。</p><h2 id="Underlay-模型"><a href="#Underlay-模型" class="headerlink" title="Underlay 模型"></a>Underlay 模型</h2><p>Underlay 指的是直接使用底层的物理设施，通过标准<strong>路由</strong>协议如 BGP、OSPF 等设置路由表等转发规则改变网络拓扑，使得 Pod 网络之间可以像正常主机一样直接发送数据包而不用经过虚拟化封装。</p><p>优点：</p><ul><li>减少了额外的封装开销，使得数据包利用率增加</li><li>和传统的网络配置没有区别</li><li>易于调试和排错。</li></ul><p>缺点：</p><ul><li>往往需要网络设备的支持，在公有云上，需要厂商配合设置 VPC 等虚拟网络。</li></ul><p>可以说，Underlay 通过直接设置<strong>路由</strong>的办法使得虚拟网络互联互通。</p><h2 id="Overlay-模型"><a href="#Overlay-模型" class="headerlink" title="Overlay 模型"></a>Overlay 模型</h2><p>Overlay 指的是不修改原有的网络拓扑，而是通过<strong>隧道协议</strong>封装数据包，例如 IPSec、VxLAN、ipip、GRE 等各种封包格式，然后再借助 Underlay 网络将封装后的数据包送达指定的主机，就像是在原来的网络拓扑上覆盖了一层新的网络拓扑一样。</p><p>优点：</p><ul><li>兼容性好，只需要运行容器的主机能互相连通即可，不需要底层网络设备支持</li><li>配置灵活，支持灵活的网络策略</li></ul><p>缺点：</p><ul><li>经过了隧道封装，带来额外开销，数据包利用率下降</li><li>虚拟化的网络包给调试和排错带来一定的困难</li></ul><h2 id="Flannel-VxLAN-模式例子"><a href="#Flannel-VxLAN-模式例子" class="headerlink" title="Flannel VxLAN 模式例子"></a>Flannel VxLAN 模式例子</h2><p>VxLAN 是一种 L2 Overlay 的封装格式，将原来的二层以太网帧使用 UDP 格式封装再发送出去：</p><p><a href="/programming/kubernetes/kubernetes-networking-model/huawei-network-model.png" data-fancybox="gallery" data-caption="VXLAN - Huawei DCN Design Guide - Huawei"><img src="/programming/kubernetes/kubernetes-networking-model/huawei-network-model.png" alt="VXLAN - Huawei DCN Design Guide - Huawei"></a></p><p>首先，Flannel 会在每个主机上创建一个 <code>flannel.&lt;VLANID&gt;</code> 的 VxLAN 网卡用于封装和解封 VxLAN 数据包，通过下面的命令可以查看其详情：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$ip</span> -d <span class="built_in">link</span> show flannel.1</span><br><span class="line">13: flannel.1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default</span><br><span class="line">    <span class="built_in">link</span>/ether da:42:52:a6:68:99 brd ff:ff:ff:ff:ff:ff promiscuity 0</span><br><span class="line">    vxlan <span class="built_in">id</span> 1 <span class="built_in">local</span> 192.168.1.110 dev eno1 srcport 0 0 dstport 8472 nolearning ageing 300 addrgenmode none$ ip -d addr show flannel.1</span><br><span class="line">13: flannel.1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UNKNOWN group default</span><br><span class="line">    <span class="built_in">link</span>/ether da:42:52:a6:68:99 brd ff:ff:ff:ff:ff:ff promiscuity 0</span><br><span class="line">    vxlan <span class="built_in">id</span> 1 <span class="built_in">local</span> 192.168.1.110 dev eno1 srcport 0 0 dstport 8472 nolearning ageing 300</span><br><span class="line">    inet 10.244.1.0/32 scope global flannel.1</span><br><span class="line">       valid_lft forever preferred_lft forever</span><br></pre></td></tr></table></figure><p>可以看到，本机的 VTEP 分配了 <code>10.244.1.0</code> 这个 IP 地址。而 MTU 被设置成了 1450 字节，这是因为 VxLAN 需要额外的 MAC + IP + UDP + VxLAN Header &#x3D; 14 + 20 + 8 + 8 &#x3D; 50 字节的封装。</p><p>而 <code>cni0</code> 网桥则相当于一个本机交换机，带有一个 VLAN：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$ip</span> -d <span class="built_in">link</span> show cni0</span><br><span class="line">14: cni0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UP mode DEFAULT group default qlen 1000</span><br><span class="line">    <span class="built_in">link</span>/ether 1a:9b:9d:6d:4f:f2 brd ff:ff:ff:ff:ff:ff promiscuity 0</span><br><span class="line">    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q addrgenmode eui64$ ip -d addr show cni0</span><br><span class="line">14: cni0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UP group default qlen 1000</span><br><span class="line">    <span class="built_in">link</span>/ether 1a:9b:9d:6d:4f:f2 brd ff:ff:ff:ff:ff:ff promiscuity 0</span><br><span class="line">    bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q</span><br><span class="line">    inet 10.244.1.1/24 scope global cni0</span><br><span class="line">       valid_lft forever preferred_lft forever</span><br><span class="line">    inet6 fe80::189b:9dff:fe6d:4ff2/64 scope <span class="built_in">link</span></span><br><span class="line">       valid_lft forever preferred_lft forever</span><br></pre></td></tr></table></figure><p>查看路由表可以发现，同一个主机上的 Pod 直接使用 <code>cni0</code> 网桥通信，而如果要跨机通信，则需要通过 Flannel 网卡进行隧道转发到对应主机上的 Flannel VxLAN 网卡：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$ip</span> route show</span><br><span class="line">...</span><br><span class="line">10.244.0.0/24 via 10.244.0.0 dev flannel.1 onlink</span><br><span class="line">10.244.1.0/24 dev cni0  proto kernel  scope <span class="built_in">link</span>  src 10.244.1.1</span><br><span class="line">10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink</span><br><span class="line">10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink</span><br><span class="line">10.244.4.0/24 via 10.244.4.0 dev flannel.1 onlink</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>那么问题来了，例如本机需要发送一个到 <code>10.244.2.233</code> 的数据包，经过路由表发现需要发送给网关 <code>10.244.2.0</code>。毫无疑问，这时候主机需要知道 <code>10.244.2.0</code> 的 MAC 地址，此时相当于发生了一次 <code>l3miss</code>，主机会尝试发送 ARP 来获取 <code>10.244.2.0</code> 的 MAC 地址。当然，为了避免广播风暴，Flannel 会提前在 ARP 表中写好对端 VTEP 的 MAC 地址：</p><figure class="highlight plaintext"><figcaption><span>ip neigh</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">10.244.1.57 dev cni0 lladdr 1e:fd:14:d0:08:51 REACHABLE</span><br><span class="line">10.244.3.0 dev flannel.1 lladdr 56:34:8f:23:99:30 PERMANENT</span><br><span class="line">10.244.7.0 dev flannel.1 lladdr ea:60:1e:c4:3d:6f PERMANENT</span><br><span class="line">10.244.5.0 dev flannel.1 lladdr d6:ea:dd:2b:cc:86 PERMANENT</span><br><span class="line">10.244.1.47 dev cni0 lladdr b6:02:9a:fd:b9:a9 REACHABLE</span><br><span class="line">10.244.1.60 dev cni0 lladdr 4a:25:01:b7:a0:b2 REACHABLE</span><br><span class="line">10.244.2.0 dev flannel.1 lladdr aa:63:8e:fa:63:4e PERMANENT</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>知道了 MAC 地址之后，还有一个问题：这个 MAC 地址应该发送到哪个主机上呢，我要从哪个端口发送数据包，用哪个 IP 地址封装？此时，相当于发生了一次 <code>l2miss</code>，如果是交换机，就会尝试泛洪（Flooding），同样为了避免广播，Flannel 会提前在 FDB （相当于交换机的 MAC 地址表）写好对应的主机 IP：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">$ bridge fdb show dev flannel.1</span><br><span class="line">...</span><br><span class="line">fa:80:e7:b1:b9:95 dst 192.168.1.117 self permanent</span><br><span class="line">52:3a:7c:<span class="built_in">fc</span>:04:ce dst 192.168.1.113 self permanent</span><br><span class="line">ea:3a:36:4a:d0:f5 dst 192.168.1.114 self permanent</span><br><span class="line">d6:ea:<span class="built_in">dd</span>:2b:cc:86 dst 192.168.1.119 self permanent</span><br><span class="line">ea:60:1e:c4:3d:6f dst 192.168.1.115 self permanent</span><br><span class="line">aa:63:8e:fa:63:4e dst 192.168.1.111 self permanent</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>于是就用 <code>192.168.1.111</code> 作为对端地址封装好 VxLAN 数据包，主机再通过物理网卡，按照正常的流程将 UDP 发送到对端主机。而对端主机收到 VxLAN 数据包后，首先解封装，得到原始二层数据包之后，按照路由表，转发到自己的 <code>cni0</code> 网桥中，网桥转发给对应的 Pod，也就完成了一次通信。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 自建 Storage Class</title>
      <link>https://blog.howardlau.me/programming/kubernetes/managing-own-kubernetes-storage-class.html</link>
      <description>
        <![CDATA[<p>前面提过 Kubernetes 中的存储通过 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 来实现，但是也提到了 PersistentVolume]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Mon, 16 Mar 2020 07:22:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>前面提过 Kubernetes 中的存储通过 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 来实现，但是也提到了 PersistentVolume 需要手动创建，实际使用上较为不便。</p><p>一般来说，在使用云服务的 Kubernetes 集群服务的时候，云服务会顺带提供动态存储服务，我们只需要使用 StorageClass 去申请即可。而这种通过 StorageClass 申请存储，背后是有 Provisioner 来支撑的。也就是说，如果我们自己部署一个 Provisioner，就能动态申请存储，不需要再手动创建 PV 资源了。而 Provisioner 做的工作就是在存储系统上划分出一块区域给应用挂载而已。</p><h1 id="配置-NFS-Provisioner"><a href="#配置-NFS-Provisioner" class="headerlink" title="配置 NFS Provisioner"></a>配置 NFS Provisioner</h1><p>因为我们已经有一台 NFS 服务器了，所以直接配置客户端即可：</p><p><a href="https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client">https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client</a></p><p>参考上面这个链接配置，将 IP、路径、名字什么的改改就可以用了。</p><p>之后如果需要使用的话，StorageClass 的名字填 <code>managed-nfs-storage</code> ，如果需要手动写 PersistentVolumeClaim，就在 metadata 里写：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">annotations:</span></span><br><span class="line">    <span class="attr">volume.beta.kubernetes.io/storage-class:</span> <span class="string">&quot;managed-nfs-storage&quot;</span></span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | Kubernetes 中应用服务暴露的原理</title>
      <link>https://blog.howardlau.me/programming/kubernetes/publishing-services.html</link>
      <description>
        <![CDATA[<p>本篇博文主要研究的是 iptables 下的 K8s 服务暴露原理，下面的每一种暴露方式是<strong>层层递进</strong>的，位于下面的暴露方式依赖上面的方式。只要是集群节点都可以使用任意一种方式来访问服务（只要存在）</p>
<h2]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Mon, 16 Mar 2020 07:18:15 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>本篇博文主要研究的是 iptables 下的 K8s 服务暴露原理，下面的每一种暴露方式是<strong>层层递进</strong>的，位于下面的暴露方式依赖上面的方式。只要是集群节点都可以使用任意一种方式来访问服务（只要存在）</p><h2 id="iptables"><a href="#iptables" class="headerlink" title="iptables"></a>iptables</h2><p>NAT 表入口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">-A PREROUTING -m comment --comment <span class="string">&quot;kubernetes service portals&quot;</span> -j KUBE-SERVICES</span><br><span class="line">-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER</span><br><span class="line">-A OUTPUT -m comment --comment <span class="string">&quot;kubernetes service portals&quot;</span> -j KUBE-SERVICES</span><br><span class="line">-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER</span><br><span class="line">-A POSTROUTING -m comment --comment <span class="string">&quot;kubernetes postrouting rules&quot;</span> -j KUBE-POSTROUTING</span><br><span class="line">-A POSTROUTING -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN</span><br><span class="line">-A POSTROUTING -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE</span><br><span class="line">-A POSTROUTING ! -s 10.244.0.0/16 -d 10.244.0.0/24 -j RETURN</span><br><span class="line">-A POSTROUTING ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE</span><br><span class="line"> </span><br><span class="line">-A KUBE-POSTROUTING -m comment --comment <span class="string">&quot;kubernetes service traffic requiring SNAT&quot;</span> -m mark --mark 0x4000/0x4000 -j MASQUERADE</span><br></pre></td></tr></table></figure><p>FILTER 表：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">-A INPUT -m conntrack --ctstate NEW -m comment --comment <span class="string">&quot;kubernetes service portals&quot;</span> -j KUBE-SERVICES</span><br><span class="line">-A INPUT -m conntrack --ctstate NEW -m comment --comment <span class="string">&quot;kubernetes externally-visible service portals&quot;</span> -j KUBE-EXTERNAL-SERVICES</span><br><span class="line">-A INPUT -j KUBE-FIREWALL</span><br><span class="line">-A FORWARD -m comment --comment <span class="string">&quot;kubernetes forwarding rules&quot;</span> -j KUBE-FORWARD</span><br><span class="line">-A FORWARD -m conntrack --ctstate NEW -m comment --comment <span class="string">&quot;kubernetes service portals&quot;</span> -j KUBE-SERVICES</span><br><span class="line">-A OUTPUT -m conntrack --ctstate NEW -m comment --comment <span class="string">&quot;kubernetes service portals&quot;</span> -j KUBE-SERVICES</span><br><span class="line">-A OUTPUT -j KUBE-FIREWALL</span><br><span class="line"> </span><br><span class="line">-A FORWARD -s 10.244.0.0/16 -j ACCEPT</span><br><span class="line">-A FORWARD -d 10.244.0.0/16 -j ACCEPT</span><br><span class="line"> </span><br><span class="line">-A KUBE-FIREWALL -m comment --comment <span class="string">&quot;kubernetes firewall for dropping marked packets&quot;</span> -m mark --mark 0x8000/0x8000 -j DROP</span><br><span class="line">-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP</span><br><span class="line">-A KUBE-FORWARD -m comment --comment <span class="string">&quot;kubernetes forwarding rules&quot;</span> -m mark --mark 0x4000/0x4000 -j ACCEPT</span><br><span class="line">-A KUBE-FORWARD -s 10.244.0.0/16 -m comment --comment <span class="string">&quot;kubernetes forwarding conntrack pod source rule&quot;</span> -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT</span><br><span class="line">-A KUBE-FORWARD -d 10.244.0.0/16 -m comment --comment <span class="string">&quot;kubernetes forwarding conntrack pod destination rule&quot;</span> -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT</span><br><span class="line">-A KUBE-SERVICES -d 10.100.18.91/32 -p tcp -m comment --comment <span class="string">&quot;judge-system/judge-system-4-server-master-service: has no endpoints&quot;</span> -m tcp --dport 80 -j REJECT --reject-with icmp-port-unreachable</span><br><span class="line">-A KUBE-SERVICES -d 10.109.222.26/32 -p tcp -m comment --comment <span class="string">&quot;judge-system/judge-system-4-server-development-service: has no endpoints&quot;</span> -m tcp --dport 80 -j REJECT --reject-with icmp-port-unreachable</span><br><span class="line">-A KUBE-SERVICES -d 10.109.183.27/32 -p tcp -m comment --comment <span class="string">&quot;jhub/hub: has no endpoints&quot;</span> -m tcp --dport 8081 -j REJECT --reject-with icmp-port-unreachable</span><br></pre></td></tr></table></figure><p>可以看到，对于一些没有 Endpoint 存在的 Service，会在 FILTER 表直接 REJECT，而其余的会进入后续的 CHAIN 进行进一步 NAT 处理。</p><h2 id="Pod-IP"><a href="#Pod-IP" class="headerlink" title="Pod IP"></a>Pod IP</h2><p>Pod 一旦创建就存在，相当于一个虚拟机的 IP，可以进行 ping 等操作，其实现是通过<strong>网络插件+路由表</strong>（我们用的 flannel 就是 VxLAN 技术创建了覆盖网络）来将流量转发到正确的容器中。</p><p>IP 不固定，而且没有负载均衡。如果从 Pod 外访问 Pod IP，需要做 SNAT：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-A POSTROUTING ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE</span><br></pre></td></tr></table></figure><h2 id="Cluster-IP"><a href="#Cluster-IP" class="headerlink" title="Cluster IP"></a>Cluster IP</h2><p>创建 Service 后就存在，通过 iptables 或者 ipvs 实现，只能转发 TCP 或 UDP 的指定端口（yaml 中规定的），最终会将 Cluster IP 的流量转发到某一个 Pod IP。</p><p>如果使用了 iptables 那么规则大概长下面这样，就是匹配 IP+端口 组合，然后转发</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.96.7.210/32 -p tcp -m comment --comment <span class="string">&quot;redis/redis:client cluster IP&quot;</span> -m tcp --dport 6379 -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-SERVICES -d 10.96.7.210/32 -p tcp -m comment --comment <span class="string">&quot;redis/redis:client cluster IP&quot;</span> -m tcp --dport 6379 -j KUBE-SVC-X65RDOJUUA3VPRY4</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -m statistic --mode random --probability 0.16667000018 -j KUBE-SEP-MWI2C5QF6IXJQU63</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-FXRXESSRETWGI65E</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-66HFVIWNCINZQPZR</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-ZH23ZMDYUP2DO5GI</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-BAPRIFMT5YYOA3WI</span><br><span class="line">-A KUBE-SVC-X65RDOJUUA3VPRY4 -j KUBE-SEP-ILU3WXFIDU2MOAD4</span><br><span class="line"> </span><br><span class="line"><span class="comment"># 下面就是将 Cluster IP DNAT 为 Pod IP</span></span><br><span class="line">-A KUBE-SEP-BAPRIFMT5YYOA3WI -s 10.244.5.216/32 -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-SEP-BAPRIFMT5YYOA3WI -p tcp -m tcp -j DNAT --to-destination 10.244.5.216:6379</span><br><span class="line"><span class="comment"># ... 省略了</span></span><br></pre></td></tr></table></figure><p>如果需要将服务暴露给集群以外的机器，可以使用下面的服务暴露方式，原理仍然是设置对应的 iptables</p><h2 id="Node-Port"><a href="#Node-Port" class="headerlink" title="Node Port"></a>Node Port</h2><p>在每个机器上都会打开一个端口，然后将这个端口的流量转发到对应的 Service：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-A KUBE-NODEPORTS -p tcp -m comment --comment <span class="string">&quot;rabbitmq/rabbitmq:http&quot;</span> -m tcp --dport 30376 -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-NODEPORTS -p tcp -m comment --comment <span class="string">&quot;rabbitmq/rabbitmq:http&quot;</span> -m tcp --dport 30376 -j KUBE-SVC-35VHXOLOORONIWYJ</span><br></pre></td></tr></table></figure><h2 id="External-IP（Load-Balancer）"><a href="#External-IP（Load-Balancer）" class="headerlink" title="External IP（Load Balancer）"></a>External IP（Load Balancer）</h2><p>LoadBalancer 类型的 Service 可以指定 External IP，这也是云服务商的云控制器原理。如果自建集群，可以手动指定 External IP，然后想办法使得 IP 包路由到机器上，或者参考<a href="https://howardlau.me/programming/kubernetes/kubernetes-load-balancer.html">这篇博文</a>来自建一个 Load Balancer 控制器。</p><p>iptables 规则跟 Cluster IP 类似：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">-A KUBE-SERVICES -d 192.168.15.8/32 -p tcp -m comment --comment <span class="string">&quot;seaweedfs/filer:http loadbalancer IP&quot;</span> -m tcp --dport 80 -j KUBE-FW-BRYNYVZ5Q2DDPJIY</span><br><span class="line"> </span><br><span class="line">-A KUBE-FW-BRYNYVZ5Q2DDPJIY -m comment --comment <span class="string">&quot;seaweedfs/filer:http loadbalancer IP&quot;</span> -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-FW-BRYNYVZ5Q2DDPJIY -m comment --comment <span class="string">&quot;seaweedfs/filer:http loadbalancer IP&quot;</span> -j KUBE-SVC-BRYNYVZ5Q2DDPJIY</span><br><span class="line">-A KUBE-FW-BRYNYVZ5Q2DDPJIY -m comment --comment <span class="string">&quot;seaweedfs/filer:http loadbalancer IP&quot;</span> -j KUBE-MARK-DROP</span><br></pre></td></tr></table></figure><p>如果指定了 trafficPolicy 为 local，不会做 SNAT，而且在本机没有 Pod 的情况下，会 Drop 掉转发流量。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">-A KUBE-SERVICES -d 192.168.15.1/32 -p tcp -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -m tcp --dport 80 -j KUBE-FW-VZFKBL2WWL2EQZ6G</span><br><span class="line"> </span><br><span class="line">-A KUBE-FW-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -j KUBE-XLB-VZFKBL2WWL2EQZ6G</span><br><span class="line">-A KUBE-FW-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -j KUBE-MARK-DROP</span><br><span class="line"> </span><br><span class="line">-A KUBE-XLB-OZONKQKEZE5YW3UK -m comment --comment <span class="string">&quot;masquerade LOCAL traffic for kube-system/traefik-load-balance-service:https LB IP&quot;</span> -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-XLB-OZONKQKEZE5YW3UK -m comment --comment <span class="string">&quot;route LOCAL traffic for kube-system/traefik-load-balance-service:https LB IP to service chain&quot;</span> -m addrtype --src-type LOCAL -j KUBE-SVC-OZONKQKEZE5YW3UK</span><br><span class="line">-A KUBE-XLB-OZONKQKEZE5YW3UK -s 10.244.0.0/16 -m comment --comment <span class="string">&quot;Redirect pods trying to reach external loadbalancer VIP to clusterIP&quot;</span> -j KUBE-SVC-OZONKQKEZE5YW3UK</span><br><span class="line">-A KUBE-XLB-OZONKQKEZE5YW3UK -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:https has no local endpoints&quot;</span> -j KUBE-MARK-DROP</span><br></pre></td></tr></table></figure><p>有 Pod 则不会 Drop：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">-A KUBE-SERVICES -d 192.168.15.1/32 -p tcp -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -m tcp --dport 80 -j KUBE-FW-VZFKBL2WWL2EQZ6G</span><br><span class="line"> </span><br><span class="line">-A KUBE-FW-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -j KUBE-XLB-VZFKBL2WWL2EQZ6G</span><br><span class="line">-A KUBE-FW-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;kube-system/traefik-load-balance-service:web loadbalancer IP&quot;</span> -j KUBE-MARK-DROP</span><br><span class="line"> </span><br><span class="line">-A KUBE-XLB-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;masquerade LOCAL traffic for kube-system/traefik-load-balance-service:web LB IP&quot;</span> -m addrtype --src-type LOCAL -j KUBE-MARK-MASQ</span><br><span class="line">-A KUBE-XLB-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;route LOCAL traffic for kube-system/traefik-load-balance-service:web LB IP to service chain&quot;</span> -m addrtype --src-type LOCAL -j KUBE-SVC-VZFKBL2WWL2EQZ6G</span><br><span class="line">-A KUBE-XLB-VZFKBL2WWL2EQZ6G -s 10.244.0.0/16 -m comment --comment <span class="string">&quot;Redirect pods trying to reach external loadbalancer VIP to clusterIP&quot;</span> -j KUBE-SVC-VZFKBL2WWL2EQZ6G</span><br><span class="line">-A KUBE-XLB-VZFKBL2WWL2EQZ6G -m comment --comment <span class="string">&quot;Balancing rule 0 for kube-system/traefik-load-balance-service:web&quot;</span> -j KUBE-SEP-EUB52TCTNKDY6XP4</span><br></pre></td></tr></table></figure><h2 id="Host-Port"><a href="#Host-Port" class="headerlink" title="Host Port"></a>Host Port</h2><p>直接在 Host 上占用掉 Pod 要占用的端口，会加入规则做 NAT。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 自建 Kubernetes Load Balancer</title>
      <link>https://blog.howardlau.me/programming/kubernetes/kubernetes-load-balancer.html</link>
      <description>
        <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Mon, 16 Mar 2020 07:08:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="写在前面"><a href="#写在前面" class="headerlink" title="写在前面"></a>写在前面</h2><p>对于负载均衡，我们有很多种办法。</p><p>负载均衡实际上包含了两个方面：健康探测以及流量分担。如果我们不能健康探测而将流量错误地分到宕机上，会导致流量黑洞。</p><p>首先最豪华的解决方案就是硬件负载均衡器，但是一般我们买不起。</p><p>其次是路由器 NAT Pool 地址池转换，不过这种不靠谱，因为这种就是简单地将流量分发到服务器上，万一服务器掉线了需要人工去改地址池，也就是缺少健康探测的功能。</p><p>我们也可以通过 ipvs 找一台服务器来进行负载均衡，这也是 K8s 集群内部的负载均衡办法，但是这样还是给转发的那台机器带来不必要的负担。</p><p>最后一种办法是在路由器上配置等价路由，由路由器进行负载均衡，这样其实和 NAT Pool 缺点是一样的，不过，我们有大杀器——动态路由协议可以帮我们搞定这个问题。</p><p>所以我们最后实现了这么一种迂回的办法，就是让机器发布路由信息到路由器上，然后由路由器选择机器进行转发，同时每台机器也及时监控其他机器，一旦发现宕机，则更新路由信息，避免路由器将流量转发到宕机上。</p><p>理解 MetalLB 的原理需要对 K8s 控制原理以及计算机网络有着比较深入的理解。但理解了之后，配置的东西其实相当简单。</p><h2 id="Load-Balancer-原理"><a href="#Load-Balancer-原理" class="headerlink" title="Load Balancer 原理"></a>Load Balancer 原理</h2><p>如果我们使用 GKE、AWS 等公有云，那么一般来说我们给 Service 的 Type 写成 LoadBalancer，云服务厂商就会分配一个 External IP 给我们的 Service，我们只需要访问这个 External IP，其内部就能自动均衡我们的流量到各个实例上。然而作为穷鬼的我们，没有金钱享受如此高端的服务，开源版 Kubernetes 是不会给我们的 LoadBalancer Service 分配 External IP 的。难道穷鬼就不配用负载均衡了吗！！</p><p>幸好，即使没有高端的负载均衡器，我们也有开源社区的支持。<a href="https://github.com/danderson/metallb">metallb</a> 就是一款开源的 K8s 负载均衡控制器，只要装上，我们的 Service 就可以拿到 IP 了。如果你对 K8s 已经比较熟悉，那么就知道，所谓 LoadBalancer Service 无非也是一种资源，只是我们缺少一个合适的控制器来为其分配 IP 而已。那么很显然，我们只要装一个分配器，就能得到一个 IP。但是，这个 IP 从哪来？显然只能我们手动分配一段 IP 池让他随便分了。然而其他电脑怎么知道这个 IP 池就是我们的服务所在？如果你熟悉计算机网络，可能会想到 BGP、OSPF 等路由协议。只要在分配好 IP 之后，把这个 IP 路由信息广播出去，那么其他电脑就能知道这个 IP 在哪里。</p><p>最简单的，我们可以在服务器的内部子网里找没用的 IP，然后等其他电脑访问这个 IP 的时候，我想办法回应一个 ARP 包，其他电脑就知道这个 IP 在哪里可以通信了，尽管这个 IP 其实没有绑定到任何网卡上，可能只是 iptables 里的一条记录。</p><p>然而，通过 ARP 广播的方式局限性非常大，分配的 IP 只能和服务器其他 IP 位于同一子网，但其实对于我们这种小型集群已经够了，但是这不够 geek！有没有高端一点的办法？</p><p>有！那就是 BGP。BGP 的原理比较复杂，简单来说就是运行 BGP 的设备之间可以交换路由信息，我们可以将自己的 IP 段通过 BGP 协议告诉其他设备，这样其他设备就能正确的路由数据包到服务器上了。BGP 需要路由器的支持，好在我们的路由器是支持的。BGP 尽管更复杂了一些，但是在 IP 段的选取上有更大的灵活性。尽管负载均衡器原理十分复杂，配置却很简单。</p><h2 id="安装控制器"><a href="#安装控制器" class="headerlink" title="安装控制器"></a>安装控制器</h2><p>请根据自己的需要选择对应的版本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml</span><br></pre></td></tr></table></figure><h2 id="配置路由器"><a href="#配置路由器" class="headerlink" title="配置路由器"></a>配置路由器</h2><p>在配置控制器之前，我们需要配置路由器的 BGP 功能，简单来说就是给路由自己分配一个 AS 号，然后给服务器分配一个 AS 号就可以了，然后声明邻居，使路由器能从服务器获取 BGP 信息。AS 号和 IP 地址一样，也是有私有段的，我们用的就是私有的 AS 号。（注：此处路由器型号为华为 AR-101S，其他路由器请参考配置手册配置 BGP 功能）</p><p>路由器的 AS 号是 65315，集群 AS 号是 65199。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">[Huawei] bgp 65315 <span class="comment"># 打开路由器 BGP 功能，分配 65315 AS 号</span></span><br><span class="line">[Huawei-bgp] group servers external <span class="comment"># 声明外部邻居组</span></span><br><span class="line">[Huawei-bgp] peer servers as-number 65199 <span class="comment"># 声明组 AS 号为 65199</span></span><br><span class="line">[Huawei-bgp] peer 192.168.1.110 group servers <span class="comment"># 声明组内邻居</span></span><br><span class="line">[Huawei-bgp] peer 192.168.1.111 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.113 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.114 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.115 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.116 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.117 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.118 group servers</span><br><span class="line">[Huawei-bgp] peer 192.168.1.119 group servers</span><br><span class="line">[Huawei-bgp] maximum load-balancing 4 <span class="comment"># 默认情况下，路由只会从学习到的 BGP 路由信息中选取最佳的 1 条写进路由表，而只有路由表中同一目的地址含有多个等价下一条才能实现负载均衡。所以，我们要指示 BGP 将多条路由同时写进路由表，我们的路由器型号最大只支持 4</span></span><br></pre></td></tr></table></figure><p>到这里路由器就配置完了。</p><h2 id="配置控制器"><a href="#配置控制器" class="headerlink" title="配置控制器"></a>配置控制器</h2><p>直接应用下面的 ConfigMap：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ConfigMap</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">metallb-system</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">config</span></span><br><span class="line"><span class="attr">data:</span></span><br><span class="line">  <span class="attr">config:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    peers:</span></span><br><span class="line"><span class="string">    - peer-address: 192.168.1.1 # 路由器的 IP 地址</span></span><br><span class="line"><span class="string">      peer-asn: 65315 # 路由器的 AS 号</span></span><br><span class="line"><span class="string">      my-asn: 65199 # 集群 AS 号</span></span><br><span class="line"><span class="string">    address-pools:</span></span><br><span class="line"><span class="string">    - name: default</span></span><br><span class="line"><span class="string">      protocol: bgp</span></span><br><span class="line"><span class="string">      addresses:</span></span><br><span class="line"><span class="string">      - 192.168.15.0/24 # 希望分配的 IP 地址池，到时候会分配给 Service 的 ExternalIP</span></span><br></pre></td></tr></table></figure><h2 id="检查负载均衡是否生效"><a href="#检查负载均衡是否生效" class="headerlink" title="检查负载均衡是否生效"></a>检查负载均衡是否生效</h2><p>首先将一个 Service 改成 LoadBalancer，然后查看是否分配到了 External IP：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ kubectl get svc traefik-load-balance-service -n kube-system</span><br><span class="line">NAME                           TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)                      AGE</span><br><span class="line">traefik-load-balance-service   LoadBalancer   10.97.104.52   192.168.15.1   80:32703/TCP,443:32237/TCP   13h</span><br></pre></td></tr></table></figure><p>可以看到确实分配到了一个我们地址池中的 IP。</p><p>然后检查路由是否学习到了 BGP 路由表：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">&lt;Huawei&gt;display bgp routing-table</span><br><span class="line"> </span><br><span class="line"> BGP Local router ID is 192.168.1.1</span><br><span class="line"> Status codes: * - valid, &gt; - best, d - damped,</span><br><span class="line">               h - <span class="built_in">history</span>,  i - internal, s - suppressed, S - Stale</span><br><span class="line">               Origin : i - IGP, e - EGP, ? - incomplete</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> Total Number of Routes: 8</span><br><span class="line">      Network            NextHop        MED        LocPrf    PrefVal Path/Ogn</span><br><span class="line"> </span><br><span class="line"> *&gt;   192.168.15.1/32    192.168.1.110                         0      65199?</span><br><span class="line"> *                       192.168.1.111                         0      65199?</span><br><span class="line"> *                       192.168.1.114                         0      65199?</span><br><span class="line"> *                       192.168.1.115                         0      65199?</span><br><span class="line"> *                       192.168.1.116                         0      65199?</span><br><span class="line"> *                       192.168.1.117                         0      65199?</span><br><span class="line"> *                       192.168.1.118                         0      65199?</span><br><span class="line"> *                       192.168.1.119                         0      65199?</span><br></pre></td></tr></table></figure><p>可以看到路由器已经收到来自集群的 BGP 信息。</p><p>然后检查路由器路由表，是否有多个下一跳：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">&lt;Huawei&gt;display ip routing-table</span><br><span class="line">Route Flags: R - relay, D - download to fib</span><br><span class="line">------------------------------------------------------------------------------</span><br><span class="line">Routing Tables: Public</span><br><span class="line">         Destinations : 12       Routes : 15</span><br><span class="line"> </span><br><span class="line">Destination/Mask    Proto   Pre  Cost      Flags NextHop         Interface</span><br><span class="line"> </span><br><span class="line">        0.0.0.0/0   Static  60   0           D   222.200.180.254 GigabitEthernet0/0/4</span><br><span class="line">      127.0.0.0/8   Direct  0    0           D   127.0.0.1       InLoopBack0</span><br><span class="line">      127.0.0.1/32  Direct  0    0           D   127.0.0.1       InLoopBack0</span><br><span class="line">127.255.255.255/32  Direct  0    0           D   127.0.0.1       InLoopBack0</span><br><span class="line">    192.168.1.0/24  Direct  0    0           D   192.168.1.1     Vlanif1</span><br><span class="line">    192.168.1.1/32  Direct  0    0           D   127.0.0.1       Vlanif1</span><br><span class="line">  192.168.1.255/32  Direct  0    0           D   127.0.0.1       Vlanif1</span><br><span class="line">   192.168.15.1/32  EBGP    255  0           D   192.168.1.110   Vlanif1</span><br><span class="line">                    EBGP    255  0           D   192.168.1.111   Vlanif1</span><br><span class="line">                    EBGP    255  0           D   192.168.1.114   Vlanif1</span><br><span class="line">                    EBGP    255  0           D   192.168.1.115   Vlanif1</span><br><span class="line">  222.200.180.0/24  Direct  0    0           D   222.200.180.45  GigabitEthernet0/0/4</span><br><span class="line"> 222.200.180.45/32  Direct  0    0           D   127.0.0.1       GigabitEthernet0/0/4</span><br><span class="line">222.200.180.255/32  Direct  0    0           D   127.0.0.1       GigabitEthernet0/0/4</span><br><span class="line">255.255.255.255/32  Direct  0    0           D   127.0.0.1       InLoopBack0</span><br></pre></td></tr></table></figure><p>针对 192.168.15.1，路由表被应用了多个下一跳，因此可以负载均衡。</p><p>最后查看 FIB 转发表：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">&lt;Huawei&gt;display fib</span><br><span class="line">Route Flags: G - Gateway Route, H - Host Route,    U - Up Route</span><br><span class="line">             S - Static Route,  D - Dynamic Route, B - Black Hole Route</span><br><span class="line">             L - Vlink Route</span><br><span class="line">--------------------------------------------------------------------------------</span><br><span class="line"> FIB Table:</span><br><span class="line"> Total number of Routes : 15</span><br><span class="line"> </span><br><span class="line">Destination/Mask   Nexthop         Flag  TimeStamp     Interface      TunnelID</span><br><span class="line">192.168.15.1/32    192.168.1.110   DGHU  t[5340582]    Vlanif1        0x0</span><br><span class="line">192.168.15.1/32    192.168.1.111   DGHU  t[5340582]    Vlanif1        0x0</span><br><span class="line">192.168.15.1/32    192.168.1.114   DGHU  t[5340582]    Vlanif1        0x0</span><br><span class="line">192.168.15.1/32    192.168.1.115   DGHU  t[5340582]    Vlanif1        0x0</span><br><span class="line">222.200.180.255/32 127.0.0.1       HU    t[611645]     InLoop0        0x0</span><br><span class="line">222.200.180.45/32  127.0.0.1       HU    t[611645]     InLoop0        0x0</span><br><span class="line">192.168.1.255/32   127.0.0.1       HU    t[201]        InLoop0        0x0</span><br><span class="line">192.168.1.1/32     127.0.0.1       HU    t[201]        InLoop0        0x0</span><br><span class="line">255.255.255.255/32 127.0.0.1       HU    t[79]         InLoop0        0x0</span><br><span class="line">127.255.255.255/32 127.0.0.1       HU    t[79]         InLoop0        0x0</span><br><span class="line">127.0.0.1/32       127.0.0.1       HU    t[79]         InLoop0        0x0</span><br><span class="line">127.0.0.0/8        127.0.0.1       U     t[79]         InLoop0        0x0</span><br><span class="line">192.168.1.0/24     192.168.1.1     U     t[201]        Vlanif1        0x0</span><br><span class="line">222.200.180.0/24   222.200.180.45  U     t[611645]     GE0/0/4        0x0</span><br><span class="line">0.0.0.0/0          222.200.180.254 GSU   t[611645]     GE0/0/4        0x0</span><br></pre></td></tr></table></figure><p>可以看到路由器在路由表的指导下，为 192.168.15.1 分配了多个下一跳。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | kubeadm 部署原理简介</title>
      <link>https://blog.howardlau.me/programming/kubernetes/kubernetes-in-docker.html</link>
      <description>
        <![CDATA[<p>通常来说，运行 Kubernetes 集群，需要 <code>kubelet</code>、<code>kube-apiserver</code>、<code>etcd</code>、<code>kube-scheduler</code> 以及]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Mon, 16 Mar 2020 06:54:33 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>通常来说，运行 Kubernetes 集群，需要 <code>kubelet</code>、<code>kube-apiserver</code>、<code>etcd</code>、<code>kube-scheduler</code> 以及 <code>kube-controller-manager</code> 等组件。对于手工部署集群而言，我们需要生成证书，生成配置文件等，然后在不同机器上安装二进制文件，并设置 <code>systemd</code> 服务。</p><p>为了方便部署，K8s 官方开发了 <code>kubeadm</code> 工具来辅助我们的安装过程，运行原理如下：</p><ol><li><code>kubeadm</code> 首先在 Master 节点上生成 API PKI 和 etcd PKI 所需的证书，然后配置必要的 Static Pod，Master 节点启动后，会将必要的配置信息写入集群 <code>etcd</code> 里，当其他节点加入集群的时候，就从集群读取模板配置文件，申请签发自己的证书，然后写到机器上。</li><li>所有节点的 <code>kubelet</code> 仍然以 <code>systemd</code> 服务形式存在。</li><li>在 Master 节点上，<code>kubelet</code> 被配置了 <code>kube-apiserver</code>、<code>etcd</code>、<code>kube-controller-manager</code> 以及 <code>kube-scheduler</code> 四个 <strong>Static Pod</strong>。Static Pod 不受调度器控制，只要 <code>kubelet</code> 服务运行，就会启动这些 Pod。相关配置文件可以在 <code>/etc/kubernetes/manifests</code> 下找到。所有的 Master Pod 默认都是使用 Host Network，会占用端口。<ol><li><code>etcd</code> 是 Kubernetes 控制平面的持久化存储，集群的所有配置都依赖 <code>etcd</code> 来存储，万一 <code>etcd</code> 服务失败，那么集群就不可用了。</li><li><code>kube-apiserver</code> 是 Kubernetes API，用来提供 REST API 以及鉴权，以达到 <code>kubectl</code> 对集群进行 CRUD 操作的目的，其他机器上的 <code>kubelet</code> 也会通过它来上报信息。</li><li><code>kube-controller-manager</code> 负责集群控制器的管理（每一个资源都有对应的控制器来监控集群状态并进行 <code>reconcile</code> 操作）</li><li><code>kube-scheduler</code> 负责集群中 Pod 的调度。</li></ol></li><li>K8s 中对于 API 的通信采用了自建 PKI 以达到可信通信的目的，CA 证书等可以在 <code>/etc/kubernetes/pki</code> 下找到。证书中写死了 SAN，如果遇到机器更换 IP 或主机名的情况，需要重新签发证书（换端口不需要）。<ol><li>修改 <code>kubeadm.config</code> （参考下面），加入需要额外添加的 SAN，本机的 hostname 和 IP 会自己添加，不需要额外填写。</li><li>运行 <code>sudo kubeadm init phase upload-certs --upload-certs</code> 重新生成证书。</li></ol></li></ol><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiServer:</span></span><br><span class="line">  <span class="attr">certSANs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;kube-master&quot;</span></span><br><span class="line">  <span class="attr">extraArgs:</span></span><br><span class="line">    <span class="attr">authorization-mode:</span> <span class="string">Node,RBAC</span></span><br><span class="line">  <span class="attr">timeoutForControlPlane:</span> <span class="string">4m0s</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">kubeadm.k8s.io/v1beta2</span></span><br><span class="line"><span class="attr">certificatesDir:</span> <span class="string">/etc/kubernetes/pki</span></span><br><span class="line"><span class="attr">controlPlaneEndpoint:</span> <span class="string">&quot;192.168.1.103:8443&quot;</span></span><br><span class="line"><span class="attr">clusterName:</span> <span class="string">kubernetes</span></span><br><span class="line"><span class="attr">controllerManager:</span> &#123;&#125;</span><br><span class="line"><span class="attr">dns:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">CoreDNS</span></span><br><span class="line"><span class="attr">etcd:</span></span><br><span class="line">  <span class="attr">local:</span></span><br><span class="line">    <span class="attr">serverCertSANs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.103</span></span><br><span class="line">    <span class="attr">peerCertSANs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.103</span></span><br><span class="line">    <span class="attr">dataDir:</span> <span class="string">/var/lib/etcd</span></span><br><span class="line"><span class="attr">imageRepository:</span> <span class="string">k8s.gcr.io</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ClusterConfiguration</span></span><br><span class="line"><span class="attr">kubernetesVersion:</span> <span class="string">v1.15.0</span></span><br><span class="line"><span class="attr">networking:</span></span><br><span class="line">  <span class="attr">dnsDomain:</span> <span class="string">cluster.local</span></span><br><span class="line">  <span class="attr">podSubnet:</span> <span class="number">10.244</span><span class="number">.0</span><span class="number">.0</span><span class="string">/16</span></span><br><span class="line">  <span class="attr">serviceSubnet:</span> <span class="number">10.96</span><span class="number">.0</span><span class="number">.0</span><span class="string">/12</span></span><br><span class="line"><span class="attr">scheduler:</span> &#123;&#125;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>我在中大修 Matrix</title>
      <link>https://blog.howardlau.me/programming/operating-matrix-in-sysu.html</link>
      <description>
        <![CDATA[<h2 id="啥是-Matrix"><a href="#啥是-Matrix" class="headerlink" title="啥是 Matrix"></a>啥是 Matrix</h2><p>Matrix]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 19 Feb 2020 06:38:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="啥是-Matrix"><a href="#啥是-Matrix" class="headerlink" title="啥是 Matrix"></a>啥是 Matrix</h2><p>Matrix 是中山大学数据科学与计算机学院维护的一套用于编程教学的课程系统，具备布置作业、举办考试、线上评测等齐全的功能。</p><p><a href="/programming/operating-matrix-in-sysu/image-1582123315951.png" data-fancybox="gallery" data-caption=""><img src="/programming/operating-matrix-in-sysu/image-1582123315951.png"></a></p><h2 id="手工部署时代"><a href="#手工部署时代" class="headerlink" title="手工部署时代"></a>手工部署时代</h2><p>在最原始的时代，我们写好一个 Web 应用之后，将代码拷贝到服务器上，然后将服务进程跑起来，访问一下端口是不是正常工作，就算部署好了。然而，每次发布新版本，我们都要重新上传代码，重新运行进程，还要手动装环境，那么，有没有办法摆脱这种原始的手工方式呢？在经历了各种失败和挫折之后，我们终于搭建起了自己的私有云服务，并将业务全部迁移到云上，实现了云上 Matrix。</p><h2 id="应用集装箱：Docker"><a href="#应用集装箱：Docker" class="headerlink" title="应用集装箱：Docker"></a>应用集装箱：Docker</h2><p>如果你上过操作系统、数据库、区块链等等等等需要实践的课程，你大概率会因为环境配置而感到挫折。不是报错少这个包就是报错软件版本不对，等好不容易把软件都装上了，终于能跑起来了，你已经被折腾个半死，不想做大作业了。更糟糕的是，要是两个课程的需要软件版本冲突了，你或许连退学的想法都有了。这时候，要是老师或者 TA 能给你一个 FTP 地址，让你下载一个虚拟机镜像，那就有如再生父母，救你于水深火热之中。</p><p>其实，在真正的 Web 服务发布的时候，也会面临着一样尴尬的问题。有时候在开发机上能正常运行，但是到了服务器上就是死活跑不起来，开发人员只好说 “But it works on my machine!”。又或许应用 A 需要包 X 的 1.0 版本，而应用 B 需要包 X 的 2.0 版本，而系统上只能同时存在一个版本，那就只能将两个应用部署在两个不同的系统上了。这显然会造成资源的浪费。尽管一些语言提供了虚拟环境管理工具，但有时更尴尬的是，有时候我们需要部署的应用忘记指定版本了，我们甚至不知道跑起来应该装什么软件包！</p><p><a href="/programming/operating-matrix-in-sysu/image-20191211171800748.png" data-fancybox="gallery" data-caption="image-20191211171800748"><img src="/programming/operating-matrix-in-sysu/image-20191211171800748.png" alt="image-20191211171800748"></a></p><p>为了解决应用部署时的环境问题，以前人们就像我们上课一样，给不同的环境准备好一个虚拟机镜像，将依赖的软件统统打包起来，并且在虚拟机里调试运行正常，就可以发布到服务器上。而运维人员也不用再操心环境的干扰问题，直接把虚拟机镜像运行起来，就算部署成功了。然而，用过虚拟机镜像的都知道，一个虚拟机镜像动辄上 G，光是传输都要花上几分钟，解压之后占用空间还大，而且运行起来比较卡和占内存，因为虚拟机中的指令需要经过翻译，有较大的性能损耗。</p><p>显然，如果我们只是想跑起来一个网站，虚拟机就显得太“重”了。在 2008 年，Linux 推出了一个类似虚拟机的功能，称为 LXC（Linux Container）<strong>容器</strong>。容器的实现思想和 C++ 中的 namespace 如出一辙：在 C++ 里面，像 <code>string</code>、<code>vector</code> 等等都在 <code>std</code> 这个 namespace 里，而我们自己可以完全再新建一个叫 <code>myns</code> 的 namespace，在里面编写我们自己的 <code>string</code> 和 <code>vector</code> 而不用担心和标准库冲突。在 Linux 也一样，我们可以将网络、进程、文件系统等等按照 namespace 隔离，我们可以在 <code>ns1</code> 里运行依赖 X 1.0 版本的 A，在 <code>ns2</code> 里运行依赖 X 2.0 版本的 B，而两者互不干扰，就像各自运行在独立的系统中一样。而且我们也能像虚拟机一样，随时启动、暂停、停止容器。但其实它们都运行在同一个主机上，使用着同一个内核，读写着同一块硬盘！所以容器实际上没有做虚拟化，它们<strong>还是运行在主机上一个个普通的进程</strong>，只是我们人为<strong>隔离</strong>了它们的资源。可以说，<strong>隔离</strong>是容器化的核心思想。相比起虚拟机，容器减少了指令翻译的损耗，运行起来也就更快。而且由于并没有启动多个系统，因此非常节省内存。</p><p>容器除了隔离，我们还可以有选择地在容器间<strong>共享</strong>资源。比如应用 A 和应用 B 都跑在 Ubuntu 18.04 （宿主机可能并不是这个系统版本）里，那么容器 A 和容器 B 就可以共享系统文件，主机上只用存一份镜像即可，相比起虚拟机，节省了大量的磁盘空间。</p><p>不过，虽然 Linux Namespace 提供了隔离机制，但却没有办法像虚拟机一样控制不同容器使用 CPU 和内存的限制，为此，还有另一个重磅技术 CGroups（Control Groups）来<strong>限制</strong>资源的使用。操作系统管理着所有的进程和线程，自然也知道不同进程和线程 CPU 和内存用量。CGroups 实现的思想也非常简单，开放一个接口让用户设置限制值，然后对比进程是不是超过限制量就可以了。</p><p>LXC 基本实现了虚拟机的<strong>隔离</strong>和<strong>限制</strong>的功能，同时在性能上有着无可比拟的优势，因此也被称为“轻量级虚拟化”。不过，由于涉及较多内核的功能，使用起来还是略显麻烦。为此，2013 年 dotCloud 公司将复杂的容器封装成简单易用的 Docker 工具并开源，让容器的打包、下载、运行变得十分方便。从此应用的部署就可以抛弃昂贵的虚拟机，转而使用容器化部署了。</p><p><a href="/programming/operating-matrix-in-sysu/image-20191211171614249.png" data-fancybox="gallery" data-caption="image-20191211171614249"><img src="/programming/operating-matrix-in-sysu/image-20191211171614249.png" alt="image-20191211171614249"></a></p><p>要是每个应用都重新做一个容器，那就会显得太麻烦，也太占空间。这时候轮到 aufs、overlayfs 等<strong>分层</strong>文件系统大显身手了。Docker 构建容器是一层一层地构建的，就像 git 一样，我们每次往容器里添加、修改或删除文件的时候，我们并不是直接在镜像中直接修改文件，而是会建立一层新的文件层，记录着变动。为此，我们可以先构造一些基础镜像，比如一个装好 Python 3.7 的 Ubuntu 18.04 镜像，后续我们就直接在这个镜像上直接添加新的层来构建我们的应用镜像即可。而 Docker 下载镜像的时候，就会看看这个镜像哪些层已经下载过了，到时候直接<strong>共享</strong>给不同容器即可，只下载没下载过的层，这样既节省流量，也节省空间。</p><p><a href="/programming/operating-matrix-in-sysu/image-20191211171712140.png" data-fancybox="gallery" data-caption="image-20191211171712140"><img src="/programming/operating-matrix-in-sysu/image-20191211171712140.png" alt="image-20191211171712140"></a></p><p>而这些基础镜像往往都是大家非常需要的，于是 Docker 推出了注册表的功能，上面记录了有哪些镜像已经被制作过并被上传了，这样其他开发者需要使用的时候，就不用再自己制作了，直接从注册表获取制作好的镜像就可以跑得欢了~就像 GitHub 托管了源码一样，DockerHub 托管了许多公共的镜像。我们也可以自己搭建一个 Docker 注册表，就像我们可以使用 GitLab 自己搭建 Git 仓库一样。</p><p>就这样，Docker 几乎完美地解决了应用的打包部署问题，我们开发好的应用就连着环境一个个地被打包进集装箱中，运往不同服务器，然后被运行起来，提供服务。当然，平时我们想用一些新版本的环境做开发，也不用费劲到处找二进制包，甚至下载源码编译了，我们只需要从 DockerHub 拉取镜像，就能在几分钟内跑起一些配置复杂的软件了~</p><h2 id="持续交付：CI-与-CD"><a href="#持续交付：CI-与-CD" class="headerlink" title="持续交付：CI 与 CD"></a>持续交付：CI 与 CD</h2><p>在我们编写好代码之后，我们可能需要编译、测试、打包和部署。而一次又一次地将代码传到服务器上，然后执行命令，不仅效率低下、重复繁琐，还极有可能出偏差，要负责任的。俗话说，程序员只会做同一件事两次，因为第三次的时候已经是自动化脚本了。</p><p>在 Git 中，其实我们每次操作都会触发一些脚本，这些触发点就叫做 Hook（钩子），例如我们在 commit 之后，会触发 commit 的钩子，push 之后，服务器会触发 receive 钩子。通过这些钩子，我们就能实现许多自动化操作。</p><p><a href="/programming/operating-matrix-in-sysu/ci-pipeline.png" data-fancybox="gallery" data-caption="ci-pipeline"><img src="/programming/operating-matrix-in-sysu/ci-pipeline.png" alt="ci-pipeline"></a></p><p>CI 指的是 Continuous Integration（持续集成），集成的意思就是将你所做的改动合并到现有代码中，看看能不能编译通过，然后再运行自动化测试看看能不能通过全部样例（听起来是不是很像你在 Matrix 上做题[奸笑]）。</p><p><a href="/programming/operating-matrix-in-sysu/image-20191207152056089.png" data-fancybox="gallery" data-caption="image-20191207152056089"><img src="/programming/operating-matrix-in-sysu/image-20191207152056089.png" alt="image-20191207152056089"></a></p><p>而 CI 通过后，我们就可以准备部署了，这时候就进入了 Continuous Deployment 阶段，持续部署。在这一阶段，我们通过一系列预先定义好的命令，将编译好的应用连同运行环境一起打包成 Docker 镜像，发布到 Docker 注册表中，并且通知运行着 Docker 的服务器拉取新版本应用、停止旧版本应用并运行新版本应用。</p><p>通过 CI&#x2F;CD，每次我们推送代码之后，都会全自动地将我们的代码部署到服务器上，全程不需要我们到服务器上执行一条命令。哪怕开发人员对服务器运维并不熟悉，也不妨碍他发布新的代码，提高了效率。</p><p>而在这个阶段，Matrix 发布服务的方式是在一台指定的机器上运行 CI Runner，负责将代码打包成 Docker 镜像，并在本地运行起来，然后通过端口暴露的方式，将容器内部的网络开放给其他机器。而如果应用需要互相访问，则可以使用 IP + 端口直接访问。</p><h2 id="容器海洋的掌舵人：Kubernetes"><a href="#容器海洋的掌舵人：Kubernetes" class="headerlink" title="容器海洋的掌舵人：Kubernetes"></a>容器海洋的掌舵人：Kubernetes</h2><p>当我们开发的应用逐渐增多时，麻烦就逐渐显露出来：我们需要小心翼翼地分配 IP 地址和端口避免冲突。为了避免冲突，我们<strong>手动</strong>维护了 IP + 端口与服务的对应表，当我们需要发布一个服务的时候就先查找这个表，看看有哪些空闲的端口，然后往表里写一条记录。而当我们想知道一个服务发布在哪里的时候，就要反过来查这个表。很显然，我们在做 DNS 做的事……同时，我们每发布一个应用，就要手动调整反向代理的配置，增加新的虚拟服务器，指向我们发布的应用。</p><p>有时候一个容器不足以应付高并发流量，又或者为了避免单点故障，我们会选择在不同的机器上部署相同的容器，这样即使其中一个机器宕机，也能继续正常提供服务。这样，我们就还得手动维护一个服务都在哪个地址提供服务的表。而应用想访问其他服务的时候，我们就把所有的地址都写到配置文件中，然后再由应用去选择一个能用的地址访问服务。或者，我们会利用反向代理的负载均衡功能，将服务写成一个固定的域名，到时候应用如果需要访问服务，则直接交给反向代理选择一个服务实例进行服务。</p><p>然而，这仍然无法处理实例动态伸缩的问题，当我们增加或者减少服务容器的数量的时候，我们还是要手动改配置。随着服务的增多和变动的频繁，手动更改配置缺点逐渐显露出来，不仅非常繁琐，而且非常不灵活。</p><p>而且我们现有的 CI 模式导致了无法将容器部署到与打包不同的机器上，这就导致如果我们想开启多个进程进行负载均衡时，只能在单机上开启多个容器，或者甚至直接在单个容器内开启多个进程。</p><p>可以看到，当前的服务部署模式极度依赖手工操作，非常不灵活。那么对于一个服务器集群，我们应该使用什么样的工具进行管理呢？</p><p>对于这个问题，Google 是当之无愧的领头羊。为了管理大规模计算机集群，Google 内部开发了一套工具，称为 Borg，使用了十多年，并发表了[相关的论文]<sup><a href="#fn-1" id="fnref-1">[1]</a></sup>。不过，Borg 本身并不开源，但是 Google 吸取了十多年来集群管理的经验教训，开源了一套管理容器的系统，称为 Kubernetes（简写 K8s）。这个单词源于希腊文，意思是掌舵的人。一个个容器就像集装箱被运上货船，由 Kubernetes 载着他们到不同的港口，最后卸货拆箱运行。</p><p>K8s 就像一个船长，我们只需要告诉船长我们想要到哪里，他就会负责帮我们完成剩下的工作。我们只需要告诉 K8s，应用 A 需要运行 3 个实例，应用 B 需要运行 5 个实例，K8s 就会帮我们妥善安排好机器资源，将容器调度到上面运行。我们也无需手动一一安排每个机器上需要运行怎样的容器了。而在 CI 时，我们也不需要自己运行容器，只需要打包好镜像，并告诉 K8s 我们需要怎样部署这个服务，我们的服务就可以部署到集群里的其他机器上了。</p><p>Google 在使用 Borg 运行任务的时候，也直接使用了主机的 IP 地址和端口来提供对外服务，因此他们也遭遇到了需要维护一堆 IP 和端口的麻烦，应用也需要根据可用的端口调整自己的配置。不过，得益于网络虚拟化、SDN等等网络技术的发展，我们可以摆脱物理网络限制，给容器分配一个虚拟的 IP，这样就不需要占用主机上宝贵的端口资源了。在 K8s 的管理概念中，调度的最小单位是 Pod，每个 Pod 都有一个自己的虚拟的 Pod IP，一个 Pod 可以含有多个容器，而一个 Pod 里的容器共享同一个网络空间。一个 Pod IP 对外表现就像是一个正常服务的主机一样，可以 Ping 通，也能直接访问。</p><p>然而，这还是没有解决其他应用访问服务的问题，由于 Pod IP 并不是确定的，甚至是会随着 Pod 创建和销毁频繁变动，因此，K8s 还会帮我们管理一个服务对应着哪些 Pod，称为 Service。在我们部署应用的时候，会指定一个 Service 应该对应到哪些应用，K8s 便会将这些应用的 Pod IP 记录在 Service 里。而一个 Service 也拥有自己的一个 IP 称为 Cluster IP，不过这个 IP 比 Pod IP 稳定得多，但却表现得不像一个正常的主机。这个 Cluster IP 并不直接对应到任何可以被访问的实体上，而是一个中介。集群里的机器会在收到 Service 的信息时候，在机器上设置 iptables 规则，任何访问这个 Cluster IP 的指定的端口和协议的流量最后都会被转发到 Pod IP 上。所以 Service 的工作原理与反向代理并不相同。而这也保证了即使 Master 宕机，应用也能暂时正常继续运行。</p><p>万一一个 Pod 宕机了，那么 Master 就会将其从 Service 中除名，并通知集群机器重新设置代理，这样，流量就不会被转发到死掉的实例上。那么问题来了，K8s 如何知道一个 Pod 是否正常运行呢？它用的方式十分简单粗暴：我定时执行一个命令，或者发送一个 HTTP 请求到你的容器中，要是没反应或者出错了，那就认为 Pod 出错了，并暂时将其从 Service 中除名，然后重启容器，希望能恢复正常状态。我们写应用也无需对异常做过多处理了，我们就大胆放心的 <code>Let it crash</code>，出错了保存一下现场，然后重启就好了。</p><p><a href="/programming/operating-matrix-in-sysu/k8s-dashboard.png" data-fancybox="gallery" data-caption="k8s-dashboard"><img src="/programming/operating-matrix-in-sysu/k8s-dashboard.png" alt="k8s-dashboard"></a></p><p>通过这种保活机制，在我们告诉 K8s 需要部署新版本的时候，K8s 并不会马上把旧的容器全部赶尽杀绝，火急火燎地上线新容器，而是会先启动一个新容器，等到确认它正常工作一段时间后，才会杀掉旧容器，然后再启动一个新容器，看看工作是否正常……这种方式就被称为滚动更新，这样通过一个个逐步替换的方式更新版本，确保了我们对系统的错误改动不会立即生效，保障了服务的正常运行。这样，我们发布新版本的时候就更大胆了，毕竟要是出错了，K8s 还会尽职尽责地帮我们守住服务呢！</p><h2 id="基础服务迁移"><a href="#基础服务迁移" class="headerlink" title="基础服务迁移"></a>基础服务迁移</h2><p>Web 后端与前端都是无状态的应用，很容易无痛上云。然而帮助其维持状态的基础服务如 MQ、Redis、文件系统等则是有状态的，要迁移会非常麻烦。而且当前部署的方式通通是单实例，我们还需要对基础服务进行集群化改造。这也花费了我们不少的时间。</p><p>对于有状态的应用而言，我们不仅需要为其分配存储空间，以免容器重启后数据全部丢失了，而不同的应用对于集群化部署也有不同的要求，甚至可能对容器的启动顺序有要求，所以还需要对其量身定制。分布式有状态的应用有一个棘手的问题，那就是状态在各个实例之间如何互相同步。假如你用过腾讯文档或者石墨文档等，想必你也好奇过大家的修改是如何同步的吧。就以数据库为例，假如现在有 A、B、C 三个数据库，那么谁可以写入呢？要是大家都能写，万一修改之间冲突了怎么办呢？如果只有一个实例可以写，那么又该如何选举呢？要是一个实例宕机了，集群状态怎么恢复呢？要是有一个新实例加入，又该如何同步状态呢？分布式有状态应用就好像一个小小的议会，可是大家都是兼职的，想来就来，想走就走，却又需要对很多规定做出一致的决定。</p><p>好在，计算机科学家们早已研究出许多种分布式一致性算法，例如 Paxos、Raft、Gossip 等。有的算法通过一阵猛如虎的数学操作，就让实例之间通过自己保存的状态以及对方发来的消息，来使整个集群的大多数节点在数据上达成一致。而有的算法同样通过猛如虎的数学操作，钦点出一个专门负责写入的 Leader，其他实例则成为 Follower，乖乖从 Leader 接收数据。Leader 要是没有了，就再通过一阵猛如虎的操作选出新的。当然还有的通过一阵猛如虎的哈希操作，将不同的读写请求分配到不同的实例上，当然分区之间有冗余，要是分区的备份数不够了，就想办法复制出来一份。这样保证了数据在大多数时候都可以访问，即使有少数实例莫名其妙宕机，也不会严重影响集群的状态。</p><p>不过还有一个问题，那就是节点之间如何发现对方呢？就像你想拉几个好友群聊，总得想办法加微信好友吧。在比较传统的部署下，IP、实例数量等相对固定，我们大可以直接将所有的节点信息写到每个节点的配置文件里，相当于让每个节点都事先认识对方，拉群聊也就很方便了。但是在 K8s 环境下，我们没有固定的 IP，也没有固定实例数，又该怎么让他们发现对方呢？</p><p>好在 K8s 也早就支持了对于有状态应用的部署，K8s 会专门为有状态应用的每个实例提供一个一致的可预测的稳定的主机名标识符，无论 Pod 如何变动，都拥有一个不变的标识符，并且总会按顺序启动。例如我们的应用叫 <code>mysql</code>，那么 K8s 就会按顺序一个接一个地启动 <code>mysql-0</code>，<code>mysql-1</code> 等等。我们可以像链表一样将这些实例连在一起。更有的应用提供了 K8s 插件，可以直接从 K8s 中获取自己的伙伴信息。这样，我们的基础服务也可以欢快地跑在 K8s 上了。为此，我们还使用了一台专门的 NFS 服务器，提供网络文件系统服务，这样，K8s 就可以利用 NFS 来供集群中的机器进行读写了。</p><p>然而，有状态应用的迁移，不仅需要分配存储空间，还要保证现有的数据也一致地迁移到新的服务上。也许你会说，我直接把文件复制粘贴一下不就好了吗？然而，我们的系统几乎每时每刻都有人在使用，也就意味着数据库可能随时在更新、不断地有新的文件写入系统，同时，原有的数据也是海量的，这意味着如果我们想停服迁移，可能花上一天也未必能搞定，然而不停服的话，中间的数据便有可能丢失，这就像是要给一辆飞驰着的火车换轮子一样难。</p><p>不过，难也要顶硬上，首先开刀的是文件系统，由于 Matrix 应对的是大量的小文件，我们并没有直接将用户的提交直接粗暴地存放在 ext4 等通用操作系统上，而是选用了 SeaweedFS 来存储我们的提交以及测试数据等，这个文件系统实现了[这篇 Facebook 论文的算法]<sup><a href="#fn-2" id="fnref-2">[2]</a></sup>，提升了小文件存储的效率。而文件系统使用了 Docker Compose 部署，将宿主机目录挂载到了容器内部，因此我们天真地以为只要将目录里的东西复制到 NFS 上，然后重新挂载目录即可。可令我们万万没想到的是，我们没有办法读取出任何文件！噢，上网一搜，原来是我们原来的 SeaweedFS 很久没更新了，新版本的 SeaweedFS 早已不兼容旧版本的数据格式。这时心中一万头草泥马奔腾而过~然而更危险的是，原有的 Docker 镜像部署直接使用 latest 标签，要是那一天我们不小心更新了原来的镜像，那可真是欲哭无泪了。不过，抱着软件版本升级一般会提供相应的数据升级工具的想法，我们去 SeaweedFS 的 Wiki 搜寻了一番。OMG，结果作者并没有提供这样一种工具！无奈，我们只好选择自己写脚本。为了保证文件不丢失，我们还先修改了应用的代码，在用户上传文件的时候，同时写入新旧文件系统，然后便开始了漫长地导出、打包、导入 210 万个小文件的过程……终于，花了两天时间，我们成功地把文件系统不停机地迁移了，给火车换上了新轮子。</p><p><a href="/programming/operating-matrix-in-sysu/seaweedfs.png" data-fancybox="gallery" data-caption="seaweedfs"><img src="/programming/operating-matrix-in-sysu/seaweedfs.png" alt="seaweedfs"></a></p><p>之后是 Redis 迁移。Redis 主要用于数据库缓存以及 Session 状态存储。Redis 集群部署有两种选择，一种是 Sentinel，一种是 Redis Cluster，各自都能实现高可用。本着学习的心态，我们选择了 Redis Cluster 部署。对于这种部署方式，其 Key 采用了分区存储的方式，因此我们需要改造原有的 Redis 客户端代码，使其能够处理 Redis 集群连接。而 Redis Cluster 的建立需要在集群所有节点正常启动之后才能进行。这时候，连 K8s 强大的管理能力也显得有点捉襟见肘了。幸好集群一旦建立便是一劳永逸，所以我们选择了先直接启动好 Redis 节点，然后在其中一个容器里执行集群初始化命令。等待集群正常运行之后，我们就直接粗暴地将服务指向新的 Redis 集群，已登录的用户也因此被通通踢下线。不过当时已经是深夜，因此被踢下线的用户并不多。</p><p>而我们与评测系统通信、或者广播实时通知使用的消息队列 RabbitMQ，则迁移难度小了很多，由于消息丢失了我们也有办法重新发送，因此我们直接选择了上线新版本的服务，运行正常后直接切换到 RabbitMQ 集群上，然后重新发送丢失的消息，就完成了迁移。</p><p><a href="/programming/operating-matrix-in-sysu/rabbitmq.png" data-fancybox="gallery" data-caption="rabbitmq"><img src="/programming/operating-matrix-in-sysu/rabbitmq.png" alt="rabbitmq"></a></p><p>而最让人头疼的，也许就是数据库了。如果说登录状态丢失了大不了再登录一次，评测消息没有发送出去大不了再发送一次，数据丢失了，那就没有办法挽回了。所以，对于数据库而言，我们仍然要如履薄冰，万分小心。不过，互联网上也有许多的开源方案可以供我们参考，其中最知名的莫过于 GitHub 的 orchestrator。我们当然是拿来主义，学习其中的思想，然后部署在我们的服务器上，搭建了数据库的集群。</p><p><a href="/programming/operating-matrix-in-sysu/mysql.png" data-fancybox="gallery" data-caption="mysql"><img src="/programming/operating-matrix-in-sysu/mysql.png" alt="mysql"></a></p><p>当然，数据库的迁移是更加困难的，通过观察日志发现，我们的系统几乎 24 小时都有人用，而当深夜的时候虽然没有什么人用，但是我们也想睡觉。所以，如何完成数据库的不停机迁移也成为了一个大难题。首先当然要将当前的数据库备份一下，如果为了数据一致性而采用锁库锁表的方式，显然会对服务造成一段时间的影响。因此，我们必须依赖于 InnoDB 存储引擎的高级功能，通过拷贝物理文件快照，并在拷贝过程中不断记录快照后发生的事务数据，实现不停机的数据导出。然后，我们只需要将快照后的事务数据在物理文件上“重放”一次，就能恢复完整的数据了。当然，启动新的数据库后，仍然存在着数据上的时间差。为此，我们将新的数据库设置为从库，从旧数据库中及时同步新产生的数据。最后，我们选取了一个低峰点，将所有数据库设置为只读状态，同时修改所有服务配置指向新数据库地址，最后将旧数据库关机，新数据库设置为可写主库，就完成了一次（几乎）不停机的数据库迁移了。</p><p>不过，由于我们采用的是主从复制模式，只有主库可以写，如果读写压力都集中在主库上面，则有可能造成瓶颈。我们可以借助 ProxySQL 的工具，来将读数据库的压力分散到整个集群，这样我们就能充分利用数据库集群的性能了。</p><p><a href="/programming/operating-matrix-in-sysu/ProxySQL.png" data-fancybox="gallery" data-caption="proxysql"><img src="/programming/operating-matrix-in-sysu/ProxySQL.png" alt="proxysql"></a></p><p>到这里，棘手的有状态服务迁移也就告一段落了，数据的安全性和可用性得到了进一步保障。</p><h2 id="The-Big-Brother-is-Watching-You"><a href="#The-Big-Brother-is-Watching-You" class="headerlink" title="The Big Brother is Watching You"></a>The Big Brother is Watching You</h2><p>如果有一辆没有仪表盘、没有后视镜、挡风玻璃也脏的看不见前面的路的车，你敢开吗？也许运气好，前面的路一马平川，闭着眼睛踩着油门也能让车撒了欢地跑，但我们完全不知道这辆车的方位、速度，更不要提控制这辆车了。因此，为了更好的控制集群系统和其中的服务，我们必须建立一套完善的监控体系。</p><p>首先要解决的是日志的收集。尽管我们在写程序的时候就已经做了充分测试，可是难免上线之后出这样那样的 bug，线上的 bug 我们没有办法预知，也没办法直接在上面挂载调试器，而且很可能出错之后进程就被自动重启了。这时候，我们唯一能依赖的就是日志了。日志是什么，就是 printf，就是 cout，就是 print！</p><p>而这其中的挑战是服务是分布式部署的，我们没有办法像从前一样在一台机器上，绑定一个文件夹到一个容器上来收集日志。好在，这也是被工程师们精心解决过的问题，只要我们在每个机器上都运行一个收集所有容器产生的日志的进程，打上标签、做一些预处理、过滤掉不需要的日志，并且再从这些进程统一收集处理好的数据，不就好了吗？Fluentd、Logstash、Filebeat 等等都是这样的工具。不过，还有一个问题，我们要怎么收集这些数据呢，是由数据库去定时拉取，还是由进程主动上报？对于日志收集而言，由于日志产生速度比较快和多，如果定时去拉取，会造成缓冲区的堆积，影响机器性能，因此会采取进程主动上报的模式。</p><p>如果你用 printf 等等打印一堆东西来调试程序的话，一定会遇到数据量太大的问题。对于日志也是这样，一个正常的服务很可能每秒都会产生大量的日志，更不要说分布式部署带来的倍数增长了，在海量的日志里，我们不可能肉眼一条条去看，就像我们上网冲浪，也不能一个个网页点开来看，需要借助搜索引擎。收集完日志后，我们同样需要一个搜索引擎来帮我们快速找到我们感兴趣的日志，而 ElasticSearch 就能对进程上报的日志进行索引，帮我们快速找到日志。</p><p>不过，ElasticSearch 本身只能通过 REST API 操作，如果我们自己写一个前端又太麻烦，好在，工程师们也早就写好了一个 Web UI 供我们操作 ElasticSearch，那就是 Kibana。通过 Kibana，我们可以使用一种叫 KQL 的查询语言，快速查询我们需要的日志信息。更强大的是，我们可以通过提取日志中的信息，来构建一些统计数据的可视化。</p><p><a href="/programming/operating-matrix-in-sysu/image-20191207151142106.png" data-fancybox="gallery" data-caption="image-20191207151142106"><img src="/programming/operating-matrix-in-sysu/image-20191207151142106.png" alt="image-20191207151142106"></a></p><p>除了日志，我们还需要对机器、应用的实时指标进行监控，比如 CPU、内存、硬盘、数据库连接等等，并且在出现异常的时候及时报警，对于这些数据，我们并不需要多么强大的文本查询功能，因为都只是数字而已，如果使用 ElasticSearch 反而会有点杀鸡用牛刀的感觉。不是不行，而是太过笨重与别扭，配套的工具也不是很好用。</p><p>对于这种场景，Prometheus 会更合适，这是针对数值时间序列优化过的存储与搜索引擎。同样地，我们需要在每个机器上安装监控程序，称为 Exporter，负责将指标“导出”给 Prometheus 服务器，但相比日志收集，Prometheus 采用了主动拉取的模式。这是因为指标每时每刻都在变动，不像日志是离散的事件，所以有采样的概念。如果让采集器主动上报，那么很难保证每个采集器都同步上报，这样从不同机器收集到的数据就很难聚合起来。所以，Prometheus 会间隔一段时间，对配置中的所有采集器进行一次拉取采样，并存储到数据库中。尽管 Prometheus 比 ElasticSearch 好在自带了一个 Web UI，但实在是过于简陋。为此人们又不辞劳苦地开发了一个 Web UI，叫 Grafana，事实上，Grafana 就是 Kibana 的一个分支版本。通过 Grafana，我们可以将数据以 PromQL 这种查询语言进行聚合、计算，并将得到的数据通过饼图、线图、柱状图等等方式可视化出来。通过 Grafana，我们从 IT 精英摇身一变监控室大爷。</p><p><a href="/programming/operating-matrix-in-sysu/image-20191207150859632.png" data-fancybox="gallery" data-caption="image-20191207150859632"><img src="/programming/operating-matrix-in-sysu/image-20191207150859632.png" alt="image-20191207150859632"></a></p><p>当然，我们不能总是没事就盯着监控看，我们还有 Alert Manager 来帮我们监控异常状况。通过设置感兴趣的监控指标以及规则，Alert Manager 会定期检查指标是否在正常范围内，如果长时间异常，就会通过电子邮件、Web Hook 等方式通知运维人员。这样我们就能从人工监控中解放出来，又能及时的感知到异常状况进行处理了。</p><p>对于大量的服务，我们还需要知道一个服务都依赖于什么服务。尽管我们可以通过代码来获取依赖信息，但有时候通过图的方式能更直观地了解服务之间的调用链。 Weave Scope 则提供了一个观察服务。和日志、指标收集一样，Scope 会在每个机器上安装一个收集器，收集容器的网络状况，并以此构建出一幅逻辑图。这样，我们就能直观地在网页上看到一个服务都调用了哪些服务，又被哪些服务调用了。</p><p><a href="/programming/operating-matrix-in-sysu/weave-scope.png" data-fancybox="gallery" data-caption="weave-scope"><img src="/programming/operating-matrix-in-sysu/weave-scope.png" alt="weave-scope"></a></p><p>而对于我们对外发布的每个服务，实际上都是由 K8s Ingress 资源所控制的，而 Ingress 最终会被 Ingress Controller 所调度，因此，我们也可以通过监控 Ingress Controller 非常集中的看到我们发布的服务的状态。</p><p><a href="/programming/operating-matrix-in-sysu/traefik.png" data-fancybox="gallery" data-caption="traefik"><img src="/programming/operating-matrix-in-sysu/traefik.png" alt="traefik"></a></p><p>除了服务的正常运行，我们还希望能够对我们写的应用进行性能或者使用情况监控。对于数值型的数据，例如在线WS 用户数，我们会选择 Prometheus 进行监控。</p><p><a href="/programming/operating-matrix-in-sysu/grafana-traefik.png" data-fancybox="gallery" data-caption="grafana-traefik"><img src="/programming/operating-matrix-in-sysu/grafana-traefik.png" alt="grafana-traefik"></a></p><p>而像数据库查询等等更深入的监控，则需要 APM 来帮助我们，通过直接修改库的代码，拦截所有的数据库操作，我们就能得到一个请求里到底进行了什么数据库操作：</p><p><a href="/programming/operating-matrix-in-sysu/kibana-apm.png" data-fancybox="gallery" data-caption="kibana-apm"><img src="/programming/operating-matrix-in-sysu/kibana-apm.png" alt="kibana-apm"></a></p><p>当然，我们还有一个最头疼的问题：前端的错误。如果说服务端发生了什么错误，我们还可以看 log 来排查 bug，那么前端出了问题，我们总不可能跑到用户的电脑上看控制台吧？所以，我们还需要在前端发生错误的时候上报错误，这样，我们就可以及时地定位 bug，而不需要麻烦用户帮我们排查问题了。</p><p><a href="/programming/operating-matrix-in-sysu/sourcemap.png" data-fancybox="gallery" data-caption="sourcemap"><img src="/programming/operating-matrix-in-sysu/sourcemap.png" alt="sourcemap"></a></p><h2 id="负载均衡与高可用"><a href="#负载均衡与高可用" class="headerlink" title="负载均衡与高可用"></a>负载均衡与高可用</h2><p>我们当然希望服务器的资源能被充分利用起来，所以，我们几乎所有的服务都以多个副本的形式部署起来。就像饭一个饭堂一样，开很多个窗口来应对络绎不绝的人流。当然，部署了冗余的服务之后，我们还能实现高可用，一个服务宕机了，还有其他服务可以使用，或者我们干脆再另外启动一个，替代掉宕机的服务。好在，K8s 会自动帮我们完成这一切，我们不需要关心我们的服务都运行在什么地方，也不需要关心怎么设置转发，一切都由 K8s 代劳。</p><p>那么问题来了，K8s 如何保证自己在 Master 重启的时候也可以正常运作呢？这时候我们需要对 K8s 本身进行高可用的改造。对于 K8s 的管理入口 API Server，调度器 Scheduler 以及控制器管理器 Controller Manager 而言，他们都是无状态的。而真正存储状态的，是 <code>etcd</code> KV 数据库。所以，K8s 本身也无非是一个 API + 数据库的典型 Web 应用。我们同样设置一个负载均衡器帮助我们自动选择可用的服务进行负载均衡，例如 nginx 等。当然，我们选择了专用于高可用的 HAProxy（High Availability Proxy，不是<del>蛤Proxy</del>）。显然，我们同样也不能只部署一个 HAProxy，而是同样的部署多个 HAProxy。那么这些 HAProxy 之间如何进行故障转移呢？这时候就请到了 keepalived 服务为我们守护。keepalived 通过一个<strong>自动漂移的虚拟 IP</strong>，来将一个 IP 绑定到能够正常提供服务的机器上。这个自动漂移的虚拟 IP 则是通过 VRRP（虚拟路由冗余协议）来完成”漂移“的过程的。</p><p><a href="/programming/operating-matrix-in-sysu/keepalived-k8s-haproxy.png" data-fancybox="gallery" data-caption="keepalived"><img src="/programming/operating-matrix-in-sysu/keepalived-k8s-haproxy.png" alt="keepalived"></a></p><p>不过，我们还有一个问题没有解决，尽管集群内部的服务可以进行负载均衡，但是从集群外部进入内部的流量，我们还是只能集中地转发到其中一台服务器上。万一我们想重启这台服务器，那么我们的网站会直接无法访问一段时间，包括我们的一些开发服务。为了避免这种情况，我们必须针对外部流量也进行负载均衡。</p><p>由于我们没有负载均衡器，但好在我们的路由支持等价路由 ECMP 均衡，也支持 BGP 协议，所以我们可以通过 BGP 协议广播多条等价路由，由路由器帮助我们进行负载均衡。</p><p><a href="/programming/operating-matrix-in-sysu/routing-table.png" data-fancybox="gallery" data-caption="routing-table"><img src="/programming/operating-matrix-in-sysu/routing-table.png" alt="routing-table"></a></p><p><a href="/programming/operating-matrix-in-sysu/load-balance.png" data-fancybox="gallery" data-caption="load-balance"><img src="/programming/operating-matrix-in-sysu/load-balance.png" alt="load-balance"></a></p><p>至此，即使我们有一台机器需要重启或者发生了故障，我们也不必担心会对服务造成影响，K8s 和应用集群会帮我们快速的 Fail Over，同时冗余性也保障了状态大概率不会丢失，服务的可用性得到了进一步的提升。</p><h2 id="自动水平伸缩"><a href="#自动水平伸缩" class="headerlink" title="自动水平伸缩"></a>自动水平伸缩</h2><p>在服务并不是使用高峰的时候，我们可能会关闭一些多余的容器，来释放服务器的内存和 CPU，节省资源；而到了服务高峰期，我们希望可以多部署一些实例，用来快速处理流量。因此，我们可以在完善的监控系统基础上，确定一些关键指标，例如 CPU 利用率、每秒请求数等，并且确定这些指标的目标，例如每个容器的 CPU 利用率不超过 50%，或者每个容器每秒处理的请求数不超过 100 等。当我们发现指标过小的时候，就可以缩减实例数，除非达到了最小限制，而当我们发现指标过大的时候，就自动扩展服务，从而应对高峰流量。</p><p>在 K8s 中，提供了 HPA API，用来帮助我们完成自动伸缩。</p><p><a href="/programming/operating-matrix-in-sysu/HPA.png" data-fancybox="gallery" data-caption="HPA"><img src="/programming/operating-matrix-in-sysu/HPA.png" alt="HPA"></a></p><p>HPA 原理也很简单，它就像一个监工，隔一段时间就去取指标看一下，并通过算法计算出应该增加还是减少多少个实例，然后向 K8s 发出 Scale 命令，剩下的就是 K8s 的工作啦~</p><h2 id="断路器"><a href="#断路器" class="headerlink" title="断路器"></a>断路器</h2><p>在服务之间调用的时候，可能会发生异常、超时等，而当一个服务发生大量错误的时候，说明很可能这个服务已经不正常了，又或者是过载了。对于这种情况，我们可以在服务链路上设置断路器。断路器就像一个保险，当我们发现一个服务不正常，就立即切断所有的流量。或许你会觉得这个很反直觉，断路了不是更用不了了么？让我们先来看看断路器的原理：</p><p><a href="/programming/operating-matrix-in-sysu/circuit-breaker-sm.jpg" data-fancybox="gallery" data-caption="circuit-breaker"><img src="/programming/operating-matrix-in-sysu/circuit-breaker-sm.jpg" alt="circuit-breaker"></a></p><p>可以看到，断路器原理非常简单，只有三个状态：</p><ul><li>最开始处于关闭状态，一旦检测到错误到达一定阈值，便转为开路；</li><li>这时候会有个 reset timeout，即开始准备恢复了，转移到半开路状态；</li><li>尝试放行一部分请求到后端，一旦检测成功便回归到闭路状态，即恢复服务。</li></ul><p>不要小瞧了这个非常简单的原理，它符合 <code>Fail Fast</code> 这个架构设计准则，而不是一直慢慢 fail，等到 fail 一定程度后才让运维人员知道情况，其实也就我们常说的做事原则：<strong>不要隐瞒问题，而是应该尽快暴露问题</strong>。</p><p>让我们想象一下，服务 A 需要对数据库进行一个耗时的操作，在某个时间，突然大量请求涌入了服务 A，数据库压力开始上升，导致其他依赖数据库的服务也开始响应缓慢，而前端受到超时的影响，开始不断重试，导致更多请求进入队列，最终导致影响扩大，所有服务都几乎进入了不可用的状态。这就演变成了一场“雪崩”事故。</p><p><a href="/programming/operating-matrix-in-sysu/circuit-breaker.jpg" data-fancybox="gallery" data-caption="circuit-breaker"><img src="/programming/operating-matrix-in-sysu/circuit-breaker.jpg" alt="circuit-breaker"></a></p><p>如果有断路器存在，我们可以在发现服务开始超时的时候立刻返回错误给客户，避免更多请求涌入服务 A，保护数据库，避免影响扩大。等服务 A 自然或者人工干预后恢复正常，再放行流量，从而避免波及更多的服务。</p><h2 id="新一代云计算技术——Serverless"><a href="#新一代云计算技术——Serverless" class="headerlink" title="新一代云计算技术——Serverless"></a>新一代云计算技术——Serverless</h2><p>在目前的云计算中，最主流的还是 IaaS，也就是云服务器。尽管云服务器省去了维护物理服务器的麻烦，但是仍然要在启动的操作系统上部署应用，我们还是需要操心操作系统以及环境等。同时，我们的应用即使没有任何请求到来，我们仍然需要为云服务器所预留的资源付费。</p><p>服务部署所带来的各种麻烦，让我们不禁畅想我们有没有可能编写好一个函数之后就直接上云，不需要操心需要什么样的服务器，也不需要操心如何在服务器的操作系统上安装软件，更不需要操心监控、日志等更麻烦的事情。这时候就轮到 Serverless 闪亮登场了。Serverless 初看上去令人一头雾水，什么叫“无服务器”？其实我们的应用并不是真的不需要服务器就能运行，相反，我们的应用还是运行在真实的服务器上，只不过开发者在整个开发流程中，都无需接触服务器罢了。当然，Serverless 并不是仅仅只有 FaaS，实际上 Serverless &#x3D; FaaS + BaaS。[可以参考这篇伯克利发布的 Serverless 调研论文]<sup><a href="#fn-3" id="fnref-3">[3]</a></sup></p><p>使用 FaaS 部署一个函数有多方便呢？我们来看一个例子。假如我想开发一个简单的 Go 服务，那么我只要先将服务商提供的代码模板拉取下来：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">faas-cli template pull golang-http</span><br></pre></td></tr></table></figure><p>然后用这个模板创建一个函数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">faas-cli new func --lang golang-http</span><br></pre></td></tr></table></figure><p>向其中的 handler 添加我们的代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> function</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"><span class="string">&quot;fmt&quot;</span></span><br><span class="line"><span class="string">&quot;net/http&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="string">&quot;github.com/openfaas-incubator/go-function-sdk&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Handle a function invocation</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Handle</span><span class="params">(req handler.Request)</span></span> (handler.Response, <span class="type">error</span>) &#123;</span><br><span class="line"><span class="keyword">var</span> err <span class="type">error</span></span><br><span class="line"></span><br><span class="line">message := fmt.Sprintf(<span class="string">&quot;Hello world, input was: %s&quot;</span>, <span class="type">string</span>(req.Body))</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> handler.Response&#123;</span><br><span class="line">Body:       []<span class="type">byte</span>(message),</span><br><span class="line">StatusCode: http.StatusOK,</span><br><span class="line">&#125;, err</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>编写完成代码后，我们可能需要对应用的环境变量等做一些配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">1.0</span></span><br><span class="line"><span class="attr">provider:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">openfaas</span></span><br><span class="line">  <span class="attr">gateway:</span> <span class="string">https://openfaas.vmatrix.org.cn</span></span><br><span class="line"><span class="attr">functions:</span></span><br><span class="line">  <span class="attr">func:</span></span><br><span class="line">    <span class="attr">lang:</span> <span class="string">golang-http</span></span><br><span class="line">    <span class="attr">handler:</span> <span class="string">./func</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">func:latest</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">KEY:</span> <span class="string">VALUE</span></span><br></pre></td></tr></table></figure><p>配置好之后，只需要简单的一句：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">faas-cli up -f func.yml</span><br></pre></td></tr></table></figure><p>Boom！稍等片刻，我们的函数就安然无恙的运行在了服务器上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Deploying: func.</span><br><span class="line"></span><br><span class="line">Deployed. 202 Accepted.</span><br><span class="line">URL: https://openfaas.vmatrix.org.cn/function/func</span><br><span class="line"></span><br><span class="line">$ curl -X POST https://openfaas.vmatrix.org.cn/function/func -d <span class="string">&quot;hello, world&quot;</span></span><br><span class="line"></span><br><span class="line">Hello world, input was: hello, world</span><br></pre></td></tr></table></figure><p>过程中，我们也不需要关心怎么构建应用镜像，怎么连接到服务器上，怎么编写 K8s 部署文件等等麻烦的事情，几行命令就将一个简单的函数部署到了云上，让开发者可以更专注于应用代码的编写，而无需花费大量的精力到应用的部署和维护上。</p><p>通过这种细粒度的部署方式，我们可以更有针对性地为函数分配计算资源，甚至可以想象将许多用户的函数的调度抽象为装箱优化问题，让一个主机内的计算资源被充分利用。对于云服务商而言，计算资源的利用率可以大大提升，无需为闲置的 VM 预留资源；对于用户而言，也无需关心底层的系统维护，省出大量精力来专注于代码的编写，而且，用户只需要为函数的调用付费，如果函数没有被调用，甚至不会产生开销，也节省了成本。</p><p>当然，Serverless 也有其自身的不足，其天生就是为无状态的应用而服务的，对于像数据库等有状态的应用，Serverless 短期内仍然不适合，还是需要 BaaS 服务作为支撑。而大大简化的部署流程，也意味着用户的自主性变少了。同时，过于分散的部署会导致请求链路大大加长，使得延迟大大增加。而且，集成调试也变得更加困难……</p><p>作为一个或许还在萌芽阶段的新技术，其优点与缺点都还比较明显，尚无确定的标准，也需要更多的迭代，我们也许并不会用来开发一个完整的应用，但相信其简洁性和背后的健壮性，能开启云计算的下一个时代。</p><h2 id="总结与展望"><a href="#总结与展望" class="headerlink" title="总结与展望"></a>总结与展望</h2><p>先来一张总体的系统图：</p><p><a href="/programming/operating-matrix-in-sysu/infra.png" data-fancybox="gallery" data-caption="infra"><img src="/programming/operating-matrix-in-sysu/infra.png" alt="infra"></a></p><p>从前辈们筚路蓝缕用 PC 搭建起 Matrix 系统来，到拥有自己的小型服务器集群，再到我们搭建起私有云来，我们感受到了一代代精神的传承。尽管由于课余时间和精力并不充分，我们甚至不得不牺牲假期时间来维护开发这一套系统。不过，也正是在与各种奇怪的 bug 的斗争中，我们学习到了许多实战方面的经验；同时也深深体会到了那些看似枯燥无味的计算机基础课是多么的重要。没有计算机网络，我们可能难以搞定集群网络中复杂的路由；没有操作系统，我们可能难以构建起安全、高效的评测系统；没有数据结构，我们可能难以对收集到的海量数据进行进一步分析……</p><p>或许我们现在的系统仍然有这样那样的问题，甚至可能有许多错误，但是我们也正努力地用我们的学识与智慧来逐渐弥补一些漏洞。技术也在不断飞速发展，也许今天还热门的技术，不久就变成了明日黄花。不过万变不离其宗，在花哨的技术名词背后，是亘古不变的基础原理。所谓大象无形，大音希声，也许我们会一时乱花渐欲迷人眼，但相信在经历过看山是山、看山不是山、看山还是山的境界之后，我们能看破纷繁复杂的表象，牢牢把握住那统治着复杂系统的根基。</p><p>最后，技术永无止境，进一寸有进一寸的欢喜，也希望青出于蓝而胜于蓝，后面维护这个系统的同学能用更先进的知识来帮助系统构建的更完善。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><div class="footnotes"><hr><ol><li id="fn-1">https://research.google/pubs/pub43438/ "Large-scale cluster management at Google with Borg" <a href="#fnref-1" class="footnote-backref">↩</a></li><li id="fn-2">https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf "Finding a needle in Haystack: Facebook’s photo storage" <a href="#fnref-2" class="footnote-backref">↩</a></li><li id="fn-3">https://www2.eecs.berkeley.edu/Pubs/TechRpts/2019/EECS-2019-3.pdf "Cloud Programming Simplified: A Berkeley View on Serverless Computing" <a href="#fnref-3" class="footnote-backref">↩</a></li></ol></div>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 VSCode + qemu 搭建 Linux 内核调试环境</title>
      <link>https://blog.howardlau.me/programming/debugging-linux-kernel-with-vscode-qemu.html</link>
      <description>
        <![CDATA[<h2 id="编译调试版-Linux-内核"><a href="#编译调试版-Linux-内核" class="headerlink" title="编译调试版 Linux 内核"></a>编译调试版 Linux 内核</h2><p>首先下载源代码：</p>
<figure]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 23 Oct 2019 22:59:11 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="编译调试版-Linux-内核"><a href="#编译调试版-Linux-内核" class="headerlink" title="编译调试版 Linux 内核"></a>编译调试版 Linux 内核</h2><p>首先下载源代码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.3.7.tar.gz</span><br></pre></td></tr></table></figure><p>具体下载哪个版本可以自己选择。</p><p>然后解压缩源代码，这里假设解压到了 <code>~/linux-5.3.7</code>。</p><p>然后安装编译依赖：</p><h3 id="Ubuntu"><a href="#Ubuntu" class="headerlink" title="Ubuntu"></a>Ubuntu</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install build-essential libncurses-dev bison flex libssl-dev libelf-dev</span><br></pre></td></tr></table></figure><h3 id="CentOS"><a href="#CentOS" class="headerlink" title="CentOS"></a>CentOS</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> yum group install <span class="string">&quot;Development Tools&quot;</span></span><br><span class="line"><span class="built_in">sudo</span> yum install ncurses-devel bison flex elfutils-libelf-devel openssl-devel</span><br></pre></td></tr></table></figure><h3 id="Fedora"><a href="#Fedora" class="headerlink" title="Fedora"></a>Fedora</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> dnf group install <span class="string">&quot;Development Tools&quot;</span></span><br><span class="line"><span class="built_in">sudo</span> dnf install ncurses-devel bison flex elfutils-libelf-devel openssl-devel</span><br></pre></td></tr></table></figure><p>进入源码目录，配置编译选项：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/linux-5.3.7</span><br><span class="line">make menuconfig</span><br></pre></td></tr></table></figure><p><strong>注意：如果你想用这个内核替换掉自己系统上的内核，要用 &#x2F;boot&#x2F;config-$(uname -r) 作为基础配置文件。可以使用 <code>cp /boot/config-$(uname -r) .config</code> 将已有的配置文件拷贝到编译目录中，然后再运行 <code>make menuconfig</code>。</strong></p><p>比较重要的配置项有：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Kernel hacking  ---&gt;</span><br><span class="line">    [*] Kernel debugging</span><br><span class="line">    Compile-time checks and compiler options  ---&gt;</span><br><span class="line">        [*] Compile the kernel with debug info</span><br><span class="line">        [*]   Provide GDB scripts for kernel debugging</span><br></pre></td></tr></table></figure><p>一定要打开。还有下面的选项会导致打断点失败，一定要关闭：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Processor type and features ----&gt;</span><br><span class="line">    [] Randomize the address of the kernel image (KASLR)</span><br></pre></td></tr></table></figure><p>保存并退出 menuconfig，开始编译之旅：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make -j$(<span class="built_in">nproc</span>)</span><br></pre></td></tr></table></figure><p>编译内核会花掉大概 20GB 的硬盘空间，而且一般需要几十分钟来编译，所以请保证有充足的硬盘空间还有时间。</p><p>编译的时候你可以抽空安装一下 qemu：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install qemu qemu-system qemu-kvm</span><br></pre></td></tr></table></figure><h2 id="直接替换已有内核"><a href="#直接替换已有内核" class="headerlink" title="直接替换已有内核"></a>直接替换已有内核</h2><p>如果希望快速替换掉现有系统内核，可以使用 <code>make deb-pkg</code> 打包成 <code>.deb</code> 包，然后使用 <code>dpkg</code> 将 <code>.deb</code> 包安装到现有系统中，这样就无需自己准备工具链。例如，我们可以先使用网上下载的 Ubuntu 20.04 等系统安装到虚拟机里，然后安装我们自己打包的内核。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">make deb-pkg</span><br><span class="line"><span class="built_in">ls</span> ../*.deb</span><br><span class="line">../linux-headers-5.3.7_5.3.7-2_amd64.deb  ../linux-image-5.3.7-dbg_5.3.7-2_amd64.deb</span><br><span class="line">../linux-image-5.3.7_5.3.7-2_amd64.deb    ../linux-libc-dev_5.3.7-2_amd64.deb</span><br></pre></td></tr></table></figure><h2 id="制作-initramfs-在虚拟机中启动基本系统"><a href="#制作-initramfs-在虚拟机中启动基本系统" class="headerlink" title="制作 initramfs 在虚拟机中启动基本系统"></a>制作 initramfs 在虚拟机中启动基本系统</h2><p>下面介绍下如何仅启动基本的系统。</p><p>内核启动的时候非常矛盾， boot loader 加载完内核文件 vmlinuz 后，内核紧接着需要挂载磁盘根文件系统，但如果此时内核没有相应驱动，无法识别磁盘，就需要先加载驱动，而驱动又位于 <code>/lib/modules</code>，得挂载根文件系统才能读取，这就陷入了一个两难境地，系统无法顺利启动。于是有了 <code>initramfs</code> 根文件系统，其中包含必要的设备驱动和工具，boot loader加载 <code>initramfs</code> 到内存中，内核会将其挂载到根目录 <code>/</code>，然后运行 <code>/init</code> 脚本，挂载真正的磁盘根文件系统。</p><p>这里借助 BusyBox 构建极简 initramfs，提供基本的用户态可执行程序。</p><p>下载 busybox 源代码，解压，假设解压到 <code>~/busybox-1.31.0</code>，解压完成后，跟内核一样先配置编译选项：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/busybox-1.31.0</span><br><span class="line">make menuconfig</span><br></pre></td></tr></table></figure><p>记得要编译成静态链接的：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Settings  ---&gt;</span><br><span class="line">    [*] Build static binary (no shared libs)</span><br></pre></td></tr></table></figure><p>然后编译，并且安装到 <code>_install</code> 目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">make -j$(<span class="built_in">nproc</span>) &amp;&amp; make install</span><br></pre></td></tr></table></figure><p>之后制作 initramfs 镜像，需要注意的是里面不包含内核模块，如果需要启用内核模块，则需要手动将 <code>.ko</code> 文件拷贝到 <code>lib/modules</code> 目录里，然后在 <code>init</code> 脚本中 <code>insmod</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> initramfs</span><br><span class="line"><span class="built_in">cd</span> initramfs</span><br><span class="line"><span class="built_in">cp</span> ../_install/* -rf ./</span><br><span class="line"><span class="built_in">mkdir</span> dev proc sys</span><br><span class="line"><span class="built_in">mkdir</span> -p lib/modules</span><br><span class="line"><span class="built_in">mkdir</span> -p etc/init.d</span><br><span class="line"><span class="built_in">touch</span> etc/init.d/rcS</span><br><span class="line"><span class="built_in">chmod</span> a+x etc/init.d/rcS</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">cp</span> -a /dev/&#123;null,console,<span class="built_in">tty</span>,tty1,tty2,tty3,tty4&#125; dev/</span><br><span class="line"><span class="built_in">cp</span> ~/linux-5.3.7/drivers/net/ethernet/intel/e1000/e1000.ko lib/modules</span><br><span class="line"><span class="built_in">rm</span> linuxrc</span><br><span class="line">vim init</span><br><span class="line"><span class="built_in">chmod</span> a+x init</span><br></pre></td></tr></table></figure><p><code>init</code> 文件内容如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">mount -t proc none /proc</span><br><span class="line">mount -t sysfs none /sys</span><br><span class="line">insmod /lib/modules/e1000.ko</span><br><span class="line">/sbin/mdev -s</span><br><span class="line">setsid cttyhack /bin/sh</span><br><span class="line"><span class="built_in">exec</span> /sbin/init</span><br></pre></td></tr></table></figure><p>最后打包initramfs:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">find . -print0 | cpio --null -ov --format=newc | gzip -9 &gt; ../initramfs.cpio.gz</span><br></pre></td></tr></table></figure><h2 id="启动内核并调试"><a href="#启动内核并调试" class="headerlink" title="启动内核并调试"></a>启动内核并调试</h2><p>执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">qemu-system-x86_64 -s -S -kernel ~/linux-5.3.7/arch/x86/boot/bzImage -initrd ~/busybox-1.31.0/initramfs.cpio.gz -nographic -append <span class="string">&quot;console=ttyS0&quot;</span> -serial mon:stdio -device e1000,netdev=net0 -netdev user,<span class="built_in">id</span>=net0,hostfwd=tcp::5555-:22</span><br></pre></td></tr></table></figure><p>启动内核以及 GDB Server，其中 -s 选项是 -gdb 的简写，会在本地的 :1234 启动一个 GDB 服务，然后 -S 代表暂停虚拟机，等待 GDB 执行 continue 指令。append 后面是内核的启动选项。</p><p>然后切换到内核源码目录，启动 <code>gdb</code>，不过在启动之前，请向 <code>~/linux-5.3.7/.gdbinit</code> 添加如下内容：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">add-auto-load-safe-path ./scripts/gdb/vmlinux-gdb.py</span><br></pre></td></tr></table></figure><p>来加载内核调试工具，然后执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ~/linux-5.3.7</span><br><span class="line">gdb vmlinux</span><br><span class="line">(gdb) target remote :1234</span><br></pre></td></tr></table></figure><p>来连接到虚拟机上的 gdb 服务。</p><p>到这里，你就可以像调试普通程序一样调试 Linux 内核了。Linux 的内核入口函数是位于 <code>init/main.c</code> 中的 <code>start_kernel</code> ，在这里完成各种内核数据结构的初始化。但是这已经是 bootstrap 很后面的过程了。要想看真正的第一行代码，请到 <code>arch/x86/boot/header.S</code></p><p>可以通过下面的命令来看看内核调试工具：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">(gdb) apropos lx</span><br><span class="line"><span class="keyword">function</span> lx_clk_core_lookup -- Find struct clk_core by name</span><br><span class="line"><span class="keyword">function</span> lx_current -- Return current task</span><br><span class="line"><span class="keyword">function</span> lx_device_find_by_bus_name -- Find struct device by bus and name (both strings)</span><br><span class="line"><span class="keyword">function</span> lx_device_find_by_class_name -- Find struct device by class and name (both strings)</span><br><span class="line"><span class="keyword">function</span> lx_module -- Find module by name and <span class="built_in">return</span> the module variable</span><br><span class="line"><span class="keyword">function</span> lx_per_cpu -- Return per-cpu variable</span><br><span class="line"><span class="keyword">function</span> lx_rb_first -- Lookup and <span class="built_in">return</span> a node from an RBTree</span><br><span class="line"><span class="keyword">function</span> lx_rb_last -- Lookup and <span class="built_in">return</span> a node from an RBTree</span><br><span class="line"><span class="keyword">function</span> lx_rb_next -- Lookup and <span class="built_in">return</span> a node from an RBTree</span><br><span class="line"><span class="keyword">function</span> lx_rb_prev -- Lookup and <span class="built_in">return</span> a node from an RBTree</span><br><span class="line"><span class="keyword">function</span> lx_task_by_pid -- Find Linux task by PID and <span class="built_in">return</span> the task_struct variable</span><br><span class="line"><span class="keyword">function</span> lx_thread_info -- Calculate Linux thread_info from task variable</span><br><span class="line"><span class="keyword">function</span> lx_thread_info_by_pid -- Calculate Linux thread_info from task variable found by pid</span><br><span class="line">lx-clk-summary -- Print clk tree summary</span><br><span class="line">lx-cmdline --  Report the Linux Commandline used <span class="keyword">in</span> the current kernel</span><br><span class="line">lx-configdump -- Output kernel config to the filename specified as the <span class="built_in">command</span></span><br><span class="line">lx-cpus -- List CPU status arrays</span><br><span class="line">lx-device-list-bus -- Print devices on a bus (or all buses <span class="keyword">if</span> not specified)</span><br><span class="line">lx-device-list-class -- Print devices <span class="keyword">in</span> a class (or all classes <span class="keyword">if</span> not specified)</span><br><span class="line">lx-device-list-tree -- Print a device and its children recursively</span><br><span class="line">lx-dmesg -- Print Linux kernel <span class="built_in">log</span> buffer</span><br><span class="line">lx-fdtdump -- Output Flattened Device Tree header and dump FDT blob to the filename</span><br><span class="line">lx-genpd-summary -- Print genpd summary</span><br><span class="line">lx-iomem -- Identify the IO memory resource locations defined by the kernel</span><br><span class="line">lx-ioports -- Identify the IO port resource locations defined by the kernel</span><br><span class="line">lx-list-check -- Verify a list consistency</span><br><span class="line">lx-lsmod -- List currently loaded modules</span><br><span class="line">lx-mounts -- Report the VFS mounts of the current process namespace</span><br><span class="line">lx-ps -- Dump Linux tasks</span><br><span class="line">lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules</span><br><span class="line">lx-timerlist -- Print /proc/timer_list</span><br><span class="line">lx-version --  Report the Linux Version of the current kernel</span><br><span class="line">(gdb) lx-cmdline</span><br><span class="line">console=ttyS0</span><br></pre></td></tr></table></figure><p>我们试下打下断点：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line">(gdb) <span class="built_in">break</span> cmdline_proc_show</span><br><span class="line">Breakpoint 1 at 0xffffffff81353110: file fs/proc/cmdline.c, line 8.</span><br><span class="line">(gdb) <span class="built_in">continue</span></span><br><span class="line">Continuing.</span><br><span class="line"> </span><br><span class="line">Breakpoint 1, cmdline_proc_show (m=0xffff8880076ebb80, v=0x1 &lt;fixed_percpu_data+1&gt;) at fs/proc/cmdline.c:8</span><br><span class="line">8       &#123;</span><br><span class="line">(gdb) n</span><br><span class="line">9               seq_puts(m, saved_command_line);</span><br><span class="line">(gdb) bt</span><br><span class="line"><span class="comment">#0  cmdline_proc_show (m=0xffff8880076ebb80, v=0x1 &lt;fixed_percpu_data+1&gt;) at fs/proc/cmdline.c:9</span></span><br><span class="line"><span class="comment">#1  0xffffffff812e7d77 in seq_read (file=&lt;optimized out&gt;, buf=&lt;optimized out&gt;, size=&lt;optimized out&gt;, ppos=&lt;optimized out&gt;)</span></span><br><span class="line">    at fs/seq_file.c:229</span><br><span class="line"><span class="comment">#2  0xffffffff8134a01e in proc_reg_read (file=&lt;optimized out&gt;, buf=&lt;optimized out&gt;, count=&lt;optimized out&gt;,</span></span><br><span class="line">    ppos=&lt;optimized out&gt;) at fs/proc/inode.c:223</span><br><span class="line"><span class="comment">#3  0xffffffff812bae45 in do_loop_readv_writev (flags=&lt;optimized out&gt;, type=&lt;optimized out&gt;, ppos=&lt;optimized out&gt;,</span></span><br><span class="line">    iter=&lt;optimized out&gt;, filp=&lt;optimized out&gt;) at fs/read_write.c:714</span><br><span class="line"><span class="comment">#4  do_loop_readv_writev (flags=&lt;optimized out&gt;, type=&lt;optimized out&gt;, ppos=&lt;optimized out&gt;, iter=&lt;optimized out&gt;,</span></span><br><span class="line">    filp=&lt;optimized out&gt;) at fs/read_write.c:701</span><br><span class="line"><span class="comment">#5  do_iter_read (file=0xffff8880076a5d00, iter=0xffffc900001f3ac8, pos=0xffffc900001f3bf0, flags=&lt;optimized out&gt;)</span></span><br><span class="line">    at fs/read_write.c:935</span><br><span class="line"><span class="comment">#6  0xffffffff812bd8dd in vfs_readv (file=0xffff8880076a5d00, vec=&lt;optimized out&gt;, vlen=&lt;optimized out&gt;,</span></span><br><span class="line">    pos=0xffffc900001f3bf0, flags=0) at fs/read_write.c:997</span><br><span class="line"><span class="comment">#7  0xffffffff812f65b1 in kernel_readv (offset=&lt;optimized out&gt;, vlen=&lt;optimized out&gt;, vec=&lt;optimized out&gt;,</span></span><br><span class="line">    file=&lt;optimized out&gt;) at fs/splice.c:359</span><br><span class="line"><span class="comment">#8  default_file_splice_read (in=&lt;optimized out&gt;, ppos=0xffffc900001f3dd0, pipe=&lt;optimized out&gt;, len=&lt;optimized out&gt;,</span></span><br><span class="line">    flags=&lt;optimized out&gt;) at fs/splice.c:414</span><br><span class="line"><span class="comment">#9  0xffffffff812f43a9 in do_splice_to (in=0xffff8880076a5d00, ppos=0xffffc900001f3dd0, pipe=0xffff88800611c180,</span></span><br><span class="line">    len=&lt;optimized out&gt;, flags=0) at fs/splice.c:877</span><br><span class="line"><span class="comment">#10 0xffffffff812f4491 in splice_direct_to_actor (in=&lt;optimized out&gt;, sd=0x1 &lt;fixed_percpu_data+1&gt;, actor=&lt;optimized out&gt;)</span></span><br><span class="line">    at fs/splice.c:954</span><br><span class="line"><span class="comment">#11 0xffffffff812f4688 in do_splice_direct (in=0xffff8880076a5d00, ppos=0xffffc900001f3ea8, out=&lt;optimized out&gt;,</span></span><br><span class="line">    opos=&lt;optimized out&gt;, len=&lt;optimized out&gt;, flags=&lt;optimized out&gt;) at fs/splice.c:1063</span><br><span class="line"><span class="comment">#12 0xffffffff812bbf12 in do_sendfile (out_fd=&lt;optimized out&gt;, in_fd=&lt;optimized out&gt;, ppos=0x0 &lt;fixed_percpu_data&gt;,</span></span><br><span class="line">    count=&lt;optimized out&gt;, max=&lt;optimized out&gt;) at fs/read_write.c:1464</span><br><span class="line"><span class="comment">#13 0xffffffff812bc5d6 in __do_sys_sendfile64 (count=&lt;optimized out&gt;, offset=&lt;optimized out&gt;, in_fd=&lt;optimized out&gt;,</span></span><br><span class="line">    out_fd=&lt;optimized out&gt;) at fs/read_write.c:1525</span><br><span class="line"><span class="comment">#14 __se_sys_sendfile64 (count=&lt;optimized out&gt;, offset=&lt;optimized out&gt;, in_fd=&lt;optimized out&gt;, out_fd=&lt;optimized out&gt;)</span></span><br><span class="line">    at fs/read_write.c:1511</span><br><span class="line"><span class="comment">#15 __x64_sys_sendfile64 (regs=&lt;optimized out&gt;) at fs/read_write.c:1511</span></span><br><span class="line"><span class="comment">#16 0xffffffff810043aa in do_syscall_64 (nr=&lt;optimized out&gt;, regs=0x1 &lt;fixed_percpu_data+1&gt;) at arch/x86/entry/common.c:296</span></span><br><span class="line"><span class="comment">#17 0xffffffff81c0008c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175</span></span><br><span class="line"><span class="comment">#18 0x0000000000000000 in ?? ()</span></span><br><span class="line">(gdb)</span><br></pre></td></tr></table></figure><p>在执行 continue 之后，到 qemu 中执行 <code>cat /proc/cmdline</code> ，就可以看到断点被触发了。这时候可以进行堆栈打印，或者是其他操作。不过，神奇的是，内核强制打开了 -O2 选项编译，而且不能关闭，因为有很多代码依赖 -O2 优化特性！所以，你会看到很多变量的值被优化掉了。。</p><p>gdb 中调用函数的方法是 <code>&amp;#36;func_name(params)</code></p><p>设置变量的方法是 <code>set &amp;#36;var = xxx：</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">(gdb) p <span class="variable">$lx</span>\_current().pid<span class="variable">$1</span> = 176</span><br><span class="line">(gdb) <span class="built_in">set</span> <span class="variable">$next</span> =<span class="variable">$lx_per_cpu</span>(<span class="string">&quot;hrtimer_bases&quot;</span>).clock_base[0].active.next</span><br><span class="line">(gdb) p *<span class="variable">$container</span>\_of(<span class="variable">$next</span>, <span class="string">&quot;struct hrtimer&quot;</span>, <span class="string">&quot;node&quot;</span>)</span><br><span class="line"><span class="variable">$3</span> = &#123;node = &#123;node = &#123;__rb_parent_color = 18446612682195984864, rb_right = 0x0 &lt;fixed_percpu_data&gt;,</span><br><span class="line">      rb_left = 0x0 &lt;fixed_percpu_data&gt;&#125;, expires = 9316000000&#125;, _softexpires = 9316000000,</span><br><span class="line">  <span class="keyword">function</span> = 0xffffffff8112f4f0 &lt;tick_sched_timer&gt;, base = 0xffff88800781da80, state = 1 <span class="string">&#x27;01&#x27;</span>, is_rel = 0 <span class="string">&#x27;00&#x27;</span>,</span><br><span class="line">  is_soft = 0 <span class="string">&#x27;00&#x27;</span>&#125;</span><br></pre></td></tr></table></figure><h2 id="配置-VSCode"><a href="#配置-VSCode" class="headerlink" title="配置 VSCode"></a>配置 VSCode</h2><p>由于 Linux 内核高度定制化，所以没有办法直接通过配置 <code>includePath</code> 等让 Intellisense 正常提示，这里借助一个 Python 脚本来生成 <code>compile_commands.json</code> 文件帮助 Intellisense 正常提示（包括头文件和宏定义等）</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> __future__ <span class="keyword">import</span> print_function, division</span><br><span class="line"> </span><br><span class="line"><span class="keyword">import</span> fnmatch</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> math</span><br><span class="line"><span class="keyword">import</span> multiprocessing</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line">CMD_VAR_RE = re.<span class="built_in">compile</span>(<span class="string">r&#x27;^\s*cmd_(\S+)\s*:=\s*(.+)\s*$&#x27;</span>, re.MULTILINE)</span><br><span class="line">SOURCE_VAR_RE = re.<span class="built_in">compile</span>(<span class="string">r&#x27;^\s*source_(\S+)\s*:=\s*(.+)\s*$&#x27;</span>, re.MULTILINE)</span><br><span class="line"> </span><br><span class="line">directory = os.path.abspath(os.getcwd())</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">print_progress_bar</span>(<span class="params">progress</span>):</span><br><span class="line">    progress_bar = <span class="string">&#x27;[&#x27;</span> + <span class="string">&#x27;|&#x27;</span> * <span class="built_in">int</span>(<span class="number">50</span> * progress) + <span class="string">&#x27;-&#x27;</span> * <span class="built_in">int</span>(<span class="number">50</span> * (<span class="number">1.0</span> - progress)) + <span class="string">&#x27;]&#x27;</span></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;\r&#x27;</span>, progress_bar, <span class="string">&quot;&#123;0:.1%&#125;&quot;</span>.<span class="built_in">format</span>(progress), end=<span class="string">&#x27;\r&#x27;</span>, file=sys.stderr)</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse_cmd_file</span>(<span class="params">cmdfile_path</span>):</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(cmdfile_path, <span class="string">&#x27;r&#x27;</span>) <span class="keyword">as</span> cmdfile:</span><br><span class="line">        cmdfile_content = cmdfile.read()</span><br><span class="line"> </span><br><span class="line">    commands = &#123; <span class="keyword">match</span>.group(<span class="number">1</span>): <span class="keyword">match</span>.group(<span class="number">2</span>) <span class="keyword">for</span> <span class="keyword">match</span> <span class="keyword">in</span> CMD_VAR_RE.finditer(cmdfile_content) &#125;</span><br><span class="line">    sources = &#123; <span class="keyword">match</span>.group(<span class="number">1</span>): <span class="keyword">match</span>.group(<span class="number">2</span>) <span class="keyword">for</span> <span class="keyword">match</span> <span class="keyword">in</span> SOURCE_VAR_RE.finditer(cmdfile_content) &#125;</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">return</span> [&#123;</span><br><span class="line">            <span class="string">&#x27;directory&#x27;</span>: directory,</span><br><span class="line">            <span class="string">&#x27;command&#x27;</span>: commands[o_file_name],</span><br><span class="line">            <span class="string">&#x27;file&#x27;</span>: source,</span><br><span class="line">            <span class="string">&#x27;output&#x27;</span>: o_file_name</span><br><span class="line">        &#125; <span class="keyword">for</span> o_file_name, source <span class="keyword">in</span> sources.items()]</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Building *.o.cmd file list...&quot;</span>, file=sys.stderr)</span><br><span class="line"> </span><br><span class="line">    cmd_files = []</span><br><span class="line">    <span class="keyword">for</span> cur_dir, subdir, files <span class="keyword">in</span> os.walk(directory):</span><br><span class="line">        cmd_files.extend(os.path.join(cur_dir, cmdfile_name) <span class="keyword">for</span> cmdfile_name <span class="keyword">in</span> fnmatch.<span class="built_in">filter</span>(files, <span class="string">&#x27;*.o.cmd&#x27;</span>))</span><br><span class="line"> </span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Parsing *.o.cmd files...&quot;</span>, file=sys.stderr)</span><br><span class="line"> </span><br><span class="line">    n_processed = <span class="number">0</span></span><br><span class="line">    print_progress_bar(<span class="number">0</span>)</span><br><span class="line"> </span><br><span class="line">    compdb = []</span><br><span class="line">    pool = multiprocessing.Pool()</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">for</span> compdb_chunk <span class="keyword">in</span> pool.imap_unordered(parse_cmd_file, cmd_files, chunksize=<span class="built_in">int</span>(math.sqrt(<span class="built_in">len</span>(cmd_files)))):</span><br><span class="line">            compdb.extend(compdb_chunk)</span><br><span class="line">            n_processed += <span class="number">1</span></span><br><span class="line">            print_progress_bar(n_processed / <span class="built_in">len</span>(cmd_files))</span><br><span class="line"> </span><br><span class="line">    <span class="keyword">finally</span>:</span><br><span class="line">        pool.terminate()</span><br><span class="line">        pool.join()</span><br><span class="line"> </span><br><span class="line">    <span class="built_in">print</span>(file=sys.stderr)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;Writing compile_commands.json...&quot;</span>, file=sys.stderr)</span><br><span class="line">    <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&#x27;compile_commands.json&#x27;</span>, <span class="string">&#x27;w&#x27;</span>) <span class="keyword">as</span> compdb_file:</span><br><span class="line">        json.dump(compdb, compdb_file, indent=<span class="number">1</span>)</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    main()</span><br></pre></td></tr></table></figure><p>将这个 Python 脚本保存成 <code>gen_commands.py</code> 之后在 Linux 源代码目录下直接运行就可以生成 <code>compile_commands.json</code> 了。</p><p>最后分别配置 VSCode 配置文件：</p><h3 id="vscode-c-cpp-properties-json"><a href="#vscode-c-cpp-properties-json" class="headerlink" title=".vscode&#x2F;c_cpp_properties.json"></a>.vscode&#x2F;c_cpp_properties.json</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="string">&quot;configurations&quot;</span>: [</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="string">&quot;name&quot;</span>: <span class="string">&quot;Linux&quot;</span>,</span><br><span class="line">            <span class="string">&quot;cStandard&quot;</span>: <span class="string">&quot;c11&quot;</span>,</span><br><span class="line">            <span class="string">&quot;intelliSenseMode&quot;</span>: <span class="string">&quot;gcc-x64&quot;</span>,</span><br><span class="line">            <span class="string">&quot;compileCommands&quot;</span>: <span class="string">&quot;$&#123;workspaceFolder&#125;/compile_commands.json&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    ],</span><br><span class="line">    <span class="string">&quot;version&quot;</span>: <span class="number">4</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="vscode-tasks-json"><a href="#vscode-tasks-json" class="headerlink" title=".vscode&#x2F;tasks.json"></a>.vscode&#x2F;tasks.json</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="comment">// See https://go.microsoft.com/fwlink/?LinkId=733558</span></span><br><span class="line">    <span class="comment">// for the documentation about the tasks.json format</span></span><br><span class="line">    <span class="string">&quot;version&quot;</span>: <span class="string">&quot;2.0.0&quot;</span>,</span><br><span class="line">    <span class="string">&quot;tasks&quot;</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&quot;label&quot;</span>: <span class="string">&quot;vm&quot;</span>,</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;shell&quot;</span>,</span><br><span class="line">        <span class="string">&quot;command&quot;</span>: <span class="string">&quot;qemu-system-x86_64 -s -S -kernel ~/linux-5.3.7/arch/x86/boot/bzImage -initrd ~/busybox-1.31.0/initramfs.cpio.gz -nographic -append &quot;</span><span class="variable language_">console</span>=ttyS0<span class="string">&quot; -serial mon:stdio -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:22&quot;</span>,</span><br><span class="line">        <span class="string">&quot;presentation&quot;</span>: &#123;</span><br><span class="line">          <span class="string">&quot;echo&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">          <span class="string">&quot;clear&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">          <span class="string">&quot;group&quot;</span>: <span class="string">&quot;vm&quot;</span></span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;isBackground&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="string">&quot;problemMatcher&quot;</span>: [</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&quot;pattern&quot;</span>: [</span><br><span class="line">              &#123;</span><br><span class="line">                <span class="string">&quot;regexp&quot;</span>: <span class="string">&quot;.&quot;</span>,</span><br><span class="line">                <span class="string">&quot;file&quot;</span>: <span class="number">1</span>,</span><br><span class="line">                <span class="string">&quot;location&quot;</span>: <span class="number">2</span>,</span><br><span class="line">                <span class="string">&quot;message&quot;</span>: <span class="number">3</span></span><br><span class="line">              &#125;</span><br><span class="line">            ],</span><br><span class="line">            <span class="string">&quot;background&quot;</span>: &#123;</span><br><span class="line">              <span class="string">&quot;activeOnStart&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">              <span class="string">&quot;beginsPattern&quot;</span>: <span class="string">&quot;.&quot;</span>,</span><br><span class="line">              <span class="string">&quot;endsPattern&quot;</span>: <span class="string">&quot;.&quot;</span>,</span><br><span class="line">            &#125;</span><br><span class="line">          &#125;</span><br><span class="line">        ]</span><br><span class="line">      &#125;,</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&quot;label&quot;</span>: <span class="string">&quot;build&quot;</span>,</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;shell&quot;</span>,</span><br><span class="line">        <span class="string">&quot;command&quot;</span>: <span class="string">&quot;make&quot;</span>,</span><br><span class="line">        <span class="string">&quot;group&quot;</span>: &#123;</span><br><span class="line">          <span class="string">&quot;kind&quot;</span>: <span class="string">&quot;build&quot;</span>,</span><br><span class="line">          <span class="string">&quot;isDefault&quot;</span>: <span class="literal">true</span></span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;presentation&quot;</span>: &#123;</span><br><span class="line">          <span class="string">&quot;echo&quot;</span>: <span class="literal">false</span>,</span><br><span class="line">          <span class="string">&quot;group&quot;</span>: <span class="string">&quot;build&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><h3 id="vscode-launch-json"><a href="#vscode-launch-json" class="headerlink" title=".vscode&#x2F;launch.json"></a>.vscode&#x2F;launch.json</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="comment">// Use IntelliSense to learn about possible attributes.</span></span><br><span class="line">    <span class="comment">// Hover to view descriptions of existing attributes.</span></span><br><span class="line">    <span class="comment">// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387</span></span><br><span class="line">    <span class="string">&quot;version&quot;</span>: <span class="string">&quot;0.2.0&quot;</span>,</span><br><span class="line">    <span class="string">&quot;configurations&quot;</span>: [</span><br><span class="line">      &#123;</span><br><span class="line">        <span class="string">&quot;name&quot;</span>: <span class="string">&quot;(gdb) linux&quot;</span>,</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;cppdbg&quot;</span>,</span><br><span class="line">        <span class="string">&quot;request&quot;</span>: <span class="string">&quot;launch&quot;</span>,</span><br><span class="line">        <span class="string">&quot;preLaunchTask&quot;</span>: <span class="string">&quot;vm&quot;</span>,</span><br><span class="line">        <span class="string">&quot;program&quot;</span>: <span class="string">&quot;$&#123;workspaceRoot&#125;/vmlinux&quot;</span>,</span><br><span class="line">        <span class="string">&quot;miDebuggerServerAddress&quot;</span>: <span class="string">&quot;localhost:1234&quot;</span>,</span><br><span class="line">        <span class="string">&quot;args&quot;</span>: [],</span><br><span class="line">        <span class="string">&quot;stopAtEntry&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="string">&quot;cwd&quot;</span>: <span class="string">&quot;$&#123;workspaceFolder&#125;&quot;</span>,</span><br><span class="line">        <span class="string">&quot;environment&quot;</span>: [],</span><br><span class="line">        <span class="string">&quot;externalConsole&quot;</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="string">&quot;MIMode&quot;</span>: <span class="string">&quot;gdb&quot;</span>,</span><br><span class="line">        <span class="string">&quot;miDebuggerArgs&quot;</span>: <span class="string">&quot;-n&quot;</span>,</span><br><span class="line">        <span class="string">&quot;targetArchitecture&quot;</span>: <span class="string">&quot;x64&quot;</span>,</span><br><span class="line">        <span class="string">&quot;setupCommands&quot;</span>: [</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&quot;text&quot;</span>: <span class="string">&quot;set arch i386:x86-64:intel&quot;</span>,</span><br><span class="line">            <span class="string">&quot;ignoreFailures&quot;</span>: <span class="literal">false</span></span><br><span class="line">          &#125;,</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&quot;text&quot;</span>: <span class="string">&quot;dir .&quot;</span>,</span><br><span class="line">            <span class="string">&quot;ignoreFailures&quot;</span>: <span class="literal">false</span></span><br><span class="line">          &#125;,</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&quot;text&quot;</span>: <span class="string">&quot;add-auto-load-safe-path ./&quot;</span>,</span><br><span class="line">            <span class="string">&quot;ignoreFailures&quot;</span>: <span class="literal">false</span></span><br><span class="line">          &#125;,</span><br><span class="line">          &#123;</span><br><span class="line">            <span class="string">&quot;text&quot;</span>: <span class="string">&quot;-enable-pretty-printing&quot;</span>,</span><br><span class="line">            <span class="string">&quot;ignoreFailures&quot;</span>: <span class="literal">true</span></span><br><span class="line">          &#125;</span><br><span class="line">        ]</span><br><span class="line">      &#125;</span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><h3 id="vscode-settings-json"><a href="#vscode-settings-json" class="headerlink" title=".vscode&#x2F;settings.json"></a>.vscode&#x2F;settings.json</h3><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="string">&quot;files.exclude&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;**/.*.*.cmd&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="string">&quot;**/.*.d&quot;</span>: <span class="literal">true</span>,</span><br><span class="line">        <span class="string">&quot;**/.*.S&quot;</span>: <span class="literal">true</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="string">&quot;[c]&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;editor.detectIndentation&quot;</span>: <span class="literal">false</span>,</span><br><span class="line">        <span class="string">&quot;editor.tabSize&quot;</span>: <span class="number">8</span>,</span><br><span class="line">        <span class="string">&quot;editor.insertSpaces&quot;</span>: <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后就可以到处打断点，按 F5 开始欢快地调试了~<a href="/programming/debugging-linux-kernel-with-vscode-qemu/image-1571903124350.png" data-fancybox="gallery" data-caption=""><img src="/programming/debugging-linux-kernel-with-vscode-qemu/image-1571903124350.png"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Spam filtering with Naïve Bayes</title>
      <link>https://blog.howardlau.me/machine-learning/spam-filtering-with-naive-bayes.html</link>
      <description>
        <![CDATA[<h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Suppose we are receving lots of]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Fri, 20 Sep 2019 04:12:34 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="The-Problem"><a href="#The-Problem" class="headerlink" title="The Problem"></a>The Problem</h2><p>Suppose we are receving lots of E-mails every day and a number of them are spam emails, we want to classify whether the E-mail is a spam or not. To help solve the problem, we have already prepared some E-mails with labels telling us whether one is spam or not.</p><h2 id="The-Algorithm"><a href="#The-Algorithm" class="headerlink" title="The Algorithm"></a>The Algorithm</h2><p>Firstly, we construct a vocabulary $V &#x3D; \{v_1, v_2, v_3, \ldots,v_n\}$ from the training dataset by collecting all the unique words in all emails and assigning them an index $i \in \mathbf{N}$, so that for a word $w$ labeled $i$, $v_i&#x3D;w$, where $n$ is the total number of unique words.</p><p>For an E-mail $D$ with words $W&#x3D;\{w_1, w_2, \ldots, w_{n_d}\}$, where $n_d$ is the number of words in the email, we can extract its feature by counting frequencies of each word and form a vector $\textbf{x} &#x3D; \langle x_1, x_2, x_3,\ldots,x_{|V|}\rangle \in \mathbf{Z}^{|V|}$, where $x_i &#x3D; count_{w \in W}(w&#x3D;v_i)$. We assign a class $y \in \mathbf{C}$ to each email, where $\mathbf{C}&#x3D;\{c_1, c_2, \ldots, c_{n_c}\}$ and $n_c$ is the number of unique labels. Then we can construct sample set  $\mathbf{X} &#x3D; \{\mathbf{x}_1, \mathbf{x}_2\ldots,\mathbf{x}_{n_x}\}$ where $n_x$ is number of training samples and ground truths set $\mathbf{Y}&#x3D;\{y_1, y_2, \ldots, y_{n_x}\}$.</p><p>For an E-mail $D$ with words $W&#x3D;\{w_1, w_2, \ldots, w_{n_d}\}$, the probability of belonging to class $c_i$ is:</p><p>$$P(c_i|w_1, w_2, \ldots, w_{n_d})&#x3D;\frac{P(w_1, w_2, \ldots, w_{n_d}|c_i)P(c_i)}{P(w_1, w_2, \ldots, w_{n_d})}$$</p><p>To determine the class the E-mail belongs to, we only need to take argmax of every probability:</p><p>$$\hat{y}&#x3D;\arg \max_{c}P(c|w_1, w_2, \ldots, w_{n_d})$$</p><p>Because $P(w_1, w_2, \ldots, w_{n_d})$ is constant, we can omit the item from denominator. And according to the naïve assumption that words are independent, we can simplify the formula to:</p><p>$$\hat{y}&#x3D;\arg \max_{c}P(c)\prod^{n_d}_{i&#x3D;1}P(w_i|c)$$</p><p>It is easy to compute $P(c_i)$:</p><p>$$P(c_i)&#x3D;\frac{count_{y \in \mathbf{Y}}(y&#x3D;c_i)}{|\mathbf{Y}|}$$</p><p>To compute $P(w|c)$:</p><p>$$P(w_i|c)&#x3D;\frac{count_{all\ words}(w&#x3D;w_i, y&#x3D;c)}{count_{all\ words}(y&#x3D;c)}$$</p><p>However, for a word $w_i$ that never appears in class $c$, the probability will become 0 which is unreasonable. So we can assume a new word <code>&lt;UNK&gt;</code>, and map all those words to <code>&lt;UNK&gt;</code>. We can achieve this by adding a smoothing factor $0\lt\alpha\le1$ to the probability:</p><p>$$P(w_i|c)&#x3D;\frac{count_{all\ words}(w&#x3D;w_i, y&#x3D;c)+\alpha}{count_{all\ words}(y&#x3D;c)+\alpha(|V|+1)}$$</p><p>Moreover, to avoid floating point precision problem, we take the logarithm of the probability and add them up instead of multiplying them:</p><p>$$\hat{y}&#x3D;\arg\max_{c}\log P(c)\sum^{n_d}_{i&#x3D;1}\log P(w_i|c)$$</p><p>This is our Naïve Bayes classifier model.</p><p>Get the code here: <a href="https://github.com/howardlau1999/learning-machine-learning/tree/master/naive_bayes">https://github.com/howardlau1999/learning-machine-learning/tree/master/naive_bayes</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>游戏推荐 | 70 亿人类</title>
      <link>https://blog.howardlau.me/game/7-billion-humans.html</link>
      <description>
        <![CDATA[<p>两年前我在博客上推荐了 <a href="https://blog.howardlau.me/game/human-resource-machine.html">《人力资源机器》</a> 这款游戏，时隔一年半，原班人马在 2018 年再度发布力作《70 亿人类》（7]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/game/">游戏推荐</category>
      <pubDate>Thu, 05 Sep 2019 20:34:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>两年前我在博客上推荐了 <a href="https://blog.howardlau.me/game/human-resource-machine.html">《人力资源机器》</a> 这款游戏，时隔一年半，原班人马在 2018 年再度发布力作《70 亿人类》（7 Billion Humans）。虽然名字看上去大不一样，但画风还是核心玩法都还是一样的味道，只不过这次编程面临着更大的挑战：你不再是为一个人编程，而是为<strong>一群人</strong>编程！</p><p><a href="/game/7-billion-humans/20190905194548_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194548_1.jpg"></a></p><h2 id="游戏介绍"><a href="#游戏介绍" class="headerlink" title="游戏介绍"></a>游戏介绍</h2><p><a href="/game/7-billion-humans/20190905194604_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194604_1.jpg"></a></p><p>在游戏中，<strong>玩家</strong>只有一个人，和游戏的交互方式就是在右边的程序框里用一些极其简单的指令编写程序，比如从地上捡起一个数据块、向一个方向移动一步等等。而游戏的<strong>目标</strong>则每关都不同，比如把所有的格子都放满数据块、或者把小于 50 的数据块都销毁掉。如果所有程序都执行完毕，游戏会检查数据块的摆放是否符合关卡要求，符合则通关，否则不通关。</p><p>这款游戏更像是功能游戏，玩的时候其实跟程序员写程序一样，可能会有点痛苦。但是它用一种更友好的方式来展现计算机的一些基本的运行原理，让一些不是学计算机的玩家也能体会到编程的感觉，甚至说不定就让玩家正式踏入了编程的大门。可以说，我就是受这款游戏启发，去学习了着色器编程。</p><p><a href="/game/7-billion-humans/20190905194728_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194728_1.jpg"></a> </p><p>如果你玩过<a href="https://blog.howardlau.me/game/human-resource-machine.html">《人力资源机器》</a> ，你一定不会对游戏界面感到陌生，但是会有一点惊讶。如果说《人力资源机器》是为 CPU 编程，那么《70 亿人类》就更像是为 GPU 编程——每个人执行的都是同样的程序。就这一个小小的改动，可能会使编程的难度直线上升：如何让同样的程序执行得到不同的结果？怎么保证程序之间不会互相冲突？更难的是，我们怎么调试程序？</p><p>还好，游戏提供了一种机制，能让我们在运行的时候单独观察每一个人的执行状态：</p><p><a href="/game/7-billion-humans/20190905194745_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194745_1.jpg"></a></p><p>地上的洞则为游戏带来的更大的<strong>挑战</strong>：如果一个人掉进了洞里，那你就少了一个运行的程序（也就是崩溃了）。要是所有人都掉下去了，游戏则会直接宣告失败：</p><p><a href="/game/7-billion-humans/20190905194804_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194804_1.jpg"></a></p><p>哪怕你写好了程序有惊无险地通关了，还有两个更大的<strong>挑战</strong>等着你：把程序写<strong>短</strong>、写<strong>快</strong>。</p><p><a href="/game/7-billion-humans/20190905194620_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190905194620_1.jpg"></a></p><p>当然很多时候这两者是矛盾的，不可能同时达成，所以有可能需要玩家写出不同的程序来达成挑战。在选关界面也能看到自己完成了哪些挑战：</p><p><a href="/game/7-billion-humans/20190906121713_1.jpg" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/20190906121713_1.jpg"></a></p><p>就像<a href="https://blog.howardlau.me/game/human-resource-machine.html">《人力资源机器》</a> 一样，游戏用一种直观的方式教育玩家计算机的工作原理：就是让硬件执行一些非常简单的指令。然而就是这些简单的指令构成了强大的计算机软件。</p><p>而这款游戏个人感觉更像是告诉了玩家 GPU 的工作原理，GPU 中有许多小型的计算单元，它们的计算能力远远不如 CPU，甚至不支持逻辑运算，只支持算术运算。但是，人多力量大，只要精心编写程序，就能实现令人惊艳的效果。</p><p>GPU 中的着色器就是这样的原理，每个着色器都并行地执行一样的程序，只是输入有所不同，而且着色器只会做算术运算，所以运算速度特别快。就像下面这个神奇的隧道效果，就是短短一百多行的程序实现的（ <a href="https://www.shadertoy.com/view/4lcGWr">https://www.shadertoy.com/view/4lcGWr</a> ）：</p><p><a href="/game/7-billion-humans/screenshot-www.shadertoy.com-2019.09.06-12-04-28.png" data-fancybox="gallery" data-caption=""><img src="/game/7-billion-humans/screenshot-www.shadertoy.com-2019.09.06-12-04-28.png"></a></p><p>而让程序写短，则是为了节省计算机的内存，让程序跑得快，则是带来更好的体验。想象一下，或许你能接受安装一个几十 GB 的大游戏，要是一个游戏卡成 PPT，你还会去玩吗？</p><p>如果你也想学习着色器编程，可以看看：<a href="https://thebookofshaders.com/">https://thebookofshaders.com/</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用 C 语言编写 ANN 实现 XOR 异或函数</title>
      <link>https://blog.howardlau.me/machine-learning/xor-net-in-c.html</link>
      <description>
        <![CDATA[<p>介绍用纯 C 语言实现一个简单的 BP 网络，并使用 XOR 数据测试。</p>
<p>完整代码可参考：<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Wed, 04 Sep 2019 06:22:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>介绍用纯 C 语言实现一个简单的 BP 网络，并使用 XOR 数据测试。</p><p>完整代码可参考：<a href="https://github.com/howardlau1999/learning-machine-learning/blob/master/mlp_work.c">https://github.com/howardlau1999/learning-machine-learning/blob/master/mlp_work.c</a></p><h2 id="辅助函数"><a href="#辅助函数" class="headerlink" title="辅助函数"></a>辅助函数</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">double</span> <span class="title function_">random_</span><span class="params">(<span class="type">double</span> low, <span class="type">double</span> high)</span> &#123; <span class="keyword">return</span> (((high - low) * (<span class="type">double</span>)rand() / RAND_MAX) + low); &#125;</span><br><span class="line"><span class="type">double</span> <span class="title function_">sigmoid</span><span class="params">(<span class="type">double</span> x)</span> &#123; <span class="keyword">return</span> <span class="number">1</span> / (<span class="number">1</span> + <span class="built_in">exp</span>(-x)); &#125;</span><br><span class="line"><span class="type">double</span> <span class="title function_">d_sigmoid</span><span class="params">(<span class="type">double</span> x)</span> &#123; <span class="keyword">return</span> sigmoid(x) * (<span class="number">1</span> - sigmoid(x)); &#125;</span><br></pre></td></tr></table></figure><h2 id="初始化网络"><a href="#初始化网络" class="headerlink" title="初始化网络"></a>初始化网络</h2><p>实现 BP 网络需要存储网络的每一层的权重以及偏置，而实现 BP 算法则需要存储神经元的输入、激活输出以及误差，因此，定义数据结构如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">mlp</span> &#123;</span></span><br><span class="line">    <span class="comment">// biases[l][i] is the bias of the i-th neuron in layer l + 1</span></span><br><span class="line">    <span class="type">double</span>** biases;</span><br><span class="line">    <span class="comment">// weights[l][i][j] is the weight of the edge</span></span><br><span class="line">    <span class="comment">// connecting the j-th neuron in layer l and i-th neuron in layer l + 1</span></span><br><span class="line">    <span class="type">double</span>*** weights;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">bp</span> &#123;</span></span><br><span class="line">    <span class="comment">// input from previous layer</span></span><br><span class="line">    <span class="type">double</span> **z;</span><br><span class="line">    <span class="comment">// output after activation</span></span><br><span class="line">    <span class="type">double</span> **a;</span><br><span class="line">    <span class="comment">// back-prop error</span></span><br><span class="line">    <span class="type">double</span> **delta;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>其中 <code>weights</code> 权重数组需要随机初始化，其他数组初始化为全 0 即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">double</span>** <span class="title function_">alloc_biases</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">double</span>** arr = <span class="built_in">malloc</span>((layers - <span class="number">1</span>) * <span class="keyword">sizeof</span>(<span class="type">double</span>*));</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = <span class="number">0</span>; l &lt; layers - <span class="number">1</span>; ++l) &#123;</span><br><span class="line">        arr[l] = <span class="built_in">malloc</span>(sizes[l + <span class="number">1</span>] * <span class="keyword">sizeof</span>(<span class="type">double</span>));</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l + <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">            arr[l][i] = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> arr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">double</span>*** <span class="title function_">alloc_weights</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">double</span>*** weights = <span class="built_in">malloc</span>(layers * <span class="keyword">sizeof</span>(<span class="type">double</span>**));</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = <span class="number">0</span>; l &lt; layers - <span class="number">1</span>; ++l) &#123;</span><br><span class="line">        weights[l] = <span class="built_in">malloc</span>(sizes[l + <span class="number">1</span>] * <span class="keyword">sizeof</span>(<span class="type">double</span>*));</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l + <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">            weights[l][i] = <span class="built_in">malloc</span>(sizes[l] * <span class="keyword">sizeof</span>(<span class="type">double</span>));</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; sizes[l]; ++j) &#123;</span><br><span class="line">                weights[l][i][j] = random_(<span class="number">-5</span>, <span class="number">5</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> weights;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">double</span>** <span class="title function_">alloc_cache</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">double</span>** arr = <span class="built_in">malloc</span>(layers * <span class="keyword">sizeof</span>(<span class="type">double</span>*));</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = <span class="number">0</span>; l &lt; layers; ++l) &#123;</span><br><span class="line">        arr[l] = <span class="built_in">malloc</span>(sizes[l] * <span class="keyword">sizeof</span>(<span class="type">double</span>));</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l]; ++i) &#123;</span><br><span class="line">            arr[l][i] = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> arr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="前向传播"><a href="#前向传播" class="headerlink" title="前向传播"></a>前向传播</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">forward</span><span class="params">(<span class="type">double</span>* input, <span class="type">double</span>* output, <span class="keyword">struct</span> mlp* net, <span class="keyword">struct</span> bp* cache)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[<span class="number">0</span>]; ++i) &#123;</span><br><span class="line">        cache-&gt;a[<span class="number">0</span>][i] = input[i];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = <span class="number">1</span>; l &lt; layers; ++l) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l]; ++i) &#123;</span><br><span class="line">            cache-&gt;z[l][i] = net-&gt;biases[l - <span class="number">1</span>][i];</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; sizes[l - <span class="number">1</span>]; ++j) &#123;</span><br><span class="line">                cache-&gt;z[l][i] +=</span><br><span class="line">                    net-&gt;weights[l - <span class="number">1</span>][i][j] * cache-&gt;a[l - <span class="number">1</span>][j];</span><br><span class="line">            &#125;</span><br><span class="line">            cache-&gt;a[l][i] = sigmoid(cache-&gt;z[l][i]);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[layers - <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">        output[i] = cache-&gt;z[layers - <span class="number">1</span>][i];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是，输出层<strong>不需要</strong>激活函数。</p><h2 id="损失函数"><a href="#损失函数" class="headerlink" title="损失函数"></a>损失函数</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">double</span> <span class="title function_">loss</span><span class="params">(<span class="type">double</span>* output, <span class="type">double</span>* ground_truth)</span> &#123;</span><br><span class="line">    <span class="type">double</span> sum = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[layers - <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">        sum += (output[i] - ground_truth[i]) * (output[i] - ground_truth[i]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> .<span class="number">5</span> * sum;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="误差回传"><a href="#误差回传" class="headerlink" title="误差回传"></a>误差回传</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">backward</span><span class="params">(<span class="type">double</span>* ground_truth, <span class="keyword">struct</span> mlp* net, <span class="keyword">struct</span> bp* cache)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[layers - <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">        cache-&gt;delta[layers - <span class="number">1</span>][i] =</span><br><span class="line">            -(ground_truth[i] - cache-&gt;z[layers - <span class="number">1</span>][i]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = layers - <span class="number">2</span>; l &gt;= <span class="number">0</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l]; ++i) &#123;</span><br><span class="line">            <span class="type">double</span> sum = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; sizes[l + <span class="number">1</span>]; ++j) &#123;</span><br><span class="line">                sum +=</span><br><span class="line">                    cache-&gt;delta[l + <span class="number">1</span>][j] * net-&gt;weights[l][j][i];</span><br><span class="line">            &#125;</span><br><span class="line">            cache-&gt;delta[l][i] = sum * d_sigmoid(cache-&gt;z[l][i]);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="参数更新"><a href="#参数更新" class="headerlink" title="参数更新"></a>参数更新</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">optimize</span><span class="params">(<span class="type">double</span> lr, <span class="keyword">struct</span> mlp* net, <span class="keyword">struct</span> bp* cache)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> l = <span class="number">0</span>; l &lt; layers - <span class="number">1</span>; ++l) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sizes[l + <span class="number">1</span>]; ++i) &#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">0</span>; j &lt; sizes[l]; ++j) &#123;</span><br><span class="line">                net-&gt;weights[l][i][j] -= lr * (cache-&gt;delta[l + <span class="number">1</span>][i] * cache-&gt;a[l][j]);</span><br><span class="line">            &#125;</span><br><span class="line">            net-&gt;biases[l][i] -= lr * cache-&gt;delta[l + <span class="number">1</span>][i]; </span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="训练代码"><a href="#训练代码" class="headerlink" title="训练代码"></a>训练代码</h2><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// input_layer, hidden_layers..., output_layer</span></span><br><span class="line"><span class="type">int</span> sizes[] = &#123;<span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">3</span>, <span class="number">1</span>&#125;;</span><br><span class="line"><span class="type">int</span> layers = <span class="keyword">sizeof</span>(sizes) / <span class="keyword">sizeof</span>(<span class="type">int</span>);</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">double</span> input[<span class="number">4</span>][<span class="number">2</span>] = &#123;&#123;<span class="number">0</span>, <span class="number">0</span>&#125;, &#123;<span class="number">0</span>, <span class="number">1</span>&#125;, &#123;<span class="number">1</span>, <span class="number">0</span>&#125;, &#123;<span class="number">1</span>, <span class="number">1</span>&#125;&#125;;</span><br><span class="line">    <span class="type">double</span> ground_truth[<span class="number">4</span>][<span class="number">1</span>] = &#123;&#123;<span class="number">0</span>&#125;, &#123;<span class="number">1</span>&#125;, &#123;<span class="number">1</span>&#125;, &#123;<span class="number">0</span>&#125;&#125;;</span><br><span class="line">    <span class="type">double</span> output[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">    cache.z = alloc_cache();</span><br><span class="line">    cache.a = alloc_cache();</span><br><span class="line">    cache.delta = alloc_cache();</span><br><span class="line"></span><br><span class="line">    net.biases = alloc_biases();</span><br><span class="line">    net.weights = alloc_weights();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> epoch = <span class="number">0</span>; epoch &lt; <span class="number">50000</span>; ++epoch) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> n = <span class="number">0</span>; n &lt; <span class="number">4</span>; ++n) &#123;</span><br><span class="line">            forward(input[n], output, &amp;net, &amp;cache);</span><br><span class="line">            <span class="keyword">if</span> (epoch % <span class="number">1000</span> == <span class="number">0</span>)</span><br><span class="line">                <span class="built_in">printf</span>(<span class="string">&quot;epoch %d, sample %d, loss %.6f\n&quot;</span>, epoch, n, loss(output, ground_truth[n]));</span><br><span class="line">            backward(ground_truth[n], &amp;net, &amp;cache);</span><br><span class="line">            optimize(<span class="number">0.3</span>, &amp;net, &amp;cache);</span><br><span class="line">            zero_grad(&amp;cache);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完整代码可参考：<a href="https://github.com/howardlau1999/learning-machine-learning/blob/master/mlp_work.c">https://github.com/howardlau1999/learning-machine-learning/blob/master/mlp_work.c</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 配置日志系统</title>
      <link>https://blog.howardlau.me/programming/kubernetes/kubernetes-logging-system.html</link>
      <description>
        <![CDATA[<p>尽管 Kubernetes Dashboard 允许我们直接进入 Pod 容器中查看日志，但是在实际的系统中，Pod 容器可能经历创建、销毁等过程，一个服务也可能由多个 Pod]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Wed, 04 Sep 2019 05:58:41 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>尽管 Kubernetes Dashboard 允许我们直接进入 Pod 容器中查看日志，但是在实际的系统中，Pod 容器可能经历创建、销毁等过程，一个服务也可能由多个 Pod 负载均衡提供。因此，我们最好配置一套完整的日志收集、分析、搜索、查看系统，方便我们对系统进行监控。</p><p>而常用的集群日志系统就是 <strong>EFK</strong>，即 Elastic Search、Filebeat、Kibana。</p><h2 id="EFK-简介"><a href="#EFK-简介" class="headerlink" title="EFK 简介"></a>EFK 简介</h2><p>Elastic Search 是一款分布式、RESTful 风格的搜索和数据分析引擎，能够解决不断涌现出的各种用例；Filebeat 则是一种 Beats（其他的还有 Metric Beats 等），用来收集数据并上报给 Elastic Search；而 Kibana 则给用户提供了友好的界面进行日志分析。</p><h2 id="添加官方-Helm-Charts-源"><a href="#添加官方-Helm-Charts-源" class="headerlink" title="添加官方 Helm Charts 源"></a>添加官方 Helm Charts 源</h2><p>Elastic 公司为 Kubernetes 集群提供了 Helm Charts，我们可以很轻松地一键部署需要的应用，但是需要执行下面的命令来添加 Elastic 的 Helm Charts 源：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm repo add elastic https://helm.elastic.co</span><br></pre></td></tr></table></figure><p>在 GitHub 上也可以看到这些 Charts 的详细配置方法：<a href="https://github.com/elastic/helm-charts/">https://github.com/elastic/helm-charts/</a></p><h2 id="部署-Elastic-Search"><a href="#部署-Elastic-Search" class="headerlink" title="部署 Elastic Search"></a>部署 Elastic Search</h2><p>由于 Elastic Search 是数据库应用，因此需要 StatefulSet 来部署。在部署之前，需要为其创建 PersistentVolume 来提供存储资源。</p><p>我们有一台 8x8T RAID5 SAS 存储服务器，运行 NFS 服务，因此我们声明了 NFS 的 PV。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">PersistentVolume</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">elasticsearch-node-1-pv</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-logging</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">capacity:</span></span><br><span class="line">    <span class="attr">storage:</span> <span class="string">250Gi</span> </span><br><span class="line">  <span class="attr">accessModes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ReadWriteOnce</span></span><br><span class="line">  <span class="attr">persistentVolumeReclaimPolicy:</span> <span class="string">Retain</span> </span><br><span class="line">  <span class="attr">nfs:</span> </span><br><span class="line">    <span class="attr">path:</span> <span class="string">/mnt/storage/elasticsearch/node-1</span></span><br><span class="line">    <span class="attr">server:</span> <span class="string">nfs-server</span></span><br><span class="line">    <span class="attr">readOnly:</span> <span class="literal">false</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">PersistentVolume</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">elasticsearch-node-2-pv</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-logging</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">capacity:</span></span><br><span class="line">    <span class="attr">storage:</span> <span class="string">250Gi</span> </span><br><span class="line">  <span class="attr">accessModes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ReadWriteOnce</span></span><br><span class="line">  <span class="attr">persistentVolumeReclaimPolicy:</span> <span class="string">Retain</span> </span><br><span class="line">  <span class="attr">nfs:</span> </span><br><span class="line">    <span class="attr">path:</span> <span class="string">/mnt/storage/elasticsearch/node-2</span></span><br><span class="line">    <span class="attr">server:</span> <span class="string">nfs-server</span></span><br><span class="line">    <span class="attr">readOnly:</span> <span class="literal">false</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">PersistentVolume</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">elasticsearch-node-3-pv</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-logging</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">capacity:</span></span><br><span class="line">    <span class="attr">storage:</span> <span class="string">250Gi</span></span><br><span class="line">  <span class="attr">accessModes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">ReadWriteOnce</span></span><br><span class="line">  <span class="attr">persistentVolumeReclaimPolicy:</span> <span class="string">Retain</span></span><br><span class="line">  <span class="attr">nfs:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">/mnt/storage/elasticsearch/node-3</span></span><br><span class="line">    <span class="attr">server:</span> <span class="string">nfs-server</span></span><br><span class="line">    <span class="attr">readOnly:</span> <span class="literal">false</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>准备好充足的存储的空间之后，就可以安装官方的 Helm Chart 了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install --namespace kube-logging --name elasticsearch elastic/elasticsearch</span><br></pre></td></tr></table></figure><p>需要注意的是，默认的配置对于实际应用来说太小了，需要手动调整一些参数。例如 <code>esJavaOpts</code> 可以适当调大一些，我使用的是 <code>-Xmx16g -Xms16g</code>，同时 <code>resources</code> 也要调大一些，否则会被 Kubernetes 杀掉或者运行过慢。</p><h2 id="安装-Filebeat"><a href="#安装-Filebeat" class="headerlink" title="安装 Filebeat"></a>安装 Filebeat</h2><p>如果没有改变 Elastic Search 的名字，那直接安装官方的 Chart 即可。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install --namespace kube-logging --name filebeat elastic/filebeat</span><br></pre></td></tr></table></figure><h2 id="安装-Kibana"><a href="#安装-Kibana" class="headerlink" title="安装 Kibana"></a>安装 Kibana</h2><p>这个也是直接安装官方 Chart 即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">helm install --namespace kube-logging --name kibana elastic/kibana</span><br></pre></td></tr></table></figure><p>如果需要浏览器访问，则需要自己安装 Ingress：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">extensions/v1beta1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">kibana</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-logging</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">host:</span> <span class="string">kibana.kubernetes</span></span><br><span class="line">      <span class="attr">http:</span></span><br><span class="line">        <span class="attr">paths:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">path:</span> <span class="string">/</span></span><br><span class="line">            <span class="attr">backend:</span></span><br><span class="line">              <span class="attr">serviceName:</span> <span class="string">kibana-kibana</span></span><br><span class="line">              <span class="attr">servicePort:</span> <span class="number">5601</span></span><br></pre></td></tr></table></figure><p>打开设定的域名，看到 Kibana 能读取 Elastic Search 收集的 Filebeat 日志就代表安装成功了。</p><p><a href="/programming/kubernetes/kubernetes-logging-system/kibana.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/kubernetes-logging-system/kibana.png"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 图解 Kubernetes 资源</title>
      <link>https://blog.howardlau.me/programming/kubernetes/illustrating-kubernetes.html</link>
      <description>
        <![CDATA[<p><a href="/programming/kubernetes/illustrating-kubernetes/kubernetes-docker.png" data-fancybox="gallery" data-caption=""><img]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Mon, 02 Sep 2019 21:12:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="/programming/kubernetes/illustrating-kubernetes/kubernetes-docker.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/kubernetes-docker.png"></a></p><p>Kubernetes 是容器<strong>编排</strong>工具，负责在多台机器组成的集群上调度容器。用户只需要使用<strong>Kubernetes 资源</strong>来<strong>声明</strong>希望集群达到的状态，Kubernetes 就会调度集群资源，自动满足所声明的状态。</p><p>使用 Kubernetes，用户不用再操心容器具体的部署过程，而是使用<strong>声明式</strong>的方法来进行应用部署，大大简化了运维工作，使得程序员可以更专注于业务的的开发。</p><p>可以看出，使用 Kubernetes，我们更关心的是集群中的各种<strong>资源</strong>的管理，下面就是 Kubernetes 中常见的资源。</p><p>更值得注意的是，Kubernetes 是通过**控制器（Controller）**来调谐资源的，一个控制器只需要关注自己的资源，并通过创建下一个层级的资源，来让底层控制器进行协调即可。也正因如此，Kubernetes 中的资源是有层级关系的。</p><h2 id="Node"><a href="#Node" class="headerlink" title="Node"></a>Node</h2><p>任容器技术被怎么吹上天，还是要有一个 Host OS 来运行它们。而在 Kubernetes 中，一个独立的拥有 IP 的操作系统，并且运行着 Kubernetes 服务，就可以认为是一个 <strong>Node</strong>。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/nodes-2.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/nodes-2.png"></a></p><p>当然，既然是管理，那么就需要有节点站出来作为集群的管理者，这种节点被称为 <strong>Master Node</strong>，负责给手下的节点布置任务，来满足用户的需求。</p><h2 id="Pod"><a href="#Pod" class="headerlink" title="Pod"></a>Pod</h2><p>在 Kubernetes 中，调度的最小单元并不是容器，而是一个 <strong>Pod</strong>。Pod 可以认为是一台小小的虚拟机，而里面作为进程运行着的就是真正的 Container。</p><p>因此，一个 Pod 资源只需要声明其中使用到的 Container 即可。而由于 Pod 是调度的最小单位，所以我们可以在 Pod Spec 中指定调度的规则，例如，Node 的亲和性，又或者是 Pod 之间的亲和性。比如你可能不想让运行数据库的 Node 再被调度其他的 Pod 以免影响性能，那么可以在 Pod 中指定规则。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/pods-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/pods-1.png"></a></p><p>每个 Pod 都会拥有一个自己的虚拟 Pod IP，而 Pod 内的容器则共享这个 IP，因此 Pod 里的容器是不可以有端口冲突的，而 Pod 内的容器可以很方便地使用对方的端口和 <code>localhost</code> 地址进行通信。Pod 之间也可以通过 Pod IP 进行通信。</p><h2 id="ReplicaSet"><a href="#ReplicaSet" class="headerlink" title="ReplicaSet"></a>ReplicaSet</h2><p>定义了 Pod 之后，Kubernetes 就已经可以开始调度容器了。但是，如果想要一次运行多个 Pod，手动创建多个 Pod 并不是一个好的选择，万一 Pod Spec 发生变化，就要一个个去改。因此，用户可以通过 ReplicaSet 来告诉 Kubernetes 自己的 Pod 需要执行多少个。顾名思义，ReplicaSet 是控制 Pod 运行数量的资源。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/replicaset-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/replicaset-1.png"></a></p><h2 id="Deployment"><a href="#Deployment" class="headerlink" title="Deployment"></a>Deployment</h2><p>由于单独定义 Pod 和 ReplicaSet 有时候过于麻烦，因此 Kubernetes 将这两种资源控制器合并在一起变成 Deployment。我们只需要在其中描述我们的 Pod，并且指定数量，Kubernetes 就会自动帮我们创建 Pod 和 ReplicaSet，帮助我们调度 Pod，使集群达到理想的状态。同时，如果我们想进行滚动升级，那么 Deployment 实际上会创建一个新的 ReplicaSet，并慢慢调大新 ReplicaSet 的数量，并减少旧的 ReplicaSet 的数量，从而达到逐步更新的效果。当然，要是发现升级过程出现什么问题，我们也可以快速回滚，也就是将新的 ReplicaSet 副本数量调低，旧的调回来。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/pods-nodes-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/pods-nodes-1.png"></a></p><h2 id="Service"><a href="#Service" class="headerlink" title="Service"></a>Service</h2><p>虽然我们可以使用 Pod IP 来进行应用之间的通信，但是在实际场景中，一个 Pod 很可能会因为 Node 上的资源不足或者因为出错而被杀掉并重新调度，导致自己的 Pod IP 改变。所以如果直接在应用中直接使用 Pod IP 来和其他服务通信的话，会带来许多麻烦。因此，Kubernetes 诞生了一个叫 Service 的资源。</p><p>Service 通过标签的方式，选择一组 Pod，并对外提供一个不变的名字，当集群内的应用需要访问 Pod 时，不再直接使用 Pod IP 访问，而是使用 Service 的名字进行访问。</p><p>当然，网络通信最终还是要 IP 地址的，因此在集群内部还有一个 DNS 负责解析 Serivce Name 到 IP 地址上。神奇的是，Service 解析到的 IP 并不是 Pod IP，而是这个 Service 对应的 Cluster IP。Kubernetes 会在 Node 上设置正确的 iptables&#x2F;ipvs 规则，使得 Node 上的应用访问这个 Cluster IP 的流量会被随机的转发到一个 Pod 上。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/service-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/service-1.png"></a></p><p>这样，通过 Service 我们就再也不怕 Pod 的增减和变动了，能通过不变的名字访问服务。</p><h2 id="DaemonSet"><a href="#DaemonSet" class="headerlink" title="DaemonSet"></a>DaemonSet</h2><p>由于 Deployment 并不能保证同一个 Pod 不会调度到同一个 Node 上，而有一些应用我们希望每个机器上都恰好运行一个，比如日志的收集、节点的监控。这时候可以借助 DaemonSet 来运行这类守护容器。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/daemonset.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/daemonset.png"></a></p><p>DaemonSet 和 ReplicaSet 类似，都是用来控制 Pod 的调度的，区别是 DaemonSet 不需要指定数量，Kubernetes 会安排每个 Node 恰好运行一个。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/daemonset-nodes-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/daemonset-nodes-1.png"></a></p><h2 id="StatefulSet"><a href="#StatefulSet" class="headerlink" title="StatefulSet"></a>StatefulSet</h2><p>有些像数据库一类的应用可能需要存储数据，甚至可能有主从的角色，主机必须要在从机之前启动，否则不能启动从机。为了部署这类有状态的应用，Kubernetes 提供了 StatefulSet 这一资源。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/statefulset-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/statefulset-1.png"></a></p><p>StatefulSet 相比 Deployment 有着更多的特性：</p><ol><li><p>如果指定运行 n 个副本，那 Pod 会从 0 到 n - 1 依次创建，前面的创建不成功，后面就不会启动。</p></li><li><p>销毁的时候按照和创建相反的顺序删除 Pod，后面的 Pod 删除不成功，前面的就不会删除。</p></li><li><p>每个 Pod 都会有固定的标识来标识自己是第几个创建的 Pod，例如 <code>elasticsearch-0</code>。</p></li></ol><p><a href="/programming/kubernetes/illustrating-kubernetes/statefulset-nodes.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/statefulset-nodes.png"></a></p><h2 id="PersistentVolume"><a href="#PersistentVolume" class="headerlink" title="PersistentVolume"></a>PersistentVolume</h2><p>前面提到 StatefulSet 需要存储数据，然而这些数据应该存储在哪里呢？肯定不能存储在 Pod 被调度到的机器上，这样万一 Pod 被调度到其他地方数据就不见了。为了解决这个问题，Kubernetes 提供了 PersistentVolume 来管理和 Pod 运行机器单独区分的存储资源，通常是存储服务器。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/pv.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/pv.png"></a></p><p>每个 PersistentVolume 都可以声明自己的容量、存储的引擎、被挂载的方式，声明了之后 Kubernetes 集群上的有状态应用就可以读写这些单独的存储资源，不需要怕被调度了。</p><p>但是 PersistentVolume 资源 Kubernetes 本身不能动态分配，需要系统管理员手动分配。</p><h2 id="PersistentVolumeClaim"><a href="#PersistentVolumeClaim" class="headerlink" title="PersistentVolumeClaim"></a>PersistentVolumeClaim</h2><p>当然，由于是共享文件系统，肯定是不能随意读写的。一个应用不能直接挂载 PersistentVolume，而是要用 PersistentVolumeClaim 去声明自己需要使用 PersistentVolume，然后由 Kubernetes 集群分配合适的 PersistentVolume 供其读写。</p><p><a href="/programming/kubernetes/illustrating-kubernetes/pvc.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/illustrating-kubernetes/pvc.png"></a></p><p>如果没有可以满足条件的 PersistentVolume，那 Pod 就不会被创建，直到有可用的 PersistentVolume 为止。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 安装 Ingress Controller</title>
      <link>https://blog.howardlau.me/programming/kubernetes/kubernetes-ingress-controller.html</link>
      <description>
        <![CDATA[<p>可以看到之前安装的 Kubernetes Dashboard Web UI 没有办法直接在集群外部访问，这是由于 Kubernetes 默认 Service 提供的访问方式是 ClusterIP，也就是在集群内部通过虚拟 IP 访问。这个 IP]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Fri, 30 Aug 2019 21:33:35 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>可以看到之前安装的 Kubernetes Dashboard Web UI 没有办法直接在集群外部访问，这是由于 Kubernetes 默认 Service 提供的访问方式是 ClusterIP，也就是在集群内部通过虚拟 IP 访问。这个 IP 在集群外是无法访问的。当然，我们可以使用 NodePort 方式来提供 Service 服务，但是这样的话我们就需要在机器上自己设置反向代理，比较麻烦。</p><h2 id="什么是-Ingress"><a href="#什么是-Ingress" class="headerlink" title="什么是 Ingress"></a>什么是 Ingress</h2><p>Kubernetes 使用 Ingress 的方式解决了这个问题。Ingress 也是集群上的一种资源，用来路由外部的流量到集群内部的 Service。</p><p>一个 Ingress 资源类似这样：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">extensions/v1beta1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">kubernetes-dashboard-web-ui</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kubernetes-dashboard</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">host:</span> <span class="string">dashboard.kubernetes</span></span><br><span class="line">      <span class="attr">http:</span></span><br><span class="line">        <span class="attr">paths:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">path:</span> <span class="string">/</span></span><br><span class="line">            <span class="attr">backend:</span></span><br><span class="line">              <span class="attr">serviceName:</span> <span class="string">kubernetes-dashboard</span></span><br><span class="line">              <span class="attr">servicePort:</span> <span class="number">443</span></span><br></pre></td></tr></table></figure><p>和其他反向代理配置很类似，在 <code>rules</code> 中指定不同的代理规则，一个规则则由 <code>host</code> 和对应的集群中的 Service 组成。不同的 <code>host</code> 可以对应不同的服务，一个 <code>host</code> 下也可以通过 <code>path</code> 来映射到不同服务，配置相当灵活。</p><p>然而，Ingress 仅仅是描述了对应关系，需要一个实际的 Ingress Controller 来响应 HTTP&#x2F;S 请求并将请求路由到正确的后端服务上，而 Ingress Controller 其实就是像 <code>traefik</code>、<code>nginx</code> 一类的反向代理。Kubernetes 负责收集集群中的 Ingress 资源，并自动将配置注入到 Ingress Controller 使其加载，从而达到服务发现等功能。</p><h2 id="安装-Ingress-Controller"><a href="#安装-Ingress-Controller" class="headerlink" title="安装 Ingress Controller"></a>安装 Ingress Controller</h2><p>使用下面的 <code>yaml</code> 文件即可在集群中部署 <code>traefik</code> 作为 Ingress Controller，这里采用了 NodePort 的方式来提供服务，可以根据自己的情况选择不同的方式。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ServiceAccount</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ClusterRole</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">rbac.authorization.k8s.io/v1beta1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line"><span class="attr">rules:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">apiGroups:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="attr">resources:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">services</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">endpoints</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">secrets</span></span><br><span class="line">    <span class="attr">verbs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">get</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">list</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">watch</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">apiGroups:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">extensions</span></span><br><span class="line">    <span class="attr">resources:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ingresses</span></span><br><span class="line">    <span class="attr">verbs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">get</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">list</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">watch</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ClusterRoleBinding</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">rbac.authorization.k8s.io/v1beta1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line"><span class="attr">roleRef:</span></span><br><span class="line">  <span class="attr">apiGroup:</span> <span class="string">rbac.authorization.k8s.io</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">ClusterRole</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line"><span class="attr">subjects:</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">kind:</span> <span class="string">ServiceAccount</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">extensions/v1beta1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">k8s-app:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">k8s-app:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">k8s-app:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">serviceAccountName:</span> <span class="string">traefik-ingress-controller</span></span><br><span class="line">      <span class="attr">terminationGracePeriodSeconds:</span> <span class="number">60</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">image:</span> <span class="string">traefik</span></span><br><span class="line">        <span class="attr">name:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">http</span></span><br><span class="line">          <span class="attr">containerPort:</span> <span class="number">80</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">admin</span></span><br><span class="line">          <span class="attr">containerPort:</span> <span class="number">8080</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">https</span></span><br><span class="line">          <span class="attr">containerPort:</span> <span class="number">443</span></span><br><span class="line">        <span class="attr">args:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--api</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--kubernetes</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--logLevel=INFO</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--defaultentrypoints=http,https</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--entrypoints=Name:http</span> <span class="string">Address::80</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--entrypoints=Name:https</span> <span class="string">Address::443</span> <span class="string">TLS</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">--insecureSkipVerify=true</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">traefik-ingress-service</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">k8s-app:</span> <span class="string">traefik-ingress-lb</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">80</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">web</span></span><br><span class="line">      <span class="attr">nodePort:</span> <span class="number">30080</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">8080</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">nodePort:</span> <span class="number">30880</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">443</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">https</span></span><br><span class="line">      <span class="attr">nodePort:</span> <span class="number">30443</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">NodePort</span></span><br></pre></td></tr></table></figure><p>安装好 Ingress Controller 之后，把上面的 Kubernetes Dashboard Ingress 部署到集群中，就可以通过你指定的域名和端口访问到 Kubernetes Dashboard 了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Kubernetes 实战 | 安装 Dashboard</title>
      <link>https://blog.howardlau.me/programming/kubernetes/installing-kubernetes-dashboard.html</link>
      <description>
        <![CDATA[<p>通过命令行方式管理 Kubernetes 集群不太直观，因此 Kubernetes 官方推出了 Kubernetes Dashboard 这一个 Web UI 工具方便我们执行管理集群相关的操作。</p>
<h2 id="安装-Web-UI"><a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/kubernetes/">Kubernetes</category>
      <pubDate>Fri, 30 Aug 2019 20:47:49 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>通过命令行方式管理 Kubernetes 集群不太直观，因此 Kubernetes 官方推出了 Kubernetes Dashboard 这一个 Web UI 工具方便我们执行管理集群相关的操作。</p><h2 id="安装-Web-UI"><a href="#安装-Web-UI" class="headerlink" title="安装 Web UI"></a>安装 Web UI</h2><p>执行官方提供的一键脚本就可以安装 Kubernetes Dashboard 了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta1/aio/deploy/recommended.yaml</span><br></pre></td></tr></table></figure><p>此时由于 Dashboard 是部署在集群内部的，外部无法直接访问，需要执行 <code>kubectl proxy</code> 来代理外部的流量到集群中，然后在机器上访问 <a href="http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/">http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/</a> 就可以看到 Kubernetes Dashboard 了。</p><p>登录 Kubernetes Dashboard 的方法继续看下面的步骤。</p><h2 id="创建-Service-Account"><a href="#创建-Service-Account" class="headerlink" title="创建 Service Account"></a>创建 Service Account</h2><p>为了能使 Kubernetes Dashboard 具有权限管理集群，需要首先给其创建一个具有管理员权限的 Service Account。</p><p>将下面的文件保存成一个 <code>.yaml</code> 文件后使用 <code>kubectl apply -f xxx.yaml</code> 来执行：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ServiceAccount</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">admin-user</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br><span class="line"></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="meta"></span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">rbac.authorization.k8s.io/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ClusterRoleBinding</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">admin-user</span></span><br><span class="line"><span class="attr">roleRef:</span></span><br><span class="line">  <span class="attr">apiGroup:</span> <span class="string">rbac.authorization.k8s.io</span></span><br><span class="line">  <span class="attr">kind:</span> <span class="string">ClusterRole</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">cluster-admin</span></span><br><span class="line"><span class="attr">subjects:</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">kind:</span> <span class="string">ServiceAccount</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">admin-user</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">kube-system</span></span><br></pre></td></tr></table></figure><h2 id="获取登录-Token"><a href="#获取登录-Token" class="headerlink" title="获取登录 Token"></a>获取登录 Token</h2><p>创建了管理员账号之后可以通过命令获取登录用的 Token，将其复制粘贴到 Dashboard 中就可以登录了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk <span class="string">&#x27;&#123;print$1&#125;&#x27;</span>)</span><br></pre></td></tr></table></figure><p><a href="/programming/kubernetes/installing-kubernetes-dashboard/token.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/installing-kubernetes-dashboard/token.png"></a></p><p>登录后在侧边导航栏选择 <code>kube-system</code> 的 Namespace 就可以看到集群核心服务的运行情况了：</p><p><a href="/programming/kubernetes/installing-kubernetes-dashboard/screenshot-2019.08.31-12-46-20.png" data-fancybox="gallery" data-caption=""><img src="/programming/kubernetes/installing-kubernetes-dashboard/screenshot-2019.08.31-12-46-20.png"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 8 文件系统</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-8.html</link>
      <description>
        <![CDATA[<p><em>这次实验课上布置了另外的任务，即实现 MTFQ 以及工作集算法。</em></p>
<p><strong>在实验过程中，发现 ucore 并没有对用户进程 swap 提供支持，因此加入了一些修正。</strong></p>
<h3]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Sat, 29 Jun 2019 22:53:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><em>这次实验课上布置了另外的任务，即实现 MTFQ 以及工作集算法。</em></p><p><strong>在实验过程中，发现 ucore 并没有对用户进程 swap 提供支持，因此加入了一些修正。</strong></p><h3 id="练习-1-完成-ucore-文件系统读文件操作的实现"><a href="#练习-1-完成-ucore-文件系统读文件操作的实现" class="headerlink" title="练习 1 完成 ucore 文件系统读文件操作的实现"></a>练习 1 完成 ucore 文件系统读文件操作的实现</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 处理一开始的块</span></span><br><span class="line"><span class="keyword">if</span> ((blkoff = offset % SFS_BLKSIZE) != <span class="number">0</span> ) &#123;</span><br><span class="line">    <span class="keyword">if</span> (nblks != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// 不在一块里</span></span><br><span class="line">        size = SFS_BLKSIZE - blkoff;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 在一块里</span></span><br><span class="line">        size = endpos - offset;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_bmap_load_nolock(sfs, <span class="built_in">sin</span>, blkno, &amp;ino)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    alen += size;</span><br><span class="line">    <span class="keyword">if</span> (nblks == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    buf += size, blkno ++, nblks --;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">size = SFS_BLKSIZE;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 循环处理中间的块</span></span><br><span class="line"><span class="keyword">while</span> (nblks != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_bmap_load_nolock(sfs, <span class="built_in">sin</span>, blkno, &amp;ino)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_block_op(sfs, buf, ino, <span class="number">1</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line">    alen += size, buf += size, blkno ++, nblks --;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理最后的块</span></span><br><span class="line"><span class="keyword">if</span> ((size = endpos % SFS_BLKSIZE) != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_bmap_load_nolock(sfs, <span class="built_in">sin</span>, blkno, &amp;ino)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> ((ret = sfs_buf_op(sfs, buf, size, ino, <span class="number">0</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> out;</span><br><span class="line">    &#125;</span><br><span class="line">    alen += size;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在读写文件系统的时候，只能按块读写，但是有可能需要读取的文件内容开始和结束偏移没有对齐到块，所以需要单独处理。而 <code>sfs_buf_op</code> 其实内部实现也是按块读取，只是在写入 <code>buf</code> 的时候丢弃一部分数据而已。</p><p>在处理开始块的时候，没有对齐的时候有两种情况：</p><ul><li>开始和结束地址都在一块里：这时候需要读取的 <code>size = endpos - offset</code></li><li>开始和结束地址不在一块里：这时候需要读取的 <code>size = SFS_BLKSIZE - blkoff</code></li></ul><p>然后就是按块读写中间块，最后读写结束的位置可能也没有对齐到块上，这时候只有一种情况，那就是 <code>size = endpos % SFS_BLKSIZE</code>。</p><p>在读写块的时候，首先需要根据 <code>inode</code> 块号获取对应的物理块号，然后调用 <code>sfs_block_op</code> 或者 <code>sfs_buf_op</code> 去操作物理磁盘。操作成功后更新 <code>alen</code>，<code>buf</code>，<code>blkno</code> 和 <code>nblks</code>。 </p><h3 id="练习-2-完成基于文件系统的执行程序机制的实现"><a href="#练习-2-完成基于文件系统的执行程序机制的实现" class="headerlink" title="练习 2 完成基于文件系统的执行程序机制的实现"></a>练习 2 完成基于文件系统的执行程序机制的实现</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* (1) create a new mm for current process</span></span><br><span class="line"><span class="comment">     * (2) create a new PDT, and mm-&gt;pgdir= kernel virtual addr of PDT</span></span><br><span class="line"><span class="comment">     * (3) copy TEXT/DATA/BSS parts in binary to memory space of process</span></span><br><span class="line"><span class="comment">     *    (3.1) read raw data content in file and resolve elfhdr</span></span><br><span class="line"><span class="comment">     *    (3.2) read raw data content in file and resolve proghdr based on info in elfhdr</span></span><br><span class="line"><span class="comment">     *    (3.3) call mm_map to build vma related to TEXT/DATA</span></span><br><span class="line"><span class="comment">     *    (3.4) callpgdir_alloc_page to allocate page for TEXT/DATA, read contents in file</span></span><br><span class="line"><span class="comment">     *          and copy them into the new allocated pages</span></span><br><span class="line"><span class="comment">     *    (3.5) callpgdir_alloc_page to allocate pages for BSS, memset zero in these pages</span></span><br><span class="line"><span class="comment">     * (4) call mm_map to setup user stack, and put parameters into user stack</span></span><br><span class="line"><span class="comment">     * (5) setup current process&#x27;s mm, cr3, reset pgidr (using lcr3 MARCO)</span></span><br><span class="line"><span class="comment">     * (6) setup uargc and uargv in user stacks</span></span><br><span class="line"><span class="comment">     * (7) setup trapframe for user environment</span></span><br><span class="line"><span class="comment">     * (8) if up steps failed, you should cleanup the env.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="type">int</span> ret = -E_NO_MEM;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">mm_struct</span> *<span class="title">mm</span>;</span></span><br><span class="line">    <span class="keyword">if</span> ((mm = mm_create()) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_mm;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (setup_pgdir(mm) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_pgdir;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">elfhdr</span> __<span class="title">elf</span>, *<span class="title">elf</span> =</span> &amp;__elf;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((ret = load_icode_read(fd, elf,<span class="keyword">sizeof</span>(<span class="keyword">struct</span> elfhdr), <span class="number">0</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_elf;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (elf-&gt;e_magic != ELF_MAGIC) &#123;</span><br><span class="line">        ret = -E_INVAL_ELF;</span><br><span class="line">        <span class="keyword">goto</span> bad_elf;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proghdr</span> __<span class="title">ph</span>, *<span class="title">ph</span> =</span> &amp;__ph;</span><br><span class="line">    <span class="type">uint32_t</span> vm_flags, perm, phnum;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (phnum = <span class="number">0</span>; phnum &lt; elf-&gt;e_phnum; ++phnum) &#123;</span><br><span class="line">        <span class="type">off_t</span> phoff = elf-&gt;e_phoff + <span class="keyword">sizeof</span>(<span class="keyword">struct</span> proghdr) * phnum;</span><br><span class="line">        <span class="keyword">if</span> ((ret = load_icode_read(fd, ph, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> proghdr), phoff)) != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 这一段不需要加载</span></span><br><span class="line">        <span class="keyword">if</span> (ph-&gt;p_type != ELF_PT_LOAD || ph-&gt;p_filesz == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">continue</span> ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (ph-&gt;p_filesz &gt; ph-&gt;p_memsz) &#123;</span><br><span class="line">            ret = -E_INVAL_ELF;</span><br><span class="line">            <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        vm_flags = <span class="number">0</span>, perm = PTE_U;</span><br><span class="line">        <span class="keyword">if</span> (ph-&gt;p_flags &amp; ELF_PF_X) vm_flags |= VM_EXEC;</span><br><span class="line">        <span class="keyword">if</span> (ph-&gt;p_flags &amp; ELF_PF_W) vm_flags |= VM_WRITE;</span><br><span class="line">        <span class="keyword">if</span> (ph-&gt;p_flags &amp; ELF_PF_R) vm_flags |= VM_READ;</span><br><span class="line">        <span class="keyword">if</span> (vm_flags &amp; VM_WRITE) perm |= PTE_W;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> ((ret = mm_map(mm, ph-&gt;p_va, ph-&gt;p_memsz, vm_flags, <span class="literal">NULL</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">off_t</span> offset = ph-&gt;p_offset;</span><br><span class="line">        <span class="type">size_t</span> off, size;</span><br><span class="line">        <span class="type">uintptr_t</span> start = ph-&gt;p_va, end = ph-&gt;p_va + ph-&gt;p_filesz, la = ROUNDDOWN(start, PGSIZE);</span><br><span class="line"></span><br><span class="line">        ret = -E_NO_MEM;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (start &lt; end) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((page = pgdir_alloc_page(mm-&gt;pgdir, la, perm)) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                ret = -E_NO_MEM;</span><br><span class="line">                <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            off = start - la, size = PGSIZE - off, la += PGSIZE;</span><br><span class="line">            <span class="keyword">if</span> (end &lt; la) &#123;</span><br><span class="line">                size -= la - end;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> ((ret = load_icode_read(fd, page2kva(page) + off, size, offset)) != <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            start += size, offset += size;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        end = ph-&gt;p_va + ph-&gt;p_memsz;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (start &lt; la) &#123;</span><br><span class="line">            <span class="keyword">if</span> (start == end) &#123;</span><br><span class="line">                <span class="keyword">continue</span> ;</span><br><span class="line">            &#125;</span><br><span class="line">            off = start + PGSIZE - la, size = PGSIZE - off;</span><br><span class="line">            <span class="keyword">if</span> (end &lt; la) &#123;</span><br><span class="line">                size -= la - end;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">memset</span>(page2kva(page) + off, <span class="number">0</span>, size);</span><br><span class="line">            start += size;</span><br><span class="line">            assert((end &lt; la &amp;&amp; start == end) || (end &gt;= la &amp;&amp; start == la));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (start &lt; end) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((page = pgdir_alloc_page(mm-&gt;pgdir, la, perm)) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                ret = -E_NO_MEM;</span><br><span class="line">                <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            off = start - la, size = PGSIZE - off, la += PGSIZE;</span><br><span class="line">            <span class="keyword">if</span> (end &lt; la) &#123;</span><br><span class="line">                size -= la - end;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">memset</span>(page2kva(page) + off, <span class="number">0</span>, size);</span><br><span class="line"></span><br><span class="line">            start += size;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    sysfile_close(fd);</span><br><span class="line"></span><br><span class="line">    vm_flags = VM_READ | VM_WRITE | VM_STACK;</span><br><span class="line">    <span class="keyword">if</span> ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, <span class="literal">NULL</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    mm_count_inc(mm);</span><br><span class="line">    current-&gt;mm = mm;</span><br><span class="line">    current-&gt;cr3 = PADDR(mm-&gt;pgdir);</span><br><span class="line">    lcr3(PADDR(mm-&gt;pgdir));</span><br><span class="line"></span><br><span class="line">    <span class="comment">//setup argc, argv</span></span><br><span class="line">    <span class="type">uint32_t</span> argv_size=<span class="number">0</span>, i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; argc; i ++) &#123;</span><br><span class="line">        argv_size += strnlen(kargv[i],EXEC_MAX_ARG_LEN + <span class="number">1</span>)+<span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">uintptr_t</span> stacktop = USTACKTOP - (argv_size/<span class="keyword">sizeof</span>(<span class="type">long</span>)+<span class="number">1</span>)*<span class="keyword">sizeof</span>(<span class="type">long</span>);</span><br><span class="line">    <span class="type">char</span>** uargv=(<span class="type">char</span> **)(stacktop  - argc * <span class="keyword">sizeof</span>(<span class="type">char</span> *));</span><br><span class="line">    </span><br><span class="line">    argv_size = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; argc; i ++) &#123;</span><br><span class="line">        uargv[i] = <span class="built_in">strcpy</span>((<span class="type">char</span> *)(stacktop + argv_size ), kargv[i]);</span><br><span class="line">        argv_size +=  strnlen(kargv[i],EXEC_MAX_ARG_LEN + <span class="number">1</span>)+<span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    stacktop = (<span class="type">uintptr_t</span>)uargv - <span class="keyword">sizeof</span>(<span class="type">int</span>);</span><br><span class="line">    *(<span class="type">int</span> *)stacktop = argc;</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> *<span class="title">tf</span> =</span> current-&gt;tf;</span><br><span class="line">    <span class="built_in">memset</span>(tf, <span class="number">0</span>, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> trapframe));</span><br><span class="line">    tf-&gt;tf_cs = USER_CS;</span><br><span class="line">    tf-&gt;tf_ds = tf-&gt;tf_es = tf-&gt;tf_ss = USER_DS;</span><br><span class="line">    tf-&gt;tf_esp = stacktop;</span><br><span class="line">    tf-&gt;tf_eip = elf-&gt;e_entry;</span><br><span class="line">    tf-&gt;tf_eflags = FL_IF;</span><br><span class="line">    ret = <span class="number">0</span>;</span><br><span class="line">out:</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">bad_cleanup_mmap:</span><br><span class="line">    exit_mmap(mm);</span><br><span class="line">bad_elf:</span><br><span class="line">    put_pgdir(mm);</span><br><span class="line">bad_pgdir:</span><br><span class="line">    mm_destroy(mm);</span><br><span class="line">bad_mm:</span><br><span class="line">    <span class="keyword">goto</span> out;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>execve</code> 中，由于程序变为从文件系统中加载，需要完全重写，但是思路很简单。</p><ol><li>调用 <code>create_mm</code> 为程序分配虚拟内存管理结构体。</li><li>调用 <code>setup_pgdir</code> 为程序建立页表。</li><li>从 <code>fd</code> 中调用 <code>load_icode_read</code> 从文件系统中读取 <code>elf</code> 文件头。</li><li>根据 <code>elfhdr</code> 中的信息，遍历所有 <code>proghdr</code>，根据 <code>proghdr</code> 中的信息，建立连续的虚拟内存映射，并设置对应的权限（读&#x2F;写&#x2F;执行），并使用 <code>mm_map</code> 将虚拟内存插入到程序的虚拟内存管理结构体中。</li><li>针对每一程序段，需要分配物理页面，然后将该程序段中的文件内容（大小为 <code>ph_filesz</code>）拷贝到内存中。而 <code>ph_memsz</code> 标识了这一段实际占用内存空间的大小，应该有 <code>ph_filesz &lt;= ph_memsz</code> ，在拷贝完文件内容之后，需要将剩下的空间置为 0，这时候有可能发生没有对齐的情况，需要特殊处理。</li><li>处理完 <code>elf</code> 中的所有程序段之后，开始初始化用户栈，所有用户栈顶地址都是 <code>USTACKTOP</code> ，用户栈大小为 <code>USTACKSIZE</code>，这里只需要调用 <code>mm_map</code> 建立内存映射即可，在发生缺页的时候会自动分配物理页。也可以先手动用 <code>pgdir_alloc_page</code> 事先分配好物理页。</li><li>此时虚拟内存已经设置完毕，将 <code>mm</code> 赋值给当前进程，然后调用 <code>lcr3</code> 使新的页表生效。</li><li>这时候开始设置 <code>argc</code> 和 <code>argv</code> ，将运行参数拷贝到用户栈上。</li><li>最后设置 <code>trapframe</code> 使操作系统从中断返回后能进入用户态从 <code>elf</code> 入口执行用户程序。</li></ol><h3 id="练习-3-多级反馈队列调度算法"><a href="#练习-3-多级反馈队列调度算法" class="headerlink" title="练习 3 多级反馈队列调度算法"></a>练习 3 多级反馈队列调度算法</h3><p>多级反馈队列调度算法是在 Round Robin 算法基础上增加了多个运行队列，所以首先对 <code>struct run_queue</code> 做一下改动，分配多个级别的调度列表。<code>MULTI_QUEUE_NUM = 6</code> 是指有多少级调度队列。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> MULTI_QUEUE_NUM 6</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">run_queue</span> &#123;</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="type">list_entry_t</span> multi_run_list[MULTI_QUEUE_NUM];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后需要在进程控制块中新增一个字段表明该进程处于哪个队列：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> &#123;</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="type">uint32_t</span> queue_level;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>每个进程创建时，<code>queue_level</code> 初始化为 0。<code>queue_level</code> 越大，表明优先级越低。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;defs.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;list.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;proc.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;assert.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;default_sched.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">multi_init</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; MULTI_QUEUE_NUM; ++i) &#123;</span><br><span class="line">        list_init(&amp;(rq-&gt;multi_run_list[i]));</span><br><span class="line">    &#125;</span><br><span class="line">    rq-&gt;proc_num = <span class="number">0</span>;</span><br><span class="line">    rq-&gt;multi_queue_q = rq-&gt;max_time_slice;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">multi_enqueue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    assert(list_empty(&amp;(proc-&gt;run_link)));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span> &amp;&amp; proc-&gt;queue_level + <span class="number">1</span> &lt; MULTI_QUEUE_NUM) &#123;</span><br><span class="line">        ++proc-&gt;queue_level;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    proc-&gt;time_slice = rq-&gt;multi_queue_q &lt;&lt; proc-&gt;queue_level;</span><br><span class="line">    list_add_before(&amp;(rq-&gt;multi_run_list[proc-&gt;queue_level]), &amp;(proc-&gt;run_link));</span><br><span class="line">    proc-&gt;rq = rq;</span><br><span class="line">    rq-&gt;proc_num ++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">multi_dequeue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    assert(!list_empty(&amp;(proc-&gt;run_link)) &amp;&amp; proc-&gt;rq == rq);</span><br><span class="line">    list_del_init(&amp;(proc-&gt;run_link));</span><br><span class="line">    rq-&gt;proc_num --;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">struct</span> proc_struct *</span><br><span class="line"><span class="title function_">multi_pick_next</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; MULTI_QUEUE_NUM; ++i) &#123;</span><br><span class="line">        <span class="type">list_entry_t</span> *le = list_next(&amp;(rq-&gt;multi_run_list[i]));</span><br><span class="line">        <span class="keyword">if</span> (le != &amp;(rq-&gt;multi_run_list[i])) &#123;</span><br><span class="line">            <span class="keyword">return</span> le2proc(le, run_link);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">multi_proc_tick</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;time_slice --;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;need_resched = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sched_class</span> <span class="title">default_sched_class</span> =</span> &#123;</span><br><span class="line">    .name = <span class="string">&quot;multi_scheduler&quot;</span>,</span><br><span class="line">    .init = multi_init,</span><br><span class="line">    .enqueue = multi_enqueue,</span><br><span class="line">    .dequeue = multi_dequeue,</span><br><span class="line">    .pick_next = multi_pick_next,</span><br><span class="line">    .proc_tick = multi_proc_tick,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>上面是 MTFQ 的实现，在初始化的时候，需要对每一级队列初始化。</p><p>在进程入队的时候，需要先判断该进程之前的时间片是否用完了，如果用完了，说明这个进程需要更多的 CPU 时间，因此需要调度进入下一级队列，然后根据队列来设置进程时间片。如果进程已经在最后一级队列，就不再往下调。</p><p>在选择下一个需要执行的进程的时候，从优先级高的队列开始遍历，如果在高优先级的队列中没有需要调度的进程，才在低优先级的队列中寻找进程。</p><p>进程的出队和时间片减少的算法和 RR 算法一样。</p><h3 id="练习-4-修改虚拟存储中的页面置换算法"><a href="#练习-4-修改虚拟存储中的页面置换算法" class="headerlink" title="练习 4 修改虚拟存储中的页面置换算法"></a>练习 4 修改虚拟存储中的页面置换算法</h3><p>在 <code>load_icode</code> 函数里，预先分配三页物理帧给进程：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, <span class="literal">NULL</span>)) != <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">goto</span> bad_cleanup_mmap;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">assert(pgdir_alloc_page(mm-&gt;pgdir, USTACKTOP-PGSIZE , PTE_USER) != <span class="literal">NULL</span>);</span><br><span class="line">assert(pgdir_alloc_page(mm-&gt;pgdir, USTACKTOP<span class="number">-2</span>*PGSIZE , PTE_USER) != <span class="literal">NULL</span>);</span><br><span class="line">assert(pgdir_alloc_page(mm-&gt;pgdir, USTACKTOP<span class="number">-3</span>*PGSIZE , PTE_USER) != <span class="literal">NULL</span>);</span><br></pre></td></tr></table></figure><p>在进程控制块中，需要加入一个字段，用来记录进程的缺页次数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> &#123;</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="type">uint32_t</span> pgfault;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后在 <code>do_pgfault</code> 函数中更新这个值：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">do_pgfault</span><span class="params">(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uint32_t</span> error_code, <span class="type">uintptr_t</span> addr)</span> &#123;</span><br><span class="line">   <span class="comment">// ...</span></span><br><span class="line">   ++current-&gt;pgfault;</span><br><span class="line">   </span><br><span class="line">   ret = <span class="number">0</span>;</span><br><span class="line">failed:</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_init_mm(<span class="keyword">struct</span> mm_struct *mm) &#123;</span><br><span class="line">    mm-&gt;sm_priv = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_map_swappable(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uintptr_t</span> addr,</span><br><span class="line">                                <span class="keyword">struct</span> Page *page, <span class="type">int</span> tick) &#123;</span><br><span class="line">    <span class="type">list_entry_t</span> *head = (<span class="type">list_entry_t</span> *)mm-&gt;sm_priv;</span><br><span class="line">    <span class="type">list_entry_t</span> *entry = &amp;(page-&gt;pra_page_link);</span><br><span class="line">    assert(entry != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">if</span> (head == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        list_init(entry);</span><br><span class="line">        mm-&gt;sm_priv = entry;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        list_add_before(head, entry);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_swap_out_victim(<span class="keyword">struct</span> mm_struct *mm, <span class="keyword">struct</span> Page **ptr_page,</span><br><span class="line">                                  <span class="type">int</span> in_tick) &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span>* <span class="title">m_proc</span> =</span> <span class="literal">NULL</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> (<span class="type">list_entry_t</span> *le = list_next(&amp;proc_list); le != &amp;proc_list; le = list_next(le)) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span> =</span> le2proc(le, list_link);</span><br><span class="line">        assert(proc != <span class="literal">NULL</span>);</span><br><span class="line">        <span class="keyword">if</span> (!proc-&gt;mm || !proc-&gt;mm-&gt;sm_priv) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="keyword">if</span> (!m_proc || proc-&gt;pgfault &lt; m_proc-&gt;pgfault) &#123;</span><br><span class="line">            m_proc = proc;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (m_proc) mm = m_proc-&gt;mm, cprintf(<span class="string">&quot;[swap_out_victim] mm = %p, pid = %d\n&quot;</span>, mm, m_proc-&gt;pid);</span><br><span class="line">    </span><br><span class="line">    <span class="type">list_entry_t</span> *head = (<span class="type">list_entry_t</span> *)mm-&gt;sm_priv;</span><br><span class="line">    <span class="type">list_entry_t</span> *p = head, *victim = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        <span class="type">pte_t</span> *ptep =</span><br><span class="line">            get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">        <span class="comment">// not accessed and not dirty</span></span><br><span class="line">        <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; !(*ptep &amp; PTE_D)) &#123;</span><br><span class="line">            victim = p;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        p = list_next(p);</span><br><span class="line">    &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; (*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            *ptep &amp;= ~PTE_A;</span><br><span class="line">            tlb_invalidate(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr);</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and not dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; !(*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; (*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            *ptep &amp;= ~PTE_A;</span><br><span class="line">            tlb_invalidate(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr);</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (list_empty(victim)) &#123;</span><br><span class="line">        mm-&gt;sm_priv = <span class="literal">NULL</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        mm-&gt;sm_priv = list_next(victim);</span><br><span class="line">        list_del(victim);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    *ptr_page = le2page(victim, pra_page_link);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在选择需要被换出的页的时候，首先遍历进程列表，找到有页可换的进程中缺页次数最少的进程，然后开始执行 Extended Clock PRA。</p><p>在普通的 Clock PRA 算法里，只考虑了页面是否被访问，但是没有考虑页面是否脏页，显然脏页换出的代价比非脏页更大，因此在 Extended Clock PRA 里，还要优先选择不是脏页的页面。</p><p>在 Extended Clock PRA 中，插入和 FIFO PRA 一样，直接插入到链表尾部。在选择需要被换出的页面的时候，从上一次被换出页的下一页开始扫描，需要至多四趟扫描：</p><ol><li>寻找没被访问 (<code>PTE_A == 0</code>) 而且没被修改的非脏页 (<code>PTE_D == 0</code>)，其中 <code>PTE_A</code> 和 <code>PTE_D</code> 都是 CPU 硬件置位的。</li><li>寻找没被访问 (<code>PTE_A == 0</code>) 的脏页 (<code>PTE_D == 1</code>)，并且将途中扫描到的页面的 <code>PTE_A</code> 都修改为 0</li><li>重复 1</li><li>重复 2</li></ol><p>为了实现定时清空缺页次数统计的要求，需要在定时器中断中调用 Swap Manager 的函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">case</span> IRQ_OFFSET + IRQ_TIMER:</span><br><span class="line">    ++ticks;</span><br><span class="line">    assert(current != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">if</span> (ticks % <span class="number">1000</span> == <span class="number">0</span>) &#123; <span class="comment">// 每 1000 ticks 调用一次</span></span><br><span class="line">        swap_tick_event(<span class="literal">NULL</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    run_timer_list();</span><br><span class="line">    <span class="keyword">break</span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在 Swap Manager 中清空：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_tick_event(<span class="keyword">struct</span> mm_struct *mm) &#123; </span><br><span class="line">    <span class="keyword">for</span> (<span class="type">list_entry_t</span> *le = list_next(&amp;proc_list); le != &amp;proc_list; le = list_next(le)) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span> =</span> le2proc(le, list_link);</span><br><span class="line">        proc-&gt;pgfault = <span class="number">0</span>;</span><br><span class="line">    &#125;    </span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>; </span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>需要注意的是，每个进程都有自己的交换页面队列。</p><h3 id="实验思考"><a href="#实验思考" class="headerlink" title="实验思考"></a>实验思考</h3><p>在 ucore 中，其实并没有对用户进程 swap 做任何处理。由于 swap 的偏移量由虚拟地址简单计算得出，因此对于用户栈而言，其地址过大，导致 swap 中找不到位置存放。经过计算，如果不修改用户栈顶地址，则需要 <code>22 * 128 MB = 2.75 GB </code> 的 swap 空间（在 <code>Makefile</code> 中调整 <code>dd</code>  命令的 <code>count</code> 参数）；如果需要避免过大的 swap 占用，可以简单将用户栈顶地址调小。</p><p>但是这样会有另外一个问题：由于每个进程的虚拟地址都是一样的，因此不同进程的同一地址的页面会交换到同一个地方，造成错误。</p><p>使用地址来计算 swap 偏移有根本上的错误，无论是使用虚拟地址还是物理地址，因为同一个地址都有可能被多个进程同时使用。因此，需要另外的机制分配 swap 空间。</p><p>为了同时解决以上两个问题，简单起见可以使用 <code>bitmap</code> 来分配 swap 空间。在 <code>swapfs</code> 中分配一个 <code>bitmap</code> 用来记录 swap 分区中已被使用的区域，在需要换出页面的时候申请一块。在换入页面的时候释放一块。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> *swap_bitmap;</span><br><span class="line"><span class="type">static</span> <span class="type">int</span> free_swap;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">swapfs_init</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">static_assert</span>((PGSIZE % SECTSIZE) == <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (!ide_device_valid(SWAP_DEV_NO)) &#123;</span><br><span class="line">        panic(<span class="string">&quot;swap fs isn&#x27;t available.\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    max_swap_offset = ide_device_size(SWAP_DEV_NO) / (PGSIZE / SECTSIZE);</span><br><span class="line">    </span><br><span class="line">    free_swap = max_swap_offset;</span><br><span class="line">    swap_bitmap = kmalloc(max_swap_offset / <span class="number">8</span>);</span><br><span class="line">    <span class="built_in">memset</span>(swap_bitmap, <span class="number">0</span>, free_swap);</span><br><span class="line">    --free_swap;</span><br><span class="line">    cprintf(<span class="string">&quot;[swapfs_init] free_swap = %d\n&quot;</span>, free_swap);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">swap_entry_t</span> <span class="title function_">get_swap_entry</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">swap_entry_t</span> entry;</span><br><span class="line">    <span class="keyword">if</span> (!free_swap) &#123;</span><br><span class="line">        panic(<span class="string">&quot;no free swap space!!!\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt; max_swap_offset; ++i) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!test_bit(i, swap_bitmap)) &#123;</span><br><span class="line">            set_bit(i, swap_bitmap);</span><br><span class="line">            --free_swap;</span><br><span class="line">            entry = i &lt;&lt; <span class="number">8</span>;</span><br><span class="line">            <span class="keyword">return</span> entry;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">free_swap_entry</span><span class="params">(<span class="type">swap_entry_t</span> entry)</span> &#123;</span><br><span class="line">    clear_bit(swap_offset(entry), swap_bitmap);</span><br><span class="line">    ++free_swap;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里 <code>bitmap</code> 分配了 1024 个 <code>int</code> 的大小，也就是 <code>4 * 8 * 1024 = 32768</code> 个比特位，有 32768 页可用来 swap，这是因为 <code>swap.img</code> 有 128 MB，每页大小 4 KB，因此有 32768 页可以交换。</p><p>然后修改 <code>swap_in</code> 和 <code>swap_out</code> 函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">swap_out</span><span class="params">(<span class="keyword">struct</span> mm_struct *mm, <span class="type">int</span> n, <span class="type">int</span> in_tick)</span></span><br><span class="line">&#123;</span><br><span class="line">     <span class="type">int</span> i;</span><br><span class="line">     <span class="keyword">for</span> (i = <span class="number">0</span>; i != n; ++ i)</span><br><span class="line">     &#123;</span><br><span class="line">          <span class="type">uintptr_t</span> v;</span><br><span class="line">          <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span>;</span></span><br><span class="line">          <span class="comment">// cprintf(&quot;i %d, SWAP: call swap_out_victim\n&quot;,i);</span></span><br><span class="line">          <span class="type">int</span> r = sm-&gt;swap_out_victim(mm, &amp;page, in_tick);</span><br><span class="line">          <span class="keyword">if</span> (r != <span class="number">0</span>) &#123;</span><br><span class="line">                    cprintf(<span class="string">&quot;i %d, swap_out: call swap_out_victim failed\n&quot;</span>,i);</span><br><span class="line">                  <span class="keyword">break</span>;</span><br><span class="line">          &#125;          </span><br><span class="line">          <span class="comment">//assert(!PageReserved(page));</span></span><br><span class="line"></span><br><span class="line">          <span class="comment">// cprintf(&quot;SWAP: choose victim page 0x%08x\n&quot;, page);</span></span><br><span class="line">          </span><br><span class="line">          v=page-&gt;pra_vaddr; </span><br><span class="line">          </span><br><span class="line">          <span class="type">pte_t</span> *ptep = get_pte(mm-&gt;pgdir, v, <span class="number">0</span>);</span><br><span class="line">          assert((*ptep &amp; PTE_P) != <span class="number">0</span>);</span><br><span class="line">          <span class="type">swap_entry_t</span> entry = get_swap_entry(); <span class="comment">// 改为从分配器中获取，不是地址</span></span><br><span class="line">         </span><br><span class="line">          <span class="keyword">if</span> (swapfs_write(entry , page) != <span class="number">0</span>) &#123;</span><br><span class="line">               cprintf(<span class="string">&quot;SWAP: failed to save\n&quot;</span>);</span><br><span class="line">               sm-&gt;map_swappable(mm, v, page, <span class="number">0</span>);</span><br><span class="line">               <span class="keyword">continue</span>;</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">else</span> &#123;</span><br><span class="line">               *ptep = entry;</span><br><span class="line">               free_page(page);</span><br><span class="line">          &#125;</span><br><span class="line">          </span><br><span class="line">          tlb_invalidate(mm-&gt;pgdir, v);</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">return</span> i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">swap_in</span><span class="params">(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uintptr_t</span> addr, <span class="keyword">struct</span> Page **ptr_result)</span></span><br><span class="line">&#123;</span><br><span class="line">     <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">result</span> =</span> alloc_page();</span><br><span class="line">     assert(result!=<span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">     <span class="type">pte_t</span> *ptep = get_pte(mm-&gt;pgdir, addr, <span class="number">0</span>);</span><br><span class="line">     <span class="type">int</span> r;</span><br><span class="line">     <span class="keyword">if</span> ((r = swapfs_read((*ptep), result)) != <span class="number">0</span>)</span><br><span class="line">     &#123;</span><br><span class="line">        assert(r!=<span class="number">0</span>);</span><br><span class="line">     &#125;</span><br><span class="line">     free_swap_entry(*ptep); <span class="comment">// 释放 swap 空间</span></span><br><span class="line">     *ptr_result=result;</span><br><span class="line">     <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>关于三个物理帧的要求，也可以理解为整个进程包括代码段、<code>BSS</code> 段只能使用 3 个物理帧。在这种情况下，需要统计每个进程使用的物理页面才能进行限制。同时，由于在 <code>fork</code> 的时候，有可能有一部分的页面在 swap 分区上，从而在 <code>copy_range</code> 的时候不会被拷贝，需要改造 <code>copy_range</code> 复制处于 swap 分区上的页面。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">copy_range</span><span class="params">(<span class="type">pde_t</span> *to, <span class="type">pde_t</span> *from, <span class="type">uintptr_t</span> start, <span class="type">uintptr_t</span> end, <span class="type">bool</span> share)</span> &#123;</span><br><span class="line">    assert(start % PGSIZE == <span class="number">0</span> &amp;&amp; end % PGSIZE == <span class="number">0</span>);</span><br><span class="line">    assert(USER_ACCESS(start, end));</span><br><span class="line">    <span class="comment">// copy content by page unit.</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        <span class="comment">//call get_pte to find process A&#x27;s pte according to the addr start</span></span><br><span class="line">        <span class="type">pte_t</span> *ptep = get_pte(from, start, <span class="number">0</span>), *nptep;</span><br><span class="line">        <span class="keyword">if</span> (ptep == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            start = ROUNDDOWN(start + PTSIZE, PTSIZE);</span><br><span class="line">            <span class="keyword">continue</span> ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//call get_pte to find process B&#x27;s pte according to the addr start. If pte is NULL, just alloc a PT</span></span><br><span class="line">        <span class="keyword">if</span> (*ptep &amp; PTE_P) &#123;</span><br><span class="line">            <span class="comment">// ....</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> ((nptep = get_pte(to, start, <span class="number">1</span>)) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> -E_NO_MEM;</span><br><span class="line">            &#125;</span><br><span class="line">            *nptep = *ptep;</span><br><span class="line">            <span class="keyword">if</span> (*nptep != <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="comment">// need to copy swap</span></span><br><span class="line">                <span class="class"><span class="keyword">struct</span> <span class="title">Page</span>* <span class="title">page</span> =</span> alloc_page();</span><br><span class="line">                swapfs_read(*nptep, page);</span><br><span class="line">                <span class="type">swap_entry_t</span> entry = get_swap_entry();</span><br><span class="line">                swapfs_write(entry, page);</span><br><span class="line">                *nptep = entry;</span><br><span class="line">                free_page(page);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        start += PGSIZE;</span><br><span class="line">    &#125; <span class="keyword">while</span> (start != <span class="number">0</span> &amp;&amp; start &lt; end);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>最后，在进程退出的时候，也需要删除其所占用的 swap 空间。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">page_remove_pte</span><span class="params">(<span class="type">pde_t</span> *pgdir, <span class="type">uintptr_t</span> la, <span class="type">pte_t</span> *ptep)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (*ptep &amp; PTE_P) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span> =</span> pte2page(*ptep);</span><br><span class="line">        <span class="keyword">if</span> (page_ref_dec(page) == <span class="number">0</span>) &#123;</span><br><span class="line">            free_page(page);</span><br><span class="line">        &#125;</span><br><span class="line">        *ptep = <span class="number">0</span>;</span><br><span class="line">        tlb_invalidate(pgdir, la);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// this is a swap page</span></span><br><span class="line">        free_swap_entry(*ptep);</span><br><span class="line">        *ptep = <span class="number">0</span>;</span><br><span class="line">        tlb_invalidate(pgdir, la);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>为了统计使用物理帧的情况，需要在 <code>struct mm_struct</code> 中增加字段，并在分配页和释放页的时候同步更新，并且在超出使用限制的时候将多余的页面交换出去，从而达到限制每个进程的页面数的目的（用来测试交换功能，此时每个进程都以为内存只有三页物理帧可以用）。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">mm_struct</span> &#123;</span></span><br><span class="line">    <span class="type">list_entry_t</span> mmap_list;        <span class="comment">// linear list link which sorted by start addr of vma</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">vma_struct</span> *<span class="title">mmap_cache</span>;</span> <span class="comment">// current accessed vma, used for speed purpose</span></span><br><span class="line">    <span class="type">pde_t</span> *pgdir;                  <span class="comment">// the PDT of these vma</span></span><br><span class="line">    <span class="type">int</span> map_count;                 <span class="comment">// the count of these vma</span></span><br><span class="line">    <span class="type">void</span> *sm_priv;                 <span class="comment">// the private data for swap manager</span></span><br><span class="line">    <span class="type">int</span> mm_count;                  <span class="comment">// the number of process which shared the mm</span></span><br><span class="line">    <span class="type">semaphore_t</span> mm_sem;            <span class="comment">// mutex for using dup_mmap fun to duplicate the mm </span></span><br><span class="line">    <span class="type">int</span> locked_by;                 <span class="comment">// the lock owner process&#x27;s pid</span></span><br><span class="line">    <span class="type">int</span> phys_count;<span class="comment">// physical pages allocated to the mm</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在申请新物理页的时候需要对 swap 信息和内存限制更新：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> Page *</span><br><span class="line"><span class="title function_">pgdir_alloc_page</span><span class="params">(<span class="type">pde_t</span> *pgdir, <span class="type">uintptr_t</span> la, <span class="type">uint32_t</span> perm, <span class="keyword">struct</span> mm_struct *mm)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span> =</span> alloc_page();</span><br><span class="line">    <span class="keyword">if</span> (page != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (page_insert(pgdir, page, la, perm) != <span class="number">0</span>) &#123;</span><br><span class="line">            free_page(page);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (swap_init_ok)&#123;</span><br><span class="line">            <span class="keyword">if</span>(check_mm_struct!=<span class="literal">NULL</span>) &#123;</span><br><span class="line">                swap_map_swappable(check_mm_struct, la, page, <span class="number">0</span>);</span><br><span class="line">                page-&gt;pra_vaddr=la;</span><br><span class="line">                assert(page_ref(page) == <span class="number">1</span>);</span><br><span class="line">                <span class="comment">//cprintf(&quot;get No. %d  page: pra_vaddr %x, pra_link.prev %x, pra_link_next %x in pgdir_alloc_page\n&quot;, (page-pages), page-&gt;pra_vaddr,page-&gt;pra_page_link.prev, page-&gt;pra_page_link.next);</span></span><br><span class="line">            &#125; </span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (mm)  &#123;  <span class="comment">// 统计物理页</span></span><br><span class="line">                swap_map_swappable(mm, la, page, <span class="number">0</span>);</span><br><span class="line">                page-&gt;pra_vaddr=la;</span><br><span class="line">                mm-&gt;phys_count++;</span><br><span class="line">                assert(page_ref(page) == <span class="number">1</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> page;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>检查物理页是否超出限制：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> </span><br><span class="line"><span class="title function_">check_phys_page</span><span class="params">(<span class="keyword">struct</span> mm_struct *mm)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!mm) <span class="keyword">return</span>;</span><br><span class="line">    <span class="keyword">if</span> (MAX_PHYS_PAGE_PER_PROC &amp;&amp; mm-&gt;phys_count == MAX_PHYS_PAGE_PER_PROC) &#123;</span><br><span class="line">        swap_out(mm, <span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line">        --mm-&gt;phys_count;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>为了检查限制物理页的程序是否正确，编写以下程序进行大量内存使用的读写：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;ulib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ARRSIZE 100000</span></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> arr[ARRSIZE];</span><br><span class="line"></span><br><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    cprintf(<span class="string">&quot;This program will test working set alg...\n&quot;</span>);</span><br><span class="line">    cprintf(<span class="string">&quot;Global arr size = %d\n&quot;</span>, ARRSIZE);</span><br><span class="line"></span><br><span class="line">    cprintf(<span class="string">&quot;!!!!! You should see many page faults !!!!!\n&quot;</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; ARRSIZE; ++i) &#123;</span><br><span class="line">        arr[i] = i;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cprintf(<span class="string">&quot;!!!!! There should be few page faults !!!!!\n&quot;</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; ARRSIZE; ++i) &#123;</span><br><span class="line">        arr[i % <span class="number">256</span>] = i;</span><br><span class="line">    &#125; </span><br><span class="line"></span><br><span class="line">    cprintf(<span class="string">&quot;finish...\n&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>前半部分读写范围大，在物理页限制较少的时候应该会产生大量缺页以及大量页面交换，后半部分读写范围不足一页，应该极少出现缺页。</p><p>测试结果发现确实符合预期：<a href="/university/ucore/ucore-os-lab-8/Screenshot_20190629_004258.png" data-fancybox="gallery" data-caption="Screenshot_20190629_004258"><img src="/university/ucore/ucore-os-lab-8/Screenshot_20190629_004258.png" alt="Screenshot_20190629_004258"></a></p><p>可以看到每个进程使用的物理页帧都被限制在三页以内，因此会出现大量的缺页异常，而在 <code>workset</code> 程序中，前半部分产生的缺页异常远远大于后半部分。由于代码段和 <code>bss</code> 段也被限制了，因此在程序执行过程中，也会出现大量的缺页异常，用来交换代码。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 7 同步互斥</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-7.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题"><a href="#练习-1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题" class="headerlink" title="练习 1]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Sat, 29 Jun 2019 22:51:06 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题"><a href="#练习-1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题" class="headerlink" title="练习 1 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题"></a>练习 1 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题</h3><p>内核级信号量实现位于 <code>sem.c</code> 中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">bool</span></span><br><span class="line"><span class="title function_">try_down</span><span class="params">(<span class="type">semaphore_t</span> *sem)</span> &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag, ret = <span class="number">0</span>;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    <span class="keyword">if</span> (sem-&gt;value &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        sem-&gt;value --, ret = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>内核级信号量中使用了等待队列的方法来实现：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> value;</span><br><span class="line">    <span class="type">wait_queue_t</span> wait_queue;</span><br><span class="line">&#125; <span class="type">semaphore_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span>;</span></span><br><span class="line">    <span class="type">uint32_t</span> wakeup_flags;</span><br><span class="line">    <span class="type">wait_queue_t</span> *wait_queue;</span><br><span class="line">    <span class="type">list_entry_t</span> wait_link;</span><br><span class="line">&#125; <span class="type">wait_t</span>;</span><br></pre></td></tr></table></figure><p>信号量中存了信号量的值和一个等待队列，而等待队列中的项就是 <code>wait_t</code> ，存储了等待的进程，唤醒的标志，等待队列和链表项。</p><p>在使用信号量之前，需要调用 <code>sem_init</code> 对信号量的值和等待队列进行初始化：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">sem_init</span><span class="params">(<span class="type">semaphore_t</span> *sem, <span class="type">int</span> value)</span> &#123;</span><br><span class="line">    sem-&gt;value = value;</span><br><span class="line">    wait_queue_init(&amp;(sem-&gt;wait_queue));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而 <code>up</code> 对应课本中的 <code>signal</code> 操作，也就是 <code>V</code> 操作：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> __noinline <span class="type">void</span> __up(<span class="type">semaphore_t</span> *sem, <span class="type">uint32_t</span> wait_state) &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="type">wait_t</span> *wait;</span><br><span class="line">        <span class="keyword">if</span> ((wait = wait_queue_first(&amp;(sem-&gt;wait_queue))) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            sem-&gt;value ++;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> &#123;</span><br><span class="line">            assert(wait-&gt;proc-&gt;wait_state == wait_state);</span><br><span class="line">            wakeup_wait(&amp;(sem-&gt;wait_queue), wait, wait_state, <span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">up</span><span class="params">(<span class="type">semaphore_t</span> *sem)</span> &#123;</span><br><span class="line">    __up(sem, WT_KSEM);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>V</code> 操作中，首先需要关闭中断保证操作的原子性，避免多个线程同时修改信号量。然后判断等待队列中是否有正在等待的进程，如果有，就出队，并唤醒该进程；否则直接将信号量的值加一。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">wakeup_wait</span><span class="params">(<span class="type">wait_queue_t</span> *<span class="built_in">queue</span>, <span class="type">wait_t</span> *wait, <span class="type">uint32_t</span> wakeup_flags, <span class="type">bool</span> del)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (del) &#123;</span><br><span class="line">        wait_queue_del(<span class="built_in">queue</span>, wait);</span><br><span class="line">    &#125;</span><br><span class="line">    wait-&gt;wakeup_flags = wakeup_flags;</span><br><span class="line">    wakeup_proc(wait-&gt;proc);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而 <code>down</code> 函数对应课本中的 <code>wait</code> 操作，也就是 <code>P</code> 操作：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> __noinline <span class="type">uint32_t</span> __down(<span class="type">semaphore_t</span> *sem, <span class="type">uint32_t</span> wait_state) &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    <span class="keyword">if</span> (sem-&gt;value &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        sem-&gt;value --;</span><br><span class="line">        local_intr_restore(intr_flag);</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">wait_t</span> __wait, *wait = &amp;__wait;</span><br><span class="line">    wait_current_set(&amp;(sem-&gt;wait_queue), wait, wait_state);</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line"></span><br><span class="line">    schedule();</span><br><span class="line"></span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    wait_current_del(&amp;(sem-&gt;wait_queue), wait);</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (wait-&gt;wakeup_flags != wait_state) &#123;</span><br><span class="line">        <span class="keyword">return</span> wait-&gt;wakeup_flags;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">down</span><span class="params">(<span class="type">semaphore_t</span> *sem)</span> &#123;</span><br><span class="line">    <span class="type">uint32_t</span> flags = __down(sem, WT_KSEM);</span><br><span class="line">    assert(flags == <span class="number">0</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样使用 <code>local_intr_save</code> 关闭中断保证操作原子性，然后检查信号量的值是否大于 0，如果是则不需要等待，恢复中断并从函数中返回。</p><p>而当信号量为 0 的时候，则需要将自己加入到等待队列中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">wait_current_set</span><span class="params">(<span class="type">wait_queue_t</span> *<span class="built_in">queue</span>, <span class="type">wait_t</span> *wait, <span class="type">uint32_t</span> wait_state)</span> &#123;</span><br><span class="line">    assert(current != <span class="literal">NULL</span>);</span><br><span class="line">    wait_init(wait, current);</span><br><span class="line">    current-&gt;state = PROC_SLEEPING;</span><br><span class="line">    current-&gt;wait_state = wait_state;</span><br><span class="line">    wait_queue_add(<span class="built_in">queue</span>, wait);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此时进程进入睡眠状态并进入等待队列，直到一个 <code>V</code> 操作将其唤醒。</p><p>之后的 <code>schedule</code> 则执行调度器，找到可以运行的进程运行。</p><p>随后调度器不断被时钟中断唤起，不断切换进程，当从 <code>schedule</code> 函数返回的时候，说明其他线程释放了一个信号量，本进程已经被唤醒，获得了信号量，所以将自己从等待队列中删除，最后从 <code>down</code> 函数中返回，继续执行接下来的代码。</p><p>而 <code>try_down</code> 函数则不会在获取不到信号量的时候阻塞：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">bool</span></span><br><span class="line"><span class="title function_">try_down</span><span class="params">(<span class="type">semaphore_t</span> *sem)</span> &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag, ret = <span class="number">0</span>;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    <span class="keyword">if</span> (sem-&gt;value &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        sem-&gt;value --, ret = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于不能保证函数返回的时候获取到了信号量，所以一般需要配合 <code>while</code> 循环使用，但是这样会占用 CPU。</p><p>给用户态进程&#x2F;线程提供信号量的方法与内核态几乎相同，区别在于由于关中断需要内核权限，因此需要给用户态增加系统调用进入内核态来实现 <code>PV</code> 操作，其他关于等待队列的实现是一样的。</p><p>而 <code>check_sync.c</code> 提供了一个哲学家就餐问题的解法：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> state_sema[N]; <span class="comment">/* 记录每个人状态的数组 */</span></span><br><span class="line"><span class="comment">/* 信号量是一个特殊的整型变量 */</span></span><br><span class="line"><span class="type">semaphore_t</span> mutex; <span class="comment">/* 临界区互斥 */</span></span><br><span class="line"><span class="type">semaphore_t</span> s[N]; <span class="comment">/* 每个哲学家一个信号量 */</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">philosopher_proc_sema</span>[<span class="title">N</span>];</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">phi_test_sema</span><span class="params">(i)</span> <span class="comment">/* i：哲学家号码从0到N-1 */</span></span><br><span class="line">&#123; </span><br><span class="line">    <span class="keyword">if</span>(state_sema[i]==HUNGRY&amp;&amp;state_sema[LEFT]!=EATING</span><br><span class="line">            &amp;&amp;state_sema[RIGHT]!=EATING)</span><br><span class="line">    &#123;</span><br><span class="line">        state_sema[i]=EATING;</span><br><span class="line">        up(&amp;s[i]);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">phi_take_forks_sema</span><span class="params">(<span class="type">int</span> i)</span> <span class="comment">/* i：哲学家号码从0到N-1 */</span></span><br><span class="line">&#123; </span><br><span class="line">        down(&amp;mutex); <span class="comment">/* 进入临界区 */</span></span><br><span class="line">        state_sema[i]=HUNGRY; <span class="comment">/* 记录下哲学家i饥饿的事实 */</span></span><br><span class="line">        phi_test_sema(i); <span class="comment">/* 试图得到两只叉子 */</span></span><br><span class="line">        up(&amp;mutex); <span class="comment">/* 离开临界区 */</span></span><br><span class="line">        down(&amp;s[i]); <span class="comment">/* 如果得不到叉子就阻塞 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">phi_put_forks_sema</span><span class="params">(<span class="type">int</span> i)</span> <span class="comment">/* i：哲学家号码从0到N-1 */</span></span><br><span class="line">&#123; </span><br><span class="line">        down(&amp;mutex); <span class="comment">/* 进入临界区 */</span></span><br><span class="line">        state_sema[i]=THINKING; <span class="comment">/* 哲学家进餐结束 */</span></span><br><span class="line">        phi_test_sema(LEFT); <span class="comment">/* 看一下左邻居现在是否能进餐 */</span></span><br><span class="line">        phi_test_sema(RIGHT); <span class="comment">/* 看一下右邻居现在是否能进餐 */</span></span><br><span class="line">        up(&amp;mutex); <span class="comment">/* 离开临界区 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">philosopher_using_semaphore</span><span class="params">(<span class="type">void</span> * arg)</span> <span class="comment">/* i：哲学家号码，从0到N-1 */</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i, iter=<span class="number">0</span>;</span><br><span class="line">    i=(<span class="type">int</span>)arg;</span><br><span class="line">    cprintf(<span class="string">&quot;I am No.%d philosopher_sema\n&quot;</span>,i);</span><br><span class="line">    <span class="keyword">while</span>(iter++&lt;TIMES)</span><br><span class="line">    &#123; <span class="comment">/* 无限循环 */</span></span><br><span class="line">        cprintf(<span class="string">&quot;Iter %d, No.%d philosopher_sema is thinking\n&quot;</span>,iter,i); <span class="comment">/* 哲学家正在思考 */</span></span><br><span class="line">        do_sleep(SLEEP_TIME);</span><br><span class="line">        phi_take_forks_sema(i); </span><br><span class="line">        <span class="comment">/* 需要两只叉子，或者阻塞 */</span></span><br><span class="line">        cprintf(<span class="string">&quot;Iter %d, No.%d philosopher_sema is eating\n&quot;</span>,iter,i); <span class="comment">/* 进餐 */</span></span><br><span class="line">        do_sleep(SLEEP_TIME);</span><br><span class="line">        phi_put_forks_sema(i); </span><br><span class="line">        <span class="comment">/* 把两把叉子同时放回桌子 */</span></span><br><span class="line">    &#125;</span><br><span class="line">    cprintf(<span class="string">&quot;No.%d philosopher_sema quit\n&quot;</span>,i);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 6 调度器</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-6.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-使用-Round-Robin-调度算法"><a href="#练习-1-使用-Round-Robin-调度算法" class="headerlink" title="练习 1 使用 Round-Robin 调度算法"></a>练习 1 使用]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Sat, 29 Jun 2019 22:49:52 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-使用-Round-Robin-调度算法"><a href="#练习-1-使用-Round-Robin-调度算法" class="headerlink" title="练习 1 使用 Round-Robin 调度算法"></a>练习 1 使用 Round-Robin 调度算法</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The introduction of scheduling classes is borrrowed from Linux, and makes the </span></span><br><span class="line"><span class="comment">// core scheduler quite extensible. These classes (the scheduler modules) encapsulate </span></span><br><span class="line"><span class="comment">// the scheduling policies. </span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sched_class</span> &#123;</span></span><br><span class="line">    <span class="comment">// the name of sched_class</span></span><br><span class="line">    <span class="type">const</span> <span class="type">char</span> *name;</span><br><span class="line">    <span class="comment">// Init the run queue</span></span><br><span class="line">    <span class="type">void</span> (*init)(<span class="keyword">struct</span> run_queue *rq);</span><br><span class="line">    <span class="comment">// put the proc into runqueue, and this function must be called with rq_lock</span></span><br><span class="line">    <span class="type">void</span> (*enqueue)(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc);</span><br><span class="line">    <span class="comment">// get the proc out runqueue, and this function must be called with rq_lock</span></span><br><span class="line">    <span class="type">void</span> (*dequeue)(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc);</span><br><span class="line">    <span class="comment">// choose the next runnable task</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *(*<span class="title">pick_next</span>)(<span class="keyword">struct</span> <span class="title">run_queue</span> *<span class="title">rq</span>);</span></span><br><span class="line">    <span class="comment">// dealer of the time-tick</span></span><br><span class="line">    <span class="type">void</span> (*proc_tick)(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc);</span><br><span class="line">    <span class="comment">/* for SMP support in the future</span></span><br><span class="line"><span class="comment">     *  load_balance</span></span><br><span class="line"><span class="comment">     *     void (*load_balance)(struct rq* rq);</span></span><br><span class="line"><span class="comment">     *  get some proc from this rq, used in load_balance,</span></span><br><span class="line"><span class="comment">     *  return value is the num of gotten proc</span></span><br><span class="line"><span class="comment">     *  int (*get_proc)(struct rq* rq, struct proc* procs_moved[]);</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><ul><li><code>init</code> 函数指针用于初始化调度器。</li><li><code>enqueue</code> 函数指针用于将一个进程放入调度队列中。</li><li><code>dequeue</code> 函数指针用于将一个进程从调度队列中出队。</li><li><code>pick_next</code> 函数指针用于从调度队列中根据算法选出下一个要被运行的进程。</li><li><code>proc_tick</code> 函数指针用于系统时钟中断时通知调度器。</li></ul><p>在 ucore 中，首先在 <code>kern_init</code> 进行内核初始化的时候调用 <code>sched_init</code> 进行调度器的初始化，在 Round Robin 算法中，则是将运行队列初始化，并将进程数置为 0：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">RR_init</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">    list_init(&amp;(rq-&gt;run_list));</span><br><span class="line">    rq-&gt;proc_num = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>schedule</code> 函数中，会检查当前进程是否可以继续运行，如果可以的话就重新放入队列中。然后调用 <code>sched_class_pick_next</code> 获取下一个执行的进程，然后将其出队，如果没有正在排队的进程，则运行 idle 进程。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">schedule</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">next</span>;</span></span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    &#123;</span><br><span class="line">        current-&gt;need_resched = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">if</span> (current-&gt;state == PROC_RUNNABLE) &#123;</span><br><span class="line">            sched_class_enqueue(current);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> ((next = sched_class_pick_next()) != <span class="literal">NULL</span>) &#123;</span><br><span class="line">            sched_class_dequeue(next);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (next == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            next = idleproc;</span><br><span class="line">        &#125;</span><br><span class="line">        next-&gt;runs ++;</span><br><span class="line">        <span class="keyword">if</span> (next != current) &#123;</span><br><span class="line">            proc_run(next);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而在 Round Robin 算法，每个进程都有固定的时间片可以执行，而且按照先来先到的顺序执行：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">RR_enqueue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    assert(list_empty(&amp;(proc-&gt;run_link)));</span><br><span class="line">    list_add_before(&amp;(rq-&gt;run_list), &amp;(proc-&gt;run_link));</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span> || proc-&gt;time_slice &gt; rq-&gt;max_time_slice) &#123;</span><br><span class="line">        proc-&gt;time_slice = rq-&gt;max_time_slice;</span><br><span class="line">    &#125;</span><br><span class="line">    proc-&gt;rq = rq;</span><br><span class="line">    rq-&gt;proc_num ++;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">RR_dequeue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    assert(!list_empty(&amp;(proc-&gt;run_link)) &amp;&amp; proc-&gt;rq == rq);</span><br><span class="line">    list_del_init(&amp;(proc-&gt;run_link));</span><br><span class="line">    rq-&gt;proc_num --;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">struct</span> proc_struct *</span><br><span class="line"><span class="title function_">RR_pick_next</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">    <span class="type">list_entry_t</span> *le = list_next(&amp;(rq-&gt;run_list));</span><br><span class="line">    <span class="keyword">if</span> (le != &amp;(rq-&gt;run_list)) &#123;</span><br><span class="line">        <span class="keyword">return</span> le2proc(le, run_link);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>RR_pick_next</code> 则是简单地取出队头。</p><p>在时钟中断发生的时候，会将进程的时间减 1，如果时间片用完了，则将进程的 <code>need_reschedule</code> 位置 1，以便下次唤醒调度器的时候将进程调度走。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">RR_proc_tick</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;time_slice --;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;need_resched = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="练习-2-实现-Stride-Scheduling-调度算法"><a href="#练习-2-实现-Stride-Scheduling-调度算法" class="headerlink" title="练习 2 实现 Stride Scheduling 调度算法"></a>练习 2 实现 Stride Scheduling 调度算法</h3><p>在 Stride Scheduling 中，使用了斜堆加速调度。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">skew_heap_init</span><span class="params">(<span class="type">skew_heap_entry_t</span> *a)</span></span><br><span class="line">&#123;</span><br><span class="line">     a-&gt;left = a-&gt;right = a-&gt;parent = <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">skew_heap_entry_t</span> *</span><br><span class="line"><span class="title function_">skew_heap_merge</span><span class="params">(<span class="type">skew_heap_entry_t</span> *a, <span class="type">skew_heap_entry_t</span> *b,</span></span><br><span class="line"><span class="params">                compare_f comp)</span></span><br><span class="line">&#123;</span><br><span class="line">     <span class="keyword">if</span> (a == <span class="literal">NULL</span>) <span class="keyword">return</span> b;</span><br><span class="line">     <span class="keyword">else</span> <span class="keyword">if</span> (b == <span class="literal">NULL</span>) <span class="keyword">return</span> a;</span><br><span class="line">     </span><br><span class="line">     <span class="type">skew_heap_entry_t</span> *l, *r;</span><br><span class="line">     <span class="keyword">if</span> (comp(a, b) == <span class="number">-1</span>)</span><br><span class="line">     &#123;</span><br><span class="line">          r = a-&gt;left;</span><br><span class="line">          l = skew_heap_merge(a-&gt;right, b, comp);</span><br><span class="line">          </span><br><span class="line">          a-&gt;left = l;</span><br><span class="line">          a-&gt;right = r;</span><br><span class="line">          <span class="keyword">if</span> (l) l-&gt;parent = a;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">return</span> a;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">else</span></span><br><span class="line">     &#123;</span><br><span class="line">          r = b-&gt;left;</span><br><span class="line">          l = skew_heap_merge(a, b-&gt;right, comp);</span><br><span class="line">          </span><br><span class="line">          b-&gt;left = l;</span><br><span class="line">          b-&gt;right = r;</span><br><span class="line">          <span class="keyword">if</span> (l) l-&gt;parent = b;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">return</span> b;</span><br><span class="line">     &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">skew_heap_entry_t</span> *</span><br><span class="line"><span class="title function_">skew_heap_insert</span><span class="params">(<span class="type">skew_heap_entry_t</span> *a, <span class="type">skew_heap_entry_t</span> *b,</span></span><br><span class="line"><span class="params">                 compare_f comp)</span></span><br><span class="line">&#123;</span><br><span class="line">     skew_heap_init(b);</span><br><span class="line">     <span class="keyword">return</span> skew_heap_merge(a, b, comp);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">skew_heap_entry_t</span> *</span><br><span class="line"><span class="title function_">skew_heap_remove</span><span class="params">(<span class="type">skew_heap_entry_t</span> *a, <span class="type">skew_heap_entry_t</span> *b,</span></span><br><span class="line"><span class="params">                 compare_f comp)</span></span><br><span class="line">&#123;</span><br><span class="line">     <span class="type">skew_heap_entry_t</span> *p   = b-&gt;parent;</span><br><span class="line">     <span class="type">skew_heap_entry_t</span> *rep = skew_heap_merge(b-&gt;left, b-&gt;right, comp);</span><br><span class="line">     <span class="keyword">if</span> (rep) rep-&gt;parent = p;</span><br><span class="line">     </span><br><span class="line">     <span class="keyword">if</span> (p)</span><br><span class="line">     &#123;</span><br><span class="line">          <span class="keyword">if</span> (p-&gt;left == b)</span><br><span class="line">               p-&gt;left = rep;</span><br><span class="line">          <span class="keyword">else</span> p-&gt;right = rep;</span><br><span class="line">          <span class="keyword">return</span> a;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">else</span> <span class="keyword">return</span> rep;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>斜堆是一种特殊的二叉树，特点是合并的时候比普通的堆更快。</p><p>为此，首先定义了一个比较两个进程的步长的函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> BIG_STRIDE (1 &lt;&lt; 30)   <span class="comment">/* you should give a value, and is ??? */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* The compare function for two skew_heap_node_t&#x27;s and the</span></span><br><span class="line"><span class="comment"> * corresponding procs*/</span></span><br><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">proc_stride_comp_f</span><span class="params">(<span class="type">void</span> *a, <span class="type">void</span> *b)</span></span><br><span class="line">&#123;</span><br><span class="line">     <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">p</span> =</span> le2proc(a, lab6_run_pool);</span><br><span class="line">     <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">q</span> =</span> le2proc(b, lab6_run_pool);</span><br><span class="line">     <span class="type">int32_t</span> c = p-&gt;lab6_stride - q-&gt;lab6_stride;</span><br><span class="line">     <span class="keyword">if</span> (c &gt; <span class="number">0</span>) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">     <span class="keyword">else</span> <span class="keyword">if</span> (c == <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">     <span class="keyword">else</span> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>初始化的时候，则使用 <code>lab6_run_pool</code> 变量，初始为  NULL 指针。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * stride_init initializes the run-queue rq with correct assignment for</span></span><br><span class="line"><span class="comment"> * member variables, including:</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> *   - run_list: should be a empty list after initialization.</span></span><br><span class="line"><span class="comment"> *   - lab6_run_pool: NULL</span></span><br><span class="line"><span class="comment"> *   - proc_num: 0</span></span><br><span class="line"><span class="comment"> *   - max_time_slice: no need here, the variable would be assigned by the caller.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * hint: see libs/list.h for routines of the list structures.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">stride_init</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">     <span class="comment">/* LAB6: YOUR CODE </span></span><br><span class="line"><span class="comment">      * (1) init the ready process list: rq-&gt;run_list</span></span><br><span class="line"><span class="comment">      * (2) init the run pool: rq-&gt;lab6_run_pool</span></span><br><span class="line"><span class="comment">      * (3) set number of process: rq-&gt;proc_num to 0       </span></span><br><span class="line"><span class="comment">      */</span></span><br><span class="line">     rq-&gt;lab6_run_pool = <span class="literal">NULL</span>;</span><br><span class="line">     rq-&gt;proc_num = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>入队的函数则如下，使用 <code>skew_heap_insert</code> 将进程按步长作比较插入到斜堆中，然后重新分配时间片，最后 <code>proc_num++</code>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * stride_enqueue inserts the process ``proc&#x27;&#x27; into the run-queue</span></span><br><span class="line"><span class="comment"> * ``rq&#x27;&#x27;. The procedure should verify/initialize the relevant members</span></span><br><span class="line"><span class="comment"> * of ``proc&#x27;&#x27;, and then put the ``lab6_run_pool&#x27;&#x27; node into the</span></span><br><span class="line"><span class="comment"> * queue(since we use priority queue here). The procedure should also</span></span><br><span class="line"><span class="comment"> * update the meta date in ``rq&#x27;&#x27; structure.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * proc-&gt;time_slice denotes the time slices allocation for the</span></span><br><span class="line"><span class="comment"> * process, which should set to rq-&gt;max_time_slice.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * hint: see libs/skew_heap.h for routines of the priority</span></span><br><span class="line"><span class="comment"> * queue structures.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">stride_enqueue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">     <span class="comment">/* LAB6: YOUR CODE </span></span><br><span class="line"><span class="comment">      * (1) insert the proc into rq correctly</span></span><br><span class="line"><span class="comment">      * NOTICE: you can use skew_heap or list. Important functions</span></span><br><span class="line"><span class="comment">      *         skew_heap_insert: insert a entry into skew_heap</span></span><br><span class="line"><span class="comment">      *         list_add_before: insert  a entry into the last of list   </span></span><br><span class="line"><span class="comment">      * (2) recalculate proc-&gt;time_slice</span></span><br><span class="line"><span class="comment">      * (3) set proc-&gt;rq pointer to rq</span></span><br><span class="line"><span class="comment">      * (4) increase rq-&gt;proc_num</span></span><br><span class="line"><span class="comment">      */</span></span><br><span class="line">     rq-&gt;lab6_run_pool = skew_heap_insert(rq-&gt;lab6_run_pool, &amp;proc-&gt;lab6_run_pool, proc_stride_comp_f);</span><br><span class="line">     <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span> || proc-&gt;time_slice &gt; rq-&gt;max_time_slice) &#123;</span><br><span class="line">        proc-&gt;time_slice = rq-&gt;max_time_slice;</span><br><span class="line">     &#125;</span><br><span class="line">     proc-&gt;rq = rq;</span><br><span class="line">     ++rq-&gt;proc_num;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>出队的函数则和入队相反：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * stride_dequeue removes the process ``proc&#x27;&#x27; from the run-queue</span></span><br><span class="line"><span class="comment"> * ``rq&#x27;&#x27;, the operation would be finished by the skew_heap_remove</span></span><br><span class="line"><span class="comment"> * operations. Remember to update the ``rq&#x27;&#x27; structure.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * hint: see libs/skew_heap.h for routines of the priority</span></span><br><span class="line"><span class="comment"> * queue structures.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">stride_dequeue</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">     <span class="comment">/* LAB6: YOUR CODE </span></span><br><span class="line"><span class="comment">      * (1) remove the proc from rq correctly</span></span><br><span class="line"><span class="comment">      * NOTICE: you can use skew_heap or list. Important functions</span></span><br><span class="line"><span class="comment">      *         skew_heap_remove: remove a entry from skew_heap</span></span><br><span class="line"><span class="comment">      *         list_del_init: remove a entry from the  list</span></span><br><span class="line"><span class="comment">      */</span></span><br><span class="line">     rq-&gt;lab6_run_pool = skew_heap_remove(rq-&gt;lab6_run_pool, &amp;proc-&gt;lab6_run_pool, proc_stride_comp_f);</span><br><span class="line">     <span class="comment">// list_del_init(&amp;proc-&gt;run_link);</span></span><br><span class="line">     <span class="comment">// proc-&gt;rq = NULL;</span></span><br><span class="line">     rq-&gt;proc_num--;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>pick_next</code> 中，直接取斜堆堆顶元素就是需要运行的进程，同时需要更新进程的 <code>stride</code> 值，更新之后需要重新插入到堆中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * stride_pick_next pick the element from the ``run-queue&#x27;&#x27;, with the</span></span><br><span class="line"><span class="comment"> * minimum value of stride, and returns the corresponding process</span></span><br><span class="line"><span class="comment"> * pointer. The process pointer would be calculated by macro le2proc,</span></span><br><span class="line"><span class="comment"> * see kern/process/proc.h for definition. Return NULL if</span></span><br><span class="line"><span class="comment"> * there is no process in the queue.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * When one proc structure is selected, remember to update the stride</span></span><br><span class="line"><span class="comment"> * property of the proc. (stride += BIG_STRIDE / priority)</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * hint: see libs/skew_heap.h for routines of the priority</span></span><br><span class="line"><span class="comment"> * queue structures.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">struct</span> proc_struct *</span><br><span class="line"><span class="title function_">stride_pick_next</span><span class="params">(<span class="keyword">struct</span> run_queue *rq)</span> &#123;</span><br><span class="line">     <span class="comment">/* LAB6: YOUR CODE </span></span><br><span class="line"><span class="comment">      * (1) get a  proc_struct pointer p  with the minimum value of stride</span></span><br><span class="line"><span class="comment">             (1.1) If using skew_heap, we can use le2proc get the p from rq-&gt;lab6_run_pool</span></span><br><span class="line"><span class="comment">             (1.2) If using list, we have to search list to find the p with minimum stride value</span></span><br><span class="line"><span class="comment">      * (2) update p;s stride value: p-&gt;lab6_stride</span></span><br><span class="line"><span class="comment">      * (3) return p</span></span><br><span class="line"><span class="comment">      */</span></span><br><span class="line">     <span class="keyword">if</span> (rq-&gt;proc_num == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">     <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span>* <span class="title">proc</span> =</span> le2proc(rq-&gt;lab6_run_pool, lab6_run_pool);</span><br><span class="line">     rq-&gt;lab6_run_pool = skew_heap_remove(rq-&gt;lab6_run_pool, &amp;proc-&gt;lab6_run_pool, proc_stride_comp_f);</span><br><span class="line">     proc-&gt;lab6_stride += BIG_STRIDE / (proc-&gt;lab6_priority == <span class="number">0</span> ? <span class="number">1</span> : proc-&gt;lab6_priority);</span><br><span class="line">     rq-&gt;lab6_run_pool = skew_heap_insert(rq-&gt;lab6_run_pool, &amp;proc-&gt;lab6_run_pool, proc_stride_comp_f);</span><br><span class="line">     </span><br><span class="line">     <span class="keyword">return</span> proc;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>时钟中断函数和 Round Robin 算法一样：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * stride_proc_tick works with the tick event of current process. You</span></span><br><span class="line"><span class="comment"> * should check whether the time slices for current process is</span></span><br><span class="line"><span class="comment"> * exhausted and update the proc struct ``proc&#x27;&#x27;. proc-&gt;time_slice</span></span><br><span class="line"><span class="comment"> * denotes the time slices left for current</span></span><br><span class="line"><span class="comment"> * process. proc-&gt;need_resched is the flag variable for process</span></span><br><span class="line"><span class="comment"> * switching.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">stride_proc_tick</span><span class="params">(<span class="keyword">struct</span> run_queue *rq, <span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">     <span class="comment">/* LAB6: YOUR CODE */</span></span><br><span class="line">     <span class="keyword">if</span> (proc-&gt;time_slice &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;time_slice --;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (proc-&gt;time_slice == <span class="number">0</span>) &#123;</span><br><span class="line">        proc-&gt;need_resched = <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行结果：</p><p><a href="/university/ucore/ucore-os-lab-6/Screenshot_20190609_231645.png" data-fancybox="gallery" data-caption="Screenshot_20190609_231645"><img src="/university/ucore/ucore-os-lab-6/Screenshot_20190609_231645.png" alt="Screenshot_20190609_231645"></a><a href="/university/ucore/ucore-os-lab-6/Screenshot_20190609_231802.png" data-fancybox="gallery" data-caption="Screenshot_20190609_231802"><img src="/university/ucore/ucore-os-lab-6/Screenshot_20190609_231802.png" alt="Screenshot_20190609_231802"></a></p><h3 id="练习-3-阅读分析源代码"><a href="#练习-3-阅读分析源代码" class="headerlink" title="练习 3 阅读分析源代码"></a>练习 3 阅读分析源代码</h3><p>结合中断处理和调度程序，再次理解进程控制块中的 trapframe 和 context 在进程切换时作用。</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="symbol">__alltraps:</span></span><br><span class="line">    <span class="comment"># push registers to build a trap frame</span></span><br><span class="line">    <span class="comment"># therefore make the stack look like a struct trapframe</span></span><br><span class="line">    pushl %ds</span><br><span class="line">    pushl %es</span><br><span class="line">    pushl %fs</span><br><span class="line">    pushl %gs</span><br><span class="line">    pushal</span><br><span class="line"></span><br><span class="line">    <span class="comment"># load GD_KDATA into %ds and %es to set up data segments for kernel</span></span><br><span class="line">    movl $GD_KDATA, %eax</span><br><span class="line">    movw %ax, %ds</span><br><span class="line">    movw %ax, %es</span><br><span class="line"></span><br><span class="line">    <span class="comment"># push %esp to pass a pointer to the trapframe as an argument to trap()</span></span><br><span class="line">    pushl %esp</span><br><span class="line"></span><br><span class="line">    <span class="comment"># call trap(tf), where tf=%esp</span></span><br><span class="line">    call trap</span><br><span class="line"></span><br><span class="line">    <span class="comment"># pop the pushed stack pointer</span></span><br><span class="line">    popl %esp</span><br><span class="line"><span class="symbol">__trapret:</span></span><br><span class="line">    <span class="comment"># restore registers from stack</span></span><br><span class="line">    popal</span><br><span class="line"></span><br><span class="line">    <span class="comment"># restore %ds, %es, %fs and %gs</span></span><br><span class="line">    popl %gs</span><br><span class="line">    popl %fs</span><br><span class="line">    popl %es</span><br><span class="line">    popl %ds</span><br><span class="line"></span><br><span class="line">    <span class="comment"># get rid of the trap number and error code</span></span><br><span class="line">    <span class="keyword">addl$0x8, </span>%esp</span><br><span class="line">    iret</span><br></pre></td></tr></table></figure><p><code>trapframe</code> 用于在中断发生的时候保存进程的现场，如果发生特权级转换还会保存段寄存器等信息，在 <code>__alltrpas</code> 中保存，<code>__trapret</code> 中恢复。</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">switch_to: </span>                     <span class="comment"># switch_to(from, to)</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># save from&#x27;s registers</span></span><br><span class="line">    movl <span class="number">4</span>(%esp), %eax          <span class="comment"># eax points to from</span></span><br><span class="line">    popl <span class="number">0</span>(%eax)                <span class="comment"># save eip !popl</span></span><br><span class="line">    movl %esp, <span class="number">4</span>(%eax)</span><br><span class="line">    movl %ebx, <span class="number">8</span>(%eax)</span><br><span class="line">    movl %ecx, <span class="number">12</span>(%eax)</span><br><span class="line">    movl %edx, <span class="number">16</span>(%eax)</span><br><span class="line">    movl %esi, <span class="number">20</span>(%eax)</span><br><span class="line">    movl %edi, <span class="number">24</span>(%eax)</span><br><span class="line">    movl %ebp, <span class="number">28</span>(%eax)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># restore to&#x27;s registers</span></span><br><span class="line">    movl <span class="number">4</span>(%esp), %eax          <span class="comment"># not 8(%esp): popped return address already</span></span><br><span class="line">                                <span class="comment"># eax now points to to</span></span><br><span class="line">    movl <span class="number">28</span>(%eax), %ebp</span><br><span class="line">    movl <span class="number">24</span>(%eax), %edi</span><br><span class="line">    movl <span class="number">20</span>(%eax), %esi</span><br><span class="line">    movl <span class="number">16</span>(%eax), %edx</span><br><span class="line">    movl <span class="number">12</span>(%eax), %ecx</span><br><span class="line">    movl <span class="number">8</span>(%eax), %ebx</span><br><span class="line">    movl <span class="number">4</span>(%eax), %esp</span><br><span class="line"></span><br><span class="line">    pushl <span class="number">0</span>(%eax)               <span class="comment"># push eip</span></span><br><span class="line"></span><br><span class="line">    ret</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>而 <code>context</code> 只是用于进程调度 <code>schedule</code> 的时候切换进程的时候保存和恢复上下文使用的，在 <code>switch_to</code> 函数中保存和恢复。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 5 用户进程管理</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-5.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-加载应用程序并执行"><a href="#练习-1-加载应用程序并执行" class="headerlink" title="练习 1 加载应用程序并执行"></a>练习 1 加载应用程序并执行</h3><p>在]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Sat, 29 Jun 2019 22:46:10 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-加载应用程序并执行"><a href="#练习-1-加载应用程序并执行" class="headerlink" title="练习 1 加载应用程序并执行"></a>练习 1 加载应用程序并执行</h3><p>在 <code>load_icode</code> 中，前面的函数体负责初始化内存内容和拷贝相应的内容到内存中，最后需要一种方法，使得 CPU 可以从用户进程入口处开始运行。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">tf-&gt;tf_cs = USER_CS;</span><br><span class="line">tf-&gt;tf_ds = tf-&gt;tf_es = tf-&gt;tf_ss = USER_DS;</span><br><span class="line">tf-&gt;tf_esp = USTACKTOP;</span><br><span class="line">tf-&gt;tf_eip = elf-&gt;e_entry;</span><br><span class="line">tf-&gt;tf_eflags = FL_IF;</span><br><span class="line">ret = <span class="number">0</span>;</span><br></pre></td></tr></table></figure><p>在中断返回之后，CPU 会使用 <code>trapframe</code> 中的值设置寄存器，特权级的切换则发生在这里。所以，我们要设置 <code>tf</code> 中的段寄存器，使其进入用户空间。</p><p>同时，用户使用的栈和内核栈也不同，需要设置栈指针到用户栈的栈顶。</p><p>而返回后应该从 <code>elf</code> 文件的入口开始执行，所以设置 <code>eip</code> 的指针到 <code>elf</code> 的入口。</p><p>最后，需要重新打开 CPU 的中断开关，以便操作系统可以处理中断。</p><p>可以看到，在创建了 Lab 4 中的两个内核线程之后，紧接着 <code>init_proc</code> 会创建一个 <code>user_main</code> 的内核线程：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">init_main</span><span class="params">(<span class="type">void</span> *arg)</span> &#123;</span><br><span class="line">    <span class="type">size_t</span> nr_free_pages_store = nr_free_pages();</span><br><span class="line">    <span class="type">size_t</span> kernel_allocated_store = kallocated();</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> pid = kernel_thread(user_main, <span class="literal">NULL</span>, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (pid &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">        panic(<span class="string">&quot;create user_main failed.\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (do_wait(<span class="number">0</span>, <span class="literal">NULL</span>) == <span class="number">0</span>) &#123;</span><br><span class="line">        schedule();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cprintf(<span class="string">&quot;all user-mode processes have quit.\n&quot;</span>);</span><br><span class="line">    assert(initproc-&gt;cptr == <span class="literal">NULL</span> &amp;&amp; initproc-&gt;yptr == <span class="literal">NULL</span> &amp;&amp; initproc-&gt;optr == <span class="literal">NULL</span>);</span><br><span class="line">    assert(nr_process == <span class="number">2</span>);</span><br><span class="line">    assert(list_next(&amp;proc_list) == &amp;(initproc-&gt;list_link));</span><br><span class="line">    assert(list_prev(&amp;proc_list) == &amp;(initproc-&gt;list_link));</span><br><span class="line"></span><br><span class="line">    cprintf(<span class="string">&quot;init check memory pass.\n&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在创建好供用户进程运行使用的内核线程会进入 <code>do_wait</code> 函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">do_wait</span><span class="params">(<span class="type">int</span> pid, <span class="type">int</span> *code_store)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">mm_struct</span> *<span class="title">mm</span> =</span> current-&gt;mm;</span><br><span class="line">    <span class="keyword">if</span> (code_store != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!user_mem_check(mm, (<span class="type">uintptr_t</span>)code_store, <span class="keyword">sizeof</span>(<span class="type">int</span>), <span class="number">1</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> -E_INVAL;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span>;</span></span><br><span class="line">    <span class="type">bool</span> intr_flag, haskid;</span><br><span class="line">repeat:</span><br><span class="line">    haskid = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">if</span> (pid != <span class="number">0</span>) &#123;</span><br><span class="line">        proc = find_proc(pid);</span><br><span class="line">        <span class="keyword">if</span> (proc != <span class="literal">NULL</span> &amp;&amp; proc-&gt;parent == current) &#123;</span><br><span class="line">            haskid = <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">if</span> (proc-&gt;state == PROC_ZOMBIE) &#123;</span><br><span class="line">                <span class="keyword">goto</span> found;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> &#123;</span><br><span class="line">        proc = current-&gt;cptr;</span><br><span class="line">        <span class="keyword">for</span> (; proc != <span class="literal">NULL</span>; proc = proc-&gt;optr) &#123;</span><br><span class="line">            haskid = <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">if</span> (proc-&gt;state == PROC_ZOMBIE) &#123;</span><br><span class="line">                <span class="keyword">goto</span> found;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (haskid) &#123;</span><br><span class="line">        current-&gt;state = PROC_SLEEPING;</span><br><span class="line">        current-&gt;wait_state = WT_CHILD;</span><br><span class="line">        schedule();</span><br><span class="line">        <span class="keyword">if</span> (current-&gt;flags &amp; PF_EXITING) &#123;</span><br><span class="line">            do_exit(-E_KILLED);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">goto</span> repeat;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> -E_BAD_PROC;</span><br><span class="line"></span><br><span class="line">found:</span><br><span class="line">    <span class="keyword">if</span> (proc == idleproc || proc == initproc) &#123;</span><br><span class="line">        panic(<span class="string">&quot;wait idleproc or initproc.\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (code_store != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        *code_store = proc-&gt;exit_code;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    &#123;</span><br><span class="line">        unhash_proc(proc);</span><br><span class="line">        remove_links(proc);</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">    put_kstack(proc);</span><br><span class="line">    kfree(proc);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>do_wait</code> 函数中，会检查当前进程是否有孩子进程，如果找到了孩子进程，就检查看孩子进程是否为 <code>PROC_ZOMBIE</code> 状态，由于 <code>user_main</code> 刚刚创建，所以是 <code>PROC_RUNNABLE</code> 状态。当操作系统发现有孩子进程还没有执行完成的时候，就会将父进程置为 <code>PROC_SLEEPING</code> 状态，然后调用调度器。</p><p>而在调度器中，会选择下一个可以运行的进程，也就是 <code>user_main</code> 进程。此时调用 <code>proc_run</code> 切换到了 <code>user_main</code> 函数。</p><p>而在 <code>user_main</code> 中，会马上调用 <code>kernel_execve</code> 加载用户程序开始执行：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">user_main</span><span class="params">(<span class="type">void</span> *arg)</span> &#123;</span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> TEST</span></span><br><span class="line">    KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE);</span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line">    KERNEL_EXECVE(<span class="built_in">exit</span>);</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line">    panic(<span class="string">&quot;user_main execve failed.\n&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而 <code>kernel_execve</code> 是一个系统调用，调用后会进入内核态执行 <code>do_execve</code> 做必要的准备工作：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">kernel_execve</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *name, <span class="type">unsigned</span> <span class="type">char</span> *binary, <span class="type">size_t</span> size)</span> &#123;</span><br><span class="line">    <span class="type">int</span> ret, len = <span class="built_in">strlen</span>(name);</span><br><span class="line">    <span class="keyword">asm</span> <span class="title function_">volatile</span> <span class="params">(</span></span><br><span class="line"><span class="params">        <span class="string">&quot;int %1;&quot;</span></span></span><br><span class="line"><span class="params">        : <span class="string">&quot;=a&quot;</span> (ret)</span></span><br><span class="line"><span class="params">        : <span class="string">&quot;i&quot;</span> (T_SYSCALL), <span class="string">&quot;0&quot;</span> (SYS_exec), <span class="string">&quot;d&quot;</span> (name), <span class="string">&quot;c&quot;</span> (len), <span class="string">&quot;b&quot;</span> (binary), <span class="string">&quot;D&quot;</span> (size)</span></span><br><span class="line"><span class="params">        : <span class="string">&quot;memory&quot;</span>)</span>;</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>SYS_exec</code> 就是调用 <code>do_execv</code> 函数了，用户进程代码在这个函数里使用 <code>load_icode</code> 被装载，并且设置好入口的地址。</p><p>等到 <code>do_execv</code> 执行完毕后，<code>trapframe</code> 内容的 <code>eip</code> 已经被设置为用户代码的入口地址，从中断 <code>iret</code> 返回后，<code>eip</code> 就会被设置为用户进程的第一条代码了。</p><p>编译用户进程代码的时候，可以看到连接脚本如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Simple linker script for ucore user-level programs.</span></span><br><span class="line"><span class="comment">   See the GNU ld &#x27;info&#x27; manual (&quot;info ld&quot;) to learn the syntax. */</span></span><br><span class="line"></span><br><span class="line">OUTPUT_FORMAT(<span class="string">&quot;elf32-i386&quot;</span>, <span class="string">&quot;elf32-i386&quot;</span>, <span class="string">&quot;elf32-i386&quot;</span>)</span><br><span class="line">OUTPUT_ARCH(i386)</span><br><span class="line">ENTRY(_start)</span><br><span class="line"></span><br><span class="line">SECTIONS &#123;</span><br><span class="line">    <span class="comment">/* Load programs at this address: &quot;.&quot; means the current address */</span></span><br><span class="line">    . = <span class="number">0x800020</span>;</span><br><span class="line"></span><br><span class="line">    .text : &#123;</span><br><span class="line">        *(.text .stub .text.* .gnu.linkonce.t.*)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PROVIDE(etext = .); <span class="comment">/* Define the &#x27;etext&#x27; symbol to this value */</span></span><br><span class="line"></span><br><span class="line">    .rodata : &#123;</span><br><span class="line">        *(.rodata .rodata.* .gnu.linkonce.r.*)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Adjust the address for the data segment to the next page */</span></span><br><span class="line">    . = ALIGN(<span class="number">0x1000</span>);</span><br><span class="line"></span><br><span class="line">    .data : &#123;</span><br><span class="line">        *(.data)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PROVIDE(edata = .);</span><br><span class="line"></span><br><span class="line">    .bss : &#123;</span><br><span class="line">        *(.bss)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    PROVIDE(end = .);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Place debugging symbols so that they can be found by</span></span><br><span class="line"><span class="comment">     * the kernel debugger.</span></span><br><span class="line"><span class="comment">     * Specifically, the four words at 0x200000 mark the beginning of</span></span><br><span class="line"><span class="comment">     * the stabs, the end of the stabs, the beginning of the stabs</span></span><br><span class="line"><span class="comment">     * string table, and the end of the stabs string table, respectively.</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line"></span><br><span class="line">    .stab_info <span class="number">0x200000</span> : &#123;</span><br><span class="line">        LONG(__STAB_BEGIN__);</span><br><span class="line">        LONG(__STAB_END__);</span><br><span class="line">        LONG(__STABSTR_BEGIN__);</span><br><span class="line">        LONG(__STABSTR_END__);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    .stab : &#123;</span><br><span class="line">        __STAB_BEGIN__ = DEFINED(__STAB_BEGIN__) ? __STAB_BEGIN__ : .;</span><br><span class="line">        *(.stab);</span><br><span class="line">        __STAB_END__ = DEFINED(__STAB_END__) ? __STAB_END__ : .;</span><br><span class="line">        BYTE(<span class="number">0</span>)     <span class="comment">/* Force the linker to allocate space</span></span><br><span class="line"><span class="comment">                   for this section */</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    .stabstr : &#123;</span><br><span class="line">        __STABSTR_BEGIN__ = DEFINED(__STABSTR_BEGIN__) ? __STABSTR_BEGIN__ : .;</span><br><span class="line">        *(.stabstr);</span><br><span class="line">        __STABSTR_END__ = DEFINED(__STABSTR_END__) ? __STABSTR_END__ : .;</span><br><span class="line">        BYTE(<span class="number">0</span>)     <span class="comment">/* Force the linker to allocate space</span></span><br><span class="line"><span class="comment">                   for this section */</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    /DISCARD/ : &#123;</span><br><span class="line">        *(.eh_frame .note.GNU-<span class="built_in">stack</span> .comment)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也就是说，用户进程都是加载到 <code>0x800020</code> 这个虚拟内存地址，而入口函数为 <code>_start</code>，这个是系统提供的函数，主要负责调用 <code>umain</code> 函数：</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">.text</span></span><br><span class="line">.globl _start</span><br><span class="line"><span class="symbol">_start:</span></span><br><span class="line">    <span class="comment"># set ebp for backtrace</span></span><br><span class="line">    movl $<span class="number">0x0</span>, %ebp</span><br><span class="line"></span><br><span class="line">    <span class="comment"># move down the esp register</span></span><br><span class="line">    <span class="comment"># since it may cause page fault in backtrace</span></span><br><span class="line">    <span class="keyword">subl$0x20, </span>%esp</span><br><span class="line"></span><br><span class="line">    <span class="comment"># call user-program function</span></span><br><span class="line">    call umain</span><br><span class="line"><span class="number">1</span>:  <span class="keyword">jmp </span><span class="number">1</span>b</span><br></pre></td></tr></table></figure><p>而 <code>umain</code> 则负责调用用户提供的 <code>main</code> 函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;ulib.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">umain</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">int</span> ret = main();</span><br><span class="line">    <span class="built_in">exit</span>(ret);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>到这里，进程进入用户的 <code>main</code> 函数，用户的代码便开始执行了。</p><h3 id="练习-2-父进程复制自己的内存空间给子进程"><a href="#练习-2-父进程复制自己的内存空间给子进程" class="headerlink" title="练习 2 父进程复制自己的内存空间给子进程"></a>练习 2 父进程复制自己的内存空间给子进程</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">copy_range</span><span class="params">(<span class="type">pde_t</span> *to, <span class="type">pde_t</span> *from, <span class="type">uintptr_t</span> start, <span class="type">uintptr_t</span> end, <span class="type">bool</span> share)</span> &#123;</span><br><span class="line">    assert(start % PGSIZE == <span class="number">0</span> &amp;&amp; end % PGSIZE == <span class="number">0</span>);</span><br><span class="line">    assert(USER_ACCESS(start, end));</span><br><span class="line">    <span class="comment">// copy content by page unit.</span></span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        <span class="comment">//call get_pte to find process A&#x27;s pte according to the addr start</span></span><br><span class="line">        <span class="type">pte_t</span> *ptep = get_pte(from, start, <span class="number">0</span>), *nptep;</span><br><span class="line">        <span class="keyword">if</span> (ptep == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            start = ROUNDDOWN(start + PTSIZE, PTSIZE);</span><br><span class="line">            <span class="keyword">continue</span> ;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//call get_pte to find process B&#x27;s pte according to the addr start. If pte is NULL, just alloc a PT</span></span><br><span class="line">        <span class="keyword">if</span> (*ptep &amp; PTE_P) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((nptep = get_pte(to, start, <span class="number">1</span>)) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> -E_NO_MEM;</span><br><span class="line">            &#125;</span><br><span class="line">        <span class="type">uint32_t</span> perm = (*ptep &amp; PTE_USER);</span><br><span class="line">        <span class="comment">//get page from ptep</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span> =</span> pte2page(*ptep);</span><br><span class="line">        <span class="comment">// alloc a page for process B</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">npage</span>=</span>alloc_page();</span><br><span class="line">        assert(page!=<span class="literal">NULL</span>);</span><br><span class="line">        assert(npage!=<span class="literal">NULL</span>);</span><br><span class="line">        <span class="type">int</span> ret=<span class="number">0</span>;</span><br><span class="line">        <span class="comment">/* LAB5:EXERCISE2 YOUR CODE</span></span><br><span class="line"><span class="comment">         * replicate content of page to npage, build the map of phy addr of nage with the linear addr start</span></span><br><span class="line"><span class="comment">         *</span></span><br><span class="line"><span class="comment">         * Some Useful MACROs and DEFINEs, you can use them in below implementation.</span></span><br><span class="line"><span class="comment">         * MACROs or Functions:</span></span><br><span class="line"><span class="comment">         *    page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)</span></span><br><span class="line"><span class="comment">         *    page_insert: build the map of phy addr of an Page with the linear addr la</span></span><br><span class="line"><span class="comment">         *    memcpy: typical memory copy function</span></span><br><span class="line"><span class="comment">         *</span></span><br><span class="line"><span class="comment">         * (1) find src_kvaddr: the kernel virtual address of page</span></span><br><span class="line"><span class="comment">         * (2) find dst_kvaddr: the kernel virtual address of npage</span></span><br><span class="line"><span class="comment">         * (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE</span></span><br><span class="line"><span class="comment">         * (4) build the map of phy addr of  nage with the linear addr start</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="type">void</span> *src_kvaddr = page2kva(page);</span><br><span class="line">        <span class="type">void</span> *dst_kvaddr = page2kva(npage);</span><br><span class="line">        <span class="built_in">memcpy</span>(dst_kvaddr, src_kvaddr, PGSIZE);</span><br><span class="line">        ret = page_insert(to, npage, start, perm);</span><br><span class="line">        assert(ret == <span class="number">0</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        start += PGSIZE;</span><br><span class="line">    &#125; <span class="keyword">while</span> (start != <span class="number">0</span> &amp;&amp; start &lt; end);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>copy_range</code> 中，最终完成了父进程的内存空间拷贝到子进程中的工作，实现思路比较简单。参数的 <code>to</code> 是子进程的页目录表，<code>from</code> 则是父进程的页目录表，<code>start</code> 是空间开始的虚拟地址，<code>end</code> 是空间结束的虚拟地址。在这个函数中，主要就是遍历每一个父进程中的页表项，然后为子进程分配新页，使用 <code>memcpy</code> 函数复制内存，最后插入到子进程的页表中。</p><p>如果要实现 <code>Copy On Write</code> 机制，则直接省去拷贝的操作，直接将父进程的页表项插入到子进程中。但是需要将 <code>PTE_W</code> 清零。这样，在两个进程访问到共享页面的时候，则会触发 <code>Page Fault</code>，而处理页面异常的例程发现虽然页表项不可写，但是所在的虚拟内存空间可写的话，就知道这是一次 <code>Copy On Write</code> 操作，到时候再进行复制即可。</p><h3 id="练习-3-阅读分析源代码，理解进程执行-fork-exec-wait-exit-的实现，以及系统调用的实现"><a href="#练习-3-阅读分析源代码，理解进程执行-fork-exec-wait-exit-的实现，以及系统调用的实现" class="headerlink" title="练习 3 阅读分析源代码，理解进程执行 fork&#x2F;exec&#x2F;wait&#x2F;exit 的实现，以及系统调用的实现"></a>练习 3 阅读分析源代码，理解进程执行 fork&#x2F;exec&#x2F;wait&#x2F;exit 的实现，以及系统调用的实现</h3><h4 id="fork-的实现"><a href="#fork-的实现" class="headerlink" title="fork 的实现"></a><code>fork</code> 的实现</h4><p><code>fork</code> 调用后最终会调用 <code>do_fork</code> 系统调用，子进程在其中被 <code>wakeup_proc</code> 函数唤醒，成为 <code>PROC_RUNNABLE</code> 态。</p><h4 id="exec-的实现"><a href="#exec-的实现" class="headerlink" title="exec 的实现"></a><code>exec</code> 的实现</h4><p><code>exec</code> 调用后最终会调用 <code>do_execve</code> 系统调用，此时进程被加载的用户程序完全替换，并且中断返回地址被设置成了用户程序的入口地址，但不会影响进程状态。</p><h4 id="wait-的实现"><a href="#wait-的实现" class="headerlink" title="wait 的实现"></a><code>wait</code> 的实现</h4><p><code>wait</code> 调用后最终会调用 <code>do_wait</code> 系统调用，在 <code>do_wait</code> 函数中，如果调用的时候指定的 <code>pid</code> 不为 0，则等待指定的子进程，如果调用的时候指定的 <code>pid</code> 是 0，则等待所有的子进程。</p><p><code>pid</code> 不为 0 的时候，系统会检查当前进程是不是需要等待的父进程，然后检查子进程是否已经执行完毕成为 <code>PROC_ZOMBIE</code> 状态，如果是则对其进行清理，否则等待这个子进程执行完毕。</p><p><code>pid</code> 为 0 的时候，系统会遍历当前进程所有的子进程，如果有任何一个执行完变为 <code>PROC_ZOMBIE</code> 状态，就清理掉。</p><p>在发现还没有执行完的子进程的时候，这个函数会把当前运行的进程设置为 <code>PROC_SLEEPING</code> 状态，并且等待状态设置为 <code>WT_CHILD</code>，并调用 <code>schedule</code>  函数，调度使子进程继续执行。</p><p>这个函数每次只会清理一个子进程，如果发现没有子进程了就会返回 <code>E_BAD_PROC</code>。所以需要在 <code>while</code> 循环中反复调用这个函数。</p><h4 id="exit-的实现"><a href="#exit-的实现" class="headerlink" title="exit 的实现"></a><code>exit</code> 的实现</h4><p><code>exit</code> 调用后最终会调用 <code>do_exit</code> 系统调用，在 <code>do_exit</code> 函数中，首先会清理进程所占用的内存，然后将当前进程设置为 <code>PROC_ZOMBIE</code> 态，然后查看当前进程的父进程是不是在 <code>WT_CHILD</code> 状态，是的话就唤醒这个父进程来清除自己。</p><p>对于这个进程而言，有可能有子进程还没有被清理，此时这些子进程都会被 <code>init_proc</code> 接管，并由 <code>init_proc</code> 负责清理。</p><p>ucore 中进程状态切换示意图如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">  alloc_proc                              RUNNING</span><br><span class="line">      +                                 +--&lt;----&lt;--+</span><br><span class="line">      +                                 + proc_run +</span><br><span class="line">      V                                 +--&gt;----&gt;--+ </span><br><span class="line">PROC_UNINIT - proc_init/wakeup_proc --&gt; PROC_RUNNABLE - try_free_pages/do_wait/do_sleep --&gt; PROC_SLEEPING -</span><br><span class="line">                                           ^      +                                                       +</span><br><span class="line">                                           |      +--- do_exit --&gt; PROC_ZOMBIE                            +</span><br><span class="line">                                           +                                                              + </span><br><span class="line">                                           -----------------------wakeup_proc------------------------------</span><br></pre></td></tr></table></figure><h3 id="实验结果"><a href="#实验结果" class="headerlink" title="实验结果"></a>实验结果</h3><p><a href="/university/ucore/ucore-os-lab-5/Screenshot_20190601_133606.png" data-fancybox="gallery" data-caption="Screenshot_20190601_133606"><img src="/university/ucore/ucore-os-lab-5/Screenshot_20190601_133606.png" alt="Screenshot_20190601_133606"></a></p><h3 id="扩展练习-Challenge-实现-Copy-On-Write-机制"><a href="#扩展练习-Challenge-实现-Copy-On-Write-机制" class="headerlink" title="扩展练习 Challenge 实现 Copy On Write 机制"></a>扩展练习 Challenge 实现 <code>Copy On Write</code> 机制</h3><p>在 <code>copy_range</code> 中，判断父子进程是否共享内存：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (share) &#123;</span><br><span class="line">    <span class="keyword">if</span>(*ptep &amp; PTE_W)&#123;</span><br><span class="line">        perm &amp;= (~PTE_W);</span><br><span class="line">        page_insert(from, page, start, perm);</span><br><span class="line">    &#125;</span><br><span class="line">    ret = page_insert(to, page, start, perm);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="type">void</span> *src_kvaddr = page2kva(page);</span><br><span class="line">    <span class="type">void</span> *dst_kvaddr = page2kva(npage);</span><br><span class="line">    <span class="built_in">memcpy</span>(dst_kvaddr, src_kvaddr, PGSIZE);</span><br><span class="line">    ret = page_insert(to, npage, start, perm);  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 COW 复制中，我们简单地插入同一个页表项即可，而且需要将可写标志位去掉。</p><p>然后，在 <code>do_pgfault</code> 函数中，添加对 COW 的处理：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span>=</span><span class="literal">NULL</span>, *npage=<span class="literal">NULL</span>;</span><br><span class="line"><span class="type">bool</span> COW = vma-&gt;vm_flags &amp; VM_WRITE;</span><br><span class="line"><span class="keyword">if</span> (COW) &#123;</span><br><span class="line">    npage = alloc_page();</span><br><span class="line">    <span class="keyword">if</span> (!npage) <span class="keyword">goto</span> failed;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (*ptep &amp; PTE_P) &#123;</span><br><span class="line">    <span class="comment">// COW</span></span><br><span class="line">    page = pte2page(*ptep);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">// 需要换入页面</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (COW) &#123;</span><br><span class="line">    <span class="keyword">if</span> (page_ref(page) &gt; <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="comment">// Copy</span></span><br><span class="line">        <span class="built_in">memcpy</span>(page2kva(npage), page2kva(page), PGSIZE);</span><br><span class="line">        <span class="comment">// page_ref_dec(page);</span></span><br><span class="line">        page = npage, npage = <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    page_insert(mm-&gt;pgdir, page, addr, perm);</span><br><span class="line">    swap_map_swappable(mm, addr, page, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (npage) &#123;</span><br><span class="line">        <span class="comment">// 说明只有一个进程在共享这个页面，不需要复制了</span></span><br><span class="line">        free_page(npage);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将 <code>dup_mmap</code> 中的 <code>share = 0</code> 改为 <code>share = 1</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">dup_mmap</span><span class="params">(<span class="keyword">struct</span> mm_struct *to, <span class="keyword">struct</span> mm_struct *from)</span> &#123;</span><br><span class="line">    assert(to != <span class="literal">NULL</span> &amp;&amp; from != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="type">list_entry_t</span> *<span class="built_in">list</span> = &amp;(from-&gt;mmap_list), *le = <span class="built_in">list</span>;</span><br><span class="line">    <span class="keyword">while</span> ((le = list_prev(le)) != <span class="built_in">list</span>) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">vma_struct</span> *<span class="title">vma</span>, *<span class="title">nvma</span>;</span></span><br><span class="line">        vma = le2vma(le, list_link);</span><br><span class="line">        nvma = vma_create(vma-&gt;vm_start, vma-&gt;vm_end, vma-&gt;vm_flags);</span><br><span class="line">        <span class="keyword">if</span> (nvma == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> -E_NO_MEM;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        insert_vma_struct(to, nvma);</span><br><span class="line"></span><br><span class="line">        <span class="type">bool</span> share = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> (copy_range(to-&gt;pgdir, from-&gt;pgdir, vma-&gt;vm_start, vma-&gt;vm_end, share) != <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> -E_NO_MEM;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终执行一下 <code>make run-forktest</code> 和 <code>make run-forktree</code> 查看 <code>fork</code> 功能是否正常：</p><p><a href="/university/ucore/ucore-os-lab-5/Screenshot_20190601_144910.png" data-fancybox="gallery" data-caption="Screenshot_20190601_144910"><img src="/university/ucore/ucore-os-lab-5/Screenshot_20190601_144910.png" alt="Screenshot_20190601_144910"></a></p><p><a href="/university/ucore/ucore-os-lab-5/Screenshot_20190601_144928.png" data-fancybox="gallery" data-caption="Screenshot_20190601_144928"><img src="/university/ucore/ucore-os-lab-5/Screenshot_20190601_144928.png" alt="Screenshot_20190601_144928"></a></p><p>可以看到功能正常。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 4 内核线程管理</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-4.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-分配并初始化一个进程控制块"><a href="#练习-1-分配并初始化一个进程控制块" class="headerlink" title="练习 1 分配并初始化一个进程控制块"></a>练习 1 分配并初始化一个进程控制块</h3><figure]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Sat, 29 Jun 2019 22:42:24 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-分配并初始化一个进程控制块"><a href="#练习-1-分配并初始化一个进程控制块" class="headerlink" title="练习 1 分配并初始化一个进程控制块"></a>练习 1 分配并初始化一个进程控制块</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">struct</span> proc_struct *</span><br><span class="line"><span class="title function_">alloc_proc</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span> =</span> kmalloc(<span class="keyword">sizeof</span>(<span class="keyword">struct</span> proc_struct));</span><br><span class="line">    <span class="keyword">if</span> (proc != <span class="literal">NULL</span>) &#123;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">     * below fields in proc_struct need to be initialized</span></span><br><span class="line"><span class="comment">     * enum proc_state state;                      // Process state</span></span><br><span class="line"><span class="comment">     * int pid;                                    // Process ID</span></span><br><span class="line"><span class="comment">     * int runs;                                   // the running times of Proces</span></span><br><span class="line"><span class="comment">     * uintptr_t kstack;                           // Process kernel stack</span></span><br><span class="line"><span class="comment">     * volatile bool need_resched;                 // bool value: need to be rescheduled to release CPU?</span></span><br><span class="line"><span class="comment">     * struct proc_struct *parent;                 // the parent process</span></span><br><span class="line"><span class="comment">     * struct mm_struct *mm;                       // Process&#x27;s memory management field</span></span><br><span class="line"><span class="comment">     * struct context context;                     // Switch here to run process</span></span><br><span class="line"><span class="comment">     * struct trapframe *tf;                       // Trap frame for current interrupt</span></span><br><span class="line"><span class="comment">     * uintptr_t cr3;                              // CR3 register: the base addr of Page Directroy              *                                                Table(PDT)</span></span><br><span class="line"><span class="comment">     * uint32_t flags;                             // Process flag</span></span><br><span class="line"><span class="comment">     * char name[PROC_NAME_LEN + 1];               // Process name</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">        proc-&gt;state = PROC_UNINIT;</span><br><span class="line">        proc-&gt;pid = <span class="number">-1</span>;</span><br><span class="line">        proc-&gt;runs = <span class="number">0</span>;</span><br><span class="line">        proc-&gt;kstack = <span class="number">0</span>;</span><br><span class="line">        proc-&gt;need_resched = <span class="number">0</span>;</span><br><span class="line">        proc-&gt;parent = <span class="literal">NULL</span>;</span><br><span class="line">        proc-&gt;mm = <span class="literal">NULL</span>;</span><br><span class="line">        <span class="built_in">memset</span>(&amp;(proc-&gt;context), <span class="number">0</span>, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> context));</span><br><span class="line">        proc-&gt;tf = <span class="literal">NULL</span>;</span><br><span class="line">        proc-&gt;cr3 = boot_cr3;</span><br><span class="line">        proc-&gt;flags = <span class="number">0</span>;</span><br><span class="line">        <span class="built_in">memset</span>(proc-&gt;name, <span class="number">0</span>, PROC_NAME_LEN);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> proc;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>proc_alloc</code> 中，需要做的只是找到一小块内存记录进程信息，然后用默认值初始化这块内存，因为一开始什么都没有确定，所以大部分默认值是 0，<code>pid</code> 则初始化为 -1 表示还没有分配 <code>pid</code>，<code>state</code> 初始化为 <code>PROC_UNINIT</code> 表示进程还没有被初始化，而页目录表地址则初始化为内核的页目录表，表示该线程在内核态中运行。这个函数只是相当于一个默认构造函数。</p><ul><li><p>请说明 <code>proc_struct</code> 中 <code>struct context context</code> 和<code>struct trapframe *tf</code> 成员变量含义和在本实验中的作用是啥？</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Saved registers for kernel context switches.</span></span><br><span class="line"><span class="comment">// Don&#x27;t need to save all the %fs etc. segment registers,</span></span><br><span class="line"><span class="comment">// because they are constant across kernel contexts.</span></span><br><span class="line"><span class="comment">// Save all the regular registers so we don&#x27;t need to care</span></span><br><span class="line"><span class="comment">// which are caller save, but not the return register %eax.</span></span><br><span class="line"><span class="comment">// (Not saving %eax just simplifies the switching code.)</span></span><br><span class="line"><span class="comment">// The layout of context must match code in switch.S.</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">context</span> &#123;</span></span><br><span class="line">    <span class="type">uint32_t</span> eip;</span><br><span class="line">    <span class="type">uint32_t</span> esp;</span><br><span class="line">    <span class="type">uint32_t</span> ebx;</span><br><span class="line">    <span class="type">uint32_t</span> ecx;</span><br><span class="line">    <span class="type">uint32_t</span> edx;</span><br><span class="line">    <span class="type">uint32_t</span> esi;</span><br><span class="line">    <span class="type">uint32_t</span> edi;</span><br><span class="line">    <span class="type">uint32_t</span> ebp;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p><code>struct context</code> 中用于记录进程执行的上下文，也就是进程执行中的寄存器状态，在进程切换的时候会保存和恢复这些值。主要是在内核态切换到内核态的进程调度的时候使用。</p><p>而 <code>tf</code> 指向的是中断帧的指针，指向内核栈的某个位置，用来在进程执行中发生中断时保存中断现场用，例如在用户态中断到核心态的时候。</p></li></ul><h3 id="练习-2-为新创建的内核线程分配资源"><a href="#练习-2-为新创建的内核线程分配资源" class="headerlink" title="练习 2 为新创建的内核线程分配资源"></a>练习 2 为新创建的内核线程分配资源</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">do_fork</span><span class="params">(<span class="type">uint32_t</span> clone_flags, <span class="type">uintptr_t</span> <span class="built_in">stack</span>, <span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    <span class="type">int</span> ret = -E_NO_FREE_PROC;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span>;</span></span><br><span class="line">    <span class="keyword">if</span> (nr_process &gt;= MAX_PROCESS) &#123;</span><br><span class="line">        <span class="keyword">goto</span> fork_out;</span><br><span class="line">    &#125;</span><br><span class="line">    ret = -E_NO_MEM;</span><br><span class="line">    <span class="comment">//    1. call alloc_proc to allocate a proc_struct</span></span><br><span class="line">    proc = alloc_proc();</span><br><span class="line">    <span class="keyword">if</span> (!proc) &#123;</span><br><span class="line">        <span class="keyword">goto</span> fork_out;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    proc-&gt;parent = current;</span><br><span class="line">    <span class="comment">//    2. call setup_kstack to allocate a kernel stack for child process</span></span><br><span class="line">    <span class="keyword">if</span> (setup_kstack(proc) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_fork_cleanup_kstack;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//    3. call copy_mm to dup OR share mm according clone_flag</span></span><br><span class="line">    <span class="keyword">if</span> (copy_mm(clone_flags, proc) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad_fork_cleanup_kstack;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//    4. call copy_thread to setup tf &amp; context in proc_struct</span></span><br><span class="line">    copy_thread(proc, <span class="built_in">stack</span>, tf);</span><br><span class="line">    <span class="comment">//    5. insert proc_struct into hash_list &amp;&amp; proc_list</span></span><br><span class="line">    <span class="type">bool</span> intr_flag;</span><br><span class="line">        </span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">        proc-&gt;pid = get_pid();</span><br><span class="line">        list_add(&amp;proc_list, &amp;proc-&gt;list_link);</span><br><span class="line">        hash_proc(proc);</span><br><span class="line">        ++nr_process;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">    <span class="comment">//    6. call wakeup_proc to make the new child process RUNNABLE</span></span><br><span class="line">    wakeup_proc(proc);</span><br><span class="line">    <span class="comment">//    7. set ret vaule using child proc&#x27;s pid</span></span><br><span class="line">    ret = proc-&gt;pid;</span><br><span class="line">fork_out:</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line"></span><br><span class="line">bad_fork_cleanup_kstack:</span><br><span class="line">    put_kstack(proc);</span><br><span class="line">bad_fork_cleanup_proc:</span><br><span class="line">    kfree(proc);</span><br><span class="line">    <span class="keyword">goto</span> fork_out;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>alloc_proc</code> 函数中，ucore 只是为 PCB 分配了一块内存并做了简单的初始化，所以创建内核线程的时候一般通过 <code>do_fork</code> 来分配一个新的进程并设置相关的资源。这个函数主要做以下操作：</p><ol><li>调用 <code>alloc_proc</code> 分配足够的内存给 PCB，并将新的 <code>proc</code> 的 <code>parent</code> 指针指向当前正在运行的进程。</li><li>调用 <code>setup_kstack</code> 为子进程设置好内核栈。</li><li>调用 <code>copy_mm</code> 将父进程的内存空间信息复制到子进程中，本实验暂时没有用到。</li><li>调用 <code>copy_thread</code> 将父进程的上下文和栈复制到子进程中。</li><li>将该进程信息添加到进程列表中。</li><li>调用 <code>wakeup_proc</code> 唤醒新进程。</li><li>将返回值设置为新进程的 <code>pid</code>。</li></ol><ul><li><p>请说明 ucore 是否做到给每个新 <code>fork</code> 的线程一个唯一的 id？请说明你的分析和理由。</p><p>查看 <code>get_pid</code> 的函数定义：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line"><span class="title function_">get_pid</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">static_assert</span>(MAX_PID &gt; MAX_PROCESS);</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">proc</span>;</span></span><br><span class="line">    <span class="type">list_entry_t</span> *<span class="built_in">list</span> = &amp;proc_list, *le;</span><br><span class="line">    <span class="type">static</span> <span class="type">int</span> next_safe = MAX_PID, last_pid = MAX_PID;</span><br><span class="line">    <span class="keyword">if</span> (++ last_pid &gt;= MAX_PID) &#123;</span><br><span class="line">        last_pid = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">goto</span> inside;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (last_pid &gt;= next_safe) &#123;</span><br><span class="line">    inside:</span><br><span class="line">        next_safe = MAX_PID;</span><br><span class="line">    repeat:</span><br><span class="line">        le = <span class="built_in">list</span>;</span><br><span class="line">        <span class="keyword">while</span> ((le = list_next(le)) != <span class="built_in">list</span>) &#123;</span><br><span class="line">            proc = le2proc(le, list_link);</span><br><span class="line">            <span class="keyword">if</span> (proc-&gt;pid == last_pid) &#123;</span><br><span class="line">                <span class="keyword">if</span> (++ last_pid &gt;= next_safe) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (last_pid &gt;= MAX_PID) &#123;</span><br><span class="line">                        last_pid = <span class="number">1</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                    next_safe = MAX_PID;</span><br><span class="line">                    <span class="keyword">goto</span> repeat;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (proc-&gt;pid &gt; last_pid &amp;&amp; next_safe &gt; proc-&gt;pid) &#123;</span><br><span class="line">                next_safe = proc-&gt;pid;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> last_pid;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，这个函数在变量 <code>last_pid</code> 中记录了最后一次分配的 <code>pid</code>，而 <code>next_safe</code> 记录了 <code>pid</code> 的安全值。每次尝试获取新的 <code>pid</code> 的时候，就会先看看把最后一次分配的 <code>pid</code> 加 1 是不是安全的，如果不行，就遍历进程列表，确保新分配的 <code>pid</code> 是安全的。</p><p>同时，在 <code>do_fork</code> 调用 <code>get_pid</code> 的时候，会关闭中断，确保 <code>get_pid</code> 是原子操作，所以 ucore 做到了给每个新 <code>fork</code> 的线程一个唯一的 id。</p></li></ul><h3 id="练习-3-分析代码-proc-run-函数"><a href="#练习-3-分析代码-proc-run-函数" class="headerlink" title="练习 3 分析代码 proc_run 函数"></a>练习 3 分析代码 <code>proc_run</code> 函数</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">proc_run</span><span class="params">(<span class="keyword">struct</span> proc_struct *proc)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (proc != current) &#123;</span><br><span class="line">        <span class="type">bool</span> intr_flag;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">prev</span> =</span> current, *next = proc;</span><br><span class="line">        local_intr_save(intr_flag);</span><br><span class="line">        &#123;</span><br><span class="line">            current = proc;</span><br><span class="line">            load_esp0(next-&gt;kstack + KSTACKSIZE);</span><br><span class="line">            lcr3(next-&gt;cr3);</span><br><span class="line">            switch_to(&amp;(prev-&gt;context), &amp;(next-&gt;context));</span><br><span class="line">        &#125;</span><br><span class="line">        local_intr_restore(intr_flag);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>proc_run</code> 的作用是运行一个进程，如果要运行的进程和现在执行的一样，那就什么都不用做，否则的话，就把下一个要执行的进程的内核栈、页目录表加载进来，然后调用 <code>switch_to</code> 切换上下文。</p><p><code>switch_to</code> 是一个汇编代码写的函数：</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">.text</span></span><br><span class="line">.globl <span class="keyword">switch_to</span></span><br><span class="line"><span class="keyword"></span><span class="keyword">switch_to: </span>                     <span class="comment"># switch_to(from, to)</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># save from&#x27;s registers</span></span><br><span class="line">    movl <span class="number">4</span>(%esp), %eax          <span class="comment"># eax points to from</span></span><br><span class="line">    popl <span class="number">0</span>(%eax)                <span class="comment"># save eip !popl</span></span><br><span class="line">    movl %esp, <span class="number">4</span>(%eax)          <span class="comment"># save esp::context of from</span></span><br><span class="line">    movl %ebx, <span class="number">8</span>(%eax)          <span class="comment"># save ebx::context of from</span></span><br><span class="line">    movl %ecx, <span class="number">12</span>(%eax)         <span class="comment"># save ecx::context of from</span></span><br><span class="line">    movl %edx, <span class="number">16</span>(%eax)         <span class="comment"># save edx::context of from</span></span><br><span class="line">    movl %esi, <span class="number">20</span>(%eax)         <span class="comment"># save esi::context of from</span></span><br><span class="line">    movl %edi, <span class="number">24</span>(%eax)         <span class="comment"># save edi::context of from</span></span><br><span class="line">    movl %ebp, <span class="number">28</span>(%eax)         <span class="comment"># save ebp::context of from</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># restore to&#x27;s registers</span></span><br><span class="line">    movl <span class="number">4</span>(%esp), %eax          <span class="comment"># not 8(%esp): popped return address already</span></span><br><span class="line">                                <span class="comment"># eax now points to to</span></span><br><span class="line">    movl <span class="number">28</span>(%eax), %ebp         <span class="comment"># restore ebp::context of to</span></span><br><span class="line">    movl <span class="number">24</span>(%eax), %edi         <span class="comment"># restore edi::context of to</span></span><br><span class="line">    movl <span class="number">20</span>(%eax), %esi         <span class="comment"># restore esi::context of to</span></span><br><span class="line">    movl <span class="number">16</span>(%eax), %edx         <span class="comment"># restore edx::context of to</span></span><br><span class="line">    movl <span class="number">12</span>(%eax), %ecx         <span class="comment"># restore ecx::context of to</span></span><br><span class="line">    movl <span class="number">8</span>(%eax), %ebx          <span class="comment"># restore ebx::context of to</span></span><br><span class="line">    movl <span class="number">4</span>(%eax), %esp          <span class="comment"># restore esp::context of to</span></span><br><span class="line"></span><br><span class="line">    pushl <span class="number">0</span>(%eax)               <span class="comment"># push eip</span></span><br><span class="line"></span><br><span class="line">    ret</span><br></pre></td></tr></table></figure><p>在调用这个函数的时候，栈的结构如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     |  高位地址</span><br><span class="line"> |       to        |</span><br><span class="line"> |      from       |</span><br><span class="line"> |    返回地址     |  &lt;-------- esp</span><br></pre></td></tr></table></figure><p>此时，<code>from</code> 在 <code>esp + 4</code> 处，存放指向 <code>prev-&gt;context</code> 的指针，然后按照 <code>struct context</code> 的结构依次保存现场，需要注意的是，在 <code>popl 0(%eax)</code> 之后，栈的结构变成了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     |  高位地址</span><br><span class="line"> |       to        |</span><br><span class="line"> |      from       |  &lt;-------- esp</span><br></pre></td></tr></table></figure><p>所以，现在 <code>esp + 4</code> 处存放的就是 <code>next-&gt;context</code> 的指针了。此时读取 <code>context</code> 中的上下文，恢复进程的现场。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     |  高位地址</span><br><span class="line"> |       to        |</span><br><span class="line"> |      from       |  </span><br><span class="line"> |   新进程 eip    |  &lt;-------- esp</span><br></pre></td></tr></table></figure><p>最后，通过将 <code>context</code> 中的 <code>eip</code> 压栈，然后调用 <code>ret</code> 指令，使指令从新进程的现场继续执行。</p><p>在这次实验中，<code>initproc</code> 的 <code>eip</code> 在 <code>copy_thread</code> 中被设置成了 <code>forkret</code>： </p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">copy_thread</span><span class="params">(<span class="keyword">struct</span> proc_struct *proc, <span class="type">uintptr_t</span> esp, <span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    proc-&gt;tf = (<span class="keyword">struct</span> trapframe *)(proc-&gt;kstack + KSTACKSIZE) - <span class="number">1</span>;</span><br><span class="line">    *(proc-&gt;tf) = *tf;</span><br><span class="line">    proc-&gt;tf-&gt;tf_regs.reg_eax = <span class="number">0</span>;</span><br><span class="line">    proc-&gt;tf-&gt;tf_esp = esp;</span><br><span class="line">    proc-&gt;tf-&gt;tf_eflags |= FL_IF;</span><br><span class="line"></span><br><span class="line">    proc-&gt;context.eip = (<span class="type">uintptr_t</span>)forkret;</span><br><span class="line">    proc-&gt;context.esp = (<span class="type">uintptr_t</span>)(proc-&gt;tf);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在本实验的执行过程中，创建且运行了几个内核线程？</p><p>在 <code>proc_init</code> 中，只有两个内核线程被创建：<code>idleproc</code> 和 <code>initproc</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">proc_init</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line"></span><br><span class="line">    list_init(&amp;proc_list);</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; HASH_LIST_SIZE; i ++) &#123;</span><br><span class="line">        list_init(hash_list + i);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((idleproc = alloc_proc()) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        panic(<span class="string">&quot;cannot alloc idleproc.\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    idleproc-&gt;pid = <span class="number">0</span>;</span><br><span class="line">    idleproc-&gt;state = PROC_RUNNABLE;</span><br><span class="line">    idleproc-&gt;kstack = (<span class="type">uintptr_t</span>)bootstack;</span><br><span class="line">    idleproc-&gt;need_resched = <span class="number">1</span>;</span><br><span class="line">    set_proc_name(idleproc, <span class="string">&quot;idle&quot;</span>);</span><br><span class="line">    nr_process ++;</span><br><span class="line"></span><br><span class="line">    current = idleproc;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> pid = kernel_thread(init_main, <span class="string">&quot;Hello world!!&quot;</span>, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (pid &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">        panic(<span class="string">&quot;create init_main failed.\n&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    initproc = find_proc(pid);</span><br><span class="line">    set_proc_name(initproc, <span class="string">&quot;init&quot;</span>);</span><br><span class="line"></span><br><span class="line">    assert(idleproc != <span class="literal">NULL</span> &amp;&amp; idleproc-&gt;pid == <span class="number">0</span>);</span><br><span class="line">    assert(initproc != <span class="literal">NULL</span> &amp;&amp; initproc-&gt;pid == <span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在其他初始化完成之后，ucore 就会调用 <code>cpu_idle</code> 函数，这个函数是一个死循环，如果发现当前的内核线程让出了 CPU，就调用 <code>schedule</code> 函数进行进程调度。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">schedule</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">bool</span> intr_flag;</span><br><span class="line">    <span class="type">list_entry_t</span> *le, *last;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proc_struct</span> *<span class="title">next</span> =</span> <span class="literal">NULL</span>;</span><br><span class="line">    local_intr_save(intr_flag);</span><br><span class="line">    &#123;</span><br><span class="line">        current-&gt;need_resched = <span class="number">0</span>;</span><br><span class="line">        last = (current == idleproc) ? &amp;proc_list : &amp;(current-&gt;list_link);</span><br><span class="line">        le = last;</span><br><span class="line">        <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> ((le = list_next(le)) != &amp;proc_list) &#123;</span><br><span class="line">                next = le2proc(le, list_link);</span><br><span class="line">                <span class="keyword">if</span> (next-&gt;state == PROC_RUNNABLE) &#123;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">while</span> (le != last);</span><br><span class="line">        <span class="keyword">if</span> (next == <span class="literal">NULL</span> || next-&gt;state != PROC_RUNNABLE) &#123;</span><br><span class="line">            next = idleproc;</span><br><span class="line">        &#125;</span><br><span class="line">        next-&gt;runs ++;</span><br><span class="line">        <span class="keyword">if</span> (next != current) &#123;</span><br><span class="line">            proc_run(next);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    local_intr_restore(intr_flag);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这次实验里，一开始是 <code>idleproc</code> 在运行，而且自己的 <code>resched</code> 置位，所以调度器会马上被唤醒，调度到 <code>initproc</code> 中，而在 <code>kernel_thread</code> 函数中，设置了 <code>initproc</code> 运行的参数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">kernel_thread</span><span class="params">(<span class="type">int</span> (*fn)(<span class="type">void</span> *), <span class="type">void</span> *arg, <span class="type">uint32_t</span> clone_flags)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> <span class="title">tf</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;tf, <span class="number">0</span>, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> trapframe));</span><br><span class="line">    tf.tf_cs = KERNEL_CS;</span><br><span class="line">    tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;</span><br><span class="line">    tf.tf_regs.reg_ebx = (<span class="type">uint32_t</span>)fn;</span><br><span class="line">    tf.tf_regs.reg_edx = (<span class="type">uint32_t</span>)arg;</span><br><span class="line">    tf.tf_eip = (<span class="type">uint32_t</span>)kernel_thread_entry;</span><br><span class="line">    <span class="keyword">return</span> do_fork(clone_flags | CLONE_VM, <span class="number">0</span>, &amp;tf);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>initproc</code> 的中断栈帧的 <code>eip</code> 寄存器被设置成了 <code>kernel_thread_entry</code>，<code>ebx</code> 中保存着真正需要调用的函数地址，<code>edx</code> 中保存着函数调用的参数。</p><p>而在 <code>do_fork</code> 的 <code>copy_thread</code> 函数中：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">copy_thread</span><span class="params">(<span class="keyword">struct</span> proc_struct *proc, <span class="type">uintptr_t</span> esp, <span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    proc-&gt;tf = (<span class="keyword">struct</span> trapframe *)(proc-&gt;kstack + KSTACKSIZE) - <span class="number">1</span>;</span><br><span class="line">    *(proc-&gt;tf) = *tf;</span><br><span class="line">    proc-&gt;tf-&gt;tf_regs.reg_eax = <span class="number">0</span>;</span><br><span class="line">    proc-&gt;tf-&gt;tf_esp = esp;</span><br><span class="line">    proc-&gt;tf-&gt;tf_eflags |= FL_IF;</span><br><span class="line"></span><br><span class="line">    proc-&gt;context.eip = (<span class="type">uintptr_t</span>)forkret;</span><br><span class="line">    proc-&gt;context.esp = (<span class="type">uintptr_t</span>)(proc-&gt;tf);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到内核线程的真正入口地址是 <code>forkret</code>，所以在 <code>switch_to</code> 调用之后，会跳转到 <code>forkret</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">forkret</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    forkrets(current-&gt;tf);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是所有新进程的入口地址，<code>forkrets</code> 是汇编函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">.globl __trapret</span><br><span class="line">__trapret:</span><br><span class="line">    <span class="meta"># restore registers from stack</span></span><br><span class="line">    popal</span><br><span class="line"></span><br><span class="line">    <span class="meta"># restore %ds, %es, %fs and %gs</span></span><br><span class="line">    popl %gs</span><br><span class="line">    popl %fs</span><br><span class="line">    popl %es</span><br><span class="line">    popl %ds</span><br><span class="line"></span><br><span class="line">    <span class="meta"># get rid of the trap number and <span class="keyword">error</span> code</span></span><br><span class="line">    addl $<span class="number">0x8</span>, %esp</span><br><span class="line">    iret</span><br><span class="line"></span><br><span class="line">.globl forkrets</span><br><span class="line">forkrets:</span><br><span class="line">    <span class="meta"># set stack to this new process<span class="string">&#x27;s trapframe</span></span></span><br><span class="line"><span class="string"><span class="meta">    movl 4(%esp), %esp</span></span></span><br><span class="line"><span class="string"><span class="meta">    jmp __trapret</span></span></span><br></pre></td></tr></table></figure><p>这里简单地将之前设置的 <code>trapframe</code> 的值写入到寄存器中，并调用 <code>iret</code> 返回（因为只有 <code>iret</code> 可以修改段寄存器的值），而此时就会进入 <code>kernel_thread_entry</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">.text</span><br><span class="line">.globl kernel_thread_entry</span><br><span class="line">kernel_thread_entry:        <span class="meta"># void kernel_thread(void)</span></span><br><span class="line"></span><br><span class="line">    pushl %edx              <span class="meta"># push arg</span></span><br><span class="line">    call *%ebx              <span class="meta"># call fn</span></span><br><span class="line"></span><br><span class="line">    pushl %eax              <span class="meta"># save the return value of fn(arg)</span></span><br><span class="line">    call do_exit            <span class="meta"># call do_exit to terminate current thread</span></span><br></pre></td></tr></table></figure><p>这里就是调用线程函数，<code>fn(arg)</code>，将返回值保存到栈上，最后调用 <code>do_exit</code> 退出进程：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">do_exit</span><span class="params">(<span class="type">int</span> error_code)</span> &#123;</span><br><span class="line">    panic(<span class="string">&quot;process exit!!.\n&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实验执行到这里就结束了。</p></li><li><p>语句 <code>local_intr_save(intr_flag); ...; local_intr_restore(intr_flag);</code> 在这里有何作用？请说明理由。</p><p>在准备切换进程的时候，需要重新设置栈和页表，并且需要切换进程上下文，如果不关中断，进程信息设置到一半的时候很可能会被中断打断，导致寄存器状态处于一个不一致的状态，造成程序运行出错。</p></li></ul><h3 id="运行结果"><a href="#运行结果" class="headerlink" title="运行结果"></a>运行结果</h3><p><a href="/university/ucore/ucore-os-lab-4/Screenshot_20190517_001039.png" data-fancybox="gallery" data-caption="Screenshot_20190517_001039"><img src="/university/ucore/ucore-os-lab-4/Screenshot_20190517_001039.png" alt="Screenshot_20190517_001039"></a><a href="/university/ucore/ucore-os-lab-4/Screenshot_20190517_001357.png" data-fancybox="gallery" data-caption="Screenshot_20190517_001357"><img src="/university/ucore/ucore-os-lab-4/Screenshot_20190517_001357.png" alt="Screenshot_20190517_001357"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 3 虚拟内存管理</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-3.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-给未被映射的地址映射上物理页"><a href="#练习-1-给未被映射的地址映射上物理页" class="headerlink" title="练习 1 给未被映射的地址映射上物理页"></a>练习 1]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Thu, 16 May 2019 01:21:39 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-给未被映射的地址映射上物理页"><a href="#练习-1-给未被映射的地址映射上物理页" class="headerlink" title="练习 1 给未被映射的地址映射上物理页"></a>练习 1 给未被映射的地址映射上物理页</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span></span><br><span class="line"><span class="title function_">do_pgfault</span><span class="params">(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uint32_t</span> error_code, <span class="type">uintptr_t</span> addr)</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="type">pte_t</span> *ptep=<span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!(ptep = get_pte(mm-&gt;pgdir, addr, <span class="number">1</span>))) <span class="keyword">goto</span> failed;            <span class="comment">//(1) try to find a pte, if pte&#x27;s PT(Page Table) isn&#x27;t existed, then create a PT.</span></span><br><span class="line">    <span class="keyword">if</span> (*ptep == <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!pgdir_alloc_page(mm-&gt;pgdir, addr, perm)) <span class="keyword">goto</span> failed;                    <span class="comment">//(2) if the phy addr isn&#x27;t exist, then alloc a page &amp; map the phy addr with logical addr</span></span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// Exercise 2</span></span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">   ret = <span class="number">0</span>;</span><br><span class="line">failed:</span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>启动分页机制以后，当程序尝试访问一个不在物理内存中的页帧或是访问权限有错的时候，会触发 CPU 的页面异常（Page Fault），进入中断处理程序。<code>do_pgfault</code> 的作用就是负责处理页面错误。</p><p>练习 1 处理的是地址还没有映射上物理页的情况。在这种情况下，只需要调用 Lab 2 中负责物理内存管理的函数即可。<code>pgdir_alloc_page</code> 就是用来给这个空地址映射上物理页的。</p><ul><li><p>请描述页目录项（Page Directory Entry）和页表项（Page Table Entry）中每个组成部分的含义以及对 ucore 而言的潜在用处。</p><p><a href="/university/ucore/ucore-os-lab-3/TIM%E6%88%AA%E5%9B%BE20190411202013.png" data-fancybox="gallery" data-caption="TIM截图20190411202013"><img src="/university/ucore/ucore-os-lab-3/TIM%E6%88%AA%E5%9B%BE20190411202013.png" alt="TIM截图20190411202013"></a></p><p>参考 Intel 官方手册，可以知道在页大小为 4KB 每页的时候，PDE 和 PTE 的高 20 位是物理基址，9-11 位为空闲位，操作系统可以自由使用。低八位为标志位，此时 PDE 与 PTE 的标志位含义有所不同，但它们的低三位都是一样的，分别用于权限控制，读写控制，以及标志该项是否有效。</p></li><li><p>如果 ucore 缺页服务例程中访问内存，出现了页访问异常，请问硬件要做哪些事情？</p><ol><li><p>将错误码压入栈中，表明此次页访问异常的具体错误，错误码具体格式如下：</p><p><a href="/university/ucore/ucore-os-lab-3/TIM%E6%88%AA%E5%9B%BE20190411205326.png" data-fancybox="gallery" data-caption="TIM截图20190411205326"><img src="/university/ucore/ucore-os-lab-3/TIM%E6%88%AA%E5%9B%BE20190411205326.png" alt="TIM截图20190411205326"></a></p></li><li><p>将访问异常的线性地址放到 <code>CR2</code> 寄存器中。</p></li><li><p>触发 14 号中断，将控制权移交中断服务例程。</p></li></ol></li></ul><h3 id="练习-2-补充完成基于-FIFO-的页面替换算法"><a href="#练习-2-补充完成基于-FIFO-的页面替换算法" class="headerlink" title="练习 2 补充完成基于 FIFO 的页面替换算法"></a>练习 2 补充完成基于 FIFO 的页面替换算法</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(swap_init_ok) &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span>=</span><span class="literal">NULL</span>;</span><br><span class="line">    swap_in(mm, addr, &amp;page);                        <span class="comment">//(1）According to the mm AND addr, try to load the content of right disk page</span></span><br><span class="line">    <span class="comment">//    into the memory which page managed.</span></span><br><span class="line">    page_insert(mm-&gt;pgdir, page, addr, perm);                        <span class="comment">//(2) According to the mm, addr AND page, setup the map of phy addr &lt;---&gt; logical addr</span></span><br><span class="line">    swap_map_swappable(mm, addr, page, <span class="number">1</span>);                        <span class="comment">//(3) make the page swappable.</span></span><br><span class="line">    page-&gt;pra_vaddr = addr;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    cprintf(<span class="string">&quot;no swap_init_ok but ptep is %x, failed\n&quot;</span>,*ptep);</span><br><span class="line">    <span class="keyword">goto</span> failed;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先把被换出的页面换入内存中，然后将该页重新插入页表项里，标识该页为可换出页，最后设置该页用于页置换算法的虚拟地址为当前访问的地址。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line">_fifo_map_swappable(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uintptr_t</span> addr, <span class="keyword">struct</span> Page *page, <span class="type">int</span> swap_in)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">list_entry_t</span> *head=(<span class="type">list_entry_t</span>*) mm-&gt;sm_priv;</span><br><span class="line">    <span class="type">list_entry_t</span> *entry=&amp;(page-&gt;pra_page_link);</span><br><span class="line"> </span><br><span class="line">    assert(entry != <span class="literal">NULL</span> &amp;&amp; head != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="comment">//record the page access situlation</span></span><br><span class="line">    <span class="comment">/*LAB3 EXERCISE 2: YOUR CODE*/</span> </span><br><span class="line">    <span class="comment">//(1)link the most recent arrival page at the back of the pra_list_head qeueue.</span></span><br><span class="line">    list_add_before(head, entry);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 FIFO 页面替换算法中，每当有新的一页可以用于交换，说明该页刚刚被换入，则将其插入到访问队列的队尾中。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span></span><br><span class="line">_fifo_swap_out_victim(<span class="keyword">struct</span> mm_struct *mm, <span class="keyword">struct</span> Page ** ptr_page, <span class="type">int</span> in_tick)</span><br><span class="line">&#123;</span><br><span class="line">     <span class="type">list_entry_t</span> *head=(<span class="type">list_entry_t</span>*) mm-&gt;sm_priv;</span><br><span class="line">         assert(head != <span class="literal">NULL</span>);</span><br><span class="line">     assert(in_tick==<span class="number">0</span>);</span><br><span class="line">     <span class="comment">/* Select the victim */</span></span><br><span class="line">     <span class="comment">/*LAB3 EXERCISE 2: YOUR CODE*/</span> </span><br><span class="line">     <span class="comment">//(1)  unlink the  earliest arrival page in front of pra_list_head qeueue</span></span><br><span class="line">     <span class="type">list_entry_t</span> *first = head-&gt;next;</span><br><span class="line">     <span class="comment">//(2)  assign the value of *ptr_page to the addr of this page</span></span><br><span class="line">     <span class="class"><span class="keyword">struct</span> <span class="title">Page</span>* <span class="title">page</span> =</span> le2page(first, pra_page_link);</span><br><span class="line">     *ptr_page = page;</span><br><span class="line">     list_del(first);</span><br><span class="line">     <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在需要换出页面的时候，按照 FIFO 算法，队首的页面是最早被换入的，所以直接将队首的元素删除，代表队首的页面被换出。</p><ul><li>如果要在 ucore 上实现 “Extended Clock 页替换算法”，现有的 <code>swap_manager</code> 框架是否足以支持在 ucore 中实现此算法？如果是，请给你的设计方案。如果不是，请给出你的新的扩展和基于此扩展的设计方案。并需要回答如下问题：<ul><li><p>需要被换出的页的特征是什么？</p><p>对于每个页面都有两个标志位，分别为使用位 A 和修改位 D。换出页的使用位必须为 0，表示该页在之前一段时间未被使用。并且算法优先考虑换出修改位为零的页面，以免频繁地将内存中的物理页写入磁盘而增大开销。</p></li><li><p>在 ucore 中如何判断具有这样特征的页？</p><p>当内存页被访问后，MMU 将在对应的页表项的 <code>PTE_A</code> 这一位设为1，当内存页被修改后，MMU 将在对应的页表项的 <code>PTE_D</code> 这一位设为1。</p></li><li><p>何时进行换入和换出操作？</p><p>当进程访问的物理页没有在内存中缓存而是保存在磁盘中时，需要进行换入操作； 当位于物理页中的内存被页面替换算法选择换出时，需要进行换出操作。</p></li></ul></li></ul><p>运行结果：<a href="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183622.png" data-fancybox="gallery" data-caption="Screenshot_20190506_183622"><img src="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183622.png" alt="Screenshot_20190506_183622"></a><a href="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183602.png" data-fancybox="gallery" data-caption="Screenshot_20190506_183602"><img src="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183602.png" alt="Screenshot_20190506_183602"></a></p><h3 id="扩展练习-Challenge-1-实现识别-Dirty-Bit-的-Extended-Clock-页替换算法"><a href="#扩展练习-Challenge-1-实现识别-Dirty-Bit-的-Extended-Clock-页替换算法" class="headerlink" title="扩展练习 Challenge 1 实现识别 Dirty Bit 的 Extended Clock 页替换算法"></a>扩展练习 Challenge 1 实现识别 Dirty Bit 的 Extended Clock 页替换算法</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_init_mm(<span class="keyword">struct</span> mm_struct *mm) &#123;</span><br><span class="line">    mm-&gt;sm_priv = <span class="literal">NULL</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_map_swappable(<span class="keyword">struct</span> mm_struct *mm, <span class="type">uintptr_t</span> addr,</span><br><span class="line">                                <span class="keyword">struct</span> Page *page, <span class="type">int</span> tick) &#123;</span><br><span class="line">    <span class="type">list_entry_t</span> *head = (<span class="type">list_entry_t</span> *)mm-&gt;sm_priv;</span><br><span class="line">    <span class="type">list_entry_t</span> *entry = &amp;(page-&gt;pra_page_link);</span><br><span class="line">    assert(entry != <span class="literal">NULL</span>);</span><br><span class="line">    <span class="keyword">if</span> (head == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        list_init(entry);</span><br><span class="line">        mm-&gt;sm_priv = entry;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        list_add_before(head, entry);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> _clock_swap_out_victim(<span class="keyword">struct</span> mm_struct *mm, <span class="keyword">struct</span> Page **ptr_page,</span><br><span class="line">                                  <span class="type">int</span> in_tick) &#123;</span><br><span class="line">    <span class="type">list_entry_t</span> *head = (<span class="type">list_entry_t</span> *)mm-&gt;sm_priv;</span><br><span class="line">    <span class="type">list_entry_t</span> *p = head, *victim = <span class="literal">NULL</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        <span class="type">pte_t</span> *ptep =</span><br><span class="line">            get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">        <span class="comment">// not accessed and not dirty</span></span><br><span class="line">        <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; !(*ptep &amp; PTE_D)) &#123;</span><br><span class="line">            victim = p;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        p = list_next(p);</span><br><span class="line">    &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; (*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            *ptep &amp;= ~PTE_A;</span><br><span class="line">            tlb_invalidate(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr);</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and not dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; !(*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!victim) <span class="keyword">do</span> &#123;</span><br><span class="line">            <span class="type">pte_t</span> *ptep =</span><br><span class="line">                get_pte(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr, <span class="number">0</span>);</span><br><span class="line">            <span class="comment">// not accessed and dirty</span></span><br><span class="line">            <span class="keyword">if</span> (!(*ptep &amp; PTE_A) &amp;&amp; (*ptep &amp; PTE_D)) &#123;</span><br><span class="line">                victim = p;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            *ptep &amp;= ~PTE_A;</span><br><span class="line">            tlb_invalidate(mm-&gt;pgdir, le2page(p, pra_page_link)-&gt;pra_vaddr);</span><br><span class="line"></span><br><span class="line">            p = list_next(p);</span><br><span class="line">        &#125; <span class="keyword">while</span> (p != head);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (list_empty(victim)) &#123;</span><br><span class="line">        mm-&gt;sm_priv = <span class="literal">NULL</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        mm-&gt;sm_priv = list_next(victim);</span><br><span class="line">        list_del(victim);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    *ptr_page = le2page(victim, pra_page_link);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在普通的 Clock PRA 算法里，只考虑了页面是否被访问，但是没有考虑页面是否脏页，显然脏页换出的代价比非脏页更大，因此在 Extended Clock PRA 里，还要优先选择不是脏页的页面。</p><p>在 Extended Clock PRA 中，插入和 FIFO PRA 一样，直接插入到链表尾部。在选择需要被换出的页面的时候，从上一次被换出页的下一页开始扫描，需要至多四趟扫描：</p><ol><li>寻找没被访问 (<code>PTE_A == 0</code>) 而且没被修改的非脏页 (<code>PTE_D == 0</code>)，其中 <code>PTE_A</code> 和 <code>PTE_D</code> 都是 CPU 硬件置位的。</li><li>寻找没被访问 (<code>PTE_A == 0</code>) 的脏页 (<code>PTE_D == 1</code>)，并且将途中扫描到的页面的 <code>PTE_A</code> 都修改为 0</li><li>重复 1</li><li>重复 2</li></ol><p>运行结果：</p><p><a href="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183507.png" data-fancybox="gallery" data-caption="Screenshot_20190506_183507"><img src="/university/ucore/ucore-os-lab-3/Screenshot_20190506_183507.png" alt="Screenshot_20190506_183507"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 2 物理内存管理</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-2.html</link>
      <description>
        <![CDATA[<h3 id="练习-1-实现-First-Fit-连续物理内存分配算法"><a href="#练习-1-实现-First-Fit-连续物理内存分配算法" class="headerlink" title="练习 1 实现 First-Fit]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Thu, 16 May 2019 01:18:28 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1-实现-First-Fit-连续物理内存分配算法"><a href="#练习-1-实现-First-Fit-连续物理内存分配算法" class="headerlink" title="练习 1 实现 First-Fit 连续物理内存分配算法"></a>练习 1 实现 First-Fit 连续物理内存分配算法</h3><p>First-Fit 连续物理内存分配算法实现比较简单。操作系统维护一个空闲页的链表，链表项根据对应地址从小到大进行排序。链表中的每一项包含的信息是<strong>连续空闲空间的第一页</strong>，同时 <code>struct Page</code> 中的 <code>property</code> 在 First-Fit 算法中意味着该连续空闲空间共有多少页。而在 <code>flags</code> 中，<code>property</code> 被置位的话，意味着这一页是<strong>连续空闲空间的第一页</strong>，<code>Reserved</code> 表示该页被操作系统保留，不能进行分配或者释放。在内存探测完成之后，所有的页都被保留了，需要在初始化函数中将其清零，否则不能被其他程序使用。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">Page</span> &#123;</span></span><br><span class="line">    <span class="type">int</span> ref;                        <span class="comment">// page frame&#x27;s reference counter</span></span><br><span class="line">    <span class="type">uint32_t</span> flags;                 <span class="comment">// array of flags that describe the status of the page frame</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> property;          <span class="comment">// the num of free block, used in first fit pm manager</span></span><br><span class="line">    <span class="type">list_entry_t</span> page_link;         <span class="comment">// free list link</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>为了便于管理内存，定义 <code>free_area_t</code> 结构如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">list_entry_t</span> free_list;         <span class="comment">// the list header</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> nr_free;           <span class="comment">// # of free pages in this free list</span></span><br><span class="line">&#125; <span class="type">free_area_t</span>;</span><br></pre></td></tr></table></figure><p>其中 <code>free_list</code> 是空闲链表的头，用来遍历空闲链表，本身不是一个页，<code>nr_free</code> 表示了总共有多少页是空闲的。</p><p>了解了各个字段表明的含义之后，就可以编写函数实现 First-Fit 物理内存分配算法了。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">default_init</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    list_init(&amp;free_list);</span><br><span class="line">    nr_free = <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首相是 <code>default_init</code> 函数，<code>free_list</code> 是空闲页链表的头，调用 <code>list_init</code> 对其进行初始化，并将 <code>nr_free</code> 置为 <code>0</code> ，表示还没有空闲页纳入管理。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">default_init_memmap</span><span class="params">(<span class="keyword">struct</span> Page *base, <span class="type">size_t</span> n)</span> &#123;</span><br><span class="line">    assert(n &gt; <span class="number">0</span>);</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">p</span> =</span> base;</span><br><span class="line">    <span class="keyword">for</span> (; p != base + n; p ++) &#123;</span><br><span class="line">        assert(PageReserved(p));</span><br><span class="line">        p-&gt;flags = p-&gt;property = <span class="number">0</span>;</span><br><span class="line">        set_page_ref(p, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    base-&gt;property = n;</span><br><span class="line">    SetPageProperty(base);</span><br><span class="line">    nr_free += n;</span><br><span class="line">    list_add_before(&amp;free_list, &amp;(base-&gt;page_link));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>default_init_memmap</code> 函数的作用是，将以 <code>base</code> 开始连续的 <code>n</code> 个页加入到空闲链表中。首先需要确保每一页都是被内核保留的，然后将每一页的 <code>flags</code> 清零，表示该页可以被程序使用且不是连续空闲物理内存的第一页。最后将引用计数置为 <code>0</code>，表示还没有程序引用。然后将 <code>base</code> 的 <code>property</code> 设置为 <code>n</code>  并置 <code>flags</code> 中的 <code>property</code> 位，表示该页是连续空闲物理内存中的第一页。</p><p>最后，将 <code>nr_free</code> 加 <code>n</code> ，表示新增 <code>n</code> 页空闲页可用。由于函数是按照地址从小到大调用的，所以需要将基址加入到空闲链表的最后一项中。至此就完成了 <code>default_init_memmap</code> 的初始化工作。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">struct</span> Page *</span><br><span class="line"><span class="title function_">default_alloc_pages</span><span class="params">(<span class="type">size_t</span> n)</span> &#123;</span><br><span class="line">    assert(n &gt; <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> (n &gt; nr_free) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span> =</span> <span class="literal">NULL</span>;</span><br><span class="line">    <span class="type">list_entry_t</span> *le = &amp;free_list;</span><br><span class="line">    <span class="keyword">while</span> ((le = list_next(le)) != &amp;free_list) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">p</span> =</span> le2page(le, page_link);</span><br><span class="line">        <span class="keyword">if</span> (p-&gt;property &gt;= n) &#123;</span><br><span class="line">            page = p;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (page != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (page-&gt;property &gt; n) &#123;</span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">p</span> =</span> page + n;</span><br><span class="line">            p-&gt;property = page-&gt;property - n;</span><br><span class="line">            SetPageProperty(p);</span><br><span class="line">            list_add_after(&amp;(page-&gt;page_link), &amp;(p-&gt;page_link));</span><br><span class="line">        &#125;</span><br><span class="line">        nr_free -= n;</span><br><span class="line">        ClearPageProperty(page);</span><br><span class="line">        list_del(&amp;(page-&gt;page_link));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> page;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在分配页面时，需要首先检查剩余的空闲页是否够用，如果不够则分配失败，返回空指针。</p><p>然后，根据 First-Fit 算法，从地址小到大遍历每一个空闲块，一旦找到了足够大的空闲块就马上分配，也就是分配第一个足够大的空闲块给程序使用。找不到，说明没有这么大的连续空闲块，则返回空指针。</p><p>如果找到了这样的空闲块，还要检查空闲块的大小是否比需要分配的大小更大，如果是，则需要进行分裂。分裂的操作便是将空闲块的后面的空闲块重新形成空闲块，这个空闲块的第一个页面是 <code>page + n</code>，大小是 <code>page-&gt;property - n</code>，设置好 <code>property</code> 和 <code>flags</code> 中的 <code>property</code> 位后，在链表中加入到当前空闲块的后面（因为地址更大）。</p><p>最后将当前空闲块从空闲链表中删除，<code>property</code> 位清零，表明该页不是空闲块的第一页，并空闲块计数器减去 <code>n</code>，即完成分配，返回页表项指针。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">default_free_pages</span><span class="params">(<span class="keyword">struct</span> Page *base, <span class="type">size_t</span> n)</span> &#123;</span><br><span class="line">    assert(n &gt; <span class="number">0</span>);</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">p</span> =</span> base;</span><br><span class="line">    <span class="keyword">for</span> (; p != base + n; p ++) &#123;</span><br><span class="line">        assert(!PageReserved(p) &amp;&amp; !PageProperty(p));</span><br><span class="line">        p-&gt;flags = <span class="number">0</span>;</span><br><span class="line">        set_page_ref(p, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    base-&gt;property = n;</span><br><span class="line">    SetPageProperty(base);</span><br><span class="line">    <span class="type">list_entry_t</span> *le = list_next(&amp;free_list);</span><br><span class="line">    <span class="keyword">while</span> (le != &amp;free_list) &#123;</span><br><span class="line">        p = le2page(le, page_link);         <span class="comment">// 检查链表中的每一个空闲块 p</span></span><br><span class="line">        le = list_next(le);</span><br><span class="line">        <span class="keyword">if</span> (base + base-&gt;property == p) &#123;   <span class="comment">// 被释放的空闲块后面紧接着空闲块 p</span></span><br><span class="line">            base-&gt;property += p-&gt;property;  <span class="comment">// 合并后面的空闲块 p</span></span><br><span class="line">            ClearPageProperty(p);</span><br><span class="line">            list_del(&amp;(p-&gt;page_link));      <span class="comment">// 释放掉后面的空闲块 p（因为已经合并）</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (p + p-&gt;property == base) &#123; <span class="comment">// 被释放的空闲块前面紧接着空闲块 p</span></span><br><span class="line">            p-&gt;property += base-&gt;property;  <span class="comment">// 合并前面的空闲块 p</span></span><br><span class="line">            ClearPageProperty(base);        </span><br><span class="line">            base = p;                       <span class="comment">// 前面的空闲块 p 成为第一页</span></span><br><span class="line">            list_del(&amp;(p-&gt;page_link));      <span class="comment">// 释放掉自己</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    nr_free += n;</span><br><span class="line">    le = list_next(&amp;free_list);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (le != &amp;free_list) &#123;</span><br><span class="line">        p = le2page(le, page_link);</span><br><span class="line">        <span class="keyword">if</span> (base + base-&gt;property &lt;= p) &#123;</span><br><span class="line">            assert(base + base-&gt;property != p);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        le = list_next(le);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    list_add_before(le, &amp;(base-&gt;page_link));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后 <code>default_free_pages</code> 负责已分配内存的释放。</p><p>首先确认每一个页面不是操作系统保留的而且不是空闲块第一页，然后清空属性。如果中途碰到了 <code>property</code> 置位的情况，说明释放的页面太多了，已经越界到下一个空闲块了。然后对于 <code>base</code> 页，重新设置对应的属性，表明其为大小为 <code>n</code> 页的空闲块的第一页，然后开始空闲块的合并。</p><p>对于被释放出来的空闲块，有三种可能：</p><ol><li>这个空闲块后面紧接着链表中的某个空闲块。</li><li>这个空闲块前面紧接着链表中的某个空闲块。</li><li>情况 1 和 2 都发生了。</li></ol><p>链表的合并过程需要遍历当前空闲链表中的每一个空闲块，然后检查这个空闲块是不是紧邻着新释放的空闲块，如果是，就合并它们。对于情况 3，程序会先将前面的空闲块合并成新的块，然后再合并后面的空闲块。</p><p>合并完成之后，由于需要维持链表有序，需要找到这个新的空闲块在链表中的插入位置。这里采用了插入排序的思想，遍历链表，找到第一个地址比需要插入的空闲块结尾地址大的空闲块，插入到它的前面即可。由于前面的过程保证了不会有空闲块越界和粘连的情况发生，所以判断条件改为 <code>base &lt;= p</code> 也是可以的。</p><p>至此，First-Fit 连续物理内存分配算法就实现完成了。</p><p>•你的 First-Fit 算法是否有进一步的改进空间？</p><p>​如果请求的内存大小不足一页，会造成空间上的浪费，这一点可以通过记录偏移和最后一次页面来改进。而且每一次操作都要进行链表的扫描，可以考虑使用哈希表来直接定位链表中的项。</p><h3 id="练习-2-实现寻找虚拟地址对应的页表项"><a href="#练习-2-实现寻找虚拟地址对应的页表项" class="headerlink" title="练习 2 实现寻找虚拟地址对应的页表项"></a>练习 2 实现寻找虚拟地址对应的页表项</h3><p>在 ucore os 中，一个一级页表（页目录）中的每一项指向二级页表（页表）的起始地址，每一个二级页表的大小为一页。因为一页大小为 $4096$ 字节，一个页表项（PTE）或者页目录项（PDE）占用 $4$ 个字节，因此每级页表都有 $1024$ 项可用，两级页表总共有 $1024 \times 1024 &#x3D; 2^{20}$ 个页可用。需要注意的是页表中存放的都是物理地址。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">pte_t</span> *</span><br><span class="line"><span class="title function_">get_pte</span><span class="params">(<span class="type">pde_t</span> *pgdir, <span class="type">uintptr_t</span> la, <span class="type">bool</span> create)</span> &#123;</span><br><span class="line">    <span class="type">pde_t</span> *pdep = pgdir + PDX(la);</span><br><span class="line">    <span class="keyword">if</span> (!(*pdep &amp; PTE_P)) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span>* <span class="title">page</span>;</span></span><br><span class="line">        <span class="keyword">if</span> (!create || (page = alloc_page()) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        set_page_ref(page, <span class="number">1</span>);</span><br><span class="line">        <span class="type">uintptr_t</span> pa = page2pa(page);</span><br><span class="line">        <span class="built_in">memset</span>(KADDR(pa), <span class="number">0</span>, PGSIZE);</span><br><span class="line">        *pdep = pa | PTE_P | PTE_U | PTE_W;</span><br><span class="line">    &#125;</span><br><span class="line">            </span><br><span class="line">    <span class="keyword">return</span>  (<span class="type">pte_t</span> *)KADDR(PDE_ADDR(*pdep)) + PTX(la);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要想获得 PTE，需要通过以下步骤实现：</p><ol><li>通过 <code>pgdir</code> 加上线性地址中的页目录偏移，得到指向页目录表项的指针 <code>pdep</code>。</li><li>检查 <code>pdep</code> 指向的表项是否有效（即指向一个有效的二级页表起始地址）</li><li>如果无效，则分配一个新的页作为新的二级页表，然后得到这个页的物理地址并转换为内核虚拟地址，调用 <code>memset</code> 将页清零。</li><li>将获得的地址填入到 PDE 中，并且设置为可读可写，该项有效。</li><li>最后获得 PDE 中的物理地址，转换为内核虚拟地址，得到二级页表的起始地址，加上 PTE 对应的偏移，就得到了真正的 PTE 地址。</li></ol><ul><li><p>请描述页目录项（Page Directory Entry）和页表项（Page Table Entry）中每个组成部分的含义以及对 ucore 而言的潜在用处。</p><p><a href="/university/ucore/ucore-os-lab-2/TIM%E6%88%AA%E5%9B%BE20190411202013.png" data-fancybox="gallery" data-caption="TIM截图20190411202013"><img src="/university/ucore/ucore-os-lab-2/TIM%E6%88%AA%E5%9B%BE20190411202013.png" alt="TIM截图20190411202013"></a></p><p>参考 Intel 官方手册，可以知道在页大小为 4KB 每页的时候，PDE 和 PTE 的高 20 位是物理基址，9-11 位为空闲位，操作系统可以自由使用。低八位为标志位，此时 PDE 与 PTE 的标志位含义有所不同，但它们的低三位都是一样的，分别用于权限控制，读写控制，以及标志该项是否有效。</p></li><li><p>如果 ucore 执行过程中访问内存，出现了页访问异常，请问硬件要做哪些事情？</p><ol><li><p>将错误码压入栈中，表明此次页访问异常的具体错误，错误码具体格式如下：</p><p><a href="/university/ucore/ucore-os-lab-2/TIM%E6%88%AA%E5%9B%BE20190411205326.png" data-fancybox="gallery" data-caption="TIM截图20190411205326"><img src="/university/ucore/ucore-os-lab-2/TIM%E6%88%AA%E5%9B%BE20190411205326.png" alt="TIM截图20190411205326"></a></p></li><li><p>将访问异常的线性地址放到 <code>CR2</code> 寄存器中。</p></li><li><p>触发 14 号中断，将控制权移交中断服务例程。</p></li></ol></li></ul><h3 id="练习-3-释放某虚地址所在的页并取消对应二级页表项的映射"><a href="#练习-3-释放某虚地址所在的页并取消对应二级页表项的映射" class="headerlink" title="练习 3 释放某虚地址所在的页并取消对应二级页表项的映射"></a>练习 3 释放某虚地址所在的页并取消对应二级页表项的映射</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">page_remove_pte</span><span class="params">(<span class="type">pde_t</span> *pgdir, <span class="type">uintptr_t</span> la, <span class="type">pte_t</span> *ptep)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (*ptep &amp; PTE_P) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">page</span> =</span> pte2page(*ptep);</span><br><span class="line">        <span class="keyword">if</span> (page_ref_dec(page) == <span class="number">0</span>) &#123;</span><br><span class="line">            free_page(page);</span><br><span class="line">        &#125;</span><br><span class="line">        *ptep = <span class="number">0</span>;</span><br><span class="line">        tlb_invalidate(pgdir, la);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>检查页表项是否有效，无效则不用处理。</li><li>获得页表项对应的页表信息，将其引用计数减一。如果页面不再被引用，则释放这一页。</li><li>将 PTE 置零，然后将对应的 TLB 缓存清除。</li></ol><ul><li><p>数据结构 <code>struct Page</code> 的全局变量 <code>pages</code>（其实是一个数组）的每一项与页表中的页目录项和页表项有无对应关系？如果有，其对应关系是啥？</p><p>有。页目录项和页表项索引组合起来就是 PFN，也就是 <code>pages</code> 数组的索引。</p></li><li><p>如果希望虚拟地址与物理地址相等，则需要如何修改 lab2，完成此事？</p><p>将开启分页这步操作先从 <code>kern/init/entry.S</code> 取消，否则在分页模式，取消掉 <code>boot_pgdir[0]</code> 的页表下 <code>kern_init</code> 会被置于一个根本无法访问到的地址。</p><p>然后：</p><ol><li>KERNBASE 调整回 0x00000000 。</li><li>链接脚本修改虚拟地址为 0x00100000 。</li></ol><p>于是这样内核就会被置于正确的位置， eip 不会跑偏。</p><p>接下来的问题是 <code>check_pgdir</code> 和 <code>check_boot_pgdir</code> 这两个检查函数会变得乱七八糟。它们都假设 KERNBASE 不为 0 ，所以会对 Linear Address 为 0 的地址为所欲为所欲为，比如查询 0 地址的页表分配、给 0 地址申请页表，对其动手动脚，写入数据等。不仅仅是威胁到内核安全，主要是页表操作失败本身就会导致断言失败。</p></li></ul><h3 id="验证测试"><a href="#验证测试" class="headerlink" title="验证测试"></a>验证测试</h3><p><a href="/university/ucore/ucore-os-lab-2/Screenshot_20190414_143810.png" data-fancybox="gallery" data-caption="Screenshot_20190414_143810"><img src="/university/ucore/ucore-os-lab-2/Screenshot_20190414_143810.png" alt="Screenshot_20190414_143810"></a></p><p>执行 <code>make qemu</code> 之后可以看到物理内存分配器成功地初始化，并且页表也成功建立完成。</p><p><a href="/university/ucore/ucore-os-lab-2/Screenshot_20190414_142702.png" data-fancybox="gallery" data-caption="Screenshot_20190414_142702"><img src="/university/ucore/ucore-os-lab-2/Screenshot_20190414_142702.png" alt="Screenshot_20190414_142702"></a></p><p>执行 <code>make grade</code> 之后可以看到打分成功了。</p><h3 id="扩展练习-伙伴系统的实现"><a href="#扩展练习-伙伴系统的实现" class="headerlink" title="扩展练习 伙伴系统的实现"></a>扩展练习 伙伴系统的实现</h3><p><em>注：本实现参考了 Linux Kernel 2.6.0 中的内存管理，由其简化而来。Linux Kernel 2.6.24 之后的伙伴系统实现为了避免碎片化增加了额外的处理。由于只是想实现一个简单的伙伴系统，所以参考了比较简单的版本。</em></p><p>Linux Kernel 2.6.0 版本实现的伙伴系统采用了通过多个空闲链表来管理不同大小的空闲块的方式，一个空闲链表的阶 <code>order</code> 指的是空闲块大小的次数。一个阶为 <code>order</code> 的空闲链表中的每一个空闲块大小都是 <code>2 ^ order</code> 页。并且，总共管理的阶数数量定义为 <code>MAX_ORDER == 11</code> ，也就是有 11 个空闲链表，最大的阶是 10。</p><p>在 Linux Kernel 2.6.0 中，伙伴系统的实现采用了每个空闲链表维护自己的 <code>bitmap</code> 的方法来标记<strong>该链表中一对伙伴</strong>的情况，一对伙伴只需要 1 位进行标记，如果该位为 0 则说明两个伙伴都没有使用或者都在使用，如果该位为 1 则说明两个伙伴只有其中之一被使用。在进行释放的时候，一个页面会有两种情况：</p><ol><li>释放前伙伴位为 0，因为自己是要被释放的页面，所以可以知道释放后另一个伙伴还在使用，不能合并。</li><li>释放前伙伴位为 1，因为自己是要被释放的页面，所以可以知道释放后另一个伙伴也是空闲的，可以和自己合并成更大的伙伴块。</li></ol><p>无论释放前情况如何，释放后都一定会改变伙伴位的值。</p><p>首先来看伙伴系统实现所需要的数据结构和一些辅助函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_ORDER 11</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MARK_USED(index, order, area) \</span></span><br><span class="line"><span class="meta">    change_bit((index) &gt;&gt; (1 + (order)), (area)-&gt;map)</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> LONG_ALIGN(x) (((x) + (sizeof(long)) - 1) &amp; ~((sizeof(long)) - 1))</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">list_entry_t</span> free_list;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> *<span class="built_in">map</span>;</span><br><span class="line">&#125; <span class="type">buddy_free_area_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">buddy_free_area_t</span> free_area[MAX_ORDER];</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> nr_free;  <span class="comment">// how many free pages</span></span><br><span class="line">&#125; <span class="type">buddy_zone_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> mem[KMEMSIZE &gt;&gt; <span class="number">15</span>];</span><br><span class="line">    <span class="type">size_t</span> offset;</span><br><span class="line">&#125; <span class="type">bitmap_allocator_t</span>;</span><br><span class="line"></span><br><span class="line"><span class="type">buddy_zone_t</span> zone;</span><br><span class="line"><span class="type">bitmap_allocator_t</span> bitmap_allocator;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">int</span> <span class="title function_">is_power_of_2</span><span class="params">(<span class="type">size_t</span> x)</span> &#123; <span class="keyword">return</span> !(x &amp; (x - <span class="number">1</span>)); &#125;</span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">int</span> <span class="title function_">log2</span><span class="params">(<span class="type">size_t</span> x)</span> &#123;</span><br><span class="line">    <span class="type">int</span> y = !is_power_of_2(x);</span><br><span class="line">    <span class="keyword">while</span> (x &gt;&gt;= <span class="number">1</span>) ++y;</span><br><span class="line">    <span class="keyword">return</span> y;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>buddy_free_area_t</code> 用于维护一个空闲链表以及它的伙伴使用情况，而 <code>buddy_zone_t</code> 用于维护所有的空闲链表和空闲页数，<code>bitmap_allocator_t</code> 用于在初始化时简单地分配 <code>bitmap</code> 内存。</p><p>而 <code>log2</code> 用于求一个整数向上取到最接近的二的次幂的次数，<code>MARK_USED</code> 用于标记空闲链表中伙伴的使用情况，<code>LONG_ALIGN</code> 则负责把需要的位数转换成需要的长整型数量，在分配 <code>bitmap</code> 的时候会使用到。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">unsigned</span> <span class="type">long</span> *<span class="title function_">alloc_bitmap</span><span class="params">(<span class="type">size_t</span> bitmap_size)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> *ptr = bitmap_allocator.mem + bitmap_allocator.offset;</span><br><span class="line">    assert(ptr &lt; bitmap_allocator.mem + <span class="keyword">sizeof</span>(bitmap_allocator.mem));</span><br><span class="line">    bitmap_allocator.offset += bitmap_size;</span><br><span class="line">    <span class="keyword">return</span> ptr;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">buddy_init</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; MAX_ORDER; ++i) &#123;</span><br><span class="line">        list_init(&amp;zone.free_area[i].free_list);</span><br><span class="line">    &#125;</span><br><span class="line">    bitmap_allocator.offset = <span class="number">0</span>;</span><br><span class="line">    zone.nr_free = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">size_t</span> size = KMEMSIZE &gt;&gt; <span class="number">12</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>;; i++) &#123;</span><br><span class="line">        <span class="type">unsigned</span> <span class="type">long</span> bitmap_size;</span><br><span class="line"></span><br><span class="line">        list_init(&amp;(zone.free_area[i].free_list));</span><br><span class="line">        <span class="keyword">if</span> (i == MAX_ORDER - <span class="number">1</span>) &#123;</span><br><span class="line">            zone.free_area[i].<span class="built_in">map</span> = <span class="literal">NULL</span>;</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        bitmap_size = (size - <span class="number">1</span>) &gt;&gt; (i + <span class="number">4</span>);</span><br><span class="line">        bitmap_size = LONG_ALIGN(bitmap_size + <span class="number">1</span>);</span><br><span class="line">        zone.free_area[i].<span class="built_in">map</span> = (<span class="type">unsigned</span> <span class="type">long</span> *)alloc_bitmap(bitmap_size);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>初始化时，首先将所有空闲链表初始化。然后为 <code>bitmap</code> 分配足够的空间，大小就是当前阶管理的内存页数的一半位，再除以 32 则得到需要的 32 位长整型的数量。</p><p>这里有一个自相矛盾的过程，由于此时还没有内存分配器，所以没有办法动态地为 <code>bitmap</code> 分配内存。有两种解决的办法：</p><ol><li>使用前面实现的 First-Fit 分配器先行分配，等伙伴系统完成初始化之后，将 First-Fit 分配器占用的内存释放掉。</li><li>直接预先使用静态变量分配足够的空间，需要的时候直接从这个空间取。</li></ol><p>为了简单起见，这里采用了第二种方法，最大需要的位数也只是 <code>KMEMSIZE &gt;&gt; 12</code>，也就是页数个比特位。再除以 32 就是需要的长整型的数量。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">buddy_init_memmap</span><span class="params">(<span class="keyword">struct</span> Page *base, <span class="type">size_t</span> n)</span> &#123;</span><br><span class="line">    assert(n &gt; <span class="number">0</span>);</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">p</span> =</span> base;</span><br><span class="line">    <span class="keyword">for</span> (; p != base + n; p++) &#123;</span><br><span class="line">        assert(PageReserved(p));</span><br><span class="line">        p-&gt;flags = p-&gt;property = <span class="number">0</span>;</span><br><span class="line">        set_page_ref(p, <span class="number">0</span>);</span><br><span class="line">        list_init(&amp;(p-&gt;page_link));</span><br><span class="line">        buddy_free_pages(p, <span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于扫描到的每一块内存，要将其加入到伙伴系统，就只要一页一页地进行释放即可。在释放页面的函数中会自动合并。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">struct</span> <span class="title class_">Page</span> *<span class="built_in">buddy_alloc_pages</span>(<span class="type">size_t</span> n) &#123;</span><br><span class="line">    <span class="type">size_t</span> order = <span class="built_in">log2</span>(n);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> min;</span><br><span class="line"></span><br><span class="line">    min = <span class="number">1UL</span> &lt;&lt; order;</span><br><span class="line">    <span class="keyword">if</span> (zone.nr_free &lt; min) <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    <span class="type">buddy_free_area_t</span> *area;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> current_order;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">Page</span> *page;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> index;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 从块的小到大遍历伙伴系统</span></span><br><span class="line">    <span class="keyword">for</span> (current_order = order; current_order &lt; MAX_ORDER; ++current_order) &#123;</span><br><span class="line">        area = zone.free_area + current_order;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">list_empty</span>(&amp;area-&gt;free_list))  <span class="comment">// 当前阶没有空闲块</span></span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">        page = <span class="built_in">le2page</span>(<span class="built_in">list_next</span>(&amp;area-&gt;free_list), page_link);  <span class="comment">// 找到了空闲块</span></span><br><span class="line">        <span class="built_in">list_del</span>(&amp;(page-&gt;page_link));  <span class="comment">// 删掉，可能会被分裂</span></span><br><span class="line">        index = page - pages;          <span class="comment">// 第几页?</span></span><br><span class="line">        <span class="keyword">if</span> (current_order != MAX_ORDER - <span class="number">1</span>)</span><br><span class="line">            <span class="built_in">MARK_USED</span>(index, current_order, area);</span><br><span class="line">        zone.nr_free -= <span class="number">1UL</span> &lt;&lt; order;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">expand</span>(page, index, order, current_order, area);  <span class="comment">// 分裂页面</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于分配过程而言，首先检查当前的可用内存是否足够，然后从需要分配的页面数对应的阶的链表开始往高阶链表查询，找到一个足够大的空闲块后，就将其从对应的空闲链表中删除。由于分配到的空闲块可能比需要的更大，所以还要对空闲块进行进一步的划分。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="keyword">struct</span> Page *<span class="title function_">expand</span><span class="params">(<span class="keyword">struct</span> Page *page, <span class="type">unsigned</span> <span class="type">long</span> index,</span></span><br><span class="line"><span class="params">                                  <span class="type">int</span> low, <span class="type">int</span> high, <span class="type">buddy_free_area_t</span> *area)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> size = <span class="number">1</span> &lt;&lt; high; <span class="comment">// 当前空闲块的大小</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (high &gt; low) &#123;</span><br><span class="line">        area--;      <span class="comment">// 往低阶链表走</span></span><br><span class="line">        high--;</span><br><span class="line">        size &gt;&gt;= <span class="number">1</span>;  <span class="comment">// 大小减半</span></span><br><span class="line">        list_add(&amp;(area-&gt;free_list), &amp;(page-&gt;page_link));</span><br><span class="line">        MARK_USED(index, high, area);  <span class="comment">// 伙伴被使用了</span></span><br><span class="line">        index += size;</span><br><span class="line">        page += size;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> page;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 <code>expand</code> 函数里，<code>size</code> 表示当前空闲块的大小，<code>index</code> 表示页号，<code>page</code> 指向当前空闲块的首页，<code>high</code> 表示当前空闲块所在的阶，<code>low</code> 表示分配需要的阶。分裂的过程如下：</p><ol><li>如果当前空闲块所在的阶比需要的阶更高，说明需要继续分裂。</li><li>将空闲链表指针指向低一阶的空闲链表，然后对应的 <code>high</code> 和 <code>size</code> 减小。</li><li>将当前的首页加入到低阶空闲链表中。</li><li>由于后面的一页是要被使用的，所以标记伙伴位表示这个分裂后的空闲块被使用了。</li><li>页号和首页地址指向分裂后的空闲块的后一半的起始地址。</li><li>回到第一步。</li></ol><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">buddy_free_pages</span><span class="params">(<span class="keyword">struct</span> Page *page, <span class="type">size_t</span> n)</span> &#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> order = log2(n);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">long</span> mask;</span><br><span class="line">    <span class="type">buddy_free_area_t</span> *area;</span><br><span class="line"></span><br><span class="line">    mask = (~<span class="number">0UL</span>) &lt;&lt; order;</span><br><span class="line">    area = zone.free_area + order;</span><br><span class="line">    <span class="keyword">if</span> (!PageReserved(page)) &#123;</span><br><span class="line">        <span class="type">size_t</span> page_idx = page - pages;</span><br><span class="line">        assert(!(page_idx &amp; ~mask));</span><br><span class="line">        assert(page_idx &lt;= (KMEMSIZE &gt;&gt; <span class="number">12</span>));</span><br><span class="line">        <span class="type">size_t</span> index = page_idx &gt;&gt; (<span class="number">1</span> + order);</span><br><span class="line"></span><br><span class="line">        zone.nr_free -= mask;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (mask + (<span class="number">1</span> &lt;&lt; (MAX_ORDER - <span class="number">1</span>))) &#123;</span><br><span class="line">            <span class="class"><span class="keyword">struct</span> <span class="title">Page</span> *<span class="title">buddy</span>;</span></span><br><span class="line"></span><br><span class="line">            assert(area &lt; zone.free_area + MAX_ORDER);</span><br><span class="line">            <span class="keyword">if</span> (!test_and_change_bit(index, area-&gt;<span class="built_in">map</span>)) <span class="keyword">break</span>;</span><br><span class="line">            buddy = pages + (page_idx ^ -mask);  <span class="comment">// 找到伙伴</span></span><br><span class="line">            list_del(&amp;(buddy-&gt;page_link));</span><br><span class="line">            mask &lt;&lt;= <span class="number">1</span>;</span><br><span class="line">            area++;</span><br><span class="line">            index &gt;&gt;= <span class="number">1</span>;</span><br><span class="line">            page_idx &amp;= mask;</span><br><span class="line">            assert(page_idx &lt;= (KMEMSIZE &gt;&gt; <span class="number">12</span>));</span><br><span class="line">        &#125;</span><br><span class="line">        list_add_before(&amp;area-&gt;free_list, &amp;(pages + page_idx)-&gt;page_link);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>释放的过程则是分配的反过程。首先需要确认释放的页面和释放的大小是否对应，然后开始释放。然后从当前阶开始递归地合并页面。合并页面主要有以下步骤：</p><ol><li><p>检查当前阶是不是最高阶，是的话表示合并结束。</p></li><li><p>检查自己的伙伴是不是空闲的，如果不是，就不能继续合并了。</p></li><li><p>如果自己的伙伴是空闲的，那么就找到当前页的伙伴。对于伙伴系统，有一个非常重要的性质，那就是伙伴之间的页号只相差一个比特位。假如说当前阶数是 <code>order</code>，说明一个空闲块大小为 <code>2^order</code> 次方，这个相差的比特位就是从低位数起的第 <code>order + 1</code> 位。也就是说，如果一个页面的页号是 <code>0x70</code>，对应二进制是 <code>0011 0000</code>，又知道它的阶数是 <code>order == 4</code>，那么它的伙伴就是 <code>0010 0000</code>，也就是 <code>0x20</code>。需要注意的是 <code>0x30</code> 的 4 阶伙伴不是 <code>0x40</code>，因为它们不一样的比特位不止一个。</p><p>利用这个性质，可以快速地找到伙伴的页号。假如知道当前阶数是 <code>order</code>，那么只要将自己的页号从低位数起的第 <code>order + 1</code> 位反转，就找到了伙伴的页号。而 <code>mask</code> 是一个低 <code>order</code> 位为 0，其余位为 1 的掩模，假如 <code>order</code> 为 4 的话，那么 <code>mask</code> 就是 <code>1111 0000</code> （省略更高位的 1），而反转第 <code>order + 1</code> 位只需要将当前页号和只有第 <code>order + 1</code> 位的为 1 的二进制数进行异或操作，这个例子里也就是 <code>0001 0000</code>。很容易就知道这个数正是 <code>mask</code> 的补码。</p><p>也就是说，要找到伙伴的页号，只需要将自己的页号和 <code>mask</code> 的补码进行异或即可。</p></li><li><p>找到伙伴之后，将它从它的空闲链表中删除，因为要和自己合并。</p></li><li><p>往高阶链表前进，假如两个伙伴在 <code>order</code> 阶，那么合并之后的页号就是任意一个伙伴页号的低 <code>order + 1</code> 阶清零得到的数。</p></li><li><p>回到第一步。</p></li></ol><p>循环终止的时候，说明已经不能再合并了，此时 <code>page_idx</code> 存放着合并完成的空闲块的首页页号，而 <code>area</code> 也指向了当前空闲块对应阶数的空闲链表，此时只要将对应的首页加入到空闲链表，就完成了释放。</p><p><a href="/university/ucore/ucore-os-lab-2/Screenshot_20190414_191737.png" data-fancybox="gallery" data-caption="Screenshot_20190414_191737"><img src="/university/ucore/ucore-os-lab-2/Screenshot_20190414_191737.png" alt="Screenshot_20190414_191737"></a></p><p>执行 <code>make qemu</code> 可以看到伙伴系统中自由链表的情况。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>ucore OS | 实验 1 ucore 的启动过程</title>
      <link>https://blog.howardlau.me/university/ucore/ucore-os-lab-1.html</link>
      <description>
        <![CDATA[<h3 id="练习-1"><a href="#练习-1" class="headerlink" title="练习 1"></a>练习 1</h3><ol>
<li><p>操作系统镜像文件 <code>ucore.img</code>]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/university/">大学学习</category>
      <category domain="https://blog.howardlau.me/category/university/ucore/">ucore</category>
      <pubDate>Thu, 16 May 2019 01:15:11 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h3 id="练习-1"><a href="#练习-1" class="headerlink" title="练习 1"></a>练习 1</h3><ol><li><p>操作系统镜像文件 <code>ucore.img</code> 是如何一步一步生成的？</p><p>通过执行命令 <code>make V=&quot;&quot;</code>，使 <code>make</code> 显示出编译过程中执行的命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">+ cc kern/init/init.c</span><br><span class="line">gcc -Ikern/init/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o</span><br><span class="line">// 省略相似命令……</span><br><span class="line">+ ld bin/kernel</span><br><span class="line">ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o  obj/libs/string.o obj/libs/printfmt.o</span><br><span class="line">+ cc boot/bootasm.S</span><br><span class="line">gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o</span><br><span class="line">// 省略相似命令……</span><br><span class="line">gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign</span><br><span class="line">+ ld bin/bootblock</span><br><span class="line">ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o</span><br><span class="line"><span class="string">&#x27;obj/bootblock.out&#x27;</span> size: 504 bytes</span><br><span class="line">build 512 bytes boot sector: <span class="string">&#x27;bin/bootblock&#x27;</span> success!</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/zero of=bin/ucore.img count=10000</span><br><span class="line">10000+0 records <span class="keyword">in</span></span><br><span class="line">10000+0 records out</span><br><span class="line">5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0156548 s, 327 MB/s</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=bin/bootblock of=bin/ucore.img conv=notrunc</span><br><span class="line">1+0 records <span class="keyword">in</span></span><br><span class="line">1+0 records out</span><br><span class="line">512 bytes copied, 7.1747e-05 s, 7.1 MB/s</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=bin/kernel of=bin/ucore.img seek=1 conv=notrunc</span><br><span class="line">154+1 records <span class="keyword">in</span></span><br><span class="line">154+1 records out</span><br><span class="line">79036 bytes (79 kB, 77 KiB) copied, 0.000255015 s, 310 MB/s</span><br></pre></td></tr></table></figure><p>可以看出，生成镜像主要有以下步骤：</p><ol><li><p>使用 <code>gcc</code> 编译 <code>kern</code> 目录和 <code>boot</code> 下的所有 <code>.c</code> C 语言文件和 <code>.S</code> 汇编代码，可以看到比起平时的编译多出了很多选项：</p><ul><li><code>-fno-builtin</code>：表示防止 <code>gcc</code> 使用自带的内置函数，比如用到的 <code>strcpy</code>，如果没有这个选项，<code>gcc</code> 会跳过我们的代码，使用它自带的 <code>strcpy</code> 函数。</li><li><code>-nostdinc</code>：表示不要在系统自带的标准库的目录下搜索包含文件，只在 <code>-I</code> 指定的目录下搜索，这也是为了防止我们自定义的 <code>strcpy</code> 之类的标准库函数和系统自带的产生冲突。</li><li><code>-fno-PIC</code>：表示不要生成 PIC (Position Independent Code)。经过查资料得知，生成 PIC 会使代码体积变大，在 <a href="https://github.com/chyyuu/ucore_os_lab/pull/22">https://github.com/chyyuu/ucore_os_lab/pull/22</a> 可以知道，由于新版本 <code>gcc</code> 默认启用 PIC，所以会导致生成的 <code>bootloader</code> 大于 512 字节，无法放进一个扇区中。并且， PIC 需要 <code>bootloader</code> 正确处理重定位，而 uCore 的 <code>bootloader</code> 不支持处理重定位，在后续实验中也会产生问题。</li><li><code>-fno-stack-protector</code>：同样是为了减小代码体积。启用栈保护的话在函数调用时会增加额外的代码。</li><li><code>-Wall</code>：表示打印出所有警告，可以帮我们尽早地发现可能出现的问题。</li><li><code>-gstabs</code> 和 <code>-ggdb</code> ：表示生成调试信息，帮助我们后续使用 <code>gdb</code> 进行调试。</li><li><code>-m32</code>：生成 32 位代码。</li><li><code>-c</code>：只编译不链接。后面由我们自己使用 <code>ld</code> 手动链接。</li></ul><p>值得注意的是，在编译 <code>bootloader</code> 的时候还增加了一个额外的选项：</p><ul><li><code>-Os</code>：意思是指示编译器尽可能地优化代码体积，因为 <code>bootloader</code> 在去掉启动标识符和分区表之后只有 466 字节可用，因此需要尽可能小。</li></ul></li><li><p>使用 <code>ld</code> 进行链接，将目标文件连接成为可执行文件。一个是 <code>kernel</code> 是操作系统的内核，一个是 <code>bootloader</code> 用来加载我们编写的操作系统。</p><ul><li><p>链接内核的命令是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o ...</span><br></pre></td></tr></table></figure><ul><li><code>-m elf_i386</code> 表示模拟 32 位体系。</li><li><code>-nostdlib</code> 不链接标准库。</li><li><code>-T tools/kernel.ld</code> 使用 <code>tools/kernel.ld</code> 链接脚本进行链接。</li></ul><p>可以看到 <code>tools/kernel.ld</code> 脚本如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">/* Simple linker script <span class="keyword">for</span> the JOS kernel.</span><br><span class="line">   See the GNU ld <span class="string">&#x27;info&#x27;</span> manual (<span class="string">&quot;info ld&quot;</span>) to learn the syntax. */</span><br><span class="line"></span><br><span class="line">OUTPUT_FORMAT(<span class="string">&quot;elf32-i386&quot;</span>, <span class="string">&quot;elf32-i386&quot;</span>, <span class="string">&quot;elf32-i386&quot;</span>)</span><br><span class="line">OUTPUT_ARCH(i386)</span><br><span class="line">ENTRY(kern_init)</span><br><span class="line"></span><br><span class="line">SECTIONS &#123;</span><br><span class="line">/* Load the kernel at this address: <span class="string">&quot;.&quot;</span> means the current address */</span><br><span class="line">. = 0x100000;</span><br><span class="line"></span><br><span class="line">.text : &#123;</span><br><span class="line">*(.text .stub .text.* .gnu.linkonce.t.*)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">PROVIDE(etext = .);/* Define the <span class="string">&#x27;etext&#x27;</span> symbol to this value */</span><br><span class="line"></span><br><span class="line">.rodata : &#123;</span><br><span class="line">*(.rodata .rodata.* .gnu.linkonce.r.*)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* Include debugging information <span class="keyword">in</span> kernel memory */</span><br><span class="line">.stab : &#123;</span><br><span class="line">PROVIDE(__STAB_BEGIN__ = .);</span><br><span class="line">*(.stab);</span><br><span class="line">PROVIDE(__STAB_END__ = .);</span><br><span class="line">BYTE(0)/* Force the linker to allocate space</span><br><span class="line">   <span class="keyword">for</span> this section */</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">.stabstr : &#123;</span><br><span class="line">PROVIDE(__STABSTR_BEGIN__ = .);</span><br><span class="line">*(.stabstr);</span><br><span class="line">PROVIDE(__STABSTR_END__ = .);</span><br><span class="line">BYTE(0)/* Force the linker to allocate space</span><br><span class="line">   <span class="keyword">for</span> this section */</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">/* Adjust the address <span class="keyword">for</span> the data segment to the next page */</span><br><span class="line">. = ALIGN(0x1000);</span><br><span class="line"></span><br><span class="line">/* The data segment */</span><br><span class="line">.data : &#123;</span><br><span class="line">*(.data)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">PROVIDE(edata = .);</span><br><span class="line"></span><br><span class="line">.bss : &#123;</span><br><span class="line">*(.bss)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">PROVIDE(end = .);</span><br><span class="line"></span><br><span class="line">/DISCARD/ : &#123;</span><br><span class="line">*(.eh_frame .note.GNU-stack)</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>比较重要的部分有：</p><ul><li>指定了程序起始地址为 <code>0x100000</code>。</li><li>导出了 <code>etext</code>、<code>edata</code> 和 <code>end</code> 地址供后续 C 语言程序使用，以便能够正确初始化内存。</li><li>加入内核调试信息。</li></ul></li><li><p>而链接 <code>bootloader</code> 的命令是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o</span><br></pre></td></tr></table></figure><p>和链接内核的有少许不同：</p><ul><li><code>-N</code>：将 <code>text</code> 段设置为可读可写，不对齐 <code>data</code> 段，不链接动态库。</li><li><code>-e</code>：程序的入口是 <code>start</code> 标签。</li><li><code>-Ttext 0x7C00</code>：程序的 <code>text</code> 段从 <code>0x7C00</code> 开始，因为 BIOS 会将 <code>bootloader</code> 放在 <code>0x7C00</code> 处开始执行。</li></ul></li></ul></li><li><p>对 <code>bootloader</code> 签名，使得操作系统可以正常引导。即在 <code>bootloader</code> 第 511、512 个字节处要写入 <code>0x55</code> 和 <code>0xAA</code>。</p></li><li><p>使用 <code>dd</code> 命令生成镜像。</p><ol><li>首先生成一个 10000 个扇区的全零的镜像。</li><li>然后在第一个扇区（扇区 0）写入大小为 512 字节的 <code>bootloader</code>。</li><li>然后在 <code>bootloader</code> 之后（扇区 1）写入真正的操作系统内核。</li></ol></li></ol></li><li><p>一个被系统认为是符合规范的硬盘主引导扇区的特征是什么？ </p><ul><li>大小为 512 字节。</li><li>最后两个字节为<code>0x55</code> 和 <code>0xAA</code>。</li><li>由不超过 466 字节的启动代码和不超过 64 字节的硬盘分区表加上两个字节的结束符 <code>0x55AA</code> 组成。</li></ul></li></ol><h3 id="练习-2"><a href="#练习-2" class="headerlink" title="练习 2"></a>练习 2</h3><ol><li>从 CPU 加电后执行的第一条指令开始，单步跟踪 BIOS 的执行。</li></ol><p>   操作步骤：</p><ol><li><p>执行 <code>make</code> 命令生成可启动镜像。</p></li><li><p>输入命令 <code>qemu-system-i386 -S -s -parallel stdio -hda ./bin/ucore.img -serial null</code> 启动 <code>qemu</code> 模拟器。</p><ul><li><code>-s</code> 表示在 <code>tcp::1234</code> 打开 <code>GDB</code> 调试服务器。</li><li><code>-S</code> 代表开机后暂停 CPU 执行，只有用户输入命令之后才开始执行。</li><li><code>-hda</code> 指定了硬盘的镜像。</li><li><code>-parallel</code> 和 <code>-serial</code> 分别指定并行口和串行口的输出重定向位置。</li></ul><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151009.png" data-fancybox="gallery" data-caption="Screenshot_20190322_151009"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151009.png" alt="Screenshot_20190322_151009"></a></p><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151025.png" data-fancybox="gallery" data-caption="Screenshot_20190322_151025"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151025.png" alt="Screenshot_20190322_151025"></a></p><p>可以看到 <code>qemu</code> 启动后没有继续加载，而是暂停了 CPU 的执行。</p></li><li><p>启动 <code>gdb</code>，输入 <code>target remote :1234</code> 连接到 <code>qemu</code> 准备调试，并且输入 <code>set architecture i8086</code> 指示 <code>qemu</code> 现在的可执行代码是 16 位的，因为 BIOS 代码是 16 位的。</p><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151147.png" data-fancybox="gallery" data-caption="Screenshot_20190322_151147"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151147.png" alt="Screenshot_20190322_151147"></a></p></li><li><p>准备完成之后。<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151400.png" data-fancybox="gallery" data-caption="Screenshot_20190322_151400"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151400.png" alt="Screenshot_20190322_151400"></a></p></li><li><p>输入命令 </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">define hook-stop</span><br><span class="line">x/i $pc</span><br><span class="line">end</span><br></pre></td></tr></table></figure><p>在每次暂停执行代码时强制反汇编当前命令。</p><p>输入 <code>stepi</code> 开始单步跟踪 BIOS 执行过程，运行结果如下图所示：<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151623.png" data-fancybox="gallery" data-caption="Screenshot_20190322_151623"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_151623.png" alt="Screenshot_20190322_151623"></a></p></li><li><p>在初始化位置 <code>0x7c00</code> 设置实地址断点，测试断点正常。 </p><ol><li>按照上面的操作启动 <code>qemu</code> 和 <code>gdb</code> 调试之后，输入 <code>break *0x7c00</code> 在地址 <code>0x7c00</code> 处设下断点。</li><li>然后输入 <code>continue</code> 让 CPU 正常执行。</li><li>可以看到，当 CPU 执行到 <code>0x7c00</code> 处时，遇到了断点，自动暂停执行。此时输入 <code>x/i &amp;#36;pc</code> 进行反汇编，正是 <code>bootloader.S</code> 的第一条指令 <code>cli</code> 。</li></ol><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_152021.png" data-fancybox="gallery" data-caption="Screenshot_20190322_152021"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_152021.png" alt="Screenshot_20190322_152021"></a></p></li><li><p>在调用 qemu 时增加 <code>-d in_asm -D q.log</code> 参数，便可以将运行的汇编指令保存在 <code>q.log</code> 中。 将执行的汇编代码与 <code>bootasm.S</code> 和 <code>bootblock.asm</code> 进行比较，看看二者是否一致。 </p><ol><li>执行命令 <code>qemu-system-i386 -d in_asm -D q.log -S -s -parallel stdio -hda ./bin/ucore.img -serial null</code> 启动 <code>qemu</code> 并记录执行的汇编代码到 <code>q.log</code> 中。</li><li>执行命令后，用文本编辑器打开 <code>q.log</code> 文件和 <code>bootasm.S</code> 进行比较，定位到地址 <code>0x00007c00</code> 处可以发现它们是一致的。<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190322_211958.png" data-fancybox="gallery" data-caption="Screenshot_20190322_211958"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190322_211958.png" alt="Screenshot_20190322_211958"></a></li></ol></li></ol><h3 id="练习-3"><a href="#练习-3" class="headerlink" title="练习 3"></a>练习 3</h3><p>BIOS 将通过读取硬盘主引导扇区到内存，并转跳到对应内存中的位置执行 bootloader。请分析 bootloader 是如何完成从实模式进入保护模式的。 </p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#include &lt;asm.h&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Start the CPU: switch to 32-bit protected mode, jump into C.</span></span><br><span class="line"><span class="comment"># The BIOS loads this code from the first sector of the hard disk into</span></span><br><span class="line"><span class="comment"># memory at physical address 0x7c00 and starts executing in real mode</span></span><br><span class="line"><span class="comment"># with %cs=0 %ip=7c00.</span></span><br><span class="line"></span><br><span class="line"><span class="meta">.set</span> PROT_MODE_CSEG,        <span class="number">0x8</span>                     <span class="comment"># kernel code segment selector</span></span><br><span class="line"><span class="meta">.set</span> PROT_MODE_DSEG,        <span class="number">0x10</span>                    <span class="comment"># kernel data segment selector</span></span><br><span class="line"><span class="meta">.set</span> CR0_PE_ON,             <span class="number">0x1</span>                     <span class="comment"># protected mode enable flag</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># start address should be 0:7c00, in real mode, the beginning address of the running bootloader</span></span><br><span class="line">.globl start</span><br><span class="line"><span class="symbol">start:</span></span><br><span class="line">.code16                                             <span class="comment"># Assemble for 16-bit mode</span></span><br><span class="line">    cli                                             <span class="comment"># Disable interrupts</span></span><br><span class="line">    cld                                             <span class="comment"># String operations increment</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Set up the important data segment registers (DS, ES, SS).</span></span><br><span class="line">    <span class="keyword">xorw </span>%ax, %ax                                   <span class="comment"># Segment number zero</span></span><br><span class="line">    movw %ax, %ds                                   <span class="comment"># -&gt; Data Segment</span></span><br><span class="line">    movw %ax, %es                                   <span class="comment"># -&gt; Extra Segment</span></span><br><span class="line">    movw %ax, %ss                                   <span class="comment"># -&gt; Stack Segment</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Enable A20:</span></span><br><span class="line">    <span class="comment">#  For backwards compatibility with the earliest PCs, physical</span></span><br><span class="line">    <span class="comment">#  address line 20 is tied low, so that addresses higher than</span></span><br><span class="line">    <span class="comment">#  1MB wrap around to zero by default. This code undoes this.</span></span><br><span class="line"><span class="symbol">seta20.1:</span></span><br><span class="line">    inb$<span class="number">0x64</span>, %al                                  <span class="comment"># Wait for not busy(8042 input buffer empty).</span></span><br><span class="line">    testb $<span class="number">0x2</span>, %al</span><br><span class="line">    <span class="keyword">jnz </span>seta20.<span class="number">1</span></span><br><span class="line"></span><br><span class="line">    movb$<span class="number">0xd1</span>, %al                                 <span class="comment"># 0xd1 -&gt; port 0x64</span></span><br><span class="line">    outb %al, $<span class="number">0x64</span>                                 <span class="comment"># 0xd1 means: write data to 8042&#x27;s P2 port</span></span><br><span class="line"><span class="symbol"></span></span><br><span class="line"><span class="symbol">seta20.2:</span></span><br><span class="line">    inb$<span class="number">0x64</span>, %al                                  <span class="comment"># Wait for not busy(8042 input buffer empty).</span></span><br><span class="line">    testb $<span class="number">0x2</span>, %al</span><br><span class="line">    <span class="keyword">jnz </span>seta20.<span class="number">2</span></span><br><span class="line"></span><br><span class="line">    movb$<span class="number">0xdf</span>, %al                                 <span class="comment"># 0xdf -&gt; port 0x60</span></span><br><span class="line">    outb %al, $<span class="number">0x60</span>                                 <span class="comment"># 0xdf = 11011111, means set P2&#x27;s A20 bit(the 1 bit) to 1</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Switch from real to protected mode, using a bootstrap GDT</span></span><br><span class="line">    <span class="comment"># and segment translation that makes virtual addresses</span></span><br><span class="line">    <span class="comment"># identical to physical addresses, so that the</span></span><br><span class="line">    <span class="comment"># effective memory map does not change during the switch.</span></span><br><span class="line">    lgdt gdtdesc</span><br><span class="line">    movl %cr0, %eax</span><br><span class="line">    <span class="keyword">orl$CR0_PE_ON, </span>%eax</span><br><span class="line">    movl %eax, %cr0</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Jump to next instruction, but in 32-bit code segment.</span></span><br><span class="line">    <span class="comment"># Switches processor into 32-bit mode.</span></span><br><span class="line">    ljmp $PROT\_MODE\_CSEG,$protcseg</span><br><span class="line"></span><br><span class="line">.code32                                             <span class="comment"># Assemble for 32-bit mode</span></span><br><span class="line"><span class="symbol">protcseg:</span></span><br><span class="line">    <span class="comment"># Set up the protected-mode data segment registers</span></span><br><span class="line">    movw $PROT_MODE_DSEG, %ax                       <span class="comment"># Our data segment selector</span></span><br><span class="line">    movw %ax, %ds                                   <span class="comment"># -&gt; DS: Data Segment</span></span><br><span class="line">    movw %ax, %es                                   <span class="comment"># -&gt; ES: Extra Segment</span></span><br><span class="line">    movw %ax, %fs                                   <span class="comment"># -&gt; FS</span></span><br><span class="line">    movw %ax, %gs                                   <span class="comment"># -&gt; GS</span></span><br><span class="line">    movw %ax, %ss                                   <span class="comment"># -&gt; SS: Stack Segment</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)</span></span><br><span class="line">    movl$<span class="number">0x0</span>, %ebp</span><br><span class="line">    movl $start, %esp</span><br><span class="line">    call <span class="keyword">bootmain</span></span><br><span class="line"><span class="keyword"></span></span><br><span class="line">    <span class="comment"># If bootmain returns (it shouldn&#x27;t), loop.</span></span><br><span class="line"><span class="symbol">spin:</span></span><br><span class="line">    <span class="keyword">jmp </span>spin</span><br><span class="line"></span><br><span class="line"><span class="comment"># Bootstrap GDT</span></span><br><span class="line">.p2align <span class="number">2</span>                                          <span class="comment"># force 4 byte alignment</span></span><br><span class="line"><span class="symbol">gdt:</span></span><br><span class="line">    SEG_NULLASM                                     <span class="comment"># null seg</span></span><br><span class="line">    SEG_ASM(STA_X<span class="title">|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel</span></span><br><span class="line"><span class="title">    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel</span></span><br><span class="line"><span class="title"></span></span><br><span class="line"><span class="title">gdtdesc:</span></span><br><span class="line"><span class="title">    .word 0x17                                      # sizeof(gdt) - 1</span></span><br><span class="line"><span class="title">    .long gdt                                       # address gdt</span></span><br></pre></td></tr></table></figure><p>启动过程如下：</p><ol><li>关闭中断，清空主要寄存器。</li><li>打开 A20 模式，通过控制 8042 键盘控制器实现。</li><li>设置全局描述符，将全局描述符表的地址和大小加载到寄存器中。</li><li>设置 <code>CR0</code> 寄存器 <code>&amp;#36;CR0_PE_ON</code> 位，启动保护模式。</li><li>然后在 32 位模式下，设置好段寄存器和栈寄存器，跳转到 <code>bootmain</code> 也就是加载内核 ELF 的程序。</li></ol><h3 id="练习-4"><a href="#练习-4" class="headerlink" title="练习 4"></a>练习 4</h3><p>分析 <code>bootloader</code> 加载 ELF 格式的 OS 的过程。</p><ol><li><p><code>bootloader</code> 是如何读取硬盘扇区的？</p><p><code>bootloader</code> 主要通过 <code>readsect()</code> 和 <code>waitdisk()</code> 两个函数进行硬盘扇区的读取。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">readsect</span><span class="params">(<span class="type">void</span> *dst, <span class="type">uint32_t</span> secno)</span> &#123;</span><br><span class="line">    <span class="comment">// wait for disk to be ready</span></span><br><span class="line">    waitdisk();</span><br><span class="line"></span><br><span class="line">    outb(<span class="number">0x1F2</span>, <span class="number">1</span>);                         <span class="comment">// count = 1</span></span><br><span class="line">    outb(<span class="number">0x1F3</span>, secno &amp; <span class="number">0xFF</span>);</span><br><span class="line">    outb(<span class="number">0x1F4</span>, (secno &gt;&gt; <span class="number">8</span>) &amp; <span class="number">0xFF</span>);</span><br><span class="line">    outb(<span class="number">0x1F5</span>, (secno &gt;&gt; <span class="number">16</span>) &amp; <span class="number">0xFF</span>);</span><br><span class="line">    outb(<span class="number">0x1F6</span>, ((secno &gt;&gt; <span class="number">24</span>) &amp; <span class="number">0xF</span>) | <span class="number">0xE0</span>);</span><br><span class="line">    outb(<span class="number">0x1F7</span>, <span class="number">0x20</span>);                      <span class="comment">// cmd 0x20 - read sectors</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// wait for disk to be ready</span></span><br><span class="line">    waitdisk();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// read a sector</span></span><br><span class="line">    insl(<span class="number">0x1F0</span>, dst, SECTSIZE / <span class="number">4</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>首先等待磁盘控制器准备好，然后写入控制信息，<code>0x1F2</code> 是要读取的扇区数，<code>0x1F3</code> 到 <code>0x1F6</code> 存储着要进行操作的扇区号码（小端存储），然后向 <code>0x1F7</code> 发送读取指令。等磁盘再次就绪后，通过反复调用 <code>insl</code> 指令 <code>SECTSIZE / 4</code> 次，将磁盘内容以 4 字节为一组读取到内存中。</p><p>基于这两个函数，<code>bootloader</code> 还有函数 <code>readseg()</code> 包装了一下基本操作，用以读取多个扇区。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">readseg</span><span class="params">(<span class="type">uintptr_t</span> va, <span class="type">uint32_t</span> count, <span class="type">uint32_t</span> offset)</span> &#123;</span><br><span class="line">    <span class="type">uintptr_t</span> end_va = va + count;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// round down to sector boundary</span></span><br><span class="line">    va -= offset % SECTSIZE;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// translate from bytes to sectors; kernel starts at sector 1</span></span><br><span class="line">    <span class="type">uint32_t</span> secno = (offset / SECTSIZE) + <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// If this is too slow, we could read lots of sectors at a time.</span></span><br><span class="line">    <span class="comment">// We&#x27;d write more to memory than asked, but it doesn&#x27;t matter --</span></span><br><span class="line">    <span class="comment">// we load in increasing order.</span></span><br><span class="line">    <span class="keyword">for</span> (; va &lt; end_va; va += SECTSIZE, secno ++) &#123;</span><br><span class="line">        readsect((<span class="type">void</span> *)va, secno);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>函数的作用是从磁盘的 <code>offset</code> 字节处开始读取 <code>count</code> 个字节到 <code>va</code> 指向的虚拟地址中。</p></li><li><p><code>bootloader</code> 是如何加载 ELF 格式的 OS？</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">bootmain</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="comment">// read the 1st page off disk</span></span><br><span class="line">    readseg((<span class="type">uintptr_t</span>)ELFHDR, SECTSIZE * <span class="number">8</span>, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// is this a valid ELF?</span></span><br><span class="line">    <span class="keyword">if</span> (ELFHDR-&gt;e_magic != ELF_MAGIC) &#123;</span><br><span class="line">        <span class="keyword">goto</span> bad;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">proghdr</span> *<span class="title">ph</span>, *<span class="title">eph</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// load each program segment (ignores ph flags)</span></span><br><span class="line">    ph = (<span class="keyword">struct</span> proghdr *)((<span class="type">uintptr_t</span>)ELFHDR + ELFHDR-&gt;e_phoff);</span><br><span class="line">    eph = ph + ELFHDR-&gt;e_phnum;</span><br><span class="line">    <span class="keyword">for</span> (; ph &lt; eph; ph ++) &#123;</span><br><span class="line">        readseg(ph-&gt;p_va &amp; <span class="number">0xFFFFFF</span>, ph-&gt;p_memsz, ph-&gt;p_offset);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// call the entry point from the ELF header</span></span><br><span class="line">    <span class="comment">// note: does not return</span></span><br><span class="line">    ((<span class="type">void</span> (*)(<span class="type">void</span>))(ELFHDR-&gt;e_entry &amp; <span class="number">0xFFFFFF</span>))();</span><br><span class="line"></span><br><span class="line">bad:</span><br><span class="line">    outw(<span class="number">0x8A00</span>, <span class="number">0x8A00</span>);</span><br><span class="line">    outw(<span class="number">0x8A00</span>, <span class="number">0x8E00</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* do nothing */</span></span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">elfhdr</span> &#123;</span></span><br><span class="line">    <span class="type">uint32_t</span> e_magic;     <span class="comment">// must equal ELF_MAGIC</span></span><br><span class="line">    <span class="type">uint8_t</span> e_elf[<span class="number">12</span>];</span><br><span class="line">    <span class="type">uint16_t</span> e_type;      <span class="comment">// 1=relocatable, 2=executable, 3=shared object, 4=core image</span></span><br><span class="line">    <span class="type">uint16_t</span> e_machine;   <span class="comment">// 3=x86, 4=68K, etc.</span></span><br><span class="line">    <span class="type">uint32_t</span> e_version;   <span class="comment">// file version, always 1</span></span><br><span class="line">    <span class="type">uint32_t</span> e_entry;     <span class="comment">// entry point if executable</span></span><br><span class="line">    <span class="type">uint32_t</span> e_phoff;     <span class="comment">// file position of program header or 0</span></span><br><span class="line">    <span class="type">uint32_t</span> e_shoff;     <span class="comment">// file position of section header or 0</span></span><br><span class="line">    <span class="type">uint32_t</span> e_flags;     <span class="comment">// architecture-specific flags, usually 0</span></span><br><span class="line">    <span class="type">uint16_t</span> e_ehsize;    <span class="comment">// size of this elf header</span></span><br><span class="line">    <span class="type">uint16_t</span> e_phentsize; <span class="comment">// size of an entry in program header</span></span><br><span class="line">    <span class="type">uint16_t</span> e_phnum;     <span class="comment">// number of entries in program header or 0</span></span><br><span class="line">    <span class="type">uint16_t</span> e_shentsize; <span class="comment">// size of an entry in section header</span></span><br><span class="line">    <span class="type">uint16_t</span> e_shnum;     <span class="comment">// number of entries in section header or 0</span></span><br><span class="line">    <span class="type">uint16_t</span> e_shstrndx;  <span class="comment">// section number that contains section name strings</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>首先函数加载 ELF 的头部到内存中，判断魔数是否合法，如果合法，则开始读取 ELF 中的每一段到内存中的相应位置。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">proghdr</span> &#123;</span></span><br><span class="line">    <span class="type">uint32_t</span> p_type;   <span class="comment">// loadable code or data, dynamic linking info,etc.</span></span><br><span class="line">    <span class="type">uint32_t</span> p_offset; <span class="comment">// file offset of segment</span></span><br><span class="line">    <span class="type">uint32_t</span> p_va;     <span class="comment">// virtual address to map segment</span></span><br><span class="line">    <span class="type">uint32_t</span> p_pa;     <span class="comment">// physical address, not used</span></span><br><span class="line">    <span class="type">uint32_t</span> p_filesz; <span class="comment">// size of segment in file</span></span><br><span class="line">    <span class="type">uint32_t</span> p_memsz;  <span class="comment">// size of segment in memory (bigger if contains bss）</span></span><br><span class="line">    <span class="type">uint32_t</span> p_flags;  <span class="comment">// read/write/execute bits</span></span><br><span class="line">    <span class="type">uint32_t</span> p_align;  <span class="comment">// required alignment, invariably hardware page size</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>ELF 段的信息由 <code>proghdr</code> 结构指示，第一个 <code>proghdr</code> 位于 <code>e_phoff</code> 偏移量，数量则是 <code>e_phnum</code>，其中 <code>p_va</code> 指示段在内存中的虚拟地址，<code>p_memsz</code> 标志了段的大小，<code>p_offset</code> 则是相对于 ELF 文件的偏移量。</p><p>将每一段都读取到了内存中的相应位置之后，就跳转到 ELF 头部中 <code>e_entry</code> 所指示的入口地址执行，控制权移交给操作系统。</p></li></ol><h3 id="练习-5"><a href="#练习-5" class="headerlink" title="练习 5"></a>练习 5</h3><p>实现函数调用堆栈跟踪函数 <code>print_stackframe()</code>。</p><p>通过附录得知，在每个函数体的开始，编译器都会插入如下汇编代码：</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pushl   %ebp</span><br><span class="line">movl    %esp, %ebp</span><br></pre></td></tr></table></figure><p>这样在程序执行到一个函数的实际指令前，已经有以下数据顺序入栈：参数、返回地址、<code>ebp</code> 寄存器。由此得到类似如下的栈结构（以 <code>cdecl</code> 方式为例）：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     |  高位地址</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |     参数 <span class="number">3</span>      |</span><br><span class="line"> |     参数 <span class="number">2</span>      |</span><br><span class="line"> |     参数 <span class="number">1</span>      |</span><br><span class="line"> |    返回地址     |</span><br><span class="line"> |   上一层[ebp]   |  &lt;-------- [ebp]</span><br><span class="line"> |    局部变量     |  低位地址</span><br></pre></td></tr></table></figure><p>此时 <code>ebp</code> 处于非常重要的地位，因为给 <code>ebp</code> 赋值之前，原 <code>ebp</code> 值已经被压栈（位于栈顶），而新的 <code>ebp</code> 又恰恰指向栈顶。从函数堆栈结构可以知道，通过 <code>ebp</code> 寄存器中的地址向上可以获得返回地址和参数，向下可以获取到局部变量，通过它本身可以获取到上一层函数调用的 <code>ebp</code> 地址。</p><p>根据这个结构，就可以写出显示函数调用堆栈的函数如下了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">print_stackframe</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">     <span class="type">uint32_t</span> ebp = read_ebp(), eip = read_eip();</span><br><span class="line">     </span><br><span class="line">     <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; STACKFRAME_DEPTH &amp;&amp; ebp != <span class="number">0</span>; ++i) &#123;</span><br><span class="line">        <span class="type">uint32_t</span> *args = (<span class="type">uint32_t</span> *)ebp + <span class="number">2</span>;</span><br><span class="line">        cprintf(<span class="string">&quot;ebp:0x%08x eip:0x%08x args:0x%08x 0x%08x 0x%08x 0x%08x&quot;</span>, </span><br><span class="line">                 ebp,       eip,       args[<span class="number">0</span>], args[<span class="number">1</span>], args[<span class="number">2</span>], args[<span class="number">3</span>]);</span><br><span class="line">        cprintf(<span class="string">&quot;\n&quot;</span>);</span><br><span class="line">        print_debuginfo(eip - <span class="number">1</span>);</span><br><span class="line">        eip = *(<span class="type">uint32_t</span>*) (ebp + <span class="number">4</span>);</span><br><span class="line">        ebp = *(<span class="type">uint32_t</span>*) ebp;</span><br><span class="line">     &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是在遇到 <code>ebp == 0</code> 的情况下，已经没有上一层的函数调用了，应该终止调用。</p><p>完成函数编写之后，执行 <code>make qemu</code> 查看执行结果：<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190325_095912.png" data-fancybox="gallery" data-caption="Screenshot_20190325_095912"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190325_095912.png" alt="Screenshot_20190325_095912"></a></p><p>输出最后一层是函数调用的最深层，也就是第一个被调用的函数 <code>kern_init()</code> 因为栈地址是从 <code>0x7c00</code> 开始，在从 <code>bootmain()</code> 函数调用到内核初始化函数 <code>kern_init()</code> 时有 <code>call</code> 指令，<code>0x7bfc</code> 地址存放的是返回地址，而编译器在函数体的头部都会插入：</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pushl   %ebp</span><br><span class="line">movl    %esp, %ebp</span><br></pre></td></tr></table></figure><p>通过查看 <code>kernel.asm</code> 也可以确认：<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190325_101758.png" data-fancybox="gallery" data-caption="Screenshot_20190325_101758"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190325_101758.png" alt="Screenshot_20190325_101758"></a>于是此时栈顶地址变为 <code>0x7bf8</code>，而这个地址赋值给了 <code>ebp</code> 寄存器，我们就看到了 <code>ebp</code> 寄存器的内容是 <code>0x7bf8</code> 了。这个函数调用没有参数，后面的参数没有意义。</p><h3 id="练习-6"><a href="#练习-6" class="headerlink" title="练习 6"></a>练习 6</h3><p>完善中断初始化和处理。</p><ol><li><p>中断向量表中一个表项占多少字节？其中哪几位代表中断处理代码的入口？ </p><p>查看 <code>struct gatedesc</code> 定义如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">gatedesc</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> gd_off_15_0 : <span class="number">16</span>;        <span class="comment">// low 16 bits of offset in segment</span></span><br><span class="line">    <span class="type">unsigned</span> gd_ss : <span class="number">16</span>;              <span class="comment">// segment selector</span></span><br><span class="line">    <span class="type">unsigned</span> gd_args : <span class="number">5</span>;             <span class="comment">// # args, 0 for interrupt/trap gates</span></span><br><span class="line">    <span class="type">unsigned</span> gd_rsv1 : <span class="number">3</span>;             <span class="comment">// reserved(should be zero I guess)</span></span><br><span class="line">    <span class="type">unsigned</span> gd_type : <span class="number">4</span>;             <span class="comment">// type(STS_&#123;TG,IG32,TG32&#125;)</span></span><br><span class="line">    <span class="type">unsigned</span> gd_s : <span class="number">1</span>;                <span class="comment">// must be 0 (system)</span></span><br><span class="line">    <span class="type">unsigned</span> gd_dpl : <span class="number">2</span>;              <span class="comment">// descriptor(meaning new) privilege level</span></span><br><span class="line">    <span class="type">unsigned</span> gd_p : <span class="number">1</span>;                <span class="comment">// Present</span></span><br><span class="line">    <span class="type">unsigned</span> gd_off_31_16 : <span class="number">16</span>;       <span class="comment">// high bits of offset in segment</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>可以看到一个 <code>struct gatedesc</code> 占用 8 字节，其中低 2 字节和高 2 字节拼接成段偏移的低 16 位和高 16 位，2-3 字节则是段选择子。</p></li><li><p>请编程完善 <code>kern/trap/trap.c </code>中对中断向量表进行初始化的函数 <code>idt_init</code>。在 <code>idt_init</code> 函数 中，依次对所有中断入口进行初始化。使用 <code>mmu.h</code> 中的 <code>SETGATE</code> 宏，填充 <code>idt</code> 数组内容。注意除了系统调用中断 (<code>T_SYSCALL</code>)以外，其它中断均使用中断门描述符，权限为内核态权限；而系统调用中断使用异常，权限为用户态权限。每个中断的入口由 <code>tools/vectors.c</code> 生成，使用 <code>trap.c</code> 中声明的 <code>vectors</code> 数组即可。 </p><p>代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">idt_init</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="keyword">extern</span> <span class="type">uintptr_t</span> __vectors[];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="keyword">sizeof</span>(idt) / <span class="keyword">sizeof</span>(<span class="keyword">struct</span> gatedesc); ++i) &#123;</span><br><span class="line">        SETGATE(idt[i], <span class="number">0</span>, GD_KTEXT, __vectors[i], DPL_KERNEL);</span><br><span class="line">    &#125;</span><br><span class="line">     </span><br><span class="line">    SETGATE(idt[T_SYSCALL], <span class="number">1</span>, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);</span><br><span class="line">    SETGATE(idt[T_SWITCH_TOK], <span class="number">0</span>, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);</span><br><span class="line"></span><br><span class="line">    lidt(&amp;idt_pd);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用 <code>SETGATE</code> 宏设置好中断描述表，调用 <code>lidt</code> 命令将其加载，以后遇到中断时就会进入我们的中断处理程序。</p></li><li><p>请编程完善 <code>trap.c</code> 中的中断处理函数 <code>trap()</code>，在对时钟中断进行处理的部分填写 <code>trap()</code> 函数中处理时钟中断的部分，使操作系统每遇到 100 次时钟中断后，调用 <code>print_ticks()</code> 子程序，向 屏幕上打印一行文字“100 ticks”。 可以知道定时器中断的中断号是 <code>IRQ_TIMER</code> ，所以在 <code>switch</code> 块中找到 <code>IRQ_TIMER</code> 填入相关代码。代码很简单：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">trap_dispatch</span><span class="params">(<span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    <span class="type">char</span> c;</span><br><span class="line">    <span class="keyword">switch</span> (tf-&gt;tf_trapno) &#123;</span><br><span class="line">    <span class="keyword">case</span> IRQ_OFFSET + IRQ_TIMER:</span><br><span class="line">        <span class="keyword">if</span> ((++ticks) % TICK_NUM == <span class="number">0</span>) print_ticks();</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><p>完成代码后，执行 <code>make qemu</code> ，可以看到大约每隔一段时间就会输出一行 “100 ticks”，同时屏幕上会回显键盘按键。<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190325_110047.png" data-fancybox="gallery" data-caption="Screenshot_20190325_110047"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190325_110047.png" alt="Screenshot_20190325_110047"></a></p><h3 id="扩展练习"><a href="#扩展练习" class="headerlink" title="扩展练习"></a>扩展练习</h3><p>这一部分的代码注释比较少，需要自己理解的部分较多。</p><p>由于切换特权级别的操作需要通过更改 <code>CS</code> 段寄存器来实现，但是 <code>CS</code> 段寄存器不能直接修改。在中断发生时，硬件会自动将 <code>CS</code> 、<code>EIP</code>、<code>EFLAGS</code> 和 <code>ERRCODE</code> 压栈，中断返回时从栈中恢复这些寄存器的值。因此，必须通过 <code>iret</code> 指令修改 <code>CS</code> 段寄存器中的值，也就是在中断处理程序中，必须更改 <code>struct trapframe</code> 中的 <code>tf_cs</code> 字段才能实现特权级的转换。对于 <code>SS</code> 段选择子寄存器而言也是一样的，要更改它的值必须通过修改 <code>struct trapframe</code> 来实现。</p><p>需要注意的是 <code>iret</code> 指令在不同情况下表现是不同的。查阅资料得：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(OperandSize == <span class="number">32</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span>(!IsWithinStackLimits(TopStackBytes(<span class="number">12</span>)) Exception(SS); <span class="comment">//top 12 bytes of stack not within stack limits</span></span><br><span class="line">    TemporaryEIP = Pop();</span><br><span class="line">    TemporaryCS = Pop();</span><br><span class="line">    TemporaryEFLAGS = Pop();</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//Protected mode return</span></span><br><span class="line"><span class="comment">//PE == 1, VM == 0 in flags image</span></span><br><span class="line"><span class="keyword">if</span>(ReturnCode.SegmentSelector == <span class="number">0</span>) Exception(GP(<span class="number">0</span>));</span><br><span class="line"><span class="keyword">if</span>(!IsWithinDescriptorTableLimits(ReturnCode.SegmentSelector.AddressesDescriptor)) Exception(GP(Selector));</span><br><span class="line">ReturnCode.SegmentDescriptor = ReadSegmentDescriptor(ReturnCode.SegmentSelector);</span><br><span class="line"><span class="keyword">if</span>(!IsCodeSegment(ReturnCode.SegmentDescriptor)) Exception(GP(Selector));</span><br><span class="line"><span class="keyword">if</span>(ReturnCode.SegmentSelector.RPL &lt; CPL) Exception(GP(Selector));</span><br><span class="line"><span class="keyword">if</span>(IsConforming(ReturnCode.SegmentDescriptor) &amp;&amp; ReturnCode.Segment.DPL &gt; ReturnCode.SegmentSelector.RPL) Exception(GP(Selector));</span><br><span class="line"><span class="keyword">if</span>(ReturnCode.SegmentSelector.RPL &gt; CPL) &#123;</span><br><span class="line">    <span class="comment">//Return to outer privilege level</span></span><br><span class="line">    <span class="keyword">if</span>(OperandSize == <span class="number">32</span> &amp;&amp; <span class="keyword">if</span>(!IsWithinStackLimits(TopStackBytes(<span class="number">8</span>)) Exception(SS(<span class="number">0</span>)); <span class="comment">//top 8 bytes of stack not within stack limits</span></span><br><span class="line">    <span class="keyword">else</span> <span class="comment">/*OperandSize == 16*/</span> <span class="keyword">if</span>(!IsWithinStackLimits(TopStackBytes(<span class="number">4</span>)) Exception(SS(<span class="number">0</span>)); <span class="comment">//top 4 bytes of stack not within stack limits</span></span><br><span class="line">    StackSegmentSelector = ReadReturnSegmentSelector();</span><br><span class="line">    <span class="keyword">if</span>(StackSegmentSelector == <span class="number">0</span>) Exception(GP(<span class="number">0</span>));</span><br><span class="line">    <span class="keyword">if</span>(!IsWithinDescriptorTableLimits(ReturnStackSegmentSelector.Index)) Exception(GP(SSSelector));</span><br><span class="line">    SegmentDescriptor = ReadSegmenDescriptor(ReturnSegmentSelector);</span><br><span class="line">    <span class="keyword">if</span>(StackSegmentSelector.RPL != ReturnCode.SegmentSelector.RPL) &#123;</span><br><span class="line">        <span class="keyword">if</span>(StackSegmentSelector.RPL != ReturnCode.SegmentSelector.RPL || !IndicatesWritableDataSegment(StackSegmentDescriptor) || StackSegment.DPL != ReturnCode.SegmentSelector.RPL) Exception(GP(SSSelector));</span><br><span class="line">        <span class="keyword">if</span>(!IsPresent(StackSegment)) Exception(SS(SSSelector));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span>(!IsWithinCodeSegmentLimit(TemporaryEIP)) Exception(GP(<span class="number">0</span>));</span><br><span class="line">    EIP = TemporaryIP;</span><br><span class="line">    CS = TemporaryCS;</span><br><span class="line">    EFLAGS.CF = TemporaryEFLAGS.CF;</span><br><span class="line">    EFLAGS.PF = TemporaryEFLAGS.PF;</span><br><span class="line">    EFLAGS.AF = TemporaryEFLAGS.ZF;</span><br><span class="line">    EFLAGS.SF = TemporaryEFLAGS.SF;</span><br><span class="line">    EFLAGS.TF = TemporaryEFLAGS.DF;</span><br><span class="line">    EFLAGS.OF = TemporaryEFLAGS.OF;</span><br><span class="line">    EFLAGS.NT = TemporaryEFLAGS.NT;</span><br><span class="line">    <span class="keyword">if</span>(OperandSize == <span class="number">32</span>) &#123;</span><br><span class="line">        EFLAGS.RF = TemporaryEFLAGS.RF;</span><br><span class="line">        EFLAGS.AC = TemporaryEFLAGS.AC;</span><br><span class="line">        EFLAGS.ID = TemporaryEFLAGS.ID;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span>(CPL &lt;= IOPL) EFLAGS.IF = TemporaryEFLAGS.IF;</span><br><span class="line">    <span class="keyword">if</span>(CPL == <span class="number">0</span>) &#123;</span><br><span class="line">        EFLAGS.IOPL = TemporaryEFLAGS.IOPL;</span><br><span class="line">        <span class="keyword">if</span>(OperandSize == <span class="number">32</span>) &#123;</span><br><span class="line">            EFLAGS.VM = TemporaryEFLAGS.VM;</span><br><span class="line">            EFLAGS.VIF = TemporaryEFLAGS.VIF;</span><br><span class="line">            EFLAGS.VIP = TemporaryEFLAGS.VIP;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//perform operation for each of the segment registers</span></span><br><span class="line">    SegmentRegisters[] = &#123;ES, FS, GS, DS&#125;;</span><br><span class="line">    <span class="keyword">while</span>(SegmentRegister = SegmentRegisters.Next()) <span class="keyword">if</span>((PointsToDate(SegmentRegister) || !IsConformingCodeSegment(SegmentRegister)) &amp;&amp; CPL &gt; SegmentDescriptor.DPL <span class="comment">/*stored in hidden part of segment register*/</span>) SegmentSelector = <span class="number">0</span>; <span class="comment">//segment register invalid; null segment selector</span></span><br><span class="line">    <span class="comment">//END</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment">//Same privilege level</span></span><br><span class="line">    <span class="comment">//PE=1, VM=0 in flags image, RPL=CPL</span></span><br><span class="line">    <span class="keyword">if</span>(!IsWithinCodeSegmentLimits(EIP)) Exception(GP(<span class="number">0</span>));</span><br><span class="line">    EIP = TemporaryEIP;</span><br><span class="line">    CS = TemporaryCS; <span class="comment">//segment descriptor information also loaded</span></span><br><span class="line">    EFLAGS.CF = TemporaryEFLAGS.CF;</span><br><span class="line">    EFLAGS.PF = TemporaryEFLAGS.PF;</span><br><span class="line">    EFLAGS.AF = TemporaryEFLAGS.ZF;</span><br><span class="line">    EFLAGS.SF = TemporaryEFLAGS.SF;</span><br><span class="line">    EFLAGS.TF = TemporaryEFLAGS.DF;</span><br><span class="line">    EFLAGS.OF = TemporaryEFLAGS.OF;</span><br><span class="line">    EFLAGS.NT = TemporaryEFLAGS.NT;</span><br><span class="line">    <span class="keyword">if</span>(OperandSize == <span class="number">32</span>) &#123;</span><br><span class="line">        EFLAGS.RF = TemporaryEFLAGS.RF;</span><br><span class="line">        EFLAGS.AC = TemporaryEFLAGS.AC;</span><br><span class="line">        EFLAGS.ID = TemporaryEFLAGS.ID;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span>(CPL &lt;= IOPL) EFLAGS.IF = TemporaryEFLAGS.IF;</span><br><span class="line">    <span class="keyword">if</span>(CPL == <span class="number">0</span>) &#123;</span><br><span class="line">        EFLAGS.IOPL = TemporaryEFLAGS.IOPL;</span><br><span class="line">        <span class="keyword">if</span>(OperandSize == <span class="number">32</span>) &#123;</span><br><span class="line">            EFLAGS.VM = TemporaryEFLAGS.VM;</span><br><span class="line">            EFLAGS.VIF = TemporaryEFLAGS.VIF;</span><br><span class="line">            EFLAGS.VIP = TemporaryEFLAGS.VIP;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//END</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面的伪代码可以看出，如果在中断返回时，代码段从特权级较高的一层转换到了特权级较低的一层，<code>iret</code> 指令还会将 <code>SS</code> 和 <code>ESP</code> 出栈覆盖原来的值，也就是说在从内核态切换到用户态的时候我们还要设置 <code>tf_ss</code> 到用户段空间中，确保栈段选择子的 <code>RPL</code> 与代码段选择子的 <code>RPL</code> 相同，否则会引发 <code>General Protection</code> 异常。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> <span class="title">switchk2u</span>, *<span class="title">switchu2k</span>;</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">trap_dispatch</span><span class="params">(<span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    <span class="type">char</span> c;</span><br><span class="line">    <span class="keyword">switch</span> (tf-&gt;tf_trapno) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> T_SWITCH_TOU:</span><br><span class="line">        switchk2u = *tf;</span><br><span class="line">        switchk2u.tf_cs = USER_CS;</span><br><span class="line">        switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;</span><br><span class="line">        switchk2u.tf_esp = (<span class="type">uintptr_t</span>) &amp;tf-&gt;tf_esp;</span><br><span class="line">        switchk2u.tf_eflags |= FL_IOPL_3;</span><br><span class="line">        <span class="comment">// Will overwrite %esp</span></span><br><span class="line">        *((<span class="type">uint32_t</span> *)tf - <span class="number">1</span>) = (<span class="type">uint32_t</span>) &amp;switchk2u;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从以上代码可以看出，从内核态切换到用户态时要先修改各段寄存器，然后将 <code>tf_esp</code> 设置为原来的栈顶，最后更改 <code>tf_eflags</code> 的特权级。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     | 高位地址</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |  &lt;-------- [tf]（tf 即原来的 %esp）</span><br><span class="line"> |       tf        |  &lt;-------- [esp]</span><br><span class="line"> |      ....       |  低位地址</span><br></pre></td></tr></table></figure><p>最后一行的</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">*((<span class="type">uint32_t</span> *)tf - <span class="number">1</span>) = (<span class="type">uint32_t</span>) &amp;switchk2u;</span><br></pre></td></tr></table></figure><p>比较巧妙，在这句执行完毕后，函数就返回到了</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">pushl %esp</span><br><span class="line">call trap</span><br><span class="line">popl %esp # &lt;------ 返回到此处</span><br></pre></td></tr></table></figure><p>而此时的栈变为了</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">+|    栈底方向     |  高位地址</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |</span><br><span class="line"> |      ....       |  &lt;-------- [tf]（tf 即原来的 %esp）</span><br><span class="line"> |   &amp;switchk2u    |  &lt;-------- [esp]</span><br><span class="line"> |      ....       |  低位地址</span><br></pre></td></tr></table></figure><p>所以，<code>ESP</code> 马上就会被 <code>switchk2u</code> 的地址所覆盖，接下来寄存器的出栈，和段寄存器的修改，都会以我们刚刚修改的 <code>struct trapframe switchk2u</code> 的内容为准，因此也就完成了从内核态到用户态的切换。在 <code>gdb</code> 中也可以观察到：<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190326_104346.png" data-fancybox="gallery" data-caption="Screenshot_20190326_104346"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190326_104346.png" alt="Screenshot_20190326_104346"></a></p><p><a href="/university/ucore/ucore-os-lab-1/TIM%E6%88%AA%E5%9B%BE20190326191809.png" data-fancybox="gallery" data-caption="TIM截图20190326191809"><img src="/university/ucore/ucore-os-lab-1/TIM%E6%88%AA%E5%9B%BE20190326191809.png" alt="TIM截图20190326191809"></a></p><p>而在用户态切换到内核态的时候，中断处理例程的 <code>CS</code> 段选择子权限高于发起中断的程序，所以会发生栈的切换，<code>struct trapframe</code> 中的 <code>tf_esp</code> 就是原来的 <code>ESP</code> 寄存器，而 <code>SS</code> 段选择子已经在发生中断的时候切换到内核的段选择子了，无需我们手动设定。同时，<code>iret</code> 返回时，<code>tf_cs</code> 的特权级也已经变成了内核特权级，没有发生特权级的转换，所以 <code>SS</code> 和 <code>ESP</code> 都不会被栈的内容覆盖，也无需设定了。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> <span class="title">switchk2u</span>, *<span class="title">switchu2k</span>;</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">trap_dispatch</span><span class="params">(<span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    <span class="type">char</span> c;</span><br><span class="line">    <span class="keyword">switch</span> (tf-&gt;tf_trapno) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> T_SWITCH_TOK:</span><br><span class="line">        <span class="keyword">if</span> (tf-&gt;tf_cs == KERNEL_CS) <span class="keyword">return</span>;</span><br><span class="line">        tf-&gt;tf_cs = KERNEL_CS;</span><br><span class="line">        tf-&gt;tf_ds = tf-&gt;tf_es = KERNEL_DS;</span><br><span class="line">        tf-&gt;tf_eflags &amp;= ~FL_IOPL_MASK;</span><br><span class="line">        switchu2k = (<span class="keyword">struct</span> trapframe *) (tf-&gt;tf_esp - (<span class="keyword">sizeof</span>(<span class="keyword">struct</span> trapframe) - <span class="number">8</span>));</span><br><span class="line">        memmove(switchu2k, tf, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> trapframe) - <span class="number">8</span>);</span><br><span class="line">        *((<span class="type">uint32_t</span> *)tf - <span class="number">1</span>) = (<span class="type">uint32_t</span>) switchu2k;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码和切换到用户态类似，只不过少了 <code>tf_ss</code> 和 <code>tf_esp</code> 的拷贝。</p><p>完成代码后，执行 <code>make grade</code> ，可以看出我们的实现是正确的：</p><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190326_212545.png" data-fancybox="gallery" data-caption="Screenshot_20190326_212545"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190326_212545.png" alt="Screenshot_20190326_212545"></a></p><p>从上面的分析可以看出，调用切换到用户态的函数时，会多 <code>pop</code> 两个 32 位数，所以在调用前要先保护栈，不过由于在中断处理程序中我们已经将栈指针改到别的地方去了，所以其实不太需要。</p><p>增加一个系统调用用来获取当前时钟计数值，则是通过 <code>T_SYSCALL</code> 中断来实现，系统调用采用 <code>EAX</code> 寄存器传递调用号，这里就取 1 作为获取时钟计数的调用号了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// kern/init/init.c</span></span><br><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">get_ticks</span><span class="params">(<span class="type">void</span>)</span> &#123;</span><br><span class="line">    <span class="type">int</span> ticks;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">asm</span> <span class="title function_">volatile</span> <span class="params">(</span></span><br><span class="line"><span class="params">        <span class="string">&quot;movl %2, %%eax \n&quot;</span></span></span><br><span class="line"><span class="params">        <span class="string">&quot;int %1 \n&quot;</span></span></span><br><span class="line"><span class="params">        <span class="string">&quot;movl %%eax, %0 \n&quot;</span></span></span><br><span class="line"><span class="params">        <span class="string">&quot;movl %%ebp, %%esp \n&quot;</span></span></span><br><span class="line"><span class="params">        : <span class="string">&quot;=r&quot;</span>(ticks)</span></span><br><span class="line"><span class="params">        : <span class="string">&quot;i&quot;</span>(T_SYSCALL), <span class="string">&quot;i&quot;</span>(<span class="number">1</span>)</span></span><br><span class="line"><span class="params">        : <span class="string">&quot;%eax&quot;</span></span></span><br><span class="line"><span class="params">    )</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ticks;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// kern/trap/trap.c</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">trapframe</span> <span class="title">switchk2u</span>, *<span class="title">switchu2k</span>;</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">trap_dispatch</span><span class="params">(<span class="keyword">struct</span> trapframe *tf)</span> &#123;</span><br><span class="line">    <span class="type">char</span> c;</span><br><span class="line">    <span class="keyword">switch</span> (tf-&gt;tf_trapno) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="keyword">case</span> T_SYSCALL:</span><br><span class="line">        <span class="keyword">if</span> (tf-&gt;tf_cs == KERNEL_CS) &#123; <span class="keyword">return</span>; &#125;</span><br><span class="line">        <span class="keyword">if</span> (tf-&gt;tf_regs.reg_eax == <span class="number">1</span>)</span><br><span class="line">            tf-&gt;tf_regs.reg_eax = ticks;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后通过键盘上的“0”和“3”切换用户态和内核态的代码就是在主循环中加入：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">    <span class="type">int</span> c = getchar();</span><br><span class="line">    cprintf(<span class="string">&quot;[kbd %c] &quot;</span>, c);</span><br><span class="line">    <span class="keyword">switch</span> (c) &#123;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;0&#x27;</span>:</span><br><span class="line">            cprintf(<span class="string">&quot;Switch to kernel mode\n&quot;</span>);</span><br><span class="line">            lab1_switch_to_kernel();</span><br><span class="line">            lab1_print_cur_status();</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;3&#x27;</span>:</span><br><span class="line">            cprintf(<span class="string">&quot;Switch to user mode\n&quot;</span>);</span><br><span class="line">            lab1_switch_to_user();</span><br><span class="line">            lab1_print_cur_status();</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> <span class="string">&#x27;t&#x27;</span>:</span><br><span class="line">            cprintf(<span class="string">&quot;get_ticks(): %d\n&quot;</span>, get_ticks());</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190327_082634.png" data-fancybox="gallery" data-caption="Screenshot_20190327_082634"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190327_082634.png" alt="Screenshot_20190327_082634"></a></p><p>按下 “3” 后，切换到用户态，按下 “0” 后，切换到内核态，按下“t”后就获得当前时钟计数值并输出。</p><p>值得注意的是，在内核态中调用切换到内核态的函数不会有任何作用；但如果在用户态调用切换到用户态模式，则会导致权限不足引发 <code>General Proctection</code>：<a href="/university/ucore/ucore-os-lab-1/Screenshot_20190327_084425.png" data-fancybox="gallery" data-caption="Screenshot_20190327_084425"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190327_084425.png" alt="Screenshot_20190327_084425"></a><a href="/university/ucore/ucore-os-lab-1/Screenshot_20190327_084409.png" data-fancybox="gallery" data-caption="Screenshot_20190327_084409"><img src="/university/ucore/ucore-os-lab-1/Screenshot_20190327_084409.png" alt="Screenshot_20190327_084409"></a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Linux 网络编程 | 多连接 TCP 文件传输程序</title>
      <link>https://blog.howardlau.me/programming/linux-socket-programming-multi-connection-tcp-file-transfer.html</link>
      <description>
        <![CDATA[<p>这篇博客介绍在 Linux 下使用 <code>epoll</code> 和 Socket API 编写一个使用多条 TCP 连接来传输文件的 C&#x2F;S 模式程序。</p>
<h2 id="程序框架"><a href="#程序框架"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 17 Apr 2019 05:24:59 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>这篇博客介绍在 Linux 下使用 <code>epoll</code> 和 Socket API 编写一个使用多条 TCP 连接来传输文件的 C&#x2F;S 模式程序。</p><h2 id="程序框架"><a href="#程序框架" class="headerlink" title="程序框架"></a>程序框架</h2><p>对于这个程序，分为服务端和客户端。服务端负责发送文件，客户端负责接收文件。客户端首先向服务端请求文件元信息，如文件名、大小等，然后根据文件大小，同时建立多条的 TCP 连接进行分块下载，达到加速文件传输的目的。</p><p>完整的项目代码可以到 <a href="https://github.com/howardlau1999/tcp-file-transfer">https://github.com/howardlau1999/tcp-file-transfer</a> 获取。</p><h2 id="Socket-API-的使用"><a href="#Socket-API-的使用" class="headerlink" title="Socket API 的使用"></a>Socket API 的使用</h2><p>对于服务端而言，其最主要的功能是监听端口，因此封装一个 <code>listen_port()</code> 函数如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">listen_port</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *PORT)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">addrinfo</span> <span class="title">hints</span>, *<span class="title">servinfo</span>, *<span class="title">p</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="type">const</span> <span class="type">int</span> BACKLOG = MAX_CONNS;</span><br><span class="line">    <span class="type">int</span> rv, fd;</span><br><span class="line">    <span class="type">int</span> yes = <span class="number">1</span>;</span><br><span class="line">    <span class="built_in">memset</span>(&amp;hints, <span class="number">0</span>, <span class="keyword">sizeof</span> hints);</span><br><span class="line">    hints.ai_family = AF_UNSPEC;</span><br><span class="line">    hints.ai_socktype = SOCK_STREAM;</span><br><span class="line">    hints.ai_flags = AI_PASSIVE;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((rv = getaddrinfo(<span class="literal">NULL</span>, PORT, &amp;hints, &amp;servinfo)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;getaddrinfo: %s\n&quot;</span>, gai_strerror(rv));</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (p = servinfo; p != <span class="literal">NULL</span>; p = p-&gt;ai_next) &#123;</span><br><span class="line">        <span class="keyword">if</span> ((fd = socket(p-&gt;ai_family, p-&gt;ai_socktype, p-&gt;ai_protocol)) == <span class="number">-1</span>) &#123;</span><br><span class="line">            perror(<span class="string">&quot;server: socket error&quot;</span>);</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &amp;yes, <span class="keyword">sizeof</span>(<span class="type">int</span>)) == <span class="number">-1</span>) &#123;</span><br><span class="line">            perror(<span class="string">&quot;server: setsockopt error&quot;</span>);</span><br><span class="line">            <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (bind(fd, p-&gt;ai_addr, p-&gt;ai_addrlen) == <span class="number">-1</span>) &#123;</span><br><span class="line">            close(fd);</span><br><span class="line">            perror(<span class="string">&quot;server: bind error&quot;</span>);</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    freeaddrinfo(servinfo);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (p == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;server: failed to listen\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (listen(fd, BACKLOG) == <span class="number">-1</span>) &#123;</span><br><span class="line">        close(fd);</span><br><span class="line">        perror(<span class="string">&quot;server: listen error&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> fd;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了能优雅地支持 IPv4 和 IPv6，这里使用 <code>getaddrinfo()</code> 来获取可用的地址，调用之后会在 <code>servinfo</code> 中返回一个可用地址的链表，我们只需要依次尝试在这些地址上建立监听端口即可。</p><p>在尝试过程中，首先调用 <code>socket()</code> 获取一个套接字，然后设置 <code>SO_REUSEADDR</code> 方便快速重启，然后调用 <code>bind()</code> 尝试将套接字绑定到指定端口上，最后调用 <code>listen()</code> 函数监听套接字上的传入连接。</p><p>对于客户端而言，需要一个主动发起连接的函数 <code>connect_to()</code>：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">connect_to</span><span class="params">(<span class="type">const</span> <span class="type">char</span> *host, <span class="type">const</span> <span class="type">char</span> *port)</span> &#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">addrinfo</span> <span class="title">hints</span>, *<span class="title">servinfo</span>, *<span class="title">p</span>;</span></span><br><span class="line">    <span class="type">int</span> rv, fd;</span><br><span class="line">    <span class="type">char</span> s[INET6_ADDRSTRLEN];</span><br><span class="line">    <span class="built_in">memset</span>(&amp;hints, <span class="number">0</span>, <span class="keyword">sizeof</span> hints);</span><br><span class="line">    hints.ai_family = AF_UNSPEC;</span><br><span class="line">    hints.ai_socktype = SOCK_STREAM;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> ((rv = getaddrinfo(host, port, &amp;hints, &amp;servinfo)) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;getaddrinfo: %s\n&quot;</span>, gai_strerror(rv));</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (p = servinfo; p != <span class="literal">NULL</span>; p = p-&gt;ai_next) &#123;</span><br><span class="line">        <span class="keyword">if</span> ((fd = socket(p-&gt;ai_family, p-&gt;ai_socktype, p-&gt;ai_protocol)) == <span class="number">-1</span>) &#123;</span><br><span class="line">            perror(<span class="string">&quot;client: socket error&quot;</span>);</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (connect(fd, p-&gt;ai_addr, p-&gt;ai_addrlen) == <span class="number">-1</span>) &#123;</span><br><span class="line">            close(fd);</span><br><span class="line">            perror(<span class="string">&quot;client: connect error&quot;</span>);</span><br><span class="line">            <span class="keyword">continue</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (p == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">&quot;client: failed to connect\n&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">2</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    freeaddrinfo(servinfo);</span><br><span class="line">    <span class="keyword">return</span> fd;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同样的，首先使用 <code>getaddrinfo()</code> 获取可用的地址信息，然后调用 <code>socket()</code> 创建一个套接字，不同的是，客户端无需手动 <code>bind()</code>，在 <code>connect()</code> 的时候操作系统会自动给程序分配一个可用的端口。</p><p>有了这两个函数，就能很方便的同时监听多个端口和连接到不同地址了。</p><h2 id="epoll-的使用"><a href="#epoll-的使用" class="headerlink" title="epoll 的使用"></a><code>epoll</code> 的使用</h2><p>在 Linux 2.5.44 之后，增加了一个 <code>epoll</code> 的系统调用，这个系统调用可以实现高效的非阻塞网络 IO，使用也非常简单，主要分为三个步骤：</p><ol><li>创建一个 <code>epoll</code> 文件描述符</li><li>添加关心的事件到 <code>epoll</code> 文件描述符中</li><li>等待关心的事件发生，然后处理</li></ol><p>相比于 <code>select</code>，<code>epoll</code> 只返回发生了事件的文件描述符集，使得我们可以不再需要遍历所有的文件描述符，因此在有大量的连接而只有少数是活跃的时候，<code>epoll</code> 会非常高效。</p><p>对应上面三个步骤是三个系统调用：</p><ol><li><code>epoll</code> 文件描述符创建 <code>epoll_create1(int flags)</code></li><li><code>epoll</code> 关心事件操作 <code>epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)</code></li><li><code>epoll</code> 等待事件发生 <code>epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);</code></li></ol><p>使用 <code>epoll</code> 编写网络程序的时候一般有如下框架：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_EVENTS 10</span></span><br><span class="line"><span class="comment">/* 创建监听套接字 */</span></span><br><span class="line"><span class="type">int</span> listen_fd = listen_port(port);</span><br><span class="line"><span class="comment">/* 步骤 1：创建 epoll 文件描述符 */</span></span><br><span class="line"><span class="type">int</span> epoll_fd = epoll_create1(<span class="number">0</span>);</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">event</span>;</span></span><br><span class="line"><span class="comment">/* 填入关心的事件，EPOLLIN 表示可读，EPOLLOUT 表示可写，EPOLLRDHUP 表示关闭，EPOLLET 表示边缘触发（Edge Trigger），EPOLLLT 表示水平触发（Level Trigger） */</span></span><br><span class="line">event.events = EPOLLIN; </span><br><span class="line"><span class="comment">/* 这里的 data 是一个 union，在事件发生的时候会随着事件一起返回 */</span></span><br><span class="line">event.data.fd = listen_fd;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 步骤 2：将关心的事件添加到列表中 */</span></span><br><span class="line">epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &amp;event);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">events</span>[<span class="title">MAX_EVENTS</span>];</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line"><span class="comment">/* 步骤 3：等待事件的发生，如果没有事件发生则会阻塞，直到有事件发生或者超时 */</span></span><br><span class="line"><span class="type">int</span> nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, <span class="number">-1</span>);</span><br><span class="line"><span class="comment">/* 遍历事件列表 */</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; nfds; ++i) &#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">ev</span> =</span> events[i];</span><br><span class="line"><span class="keyword">if</span> (ev.data.fd == listen_fd) &#123;</span><br><span class="line"><span class="comment">/* 接受新连接... */</span></span><br><span class="line"><span class="keyword">continue</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (ev.events &amp; EPOLLIN) &#123;</span><br><span class="line"><span class="comment">/* 有数据传入，处理数据 */</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (ev.events &amp; EPOLLOUT) &#123;</span><br><span class="line"><span class="comment">/* 缓冲区有空闲了，可以发送数据 */</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><p>可以看到，使用 <code>epoll</code> 的程序最终都会进入一个主循环不断等待事件的发生并处理这些事件，这样的循环也叫做事件循环（<code>Event Loop</code>）</p><p>根据这个框架，对于服务端而言，就是不断查看 <code>listen_fd</code> 上有没有新的连接传入，如果有，那就调用 <code>accept4()</code> 系统调用接受新的连接。然后监听这个连接上的事件并处理。</p><p>需要注意的是，对于监听套接字，不能使用 <code>EPOLLET</code>，否则会漏掉连接。<code>EPOLLET</code> 和 <code>EPOLLLT</code> 的区别主要是：</p><ul><li><code>EPOLLET</code> 只在事件第一次发生的时候被触发，之后无论事件有没有被处理都不再触发，因此在调用 <code>recv()</code> 或者 <code>send()</code> 的时候，要循环调用至函数返回 <code>EAGAIN</code> 或者 <code>EWOULDBLOCK</code> 为止。</li><li><code>EPOLLLT</code> 只要有关心的事件还没被处理，就会一直触发，<code>send()</code> 和 <code>recv()</code> 没有处理完也没关系，下一次还是会被通知到。</li></ul><p>在这个例子里，也就是说如果监听套接字上本来没有连接传入，这时候有新的连接传入了，对于 <code>EPOLLET</code> 而言，会通知事件循环。然而，调用 <code>accept4()</code> 只能处理一个连接，如果这时候有多个连接同时传入，那么第一个连接处理完之后，后面没有被处理的连接就会被忽略掉。因此，对于监听套接字而言，需要使用 <code>EPOLLLT</code> 电平触发。</p><p>因此，处理监听套接字上的连接可以这样操作：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (event.data.fd == server_fd &amp;&amp; (event.events &amp; EPOLLIN)) &#123;</span><br><span class="line"><span class="type">int</span> new_fd =</span><br><span class="line">accept4(server_fd, (<span class="keyword">struct</span> sockaddr *)&amp;clients_addr,</span><br><span class="line">&amp;sin_size, SOCK_NONBLOCK);</span><br><span class="line">send(new_fd, &amp;meta, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> filemetadata), <span class="number">0</span>);</span><br><span class="line">close(new_fd);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里接受连接之后，马上发送文件的元信息给客户端，客户端接下来就会发起请求到数据连接的监听端口，并且发送文件分块请求信息，这时候，我们就可以利用 <code>event.data.ptr</code> 来存储相关的信息，以供后面发送文件块使用。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (event.data.fd == data_fd &amp;&amp; (event.events &amp; EPOLLIN)) &#123;</span><br><span class="line"><span class="type">int</span> new_fd = accept4(data_fd, (<span class="keyword">struct</span> sockaddr *)&amp;clients_addr,</span><br><span class="line"> &amp;sin_size, SOCK_NONBLOCK);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">request</span> *<span class="title">req</span> =</span> <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="keyword">struct</span> request));</span><br><span class="line">req-&gt;fd = new_fd;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> <span class="title">data_event</span> =</span> &#123;<span class="number">0</span>&#125;;</span><br><span class="line">data_event.events = EPOLLIN | EPOLLRDHUP;</span><br><span class="line">data_event.data.ptr = req;</span><br><span class="line"></span><br><span class="line">epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &amp;data_event);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于新的连接，我们首先期望收到请求文件块的信息，所以设置了 <code>EPOLLIN</code>。而收到了请求块之后，将请求信息保存下来，然后就可以开始根据请求信息开始发送数据了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (event.events &amp; EPOLLIN) &#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">request</span> *<span class="title">req</span> =</span> event.data.ptr;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">request</span> <span class="title">req_recv</span>;</span></span><br><span class="line"><span class="type">int</span> fd = req-&gt;fd;</span><br><span class="line">recv(fd, &amp;req_recv, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> request), <span class="number">0</span>);</span><br><span class="line">req-&gt;length = req_recv.length;</span><br><span class="line">req-&gt;offset = req_recv.offset;</span><br><span class="line">req-&gt;progress.written = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">event.events = EPOLLOUT | EPOLLRDHUP | EPOLLET;</span><br><span class="line">epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &amp;event);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们关心的只有两个信息：文件的偏移量和文件块的大小，保存下来之后，我们就可以关心 <code>EPOLLOUT</code> 事件了，也就是等待套接字可写。这里等待套接字可写的意思其实是等待缓冲区有空位，而设置 <code>EPOLLET</code> 是为了避免缓冲区空而又没有数据可以写的时候一直通知造成忙等待。</p><p>之后当套接字可写的时候，我们就根据当前进度使劲往里面写入要发送的文件数据，直到写满缓冲区为止，然后记录我们新的进度，看看发送完了没有：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (event.events &amp; EPOLLOUT) &#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">request</span> *<span class="title">req</span> =</span> event.data.ptr;</span><br><span class="line"><span class="type">int</span> fd = req-&gt;fd;</span><br><span class="line">fseek(fp, req-&gt;offset + req-&gt;progress.written, SEEK_SET);</span><br><span class="line"><span class="type">int</span> size = fread(</span><br><span class="line">buffer, <span class="number">1</span>,</span><br><span class="line">MIN(BUFFER_LEN, req-&gt;length - req-&gt;progress.written), fp);</span><br><span class="line"><span class="keyword">while</span> ((n = send(fd, buffer, size, <span class="number">0</span>)) &gt; <span class="number">0</span> &amp;&amp; size &gt; <span class="number">0</span> &amp;&amp;</span><br><span class="line">   req-&gt;progress.written &lt; req-&gt;length) &#123;</span><br><span class="line">req-&gt;progress.written += n;</span><br><span class="line">size -= n;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (req-&gt;progress.written == req-&gt;length) &#123;</span><br><span class="line"><span class="built_in">free</span>(req);</span><br><span class="line">close(fd);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于 <code>epoll</code> 而言，当一个文件描述符被关闭的时候，也就会自动被移除出关心列表，所以这里可以不使用 <code>epoll_ctl()</code> 手动删除。</p><p>服务端的程序主要的逻辑就是这些，总结一下就是：</p><ol><li>监听连接</li><li>接受连接</li><li>收发数据</li></ol><p>有了服务端程序，客户端程序也就依葫芦画瓢，水到渠成了：</p><ol><li>发起连接</li><li>收发数据</li></ol><p>完整的项目代码可以到 <a href="https://github.com/howardlau1999/tcp-file-transfer">https://github.com/howardlau1999/tcp-file-transfer</a> 获取。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>循环神经网络小项目 | 七绝作诗</title>
      <link>https://blog.howardlau.me/machine-learning/rnn-poet.html</link>
      <description>
        <![CDATA[<h2 id="数据集"><a href="#数据集" class="headerlink" title="数据集"></a>数据集</h2><p>做深度的第一步就是要收集炼丹原料准备数据集，这里我选择了 GitHub 上的这个项目 <a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Tue, 25 Dec 2018 08:34:18 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="数据集"><a href="#数据集" class="headerlink" title="数据集"></a>数据集</h2><p>做深度的第一步就是要收集炼丹原料准备数据集，这里我选择了 GitHub 上的这个项目 <a href="https://github.com/chinese-poetry/chinese-poetry">chinese-poetry</a> 然后取其中的唐诗部分，并去除含有现在字库缺少的字的唐诗，将诗中一些注解删除（比如一字多解），再利用正则匹配提取出七绝唐诗，将每个汉字拆成单独的词，在唐诗开头和结尾添加 <code>&lt;SOP&gt;</code> 和 <code>&lt;EOP&gt;</code> 符号标志开始和结束，一共准备了 10922 首七绝唐诗作为训练数据。</p><p>数据集长这样：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;SOP&gt; 中 管 五 弦 初 半 曲 ， 遙 教 合 上 隔 簾 聽 。 一 聲 聲 向 天 頭 落 ， 效 得 仙 人 夜 唱 經 。 &lt;EOP&gt;</span><br><span class="line">&lt;SOP&gt; 自 直 梨 園 得 出 稀 ， 更 番 上 曲 不 教 歸 。 一 時 跪 拜 霓 裳 徹 ， 立 地 階 前 賜 紫 衣 。 &lt;EOP&gt;</span><br><span class="line">&lt;SOP&gt; 旋 翻 新 譜 聲 初 足 ， 除 却 梨 園 未 教 人 。 宣 與 書 家 分 手 寫 ， 中 官 走 馬 賜 功 臣 。 &lt;EOP&gt;</span><br><span class="line">&lt;SOP&gt; 伴 教 霓 裳 有 貴 妃 ， 從 初 直 到 曲 成 時 。 日 長 耳 裏 聞 聲 熟 ， 拍 數 分 毫 錯 總 知 。 &lt;EOP&gt;</span><br><span class="line">&lt;SOP&gt; 弦 索 摐 摐 隔 綵 雲 ， 五 更 初 發 一 山 聞 。 武 皇 自 送 西 王 母 ， 新 換 霓 裳 月 色 裙 。 &lt;EOP&gt;</span><br></pre></td></tr></table></figure><h2 id="模型"><a href="#模型" class="headerlink" title="模型"></a>模型</h2><p>模型搭建非常简单，就是普通的双层单向 LSTM。每一个汉字首先经过 Embedding 层，然后输入 LSTM，最后将 LSTM 的输出经过一个线性层解码得到输出的词。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BasicRNN</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, vocab_size, embedding_dim, hidden_dim, num_layers, dropout=<span class="number">0.5</span></span>):</span><br><span class="line">        <span class="built_in">super</span>(BasicRNN, <span class="variable language_">self</span>).__init__()</span><br><span class="line">        <span class="variable language_">self</span>.vocab_size = vocab_size</span><br><span class="line">        <span class="variable language_">self</span>.embedding_dim = embedding_dim</span><br><span class="line">        <span class="variable language_">self</span>.embed = nn.Embedding(vocab_size, embedding_dim)</span><br><span class="line">        <span class="variable language_">self</span>.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, dropout=dropout)</span><br><span class="line">        <span class="variable language_">self</span>.decode = nn.Linear(hidden_dim, vocab_size)</span><br><span class="line">        <span class="variable language_">self</span>.dropout = nn.Dropout(dropout)</span><br><span class="line">        <span class="variable language_">self</span>.num_layers = num_layers</span><br><span class="line">        <span class="variable language_">self</span>.hidden_dim = hidden_dim</span><br><span class="line">        <span class="variable language_">self</span>.init_weights()</span><br><span class="line">        </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x, hidden_states</span>):</span><br><span class="line">        embedding = <span class="variable language_">self</span>.dropout(<span class="variable language_">self</span>.embed(x))</span><br><span class="line">        output, (h_n, c_n) = <span class="variable language_">self</span>.lstm(embedding, hidden_states)</span><br><span class="line">        output = <span class="variable language_">self</span>.dropout(output)</span><br><span class="line">        bsz = output.size(<span class="number">1</span>)</span><br><span class="line">        decoded = <span class="variable language_">self</span>.decode(output.view(-<span class="number">1</span>, output.size(<span class="number">2</span>)))</span><br><span class="line">        decoded = decoded.view(-<span class="number">1</span>, bsz, <span class="variable language_">self</span>.vocab_size)</span><br><span class="line">        <span class="keyword">return</span> decoded, (h_n, c_n)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_weights</span>(<span class="params">self</span>):</span><br><span class="line">        initrange = <span class="number">0.1</span></span><br><span class="line">        <span class="variable language_">self</span>.embed.weight.data.uniform_(-initrange, initrange)</span><br><span class="line">        <span class="variable language_">self</span>.decode.bias.data.zero_()</span><br><span class="line">        <span class="variable language_">self</span>.decode.weight.data.uniform_(-initrange, initrange)</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">init_hidden</span>(<span class="params">self, bsz</span>):</span><br><span class="line">        weight = <span class="built_in">next</span>(<span class="variable language_">self</span>.parameters())</span><br><span class="line">        <span class="keyword">return</span> (weight.new_zeros(<span class="variable language_">self</span>.num_layers, bsz, <span class="variable language_">self</span>.hidden_dim),</span><br><span class="line">                weight.new_zeros(<span class="variable language_">self</span>.num_layers, bsz, <span class="variable language_">self</span>.hidden_dim))</span><br></pre></td></tr></table></figure><p>需要注意的是输出层不需要任何的激活函数，这里当时调试了好久，loss 无论如何都下降不了，后来发现是 <code>nn.CrossEntropyLoss</code> 会自带一个 <code>softmax</code> 激活函数。</p><h2 id="训练"><a href="#训练" class="headerlink" title="训练"></a>训练</h2><p>实际上这个作诗模型是一个语言模型（Language Model），为了简化操作，我用了 <code>torchtext</code> 中的 <code>BPTTIterator</code> 来生成 Mini Batch。</p><p>需要注意的是，隐藏层每次都需要和之前的历史记录分离开来，否则梯度会一直回传下去。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">train</span>(<span class="params">model, dataset, lr=<span class="number">1e-3</span>, epochs=<span class="number">10</span>, start=<span class="number">0</span>, save_per=<span class="number">1000</span>, debug=<span class="literal">False</span></span>):</span><br><span class="line">    train_iter = torchtext.data.BPTTIterator(</span><br><span class="line">            dataset,</span><br><span class="line">            batch_size=<span class="number">2048</span>,</span><br><span class="line">            bptt_len=<span class="number">33</span>,</span><br><span class="line">            device=device,</span><br><span class="line">            repeat=<span class="literal">False</span></span><br><span class="line">        )</span><br><span class="line">    vocab_size = <span class="built_in">len</span>(dataset.fields[<span class="string">&#x27;text&#x27;</span>].vocab)</span><br><span class="line">    criterion = nn.CrossEntropyLoss()</span><br><span class="line">    optimizer = optim.Adam(model.parameters(), lr=lr)</span><br><span class="line">    hidden = <span class="literal">None</span></span><br><span class="line">    total_loss = []</span><br><span class="line">    <span class="keyword">for</span> epoch <span class="keyword">in</span> <span class="built_in">range</span>(epochs):</span><br><span class="line">        epoch = epoch + start</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            model.train()</span><br><span class="line">            epoch_loss = []</span><br><span class="line">            train_iter.init_epoch()</span><br><span class="line">            <span class="keyword">for</span> i, batch <span class="keyword">in</span> <span class="built_in">enumerate</span>(tqdm(train_iter)):</span><br><span class="line">                <span class="keyword">if</span> hidden <span class="keyword">is</span> <span class="literal">None</span>:</span><br><span class="line">                    hidden = model.init_hidden(batch.batch_size)</span><br><span class="line">                <span class="keyword">else</span>:</span><br><span class="line">                    hidden = detach_hidden(hidden)</span><br><span class="line"></span><br><span class="line">                text, target = batch.text, batch.target</span><br><span class="line">                output, hidden = model(text, hidden)</span><br><span class="line">                optimizer.zero_grad()</span><br><span class="line">                loss = criterion(output.view(-<span class="number">1</span>, vocab_size), target.view(-<span class="number">1</span>))</span><br><span class="line">                loss.backward()</span><br><span class="line">                optimizer.step()</span><br><span class="line">                epoch_loss.append(loss.item())</span><br><span class="line"></span><br><span class="line">            epoch_loss = np.mean(epoch_loss)</span><br><span class="line">            total_loss.append(epoch_loss)</span><br><span class="line">            <span class="keyword">if</span> debug:</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Epoch %d Loss: %f&quot;</span> % (epoch, epoch_loss))</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&#x27;&#x27;</span>.join(generate_poem(model)))</span><br><span class="line">            <span class="keyword">elif</span> (epoch + <span class="number">1</span>) % <span class="number">10</span> == <span class="number">0</span>: </span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Epoch %d Loss: %f&quot;</span> % (epoch, epoch_loss))</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&#x27;&#x27;</span>.join(generate_poem(model)))</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&#x27;&#x27;</span>.join(generate_poem(model, <span class="literal">True</span>)))</span><br><span class="line">                <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;loss.log&quot;</span>, <span class="string">&quot;a&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">                    f.write(<span class="string">&quot;Epoch %d Loss: %f\n&quot;</span> % (epoch, epoch_loss))</span><br><span class="line">                    f.write(<span class="string">&#x27;&#x27;</span>.join(generate_poem(model)) + <span class="string">&#x27;\n&#x27;</span>)</span><br><span class="line">                    f.write(<span class="string">&#x27;&#x27;</span>.join(generate_poem(model, <span class="literal">True</span>)) + <span class="string">&#x27;\n&#x27;</span>)</span><br><span class="line">            <span class="keyword">if</span> (epoch + <span class="number">1</span>) % save_per == <span class="number">0</span> <span class="keyword">or</span> epoch == <span class="number">0</span> <span class="keyword">and</span> <span class="keyword">not</span> debug:</span><br><span class="line">                torch.save(model.state_dict(), <span class="string">&quot;model_&#123;0:d&#125;.pth&quot;</span>.<span class="built_in">format</span>(epoch))</span><br><span class="line">        <span class="keyword">except</span> KeyboardInterrupt:</span><br><span class="line">            torch.save(model.state_dict(), <span class="string">&quot;model_&#123;0:d&#125;.pth&quot;</span>.<span class="built_in">format</span>(epoch))</span><br><span class="line">            <span class="keyword">return</span> total_loss</span><br><span class="line">    <span class="keyword">return</span> total_loss</span><br></pre></td></tr></table></figure><p>我采用的训练配置是：</p><ul><li><code>batch_size=2048</code></li><li><code>BPTT=33</code></li><li><code>embedding_size=300</code></li><li><code>num_layers=2</code></li><li><code>hidden_dim=1024</code></li></ul><p>该配置在 NVIDIA GTX Titan X 上消耗大约 11GB 显存，约 10 秒运行一个 <code>epoch</code>，最终花了两天时间训练了 15755 个 <code>epoch</code>。</p><h2 id="生成"><a href="#生成" class="headerlink" title="生成"></a>生成</h2><p>生成唐诗则是传进去一个 <code>&lt;SOP&gt;</code> 标识符，然后将上一时刻的输出作为下一时刻的输入，直到遇到 <code>&lt;EOP&gt;</code> 则停止生成。</p><p>从输出中生成古诗有两种办法，一种是直接取概率最大的字作为输出，另一种则是按照概率随机采样。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">generate_poem</span>(<span class="params">model, sample=<span class="literal">False</span></span>):</span><br><span class="line">    model.<span class="built_in">eval</span>()</span><br><span class="line">    idx = TEXT.vocab.stoi[<span class="string">&quot;&lt;SOP&gt;&quot;</span>]</span><br><span class="line">    x = torch.Tensor([idx]).view(<span class="number">1</span>, <span class="number">1</span>).long().to(device)</span><br><span class="line">    poem = []</span><br><span class="line">    hidden = model.init_hidden(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">with</span> torch.no_grad():</span><br><span class="line">        <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(<span class="number">128</span>):</span><br><span class="line">            output, hidden = model(x, hidden)</span><br><span class="line">            output = output.view(model.vocab_size)</span><br><span class="line">            <span class="keyword">if</span> sample:</span><br><span class="line">                probs = F.softmax(output, dim=<span class="number">0</span>).cpu().numpy()</span><br><span class="line">                probs /= probs.<span class="built_in">sum</span>()</span><br><span class="line">                idx = np.random.choice(<span class="built_in">range</span>(model.vocab_size), p=probs)</span><br><span class="line">            <span class="keyword">else</span>:</span><br><span class="line">                idx = torch.argmax(output)</span><br><span class="line">            <span class="keyword">if</span> idx == TEXT.vocab.stoi[<span class="string">&quot;&lt;EOP&gt;&quot;</span>]: <span class="keyword">break</span></span><br><span class="line">            poem.append(TEXT.vocab.itos[idx])</span><br><span class="line">            x = torch.Tensor([idx]).view(<span class="number">1</span>, <span class="number">1</span>).long().to(device)</span><br><span class="line">    <span class="keyword">return</span> poem</span><br></pre></td></tr></table></figure><p>最终生成的古诗效果如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Epoch 15729 Loss: 1.304158</span><br><span class="line">君不到山無處物，一生無事與身閑。人間盡說逢花雨，不是一生一覺塵。</span><br><span class="line">如中百歲曾留得，遙看還鄉夢覺看。不知日夜東山遠，獨照紅塵滿地花。</span><br><span class="line">Epoch 15739 Loss: 1.304422</span><br><span class="line">興不見君心不知，空留一鶴到山邊。莫言花重船應沒，自有人間不不知。</span><br><span class="line">看花莫羨新條在，花裏人呼萬古同。聞道不堪猶自異，兩頭分上一枝梅。</span><br><span class="line">Epoch 15749 Loss: 1.305263</span><br><span class="line">興不見君來未歸，今朝同向五湖中。相逢一宿最高寺，半夜不知何處去。</span><br><span class="line">今年閑向人中見，已見臨川五月月。青山山下何日期，西林宿竹獨相思。</span><br></pre></td></tr></table></figure><p>完整代码和预训练的模型：<a href="https://github.com/howardlau1999/char-rnn-poet">https://github.com/howardlau1999/char-rnn-poet</a></p><h2 id="改进"><a href="#改进" class="headerlink" title="改进"></a>改进</h2><ul><li>加 Attention</li><li>加平仄信息</li><li>改成 VAE 模型</li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Linux C/C++ 实现热更新</title>
      <link>https://blog.howardlau.me/programming/c-cpp-hot-reload.html</link>
      <description>
        <![CDATA[<p>有时候在服务器程序中，需要不停止运行程序但是又要更新一些程序代码，这时候可以借助动态加载库来实现功能的热更新，不过不是在编译的时候链接动态链接库，而是在程序中手动控制加载和卸载。</p>
<p>动态链接库的手动使用很简单，主要有四个]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 17 Dec 2018 22:56:02 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>有时候在服务器程序中，需要不停止运行程序但是又要更新一些程序代码，这时候可以借助动态加载库来实现功能的热更新，不过不是在编译的时候链接动态链接库，而是在程序中手动控制加载和卸载。</p><p>动态链接库的手动使用很简单，主要有四个 API：<code>dlopen()</code> 用于打开 <code>.so</code> 文件，<code>dlsym()</code> 用于加载符号，<code>dlclose()</code> 用来卸载链接库以及 <code>dlerror()</code> 用来输出错误信息。</p><p>这里我简单写了一个主程序，通过输入函数名，获取动态链接库中的符号，然后调用函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;dlfcn.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/stat.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">typedef</span> <span class="title">int</span> <span class="params">(*calc_func)</span><span class="params">(<span class="type">int</span>, <span class="type">int</span>)</span></span>;</span><br><span class="line"><span class="type">const</span> <span class="type">char</span> *LIB_PATH = <span class="string">&quot;./libctest.so&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">ino_t</span> so_ino = <span class="number">-1</span>;</span><br><span class="line">    <span class="type">void</span> *handle = <span class="literal">nullptr</span>;</span><br><span class="line">    <span class="type">char</span> *error = <span class="literal">nullptr</span>;</span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        string function_name;</span><br><span class="line">        cout &lt;&lt; <span class="string">&quot;What function (type `quit` to exit): &quot;</span>;</span><br><span class="line">        cin &gt;&gt; function_name;</span><br><span class="line">        <span class="keyword">if</span> (function_name == <span class="string">&quot;quit&quot;</span>) <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">struct</span> <span class="title class_">stat</span> attr;</span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">stat</span>(LIB_PATH, &amp;attr) == <span class="number">0</span> &amp;&amp; attr.st_ino != so_ino) &#123;</span><br><span class="line">            <span class="keyword">if</span> (handle) &#123;</span><br><span class="line">                <span class="built_in">dlclose</span>(handle);</span><br><span class="line">            &#125;</span><br><span class="line">            handle = <span class="built_in">dlopen</span>(LIB_PATH, RTLD_LAZY);</span><br><span class="line">            <span class="keyword">if</span> (!handle) &#123;</span><br><span class="line">                cerr &lt;&lt; <span class="built_in">dlerror</span>() &lt;&lt; endl;</span><br><span class="line">                <span class="built_in">exit</span>(EXIT_FAILURE);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            calc_func func = <span class="built_in">reinterpret_cast</span>&lt;calc_func&gt;(</span><br><span class="line">                <span class="built_in">dlsym</span>(handle, function_name.<span class="built_in">c_str</span>()));</span><br><span class="line">            <span class="keyword">if</span> (error = <span class="built_in">dlerror</span>()) &#123;</span><br><span class="line">                cerr &lt;&lt; error &lt;&lt; endl;</span><br><span class="line">                <span class="keyword">continue</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="type">int</span> a, b;</span><br><span class="line">                cout &lt;&lt; <span class="string">&quot;Input two numbers: &quot;</span>;</span><br><span class="line">                cin &gt;&gt; a &gt;&gt; b;</span><br><span class="line">                cout &lt;&lt; <span class="built_in">func</span>(a, b) &lt;&lt; endl;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">g++ -rdynamic -o main main.c -ldl</span><br></pre></td></tr></table></figure><p>这里使用了 <code>stat()</code> 函数来判断动态库是不是已经被更新过，还有一个需要注意的地方是 <code>dlclose()</code> 一定要在 <code>dlopen()</code> 之前调用，否则得到的是缓存版本的动态库。<code>RTLD_LAZY</code> 表示的是懒加载模式，直到请求一个符号之前都不会加载符号相关的信息，其中的技术是 <strong>GOT (Global Offset Table)</strong> 和 <strong>PLT (Procedure Linkage Table)</strong>，具体内容可以在 CSAPP 一书的第七章找到相关介绍。</p><p>而库的源代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// shared_sum.cpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">extern</span> <span class="string">&quot;C&quot;</span> &#123;</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">add</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span> </span>&#123; <span class="keyword">return</span> a + b; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里使用 <code>extern &quot;C&quot;</code> 关键字来导出符号，避免 C++ 的符号重整机制改变函数的名字。</p><p>然后使用的编译命令是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">g++ -fPIC -shared shared_sum.cpp -o libctest.so</span><br></pre></td></tr></table></figure><p>其中 <code>-fPIC</code> 选项代表生成位置无关代码，这是动态链接技术实现的关键，然后 <code>-shared</code> 表示生成动态链接符号，这样其他程序可以动态加载符号并调用其中的函数。</p><p>编译完成之后执行 <code>./main</code> 运行主程序，第一次调用函数的时候是这样的结果：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">What <span class="keyword">function</span> (<span class="built_in">type</span> `quit` to <span class="built_in">exit</span>): add</span><br><span class="line">Input two numbers: 3 4</span><br><span class="line">7</span><br></pre></td></tr></table></figure><p>然后我们不关闭主程序，修改 <code>shared_sum.cpp</code> 文件：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// shared_sum.cpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="keyword">extern</span> <span class="string">&quot;C&quot;</span> &#123;</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">add</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span> </span>&#123; <span class="keyword">return</span> a * b; &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重新生成 <code>.so</code> 文件，再到主程序中调用函数，得到结果：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">What <span class="keyword">function</span> (<span class="built_in">type</span> `quit` to <span class="built_in">exit</span>): add</span><br><span class="line">Input two numbers: 3 4</span><br><span class="line">12</span><br></pre></td></tr></table></figure><p>可以看到我们的修改生效了。</p><p>总结一下，就是用 <code>dlopen()</code>、<code>dlsym()</code>、<code>dlclose()</code> 等 API 实现模块的重新加载和符号的解析。具体的重加载时机和符号解析方式可以由我们自行决定。另外需要注意的一点是禁止在动态链接库中声明全局变量或者静态变量，因为每次重新加载的时候都会改变其内容。动态链接库中应当实现的是纯函数，只依赖于输入状态计算出输出状态。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>LeetCode | 173 二叉搜索树迭代器</title>
      <link>https://blog.howardlau.me/programming/leetcode/leetcode-173-bst-iterator.html</link>
      <description>
        <![CDATA[<p>实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。</p>
<p>调用 next() 将返回二叉搜索树中的下一个最小的数。</p>
<p>注意: next() 和hasNext() 操作的时间复杂度是O(1)，并使用 O(h) 内存，其中 h]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/leetcode/">LeetCode</category>
      <pubDate>Thu, 06 Dec 2018 04:14:06 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。</p><p>调用 next() 将返回二叉搜索树中的下一个最小的数。</p><p>注意: next() 和hasNext() 操作的时间复杂度是O(1)，并使用 O(h) 内存，其中 h 是树的高度。</p><p>其实就是二叉树的中序遍历，手动模拟函数递归调用时候的出入栈即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BSTIterator</span> &#123;</span><br><span class="line">    stack&lt;TreeNode*&gt; s;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">BSTIterator</span>(TreeNode *root) &#123;</span><br><span class="line">        TreeNode* cur = root;</span><br><span class="line">        <span class="keyword">while</span> (cur) &#123;</span><br><span class="line">            s.<span class="built_in">push</span>(cur);</span><br><span class="line">            cur = cur-&gt;left;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** @return whether we have a next smallest number */</span></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">hasNext</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> !s.<span class="built_in">empty</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** @return the next smallest number */</span></span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">next</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        TreeNode* cur = s.<span class="built_in">top</span>();</span><br><span class="line">        s.<span class="built_in">pop</span>();</span><br><span class="line">        TreeNode* tmp = cur-&gt;right;</span><br><span class="line">        <span class="keyword">while</span> (tmp) &#123;</span><br><span class="line">            s.<span class="built_in">push</span>(tmp);</span><br><span class="line">            tmp = tmp-&gt;left;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> cur-&gt;val;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>平衡二叉树 | 红黑树</title>
      <link>https://blog.howardlau.me/programming/balanced-binary-search-tree-red-black-tree.html</link>
      <description>
        <![CDATA[<p>注：下面这个版本基于《算法（第四版）》实现。这个红黑树是简化版本的。</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 06 Dec 2018 03:00:38 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>注：下面这个版本基于《算法（第四版）》实现。这个红黑树是简化版本的。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Color</span> &#123; RED, BLACK &#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">K</span>, <span class="keyword">class</span> <span class="title class_">V</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">RBTree</span> &#123;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">RBNode</span> &#123;</span><br><span class="line">        K key;</span><br><span class="line">        V value;</span><br><span class="line">        <span class="type">size_t</span> N;</span><br><span class="line">        Color color;</span><br><span class="line">        RBNode *left, *right;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">RBNode</span>(K key, V value, <span class="type">size_t</span> N, Color color)</span><br><span class="line">            : <span class="built_in">key</span>(key), <span class="built_in">value</span>(value), <span class="built_in">N</span>(N), <span class="built_in">color</span>(color), <span class="built_in">left</span>(<span class="literal">nullptr</span>), <span class="built_in">right</span>(<span class="literal">nullptr</span>) &#123;&#125;</span><br><span class="line">    &#125;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">using</span> Node = RBNode;</span><br><span class="line"></span><br><span class="line">    Node* root = <span class="literal">nullptr</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">const</span> <span class="type">size_t</span> <span class="title">size</span><span class="params">(<span class="type">const</span> Node* h)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!h) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">return</span> h-&gt;N;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">const</span> <span class="type">bool</span> <span class="title">isRed</span><span class="params">(<span class="type">const</span> Node* h)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (h == <span class="literal">nullptr</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">return</span> h-&gt;color == RED;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">put</span><span class="params">(Node* h, K <span class="type">const</span>&amp; key, V <span class="type">const</span>&amp; value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (h == <span class="literal">nullptr</span>) <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Node</span>(key, value, <span class="number">1</span>, RED);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (key &lt; h-&gt;key)</span><br><span class="line">            h-&gt;left = <span class="built_in">put</span>(h-&gt;left, key, value);</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (h-&gt;key &lt; key)</span><br><span class="line">            h-&gt;right = <span class="built_in">put</span>(h-&gt;right, key, value);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            h-&gt;value = value;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// A red link appears on the right</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">isRed</span>(h-&gt;right) &amp;&amp; !<span class="built_in">isRed</span>(h-&gt;left)) h = <span class="built_in">rotateLeft</span>(h);</span><br><span class="line">        <span class="comment">// Two continuous red links</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">isRed</span>(h-&gt;left) &amp;&amp; <span class="built_in">isRed</span>(h-&gt;left-&gt;left)) h = <span class="built_in">rotateRight</span>(h);</span><br><span class="line">        <span class="comment">// Red links on both sides</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="built_in">isRed</span>(h-&gt;left) &amp;&amp; <span class="built_in">isRed</span>(h-&gt;right)) <span class="built_in">flipColors</span>(h);</span><br><span class="line"></span><br><span class="line">        h-&gt;N = <span class="number">1</span> + <span class="built_in">size</span>(h-&gt;left) + <span class="built_in">size</span>(h-&gt;right);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> h;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">flipColors</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        h-&gt;left-&gt;color = h-&gt;right-&gt;color = BLACK;</span><br><span class="line">        h-&gt;color = RED;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotateLeft</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;right;</span><br><span class="line">        h-&gt;right = x-&gt;left;</span><br><span class="line">        x-&gt;left = h;</span><br><span class="line">        x-&gt;color = h-&gt;color;</span><br><span class="line">        x-&gt;N = h-&gt;N;</span><br><span class="line">        h-&gt;N = <span class="number">1</span> + <span class="built_in">size</span>(h-&gt;left) + <span class="built_in">size</span>(h-&gt;right);</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotateRight</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;left;</span><br><span class="line">        h-&gt;left = x-&gt;right;</span><br><span class="line">        x-&gt;right = h;</span><br><span class="line">        x-&gt;color = h-&gt;color;</span><br><span class="line">        x-&gt;N = h-&gt;N;</span><br><span class="line">        h-&gt;N = <span class="number">1</span> + <span class="built_in">size</span>(h-&gt;left) + <span class="built_in">size</span>(h-&gt;right);</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">get</span><span class="params">(Node* h, K <span class="type">const</span>&amp; key)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!h) <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (h-&gt;key &lt; key)</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">get</span>(h-&gt;left, key);</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (key &lt; h-&gt;key)</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">get</span>(h-&gt;left, key);</span><br><span class="line">        <span class="keyword">return</span> h;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">size_t</span> <span class="title">size</span><span class="params">()</span> </span>&#123; <span class="keyword">return</span> <span class="built_in">size</span>(root); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">put</span><span class="params">(K <span class="type">const</span>&amp; key, V <span class="type">const</span>&amp; value)</span> </span>&#123;</span><br><span class="line">        root = <span class="built_in">put</span>(root, key, value);</span><br><span class="line">        root-&gt;color = BLACK;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">V&amp; <span class="title">get</span><span class="params">(K <span class="type">const</span>&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* x = <span class="built_in">get</span>(root, key);</span><br><span class="line">        <span class="keyword">if</span> (x == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">            <span class="built_in">put</span>(key, V&#123;&#125;);</span><br><span class="line">            x = <span class="built_in">get</span>(root, key);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> x-&gt;value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    V&amp; <span class="keyword">operator</span>[](K <span class="type">const</span>&amp; key) &#123; <span class="keyword">return</span> <span class="built_in">get</span>(key); &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>平衡二叉树 | AVL 树</title>
      <link>https://blog.howardlau.me/programming/balanced-binary-search-tree-avl-tree.html</link>
      <description>
        <![CDATA[<p><a href="/programming/balanced-binary-search-tree-avl-tree/Tree_Rebalancing.png" data-fancybox="gallery" data-caption=""><img]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 06 Dec 2018 00:01:31 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="/programming/balanced-binary-search-tree-avl-tree/Tree_Rebalancing.png" data-fancybox="gallery" data-caption=""><img src="/programming/balanced-binary-search-tree-avl-tree/Tree_Rebalancing.png"></a></p><center>四种不平衡的情况</center><p>参考代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> K, <span class="keyword">typename</span> V&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AVLNode</span> &#123;</span><br><span class="line">    K key;</span><br><span class="line">    V value;</span><br><span class="line">    AVLNode *left, *right;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">AVLNode</span>(K key, V value) : <span class="built_in">key</span>(key), <span class="built_in">value</span>(value), <span class="built_in">left</span>(<span class="literal">NULL</span>), <span class="built_in">right</span>(<span class="literal">NULL</span>) &#123;&#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> K, <span class="keyword">typename</span> V&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AVLTree</span> &#123;</span><br><span class="line">    <span class="keyword">typedef</span> AVLNode&lt;K, V&gt;  Node;</span><br><span class="line"></span><br><span class="line">    Node* root;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">put</span><span class="params">(Node* h, K <span class="type">const</span>&amp; key, V <span class="type">const</span>&amp; value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (h == <span class="literal">nullptr</span>) <span class="keyword">return</span> <span class="keyword">new</span> <span class="built_in">Node</span>(key, value);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (key &lt; h-&gt;key)</span><br><span class="line">            h-&gt;left = <span class="built_in">put</span>(h-&gt;left, key, value);</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (h-&gt;key &lt; key)</span><br><span class="line">            h-&gt;right = <span class="built_in">put</span>(h-&gt;right, key, value);</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            h-&gt;value = value;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">balance</span>(h);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotationRR</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;right;</span><br><span class="line">        h-&gt;right = x-&gt;left;</span><br><span class="line">        x-&gt;left = h;</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotationLL</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;left;</span><br><span class="line">        h-&gt;left = x-&gt;right;</span><br><span class="line">        x-&gt;right = h;</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotationLR</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;left;</span><br><span class="line">        h-&gt;left = <span class="built_in">rotationRR</span>(x);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">rotationLL</span>(h);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">rotationRL</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        Node* x = h-&gt;right;</span><br><span class="line">        h-&gt;right = <span class="built_in">rotationLL</span>(x);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">rotationRR</span>(h);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">balance</span><span class="params">(Node* x)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> bal_factor = <span class="built_in">diff</span>(x);</span><br><span class="line">        <span class="keyword">if</span> (bal_factor &gt; <span class="number">1</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">diff</span>(x-&gt;left) &gt; <span class="number">0</span>)</span><br><span class="line">                x = <span class="built_in">rotationLL</span>(x);</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                x = <span class="built_in">rotationLR</span>(x);</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (bal_factor &lt; <span class="number">-1</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">diff</span>(x-&gt;right) &gt; <span class="number">0</span>)</span><br><span class="line">                x = <span class="built_in">rotationRL</span>(x);</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                x = <span class="built_in">rotationRR</span>(x);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">get</span><span class="params">(Node* h, K <span class="type">const</span>&amp; key)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!h) <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (h-&gt;key &lt; key)</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">get</span>(h-&gt;left, key);</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (key &lt; h-&gt;key)</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">get</span>(h-&gt;left, key);</span><br><span class="line">        <span class="keyword">return</span> h;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">height</span><span class="params">(Node* h)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!h) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">max</span>(<span class="built_in">height</span>(h-&gt;left), <span class="built_in">height</span>(h-&gt;right)) + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">diff</span><span class="params">(Node* h)</span> </span>&#123; <span class="keyword">return</span> <span class="built_in">height</span>(h-&gt;left) - <span class="built_in">height</span>(h-&gt;right); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">preorder</span><span class="params">(Node* node, <span class="type">void</span> (*visit)(V))</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!node) <span class="keyword">return</span>;</span><br><span class="line">        <span class="built_in">visit</span>(node-&gt;key);</span><br><span class="line">        <span class="built_in">preorder</span>(node-&gt;left, visit);</span><br><span class="line">        <span class="built_in">preorder</span>(node-&gt;right, visit);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">AVLTree</span>() : <span class="built_in">root</span>(<span class="literal">nullptr</span>) &#123;&#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">put</span><span class="params">(K <span class="type">const</span>&amp; key, V <span class="type">const</span>&amp; value)</span> </span>&#123; root = <span class="built_in">put</span>(root, key, value); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">preorder</span><span class="params">(<span class="type">void</span> (*visit)(V))</span> </span>&#123; <span class="built_in">preorder</span>(root, visit); &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">V&amp; <span class="title">get</span><span class="params">(K <span class="type">const</span>&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* x = <span class="built_in">get</span>(root, key);</span><br><span class="line">        <span class="keyword">if</span> (x == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">            <span class="built_in">put</span>(key, V&#123;&#125;);</span><br><span class="line">            x = <span class="built_in">get</span>(root, key);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> x-&gt;value;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    V&amp; <span class="keyword">operator</span>[](K <span class="type">const</span>&amp; key) &#123; <span class="keyword">return</span> <span class="built_in">get</span>(key); &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>C++11 字面量操作符</title>
      <link>https://blog.howardlau.me/programming/cpp-11-literal-operator.html</link>
      <description>
        <![CDATA[<p>在 C++ 11 中，引入了字面量操作符（literal operator），使用的方法就像下面这样：</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 05 Dec 2018 23:51:39 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在 C++ 11 中，引入了字面量操作符（literal operator），使用的方法就像下面这样：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">long</span> <span class="type">double</span> <span class="keyword">operator</span> <span class="string">&quot;&quot;</span> _w(<span class="type">long</span> <span class="type">double</span>);</span><br><span class="line">std::string <span class="keyword">operator</span> <span class="string">&quot;&quot;</span> _w(<span class="type">const</span> <span class="type">char16_t</span>*, <span class="type">size_t</span>);</span><br><span class="line"><span class="type">unsigned</span> <span class="keyword">operator</span> <span class="string">&quot;&quot;</span> _w(<span class="type">const</span> <span class="type">char</span>*);</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="number">1.2</span>_w; <span class="comment">// calls operator &quot;&quot; _w(1.2L)</span></span><br><span class="line">    <span class="string">u&quot;one&quot;</span>_w; <span class="comment">// calls operator &quot;&quot; _w(u&quot;one&quot;, 3)</span></span><br><span class="line">    <span class="number">12</span>_w; <span class="comment">// calls operator &quot;&quot; _w(&quot;12&quot;)</span></span><br><span class="line">    <span class="string">&quot;two&quot;</span>_w; <span class="comment">// error: no applicable literal operator</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个操作符也可以声明为模板函数，如果声明为了模板函数，参数列表必须为空，而且模板参数列表只能有一个参数——用来获取每一个字符的变长列表：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="type">char</span>...&gt; <span class="type">double</span> <span class="keyword">operator</span> <span class="string">&quot;&quot;</span> _x();</span><br></pre></td></tr></table></figure><p>通常情况下，这个也可以叫做数值字面量操作符。</p><p>有了这个操作符的帮助，可以做一些编译期的运算：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// convert single char to corresponding int value at compile time:</span></span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="type">int</span> <span class="title">toInt</span><span class="params">(<span class="type">char</span> c)</span> </span>&#123;</span><br><span class="line">  <span class="comment">// hexadecimal letters:</span></span><br><span class="line">  <span class="keyword">if</span> (c &gt;= <span class="string">&#x27;A&#x27;</span> &amp;&amp; c &lt;= <span class="string">&#x27;F&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(c) - <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(<span class="string">&#x27;A&#x27;</span>) + <span class="number">10</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (c &gt;= <span class="string">&#x27;a&#x27;</span> &amp;&amp; c &lt;= <span class="string">&#x27;f&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(c) - <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(<span class="string">&#x27;a&#x27;</span>) + <span class="number">10</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// other (disable &#x27;.&#x27; for floating-point literals):</span></span><br><span class="line">  <span class="built_in">assert</span>(c &gt;= <span class="string">&#x27;0&#x27;</span> &amp;&amp; c &lt;= <span class="string">&#x27;9&#x27;</span>);</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(c) - <span class="built_in">static_cast</span>&lt;<span class="type">int</span>&gt;(<span class="string">&#x27;0&#x27;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// parse array of chars to corresponding int value at compile time:</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;std::<span class="type">size_t</span> N&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> <span class="type">int</span> <span class="title">parseInt</span><span class="params">(<span class="type">char</span> <span class="type">const</span> (&amp;arr)[N])</span> </span>&#123;</span><br><span class="line">  <span class="type">int</span> base = <span class="number">10</span>;       <span class="comment">// to handle base (default: decimal)</span></span><br><span class="line">  <span class="type">int</span> offset = <span class="number">0</span>;      <span class="comment">// to skip prefixes like 0x</span></span><br><span class="line">  <span class="keyword">if</span> (N &gt; <span class="number">2</span> &amp;&amp; arr[<span class="number">0</span>] == <span class="string">&#x27;0&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">switch</span> (arr[<span class="number">1</span>]) &#123;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;x&#x27;</span>:        <span class="comment">// prefix 0x or 0X, so hexadecimal</span></span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;X&#x27;</span>:</span><br><span class="line">        base = <span class="number">16</span>;</span><br><span class="line">        offset = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;b&#x27;</span>:        <span class="comment">// prefix 0b or 0B (since C++14), so binary</span></span><br><span class="line">      <span class="keyword">case</span> <span class="string">&#x27;B&#x27;</span>:</span><br><span class="line">        base = <span class="number">2</span>;</span><br><span class="line">        offset = <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">      <span class="keyword">default</span>:         <span class="comment">// prefix 0, so octal</span></span><br><span class="line">        base = <span class="number">8</span>;</span><br><span class="line">        offset = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// iterate over all digits and compute resulting value:</span></span><br><span class="line">  <span class="type">int</span> value = <span class="number">0</span>;</span><br><span class="line">  <span class="type">int</span> multiplier = <span class="number">1</span>;</span><br><span class="line">  <span class="keyword">for</span> (std::<span class="type">size_t</span> i = <span class="number">0</span>; i &lt; N - offset; ++i) &#123;</span><br><span class="line">    <span class="keyword">if</span> (arr[N<span class="number">-1</span>-i] != <span class="string">&#x27;\&#x27;&#x27;</span>) &#123; <span class="comment">// ignore separating single quotes (e.g. in 1&#x27;000)</span></span><br><span class="line">      value += <span class="built_in">toInt</span>(arr[N<span class="number">-1</span>-i]) * multiplier;</span><br><span class="line">      multiplier *= base;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> value;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T, T Value&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">CTValue</span></span><br><span class="line">&#123; </span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> T value = Value;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// literal operator: parse integral literals with suffix _c as sequence of chars:</span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="type">char</span>... cs&gt;</span><br><span class="line"><span class="keyword">constexpr</span> <span class="keyword">auto</span> <span class="keyword">operator</span><span class="string">&quot;&quot;</span> _c() &#123;</span><br><span class="line">  <span class="keyword">return</span> CTValue&lt;<span class="type">int</span>, <span class="built_in">parseInt</span>&lt;<span class="keyword">sizeof</span>...(cs)&gt;(&#123;cs...&#125;)&gt;&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>C++ 模板元编程 | 快速排序</title>
      <link>https://blog.howardlau.me/programming/cpp-templates-metaprogramming-quick-sort.html</link>
      <description>
        <![CDATA[<p>有了之前<a href="https://howardlau.me/programming/cpp-templates-metaprogramming-insertion-sort.html">插入排序</a>的基础，实现快速排序不是一件难事，快排的算法用]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sun, 02 Dec 2018 07:50:58 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>有了之前<a href="https://howardlau.me/programming/cpp-templates-metaprogramming-insertion-sort.html">插入排序</a>的基础，实现快速排序不是一件难事，快排的算法用 <code>Haskell</code> 描述就像是下面这样的：</p><figure class="highlight haskell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">QuickSort</span> [] = []</span><br><span class="line"><span class="type">QuickSort</span> (x:xs) = <span class="type">QuickSort</span> [a | a &lt;- xs, a &lt; x] ++ [x] ++ <span class="type">QuickSort</span> [a | a &lt;- xs, a &gt;= x]</span><br></pre></td></tr></table></figure><p>可以看出来，我们还需要补充的列表操作是 <code>Concat</code>，把两个表连接起来，还有 <code>Filter</code>，过滤一个表的元素，实现思路很简单：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ListA</span>, <span class="keyword">class</span> <span class="title class_">ListB</span>, <span class="keyword">class</span>... Tail&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ConcatT</span> &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... ValuesA, T... ValuesB, <span class="keyword">class</span>... Tail&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ConcatT</span>&lt;ValueList&lt;T, ValuesA...&gt;, ValueList&lt;T, ValuesB...&gt;, Tail...&gt;</span><br><span class="line">    : ConcatT&lt;ValueList&lt;T, ValuesA..., ValuesB...&gt;, Tail...&gt; &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... ValuesA, T... ValuesB&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ConcatT</span>&lt;ValueList&lt;T, ValuesA...&gt;, ValueList&lt;T, ValuesB...&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = ValueList&lt;T, ValuesA..., ValuesB...&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ListA</span>, <span class="keyword">class</span> <span class="title class_">ListB</span>, <span class="keyword">class</span>... Tail&gt;</span><br><span class="line"><span class="keyword">using</span> Concat = <span class="keyword">typename</span> ConcatT&lt;ListA, ListB, Tail...&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>,</span><br><span class="line">          <span class="type">bool</span> = IsEmpty&lt;List&gt;::value&gt;</span><br><span class="line"><span class="keyword">struct</span> FilterT &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>,</span><br><span class="line">          <span class="type">bool</span> = IsEmpty&lt;List&gt;::value&gt;</span><br><span class="line"><span class="keyword">using</span> Filter = <span class="keyword">typename</span> FilterT&lt;List, PredicateT&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">FilterT</span>&lt;List, PredicateT, <span class="literal">false</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> NewTail = Filter&lt;PopFront&lt;List&gt;, PredicateT&gt;;</span><br><span class="line">    <span class="keyword">using</span> NewHead = Front&lt;List&gt;;</span><br><span class="line">    <span class="keyword">using</span> Type = IfThenElse&lt;PredicateT&lt;NewHead&gt;::value,</span><br><span class="line">                            PushFront&lt;NewTail, NewHead&gt;, NewTail&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">FilterT</span>&lt;List, PredicateT, <span class="literal">true</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = List;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>然后根据 <code>Haskell</code> 代码写出排序算法：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>,</span><br><span class="line">          <span class="type">bool</span> = IsEmpty&lt;List&gt;::value&gt;</span><br><span class="line"><span class="keyword">struct</span> PartitionT &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">PartitionT</span>&lt;List, PredicateT, <span class="literal">false</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">NegationT</span> &#123;</span><br><span class="line">        <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = !PredicateT&lt;T&gt;::value;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">using</span> Left = Filter&lt;List, PredicateT&gt;;</span><br><span class="line">    <span class="keyword">using</span> Right = Filter&lt;List, NegationT&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">PredicateT</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">PartitionT</span>&lt;List, PredicateT, <span class="literal">true</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Left = List;</span><br><span class="line">    <span class="keyword">using</span> Right = List;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>, <span class="type">bool</span> = IsEmpty&lt;List&gt;::value&gt;</span><br><span class="line"><span class="keyword">struct</span> QuickSortT &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>, <span class="type">bool</span> = IsEmpty&lt;List&gt;::value&gt;</span><br><span class="line"><span class="keyword">using</span> QuickSort = <span class="keyword">typename</span> QuickSortT&lt;List, Compare&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">QuickSortT</span>&lt;List, Compare, <span class="literal">false</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Pilot = Front&lt;List&gt;;</span><br><span class="line">    <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">    <span class="keyword">using</span> PredicateT = Compare&lt;T, Front&lt;List&gt;&gt;;</span><br><span class="line">    <span class="keyword">using</span> Partitioned = PartitionT&lt;PopFront&lt;List&gt;, PredicateT&gt;;</span><br><span class="line">    <span class="keyword">using</span> SortedLeft = QuickSort&lt;<span class="keyword">typename</span> Partitioned::Left, Compare&gt;;</span><br><span class="line">    <span class="keyword">using</span> SortedRight = QuickSort&lt;<span class="keyword">typename</span> Partitioned::Right, Compare&gt;;</span><br><span class="line">    <span class="keyword">using</span> Type = Concat&lt;SortedLeft, ValueList&lt;<span class="keyword">typename</span> Pilot::Type, Pilot::value&gt;, SortedRight&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">List</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">QuickSortT</span>&lt;List, Compare, <span class="literal">true</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = List;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>用插入排序的主函数测试一下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">using</span> TestList = ValueList&lt;<span class="type">int</span>, <span class="number">17</span>, <span class="number">1</span>, <span class="number">15</span>, <span class="number">9</span>, <span class="number">8</span>, <span class="number">19</span>, <span class="number">16</span>, <span class="number">10</span>, <span class="number">11</span>, <span class="number">7</span>, <span class="number">4</span>, <span class="number">14</span>,</span><br><span class="line">                               <span class="number">18</span>, <span class="number">13</span>, <span class="number">3</span>, <span class="number">12</span>, <span class="number">2</span>, <span class="number">5</span>, <span class="number">6</span>, <span class="number">20</span>&gt;;</span><br><span class="line">    <span class="keyword">using</span> SortedList = QuickSort&lt;TestList, LessThanT&gt;;</span><br><span class="line">    <span class="keyword">using</span> ReverseSortedList = QuickSort&lt;TestList, GreaterThanT&gt;;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Before sorted&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">TestList</span>());</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;After sorted (from small to great)&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">SortedList</span>());</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;After sorted (from great to small)&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">ReverseSortedList</span>());</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>程序的输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Before sorted</span><br><span class="line">17 1 15 9 8 19 16 10 11 7 4 14 18 13 3 12 2 5 6 20</span><br><span class="line">After sorted (from small to great)</span><br><span class="line">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20</span><br><span class="line">After sorted (from great to small)</span><br><span class="line">20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1</span><br></pre></td></tr></table></figure><p>完整代码片段：<a href="https://gist.github.com/howardlau1999/cacaf41c83511cfd591a40aa2527ec3e">https://gist.github.com/howardlau1999/cacaf41c83511cfd591a40aa2527ec3e</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>C++ 模板元编程 | 插入排序</title>
      <link>https://blog.howardlau.me/programming/cpp-templates-metaprogramming-insertion-sort.html</link>
      <description>
        <![CDATA[<p>实现快速排序：<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 01 Dec 2018 05:48:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>实现快速排序：<a href="https://howardlau.me/programming/cpp-templates-metaprogramming-quick-sort.html">https://howardlau.me/programming/cpp-templates-metaprogramming-quick-sort.html</a></p><h2 id="辅助模板类"><a href="#辅助模板类" class="headerlink" title="辅助模板类"></a>辅助模板类</h2><p>下面这些类利用了 C++ 的模板，实现了一些基本的逻辑功能：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="comment">// 布尔常量类型</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">TrueType</span> &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = <span class="literal">true</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">FalseType</span> &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = <span class="literal">false</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// SFINAE 用来特定时机启用模板</span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="type">bool</span>, <span class="keyword">typename</span> T = <span class="type">void</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> EnableIfT &#123; </span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">EnableIfT</span>&lt;<span class="literal">true</span>, T&gt; &#123;</span><br><span class="line">  <span class="keyword">using</span> Type = T;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="type">bool</span> Cond, <span class="keyword">typename</span> T = <span class="type">void</span>&gt;</span><br><span class="line"><span class="keyword">using</span> EnableIf = <span class="keyword">typename</span> EnableIfT&lt;Cond, T&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IdentityT</span> &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = T;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">using</span> Identity = <span class="keyword">typename</span> IdentityT&lt;T&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 模板的逻辑表达式功能</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="type">bool</span> COND, <span class="keyword">typename</span> TrueType, <span class="keyword">typename</span> FalseType&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IfThenElseT</span> &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = TrueType;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> TrueType, <span class="keyword">typename</span> FalseType&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IfThenElseT</span>&lt;<span class="literal">false</span>, TrueType, FalseType&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = FalseType;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="type">bool</span> COND, <span class="keyword">typename</span> TrueType, <span class="keyword">typename</span> FalseType&gt;</span><br><span class="line"><span class="keyword">using</span> IfThenElse = <span class="keyword">typename</span> IfThenElseT&lt;COND, TrueType, FalseType&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 列表的值类型</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T Value&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">CTValue</span> &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = T;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> T value = Value;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T a, T b&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">LessThanT</span>&lt;CTValue&lt;T, a&gt;, CTValue&lt;T, b&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = a &lt; b;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T a, T b&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">GreaterThanT</span>&lt;CTValue&lt;T, a&gt;, CTValue&lt;T, b&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = a &gt; b;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="列表的操作"><a href="#列表的操作" class="headerlink" title="列表的操作"></a>列表的操作</h2><p>下面这些模板都是用于对于列表类的操作，顾名思义：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义了列表的基本类型</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... Values&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">ValueList</span> &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... Values&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsEmpty</span>&lt;ValueList&lt;T, Values...&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> <span class="type">bool</span> value = <span class="keyword">sizeof</span>...(Values) == <span class="number">0</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T Head, T... Tail&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">FrontT</span>&lt;ValueList&lt;T, Head, Tail...&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = CTValue&lt;T, Head&gt;;</span><br><span class="line">    <span class="keyword">constexpr</span> <span class="type">static</span> T value = Head;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>&gt;</span><br><span class="line"><span class="keyword">using</span> Front = <span class="keyword">typename</span> FrontT&lt;ValueList&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T Head, T... Tail&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">PopFrontT</span>&lt;ValueList&lt;T, Head, Tail...&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = ValueList&lt;T, Tail...&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>&gt;</span><br><span class="line"><span class="keyword">using</span> PopFront = <span class="keyword">typename</span> PopFrontT&lt;ValueList&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... Values, T NewValue&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">PushFrontT</span>&lt;ValueList&lt;T, Values...&gt;, CTValue&lt;T, NewValue&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = ValueList&lt;T, NewValue, Values...&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">class</span> <span class="title class_">NewValue</span>&gt;</span><br><span class="line"><span class="keyword">using</span> PushFront = <span class="keyword">typename</span> PushFrontT&lt;ValueList, NewValue&gt;::Type;</span><br></pre></td></tr></table></figure><h2 id="编写插入排序"><a href="#编写插入排序" class="headerlink" title="编写插入排序"></a>编写插入排序</h2><p>利用上面的操作，我们可将插入排序递归地写成这种形式：把表头元素取出，递归排序表尾，再将表头元素插入到排序好的表尾中；而将元素插入到排序完成的表中，则是递归进行如下操作：如果新的元素直接可以插入到表头，那就成为新的表头，新的表尾就是原来的表尾，否则新的表头就是原来的表头，而新的表尾就是将这个元素插入到表尾的适当位置。</p><p>两个递归的终止条件都是是：表为空。这时候认为空表就是排序好的表，对于 <code>InsertionSort</code> 来说，直接返回空表，对于 <code>InsertSorted</code> 来说，直接插入元素即可。</p><p>虽然代码看起来比较复杂，但是逻辑还是很清楚的。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">class</span> <span class="title class_">Element</span>,</span><br><span class="line">          <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>,</span><br><span class="line">          <span class="type">bool</span> = IsEmpty&lt;ValueList&gt;::value&gt;</span><br><span class="line"><span class="keyword">struct</span> InsertSortedT;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">class</span> <span class="title class_">Element</span>,</span><br><span class="line">          <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">using</span> InsertSorted = <span class="keyword">typename</span> InsertSortedT&lt;ValueList, Element, Compare&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">class</span> <span class="title class_">Element</span>,</span><br><span class="line">          <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">InsertSortedT</span>&lt;ValueList, Element, Compare, <span class="literal">false</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> NewTail = IfThenElse&lt;</span><br><span class="line">        Compare&lt;Element, Front&lt;ValueList&gt;&gt;::value, Identity&lt;ValueList&gt;,</span><br><span class="line">        InsertSorted&lt;PopFront&lt;ValueList&gt;, Element, Compare&gt;&gt;;</span><br><span class="line">    <span class="keyword">using</span> NewHead = IfThenElse&lt;Compare&lt;Element, Front&lt;ValueList&gt;&gt;::value,</span><br><span class="line">                               Element, Front&lt;ValueList&gt;&gt;;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">using</span> Type = PushFront&lt;NewTail, NewHead&gt;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">class</span> <span class="title class_">Element</span>,</span><br><span class="line">          <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">InsertSortedT</span>&lt;ValueList, Element, Compare, <span class="literal">true</span>&gt; : PushFrontT&lt;ValueList, Element&gt; &#123;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>,</span><br><span class="line">          <span class="type">bool</span> = IsEmpty&lt;ValueList&gt;::value&gt;</span><br><span class="line"><span class="keyword">struct</span> InsertionSortT;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">InsertionSortT</span>&lt;ValueList, Compare, <span class="literal">false</span>&gt;</span><br><span class="line">    : InsertSortedT&lt;InsertionSort&lt;PopFront&lt;ValueList&gt;, Compare&gt;,</span><br><span class="line">                    Front&lt;ValueList&gt;, Compare&gt; &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">InsertionSortT</span>&lt;ValueList, Compare, <span class="literal">true</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> Type = ValueList;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> List, <span class="keyword">typename</span> Element,</span><br><span class="line">          <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T, <span class="keyword">typename</span> U&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">using</span> InsertSorted = <span class="keyword">typename</span> InsertSortedT&lt;List, Element, Compare&gt;::Type;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">ValueList</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">U</span>&gt; <span class="keyword">class</span> <span class="title class_">Compare</span>&gt;</span><br><span class="line"><span class="keyword">using</span> InsertionSort = <span class="keyword">typename</span> InsertionSortT&lt;ValueList, Compare&gt;::Type;</span><br></pre></td></tr></table></figure><h2 id="测试输出"><a href="#测试输出" class="headerlink" title="测试输出"></a>测试输出</h2><p>编写用于测试的输出代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">OutputValueList</span><span class="params">(ValueList&lt;T&gt;)</span> </span>&#123;</span><br><span class="line">    std::cout &lt;&lt; std::endl;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, T... Values&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">OutputValueList</span><span class="params">(ValueList&lt;T, Values...&gt;)</span> </span>&#123;</span><br><span class="line">    std::cout &lt;&lt; Front&lt;ValueList&lt;T, Values...&gt;&gt;::value &lt;&lt; <span class="string">&#x27; &#x27;</span>;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(PopFront&lt;ValueList&lt;T, Values...&gt;&gt;());</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>最后编写主函数测试：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">using</span> TestList = ValueList&lt;<span class="type">int</span>, <span class="number">17</span>, <span class="number">1</span>, <span class="number">15</span>, <span class="number">9</span>, <span class="number">8</span>, <span class="number">19</span>, <span class="number">16</span>, <span class="number">10</span>, <span class="number">11</span>, <span class="number">7</span>, <span class="number">4</span>, <span class="number">14</span>,</span><br><span class="line">                               <span class="number">18</span>, <span class="number">13</span>, <span class="number">3</span>, <span class="number">12</span>, <span class="number">2</span>, <span class="number">5</span>, <span class="number">6</span>, <span class="number">20</span>&gt;;</span><br><span class="line">    <span class="keyword">using</span> SortedList = InsertionSort&lt;TestList, LessThanT&gt;;</span><br><span class="line">    <span class="keyword">using</span> ReverseSortedList = InsertionSort&lt;TestList, GreaterThanT&gt;;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;Before sorted&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">TestList</span>());</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;After sorted (from small to great)&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">SortedList</span>());</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;After sorted (from great to small)&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">    <span class="built_in">OutputValueList</span>(<span class="built_in">ReverseSortedList</span>());</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后程序的输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Before sorted</span><br><span class="line">17 1 15 9 8 19 16 10 11 7 4 14 18 13 3 12 2 5 6 20 </span><br><span class="line">After sorted (from small to great)</span><br><span class="line">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 </span><br><span class="line">After sorted (from great to small)</span><br><span class="line">20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1</span><br></pre></td></tr></table></figure><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p>完整的代码片段：<a href="https://gist.github.com/howardlau1999/cd90d591869a98ac72f92ad02b29d9c2">https://gist.github.com/howardlau1999/cd90d591869a98ac72f92ad02b29d9c2</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Python 导入时和运行时代码运行的时机</title>
      <link>https://blog.howardlau.me/programming/python-importing-and-executing.html</link>
      <description>
        <![CDATA[<p>在 Python 中，<code>import</code> 语句会执行一个文件中的顶层代码，然后将各种函数定义体预先编译成字节码（Python 中字节码可以通过 <code>dis</code>]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 30 Nov 2018 08:31:25 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在 Python 中，<code>import</code> 语句会执行一个文件中的顶层代码，然后将各种函数定义体预先编译成字节码（Python 中字节码可以通过 <code>dis</code> 反编译模块看到）缓存起来，以后重新导入相同的模块就使用缓存，只做名称绑定。对于函数来说，Python 解释器会将 <code>def</code> 定义的函数体编译成字节码，并且将函数对象绑定到对应的全局名称上，但是这时候显然不会执行定义体，解释器只会在运行时调用函数的时候才会执行定义体。</p><p>但是对于类来说，导入的时候，解释器会<strong>执行</strong>类的定义体，类中<strong>嵌套</strong>的类定义体也会被执行。</p><p>下面通过代码来理解导入时和运行时的差别，建议先不要执行代码，先通过纸笔模拟一遍执行过程：</p><p>假设 <code>evaltime.py</code> 通过两种方式调用：</p><ol><li>通过在 Python 交互式解释器导入运行：</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> evaltime</span><br></pre></td></tr></table></figure><ol start="2"><li>通过命令行调用解释器解释脚本：</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python evaltime.py</span><br></pre></td></tr></table></figure><p>输出的结果分别是什么？</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># evaltime.py</span></span><br><span class="line"><span class="keyword">from</span> evalsupport <span class="keyword">import</span> deco_alpha</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;&lt;[1]&gt; evaltime module start&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ClassOne</span>():</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[2]&gt; ClassOne body&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[3]&gt; ClassOne.__init__&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__del__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[4]&gt; ClassOne.__del__&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">method_x</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[5]&gt; ClassOne.method_x&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">class</span> <span class="title class_">ClassTwo</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[6]&gt; ClassTwo body&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@deco_alpha</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ClassThree</span>():</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[7]&gt; ClassThree body&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">method_y</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[8]&gt; ClassThree.method_y&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ClassFour</span>(<span class="title class_ inherited__">ClassThree</span>):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[9]&gt; ClassFour body&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">method_y</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[10]&gt; ClassFour.method_y&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[11]&gt; ClassOne tests&#x27;</span>, <span class="number">30</span> * <span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    one = ClassOne()</span><br><span class="line">    one.method_x()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[12]&gt; ClassThree tests&#x27;</span>, <span class="number">30</span> * <span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    three = ClassThree()</span><br><span class="line">    three.method_y()</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[13]&gt; ClassFour tests&#x27;</span>, <span class="number">30</span> * <span class="string">&#x27;.&#x27;</span>)</span><br><span class="line">    four = ClassFour()</span><br><span class="line">    four.method_y()</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;&lt;[14]&gt; evaltime module end&#x27;</span>)</span><br></pre></td></tr></table></figure><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># evalsupport.py</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;&lt;[100]&gt; evalsupport module start&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">deco_alpha</span>(<span class="params">cls</span>):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[200]&gt; deco_alpha&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">inner_1</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[300]&gt; deco_alpha:inner_1&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    cls.method_y = inner_1</span><br><span class="line">    <span class="keyword">return</span> cls</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MetaAleph</span>(<span class="title class_ inherited__">type</span>):</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&#x27;&lt;[400]&gt; MetaAleph body&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">cls, name, bases, dic</span>):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;&lt;[500]&gt; MetaAleph.__init__&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">def</span> <span class="title function_">inner_2</span>(<span class="params">self</span>):</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&#x27;&lt;[600]&gt; MetaAleph.__init__:inner_2&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        cls.method_z = inner_2</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;&lt;[700]&gt; evalsupport module end&#x27;</span>)</span><br></pre></td></tr></table></figure><p>在第一种情况下，输出的结果是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">&lt;[100]&gt; evalsupport module start</span><br><span class="line">&lt;[400]&gt; MetaAleph body</span><br><span class="line">&lt;[700]&gt; evalsupport module end</span><br><span class="line">&lt;[1]&gt; evaltime module start</span><br><span class="line">&lt;[2]&gt; ClassOne body</span><br><span class="line">&lt;[6]&gt; ClassTwo body</span><br><span class="line">&lt;[7]&gt; ClassThree body</span><br><span class="line">&lt;[200]&gt; deco_alpha</span><br><span class="line">&lt;[9]&gt; ClassFour body</span><br><span class="line">&lt;[14]&gt; evaltime module end</span><br></pre></td></tr></table></figure><p>可以看到，解释器会先处理模块的依赖，并且执行每个类以及嵌套类的定义体，而装饰器函数在定义的时候没有被执行，当需要装饰类的时候，类的定义体首先被执行（这很合理，有了类才能被装饰），然后执行了装饰器函数。</p><p>由于是通过 <code>import</code> 导入，所以 <code>if __name__ == &#39;__main__&#39;</code> 块不会被执行。</p><p>对于第二种情况，输出的结果是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">&lt;[100]&gt; evalsupport module start</span><br><span class="line">&lt;[400]&gt; MetaAleph body</span><br><span class="line">&lt;[700]&gt; evalsupport module end</span><br><span class="line">&lt;[1]&gt; evaltime module start</span><br><span class="line">&lt;[2]&gt; ClassOne body</span><br><span class="line">&lt;[6]&gt; ClassTwo body</span><br><span class="line">&lt;[7]&gt; ClassThree body</span><br><span class="line">&lt;[200]&gt; deco_alpha</span><br><span class="line">&lt;[9]&gt; ClassFour body</span><br><span class="line">&lt;[11]&gt; ClassOne tests ..............................</span><br><span class="line">&lt;[3]&gt; ClassOne.__init__</span><br><span class="line">&lt;[5]&gt; ClassOne.method_x</span><br><span class="line">&lt;[12]&gt; ClassThree tests ..............................</span><br><span class="line">&lt;[300]&gt; deco_alpha:inner_1</span><br><span class="line">&lt;[13]&gt; ClassFour tests ..............................</span><br><span class="line">&lt;[10]&gt; ClassFour.method_y</span><br><span class="line">&lt;[14]&gt; evaltime module end</span><br><span class="line">&lt;[4]&gt; ClassOne.__del__</span><br></pre></td></tr></table></figure><p>前面几行和第一种情况是一致的，在 <code>__main__</code> 块里，可以看到各个类的方法被执行了，而 <code>ClassThree</code> 的类方法被装饰器替换了，最后程序结束的时候，绑定在全局变量上的 <code>ClassOne</code> 实例被 GC 释放。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>C++ 模板笔记</title>
      <link>https://blog.howardlau.me/programming/cpp-templates-notes.html</link>
      <description>
        <![CDATA[<p>这篇博客记录一下学习 C++ 模板中遇到的一些知识，参考书籍是 <a href="http://www.tmplbook.com/">C++ Templates: The Complete Guide (2nd]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 26 Nov 2018 18:28:12 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>这篇博客记录一下学习 C++ 模板中遇到的一些知识，参考书籍是 <a href="http://www.tmplbook.com/">C++ Templates: The Complete Guide (2nd Edition)</a>，本文所用的编译器版本：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&gt; g++ --version</span><br><span class="line">g++ (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0</span><br><span class="line">Copyright (C) 2017 Free Software Foundation, Inc.</span><br></pre></td></tr></table></figure><h2 id="typename-的意义"><a href="#typename-的意义" class="headerlink" title="typename 的意义"></a><code>typename</code> 的意义</h2><p>在模板声明中，<code>typename</code> 和 <code>class</code> 没有任何区别，但是在一个模板类里，如果要声明一个模板类中的一个类型的变量，例如 <code>vector&lt;T&gt;::iterator</code>，也就是模板依赖，则需要在最前面加上关键字 <code>typename</code> 来告诉编译器这是一个类型，而不是模板类中的静态成员或者常量：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">A</span> &#123;</span><br><span class="line"><span class="keyword">typename</span> T::some_type* variable;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>如果写成：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">A</span> &#123;</span><br><span class="line"> T::some_type* variable;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>编译器就会将 <code>T::some_type* variable</code> 当做两个成员相乘。</p><p>如果将 </p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printContainer</span><span class="params">(<span class="type">const</span> T&amp; container)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">typename</span> T::const_iterator it, <span class="built_in">end</span>(container.<span class="built_in">cend</span>());</span><br><span class="line">    <span class="keyword">for</span> (it = container.<span class="built_in">cbegin</span>(); it != end; ++it)</span><br><span class="line">        cout &lt;&lt; *it &lt;&lt; endl;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>写成</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printContainer</span><span class="params">(<span class="type">const</span> T&amp; container)</span> </span>&#123;</span><br><span class="line">    T::const_iterator it, <span class="built_in">end</span>(container.<span class="built_in">cend</span>());</span><br><span class="line">    <span class="keyword">for</span> (it = container.<span class="built_in">cbegin</span>(); it != end; ++it)</span><br><span class="line">        cout &lt;&lt; *it &lt;&lt; endl;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译器会报错：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">main.cpp: In <span class="keyword">function</span> ‘void printContainer(const T&amp;)’:</span><br><span class="line">main.cpp:20:11: error: need ‘typename’ before ‘T::const_iterator’ because ‘T’ is a dependent scope</span><br><span class="line">     const T::const_iterator it, end(container.cend());</span><br></pre></td></tr></table></figure><h2 id="变长模板参数"><a href="#变长模板参数" class="headerlink" title="变长模板参数"></a>变长模板参数</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;vector&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">print</span><span class="params">()</span> </span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">Arg</span>, <span class="keyword">class</span>... Rest&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">print</span><span class="params">(Arg first, Rest... rest)</span> </span>&#123;</span><br><span class="line">    cout &lt;&lt; first &lt;&lt; endl;</span><br><span class="line">    <span class="built_in">print</span>(rest...);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">Collection</span>, <span class="keyword">class</span>... Indicies&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printElems</span><span class="params">(<span class="type">const</span> Collection&amp; coll, Indicies... indicies)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">print</span>(coll[indicies]...);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> <span class="type">const</span> *argv[])</span> </span>&#123;</span><br><span class="line">    vector&lt;string&gt; strings&#123;<span class="string">&quot;first&quot;</span>, <span class="string">&quot;second&quot;</span>, <span class="string">&quot;third&quot;</span>, <span class="string">&quot;fourth&quot;</span>, <span class="string">&quot;fifth&quot;</span>&#125;;</span><br><span class="line">    <span class="built_in">print</span>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>);</span><br><span class="line">    <span class="built_in">printElems</span>(strings, <span class="number">3</span>, <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="默认初始化成员值"><a href="#默认初始化成员值" class="headerlink" title="默认初始化成员值"></a>默认初始化成员值</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">foo</span><span class="params">()</span> </span>&#123;</span><br><span class="line">T x&#123;&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="模板模板参数"><a href="#模板模板参数" class="headerlink" title="模板模板参数"></a>模板模板参数</h2><p>假如现在有一个类：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">ElementType</span>, <span class="keyword">class</span> <span class="title class_">Container</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Stack</span> &#123;</span><br><span class="line">Container container;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>那么调用的时候就要像这样使用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Stack&lt;<span class="type">int</span>, std::deque&lt;<span class="type">int</span>&gt;&gt; stack;</span><br></pre></td></tr></table></figure><p>如果想这样使用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Stack&lt;<span class="type">int</span>, std::deque&gt; stack;</span><br></pre></td></tr></table></figure><p>就要像这样声明模板：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">ElementType</span>, <span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt; <span class="keyword">class</span> <span class="title class_">Container</span> = std::deque&gt;</span><br><span class="line"><span class="keyword">class</span> Stack &#123;</span><br><span class="line">Container&lt;ElementType&gt; container;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这么做还不行，因为 STL 中的容器类实际上有两个模板参数，除了元素类型，还有 <code>allocator</code> 类型，在默认匹配的时候，如果只提供一个参数，编译器无法匹配，所以我们要写成下面这种形式：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">ElementType</span>, </span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">class</span> <span class="title class_">T</span>, <span class="keyword">class</span> <span class="title class_">Alloc</span> = std::allocator&lt;T&gt;&gt; <span class="keyword">class</span> Container = std::deque&gt;</span><br><span class="line"><span class="keyword">class</span> Stack &#123;</span><br><span class="line">Container&lt;ElementType&gt; container;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="移动语义"><a href="#移动语义" class="headerlink" title="移动语义"></a>移动语义</h2><p>在 C++ 11 中新增了移动语义，可以减少不必要的拷贝操作，但是在模板中，如果这样写：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">X</span> &#123;</span><br><span class="line">  <span class="comment">//...</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span> <span class="params">(X&amp;)</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;g() for variable\n&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span> <span class="params">(X <span class="type">const</span>&amp;)</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;g() for constant\n&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span> <span class="params">(X&amp;&amp;)</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="string">&quot;g() for movable object\n&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp;&amp; val)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">g</span>(val);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">  X v;              <span class="comment">// create variable</span></span><br><span class="line">  X <span class="type">const</span> c;        <span class="comment">// create constant</span></span><br><span class="line"></span><br><span class="line">  <span class="built_in">f</span>(v);             <span class="comment">// f() for variable calls f(X&amp;)  =&gt;  calls g(X&amp;)</span></span><br><span class="line">  <span class="built_in">f</span>(c);             <span class="comment">// f() for constant calls f(X const&amp;)  =&gt;  calls g(X const&amp;)</span></span><br><span class="line">  <span class="built_in">f</span>(<span class="built_in">X</span>());           <span class="comment">// f() for temporary calls f(X&amp;&amp;)  =&gt;  calls g(X&amp;&amp;)</span></span><br><span class="line">  <span class="built_in">f</span>(std::<span class="built_in">move</span>(v));  <span class="comment">// f() for move-enabled variable calls f(X&amp;&amp;)  =&gt;  calls g(X&amp;&amp;)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>得到的输出是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">g() <span class="keyword">for</span> variable</span><br><span class="line">g() <span class="keyword">for</span> constant</span><br><span class="line">g() <span class="keyword">for</span> variable</span><br><span class="line">g() <span class="keyword">for</span> variable</span><br></pre></td></tr></table></figure><p>需要用 <code>std::forward&lt;&gt;</code> 来完美转发 （Perfect Forwarding）潜在的移动语义：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span> <span class="params">(T&amp;&amp; val)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;T&gt;(val));   <span class="comment">// call the right g() for any passed argument val</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">g</span>() <span class="function"><span class="keyword">for</span> variable</span></span><br><span class="line"><span class="function"><span class="title">g</span><span class="params">()</span> <span class="keyword">for</span> constant</span></span><br><span class="line"><span class="function"><span class="title">g</span><span class="params">()</span> <span class="keyword">for</span> movable object</span></span><br><span class="line"><span class="function"><span class="title">g</span><span class="params">()</span> <span class="keyword">for</span> movable object</span></span><br></pre></td></tr></table></figure><h2 id="enable-if-的应用"><a href="#enable-if-的应用" class="headerlink" title="enable_if&lt;&gt; 的应用"></a><code>enable_if&lt;&gt;</code> 的应用</h2><p>为了利用右值引用和移动语义，在声明类和构造函数的时候可以分别为左值和右值写构造函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;utility&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123;</span><br><span class="line">   <span class="keyword">private</span>:</span><br><span class="line">    std::string name;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">public</span>:</span><br><span class="line">    <span class="comment">// constructor for passed initial name:</span></span><br><span class="line">    <span class="function"><span class="keyword">explicit</span> <span class="title">Person</span><span class="params">(std::string <span class="type">const</span>&amp; n)</span> : name(n) &#123;</span></span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;copying string-CONSTR for &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">explicit</span> <span class="title">Person</span><span class="params">(std::string&amp;&amp; n)</span> : name(std::move(n)) &#123;</span></span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;moving string-CONSTR for &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// copy and move constructor:</span></span><br><span class="line">    <span class="built_in">Person</span>(Person <span class="type">const</span>&amp; p) : <span class="built_in">name</span>(p.name) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;COPY-CONSTR Person &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">Person</span>(Person&amp;&amp; p) : <span class="built_in">name</span>(std::<span class="built_in">move</span>(p.name)) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;MOVE-CONSTR Person &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::string s = <span class="string">&quot;sname&quot;</span>;</span><br><span class="line">    <span class="function">Person <span class="title">p1</span><span class="params">(s)</span></span>;      <span class="comment">// init with string object =&gt; calls copying string-CONSTR</span></span><br><span class="line">    <span class="function">Person <span class="title">p2</span><span class="params">(<span class="string">&quot;tmp&quot;</span>)</span></span>;  <span class="comment">// init with string literal =&gt; calls moving string-CONSTR</span></span><br><span class="line">    <span class="function">Person <span class="title">p3</span><span class="params">(p1)</span></span>;     <span class="comment">// copy Person =&gt; calls COPY-CONSTR</span></span><br><span class="line">    <span class="function">Person <span class="title">p4</span><span class="params">(std::move(p1))</span></span>;  <span class="comment">// move Person =&gt; calls MOVE-CONST</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>利用上面的转发机制，可以将构造函数写成模板函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123;</span><br><span class="line">   <span class="keyword">private</span>:</span><br><span class="line">    std::string name;</span><br><span class="line"></span><br><span class="line">   <span class="keyword">public</span>:</span><br><span class="line">    <span class="comment">// generic constructor for passed initial name:</span></span><br><span class="line">    <span class="keyword">template</span> &lt;<span class="keyword">typename</span> STR&gt;</span><br><span class="line">    <span class="function"><span class="keyword">explicit</span> <span class="title">Person</span><span class="params">(STR&amp;&amp; n)</span> : name(std::forward&lt;STR&gt;(n)) &#123;</span></span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;TMPL-CONSTR for &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// copy and move constructor:</span></span><br><span class="line">    <span class="built_in">Person</span>(Person <span class="type">const</span>&amp; p) : <span class="built_in">name</span>(p.name) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;COPY-CONSTR Person &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">Person</span>(Person&amp;&amp; p) : <span class="built_in">name</span>(std::<span class="built_in">move</span>(p.name)) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;MOVE-CONSTR Person &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>但是这么做会有问题，在编译的时候，下面的语句会报错：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Person <span class="title">p3</span><span class="params">(p1)</span></span>;     <span class="comment">// copy Person =&gt; calls COPY-CONSTR</span></span><br></pre></td></tr></table></figure><p>原因是，对于左值 <code>p1</code> 来说，调用模板函数只需要简单地将类型 <code>STR</code> 替换为 <code>Person&amp;</code>，但是对于拷贝构造函数而言，则需要一次额外的 <code>const</code> 转换，因此解决函数重载的时候，模板函数比下面的拷贝构造函数更优。</p><p>一种可能的解决办法是像下面这样定义一个构造函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Person</span>(Person&amp; p) </span><br></pre></td></tr></table></figure><p>然而对于 <code>Person</code> 的派生类而言，模板函数依然是更优的匹配，不能完全解决问题。我们需要的解决方案是，对于从 <code>Person</code> 或者其派生类的对象，需要屏蔽掉模板函数。C++ 11 中提供了 <code>std::enable_if&lt;&gt;</code> 来解决这个问题。</p><h3 id="enable-if-的作用"><a href="#enable-if-的作用" class="headerlink" title="enable_if&lt;&gt; 的作用"></a><code>enable_if&lt;&gt;</code> 的作用</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">typename</span> std::enable_if&lt;condition, OptionalType&gt;::<span class="function">type</span></span><br><span class="line"><span class="function"><span class="title">foo</span><span class="params">()</span> </span>&#123;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>当 <code>condition</code> 为真的时候，<code>type</code> 类型就是 <code>OptionalType</code>，如果第二个模板参数没有指定，那么就是 <code>void</code></li><li>当 <code>condition</code> 为假的时候，<code>type</code> 类型未定义，由 <strong>SFINAE（Substitution Failure Is Not An Error）</strong> 原则，该模板当前的特化被忽略，达到屏蔽的效果</li></ul><p>一般也可以写成下面这种形式，更简洁一些：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T, <span class="keyword">typename</span> = std::enable_if&lt;condition&gt;::type&gt;</span><br><span class="line"><span class="type">void</span> <span class="built_in">foo</span>() &#123;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>回到上面 <code>Person</code> 类的问题，我们可以通过这样的方式来在特定时候启用模板构造函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;type_traits&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">using</span> EnableIfString = std::enable_if&lt;</span><br><span class="line">                         std::is_convertible&lt;T,std::string&gt;::value&gt;::type;</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> STR, <span class="keyword">typename</span> = EnableIfString&lt;STR&gt;&gt;</span><br><span class="line">    <span class="keyword">explicit</span> <span class="built_in">Person</span>(STR&amp;&amp; n)</span><br><span class="line">     : <span class="built_in">name</span>(std::forward&lt;STR&gt;(n)) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;TMPL-CONSTR for &#x27;&quot;</span> &lt;&lt; name &lt;&lt; <span class="string">&quot;&#x27;\n&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>就可以解决问题了。</p><h2 id="连接问题"><a href="#连接问题" class="headerlink" title="连接问题"></a>连接问题</h2><p>按照惯常的做法，一般我们会在写大程序的时候，将声明放在头文件中，比如 <code>.h</code> 一类的，然后在另外的 <code>.cpp</code> 文件中定义函数。但是如果按照这种方式组织模板代码，会发生下面的问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// myfirst.hpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> MYFIRST_HPP</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MYFIRST_HPP</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// declaration of template</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printTypeof</span> <span class="params">(T <span class="type">const</span>&amp;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span> <span class="comment">// MYFIRST_HPP</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// myfirst.cpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;typeinfo&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;myfirst.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// implementation/definition of template</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printTypeof</span> <span class="params">(T <span class="type">const</span>&amp; x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="built_in">typeid</span>(x).<span class="built_in">name</span>() &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// myfirstmain.cpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;myfirst.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// use of the template</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">double</span> ice = <span class="number">3.0</span>;</span><br><span class="line">    <span class="built_in">printTypeof</span>(ice);   <span class="comment">// call function template for type double</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当我们试着编译这段程序的时候，会报连接错误：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&gt; g++ myfirstmain.cpp myfirst.cpp -o myfirst</span><br><span class="line">/tmp/cc3q27zY.o: In <span class="keyword">function</span> `main<span class="string">&#x27;:</span></span><br><span class="line"><span class="string">myfirstmain.cpp:(.text+0x2c): undefined reference to `void printTypeof&lt;double&gt;(double const&amp;)&#x27;</span></span><br><span class="line">collect2: error: ld returned 1 <span class="built_in">exit</span> status</span><br></pre></td></tr></table></figure><p>原因是编译器在遇到模板函数的时候需要实例化，但是实例化所需要的信息分别在两个编译模块中，因此，当编译器遇到 <code>printTypeof()</code>  调用的时候，并不知道如何具体实例化这个函数，只能假定在别的地方定义了实例函数；而当编译器遇到了 <code>printTypeof()</code> 的模板定义的时候，它并不知道需要用什么模板参数去实例化，也就没有实例化，因此导致了连接错误。</p><p>解决办法是将模板函数的定义放在头文件中：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifndef</span> MYFIRST_HPP</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MYFIRST_HPP</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;typeinfo&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// declaration of template</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printTypeof</span> <span class="params">(T <span class="type">const</span>&amp;)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// implementation/definition of template</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printTypeof</span> <span class="params">(T <span class="type">const</span>&amp; x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="built_in">typeid</span>(x).<span class="built_in">name</span>() &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span> <span class="comment">// MYFIRST_HPP</span></span></span><br></pre></td></tr></table></figure><p>这样可以解决问题，但是会增加编译时间。另一种办法是模板显式实例化：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// myfirst.cpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;typeinfo&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;myfirst.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// implementation/definition of template</span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">printTypeof</span> <span class="params">(T <span class="type">const</span>&amp; x)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    std::cout &lt;&lt; <span class="built_in">typeid</span>(x).<span class="built_in">name</span>() &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <span class="type">void</span> printTypeof&lt;&gt;(<span class="type">double</span> <span class="type">const</span>&amp;);</span><br></pre></td></tr></table></figure><p>但是这样就需要每种可能的模板参数都声明一遍，所以还是内含在头文件中比较方便，尽管会带来额外的编译开销。</p><h2 id="类型特性（Type-Traits"><a href="#类型特性（Type-Traits" class="headerlink" title="类型特性（Type Traits)"></a>类型特性（Type Traits)</h2><p>从 C++ 11 起，可以通过引入头文件 <code>&lt;type_traits&gt;</code> 实现编译时的类型特性检查和操作，在编写通用库的时候非常有用。</p><p>其中值得注意的是 <code>std::remove_const_t&lt;&gt;</code> 和 <code>std::remove_reference_t&lt;&gt;</code> 两者的使用顺序：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">std::<span class="type">remove_const_t</span>&lt;<span class="type">int</span> <span class="type">const</span>&amp;&gt; <span class="comment">// -&gt; int const&amp;</span></span><br><span class="line">std::<span class="type">remove_const_t</span>&lt;std::<span class="type">remove_reference_t</span>&lt;<span class="type">int</span> <span class="type">const</span>&amp;&gt;&gt; <span class="comment">// -&gt; int</span></span><br><span class="line">std::<span class="type">remove_reference_t</span>&lt;std::<span class="type">remove_const_t</span>&lt;<span class="type">int</span> <span class="type">const</span>&amp;&gt;&gt; <span class="comment">// -&gt; int const</span></span><br></pre></td></tr></table></figure><p>当然还可以：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::<span class="type">decay_t</span>&lt;<span class="type">int</span> <span class="type">const</span>&amp;&gt; <span class="comment">// -&gt; int </span></span><br></pre></td></tr></table></figure><h2 id="特性（Traits）"><a href="#特性（Traits）" class="headerlink" title="特性（Traits）"></a>特性（Traits）</h2><p>假设有一个求和函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function">T <span class="title">accum</span> <span class="params">(T <span class="type">const</span>* beg, T <span class="type">const</span>* end)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    T total&#123;&#125;;  <span class="comment">// assume this actually creates a zero value</span></span><br><span class="line">    <span class="keyword">while</span> (beg != end) &#123;</span><br><span class="line">        total += *beg;</span><br><span class="line">        ++beg;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> total;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段模板代码在如下调用的时候会发生溢出错误：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">char</span> name[] = <span class="string">&quot;templates&quot;</span>;</span><br><span class="line"><span class="type">int</span> length = <span class="built_in">sizeof</span>(name)<span class="number">-1</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// (try to) print average character value</span></span><br><span class="line">std::cout &lt;&lt; <span class="string">&quot;the average value of the characters in \&quot;&quot;</span></span><br><span class="line">&lt;&lt; name &lt;&lt; <span class="string">&quot;\&quot; is &quot;</span></span><br><span class="line">&lt;&lt; <span class="built_in">accum</span>(name, name+length) / length</span><br><span class="line">              &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br></pre></td></tr></table></figure><p>原因是，模板中的类型 <code>T</code> 被实例化成为 <code>char</code>，所以发生了溢出错误，当然我们可以通过额外声明求和变量的类型来解决问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">accum</span>&lt;<span class="type">int</span>&gt;(name, name+length) </span><br></pre></td></tr></table></figure><h3 id="类型特性（Type-Traits）"><a href="#类型特性（Type-Traits）" class="headerlink" title="类型特性（Type Traits）"></a>类型特性（Type Traits）</h3><p>通过类型特性，可以更好地解决这个问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>&lt;<span class="type">char</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> AccT = <span class="type">int</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>&lt;<span class="type">short</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> AccT = <span class="type">int</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>这里的技巧是通过模板特化来选择我们需要的类型，然后求和函数代码只需要改造成下面这样既可：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">accum</span> <span class="params">(T <span class="type">const</span>* beg, T <span class="type">const</span>* end)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">// return type is traits of the element type</span></span><br><span class="line">    <span class="keyword">using</span> AccT = <span class="keyword">typename</span> AccumulationTraits&lt;T&gt;::AccT;</span><br><span class="line"></span><br><span class="line">    AccT total&#123;&#125;;  <span class="comment">// assume this actually creates a zero value</span></span><br><span class="line">    <span class="keyword">while</span> (beg != end) &#123;</span><br><span class="line">        total += *beg;</span><br><span class="line">        ++beg;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> total;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="值特性（Value-Traits）"><a href="#值特性（Value-Traits）" class="headerlink" title="值特性（Value Traits）"></a>值特性（Value Traits）</h3><p>在原本的代码中，<code>AccT</code> 是根据一个 <code>T</code> 推断出来的类型，并且使用了花括号进行默认构造，但是 <code>AccT</code> 很可能没有默认构造函数，或者不能提供一个足够好的默认值，这时候可以用值特性来提供默认值：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>&lt;<span class="type">char</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> AccT = <span class="type">int</span>;</span><br><span class="line">    <span class="type">static</span> AccT <span class="type">const</span> zero = <span class="number">0</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">AccumulationTraits</span>&lt;<span class="type">short</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">using</span> AccT = <span class="type">int</span>;</span><br><span class="line">    <span class="type">static</span> AccT <span class="type">const</span> zero = <span class="number">0</span>;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>模板代码改造成下面这样：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">accum</span> <span class="params">(T <span class="type">const</span>* beg, T <span class="type">const</span>* end)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">  <span class="comment">// return type is traits of the element type</span></span><br><span class="line">  <span class="keyword">using</span> AccT = <span class="keyword">typename</span> AccumulationTraits&lt;T&gt;::AccT;</span><br><span class="line"></span><br><span class="line">  AccT total = AccumulationTraits&lt;T&gt;::zero;  <span class="comment">// init total by trait value</span></span><br><span class="line">  <span class="keyword">while</span> (beg != end) &#123;</span><br><span class="line">    total += *beg;</span><br><span class="line">    ++beg;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> total;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是这样也有限制：C++ 在类中的静态常量只能使用整形或者字面量来初始化。</p><h3 id="SFINAE"><a href="#SFINAE" class="headerlink" title="SFINAE"></a>SFINAE</h3><p>通过<strong>SFINAE</strong>，可以实现<code>std::is_default_constructible&lt;&gt;</code>的功能，首先利用值特性创建布尔常量：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="type">bool</span> val&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">BoolConstant</span> &#123;</span><br><span class="line">  <span class="keyword">using</span> Type = BoolConstant&lt;val&gt;;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">constexpr</span> <span class="type">bool</span> value = val;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">using</span> TrueType  = BoolConstant&lt;<span class="literal">true</span>&gt;;</span><br><span class="line"><span class="keyword">using</span> FalseType = BoolConstant&lt;<span class="literal">false</span>&gt;;</span><br></pre></td></tr></table></figure><p>通过模板特化，实现判断两个类型是否相同的功能：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;boolconstant.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T1, <span class="keyword">typename</span> T2&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsSameT</span> : FalseType &#123;&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsSameT</span>&lt;T, T&gt; : TrueType &#123;&#125;;</span><br></pre></td></tr></table></figure><p>最后实现模板来检查一个类是否有默认构造函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;issame.hpp&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsDefaultConstructibleT</span> &#123;</span><br><span class="line">  <span class="keyword">private</span>:</span><br><span class="line">    <span class="comment">// test() trying substitute call of a default constructor for T passed as U:</span></span><br><span class="line">    <span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> U, <span class="keyword">typename</span> </span>= <span class="keyword">decltype</span>(<span class="built_in">U</span>())&gt;</span><br><span class="line">      <span class="function"><span class="type">static</span> <span class="type">char</span> <span class="title">test</span><span class="params">(<span class="type">void</span>*)</span></span>;</span><br><span class="line">    <span class="comment">// test() fallback:</span></span><br><span class="line">    <span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>&gt;</span></span><br><span class="line"><span class="function">      <span class="type">static</span> <span class="type">long</span> <span class="title">test</span><span class="params">(...)</span></span>;</span><br><span class="line">  <span class="keyword">public</span>:</span><br><span class="line">    <span class="type">static</span> <span class="keyword">constexpr</span> <span class="type">bool</span> value</span><br><span class="line">      = IsSameT&lt;<span class="keyword">decltype</span>(<span class="built_in">test</span>&lt;T&gt;(<span class="literal">nullptr</span>)), <span class="type">char</span>&gt;::value;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这段代码的意思是：尝试用模板类中的 <code>T</code> 替换模板类中的模板函数中的 <code>U</code>，假如没有默认构造函数，那么 <code>decltype(U())</code> 就会发生错误，从而 <code>char test(void*)</code> 替换失败，这时候会回退到下面定义的函数，由于参数列表是省略号，因此总是最后匹配到，这时候 <code>test()</code> 的返回值类型就变为 <code>long</code> 。利用这个特性，我们只需要判断 <code>test()</code> 的返回值类型就知道有没有默认构造函数了。</p><p>注意，如果我们直接用 <code>T</code> 来测试，而不是通过传递模板参数的方法的话，会直接产生编译错误：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>, <span class="keyword">typename</span> </span>= <span class="keyword">decltype</span>(<span class="built_in">T</span>())&gt;</span><br><span class="line">      <span class="function"><span class="type">static</span> <span class="type">char</span> <span class="title">test</span><span class="params">(<span class="type">void</span>*)</span></span>;</span><br></pre></td></tr></table></figure><p>这是因为在模板实例化的时候，所有的成员函数都会被替换进去，如果没有默认构造函数，就会发生编译错误，并且不会触发 <code>SFINAE</code>。通过传递模板参数的方法，则创造了一个局部性的模板替换上下文，从而可以触发 <code>SFINAE</code>。</p><h3 id="改进版本"><a href="#改进版本" class="headerlink" title="改进版本"></a>改进版本</h3><p>当然这么写有点复杂，我们可以直接让函数的返回类型就是布尔常量，同时也符合了一个谓词模板应该返回布尔常量类的惯例：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;type_traits&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsDefaultConstructibleHelper</span> &#123;</span><br><span class="line">  <span class="keyword">private</span>:</span><br><span class="line">    <span class="comment">// test() trying substitute call of a default constructor for T passed as U:</span></span><br><span class="line">    <span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> U, <span class="keyword">typename</span> </span>= <span class="keyword">decltype</span>(<span class="built_in">U</span>())&gt;</span><br><span class="line">      <span class="function"><span class="type">static</span> std::true_type <span class="title">test</span><span class="params">(<span class="type">void</span>*)</span></span>;</span><br><span class="line">    <span class="comment">// test() fallback:</span></span><br><span class="line">    <span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>&gt;</span></span><br><span class="line"><span class="function">      <span class="type">static</span> std::false_type <span class="title">test</span><span class="params">(...)</span></span>;</span><br><span class="line">  <span class="keyword">public</span>:</span><br><span class="line">    <span class="keyword">using</span> Type = <span class="keyword">decltype</span>(<span class="built_in">test</span>&lt;T&gt;(<span class="literal">nullptr</span>));</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsDefaultConstructibleT</span> : IsDefaultConstructibleHelper&lt;T&gt;::Type &#123;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="利用偏特化改进"><a href="#利用偏特化改进" class="headerlink" title="利用偏特化改进"></a>利用偏特化改进</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;type_traits&gt;</span>  <span class="comment">// defines true_type and false_type</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// helper to ignore any number of template parameters: </span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>...&gt; <span class="keyword">using</span> VoidT = <span class="type">void</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// primary template:</span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>, <span class="keyword">typename</span> = VoidT&lt;&gt;&gt;</span><br><span class="line"><span class="keyword">struct</span> IsDefaultConstructibleT : std::false_type</span><br><span class="line">&#123;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// partial specialization (may be SFINAE&#x27;d away):</span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">IsDefaultConstructibleT</span>&lt;T, VoidT&lt;<span class="keyword">decltype</span>(<span class="built_in">T</span>())&gt;&gt; : std::true_type</span><br><span class="line">&#123;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>Python 生命游戏（生成器的应用）</title>
      <link>https://blog.howardlau.me/programming/python-game-of-life-using-generators.html</link>
      <description>
        <![CDATA[<h2 id="生命游戏"><a href="#生命游戏" class="headerlink" title="生命游戏"></a>生命游戏</h2><p>康威生命游戏（英语：Conway’s Game of]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sun, 25 Nov 2018 07:04:24 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="生命游戏"><a href="#生命游戏" class="headerlink" title="生命游戏"></a>生命游戏</h2><p>康威生命游戏（英语：Conway’s Game of Life），又称康威生命棋，是英国数学家约翰·何顿·康威在1970年发明的细胞自动机。</p><p><a href="/programming/python-game-of-life-using-generators/gospers-glider-gun.gif" data-fancybox="gallery" data-caption="生命游戏"><img src="/programming/python-game-of-life-using-generators/gospers-glider-gun.gif" alt="生命游戏"></a>生命游戏</p><p>生命游戏中，对于任意细胞，规则如下：</p><ul><li>每个细胞有两种状态 - <strong>存活</strong>或<strong>死亡</strong>，每个细胞与以自身为中心的周围八格细胞产生互动。</li><li>当前细胞为存活状态时，当周围的存活细胞低于2个时（不包含2个）， 该细胞变成死亡状态。（模拟生命数量稀少）</li><li>当前细胞为存活状态时，当周围有2个或3个存活细胞时， 该细胞保持原样。</li><li>当前细胞为存活状态时，当周围有3个以上的存活细胞时，该细胞变成死亡状态。（模拟生命数量过多）</li><li>当前细胞为死亡状态时，当周围有3个存活细胞时，该细胞变成存活状态。 （模拟繁殖）</li></ul><p>可以把最初的细胞结构定义为种子，当所有在种子中的细胞<strong>同时</strong>被以上规则处理后, 可以得到第一代细胞图。按规则继续处理当前的细胞图，可以得到下一代的细胞图，周而复始。</p><p>这篇文章将用 Python 实现这个游戏，作为 <code>yield</code> 的演示。</p><h2 id="什么是-yield"><a href="#什么是-yield" class="headerlink" title="什么是 yield"></a>什么是 <code>yield</code></h2><p><code>yield </code> 是 Python 中一个语法关键字，在英文里，这个单词有“产生”和“让出”两个意思。在 Python 里，两个意思都有，作用是<strong>产出</strong>一个值并<strong>让出</strong>控制权，当一个函数内包含了 <code>yield</code> 语句，那么这个函数就是一个<strong>生成器函数</strong>，生成器函数类似于<strong>迭代器</strong>，可以作为 <code>next</code> 的参数，除此之外还有 <code>send</code> 方法，用来接收外部值。当函数执行到含有 <code>yield</code> 的语句的时候，就会将 <code>yield</code> 后面的值作为下一个迭代的值，并<strong>暂停</strong>函数执行，直到外部调用 <code>next</code> 或者 <code>send</code> 才会继续执行。当函数执行完毕之后，就会抛出 <code>StopIteration</code> 异常；如果是通过 <code>return</code> 语句返回的，那么 <code>StopIteration</code> 里会有返回值。下面是一个例子：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; def gen():</span><br><span class="line">...     a = 1</span><br><span class="line">...     b = yield a</span><br><span class="line">...     c = yield b</span><br><span class="line">...     <span class="built_in">return</span> c</span><br><span class="line">... </span><br><span class="line">&gt;&gt;&gt; it = gen()</span><br><span class="line">&gt;&gt;&gt; <span class="built_in">print</span>(next(it))</span><br><span class="line">1</span><br><span class="line">&gt;&gt;&gt; <span class="built_in">print</span>(next(it))</span><br><span class="line">None</span><br><span class="line">&gt;&gt;&gt; <span class="built_in">print</span>(it.send(42))</span><br><span class="line">Traceback (most recent call last):</span><br><span class="line">  File <span class="string">&quot;&lt;stdin&gt;&quot;</span>, line 1, <span class="keyword">in</span> &lt;module&gt;</span><br><span class="line">StopIteration: 42</span><br></pre></td></tr></table></figure><p>如果函数没有 <code>return</code> 语句，表现是这样的：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; def gen():</span><br><span class="line">...     a = 1</span><br><span class="line">...     yield a</span><br><span class="line">... </span><br><span class="line">&gt;&gt;&gt; it = gen()</span><br><span class="line">&gt;&gt;&gt; next(it)</span><br><span class="line">1</span><br><span class="line">&gt;&gt;&gt; next(it)</span><br><span class="line">Traceback (most recent call last):</span><br><span class="line">  File <span class="string">&quot;&lt;stdin&gt;&quot;</span>, line 1, <span class="keyword">in</span> &lt;module&gt;</span><br><span class="line">StopIteration</span><br></pre></td></tr></table></figure><h2 id="生成器嵌套之-yield-from"><a href="#生成器嵌套之-yield-from" class="headerlink" title="生成器嵌套之 yield from"></a>生成器嵌套之 <code>yield from</code></h2><p>当有多个生成器嵌套的时候，可以用 <code>yield from</code> 简化代码，这时候相当于在当前生成器函数和子生成器函数之间建立了一个通道，比如说：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">chain</span>(<span class="params">*args</span>):</span><br><span class="line">    <span class="keyword">for</span> arg <span class="keyword">in</span> args:</span><br><span class="line">        <span class="keyword">for</span> it <span class="keyword">in</span> arg:</span><br><span class="line">            <span class="keyword">yield</span> it</span><br></pre></td></tr></table></figure><p>可以写成：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">chain</span>(<span class="params">*args</span>):</span><br><span class="line">    <span class="keyword">for</span> arg <span class="keyword">in</span> args:</span><br><span class="line">        <span class="keyword">yield</span> <span class="keyword">from</span> arg</span><br></pre></td></tr></table></figure><p>理解了这两个关键字的作用之后，就可以开始写生命游戏了。</p><h2 id="生命游戏-1"><a href="#生命游戏-1" class="headerlink" title="生命游戏"></a>生命游戏</h2><p>用 <code>yield</code> 和 <code>yield from</code> 编写这个游戏的基本思路就是通过这两个语句不断产生基本事件，并在一个驱动循环中处理事件。这对于以后编写并发程序是一个比较基础的认识。</p><p>假设我们用这两个符号代表不同的状态：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ALIVE = <span class="string">&quot;#&quot;</span></span><br><span class="line">DEATH = <span class="string">&quot;.&quot;</span></span><br></pre></td></tr></table></figure><h3 id="定义事件"><a href="#定义事件" class="headerlink" title="定义事件"></a>定义事件</h3><p>首先，定义几种基本的事件：<code>Query</code> 表示查询某个格子的状态，<code>Transition</code> 表示某个格子状态的转换，<code>Tick</code> 表示一代过去了。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Query = namedtuple(<span class="string">&quot;Query&quot;</span>, (<span class="string">&quot;r&quot;</span>, <span class="string">&quot;c&quot;</span>))</span><br><span class="line">Transition = namedtuple(<span class="string">&quot;Transition&quot;</span>, (<span class="string">&quot;r&quot;</span>, <span class="string">&quot;c&quot;</span>, <span class="string">&quot;next_state&quot;</span>))</span><br><span class="line">Tick = namedtuple(<span class="string">&quot;Tick&quot;</span>, <span class="string">&quot;step&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="定义步骤"><a href="#定义步骤" class="headerlink" title="定义步骤"></a>定义步骤</h3><p>在每一代中，我们需要对每一个细胞做下面的操作：</p><ol><li>查询自己的状态（生成一个 Query）</li><li>查询邻居的状态（子生成器生成 Query)</li><li>计算新的状态</li><li>生成一个 Transition 事件用于转换状态</li></ol><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">step_cell</span>(<span class="params">r, c</span>):</span><br><span class="line">    state = <span class="keyword">yield</span> Query(r, c)</span><br><span class="line">    alive_neighbours = <span class="keyword">yield</span> <span class="keyword">from</span> count_alive_neighbours(r, c)</span><br><span class="line">    next_state = game_logic(state, alive_neighbours)</span><br><span class="line">    <span class="keyword">yield</span> Transition(r, c, next_state)</span><br></pre></td></tr></table></figure><p>查询邻居的状态是一个子生成器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Neighbour = namedtuple(<span class="string">&quot;Neighbour&quot;</span>, (<span class="string">&quot;dr&quot;</span>, <span class="string">&quot;dc&quot;</span>))</span><br><span class="line">neighbours = [Neighbour(dr, dc) <span class="keyword">for</span> dr <span class="keyword">in</span> <span class="built_in">range</span>(-<span class="number">1</span>, <span class="number">2</span>)</span><br><span class="line">              <span class="keyword">for</span> dc <span class="keyword">in</span> <span class="built_in">range</span>(-<span class="number">1</span>, <span class="number">2</span>) <span class="keyword">if</span> dr <span class="keyword">or</span> dc]</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">count_alive_neighbours</span>(<span class="params">r, c</span>):</span><br><span class="line">    neighbour_states = []</span><br><span class="line">    <span class="keyword">for</span> neighbour <span class="keyword">in</span> neighbours:</span><br><span class="line">        state = <span class="keyword">yield</span> Query(r + neighbour.dr, c + neighbour.dc)</span><br><span class="line">        neighbour_states.append(state)</span><br><span class="line">    <span class="keyword">return</span> Counter(neighbour_states)[ALIVE]</span><br></pre></td></tr></table></figure><p>注意这里不能用列表推导的方式：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Wrong!!</span></span><br><span class="line">neighbour_states = [<span class="keyword">yield</span> Query(r + neighbour.dr, c + neighbour.dc) <span class="keyword">for</span> neighbour <span class="keyword">in</span> neighbours] </span><br></pre></td></tr></table></figure><p>这样是语法错误，是不允许的。</p><p>而游戏的逻辑则根据规则简单地写一个函数：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">game_logic</span>(<span class="params">state, alive_neighbours</span>):</span><br><span class="line">    <span class="keyword">if</span> state == ALIVE:</span><br><span class="line">        <span class="keyword">if</span> alive_neighbours &lt; <span class="number">2</span>:</span><br><span class="line">            <span class="keyword">return</span> DEATH</span><br><span class="line">        <span class="keyword">elif</span> alive_neighbours &gt; <span class="number">3</span>:</span><br><span class="line">            <span class="keyword">return</span> DEATH</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">if</span> alive_neighbours == <span class="number">3</span>:</span><br><span class="line">            <span class="keyword">return</span> ALIVE</span><br><span class="line">    <span class="keyword">return</span> state</span><br></pre></td></tr></table></figure><h3 id="编写驱动循环"><a href="#编写驱动循环" class="headerlink" title="编写驱动循环"></a>编写驱动循环</h3><p>我们通过一个 <code>GridWorld</code> 来存放世界的状态，并通过一个循环来驱动各个生成器：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">GridWorld</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, width, height</span>):</span><br><span class="line">        <span class="variable language_">self</span>.width = width</span><br><span class="line">        <span class="variable language_">self</span>.height = height</span><br><span class="line">        <span class="variable language_">self</span>.grids = []</span><br><span class="line">        <span class="keyword">for</span> _ <span class="keyword">in</span> <span class="built_in">range</span>(height):</span><br><span class="line">            <span class="comment"># 注意不能写成 [[DEATH] * width] * height，这样会变成多个列表的引用的复制</span></span><br><span class="line">            <span class="variable language_">self</span>.grids.append([DEATH] * width)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 求模用来连接边界</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">query</span>(<span class="params">self, r, c</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">self</span>.grids[r % <span class="variable language_">self</span>.height][c % <span class="variable language_">self</span>.width]</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">transition</span>(<span class="params">self, r, c, next_state</span>):</span><br><span class="line">        <span class="variable language_">self</span>.grids[r % <span class="variable language_">self</span>.height][c % <span class="variable language_">self</span>.width] = next_state</span><br><span class="line">    </span><br><span class="line">    <span class="comment">#驱动循环</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">event_loop</span>(<span class="params">self</span>):</span><br><span class="line">        step = <span class="number">0</span></span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            <span class="keyword">for</span> r <span class="keyword">in</span> <span class="built_in">range</span>(<span class="variable language_">self</span>.height):</span><br><span class="line">                <span class="keyword">for</span> c <span class="keyword">in</span> <span class="built_in">range</span>(<span class="variable language_">self</span>.width):</span><br><span class="line">                    <span class="keyword">yield</span> <span class="keyword">from</span> step_cell(r, c)</span><br><span class="line">            <span class="keyword">yield</span> Tick(step)</span><br><span class="line">            step += <span class="number">1</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 从驱动中获取事件并处理</span></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">simulate</span>(<span class="params">self, steps=<span class="number">10</span></span>):</span><br><span class="line">        step = <span class="number">0</span></span><br><span class="line">        event_loop = <span class="variable language_">self</span>.event_loop()</span><br><span class="line">        event = <span class="built_in">next</span>(event_loop) <span class="comment"># 预激活</span></span><br><span class="line">        next_generation = GridWorld(<span class="variable language_">self</span>.width, <span class="variable language_">self</span>.height)</span><br><span class="line">        <span class="keyword">while</span> step &lt; steps:</span><br><span class="line">            <span class="keyword">if</span> <span class="built_in">isinstance</span>(event, Tick):</span><br><span class="line">                step = event.step</span><br><span class="line">                <span class="variable language_">self</span>.grids = next_generation.grids</span><br><span class="line">                next_generation = GridWorld(<span class="variable language_">self</span>.width, <span class="variable language_">self</span>.height)</span><br><span class="line">                <span class="built_in">print</span>(<span class="string">&quot;Step: &#123;step&#125;/&#123;steps&#125;&quot;</span>.<span class="built_in">format</span>(step=step, steps=steps))</span><br><span class="line">                <span class="built_in">print</span>(<span class="variable language_">self</span>)</span><br><span class="line">                event = <span class="built_in">next</span>(event_loop)</span><br><span class="line">            <span class="keyword">elif</span> <span class="built_in">isinstance</span>(event, Query):</span><br><span class="line">                <span class="comment"># 在当前代查询</span></span><br><span class="line">                event = event_loop.send(<span class="variable language_">self</span>.query(*event))</span><br><span class="line">            <span class="keyword">elif</span> <span class="built_in">isinstance</span>(event, Transition):</span><br><span class="line">                <span class="comment"># 在下一代修改</span></span><br><span class="line">                next_generation.transition(*event)</span><br><span class="line">                event = <span class="built_in">next</span>(event_loop)</span><br><span class="line">            </span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__str__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27;\n&#x27;</span>.join([<span class="string">&#x27;&#x27;</span>.join(row) <span class="keyword">for</span> row <span class="keyword">in</span> <span class="variable language_">self</span>.grids])</span><br></pre></td></tr></table></figure><p>然后，只需实例化 <code>GridWorld</code>，并执行 <code>simulate</code> 方法，就能看到程序输出的仿真结果了。完整代码请参考：<a href="https://github.com/howardlau1999/learning-python/blob/master/gameoflife.py">https://github.com/howardlau1999/learning-python/blob/master/gameoflife.py</a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>yield</code> 和 <code>yield from</code> 非常适用于离散事件仿真。在使用生成器的时候，要记得先预激活生成器，否则不能进行 <code>send</code> 操作。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>2018 中山大学软件工程初级实训 - Agenda</title>
      <link>https://blog.howardlau.me/projects/2018-sysu-software-engineering-agenda.html</link>
      <description>
        <![CDATA[<p>本项目是 2018 年中山大学软件工程初级实训课程内容。项目为编写一个简单的会议管理系统，需要在完成用户添加、删除，会议添加、删除、修改等功能，主要目的是让学生了解软件工程设计的思想（例如三层模型）以及锻炼实际编码能力（主要为 C++11，运用 Lambda 表达式和]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/projects/">项目分享</category>
      <pubDate>Sat, 17 Nov 2018 05:28:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>本项目是 2018 年中山大学软件工程初级实训课程内容。项目为编写一个简单的会议管理系统，需要在完成用户添加、删除，会议添加、删除、修改等功能，主要目的是让学生了解软件工程设计的思想（例如三层模型）以及锻炼实际编码能力（主要为 C++11，运用 Lambda 表达式和 STL）。代码通过教师编写的 <code>gtest</code> 测试用例进行测试，每周六定时评测三次。本人在实现了基本要求之外，还实现了 C++&#x2F;Python 接口，使用 Django 构建后端 RESTFul API，并使用 React 和 MaterialUI 构建前端页面。</p><p>完整代码参考：<a href="https://github.com/howardlau1999/sysu-agenda">https://github.com/howardlau1999/sysu-agenda</a></p><p>Web 端在线演示：<a href="https://agenda.howardlau.me/">https://agenda.howardlau.me/</a></p><h2 id="基本要求"><a href="#基本要求" class="headerlink" title="基本要求"></a>基本要求</h2><p>基本的需求是实现一个程序，用户可以</p><ul><li>查询会议</li><li>创建会议</li><li>查询用户列表</li><li>删除自己发起的会议</li><li>退出自己参与的会议</li></ul><p>会议包括会议标题、发起人、参与人（可以有一个或多个、<strong>不能和发起人一样、不能重复</strong>）、开始时间、结束时间等。任何用户在任何时间点只可以至多参与一个会议。会议的标题是唯一的，不可以重复。</p><h3 id="数据封装类"><a href="#数据封装类" class="headerlink" title="数据封装类"></a>数据封装类</h3><p>在课程的第一阶段，需要完成的内容是类 <code>Date</code>、<code>User</code>、<code>Meeting</code>、<code>Storage</code>的编写，都属于比较基础的编码。其中 <code>User</code> 和 <code>Meeting</code> 为简单的数据封装类，提供 <code>getter</code> 和 <code>setter</code>。而 <code>Date</code> 类主要完成的任务是日期的存储（但不涉及计算），只需要完成和指定格式字符串的相互存取、验证日期合法性以及比较日期前后即可，都属于比较简单而基础的内容。其中可能比较容易出错的是验证日期合法性，下面给出一份参考代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">is_leap_year</span><span class="params">(<span class="type">int</span> year)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> ((t_date.m_year % <span class="number">4</span> == <span class="number">0</span> &amp;&amp; t_date.m_year % <span class="number">100</span> != <span class="number">0</span>) ||</span><br><span class="line">         t_date.m_year % <span class="number">400</span> == <span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Date::isValid</span><span class="params">(<span class="type">const</span> Date&amp; t_date)</span> </span>&#123;</span><br><span class="line">    <span class="type">static</span> <span class="type">const</span> <span class="type">int</span> days[] = &#123;<span class="number">31</span>, <span class="number">28</span>, <span class="number">31</span>, <span class="number">30</span>, <span class="number">31</span>, <span class="number">30</span>, <span class="number">31</span>, <span class="number">31</span>, <span class="number">30</span>, <span class="number">31</span>, <span class="number">30</span>, <span class="number">31</span>&#125;;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_year &lt; <span class="number">1000</span> || t_date.m_year &gt; <span class="number">9999</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_month &lt; <span class="number">1</span> || t_date.m_month &gt; <span class="number">12</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_day &lt; <span class="number">1</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_month == <span class="number">2</span> &amp;&amp; <span class="built_in">is_leap_year</span>(t_date.year)) &#123;</span><br><span class="line">        <span class="keyword">if</span> (t_date.m_day &gt; <span class="number">29</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (t_date.m_day &gt; days[t_date.m_month - <span class="number">1</span>]) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_hour &lt; <span class="number">0</span> || t_date.m_hour &gt; <span class="number">23</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">if</span> (t_date.m_minute &lt; <span class="number">0</span> || t_date.m_minute &gt; <span class="number">59</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="存储类"><a href="#存储类" class="headerlink" title="存储类"></a>存储类</h3><p>而 <code>Storage</code> 类则负责完成数据增删改查（不负责操作的合法性验证）和序列化、反序列化，使用 <code>csv</code> 文件格式存储。难点在于 <code>csv</code> 文件的读取和写入。不过只要多加细心，就可以通过测试。<code>csv</code> 的读取最好是一个个字符读取，并维持一个状态机状态，实现起来逻辑比较清晰：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line">std::list&lt;std::list&lt;std::string&gt;&gt; <span class="built_in">parse_csv</span>(std::istream &amp;file) &#123;</span><br><span class="line">    std::list&lt;std::list&lt;std::string&gt;&gt; lines;</span><br><span class="line">    <span class="keyword">while</span> (!file.<span class="built_in">eof</span>()) &#123;</span><br><span class="line">        std::list&lt;std::string&gt; line_items;</span><br><span class="line">        <span class="type">bool</span> same_line = <span class="literal">true</span>;</span><br><span class="line">        <span class="type">int</span> ch;</span><br><span class="line">        <span class="keyword">while</span> (same_line &amp;&amp; (ch = file.<span class="built_in">get</span>()) != EOF) &#123;</span><br><span class="line">            std::string item;</span><br><span class="line">            <span class="keyword">if</span> (ch == <span class="string">&#x27;&quot;&#x27;</span>) &#123;</span><br><span class="line">                <span class="keyword">while</span> ((ch = file.<span class="built_in">get</span>()) != EOF) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (ch == <span class="string">&#x27;&quot;&#x27;</span>) &#123;</span><br><span class="line">                        ch = file.<span class="built_in">get</span>();</span><br><span class="line">                        <span class="keyword">if</span> (ch == <span class="string">&#x27;,&#x27;</span>) <span class="keyword">break</span>;</span><br><span class="line">                        <span class="keyword">if</span> (ch == <span class="string">&#x27;\n&#x27;</span> || ch == EOF) &#123;</span><br><span class="line">                            same_line = <span class="literal">false</span>;</span><br><span class="line">                            <span class="keyword">break</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                    item += ch;</span><br><span class="line">                &#125;</span><br><span class="line">                line_items.<span class="built_in">push_back</span>(item);</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                item += ch;</span><br><span class="line">                <span class="keyword">while</span> ((ch = file.<span class="built_in">get</span>()) != EOF) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (ch == <span class="string">&#x27;,&#x27;</span>) <span class="keyword">break</span>;</span><br><span class="line">                    <span class="keyword">if</span> (ch == <span class="string">&#x27;\n&#x27;</span>) &#123;</span><br><span class="line">                        same_line = <span class="literal">false</span>;</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                    item += ch;</span><br><span class="line">                &#125;</span><br><span class="line">                line_items.<span class="built_in">push_back</span>(item);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (!line_items.<span class="built_in">empty</span>()) lines.<span class="built_in">push_back</span>(line_items);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> lines;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>csv</code> 的写入相对来说就比较简单了，由于课程要求每一个字段都用双引号包裹起来，所以就不存在判断是不是需要加双引号的情况，但需要注意的是， <code>csv</code> 在遇到字段数据中包含双引号，逗号和换行符的时候，是需要加双引号的，并且双引号需要写成两个双引号的形式，用一个函数预处理一下字符串就好了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">std::string <span class="title">csv_value</span><span class="params">(<span class="type">const</span> std::string &amp;value)</span> </span>&#123;</span><br><span class="line">    std::string formatted;</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">auto</span> ch : value) &#123;</span><br><span class="line">        formatted += ch;</span><br><span class="line">        <span class="keyword">if</span> (ch == <span class="string">&#x27;&quot;&#x27;</span>) formatted += ch;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> std::<span class="built_in">move</span>(formatted);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="逻辑处理类"><a href="#逻辑处理类" class="headerlink" title="逻辑处理类"></a>逻辑处理类</h3><p><code>AgendaService</code> 类是整个程序最难编写的部分，在这个类里，需要对输入做合法性检验，这其中有许多的细节需要考虑：</p><ul><li>任何对数据的修改，都需要验证操作用户的合法性</li><li>任何涉及对会议参与者、发起者的增加，都需要验证用户存在性，并且用户在时间段内是空闲的，并且不会产生重复的参与者</li><li>一旦某个会议没有参与者，则需要删除这个会议</li></ul><p>其他的话就没有什么需要特别注意的地方了。据说这个类的单元测试代码有超过五百行，细节覆盖很全面，所以比较难全部通过测试用例，需要细心和耐心来修正代码。</p><h2 id="构建-RESTFul-API"><a href="#构建-RESTFul-API" class="headerlink" title="构建 RESTFul API"></a>构建 RESTFul API</h2><p>一开始想利用 <code>asio</code>、<code>restbed</code> 等库直接使用 <code>C++</code> 来构建后端，并想用 <code>Qt</code> 构建 GUI，后来经过一点调查，决定尝试融合不同的语言，在 <code>C++</code> 代码基础上，用 <code>Python</code> 构建后端（为什么不用 <code>Node.js</code>？当时没想到……而且以为它不能调用 <code>C++</code> 模块，事实上 <code>Node.js</code> 可以通过 <code>node-gyp</code> 来构建 <code>V8</code> 引擎可以调用的 <code>C++</code> 模块。）</p><p>首先需要将写代码将 <code>AgendaService</code> 类接口包装起来，具体方法参考这篇文章：<a href="https://blog.howardlau.me/programming/extending-python-with-cpp.html">将 C++ 程序编译成 Python 模块</a>。</p><p>之后便是通过 <code>djangorestframework</code> 包装这些方法，提供 <code>API</code> 供前端调用。这一部分主要都是编写包装代码。</p><h2 id="构建前端"><a href="#构建前端" class="headerlink" title="构建前端"></a>构建前端</h2><p>由于 Agenda 不是重交互类型的应用，所以可以使用 <code>React</code> 和 <code>React Material UI</code>，可以比较方便地做出比较好看的界面。前端采用的是 <code>SPA</code> 单页应用技术，使用 <a href="https://jwt.io/">JWT</a> 来进行用户身份验证。在用户创建会议时，添加参与人的时候会有自动补全，对会议的操作也很简单直接。</p><p>最终实现效果如下图所示：</p><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_150744.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_150744.png"></a></p><center>登录界面</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_150809.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_150809.png"></a></p><center>注册界面</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_151936.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_151936.png"></a></p><center>会议列表</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152028.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152028.png"></a></p><center>新增会议</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152050.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152050.png"></a></p><center>自动补全</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152132.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152132.png"></a></p><center>用户列表</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152154.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152154.png"></a></p><center>添加参与人</center><p><a href="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152305.png" data-fancybox="gallery" data-caption=""><img src="/projects/2018-sysu-software-engineering-agenda/Screenshot_20181117_152305.png"></a></p><center>查询功能</center><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>在本地调试好前后端之后，就可以在服务器上部署了。前端没有动态生成的 <code>HTML</code>，全部是静态文件，最好使用 <code>nginx</code> 等服务器来处理请求。而 <code>django</code> 应用一种部署方式就是通过 <code>uwsgi</code> 接口（<code>python manage.py runserver</code> 仅供调试使用）。为了避免跨域问题，API 和前端文件需要部署在同一个域名和端口下。</p><h3 id="部署前端"><a href="#部署前端" class="headerlink" title="部署前端"></a>部署前端</h3><p>首先对前端进行打包操作，运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm run build</span><br></pre></td></tr></table></figure><p>如果不想要 <code>.map</code> 文件，就运行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">GENERATE_SOURCEMAP=<span class="literal">false</span> npm run build</span><br></pre></td></tr></table></figure><p>将生成的 <code>build</code> 文件夹放在喜欢的地方，记录好路径。</p><h3 id="部署后端"><a href="#部署后端" class="headerlink" title="部署后端"></a>部署后端</h3><p>在 <code>django</code> 的应用目录下运行命令 </p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uwsgi --socket webagenda.sock --module webagenda.wsgi --chmod-socket=666 &amp; </span><br></pre></td></tr></table></figure><p>启动一个 <code>uwsgi</code> 服务进程。</p><h3 id="配置-nginx"><a href="#配置-nginx" class="headerlink" title="配置 nginx"></a>配置 <code>nginx</code></h3><p>在 <code>nginx</code> 配置文件里配置好静态前端文件的地址，并且指示其将 API 请求通过 <code>sock</code> 文件的方式传递给 <code>uwsgi</code> 服务进程。</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> agenda &#123;</span><br><span class="line">    <span class="attribute">server</span> unix:///path/to/webagenda/webagenda.sock;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">443</span> ssl http2 ;</span><br><span class="line">    <span class="attribute">listen</span> [::]:<span class="number">443</span> ssl http2;</span><br><span class="line">    <span class="attribute">ssl</span> <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">ssl_certificate</span> /path/to/your.crt;</span><br><span class="line">    <span class="attribute">ssl_certificate_key</span> /path/to/your.key;</span><br><span class="line">    <span class="attribute">server_name</span> your.domain;</span><br><span class="line">    <span class="attribute">root</span> /path/to/your/build;</span><br><span class="line">    <span class="attribute">index</span> index.html index.htm;</span><br><span class="line">    <span class="section">location</span> /api/v1 &#123;</span><br><span class="line">        <span class="attribute">uwsgi_pass</span>  agenda;</span><br><span class="line">        <span class="attribute">include</span> uwsgi_params;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>重启 <code>nginx</code>，打开指向的域名验证成功与否，就完成部署了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>FlapPGA Mario - 用 FPGA 编写游戏</title>
      <link>https://blog.howardlau.me/projects/flappga-mario-a-video-game-in-fpga.html</link>
      <description>
        <![CDATA[<p>完整代码参考：<a href="https://github.com/howardlau1999/flapga-mario">https://github.com/howardlau1999/flapga-mario</a></p>
<h2 id="构想蓝图"><a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/projects/">项目分享</category>
      <pubDate>Fri, 16 Nov 2018 07:21:18 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>完整代码参考：<a href="https://github.com/howardlau1999/flapga-mario">https://github.com/howardlau1999/flapga-mario</a></p><h2 id="构想蓝图"><a href="#构想蓝图" class="headerlink" title="构想蓝图"></a>构想蓝图</h2><p>在学习数字电路课程的时候，教授向我们提出了自愿完成一个 FPGA 项目的要求，还展示了往年学生的成果。可是大部分人选择完成的是多周期 MIPS CPU 的设计，有点无趣，其中有一个让我印象深刻，那就是有人用 FPGA 实现了一个类似打砖块的游戏，当时我就觉得这太酷了，所以我也选择了用 FPGA 做一个游戏。但是当时的我对 FPGA 一无所知，连 <code>Verilog</code> 的语法都不清楚，原本的构想是在 FPGA 上实现一台 FC，但是很快我就发现这不现实，所以我给自己定的目标是，不用 CPU，单纯用 FPGA 的硬件语言来实现一个游戏，而这个游戏最好达到下面的目标：</p><ul><li>由于没有 CPU，游戏逻辑不能太复杂</li><li>但是要够好玩</li><li>要有彩色画面，最好有动画</li><li>最好还没有人实现过</li></ul><p>经过一番搜索，我在网上看到了 <a href="https://embeddedthoughts.com/2016/12/09/yoshis-nightmare-fpga-based-video-game/">Yoshi’s Nightmare</a> 这篇博客，给了我很大的启发。一开始我想设计一个类似于“是男人就下一百层”的游戏，只不过主角换成了马里奥，后来发现这个也不简单，所以最终我确定了做 FlapPGA Mario，一个类似于 Flappy Bird 的游戏。下面先上视频~</p><iframe width="560" height="315" src="https://www.youtube.com/embed/SdphC7bAz7M" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><h2 id="架构概览"><a href="#架构概览" class="headerlink" title="架构概览"></a>架构概览</h2><p>参考 FC 等游戏机的硬件设计，经过设计中的不断迭代，最终形成了下图所示的系统架构：</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/flappga-mario_arch.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/flappga-mario_arch.png"></a></p><h2 id="VGA-模块"><a href="#VGA-模块" class="headerlink" title="VGA 模块"></a>VGA 模块</h2><p>要想进行视频输出，最底层的模块就是 VGA 同步单元，负责向显示器输出同步信号和像素信息，虽然这部分比较复杂，但由于是底层基础模块，所以有现成的代码可以直接使用，基于 Don’t reinvent the wheels 原则，我就直接拿来主义了，嘿嘿。当然，VGA 输出的原理还是要了解一下的。</p><h3 id="VGA-同步单元"><a href="#VGA-同步单元" class="headerlink" title="VGA 同步单元"></a>VGA 同步单元</h3><p>VGA 的扫描方式和 CRT 显示器很类似，有一条扫描线从左到右，从上到下地扫描每一个像素点，显示器则在收到同步信号之后点亮对应的像素点到指定的颜色。VGA 控制器需要发出 HS 信号和 VS 信号对显示器进行同步，然而，并不是每一个扫描点都可以显示像素，下面这幅图展现了扫描的过程：</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/basys3-_vga_crt_theory_timing.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/basys3-_vga_crt_theory_timing.png"></a></p><p>可以看到，在水平方向的左右，各有一段 “Porch” 时间，这个时间在一行扫描完成之后 CRT 显示器稳定电压的时间，因为 CRT 是通过控制电压来控制电子偏转的，电压的下降（也就是回到最左边）和稳定需要一点时间，在电压不稳定的情况下，不能输出像素信息；类似的，最上面和最下面也各有一段 “Porch” 时间，具体的时间和时钟周期由时钟频率和帧率还有分辨率共同决定，下面给出 640 x 480 @ 60Hz 的时钟信息：</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/basys3-_vga_highsidelowside_timings.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/basys3-_vga_highsidelowside_timings.png"></a></p><p>到这里，只需要按照表格信息编写 Verilog 程序就好了。同时，这个模块需要输出当前扫描的像素点的 x 坐标和 y 坐标。</p><h2 id="图形引擎"><a href="#图形引擎" class="headerlink" title="图形引擎"></a>图形引擎</h2><p>Basys 3 的 VGA 接口对 RGB 每个分量分别提供 4 位输出，也就是 12 比特颜色位深，可以显示 4096 色。经过简单的计算，Basys 3 提供的 BRAM 有 1800000 位比特，但是如果要将像素信息放到显存中则需要 640 x 480 x 12 &#x3D; 36864000 位比特，很显然这是远远不够的。所以我们需要根据当前 VGA 的像素坐标<strong>实时计算</strong>出像素点的颜色信息，事实上，在内存相当吃紧的年代，FC 也是这么做的，而 FC 有专门的 PPU (Picture Processing Unit) 进行相当复杂的像素操作，虽然不能直接实现一个 PPU，但是其中不少设计思想还是可以借鉴的。</p><h2 id="ROM-和-RAM"><a href="#ROM-和-RAM" class="headerlink" title="ROM 和 RAM"></a>ROM 和 RAM</h2><p>确定了存储方式之后，就可以编写 ROM 和 RAM 代码，RAM 的代码用的是 Vivado 自带的模板代码，ROM 的代码就更简单了，直接声明寄存器数组就可以了，这里我偷了个懒，存储图像的 ROM 我选择直接将坐标信息的 x, y 拼接成地址信息。要将数据在综合时读取到寄存器中的话，要用到 <code>&amp;#36;readmemh</code> 这个系统指令，网上有很多说明指令的使用方法，这里不再赘述。</p><h3 id="背景图层"><a href="#背景图层" class="headerlink" title="背景图层"></a>背景图层</h3><p>一般而言，游戏机会有两个图层，一个是背景图层，用来显示大块的图片，但不能自由移动，在内存中只存储下面的信息：</p><table><thead><tr><th align="center">第 8 位</th><th align="center">第 7 位</th><th align="center">第 6 位</th><th align="center">第 5:3 位</th><th align="center">第 2:0 位</th></tr></thead><tbody><tr><td align="center">启用</td><td align="center">上下翻转</td><td align="center">左右翻转</td><td align="center">Tile 行</td><td align="center">Tile 列</td></tr></tbody></table><p>这里 Tile 指的就是在背景 ROM 中存储的图像块，长宽固定。那么我们怎么知道当前的块要显示在什么地方呢？这由内存信息的地址决定，按行存储，直到铺满画面为止。这样就可以节省大量的空间，因为不用存储坐标信息了。</p><h3 id="活动图层"><a href="#活动图层" class="headerlink" title="活动图层"></a>活动图层</h3><p>另一个图层就是活动图层，一般用来显示需要灵活移动的图像块，比如游戏的主角等等，这时候我们就要存储具体的坐标信息了，占用的位数会更多：</p><table><thead><tr><th align="center">第 31 位</th><th align="center">第 27 位</th><th align="center">第 26 位</th><th align="center">第 25:16 位</th><th align="center">第 15:6 位</th><th align="center">第 5:3 位</th><th align="center">第 2:0 位</th></tr></thead><tbody><tr><td align="center">启用</td><td align="center">上下翻转</td><td align="center">左右翻转</td><td align="center">Y 坐标</td><td align="center">X 坐标</td><td align="center">Tile 行</td><td align="center">Tile 列</td></tr></tbody></table><p>同样地，Tile 的长宽是固定的。</p><h3 id="读取-ROM"><a href="#读取-ROM" class="headerlink" title="读取 ROM"></a>读取 ROM</h3><p>读取 ROM 的方式其实就是通过 Tile 行列信息和坐标信息，计算出在 ROM 中的地址，实现图像数据的存取。计算的方式很简单：如果当前坐标还没到达图像块的显示范围，就不读取；到达范围之后，读取的地址就是当前的 x, y 坐标减去图像块的 x, y 坐标，再加上 Tile 行列对应的偏移即可。</p><h3 id="图层融合"><a href="#图层融合" class="headerlink" title="图层融合"></a>图层融合</h3><p>两个图层需要在渲染的时候融合起来，每个图层的引擎需要负责输出当前的像素是否为透明的信息，然后由上层模块通过这个信息和遮挡关系计算出最终显示的像素信息，这样需要我们确定一个透明色，当引擎遇到这个颜色的时候就认为当前像素是透明的（注意和黑色不同），我选择的是纯蓝，下面展示游戏用到的背景图像块和活动图像块:</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/bg_sprites.bmp" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/bg_sprites.bmp"></a> <a href="/projects/flappga-mario-a-video-game-in-fpga/mario.bmp" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/mario.bmp"></a></p><h2 id="音频引擎"><a href="#音频引擎" class="headerlink" title="音频引擎"></a>音频引擎</h2><p>由于 Basys 3 没有 DAC 模块，我们需要用 PWM 来模拟正弦波。尽管方波更方便，正弦波听上去好一点。实现音频输出的办法是先对正弦波进行离散采样成 64 个点：</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/sine_disc.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/sine_disc.png"></a></p><p>Basys 3 的系统时钟为 100 MHz，那么我们的计数器应该设置成 100MHz &#x2F; freq，这样就是指定频率的方波，由于一个周期内有 64 个点，这个计数器值还需要除以 64，得到 100MHz &#x2F; freq &#x2F; 64，然后我们设置一个计数器，每当计数器的值达到这个值的时候，就取下一个采样点，然后在计数周期内一直输出这个幅度值。</p><p>而 PWM 模块就根据幅度值进行 PWM 脉冲调整，达到输出正弦波的目的。需要注意的是，我们需要将幅度值变成正数。</p><p>而存储在游戏里的音乐不是波形，而是一个个音符：</p><table><thead><tr><th align="center">第 31:16 位</th><th align="center">第 15:0 位</th></tr></thead><tbody><tr><td align="center">频率计数器（计算后的）</td><td align="center">持续时间（ms）</td></tr></tbody></table><h2 id="游戏逻辑"><a href="#游戏逻辑" class="headerlink" title="游戏逻辑"></a>游戏逻辑</h2><h3 id="马里奥"><a href="#马里奥" class="headerlink" title="马里奥"></a>马里奥</h3><p>这个游戏其实只用到一个上键，每当我们按键的时候，需要将马里奥的状态设置成“上升”，并切换人物动画，然后给定一个初始计数器值，每当计数器到达零，就让马里奥上升一个像素，然后给一个大一点的计数器值，这样就实现了有重力的上升，直到计数器值达到最大设定值为止，此时把状态设置成“下降”，马里奥开始掉落，同样给一个初始计数器值，不过这个计数器值是慢慢变小的，就是实现了加速下落的效果。马里奥模块需要告诉上层模块当前的位置，方便碰撞检测。</p><h3 id="水管生成"><a href="#水管生成" class="headerlink" title="水管生成"></a>水管生成</h3><p>首先确定水管信息如何记录，需要存储的信息有：水管左端的 X 坐标，水管上端的结束 Y 位置，下端的开始 Y 位置，全部是以背景图像块作为索引。游戏中水管的数量其实是恒定为 3 个的，并且设置好了初始值，按照一定周期向左移动，当检测到一个水管到达了画面最左端，就重置信息到最右边，并随机给两端开始结束赋值。游戏引擎根据水管信息向背景内存写入图像信息。</p><h3 id="金币生成"><a href="#金币生成" class="headerlink" title="金币生成"></a>金币生成</h3><p>如果仅仅是躲水管，有点无聊，所以还加入了金币这个设定，生成的方式和存储信息和水管大同小异，不同的是金币有动画效果，同样以一定周期更新动画帧数信息，然后游戏引擎根据信息写入背景内存。</p><h3 id="碰撞检测"><a href="#碰撞检测" class="headerlink" title="碰撞检测"></a>碰撞检测</h3><p><a href="/projects/flappga-mario-a-video-game-in-fpga/collisions_overlap.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/collisions_overlap.png"></a></p><p>由于游戏中各个图像都是方形，碰撞检测其实就是检测两个矩形有没有交集，为了简单起见，这里不考虑像素透明的问题。如果马里奥碰撞到了水管，就设置一个 <code>game_over</code> 标志，游戏不再进行；碰到金币则设置一个 <code>coin_eaten</code> 标志，并重新生成金币。</p><h3 id="分数统计"><a href="#分数统计" class="headerlink" title="分数统计"></a>分数统计</h3><p>当一个水管的 X 坐标小于马里奥并且此时游戏没有结束，就可以认为马里奥成功通过了水管，给分数加上一分，如果马里奥吃到了金币，那么就加十分。分数显示在屏幕的最上方。</p><h3 id="滚动逻辑"><a href="#滚动逻辑" class="headerlink" title="滚动逻辑"></a>滚动逻辑</h3><p>前面提到，背景图层是整整齐齐按照图像块的方式排列的，那么要怎么滚动呢？方法是设置一个 <code>scroll_x_offset</code> 变量来对整个画面进行向左偏移，只需要将其加在传给背景引擎的 x 值上就能达到目的。这个变量的最大值应该是 Tile 的宽度减 1，当这个值达到最大的时候，我们需要更新所有水管的 X 坐标，使其减 1，这样就可以造成水管连续向左滚动的效果了！</p><h4 id="滚动分裂"><a href="#滚动分裂" class="headerlink" title="滚动分裂"></a>滚动分裂</h4><p>前面提到，画面的最上方显示的是分数，分数也是通过背景来显示的，如果分数跟着水管滚动，那看上去就太蠢了！所以我们需要用到“滚动分裂”的技术，就像下图所示：</p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/split_scroll1.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/split_scroll1.png"></a></p><p><a href="/projects/flappga-mario-a-video-game-in-fpga/split_scroll2.png" data-fancybox="gallery" data-caption=""><img src="/projects/flappga-mario-a-video-game-in-fpga/split_scroll2.png"></a></p><p>横线表示当前的扫描行，竖线表示滚动偏移量，可以看到在显示分数的时候，偏移量为 0，等到显示游戏内容，才开始设置偏移量。因此我们也可以借鉴一下，当 y 扫描分数的时候，我们不设置偏移量，等到 y 进入游戏内容范围，才设置应有的偏移量。这样就可以分开滚动的内容。其实利用这个技巧，可以达到画面扭曲，伪 3D 等奇妙的效果，虽然简单，但是强大！</p><h4 id="视差滚动"><a href="#视差滚动" class="headerlink" title="视差滚动"></a>视差滚动</h4><p>视频里可以看到背景图滚动速度比水管要慢，制造出了视差滚动效果，其实这个效果实现很简单，就是给最底层的滚动逻辑传一个分频过的（也就是频率变慢了的）时钟信号即可~</p><h3 id="数据读写控制"><a href="#数据读写控制" class="headerlink" title="数据读写控制"></a>数据读写控制</h3><p>由于没有 CPU，加上当时也没有数据总线这些概念……所以我就粗暴用时钟来控制不同游戏数据的写入，首先将水管数据写入背景内存，然后将金币写入，然后是分数显示，而对象内存独立于背景内存，也就不用轮流写入了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>完整代码参考：<a href="https://github.com/howardlau1999/flapga-mario">https://github.com/howardlau1999/flapga-mario</a></p><p>前前后后，这个项目花了我三周的课余时间，回头看其实个人觉得完成度还是不错的 XD。从零开始写一个游戏，还是在 FPGA 上，而且我还没有基础，非常具有挑战性，但也非常有趣。在这个过程中，其实曾推倒过一次项目重来，因为当时设计的显示方法有缺陷 :( </p><p>完成这个项目需要极大的恒心和细心，由于是输出到 VGA，没有办法通过仿真波形调试，而且没有调试工具可以用，也没有太多的参考资料，需要大量的思考，甚至需要借鉴一些老式游戏机的设计思路，比如 FC 就给了我很多很多的启发（感谢任天堂！）。而每次修改之后都要烧板验证，通过观察程序表现推测可能出错的代码，并且需要仔细阅读才能找出一些细微的错误。而且在项目初期，遇到了很多失败，使得我多次产生放弃的念头，但是我还是想坚持把这个项目真真切切地做出来。</p><p>完成项目还需要多方面的知识，例如图像、音频的存储和输出等，还需要协调大量的模块，对于设计能力也是一个极大的考验。总之，做这个项目，我个人感觉收获良多！</p><h2 id="展望"><a href="#展望" class="headerlink" title="展望"></a>展望</h2><p>当然，由于只花了三周时间（还是课余）来完成，这个项目改进空间还很大，比如：</p><ul><li>由于没有 CPU，游戏的复杂度受限，而且远没有充分发挥 Basys 3 的潜力，未来可以考虑加入 CPU</li><li>音频引擎只能反复播放同一段 BGM，也没有音效</li><li>活动块引擎最多只能同时处理 8 个活动块</li></ul><p>不过考虑到时间，我个人还是很满意这个成果的~</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="图像和音频的转换"><a href="#图像和音频的转换" class="headerlink" title="图像和音频的转换"></a>图像和音频的转换</h3><p>图像处理比较简单，只需要读取图像文件的像素点，然后重新计算 RGB 值，输出成十六进制即可。音频转换比较复杂，需要读取 MIDI 文件，分离轨道，然后得到每个音符对应的频率和时间，计算后再存入十六进制中。资源的准备都是通过 <code>Python</code> 脚本来完成的。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://reference.digilentinc.com/basys3/refmanual">Basys 3 Reference</a></li><li><a href="https://embeddedthoughts.com/2016/12/09/yoshis-nightmare-fpga-based-video-game/">Yoshi’s Nightmare (an FPGA based game)</a></li><li><a href="https://wiki.nesdev.com/w/index.php/PPU">NES (FC) Picture Processing Unit (PPU) hardware behaviours</a></li><li><a href="https://retrocomputing.stackexchange.com/questions/1898/how-can-i-create-a-split-scroll-effect-in-an-nes-game">Split Scrolling</a></li><li><a href="http://www.instructables.com/id/Basys3-FPGA-Digital-Audio-Synthesizer/">Audio output</a></li><li><a href="http://sites.uci.edu/camp2014/2014/05/19/timing-in-midi-files/">MIDI timing</a></li></ol><h2 id="注"><a href="#注" class="headerlink" title="注"></a>注</h2><ol><li><p>项目作为课程设计完成于 2018 年 6 月，本文写作（2018 年 11 月）基于当时提交的项目报告：<a href="/projects/flappga-mario-a-video-game-in-fpga/FlapPGA_Mario.pdf">FlapPGA_Mario.pdf</a></p></li><li><p>当时音乐处理使用了 <code>mido</code> 库，非常不方便，各种信息需要手动计算，后来发现了 <code>pretty_midi</code> 处理起来方便很多，推荐使用这个库。</p></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>Python 中互相切换流式和数组式存取</title>
      <link>https://blog.howardlau.me/programming/python-stream-list.html</link>
      <description>
        <![CDATA[<p>在 Python 中，存取文件的一般操作是像下面这样的：</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Thu, 15 Nov 2018 09:45:59 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在 Python 中，存取文件的一般操作是像下面这样的：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;foobar.txt&quot;</span>, <span class="string">&quot;rb+&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    data = f.read(<span class="number">42</span>)</span><br><span class="line">    data[<span class="number">0</span>:<span class="number">5</span>] = <span class="string">b&#x27;bytes&#x27;</span></span><br><span class="line">    f.seek(-<span class="number">42</span>, <span class="number">1</span>)</span><br><span class="line">    f.write(data)</span><br></pre></td></tr></table></figure><p>对于文件操作，我们需要通过 <code>read()</code>、<code>write()</code>、<code>seek()</code> 等函数来进行流式存取。某些情况下，这种操作显得非常不便，比如说我们想修改文件中某些字节，则需要先读取出来到 <code>bytes</code> 中，做完修改之后 <code>seek()</code> 回到原来的位置，再 <code>write()</code>。而借助 <code>mmap</code> 就可以方便地完成这些操作。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> mmap</span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;foobar.txt&quot;</span>, <span class="string">&quot;rb+&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    data = mmap.mmap(f.fileno(), <span class="number">0</span>)</span><br><span class="line">    data[<span class="number">0</span>:<span class="number">5</span>] = <span class="string">b&#x27;bytes&#x27;</span></span><br></pre></td></tr></table></figure><p><code>mmap</code> 其实是 Linux 系统下的一个系统调用，作用是把文件映射到内存中，方便存取。<code>mmap.mmap()</code> 就完成了这样的功能，第一个参数是文件句柄，第二个参数是映射的最大长度（从文件头算起，如果为 0 则为整个文件的大小）。需要注意的是，如果要打开一个有写缓冲的文件，需要先对其 <code>flush()</code> 获得 <code>mmap</code> 对象之后，我们就可以像普通的 <code>bytes</code> 一样通过 <code>slice</code> 进行存取了。还有需要注意的是，<code>mmap</code> 不支持删除某一段内容：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; del data[0:3]</span><br><span class="line">Traceback (most recent call last):</span><br><span class="line">  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;</span><br><span class="line">TypeError: mmap object doesn&#x27;t support slice deletion</span><br><span class="line">mmap object doesn&#x27;t support slice deletion</span><br></pre></td></tr></table></figure><p>而有时候我们希望对 <code>bytes</code> 进行流式存取，这时候只需要使用 <code>BytesIO</code> 将其包装起来，就可以像文件一样读写了。类似的还有 <code>StringIO</code> 等：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> io <span class="keyword">import</span> BytesIO</span><br><span class="line"><span class="comment"># or from six import BytesIO</span></span><br><span class="line">stream = BytesIO(<span class="string">b&#x27;foobar&#x27;</span>)</span><br><span class="line">stream.write(<span class="string">b&#x27;bar&#x27;</span>)</span><br></pre></td></tr></table></figure><p>参考资料：</p><ol><li><a href="https://docs.python.org/3/library/io.html">https://docs.python.org/3/library/io.html</a></li><li><a href="https://docs.python.org/3/library/mmap.html">https://docs.python.org/3/library/mmap.html</a></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>二维离散卷积转换为矩阵相乘——卷积与反卷积</title>
      <link>https://blog.howardlau.me/machine-learning/convolution-to-matrix-multiplication.html</link>
      <description>
        <![CDATA[<p>假设我们需要将矩阵</p>
<p>$$
\mathbf{X} &#x3D; \begin{bmatrix}
x_1 &amp; x_2 &amp; x_3 \\
x_4 &amp; x_5 &amp; x_6 \\
x_7 &amp; x_8 &amp;]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Thu, 15 Nov 2018 06:28:48 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>假设我们需要将矩阵</p><p>$$\mathbf{X} &#x3D; \begin{bmatrix}x_1 &amp; x_2 &amp; x_3 \\x_4 &amp; x_5 &amp; x_6 \\x_7 &amp; x_8 &amp; x_9\end{bmatrix}$$</p><p>以</p><p>$$\mathbf{H}&#x3D;\begin{bmatrix}h_1 &amp; h_2 \\h_3 &amp; h_4\end{bmatrix}$$</p><p>为卷积核做卷积操作，普通操作是用卷积核在矩阵上进行滚动相乘相加，但是这样不利于 GPU 等硬件加速运算，一般而言，矩阵的卷积会先转换为稀疏矩阵的乘法，再通过 BLAS 等已经极度优化过的线性代数计算库来计算，效率可以得到提升。</p><p>下面说明一下转换的方法：对于原来的矩阵，我们展开成一个列向量 </p><p>$$\mathbf{X} &#x3D; \begin{bmatrix}x_1 \\x_2 \\x_3 \\x_4 \\x_5 \\x_6 \\x_7 \\x_8 \\x_9\end{bmatrix}$$</p><p>（说明：以下卷积操作没有对卷积核进行旋转操作，但道理类似）之后我们对卷积核进行循环展开，展开成为这样的矩阵：</p><p>$$\mathbf{H} &#x3D; \begin{bmatrix} h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 &amp; 0 &amp; 0 \\0 &amp; 0 &amp; 0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 \\0 &amp; 0 &amp; 0 &amp; 0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 \\\end{bmatrix}$$</p><p>最后计算两个矩阵的乘积 $\mathbf{HX}$ 就得到了：</p><p>$$\begin{bmatrix} h_1x_1+h_2x_2+h_3x_4+h_4x_5 \\h_1x_2+h_2x_3+h_3x_5+h_4x_6 \\h_1x_4+h_2x_5+h_3x_7+h_4x_8 \\h_1x_5+h_2x_6+h_3x_8+h_4x_9 \\\end{bmatrix}$$</p><p>最后对这个列向量进行 <code>reshape</code> 操作就得到：</p><p>$$\begin{bmatrix} h_1x_1+h_2x_2+h_3x_4+h_4x_5 &amp;h_1x_2+h_2x_3+h_3x_5+h_4x_6 \\h_1x_4+h_2x_5+h_3x_7+h_4x_8 &amp;h_1x_5+h_2x_6+h_3x_8+h_4x_9 \\\end{bmatrix}$$</p><p>正是卷积的结果。</p><p>那么通用的做法是什么呢？假设输入矩阵 $\mathbf{X}$ 维数为 $m_1 \times n_1$，卷积核 $\mathbf{H}$ 维数为 $m_2 \times n_2$，输出尺寸则为 $(m_1+m_2-1) \times (n_1+n_2-1)$</p><p>首先将卷积核向右上补零为输出结果的尺寸（举例为 $4 \times 4$）：</p><p>$$\mathbf{H}&#x3D;\begin{bmatrix} 0 &amp; 0 &amp; 0 &amp; 0 \\ 0 &amp; 0 &amp; 0 &amp; 0 \\ h_1 &amp; h_2 &amp; 0 &amp; 0 \\ h_3 &amp; h_4 &amp; 0 &amp; 0 \end{bmatrix}$$</p><p>从下到上将每一行展开成列数为 $n_1$ 的 <a href="https://en.wikipedia.org/wiki/Toeplitz_matrix">Toeplitz 矩阵</a>：</p><p>$$\mathbf{F_0} &#x3D; \begin{bmatrix} h_3 &amp; 0 &amp; 0 \\h_4 &amp; h_3 &amp; 0 \\0 &amp; h_4 &amp; h_3 \\0 &amp; 0 &amp; h_4\end{bmatrix}$$</p><p>$$\mathbf{F_1} &#x3D; \begin{bmatrix} h_1 &amp; 0 &amp; 0 \\h_2 &amp; h_1 &amp; 0 \\0 &amp; h_2 &amp; h_1 \\0 &amp; 0 &amp; h_2\end{bmatrix}$$</p><p>$$\mathbf{F_3} &#x3D; \mathbf{F_2} &#x3D; \begin{bmatrix} 0 &amp; 0 &amp; 0 \\0 &amp; 0 &amp; 0 \\0 &amp; 0 &amp; 0 \\0 &amp; 0 &amp; 0\end{bmatrix}$$</p><p>再将这些矩阵组合成 Doubly Blocked Toeplitz 矩阵，此时列数为 $m_1$：</p><p>$$\mathbf{D} &#x3D; \begin{bmatrix} \mathbf{F_0} &amp; \mathbf{0} &amp; \mathbf{0} \\\mathbf{F_1} &amp; \mathbf{F_0} &amp; \mathbf{0} \\\mathbf{F_2} &amp; \mathbf{F_1} &amp; \mathbf{F_0} \\\mathbf{F_3} &amp; \mathbf{F_2} &amp; \mathbf{F_1} \\\end{bmatrix}$$</p><p>然后我们同样展开 $\mathbf{X}$ 为列向量，不过要从下到上展开：</p><p>$$\mathbf{X}&#x3D; \begin{bmatrix} x_7 \\ x_8 \\ x_9 \\ x_4 \\ x_5 \\ x_6 \\ x_1 \\ x_2 \\ x_3\end{bmatrix}$$</p><p>然后计算 $\mathbf{D}\mathbf{X}$ 即可，计算完毕的结果同样进行 <code>reshape</code>，然后<strong>上下翻转</strong>就得到了我们想要的结果。</p><h2 id="反卷积"><a href="#反卷积" class="headerlink" title="反卷积"></a>反卷积</h2><p>在 <code>pytorch</code> 中，反卷积也叫 TransposedConvolution，这是为什么呢，我们看到文章前半段提到的矩阵乘法：</p><p>$$\mathbf{X}&#x3D; \begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ x_4 \\ x_5 \\ x_6 \\ x_7 \\ x_8 \\ x_9\end{bmatrix}$$</p><p>$$\mathbf{H} &#x3D; \begin{bmatrix}  h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\                   0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 &amp; 0 &amp;  0 \\                   0 &amp; 0 &amp; 0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4 &amp; 0 \\                   0 &amp; 0 &amp; 0 &amp; 0 &amp; h_1 &amp; h_2 &amp; 0 &amp; h_3 &amp; h_4  \\\end{bmatrix}$$</p><p>$\mathbf{X}$ 是一个 $9\times1$ 的矩阵，而 $\mathbf{H}$ 是一个 $4\times9$ 的矩阵，因此 $\mathbf{HX}$ 就是一个 $4\times1$ 的矩阵，<code>reshape</code> 之后得到的是 $2\times2$ 的矩阵。</p><p>假设我们现在有一张 $2\times2$ 的特征图，要怎么反卷积得到一张上采样之后的图呢？看到这里你已经明白了，方法意外地简单，只需要把这个特征图也展开成列向量，然后用上面的卷积矩阵的<strong>转置</strong>进行左乘，就得到我们想要的结果！</p><p>展开成列向量的特征图尺寸是 $4\times1$，$\mathbf{H}^{\mathsf{T}}$ 的尺寸是 $9\times4$，左乘之后得到 $9\times1$ 的矩阵，进行 <code>reshape</code> 得到了 $3\times3$ 的原始图！</p><p>当然，在实际中，卷积与反卷积的权重不一定需要一样，尺寸也可以不一样。</p><p>参考资料：</p><p><a href="https://stackoverflow.com/questions/16798888/2-d-convolution-as-a-matrix-matrix-multiplication">https://stackoverflow.com/questions/16798888/2-d-convolution-as-a-matrix-matrix-multiplication</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>将 C++ 程序编译成 Python 模块</title>
      <link>https://blog.howardlau.me/programming/extending-python-with-cpp.html</link>
      <description>
        <![CDATA[<p>有些时候我们需要将已有的 C&#x2F;C++ 程序编译成 Python 模块方便调用，其实 Python 提供了简便的支持。主要分为以下几步：</p>
<h2 id="在-C-C-中编写-Python-方法"><a href="#在-C-C-中编写-Python-方法"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 22 Oct 2018 23:43:45 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>有些时候我们需要将已有的 C&#x2F;C++ 程序编译成 Python 模块方便调用，其实 Python 提供了简便的支持。主要分为以下几步：</p><h2 id="在-C-C-中编写-Python-方法"><a href="#在-C-C-中编写-Python-方法" class="headerlink" title="在 C&#x2F;C++ 中编写 Python 方法"></a>在 C&#x2F;C++ 中编写 Python 方法</h2><p>为了举例方便，假设我们要写一个简单的加法模块。要开始在 C&#x2F;C++ 程序中编写 Python 方法，首先要包含 <code>Python.h</code> 这个头文件。然后根据需要声明函数，函数的形式非常统一，返回值和参数都是 <code>PyObject*</code> 类型，具体实现方法可以参考下面的代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;Python.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> PyObject* <span class="title function_">add</span><span class="params">(PyObject* self, PyObject* args)</span> &#123;</span><br><span class="line">    <span class="type">int</span> a, b;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!PyArg_ParseTuple(args, <span class="string">&quot;ii&quot;</span>, &amp;a, &amp;b)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> sum = a + b;</span><br><span class="line">    PyObject* ret = Py_BuildValue(<span class="string">&quot;i&quot;</span>, sum);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于每一个函数，要得到其中的参数，可以使用 <code>PyArg_ParseTuple</code> 这个函数来获取 Python 传进来的参数（保存在 <code>args</code> 这个变量中），<code>args</code> 可以理解为 Python 传进来的 Tuple，但在 C&#x2F;C++ 中需要我们一个个提取其中的参数，所以，其用法非常接近于 <code>sscanf</code>，区别在于格式字符串的写法而已，对于格式字符串中不同字母代表的意义，这里不再赘述，可以参考官方文档： <a href="https://docs.python.org/3/c-api/arg.html">https://docs.python.org/3/c-api/arg.html</a></p><p>这个函数解析成功则返回 <code>true</code>，失败则是 <code>false</code>。当我们获取了所需的参数之后，就可以调用 C&#x2F;C++ 方法来进行处理。</p><p>处理完成的结果需要我们手动保存成一个 <code>PyObject*</code> 返回给 Python，保存的过程就是解析的反过程，用到的函数是 <code>Py_BuildValue</code>，同样提供一个格式字符串，然后提供对应的变量即可。当然，构造返回变量也可以用另外一种办法：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">PyObject* ret = PyLong_FromLong(success);</span><br></pre></td></tr></table></figure><p>其中 <code>Py*_From*</code> 顾名思义，就是用对应类型的 C 变量构造一个对应类型的 Python 对象。</p><h2 id="声明方法列表"><a href="#声明方法列表" class="headerlink" title="声明方法列表"></a>声明方法列表</h2><p>当我们写好所需要的方法之后，要定义一个数组告诉 Python 哪些方法应该以什么方式调用，代码像下面一样：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> PyMethodDef MySumMethods[] = &#123;</span><br><span class="line">    &#123;<span class="string">&quot;add&quot;</span>, add, METH_VARARGS, <span class="string">&quot;Add two integers&quot;</span>&#125;,</span><br><span class="line">    &#123;<span class="literal">NULL</span>, <span class="literal">NULL</span>, <span class="number">0</span>, <span class="literal">NULL</span>&#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>数组元素结构很简单，每一个元素都用四个信息定义一个 Python 方法：Python 里的函数名、实际调用的 C 方法、Python 传参类型（大部分时候是 <code>METH_VARARGS</code> 或者 <code>METH_VARARGS | METH_KEYWORDS</code>）、对于该方法的描述，最后以 <code>{NULL, NULL, 0, NULL}</code> 结尾。</p><p>声明完方法之后，就可以声明一个模块并导出了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="class"><span class="keyword">struct</span> <span class="title">PyModuleDef</span> <span class="title">mysummodule</span> =</span> &#123;PyModuleDef_HEAD_INIT, <span class="string">&quot;mysum&quot;</span>,</span><br><span class="line">                                            <span class="literal">NULL</span>, <span class="number">-1</span>, MySumMethods&#125;;</span><br><span class="line"></span><br><span class="line">PyMODINIT_FUNC <span class="title function_">PyInit_mysum</span><span class="params">()</span> &#123; <span class="keyword">return</span> PyModule_Create(&amp;mysummodule); &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>代码也很简单，只要用类似的方法，根据定义传递信息即可。需要注意的是 <code>PyInit_mysum()</code> 这个函数是 Python 用来初始化模块的，后面的名字要和模块名字对应。</p><h2 id="编写-setup-py-并编译"><a href="#编写-setup-py-并编译" class="headerlink" title="编写 setup.py 并编译"></a>编写 setup.py 并编译</h2><p>至此，我们已经完成了一个 Python 模块所需要的必须代码，接下来的工作就是要将其编译成 Python 可以调用的模块，这项工作是通过编写 <code>setup.py</code> 并调用 <code>distutils</code> 来完成的。<code>setup.py</code> 的代码如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> distutils.core <span class="keyword">import</span> setup, Extension</span><br><span class="line"></span><br><span class="line">mysum = Extension(<span class="string">&#x27;mysum&#x27;</span>,</span><br><span class="line">                     include_dirs=[<span class="string">&#x27;./include&#x27;</span>],</span><br><span class="line">                     language=<span class="string">&quot;c++&quot;</span>,</span><br><span class="line">                     sources=[<span class="string">&#x27;mysum.cpp&#x27;</span>])</span><br><span class="line"></span><br><span class="line">setup(name=<span class="string">&#x27;mysum&#x27;</span>,</span><br><span class="line">      version=<span class="string">&#x27;0.1&#x27;</span>,</span><br><span class="line">      description=<span class="string">&#x27;Python interfaces of a C++ program&#x27;</span>,</span><br><span class="line">      ext_modules=[mysum])</span><br></pre></td></tr></table></figure><p>代码也十分简单，只需要按照格式填写需要的信息即可。编写完成之后，我们就可以用 <code>pip install .</code> 来安装我们写好的模块了。之后就像调用普通 Python 模块一样调用我们自己的模块啦。</p><p><a href="/programming/extending-python-with-cpp/Screenshot_20181023_153107.png" data-fancybox="gallery" data-caption=""><img src="/programming/extending-python-with-cpp/Screenshot_20181023_153107.png"></a></p><p>至于更多的细节，比如如何传递字典参数，就请大家自行参考官方文档吧。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>DeeCamp 日记 | 开营</title>
      <link>https://blog.howardlau.me/deecamp-2018/deecamp-1-opening.html</link>
      <description>
        <![CDATA[<p>五月底看到 DeeCamp]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/deecamp-2018/">DeeCamp 2018</category>
      <pubDate>Mon, 23 Jul 2018 07:48:41 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>五月底看到 DeeCamp 相关通知，怀着试一试的心态发了一封极其简陋的简历报名，六月初参加笔试面试，到六月底幸运地收到了录取通知书，七月中考完期末考急急忙忙地赶到机场，经历了飞机返航等各种惊心动魄的意外之后，终于一路舟车劳顿来到了首都北京，在北京吃喝玩乐了三四天之后，终于等到了期待已久的 DeeCamp 的正式开营。</p><p>DeeCamp 模仿了 FooCamp 等一群极客工程师们自发组织的会议形式。这种会议并没有传统会议一般有着固定议程和主持人，全部是由与会者自发讨论决定主题，决定会议的进程。而 DeeCamp 在第一周会邀请产业第一线的大佬们来进行知识授课，在随后的三周半时间里，则由参加 DeeCamp 训练营的学员们按小组进行课题实践，全程有产业导师和学业导师的指导，做出一个 AI Demo。训练营鼓励学员们自主探索，尽情发挥创造力，这也是 DeeCamp 创办的初衷。</p><p>在开营第一天的早上，按照惯例是自我介绍等常规环节。而下午则邀请了李开复老师和图灵奖得主 John Hopcroft 进行现场授课。</p><h2 id="李开复老师演讲笔记"><a href="#李开复老师演讲笔记" class="headerlink" title="李开复老师演讲笔记"></a>李开复老师演讲笔记</h2><p><a href="/deecamp-2018/deecamp-1-opening/vbox8085_BX4Q8286_141343_small.jpg" data-fancybox="gallery" data-caption=""><img src="/deecamp-2018/deecamp-1-opening/vbox8085_BX4Q8286_141343_small.jpg"></a> 李开复老师在演讲</p><p>李开复老师演讲的主题是“产业需要怎样的 AI 人才”，在此简要的做一下开复老师的演讲笔记给大家分享。</p><h3 id="人工智能行业发展趋势"><a href="#人工智能行业发展趋势" class="headerlink" title="人工智能行业发展趋势"></a>人工智能行业发展趋势</h3><h4 id="人工智能的三次变革"><a href="#人工智能的三次变革" class="headerlink" title="人工智能的三次变革"></a>人工智能的三次变革</h4><p>人工智能从 1960 年代以来，首先经历了符号主义，也就是传统的 if-then-else 来进行人工构建知识，毫无疑问，这样做只能覆盖很少的方面，人工智能迎来寒冬；随后发展到了统计主义，主张通过大量数据来发现其中的统计学规律，以此发展人工智能，这时开复老师提到 1980 年代他在做语音识别系统的时候向导师申请要了 100MB 的硬盘来存放 4000 句语料，而在当时这么点容量的硬盘就要价 100 万人民币，受限于落后的硬件，人工智能再次迎来寒冬；而随着科技和互联网的发展，在大量数据和计算力的支撑下，统计学再次迎来春天，这一次人工智能则被冠上了“大数据+深度学习”的称号。（开复老师此时还推荐了美剧《黑镜》和《硅谷》）</p><h4 id="人工智能的四次浪潮"><a href="#人工智能的四次浪潮" class="headerlink" title="人工智能的四次浪潮"></a>人工智能的四次浪潮</h4><ol><li><strong>互联网智能化</strong>：例如美图秀秀等 App，用户的每一次操作都在默默地提供着标注数据，比如拍照了之后分享、保存、修图、删除等都是为图片的质量进行评价。AI 最有效率的应用就是互联网，互联网公司拥有海量用户，而且能形成闭环，进行大数据的交互，在垂直细分领域，也有独角兽公司可以和巨无霸公司同台竞技，比如今日头条、快手等。不过这是 Consumer AI，需要产品前期一定的用户累积，还有需要长时间的数据累积。</li><li><strong>商业智能化</strong>：例如第四范式，打造 AI 系统给银行进行理财产品的精准营销。这一类应用主要是应用在银行、保险、证券等已经有大量结构化标准化的数据的领域中。还有追一科技，专注于客服领域的聊天机器人。</li><li><strong>实体世界智能化</strong>：例如 Face++。主要是通过传感器的大量应用（如麦克风、摄像头等）将现实世界数字化，将过去没有用到的信息捕捉起来，用新应用、体验、界面等提升传统行业的体验。而传感器的大量应用会使价格下降，反过来促进传感器的普及。</li><li><strong>完全自动化</strong>：毫无疑问这是最有挑战性的，也是最激动人心的。例如无人驾驶，很有可能成为继 Windows、Android 之后的第三大操作系统，而且所有公司已经屈服并接受这一科技。最棒的人才与资本也正源源不断地涌入这一行业，正如当年的 PC 行业和手机行业；另一方面则是机器人，首先会从工业入手，再到商业、家用，首先创造巨大的商业价值，再慢慢迭代变便宜。然而可惜的是在这一方面，硬件的进步远远慢于软件的进步，因此需要大量的芯片和传感器的研发的跟进。</li></ol><h4 id="人工智能产业化的五个条件"><a href="#人工智能产业化的五个条件" class="headerlink" title="人工智能产业化的五个条件"></a>人工智能产业化的五个条件</h4><ol><li>海量数据（挑战：如何用更少、不涉及隐私的数据？）</li><li>客观精准标注（挑战：自动标注、Unsupervised Learning）</li><li>单一领域（挑战：如何做到跨领域？）</li><li>大量计算（挑战：能否减少计算量？）</li><li>AI 科学家（挑战：能否像 iOS 一样搭建一个 AI 平台供广大软件工程师发挥？）</li></ol><h4 id="中国人工智能的优势和挑战"><a href="#中国人工智能的优势和挑战" class="headerlink" title="中国人工智能的优势和挑战"></a>中国人工智能的优势和挑战</h4><p>优势：</p><ul><li>巨大市场，海量数据（算法重要，但数据更重要！）</li><li>产品创新开始领先全球</li><li>政策大力推动 AI 发展</li><li>政策和环境有利于 AI 发展，外国限制重重</li><li>政府的强大行政执行力</li></ul><p>挑战:</p><ul><li>产业差距大<ul><li>中国 AI 投资超过美国</li><li>中国 AI 独角兽产生</li><li>AI 被资本追捧</li><li>除了商业智能化五年后都是中国赶上</li></ul></li><li>顶尖人才巨大鸿沟<ul><li>AI 产业迫切需要更多人才</li><li>金字塔的顶尖也是象牙塔之中</li><li>改变世界还是要和产业连接</li></ul></li></ul><h4 id="社会颠覆的案例"><a href="#社会颠覆的案例" class="headerlink" title="社会颠覆的案例"></a>社会颠覆的案例</h4><p><strong>无人驾驶</strong>：如今的私家车 96% 都在闲置，而且价值在不断下降。等到以后无人驾驶普及了，人们在需要的时候车辆随叫随到，解决了交通拥塞问题、环境污染问题，同时，车与车之间可以进行高效的互通互联，实现更高的通行效率和更安全的通行体验。</p><p>总而言之，AI 将会完全取代大多数行业，但很少创造新的工作。未来那些低同情心，技术含量低的工作将毫无疑问将被 AI 取代。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>LeetCode | 115 不同的子序列</title>
      <link>https://blog.howardlau.me/programming/leetcode-115-distinct-subsequences.html</link>
      <description>
        <![CDATA[<p>题目地址：<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/leetcode/">LeetCode</category>
      <pubDate>Fri, 27 Apr 2018 22:08:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>题目地址：<a href="https://leetcode-cn.com/problems/distinct-subsequences/description/">https://leetcode-cn.com/problems/distinct-subsequences/description/</a>给定一个字符串 <strong>S </strong>和一个字符串 <strong>T</strong>，计算在 <strong>S</strong> 的子序列中 <strong>T</strong> 出现的个数。</p><p>一个字符串的一个子序列是指，通过删除一些（也可以不删除）字符且不干扰剩余字符相对位置所组成的新字符串。（例如，<code>&quot;ACE&quot;</code> 是 <code>&quot;ABCDE&quot;</code> 的一个子序列，而 <code>&quot;AEC&quot;</code> 不是）</p><p>比如：<code>S = &quot;rabbbit&quot;, T = &quot;rabbit&quot;</code>，则输出 <code>3</code>。</p><h3 id="思路一"><a href="#思路一" class="headerlink" title="思路一"></a>思路一</h3><p>最简单直接的思路就是暴力枚举 <code>S</code> 的所有子序列，然后逐一比较，时间复杂度为 $O(2^n)$，显然是不可接受的。</p><h3 id="思路二"><a href="#思路二" class="headerlink" title="思路二"></a>思路二</h3><p>一个比较容易想到的思路是递归求解，先在 <code>S</code> 中找到 <code>T</code> 的第一个字符，然后每找到一个就从 <code>S</code> 的下一个位置开始递归找 <code>T</code> 的下一个字符，递归的边界条件就是 <code>T</code> 的下标为 <code>T</code> 的长度，这时候说明找到了一个子序列，返回 <code>1</code>，然后将结果累加即可。代码如下：</p><p>函数中 <code>s_i</code> 和 <code>t_i</code> 表示从 <code>s</code> 的下标为 <code>s_i</code> 的地方开始查找 <code>t</code> 下标为 <code>t_i</code> 的字符，初始调用即为 <code>work(s, t, 0, 0)</code>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">vector&lt;vector&lt;<span class="type">int</span>&gt;&gt; cache;</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">numDistinctR</span><span class="params">(string &amp; s, string &amp; t, <span class="type">size_t</span> s_i, <span class="type">size_t</span> t_i)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (t_i == t.<span class="built_in">size</span>()) <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = s_i; i &lt; s.<span class="built_in">size</span>(); ++i) &#123;</span><br><span class="line">        <span class="keyword">if</span> (s[i] == t[t_i]) &#123;</span><br><span class="line">            <span class="keyword">if</span> (cache[i + <span class="number">1</span>][t_i + <span class="number">1</span>] &gt;= <span class="number">0</span>) sum += cache[i + <span class="number">1</span>][t_i + <span class="number">1</span>]; </span><br><span class="line">            <span class="keyword">else</span> sum += <span class="built_in">work</span>(s, t, i + <span class="number">1</span>, t_i + <span class="number">1</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    cache[s_i][t_i] = sum;</span><br><span class="line">    <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于直接递归会有大量重复计算，同样会超时，这里使用一个简单的优化技巧就是使用缓存，或者叫备忘录，来记录那些已经计算过的值。递归的办法比较容易想到，但效率不高，而且有可能发生栈溢出，这段代码的耗时为 <code>16ms</code>。</p><h3 id="思路三"><a href="#思路三" class="headerlink" title="思路三"></a>思路三</h3><p>通常这种递归是有可能通过动态规划来解决的，这道题也确实可以通过动态规划解决，那么我们应该怎么使用动态规划来求解这道题呢？首先我们要定义状态，我们可以定义 <code>dp[i][j]</code> 为 <code>s</code> 中前 <code>j</code> 个字符组成的字符串子序列为 <code>t</code> 中前 <code>i</code> 个字符的个数。无论 <code>t[i - 1]</code> 是否等于 <code>s[j - 1]</code>，<code>s</code> 多加上来一个字符是不影响原来的 <code>dp[i][j - 1]</code> 的，而如果加上来这个刚好跟 <code>t</code> 对应的字符相等，就会多出来 <code>dp[i - 1][j - 1]</code> 个子序列，所以我们就可以得到 <code>dp[i][j] = dp[i][j - 1] + t[i - 1] == s[j - 1] ? dp[i - 1][j - 1] : 0</code> 的递推关系。</p><p>动态规划还有一个需要思考的地方就是初始条件。很显然，<code>dp[0][·]</code> 也就是 <code>t</code> 为空的时候，子序列的个数永远是 <code>1</code>，而当 <code>t</code> 非空而 <code>s</code> 为空的时候，不可能有子序列。综合初始条件和递推关系，就得到了动态规划版的程序：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">numDistinctDP</span><span class="params">(string S, string T)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> dp[T.<span class="built_in">size</span>() + <span class="number">1</span>][S.<span class="built_in">size</span>() + <span class="number">1</span>];  </span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt;= S.<span class="built_in">size</span>(); ++i)</span><br><span class="line">        dp[<span class="number">0</span>][i] = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= T.<span class="built_in">size</span>(); ++i)</span><br><span class="line">        dp[i][<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= T.<span class="built_in">size</span>(); ++i)</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = <span class="number">1</span>; j &lt;= S.<span class="built_in">size</span>(); ++j)</span><br><span class="line">            dp[i][j] = dp[i][j - <span class="number">1</span>] + dp[i - <span class="number">1</span>][j - <span class="number">1</span>] * (S[j - <span class="number">1</span>] == T[i - <span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> dp[T.<span class="built_in">size</span>()][S.<span class="built_in">size</span>()];  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>动态规划版少了递归时函数调用的开销，而且程序也更简洁了，运行时间为 <code>4ms</code>，速度提升了四倍，时间复杂度和空间复杂度都为 $O(S T)$。</p><h3 id="思路三优化"><a href="#思路三优化" class="headerlink" title="思路三优化"></a>思路三优化</h3><p>思路三中空间复杂度还可以进一步优化，可以看出来，递推时我们只用到了 <code>s</code> 的上一个字符时的状态，我们可以压缩空间，只存储关于 <code>t</code> 的状态。这里采用的是类似于 <code>01 背包问题</code> 的一维数组优化技巧。需要注意的是，因为我们需要上一状态的的前一个字符的信息，所以在循环的时候必须倒着循环，否则就破坏了状态。空间优化过的代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">numDistinct</span><span class="params">(string S, string T)</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> dp[T.<span class="built_in">size</span>() + <span class="number">1</span>] = &#123;<span class="number">1</span>&#125;;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= S.<span class="built_in">size</span>(); ++i)</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> j = T.<span class="built_in">size</span>(); j &gt; <span class="number">0</span>; --j)</span><br><span class="line">            dp[j] += T[j - <span class="number">1</span>] == S[i - <span class="number">1</span>] ? dp[j - <span class="number">1</span>] : <span class="number">0</span>;  </span><br><span class="line">    <span class="keyword">return</span> dp[T.<span class="built_in">size</span>()];  </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然运行时间没有变化，但是空间复杂度降到了 $O(T)$，程序也更加简洁了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>LeetCode | 174 地下城游戏</title>
      <link>https://blog.howardlau.me/programming/leetcode/leetcode-174-dungeon.html</link>
      <description>
        <![CDATA[<p>题目地址：<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/programming/leetcode/">LeetCode</category>
      <pubDate>Mon, 23 Apr 2018 01:27:32 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>题目地址：<a href="https://leetcode-cn.com/problems/dungeon-game/description/">https://leetcode-cn.com/problems/dungeon-game/description/</a>初看这题的时候，觉得是动态规划，自然而然地想到从左上角开始递推，把起点固定在 (0, 0)，用 <code>min_hp[r][c]</code> 表示终点在 r + 1 行 c + 1 列时，起点所需最小的 HP，但是问题在于，在这种情况下如果我知道 <code>min_hp[r - 1][j]</code> 和 <code>min_hp[i][j - 1]</code>，其实是很难得到 <code>min_hp[i][j]</code> 的。举个例子：</p><div style="max-width: 60%; text-align:center;"><table class="dungeon"><tbody><tr><td>1 (K)</td><td>-3</td><td>3</td></tr><tr><td>0</td><td>-2</td><td>0</td></tr><tr><td>-3</td><td>-3</td><td>-3 (P)</td></tr></tbody></table></div><p>当走到第 3 行第 3 列的时候，<code>min_hp[2][1]</code> 和 <code>min_hp[1][2]</code> 都为 2，由于当前格子为 -3，所以求得答案为 5，但正确答案是 3。尝试了加入 <code>hp</code> 数组保存当前 HP，不仅使程序更复杂，而且不能使程序变得正确。</p><p>这时候只能转换思路，固定终点，使 <code>min_hp[r][c]</code> 表示的是起点为第 r + 1 行第 c + 1 列时的最小初始 HP，问题就很好解决了，得到递推式：<code>min_hp[r][c] = max(min(min_hp[r + 1][c], min_hp[r][c + 1])  - dungeon[r][c], 1)</code></p><p>加上一些初始化处理之后，就得到了正确的程序：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">calculateMinimumHP</span><span class="params">(vector&lt;vector&lt;<span class="type">int</span>&gt;&gt;&amp; dungeon)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> rows = dungeon.<span class="built_in">size</span>();</span><br><span class="line">        <span class="type">int</span> cols = dungeon[<span class="number">0</span>].<span class="built_in">size</span>();</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> min_hp[rows][cols];</span><br><span class="line">        min_hp[rows - <span class="number">1</span>][cols - <span class="number">1</span>] = dungeon[rows - <span class="number">1</span>][cols - <span class="number">1</span>] &lt; <span class="number">0</span> ? -dungeon[rows - <span class="number">1</span>][cols - <span class="number">1</span>] + <span class="number">1</span> : <span class="number">1</span>;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> r = rows - <span class="number">2</span>; r &gt;= <span class="number">0</span>; --r) </span><br><span class="line">            min_hp[r][cols - <span class="number">1</span>] = <span class="built_in">max</span>(min_hp[r + <span class="number">1</span>][cols - <span class="number">1</span>] - dungeon[r][cols - <span class="number">1</span>], <span class="number">1</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> c = cols - <span class="number">2</span>; c &gt;= <span class="number">0</span>; --c) </span><br><span class="line">            min_hp[rows - <span class="number">1</span>][c] = <span class="built_in">max</span>(min_hp[rows - <span class="number">1</span>][c + <span class="number">1</span>] - dungeon[rows - <span class="number">1</span>][c], <span class="number">1</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> r = rows - <span class="number">2</span>; r &gt;= <span class="number">0</span>; --r)</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> c = cols - <span class="number">2</span>; c &gt;= <span class="number">0</span>; --c)</span><br><span class="line">                min_hp[r][c] = <span class="built_in">max</span>(<span class="built_in">min</span>(min_hp[r + <span class="number">1</span>][c], min_hp[r][c + <span class="number">1</span>])  - dungeon[r][c], <span class="number">1</span>);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> min_hp[<span class="number">0</span>][<span class="number">0</span>];</span><br><span class="line">        </span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这道题对于我这个在动态规划方面还不是很熟悉的新手提供了一个新的视角：动态规划的问题并不一定就是 <code>dp[m][n]</code>，有时候这样得不到递推式，这时候不妨转换方向，变换一个顺序来进行递推。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>数据结构之二叉搜索树</title>
      <link>https://blog.howardlau.me/programming/binary-search-tree.html</link>
      <description>
        <![CDATA[<h2 id="二叉搜索树"><a href="#二叉搜索树" class="headerlink" title="二叉搜索树"></a>二叉搜索树</h2><p>二叉搜索树（Binary Search]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 17 Apr 2018 05:32:52 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="二叉搜索树"><a href="#二叉搜索树" class="headerlink" title="二叉搜索树"></a>二叉搜索树</h2><p>二叉搜索树（Binary Search Tree）是一种简单经典的数据结构，其特点有三：</p><ol><li>一个结点的左子树的值都比该结点的值要小</li><li>一个结点的右子树的值都比该节点的值要大</li><li>没有两个结点拥有相同的值</li></ol><p>得益于二叉树的特性，支持在 $O(\lg n)$ 的时间复杂度内进行查询操作。本文将介绍一个二叉搜索树的建立、查询以及删除节点操作。</p><h3 id="结点的表示"><a href="#结点的表示" class="headerlink" title="结点的表示"></a>结点的表示</h3><p>二叉搜索树也是二叉树，结点的表示和普通的二叉树没有什么区别。最简单的情况下，有一个存放数据的数据域，以及指向左右子树的指针。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">TreeNode</span> &#123;</span><br><span class="line">    T data;</span><br><span class="line">    TreeNode&lt;T&gt; *pLeft, *pRight;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="二叉搜索树类"><a href="#二叉搜索树类" class="headerlink" title="二叉搜索树类"></a>二叉搜索树类</h3><p>为了代码的条理性，首先定义一个 <code>BST</code> 类，存放根节点指针，并提供一系列操作树的方法。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BST</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">BST</span>();</span><br><span class="line">    <span class="comment">// 用于查找结点</span></span><br><span class="line">    <span class="function">TreeNode&lt;T&gt; *<span class="title">find</span><span class="params">(T key)</span></span>;</span><br><span class="line">    <span class="comment">// 用于插入结点</span></span><br><span class="line">    <span class="function">TreeNode&lt;T&gt; *<span class="title">insert</span><span class="params">(T data)</span></span>;</span><br><span class="line">    <span class="comment">// 用于删除结点</span></span><br><span class="line">    <span class="function">TreeNode&lt;T&gt; *<span class="title">remove</span><span class="params">(T key)</span></span>;</span><br><span class="line">    <span class="comment">// 中序遍历二叉搜索树</span></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">mid_traverse</span><span class="params">(<span class="type">void</span> (*f)(TreeNode&lt;T&gt; *node))</span></span>;</span><br><span class="line">    ~<span class="built_in">BST</span>();</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">    <span class="comment">// 由于二叉树具有分形结构，对其进行遍历操作时用递归函数比较方便，特建立以下的辅助函数</span></span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">void</span> <span class="title">destroy_tree</span><span class="params">(TreeNode&lt;T&gt; *root)</span></span>;</span><br><span class="line">    <span class="function"><span class="type">static</span> <span class="type">void</span> <span class="title">do_mid_traverse</span><span class="params">(TreeNode&lt;T&gt; *root, <span class="type">void</span> (*f)(TreeNode&lt;T&gt; *node))</span></span>;</span><br><span class="line">    <span class="comment">// 存放树的根结点</span></span><br><span class="line">    TreeNode&lt;T&gt; *root;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="二叉搜索树的结点插入"><a href="#二叉搜索树的结点插入" class="headerlink" title="二叉搜索树的结点插入"></a>二叉搜索树的结点插入</h2><p>如果一个二叉搜索树的根结点为空，那么直接让该结点成为根结点即可；否则，递归执行以下过程：比较要插入的值与当前树的根的值的大小，如果更小，则让根的左结点成为新的根；如果更大，则让根的右结点成为新的根，直到根为空。</p><p>可以看出，以上过程没有区分一个当前的根处于原来的二叉搜索树中的什么位置，也不用管一个结点的左右指针是否为空，只要不停迭代，直到根为空，就将新的值插入即可。</p><p>由于我们简化了判断过程，直接修改结点的左右指针，所以我们需要用到二重指针来完成插入操作，具体见代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">TreeNode&lt;T&gt; *BST&lt;T&gt;::<span class="built_in">insert</span>(T data) &#123;</span><br><span class="line">    TreeNode&lt;T&gt; *pNode = <span class="keyword">new</span> TreeNode&lt;T&gt;&#123;data, <span class="literal">nullptr</span>, <span class="literal">nullptr</span>&#125;;</span><br><span class="line">    TreeNode&lt;T&gt; **curRoot = &amp;root;</span><br><span class="line">    <span class="keyword">while</span> (*curRoot) &#123;</span><br><span class="line">        <span class="keyword">if</span> (data == (*curRoot)-&gt;data) <span class="keyword">return</span> *curRoot;</span><br><span class="line">        <span class="keyword">if</span> (data &lt; (*curRoot)-&gt;data) &#123;</span><br><span class="line">            curRoot = &amp;((*curRoot)-&gt;pLeft);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            curRoot = &amp;((*curRoot)-&gt;pRight);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    *curRoot = pNode;</span><br><span class="line">    <span class="keyword">return</span> pNode;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="二叉搜索树的结点查找"><a href="#二叉搜索树的结点查找" class="headerlink" title="二叉搜索树的结点查找"></a>二叉搜索树的结点查找</h2><p>二叉搜索树的查找比插入还简单一些，同样是递归操作，按照二叉搜索树的特性，如果当前根结点的值恰好就是要找的值，那么直接返回即可；如果要找的值比根结点要小，就让左结点变为新的根，否则就让右结点变为新的根。</p><p>这里并不涉及到左右指针的修改问题，所以直接用一重指针即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">TreeNode&lt;T&gt; *BST&lt;T&gt;::<span class="built_in">find</span>(T data) &#123;</span><br><span class="line">    TreeNode&lt;T&gt; *curRoot = root;</span><br><span class="line">    <span class="keyword">while</span> (curRoot) &#123;</span><br><span class="line">        <span class="keyword">if</span> (curRoot-&gt;data == data)</span><br><span class="line">            <span class="keyword">return</span> curRoot;</span><br><span class="line">        <span class="keyword">if</span> (data &lt; curRoot-&gt;data) &#123;</span><br><span class="line">            curRoot = curRoot-&gt;pLeft;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            curRoot = curRoot-&gt;pRight;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="二叉搜索树的结点删除"><a href="#二叉搜索树的结点删除" class="headerlink" title="二叉搜索树的结点删除"></a>二叉搜索树的结点删除</h2><p>相比于插入和查找，二叉搜索树的删除显得复杂得多，我们应当如何在删除结点之后仍然保持二叉搜索树的性质不变呢？</p><p>其实删除也并不复杂，主要分为三种情况：</p><ol><li>要删除的结点只有右子树而无左子树</li><li>要删除的结点只有左子树而无右子树</li><li>要删除的结点既有左子树也有右子树</li></ol><p>首先看比较简单的前两种情况。</p><p>假设我们有这样一棵简单的二叉搜索树：<a href="/programming/binary-search-tree/tree2.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree2.png"></a></p><ol><li>如果我们要删除的结点是 <code>6</code> 也就是根结点，那么我们直接将二叉搜索树的根结点指向右结点即可。<a href="/programming/binary-search-tree/tree4.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree4.png"></a></li><li>假设我们要删除的结点是 <code>7</code>，那么只需将 <code>7</code> 的父节点指向 <code>7</code> 的指针指向 <code>7</code> 的右结点即可。<a href="/programming/binary-search-tree/tree5.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree5.png"></a></li><li>如果要删除的结点是 <code>9</code> 或者 <code>10</code>，那么情况就相对简单，直接删除这个连接即可。<a href="/programming/binary-search-tree/tree6.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree6.png"></a></li></ol><p>而对于只有左子树的结点而言，只是左右对调了而已，跟上面所说的没有什么不同，因此不再赘述了。</p><p>接下来看最复杂的第三种情况：</p><p><a href="/programming/binary-search-tree/tree.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree.png"></a></p><p>假如二叉树变成现在这样复杂的一棵，如果我们要删除结点 <code>6</code>，那么我们需要在左右子树中找到一个结点来维持二叉搜索树的性质。</p><p>容易想到，我们只需要用左子树中最大的结点，或者是右子树中最小的结点来替换我们的根结点即可。问题到这里就迎刃而解了，左子树中最大的结点，就是左子树中最右边的结点；同理，右子树中最小的结点，就是右子树中最左边的结点。我们只需要跟随着左右子树的右左指针不停向下遍历，直到遇到空指针即可。</p><p>而所谓的“替换”其实是将两个结点的值对调了，然后删除原来的结点而已。而很显然，要删除的这个结点只有一边有子树，或者是叶子结点，这就回归到了上面两种简单的情况上，是不是觉得其实这种复杂的情况很简单呢？</p><p>具体代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">class</span> <span class="title class_">T</span>&gt;</span><br><span class="line">TreeNode&lt;T&gt; *BST&lt;T&gt;::<span class="built_in">remove</span>(T data) &#123;</span><br><span class="line">    TreeNode&lt;T&gt; *parent, *node;</span><br><span class="line">    parent = <span class="literal">nullptr</span>;</span><br><span class="line">    node = root;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> (node) &#123;</span><br><span class="line">        <span class="keyword">if</span> (node-&gt;data == data) </span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        parent = node;</span><br><span class="line">        <span class="keyword">if</span> (data &lt; node-&gt;data)</span><br><span class="line">            node = node-&gt;pLeft;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            node = node-&gt;pRight;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (node == <span class="literal">nullptr</span>) <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 1. 为叶子结点</span></span><br><span class="line">    <span class="keyword">if</span> (node-&gt;pLeft == <span class="literal">nullptr</span> &amp;&amp; node-&gt;pRight == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pLeft == node) parent-&gt;pLeft = <span class="literal">nullptr</span>;</span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pRight == node) parent-&gt;pRight = <span class="literal">nullptr</span>;</span><br><span class="line">        <span class="keyword">return</span> node;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. 只有左子树而无右子树</span></span><br><span class="line">    <span class="keyword">if</span> (node-&gt;pLeft &amp;&amp; node-&gt;pRight == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (parent == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">            root = node-&gt;pLeft;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pLeft == node) &#123;</span><br><span class="line">            parent-&gt;pLeft = node-&gt;pLeft;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pRight == node) &#123;</span><br><span class="line">            parent-&gt;pRight = node-&gt;pLeft;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 3. 只有右子树而无左子树，与上面的操作是对称的</span></span><br><span class="line">    <span class="keyword">if</span> (node-&gt;pRight &amp;&amp; node-&gt;pLeft == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (parent == <span class="literal">nullptr</span>) &#123;</span><br><span class="line">            root = node-&gt;pRight;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pRight == node) &#123;</span><br><span class="line">            parent-&gt;pRight = node-&gt;pRight;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (parent-&gt;pLeft == node) &#123;</span><br><span class="line">            parent-&gt;pLeft = node-&gt;pRight;</span><br><span class="line">            <span class="keyword">return</span> node;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 左右子树都有</span></span><br><span class="line">    <span class="keyword">if</span> (node-&gt;pLeft &amp;&amp; node-&gt;pRight) &#123;</span><br><span class="line">        TreeNode&lt;T&gt; *target;</span><br><span class="line">        <span class="comment">// 这里我们可以随便选择左右子树</span></span><br><span class="line">        target = node-&gt;pLeft;</span><br><span class="line">        <span class="keyword">while</span> (target-&gt;pRight) target = target-&gt;pRight;        </span><br><span class="line">        <span class="keyword">this</span>-&gt;<span class="built_in">remove</span>(target-&gt;data);</span><br><span class="line">        node-&gt;data = target-&gt;data;</span><br><span class="line">        <span class="keyword">return</span> target;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然删除的代码长了点，但是其实只是情况复杂了一点，并不困难，仔细分类就可以明白了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>二叉搜索树是一种基础的数据结构，其三种操作的实现都并不太复杂，平均情况下可以提供 $O(\lg n)$ 的查找时间复杂度。</p><p><a href="/programming/binary-search-tree/tree_bad.png" data-fancybox="gallery" data-caption=""><img src="/programming/binary-search-tree/tree_bad.png"></a></p><p>不过，如果遇到了这种情况，就是最坏的情况，这时候的查找复杂度退化为 $O(n)$。</p><p>如果我们能使二叉搜索树尽量保持平衡，那么就可以使得最坏情况下也有平均复杂度，这时候我们需要实现的数据结构便是<strong>平衡二叉搜索树</strong>，然而其实现比二叉搜索树更为复杂，详情另一篇博文更新再详细陈述。</p><p>更新：平衡二叉搜索树：<a href="https://howardlau.me/programming/balanced-binary-search-tree-avl-tree.html">AVL树</a>，<a href="https://howardlau.me/programming/balanced-binary-search-tree-red-black-tree.html">红黑树</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>解决双系统启动 Ubuntu 进入 initramfs 问题</title>
      <link>https://blog.howardlau.me/it-chat/ubuntu-dual-boot-initramfs.html</link>
      <description>
        <![CDATA[<p>在我的电脑上，安装了 Kunbutu 17.04 和 Windows 10 两个系统。启动时通过 <code>grub</code> 来选择进入 Kubuntu 还是 Windows Boot Manager。之前使用一直正常，直到最近重装了一次 Kubuntu]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/it-chat/">IT 杂谈</category>
      <pubDate>Fri, 13 Apr 2018 06:26:49 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在我的电脑上，安装了 Kunbutu 17.04 和 Windows 10 两个系统。启动时通过 <code>grub</code> 来选择进入 Kubuntu 还是 Windows Boot Manager。之前使用一直正常，直到最近重装了一次 Kubuntu 之后发现，在进入 Windows 并重新启动之后，再进入 Kubuntu 的时候一定会进入 <code>initramfs</code> 命令提示符不能正常启动。一种解决办法是通过命令 <code>e2fsck -y -f /dev/nvme0n1p5</code> （<code>nvme0n1p5</code> 是我的 Kubuntu 系统所在分区）来修复磁盘错误，然后重启即可正常进入 Kubuntu。但是每次都这样输入命令修复错误太麻烦了，而且问题一定出在 Windows 上。经过一番搜索，发现是 <code>ext2fsd</code> 这个程序还不能很好的支持 <code>ext4</code> 分区格式，将其卸载之后问题永久解决，没有再出现过进入 <code>initramfs</code> 的情况。</p><p>但是之前我安装 Kubuntu 16.04 就没有出现这种情况，这是为什么呢？经过一番搜索，发现 <code>ext2fsprogs</code> 从 1.43 版本开始（Ubuntu 16.10），在新建 <code>ext4</code> 分区的时候，会默认启用 64 位和 <code>metadata_csum</code> 功能，而 <code>ext2fsd</code> 目前的版本（0.69）还不能完美支持这两个特性，所以在使用 Kubuntu 17.04 安装之后，每次启动 Windows 都会对 <code>ext4</code> 分区造成破坏，也就导致了分区不能正常被挂载，从而启动失败，进入到 <code>initramfs</code>。</p><p>因此，除了卸载 <code>ext2fsd</code> 还有另外一种办法，那就是使用 Live CD 启动，然后执行下面的命令禁用掉那两个特性即可：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> tune2fs -O ^metadata_csum,^64bit /dev/[Ubuntu 所在的分区]</span><br></pre></td></tr></table></figure><p>参考链接：<a href="https://askubuntu.com/questions/849872/how-can-i-prevent-windows-10-from-corrupting-the-ext4-superblock-every-time">https://askubuntu.com/questions/849872/how-can-i-prevent-windows-10-from-corrupting-the-ext4-superblock-every-time</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Visual Studio 2017 新建 C++ 单元测试</title>
      <link>https://blog.howardlau.me/programming/visual-studio-2017-cpp-unit-test.html</link>
      <description>
        <![CDATA[<p>在 Visual Studio 2017 中，如果想要为一个 C++ 项目新建单元测试，首先在解决方案上右键，选择“添加”-&gt;“新建项目”，出现如图的对话框</p>
<p><a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 09 Apr 2018 19:01:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在 Visual Studio 2017 中，如果想要为一个 C++ 项目新建单元测试，首先在解决方案上右键，选择“添加”-&gt;“新建项目”，出现如图的对话框</p><p><a href="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-1.png" data-fancybox="gallery" data-caption=""><img src="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-1.png"></a></p><p>选择 Visual C++ 的测试后，选择“本机单元测试”，点击确定生成测试项目。</p><p><a href="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-2.png" data-fancybox="gallery" data-caption=""><img src="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-2.png"></a></p><p>右键单元测试项目中的“引用”，选择添加引用，弹出对话框：</p><p><a href="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-3.png" data-fancybox="gallery" data-caption=""><img src="/programming/visual-studio-2017-cpp-unit-test/2018-04-10-3.png"></a></p><p>勾上需要测试的项目后，点击确定。</p><p>之后打开 unittest1.cpp 编辑测试代码，在访问需要的类的时候，需要 <code>#include</code> 相关文件，需要注意相对路径。但是在本机上，**包含 <code>.h</code> 头文件的话生成解决方案的时候会出现 <code>LNK2019</code> 错误。一个解决办法是 <code>#include</code> cpp 文件。**参考代码如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;stdafx.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;CppUnitTest.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;../PhyCal/Rational.cpp&quot;</span> <span class="comment">// 包含 .cpp 文件而不是 .h 文件</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> Microsoft::VisualStudio::CppUnitTestFramework;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> UnitTest1</span><br><span class="line">&#123;</span><br><span class="line"><span class="built_in">TEST_CLASS</span>(UnitTest1)</span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"></span><br><span class="line"><span class="built_in">TEST_METHOD</span>(TestMethod1)</span><br><span class="line">&#123;</span><br><span class="line"><span class="function">Rational <span class="title">n</span><span class="params">(<span class="number">2</span>, <span class="number">2</span>)</span></span>;</span><br><span class="line">Assert::<span class="built_in">AreEqual</span>(n.<span class="built_in">getNumerator</span>(), <span class="number">1l</span>);</span><br><span class="line">Assert::<span class="built_in">AreEqual</span>(n.<span class="built_in">getDenominator</span>(), <span class="number">1l</span>);</span><br><span class="line"></span><br><span class="line"><span class="function">Rational <span class="title">p</span><span class="params">(<span class="number">6</span>, <span class="number">2</span>)</span></span>;</span><br><span class="line">Assert::<span class="built_in">AreEqual</span>(p.<span class="built_in">getNumerator</span>(), <span class="number">3l</span>);</span><br><span class="line">Assert::<span class="built_in">AreEqual</span>(p.<span class="built_in">getDenominator</span>(), <span class="number">1l</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后选择“测试”菜单中的“运行”-&gt;“所有测试”，即可看到测试结果。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>机器学习笔记：对率几率回归</title>
      <link>https://blog.howardlau.me/machine-learning/ml-logistic-regression-implementation.html</link>
      <description>
        <![CDATA[<p>对率几率回归（Logistic Regression）是机器学习中一种基本的分类算法，可以用来完成对输入数据的二分类任务。本文记录了使用 <code>numpy</code> 从基础的公式出发，逐步实现 Logistic Regression 的过程。</p>
<h2]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/machine-learning/">机器学习</category>
      <pubDate>Sun, 18 Mar 2018 07:33:01 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>对率几率回归（Logistic Regression）是机器学习中一种基本的分类算法，可以用来完成对输入数据的二分类任务。本文记录了使用 <code>numpy</code> 从基础的公式出发，逐步实现 Logistic Regression 的过程。</p><h2 id="对数概率几率函数"><a href="#对数概率几率函数" class="headerlink" title="对数概率几率函数"></a>对数概率几率函数</h2><p>我们很容易想到二分类方法可以用阶跃函数（Step Function）来分类，但是阶跃函数不连续、不可导，无法训练，因此我们可以用 S 型函数（Sigmoid Function）来进行预测。</p><p>$$\sigma (x) &#x3D; \frac{1}{1 + e^{-x}}$$</p><p><a href="/machine-learning/ml-logistic-regression-implementation/logistic-curve.svg" data-fancybox="gallery" data-caption=""><img src="/machine-learning/ml-logistic-regression-implementation/logistic-curve.svg"></a>Sigmoid 函数及其图像</p><p>该函数实际上是一种概率分布函数，它表示的是随机事件发生的概率 $P(X\leq x)$。该函数有着单调递增，连续以及值域在 $[0, 1]$ 等良好特性，常常被用于机器学习的任务中。</p><p>而它有一个有趣的性质，它的导数也可以用它本身来表达，即</p><p>$$\sigma’(x) &#x3D; \sigma(x)(1-\sigma(x)) \tag{*}$$</p><p>推导过程如下：</p><p>$$\frac{\mathrm{d}}{\mathrm{d}x}\left( \frac{1}{\sigma(x)}\right) &#x3D; -\frac{\sigma’(x)}{\sigma ^2 (x)} \tag{1}$$</p><p>$$\frac{\mathrm{d}}{\mathrm{d}x}\left( \frac{1}{\sigma(x)}\right) &#x3D; -e^{-x} &#x3D; -\frac{1}{\sigma(x)}+1 \tag{2}$$</p><p>两式联立就得到了 $(*)$ 式。</p><p>假设数据的两个分类服从伯努利分布，则有 $P(样本是正例)&#x3D;\phi$，$P(样本是负例)&#x3D;1 - \phi$。我们的目标就是要找到一个映射函数 $f:R^{n+1}\rightarrow R$，将样本的特征映射到一个实数上，然后再根据函数值进行分类。</p><p>假设样本向量为 $\vec x$，可以得到分类公式：</p><p>$$P(Y &#x3D; 1|{\bf x}) &#x3D; p &#x3D; \frac{1}{1 + e^{-\bf W  x}}$$</p><p>$$P(Y &#x3D; 0|{\bf x}) &#x3D; 1 - p &#x3D; \frac{e^{-\bf Wx}}{1 + e^{-\bf W x}}$$</p><p>两式相比并取对数，得到：</p><p>$$\ln\frac{p}{1-p} &#x3D; \bf W  x$$</p><p>式子的意义是样本是正例的概率与样本是反例的概率的比值的对数等于 $\bf W  x$，这就是对数概率回归的名字由来。对率回归就是要找到合适的参数 $\bf W$，也就是找到样本空间伯努利分布中的 $\phi$。</p><h2 id="参数学习"><a href="#参数学习" class="headerlink" title="参数学习"></a>参数学习</h2><p>对于一些给定的样本，我们可以采用极大似然估计法来估计最佳的模型参数。假设我们有 $m$ 个样本，输入特征有 $n$ 个，那么设 $P(Y &#x3D; 1|\vec x)&#x3D;\phi(\vec x)$，$P(Y &#x3D; 0|\vec x)&#x3D;1-\phi(\vec x)$，得到似然估计函数为：</p><p>$$\prod_{i&#x3D;1}^{m}[\phi(\vec {\bf x_i})]^{y_i}[1-\phi(\vec {\bf x_i})]^{1-y_i}$$</p><p>将其取对数得到：</p><p>$$L({\bf W}) &#x3D; \sum_{i&#x3D;1}^{m}{y_i}\ln\phi( {\bf x_i}) + (1-y_i)\ln(1-\phi( {\bf x_i}))$$</p><p>最大化$L({\bf W})$，即最小化$J({\bf W}) &#x3D; -L({\bf W})$。</p><p>通常使用梯度下降法和拟牛顿法来求最佳参数，本文两种方法都实现了。</p><h2 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h2><p>以下代码规定 ${\bf W}$ 为 $n+1$ 维行向量， ${\bf X}$ 为 $(n + 1) \times m$ 维的输入样本矩阵，${\bf y}$ 为 $m$ 维行向量，为输入数据对应的类别。</p><h3 id="sigmoid-函数"><a href="#sigmoid-函数" class="headerlink" title="sigmoid 函数"></a>sigmoid 函数</h3><p>按照定义，我们可以定义 sigmoid 函数如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">sigmoid</span>(<span class="params">X</span>):</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span> / (<span class="number">1</span> + np.exp(-X))</span><br></pre></td></tr></table></figure><p>但是在实际应用中，指数函数很有可能发生溢出问题，所以使用下面这个函数来代替计算 sigmoid 函数。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">sigmoid</span>(<span class="params">X</span>):</span><br><span class="line">    <span class="comment"># More stable</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">.5</span> * (<span class="number">1</span> + np.tanh(<span class="number">.5</span> * X))</span><br></pre></td></tr></table></figure><h3 id="Loss-函数"><a href="#Loss-函数" class="headerlink" title="Loss 函数"></a>Loss 函数</h3><p>上文中的 $J({\bf W})$ 可以理解为分错类的分数，最小化这个函数就相当于使得模型尽可能少错分类。同样按照公式直接写出 <code>loss</code> 函数，对$J({\bf W})$除以$m$可以使得梯度较小。不同的是这里加入了正则化项，以避免过拟合的问题。同时，为了避免对数函数的输入为 0， 对输入要加上一个小的正数 $\epsilon&#x3D;10^{-8}$。</p><p>为了效率起见，使用了向量化技巧。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">loss</span>(<span class="params">X, y, parameters, lambd</span>):</span><br><span class="line">        m = X.shape[<span class="number">1</span>]</span><br><span class="line">        g = sigmoid(np.dot(parameters, X))</span><br><span class="line">        <span class="comment"># Aviod divided by zeros</span></span><br><span class="line">        epsilon = <span class="number">1e-8</span></span><br><span class="line">        J = -(np.dot(np.log(g + epsilon), y.T) + np.dot(np.log(<span class="number">1</span> - g + epsilon), (<span class="number">1</span> - y).T)) / m</span><br><span class="line">        J += lambd / (<span class="number">2</span> * m) * np.linalg.norm(parameters[<span class="number">0</span>, <span class="number">1</span>:]) ** <span class="number">2</span></span><br><span class="line">        <span class="keyword">return</span> J</span><br></pre></td></tr></table></figure><h3 id="梯度函数"><a href="#梯度函数" class="headerlink" title="梯度函数"></a>梯度函数</h3><p>对 $J({\bf W})$ 求导，直接将数据代入公式即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">gradients</span>(<span class="params">X, y, parameters, lambd</span>):</span><br><span class="line">        m = X.shape[<span class="number">1</span>]</span><br><span class="line">        gradients = np.dot((sigmoid(np.dot(parameters, X)) - y), X.T) / m</span><br><span class="line">        gradients += np.hstack((<span class="number">0</span>, lambd / m * gradients[<span class="number">0</span>, <span class="number">1</span>:]))</span><br><span class="line">        <span class="keyword">return</span> gradients</span><br></pre></td></tr></table></figure><h3 id="梯度下降法"><a href="#梯度下降法" class="headerlink" title="梯度下降法"></a>梯度下降法</h3><p>更新公式为：${\bf W} :&#x3D; {\bf W} - \alpha \nabla J({\bf W})$，其中 $\alpha$ 为学习率。学习率的选取至关重要，会影响模型的训练效果。过大可能导致 Overshooting，错过最优解并朝着反方向前进，过小会导致学习过慢。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">GradientDescentOptimizer</span>(<span class="title class_ inherited__">Optimizer</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, X, y, parameters, loss_function, gradients_function, learning_rate = <span class="number">0.001</span>, lambd = <span class="number">0.01</span></span>):</span><br><span class="line">        Optimizer.__init__(<span class="variable language_">self</span>, X, y, parameters, loss_function, gradients_function)</span><br><span class="line">        <span class="variable language_">self</span>.learning_rate = learning_rate</span><br><span class="line">        <span class="variable language_">self</span>.lambd = lambd</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">        gradients = <span class="variable language_">self</span>.gradients(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        <span class="variable language_">self</span>.parameters -= <span class="variable language_">self</span>.learning_rate * gradients</span><br><span class="line">        loss = <span class="variable language_">self</span>.loss(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)[<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">        <span class="keyword">return</span> loss</span><br></pre></td></tr></table></figure><p>在实验中，发现对于给定的测试数据，学习率的选取必须非常小，否则极其容易训练失败，而学习率的选取并不容易，为了避免手动选取学习率的麻烦，我们可以采用牛顿法。</p><h3 id="牛顿法"><a href="#牛顿法" class="headerlink" title="牛顿法"></a>牛顿法</h3><p>要找到函数的极小值点，我们可以通过解方程 $\nabla J({\bf W})&#x3D;0$ 来得到估计的参数值。一般情况下这个方程难以直接求解，但是可以通过牛顿法迭代来找到方程的解。牛顿法的一维情形是：</p><p>$$x_{n+1} &#x3D; x_{n} - \frac{f(x_n)}{f’(x_n)}$$</p><p>从泰勒展开的理解便是函数在 $x_n$ 处的泰勒式等于零来近似求解方程：</p><p>$$f(x) &#x3D; f(x_n) + f’(x_n)(x - x_n) + o(x_n)$$</p><p>若果要求 $f’(x)&#x3D;0$ 的解，直接替换上式中的 $f(x)$，$f’(x)$ 为 $f’(x), f’’(x)$ &#96;即可。对于高维情形则是：</p><p>$$x_{n+1} &#x3D; x_{n} - [{{\bf H}f(x_n)}]^{-1}{\nabla f(x_n)}$$</p><p>其中 $[{{\bf H}f(x_n)}]^{-1}$ 为黑塞矩阵的逆矩阵，黑塞矩阵是函数的二阶偏导数排列而成的矩阵。</p><h4 id="阻尼牛顿法"><a href="#阻尼牛顿法" class="headerlink" title="阻尼牛顿法"></a>阻尼牛顿法</h4><p>牛顿法有一个缺点：某些时候更新后的参数并不能稳定地收敛到零点，严重的情况下会导致点列 $\{x_n\}$ 发散而迭代失败。为了改进这一缺点，在对参数更新之前，先进行一维搜索，找到使函数下降最快的一个方向，然后再对参数进行更新。这种方法也叫 Line Search，即找到一个参数 $\alpha^* &#x3D; \arg \min_{\alpha \in \bf{R}} f(x_n - \alpha[{{\bf H}f(x_n)}]^{-1}{\nabla f(x_n)})$，然后再根据找到的方向进行参数更新。 Line Search 方法比较冗长，具体请参考代码。在此不再贴出。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">NewtonMethodOptimizer</span>():</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, X, y, parameters, loss_function, gradients_function, second_derivatives_function, lambd=<span class="number">0.01</span>, epsilon=<span class="number">1e-6</span></span>):</span><br><span class="line">        Optimizer.__init__(<span class="variable language_">self</span>, X, y, parameters, loss_function, gradients_function)</span><br><span class="line">        <span class="variable language_">self</span>.second_derivatives = second_derivatives_function</span><br><span class="line">        <span class="variable language_">self</span>.lambd = lambd</span><br><span class="line">        <span class="variable language_">self</span>.epsilon = epsilon</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">        n, m = <span class="variable language_">self</span>.X.shape</span><br><span class="line">        gradients = <span class="variable language_">self</span>.gradients(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        H = <span class="variable language_">self</span>.second_derivatives(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        d_k = -np.dot(gradients, np.linalg.pinv(H))</span><br><span class="line">        f = <span class="keyword">lambda</span> alpha : <span class="variable language_">self</span>.loss(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters + alpha * d_k, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        alpha = line_search(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, f)</span><br><span class="line">        <span class="variable language_">self</span>.parameters += alpha * d_k</span><br><span class="line">        loss = <span class="variable language_">self</span>.loss(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)[<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">        <span class="keyword">return</span> loss</span><br></pre></td></tr></table></figure><h3 id="拟牛顿法"><a href="#拟牛顿法" class="headerlink" title="拟牛顿法"></a>拟牛顿法</h3><p>由于牛顿法需要用到函数的二阶导数，计算较为不便，而且需要求解黑塞矩阵的逆，时间复杂度高，为此人们提出了通过迭代来近似黑塞矩阵的拟牛顿法，其中较为知名的有 DFP 算法和 BFGS 算法，本文实现了 BFGS 算法。</p><p>BFGS 的原理如下，由泰勒公式我们有</p><p>$$\nabla J({\bf W}) &#x3D; {\nabla J({\bf W_{k+1}}) + {\bf H}J({\bf W_{k+1}})({\bf W} - {\bf W_{k+1}}) }$$</p><p>代入${\bf W} &#x3D; {\bf W_{k}}$，并记 $[{\bf H}J({\bf W_{k+1}})]^{-1} &#x3D; {\bf D_{k+1}}$，${\bf W_{k+1}} - {\bf W_{k}}&#x3D;{\bf s_k}$，$\nabla J({\bf W_{k+1}})-{\nabla J({\bf W_{k}})} &#x3D; {\bf y_k}$, 得到：</p><p>$${\bf s_k} &#x3D; {\bf D_{k+1}}{\bf y_k}$$</p><p>根据牛顿法迭代公式，并应用 Line Search 方法，又有 ${\bf s_k &#x3D; -\alpha^*D_kg_k}$</p><p>记黑塞矩阵的近似矩阵为${\bf B_k}$，则有</p><p>$${{\bf B_{k+1}}&#x3D;{\bf B_k} + \alpha {\bf uu}^{\mathsf{T}}+\beta {\bf vv^{\mathsf{T}}}}$$</p><p>$${\bf B_{k+1} s_k&#x3D;y_k}&#x3D;{\bf B_k s_k} + \alpha {\bf uu}^{\mathsf{T}}{\bf s_k}+\beta {\bf vv^{\mathsf{T}}}{\bf s_k}$$</p><p>通过令${\bf u &#x3D; y_k}$，${\bf v&#x3D;B_k s_k}$，可以得到：</p><p>$${{\bf B_{k+1}}&#x3D;{\bf B_k} + \frac{\bf y_ky_k^{\mathsf{T}}}{\bf y_k^{\mathsf{T}}s_k} - \frac{\bf B_ks_ks_k^{\mathsf{T}}B_k}{\bf s_k^{\mathsf{T}}B_ks_k}}$$</p><p>对其应用 Sherman-Morrison 公式求逆得到（这里我不太会推导，是参考书上的）：</p><p>$${\bf D_{k+1}&#x3D;\left(\bf I - \frac{s_ky_k^{\mathsf{T}}}{y_k^{\mathsf{T}}s_k} \right)D_k\left(I-\frac{s_ky_k^{\mathsf{T}}}{y_k^{\mathsf{T}}s_k}\right)^{\mathsf{T}} + \frac{s_k s_k^{\mathsf{T}}}{y_k^{\mathsf{T}}s_k}}$$</p><p>只要初始矩阵 ${\bf D_0}$ 是正定的，那么 ${\bf D_k}$ 就是正定的，所以我们可以选择 ${\bf D_0 &#x3D; I}$</p><p>至此我们就完成了黑塞矩阵逆矩阵的更新，参考代码如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">BFGSOptimizer</span>(<span class="title class_ inherited__">Optimizer</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, X, y, parameters, loss_function, gradients_function, lambd=<span class="number">0.01</span>, epsilon=<span class="number">1e-6</span></span>):</span><br><span class="line">        Optimizer.__init__(<span class="variable language_">self</span>, X, y, parameters, loss_function, gradients_function)</span><br><span class="line">        <span class="variable language_">self</span>.lambd = lambd</span><br><span class="line">        <span class="variable language_">self</span>.epsilon = epsilon</span><br><span class="line"></span><br><span class="line">        n, m = X.shape</span><br><span class="line">        <span class="variable language_">self</span>.D_k = np.eye(n)</span><br><span class="line">        <span class="variable language_">self</span>.g_k = <span class="variable language_">self</span>.gradients(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        <span class="variable language_">self</span>.s_k = np.zeros((<span class="number">1</span>, n))</span><br><span class="line">        <span class="variable language_">self</span>.d_k = np.zeros((<span class="number">1</span>, n))</span><br><span class="line">        <span class="variable language_">self</span>.y_k = np.zeros((<span class="number">1</span>, n))</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">step</span>(<span class="params">self</span>):</span><br><span class="line">        n, m = <span class="variable language_">self</span>.X.shape</span><br><span class="line">        <span class="variable language_">self</span>.d_k = -np.dot(<span class="variable language_">self</span>.g_k, <span class="variable language_">self</span>.D_k)</span><br><span class="line">        f = <span class="keyword">lambda</span> alpha : <span class="variable language_">self</span>.loss(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters + alpha * <span class="variable language_">self</span>.d_k, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        alpha = line_search(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, f)</span><br><span class="line">        <span class="variable language_">self</span>.s_k = alpha * <span class="variable language_">self</span>.d_k</span><br><span class="line">        <span class="variable language_">self</span>.parameters += <span class="variable language_">self</span>.s_k</span><br><span class="line">        old_g = <span class="variable language_">self</span>.g_k.copy()</span><br><span class="line">        <span class="variable language_">self</span>.g_k = <span class="variable language_">self</span>.gradients(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)</span><br><span class="line">        <span class="variable language_">self</span>.y_k = <span class="variable language_">self</span>.g_k - old_g</span><br><span class="line">        <span class="comment"># Update inversed approximate Hessian matrix</span></span><br><span class="line">        ys = np.dot(<span class="variable language_">self</span>.y_k, <span class="variable language_">self</span>.s_k.T)</span><br><span class="line">        ident = np.eye(n)</span><br><span class="line">        mat_left = np.asmatrix((ident - np.dot(<span class="variable language_">self</span>.s_k.T, <span class="variable language_">self</span>.y_k) / ys))</span><br><span class="line">        mat_right = np.asmatrix(mat_left.T)</span><br><span class="line">        <span class="variable language_">self</span>.D_k = mat_left * np.asmatrix(<span class="variable language_">self</span>.D_k) * mat_right + np.dot(<span class="variable language_">self</span>.s_k.T, <span class="variable language_">self</span>.s_k) / ys</span><br><span class="line">        loss = <span class="variable language_">self</span>.loss(<span class="variable language_">self</span>.X, <span class="variable language_">self</span>.y, parameters=<span class="variable language_">self</span>.parameters, lambd=<span class="variable language_">self</span>.lambd)[<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">        <span class="keyword">return</span> loss</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>BFGS 虽然需要迭代的次数较牛顿法更多，但却避免了二阶导数的计算，也没有矩阵的直接求逆，提升了运行效率。</p><h2 id="运行效果"><a href="#运行效果" class="headerlink" title="运行效果"></a>运行效果</h2><p><a href="/machine-learning/ml-logistic-regression-implementation/Figure_2.png" data-fancybox="gallery" data-caption=""><img src="/machine-learning/ml-logistic-regression-implementation/Figure_2.png"></a></p><center>原始数据，无映射</center><p><a href="/machine-learning/ml-logistic-regression-implementation/Figure_1.png" data-fancybox="gallery" data-caption=""><img src="/machine-learning/ml-logistic-regression-implementation/Figure_1.png"></a></p><center>添加了特征的平方</center><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p>完整代码请参考：<a href="https://github.com/howardlau1999/ml-scratch/blob/master/logistic_regression.py">logistic_regression.py</a>，数据来自 Andrew Ng 的 Machine Learning Coursera 课程，如有任何建议和想法欢迎交流！</p><p>注：通常 BFGS 存储完整的 ${\bf D_k}$ 较为消耗内存，尤其是当特征维数很大的时候，可能会占用几十 GB 的内存，这是普通计算机很难承受的，为此人们也提出了 L-BFGS 算法，目前本文还没有实现，也为了避免文章过于冗长，选择在另一篇博文继续更新。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>01 子串问题</title>
      <link>https://blog.howardlau.me/programming/zero-one-substring.html</link>
      <description>
        <![CDATA[<p>问题：给定一个长度为 n (1 &lt; n &lt; 1000000)]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 07 Mar 2018 23:55:15 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>问题：给定一个长度为 n (1 &lt; n &lt; 1000000) 的只含有<code>0</code>和<code>1</code>的字符串，求其中<code>0</code>和<code>1</code>数量相等的子字符串个数（不计空串）？子字符串指的是原串中下标为<code>i</code>到<code>j</code>（0 &lt;&#x3D; i &lt;&#x3D; j &lt;&#x3D; n-1）的字符组成的字符串。</p><h2 id="解法一（枚举）"><a href="#解法一（枚举）" class="headerlink" title="解法一（枚举）"></a>解法一（枚举）</h2><p>我们很容易想到，要使得<code>0</code>和<code>1</code>个数相等，这个字符串的长度一定为偶数。只需要枚举原字符串中长度为偶数的子串，然后检验是否满足条件即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">count_substring_slow</span><span class="params">(<span class="type">char</span> *str)</span> </span>&#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> n = <span class="built_in">strlen</span>(str);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> ans = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> len = <span class="number">2</span>; len &lt;= n; ++len) &#123;</span><br><span class="line">         <span class="keyword">for</span> (<span class="type">int</span> start = <span class="number">0</span>; start &lt;= n - len; ++start) &#123;</span><br><span class="line">            <span class="type">int</span> ones = <span class="number">0</span>, zeros = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> i = start; i &lt; start + len; ++i) &#123;</span><br><span class="line">                 <span class="keyword">if</span> (str[i] == <span class="string">&#x27;0&#x27;</span>) ++zeros;</span><br><span class="line">                 <span class="keyword">else</span> <span class="keyword">if</span> (str[i] == <span class="string">&#x27;1&#x27;</span>) ++ones;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (ones == zeros) ++ans;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ans;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然而，枚举的时间复杂度过高，达到了$O(n^3)$级别，我们要想办法优化。</p><h2 id="解法二（前缀和）"><a href="#解法二（前缀和）" class="headerlink" title="解法二（前缀和）"></a>解法二（前缀和）</h2><p>解法一时间复杂度高的其中一个原因是在统计子串字符数量时进行了过多重复的运算。举个例子，假如我们需要统计[i, j]的 0 和 1 的数量的时候其实可以利用[i, j - 1]的 0 和 1 的数量来进行增量计算。但是如果我们用 <code>ones[i][j]</code> 和 <code>zeros[i][j]</code> 来存储中间结果，不仅十分消耗空间，甚至可能超出了栈内存空间，导致运行时错误。</p><p>我们可以想到，可以直接将 0 和 1 求和，只要一个长度为 n 的字符串求得的结果为 n&#x2F;2 即可，只需要一个数组 <code>sum[i][j]</code> 就可以了。但是在面对 <code>1000000</code> 这个数据量的时候，<code>sum</code> 数组仍然会超出限制。数据量限制了我们必须用一维数组。这时候，<strong>前缀和</strong>就派上了用场。</p><p>前缀和，顾名思义，就是记录一个数组前 n 项和的数组。要求得任意一个下标区间为 [i, j] (i &gt; 0)的子数组的和，只需要计算 <code>sum[j] - sum[i - 1]</code> 即可。这样，我们可以先扫描一遍数组，计算出前缀和，然后计算长度为偶数的子区间的和是否等于 n &#x2F; 2 即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">count_substring_sum</span><span class="params">(<span class="type">char</span> *str)</span> </span>&#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> n = <span class="built_in">strlen</span>(str);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> ans = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> tmp_sum = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> sum[n]; <span class="comment">// VLA</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; ++i) &#123;</span><br><span class="line">        tmp_sum += (str[i] - <span class="string">&#x27;0&#x27;</span>);</span><br><span class="line">        sum[i] = tmp_sum;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> len = <span class="number">2</span>; len &lt;= n; ++len) &#123;</span><br><span class="line">         <span class="keyword">for</span> (<span class="type">int</span> start = <span class="number">0</span>; start &lt;= n - len; ++start) &#123;</span><br><span class="line">            <span class="keyword">if</span> (start == <span class="number">0</span> &amp;&amp; sum[len - <span class="number">1</span>] == len / <span class="number">2</span>) &#123;</span><br><span class="line">                ++ans;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (sum[start + len - <span class="number">1</span>] - sum[start - <span class="number">1</span>] == len / <span class="number">2</span>) &#123;</span><br><span class="line">                ++ans;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ans;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种解法相较于直接枚举，少了一层循环，但是时间复杂度仍然有O(n^2)，对于百万级别仍然是不可接受的，需要继续改进。</p><h2 id="解法三"><a href="#解法三" class="headerlink" title="解法三"></a>解法三</h2><p>为了方便计算，我们可以定义 0 的价值为 -1，1 的价值为 1，只要我们计算得到的子区间的和为 0 即可。然而这仅仅只能少做了除法，没有根本上改变算法的复杂度。那么，我们还能从前缀和发现什么规律呢？</p><p>仔细观察发现，如果前缀和 sum[i] &#x3D;&#x3D; sum[j]，那么说明在(i, j]这一段的子数组区间和为 0。这样，我们只需要找到有多少对这样的 i 和 j 即可。如果我们采用扫描两次的方法，仍然效率不高，我们可以直接统计前缀和为 k 的数量 nk，那么我们只需要对所有这样的 k 所组成的组合数  Ck^2 进行求和，然后特别地再加上前缀和为 0 的数量，就是我们所要的答案。</p><p>由于 k 有可能小于 0，但最小只会是 -n，所以我们直接将 k 加上 n 进行哈希，就可以将其限制在数组范围内了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> score(ch) (ch == <span class="string">&#x27;1&#x27;</span> ? 1 : -1)</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">count_substring_fast</span><span class="params">(<span class="type">char</span> *str)</span> </span>&#123;</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> n = <span class="built_in">strlen</span>(str);</span><br><span class="line">    <span class="type">unsigned</span> <span class="type">int</span> ans = <span class="number">0</span>;</span><br><span class="line">    <span class="type">int</span> *counter = <span class="keyword">new</span> <span class="type">int</span>[<span class="number">2</span> * n + <span class="number">1</span>](); <span class="comment">// initialize</span></span><br><span class="line">    <span class="type">int</span> tmp_sum = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; ++i) &#123;</span><br><span class="line">        tmp_sum += <span class="built_in">score</span>(str[i]);</span><br><span class="line">        <span class="keyword">if</span> (tmp_sum == <span class="number">0</span>) ++ans;</span><br><span class="line">        counter[n + tmp_sum]++;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">2</span> * n + <span class="number">1</span>; ++i) &#123;</span><br><span class="line">         <span class="keyword">if</span> (counter[i]) ans += ((counter[i]) * (counter[i] - <span class="number">1</span>) / <span class="number">2</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">delete</span>[] counter;</span><br><span class="line">    <span class="keyword">return</span> ans;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>算法则优化到了O(n)的线性时间复杂度级别。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>经典寻路算法：A* 算法</title>
      <link>https://blog.howardlau.me/programming/a-star-algorithm.html</link>
      <description>
        <![CDATA[<p>（本文图片引用自 <a href="http://www-cs-students.stanford.edu/~amitp/">Amit Patel</a> 的博客）
在游戏中，我们常常会遇到寻找两点之间最短路径的任务。而寻路算法中比较经典的就是 <strong>A*]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 06 Mar 2018 06:00:38 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>（本文图片引用自 <a href="http://www-cs-students.stanford.edu/~amitp/">Amit Patel</a> 的博客）在游戏中，我们常常会遇到寻找两点之间最短路径的任务。而寻路算法中比较经典的就是 <strong>A* <code>A*</code> 算法</strong> 了。实际上，对于理解了它的人来说这是个很简单的算法，但对于不了解的人来说，可能这是一个比较复杂的算法。</p><p>理解一个复杂概念的最好办法便是由浅入深，<code>A*</code> 算法的提出也并非一蹴而就，它也是由一些简单的算法改造而来的。本文尝试带你一步步去学习 <code>A*</code> 算法的基本知识，帮助你理解这个算法。</p><h2 id="最简单的寻路——贪心算法"><a href="#最简单的寻路——贪心算法" class="headerlink" title="最简单的寻路——贪心算法"></a>最简单的寻路——贪心算法</h2><p>一个最简单的寻路算法便是直接计算起点各个方向到终点的距离，选择最小的那个方向走一步就可以了。这个算法理解起来很直观，计算量也不大，某些情况下是很好的算法，比如下图所示：<a href="/programming/a-star-algorithm/best-first-search.png" data-fancybox="gallery" data-caption=""><img src="/programming/a-star-algorithm/best-first-search.png"></a></p><p>然而，这个算法有一个缺点：过于“短视”，只考虑了一步，如果途中有障碍物，必须等到撞上了障碍物才回头走另外的路，这时候找到的路径也不是最短的，就像下面的情况：<a href="/programming/a-star-algorithm/best-first-search-trap.png" data-fancybox="gallery" data-caption=""><img src="/programming/a-star-algorithm/best-first-search-trap.png"></a></p><h2 id="一定正确的寻路——Dijkstra-算法"><a href="#一定正确的寻路——Dijkstra-算法" class="headerlink" title="一定正确的寻路——Dijkstra 算法"></a>一定正确的寻路——Dijkstra 算法</h2><p>为了解决贪心算法的缺点，Dijkstra 算法则采用了一种更加“目光长远”的搜索方式，它不仅仅只看当前方向可以走的下一步，还会在下一步的基础上再进一步搜索下去，直到找到一条到终点的路径为止。</p><p>Dijkstra 算法事实上有点像带了记忆的 BFS 算法（深度优先搜索），它采用了一个队列记忆即将要搜索的节点，还有一个数组来记录各个节点到起点的最短距离，如果队列不空的话就从里面取出一个进行扩展，并更新距离数组，循环直到到达终点或者队列已经清空的情况则终止搜索。关于 Dijkstra 的详细算法，本文不再赘述，可以到网上搜索相关文章学习。</p><p>Dijkstra 算法保证找到的路径是最短的，不会出现绕远路的现象：</p><p><a href="/programming/a-star-algorithm/dijkstra-trap.png" data-fancybox="gallery" data-caption=""><img src="/programming/a-star-algorithm/dijkstra-trap.png"></a></p><p>但是相比于贪心算法，它也有一个缺点：搜索量太大了，贪心算法很快就能找到的路径，Dijkstra 算法很可能要搜索多不少的节点：</p><p><a href="/programming/a-star-algorithm/dijkstra.png" data-fancybox="gallery" data-caption=""><img src="/programming/a-star-algorithm/dijkstra.png"></a></p><p>有没有搜索又快又准的算法呢？有！那就是大名鼎鼎的 <code>A*</code> 算法了！</p><h2 id="又快又准的寻路——A-算法"><a href="#又快又准的寻路——A-算法" class="headerlink" title="又快又准的寻路——A* 算法"></a>又快又准的寻路——<code>A*</code> 算法</h2><h3 id="启发式搜索"><a href="#启发式搜索" class="headerlink" title="启发式搜索"></a>启发式搜索</h3><p>观察 Dijkstra 算法和贪心算法的尝试节点，我们会发现 Dijkstra 在搜索时会搜索一些明显很差的节点，如果我们能用某种办法来衡量一个节点是否应该被搜索，就可以一定程度上减少 Dijkstra 的搜索量，加快算法速度。那么这个办法是什么呢？那就是在 Dijkstra 算法基础上加上一个<strong>启发式搜索</strong>的思想，也是 <code>A*</code> 算法最重要的思想。</p><p>我们先来看一个简单的公式：$f(n)&#x3D;g(n)+h(n)$</p><p>如果你在别的地方想学习 <code>A*</code> 算法，一定也会看见这条公式。这条公式为启发式搜索提供了判断依据，其中f(n)表示一个节点的“搜索代价”，越小越好；而f(n)有两个计算依据：一个是g(n)，是当前节点到起点的<strong>真实距离</strong>；另一个是h(n)，是当前节点到终点的“估算距离”。g(n)的计算方法很简单，就是前一个节点的g(n)值加上两节点之间的距离，而h(n)则不能随便选取，需要满足启发一致性，也就是 h(n) &lt;&#x3D; c(n, n’) + h(n’)，其中 n’ 是 n 的所有邻接节点，c 是两个节点间的真实距离，并且要满足 h(G) &#x3D; 0，其中 G 是终点，如果可以上下左右四个方向移动，一般采用的是<strong>曼哈顿距离</strong>，如果可以朝八个方向移动，则一般采用<strong>欧几里得距离</strong>。</p><p>这条公式有两种特殊情况：1、h(n)&#x3D;0，意味着算法没有任何的启发性，退化为效率较低的 Dijkstra 算法。2、g(n)&#x3D;0，意味着算法没有任何的准确性，退化为未必最短的贪心算法。</p><p>而 <code>A*</code> 算法搜索时，不再一一扩展搜索队列中的每一个元素，而是只扩展搜索队列中f(n)值最小的那一个节点，这样就可以避免少搜索很多的节点。</p><p>话不多说，来看看代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// map 数组记录了地图，也就是某个节点可不可以走</span></span><br><span class="line"><span class="comment">// visited 记录了某个节点是否被访问过了</span></span><br><span class="line"><span class="comment">// to_visit 记录某个节点是不是在搜索队列中</span></span><br><span class="line"><span class="comment">// open 搜索队列</span></span><br><span class="line"><span class="comment">// 将起点加入到搜索队列中</span></span><br><span class="line">start.h_value = start.f_value = <span class="built_in">h_dist</span>(start, end);</span><br><span class="line">start.g_value = <span class="number">0</span>;</span><br><span class="line">open.<span class="built_in">add</span>(start);</span><br><span class="line"><span class="keyword">while</span> (!open.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">    <span class="function">shared_ptr&lt;Node&gt; <span class="title">cur</span><span class="params">(<span class="keyword">new</span> Node)</span></span>;</span><br><span class="line">    <span class="comment">// 获取 f 函数最小的一个节点</span></span><br><span class="line">    *cur = open.<span class="built_in">getMin</span>();</span><br><span class="line">    visited[cur-&gt;y][cur-&gt;x] = <span class="literal">true</span>;</span><br><span class="line">    to_visit[cur-&gt;y][cur-&gt;x] = <span class="literal">false</span>;</span><br><span class="line">    <span class="comment">// 到达了终点</span></span><br><span class="line">    <span class="keyword">if</span> (*cur == end) &#123;</span><br><span class="line">        <span class="built_in">printPath</span>(cur)</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">4</span>; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> ny = cur-&gt;y + dy[i], nx = cur-&gt;x + dx[i];</span><br><span class="line">        <span class="comment">// 确保数组不越界</span></span><br><span class="line">        <span class="keyword">if</span> (ny &lt;= max_y &amp;&amp; ny &gt;= <span class="number">0</span> &amp;&amp; nx &lt;= max_x &amp;&amp; nx &gt;= <span class="number">0</span>)</span><br><span class="line">            <span class="keyword">if</span> (!visited[ny][nx] &amp;&amp; map[ny][nx] != BLOCK) &#123;</span><br><span class="line">                <span class="comment">// 计算新的 g 函数值，右边的 + 1 可以依照需要替换为两个节点的距离</span></span><br><span class="line">                <span class="type">int</span> tmp_g_value = cur-&gt;g_value + <span class="number">1</span>;</span><br><span class="line">                <span class="type">bool</span> better = <span class="literal">true</span>;</span><br><span class="line">                Node next;</span><br><span class="line">                next.y = ny;</span><br><span class="line">                next.x = nx;</span><br><span class="line">                </span><br><span class="line">                <span class="keyword">if</span> (to_visit[ny][nx]) &#123;</span><br><span class="line">                    next = open.<span class="built_in">find</span>(next);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="comment">// 如果之前记录的 g 值比新的小，说明新的绕了远路，放弃更新</span></span><br><span class="line">                <span class="keyword">if</span> (next.g_value &lt; tmp_g_value)</span><br><span class="line">                    better = <span class="literal">false</span>;</span><br><span class="line">                <span class="keyword">if</span> (better) &#123;</span><br><span class="line">                    <span class="comment">// 用于构造最短路径</span></span><br><span class="line">                    next.parent = <span class="built_in">shared_ptr</span>&lt;Node&gt;(cur);</span><br><span class="line">                    next.g_value = tmp_g_value;</span><br><span class="line">                    next.h_value = <span class="built_in">h_dist</span>(next, end);</span><br><span class="line">                    next.f_value = next.g_value + next.h_value;</span><br><span class="line">                    <span class="keyword">if</span> (!to_visit[ny][nx]) &#123;</span><br><span class="line">                        open.<span class="built_in">add</span>(next);</span><br><span class="line">                        to_visit[ny][nx] = <span class="literal">true</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>相信上面的代码可以给你一定的启发，编写自己的 <code>A*</code> 算法。</p><h3 id="更多的优化"><a href="#更多的优化" class="headerlink" title="更多的优化"></a>更多的优化</h3><p>在上面的算法中，如果我们使用遍历搜索队列的办法来找到f(n)最小的函数，那么算法复杂度达到O(n^2)。这一部分可以采用<strong>优先队列</strong>或者<strong>小根堆</strong>的办法来实现O(n\log n)的算法复杂度。</p><p>希望本文可以帮助你理解 <code>A*</code> 算法。其实许多的算法也是从简单的情况入手，慢慢寻找条件优化时间的，我们学习比较复杂的算法的时候也可以用这样的方法来帮助自己学习。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>一些神经网络结构的初步认识</title>
      <link>https://blog.howardlau.me/programming/neural-networks.html</link>
      <description>
        <![CDATA[<p>本文粗略地记录一下各个神经网络的特点和发展，仅供抛砖引玉，如需了解详情，可以自行深入搜索。</p>
<h2 id="神经网络-（Neural-Network）"><a href="#神经网络-（Neural-Network）" class="headerlink"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Fri, 02 Mar 2018 02:01:23 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>本文粗略地记录一下各个神经网络的特点和发展，仅供抛砖引玉，如需了解详情，可以自行深入搜索。</p><h2 id="神经网络-（Neural-Network）"><a href="#神经网络-（Neural-Network）" class="headerlink" title="神经网络 （Neural Network）"></a>神经网络 （Neural Network）</h2><p>在出现多层神经网络之前，Frank Rosenblatt 在 1957 年就发明了一种名为 Perceptron 的神经结构，这种神经结构十分简单，可以用一条公式来概括，也就是 $$y &#x3D; \sigma (\mathbf{W}\mathbf{x}+b)$$它的结构也可以用下图表示：<a href="/programming/neural-networks/ArtificialNeuronModel.png" data-fancybox="gallery" data-caption=""><img src="/programming/neural-networks/ArtificialNeuronModel.png"></a></p><p>尽管结构看起来十分简单，但却能完成较为复杂的线性二元分类问题。然而，其严重的缺陷是不能完成<strong>非线性</strong>的二元分类问题，例如异或问题。在此基础上，人们将其改进为具有多层结构的多层感知机，也就是现在所说的神经网络。</p><p><a href="/programming/neural-networks/main-qimg-7f4840d91eb9da1b6673511a0eb806f2.png" data-fancybox="gallery" data-caption=""><img src="/programming/neural-networks/main-qimg-7f4840d91eb9da1b6673511a0eb806f2.png"></a></p><p>其特点是将上一层网络的输出输入到下一层网络中，使用非线性的激活函数（Activation）如著名的$\sigma (x) &#x3D; \frac{1}{1+e^{-x}}$、$\tanh (x)$函数，或者是其后发明的$\mathbf{ReLU}$函数，赋予神经网络强大的非线性拟合能力。</p><p>改进后的多层神经网络有了质的飞跃，理论上可以拟合任何连续函数。</p><p>然而我们并不知道神经网络各层的具体参数，但是借助 BP 算法，我们可以使用已有的数据来<strong>训练</strong>神经网络，这种使用标注好的数据来训练网络也称作<strong>监督式学习</strong>（Supervised Learning）。</p><h3 id="反向传播（Back-Propagation）"><a href="#反向传播（Back-Propagation）" class="headerlink" title="反向传播（Back Propagation）"></a>反向传播（Back Propagation）</h3><p>给定神经网络的结构之后我们可以对其进行训练，也就是自动调整参数使得其能较好地拟合出数据中隐含的函数。其思想是给定一个损失函数（Loss Function）来衡量当前网络参数的好坏，然后通过求导，找出使损失函数下降最快的方向，并且依据这个标准更新已有的参数，当损失函数下降的幅度足够小，或者训练足够的次数时候，我们就可以停止训练，认为训练已经结束。</p><p>BP 算法是机器学习中最为重要的一个概念，它几乎广泛地应用于每一个神经网络的训练中，为了方便计算，计算机科学家利用<strong>计算图</strong>发明了<strong>自动求导</strong>的技术，使得人们可以专注于神经网络结构的部分，而不必太操心过多细节。著名的框架有 Google 的 <code>TensorFlow</code>，Amazon 的 <code>MXNet</code> 和新兴的 <code>pytorch</code> 等等。由于 <code>Python</code> 语言编程方便，且多数机器学习框架也提供了 <code>Python</code> 接口，<code>Python</code> 成为了人工智能领域使用广泛的一种编程语言。</p><h3 id="调参"><a href="#调参" class="headerlink" title="调参"></a>调参</h3><p>尽管多数参数的学习是全自动的，但是仍有一些需要人工调整的参数来改善神经网络结构的学习性能，这些参数也称为超参数（Hyper Parameter），比如学习率（Learning Rate）$\alpha$，它决定了参数更新的幅度，如果太小会造成学习过于缓慢，如果太大则会导致神经网络错过最优解，甚至朝着相反的方向学习。人们可以借助学习过程中一些标准的变化来调整参数，使得网络尽可能最优。</p><h2 id="卷积神经网络（Convolutional-Neural-Network）"><a href="#卷积神经网络（Convolutional-Neural-Network）" class="headerlink" title="卷积神经网络（Convolutional Neural Network）"></a>卷积神经网络（Convolutional Neural Network）</h2><p>卷积神经网络，简称 CNN，顾名思义，网络中含有<strong>卷积</strong>这个操作。其最初由 LeCun 提出。当时人们想利用神经网络来解决图片识别问题，比如邮政编码的手写数字识别。如果将图片的每一个像素都当成特征输入到神经网络中，会造成参数过多的问题，而且训练效果也不如人意。卷积操作简单来讲就是用一个小的矩阵在图片上滚动然后按元素相乘，再将结果求和，便得到了一个新的图片。这样做的好处是<strong>参数共享</strong>，不仅减少了参数量、加快了学习速度，也使得神经网络可以学习到图片的一些共同特征。它的一种结构如下图所示：</p><p><a href="/programming/neural-networks/mnist.png" data-fancybox="gallery" data-caption=""><img src="/programming/neural-networks/mnist.png"></a></p><p>其中除了卷积操作还包含了<strong>池化</strong>操作，可以起到一定的降维效果，同时最大池化也有特征强化的效果，它的操作和卷积类似，常见的有最大池化和平均池化两种，最常用的是最大池化。</p><p>在简单的 CNN 基础上，人们不停地改进，发明了 <code>VGG-16</code>、<code>GoogLeNet</code>、<code>YOLO</code>、<code>R-CNN</code>、<code>Mask R-CNN</code> 等等更深的、效果也更好的神经网络，其中有些网络不仅可以实现分类的功能，还可以对图片中的物体位置进行具体的标注。</p><p>CNN 也有类似艺术风格迁移的有趣的应用，可以将图片变成指定的艺术风格。CNN 在人脸识别任务上也表现得十分出色。</p><h2 id="循环神经网络（Recurrent-Neural-Network）"><a href="#循环神经网络（Recurrent-Neural-Network）" class="headerlink" title="循环神经网络（Recurrent Neural Network）"></a>循环神经网络（Recurrent Neural Network）</h2><p>循环神经网络，简称 RNN，则是一种循环式的神经网络。它主要用于随时间变化的序列问题，比如自然语言、语音识别等等。其特点是不断将自己的输出作为输入，以达到循环的效果。为了赋予其像人类一样的记忆力，人们发明了 <code>LSTM</code> （长短时记忆）、<code>GRU</code>（门控单元）等单元，改善学习效果。在此基础上，由于一个序列中的部分可能受到序列中不同位置不同程度的影响，比如英语中一个句子的谓语形式由主语、时态等等决定，所以人们引入了注意力模型（Attention），来让模型具有像人一样的注意力。</p><p>值得一提的是，基于神经网络的语音识别效果已经远远好于以前人们用一些数学模型手动构建的识别模型的效果了，可见神经网络的强大。</p><h2 id="对抗生成网络（Generative-Adversarial-Network）"><a href="#对抗生成网络（Generative-Adversarial-Network）" class="headerlink" title="对抗生成网络（Generative Adversarial Network）"></a>对抗生成网络（Generative Adversarial Network）</h2><p>对抗生成网络，简称 GAN，是近年来比较有趣的神经网络之一。它可以用来“无中生有”，生成图片。它的特点是同时训练两个网络，一个生成者，一个识别者。生成者需要尽可能生成识别者不能识别为假图片的图片，而识别者则需要尽可能识别出生成者生成的假图片。训练的时候可以用真假图片混合输入的办法，来训练识别者，而生成者则根据识别者的反馈训练自己，十分有趣。</p><h2 id="组合使用"><a href="#组合使用" class="headerlink" title="组合使用"></a>组合使用</h2><p>不同的神经网络可以组合使用，比如我们可以组合 CNN 以及 RNN 来进行看图说话的任务。可以用 CNN 来提取图片的特征，然后 RNN 根据特征合成出一个句子。又或者两个 RNN 组合起来实现机器翻译。多种网络组合可以实现更多惊人的功能。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>机器学习或者深度学习正在蓬勃发展之中，尽管现成有不少的框架和模型可以使用，但其本质仍然是由微积分、线性代数、概率统计学构建的数学模型。万变不离其宗，学好数学物理，可以大大加深我们对机器学习的理解，同时帮助我们发明新的算法或者新的模型。希望看了本文的你可以对现在一些比较通用的机器学习模型有所了解。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>十进制转任意进制的算法原理</title>
      <link>https://blog.howardlau.me/science/base-conversion-of-number-proof.html</link>
      <description>
        <![CDATA[<p>在中学课本里，我们学到了一个将十进制数转换为任意进制的算法：将十进制数除以新的进制，然后将商作为下一次除法的被除数，直到商为]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <category domain="https://blog.howardlau.me/category/science/">科普</category>
      <pubDate>Tue, 23 Jan 2018 05:10:16 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在中学课本里，我们学到了一个将十进制数转换为任意进制的算法：将十进制数除以新的进制，然后将商作为下一次除法的被除数，直到商为 0，然后将每次除法的余数从右到左写，就得到了新的进制。这篇小文章用数学公式来说明这个神奇算法中的数学道理。</p><p>首先，我们不妨假设十进制正数为 $N$，新的进制为 $R (R \geq 2)$ 进制。我们设第 $i$ 次除法的商和余数分别记作 $q_i$ 和 $y_i$。根据算法，我们可以得到下面的递推公式：$$q_{i-1} \div R &#x3D; q_{i} \cdots \cdots y_i (1 \leq i \leq k)$$其中，我们约定$q_0 &#x3D; N$，$q_k &#x3D; 0$，$q_{k-1} \neq 0$根据余数的意义，我们将上述公式变为$$q_{i} \times R + y_i &#x3D; q_{i-1} \tag{*}$$我们的出发点是要证明$$N &#x3D; \sum_{i&#x3D;1}^{k} y_i \times R^{i-1}$$因此，我们很容易联想到将$(*)$式左右同时乘上$R^{i-1}$，得到$$q_{i} \times R^i + y_i \times R^{i-1} &#x3D; q_{i-1} \times R^{i-1}$$当我们把公式累加起来之后，等式左右两边许多的项都相互抵消了，所以可以得到一个简单的公式：$$q_{k} \times R^k + \sum_{i&#x3D;1}^{k} y_i \times R^{i-1} &#x3D; q_{0} \times R^{0}$$根据约定，我们最终得到$$\sum_{i&#x3D;1}^{k} y_i \times R^{i-1} &#x3D; N \times R^{0} &#x3D; N$$其中等式左边便等于用余数从右到左排列得到的$R$进制数，而它又等于十进制数$N$，这就是进制转换的算法的数学道理。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>计算机负整数的二进制表示</title>
      <link>https://blog.howardlau.me/science/binary-representation-of-negative-numbers.html</link>
      <description>
        <![CDATA[<p>在中学的信息技术课上，相信大家也听说过电脑中原码、反码、补码这些名词，这篇小文章就是来用数学公式来解释为什么负数的二进制表示是补码。如果你不知道补码是什么，请<a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/science/">科普</category>
      <pubDate>Tue, 23 Jan 2018 04:38:37 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在中学的信息技术课上，相信大家也听说过电脑中原码、反码、补码这些名词，这篇小文章就是来用数学公式来解释为什么负数的二进制表示是补码。如果你不知道补码是什么，请<a href="https://baike.baidu.com/item/%E8%A1%A5%E7%A0%81">点击这里查看百科解释</a>。</p><p>假设计算机内部用 $k$ 位二进制来表示一个整数。那么，一个正整数 $I$ 就可以写成下面的形式：$$I &#x3D; \sum_{i&#x3D;1}^{k} d_i \times 2^{i-1}$$其中$d_i$表示二进制数从右往左数的第$i$位数字。</p><p>而根据 <a href="https://en.wikipedia.org/wiki/Two%27s_complement">Two’s Complement</a> 原则，一个正整数$I$的负数就应该这样表示：$$Neg(I) &#x3D; 2^k - I$$从这个公式出发，怎样推出补码就是我们想要的结果呢？我们知道，补码就是每一位取反，然后加一。而对每一位取反，就相当于用 1 减去二进制上的每一位数，所以，我们得到了关于负数的另外一个公式：$$Neg(I) &#x3D; \sum_{i&#x3D;1}^{k} (1-d_i) \times 2^{i-1} +1$$将求和符号拆开，然后展开，我们得到$$Neg(I) &#x3D; \sum_{i&#x3D;1}^{k} 2^{i-1} - \sum_{i&#x3D;1}^{k} d_i \times 2^{i-1} + 1$$注意到公式左边是等比数列求和，所以$$Neg(I) &#x3D; \frac{2^0(2^k-1)}{2-1} - \sum_{i&#x3D;1}^{k} d_i \times 2^{i-1} + 1$$</p><p>$$&#x3D; 2^k- \sum_{i&#x3D;1}^{k} d_i \times 2^{i-1}$$到这里，结果就很显然了，减号的右边就是$I$，所以$$Neg(I) &#x3D; 2^k - I$$</p><p>可以看出来，补码的运算结果和 Two’s complement 公式相同，所以我们可以下一个结论，补码的运算可以得到一个正整数的负数！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Snake.c 用 C 语言写贪吃蛇！</title>
      <link>https://blog.howardlau.me/programming/snake-c-first.html</link>
      <description>
        <![CDATA[<p>在学习编程语言的过程中，偶尔写一些字符游戏，既能锻炼编程能力，又可以娱乐自己。今天就让我们一起学习怎样用 C]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Tue, 12 Dec 2017 04:42:43 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在学习编程语言的过程中，偶尔写一些字符游戏，既能锻炼编程能力，又可以娱乐自己。今天就让我们一起学习怎样用 C 语言写经典游戏御三家之一的贪吃蛇吧！</p><p>当然，为了让程序编写更有条理，我们要采用<strong>自顶向下</strong>的方法来设计我们的贪吃蛇程序。</p><p>这个版本还十分粗糙，更多的打磨就留给大家自己尝试吧！</p><h2 id="第一版：会动的蛇"><a href="#第一版：会动的蛇" class="headerlink" title="第一版：会动的蛇"></a>第一版：会动的蛇</h2><h3 id="主函数"><a href="#主函数" class="headerlink" title="主函数"></a>主函数</h3><p>贪吃蛇的游戏逻辑十分简单：如果游戏没有结束，那就读入一个字符，判断方向，然后让蛇走一步,然后把界面输出让玩家知道现在的游戏状态。根据<strong>自顶向下</strong>的设计思想，我们先简单地编写主程序框架：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> UP 0,-1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> DOWN 0,1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> LEFT -1,0</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> RIGHT 1,0</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span>* argv[])</span> &#123;</span><br><span class="line">    printGrid();</span><br><span class="line">    <span class="keyword">while</span> (!gameOver()) &#123;</span><br><span class="line">        <span class="type">char</span> ch = readInput();</span><br><span class="line">        <span class="keyword">switch</span> (ch) &#123;</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&#x27;W&#x27;</span>:</span><br><span class="line">                snakeMove(UP);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&#x27;A&#x27;</span>:</span><br><span class="line">                snakeMove(LEFT);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&#x27;S&#x27;</span>:</span><br><span class="line">                snakeMove(DOWN);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> <span class="string">&#x27;D&#x27;</span>:</span><br><span class="line">                snakeMove(RIGHT);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        printGrid();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，我们只需一步步实现其中的函数就可以了，就暂时不用操心这段代码了，这就是自顶向下设计的好处：减少了记忆量和思维量。</p><h3 id="定义数据结构"><a href="#定义数据结构" class="headerlink" title="定义数据结构"></a>定义数据结构</h3><p>在实现函数之前，我们最好先定义怎样存储我们的游戏状态。先不考虑食物，我们将游戏界面分解成几个元素：边界（障碍）、蛇，而蛇又会有蛇头和蛇身的区别。当然，很容易想到一个位置只可能是上面几种元素的一种，所以我们可以直接定义一个二维的字符数组来存放我们的界面。</p><p>但是，蛇是动态变化的元素，为了方便，我们最好再定义几个数组，存放蛇和食物的坐标。</p><p>有了上面的分析，我们就可以写出我们存放数据的数据结构代码了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> SNAKE_MAX_LENGTH 20</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> GRID_WIDTH 12</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> GRID_HEIGHT 12</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CHAR_GRID_WALL <span class="string">&#x27;*&#x27;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CHAR_GRID_BLANK <span class="string">&#x27; &#x27;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CHAR_SNAKE_BODY <span class="string">&#x27;X&#x27;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> CHAR_SNAKE_HEAD <span class="string">&#x27;H&#x27;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">char</span> grid[GRID_HEIGHT][GRID_WIDTH] = &#123;</span><br><span class="line">    <span class="string">&quot;************&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*XXXXH     *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;*          *&quot;</span>,</span><br><span class="line">    <span class="string">&quot;************&quot;</span></span><br><span class="line">&#125;;  <span class="comment">/* 先直接打表 */</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> snakeX[SNAKE_MAX_LENGTH] = &#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>, <span class="number">5</span>&#125;;</span><br><span class="line"><span class="type">int</span> snakeY[SNAKE_MAX_LENGTH] = &#123;<span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>, <span class="number">1</span>&#125;;</span><br><span class="line"><span class="type">int</span> snakeLength = <span class="number">5</span>;</span><br></pre></td></tr></table></figure><h3 id="实现输出界面函数"><a href="#实现输出界面函数" class="headerlink" title="实现输出界面函数"></a>实现输出界面函数</h3><p>首先我们先实现 <code>printGrid()</code> 函数，让我们的程序可以输出界面。这个函数很简单，我们只要一行一行地输出 <code>grid</code> 数组中的字符即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Clear the screen and print out the grid. */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">printGrid</span><span class="params">()</span> &#123;</span><br><span class="line">    clearScreen();</span><br><span class="line">    <span class="type">int</span> x, y;</span><br><span class="line">    <span class="keyword">for</span> (y = <span class="number">0</span>; y &lt; GRID_HEIGHT; ++y) &#123;</span><br><span class="line">        <span class="keyword">for</span> (x = <span class="number">0</span>; x &lt; GRID_WIDTH; ++x) &#123;</span><br><span class="line">            <span class="built_in">putchar</span>(grid[y][x]);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">putchar</span>(<span class="string">&#x27;\n&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们又用到了一个我们没有实现的函数 <code>clearScreen()</code> ，我们希望每次输出界面的时候都先清空屏幕，经过查资料，在 Linux 系统上，我们只需要调用 <code>system(&quot;clear&quot;)</code> 就能实现我们的目的。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Clear the screen. */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">clearScreen</span><span class="params">()</span> &#123;</span><br><span class="line">    system(<span class="string">&quot;clear&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="实现输入字符函数"><a href="#实现输入字符函数" class="headerlink" title="实现输入字符函数"></a>实现输入字符函数</h3><p>现在，我们已经完成了我们主函数里的第一个函数，接下来要实现的便是 <code>readInput()</code> 函数，让我们能得到用户输入的字符。我们希望只获取到用户输入的第一个字符，其他的全部抛弃，其实现也很简单：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Read in only one character and discard the following characters. */</span></span><br><span class="line"><span class="comment">/* If no character is read, return space character. */</span></span><br><span class="line"><span class="type">char</span> <span class="title function_">readInput</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">char</span> ch;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">scanf</span>(<span class="string">&quot;%c&quot;</span>, &amp;ch)) &#123;</span><br><span class="line">        <span class="keyword">while</span> (getchar() != <span class="string">&#x27;\n&#x27;</span>) <span class="keyword">continue</span>;</span><br><span class="line">        <span class="keyword">return</span> ch;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27; &#x27;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="让蛇动起来"><a href="#让蛇动起来" class="headerlink" title="让蛇动起来"></a>让蛇动起来</h3><p>实现完输出输入函数之后，我们就要开始真正的任务了：让我们的蛇可以动起来，也就是实现 <code>snakeMove(int dx, int dy)</code> 函数。</p><p>思路是什么呢？从前面数据结构的定义来看，坐标数组的下标为 <code>snakeLength - 1</code> 的元素就是我们蛇头的坐标，其余都是蛇身的坐标。首先，蛇身部分则依次往前挪一个单位，也就是用第 <code>i</code> 个坐标代替第 <code>i - 1</code> 个坐标（这里，<code>1 &lt;= i &lt;= snakeLength - 1</code>），然后蛇头根据方向进行坐标变换，这样我们的蛇就像是在动起来了。</p><p>而移动之前，我们要先让原来的蛇在地图上消失，然后移动之后再画上去，这样就不会出现重复的蛇了。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Move the snake one step according to dx and dy */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">snakeMove</span><span class="params">(<span class="type">int</span> dx, <span class="type">int</span> dy)</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="comment">/* Clear the original snake */</span></span><br><span class="line">    clearSnake();</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Propagate the movement first */</span></span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; snakeLength - <span class="number">1</span>; ++i) &#123;</span><br><span class="line">        snakeX[i] = snakeX[i + <span class="number">1</span>];</span><br><span class="line">        snakeY[i] = snakeY[i + <span class="number">1</span>];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Set the new head */</span></span><br><span class="line">    snakeX[snakeLength - <span class="number">1</span>] = snakeX[snakeLength - <span class="number">1</span>] + dx;</span><br><span class="line">    snakeY[snakeLength - <span class="number">1</span>] = snakeY[snakeLength - <span class="number">1</span>] + dy;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Print the new snake */</span></span><br><span class="line">    drawSnake();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们再次应用自顶向下的方法分解函数，按照思路继续实现 <code>drawSnake()</code> 和 <code>clearSnake()</code> 即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Clear the snake */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">clearSnake</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; snakeLength; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> x = snakeX[i];</span><br><span class="line">        <span class="type">int</span> y = snakeY[i];</span><br><span class="line">        grid[y][x] = CHAR_GRID_BLANK;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Draw the snake */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">drawSnake</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; snakeLength; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> x = snakeX[i];</span><br><span class="line">        <span class="type">int</span> y = snakeY[i];</span><br><span class="line">        grid[y][x] = (snakeLength - <span class="number">1</span>) == i ? CHAR_SNAKE_HEAD : CHAR_SNAKE_BODY;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="判断游戏是否结束"><a href="#判断游戏是否结束" class="headerlink" title="判断游戏是否结束"></a>判断游戏是否结束</h3><p>我们先不编写这部分函数，让游戏永不结束。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">gameOver</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="尝试运行"><a href="#尝试运行" class="headerlink" title="尝试运行"></a>尝试运行</h3><p>到这里，我们就已经完成了贪吃蛇的一小部分了，至少我们的蛇可以动起来了！让我们编译运行它吧！</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">gcc snake_move.c -osnake.out</span><br><span class="line">./snake.out</span><br></pre></td></tr></table></figure><p><a href="/programming/snake-c-first/snake_move.png" data-fancybox="gallery" data-caption=""><img src="/programming/snake-c-first/snake_move.png"></a></p><p>通过输入 <code>WSAD</code> 四个不同的大写字母，然后回车，我们可以看到我们的蛇真的动起来了。</p><h2 id="第二版：可以吃东西的蛇"><a href="#第二版：可以吃东西的蛇" class="headerlink" title="第二版：可以吃东西的蛇"></a>第二版：可以吃东西的蛇</h2><p>这时候，仅仅是会动的蛇看上去很无聊，没什么意思，这时候，我们希望可以随机地出现一些食物，有了上面会动的蛇做基础，我们只需按部就班，添加关于食物的数据结构和函数即可。</p><h3 id="食物的数据结构"><a href="#食物的数据结构" class="headerlink" title="食物的数据结构"></a>食物的数据结构</h3><p>和蛇一样，由于食物是动态的，我们最好也跟蛇的实现一样，存放食物的数量和食物的坐标。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> CHAR_GRID_FOOD <span class="string">&#x27;$&#x27;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> foodX[FOOD_MAX_NUMBER] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line"><span class="type">int</span> foodY[FOOD_MAX_NUMBER] = &#123;<span class="number">0</span>&#125;;</span><br><span class="line"><span class="type">int</span> foodNumber = <span class="number">0</span>;</span><br></pre></td></tr></table></figure><h3 id="随机放食物"><a href="#随机放食物" class="headerlink" title="随机放食物"></a>随机放食物</h3><p>我们希望食物可以随机出现，所以我们传入一个概率参数。之后便是随机挑选一个空白的格子放食物。</p><p>这里采用的策略是每走一步之后就随机放食物。所以我们要在主函数 <code>switch</code> 后添加一句 <code>placeFood(FOOD_PROBABILITY);</code>，然后我们开始实现这个函数。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Randomly place food at a blank place according to probability*/</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">placeFood</span><span class="params">(<span class="type">double</span> food_prob)</span> &#123;</span><br><span class="line">    <span class="type">int</span> x, y;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Too much food! */</span></span><br><span class="line">    <span class="keyword">if</span> (foodNumber + <span class="number">1</span> &gt; FOOD_MAX_NUMBER) <span class="keyword">return</span>;</span><br><span class="line">    <span class="comment">/* Unlucky! */</span></span><br><span class="line">    <span class="keyword">if</span> ((<span class="type">double</span>)rand() / RAND_MAX &gt; food_prob) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        x = (<span class="type">int</span>)(GRID_WIDTH * ((<span class="type">double</span>)rand() / RAND_MAX));</span><br><span class="line">        y = (<span class="type">int</span>)(GRID_HEIGHT * ((<span class="type">double</span>)rand() / RAND_MAX));</span><br><span class="line">    &#125; <span class="keyword">while</span> (grid[y][x] != CHAR_GRID_BLANK);</span><br><span class="line"></span><br><span class="line">    foodNumber++;</span><br><span class="line">    foodX[foodNumber - <span class="number">1</span>] = x;</span><br><span class="line">    foodY[foodNumber - <span class="number">1</span>] = y;</span><br><span class="line">    drawFood();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里 <code>drawFood()</code> 的实现和 <code>drawSnake()</code> 类似，节省篇幅，我直接放代码了。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Draw food */</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">drawFood</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; foodNumber; ++i) &#123;</span><br><span class="line">        <span class="type">int</span> y = foodY[i];</span><br><span class="line">        <span class="type">int</span> x = foodX[i];</span><br><span class="line">        grid[y][x] = CHAR_GRID_FOOD;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="让蛇可以吃到食物"><a href="#让蛇可以吃到食物" class="headerlink" title="让蛇可以吃到食物"></a>让蛇可以吃到食物</h3><p>假如蛇头碰到了食物，那么我们就让蛇长一节，直接将食物的位置当成新的蛇头即可，这时就不必再一格格移动蛇身了。所以，我们先修改 <code>snakeMove</code> 函数，将原来移动蛇身的代码加一层 <code>if</code> 判断：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!eatFood(dx, dy)) &#123;</span><br><span class="line"><span class="comment">/* Original code for movement */</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后我们实现 <code>eatFood</code> 函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* See if our lovely snake has eaten a food */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">eatFood</span><span class="params">(<span class="type">int</span> dx, <span class="type">int</span> dy)</span> &#123;</span><br><span class="line">    <span class="comment">/* Get future head coordinates */</span></span><br><span class="line">    <span class="type">int</span> headX = snakeX[snakeLength - <span class="number">1</span>] + dx;</span><br><span class="line">    <span class="type">int</span> headY = snakeY[snakeLength - <span class="number">1</span>] + dy;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (grid[headY][headX] == CHAR_GRID_FOOD) &#123;</span><br><span class="line">        <span class="comment">/* Will it be too long? */</span></span><br><span class="line">        <span class="keyword">if</span> (snakeLength + <span class="number">1</span> &gt; SNAKE_MAX_LENGTH) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/* Eat it! */</span></span><br><span class="line">        snakeLength++;</span><br><span class="line">        snakeX[snakeLength - <span class="number">1</span>] = headX;</span><br><span class="line">        snakeY[snakeLength - <span class="number">1</span>] = headY;</span><br><span class="line"></span><br><span class="line">        foodNumber--;</span><br><span class="line">        <span class="type">int</span> i;</span><br><span class="line">        <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; foodNumber; ++i) &#123;</span><br><span class="line">            <span class="keyword">if</span> (headX == foodX[i] &amp;&amp; headY == foodY[i]) &#123;</span><br><span class="line">                foodX[i] = foodX[foodNumber];</span><br><span class="line">                foodY[i] = foodY[foodNumber];</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="判断游戏是否结束-1"><a href="#判断游戏是否结束-1" class="headerlink" title="判断游戏是否结束"></a>判断游戏是否结束</h3><p>为了简单方便起见，我们用一个全局变量 <code>gameStatus</code> 来存储当前的游戏状态，然后定义一些常量来代表游戏状态。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> STATUS_NORMAL 0</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> STATUS_GAME_OVER 1</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> gameStatus = STATUS_NORMAL;</span><br></pre></td></tr></table></figure><p>那么，什么时候会结束呢？无非三种情况：</p><ol><li>蛇跑出了边界</li><li>蛇撞上了障碍物</li><li>蛇撞到了自己</li></ol><p>那么，我们在蛇移动一步之前先试探会不会输就可以了。在 <code>snakeMove</code> 之前添加一段代码试探试探：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!predictMovable(dx, dy)) &#123; </span><br><span class="line">    gameStatus = STATUS_GAME_OVER;</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后我们按照刚才的分析，实现 <code>predictMovable</code> 函数：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Check if out lovely snake can make its move */</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">predictMovable</span><span class="params">(<span class="type">int</span> dx, <span class="type">int</span> dy)</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Get future head coordinates */</span></span><br><span class="line"><span class="type">int</span> headX = snakeX[snakeLength - <span class="number">1</span>] + dx;</span><br><span class="line"><span class="type">int</span> headY = snakeY[snakeLength - <span class="number">1</span>] + dy;</span><br><span class="line"><span class="type">int</span> i;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 1. Will it run out of bounds? */</span></span><br><span class="line"><span class="keyword">if</span> (headX &lt;= <span class="number">0</span> || headX &gt;= GRID_WIDTH - <span class="number">1</span> || \</span><br><span class="line">headY &lt;= <span class="number">0</span> || headY &gt;= GRID_HEIGHT - <span class="number">1</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 2. Will it hit the bricks? */</span></span><br><span class="line"><span class="keyword">if</span> (grid[headY][headX] == CHAR_GRID_BRICK) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 3. Will it bump into itself? */</span></span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; snakeLength; ++i) &#123;</span><br><span class="line"><span class="type">int</span> bodyX = snakeX[i];</span><br><span class="line"><span class="type">int</span> bodyY = snakeY[i];</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (headX == bodyX &amp;&amp; headY == bodyY) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Yes, it can move! */</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看上去 1 和 2 似乎是重复的，但如果我们不事先判断我们的蛇会不会超出边界，后面的操作就会发生数组越界错误。</p><p>最后修改 <code>gameOver</code> 函数即可：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">gameOver</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> gameStatus == STATUS_GAME_OVER;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="检验成果"><a href="#检验成果" class="headerlink" title="检验成果"></a>检验成果</h3><p><a href="/programming/snake-c-first/snake_eat.png" data-fancybox="gallery" data-caption=""><img src="/programming/snake-c-first/snake_eat.png"></a></p><p>再次编译运行，可以看到有食物随机地出现，蛇吃到食物也会长长了！</p><h2 id="第三版：不用回车的酷版本！"><a href="#第三版：不用回车的酷版本！" class="headerlink" title="第三版：不用回车的酷版本！"></a>第三版：不用回车的酷版本！</h2><p>如果每次输入完方向都要按回车，我们的蛇才能动的话，这样就太不酷了！怎样才能像真正的贪吃蛇那样蛇不停地跑，我们按下键能马上转向不用回车呢？这时候，我们就要用到一些比较高级的函数了，我们参考别人的代码，将我们自己的贪吃蛇代码融合到其中即可：<a href="http://bbs.chinaunix.net/thread-935410-1-1.html">Linux 下非阻塞地检测键盘输入的方法</a>，由于代码较长，就不粘贴在此了，可以参考文末的 GitHub 链接的代码。</p><p>再次编译运行，这下玩起来像是真正的贪吃蛇了！</p><h2 id="第四版：智能蛇"><a href="#第四版：智能蛇" class="headerlink" title="第四版：智能蛇"></a>第四版：智能蛇</h2><p>在这个版本里，我们编写一个简单的函数，让贪吃蛇能够自己找食物、躲障碍！既然是“贪吃”蛇，那我们就写一个简单的贪吃函数好了！</p><p>思路也很简单：先判断蛇头四个方向哪个可以走，然后找找看哪个方向离食物最近就好了，如果没有食物，那就保证不死就好了。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">manhattanDist</span><span class="params">(<span class="type">int</span> x1, <span class="type">int</span> y1, <span class="type">int</span> x2, <span class="type">int</span> y2)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">abs</span>(x1 - x2) + <span class="built_in">abs</span>(y1 - y2);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Caculate and make an intelligent move */</span></span><br><span class="line"><span class="type">char</span> <span class="title function_">nextMove</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="comment">/* Find a direction that is near one food */</span></span><br><span class="line"><span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line"><span class="type">int</span> headX = snakeX[snakeLength - <span class="number">1</span>];</span><br><span class="line"><span class="type">int</span> headY = snakeY[snakeLength - <span class="number">1</span>];</span><br><span class="line"><span class="type">int</span> distMin = <span class="number">9999</span>;</span><br><span class="line"><span class="type">char</span> direction = <span class="string">&#x27;D&#x27;</span>;</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; foodNumber; ++i) &#123;</span><br><span class="line"><span class="type">int</span> fX = foodX[i];</span><br><span class="line"><span class="type">int</span> fY = foodY[i];</span><br><span class="line"><span class="type">int</span> dist = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">if</span> (predictMovable(UP)) &#123;</span><br><span class="line">dist = manhattanDist(headX, headY - <span class="number">1</span>, fX, fY);</span><br><span class="line"><span class="keyword">if</span> (dist &lt; distMin) &#123;</span><br><span class="line">distMin = dist;</span><br><span class="line">direction = <span class="string">&#x27;W&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (predictMovable(DOWN)) &#123;</span><br><span class="line">dist = manhattanDist(headX, headY + <span class="number">1</span>, fX, fY);</span><br><span class="line"><span class="keyword">if</span> (dist &lt; distMin) &#123;</span><br><span class="line">distMin = dist;</span><br><span class="line">direction = <span class="string">&#x27;S&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (predictMovable(LEFT)) &#123;</span><br><span class="line">dist = manhattanDist(headX - <span class="number">1</span>, headY, fX, fY);</span><br><span class="line"><span class="keyword">if</span> (dist &lt; distMin) &#123;</span><br><span class="line">distMin = dist;</span><br><span class="line">direction = <span class="string">&#x27;A&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (predictMovable(RIGHT)) &#123;</span><br><span class="line">dist = manhattanDist(headX + <span class="number">1</span>, headY, fX, fY);</span><br><span class="line"><span class="keyword">if</span> (dist &lt; distMin) &#123;</span><br><span class="line">distMin = dist;</span><br><span class="line">direction = <span class="string">&#x27;D&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (distMin == <span class="number">9999</span>) &#123;</span><br><span class="line"><span class="keyword">if</span> (predictMovable(RIGHT)) direction = <span class="string">&#x27;D&#x27;</span>;</span><br><span class="line"><span class="keyword">if</span> (predictMovable(LEFT)) direction = <span class="string">&#x27;A&#x27;</span>;</span><br><span class="line"><span class="keyword">if</span> (predictMovable(UP)) direction = <span class="string">&#x27;W&#x27;</span>;</span><br><span class="line"><span class="keyword">if</span> (predictMovable(DOWN)) direction = <span class="string">&#x27;S&#x27;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> direction;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里为了简单起见，用了<strong>曼哈顿距离</strong>来表示距离食物的远近。</p><p>用这个函数替代原本获取终端输入的函数，再次编译运行，可以看到我们的蛇可以自己动起来，还能自动找食物了，是不是很有趣呢！不过等他长到一定长度的时候，很容易就把自己困死了……</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>当然，虽然我们实现了看似不少的功能，但是这个版本仍然非常粗糙，我们还可以增加不少的功能，比如：</p><ol><li>提供友好的菜单，供用户选择场地大小、速度等</li><li>提供计分功能</li><li>在场地内设置障碍，使游戏更有挑战性</li><li>改善智能蛇函数，让蛇蛇变得更智能</li></ol><p>等等……</p><p>可以看到，如果我们使用自顶向下的设计方法来编写程序，我们轻而易举就能写出很不错的程序，所以，想要再添加功能，也不是难事。</p><p>附上可以编译的源代码地址吧！（符合 ANSI C 标准）：<a href="https://github.com/howardlau1999/snake">https://github.com/howardlau1999/snake</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>色彩的表示</title>
      <link>https://blog.howardlau.me/science/color-representation.html</link>
      <description>
        <![CDATA[<p>在开始给你介绍颜色编码之前，我想让你回忆起小时候用颜料画画的场景。假如你在画一幅草地和蓝天的画，你用黄色画了金黄的太阳，又用蓝色渲染了一片浅蓝的天空。这时候，你想铺上绿油油的草地，结果你却发现，手头没有绿色颜料！于是你灵机一动，开始试着混合你手头有的颜料……终于，你发现了，]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/science/">科普</category>
      <pubDate>Fri, 27 Oct 2017 22:22:54 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在开始给你介绍颜色编码之前，我想让你回忆起小时候用颜料画画的场景。假如你在画一幅草地和蓝天的画，你用黄色画了金黄的太阳，又用蓝色渲染了一片浅蓝的天空。这时候，你想铺上绿油油的草地，结果你却发现，手头没有绿色颜料！于是你灵机一动，开始试着混合你手头有的颜料……终于，你发现了，蓝色和黄色混起来可以得到绿色！</p><p>通过直观的感受，我想每一个人都会明白颜料之间混合可以得到更多不同的颜色。那么颜色到底是什么呢？这一切还要从四百年前牛顿的时代说起。</p><h2 id="牛顿和分光实验"><a href="#牛顿和分光实验" class="headerlink" title="牛顿和分光实验"></a>牛顿和分光实验</h2><p>在科学技术还不发达的年代，人们普遍认为白色光是一种“单纯的光”，而彩色光则是一种“不知道发生了什么变化的光”。但富有科学精神的科学家牛顿（Issac Newton）决定对光进行更深入的研究，他写道：</p><p><a href="/science/color-representation/Newtons_prism_eperiment.jpg" data-fancybox="gallery" data-caption=""><img src="/science/color-representation/Newtons_prism_eperiment.jpg"></a></p><blockquote><p>1666 年初，我做了一个三角形的玻璃棱柱镜，利用它来研究光的颜色。为此，我把房间里弄成漆墨的，在窗户上做一个小孔，让适量的日光射进来。我又把棱镜放在光的入口处，使折射的光能够射到对面的墙上去，当我第一次看到由此而产生的鲜明强烈的光色时，使我感到极大的愉快。</p></blockquote><p>这就是著名的牛顿分光实验。通过这个实验，牛顿在墙上看到了一个彩色光斑，颜色的排列是红、橙、黄、绿、蓝、靛、紫。牛顿把这种现象叫做光的色散，把这个颜色光斑叫做光谱。而通过进一步的实验，牛顿确定了每一种单色的色光都是纯净的，三棱镜只是分解了光线，而不是了改变了它的性质。</p><h2 id="颜色的产生"><a href="#颜色的产生" class="headerlink" title="颜色的产生"></a>颜色的产生</h2><p>1801 年，英国学者汤玛士‧杨格首先研究人眼对颜色的感觉。他指出在可见光谱的位置排列上，只需选择三种彼此有相当差距的基本色光，按不同的比例组合，几乎可产生任何一种颜色。随后德国学者赫尔曼·冯·亥姆霍兹在 1856 年至 1867 年，继续深入对颜色的研究，确立了光的三原色理论。</p><p>如今我们通常选择的三原色是红色、绿色和蓝色。当红光、蓝光和绿光以同一亮度混合时，我们就得到了白色。虽然理论上我们可以选择其他三种颜色作为原色，但在电脑显示屏上，红绿蓝能最大地达到人的色彩空间。</p><h2 id="RGB-表示"><a href="#RGB-表示" class="headerlink" title="RGB 表示"></a>RGB 表示</h2><p>刚才我们说到，颜色可以由红、绿和蓝三种颜色来表示，因此，只要我们想办法控制红绿蓝的比例，我们就能显示出缤纷多彩的颜色。</p><p>平时买手机、显示屏的时候我们常常会了解一个参数——分辨率，就好像 1920 x 1080 这样的参数。那么它意味着什么呢？如果我们用放大镜（或者显微镜）仔细看你的显示屏，耐心数数，就会发现横向有 1920 个，纵向有 1080 个发光点。而小小的发光点，我们称之为像素，如果我们再更近距离地观察像素，就会发现，它其实是由红绿蓝三种颜色构成的。</p><p><a href="/science/color-representation/sony-dsc-29.jpg" data-fancybox="gallery" data-caption=""><img src="/science/color-representation/sony-dsc-29.jpg"></a></p><p>上面的图片就是你的显示屏里最微小的发光部件了。而在电脑里，RGB (Red Green Blue) 便是用来控制这三个小发光点的值，取值范围是 $0\sim 255$，代表了它的亮度，从不发光，到最亮。这样，我们就能表示 $256^3&#x3D;16777216$ 种不同的颜色了。</p><p>这里是一个小 Demo，试着拖动一下滑块看看颜色的变化吧！</p><div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%"><iframe src="/science/color-representation/rgb_demo.html" style="position:absolute;top:0;left:0;width:100%;height:100%" frameborder="0"></iframe></div><p>这里插两句，为什么是 $0\sim 255$ 呢？因为这正好是一个字节（也就是八个二进制位）所能表示的最大的数，便于计算机存储。那为什么不是 $2^{16}-1&#x3D;65535$ 甚至更大呢？因为科学家们发现，对于人眼而言，$16777216$ 种颜色已经足够丰富，甚至超出了人眼的辨别范围了，用更多的存储位数是浪费空间了。而如果我们用四位存储一种颜色，就只能表示 $16^3&#x3D;4096$ 种颜色，又显得太少了，所以最终人们确定用八位，来存储 RGB 的每一个值。</p><p>不过遗憾的是，每一个显示器厂家对红绿蓝色的定义不一定相同，也就造成了不同显示器之间的色差。就这样，带着一点小遗憾，人们解决了怎样显示不同颜色的事情。</p><h2 id="印刷与-CMYK"><a href="#印刷与-CMYK" class="headerlink" title="印刷与 CMYK"></a>印刷与 CMYK</h2><p>但是，当人们想把电脑的照片打印下来的时候，却发现，如果按照 RGB 值混合墨水，得到的和显示的完全不一样，本来是白色的，却变成了黑色！为什么呢？因为 RGB 是表示光的混合，光的混合是一种<strong>加法混合</strong>，而颜料的混合是<strong>减法混合</strong>，什么意思呢？这里放两张图片让你直观了解一下：</p><p><a href="/science/color-representation/AdditiveColorMixiing.svg_.png" data-fancybox="gallery" data-caption=""><img src="/science/color-representation/AdditiveColorMixiing.svg_.png"></a></p><p><a href="/science/color-representation/300px-SubtractiveColorMixing.png" data-fancybox="gallery" data-caption=""><img src="/science/color-representation/300px-SubtractiveColorMixing.png"></a></p><p>左边便是光的加法混合，而右边则是颜料的减法混合，差别还是很大的。而且我们发现，要是想得到黑色，那可要费掉不少墨水！于是人们规定了印刷用的 <strong>CMYK</strong> 表示，CMYK 分别是 Cyan（青色），Magenta（品红色），Yellow（黄色），blacK（黑色，不用 B 是因为 B 已经代表了蓝色）。</p><p>CMYK 则使用了 $0\sim100%$ 的表示方法来表示每一个分量，这样们就有$101^3+101&#x3D;1030402$可以印刷的颜色。我们可以发现，CMYK 表示的颜色远远少于 RGB 可以表示的颜色，更不用说实际上颜料的品质不可能 100% 完美，所以从显示器到印刷成品，往往有较大的色差。</p><h2 id="还有更多！"><a href="#还有更多！" class="headerlink" title="还有更多！"></a>还有更多！</h2><p>当然，随着研究和应用的发展，人们在 RGB 和 CMYK 以外还定义了更多的颜色表示方法，比如 HSL、HSV、YUV 等等等等，它们有着各自便利的一面。这篇文章仅仅是抛砖引玉，如果想要了解更多，欢迎大家自己搜索了解更多关于颜色编码的知识！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>博客建站全记录</title>
      <link>https://blog.howardlau.me/programming/setting-up-the-blog.html</link>
      <description>
        <![CDATA[<h2 id="域名和服务器"><a href="#域名和服务器" class="headerlink"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Wed, 18 Oct 2017 01:51:24 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="域名和服务器"><a href="#域名和服务器" class="headerlink" title="域名和服务器"></a>域名和服务器</h2><p>在挺早一段时间就有建个个人博客的想法了，但一直苦于没有时间金钱，一直拖到了最近才将这个想法付诸实践。</p><p>在 <a href="https://sg.godaddy.com/zh/">GoDaddy</a> 上花了一笔小钱买了现在这个域名之后，马上就将域名交给 DNSPod 解析，毕竟是国内专业的 DNS 服务商，速度有保证。</p><p>搞定了域名，开始物色服务器。一开始是在 <a href="https://vultr.com/">Vultr</a> 上买了月付 $5 的 LA 服务器，因为便宜而且不用备案，然而使用过一段时间后，实在觉得太慢了，对网站体验造成了很大的影响。</p><p>于是，我便转向阿里云，使用阿里云的学生计划，可以买到月付 ￥9.9 的服务器，比 Vultr 便宜多了！而且服务器在国内，访问速度得到了极大的改善。然而美中不足的是带宽只有 1 Mbps，而且服务器在国内意味着我要履行备案手续了。</p><h2 id="备案"><a href="#备案" class="headerlink" title="备案"></a>备案</h2><p>在中国，凡是要开办网站，就要到进行 ICP 备案，没有备案的域名一律不能接入国内服务器。而且许多涉及到网站的操作比如 CDN 等都需要备案。为了更好的访问速度，和更便利的操作，我只好选择备案。</p><p>其实备案并不麻烦，而且不需要任何费用。不过，现在备案必须通过服务器提供商来进行，比如我就需要通过阿里云来进行。一般来说，只要按照服务商备案要求上传资料就可以了，而且服务商会先审核你的资料，降低了被管局打回的可能性。而且，广东、重庆、辽宁、安徽、福建在阿里云备案时，不需要邮寄幕布拍照，可以直接通过 APP 刷脸核验身份，大大加速了流程。</p><p>当然，按照规定，在网站备案期间网站是不能访问的，但只要打开网站显示空白或者连接不上就行，我懒得改网站文件，直接停掉了 DNS 解析。提交资料之后，过了一天，阿里云就打电话过来核实资料，然后就帮我提交管局审核了。虽然网站说可能需要 15 到 20 个工作日，但其实只过了一周（包括周末），备案号就发下来了，速度让我有些小惊喜。据说上海审核更快，只需要几个小时。不过，户口在哪，就要给哪里管局审核。:)</p><h2 id="配置网站"><a href="#配置网站" class="headerlink" title="配置网站"></a>配置网站</h2><p>在等待备案号的过程中，我给服务器安装了 lnmp 环境和 Wordpress。安装 lnmp 用的是 lnmp.org 提供的一键安装脚本，比较省事。但编译安装过程花了一个小时，让我有点怀疑阿里云的性能。之后便是安装 Wordpress，只用到官网下载压缩包解压，配置下数据库然后开始安装就好了。其实也曾纠结过 Typecho 和 Wordpress，但最后还是觉得 Wordpress 可能功能更丰富一点，选择了 Wordpress。</p><h2 id="HTTPS-保护和-HTTP-2"><a href="#HTTPS-保护和-HTTP-2" class="headerlink" title="HTTPS 保护和 HTTP&#x2F;2"></a>HTTPS 保护和 HTTP&#x2F;2</h2><p>为了防止网站劫持，也顺应互联网潮流，我顺便到阿里云上弄了一个免费的 DV SSL 证书，虽然有效期只有一年，但对于个人用户来说足够了。阿里云上也有如何配置 nginx https 的教程，再次也不粘贴代码了。</p><p>有了 SSL，开启 http&#x2F;2 也是一件顺手的事，只需要将 <code>listen 443 ssl;</code> 改为 <code>listen 443 http2 ssl;</code>，重启 nginx，搞定！看着绿色的小锁，心里踏实了不少。 </p><h2 id="CDN-加速"><a href="#CDN-加速" class="headerlink" title="CDN 加速"></a>CDN 加速</h2><p>之前提到过阿里云的服务器只有 1 Mbps 的带宽，在尝试打开几个有图的博文之后，图片加载速度不容乐观，开始物色 CDN。然而主流 CDN 虽然有免费流量，但 https 却要收费。一番苦苦搜索之后，找到了 <a href="https://verycloud.cn/">VeryCloud</a>。不仅每个月有 50 GB 免费流量，而且还支持 https！还等什么呢？马上注册账号，申请 CDN（需要备案号），等待半个小时的审核，就能将网站接入 CDN 了！</p><p>配置完 DNS，再打开页面，虽然没有非常快，但也有所改观了！</p><p>当然，网站还有许多优化需要做，这些只是一些基本的建站流程。希望这个网站能长久下去，我也能坚持写点什么吧！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>IT 杂谈 | 扎克伯格和电影《社交网络》</title>
      <link>https://blog.howardlau.me/it-chat/mark-zuckerberg-and-movie-social-network.html</link>
      <description>
        <![CDATA[<h2 id="电影简介"><a href="#电影简介" class="headerlink" title="电影简介"></a>电影简介</h2><p>2003 年秋，哈佛大学。恃才放旷的天才学生马克·扎克伯格（Jesse Eisenberg]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/it-chat/">IT 杂谈</category>
      <pubDate>Sat, 14 Oct 2017 08:01:41 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="电影简介"><a href="#电影简介" class="headerlink" title="电影简介"></a>电影简介</h2><p>2003 年秋，哈佛大学。恃才放旷的天才学生马克·扎克伯格（Jesse Eisenberg 饰）被女友甩掉，愤怒之际，马克利用黑客手段入侵了学校的系统，盗取了校内所有漂亮女生的资料，并制作名为 Facemash 的网站供同学们对辣妹评分。他的举动引起了轰动，一度致令服务器几近崩溃，马克因此遭到校方的惩罚。正所谓因祸得福，马克的举动引起了温克莱沃斯兄弟的注意，他们邀请马克加入团队，共同建立一个社交网站。与此同时，马克也建立了日后名声大噪的 Facebook。 </p><p>经过一番努力，Facebook 的名气越来越大，马克的财富与日俱增。然而各种麻烦与是非接踵而来，昔日的好友也反目成仇……〔1〕</p><h2 id="传奇的-Mark"><a href="#传奇的-Mark" class="headerlink" title="传奇的 Mark"></a>传奇的 Mark</h2><h3 id="Mark-的童年"><a href="#Mark-的童年" class="headerlink" title="Mark 的童年"></a>Mark 的童年</h3><p>虽然电影是从 Mark 的大学时代开始故事的，我还是想先介绍一下 Mark 的童年背景。</p><p>Mark 出生于纽约一个犹太家庭，父亲是自有诊所的牙医，母亲是一名精神科医生，家境优越。</p><p>Mark 在中学时代便开始了编程。他的父亲教导他 BASIC 语言后聘请了一个软件开发者来当他的家教。这个老师称 Mark 为神童 (prodigy)，由此可见 Mark 的非凡智慧。</p><p>在高中时代，Mark 已经在家附近的 Mercy College 上课。Mark 非常喜欢编程，尤其是通讯工具和游戏。他曾开发过 ZuckNet，让父亲可以在家中和牙医诊所交流。他还曾开发过一个叫 Synapse Media Player 的软件，通过人工智慧学习用户听音乐的习惯并推荐新歌。</p><p>在他 18 岁时，微软不惜给出 98 万美元的年薪想招揽这位人才，但 Mark 仍选择在 2002 年 9 月进入哈佛大学学习。〔2〕接下来便是电影所述的 Mark 的大学生活。</p><h3 id="Mark-的大学生活"><a href="#Mark-的大学生活" class="headerlink" title="Mark 的大学生活"></a>Mark 的大学生活</h3><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_125219.392.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_125219.392.jpg"></a></p><p>在电影的一开始，影片展现了 Mark 与她的女友在聊天的场景。然而话不投机，女友气愤地决定与 Mark 分手。Mark 也感到十分沮丧，回到宿舍，喝了几瓶酒，开始写博客发泄自己的情绪，将这个前女友比喻成 “farm animal” （家畜）。</p><p>而这时舍友的一句话启发了 Mark。</p><blockquote><p>你知道有网站将人的照片和家畜放在一起让人们比较谁更性感（hot）吗？</p></blockquote><p>Mark 灵机一动，想：“为什么不把两个女孩的照片放在一起让人们比比谁更火辣呢？”。趁着酒劲，Mark 开始着手于 Facemash 的开发。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_125928.430.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_125928.430.jpg"></a></p><p>他利用高超的黑客手段，从各个宿舍楼下载了许多哈佛女生的照片。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_130428.743.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_130428.743.jpg"></a></p><p>他还从他的好友 Eduardo Saverin 中得到了评分公式。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_130407.652.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_130407.652.jpg"></a></p><p>短短几个小时，网站就开发完成了。他不过将链接发给了寥寥数人，Facemash 却在几个小时内便收获了 22,000 次点击，巨大的流量甚至将哈佛的网络搞瘫痪了。</p><p>Facemash 让 Mark 名声大噪，也让他陷入了风波当中，他因为将哈佛网络搞瘫痪了，而且还盗取学生照片并公之于互联网，被校方处以 6 个月留校察看的处分。更糟糕的是，他因为在博客上辱骂女友，而被女权组织声讨。</p><p>这时候，Winklevoss 兄弟找到了 Mark，想让他帮忙开发 HarvardConnection 网站。Mark 爽快地答应了。然而，Mark 并没有投身于这个网站的开发，而是废寝忘食地开发自己的作品 TheFacebook。宿舍里贴满了网站的设计稿，上课也不忘全神贯注地敲代码实现网站。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_133415.591.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_133415.591.jpg"></a></p><p>中途，Eduardo 迫不及待地向 Mark 介绍自己被凤凰俱乐部邀请的喜讯，而 Mark 却不以为意，向 Eduardo 介绍着 TheFacebook。他还清醒地认识到，人们上 Facemash 并不是想看女孩，而是想看自己认识的女孩。现实的人际关系是最重要的。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_134421.428.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_134421.428.jpg"></a></p><p>终于，开发了一个星期之后，他向好友 Eduardo 要了一笔钱，注册了域名，买了服务器，网站在深夜 2 点上线了。而此时 Mark 了却了一个心事，突然闭上了双眼打起了瞌睡。</p><h3 id="TheFacebook-的初期"><a href="#TheFacebook-的初期" class="headerlink" title="TheFacebook 的初期"></a>TheFacebook 的初期</h3><p>尽管只限于哈佛学生注册，TheFacebook 仍然在上线第一天便收获了 650 个用户。从校报得知消息的 Winklevoss 兄弟气急败坏，认为 Mark 窃取了他们的创意，开始联系律师，意图强制 Mark 关停网站。</p><p>然而一切都没能阻止 TheFacebook 的扩张，短短两个星期，”Facebook me” 已成为了哈佛学生的口头禅。</p><p>在一个 Bill Gates 的演讲上，Bill Gates 说到听众中或许就有下一个 “Bill Gates” 时，他看着的正是 Mark。</p><h3 id="TheFacebook-的扩张"><a href="#TheFacebook-的扩张" class="headerlink" title="TheFacebook 的扩张"></a>TheFacebook 的扩张</h3><p>很快，Mark 已经不满足于仅仅在哈佛推广网站，而是决定让更多的学校加入到 TheFacebook 中，他拉上自己的舍友，一个成为编程组组长，而另一个负责公关。</p><p>同在哈佛一样，TheFacebook 在其他学校也迅速取得了影响力。这时候，Sean Parker 从一个在斯坦福就读的女友中得知了 TheFacebook，并提出要和 Mark 见面。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_150249.308.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_150249.308.jpg"></a></p><p>很快，Sean 和 Mark 还有 Eduardo 在一家餐厅见面了，尽管大部分时间都是 Sean 在吹嘘自己的经历，但他说的几句话却让 Mark 受益匪浅。</p><blockquote><p>现在还不是你们停手的时候。十亿美元的价值才是你们的目标。还有，把 The 去掉吧，这样更简洁。</p></blockquote><p>就这样，Sean 抓住了 Mark 的心，TheFacebook 也正式变为 Facebook。</p><h3 id="Facebook-的发展"><a href="#Facebook-的发展" class="headerlink" title="Facebook 的发展"></a>Facebook 的发展</h3><p>Mark 之后招聘了更多的实习生，也听从 Sean 的建议搬到了加州。Sean 又建议 Mark 将 Facebook 扩张到更多大陆。很快，Facebook 也风靡英国。</p><p>Sean 之后帮助 Mark 重组 Facebook，吸引资本的加入，又从 Peter Thiel 中获得了 50 万美元的第一笔投资和一间新的办公室，Facebook 的发展走上了正轨，成为了世界上影响力最大的社交网络。</p><h3 id="成功背后的风波"><a href="#成功背后的风波" class="headerlink" title="成功背后的风波"></a>成功背后的风波</h3><p>Facebook 的成功并非一帆风顺，其背后也隐藏了一些危机。</p><h4 id="Winklevoss-兄弟的指控"><a href="#Winklevoss-兄弟的指控" class="headerlink" title="Winklevoss 兄弟的指控"></a>Winklevoss 兄弟的指控</h4><p>在 Facebook 发布后，Winklevoss 兄弟便一心认为 Mark 剽窃了他们的 HarvardConnection 创意，动用了父亲的家庭律师来起诉 Mark。而 Mark 不得不面对律师的指控，而且他最好的朋友 Eduardo 坐在了证人席上。</p><h4 id="Eduardo-的股权风波"><a href="#Eduardo-的股权风波" class="headerlink" title="Eduardo 的股权风波"></a>Eduardo 的股权风波</h4><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_154025.622.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_154025.622.jpg"></a></p><p>Eduardo 从一开始便不愿意 Sean 插手 Facebook。而在 Sean 重组 Facebook 后，Eduardo 重新签订了几份合同，虽然股权比例没有变化，但由于 Eduardo 没有仔细阅读合同文件，在一次 Mark 发行了 2,400 万新股之后，其他重要合伙人的股份比例没有稀释，而 Eduardo 的股份从 30% 降到了 0.7%，Sean 也在办公室和他发生了争执，认为他从不是 Facebook 的一份子。Eduardo 也不得不起诉 Mark，昔日的友谊遭受了重创。</p><p>最终，Mark 为了避免 Facebook 受到影响，选择用赔款的方式庭外和解了两单案子。</p><h3 id="放荡不羁的天才"><a href="#放荡不羁的天才" class="headerlink" title="放荡不羁的天才"></a>放荡不羁的天才</h3><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_131240.584.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_131240.584.jpg"></a></p><p>电影中，无论是面对学校委员会的质问，还是面对大律师，Mark 总是穿着邋遢，穿着一双阿迪达斯拖鞋。而且对这些大人物，他都表现出深深的不屑，没有认真听他们说话，也没有认真回答他们的问题。哪怕是个人的名片，也写了著名的 “I’m CEO, bitch”。</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155128.071.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155128.071.jpg"></a></p><p>或许是年少轻狂，或许是恃才放旷，影片塑造的，是一个放荡不羁的天才少年。</p><h2 id="关于电影"><a href="#关于电影" class="headerlink" title="关于电影"></a>关于电影</h2><p>电影的叙事非常紧凑。事实上，关于 Mark 创立 Facebook 的故事与他两件案子穿插着介绍，并不拖泥带水，让人直呼过瘾。而细节方面，屏幕上的代码都是真正的编程语言，而且恰到好处的展现了 Mark 的编程能力，让人感受到这位非凡的年轻人的天才。</p><p>影片的开头，Mark 的前女友骂了 Mark 一句：</p><blockquote><p>You’re an asshole. (你是个混蛋。)</p></blockquote><p>影片的结尾，律师所助理告诉 Mark :</p><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155359.395.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155359.395.jpg"></a></p><blockquote><p>You’re not an asshole. You’re just trying so hard to be. (你不是混蛋，你只是太努力变成混蛋而已。)</p></blockquote><p><a href="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155439.253.jpg" data-fancybox="gallery" data-caption=""><img src="/it-chat/mark-zuckerberg-and-movie-social-network/The.Social.Network.2010.720p.BluRay.x264-EbP.mkv_20171014_155439.253.jpg"></a></p><p>听了之后，Mark 若有所思，在深夜空旷的办公室里，打开前女友的 Facebook 页面，发送了好友请求，并不断地刷新页面。</p><p>首尾呼应让故事线更加完整，也更好了展现这位天才的孤僻与怪异。或许 Facebook 正弥补了他在现实中社交的缺憾吧。</p><h2 id="最后说几句"><a href="#最后说几句" class="headerlink" title="最后说几句"></a>最后说几句</h2><p>现在的 Mark，已经变得十分成熟，不再是一个随便的人，并且成为了世界上最年轻的亿万富翁，投身于慈善事业中。</p><p>在他身上，有许多的闪光点值得我们去学习，如果想了解更多，可以观看这部电影，或者到互联网搜搜关于他的资料吧！</p><h2 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h2><ol><li>摘自<a href="https://movie.douban.com/subject/3205624/">豆瓣电影</a></li><li>参考<a href="https://zh.wikipedia.org/zh-hans/%E9%A9%AC%E5%85%8B%C2%B7%E6%89%8E%E5%85%8B%E4%BC%AF%E6%A0%BC">马克·扎克伯格的维基百科</a></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>不会编程没关系，Construct 2 做游戏！</title>
      <link>https://blog.howardlau.me/programming/make-games-with-construct2-without-coding.html</link>
      <description>
        <![CDATA[<h2 id="Construct-2-简介"><a href="#Construct-2-简介" class="headerlink" title="Construct 2 简介"></a>Construct 2 简介</h2><p><a]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Mon, 09 Oct 2017 03:39:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<h2 id="Construct-2-简介"><a href="#Construct-2-简介" class="headerlink" title="Construct 2 简介"></a>Construct 2 简介</h2><p><a href="https://www.scirra.com/construct2">Construct 2</a> 是一款由 <a href="https://www.scirra.com/">Scirra</a> 出品的游戏制作软件。它不需要任何的编程基础，便能让用户轻松制作出好玩有趣的 2D HTML 5 游戏。</p><h2 id="初探-Construct-2"><a href="#初探-Construct-2" class="headerlink" title="初探 Construct 2"></a>初探 Construct 2</h2><p>在<a href="https://www.scirra.com/construct2/releases/new">官网下载 Construct 2</a> 后，安装运行，我们看到了这样的界面。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE7.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE7.png"></a></p><h3 id="新建项目"><a href="#新建项目" class="headerlink" title="新建项目"></a>新建项目</h3><p>选择 <strong>New Project</strong> 就可以新建项目。可以看到，Construct 2 为我们提供了丰富的游戏模板，无论是做 2D 横板过关游戏，还是做射击游戏，我们都可以直接利用 Construct 2 提供的模板快速制作。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE8.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE8.png"></a></p><p>但是我想制作的是一款模仿 Orbitum 的游戏 Orbital，所以我没有选用它的模板，而是直接新建了空项目。</p><h3 id="初探-Layout"><a href="#初探-Layout" class="headerlink" title="初探 Layout"></a>初探 Layout</h3><p>Layout，顾名思义，就是游戏的布局文件。它描述了我们的各种项目摆放的位置和各自的状态。</p><h4 id="为游戏设置背景"><a href="#为游戏设置背景" class="headerlink" title="为游戏设置背景"></a>为游戏设置背景</h4><p>面对空白的 Layout，我们首先需要为其设置背景。我们虽然可以设置整张图片作为背景，但考虑到 HTML 5 游戏的环境，选用 <strong>Tiled Background</strong>，利用小块图片平铺背景，可以减小游戏资源大小，使其加载更快。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170248.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170248.png"></a></p><p>双击 Layout，选择 Tiled Background 添加，然后鼠标变成了十字光标，在 Layout 上点击之后，便会自动弹出编辑图像的窗口。我们可以选择直接在窗口中绘制，也可以导入我们准备好的素材。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170634.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170634.png"></a></p><p>导入完成后，将这个 Object 放大到和 Layout 一样大小，我们就可以看到 Construct 2 已经为我们自动平铺好背景了。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170705.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009170705.png"></a></p><h4 id="添加元件"><a href="#添加元件" class="headerlink" title="添加元件"></a>添加元件</h4><p>当我们有了背景之后，接下来就是添加可以操作的物件了。和许多 2D 游戏引擎一样，会动的小元件称为 <strong>Sprite</strong> （精灵）。和添加背景类似，我们同样可以双击 Layout，选择 Sprite 添加，确定位置，导入图片。当然，也可以直接将外部的图片直接拖动到 Construct 2 中，软件会为我们自动创建 Sprite 对象，这样操作更方便。</p><h3 id="初探-Event-Sheet"><a href="#初探-Event-Sheet" class="headerlink" title="初探 Event Sheet"></a>初探 Event Sheet</h3><h4 id="用事件来驱动游戏"><a href="#用事件来驱动游戏" class="headerlink" title="用事件来驱动游戏"></a>用事件来驱动游戏</h4><p>如果说 Layout 是舞台和演员，那么 Event Sheet 就像是导演和剧本。在 Event Sheet 的语言中，语句都是类似于 Conditions -&gt; Actions 的格式。顾名思义，我们只需要设置好条件和动作，当 Construct 2 检测到条件满足的时候，便会自动执行设定好的动作。</p><p>下面是事件的一个示例。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009181245.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009181245.png"></a></p><p>Construct 2 中的条件与动作都非常的直观，只要有英语基础就能轻松操作，如果有什么疑问，也可以通过点击对话框下方的 Help 链接跳转到官网查看文档。所以关于具体事件我就不再赘述，还请读者多多尝试。</p><p>在 Construct 2 中，Event Sheet 是每一个 tick 都会判断一次的，而且有顺序之分，在设置 Event 的时候一定要注意好这两点，以免出现不恰当的逻辑。</p><p>需要注意的是，游戏中的输入检测也是需要通过添加 Keyboard 和 Mouse 等 Object 来检测的，千万不要忘记了哦！</p><h4 id="行为和动画"><a href="#行为和动画" class="headerlink" title="行为和动画"></a>行为和动画</h4><h5 id="行为"><a href="#行为" class="headerlink" title="行为"></a>行为</h5><p>对于一个 Sprite 而言，Construct 2 提供了一些便捷的操作，我们有时候可以不再需要亲自编写操作他们一些动作的 Event。只需要右键 Object，选择 Behaviors…，就可以为对象添加一些行为。比如这里可以添加一个 Bound to layout，使对象不会超出 Layout 的范围。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009185106.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009185106.png"></a></p><h5 id="动画"><a href="#动画" class="headerlink" title="动画"></a>动画</h5><p>有时候做动作游戏时，我们希望给角色添加一些走路或者是攻击的动画。Construct 2 也提供了非常方便的操作，同样右键 Object，选择 Edit animations，就可以在熟悉的编辑器中编辑动画帧了，在左边的动画属性栏也可以设置一些属性。一个 Sprite  可以有很多个动画，具体的播放和暂停可以在 Event Sheet 中操作。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009185554.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009185554.png"></a></p><h4 id="对象和实例"><a href="#对象和实例" class="headerlink" title="对象和实例"></a>对象和实例</h4><p>可以看到，我们添加的 Sprite 和 Tiled Background 等等都是属于 <strong>Object</strong> （对象）。然而，有时候屏幕上一个 Object 可能需要出现多次，那么我们怎么单独地操作某一个特定的对象呢，比如为他们设置各自的位置等？</p><p>首先需要说明的是，每一个 Object 出现在屏幕上时，就成为了一个 <strong>Instance</strong> （实例）。就像一个班里虽然有许许多多人，但他们都有不同的名字、身高等。这里的“人”就相当于 Object，而每一个特定的人就是这个 Object 的 Instance。每一个 Instance 都有自己的 Position 等等属性，假如我们需要存储除了系统给定的属性之外的数据，可以通过右键 Object，然后选择 Instance Variables…，就可以添加自己需要存储的变量了。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009184114.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009184114.png"></a></p><p>假如一个事件的判断对象是 Object 时，那么在 Actions 中涉及到这个 Object 的操作默认就是针对满足条件的 Instance 而言的；假如需要在其他地方访问 Instance，则需要用到 For Each 语句，这里不再赘述，可以查看文档。</p><h2 id="测试游戏"><a href="#测试游戏" class="headerlink" title="测试游戏"></a>测试游戏</h2><p>在做游戏的途中，我们可能想随时看一下游戏效果，这时候，我们可以点击上方菜单的 Run Layout 按钮，就会自动打开浏览器，让我们的游戏运行起来。</p><p>如果需要更高级的功能，我们也可以点击 Debug Layout，可以观察许多变量的变化情况。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009190601.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009190601.png"></a></p><h2 id="发布游戏"><a href="#发布游戏" class="headerlink" title="发布游戏"></a>发布游戏</h2><p>当我们做好了游戏之后，点击 Export Project，就可以发布了。虽然 Construct 2 为我们提供了丰富的导出选项，但不少是收费版本的功能，这里我们选 HTML 5 website，然后选择一个空文件夹用默认选项导出即可。</p><p><a href="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009190754.png" data-fancybox="gallery" data-caption=""><img src="/programming/make-games-with-construct2-without-coding/%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20171009190754.png"></a></p><p>导出游戏后，游戏是不能直接在本地运行的，需要上传到一个网页服务器才能被大家访问并运行，这里就不再阐述怎样上传了。</p><p>我使用的是七牛云存储，欢迎大家来试玩我的作品 <a href="/wp-content/uploads/orbitalh5v1/">Orbital</a>！</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><h3 id="Construct-2-的优势"><a href="#Construct-2-的优势" class="headerlink" title="Construct 2 的优势"></a>Construct 2 的优势</h3><ul><li>不需要编程基础</li><li>操作简单、直观</li><li>提供了大量模板，可以快速制作游戏</li><li>提供了物理引擎等复杂的效果</li></ul><h3 id="Construct-2-的缺点"><a href="#Construct-2-的缺点" class="headerlink" title="Construct 2 的缺点"></a>Construct 2 的缺点</h3><ul><li>收费软件，免费版有许多限制</li><li>自由度一般，不适宜制作太复杂的游戏</li><li>英文界面，需要一定的英文基础</li></ul><p>这篇文章只介绍了 Construct 2 的冰山一角，更多的功能还等待读者自己去发现！</p><p>大家可以来尝试我的作品 <a href="/wp-content/uploads/orbitalh5v1/">Orbital</a> 噢！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>编程分享 | 大整数的实现</title>
      <link>https://blog.howardlau.me/programming/big-integer.html</link>
      <description>
        <![CDATA[<p>在处理数据过程中，我们可能会遇到 <code>int</code> 类型或者 <code>long long</code> 类型都无法存储的大整数。有的语言如 <code>python</code> 和 <code>java</code>]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/programming/">编程分享</category>
      <pubDate>Sat, 23 Sep 2017 20:41:46 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>在处理数据过程中，我们可能会遇到 <code>int</code> 类型或者 <code>long long</code> 类型都无法存储的大整数。有的语言如 <code>python</code> 和 <code>java</code> 已经有内建的大整数支持。然而，在没有内建大整数支持的语言中，我们需要自己实现大整数的存储以及运算。</p><p>实现大整数有十分多细节需要考虑，十分考验编程者的细心程度，因此本文篇幅也会比较长，如有错漏请指出。</p><p>本文介绍的是比较朴素简单的一种实现。用以完成<a href="https://vijos.org/p/1047">这道最小公倍数的题目</a>。</p><h2 id="大整数的基础"><a href="#大整数的基础" class="headerlink" title="大整数的基础"></a>大整数的基础</h2><h3 id="大整数的存储"><a href="#大整数的存储" class="headerlink" title="大整数的存储"></a>大整数的存储</h3><p>我们很自然可以想到，存储整数，需要存储它的正负、每一位的数字，以及这个数的长度。为此，我们定义 <code>BigInt</code> 结构如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span>&#123;</span></span><br><span class="line"><span class="type">bool</span> negative; <span class="comment">// false 为正，true 为负</span></span><br><span class="line"><span class="type">int</span> len;</span><br><span class="line"><span class="type">int</span> nums[<span class="number">10000</span>];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>为了方便起见，我们直接规定最大存储位数为 10000 位，对于这道题目而言已经足够，日后可以改写为动态分配长度以满足不同需求。</p><h3 id="大整数的读入"><a href="#大整数的读入" class="headerlink" title="大整数的读入"></a>大整数的读入</h3><p>为了方便理解，我们规定 <code>nums[0]</code> 为整数的个位，<code>nums[1]</code> 为整数的百位，依此类推。</p><p>然而，在将数字作为字符串 <code>s</code> 读取时，<code>s[0]</code> 存储的是数字的最高位。所以，在读取后，我们需要将其进行倒序处理，同时，将数字长度也存储起来。函数如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">read</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">a</span>;</span></span><br><span class="line"><span class="built_in">memset</span>(&amp;a, <span class="number">0</span>, <span class="keyword">sizeof</span>(a));</span><br><span class="line"></span><br><span class="line"><span class="type">char</span> input[<span class="number">10001</span>];</span><br><span class="line"><span class="built_in">scanf</span>(<span class="string">&quot;%s&quot;</span>, input);</span><br><span class="line"><span class="comment">// 符号的处理</span></span><br><span class="line"><span class="type">int</span> end = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">if</span> (input[<span class="number">0</span>] == <span class="string">&#x27;-&#x27;</span>) &#123;</span><br><span class="line">a.negative = <span class="literal">true</span>;</span><br><span class="line">end = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (input[<span class="number">0</span>] == <span class="string">&#x27;+&#x27;</span>) &#123;</span><br><span class="line">end = <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="built_in">strlen</span>(input) - <span class="number">1</span>; i &gt;= end ; i--)&#123;</span><br><span class="line"><span class="keyword">if</span> (input[i] &gt;= <span class="number">48</span> &amp;&amp; input[i] &lt;= <span class="number">57</span>) <span class="comment">// 过滤非数字</span></span><br><span class="line">a.nums[a.len++] = input[i] - <span class="number">48</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> a;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="大整数的输出"><a href="#大整数的输出" class="headerlink" title="大整数的输出"></a>大整数的输出</h3><p>只需要输出负号（如有），然后从高位到低位输出数字即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">output</span><span class="params">(<span class="keyword">struct</span> BigInt a)</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (a.negative) <span class="built_in">printf</span>(<span class="string">&quot;-&quot;</span>);</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = a.len - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--)&#123;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;%d&quot;</span>, a.nums[i]);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="大整数的四则运算"><a href="#大整数的四则运算" class="headerlink" title="大整数的四则运算"></a>大整数的四则运算</h2><h3 id="大整数的加法"><a href="#大整数的加法" class="headerlink" title="大整数的加法"></a>大整数的加法</h3><p>当我们实现读入并存储了大整数之后，就要开始实现加法了。</p><p>实现加法的过程实际上就是我们手动列竖式计算的过程：</p><p>$$\begin{array}{ccc} &amp; a_n &amp; a_{n-1} &amp; \cdots &amp; a_1 \\ + &amp; b_n &amp; b_{n-1} &amp; \cdots &amp; b_1 \\\hline&#x3D;&amp; a_n + b_n &amp; a_{n-1} + b_{n-1} &amp; \cdots &amp; a_1 + b_1 \end{array}$$</p><p>上面的公式并没有考虑进位，而进位的处理并不复杂，进位的处理：</p><p>$$c_{i+1} &#x3D; \lfloor \frac{c_{i}} {10} \rfloor \tag{*} \\c_i &#x3D; c_i\ mod\ 10$$</p><p>由于都是整形，在 C 语言中，(*)可以直接写除法而不必使用 <code>floor</code> 函数。</p><p>对于加法处理后的数字长度，最长不会超过 <code>max(a.len, b.len) + 1</code> ，我们可以默认它就是这个数，然后在做完加法之后，从高位遍历到低位，找到第一个非零数，来确定真正的长度。</p><p>对于负数的处理，可以分为以下几种情况：</p><ol><li>两个数一正一负</li><li>两个数同为负数</li></ol><p>对于第一种情况，我们将其转换为两个正数的减法；对于第二种情况，我们直接做加法，然后将结果标记为负数即可。</p><p>参考代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">add</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">c</span>;</span></span><br><span class="line"><span class="built_in">memset</span>(&amp;c, <span class="number">0</span>, <span class="keyword">sizeof</span>(c));</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (a.negative ^ b.negative) <span class="comment">// a, b 异号</span></span><br><span class="line"><span class="keyword">if</span> (a.negative) &#123;</span><br><span class="line">a.negative = <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">return</span> sub(b, a);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span> &#123;</span><br><span class="line">b.negative = <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">return</span> sub(a, b);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len = max(a.len, b.len) + <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; c.len; i++)&#123;</span><br><span class="line">c.nums[i] = a.nums[i] + b.nums[i];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; c.len; i++)&#123;</span><br><span class="line">c.nums[i+<span class="number">1</span>] = c.nums[i] / <span class="number">10</span> + c.nums[i+<span class="number">1</span>];</span><br><span class="line">c.nums[i] = c.nums[i] % <span class="number">10</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (c.nums[c.len] == <span class="number">0</span> &amp;&amp; c.len &gt; <span class="number">0</span>)&#123;</span><br><span class="line">c.len--;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len++;</span><br><span class="line">c.negative = a.negative &amp; b.negative;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="大整数的减法"><a href="#大整数的减法" class="headerlink" title="大整数的减法"></a>大整数的减法</h3><p>在加法的实现中，我们调用了尚未实现的 <code>sub</code> 减法函数，下面我们就来实现 <code>a - b</code>。</p><p>位数方面，结果不会超过 <code>max(a.len, b.len)</code>，其余思想和加法类似。</p><p>计算的过程同样基于竖式计算：</p><p>$$\begin{array}{ccc} &amp; a_n &amp; a_{n-1} &amp; \cdots &amp; a_1 \\ - &amp; b_n &amp; b_{n-1} &amp; \cdots &amp; b_1 \\\hline&#x3D;&amp; a_n - b_n &amp; a_{n-1} - b_{n-1} &amp; \cdots &amp; a_1 - b_1 \end{array}$$</p><p>不同的是，我们需要处理借位的情况。这时，我们从低位向高位处理，假如发现某位小于零，则向高位“借” 10。</p><p>$$if\ c_i &lt; 0 \\c_{i+1} &#x3D; c_{i+1} - 1 \\c_{i} &#x3D; c_{i} + 10$$</p><p>同样，关于负数的处理，有下面几种情况：</p><ol><li>a 正 b 负</li><li>a 负 b 正</li><li>a 负 b 负</li></ol><p>对于第一种以及第二种情况，我们可以将 b 的符号置反，然后调用加法实现；对于第三种情况，将其作为 <code>(-b) - (-a)</code> 处理即可。</p><p>通常为了编写程序方便，我们用绝对值较大的数减去绝对值较小的数，为此，我们编写一个比较两个数绝对值大小的函数。</p><h4 id="大整数的绝对值比较"><a href="#大整数的绝对值比较" class="headerlink" title="大整数的绝对值比较"></a>大整数的绝对值比较</h4><p>比较绝对值可以分为几个步骤：</p><ol><li>比较长度，长度大的更大；</li><li>若长度相等，从高位依次比较到低位，若有不一致，即为比较结果；</li><li>若每一位都相等，则认为两个数绝对值相等。</li></ol><p>参考代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> GREATER 1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> LESS -1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> EQUAL 0</span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">cmp</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="keyword">if</span>(a.len &gt; b.len) <span class="keyword">return</span> GREATER;</span><br><span class="line"><span class="keyword">if</span>(a.len &lt; b.len) <span class="keyword">return</span> LESS;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(a.len == b.len)&#123;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = a.len - <span class="number">1</span>; i &gt;= <span class="number">0</span>; i--)&#123;</span><br><span class="line"><span class="keyword">if</span>(a.nums[i] &gt; b.nums[i]) <span class="keyword">return</span> GREATER;</span><br><span class="line"><span class="keyword">if</span>(a.nums[i] &lt; b.nums[i]) <span class="keyword">return</span> LESS;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> EQUAL;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> EQUAL;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有了这个比较函数，我们就可以实现减法函数了。</p><p>参考代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">sub</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">c</span>;</span></span><br><span class="line"><span class="built_in">memset</span>(&amp;c, <span class="number">0</span>, <span class="keyword">sizeof</span>(c));</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (a.negative ^ b.negative) &#123;</span><br><span class="line">b.negative = !b.negative;</span><br><span class="line"><span class="keyword">return</span> add(a, b);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (a.negative &amp; b.negative) &#123;</span><br><span class="line">a.negative = b.negative = <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">return</span> sub(b, a);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(cmp(a, b) == LESS)&#123;</span><br><span class="line">c = a;</span><br><span class="line">a = b;</span><br><span class="line">b = c;</span><br><span class="line"><span class="built_in">memset</span>(&amp;c, <span class="number">0</span>, <span class="keyword">sizeof</span>(c));</span><br><span class="line">c.negative = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len = max(a.len, b.len);</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; c.len; i++)&#123;</span><br><span class="line">c.nums[i] = a.nums[i] - b.nums[i];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; c.len; i++)&#123;</span><br><span class="line"><span class="keyword">if</span>(c.nums[i] &lt; <span class="number">0</span>)&#123;</span><br><span class="line">c.nums[i+<span class="number">1</span>]--;</span><br><span class="line">c.nums[i] += <span class="number">10</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (c.nums[c.len] == <span class="number">0</span> &amp;&amp; c.len &gt; <span class="number">0</span>)&#123;</span><br><span class="line">c.len--;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len++;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> c;</span><br><span class="line">&#125; </span><br></pre></td></tr></table></figure><h3 id="大整数的乘法"><a href="#大整数的乘法" class="headerlink" title="大整数的乘法"></a>大整数的乘法</h3><p>我们知道，乘法其实是做很多次加法。但是显然，单纯的一遍遍模拟加法是不现实的，我们同样使用竖式乘法来实现计算过程，只是相比起加法更复杂了一些。计算过程请阅读代码理解。</p><p>位数方面，不会超过 <code>a.len + b.len</code> 。实际位数的确定和加法一样，进位的处理也和加法一样。</p><p>而符号，则是两个数符号异或的结果，因为负负得正嘛。</p><p>参考代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">multiply</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">c</span>;</span></span><br><span class="line"><span class="built_in">memset</span>(&amp;c, <span class="number">0</span>, <span class="keyword">sizeof</span>(c));</span><br><span class="line"></span><br><span class="line">c.len = a.len + b.len;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; a.len; i++)&#123;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> j = <span class="number">0</span>; j &lt; b.len; j++)&#123;</span><br><span class="line">c.nums[i+j] = c.nums[i+j] + a.nums[i] * b.nums[j];</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>; i &lt; c.len; i++)&#123;</span><br><span class="line">c.nums[i+<span class="number">1</span>] = c.nums[i] / <span class="number">10</span> + c.nums[i+<span class="number">1</span>];</span><br><span class="line">c.nums[i] = c.nums[i] % <span class="number">10</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (c.nums[c.len] == <span class="number">0</span> &amp;&amp; c.len &gt; <span class="number">0</span>)&#123;</span><br><span class="line">c.len--;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len++;</span><br><span class="line"></span><br><span class="line">c.negative = a.negative ^ b.negative;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="大整数的除法"><a href="#大整数的除法" class="headerlink" title="大整数的除法"></a>大整数的除法</h3><p>这里的除法和 C 语言的整数除法一样，我们默认结果是向下取整的。同样我们可以想到，除法可以用减法来模拟，但这同样太过耗时。而我的一种朴素的想法是逐位试商，然后将试出来的商和被除数比较。而商的位数不会超过被除数的位数，所以我们一开始便默认商的位数是被除数的位数，然后开始试商过程。这里需要用到之前的 <code>cmp</code> 函数来比较结果。</p><p>符号的处理和乘法一样。</p><p>虽然个人感觉效率并不高，但比减法还是要快上不少。如果有更好的解决方法，欢迎在评论中提出。</p><p>参考代码如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">divide</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">c</span>;</span> </span><br><span class="line"><span class="built_in">memset</span>(&amp;c, <span class="number">0</span>, <span class="keyword">sizeof</span>(c));</span><br><span class="line"></span><br><span class="line">c.len = a.len;</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> i = a.len; i &gt; <span class="number">0</span>; i--)&#123;</span><br><span class="line"><span class="keyword">while</span>(<span class="number">1</span>)&#123;</span><br><span class="line">c.nums[i - <span class="number">1</span>]++;</span><br><span class="line"><span class="keyword">if</span>(cmp(multiply(c, b), a) == GREATER)&#123;</span><br><span class="line">c.nums[i - <span class="number">1</span>]--;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125; </span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span> (c.nums[c.len] == <span class="number">0</span> &amp;&amp; c.len &gt; <span class="number">0</span>)&#123;</span><br><span class="line">c.len--;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">c.len++;</span><br><span class="line"></span><br><span class="line">c.negative = a.negative ^ b.negative;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> c;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="大整数的其他运算"><a href="#大整数的其他运算" class="headerlink" title="大整数的其他运算"></a>大整数的其他运算</h2><h3 id="大整数的取余"><a href="#大整数的取余" class="headerlink" title="大整数的取余"></a>大整数的取余</h3><p>我们知道，<code>lcm(a, b) = a * b / gcd(a, b)</code>。这里，最大公约数函数我们使用<strong>辗转相除法</strong>来实现。</p><p>一般整数的辗转相除法实现是这样子的：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">gcd</span><span class="params">(<span class="type">int</span> a, <span class="type">int</span> b)</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (a &lt; b) &#123;</span><br><span class="line"><span class="type">int</span> c = a;</span><br><span class="line">a = b;</span><br><span class="line">b = c;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span>(r = a % b) &#123;</span><br><span class="line">a = b;</span><br><span class="line">b = r;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> b;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，我们需要用到取余运算。在除法的基础上，取余十分容易，这里直接给出代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">mod</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="keyword">return</span> sub(a, multiply(divide(a, b), b));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="大整数的最大公约数"><a href="#大整数的最大公约数" class="headerlink" title="大整数的最大公约数"></a>大整数的最大公约数</h3><p>至此，我们可以按照上述辗转相除法的思想，编写出大整数的 <code>gcd</code> 函数了。同样，这里直接给出代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">gcd</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span>&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">zero</span>;</span></span><br><span class="line"><span class="built_in">memset</span>(&amp;zero, <span class="number">0</span>, <span class="keyword">sizeof</span>(zero));</span><br><span class="line">zero.len = <span class="number">1</span>;</span><br><span class="line">zero.nums[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(cmp(a, b) == LESS)&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">c</span> =</span> a;</span><br><span class="line">a = b;</span><br><span class="line">b = c;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">BigInt</span> <span class="title">r</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">while</span>(<span class="number">1</span>) &#123;</span><br><span class="line">r = mod(a, b);</span><br><span class="line"><span class="keyword">if</span>(cmp(r, zero) == EQUAL) <span class="keyword">break</span>;</span><br><span class="line">a = b;</span><br><span class="line">b = r;</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> b;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="大整数的最小公倍数"><a href="#大整数的最小公倍数" class="headerlink" title="大整数的最小公倍数"></a>大整数的最小公倍数</h3><p>有了前面几个函数的基础，求最小公倍数就十分容易了：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> BigInt <span class="title function_">lcm</span><span class="params">(<span class="keyword">struct</span> BigInt a, <span class="keyword">struct</span> BigInt b)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> divide(multiply(a, b), gcd(a, b));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>利用上面实现的函数，我们基本可以满足常见的大整数运算，也可以完成<a href="https://vijos.org/p/1047">这道最小公倍数的题目</a>了。</p><p>当然，以上的代码还有很多优化的空间，例如<strong>万进制优化</strong>，也有比如<strong>乘方</strong>等运算没有实现，但是，当我们完成了四则运算的实现之后，想必对大整数编程也有一定的理解，后续的优化和改进也变得不那么难了。</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>游戏推荐 | 人力资源机器</title>
      <link>https://blog.howardlau.me/game/human-resource-machine.html</link>
      <description>
        <![CDATA[<p>今天给大家推荐一款我很喜欢的益智手游：《人力资源机器》(Human Resource Machine)。在游玩的过程中，不知不觉就能学习到一些程序的基本知识，也将一些算法的思想传递给玩家。</p>
<h2 id="游戏介绍"><a href="#游戏介绍"]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/game/">游戏推荐</category>
      <pubDate>Wed, 20 Sep 2017 06:05:53 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>今天给大家推荐一款我很喜欢的益智手游：《人力资源机器》(Human Resource Machine)。在游玩的过程中，不知不觉就能学习到一些程序的基本知识，也将一些算法的思想传递给玩家。</p><h2 id="游戏介绍"><a href="#游戏介绍" class="headerlink" title="游戏介绍"></a>游戏介绍</h2><p>在游戏中，你是一个苦逼的搬砖工，负责将一些数字和字母搬来搬去，偶尔还得做点加减法。而你的任务便是给自己写一套程序，完成你的 Boss 给你布置的各种任务。对了，其实这就是一款编程游戏，但它并不需要你有任何的编程知识，游戏提供的指令虽然类似于汇编语言，但都十分简单易懂且种类不多，编写过程也十分直观。而且，游戏会随着关卡的进行而逐渐介绍新的指令的用法，所以只需要你有一点的数学知识和逻辑思维，就能轻松上手。</p><h2 id="初入茅庐"><a href="#初入茅庐" class="headerlink" title="初入茅庐"></a>初入茅庐</h2><p><a href="/game/human-resource-machine/IMG_6604.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6604.png"></a></p><center>第一关</center><p>这是游戏的第一关，介绍了游戏的基本玩法，也引入了程序的顺序结构。可能你会想：这不停地复制粘贴 inbox 和 outbox，不是浪费时间吗？</p><h2 id="更多的结构"><a href="#更多的结构" class="headerlink" title="更多的结构"></a>更多的结构</h2><p><a href="/game/human-resource-machine/IMG_6605.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6605.png"></a></p><center>循环结构的引入</center><p>这时，通过引入的新的指令—— jump，我们可以写出具有循环结构的程序，甚至是一些更复杂的控制流程。</p><h2 id="存储数据和运算"><a href="#存储数据和运算" class="headerlink" title="存储数据和运算"></a>存储数据和运算</h2><p><a href="/game/human-resource-machine/IMG_6617.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6617.png"></a></p><center>看到绿色的地砖了吗？</center><p>这时候，我们终于能够存储点什么了，这个类似于 CPU 中的寄存器。你可能还注意到多了一条 add 指令，没错，寄存器中的数据，是可以进行运算的。这样我们的小员工就更像一台电脑了。</p><h2 id="条件判断——程序逻辑的灵魂"><a href="#条件判断——程序逻辑的灵魂" class="headerlink" title="条件判断——程序逻辑的灵魂"></a>条件判断——程序逻辑的灵魂</h2><p><a href="/game/human-resource-machine/IMG_6607.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6607.png"></a></p><center>留意那个新的指令</center><p>有了 if 指令，我们终于可以进行判断了，程序的最后一个基础结构——条件结构，也被引入了。到这里，真正的挑战才刚刚开始，到后期，程序的指令将会增加到 11 条之多，但始终离不开寄存器以及三大结构的概念，当然，我还少了一个概念没有介绍，就留给大家自己在游戏中探索啦~</p><h2 id="噩梦——Debug"><a href="#噩梦——Debug" class="headerlink" title="噩梦——Debug"></a>噩梦——Debug</h2><p><a href="/game/human-resource-machine/IMG_6614.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6614.png"></a></p><center>程序出错了</center><p>而写程序，少不了的是调试，到后期任务变得复杂时，我们很难一次将程序写对（大神除外~），当运行出现错误时，我们可以重置或者进行单步调试来修正错误。</p><p>最后，你的程序并不只是需要处理画面上的一组数据这么简单，有时看似正确的程序，在特殊的数据输入下，也会发生错误。不过游戏会很贴心的给出这种特殊的数据，只要多思考思考便能发现问题所在。</p><h2 id="挑战——速度与体积的优化"><a href="#挑战——速度与体积的优化" class="headerlink" title="挑战——速度与体积的优化"></a>挑战——速度与体积的优化</h2><p><a href="/game/human-resource-machine/IMG_6612.png" data-fancybox="gallery" data-caption=""><img src="/game/human-resource-machine/IMG_6612.png"></a></p><center>你能做到吗？</center><p>程序光是需要正确性还不够，我们还需要运行得足够快，有时需要它足够短。</p><p>如何优化速度，需要一定的算法知识；而缩小体积，需要灵光一闪。这便是游戏的挑战性所在，我还没能完成游戏所给的所有目标，不知道你又能不能用自己的聪明才智完成呢？</p><h2 id="跃跃欲试"><a href="#跃跃欲试" class="headerlink" title="跃跃欲试"></a>跃跃欲试</h2><p>这个游戏便先大致介绍到这里，游戏同时也有一个耐人寻味的故事线在其中，想要了解我没有说到的秘密，又或者想活跃一下太久没动的思维，不妨试试这款游戏吧！</p><p>下载链接（请多多支持正版！）：</p><p><a href="https://appsto.re/cn/Evj67.i">App Store 下载</a>  <a href="http://store.steampowered.com/app/375820/">Steam 下载</a>  <a href="http://www.wandoujia.com/apps/com.tomorrowcorporation.humanresourcemachine">豌豆荚下载</a></p><p>祝大家玩得开心~！</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hello World!</title>
      <link>https://blog.howardlau.me/uncategorized/hello-world.html</link>
      <description>
        <![CDATA[<p><a href="/uncategorized/hello-world/IMG_7402.jpg" data-fancybox="gallery" data-caption=""><img]]>
      </description>
      <author>howardlau</author>
      <category domain="https://blog.howardlau.me/category/uncategorized/">Uncategorized</category>
      <pubDate>Sun, 30 Jul 2017 08:22:48 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p><a href="/uncategorized/hello-world/IMG_7402.jpg" data-fancybox="gallery" data-caption=""><img src="/uncategorized/hello-world/IMG_7402.jpg"></a></p><p style="text-align: center;">终于弄了一个自己的域名和博客，以后或许有什么想法会记录在这里。</p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
