Project RC

一种标准阿西的设计与实现。

2023 仍然还在开始,2024 会走到哪里呢

创建于
分类:Misc
标签:年度总结

其实本来刚进入 12 月下旬的时候,我是满怀期待地准备好好总结一下过去的一年的,当时非常自信地觉得今年自己发生了很大的改变、有了很大的进步,可是直到 12 月最后两天,翻看了这一年的日记后,我才又意识到自己在很多方面仍然处在「愚昧之巅」。在这两天,有很多瞬间我不想再整理年终总结了,就像在 3 月的某一个瞬间突然决定不去自己的毕业典礼一样,可是等心情平复之后我知道很多变化和新的理解仍然是值得记录和分享的。

这次我不想再像去年一样按时间和事件来把 2023 年的时间轴切块罗列,而是从一个横切的视角,尝试记录发生在全年的事件给我带来的不同思考和感悟。

Dating app、人生简历和形象管理

我今天彻底理解了一件事:女朋友,首先应该是朋友,如果去掉了爱情,两人就没啥可说的了,去掉了心动,两人就没啥好交流的,那没意思。

我觉得 dating app 的 profile 就是人生简历,你需要展示全面的你,比工作简历还难写,我有时候会改一小块,然后看看最近「喜欢我的人」的增长有什么变化。

所谓的「dating」是我今年除了工作之外的隐藏主线,虽然说从 2021 年底其实就开始刷 dating app 了,但今年确实和去年的感受有所不同。

首先,我必须帮助怀有「dating app 上的人不是都不正经吗」看法的读者破除对 dating app 的偏见。Dating app 是一大类型的 app 的总和,而不只是一个 app。就像购物网站有大部分都是假货烂货的,也有只卖「官方旗舰店」的或是「严选」的一样,dating app 中有更倾向于擦边的,也有更倾向于认真找对象甚至于像相亲一样直接的。同样,就像购物网站的前一类中也有好东西,后一类中也有假东西,dating app 的前一类中肯定也有正经人,后一类中也有不正经人。所以在 dating app 上认识什么样的人,完全取决于你想找什么样的人、是否用了适当的 app、是否有足够的能力判断对方是什么样的人。

再来说我的感受。在不断的认识、见面、甚至于真正的「dating」的循环中,我不断地调整我的 dating app profile,以期更好地表达自己,展现自己值得展现的一面。很多时候我感到自己没有什么实质性的优势,感到自己所擅长的兴趣爱好很贫瘠,于是我又不断地想去学习、见识更多事物,以此来更新我的 profile。进而我意识到 dating app 的 profile 实际上是一种「人生简历」,它就像找工作的简历一样,区别在于因为在找人生伴侣,所以需要更全面地介绍自己的人生。

尽管很多人会说,长相不重要,但其实在 dating 的逻辑里,照片和第一次见面时的外貌形象具有极其重要的意义。在 2023 年我首次尝试了烫发、染发、夹板、遮瑕膏等,继续用了香水,学习改进了穿搭,虽然最终仍然远远称不上帅,但确实相比去年的形象有了很大改善。

这两年刷 dating app 的经历虽然带给我不少焦虑,但其中很多成为了我提高自己的动力,确确实实地让我进步了很多。除此之外,这些经历也让我更清楚自己是什么样的人、自己喜欢什么样的人

然而,我还是会时常困惑自己想成为什么样的人

我做的事,到底是不是我想做的事?

我开始怀疑,我做的那些事情,旅游、听某某的歌、看演唱会、调酒、写 app、学吉他等等等等,到底是我真的喜欢,还是只是为了发朋友圈,为了增加和别人聊天时候的谈资?

这一年我做了很多事情来开拓自己的视野、扩展自己的兴趣和技能。主要包括读了 18 本书(虽然其中有几本编程相关的书「充数」),看了 54 部影视剧(不算多,且没有完成年初定的 66 部的目标),看了 12 次演出/展览(包括演唱会及其录像、音乐节、脱口秀、美术展),旅行了 5 次左右,入门了一点调酒,入门了一点吉他,开始听更多种类的音乐,工作地点 relocate 到了新加坡,以及一些可能已经忘掉的事。

虽然看起来还算充实,也不乏有人称之为「成功」,可我经常会思索自己做这些到底是为了什么。或许可能源于刷 dating app 产生的焦虑,或许因为需要在朋友圈子里拥有谈资、得到认可,又或许有一些我确实真的喜欢。在这个「扩展见闻」的过程中,我不断地想听到自己内心的声音,想知道自己到底想成为什么样的人、想走什么样的道路,可是很难得出结论,我好像想成为任何人、想走每一条道路。可这显然是贪婪的,是不可能的。什么都想做的结果是什么都只能浅尝辄止。

在和朋友、和自己反复对话后,我对这个问题的最新理解是:至少,为了谈资而发展某项兴趣,并不是过错,而是人之常情,因为人本来就是通过社会关系而存在的;但由于时间有限,我必须在各项活动中找到真正属于自己的那么几个去深入,而其它的则应该抱以非常松弛的态度,有时间做最好,没时间做那就不做,不该为此感到难受。

最初理想的再现,某种程度的重生

十年前的 9 月,高二的我发布了第一个 iOS app 的第一个版本到 App Store,兜兜转转十年后,我又开始在业余时间写 app 了,复健的第一个 app 写个用来分享文字摘录的小东西,摘录个同样是大约十年前读的去年又读了一遍的王小波书信作为演示,以示对人生往复的感慨。

在开始读罗素的《西方哲学史》之后,我终于领悟到,我对代码优雅性的追求其实不过是对人类完美心智和完美逻辑的追求和探寻的一种具体表现,同样在其他理科、工科领域,乃至文学、音乐、绘画、设计中,也都可以进行这种追求和探寻。编程,于我此前所说的「理想主义」而言,就是刚刚所说的对完美心智和逻辑的追求的一个具体形式;而它作为一件工作中从事的活动而言,或者说从工程意义上而言,就只是为商业服务的工具。编程是我追求完美的启蒙形式,但我意识到哲学才是这种追求的最本质形式。

值得庆幸的是,在过去一年的诸多尝试中,我找回了两件小时候就感兴趣,但在读研时 995 甚至一度 9117 的不知道为谁而做的工作中逐渐忘记的事,那就是独立 app 开发和哲学。

虽然只是写了一个极小的 Apple Books 辅助工具,用来熟悉 SwiftUI,但这让我意识到十多年前初中和高中的时候促使我开始学编程的最初理想还在。除此之外,我理解了我写 app 的目的不在于追热点、做用户想用的东西赚钱,而在于表达自己对一项活动(记日记、读书、记笔记、目标管理等等)的工作流的理解,做自己想用的东西,同时向用户推广它的理念,顺便赚钱。碰巧我在某期讲咖啡店的播客中也听到一个咖啡店老板说,自己是因为有想表达的东西而开店,当时就感觉很有意思,不同行业的思想是相通的。

哲学则似乎是一个从更小就一直在瞎想的主题。高中毕业时候参加自主招生的简历上赫然写着「努力了解万物背后的原理,探寻真理的脚步永不停歇」,在开始认真读了一些哲学书之后我才终于明白那个时候我的理想的最根本形式其实就是哲学。我无法停止思考那些简单而深邃的哲学问题,但我知道没有知识积累的思考极可能是朴素而幼稚的,所以我想首先通过理解过去伟大哲学家的思想来强化自己的理性思辨能力,为自己的思考提供更好的支撑。

2024 年做什么?

虽然之前说 2022 年对我来说是一个新的开始,可是现在来看,2023 年仍然还只是在开始之中——只是开始了一些事情,没有什么里程碑式的进展。在有了上面的种种感悟之后,我想我会在新的一年尝试更专注一些,不再追求数量,而是追求深度,在一部分已经开始的领域中深入一些。还是像去年一样列一些或抽象或具体的目标吧:

  • 降低对物质的欲望,保持生活精简;
  • 减少抱怨和对别人的羡慕,摆脱年龄焦虑,沉淀自身;
  • 保持理想主义,只在顺便的时候追求物质利益;
  • 读完《西方哲学史》和可能的其它相关书,入门西方哲学;
  • 读完传说中的神书《哥德尔、艾舍尔、巴赫》;
  • 开发一个比较完整的 iOS/macOS app;
  • 学习吉他和乐理到一定水平,虽然现在也不知道能到什么水平;
  • 学会游泳,继续多抽空运动。
