光に覆われし漆黒よ。夜を纏いし爆炎よ。紅魔の名のもとに原初の崩壊を顕現す。終焉の王国の地に、力の根源を隠匿せし者。我が前に統べよ!エクスプロージョン!
13839 words
https://typeblog.net

除了自己,没有人能保护你的隐私

让我们再一次翻出“隐私”这个亘古不变的话题。

不久前,国内某大厂的 App 在微博上被曝光偷偷后台读取隐私信息,也有可能在不为人知的情况下将其上传。因此,我写了一条长微博,主要内容是宣传「在互联网时代,只有自己在意隐私,自己保护隐私,隐私才能得到保护」,指出了某 App 的问题非常可能仅仅是冰山一角。微博一出,我收到了大量完全是出于误读的回复,包括但不限于诸如 「Android 真惨,我用 iOS 就不怕了」 / 「普通用户没有能力保护自己的隐私,只有政府监管才行」 等等。更有甚者认为我是在为监管的缺失开脱,甚至以为我是在给出事的某大厂 App “洗地”,以反讽的态度在评论我的微博。我实在缺乏更多与这些根本没有读懂我在说什么的人争论的动力,因此部分这样的微博用户被我暂时屏蔽 —— 没有别的意思,仅仅是感到疲倦而已,如果被我屏蔽的用户正在看这篇文章的话,我在此道歉,但是我已经停用微博,所以屏蔽与否已经没有意义;但是我仍然感觉到有必要单独写一篇更详细的文章和大家聊一聊这个话题。微博这类社交平台不是一个适合放大于 140 字的文章的地方,因此我决定写这篇博客。

首先,我还是想谈谈某些人与我争论不休的监管和个人自身的隐私意识的问题。必须明确的一点是,我对在保护隐私权方面的监管永远是欢迎的 —— 否则我也不会对 GDPR 的出现表示支持。来自强制力量的对隐私权保护的监管对企业永远是有约束作用的,永远是聊胜于无的,并且“他们”也 有责任 去保护一般人的隐私权。只是,这完全不是我所发布的内容的关注点 —— 我所关注的是,从个人的角度来看,除了自己,你没有任何其他人去信任、去依赖,期望他们能够为你的隐私权而战。换句话说,监管确实是必须的,但是一般用户没有理由信任任何政府、任何“平台”性质的企业的监管措施,所有站在个人的角度对监管的盲目信任和期待都是妄想。

因为「不保护隐私」相对于「保护隐私」实在有太多的利益隐藏在后面。不管是从企业的角度还是从监管者的角度,对隐私权保护的松懈往往可以带来丰厚的利润 —— 隐私可以用于“用户画像”,可以用于更加精准的广告投放,可以用于“人工智能”的训练,可以用于所谓“大数据”,甚至可以用于某些见不得人的交易。在今天的环境下,用户的隐私就可谓是数不尽的金钱 —— 而隐私又不像是社会治安,也不像是财产权,对很多人来说又不痛又不痒。仔细考虑这些,怎么会有人觉得一纸规定就足够保护自己了呢?假使用户自己不去关心、不去争取,还凭什么去信任别的人会愿意放弃自己更多的利益去保护与他们完全不相关的利益?从某种程度上,正是某些用户的这种信任,使得有的人肆无忌惮地获取并利用他人的隐私作为自身获利的来源 —— 因为只要大部分人不关心,作恶就没有一个能够抵消那些利益的后果。比起监管的缺失,一般人的隐私意识的缺失只会更加可怕。

从另一个角度来说,有效的监管永远只会出现在隐私意识普及之后。在隐私意识得不到普及的情况下,即使存在 GDPR 这样的法律,作用也会非常有限 —— 它总是需要有人去发现违反规定的人,需要大家知道什么时候自己合法的权利受到了侵犯,需要人们知道去利用已有的规定为自己争取权利。没有人能像父母一样随时监护着每一个人。还是像刚刚我所说的那样,只要大部分人都不关心,那么即使是违法也很可能不会出现非常严重的后果 —— 毕竟,只要没人关心,什么“交易”都是有可能存在的,监管也就成了一纸空文。在这种情况下,铤而走险反而变成了一个在追逐最大利益的情况下权衡风险以后合理的选择。而反过来,在隐私意识普及的时候,即使完善的监管体系暂时不存在,即使“他们”仍然想要利用隐私获利,“他们”也只能处处小心。如果只要露出一点点马脚就能引发大部分人的担心和唾弃,在最坏的情况下,他们的作恶行为也必须要付出比以前更高的成本,也必须承担大得多的风险 —— 因为现在,关心隐私的人变成了他们的主流用户群体。在这点上,看看那些注重隐私的服务的用户群体分布就非常明显了 —— 那些为我们所称赞的服务,往往从最初(在没有以“注重隐私”受到称赞的时候)开始面向的主流用户就是更倾向于注重隐私的群体,比如自由软件用户和开发者。他们具有足够的隐私意识,也有能力去反过来限制服务提供者的行为。

有人尝试用警察的责任的类比来反驳「一般人需要知道保护自己的隐私」,但我认为这个类比恰恰可以成为对以上内容的支撑。试想,假如大部分人不关心自己的人身安全,认为自己被杀被剐都不是一件大事,看见自己身边的人被害也想不到为他们伸冤,那么故意杀人还会成为一个(可能的)死罪吗?假如没有人会因为公共安全事件而不安,还会有真正干活的警察存在吗?我不知道他们怎么看,我只能说我认为不会。这当然 绝对不是他们不作为的理由,只是在没人意识到的情况下,他们不管怎么做都不会触发一丝疑问,那么我们就只能用最坏的可能来预测他们的行为。而这正是隐私意识的现状 —— 人们不知道也不关心什么时候自己的隐私受到了侵犯,就像不知道对人身安全感到不安一样。如若不是这样,怎么会出现诸如「不添加某支付厂商的私有系统级黑箱组件的系统就受到唾骂」的现象?因此,对于这个类比,我认为结论只能是,对于“他们”什么能做、什么不能做的共识是产生合理的监管和对监管的执行的监督的前提。

然而即使有了对于隐私权保护的共识,我们还不能忘记的一点是,每个人对于隐私的定义很可能是不同的。这一点可能永远无法改变 —— 你可能认为你的位置信息是隐私,而有的人可能认为只有敏感的私人照片才算作隐私。这是极端的例子,毕竟有隐私意识的人一般不会把隐私的范围定义弱化到这种地步,但是不同必然是存在的;科技发展也可能带来新的、潜在的、但目前还有争议的隐私数据,比如 DNA 数据。即使监管已经存在,它往往只能用于提供对共识中的隐私范围的保护,非常有限而且滞后;只要一个人的隐私有超出“默认范围”的,他就只能依靠自己,或者等待共识的范围随着隐私意识的增长渐渐扩大。后者对于一个人来说显然是不现实的:他不可能依靠任何人预先定义的一个集合来保护自己的隐私;他必须能够根据自己独立的思考做出自己的选择。这也是隐私与其他权利的一个不同:它不一定有统一的、泾渭分明的界定。

从以上内容中我想已经不难看出,那些鼓吹依靠 「XX公司」「XXOS」 甚至 「XX国」 就能解决隐私问题的想法是有多么可笑。如果自己不重视,就不可能会有人有动力为你重视。但是还有一类人,认为「普通用户想靠自己保护自己是不可能的,只有XX被改变/消灭才可能有效」,甚至以此对以上观点加以嘲讽,私以为这和前面那种事实上是同一种类型的 b*llsh*t。当然,我完全理解这类人可能遭受过无数不公正的待遇,可能感到非常无力和无助;但是这并不能成为认为普通人什么也改变不了的理由。有的人在逃避保护他人隐私权的责任,你就连自己的都不尝试保护一下,甚至对其他在尝试保护自己的人加以嘲讽?还是期望着等你换成了 XX 就再也不会遇到隐私问题?如果是这样的话,那也和前一种人没有什么区别了。我只知道,当我不敢去做某一件我认为正确的事情的时候,我不会去嘲讽任何有勇气去做的人 —— 我会暗自佩服他们。不知从什么时候开始,不管是什么东西都变成了“普通人什么也做不了”,只能躺下打滚撒娇,大喊“谁来管管”?普通人还能做什么?等着别人来喂饭吗?这就是大家追求隐私想要的结果吗?不,这正是那些敢于明目张胆侵犯隐私的人想要的 —— 当人们从不关心变成了即使了解也懒得关心,一切都结束了。况且,在彻底变成这种样子之前,保护隐私权和某些其他的“维权”也不是一回事 —— 就算你没有地方可以“哭诉”,没有人给你“主持公道”,只要稍微愿意钻研的人,还是有方法至少 尝试 保护好自己的 —— 至少目前在世界的大多数地方仍然是可能的。至于未来,我想或许连现在仅剩的保护自己的机会都会被某些人白白送给 「XX公司」「XXOS」 甚至某些更可怕的实体吧。到那时候,才是真的只能默默悲哀了。

我不认为「提高保护自己的意识」这个要求对普通人而言是过高的。我不奢望所有人都有非常高的技术能力,但是作为一个生活在互联网时代的人,有些东西是需要学习的,是需要用来武装自己的,而不是赤裸裸地、一无所知地面对所有现代技术。诚然,让每个人突然都去学习这些东西是不可能的,可是我们完全可以一步一步地来。这其中最简单的是,我们可以尝试不再对那些试图保护我们的人和事物嗤之以鼻。比如某大厂的某知名 App 以 “安全” 为理由要求 Android 系统中插入其闭源的具有系统权限的守护进程才能使用,而一开始并不是所有手机厂商都默默接受了这一要求 —— 正是用户们不分青红皂白地向这些厂商们施压甚至骂战,才是这个守护进程如今几乎存在于所有国产和国行手机的真正推手。诚然,大部分用户没有足够的知识理解这些,但是我们至少可以做到 “兼听则明”,至少可以在听取多方观点以后思考其利害关系,至少可以做到不在一些人科普其背后原因的时候还恶语相向 —— 相信我,只要做到这一点,现在的隐私环境会好得多。其次,我们可以尝试走出以 “能用” “方便” 为第一的这种思维定式,需要关心到在 “能用” 以外的更多理由,而不是像某李姓人士说的那样 「中国人愿意用隐私换取方便」。我并没有要求任何人一下子学会所有技术知识,但是不懂得技术知识并不是大多数人忽略事物的隐藏的方面的理由 —— 我认为真正的理由正是上面提及过的 “懒”,而能够 “懒” 则是因为并不了解其后果 —— 而这完全可以通过教育和科普来改变。

隐私作为一个个人权利,其保护的过程必然不可能是自顶向下的。只有当人们有了上述这些意识以后,才会出现隐私保护的共识,在这以后,“他们” 才能有足够的动力制定并且 落实 保护隐私的法规和政策,才能够反过来通过共识限制企业和政府的行为。在这个隐私的领地渐渐被缩小的时候,企业、政府必然要承担他们的责任,但是杀死隐私的真正刽子手正是每个人自己,与你我一样的人们才是导致这样的结果的最大原因。拒绝学习、自欺欺人、掩耳盗铃,这些词语放在当今的普通互联网用户上并不为过。曾经,我们都有机会阻止这一切的发生,可是放在今天,很多事情已经晚了;要夺回属于我们的领地,需要付出的努力将不只是一倍两倍之多。

希望从今以后,隐私不会再只是茶余饭后一笑而过的谈资。

重新开始博客: 使用 StandardNotes

距离上一篇博文已经过去 10 个月了,而本博客连一篇文章都没有更新,可能已经开始给人一种几乎要荒废的感觉了。而事实是,确实是这样,这个博客已经在荒废的边缘。

这几乎一年间,因为自身情绪上的一些问题,我很少有想要写一些东西的心情,与以前过个几天就想提~~笔~~键盘的状况完全不同。而在那非常少的有“想写一些东西的心情”的时候,我每次都会因为害怕麻烦而打消念头。“麻烦”的缘由则很大程度上是我自己埋下的 —— 我在多年以前抛弃了 WordPressGhost 这样的博客平台,自制了一个简易的博客程序 Typeblog,而这个程序是没有实现编辑器 UI 的。它直接依托于配置文件、插件和文章源文件,而这些文件则需要存储在 Git Repository 中。因此,我以前的做法是使用单独的 Markdown 编辑器,比如 Typora,编辑后提交到 GitHub 并通过 WebHook 来实现自动同步到服务器。这一个过程确实没有几个步骤,在以前我也就很自然地做完了。然而,也许是不良情绪放大了很多事情的复杂性,这确实就是过去这段时间里我不想动博客的主要原因。

关于那些“情绪问题”,我近期可能写一些文章描述,而这篇文章的正题并不是那些问题。总之最近,所谓情绪问题似乎有所缓解,也稍微有些能写的心情,只是回想起来之前的写作体验确实差强人意。当时也没有一个可以接受的手机写作解决方案,因为 Android 端我并没有找到非常好用的、支持同步的 Markdown 编辑器,即使找到也存在如何 push 到 GitHub 的问题;而我却经常在路上、在睡前玩手机无聊的时候想要写一些东西。当然,我的朋友 drakeet 有一款在手机上写作的 纯纯写作 应用,写作体验已经非常棒,只是我需要的并不只是写作,我还需要

  • 多平台统一 (PC Linux + Mobile Android)
  • 支持自托管 (self-hosted) 的完整同步解决方案
  • 完整加密支持 (明文数据不离开客户端)
  • 插件支持 (这样我至少可以自行实现导出到自己的博客)

于是,这就到了该开始安利的时间了。

Standard Notes

我第一次接触到 Standard Notes 与博客完全无关。正如其名,它是我在寻找笔记软件的时候发现的。

从上大学开始,我就习惯于用笔记软件来写电子版笔记,顺便用于记录一些临时起意的想法之类。原因很显然是我的凄惨手写字体和手写速度,但无论如何,经过了一两年以后,使用笔记软件这也就成为了一个习惯,经常没事就打开看一看写一写。

那时候我使用的是 Evernote 的付费订阅 —— 认识的很多人都在用,所以我也自然地使用了它。但是,使用了这个产品以后才发现,它不要说端到端加密和 Markdown 支持了,就连基本的富文本编辑都存在这样那样的很多 BUG,其中不少完全是由于自作聪明。它的扩展性也几乎是零 —— 没有任何办法做出能用的编辑器插件。大一的时候数学课的笔记需要使用 LaTeX 记录公式,唯一可行的办法就是用某些使用 Evernote API 做的 Wrapper 来输入,然后让它们代为同步到 Evernote。这既不优雅也不安全,还要多带上一个专门用来记数学笔记的软件,已经失去了统一笔记平台的意义。

后来 Evernote 出现了隐私问题,尽管后来隐私条款的修改被撤回了,我还是觉得切换自己使用的笔记平台的时间到了。Evernote 官方也迟迟不肯实现插件支持和加密等特性,整个平台也已经很长一段时间没有变动,除了隐私条款之外。如此决定之后,我就开始四处寻找新的替代,希望能够用支持加密、自托管且有足够插件支持的软件来替代。当然,使用代码编辑器 + Markdown + Git 也是完全可行的方案,我可能希望的只是一个更适合做笔记的 UI;同时也不希望自己每次打开代码编辑器,默认记录的上次打开文件都是一堆与代码无关的东西。

就是在这个时候,我从某位朋友那里听说了这个 Standard Notes。那时候,它似乎还在非常早期的阶段,功能也非常简单 —— 默认只有一个纯文本编辑功能,它背后的协议 StandardFile 也就是一个普通的文件同步协议加上客户端加密。除了加密部分[1]以外,这个东西的一切都似乎是从简单和方便长期维护的角度考虑的,这其中就包括了它的插件化的结构。开发者宣称,为了“专注安全和持久性”,

We say 'no' to feature requests.

可是说着这句话的它却有很棒的可扩展性[2],大概是因为傲娇也是一种正面属性吧。利用插件 API 几乎可以自己做任何想要的功能,事实上,官方的收费计划中就有很多他们自己利用这个 API 制作的插件,包括了 Markdown 编辑器 (LaTeX 支持),TODO 编辑器,以及各种加密备份方案等等。

这个收费订阅,也是在不影响开源的前提下的收费 —— 有能力的人完全可以很方便地自行从源码构建所有插件并导入,也可以在不交钱的前提下自行开发插件。我最初就是通过这个方式在没有付费订阅的情况下使用了插件,之后加入了付费订阅。

在当时,它的移动端仍然缺少除了同步以外的很多基本功能,包括 Markdown 编辑器;也因为我那时还有当时自己认为方便的写作方案,所以完全没有考虑使用它作为主要的写作工具,只是作为一个方便的 self-hosted 笔记工具来使用。

Listed

接下来的某个时间,我发现了同样由 Standard Notes 团队提供的工具 Listed。它是一个模仿 Medium 的平台,只是比 Medium 要简单得多,本质上就是一个导出并展示部分 Standard Notes 笔记的地方。

虽然 Listed 本身并没有开源[3],但是其实现方式和大部分关键代码都已经在开发者文档中作为“示例代码”公开了。或者倒不如说,Standard Notes 的整个 Actions API 几乎都是为了这个用例而设计的,Listed 服务则只是最简单的用例而已。

这个服务让我眼前一亮,既然我的笔记软件已经有非常好的 Markdown 编辑体验,为什么不直接把它当作写作工具?况且本来平时的写作灵感最先进入的就是笔记,若是一篇文章没有彻底完成,则行文思路也是在笔记中,使用笔记软件直接写作的话反倒可能免去了不少来回切换的麻烦。

因此,我决定先用 Listed 尝试一下这么做的可行性与实际体验,这也就有了我的英文博客。这个英文博客是直接绑定在 Listed 服务上的,想更多的记录一些想要写成正式文章又想早些公开的想法,当然目前也只有比较完整的英文文章,主要还是之前作为体验写作的几篇。

而体验就是,Standard Notes 的客户端确实是一个不错的写作工具。

重新开始写博客

虽然有了英文博客,发布也方便得多,它仍然也只比这个中文的主博客多更新了一篇文章而已,原因也就是开头提到的那些问题。

