Arch Linux 星球

September 26, 2020

Alynx Zhou

固定 GNOME Shell 的输入法列表

GNOME Shell 有个令人很不爽的“特性”,它的输入法列表使用的是最近使用优先排列。也就是说当你有三个或以上输入法的时候,比如我,我有英文简体中文和日语输入法,我经常在中英之间切换,这没什么,前两个总是中英所以按一次就可以在这两个之间切换,但假如我偶尔用了一次日语输入法,我的列表就被打乱了,我不清楚按几下才能切回中文,并且再切到英文也得看一眼才能知道。

我不是很理解这个特性存在的意义,设置里面是可以手动调节输入法的顺序的,我明明调成了我想要的顺序,你就给我这个顺序好了,这样我闭着眼睛不用动脑子都能猜出来要按几下,比如从英文到日语按两下,中文到日语按一下等等。可能有些人的脑子长得比较擅长模拟最近使用优先排列?反正我不行。

既然感觉不爽那就动手处理一下好了,最近看了一些有关写 GNOME Shell 扩展的文档,所以写个扩展解决一下就可以了。为什么不直接提交给上游?因为上游一开始是固定顺序的,但是很久以前某个人加了这个“特性”,现在如果提个请求说删掉这个特性,势必会陷入一场“用户到底是喜欢最近使用优先排列还是固定排列”的争论,这肯定很难得出结论(毕竟大部分的人实际上是不需要使用输入法的英语用户以及只有两种输入法的用户!),并且按照 GNOME 上游的习惯他们也是不愿意为了这个多添加一个开关的。所以比起在拉锯战上浪费时间,先搞一个能用的才是我的风格。至于升级之后扩展挂掉……不就是在上游里和其他代码一起被重构和我自己单独重构的区别吗?只要我还在用应该就会持续更新了。

具体的解决方法比较 dirty,是我从别的扩展里学来的:把 GNOME Shell 里面的类的原型上的方法替换成自己的,就可以修改实例调用时的函数了(也算 JS 特性之一),不过不要用箭头函数,因为显然我们希望 this 是调用时的上下文也就是实例,而不是绑定到当前上下文。

因为这算是我第一个扩展所以也多少记录一下踩的坑。

首先 Gjs 的导入和 Node.js 的导入是不一样的,它通过一个 imports 对象引入其他库,比如通过 GI 导入的就在 gi 下面,因为是 GNOME Shell 扩展所以可以访问 GNOME Shell 的 JS 库,就是简单地把 JS 路径换成对象的 key 然后 JS 文件里所有的 varfunction 都会被导出。比如导入 Main 就是 imports.ui.main.Main

然后就是怎么知道要修改什么以及如何获取到相关对象,不过因为 GNOME Shell JS 部分经常重构,也没什么完整的文档,反正只能多花时间看代码吧,而且它的结构其实比看起来的要复杂,所以经常需要仔细翻来翻去的。比如 GNOME Shell 的输入法部分很多人认为是需要修改 iBus,实际上 GNOME Shell 只是调用 iBus 作为后端,自己处理状态和界面,这部分的代码都在 js/ui/status/keyboard.js 里面。

扩展主要有 init()enable()disable() 三个函数,init() 在 GNOME Shell 加载扩展时候调用,我这个显然不需要。enable() 是你在 Extensions app 里面打开开关时候调用的,disable() 是关掉开关时候调用的。

enable() 里面有几个需要我修改的地方,一个是阻止 InputSourceManager 在输入法切换之后的最近使用优先排列,解决方法很简单,需要自己替换掉 _currentInputSourceChanged 函数,注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L447-453 这一段更新代码。

当然光有这个还是不行的,这样假如你是先切换过再打开扩展,实际上列表是你开启扩展之前的状态而不是用户设置的顺序,所以我们还需要在打开扩展之后更新它的列表,让它直接读取用户设置。更新列表的函数是 _updateMruSources,假如检测到当前列表为空,会先从一个缓存的 gsettings 里读取之前存储的最近使用优先排列列表,这显然是很恶心的所以要注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L504-522 这一段。之后它会先加载当前列表里的,然后再把用户列表里增加的当前列表里没有的加到后面,因为我们已经决定要清空当前列表并且不加载 gsettings 里面的缓存,所以这个当前列表肯定是空,那直接加用户列表就行了,所以注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L525-533。这样我们后续只要清空 _mruSources 设置 _mruSourcesBackupnull 然后调用 _updateMruSources 就可以了。

然后我们需要获取运行时的这个 InputSourceManager 实例,这个实例没有被绑定到 Main 对象上,不过我阅读代码发现它是个单例模式,就是说在 js/ui/status/keyboard.js 有一个 _inputSourceManager 变量,然后有个 getInputSourceManager() 的函数,被调用时候如果有就返回 _inputSourceManager 否则创建一个赋值返回,其他代码都用的这个,所以我们也导入这个就行了。

然后你会发现另一个弱智的地方,怎么每次按下切换键,切换框都是从第一个切换到第二个?不是应该从我当前的切换到下一个吗?这个对于当前输入法总在第一个的最近使用优先排列是可以的,但在我们这个场景选中的并不总是第一个,所以需要修改。这部分函数是 _switchInputSource,可以看到它只是展示了一个 InputSourcePopup,而 InputSourcePopup 继承的是 imports.ui.switcherPopup.SwitcherPopup,这个类有一个叫做 _selectedIndex 的变量用于选择下一个上一个时候的计算,而且它默认是 0!不能通过参数初始化!真是头秃,不过我们可以在创建完切换框但展示之前单独设置这个值就行了,所以我在 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L412 的下一行插入如下代码:

if (this._currentSource != null) {
  popup._selectedIndex = this._mruSources.indexOf(this._currentSource);
}

因为我们不一定总有 _currentSource 所以还是要检查一下,如果没有的话让它从 0 开始也无所谓。

然后还有一个比较头痛的是快捷键是绑定的回调函数,回调函数又绑定了 this,所以我们光修改原型是改不了被回调的函数的,这个也简单,我们需要读一下 InputSourceManagerconstructor 的代码,然后删掉它在 Main.wm 里面绑定的组合键重新绑定成我们的,就是这样:

Main.wm.removeKeybinding("switch-input-source");
_inputSourceManager._keybindingAction =
  Main.wm.addKeybinding(
    "switch-input-source",
    new Gio.Settings({"schema_id": "org.gnome.desktop.wm.keybindings"}),
    Meta.KeyBindingFlags.NONE,
    Shell.ActionMode.ALL,
    InputSourceManager.prototype._switchInputSource.bind(_inputSourceManager)
  );
Main.wm.removeKeybinding("switch-input-source-backward");
_inputSourceManager._keybindingActionBackward =
  Main.wm.addKeybinding(
    "switch-input-source-backward",
    new Gio.Settings({"schema_id": "org.gnome.desktop.wm.keybindings"}),
    Meta.KeyBindingFlags.IS_REVERSED,
    Shell.ActionMode.ALL,
    InputSourceManager.prototype._switchInputSource.bind(_inputSourceManager)
  );

同样我们也不要忘记绑定 this,实际上我们希望调用的时候绑定的 this 是那个单例,那直接 bind 它就好了。

但是你会发现这个弱智的家伙没有按我们想象的工作!这是什么意思!仔细阅读代码之后我发现有如下逻辑:你按下的第一次组合键其实并不是算在那个弹框的按键回调里面,而是我们通过构造函数传递进去的,然后它分析这个传进去的按键是哪一种,调用 _initialSelection 执行第一次切换,而这个家伙更弱智了!明明有 _selectedIndex 它不用,竟然用硬编码的倒数第一个和第一个!真有你的啊!我不太敢修改 SwitcherPopup 因为还有别的东西使用它,那就修改 InputSourcePopup 这个子类吧,其实就是把 InputSourcePopup.prototype._initialSelection 这个函数原来的的 this._select(this._items.length - 1); 换成 this._select(this._previous());this._select(1) 换成 this._select(this._next())(1 其实是 0 + 1 的意思),不但功能增加了,可读性也提升了!

现在搭配起来应该和我们的需求一致了!但假如我关掉扩展之后希望列表是打开之前的状态怎么办!还记得之前说的那个 _updateMruSources 会读取 gsettings 吗?这个 gsettings 实际上在每次切换输入法的时候都会写入当前状态,那我们只要让它开启扩展时候不要写入,关掉扩展恢复的时候再更新不就读取了之前的状态吗。所以需要修改 InputSourceManager.prototype._updateMruSettings,注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L432-438

总结一下其实就是在 enable 的时候修改这些函数,然后获取单例,重新绑定快捷键,然后清空当前的列表重新更新列表,然后为了避免 bug,我们总是激活列表里的第一个输入法:

if (_inputSourceManager._mruSources.length > 0) {
  _inputSourceManager._mruSources[0].activate(true);
}

disable 的时候同样是把函数修改回去,然后获取单例,重新绑定快捷键这样它又绑成了原来的函数,然后清空当前的列表重新更新列表这样它就恢复到开启之前的顺序了,接下来同样地,因为最近使用优先列表的第一个元素肯定是正在用的,所以我们也还是激活第一个输入法就可以了。

完整的项目参见 GitHub,Arch Linux 用户也可以从 AUR 或者 Arch Linux CN 源安装 gnome-shell-extension-fixed-ime-list

Alynx Zhou

A Coder & Dreamer

September 26, 2020 02:40 AM

September 24, 2020

中文社区新闻

Arch Conf 2020 日程

我们将在10月10日和11日举办在线 Arch Conf。会议中将会有来自 Arch 团队的讲演和从社区提交的讲演和闪电讨论。

我们非常高兴能发布日程安排的初版计划!

https://pretalx.com/arch-conf-online-2020/talk/

会议所在时区是 CEST/UTC+2:
https://everytimezone.com/s/40cc4784

更新和附加消息将发表在会议主页上: https://conf.archlinux.org

期待您的参与!

来自会议主办团队的祝贺

by farseerfc at September 24, 2020 12:34 AM

September 23, 2020

frantic1048

Amakuni - 梅瑟·恩达斯特 (Endro~!)

Mei

期待已久的梅依这周平安无事到了,带着前些天到的大号背景纸一起开个箱。另外看到最近 Darktable 3.21 更新了 filmic rgb v42 模块,这次也尝试了一下它带来的新的工作流,总体体验还是令人满意的。

这次手办细节上感觉很稳,各个角度过去没有一眼就让人想开 retouch 模块开始划拉的地方,而且服装细节还原得非常棒。与之对比的是 之前的四糸乃,衣服的蓝色区域有很多并不是尘土的细小白点,虽然普通眼睛观察不怎么看不出来,但是到了照片里,就像平时看不到的尘土一样全都冒出来了,结果就是令人头秃的修图,毕竟都断过一次手了,这点不算什么

Yoshino with many retouches

好了,上图!

Mei Mei

Mei Mei Mei Mei

Mei Mei Mei

Mei


  1. darktable 3.2: containment effect! https://www.darktable.org/2020/08/darktable-3-2/

  2. darktable ep 067 - Filmic v4 https://www.youtube.com/watch?v=qVnuqbR7Z-M

September 23, 2020 12:00 AM

September 14, 2020

ヨイツの賢狼ホロ

如何可能正确的以大部分人可能会接受的方式提出电脑相关的问题

汝问咱为啥标题起得这么长,那是因为咱不敢保证所有的人都乐于接受这种提问方法啊……

以及似乎能衍生到其它领域?那就靠汝自己斟酌了。

本文参考了下面几篇文章的观点,某些文章可能正在经常的被提起。

  • 提问的智慧,原文作者 Eric S. Raymond,以开放源代码运动(不是自由软件运动,这个的领导者是 Richard Stallman)的提出者和主要领导者为人所知的黑客。本指南将教你如何正确的提问以获得你满意的答案。
  • 如何有效的报告 Bug,以程序员的视角阐述如何提交一份足够准确的 Bug 报告
  • X-Y 问题,一种常见的令人疑惑的提问方式,至于为啥令人疑惑嘛……
  • 真的,再这样提问就没人理你了,以提问的智慧的方法提出不一定是电脑相关的问题的方法,大概吧。

提问真的有那么多讲究的地方嘛?

大多数时候如此,因为有一个大前提:

“我们(在很大程度上)是自愿的,从繁忙的生活中抽出时间来解答疑惑,而且时常被提问淹没。所以我们无情的滤掉一些话题,特别是拋弃那些看起来像失败者的家伙,以便更高效的利用时间来回答赢家(winner)的问题。”

大多数人其实都是出于各种志愿目的回答来自不知道何处的提问的,除非……(“我本来想这样拒绝他的,但是他给的钱实在是太多了……”)

所以,你不必在技术上很在行才能吸引我们的注意,但你必须表现出能引导你变得在行的特质 -- 机敏、有想法、善于观察、乐于主动参与解决问题。如果你做不到这些使你与众不同的事情,我们建议你花点钱找家商业公司签个技术支持服务合同,而不是要求黑客个人无偿地帮助你。

于是换做汝自己来回答的话,下面两种问题汝更倾向于回答哪一种?(假设汝有回答这个问题所需的能力的话):

  • 我从 foo 项目找来的源码没法编译。它怎么这么烂?
  • foo 项目代码在 Nulix 6.2 版下无法编译通过。我读过了 FAQ,但里面没有提到跟 Nulix 有关的问题。这是我编译过程的记录,我有什么做的不对的地方吗?

可能不那么显然的,后面的提问者已经指明了环境,也读过了 FAQ,还列出了错误,并且他没有把问题的责任推到别人头上,他的问题值得被关注。

除此之外,对于黑客来说,如果能回答一个有挑战性的问题,或者能激发他们思维的好问题。对他们来说可能就不再是负担,而成了他们的一种乐趣。对黑客而言,"好问题!"是诚挚的大力称赞。(当然放在其它领域里也差不多啦。)

因为时间有限,所以不要活在别人的生活里。也是因为时间有限,于是大家都习惯的去忽略那些不愿思考、或者在发问前不做他们该做的事的人。汝在提问的时候一定不想被这样忽略,对吧?

如果你决定向我们求助,当然你也不希望被视为失败者,更不愿成为失败者中的一员。能立刻得到快速并有效答案的最好方法,就是像赢家那样提问 -- 聪明、自信、有解决问题的思路,只是偶尔在特定的问题上需要获得一点帮助。

在汝遇到问题并决定提出问题之前

于是首先汝在什么地方有了麻烦……是呗?

据完全不可靠的实践发现,大多数的问题都能在这几步方法全运用完之前得到解决……

尝试阅读硬件或软件的说明文档找到答案

硬件设计者和开发者遇到的问题大多数时候不会比汝少,有一部分他们曾经遇到的问题可能的解决方法都被他们写进了自己硬件或软件的说明书或其它地方里。

对于硬件的话,最常见的是说明书或者网站上的教学手册以及常见问题解答一类的。

对于常见的的桌面软件的话,可以在菜单中寻找“帮助”菜单,或者去软件的网站查找说明。

截屏2020-09-07 14.28.56

例如大多数 macOS 的软件的“帮助”菜单里可以找到部分说明。

如果是有点那么不常见的命令行软件的话,比较常用的获得说明的方法有两种:

一种是在后面加上 --help 再运行,会列出像是可以添加的参数一类的信息。

部分程序也有可能是 -h 之类的短参数,或者其它别的(例如 Windows 里的 /?

$ python3 --help
usage: /usr/local/bin/python3 [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-b     : issue warnings about str(bytes_instance), str(bytearray_instance)
         and comparing bytes/bytearray with str. (-bb: issue errors)
-B     : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
-c cmd : program passed in as string (terminates option list)
-d     : debug output from parser; also PYTHONDEBUG=x
-E     : ignore PYTHON* environment variables (such as PYTHONPATH)
-h     : print this help message and exit (also --help)

...

另一种做法是查阅相应的手册页,这里需要用到 man 命令。最简单的用法像这样:

$ man 要查询的命令的名称

这个 man 是男人还是 manual 的缩写呢……

典型的手册页大概像这个样子(还是拿刚才的 python 来举例),这里再加上一些解释?

实际上 man 会调用系统设置的分页程序来打开手册页(比较常见的是 less),至于 less 的用法嘛……大家可以去查阅 less 的手册页,溜了溜了……

PYTHON(1)                                                                        PYTHON(1)


# 程序的名字和一行简介
NAME
       python - an interpreted, interactive, object-oriented programming language

# 这里的 python 是一个命令,于是描述它如何运行,以及需要什么样的命令行参数。
# 如果查阅的是函数的手册页,可以看到函数所需的参数,以及哪个头文件包含该函数的定义。
SYNOPSIS
       python [ -B ] [ -b ] [ -d ] [ -E ] [ -h ] [ -i ] [ -I ]
              [ -m module-name ] [ -q ] [ -O ] [ -OO ] [ -s ] [ -S ] [ -u ]
              [ -v ] [ -V ] [ -W argument ] [ -x ] [ [ -X option ] -?  ]
              [ --check-hash-based-pycs default | always | never ]
              [ -c command | script | - ] [ arguments ]

# 更长的描述
DESCRIPTION
       Python  is  an  interpreted, interactive, object-oriented programming language that
       combines remarkable power with very clear syntax.  For an introduction to  program-
       ming  in  Python,  see the Python Tutorial.  The Python Library Reference documents
       built-in and standard types, constants, functions and modules.  Finally, the Python
       Reference  Manual  describes the syntax and semantics of the core language in (per-
       haps too) much detail.  (These documents may be located via the INTERNET  RESOURCES
       below; they may be installed on your system as well.)

# 可以在命令行下使用的参数以及参数
COMMAND LINE OPTIONS
       -B     Don't write .pyc files on import. See also PYTHONDONTWRITEBYTECODE.

       -b     Issue  warnings  about str(bytes_instance), str(bytearray_instance) and com-
              paring bytes/bytearray with str. (-bb: issue errors)

       -c command
              Specify the command to execute (see  next  section).   This  terminates  the
              option list (following options are passed as arguments to the command).

(下面其实还有很多的为了节省空间就省掉了……)

# 这一部分是 Python 的手册页特别的,介绍了解释器的接口
INTERPRETER INTERFACE
       The  interpreter interface resembles that of the UNIX shell: when called with stan-
       dard input connected to a tty device, it prompts for  commands  and  executes  them
       until an EOF is read; when called with a file name argument or with a file as stan-
       dard input, it reads and executes a script from that file; when called with -c com-
       mand,  it executes the Python statement(s) given as command.  Here command may con-
       tain multiple statements separated by newlines.  Leading whitespace is  significant
       in  Python  statements!  In non-interactive mode, the entire input is parsed before
       it is executed.

# 安装的文件和目录
FILES AND DIRECTORIES
       These are subject to difference depending on local installation conventions; ${pre-
       fix} and ${exec_prefix} are installation-dependent and should be interpreted as for
       GNU software; they may be the same.  The default for both is /usr/local.

# 可以设置的环境变量
ENVIRONMENT VARIABLES
       PYTHONHOME
              Change the location of the  standard  Python  libraries.   By  default,  the
              libraries  are  searched  in  ${prefix}/lib/python<version>  and ${exec_pre-
              fix}/lib/python<version>, where ${prefix} and ${exec_prefix}  are  installa-
              tion-dependent directories, both defaulting to /usr/local.  When $PYTHONHOME
              is set to  a  single  directory,  its  value  replaces  both  ${prefix}  and
              ${exec_prefix}.   To  specify different values for these, set $PYTHONHOME to
              ${prefix}:${exec_prefix}.

# 手册页的作者
AUTHOR
       The Python Software Foundation: https://www.python.org/psf/

# 一些在网上的资源的链接
INTERNET RESOURCES
       Main website:  https://www.python.org/
       Documentation:  https://docs.python.org/
       Developer resources:  https://devguide.python.org/
       Downloads:  https://www.python.org/downloads/
       Module repository:  https://pypi.org/
       Newsgroups:  comp.lang.python, comp.lang.python.announce

# 许可协议信息
LICENSING
       Python is distributed under an Open Source license.  See the file "LICENSE" in  the
       Python  source distribution for information on terms & conditions for accessing and
       otherwise using Python and for a DISCLAIMER OF ALL WARRANTIES.



                                                                                 PYTHON(1)

偶尔汝会在手册页见到 malloc(3) 这样的描述,这表示了特定区块的手册页。在 BSD 和 GNU/Linux 中,手册页通常被分作八个区块:

1 一般命令 2 系统调用 3 库函数,涵盖C标准函数库 4 特殊文件(通常是/dev中的设备)和驱动程序 5 文件格式和约定 6 游戏和屏保 7 杂项 8 系统管理命令和守护进程

于是要查阅特定区块的手册页的话,大概是这个样子(例如刚才的 malloc(3) ):

$ man 3 malloc

尝试上网搜索找到答案

有很多的问题也许不是第一次发生,那大抵汝也不会是第一次遇到的家伙。也许有人留下过类似的解决过程,如果汝找到了而且能成功地解决汝目前的问题,那这世界上就可能少了一个蠢问题(?)

可以搜索的地方也有很多,例如汝正在使用的软件或操作系统发行版的网站(如果汝没在上一步试图寻找的话)、论坛(有可能是官方名义的,或者是社区建立的)和邮件列表的存档,或者某个曾经解决过类似问题的家伙留下的文章等等。

至于搜索引擎要咋用,这个实在是太复杂了,咱也只能给出一些建议:

  • 选对一个好的搜索引擎就差不多成功了一半,嗯。
  • 把程序提示汝的消息作为搜索关键词可能有奇效,例如 Permission denied (publickey). (当然太长的话可能会被搜索引擎剪掉,于是尝试一下找出关键的部分吧。
  • 如果要自己决定关键词的话,首先不要用提问的方式,比如「我的电脑上不了网怎么办」,要寻找问题的线索,将线索变成关键词去搜索,一个关键词找不到就换另一个。啥?汝连这一步都懒得做?那么汝大抵有足够的资金找一个或者一群人帮汝解决汝现在遇到的问题,这就不在讨论范围内了……
  • 介于现在的硬件和软件都大量使用英语,有时也可以试试用英文搜索。
  • ……

尝试问一下汝身边熟悉的朋友?

这个有很大一部分是感性原因,因为某个完全不可靠的证据表明,汝身边最亲近的朋友大抵是最能忍受来自汝自己的看着很蠢的问题的。至少会比一无所知的陌生人容忍度大一点点。

不过朋友毕竟也是人,耐性也是有限的。(汝要是来问咱的话大概耐性会更低)于是为了避免发生像是喋血街头一类的惨剧,还是不负责的建议先把功课做一做了再提问。

天有不测风云?

不过要是汝已经做过了之前的尝试但是问题还是没解决的话……

  • 坐下来放松,然后再来一次(?)。

不要指望几秒钟的 Google 搜索就能解决一个复杂的问题。在向专家求助之前,再阅读一下常见问题文件(FAQ)、放轻松、坐舒服一些,再花点时间思考一下这个问题。相信我们,他们能从你的提问看出你做了多少阅读与思考,如果你是有备而来,将更有可能得到解答。不要将所有问题一股脑拋出,只因你的第一次搜索没有找到答案(或者找到太多答案)

  • 再次思考汝将要提出的问题。有不可靠的统计显示出寻求帮助前汝为解决问题所付出的努力程度和汝获得实质性的帮助的机率很大概率成正相关。

另一方面,表明你愿意在找答案的过程中做点什么是一个非常好的开端。谁能给点提示?我的这个例子里缺了什么?以及我应该检查什么地方请把我需要的确切的过程贴出来更容易得到答复。因为你表现出只要有人能指个正确方向,你就有完成它的能力和决心。

  • 再收集一些更详细的信息。例如 POST 画面的提示,主板七划显示器上的字形,软件弹出的提示,汝在之前做了些什么等等。

https://wiki.archlinux.org/index.php/Bug_reporting_guidelines#Gather_useful_information 上也列出了哪些信息可能是有用的。

  • 然后多想一下汝要在哪里提问。

我要在哪里提问?

在网上比较常见的发问场所,大抵有聊天群组,论坛和邮件列表。不过无论在哪里,下面的建议似乎都非常实用。(但未经过详细的检验)

  • 不要在与主题不合的地方贴出汝的问题。例如在绘画群组里提问游戏技巧,多半没人会搭理。
  • 不要在探讨进阶技术问题的论坛张贴非常初级的问题;反之亦然。至于怎么样算进阶怎么样算初级嘛,这个要自己掂量一下了。
  • 不要在太多的不同地方上重复转贴同样的问题。有很大一部分原因是不同的地方有可能都是同一群人(大雾)。
  • 别像机关枪似的一次"扫射"所有的帮助渠道,这就像大喊大叫一样会使人不快。要一个一个地来。
  • 先特定后通用,例如先去特定发行版的论坛或邮件列表中提问,再到程序本身的论坛或邮件列表提问。
  • 有些软件的网站上可能记载有官方支持和提交 Bug 报告的规则,如果有看到的话,照做就是了。
  • 通过论坛或聊天群组来提供使用者支持服务有增长的趋势,电子邮件则大多为项目开发者间的交流而保留。所以最好先在论坛或聊天群组中寻求与该项目相关的协助。

别问蠢问题!以及……

最常见的蠢问题大概有这么几种,至于为什么这些问题被觉得蠢……

  • 我能在哪找到 X 程序或 X 资源?

就在咱找到它的地方啊,大笨驴 —— 搜索引擎的那一头。天哪!难道还有人不会用 Google 吗?

  • 我怎样用 X 做 Y ?

如果汝想解决的是 Y ,提问时别给出可能并不恰当的方法。这种问题说明提问者不但对 X 完全无知,也对 Y 要解决的问题糊涂,还被特定形势禁锢了思维。最好忽略这种人,等他们把问题搞清楚了再说。

  • 如何设定我的 shell 提示??

如果汝有足够的智慧提这个问题,汝也该有足够的智慧去 RTFM,然后自己去找出来。

  • 我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗?

试试看就知道了。如果汝有试过的话,汝既知道了答案,也不用浪费我的时间了。

  • 我的{程序/设定/SQL 语句}不工作

这不算是问题吧,咱有更有意思的事要做呢,对汝这还要再问二十句才能知道问题在哪的问题完全提不起劲。

在看到这类问题的时候,大多数人的反应通常不外如下三种:

你还有什么要补充的吗?| 真糟糕,希望你能搞定。| 这关我屁事?

  • 我的 Windows 电脑有问题,你能帮我吗?

能啊,扔掉微软的辣鸡,换个像 GNU/Linux 或 BSD 的开放源代码的操作系统吧。

注意:如果程序有官方 Windows 版本或者与 Windows 有互动(如 Samba),你可以问与 Windows 相关的问题, 只是别对问题是由 Windows 操作系统而不是程序本身造成的回复感到惊讶, 因为 Windows 对一般开发者来说实在太烂,这种说法通常都是对的。

  • 我的程序不会动了,我认为系统工具 X 有问题

汝完全有可能是第一个注意到被成千上万用户反复使用的系统调用与函数库档案有明显缺陷的家伙,或者汝完全没有根据。

不同凡响的说法需要不同凡响的证据,当汝这样声称的时候,汝大概已经有了清楚而详尽的报告作后盾了吧?

  • 我在安装 Linux(或者 X )时有问题,你能帮我吗?

不能。

咱只有亲自在汝自己的电脑上动手才能找到毛病。要么试试寻求汝附近的 GNU/Linux 使用者群组的实际指导?

注意:如果安装问题与某 Linux 的发行版有关,在它的邮件列表、论坛或本地使用者群组中提问也许是恰当的。此时,应描述问题的准确细节。在此之前,先用 Linux所有被怀疑的硬件作关键词仔细搜索。

  • 问题:我怎么才能破解 root 帐号/窃取 OP 特权/读别人的邮件呢?

想要这样做,说明了汝是个卑鄙小人;想找个黑客帮忙,说明汝是个不折不扣的大笨驴!

以及不要问那种看着就很像家庭作业的问题,这些问题很明显要汝自己找到答案。尽管汝也许需要点提示,但不要指望能从别人那里得到完整答案。

保持一个平和的正确态度

  • 虽然骄傲是不行的,但是也不要走向另一个极端。尽可能清楚地描述背景条件和问题情况,这比低声下气更好地定位了汝自己的位置。
  • 彬彬有礼,多用谢谢您的关注,或谢谢你的关照。让大家都知道汝对他们花时间免费提供帮助心存感激。当然这不是汝可以把报告写的粗心又含糊的借口。

使用有意义且描述明确的标题

在这个领域,卖惨大概是最没有作用的行为之一,宣称紧急极有可能事与愿违:大多数黑客会直接删除无礼和自私地企图即时引起关注的问题。更严重的是,紧急这个字(或是其他企图引起关注的标题)通常会被垃圾信过滤器过滤掉 —— 这样汝所希望能看到汝的问题的人可能永远都看不到汝那看似紧急的问题。

于是来看一下下面的例子:

救命啊!我的笔记本电脑不能正常显示了!

X.org 6.8.1 的鼠标光标会变形,某牌显卡 MV1005 芯片组。

现在用上面的想法思考一下哪一个问题更蠢?再小声的说一句,大家都觉得“目标-差异”格式的标题往往最能吸引黑客,因为这样他们马上就能知道汝的环境和遇到的问题。试试看用更聪明的方法改写上面提问的标题?

用清晰、正确、精准且语法正确的语句仔细组织汝的提问

从经验中大家得出了一个结论,粗心的提问者通常也会粗心的写程序与思考。(真的如此吗,欢迎来证实或证伪)

  • 正确的拼写、标点符号和大小写是很重要的。汝说关注这个很麻烦?那好咱们也觉得思考汝那错词百出的问题很麻烦,于是就不回答了。
  • 如果在非母语的论坛发问,尽管拼写错误之类的会宽容些,但还是不能怠于思考。以及拿不懂回复者使用的语言的话就用英文撰写问题。
  • 清楚的表达汝的问题以及需求。繁忙的人往往厌恶那种漫无边际的空泛提问。

要理解专家们所处的世界,请把专业技能想像为充裕的资源,而回复的时间则是稀缺的资源。你要求他们奉献的时间越少,你越有可能从真正专业而且很忙的专家那里得到解答。

  • 仔细、清楚地描述你的问题或 Bug 的症状。

来看看这句话:“我运行了FooApp,它弹出一个警告窗口,我试着关掉它,它就崩溃了。”这种表述并不清晰,用户究竟关掉了哪个窗口?是警告窗口还是整个FooApp程序?如果这样说,“我运行FooApp程序时弹出一个警告窗口,我试着关闭警告窗口,FooApp崩溃了。”这样虽然罗嗦点,但是很清晰不容易产生误解。

  • 描述问题发生的环境(机器配置、操作系统、应用程序、以及相关的信息),提供发行版和版本号。
  • 描述在提问前汝是怎样去研究和理解这个问题的。以及确定问题而采取的诊断步骤。

简单介绍一下汝来提问之前都做了些什么,我在 Google 中搜过下列句子但没有找到什么有用的东西 之类的反馈也是有用的参考意见。

  • 描述最近做过什么可能相关的硬件或软件变更。

问题发生前的一系列操作,往往就是对找出问题最有帮助的线索。因此,你的说明里应该包含你的操作步骤,以及机器和软件的反应,直到问题发生。在命令行处理的情况下,提供一段操作记录(例如运行脚本工具所生成的),并引用相关的若干行(如 20 行)记录会非常有帮助。

  • 尽可能的提供一个可以重现这个问题的可控环境的方法。

报告bug的最好的方法之一是“演示”给程序员看。让程序员站在电脑前,运行他们的程序,指出程序的错误。让他们看着您启动电脑、运行程序、如何进行操作以及程序对您的输入有何反应。

如果您必须报告bug,而此时程序员又不在您身边,那么您就要想办法让bug重现在他们面前。当他们亲眼看到错误时,就能够进行处理了。

确切地告诉程序员您做了些什么。如果是一个图形界面程序,告诉他们您按了哪个按钮,依照什么顺序按的。如果是一个命令行程序,精确的告诉他们您键入了什么命令。您应该尽可能详细地提供您所键入的命令和程序的反应。

  • 如果汝做完前一步后发现整个问题显得太长(特别是复现的方法和样例太过复杂时),尽量将它剪裁得越小越好。

这样做的用处至少有三点。 第一,表现出你为简化问题付出了努力,这可以使你得到回答的机会增加; 第二,简化问题使你更有可能得到有用的答案; 第三,在精炼你的 bug 报告的过程中,你很可能就自己找到了解决方法或权宜之计。

  • 除非汝非常、非常的有根据(例如有可以证明的测试或者修复的补丁),不要动辄声称找到了 Bug。最好写得像是自己做错了什么。

如果真的有 Bug,你会在回复中看到这点。这样做的话,如果真有 Bug,维护者就会向你道歉,这总比你惹恼别人然后欠别人一个道歉要好一点。

  • 描述目标而不是过程。在一开始就清楚的表示出来汝想做什么,然后再描述问题在哪里。

经常寻求技术帮助的人在心中有个更高层次的目标,而他们在自以为能达到目标的特定道路上被卡住了,然后跑来问该怎么走,但没有意识到这条路本身就有问题。结果要费很大的劲才能搞定。

所谓的 X-Y Problem 大抵如此,提出这种问题就是在一个根本错误的方向上浪费他人大量的时间和精力。这里有一个来自 CoolShell 的例子:

Q)问一下大家,我如何得到一个文件的大小 A1) size = ls -l $file | awk ‘{print $5}’ Q) 哦,要是这个文件名是个目录呢? A2) 用du吧 A3) 不好意思,你到底是要文件的大小还是目录的大小?你到底要干什么? Q) 我想把一个目录下的每个文件的每个块(第一个块有512个字节)拿出来做md5,并且计算他们的大小 …… A1) 哦,你可以使用dd吧。 A2) dd不行吧。 A3) 你用md5来计算这些块的目的是什么?你究竟想干什么啊? Q) 其实,我想写一个网盘,对于小文件就直接传输了,对于大文件我想分块做增量同步。 A2) 用rsync啊,你妹!*

  • 描述症状,而不是汝的猜测。(如果汝的推断如此有效的话,那汝还用向别人求助吗?)

因此要确信你原原本本告诉了他们问题的症状,而不是你的解释和理论;让黑客们来推测和诊断。如果你认为陈述自己的猜测很重要,清楚地说明这只是你的猜测,并描述为什么它们不起作用。

针对诊断者而言,这并不是一种怀疑,而只是一种真实而有用的需求,以便让他们看到的是与你看到的原始证据尽可能一致的东西,而不是你的猜测与归纳的结论。所以,大方的展示给我们看吧!

  • 然后去掉无意义的提问句,例如有人能帮我吗?或者这有答案吗?。除非汝想得到这样的回答:没错,有人能帮你或者不,没答案

使用易于读取且标准的文件格式发送易于回复的问题

没有人喜欢自找麻烦,难以阅读的问题让人没有阅读的欲望,难于回复的问题让人没有回答的热情。

首先不管在哪里提问,绝对,永远不要指望黑客们阅读使用封闭格式编写的文档,像微软公司的 Word 或 Excel 文件等。即便他们能够处理,他们也很厌恶这么做。

如果汝在邮件中提问,下面的建议可以参考:

  • 使用纯文字而不是 HTML (关闭 HTML 并不难)。
  • 使用 MIME 附件通常是可以的,前提是真正有内容(譬如附带的源代码或 patch),而不仅仅是邮件程序生成的模板(譬如只是信件内容的拷贝)。
  • 不要发送一段文字只是一行句子但自动换行后会变成多行的邮件(这使得回复部分内容非常困难)。设想你的读者是在 80 个字符宽的终端机上阅读邮件,最好设置你的换行分割点小于 80 字。
  • 但是,对一些特殊的文件不要设置固定宽度(譬如日志档案拷贝或会话记录)。数据应该原样包含,让回复者有信心他们看到的是和你看到的一样的东西。

如果汝在论坛中提问,下面的建议可以参考:

  • 一两个表情符号和彩色文本通常没有问题,但是不要滥用

花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来像个傻笑的小姑娘。这通常不是个好主意,除非你只是对性而不是对答案感兴趣。

  • 别要求通过电子邮件回复。

    要求通过电子邮件回复是非常无礼的,除非你认为回复的信息可能比较敏感(有人会为了某些未知的原因,只让你而不是整个论坛知道答案)。如果你只是想在有人回复讨论串时得到电子邮件提醒,可以要求网页论坛发送给你。几乎所有论坛都支持诸如追踪此讨论串有回复时发送邮件提醒等功能。

古老和神圣的传统和它的亲戚 - RTFM 和 STFW

有一个古老而神圣的传统:如果你收到RTFM (Read The Fucking Manual)的回应,回答者认为你应该去读他妈的手册。当然,基本上他是对的,你应该去读一读。

RTFM 有一个年轻的亲戚。如果你收到STFW(Search The Fucking Web)的回应,回答者认为你应该到他妈的网上搜索。那人多半也是对的,去搜索一下吧。(更温和一点的说法是 Google 是你的朋友!有时也许是别的样式,例如 LMGTFY 的链接或者“本群已和 Google 达成战略合作”等等。)

通常,用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址,而且他们打这些字的时候也正在读着。这些答复意味着回答者认为

  • 你需要的信息非常容易获得
  • 你自己去搜索这些信息比灌给你,能让你学到更多

你不应该因此不爽;依照黑客的标准,他已经表示了对你一定程度的关注,而没有对你的要求视而不见。你应该对他祖母般的慈祥表示感谢。

其实如果汝有努力的在提问前做好功课的话,应该不会收到这样的回复的吧……

我得到一个回复,但这是啥?

看来似乎是 zentry 卡住了;你应该先清除它。

如果汝看不懂回应,别立刻要求对方解释。先像以前试着自己解决问题时那样(利用手册,FAQ,网络,身边的高手),先试着去搞懂他的回应。如果真的需要对方解释,记得表现出汝已经从中学到了点什么。

哦~~~我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries,而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗?还是我看漏了什么?

我没得到答案,怎么办?

如果仍得不到回答,请不要以为我们觉得无法帮助你。有时只是看到你问题的人不知道答案罢了。没有回应不代表你被忽视,虽然不可否认这种差别很难区分。

总的来说,简单的重复张贴问题是个很糟的点子。这将被视为无意义的喧闹。有点耐心,知道你问题答案的人可能生活在不同的时区,可能正在睡觉,也有可能你的问题一开始就没有组织好。

你可以通过其他渠道获得帮助,这些渠道通常更适合初学者的需要。

有许多网上的以及本地的使用者群组,由热情的软件爱好者(即使他们可能从没亲自写过任何软件)组成。通常人们组建这样的团体来互相帮助并帮助新手。

另外,你可以向很多商业公司寻求帮助,不论公司大还是小。别为要付费才能获得帮助而感到沮丧!毕竟,假使你的汽车发动机汽缸密封圈爆掉了 —— 完全可能如此 —— 你还得把它送到修车铺,并且为维修付费。就算软件没花费你一分钱,你也不能强求技术支持总是免费的。

对像是 GNU/Linux 这种大众化的软件,每个开发者至少会对应到上万名使用者。根本不可能由一个人来处理来自上万名使用者的求助电话。要知道,即使你要为这些协助付费,和你所购买的同类软件相比,你所付出的也是微不足道的(通常私有软件的技术支持费用比开放源代码软件的要高得多,且内容也没那么丰富)。

我得到了能解决我的问题的答案,然后呢?

问题解决以后,别忘了感谢拿些帮助过汝的人啦,以及:

  • 写一个补充说明,不用太长。这样做的好处不止可以为汝赢得声誉(以及可能在下次提问时尝到甜头),也可能会帮助到未来遇到相似问题的家伙们。
  • 思考一下怎样才能避免他人将来也遇到类似的问题,思考一下写一份文件或加到常见问题(FAQ)中会不会有帮助。如果是的话就将它们发给文档维护者。

在黑客中,这种良好的后继行动实际上比传统的礼节更为重要,也是你如何透过善待他人而赢得声誉的方式,这是非常有价值的资产。

by Horo at September 14, 2020 09:00 AM

Phoenix Nemo

软件工程实践上的一点思考

曾经大学时对于软件工程这类理论课不屑一顾,认为这些课本都是只在大学里讲学而并不实际参与工程的教授写的东西。但是经过这些年从自己开发程序编写代码,到与公司团队同学、兴趣圈的朋友一起开发项目,也积累、总结了一些经验和教训。正巧昨晚在游戏建设里参与了这类讨论,于是记下一些思考免得忘记。

案例 1

命令方块是 Minecraft 里用于执行游戏命令、实现各种触发性或持续性功能的方块。在游戏地图中需要展示一些浮空的名称标签,便是用命令方块生成隐形盔甲架实现的。这些盔甲架参数复杂且需要在地图里很多特定位置生成,负责的同学便在每个生成的位置下面放了重新生成的命令方块,生成的坐标是相对坐标,因此写好标签的命令方块便可被无限复用。

由于盔甲架属于实体,而实体在 Minecraft 中被认为是不可靠的:有无数种可能这实体会被移动或被清除。
因此我的建议是:将这些命令方块全部放到控制室,坐标写成绝对坐标并加上统一标签,便可做到一键生成全部、一键清除全部。

该同学表示:不想写绝对坐标,因为很麻烦。

案例 2

由于游戏玩法的需要,编写了新的插件。几天后按照原计划应当可以准备第一次基本功能测试时,负责开发的同学表示只写了大约 1/4 的功能。进度很慢的原因是 Minecraft 的实现过于糟糕,而 Spigot 和 Paper 等修改版也没有很好封装 API 导致几乎所有的事件都需要手动处理。

接下来的协同开发中该同学又在反复尝试对配置文件中属性类似的部分使用同一个序列化/反序列化方法处理、对不同配置文件中的不同物品记录项也加上了一层包装来使得其能够被一个序列化/反序列化方法处理、在其他一些程序逻辑上也在尝试复用代码减少冗余度。

我说,你先专心把功能快速叠出来,然后再去想优化的事情。
这位同学表示不能接受,他认为代码应该从编写时就是整洁的。

论点:矫枉过正的代码复用

代码复用是很常见的代码结构优化方式。更少的代码冗余可以减少维护的复杂度,也降低出错的可能。

但是在案例 1 中,如此复用代码(放置同样的命令方块)却实际上造成了更多的冗余:如果要修改一个属性,就需要记录整个世界里每一个命令方块的位置,然后一个个去修改它。相反,由于游戏世界地图里的建筑几乎不可能变化(虽然现实需求很少会有这种条件),统一放在控制室、hard code
所有的坐标作为一个大方法调用,却是在这需求前提下的更好的实现方式。如果需要修改属性,可以只在一个地方修改所有的命令方块。

或者说,重复放置命令方块的过程,就是 copy’n’paste 冗余代码的过程。

而案例 2 则更具有代表性。在项目初期,是否应当关注代码质量?
我认为是应当关注的,但是这基于开发者的工程实践经验。优秀的、熟练的开发者应当在代码编写时就能灵活使用各种简单的优化手段减少初期的代码冗余,但是对于在校大学生没有足够的项目经验时,面对紧凑的项目时间安排应当集中更多精力实现功能。此时过分关注代码优化会被分心导致各种问题——例如这位同学编写的代码基本没有能够一次通过所有测试的情况,而且绝大多数的错误都看起来只是粗心,并不是不理解、写不出的问题。

论点:实现,调整,优化

“Make it work, make it right, make it fast.” – Kent Beck

这是很多软件工程推崇的敏捷开发指导方向。在案例 1 中,该同学只做了第一步——复用同样的、带有相对坐标的命令方块(方法)快速实现了所有的功能。但是从后续维护的角度来讲,这样的实现没有 make it right,更不用提 fast。

而在案例 2 中,这位同学将三个阶段在初期就全部揉进去,但是由于工程经验不足,在思考优化方案时花费了过多的精力,也导致了代码精度不够,反复修改也无法顺利通过测试。

从个人经验来看,前期的代码编写应注重功能实现,并在编码能力基础上直接编写清晰的代码结构。功能实现后,再根据需求和测试中的问题「重构」打磨细节、尝试更好的实现方式。这个过程不仅在完善整个程序,对自己的系统架构把握和设计经验也有很大的提升。最后一个阶段,则是针对性的优化少量代码使整个系统更加稳定、高效。

论点:架构的改动

这是一个比较小型的项目。需求和基本功能架构从一开始便已经讨论清楚。后续的调整不大,但是每当有少量的需求修改或架构微调,都导致了很大的代码变动。而按照这位同学的思路,每次改动都要重新思考代码结构,这浪费很多的时间。

从实际工程角度,需求变化并带来架构的微调甚至大改动都可以说是很常见的事情。在前期编码实现阶段如果揉入过多对于代码结构的过多考量,每次改动都可能会使这些思考的时间被浪费。因此,在前期编码时不应为架构考虑消耗过多的时间,而在重构过程中,由于已经完成基本的功能实现,且对已有代码还处在熟悉的热度,可以快速适配需要修改、调整的架构,并基于前期编码时的各种尝试和实验的结论选择最佳的实现方式。

以上是基于近期项目中的讨论,在软件工程层面上的思考。如有缺漏不当之处,欢迎指正。

曾经大学时对于软件工程这类理论课不屑一顾,认为这些课本都是只在大学里讲学而并不实际参与工程的教授写的东西。但是经过这些年从自己开发程序编写代码,到与公司团队同学、兴趣圈的朋友一起开发项目,也积累、总结了一些经验和教训。正巧昨晚在游戏建设里参与了这类讨论,于是记下一些思考免得忘记。

September 14, 2020 04:20 AM

WireGuard 真香

真是老了跟不上时代了,这么好的东西为什么我现在才开始用??

其实这东西刚出来就在关注了不过确实前段时间才有机会尝试折腾一下。优点很多,也有无数人写过文章介绍,所以就不再多废话。主要看中它的 PtP 特性(服务器之间)和支持漫游(服务器-客户端)。当然目前在梯子方面的表现,即便是优秀的隧道方案,但由于折腾的人多了,面对万里城墙,这谁顶得住哇。

所以本文只讨论 WireGuard 作为访问企业网的隧道方案,算是初步折腾的笔记。

服务器配置

一个基本的 PtP 配置结构 /etc/wireguard/wg0.conf

1
2
3
4
5
6
7
8
9
[Interface]
Address = 10.0.0.1/32
PrivateKey = [CLIENT PRIVATE KEY]

[Peer]
PublicKey = [SERVER PUBLICKEY]
AllowedIPs = 10.0.0.0/24, 10.123.45.0/24, 1234:4567:89ab::/48
Endpoint = [SERVER ENDPOINT]:48574
PersistentKeepalive = 25

生成私钥

1
2
wg genkey > privatekey
chmod 600 privatekey

基于私钥生成本机的公钥

1
wg pubkey < privatekey > publickey

或者一步完成的操作

1
wg genkey | tee privatekey | wg pubkey > publickey

额外生成预共享密钥来进一步增强安全性

1
wg genpsk > preshared

这样服务器之间的互联配置就基本完成了。使用 wg-quick up <config> 来快速启动 WireGuard。

如果要配合客户端使用,则需要配置 NAT。顺便如果客户端没有 IPv6,也可以通过此法来给客户端提供 IPv6 Enablement。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Interface]
Address = 10.200.200.1/24
Address = fd42:42:42::1/64
SaveConfig = true
ListenPort = 51820
PrivateKey = [SERVER PRIVATE KEY]

# note - substitute eth0 in the following lines to match the Internet-facing interface
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# client foo
PublicKey = [FOO's PUBLIC KEY]
PresharedKey = [PRE-SHARED KEY]
AllowedIPs = 10.200.200.2/32, fd42:42:42::2/128

[Peer]
# client bar
PublicKey = [BAR's PUBLIC KEY]
AllowedIPs = 10.200.200.3/32, fd42:42:42::3/128

在此例中需注意 Allowed IPs 不可 overlap 否则会造成包转发错误。

客户端

与上文中服务器配置相照应的客户端配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
[Interface]
Address = 10.200.200.2/24
Address = fd42:42:42::2/64
PrivateKey = [FOO's PRIVATE KEY]
DNS = 1.1.1.1

[Peer]
PublicKey = [SERVER PUBLICKEY]
PresharedKey = [PRE-SHARED KEY]
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = [SERVER PUBLIC IP ADDRESS]:51820

客户端的 AllowedIPs 如果使用 catch-all 0.0.0.0/0, ::/0 也就会默认转发所有的流量到服务器。该选项实际作用是路由表,控制哪些流量需要经由服务器转发。

配置完毕即可使用 wg-quick up <config> 启动 WireGuard。如果一切顺利,通过路由追踪应该可以看到流量已经交由服务器转发。

总结

由于工作需要,经常合上笔记本动身前往其他地方。在接入传统企业网例如 L2TP/IPSec 甚至 AnyConnect 都无法保证设备下次进入工作状态时可以立即恢复连接。而 WireGuard 在不同网络、不同地域、不同网络中断时间等各种情况下均可在下次进入网络覆盖时立即恢复连接,再也不必担心网络中断恢复时手忙脚乱配置隧道或者不小心泄密啦。

目前唯一的不足,大概就是还没有 Windows 客户端,没有办法推广到非技术部门(虽然影响不到我…

总之,真香.jpg

Reference:

[1] https://wiki.archlinux.org/index.php/WireGuard

真是老了跟不上时代了,这么好的东西为什么我现在才开始用??

September 14, 2020 04:20 AM

制作 Arch Linux 内存系统启动盘

之前尝试过 Arch Linux in RAM 完全运行在内存中的轻量业务系统,最近在维护一些物理服务器看到没有安装系统的服务器不断重启,想到了可以制作类似的内存系统启动盘,以高效完成系统测试、安装、远程维护等任务。

这时候就要祭出 mkarchiso 大法了。这是自动化制作最新版 Arch Live 镜像的工具集,当然也可用于制作定制化的 Arch 镜像。

准备

首先安装 archiso

1
~> sudo pacman -Syy archiso

它提供了两种配置方案,一种是只包含基本系统的 baseline,一种是可以制作定制 ISO 的 releng。要制作维护用 ISO,当然是复制 releng 配置啦。

1
2
~> cp -r /usr/share/archiso/configs/releng/ archlive
~> cd archlive

定制

整个过程不要太简单。先来了解下各个文件的用途:

  • build.sh - 用于制作镜像的自动化脚本,可以在这里修改一些名称变量或制作过程的逻辑。
  • packages.x86_64 - 一份要安装的包列表,一行一个。
  • pacman.conf - pacman 的配置文件,不用多说了吧。
  • airootfs - Live 系统的 rootfs,除了安装的包之外,其他的定制(以及启动执行脚本等)都在这里。遵循 rootfs 的目录规则。
  • efiboot / syslinux / isolinux 用于设置 BIOS / EFI 启动的配置。

[archlinuxcn] 仓库加入 pacman.conf

1
2
[archlinuxcn]
Server = https://cdn.repo.archlinuxcn.org/$arch

然后修改 packages.x86_64,加入 archlinuxcn-keyring 和其他需要预安装的包:

1
2
3
4
5
archlinuxcn-keyring
htop
iftop
iotop
ipmitool

按需修改即可啦。

要启动为内存系统,需要加启动参数 copytoram

修改文件 syslinux/archiso_pxe.cfgsyslinux/archiso_sys.cfg 文件,在启动参数后加 copytoram,像这样:

1
2
3
4
5
6
7
8
9
10
11
INCLUDE boot/syslinux/archiso_head.cfg

LABEL arch64
TEXT HELP
Boot the Arch Linux (x86_64) live medium.
It allows you to install Arch Linux or perform system maintenance.
ENDTEXT
MENU LABEL Boot Arch Linux (x86_64)
LINUX boot/x86_64/vmlinuz
INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img
APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram

启动时即可将整个 SquashFS 文件复制到内存。如果内存比较小,也可以指定 copytoram_size 来限制 tmpfs 占用内存的最大数量。

同样,也需要修改 efiboot/loader/entries/archiso-x86_64-usb.conf 的启动参数。在 options 行添加

1
options archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram

制作

创建工作目录和输出目录

1
mkdir -p work out

最后一步,只需要以 root 权限执行 ./build.sh 就可以啦。

要看具体执行过程的话,加 -v-h 看所有参数。

完成后,即可在 out 目录得到准备好的 ISO 文件。将其 dd 到 USB 闪存盘,大功告成(‘・ω・’)

Ref:

  1. https://wiki.archlinux.org/index.php/Archiso
  2. https://git.archlinux.org/archiso.git/tree/docs/README.bootparams#n53

之前尝试过 Arch Linux in RAM 完全运行在内存中的轻量业务系统,最近在维护一些物理服务器看到没有安装系统的服务器不断重启,想到了可以制作类似的内存系统启动盘,以高效完成系统测试、安装、远程维护等任务。

September 14, 2020 04:20 AM

使用 fs.WriteStream 编写超简单的日志流

虽然 console.log 很好用,但是生产环境需要保存日志的时候就比较蛋疼。暴力 fs.appendFile 会消耗大量的 file handler,因此用 writable stream 来复用 file handler 是更好的选择。

大概是个不能再简单的思路了。先创建一个写入流

1
2
3
const fs = require('fs');

let logStream = fs.createWriteStream('./test.log');

这样便创建了一个文件写入口,需要时直接调用 logStream.write 即可写入数据。
接下来编写一个用于记录日志的函数替代 console.log

1
2
3
function logger (message) {
logStream.write(message);
}

至此基本功能就写完啦。但是太简陋了对不对,还是要再加点装饰。

重写 logger 函数,区分 stdoutstderr

1
2
3
4
5
6
7
8
9
10
11
12
let logInfo = fs.createWriteStream('./stdout.log');
let logError = fs.createWriteStream('./stderr.log');

let Logger = {};

Logger.info = (message) => {
logInfo.write('[INFO] ' + message);
}

Logger.error = (message) => {
logError.write('[ERROR] ' + message);
}

感觉还是少了点什么…日期?

1
2
3
Logger.info = (message) => {
logInfo.write(new Date().toISOString() + ' [INFO] ' + message + '\n');
}

嗯嗯。这就像样了。把代码整合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs');

let logInfo = fs.createWriteStream('./stdout.log');
let logError = fs.createWriteStream('./stderr.log');

let Logger = {};

Logger.info = (message) => {
logInfo.write(new Date().toISOString() + ' [INFO] ' + message + '\n');
}
Logger.error = (message) => {
logError.write(new Date().toISOString() + ' [ERROR] ' + message + '\n');
}

module.exports = Logger;

需要用时

1
Logger.info('This is an information.');

现在看对应的 stdout.log 文件就有相应内容啦。

1
2
~> tail -f stdout.log
2018-11-18T10:52:57.333Z [INFO] This is an information.

不够刺激?

1
2
3
[...Array(10000)].forEach((item, index) => {
Logger.info('Hello! ' + index);
});
1
2
3
4
5
6
7
8
9
10
11
12
~> tail -f stdout.log
...
2018-11-18T10:58:30.661Z [INFO] Hello! 9990
2018-11-18T10:58:30.661Z [INFO] Hello! 9991
2018-11-18T10:58:30.661Z [INFO] Hello! 9992
2018-11-18T10:58:30.661Z [INFO] Hello! 9993
2018-11-18T10:58:30.661Z [INFO] Hello! 9994
2018-11-18T10:58:30.661Z [INFO] Hello! 9995
2018-11-18T10:58:30.661Z [INFO] Hello! 9996
2018-11-18T10:58:30.661Z [INFO] Hello! 9997
2018-11-18T10:58:30.661Z [INFO] Hello! 9998
2018-11-18T10:58:30.661Z [INFO] Hello! 9999

搞定(┌・ω・)┌超简单的吧。

虽然 console.log 很好用,但是生产环境需要保存日志的时候就比较蛋疼。暴力 fs.appendFile 会消耗大量的 file handler,因此用 writable stream 来复用 file handler 是更好的选择。

September 14, 2020 04:20 AM

重构 StickerSetBot

关注 Telegraf 有一段时间了。特别是最近 Telegram 上 spammer 猖獗导致 Telegram 对于用户行为限制越来越严格,由此想过写一个简单的 bot 来处理加群请求之类的。

总之原因都是没时间。终于搞定一些事情之后发现之前瞎写的 Telegram 导出贴图 bot 居然备受欢迎…正好 Telegram Bot API 也更新了,来重构吧!

拆分逻辑代码

最头疼的事情首先是当时写这 bot 的时候只顾着考虑各种情况,逻辑像流水一样全部写成一坨。虽然实际不复杂吧但这不是 best practice。于是把每个功能单独拆出来先。

on('command') 的逻辑代码整块移出来作为 handler,然后能够原子化的功能再单独拆分成函数调用。目前的效果虽然还是有不少逻辑层在 handler 里,但是基本达到了比较方便维护的目的。

handler 本来就是拿来写逻辑的啊摔

接下来再清理冗余代码和各种 hard code,加了两个方法让代码看起来更整洁一些。于是就先这样。

迁移框架

好在 Telegraf 和之前用的框架在参数上很多兼容,所以这没有花太多时间。顺便尝试采用了一部分 ES6 的风格,嘛…果然不喜欢。

所以就不要吐槽为什么 ES5 和 ES6 的风格混写了。

之前要一大长串的传参现在只要一个 context 了好方便啊。中间件也好方便啊~

以上。

调试:无尽的 bugfix

并不指望一通大换血之后的代码能一次跑起来…但是没跑起来的原因是我传错了中间件值这不能忍!!为什么一会儿传的是函数本体一会儿传的是函数调用啊摔!!

而且这问题还让我调了两个小时!!!

调通了之后就很舒服了

遇到的坑还有 context 本身不能当 session 用,然而不想再引入 session 中间件于是自己写了个超简陋的内存 session。就是为了多语言支持。因为一觉醒来发现这 bot 语言莫名其妙变中文了(messages 成了全局变量 = =

当然还有 Telegram 自己的坑,比如什么贴纸就是死下载不能然后整个程序就 hang 着了。

一键导出贴纸包

终于!Telegram bot API 添加了 StickerSet 类型。只要有贴纸包名称,就可以获取整个贴纸包的信息。考虑不改变用户习惯的情况下(你哪有什么用户啊可恶)对本身处理贴纸和其他消息的函数做了修改,顺便又拆了俩函数出来(怎么代码越来越多了啊喂!

最后结果就是没有一屏看不到头的函数啦~(你快够

以及加入了用贴纸包链接导出一整组贴纸的功能,算是真正意义上的 StickerSetBot 了。

然后贴纸过多卡死了 Telegram 的 ratelimiting

直接导出单张贴纸

既然功能拆分了那也就方便加更多别的功能啦。比如不新建任务,直接甩过去一张贴纸来获得 PNG 文件~

这只 bot 在这里,源码在这里。欢迎各种玩坏~(记得去发 issue

就酱(,,•﹏•,,)

关注 Telegraf 有一段时间了。特别是最近 Telegram 上 spammer 猖獗导致 Telegram 对于用户行为限制越来越严格,由此想过写一个简单的 bot 来处理加群请求之类的。

总之原因都是没时间。终于搞定一些事情之后发现之前瞎写的 Telegram 导出贴图 bot 居然备受欢迎…正好 Telegram Bot API 也更新了,来重构吧!

September 14, 2020 04:20 AM

Office Service Router 解决方案:Arch Linux in RAM

一直把自己在办公室的 PC 保持开机用于连回办公区、存取数据工作需求。由于最近办公室所在的写字楼要全馆断电检点,所以诞生了构建一个 Service Router 的想法。

思路

运行在内存里对于 Linux 系统来说是完全可能(而且简单)的事情。

最直接的想法就是使用内核 hook 在启动时复制根分区到内存盘然后挂载内存里的数据作为根分区即可。

设备的话,设置 Power on AC 即可通电自启动。

ramroot

作为一只懒卷,这种简单的事情当然先顺手搜索下啦。然后就发现了几乎完美的解决方案——ramroot

ramroot 通过加入内核 hook 然后自动在内存建立 zram 分区,同步根分区数据再启动。还可以在启动时选择是否启动进内存,正好解决了所有的需求。

实现

硬件选择是一台便宜的 Intel NUC,安装两根 4GB LPDDR3 低压内存和一块 120G 2.5 SSD。虽然说起来其实并不需要 SSD(因为数据全部都在内存里,速度比 SSD 更快)但是毕竟日本多震,还是为数据安全着想。毕竟硬盘坏了的话内存系统也无法启动了。

当然如果有集成 32GB eMMC 的小型 PC 的话也是好的选择。

正常安装完 Arch Linux 系统,安装 openssh 和各种必要的服务程序,修改配置文件,然后安装 ramroot 并执行

1
# ramroot enable

此时先别急着重启,先把不需要的包、缓存等文件(/var/cache)删除,保持最小化的根分区。然后再重启。便可看到加载内核 hook 时的提示是否进入内存系统,默认超时后就会自动复制根分区到内存啦。

由于整个系统是运行在内存中的,所以完全没有等待读盘的时间。整个系统的响应速度非常快。限制是内存不够大的话运行一些业务会比较捉襟见肘,而且这样低功耗、低发热的 SoC 处理性能也只能运行一些轻型任务。

下面是一些 IO 性能测试

1
2
3
4
5
6
7
8
9
10
11
12
# ioping -s 1G /
1 GiB <<< . (ext4 /dev/zram0): request=1 time=1.04 s (warmup)
1 GiB <<< . (ext4 /dev/zram0): request=2 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=3 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=4 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=5 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=6 time=1.04 s ^C

--- / (ext4 /dev/zram0) ioping statistics ---
5 requests completed in 5.18 s, 5 GiB read, 0 iops, 988.4 MiB/s
generated 6 requests in 7.20 s, 6 GiB, 0 iops, 853.8 MiB/s
min/avg/max/mdev = 1.04 s / 1.04 s / 1.04 s / 550.7 us
1
2
3
4
5
6
# ioping -RD /

--- / (ext4 /dev/zram0) ioping statistics ---
530.0 k requests completed in 2.49 s, 2.02 GiB read, 212.9 k iops, 831.7 MiB/s
generated 530.0 k requests in 3.00 s, 2.02 GiB, 176.7 k iops, 690.1 MiB/s
min/avg/max/mdev = 3.44 us / 4.70 us / 69.0 us / 1.39 us

可以看到系统根分区在 zram 里,经过压缩因此 IO 带宽受到了 CPU 处理性能的限制。但是 IOPS 依然高得爆表,对比一下 Intel Optane 900P 的 IOPS 性能:

1
2
3
4
# ioping -RD /
--- / (ext4 /dev/nvme0n1p1) ioping statistics ---
163.1 k requests completed in 3.00 s, 56.5 k iops, 220.8 MiB/s
min/avg/max/mdev = 11 us / 17 us / 114 us / 4 us

炒鸡厉害对不对!

不过需要做永久性修改的话还是要下面的方法之一

  • 重新挂载磁盘(虽然并不麻烦)然后手动修改配置文件
  • 重新挂载磁盘然后 rsync zram 到磁盘(方便但是可能会多一些不必要的东西)
  • 重启进入磁盘系统然后运行修改(需要物理接触)

硬件设置

进入系统 BIOS 设置,开启 Power on AC 或设置 Power Failure 后的操作,选择为 Power On (默认一般是 Last State)。

关闭系统、拔出电源,或意外断电后,再接入电源即可自动开机引导系统。因为数据本身就只在内存中,除了运行中的临时更改会丢失,系统和硬盘本体都是安然无恙的。

再也不担心办公室断电检查啦。

大概就是这样。

一直把自己在办公室的 PC 保持开机用于连回办公区、存取数据工作需求。由于最近办公室所在的写字楼要全馆断电检点,所以诞生了构建一个 Service Router 的想法。

September 14, 2020 04:20 AM

通过 SSH 修正安装有 GPU 的 HPE Proliant 服务器

由于越来越多的渲染、压制等需求,托供货商的关系搞来一台带有独立显卡的 HPE 服务器。经过几番折腾(包括特别奇怪的 LS26-C14 电源线)麻烦了帮忙托管的数据中心的大兄弟好几回,终于算是上架可以开机了。

登入 iLO,安装许可证,启动 iLO Remote Console,打开电源,一切都很顺利。但是 Console 里显示 Early Initialization… 完成后,突然画面一黑,完全没了动静。

以为 iLO 出了 bug,冷重启好几次都是一样的结果。百思不得其解。

再重启一次。仔细观察了一番发现虽然没了画面,但是 POST Code 还是不断变化的,而且 Virtual Media 指示灯不断在闪烁,说明系统仍在正常运行,只是没有视频输出而已。

因此问题定位在视频输出而非系统硬件。既然这台服务器装了显卡,那么很可能是 PCI-e 初始化后视频输出全部交给显卡处理了。搜索了一下 HPE Community,确实有这样的情况存在。解决方案是通过 BIOS 修改显卡设置为默认集成显卡、备选独立显卡。

尝试在设备初始化阶段进入 BIOS,失败。

联系数据中心远程操作的话,可能要等一段时间。

纠结时随便点开 iLO 的管理页面,突然发现了华点:这货居然支持 SSH。

对啦,HPE 的底层系统几乎都是魔改版 Linux,连他们的 SmartArray 都是 Linux 启动一个 Firefox 浏览器来操作的(X

于是正好在网上搜到一篇通过 SSH 修改 BIOS 视频设置的方法。记录如下。

SSH 进入 iLO

确保 SSH 在 iLO 管理页面中已开启,然后使用 SSH 客户端正常连接:

1
ssh Administrator@10.6.254.121

(ssh 用户名是 Administrator 感觉各种违和)

连接到 Virtual Serial Port

命令很简单:vsp

在 iLO 管理页面重启系统,然后等待初始化完成。如果看到按下 F9 进入 BIOS 设置的提示,不要按下它否则会进入 GUI 模式(于是又去独立显卡了就。

看到 ESC + 9 进入 BIOS Setup Utility 时按下键组合,稍等一会儿应该就可以看到提示符 rbsu>

修改视频设置

命令 SHOW CONFIG VIDEO OPTIONS

显示如下

1
2
3
1|Optional Video Primary, Embedded Video Disabled <=
2|Optional Video Primary, Embedded Video Secondary
3|Embedded Video Primary, Optional Video Secondary

即默认关闭了集成显卡,只用独立显卡(不觉得很蠢吗!

于是修改为第三项,默认使用集成显卡,独立显卡作为备用。

1
SET CONFIG VIDEO OPTIONS 3
1
2
3
1|Optional Video Primary, Embedded Video Disabled
2|Optional Video Primary, Embedded Video Secondary
3|Embedded Video Primary, Optional Video Secondary <=

然后敲 EXIT 退出并重启系统。

安装系统和驱动

至此即可通过 iLO Advanced Console 正常安装操作系统。不过需要注意的是进入操作系统后即便安装了对应的显卡驱动,依然默认使用的是集成显卡。以及 RDP 只能使用软解,无法使用独立显卡加速视频输出。这不影响 Blender 或者 Cinema 4D 等直接操作显卡进行计算的程序,但是会影响直接输出视频到桌面的程序。通过 Teamviewer 则可以强制桌面运行在独立显卡上。

顺便吐槽:Blender 把我的工程材质弄丢了…

由于越来越多的渲染、压制等需求,托供货商的关系搞来一台带有独立显卡的 HPE 服务器。经过几番折腾(包括特别奇怪的 LS26-C14 电源线)麻烦了帮忙托管的数据中心的大兄弟好几回,终于算是上架可以开机了。

登入 iLO,安装许可证,启动 iLO Remote Console,打开电源,一切都很顺利。但是 Console 里显示 Early Initialization… 完成后,突然画面一黑,完全没了动静。

September 14, 2020 04:20 AM

在线扩展 LVM root 分区

才不是没东西写了呢

遇到一个奇葩的原因导致 root 分区被占满的。而且还是奇葩的 CentOS,root 分区是 LVM,Hypervisor 里扩展磁盘后无法直接用 resize2fs。

既然如此就只能暴力重建分区咯。

重建分区

操作前确保操作的分区和之后新建时 Start 保持一致,修改分区表后不至于分区崩坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
~> fidks /dev/sda
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): p

Disk /dev/sda: 103.1 GB, 103079215104 bytes, 201326592 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a8e23

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 2099199 1048576 83 Linux
/dev/sda2 2099200 50331647 24116224 8e Linux LVM

Command (m for help): d
Partition number (1,2, default 2): 2
Partition 2 is deleted

Command (m for help): n
Partition type:
p primary (1 primary, 0 extended, 3 free)
e extended
Select (default p):
Using default response p
Partition number (2-4, default 2):
First sector (2099200-201326591, default 2099200):
Using default value 2099200
Last sector, +sectors or +size{K,M,G} (2099200-201326591, default 201326591):
Using default value 201326591
Partition 2 of type Linux and of size 95 GiB is set

Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: Re-reading the partition table failed with error 16: Device or resource busy.
The kernel still uses the old table. The new table will be used at
the next reboot or after you run partprobe(8) or kpartx(8)
Syncing disks.

~> partprobe

现在就可以看到 /dev/sda2 的大小已经变化了:

1
2
3
4
5
6
7
8
~> lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 96G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 95G 0 part
├─centos-root 253:0 0 20.6G 0 lvm /
└─centos-swap 253:1 0 2.4G 0 lvm [SWAP]
sr0 11:0 1 906M 0 rom

扩展 Volume Group

VG 的好处也就是能够灵活扩展分区大小…

1
2
3
~> pvresize /dev/sda2
Physical volume "/dev/sda2" changed
1 physical volume(s) resized / 0 physical volume(s) not resized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~> vgdisplay
--- Volume group ---
VG Name centos
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 4
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 2
Open LV 2
Max PV 0
Cur PV 1
Act PV 1
VG Size <95.00 GiB
PE Size 4.00 MiB
Total PE 24319
Alloc PE / Size 5887 / <23.00 GiB
Free PE / Size 18432 / 72.00 GiB
VG UUID TpbtuH-AjTZ-PU3v-UN31-FvfX-kSLv-xLiJG7

至此已经可以看到 Free PE 的部分有多出的 72GB 空间。

扩展 Logic Volume

1
2
3
4
5
6
7
8
9
10
11
12
13
~> lvextend -r -l +100%FREE /dev/centos/root
Size of logical volume centos/root changed from 20.59 GiB (5272 extents) to 92.59 GiB (23704 extents).
Logical volume centos/root successfully resized.
meta-data=/dev/mapper/centos-root isize=512 agcount=4, agsize=1349632 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=0 spinodes=0
data = bsize=4096 blocks=5398528, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0 ftype=1
log =internal bsize=4096 blocks=2636, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
data blocks changed from 5398528 to 24272896

确认效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~> lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 96G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 95G 0 part
├─centos-root 253:0 0 92.6G 0 lvm /
└─centos-swap 253:1 0 2.4G 0 lvm [SWAP]
sr0 11:0 1 906M 0 rom

~> df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 93G 21G 73G 23% /
devtmpfs 3.9G 0 3.9G 0% /dev
tmpfs 3.9G 8.0K 3.9G 1% /dev/shm
tmpfs 3.9G 8.6M 3.9G 1% /run
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda1 1014M 185M 830M 19% /boot
tmpfs 783M 0 783M 0% /run/user/0

搞定收工(‘・ω・’)

后话

其实虚拟机还用 LVM 的话,直接新增一块虚拟硬盘是最方便的方案。直接 vgextend 一路搞定…

才不是没东西写了呢

遇到一个奇葩的原因导致 root 分区被占满的。而且还是奇葩的 CentOS,root 分区是 LVM,Hypervisor 里扩展磁盘后无法直接用 resize2fs。

September 14, 2020 04:20 AM

在 Linux 服务器配置 LACP 与 VLAN

存储服务器不想放在 OVH 了。所以自己来托管一台机器,顺便折腾下 2x1Gbps 组 LACP Bonding。

前提:服务器需要至少 2 个千兆物理网卡,上联交换机支持 802.3ad。

配置交换机

这里使用的是 Cisco Nexus 3064PQ-10GE 交换机,我们的接口在 Eth1/21-22,port-channel 的配置如下:

1
2
3
4
5
6
7
8
9
# show interface trunk

--------------------------------------------------------------------------------
Port Native Status Port
Vlan Channel
--------------------------------------------------------------------------------
Eth1/21 1 trnk-bndl Po100
Eth1/22 1 trnk-bndl Po100
Po100 1 trunking --
1
2
3
4
5
6
7
8
9
10
show port-channel database
port-channel100
Last membership update is successful
2 ports in total, 2 ports up
First operational port is Ethernet1/21
Age of the port-channel is 0d:00h:02m:16s
Time since last bundle is 0d:00h:02m:04s
Last bundled member is Ethernet1/22
Ports: Ethernet1/21 [active ] [up] *
Ethernet1/22 [active ] [up]

配置服务器

服务器操作系统是 Arch Linux,由于蜜汁问题 netctl 无法启动网卡,就只好用 systemd-networkd 啦。

麻烦一些,但是也还算顺利。与往常一样,折腾服务器网络的时候需要备着 IPMI 以防 connection lost。

内核模块

需要加载 bonding 模块。将模块名写入列表,文件 /etc/modules-load.d/bonding.conf,内容只需要一行:

1
bonding

先别急着加载模块,为了防止模块自动建立一个默认网卡影响后续配置,以及设置 LACP Mode=4 … 等等,先加入一行参数。文件 /etc/modprobe.d/bonding.conf

1
options bonding mode=4 miimon=100 max_bonds=0

然后安装 ifenslave 包,再 modprobe bonding 即可。

bonding 虚拟网卡

首先创建一个虚拟网卡的设备。文件 /etc/systemd/network/bond0.netdev 内容为

1
2
3
4
5
6
7
8
9
[NetDev]
Name=bond0
Kind=bond

[Bond]
Mode=802.3ad
TransmitHashPolicy=layer2+3
LACPTransmitRate=fast
AdSelect=bandwidth

然后在此虚拟网卡上创建网络。这里使用两个物理网卡 eth0eth1 作为 bundle,交换机上的 VLAN id 是 113。文件 /etc/systemd/network/bond0.network 内容为

1
2
3
4
5
6
[Match]
Name=bond0

[Network]
VLAN=vlan113
BindCarrier=eth0 eth1

接下来分别为 eth0eth1 建立网络设置。

  • /etc/systemd/network/eth0.network
1
2
3
4
5
[Match]
Name=eth0

[Network]
Bond=bond0
  • /etc/systemd/network/eth1.network
1
2
3
4
5
[Match]
Name=eth1

[Network]
Bond=bond0

最后是 VLAN 的设置。前面设置了上联 VLAN id 是 113,这里分别建立 VLAN 的虚拟网卡(based on bond0) 并设置网络(IP, etc)。

  • /etc/systemd/network/vlan113.netdev
1
2
3
4
5
6
[NetDev]
Name=vlan113
Kind=vlan

[VLAN]
Id=113
  • /etc/systemd/network/vlan113.network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Match]
Name=vlan113

[Network]
VLAN=vlan113

[Address]
Address=10.1.0.100/24

[Route]
Destination=0.0.0.0/0
Gateway=10.1.0.1
DNS=1.1.1.1

[Address]
Address=2600:x:x:x::2/64

[Route]
Gateway=2600:x:x:x::1

多个地址、IPv6 等可以写多个 [Address][Route]

至此就完成啦。开启 systemd-networkd 的自启动:

1
systemctl enable systemd-networkd.service

然后重启网络:

1
systemctl restart systemd-networkd.service

如果配置都没有问题,网络会中断十几秒然后恢复。现在查看网卡列表已经可以看到组合的网卡了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
4: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
5: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
6: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
7: vlan113@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff

ethtool 查看 bond0 的速率显示 2000Mb/s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ethtool bond0
Settings for bond0:
Supported ports: [ ]
Supported link modes: Not reported
Supported pause frame use: No
Supports auto-negotiation: No
Supported FEC modes: Not reported
Advertised link modes: Not reported
Advertised pause frame use: No
Advertised auto-negotiation: No
Advertised FEC modes: Not reported
Speed: 2000Mb/s
Duplex: Full
Port: Other
PHYAD: 0
Transceiver: internal
Auto-negotiation: off
Link detected: yes

搞定收工(‘・ω・’)

Reference:

存储服务器不想放在 OVH 了。所以自己来托管一台机器,顺便折腾下 2x1Gbps 组 LACP Bonding。

September 14, 2020 04:20 AM

迁移 DokuWiki 到 BookStackApp

Wiki 这么反人类的语法是怎么存在这么久的???????

总之受不了 Wiki 语法的可维护性了。什么?这玩意儿有维护性???

以及万年不更新的各种插件。系统升级后 PHP 7 不兼容,一看还是 swiftmail 的问题。生气。

正好有需求要整合一套知识库平台,搜索了一下 Confluence 的 alternative,发现了 BookStackAppPhabricator

前者适合个人或开源社区使用,后者则是一整套企业协作解决方案。对于我的需求来讲,BookStackApp 就足够啦。

页面数据

DokuWiki 并不使用数据库,因此没有一个通用的中间件来实现数据格式转换。而 DokuWiki 的语法非常奇葩——比如,它的一级标题是 ====== 这样 ======,六级标题才是 = 这样 =,正好和一般的 Wikitext 倒置。图片、内链等的表达方式也相当愚蠢,这些问题使我在思考迁移方案的第一个小时内就放弃了直接从源码转移的途径。

顺便,还有另外一个问题——本来为了使 Wiki 易于编写,这 DokuWiki 还安装了 Markdown 插件。因此部分页面中混杂着 Markdown 语法,更增加了源码处理的复杂度。

综合来看,最通用的数据格式,就是最终渲染出来的 XHTML 了。

图片

DokuWiki 的图片存储策略也是非常的奇特。由于它没有数据库,因此为了保持图片与页面的对应,它将图片存储在每个页面同样的路径下,并通过执行 PHP 的方式获取(扶额。

更甚者!!!

外链的图片,也是通过 /lib/exe/fetch.php 带参数来获取!!

我 的 天 哪。

因此既然在页面数据的考量中决定了使用最终渲染输出的 XHTML 来处理数据格式,图片也需要特殊的下载和归档技巧。这将需要使用 sanitize-html 提供的 transformer 方法来实现。

逻辑实现

一开始尝试了一些 Site Exporter 插件,但遗憾的是并没有什么真正能派上用场。甚至一些暴力递归下载所有页面和资源的脚本的表现也非常糟糕。

但是根据 DokuWiki 的官方 Tips,它可以将文章内容单纯导出 XHTML,只需要加上 ?do=export_xhtmlbody 参数即可。这就方便了,因为这样只需要一个完整的页面列表就可以了。随便找一个可以输出子命名空间的插件,新建一个页面用于从根命名空间展开就 OK 啦。

请求这个列表页面的 XHTML body 输出,使用 cheerio 遍历所有的 a 标签,就获得了所有要导出的页面地址。分别再去请求这些页面的 XHTML body 输出,做如下处理:

  1. 跟踪所有的 img 标签,下载图片文件并按预定义的路径规则和文件名归档。
  2. sanitize-html 清除所有不必要的标签、样式、id 和 class。
  3. sanitize-html 按预定义的路径规则更新所有 aimg 标签属性。

看代码

后来发现 DokuWiki 的性能不足以支撑异步请求的速度,额外加上了 sleep 模块来控制请求频率(扶额。

脚本执行完后,将图片目录移动到 BookStackApp 的对应位置,便可以直接读取所有的 HTML 文件来导入数据啦。

用了这么久,才发现原来还有比 raw HTML 更难以维护的数据格式啊…(望天。

Wiki 这么反人类的语法是怎么存在这么久的???????

September 14, 2020 04:20 AM

制作 Arch Linux 系统模板镜像

阿里云镜像制作踩坑记。

此文章主要记录按照阿里云 Customized Linux 制作 VPC 镜像的过程。一些部分也可用作制作其他平台镜像的参考。

当然记录的原因主要是 Arch 上的 cloud-init 打死无法在阿里云上修改 root 密码,就很气。

建立虚拟机

因为要制作 Customized Linux,所以第一步无法在阿里云平台上使用公共镜像制作。本机启动一个 Virtual Box,新建虚拟机,虚拟磁盘选择 RAW/IMG 格式即可。

按照一般步骤安装 Arch Linux,需要整个磁盘仅有一个分区。虽然很多平台支持多分区的镜像文件,但是莫名在这里踩了坑所以。

(另外吐槽:vps2arch 居然不帮我把 base-devel 装全了?!)

系统配置

安装一些必需的包。

1
# pacman -S qemu-guest-ga openssh

启用服务。

1
2
3
# systemctl enable qemu-ga
# systemctl enable sshd
# systemctl enable systemd-networkd

网络配置

哪个魂淡跟我讲 VPC 是 DHCP?装着 cloud-init 的 Arch 就可以自动设置内网 IP,这个没装的就 GG。

修改文件 /etc/systemd/network/default.network

1
2
3
4
5
[Match]
Name=en*

[Network]
DHCP=ipv4

总之先这样放着。

定制脚本

根据阿里云的文档,cloud init 不生效的时候需要用约定好的配置文件和脚本完成各种兼容动作。

新建目录 /aliyun_custom_image

新建文件 /usr/bin/aliyun-custom-os,写入内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/bin/bash

os_conf_dir=/aliyun_custom_image
os_conf_file=${os_conf_dir}/os.conf

load_os_conf() {
if [[ -f $os_conf_file ]]; then
. $os_conf_file
echo $password
return 0
else
return 1
fi
}

cleanup() {
# ensure $os_conf_file is deleted, to avoid repeating config system
rm $os_conf_file >& /dev/null
# ensure $os_conf_dir is exitst
mkdir -p $os_conf_dir
}

config_password() {
if [[ -n $password ]]; then
password=$(echo $password | base64 -d)
if [[ $? == 0 && -n $password ]]; then
echo "root:$password"
echo "root:$password" | chpasswd
fi
fi
}
config_hostname() {
if [[ -n $hostname ]]; then
echo "$hostname" > /etc/hostname
hostnamectl set-hostname $hostname
fi
}
config_network() {
if [[ -n $eth0_ip_addr ]]; then
config_interface
systemctl restart systemd-networkd
fi
}
config_interface() {
mask2cdr $eth0_netmask
cat << EOF > /etc/systemd/network/default.network
# Generated by Aliyun Custom OS helper
# DO NOT EDIT THIS FILE! IT WILL BE OVERWRITTEN

[Match]
Name=$(ip link | awk -F: '$0 !~ "lo|vir|wl|^[^0-9]"{print $2a;getline}' | sed -e 's/^[[:space:]]*//')

[Network]
Address=$eth0_ip_addr/$netmask
Gateway=$eth0_gateway

[Link]
MACAddress=$eth0_mac_address

[Address]
Address=$eth0_ip_addr/$netmask
EOF
echo "nameserver 1.1.1.1" > /etc/resolv.conf
for ns in $dns_nameserver
do
echo "nameserver $ns" >> /etc/resolv.conf
done
}

mask2cdr() {
# Assumes there's no "255." after a non-255 byte in the mask
local x=${1##*255.}
set -- 0^^^128^192^224^240^248^252^254^ $(( (${#1} - ${#x})*2 )) ${x%%.*}
x=${1%%$3*}
netmask=$(( $2 + (${#x}/4) ))
}

if load_os_conf ; then
config_password
config_hostname
config_network
cleanup
else
echo "not load $os_conf_file"
fi

赋予执行权限

1
# chmod +x /usr/bin/aliyun-custom-os

新建 systemd unit 文件 /usr/lib/systemd/system/aliyun-custom-os.service 写入内容

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Aliyun Custom OS Helper Script

[Service]
Type=oneshot
ExecStart=/usr/bin/aliyun-custom-os
TimeoutSec=30
StandardInput=tty
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

然后启用这个服务

1
systemctl enable aliyun-custom-os

挂载镜像

正常 shutdown 虚拟机,然后拿到镜像文件的路径。例如 ~/vm/archlinux.img

接下来需要将此镜像挂载到宿主机系统中修改、清理文件。首先确定镜像文件中的分区位置:

1
2
$ file ~/vm/archlinux.img
archlinux.img: x86 boot sector; partition 1: ID=0x83, active, starthead 32, startsector 2048, 41938944 sectors, code offset 0x63

得知 startsector2048

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ fdisk -l ~/vm/archlinux.img
You must set cylinders.
You can do this from the extra functions menu.

Disk archlinux.img: 0 MB, 0 bytes
255 heads, 63 sectors/track, 0 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x91d8e293

Device Boot Start End Blocks Id System
archlinux.img1 * 1 2611 20969472 83 Linux
Partition 1 has different physical/logical endings:
phys=(1023, 254, 63) logical=(2610, 180, 2)

得知 sectorsize512

使用 mount 命令带 offset 参数挂载镜像中的分区:

1
2
$ sudo mkdir -p /mnt/img
$ sudo mount -t ext4 -o loop,offset=$((2048*512)) /path/to/archlinux.img /mnt/img/ # 更改 -t auto 或者其他此分区使用的文件系统格式

就可以 cd /mnt/img 看到镜像里的 rootfs 啦。

清理/检查文件

要删除的:

1
2
3
4
5
# rm /root/.bash_history # _(:з」∠)_
# rm /etc/ssh/ssh_host_* # 强制每次部署的时候重新生成密钥对
# rm -r /var/log/* # 清理不需要的日志
# rm -r /var/cache/* # 清理缓存
# rm /etc/resolv.conf.bak # 避免恢复成制作时的 DNS

要检查的:

/etc/hosts - 我不知道为什么,第一次的时候把这个文件留空了(:з」∠)

/etc/resolv.conf - 鉴于总是有人喜欢手动修改这个文件,所以直接把它写成静态文件好了。内容例如

1
2
nameserver 8.8.8.8
nameserver 8.8.4.4

/etc/ssh/sshd_config 中是否允许 root 密码登陆。

准备镜像

退出 /mnt/img 目录,然后卸载镜像

1
# umount /mnt/img

(可选)使用 qemu-img 转换镜像格式到 VHD,减少镜像文件大小。特别是对国内的小水管上传(心疼

1
$ qemu-img convert -f raw -O vpc archlinux.img archlinux.vhd

上传镜像

在相同的 region 创建一个 OSS bucket,然后创建一个 RAM 子用户赋予 OSS 写权限并创建 Access Key,使用 OSSBrowser 上传准备好的 VHD 文件。

上传完毕后,在 ECS 标签下的镜像标签即可导入镜像。如果是第一次操作,需要给 ECS 授权访问 OSS。在导入的页面提示中提供了授权的链接。镜像内容配置如下:

  • OSS Object 地址:镜像文件在 OSS 中的 URL
  • Image 名称:archlinux-2018.1-x86_64 … 等符合要求即可
  • 操作系统:Linux
  • 系统盘大小:40GB
  • 系统架构:x86_64
  • 系统平台:Customized Linux
  • 镜像格式:VHD(如果是 img 就选 RAW)
  • 镜像描述:随便写啦。

确定后应该就会开始制作镜像了。

测试

因为没有做经典实例的兼容,这个镜像只能用于 VPC 的实例。总体而言,cloud-init 本来兼容的 Arch 却无法更改 root 密码(其他的倒是没问题),所以才选择了用一个 dirty 的方案来实现。

不知道应该说阿里云的工程师对自定义镜像的考虑周到还是对不同发行版的考虑欠妥…?

最后庆幸倒腾来去上传了好多遍 20G 的文件,日本运营商家宽带宽对等真的是帮了大忙,不然一个镜像制作不知道要到什么时候 > > (斜眼看国内三大运营商

参考:

阿里云镜像制作踩坑记。

此文章主要记录按照阿里云 Customized Linux 制作 VPC 镜像的过程。一些部分也可用作制作其他平台镜像的参考。

当然记录的原因主要是 Arch 上的 cloud-init 打死无法在阿里云上修改 root 密码,就很气。

September 14, 2020 04:20 AM

自托管的在线协作翻译平台 Weblate

起因:Transifex 这货闭源一段时间后突然开始抢钱了。

正巧一堆开源项目需要一个在线协作的翻译平台,于是测试了几个比较知名的开源程序。一遍折腾下来,发现 Weblate 可以最大化满足要求。顺便提一句,Weblate 也是有 hosted 付费服务的,但是在预算内的源字符串等限制依旧太多,所以选择使用他们的源码来搭建一套。

以及:我讨厌 Docker。

Weblate 文档 提供了非常全面的从起步到上手到各种高级用法的指南,因此这里不多赘述安装的过程。只记录少许踩过的坑。

这套程序看似简单,但实际上是基于 Django、使用了一大堆组件的复杂程序。如果想保持 system clean,最好(最快)的办法还是使用 Docker。

准备

小型实例只需要一台虚拟机即可。但是即便只托管几个项目,它依旧会吃掉 2 个 CPU 核心和 4GB 内存,和曾经开源版的 Transifex 有得一拼 大概也解释了为何这类服务都死贵

如果是托管在公网上的实例,则推荐使用 HTTPS。Weblate 的 Docker compose 提供了 HTTPS 支持,稍后会提到。

安装 Git, Docker 和 docker compose,在一些软件仓库中一般是 docker-cedocker-compose,其他软件均不需要手动安装。

搭建

首先克隆 docker compose 配置到本地

1
2
git clone https://github.com/WeblateOrg/docker.git weblate-docker
cd weblate-docker

为了直接开始使用 HTTPS,现在需要先建立域名解析记录,将要使用的域名(例如 weblate.example.com)指向服务器 IP。然后在该目录下创建配置文件 docker-compose-https.override.yml 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '2'
services:
weblate:
environment:
- WEBLATE_DEBUG=0
- WEBLATE_EMAIL_HOST=smtp.gmail.com
- WEBLATE_EMAIL_HOST_USER=noreply@weblate.org
- WEBLATE_EMAIL_HOST_PASSWORD=system.email.password.here
- WEBLATE_ALLOWED_HOSTS=weblate.example.com
- WEBLATE_SERVER_EMAIL=noreply@weblate.org
- DEFAULT_FROM_EMAIL=noreply@weblate.org
- WEBLATE_REGISTRATION_OPEN=0
- WEBLATE_TIME_ZONE=UTC
- WEBLATE_OFFLOAD_INDEXING=1
- WEBLATE_REQUIRE_LOGIN=1
- WEBLATE_ADMIN_NAME=Weblate Admin
- WEBLATE_ADMIN_EMAIL=admin@weblate.org
- WEBLATE_ADMIN_PASSWORD=your+initial+password
https-portal:
environment:
DOMAINS: 'weblate.example.com -> http://weblate'

这份配置文件指定了:

  • 关闭 Django DEBUG mode (即生产模式)
  • 系统外发邮件服务器 smtp.gmail.com
  • 系统外发邮件用户名 noreply@weblate.org
  • 系统外发邮件密码 system.email.password.here
  • 允许使用的域名 weblate.example.com,如果有多个域名,使用逗号隔开
  • 系统外发邮件地址 noreply@weblate.org
  • 关闭注册通道,用户必须管理员手动添加
  • 设置系统时间为 UTC
  • 打开后台索引,降低运行时的负载
  • 执行任何操作前要求登陆
  • 默认管理员名称是 Weblate Admin
  • 默认管理员邮箱地址是 admin@weblate.org
  • 默认管理员密码是 your+initial+password

然后在 https-portal 容器中指定了要使用 SSL 的域名 weblate.example.com 和后端指向的容器 http://weblate

在当前目录中执行

1
docker-compose -f docker-compose-https.yml -f docker-compose-https.override.yml up

会顺序拉取、启动 4 个 docker 容器,分别是:

  • https-portal
  • weblate
  • postgresql
  • memcached

第一次启动需要一些时间拉取镜像并导入初始数据。全部完成后,访问 weblate.example.com 应该可以看到一个 HTTPS 的 Weblate 实例运行,使用之前定义的默认管理员邮箱地址和密码即可登入。

此时转回终端,按一次 ^C 等待四个容器正确关闭,然后编辑 docker-compose-https.override.yml,删除以下配置

  • WEBLATE_ADMIN_NAME
  • WEBLATE_ADMIN_EMAIL
  • WEBLATE_ADMIN_PASSWORD

否则,如果更改了默认管理员的信息(如用户名等)下次启动会再次创建管理员帐号,并使用相同的邮箱,导致默认管理员无法使用邮箱登陆(报错返回 2 个用户信息)。解决办法是使用用户名…(摔

集成配置

再次运行 docker-compose -f docker-compose-https.yml -f docker-compose-https.override.yml up 后可以很快启动所有需要的程序。此时登入 Weblate 实例,指向 /admin/ssh/ 点击创建 SSH 密钥。

在对单一 repo 提交的情况下,此 SSH Key 可作为 GitHub deploy key,但是如果需要多个不同 repo 提交时,有两种方法:

  • 创建一个 GitHub 用户,然后将此 SSH Key 添加到此用户下,再给此用户所有必要的写权限
  • 使用 Access Token 作为 HTTPS 密码访问必要的 repo

浏览器指向 /admin/trans/project/ 新建一个 Project。这个 Project 不仅指一个项目,也可以作为一个 Organization 的存在,更精确的解释是一个软件集,例如 KDE 套件可以包含一大堆的组件。

Weblate Project

指向 /admin/trans/subproject/ 这里才是可以添加要翻译的项目的地方。如果对应的 repo 添加了公钥,这里可以直接使用 SSH 方式的 push URL。

Weblate Import strings

File mask 填写所有语言文件相对 repo root 的路径,使用 * 代替语言代号。如果是 Monolingual language file,例如 key 是 user.info.comment_posted 这样而非原本即可阅读的文本,则 Monolingual base language fileBase file for new translations 均为源语言文件相对 repo root 的路径,这样即可正确识别源语言的字符串不至于让别人拿着 comment_posted 这样的 key 来猜意思

Weblate Translation Interface

持续集成

提交翻译后,Weblate 会在后台完成索引并提交必要的更改。当然也会一不小心刷了别人的屏…

Weblate continous translation

在 repo 的 settings -> integration 中可以添加 Weblate 作为集成,每次有新的提交即可触发 Weblate 更新源语言文件。

结论

我很开心可以省下每年数千美元来用一个非常卡的在线协作翻译平台

当然,我依旧讨厌 Docker。

起因:Transifex 这货闭源一段时间后突然开始抢钱了。

正巧一堆开源项目需要一个在线协作的翻译平台,于是测试了几个比较知名的开源程序。一遍折腾下来,发现 Weblate 可以最大化满足要求。顺便提一句,Weblate 也是有 hosted 付费服务的,但是在预算内的源字符串等限制依旧太多,所以选择使用他们的源码来搭建一套。

以及:我讨厌 Docker。

September 14, 2020 04:20 AM

使用 Blender 渲染 Minecraft 3D 效果图

突发奇想渲染 Minecraft 3D 效果图,首先用 Chunky 尝试了一下发现效果虽好但:

  • 人物动作过于限制
  • 渲染太!慢!了!

然而并买不起 Cinema 4D,所以来尝试一下 Blender 啦~

准备工作

  • 比较好的显卡。我的家用游戏机是 NVIDIA GTX 1080Ti,Cycles Render 可以 offload 掉绝大部分 CPU 的压力。
  • 安装 Blender
  • 下载 jmc2obj
  • 下载 MCPrep
  • 准备地图、材质、玩家皮肤等资源。

导出地图文件到 obj

启动 jmc2obj,在最上方选择地图存档位置并单击 load。在 UI 里选择要导出的地图部分,点击 Export。在左侧的选项中依次:

  1. Map Scale = 1.0
  2. Center 选中,否则可能会出现在距离地图很远的地方
  3. Texture Export
    • Pre-scale textures - 如果是原版材质,建议设置为 4x。如果是高清材质,按需要选择即可。
    • 不勾选 Export alpha channel in separate file(s)
    • 不勾选 Export all textures in a single file
    • 选择从 Minecraft 安装里导出默认材质,或自行选择一个额外的材质包。
    • 然后选择材质的导出位置。建议在目标目录中新建一个 textures 目录,然后导出到此目录中。此目录里面会出现一个 tex 目录,包含所有的材质文件。

材质导出进度完成后,开始导出地图文件。在右侧的选项中依次:

  1. 取消所有的选项勾选
  2. 勾选 Create a separate object for each material
  3. 可选勾选:
    • Render Entities
    • Occulude different adjacent materials
    • Optimize mesh
    • Do not allow duplicate vertexes
  4. 其余选项均保持非勾选状态
  5. 点击 Export 导出到之前创建的 textures同级目录
  6. 可能会遇到 banner 找不到材质的问题,忽略继续。导出后还不能使用,需要一个简单而 ugly 的 hack - 再导出一遍覆盖之前的 obj 和 mtl 文件。

导出后的工作目录如下:

Blender Working Directory

.obj 文件是地图数据,.mtl 则是刚才导出材质的材质索引,指向 textures/tex 的相对路径,因此这些文件的相对位置不能改变。

安装 MCPrep

启动 Blender,在 File -> User Preferences -> Add-ons 里,选择 Install Add-on from File...

然后点击下载好的 MCPrep 的 .zip 文件即可。

导入 blender

启动 Blender,先删掉默认的 object 和 lamp,然后依次选择 File -> Import -> Wavefront (.obj)

选择刚才生成的 .obj 文件导入。

(大概会卡一会儿… 喝杯茶先)

导入完成后,首先需要设置材质。选中所有 object (快捷键 A,如果被反选则再摁一次即可),左侧标签页切换到 MCPrep,然后点击 Prep Materials。

Prep Materials

进入 Walk Navigation 模式(或者其他熟悉的移动模式),Add -> lamp -> Sun 然后将添加的 Sun 光源移动到地图的合适位置(比如一个边角)。

Walk Navigation

然后移动 Viewport 到合适的位置,将默认的 Camera 对齐到当前视角。快捷键 Ctrl+Alt+0

Align Camera to View

设置背景

在 Blender 的上方菜单栏中,有 Blender Render 和 Cycles Render 两个选项。

如果使用 Blender Render,则需要在 Sun 的属性里设置 Sky & Atmosphere 并设置 Ray Shadow 以使光源和阴影正确对应。

如果使用 Cycles Render,则在 World 属性中设置 background 为 Sky Texture。

Sky texture

当然也可以使用自己的天空图像或其他材质。

渲染

在渲染属性中设置使用 CPU 或者 GPU 渲染图像,并可以设置分辨率等。

Render Properties

修改好设置,在 Blender 上方菜单栏中点击 Render -> Render Image 即可开始渲染啦~

玩家和实体

我相信熟练使用 C4D/Maya/Blender 的玩家们不需要看这篇教程… 所以玩家实体的高级用法不多讲。

需要的材料是一份玩家或者其他实体的 Rig,在 PlanetMinecraft 或者 MinecraftForum 上有很多。贴上皮肤就可以用啦。

在 player rig 中调整好 pose,保存。在要渲染的世界工程里,选择 File -> Append,然后选择刚才的玩家 pose 的 blender 文件,进入后选择 Scene 并 append。

选择 Scene 的原因是有不少 rig 并不是一个单一 object,如果是单一 object,则可以直接导入 object。

在右上方的 object 列表中选择刚才 append 的 scene,定位玩家实体并全部选中 -> Ctrl+C 复制 -> 返回渲染世界 -> Ctrl+V 粘贴。

选择粘贴进来的玩家实体,然后调整到合适的位置。

成果

这大概是我第一次玩 blender。

总之以下是成果啦。

比如某玩家的 pose 效果图:

leeder's pose

比 chunky 的动作自然多了吧~

最后是完成的效果:

Project xport

Happy Rendering~

突发奇想渲染 Minecraft 3D 效果图,首先用 Chunky 尝试了一下发现效果虽好但:

  • 人物动作过于限制
  • 渲染太!慢!了!

然而并买不起 Cinema 4D,所以来尝试一下 Blender 啦~

September 14, 2020 04:20 AM

NetFLOW / sFLOW 流量报告:FastNetMon + InfluxDB + Grafana

最近稍微有点时间折腾了下 Cisco 的三层交换,尝试搭建了一套数据中心用的流量统计/监控/报告系统。过程不是很复杂,但是也只算利用了一套高级软件组合的一点点功能。之后打算继续研究更多的功能实现,不过也要看有没有时间了…

准备工作

首先确认出口路由设备支持 netflow/sflow 的对应版本。一般 Cisco 的路由器或者三层交换都是支持的。

然后准备一个常见的 Linux 系统,虚拟机或者物理机都可以。

出口路由设备能够连通到该 Linux 系统,并且 flow collector 设置到该 Linux 系统的 IP 地址和对应端口。

FastNetMon

安装 fastnetmon,只需要一条简单的脚本命令。

然后将所有要监控的网段加入 /etc/networks_list。一行一个,例如:

1
2
3
10.1.0.0/16
192.168.254.0/24
8.8.0.0/16

按照安装文档打开两个终端,分别启动主进程和客户端

1
/opt/fastnetmon/fastnetmon
1
/opt/fastnetmon/fastnetmon_client

如果没有问题,应该在客户端上可以看到收到的 flow 数据。

先关闭 fastnetmon 进程,修改配置文件打开 Graphite 支持:

1
2
3
4
graphite = on
graphite_host = 127.0.0.1
graphite_port = 2003
graphite_prefix = fastnetmon

=== 2018-07-26 更新 ===

如果有比较新的发行版(内核 >= 3.6)可以开启 AF_PACKET,安装并启动 irqbalance 来获得更好的抓包性能。

InfluxDB

安装 InfluxDB,官方提供了各种包管理器的安装方式。

配置文件一般位于 /etc/influxdb/influxdb.conf,需要根据环境做安全相关设置(侦听地址、端口、鉴权、etc)并打开 Graphite Simulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[graphite]]
enabled = true
bind-address = "127.0.0.1:2003"
database = "flow_dc1"
protocol = "tcp"
consistency-level = "one"
name-separator = "."

# batch-size / batch-timeout requires InfluxDB >= 0.9.3
batch-size = 5000 # will flush if this many points get buffered
batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit

templates = [
"fastnetmon.hosts.* app.measurement.cidr.direction.function.resource",
"fastnetmon.networks.* app.measurement.cidr.direction.resource",
"fastnetmon.total.* app.measurement.direction.resource"
]

顺序重启 InfluxDB 和 fastnetmon。检查 flow 数据是否记录到 InfluxDB:

1
2
3
4
5
6
7
8
9
10
11
$ influx
Connected to http://localhost:8086 version 1.2.4
InfluxDB shell version: 1.2.4
> use flow_dc1
Using database flow_dc1
> select mean(value) from networks where direction = 'incoming' and resource = 'bps' group by *
name: networks
tags: app=fastnetmon, cidr=10_1_0_0_24, direction=incoming, resource=bps
time mean
---- ----
0 4735.049632696411

Grafana

Grafana 是一款非常强大且易用的数据可视化工具。安装 Grafana 然后修改配置文件的必要部分,配置文件一般位于 /etc/grafana/grafana.ini

完成后重启 Grafana,将浏览器指向 Grafana 的 HTTP 服务器地址即可看到登录界面。如果内部使用的话,建议关闭匿名访问和注册功能。

使用默认的 admin / admin 登录,按照引导完成配置、添加数据源(Data source),数据源即是 InfluxDB 的 HTTP API 地址。如果 Grafana 中限制了数据源白名单,需要将 InfluxDB 的 HTTP API 地址和端口加到白名单里。

添加面板、Graph,在 Graph 编辑模式里写入类似这样的查询语句:

1
SELECT mean("value") FROM "networks" WHERE "direction" = 'incoming' AND "resource" = 'bps' AND "cidr" =~ /^10_1_0_0_16/ AND $timeFilter GROUP BY time($interval) fill(previous)

即可看到有图表出现。根据需求完善查询语句和图表配置即可简单实现各种可视化效果。例如流量和数据包的实时报告:

Traffic Graph

PPS Graph

总结

通过配合 FastNetMon,InfluxDB 和 Grafana 即可快速实现一套基于 NetFLOW / sFLOW 的流量统计报告系统。但是 FastNetMon 的功能远不止流量统计,Grafana 也有大量插件和灵活的用法可以满足更多需求。如果配置合理,此方案也可适用于 40Gbps+ 接入的中型数据中心且成本低廉。以及——

  1. InfluxDB 真的很快!
  2. Grafana 的图表真的很省资源!
  3. Chronograph 卡死了我的浏览器!(i7-7700K / Chrome)

以及一大早手工修好了 K812 的耳机线,省掉了 2 万日元的线材费用非常开心

最近稍微有点时间折腾了下 Cisco 的三层交换,尝试搭建了一套数据中心用的流量统计/监控/报告系统。过程不是很复杂,但是也只算利用了一套高级软件组合的一点点功能。之后打算继续研究更多的功能实现,不过也要看有没有时间了…

September 14, 2020 04:20 AM

不作死就不会死系列,TFTP 修复变砖的 Nighthawk X6

由于之前买的 AC87U 经常被 roommate 抱怨掉线(风评表示这货 5G 有问题,然而我连着 5G 毛事儿没有,隔壁用 2.4G 却一直掉线)…

新购入的路由器是 Netgear Nighthawk X6 R8000。

由于之前的 Security Advisory,所以到手第一件事就是配上网络更新固件啦。更新挺慢的于是点点点完事儿撸猫去了。过了一会儿回来一看怎么还没网络?得,砖了…

讲道理,Netgear 也算大厂了,这种 online update 干了不知道多少回,第一次遇到这都能变砖的(扶额。

现象就是电源橙色灯亮后一会儿变成白色灯闪烁,且网络服务没有启动。尝试过 factory reset 无效,官方提供的 TFTP 强刷工具也无效(刷不进…

解决方案反而是意想不到的简单。总之大概记录下修复的过程。

  1. 官方网站下载适用的新版固件并解压,应该得到一个 .chk 文件
  2. 关闭路由器电源等待 10 秒,网线插 LAN 口开机。
  3. 检查是否获得了正确的 IP。如果没有,可能 DHCP 服务没起来。手动设置一个正确的 IP 吧。然后能 ping 通路由器 IP 即可。
  4. 电源灯开始闪烁的时候,执行命令 tftp -i [router ip] put [path/to/firmware.chk]。例如 tftp -i 192.168.1.1 put ./R8000-V1.0.3.36_1.1.25.chk
  5. 等一会儿路由器自动重启,搞定。

配置都没丢…然后我依旧没有搞定 OCN 要怎么连 IPv6… 说好的 IPv6 PPPoE 呢…

心累.png

由于之前买的 AC87U 经常被 roommate 抱怨掉线(风评表示这货 5G 有问题,然而我连着 5G 毛事儿没有,隔壁用 2.4G 却一直掉线)…

新购入的路由器是 Netgear Nighthawk X6 R8000。

由于之前的 Security Advisory,所以到手第一件事就是配上网络更新固件啦。更新挺慢的于是点点点完事儿撸猫去了。过了一会儿回来一看怎么还没网络?得,砖了…

September 14, 2020 04:20 AM

Minecraft 服务器资源控制策略:AI 抑制而非数量限制

Minecraft 的 lag 问题已经司空见惯,各种控制资源消耗和卡顿的插件也层出不穷。但是它们几乎都非常用力地在一个点上:控制实体数量。

这并不无道理,因为 Minecraft 中最消耗资源的部分就是实体。但是暴力控制实体数量会导致刷怪塔无法正常工作、掉落物清理速度过快等问题,在生存服务器中可能引发玩家的强烈不满。

所以,喵窝开发组从另一个角度做出了一些尝试。

启发

生物实体的数量巨大,主要集中的地区显然不是野外的自然刷怪区,而是玩家聚集的刷怪场、村民工程、动物养殖场等。如果不限制生物的数量和密度同时降低资源消耗,那么只能从生物实体的特性入手了。

Minecraft 最近的版本中引用了 NoAI 的 NBT Tag,带有此标签的生物将不会进行 AI 计算。换句话说,除了占用服务器内存中的一点数据,几乎不会对这个生物实体有任何其他的 CPU 算力消耗。

也就是说,实体消耗的算力资源,绝大部分都是 AI 计算的消耗。

方案

抓上一票人做了一些测试,结果证实生物失去 AI 后大幅降低了 CPU 的算力消耗。这是个 positive 的信号,但是接下来的测试则遇到了问题。

对于养殖场,等生物数量变化不大(或者说只是定期来清理并重新养殖一次)的设施,生物失去 AI 的影响很小,只有在重新繁殖时需要恢复 AI。但是刷怪塔则因为生物没有 AI,同时也被强制不受重力影响而几乎无法使用,即便同时设置 NoGravityfalse 也无效。

开发组中 @Librazy 提到了 Spigot 的一个参数 nerf-spawner-mobs,开启时刷怪笼生成的生物将不会拥有 AI,但是会被外界影响(例如水流和火球等)而移动。这个选项是全局的,因此不需要开启,只需要反射 spigot 中设置该功能的方法即可。

于是整个方案的流程便是当服务器卡顿时抑制生物密集区的生物 AI 从而降低资源占用,同时最大程度上保证玩家对生物的需求。「服务器卡顿」的考量以服务器 TPS 而非实体数量为准,当服务器 TPS 高于一定值时即认为服务器没有超负荷,不会有任何操作,最大程度上利用硬件的性能。

实现

插件主要由开发组的 @Cylin@Librazy 编写,源代码以 MIT 协议发布在 GitHub 上。

插件每隔一段时间扫描服务器的 TPS 确认运行状况,如果 TPS 低于阈值则触发 AI 控制,TPS 高于一定值且持续一段时间即认为服务器已恢复正常运行状态,自动恢复被抑制的实体 AI 减少对生存体验的影响。

实现过程中额外添加了一些额外可能被生存服务器用到的功能:

  • per-world 控制,如果玩家需要建造以仇恨为基础的小黑塔,可以关闭对末地的控制。
  • 实体总量和单区块实体密度在 AI 抑制时纳入考虑,更加精准抑制资源消耗较高的区块。

测试

yasui 插件在 毛玉線圈物語 服务器中应用测试。由于近期玩家数量爆炸式增长(日常在线 5 人到 ~30 人甚至 50 人),各种实体控制插件均告无效。yasui 插件应用后被证实数次发挥作用,没有任何实体数量限制的前提下将服务器 TPS 稳定在 19 以上,服务器实体承载数量从 ~2500 提到至接近 5000,并且还有继续提高的可能(数次触发中最高一次单世界实体记录是 4808,其他世界中仍有大约 2000 实体未被计入)。

吐槽:你们贼能刷

Minecraft 的 lag 问题已经司空见惯,各种控制资源消耗和卡顿的插件也层出不穷。但是它们几乎都非常用力地在一个点上:控制实体数量。

这并不无道理,因为 Minecraft 中最消耗资源的部分就是实体。但是暴力控制实体数量会导致刷怪塔无法正常工作、掉落物清理速度过快等问题,在生存服务器中可能引发玩家的强烈不满。

所以,喵窝开发组从另一个角度做出了一些尝试。

September 14, 2020 04:20 AM

新轮子:Planet.js

一开始的选择(需要登陆)中纠结使用 Planet 还是博客的时候就已经在关注 Planet 这个东西。

非常奇特,本身并不存储什么数据,但是用这样一个简单的方式在极低的成本下将大量的用户和内容结合到了一起。

终于决定要建立一个社区星球的时候,我才发现它连主题模板都不支持响应式才不是我想写个好看点的主题结果折腾一天无果(ノ=Д=)ノ┻━┻

总之,使用的是 Ubuntu 仓库内的 planet-venus 包,遇到了各种问题…

  • 莫名其妙无法获取 feed,浏览器访问正常,用 curl 正常
  • 莫名其妙无法读取 feed 内容,其他 feedparser 均正常
  • 对文章内使用相对链接的内容无能为力
  • 模板语言落后,不要跟我说 old fashion
  • 输出路径一直是 cwd,不知道是不是 feature。但是这导致我的 home 下面到处都是 output 目录

强迫症犯了。遂决定自己实现一个因为逻辑很简单啊不过就是把 RSS 抓下来排个序再丢出去么

但是到具体的细节,还是有不少需要考量的东西。

压缩和编码

有些网站打开了 gzip 压缩,有些网站使用了非 UTF-8 编码…只是暴力读取的结果就是页面上一半正常一半乱码。

好在 feedparser 给出了使用 zlibiconv代码样例,这个问题就迎刃而解啦。

相对路径和内容安全

Planet Venus 似乎是会解析处理文章内不带有协议的链接和图片,以使这些资源能够在 planet 的页面中直接通过原地址访问。但是问题在于,planet venus 似乎只是暴力给所有非完整 URL 形式的资源地址都强行加上协议和主机名。于是就出现了这种情况——

  • 原文地址 http://www.example.com/2017/01/21/example.html
  • 原文中引用的资源 images/1234.jpg
  • 看起来没毛病,但是 Planet Venus 解析过来就是 http://www.example.com/images/1234.jpg
  • 这似乎看起来也没毛病… 然!而!正确的地址是 http://www.example.com/2017/01/21/images/1234.jpg

…呵呵哒。

先不管为什么会有这么奇怪的资源路径,但是 Feedparser 却可以正确解析到带有跟路径的地址。也就是上面的资源地址,Feedparser 解析完之后就是正确的 /2017/01/21/images/1234.jpg

但是只有正确的相对路径还不够,因为 planet 是单独的站点,直接把 HTML 往页面中插,结果就是浏览器会去请求 planet/2017/01/21/images/1234.jpg 然而 planet 的服务器上并不会有这个资源。

于是这个问题先放着,来看另一个问题。

聚合的一个特点是,几乎无法控制来源内容的安全性——因为其他的网站服务器都不是自己维护的,其他人能否保证自己的网站不被入侵、被跨站…都是未知数。如果有订阅的网站被入侵挂了马,阅读/订阅聚合 planet 的用户也会中招。

解决这个问题,常见的办法就是过滤掉不安全的 HTML tag。于是这里引入了 sanitize-html

其实我一开始打算用 regex 直接切掉不过还是不够 robust 所以乖乖去找包了… 但是惊喜的是这个包居然可以实现按规则替换!这就顺利解决了之前的问题,可以将 Feedparser 解析得到的路径加上正确的协议和主机地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
sanitizeHtml(html, {
transformTags: {
// restore origin links
'img': function (tagName, attribs) {
if (attribs && attribs.src) {
if (attribs.src.startsWith('/') && attribs.src.charAt(1) !== '/') {
// root-relative path, add corresponding host before root
return {
tagName: tagName,
attribs: {
src: host + attribs.src
}
};
} else if (attribs.src.startsWith('//') ||
attribs.src.startsWith('http://') ||
attribs.src.startsWith('https://')) {
// absolute path, do nothing
return {
tagName: tagName,
attribs: attribs
};
} else {
// Feedparser may not correctly recognize this path, try with host/path
return {
tagName: tagName,
attribs: {
src: host + '/' + attribs.src
}
};
}
}
// don't miss the default action or it throws exception.
return {
tagName: tagName,
attribs: attribs
};
}
}
});

解决了文章内容相对路径的问题,要过滤特定的标签或者标签属性则是这个包本来就要做的事情了,小菜。

处理完文章对象之后,用 Array.prototype.sort 带一个 compare function 通过更新日期排序,接下来就可以简单渲染页面和 RSS 文件啦。

其他

一些不太常见的功能,例如 http proxy 的支持(在特殊的网络环境下可能用到),长文章仅展示 summary 并提示继续阅读,avatar 的支持,模板和输出目录保持和配置文件的相对路径,等。

然后作为深有体会——

1
2
3
CSS
IS
AWESOME

因此打死不写前端的家伙选择直接套用了 YUI Library 的 purecss 框架做一个还算看得过去的模板。至少… 几番折腾之后把响应式和 pre 等难缠的宽度搞定了顺便玩了下 media query

于是代码 -> GitHub

以及已经在使用的 Planet NyaaCat

从开坑到基本完善大概花了 15 个小时… 果然长时间不写代码手生才不是冻得手打字都变慢了呢

一开始的选择(需要登陆)中纠结使用 Planet 还是博客的时候就已经在关注 Planet 这个东西。

非常奇特,本身并不存储什么数据,但是用这样一个简单的方式在极低的成本下将大量的用户和内容结合到了一起。

终于决定要建立一个社区星球的时候,我才发现它连主题模板都不支持响应式才不是我想写个好看点的主题结果折腾一天无果(ノ=Д=)ノ┻━┻

总之,使用的是 Ubuntu 仓库内的 planet-venus 包,遇到了各种问题…

  • 莫名其妙无法获取 feed,浏览器访问正常,用 curl 正常
  • 莫名其妙无法读取 feed 内容,其他 feedparser 均正常
  • 对文章内使用相对链接的内容无能为力
  • 模板语言落后,不要跟我说 old fashion
  • 输出路径一直是 cwd,不知道是不是 feature。但是这导致我的 home 下面到处都是 output 目录

强迫症犯了。遂决定自己实现一个因为逻辑很简单啊不过就是把 RSS 抓下来排个序再丢出去么

但是到具体的细节,还是有不少需要考量的东西。

September 14, 2020 04:20 AM

玩了一下 NGINX RealIP 模块

最近要给网站上 CDN 于是折腾了下在 NGINX 部分获取客户端真实 IP 的方案。

嘛… 意想不到的简单就是…

安装 realip 模块

如果是 Debian/Ubuntu 系统,直接安装 nginx-extras 这个包即可。包含了很多有用的模块,不需要再自己编译。

如果是其他发行版,且没有提供额外模块的包的话,需要自己编译 NGINX。编译参数加 --with-http_realip_module 即可。

获得前端服务器地址

常见的 CDN 前端 IP 都可以从 CDN 提供商处获得,例如 CloudFlare 的 IP 地址段在这里

如果需要找到 Google Cloud Platform 的 IP 地址段,可以使用 Google 提供的 TXT 记录查询。

1
$ dig @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42732
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks.googleusercontent.com. 3599 IN TXT "v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all"

;; Query time: 51 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:31:29 JST 2017
;; MSG SIZE rcvd: 331

这里面的 _cloud-netblocks1.googleusercontent.com 等地址即是用于保存 GCP IP 段的地址。继续查询:

1
dig @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22867
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks1.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks1.googleusercontent.com. 3599 IN TXT "v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all"

;; Query time: 55 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:32:39 JST 2017
;; MSG SIZE rcvd: 270

于是得到了一堆 IP 地址。

设置 RealIP 模块

文件 /etc/nginx/snippets/realip.conf,请注意这个位置在其他发行版未必存在,放在 NGINX 配置目录下即可。

1
2
3
4
set_real_ip_from  192.168.1.0/24;
set_real_ip_from 192.168.2.1;
set_real_ip_from 2001:0db8::/32;
real_ip_header X-Forwarded-For;

然后在 vhost 配置文件中引用这个配置。

1
include snippets/realip.conf;

搞定,重启 NGINX 即可获得客户端真实 IP。

Note:

GCP 的 X-Forwarded-For 的客户端 IP 在第一个 , 的前面,所以一般需要 split(',')[0]

最近要给网站上 CDN 于是折腾了下在 NGINX 部分获取客户端真实 IP 的方案。

嘛… 意想不到的简单就是…

September 14, 2020 04:20 AM

重新迁移回 GCP

忍得住打 Call,忍不住折腾。看到黑科技就手痒。

Performance with GCP

虽然 AWS 用得挺安心不过越来越多的人向我抱怨博客打开速度很慢,想看文章都要等好久什么的。于是纠结了下,还是迁回了最贵但是最快的 Google Cloud Platform。这里记录一下调(zhe)教(teng)过程,毕竟 GCP 的 Cloud CDN 依然在 Alpha/Beta 阶段,可配置的选项实在太少而且很多 caveats。

前期准备

  • 域名
  • SSL 证书
  • 网站程序或内容

创建实例

我这样没什么访问量的静态网站用 f1.micro 就好啦~ 不过流量比较大的网站的话,还是建议选择高一些的配置和 SSD。

然后创建一个实例组(instance group),确保该实例组包含刚才创建的实例。

NGINX 配置

网站等会说。总之先配置好 SSL 和 Health Check。

1
2
# mkdir -p /var/www/hc
# touch /var/www/hc/index.html

默认站点配置 default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
listen [::]:80;
server_name hc; # health check
root /var/www/hc;
index index.html;
}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}

网站配置 example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;

ssl on;
ssl_certificate /etc/ssl/private/example_com.pem;
ssl_certificate_key /etc/ssl/private/example_com.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

root /var/www/example.com;
location / {
# ...
}
}

(SSL 相关的很多配置步骤省略,请自行查阅其他文档)

创建负载均衡器

戳到 Networking 标签,选择 Load balancing 然后创建一个 HTTP(S) Load Balancing。首先为负载均衡器命名,因为要分别创建 2 个负载均衡所以这里可以写例如 website-http 之类作为区分。配置页面的三个标签:

  • backend
    • 创建一个新的 backend
    • backend service 选择 http
    • instance group 选择刚才创建的实例组
    • port numbers 80
    • health check 创建新的 health check 并设置 custom header,和之前 NGINX 里的配置一致,这里是 hc
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 添加域名对应的 //* 两个路径规则。如果有特殊需求则另外添加。
  • Frontend configuration
    • Protocol 选择 HTTP
    • Create IP Address 然后 reserve 一个 IP

第二个负载均衡器用于 HTTPS,名称可以是 website-https 这样。配置:

  • backend
    • 因为配置不一样,所以要再创建一个新的 backend
    • backend service 选择 https (要点 Edit)
    • instance group 选择刚才创建的同一个实例组
    • port numbers 443
    • health check 创建新的 health check 并设置协议为 https,custom header 为网站域名(因为要和 SSL 证书相符,示例里是 example.com
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 同上
  • Frontend configration
    • Protocol 选择 HTTPS
    • IP 选择刚才 reserve 的 IP
    • 创建证书,分别上传签发的证书、CA 证书链和私钥

更新解析&收尾

将域名的 DNS 解析到刚才 reserve 的 IP 即可。

至此就是基本的(当前版本的)Google Cloud CDN 配置步骤。还有很多可以自定义和扩展、优化的空间,但是这些需要根据特定的需求变化因此不再详细记录。

以及,不要忘记在虚拟机里把网站跑起来~

最后,南酱的 live 超棒(๑•̀ㅂ•́)و✧

忍得住打 Call,忍不住折腾。看到黑科技就手痒。

September 14, 2020 04:20 AM

搭建一套权威 DNS 服务架构

萌 DNS 已经年久失修。尽管一直有计划完全重写出一套应用目前各种 DNS 特性和优化的完整平台,但是目前的精力不允许。所以为了先让萌 DNS 的用户们至少先有一个能支持 Let’s Encrypt 的 DNS 服务,决定暂时舍弃 GeoDNS 功能,使用一套更加成熟的解决方案提供服务。

搭配方案如下:

服务器部署:

  • 管理服务器 x1
    • MySQL Master
    • PowerDNS
    • PowerDNS-Admin, supervisor, virtualenv, gunicorn…
    • NGINX, Let’s Encrypt
  • DNS 服务器 x4
    • MySQL Slave
    • PowerDNS

在管理服务器上安装 PowerDNS 和 MySQL Master 的考量是由于 PowerDNS-Admin 使用 PowerDNS HTTP API,在管理服务器(或管理私网中)启动一个仅用于提供 API 和操作主数据库的 PowerDNS 实例能够减轻 Primary NS Server 的压力并提升安全性。整套架构使用 Ansible 进行自动化部署,不过好久没用了各种生疏,照着文档折腾好久的配置…

于是这里暂且记录下整个过程。有些坑只是作者一时疏忽或者有别的考量但没有明确记录,也许在未来的版本中会修复。

安装 PowerDNS

所有服务器均使用 Ubuntu 16.04,需要 PowerDNS 4.0 以上的版本。按照此页面的说明添加 PowerDNS 官方的仓库即可。

1
# apt install pdns-server pdns-backend-mysql mysql-server

由 dpkg 自动配置 PowerDNS 的数据库,然后删除 /etc/powerdns/pdns.d无关的配置文件。

1
2
# rm /etc/powerdns/pdns.d/pdns.local.conf
# rm /etc/powerdns/pdns.d/pdns.simplebind.conf

配置 MySQL Replication,管理服务器作为 Master,其他 DNS 服务器作为 Slave。细节不多讲,官方文档 或者 DigitalOcean Tutorial

管理服务器 (MySQL Master) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
api=yes
api-key=yourapisecretkey
api-logfile=/var/log/pdns-api.log
config-dir=/etc/powerdns
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
local-address=127.0.0.1 # 不对外提供服务
local-ipv6=::1
security-poll-suffix=
setgid=pdns
setuid=pdns
webserver=yes
webserver-address=127.0.0.1 # 仅向本机的 PowerDNS-Admin 调用。如果配置在内网,请使用内网 IP
webserver-allow-from=127.0.0.1/32 # 同上,如果使用内网则写 PowerDNS-Admin 在内网的 IP
webserver-port=8081
default-soa-name=ns1.example.com # 改为 Primary NS 的地址
default-soa-edit=INCEPTION-INCREMENT
default-soa-mail=hostmaster.example.com # 改为默认服务器管理员的邮箱地址,并将 '@' 替换为 '.'
default-ttl=3600

DNS 服务器 (MySQL Slaves) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
config-dir=/etc/powerdns
daemon=yes
disable-axfr=yes
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
security-poll-suffix=
server-id=ns1.example.com # 改为当前服务器的 ID,ns1/ns2/ns3/etc...
setgid=pdns
setuid=pdns
version-string=anonymous # 可以写任意字符串恶搞_(:з」∠)_

安装 PowerDNS-Admin

作者有提供详细的教程但是还是有坑

安装依赖:

1
# apt install git python-pip supervisor virtualenv python-dev libmysqlclient-dev libsasl2-dev libldap2-dev libssl-dev letsencrypt

创建数据库,切换到普通用户权限,clone 仓库到本地,然后一步一步操作即可。

1
2
3
4
5
6
7
8
$ git clone https://github.com/ngoduykhanh/PowerDNS-Admin.git
$ cd PowerDNS-Admin
$ virtualenv flask
$ source ./flask/bin/activate
$ pip install -r requirements.txt
$ pip install mysql gunicorn
$ cp config_template.py config.py
$ vim config.py

配置文件 config.py 中需要更改的地方:

1
2
3
4
5
6
7
8
9
SECRET_KEY = 'yoursessionencryptkey'
SQLA_DB_USER = 'yourdbusername'
SQLA_DB_PASSWORD = 'yourdbpassword'
SQLA_DB_HOST = 'localhost'
SQLA_DB_NAME = 'yourdbname'
PDNS_STATS_URL = 'http://localhost:8081/'
PDNS_API_KEY = 'yourapisecretkey'
PDNS_VERSION = '4.0.0'
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CNAME', 'SPF', 'PTR', 'MX', 'TXT', 'SRV', 'NS', 'SOA']

然后执行 ./create_db.py。如果没有报错说明数据库安装成功,执行 ./run.py 即可访问 http://127.0.0.1:9393 看到登陆页面了。

部署 Web 服务

直接跑 run.py 当然不科学。Supervisor 配置文件 /etc/supervisor/conf.d/pdnsadmin.conf

1
2
3
4
5
6
7
8
9
10
11
[program:pdnsadmin]
command=/home/pdns/PowerDNS-Admin/flask/bin/gunicorn run:app
directory=/home/pdns/PowerDNS-Admin/
user=pdns
autostart=true
stdout_logfile=/var/log/supervisor/pdns-stdout.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=2
stderr_logfile=/var/log/supervisor/pdns-stderr.log
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=2

创建 DHParam

1
2
# cd /etc/ssl/certs
# openssl dhparam -out dhparam.pem 4096 # 如果性能不够请使用 2048

NGINX 配置文件 /etc/nginx/site-enabled/pdnsadmin.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
server {
listen 80;
server_name dns.example.com;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location / {
return 301 https://dns.example.com$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dns.example.com;

ssl on;
ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/dns.example.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;


access_log /var/log/nginx/dns.example.com.access.log;
error_log /var/log/nginx/dns.example.com.error.log;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location /static {
alias /home/pdns/PowerDNS-Admin/app/static;
}

location / {
proxy_pass http://127.0.0.1:8000;
proxy_redirect default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forward-IP $remote_addr;
port_in_redirect on;
server_name_in_redirect off;
proxy_connect_timeout 300;
}
}

记得把 dns.example.com 换成自己的域名。

签发 Let’s Encrypt。也不多讲。NGINX 配置中已经有了针对 Let’s Encrypt 的续期设置。

然后重启各项服务

1
2
# systemctl restart supervisor
# systemctl restart nginx

查看 PowerDNS-Admin 的运行状态,使用 supervisorctl status

添加 GLUE 记录

要使自己的 NS 生效,必须有保存在上级 NS 中的记录。很多域名注册商都提供了配置 GLUE 记录的功能,例如 Hexonet (1API):

Glue Records

简言之,需要把自己的 NS 服务器及对应的 IP 记录到上级 NS。完成之后,通过 PowerDNS-Admin 添加自己的域名,zone 类型为 NATIVE。然后添加所有 NS 服务器的 A/AAAA 以及所有的 NS 记录——你没听错,要自己写 NS 记录。其他域名也需要添加这些 NS 记录,否则不会托管。

Glue Records

收尾

全部完成之后就是一个完整功能的 DNS 服务了。如果希望启用 DNSSEC,需要在管理服务器中通过 pdnsutil 来添加 key。

由于目前 PowerDNS-Admin 没有限制不能添加提供的 NS 之外的名称服务器,所以其他域名按照添加 GLUE 记录的方法,也可以将这些 NS 服务器「变成」自己的 NS。

好了,不会说话了。讲效果——

一般来说,DNS 服务都会提供多台 NS 服务器域名,将域名的 DNS 改为这些 NS 服务器才能托管到该 DNS 服务上。但是现在只需要知道这套 DNS 的服务器 IP 地址,即可给自己的域名添加 GLUE 记录、NS 记录和 NS 对应的 A/AAAA 记录进而使用自己的域名作为 NS,而不需要用 DNS 服务的 NS 域名。当然一般就是看起来会比较厉害而已…

萌 DNS 已经年久失修。尽管一直有计划完全重写出一套应用目前各种 DNS 特性和优化的完整平台,但是目前的精力不允许。所以为了先让萌 DNS 的用户们至少先有一个能支持 Let’s Encrypt 的 DNS 服务,决定暂时舍弃 GeoDNS 功能,使用一套更加成熟的解决方案提供服务。

September 14, 2020 04:20 AM

September 10, 2020

中文社区新闻

Arch除虫日:9月13日帮我们一起清理 bug !

我们将于9月13日举办一次除虫日以解决大量还开着的问题。如果你当天不能参与,可以在那之前的任何时间帮助我们。

如何?

请复查所有你报告的工单,检查它们现在是否还能复现。如果问题已经消失了,请在 bug 跟踪页面请求关闭任务(request task closure)。否则请提供更多信息以便我们能解决这些 bug 。没有你们的回馈我们没法修复这些 bug 。

问题?

在9月13日来 irc.freenode.net 上的 #archlinux-bugs 频道参与活动。因为我们生活在不同的时区,不是所有开发者和 bug 清理者总能保持一直在线,但是你可以向任何在线的开发者提问。

以及请检查你的邮箱,可能包含对你开的工单的回复通知。

by farseerfc at September 10, 2020 12:37 AM

September 05, 2020

Alynx Zhou

GObject 备忘录

说来惭愧,我一直记不太清 GObject 到底是怎么用的,毕竟作为一个写过 C++ 和 Python 然后常用 JS 的人来说,面向对象的实现是非常自然的,不需要考虑为什么。所以我总是看着一大堆类型转换和分散的定义以及各种 chain up 感到眩晕。而 GObject 的文档写的也相当分散,有种管中窥豹之感。

有同学觉得其实把函数变成指针放在结构体里看起来就面向对象了,以前我也这么觉得,但是显然这样不能实现继承封装等等特性,GObject 独特之处在于它实现了这些,并且是和语言独立的。当然,要想在一个没有这些概念的语言里面做到这些,就有很多需要自己手动处理和函数库处理的地方,就是因为有些隐藏了起来有些又要自己做,才让它看起来像古老的黑魔法。

最近我尝试做了一个小项目,以便搞清楚如何按照 GObject 的模式编写一些继承封装的代码。

首先得给项目起名字,因为 GObject 要求你的命名符合一定的约定,其中一个前缀是项目名,不过这个 简单,就叫 test 好了,然后做一个基类叫 animal,于是就有了 test-animal.h:

#ifndef __TEST_ANIMAL_H__
#define __TEST_ANIMAL_H__

#include <glib-object.h>

G_BEGIN_DECLS

// 这个玩意必须要手动定义。
#define TEST_TYPE_ANIMAL test_animal_get_type()
// 这个玩意会展开成一大堆函数声明、typedef 什么的,
// 所以我们只要按约定定义结构体实现函数。
G_DECLARE_DERIVABLE_TYPE(TestAnimal, test_animal, TEST, ANIMAL, GObject)

// 但是上面那个玩意其实不会给你搞一个类结构体出来,
// 而且这个因为是给其他文件用的所以必须写在头文件里,
// 不然人家怎么知道你有什么虚函数!
struct _TestAnimalClass {
    GObjectClass parent_class;
    // 定义一个可以继承的函数。
    void (*print)(TestAnimal *animal);
    gpointer padding[12];
};

// 因为上面的 class 里面定义了,这个只是调用那个。
void test_animal_print(TestAnimal *animal);
// 这个玩意得手动定义,是个不可继承的公开函数。
TestAnimal *test_animal_new(gchar *animal_name);

G_END_DECLS

#endif

因为代码里写了很多注释所以我这里就不再啰嗦一遍了,说点里面没写的。

G_DECLARE_DERIVABLE_TYPE 表示你声明了一个可以继承的类,也就是说你需要自己弄一个虚函数表出来。有时候你会在某个 GObject 的项目里定义了一大堆宏(比如 GTK 就不爱用这个而是手动定义),其实它们和 G_DECLARE_DERIVABLE_TYPE 做了一样的工作,因为总要做,就写了个宏实现。这个并不会给你定义具体的类结构体(其实就是虚函数表,用来存放所有可以继承重载的函数),所以我们要按照约定自己写一个 _TestAnimalClass 的结构体,在类型名字前面加下划线作为结构体名也是约定俗成的,G_DECLARE_DERIVABLE_TYPE 会展开出一句 typedef struct _TestAnimalClass TestAnimalClass

这个类结构体约定第一个元素是它的父类型的类结构体——这其实意味着我们复制了一份父类型的虚函数表出来,于是我们就可以覆盖父类型的方法而不修改原本的父类型。这一句可能比较难懂,不过后面还有关联。

这个类结构体存放的并不是实例的变量,它有点类似于 JS 里面的原型,这样我们就不需要给每一个生成的实例复制一份虚函数了,它们共用一个虚函数表。

然后会有一个 test-animal.c:

#include "test-animal.h"

// Derivable 类型会自动帮你定义实例结构体,
// 所以你想夹带点私货就得自己搞个 Private 类型。
typedef struct {
    gchar *animal_name;
} TestAnimalPrivate;

// 这个也会展开一大堆声明什么的。
G_DEFINE_TYPE_WITH_PRIVATE(TestAnimal, test_animal, G_TYPE_OBJECT)

// 要想通过 new 函数直接初始化一些值就需要搞点属性。
enum {
    PROP_0,
    PROP_ANIMAL_NAME,
    N_PROPERTIES
};

static GParamSpec *obj_properties[N_PROPERTIES] = {NULL};

static void set_property_impl(
    GObject *object,
    guint property_id,
    const GValue *value,
    GParamSpec *pspec
)
{
    TestAnimal *animal = TEST_ANIMAL(object);
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);

    switch (property_id) {
    case PROP_ANIMAL_NAME:
        if (priv->animal_name)
            g_free(priv->animal_name);
        // 所以其实我们是在设置属性的时候更新私有成员。
        priv->animal_name = g_value_dup_string(value);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void get_property_impl(
    GObject *object,
    guint property_id,
    GValue *value,
    GParamSpec *pspec
)
{
    TestAnimal *animal = TEST_ANIMAL(object);
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);

    switch (property_id) {
    case PROP_ANIMAL_NAME:
        // 所以你看属性这个名称和我们想的属性不一样,
        // 其他语言里面属性就是成员,能存点东西,
        // 但这里好像属性只是成员的一个代理,
        // 具体的东西存在成员里面,通过属性设置。
        g_value_set_string(value, priv->animal_name);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

// 给子类提供一个默认的实现,这样它就不是一个纯虚函数了。
static void print_impl(TestAnimal *animal)
{
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);
    g_message("I am an Animal called %s.", priv->animal_name);
}

static void test_animal_class_init(TestAnimalClass *animal_class)
{
    // 首先我们覆盖这个类里面的 GObject 类的方法,
    // 你调用 GObject 的函数,他会先获取参数的 GObject 类,
    // 那就获取到我们这个了,然后具体的实现就是我们覆盖的这个。
    GObjectClass *object_class = G_OBJECT_CLASS(animal_class);
    object_class->get_property = get_property_impl;
    object_class->set_property = set_property_impl;

    // 给虚方法设置默认实现。
    animal_class->print = print_impl;

    obj_properties[PROP_ANIMAL_NAME] = g_param_spec_string(
        "animal-name",
        "Animal Name",
        "Name of Animal",
        NULL,
        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
    );

    g_object_class_install_properties(
        object_class,
        N_PROPERTIES,
        obj_properties
    );
}

static void test_animal_init(TestAnimal *animal)
{
}

void test_animal_print(TestAnimal *animal)
{
    // 你看其实这里和 GObject 类设置属性的原理是一样的。
    // 子类调用这个函数,实际上获取到的是子类自己那个 Animal 类,
    // 于是他只要覆盖自己的那个 Animal 类,调用的就是他自己的。
    TestAnimalClass *animal_class;
    animal_class = TEST_ANIMAL_GET_CLASS(animal);
    g_return_if_fail(animal_class->print != NULL);
    animal_class->print(animal);
}

TestAnimal *test_animal_new(gchar *animal_name)
{
    g_return_val_if_fail(animal_name != NULL, NULL);
    return g_object_new(
        TEST_TYPE_ANIMAL,
        "animal-name", animal_name,
        NULL
    );
}

理论上来说我们直接往实例结构体里添加成员作为私有成员就可以,但是在头文件里定义实例结构体,其它文件是可以看到实例结构体的成员的,所以 G_DECLARE_DERIVABLE_TYPE 不允许我们这么做。

但有一个省事的宏 G_DEFINE_TYPE_WITH_PRIVATE,它要求我们做一个 Private 类型,里面存放我们想要的任何私有元素,它不会被继承,并且因为定义在 .c 文件里所以也不会被其它文件看到。它还会提供一些 GObject 内置类型系统的实现函数(不然 GObject 系统怎么知道你建了哪些类型!),还有一些只在实现里面用到的定义。当然 Private 类型的名字也是约定好的。

多说一句,按照 GTK 的代码实现,其实 Private 最后就是在实例结构体里面定义了一个叫 priv 的指针,类型是自己定义的 Private 类型,因为 GTK 是手写的头文件定义然后用的 G_DEFINE_TYPE_WITH_PRIVATE 同时又没产生冲突,其实你也可以完全不理会这两个宏,全都自己写,只要保证类结构体和实例结构体的第一个成员是父类结构体和父实例结构体就可以了。但是 G_DEFINE_TYPE_WITH_PRIVATE 提供了一个 项目名_类型名_get_instance_private 的函数,我们就不用直接访问 priv 成员了。

在使用 g_object_new 新建一个对象的时候可以通过 key-value 的方式设置一些初始值,这个其实是通过 GObject 提供的 property 功能实现的,因为 C 并没有哈希表这种东西。这个过程很繁琐但也很固定,其实就是实现其它语言里面传一个对象作为构造函数参数然后以此设置私有成员初始值的功能。property 就是参数,具体的内容还是存在初始值里面的,不过其实你也可以通过 property 访问具体的值。这里很容易理解成 property 有自己单独的存储空间,其实不是。property 是可以继承的,所以子类可以同时设置父类和自己的 property。

然后我们要接触到第一个重载的部分了,因为 TestAnimal 继承了 GObject,所以我们要重载掉它类结构体里面的父类结构体的 set_propertyget_property 函数。过程也很简单,GObject 要求我们实现两个函数 项目名_类型名_class_init项目名_类型名_class_init,其中前者就是让我们初始化类结构体用的。首先进行一个类型转换把 TestAnimalClass 转换成 GObjectClass(为什么可以强转?那你先思考一下为什么定义类结构体时候第一个元素是父类结构体?就是因为要这样才能进行类型转换,本质上是个套娃),然后直接赋值。这样假如有人对我们这个类型执行 g_object_set_property,实际上是调用的我们重载过的函数(为什么?怎么做到的?往下看)。

那其实我们知道如何覆盖父类的方法,但运行的时候是如何动态重载到我们自己的函数的也不清楚,我们先搞定我们自己的那个虚函数,其实很简单,我们定义具体的方法的时候(指 test_animal_print),不要让它实现具体的逻辑,而是让它通过参数的实例执行虚函数表里的函数就可以了。这需要一个自动生成的 项目名_类型名_GET_CLASS 的宏,它的作用是通过一个实例查找到 这个实例本身 对应的类结构体,然后就可以运行虚函数了。比如我给 g_object_set_property 传一个 TestAnimal,那我们调用的其实是 TestAnimalClass 的第一个成员那个 GObjectClassset_property,这个已经被我们改成自己的了,于是就实现了一个 不那么直观的 重载。

当然假如我们不想写一个纯虚函数,可以在 项目名_类型名_class_init 里面设置一个初值,这样假如子类没有重载,调用的就是这个。

那你可能要问假如我有一个不想被重载的函数呢,那你就不要跳虚函数表了,直接写逻辑就可以了。

接下来我们终于可以写子类的,首先就是 test-cat.h:

#ifndef __TEST_CAT_H__
#define __TEST_CAT_H__

#include <glib-object.h>
#include "test-animal.h"

G_BEGIN_DECLS

#define TEST_TYPE_CAT test_cat_get_type()
// Final 类型就不用写类结构体啦,反正那个最大的用处是用来写可以继承的虚函数。
G_DECLARE_FINAL_TYPE(TestCat, test_cat, TEST, CAT, TestAnimal);

TestCat *test_cat_new(gchar *animal_name, gchar *cat_name);

G_END_DECLS

#endif

TestCat 是继承 TestAnimal 的,并且我们不想让它被继承,所以它就不需要写类结构体了(因为虚函数表是用来重载的,没有继承当然也没有重载)。

所以接下来就直接到实现部分了,在 test-cat.c:

#include "test-cat.h"

// Final 类型也没有 Private,但是它自己本身就是容器嘛!
// 所以实例结构体就给你自己写了。
// 当然别的文件不需要知道你有什么私货所以不要写在头文件里。
struct _TestCat {
    TestAnimal parent_instance;
    gchar *cat_name;
};

G_DEFINE_TYPE(TestCat, test_cat, TEST_TYPE_ANIMAL)

enum {
    PROP_0,
    PROP_CAT_NAME,
    N_PROPERTIES
};

static GParamSpec *obj_properties[N_PROPERTIES] = {NULL};

static void set_property_impl(
    GObject *object,
    guint property_id,
    const GValue *value,
    GParamSpec *pspec
)
{
    TestCat *cat = TEST_CAT(object);

    switch (property_id) {
    case PROP_CAT_NAME:
        if (cat->cat_name)
            g_free(cat->cat_name);
        cat->cat_name = g_value_dup_string(value);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void get_property_impl(
    GObject *object,
    guint property_id,
    GValue *value,
    GParamSpec *pspec
)
{
    TestCat *cat = TEST_CAT(object);

    switch (property_id) {
    case PROP_CAT_NAME:
        g_value_set_string(value, cat->cat_name);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

// 那我们不想要默认的实现就可以覆盖掉自己的。
static void print_impl(TestAnimal *animal)
{
    TestCat *cat = TEST_CAT(animal);
    g_message("I am a Cat called %s.", cat->cat_name);
    // 这个自动生成的 parent 变量指向的是真正的父类!
    // 因为我们自己带的父类结构体被我们覆盖了,所以这里给你一个访问正主的机会。
    // 它是个 gpointer 所以你得手动设置类型。
    // 其实你的子类如果是完全覆盖父类的功能其实我觉得不用写这一句了。
    // 但是有时候你的子类是在父类上面增加功能就得写。
    // 这不就是 super 指针嘛!
    TEST_ANIMAL_CLASS(test_cat_parent_class)->print(animal);
}

static void test_cat_class_init(TestCatClass *cat_class)
{
    GObjectClass *object_class = G_OBJECT_CLASS(cat_class);
    object_class->get_property = get_property_impl;
    object_class->set_property = set_property_impl;

    // 干掉我们自己的这个父类结构体里面带的内置实现,
    // 这样父类的函数调用的其实是我们自己的这个实现。
    // 那你看你一会让父类调用自己一会又让自己调用父类可真是麻烦。
    TestAnimalClass *animal_class = TEST_ANIMAL_CLASS(cat_class);
    animal_class->print = print_impl;

    obj_properties[PROP_CAT_NAME] = g_param_spec_string(
        "cat-name",
        "Cat Name",
        "Name of Cat",
        NULL,
        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
    );

    g_object_class_install_properties(
        object_class,
        N_PROPERTIES,
        obj_properties
    );
}

static void test_cat_init(TestCat *cat)
{
}

TestCat *test_cat_new(gchar *animal_name, gchar *cat_name)
{
    g_return_val_if_fail(animal_name != NULL && cat_name != NULL, NULL);
    return g_object_new(
        TEST_TYPE_CAT,
        "animal-name", animal_name,
        "cat-name", cat_name,
        NULL
    );
}

Final 类型没有 Private,因为它自己就是 Private,所以这个定义实例结构体的权力交给了你,直接在里面写私有成员即可,但是不要忘了套娃(指第一个成员一定要是父实例结构体)。

然后就是设置我们自己的 property 并重载 GObject 的虚函数,当然你可能会说这样父类型的 property 不就丢了吗?其实 property 是放在 GObject 系统单独的一个表里,需要的时候它去查询,并且会考虑父类型的继承链,所以我们不需要考虑重载时候覆盖掉父类型的问题。

接下来就是重载父类型提供给我们的虚函数也就是 TestCatClass.print 了!当然首先是把我们自己的类结构体里面的父类结构体的虚函数换掉。

在写我们自己重载的函数的时候,一个比较重要的事情是 chain up,也就是最后一行那个 TEST_ANIMAL_CLASS(test_cat_parent_class)->print(animal);,有时候你重载只是做了一些子类自己的处理,然后还是需要父类提供的处理函数的,这该怎么办?你可能会说类结构体里面不是有个父类结构体了吗?但那个已经被我们覆盖了!你这样不就是咬自己尾巴的猫吗?

当我们自己解决不了的时候就得 GObject 解决了,G_DEFINE_TYPE 会提供一个 项目名_类型名_parent_class 的指针定义,这个指向的是那个真正的唯一的 不是我们自己包含的 那个父类结构体!也就是说对于子类和父类继承而言,有两个父类结构体,一个是父类自己作为一个类的那个,另一个是子类为了实现重载包含的那个,这个访问的就是前者。

当然这个类型其实是个 gpointer 所以我们还得自己转换一下类型,然后只要调用方法就可以了(这不就是 super 指针嘛)。

最后我们写一个测试的程序 main.c:

#include "test-animal.h"
#include "test-cat.h"

int main(int argc, char *argv[])
{
    TestAnimal *animal = test_animal_new("animal1");
    TestCat *cat = test_cat_new("animal2", "cat1");
    test_animal_print(animal);
    test_animal_print(TEST_ANIMAL(cat));
    return 0;
}

然后用这个 Makefile:

CC := cc
OBJECTS := main.o test-animal.o test-cat.o
CFLAGS := `pkg-config --cflags gobject-2.0`
LIBS := `pkg-config --libs gobject-2.0`

all: ${OBJECTS}
    ${CC} -o test ${OBJECTS} ${LIBS}

main.o: test-animal.h test-cat.h
    ${CC} -c main.c ${CFLAGS}

test-animal.o:
    ${CC} -c test-animal.c ${CFLAGS}

test-cat.o: test-animal.h
    ${CC} -c test-cat.c ${CFLAGS}

.PHONY: clean
clean:
    -rm test ${OBJECTS}

运行的结果是这样的:

** Message: 11:23:25.727: I am an Animal called animal1.
** Message: 11:23:25.729: I am a Cat called cat1.
** Message: 11:23:25.729: I am an Animal called animal2.

你可以看到第一行是父类也就是 TestAnimalClass 的函数的输出。第二行是 TestCatClass 重载的函数的输出,第三行则是 TestCatClass 重载的函数 chain up 到 TestAnimalClass 的输出,但是输出的是子类继承的父类的属性!

最后有关我到底该声明成子类还是父类以及传参时候要不要类型转换:其实在这个 GObject 的样例里面看起来是无所谓的,类型转换可行不可行其实取决于具体 new 的是什么,而不是声明的指针是什么,所以其实声明成父类和子类都没有关系,类型转换也主要是为了满足 C 语言对指针类型和函数声明的要求罢了。

Alynx Zhou

A Coder & Dreamer

September 05, 2020 01:23 AM

August 18, 2020

Alynx Zhou

基于 GitHub Issue 的前端评论框

造轮子是病,得治。

自从造了 前端博客搜索引擎 的轮子之后,我对自己的能力有了极大的信心,同时也掌握了一些有趣的用法,于是把枪口瞄准了下一个我看着不顺眼的地方——评论框。

(这标题怎么写的和毕业论文似的!)

轮子也不是白造的。

从我建站以来我的评论框就命途多舛,Disqus 虽然是最著名的评论系统,但是在国内访问不太顺畅。多说火了一段时间之后便关门大吉,HyperComments 则在我用了一段时间后发邮件提示要收费了,于是我之前的评论便华丽流失。而对于 Valine 这种基于 LeanCloud 的评论系统,我对 LeanCloud 不甚了解所以也不想尝试(而且 Valine 现在似乎转向闭源了,当初也许是个正确的决定)。然后赶上 Gitment 和 Gitalk 火了起来,大家意识到 GitHub Issue 正是个放评论的好地方。但是由于 Gitment 和 Gitalk 采用他们自己的服务器实现博客评论框提交——转发到 GitHub API 的实现,以及 采用的 OAuth App 权限过高,有人怀疑这不太安全,于是我也没太敢参与。后来遇到 comment.js 这个项目,它绕过了提交评论的问题——直接给一个到 GitHub Issue 的评论框的链接就可以了。于是我就开始用这个,至于什么 utterances 这种用 GitHub App 降低权限的评论系统,我也懒得尝试了。

但我为什么决定替换掉 comment.js 我已经记不清楚了,可能是为了对主题的显示有更好的掌握吧,毕竟它带有自己的 CSS 样式而且经常和我的冲突,也可能是因为它迟迟没提供像 Disqus 一样查找每篇文章评论数目的功能,不过它现在已经不再维护了,所以我也算是未雨绸缪。

事情本该很简单。

研究了一下原理其实并不是很难,首先就是从 GitHub 的 API 上 ajax 获取数据,然后操作 DOM 添进去就可以了,所以我就先阅读了 GitHub API 的文档,总之还算容易,只要先获取一个仓库的 issue 列表,然后按照某种方法在里面查找相关的 issue,如果没有就渲染一个到新建 issue 的链接,否则获取该 issue 的全部评论并显示就可以了。

查找 issue 的 key 也算容易,我给它做成了函数参数,然后在主题模板里填一个每个页面唯一的字符串就行了,比如文章标题,然后新生成 issue 页面时候就把这个作为 issue 的标题,这样查找 GitHub Issue 页面时候也比较容易看。然后 GitHub 把 PR 也视为 issue,这个不要紧,收到数组之后过滤一下就好了。而显示数目我琢磨了一下其实也没什么难的嘛!我都获取到所有 issue 的信息了。做一个新的函数,主题作者在页面上放置一系列空的元素并设置好 class 属性,然后同样地把每个页面唯一的字符串设置成这些元素的属性(Disqus 也是这样的),加载函数时候把 class 作为参数传进去,分别从 issue 列表里查找对应的标题就可以了。还有一个奇葩问题是 GitHub issue 的 comments 指的是除了顶楼以外的评论,但很显然看起来不是这样的,这个也简单,直接把顶楼加到数组里就成了嘛。剩下就是艰苦的在浏览器里刀耕火种写 JS 拼 HTML 字符串发 ajax 请求写嵌套回调(没有 async/await 也太痛苦了吧!。

一切正常工作了一段时间一后我发现不太对劲,怎么评论太多的时候显示不全呢?仔细查了一下发现 GitHub API 是强制分页的,也就是说不管你怎么设置,最多一次只能获取 100 条评论,默认则是 30 条,本来我不太想给博客加评论分页功能的,现在看来是 GitHub 被迫让我加啊。当然这个并不是最痛苦的,最痛苦的是它查找仓库的 issue 列表的时候也是强制分页的!这就麻烦了,还记得我们之前说要获取到列表之后查找标题吗?获取不到完整列表还怎么查找啊!

当然你可以说按顺序多查几页不就行了嘛,这就是它分页难受的地方了!ajax 是异步的啊亲!你不会想让我一个 for 循环几个 ajax 还指望优雅的等他们结束了再跑查找吧!我知道能用 Promise.all() 解决,但是由于我大发慈悲的支持使用 IE11 的用户(微软我&A%¥S&……省略一千字儒雅随和),所以我的函数是基于回调的,那也没什么办法。而且这样首先你得读一下仓库信息才能知道有多少个 open 的 issue(没错只能算 open 的不能算 close 的!所以在后面查 issue 的时候我也不得不筛选掉 close 的,不过这大概也许是个 feature?),然后自己计算有多少页。最后我只能造了一个不那么优雅的尾递归回调(反正就那个意思),不过它工作的不错,这样我就可以获取到全部的 issue 列表了。

然后后面其实还是差不多,至于评论分页又不一样了,既然 GitHub 只有分页 API,我也就半推半就啊不是将计就计吧。我才不要继续获取全部评论了,我也每次直接获取 GitHub 那边的一页就行了,每页个数则由主题作者传参进来。至于如何确定我要哪一页呢?和搜索功能一样,继续前端解析 query string 呗。根据 issue 评论总数计算一下有几页,然后生成几个链接放在页尾,每次加载时候解析一下参数确定当前页是哪个即可。当然,不要忘了 issue 顶楼不算评论,计算分页的时候也不要给它加进去!而且既然是这么分页,我也懒得把顶楼算在里面了(不然真的麻烦的要死啊后面分页和每页个数全乱了),所以假如你设置每页 10 个评论,那第一页其实有 11 个,别烦我,代码在那,不满意自己改……然后继续刀耕火种……

为了减轻负担,我没有实现太多的功能,比如时间戳我没有搞成什么几分钟几小时前,这种东西又不清晰又浪费带宽,我只搞了基于 marked 的 Markdown 渲染(必须的)和语法高亮,Markdown 渲染不是必须的,因为你可以 设置 header 让 GitHub 直接返回 HTML。为了保证效果统一,这个 JS 只是建立了 HTML 布局,给每个元素添加了 class,具体的样式则完全是主题自己编写的,所以配合起来也比较好看。

最后的效果其实还可以,完整的脚本就是 这个网站在用的 JS,具体和主题的整合方法就慢慢翻 ARIA 的模板吧。唯一的缺点是 GitHub API 的频率限制太低,按照这个弱智的 issue 列表分页的话又不得不多一次读取仓库有多少 issue 的请求,假如你的 issue 太多估计也是问题?不过应该不会有那么多博文吧!我只有调试脚本的时候遇到过被 GitHub 提示等会的问题,所以对于访问者应该没什么影响。有影响以后再想解决办法(或者没办法)。

也许最好的办法是解决掉需求——要什么评论框?不就是破事水?如果有问题想联系作者电子邮件又不是不能用!(逃

其实你知道,烦恼(bug)会解决烦恼(bug)。

这一部分更新于 2020-08-17 10:34:00。

GitHub API 推荐用户 缓存之前的请求响应,然后根据缓存的响应的 Header 里面的 ETag 发送请求查询是否过期,若未过期则返回一个不消耗频率限制次数的 304 状态码。我心想这也简单,那就在前端搞一个缓存就可以了。

然后我搜索了一番找到了 CacheStorage,看起来它是唯一一个跨标签页的基于 Session 的正宗的前端缓存。但是很显然 IE 又不支持,而且这个 API 基于 Promise 并且只能缓存 Response 对象,也就是说没办法简单的通过在 XHR 的时候判断一下跳过不支持的情况,要 Polyfill 则需要引入完整的 Promise 和 fetch/Response,所以我们做了一个艰难容易的决定——是时候去掉 IE 支持了!

于是我把请求 API 的函数改成了如下操作:

let cachePromise = window.caches.open("cacheName");

// Fetching JSON with cache for GitHub API.
const cachedFetchJSON = (path, opts = {}) => {
  let cachedResponse = null;
  return cachePromise.then((cache) => {
    return cache.match(path);
  }).then((response) => {
    // No cache or no ETag, just re-fetch;
    if (response == null || !response.headers.has("ETag")) {
      return window.fetch(path, opts);
    }
    // Ask GitHub API whether cache is outdated.
    cachedResponse = response;
    opts["headers"] = opts["headers"] || {};
    opts["headers"]["If-None-Match"] = cachedResponse.headers.get("ETag");
    return window.fetch(path, opts);
  }).then((response) => {
    if (response.status === 200) {
      // No cache or cache outdated and succeed.
      // Update cache.
      cachePromise.then((cache) => {
        return cache.put(path, response);
      });
      // Cache needs an unconsumed response,
      // so we clone respone before consume it.
      return response.clone().json();
    } else if (response.status === 304 && cachedResponse != null) {
      // Not modified so use cache.
      return cachedResponse.clone().json();
    } else {
      // fetch does not reject on HTTP error, so we do this manually.
      throw new Error("Unexpected HTTP status code " + response.status);
    }
  });
};

当然理想很丰满现实很骨感,在不支持 CacheStorage 的浏览器里要 fallback 到不带缓存的版本,本来我以为很简单,但是……(下面开启吐槽时间。)

支持 IE 的前端的痛苦都是相似的,不支持 IE 的前端则各有各的痛苦。

为什么非 HTTPS + localhost 不能用 CacheStorage 啊,难道他们没考虑过在电脑上开发然后手机访问测试移动版吗?还是说他们打算在手机上起一个开发服务器?为什么 Firefox 在非 HTTPS 时限制 CacheStorage 的方法是在 Promise 里 reject 一个 Error 从而导致这个过程变成了异步的?为什么 CacheStorage 只能缓存 Response 而不是任意数据结构?Safari 不能完整支持 Response 对象也就算了,为什么移动版 Chrome 和 Firefox 也不支持?合着你们 fetch 返回的 Response 还不是 Response?这世界到底怎么了……

所以最后需要一个长长的基于 Promise 的判断加载函数:

// 加载评论的时候才加载缓存。
let cachePromise = null;

let fetchJSON = uncachedFetchJSON;

const loadCache = (name) => {
  // Unlike in .then(),
  // we must explicit resolve and reject in a Promise's execuator.
  return new Promise((resolve, reject) => {
    if (cachePromise != null && fetchJSON !== uncachedFetchJSON) {
      return reject(new Error("Cache is already loaded!"));
    }
    // Old version browsers does not support Response.
    if (window.Response == null) {
      return reject(
        new Error("Old version browsers does not support Response.")
      );
    }
    const testResponse = new window.Response();
    // Safari and most mobile browsers do not support `Response.clone()`.
    if (testResponse.headers == null || testResponse.clone == null) {
      return reject(new Error(
        "Safari and most mobile browsers do not support `Response.clone()`."
      ));
    }
    // Chromium and Safari set `window.caches` to `undefined` if not HTTPS.
    if (window.caches == null) {
      return reject(new Error(
        "Chromium and Safari set `window.caches` to `undefined` if not HTTPS."
      ));
    }
    window.caches.open("CacheStorageTest").then((cache) => {
      fetchJSON = cachedFetchJSON;
      cachePromise = window.caches.open(name);
      return window.caches.delete("CacheStorageTest");
    }).then(() => {
      return resolve();
    }).catch((error) => {
      // Firefox throws `SecurityError` if not HTTPS.
      console.error(error);
      return reject(new Error("Firefox throws `SecurityError` if not HTTPS."));
    });
  }).catch((error) => {
    console.error(error);
  });
};

不管怎么样现在这个网站在支持 CacheStorage 和 Response 的浏览器上(似乎也就桌面版 Chrome/Firefox……)是缓存 GitHub API 的结果了,打开 DevTools 切到 Network 面板可以看到 GitHub API 返回的是 304 而不是 200,其他浏览器则 fallback 到无缓存的 fetch。当然其他浏览器不包含 IE 咯。

由俭入奢易,由奢入俭难。

这一部分更新于 2020-08-18 12:25:00。

我后来又仔细想了想,其实要兼容 IE 还是有办法的,首先 fetchPromise 都有成熟的 polyfill,甚至 URLSearchParams 也有,只要写一段脚本在不支持的时候加载他们就可以了。然后去掉所有 IE 不支持的 ES6 特性,比如箭头函数、模板字符串、for…of… 循环以及 MapReduce(IE 竟然支持 constlet 真是惊到我了)。但是能做到并不意味着一定要做,人总是还要向前看的,现在是 2020 年,连罪魁祸首始作俑者微软都放弃了 IE,就算是照顾用户量,IE 用户也是可以忽略的那一部分了。既然我已经用 ES6 重写了,就不要想再让我为这种历史垃圾放弃我得到的好处了,从我开始写主题到现在丢掉 IE 支持也算是仁至义尽了,所以为什么不让这些用户支持一下 Firefox 呢?

Alynx Zhou

A Coder & Dreamer

August 18, 2020 04:25 AM

August 15, 2020

Alynx Zhou

前端博客搜索引擎

本来我的博客有一个前端搜索框,当输入文字时就在侧边栏展开搜索结果,虽然看起来很时髦,但也不能算是什么食用啊不实用的设计方式。而且一开始我觉得既然没有后端处理请求,也就没法单独打开一个专门搜索页面了。

但是这个设计引发了 @依云 的吐槽,有一个单独的搜索页面并且对每个搜索结果有专门的 URL 看起来还是个挺合理的需求。但第一我不知道怎么实现第二我一直觉得能用就行所以开始并没有改。不过依云给我发了 Python 文档的搜索实现,让我突然想明白其实按照标准的 HTML 和 JavaScript 是可以实现无后端的搜索引擎的,然后就动手实现了一个。

首先既然要搜索那还是得有个索引或者数据库,比较简单的方案就是把所有文章的标题 URL 和内容丢到一个 json 文件里面,这个功能在 Hexo 里可以使用 hexo-generator-search 实现,我也给我的 Hikaru 添加了这个生成器,用来生成 JSON。

首先第一件事是实现搜索跳转页面,这一步只要简单的使用 HTML 表单就能实现,首先将我的搜索框改造为如下格式:

<form action="{{ getPath("search.html") }}" method="get">
  <button type="submit" class="search-submit" aria-label="{{ __("search") }}">
    <i class="fas fa-search"></i>
  </button>
  <input type="search" id="search-input" class="search-input" name="q" results="0" placeholder="{{ __("search") }}" aria-label="{{ __("search") }}">
</form>

使用 button 而不是 input 的原因是我想用我的图标做搜索按钮,反正 <input type="submit"> 只是个特化的 button。理论上来说不放按钮靠回车提交也是 OK,但是觉得这样又会被某些用户批评不友好了……

搜索框就很简单,name="q" 表示生成的 query string 里 keywords 的 key 是 q,然后按照表单写法会被提交到 search.html,理论上来说搜索应该发 GET 请求所以就是 method="get",当然 POST 就实现不了复制链接查看搜索结果了。

这个表单就是标准的 HTML 表单,不需要用 JavaScript 处理。生成的 GET 请求的 URL 类似于 /search.html?q=xxx

然后接下来是处理请求了,既然是发送到 search.html,对于静态后端肯定是要返回这个页面的,那就得先创建页面,然后对这个页面进行特殊处理,这里我同样利用生成器生成一个 layout 设置成 search 的页面,然后就可以单独给它编写模板添加处理部分了。

search.html 加载之后是可以通过 window.location.search 获取到 query string 的,然后我加载我修改过的 search.js这里),用它处理搜索过程。同时在页面里添加了一个 container 用来放置检索结果。

search.js 其实没什么黑科技,毕竟我们的难点就是在无后端情况下处理关键词和数据库,关键词已经用 window.location.search 拿到了,解析一下然后只要通过 ajax/fetch 请求数据就行了,封装 ajax 的代码网上到处都是,实在不行用 jQuery 也成。

然后剔除重复关键词主要是为了优化一下性能,接下来我的解决方案就是简单粗暴 indexOf(),不要跟我提什么算法什么优化,短平快实现效果,我是个实用主义者,目前这一步其实还没有成为瓶颈。

然后对于原版文件我的改进主要是按匹配次数排序,文章出现关键词越多则排序越靠前,相对可以提高效率。

接下来使用正则表达式给关键词加上 <strong>,这样显示起来比较显眼。最后把字符串拼起来显示就好了。其实这里的算法还有点意思,比如假如两个关键词出现的位置中间大于多少个字符则插入省略号,否则合并两个的上下文,具体实现也可以参照代码。

如果有性能瓶颈的话,多半也会先出现在 ajax,不过目前我文章还没有多到加载不出来的情况,也许可以靠分块加载解决?

更新(2020-08-15 18:25:00):我给代码添加了简单的分块支持。由于这里需要主题和生成器约定好路径,不太适合让生成器自动生成路径,所以采用了一个简单的方法就是让用户在设置文件里手动指定几个 JSON 文件的路径,然后生成器只是读取一下配置,假如是数组就分块写到指定好的路径里面。然后前端查找的时候分别异步查找每个文件并合并排序结果,理论来说大概会有性能提升?

最后我加了个简单的 SpinKit 动画,在查询结束之前先跑一下提升用户体验。

Alynx Zhou

A Coder & Dreamer

August 15, 2020 10:25 AM

August 07, 2020

Alynx Zhou

给你的主题来点暗色!

我自己对暗色模式其实是没什么兴趣的,因为设计一种配色就已经让我绞尽脑汁了,还要我设计另一种。但是我也确实意识到暗色模式在晚上玩手机实在是很方便,而且做这个也很流行,于是我也做了一个,只是因为我能做到。

跟着系统变色就行了吗?

现在的系统大概都支持暗色模式(Linux 的桌面环境早就有这种设置了,Firefox 可以直接读取我的系统设置,Android/iOS 也都有暗色模式开关),浏览器也紧跟潮流提供了 @media查询属性(IE:那……是……什……么……)。理论上来说只要简单地在 CSS 里面查询然后编写修改颜色的代码就可以了,问题只是如何修改颜色比较轻松。

CSS 变量是好东西!

使用 CSS 变量 当然是最简单的解决方案了,就像我们平时编程一样使用变量作为 colorbackground 的值,然后在查询到暗色模式的代码块里给这些变量重新赋值,一切都十分简单有条理,而且最重要的是你不需要一个一个选择器查找有哪些需要变色的属性,所有的颜色变量都是放在一起的。

真是恨死 IE 这废物了!

虽然 IE 有很多不支持的选项,但是不支持 CSS 变量真是让我工作量剧增的一件事情。虽然我从不测试我的网站在 IE 上能否运行,但是在最新版 IE 上一般都是没问题的,因此我也会放弃一些最新版 IE 不支持的新特性,CSS 变量就是其中之一。

既然没有办法用 CSS 变量,那就只能自己一个一个找选择器下面和颜色相关的属性,然后给它们重新设置属性了,真是找的人头晕眼花啊。

可能有人会说你不是用 CSS 预处理器吗,预处理器不是也有变量吗?但是预处理器是在生成阶段把变量编译掉了啊!不方便到运行时(浏览器)里去替换变量。

我就是想在暗色浏览器里用亮色啊!

在全部重新调整过颜色并能看之后(其实就是把浅色的色块换成深色,颜色层次基本不变,背景图搞个反色,至于那些彩色的按钮标签我实在没精力重新配色了,把透明度调低一点就好了),我自己还是比较喜欢自己一开始设计的样子,但我又是个习惯电脑全局暗色的人,这怎么能忍!

CSS 的媒体属性不像一般的属性,只能是浏览器设置我们读取,没有办法用 JS 控制,于是也就没法简单地利用这个添加切换按钮。上网搜了半天也只有曲线救国的方案。

曲线救国

如果并不是想那么和系统的设置同步而只是给自己的网站添加切换的话,并不需要媒体查询。只要设计一个按钮给 <html><body> 添加删除 class/attribute 就行了。然后如果要和系统同步,在 JavaScript 里也有 相关的 API 可以做到查询和监听,在检测到变化的时候也修改 class/attribute 即可。

我选择的是给 <html>data-theme="dark"data-theme="light" 属性,不选 <body> 是因为 WebKit 那些该死的不遵循标准的 scrollbar 伪类,文档没有说他们到底依附哪个元素,我尝试得到的是 <html>。接下来只要把之前的 CSS 里面的媒体查询选择器改成 html[data-theme="dark"] 就行了。

不过还要注意继承关系,这样写的话有些属性并不是继承外面的,而是在这个选择器里面就近继承。比如假如修改了 html[data-theme="dark"] a 的边框,那 html[data-theme="dark"] a.cls 的边框会优先继承这个,而不是 a.cls。我知道有些人可能会笑我半懂不懂了,但是我确实遇到了这个问题,并且思考了一下找到了原因。

还有一个比较尴尬的事情,我给一些元素设置了 transition 用于 :hover 之后加一个渐变颜色的效果,现在暗色模式也是修改颜色,导致这些元素会比其他元素慢变一下,没什么好办法因为你分不开两种 color 变化。我的解决方案是一个一个找切换暗色模式时候会变属性的选择器,给它们也添加 transition。效果还不错,不过 Chrome 在处理这种 CSS 动画时候竟然会掉帧???

不管了,反正我用 Firefox,Firefox 效果好得很,完全不掉帧。

更新(2020-08-07 10:50:00):我怀疑 Chrome 想做新时代的 IE,其实并不是性能问题导致掉帧,WebKit 对于继承来的属性的 transition 存在问题,会导致不是同时变换而是有延迟的变换,效果糟透了,StackOverflow 上也有人遇到这个问题,看起来是 bug 并且不打算解决,而 Firefox 就没有这个问题。使用 CSS 变量在 WebKit 下效果会好一点,不过也不能给所有变色的元素加 transition,还是会卡,只能给 <body> 加一个,因为我的链接有 :hover 时变色的 transition,都是 color 就没办法在那时 transition 而暗色模式时候不 transition。总之由于 WebKit 的存在导致没法让全部元素同步 transition,只能近似。两权相害取其轻,还是让 IE 用户只能用亮色吧,我最后还是选择了 CSS 变量。

怎么你这破网站换个页面还要重新点一次?

一切看起来都十分美好,直到我把一个暗色页面切成亮色然后点了个链接,下一个页面并不会理会我们上一个页面设置了什么主题,又变成了暗色。每个页面点一下切换按钮也太烦人了,我们得来点持久化。

某个域名想在用户的浏览器里存点是完全可行的,使用 localStorage 就行了,就是简单的键值对。但是这样我们就有了多种可能切换亮暗的动作:localStorage 里面存的选项,网页加载时浏览器媒体查询的结果,用户点了网页上的切换按钮,用户点了系统切换亮暗的设置。这些的判断顺序要好好处理一下,不然某些就会被忽视掉变成“我点了怎么不动啊!!!”。

经过我考虑一下之后,这个玩意的逻辑应该是这样的:

  1. 假如 localStorage 里面没有值,用户是首次打开网站,此时读取媒体查询按照系统的主题设置。
  2. 否则说明用户之前打开过网站,已经有他自己的喜好了,按照 localStorage 里面的值设置。
  3. 上两个步骤结束之后注册一个媒体查询监听器,用于响应用户修改系统设置。
  4. 注册一个按钮监听器,用于响应用户点击网页切换按钮设置。

以上 1 和 2 的顺序不能反了,并且每一个设置动作里都要把这次设置的值写入 localStorage 用于后续加载用户的选择。

现在看起来一切都很满意,是时候发布了!

用户:啊!我的眼睛!

我躺在床上用手机测试的时候又发现了一个问题。因为网页在生成的时候总有一个初始状态(JS 要等到 document 加载完成才开始处理 DOM,不像那些单页应用),假如我设置成暗色然后切换到别的页面,网页就会以亮色加载然后变成暗色,用户在晚上看起来就像是个闪光弹(伏拉什棒!)。

其实没什么好的解决办法,因为这是传统 HTML 页面的限制之一,我也不想找有没有什么新的东西能解决这些问题,最后的方案其实相当简单,既然亮色到暗色会让人受不了,我搞成暗色到亮色不就行了。

于是就是把模板里按钮的初始状态修改一下,渲染的时候出来的是 data-theme="dark",假如用户选择亮色,加载页面时会有一个暗色到亮色的变化。反正我自己都不在意。

更新(2020-08-06 18:23:00):我后来阅读了一些其它主题的代码,看它们是怎么在不使用单页应用的前提下解决这个问题的,结果方法相当简单,我自己也想得到:不用等到 DOMContentLoaded 事件之后,反正只是修改 <html> 标签,直接在 <script> 标签里面编辑 document.documentElement 是可以的,因为反正加载到 <script> 的时候肯定也已经加载到 <html> 了。所以就把修改这个属性和修改按钮的 DOM 分开了两部分,并且添加了一个 storage 的监听器,这样假如打开了多个页面,一个页面切换其它页面也会跟随切换。

不要过度设计!

经过这么一大堆折腾(代码行数++++++++),我甚至在想要不要支持每个页面单独设置亮暗初值,反正只要添加一个 front matter 然后在模板里判断一下嘛。不过后来想了想,就算有这么个功能又有什么用?实际意义几乎为零,徒增复杂度,所以还是不要过度设计了。

Alynx Zhou

A Coder & Dreamer

August 07, 2020 02:50 AM

August 04, 2020

Alynx Zhou

和 cheerio 说再见!

我早就想把 cheerio 从 Hikaru 的依赖里移出去了,倒不是我对他的功能有什么不满,但是一年不更新 NPM 上的包也太恶心了吧!

我的生成器使用 cheerio 的地方主要有两个,一是给标题生成喵点并依次生成 TOC,二是检查文章里相对路径的图片和链接引用并改成绝对路径,否则如果截取之后放在首页的摘要包含图片的话,会因为当前页面的地址变化而不工作。后者也是我自己编写生成器的原因之一,就是因为像 Hexo 或者 Hugo 这种已有的生成器内部并没有考虑,只能通过插件解决,我在写 Hexo 主题时嵌入了一个使用正则表达式的脚本,但相比之下我觉得还是一个能理解 HTML 的库更加可靠。所以后来我选了 cheerio 来做解析和修改以实现这些功能。

但是 cheerio 有一个 很古怪的 bug,存在于它目前在 NPM 上最新的版本 1.0.0-rc3 上。假如你使用 cheerio 解析并编辑包含中文的 HTML 的话,导出字符串时所有的中文都被编码成了 HTML 实体,导致进行下一步处理比如用 substring 截断的话,字符串长度变化了,而且可能会在文字中间截断。假如你使用 decodeEntities: false 的话,原本文档里的 &lt; 一类的字符反而会被 cheerio 导出成 <,变成一团乱麻。

这个 bug 非常古怪并且我花了一段时间来研究它,上一个版本 0.22.0 使用 decodeEntities: false 是没有这个问题的。最后发现原因是 cheerio 在 1.0.0-rc1 开始引入 parse5 代替原本的 htmlparser2 作为默认的 HTML 解析器,而 parse5 在解析时并不会使用 decodeEntities 这个参数,比如你输入 &lt;,parse5 解析时会在节点里存储原本的值 <,但 cheerio 在序列化的时候还是会依赖这个参数进行编码,所以假如传递 false 进去,cheerio 就不会主动进行 encode,导致最后出来的是 <。而 htmlparser2 会按照这个参数选择解不解码,所以也不会发生这种错误。

那这么看起来是 parse5 的问题?并不!parse5 有自己的序列化函数,和自己的解析函数是配套的,所以只要使用 parse5 的序列化函数就不会存在这个问题了。

最后总结起来修复的办法也有很多:首先其实序列化的时候并不需要将所有元素都编码成 HTML 实体,只要对几个字符进行转义即可,我提交了这样的 PR,但因为 cheerio 和它自己的序列化库 dom-serializer 是两个仓库,不是很好处理,而且其实也没有从根源上解决 htmlparser2 和 parse5 表现不一致的问题。cheerio 后来的提交中采用的是简单办法,假如使用 parse5 解析就继续使用 parse5 序列化就好了。

但是这个提交之后并没有发布到 NPM 上,NPM 挂的一直是有问题的 1.0.0-rc3 版本,至于原因呢很简单,他们打算释出第一个稳定版本 1.0.0,所以要等到所有 TODO 都解决了再发新版本!

搞毛啊老哥,你这样放着有 bug 的版本是把用户做宝搞吗?而且你是个函数库,下面好多人依赖你处理 DOM 呢,NodeJS 上提供 jQuery 模式的函数库大概也没什么别的替代品了,一堆项目在 issue 里问你就给个这破理由?

我选择的是锁死 0.22.0 版本,其他下游的项目也都各自做了 workaround,要么安装 GitHub 上的 1.0.0 分支要么回退版本。但总之都让强迫症很不爽啊!

大家等他发稳定版就这么等了一年,在这一年间有些项目比如 Hexo 直接抛弃了 cheerio 用正则处理 HTML,虽然他们主要是为了性能。我倒不是那么在乎生成器的性能(真的在乎的话我就该去用 Hugo,而且我也不敢说我自己的代码写的很好)。

晚上睡觉前我突然想到假如只是过滤链接和图片并检查他们的属性的话其实不需要 jQuery 一样的 API,只需要能理解 HTML,那找个简单的解析器就可以了。而且第二天正好读了 卷老师的这篇博文,发现和我想的也差不多。于是就开始动手。

卷老师用的 sanitize-html 对我来说不太合适,因为我的静态生成器并不需要过滤内容——都是 Markdown 生成的,而且不安全也是使用者自己故意写的,不是我的责任,生成器也没有不安全的 HTML 的运行环境。sanitize-html 和 cheerio 用的都是 htmlparser2 作为解析器,虽然它号称自己是跑得最快的 HTML 解析器,但经过之前那个问题我还是心有余悸。而且 htmlparser2 是非常简单的基于事件回调的解析器(就像 Python 自带的那个,我太久不写 Python 不记得叫什么了),不会给你构建树状结构也不包含序列化,自己写序列化就容易像之前那个 cheerio bug 一样出问题。所以我考察了一下 parse5 发现还不错,会构建树状结构,数据结构都写在文档里,同时就是简单的可以直接操作的 Object,至于速度虽然慢一点但我并不太在乎那 10 毫秒。

于是我就先手动造了一些 wrapper 函数比如递归前置遍历一颗树来代替 cheerio 的 $.each(),前置遍历正好是 HTML 文档自上往下的顺序,生成 TOC 的时候也是按照这个顺序来的,然后就是比如获取结点内文本的函数、获取属性和设置属性的函数之类的。至于插入结点我想了个巧妙的办法,因为 parse5 把最终解析到的文本作为单独的结点,我就直接在插入文本的函数里让 parse5 解析输入的 HTML,然后用得到的子结点替换被插入结点的子结点即可。

随后我重写了生成 TOC,生成标题 ID 和检查文章里相对路径的图片和链接引用并改成绝对路径的函数。然后写了点简单的测试样例跑了一下发现没问题,就用它们替换掉 Hikaru 的 utils.js 里面用 cheerio 的版本,然后修改了 process 代码。我造的这几个简单的 wrapper 完全符合我的要求,于是提交打版本号发布一气呵成,一年不更新的 cheerio 就从我的生成器里拜拜了。

虽然嘴上说着不追求速度,但是凭我肉体的感觉还是多少快了一点儿,不过我也并不是很在乎生成的时间,感知真的不强,无所谓了。但我真的不能理解这种为了憋个大的不发版本把用户做宝搞的行为,就算是发个 1.0.0-rc4 也比一年不发强吧!就算只是 make user happy 也好,何况面对的不是 user 而是其它项目的 dev 呢,都是同行,这行为算不算托大?反正我是习惯做了点改动就打个新版本,即使我自己安装的都是开发版本,我也希望能把我最新的修改送到用户手上,又不是 breaking change 或者什么不能随便更新的软件,打个小版本号至于那么难吗???

Alynx Zhou

A Coder & Dreamer

August 04, 2020 01:06 PM

July 28, 2020

中文社区新闻

AUR服务器迁移:新的 SSH HostKeys

因为我们已将 AUR 迁移到了一个新的服务器,通过 SSH 连接服务器时的 HostKeys 有所变化。以下是新的 key 指纹:

Ed25519: SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
ECDSA: SHA256:uTa/0PndEgPZTf76e1DFqXKJEXKsn7m9ivhLQtzGOCI
RSA: SHA256:5s5cIyReIfNNVGRFdDbe3hdYiI5OelHGpw2rOUud3Q8

以上指纹也可以在 AUR 主页上未登入时看到。

by farseerfc at July 28, 2020 01:17 AM

July 23, 2020

frantic1048

Pulchra - 四糸乃

原本去年就已经收到了的四糸乃,结果因为左臂脱胶掉下来了就换了个货,这一换就是一年多,客服感觉都换了一波又一波。最近总算是到了,这次手臂很稳,非常科学。

四糸乃帽子是带磁吸的,很方便把帽子固定住,结果角度没注意好,回头才发现帽子上的蝴蝶结一直没拍到 ˊ_>ˋ

Yoshino Yoshino Yoshino Yoshino Yoshino

Yoshino Yoshino Yoshino

Yoshino Yoshino

July 23, 2020 12:00 AM

July 22, 2020

百合仙子

Linux 的环境变量怎么设

本文来自依云's Blog,转载请注明。

最近,Arch Linux 的 pam 包将更新到 1.4.0,然后因为一个字符的变化,不少中文用户都开始避难了:pam_env 将默认不读取用户的环境变量设置。许多中文用户使用~/.pam_environment文件来配置 fcitx 输入法所需要的那三个环境变量。更新之后,这些配置将不再生效,意味着他们可能无法输入中文了,于是大家热烈讨论现在要在哪里配置环境变量比较好。

当然了,在 pam 的配置文件里加上user_readenv=1就可以恢复原先的行为。只是,pam 的开发者这么改当然是有原因的:CVE-2010-4708。简单地说,就是在鉴权模块里设置用户指定的环境变量不安全,怕被提权。

2020年08月20日更新:Arch Linux 打包时添加了以上这个配置

当然啦,桌面用户完全可以去改系统级的环境变量设置,只要你愿意让你个人的配置信息跑到系统配置那边去的话。

正文

用户经常需要设置一些环境变量,比如输入法,比如语言和地区选项,比如自定义的 PATH,等等。然而 Linux 的环境变量又不像 Windows 那样有一个专门设置所谓「系统环境变量」的地方,而是所有环境变量全部由子进程继承父进程获得,这么一路继承下来的。于是,想要给所有想设置上的进程都设置上,就不是一件简单的事情了。

这里关注的是桌面用户,自然首先从图形界面说起。

使用 X11 的桌面环境,通常通过 display manager 来登录,比如 lightdm 和 sddm。这俩都支持 ~/.xprofile。这个文件会在启动过程中被 source,使用的 shell 是由 dm 自己确定的。lightdm 和 sddm 都是用的 /bin/sh(分别位于 /etc/lightdm/Xsession 和 /usr/share/sddm/scripts/Xsession 文件里)。可以看到,除了读取 .xprofile 外,lightdm 也会读取 .profile。sddm 甚至连 bash、zsh、tcsh、fish 的启动配置脚本都给读了。

对于 Wayland,我这儿只有 sddm,类似的,也是各种 shell 的都给你读了。(当然就不读 X11 相关的东西了。)

而对于手动 startx 的用户,~/.xinitrc 里自己写上呗。还有 VNC 啥的,也都可以自己看启动脚本找到方法。

没有图形界面的登录,通常是从 tty 或者 ssh 登录。这两者都是登录到命令行 shell。写在 shell 配置里就可以了。具体到 zsh,我是写到 ~/.zprofile,这样不管是否交互,只要是登录 shell 就执行,不登录的大概已经继承到了。另外有些情况可能写这儿也没有用,比如 cron 调用的时候。非要搞的话其实可以写到 ~/.zshenv,这个基本上所有的 zsh 都会读,只能被系统级设置或者命令行参数禁用掉。而如果是 bash 用户,.bash_profile .bash_login .profile 这仨,会读找到的第一个(sddm 也会按这个顺序找,但是 lightdm 只会找 .profile)。Arch Linux 现在默认会给新用户建立 .bash_profile,我建议 bash 用户把它改名成 .profile。这里有个图 ,大致上说明了 bash 和 zsh 不同情况下会读取的配置文件

systemd 用户实例也有一份环境变量,用于通过 systemd 启动的用户级服务,通过 systemctl --user 的子命令可以查看和更新。D-Bus 也有一份,可以通过 dbus-update-activation-environment 命令来更新。这俩通常不需要用户操心,桌面环境应该会维护好。

如果你用 tmux 的话,那么 tmux 还有一份,用于新创建的窗格。tmux 会在被连接时更新 update-environment 选项指定的环境变量。当然你也可以用 tmux setenv 子命令去更新。这个尤其值得注意,因为 tmux 可能在不同的环境中被连接,导致环境变量混乱(比如 X11 下没有 DISPLAY 变量)。

要注意的一点是,不同的登录过程并不是只会读取自己专属的配置文件,尽管大家都在努力,也并没有完全统一好用的配置文件。所以有些环境变量,你可能想尽量避免重复设置(比如冗长重复的 PATH 项就很讨厌)。好在这些都是 shell 脚本,不光能设置环境变量,也能做条件判断。

要查看当前进程的环境变量,可以用 env 命令。要查看别的进程的,去读它的 /proc/PID/environ 文件,或者开个 htop 然后在进程上按 e 键。要看看某个环境变量在不同进程里是什么情况,可以用我写的小工具 compare-env

Linux 软件生态的特点就是这样,没有谁规定一定要用怎样的方式做一件事情,所以大家的想法总会有些出入,就导致设置个环境变量还有这么一大群配置文件(systemd 还有一份呢,我还没读没试所以没说)。但另一方面,理解它们也是十分容易的:不用猜测,不用撞大运般地耗费时间去尝试,顺着代码摸过去,总能弄明白软件的行事逻辑。即使文档像 pam_env 那样前后矛盾,也有源代码这条路可以精确刻画软件的行为。

by 依云 at July 22, 2020 02:59 PM

July 01, 2020

ヨイツの賢狼ホロ

给 GNU/Linux 萌新的 Arch Linux 安装指南 rev.B

给会用点 Windows 的彻头彻尾的 GNU/Linux 新手的 Arch Linux 安装指南 😣

为啥要搞这个?

因为 ArchWiki 上的 Beginner Guide 已经和 Installation Guide 合成一个啦😂, 然后有小白开始抱怨看不懂啦(误 (╯・∧・)╯ ┻━┻

其实咱最早看的也是 Beginner Guide ……

算了概念用到时再解释 😂

我是一个彻头彻尾的Linux新手,我应该用Arch吗?

如果你是新手,要使用 Arch 就必须愿意花时间学习新系统,接受 Arch 是一个 DIY 的系统,每个用户都是自己系统的组建者。

在开始问任何问题之前,自己先通过Google、Wiki或者论坛进行搜索。我们为你创建了这些资源并让你可以随时访问,上千志愿者为你提供了大量的信息资源。

推荐阅读: Arch terminology#RTFM

首先说点废话

  • Arch 这种经常更新内核的还是适合上实体机……

  • 看得懂英语的话最好还是看官方的 Installation Guide 吧(人家更新的比咱要勤快):

    https://wiki.archlinux.org/index.php/Installation_guide

  • 这里的目标是装好最简单的一个带有桌面的 Arch Linux ,对于部分特殊需求(例如无 GUI 的环境), 请直接参阅上方官方 Wiki 的安装指南装出基本系统,然后再参阅其他的文章呗~

备份……

万一手抖格错了盘别抱怨 GNU/Linux ……

但如果汝有备份的话,是不是已经下定决心把整个硬盘格式化掉了?(雾)

下载 ISO

https://www.archlinux.org/download/

BT 种子和磁力链接在上面,直接下载的话往下拉,找 China 下面的镜像网站挑一个下载就好。 (っ╹ ◡ ╹ )っ

如果是用 HTTP/HTTPS 下载的话,下载完以后最好验证一下文件的散列值来确定文件下载完了 (╯艹︿艹)╯ ┻━┻

Windows 上这样的工具有很多呐,例如 HashTab 和 FCIV。

但是如果汝正好没有这些工具或者从来没听说过散列(或者另一种译名叫哈希)的说法的话,可以用 Windows PowerShell 来完成:

# 例如获得某一个文件的 md5 散列值,用汝下载的 ISO 的路径换掉 <filepath>
Get-FileHash <filepath> -Algorithm MD5

# 尽管 md5 和 sha1 已经被认为是不安全的散列算法了(容易发生两个文件不同但散列值相同的情况,
也被称作散列碰撞”,然而 Arch Linux 的官方网站上还是只有这两种 😂, 先凑合着用吧……

至于下面那个 PGP 签名,又是个大坑,先鸽了(咕咕咕……)

确定启动类型

  • 首先打开设置 ( Windows 8/8.1 叫做 "电脑设置"),然后通过 "更新和恢复" -> "恢复" -> "高级启动" 重启电脑.

    Windows 10 的话,按住 Shift 再点击电源按钮里的重启也行……

如果是 UEFI 启动的话,大概是这个样子:

UEFI 系统启动之后大概像这样

没错就是有个 "使用设备" 的选项 😂

  • 或者同时按下键盘上的 Windows 徽标键(就是有 Windows 标志那个) 和 R 键,会打开“运行” 对话框。
“运行”对话框

在里面输入 msinfo32 然后回车(按 Enter 键)确认,打开”系统信息”应用。

“系统信息”窗口

看“BIOS模式”里是不是 UEFI 😂😂,还有下面那个 “安全启动状态”是不是“以关闭”(咱这台电脑的 UEFI 太旧所以显示的是不支持)

如果安全启动是打开的还需要自己进 UEFI 固件设置里手动关闭 😂

具体怎么关因为每种电脑的方法不一样于是汝要自己 STFW (Search the f**king Web,搜索一下) 了😂
  • 再或者打开磁盘管理(Windows 8 以后的系统可以通过按下 Windows + X 的菜单里找到 “磁盘管理”)
磁盘管理在这~

嗯,大概就是这样子的呗 (虽然具体的磁盘分区可能和咱的不一样)

大概长这样~

看汝的硬盘上有没有一个 EFI 系统分区 😂😂😂


还是搞不懂的下面也不用看了,准备下最后的晚餐吧 😋 (误

在硬盘上准备一块空闲空间

不然要把 Arch Linux 装到哪里去呐?

这里拿来演示的是 Windows 7 以后都自带的 “磁盘管理” 程序,应该能解决大多数问题 _(:з」∠)_

  • Windows 8 以后的系统可以通过按下 Windows + X 的菜单里找到 “磁盘管理”
磁盘管理在这~
  • 嗯,大概就是这样子的呗 (虽然具体的磁盘分区可能和咱的不一样)
大概长这样~
  • 汝哪个硬盘分区比较空闲? 右键点击它,有一个"压缩卷的选项"
”压缩卷“ 在这~
  • 输入压缩的大小 _(:з」∠)_
多少
  • 然后就多了一块未分配的空间 😂
多了一块未分配的空间

如果汝的硬盘分区有些刁钻而磁盘管理没法解决的话,AOMEI 家的分区助手不错, 这是官方网站 , 这是分区教程

制作启动盘

但前提是汝的电脑能从 U 盘启动 😂 (不过最近几年生产的电脑都应该可以了吧……

Windows 下咱比较推荐一个叫 rufus 的软件,官方网站在这

下载完以后双击运行,需要管理员权限,记得看有没有数字签名。(有数字签名时用户账户控制的对话框是蓝色的)

Rufus 自带多国语言(当然也包括中文啦),如果汝系统语言不是中文的话,点击那个地球图标就可以修改语言了啦~

选择语言

然后戳有点像光盘的按钮选择刚下载好的 ISO 镜像

选择映像

然后选择一种启动类型,UEFI 就选最后一个,不是的话就选第一个。

选择启动类型

写入方式选推荐的就好 (´_`)

选择写入方式

确认(要知道汝按下确认以后就没有回头路了,所以记得提前备份 U 盘上的资料 😂)

确认

然后坐等完成,完成以后汝的 U 盘卷标应该是 "ARCH_201610" 这样的 (后面四位年份和两位月份),不要改成别的,万一不对记得照 ISO 改回来 😂😂

准备启动

重启电脑,然后让电脑从 U 盘启动。

具体怎么搞还是要看电脑的硬件啦 😂

大多数的电脑都有一个在开机时按下一个按键来选择从哪里启动的选项(例如 Dell 和 ThinkPad 是 F12)。 UEFI 的话, 刚才提到的那个“使用设备” 的选项也可以。

还是去翻一翻汝自己的电脑生产商的说明书更快一点 ……

  • MBR 成功启动以后像这样
MBR

选第一项。😂 (除了 CPU 不支持的都应该用x86_64 😋)

后来 Arch Linux 不再支持 i686 啦,所以这里应该只会看见 x86_64 那一项(但是咱懒得换图片啦……)
  • UEFI 成功启动以后像这样
UEFI

还是选第一项。😂

然后等待一会以后会出现……

root@archiso ~ #

这就表示已经启动完毕啦 ~(>_<~)

root是用戶名,前面那個數字是上一個命令的exit status啦,如果正常結束的命令exit status是0,就不會顯示出來,你有 1 2 127 這種都是某種東西報錯了.

----现任 Arch Linux TU 之一的 farseerfchttps://www.zhihu.com/question/45329752/answer/98733823 中写到……

联网

因为 ArchISO 就是个重启就没了的 Live 环境,安装时需要的软件包都要另外下载。

首先当然是联网啦,如果是自动获取 IP 地址的有线网络,那么应该啥也不用做,ping 一下试试?

root@archiso ~ # ping archlinux.org
# 汝应该会看到像这样的东西 ……
PING archlinux.org (138.201.81.199) 56(84) bytes of data.
64 bytes from apollo.archlinux.org (138.201.81.199): icmp_seq=1 ttl=49 time=361 ms
……

(把电脑用网线接到家里的路由器上就有相同的效果)

如果没网的话…… 😂


  • 先用 ip link 确定一下网卡
root@archiso ~ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp4s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
    link/ether c8:9c:dc:a8:ab:c3 brd ff:ff:ff:ff:ff:ff
3: wlp0s29u1u1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 44:94:fc:0f:63:b9 brd ff:ff:ff:ff:ff:ff

这个例子里,lo 是本地环回不用管它,enp 开头的是有线网卡,wlp 开头的是无线网卡。

如果汝明明有无线网卡却没识别的话,有可能汝是某无线网卡厂商受害者😂😂

这时可以:

  • 有 Android 手机的话,手机连 WiFi ,然后用“USB 网络共享”共享给电脑。
  • 找个 USB 无线网卡插上 😂
  • 连有线😂😂
  • 如果有无线网卡的话,试试连接到 WiFi ……

    2020 年 7 月的 ArchISO 拿掉了 netctl,于是经典的 wifi-menu 也被 iwctl 取代了。

    如果汝之前也能熟练使用 wpa_supplicant 啥的那当咱没说……

# iwctl 命令来进入 iwd 的 shell。
root@archiso ~ # iwctl
# 以下的操作都在 iwctl 的 shell 中,输入 help 来获得可用的命令。
[iwd]# help
# 列出无线网络设备
[iwd]# device list
# 用所指定的设备扫描网络(用上一步的设备名称替换 device)
[iwd]# station device scan
# 或者列出所有的网络
[iwd]# station device get-networks
# 连接到网络, SSID 就是汝要连接的网络的 SSID 啦
[iwd]# station device connect SSID
# 如果所连接的网络需要密码,则接下来会提示输入,汝当然也可以直接在 iwctl 命令中输入
root@archiso ~ # iwctl --passphrase passphrase station device connect SSID

谁叫 Arch 连不上网的话都装不了 😂

另外,如果汝连接的网络需要网页登录(Captive Portal),可以用 elinks 碰碰运气 😂

# 用汝的门户的 URL 替换 <your_captive_portal_url>
# 如果不知道的话,随便访问一个 HTTP 网站试试,应该就会被重定向到 Portal 了
root@archiso ~ # elinks http://<your_captive_portal_url>

时间同步

timedatectl set-ntp true 保证时间同步 。

root@archiso ~ # timedatectl set-ntp true
root@archiso ~ # timedatectl status
    Local time: Fri 2016-10-28 17:39:42 UTC
    Universal time: Fri 2016-10-28 17:39:42 UTC
    RTC time: Fri 2016-10-28 17:39:42
    Time zone: UTC (UTC, +0000)
Network time on: yes
NTP synchronized: no
RTC in local TZ: no

因为有不少操作需要准确的时间呐,例如 HTTPS 和 GnuPG 都需要准确的时间来验证证书的有效性。

但是如果因为各种原因没法同步的话,那就只好手动设置咯~

# timectl set-time "yyyy-MM-dd hh:mm:ss"
root@archiso ~ # timectl set-time "2016-10-28 17:39:42"

准备硬盘空间

这里用 cgdisk (UEFI)/ cfdisk (MBR) 来给硬盘分区。

首先输入 lsblk 看看汝的硬盘是哪个设备:

root@archiso ~ # lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 463.9G  0 disk
├─sda1   8:1    0   512M  0 part
├─sda2   8:2    0    16M  0 part
├─sda3   8:3    0 103.4G  0 part
└─sda4   8:4    0 253.4G  0 part
sdb      8:16   1   7.5G  0 disk
└─sdb1   8:17   1   7.5G  0 part /run/archiso/bootmnt
sr0     11:0    1  1024M  0 rom
loop0    7:0    0 346.1M  1 loop /run/archiso/sfs/airootfs

如果那是一块新硬盘或者已经清空的硬盘的话,汝大概看不到 /dev/sda1 一类的分区。


如果汝没看到 /dev/sda 却看到了一些其它的东西的话:

  • /dev/nvme0n1 一类的:表示汝的电脑上有一块 NVMe SSD (发出了羡慕的目光(咳咳))
  • /dev/mmcblk0 一类的:表示汝的电脑上有 eMMC 存储(例如是早期的 Windows 8 平板电脑啥的)

这种时候的话可能汝 ArchISO 的 U 盘就跑到 sda 去了,在下面的操作时记得注意一下。

比如咱这里 sda 是咱的硬盘,于是运行 cgdisk 时加上 /dev/sda 这个参数:

/dev 是一个虚拟目录(也就是并不在硬盘上),它会把电脑上的设备映射成一个个文件 _(:з」∠)_

再学究的解释真不会了……

cgdisk

root@archiso ~ # cgdisk /dev/sda

如果汝在用 cgdisk 的话汝可能会看到这样的提示:

Warning! Non-GPT or damaged disk detected! This program will attempt to
convert to GPT form or repair damage to GPT data structures, but may not
succeed. Use gdisk or another disk repair tool if you have a damaged GPT
disk.


                                            Press any key to continue....

这表示 cdgisk 在这块硬盘上找不到 GPT 分区表。如果这是一块新硬盘或者已经清空的硬盘的话 不必担心,继续即可。否则请立即停下来检查一下硬盘(或者汝有这块硬盘的备份的话也可以继续)。

                                            cgdisk 1.0.1

                                        Disk Drive: /dev/sda
                                    Size: 972906545, 463.9 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
1           512.0 MiB   EFI System                EFI system partition
2           16.0 MiB    Microsoft reserved        Microsoft reserved partition
3           103.4 GiB   Microsoft basic data      Basic data partition
4           253.4 GiB   Microsoft basic data      Basic data partition
            106.6 GiB   free space





    [ Align  ]  [ Backup ]  [  Help  ]  [  Load  ]  [  New   ]  [  Quit  ]  [ Verify ]  [ Write  ]

cgdisk 的界面大概像这样啦,用上下方向键把光标移动到汝之前的空闲空间上去(例如咱这里是最后一个)

新硬盘的话应该只有一个 free space 😂

用左右方向键把下面一排按钮上的光标移动到 New 上,然后按 Enter。

(这里看不出光标😂,黑色背景下光标应该是白的吧😂😂)

接下来会问几个问题(# 开头的是咱加上的注释😂):

# 数字可能和汝看到的不一样😂
# 起始扇区的位置,直接 Enter 就行
First sector (749424640-972906511, default = 749424640):
# 大小,可以是扇区数,也可以是实际的大小(例如 100M,20G一类的),要用掉整个剩余空闲空间的话,直接 Enter 就行。
Size in sectors or {KMGTP} (default = 223481872):
# 分区类型,默认的就好
# 但是如果要建立新的 EFI 系统分区的话 ,分区类型是 :code:`ef00`
# 但是如果要建立新的 交换空间(就是虚拟内存啦)的话 ,分区类型是 :code:`8200`
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):
# 设置卷标,不设置也行。
Current partition name is ''
Enter new partition name, or <Enter> to use the current name:

然后汝应该会发现下面的空闲空间变成 Linux filesystem 了呗~

要保存分区表的话,用左右方向键把下面一排按钮上的光标移动到 Write 上,然后按 Enter。

Are you sure you want to write the partition table to disk? (yes or no):

        Warning!! This may destroy data on your disk!

在这里输入 yes (就是 yes,不是 y Y YES 啥的😂),然后按 Enter。

然后下面会闪过一行 "The operation has completed successfully" ,这时就可以退出了。

用左右方向键把下面一排按钮上的光标移动到 Quit 上,然后按 Enter。

cfdisk

如果汝在用 cfdisk 的话汝可能会看到这样的提示:

                        ┌ Select label type ───┐
                        │ gpt                  │
                        │ dos                  │
                        │ sgi                  │
                        │ sun                  │


        Device does not contain a recognized partition table.
Select a type to create a new label or press 'L' to load script file.

这表示 cfgisk 在这块硬盘上找不到可以识别的分区表。如果这是一块新硬盘或者已经清空的硬盘的话 不必担心,选择 dos 继续即可。否则请立即停下来检查一下硬盘(或者汝有这块硬盘的备份的话也可以继续)。

这就是 cfdisk 的主界面啦:

                                                    Disk: /dev/sdc
                                Size: 465.8 GiB, 500107862016 bytes, 976773168 sectors
                                        Label: dos, identifier: 0x178bfb32

    Device            Boot               Start              End          Sectors         Size       Id Type
>>  Free space                            2048        976773167        976771120       465.8G










┌──────────────────────────────────────────────────────────┐
│Filesystem UUID: 3AB8802AB87FE2B5                                                                                   │
│     Filesystem: ntfs                                                                                               │
└──────────────────────────────────────────────────────────┘
                            [   New  ]  [  Quit  ]  [  Help  ]  [  Write ]  [  Dump  ]


                                        Create new partition from free space

也是选择 new 然后输入大小:

Partition size:
# 然后可能会让汝选择是主分区还是扩展分区,按需选择就 OK
[primary]  [extended]

但是如果汝需要调整分区类型的话,选中要改变的分区然后把光标移动到 Type 上按 Enter,选择一个分区类型:

                                                    Disk: /dev/sdc
                                Size: 465.8 GiB, 500107862016 bytes, 976773168 sectors
                                        Label: dos, identifier: 0xe0a3ecf7

    Device           Boot               Start             End        Sectors        Size      Id Type
>>  /dev/sdc1                            2048         1050623        1048576        512M      ef EFI (FAT-12/16/32)
    /dev/sdc2                         1050624       976773167      975722544      465.3G      83 Linux










┌──────────────────────────────────────────────────────────┐
│Partition type: EFI (FAT-12/16/32) (ef)
└──────────────────────────────────────────────────────────┘
            [Bootable]  [ Delete ]  [ Resize ]  [  Quit  ]  [  Type  ]  [  Help  ]  [  Write ]  [  Dump  ]

要保存分区表的话,用左右方向键把下面一排按钮上的光标移动到 Write 上,然后按 Enter。

Are you sure you want to write the partition table to disk? (yes or no):

在这里输入 yes (就是 yes,不是 y Y YES 啥的😂),然后按 Enter。

然后下面会闪过一行 "The operation has completed successfully" ,这时就可以退出了。

用左右方向键把下面一排按钮上的光标移动到 Quit 上,然后按 Enter。


然而汝以为这样就结束了?还没格式化呢 (╯°Д°)╯︵/(.□ . )

创建文件系统+挂载

所以挂载是啥玩意?

GNU/Linux 不是用 Windows 那样的盘符来分区的啦,GNU/Linux 继承了 Unix 的整个目录结构 从根目录 / 开始。然后下面是各种子目录。

大多数的 GNU/Linux 发行版都遵循一个叫做“文件系统层次结构标准”(Filesystem Hierarchy Standard, 有时也简称作 FHS)的标准,汝可以 ls / 一下看看会不会发现一些例如 /bin /home /usr 一类的目录? 这就是拜这个标准所赐 😂

在 Windows 里,汝大概会把不同的文件放在不同的盘符下的硬盘分区里。 在 GNU/Linux 中,咱们通常就是把不同的分区 挂载到整个目录结构中的一处。向挂载好的目录写入的文件就会保存在被挂载的磁盘分区上。

首先还是用 lsblk 确定一下分区的名称,为了以防万一记得加上 -f 参数:

root@archiso ~ # lsblk -f
NAME   FSTYPE   LABEL       UUID                                 MOUNTPOINT
sda
├─sda1 vfat               3C44-B4ED
├─sda2
├─sda3 ntfs               42E243C5E243BBC3
├─sda4 ntfs   新加卷      58741F29741F0A00
└─sda5
sdb
└─sdb1 vfat   ARCH_201610 EAC8-F012                            /run/archiso/bootmnt
sr0
loop0  squashfs                                                  /run/archiso/sfs/airootfs

第一排分别表示设备名称,文件系统类型,卷标,UUID和挂载点。

咱这里的话 sda1 那个 vfat 分区就是 EFI 系统分区啦,sda5 就是刚刚新建的分区啦~(因为还没格式化所以没有文件系统😂)

mkfs.ext4 把那个分区格式化成 ext4 文件系统咯~

mkfs 可以格式化成某一个汝指定的文件系统(就像刚才那样 mkfs.ext4 可以格式化出一个 ext4 文件系统)

ext 家族大概是 GNU/Linux 最古老,应用的最广泛的文件系统了吧……

当然可以用的文件系统有很多啦,如果汝想再了解一些的话,可以去 ArchWiki 看一看:

https://wiki.archlinux.org/index.php/File_systems

记得自己看清楚是哪个分区别格式化错了 😂

root@archiso ~ # mkfs.ext4 /dev/sda5
mke2fs 1.43.3 (04-Sep-2016)
Creating filesystem with 27935234 4k blocks and 6987776 inodes
Filesystem UUID: a3943e57-6217-4a5f-8e57-ade5771315c0
Superblock backups stored on blocks:
    32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
    4096000, 7962624, 11239424, 20480000, 23887872

Allocating group tables: done
Writing inode tables: done
Creating journal (131072 blocks): done
Writing superblocks and filesystem accounting information: done

root@archiso ~ #

等一排文字闪过就格式化完了……

如果要格式化新的 EFI 系统分区的话,用 mkfs.vfat

如果要格式化新的 交换空间的话,用 mkswap

接下来用 mount 挂载分区啦~ (。>ω<)。

# mount <设备名称> <目标文件夹>
# /mnt 挺合适的
root@archiso ~ # mount /dev/sda5 /mnt
# 如果要挂载 EFI 系统分区的话,建议挂载到 /mnt/boot
# 所以先建立相应的文件夹
root@archiso ~ # mkdir /mnt/boot
root@archiso ~ # mount /dev/sda1 /mnt/boot
# 有交换空间的话不用挂载,用 swapon 命令。
root@archiso ~ # swapon /dev/sda6

选择软件仓库镜像

软件仓库(在Debian系发行版中,又叫做“软件源”)是软件包存储的地方。通常我们所说的软件仓库指在线软件仓库,亦即用户从互联网获取软件的地方。

以及新的 ArchISO 中内置了通过测速和筛选镜像服务器位置来选择镜像的 Reflector, 使用方法在这里。

用 nano 打开 /​etc/​pacman.d/​mirrorlist

root@archiso ~ # nano /etc/pacman.d/mirrorlist


GNU nano 2.7.0                        File: /etc/pacman.d/mirrorlist

##
## Arch Linux repository mirrorlist
## Sorted by mirror score from mirror status page
## Generated on 2016-10-01
##

## Score: 0.2, France
Server = http://archlinux.polymorf.fr/$repo/os/$arch
## Score: 0.3, France
Server = http://arch.tamcore.eu/$repo/os/$arch
## Score: 0.3, Germany
Server = http://mirrors.cicku.me/archlinux/$repo/os/$arch
## Score: 0.3, Czech Republic
Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch
## Score: 0.3, Germany
Server = http://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
## Score: 0.4, Netherlands
Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch
## Score: 0.4, Poland
                                        [ Read 517 lines ]
^G Get Help    ^O Write Out   ^W Where Is    ^K Cut Text    ^J Justify     ^C Cur Pos     ^Y Prev Page
^X Exit        ^R Read File   ^\ Replace     ^U Uncut Text  ^T To Spell    ^_ Go To Line  ^V Next Page

这是 GNU nano 的主界面,最简单的方法还是把下面那些 Mirrors 先全删掉然后输入一个新的, 用光标指向某一行以后同时按下 Ctrl+K 就好。然后自己输入一个 Mirror ,下面给出几个中国国内的 Mirror:

(所谓的 Ctrl+K 就是这两个键一起按😂)

# 网易
Server = http://mirrors.163.com/archlinux/$repo/os/$arch
# 清华大学 TUNA 协会
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
# 中国科学技术大学
Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
# 西安交通大学
Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch

输入完以后按下 Ctrl+O 写入,按 Enter 确定,再按 Ctrl+X 退出。

然后用 pacman -Syy 刷新一下软件包数据库。

root@archiso ~ # pacman -Syy
:: Synchronizing package databases...
core                                  120.9 KiB  4.92M/s 00:00 [##################################] 100%
extra                                1755.6 KiB  5.24M/s 00:00 [##################################] 100%
community                               3.7 MiB  6.82M/s 00:01 [##################################] 100%
root@archiso ~ #

安装基本系统

用 pacstrap 安装基本系统:

  • 默认会安装 base 元包(包含基本系统所需的依赖)。

    如果汝在别的地方见到说 base 是个软件包组的说法,忘了它吧……

  • 然后大多数情况下,汝还需要一个内核。目前官方仓库里有这些:

    对于大多数情况下,汝可能会考虑安装固件包 linux-firmware 。

  • 以及一个文字编辑器,例如刚才用到的 nano

    诶,base 组不包含 live 环境中所有的软件包么?是的, https://projects.archlinux.org/archiso.git/tree/configs/releng/packages.x86_64 列出了在 ISO 中的 live 环境中安装的但不在 base 包中的软件包。

  • 以及一些文件系统工具,如果汝和咱这里一样用了 ext4 的话,安装 e2fsprogs 就好, 需要读写其它文件系统的话,可以在 https://wiki.archlinux.org/index.php/File_system 找到相应的用户空间工具。

  • 要通过 AUR 或者 ABS 编译安装软件包,还需要安装 base-devel 啦 (现在它还是个软件包组)。

  • 要通过刚刚用过的 iwctl 连接无线网络的话,记得安装 iwd 。

于是一个栗子大概像这样

root@archiso ~ # pacstrap /mnt base base-devel linux linux-firmware nano e2fsprogs iwd

其他软件以后会用 pacman 再安装啦~

安装完以后大概会是这个样子 (´・ω・`)

pacstrap /mnt base linux    29.09s user 2.61s system 85% cpu 37.271 total

准备进入 chroot 环境

生成 fstab 啦 ~

所以 fstab 又是个啥玩意?

fstab(5)文件可用于定义磁盘分区,各种其他块设备或远程文件系统应如何装入文件系统。

每个文件系统在一个单独的行中描述。这些定义将在引导时动态地转换为系统挂载单元,并在系统管理器的配置重新加载时转换。 在启动需要挂载的服务之前,默认设置会自动fsck和挂载文件系统。例如,systemd会自动确保远程文件系统挂载 (如NFS或Samba)仅在网络设置完成后启动。因此,在/etc/fstab中指定的本地和远程文件系统挂载应该是开箱即用的。

emmmmm

# genfstab
usage: genfstab [options] root

Options:
    -L             Use labels for source identifiers (shortcut for -t LABEL)
    -p             Exclude pseudofs mounts (default behavior)
    -P             Include printing mounts
    -t TAG         Use TAG for source identifiers
    -U             Use UUIDs for source identifiers (shortcut for -t UUID)

    -h             Print this help message

genfstab generates output suitable for addition to an fstab file based on the
devices mounted under the mountpoint specified by the given root.
root@archiso ~ # genfstab -U /mnt >> /mnt/etc/fstab

然后用 arch-chroot 向新系统出发~

因为还有些配置没完成嘛……
root@archiso ~ # arch-chroot -help
usage: arch-chroot chroot-dir [command]

    -h                  Print this help message
    -u <user>[:group]   Specify non-root user and optional group to use

If 'command' is unspecified, arch-chroot will launch /bin/bash.
root@archiso ~ # arch-chroot /mnt /bin/bash
[root@archiso /] #

设置基本系统

# 开头只表示以 root 用户运行,汝不用把 # 输入到终端里啦~
  • 设置时区(中国的时区是 Asia/Shanghai)
# ln -s <源文件> <目标> 创建一个符号链接

# ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  • 设置时间标准 为 UTC,并调整 时间漂移:
# hwclock --systohc --utc

如果汝正在尝试安装双系统,在进入 Windows 以后可能会发现 Windows 的时间不对了 ,因为 Windows 默认的硬件时钟是 localtime(

可以用一条注册表键值让 Windows 使用 UTC 作为硬件时钟(在早于 Windows 7 的系统上发现过这样做会出现一些严重的问题: http://www.cl.cam.ac.uk/~mgk25/mswish/ut-rtc.html

reg add "HKEY_LOCAL_MACHINESystemCurrentControlSetControlTimeZoneInformation" /v RealTimeIsUniversal /d 1 /t REG_DWORD /f
  • /etc/locale.gen 是一个仅包含注释文档的文本文件。指定您需要的本地化类型,去掉对应行前面的注释符号(#)就可以啦,还是用 nano 打开,建议选择帶UTF-8的項:
# nano /etc/locale.gen

en_US.UTF-8 UTF-8
zh_CN.UTF-8 UTF-8
zh_TW.UTF-8 UTF-8
  • 执行 locale-gen 以生成 locale 讯息:
# locale-gen
  • 创建 locale.conf 并提交您的本地化选项:

    将系统 locale 设置为en_US.UTF-8,系统的 Log 就会用英文显示,这样更容易问题的判断和处理。用户可以设置自己的 locale。

    警告: 不推荐在此设置任何中文locale,或导致tty乱码。

# echo 用来输出某些文字,后面的大于号表示把输出保存到某个文件里啦~

# 或者可以用文字编辑器新建这个文件加上这一行。

# echo LANG=en_US.UTF-8 > /etc/locale.conf
  • 设置一个喜欢的主机名(用汝的主机名代替 myhostname ):
# echo myhostname > /etc/hostname
  • 设置 root 的密码(输入密码的时候就是啥也没有 ╮( ̄▽ ̄)╭ ):
[root@archiso /]# passwd
New password:
Retype new password:
passwd: password updated successfully

以及汝大多数时候应该是用不到这个密码的(

  • 安装启动加载器(例如 GRUB ):

    启动加载器是 BIOS 或 UEFI 启动的第一个程序。它负责使用正确的内核参数加载内核, 并根据配置文件加载初始化 RAM disk。

    如果对其它的启动管理器有兴趣的话,记得去看 https://wiki.archlinux.org/index.php/Arch_boot_process#Boot_loader

** UEFI 用户先再安装几个必要的软件包咯, efibootmgr 用于修改 UEFI 固件中的某些信息,

dosfstools 包含了操作 FAT/FAT32 文件系统所需的用户空间工具。

# pacman -S efibootmgr dosfstools

** 然后安装 GRUB (如果汝的硬盘上没有其它系统,那么可以不用装 os-prober )

# pacman -S grub os-prober

** 把 GRUB 安装到硬盘:

# MBR 用户这么做 (记得用汝自己硬盘的名称代替 sda ,不要带上表示分区的数字啦~):

# grub-install --target=i386-pc /dev/sda --recheck

# UEFI 用户这么做(如果汝没把 EFI 系统分区挂载到 /boot ,请自行修改 --efi-directory 的值):

# grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub --recheck

EFI 安装成功以后大概像这样 😂

[root@archiso /]# grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub --recheck
Installing for x86_64-efi platform.
Installation finished. No error reported.

然后生成必要的配置文件:

[root@archiso /]# grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-linux
Found initrd image(s) in /boot: initramfs-linux.img
Found fallback initrd image(s) in /boot: initramfs-linux-fallback.img
done

如果汝安装了 os-prober 然后看到这样的警告:

WARNING: Failed to connect to lvmetad. Falling back to device scanning.

这是因为在 chroot 环境里面 /run 是不可用的。只要每个步骤都做对了,这些警告不会影响系统启动, 汝可以放心继续进行下一步的系统安装咯。 如果汝重启之后没看到 Windows 的启动项,试着进入 Arch 之后再运行那条生成配置文件的命令。

因为 GRUB 是一个操作系统,它的配置和使用可能会非常复杂,可以在 https://wiki.archlinux.org/index.php/GRUB 上找到一些提示和疑难排解技巧。

设置 sudo

所以这又是个啥玩意? (´_`)

因为 root 用户的权力很大而且很危险,所以轻易不会用到它 (。>ω<)。

所以就有了 sudo(substitute user do) 使得系统管理员可以授权特定用户或用户组作为 root 或其他用户执行某些(或所有)命令,同时还能够对命令及其参数提供审核跟踪。

(话风突变)

sudo 应该已经作为 base-devel 的一部分装上去了,如果没有的话汝也可以自己手动安装一下:

[root@archiso /]# pacman -S sudo

sudo 的配置文件是 /etc/sudoers ,但是咱们不会直接去编辑它(因为一旦搞坏了不好修)。 所以有一个 visudo 的命令用来代理编辑它(就是先编辑一个临时文件,然后检查有没有错误, 一切 OK 后再覆盖)。

嗯…… visudo? 如果汝觉得 vi 哪里眼熟的话,没错! 这个命令默认会用 vi 去编辑那个文件。 如果汝想用其他的编辑器(例如刚刚用的 nano)的话,可以通过一个环境变量:

[root@archiso /]# EDITOR=nano visudo

现在大概像这个样子:

GNU nano 3.2                                        /etc/sudoers.tmp

## sudoers file.
##
## This file MUST be edited with the 'visudo' command as root.
## Failure to use 'visudo' may result in syntax or file permission errors
## that prevent sudo from running.
##
## See the sudoers man page for the details on how to write a sudoers file.
##

##
## Host alias specification
##
## Groups of machines. These may include host names (optionally with wildcards),
## IP addresses, network numbers or netgroups.
# Host_Alias    WEBSERVERS = www1, www2, www3

##
## User alias specification
##
                                                [ Read 97 lines ]
^G Get Help    ^O Write Out   ^W Where Is    ^K Cut Text    ^J Justify     ^C Cur Pos     M-U Undo       M-A Mark Text
^X Exit        ^R Read File   ^\ Replace     ^U Uncut Text  ^T To Spell    ^_ Go To Line  M-E Redo       M-6 Copy Text

虽然这个文件有很多行,但是咱们还是先从让它能够工作开始来最小的修改它。

找到下面的这一行,然后把 %wheel 前面的注释符号(#)去掉,不过百分号要留下:

## Uncomment to allow members of group wheel to execute any command
# %wheel ALL=(ALL) ALL

然后就可以保存退出啦~ (效果就是注释里说明的,给 wheel 组执行所有命令的权限)

如果汝不想每一次都在前面加上 EDITOR 来指定编辑器的话,可以加上这几行:

# 重设默认的环境变量
Defaults      env_reset
# 设置默认的编辑器,并使 visudo 不再读取环境变量 editor 的值。
Defaults      editor=/usr/bin/nano, !env_editor

如果汝想进一步了解 sudo 的配置的话,还是去看 ArchWiki: https://wiki.archlinux.org/index.php/Sudo

安装桌面环境

  • 安装桌面环境需要的基础包 (就是 xorg 啦)
[root@archiso /]# pacman -S xorg
:: There are 80 members in group xorg:
:: Repository extra
1) xf86-input-evdev  2) xf86-input-joystick  3) xf86-input-keyboard  4) xf86-input-libinput
5) xf86-input-mouse  6) xf86-input-synaptics  7) xf86-input-vmmouse  8) xf86-input-void
9) xf86-video-amdgpu  10) xf86-video-ark  11) xf86-video-ati  12) xf86-video-dummy
13) xf86-video-fbdev  14) xf86-video-glint  15) xf86-video-i128  16) xf86-video-intel
17) xf86-video-mach64  18) xf86-video-neomagic  19) xf86-video-nouveau  20) xf86-video-nv
21) xf86-video-openchrome  22) xf86-video-r128  23) xf86-video-savage  24) xf86-video-siliconmotion
25) xf86-video-sis  26) xf86-video-tdfx  27) xf86-video-trident  28) xf86-video-vesa
29) xf86-video-vmware  30) xf86-video-voodoo  31) xorg-bdftopcf  32) xorg-docs  33) xorg-font-util
34) xorg-fonts-100dpi  35) xorg-fonts-75dpi  36) xorg-fonts-encodings  37) xorg-iceauth
38) xorg-luit  39) xorg-mkfontdir  40) xorg-mkfontscale  41) xorg-server  42) xorg-server-common
43) xorg-server-devel  44) xorg-server-xdmx  45) xorg-server-xephyr  46) xorg-server-xnest
47) xorg-server-xvfb  48) xorg-server-xwayland  49) xorg-sessreg  50) xorg-setxkbmap
51) xorg-smproxy  52) xorg-x11perf  53) xorg-xauth  54) xorg-xbacklight  55) xorg-xcmsdb
56) xorg-xcursorgen  57) xorg-xdpyinfo  58) xorg-xdriinfo  59) xorg-xev  60) xorg-xgamma
61) xorg-xhost  62) xorg-xinput  63) xorg-xkbcomp  64) xorg-xkbevd  65) xorg-xkbutils  66) xorg-xkill
67) xorg-xlsatoms  68) xorg-xlsclients  69) xorg-xmodmap  70) xorg-xpr  71) xorg-xprop
72) xorg-xrandr  73) xorg-xrdb  74) xorg-xrefresh  75) xorg-xset  76) xorg-xsetroot  77) xorg-xvinfo
78) xorg-xwd  79) xorg-xwininfo  80) xorg-xwud

Enter a selection (default=all):

这时会让汝选择需要哪些软件包啦,其实大多数时候默认的就行……

  • 接下来挑一个喜欢的桌面环境包组装上咯~

    (咱这里就只举例 GNOME 、KDE 和 xfce 啦,其他官方支持的桌面环境可以去 https://wiki.archlinux.org/index.php/Desktop_environment_(简体中文) 查看)

    GNOME , 想要 GNOME 全家桶的话带上 gnome-extras

    # pacman -S gnome

    KDE Plasma , 想要 KDE 全家桶的话用 kde-applications-meta 代替 。 kde-applications 会提示汝选择要安装哪些包。

    以及一个显示管理器, KDE 和 sddm 一起使用最好。

    # pacman -S plasma sddm kde-applications

    或者只安装 kdebase 组,包含了一些基本组件(例如文件管理器和终端模拟器)。

    # pacman -S plasma sddm kdebase

    xfce4,xfce 不带显示管理器,所以要装个其他的(例如 lightdm,还要装一个 greeter)

    # pacman -S xfce4 xfce4-goodies lightdm lightdm-gtk-greeter

    桌面环境大多数使用 NetworkManager ,xfce 的话,记得安装 network-manager-applet, 一个控制 NetworkManager 的小工具:

    # pacman -S networkmanager

  • 然后安装中文字体( 同样的方法安装 😋)

    Google Noto Fonts 系列: noto-fonts noto-fonts-cjk noto-fonts-emoji

    思源黑体:adobe-source-han-sans-otc-fonts

    文泉驿:wqy-microhei wqy-zenhei

更多的字体可以在 https://wiki.archlinux.org/index.php/Fonts_(简体中文) 找到。

收尾工作

  • 新建一个用户

    -m 为新用户创建一个目录,-s 设置用户的登录 Shell -G 会把新建的用户追加到某个组中 (这里是刚刚 sudo 里的那个 wheel)

    记得最后是用户名就好 😂

    # useradd -m -G wheel horo

    然后设置密码(记得是输入两次,而且输入过程中不会有显示,不要以为是键盘坏了哦)

    # passwd horo

  • 激活需要的服务,例如一个显示管理器,在例如 gdm :

    # systemctl enable gdm

    当然还有 NetworkManager:

    # systemctl enable NetworkManager

    (这个里面有大写😂)

    systemd 是 Arch Linux 现在唯一的 init 程序,当内核加载完毕后,首先被加载的 就是 init (这里就是 systemd 啦)。然后 systemd 会接着加载剩下的部分。

    https://wiki.archlinux.org/index.php/systemd

  • 设置用户级别的 locale

    用 su 切换到刚建立的用户,然后编辑 ~/.config/locale.conf 修改自己的 Locale ,例如:

LANG=zh_CN.UTF-8
LC_CTYPE="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=

或者可以重启以后用桌面环境的设置程序改 😂

完工啦

  • 离开 chroot 环境:

    # exit

  • 卸载挂载的分区,(其实不是必须的,因为马上就重启啦~)

    # umount -R /mnt

  • 重新启动,准备迎接新的系统吧 ~(>_<~)


欢迎来到 Arch Linux 的世界!

Cheers! 汝刚刚成功的安装 Arch Linux 到汝的电脑中了呐!

那接下来要干啥呢?

by ホロ at July 01, 2020 04:00 PM

June 28, 2020

Alynx Zhou

修复更换 PCI-E 插槽后 Windows 下无线网卡的名称问题

这是一篇其实没什么逻辑也没什么技术的文章,但踩到这个坑又很头痛,所以记录一下。

我用的是一张 PCI-E 的网卡,占据一个 PCI-Ex1 的插槽,对于一个主流的 ATX 主板来说有三个选择,第二条 x16 长度的插槽上面的一个和下面的两个,由于显卡一般会插在第一条 x16 上,所以如果插在第二条 x16 长度上面的那个,会挡住一部分显卡风扇,所以我一开始插在了第二条 x16 长度插槽下面的第一个插槽。这样也是有点问题的,因为这里 x16 和 x1 之间并没有留出一个槽的空位,一般主流显卡又都是双槽的,虽然我不太可能在机箱里塞第二张显卡,但是这么装直接就干掉了这种可能性,所以我一直想把无线网卡换到最底下的插槽。(这一部分比较乱,但是装过机的直接上网搜一下主流 ATX 主板的图就能理解了。)

之前由于最后一个插槽挡板的螺丝拧的太紧我一直没换,今天买了一个柄比较粗比较省力的螺丝刀,终于拧下来螺丝把网卡换了个插槽。开机之后 Linux 下其实没什么问题,检测设备和持久化命名都正常工作了。不过 Network Manager 似乎认为更换了设备,所以建议删掉之前的连接配置重新连接网络,之后一切都正常。

切到 Windows 发现不太对劲,首先就是我的 WiFi 适配器名字变成了 WLAN 2,想要重命名成 WLAN 又提示已经存在,但是我又看不到,点进去发现它选择的无线网卡名字后面多了个 #2 的后缀,总之我也不知道怎么回事,但是强迫症觉得很难受,明明我只有一张网卡啊。

上网搜索了一下,首先是在设备管理器的查看菜单里选择显示隐藏的设备,这样就会发现一个灰色的没有后缀的网卡,我也不知道 Windows 为什么要存一个隐藏的之前存在现在不存在的 PCI-E 设备,首先按照网上的说法卸载掉这个设备然后重启,再开机就没有这个隐藏设备了。

接下来需要去掉 #2 的后缀,让 Windows 把这个插槽里的卡认成唯一的一张,这里就十分晦涩,Windows 试图隐藏起这个逻辑,但导致了一个令强迫症十分不爽的问题。我搜索了一下,发现了一个 百度文库文档 提供的方案。

首先打开注册表编辑器定位到 HKEY_LOCAL_MACHINE\SYSTEM\_ControlSet001\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}\Descriptions 这一项,然后在右侧找到你无线网卡的名称,里面的值应该是 2,改成 1,然后卸载网卡设备,然后重启,应该就解决了,而且因为设备名字恢复了,WLAN 2 也应该变回 WLAN 了。

所以其实这篇文章并没有什么逻辑和技术,但是 Windows 自作主张非要保存一个隐藏的不存在的设备,实在是令人头痛,又没什么明显的解决方式,所以也只好记下来以防再次被恶心到。

更新:Arch Linux CN offtopic Telegram 群组里面叫 Give Way 的网友表示并不需要这么复杂,只要把两个无线网卡的设备都卸载掉重启就可以了。理论上确实是如此,但我没机会再试了,遇到相同问题的朋友可以试试。

Alynx Zhou

A Coder & Dreamer

June 28, 2020 11:02 AM

June 21, 2020

Leo Shen

A Breif Look at Linux's Audio System

You may have never paid attention to the Linux audio system - you install a desktop environment (or, just install a system that ships with a graphical user interface), and the sound just works. Chances are, the sound would be "good enough," and you would take it as it is.

Under the hood though, Linux has quite a complicated audio subsystem. If you are interested in how Linux turns your music file into some physical movement in the air or, if you ran into some trouble with the sound system, and wanted an overview so that you could figure out which part went wrong, here's an introduction for you.

An overview

So, you have a piece of music you want to play. What would you do?

First, we decode the audio file into raw waveform. The audio file may not contain raw waveforms, as it would take up a lot of storage. Audio files like .mp3 and .flac use compression to help reduce file sizes. So by using a decoder, we convert compresed audio into raw waveforms, so that our sound card can understand.

Then, we feed the waveforms into an sound server. For instance, (for some reason) you may want to watch a YouTube video while a Zoom meeting is happening simultaneously, so we will need a sound server that mixes multiple audio streams together, and send them to the sound hardware.

Finally, we feed the mixed audio stream to the sound card driver. Since it is already processed by the sound server, the driver can simply throw the audio stream to the sound card and let it do its job.

How Linux does it

So how does Linux implement such architecture?

ALSA: The sound card driver

To be fair, ALSA is a complete audio architecture (it is called Advanced Linux Sound Architecture after all), but now in many cases, it is just used as a sound card driver. We will stick to the real world case here.

ALSA talks to the hardware directly. It provides an interface so that applications can set specifications that the audio card should use (bit rate, bit depth, all those nerdy stuff) and send audio streams.

A big downside of using ALSA directly is that it is somewhat tricky to let multiple applications play sounds at the same time. Also, it is hard to control volume on a per-application basis. So with most modern desktop environments, a sound server is used for these functionalities.

PulseAudio: The de facto sound server

If you using a Desktop Environment (KDE, GNOME, XFCE, you name it), you may be familiar with the volume control applet on the corner of your screen. This is a tiny but simple frontend for PulseAudio.

PulseAudio has an internal audio mixer, so that it can accept input from multiple applications, mix them together with user-specified volume(s), then send it to the sound card driver (usually ALSA). It also provides an API that integrates nicely within GUI applications (like the volume applet we mentioned above).

But since PulseAudio is aimed for desktop users who don't know much detail about their hardware, it uses an automatic probe system to determine which configuration to use. The result is usually… not great. Also, to save power, the internal mixer usually does not provide the best audio quality.

In short, PulseAudio should be enough for content consuming, as it provides adequate sound quality and doesn't require much knowledge to setup.

JACK: The sound server for the professional

What if we want to do some audio editing, or we just not satisfied with PulseAuio's internal mixer quality?

Well, then JACK is for you! To be fair, JACK is NOT designed to be a general purpose "make the speaker sound" type of sound server. It is designed to be an "Audio Connection Kit," as its name implies. The ideal use case for JACK is, for example, you have a MIDI input, and want to use a software synthesizer on your Linux computer, and then record what you played and send the audio to the headphone out for monitoring at the same time. JACK can connect arbitrary inputs to outputs without quality lose and with very low latency, essentially turn your computer into a giant digital mixer.

Since JACK's internal mixer is designed for professional audio production, it's quality is remarkable. Outside the production community, many people just use JACK as an alternative to a more general purpose audio server, but with greater flexibility and quality. You want to stream your music to your buddies on a video conference? No problem! Just connect the output from the music player to the input of the video conferencing software, and that's it.

Sounds great, eh? So why didn't JACK take over the Linux desktop scene? Well, JACK requires (a lot of) additional work to be set up. You may need to manually select an appropriate bit rate and buffer size that will not overwhelm your sound card, or you will encounter glitchy sounds. Also, its great flexibility could sometimes become its downfall, since it means that user may have to manually connect the source to the desired output.

So what should I use?

PulseAudio should be a good-enough choice for most people. If you encounter any issue with audio fidelity, it may just be due to the fact that PulseAudio failed to detect optimal settings for your sound card, or simply because the sound card's driver implementation is buggy. Take a look at PulseAudio/Troubleshooting on ArchWiki, and you should be good.

If you want to engage in music production, definitely check out JACK. You will need some settings (give JACK realtime privilege, find optimal setting for your sound card, etc.), but after that, JACK's flexibility and quality will impress you.

For some special circumstances, like using an external USB DAC and you don't need any mixing, you can just directly use ALSA. Music players can send audio streams directly (even DSD stream, if you are really into this) to the sound card, and let the sound card do all the fancy job.

June 21, 2020 11:18 AM

frantic1048

PLUM - 条河麻耶 夏季制服

Maya Summer Uniform

近来搬家各种折腾,终于有时候来开箱收到许久的麻耶了。正好 618 活动搞了一对 60W 的 LED,是我想要的那种亮瞎!现在可以不用把手办放在小棚棚里拍也能获得充足的光,也可以随意扭曲身子到达想要的视角了。但是亮也有缺点,太亮了导致根本看不清相机屏幕,不知道到底有没有对上焦,只能用气势去拍了!

虽说习惯了手办总会遇到的各种小问题,但这次的姿势还是超出了我的预想。组装起来之后,发现花束的丝带怎么都凹不出包装盒上的那个造型,总是会过度弯曲搭在裙边上,真是令人头秃 ˊ_>ˋ

Maya Summer Uniform Maya Summer Uniform Maya Summer Uniform

帽子启动!

Maya Summer Uniform Maya Summer Uniform Maya Summer Uniform

吐槽归吐槽,总体的细节来说还是很棒的。

Maya Summer Uniform Maya Summer Uniform Maya Summer Uniform

June 21, 2020 12:00 AM

June 11, 2020

farseerfc

系统中的大多数文件有多大?

你觉得,你的系统中大多数文件大概有多大?

这是一个很有意思的问题,你可以试着先猜一下。

基于对系统中保存文件的了解,可能有这样的思考过程:

  • 我收藏了好多照片,每个有 2~5MiB 吧。
  • 我下载了好多漫画,每个 100KiB 左右,这些大概占了不少比例。
  • 我还收藏了不少动画电影电视剧,虽然这些文件总数量可能不多?
  • 我下载了 Linux 的源码,那里面每个 C 代码文件都几千行,每行 100 字宽,平均也得有 30KiB 吧,有几万个源码文件呢,占比应该挺大的……

问题中「大多数」其实是个挺不精确的称呼,换个精确点的问法:你觉得你的系统中 文件大小的中位数 大概在什么范围内?或者说,文件系统中 文件大小的分布情况 一般是怎样的曲线?

这个问题其实还有多种别的问法,比如:一个常见的桌面或者服务器系统中,多大的文件算大文件, 多小的文件算小文件,什么范围内的大小算是普通呢?

经历过基本的科学教育的人,大概会做这样的基于科学假设的猜测:

  • 统计学上说,大量独立随机事件的累积概率满足正态分布(常态分布)曲线。假设我们把某个特定文件的大小增长 1字节看作是一次独立随机事件,那么文件大小在文件系统中应该是满足正态分布的?
  • 正态分布的前提下,平均数接近中位数,文件系统的已占用大小除以文件数量大概就是大部分文件的大小了吧。
  • 根据我现在文件系统的占用大小和文件数量,平均数大概是 500KiB 左右?
  • 虽然我还存了几个非常大,上 GiB 的文件,但是看起来似乎也有很多很多非常小的文件, 平均一下的话应该会把平均数拉大,大于中位数吧。那么中位数应该在 100KiB 这样的量级附近?

你说为什么要关心这个?因为我经常在网上看到这样的讨论:

「我有个仓库盘要存很多下载到的漫画,每个漫画都是一个文件夹里面一堆 小 JPG ,每个就几十 KiB 。网上看到的说法是 XFS 对 小文件 的性能不那么好,我是不是该换 EXT4 ?我还想在 Windows 上能读写,是不是 ExFAT 这种简单的文件系统更合适一点?」

「软件源的镜像服务器需要存的都是些 小文件 吧,大多数软件包压缩后也就是几个 KiB 到几个 MiB 的量级,这种需求是不是适合用对 小文件 优化比较好的文件系统?」

「我的程序需要分析的数据是大量几百K的 小文件 ,该怎么存合适呢,直接用文件系统还是应该上数据库? 我还想多线程并发分析,是不是 SQL 数据库的并发能力强一些?又或者 MongoDB 的 GridFS 看起来似乎能结合文件系统和数据库的特点,选它应该还不错?」

有没有觉得上面这些讨论和直觉有些出入?如果你的直觉告诉你,上面的讨论似乎很自然的话, 那说明你需要继续看下去了。

好了写了这么多废话给大家思考时间,现在请回答一下我标题中那个问题, 你觉得,你的系统中大多数文件大概有多大? ,接下来我要揭晓答案了。

统计实际系统中文件大小的学术研究

最近看到一个挺早以前的研究报告,是 FAST'11 的最优秀论文奖,研究的课题叫 《A Study of Practical Deduplication》 。这个研究原本是想考察一下在桌面文件系统中「去重」(deduplication)的可行性和潜在收益,作为背景调查, 他们收集了一个挺大的调查样本,记录文件大小和校验和之类的。从论文摘要看,他们在微软公司内, 通过邮件的形式让微软员工在各自的工作机上执行他们的调查程序,大概在1个月左右的时间内收集到了 857 份调查结果。关于去重的研究结果这里我们这里先不深究,只看这个背景调查,他们对收集到的文件大小画了个图表:

file-histogram-4k.jpg

他们结果显示最常见的文件大小是 4K

注意上图里的横轴座标,是按2的指数来给文件大小分类的。比如 128~256 字节的算一类, 4K~8K 字节的算一类,分类之后统计每一类里面文件的数量所占比例,也就是说横轴座标是指数增长的。 在指数增长的横轴座标上,画出的曲线才看起来像是正态分布的曲线,如果把横轴座标画成线性的话, 中位数会出现在非常靠近左侧小文件的地方。

也就是说根据他们的统计,文件系统中大部分文件都是大概 2K 到 8K 这样的范围,最常见 4K 大小。 非常大的比如 8M 以上的文件只是极个别,位于图表右侧非常长的尾巴中。

其实我对这个结果还不是很惊讶,因为我记得在 2000 年左右,当我家的电脑还在用 Windows 98 跑在 40G 的 FAT32 文件系统中的时候,读到过一篇介绍 NTFS 的「新」特性的文章。那篇文章讲到 FAT32 的簇大小随着分区大小增长,越来越大的簇大小对保存大量小文件极其浪费,而 NTFS 用固定的 4K 簇大小可避免这样的浪费,并且 1K MFT 记录甚至能「内联(inline)」存储非常小的文件。 为了证明大量小文件对文件系统是个现实存在的问题,那篇文章也提到了常见系统中的文件大小分布曲线, 提到了大部分文件都是 4K 大小这有点反直觉的结论。

这次这个研究让我觉得吃惊的是,文件大小分布并没有随着硬盘大小的增加而增加,稳定在了 4K 这个数字上。 他们以前还进行过两次类似的统计,分别在 2000 年和 2004 年,图中的点线画出了历史上的统计分布,实线是 2009 年的最新统计。三年获得的统计结果的曲线基本吻合,这意味着随着存储容量增长,文件大小的分布几乎没有变化。

正当我疑惑,这种文件大小不变的趋势,是否是因为微软公司内特定的操作系统和工作内容, 在别的系统上或者在更长的时间跨度上是否有类似的趋势呢?这时演讲的幻灯片翻了一页:

file-histogram-4k-since1981.jpg

从早在 1981 年起,有研究表明文件系统中文件大小中位数就稳定在了 4K

在他们论文的参考文献中,能找到 这个 1981 年的研究 。这篇早年的调查是在 DEC 的 PDP-10 机器上,使用 TOPS-10 操作系统。从现在的视点来看,被调查的 TOPS-10 的文件系统已经可以说非常初级了,没法支持很大的文件或者很多的文件, 然而即便如此常见文件大小也还是非常符合现代系统中得到的结果。

微软的研究者们还回顾了计算机科学领域多年的相关研究,结论是常见文件大小这个值在 1981 到 2009 这近 30 年中都非常稳定。演讲的原文中这么评价:

…… the median file size is 4k. It was 4k the other two years of the study. We've actually gone back through the literature. It turns out it's 4k in every study going back to the last 30 years. So this is great news. We can finally compete with physicists: we have our own fundamental constant of the universe, it's a medium file size ……

文件大小中位数是 4K 。在前几年的两次研究中它也是 4K 。其实我们回顾了既往的学术研究,发现在过去30 年中每个研究都说它是 4K 这个值。这是个好消息,我们终于有了一个堪比物理学家的结论:我们有我们自己的 宇宙基本常数了,是文件大小中位数。

这个结论很有意思,文件大小中位数在计算机科学领域的稳定程度堪比宇宙基本常数: 4K

很明显这是在调侃,文件大小这种变化很大的数字显然和文件系统内存储的内容直接相关, 存游戏的可能不同于存音乐的。但是这调侃的背后也有一定真实性:文件系统中保存的文件, 除了用户直接使用的那些视频、文档、代码,还有大量文件是程序内部创建使用的,比如浏览器的缓存和 cookie ,这类不被用户知晓的文件可能在数量上反而占据绝大多数。 于是从文件系统这边来看,大多数文件都是在 4K 左右的数量级,更大的文件是少数。

不信?你可以测一下自己的文件系统

我也想测一下我的文件系统中文件大小的分布情况,于是稍微写了点代码测量和画图。如果你也想知道你的系统中 文件大小的分布,那么可以像我这样测。

首先用 find 命令统计一下每个文件的大小,输出到一个文件里:

find /home -type f -printf "%s %p\n" > myhome.txt

上述命令对 /​home 中的所有普通文件而忽略文件夹和符号链接之类的( -type f ),输出文件大小字节数和文件路径( -printf "%s %p\n" )。 如果文件名路径中有特殊符号可能之后比较难处理,那么可以 -printf "%s\n" 忽略路径。

然后用 Python 的 Matplotlib 和 NumPy 对收集到的文件大小数据画个直方图(histogram)。 以下 filesizehistogram.py 脚本在这儿 能下载到。

#!/usr/bin/python3
import argparse
import matplotlib.pyplot as plt
import numpy as np
import sys
from math import *
from bisect import bisect_left


def numfmt(s):
    marks = "KMGTP"
    m = 0
    f = type(s) is float
    while s >= 1024 and m < len(marks):
        if f:
            s /= 1024.0
        else:
            s //=1024
        m += 1
    if f:
        return f"{s:.2f}{marks[m-1:m]}"
    else:
        return f"{s}{marks[m-1:m]}"

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        prog = "filesizehistogram",
        description = """
            can use "-" as input filename, indicate input is taken from stdin.
            otherwise input file should be a result of "find -printf \'%s %p\\n\'"
            """
    )
    parser.add_argument('-o', '--output', help="output filename, will recognize common extensions by matplot")
    parser.add_argument('input', nargs='+',  help="input filenames")
    args = parser.parse_args()

    filenames = [x if x != '-' else '/dev/stdin' for x in args.input]
    data=np.array([int(x.split(' ')[0]) for fn in filenames for x in open(fn)])
    mindatalog2 = 5 # cut from 32
    maxdatalog2 = min(ceil(log2(data.max())), 31) # cut at 1G and above
    # bins [0, 1, 32, 64, 128, 256, ... , 1G, 2G] , last bin is open range
    bins=[0,1,] + [2**x for x in range(mindatalog2, maxdatalog2 + 1)]

    median = float(np.median(data))
    mean = float(data.mean())
    bmedian = bisect_left(bins, median) - 1
    bmean = bisect_left(bins, mean) - 1
    files = len(data)
    total = data.sum()

    hist, bin_edges = np.histogram(data,bins)
    fig,ax = plt.subplots(figsize=(20,8))
    ax.bar(range(len(hist)), hist, width=0.9)
    ax.set_xticks([i for i in range(len(hist))])
    tickbar = "┊\n"
    ax.set_xticklabels([f'{tickbar*(i%3)}{numfmt(bins[i])}~{numfmt(bins[i+1])}' for i in range(len(hist)-1)] +
                    [f"{numfmt(bins[len(hist)-1])}~"])

    ax.axvline(bmean, color='k', linestyle='dashed', linewidth=1)
    ax.axvline(bmedian, color='r', linestyle='dashed', linewidth=2)
    min_ylim, max_ylim = plt.ylim()
    min_xlim, max_xlim = plt.xlim()
    ax.text(bmean + 0.5   , max_ylim * 0.9, f'Mean: {numfmt(mean)}')
    ax.text(bmedian + 0.5 , max_ylim * 0.9, f'Median: {numfmt(median)}', color='r')
    ax.text(max_xlim * 0.8, max_ylim * 0.9, f'Files: {files}')
    ax.text(max_xlim * 0.9, max_ylim * 0.9, f'Total: {numfmt(float(total))}')

    for i in range(len(hist)):
        ax.text(i - 0.5, hist[i] + files / 400, f"{hist[i]:5}") # label on top of every bar, uplefted a little

    if args.output:
        plt.savefig(args.output)
    else:
        plt.show()

然后就能 ./​filesizehistogram.py myhome.txt 这样画出一张图。以下是我一台机器上根目录 /​ 和家目录 /​home 放在一起的结果:

myroot.png

图中我用点线标出了中位数(median)和平均数(mean)大小的位置,可见在我的文件系统中, 文件大小中位数在 2.24K ,平均数是 88.09K ,512~8K 范围内的文件数量加在一起超过了文件总数一半。文件数量最多的范围是 1K~2K ,大概因为我家里存了好多源代码。还有一个小突起在 64K~128K ,这堆主要是我收藏的漫画 JPG 文件。

图的横座标和上面微软的研究类似,用2倍增长的bin统计文件数量。 不过稍微修改了一下,因为我想知道 0 大小文件的个数,还想把 1~32 和 1G~ 以上这两个曲线底端的尾巴放在一起统计。图的纵座标是文件数。

也可以用这个来画你感兴趣的文件夹的文件大小分布,比如用 linux 内核代码树画出来的图大概这样:

linux-filesize.png

linux 代码树的文件大部分比我猜的 30K 要小呢,主要在 1K~16K ,中位数 3.28K 。而且意外得在代码树里有好几个 0 大小的文件,看了几个文件路径确认了一下,它们的确是 0 大小的头文件,并不是我的文件系统丢了文件内容。

结论

有没有觉得「文件大小的中位数是 4K 」这个结论出乎意料呢?

你在用的系统中文件大小的分布曲线又是什么样的呢?欢迎留言告诉我。(贴图可以用 https://fars.ee/f 图床呀)

知道了文件大小分布的规律,就会发现设计文件系统的时候,需要考虑两个极端情况: 既要照顾到文件系统中数量很少而大小超大的那些文件,又要考虑到这么多数量众多而大小只有数 K 的文件。也会发现,对于文件系统而言,超过 16K 的文件就绝不会被算作是「小文件」了,而文件系统设计中说的 「小文件优化」针对的通常是更小的文件大小。并且这一趋势并不会随着存储设备容量增加而改变, 不能妄图通过随着容量逐步增加文件分配「簇」大小的方式,来简化文件系统设计。

那么众多文件系统实际是如何满足这些极端情况的呢?待我有空再细聊……

by farseerfc at June 11, 2020 06:45 AM

June 09, 2020

百合仙子

终端色彩总结

本文来自依云's Blog,转载请注明。

终端转义序列里有一个 SGR 代码用于表示「粗体或者增加强度」的字体渲染。相当多的终端将其实现作更明亮的颜色而非粗体字,也有很多终端应用程序如此(比如 mutt 中的 brightXXX 颜色使用此代码;新一点的 mutt 支持使用 lightXXX 来表示亮色了)。

然而最近,GNOME 终端决定将这个「或」字去掉,让 SGR 代码 1 表示粗体,亮色使用另外的颜色代码来表示。这无疑导致了我看到的许多软件里出现粗体字。有时候看着还行(比如 systemd、htop 中的),有时候会让人觉得很不舒服(比如 zsh 里打着命令,字体一会儿变粗一会儿变普通粗细)。我就认真去查了一下相关信息,作出相应的配置更改,顺便做个总结笔记好了。

转义序列

「选择图形展现」(Select Graphic Rendition; SGR)序列用于设置字体的渲染方式。其格式为CSI (code ";")* code m。CSI 就是 ESC "["。一个序列里可以给出多个代码,使用分号分隔。如果一个都不给出,当作代码 0 解释。

八色

这是最基础的八种颜色,代码 30-37 设置前景色,40-47 设置背景色。

16色

这就是大多数终端设置里,那个14色的调色板所指定的色彩了。为什么只有14色呢?因为默认的前景色和背景色是单独的设置项。

如序言中所说,相当多的终端将代码 1 解释为亮色,也就是上述八色再加上对应的明亮版本。

自 GNOME 终端此次改革之后,代码 1 只管粗体。要亮色,使用代码 90-97 设置前景色,100-107 设置背景色。除了 GNOME 终端外,也有一些其它终端跟进,比如 alacritty。有些终端保留了将代码 1 解释为亮色的选项,而 GNOME 终端最近去掉了这个选项(但是还有一个将粗体字显示为亮色的选项)。

256色

16色怎么够?于是出现了256色。

ESC[38;5;<n>m用于设置前景色,ESC[48;5;<n>m用于设置背景色。其中,n 是一个数,0-7 同代码 30-37,是那最初八种颜色。8-15 是它们的亮色变种。16-231 是216种彩色,在RGB空间构成了一个6x6x6的立方体。232-255 是24个灰阶色彩。

一些标准中,这中间的分隔符使用冒号。

真彩色

现代显示器普遍使用8位值表示RGB中每一个通道、共计24位了,终端也不能落后呀。于是有些终端也支持了这个被称为「真彩色」的24位色彩了。

代码格式是256色的扩充。ESC[38;2;<r>;<g>;<b>m来设置前景色,ESC[48;2;<r>;<g>;<b>m设置背景色。这里的 r、g、b 当然就是RGB色彩空间的值了,每一项的取值范围是 0-255。

测试脚本

整合了我从各处收集到的展示方式,写了一个脚本,可以直观地感受终端对各种色彩的支持情况:

colors.py

参考资料

by 依云 at June 09, 2020 06:39 AM

May 22, 2020

ヨイツの賢狼ホロ

Chromium OS 和 Crostini 初体验

日常动机不明瞎折腾……

所以 Chromium OS 是啥?

Chromium OS是Google Chrome OS的开放源代码开发版本。 自2009年11月19日开始,Chrome OS以Chromium OS为名陆续发布其开发源代码, 并在遵守着BSD授权条款不断有新版本发布,并试图能够提供绝大多数长时间浏览 万维网的用户一个快速、方便且安全的操作系统。

历史上Chromium OS曾经整体是创建在以Linux核心为主的Ubuntu 4.10版本上,而操作系统的的软件包管理系统则是使用Gentoo Linux的Portage。 但是现在实际上只是单纯利用了Gentoo Linux 的 Portage 而独立编译出来的特制化 GNU/Linux 操作系统,而这个系统本身也与 Gentoo Linux 无关。

Wikipedia: Chromium OS

愿意的话当作一个核心是 Chromium 浏览器的操作系统也行……

Chromium OS 大概像这个样子

于是 Chromium OS 大概像这个样子(嗯?)

先有装到硬盘上的 Chromium OS

要安装 Chromium OS 的话, 可以参照官方的说明自己编译 。 或者也可以捡现成的,例如 https://chromium.arnoldthebat.co.uk/index.php

下载来是一个 raw img 映像,用 Rufus / Etcher 甚至 dd 啥的找个 U 盘写进去, 再从 U 盘启动电脑就能试用啦……

如果汝和咱一样是捡现成的然后还 想要安装的话,按下 Ctrl + Alt + F2 进入 Developer shell,以 chronos 用户登录。 运行下面的命令就好啦(然而这会把汝选择的硬盘上的所有内容抹掉,小心操作)

sudo /usr/sbin/chromeos-install --dst /dev/sda

当汝看到 "Please shutdown, remove the USB device, cross your fingers, and reboot." 这一句时就表示安装成功啦,可以重新启动然后拔掉 U 盘啦……

企鹅开采铬铁矿 - Chromium OS 中的 GNU/Linux 容器

Crostini 是 Google的总括术语,用于使 GNU/Linux 应用程序支持易于使用, 并与Chrome操作系统很好地集成。

Google 官方的配置指南在这里。

要启用 Linux (Beta 版)的话,首先要从设置里把这个选项打开,启动的时候 会下载一定量的文件。

启动 Chromium OS 中的 Crostini

配置完成之后起动器中会增加一个终端应用,日后安装的有 Desktop Entry 的 GNU/Linux 应用 会自动添加到那个 “Linux 应用” 文件夹里。

启动 Chromium OS 中的 Crostini

刚刚启动的地方也能共享文件夹和 USB 设备给容器:

容器设置

不过当然缺少的功能还是有的 ( 依官方的说法的话 ):

  • 除了 USB 以外的外部设备
  • 图形加速
  • 硬件加速解码
  • 和输入法(虽然能通过某些奇怪的技巧绕过)

因为咱把容器换成 Arch 啦,于是……

在默认的 Debian 容器上安装中文输入法的话可以 参考这篇文章

替换 Crostini 的容器

因为 Crostini 其实就是一个定制的带 LXC/LXD 的容器(大概吧,要是咱说 错了的话麻烦给咱一个改正的机会……),要把默认的 Debian 容器换掉也是比较容易的(嗯?)

想换成 Fedora 的话 可以参考这篇文章。

想和咱一样换成 Arch 的话可以 参考 ArchWiki 的那篇文章。 具体的操作大致如下:

按 Ctrl + Alt + T 打开 Crosh (Chromium OS 在 Chromium 里的 shell), 然后:

vmc container termina arch https://us.images.linuxcontainers.org archlinux/current

这个命令运行完后肯定会因为某个错误失败:

Error: routine at frontends/vmc.rs:403 `container_setup_user(vm_name,
user_id_hash,container_name,username)` failed: timeout while waiting
for signal

不要方(慌?)。在刚才的 crosh 里打开 Crostini 的容器里的 Shell :

crosh> vsh termina
(termina) chronos@localhost ~ $

列出容器列表(可能需要几分钟才能看到刚建立的容器)

lxc list

启动新建立的 Arch Linux 容器,以及在容器里打开一个 Shell:

lxc start arch

lxc exec arch -- bash

在 Arch Linux 容器的 Shell 里需要完成这些操作:

  • 创建用户(UID 1000 那位),以及修改 sudoers 配置(记得用 visudo)。
  • 为了和 Chroium OS 的集成,安装 wayland xorg-server-xwayland 包,以及从 AUR 安装 cros-container-guest-tools-git 。记得启动相应的服务。
$ systemctl --user enable --now sommelier@0 sommelier-x@0 sommelier@1 sommelier-x@1
  • 可以的话装些别的喜欢的,像是中文字体或者编译工具什么的。(就像新装一个 Arch 一样啦)

最后在 Termina 的 Shell 里把新建的容器的名称改回 penguin (lxc rename arch penguin), 再重新启动容器。应该就能在 Chromium OS 起动器的终端里启动 Arch Linux 容器啦。

利用奇怪的技巧增加输入法支持(fcitx 啦)

首先把 fcitx 、相应的输入法模块(偷懒的话就 fcitx-im)和输入法(例如 fcitx-rime) 装上。然后用配置工具(例如 fcitx-configtool)或者编辑配置文件的方法设置好输入方案。 (以及记得换个组合键,原来的 Ctrl + Space 已经被 Chromium 的输入法切换抢了狸……)

然后编辑 /etc/systemd/user/cros-garcon.service.d/cros-garcon-override.conf 文件,加上相应的环境变量:

Environment="GTK_IM_MODULE=fcitx"
Environment="QT_IM_MODULE=fcitx"
Environment="XMODIFIERS=@im=fcitx"

编辑 ~/.sommelierrc ,让容器在启动时运行 fcitx:

/usr/bin/fcitx-autostart

然后重新启动一下容器,是不是能在 Linux 应用里输入中文啦?

就像这样

但是不知道为啥 Telegram Desktop 还是不吃这一套(emmmm)

从 Chromium 访问容器的服务

如果容器里的服务监听了 0.0.0.0 的话,在 Chromium 里可以通过 访问 penguin.linux.test 访问到。

就像这样

没错这篇文章就是在这台 ”Chromebook“ 上写出来的……

蓝色洗彩色(大雾) - 为 Chromium OS 加入 Chrome OS 功能

和 Chromium OS 相比,Chrome OS 有 Google Play 支持,可以运行 Play 商店上的应用。 以及可以使用 Google 账户连接 Android 设备完成一些像是通过手机收发短信或者 Chromebook 打开手机热点之类的操作。

汝要是来了兴趣的话, 那直接去买台 Chromebook 啊…… (笑)

如果只是想拿来尝试一下的话,有个名字叫做 Chromefy (也叫 Project Croissant) 的项目可以把 Chrome OS 的组件塞进一个 Chromium OS 映像里面。具体的操作方法去看他们的 README 啊 ……

咱是试过一次之后发现运行 Android 应用时 Core m5 不太能 Hold 的住就回去 Chromium OS 啦。

by ホロ at May 22, 2020 04:00 PM

May 19, 2020

百合仙子

桥接无线网卡!

本文来自依云's Blog,转载请注明。

众所周知,大部分无线网卡是不支持桥接操作的。

但是 VirtualBox 就是能,因为它做了特殊处理:来回改 MAC。

那么,我的 LXCnetnsKVM 啥的也想这么玩,成不?

实际上不仅能成,而且 Debian Wiki 还给出了两个方案。方案一是用 ebtables 来回改 MAC。不过我失败了,可能是 ebtables 不支持改完 MAC 再把包发往另外的网络接口吧。

方案二是内核的一个叫 Proxy ARP 的功能。设置起来超级简单:往/proc/sys/net/ipv4/conf/all/proxy_arp里写1,然后给需要的 IP 地址加一条 /32 路由项就可以了。

这方案相比起 VirtualBox 来是非常手动了,也不支持 DHCP 自动配置的 IP 地址,但好歹能用。至少微信备份能用。(火狐的 Wi-Fi 远程调试已经坏掉了,倒是那个「USB 调试」其实只要 adb 连接上就能用,不一定要走 USB 线。)

by 依云 at May 19, 2020 07:02 AM

May 14, 2020

ヨイツの賢狼ホロ

Windows Subsystem for Linux + DistroLauncher

利用 DistroLauncher 在 WSL 上运行自己喜欢的 GNU/Linux 发行版, ,例如还是咱喜欢的 Arch 🌝

复习(?):Windows Subsystem for Linux 是个啥玩意?

从 Windows 10 Insider Preview 开始,加入了 Windows Subsystem for Linux (适用于 Linux 的 Windows 子系统) 功能.

Windows Subsystem for Linux(简称WSL)是一个为在Windows 10上能够原生运行 Linux 二 进制可执行文件(ELF 格式)的兼容层。 它是由微软与 Canonical 公司合作开发,目标是使纯正的 Ubuntu 映像能下载和解压到用户的 本地计算机,并且映像内的工具和实用工具能在此子系统上原生运行。

时光荏苒, WSL 本身也有了第二次演进。 最初的 WSL 提供了一个微软开发的 Linux 兼容内核接口(不包含Linux代码),来自 Ubuntu 的用户模式二进制文件在其上运行。

WSL 2 中的 Linux 内核是根据最新的稳定版分支(基于 kernel.org 上提供的源代码)在内部 构建的。此内核专门针对 WSL 2 进行了优化。 (汝大概可以当作 M$ 专门整了个 Linux 内核 然后再塞进一个专门的虚拟机里来运行 WSL 和下面的一堆发行版)。

有关 WSL 1 和 2 的区别可以看 M$ 官方的对比图。

WSL 的具体应用就是 Bash on Ubuntu on Windows 啦,在 Windows 上实现了一个 Ubuntu 子 系统。(当然后面也支持了几个别的发行版)

开始之前

这不是一篇教授如何使用 WSL 的教程,所以汝应该先把 WSL 装上,然后有一个可用的发行版。

接下来去把 Visual Studio 装上(担心空间不够就只先安装一个编辑器, 接下来打开 DistroLuncher 的时候应该会提醒汝装剩下的)

接着把 DistroLauncher 下载(或者克隆下来),在 Visual Studio 里打开解决方案。

准备发行版 tarball

考虑到 Microsoft Store 里有好几个打包好的发行版了(像是 Ubuntu/Debian/Fedora/ openSUSE 啥的),咱还是拿咱自己中意的 Arch 举例好了。

以及可以参考 ArchWiki 上从现有 GNU/Linux 发行版安装的那一节。

然后用汝手上现有的 WSL 发行版来完成接下来的操作。(咱个人比较中意 Debian)

首先下载 Bootstrap 用的 tarball ,并把它解压到哪里去。(例如 /tmp 或者 /root )

# tar xzf <path-to-bootstrap-image>/archlinux-bootstrap-*-x86_64.tar.gz

用文字编辑器打开 /root.x86_64/etc/pacman.d/mirrorlist ,选取一个镜像仓库。

如果汝所使用的发行版有 Bash 4,而且 unshared 支持 --fork 和 --pid 选项的话,可以 直接用 tarball 里面的 arch-chroot 命令进入 chroot 环境:

# 为了避免 error: could not determine cachedir mount point /var/cache/
  pacman/pkg 错误,要把 tarball 的目录原地绑定挂载一遍。
  (当然汝也可以通过修改 /etc/pacman.conf 关掉安装前的剩余空间检查来绕开
  这个问题,不过不推荐就是了……)
# mount --bind /tmp/root.x86_64 /tmp/root.x86_64
# /tmp/root.x86_64/bin/arch-chroot /tmp/root.x86_64/

不然传统的方法也是能用的,例如:

# mount --bind /tmp/root.x86_64 /tmp/root.x86_64
# cd /tmp/root.x86_64
# cp /etc/resolv.conf etc
# mount -t proc /proc proc
# mount --make-rslave --rbind /sys sys
# mount --make-rslave --rbind /dev dev
# mount --make-rslave --rbind /run run
# chroot /tmp/root.x86_64 /bin/bash

初始化一下 pacman 的密钥环:

# pacman-key --init

# pacman-key --populate archlinux

更新,和安装一些别的软件包,看汝自己的需要啦(--needed 选项会跳过已是最新的 软件包而不是重新安装它们):

# pacman -Syu base base-devel nano --needed

如果有需要的话,汝也可以在这个时候干些别的,例如修改 /etc/sudoers 或者把 linux 包卸载掉之类的 (反正也用不上嘛……)

接着把修改好的 tarball 打包:

# tar --numeric-owner -cvzf /path/to/your/arch.tar.gz /tmp/root.x86_64/

然后把 tarball 复制到汝的 DistroLauncher 下面备用,名字起作 install.tar.gz 。

  • 比较新的 Windows Insider Preview 的话能通过文件资源管理器直接访问 WSL 的 文件系统,大概像这个样子:
WSL on Windows
  • 不然的话,在 WSL 里复制出来也是 OK 的:

    # cp arch.tar.gz /mnt/c/Users/Horo/source/repos/WSL-DistroLauncher/x64/install.tar.gz

修改 DistroLauncher

在 Visual Studio 里打开 DistroLauncher-Appx/MyDistro.appxmanifest , 、 修改一些基本属性(像是名字啥的)。不过别忘了在 Packaging 那里选择一个 打包用的测试证书(不管是现成的还是临时创建一个都 OK)

Packaging in Manifest

打开 Launcher 下面的 DistributionInfo.h ,修改一些和汝的发行版相关的信息:

namespace DistributionInfo
{
    // The name of the distribution. This will be displayed to the user via
    // wslconfig.exe and in other places. It must conform to the following
    // regular expression: ^[a-zA-Z0-9._-]+$
    //
    // WARNING: This value must not change between versions of your app,
    // otherwise users upgrading from older versions will see launch failures.
    // 在 WSL 中区别汝的发行版的名称,如果汝有计划分发自己制作的发行版,记得之后
    // 不能修改它。
    const std::wstring Name = L"Arch";

    // The title bar for the console window while the distribution is installing.
    // 在运行时命令提示符窗口上方的标题。
    const std::wstring WindowTitle = L"Arch Linux";

    // 下面两个函数在 DistributionInfo.cpp 里,默认是适合类 Ubuntu 系统的,
    // 稍后也会修改。

    // Create and configure a user account.
    // 初始化时创建新用户的函数。
    bool CreateUser(std::wstring_view userName);

    // Query the UID of the user account.
    // 查询 UNIX 用户 ID 的函数
    ULONG QueryUid(std::wstring_view userName);
}

打开 Launcher 下面的 DistributionInfo.cpp ,接着修改和汝的发行版相关的信息: (关键大概就是上面有提到的那个 CreateUser 函数)

bool DistributionInfo::CreateUser(std::wstring_view userName)
{
    // 创建用户账户的函数。
    DWORD exitCode;
    // std::wstring commandLine = L"/usr/sbin/adduser --quiet --gecos '' ";
    // Arch 这边没有 adduser ,所以用 useradd 和 passwd 代替。
    // 记得 commandLine 是要和用户名拼起来的,所以最后面要有个空格。
    std::wstring commandLine = L"/usr/bin/useradd -m -s /bin/bash ";
    commandLine += userName;
    HRESULT hr = g_wslApi.WslLaunchInteractive(commandLine.c_str(), true, &exitCode);

    commandLine = L"/usr/bin/passwd ";
    commandLine += userName;
    hr = g_wslApi.WslLaunchInteractive(commandLine.c_str(), true, &exitCode);
    if ((FAILED(hr)) || (exitCode != 0)) {
        return false;
    }

    // 把用户添加进合适的组中,这个也要看不同的发行版调整。
    // commandLine = L"/usr/sbin/usermod -aG adm,cdrom,sudo,dip,plugdev ";
    commandLine = L"/usr/bin/usermod -aG wheel ";
    commandLine += userName;
    hr = g_wslApi.WslLaunchInteractive(commandLine.c_str(), true, &exitCode);
    if ((FAILED(hr)) || (exitCode != 0)) {

        // 如果前面的两条命令失败的话,如何删除用户?
        // commandLine = L"/usr/sbin/deluser ";
        commandLine = L"/usr/bin/userdel ";
        commandLine += userName;
        g_wslApi.WslLaunchInteractive(commandLine.c_str(), true, &exitCode);
        return false;
    }

    return true;
}

如果汝有意愿换个图标的话,把图标放进 /images 文件夹里。

编辑 DistroLauncher-Appx/DistroLauncher-Appx.vcxproj 文件,修改汝的发行版 的可执行文件的名称(例如 arch.exe ?)

<PropertyGroup Label="Globals">
 ...
    <ProjectName>mydistro</ProjectName>
</PropertyGroup>

检查 DistroLauncher-Appx/MyDistro.appxmanifest 文件,确保……

  • <desktop:ExecutionAlias Alias="mydistro.exe" /> 是汝刚刚决定的名字,例如 <desktop:ExecutionAlias Alias="arch.exe" />
  • 以及每一个 Executable 的值也是那个名字,例如 <Application Id="mydistro" Executable="arch.exe" EntryPoint="Windows.FullTrustApplication">

构建和测试

用 Visual Studio 的“部署解决方案”生成 Appx:

部署解决方案

假如一切顺利的话,新的应用会出现在汝的开始菜单里面:

新的应用会出现在汝的开始菜单里面

打开然后创建 UNIX 用户试一下?

效果?

by ホロ at May 14, 2020 04:00 PM

May 11, 2020

百合仙子

Linux 的进程优先级与 nice 值

本文来自依云's Blog,转载请注明。

假设有一个程序叫 use-cpu,它运行的时候会一直消耗一个 CPU 核心。试问,如果我开两个终端窗口,分别执行以下两个进程,其 CPU 会如何分配?

$ taskset 2 ./use-cpu
$ taskset 2 nice -n 10 ./use-cpu

两个进程都在1号CPU上运行,意味着它们必定争抢时间片。第二个进程使用 nice 命令设置其 nice 为 10,那么,是不是第二个进程会占用比较少的 CPU 呢?

很合情合理的推理,然而答案是否定的。

呃,也不一定。cat /proc/sys/kernel/sched_autogroup_enabled 看一下,这个开了的话,CPU 调度会多一个层级。默认是开的,所以这两个进程会均分 CPU 时间。

首先说好了呀,这里只讨论普通进程(SCHED_OTHER)的调度。实时进程啥的不考虑。当然啦,CPU 分配只发生在 R(Runnable)状态的进程之间,暂时用不到 CPU 的进程不管。

最上面的层级是 cgroups 了。按照 cgroup 层级,每一层的子组或者进程间按权重分配。对于 cgroups v1,权重文件是 cpu.shares;cgroups v2 则是 cpu.weight。不管用哪个版本的 cgroups,systemd 默认都没有做特别的设置的。对于 v1,大家全在根组里;对于 v2,CPU 控制器都没有启用。如果你去设置 systemd 单元的 CPUWeight 属性(或者旧的 CPUShares 属性),那么 systemd 就会开始用 cgroups 来分配 CPU 了。一个意外的状况是,使用 cgroups v2 时,systemd-run 不会自动启用上层组的 CPU 控制器,以至于如果上层未手动启用的话,设置不起作用。在使用 cgroups v1 时,用 systemd 设置 CPUWeight 或者 CPUShares 也不太好用,因为它并不会自动把进程挪到相应的层级里去……

其次就是这个默认会开的 autogroup。这个特性是为了提升交互式的桌面系统的性能而引入的,会把同一 session 的进程放一个组里。每个 autogroup 作为一个整体进行调度,每个 autogroup 也有个 nice 值,在 /proc/PID/autogroup 里可以看到,也可以 echo 一个想要的 nice 值进去。至于这个 session,不是 systemd 的那个 session,而是传统 UNIX 的那个 session,也是 setsid(2) 的那个 session。我一直以为它没多大作用,没想到在这里用上了。基本上,同一群进程会在同一个 session 里(比如一群火狐进程,或者一群 make 进程),同一个终端里跑的进程也都在一个 session 里(除非你 setsid 了)。

最后才是轮到进程。其实准确地讲是线程。同一个 autogroup 里的时间片如何分配,就看里边这些线程的 nice 值的大小了。所以其实,我系统里的那些高 nice 值的进程,由于 autogroup 的存在而它们又没有去设置 autogroup 的 nice 值,其实调度起来没什么差别的。

参考资料

  • man 7 sched
  • man 7 cgroups

by 依云 at May 11, 2020 10:21 AM

May 07, 2020

百合仙子

Intel GVT-g 初体验

本文来自依云's Blog,转载请注明。

准备 GVT-g

把 kvmgt vfio-iommu-type1 vfio-mdev 这仨加到 /etc/mkinitcpio.confMODULES 数组里去。mkinitcpio -P 重新生成一下 initramfs。

添加内核参数 i915.enable_gvt=1。比如是 grub 引导就去改 /etc/default/grub 里的 GRUB_CMDLINE_LINUX 变量,然后 grub-mkconfig ...

去把 /etc/systemd/system.conf 里的 DefaultLimitMEMLOCK 给改了。比如 DefaultLimitMEMLOCK=65536:1073741824

重启。

这个时候应该已经有 /sys/devices/pciXXXX:XX/XXXX:XXXX.X/mdev_supported_types 这个目录了。里边有好几个选项呢。选择一下合适的(查看 description 文件),然后往里边的 create 文件里写一个 UUID 就创建了。

启动 KVM 虚拟机

呃,如果你还没有磁盘镜像就自己 qemu-img 创建一个,然后装机。如果你有别的虚拟机的,也可以用 qemu-img 去转格式。

另外准备一下网络。我早就有个网桥了,所以直接用它了。在 /etc/qemu/bridge.conf 里写一句 allow br0 不然不给用的,毕竟我是普通用户权限而网络接口是要 root 权限操作的,得明确允许一下。

我尽可能地使用了 virtio,据说性能好(VirtualBox 也支持一部分了呢)。如果用已有的虚拟机系统但以前没用过 virtio 的话,记得用 fallback 那个 initramfs 启动,然后进系统之后重新生成一个。

我给分配了四个逻辑 CPU 核,4G 内存。VGA 要关掉,不然两个显卡用起来麻烦。为了避免部分内容显示到别处去(如果关了 VGA 的话就看不到,否则能在默认的那个上看到),要加上 ramfb=on,driver=vfio-pci-nohotplug 选项。

声音当然是要的。添加个 PulseAudio 后端,一张 HDA 声卡。我不懂声卡型号所以找了个顺眼的,能用就好。

合起来是这样子的(那两个省略号,一个是磁盘镜像路径,一个是创建 vGPU 用的 UUID):

#!/bin/bash -e

ulimit -l 1024000

exec qemu-system-x86_64 -enable-kvm \
       -name "ArchKDE" \
       -cpu host -smp 4 \
       -m 4G \
       -drive file=/.../ArchLinuxKDE.qcow2,if=virtio \
       -netdev bridge,id=eth0,br=br0 \
       -device virtio-net,netdev=eth0 \
       -device vfio-pci,sysfsdev=/sys/bus/mdev/devices/...,display=on,x-igd-opregion=on,ramfb=on,driver=vfio-pci-nohotplug \
       -vga none \
       -display gtk,gl=on \
       -audiodev pa,id=pa0,server=/run/user/$UID/pulse/native -device intel-hda -device hda-output,audiodev=pa0 \
       "$@"

如果你使用 GVT-g 显卡的时候整个系统都卡卡卡的话,去看一下宿主的内核日志,是不是有 vfio_pin_page_external: Task qemu-system-x86 (257364) RLIMIT_MEMLOCK (104857600) exceeded 这样的提示,然后去把 RLIMIT_MEMLOCK 给调大,大到它不再报这个错为止。我最后给了1000M才终于不报错地把 KDE 给跑起来了(默认是64K)。

当然如果你没有 GVT-g 支持的话,去掉那行配置,然后 -vga virtio 也能用。

参考链接

by 依云 at May 07, 2020 10:30 AM

May 06, 2020

frantic1048

PLUM - 香风智乃 夏季制服

原本看到假期智乃到了可以早早拍个爽的,结果著名的 Enermax TR4 散热器抢先一步坠毁了,考量一番之后换装了一套分体水冷,新的散热器十分强大,以至于 Darktable 的体验改善可以说是从吊着比自己还巨大的轮胎爬行直接提升到星际跃迁一般。


2020-05-26 更新:回头看了很多眼,感觉左眼有点暗,最后发现左眼竟然没有高光!!!只好手动全部修复一下了。

before: Chino Summer Uniform eye before fix after: Chino Summer Uniform eye fixed


全力全开,Gochiusa Strike!(点击照片可以查看大图)

Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform

Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform

贴近看各个角度的细节也非常棒。

Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform

花束上的提比也很可爱。

Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform Chino Summer Uniform

最后的回眸一击~

Chino Summer Uniform

May 06, 2020 12:00 AM

May 05, 2020

frantic1048

PLUM - 奈津恵 夏季制服

期待已久的这套点兔夏服三巨头陆续到货了,惠是三月到的,最近两天智乃也到了,之后可以快乐合照了。

帽子是可拆卸的(只是普通的盖在头上),然后一开始忘了帽子,拍了一堆之后才发现(不管是戴还是不戴都很棒!)。

Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform

帽子来了!

Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform Megu Summer Uniform

May 05, 2020 12:00 AM

May 03, 2020

ヨイツの賢狼ホロ

切点洋葱 - 搭建一个洋葱服务

“如果你愿意一层一层剥开我的心……”

所以洋葱服务(Onion Service)是啥?

洋葱服务 (Onion Service ,旧名为“隐身服务/隐藏服务/ Hidden Service”) 是一种只能透过洋葱路由网络访问的网络服务 (例如网站)。

那么有人为啥要使用洋葱服务呢?

  • 洋葱服务在 Tor 网络中传递,因此可以隐藏服务器的真实地址(例如 IP 地址和地理位置等等)。
  • Tor 用户与洋葱服务之间所有的流量都是端到端加密的,所以汝不必担心像没有 HTTPS 这样的问题。
  • 洋葱服务的网址是为自动生成,因此网站的架设者或管理员无需另行购买网络域名。其网址皆是以 .onion 结尾的, 这样的设计可以让洋葱路由系统确保所有网络连接都通往正确的站点,并且其连接数据未被窜改。

访问一个洋葱服务网站时,除了一般 Tor 网络中会经过的节点以外,还会额外通过若干个中继。 (图上的是 DuckDuckGo 提供的洋葱服务 https://3g2upl4pq6kufc4m.onion/

访问一个洋葱服务网站时

我需要搭建一个洋葱服务么?

如果汝遇到了这些情况,那汝大概会对如何搭建一个洋葱服务引起兴趣(大概吧……):

  • 匿名、不被截获和篡改的传输一个文件。(正好有 OnionShare 可以帮忙这么做)。
  • 在没有公共 IP 地址(甚至没有域名)的情况下提供 Web 服务。
  • 匿名的和另一位匿名者交流。
  • 汝也重视汝的读者的隐私权。 (要是汝不是作者怎么办?)
  • 或者别的? (详见:Tor at the Heart

切洋葱之前当然要做好保护措施啊……

于是亦然(?),要搭建一个洋葱服务的话,首先要有一个能够正常连接的 Tor 。

介于应该没人会一直开着个浏览器(?),所以最好是有个独立的 Tor 在运行(就是那啥所谓的 Expert Bundle 啦……)。

以及现在用 GNU/Linux 提供 Web 服务的家伙应该不少吧,于是汝大概能从汝所使用的 GNU/Linux 发行版的 软件仓库中找到 tor:

准备一个 Web 服务

这个就看人下菜了(?),如果有只用洋葱服务访问的需要的话,可以调整 Web 服务器的配置文件, 让她只接受本地地址的连接。 例如 Nginx 和 Apache 的话,大概可以这么做:

listen localhost:8000;

不放心的话还可以用防火墙啊(x)

生成一个洋葱服务地址

洋葱服务使用的 .onion 地址是随机产生的,地址是在配置洋葱服务时用新生成的公钥计算而成的。 所以叫做“生成”而不是注册,以及虽然有方法可以提前算出来生成特定地址的公钥但是不建议尝试……

用汝最顺手的编辑器和 root 权限(?)打开 Tor 的配置文件(通常是 /etc/tor/torrc ), 然后加上这些:

# 每个不同的洋葱服务都需要一个独立的文件夹来存储其所需要的私钥和主机名,
# 要创建不同的洋葱服务时,需要修改 HiddenServiceDir 所指向的文件夹。
#(注:不要手动创建 HiddenServiceDir 的路径,否则会出现权限的错误而无法启动。
# 在配置完成后启动 Tor 后,程序会自动生成权限合适的对应文件夹。)

HiddenServiceDir /var/lib/tor/path_to_your_service

# HiddenServicePort {汝希望汝的洋葱服务开在哪个端口上} {汝这个端口的洋葱服务交给谁处理}

HiddenServicePort 80 127.0.0.1:8080

保存完毕后重新启动 Tor ,如果一切 OK 的话,可以在汝设置的 HiddenServiceDir 中看到一个 名为 hostname 的文件,里面就是汝新鲜的洋葱服务的域名啦~

同时提供普通网络服务和洋葱服务时?

  • 可以考虑让来自 Tor 的出口 IP 地址的连接自动重定向至洋葱服务地址,例如 这样
  • 也有可能需要调整汝的应用(或者主题什么别的)来适应洋葱服务和 Tor 浏览器。

更进一步?读读看这些文章

当然都只有英文版(汗)

你会鼻酸,你会流泪……

by ホロ at May 03, 2020 04:00 PM

April 28, 2020

ヨイツの賢狼ホロ

在 Android 上创建 GNU/Linux 容器

那这次再说吧……

再一次提醒没有耐心和同理心的家伙们去用其它即用的工具像是 TermuxArch 和 Linux Deploy ……

所以这回咱们要干什么?

由于咱不可能把所有发行版都装一次,也不可能在各种手机上都测试一遍,于是咱只能拿 咱手边的家伙举个例子,例如咱手上的小米平板4 ……

  • 一部 aarch64 / arm64 设备

现在的手机 CPU 基本上都是 64 位了吧,不知道的话搜索一下手上 CPU 的型号应该就能看个大概。

  • 比较新的 Android 系统,最好有 root 权限和完整 busybox 支持。

如果汝已经动手给手机安装了第三方 ROM,那应该不是什么难事。 某蓝绿海厂等受害者可以尝试 UserLAnd……

  • 一些剩余存储空间 (这不是废话么)
  • 安装好终端模拟器和合适的键盘。

或者 Termux 也可以,在里面装上 tsu 以后可以让 Termux 里的 Bash 以 root 用户运行。

作为好几年的 Arch Linux 受害 爱好者,咱当然就装一个 ArchLinux ARM 试试看啦 (虽然这不是官方分支,以及不是有 TermuxArch 了么……)

那么我们的目标是,没有蛀牙 ……

  • 能够在 chroot 下基本正常工作

例如正常的使用包管理器、安装软件以及用它们等等。

  • 能够访问网络和内存设备 (踩坑警告)
  • 能够通过外部访问 (ssh 或者 vnc 啥的)

如有任何不适还请装作没事

天降大坑 nosuid ……(?)

某些手机上的 /data 挂载的时候加上了 nosuid 选项,于是 sudo 就炸了……

sudo: effective uid is not 0, is /sbin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?

这种情况可以考虑创建一个磁盘映像把容器放进去(大概吧……)

# 找个地方创建一个映像文件
dd if=/dev/zero of=/path/to/root.img bs=1048576 count=4096

# 给映像创建文件系统和挂载
mke2fs -t ext4 -F /path/to/root.img
mkdir -p /data/linux/arch
mount -t ext4 -o loop /path/to/root.img /data/linux/arch

如果肯定不用 sudo 的话能不能当做没事……

要有空间,于是……

丢了个 tarball 下来让汝自己 bootstrap 去(啥?)

安装 GNU/Linux 发行版的过程,其实就是在解决一个先有鸡还是先有蛋的问题,这个过程就是 bootstrap。 大概就是……

  • 从别的什么可以启动的地方把要安装的 GNU/Linux 发行版的最小的部份下载回来,安装上。
  • 通过一些方法进入这个最小的部份中(例如 chroot ),为目标设备特化配置。

只不过大部分发行版的安装过程中被自动化了(Arch / Gentoo 等用户表示情绪稳定。)。 在 Android 上安装,同样要经过这个过程。

于是,首先咱们要下载 Arch Linux ARM 的 tarball ,可以在电脑上下载下来发送到手机上,也可以用手机 的浏览器直接下载,也可以用 curl 或者 wget:

wget http://os.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz

如果直接从官方仓库下载不够快的话,可以通过镜像网站下载,例如:

https://mirrors.tuna.tsinghua.edu.cn/archlinuxarm/os/ArchLinuxARM-aarch64-latest.tar.gz

然后创建一个目录存放解压出来的基本系统,再解压出来刚下载的 tarball:

# 因为 SD 卡可能放不下权限等神奇的原因,就放在内存设备上好了……
# mkdir -p /data/linux/arch
# 以及 Arch Linux ARM 官方是建议用 bsdtar 的,要是汝的手机上凑巧有(不管是
# 系统里的还是 Termux 的),就用上吧……
# bsdtar -xpf /path/to/ArchLinuxARM-aarch64-latest.tar.gz -C /path/to/mountpoint
# 不然 tar 也是可以的。
# tar -xzvf /path/to/ArchLinuxARM-aarch64-latest.tar.gz -C /path/to/mountpoint

Chroot on Android

在普通的 GNU/Linux 发行版上 chroot 时,咱们大抵会这么做:

# 切换到 chroot 的目标目录并挂载上 /dev /sys 这样的伪文件系统
# cd /location/of/new/root
# mount -t proc proc proc/
# mount --rbind /sys sys/
# mount --rbind /dev dev/
# 通过给定的 shell 进入 chroot
# chroot /location/of/new/root /bin/bash

在 Android 上其实也差不多:

# mount -o bind /dev /data/linux/arch
# mount -o bind /sys /data/linux/arch/sys
# mount -o bind /proc /data/linux/arch/proc
# mount -t tmpfs tmpfs /data/linux/arch/tmp

以及记得修改一下 chroot 里面的 /etc/resolv.conf , Arch 这个默认是到 /run/systemd/resolve/resolv.conf 的软链接。 反正里面也没有 systemd 用 ,于是 删掉重建好了……

# /data/linux/arch/etc/resolv.conf

nameserver 8.8.8.8
nameserver 8.8.4.4

如果有必要的话,可以同时修改里面的镜像仓库地址。

# /data/linux/arch/etc/pacman.d/mirrorlist

Server = https://mirrors.ustc.edu.cn/archlinuxarm/$arch/$repo

然后就和平常一样 chroot 进去咯~

clover:/data/linux # chroot /data/linux/arch /bin/bash
[root@localhost /]#

起始配置

The bootstrap environment is really barebones (no nano or lvm2). Therefore, we need to set up pacman in order to download other necessary packages.

这下知道为啥要在外面改 /etc/resolv.conf 了吧(当然汝要是独辟蹊径的话那当咱没说……)

正如 官方说明提示的一样 ,初始化 pacman 密钥环和获得密钥:

[root@localhost /]# pacman-key --init
gpg: /etc/pacman.d/gnupg/trustdb.gpg: trustdb created
gpg: no ultimately trusted keys found
gpg: starting migration from earlier GnuPG versions
gpg: porting secret keys from '/etc/pacman.d/gnupg/secring.gpg' to gpg-agent
gpg: migration succeeded
gpg: Generating pacman keyring master key...
gpg: key 56753AA14274D5A7 marked as ultimately trusted
gpg: directory '/etc/pacman.d/gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/etc/pacman.d/gnupg/openpgp-revocs.d/46C9147EE071F7E5D16A085856753AA14274D5A7.rev'
gpg: Done
==> Updating trust database...
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
[root@localhost /]# pacman-key --populate archlinuxarm
==> Appending keys from archlinuxarm.gpg...
==> Locally signing trusted keys in keyring...
-> Locally signing key 69DD6C8FD314223E14362848BF7EEF7A9C6B5765...
-> Locally signing key 02922214DE8981D14DC2ACABBC704E86B823CD25...
-> Locally signing key 9D22B7BB678DC056B1F7723CB55C5315DCD9EE1A...
==> Importing owner trust values...
gpg: setting ownertrust to 4
gpg: inserting ownertrust of 4
gpg: setting ownertrust to 4
==> Updating trust database...
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   3  trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: depth: 1  valid:   3  signed:   1  trust: 0-, 0q, 0n, 3m, 0f, 0u
gpg: depth: 2  valid:   1  signed:   0  trust: 1-, 0q, 0n, 0m, 0f, 0u
[root@localhost /]#

接下来安装(和更新)基本系统,如果有别的需要的话也可以装别的。

[root@localhost /]# pacman -Syu base base-devel nano --needed

如果不幸遇到了像是 error: could not determine cachedir mount point /var/cache/pacman/pkg 这样的错误, 那在 /etc/pacman.conf 里把 Misc options 下面的 CheckSpace 注释掉应该能绕过 (于是有别的方法嘛)

基于正经的 GNU/Linux 用户不会日用 root 账户这一指导原则(啥?),咱们也要新建一个用户:

[root@localhost /]# useradd -m -s /bin/bash horo
[root@localhost /]# passwd horo

有必要的话也可以修改 sudoers 文件(记得用 visudo),把汝刚刚创建的用户添加到 sudoers 中。

以及介于 Android 的魔改属性,只有特定的组可以进行像是访问网络或者访问 SD 卡等操作,于是 还要在 chroot 里新建相应的组,并把新建的用户加到这些组去:

# groupadd 可以用 -g 参数制定新增组的 id ,至于这些组分别是啥
# 看后面的组名应该就知道了吧……
groupadd -g 3001 android_bt
groupadd -g 3002 android_bt-net
groupadd -g 3003 android_inet
groupadd -g 3004 android_net-raw
groupadd -g 1015 sdcard-rw
groupadd -g 1028 sdcard-r
# 然后把新建的用户添加到合适的组中
gpasswd -a horo android_bt
gpasswd -a horo android_bt-net
gpasswd -a horo android_inet
gpasswd -a horo android_net-raw
gpasswd -a horo sdcard-rw
gpasswd -a horo sdcard-r

最后(?),因为没有 systemd,所以请像新装 Arch Linux 的时候一样手动设置一下 locales:

  • /etc/locale.gen 是一个仅包含注释文档的文本文件。指定您需要的本地化类型,去掉对应行前面的注释符号(#)就可以啦,还是用 nano 打开,建议选择帶UTF-8的項:
# nano /etc/locale.gen

en_US.UTF-8 UTF-8
  • 执行 locale-gen 以生成 locale 讯息:
# locale-gen
  • 创建 locale.conf 并提交本地化选项:
# echo 用来输出某些文字,后面的大于号表示把输出保存到某个文件里啦~

# 或者可以用文字编辑器新建这个文件加上这一行。

# echo LANG=en_US.UTF-8 > /etc/locale.conf
  • 设置用户级别的 locale

    用 su 切换到刚建立的用户,然后编辑 ~/.bashrc 修改自己的 Locale ,例如:

LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=

(为啥不是 ~/.config/locale.conf 了啊…… 其实咱也不知道…… )

于是现在大概就有了这个样子:

于是现在大概就有了这个样子

(这个在终端里显示发行版等信息的软件是 screenfetch 啦,也有人喜欢另一个 小修改版 neofetch )

设置 SSH 和 SD 卡访问

SSH 的话,生成好主机密钥然后再启动 sshd 就可以:

## 在有 systemd 这样的 init 系统的发行版上启动 sshd 时会帮汝运行这一步,
## 不过这里就只有自己代劳啦……
# ssh-keygen -A
## 这里一定要是绝对路径,不然就会出 "sshd re-exec requires execution with an absolute path" 错误。
# /usr/bin/sshd

然后就可以用一个新的终端模拟器窗口通过 ssh 连接进来啦……

clover:/ $ ssh horo@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256: .
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
horo@127.0.0.1's password:
[horo@localhost ~]$

要是汝在登录时遇到了 PTY allocation failed on channel 0 这样的错误,在 chroot 里(重新) 挂载一下 /dev/pts 试试?

# umount /dev/pts
# mount -t devpts devpts /dev/pts

以及可以的话记得修改 /etc/sshd_config 把 sshd 限定到只允许本地连接?

要把 SD 卡挂载到哪里的话,在 chroot 的外面运行:

## 当然汝的 /sdcard 可能是汝的内存设备什么的,别忘了自己调整路径……
# mkdir /data/linux/arch/mnt/sdcard
# mount -o bind /sdcard /data/linux/arch/mnt/sdcard

设置 VNC 访问

首先安装桌面和 VNC Server ,考虑到两大桌面的资源需求和或多或少有点依赖 GNOME,比较合适的是 xfce:

## 当然汝可以看需要装些别的,像是中文字体什么的。
# pacman -S xorg xfce4 xfce4-goodies tigervnc

接下来启动 vncserver,设置一个密码:

[horo@localhost ~]$ vncserver

You will require a password to access your desktops.

Password:
Verify:
# 要创建一个只读桌面用的密码嘛?
Would you like to enter a view-only password (y/n)?
xauth:  file /home/horo/.Xauthority does not exist

# 桌面的位置
New 'localhost:1 (horo)' desktop is localhost:1

Creating default startup script /home/horo/.vnc/xstartup
Creating default config /home/horo/.vnc/config
Starting applications specified in /home/horo/.vnc/xstartup
Log file is /home/horo/.vnc/localhost:1.log

然后先用 vncserver -kill :1 中止掉现有的 VNC Server 进程,修改一下新生成的配置文件。

# /home/horo/.vnc/xstartup

#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
exec xfce4-session

# /home/horo/.vnc/config

## Supported server options to pass to vncserver upon invocation can be listed
## in this file. See the following manpages for more: vncserver(1) Xvnc(1).
## Several common ones are shown below. Uncomment and modify to your liking.
##
# securitytypes=vncauth,tlsvnc
# desktop=sandbox
# 可以在这里修改分辨率和限定本地连接,具体的参数可以查阅相应的手册页来了解。 (vncserver(1) Xvnc(1))
geometry=1920x1080
# localhost
# alwaysshared

如果一切正常的话应该会连上去之后有 xfce 的界面的,但是咱这回就只有 Unable to contact settings server 这个错误,以后再说吧(跑) 不过隔壁 Linux Deploy 出的 Debian 是好的……

总结?

所以有 Linux Deploy 那样好用(?)的工具了为啥不直接拿来用呢(划掉)

以及纯粹是闲的,找一台正经的电脑装正经的 GNU/Linux 不好么 (x)

by ホロ at April 28, 2020 04:00 PM

April 27, 2020

ヨイツの賢狼ホロ

在 Android 上使用 GNU/Linux 工具

虽然……

虽然汝可能从哪里听说过 Android 是基于 Linux 内核的啦,不过大多数时候汝大概没办法直接把汝爱用的 GNU/Linux 发行版上的 工具直接拿过来用,为啥咧?

  • 手机和电脑不是一种 CPU(PC 常见的就是 x86_64 , 手机上比较常见的是 arm64(有时也称作 aarch64)),因此 两边的二进制文件并不能直接拿来换着用。
  • 虽然 Android 用了 Linux 内核,但是和普通的 GNU/Linux 发行版还是有很大的区别的。例如以 bionic 取代glibc (C 函数库)、以 Skia 取代 Cairo (用于向量图形绘图)、再以 OpenCORE 取代 FFmpeg(常用于音视频的录影和转换)。 于是不少依赖它们的软件不好运行。
  • 以及随着越来越多的 GNU/Linux 发行版开始用 Systemd 作为初始化程序,很多程序或多或少就和 Systemd 有了些关联(或者依赖)。 不巧的是……

所以 Android 应用们不好用了么?

STAGE 0 - Android 上的终端(模拟器)

如果汝有用过一些需要在电脑上运行一个辅助程序的 Android 应用(像是 AppOps 这样的),汝其实已经离 Shell 很近了。

在手机和电脑连接以后,可以运行 adb shell 命令获得一个 shell:

(PC) $ adb shell
walleye:/ $

接下来就可以 cd ls 乱出了…… (啥?) 因为别的汝也干不了啊……

如果汝的手机有 root 权限的话,在 Shell 里运行 su 试试?第一次的话汝的手机上的 root 权限管理程序应该会提示汝授予权限什么的。

要想在手机上试试的话,也有差不多的终端模拟器程序可以选择。 例如这个?

STAGE 1 - 在 Android 上使用(部份) GNU/Linux 程序

只有个 shell 哪里够……

正好也有一群人这么想,于是它们把终端模拟器和一些常用的 GNU/Linux 应用程序组合起来,创造出了 Termux

Termux 的启动界面

(这个在电脑上显示 Android 设备画面的程序是 scrcpy 啦~)

这里面提到的 pkg 其实就是有点小包装的 apt 啦,熟悉 Debian / Ubuntu 的话应该不会陌生。

下面的 root / Unstable / x11 是额外的仓库,需要使用要求 root 权限或者图形界面的软件在里面。

  • Play 商店上也有几个 Termux 使用的插件,可以实现自定义字体、浮动窗口、快速运行脚本、访问手机内存设备 等功能。有几个是收费的,不过有别的地方可以免费下载(小声)
  • 键盘上面的特殊按键可以通过编辑 ~/.termux/termux.properties 文件中的 extra-keys 属性来修改, 特殊键的定义在这里。
  • 偷懒的话也可以直接安装一个有这些特殊键的输入法,例如 Hacker's Keyboard
  • 当然外接键盘也是可以的。

以及大概有人踩过了不少的坑的样子……

STAGE 2 - 在 Android 上运行(部份) GNU/Linux 容器

啊 pkg/apt 好不习惯……

怀念汝习惯的 GNU/Linux 发行版的原汁原味的包管理器和其它工具?可以考虑运行一个容器来解馋(?)

Linux® 容器是与系统其他部分隔离开的一系列进程。运行这些进程所需的所有文件都由另一个镜像提供, 这意味着从开发到测试再到生产的整个过程中,Linux 容器都具有可移植性和一致性。

RedHat : 什么是 Linux 容器?

实现这种操作的关键要素就是 chroot ,它针对正在运作的软件行程和它的子进程,改变它外显的根目录。 在使用 chroot 之后,进程就会认为设置的目录是根目录,就可以进行各种操作啦,例如隔离访问或者修复系统什么的。

当然这操作是要 root 权限的,至于后来有人写出用户空间中的 proot ,就是另一个故事啦……

  • 手机有 root 权限的话,可以用 chroot 安装一个 GNU/Linux 容器。也有像是 Linux Deploy 这样的应用可以简化这一过程。
  • 没有 root 权限的话,借助前面提起的 proot 也能实现差不多的操作。也有像是 UserLAndTermuxArch 这样的工具帮忙简化这一过程。

以及大概也有人踩过了不少的坑的样子……

那如果咱就是想从零(?)开始呢?

那下次再说吧……

by ホロ at April 27, 2020 04:00 PM

April 24, 2020

Lainme

修复Skype在CentOS7上打不开的问题

最近下载了新版本的Skype后发现无法在CentOS7上无法打开,点击图标没有反应,在终端输入skypeforlinux命令也没有任何报错。

找到skypeforlinux这个命令后发现这就是一个bash脚本,实际调用了/usr/share/skypeforlinux/skypeforlinux这个可执行文件。于是我直接执行了它,终于有了错误信息

The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /usr/share/skypeforlinux/chrome-sandbox is owned by root and has mode 4755.

之后根据这个报错进行修改就可以了,即

sudo chmod 4755 /usr/share/skypeforlinux/chrome-sandbox

不过这么操作之后仍然有一个问题,就是CentOS 7上的gcc版本太低,会提示GLIBCXXX 3.4.21 NOT FOUND错误。这样有几个解决方法

  • 升级系统的GCC到6.1.0或以上
  • 降级Skype,可能需要降到8.54.0
  • 在别的位置另外安装一个高版本的GCC并设置LD_LIBRARY_PATH来进行使用

我自己使用的是第三种方法,GCC 6.1.0安装到了/usr/local,然后只需要修改/usr/bin/skypeforlinux这个启动脚本

#!/bin/sh
 
SCRIPT=$(readlink -f "$0")
USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..)
 
SKYPE_EXPO="export LD_LIBRARY_PATH=/usr/local/lib64:$LD_LIBRARY_PATH"
SKYPE_PATH="$USR_DIRECTORY/share/skypeforlinux/skypeforlinux"
SKYPE_LOGS="$HOME/.config/skypeforlinux/logs"
 
mkdir -p $SKYPE_LOGS
 
$SKYPE_EXPO && nohup "$SKYPE_PATH" --executed-from="$(pwd)" --pid=$$ "$@" > "$SKYPE_LOGS/skype-startup.log" 2>&1 &

by lainme (lainme@undisclosed.example.com) at April 24, 2020 04:57 PM

April 18, 2020

ヨイツの賢狼ホロ

两次理想主义的尝试

几月不见甚是想念…… 会这么想的肯定是没看 咱的 Matters 啦…… (虽然那边也沉寂了好一阵子), 以及「集合啦!动物森友会」真好玩……

所以“两次理想主义的尝试”中的其中一次是什么?

在赞助了 Utopiosphere「本格异想录」 的会员计划之后,咱打算推出咱自己的会员计划了。

诶???

为啥开始寻求赞助啦?

因为穷.png

不像公众号、文章平台等可以"免费"存放内容(虽然汝和汝的读者可能会因为免费付出 其它的代价,例如自由和广告什么的……),独立博客是需要博主自己付出一定的成本来 维持运转的(例如域名和存放内容的地方)。

虽然咱前几个月终于悟出了 Likecoin 的赞赏键其实就是一个 iframe, 然后把它“移植” 到了这里,不过这几个月咱也没在这边写什么的样子,于是也形同虚设了(hmmmm)

最后还有咱自己的一点个人(?)原因,希望能得到一些声音(嗯?),咱当然也是有 回报可以拿出来的啦……

赞助者可以获得的回报有?

咱不知道还有啥词汇可以描述这群人了……

在写完这篇文章的时候,咱能够想到的大概有这些:

  • 优先阅读咱的拙作(诶?以及对于会员计划来说这好像很平常啊)

其实咱有好几次都是有好几个想法不知道先写那一篇,然后就咕咕咕了……这样大概 能让咱知道大家更想看到哪一篇,以及鞭策咱不要咕咕咕(x)

  • 某些方面的咨询服务(诶??)

只要是咱能够帮的上忙的地方,咱会努力。以及既然收了钱就会接纳(几乎全部)咱觉得 (有些)蠢的问题……

至于其它的嘛,让咱再想想……

赞助的方式?

还是草案(首先要有人愿意赞助……)
  • 「异想星空」 那边用了爱发电,看着就像有本地化的 Patreon ……
  • 在有了一些数字货币(就是 Likecoin 啦……)以后,也有意愿接收数字货币赞助 (但是有人愿意出嘛)
  • 以及可能会有优惠?

那另一次又是什么?

啊没错,咱又双想把 某个咱之前挖的大坑 的铲子捡起来了。

那么这个想法是怎么想起来的呢?

忘了……依稀还记得有一天吐槽过中文的 GNU/Linux 教程几乎都是默认用户只会用 GNU/Linux 来架设服务的那种。以及觉着 鸟哥的Linux私房菜 虽然写的很不错,但是是用的 CentOS 当作的教学范本……

于是就有想法写一部和 GNU/Linux 日常使用相关的手册的想法就这么产生了。

咱的目标是什么呢?

简单来说,咱有计划完成一部 GNU/Linux 桌面应用相关的手册,那具体有哪些方面呢?

  • 了解和 GNU/Linux 相关的一些概念
  • 自己动手安装 GNU/Linux 发行版
  • 使用 GNU/Linux 完成某些操作 (不只是运行服务)

具体的细节可以去看 咱之前的计划

自由的理想

作为 并不纯的 自由软件爱好者,咱也十分理想的想这么做:

  • 使用一个 完全自由的 GNU/Linux 发行版 作为范本,然而中途一度退而求其次的换掉了……
  • 以及尽可能的只用自由软件进行教学(?),虽然有些地方肯定不可能……
  • 以及想提醒大家稍稍关注一下隐私和安全意识(希望如此……)

但是……


大家快来投币(?)催咱不要咕咕咕啊……

想要了解两个之一(或全部)的更多细节的话,欢迎来咱的 Telegram 群交流更多细节:

链接在这里 ,如果链接失效的话 那就 @咱 请求链接好了。

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

https://afdian.net/@KenOokamiHoro
-----BEGIN PGP SIGNATURE-----

iHUEARYIAB0WIQQRcWZ9dT8okL6OyLwb09jRlMdzdQUCXpwxZgAKCRAb09jRlMdz
dVFQAQDjUk6b9H8MJaOV7+cX8ssyyBBdh3SsM1I/Sp2LK1S3fAD9HRR9ZzEv0CJR
OvdcglAV5zI9Lj1pRjGQvOti7VFaPwk=
=ygU/
-----END PGP SIGNATURE-----

by ホロ at April 18, 2020 04:00 PM

April 14, 2020

中文社区新闻

zn_poly >= 0.9.2-2 升级需要手动干预

zn_poly 包在版本 0.9.2-2 之前缺失了一个动态库链接。这个问题已经在 0.9.2-2 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

zn_poly: /usr/lib/libzn_poly-0.9.so  exists in filesystem

那么请使用命令:

pacman -Syu --overwrite usr/lib/libzn_poly-0.9.so

完成更新。

by farseerfc at April 14, 2020 05:30 PM

April 13, 2020

中文社区新闻

nss>=3.51.1-1 及 lib32-nss>=3.51.1-1 更新需要手动干预

nsslib32-nss 两个包在版本 3.51.1-1 之前各缺失了一个动态库软链接。这个问题已经在 3.51.1-1 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

nss: /usr/lib/p11-kit-trust.so exists in filesystem
lib32-nss: /usr/lib32/p11-kit-trust.so exists in filesystem

那么请使用命令:

pacman -Syu --overwrite /usr/lib\*/p11-kit-trust.so

完成更新。

by farseerfc at April 13, 2020 01:35 AM

March 26, 2020

Leo Shen

Record to MiniDisc with correct Track Marker on Linux

Recently I got my first MiniDisc Walkman at a local Goodwill. It's a Sony MZ-R37, and it is an absolutely gorgeous machine. Most of the recorder is built with aluminum, and the disc loading mechanism is simply too fun to play with.

However, since the machine was released at 1999, it is one of the older MiniDisc recorders, and does not have the fancy capabilities some later models have, like NetMD, MDLP, and Hi-MD. So, the best way to put music onto a disc is limited to use a good old Optical In port. If you are not familiar with MiniDisc recorders, they usually have a Line In port that can accept both an analog input (which is our old friend, 3.5mm Stereo Mini Jack) and a Mini TOSLINK connection.

By the way:

Mini TOSLINK has the same diameter as the 3.5mm Stereo Jack, but slightly longer and doesn't have the cooper connectors. It uses the same signal as the TOSLINK (or Optical S/PDIF) connector found at TVs and computers. It can give mobile devices the capability to transfer digital audio streams. But now it is largely replaced by USB audio interfaces.

So, I grabbed a TOSLINK to Mini TOSLINK cable, and connect it to my computer's Optical Out port. Then, it is as simple as letting MPD to use the Optical Out, and then play the desired songs. Everything just works.

Until it doesn't! When transferring audio from a CD player, the player will send a marker when each song ends, so the MiniDisc player can automatically add a marker when each song ends. However, all the players I tried does not have this feature, which makes sense, since MiniDisc is pretty much obsolete now.

First attempt: A simple bash script!

When messing around alsamixer, I discovered that ALSA will completely cut the optical stream when the S/PDIF output is set to mute (which means, the light on the output will not shine at all). My specific recorder consider this as a NO SIGNAL situation, and will stop recording. When the recording resumes, it would add a new track marker to where the new streams comes in.

So, in theory, we can abuse this behavior to add track maker for each song we record. At first, I decided to use mpv, since it almost guarantee to work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash

amixer -c 0 set 'IEC958',0 mute # First mute S/PDIF, prevent blanks before first song
rm temp.wav
sleep 5s # Give some time to start record on MiniDisc recorder

while read file
do
	amixer -c 0 set 'IEC958',0 unmute # Unmute to start recording
	sleep 1.1s # The recorder need some time to start reading stream. Can adjust time if sense blank before song.
	mpv --audio-device=alsa/iec958 "$file"
	amixer -c 0 set 'IEC958',0 mute # Set it back to mute again, thus give us a track mark
	sleep 1s
done <$1

This seems to work, as I can see the track number bumping on the front LCD.

However, when I play it back, there is an issue: there's a very brief blank track between each song. This is due to the fack that mpv closes the audio interface before we unmute the output. And since a digital signal, the recorder successfully capture the very brief second before line 11 and 12, and add a blank track.

Second attempt: Hack ogg123!

If you want to use the method here, don't forget to adjust the commands accordingly so that it fits your sound card. You can find information via executing amixer -c CARD_ID.

So, I have to find a way to mute the sound output just before the audio stream is closed. The best way would be to find a simple command line audio player with the minimal number of code, since I don't want to view a huge codebase and potentially mess everything up. Fortunately, there's a program called ogg123, created by the awesome lads at Xiph.org Foundation1. This is a very basic audio player, which is desirable for this project.

After grabbing a copy of vorbis-tools (which contains ogg123), we can take a look. The main sound output codes are located at ogg123/audio.c. And surely, there is a function called void free_audio_devices. All we have to do should be add some codes to mute the output before the free process actually begins.

I can do it cleanly by using the user space library for ALSA, but that means I may have to investigate a good amount of hours into learning, and I'm kinda busy. So nah, I just hacked it.

Since we already know how to use amixer to mute the command, we can just use system() function (provided by stdlib.h) to execute a shell command. It is not expandable and flexible AT ALL, but it works and it does not kill my brain cells.

So, the free_audio_devices looks like this now:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void free_audio_devices (audio_device_t *devices)
{
  audio_device_t *current;

  system("amixer -c 0 set 'IEC958',0 mute");

  while (devices != NULL) {
    current = devices->next_device;
    free (devices);
    devices = current;
  }
}

Then we can trigger make and compile the patched code. Since I don't want to use this version globally, I did not install it.

Then the updated script should look like this:

1
2
3
4
5
6
7
8
#!/bin/bash
while read file
do
	amixer -c 0 set 'IEC958',0 unmute
	sleep 1.1s
	/$SOMEWHERE/vorbis-tools-1.4.0/ogg123/ogg123 -d alsa --device-option dev:hw:0,1 "$file"
	sleep 1s
done <$1

Since now ogg123 is responsible to mute the output, we can save a line here.

And now it works! No more additional empty tracks, only accurate track data.

File format conversion: make life easier.

So the recording process is way easier. No more manual operation, eh?

Well, if all your music is in CD quality (or less), just like in the 90s, that's it. However, for more advanced music format (like DSD and FLAC with higer bit rate), the MiniDisc recorder will be confused about the crazy burst of data, and won't record them at all.

So, we still have to somehow downsample the audio file. Luckily, that can also be automated too. I just used ffmpeg to convert the audio sample to 16bit, 48000Hz (which is probably the best quality the little recorder can accept). Since the script has become pretty long, I won't quote it here. You can find the exact script I use at here.

Done!

And that's it! Now, we can create an awesome MiniDisc mix tape with the correct track marker easily with a command. The only drawback for now is that the track name is still lacking, but it is not a huge deal for me.

Enjoy your music!


1

Seriously, these guys deserve a medal for their work on free audio codecs!

March 26, 2020 12:19 AM

March 19, 2020

中文社区新闻

hplip 3.20.3-2 更新需要手动干预

hplip 包在 3.20.3-2 之前的版本缺失了一些预编译的 python 模块。这个问题已经在 3.20.3-2 版本中修复,所以更新时需要覆盖掉那些没有被跟踪到的 pyc 文件。如果你遇到如下报错


hplip: /usr/share/hplip/base/__pycache__/__init__.cpython-38.pyc exists in filesystem
hplip: /usr/share/hplip/base/__pycache__/avahi.cpython-38.pyc exists in filesystem
hplip: /usr/share/hplip/base/__pycache__/codes.cpython-38.pyc exists in filesystem
...更多类似报错...

那么在更新时请使用命令:

pacman -Suy --overwrite /usr/share/hplip/\*

来完成更新

by farseerfc at March 19, 2020 08:08 AM

March 18, 2020

farseerfc

SSD 就是大U盘?聊聊闪存类存储的转换层

上篇 「柱面-磁头-扇区寻址的一些旧事」 整理了一下我对磁盘类存储设备(包括软盘、硬盘,不包括光盘、磁带)的一些理解, 算是为以后讨论文件系统作铺垫;这篇整理一下我对闪存类存储设备的理解。

这里想要讨论的闪存类存储是指 SSD 、SD卡、U盘、手机内置闪存等基于 NAND 又有闪存转换层的存储设备(下文简称闪存盘),但不包括裸 NAND 设备、3D Xpoint (Intel Optane)等相近物理结构但是没有类似的闪存转换层的存储设备。 闪存类存储设备这几年发展迅猛,SD卡和U盘早就替代软盘成为数据交换的主流, SSD 大有替代硬盘的趋势。 因为发展迅速,所以其底层技术变革很快,不同于磁盘类存储技术有很多公开资料可以获取, 闪存类存储的技术细节通常是厂商们的秘密,互联网上能找到很多外围资料, 但是关于其如何运作的细节却很少提到。所以我想先整理一篇笔记,记下我搜集到的资料,加上我自己的理解。 本文大部分信息来源是 Optimizing Linux with cheap flash drivesA Summary on SSD & FTL ,加上我的理解,文中一些配图也来自这两篇文章。

1   NAND Flash 原理

比 NAND Flash 更早的 EEPROM 等存储技术 曾经用过 NOR Flash cell ,用于存储主板配置信息等少量数据已经存在 PC 中很久了。后来 NAND Flash 的微型化使得 NAND Flash 可以用于存储大量数据,急剧降低了存储成本,所以以 NAND Flash 为基础的存储技术能得以替代硬盘等存储设备。

这里不想涉及太多 NAND Flash 硬件细节,有个演讲 Tutorial: Why NAND Flash Breaks DownYouTube 视频 介绍了其原理,感兴趣的可以参考一下。只罗列一下视频中提到的一些 NAND Flash 的特点:

  • NAND Flash 使用 floating gate 中束缚电子来保存二进制数据,对这些 Cell 有读取(Read)、 写入(Programming)、擦除(Erase)的操作。擦写次数叫 P/E cycle。
  • 电子的量导致的电势差可以区别 1 和 0 ,这是 Single Level Cell (SLC) 的存储方式。 或者可以用不同的电势差区分更多状态保存更多二进制位,从而有 Multi-Level Cell (MLC), TLC, QLC 等技术。可以对 MLC 的 Flash Cell 使用类似 SLC 的写入模式,物理区别只是参考电压, 只是 SLC 模式写入下容量减半。
  • 高密度设计下,一组 NAND Flash Cell 可以同时并发读写。所以有了读写页 2KiB/4KiB 这样的容量。 页面越大,存储密度越高,为了降低成本厂商都希望提高读写页的大小。
  • 为了避免添加额外导线,NAND Flash Cell 是使用基板上加负电压的方式擦除 floating gate 中的二进制位的,所以擦除操作没法通过地址线选择特定 Cell 或者读写页,于是整块擦除有块大小。
  • 写入操作对 SLC 单个 Cell 而言,就是把 1 置 0 ,而擦除操作则是把整块置 1 。SLC 可以通过地址线单独选择要写入的 Cell ,MLC 则把不同页的二进制放入一个 Cell ,放入时有顺序要求, 先写处于高位的页,再写低位的。所以 MLC 中不同页面地址的页面是交错在同一组 Cell 中的。
  • SLC 其实并没有特别要求擦除块中的写入顺序,只是要求仅写一次(从 1 到 0)。 MLC 则有先写高位页再写低位页的要求。厂商规格中的要求更严格,擦除块中必须满足按页面编号顺序写入。
  • 写入和擦除操作是通过量子隧道效应把电子困在 floating gate 中的,所以是个概率事件。通过多次脉冲 可以缩小发生非预期概率事件的可能性,但是没法完全避免,所以需要 ECC 校验纠错。
  • 根据 ECC 强度通常有三种 ECC 算法,强度越强需要越多算力:
    • 汉民码 可根据 n bit 探测 \(2^n - n -1\) 中的 2 bit 错误,修正 1 bit 错误。
    • BCH码 可根据 \(n*m\) bit 纠错 \(2^n\) bit 中的 \(m\) bit 错误。
    • LDPC 原理上类似扩展的汉民码,能做到使用更少校验位纠错更多错误。
  • 因为 ECC 的存在,所以读写必须至少以 ECC 整块为单位,比如 256 字节或者整个页面。
  • 也因为 ECC 的存在, \(ECC(\texttt{0xFF}) \ne \texttt{0xFF}\) ,空页(擦除后全1的页面)必须特殊处理。所以需要区分写了数据全 1 的页和空页。
  • ECC校验多次失败的页面可以被标记为坏页,出厂时就可能有一些坏页,这些由转换层隐藏起来。
  • 断电后,也有小概率下束缚的电子逃逸出 floating gate ,时间越长越可能发生可以探测到的位反转。 所以基于 NAND Flash 的存储设备应该避免作为存档设备离线保存。
  • 电子逃逸的概率也和温度有关,温度越高越容易逃逸,所以高温使用下会有更高的校验错误率。
  • 读取时,因为用相对较高的电压屏蔽没有读取的地址线,有一定概率影响到没被读取的页面中存储的数据。 控制器可能考虑周期性地刷新这些写入后多次读取的页面,这可能和后文的静态擦写均衡一起做。
  • 正在写入或者擦除中突然断电的话下,写入中的一整页数据可能并不稳定,比如短期内能正常读取但是难以持续很长时间。

上篇讲硬盘的笔记中提到过,硬盘物理存储也有越来越强的校验机制,不过相比之下 NAND Flash 出现临时性校验失败的可能性要高很多,需要控制器对校验出错误的情况有更强的容忍能力。 厂商们制作存储设备的时候,有一个需要达到的错误率目标(比如平均 \(10^{14}\) bit 出现一次位反转),针对这个目标和实际物理错误率,相应地设计纠错强度。校验太强会浪费存储密度和算力, 从而提升成本,这里会根据市场细分找折衷点。

2   封装结构

从外部来看,一个闪存盘可能有这样的结构:

ssd-enclosure.svg

从上往下,我们买到的一个闪存盘可能一层层分级:

  1. 整个闪存盘有个控制器,其中含有一部分 RAM 。然后是一组 NAND Flash 封装芯片(chip)。
  2. 每个封装芯片可能还分多个 Device ,每个 Device 分多个 Die ,这中间有很多术语我无法跟上,大概和本文想讨论的事情关系不大。
  3. 每个 Die 分多个平面(Plane),平面之间可以并行控制,每个平面相互独立。从而比如在一个平面内 做某个块的擦除操作的时候,别的平面可以继续读写而不受影响。
  4. 每个平面分成多个段(Segment),段是擦除操作的基本单位,一次擦除一整个段。
  5. 每个段分成多个页面(Page),页面是读写操作的基本单位,一次可以读写一整页。
  6. 页面内存有多个单元格(Cell),单元格是存储二进制位的基本单元,对应 SLC/MLC/TLC/QLC 这些, 每个单元格可以存储一个或多个二进制位。

以上这些名字可能不同厂商不同文档的称法都各有不同,比如可能有的文档把擦除块叫 page 或者叫 eraseblock 。随着容量不断增大,厂商们又新造出很多抽象层次,比如 chip device die 这些, 不过这些可能和本文关系不大。如果看别的文档注意区别术语所指概念,本文中我想统一成以上术语。 重要的是有并行访问单元的平面(Plane)、擦除单元的段(Segment)、读写单元的页(Page)这些概念。 抽象地列举概念可能没有实感,顺便说一下这些概念的数量级:

  1. 每个 SSD 可以有数个封装芯片。
  2. 每个芯片有多个 Die 。
  3. 每个 Die 有多个平面。
  4. 每个平面有几千个段。比如 2048 个。
  5. 每个段有数百个页到几千页,比如 128~4096 页,可能外加一些段内元数据。
  6. 每个页面是 2KiB~8KiB 这样的容量,外加几百字节的元数据比如 ECC 校验码。

和硬盘相比,一个闪存页面大概对应一个到数个物理扇区大小,现代硬盘也逐渐普及 4KiB 物理扇区, 文件系统也基本普及 4KiB 或者更大的逻辑块(block)或者簇(cluster)大小,可以对应到一个闪存页面。 每次读写都可以通过地址映射直接对应到某个闪存页面,这方面没有硬盘那样的寻址开销。 闪存盘的一个页面通常配有比硬盘扇区更强的 ECC 校验码,因为 NAND 单元格丧失数据的可能性比磁介质高了很多。

闪存有写入方式的限制,每次写入只能写在「空」的页面上,不能覆盖写入已有数据的页面。 要重复利用已经写过的页面,需要对页面所在段整个做擦除操作,每个段是大概 128KiB 到 8MiB 这样的数量级。每个擦除段需要统计校验失败率或者跟踪擦除次数,以进行擦写均衡(Wear Leveling)。

3   擦写均衡(Wear Leveling)和映射层(Flash Translation Layer)

擦除段的容量大小是个折衷,更小的擦除段比如 128KiB 更适合随机读写, 因为每随机修改一部分数据时需要垃圾回收的粒度更小;而使用更大的擦除段可以减少元数据和地址映射的开销。 从擦除段的大小这里,已经开始有高端闪存和低端闪存的差异,比如商用 SSD 可能比 U 盘和 SD 卡使用更小的擦除段大小。

闪存盘中维护一个逻辑段地址到物理段地址的映射层,叫闪存映射层(Flash Translation Layer )。每次写一个段的时候都新分配一个空段, 写完后在映射表中记录其物理地址。映射表用来在读取时做地址转换,所以映射表需要保存在闪存盘控制器的 RAM 中,同时也需要记录在闪存内。具体记录方式要看闪存盘控制器的实现,可能是类似日志的方式记录的。

「段地址映射表」的大小可以由段大小和存储设备容量推算出来。比如对一个 64GiB 的 SD 卡,如果使用 4MiB 的段大小,那么需要至少 16K 个表项。假设映射表中只记录 2B 的物理段地址, 那么需要 32KiB 的 RAM 存储段地址映射表。对一个 512GiB 的 SSD ,如果使用 128KiB 的段大小, 那么至少需要 4M 个表项。记录 4B 的物理段地址的话,需要 16MiB 的 RAM 存储地址映射, 或者需要动态加载的方案只缓存一部分到 RAM 里。控制器中的 RAM 比 NAND 要昂贵很多,这里可以看出成本差异。

除了地址映射表,每个物理段还要根据擦除次数或者校验错误率之类的统计数据,做擦写均衡。有两种擦写均衡:

  • 动态擦写均衡(Dynamic Wear Leveling):每次写入新段时选择擦除次数少的物理段。
  • 静态擦写均衡(Static Wear Leveling):空闲时,偶尔将那些许久没有变化的逻辑段搬运到 多次擦除的物理段上。

低端闪存比如 SD 卡和 U 盘可能只有动态擦写均衡,更高端的 SSD 可能会做静态擦写均衡。 静态擦写均衡想要解决的问题是:盘中写入的数据可以根据写入频率分为冷热, 总有一些冷数据写入盘上就不怎么变化了,它们占用着的物理段有比较低的擦除计数。 只做动态擦写均衡的话,只有热数据的物理段被频繁擦写,加速磨损, 通过静态擦写均衡能将冷数据所在物理段释放出来,让整体擦写更平均。 但是静态擦写均衡搬运数据本身也会磨损有限的擦写次数,这需要优秀的算法来折衷。

除了擦写均衡用的统计数据外, FTL 也要做坏块管理。闪存盘出厂时就有一定故障率,可能有一部分坏块。 随着消耗擦写周期、闲置时间、环境温度等因素影响,也会遇到一些无法再保证写入正确率的坏块。 NAND Flash 上因为量子隧道效应,偶尔会有临时的校验不一致,遇到这种情况,除了根据 ECC 校验恢复数据, FTL 也负责尝试对同一个物理段多次擦除和读写,考察它的可用性。排除了临时故障后, 如果校验不一致的情况仍然持续,那么需要标注它为坏块,避免今后再写入它。

出厂时,闪存盘配有的物理段数量就高于标称的容量,除了出厂时的坏块之外,剩余的可用物理段可以用于 擦写均衡,这种行为称作 Over Provisioning 。除了盘内预留的这些空间,用户也可以主动通过分区的方式或者文件系统 TRIM 的方式预留出更多可用空间, 允许 FTL 更灵活地均衡擦写。

4   段内写入顺序与垃圾回收策略

段是闪存盘的擦写单元,考虑到段是 128KiB ~ 8MiB 这样的数量级,现实中要求每次连续写入一整段的话, 这样的块设备接口不像硬盘的接口,不方便普通文件系统使用。所以在段的抽象之下有了更小粒度的页面抽象, 页面对应到文件系统用的逻辑块大小,是 2KiB~8KiB 这样的数量级,每次以页面为单位读写。

写入页面时有段内连续写入的限制,于是需要段内映射和垃圾回收算法,提供对外的随机写入接口。 写入操作时, FTL 控制器内部先「打开(open)」一个段,等写入完成,再执行垃圾回收「关闭(close)」一个段。 写入过程中处于打开状态的段需要一些额外资源(RAM等)跟踪段内的写入状况,所以闪存盘同时能「打开」 的段数量有限。并且根据不同的垃圾回收算法,需要的额外资源也不尽相同,在 Optimizing Linux with cheap flash drives 一文中介绍几种可能的垃圾回收算法:

4.1   线性写入优化

假设写入请求大部分都是连续写入,很少有地址跳转,那么可以使用线性优化算法。

  • Open:当第一次打开一个段,写入其中一页时,分配一个新段。如果要写入的页不在段的开头位置,那么搬运写入页面地址之前的所有页面到新段中。
  • Write: 在 RAM 中跟踪记录当前写入位置,然后按顺序写下新的页面。
  • Close: 最后搬运同段中随后地址上的页面,并关闭整段,调整段映射表。

如果在段内写入了几页之后,又跳转到之前的位置,那需要在跳转时关闭当前段写入(并完整搬运剩下的页面), 然后重新打开这一段,搬运调转地址之前的页面,从跳转的页面位置开始写入。

线性优化算法的好处在于:没有复杂的页面地址映射,段内的逻辑页面地址就是物理页面地址。 读一页的时候根据页面偏移和当前写入位置就能判断读新物理段还是老物理段。遇到突然断电之类的情况, 即使丢失最近写入的新物理段,老物理段的数据仍然还在,所以没必要保存 RAM 中的地址映射到闪存元数据中。

线性优化算法的坏处是:每遇到一次乱序的写入,都要整段执行一次搬运,造成 写入放大(Write Amplification)

一些文档中,将这种地址映射垃圾回收方式叫做「段映射(Segment Mapping)」,因为从 FTL 全局来看只维护了擦写段的地址映射关系。

4.2   段内地址映射

对需要随机乱序写入的数据,可以使用段内地址映射。方式是额外在段外的别的闪存区域维护一张段内地址映射表, 像段地址一样,通过查表间接访问页面地址。

  • Open: 分配一块新的段,同时分配一个新的段内映射表。
  • Write: 每写入一页,在段内映射表记录页面的在新段中的物理地址。
  • Close: 复制老段中没有被覆盖写入的页到新段,并记录在段内映射表中,然后释放老段和老的段内映射表。

也就是说同时维护两块不同大小的闪存空间,一块是记录段数据的,一块是记录段内地址映射表的, 两块闪存空间有不同的写入粒度。可以在每个物理段内额外留出一些空间记录段内地址映射表,也可以在 FTL 全局维护一定数量的段内地址映射表。 每次读取段内的数据时,根据映射表的内容,做地址翻译。新段中页面的排列顺序将是写入的顺序, 而不是地址顺序。

根据实现细节,段内地址映射可以允许覆盖写入老段中的页面,但是可能不允许覆盖写入新段(正在写入的段) 中已经写入的页面,遇到一次连续的写请求中有重复写入某一页面的时候,就需要关闭这一段的写入,然后重新打开。

段内地址映射的优点是:支持随机写入,并且只要段处于打开状态,随机写入不会造成写入放大(Write Amplification)。

缺点是:首先地址映射这层抽象有性能损失。其次遇到突然断电之类的情况, 下次上电后需要扫描所有正打开的段并完成段的关闭操作。

和「段映射」术语一样,在一些文档中,将这种段内地址映射的方式叫做「页面映射(Page Mapping)」,因为从 FTL 全局来看跳过了擦写段这一层,直接映射了页面的地址映射。

4.3   日志式写入

除了大量随机写入和大量连续写入这两种极端情况,大部分文件系统的写入方式可能会是对某个地址空间 进行一段时间的随机写入,然后就长时间不再修改,这时适合日志式的写入方式。

日志式的写入方式中写入一段采用三个物理段:老物理段,用于日志记录的新物理段,和垃圾回收后的段。

  • Open: 分配一块新的段。可能额外分配一个用于记录日志的段,或者将日志信息记录在数据段内。
  • Write:每写入一页,同时记录页面地址到日志。
  • Close:再分配一个新段执行垃圾回收。按日志中记录的地址顺序将数据段中(新写入)的页面或者老段中 没有被覆盖的页面复制到垃圾回收结束的新段中。

日志式写入在写入过程中像段内地址映射的方式一样,通过日志记录维护页面地址映射关系, 在写入结束执行垃圾回收之后,则像线性写入的方式一样不再需要维护页面映射。 可以说日志式写入某种程度上综合了前面两种写入方式的优点。

日志式写入的优点:允许随机顺序写入,并且在执行垃圾回收之后,不再有间接访问的地址转换开销。

日志式写入的缺点:触发垃圾回收的话,可能比段地址映射有更大的写入放大(Write Amplification)。

在一些文档中,将这种日志式写入方式称作「混合映射(Hybrid Mapping)」,因为在段开启写入期间行为像页面映射, 在段关闭写入后行为像段映射。

5   针对特定写入模式的优化

上述三种地址映射和垃圾回收方式,各有不同的优缺点,根据数据块的写入模式可能需要挑选相应的策略。 并且「全局段地址映射表」、「段内页面地址映射表」、「写入页面地址日志」之类的元数据因为频繁修改, FTL 也可能需要用不同的策略来记录这些元数据。这里面向不同使用场景的闪存设备可能有不同的 FTL 策略,并且 FTL 可能根据逻辑地址来选择哪种策略。

5.1   混合垃圾回收策略

用来记录照片、视频等的 SD 卡、microSD、U盘等设备可能根据数据的逻辑地址,为特定文件系统布局优化, 这里特定文件系统主要是指 FAT32 和 exFAT 这两个 FAT 系文件系统。 FAT 系文件系统的特点在于, 地址前端有一块空间被用来放置 文件分配表(File Allocation Table) ,可以根据文件系统簇大小和设备存储容量推算出 FAT 表占用大小,这块表内空间需要频繁随机读写。 对 FTL 自身的元数据,和 FAT 表的逻辑地址空间,需要使用「段内地址映射」来保证高效的随机读写, 而对随后的数据空间可使用「线性写入优化」的策略。

右侧上图有张性能曲线,测量了一个 class 10 SDHC 卡上,不同读写块大小时,顺序读取、顺序写入、随机写入、 对 FAT 区域的写入之类的性能差异。下图是测量的读取延迟。可以看出 FAT 区域的随机写入和其余逻辑地址上有明显不同的性能表现。

为容纳普通操作系统设计的 eMMC 和 SSD 难以预测文件系统的读写模式,可能需要使用更复杂的地址映射和垃圾回收策略。 比如一开始假定写入会是顺序写入,采用「线性优化」方式;当发生乱序写入时,转变成类似「日志式写入」 的方式记录写入地址并做地址映射;关闭段时,再根据积累的统计数据判断,可能将记录的日志与乱序的数据 合并(merge)成顺序的数据块,也可能保持页面映射转变成类似「段内地址映射」的策略。

5.2   利用 NAND Flash 物理特性的优化

再考虑 NAND Flash 的物理特性,因为 MLC 要不断调整参考电压做写入, MLC 的写入比 SLC 慢一些,但是可以对 MLC Flash 使用 SLC 式的写入, FTL 控制器也可能利用这一点,让所有新的写入处于 SLC 模式,直到关闭整段做垃圾回收时把积攒的 SLC 日志段回收成 MLC 段用于长期保存。 一些网页将这种写入现象称作「SLC 缓存」甚至称之为作弊,需要理解这里并不是用单独的 SLC Flash 芯片做 writeback 缓存,更不是用大 RAM 做缓存,处于 SLC 模式的写入段也是持久存储的。

5.3   同时打开段数

上述地址映射和垃圾回收策略都有分别的打开(open)、写入(write)、关闭(close)时的操作, 闪存盘通常允许同时打开多个段,所以这三种操作不是顺序进行的,某一时刻可能同时有多个段处在打开的状态, 能接受写入。不过一个平面(Plane)通常只能进行一种操作(读、写、擦除),所以打开写入段时, FTL 会尽量让写入分部在不同的平面上。还可能有更高层次的抽象比如 Device、 Chip 、 Die 等等,可能对应闪存盘内部的 RAID 层级。

闪存盘能同时打开的段不光受平面之类的存储结构限制,还受控制器可用内存(RAM)限制之类的。 为 FAT 和顺序写入优化的 FTL ,可能除了 FAT 区域之外,只允许少量(2~8)个并发写入段, 超过了段数之后就会对已经打开的段触发关闭操作(close),执行垃圾回收调整地址映射,进而接受新的写入。 更高端的 SSD 的 FTL 如果采用日志式记录地址的话,同时打开的段数可能不再局限于可用内存限制, 连续的随机写入下按需动态加载段内地址映射到内存中,在空闲时或者剩余空间压力下才触发垃圾回收。

5.4   预格式化

FTL 可能为某种文件系统的写入模式做优化,同时如果文件系统能得知 FTL 的一些具体参数(比如擦除段大小、 读写页大小、随机写入优化区域),那么可能更好地安排数据结构,和 FTL 相互配合。 F2FS 和 exFAT 这些文件系统都在最开头的文件系统描述中包含了一些区域,记录这些闪存介质的物理参数。 闪存盘出厂时,可能预先根据优化的文件系统做好格式化,并写入这些特定参数。

5.5   TRIM 和 discard

另一种文件系统和 FTL 相互配合的机制是 TRIM 指令。TRIM 由文件系统发出,告诉底层闪存盘( 或者别的类型的 thin provisioning 块设备)哪些空间已经不再使用, FTL 接受 TRIM 指令之后可以避免一些数据搬运时的写入放大。关于 TRIM 指令在 Linux 内核中的实现,有篇 The best way to throw blocks away 介绍可以参考。

考虑到 FTL 的上述地址映射原理, TRIM 一块连续空间对 FTL 而言并不总是有帮助的。 如果被 TRIM 的地址位于正在以「段内地址映射」或「日志式映射」方式打开的写入段中,那么 TRIM 掉一些页面可能减少垃圾回收时搬运的页面数量。但是如果 TRIM 的地址发生在已经垃圾回收结束的段中, 此时如果 FTL 选择立刻对被 TRIM 的段执行垃圾回收,可能造成更多写入放大, 如果选择不回收只记录地址信息,记录这些地址信息也需要耗费一定的 Flash 写入。 所以 FTL 的具体实现中,可能只接受 TRIM 请求中,整段擦除段的 TRIM ,而忽略细小的写入页的 TRIM 。

可见 FTL 对 TRIM 的实现是个黑盒操作,并且 TRIM 操作的耗时也非常难以预测,可能立刻返回, 也可能需要等待垃圾回收执行结束。

对操作系统和文件系统实现而言,有两种方式利用 TRIM :

  1. 通过 discard 挂载选项,每当释放一些数据块时就执行 TRIM 告知底层块设备。
  2. 通过 fstrim 等外部工具,收集连续的空块并定期发送 TRIM 给底层设备。

直觉来看可能 discard 能让底层设备更早得知 TRIM 区域的信息并更好利用,但是从实现角度来说, discard 不光影响文件系统写入性能,还可能发送大量被设备忽略掉的小块 TRIM 区域。可能 fstrim 方式对连续大块的区间执行 TRIM 指令更有效。

6   TL;DR 低端 vs 高端

标题中的疑问「SSD就是大U盘?」相信看到这里已经有一些解答了。 即使 SSD 和U盘中可以采用类似的 NAND Flash 存储芯片,由于他们很可能采用不同的 FTL 策略,导致在读写性能和可靠性方面都有不同的表现。(何况他们可能采用不同品质的 Flash )。

如果不想细看全文,这里整理一张表,列出「高端」闪存盘和「低端」闪存盘可能采取的不同策略。 实际上大家买到的盘可能处于这些极端策略中的一些中间点,市场细分下并不是这么高低端分明。 比如有些标明着「为视频优化」之类宣传标语的「外置SSD」,对消费者来说可能会觉得为视频优化的话一定性能好, 但是理解了 FTL 的差异后就可以看出这种「优化」只针对线性写入,不一定适合放系统文件根目录的文件系统。

参数 低端 高端
段大小 8MiB 128KiB
段地址映射 静态段映射 日志式映射
随机写入范围 FTL元数据与FAT表区域 全盘
同时打开段数 4~8 全盘
物理段统计信息 无(随机挑选空闲段) 擦除次数、校验错误率等
擦写均衡 动态均衡(仅写入时分配新段考虑) 静态均衡(空闲时考虑搬运)
写入单元模式 TLC 长期存储 MLC, 模拟 SLC 日志

介绍完闪存类存储,下篇来讲讲文件系统的具体磁盘布局,考察一下常见文件系统如何使用 HDD/SSD 这些不同读写特性的设备。

by farseerfc at March 18, 2020 06:45 AM

Leo Shen

Build Fcitx5 IM module for proprietary software with its Qt library

So there's some really interesting stuff going on with Fcitx5, and for most time it has been stable enough to replace the aging Fcitx4.

For most open source software, they uses Qt Library provided by the operating system, and so they have no problem loading the corresponding IM module. But for some proprietary software, they have its own Qt library (likely due to the incompatibility between different Qt versions), which does not want to work with the system IM module at all. Yikes.

So we are gonna to fix it! The author of Fcitx, csslayer, has already wrote an awesome article about this topic (which you can find it here). This article just explain some more details about the procedure and change some steps to make it work with Fcitx5.

Prepare the stuff

For obvious reasons, you will need to build the library with the same Qt version that the absolutely proprietary software. Just take a look at where the Qt libraries live in, and some of them has the exact version number at the end of their file name (something like libQt5Core.so.5.9.6).

If they don't have a version number in their file name (like Mathematica), you can also just check the contents of the library. Use the following command:

1
strings libQt5Core.so.5 | grep "Qt 5"

should give you an exact version.

Then, we will need to download the corresbonding version of Qt from Qt website. In this example, we will try to add Fcitx5 support to Zoom, and (for now) they are using Qt 5.9.6, so just download the installer from here and install it. We only need Qt itself for Desktop (Something like Desktop gcc 64bit).

We will need a compiled version fcitx5 in the system. In Arch Linux and AOSC OS, this can be easily done by installing the package from official repository.

Also, we need extra-cmake-modules, also available in official repository.

Compile the module.

Clone the fcitx5-qt repository, and run this in the repository:

1
2
# $QT_PATH is where you install your Qt just now.
cmake -DCMAKE_PREFIX_PATH=$QT_PATH/Qt5.9.6/5.9.6/gcc_64 -DENABLE_QT4=0 .

It should go very smoothly if you have everything prepared.

Then, head toward $REPO/qt5/platforminputcontext, and run

1
make VERBOSE=1

And it should spill out a libfcitx5platforminputcontextplugin.so we need. Hooray!

Relink the module to the right Qt library

But not so fast. Since during the build, it is using the Qt library from our Qt installation, not the actual Qt library the proprietary software is actually using. Sometimes the library will (magically) work, but we'd still better link it to the correct library the program is going to use.

Check the output for the last output, there should be something like this:

1
/bin/c++ -fPIC -Wall -Wextra  -Wl,--no-undefined -Wl,--as-needed  -shared  -o libfcitx5platforminputcontextplugin.so CMakeFiles/fcitx5platforminputcontextplugin.dir/fcitx5platforminputcontextplugin_autogen/mocs_compilation.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/qfcitxplatforminputcontext.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/qtkey.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/main.cpp.o  -Wl,-rpath,/home/sya/Qt5.9.6/5.9.6/gcc_64/lib:/home/sya/Temp/fcitx5-qt/qt5/dbusaddons: /usr/lib/libFcitx5Utils.so.1.0 /home/sya/Qt5.9.6/5.9.6/gcc_64/lib/libQt5X11Extras.so.5.9.6 /usr/lib/libxcb.so ../dbusaddons/libFcitx5Qt5DBusAddons.so.1.0 /usr/lib/libxkbcommon.so /home/sya/Qt5.9.6/5.9.6/gcc_64/lib/libQt5Gui.so.5.9.6 /home/sya/Qt5.9.6/5.9.6/gcc_64/lib/libQt5DBus.so.5.9.6 /home/sya/Qt5.9.6/5.9.6/gcc_64/lib/libQt5Core.so.5.9.6

Note that at the end, the library is dynamically linked to the Qt libraries in our Qt installation. That's not correct. We need to replace it with the Qt library found in the software. Replace the path with the actual location of Qt lib in the proprietary software. The end result should look like this (still use zoom as an example):

1
/usr/bin/c++ -fPIC -Wall -Wextra  -Wl,--no-undefined -Wl,--as-needed  -shared  -o libfcitx5platforminputcontextplugin.so CMakeFiles/fcitx5platforminputcontextplugin.dir/fcitx5platforminputcontextplugin_autogen/mocs_compilation.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/qfcitxplatforminputcontext.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/qtkey.cpp.o CMakeFiles/fcitx5platforminputcontextplugin.dir/main.cpp.o  -Wl,-rpath,/opt/zoom:/home/sya/fcitx5/fcitx5-qt/qt5/dbusaddons: /usr/lib/libFcitx5Utils.so.1.0 /opt/zoom/libQt5X11Extras.so.5.9.6 /usr/lib/libxcb.so ../dbusaddons/libFcitx5Qt5DBusAddons.so.1.0 /usr/lib/libxkbcommon.so /opt/zoom/libQt5Gui.so.5.9.6 /opt/zoom/libQt5DBus.so.5.9.6 /opt/zoom/libQt5Core.so.5.9.6

After this, we can now copy the generated library to the destination folder, which is usually located in platforminputcontexts inside the proprietary software's Qt library directory.

And then, everything should work.

March 18, 2020 01:19 AM

March 15, 2020

frantic1048

Random Pyon Pyon Photography

糊了一下图片的 build,现在没有 webp 支持的用户应该也能看个亮了,再来点照片看看 ( ಠ ͜ʖ ಠ)

(PLUM) Ujimatsu Chiya Cafe Style

IMG_1732_01 IMG_1727_01 IMG_1730_01

(Aquamarine) Alice Cartelet Miko

IMG_0211 IMG_0215 IMG_0217

(Aquamarine) Kujou Karen Miko

DSC_0696

(Banpresto) Oshino Shinobu

去年去大阪玩在中古店里遇到的野生小忍,一眼过去就十分心动然后入手了(

shinobu-half.fusion.2_03 shinobu.fusion_04

(Pulchra) Yoshino

从插画开始关注了老久的 Yoshino,十分棒。比较惨的到手时候断手了,换货至今未归 ( ͠° ͟ʖ ͡°)

DSC_0000_BURST20181209214409075 DSC_0000_BURST20181209214923186

Misc

DSC_0000_BURST20180903222147786 IMG_1750 IMG_1752

March 15, 2020 12:00 AM

March 11, 2020

frantic1048

Random Chino Photography

过去的一些智乃手办照片 ╰( ͡° ͜ʖ ͡° )つ ──☆*:・゚

(FuRyu) Chino Ohana no Buranko

Chino Ohana no Buranko Chino Ohana no Buranko Chino Ohana no Buranko Chino Ohana no Buranko Chino Ohana no Buranko Chino Ohana no Buranko

(Stronger) Chino Mahou Shuojo ver.

Chino mahou shuojo Chino mahou shuojo 2

(GSC) Chino

GSC Chino

(PLUM) Chino Cafe Style

Chino Cafe Style Chino Cafe Style 2

(Easy Eight) Chino and Rabbit Dolls

拍的时候还没来得及部署周围的那六只小兔子 (¯ . ¯٥)

Chino and Rabbit Dolls 1 Chino and Rabbit Dolls 2

(Chara-Ani, Toy's Works) Chino Cheer Girl

Chino Cheer Girl

(FuRyu) Chino Sailor ver.

不知为何感觉和可乐很搭(

Chino Sailor Chino Sailor 2

(FuRyu) Chino Tea Party ver.

Chino Tea Party Chino Tea Party 2

(SEGA) Chino Pajama ver.

Chino pajama

March 11, 2020 12:00 AM

March 06, 2020

farseerfc

柱面-磁头-扇区寻址的一些旧事

在 SSD 这种新兴存储设备普及之前,很长一段时间硬盘是个人计算机的主要存储设备。 更往前的磁带机不常见于个人计算机,软盘的地位很快被硬盘取代,到 SSD 出现为止像 MiniDiscDVD-RAM 等存储设备也从未能挑战过硬盘的地位。硬盘作为主要存储设备,自然也影响了文件系统的设计。

这篇笔记稍微聊一聊硬盘这种存储设备的寻址方式对早期文件系统设计的一些影响,特别是 柱面-磁头-扇区寻址(Cylinder-head-sector addressing, 简称CHS寻址)的起源和发展。 大部分内容来自维基百科 Cylinder-head-sector 词条 这里只是记录笔记。现今的硬盘已经不再采用 CHS 寻址,其影响却还能在一些文件系统设计中看到影子。

柱面、磁头、扇区以及相关术语

磁盘示意图(来自维基百科 Cylinder-head-sector 词条
chs-illustrate-trans.svg

如右图所示,一块硬盘(Hard Disk Drive, HDD)是一个圆柱体转轴上套着一些磁碟片(platter), 然后有一条磁头臂(actuator arm)插入磁碟片间的位置,加上一组控制芯片(controller)。 每个磁碟片有上下两面涂有磁性材质,磁头臂上有一组磁头(head),每个磁头对应磁盘的一个面, 所以比如一个 3 碟的硬盘会有 6 个磁头。

每个磁碟片上定义了很多同心圆的磁头轨道,叫做磁道(track),磁道位于盘面上不同半径的位置, 通过旋转磁碟臂能让磁头移动到特定的半径上,从而让读写磁头在不同的磁道间跳转。 不同磁头上同磁道的同心圆共同组成一个柱面(cylinder),或者说移动磁碟臂能选定磁盘中的一个柱面。 磁道上按等角度切分成多个小段,叫做扇区(sector),每个扇区是读写数据时采用的最小单元。 早期在 IBM 大型机之类上使用的硬盘的扇区大小比较小,到 IBM PC 开始个人计算机用的硬盘扇区基本被统一到 512 字节。现代硬盘内部可能采用 Advanced Format 使用 4K 字节扇区。

在早期软盘和硬盘的寻址方式被称作「柱面-磁头-扇区寻址」,简称 CHS 寻址, 是因为这三个参数是软件交给硬件定位到某个具体扇区单元时使用的参数。 首先柱面参数让磁头臂移动到某个半径上,寻址到某个柱面,然后激活某个磁头,然后随着盘面旋转, 磁头定位到某个扇区上。

「柱面-磁头-扇区」这个寻址方式,听起来可能不太符合直觉,尤其是柱面的概念。直觉上, 可能更合理的寻址方式是「盘片-盘面-磁道-扇区」,而柱面在这里是同磁道不同盘片盘面构成的一个集合。 不过理解了磁盘的机械结构的话,柱面的概念就比较合理了,寻址时先驱动磁头臂旋转, 磁头臂上多个磁头一起飞到某个磁道上,从而运动磁头臂的动作定义了一个柱面。 柱面和磁头(CH)组合起来能定位到某个特定的磁道,画张图大概如下图所示:

tikz diagram

上图中值得注意的是磁道的编号方式,我用相同的颜色画出了相同的磁道。因为按照 CHS 的顺序寻址,所以先定位柱面,然后选定磁头。磁盘上按半径从外向内定义柱面的编号,最外圈的磁道位于 0号柱面,由0号磁头开始。随着柱面编号增加,逐步从外圈定位到内圈。

物理 CHS 寻址

以上术语中,柱面号和磁头号直接对应了硬盘上的物理组成部分,所以在物理 CHS 寻址方式下,通过扇区地址的写法能对应到扇区的具体物理位置。之所以这样描述扇区, 是因为早期的软盘和硬盘驱动器没有内置的控制芯片,可以完全由宿主系统执行驱动程序驱动。

在 IBM PC 上,驱动软盘和硬盘的是 CPU 执行位于主板 BIOS (Basic Input/Output System) 中的程序,具体来说操作系统(比如DOS)和应用程序调用 INT 13H 中断,通过 AH=02H/03H 选择读/写操作,BIOS 在中断表中注册的 13H 中断处理程序执行在 CPU 上完成读写请求。调用 INT 13H 读写扇区的时候,CPU 先通过 INT 13H AH=0CH 控制硬盘的磁头臂旋转到特定柱面上,然后选定具体磁头,让磁头保持在磁道上读数据, 通过忙轮训的方式等待要读写的扇区旋转到磁头下方,从而读到所需扇区的数据。在 DOS 之后的操作系统, 比如早期的 Windows 和 Linux 和 BSD 能以覆盖中断程序入口表的方式提供升级版本的这些操作替代 BIOS 的程序。

以上过程中可以看出两点观察:

  1. CHS 寻址下,跨磁道的寻址(不同 CH 值),和磁道内的寻址(同 CH 不同 S ),是本质上不同的操作。跨磁道的寻址有移动磁头臂的动作,会比磁道内寻址花费更多时间。
  2. 通过扇区号的磁道内寻址是个忙轮训操作,需要占用完整 CPU 周期。这也隐含扇区号在一个磁道内的物理排列不必是连续的。

实际上扇区号的物理排列的确不是连续的,每个物理扇区中除了用512字节记录扇区本身的数据, 还有扇区的开始记录和结束记录,写有扇区编号和扇区校验码。每读到一个扇区, CPU 可能需要做一些额外操作(比如计算比对校验、写入内存缓冲区、调整内存段页映射) 后才能继续读下一个扇区,如果物理排列上连续编号扇区,可能等 CPU 做完这些事情后磁头已经旋转到之后几个扇区上了。所以出厂时做磁盘低级格式化的时候, 会跳跃着给扇区编号,给 CPU 留足处理时间。比如下图:

tikz diagram

上图中假设有3个柱面,每个柱面6个磁头,每个磁道内11个扇区,并且画出了三种不同的扇区编号跳转情况, 分别是磁道内的扇区跳转(+3),柱面内的磁头跳转(+5),以及柱面间跳转(+10)。 实际磁盘上的柱面数、扇区数要多很多,寻址时需要跳转的距离也可能更长,这里只是举例说明。 图中和实际情况相同的是,柱面号和磁头号从 0 开始编号,而扇区号从 1 开始编号, 所以做逻辑地址换算的时候要考虑编号差异。

早期 IBM PC 的 BIOS 使用 24bit 的 CHS 地址,其中 10bit 柱面(C)、 8bit 磁头(H)、 6bit 扇区(S)。从而用物理 CHS 寻址方式的软盘和硬盘驱动器最多可以寻址 1024 个柱面,256 个磁头, 63 个扇区,其中扇区数因为从 1 开始编号所以少了 1 个可寻址范围。比如 3.5 吋高密度(HD)软盘有双面, 出厂时每面 80 磁道,每磁道 18 扇区,从而能算出 1,474,560 字节的容量。

如此跳跃编号扇区之后,不是总能给磁道中所有扇区编号,可能在磁道的末尾位置留几个没有使用的扇区空间, 这些是磁道内的保留扇区,可以在发现坏扇区后使用这些隐藏扇区作为替代扇区。当然读写替代扇区的时候 因为扇区寻址不连续可能会有一定性能损失。

因为物理 CHS 寻址下,磁盘由 CPU 执行驱动程序来驱动,所以以上扇区跳跃的长短实际是由 CPU 的速度等因素决定的,理论上 CPU 越快,跳跃间隔可以越短,从而磁盘读写速度也能加快。磁盘出厂时, 厂商并不知道使用磁盘的计算机会是怎样的性能,所以只能保守地根据最慢的 CPU 比如 IBM 初代 PC 搭配的 8086 的速度来决定跳跃间隔。所以在当年早期玩家们流传着这样一个操作:买到新硬盘, 或者升级了电脑配置之后,对硬盘做一次 低级格式化(Low level formating) ,聪明的低级格式化程序能智能安排扇区编号,提升硬盘读写速度,也能跳过已知坏道位置继续编号, 甚至可能将更多保留扇区暴露成可用扇区。这对现代有硬盘控制器的硬盘而言已经没有意义了。

逻辑 CHS 寻址

随着硬盘容量不断增加, BIOS 中用来 CHS 寻址的地址空间逐渐不够用了。早期 24bit 地址按 C H S 的顺序分为 10 8 6 的位数,用 8bit 来寻址磁头最多可以有 256 个磁头,而只有 10bit 来寻址柱面,就只能有 1024 个柱面。最初 IBM 这么划分是因为早期用于 IBM 大型机之类的硬盘可以有 厚厚一叠的盘片组,同样的寻址方式就直接用于了 IBM PC 。而 PC 用的硬盘迫于硬盘仓空间大小, 有厚度限制,硬盘中物理盘面可能只有四五个盘片,硬盘容量增加主要是增加盘片表面的数据密度而非增加盘片数量。

于是逐渐地,硬盘厂商开始对 CHS 寻址的地址空间做一些手脚。比如最初的简单想法是重新定义 CH ,将一些磁头数挪用做柱面数。从而有了逻辑 CHS 寻址,其中 CH 是固定一组,通过简单换算从 CH 值找到物理的柱面和磁头数。结合 CH 而不映射 S 的优势在于,从操作系统和文件系统来看依然能根据逻辑 CHS 地址估算出地址跳转所需大概的时间,只是原本一次切换磁头的动作可能变成一次短距离的切换柱面。

此时的操作系统和文件系统已经开始出现针对 CHS 寻址特点的优化方式, 尽量减少跨磁道的寻址能一定程度提升读写速度,跨磁道时的磁道间距离也会影响寻道时间, 文件系统可能会根据CHS地址来安排数据结构,优化这些寻址时间。

即便使用没有针对 CHS 寻址方式优化过的操作系统和文件系统,比如局限在早期 Windows 和 FAT 系文件系统上,早期这些桌面系统用户们仍然能自己优化磁盘读写性能:通过分区。 分区是硬盘上连续的一段空间,早期由于 BIOS 和 bootloader 的一些技术限制, 每个分区必须对齐到柱面大小上。早期 PC 玩家们通过把一个大硬盘切分成多个小分区, 使用时尽量保持近期读写针对同一个分区,就可以减少寻址时的额外开销,改善读写速度。

于是隐含地,CHS 寻址导致底层硬盘和上层操作系统之间有一层性能约定: 连续读写保证最快的读写速度 。硬盘实现 CHS 寻址时,调整扇区编号方式让连续的 CHS 地址有最快读写速度,文件系统也根据这个约定, 按照 CHS 地址的跳跃来估算读写速度耗时并针对性优化。

区位记录(Zone bit recoding, ZBR)

以上物理 CHS 寻址,其实依赖一个假设: 每个磁道上有同样数量的扇区 。早期硬盘上也的确遵循这个假设, 所以我们上面的图示里才能把一个盘面上的扇区展开成一张长方形的表格,因为每个磁道的扇区数是一样的。 实际上当时的硬盘都是恒定角速度(constant angular velocity, CAV)的方式读写,无论磁头在哪儿, 盘片都旋转保持恒定的转速,所以对磁头来说在单位时间内转过的角度影响读写二进制位的数量, 而磁头扫过的面积在这里没有影响。

区位记录(来自维基百科 Zone bit recording 词条
DiskStructure.svg

不过随着硬盘容量增加,盘面的数据密度也随之增加,单位面积中理论能容纳的二进制位数量有限。 理论上,如果保持相同密度的话,盘片外圈能比内圈容纳更多数据。因此硬盘厂商们开始在盘面上将磁道划分出 区块(zone),外圈区块中的磁道可以比内圈区块中的磁道多放入一些扇区。这种方式下生产出的硬盘叫 区位记录硬盘(Zone bit recoding, ZBR),相对的传统固定磁道中扇区数的硬盘就被叫做恒定角速度(CAV) 硬盘。

如右图所示,区位记录在硬盘上将多个柱面组合成一个区块,区块内的磁道有相同数量的扇区, 而不同区块的磁道可以有不同数量的扇区,外圈区块比内圈区块有更多扇区。

显然要支持 ZBR ,物理 CHS 寻址方式不再有效,于是 ZBR 硬盘将原本简单的地址换算电路升级为更复杂的磁盘控制器芯片,替代 CPU 来驱动硬盘,把来自文件系统的逻辑 CHS 地址通过换算转换到物理 CHS 地址,并且驱动磁头做跳转和寻址。 从而有了独立的控制芯片之后,硬盘读写扇区的速度不再受 CPU 速度影响。有了完整的逻辑-物理地址转换后, 逻辑扇区编号不再对应物理扇区编号,上述编号跳转和坏扇区处理之类的事情都由磁盘控制芯片代为完成。 从而 CHS 地址已经丧失了物理意义,只留下 连续读写保证最快的读写速度 这样的性能约定。

有了 ZBR 之后,硬盘读写速度也不再恒定,虽然仍然保持恒定转速,但是读写外圈磁道时单位时间扫过的扇区 多于读写内圈磁道时扫过的扇区。所以 ZBR 硬盘的低端地址比高端地址有更快的读写速度, 通过硬盘测速软件能观察到阶梯状的「掉速」现象。

逻辑地址转换也会造成逻辑 CHS 寻址能访问到的扇区数少于物理 CHS 寻址的现象, 磁盘中扇区被重新编号后可能有一些扇区剩余,于是 ZBR 硬盘的出厂低级格式化可能会均分这些访问不到的扇区 给每个磁道作为保留扇区,留作坏扇区后备。

另外有了独立磁盘控制器芯片之后,扇区内的校验算法也不再受制于 BIOS INT 13H 接口。 原本 BIOS 的 INT 13H 接口定义了每个扇区 512 字节,额外配有 4 字节校验, 32bit 的校验码对 4096bit 的数据来说,只能允许一些简单的校验算法,比如 32bit CRC ,或者比如 汉明码 对 4096bit 的数据需要 13bit 的校验。突破了校验算法限制后硬盘可以在物理扇区中放更多校验位,使用更复杂的 ECC 算法,提供更强的容错性。 IDE/SATA 接口的硬盘由内部控制器负责计算和比对校验,而 SAS 接口的硬盘(主要用于服务器)可以读取 520/528 字节长度的扇区,包含额外校验位。

通过 ZBR ,逻辑 CHS 寻址不再局限在具体每磁道扇区数等物理限制上,但是仍然局限在 CHS 总位数。 24bit 的 CHS 地址能寻址 \(1024*256*63 = 16515072\) 个扇区,也就是 8064MiB 的空间。 于是早期很多操作系统有 7.8G 硬盘大小的限制。后来 ATA/IDE 标准提升了 CHS 寻址数量,从 24bit 到 28bit 到 32bit ,不过在系统引导早期仍然依赖 BIOS 最基本的 24bit CHS 寻址能力,于是那时候安装系统时要求引导程序装在前 8G 范围内也是这个原因。

从 CHS 到 LBA

随着硬盘大小不断提升,无论是操作系统软件层,还是硬盘厂商硬件层,都逐渐意识到逻辑 CHS 寻址是两边相互欺骗对方的骗局:文件系统根据假的 CHS 地址的提示苦苦优化,而硬盘控制器又要把物理 CHS 模拟到假的 CHS 地址上以兼容 BIOS 和操作系统。和 CS 领域太多别的事情一样, CHS 寻址过早地暴露出太多底层抽象细节,而上层软件又转而依赖于这些暴露出的细节进行优化, 底层细节的变动使得上层优化不再是有意义的优化。

于是 ATA 标准 引入了 逻辑块寻址(Logical Block Addressing, LBA) 来替代 CHS 寻址,解决其中的混乱。LBA 的思路其实就是逻辑 CHS 寻址的简单换算,因为 CHS 寻址下 S 从 1 开始计算,而 LBA 使用连续扇区编号,从 0 开始编号,所以换算公式如下:

\begin{equation*} LBA 地址 = ( C \times 磁头数 + H ) \times 扇区数 + ( S - 1 ) \end{equation*}

使用 LBA 寻址,操作系统和文件系统直接寻址一个连续地址空间中的扇区号, 不应该关心柱面和磁头之类的物理参数,将这些物理细节交由磁盘控制器。 对操作系统和文件系统这些上层软件而言,LBA寻址的抽象仍然保证了 连续读写提供最快的读写速度 ,文件系统仍然会尝试根据 LBA 地址优化,尽量连续读写从而减少寻道时间。

从 CHS 寻址切换到 LBA 寻址,需要硬盘和操作系统两方面的努力,所以很长一段时间, 硬盘同时支持两种寻址方式,在控制器内部做转换。最后需要放弃支持的是深植了 CHS 寻址的 BIOS ,使用 BIOS 引导的 MBR 引导程序还在用 CHS 寻址方式读取数据加载操作系统,直到大家都切换到 UEFI 。

并且随着硬盘使用 LBA 寻址,导致上层软件很难预测底层硬件实际切换柱面切换磁头之类的时机, 潜在地导致一些性能不确定性。于是硬盘控制器在除了负责实际驱动物理磁盘之外, 还开始负责维护一块盘内缓冲区,实现盘内的 IO 队列。缓冲区的存在允许磁盘控制器同时接收更多来自上层软件 的读写请求,转换成实际物理布局参数,并根据磁盘物理布局来调整读写顺序,增加总体吞吐率。 比如 ATA TCQSATANCQ 就是这样的盘内队列协议。

当然有缓冲区和盘内队列的存在也使得突然断电之类的情况下更难保证数据一致性,于是 SCSI/SATA 标准开始约定特殊的请求,从操作系统能发送命令让底层设备清空自己的读写队列。

叠瓦磁记录(Shingled Magnetic Recording, SMR)

逐渐从历史讲到了现在,随着硬盘记录密度的不断增加,硬盘厂商们也在不断发明新技术尝试突破磁盘记录的物理极限。 因为有了在硬盘上独立的控制器,并且切换到了逻辑块地址(LBA)的寻址方式, 操作系统大部分时候不用再关心底层硬盘的物理技术革新,比如垂直写入技术(perpendicular magnetic recording, PMR)将磁头记录方式从水平转换成垂直记录,增加了记录密度,但不影响寻址方式。

不过技术革新中也有影响寻址方式的技术,比如 叠瓦磁记录技术(Shingled Magnetic Recording, SMR) 。 SMR 技术基于一个技术事实:物理上磁头的写入头(write head)需要比读取头(read head )占用更大面积,如果按照写入头的物理极限放置磁记录,那么对于读取头会有很多空间浪费。从而 SMR 试图让相邻磁道的写入有部分重叠,从而增加记录密度。即便重叠了相邻磁道,读取磁道还是能随机定位, 而写入磁道会覆盖它后面叠加上的磁道,所以写入磁道必须严格按地址顺序写入。为了满足随机顺序写入的需要, SMR 硬盘把连续的几个磁道组织成区块(zone),在一个区块内必须按顺序写入。 这里的区块可以和区位记录(ZBR)是同样的区块,也可以独立于 ZBR 做不同大小的区块分割。

这种区块内连续写入的要求,很像是 SSD 这种基于闪存介质的记录方式, SMR 硬盘也同样像 SSD 一样在磁盘控制器内引入 日志结构式的记录方式,采用类似的 GC 算法 ,收到随机写入请求的时候,在区块间执行 GC 搬运数据块,对操作系统提供可以任意写入的抽象接口。

当然这种类似闪存介质的 FTL 的抽象有对读写性能的直接影响。SMR 硬盘可以将这些细节完全隐藏起来( Device Managed),或者完全暴露给宿主系统(Host Managed ),或者在读写时隐藏细节的同时在宿主想查询的时候提供接口查询(Host Aware)。和 SSD 一样,消费级的 SMR 硬盘通常选择隐藏细节只在被询问时暴露,完全暴露细节的设备通常只在企业服务器级别 的产品中看到。

可以期待,随着 SMR 硬盘的逐渐普及,文件系统设计中也将更多考虑 SMR 的特性加以优化。这些优化可能参考 对 SSD 的优化(比如尽量连续写入),但是又不能完全照搬(比如 SSD 需要考虑写平衡而 SMR 硬盘不需要,比如 SSD 不用担心随机寻道时间而 SMR 硬盘需要)。这些对现在和未来文件系统的设计提供了更多挑战。

4KiB 扇区大小

不局限于硬盘,存储设备发展中另一个方向是增加扇区大小。如前所述,在应用于 PC 之前的硬盘设计也曾有过比 512 字节更小的扇区大小,而自从 PC 普及之后 512 字节扇区逐渐成为主流, 甚至到了挥之不去的地步。随着硬盘容量提升,直接寻址 512 字节的扇区显得不再那么高效, 文件系统内部也早已把多个扇区合并成一个逻辑簇(cluster)或者块(block),按簇或块的粒度管理。 在底层硬件同样也是按照 512 字节大小划分扇区,每个扇区都要独立计算校验,如果能增大扇区大小到比如 4KiB,将能更经济地安排扇区校验码,从而得到更多可用容量。可见 512 字节扇区大小这一设计,和 CHS 寻址一样,逐渐成为了操作系统和硬盘厂商彼此间互相努力维护的谎言。

硬盘物理扇区提升为 4KiB 大小的设计,叫做「 先进格式化(Advanced Format) 」,这样的硬盘叫做先进格式化硬盘(AFD)。在此基础上,硬盘控制器可以提供模拟 512 字节扇区的模拟层, 叫做 512e ,也可以直接提供 4K 大小的扇区给操作系统,叫做 4K native (4Kn)。 操作系统和文件系统要尽量避免依赖 512e 以提供最优性能,支持 4Kn 扇区寻址也是现在和未来 文件系统设计中一个重要挑战。

双磁头臂(Dual Actuator)

除了提升容量,硬盘发展的另一个方向是提升读写速度。通过上述 CHS 寻址方式可见, 传统方式下提升硬盘读写速度有两种方式:

  1. 提升磁记录密度
  2. 提升(磁头臂和盘片)转速

第一种方式提升记录密度,在增加容量的同时也能提升硬盘读写速度,所以是长久以来硬盘厂商的主要方式。 第二种方式提升转速则很快就遇到了物理瓶颈,硬盘以前是 5400rpm 现在最高能到 15000rpm 附近,高速旋转的盘片就像一个螺旋桨一样,外圈线速度已经到了接近声速,很难再往上提升。 以及盘片转速影响连续读写速度,而磁头臂转速影响寻道速度,高速寻道对磁头臂旋转有极高精度要求。

所以长久以来,衡量硬盘速度有两项指标:连续读写速度和每秒操作数(IOPS),随着容量提升, 也在提升连续读写速度,但是很难提升 IOPS ,相对而言随机寻道所需的开销越来越昂贵。

目前硬盘厂商们在尝试一种新的方式提升硬盘 IOPS :增加一条磁头臂。一个硬盘驱动器内封入两组甚至多组 磁头臂,每个磁头臂能独立旋转,从而能独立寻址定位。这样的硬盘叫双/多磁头臂(Dual/Multi Actuator)硬盘。

从操作系统角度来看,双磁头臂硬盘更像是一根连接线上接有等容量的两个独立驱动器, 可以在盘内控制器上组 RAID0 ,或者把两个磁头臂都暴露给操作系统,由操作系统组 RAID0 或更智能地使用独立寻址的能力。

结论(TL;DR)和预告

软件层面的优化与硬件层面的革新一直是一组矛盾。长久以来文件系统和硬盘设备在关于寻址方式的磨合中, 逐渐演化出一条真理,也是我文中一直在强调的: 连续读写提供最快的读写速度 。文件系统总是能根据底层设备暴露出的一些抽象泄漏,比如物理 CHS 布局,比如 512 字节扇区大小, 针对性做更多优化,但是随着底层设备的技术革新这些优化也随之成为泡影。

从 SMR 技术中也能看出, 硬盘的读写接口也在逐渐向 SSD 的接口靠拢,从而文件系统的「优化」也在逐渐 向这种「倾向顺序写入」的方向优化。关于这些发展趋势待我有空再谈。

by farseerfc at March 06, 2020 06:45 AM