2023 仍然还在开始,2024 会走到哪里呢

RisingWave 窗口函数:滑动的艺术与对称的美学

创建于
分类:Dev
标签:RisingWaveSQL数据库流处理窗口函数滑动窗口

本文发表于 RisingWave 中文开源社区

窗口函数(Window Function)是数据库和流处理中一项非常常用的功能,该功能可用于对每一行输入数据计算其前后一定窗口范围内的数据的聚合结果,或是获取输入行的前/后指定偏移行中的数据。在其他一些流系统中,窗口函数功能也被称作“Over Aggregation”1。RisingWave 在此前的 1.1 版本中加入了窗口函数支持2。在 RisingWave 的窗口函数实现中,我们把实施窗口函数计算的算子称为 OverWindow 算子,本文将尝试解析 OverWindow 算子的设计与实现。

基本例子

首先用两个简单的例子展示窗口函数的基本用法。更完整的语法说明请参考 RisingWave 用户文档3

例 1

下面的例子会持续计算每次股票价格更新事件时,当前价格相比上次更新时的价格差。

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;

这里使用了 LAG 窗口函数,获得与当前行的 stock_id 相同的行中,按 event_time 排序,排在当前行的前一行的 price 值。与 LAG 相对应的,还有 LEAD 函数,用于获取后一行(按时间排序的话,即更新的一行——更“领先(lead)”的一行)。这类窗口函数我们称之为通用窗口函数(General-Purpose Window Function),与 PostgreSQL 中的概念保持一致4

例 2

下面的例子则对每笔订单,计算该订单的用户在该订单前的 10 笔订单的平均消费金额。

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;

这里使用了 AVG 函数,它实际上是一个聚合函数(Aggregate Function)。在 RisingWave 中,所有聚合函数都可以用作窗口函数,后面跟 OVER 子句指定计算窗口,我们称该类窗口函数为聚合窗口函数(Aggregate Window Function)。同样,这与 PostgreSQL 的概念保持一致4,便于用户快速理解。

两种输出触发模式

在此前的文章《深入理解 RisingWave 流处理引擎(三):触发机制》中5,我们已经介绍了 RisingWave 流计算引擎的两种输出触发模式,包括默认的 Emit-On-Update 和可通过关键字启用的 Emit-On-Window-Close 模式。OverWindow 算子也支持这两种输出模式。

通用模式(Emit-On-Update)

在通用模式下,OverWindow 算子在收到输入变更时,立即从内部状态中找到变更行所影响的行范围,并重新计算该范围内所有行对应的窗口函数结果。

上一节中两个 SQL 例子即是采用通用模式进行计算。

EOWC 模式(Emit-On-Window-Close)

通过在查询中加入 EMIT ON WINDOW CLOSE 关键字67,即可采用 EOWC 输出模式。

在 EOWC 模式下,OverWindow 仅在收到 watermark 时输出 ORDER BY 列和所对应的窗口均被 watermark “淹没”的行。这和我们熟悉的 EOWC 模式下 GROUP BY watermark 列的 HashAgg 算子行为有细微差别,在后者中,收到一个 group 的 watermark,即标志着该 watermark 前的 group 已经“完成”,即可输出;而在 OverWindow 中,需要等待两个条件满足才会输出,首先是 ORDER BY 列的“完成”,即输入行在 watermark 语义上允许下游可见,其次是窗口函数所定义的窗口的“完成”,即输入行所对应窗口的最后一个行也对下游可见。

出于性能考量,我们为通用模式和 EOWC 模式分别编写了两个执行器实现(不过许多代码是复用的),以充分利用两种输出模式的语义特征,下文将对它们进行分别介绍。

EOWC 版本:滑动的艺术

EOWC 版本的 OverWindow 算子(后称 EowcOverWindow)的实现算法相比通用版本要稍简单,因此这里先介绍它。

如前所述,EowcOverWindow 要等到一个输入行的 ORDER BY 列“完成”(条件 ①),且其所对应的窗口“完成”(条件 ②),才能输出这个行及其窗口函数计算结果。也就是说,即使窗口函数的 frame 是 ROWS BETWEEN 10 PRECEDING AND 1 PRECEDING,在 CURRENT ROW前一行的条件 ① 满足时,CURRENT ROW 的条件 ② 看起来就已经满足,算子仍然要等到 CURRENT ROW 的条件 ① 满足才能输出。我们可以换一个角度来理解,把输出中包含的所有输入列认为是 LAG(?, 0),进而就可以迅速发现条件 ① 实际上是条件 ② 的前提。

基于这个观察,我们把 EowcOverWindow 实现为两个阶段,对于一个输入行:

  1. 第一阶段等待条件 ① 满足,满足后把该行释放给第二阶段;
  2. 第二阶段等待条件 ② 满足,满足后计算窗口函数结果。

窗口函数的实际计算在两个条件都满足后才进行,可以避免大量不必要的无效计算。这与 HashAgg 算子的 EOWC 实现略有不同(后续会有文章介绍),因为 OverWindow 中一行修改会导致多行变更,而 HashAgg 中每个 group 至多有一行修改,前者无论在计算还是 I/O 层面均有明显的放大效应。

第一阶段:SortBuffer

第一阶段是对输入行的一个缓冲,又由于 watermark 的非递减性质,很容易把第一阶段的输出实现为是有序的,因此我们把第一阶段命名为 SortBuffer。更进一步,我们引入了一个名为 EowcSort 的算子来解耦 SortBuffer 与第二阶段,使 SortBuffer 可以在其他需要的地方复用。于是,EowcOverWindow 算子以 EowcSort 作为上游,其内部只需对满足条件 ① 的有序输入行实现第二阶段。

第二阶段:滑动窗口

由于条件 ② 满足之后才会进行计算,EowcOverWindow 需要先将输入行按 PARTITION BYORDER BY 列有序存储在其内部 state table 中。并且,对每个 partition,EowcOverWindow 在内存中维护着当前正在等待窗口完成的 CURRENT ROW(“当前行”)及其对应窗口(“当前窗口”)中的行(该内存结构可以在 recovery 时从 state table 重建)。

当一些输入行从 SortBuffer 进入 EowcOverWindow 时,后者便会找到对应 partition 的上述内存结构,如果其中的“当前窗口”已完成,则输出“当前行”和“当前窗口”上的窗口函数计算结果,并将“当前行”及其窗口滑动到下一行,如此循环直到“当前窗口”不再完成。窗口滑动时,一些最旧的行会被移出“当前窗口”,EowcOverWindow 于是可以把它们从 state table 中清除。

下面,我们通过一个例子来演示上述两个阶段的算法过程。考虑下面的查询7

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;

其中,三个窗口函数调用的 PARTITION BYORDER BY 相同(对于实际场景中不同的情况,优化器首先对查询进行拆分,由多个 OverWindow 算子处理),窗口 frame 不同。另外,ts 列定义了延迟为 5 分钟的 watermark。

在给出算法过程的动画演示之前,先给出动画中几种箭头所表示的含义:

现在,可以通过下面的动画理解 EowcOverWindow 的实现算法:

通用版本:对称的美学

相比 EOWC 版本,通用版本的 OverWindow(后称 GeneralOverWindow)看似更加简单粗暴,实际上实现起来是更为复杂的。

在 GeneralOverWindow 中,ORDER BY 列通常没有定义 watermark,于是输入行的 ORDER BY 列的值可能是任意大小的(表现在现实场景中就可能是几天前的数据仍然会被插入、修改或删除)。因此,不同于 EowcOverWindow 始终知道“当前窗口”在哪,GeneralOverWindow 在收到输入行之后,首先需要找到其对应的窗口,然后才能计算窗口函数结果。

例如,考虑上一节最后的查询例子(去掉 EMIT ON WINDOW CLOSE 关键字),假设我们已有如下数据:

ts     pk   x
10:00  100  5
10:02  101  3
10:10  103  9
10:17  104  0

