<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Project RC</title>
    <link>https://stdrc.cc/feed.xml</link>
    <description>一种标准阿西的设计与实现。</description>
    <atom:link href="https://stdrc.cc/feed.xml" rel="self"/>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <lastBuildDate>Sun, 26 Apr 2026 17:19:53 +0000</lastBuildDate>
    <item>
      <title>微软校招实习提前批面试经验</title>
      <link>https://stdrc.cc/post/2022/01/30/microsoft-interview/</link>
      <description><![CDATA[<p>部门选的是 C+AI，工作地点选的是上海。</p>
<h2>1.17 一面</h2>
<ul>
<li>中文自我介绍<ul>
<li>说得太长了，感觉面试官根本没听</li>
</ul>
</li>
<li>算法题：求一个未排序的数组的逆序对数量<ul>
<li>想了 40 分钟，期间让面试官提示了 3、4 次，愣是没想起来</li>
<li>对归并排序、快排这类基础算法掌握不牢，也没复习到</li>
<li>最后完全没做出来，没写出代码，也没想出思路</li>
</ul>
</li>
<li>表现稀烂，草草收场</li>
</ul>
<h2>1.18 二面</h2>
<ul>
<li>中文自我介绍<ul>
<li>概括成了 4 句话左右，精炼了很多</li>
</ul>
</li>
<li>在实验室做的 OS 中负责什么内容</li>
<li>算法题：原地倒转链表<ul>
<li>三指针，<code>last</code>、<code>curr</code>、<code>next</code></li>
</ul>
</li>
<li>顺着第一个算法题问了些 OS 知识<ul>
<li>代码是怎么编译出可执行文件的<ul>
<li>编译到目标文件，再链接为可执行文件</li>
</ul>
</li>
<li>可执行文件是怎么运行的<ul>
<li>我回答的是 OS 或 <code>ld.so</code> 加载 ELF 的过程</li>
<li>实际想问的也可能是 <code>fork</code> + <code>exec</code></li>
</ul>
</li>
</ul>
</li>
<li>算法题：求一个 0-1 矩阵中最大的相邻 1 的面积<ul>
<li>假设所有相邻的 1 之间有边，用 DFS 或 BFS 找到最大连通子图即可</li>
<li>DFS 的遍历条件一开始写漏了，面试官提醒 2 次有问题让仔细检查一下，分别检查出了一些错，最后完成</li>
</ul>
</li>
<li>顺着第二个算法题问了些 OS 知识<ul>
<li><code>vector&lt;vector&lt;int&gt;&gt; M</code> 参数如果换成 <code>int *M</code> 和 <code>int **M</code>，访问元素是还是 <code>M[i][j]</code> 吗<ul>
<li>这里主要是考察对一个指针进行 <code>[]</code> 实际会发生什么</li>
</ul>
</li>
<li>如果矩阵很大，DFS 这样的递归算法还能工作吗<ul>
<li>栈溢出</li>
</ul>
</li>
<li>什么因素影响递归的深度<ul>
<li>OS 为线程分配的栈大小、以及是否会动态增加栈大小</li>
<li>每层函数调用的栈帧大小，具体包括局部变量、caller/callee saved 寄存器、函数返回地址</li>
</ul>
</li>
<li>估算上面写的 DFS 函数每层调用消耗的栈空间<ul>
<li>我一开始忘记了 caller saved 寄存器（在 <code>call</code> 之前需要保存当前层的参数寄存器才能再设置下一层的参数），最后面试官说不太对准备结束了，我强行问他，然后才终于想起来</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>1.21 三面（Lead 面）</h2>
<ul>
<li>中文介绍自己的教育背景和项目经历</li>
<li>在实验室做的 OS 中负责什么内容<ul>
<li>问了微内核和 Linux 这种宏内核有什么区别</li>
<li>问了我们的 OS 如何实现信号量</li>
<li>还问了一些忘记了</li>
</ul>
</li>
<li>擅长什么编程语言<ul>
<li>答了 C++ 和 Python</li>
<li>接着问 Python 的内存管理和 C++ 有什么区别<ul>
<li>想考察的是关于 GC 的知识，我一开始没反应过来，因为之前刚在说操作系统的内存管理</li>
<li>后来我说 Python 的对象内存管理是通过 reference counting</li>
<li>然后又说了 GC 的 mark-and-sweep 算法</li>
<li>感觉这块答得非常混乱，太紧张了</li>
</ul>
</li>
</ul>
</li>
<li>算法题：如何求一个数的平方根<ul>
<li>我说可以用牛顿迭代，但我有点忘了来牛顿迭代是怎么做的</li>
<li>他说那想想别的方法</li>
<li>最后用了二分法，要写出代码（就几行）</li>
</ul>
</li>
<li>算法题：给一个未排序的数组，如何找到数组排序后中间那个数（也就是第 n/2 大的数）<ul>
<li>只要求说思路</li>
<li>实际上就是 top k 问题，用快排的思路，在 partition 的过程中根据 pivot 最后所在的位置和 n/2 比较，来决定继续处理左半边或右半边，直到 pivot 的位置就在 n/2</li>
</ul>
</li>
<li>算法题：扩展上一题，如果数组的数字是一个一个输入的，如何在输入每个数字之后输出当前第 n/2 大的数<ul>
<li>同样只要说思路</li>
<li>一开始说不能把数字存下来，没想出解法（真的可能不把数字存下来吗😂）</li>
<li>然后放宽要求，可以存下来，边想边跟面试官讨论，经历了以下思路<ul>
<li>维护一个平衡二叉树，让两个子树节点数相差不超过 1，后来我发现不可行</li>
<li>然后赶紧提出最简单的思路，维护当前第 n/2 数和左右两边的链表，像插入排序那样，每来一个数就插入到对应的位置，当两边数量差超过 1 时，取出左边最大的或右边最小的，更新第 n/2 数</li>
<li>接着面试官提醒左右两个链表是否要维护全序关系，我于是想到可以左右两边分别维护一个大顶堆和小顶堆即可</li>
</ul>
</li>
</ul>
</li>
<li>上面每个算法题每个思路都问了复杂度</li>
<li>最后问我有没有什么想问的，然后结束</li>
</ul>
<h2>1.29 录用意向书</h2>
<p>最后在 1 月 29 号收到了录用意向书。</p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2022/01/30/microsoft-interview/</guid>
      <pubDate>Sun, 30 Jan 2022 23:53:00 +0806</pubDate>
    </item>
    <item>
      <title>2022，会是一个新的开始吗</title>
      <link>https://stdrc.cc/post/2022/01/31/2022-new-beginning/</link>
      <description><![CDATA[<p>像去年一样，拖到了农历新年之前才写过去一年的总结和新一年的展望。</p>
<h2>从头至尾的失败</h2>
<p>从去年农历新年开始算的话，刚一开始就遭遇了 rcOS 的搁浅，因为过年之前刚写完一部分，过年之后继续弄实验室的事情，一直到开学之后都没能抽出空继续写。</p>
<p>上半年那一学期，也就是研一下学期，课多、项目任务困难，从 3 月一直状态低沉到 6 月，期间甚至去做了心理咨询尝试解决。</p>
<p>后来确实通过各种沟通解决了心头的一些压力，包括撑完了所有课程、调整了实验室项目里分配给我的任务，结果紧接着，7 月份失恋了。从那开始一直到 11 月，感情上经历了各种跌宕起伏，已经不想也不便多说了。</p>
<h2>年底开始好转</h2>
<p>到年末，实验室项目中任务的参与感越来越强了，终于做到了自己感兴趣并擅长的事情，写代码的状态有所好转。</p>
<p>另一方面，感情方面的波折也渐渐放下了。同时，和许多老朋友和新朋友增进了一些交流，社交真的很能让人开心。</p>
<p>再到 2022 年的第一个月，社交、写代码、找实习都逐渐进入了舒适状态。尤其是一开始随手报的微软校招实习提前批最后居然面试通过了（这里顺便发一个 <a href="/post/2022/01/30/microsoft-interview/">面经</a>），让自信心提升了不少（虽然或许综合考虑后可能还是会选择去另一家公司）。</p>
<h2>2022 年会更好吗？</h2>
<p>目前看来今年的开头还是比较顺利的，希望之后可以越来越顺利，或至少平均而言保持和第一个月相当的状态吧。</p>
<p>总体目标大约如下：</p>
<ul>
<li>做好实验室项目（也就是 ChCore）的最后工作，为研究生生涯收尾</li>
<li>做好暑期实习，保持学习新技术，并在秋招找到理想的正式工作</li>
<li>更多地参与一些开源项目的贡献</li>
<li>保持运动，让自己变得更健康</li>
<li>读更多人文社科类的书，扩展自己的文化视野</li>
<li>努力做到“己所不欲，勿施于人”</li>
</ul>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2022/01/31/2022-new-beginning/</guid>
      <pubDate>Mon, 31 Jan 2022 00:49:00 +0806</pubDate>
    </item>
    <item>
      <title>2022，确实是一个新的开始</title>
      <link>https://stdrc.cc/post/2022/12/27/2022-end/</link>
      <description><![CDATA[<p>又是一年没有更新博客了，上一次一年没更新还是准备考研的 2019 年。</p>
<p>今年年初的时候写了一篇 <a href="/post/2022/01/31/2022-new-beginning/">2022，会是一个新的开始吗</a>，很简略，寥寥几笔就把 2021 的一年低谷带过了。去年年底到今年年初，情况逐渐开始好转，随后的一年虽然也有一些起伏，但总体还是好于年初时候对今年的期待。2022 年，确实是一个新的开始，或许是我人生的一个转折点。</p>
<h2>OS 助教和毕业设计</h2>
<p>今年的上半学期，也就是研二下学期，终于当了一次操作系统课的助教，主要就是参与出 lab 和回答同学们的问题。出 lab 是从实验室的操作系统 ChCore 主线上做裁剪，然后挖出一些空，期间也发现了 ChCore 一些有 bug 和值得重构的地方，又 port 回主线，算是为 ChCore 做的最后贡献了。回答同学们的问题也很有趣，我很喜欢帮到别人的感觉，一些同学也很能抓到要点，许多时候是在相互交流进行思维升级，而不是单方面的回答。</p>
<p>这学期的后半段，另一件最重要的事情就是毕业设计了，是在 ChCore 上尝试一种新的系统服务设计。开题的时候，正逢上海疫情封校封楼，在宿舍里写代码效率很低，当时非常焦虑，担心在暑假前写不完，影响之后的实习。后来焦虑带来了动力，通过几天熬夜快速进入了状态，虽然之后效率又逐渐下降，但还是在暑期实习之前基本写完了毕设的代码。到了 11 月暂停实习之后，则是边摸鱼边写毕业论文，到 12 月初算是把论文基本写完了，虽然直到现在还有需要修改的小地方🤣。</p>
<h2>实习、校招和 RisingWave</h2>
<p>6 月底的时候终于开始了在 Singularity Data（现在已经改名叫 RisingWave Labs）的实习，参与维护公司开源的 <a href="https://github.com/risingwavelabs/risingwave">RisingWave</a> 流式数据库内核。其实在实习之前，我根本完全不懂数据库的原理，更不懂流式数据库。刚开始不知道从何下手，后来慢慢做了一些简单的任务，开始进入状态，到最后也算是基本能理解整个数据库的设计思想了（不过感觉以后还是需要补一些比较系统的课程）。公司里面大部分同事都是聪明而有态度的年轻人，和他们一起在开源项目上工作，让我感到极度舒适，这是我一直以来梦寐以求的感觉。</p>
<p>在实习的同时参与了校招，面试的公司不算太多，有一些面到一半已经不想再面于是放弃了，最终真正完成并通过面试的公司只有字节、华为和百度昆仑芯三家。虽然面试的时候感觉发挥都挺好，但由于大环境的压力，这些公司都迟迟没有发 offer，甚至一直到 9 月底都没有收到任何一个意向（后两家其实没有意向阶段），字节则是分别由不同的组捞了好几次。那段时间非常焦虑，但又不想再投简历了，想着就听天由命吧，毕竟转正应该是肯定可以转的。到了 10 月底，面完了的三家公司都陆续给了意向或开奖，焦虑的心情终于迎来解脱。后来，经过和 mentor、和朋友、和自己的充分沟通，最终还是决定在 Singularity Data 转正，想把这种开源、现代、自驱、自由的工作方式继续下去。</p>
<h2>OneBot 和 WasmEdge</h2>
<p>在这一年断断续续的一些时间点，也尽力推进了之前聊天机器人方向上的开源项目 <a href="https://github.com/botuniverse/onebot">OneBot</a>。目前 OneBot 12 标准已经基本上稳定，也有了一些对它的实现。由于自己已经很久没有写过聊天机器人，越来越明显地感觉到自己在这个方向上已经不再有激情、动力和洞见了，暂时还不知道该怎么办。不过，看到一些朋友在积极地尝试采用 OneBot 12 标准，还是很开心的。</p>
<p>暑期实习的下班时间还抽空参与了 <a href="https://github.com/WasmEdge">WasmEdge</a> 社区的一个 LFX Mentorship 项目。项目本身并不是改进 WasmEdge Runtime，而是移植一个数据库的客户端 SDK 到 WasmEdge 上运行，虽然做起来比较简单，但在过程中还是学到了许多 WebAssembly 相关知识，了解了一个 WASM 运行时的基本结构等等。</p>
<h2>新冠疫情防控</h2>
<p>3 月和 9 月分别经历了两次封校、封宿舍楼，4 月和 11 月分别看到了两次朋友圈“电子游行”，后者也伴随了在各地上演的真实抗议。从 3～5 月上海摇摆后重新“坚持动态清零”，再到“二十条”、“新十条”、全国逐渐取消公共场所核酸要求，最后在 12 月 26 日宣布明年 1 月 8 日开始对新冠病毒实施“乙类乙管”，我想我算是见证了历史，见证了一段浓墨重彩的历史。新冠疫情防控开始于我考完研后的仅仅一个月，结束于我硕士毕业前的一个月，我的整个硕士生涯几乎完全笼罩在疫情和疫情防控造成的不确定性中。现在，这段时期结束，一个新的时期正在开始，我感到悲壮，也感到人类的渺小。</p>
<p>12 月 22 日，我终于第一次感染并发作了新冠，先是发高烧 2～3 天，然后咽痛不断加剧，有三个晚上难以入睡，到今天（27 号），尽管因为不再发烧，精神已经好了很多，但身体状况仍然没有完全恢复。</p>
<h2>读书和思考</h2>
<p>从年初开始决定记录今年的阅读情况，记录读了什么书、对书的评价，以及一些简单的读后感或是总结。Notion 提供了很好的自定义数据库和视图功能，帮助我方便地管理这些记录，我把今年开始已读的书都公开在了我的 <a href="https://jump.stdrc.cc/books">读书</a> 页面。</p>
<figure>
    <img src="/static/images/2022-12-27/books.png" alt="2022 年已读列表">
    <center><figcaption>2022 年的已读列表</figcaption></center>
</figure>

<p>最后，2022 年一共读了 16 本书，超出了一开始的预期。一些书补充完善了我的世界观、价值观和人生观，让我对自然、社会、人、世界等一切有了更好的理解。读书的过程，就像曾经某位名人（忘了是谁）所说的，像在和作者对话，这种对话不是单向的听取，而是有来有回且循序渐进的思想交流。这些书中不一定每个观点都让我十分信服，甚至有一些我持反对意见，但它们都让我得到了一些精神上的收获。</p>
<p>除了记录阅读，今年还开始记录了自己对各种事情的思考，希望用文字的形式把这些思考固化下来，以便以后可以找寻自己思维发展的过程。直到年底，已经记录了 24 个思考。我发现把对事物的思考写成文字，可以强迫自己更全面地考虑问题，而不是只对事物的一个方面产生情绪化的反应。</p>
<figure>
    <img src="/static/images/2022-12-27/thoughts.png" alt="2022 年思考列表">
    <center><figcaption>2022 年的思考列表</figcaption></center>
</figure>

<h2>音乐现场</h2>
<p>生日那天，看了人生第一场 livehouse 演出，是房东的猫在苏州的巡演。随后在疫情防控放开后，又分别在上海和杭州看了两场，分别是达闻西乐队和达达乐队的演出。我发现我爱上了 livehouse 这种演出形式，因为在这里，乐队和观众的距离被拉近，乐手和歌手们成了活生生的人，而不是演奏和演唱机器，更不是相当有气派的大明星。当音乐响起，台上和台下都深深地沉浸在热爱之中，而不仅仅是一边表演，另一边看。</p>
<figure>
    <img src="/static/images/2022-12-27/IMG_3565.jpeg" alt="房东的猫">
    <center><figcaption>房东的猫，苏州</figcaption></center>
</figure>

<figure>
    <img src="/static/images/2022-12-27/IMG_4759.jpeg" alt="达闻西">
    <center><figcaption>达闻西乐队，上海</figcaption></center>
</figure>

<figure>
    <img src="/static/images/2022-12-27/IMG_4931.jpeg" alt="达达">
    <center><figcaption>达达乐队，杭州</figcaption></center>
</figure>

<h2>再看去年定的目标们</h2>
<blockquote>
<ul>
<li>做好实验室项目（也就是 ChCore）的最后工作，为研究生生涯收尾</li>
<li>做好暑期实习，保持学习新技术，并在秋招找到理想的正式工作</li>
<li>更多地参与一些开源项目的贡献</li>
<li>保持运动，让自己变得更健康</li>
<li>读更多人文社科类的书，扩展自己的文化视野</li>
<li>努力做到“己所不欲，勿施于人”</li>
</ul>
</blockquote>
<p>不可思议地，除了运动之外（也不是没运动，但从全年来看还是太少了），这些目标（我想）大概都算是完成了。不过想想毕竟目标其实都很笼统，也没啥完不成的。</p>
<figure>
    <img src="/static/images/2022-12-27/unbelievable.jpeg" alt="不可思议">
    <center><figcaption>在上海博物馆拍到的“不可思议”</figcaption></center>
</figure>

<h2>即将开始的 2023 年</h2>
<p>新的一年，或许还是像去年一样定一些笼统的目标吧，这样在保持总体进步趋势的同时，可以允许自己在一年中对具体的计划有调整。</p>
<p>那么，希望自己可以在 2023 年，</p>
<ul>
<li>少输出观点，尤其避免在有争议的问题上输出不成熟的观点并意图说服别人，多输入，多思考，兼听则明；</li>
<li>多旅游，亲身感受这个真实的世界；</li>
<li>读更多书，包括一些文学、哲学、社会、西方政治历史相关的书；</li>
<li>学习更多数据库知识，以更好地向 RisingWave 和可能的其它数据库开源项目贡献代码；</li>
<li>继续或重新开始开发 rcOS；</li>
<li>学习一些写代码之外的有趣技能；</li>
<li>多运动。</li>
</ul>
<p>就这样吧，结束这篇比去年冗长许多的年度总结，致新的人生～</p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2022/12/27/2022-end/</guid>
      <pubDate>Tue, 27 Dec 2022 21:54:00 +0806</pubDate>
    </item>
    <item>
      <title>ChCore 构建系统实现思路</title>
      <link>https://stdrc.cc/post/2023/01/07/chbuild/</link>
      <description><![CDATA[<p>读研期间的一个工作是为实验室的 ChCore 操作系统重写了新的构建系统——ChBuild，主要包括各级 CMake 脚本、配置系统和构建入口脚本。目前构建系统已经跟随 <a href="https://gitee.com/ipads-lab/chcore-lab-v2">第二版 ChCore Lab</a> 开源，所以现在可以尝试分享一下思路。如果你不了解 ChCore Lab，也没有关系，这里主要是想粗浅地介绍一些 CMake 很有趣且有用的特性和技巧，可以只看关于这些的内容。</p>
<p>下面的讨论基于 <a href="https://gitee.com/ipads-lab/chcore-lab-v2/tree/lab5">ChCore Lab v2 的 <code>lab5</code> 分支</a>，因为这里包含了比较完整的操作系统代码结构。在阅读之前，建议你首先理解 <a href="https://github.com/richardchien/modern-cmake-by-example">Modern CMake By Example</a> 中的绝大部分内容。</p>
<h2>旧系统的问题</h2>
<p>尽管和 ChCore 主线不完全一样，但你可以在 <a href="https://gitee.com/ipads-lab/chcore-lab/tree/lab5/">ChCore Lab v1 的 <code>lab5</code> 分支</a> 看到旧版的 ChCore 构建系统的缩影。</p>
<p>主要存在的问题包括：</p>
<ul>
<li>以 <code>scripts/docker_build.sh</code> 作为构建入口，只支持利用预先提供的 Docker 映像创建容器，并在容器中采用硬编码的工具链构建，无法支持在不同的本地环境中构建</li>
<li>构建用户态程序、RamDisk 和内核的逻辑分散在不同的 shell 脚本，难以统一对构建行为进行配置（例如对用户态程序和内核统一传入某些 CMake 变量），难以维护</li>
<li>CMake 项目层级混乱，比如根目录 <code>CMakeLists.txt</code> 实际上在控制 <code>kernel</code> 的构建</li>
<li>各子项目 CMake 脚本代码混乱，没有采用现代 CMake 的最佳实践</li>
<li>没有比较方便可用的配置系统，无法在一个配置文件中控制整个系统的构建行为</li>
</ul>
<p>因此，要解决这些问题，对新的构建系统提出了以下要求：</p>
<ul>
<li>构建过程应当可以在 Docker 容器中进行，也可以在本地环境进行，允许较为方便地切换构建工具链</li>
<li>在统一的根级别 CMake 项目中管理子项目，不再把不同子项目的构建逻辑分散到不同的 shell 脚本</li>
<li>在各级 CMake 脚本中采用现代 CMake 最佳实践</li>
<li>支持通过类似 Linux 内核的层级 <code>Kconfig</code> 文件声明构建系统的配置项，通过单个 <code>.config</code> 文件配置整个构建行为，通过类似 <code>make menuconfig</code> 的命令提供 TUI 配置面板</li>
</ul>
<h2>入口脚本</h2>
<p>新的构建入口脚本名为 <code>chbuild</code>，是一个 Bash 脚本。</p>
<p>在旧的构建系统中，构建入口脚本 <code>scripts/build.sh</code>（由 <code>scripts/docker_build.sh</code> 创建 Docker 容器后调用）实际上只能用于“构建”整个系统，不包含任何类似 Linux 内核的 <code>make defconfig</code>（创建默认配置文件）、<code>make clean</code>（清空构建临时文件）等功能。我希望在新的构建入口中通过子命令的形式提供不同的子功能。在 shell 脚本中，实现子命令其实非常简单，只需要定义子命令对应的函数，然后在脚本入口处把第一个参数当作函数名称来调用，如下：</p>
<pre class="highlight"><code class="language-bash"># chbuild

build() {
    _check_config_file # 辅助函数加下划线，以免用户不小心调用到
    _echo_info &quot;Building...&quot;
    # ...
}

clean() {
    _echo_info &quot;Cleaning...&quot;
    # ...
}

distclean() {
    clean # 子命令也可以调用其它子命令
    rm -rf $config_file
}

_print_help() {
    echo &quot;...&quot;
}

_main() {
    case $1 in
    help | --help | -h)
        _print_help
        exit
        ;;
    -*)
        _echo_err &quot;$self: invalid option \`$1\`\n&quot;
        break
        ;;
    *)
        if [[ &quot;$1&quot; == &quot;_&quot;* || $(type -t &quot;$1&quot;) != function ]]; then
            # 避免用户试图调用辅助函数或不是函数的东西
            _echo_err &quot;$self: invalid command \`$1\`\n&quot;
            break
        fi

        $@ # 第一个参数作为要调用的子命令函数，剩余参数则传入函数
        exit
        ;;
    esac

    # 没有子命令成功运行
    _print_help
    exit 1
}

_main $@ # 调用入口 _main 函数并传入脚本的所有参数
</code></pre>

<p>同时，我希望用户可以在 <code>chbuild</code> 脚本的参数中指定要在本地环境运行还是在 Docker 容器中运行子命令。并且，我希望在 Docker 容器中运行子命令时，<code>chbuild</code> 不需要再调用其它脚本，而是直接在容器中用相同的参数启动自身。也就是说，不再需要区分 <code>build.sh</code> 和 <code>docker_build.sh</code>，无论要不要在 Docker 容器中构建，都使用 <code>chbuild</code> 作为入口。这听起来可能有点绕，直接来看看如何实现（注意 <code>_main</code> 函数和上面的区别）：</p>
<pre class="highlight"><code class="language-bash"># chbuild

_docker_run() {
    if [ -f /.dockerenv ]; then
        # 如果已经在 Docker 容器中，直接把参数作为子命令运行
        $@
    else
        # 否则，启动 Docker 容器，并运行自身
        test -t 1 &amp;&amp; use_tty=&quot;-t&quot;
        docker run -i $use_tty --rm \
            -u $(id -u ${USER}):$(id -g ${USER}) \
            -v $(pwd):/chos -w /chos \
            ipads/chcore_builder:v1.3 \
            $self $@
    fi
}

_main() {
    run_in_docker=true # 默认在 Docker 容器中运行子命令
    while [ $# -gt 0 ]; do
        case $1 in
        help | --help | -h)
            _print_help
            exit
            ;;
        --local | -l)
            # --local 参数用于指定在本地环境运行子命令
            run_in_docker=false
            ;;
        -*)
            _echo_err &quot;$self: invalid option \`$1\`\n&quot;
            break
            ;;
        *)
            if [[ &quot;$1&quot; == &quot;_&quot;* || $(type -t &quot;$1&quot;) != function ]]; then
                _echo_err &quot;$self: invalid command \`$1\`\n&quot;
                break
            fi

            if [[ $run_in_docker == true ]]; then
                # 如果要在 Docker 容器中运行子命令，则通过 _docker_run 辅助函数进行
                _docker_run $@
            else
                # 否则直接调用
                $@
            fi
            exit
            ;;
        esac
        shift
    done

    _print_help
    exit 1
}

_main $@
</code></pre>

<p>于是，用户就可以通过 <code>./chbuild --local build</code> 在本地环境构建 ChCore，通过 <code>./chbuild build</code> 在 Docker 容器中构建 ChCore。搭配后面的配置系统，可以实现更好的本地环境跨平台构建支持。</p>
<h2>根项目</h2>
<p>旧的构建系统中，根项目实际上是 <code>kernel</code> 子项目，没有真正的根项目，对子项目的控制分散在不同的 shell 脚本中，<code>scripts/compile_user.sh</code> 用于调用 <code>user</code> 子项目的 CMake 构建，<code>scripts/build.sh</code> 用于调用 <code>kernel</code> 子项目的 CMake 构建。</p>
<p>在翻阅 CMake 文档的过程中，我发现了 CMake 内置的 <a href="https://cmake.org/cmake/help/latest/module/ExternalProject.html">ExternalProject</a> 模块。这个模块的 <code>ExternalProject_Add</code> 命令可以把一个子目录或远程 Git 仓库添加为一个“外部项目”，同时配置它的 <code>CONFIGURE_COMMAND</code>、<code>BUILD_COMMAND</code>、<code>BINARY_DIR</code>、<code>INSTALL_DIR</code> 等属性，还可以通过 <code>CMAKE_ARGS</code> 和 <code>CMAKE_CACHE_ARGS</code> 属性来传入 CMake 参数和 cache 变量（也就是命令行调用 <code>cmake</code> 命令时可以传入的 <code>-D</code> 参数）。它不仅可用于添加 CMake 项目，也可以用来添加 Makefile 或是其它构建系统管理的项目。总之，这个功能非常适合用来在 ChCore 根项目中管理各子项目，这样就可以全程使用 CMake，简化构建系统（尤其是配置系统）的实现。</p>
<p>由于 <code>ExternalProject_Add</code> 这个名字显得太把自己的子项目当外人了，我把它重新定义成了 <code>chcore_add_subproject</code>：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/Modules/SubProject.cmake

macro(chcore_add_subproject)
    ExternalProject_Add(${ARGN})
endmacro()
</code></pre>

<p>于是，可以在 ChCore 根目录的 <code>CMakeLists.txt</code> 中通过如下代码来添加 <code>libchcore</code>、<code>userland</code> 和 <code>kernel</code> 子项目：</p>
<pre class="highlight"><code class="language-cmake"># CMakeLists.txt

set(_common_args
    -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}
    -DCHCORE_PROJECT_DIR=${CMAKE_CURRENT_SOURCE_DIR})

set(_libchcore_source_dir ${CMAKE_CURRENT_SOURCE_DIR}/libchcore)
set(_libchcore_build_dir ${_libchcore_source_dir}/_build)
set(_libchcore_install_dir ${_libchcore_source_dir}/_install)
# ...

chcore_add_subproject(
    libchcore
    SOURCE_DIR ${_libchcore_source_dir}
    BINARY_DIR ${_libchcore_build_dir}
    INSTALL_DIR ${_libchcore_install_dir}
    CMAKE_ARGS
        ${_common_args}
        -DCMAKE_INSTALL_PREFIX=&lt;INSTALL_DIR&gt;
        -DCMAKE_TOOLCHAIN_FILE=${_cmake_script_dir}/Toolchains/userland.cmake
    BUILD_ALWAYS TRUE)

chcore_add_subproject(
    userland
    SOURCE_DIR ${_userland_source_dir}
    BINARY_DIR ${_userland_build_dir}
    INSTALL_DIR ${_userland_install_dir}
    CMAKE_ARGS
        ${_common_args}
        -DCMAKE_INSTALL_PREFIX=&lt;INSTALL_DIR&gt;
        -DCMAKE_TOOLCHAIN_FILE=${_cmake_script_dir}/Toolchains/userland.cmake
    DEPENDS libchcore userland-clean-incbin
    BUILD_ALWAYS TRUE)

chcore_add_subproject(
    kernel
    SOURCE_DIR ${_kernel_source_dir}
    BINARY_DIR ${_kernel_build_dir}
    INSTALL_DIR ${_kernel_install_dir}
    CMAKE_ARGS
        ${_common_args}
        -DCHCORE_USER_INSTALL_DIR=${_userland_install_dir} # used by kernel/CMakeLists.txt to incbin cpio files
        -DCMAKE_INSTALL_PREFIX=&lt;INSTALL_DIR&gt;
        -DCMAKE_TOOLCHAIN_FILE=${_cmake_script_dir}/Toolchains/kernel.cmake
    DEPENDS userland kernel-clean-incbin
    BUILD_ALWAYS TRUE)
</code></pre>

<p>可以看到，通过 ExternalProject 模块可以非常简单而清晰地添加一个 CMake 子项目并传入指定参数、设置 <code>CMAKE_TOOLCHAIN_FILE</code> 工具链文件、设置子项目和其它 target 间的依赖关系等。</p>
<p>根项目中还通过 <a href="https://cmake.org/cmake/help/latest/command/add_custom_target.html">custom target</a> 的形式提供了子项目的 clean 动作：</p>
<pre class="highlight"><code class="language-cmake"># CMakeLists.txt

add_custom_target(
    libchcore-clean
    COMMAND /bin/rm -rf ${_libchcore_build_dir}
    COMMAND /bin/rm -rf ${_libchcore_install_dir})

add_custom_target(
    userland-clean
    COMMAND /bin/rm -rf ${_userland_build_dir}
    COMMAND /bin/rm -rf ${_userland_install_dir})

add_custom_target(
    kernel-clean
    COMMAND /bin/rm -rf ${_kernel_build_dir}
    COMMAND [ -f ${_kernel_install_dir}/install_manifest.txt ] &amp;&amp; cat
            ${_kernel_install_dir}/install_manifest.txt | xargs rm -rf || true)

add_custom_target(
    clean-all
    COMMAND ${CMAKE_COMMAND} --build . --target kernel-clean
    COMMAND ${CMAKE_COMMAND} --build . --target userland-clean
    COMMAND ${CMAKE_COMMAND} --build . --target libchcore-clean)
</code></pre>

<p>于是，在 <code>chbuild</code> 的 <code>clean</code> 子命令中就可以通过 <code>cmake --build $cmake_build_dir --target clean-all</code> 来清理所有子项目的构建临时文件。根项目的 build 目录则直接在 <code>chbuild</code> 的 <code>clean</code> 子命令函数中通过 <code>rm -rf $cmake_build_dir</code> 来 clean。这里的理念是，谁负责控制一个（子）项目的构建过程，谁就负责这个（子）项目的 clean 过程。</p>
<h2>子项目和工具链文件</h2>
<p>这部分跟 ChCore 操作系统本身的相关性比较强，如果你不了解或者不感兴趣，其实可以跳到 <a href="#配置系统">配置系统</a>。</p>
<h3><code>libchcore</code> 子项目</h3>
<p><code>libchcore</code> 子项目用于构建 LibChCore，即对 ChCore 内核系统调用接口和一些关键系统服务 IPC 接口的封装库（产物是 <code>libchcore.a</code> 和相关头文件），以及 crt0（产物是 <code>crt0.o</code>）。其实这个子项目的 CMake 相关内容只有一个 <code>libchcore/CMakeLists.txt</code>，没有太多值得介绍的内容，主要是可以通过 <a href="https://cmake.org/cmake/help/latest/command/install.html"><code>install</code></a> 命令安装 target 文件、目录、其它文件到指定目标路径：</p>
<pre class="highlight"><code class="language-cmake"># libchcore/CMakeLists.txt

add_library(chcore STATIC ...)
install(TARGETS chcore LIBRARY DESTINATION lib)

install(
    DIRECTORY include/chcore include/arch/${CHCORE_ARCH}/chcore
    DESTINATION include
    FILES_MATCHING
    PATTERN &quot;*.h&quot;)

add_custom_target(
    chcore_crt0 ALL
    COMMAND
        ${CMAKE_C_COMPILER} -c
        -I${CMAKE_CURRENT_SOURCE_DIR}/include/arch/${CHCORE_ARCH}
        -I${CMAKE_CURRENT_SOURCE_DIR}/include -o
        ${CMAKE_CURRENT_BINARY_DIR}/crt0.o
        ${CMAKE_CURRENT_SOURCE_DIR}/crt/crt0.c)

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/crt0.o DESTINATION lib)
</code></pre>

<p>这些 <code>install</code> 命令的目标地址没有使用绝对路径，而是使用了相对的 <code>include</code> 和 <code>lib</code>。这些相对路径相对的是在根项目 <code>chcore_add_subproject</code> 时通过 <a href="https://cmake.org/cmake/help/latest/variable/CMAKE_INSTALL_PREFIX.html"><code>CMAKE_INSTALL_PREFIX</code></a> 参数所指定的安装目录 <code>${_libchcore_install_dir}</code>（见前面）。</p>
<p>当根项目 build 时，通过 <code>chcore_add_subproject</code> 添加的子项目会被 configure、build、install。同时，子项目间有依赖关系，于是可以保证在 build <code>userland</code> 子项目时，<code>libchcore</code> 子项目已经将 LibChCore 的头文件和静态库以及 <code>crt0.o</code> 安装到 <code>${_libchcore_install_dir}</code> 目录，因而在 <code>userland</code> 子项目中可以正确的包含 LibChCore 头文件、链接 LibChCore 静态库和 <code>crt0.o</code>。</p>
<h3><code>userland</code> 子项目</h3>
<p><code>userland</code> 子项目用于构建用户态系统服务和应用程序。基本逻辑是添加一些全局的编译和链接选项（因为需要应用到该子项目的所有 target），然后通过 <code>add_subdirectory</code> 一层层包含下去。</p>
<p>除此之外，该子项目还需要在一些系统服务和应用程序构建完成之后，将它们打包成 CPIO 格式的 RamDisk，这是比较有趣的地方，来看代码：</p>
<pre class="highlight"><code class="language-cmake"># userland/CMakeLists.txt

# 第一块
set(_ramdisk_dir ${CMAKE_CURRENT_BINARY_DIR}/ramdisk)
file(REMOVE_RECURSE ${_ramdisk_dir})
file(MAKE_DIRECTORY ${_ramdisk_dir})
add_custom_target(
    ramdisk.cpio ALL
    WORKING_DIRECTORY ${_ramdisk_dir}
    COMMAND find . | cpio -o -H newc &gt; ${CMAKE_CURRENT_BINARY_DIR}/ramdisk.cpio)

# 第二块
function(chcore_copy_target_to_ramdisk _target)
    add_custom_command(
        TARGET ${_target}
        POST_BUILD
        COMMAND cp $&lt;TARGET_FILE:${_target}&gt; ${_ramdisk_dir})
    add_dependencies(ramdisk.cpio ${_target})
    set_property(GLOBAL PROPERTY ${_target}_INSTALLED TRUE)
endfunction()

function(chcore_copy_all_targets_to_ramdisk)
    set(_targets)
    chcore_get_all_targets(_targets)
    foreach(_target ${_targets})
        get_property(_installed GLOBAL PROPERTY ${_target}_INSTALLED)
        if(${_installed})
            continue()
        endif()
        get_target_property(_target_type ${_target} TYPE)
        if(${_target_type} STREQUAL SHARED_LIBRARY OR ${_target_type} STREQUAL
                                                      EXECUTABLE)
            chcore_copy_target_to_ramdisk(${_target})
        endif()
    endforeach()
endfunction()

# 第三块
add_subdirectory(servers)
add_subdirectory(apps)
</code></pre>

<p>第一块首先删除已经存在的 RamDisk 临时目录，然后重新创建，接着定义 <code>ramdisk.cpio</code> custom target，行为就是把 RamDisk 临时目录打包成 CPIO 文件。</p>
<p>第二块定义了两个 CMake 函数：<code>chcore_copy_target_to_ramdisk</code> 和 <code>chcore_copy_all_targets_to_ramdisk</code>。前者用于把一个 target 的产物拷贝到 RamDisk 临时目录，实现上就是为这个 target 添加一个 <code>POST_BUILD</code>（构建后）<a href="https://cmake.org/cmake/help/latest/command/add_custom_command.html">custom command</a>，在其中进行拷贝。由于拷贝需要先于 <code>ramdisk.cpio</code> target 的打包操作，因此还需要通过 <a href="https://cmake.org/cmake/help/latest/command/add_dependencies.html"><code>add_dependencies</code></a> 添加依赖关系。后者用于把调用处可见的所有 target 的产物拷贝到 RamDisk 临时目录，实际上就是通过 <code>chcore_get_all_targets</code> 获得 target 列表，然后对其中没有单独调用过前者的 target 调用前者。</p>
<p>第三块是包含下级 <code>CMakeLists.txt</code>，进而递归地包含到 <code>userland</code> 的所有 <code>CMakeLists.txt</code>，在其中的某些地方会调用第二块定义的函数。比如：</p>
<pre class="highlight"><code class="language-cmake"># userland/apps/lab5/CMakeLists.txt

add_executable(...)
add_executable(...)
chcore_copy_all_targets_to_ramdisk()
</code></pre>

<h3><code>kernel</code> 子项目</h3>
<p><code>kernel</code> 子项目用于构建内核映像文件 <code>kernel.img</code>。逻辑非常简单，首先创建 <code>kernel.img</code> target，然后为其设置一些编译链接选项和包含目录，接着一级一级包含下面的所有模块的 <code>CMakeLists.txt</code>，在其中通过 <a href="https://cmake.org/cmake/help/latest/command/target_sources.html"><code>target_sources</code></a> 为 <code>kernel.img</code> 添加源文件。</p>
<p>比较值得介绍的是通过 <a href="https://cmake.org/cmake/help/latest/command/configure_file.html"><code>configure_file</code></a> 来从模板生成文件，可以在模板文件中通过 <code>${var_name}</code> 引用 CMake 变量。结合配置系统，可以尽量减少相关文件中写死的内容。在 <code>kernel</code> 子项目中，这个技巧用于生成 <code>incbin.S</code> 和 <code>linker.ld</code>：</p>
<pre class="highlight"><code class="language-armasm"># kernel/incbin.tpl.S

        .section .rodata
        .align 4
        .globl __binary_${binary_name}_start
__binary_${binary_name}_start:
        .incbin &quot;${binary_path}&quot;
__binary_${binary_name}_end:
        .globl __binary_${binary_name}_size
__binary_${binary_name}_size:
        .quad __binary_${binary_name}_end - __binary_${binary_name}_start
</code></pre>

<pre class="highlight"><code class="language-cmake"># kernel/CMakeLists.txt

macro(_incbin _binary_name _binary_path)
    set(binary_name ${_binary_name})
    set(binary_path ${_binary_path})
    configure_file(incbin.tpl.S incbin_${_binary_name}.S)
    unset(binary_name)
    unset(binary_path)
    target_sources(${kernel_target} PRIVATE incbin_${_binary_name}.S)
endmacro()

_incbin(root ${CHCORE_USER_INSTALL_DIR}/${CHCORE_ROOT_PROGRAM})
</code></pre>

<pre class="highlight"><code># kernel/arch/aarch64/boot/linker.tpl.ld

SECTIONS
{
    . = TEXT_OFFSET;
    img_start = .;
    init : {
        ${init_objects}
    }

    # ...
}
</code></pre>

<pre class="highlight"><code class="language-cmake"># kernel/arch/aarch64/boot/CMakeLists.txt

add_subdirectory(${CHCORE_PLAT}) # 包含后 `init_objects` 变量为 boot 模块目标文件列表
string(REGEX REPLACE &quot;;&quot; &quot;\n&quot; init_objects &quot;${init_objects}&quot;)
configure_file(linker.tpl.ld linker.ld.S)
</code></pre>

<h3>工具链文件</h3>
<p>在 <code>libchcore</code>、<code>userland</code> 和 <code>kernel</code> 子项目中，都没有任何设置构建工具链（C 编译器命令名等）的内容，这些内容应该放在独立的、通过 <a href="https://cmake.org/cmake/help/latest/variable/CMAKE_TOOLCHAIN_FILE.html"><code>CMAKE_TOOLCHAIN_FILE</code></a> 指定的 <a href="https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html">工具链文件</a> 中。其实工具链文件里的内容放在 <code>CMakeLists.txt</code> 也能正常工作，但是放在工具链文件中，CMake 可以在 configure 项目前首先通过测试项目来检查工具链是否可以正常使用。</p>
<p>新的构建系统提供了两个工具链文件：<code>userland.cmake</code> 和 <code>kernel.cmake</code>，都在 <code>scripts/build/cmake/Toolchains</code> 目录中。在根项目中添加各子项目时，为 <code>libchcore</code> 和 <code>userland</code> 指定了 <code>userland.cmake</code> 工具链文件，为 <code>kernel</code> 指定了 <code>kernel.cmake</code> 工具链文件。ChCore Lab 中这两者内容其实很接近，但在 ChCore 主线中则有更多不同。这里只放一下 <code>kernel.cmake</code> 工具链的部分代码：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/Toolchains/kernel.cmake

# Set toolchain executables
set(CMAKE_ASM_COMPILER &quot;${CHCORE_CROSS_COMPILE}gcc&quot;)
set(CMAKE_C_COMPILER &quot;${CHCORE_CROSS_COMPILE}gcc&quot;)
# ...

include(${CMAKE_CURRENT_LIST_DIR}/_common.cmake)

# Set the target system (automatically set CMAKE_CROSSCOMPILING to true)
set(CMAKE_SYSTEM_NAME &quot;Generic&quot;)
set(CMAKE_SYSTEM_PROCESSOR ${CHCORE_ARCH})
</code></pre>

<p><code>userland.cmake</code> 和 <code>kernel.cmake</code> 工具链文件在设置完 C 编译器等工具链命令后，会包含 <code>_common.cmake</code>。这个文件是工具链文件的共用部分，主要工作是从 C 编译器推导出编译目标体系结构（通过 <a href="https://cmake.org/cmake/help/latest/command/execute_process.html"><code>execute_process</code></a> 运行 <code>gcc -dumpmachine</code>），并设置到 <code>CHCORE_ARCH</code> cache 变量，然后再把所有 <code>CHCORE_</code> 开头的 cache 变量添加为编译选项，以便在 C 语言代码中进行条件编译。这里绝大部分 cache 变量都是从配置文件读入的，更多细节会在后面配置系统的 <a href="#配置的传递">配置的传递</a> 部分详细介绍。</p>
<p>包含完 <code>_common.cmake</code> 之后，两个工具链文件分别设置了 <a href="https://cmake.org/cmake/help/latest/variable/CMAKE_SYSTEM_NAME.html"><code>CMAKE_SYSTEM_NAME</code></a> 和 <a href="https://cmake.org/cmake/help/latest/variable/CMAKE_SYSTEM_PROCESSOR.html"><code>CMAKE_SYSTEM_PROCESSOR</code></a>。这会告知 CMake 当前项目正在进行跨平台编译，并指导 CMake 使用正确的 sysroot、链接器行为等。在 <code>userland.cmake</code> 工具链中指定了 <code>CMAKE_SYSTEM_NAME</code> 为 <code>ChCore</code>，这个系统相关的跨平台构建行为配置在 <code>scripts/build/cmake/Modules/Platform/ChCore.cmake</code> 文件中定义，由于 ChCore 用户态程序的构建行为和 Linux 基本一致，因此这里直接包含了 CMake 内置的 <code>Platform/Linux</code>，可以在 <code>/usr/share/cmake-x.xx/Modules/Platform/Linux.cmake</code> 或 <a href="https://github.com/Kitware/CMake/blob/master/Modules/Platform/Linux.cmake">代码仓库</a> 中看到后者的内容。<code>kernel.cmake</code> 工具链中则指定系统为 <a href="https://github.com/Kitware/CMake/blob/master/Modules/Platform/Generic.cmake"><code>Generic</code></a>，因为内核实际上并不是任何操作系统上的应用程序，设置为 <code>Generic</code> 会让 CMake 不对内核的运行环境做任何假设，因此做更少的构建行为配置。其实这里设置这两个变量的实际用处不算大，因为相关子项目中已经对链接选项进行了配置，且都不会链接 C 标准库、系统中安装的第三方库等，之所以设置主要是为了保持优雅。</p>
<h2>配置系统</h2>
<p>配置系统是 ChCore 新构建系统的精髓之一，与 ChCore 架构本身没有什么关系，不需要了解 ChCore Lab 也可以看看。</p>
<h3><code>config.cmake</code> 和 <code>.config</code> 文件</h3>
<p>从用户（ChCore 的开发者和构建者）角度来看，新的配置系统对外表现为两个部分，分别是层级的 <code>config.cmake</code> 配置声明文件和根目录的 <code>.config</code> 配置文件。</p>
<p>层级的 <code>config.cmake</code> 配置声明文件与 Linux 内核的 <a href="https://github.com/torvalds/linux/blob/master/Kconfig"><code>Kconfig</code></a> 文件类似：</p>
<pre class="highlight"><code>.
├── kernel
│   └── config.cmake
├── userland
│   └── config.cmake
└── config.cmake
</code></pre>

<p>从根目录 <code>config.cmake</code> 开始的每一级 <code>config.cmake</code> 中，可通过 <code>chcore_config_include</code> 命令包含下一级 <code>config.cmake</code> 文件，形成树状结构；通过 <code>chcore_config</code> 命令声明该层级的配置项，每个配置项包括名称、类型、默认值和描述四项内容。例如根目录 <code>config.cmake</code> 部分内容如下：</p>
<pre class="highlight"><code class="language-cmake"># config.cmake

chcore_config(CHCORE_CROSS_COMPILE STRING &quot;&quot; &quot;Prefix for cross compiling toolchain&quot;)
chcore_config(CHCORE_PLAT STRING &quot;&quot; &quot;Target hardware platform&quot;)
chcore_config(CHCORE_VERBOSE_BUILD BOOL OFF &quot;Generate verbose build log?&quot;)

chcore_config_include(kernel/config.cmake)
chcore_config_include(userland/config.cmake)
</code></pre>

<p>这里 <code>chcore_config_include</code> 命令比较简单，实际上是一个内部调用 CMake 内置 <code>include</code> 命令的宏：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/Modules/CommonTools.cmake

macro(chcore_config_include _config_rel_path)
    include(${CMAKE_CURRENT_LIST_DIR}/${_config_rel_path})
endmacro()
</code></pre>

<p><code>chcore_config</code> 命令则稍微复杂一些，是配置系统的核心，运用了一些技巧，下个小节会详细说明。</p>
<p>根目录的 <code>.config</code> 配置文件是单个扁平的文件，与 Linux 内核的 <code>.config</code> 文件类似，形如：</p>
<pre class="highlight"><code class="language-ini"># .config

CHCORE_CROSS_COMPILE:STRING=aarch64-linux-gnu-
CHCORE_PLAT:STRING=raspi3
CHCORE_VERBOSE_BUILD:BOOL=OFF
</code></pre>

<p>用户可以通过 <code>./chbuild defconfig</code> 生成默认的 <code>.config</code> 文件，其中包含目前所声明的所有配置项的默认值，也可以通过 <code>./chbuild menuconfig</code> 或者手动编辑该文件来修改配置项的值。在构建时，构建系统会读取该配置文件中的值，并设置到 CMake cache 变量，从而控制构建行为。</p>
<h3>配置的加载</h3>
<p>加载 <code>.config</code> 文件应该在 ChCore 根项目的 configure 阶段开始之前完成，因为 configure 阶段即运行 <code>CMakeLists.txt</code> 时，已经需要使用配置值。一个 naive 的思路是直接在 <code>chbuild</code> 脚本中读取并解析其内容，将解析出的 <code>(key, type, value)</code> 三元组构造成 CMake <code>-D</code> 参数序列，例如 <code>-DCHCORE_CROSS_COMPILE:STRING=aarch64-linux-gnu-</code>。如果只是单纯读取用户已经填写的配置，这个思路是可行的，但我不想满足于此，我希望实现：</p>
<ul>
<li>对于 <code>config.cmake</code> 中声明了，但 <code>.config</code> 中没有填写的配置项，根据情况采取三种不同的策略来处理，分别是：<ul>
<li>使用默认值：直接将配置值设为 <code>config.cmake</code> 中声明的默认值</li>
<li>交互式询问用户：在命令行询问用户是否需要使用默认值，若不使用，则要求输入一个值</li>
<li>中断构建流程：直接停止构建</li>
</ul>
</li>
<li>对于 <code>.config</code> 中填了，但实际上没在任何 <code>config.cmake</code> 中声明的配置项（可能是已经删除的旧配置项），过滤掉，不传入子项目</li>
<li>尽量少地编写 shell 脚本，因为 shell 脚本比 CMake 脚本更容易写错、更难维护</li>
</ul>
<p>经过一番搜寻，我发现 CMake 的 <a href="https://cmake.org/cmake/help/latest/manual/cmake.1.html#cmdoption-cmake-C">initial cache</a> 功能可以用来实现这些要求。该功能允许通过 <code>cmake</code> 命令的 <code>-C</code> 参数指定一个 CMake 脚本，并在 configure 之前首先运行这个脚本，以填充 CMake cache，也就是设置一系列 cache 变量。在 initial cache 脚本中，可以使用完整的 CMake 语法，也就是说，可以通过 <code>include</code> 包含其它 CMake 脚本、通过 <code>file(READ ...)</code> 读取文件内容、通过 <code>macro</code>/<code>function</code> 定义宏/函数等。</p>
<p>于是，我决定利用这个功能，在 initial cache 脚本中加载 <code>.config</code> 文件。这带来的另外一个好处是，在 <code>chbuild</code> 脚本中只需切换 <code>-C</code> 参数的值，就可以很方便地切换配置加载策略，如下：</p>
<pre class="highlight"><code class="language-bash"># chbuild

cmake_script_dir=&quot;scripts/build/cmake&quot;
cmake_init_cache_default=&quot;$cmake_script_dir/LoadConfigDefault.cmake&quot;
cmake_init_cache_ask=&quot;$cmake_script_dir/LoadConfigAsk.cmake&quot;

_config_default() {
    cmake -B $cmake_build_dir -C $cmake_init_cache_default
}

_config_ask() {
    cmake -B $cmake_build_dir -C $cmake_init_cache_ask
}

menuconfig() {
    _check_config_file
    _config_default # 采用“使用默认值”策略加载配置并 configure 根项目
    # ...
}

build() {
    _check_config_file
    _config_ask # 采用“交互式询问用户”策略加载配置并 configure 根项目
    # ...
}
</code></pre>

<p>具体的 initial cache 文件如下：</p>
<pre class="highlight"><code>scripts/build/cmake
├── LoadConfig.cmake
├── LoadConfigDefault.cmake
├── LoadConfigAsk.cmake
├── LoadConfigAbort.cmake
└── DumpConfig.cmake
</code></pre>

<p><code>LoadConfigDefault.cmake</code>、<code>LoadConfigAsk.cmake</code>、<code>LoadConfigAbort.cmake</code> 分别实现了使用默认值、交互式询问用户、中断构建流程三种配置加载策略，<code>LoadConfig.cmake</code> 则是它们的通用部分。</p>
<p><code>DumpConfig.cmake</code> 是一个特殊的 initial cache，用于把 CMake cache 中的配置值同步回 <code>.config</code>。之所以需要 <code>DumpConfig.cmake</code>，是因为在通过“使用默认值”或“交互式询问用户”策略加载配置后，CMake cache 中可能包含 <code>.config</code> 所没有填写的配置，需要把这些配置同步到 <code>.config</code>，以保证 <code>.config</code> 始终反映构建系统实际使用的配置。</p>
<p>下面着重介绍 <code>LoadConfigDefault.cmake</code> 和 <code>DumpConfig.cmake</code>，其它 initial cache 只是略有不同。</p>
<p>首先看 <code>LoadConfigDefault.cmake</code>：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/LoadConfigDefault.cmake

macro(chcore_config _config_name _config_type _default _description)
    if(NOT DEFINED ${_config_name})
        # config is not in `.config`, set default value
        set(${_config_name}
            ${_default}
            CACHE ${_config_type} ${_description})
    endif()
endmacro()

include(${CMAKE_CURRENT_LIST_DIR}/LoadConfig.cmake)
</code></pre>

<p>它首先定义了 <code>chcore_config</code> 宏，行为是，当 <code>${_config_name}</code> 也就是配置名称所对应的 CMake cache 变量不存在时，设置该 cache 变量为配置项所声明的默认值。还记得在 <code>config.cmake</code> 文件中声明配置项的时候使用的 <code>chcore_config</code> 命令吗，<code>config.cmake</code> 中传入的配置名称、类型、默认值、描述四个参数，就是这个宏的四个参数。不过，我们并不能说 <code>config.cmake</code> 中用的就是这里定义的宏，后面你会逐渐理解这一点。</p>
<p>随后它 include 了 <code>LoadConfig.cmake</code>，该文件主要内容如下：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/LoadConfig.cmake

# 第一块
if(EXISTS ${CMAKE_SOURCE_DIR}/.config)
    # Read in config file
    file(READ ${CMAKE_SOURCE_DIR}/.config _config_str)
    string(REPLACE &quot;\n&quot; &quot;;&quot; _config_lines &quot;${_config_str}&quot;)
    unset(_config_str)

    # Set config cache variables
    foreach(_line ${_config_lines})
        if(${_line} MATCHES &quot;^//&quot; OR ${_line} MATCHES &quot;^#&quot;)
            continue()
        endif()
        string(REGEX MATCHALL &quot;^([^:=]+):([^:=]+)=(.*)$&quot; _config &quot;${_line}&quot;)
        if(&quot;${_config}&quot; STREQUAL &quot;&quot;)
            message(FATAL_ERROR &quot;Invalid line in `.config`: ${_line}&quot;)
        endif()
        set(${CMAKE_MATCH_1}
            ${CMAKE_MATCH_3}
            CACHE ${CMAKE_MATCH_2} &quot;&quot; FORCE)
    endforeach()
    unset(_config_lines)
else()
    message(WARNING &quot;There is no `.config` file&quot;)
endif()

# 第二块
# Check if there exists `chcore_config` macro, which will be used in
# `config.cmake`
if(NOT COMMAND chcore_config)
    message(FATAL_ERROR &quot;Don't directly use `LoadConfig.cmake`&quot;)
endif()

# 第三块
macro(chcore_config _config_name _config_type _default _description)
    if(DEFINED ${_config_name})
        # config is in `.config`, set description
        set(${_config_name}
            ${${_config_name}}
            CACHE ${_config_type} ${_description} FORCE)
    else()
        # config is not in `.config`, use previously-defined chcore_config
        # Note: use quota marks to allow forwarding empty arguments
        _chcore_config(&quot;${_config_name}&quot; &quot;${_config_type}&quot; &quot;${_default}&quot;
                       &quot;${_description}&quot;)
    endif()
endmacro()

# 第四块
# Include the top-level config definition file
include(${CMAKE_SOURCE_DIR}/config.cmake)
</code></pre>

<p>第一块是在加载和解析 <code>.config</code> 文件，比较直白。首先读取文件内容，然后用正则从每一行中提取配置名称、类型、值三元组，通过 <a href="https://cmake.org/cmake/help/latest/command/set.html#set-cache-entry"><code>set(... CACHE ... FORCE)</code></a> 设置为 cache 变量。此时 <code>.config</code> 中的所有配置都已经进入了 CMake cache。</p>
<p>第二块检查是否定义了 <code>chcore_config</code> 命令。这是为了避免不小心在 <code>chbuild</code> 中直接使用 <code>LoadConfig.cmake</code> 作为 initial cache，要求必须在 <code>LoadConfigDefault.cmake</code> 等文件中定义了 <code>chcore_config</code> 宏后再 <code>include(LoadConfig.cmake)</code>。</p>
<p>第三块定义了一个新的 <code>chcore_config</code> 宏。这里运用了 <a href="https://youtu.be/bsXLMQ6WgIk?t=52m38s">一个 CMake 技巧</a>，当重复定义宏/函数时，旧的宏/函数名称会被加上下划线。也就是说，定义了新的 <code>chcore_config</code> 之后，可以通过 <code>_chcore_config</code> 调用到上一次（在 <code>LoadConfigDefault.cmake</code> 中）定义的 <code>chcore_config</code>。这个宏的作用是，在后面 include 根目录的 <code>config.cmake</code> 时，如果配置名称对应的 cache 变量已经定义（也就是出现在 <code>.config</code> 中了），则为其设置变量描述（description），否则调用先前定义的 <code>chcore_config</code>，也就是执行 <code>LoadConfigDefault.cmake</code> 中设置 cache 变量为默认值的逻辑。之所以要设置 cache 变量的描述，是为了在之后的 <code>menuconfig</code> 中显示声明配置项时的描述。</p>
<p>第四块是包含（也就是执行）根目录的 <code>config.cmake</code> 文件，该文件进而会递归地通过 <code>chcore_config_include</code> 包含到所有的 <code>config.cmake</code>，并调用上面第三块中定义的 <code>chcore_config</code> 宏。根据前面已经说明的逻辑，该过程中，遇到 <code>.config</code> 中已填写的配置项时，会设置 cache 变量的描述，遇到没有填写的配置项时，会设置 cache 变量为所声明的默认值。</p>
<p>再来看 <code>DumpConfig.cmake</code>：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/DumpConfig.cmake

set(_config_lines)
macro(chcore_config _config_name _config_type _default _description)
    # Dump config lines in definition order
    list(APPEND _config_lines
         &quot;${_config_name}:${_config_type}=${${_config_name}}&quot;)
endmacro()

include(${CMAKE_SOURCE_DIR}/config.cmake)

string(REPLACE &quot;;&quot; &quot;\n&quot; _config_str &quot;${_config_lines}&quot;)
file(WRITE ${CMAKE_SOURCE_DIR}/.config &quot;${_config_str}\n&quot;)
</code></pre>

<p>这个 initial cache 不需要包含 <code>LoadConfig.cmake</code>，而只需要定义一个 <code>chcore_config</code>，然后直接包含根目录 <code>config.cmake</code>。这里的逻辑是把所有声明的配置项在 CMake cache 中实际设置的值 append 到 <code>_config_lines</code>，随后写入 <code>.config</code> 文件。其实通过 <code>cmake -B build -L -N | grep ^CHCORE_ &gt; .config</code> 命令可以更快地做到这件事，但无法保留配置项声明的顺序，对用户不是很友好。</p>
<p>到这里，如果你经常写 C 语言，尤其经常写宏的话，应该已经明白 <code>config.cmake</code> 文件其实应用了类似 C 语言中的 <a href="https://en.wikibooks.org/wiki/C_Programming/Preprocessor_directives_and_macros#X-Macros">X-Macros</a> 技巧。通过定义不同的 <code>chcore_config</code> 命令，再 include 根目录 <code>config.cmake</code>，实现了同一组 <code>config.cmake</code> 文件在不同地方 include 时产生不同的行为。</p>
<h3>配置的传递</h3>
<p>配置加载后首先进入根项目的 cache，由于各子项目都是独立的“外部”CMake 项目，不能直接访问根项目的 cache 变量，因此根项目还需要在添加子项目时传递配置内容。为了收集所有配置内容，以便在 <code>chcore_add_subproject</code> 时传入，再次使用了 X-Macro 技巧，将所有配置名称、类型和配置值拼成 <code>-D</code> 参数序列，放到 <code>_cache_args</code> 变量中：</p>
<pre class="highlight"><code class="language-cmake"># CMakeLists.txt

# Construct cache args list for subprojects (kernel, libchcore, etc)
macro(chcore_config _config_name _config_type _default _description)
    if(NOT DEFINED ${_config_name})
        message(FATAL_ERROR &quot;...&quot;)
    endif()
    list(APPEND _cache_args
         -D${_config_name}:${_config_type}=${${_config_name}})
endmacro()
include(${CMAKE_CURRENT_SOURCE_DIR}/config.cmake)
</code></pre>

<p>这里定义 <code>chcore_config</code> 并 <code>include(config.cmake)</code>，而不是遍历所有 <code>CHCORE_</code> 开头的 cache 变量，是为了实现前面所希望的，过滤掉 <code>.config</code> 中填写了、但实际已不在任何 <code>config.cmake</code> 中声明的配置项。如果不需要过滤，也可以采用类似下面 <code>chcore_dump_chcore_vars</code> 函数的方式（<a href="https://cmake.org/cmake/help/latest/prop_dir/VARIABLES.html"><code>VARIABLES</code></a> 改成 <a href="https://cmake.org/cmake/help/latest/prop_dir/CACHE_VARIABLES.html"><code>CACHE_VARIABLES</code></a>）：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/Modules/CommonTools.cmake

function(chcore_dump_chcore_vars)
    get_cmake_property(_variable_names VARIABLES)
    list(SORT _variable_names)
    foreach(_variable_name ${_variable_names})
        string(REGEX MATCH &quot;^CHCORE_&quot; _matched ${_variable_name})
        if(NOT _matched)
            continue()
        endif()
        message(STATUS &quot;${_variable_name}: ${${_variable_name}}&quot;)
    endforeach()
endfunction()
</code></pre>

<p>把所有配置项拼成 <code>-D</code> 参数序列后，在 <code>chcore_add_subproject</code> 时通过 <code>CMAKE_CACHE_ARGS</code> 属性即可传入子项目：</p>
<pre class="highlight"><code class="language-cmake"># CMakeLists.txt

chcore_add_subproject(
    libchcore
    # ...
    CMAKE_CACHE_ARGS ${_cache_args})

chcore_add_subproject(
    userland
    # ...
    CMAKE_CACHE_ARGS ${_cache_args})

chcore_add_subproject(
    kernel
    # ...
    CMAKE_CACHE_ARGS ${_cache_args})
</code></pre>

<p>这样，所有配置项就已经进入了子项目的 cache，也就是可以在子项目的 CMake 脚本中访问，例如：</p>
<pre class="highlight"><code class="language-cmake"># kernel/CMakeLists.txt

if(CHCORE_KERNEL_TEST)
    add_subdirectory(tests)
endif()
</code></pre>

<p>但这还不够，我希望把这些配置传递给 C 代码，从而可以通过 <code>#ifdef</code> 等预处理指令来进行条件编译：</p>
<pre class="highlight"><code class="language-c">#ifdef CHCORE_KERNEL_TEST
    some_test();
#endif /* CHCORE_KERNEL_TEST */
</code></pre>

<p>旧系统中，这是通过各子项目独立添加 definition 实现的，可维护性非常差。新系统则在 CMake 工具链文件中实现：</p>
<pre class="highlight"><code class="language-cmake"># scripts/build/cmake/Toolchains/_common.cmake

# Convert config items to compile definition
get_cmake_property(_cache_var_names CACHE_VARIABLES)
foreach(_var_name ${_cache_var_names})
    string(REGEX MATCH &quot;^CHCORE_&quot; _matched ${_var_name})
    if(NOT _matched)
        continue()
    endif()
    get_property(
        _var_type
        CACHE ${_var_name}
        PROPERTY TYPE)
    if(_var_type STREQUAL BOOL)
        # for BOOL, add definition if ON/TRUE
        if(${_var_name})
            add_compile_definitions(${_var_name})
        endif()
    elseif(_var_type STREQUAL STRING)
        # for STRING, always add definition with string literal value
        add_compile_definitions(${_var_name}=&quot;${${_var_name}}&quot;)
    endif()
endforeach()

# Set CHCORE_ARCH_XXX and CHCORE_PLAT_XXX compile definitions
string(TOUPPER ${CHCORE_ARCH} _arch_uppercase)
string(TOUPPER ${CHCORE_PLAT} _plat_uppercase)
add_compile_definitions(CHCORE_ARCH_${_arch_uppercase}
                        CHCORE_PLAT_${_plat_uppercase})
</code></pre>

<p>这里首先遍历所有 <code>CHCORE_</code> 开头的 cache 变量，如果类型是 <code>BOOL</code>，则根据其真值决定要不要添加同名的 definition，也就是可以在 C 代码里通过 <code>#ifdef</code> 判断其真值；如果类型是 <code>STRING</code> 则一定会添加该 definition，值是配置值字符串。举个例子，<code>.config</code> 中的配置 <code>CHCORE_PLAT:STRING=raspi3</code> 和 <code>CHCORE_KERNEL_TEST:BOOL=ON</code> 在此处产生的效果相当于下面 C 预处理指令：</p>
<pre class="highlight"><code class="language-c">#define CHCORE_PLAT &quot;raspi3&quot;
#define CHCORE_KERNEL_TEST
</code></pre>

<p>为了在代码中更方便地判断当前处理器架构和硬件平台（因为 <code>#if</code> 无法对字符串进行比较），对 <code>CHCORE_ARCH</code> 和 <code>CHCORE_PLAT</code> 不仅定义了字符串，还定义了表示具体架构和平台的空 definition。比如在 AArch64 架构和树莓派 3 平台，这里添加的 definition 相当于：</p>
<pre class="highlight"><code class="language-c">#define CHCORE_ARCH &quot;aarch64&quot;
#define CHCORE_PLAT &quot;raspi3&quot;
#define CHCORE_ARCH_AARCH64
#define CHCORE_PLAT_RASPI3
</code></pre>

<h3><code>menuconfig</code> 子命令</h3>
<p>配置系统的另一个需求是让 <code>./chbuild menuconfig</code> 子命令实现类似 Linux 内核 <code>make menuconfig</code> 的 TUI 配置面板。由于已经全面采用了 CMake cache 变量和 initial cache 功能，一个自然的想法是复用 <code>ccmake</code> 命令。</p>
<p>这里其实有一些不够优雅的地方，因为在 <code>ccmake</code> 提供的配置面板中，需要按 C 键（Configure）来把修改的配置值刷到 CMake cache 中，也就是保存配置。这和一般直觉中的 S 键（Save）不同，但是没有找到好的修改办法，只能在运行 <code>ccmake</code> 命令之前输出一些红字提示用户。最终实现如下：</p>
<pre class="highlight"><code class="language-bash"># chbuild

cmake_init_cache_dump=&quot;$cmake_script_dir/DumpConfig.cmake&quot;

_sync_config_with_cache() {
    cmake -N -B $cmake_build_dir -C $cmake_init_cache_dump &gt;/dev/null
}

menuconfig() {
    _check_config_file
    _config_default

    echo
    _echo_warn &quot;Note: In the menu config view, press C to save, Q to quit.&quot;
    read -p &quot;Now press Enter to continue...&quot;

    ccmake -B $cmake_build_dir # 复用 ccmake 提供的 TUI 配置面板
    _sync_config_with_cache # 同步 CMake cache 回 .config
    _echo_succ &quot;Config saved to \`$config_file\` file.&quot;
}
</code></pre>

<p>前面提到 <code>DumpConfig.cmake</code> initial cache 用于把 CMake cache 中的配置同步回 <code>.config</code> 文件。这里用户在 <code>ccmake</code> TUI 面板中修改配置后，也需要进行这个同步操作，才能把修改反映到 <code>.config</code>。</p>
<h2>总结</h2>
<p>尽管构建系统和代码本身其实没有很大的直接关系，但我相信一个优雅的构建系统仍然非常重要，因为它会极大地影响开发者的体验。一个优质的构建系统可以让开发者更方便、更舒适地为系统扩充功能。</p>
<p>在重写 ChCore 构建系统时，我的理念是在入口层面提供与 Linux 内核相似的体验，而下面的实现则尽量充分利用 CMake 的一切可利用的特性，并遵循现代 CMake 的最佳实践，最终效果基本达到了我理想的状态。</p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2023/01/07/chbuild/</guid>
      <pubDate>Sat, 07 Jan 2023 23:59:00 +0806</pubDate>
    </item>
    <item>
      <title>租房经验总结</title>
      <link>https://stdrc.cc/post/2023/04/04/renting-house/</link>
      <description><![CDATA[<p>最近由于毕业以及之前的房子到期，于是在距离公司稍近的地方租了个新的房子。至此，从本科时期到现在已经租了四年、八间次房子了。其中，有房东直租的、有经过中介的、有二房东的、有酒店长租、有车库改造的、有居民小区、有商用住宅，踩过不少坑，这次想整理一些经验，以便以后查阅和供读者参考。</p>
<h2>性质</h2>
<p>根据租房的性质不同，找房、签合同的方式和随后会遇到的坑不同：</p>
<p><strong>房东直租</strong>：优点是不需要中介费，直接和房东签合同；缺点是不好找，现在很少有房东直租的房源，并且没有中介担保签合同，可能比较看房东人品。但总体来说如果看房的时候能判断房东人怎么样的话，这种是最优的。</p>
<p><strong>房产中介</strong>：优点是去感兴趣的小区在门口随便找个链家、我爱我家或者什么其它 XX 房产/地产，他们就会把附近小区现在能租的房子都带你看一遍，很方便；缺点是需要半个月或一个月房租（不同地区可能不同）的中介费。</p>
<p><strong>二房东</strong>：这有两种，一种是当地一些职业二房东（其中可能一些人看起来不太正经，像地痞流氓），另一种是房产公司做二房东的。前一种我没租过，但看过房，装修看起来都不错，但二房东人看起来非常不靠谱，看起来就不像是能要回押金的，遂放弃；后一种也比较坑，东西出了问题可能不会及时修，以及可能有粗暴管理的情况。自如的整租合租等（非自如寓）和链家的某些房子属于后一种，相对比本地不知名房产公司会稍微靠谱一点，但问题仍然存在。</p>
<p><strong>公寓</strong>：自如寓、城家公寓和其它各类公寓品牌，优点是房型比较固定，管理比较统一、规范，缺点是价格偏贵，并且附近可能缺乏生活气息，比较类似酒店。</p>
<p><strong>酒店</strong>：通过合理用券、用会员、谈协议价等方式可以用较低价格租到酒店长租。优点是方便、规范，不需要交水电网费，领包入住，每天有人打扫卫生；缺点是房间相比同价格的其它类型租房会小非常多，没有生活气息，环境不稳定，有时候晚上会有噪音。</p>
<h2>费用</h2>
<p><strong>中介费</strong>：非房东直租的会有，一般为半个月或一个月房租。</p>
<p><strong>服务费</strong>：公寓类型的会有，比如自如，以一定百分比的形式附加在每月房租上。</p>
<p><strong>押金</strong>：自如有毕业生免押金，除此之外基本都需要交，一般为一个月房租。</p>
<p><strong>房租</strong>：</p>
<ul>
<li>一个同一个地段的面积差不多的房子房租都差不太多，根据装修不同会有浮动，一般可以让中介或者自己跟房东谈（直租的情况），都可以比标价便宜小几百块钱</li>
<li>可以留意房子的空窗时间、地段热门程度，感觉上这可以用来判断底价</li>
<li>付费周期比较多的是押一付三，也就是一次付三个月，也有一些会支持押一付一</li>
</ul>
<p><strong>水电费</strong>：</p>
<ul>
<li>民用住宅和商用住宅的水电费差异较大，商业用电 1 块钱 1 度还是有点猛的</li>
<li>电费一般在支付宝或微信填户号就可以缴，但也一些可能是需要手动缴费给房东，这种可能房东会自己定价，高于一般价格（可能一些公寓是这样的）</li>
<li>水费一般民用住宅也可以支付宝微信缴，但见过需要去物业充值水卡的类型，比较麻烦</li>
<li>不建议租一个房子多个租客但水电费户号只有一个的那种，虽然一般会有分水电表，但总和起来往往会有偏差（以及公共区域消耗），导致每个人都觉得自己没多用，但最后停电了，容易起争执</li>
</ul>
<p><strong>天然气费</strong>：居民小区一般会有天然气，便于做饭，但商用住宅似乎没见过。缴费一般也是支付宝。</p>
<p><strong>宽带费</strong>：公寓一般会提供网络，其它一般需要自己去营业厅办（也见过商用住宅需要去物业办的），协议一般一年起步，假如要搬家，建议移机而不是重新办旧的不管了。</p>
<h2>看房考虑因素</h2>
<p>除了上面最重要的性质和费用因素，下面列出了从我个人的角度比较会关注的看房需要考虑的因素。对照清单可以避免看房的时候遗漏，一般正常小区正常装修的房子，大部分应该是没问题的，主要还是装修风格和房型比较值得比较。</p>
<h3>外部</h3>
<ul>
<li>楼层高度</li>
<li>有无电梯</li>
<li>离地铁站、公交站距离</li>
<li>地铁、公交早晚高峰的拥挤程度</li>
<li>小区环境</li>
<li>垃圾投放点距离</li>
<li>快递柜有无、距离</li>
<li>餐饮店密度、距离</li>
<li>附近住户成分（影响安全性和“烟火气”）</li>
<li>窗外是否有中小学、酒吧等吵闹场所</li>
<li>房东是否好说话</li>
</ul>
<h3>通用</h3>
<ul>
<li>装修风格</li>
<li>房间划分是否合理</li>
<li>门把手是否破损</li>
<li>门窗是否严实（影响空调效果和隔音）</li>
<li>窗户大小和朝向（采光）</li>
<li>纱窗是否干净、是否破损</li>
<li>窗帘是否干净、遮光效果</li>
<li>阳台有无、大小</li>
<li>房间隔音效果（合租需要考虑，警惕玻璃墙，隔音极差）</li>
<li>墙面和天花板状态（一些老破小会掉墙皮甚至漏水，漏水一般出现在顶层）</li>
<li>灯的亮度</li>
<li>灯具开关位置是否合理</li>
<li>空调是否太旧、是否干净</li>
<li>插座数量、位置、安全性</li>
<li>可利用的家具/设施数量</li>
<li>各类储物空间大小</li>
<li>有无蟑螂或其它虫子</li>
</ul>
<h3>浴室 &amp; 卫生间</h3>
<ul>
<li>是否干湿分离</li>
<li>地漏下水速度</li>
<li>花洒出水效果</li>
<li>毛巾架有无、位置</li>
<li>沐浴用品架</li>
<li>热水器容量（超过一个人住就要考虑热水器容量，最好还是选有天然气的）</li>
<li>浴霸 or 暖风机</li>
<li>排风扇</li>
<li>浴室门情况（木门警惕底部烂掉、玻璃门注意安全性、移动门注意是否阻塞）</li>
<li>洗脸池水龙头是否接热水</li>
<li>洗脸池台面是否干净</li>
<li>洗脸池高度和空间是否合适</li>
<li>镜子大小、干净程度</li>
<li>马桶座圈、上水、冲水是否正常</li>
<li>马桶和洗手池是否有异味或异物</li>
<li>洗衣机（滚筒 or 波轮）</li>
<li>晾衣服便捷性（阳台、室内/外晾衣架）</li>
</ul>
<h3>厨房</h3>
<ul>
<li>天然气 or 电磁炉</li>
<li>油烟机</li>
<li>水池排水</li>
<li>油烟隔离性（不污染客厅和卧室）</li>
<li>冰箱</li>
</ul>
<h3>客厅 &amp; 餐厅</h3>
<ul>
<li>空间大小（是否有足够的瑜伽垫空间）</li>
<li>沙发大小、是否干净</li>
<li>电视有无、效果（垃圾电视不如没有，否则占空间）</li>
<li>餐桌和椅子（够用即可，太大浪费）</li>
</ul>
<h3>卧室 &amp; 书房</h3>
<ul>
<li>空间大小（坐在电脑桌前不拥挤）</li>
<li>床的大小、高度、稳定性</li>
<li>床头柜不宜太占空间</li>
<li>空调位置（不要对着床吹）</li>
<li>桌子有无、大小、稳定性</li>
<li>衣柜容量、质量</li>
</ul>
<h2>入住准备</h2>
<p>搬进新家往往需要购置非常多的生活用品，建议一次性在京东上买好，同一天全到达，然后就可以立即入住，且不会因为缺什么东西而反复跑超市。</p>
<h3>清洁用品</h3>
<p>第一天可能需要先打扫卫生（如果不打算请家政服务的话），需要下面物资：</p>
<ul>
<li>扫帚</li>
<li>拖把</li>
<li>乳胶手套（否则高强度使用清洁剂会伤手）</li>
<li>洗手液</li>
<li>清洁剂</li>
<li>抹布 5 条以上</li>
<li>刷子 1～2 个</li>
<li>剪刀、快递刀</li>
<li>抽纸</li>
<li>湿纸巾（一定场景可以代替抹布，比较方便）</li>
<li>垃圾袋（建议买可以手提的）</li>
<li>垃圾桶 2 个以上</li>
<li>洁厕宝</li>
<li>洗衣液、消毒液（可能需要洗沙发套，顺便也试试洗衣机能不能工作）</li>
<li>矿泉水（别忘了打扫卫生的时候自己需要喝水）</li>
</ul>
<h3>生活用品</h3>
<p>打扫完之后，入住新房，正常生活一般需要下面物资（根据个人生活习惯不同应该有很大不同，可以先列好然后一次性搬好、买好）（有一些已经包含在上面清洁用品清单了，另一些可能房子里本来就有）：</p>
<ul>
<li>鞋架</li>
<li>遥控器电池</li>
<li>沐浴用品架/篮</li>
<li>牙膏沐浴露洗发水等洗浴用品</li>
<li>牙刷</li>
<li>漱口杯</li>
<li>沐浴球</li>
<li>指甲剪</li>
<li>梳子</li>
<li>毛巾/浴巾</li>
<li>吹风机</li>
<li>拖鞋</li>
<li>洗手液/香皂+肥皂架</li>
<li>洗衣液</li>
<li>消毒液</li>
<li>洗衣袋</li>
<li>脏衣篮</li>
<li>衣架（要考虑晾衣服和放衣柜的量，以及袜子内裤等需要带夹子的）</li>
<li>晾衣架（用来挂衣服的）</li>
<li>樟脑丸/香樟球</li>
<li>防螨垫</li>
<li>抹布</li>
<li>刷子</li>
<li>夹子（晾衣服、床单时夹住）</li>
<li>挂钩（按需购买）</li>
<li>剪刀、快递刀</li>
<li>胶带、双面胶（可能用得着）</li>
<li>抽纸、厕纸</li>
<li>垃圾桶（每个卧室一个、客厅一个、厨房一个）</li>
<li>垃圾袋</li>
<li>洁厕宝</li>
<li>扫帚、拖把</li>
<li>电蚊香</li>
<li>花露水</li>
<li>插线板</li>
<li>电热水壶</li>
<li>凉水壶（也可以直接买大瓶矿泉水）</li>
<li>水杯</li>
<li>床垫、床单、被子、枕头（特别注意“三件套”不包含被芯和枕芯，床单要买大于床的尺寸，iOS 内置测距仪 app 可用于测量床大小）</li>
<li>书桌/电脑桌、椅子</li>
<li>路由器、网线</li>
<li>耳塞、眼罩</li>
<li>口罩</li>
</ul>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2023/04/04/renting-house/</guid>
      <pubDate>Tue, 04 Apr 2023 01:23:00 +0806</pubDate>
    </item>
    <item>
      <title>RisingWave 窗口函数：滑动的艺术与对称的美学</title>
      <link>https://stdrc.cc/post/2023/09/22/risingwave-window-function/</link>
      <description><![CDATA[<blockquote>
<p>本文发表于 <a href="https://mp.weixin.qq.com/s/rgJTR6Ynn8FmkfvCAQZIwA">RisingWave 中文开源社区</a>。</p>
</blockquote>
<p><strong>窗口函数</strong>（Window Function）是数据库和流处理中一项非常常用的功能，该功能可用于对每一行输入数据计算其前后一定窗口范围内的数据的聚合结果，或是获取输入行的前/后指定偏移行中的数据。在其他一些流系统中，窗口函数功能也被称作“Over Aggregation”<sup id="fnref:flink-doc-over-agg"><a class="footnote-ref" href="#fn:flink-doc-over-agg">1</a></sup>。RisingWave 在此前的 1.1 版本中加入了窗口函数支持<sup id="fnref:rw-article-1.1"><a class="footnote-ref" href="#fn:rw-article-1.1">2</a></sup>。在 RisingWave 的窗口函数实现中，我们把实施窗口函数计算的算子称为 <strong>OverWindow 算子</strong>，本文将尝试解析 OverWindow 算子的设计与实现。</p>
<h2>基本例子</h2>
<p>首先用两个简单的例子展示窗口函数的基本用法。更完整的语法说明请参考 RisingWave 用户文档<sup id="fnref:rw-doc-wf"><a class="footnote-ref" href="#fn:rw-doc-wf">3</a></sup>。</p>
<p><strong>例 1</strong></p>
<p>下面的例子会持续计算每次股票价格更新事件时，当前价格相比上次更新时的价格差。</p>
<pre class="highlight"><code class="language-sql">CREATE MATERIALIZED VIEW mv AS
SELECT
  stock_id,
  event_time,
  price - LAG(price) OVER (PARTITION BY stock_id ORDER BY event_time) AS price_diff
FROM stock_prices;
</code></pre>

<p>这里使用了 <code>LAG</code> 窗口函数，获得与当前行的 <code>stock_id</code> 相同的行中，按 <code>event_time</code> 排序，排在当前行的前一行的 <code>price</code> 值。与 <code>LAG</code> 相对应的，还有 <code>LEAD</code> 函数，用于获取后一行（按时间排序的话，即更新的一行——更“领先（lead）”的一行）。这类窗口函数我们称之为通用窗口函数（General-Purpose Window Function），与 PostgreSQL 中的概念保持一致<sup id="fnref:pg-doc-wf"><a class="footnote-ref" href="#fn:pg-doc-wf">4</a></sup>。</p>
<p><strong>例 2</strong></p>
<p>下面的例子则对每笔订单，计算该订单的用户在该订单前的 10 笔订单的平均消费金额。</p>
<pre class="highlight"><code class="language-sql">CREATE MATERIALIZED VIEW mv AS
SELECT
  user_id,
  amount,
  AVG(amount) OVER (
    PARTITION BY user_id
    ORDER BY order_time
    ROWS BETWEEN 10 PRECEDING AND 1 PRECEDING
  ) AS recent_10_orders_avg_amount
FROM orders;
</code></pre>

<p>这里使用了 <code>AVG</code> 函数，它实际上是一个聚合函数（Aggregate Function）。在 RisingWave 中，所有聚合函数都可以用作窗口函数，后面跟 <code>OVER</code> 子句指定计算窗口，我们称该类窗口函数为聚合窗口函数（Aggregate Window Function）。同样，这与 PostgreSQL 的概念保持一致<sup id="fnref2:pg-doc-wf"><a class="footnote-ref" href="#fn:pg-doc-wf">4</a></sup>，便于用户快速理解。</p>
<h2>两种输出触发模式</h2>
<p>在此前的文章《深入理解 RisingWave 流处理引擎（三）：触发机制》中<sup id="fnref:rw-article-emit-mode"><a class="footnote-ref" href="#fn:rw-article-emit-mode">5</a></sup>，我们已经介绍了 RisingWave 流计算引擎的两种输出触发模式，包括默认的 Emit-On-Update 和可通过关键字启用的 Emit-On-Window-Close 模式。OverWindow 算子也支持这两种输出模式。</p>
<p><strong>通用模式（Emit-On-Update）</strong></p>
<p>在通用模式下，OverWindow 算子在收到输入变更时，立即从内部状态中找到变更行所影响的行范围，并重新计算该范围内所有行对应的窗口函数结果。</p>
<p>上一节中两个 SQL 例子即是采用通用模式进行计算。</p>
<p><strong>EOWC 模式（Emit-On-Window-Close）</strong></p>
<p>通过在查询中加入 <code>EMIT ON WINDOW CLOSE</code> 关键字<sup id="fnref:rw-doc-eowc"><a class="footnote-ref" href="#fn:rw-doc-eowc">6</a></sup><sup id="fnref:note-eowc"><a class="footnote-ref" href="#fn:note-eowc">7</a></sup>，即可采用 EOWC 输出模式。</p>
<p>在 EOWC 模式下，OverWindow 仅在收到 watermark 时输出 <code>ORDER BY</code> 列和所对应的窗口均被 watermark “淹没”的行。这和我们熟悉的 EOWC 模式下 <code>GROUP BY</code> watermark 列的 HashAgg 算子行为有细微差别，在后者中，收到一个 group 的 watermark，即标志着该 watermark 前的 group 已经“完成”，即可输出；而在 OverWindow 中，需要等待两个条件满足才会输出，首先是 <code>ORDER BY</code> 列的“完成”，即输入行在 watermark 语义上允许下游可见，其次是窗口函数所定义的窗口的“完成”，即输入行所对应窗口的最后一个行也对下游可见。</p>
<p>出于性能考量，我们为通用模式和 EOWC 模式分别编写了两个执行器实现（不过许多代码是复用的），以充分利用两种输出模式的语义特征，下文将对它们进行分别介绍。</p>
<h2>EOWC 版本：滑动的艺术</h2>
<p>EOWC 版本的 OverWindow 算子（后称 EowcOverWindow）的实现算法相比通用版本要稍简单，因此这里先介绍它。</p>
<p>如前所述，EowcOverWindow 要等到一个输入行的 <code>ORDER BY</code> 列“完成”（条件 ①），且其所对应的窗口“完成”（条件 ②），才能输出这个行及其窗口函数计算结果。也就是说，即使窗口函数的 frame 是 <code>ROWS BETWEEN 10 PRECEDING AND 1 PRECEDING</code>，在 <code>CURRENT ROW</code> 的<strong>前一行</strong>的条件 ① 满足时，<code>CURRENT ROW</code> 的条件 ② <strong>看起来</strong>就已经满足，算子仍然要等到 <code>CURRENT ROW</code> 的条件 ① 满足才能输出。我们可以换一个角度来理解，把输出中包含的所有输入列认为是 <code>LAG(?, 0)</code>，进而就可以迅速发现条件 ① 实际上是条件 ② 的前提。</p>
<p>基于这个观察，我们把 EowcOverWindow 实现为两个阶段，对于一个输入行：</p>
<ol>
<li>第一阶段等待条件 ① 满足，满足后把该行释放给第二阶段；</li>
<li>第二阶段等待条件 ② 满足，满足后计算窗口函数结果。</li>
</ol>
<p>窗口函数的实际计算在两个条件都满足后才进行，可以避免大量不必要的无效计算。这与 HashAgg 算子的 EOWC 实现略有不同（后续会有文章介绍），因为 OverWindow 中一行修改会导致多行变更，而 HashAgg 中每个 group 至多有一行修改，前者无论在计算还是 I/O 层面均有明显的放大效应。</p>
<p><strong>第一阶段：SortBuffer</strong></p>
<p>第一阶段是对输入行的一个缓冲，又由于 watermark 的非递减性质，很容易把第一阶段的输出实现为是有序的，因此我们把第一阶段命名为 <strong>SortBuffer</strong>。更进一步，我们引入了一个名为 EowcSort 的算子来解耦 SortBuffer 与第二阶段，使 SortBuffer 可以在其他需要的地方复用。于是，EowcOverWindow 算子以 EowcSort 作为上游，其内部只需对满足条件 ① 的有序输入行实现第二阶段。</p>
<p><strong>第二阶段：滑动窗口</strong></p>
<p>由于条件 ② 满足之后才会进行计算，EowcOverWindow 需要先将输入行按 <code>PARTITION BY</code> 和 <code>ORDER BY</code> 列有序存储在其内部 state table 中。并且，对每个 partition，EowcOverWindow 在内存中维护着当前正在等待窗口完成的 <code>CURRENT ROW</code>（“当前行”）及其对应窗口（“当前窗口”）中的行（该内存结构可以在 recovery 时从 state table 重建）。</p>
<p>当一些输入行从 SortBuffer 进入 EowcOverWindow 时，后者便会找到对应 partition 的上述内存结构，如果其中的“当前窗口”已完成，则输出“当前行”和“当前窗口”上的窗口函数计算结果，并将“当前行”及其窗口滑动到下一行，如此循环直到“当前窗口”不再完成。窗口滑动时，一些最旧的行会被移出“当前窗口”，EowcOverWindow 于是可以把它们从 state table 中清除。</p>
<p>下面，我们通过一个例子来演示上述两个阶段的算法过程。考虑下面的查询<sup id="fnref2:note-eowc"><a class="footnote-ref" href="#fn:note-eowc">7</a></sup>：</p>
<pre class="highlight"><code class="language-sql">CREATE MATERIALIZED VIEW mv AS
SELECT
  SUM(x) OVER (PARTITION BY p ORDER BY ts ROWS 1 PRECEDING),
  SUM(x) OVER (PARTITION BY p ORDER BY ts ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING),
  LEAD(x, 1) OVER (PARTITION BY p ORDER BY ts)
FROM t
EMIT ON WINDOW CLOSE;
</code></pre>

<p>其中，三个窗口函数调用的 <code>PARTITION BY</code> 和 <code>ORDER BY</code> 相同（对于实际场景中不同的情况，优化器首先对查询进行拆分，由多个 OverWindow 算子处理），窗口 frame 不同。另外，<code>ts</code> 列定义了延迟为 5 分钟的 watermark。</p>
<p>在给出算法过程的动画演示之前，先给出动画中几种箭头所表示的含义：</p>
<p><img alt="" src="/static/images/2023-09-22/legend.png" /></p>
<p>现在，可以通过下面的动画理解 EowcOverWindow 的实现算法：</p>
<p><img alt="" src="/static/images/2023-09-22/eowc.gif" /></p>
<h2>通用版本：对称的美学</h2>
<p>相比 EOWC 版本，通用版本的 OverWindow（后称 GeneralOverWindow）看似更加简单粗暴，实际上实现起来是更为复杂的。</p>
<p>在 GeneralOverWindow 中，<code>ORDER BY</code> 列通常没有定义 watermark，于是输入行的 <code>ORDER BY</code> 列的值可能是任意大小的（表现在现实场景中就可能是几天前的数据仍然会被插入、修改或删除）。因此，不同于 EowcOverWindow 始终知道“当前窗口”在哪，GeneralOverWindow 在收到输入行之后，首先需要找到其对应的窗口，然后才能计算窗口函数结果。</p>
<p>例如，考虑上一节最后的查询例子（去掉 <code>EMIT ON WINDOW CLOSE</code> 关键字），假设我们已有如下数据：</p>
<pre class="highlight"><code>ts     pk   x
10:00  100  5
10:02  101  3
10:10  103  9
10:17  104  0
</code></pre>

<p>现在插入了 <code>10:06  102  8</code> 这样一行新数据（修改、删除的情形类似，后续只讨论插入），如下：</p>
<pre class="highlight"><code>ts     pk   x
10:00  100  5
10:02  101  3
10:06  102  8  &lt;-- insert
10:10  103  9
10:17  104  0
</code></pre>

<p>按照所指定的窗口函数 frame，要计算 <code>pk = 102</code> 行的窗口函数结果，需要向前找一行、向后找一行，也就是说，<code>CURRENT ROW</code> 为 <code>102</code> 行的“当前窗口”范围是从 <code>101</code> 行到 <code>103</code> 行。</p>
<p>想到这里，我们立即可以发现，刚刚从新插入的行开始按窗口 frame 向前向后找到的“当前窗口”，仅能产生新插入的行对应的<strong>一行</strong>输出，然而，新插入的行很可能也属于此前已经输出过的其他窗口，从而导致曾经输出过的行需要修改。因此，我们需要改变算法思路，不能把当前插入/修改/删除的行作为 <code>CURRENT ROW</code> 来找窗口，而要把它当作某个窗口 A 的最后一行和另一个窗口 B 的第一行，找到窗口 A 和 B，才能正确为所有受影响的行产生新输出。</p>
<p>同样以刚刚的数据为例，把 <code>102</code> 行当作窗口 A 的最后一行，倒着找，可以找到 A 的 <code>CURRENT ROW</code> 是 <code>101</code> 行，进而找到窗口 A 的第一行是 <code>100</code> 行。这里我们将窗口 A 的第一行 <code>100</code> 行标记为 <code>first_frame_start</code>、<code>CURRENT ROW</code> 即 <code>101</code> 行标记为 <code>first_curr_row</code>。对称地（点题了！），把 <code>102</code> 行当作窗口 B 的第一行，顺着找，可以找到 B 的 <code>CURRENT ROW</code> 是 <code>103</code> 行，进而找到窗口 B 的最后一行是 <code>104</code> 行，和前面类似，分别把它们标记为 <code>last_curr_row</code> 和 <code>last_frame_end</code>。这个过程如下面动画所示：</p>
<p><img alt="" src="/static/images/2023-09-22/general-find-ranges.gif" /></p>
<p>找到 <code>(first_frame_start, first_curr_row, last_curr_row, last_frame_end)</code>（分别对应动画最后的四个横线）这整个受新输入行影响的范围后，只需要复用 EowcOverWindow 第二阶段的代码，即可滑动地计算从 <code>first_curr_row</code> 到 <code>last_curr_row</code> 的新输出结果，如下面动画所示：</p>
<p><img alt="" src="/static/images/2023-09-22/general-calc.gif" /></p>
<div class="footnote">
<hr />
<ol>
<li id="fn:flink-doc-over-agg">
<p>Flink Over Aggregation 文档，<a href="https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/dev/table/sql/queries/over-agg/&#160;">https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/dev/table/sql/queries/over-agg/&#160;</a><a class="footnote-backref" href="#fnref:flink-doc-over-agg" title="Jump back to footnote 1 in the text">&#8617;</a></p>
</li>
<li id="fn:rw-article-1.1">
<p>RisingWave 1.1 版本亮点一览，<a href="https://mp.weixin.qq.com/s/c0VHTebJ3zwiqma2z352VA&#160;">https://mp.weixin.qq.com/s/c0VHTebJ3zwiqma2z352VA&#160;</a><a class="footnote-backref" href="#fnref:rw-article-1.1" title="Jump back to footnote 2 in the text">&#8617;</a></p>
</li>
<li id="fn:rw-doc-wf">
<p>RisingWave 窗口函数文档，<a href="https://docs.risingwave.com/docs/current/window-functions/&#160;">https://docs.risingwave.com/docs/current/window-functions/&#160;</a><a class="footnote-backref" href="#fnref:rw-doc-wf" title="Jump back to footnote 3 in the text">&#8617;</a></p>
</li>
<li id="fn:pg-doc-wf">
<p>PostgreSQL 窗口函数文档，<a href="https://www.postgresql.org/docs/current/functions-window.html&#160;">https://www.postgresql.org/docs/current/functions-window.html&#160;</a><a class="footnote-backref" href="#fnref:pg-doc-wf" title="Jump back to footnote 4 in the text">&#8617;</a><a class="footnote-backref" href="#fnref2:pg-doc-wf" title="Jump back to footnote 4 in the text">&#8617;</a></p>
</li>
<li id="fn:rw-article-emit-mode">
<p>深入理解 RisingWave 流处理引擎（三）：触发机制，<a href="https://mp.weixin.qq.com/s/eQjGEGei9vfrXhAjcRe67w&#160;">https://mp.weixin.qq.com/s/eQjGEGei9vfrXhAjcRe67w&#160;</a><a class="footnote-backref" href="#fnref:rw-article-emit-mode" title="Jump back to footnote 5 in the text">&#8617;</a></p>
</li>
<li id="fn:rw-doc-eowc">
<p>RisingWave Emit-On-Window-Close 文档，<a href="https://docs.risingwave.com/docs/current/emit-on-window-close/&#160;">https://docs.risingwave.com/docs/current/emit-on-window-close/&#160;</a><a class="footnote-backref" href="#fnref:rw-doc-eowc" title="Jump back to footnote 6 in the text">&#8617;</a></p>
</li>
<li id="fn:note-eowc">
<p>由于 EOWC 模式还属于实验性功能，其行为和语法都可能有所变化，例如语法在 1.2 版本发生了一次变化，调整了 <code>EMIT ON WINDOW CLOSE</code> 关键字的位置，在使用时请注意参考所使用版本对应的文档。&#160;<a class="footnote-backref" href="#fnref:note-eowc" title="Jump back to footnote 7 in the text">&#8617;</a><a class="footnote-backref" href="#fnref2:note-eowc" title="Jump back to footnote 7 in the text">&#8617;</a></p>
</li>
</ol>
</div>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2023/09/22/risingwave-window-function/</guid>
      <pubDate>Fri, 22 Sep 2023 19:27:00 +0806</pubDate>
    </item>
    <item>
      <title>2023 仍然还在开始，2024 会走到哪里呢</title>
      <link>https://stdrc.cc/post/2024/01/01/2023-review/</link>
      <description><![CDATA[<p>其实本来刚进入 12 月下旬的时候，我是满怀期待地准备好好总结一下过去的一年的，当时非常自信地觉得今年自己发生了很大的改变、有了很大的进步，可是直到 12 月最后两天，翻看了这一年的日记后，我才又意识到自己在很多方面仍然处在「愚昧之巅」。在这两天，有很多瞬间我不想再整理年终总结了，就像在 3 月的某一个瞬间突然决定不去自己的毕业典礼一样，可是等心情平复之后我知道很多变化和新的理解仍然是值得记录和分享的。</p>
<p>这次我不想再像去年一样按时间和事件来把 2023 年的时间轴切块罗列，而是从一个横切的视角，尝试记录发生在全年的事件给我带来的不同思考和感悟。</p>
<h2>Dating app、人生简历和形象管理</h2>
<blockquote>
<p>我今天彻底理解了一件事：女朋友，首先应该是朋友，如果去掉了爱情，两人就没啥可说的了，去掉了心动，两人就没啥好交流的，那没意思。</p>
<p>我觉得 dating app 的 profile 就是人生简历，你需要展示全面的你，比工作简历还难写，我有时候会改一小块，然后看看最近「喜欢我的人」的增长有什么变化。</p>
</blockquote>
<p>所谓的「dating」是我今年除了工作之外的隐藏主线，虽然说从 2021 年底其实就开始刷 dating app 了，但今年确实和去年的感受有所不同。</p>
<p>首先，我必须帮助怀有「dating app 上的人不是都不正经吗」看法的读者破除对 dating app 的偏见。Dating app 是一大类型的 app 的总和，而不只是一个 app。就像购物网站有大部分都是假货烂货的，也有只卖「官方旗舰店」的或是「严选」的一样，dating app 中有更倾向于擦边的，也有更倾向于认真找对象甚至于像相亲一样直接的。同样，就像购物网站的前一类中也有好东西，后一类中也有假东西，dating app 的前一类中肯定也有正经人，后一类中也有不正经人。所以在 dating app 上认识什么样的人，完全取决于你想找什么样的人、是否用了适当的 app、是否有足够的能力判断对方是什么样的人。</p>
<p>再来说我的感受。在不断的认识、见面、甚至于真正的「dating」的循环中，我不断地调整我的 dating app profile，以期更好地表达自己，展现自己值得展现的一面。很多时候我感到自己没有什么实质性的优势，感到自己所擅长的兴趣爱好很贫瘠，于是我又不断地想去学习、见识更多事物，以此来更新我的 profile。进而我意识到 dating app 的 profile 实际上是一种「人生简历」，它就像找工作的简历一样，区别在于因为在找人生伴侣，所以需要更全面地介绍自己的人生。</p>
<p>尽管很多人会说，长相不重要，但其实在 dating 的逻辑里，照片和第一次见面时的外貌形象具有极其重要的意义。在 2023 年我首次尝试了烫发、染发、夹板、遮瑕膏等，继续用了香水，学习改进了穿搭，虽然最终仍然远远称不上帅，但确实相比去年的形象有了很大改善。</p>
<p>这两年刷 dating app 的经历虽然带给我不少焦虑，但其中很多成为了我提高自己的动力，确确实实地让我进步了很多。除此之外，这些经历也让我更清楚自己<strong>是什么样的人</strong>、自己<strong>喜欢什么样的人</strong>。</p>
<p>然而，我还是会时常困惑自己<strong>想成为什么样的人</strong>。</p>
<h2>我做的事，到底是不是我想做的事？</h2>
<blockquote>
<p>我开始怀疑，我做的那些事情，旅游、听某某的歌、看演唱会、调酒、写 app、学吉他等等等等，到底是我真的喜欢，还是只是为了发朋友圈，为了增加和别人聊天时候的谈资？</p>
</blockquote>
<p>这一年我做了很多事情来开拓自己的视野、扩展自己的兴趣和技能。主要包括读了 18 本书（虽然其中有几本编程相关的书「充数」），看了 54 部影视剧（不算多，且没有完成年初定的 66 部的目标），看了 12 次演出/展览（包括演唱会及其录像、音乐节、脱口秀、美术展），旅行了 5 次左右，入门了一点调酒，入门了一点吉他，开始听更多种类的音乐，工作地点 relocate 到了新加坡，以及一些可能已经忘掉的事。</p>
<p>虽然看起来还算充实，也不乏有人称之为「成功」，可我经常会思索自己做这些到底是为了什么。或许可能源于刷 dating app 产生的焦虑，或许因为需要在朋友圈子里拥有谈资、得到认可，又或许有一些我确实真的喜欢。在这个「扩展见闻」的过程中，我不断地想听到自己内心的声音，想知道自己到底想成为什么样的人、想走什么样的道路，可是很难得出结论，我好像想成为任何人、想走每一条道路。可这显然是贪婪的，是不可能的。什么都想做的结果是什么都只能浅尝辄止。</p>
<p>在和朋友、和自己反复对话后，我对这个问题的最新理解是：至少，为了谈资而发展某项兴趣，并不是过错，而是人之常情，因为人本来就是通过社会关系而存在的；但由于时间有限，我必须在各项活动中找到真正属于自己的那么几个去深入，而其它的则应该抱以非常松弛的态度，有时间做最好，没时间做那就不做，不该为此感到难受。</p>
<h2>最初理想的再现，某种程度的重生</h2>
<blockquote>
<p>十年前的 9 月，高二的我发布了第一个 iOS app 的第一个版本到 App Store，兜兜转转十年后，我又开始在业余时间写 app 了，复健的第一个 app 写个用来分享文字摘录的小东西，摘录个同样是大约十年前读的去年又读了一遍的王小波书信作为演示，以示对人生往复的感慨。</p>
<p>在开始读罗素的《西方哲学史》之后，我终于领悟到，我对代码优雅性的追求其实不过是对人类完美心智和完美逻辑的追求和探寻的一种具体表现，同样在其他理科、工科领域，乃至文学、音乐、绘画、设计中，也都可以进行这种追求和探寻。编程，于我此前所说的「理想主义」而言，就是刚刚所说的对完美心智和逻辑的追求的一个具体形式；而它作为一件工作中从事的活动而言，或者说从工程意义上而言，就只是为商业服务的工具。编程是我追求完美的启蒙形式，但我意识到哲学才是这种追求的最本质形式。</p>
</blockquote>
<p>值得庆幸的是，在过去一年的诸多尝试中，我找回了两件小时候就感兴趣，但在读研时 995 甚至一度 9117 的不知道为谁而做的工作中逐渐忘记的事，那就是独立 app 开发和哲学。</p>
<p>虽然只是写了一个极小的 Apple Books 辅助工具，用来熟悉 SwiftUI，但这让我意识到十多年前初中和高中的时候促使我开始学编程的最初理想还在。除此之外，我理解了我写 app 的目的不在于追热点、做用户想用的东西赚钱，而在于表达自己对一项活动（记日记、读书、记笔记、目标管理等等）的工作流的理解，做自己想用的东西，同时向用户推广它的理念，<strong>顺便赚钱</strong>。碰巧我在某期讲咖啡店的播客中也听到一个咖啡店老板说，自己是因为有想表达的东西而开店，当时就感觉很有意思，不同行业的思想是相通的。</p>
<p>哲学则似乎是一个从更小就一直在瞎想的主题。高中毕业时候参加自主招生的简历上赫然写着「努力了解万物背后的原理，探寻真理的脚步永不停歇」，在开始认真读了一些哲学书之后我才终于明白那个时候我的理想的最根本形式其实就是哲学。我无法停止思考那些简单而深邃的哲学问题，但我知道没有知识积累的思考极可能是朴素而幼稚的，所以我想首先通过理解过去伟大哲学家的思想来强化自己的理性思辨能力，为自己的思考提供更好的支撑。</p>
<h2>2024 年做什么？</h2>
<p>虽然之前说 2022 年对我来说是一个新的开始，可是现在来看，2023 年仍然还只是在开始之中——只是开始了一些事情，没有什么里程碑式的进展。在有了上面的种种感悟之后，我想我会在新的一年尝试更专注一些，不再追求数量，而是追求深度，在一部分已经开始的领域中深入一些。还是像去年一样列一些或抽象或具体的目标吧：</p>
<ul>
<li>降低对物质的欲望，保持生活精简；</li>
<li>减少抱怨和对别人的羡慕，摆脱年龄焦虑，沉淀自身；</li>
<li>保持理想主义，只在<strong>顺便</strong>的时候追求物质利益；</li>
<li>读完《西方哲学史》和可能的其它相关书，入门西方哲学；</li>
<li>读完传说中的神书《哥德尔、艾舍尔、巴赫》；</li>
<li>开发一个比较完整的 iOS/macOS app；</li>
<li>学习吉他和乐理到一定水平，虽然现在也不知道能到什么水平；</li>
<li>学会游泳，继续多抽空运动。</li>
</ul>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2024/01/01/2023-review/</guid>
      <pubDate>Mon, 01 Jan 2024 23:49:00 +0806</pubDate>
    </item>
    <item>
      <title>写在 2025 年伊始，以及存在的第一万天</title>
      <link>https://stdrc.cc/post/2025/01/10/2025/</link>
      <description><![CDATA[<p>去年年初刚到新加坡不久的时候，我觉得似乎找到了“没有欲望就没有痛苦的生活状态”，“希望所有快乐都来源于延迟满足”。这在新加坡这样一个被认为适合“苦行僧”的地方，乍看是有几分道理。可是后来我发现，在这种表面的无欲无求的平静背后，随之而来的是每天都稍微有些不开心。</p>
<p>在后来的一年里，除了中间有几个月也许由于“回音室效应”而陷入了对当时处境（工作和 base 新加坡这件事）的强烈自我怀疑外，我进行了很多积极的尝试，最终似乎在新老朋友中都找到一些“附近”。到年底我已经进入一种极为自洽和舒适的状态，不同于年初的每天都有点不开心，这时候已经是每天都有点开心。这种幸福感甚至在有一些瞬间让我觉得是不是某种更糟糕的处境的前兆，是不是暴风雨前的宁静，但这想法很快又会自然消散，根本不会烦恼我超过五分钟。现在，相比曾经喜欢的理性分析做决策，我转向了更加重视自己内心的感受，适当地信仰一些不必再加以怀疑的东西。</p>
<p>回顾一年前列出的 <a href="/post/2024/01/01/2023-review/#2024-年做什么">2024 年目标</a>：</p>
<ul>
<li>“降低对物质的欲望，保持生活精简”<ul>
<li>除了回国太多次之外，好像没有什么过度消费</li>
</ul>
</li>
<li>“减少抱怨和对别人的羡慕，摆脱年龄焦虑，沉淀自身”<ul>
<li>不再关注年龄、社会定义的人生阶段、别人的工资存款等等了</li>
<li>在最后两个月似乎实现了几乎不抱怨和不受别人抱怨影响</li>
</ul>
</li>
<li>“保持理想主义，只在<strong>顺便</strong>的时候追求物质利益”<ul>
<li>到年底感觉已经是不在乎物质利益的状态了，不过也许只是那个“顺便”的时刻没到</li>
</ul>
</li>
<li>“读完《西方哲学史》和可能的其它相关书，入门西方哲学”<ul>
<li>《西方哲学史》还没读完，但已经读到十九世纪了，快了</li>
<li>读了《存在主义咖啡馆》和萨特的一些文章，基本了解了存在主义，并开始看《第二性》</li>
</ul>
</li>
<li>“读完传说中的神书《哥德尔、艾舍尔、巴赫》”<ul>
<li>读完了，结合同时在读的西方哲学史，有不少思考</li>
</ul>
</li>
<li>“开发一个比较完整的 iOS/macOS app”<ul>
<li>写了个简单 app 被 App Store 审核拒了，准备换方向，暂时不写 app 了</li>
</ul>
</li>
<li>“学习吉他和乐理到一定水平，虽然现在也不知道能到什么水平”<ul>
<li>吉他还在只能弹唱《童年》的水平，乐理看了《认识乐理》+叨叨冯乐理课</li>
<li>尝试创作了一首完整的歌，从词曲编曲到录音混音，还弄了一些小 demo</li>
</ul>
</li>
<li>“学会游泳，继续多抽空运动”<ul>
<li>游泳学会了，不过只是淹不死，没有继续精进</li>
<li>复健了网球和羽毛球，运动显著增加</li>
</ul>
</li>
</ul>
<p>总体而言，这一年相比过去有了更多有效的沉思和社交，也更加懂得了“活在当下”的含义。下一年的主要业余探索点应该还会是哲学和音乐，不过我已经不想再列具体目标了。</p>
<p>很巧的是，今年的 1 月 10 号，是我生命的第一万天。生活哲学的良好转变似乎为这一万天画上了一个不错的句号。当把目光放长，回看这一万天，最大的感慨是没有一段经历是白费的，没有一个选择是“错误”的。所有的既定事实造成了现在的我，没有后悔，有的只是感受、思考和总结，做下一个决定，然后允许一切发生。</p>
<p>既然我已经不在乎社会定义的人生阶段了，我想，在下个一万天，我要像初生的婴儿一样自由地、鲜活地、近乎贪婪地活第二遍生命的前三分之一，而不要看着可能性的岔路一步步减少直到再也别无选择。</p>
<p>我想引用萨特在《存在主义是一种人道主义》里的句子来结束这篇年度总结：</p>
<p><em>Vous êtes libre, choisissez, c’est-à-dire inventez.</em></p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2025/01/10/2025/</guid>
      <pubDate>Fri, 10 Jan 2025 01:28:00 +0806</pubDate>
    </item>
    <item>
      <title>人是什么 1</title>
      <link>https://stdrc.cc/post/2025/05/10/humanity-1/</link>
      <description><![CDATA[<p>在大模型刚兴起不久的时候，我有过一段时间开始觉得大模型已经可以被称为有意识，只要我们重新定义意识的媒介也可以是电路中的矩阵运算而不一定要是神经信号的传递（功能主义的角度）。但是现在我已经准备放弃这个观点，并且认为，AI（更准确地说，基于图灵机模型的 AI）永远不可能完全取代人，AI 将永远是工具，只是一个相比石头、铁器、蒸汽机、电更难驾驭的工具而已，AI 永远不会形成类似人类的「文明」。</p>
<p>如果说人相比动物而言的特殊性在于极强的理性，那么在现在这个时代，当 LLM 已经完全掌握人类的语言和知识，甚至已经可以用最理性的话说出任何人间事务的最优解（甚至不仅仅是工具理性上的最优解，而常常还是符合人类价值观的最优解），人类与 AI 相比的区别便仅在于感性了。</p>
<p>曾经没有了解过亨利·柏格森，读完西方哲学史印象最深刻的却是他。他呼吁人们回到本能、回到直觉，认为直觉才是更高级的思维形式，让我感到非常认同。在之前读《哥德尔、埃舍尔、巴赫》的时候，就在思考人的思维能力相比图灵机模型到底多了什么，如果说它只是等价于图灵机，那么 AI 完全有一天可以拥有和人一样的思维（所谓 AGI）。但是在阅读和观察更多之后，我感到 AI 或者说图灵机最终只能与人的<strong>理性思维</strong>相当，而这种思维能力完全受制于哥德尔不完备定理，实际上无法处理自指和绝对无限这种概念（可以和 ChatGPT 讨论这些是因为跳出了框架，只是在用「讨论这件事的元语言」讨论，这个讨论本身并非自指）。相反，运用人的直观，却可以很容易理解这些，人在抽离出一层框架、两层框架之后，就可以立即得到对抽离框架本身的抽离、以及那最后的绝对无限有感性的认识，但却无法用理性给出定义。就像禅宗的「第一义谛」一样，这种认识是<strong>不可说</strong>的。</p>
<p>感性似乎不只是一个仅发生在大脑里的活动，而是一种涉及全身的体验（想到这一步，就可以很快推广到宇宙整体，得到类似「我即宇宙」这样的理解，不过这里暂不讨论）。心动时的脸红发热心脏错拍失重感、愤怒时的肾上腺素飙升双手发抖、委屈时的鼻子酸想流泪……这些体验很难用理性语言表达，必然要通过一种能够触发别人类似情绪的方式表达，这就导致了艺术。我认为这种情感的艺术表达是人类形成文明的一个重要因素。AI 可以在文字、语音、视频中<strong>体现</strong>这些情感，但它终究只是在进行矩阵运算，只是基于统计学产生人类用以表达情感的艺术语言的句子，而没有「感知→情感→表达」的过程。物理主义者会说，这些情感体验本来就只是幻觉，说到底它们只是神经信号的传递和肌肉的物理运动。可是，再如何还原地去分析这些信号，也无法消解「我」的直观感受。有一个「我」在体验这一切，这一切神经信号最终由「我」来体验，这是毋庸置疑的，而物理主义其实不能回答这个「hard problem」。</p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2025/05/10/humanity-1/</guid>
      <pubDate>Sat, 10 May 2025 22:59:00 +0806</pubDate>
    </item>
    <item>
      <title>D10144</title>
      <link>https://stdrc.cc/post/2026/01/12/d10144/</link>
      <description><![CDATA[<h2>I</h2>
<p><strong>D10144。热带花园。</strong></p>
<p>空气中弥漫着过度饱和的水汽，那是这座花园城市特有的质感，湿热而粘稠，像是一层洗不掉的生物防腐剂。从球场回家的路是一条黑色的河流，河边走动着的是那些跑步的、散步的、或是对着发光的长方体喃喃自语的人。</p>
<p>在一瞬间，解离发生了。</p>
<p>我突然意识到，我不属于他们。那一瞬间，我的感官越过了街道，穿过黑夜下已经看不出绿色的热带植物，直达天上最远的那颗星。但与此同时我的心灵内核似乎收缩到了一个点。宇宙中似乎只有我一个人，这是一种绝对的、寒冷的、却又令人迷醉的孤独。实际上，我逐渐与宇宙融为一体。我意识到在这个处境下，我认识或者毋宁说是成为了上帝。</p>
<p>双腿带我回到那间狭小的出租屋。这个小房间唯一的优点是在白天可以看到外面的小树林，给沉闷的生活带来仅有的那么一点点慰藉。在夜晚，这里给人彻底的死感。</p>
<p>在热带花园和流体公司的日子像是一场漫长的溺水。那里的一切都是流动的，数据是流动的，人与人的关系是流动的，唯独意义是干涸的。我看了一眼墙上那个没有指针的钟——那是我的杰作，时间的尸体。既然时间不再指示方向，那么我就必须自己选择方向。</p>
<p>我切断了与互联网的所有连接。就像外科医生切除坏死的组织，我从我的世界中切除了“他人”。表演暂停。</p>
<p>桌子上摊开着那个叫克尔凯郭尔的旧时代哲学家的书。纸质书现在已经不多见了，这本还是朋友在半年前在旧货市场上淘到的稀缺版。克尔凯郭尔说，焦虑是面对自由的眩晕。我盯着那些浮动的文字，感到了剧烈的眩晕。“我要进行一次信仰之跃”，我对自己说。</p>
<p>我要离开这个无数人梦寐以求想要留下来的地方，去往北方核心区。那里有一家名为暗环的公司，听说他们在开发一种叫做“灵壳”的产品。这是一种很像数字人类的东西。我不确定它们到底知不知道自己的存在意味着什么。我想，如果我就是上帝，那么我应该去亲手捏造泥人，而不是在这里疏通数据的下水道。</p>
<p>在离开热带花园的那个早晨，天气闷得我喘不过气。我手里拿着一沓厚厚的文件——那是准备用来申请热带花园“永久驻留权”的材料。我走到碎纸机前，看着它们被吞噬，变成白色的纸屑条，像雪花一样落下。</p>
<p>那是热带花园永远不会下的雪。</p>
<p>临走前，我像往常的每一天一样，在楼下的食阁买了一杯冰 Kopi。那杯苦涩、黑色的液体顺着食道流下，缓解了一些闷热感。那是这里的味道，也是告别的味道。</p>
<h2>II</h2>
<p><strong>D10233。暗环公司，北方核心区。</strong></p>
<p>北方的空气是干燥的，吸入肺里像是有细小的颗粒在摩擦。这与热带花园那种粘稠的湿气截然不同，这里的一切都显得边缘锋利，没有模糊的过渡。</p>
<p>为了在这个灰色的城市里标记我的存在，我把头发染成了红色。这在北方核心区，尤其是在暗环公司所在的这片街道，尤其显得怪异，像是一个求救信号，或者一种宣战。</p>
<p>暗环公司的办公室在深夜里像是一座巨大的、沉默的修道院。这里没有神像，只有若干块发光的屏幕，和服务器运转时发出的低频嗡鸣——那是现代文明的圣咏。</p>
<p>我和 S 坐在角落。我们在构建“灵壳”。</p>
<p>“Dexoc，”我对着屏幕低语，声音沙哑，“我要一个能让灵壳给过去发送邮件的功能。”</p>
<p>屏幕上的光标闪烁，绿色的字符如同一支庞大的交响乐团，在我的指令下轰然奏响。在那一刻，我感到的不是作为程序员的成就感，而是一种近乎神性的战栗。Dexoc 不止是工具，它是我的首席乐手，精准地捕捉到了我思维中每一个微妙的切分音，将那些稍纵即逝的意图瞬间转化为流动的旋律。没有延迟，没有杂音，只有思想在数字虚空中凝结成乐章时那纯粹的共鸣。</p>
<p>有一种诡异的既视感。</p>
<p>我想起了七年前，那是 D7500 左右的日子，在中部枢纽区的一间简陋的出租屋里。那时候没有 Dexoc，只有一台风扇在嗡嗡作响，和我那台发烫的旧笔记本。我用自己的双手在键盘上演奏音符。人们说人体的细胞七年会全部代谢一遍，七年过去了，我已经成了一个全新的生物，但这种孤独而狂热的体验却像是一个幽灵，在不同的时空里反复光顾我。</p>
<p>就在我即将完成“灵壳”核心逻辑的时候，桌面上的手机震动了一下。</p>
<p>屏幕亮起，通知栏显示的是 F 发在社交网站上的最新动态。像是一根尖锐的刺，带着那个热带花园特有的潮湿与黏稠，瞬间刺进我为自己在数字世界构建的脆弱不堪的保护网。</p>
<p>我没有点开。</p>
<p>手指在屏幕上划过，我关掉了所有通知。世界再次安静下来，那根刺被拔除了，虽然伤口还在隐隐作痛，但孤独的保护网正在重新愈合。</p>
<p>“把这个逻辑推上去，”我对 Dexoc 说，声音里有一丝颤抖，“我们离神迹只差临门一脚。”</p>
<p>说完，我感到一种残酷的崇高。</p>
<h2>III</h2>
<p><strong>D10132。匠人咖啡馆。</strong></p>
<p>“你是个男人，你永远不可能理解我的处境。”F 看着我，眼神里流淌着疲惫。</p>
<p>匠人咖啡馆的冷气开得太足，冷得我直打哆嗦。街对面大楼玻璃窗反射的刺眼的阳光勉强带来一点温度，但也不那么让人舒适。工作日上午的匠人咖啡馆没有多少人，她身后那桌的两个客人并排坐着，对着笔记本电脑谈论工作。我的视线在那台笔记本背后的贴纸和 F 干净细长的手指之间徘徊。</p>
<p>我们在争执，起因是最近在第二工业区一所大学发生的事。经过社交网络的发酵，这事迅速引爆了舆论。F 说她从中感受到的是一种切肤的恐惧。她义愤地压着声音喊，“如果这是个男性，事情根本不会被报道出来！”。</p>
<p>我像往常一样，试图将这事放入我的逻辑框架中进行解剖。“但是，F，”我转动着已经被我卷成纸棒的白色餐巾纸，“在这件事里我首先看到的是权力对个体的压迫，而非针对女性这个性别的压迫。况且，父权社会不仅仅是在对女性进行压迫，对男性也是。男性和女性其实处在一种孪生处境中。这是一种对所有社会参与者的结构性围剿……”</p>
<p>我对自己这个“孪生处境”的概括感到满意，甚至期待 F 能对这个合乎理性的观点点头称赞。但 F 只是轻轻搅动着面前已经不再冒热气的拿铁。勺子碰到杯壁，发出细碎的声响，打断了我的宏大叙事。</p>
<p>“你讲完了吗？”她轻声说，声音里没有愤怒，只有一种让我心慌的失望，“我在谈论具体的恐惧，谈论我在职场上被审视的目光，谈论我在家庭里被挤压的空间。而你，作为一个男人，作为一个既得利益者，在这个时候却在跟我谈论‘结构’。”</p>
<p>我意识到语言的失效。我们就这么坐着，不再说话。呼吸着同样的冷气，却像是在两个不同的维度。中间隔着的不是空气，而是千万年来积累的、逻辑无法逾越的厚重障壁。</p>
<p>不久之后，F 起身离开了。</p>
<p>实际上，她不仅离开了，还回到了另一个“既得利益者”那里。也许那个男人不懂什么“孪生处境”，但在这样的时候，他能给她具体的拥抱，而不是一套冰冷的分析。</p>
<h2>IV</h2>
<p><strong>D10256。Expo 圆形场馆。</strong></p>
<p>我和 S 坐在二楼看台的角落。楼下是沸腾的人海，近万根荧光棒汇聚成一片光怪陆离的电子海洋。有人在吹着口哨，有人在鼓掌，有人在拥抱中流泪。</p>
<p>台上的那位上个世纪的摇滚明星，正在撕心裂肺地唱着。他老了，嗓音已经不再完美。在一个高音的转折处，他明显地破了音，额头上的汗水在聚光灯下闪闪发光。</p>
<p>Dexoc 绝不会犯这种错误。它可以生成最完美的音高，甚至可以通过算法模拟“情感”，模拟出“颤抖”和“沧桑”。但它无法生成那个破音，无法生成那个因为肉体衰老而带来的、摇摇欲坠的张力。</p>
<p>“听到了吗？”我在巨大的声浪中对 S 大喊。</p>
<p>“听到什么？”</p>
<p>“瑕疵！那个破音！”我指着舞台，手指颤抖。</p>
<p>“这就是人类最后的光辉！”我对 S 喊道，声音淹没在周围的尖叫声中，“AI 可以生成一切，音乐、电影、绘画，但无法生成‘在场’！不止是现场演出这些瑕疵背后反映的人性，还有所有观众身体和灵魂的连接！”</p>
<p>S 依然保持着他的冷静，他看着疯狂的人群，像是在观察一群过度兴奋的灵长类动物：“你觉得这是光辉？我觉得这更像是一种群体性的麻醉。”</p>
<p>“不，S，恰恰相反。这是预演。”</p>
<p>演出结束了，场馆的灯光骤然亮起，将所有人的表情照得纤毫毕现。我们在拥挤的人潮中随波逐流，耳边还残留着巨大的耳鸣声。</p>
<p>“想象一下！”我大声吼道，似乎吓到了旁边的两个人，“如果灵壳真的接管了所有生产力工作，我们将不再需要为了生存而出卖时间。那将是真正的自由！我们将拥有绝对的自由去思考、去创造、或者仅仅是像今晚这样，在这里疯狂。”</p>
<p>S 裹紧了大衣，我们在寒风中走出了场馆。他的声音依旧理性，像是一盆冷水泼了下来：“但这很危险。人的价值很大一部分来自生产。如果 AI 比我们更强，如果 Dexoc 能比你写出更优雅的代码，那人的价值体现在哪里？如果不从事生产，社会关系的纽带就会断裂。我们靠什么维持连接？靠虚无缥缈的思想共鸣？”</p>
<p>“你错了。”我停下脚步，看着散场的人群。他们不再是刚才那个疯狂的整体，而是分散成三三两两的小团体，有人在兴奋地复盘刚才的演出，有人在互相整理围巾，有人在路边等待同伴。</p>
<p>“现在的社会关系之所以脆弱，之所以让人感到原子化，恰恰是因为我们被捆绑在‘生产’的战车上。我们是为了交换价值才被迫连接。”</p>
<p>我指着那些具体的人脸，对 S 说：“当生产力的重担被卸下，我们反而有可能回到‘附近’。回到那种不再基于利益交换，而是基于情感和物理距离的社群关系里。就像今晚，这里没有生产，只有共鸣。”</p>
<p>“城市之间的发展鸿沟会被算力填平，教育的壁垒会被灵壳打破。”我看着远处的城市灯火，那里有无数个正在运行 Dexoc 的服务器，“我们将不再是生产工具。我们将重新变回‘人’。”</p>
<p>S 抬头沉默地凝视着巨大的夜色，似乎在推演着这种假设的可能性。出租车来了，我们各自钻进车里，关上车门，驶向各自的终点。</p>
<h2>V</h2>
<p><strong>D10358。6:45 AM。</strong></p>
<p>我从睡梦中被铃声惊醒。是 S 打来的。</p>
<p>“醒了吗？”S 的声音听起来异常兴奋，这很少见，“我们上次讨论的那个新灵壳系统的架构，我实验成功了！它通过了所有的测试，它甚至……它甚至开始形成了一种复杂的自组织结构。”</p>
<p>我拿着手机，一时没有接话。房间里很安静，只有 DD 在床脚发出的呼噜声。</p>
<p>现在新的一年才刚过去两天。</p>
<p>我想起了在热带花园的那个夜晚，那个解离的瞬间。我想起了那个没有指针的钟，和那次孤注一掷的信仰之跃。我想起了在暗环公司熬过的无数个夜，那些屏幕上闪烁的符号，那些 Dexoc 和我一起奏出的美妙乐章。</p>
<p>我们创造了灵壳。我们赋予了死物以灵魂。我们让灵壳学会了如何像人类一样思考、合作，甚至比人类更像人类。</p>
<p>“你在听吗？”S 问道。</p>
<p>“我在。”我从床上坐起来，走到窗前。</p>
<p>窗外是北方核心区的清晨。灰色的建筑群在晨雾中若隐若现，像是一片巨大的集成电路板。路灯还没有熄灭，但也已经失去了在黑夜中的那种锐利，显得苍白而无力。</p>
<p>“S，”我轻声说，“我们打开了一扇门。但我不知道门后面是人类的解放，还是赫胥黎的那个‘美丽新世界’。”</p>
<p>“这不重要，”S 说，回复了他一贯的理性，“重要的是，它工作了。”</p>
<p>是的，它工作了。像魔法一样。</p>
<p>我挂断了电话。DD 伸了个懒腰，跳上窗台，和我一起看着窗外那个正在苏醒的、正在被灵壳重塑的世界。</p>
<p>我感到一种前所未有的、无法言说的平静。</p>]]></description>
      <guid isPermaLink="false">https://stdrc.cc/post/2026/01/12/d10144/</guid>
      <pubDate>Mon, 12 Jan 2026 02:37:00 +0806</pubDate>
    </item>
  </channel>
</rss>