最近也许会恢复更新博客这件事情也是最先在英文版上发布的。其实,这其中的第二重要的动机就是,最近我更新了一下 Standard Notes 的客户端们,发现现在的体验比上次有心情好好使用笔记的时候又好了许多。PC 端的不少 BUG 都修复了,终于有了可以管理附件的插件,而之前插件实现还在讨论阶段的移动端也已经支持了主题和编辑器插件。尽管移动端插件实现仍然是通过使用非原生代码(JavaScript),但是考虑到为了能完全和桌面端通用插件,减少复杂度,我觉得也没有很大问题;Standard Notes 的移动端 App 也可能是我见过的体验最好的非原生代码实现的 App 之一了,在现在的状态几乎与原生体验无异。而 Listed 支持绑定自定义域名,也是我这次更新以后发现的事情。

开头已经说过,我并不再想通过老的方式来写作博客了,而且也不想使用某些写作体验更不舒服的博客方案。之前已经发现 Standard Notes 作为写作工具的潜力,但是我也不希望直接把我的整个独立博客都替换成 Listed -- 那样就失去我专门造个博客的价值了。

我需要想办法将 Standard Notes 与我自己的博客程序,Typeblog,整合。这时候,我的计划是使用 Standard Notes 的 Actions API 模仿 Listed 展示一个用于发布博客的菜单。但我很快发现了这么做的问题: Actions 是通过请求远程接口实现的,无法在 Standard Notes 客户端中加密保存任何数据,这也就无法实现从客户端自定义配置 (e.g. 指定 Typeblog 的博客所在的上游 git repository 地址)。虽然即使做出来,这个东西大概也就我一个人使用,但是这样实在令我感觉非常不优雅 —— 毕竟,就连 Typeblog 这个只有我一个人使用的东西本身,也被我加上了很多配置选项甚至扩展。

于是,我尝试给 Standard Notes 写一个 editor-stack 扩展,从自定义远端的 Git repository 里面读取配置,允许直接将当前编辑的 Note 推送到这个 Git repository 并更新 Typeblog 的 config.json 以发布文章。但是很快,我发现要这么做的话至少需要一个将 Git 的功能暴露在 HTTP 上的程序[4],而我的博客的存储部分本身就是托管在具有 HTTP API 的 GitHub 上[5];而 Standard Notes 的本来就有一个名为 GitHub Push 的插件可以直接将笔记原文推送到 GitHub Repository。尝试以后,发现它可以选择推送的目标 repository、目录前缀和文件名后缀,而文件名则是与笔记标题相同。因此,以下 workflow 似乎就可以完全满足需要:

  1. 创建标题为新文章的URL链接域名后部分的笔记,比如这篇的 blogging-with-standardnotes
  2. 写作新文章内容
  3. 使用 GitHub Push 推送到博客 repo 的 posts/ 目录下,后缀名 md
  4. 创建名为 config.json 的笔记并保证其内容与原来 repo 中的同名配置文件相同
  5. 将已经 push 的新文章的相对路径加入这个配置文件
  6. 将其推送到远端 config.json,后缀当然是 json

如果自己编写插件的话,那么手动在笔记软件中同步配置文件的不优雅的几步完全可以自动执行,但是我再次决定这点麻烦并不算麻烦。也许等下一次我因为这种问题懒得更新博客的时候,我就会想起来去解决这个问题了吧。

仍然存在的问题

  • StandardNotes 手机 App 仍未支持 Actions API 和 editor-stack 插件,其中就包括了我使用的 GitHub Push, 也就是说我无法在手机上直接更新博客
  • GitHub Push 仅限于 GitHub API,将来我的博客存储也迁移到自建方案以后需要自己重写这个插件
  • StandardNotes 的部分编辑器插件在处理非英文输入法的时候存在奇怪的 BUG
  • 我还没有实现比较方便的直接给博客贴图的插件,目前仍然是手动上传到自己的 minio
  • 这套自制博客方案实在太垃圾,而且已经多年没有维护,大概是时候重新做一个了……

总结

本来这篇文章的计划是比较各种方案的优势和缺点,以及最后推荐 Standard Notes 并对它作出评测。写完之后回头看才发现,这已经变成了完全在记述“心路历程”的文章。也罢,我确实也一直不太擅长评测这样的事情。

总之,这就是我的新的博客 workflow,也希望我在这篇文章之后能告别咕咕咕……

脚注

  1. Standard Notes 背后的 Standard Files 协议本身经过了一次 Security Audit,但是其具体客户端实现并没有;这个协议也确实还有一些可以发现的问题;不过 Standard Files 至少本身是 self-hosted,协议也基本上都按照正常的 security practice 在做,不像某 T 开头的聊天工具
  2. 使用扩展会带来更高的安全风险,这是可以预料的,尤其是编辑器类的扩展,好在扩展有 iframe 隔离并且必须显式声明权限
  3. 或者我没有发现 Listed 开源在哪里
  4. 浏览器端的 JavaScript 难以操作原生 Git,而 Standard Notes 的插件并没有浏览器端和桌面端的硬性区分
  5. Typeblog 的程序使用 Git 管理文章源文件;博客还暂时没有迁移到自建的 Gitea,但是即使迁移之后本质上也是一样的,只是需要用 Gitea 的 API 重新做一个插件罢了

systemd-nspawn 踩坑记

已经有半年没更新博客了,一方面是这段时间确实情绪之类的方方面面不太稳定导致一直没心情更新,另一方面是觉得没啥好更新的,无非是一些琐碎,所以就一直拖着拖着,直到今天才发现,已经半年了。

而正好最近把自己的网络服务都迁移到了一台新的服务器,尝试了全新的部署方式(systemd-nspawn),正好踩中了一些坑,所以随便写写记录一下,也算是重新开始做起博客这件事情了吧。

What & Why

以前我使用的是一台在 online.net 捡来的特价独服,因为只有一个人使用,所以我直接在主机上开了很多个 KVM 虚拟机,使用(几乎是)一个服务一个虚拟机的方式来部署自己的服务。这在一个人使用的时候确实没有什么太大的问题,唯一的问题可能就是因为自己懒,而虚拟机的数量太多,所以经常忘记更新 / 维护那些虚拟机。

而这次则是捡特价弄了一台特别划算的 E5-2680v2 的独服(购买的时候下单的是 E5-2660v1,但是不知道是商家特别有钱还是那天机房小哥心情好,给弄了一台 E5-2680v2),几个人合用一台。因为是合用,所以大家各自开了一个 KVM 虚拟机,各自隔离。这时候,我就不能再使用虚拟机的方式来隔离自己的服务了,因为我本身已经被隔离在了一个虚拟机里,双重虚拟机可从来都不是什么好主意。

所以我转向了容器方案。其中,Docker 不太适合我的情况 —— 我并不是希望把所有服务都做成不可变的镜像然后到处部署,我的目的仅仅是简单的隔离(看起来整洁 / 给有些傲娇的应用提供最适合的环境)。因此,我转向了 systemd-nspawn,毕竟我是 Systemd 的~~卖底裤~~粉丝(雾),而且自带 SystemdArchLinux 在安装完成后就自带这个东西。

于是,我成功开始了踩坑之旅。(大量 dirty fix 预警)

非特权用户 (Private Users)

按照 ArchWiki 上的说明,使用各个发行版的 bootstrap 工具在 /var/lib/machines 下创建目录并部署系统是一个很快的工作。然而,当我部署成功并尝试启动容器的时候,我却根本看不到任何反应,无论 machinectl status 还是 systemctl status 都没法给出任何有用的信息。

再次查看 /var/lib/machines 下我部署的目录的时候,发现里面文件的权限全部被修改成了奇怪的 UID 和 GID 值。从 ArchWiki 上的描述来看,这似乎是启用了 Private Users 的正常现象。然而,死马当活马医,我尝试在 /etc/systemd/nspawn/myContainer.nspawn (myContainer 是我的容器的名字) 里面加入了

[Exec]
PrivateUsers=no

然后容器就神奇般地可以启动了。不过这样启动以后,尝试访问容器的时候,会发现里面的程序一定会报一大堆权限问题 —— 因为之前已经用 Private User 起过容器。所以,我的做法是,直接重新部署一遍……

然而这个问题并没有被彻底解决,我到现在也不能理解为什么使用 Private User 会导致无法启动,而且 systemd-nspawn 完全没有给我任何有用的错误信息。更奇怪的是,我直接用命令行的 systemd-nspawn 去启动容器是完全正常的,而使用 systemd-nspawn@.service 就必须关掉 Private Users 才能正常使用。鉴于我的使用场景并不需要多么严格的安全策略(另一方面,Linux 下的容器这个概念本身也不是用来做安全的),我暂时并没有去处理这个问题。所以,这算是一个 dirty fix 吧。

无法访问容器的 TTY

容器起来了,我遇到的第二个问题就是无法访问容器的 TTY。

尝试执行 machinectl login myContainer, 直接给我扔了一个 protocol error 出来。在 Google 上找了很久也没有找到任何一个人遇到类似的问题。最多只有进入了容器的登录界面却无法登录的问题,而遇到那种问题的人至少已经获得了容器的一个 Login Shell, 而我则是什么都没有……

是的,这个问题我也完全不知道怎么解决。当时我折腾了很久,然后怀疑是一个临时性的 bug —— 毕竟 ArchLinux 喜欢 break 东西。所以我决定作为一个临时的解决方案,先手动使用 systemd-nspawn 命令启动容器进去,配置好 openssh,然后用 machinectl 启动容器,并在外面直接使用 ssh 访问容器内部的 shell。是的,你没有看错,直接在 /var/lib/machines 使用 systemd-nspawn -b -D myContainer 命令启动容器是完全可以访问容器内的 shell 的,而从外面使用 machinectl login 或者 machinectl shell 就是不行的……

而当我下笔写这篇博客的时候,我尝试再次复现这个错误,却发现现在已经完全正常了,使用 machinectl login 可以获取到容器的正常的 Login Shell... 天知道把我折腾的要死要活的那个问题是怎么回事…… 而且从出现那个问题到现在我并没有更新服务器上的任何软件,也没有针对这个问题做任何处理…… 而当时我重启了不知道多少遍都完全没有作用。

假如你遇到这个问题,也许你坐和放宽一下,就好了。

容器内的内核模块问题 (OverlayFS / ip6tables / FUSE ...)

遇到这种问题其实觉得自己很智障,但是还是要花几句话说一次,容器内是没有办法加载内核模块的,而 Linux 启动的时候默认很多模块都没有加载。那些模块正常情况下会被使用它们的程序自动加载,但是在容器里这是不行的。所以如果你在使用程序的时候遇到这种问题,请记住在主机里加载它需要的模块后再试 (推荐加入 /etc/modules-load.d/ 自动载入)

Systemd-nspawn 内运行 Docker

这个需求看起来很无厘头,但是我觉得我的使用场景是有道理的。我有一个 Mastodon 节点,这个东西是主要使用 Ruby on Rails 编写但同时有很多其他依赖的东西。在以前的机器上,我是使用 docker-compose 通过容器的组合在一个虚拟机里直接部署上这一系列依赖。而现在我需要迁移到我的新独服上,我不能使用虚拟机,也不想自找麻烦手动部署,也不想让 Docker 产生的一大堆网络接口之类的东西挂在主机的 namespace 里。总而言之,因为这种 Ruby on Rails 程序是好多个大怪兽,虽然各自有笼子,但是因为数量比较多,分散放置还是感觉很凌乱,所以我想进一步把它们的笼子也都关进一个动物园里统一管理。

但是问题来了,systemd-nspawn 似乎并不支持在它内部再启动 Docker 容器。尝试使用 Docker 容器会直接带来 Operation not permitted 异常。从 https://github.com/opencontainers/runc/issues/1456 了解到,Docker 依赖了 cgroups 功能,并且需要比较高的权限,而在默认情况下,systemd-nspawn 是隔离了 cgroup 命名空间的,而且也没有给予不必要的权限。所以,作为测试,我在 /etc/systemd/nspawn/myContainer.nspawn 里加入了

[Exec]
Capability=all

[Files]
Bind=/sys/fs/cgroup

这在事实上把主机的 cgroup 命名空间共享给了容器里的系统,并给予了所有可以给予的 Capabilities。同时,还需要关闭 systemd-nspawncgroup 隔离功能,只需要 systemctl edit systemd-nspawn@myContainer

[Service]
Environment=SYSTEMD_NSPAWN_USE_CGNS=0

到了这一步,我期望 Docker 已经可以使用了,但是很不幸的是,并不能。这回出现的是一个莫名奇妙的 session key 无法创建的异常。这次这个异常我就完全没有看懂了……