现在插入了 10:06 102 8 这样一行新数据(修改、删除的情形类似,后续只讨论插入),如下:

ts     pk   x
10:00  100  5
10:02  101  3
10:06  102  8  <-- insert
10:10  103  9
10:17  104  0

按照所指定的窗口函数 frame,要计算 pk = 102 行的窗口函数结果,需要向前找一行、向后找一行,也就是说,CURRENT ROW102 行的“当前窗口”范围是从 101 行到 103 行。

想到这里,我们立即可以发现,刚刚从新插入的行开始按窗口 frame 向前向后找到的“当前窗口”,仅能产生新插入的行对应的一行输出,然而,新插入的行很可能也属于此前已经输出过的其他窗口,从而导致曾经输出过的行需要修改。因此,我们需要改变算法思路,不能把当前插入/修改/删除的行作为 CURRENT ROW 来找窗口,而要把它当作某个窗口 A 的最后一行和另一个窗口 B 的第一行,找到窗口 A 和 B,才能正确为所有受影响的行产生新输出。

同样以刚刚的数据为例,把 102 行当作窗口 A 的最后一行,倒着找,可以找到 A 的 CURRENT ROW101 行,进而找到窗口 A 的第一行是 100 行。这里我们将窗口 A 的第一行 100 行标记为 first_frame_startCURRENT ROW101 行标记为 first_curr_row。对称地(点题了!),把 102 行当作窗口 B 的第一行,顺着找,可以找到 B 的 CURRENT ROW103 行,进而找到窗口 B 的最后一行是 104 行,和前面类似,分别把它们标记为 last_curr_rowlast_frame_end。这个过程如下面动画所示:

找到 (first_frame_start, first_curr_row, last_curr_row, last_frame_end)(分别对应动画最后的四个横线)这整个受新输入行影响的范围后,只需要复用 EowcOverWindow 第二阶段的代码,即可滑动地计算从 first_curr_rowlast_curr_row 的新输出结果,如下面动画所示:


  1. RisingWave 1.1 版本亮点一览,https://mp.weixin.qq.com/s/c0VHTebJ3zwiqma2z352VA 

  2. RisingWave 窗口函数文档,https://docs.risingwave.com/docs/current/window-functions/ 

  3. PostgreSQL 窗口函数文档,https://www.postgresql.org/docs/current/functions-window.html 

  4. 深入理解 RisingWave 流处理引擎(三):触发机制,https://mp.weixin.qq.com/s/eQjGEGei9vfrXhAjcRe67w 

  5. RisingWave Emit-On-Window-Close 文档,https://docs.risingwave.com/docs/current/emit-on-window-close/ 

  6. 由于 EOWC 模式还属于实验性功能,其行为和语法都可能有所变化,例如语法在 1.2 版本发生了一次变化,调整了 EMIT ON WINDOW CLOSE 关键字的位置,在使用时请注意参考所使用版本对应的文档。 

RisingWave 窗口函数:滑动的艺术与对称的美学

租房经验总结

创建于
分类:Misc
标签:租房总结经验攻略

最近由于毕业以及之前的房子到期,于是在距离公司稍近的地方租了个新的房子。至此,从本科时期到现在已经租了四年、八间次房子了。其中,有房东直租的、有经过中介的、有二房东的、有酒店长租、有车库改造的、有居民小区、有商用住宅,踩过不少坑,这次想整理一些经验,以便以后查阅和供读者参考。

性质

根据租房的性质不同,找房、签合同的方式和随后会遇到的坑不同:

房东直租:优点是不需要中介费,直接和房东签合同;缺点是不好找,现在很少有房东直租的房源,并且没有中介担保签合同,可能比较看房东人品。但总体来说如果看房的时候能判断房东人怎么样的话,这种是最优的。

房产中介:优点是去感兴趣的小区在门口随便找个链家、我爱我家或者什么其它 XX 房产/地产,他们就会把附近小区现在能租的房子都带你看一遍,很方便;缺点是需要半个月或一个月房租(不同地区可能不同)的中介费。

二房东:这有两种,一种是当地一些职业二房东(其中可能一些人看起来不太正经,像地痞流氓),另一种是房产公司做二房东的。前一种我没租过,但看过房,装修看起来都不错,但二房东人看起来非常不靠谱,看起来就不像是能要回押金的,遂放弃;后一种也比较坑,东西出了问题可能不会及时修,以及可能有粗暴管理的情况。自如的整租合租等(非自如寓)和链家的某些房子属于后一种,相对比本地不知名房产公司会稍微靠谱一点,但问题仍然存在。

公寓:自如寓、城家公寓和其它各类公寓品牌,优点是房型比较固定,管理比较统一、规范,缺点是价格偏贵,并且附近可能缺乏生活气息,比较类似酒店。

酒店:通过合理用券、用会员、谈协议价等方式可以用较低价格租到酒店长租。优点是方便、规范,不需要交水电网费,领包入住,每天有人打扫卫生;缺点是房间相比同价格的其它类型租房会小非常多,没有生活气息,环境不稳定,有时候晚上会有噪音。

费用

中介费:非房东直租的会有,一般为半个月或一个月房租。

服务费:公寓类型的会有,比如自如,以一定百分比的形式附加在每月房租上。

押金:自如有毕业生免押金,除此之外基本都需要交,一般为一个月房租。

房租

  • 一个同一个地段的面积差不多的房子房租都差不太多,根据装修不同会有浮动,一般可以让中介或者自己跟房东谈(直租的情况),都可以比标价便宜小几百块钱
  • 可以留意房子的空窗时间、地段热门程度,感觉上这可以用来判断底价
  • 付费周期比较多的是押一付三,也就是一次付三个月,也有一些会支持押一付一

水电费

  • 民用住宅和商用住宅的水电费差异较大,商业用电 1 块钱 1 度还是有点猛的
  • 电费一般在支付宝或微信填户号就可以缴,但也一些可能是需要手动缴费给房东,这种可能房东会自己定价,高于一般价格(可能一些公寓是这样的)
  • 水费一般民用住宅也可以支付宝微信缴,但见过需要去物业充值水卡的类型,比较麻烦
  • 不建议租一个房子多个租客但水电费户号只有一个的那种,虽然一般会有分水电表,但总和起来往往会有偏差(以及公共区域消耗),导致每个人都觉得自己没多用,但最后停电了,容易起争执

天然气费:居民小区一般会有天然气,便于做饭,但商用住宅似乎没见过。缴费一般也是支付宝。

宽带费:公寓一般会提供网络,其它一般需要自己去营业厅办(也见过商用住宅需要去物业办的),协议一般一年起步,假如要搬家,建议移机而不是重新办旧的不管了。

看房考虑因素

除了上面最重要的性质和费用因素,下面列出了从我个人的角度比较会关注的看房需要考虑的因素。对照清单可以避免看房的时候遗漏,一般正常小区正常装修的房子,大部分应该是没问题的,主要还是装修风格和房型比较值得比较。

外部

  • 楼层高度
  • 有无电梯
  • 离地铁站、公交站距离
  • 地铁、公交早晚高峰的拥挤程度
  • 小区环境
  • 垃圾投放点距离
  • 快递柜有无、距离
  • 餐饮店密度、距离
  • 附近住户成分(影响安全性和“烟火气”)
  • 窗外是否有中小学、酒吧等吵闹场所
  • 房东是否好说话

通用

  • 装修风格
  • 房间划分是否合理
  • 门把手是否破损
  • 门窗是否严实(影响空调效果和隔音)
  • 窗户大小和朝向(采光)
  • 纱窗是否干净、是否破损
  • 窗帘是否干净、遮光效果
  • 阳台有无、大小
  • 房间隔音效果(合租需要考虑,警惕玻璃墙,隔音极差)
  • 墙面和天花板状态(一些老破小会掉墙皮甚至漏水,漏水一般出现在顶层)
  • 灯的亮度
  • 灯具开关位置是否合理
  • 空调是否太旧、是否干净
  • 插座数量、位置、安全性
  • 可利用的家具/设施数量
  • 各类储物空间大小
  • 有无蟑螂或其它虫子

浴室 & 卫生间

  • 是否干湿分离
  • 地漏下水速度
  • 花洒出水效果
  • 毛巾架有无、位置
  • 沐浴用品架
  • 热水器容量(超过一个人住就要考虑热水器容量,最好还是选有天然气的)
  • 浴霸 or 暖风机
  • 排风扇
  • 浴室门情况(木门警惕底部烂掉、玻璃门注意安全性、移动门注意是否阻塞)
  • 洗脸池水龙头是否接热水
  • 洗脸池台面是否干净
  • 洗脸池高度和空间是否合适
  • 镜子大小、干净程度
  • 马桶座圈、上水、冲水是否正常
  • 马桶和洗手池是否有异味或异物
  • 洗衣机(滚筒 or 波轮)
  • 晾衣服便捷性(阳台、室内/外晾衣架)

厨房

  • 天然气 or 电磁炉
  • 油烟机
  • 水池排水
  • 油烟隔离性(不污染客厅和卧室)
  • 冰箱

客厅 & 餐厅

  • 空间大小(是否有足够的瑜伽垫空间)
  • 沙发大小、是否干净
  • 电视有无、效果(垃圾电视不如没有,否则占空间)
  • 餐桌和椅子(够用即可,太大浪费)

卧室 & 书房

  • 空间大小(坐在电脑桌前不拥挤)
  • 床的大小、高度、稳定性
  • 床头柜不宜太占空间
  • 空调位置(不要对着床吹)
  • 桌子有无、大小、稳定性
  • 衣柜容量、质量

入住准备

搬进新家往往需要购置非常多的生活用品,建议一次性在京东上买好,同一天全到达,然后就可以立即入住,且不会因为缺什么东西而反复跑超市。

清洁用品

第一天可能需要先打扫卫生(如果不打算请家政服务的话),需要下面物资:

  • 扫帚
  • 拖把
  • 乳胶手套(否则高强度使用清洁剂会伤手)
  • 洗手液
  • 清洁剂
  • 抹布 5 条以上
  • 刷子 1~2 个
  • 剪刀、快递刀
  • 抽纸
  • 湿纸巾(一定场景可以代替抹布,比较方便)
  • 垃圾袋(建议买可以手提的)
  • 垃圾桶 2 个以上
  • 洁厕宝
  • 洗衣液、消毒液(可能需要洗沙发套,顺便也试试洗衣机能不能工作)
  • 矿泉水(别忘了打扫卫生的时候自己需要喝水)

生活用品

打扫完之后,入住新房,正常生活一般需要下面物资(根据个人生活习惯不同应该有很大不同,可以先列好然后一次性搬好、买好)(有一些已经包含在上面清洁用品清单了,另一些可能房子里本来就有):

  • 鞋架
  • 遥控器电池
  • 沐浴用品架/篮
  • 牙膏沐浴露洗发水等洗浴用品
  • 牙刷
  • 漱口杯
  • 沐浴球
  • 指甲剪
  • 梳子
  • 毛巾/浴巾
  • 吹风机
  • 拖鞋
  • 洗手液/香皂+肥皂架
  • 洗衣液
  • 消毒液
  • 洗衣袋
  • 脏衣篮
  • 衣架(要考虑晾衣服和放衣柜的量,以及袜子内裤等需要带夹子的)
  • 晾衣架(用来挂衣服的)
  • 樟脑丸/香樟球
  • 防螨垫
  • 抹布
  • 刷子
  • 夹子(晾衣服、床单时夹住)
  • 挂钩(按需购买)
  • 剪刀、快递刀
  • 胶带、双面胶(可能用得着)
  • 抽纸、厕纸
  • 垃圾桶(每个卧室一个、客厅一个、厨房一个)
  • 垃圾袋
  • 洁厕宝
  • 扫帚、拖把
  • 电蚊香
  • 花露水
  • 插线板
  • 电热水壶
  • 凉水壶(也可以直接买大瓶矿泉水)
  • 水杯
  • 床垫、床单、被子、枕头(特别注意“三件套”不包含被芯和枕芯,床单要买大于床的尺寸,iOS 内置测距仪 app 可用于测量床大小)
  • 书桌/电脑桌、椅子
  • 路由器、网线
  • 耳塞、眼罩
  • 口罩
租房经验总结

ChCore 构建系统实现思路

创建于
分类:Dev
标签:ChCoreChBuildCMakeShellBuild System构建系统配置系统

读研期间的一个工作是为实验室的 ChCore 操作系统重写了新的构建系统——ChBuild,主要包括各级 CMake 脚本、配置系统和构建入口脚本。目前构建系统已经跟随 第二版 ChCore Lab 开源,所以现在可以尝试分享一下思路。如果你不了解 ChCore Lab,也没有关系,这里主要是想粗浅地介绍一些 CMake 很有趣且有用的特性和技巧,可以只看关于这些的内容。

下面的讨论基于 ChCore Lab v2 的 lab5 分支,因为这里包含了比较完整的操作系统代码结构。在阅读之前,建议你首先理解 Modern CMake By Example 中的绝大部分内容。

旧系统的问题

尽管和 ChCore 主线不完全一样,但你可以在 ChCore Lab v1 的 lab5 分支 看到旧版的 ChCore 构建系统的缩影。

主要存在的问题包括:

  • scripts/docker_build.sh 作为构建入口,只支持利用预先提供的 Docker 映像创建容器,并在容器中采用硬编码的工具链构建,无法支持在不同的本地环境中构建
  • 构建用户态程序、RamDisk 和内核的逻辑分散在不同的 shell 脚本,难以统一对构建行为进行配置(例如对用户态程序和内核统一传入某些 CMake 变量),难以维护
  • CMake 项目层级混乱,比如根目录 CMakeLists.txt 实际上在控制 kernel 的构建
  • 各子项目 CMake 脚本代码混乱,没有采用现代 CMake 的最佳实践
  • 没有比较方便可用的配置系统,无法在一个配置文件中控制整个系统的构建行为

因此,要解决这些问题,对新的构建系统提出了以下要求:

  • 构建过程应当可以在 Docker 容器中进行,也可以在本地环境进行,允许较为方便地切换构建工具链
  • 在统一的根级别 CMake 项目中管理子项目,不再把不同子项目的构建逻辑分散到不同的 shell 脚本
  • 在各级 CMake 脚本中采用现代 CMake 最佳实践
  • 支持通过类似 Linux 内核的层级 Kconfig 文件声明构建系统的配置项,通过单个 .config 文件配置整个构建行为,通过类似 make menuconfig 的命令提供 TUI 配置面板

入口脚本

新的构建入口脚本名为 chbuild,是一个 Bash 脚本。

在旧的构建系统中,构建入口脚本 scripts/build.sh(由 scripts/docker_build.sh 创建 Docker 容器后调用)实际上只能用于“构建”整个系统,不包含任何类似 Linux 内核的 make defconfig(创建默认配置文件)、make clean(清空构建临时文件)等功能。我希望在新的构建入口中通过子命令的形式提供不同的子功能。在 shell 脚本中,实现子命令其实非常简单,只需要定义子命令对应的函数,然后在脚本入口处把第一个参数当作函数名称来调用,如下:

# chbuild

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

clean() {
    _echo_info "Cleaning..."
    # ...
}

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

_print_help() {
    echo "..."
}

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

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

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

_main $@ # 调用入口 _main 函数并传入脚本的所有参数

同时,我希望用户可以在 chbuild 脚本的参数中指定要在本地环境运行还是在 Docker 容器中运行子命令。并且,我希望在 Docker 容器中运行子命令时,chbuild 不需要再调用其它脚本,而是直接在容器中用相同的参数启动自身。也就是说,不再需要区分 build.shdocker_build.sh,无论要不要在 Docker 容器中构建,都使用 chbuild 作为入口。这听起来可能有点绕,直接来看看如何实现(注意 _main 函数和上面的区别):

# chbuild

_docker_run() {
    if [ -f /.dockerenv ]; then
        # 如果已经在 Docker 容器中,直接把参数作为子命令运行
        $@
    else
        # 否则,启动 Docker 容器,并运行自身
        test -t 1 && use_tty="-t"
        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 "$self: invalid option \`$1\`\n"
            break
            ;;
        *)
            if [[ "$1" == "_"* || $(type -t "$1") != function ]]; then
                _echo_err "$self: invalid command \`$1\`\n"
                break
            fi

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

    _print_help
    exit 1
}