还好,经过一番 Google,我了解到这其实是因为 Docker 在尝试使用 kernel keyring,而这个功能是不支持(ref: https://github.com/moby/moby/issues/10939)命名空间隔离的。所以,为了安全,systemd-nspawn 默认把与此相关的系统调用都过滤掉了,不允许内部的系统调用。因此,只需要开启这两个系统调用的权限(在 /etc/systemd/nspawn/myContainer.nspawn[Exec] 段中加入)

SystemCallFilter=add_key keyctl

然后重启 nspawn 容器即可使用 Docker

Docker 正常运行之后,我发现一个问题,那就是它在使用非常慢、非常不科学的 vfs 作为存储后端。根据文档,这个存储后端会对每个 layer 都创建一个拷贝。于是我想起来了遇到的上一个问题 —— 主机没有加载 OverlayFS 的内核模块,因此默认的 overlay2 存储后端加载失败了。尝试在主机上加载 overlay 模块,然后重新启动容器里的 Docker,发现 overlay2 存储后端果然已经在正确运行了。

以上文档我已经写入 ArchWiki 上的对应章节, 因为我发现我在整个网络上都找不到关于这件事情的文档,有的只是一段 Twitter 对话,而且他们其实还并没有解决这个问题……希望我并不是唯一一个有这种奇葩需求的人吧。

当然,这么做以后,这个 nspawn 容器就成为了名副其实的特权容器,拥有很多很多高权限操作的能力。考虑到我的本意仅仅是出于洁癖一般的理由,这个问题我觉得并不是非常大……总之,给大家一个参考。

容器内使用 FUSE

FUSE 是指用户态文件系统,比如 sshfs, ntfs-3g 等。想要直接在 systemd-nspawn 容器里使用它们是会直接失败的。当然,这个解决办法很简单,因为这仅仅是因为容器里没有 /dev/fuse

首先要确保主机上加载了 fuse 内核模块。然后,你需要在 /etc/systemd/nspawn/myContainer.nspawn 加入

[Exec]
Capability=CAP_MKNOD
DeviceAllow=/dev/fuse rwm

(注:如果你前面已经 Capability 设为 all 了,那就不用再单独设置一次 CAP_MKNOD 了)

然后在容器里执行

mknod /dev/fuse c 10 229

即可。

其他:网络配置

网络配置这算是一点附加说明,就是如果你想要给容器使用静态 IP,或者你想给容器使用 IPv6,你需要首先在 /etc/systemd/nspawn/myContainer.nspawn 里给容器增加一个网络接口

[Network]
VirtualEthernetExtra=name_on_host:name_in_container

然后分别在主机和容器里配置对应的网络接口即可。

当然,你可能也需要

[Network]
Private=true
VirtualEthernet=true

虽然这些应该是默认的。

结论

Systemd 坑很多,而且很玄学,但这并不影响我卖底裤(笑)。

GPD Pocket 上手 & ArchLinux

之前在 archlinux-cn-offtopic 群组里偶然看见 farseerfc 教授在晒图,是一台看起来非常非常小的电脑,但是却赫然写着 x86_64 并运行着 ArchLinux。我当时就起了兴趣,因为我一直苦于整天搬着一台 1.7kg 重的 XPS15,想要一台比较迷你而且便携的 x86 设备。我想要 x86 设备的理由是在我眼里只有 x86 才能算是完整的 PC 体验:有些 ARM 平台确实性能很好,可是连主线 Linux 都跑不了,又能算什么 PC 呢……

在 YouTube 上逛了一圈以后,感觉负面评价不是很多,加上双十一又有一定程度的打折活动,自己又真的非常想要,就在本周早些入手了一台。入手价格是 3000 人民币。

于是,这里是一个简单的评测和对我装 ArchLinux 过程中遇到的坑的记录。

(本文有补充编辑的内容,可能下面提及的部分问题已经被我解决,如果想看请直接翻到最后。)

配置

首先看一下它的配置。

  • CPU: Intel Atom x7-Z8750 (1.6GHz)
  • 存储: 128 GB _eMMC _(不是 SSD)
  • RAM: 8 GB DDRIII 1066MHz
  • 图形: Intel HD Graphics 405
  • WiFi / Bluetooth: BCM4356
  • 显示屏: 7吋 IPS 多点触控 (1920 x 1200)
  • 音频: Realtek ALC5645 双声道 (扬声器为单声道)
  • 电池: 7000 mAh
  • 接口: 1x USB Type-A, 1x 3.5mm 音频, 1x HDMI MINI, 1x USB Type-C (可充电, 使用 PD 协议)
  • 系统: 预装 Windows 10 Home, 可运行 Linux

买之前最担心的是 CPU 和 WiFi / 蓝牙 芯片。CPU 感觉性能可能不太足够,而 BCM4356 这个……看名字就知道,又是 BCM,进了 Linux 必然有坑 :( 斜眼看我的同样是 BCM 芯片的 XPS15……

但是担心终于没有战胜我想要剁手的心情。思考了几天之后我还是没忍住入手了。这也是我第一次发现某宝上还有 “拍下后联系卖家改优惠价” 一说 XD

上手

拿到手的第一感觉是这个东西真的很精致。7 英寸的身体看起来很娇小,但是拿在手里又感觉非常结实。重量 480g 可以算是一款比较轻便的设备了,装在口袋里也不显得比较沉,而且它 确实 可以装进口袋里,是名副其实的「Pocket Device」。

img_1

铝制的外壳看起来则非常 MacBook,正如大部分评测里所说的一样。我不想评价这是不是好事,我只能说这个外壳的手感算是我碰过的设备里最好的一个。而这块 IPS 屏幕则绝对是一个惊喜 —— 颜色非常正,没有坏点,也没有漏光问题。7吋的 1920x1200 屏幕看起来非常清晰(当然,这也导致了之后运行 Linux 的时候遇到的一些问题)。整个屏幕看起来的舒适程度要胜过我的 XPS15。当然,我手上的这台 XPS15 的屏幕有几个坏点,而且不属于 HiDPI 范围,似乎也没有什么可比较的……这里有一点要吐槽的是官方送的那一块贴膜的 正反面标反了,直接导致我把那块贴膜给贴废了……从其他几位用户那里得知这似乎是普遍情况,请各位想要购买的朋友注意了。

预装的是 Windows 10 Home。我不知道盒子上贴的序列号有什么用,因为一开机就已经是激活状态了 —— 也许是给重装使用的?我只开机了一次稍微测试了一下各种功能确认没有问题以后就把 Windows 10 格式化掉然后安装 ArchLinux 了。在讨论这个设备上安装 Linux 的过程之前,我想先讨论几个别的问题。

键盘

之所以把键盘单独拿出来说,是因为这个键盘是很多评测吐槽的对象。确实,因为只有 7 吋的大小,这个键盘的布局非常奇葩 —— 大写锁定缩的小小的被塞在 A 的左边,整个 A 行被往右平移了,退格在上删除在下,几个特殊符号被塞在了右下角。适应了全键盘以后,再使用这个键盘显得非常困难。

不过,把完整的键盘塞在这么小一个设备上也不是什么容易的事情。又要完整的键盘,又要键的大小足够手指敲击,相当的困难。GPD 家的前代作品 GPD Win 就有一个非常奇葩的键盘,我上上周使用过一会儿别人的 GPD Win,觉得那个超级迷你版的全键盘才更加的恐怖 —— 问题不在于布局,而在于那个东西上的键盘的键都只有不到一个指甲盖的大小……

在使用了两天之后,我觉得 GPD Pocket 的这块键盘还算可以接受。稍微适应以后双手打字并没有太大的问题,两个手也不会撞在一起,只是当用到一些键位特殊的键的时候需要反应一段时间。当然,我也不会建议谁在这个设备上输入大段的文字。还有一种操作方法是双手握持设备然后用拇指敲击键盘,但是这样的话使用指点杆稍微有点难受。说到指点杆,我觉得这个设备上使用指点杆简直是绝配了,有完整的鼠标体验而且节省了空间 —— 只可惜这个指点杆不支持中键滚动。

性能

性能是很多人关注的问题,而实话实说,这个设备的性能绝对不算好,也算是比较长的续航的代价之一吧。原装的 Windows 我没有详细测试过,但是我运行的 GNOME 时常有卡顿的现象存在。考虑到 GNOME 大量依赖 JavaScript,我猜测如果使用 KDE Plasma 的话可能会好很多。

不过我使用 Firefox Nightly 进行基本的网页浏览并没有遇到太大的问题,基本上都能够胜任。看 YouTube 1080P 也没有太大的压力,而 4K 则经常出现掉帧。在访问大量使用 JS 的网站的时候,例如淘宝,耗电量会有一定程度上的上升。这也在合情合理的范围之中。

我粗略尝试运行了一下 Visual Studio Code,发现基本的功能使用上是没问题的。虽然我不指望用这个进行什么高性能的开发,但是我估计应急写写代码也是完全可行的操作。在安装新字体更新 fontcache 的时候则会感受到明显的卡死现象,这时候 CPU 占用变成 100%,显然是性能不足了。好在这种操作也不会天天执行。

作为一个(伪)音乐爱好者,我也尝试了使用这个东西作为 MIDI 合成器,结果是几乎完美。只是扬声器比较烂,需要自己插耳机解决 :(

img_midi

也算是终于不用拖老远的线把它接到我的笔记本电脑上了(这个键盘附近已经放不下我的大 XPS 了)

对于性能这个话题,总而言之,它不是一个高性能设备,如果你是为了性能而来,那有更多的设备可供选择。但是它是绝对可以满足基本的使用需求的,甚至可以进行一点低性能要求的开发工作。游戏运行我暂时没有测试,根据其他的评测所言,进行一些微调以后,这个 Intel HD Graphics 405 是足够胜任简单的 3D 游戏的。

P.S. 我并没有进行跑分,但是昨天晚上运行了一下 openssl speed rsa2048openssl speed ecdsap256,结果分别是 219.7 sign/s + 7584.8 verify/s5965.6 sign/s + 2638.2 verify/s,供各位参考。

ArchLinux 安装

オニーチャン、ArchLinux をインストールしてください。

emmm 开玩笑的。不过说了这么多,是时候安装 ArchLinux 了。我们伟大的先驱者(雾)们已经在 ArchWiki 上给 GPDP 开了一个页面来描述可能遇到的问题和解决方案,链接在 这里。下面对于这些已经提及的问题可能就不再描述了。

首先是安装的方式。要从 USB 启动,你需要首先进入 BIOS 关闭 Fast boot。进入 BIOS 的方法是开机狂敲 Del。BIOS 内屏幕的方向是错误的,你需要把设备旋转过来才能操作。建议不要使用鼠标而是使用方向键来选择,电源键确认。关闭之后,插上 ArchLinux 引导介质,然后在开机的时候按 F7 (注意你需要按住 Fn 键以使用 F 系列按键),即可选择 USB 引导。需要注意的是它只能使用 UEFI 的引导介质。

引导进入 ArchLinux 安装环境之后默认的屏幕旋转也是错误的。要想解决这个问题,需要在引导进入安装环境之前的启动菜单(systemd-boot)界面上按 e 编辑内核命令行,在最后加入 fbcon=rotate:1。如此操作之后启动就是正确的屏幕方向了。进入之后的命令行的字实在太小,可以暂时使用 setfont sun12x22 来获得一个稍微大点的字体。

之后的操作和标准的 ArchLinux 安装过程一样,只是磁盘路径比较特殊,是 /dev/mmcblk0,因为这个小家伙使用的是 eMMC。不过,在安装盘里是没法正常使用 WiFi 的,你可以选择使用 USB 有线网络或者干脆直接用安卓手机来共享网络进行安装。安装之后参照 ArchWiki 上的 WiFi 部分,把两个文件 brcmfmac4356-pcie.{txt,bin} 放入 /lib/firmware/brcm/ 就可以正常使用无线网络了。

在设置声音的时候,似乎 ArchWiki 上提供的配置中的最后一行

set-sink-port alsa_output.platform-cht-bsw-rt5645.HiFi__hw_chtrt5645__sink [Out] Speaker

会导致 PulseAudio 直接启动不了。我直接删除了这一行,~~然后在桌面环境里选择默认输出,解决了这个问题。~~ 后来发现正确的配置应当是这样

set-card-profile alsa_card.platform-cht-bsw-rt5645 HiFi
set-default-sink alsa_output.platform-cht-bsw-rt5645.HiFi__hw_chtrt5645_0__sink

以上内容添加进 /etc/pulse/default.pa 即可

ArchLinux 默认安装的是主线内核。使用主线内核是可以正常启动 GPDP 的,大部分功能也是可用的,除了 亮度调节、蓝牙、电池充电状态 这些功能以外。另外,主线内核的音频还存在撕裂问题。要使用这些功能,你需要使用 linux-jwrdegoede —— 这是一个以前玩 Allwinner 的大佬做的内核,使用它的话几乎全部功能都正常(你需要学会如何给 ArchLinux 使用非默认内核,这个教程网上一大堆)。当然,蓝牙的话,需要手动载入一下 btusb 模块,编辑 /etc/modules-load.d/ 里面的内容让它自动载入即可。

我一般习惯在安装环境里把命令行和网络配好就重启进入系统继续安装。在这里需要注意的问题是,当你配置 bootloader 的时候,一定要记得给内核命令行加上 fbcon=rotate:1,否则重启以后你的屏幕就又不对了 :(

桌面环境

桌面环境安装和标准方式一样,我选择了 GNOME,所以 pacman -S gnome 即可。由于 ArchWiki 上没有包含关于 GNOME Wayland 的内容,我在这里稍微描述一下遇到的问题。

首先是屏幕旋转。你需要编辑 ~/.config/monitors.xml 用以下配置把它转过来(不知道为什么我的 GNOME 没有自动生成这个文件的默认内容,以下来自于 farseerfc 提供的配置)

<monitors version="2">
  <configuration>
    <logicalmonitor>
      <x>0</x>
      <y>0</y>
      <scale>2</scale>
      <primary>yes</primary>
      <transform>
        <rotation>right</rotation>
        <flipped>no</flipped>
      </transform>
      <monitor>
        <monitorspec>
          <connector>DSI-1</connector>
          <vendor>unknown</vendor>
          <product>unknown</product>
          <serial>unknown</serial>
        </monitorspec>
        <mode>
          <width>1200</width>
          <height>1920</height>
          <rate>60.384620666503906</rate>
        </mode>
      </monitor>
    </logicalmonitor>
  </configuration>
</monitors>

保存后重新进入 GNOME 即可。这会同时把显示内容缩放为两倍大小(一倍大小在旋转正确以后实在看不见任何内容……)但是两倍有点大了,要想使用分数缩放需要执行

gsettings set org.gnome.mutter experimental-features "['scale-monitor-framebuffer']"

然后继续编辑 ~/.config/monitors.xml<scale> 那边的数值改成 1.5 之类的就可以了。不过这样设置以后部分界面会显得有点模糊,大概得等 GNOME 和软件开发者们修复 HiDPI 的问题了。有的软件也有自己的缩放设置,可能需要单独调节。另外推荐在 GNOME Tweak Tool 里把字体缩放也设置成 1.1 或者更大,这样看起来舒服一些。

如果你想要把登录界面也转过来,你需要在 /var/lib/gdm/.config/monitors.xml 中键入同样的内容。~~不过我暂时没有找到让登录界面也使用分数缩放的方法,所以我直接让它两倍缩放了。~~ 请看本文最后 EDIT 部分中让登录界面(GDM)也能分数缩放的方法。

P.S. 我从奇怪的地方看见了下面这句东西

gsettings set org.gnome.desktop.interface scaling-factor 2

似乎也是设置缩放的,但好像并不管用。

总结

似乎要说的暂时就这么多,Linux 上的更多问题在 ArchWiki 上都有详细的说明。一篇博客也差不多水完了,下面是总结

优点:

  • 便携
  • x86 完整 PC 体验
  • 屏幕养眼
  • 做工精致
  • 接口足够多

缺点:

  • 性能较低
  • 键盘布局很谜
  • WiFi 信号似乎有时候不太好
  • 自带扬声器不行,不过耳机输出还好
  • BIOS 对屏幕默认旋转设置不对导致自己装系统有点麻烦
  • 比较贵

这并不是针对每一个人的设备。如果你需要的是一个非常便携而且可爱的 x86 设备,而且你又是一个折腾党,喜欢玩各种各样的东西,那么它可能正是你的菜。否则,可能安卓平板会是更好的选择。当然,在购买之前请详细阅读各种 Wiki 和其他人的各种评测再做决定。

剩下的图

emm 还有几张和 XPS 的合照

img_xps1

img_xps2

EDIT1: 蓝牙耳机

之前蓝牙正常了一直没测试过,今天突然想起来测试一下蓝牙耳机是否可用,结果当然是 —— 默认配置下并不能工作。连接以后识别不出 A2DP,导致直接没办法输出音频……

我首先按照各种奇奇怪怪的论坛上的说明在 /etc/pulse/system.pa 里加入了

load-module module-bluez5-device
load-module module-bluez5-discover

然后按照 ArchWiki 上的说明,我禁用了 gdm 开启的 PulseAudio (创建一个 /var/lib/gdm/.config/systemd/user/pulseaudio.socket,把它软链接到 /dev/null 即可),然后使用

bluetoothctl
pair YOUR_HEADPHONE_MAC_ADDRESS
connect YOUR_HEADPHONE_MAC_ADDRESS

手动连接。之后,使用 pacmd ls 查看你的蓝牙耳机的设备编号(假设它是 INDEX),然后执行

pacmd set-card-profile INDEX a2dp_sink

耳机就可用了。不过在这之后,每次连接的时候似乎都要重新连接几次并在 GNOME 的音频设置里手动选择耳机为音频设备以后才能使用…… 至少是能用啦。

EDIT2: GDM 分数缩放

sudo machinectl gdm@
gsettings set org.gnome.mutter experimental-features "['scale-monitor-framebuffer']"
exit

以上命令会 登录进 gdm 用户并开启分数缩放功能。注意此处用 sudo -u gdm 或者 su gdm 是无效的。执行以后将 /var/lib/gdm/.config/monitors.xml 里面的 <scale> 设置为 1.5 然后重启即可。(如果没有这个文件,参考我上面的步骤,把自己用户目录下面的那个复制过去就好。)

EDIT2: 右键模拟鼠标滚动

GPD Pocket 的指点杆不自带滚动功能,于是有人提出让它的右键变成一个模拟滚轮,也就是「按住右键并移动鼠标」这一动作来代替滚轮。

这是在 Xorg 下面可以通过配置 libinput 实现的功能,然而在 Wayland 下得看 compositor 的脸色 —— 很不幸,GNOME 并没有提供这个功能,于是很长一段时间我认为这是不可能的,然后忍受着没有滚轮功能的指点杆。

后来实在有点受不了,甚至试图修改内核来实现这个功能 —— 当然,因为不了解内核驱动,我瞎改了半天并没有起作用。后来晚上做梦的时候梦见了 LD_PRELOAD,突然惊醒,觉得我完全可以利用 LD_PRELOAD 来 hook 进 libinput 的函数,强行开启这个功能。

花了不到一个小时研究了一下 libinput 和使用 LD_PRELOAD 的方法,写出了这么一个简单的小程序 https://github.com/PeterCxy/scroll-emulation,按照使用说明编译后加入 LD_PRELOAD 即可。

基本原理是劫持桌面环境对 libinput_device_get_name 的调用,在返回之前使用这个指令序列

libinput_device_config_middle_emulation_set_enabled(device, LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED);
libinput_device_config_scroll_set_method(device, LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN);
libinput_device_config_scroll_set_button(device, 273);

对所有可以开启的设备开启 libinput 的中键滚动模拟功能。开启以后,即可使用「按住鼠标右键并移动鼠标」来模拟滚轮。

EDIT2: 键位调整

GPD Pocket 上的键盘键位很谜,尤其是退格和 Delete 放在一起,以及那个超级小的大写锁定键。我本来也想通过 hook libinput 来解决,然而 archlinux-cn-offtopic 里的大佬给了我一个更好的解决方法,那就是使用 udev 自带的 hwdb 来修改 Keymap.

简单研究了一下这玩意怎么用,写出了下面这个配置:

evdev:input:b0003v258Ap0111*
 KEYBOARD_KEY_7004c=backspace
 KEYBOARD_KEY_7002a=delete
 KEYBOARD_KEY_70039=a

以上配置的作用是 1) 交换退格和删除键 2) 将大写锁定键去掉,改成另一个A键(大写锁定可以用按住 SHIFT 来代替)。将这个配置文件放在 /etc/udev/hwdb.d/90-gpdp.hwdb 即可。

你们要的 Inline ASM 疑难解答

背景

我校 (西交利物浦) 的 信息与计算科学 专业的大二的课程中,有一门 (CSE101,计算机系统) 涉及到一个使用 MSVC Inline ASM 完成的作业。由于大部分人此前没有接触过 ASM 甚至没有接触过编程,而课程本身因为 一些原因 难以理解 (I_cant_hear_you.jpg),所以最近几天我一直收到关于这个作业的各种各样的问题,其中很多都是重复的类似的问题。当然,这也怪我本人在群里 “假装自己很会 ASM”(手写 x86 ASM 真的是第一次)。不管怎么说,因为大家都会遇到类似的问题,所以我想着我要不还是干脆写篇博客一起回答一下,以减轻我的多线程工作负担(x)。

当然,写这篇文章的直接原因是应某同学的要求 —— 她发给我一篇来自其他同学的教程,让我修改一下其中可能存在的没有讲清楚的点。于是我决定还是自己从头写一份好了……

作业要求

使用 MSVC Inline ASM 编写一个满足如下要求的程序

  1. 让用户输入一个在 [2, 5] 之间的整数 n
  2. 循环 n 次,每次问用户索取一个新的正整数(如果不是,退出循环,跳到 3)
  3. 当循环退出的时候,打印退出消息并打印总循环次数
  4. 从小到大排序用户输入的数字并输出
  5. 输出用户输入的数字的总和

示例输出

Select total number of positive integers (between 2-5): 5

Enter positive integer 1: 67

Enter positive integer 2: 13

Enter positive integer 3: 21

Enter positive integer 4: 39

Enter positive integer 5: -9

Program terminates and has looped 4 times.

Your integers from lowest to highest is 13, 21, 39, 67

The total amount is 140

技能和前置要求

部分来自 来自其他同学的教程

  • Microsoft Visual Studio, 非 Windows 用户请使用虚拟机或者双系统
  • Visual Studio 的基本用法和调试器使用方法
  • C / C++ 的 基础的基础 (变量定义,数组定义,printfscanf
  • 基本的 x86 汇编指令和寄存器知识
  • MSVC Inline ASM 中与 C 部分交互的知识
  • 指针 和 值 的区别 (leamov 作用有啥不同,方括号有啥作用)
  • 什么是 esp
  • 理解冒泡排序算法

声明

这篇文章仅作参考使用,本文中不会附带完整的源代码片段,更不会直接给出作业的答案。本人无法保证该文的绝对正确性,以下全部是本人在自己完成这个作业的时候得出的经验性结论。我此前没有学习过 x86 ASM, 也无法对该话题作出最精确的描述。

作为未来的软件工程师,应该善用搜索引擎,知识库和问答网站(此处指 Google / Bing / DuckDuckGo,问答网站包括 StackOverflow,对于这个作业的知识库包括 Wikipedia / MSVC 官方文档不包括 百度搜索 / 百度文库 / 百度百科 / 百度知道)。我也从来没有专门学习过 x86 ASM,以下所有知识都是我在国庆假期前做这个作业的时候通过这些渠道获得。没有人能一直给出答案或者专门为了一个问题编写教程和问答,请一定要明白这一点。

(偷偷附加一点,我其实对这个课程使用 MSVC Inline ASM 挺有微词,不过主要是因为我的日常系统是 Linux,而 C 的不同实现之间的内联汇编并不兼容。况且如此使用 ASM 还依赖了一部分 C 的知识 —— 倒不如给学生提供一个能进行输入输出的库,然后让学生直接使用一个专门的汇编器来做这个作业。)

输入 / 输出

大部分人遇到的第一个问题就是如何输出字符串,如何读入来自用户的输入。

这个问题实际上就是使用 C 标准库中的两个函数

// 输出
printf(format, arg1, arg2, arg3....);
// 输入
scanf(format, &some_variable);

我在这里不想细讲这两个函数是怎么回事,而是提供在本作业中会用到的特殊情况。

1. 输出一个字符串,例如 Hello World

在 C 中是这样写的

printf("Hello World");

转换成内联汇编

const char msg1[] = "Hello World";

__asm {
  ........
  lea    eax, msg1
  push   eax
  call   printf
  add    esp, 4
}

由于这时候 printf 只消耗了一个参数(要打印的那个字符串),所以后面对栈顶指针 esp 加了 4 (4 = 1 * 4)

2. 输出一个整数变量,例如循环变量 i

在 C 中是这样写的

int i = 1; // 假设你有个变量 i,不一定是定值 1
printf("%d", i);

这里的 %d 是一个占位符,表示稍后输出的时候把这里替换成一个整数。

转换成内联汇编

int i = 1; // 同上
const char format1[] = "%d";

__asm {
  ........
  mov   eax, i
  push  eax
  lea   eax, format1
  push  eax
  call  printf
  add   esp, 8
}

当然,这里的 %d 还可以附加上其他内容,比如 Your number is: %d, 输出的时候这个 %d 所在的位置还是会被换成你传入的那个整数变量。

注意,这里由于 printf 消耗了两个参数,所以对 esp 增加了 8 (8 = 2 * 4)

同时,给 printf 传值的时候,如果传入整数变量,请使用 mov,而字符串请使用 lea,这是传值和传指针的区别,我不想在这里细讲(涉及到 C 的字符串和内存结构的问题)

3. 请用户输入一个整数,并存储到一个变量,比如 j

使用 C

int j; // 来个空变量
scanf("%d", &j);

应该不难看出来,这里的第一个参数是和 printf 一样的格式。%d 表示此处将要读入一个整数输入。

转换成内联汇编

int j;
const char format2[] = "%d";

__asm {
  ........
  lea   eax, j
  push  eax
  lea   eax, format
  push  eax
  call  scanf
  add   esp, 8
}

此处 j 同样是个整数变量,但是却使用了 lea 而不是 mov,原因是 scanf 需要一个内存指针以便把读取的内容写入变量(注意 C 版本中的那个 &j,即取指针)。

这里由于 scanf 消耗了两个参数,所以同样给 esp 增加 8。

当然,你在要求用户输入之前,可能最好先输出一个消息,提示用户需要输入什么内容。

_P.S.: 如果要换行,可以在字符串中使用 \n _

总而言之,在内联汇编中调用 C 的函数,主要的点就是把参数或者参数的指针倒序推进栈里,然后 call 函数,然后根据参数数量调整 esp 的值。

循环

循环其实在课件里讲的挺清楚的,就是 loop 这个指令,它会把 ecx 减 1,如果此时 ecx 是零了,那就跳出循环,否则跳回循环开始的地方继续循环。

只有一个可能踩到的坑,那就是 ecx 这个值实际上是 没有保证 的 —— 当你调用其他函数,比如说按照上面讲到的方法进行输入输出的时候,由于被调用的函数里也可能存在循环,当输入输出完成的时候,这个 ecx 的值就可能已经被改变了,由此会导致你的 loop 判断失效。

解决这个问题的办法非常简单,只需要在循环的开始保存 ecx 的值,并在调用 loop 指令之前将其恢复。有两种办法:

1. 利用栈来保存

我们可以在循环的开始把 ecx 推进栈,再在循环的结尾把它弹出

__asm {
  ........
  L1:
  push  ecx
  ........
  pop   ecx
  loop  L1
}

有的人问过我,这个栈不是用来传参数的吗?它确实 可以 用来给 printf 这样的函数传参数,但是,如果你给函数传了两个参数,当函数被调用结束以后,我们自行恢复了栈顶指针 esp 的值(你应该已经注意到上面对 esp 进行的加操作),由此保证了我们自己推进去的 ecx 的值永远在栈顶 —— 所以最后只需要一次 pop 就可以拿到 ecx 的值。这也是为什么 call 完成以后,对 esp 的加操作的数值 (4, 8, 12...) 只依赖于被调用的函数消耗了多少参数(参数数量 * 4)而不是总共进行了多少次 push

2. 利用变量来保存

其实是一回事,只不过这次我们让 C 自己帮我们在栈上分配空间来保存而已。

int i;

__asm {
  ........
  L1:
  mov   i, ecx
  ........
  mov   ecx, i
  loop  L1
}

我想我就不必再多加解释了。

不使用 loop

你也可以完全不使用 loop,自己使用 jmp 系列指令并自己维护循环计数器来完成循环操作。这也是我使用的方法,但在这里不想细说 —— 基本的逻辑和 loop 是完全一样的。

计算循环次数

这是部分人碰到的一个问题。他们直接把循环中的 ecx 当成「第几次循环」来输出,结果发现这个值是倒着走的 —— 请仔细看 loop 指令的说明,你就明白为什么了。

要想输出正向递增的循环次数,你有两种途径,一是自行维护一个往上走的循环变量(或者干脆像我一样不使用 loop

int k = 0;

__asm {
  ........
  L1:
  push  ecx
  mov   eax, k
  add   eax, 1
  mov   k, eax
  ........
  pop   ecx
  loop  L1
}

这个 k 就是当前循环的次数了。另一个方法是你只要对 ecx 进行一点简单的运算就可以

int total;

__asm {
  ........
  mov   total, ecx // 把总循环次数保存下来
  L1:
  push  ecx
  ........
  // 要用到当前循环次数的地方
  pop   ecx // 还原 ecx
  push  ecx // 赶紧把它保存回去,这时候 ecx 仍然是读出来的值
  mov   eax, total
  sub   eax, ecx
  add   eax, 1 // total - ecx + 1
  // 此时 eax 的值就是当前循环次数了
  ........
  pop   ecx
  loop  L1
}

数组读写

  1. 循环 n 次,每次问用户索取一个新的正整数(如果不是,退出循环,跳到 3)

中,由于你需要循环 n 次向用户获取正整数,所以你需要一个数组来保存输入的数值。如果你不知道数组是什么,请自行搜索。

这个作业中我们只需要定义一个长度为 5 的数组就好了,因为 n 最大只能是 5 (在 [2, 5] 之外的输入是非法的,你需要进行判断并输出错误信息)。像这样在 C 中定义一个长度为 5 的数组

int arr[5];

这个 arr 变量就是一个长度为 5 的数组。C 中的整型数组实际上是内存中的连续区域,以 4 字节划分,而变量名 arr 对应的就是它的第一个成员的地址。在这个地址上,+4 即可获取第二个成员,+8 即可获取第三个成员,以此类推。

比如我们修改上面 输入 / 输出 部分的例子,把用户的输入存进 arr 的第 (i + 1) 个成员里(i 大概会是你的循环的当前次数,请看上面关于循环部分的描述)

int i; // 这个变量你可以用来保存当前循环次数,我不作强制规定,这里只是个定义
int arr[5];
const char format2[] = "%d";

__asm {
  ........
  lea   eax, arr[i * 4]
  push  eax
  lea   eax, format2
  push  eax
  call  scanf
  add   esp, 8
}

以上汇编对应 C 的代码

int i; // 只是个定义
int arr[5]; // 也只是个定义
scanf("%d", &arr[i]); // C 版本里不需要乘 4

比对一下这个汇编代码和原本读取输入的汇编代码的区别,你就知道如果要输出数组成员该怎么做了。实际上就是把原来的变量名替换成了 arr[i * 4],之所以乘 4,就是因为我上面提及的内存结构。请再次注意,这里我说的是使用第 (i + 1) 个成员,而不是第 i 个。也就是说,这个计数 i 是要从零开始的,i = 0 代表第一个。

当然,你也可以学习来自其他同学的教程中的做法

int num_array[5];
_asm{
  .......
  lea   ebx, num_array 
  Loop1:
  ........
  //只写存值的部分
  mov   edi, temp // temp 是一个临时变量,内容应该是本次接受的用户输入的整数
  mov   [ebx], edi 
  add   ebx, 4
  ........
}

这里的做法是,在循环开始之前,先把 num_array 的起始成员的地址放进 ebx 中,每次循环的结尾对 ebx 加 4,这就意味着,在第 k 次循环的时候,ebx 中永远是数组 num_array 的第 k 个成员的 指针。于是,mov [ebx], something 就意味着把 something 的值复制到 ebx 这个寄存器中的 那个指针 所指向的内存区域,也就是 num_array 的第 k 个成员。([ebx] 这个中括号的作用就在这里。如果没有中括号,就是直接赋值给 ebx 寄存器,而不是 它所含有的指针 所指向的内存区域)

当然,这么做的话,你会需要像保存 ecx 一样,保存 ebx 的值,以免它被莫名其妙修改 —— 具体怎么做,我在上面的 循环 部分已经描述过了。

排序

这里大部分人都打算使用冒泡排序 —— 反正我也不高兴用汇编写什么快排……

冒泡排序的思路和具体算法,我就不想在此赘述了,作为最简单的排序算法之一,到处都能查询到。不过要注意的是,由于课件上给出的示例代码是 MASM,你并不能直接把它用在内联汇编中 —— 这个算法过于简单,也没有这个必要。排序本身并没有什么难的,之所以很多人卡在这里,其实大部分都是因为不会使用循环和数组操作。

我们来看看冒泡排序中涉及到的操作

  1. 两层循环: 和一层循环在操作上并没有什么区别,仍然是那几个注意事项。只要你在内外层都做好 ecx 的保护工作,两层循环就完全不会互相影响
  2. 读取数组成员: 上面已经讲过
  3. 比较大小: 就是 cmp 系列指令和 jmp 系列指令组合
  4. 交换数组成员: 和读取数组成员是一样的,上面也已经讲过,无非几次 movarr[i * 4] 来替代变量名)

就这么多了。最后的输出,你还需要另一个循环,把排序好的数组成员一个个输出,这也是之前已经提及的。

“按任意键退出”

大部分人都遇到了这么一个问题:写好的程序,戳运行,然后黑框框一闪而过,输出了什么都看不到。

所以你需要做一个按任意键退出的功能,其实就是等待用户输入一个任意的字符。实现也很简单,在汇编部分的最后

__asm {
  ........
  call getchar
  call getchar
}

是的,大部分情况下你需要 call 两次 getchar,原因是前面 scanf 会在标准输入中留下一个换行符,然后被 getchar 读取,导致第一个 getchar 立即返回从而失效。

你可以在调用两次 getchar 之前先打印一条消息,比如 Press any key to exit... 来获得更好的 用户体验

后记

关于这个问题,我能说的也就这么多。也感谢一直在问我这些问题的同学(们),否则我可能到现在还在看错作业要求(……)

以上大部分问题都可以在搜索引擎/问答网站上找到答案,这也是我完成这个作业的方法。当然我觉得这个课程最好在讲一定的 C 语言知识以后再开设,因为其实整个过程就是在充当 C 的人脑编译器,把 C 源代码(的一部分)人肉编译成 ASM。

作为工程师,解决问题的能力肯定是必要条件。

从 root 手机说起

昨天在知乎上回答了一个问题

如何评价魅族Flyme系统即将关闭root功能?

问题的内容大致是,魅族计划以「安全」为理由去除系统中的 root 功能,请问大家如何看待。我在回答中主要提及了「用户对自己购买的手机的控制权」这一问题。当然,在知乎上发表这类言论,必然的引来了评论区的一场“大战”。由于知乎实在不是一个保存和展示文字内容的好地方,所以我选择把更详细的内容放在这篇文章里阐述。

写在前面

在讨论评论中一些人的问题之前,我想我有必要重新摆明我在这种事情上的立场。

第一,我认为自己购买的设备应当是完全属于自己,也就是说「购买」这个行为是针对所有权而不应该是使用权。当我购买手机的时候,我不是在购买手机这个设备的使用权,而是完整的所有权。我应该能够在这个设备上运行任何我希望且能够运行的软件而不受厂商的控制。当然,在今天,这个要求其实是很高的 —— 比如越狱就是一个合法性一直饱受争议的行为 [1],尽管从我的角度来看,「越狱违法」是很不可理喻的。同时,也有很多人提及,购买手机时立下的用户协议中,可能就是仅仅授予了使用权,而其他一切都与最终用户无关 —— 这正是我想要反对的东西。令人欣慰的是,EFF [2] 等组织也一直在为了实现这个目标而努力。

第二,「Root」这一行为的目的是获取自己设备的更高控制权,这是正当而且合理的行为,而不是需要隐藏的不齿行为。「Root」不是为了破解别人的软件而存在,不是为了侵犯开发者的利益而存在,是用户为了保护自己的利益和获取自己应有的控制权的正当手段。通过「Root」可以更好地定制自己的 Android 设备,包括但不限于内核参数调节、使用主题或自定义字体、更方便地使用非官方应用市场、控制某些软件滥用权限或滥用后台等等。

第三,厂商应该通过为用户提供产品和服务获取利益,而不是通过限制用户的自由获取利益。在这里我想强调的是,用户花钱买的是产品和服务,而不是花钱去买“爹”。厂商的目标是切合用户需求而不是控制用户的需求,是听取用户想要什么而不是告诉用户应该要什么。所有举措,包括所谓安全举措,前提都必须是用户拥有知情权和选择权,他应当有选择拒绝的权利,哪怕是「拒绝安全」,这也都是自己的选择。厂商没有必要为用户的选择承担责任,但必须要确保用户有能力进行自己的选择。对于不能满足自己要求的厂商,用户可以选择不买,不买的同时更加有权利向他人说明自己拒绝该厂商的理由以供他人参考。

第四,本人关于该话题的所有观点,其重点都在于「能不能」,而不是「会不会」。也就是说,如果有的人,他追求的是简单方便,他没有那个心情去折腾什么自定义,也没有兴趣去获取更高的控制权,那么我完全不反对,因为这是他的选择。但是,同样也会有人不喜欢什么简单方便,什么傻瓜式设计,他需要运行他自己的软件,因此需要更高的权限,他也应该有这个能力去作出这样的选择 —— 毕竟,这是智能设备,不是大哥大板砖。「我不会去 root」「他也不会去 root」「我认识的人都不 root」都不能成为直接关闭这个功能的理由 —— 因为它不是一个简单的功能,它是用户控制设备的一个途径。

以上就是我对于这个问题的立场,以下所有讨论都是在上面的立场的基础上进行的。

苹果设备

发布了这些言论以后,最早收到的评论就是「那么苹果呢?」,更有人说我对苹果就“跪舔”而对 Android 就要求很多。老实说,我完全没有在知乎上发表过我对苹果设备的看法,因此我完全不知道他们是从哪里看出来我对苹果设备的态度,不过既然这样说了,我就可以顺便阐述一下自己对苹果设备(注:此处相应地只讨论 iOS 设备,即 iPhone, iPad, iPod Touch)的观点。

首先我完全不否认苹果在乔布斯时代的设计水平,乔布斯也是我比较佩服的人物之一,我自己和我的家人也使用过或者正在使用部分苹果产品。但是,这并不代表我认同他们的产品理念,也并不代表我认同他们在软件方面的策略。我不喜欢用「X宗罪」的形式评价一个产品,但是在这一点上我实在觉得自己有点数不清苹果究竟有多少「没法用」的地方。因此,我想概述 Richard M. Stallman 在自己的博客 [3] 上写明的 一部分 我认同的「不使用苹果设备的理由」

  1. 苹果的应用商店有严格的审查制度,并且在正常的苹果设备上使用第三方商店是不可能的。苹果对于自己商店里的应有有比法律法规更严格的审查,直接导致有些应用是根本无法在苹果的设备上使用的。同时,这个审查制度也具有双重标准,比如 iOS 上至今没有一款非 Safari 内核的浏览器(因为可执行代码的问题),可是一些厂商的“热更新”技术却能在应用商店上架。

  2. 苹果曾(且仍然在)尝试阻止用户修理自己的设备,并且试图阻止使其合法的法案通过 [4]。iPhone 7 甚至有通过让设备“变砖”来阻止拆开修理的技术手段 [5]。他们不把用户当成客户,而是当成自己控制的对象。他们想要成为你的「老大哥」。

这两点正是最重要的两点。总而言之,从这样的商家购买设备,你花钱买来的不是产品,也不是服务,而是一个远在千里之外的“爹”。也许有的人会喜欢这样,不需要自己动手或者动脑子,但是对不起,至少我很反感,而且我也不会再去购买苹果设备 —— 即使他们的设计再吸引人。

为什么不买个开发板

于是有人就问我,「你为什么不买个开发板,自己DIY一个手机出来用」。

这个问题非常有意思。答案是很简单的:因为我没有选择。

最重要的问题就是「基带」—— 手机的“移动通讯”功能的底层部件。现存的基带基本都是黑盒 [6],同时具有相当高的系统权限(它们本身甚至也运行了一套完整的操作系统),完全可以用于大规模监视。更严重的是,由于太过大量的通讯标准以及专利壁垒的存在,开源实现几乎是不可能的事情,这直接导致了目前市面上几乎不存在「能用的」开源实现的基带 —— 即使有,也基本上只实现了一万年前的协议和功能…… 我在 Indiegogo 上找到了一个通过逆向工程制造开源基带硬件的众筹项目 [7],但那是 2015 年的事情,众筹目标并没有达成,而且似乎很久没有后续更新了……

而没有了通讯功能,还做什么“手机”……做出来了也只是个板砖罢了。

什么?你说你想自己发明一套完全开放的标准?不好意思,你还得通过各大机构的认证,还得想办法让全世界,至少很多国家的通讯企业都接受这个标准 —— 可是这个行业是非常非常喜欢自己造轮子、自己搞自己的一套的。因为标准拿在手上就是利润,可以大肆向同行甚至竞争对手收取费用。在这些企业面前,开放的力量几乎是不存在的。

所以,对于这个问题,结论就是:不存在的。

大部分人不需要 ROOT

这是我也认同的一个观点。大部分人确实没有那个时间或者能力去折腾自己买来的手机,只要够用就好了。

可是我们这里讨论的问题是「能不能」,而不是「要不要」,「会不会」。对于一个手机小白,我也绝对不会跟他说「你买了手机先 ROOT 一下」,因为这本身是很危险的事情,他们自己都不一定知道自己在做些什么。自己不知道后果的选择不能称之为用户的选择。同理,我在这里再怎么说我 ROOT,也绝对不会给任何欺骗用户授权 ROOT 以获得更高权限的流氓软件带来任何的合理性。

换句话说,我所支持的不是 ROOT 这个行为本身,而是在关注我是否被允许、是否可以对自己购买的设备作出修改,是否能够在它上面运行官方之外的软件。你做不做这件事情不重要,那是你自己选择的事情,问题在于是否可以这么做。「是否可以」意味着身为一个用户的自由和被尊重,而自己放弃是完全合理而且没有任何问题的事情。

从另外一个角度,“大部分人”的论述真的能成立吗?比如,大部分人都不关心也搞不懂窃听之类的东西,搞不懂什么叫中间人攻击,搞不懂 HTTPS 和 HTTP 有什么区别,也搞不懂为什么使用无加密的公共 WiFi 非常危险,于是操作系统、浏览器里面实现的安全功能就能说是多余的吗?他们不关心自己的隐私和安全,就是作为软件开发者 / 厂商不关心用户的隐私和安全的理由吗?用户不关心自己是否拥有自己该有的权利和自由,就是厂商漠视这些的理由吗?不是的。作为开发者,作为厂商,当然需要(在一定程度上,在用户知情且有选择权的前提下)保护用户的权益(比如说,为了安全默认关闭 ROOT,同时允许部分用户 ROOT,但是给 ROOT 操作设置一定的门槛,让懂得后果的人才能执行),否则谈何“服务”呢。虽然作为用户,完全信任商业公司是一件比较危险的事情,但这也不能成为商业公司作恶的理由。

总而言之,关于这个问题,我想说的是

  1. 用户做不做 ROOT 之类的事情和我讨论的「能不能」是无关的
  2. 我尊重每个人自己选择
  3. 「大部分人」这种论述方式存在一些问题

即使 ROOT 了,也仍然不能控制设备

是的,这正是现在的移动平台存在的问题。除了上面我提及的基带问题之外,现在 Android 设备上都存在大量的私有驱动,通过 HAL 绕过了 Linux 内核的协议限制。同时,很多设备的 bootloader 都是不开源的,这直接导致启动过程的第一环就已经不受控制。再加上 TrustZone 等私有固件,你想要完全控制你手上的那个设备,恐怕还有很远的路要走 —— 而且这里我还仅仅列举了软件部分。到了硬件层面,那就是个更加无解的复杂问题。

所以我们能做的,只能是

  1. 争取到可能获取到的最多的权限
  2. 拒绝那些不信任用户、不给用户合理的权限的厂商
  3. 让人们知道这些不合理的东西的存在

至于更多的,就像我上面提及的关于自己做手机的问题一样,会变成非常非常困难的事情。虽然还是期待有人能够这么做,但是凭我自己的能力已经不可能了。

其他

上面已经讨论了之前评论区里被提及的几个很重要的问题。不过,评论区里还有一些我觉得非常可笑的问题,想要拿出来放在这里。

同理建议空客波音特斯拉开放系统硬件最高权限

以及

你在天朝跟政府和公司领导要root权限了嘛

首先,这完全就是不同的概念。政府和你的关系是一种契约,公司和你的关系是一种劳务关系,而你乘坐空客波音的时候仅仅是临时使用他们的服务而已。你本来就不可能拥有你的政府和你的公司,谈何索取权限?而关于波音的飞机,第一如果你只是临时乘坐,你当然不可能索取权限;第二如果你真的购买了一架飞机,你还要考虑到高权限可能给他人带来的人身损害(当然,如果你愿意把所有的锅都放到自己头上,为所有一切可能出现的问题埋单的话,你大可以这么做)。可是手机只是一个贴身的设备而已,即使真的发生了爆炸,基本上也只能损伤你自己而已。

好吧,也许你买的只是一堆没用的金属塑料玻璃材料,我们买的都是体验。

不好意思,我想我比你更注重体验。

你要那么多权限为的是什么啊?不折腾,手机就差到没法使用吗?

不好意思,我想我比你更懂手机。

手机丢了以后,root的就等着个人信息满天飞,指不定还有更有意思的发生

不好意思,你的个人信息很可能早就被你的可爱的手机厂商们、应用厂商们卖掉了 [8] —— 而你的手机还在你的手上。另外,厂商的责任永远不是告诉我应该做什么,而是告诉我我的操作的后果,让我知道后果并且在我能够接受后果的前提下进行操作。况且,ROOT 之后真的说不定更安全(笑)。一个注重隐私的人,更应该对开放的硬件和软件有更高的需求。

在美国,公民拥枪是权利。但我感觉像中国这样,把公民的这个权利剥夺了更好。当然,如果某天政府的人对我实施非法侵害,我又希望能拥枪。但一般情况下,可能我成为那59个死者和那数百伤者中的一员的可能性要大得多

我一点也看不懂这个类比是怎么回事。ROOT 手机,和禁止持枪,有任何可比性吗?持枪不需要任何多余操作就可以危及他人的人身安全,可是 ROOT 一下手机会吗?你就算把手机的温度保护之类的措施强行去掉,也不能当成炸弹使用。再者,菜刀也能杀人,菜刀比一个 ROOT 之后的手机杀伤力要强得多,可是菜刀被禁止了吗?(菜刀确实是实名购买的,但是你的手机要入网,也需要实名)因此这根本就是一个很站不住脚的偷换概念……

补充

还有一个问题(不属于上面提及的可笑的问题)

反对。买手机并使用,是遵守用户协议的合同行为。买的不是手机本身,而是合同里面规定的内容。

是的,正是这样。但是我并不是在讨论是否应该遵守用户协议,而是这些严重限制用户的协议是否合理,或者说,用户在不知不觉间订立的这些协议到底给用户带来了什么,又让用户损失了些什么。如果一定要说是协议的话,那么我的结论是这样的协议不能去订立,也就是说我,从我的观点得出,我会拒绝购买这样的手机。

参考资料

  1. US government says it's now okay to jailbreak your tablet and smart TV
  2. Jailbreaking Is Not A Crime—And EFF Is Fighting To Keep It That Way
  3. Reasons not to use Apple
  4. Apple Is Fighting A Secret War To Keep You From Repairing Your Phone
  5. The iPhone 7 Has Arbitrary Software Locks That Prevent Repair
  6. The second operating system hiding in every mobile phone
  7. Free Software Cellular Baseband
  8. Privacy Change: Apple Knows Where Your Phone Is And Is Telling People

在西浦的一年

刚刚期末考完,想到 2017 年高考也已经结束,突然意识到自己已经在西交利物浦呆了一个学年。去年的这个时候,我也是刚刚高考结束,还在想着自己那个成绩可以报什么大学,还在思考如何处理我高中所谓的“初恋”(不,是暗恋),想着要不要送生日礼物什么的。转眼已经这个时候,到了下一届的小朋友们要开始考虑这些问题的时候了。看看我的博客,也已经一整个学期都没有更新了。于是,我想以此再水一篇博客,就当回顾一下这一年来在这个学校的体验,(也许)能给这一届同学们一些参考。


为什么来这里

本来,去年这时候,我希望报的大学是隔壁的苏州大学。在高中的时候,曾经有同学跟我提起过要不要参加西交利物浦的自招,我当时根本没有把它放在考虑的列表里 —— 因为很多人报名的目的实际上是看中两年后可以出国到利物浦以及它的双文凭,而我当时并没有这些想法,再加上对学费望而生畏,所以我完全没有上这个大学的想法。

但是后来高考出成绩,我一下子就慌了。去年江苏的数学特别简单然而我砸了,这就意味着我的分数一下子比预期低了十多分(江苏的分数密集性我想不用多解释……)。这样的分数上苏大是不可能的。而由于我行动不便,希望找一个本地的大学,当时我父母建议我直接放弃一本然后去二本中比较好的苏州科技大学 —— 这在我心理上是过不去的。这就是我后来抱着试试看的态度在一本批次里填写了西交利物浦的原因 —— 至少它是个一本,虽然学费很贵。很幸运地,我恰好比录取线高出了一分,进入了这个学校。

现在想起来,其实来这里是个不错的选择。虽然有不少值得吐槽的地方,但是这里有一个好,那就是无障碍设施很完善(毕竟学费 88k 一年呢)。之前随合唱团去了一趟苏大,发现这样一个 211 学校的新校区里的无障碍设施也依然是非常令人担忧。一旦课程不在一楼,我就只能彻底抓瞎。还有食堂等地方,也是非常困难的 —— 苏科大也有同样的问题。在西交利物浦,至少这一点我是非常满意的,大部分地方都有办法无障碍地到达。

所以这些大概就是我来这里的理由。


英语(EAP)

来之前我父母曾经担心过我的英语水平是不是足够,因为学校反复说,英语教学,英语教学,英语教学……但实际上我发现,大概正常经过高考的考生,英语水平都是足够的 —— 大一一开始并不会上来就全英文教学,还有很长很长时间的 EAP (English for Academic Purposes) 课程,足够慢慢练习听力练习口语了。

但是英语在某些程度上决定在这里能不能学到点什么东西。为什么这么说呢?因为 EAP 这一系列课程有一个分层机制,就是入学初有英语水平测试,然后将学生分为 High-Level, C, B, A 四个层级。就我目前的观察来看,虽然实际上四个层级的区别并不大,但是在非 High-Level 班级里,你更可能遇到坑爹老师和坑爹队友 —— 有很多 EAP 老师甚至就只是来中国混日子的,并不是真的想要教书。当然,High-Level 班级并没有高到哪里去,High-Level 班级中也存在很多这样的现象,比如说我这学期的 EAP 课程就有一个比较坑的老师,甚至在对我们的计入总成绩的作业给分的时候都不认真读,不认真给分,很多人除了分数一样以外连老师给的反馈也一模一样,而其他老师则都是很认真地给每个人写很有针对性的反馈。(在此还是想为那些在这些人的影响下还在认真工作的 EAP 老师们点个赞……)

总体上来说,EAP 系列课程的水分还是非常大的,这并不是高或者低分层的问题……而且我对 EAP 课程的评级标准表示严重怀疑。他们声称是按照 CEFR 标准来的,但我自己的感觉是所有标准明显偏低(各位可以参考其他地方的 CEFR 测试),有放水的嫌疑。我只能说,在这个学校,EAP 课程很重要,但是为了更多地提高自己,还是要尽量进 High-Level (入学测试 >70 或者第一学期 EAP 成绩 >70),而且所学到的并非英语本身,而是学术写作的基本知识。至于英语水平本身的提高,靠学校的课程肯定是远远不够的,毕竟其标准本身的水分在那里。我也只能说,在 High-Level 里面,你更可能遇到上进的同学和负责的老师。另外,西浦的国际学生都是自动进入 High-Level, 也就意味着更多的差异性 —— 至少我是享受多样性的,我并不喜欢一整个班级清一色全都是中国人,否则我还到这个学校来做什么呢?

稍微扯远一点话题,我真的想对很多人说,你们并不是英语不好,你们只是不想读,不想看,不想听英语的内容而已。我在某些技术群里也经常看到这样的人,明明官网 Wiki 上写的很清楚,却偏偏要在群里一遍又一遍地问,还说得很可怜,“大佬们帮帮我这个萌新吧”。问之,则曰,“Wiki 是英文的,我看不懂啊”。但中国的英语教育并没有这么不堪 —— 至少,阅读,在高中/初中阶段的英语教育中,是非常重视的。我不相信任何好好上过高中/初中的人,在认真阅读的情况下,会看不懂这些文档,况且我们并不是在阅读文学著作,这些实用性的文本一般不会故意设置什么阅读障碍。即使真的不懂,借助在线英语词典,也完全足够了。不要自己以为自己不能完成,而白白放弃了学习的机会。


“政治课”

实际上这里并没有传统中国大学意义上的「政治课」。我这里所说的是带引号的,它就是指大一学生的公共课程「中国文化」(CCT)系列。我没有上过其他大学的马哲、思修,所以具体的对比我是做不到的。我只能就这个课程本身来说,它比我想象中的政治课要可以接受得多,至少上半学期的中西文化比较,下半学期的中国现代化,我所遇到的老师,在描述一些历史问题的时候,有一个比较中立的立场,并且确实是在引导大家自己思考而不是轻易相信一些被政治立场左右的观点。当然,我也有朋友告诉我说,有一些老师的立场则有明显的倾向性,就是「中国的都不好,西方的都好」然后 blahblahblah 批判一番,在此我无法考证,只能以我自己的经历为参考。遇到一个好的老师,也是一件非常重要的事情,虽然自己是无法掌控的。

当然,你大可以质疑这类课程作为通识课程存在的必要性。就我来说,如果存在这么一个要求,大学必须开设类似「政治课」的课程的话,我将会倾向于这种模式。


学风

另一个经常被吐槽的就是我校的学风。这点我是同意的。这一年里,我见过不少人,他们 CCT 课每次都只在最后几分钟到场(因为每节课都有随堂测验),然后随便抄抄完事;也有不少人,他们直到计算机课作业要交的前一天晚上,还在四处询问 “LaTeX这玩意咋用啊” “能不能帮我做一下”;也有不少人数学从头旷课到尾,最后混个及格分(不排除有些大佬认为老师讲的实在没有价值,自行学完了整个课程,这样的大佬还是挺多的;我校的数学课程也确实要求比较低;当然,这就不属于“混个及格分”的范畴了);还有人在上了一年的 EAP 课以后还在说 “凭什么我自己的观点写在文章里还要 citation”。比较宽松的环境(每天最多四节课,6.9开始的暑假和其他大量的假期)给了很多人全天摸鱼的机会 —— 包括我自己,也觉得自己来了这个学校以后少了很多干劲。

我不知道其他的中国大学是不是这么一个样子,但是这些人的行为至少和开学典礼上校长所说的景象大相径庭。好在这一年里我遇到了不少优秀的人,比如一位愿意在我的~~安利~~推荐下入 Linux 坑的同学;几位搞人工智能项目的学长;一个同样是大一但是写出了一个游戏以期赶上绿光计划末班车的同学(~~更感动的是他的游戏竟然兼容 Linux~~);一位一样喜欢二次元但认真学习并不像我一样整天死宅的小姐姐;还有艺术社团的各位辛苦工作的小哥哥小姐姐们。或许,在大学生中,这样的人才是少数?我无法回答这个问题。

或者可以这么说,这里更多地是一个培养商人的地方,而不是一个培养科学家和工程师的地方。很多人热衷于如何以最小的时间投入通过课程,而不是考虑自己要从课程里学到什么东西。私以为这不是正确的学习的逻辑。

也许,中国的大学生,在刚刚结束极端高压的高中课程之后,并不能直接适应大学的环境?不过我个人还是比较感激这样的环境,因为至少我们没有严重神经质的网络限制策略,没有严重作秀的强行参加xxxx活动,没有为了领导调研而故意拔高的出勤率(笑)。

不过我的 GitHub 绿格子,怕是彻底没救了哦。


上面可能吐槽得比较多,希望各位见谅,也不要对号入座。正是因为在这里呆了一年,对这个地方比较喜爱,所以才会看到这么多觉得无法接受的地方。如果只是写一些优势,恐怕就变成了学校官网的腔调,也就失去了意义。我自己非常享受这样一种中国国内的国际化的氛围 —— 一种少见的「自由」的氛围。

非常担心这么一篇文章发出来以后我会被喷 —— 然后自己看了一下自己博客可怜的访问量,也就释然了。各位若有不同意见,就把这篇文章权当笑话吧。请记住以上所有内容并不特指任何一个人。

说一些题外话。距离去年的高考已经一年了,而去年的这时候,就像我在开头提及的一样,正在为高中的一些很 naïve 的事情而苦恼。才不过一年,当时所谓心中很重要的人,就已经说再见了。毕竟最后我们仍然不是一个世界的人,仍然没有机会走到一起。有时候有点想嘲笑过去的自己。

所谓重要的事情,重要的人,大抵如此吧,时间会消灭一切的 —— 啊,也算是在西浦一年的体验之一吧。

差不多了,希望各位学弟学妹们也能开启自己新的生活。

以上。

自由的消逝

我第一次听说「驴得水」这部电影,是在 中西文化比较 课上。当时那一节课,是关于中西方人对性和爱情的态度的,于是在课前放了「驴得水」的宣传曲「我要你」。当时我就被这首歌曲打动了,大概是旋律非常优美,而我又正好处在这样一个渴望爱情的年龄。后来老师也向我们推荐了这一部电影。然而,在这之后我只是找到了这首歌的两个版本和吉他伴奏谱,当成一首好听的情歌循环并练习了好久。

直到昨天,我终于想起来应当把这部电影看一遍。在这之前,我所听说的对这部电影的定性都是「喜剧」。可是,在进度条走到一半之后,我却再也笑不出来了。

这绝对不是一部关于性观念和爱情的电影,更绝对不是一部喜剧。这是一部彻头彻尾的人性的悲剧。


张一曼

所有的事情,都要从这个人物说起。

在写这篇博客之前,我看过许多关于这部电影的影评。其中,关于张一曼的评价,有两个极端:一个是视其为女权的「英雄」,认为她具有一种超前的性观念; 另一种是视其为「渣女」,「荡妇」,认为这样的生活态度绝对不能作为女权的代表。

从这部电影的背景来看,它设定的年代是上个世纪,新中国成立之前。这是一个比较久远的年代。而张一曼对于性的态度简直有当代女权主义的影子 —— 我有处置我身体的权利; 性是生理需要,而爱是心理需要,两者应当分离。她为了脱离「被人管」的境地,为了自由地生活而来到了偏僻的乡村。

你看我像是那种能跟你过一辈子的人吗?

她对裴魁山的一句话就道出了她的不羁。她不愿意因为爱而被拘束,不愿意因为家庭而被拘束。因而,她可以「睡服」一个人,但不会因此而被这个人拥有 —— 没有人拥有她。她的「放荡」,看的是自己是否愿意,而不是别人的心情。这与以出卖自己身体为生的人截然不同。·她所具有的是一种追求自由的信仰。如此看来,她不失为一个性观念超前的女性,不失为一位失败的英雄。

而对她持反面观点的人所看到的是这样一件事情,一件足以毁掉她的形象的事情 —— 为了利用铜匠而和他发生关系。这一次,与其他不同,她有明显的目的,那就是利用铜匠骗过特派员; 这一次,带有明显的「交易」特性。同时,铜匠有自己的家庭,而这次的关系显然打破了这个家庭本身的平静 —— 铜匠与妻子之间因为这次事情爆发了严重的矛盾。这也是铜匠本人所不能理解、不能接受和不愿看到的。毕竟,铜匠和铜匠的妻子并没有一曼的这种性观念。

所以,既然有了这样一次事情,她还能被称为女权的先驱,还能被认为具有超前的性观念吗?还能说,她是一个追求自由的女性吗?

作为本片中唯一死亡的人物,我觉得不管怎么说,电影本身是要借她来表达一些东西的。而我对于这个人物,至少是喜欢的,并且非常同情她,惋惜她遭遇了自己所不该遭遇的不幸。我呢,不认为应当把这个人物看作「英雄」或者「流氓」中的任何一种 —— 张一曼她本身是一个矛盾的、复杂的形象。她骨子里确实有超脱世俗、追求自由的性格,这无论如何是无法否认的,从「救火」一事中也可以看出来 —— 一种「傻」、「善良」和「天真」。然而,这与她周围的人格格不入,与时代格格不入,所以她才来到了偏僻的乡村来追寻自己想要的自由。可是她并没有找到她想要的自由 —— 人性的阴暗即使在这样一个地方也存在着。她自己也无法逃脱一些人性的弱点,为了自己的「事业」不得不与阴暗的利益集团同流合污 —— 也为了自己所爱的学生们 —— 不得不做出一些违背信念的事情。正是这样的矛盾给这个角色加上了更深重的悲剧色彩。

况且,从她对铜匠的那个灿烂的微笑看来,又怎么能说,铜匠在她眼里就真的只是个牲口呢?又怎么能说,她就真的没有对这个天真可爱的铜匠动过情呢?

她也许不想利用铜匠,但是,他们都是被利用的对象。直到最后,都是这样。为了一场闹剧,没有下限地牺牲着两个无辜的小人物。


铜匠

铜匠也是一个很有意思的角色。一方面,他就是个淳朴的村民,他落后、封闭、愚昧,对新的文化毫无感知; 但另一方面,他却能够接受新的东西,具有相当的学习能力(或许只是因为对张一曼动情?),向往知识(「我也要去美国!」)。当张一曼迫于压力不得不中伤他之后,他像一个小孩子一样对张一曼发着脾气。经由旁观者们的放大,这样的脾气终于对张一曼造成了毁灭性的打击。

铜匠希望脱离自己的生活 —— 他自己也表示过对自己的生活已经受够了; 从他和妻子的关系中,我猜测他与妻子的婚姻应该也不是自由恋爱的结果,而是包办买卖式的婚姻。他并未对自己的妻子动过情,因而,对于使他动情的张一曼,他呵护有加; 因为使他动情的张一曼侮辱了他,他的情绪犹如火山爆发。在这个方面,他就像一个初恋的男孩一样,什么也不懂,什么也不会。当然,他受到利益诱惑,一样会不择手段,一样会搬出封建的那一套观点 —— 否则,他怎么会接受毫无感情基础孙佳作为自己的「新妻子」,仅仅是为了去美国留学呢?所谓「底层劳动人民」的矛盾本质,在这个人物身上得到了集中的体现。

当然,铜匠这个形象,比起其他人来说,还是简单很多的。但他,应当也是一位主角。他的悲剧,则是寻求改变自己的生活,误打误撞撞上了腐败的利益集团,最后落得个什么也没有的下场,重又回到自己百般无奈的日常。

比起张一曼来,他是幸运得多了。至少他不会因为追求什么自由而自己烦恼自己。所谓「庸人方自扰」吧。


孙佳 / 周铁男

与前面两位不一样的是,这两位,借用特派员的话说,有着典型的「知识分子脾气」。他们所追求的不是什么自由,不是什么高大上的东西,而是自己心里的那一份「正义」。他们为了自己所认准的道理,为了自己所爱的人和物,可以放弃一切,甚至置亲生父亲于不顾。

这一对,可爱得像中学时代情窦初开的情侣们。那种含苞待放的情愫,那种想说而不敢说出口的心情,那种故作矜持的姿态……让人少女心爆棚。我一度觉得,他们是这部沉重的电影中的一股清流。

然而电影的结尾给了我重重的一记打脸。这部电影的每一个角色最后面临的都是一个悲剧,没有任何一个人有着圆满的结局。

周铁男尝试顶撞特派员,被用枪指脑袋; 子弹打偏,逃过一劫的周铁男彻底放弃了自己的那一套所谓的理念,开始不再以「正义」而是以「武力」为标准,甚至看着张一曼被强奸,因对方有枪而见死不救,最后甚至「劝说」自己所喜欢的孙佳去和铜匠演一场结婚的闹剧,口口声声说着「没办法」。而孙佳,空有一腔热情,却从未下定决心付诸事实,屈从于看似「没办法」的现实。可以说,在那一枪之后,这所谓知识分子骨子里的奴性暴露得淋漓尽致。

用时髦一点的话来说,这就是对「键盘侠」的完美的演绎 —— 其中可能也包括我自己。那些道貌岸然却软弱无能的人们啊!


至于影片里的其他人,我不想再一一赘述,无非都是利益集团中的一分子或者被利益集团利用的对象。

从影片一开始几位老师无拘无束的生活,到最后张一曼自杀、孙佳离开,我似乎感觉到这部电影所描绘的是这种自由的消逝。校长屈从于利益集团,裴魁山因无法占有而心生厌恨,周铁男和孙佳放弃了自己的主张,没有一个人获得了张一曼所追求的自由,口口声声说着爱她的人们纷纷离她而去,与她背道而驰。「我要你」所讲述的,恐怕是张一曼对真正的自由的人和自由的爱的追求,而显然,剧中没有人符合这个标准。

张一曼在追求自由,而大家则离自由越来越远。自由是什么?自由就是拒绝成为自己所讨厌的样子。影片里没有人做到了这一点。人性的弱点使他们不自由,同时也使屏幕面前的我们不自由。而影片在喜剧的外衣下,用黑色幽默的手法,很讽刺地体现了这一点。这些人的嘴脸,无法避免地,会在你的亲人、你的爱人、你的朋友、你的上司,甚至整个社会的身上找到影子。

过去的让它过去,只会越来越糟

而如果我们这样下去,会不会越来越糟呢?

一声枪响,张一曼绝望地结束了自己的生命,同时带走了她一生的追求。从此以后,如同「驴得水」这样的闹剧,可能会一次又一次地上演,无论是在电影中,还是在现实中。

人性使我们追求无拘无束的自由,同时,人性又让我们放弃自己信仰的东西。成也人性,败也人性。自由是宣扬人性解放,同时又不能避免地必须战胜人性中的那些威胁着自由的阴暗的成分。我们都身负这样的双重性,我们都是这样的矛盾角色。

所谓自由,恐怕只会随着岁月的流转而渐渐消逝吧,正如整个电影的剧情一样。

我要 你在我身旁

我要 看着你梳妆

这夜色太紧张 时间太漫长 我的姑娘

我在他乡 望着月亮

这样理想的人啊,你又在何方?

致12岁

12岁,一个稍微有那么一点遥远的年龄,一段仿佛又刚刚过去仍有余味的时光。

在知乎看到了两个问题

假设现在的你看到 12 岁的你,你想对他说什么?

假设 12 岁的你看到现在的你,ta最震惊的事会是什么?

实际上是同一个问题。

看了几位答主的回答,感慨万千。看似调侃的文字后面都隐藏着各位对那时生活的回忆和各种各样一言难尽的遗憾。确实,在一段时间以后,回首过去的那些事情,会有一番不一样的体验。即便是黑历史,也仿佛变得可爱和值得怀念了。

我12岁的那一年则是在六年之前。相比一些20-30岁的答主来说,那段时光离我还不算太远。然而,短短的六年之间,我似乎已经变了。我可能已经不会认识那时的自己,而那时的自己恐怕也不太会承认现在这样一个我了。

不太喜欢知乎的编辑体验和排版。所以,我把我想写给12岁的我的那些话,作为一篇博客,放在这里。当然,我也不太喜欢调侃这些事情,所以可能稍显沉重(?),希望不要介意。


1

上课打 Minecraft 吗?

你之后会经常在上一些无聊的课之前邀请周边的几个朋友「不务正业」。

一是那时候在初中,有些课程,的确就是反反复复反反复复,为了某些跟不上的同学而一遍遍重复,一次次听确实令人厌烦; 二是在管理严格的地方做这种突破规则的事情,不免有些刺激感。

可是这些反复,往往就是为了我那几个朋友。

恕我直言,你实在有一些自我中心。看起来似乎是几个人一起「同生死共患难」,可是他们和你不一样,你即使不听几堂课也能考上隔壁高中,他们是徘徊在职高边缘的那种学生。

你会安慰自己,说你已经寻求了他们的意见,得到了同意,他们是自愿的。

这一点我无法反驳,可是他们,和你一样,未必真的知道后果。

Joy Neop 说过

我们手上都沾满鲜血

即使我们做的是我们从来都以为正确的事情。在不知不觉之间,你可能毁掉了一个人的前程。

那几位「朋友」,初中毕业以后,我就再也没有见过了,也没有联系过。我所知道的只是一位去了某职高,其他的只能说下落不明,基本都没有能上高中。

是吧。难以想象。很悲伤的故事。

你们根本就不是什么朋友。他们是被你利用来取乐的工具罢了。

如此对你说的我,却也对自己的观点打上了一个问号。或许我会再见到他们,然后将一切疑问解决,将自己的一万个「对不起」说出; 若是不再见到,或许此生也没有这个机会了。

很不好意思一开篇就说这么冲的话。尽管我也不知道。也许再过个六年我就明白了,吧。


2

啊,那就换个话题吧,谈谈技术。

实际上我现在做的事情跟你并没有什么区别……如果我没记错,你不久之后买了一个 Android 手机,开始折腾 ROM,开始尝试编译,尝试修改源代码。而在不久之前,你刚刚抛弃了易语言这个大坑。

而初三毕业以后,你会把一整个暑假花在这些上面。再过不久,你会因为高中的学业而对此感到十分疲倦,然后转向 App 开发,再后来因为实在太忙而暂停……

这些,怎么说呢,还是要坚持吧。只是我可能更愿意你没有忽略一些其他的事情。这些我稍后再谈。

不久之前你可能也刚刚开始做主机商,出售博客用的虚拟主机之类。这些是你接触 Linux 的开始。不过显然,我已经放弃了做主机商这件事情。就现在的我来说,我觉得我是对勾心斗角的市场环境非常反感的。如果我没记错的话,放弃这件事情,就在对你来说的不久之后了。但是这些不可否认地是宝贵的经历。

然而这个领域发展太快,现在的有些事情,即使早那么多年知道了也不会起作用,还是慢慢来吧。

其实倒反而希望自己当年多读了一点书; 可是学到这些东西,也未必就是坏事。只是经常觉得自己文笔太差罢了……

~~(小声) 不过你倒是真的可以尝试用CPU挖几个比特币出来留着~~


3

不久之后你可能会遭遇一系列不太好的事情。你会因此躺一整个暑假,会这样度过半个初三。

不过好像这点事情在我经历过的所有不太好的事情中,也不算什么。

往往让我不安的只是害怕这些不好的事情发生 —— 这种「害怕」的情绪本身会使我不舒服。

现在的我其实也遇到了和你之后会遇到的事情一样的情况。所以那个时候的各种感觉仿佛又重新浮现在脑海中。简直一模一样。

而你也会对将要到来的「人生第一场重大考试」感到迷茫。

无论如何,我也只能说,相信这一切都会过去。

Everything will be fine.

把它当作一个信条吧。

或许,不幸会让自己在其他方面变得更加幸运呢 —— 缘分啊,考运啊什么的。哈哈。

对了,你考不上清北的,不如踏踏实实做最好的自己吧。—— 好像打击自己也不是个好主意?

一切都需要自己慢慢去走过,这才是人生的快乐所在。


4

请务必保持自己雪亮的双眼,不要和某些人同流合污。

很不幸的是,你会这样,我改变不了。

初中开始没过多久,班上的人就会一起排挤某一个女同学,像这样

这个东西被XX碰过了,真恶心,不能用了

这种感觉。

你呢,也会为了保持和其他朋友的关系,加入这个队伍之中。即便当时的班主任一再劝说,也丝毫不会起作用。

可是能做几年的同学是多么难得的事情啊。可是你知不知道这样会对一个少女的心灵造成多大的创伤 —— 仅仅是因为她没那么漂亮?

我认为这种现象的原因还是在于从众。自始至终,没有任何一个人说过,如果你不排挤某某,就不和你做朋友 —— 绝对,绝对,绝对,没有任何一个人。自始至终,大家加入这样一个队伍,仅仅是出于害怕自己和她一样被孤立。

害怕

真是一件可怕的事情。

无形中的暴力,无形中的欺凌,就这样产生了。

我真希望你能打破这样一个怪圈。虽然我明明知道,你不能,我深深地理解自己懦弱的本性,因而也无法要求太多……

可是,能不能稍微努力一下呢。

我告诉现在的自己,也告诉你。

请珍惜自己身边的人,善待他们,因为他们都会善待你,因此他们会善待你。

共勉。

还是那句话,我们手上都沾满鲜血。


5

还有一件事不知道当讲不当讲。

三年之后,也就是上高一的时候,你会遇到你第一个喜欢的女孩子。

我知道,你这时候还持对这种言情小说一般的事情表示「噫」的态度,还完全不会想到自己有一天会喜欢上一个人,也对早恋这种事情非常忌讳,害怕老师和家长知道。

我想说的是,请不要欺骗自己。

你遇到的老师,都会是非常好的人。即使班上有那么多明显的情侣,他们也从来没有暴力拆散过一对; 而因此他们也不会做出什么出格的事情,所谓「互相信任」。

所以还有什么好害怕的呢?请不要欺骗自己。

即便最后不能永远在一起,这也一定会是一段宝贵的经历。想想,假如在高三的压力之下,有一个人能和你牵着手去吃个饭,去稍微休息一会儿,那么一天都会变得开心吧。

况且,没有经历过这种情感,今后又怎么会知道如何应对呢?

请一定要抓住这次机会。可以剧透一下,她和你所上的大学,距离不会超过500米。还有什么好害怕的吗?

当然,我没有抓住。好在机会并不是只有一次。

请我也务必要抓住这第二次机会。


说了这么多,其实自己也清楚,这些并不是给过去的自己,而是给未来的自己看的。

再多的文字也改变不了那些既成事实,只能告诉自己,以后要成为一个更好的人。

成为一个温柔的人,成为一个善良的人。

写着写着感觉自己像在写小学生的作文……

不知道再过六年,我看见这些,会有什么感想呢。抑或我根本就不会再看见这些。

差不多了,也就这么多话了,再说就变成扯家常了。如果还会想到其他重要的事情,就今后再补充吧。

以上。

再多一点点时间

我呢,今天去看了「你的名字。」。是的,和你们想象的一样,我和喜欢的人一起去看了「你的名字。」。

于是产生了一种非常讽刺性的感觉。

当电影中的三叶和泷在夕阳中短暂见面,没来得及写完名字,然后被隔离在不同的时空,因为忘记对方的名字而痛苦不堪的时候,我喜欢的人,那个重要的人,就坐在我身边,看着同样的片段。

可是我呢。泷在三叶的手上写下了「すきだ」(「喜欢你」),而我连说出这三个字的勇气都没有啊。

而且我的这一切并不是一场梦,我们活在同一个时代,同一个城市,曾经是同班同学,我打电话的话,也不会出现「不在服务区」。

这部电影就是这样,在看完以后,独自回味的时候,就不禁热泪盈眶,比在电影院里产生的情感还要强烈。因为它把几乎每个人都会经历的故事加上了奇幻的色彩,再配上精良的画面和音乐,这种「可回味」的程度,这种可以引起共鸣的力量,就突显出来了。

这个故事平凡化的版本,确实是几乎人人都会经历的。它就是一个讲述着无法传达的爱意的故事 —— 曾经互相喜欢过,但没有互相表示过; 后来随着时间的流逝,可能去了不同的学校,不同的公司,不同的城市,甚至不同的国家,于是渐渐忘了对方,忘了TA的模样,忘了TA的名字。这一段未曾发生过的恋情,也就慢慢埋没在时间的长河里了,可能到死也不会互相知道,原来某某人和我曾经互相喜欢过。

泷和三叶的故事,在这之上加上了各种各样的奇迹。从一开始的相识,即跨越三年时间的交换身体,或者说交换灵魂,这就已经是不可能发生的事情。后来,得知三叶已故之后,通过喝「口嚼酒」连接未来和过去,靠一己之力说服镇长拯救全镇三分之一人口的性命,然后在多年后与泷君团聚,这就更是只存在于日漫中的天方夜谭。可是,正是因为这个故事太过完美,太过梦幻,才会给人带来那种看完电影过后的空虚和失落的感觉。电影的情节和现实的自己一对比……

它奇幻,但它处处是真实的影子。还有一个桥段,就是两人交换身体以后互相帮助对方追喜欢的人的那一段,就是非常典型的。类似的故事,「龙与虎」就曾经演绎过。因为互相喜欢,所以想帮助对方得到幸福,然后在这个过程中终于发现自己其实深爱着对方 —— 这绝对不仅仅是桥段而已。我的某个朋友就亲身经历过这样的故事……

看过「告白实行委员会」,那部番里面的喜欢,就远远没有「你的名字。」中的爱情完美,因为它少了这样的曲折的过程,仅仅是甜美而已。相差三年的平行世界,生与死的阻隔,都拦不住这份喜欢 —— 这大概是很多人一生都在苦苦追求的完美爱情吧。

可是当他们在夕阳中的山顶,跨越时间而相会的时候,这份完美的爱情或许仅仅是希望能再多一点时间在一起,哪怕再大声说几次自己的名字,哪怕就是那么互相看几眼。—— 这些在那个时候都成了奢求。

对我来说也就是如此。我真的不敢说什么一生一世,什么「无论你在世界的哪个地方,只要记得你的名字,我一定会去见你」,或是我能为你而如何如何如何的誓言。人容易改变,世界也容易改变,甚至可能某一天就突然再也不可能见面……这都不是不可能发生的事情。我即使有中二病,也深深地知道自己不可能做到只靠自己拯救一个城镇啊。

她是我三年前,也就是高一的同班同学。我提到过,我害怕周围人的目光,我害怕老师和父母的责骂,所以我连搭讪都不敢,连聊两句天都不敢,连碰面以后看见她的笑容都感到小鹿乱撞。但我只是觉得,能和她见面的每一天都那么美好……

想来我也是比较幸运的。新海诚让「你的名字。」的男主和女主在8年后仍能再见,而我,明明已经错过了最好的时光,却仍然能够和她去只隔一条马路的两所学校。错过的还能相遇,这也许是我一生中遇到的最幸运的事情之一吧。

所以,我才终于鼓起勇气,送礼物,请吃饭,约出来一起看电影……说实话,我之前从来没有和异性朋友一起做过这些事情。

正如「你的名字。」中美丽的彗星却带来了小镇的灭顶之灾一样,任何美好的事情都可能消逝,甚至变成悲伤的回忆。但是,在这之前,在这一切发生之前 ——

请给我多一点点时间和你在一起吧,一点点也好。

拜托了。

近期随想

距离上次更新博客,已经有好几个月的时间了。我已经好久没有写博客的心情了。这几天得流感在家休息,打开自己的博客,看见很久没有维护过的首页,再看看自己高中时写博客的豪情壮志,不禁感慨万千。这么久没有更新博客,想说的话积累了太多,无奈表达和组织语言能力太差……终于决定写这么一篇没什么主题的文章,权当灌水和发泄了。


1

其实不如这么说,进入大学以后,我似乎就再也没有更新过博客了。

我不敢说我自己很忙,毕竟我仔细算了一下,每周上课不过20+小时,双休,没有晚自习,周三下午没有课程。和在其他学校的高中同学比一比,这生活简直在度假。

然后我仔细回忆了一下这几个月我做了什么在大学课程之外的事情。

答案是基本上没有做什么。除了写了几行没什么用的,没有成为完整项目的代码以外,确实基本上什么也没做。干的最多的事情也许就是开着代码编辑器发呆,然后就这么度过几个小时。

看了看自己的 GitHub 贡献数量,发现我贡献最多的时候就是高二的那一年。然而高中是绝对不可能有这么多闲暇时间的。

也许这就叫做「犯贱」吧。没有时间的时候拼命挤出时间要做自己的所谓爱好,等到有时间了,反而觉得这「爱好」没那么重要了。

呜呼。


2

十月的时候,和一个高中同学一起回高中去玩了一趟。因为大学就在本市,所以去一趟也没有什么困难的地方。

在那里,一切感受都可以用两个字来形容,那就是「怀念」。至于高三时候那种压抑、怨恨以及一点点的戾气,根本就不会进入脑海。

所以说,记忆也许就是这么一个过滤器,剩下的总是美好的,那些苦涩全部都会化作一句句轻描淡写的说笑。

这大概就是为什么,作为「过来人」,仿佛总是难以理解那些后来的人; 这大概就是为什么,每一代人都会觉得下一代人「药丸」。

因为我们善于遗忘。


3

有时候觉得自己和这所大学的氛围格格不入。

我说的就是社团和社团活动。形形色色的社团活动。不知道有什么目的的社团活动。甚至出现吉他社搞「非诚勿扰」的让人摸不着头脑的事情……

我说话可能有点冲,但这些活动中的很多一部分,确实有点为了活动而活动的感觉。想起那个莫名其妙的秋日祭……宣传海报上各种浪漫,结果到头来只是一帮死宅摆着零散的几个摊位,一群 coser 莫名其妙地到处闲逛。

要我说,活动本来就在于质量而不是数量。有些社团几乎每隔一两个星期就要搞个大新闻,其质量可想而知。

对于社团成员的招收也应该同理。某些社团,空有数百名成员,一半以上是划水,整个社团组织松散,什么也做不成。

哦不,也许这一整个学校都有种这样的感觉。推行一个想法,几乎必定不会有多少人积极回应……

整个学校似乎都是划水的人居多 —— 讲到这我又忍不住要提到我之前参与又退出的某个创业项目,开始了几个月结果连一个靠谱的前端都没有,整个一「我们就差一个前端了」的感觉,还希望在圣诞节前(后来改成明年四月)上线……作为负责后端的人,我,实在是看不下去……

有毒。


4

喜欢就追。

这句话我想说给三年前的自己听。

我在之前的博客里应该已经有提到。高一的时候,有一个跟我同班的女孩子 —— 我喜欢她。大概是第一次见到以后没多久就有这种朦朦胧胧的感觉。

按照其他人的惯例,这时候应该多多尝试去和她说话,和她瞎扯,然后要到她的手机号,QQ号……我呢?见到她就紧张得一个字都说不出来。

后来高二分了班,选了一样的科目,不在同一个班但经常在门口遇见。她见到我的时候经常对我一笑,笑得实在太美,又让我一个字都说不出来……

高三毕业前,也就是我之前博客里提及这件事的时候,我在收集毕业留言的时候想到了她。因为不敢直接和她说话,我就让一位同学代我去请求她给我写一份留言。她认认真真写了千字还多,当然没有少「吐槽」我表现地那么冷漠。

那天下午就又碰面了。她冲我微笑,我挥手致意。

阴差阳错,大学竟然也只隔一条马路。我终于开始觉得,再不尝试一下,就太没有道理了,也太对不起过去的自己了。

其实,说说话,聊聊天,请出来玩玩,也没有那么难。只是高中时的自己太怯弱,害怕别人的目光,害怕老师,害怕父母……现在的我也没有摆脱这些问题,但是我在尝试着,尝试着传达这一份心意。

这一切已经晚了。我所做的不过是想弥补自己早就应该做而没做的事情所带来的缺憾 —— 请允许我这一点点的自私。我知道,我不可能被接受,她不太可能喜欢我。只要传达到,就够了吧。「届かない恋」的事情,还是不要在自己身上上演比较好 —— 我现在正是有这么一个机会去改变。

打算在圣诞节左右表白,了结这一切。祝我好运 —— 尽管成功的可能性太过渺茫。

我喜欢你。谢谢你给我带来的美好回忆。

如果可能重来的话,我愿意为这样的尝试付出一切。

感情是不能人为控制它开始,也不能人为强制结束的

—— 「恋爱管理」课程

早点听这课的话,就好了。


5

人渣总是比较受欢迎。

我不想指名道姓,也没有必要。我说的并不是某一个具体的人。

他们想要控制。恋情是他们的财产。他们热衷于后宫。他们视感情为游戏。

然而,恋爱中的人是没有理性的。

所以他们从来不会失手。

这是我在听了某朋友的故事,看了近期某微博上热门事件以后的感想。

保持文明,不多说。

自己不要变成这样的人就好了。—— 如此希望着。


6

还有一些事情,似乎不太适合在博客上公开发表,只能继续憋在心里了。

文笔实在有限,好多话还是无法精准地表达。我可能会在今后,就这篇文章中的某些话题,详细展开写一些单独的文章。

罢了,这样已经舒服许多了。

谢谢大家耐心看完我的牢骚。

在 ArchLinux 上配置 shadowsocks + iptables + ipset 实现自动分流

本来我是决定不再写这样的文章了的。但是呢,最近连续配置了两次 ArchLinux,在配置这种东西的时候连续撞到了同样的坑,加上今天 Issac 亲问我关于 Linux 下的 shadowsocks 的问题,所以我想了想还是写一篇记录一下吧,也免得自己以后再忘记了。


2017-01-25 更新:

我编写了一个脚本来自动化原文所述过程,源码和使用方法在

https://github.com/PeterCxy/shadowsocks-auto-redir.sh

比本文中介绍的方法要方便很多。

以下是原文。


本篇的目标是使用 ipset 载入 chnroute 的 IP 列表并使用 iptables 实现带自动分流国内外流量的全局代理。为什么不用 PAC 呢?因为 PAC 这种东西只对浏览器有用。难道你在浏览器之外就不需要科学上网了吗?反正我是不信的……

前置条件

  • 一个能使用的 shadowsocks 服务端,假设它的 IP 是 192.168.1.100, 端口是 6666, 加密方式是 chacha20, 密码是 1234
  • 一个安装了 shadowsocks-libev 的 ArchLinux; 其他发行版不保证可用,但如果有 shadowsocks-libev 以及 shadowsocks-libev@.service 的话,步骤应该大同小异
  • ipset 和 iptables 工具
  • systemd ~~卖底裤~~全家桶

创建配置

首先创建配置目录 /etc/shadowsocks/config.json

{
  "server": "192.168.1.100",
  "server_port": 6666,
  "local_port": 1080,
  "method": "chacha20",
  "password": "1234"
}

然后运行

systemctl start shadowsocks-libev@config
systemctl status shadowsocks-libev@config

看看有无异常输出,此时你也可以打开浏览器连接到 1080 端口的 socks5 代理测试服务器是否正常。

获取 IP 列表

接下来我们需要获取中国的 IP 列表。在此之前我们需要创建一个目录来储存需要的脚本和其他文件。我建议放在 $HOME 下,或者 /opt 下。这里假设我们创建并切换到了目录 /home/peter/shadowsocks

curl 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep ipv4 | grep CN | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > chnroute.txt

这是来自 ChinaDNS 的指令。

创建启动和关闭脚本

创建 /home/peter/shadowsocks/ss-up.sh

#!/bin/bash

# Setup the ipset
ipset -N chnroute hash:net maxelem 65536

for ip in $(cat '/home/peter/shadowsocks/chnroute.txt'); do
  ipset add chnroute $ip
done

# Setup iptables
iptables -t nat -N SHADOWSOCKS

# Allow connection to the server
iptables -t nat -A SHADOWSOCKS -d 192.168.1.100 -j RETURN

# Allow connection to reserved networks
iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN

# Allow connection to chinese IPs
iptables -t nat -A SHADOWSOCKS -p tcp -m set --match-set chnroute dst -j RETURN

# Redirect to Shadowsocks
iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-port 1080

# Redirect to SHADOWSOCKS
iptables -t nat -A OUTPUT -p tcp -j SHADOWSOCKS

大部分代码还是来自 shadowsocks-libev 项目。

这是在启动 shadowsocks 之前执行的脚本,用来设置 iptables 规则,对全局应用代理并将 chnroute 导入 ipset 来实现自动分流。注意要把服务器 IP 和本地端口相关的代码全部替换成你自己的。

这里就有一个坑了,就是在把 chnroute.txt 加入 ipset 的时候。因为 chnroute.txt 是一个 IP 段列表,而中国持有的 IP 数量上还是比较大的,所以如果使用 hash:ip 来导入的话会使内存溢出。我在第二次重新配置的时候就撞进了这个大坑……

但是你也不能尝试把整个列表导入 iptables。虽然导入 iptables 不会导致内存溢出,但是 iptables 是线性查表,即使你全部导入进去,也会因为低下的性能而抓狂。

然后再创建 /home/peter/ss-down.sh, 这是用来清除上述规则的脚本,比较简单

#!/bin/bash

iptables -t nat -D OUTPUT -p tcp -j SHADOWSOCKS
iptables -t nat -F SHADOWSOCKS
iptables -t nat -X SHADOWSOCKS
ipset destroy chnroute

接着执行

chmod +x ss-up.sh
chmod +x ss-down.sh

至此需要的脚本和配置文件已经全部准备完成了。

配置 systemd

首先,默认的 ss-local 并不能用来作为 iptables 流量转发的目标,因为它是 socks5 代理而非透明代理。我们至少要把 systemd 执行的程序改成 ss-redir。其次,上述两个脚本还不能自动执行,必须让 systemd 分别在启动 shadowsocks 之前和关闭之后将脚本执行,这样才能自动配置好 iptables 规则。

执行

sudo EDITOR=vim systemctl edit shadowsocks-libev@config

然后键入如下内容

[Service]
User=root
CapabilityBoundingSet=~CAP_SYS_ADMIN
ExecStart=
ExecStartPre=/home/peter/shadowsocks/ss-up.sh
ExecStart=/usr/bin/ss-redir -u -A -c /etc/shadowsocks/%i.json
ExecStopPost=/home/peter/shadowsocks/ss-down.sh

是的,那两个脚本必须以 root 权限才能执行,所以我把整个服务的执行用户都设为 root。这显然是存在安全隐患的,但是因为我的懒癌,所以我没有专门处理。如果要提高安全性的话,应该把两个脚本的执行单独抽出来做一个 shadowsocks-iptables.service, 然后利用 Systemd Unit 的依赖特性来实现自动执行。

至此,带自动国内外分流的 shadowsocks 客户端已经配置完毕。要启动的话

systemctl restart shadowsocks-libev@config

还可以设置自动启动

systemctl enable shadowsocks-libev@config

以上。

修订:

2016.9.24 - 由于 ArchLinux 更新,添加关于 CapabilityBoundingSet 的设定

结束的最后

我之前写过一篇题为 结束的开端 的文章。那是在高三的最后一个学期到来之前写下的。当时的我,对于如今的生活,是怀着一种恐惧和期待交加的心情。而现在,高考已经过去了一个多月,我的录取结果也已经知晓。不知不觉之间我自己早已身处当时我所憧憬的那个「结束」。

有开端则必有结束。身处高考之后一个多月的我,大概已经有足够的理智来重新审视刚刚过去的这三年的时光。最近看见 Touko 聚聚也更新了博客 关于高考, 我才终于决定写下这么一篇文章,以完成过去的我所留下的所谓「伏笔」。权当这是我的自我反省吧。当然,如果能传授一点人生经验,那就是坠吼的了。

抓住机会

所谓抓住机会,换一句话说,便是「不要怂」。这里咱可以拿自己作反面教材。

三月到四月的时候,西交利物浦大学有面向高中生的自主招生活动。那时候有同学邀请我一起参加这自招。那时的我的目标还是「至少上个苏大」,因为

考个苏大,还不容易?

我就抱着这样的想法错过了这次自招活动。

然而现在的情况是,我的分数和苏大差了十万八千里,倒是以压线的成绩进了西交利物浦。这也真是惊险的一幕。那时的我如果尝试了自招,如果万一就那么让我通过了,这情况就会发生一百八十度的转变了。

当然,说自招有什么黑幕,说难以以实力通过自招,那便是另一个话题了。我则是连「报名」的机会都拱手相让了。

所谓「怂」者,大抵如此,我在之前几篇讲述「情感生活」的博客中已经无数次吐槽过自己的这一点了。踌躇,寡断,选择困难,这就是高中时代的我。我最近将自己的高中时代定性为「失败的」,因为

既没有当成学霸,也没有追到喜欢的女孩子,就连自己所热爱的东西也没有做出多大的成就。

面对机会的我往往会经历这么一个流程

  1. 机会来了,一定要抓住
  2. 机会快到了,可是好像……不太好?
  3. 算了,过去吧
  4. woc 我为什么没有抓住机会啊!!!!!!!!!!!

专业马后炮,大抵如此。啊,顺便一提,曾经的我甚至连喜欢的女孩子抛给我的一个大好机会都没有抓住。那可是摆在眼前的……然而,在高考这种时候,生命是不允许你对自己的方向有任何含糊的。如果你想,就去做,如果有~~喜欢的人~~梦,就去追,这大概就是所谓「抓住机会」了吧。

如果有学弟~~/学妹~~看到这篇文章的话,我就这么装作学长的样子灌一灌心灵鸡汤吧。毕竟,就算这是我自己都看过千百遍的心灵鸡汤,我也没有学到任何东西……

我已经从你的全世界路过

像一颗流星划过命运的天空

—— 「从你的全世界路过」

珍惜身边的人

真实的故事是,当我知道我自己所选择的专业的性别比例是 9(♂):1(♀) 的时候,我几乎是要崩溃的,虽然我早就做好了这样的心理准备。

犹记得我们可爱的副校长在高考前的一次演讲上说过这么一句话

大学里有更多的人,有更多的……,有更多的机♂会♀在等着你

可是高中时代的那些朴素的情感,师生情也好,友情也好,所谓懵懂的「爱情」也罢,恐怕是再也不会有了。

林妹妹一蹦一跳地推开了门,吓了大家一大跳;Jessie 又开始晒和女儿的种种故事;赵太爷又说书般地讲述着文革往事;而我喜欢的女孩子又露出了开心的笑颜……

当时的我所没有察觉到的是,终于有这么一天我会必须向这一切说再见。终于有一天,那个她会穿上高跟鞋与工作服,那个他会套上西装打上领带,然后世界都变了模样。

这个三年只有一次。这个机会,若不抓住,便不会有下一次了。成为学霸也好,谈一场恋爱也好,那都是我们所活过的无法修改、不能美化的真真实实的人生啊。而且还有一个痛苦的领悟就是:对于很多人来讲,这两件事情是完全不冲突的。关于这个问题我可能会在另一篇文章中详细阐述——真正的爱,不应该成为一种负担。所谓学习,也是一样的道理。

总而言之,珍惜这一切,珍惜你在教室里所见到的每一张面孔。如果喜欢,那就努力地靠近那个人,努力地去追求。如果有朋友,那就成为真正的好哥们、好闺蜜。这样的心情,可能再也不会有了。

珍惜那些不会因为小事而嘲笑你,总是向你敞开心扉并准备好接受你的倾诉的那些人。

~~而我也差不多是一条废虾了。~~

きみとがよかった

ほかの谁でもない

でも目覚めた朝

きみは居ないんだね

—— 「一番の宝物」

Talk is cheap.

也就是说,少听别人关于高考「技巧」「方法」的瞎扯。

是的,我不是针对某一个人。我的意思是,在座的所有「技巧」和「方法」都是垃圾。

为什么这么说呢,因为就我个人的体验而言,真的到了考场上,真的到了高考这种紧张的考场上,一个人的所有行为,靠的都几乎是一种本能,很少有人能冷静到能够斟酌「技巧」与「方法」的程度。而当你满脑子都是各种方法和技巧的时候,又怎么可能真的能冷静下来去面对题目呢?

况且,这些「技巧」和「方法」的「提供者」们,自己也不过就像我们一样,曾经只是普通的高中生。他们很可能只是迫于母校、迫于过去的老师、迫于旧时好友的要求,而赶 deadline 般地写出来的。这种「技巧」和「方法」,价值能有多少?不过是一堆被咀嚼过的二手垃圾罢了。与其如此,不如好好自己看几页书,刷几道题。

Talk is cheap. Show me the code.

这句话是 Linus Torvalds 大神说的。我权且借用一下这句话。

总之,再听到有人关于「应试技巧」的话,就把它当作胡扯就好了。

不需要脆弱的伪装

我不要卑微的祈祷

我只要打开我的触角

去寻找、去寻找

—— 「去寻找」

给!我!好!好!睡!觉!

这句话一共有六个字,我用了六个感叹号。因为睡觉真的很重要。

不知道是不是我「提前衰老」,我初中的时候还能做到晚上12点以后睡觉,上了高中以后反而不行了。如果12点以后睡觉,那么第二天一醒来就会感觉~~身体被掏空~~疲倦至极,从上午开始就会在课上睡着。然而其实对于高考来说,最重要的就是在课上这点时间了,晚上即使睡得再晚也无济于事。

这也没有什么再多的话好说了。总之,给!我!好!好!睡!觉!

只想看你熟睡的样子

我只想走进你梦中

—— 「晚安,晚安」

我不后悔

成功也好,失败也罢,高中生活也算是就这么画上句号了。不管完美不完美,它都有值得回忆的事情。那便是「最珍贵的宝物」。

我在微博上发过我刚刚提及的描述我「失败的高中生活」的时候甚至有人这么评论

可是你成了网红

然而网红并不会只有4000+的微博关注。不过不管怎么说,这三年里,我也算是开了 BlackLight 等几个大坑,填上了一部分,并获得了这么一群关心着我的人。当然,真正开始理解「博客」,开始写博客,这也是很重要的。

反观这三年,尽管我没当成学霸,也怂得没有追到喜欢的人,但是我至少没心没肺地过得很开心,还成功由「Angel Beats」入了宅。至于这最后的成绩嘛,用我同学的一句话说,就是 I deserve it. 认识到这一点,便好了。

过去终于会化为记忆中的一团泡影,即使它再美好,再绚丽。那个穿着长裙的她,也会永远只活在那幅画里,「拉不出来,自己也回不去」。我并不会后悔自己所错过的这一切,因为即便是错过,那也是所真实地活过的人生之一。不可篡改,不可美化,不可磨灭。

不小心写成了鸡汤文的风格。恳请大家再允许我这么鸡汤一次吧。

ふわふわる ふわふわり

あなたが笑っている それだけで笑顔になる

神様ありがとう 運命のいたずらでも

めぐり逢えたことが しあわせなの

—— 「恋愛サーキュレーション」

再见,Ghost

旧爱

我切换到 Ghost 这个博客引擎,其实也没有很久的时间。当时切换到 Ghost,主要原因是 Jekyll 这样的博客引擎没有一个好用的网页编辑器或者客户端,而当时的我还是高中生,经常需要在手机上编辑并发布博客。而 Ghost 恰好有一个好用的网页前端,所以我当时就决定把博客迁移到 Ghost 平台上。

但是 Ghost 也存在相当多的问题

  • 插件系统较为鸡肋,难以扩展
  • 服务端不能执行代码高亮,代码高亮需要在客户端执行
  • 不能自定义主题的参数,导致不修改主题文件难以实现自定义
  • 编辑器不自带 Markdown 语法高亮

再加上我现在已经可以使用电脑写作,使用手机的时间大大减少,这就是换一个博客系统的好时机了。

新欢?

我曾经是 Jekyll 的用户,当时使用中比较蛋疼的一个问题就是自动更新与缓存刷新 —— 因为那是纯静态博客,所以必须再单独实现一个服务来监听更新并同步。虽然说博客这种东西本身静态和动态就没什么大的差别,但是我还是更倾向于「半动态」的博客,这样也更便于实现插件系统。

而我又恰好正在为「手生」烦恼 —— 整个高三没写几句代码,突然放暑假,想要填上自己的那些坑,却猛然发现自己已经不习惯于写代码,对着 IDE 无从下手。博客系统这种东西,说复杂,也没什么复杂的地方,倒不如就此机会自己开个坑,也好练练手,满足一下自己的「虚荣心」,咱也不指望会有其他人使用我写的博客系统了……

一个博客系统,无非就这样几个部分

  • HTTP 服务器
  • 渲染引擎
  • 模板引擎
  • RSS生成器

至于评论系统,我暂时觉得依赖 Disqus 还不是什么大问题。这几个部分,我稍微思考了一下,发现并没有什么特别难做的 —— 毕竟我的目标并不是完全从头造一遍轮子。HTTP 服务完全可以依赖 express.js 之类的现成框架,模板引擎可以使用 Handlebars.js —— 这可以极大地方便我把 Ghost 的模板直接移植过来。所以,说开坑,我就这么开坑了。

这个博客系统坑的名字就叫 Typeblog,和本博客的名字一样,因为这个名字具有特殊的含义。下文中如不作特殊说明, Typeblog 均指该博客系统。

存储结构

按照我的设计,一个博客应该是一个目录 —— 与博客程序独立的目录。这个目录应该同时是一个 npm 包,即含有 package.json。这个包通过依赖的形式把 Typeblog 安装至自己的 nodemodules 下。Typeblog 程序有一个主可执行文件 typeblog(当使用 npm 安装时,它将会位于 `nodemodules/.bin/typeblog`),执行即代表启动该博客。程序将会自动载入当前目录下(博客根目录)的 config.json 作为配置文件。这个 config.json 应该具有类似这样的结构

{
  "title": "MyBlog",
  "description": "Just my blog",
  "url": "http://example.com",
  "plugins": [
    "..."
  ],
  "posts": [
    "posts/post1.md",
    "posts/post2.md"
  ]
}

其中配置的值和键的语义相符,我就不一一解释了。其中 posts 字段下的是一个文件路径数组,里面指定的是相对于博客根目录(即程序工作目录)的路径。程序启动和重载时将载入这些路径上的文章,至于解析过程稍后会提及。这里需要提前说明的是,该数组是一个有序数组,排序靠前的文章在最终生成的博客中也将靠前(因为我受够了按日期自动排序的博客系统 —— 不是所有的文章都能按日期排序!)

同时,博客程序还会监听这个 config.json 的改动(使用 chokidar 实现),一旦发生改动,程序将自动触发配置重载,同时重载文章列表。这主要是为了方便本地调试。在服务器端,我们将采用其他方式触发配置的重载。

插件系统

我这个博客程序,非常重要的一部分就是插件。我的计划是,渲染引擎中的大部分将使用插件的形式呈现,包括文章格式解析, Markdown 解析,代码高亮等。这就要求我在一开始就考虑到插件的存在。

于是呢,我设计了一个插件基类

class Plugin
  constructor: ->
    registerPlugin @

plugins = []
registerPlugin = (plugin) ->
  plugins.push plugin

当一个继承了 Plugin 类的子类被实例化时,它就会自动被加入这里的数组 plugins。子类需要实现它们自己可以实现的方法。当主程序需要调用一个支持插件的方法时,它将使用这个过程

callPluginMethod = (name, args) ->
  for p in plugins
    if p[name]? and (typeof p[name] is 'function')
      [ok, promise] = p[name].apply @, args
      return promise if ok
  [ok, promise] = defaultPlugin[name].apply @, args
  return promise if ok
  throw new Error "No plugin for #{name} found"

将会遍历整个 plugins 数组,寻找含有需要的方法的插件类实现。当找到以后,程序将试图调用这个方法。方法的返回值是 [ok, promise] 的形式,如果 ok 为真,表示该方法支持当前的输入,此时 promise 将不会为 null,这个 promise 将会在方法内容执行完成后完成,它将直接作为这个函数的返回值返回。如果 ok 为假,表示该方法不支持当前输入,于是程序将继续搜寻其他支持该输入的实现。如果循环已经结束而没有任何实现被找到,程序将使用默认的实现。这个默认实现是在一开始就被实例化的,它默认被载入,不属于 plugins 数组,提供所有已知的插件方法的默认实现。

这种机制主要是考虑到类似这种需要

[
  "posts/my-blog.rst",
  "posts/hello-blog.md",
  "some-remote://xxxxx.md"
]

同一个博客中出现了不同格式的文章,还有不同的存储后端 —— 有的文章甚至存在于远端。这就需要实现同一个方法的插件能够互相分工各司其职。

在刚刚的 config.json 中,大家也能看见我专门安排了一个 plugins 字段。当程序启动或重载时,将执行这样一个过程来载入全部插件

loadPlugins = (config) ->
  return if not config.plugins?
  config.plugins.forEach (it) ->
    if it.startsWith 'npm://'
      require it.replace 'npm://', ''
    else
      require "#{process.cwd()}/#{it}"

如果以 npm:// 开头,程序将作为一个 npm 包来载入这个插件,否则将作为相对于当前路径的单个文件来载入。插件是可以直接使用 CoffeeScript 编写的 —— 博客程序已经载入了 coffee-script/register

当然,这里还存在一个问题,就是被载入的插件无法载入它的父模块 plugins。而在这个父模块里,我将必须的依赖及 Plugin 基类都作为 module.exports 导出了。这就十分尴尬了。于是我使用了一个小小的 hack

require.cache['plugin'] = module # Enable this to be directly required
Module = require 'module'
realResolve = Module._resolveFilename
Module._resolveFilename = (request, parent) ->
  if request is 'plugin'
    return 'plugin'
  realResolve request, parent

将这个模块强制加入 require.cache 并替换 _resolveFilename 方法使其不会找不到模块。于是,在其他插件中,只需要 require 'plugin' 即可载入这个父模块,也就能够继承基类了。

文章格式

完成了插件系统,下面就该提供文章格式解析的默认实现了。

当载入文章时,程序将会调用插件系统的 parsePost 方法,这个方法只有一个参数,就是文件的原始内容。默认实现中,文章的头部应该包含文章的元数据。因为我自己多数时候使用 Markdown 格式写作,所以我提供了兼容 Markdown 的默认元数据格式,即类似这样

json
{
"title": "再见,Ghost",
"url": "goodbye-ghost",
"date": "2016-07-11",
"tags": ["Tech", "Blog"],
"parser": "Markdown"
}

这样,在解析文章时,只需要找到这一个代码块,然后使用 JSON.parse 解析元数据即可。

  parsePost: (content) ->
    end = content.indexOf '```\n'
    return [false, null] if not (content.startsWith('```json') and end > 0)
    start = '```json'.length + 1

    promise = Promise.try ->
      json = content[start...end]
      data = JSON.parse json
      data.content = content[end + '```'.length...].trim()
      return data
    .then (data) ->
      if not (data.title? and data.date?)
        throw new Error 'You must provide at least `title` and `date`'
      if not data.parser?
        data.parser = 'Default'
      if not data.url?
        data.url = encodeURIComponent data.title
      if not data.template?
        data.template = "post"
      return data
    .then (data) ->
      data.date = new Date data.date
      return data

这个 parser 指定的是解析程序,我将稍后解释。

所有的文章载入和解析的过程,都是在程序启动和重载的过程中完成的,不会在每次请求时执行,这是因为文章的内容一般不会随意变化,除非被触发重载事件。然而,模板的渲染却是在每次请求时实时执行的 —— 因为有的插件可能需要实时影响渲染的结果。

解析器

解析好元数据以后,程序会把剩下的内容丢给元数据中指定的解析器。所有的解析器都是插件方法,格式是 parseContent#{parser_name}。比如说 Markdown 的解析方法就是 parseMarkdown。对于未指定解析器的文章,程序提供了一个默认解析器 parseContentDefault

  parseContentDefault: (content) ->
    promise = Promise.try ->
      return content # Do no change on the content
    return [true, promise]

而我自己实现的 typeblog-markdown 插件则提供了一个 Markdown 解析器 (基于 marked)

{Plugin, dependencies, callPluginMethod} = require 'plugin'
{Promise} = dependencies
marked = require 'marked'

marked.setOptions highlight: (code, lang, cb) ->
  callPluginMethod 'highlight', [code, lang]
    .then (result) -> cb null, result
    .catch (err) -> cb null, code

class MarkdownPlugin extends Plugin
  parseContentMarkdown: (content) ->
    promise = new Promise (resolve, reject) ->
      marked content, (err, result) ->
        if err?
          reject err
        else
          resolve result

    return [true, promise]

  highlight: (code, lang) ->
    return [true, Promise.try ->
      return code
    ]

module.exports = new MarkdownPlugin

这个插件又需要一个名为 highlight 的插件方法。这个插件方法的作用是给代码块加上高亮。这个插件里提供了一个默认实现,就是什么也不支持的默认实现。要真正实现高亮需要再载入实现了 highlight 方法的插件。注意,为了覆盖这个默认实现,所载入的代码高亮插件必须在这个 Markdown 解析插件之前载入,即在 plugins 列表中位于 (npm://)typeblog-markdown 之前。我自己也实现了一个基于 highlight.js 的代码高亮插件,大家可以在文章尾部的 GitHub 仓库中找到。

主题引擎

主题引擎我采用了 Handlebars.js ,以便于移植我给 Ghost 做的主题。所有的主题文件都存放于博客根目录的 template 目录下,该目录的结构

/
- /assets
- - .....
- /partials
- - .....
- default.hbs
- index.hbs
- post.hbs

其中 {default,index,post}.hbs 是必须的。assets 目录会被映射到 blog_url/assets ,可用于存放 css 等。 partials 目录下的 .hbs 文件会在启动时被注册为 Handlebars 的 partial。同样,为了调试方便,当这个目录的文件有改动时,主题引擎会自动重载。不过,主题的重载支持是有限的,要完全刷新主题,还是最好重启。

default.hbs 是最后被渲染的,它是博客所有页面共享的框架,包含头部和尾部。index.hbs 和 post.hbs 都是具体页面的模板。渲染首页、首页的分页、Tag 页面和 Tag 页面的分页时,程序将调用 index.hbs 作为模板,它将被传递这样的上下文

{
  "blog": {
    "title": "..",
    "description": "..",
    "url": "..",
    "isHome": true
  },
  "firstPage": true,
  "lastPage": false,
  "nextPage": "/page/2",
  "prevPage": "",
  "curPage": 1
}

而当渲染具体文章时,它将被传递这样的上下文

{
  "blog": {
    "title": "..",
    "description": "..",
    "url": "..",
    "isHome": true
  },
  "post": {
    "...": "..."
  }
}

其中 post 就是文章的元数据,加上 content 字段即文章的解析后的内容。当 post 或 index 渲染完成后,其内容将被作为 content 字段传递给 default.hbs 作最后的渲染,它将收到这样的上下文

{
  "blog": {
    "title": "..",
    "description": "..",
    "url": "..",
    "isHome": true
  },
  "content": "...",
  "pageContext": {
    "...": "..."
  }
}

其中 pageContext 是当前页面的上下文,即刚才传给 index 或 post 的上下文对象。

另外,如果在 config.json 里面定义了 template_arguments 字段,那么这个字段会被传递到所有模板的上下文里,字段名称为 arguments。在文章元数据里定义的其他扩展字段也会被原样传递到 post.hbs 的上下文中。

默认我也提供了几个 helper, 包括用于引用 asset 并通过 md5 后缀强制更新浏览器缓存的 helper 和格式化日期的 helper 等。大家还是直接去 GitHub 仓库里面看。

其他

至于 RSS,我直接使用了 node-rss 来生成。

在搭建我自己的博客的时候,我又实现了两个(可能)只有我自己会用到的插件,比如一个 chinese-cdn 插件用于把 Google Fonts 和 cdnjs 等资源替换到国内 CDN,和一个 github-webhook 用于接收 GitHub 的更新通知并重载博客。这些插件都可以在本博客的仓库 https://github.com/PeterCxy/typeblog.net 看到。

我自己移植了一个主题 typeblog-diaspora 使用,就是之前移植的 ghost-diaspora 的修改版。这个主题需要在 config.json 的 template_arguments 内定义如下字段

  • cover: 博客的封面
  • disqus_username: Disqus 用户名
  • navigation: 博客主导航。是一个数组,每个成员都应该有 label 和 url 两个属性
  • social: 社交链接。同样是数组,每个成员都有 url 和 icon 两个属性。其中 icon 是 material-design-icons 中的图标名称(去掉 mdi- )

每篇文章的元数据中也可以指定每篇文章自己的封面。详细的配置方式还是请看我的博客配置。

该博客系统以 WTFPL 开源于 https://github.com/PeterCxy/Typeblog。另外,目前没有什么文档,如果有人真的想要使用这个博客引擎的话,我将会在有空的时候逐步完成文档。当然,你也可以直接联系 Telegram 上的 @PeterCxy 也就是我,我可以直接回答你的问题。

我理想中的现充生活

我并不是一位现充1,但我羡慕现充的生活很久了。我有成为现充的理想,只是一直没有实现罢了。于是,这是


理想的爱,应当是一种平等。而所谓平等,可能就体现于有争执的问题上了。

见过很多人吵架,往往是这么一个模式:

你说说看,这个A2是怎么回事?

那是因为我B2啊!

那你B有什么理由吗?

那是因为我C2

C你个***!还不老实告诉我?

嘿!我说了B啊!B这事有错吗?

我不管!你给我********

跟你说了B就是B!

那你A又是怎么回事?

于是便无限循环下去。我父母的吵架就是个典型的例子。在吵架的时候,他们可以把两三句话重复千百次,一两个小时都听他们不断重复一两句话,最后双方都没力气了才停下来。

这种呢,很多人觉得是吵架,其实可能连吵架都算不上,充其量是个无理取闹。在这种争执中,双方都试图凌驾于另一方之上,试图把自己的观点强加给对方。

我理想中的现充生活,是不能存在这样的争执的。当然,我并不是在说没有争执,而是认为争执的解决不应该以这样一种除了能够表现自己的嗓子能吼多大音以外没有任何作用的方式。

我所期待的是,晚上能够就今天白天未解决的问题查找资料,为第二天做准备;白天和「她」见面以后,几句寒暄,然后拿出昨天没有解决的问题来讨论。气氛很愉快,而语气应该尽量平和。即便观点分歧很大,「我」和「她」之间也应该做到互相尊重,并且应该就事论事,不要把问题扩展到问题本身之外,更不要作无意义的争吵。

只有两个人能心平气和地交谈,这才是平等的体现。失去了平等的基础的爱,我无法想象它会是一个什么样的存在。

于是几个小时就这么充实而愉悦地度过了。

这样的生活,才能叫「充实」,不是吗?


现充的生活应该充满了牵挂。


脚注


  1. 现充一词是源自日语“リア充”(リアじゅう,REAL→リアル→リア)的网络语言,指三次元的住民,也就是无需ACGN,单凭现实生活就能过得很充实的人[1],也可指某些二次元角色。常含贬义,特别是与“去死”二字连用时。近义词为人生赢家、人生胜利组和土豪等等。一般来说,容姿端丽、学业有成、财力雄厚、交际广泛和恋爱幸福,是现充的决定性要素。 —— Wikipedia 

  2. 这一段文字中所有A, B, C均指某一个事件或一个话题。