_main $@

于是,用户就可以通过 ./chbuild --local build 在本地环境构建 ChCore,通过 ./chbuild build 在 Docker 容器中构建 ChCore。搭配后面的配置系统,可以实现更好的本地环境跨平台构建支持。

根项目

旧的构建系统中,根项目实际上是 kernel 子项目,没有真正的根项目,对子项目的控制分散在不同的 shell 脚本中,scripts/compile_user.sh 用于调用 user 子项目的 CMake 构建,scripts/build.sh 用于调用 kernel 子项目的 CMake 构建。

在翻阅 CMake 文档的过程中,我发现了 CMake 内置的 ExternalProject 模块。这个模块的 ExternalProject_Add 命令可以把一个子目录或远程 Git 仓库添加为一个“外部项目”,同时配置它的 CONFIGURE_COMMANDBUILD_COMMANDBINARY_DIRINSTALL_DIR 等属性,还可以通过 CMAKE_ARGSCMAKE_CACHE_ARGS 属性来传入 CMake 参数和 cache 变量(也就是命令行调用 cmake 命令时可以传入的 -D 参数)。它不仅可用于添加 CMake 项目,也可以用来添加 Makefile 或是其它构建系统管理的项目。总之,这个功能非常适合用来在 ChCore 根项目中管理各子项目,这样就可以全程使用 CMake,简化构建系统(尤其是配置系统)的实现。

由于 ExternalProject_Add 这个名字显得太把自己的子项目当外人了,我把它重新定义成了 chcore_add_subproject

# scripts/build/cmake/Modules/SubProject.cmake

macro(chcore_add_subproject)
    ExternalProject_Add(${ARGN})
endmacro()

于是,可以在 ChCore 根目录的 CMakeLists.txt 中通过如下代码来添加 libchcoreuserlandkernel 子项目:

# 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=<INSTALL_DIR>
        -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=<INSTALL_DIR>
        -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=<INSTALL_DIR>
        -DCMAKE_TOOLCHAIN_FILE=${_cmake_script_dir}/Toolchains/kernel.cmake
    DEPENDS userland kernel-clean-incbin
    BUILD_ALWAYS TRUE)

可以看到,通过 ExternalProject 模块可以非常简单而清晰地添加一个 CMake 子项目并传入指定参数、设置 CMAKE_TOOLCHAIN_FILE 工具链文件、设置子项目和其它 target 间的依赖关系等。

根项目中还通过 custom target 的形式提供了子项目的 clean 动作:

# 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 ] && 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)

于是,在 chbuildclean 子命令中就可以通过 cmake --build $cmake_build_dir --target clean-all 来清理所有子项目的构建临时文件。根项目的 build 目录则直接在 chbuildclean 子命令函数中通过 rm -rf $cmake_build_dir 来 clean。这里的理念是,谁负责控制一个(子)项目的构建过程,谁就负责这个(子)项目的 clean 过程。

子项目和工具链文件

这部分跟 ChCore 操作系统本身的相关性比较强,如果你不了解或者不感兴趣,其实可以跳到 配置系统

libchcore 子项目

libchcore 子项目用于构建 LibChCore,即对 ChCore 内核系统调用接口和一些关键系统服务 IPC 接口的封装库(产物是 libchcore.a 和相关头文件),以及 crt0(产物是 crt0.o)。其实这个子项目的 CMake 相关内容只有一个 libchcore/CMakeLists.txt,没有太多值得介绍的内容,主要是可以通过 install 命令安装 target 文件、目录、其它文件到指定目标路径:

# 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 "*.h")

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)

这些 install 命令的目标地址没有使用绝对路径,而是使用了相对的 includelib。这些相对路径相对的是在根项目 chcore_add_subproject 时通过 CMAKE_INSTALL_PREFIX 参数所指定的安装目录 ${_libchcore_install_dir}(见前面)。

当根项目 build 时,通过 chcore_add_subproject 添加的子项目会被 configure、build、install。同时,子项目间有依赖关系,于是可以保证在 build userland 子项目时,libchcore 子项目已经将 LibChCore 的头文件和静态库以及 crt0.o 安装到 ${_libchcore_install_dir} 目录,因而在 userland 子项目中可以正确的包含 LibChCore 头文件、链接 LibChCore 静态库和 crt0.o

userland 子项目

userland 子项目用于构建用户态系统服务和应用程序。基本逻辑是添加一些全局的编译和链接选项(因为需要应用到该子项目的所有 target),然后通过 add_subdirectory 一层层包含下去。

除此之外,该子项目还需要在一些系统服务和应用程序构建完成之后,将它们打包成 CPIO 格式的 RamDisk,这是比较有趣的地方,来看代码:

# 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 > ${CMAKE_CURRENT_BINARY_DIR}/ramdisk.cpio)

# 第二块
function(chcore_copy_target_to_ramdisk _target)
    add_custom_command(
        TARGET ${_target}
        POST_BUILD
        COMMAND cp $<TARGET_FILE:${_target}> ${_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)

第一块首先删除已经存在的 RamDisk 临时目录,然后重新创建,接着定义 ramdisk.cpio custom target,行为就是把 RamDisk 临时目录打包成 CPIO 文件。

第二块定义了两个 CMake 函数:chcore_copy_target_to_ramdiskchcore_copy_all_targets_to_ramdisk。前者用于把一个 target 的产物拷贝到 RamDisk 临时目录,实现上就是为这个 target 添加一个 POST_BUILD(构建后)custom command,在其中进行拷贝。由于拷贝需要先于 ramdisk.cpio target 的打包操作,因此还需要通过 add_dependencies 添加依赖关系。后者用于把调用处可见的所有 target 的产物拷贝到 RamDisk 临时目录,实际上就是通过 chcore_get_all_targets 获得 target 列表,然后对其中没有单独调用过前者的 target 调用前者。

第三块是包含下级 CMakeLists.txt,进而递归地包含到 userland 的所有 CMakeLists.txt,在其中的某些地方会调用第二块定义的函数。比如:

# userland/apps/lab5/CMakeLists.txt

add_executable(...)
add_executable(...)
chcore_copy_all_targets_to_ramdisk()

kernel 子项目

kernel 子项目用于构建内核映像文件 kernel.img。逻辑非常简单,首先创建 kernel.img target,然后为其设置一些编译链接选项和包含目录,接着一级一级包含下面的所有模块的 CMakeLists.txt,在其中通过 target_sourceskernel.img 添加源文件。

比较值得介绍的是通过 configure_file 来从模板生成文件,可以在模板文件中通过 ${var_name} 引用 CMake 变量。结合配置系统,可以尽量减少相关文件中写死的内容。在 kernel 子项目中,这个技巧用于生成 incbin.Slinker.ld

# kernel/incbin.tpl.S

        .section .rodata
        .align 4
        .globl __binary_${binary_name}_start
__binary_${binary_name}_start:
        .incbin "${binary_path}"
__binary_${binary_name}_end:
        .globl __binary_${binary_name}_size
__binary_${binary_name}_size:
        .quad __binary_${binary_name}_end - __binary_${binary_name}_start
# 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})
# kernel/arch/aarch64/boot/linker.tpl.ld

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

    # ...
}
# kernel/arch/aarch64/boot/CMakeLists.txt

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

工具链文件

libchcoreuserlandkernel 子项目中,都没有任何设置构建工具链(C 编译器命令名等)的内容,这些内容应该放在独立的、通过 CMAKE_TOOLCHAIN_FILE 指定的 工具链文件 中。其实工具链文件里的内容放在 CMakeLists.txt 也能正常工作,但是放在工具链文件中,CMake 可以在 configure 项目前首先通过测试项目来检查工具链是否可以正常使用。

新的构建系统提供了两个工具链文件:userland.cmakekernel.cmake,都在 scripts/build/cmake/Toolchains 目录中。在根项目中添加各子项目时,为 libchcoreuserland 指定了 userland.cmake 工具链文件,为 kernel 指定了 kernel.cmake 工具链文件。ChCore Lab 中这两者内容其实很接近,但在 ChCore 主线中则有更多不同。这里只放一下 kernel.cmake 工具链的部分代码:

# scripts/build/cmake/Toolchains/kernel.cmake

# Set toolchain executables
set(CMAKE_ASM_COMPILER "${CHCORE_CROSS_COMPILE}gcc")
set(CMAKE_C_COMPILER "${CHCORE_CROSS_COMPILE}gcc")
# ...

include(${CMAKE_CURRENT_LIST_DIR}/_common.cmake)

# Set the target system (automatically set CMAKE_CROSSCOMPILING to true)
set(CMAKE_SYSTEM_NAME "Generic")
set(CMAKE_SYSTEM_PROCESSOR ${CHCORE_ARCH})

userland.cmakekernel.cmake 工具链文件在设置完 C 编译器等工具链命令后,会包含 _common.cmake。这个文件是工具链文件的共用部分,主要工作是从 C 编译器推导出编译目标体系结构(通过 execute_process 运行 gcc -dumpmachine),并设置到 CHCORE_ARCH cache 变量,然后再把所有 CHCORE_ 开头的 cache 变量添加为编译选项,以便在 C 语言代码中进行条件编译。这里绝大部分 cache 变量都是从配置文件读入的,更多细节会在后面配置系统的 配置的传递 部分详细介绍。

包含完 _common.cmake 之后,两个工具链文件分别设置了 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_PROCESSOR。这会告知 CMake 当前项目正在进行跨平台编译,并指导 CMake 使用正确的 sysroot、链接器行为等。在 userland.cmake 工具链中指定了 CMAKE_SYSTEM_NAMEChCore,这个系统相关的跨平台构建行为配置在 scripts/build/cmake/Modules/Platform/ChCore.cmake 文件中定义,由于 ChCore 用户态程序的构建行为和 Linux 基本一致,因此这里直接包含了 CMake 内置的 Platform/Linux,可以在 /usr/share/cmake-x.xx/Modules/Platform/Linux.cmake代码仓库 中看到后者的内容。kernel.cmake 工具链中则指定系统为 Generic,因为内核实际上并不是任何操作系统上的应用程序,设置为 Generic 会让 CMake 不对内核的运行环境做任何假设,因此做更少的构建行为配置。其实这里设置这两个变量的实际用处不算大,因为相关子项目中已经对链接选项进行了配置,且都不会链接 C 标准库、系统中安装的第三方库等,之所以设置主要是为了保持优雅。

配置系统

配置系统是 ChCore 新构建系统的精髓之一,与 ChCore 架构本身没有什么关系,不需要了解 ChCore Lab 也可以看看。

config.cmake.config 文件

从用户(ChCore 的开发者和构建者)角度来看,新的配置系统对外表现为两个部分,分别是层级的 config.cmake 配置声明文件和根目录的 .config 配置文件。

层级的 config.cmake 配置声明文件与 Linux 内核的 Kconfig 文件类似:

.
├── kernel
│   └── config.cmake
├── userland
│   └── config.cmake
└── config.cmake

从根目录 config.cmake 开始的每一级 config.cmake 中,可通过 chcore_config_include 命令包含下一级 config.cmake 文件,形成树状结构;通过 chcore_config 命令声明该层级的配置项,每个配置项包括名称、类型、默认值和描述四项内容。例如根目录 config.cmake 部分内容如下:

# config.cmake

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

chcore_config_include(kernel/config.cmake)
chcore_config_include(userland/config.cmake)

这里 chcore_config_include 命令比较简单,实际上是一个内部调用 CMake 内置 include 命令的宏:

# scripts/build/cmake/Modules/CommonTools.cmake

macro(chcore_config_include _config_rel_path)
    include(${CMAKE_CURRENT_LIST_DIR}/${_config_rel_path})
endmacro()

chcore_config 命令则稍微复杂一些,是配置系统的核心,运用了一些技巧,下个小节会详细说明。

根目录的 .config 配置文件是单个扁平的文件,与 Linux 内核的 .config 文件类似,形如:

# .config

CHCORE_CROSS_COMPILE:STRING=aarch64-linux-gnu-
CHCORE_PLAT:STRING=raspi3
CHCORE_VERBOSE_BUILD:BOOL=OFF

用户可以通过 ./chbuild defconfig 生成默认的 .config 文件,其中包含目前所声明的所有配置项的默认值,也可以通过 ./chbuild menuconfig 或者手动编辑该文件来修改配置项的值。在构建时,构建系统会读取该配置文件中的值,并设置到 CMake cache 变量,从而控制构建行为。

配置的加载

加载 .config 文件应该在 ChCore 根项目的 configure 阶段开始之前完成,因为 configure 阶段即运行 CMakeLists.txt 时,已经需要使用配置值。一个 naive 的思路是直接在 chbuild 脚本中读取并解析其内容,将解析出的 (key, type, value) 三元组构造成 CMake -D 参数序列,例如 -DCHCORE_CROSS_COMPILE:STRING=aarch64-linux-gnu-。如果只是单纯读取用户已经填写的配置,这个思路是可行的,但我不想满足于此,我希望实现:

  • 对于 config.cmake 中声明了,但 .config 中没有填写的配置项,根据情况采取三种不同的策略来处理,分别是:
    • 使用默认值:直接将配置值设为 config.cmake 中声明的默认值
    • 交互式询问用户:在命令行询问用户是否需要使用默认值,若不使用,则要求输入一个值
    • 中断构建流程:直接停止构建
  • 对于 .config 中填了,但实际上没在任何 config.cmake 中声明的配置项(可能是已经删除的旧配置项),过滤掉,不传入子项目
  • 尽量少地编写 shell 脚本,因为 shell 脚本比 CMake 脚本更容易写错、更难维护

经过一番搜寻,我发现 CMake 的 initial cache 功能可以用来实现这些要求。该功能允许通过 cmake 命令的 -C 参数指定一个 CMake 脚本,并在 configure 之前首先运行这个脚本,以填充 CMake cache,也就是设置一系列 cache 变量。在 initial cache 脚本中,可以使用完整的 CMake 语法,也就是说,可以通过 include 包含其它 CMake 脚本、通过 file(READ ...) 读取文件内容、通过 macro/function 定义宏/函数等。

于是,我决定利用这个功能,在 initial cache 脚本中加载 .config 文件。这带来的另外一个好处是,在 chbuild 脚本中只需切换 -C 参数的值,就可以很方便地切换配置加载策略,如下:

# chbuild

cmake_script_dir="scripts/build/cmake"
cmake_init_cache_default="$cmake_script_dir/LoadConfigDefault.cmake"
cmake_init_cache_ask="$cmake_script_dir/LoadConfigAsk.cmake"

_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 根项目
    # ...
}

具体的 initial cache 文件如下:

scripts/build/cmake
├── LoadConfig.cmake
├── LoadConfigDefault.cmake
├── LoadConfigAsk.cmake
├── LoadConfigAbort.cmake
└── DumpConfig.cmake

LoadConfigDefault.cmakeLoadConfigAsk.cmakeLoadConfigAbort.cmake 分别实现了使用默认值、交互式询问用户、中断构建流程三种配置加载策略,LoadConfig.cmake 则是它们的通用部分。

DumpConfig.cmake 是一个特殊的 initial cache,用于把 CMake cache 中的配置值同步回 .config。之所以需要 DumpConfig.cmake,是因为在通过“使用默认值”或“交互式询问用户”策略加载配置后,CMake cache 中可能包含 .config 所没有填写的配置,需要把这些配置同步到 .config,以保证 .config 始终反映构建系统实际使用的配置。

下面着重介绍 LoadConfigDefault.cmakeDumpConfig.cmake,其它 initial cache 只是略有不同。

首先看 LoadConfigDefault.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)

它首先定义了 chcore_config 宏,行为是,当 ${_config_name} 也就是配置名称所对应的 CMake cache 变量不存在时,设置该 cache 变量为配置项所声明的默认值。还记得在 config.cmake 文件中声明配置项的时候使用的 chcore_config 命令吗,config.cmake 中传入的配置名称、类型、默认值、描述四个参数,就是这个宏的四个参数。不过,我们并不能说 config.cmake 中用的就是这里定义的宏,后面你会逐渐理解这一点。

随后它 include 了 LoadConfig.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 "\n" ";" _config_lines "${_config_str}")
    unset(_config_str)

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

# 第二块
# Check if there exists `chcore_config` macro, which will be used in
# `config.cmake`
if(NOT COMMAND chcore_config)
    message(FATAL_ERROR "Don't directly use `LoadConfig.cmake`")
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("${_config_name}" "${_config_type}" "${_default}"
                       "${_description}")
    endif()
endmacro()

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

第一块是在加载和解析 .config 文件,比较直白。首先读取文件内容,然后用正则从每一行中提取配置名称、类型、值三元组,通过 set(... CACHE ... FORCE) 设置为 cache 变量。此时 .config 中的所有配置都已经进入了 CMake cache。

第二块检查是否定义了 chcore_config 命令。这是为了避免不小心在 chbuild 中直接使用 LoadConfig.cmake 作为 initial cache,要求必须在 LoadConfigDefault.cmake 等文件中定义了 chcore_config 宏后再 include(LoadConfig.cmake)

第三块定义了一个新的 chcore_config 宏。这里运用了 一个 CMake 技巧,当重复定义宏/函数时,旧的宏/函数名称会被加上下划线。也就是说,定义了新的 chcore_config 之后,可以通过 _chcore_config 调用到上一次(在 LoadConfigDefault.cmake 中)定义的 chcore_config。这个宏的作用是,在后面 include 根目录的 config.cmake 时,如果配置名称对应的 cache 变量已经定义(也就是出现在 .config 中了),则为其设置变量描述(description),否则调用先前定义的 chcore_config,也就是执行 LoadConfigDefault.cmake 中设置 cache 变量为默认值的逻辑。之所以要设置 cache 变量的描述,是为了在之后的 menuconfig 中显示声明配置项时的描述。

第四块是包含(也就是执行)根目录的 config.cmake 文件,该文件进而会递归地通过 chcore_config_include 包含到所有的 config.cmake,并调用上面第三块中定义的 chcore_config 宏。根据前面已经说明的逻辑,该过程中,遇到 .config 中已填写的配置项时,会设置 cache 变量的描述,遇到没有填写的配置项时,会设置 cache 变量为所声明的默认值。

再来看 DumpConfig.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
         "${_config_name}:${_config_type}=${${_config_name}}")
endmacro()

include(${CMAKE_SOURCE_DIR}/config.cmake)

string(REPLACE ";" "\n" _config_str "${_config_lines}")
file(WRITE ${CMAKE_SOURCE_DIR}/.config "${_config_str}\n")

这个 initial cache 不需要包含 LoadConfig.cmake,而只需要定义一个 chcore_config,然后直接包含根目录 config.cmake。这里的逻辑是把所有声明的配置项在 CMake cache 中实际设置的值 append 到 _config_lines,随后写入 .config 文件。其实通过 cmake -B build -L -N | grep ^CHCORE_ > .config 命令可以更快地做到这件事,但无法保留配置项声明的顺序,对用户不是很友好。

到这里,如果你经常写 C 语言,尤其经常写宏的话,应该已经明白 config.cmake 文件其实应用了类似 C 语言中的 X-Macros 技巧。通过定义不同的 chcore_config 命令,再 include 根目录 config.cmake,实现了同一组 config.cmake 文件在不同地方 include 时产生不同的行为。

配置的传递

配置加载后首先进入根项目的 cache,由于各子项目都是独立的“外部”CMake 项目,不能直接访问根项目的 cache 变量,因此根项目还需要在添加子项目时传递配置内容。为了收集所有配置内容,以便在 chcore_add_subproject 时传入,再次使用了 X-Macro 技巧,将所有配置名称、类型和配置值拼成 -D 参数序列,放到 _cache_args 变量中:

# 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 "...")
    endif()
    list(APPEND _cache_args
         -D${_config_name}:${_config_type}=${${_config_name}})
endmacro()
include(${CMAKE_CURRENT_SOURCE_DIR}/config.cmake)

这里定义 chcore_configinclude(config.cmake),而不是遍历所有 CHCORE_ 开头的 cache 变量,是为了实现前面所希望的,过滤掉 .config 中填写了、但实际已不在任何 config.cmake 中声明的配置项。如果不需要过滤,也可以采用类似下面 chcore_dump_chcore_vars 函数的方式(VARIABLES 改成 CACHE_VARIABLES):

# 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 "^CHCORE_" _matched ${_variable_name})
        if(NOT _matched)
            continue()
        endif()
        message(STATUS "${_variable_name}: ${${_variable_name}}")
    endforeach()
endfunction()

把所有配置项拼成 -D 参数序列后,在 chcore_add_subproject 时通过 CMAKE_CACHE_ARGS 属性即可传入子项目:

# 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})

这样,所有配置项就已经进入了子项目的 cache,也就是可以在子项目的 CMake 脚本中访问,例如:

# kernel/CMakeLists.txt

if(CHCORE_KERNEL_TEST)
    add_subdirectory(tests)
endif()

但这还不够,我希望把这些配置传递给 C 代码,从而可以通过 #ifdef 等预处理指令来进行条件编译:

#ifdef CHCORE_KERNEL_TEST
    some_test();
#endif /* CHCORE_KERNEL_TEST */

旧系统中,这是通过各子项目独立添加 definition 实现的,可维护性非常差。新系统则在 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 "^CHCORE_" _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}="${${_var_name}}")
    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})

这里首先遍历所有 CHCORE_ 开头的 cache 变量,如果类型是 BOOL,则根据其真值决定要不要添加同名的 definition,也就是可以在 C 代码里通过 #ifdef 判断其真值;如果类型是 STRING 则一定会添加该 definition,值是配置值字符串。举个例子,.config 中的配置 CHCORE_PLAT:STRING=raspi3CHCORE_KERNEL_TEST:BOOL=ON 在此处产生的效果相当于下面 C 预处理指令:

#define CHCORE_PLAT "raspi3"
#define CHCORE_KERNEL_TEST

为了在代码中更方便地判断当前处理器架构和硬件平台(因为 #if 无法对字符串进行比较),对 CHCORE_ARCHCHCORE_PLAT 不仅定义了字符串,还定义了表示具体架构和平台的空 definition。比如在 AArch64 架构和树莓派 3 平台,这里添加的 definition 相当于:

#define CHCORE_ARCH "aarch64"
#define CHCORE_PLAT "raspi3"
#define CHCORE_ARCH_AARCH64
#define CHCORE_PLAT_RASPI3

menuconfig 子命令

配置系统的另一个需求是让 ./chbuild menuconfig 子命令实现类似 Linux 内核 make menuconfig 的 TUI 配置面板。由于已经全面采用了 CMake cache 变量和 initial cache 功能,一个自然的想法是复用 ccmake 命令。

这里其实有一些不够优雅的地方,因为在 ccmake 提供的配置面板中,需要按 C 键(Configure)来把修改的配置值刷到 CMake cache 中,也就是保存配置。这和一般直觉中的 S 键(Save)不同,但是没有找到好的修改办法,只能在运行 ccmake 命令之前输出一些红字提示用户。最终实现如下:

# chbuild

cmake_init_cache_dump="$cmake_script_dir/DumpConfig.cmake"

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

menuconfig() {
    _check_config_file
    _config_default

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

    ccmake -B $cmake_build_dir # 复用 ccmake 提供的 TUI 配置面板
    _sync_config_with_cache # 同步 CMake cache 回 .config
    _echo_succ "Config saved to \`$config_file\` file."
}

前面提到 DumpConfig.cmake initial cache 用于把 CMake cache 中的配置同步回 .config 文件。这里用户在 ccmake TUI 面板中修改配置后,也需要进行这个同步操作,才能把修改反映到 .config

总结

尽管构建系统和代码本身其实没有很大的直接关系,但我相信一个优雅的构建系统仍然非常重要,因为它会极大地影响开发者的体验。一个优质的构建系统可以让开发者更方便、更舒适地为系统扩充功能。

在重写 ChCore 构建系统时,我的理念是在入口层面提供与 Linux 内核相似的体验,而下面的实现则尽量充分利用 CMake 的一切可利用的特性,并遵循现代 CMake 的最佳实践,最终效果基本达到了我理想的状态。

ChCore 构建系统实现思路

2022,确实是一个新的开始

创建于
分类:Misc
标签:年度总结

又是一年没有更新博客了,上一次一年没更新还是准备考研的 2019 年。

今年年初的时候写了一篇 2022,会是一个新的开始吗,很简略,寥寥几笔就把 2021 的一年低谷带过了。去年年底到今年年初,情况逐渐开始好转,随后的一年虽然也有一些起伏,但总体还是好于年初时候对今年的期待。2022 年,确实是一个新的开始,或许是我人生的一个转折点。

OS 助教和毕业设计

今年的上半学期,也就是研二下学期,终于当了一次操作系统课的助教,主要就是参与出 lab 和回答同学们的问题。出 lab 是从实验室的操作系统 ChCore 主线上做裁剪,然后挖出一些空,期间也发现了 ChCore 一些有 bug 和值得重构的地方,又 port 回主线,算是为 ChCore 做的最后贡献了。回答同学们的问题也很有趣,我很喜欢帮到别人的感觉,一些同学也很能抓到要点,许多时候是在相互交流进行思维升级,而不是单方面的回答。

这学期的后半段,另一件最重要的事情就是毕业设计了,是在 ChCore 上尝试一种新的系统服务设计。开题的时候,正逢上海疫情封校封楼,在宿舍里写代码效率很低,当时非常焦虑,担心在暑假前写不完,影响之后的实习。后来焦虑带来了动力,通过几天熬夜快速进入了状态,虽然之后效率又逐渐下降,但还是在暑期实习之前基本写完了毕设的代码。到了 11 月暂停实习之后,则是边摸鱼边写毕业论文,到 12 月初算是把论文基本写完了,虽然直到现在还有需要修改的小地方🤣。

实习、校招和 RisingWave

6 月底的时候终于开始了在 Singularity Data(现在已经改名叫 RisingWave Labs)的实习,参与维护公司开源的 RisingWave 流式数据库内核。其实在实习之前,我根本完全不懂数据库的原理,更不懂流式数据库。刚开始不知道从何下手,后来慢慢做了一些简单的任务,开始进入状态,到最后也算是基本能理解整个数据库的设计思想了(不过感觉以后还是需要补一些比较系统的课程)。公司里面大部分同事都是聪明而有态度的年轻人,和他们一起在开源项目上工作,让我感到极度舒适,这是我一直以来梦寐以求的感觉。

在实习的同时参与了校招,面试的公司不算太多,有一些面到一半已经不想再面于是放弃了,最终真正完成并通过面试的公司只有字节、华为和百度昆仑芯三家。虽然面试的时候感觉发挥都挺好,但由于大环境的压力,这些公司都迟迟没有发 offer,甚至一直到 9 月底都没有收到任何一个意向(后两家其实没有意向阶段),字节则是分别由不同的组捞了好几次。那段时间非常焦虑,但又不想再投简历了,想着就听天由命吧,毕竟转正应该是肯定可以转的。到了 10 月底,面完了的三家公司都陆续给了意向或开奖,焦虑的心情终于迎来解脱。后来,经过和 mentor、和朋友、和自己的充分沟通,最终还是决定在 Singularity Data 转正,想把这种开源、现代、自驱、自由的工作方式继续下去。

OneBot 和 WasmEdge

在这一年断断续续的一些时间点,也尽力推进了之前聊天机器人方向上的开源项目 OneBot。目前 OneBot 12 标准已经基本上稳定,也有了一些对它的实现。由于自己已经很久没有写过聊天机器人,越来越明显地感觉到自己在这个方向上已经不再有激情、动力和洞见了,暂时还不知道该怎么办。不过,看到一些朋友在积极地尝试采用 OneBot 12 标准,还是很开心的。

暑期实习的下班时间还抽空参与了 WasmEdge 社区的一个 LFX Mentorship 项目。项目本身并不是改进 WasmEdge Runtime,而是移植一个数据库的客户端 SDK 到 WasmEdge 上运行,虽然做起来比较简单,但在过程中还是学到了许多 WebAssembly 相关知识,了解了一个 WASM 运行时的基本结构等等。

新冠疫情防控

3 月和 9 月分别经历了两次封校、封宿舍楼,4 月和 11 月分别看到了两次朋友圈“电子游行”,后者也伴随了在各地上演的真实抗议。从 3~5 月上海摇摆后重新“坚持动态清零”,再到“二十条”、“新十条”、全国逐渐取消公共场所核酸要求,最后在 12 月 26 日宣布明年 1 月 8 日开始对新冠病毒实施“乙类乙管”,我想我算是见证了历史,见证了一段浓墨重彩的历史。新冠疫情防控开始于我考完研后的仅仅一个月,结束于我硕士毕业前的一个月,我的整个硕士生涯几乎完全笼罩在疫情和疫情防控造成的不确定性中。现在,这段时期结束,一个新的时期正在开始,我感到悲壮,也感到人类的渺小。

12 月 22 日,我终于第一次感染并发作了新冠,先是发高烧 2~3 天,然后咽痛不断加剧,有三个晚上难以入睡,到今天(27 号),尽管因为不再发烧,精神已经好了很多,但身体状况仍然没有完全恢复。

读书和思考

从年初开始决定记录今年的阅读情况,记录读了什么书、对书的评价,以及一些简单的读后感或是总结。Notion 提供了很好的自定义数据库和视图功能,帮助我方便地管理这些记录,我把今年开始已读的书都公开在了我的 读书 页面。

2022 年已读列表
2022 年的已读列表

最后,2022 年一共读了 16 本书,超出了一开始的预期。一些书补充完善了我的世界观、价值观和人生观,让我对自然、社会、人、世界等一切有了更好的理解。读书的过程,就像曾经某位名人(忘了是谁)所说的,像在和作者对话,这种对话不是单向的听取,而是有来有回且循序渐进的思想交流。这些书中不一定每个观点都让我十分信服,甚至有一些我持反对意见,但它们都让我得到了一些精神上的收获。

除了记录阅读,今年还开始记录了自己对各种事情的思考,希望用文字的形式把这些思考固化下来,以便以后可以找寻自己思维发展的过程。直到年底,已经记录了 24 个思考。我发现把对事物的思考写成文字,可以强迫自己更全面地考虑问题,而不是只对事物的一个方面产生情绪化的反应。

2022 年思考列表
2022 年的思考列表

音乐现场

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

房东的猫
房东的猫,苏州
达闻西
达闻西乐队,上海
达达
达达乐队,杭州

再看去年定的目标们

  • 做好实验室项目(也就是 ChCore)的最后工作,为研究生生涯收尾
  • 做好暑期实习,保持学习新技术,并在秋招找到理想的正式工作
  • 更多地参与一些开源项目的贡献
  • 保持运动,让自己变得更健康
  • 读更多人文社科类的书,扩展自己的文化视野
  • 努力做到“己所不欲,勿施于人”

不可思议地,除了运动之外(也不是没运动,但从全年来看还是太少了),这些目标(我想)大概都算是完成了。不过想想毕竟目标其实都很笼统,也没啥完不成的。

不可思议
在上海博物馆拍到的“不可思议”

即将开始的 2023 年

新的一年,或许还是像去年一样定一些笼统的目标吧,这样在保持总体进步趋势的同时,可以允许自己在一年中对具体的计划有调整。

那么,希望自己可以在 2023 年,

  • 少输出观点,尤其避免在有争议的问题上输出不成熟的观点并意图说服别人,多输入,多思考,兼听则明;
  • 多旅游,亲身感受这个真实的世界;
  • 读更多书,包括一些文学、哲学、社会、西方政治历史相关的书;
  • 学习更多数据库知识,以更好地向 RisingWave 和可能的其它数据库开源项目贡献代码;
  • 继续或重新开始开发 rcOS;
  • 学习一些写代码之外的有趣技能;
  • 多运动。

就这样吧,结束这篇比去年冗长许多的年度总结,致新的人生~

2022,确实是一个新的开始