Arch Linux 星球

July 05, 2020

百合仙子

品《当我开始做软件开发时,最希望听到的七条建议》

本文来自依云's Blog,转载请注明。

在 Twitter 上看到一张写在长图片里的文字《当我开始做软件开发时,最希望听到的七条建议》,记录一下感想。

读书

书确实要读,但每年都读软件工程方面的书,会受边际效用递减的影响的。毕竟软件工程并不是很大的话题,而工程还是得实践出真知。看过几本之后,不如把精力花在更理论和学术的方面,比如编程语言理论、分布式算法、概率论与统计、博弈论,等等。总之就是要「move on」。而实际上我读的书更杂一些。

编程语言

学习多种编程语言是极好的。但也千万不要限于两种,尤其是别 Java + C# 或者 C + C++ 这种组合。会一叶障目的。我在大学期间学习过非常多种编程语言(几乎是把我听说到的都学了个开头)。

我的建议是,至少 C 系一门,ML 系一门,Lisp 系一门,脚本系一门,简单学一学。然后挑自己感兴趣、用得上和有钱途的深学。了解过的编程语言越多样,学习新语言就越容易,甚至可以在不学习的情况下上手阅读、做小改动。

测试

测试确实挺有用的。但更多的时候,它挺烦的:不是代码本身坏了,而是测试坏了……

现在我更看重的是,一个像 Rust 甚至是 Idris 那样强大的类型系统,能在省力的同时发现更多的疏忽。

Python 的类型注解不是很强大,不过也是非常有用的。

重构

我不太明白作者在这里讲重构是什么意思。

我的习惯是,按需重构。如果一份代码一直良好运行,我才不管它写得有多糟糕呢。但当我要修改它的时候(不管是为了添加新功能还是修 bug),我会把看着不舒服的代码给顺势改掉。

一个很重要的点是,不要为了重构而重构。另一个点是,重构中的代码应当是可以正确运行的。至少每天工作结束时提交,提交的时候代码应当是可以正确运行的(可能有 bug,但不能有未完成而无法运行的部分)。这样,重构可以随时因为各种原因中止,不会影响后续不相关的工作,也不会分出分支来时间一长根本合不回去。

我才不要假设我的代码可以使用五年。再过两个月 lilac 就要六岁了,而 nvchecker 已经七岁了呢。

lilac 经历过三次重构。开始重构的原因是,一开始并没有想到会发展成现在这样子,管理一千多个包,每天都要打好几十个。所以那时候的代码写得很烂,全局变量也很多,以至于新功能加不上去。前两次重构都是在我有闲的时候,拉一个分支出来,看哪里不顺眼就改哪里。可问题是,改了几天之后,我完全无法预测这代码还跑不跑得动。另一方面,修新 bug 当然是在老代码上改的。那我重构到一半的新代码怎么办呢?作为业余项目,这种情况持续一些天之后,我稍一忙碌,把它放下之后,我就再也想不起来我当时的重构思路了。

第三次重构,并不彻底,但是它很有效。因为每次改完收工的时候,我会判断它现在能正常跑起来吗?如果觉得不行,那就 git checkout .。有希望才提交,然后上线。跑出 bug 就去修。忙了就放下,等有时间才继续。思路不那么重要了,因为代码是好的,换个思路也可以继续。

当然了,类型注解在重构过程中我加上了许多,也避免了很多疏忽导致的 bug 被提交上线。

结对编程

并没有人和我结对啊……

工作经验和舒适区

我不是很理解什么叫「舒适区」。不过我主动做的工作,要么能解决我当前面对的问题,或者是想要达成的目的,要么能让我学到有意思的东西。一直重复昨天的事,你们把这个叫「舒适」??

分享

这正是我正在做的事情呢。不过很可惜的是,我并不太会讲话,所以现在流行的直播我还是算了。写作还是比视频有意思,又省力、又能有深度,多好。

by 依云 at July 05, 2020 05:01 PM

June 28, 2020

Alynx Zhou

修复更换 PCI-E 插槽后 Windows 下无线网卡的名称问题

这是一篇其实没什么逻辑也没什么技术的文章,但踩到这个坑又很头痛,所以记录一下。

June 28, 2020 11:41 AM

June 17, 2020

Alynx Zhou

装机小记

由于笔记本散热和性能实在是不适合打游戏(有一说一,Optimus 双显卡还是没有直接单卡来得爽),所以很早就想组装一台台式机。特别是最近一直和高中同桌玩 Dota2,我的笔记本如果直播 Dota2,直播推流就会十分卡顿,而我又不想像 CSGO 一样降低画质玩。

June 17, 2020 03:12 PM

可能只适合我自己的 RIME 配置

为什么我要折腾这个

在第 n 次忍受不了 RIME 的奇怪操作逻辑之后,我终于决定彻底教育一下这个不听话的输入法,考虑到已经有 n - 1 次失败的前提,做这个决定并不容易。

June 17, 2020 03:03 PM

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 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.

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.

March 18, 2020 01:19 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

March 02, 2020

中文社区新闻

firewalld>=0.8.1-2 更新需要手动干预

firewalld 包在 0.8.1-2 之前的版本打包时遗漏了编译 python 模块。这已在 0.8.1-2 中修复,所以更新时需要覆盖掉没有被跟踪到的 pyc 文件。如果你升级时遇到如下报错:

firewalld: /usr/lib/python3.8/site-packages/firewall/__pycache__/__init__.cpython-38.pyc exists in filesystem
firewalld: /usr/lib/python3.8/site-packages/firewall/__pycache__/client.cpython-38.pyc exists in filesystem
firewalld: /usr/lib/python3.8/site-packages/firewall/__pycache__/dbus_utils.cpython-38.pyc exists in filesystem
...更多报错...

那么请使用如下命令升级:

pacman -Suy --overwrite /usr/lib/python3.8/site-packages/firewall/\*

译注:
如果升级 firewalld 前删除了 firewalld 包,下次安装 firewalld 包仍然会有文件冲突,此时请使用:

pacman -Suy --overwrite /usr/lib/python3.8/site-packages/firewall/\* firewalld

安装 firewalld 包。

by farseerfc at March 02, 2020 01:38 AM

February 26, 2020

中文社区新闻

Arch Linux 项目负责人的未来

Aaron Griffin 于 2020-02-24 发布:

大家好,

以前当我在 Arch 更活跃的时候大概还有很多人认识我,但是现在大概大部分人只在网站上看到过我的名字。我已经参与 Arch 很长一段时间了,在 2007 年的时候从 Judd 接过这个项目的负责人位置。但是就像很多事情一样,我的参与随着时间已经下滑到底端。现在是改变的时候了。

Arch Linux 需要有更多参与的领导来做出困难抉择并带领整个计划走向未来方向。而我不再适合做这件事。

经过团队共同努力,Arch Linux 团队决定了未来推选负责人的一套新流程。从今开始,团队将通过选举方式推选出任期两年的负责人。新流程的细节可以参考这里

在第一次正式选举中,Levente Polyak (anthraxx), Gaetan Bisson (vesath), Giancarlo Razzolini (grazzolini),和 Sven-Hendrik Haase (svenstaro) 作为候选人,通过 58 名记名投票,选举出了新的当选者:

Levente Polyak (anthraxx) 将成为团队的新带领者。恭喜!

感谢大家多年来的支持,
Aaron Griffin (phrakture)

by farseerfc at February 26, 2020 06:51 AM

February 19, 2020

farseerfc

Btrfs vs ZFS 实现 snapshot 的差异

zfs 这个东西倒是名不符实。叫 z storage stack 明显更符合。 叫 fs 但不做 fs 自然确实会和 btrfs 有很大出入。
我反而以前还好奇为什么 btrfs 不弄 zvol , 直到我意识到这东西真是一个 fs ,名符奇实。
—— 某不愿透露姓名的 Ext2FSD 开发者

Btrfs 和 ZFS 都是开源的写时拷贝(Copy on Write, CoW)文件系统,都提供了相似的子卷管理和 快照(snapshot)的功能。网上有不少文章都评价 ZFS 实现 CoW FS 的创新之处,进而想说「 Btrfs 只是 Linux/GPL 阵营对 ZFS 的拙劣抄袭」。或许(在存储领域人尽皆知而在领域外)鲜有人知,在 ZFS 之前就有 NetApp 的商业产品 WAFL (Write Anywhere File Layout) 实现了 CoW 语义的文件系统,并且集成了快照和卷管理之类的功能。描述 btrfs 原型设计的 论文发表幻灯片 也明显提到 WAFL 比提到 ZFS 更多一些,应该说 WAFL 这样的企业级存储方案才是 ZFS 和 btrfs 共同的灵感来源,而无论是 ZFS 还是 btrfs 在其设计中都汲取了很多来自 WAFL 的经验教训。

我一开始也带着「 Btrfs 和 ZFS 都提供了类似的功能,因此两者必然有类似的设计」这样的先入观念,尝试去使用这两个文件系统, 却经常撞上两者细节上的差异,导致使用时需要不尽相同的工作流, 或者看似相似的用法有不太一样的性能表现,又或者一边有的功能,比如 ZFS 的在线去重(in-band dedup) , Btrfs 的 reflink ,在另一边没有的情况,进而需要不同细粒度的子卷划分方案。后来看到了 LWN 的这篇 《A short history of btrfs》 让我意识到 btrfs 和 ZFS 虽然表面功能上看起来类似,但是实现细节上完全不一样, 所以需要不一样的用法,适用于不一样的使用场景。

为了更好地理解这些差异,我四处搜罗这两个文件系统的实现细节,于是有了这篇笔记, 记录一下我查到的种种发现和自己的理解。(或许会写成一个系列?还是先别乱挖坑不填。) 只是自己的笔记,所有参阅的资料文档都是二手资料,没有深挖过源码,还参杂了自己的理解, 于是难免有和事实相违的地方,如有写错,还请留言纠正。

1   Btrfs 的子卷和快照

关于写时拷贝(CoW)文件系统的优势,我们为什么要用 btrfs/zfs 这样的写时拷贝文件系统, 而不是传统的文件系统设计,或者写时拷贝文件系统在使用时有什么区别之类的,网上同样也能找到很多介绍 ,这里不想再讨论。这里假设你用过 btrfs/zfs 至少一个的快照功能,知道它该怎么用, 并且想知道更多细节,判断怎么用那些功能才合理。

先从两个文件系统中(表面上看起来)比较简单的 btrfs 的子卷(subvolume)和快照(snapshot)说起。 关于子卷和快照的常规用法、推荐布局之类的话题就不细说了,网上能找到很多不错的资料,比如 btrfs wiki 的 SysadminGuide 页 和 Arch wiki 上 Btrfs#Subvolumes 页都有不错的参考价值。

1.1   子卷和快照的术语

在 btrfs 中,存在于存储媒介中的只有「子卷」的概念,「快照」只是个创建「子卷」的方式, 换句话说在 btrfs 的术语里,子卷(subvolume)是个名词,而快照(snapshot)是个动词。 如果脱离了 btrfs 术语的上下文,或者不精确地称呼的时候,也经常有文档把 btrfs 的快照命令创建出的子卷叫做一个快照,所以当提到快照的时候,根据上下文判断这里是个动词还是名词, 把名词的快照当作用快照命令创建出的子卷就可以了。或者我们可以理解为, 互相共享一部分元数据(metadata)的子卷互为彼此的快照(名词) , 那么按照这个定义的话,在 btrfs 中创建快照(名词)的方式其实有两种:

  1. btrfs subvolume snapshot 命令创建快照
  2. btrfs send 命令并使用 -p 参数发送快照,并在管道另一端接收
btrfs send 命令的 -p -c

这里也顺便提一下 btrfs send 命令的 -p 参数和 -c 参数的差异。 只看 btrfs-send(8) 的描述的话:

-p <parent>
send an incremental stream from parent to subvol

-c <clone-src>
use this snapshot as a clone source for an incremental send (multiple allowed)

看起来这两个都可以用来生成两个快照之间的差分,只不过 -p 只能指定一个「parent」, 而 -c 能指定多个「clone source」。在 unix stackexchange 上有人写明了这两个的异同 。使用 -p 的时候,产生的差分首先让接收端用 subvolume snapshot 命令对 parent 子卷创建一个快照, 然后发送指令将这个快照修改成目标子卷的样子,而使用 -c 的时候,首先在接收端用 subvolume create 创建一个空的子卷,随后发送指令在这个子卷中填充内容,其数据块尽量共享 clone source 已有的数据。 所以 btrfs send -p 在接收端产生是有共享元数据的快照,而 btrfs send -c 在接收端产生的是仅仅共享数据而不共享元数据的子卷。

定义中「互相共享一部分 元数据 」比较重要,因为除了快照的方式之外, btrfs 的子卷间也可以通过 reflink 的形式共享数据块。我们可以对一整个子卷(甚至目录)执行 cp -r --reflink=always ,创建出一个副本,副本的文件内容通过 reflink 共享原本的数据,但不共享元数据,这样创建出的就不是快照。

说了这么多,其实关键的只是 btrfs 在传统 Unix 文件系统的「目录/文件/inode」 这些东西之外只增加了一个「子卷」的新概念,而子卷间可以共享元数据或者数据, 用快照命令创建出的子卷就是共享一部分元数据。

1.2   于是子卷在存储介质中是如何记录的呢?

首先要说明, btrfs 中大部分长度可变的数据结构都是 CoW B-tree ,一种经过修改适合写时拷贝的B树结构,所以在 on-disk format 中提到了很多个树。这里的树不是指文件系统中目录结构树,而是写时拷贝B树(CoW B-tree,下文简称B树) ,如果不关心B树细节的话可以把 btrfs 所说的一棵树理解为关系数据库中的一个表, 和数据库的表一样 btrfs 的树的长度可变,然后表项内容根据一个 key 排序。

B树结构由索引 key 、中间节点和叶子节点构成。每个 key 是一个 (uint64_t object_id, uint8_t item_type, uint64_t item_extra) 这样的三元组,三元组每一项的具体含义由 item_type 定义。 key 三元组构成了对象的概念,每个对象(object)在树中用一个或多个表项(item)描述,同 object_id 的表项共同描述一个对象。B树中的 key 只用来比较大小而不必连续,从而 object_id 也不必连续,只是按大小排序。有一些预留的 object_id 不能用作别的用途,他们的编号范围是 -255ULL 到 255ULL,也就是表中前 255 和最后 255 个编号预留。

B树中间节点和叶子节点结构大概像是这个样子:

btree_nodes btree_node header TREE_NODE key0: address key10: address key20: address ... free space btree_leaf1 header LEAF_NODE key0: offset, size key1: offset, size key2: offset, size ... keyN offset, size free space dataN ... data2 data1 data0 btree_node:key00->btree_leaf1:label btree_leaf1:e->btree_leaf1:e btree_leaf1:w->btree_leaf1:w btree_leaf1:e->btree_leaf1:e

由此,每个中间节点保存一系列 key 到叶子节点的指针,而叶子节点内保存一系列 item ,每个 item 固定大小,并指向节点内某个可变大小位置的 data 。从而逻辑上一棵B树可以包含任何类型的 item ,每个 item 都可以有可变大小的附加数据。通过这样的B树结构,可以紧凑而灵活地表达很多数据类型。

有这样的背景之后,比如在 SysadminGuide 这页的 Flat 布局 有个子卷布局的例子。

toplevel         (volume root directory, not to be mounted by default)
    +-- root       (subvolume root directory, to be mounted at /)
    +-- home       (subvolume root directory, to be mounted at /home)
    +-- var        (directory)
    |   \-- www    (subvolume root directory, to be mounted at /var/www)
    \-- postgres   (subvolume root directory, to be mounted at /var/lib/postgresql)

用圆柱体表示子卷的话画成图大概是这个样子:

Flat_layout toplevel toplevel root root toplevel->root home home toplevel->home var var toplevel->var postgres postgres toplevel->postgres www www var->www

上图例子中的 Flat 布局在 btrfs 中大概是这样的数据结构, 其中实线箭头是B树一系列中间节点和叶子节点,逻辑上指向一棵B树,虚线箭头是根据 inode 号之类的编号的引用:

Flat_layout_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label roottree:e->roottree:e toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label toplevel:toplevel_dir_root->roottree:root_sub_root toplevel:toplevel_dir_home->roottree:root_sub_home toplevel:toplevel_dir_postgres->roottree:root_sub_postgres toplevel:toplevel_dir_www->roottree:root_sub_www toplevel:e->toplevel:e

上图中已经隐去了很多和本文无关的具体细节,所有这些细节都可以通过 btrfs inspect-internal 的 dump-super 和 dump-tree 查看到。

ROOT_TREE 中记录了到所有别的B树的指针,在一些文档中叫做 tree of tree roots 。「所有别的B树」 举例来说比如 2 号 extent_tree ,3 号 chunk_tree , 4 号 dev_tree ,10 号 free_space_tree ,这些B树都是描述 btrfs 文件系统结构非常重要的组成部分,但是在本文关系不大, 今后有机会再讨论它们。在 ROOT_TREE 的 5 号对象有一个 fs_tree ,它描述了整个 btrfs pool 的顶级子卷,也就是图中叫 toplevel 的那个子卷(有些文档用定冠词称 the FS_TREE 的时候就是在说这个 5 号树,而不是别的子卷的 FS_TREE )。除了顶级子卷之外,别的所有子卷的 object_id 在 256ULL 到 -256ULL 的范围之间,对子卷而言 ROOT_TREE 中的这些 object_id 也同时是它们的 子卷 id ,在内核挂载文件系统的时候可以用 subvolid 找到它们,别的一些对子卷的操作也可以直接用 subvolid 表示一个子卷。 ROOT_TREE 的 6 号对象描述的不是一棵树,而是一个名叫 default 的特殊目录,它指向 btrfs pool 的默认挂载子卷。最初 mkfs 的时候,这个目录指向 ROOT_ITEM 5 ,也就是那个顶级子卷,之后可以通过命令 btrfs subvolume set-default 修改它指向别的子卷,这里它被改为指向 ROOT_ITEM 256 亦即那个名叫 "root" 的子卷。

每一个子卷都有一棵自己的 FS_TREE (有的文档中叫 file tree),一个 FS_TREE 相当于传统 Unix 文件系统中的一整个 inode table ,只不过它除了包含 inode 信息之外还包含所有文件夹内容。在 FS_TREE 中, object_id 同时也是它所描述对象的 inode 号,所以 btrfs 的 子卷有互相独立的 inode 编号 ,不同子卷中的文件或目录可以拥有相同的 inode 。 或许有人不太清楚子卷间 inode 编号独立意味着什么,简单地说,这意味着你不能跨子卷创建 hard link ,不能跨子卷 mv 移动文件而不产生复制操作。不过因为 reflink 和 inode 无关, 可以跨子卷创建 reflink ,也可以用 reflink + rm 的方式快速「移动」文件(这里移动加引号是因为 inode 变了,传统上不算移动)。

FS_TREE 中一个目录用一个 inode_item 和多个 dir_item 描述, inode_item 是目录自己的 inode ,那些 dir_item 是目录的内容。 dir_item 可以指向别的 inode_item 来描述普通文件和子目录, 也可以指向 root_item 来描述这个目录指向一个子卷。有人或许疑惑,子卷就没有自己的 inode 么?其实如果看 数据结构定义 的话 struct btrfs_root_item 结构在最开头的地方包含了一个 struct btrfs_inode_item 所以 root_item 也同时作为子卷的 inode ,不过用户通常看不到这个子卷的 inode ,因为子卷在被(手动或自动地)挂载到目录上之后, 用户会看到的是子卷的根目录的 inode 。

比如上图 FS_TREE toplevel 中,有两个对象,第一个 256 是(子卷的)根目录,第二个 257 是 "var" 目录,256 有4个子目录,其中 "root" "home" "postgres" 这三个指向了 ROOT_TREE 中的对应子卷,而 "var" 指向了 inode 257 。然后 257 有一个子目录叫 "www" 它指向了 ROOT_TREE 中 object_id 为 258 的子卷。

1.3   那么快照又是如何记录的呢?

以上是子卷、目录、 inode 在 btrfs 中的记录方式,你可能想知道,如何记录一个快照呢? 特别是,如果对一个包含子卷的子卷创建了快照,会得到什么结果呢?如果我们在上面的布局基础上执行:

btrfs subvolume snapshot toplevel toplevel/toplevel@s1

那么产生的数据结构大概如下所示:

Flat_layout_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" 260: fs_tree "toplevel@s1" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label roottree:e->roottree:e toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 256: dir_item: "toplevel@s1" -> ROOT_ITEM 260 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label toplevels1 FS_TREE "toplevel@s1" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_sub_s1->toplevels1:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label toplevel:toplevel_dir_root->roottree:root_sub_root toplevel:toplevel_dir_home->roottree:root_sub_home toplevel:toplevel_dir_postgres->roottree:root_sub_postgres toplevel:toplevel_dir_toplevels1->roottree:root_sub_s1 toplevel:toplevel_dir_www->roottree:root_sub_www toplevel:e->toplevel:e

在 ROOT_TREE 中增加了 260 号子卷,其内容复制自 toplevel 子卷,然后 FS_TREE toplevel 的 256 号 inode 也就是根目录中增加一个 dir_item 名叫 toplevel@s1 它指向 ROOT_ITEM 的 260 号子卷。这里看似是完整复制了整个 FS_TREE 的内容,这是因为 CoW b-tree 当只有一个叶子节点时就复制整个叶子节点。如果子卷内容再多一些,除了叶子之外还有中间节点, 那么只有被修改的叶子和其上的中间节点需要复制。从而创建快照的开销基本上是 O( level of FS_TREE ),而B树的高度一般都能维持在很低的程度,所以快照创建速度近乎是常数开销。

从子卷和快照的这种实现方式,可以看出: 虽然子卷可以嵌套子卷,但是对含有嵌套子卷的子卷做快照的语义有些特别 。上图中我没有画 toplevel@s1 下的各个子卷到对应 ROOT_ITEM 之间的虚线箭头, 是因为这时候如果你尝试直接跳过 toplevel 挂载 toplevel@s1 到挂载点, 会发现那些子卷没有被自动挂载,更奇怪的是那些子卷的目录项也不是个普通目录, 尝试往它们中放东西会得到无权访问的错误,对它们能做的唯一事情是手动将别的子卷挂载在上面。 推测原因在于这些子目录并不是真的目录,没有对应的目录的 inode ,试图查看它们的 inode 号会得到 2 号,而这是个保留号不应该出现在 btrfs 的 inode 号中。 每个子卷创建时会记录包含它的上级子卷,用 btrfs subvolume list 可以看到每个子卷的 top level subvolid ,猜测当挂载 A 而 A 中嵌套的 B 子卷记录的上级子卷不是 A 的时候, 会出现上述奇怪行为。嵌套子卷的快照还有一些别的奇怪行为,大家可以自己探索探索。

建议用平坦的子卷布局

因为上述嵌套子卷在做快照时的特殊行为, 我个人建议是 保持平坦的子卷布局 ,也就是说:

  1. 只让顶层子卷包含其它子卷,除了顶层子卷之外的子卷只做手工挂载,不放嵌套子卷
  2. 只在顶层子卷对其它子卷做快照,不快照顶层子卷
  3. 虽然可以在顶层子卷放子卷之外的东西(文件或目录),不过因为想避免对顶层子卷做快照, 所以避免在顶层子卷放普通文件。

btrfs 的子卷可以设置「可写」或者「只读」,在创建一个快照的时候也可以通过 -r 参数创建出一个只读快照。通常只读快照可能比可写的快照更有用,因为 btrfs send 命令只接受只读快照作为参考点。子卷可以有两种方式切换它是否只读的属性,可以通过 btrfs property set <subvol> ro 直接修改是否只读,也可以对只读子卷用 btrfs subvolume snapshot 创建出可写子卷,或者反过来对可写子卷创建出只读子卷。

只读快照也有些特殊的限制,在 SysadminGuide#Special_Cases 就提到一例,你不能把只读快照用 mv 移出包含它的目录,虽然你能用 mv 给它改名或者移动包含它的目录 到别的地方。 btrfs wiki 上给出这个限制的原因是子卷中记录了它的上级, 所以要移动它到别的上级需要修改这个子卷,从而只读子卷没法移动到别的上级( 不过我还没搞清楚子卷在哪儿记录了它的上级,记录的是上级目录还是上级子卷)。不过这个限制可以通过 对只读快照在目标位置创建一个新的只读快照,然后删掉原位置的只读快照来解决。

2   ZFS 的文件系统、快照、克隆及其它

Btrfs 给传统文件系统只增加了子卷的概念,相比之下 ZFS 中类似子卷的概念有好几个,据我所知有这些:

  • 数据集(dataset)
  • 文件系统(filesystem)
  • 快照(snapshot)
  • 克隆(clone)
  • 书签(bookmark):从 ZFS on Linux v0.6.4 开始
  • 检查点(checkpoint):从 ZFS on Linux v0.8.0 开始

梳理一下这些概念之间的关系也是最初想写下这篇笔记的初衷。先画个简图,随后逐一讲讲这些概念:

ditaa diagram

上图中,假设我们有一个 pool ,其中有 3 个文件系统叫 fs1~fs3 和一个 zvol 叫 zv1 ,然后文件系统 fs1 有两个快照 s1 和 s2 ,和两个书签 b1 和 b2。pool 整体有两个检查点 cp1 和 cp2 。这个简图将作为例子在后面介绍这些概念。

2.1   ZFS 设计中和快照相关的一些术语和概念

数据集

ZFS 中把文件系统、快照、克隆、zvol 等概念统称为数据集(dataset)。 一些文档和介绍中把文件系统叫做数据集,大概因为在 ZFS 中,文件系统是最先创建并且最有用的数据集。

在 ZFS 的术语中,把底层管理和释放存储设备空间的叫做 ZFS 存储池(pool), 简称 zpool ,其上可以容纳多个数据集,这些数据集用类似文件夹路径的语法 pool_name/​dataset_path@snapshot_name 这样来称呼。 存储池中的数据集一同共享可用的存储空间,每个数据集单独跟踪自己所消耗掉的存储空间。

数据集之间有类似文件夹的层级父子关系,这一点有用的地方在于可以在父级数据集上设定一些 ZFS 参数, 这些参数可以被子级数据集继承,从而通过层级关系可以方便地微调 ZFS 参数。在 btrfs 中目前还没有类似的属性继承的功能。

zvol 的概念和本文关系不大,可以参考我上一篇 ZFS 子系统笔记中 ZVOL 的说明 。用 zvol 能把 ZFS 当作一个传统的卷管理器,绕开 ZFS 的 ZPL(ZFS Posix filesystem Layer) 层。在 Btrfs 中可以用 loopback 块设备某种程度上模拟 zvol 的功能。

文件系统

创建了 ZFS 存储池后,首先要在其中创建文件系统(filesystem),才能在文件系统中存储文件。 容易看出 ZFS 文件系统的概念直接对应 btrfs 中的子卷。文件系统(filesystem)这个术语, 从命名方式来看或许是想要和(像 Solaris 的 SVM 或者 Linux 的 LVM 这样的)传统的卷管理器 与其上创建的多个文件系统(Solaris UFS 或者 Linux ext)这样的上下层级做类比。 从 btrfs 的子卷在内部结构中叫作 FS_TREE 这一点可以看出,至少在 btrfs 早期设计中大概也是把子卷称为 filesystem 做过类似的类比的。 和传统的卷管理器与传统文件系统的上下层级不同的是, ZFS 和 btrfs 中由存储池跟踪和管理可用空间, 做统一的数据块分配和释放,没有分配的数据块算作整个存储池中所有 ZFS 文件系统或者 btrfs 子卷的可用空间。

与 btrfs 的子卷不同的是, ZFS 的文件系统之间是完全隔离的,(除了后文会讲的 dedup 方式之外)不可以共享任何数据或者元数据。一个文件系统还包含了隶属于其中的快照(snapshot)、 克隆(clone)和书签(bookmark)。在 btrfs 中一个子卷和对其创建的快照之间虽然有父子关系, 但是在 ROOT_TREE 的记录中属于平级的关系。

上面简图中 pool 里面包含 3 个文件系统,分别是 fs1~3 。

快照

ZFS 的快照对应 btrfs 的只读快照,是标记数据集在某一历史时刻上的只读状态。 和 btrfs 的只读快照一样, ZFS 的快照也兼作 send/receive 时的参考点。 快照隶属于一个数据集,这说明 ZFS 的文件系统或者 zvol 都可以创建快照。

ZFS 中快照是排列在一个时间线上的,因为都是只读快照,它们是数据集在历史上的不同时间点。 这里说的时间不是系统时钟的时间,而是 ZFS 中事务组(TXG, transaction group)的一个序号。 整个 ZFS pool 的每次写入会被合并到一个事务组,对事务组分配一个严格递增的序列号, 提交一个事务组具有类似数据库中事务的语义:要么整个事务组都被完整提交,要么整个 pool 处于上一个事务组的状态,即使中间发生突然断电之类的意外也不会破坏事务语义。 因此 ZFS 快照就是数据集处于某一个事务组时的状态。

如果不满于对数据集进行的修改,想把整个数据集恢复到之前的状态,那么可以回滚(rollback )数据集到一个快照。回滚操作会撤销掉对数据集的所有更改,并且默认参数下只能回滚到最近的一个快照。 如果想回滚到更早的快照,可以先删掉最近的几个,或者可以使用 zfs rollback -r 参数删除中间的快照并回滚。

除了回滚操作,还可以直接只读访问到快照中的文件。 ZFS 的文件系统中有个隐藏文件夹叫 ".zfs" ,所以如果只想回滚一部分文件,可以从 ".zfs/snapshots/SNAPSHOT-NAME" 中把需要的文件复制出来。

比如上面简图中 fs1 就有 pool/​fs1@s1 pool/​fs1@s2 这两个快照, 那么可以在 fs1 挂载点下 .zfs/​snapshots/​s1 的路径直接访问到 s1 中的内容。

克隆

ZFS 的克隆(clone)有点像 btrfs 的可写快照。因为 ZFS 的快照是只读的,如果想对快照做写入,那需要先用 zfs clone 从快照中建出一个克隆,创建出的克隆和快照共享元数据和数据, 然后对克隆的写入不影响数据集原本的写入点。 创建了克隆之后,作为克隆参考点的快照会成为克隆的依赖,克隆存在期间无法删除掉作为其依赖的快照。

一个数据集可以有多个克隆,这些克隆都独立于数据集当前的写入点。使用 zfs promote 命令可以把一个克隆「升级」成为数据集的当前写入点,从而数据集原本的写入点会调转依赖关系, 成为这个新写入点的一个克隆,被升级的克隆原本依赖的快照和之前的快照会成为新数据集写入点的快照。

比如上面简图中 fs1 有 c1 的克隆,它依赖于 s2 这个快照,从而 c1 存在的时候就不能删除掉 s2 。

书签

这是 ZFS 一个比较新的特性,ZFS on Linux 分支从 v0.6.4 开始支持创建书签的功能。

书签(bookmark)特性存在的理由是基于这样的事实:原本 ZFS 在 send 两个快照间的差异的时候,比如 send S1 和 S2 之间的差异,在发送端实际上只需要 S1 中记录的时间戳(TXG id),而不需要 S1 快照的数据, 就可以计算出 S1 到 S2 的差异。在接收端则需要 S1 的完整数据,在其上根据接收到的数据流创建 S2 。 因此在发送端,可以把快照 S1 转变成书签,只留下时间戳元数据而不保留任何目录结构或者文件内容。 书签只能作为增量 send 时的参考点,并且在接收端需要有对应的快照,这种方式可以在发送端节省很多存储。

通常的使用场景是,比如你有一个笔记本电脑,上面有 ZFS 存储的数据,然后使用一个服务器上 ZFS 作为接收端,定期对笔记本上的 ZFS 做快照然后 send 给服务器。在没有书签功能的时候, 笔记本上至少得保留一个和服务器上相同的快照,作为 send 的增量参考点, 而这个快照的内容已经在服务器上,所以笔记本中存有相同的快照只是在浪费存储空间。 有了书签功能之后,每次将定期的新快照发送到服务器之后,就可以把这个快照转化成书签,节省存储开销。

检查点

这也是 ZFS 的新特性, ZFS on Linux 分支从 v0.8.0 开始支持创建检查点。

简而言之,检查点(checkpoint)可以看作是整个存储池级别的快照,使用检查点能快速将整个存储池都恢复到上一个状态。 这边有篇文章介绍 ZFS checkpoint 功能的背景、用法和限制 ,可以看出当存储池中有检查点的时候很多存储池的功能会受影响(比如不能删除 vdev 、不能处于 degraded 状态、不能 scrub 到当前存储池中已经释放而在检查点还在引用的数据块), 于是检查点功能设计上更多是给系统管理员准备的用于调整整个 ZFS pool 时的后悔药, 调整结束后日用状态下应该删除掉所有检查点。

2.2   ZFS 的概念与 btrfs 概念的对比

先说书签和检查点,因为这是两个 btrfs 目前完全没有的功能。

书签功能完全围绕 ZFS send 的工作原理,而 ZFS send 位于 ZFS 设计中的 DSL 层面,甚至不关心它 send 的快照的数据是来自文件系统还是 zvol 。在发送端它只是从目标快照递归取数据块,判断 TXG 是否老于参照点的快照,然后把新的数据块全部发往 send stream ;在接收端也只是完整地接收数据块, 不加以处理,。与之不同的是 btrfs 的 send 的工作原理是工作在文件系统的只读子卷层面, 发送端在内核代码中根据目标快照的 b 树和参照点快照的 generation 生成一个 diff (可以通过 btrfs subvolume find-new 直接拿到这个 diff ),然后在用户态代码中根据 diff 和参照点、目标快照的两个只读子卷的数据产生一连串修改文件系统的指令, 指令包括创建文件、删除文件、让文件引用数据块(保持 reflink )等操作;在接收端则完全工作在用户态下, 根据接收到的指令重建目标快照。可见 btrfs send 需要在发送端读取参照点快照的数据(比如找到 reflink 引用),从而 btrfs 没法(或者很难)实现书签功能。

检查点也是 btrfs 目前没有的功能。 btrfs 目前不能对顶层子卷做递归的 snapshot ,btrfs 的子卷也没有类似 ZFS 数据集的层级关系和可继承属性,从而没法实现类似检查点的功能。

除了书签和检查点之外,剩下的概念可以在 ZFS 和 btrfs 之间有如下映射关系:

ZFS 文件系统:btrfs 子卷
ZFS 快照:btrfs 只读快照
ZFS 克隆:btrfs 可写快照

对 ZFS 数据集的操作,大部分也可以找到对应的对 btrfs 子卷的操作。

zfs list: btrfs subvolume list
zfs create: btrfs subvolume create
zfs destroy: btrfs subvolume delete
zfs rename: mv
zfs snapshot: btrfs subvolume snapshot -r
zfs rollback:这个在 btrfs 需要对只读快照创建出可写的快照(用 snapshot 命令,或者直接修改读写属性),然后改名或者调整挂载点
zfs diff: btrfs subvolume find-new
zfs clone: btrfs subvolume snapshot
zfs promote:和 rollback 类似,可以直接调整 btrfs 子卷的挂载点

可见虽然功能上类似,但是至少从管理员管理的角度而言, zfs 对文件系统、快照、克隆的划分更为清晰, 对他们能做的操作也更为明确。这也是很多从 ZFS 迁移到 btrfs ,或者反过来从 btrfs 换用 zfs 时,一些人困惑的起源(甚至有人据此说 ZFS 比 btrfs 好在 cli 设计上)。

不过 btrfs 子卷的设计也使它在系统管理上有了更大的灵活性。比如在 btrfs 中删除一个子卷不会受制于别的子卷是否存在,而在 zfs 中要删除一个快照必须先保证先摧毁掉依赖它的克隆。 再比如 btrfs 的可写子卷没有主次之分,而 zfs 中一个文件系统和其克隆之间有明显的区别,所以需要 promote 命令调整差异。还有比如 ZFS 的文件系统只能回滚到最近一次的快照, 要回滚到更久之前的快照需要删掉中间的快照,并且回滚之后原本的文件系统数据和快照数据就被丢弃了; 而 btrfs 中因为回滚操作相当于调整子卷的挂载,所以不需要删掉快照, 并且回滚之后原本的子卷和快照还可以继续保留。

加上 btrfs 有 reflink ,这给了 btrfs 在使用中更大的灵活性,可以有一些 zfs 很难做到的用法。 比如想从快照中打捞出一些虚拟机镜像的历史副本,而不想回滚整个快照的时候,在 btrfs 中可以直接 cp --reflink=always 将镜像从快照中复制出来,此时的复制将和快照共享数据块; 而在 zfs 中只能用普通 cp 复制,会浪费很多存储空间。

2.3   ZFS 中是如何存储这些数据集的呢

要讲到存储细节,首先需要 了解一下 ZFS 的分层设计 。不像 btrfs 基于现代 Linux 内核,有许多现有文件系统已经实现好的基础设施可以利用, 并且大体上只用到一种核心数据结构(CoW的B树); ZFS 则脱胎于 Solaris 的野心勃勃, 设计时就分成很多不同的子系统,逐步提升抽象层次, 并且每个子系统都发明了许多特定需求下的数据结构来描述存储的信息。 在这里和本文内容密切相关的是 ZPLDSLDMU 这些 ZFS 子系统。

Sun 曾经写过一篇 ZFS 的 On disk format 对理解 ZFS 如何存储在磁盘上很有帮助,虽然这篇文档是针对 Sun 还在的时候 Solaris 的 ZFS ,现在 ZFS 的内部已经变化挺大,不过对于理解本文想讲的快照的实现方式还具有参考意义。这里借助这篇 ZFS On Disk Format 中的一些图示来解释 ZFS 在磁盘上的存储方式。

ZFS 的块指针

zfs-block-pointer.svg

要理解 ZFS 的磁盘结构首先想介绍一下 ZFS 中的块指针(block pointer, blkptr_t ),结构如右图所示。 ZFS 的块指针用在 ZFS 的许多数据结构之中,当需要从一个地方指向任意另一个地址的时候都会 插入这样的一个块指针结构。大多数文件系统中也有类似的指针结构,比如 btrfs 中有个8字节大小的逻辑地址(logical address),一般也就是个 4字节 到 16字节 大小的整数写着扇区号、块号或者字节偏移,在 ZFS 中的块指针则是一个巨大的128字节(不是 128bit !)的结构体。

128字节块指针的开头是3个数据虚拟地址(DVA, Data Virtual Address),每个 DVA 是 128bit ,其中记录这块数据在什么设备(vdev)的什么偏移(offset)上占用多大(asize),有 3个 DVA 槽是用来存储最多3个不同位置的副本。然后块指针还记录了这个块用什么校验算法( cksum )和什么压缩算法(comp),压缩前后的大小(PSIZE/LSIZE),以及256bit的校验和(checksum)。

当需要间接块(indirect block)时,块指针中记录了间接块的层数(lvl),和下层块指针的数量(fill)。 一个间接块就是一个数据块中包含一个块指针的数组,当引用的对象很大需要很多块时,间接块构成一棵树状结构。

块指针中还有和本文关系很大的一个值 birth txg ,记录这个块指针诞生时的整个 pool 的 TXG id 。一次 TXG 提交中写入的数据块都会有相同的 birth txg ,这个相当于 btrfs 中 generation 的概念。 实际上现在的 ZFS 块指针似乎记录了两个 birth txg ,分别在图中的9行和a行的位置, 一个 physical 一个 logical ,用于 dedup 和 device removal 。值得注意的是块指针里只有 birth txg ,没有引用计数或者别的机制做引用,这对后面要讲的东西很关键。

DSL 的元对象集

理解块指针和 ZFS 的子系统层级之后,就可以来看看 ZFS 存储在磁盘上的具体结构了。 因为涉及的数据结构种类比较多,所以先来画一张逻辑上的简图,其中箭头只是某种引用关系不代表块指针, 方框也不是结构体细节:

zfs_layout_simple uberblock UBERBLOCK ... mos_blkptr mos Meta Object Set root dataset config ... uberblock:ub_rootbp->mos:mos_label root_dataset ROOT dataset dataset1 directory dataset2 directory ... mos:mos_root_dataset->root_dataset:rd_label ds1_directory DSL Directory ds1 property ZAP object ds1 child ZAP object ds1 dataset (active) ds1 snapshot1 ds1 snapshot2 ... root_dataset:rd_ds1->ds1_directory:ds1_label ds1_dataset ds1 DMU Object Set ... ds1_directory:ds1_dataset->ds1_dataset:ds1_ds_label ds1_snapshot1 ds1 snapshot1 DMU Object Set ... ds1_directory:ds1_s1->ds1_snapshot1:ds1_s1_label

如上简图所示,首先 ZFS pool 级别有个 uberblock ,具体每个 vdev 如何存储和找到这个 uberblock 今后有空再聊,这里认为整个 zpool 有唯一的一个 uberblock 。从 uberblock 有个指针指向元对象集(MOS, Meta Object Set),它是个 DMU 的对象集,它包含整个 pool 的一些配置信息,和根数据集(root dataset)。根数据集再包含整个 pool 中保存的所有顶层数据集,每个数据集有一个 DSL Directory 结构。然后从每个数据集的 DSL Directory 可以找到一系列子数据集和一系列快照等结构。最后每个数据集有个 active 的 DMU 对象集,这是整个文件系统的当前写入点,每个快照也指向一个各自的 DMU 对象集。

DSL 层的每个数据集的逻辑结构也可以用下面的图表达(来自 ZFS On Disk Format ):

zfs-dsl-infrastructure.svg

ZFS On Disk Format 中 4.1 节的 DSL infrastructure

需要记得 ZFS 中没有类似 btrfs 的 CoW b-tree 这样的统一数据结构,所以上面的这些设施是用各种不同的数据结构表达的。 尤其每个 Directory 的结构可以包含一个 ZAP 的键值对存储,和一个 DMU 对象。 可以理解为, DSL 用 DMU 对象集(Objectset)表示一个整数(uinit64_t 的 dnode 编号)到 DMU 对象的映射,然后用 ZAP 对象表示一个名字到整数的映射,然后又有很多额外的存储于 DMU 对象中的 DSL 结构体。如果我们画出不同的指针和不同的结构体,那么会得到一个稍显复杂的图,见右边「ZFS On Disk Format 中 4.2 节的 Meta Object Set」,图中还只画到了 root_dataset 为止。

看到这里,大概可以理解在 ZFS 中创建一个 ZFS 快照的操作其实很简单:找到数据集的 DSL Directory 中当前 active 的 DMU 对象集指针,创建一个表示 snapshot 的 DSL dataset 结构,指向那个 DMU 对象集,然后快照就建好了。因为今后对 active 的写入会写时复制对应的 DMU 对象集,所以 snapshot 指向的 DMU 对象集不会变化。

3   创建快照这么简单么?那么删除快照呢?

按上面的存储格式细节来看, btrfs 和 zfs 中创建快照似乎都挺简单的,利用写时拷贝,创建快照本身没什么复杂操作。

如果你也听到过别人介绍 CoW 文件系统时这么讲,是不是会觉得似乎哪儿少了点什么。创建快照是挺简单的, 直到你开始考虑如何删除快照 ……

或者不局限在删除单个快照上, CoW 文件系统因为写时拷贝,每修改一个文件内容或者修改一个文件系统结构, 都是分配新数据块,然后考虑是否要删除这个数据替换的老数据块,此时如何决定老数据块能不能删呢? 删除快照的时候也是同样,快照是和别的文件系统有共享一部分数据和元数据的, 所以显然不能把快照引用到的数据块都直接删掉,要考察快照引用的数据块是否还在别的地方被引用着, 只能删除那些没有被引用的数据。

深究「如何删快照」这个问题,就能看出 WAFL 、 btrfs 、 ZFS 甚至别的 log-structured 文件系统间的关键区别,从而也能看到另一个问题的答案: 为什么 btrfs 只需要子卷的抽象,而 zfs 搞出了这么多抽象概念? 带着这两个疑问,我们来研究一下这些文件系统的块删除算法。

3.1   日志结构文件系统中用的垃圾回收算法

讲 btrfs 和 zfs 用到的删除算法之前,先讲一下日志结构(log-structured)文件系统中的垃圾回收( GC, Garbage Collection)算法。对熟悉编程的人来说,讲到空间释放算法,大概首先会想到 GC ,因为这里要解决的问题乍看起来很像编程语言的内存管理中 GC 想要解决的问题:有很多指针相互指向很多数据结构,找其中没有被引用的垃圾然后释放掉。

首先要澄清一下 日志结构文件系统(log-structured file system) 的定义,因为有很多文件系统用日志,而用了日志的不一定是日志结构文件系统。 在维基百科上有个页面介绍 日志结构文件系统 ,还有个 列表列出了一些日志结构文件系统 。通常说,整个文件系统的存储结构都组织成一个大日志的样子,就说这个文件系统是日志结构的, 这包括很多早期学术研究的文件系统,以及目前 NetBSD 的 LFS 、Linux 的 NILFS ,用在光盘介质上的 UDF ,还有一些专门为闪存优化的 JFFSYAFFS 以及 F2FS 。日志结构文件系统不包括那些用额外日志保证文件系统一致性,但文件系统结构不在日志中的 ext4 、 xfs 、 ntfs 、 hfs+ 。

简单来说,日志结构文件系统就是把存储设备当作一个大日志,每次写入数据时都添加在日志末尾, 然后用写时复制重新写入元数据,最后提交整个文件系统结构。因为这里用了写时复制,原本的数据块都还留着, 所以可以很容易实现快照之类的功能。从这个特征上来说,写时拷贝文件系统(CoW FS)像 btrfs/zfs 这些在一些人眼中也符合日志结构文件系统的特征, 所以也有人说写时拷贝文件系统算是日志结构文件系统的一个子类。不过日志结构文件系统的另一大特征是利用 GC 回收空间,这里是本文要讲的区别,所以在我看来不用 GC 的 btrfs 和 zfs 不算是日志结构文件系统。

举个例子,比如下图是一个日志结构文件系统的磁盘占用,其中绿色是数据,蓝色是元数据(比如目录结构和 inode),红色是文件系统级关键数据(比如最后的日志提交点),一开始可能是这样,有9个数据块, 2个元数据块,1个系统块:

ditaa diagram

现在要覆盖 2 和 3 的内容,新写入 n2 和 n3 ,再删除 4 号的内容 ,然后修改 10 里面的 inode 变成 n10 引用这些新数据,然后写入一个新提交 n12 ,用黄色表示不再被引用的垃圾,提交完大概是这样:

ditaa diagram

日志结构文件系统需要 GC 比较容易理解,写日志嘛,总得有一个「添加到末尾」的写入点,比如上面图中的 n12 就是当前的写入点。空盘上连续往后写而不 GC 总会遇到空间末尾,这时候就要覆盖写空间开头, 就很难判断「末尾」在什么地方,而下一次写入需要在哪里了。 这时文件系统也不知道需要回收哪些块(图中的 o2 o3 o4 o10 和 o12),因为这些块可能被别的地方还继续 引用着,需要等到 GC 时扫描元数据来判断。

和内存管理时的 GC 不同的一点在于,文件系统的 GC 肯定不能停下整个世界跑 GC ,也不能把整个地址空间对半分然后 Mark-and-Sweep ,这些在内存中还尚可的简单策略直接放到文件系统中绝对是性能灾难。所以文件系统的 GC 需要并行的后台 GC ,并且需要更细粒度的分块机制能在 Mark-and-Sweep 的时候保持别的地方可以继续写入数据而维持文件系统的正常职能。

通常文件系统的 GC 是这样,先把整个盘分成几个段(segment)或者区域(zone),术语不同不过表达的概念类似, 然后 GC 时挑一个老段,扫描文件系统元数据找出要释放的段中还被引用的数据块,搬运到日志末尾,最后整个释放一段。 搬运数据块时,也要调整文件系统别的地方对被搬运的数据块的引用。

物理磁盘上一般有扇区的概念,通常是 512B 或者 4KiB 的大小,在文件系统中一般把连续几个物理块作为一个数据块, 大概是 4KiB 到 1MiB 的数量级,然后日志结构文件系统中一个段(segment)通常是连续的很多块,数量级来看大约是 4MiB 到 64MiB 这样的数量级。相比之下 ufs/ext4/btrfs/zfs 的分配器通常还有 block group 的概念, 大概是 128MiB 到 1GiB 的大小。可见日志结构文件系统的段,是位于数据块和其它文件系统 block group 中间的一个单位。段大小太小的话,会显著增加空间管理需要的额外时间空间开销,而段大小太大的话, 又不利于利用整个可用空间,这里的抉择有个平衡点。

继续上面的例子,假设上面文件系统的图示中每一列的4块是一个段,想要回收最开头那个段, 那么需要搬运还在用的 1 到空闲空间,顺带修改引用它的 n10 ,最后提交 n12 :

ditaa diagram

要扫描并释放一整段,需要扫描整个文件系统中别的元数据(图中的 n12 和 n10 和 11)来确定有没有引用到目标段中的地址,可见释放一个段是一个 \(O(N)\) 的操作,其中 N 是元数据段的数量,按文件系统的大小增长, 于是删除快照之类可能要连续释放很多段的操作在日志文件系统中是个 \(O(N^2)\) 甚至更昂贵的操作。 在文件系统相对比较小而系统内存相对比较大的时候,比如手机上或者PC读写SD卡,大部分元数据块( 其中包含块指针)都能放入内存缓存起来的话,这个扫描操作的开销还是可以接受的。 但是对大型存储系统显然扫描并释放空间就不合适了。

段的抽象用在闪存类存储设备上的一点优势在于,闪存通常也有擦除块的概念,比写入块的大小要大, 是连续的多个写入块构成,从而日志结构的文件系统中一个段可以直接对应到闪存的一个擦除块上。 所以闪存设备诸如U盘或者 SSD 通常在底层固件中用日志结构文件系统模拟一个块设备,来做写入平衡。 大家所说的 SSD 上固件做的 GC ,大概也就是这样一种操作。

基于段的 GC 还有一个显著缺陷,需要扫描元数据,复制搬运仍然被引用到的块,这不光会增加设备写入, 还需要调整现有数据结构中的指针,调整指针需要更多写入,同时又释放更多数据块, F2FS 等一些文件系统设计中把这个问题叫 Wandering Tree Problem ,在 F2FS 设计中是通过近乎「作弊」的 NAT 转换表 放在存储设备期待的 FAT 所在位置,不仅能让需要扫描的元数据更集中,还能减少这种指针调整导致的写入。

不过基于段的 GC 也有一些好处,它不需要复杂的文件系统设计,不需要特殊构造的指针, 就能很方便地支持大量快照。一些日志结构文件系统比如 NILFS 用这一点支持了「连续快照(continuous snapshots)」,每次文件系统提交都是自动创建一个快照,用户可以手动标记需要保留哪些快照, GC 算法则排除掉用户手动标记的快照之后,根据快照创建的时间,先从最老的未标记快照开始回收。 即便如此, GC 的开销(CPU时间和磁盘读写带宽)仍然是 NILFS 最为被人诟病的地方,是它难以被广泛采用的原因。 为了加快 NILFS 这类日志文件系统的 GC 性能让他们能更适合于普通使用场景,也有许多学术研究致力于探索和优化 GC ,使用更先进的数据结构和算法跟踪数据块来调整 GC 策略,比如这里有一篇 State-of-the-art Garbage Collection Policies for NILFS2

3.2   WAFL 早期使用的可用空间位图数组

从日志结构文件系统使用 GC 的困境中可以看出,文件系统级别实际更合适的, 可能不是在运行期依赖扫描元数据来计算空间利用率的 GC ,而是在创建快照时或者写入数据时就预先记录下快照的空间利用情况, 从而可以细粒度地跟踪空间和回收空间,这也是 WAFL 早期实现快照的设计思路。

WAFL 早期记录快照占用数据块的思路从表面上来看也很「暴力」,传统文件系统一般有个叫做「位图(bitmap )」的数据结构,用一个二进制位记录一个数据块是否占用,靠扫描位图来寻找可用空间和已用空间。 WAFL 的设计早期中考虑既然需要支持快照,那就把记录数据块占用情况的位图,变成快照的数组。 于是整个文件系统有个 256 大小的快照利用率数组,数组中每个快照记录自己占用的数据块位图, 文件系统中最多能容纳 255 个快照。

ditaa diagram

上面每个单元格都是一个二进制位,表示某个快照有没有引用某个数据块。有这样一个位图的数组之后, 就可以直接扫描位图判断出某个数据块是否已经占用,可以找出尚未被占用的数据块用作空间分配, 也可以方便地计算每个快照引用的空间大小或者独占的空间大小,估算删除快照后可以释放的空间。

需要注意的是,文件系统中可以有非常多的块,从而位图数组比位图需要更多的元数据来表达。 比如估算一下传统文件系统中一块可以是 4KiB 大小,那么跟踪空间利用的位图需要 1bit/4KiB , 1TiB 的盘就需要 32MiB 的元数据来存放位图; 而 WAFL 这种位图数组即便限制了快照数量只能有255个,仍需要 256bit/4KiB 的空间开销, 1TiB 的盘需要的元数据开销陡增到 8GiB ,这些还只是单纯记录空间利用率的位图数组,不包括别的元数据。

使用这么多元数据表示快照之后,创建快照的开销也相应地增加了,需要复制整个位图来创建一个新的快照, 按上面的估算 1TiB 的盘可能需要复制 32MiB 的位图,这不再是一瞬能完成的事情, 期间可能需要停下所有对文件系统的写入等待复制完成。 位图数组在存储设备上的记录方式也很有讲究,当删除快照时希望能快速读写上图中的一整行位图, 于是可能希望每一行位图的存储方式在磁盘上都尽量连续, 而在普通的写入操作需要分配新块时,想要按列的方式扫描位图数组,找到没有被快照占用的块, 从而上图中按列的存储表达也希望在磁盘上尽量连续。 WAFL 的设计工程师们在位图数组的思路下,实现了高效的数据结构让上述两种维度的操作都能快速完成, 但是这绝不是一件容易的事情。

位图数组的表达方式也有其好处,比如除了快照之外,也可以非常容易地表达类似 ZFS 的克隆和独立的文件系统这样的概念,这些东西和快照一样,占用仅有的 256 个快照数量限制。 这样表达的克隆可以有数据块和别的文件系统共享,文件系统之间也可以有类似 reflink 的机制共享数据块,在位图数组的相应位置将位置1即可。

使用位图数组的做法,也只是 WAFL 早期可能采用的方式,由于 WAFL 本身是闭源产品, 难以获知它具体的工作原理。哈佛大学和 NetApp 的职员曾经在 FAST10 (USENIX Conference on File and Storage Technologies) 上发表过一篇讲解高效跟踪和使用 back reference 的论文,叫 Tracking Back References in a Write-Anywhere File System ,可以推测在新一代 WAFL 的设计中可能使用了类似 btrfs backref 的实现方式,接下来会详细介绍。

3.3   ZFS 中关于快照和克隆的空间跟踪算法

How ZFS snapshots really work And why they perform well (usually)

OpenZFS 的项目领导者,同时也是最初设计 ZFS 中 DMU 子系统的作者 Matt Ahrens 在 DMU 和 DSL 中设计并实现了 ZFS 独特的快照的空间跟踪算法。他也在很多地方发表演讲,讲过这个算法的思路和细节, 比如右侧就是他在 BSDCan 2019 做的演讲 How ZFS snapshots really work And why they perform well (usually) 的 YouTube 视频。

其中 Matt 讲到了三个删除快照的算法,分别可以叫做「🐢乌龟算法」、「🐰兔子算法」、「🐆豹子算法」, 接下来简单讲讲这些算法背后的思想和实现方式。

🐢乌龟算法:概念上 ZFS 如何删快照

乌龟算法没有实现在 ZFS 中,不过方便理解 ZFS 在概念上如何考虑快照删除这个问题,从而帮助理解 后面的🐰兔子算法和🐆豹子算法。

要删除一个快照, ZFS 需要找出这个快照引用到的「独占」数据块,也就是那些不和别的数据集或者快照共享的 数据块。 ZFS 删除快照基于这几点条件:

  1. ZFS 快照是只读的。创建快照之后无法修改其内容。
  2. ZFS 的快照是严格按时间顺序排列的,这里的时间指 TXG id ,即记录文件系统提交所属事务组的严格递增序号。
  3. ZFS 不存在 reflink 之类的机制,从而在某个时间点删除掉的数据块,不可能在比它更后面的快照中「复活」。

第三点关于 reflink 造成的数据复活现象可能需要解释一下,比如在(支持 reflink 的) btrfs 中有如下操作:

btrfs subvolume snapshot -r fs s1
rm fs/somefile
btrfs subvolume snapshot -r fs s2
cp --reflink=always s1/somefile fs/somefile
btrfs subvolume snapshot -r fs s3

我们对 fs 创建了 s1 快照,删除了 fs 中某个文件,创建了 s2 快照,然后用 reflink 把刚刚删除的文件从 s1 中复制出来,再创建 s3 。如此操作之后,按时间顺序有 s1、s2、s3 三个快照:

ditaa diagram

其中只有 s2 不存在 somefile ,而 s1 、 s3 和当前的 fs 都有,并且都引用到了同一个数据块。 于是从时间线来看, somefile 的数据块在 s2 中「死掉」了,又在 s3 中「复活」了。

而 ZFS (目前还)不支持 reflink ,所以没法像这样让数据块复活。一旦某个数据块在某个快照中「死」了, 就意味着它在随后的所有快照中都不再被引用到了。

ZFS 的快照具有的上述三点条件,使得 ZFS 的快照删除算法可以基于 birth time 。回顾上面 ZFS 的块指针 中讲到, ZFS 的每个块指针都有一个 birth txg 属性,记录这个块诞生时 pool 所在的 txg 。于是可以根据这个 birth txg 找到快照所引用的「独占」数据块然后释放掉它们。

具体来说,🐢乌龟算法可以这样删除一个快照:

  1. 在 DSL 层找出要删除的快照(我们叫他 s ),它的前一个快照(叫它 ps ),后一个快照(叫它 ns ),分别有各自的 birth txg 叫 s.birth, ps.birth, ns.birth 。
  2. 遍历 s 的 DMU 对象集指针所引出的所有块指针。 这里所有块指针在逻辑上构成一个由块指针组成的树状结构,可以有间接块组成的指针树,可以有对象集的 dnode 保存的块指针,这些都可以看作是树状结构的中间节点。
    1. 每个树节点的指针 bp,考察如果 bp.birth <= ps.birth ,那么这个指针和其下所有指针都还被前一个快照引用着,需要保留这个 bp 引出的整个子树。
    2. 按定义 bp.birth 不可能 > s.birth 。
    3. 对所有满足 ps.birth < bp.birtu <= s.birth 的 bp ,需要去遍历 ns 的相应块指针(同样文件的同样偏移位置),看是否还在引用 bp 。
      • 如果存在,继续递归往下考察树状结构中 bp 的所有子节点指针。因为可能共享了这个 bp 但 CoW 了新的子节点。
      • 如果不存在,说明下一个快照中已经删了 bp 。这时可以确定地说 bp 是 s 的「独占」数据块。
  3. 释放掉所有找到的 s 所「独占」的数据块。

上述算法的一些边角情况可以自然地处理,比如没有后一个快照时使用当前数据集的写入点, 没有前一个快照时那么不被后一个快照引用的数据块都是当前要删除快照的独占数据块。

分析一下乌龟算法的复杂度的话,算法需要分两次,读 s 和 ns 中引用到的所有 ps 之后创建的数据块的指针,重要的是这些读都是在整个文件系统范围内的随机读操作,所以速度非常慢……

🐰兔子算法:死亡列表算法(ZFS早期)

可以粗略地认为🐢乌龟算法算是用 birth txg 优化代码路径的 GC 算法,利用了一部分元数据中的 birth txg 信息来避免扫描所有元数据,但是概念上仍然是在扫描元数据找出快照的独占数据块, 而非记录和跟踪快照的数据块,在最坏的情况下仍然可能需要扫描几乎所有元数据。

🐰兔子算法基于🐢乌龟算法的基本原理,在它基础上跟踪快照所引用数据块的一些信息, 从而很大程度上避免了扫描元数据的开销。ZFS 在早期使用这个算法跟踪数据集和快照引用数据块的情况。

🐰兔子算法为每个数据集(文件系统或快照)增加了一个数据结构,叫死亡列表(dead list), 记录 前一个快照中还活着,而当前数据集中死掉了的数据块指针 ,换句话说就是在本数据集中「杀掉」的数据块。举例画图大概是这样

ditaa diagram

上图中有三个快照和一个文件系统,共 4 个数据集。每个数据集维护自己的死亡列表, 死亡列表中是那些在该数据集中被删掉的数据块。于是🐰兔子算法把🐢乌龟算法所做的操作分成了两部分, 一部分在文件系统删除数据时记录死亡列表,另一部分在删除快照时根据死亡列表释放需要释放的块。

在当前文件系统删除数据块(不再被当前文件系统引用)时,负责比对 birth txg 维护当前文件系统的死亡列表。每删除一个数据块,指针为 bp 时,判断 bp.birth 和文件系统最新的快照(上图为 s3)的 birth:

  • bp.birth <= s3.birth: 说明 bp 被 s3 引用,于是将 bp 加入 fs1 的 deadlist
  • bp.birth > s3.birth:说明 bp 指向的数据块诞生于 s3 之后,可以直接释放 bp 指向的块。

创建新快照时,将当前文件系统(图中 fs1)的死亡列表交给快照,文件系统可以初始化一个空列表。

删除快照时,我们有被删除的快照 s 和前一个快照 ps 、后一个快照 ns ,需要读入当前快照 s 和后一个快照 ns 的死亡列表:

  1. 对 s.deadlist 中的每个指针 bp
    • 复制 bp 到 ns.deadlist
  2. 对 ns.deadlist 中的每个指针 bp (其中包含了上一步复制来的)
    • 如果 bp.birth > ps.birth ,释放 bp 的空间
    • 否则保留 bp

换个说法的话, 死亡列表记录的是每个数据集需要负责删除,但因为之前的快照还引用着所以不能删除的数据块列表 。从当前文件系统中删除一个数据块时,这个职责最初落在当前文件系统身上,随后跟着创建新快照职责被转移到新快照上。 每个负责的数据集根据数据块的出生时间是否早于之前一个快照来判断现在是否能立刻释放该块, 删除一个快照时则重新评估自己负责的和下一个快照负责的数据块的出生时间。

从所做的事情来看,🐰兔子算法并没有比🐢乌龟算法少做很多事情。🐢乌龟算法删除一个快照, 需要遍历当前快照和后一个快照两组数据块指针中,新写入的部分; 🐰兔子算法则需要遍历当前快照和后一个快照两个死亡列表中,新删除的块指针。 但是实际🐰兔子算法能比🐢乌龟算法快不少,因为维护死亡列表的操作只在文件系统删除数据时和删除快照时, 顺序写入,并且删除快照时也只需要顺序读取死亡列表。在磁盘这种块设备上,顺序访问能比随机访问有数量级的差异。

不过记录死亡列表也有一定存储开销。最差情况下,比如把文件系统写满之后,创建一个快照, 再把所有数据都删掉,此时文件系统引用的所有数据块的块指针都要保存在文件系统的死亡列表中。 按 ZFS 默认的 128KiB 数据块大小,每块需要 128 字节的块指针,存储这些死亡列表所需开销可能要 整个文件系统大小的 1/1024 。如果用 4KiB 的数据块大小,所需开销则是 1/32 , 1TiB 的盘会有 32GiB 拿来存放这些块指针,将高于用位图数组所需的存储量。

🐆豹子算法:死亡列表的子列表

🐆豹子算法是 ZFS 后来在 2009 年左右实现的算法。在🐰兔子算法中就可以看到,每次删除快照操作死亡列表的时候, 都需要扫描死亡列表中的块指针,根据指针中记录的 birth txg 做判断是否能直接释放或是需要保留到另一个快照的死亡列表。 于是🐆豹子算法的思路是,在死亡列表中记录块指针时,就把其中的块指针按 birth txg 分成子列表(sublist)。

比如上面🐰兔子算法中那4个死亡列表,可以这样拆成子列表:

ditaa diagram

这样拆成子列表之后,每次从死亡列表中释放数据块都能根据出生时间找到对应的子列表, 然后连续释放整个子列表。每次合并死亡列表时,也能直接用单链表穿起需要合并的子列表,不需要复制块指针。

死亡列表并不在跟踪快照的独占大小,而是在跟踪快照所需负责删除的数据块大小, 从这个数值可以推算出快照的独占大小之类的信息。 有了按出生时间排列的死亡列表子列表之后,事实上给任何一个出生时间到死亡时间的范围, 都可以找出对应的几个子列表,从而根据子列表的大小可以快速计算出每个快照范围的「独占」数据块、 「共享」数据块等大小,这不光在删除快照时很有用,也可以用来根据大小估算 zfs send 或者别的基于快照操作时需要的时间。

从直觉上理解,虽然 ZFS 没有直接记录每个数据块属于哪个数据集,但是 ZFS 跟踪记录了每个数据块的归属信息,也就是说由哪个数据集负责释放这个数据块。 在文件系统中删除数据块或者快照时,这个归属信息跟着共享数据块转移到别的快照中,直到最终被释放掉。

生存日志:ZFS 如何管理克隆的空间占用

Fast Clone Deletion by Sara Hartse

以上三种算法负责在 ZFS 中跟踪快照的空间占用,它们都基于数据块的诞生时间,所以都假设 ZFS 中对数据块的分配是位于连续的快照时间轴上。但是明显 ZFS 除了快照和文件系统, 还有另一种数据集可能分配数据块,那就是 克隆 ,于是还需要在克隆中使用不同的算法单独管理因克隆而分配的数据块。 OpenZFS Summit 2017 有个演讲 Fast Clone Deletion by Sara Hartse 解释了其中的细节。

首先克隆的存在本身会锁住克隆引用到的快照,不能删除这些被依赖的快照, 所以克隆无须担心靠快照共享的数据块的管理问题。因此克隆需要管理的,是从快照分离之后, 新创建的数据块。

和🐢乌龟算法一样,原理上删除克隆的时候可以遍历克隆引用的整个 DMU 对象集,找出其中晚于快照的诞生时间的数据块,然后释放它们。也和🐢乌龟算法一样, 这样扫描整个对象集的开销很大,所以使用一个列表来记录数据块指针。 克隆管理新数据块的思路和快照的🐰兔子算法维持死亡列表的思路相反, 记录所有新诞生的数据块,这个列表叫做「生存日志(livelist)」。

克隆不光要记录新数据块的诞生,还要记录新数据块可能的死亡,所以磁盘上保存的生存日志虽然叫 livelist ,但不像死亡列表那样是列表的形式,而是日志的形式,而内存中保存的生存日志则组织成了棵 自平衡树(AVLTree) 来加速查找。

ditaa diagram

磁盘上存储的生存日志如上图,每个表项记录它是分配(A)或者删除(F)一个数据块,同时记录数据块的地址。 这些记录在一般情况下直接记录在日志末尾,随着对克隆的写入操作而不断增长,长到一定程度则从内存中的 AVL Tree 直接输出一个新的生存日志替代掉旧的,合并其中对应的分配和删除操作。

生存日志可以无限增长,从而要将整个生存列表载入内存也有不小的开销,这里的解决方案有点像快照管理中用 🐆豹子算法改进🐰兔子算法的思路,把一个克隆的整个生存日志也按照数据块的诞生时间拆分成子列表。 Sara Hartse 的演讲 Fast Clone Deletion 中继续解释了其中的细节和优化方案,感兴趣的可以看看。

3.4   btrfs 的空间跟踪算法:引用计数与反向引用

理解了 ZFS 中根据 birth txg 管理快照和克隆的算法之后,可以发现它们基于的假设难以用于 WAFL 和 btrfs 。 ZFS 严格区分文件系统、快照、克隆,并且不存在 reflink ,从而可以用 birth txg 判断数据块是否需要保留,而 WAFL 和 btrfs 中不存在 ZFS 的那些数据集分工,又想支持 reflink ,可见单纯基于 birth txg 不足以管理 WAFL 和 btrfs 子卷。

让我们回到一开始日志结构文件系统中基于垃圾回收(GC)的思路上来,作为程序员来看, 当垃圾回收的性能不足以满足当前需要时,大概很自然地会想到:引用计数(reference counting)。 编程语言中用引用计数作为内存管理策略的缺陷是:强引用不能成环, 这在文件系统中看起来不是很严重的问题,文件系统总体上看是个树状结构,或者就算有共享的数据也是个 上下层级分明的有向图,很少会使用成环的指针,以及文件系统记录指针的时候也都会区分指针的类型, 根据指针类型可以分出强弱引用。

EXTENT_TREE 和引用计数

btrfs 中就是用引用计数的方式跟踪和管理数据块的。引用计数本身不能保存在 FS_TREE 或者指向的数据块中,因为这个计数需要能够变化,对只读快照来说整个 FS_TREE 都是只读的。 所以这里增加一层抽象, btrfs 中关于数据块的引用计数用一个单独的 CoW B树来记录,叫做 EXTENT_TREE ,保存于 ROOT_TREE 中的 2 号对象位置。

btrfs 中每个块都是按 区块(extent) 的形式分配的,区块是一块连续的存储空间,而非 zfs 中的固定大小。每个区块记录存储的位置和长度, 以及这里所说的引用计数。所以本文最开始讲 Btrfs 的子卷和快照 中举例的那个平坦布局,如果画上 EXTENT_TREE 大概像是下图这样,其中每个粗箭头是一个区块指针,指向磁盘中的逻辑地址,细箭头则是对应的 EXTENT_TREE 中关于这块区块的描述:

Flat_layout_extents_on_disk superblock SUPERBLOCK ... root_tree ... roottree ROOT_TREE 2: extent_tree 3: chunk_tree 4: dev_tree 5: fs_tree 6: root_dir "default" -> ROOT_ITEM 256 10: free_space_tree 256: fs_tree "root" 257: fs_tree "home" 258: fs_tree "www" 259: fs_tree "postgres" -7: tree_log_tree -5: orphan_root superblock:sn_root->roottree:label toplevel FS_TREE "toplevel" 256: inode_item DIR 256: dir_item: "root" -> ROOT_ITEM 256 256: dir_item: "home" -> ROOT_ITEM 257 256: dir_item: "var" -> INODE_ITEM 257 256: dir_item: "postgres" -> ROOT_ITEM 259 257: inode_item DIR 257: dir_item: "www" -> ROOT_ITEM 258 roottree:root_fs->toplevel:label root FS_TREE "root" 256: inode_item DIR roottree:root_sub_root->root:label home FS_TREE "home" 256: inode_item DIR roottree:root_sub_home->home:label www FS_TREE "www" 256: inode_item DIR roottree:root_sub_www->www:label postgres FS_TREE "postgres" 256: inode_item DIR roottree:root_sub_postgres->postgres:label extent_tree EXTENT_TREE 0x2000 len=0x1000 : ref=1 gen=8 0x3000 len=0x1000 : ref=1 gen=8 0x11000 len=0x1000 : ref=1 gen=8 0x12000 len=0x1000 : ref=1 gen=6 0x13000 len=0x1000 : ref=1 gen=6 0x14000 len=0x1000 : ref=1 gen=6 0x15000 len=0x1000 : ref=1 gen=7 ... roottree:root_extent->extent_tree:label roottree:label->extent_tree:extent_roottree toplevel:label->extent_tree:extent_toplevel root:label->extent_tree:extent_root home:label->extent_tree:extent_home www:label->extent_tree:extent_www postgres:label->extent_tree:extent_postgres extent_tree:extent_extent->extent_tree:label
btrfs 中关于 chattr +C 关闭了 CoW 的文件的处理
2020年2月20日补充

这里从 EXTENT_TREE 的记录可以看出,每个区块都有引用计数记录。对用 chattr +C 关闭了 CoW 的文件而言,文件数据同样还是有引用计数,可以和别的文件或者快照共享文件数据的。 这里的特殊处理在于,每次写入一个 nocow 的文件的时候,考察这个文件指向区块的引用计数, 如果引用计数 >1 ,表示这个文件的区块发生过 reflink ,那会对文件内容做一次 CoW 断开 reflink 并写入新位置;如果引用计数 =1 ,那么直接原地写入文件内容而不 CoW 。于是 nocow 的文件仍然能得到 reflink 和 snapshot 的功能, 使用这些功能仍然会造成文件碎片并伴随性能损失,只是在引用计数为 1 的时候不发生 CoW 。

包括 ROOT_TREE 和 EXTENT_TREE 在内,btrfs 中所有分配的区块(extent)都在 EXTENT_TREE 中有对应的记录,按区块的逻辑地址索引。从而给定一个区块,能从 EXTENT_TREE 中找到 ref 字段描述这个区块有多少引用。不过 ROOT_TREE 、 EXTENT_TREE 和别的一些 pool-wide 数据结构本身不依赖引用计数的,这些数据结构对应的区块的引用计数总是 1 ,不会和别的树共享区块;从 FS_TREE 开始的所有树节点都可以共享区块,这包括所有子卷的元数据和文件数据,这些区块对应的引用计数可以大于 1 表示有多处引用。

EXTENT_TREE 按区块的逻辑地址索引,记录了起始地址和长度,所以 EXTENT_TREE 也兼任 btrfs 的空间利用记录,充当别的文件系统中 block bitmap 的指责。比如上面例子中的 extent_tree 就表示 [0x2000,0x4000) [0x11000,0x16000) 这两段连续的空间是已用空间, 剩下的空间按定义则是可用空间。为了加速空间分配器, btrfs 也有额外的 free space cache 记录在 ROOT_TREE 的 10 号位置 free_space_tree 中,不过在 btrfs 中这个 free_space_tree 记录的信息只是缓存,必要时可以通过 btrfs check --clear-space-cache 扔掉这个缓存重新扫描 extent_tree 并重建可用空间记录。

比如我们用如下命令创建了两个文件,通过 reflink 让它们共享区块,然后创建两个快照, 然后删除文件系统中的 file2 :

write fs/file1
cp --reflink=always fs/file1 fs/file2
btrfs subvolume snapshot fs sn1
btrfs subvolume snapshot fs sn2
rm fs/file2

经过以上操作之后,整个 extent_tree 的结构中记录的引用计数大概如下图所示:

btrfs_reflink_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 sn2 fs_tree : ref 1 sn1 sn2 leaf_node: ref 2 fs fs_tree : ref 1 fs leaf_node : ref 1 file1 : ref 3 root:label->extent:root snleaf FS_TREE leaf_node file1 file2 sn1:leaf->snleaf:label sn1:label->extent:sn1 sn2:leaf->snleaf:label sn2:label->extent:sn2 fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label fs:label->extent:fs snleaf:label->extent:snleaf snleaf:f1->extent:f1 snleaf:f2->extent:f1 fsleaf:label->extent:fsleaf fsleaf:f1->extent:f1

上图简化了一些细节,实际上每个文件可以引用多个区块(文件碎片), 其中每个对区块的引用都可以指明引用到具体某个区块记录的某个地址偏移和长度, 也就是说文件引用的区块可以不是区块记录中的一整个区块,而是一部分内容。

图中可见,整个文件系统中共有5个文件路径可以访问到同一个文件的内容,分别是 sn1/​file1, sn1/​file2, sn2/​file1, sn2/​file2, fs/​file1 , 在 extent_tree 中, sn1 和 sn2 可能共享了一个 B树 叶子节点,这个叶子节点的引用计数为 2 ,然后每个文件的内容都指向同一个 extent ,这个 extent 的引用计数为 3 。

删除子卷时,通过引用计数就能准确地释放掉子卷所引用的区块。具体算法挺符合直觉的:

  1. 从子卷的 FS_TREE 往下遍历
    • 遇到引用计数 >1 的区块,减小该块的计数即可,不需要再递归下去
    • 遇到引用计数 =1 的区块,就是子卷独占的区块,需要释放该块并递归往下继续扫描

大体思路挺像上面介绍的 ZFS 快照删除的🐢乌龟算法 ,只不过根据引用计数而非 birth txg 判断是否独占数据块。性能上说, btrfs 的B树本身内容就比较紧凑,FS_TREE 一个结构就容纳了文件 inode 和引用的区块信息, EXTENT_TREE 按地址排序也比较紧凑,所以删除算法的随机读写不像 ZFS 的🐢乌龟算法那么严重, 实际实现代码里面也可能通过 btrfs generation 做一些类似基于 birth txg 优化的快速代码路径。 即便如此,扫描 FS_TREE 仍然可能需要耗时良久,这个递归的每一步操作都会记录在 ROOT_TREE 中专门的结构,也就是说删除一个子卷的操作可以执行很长时间并跨越多个 pool commit 。 btrfs subvolume delete 命令默认也只是记录下这个删除操作,然后就返回一句类似: Delete subvolume (no-commit): /​subvolume/​path 的输出,不会等删除操作执行结束。 相比之下 ZFS 那边删除一个快照或文件系统必须在一个 txg 内执行完,没有中间过程的记录, 所以如果耗时很久会影响整个 pool 的写入,于是 ZFS 那边必须对这些操作优化到能在一个 txg 内执行完的程度(摧毁克隆方面 ZFS 还有 async_destroy 优化 可能有些帮助)。

只需要引用计数就足够完成快照的创建、删除之类的功能,也能支持 reflink 了(仔细回想, reflink 其实就是 reference counted link 嘛),普通读写下也只需要引用计数。 但是只有引用计数不足以知道区块的归属,不能用引用计数统计每个子卷分别占用多少空间, 独占多少区块而又共享多少区块。上面的例子就可以看出,所有文件都指向同一个区块,该区块的引用计数为 3 ,而文件系统中一共有 5 个路径能访问到该文件。可见从区块根据引用计数反推子卷归属信息不是那么一目了然的。

反向引用(back reference)

单纯从区块的引用计数难以看出整个文件系统所有子卷中有多少副本。 也就是说单有引用计数的一个数字还不够,需要记录具体反向的从区块往引用源头指的引用,这种结构在 btrfs 中叫做「反向引用(back reference,简称 backref)」。所以在上图中每一个指向 EXTENT_TREE 的单向箭头,在 btrfs 中都有记录一条反向引用,通过反向引用记录能反过来从被指针指向的位置找回到记录指针的地方。

反向引用(backref)是 btrfs 中非常关键的机制,在 btrfs kernel wiki 专门有一篇页面 Resolving Extent Backrefs 解释它的原理和实现方式。

对上面的引用计数的例子画出反向引用的指针大概是这样:

btrfs_reflink_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 backref ROOT_TREE sn1 sn2 fs_tree : ref 1 backref ROOT_TREE sn2 sn1 sn2 leaf_node: ref 2 backref sn1 FS_TREE node backref sn2 FS_TREE node fs fs_tree : ref 1 backref ROOT_TREE fs fs leaf_node : ref 1 backref fs FS_TREE node file1 : ref 3 backref sn1 FS_TREE leaf_node file1 backref sn1 FS_TREE leaf_node file2 backref fs FS_TREE leaf_node file1 snleaf FS_TREE leaf_node file1 file2 sn1:leaf->snleaf:label sn2:leaf->snleaf:label fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label extent:br1->root:label extent:br2->root:label extent:br5->root:label extent:br3->sn1:label extent:br4->sn2:label extent:br6->fs:label extent:br7->snleaf:label extent:br8->snleaf:label extent:br9->fsleaf:label

EXTENT_TREE 中每个 extent 记录都同时记录了引用到这个区块的反向引用列表。反向引用有两种记录方式:

  1. 普通反向引用(Normal back references)。记录这个指针来源所在是哪颗B树、 B树中的对象 id 和对象偏移。
    • 对文件区块而言,就是记录文件所在子卷、inode、和文件内容的偏移。
    • 对子卷的树节点区块而言,就是记录该区块的上级树节点在哪个B树的哪个位置开始。
  2. 共享反向引用(Shared back references)。记录这个指针来源区块的逻辑地址。
    • 无论对文件区块而言,还是对子卷的树节点区块而言,都是直接记录了保存这个区块指针的上层树节点的逻辑地址。

有两种记录方式是因为它们各有性能上的优缺点:

普通反向引用:因为通过对象编号记录,所以当树节点 CoW 改变了地址时不需要调整地址, 从而在普通的读写和快照之类的操作下有更好的性能, 但是在解析反向引用时需要额外一次树查找。 同时因为这个额外查找,普通反向引用也叫间接反向引用。
共享反向引用:因为直接记录了逻辑地址,所以当这个地址的节点被 CoW 的时候也需要调整这里记录的地址。 在普通的读写和快照操作下,调整地址会增加写入从而影响性能,但是在解析反向引用时更快。

通常通过普通写入、快照、 reflink 等方式创建出来的引用是普通反向引用, 由于普通反向引用记录了包含它的B树,从而可以说绑在了某棵树比如某个子卷上, 当这个普通反向引用指向的对象不再存在,而这个反向引用还在通过别的途径共享时, 这个普通反向引用会转换共享反向引用;共享反向引用在存在期间不会变回普通反向引用。

比如上图反向引用的例子中,我们先假设所有画出的反向引用都是普通反向引用,于是图中标为 file1 引用数为 3 的那个区块有 3 条反向引用记录,其中前两条都指向 sn1 里面的文件,分别是 sn1/file1 和 sn1/file2 ,然后 sn1 和 sn2 共享了 FS_TREE 的叶子节点。

假设这时我们删除 sn1/file2,执行了代码 rm sn1/​file2 之后:

btrfs_reflink_shared_backref root ROOT_TREE sn1 sn2 fs sn1 FS_TREE sn1 leaf_node root:sn1->sn1:label sn2 FS_TREE sn2 leaf_node root:sn2->sn2:label fs FS_TREE fs leaf_node root:fs->fs:label extent EXTENT_TREE extent_tree root_tree : ref 1 sn1 fs_tree : ref 1 backref ROOT_TREE sn1 sn2 fs_tree : ref 1 backref ROOT_TREE sn2 sn1 sn2 leaf_node: ref 2 backref sn1 FS_TREE node backref sn2 FS_TREE node fs fs_tree : ref 1 backref ROOT_TREE fs fs leaf_node : ref 1 backref fs FS_TREE node file1 : ref 4 backref FS_TREE leaf_node file1 backref FS_TREE leaf_node file2 backref fs FS_TREE leaf_node file1 backref sn1 FS_TREE leaf_node file1 sn1leaf FS_TREE leaf_node file1 sn1:leaf->sn1leaf:label snleaf FS_TREE leaf_node file1 file2 sn2:leaf->snleaf:label fsleaf FS_TREE leaf_node file1 fs:leaf->fsleaf:label extent:br1->root:label extent:br2->root:label extent:br5->root:label extent:br3->sn1:label extent:br4->sn2:label extent:br6->fs:label extent:br10->sn1leaf:label extent:br7->snleaf:label extent:br8->snleaf:label extent:br9->fsleaf:label

那么 sn1 会 CoW 那个和 sn2 共享的叶子节点,有了新的属于 sn1 的叶子,从而断开了原本 file1 中对这个共享叶子节点的两个普通反向引用,转化成共享反向引用(图中用虚线箭头描述), 并且插入了一个新的普通反向引用指向新的 sn1 的叶子节点。

遍历反向引用(backref walking)

有了反向引用记录之后,可以给定一个逻辑地址,从 EXTENT_TREE 中找到地址的区块记录, 然后从区块记录中的反向引用记录一步步往回遍历,直到遇到 ROOT_TREE ,最终确定这个逻辑地址的区块在整个文件系统中有多少路径能访问它。 这个遍历反向引用的操作,在 btrfs 文档和代码中被称作 backref walking 。

比如还是上面的反向引用图例中 sn1 和 sn2 完全共享叶子节点的那个例子,通过 backref walking ,我们能从 file1 所记录的 3 个反向引用,推出全部 5 个可能的访问路径。

backref walking 作为很多功能的基础设施,从 btrfs 相当早期(3.3内核)就有,很多 btrfs 的功能实际依赖 backref walking 的正确性。列举一些需要 backref walking 来实现的功能:

  1. qgroup

    btrfs 的子卷没有记录子卷的磁盘占用开销,靠引用计数来删除子卷, 所以也不需要详细统计子卷的空间占用情况。不过对一些用户的使用场景,可能需要统计子卷空间占用。由于 可能存在的共享元数据和数据,子卷占用不能靠累计加减法的方式算出来,所以 btrfs 有了 qgroup 和 quota 功能,用来统计子卷或者别的管理粒度下的占用空间情况。为了实现 qgroup ,需要 backref walking 来计算区块共享的情况。

  2. send

    btrfs send 在计算子卷间的差异时,也通过 backref walking 寻找能靠 reflink 共享的区块,从而避免传输数据。

  3. balance/scrub

    balance 和 scrub 都会调整区块的地址,通过 backref walking 能找到所有引用到这个地址的位置并正确修改地址。

  4. check

    当需要打印诊断信息的时候,除了提供出错的数据所在具体地址之外,通过 backref walking 也能提供受影响的文件路径之类的信息。

btrfs 的 reflink-aware defrag
这里想提一下 btrfs 一直计划中,但是还没有成功实现的 reflink-aware defrag 。文件碎片一直是 CoW 文件系统的大问题,对 btrfs 和对 ZFS 都是同样。ZFS 完全不支持碎片整理, 而 btrfs 目前只提供了文件级别的碎片整理,这会切断现有的 reflink 。计划中的 reflink-aware defrag 也是基于 backref walking ,根据区块引用的碎片程度,整理碎片而某种程度上保持 reflink 。btrfs 曾经实现了这个,但是因为 bug 太多不久就取消了相关功能,目前这个工作处于停滞阶段。

可见 backref walking 的能力对 btrfs 的许多功能都非常重要(不像 ZPL 的 dnode 中记录的 parent dnode 那样只用于诊断信息 )。不过 backref walking 根据区块共享的情况的不同,也可能导致挺大的运行期开销,包括算法时间上的和内存占用方面的开销。 比如某个子卷中有 100 个文件通过 reflink 共享了同一个区块,然后对这个子卷做了 100 个快照, 那么对这一个共享区块的 backref walking 结果可能解析出 10000 个路径。可见随着使用 reflink 和快照, backref walking 的开销可能爆炸式增长。最近 btrfs 邮件列表也有一些用户汇报,在大量子卷 和通过 reflink 做过 dedup 的 btrfs 文件系统上 send 快照时,可能导致内核分配大量内存甚至 panic 的情形,在 5.5 内核中 btrfs send 试图控制 send 时 clone reference 的数量上限来缓解这种边角问题。

值得再强调的是,在没有开启 qgroup 的前提下,正常创建删除快照或 reflink ,正常写入和覆盖区块之类的文件系统操作,只需要引用计数就足够,虽然可能需要调整反向引用记录( 尤其是共享反向引用的地址),但是不需要动用 backref walking 这样的重型武器。

4   ZFS vs btrfs 的 dedup 功能现状

上面讨论 ZFS 的快照和克隆如何跟踪数据块时,故意避开了 ZFS 的 dedup 功能,因为要讲 dedup 可能需要先理解引用计数在文件系统中的作用,而 btrfs 正好用了引用计数。 于是我们再回来 ZFS 这边,看看 ZFS 的 dedup 是具体如何运作的。

稍微了解过 btrfs 和 ZFS 两者的人,或许有不少 btrfs 用户都眼馋 ZFS 有 in-band dedup 的能力,可以在写入数据块的同时就去掉重复数据,而 btrfs 只能「退而求其次」地选择第三方 dedup 方案,用外部工具扫描已经写入的数据,将其中重复的部分改为 reflink 。又或许有不少 btrfs 用户以为 zfs 的 dedup 就是在内存和磁盘中维护一个类似 Bloom filter 的结构,然后根据结果对数据块增加 reflink ,从而 zfs 内部大概一定有类似 reflink 的设施,进一步质疑为什么 btrfs 还迟迟没有实现这样一个 Bloom filter 。 或许还有从 btrfs 转移到 ZFS 的用户有疑惑, 为什么 ZFS 还没有暴露出 reflink 的用户空间接口 ,或者既然 ZFS 已经有了 dedup , 能不能临时开关 dedup 来提供类似 reflink 式的共享数据块 而避免 ZFS 长期开 dedup 导致的巨大性能开销。

看过上面 ZFS 中关于快照和克隆的空间跟踪算法 之后我们会发现,其实 ZFS 中并没有 能对应 btrfs reflink 的功能,而是根据数据块指针中的 birth txg 来跟踪快照和克隆的共享数据块的。这引来更多疑惑:

4.1   ZFS 是如何实现 dedup 的?

Dedup Performance by Matt Ahrens

ZFS 是在 Sun/OpenSolaris 寿命相当晚期的 2009 年获得的 dedup 功能,就在 Oracle 收购 Sun ,OpenSolaris 分裂出 Illumos 从而 ZFS 分裂出 Oracle ZFS 和 OpenZFS 的时间点之前。因此 关于 ZFS dedup 如何实现的文档相对匮乏 ,大部分介绍 ZFS 的文档或者教程会讲到 ZFS dedup 的用法,但是对 dedup 的实现细节、性能影响、乃至使用场景之类的话题就很少提了(甚至很多教程讲了一堆用法之后说类似, 「我评估之后觉得我不需要开 dedup ,你可以自己评估一下」这样的建议)。

OpenZFS Summit 2017 上 Matt 有个演讲,主要内容关于今后如何改进 dedup 性能的计划,其中讲到的计划还没有被具体实现,不过可以窥探一下 dedup 现在在 ZFS 中是如何工作的。 Chris 的博客也有两篇文章《 What I can see about how ZFS deduplication seems to work on disk 》和《 An important addition to how ZFS deduplication works on the disk 》介绍了他对此的认识,在这里我也尝试来总结一下 ZFS dedup 特性如何工作。

ZFS dedup 是存储池级别(pool-wide)开关的特性,所以大概在 MOS 之类的地方有存储一个特殊的数据结构, 叫 DeDup Table 简称 DDT 。DDT 目前是存储设备上的一个 hash table ,因为是存储池级别的元数据, 所以在 ZFS 中存储了三份完全一样的 DDT ,DDT 的内容是大概如下结构:

Checksum DVA(Data Virtual Address) Refcount
0x12345678 vdev=1 addr=0x45671234 3
0x5678efab vdev=2 addr=0x37165adb 0
0x98765432 vdev=1 addr=0xac71be12 1
0xabcd1234 vdev=0 addr=0xc1a2231d 5
... ... ... ... ... ...

DDT 中对每个数据块存有3个东西:数据块的 checksum 、DVA (就是 ZFS 的块指针 中的 DVA)和引用计数。在存储池开启 dedup 特性之后,每次新写入一个数据块,都会先计算出数据块的 checksum ,然后查找 DDT ,存在的话增加 DDT 条目的引用计数,不存在的话插入 DDT 条目。每次释放一个数据块,同样需要查找 DDT 调整引用计数。

除了 DDT 之外,文件系统中记录的块指针中也有个特殊标志位记录这个块是否经过了 DDT 。读取数据不需要经过 DDT ,但是子卷、克隆或者文件系统正常删除数据块的时候, 需要根据块指针中的标志位判断是否需要检查和调整 DDT 。

从而关于 dedup 的实现可以得知以下一些特点:

  • 开启 dedup 之后,每个写入操作放大成 3+1 个随机位置的写入操作,每个删除操作变成 1 个写入操作。没有 dedup 时删除块并不需要立刻写入,只需要记录在内存中并在 MOS 提交的时候调整磁盘占用情况即可。
  • 只有开启 dedup 期间写入的数据块才会参与 dedup 。对已经有数据的存储池,后来开启的 dedup 不会影响已经写好的数据,从而即使后来新的写入与之前的写入有重复也得不到 dedup 效果。 DDT 中没有记录的数据块不会参与 dedup 。换句话说 DDT 中那些引用计数为 1 的记录也是必须存在的,否则这些数据块没有机会参与 dedup 。
  • 关闭 dedup 之后,只要 DDT 中还存有数据,那么对这些数据的删除操作仍然有性能影响。

从直觉上可以这样理解:在 ZFS 中每个数据块都有其「归属」,没有 dedup 的时候,数据块归属于某个数据集(文件系统、快照、克隆), 该数据集需要负责释放该数据块或者把从属信息转移到别的数据集(快照)上。 而在开启 dedup 期间,产生的写入的数据块实际归属于 DDT 而不是任何一个数据集,数据集需要查询和调整 DDT 中记录的引用计数来决定是否能释放数据块。

乍看起来 DDT 貌似挺像 btrfs 的 EXTENT_TREE ,但是本质上 EXTENT_TREE 是根据区块地址排序的, 而 DDT 因为是个 hashtable 所以是根据 checksum 排序的。并且 EXTENT_TREE 中记录的区块可以是任意大小,而 DDT 中记录的数据块是固定大小的,所以碎片不严重的情况下 DDT 要比 EXTENT_TREE 多记录很多数据块。这些区别都非常影响操作 DDT 时的性能。

DDT 本身是个 DMU 对象,所以对 DDT 的读写也是经过 DMU 的 CoW 读写,从而也经过 ARC 的缓存。想要有比较合理的 dedup 性能,需要整个 DDT 都尽量保持在内存 ARC 或者 L2ARC 缓存中, 于是 dedup 特性也有了非常占用内存的特点。每个 DDT 表项需要大概 192 字节来描述一个( 默认 128KiB 大小的)数据块,由此可以估算一下平均每 2TiB 的数据需要 3GiB 的内存来支持 dedup 的功能。

Matt 的视频中后面讲到优化 ZFS dedup 的一些思路,大体上未来 ZFS 可以做这些优化:

  1. DDT 在内存中仍然是 hashtable ,在存储介质上则换成类似 ZIL 的日志结构,让 DDT 尽量保持在内存中,并且绕过 DMU 减少写入放大。
  2. 给 DDT 表项瘦身,从192字节缩减到接近64字节。
  3. 当遇到内存压力时,从 DDT 中随机剔除掉引用计数为 1 的表项。被剔除的表项没有了未来参与 dedup 的可能性,但是能减轻内存压力。剔除引用计数为 1 的表项仍然可以维持数据块的归属信息( 处理上当作是没有 dedup 的形式),但是引用计数更高的表项没法剔除。

这些优化策略目的是想让 dedup 的性能损失能让更多使用场景接受。不过因为缺乏开发者意愿, 目前这些策略还只是计划,没有实现在 ZFS 的代码中。

因为以上特点, ZFS 目前 dedup 特性的适用场景极为有限,只有在 IO 带宽、内存大小都非常充裕, 并且可以预见到很多重复的数据的时候适合。听说过的 ZFS dedup 的成功案例是,比如提供虚拟机服务的服务商,在宿主文件系统上用 ZFS 的 zvol 寄宿虚拟机的磁盘镜像,客户在虚拟机内使用其它文件系统。大部分客户可能用类似版本的操作系统, 从而宿主机整体来看有很多 dedup 的潜质。不过这种应用场景下,服务商很可能偏向选择 CephFS 这样的分布式文件系统提供虚拟机镜像存储,而不是 ZFS 这样局限在单系统上的本地文件系统。

4.2   btrfs 的 dedup

btrfs 目前没有内建的 dedup 支持,但是因为有 reflink 所以可以通过第三方工具在事后扫描文件块来实现 dedup 。这一点乍看像是某种将就之策,实际上了解了 ZFS dedup 的实现之后可以看出这个状况其实更灵活。

在 btrfs 中实现 in-band dedup 本身不算很复杂,增加一个内存中的 bloom filter 然后按情况插入 reflink 的正常思路就够了。在 btrfs kernel wiki 中有篇笔记 提到已经有了实验性的 in-band dedup 内核支持的实现。这个实现已经越来越成熟,虽然还有诸多使用限制, 不过实现正确性上问题不大,迟迟没有办法合并进主线内核的原因更多是性能上的问题。

如果 btrfs 有了 in-band dedup 这样系统性的 dedup 方案,那么不可避免地会增加文件系统中使用 reflink 的数量。这将会暴露出 backref walking 这样的基础设施中许多潜在的边角情况下的性能瓶颈。 前面解释过 backref walking 操作是个挺大开销的操作,并且开销随着快照和 reflink 的使用而爆炸式增长。直到最近的 btrfs 更新仍然在试图优化和改善现有 backref walking 的性能问题,可以预测 btrfs 的内建 dedup 支持将需要等待这方面更加成熟。

5   结论和展望

不知不觉围绕 btrfs 和 zfs 的快照功能写了一大篇,前前后后写了一个半月, 文中提及的很多细节我自己也没有自信,如果有错误还请指出。

稍微列举一些我觉得比较重要的结论,算是 TL;DR 的 takeaway notes 吧:

  • ZFS 的快照非常轻量。完全可以像 NILFS2 的连续快照那样,每小时一个快照,每天24小时,每年 365天不间断地创建快照,实际似乎也有公司是这样用的。如此频繁的快照不同于 NILFS2 等文件系统提供的连续快照,但是也没有那些日志结构文件系统实现连续快照所需承担的巨大 GC 开销。 并且 ZFS 可以没有额外开销地算出快照等数据集的空间占用之类的信息。
  • btrfs 的快照相对也很轻量,比 LVM 和 dm-thin 的快照轻便很多,但是不如 ZFS 的快照轻,因为 btrfs 有维护反向引用的开销。 btrfs 要得知子卷的空间占用情况需要开启 qgroup 特性,这会对一些需要 backref walking 的操作有一些额外性能损失。
  • btrfs 对快照和 reflink 没有限制,日常桌面系统下使用也不太会遇到性能问题。 不过系统性地(自动化地)大量使用快照和 reflink ,在一些操作下可能会有性能问题,值得注意。
  • 因为没有 reflink , ZFS 的数据集划分需要一些前期计划。 ZFS 中共享元数据的方式只有快照, 所以要尽量多细分文件系统,方便以后能利用到快照特性,划分的粒度大致按照可能要回滚快照的粒度来。 btrfs 有 reflink ,于是这里有很多自由度,即便前期计划不够详细也可以通过 reflink 相对快速调整子卷结构。
  • dedup 在 zfs 和 btrfs 都是个喜忧参半的特性,开启前要仔细评估可能的性能损失。ZFS dedup 的成功案例是,比如虚拟机服务的服务商,在宿主文件系统上用 ZFS 寄宿虚拟机的磁盘镜像,客户在虚拟机可能用类似版本的操作系统,从而宿主机整体来看有很多 dedup 的潜质。一般桌面场景下 dedup 的收益不明显,反而有巨大内存和IO带宽开销。
  • 相比 btrfs ,ZFS 更严格地遵守 CoW 文件系统「仅写一次」的特点,甚至就算遇到了数据块损坏, 修复数据块的时候也只能在原位写入。 btrfs 因为有反向引用所以在这方面灵活很多。
  • ZFS 不支持对单个文件关闭 CoW ,所有文件(以及所有 zvol)都经过 DMU 层有 CoW 语义,这对一些应用场景有性能影响。btrfs 可以对单个文件关闭 CoW ,但是关闭 CoW 同时也丢失了写文件的事务性语义。
  • ZFS 不支持碎片整理,靠 ARC 加大缓存来解决碎片带来的性能问题。 btrfs 有 defrag ,但是目前的实现会切断 reflink 。

最后关于 ZFS 没有 reflink 也没有反向引用的情况,想引用几段话。

FreeBSD 的发起人之一,FreeBSD 的 FFS 维护者, Kirk McKusick 曾经在 OpenZFS developer summit 2015 这么说过:

I decided I'd add a wish list since I have a whole bunch of people here that could actually possibly consider doing this. Both competitors of ZFS, which are basically WAFL and BTRFS, kind of maintained back pointers. And back pointers allow a lot of things like disk migration, you can go through and tune up file layout, if you're working with direct-mapped flash it allows you to do that effectively. This has been a long -- and I understand big debate with the ZFS people and I'm not going to try and talk about that -- but there's a very nice paper that I've cited here, "Tracking Back References in a Write Anywhere File System", that is it integrates keeping track of the back pointers in a way that would work very well with ZFS. And so the cost is low, the cost of actually using it is a little higher, but it's not unreasonable. So there's the reference to that paper and if any of you are contemplating that you should read the paper because if nothing else it's a great paper.

Kirk McKusick 呼吁 ZFS 开发者们考虑在 ZFS 中实现类似 backref 的基础设施,从而可能在未来获得更多有用的特性。

和 ZFS 实现 backref 相关的一点是目前 ZFS 的块指针的组织结构。对此 ZFS 的 ZPL 层原作者之一的 Mark Shellenbaum 在 OpenZFS developer summit 2016 也曾说过这样的话:

(Q: Are there any things that we that we have regretted we did?) A: I guess not so much on the ZPL, but with the way block pointers maybe weren't fully virtualized, you know that things like that.

以及 ZFS 的最初的作者 Jeff 在 OpenZFS developer summit 2015 也曾说过:

... and then certainly one thing i'd always wish we had done but there really were always implementation difficulties was true virtual block addressing. Because it would made dedup simpler, or would have made you know compression of data, defragging, all that kind of stuff simpler. That would have been really nice to have. But we never did the way that was sort of tracable in terms of both the cost and the transactional semantics.

ZFS 这些开发者元老们都希望 ZFS 能有某种类似 backref 的机制,或者让块指针记录的地址更抽象的机制。

关于这一点,ZFS 最重要的作者 Matt 如何看的呢? Matt 近期似乎没有发表过看法,但是熟悉 ZFS 的人可能听到过 Matt 一直在计划的另一项 ZFS 特性中看出些端倪,叫 BP rewrite ,或者 BP virtualization 。从 Matt 还在 Sun 的时候开始,就试图在 ZFS 中实现 BP rewrite 特性,提供某种系统性的基础设施,能够快速地找到并改写大量数据块指针。 在网上搜索很多 ZFS 功能的实现细节,最终都会带到关于 BP rewrite 的讨论(甚至可以说论战)中。 Matt 最近给 OpenZFS 实现的两项功能, toplevel vdev removal 和 raidz expansion 如果有 BP rewrite 将会容易很多,而他们目前是在没有 BP rewrite 的前提下,通过一连串额外抽象实现的。

从 BP rewrite 这个兔子洞中,还能引出更多 btrfs 和 ZFS 关于设备管理的差异,这个有待今后再谈。

by farseerfc at February 19, 2020 06:45 AM

February 17, 2020

中文社区新闻

更新到 openssh-8.2p1 后需要重启 sshd

更新到 openssh-8.2p1 之后,已经开启的 SSH 服务会无法接受新的连接(,详见 FS#65517 )。在远程服务器上更新包时,请确保在 pacman -Syu 升级之后立刻用命令 systemctl restart sshd 重启后台服务。如果更新到了 openssh-8.2p1-3 或以后的版本,将会在升级包时自动重启服务。

by farseerfc at February 17, 2020 02:24 AM

Leo Shen

Way to AOSC OS Maintainer: Advanced Techniques

This article has been officially published in the AOSC Wiki. The following information will not receive any update. So you want to make a package, you've got the urge to make a package, you've got the nerve to make a package, so go ahead, so go ahead, so go ahead and make a package we can use! After learning the basics about building packages, we can now start exploring some advanced techniques.

February 17, 2020 12:00 AM

February 16, 2020

Leo Shen

Way to AOSC OS Maintainer: Basics

This article has been officially published in the AOSC Wiki. The following information will not receive any update. So you want to make a package, you got the urge to make a package, you got the nerve to make a package, so go ahead, so go ahead, so go ahead and make a package we can use! NOTICE: This guide assumes you have moderate knowledge about Linux and its CLI (command line interface).

February 16, 2020 05:50 PM

February 09, 2020

Leo Shen

Have fun with ZFS: Introduction

For many people, filesystem is pretty boring. On Windows, your only choice is NTFS 1. On macOS, it is APFS. There is a little bit more choice on Linux, but xfs and ext4 are good enough and work just fine. But filesystems can actually be very powerful. With some special design principles, modern filesystems can achieve many astonishing functions in order to protect your data. In this article, we will investigate a filesystem that is truly amazing: ZFS.

February 09, 2020 06:09 PM

February 06, 2020

Lainme

修复 Fcitx 在 CentOS 7 上无法使用的问题

CentOS 7上因为gnome的问题,无法正常的选择Fcitx作为输入法。如果你尝试使用imsettings-switch或者im-chooser,会报告以下错误

GDBus.Error:org.gtk.GDBus.UnmappedGError.Quark._imsettings_2derror_2dquark.Code5: Current desktop isn't targeted by IMSettings.

网上搜到的方法大多是更改gnome的如下设置

gsettings set org.gnome.settings-daemon.plugins.keyboard active false

但会显示如下错误

No such schema “org.gnome.settings-daemon.plugins.keyboard”

这个BUG的详情可以看

https://bugzilla.redhat.com/show_bug.cgi?id=1528892

目前这一问题可以通过下面这种方法解决

$ sudo mv /etc/xdg/autostart/org.gnome.SettingsDaemon.Keyboard.desktop /etc/xdg/autostart/org.gnome.SettingsDaemon.Keyboard.desktop.backup
$ im-chooser
$ sudo mv /etc/xdg/autostart/org.gnome.SettingsDaemon.Keyboard.desktop.backup /etc/xdg/autostart/org.gnome.SettingsDaemon.Keyboard.desktop
$ gsettings set org.gnome.desktop.wm.keybindings switch-input-source "['']"
$ gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward  "['']"

by lainme (lainme@undisclosed.example.com) at February 06, 2020 06:59 PM

February 04, 2020

farseerfc

ZFS 分层架构设计

2020年2月9日更新过

ZFS 在设计之初源自于 Sun 内部多次重写 UFS 的尝试,背负了重构 Solaris 诸多内核子系统的重任,从而不同于 Linux 的文件系统只负责文件系统的功能而把其余功能(比如内存脏页管理, IO调度)交给内核更底层的子系统, ZFS 的整体设计更层次化并更独立,很多部分可能和 Linux/FreeBSD 内核已有的子系统有功能重叠。

似乎很多关于 ZFS 的视频演讲和幻灯片有讲到子系统架构,但是找了半天也没找到网上关于这个的说明文档。 于是写下这篇笔记试图从 ZFS 的早期开发历程开始,记录一下 ZFS 分层架构中各个子系统之间的分工。 也有几段 OpenZFS Summit 视频佐以记录那段历史。

早期架构

早期 ZFS 在开发时大体可以分为上下三层,分别是 ZPL, DMU 和 SPA ,这三层分别由三组人负责。

最初在 Sun 内部带领 ZFS 开发的是 Jeff Bonwick ,他首先有了对 ZFS 整体架构的构思,然后游说 Sun 高层,亲自组建起了 ZFS 开发团队,招募了当时刚从大学毕业的 Matt Ahrens 。作为和 Sun 高层谈妥的条件, Jeff 也必须负责 Solaris 整体的 Storage & Filesystem Team ,于是他又从 Solaris 的 Storage Team 抽调了 UFS 部分的负责人 Mark Shellenbaum 和 Mark Maybee 来开发 ZFS 。而如今昔日升阳已然日落, Jeff 成立了独立公司继续开拓服务器存储领域, Matt 是 OpenZFS 项目的负责人,两位 Mark 则留在了 Sun/Oracle 成为了 Oracle ZFS 分支的维护者。

The Birth of ZFS by Jeff Bonwick
Story Time (Q&A) with Matt and Jeff
ZFS First Mount by Mark Shellenbaum
ZFS past & future by Mark Maybee

在开发早期,作为分工, Jeff 负责 ZFS 设计中最底层的 SPA ,提供多个存储设备组成的存储池抽象; Matt 负责 ZFS 设计中最至关重要的 DMU 引擎,在块设备基础上提供具有事务语义的对象存储; 而两位 Mark 负责 ZFS 设计中直接面向用户的 ZPL ,在 DMU 基础上提供完整 POSIX 文件系统语义。 ZFS 设计中这最初的分工也体现在了 ZFS 现在子系统分层的架构上,继续影响(增强或者限制) ZFS 今后发展的方向。

子系统整体架构

首先 ZFS 整体架构如下图,其中圆圈是 ZFS 给内核层的外部接口,方框是 ZFS 内部子系统( 我给方框的子系统加上了超链接):

ZFS_Layer_Architecture clusterTOL TOL clusterSPA SPA Filesystem API Filesystem API VFS VFS Filesystem API->VFS Block device API Block device API /dev/zvol/... /dev/zvol/... Block device API->/dev/zvol/... ZFS Management API (libzfs) ZFS Management API (libzfs) /dev/zfs ioctl /dev/zfs ioctl ZFS Management API (libzfs)->/dev/zfs ioctl NFS/Samba API (libshare) NFS/Samba API (libshare) NFS/CIFS vop_rwlock NFS/CIFS vop_rwlock NFS/Samba API (libshare)->NFS/CIFS vop_rwlock VFS->NFS/CIFS vop_rwlock ZPL ZPL VFS->ZPL ZVOL ZVOL /dev/zvol/...->ZVOL DSL DSL /dev/zfs ioctl->DSL VDEV VDEV /dev/zfs ioctl->VDEV DMU DMU NFS/CIFS vop_rwlock->DMU ZAP ZAP ZPL->ZAP ZPL->DMU ZIL ZIL ZPL->ZIL ZVOL->DMU DSL->ZAP DSL->DMU ZAP->DMU ARC ARC DMU->ARC MetaSlab MetaSlab DMU->MetaSlab ZIO ZIO ARC->ZIO L2ARC L2ARC ARC->L2ARC ZIL->ZIO SLOG SLOG ZIL->SLOG ZIO->MetaSlab ZIO->VDEV L2ARC->ZIO L2ARC->VDEV SLOG->VDEV MetaSlab->VDEV physical storage devices physical storage devices VDEV->physical storage devices

接下来从底层往上介绍一下各个子系统的全称和职能。

SPA

Storage Pool Allocator

从内核提供的多个块设备中抽象出存储池的子系统。 SPA 进一步分为 ZIO 和 VDEV 两大部分和其余一些小的子系统。

SPA 对 DMU 提供的接口不同于传统的块设备接口,完全利用了 CoW 文件系统对写入位置不敏感的特点。 传统的块设备接口通常是写入时指定一个写入地址,把缓冲区写到磁盘指定的位置上,而 DMU 可以让 SPA 做两种操作:

  1. write , DMU 交给 SPA 一个数据块的内存指针, SPA 负责找设备写入这个数据块,然后返回给 DMU 一个 block pointer 。
  2. read ,DMU 交给 SPA 一个 block pointer ,SPA 读取设备并返回给 DMU 完整的数据块。

也就是说,在 DMU 让 SPA 写数据块时, DMU 还不知道 SPA 会写入的地方,这完全由 SPA 判断, 这一点通常被称为 Write Anywhere ,在别的 CoW 文件系统比如 Btrfs 和 WAFL 中也有这个特点。 反过来 SPA 想要对一个数据块操作时,也完全不清楚这个数据块用于什么目的,属于什么文件或者文件系统结构。

VDEV

Virtual DEVice

VDEV 在 ZFS 中的作用相当于 Linux 内核的 Device Mapper 层或者 FreeBSD GEOM 层,提供 Stripe/Mirror/RAIDZ 之类的多设备存储池管理和抽象。 ZFS 中的 vdev 形成一个树状结构,在树的底层是从内核提供的物理设备, 其上是虚拟的块设备。每个虚拟块设备对上对下都是块设备接口,除了底层的物理设备之外,位于中间层的 vdev 需要负责地址映射、容量转换等计算过程。

除了用于存储数据的 Stripe/Mirror/RAIDZ 之类的 VDEV ,还有一些特殊用途的 VDEV ,包括提供二级缓存的 L2ARC 设备,以及提供 ZIL 高速日志的 SLOG 设备。

ZIO

ZIO Pipeline by George Wilson

ZFS I/O

作用相当于内核的 IO scheduler 和 pagecache write back 机制。 OpenZFS Summit 有个演讲整理了 ZIO 流水线的工作原理。 ZIO 内部使用流水线和事件驱动机制,避免让上层的 ZFS 线程阻塞等待在 IO 操作上。 ZIO 把一个上层的写请求转换成多个写操作,负责把这些写操作合并到 transaction group 提交事务组。 ZIO 也负责将读写请求按同步还是异步分成不同的读写优先级并实施优先级调度, 在 OpenZFS 项目 wiki 页有一篇描述 ZIO 调度 的细节。

除了调度之外, ZIO 层还负责在读写流水线中拆解和拼装数据块。上层 DMU 交给 SPA 的数据块有固定大小, 目前默认是 128KiB ,pool 整体的参数可以调整块大小在 4KiB 到 8MiB 之间。ZIO 拿到整块大小的数据块之后,在流水线中可以对数据块做诸如以下操作:

  1. 用压缩算法,压缩/解压数据块。
  2. 查询 dedup table ,对数据块去重。
  3. 加密/解密数据块。
  4. 计算数据块的校验和。
  5. 如果底层分配器不能分配完整的 128KiB (或 zpool 设置的大小),那么尝试分配多个小块,然后用多个 512B 的指针间接块连起多个小块的,拼装成一个虚拟的大块,这个机制叫 gang block 。通常 ZFS 中用到 gang block 时,整个存储池处于极度空间不足的情况,由 gang block 造成严重性能下降,而 gang block 的意义在于在空间接近要满的时候也能 CoW 写入一些元数据,释放亟需的存储空间。

可见经过 ZIO 流水线之后,数据块不再是统一大小,这使得 ZFS 用在 4K 对齐的磁盘或者 SSD 上有了一些新的挑战。

MetaSlab

MetaSlab Allocation Performance by Paul Dagnelie

MetaSlab 是 ZFS 的块分配器。 VDEV 把存储设备抽象成存储池之后, MetaSlab 负责实际从存储设备上分配数据块,跟踪记录可用空间和已用空间。

叫 MetaSlab 这个名字是因为 Jeff 最初同时也给 Solaris 内核写过 slab 分配器 ,一开始设计 SPA 的时候 Jeff 想在 SPA 中也利用 Solaris 的 slab 分配器对磁盘空间使用类似的分配算法。后来 MetaSlab 逐渐不再使用 slab 算法,只有名字留了下来。

MetaSlab 的结构很接近于 FreeBSD UFS 的 cylinder group ,或者 ext2/3/4 的 block group ,或者 xfs 的 allocation group ,目的都是让存储分配策略「局域化」, 或者说让近期分配的数据块的物理地址比较接近。在存储设备上创建 zpool 的时候,首先会尽量在存储设备上分配 200 个左右的 MetaSlab ,随后给 zpool 增加设备的话使用接近的 MetaSlab 大小。每个 MetaSlab 是连续的一整块空间,在 MetaSlab 内对数据块空间做分配和释放。磁盘中存储的 MetaSlab 的分配情况是按需载入内存的,系统 import zpool 时不需要载入所有 MetaSlab 到内存,而只需加载一小部分。当前载入内存的 MetaSlab 剩余空间告急时,会载入别的 MetaSlab 尝试分配,而从某个 MetaSlab 释放空间不需要载入 MetaSlab 。

OpenZFS Summit 也有一个对 MetaSlab 分配器性能的介绍,可以看到很多分配器内的细节。

ARC

ELI5: ZFS Caching Explain Like I'm 5: How the ZFS Adaptive Replacement Cache works

Adaptive Replacement Cache

ARC 的作用相当于 Linux/Solaris/FreeBSD 中传统的 page/buffer cache 。 和传统 pagecache 使用 LRU (Least Recently Used) 之类的算法剔除缓存页不同, ARC 算法试图在 LRU 和 LFU(Least Frequently Used) 之间寻找平衡,从而复制大文件之类的线性大量 IO 操作不至于让缓存失效率猛增。最近 FOSDEM 2019 有一个介绍 ZFS ARC 工作原理的视频。

不过 ZFS 采用它自有的 ARC 一个显著缺点在于,不能和内核已有的 pagecache 机制相互配合,尤其在 系统内存压力很大的情况下,内核与 ZFS 无关的其余部分可能难以通知 ARC 释放内存。所以 ARC 是 ZFS 消耗内存的大户之一(另一个是可选的 dedup table),也是 ZFS 性能调优 的重中之重。

当然, ZFS 采用 ARC 不依赖于内核已有的 pagecache 机制除了 LFU 平衡的好处之外,也有别的有利的一面。 系统中多次读取因 snapshot 或者 dedup 而共享的数据块的话,在 ZFS 的 ARC 机制下,同样的 block pointer 只会被缓存一次;而传统的 pagecache 因为基于 inode 判断是否有共享, 所以即使这些文件有共享页面(比如 btrfs/xfs 的 reflink 形成的),也会多次读入内存。 Linux 的 btrfs 和 xfs 在 VFS 层面有共用的 reflink 机制之后,正在努力着手改善这种局面,而 ZFS 因为 ARC 所以从最初就避免了这种浪费。

和很多传言所说的不同, ARC 的内存压力问题不仅在 Linux 内核会有,在 FreeBSD 和 Solaris/Illumos 上也是同样,这个在 ZFS First Mount by Mark Shellenbaum 的问答环节 16:37 左右有提到 。其中 Mark Shellenbaum 提到 Matt 觉得让 ARC 并入现有 pagecache 子系统的工作量太大,难以实现。

因为 ARC 工作在 ZIO 上层,所以 ARC 中缓存的数据是经过 ZIO 从存储设备中读取出来之后解压、解密等处理之后的,原始的数据。最近 ZFS 的版本有支持一种新特性叫 Compressed ARC ,打破 ARC 和 VDEV 中间 ZIO 的壁垒,把压缩的数据直接缓存在 ARC 中。这么做是因为压缩解压很快的情况下,压缩的 ARC 能节省不少内存,让更多数据保留在 ARC 中从而提升缓存利用率,并且在有 L2ARC 的情况下也能增加 L2ARC 能存储的缓存。

L2ARC

Level 2 Adaptive Replacement Cache

这是用 ARC 算法实现的二级缓存,保存于高速存储设备上。常见用法是给 ZFS pool 配置一块 SSD 作为 L2ARC 高速缓存,减轻内存 ARC 的负担并增加缓存命中率。

SLOG

Separate intent LOG

SLOG 是额外的日志记录设备。 SLOG 之于 ZIL 有点像 L2ARC 之余 ARC , L2ARC 是把内存中的 ARC 放入额外的高速存储设备,而 SLOG 是把原本和别的数据块存储在一起的 ZIL 放到额外的高速存储设备。

TOL

Transactional Object Layer

这一部分子系统在数据块的基础上提供一个事务性的对象语义层,这里事务性是指, 对对象的修改处于明确的状态,不会因为突然断电之类的原因导致状态不一致。TOL 中最主要的部分是 DMU 层。

DMU

Data Management Unit

在块的基础上提供「对象(object)」的抽象。每个「对象」可以是一个文件,或者是别的 ZFS 内部需要记录的东西。

DMU 这个名字最初是 Jeff 想类比于操作系统中内存管理的 MMU(Memory Management Unit), Jeff 希望 ZFS 中增加和删除文件就像内存分配一样简单,增加和移除块设备就像增加内存一样简单, 由 DMU 负责从存储池中分配和释放数据块,对上提供事务性语义,管理员不需要管理文件存储在什么存储设备上。 这里事务性语义指对文件的修改要么完全成功,要么完全失败,不会处于中间状态,这靠 DMU 的 CoW 语义实现。

DMU 实现了对象级别的 CoW 语义,从而任何经过了 DMU 做读写的子系统都具有了 CoW 的特征, 这不仅包括文件、文件夹这些 ZPL 层需要的东西,也包括文件系统内部用的 spacemap 之类的设施。 相反,不经过 DMU 的子系统则可能没法保证事务语义。这里一个特例是 ZIL ,一定程度上绕过了 DMU 直接写日志。说一定程度是因为 ZIL 仍然靠 DMU 来扩展长度,当一个块写满日志之后需要等 DMU 分配一个新块,在分配好的块内写日志则不需要经过 DMU 。所有经过 DMU 子系统的对象都有 CoW 语义,也意味着 ZFS 中不能对某些文件可选地关闭 CoW ,不能提供数据库应用的 direct IO 之类的接口。

「对象(object)」抽象是 DMU 最重要的抽象,一个对象的大小可变,占用一个或者多个数据块( 默认一个数据块 128KiB )。上面提到 SPA 的时候也讲了 DMU 和 SPA 之间不同于普通块设备抽象的接口,这使得 DMU 按整块的大小分配空间。当对象使用多个数据块存储时, DMU 提供间接块(indirect block)来引用这些数据块。 间接块很像传统 Unix 文件系统(Solaris UFS 或者 Linux ext2)中的一级二级三级间接块, 一个间接块存储很多块指针(block pointer),多个间接块形成树状结构,最终一个块指针可以引用到一个对象。 更现代的文件系统比如 ext4/xfs/btrfs/ntfs 提供了 extent 抽象,可以指向一个连续范围的存储块, 而 ZFS 不使用类似 extent 的抽象。DMU 采用间接块而不是 extent ,使得 ZFS 的空间分配更趋向碎片化,为了避免碎片化造成的性能影响,需要尽量延迟写入使得一次写入能在磁盘上 尽量连续,这里 ARC 提供的缓存和 ZIO 提供的流水线对延迟写入避免碎片有至关重要的帮助。

有了「对象(object)」的抽象之后, DMU 进一步实现了「对象集(objectset)」的抽象, 一个对象集中保存一系列按顺序编号的 dnode ( ZFS 中类似 inode 的数据结构),每个 dnode 有足够空间 指向一个对象的最多三个块指针,如果对象需要更多数据块可以使用间接块,如果对象很小也可以直接压缩进 dnode 。随后 DSL 又进一步用对象集来实现数据集(dataset)抽象,提供比如文件系统(filesystem )、快照(snapshot)、克隆(clone)之类的抽象。一个对象集中的对象可以通过 dnode 编号相互引用, 就像普通文件系统的硬链接引用 inode 编号那样。

上面也提到因为 SPA 和 DMU 分离, SPA 完全不知道数据块用于什么目的;这一点其实对 DMU 也是类似, DMU 虽然能从某个对象找到它所占用的数据块,但是 DMU 完全不知道这个对象在文件系统或者存储池中是 用来存储什么的。当 DMU 读取数据遇到坏块(block pointer 中的校验和与 block pointer 指向的数据块内容不一致)时,它知道这个数据块在哪儿(具体哪个设备上的哪个地址), 但是不知道这个数据块是否和别的对象共享,不知道搬动这个数据块的影响,也没法从对象反推出文件系统路径, (除了明显开销很高地扫一遍整个存储池)。所以 DMU 在遇到读取错误(普通的读操作或者 scrub/resilver 操作中)时,只能选择在同样的地址,原地写入数据块的备份(如果能找到或者推算出备份的话)。

或许有人会疑惑,既然从 SPA 无法根据数据地址反推出对象,在 DMU 也无法根据对象反推出文件,那么 zfs 在遇到数据损坏时是如何在诊断信息中给出损坏的文件路径的呢?这其实基于 ZPL 的一个黑魔法: 在 dnode 记录父级 dnode 的编号 。因为是个黑魔法,这个记录不总是对的,所以只能用于诊断信息,不能基于这个实现别的文件系统功能。

ZAP

ZFS Attribute Processor

在 DMU 提供的「对象」抽象基础上提供紧凑的 name/value 映射存储, 从而文件夹内容列表、文件扩展属性之类的都是基于 ZAP 来存。 ZAP 在内部分为两种存储表达: microZAP 和 fatZAP 。

一个 microZAP 占用一整块数据块,能存 name 长度小于 50 字符并且 value 是 uint64_t 的表项, 每个表项 64 字节。 fatZAP 则是个树状结构,能存更多更复杂的东西。fatZAP 是个 on disk 的散利表,指针表中是 64bit 对 name 的 hash ,指向单链表的子节点列表,子节点中的 value 可以是任意类型的数据(不光是 uint64_t )。

可见 microZAP 非常适合表述一个普通大小的文件夹里面包含到很多普通文件 inode (ZFS 是 dnode)的引用。 fatZAP 则不光可以用于任意大小的文件夹,还可以表达 ZFS 的配置属性之类的东西,非常灵活。

ZFS First Mount by Mark Shellenbaum 的8:48左右 提到,最初 ZPL 中关于文件的所有属性(包括访问时间、权限、大小之类所有文件都有的)都是基于 ZAP 来存,也就是说每个文件都有个 ZAP ,其中有叫做 size 呀 owner 之类的键值对,就像是个 JSON 对象那样,这让 ZPL 一开始很容易设计原型并且扩展。然后文件夹内容列表有另一种数据结构 ZDS(ZFS Directory Service),后来常见的文件属性在 ZPL 有了专用的紧凑数据结构,而 ZDS 则渐渐融入了 ZAP 。 这些变化详见下面 ZPL 。

DSL

Dataset and Snapshot Layer

数据集和快照层,负责创建和管理快照、克隆等数据集类型,跟踪它们的写入大小,最终删除它们。 由于 DMU 层面已经负责了对象的写时复制语义和对象集的概念,所以 DSL 层面不需要直接接触写文件之类来自 ZPL 的请求,无论有没有快照对 DMU 层面一样采用写时复制的方式修改文件数据。 不过在删除快照和克隆之类的时候,则需要 DSL 参与计算没有和别的数据集共享的数据块并且删除它们。

DSL 管理数据集时,也负责管理数据集上附加的属性。ZFS 每个数据集有个属性列表,这些用 ZAP 存储, DSL 则需要根据数据集的上下级关系,计算出继承的属性,最终指导 ZIO 层面的读写行为。

除了管理数据集, DSL 层面也提供了 zfs 中 send/receive 的能力。 ZFS 在 send 时从 DSL 层找到快照引用到的所有数据块,把它们直接发往管道,在 receive 端则直接接收数据块并重组数据块指针。 因为 DSL 提供的 send/receive 工作在 DMU 之上,所以在 DSL 看到的数据块是 DMU 的数据块,下层 SPA 完成的数据压缩、加密、去重等工作,对 DMU 层完全透明。所以在最初的 send/receive 实现中,假如数据块已经压缩,需要在 send 端经过 SPA 解压,再 receive 端则重新压缩。最近 ZFS 的 send/receive 逐渐打破 DMU 与 SPA 的壁垒,支持了直接发送已压缩或加密的数据块的能力。

ZIL

ZFS Intent Log

记录两次完整事务语义提交之间的日志,用来加速实现 fsync 之类的文件事务语义。

原本 CoW 的文件系统不需要日志结构来保证文件系统结构的一致性,在 DMU 保证了对象级别事务语义的前提下,每次完整的 transaction group commit 都保证了文件系统一致性,挂载时也直接找到最后一个 transaction group 从它开始挂载即可。 不过在 ZFS 中,做一次完整的 transaction group commit 是个比较耗时的操作, 在写入文件的数据块之后,还需要更新整个 object set ,然后更新 meta-object set ,最后更新 uberblock ,为了满足事务语义这些操作没法并行完成,所以整个 pool 提交一次需要等待好几次磁盘写操作返回,短则一两秒,长则几分钟, 如果事务中有要删除快照等非常耗时的操作可能还要等更久,在此期间提交的事务没法保证一致。

对上层应用程序而言,通常使用 fsync 或者 fdatasync 之类的系统调用,确保文件内容本身的事务一致性。 如果要让每次 fsync/fdatasync 等待整个 transaction group commit 完成,那会严重拖慢很多应用程序,而如果它们不等待直接返回,则在突发断电时没有保证一致性。 从而 ZFS 有了 ZIL ,记录两次 transaction group 的 commit 之间发生的 fsync ,突然断电后下次 import zpool 时首先找到最近一次 transaction group ,在它基础上重放 ZIL 中记录的写请求和 fsync 请求,从而满足 fsync API 要求的事务语义。

显然对 ZIL 的写操作需要绕过 DMU 直接写入数据块,所以 ZIL 本身是以日志系统的方式组织的,每次写 ZIL 都是在已经分配的 ZIL 块的末尾添加数据,分配新的 ZIL 块仍然需要经过 DMU 的空间分配。

传统日志型文件系统中对 data 开启日志支持会造成每次文件系统写入操作需要写两次到设备上, 一次写入日志,再一次覆盖文件系统内容;在 ZIL 实现中则不需要重复写两次, DMU 让 SPA 写入数据之后 ZIL 可以直接记录新数据块的 block pointer ,所以使用 ZIL 不会导致传统日志型文件系统中双倍写入放大的问题。

ZVOL

ZFS VOLume

有点像 loopback block device ,暴露一个块设备的接口,其上可以创建别的 FS 。对 ZFS 而言实现 ZVOL 的意义在于它是比文件更简单的接口,所以在实现完整 ZPL 之前,一开始就先实现了 ZVOL ,而且 早期 Solaris 没有 thin provisioning storage pool 的时候可以用 ZVOL 模拟很大的块设备,当时 Solaris 的 UFS 团队用它来测试 UFS 对 TB 级存储的支持情况

因为 ZVOL 基于 DMU 上层,所以 DMU 所有的文件系统功能,比如 snapshot / dedup / compression 都可以用在 ZVOL 上,从而让 ZVOL 上层的传统文件系统也具有类似的功能。并且 ZVOL 也具有了 ARC 缓存的能力,和 dedup 结合之下,非常适合于在一个宿主机 ZFS 上提供对虚拟机文件系统镜像的存储,可以节省不少存储空间和内存占用开销。

ZPL

ZFS Posix Layer

提供符合 POSIX 文件系统语义的抽象,也就是包括文件、目录、软链接、套接字这些抽象以及 inode 访问时间、权限那些抽象,ZPL 是 ZFS 中对一个普通 FS 而言用户直接接触的部分。 ZPL 可以说是 ZFS 最复杂的子系统,也是 ZFS 作为一个文件系统而言最关键的部分。

ZPL 的实现中直接使用了 ZAP 和 DMU 提供的抽象,比如每个 ZPL 文件用一个 DMU 对象表达,每个 ZPL 目录用一个 ZAP 对象表达,然后 DMU 对象集对应到 ZPL 下的一个文件系统。 也就是说 ZPL 负责把操作系统 VFS 抽象层的那些文件系统操作接口,翻译映射到基于 DMU 和 ZAP 的抽象上。传统 Unix 中的管道、套接字、软链接之类的没有什么数据内容的东西则在 ZPL 直接用 dnode 实现出来。 ZPL 也需要进一步实现文件权限、所有者、访问日期、扩展属性之类杂七杂八的文件系统功能。

2020年2月9日添加

继续上述 ZAP 格式变化的讨论,在 ZPL 抛弃早期用 ZAP 的设计之后, ZPL 中 znode (ZPL 扩展的 dnode) 保存文件属性的机制成为了一个小的子系统,叫 ZFS System Attributes 。 SA 的设计照顾了旧版 ZPL znode 格式兼容问题,有新旧两代格式。旧版 znode 格式是固定偏移位置存取属性的 SA ,因此透过预先注册好的描述旧版 znode 格式的固定映射表, SA 依然能用同样的代码路径存取旧版的 znode 。而后来 灵活的新设计下的 SA 更有意思 ,ZFS 认识到,大部分 znode 的属性都可以用有限的几种属性集来表达, 比如普通文件有一组类似的属性(权限、所有者之类的), zvol 有另一组(明显 zvol 不需要很多 ZPL 文件的属性),整个 ZFS dataset 可以「注册」几种属性布局,然后让每个 znode 引用其中一种布局, 这样 znode 保存的属性仍然是可以任意变化的,又不需要在每个 znode 中都记录所有属性的名字。 SA 的出现提升了 ZPL 的可扩展性。 ZPL 为了应付不同的操作系统之间文件系统 API 的差异,可以使用 SA 在 znode 之中加入针对不同操作系统和应用场景的属性。例如,在支持 NFSv4 ACL 的操作系统上,ZFS 既可以用现有方式把 DACL ACEs 放在独立于文件对象的单独对象中,也可以把 DACL ACEs 放在 SA 内。

在 ZFS First Mount by Mark Shellenbaum 中介绍了很多在最初实现 ZPL 过程中的坎坷, ZPL 的困难之处在于需要兼容现有应用程序对传统文件系统 API 的使用方式,所以他们需要大量兼容性测试。视频中讲到非常有意思的一件事是, ZFS 在设计时不想重复 Solaris UFS 设计中的很多缺陷,于是实现 VFS API 时有诸多取舍和再设计。 其中他们遇到了 VOP_RWLOCK ,这个是 UFS 提供的文件级别读写锁。对一些应用尤其是 NFS 而言,文件读写锁能保证应用层的一致性,而对另一些应用比如数据库而言, 文件锁的粒度太大造成了性能问题。在设计 ZPL 的时候他们不想在 ZFS 中提供 VOP_RWLOCK ,这让 NFS 开发者们很难办(要记得 NFS 也是 Solaris 对 Unix 世界贡献的一大发明)。 最终 ZFS 把 DMU 的内部细节也暴露给了 NFS ,让 NFS 基于 DMU 的对象创建时间( TXG id )而不是文件锁来保证 NFS 的一致性。结果是现在 ZFS 中也有照顾 NFS 的代码,后来也加入了 Samba/CIFS 的支持,从而在 ZFS 上设置 NFS export 时是通过 ZFS 的机制而非系统原生的 NFS export 机制。

by farseerfc at February 04, 2020 07:59 AM

January 28, 2020

Justin Wong

把 CUPS 扔进 docker 里

开头废话

看了一下我竟然整整 3 年没写过 blog 了! 有生之年能更新一下也是挺难得的。

其实我这几年没少写东西,只是和工作重合度太高,都发在内网论坛里了。

以下是正文。


故事是这样的,某年某月滚了一把 Arch,然后打印机就不听话了,会在打印机开始打印的瞬间任务失败…… 由于打印机不常用,也不知道具体是哪次滚动,哪个库的更新引起的问题,很长时间都没搞定。

于是弃疗,把 cups 扔进 docker 里,固定一个版本解决。

January 28, 2020 12:00 AM

January 27, 2020

百合仙子

自制大上 Paperlike HD「驱动」

本文来自依云's Blog,转载请注明。

大上 Paperlike HD 使用有一段时间了,然而有一点我对其非常不满:它需要以 root 权限运行一个图形界面的程序。具体麻烦的地方是:

  • 图形界面的程序不方便使用 systemd 管理,那个窗口我得找个地方安放,并且在登出图形界面或者 Xorg 出问题时会随之关闭
  • 即使持续运行此程序,当几秒内不使用键盘或者鼠标的时候屏幕就会休眠。这导致我无法将此屏幕用于关注程序日志或者聊天工具的新消息。
  • 它持续不断地执行多个线程的任务(读取键盘事件、读取鼠标事件、通过 ioctl 与设备通讯),耗费了不少 CPU
  • 在屏幕尚未连接时,它的运行会导致内核不断输出日志「drm_dp_i2c_do_msg: 2 callbacks suppressed」

我曾多次想自己实现一个符合自己使用习惯的方案。

首先当然是 strace 上去啦。这会得到许多类似这样的消息:

ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805b70) = 1
nanosleep({tv_sec=0, tv_nsec=100000000}, NULL) = 0
ioctl(9</dev/i2c-1<char 89:1>>, _IOC(_IOC_NONE, 0x7, 0x7, 0), 0x7f47d8805be0) = 1
nanosleep({tv_sec=0, tv_nsec=200000000},  <unfinished ...>

可以看到它在对 /dev/i2c-1 这个文件进行操作,但是具体内容是个指针,strace 看不到。

我尝试过使用大名鼎鼎的 IDA 的免费版本来分析其具体行为。但我对 IDA 并不熟悉,并且 IDA 只支持 Intel 语法的汇编,而我见的 AT&T 语法的比较多,Intel 的很多表示法我不太能看懂。

后来根据 ioctl 的请求参数找到这个文档,里边有这些 i2c 消息的结构体定义。于是想着先把 ioctl 的数据弄出来看看。一开始尝试用 gdb 去看那个地址的数据,但想到数据是变动的,再加上 gdb 查看太累了,就想起了通过 LD_PRELOAD 去 hook ioctl。

所以又要写 C 了?并没有呢!C 写起来那么不舒服,还是用 Rust 吧~然后搜了一下,还真有现成的用于写 LD_PRELOAD 库的 crate,比如我用的 redhook。不用自己去 dlopen,不用在各处写很多错误处理代码,很容易就写好了。代码链接

拿到了 ioctl 里用的消息,我不用理会它具体是什么意思,也没办法去猜测,自然是把它按大上提供的程序那个样子给发过去了。于是又一个 Rust 程序出来了。

一开始写的时候不小心往 unsafe 代码块里传了个悬空的指针,导致程序不工作,调试了好久,甚至我都把完整的整个流程给复刻了一遍。这要是用 C 写文本解析的逻辑可头疼了,不过 Rust 写起来就跟 Python 差不多的了~

至于那个 bug,是 Rust 语句中的临时对象(此例中是包含一个对象的数组)会在语句结束之后就释放导致的。有点坑,但也没什么好的办法。

程序运行起来之后就会保持 Paperlike HD 显示器可用,不会报错让装驱动,也不会过几秒就休眠了。我大幅降低了消息发送的频率(由差不多每秒三次改成了三秒才一次),再加上不需要读取键鼠输入,所以 CPU 占用也会大幅减少。另外内核也不会再打印「drm_dp_i2c_do_msg: 2 callbacks suppressed」的消息了,大概是因为消息频率降低了?重新连接显示器之后,和大上原版程序一样有概率出现显示器亮蓝灯、屏幕不工作的情况。拔插一下电源可解。

当然啦,如果有人要用这个程序的话,记得先确认一下你的 i2c 设备文件路径(去 lsof 大上原版程序就行)。另外,使用此程序后果自负,由此造成的任何设备损坏或者其它损失,我都不负责任的哦~

by 依云 at January 27, 2020 12:37 PM

January 23, 2020

Leo Shen

Fix clipboard permission on Android 10

This guide is updated on 2020-03-29 due to update of the upstream module. The lazy way no longer works. You'll need to add the entry manually. Android 10 has some great improvements on user privacy, but it also brings problems. One issue I faced is that background applications can no longer access the clipboard 1. This directly breaks the clipboard share functionality of KDE Connect, and that's annoying.

January 23, 2020 12:05 PM

January 22, 2020

中文社区新闻

rsync 兼容性

我们的 rsync 包一直以来通过内嵌 zlib 的方式提供和老式 --compress 参数的兼容,维持对 3.1.0 以前版本的兼容性。版本 3.1.1 是2014年6月22日发布的,现在主流发行版应该都已经提供了。

所以我们决定去掉内嵌的依赖库,发布一个使用系统 zlib 库的新版本。这也能修复迄今为止的和未来的安全问题。如果你运行 rsync 3.1.3-3 遇到报错,请去指责那些还在使用老版本的系统。

by farseerfc at January 22, 2020 03:28 AM

January 10, 2020

Alynx Zhou

不要拿愚蠢的广告来污染我的邮箱

一个多月没更新了,这次要写的不是什么技术问题,而是技术的附属问题。

January 10, 2020 10:35 AM

让你们的主题商店离我远点!

我不觉得我是个刻薄的人,但对于一些实在令我讨厌的人,不把他们的名字挂出来就是我最后的怜悯。

January 10, 2020 10:35 AM

蜗牛星际安装 Arch Linux 作为 NAS

需求

以前我一直使用树莓派 + 移动硬盘做 Samba 服务器,好处是完全静音,功耗很低,但是树莓派 2 的网络性能差强人意,并且 USB 2.0 的速度也赶不上移动硬盘的速度,看电影什么的只是能看,要花好长时间缓冲,并且我一直开着,对树莓派的电源也不是很放心。特别是我还拆下来一块笔记本上的 1TB 机械硬盘,完全没法装在树莓派上。

January 10, 2020 10:35 AM

六月,如梦一般的日子

One day I wake up,finding that I am on one of the most crazy trips in my life.It has been into my dream for many times,but now it comes into reality,which becomes the best birthday present.

January 10, 2020 10:35 AM

January 05, 2020

farseerfc

和萌狼交换问题

很抱歉萌狼很早就提过交换问题的事,被我一直咕咕了许久。 拖延症晚期有药么

我的提问和萌狼的回答

可以去萌狼的博客上看呀

Q1:除了博客的「关于」页面以外,还愿意再向咱介绍一下自己嘛?

介绍自己啊。 写了删删了写,不知道该介绍点啥 就说点自己的兴趣?

喜欢自由开源软件,喜欢 Arch Linux 。喜欢这些倒不是出于 RMS 和 FSF 那样道义上的原因, 我觉得商业软件公司要赚钱吃饭也是无可厚非的。

喜欢自由软件是因为,当我需要知道它到底怎么工作的时候,有可能去挖代码,必要的话能去改代码。 当然我一个人肯定不能读所有在用的软件,但是我知道我有读和修改代码的权利的话, 那么我认识的朋友们也同样有这样的权利,我不认识的广大社区有千千万万的人也同样有这样的权利, 从而我相信当我遇到问题的时候不至于卡在某些人某些公司某些集体的决策上而无法解决。

基于这个理由,我对开源社区也同样有公开全部细节的期待。我喜欢 Arch Linux 因为即便它的内部决策只是一小波人,但是导致决策的讨论以及决策的执行方式全是公开的,可以在网上翻阅, 可以追根溯源,这让我有种安心感。就像我不喜欢 Manjaro 的一点是它有太多细节是翻阅不到的, 虽然它也是开源社区,但是打包细节翻阅不到,包列表翻阅不到,决策的制定和执行的过程也翻阅不到, 通常就只是在他们的论坛上发个通知了事,这我很不喜欢。

除了喜欢自由开源软件之外,可能我在网上比较有特点的地方是用繁体字了吧, 也曾经年幼时在水木社区和别人因为这个吵过嘴,也在 知乎上写过篇「在知乎用繁体字是怎样一种体验」 。 致力于在我存在的地方为繁体字爱好者们提供一个安逸的环境,不过好像最近也不见很多反对的声音了。

除了网上之外,现实中的自己嘛,特点可能算是不知道自己属于哪儿了……一个漂泊的人。 小时候8岁前在陕西长大,把自己当作陕西人,但是身边的邻里街坊们却以河南人和江浙人居多。 厂办环境,好几个大型重工都从江浙搬到了陕西秦川一带,加上国共内战的时候河南黄河缺口造成的难民慌西逃, 构成了当时厂办的主要人口拿着城市户口,反而是当地的陕西人都是农民户口, 于是和厂办子弟们形成了鲜明的隔阂。我对社会主义,对苏式厂办,对整个国家结构的理解大概也是从那儿来的。 跟着邻里们学会了河南话,在家里说普通话,从老一辈们身上又学会了江浙的语调。 都说一个厂办是一个社会的缩影,那时候的环境可能算聚集了全国东南西北的样子吧。 8、9岁左右随父母到了上海,因为不会说上海话受同学们排挤,倒也不是很在意,渐渐和同学们学起了上海话, 可能还参杂点爷爷奶奶的江苏方言。十多年后考入大学,五湖四海的同学都有,就不算是在上海了。 大学毕业来了日本,一晃又是7年过去。至此我大概比起同龄人接触到更多全国各地的人, 也分不清自己的归属地了。但有一条,我知道自己是个中国人,为自己是个中国人自豪,觉得虽在他乡, 该为中国做点自己的贡献。

Q2:现在这个名字是怎么想到的呢?

farseerfc 这个名字嘛,来自 firechild 这个更早的网名,和魔兽争霸里面 farseer 这个英雄。 farseer 本算是 Anglish ,以日耳曼语系的构词法再造的英语词,对应拉丁构词法的话 far = tele , seer = visioner ,于是 farseer 也就是 tele-visioner ,看得远的人,电视一词 television 的原本的词干的衍生词。 不过说为什么选 farseer 这个名字,更多是为了符合 fc 这个缩写,而 fc 来自 firechild 这个词。 再深挖黑历史也不再有什么意义了, farseerfc 作为网名只是一直以来的习惯吧。

Q3:觉得咱俩之间最令汝印象深刻的时候是什么?

近期来看,印象最深刻的可能算是起草 Arch Linux 中文社区交流群指引 吧,看得出萌狼对社区发展的热心和好意。

再往前,印象深刻的时候可能是萌狼用 Pelican 搭博客吧,最初认识萌狼的时候觉得是 MediaWiki 方面的行家,还以为博客也会继续用 MediaWiki 打造,没想到能吃了 Pelican 的安利,外加萌狼写博文的产量着实让人望尘莫及。

然后 ArchWiki 上 Beginner's Guide 被删除之后,萌狼的博客多了一篇为新人们写的入门安装手册, 配有完整截图指引,详尽程度令人感叹。感觉得到萌狼作为一个「过来人」对新人们的照顾。 每次群中闹起争执,老用户们对新人发起调侃的时候,也是萌狼站出来为新人们解围, 帮助有能力的人适应群里的讨论环境。或许最初写交流群指引的时候也是出于这样的良苦用心吧。

Q4:对咱的印象怎么样?

最早来 Arch Linux CN 的时候,似乎萌狼还不叫萌狼?不记得那时候用的名字了。只记得来自 AOSC ,和那边一众谈笑风声,着实令人羡慕,经常跑他们的聚会也令人羡慕。

后来有了萌狼的名字,群里的狼们也渐渐多了起来,一时间都分不清哪个狼是哪个了。 不过萌狼的口癖和说话方式总是在狼群中非常有标志性。

后来似乎发生了好多事情,我不知道的事情,也不敢妄加揣测。萌狼开始变身音游大佬, 群里的别的狼们渐渐也各忙东西。不知道什么原因,萌狼会偶尔退群,想问下前因后果, 又觉得自己不该多管闲事。不过无论萌狼退群多少次,总是在默默关心着社区发展, 关心着新人融入社区的环境。

似乎萌狼加入了 FSF ?玩起了 Parabola ,玩起了 linux-libre 。有能跑起完全自由的发行版的设备, 这一点也非常令人羡慕。似乎有很多设备,但是似乎又很不满于现状。看得出萌狼为了理想放弃了很多东西, 或许大家都是如此吧,也或许只是我多心。

还有就是萌狼用 Gnome ,感觉 AOSC 那边很多人都用 Gnome ,给 Gnome 贡献翻译之类的, 萌狼或许也是其中一员。DE 党争是水群久胜不衰的话题,或许我也有些责任,但是我觉得以发行版角度而言 DE 多样性非常重要,萌狼在社区中的作用也不可或缺。

Q5:在汝用过的 GNU/Linux 发行版之间汝最喜欢的是哪一个,为啥咧?

最喜欢的当然是 Arch Linux 啦,喜欢的理由前面 Q1 算是提到了一些。其实别的发行版的很多特性也很眼馋, 眼馋 Fedora Silverblue 的 A/B 更新机制,眼馋 Fedora 的 SELinux 和诸多企业级特性支援,眼馋 openSUSE 的 OBS 和 btrfs 支持,眼馋 debian 的小巧和细化打包,眼馋 NixOS 的函数式包管理, 眼馋 Gentoo 的可定制性,眼馋 Parabola / GuixSD 的完全自由。

但是总得来说, Arch Linux 提供的基础足够让我折腾系统成自己喜欢的方式,足够顺手, 也在需要软件的时候足够自己打包使用,不需要等待某些远在天边的议会做决策,或许是让我留在 Arch Linux 的原因吧(当然更大原因可能是因为惯性)。发行版之间的技术区别可能并不那么重要, 重要的是该干活的时候能找到干活的人,这一点 Arch Linux 还是有很多人在认真做事情的。 没有繁琐的议会投票表决,没有细碎的打包步骤,用最快的方式把活干了,这在我看来是最重要的。

或许有一天,干活的人没了,或者我想要的特殊特性因为太复杂没人想带头干,而别的发行版有, 那时可能我会换去别的发行版吧。又或许我会自己干,谁知道呢。

比起发行版之争,甚至比起 Linux/Windows/macOS 的桌面系统地位之争,可能日后更关键的是别的平台 比如 Android 在手持设备甚至物联网设备上的兴起导致的 PC 桌面的衰落。虽然这些新设备大多都是跑着 Linux 的内核,但是其上的生态环境不能说像 GNU/Linux 那样自由。这一点上,自由软件该如何发挥优势 争取用户和生态可能是更关键的。

当然这些都于我而言过于遥远,一人之力难挽狂澜……我只希望自己和朋友们所在的自由的土地能保持下去, 或许我也仅能做到这些。

Q6:在 Arch Linux 做 Trusted Users 时有没有什么心得?

说来非常惭愧,做 TU 这么4年了,实际做的事情着实有限,只能隔几天打打包而已。要做的事情太多, 而自己上面也说了有干活的人最重要,设身处地深刻体会到在开源社区的诸位志愿者们大家都不容易。

TU 应该做的事情,细数一下除了给 community 打包之外,还有处理包的 bug ,处理 AUR 的争议, 测试新包给反馈,以及沟通和反馈上游。反观自己做的事情,真的太少了。比起肥猫和其他 TU 们的辛勤, 总觉得自己不够格。「精力有限,凭着志愿者热情」,什么的说辞可以说很多, 但是良心上对着自己热爱的事情却不能百分百扑上去做,真的没有颜面腆着脸说……

打包和沟通上游之类的心得倒是有不少,也一直想写点笔记记录一下,挖坑却没时间填上。该说, 或许应该换个本职工作了,又想,孰重孰轻哪边是本行需要自己掂量。

Q7:有什么话要对咱说嘛?

不知何时起,不知萌狼经历了什么,有时候感觉萌狼傲娇的性格让人看不透,不过事后能看出萌狼都是本着好心。 或许,如果能更坦诚一些的话,也能更融入大家吧。虽然我也没资格这么说。

像前面写的,隐约能感觉到萌狼似乎为了理想放弃了很多,孰重孰轻是每个人自己的权衡。

以及还有感谢,感谢萌狼把我当作朋友,感谢萌狼的耐心。

最后还有抱歉,这篇拖了太久,是该治治我的拖延症了。

by farseerfc at January 05, 2020 08:51 AM

中文社区新闻

现在开始使用 zstd 替代 xz 进行软件包压缩

邮件列表上已经宣布了,从2019年12月27日开始,我们的软件包压缩格式已经从 xz (.pkg.tar.xz) 改为了 zstd (.pkg.tar.zst)

zstd 相较于 xz 用压缩比换来高性能。用我们的压缩参数调用 zstd 重新压缩软件包导致了总体包大小增加 ~0.8% ,相对的这些包的解压时间总体有 ~1300% 的提速。

我们的软件源中已经有超过 545 个 zstd 压缩的软件包了,随着我们发布更新包,更多的会不断加入。目前为止我们还未发现任何用户可见的问题,所以感觉一切顺利。

如果你是一名打包者,如果你在使用最新的 devtools (>= 20191227) 那么你将自动开始打包新的 .pkg.tar.zst 包。
如果你是一名最终用户,没有手动操作需要做,只要你已经阅读并遵从了去年新闻中的建议。

如果你从 2018 年到现在还没有升级过 libarchive ,还有希望拯救你的系统!在 Eli Schwartz 的个人源中提供了打包好的 pacman-static 二进制包,用他的受信用户(Trusted User)密钥签名,可以用这个完成系统升级。

译注:除Eli Schwartz 的个人源之外,[archlinuxcn]社区源也提供了 pacman-static 的二进制包,由 lilac 签名,欢迎使用。

by farseerfc at January 05, 2020 02:33 AM

January 04, 2020

Alynx Zhou

X 和 Wayland 的主要区别

最近在写一个 Wayland Compositor,虽然我以前大概知道这是什么,但是并不是很清楚它和 Xserver 有什么区别,虽然 fc 老师的这篇文章 写的相当不错,但我一点也不懂 X 所以看的迷迷糊糊。偶然读了 这篇文章 发现十分不错,但因为是英文文章读起来很累,打算把一些理解的内容记下来。顺便说一下,原文是带示例的,效果非常不错,建议有时间的人慢慢看一遍。

January 04, 2020 04:20 PM

ヨイツの賢狼ホロ

和 farseerfc 交换问题!

终于还是咕咕出来了?

虽然咱催了好久但是最后因为咱在年前把笔记本寄回去的原因结果自己咕了……

farseerfc 的问题和咱的回答~

Q1: 萌狼最開始是如何接觸到 Linux 的呢?有什麼契機導致使用 Linux 麼?

之前有订阅个叫电脑报的报纸来着,然后有一期介绍了 Ubuntu ,然后就下载来试了试。 (结果马上就把 Windows 的引导搞炸了 😂)

后来咱第一台笔记本运行 Windows 10 有点吃力的时候就往上面装了 Debian, 开始试着做主力了。再后来试着自己搭 Shadowsocks 的服务器,在 VPS 上用上了 CentOS。

看到了这篇文章 开始用 Arch Linux 了。最近是看到了 FSF 的自由发行版推荐,然后也开始用 Parabola 了 🤔

Q2: 這個筆談又是如何開始的呢?

不知道那一天的晚上咱更新友链的时候,就突然想到,友链归友链, 也想稍微深入的了解一下一个个名字后面的人呐,介于咱不想露面也不想出声,就退而求其次(?)的这样开始了。

只是咱提问的话,对方会不会觉得血亏 😂 于是就有了让咱回答对方提出的等量问题的决定(?)。

Q3: 音遊和 Linux 哪邊對萌狼而言更重要?

TL;DR: 两边不会因为各种原因顾此失彼。

一样重要,一个是咱为数不多的娱乐(尝试其它类型的游戏的尝试几乎都以发现自己苦手告终……), 一个咱还有兴趣乱钻(?)

或是一样不重要,一个因为成绩不及(不管是已经很强的还是后来居上的) 自闭,一个因为知识和经验不足(又轻易不敢胡乱提问(自闭。嘛反正都差不多就是了(

Q4: 如何看微軟喜歡 Open Source / Linux 整件事?

爱“开源”嘛,改不了它骨子里是个私有软件公司的本质鸭, 于是该黑的继续黑(大雾)。什么时候它们说自由化的时候再说吧(?)

Q5: 對我的印象如何?

完美潇洒的好(?)爸爸(??),咱认为是 #archlinux-cn 里为数不多向往自由的 GNU/Linux 用户,使(cai)用(keng)经验丰富, 经常能提出关键的建议,以及为数不多的能感受到咱的变化还能安慰咱的人。

Q6: 社區水聊時,什麼樣的話題會令萌狼覺得反感,以至於想離開社區避開討論?

(以下排名不分先后)

  • 群聊看着很火热但是咱一说话就完全没人搭理的时候;

  • 令不行禁不止的时候,例如群聊管理员在限定主题的群聊带头跑题还没有自觉的。

  • 居高临下的,例如轻易断定朽木不可雕然后劝退的(谁不是那么走过来的呢…… 也有可能是咱视野狭窄没见过那种天生神力的人)。

  • 强行安插印象的。(啥,例如说啥都会“奉承”自己的时候,还是不懂……)

  • 一言不合换掉的,就像啥桌面出问题都吹说换某桌面的。

  • 有人讨厌咱的时候,但是只要见不到就没什么大碍。

    (不过再想想,咱自己都不保证完全没有问题,也有不时说些怪话的时候,似乎没有什么权力要求别人的样子……)

Q7: 有什麼話想對我說嘛?

要努力当个好爸爸哟~ 以及咱们有没有<s 在没有黑暗的地方</s 见面的一天呢? (为什么咱每次说到见面都能想到那种地方2333……)

那么下一个是?

其实还没想好 😂,如果咱友链里剩下的谁愿意自告奋勇也热烈欢迎啦 ……

熟悉的感觉+=1……

by ホロ at January 04, 2020 04:00 PM

December 20, 2019

中文社区新闻

Xorg 清理需要手动干预

我们正在清理 Xorg,此次更新如果你遇到如下错误信息那么需要手动干预:

:: installing xorgproto (2019.2-2) breaks dependency 'inputproto' required by lib32-libxi
:: installing xorgproto (2019.2-2) breaks dependency 'dmxproto' required by libdmx
:: installing xorgproto (2019.2-2) breaks dependency 'xf86dgaproto' required by libxxf86dga
:: installing xorgproto (2019.2-2) breaks dependency 'xf86miscproto' required by libxxf86misc

更新时,请使用命令: pacman -Rdd libdmx libxxf86dga libxxf86misc && pacman -Syu 来完成更新。

by farseerfc at December 20, 2019 02:03 PM

December 11, 2019

ヨイツの賢狼ホロ

爱丽丝的树洞 - 在匿名网络(?)的一角

(一如既往的不擅长写最前面的摘要……其实只是把这里往 i2p 上搬了一份以后 的有感而发罢了。)

至于汝问咱这一个月咕咕咕到哪里去了的话,那肯定是没看前面那篇文章啦……

从深网到匿名网络

提到所谓的“深网”,大家最有印象的大概就是那张冰山图了吧:

网络层次“冰山”图

普遍认为的深网指的是那些不能用普通的搜索引擎搜索到的内容,这个概念下就有很多啦。 像是各种内部网络的服务、需要登录的外部服务、需要付费解锁的服务甚至某微幕公众号等 等都可以叫做“深网”。

接下来,网络上有一些 IP 地址上看起来没有分配任何服务,因此网络上的流量不太可能 到达这种地区。因为和宇宙学中的黑洞差不多(没啥能从里面出来的?)于是这一段网络 有时也被称作“黑暗网络”。

再接下来,出于各种奇怪的目的(例如隐藏沟通各方的身份、缓解单点故障风险等等), 不少人利用现有的网络搭建了各种需要特殊手段才能进入的网络。再因为某些不可知的宣传 或者渲染恐惧等因素,这种网络一度被错误的称作“暗网”。两大知名匿名网络软件(Tor 和 i2p) 的开发者们也一直在努力的改正这个错误……

比较著名的匿名网络软件有 Tor (因为传输过程中解密流量的过程有点像剥洋葱有时被 称作“洋葱路由”)、i2p(有时和 Tor 对应的被称作“大蒜路由”)和 Freenet 自由网 等等。有的网络的功能要点在于分布式和去中心化而不是匿名,因此通常不被称作是匿名网络。 像是 ZeroNet 和 ipfs 等等。

出于使用经验在内的种种原因(意思就是这么多咱没都用过),咱就先介绍前两个 比较著名的好了……

兔子洞前的兔子?

Tor是实现匿名通信的自由软件。其名源于“The Onion Router”(洋葱路由器) 的英语缩写。用户可透过Tor接达由全球志愿者免费提供,包含7000+个中继 的覆盖网络,从而达至隐藏用户真实地址、避免网络监控及流量分析的目的。

Tor用户的互联网活动(包括浏览在线网站、帖子以及即时消息等通信形式) 相对较难追踪。Tor的设计原意在于保障用户的个人隐私,以及不受监控地进 行秘密通信的自由和能力。Tor透过在传输协议栈中的应用层进行加密,从而 实现洋葱路由这一种技术。

Tor会对包括下一个节点的IP地址在内的数据, 进行多次加密,并透过虚拟电路(包括随机选择的Tor节点)将其提交。 每个中继都会对一层加密的数据进行解密,以知道数据的下一个发送目的地, 然后将剩余的加密数据发送给它。最后的中继会解密最内层的加密数据, 并在不会泄露或得知源IP地址的情况下,将原始数据发送至目标地址。

值得注意的地方是(?),这个让各国政府和监控部门头痛不已的网络开发过程 中的主要赞助者也包括美国海军研究办公室和联邦政府等等政府部门。(自己 帮忙造了个烫手山芋么……)

I2P(Invisible Internet Project即“隐形网计划”),是一项混合授权的匿名网络项目。

I2P网络是由I2P路由器以大蒜路由方式组成的表层网络, 创建于其上的应用程序可以安全匿名的相互通信。它可以同时使用UDP及 TCP协议,支持UPnP映射。其应用包括匿名上网、聊天、撰写博客和文件传输。

I2P是可伸缩性强,具有自我组织与恢复能力的包切换匿名网络。 其上运行有多种不同的匿名安全程序,各程序可以自行决定匿名性、延迟、 流量平衡而不用考虑混淆式路由网络的具体实现。它们的数据活动可以与现有 的I2P用户的匿名数据相混合。

Tor 和 i2p 的区别主要在于路由上,Tor 所使用的“洋葱路由”使用同一条 网络链路实现数据的发送和接收,而 i2p 使用的“大蒜路由”使用多条网络链路 发送数据和接受数据。

Tor 和 i2p 在路由上的区别。

以及为了获得网络中的中继列表, Tor 需要和某几个固定的目录服务器联系,于是 目录服务器就很容易成为审查者封锁的目标。相较之下, i2p 使用 Kad 算法获得网络中 节点的数据(就像 eMule 一样),因此只要客户端还能连接到一个中继,就能取得连接 需要的服务器信息,封锁难度也随之上升。

最后的最后(?), Tor 的主要目的是为了相对匿名(?)地访问常规网站,如果汝 去看 Tor 的流量统计的话,就会发现访问洋葱服务的流量只占 Tor 网络中总体流量 的小部分。

而 i2p 设计的主要目的就是构建一个匿名网络,因此不像 Tor 那样有很多的访问明网 (普通的互联网)的节点(经常只有一个,或者没有……)。以及由于通过多条链路发送 和接收数据的原因,速度也稍微慢一些。

i2p 自己有一份文档稍微区分了一下自己和 Tor: https://geti2p.net/zh-cn/comparison/tor

关于这两个匿名网络软件的使用方法嘛,并不是这里的重点(啥?)。 最好的教程还是 官方提供的:

众人拾柴火焰高 - 帮助匿名网络成长壮大

什么网络都要有足够多的人使用才能发展,就像现在的互联网一样。对于术业有专攻的 匿名网络来说更是如此。在汝使用过这些匿名网络工具之后,有没有想过助它一臂之力呢?

  • 宣传和修正错误观念

向朋友们宣传匿名网络的存在和它的重要性,以及他们了解到 匿名网络不只是某些破坏行为的栖息之所,它还是保护汝所珍视的某些事物的关键武器。

  • 架设匿名网络服务,或者把汝的普通网站调整成匿名网络友好

出于某些商业或政治因素,大多数网站对来自匿名网络的访问者有各种各样的限制 (例如要求输入验证码或者拒绝访问等等)。 如果汝是一个网站的所有者(像咱一样),汝可以考虑为匿名网络使用者放行(例如 如果汝有在使用 Cloudflare 的话,可以考虑为 Tor 的出口 IP 设置白名单),或者 更进一步,在匿名网络上放置汝的网站。(当然可能需要稍微调整一下汝的网站以适应 匿名网络的需求)

  • 架设或赞助匿名网络节点

匿名网络毕竟还要靠普通网络中的设备来驱动,通过增加匿名网络中的节点(无论 是汝自己亲力亲为还是提供资金给其它提供者),既可以部分提高(?)网络的速度, 也可以增强网络的健壮性。

  • 参与匿名网络软件开发

不一定是编写代码啦,撰写文档,提供翻译,参加测试和讨论也都是行之有效的方法呢。

Tor 和 i2p 都有自己的社区页面,从那里开始汝的第一步吧!


by ホロ at December 11, 2019 04:00 PM

December 10, 2019

Phoenix Nemo

使用 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 是更好的选择。

December 10, 2019 06:00 PM

November 26, 2019

百合仙子

Python 3.8 升级记录

本文来自依云's Blog,转载请注明。

Python 3.8 发布有好多天了,Arch Linux 也早就重新打包了一千多个包(感谢辛勤的肥猫猫),隔天就从 [staging] 进入 [testing] 了,四天之后进入正式仓库([extra] 和 [community])。

Python 3.8 进入官方仓库的次日,我本地进行了更新。之所以要等一天,自然是等 [archlinuxcn] 的更新啦。然后那些需要人工干预而又暂时没人理的我本地重新打包了。使用 pacman -Qo /usr/lib/python3.7/site-packages 查询尚未更新的软件包,然后对着对应的 PKGBUILD 一顿改(基本上也就是 pkgrel 加 0.1 而已),makepkg -si 就好了。

但是这样还没完事哦。

sudo updatedb 更新一下 mlocate 的数据库。然后 locate -be python3.7 | grep -v /var/lib/lxc 找到一些残留的文件,主要是 ~/.local/lib 下的,以及散落在管理之外的 venv 里的。~/.local/lib 下的都是我自己的项目,删掉然后重新去项目里 python setup.py develop --user 就好了。venv 的话,直接删掉吧……

然后是 locate -be python-37 | grep -v /var/lib/lxc | grep -v /usr/lib/python3.7/site-packages。这个是为了查 Python 3.7 的 pyc 文件,所以这次也排除了 Python 3.7 的 site-packages,避免尚未更新的 Python 包的干扰(有些暂时用不到的包我就懒得自己 makepkg 了),等更新完之后整个目录删掉。有些软件包(比如 gdb-common)没有使用标准的 Python 安装流程(比如因为并不是标准的 Python 库),打包者(比如著名的 Allan McRae)没有或者拒绝在打包时编译 pyc 文件,造成 Python 自行创建不被管理的 pyc 文件,软件包卸载或者 Python 升级后就残留下来了。

确认没有问题之后(比如有些软件可能自带了个旧版本的 Python,或者有些并不是 pyc 的文件也包含这个字符串),执行 locate -be python-37 | grep -v /var/lib/lxc | grep -v /usr/lib/python3.7/site-packages | sudo xargs rm -v 删除这些文件。当然如果有需要保留的文件自行从文件列表中删掉先。

pyc 清理之后,接下来清理一下空的 __pycache__ 目录啦。locate -be __pycache__ | sudo xargs rmdir -v 2>/dev/null 就可以了,非空目录不会被删掉的。

哦对了,我现在在用 mypy 了,所以还要 locate -we .mypy_cache/3.7 一下。

我之所以现在记录这事儿,「现在」的原因是,我要在另一个系统上再测试一遍再发布出来,「记录」的原因是,下一次我就不用想要执行哪些命令了。

by 依云 at November 26, 2019 02:48 PM

中文社区新闻

primus_vk>=1.3-1 更新需要手动干预

primus_vk 包在版本 1.3-1 之前缺少了一些动态库链接。这个错误已经在 1.3-1 中修正了,所以升级时需要手动覆盖掉没有被跟踪到的动态库链接。如果你遇到如下报错:


primus_vk: /usr/lib/libnv_vulkan_wrapper.so.1 exists in filesystem
primus_vk: /usr/lib/libprimus_vk.so.1 exists in filesystem

那么请使用如下命令:


pacman -Syu --overwrite=/usr/lib/libnv_vulkan_wrapper.so.1,/usr/lib/libprimus_vk.so.1

进行更新。

by farseerfc at November 26, 2019 07:41 AM

November 22, 2019

Leo Shen

MDR-7506 Detachable Cable Mod

Earlier this year I brought a pair of Sony MDR-7506. It's great. The audio quality is gorgeous and it is pretty light weight. However, since it is intended to be a studio headphone, it comes with a very long coiled cable. This causes a ton of headache. It is convenient when using it at home, but when I want to take it out, sometimes the cable takes up more of my bag space than the headphone itself.

November 22, 2019 09:33 PM

November 18, 2019

ヨイツの賢狼ホロ

状态回报 - 20191119

这啥?

嘛咱也不知道叫什么好 😂(汝翻翻之前起这种 slug 的文章?)

在不同的地方写不同的字

就像标题一样,咱要挖新的大坑啦(?)……

  • 约伊兹的萌狼乡手札 Another (名字什么的最难起了),在折腾了那么久博客软件和生成器后还是回到最早用的 WordPress 了…… (因为 Like Button 这时候只支持 Matters/Medium 和 WordPress 鸭……)之后好多图的文章大概会发到那里。

  • KenOokamiHoro @ Matters

    Matters 是一個以分佈式網絡為基礎、加密貨幣驅動的公共討論平台。

    所有在 Matters 上發佈的作品(不含評論),皆會上載到星際文件系統(InterPlanetary File System,IPFS)的節點上,實現作品內容的分佈式存儲,完成將數據回歸創作者的第一步。

    Matters 希望圍繞公共議題、知識生產,重構內容價值生態,搭建優質社群平台, 保護創作版權;以獨特算法令優質內容浮現,以數字貨幣讓創作者、參與者獲得持續回報。

    2019 年 10 月,Matters 和 LikeCoin 聯手,將 LikeCoin 這一基於寫作者創造力為衡量的 加密貨幣引入到 Matters 自由創作和公共討論空間之中,平台效應以收入的形式回饋給作者。

    Matters - 关于我们

    初期大概会把这边不会更新的文章放上去吧(因为 IPFS 的特性以致于 Matters 不能编辑已发表的文章), 以后可能会新写些什么。

    除了给文章点赞以外,大家也可以考虑 加入赞赏公民 , 助力咱和各位独立作者。

  • 至于这里嘛,咱只能讲短期内不会消失,不敢保证一直都在呐…… 这里应该还是以平时 折腾的记录为主了。 剩下有些关于朋友和生活的系列文章可能会先改在 Another 上发布。


大概就是这个样子……

by ホロ at November 18, 2019 04:00 PM

November 11, 2019

中文社区新闻

关于新内核包和 mkinitcpio 挂钩的变动

我们的官方内核: linux, linux-lts, linux-zen 和 linux-hardened ,将不再直接把内核安装到 /boot 中去了。

安装和删除的步骤现在由 mkinitcpio 的挂钩(hook)和脚本(script)接管,因此无需手动干预升级过程。

此次变更的目的是想让内核包更独立(self-contained),并且让启动过程更灵活,同时保持向后兼容性。

目前只有 mkinitcpio 有挂钩负责处理安装删除内核,我们还没有为 dracut 提供类似的支持,不过今后 dracut 将会有类似的挂钩。

by farseerfc at November 11, 2019 03:23 AM

November 07, 2019

Leo Shen

Update firmware of a Crucial SSD with systemd-boot

I bought this Crucial MX300 around two years ago and it has served me well. Today I (randomly) found out that I have not update the firmware for this drive for a long time now. I looked at the firmware page and, sure enough, there's a firmware update. Grab firmware There are two ways to update the firmware. The first way is to use a software on Windows, which is HUGE and slow.

November 07, 2019 08:42 PM

November 04, 2019

ヨイツの賢狼ホロ

一点点深入端到端加密 -- 公钥密码系统如何保护我们的信息?

来自 https://ssd.eff.org/en/module/deep-dive-end-end-encryption-how-do-public-key-encryption-systems-work

这个页面引用了 https://ssd.eff.org 的图片呐,汝可能需要根据情况调整汝浏览器 扩展的设置来查看它们。

如果使用得当,端到端加密可以保护汝的消息、文字甚至文件除了为汝期望的接收者以外 其他的家伙都看不到。它也能保证汝收到的消息确实来自汝所认识的那一位发件人(而不是 别的冒充他/她/它的谁),是呗~

过去的几年中,端到端加密工具如雨后春笋般在各个领域出现(而且达到了可用标准)。例如 像 Signal 这样支持文字消息、语音/视频通话和文件传送的 IM 就是其中的一种咯。这些工具让 监听者无法读取消息的内容(包括服务提供商自己)。

是不是觉得有些端到端加密的实现难以理解?在汝开始使用这些端到端加密工具以前,咱们推荐汝先花一点时间了解一下公钥密码学的基础。

这篇文章所讨论的公钥密码学正是端到端加密所依赖的基础。啥,汝想了解其它的加密类型?去翻翻咱们以前写(翻译)的 《当汝看到加密(encryption)时应该想到什么》 咯~

理解公钥密码学的基本原则(大概)能让汝更正确的使用那些工具,例如公钥加密可以做什么,不能做什么,和何时以及如何使用它。

太长不看版小结

  • 对称密码系统中,加密和解密使用相同的密钥。
  • 公钥密码系统中,参加信息交换的双方各自有自己的两对密钥,称作私钥和公钥。
  • 公钥可以公开,私钥需要安全保护。
  • 发件人用收件人的公钥加密消息,收件人用自己的私钥解密消息。
  • 发件人用自己的私钥签名消息,收件人用发件人的公钥验证消息。
  • 同样地,公钥密码系统保护的是消息的内容而不是元数据。

加密做什么?

当汝以加密的方式传输一些数据的时候:

  • 可以清楚的直接读取的消息(称作“明文”,例如“hello mum”)被加密的话会被混乱成轻易无法理解的形式(例如“OhsieW5ge+osh1aehah6”,称作“密文”)。
  • 这段看起来很像乱码的消息在网上传递。
  • 消息唯一的接收者可以通过某些方法把它还原成原来的形式(“hello mum”),这个过程称作“解密”。

对称加密:以一把密钥传递私密信息的故事

名字什么的最难起啦……以及平常的 Bob 和 Alice 呢?

汝不知道 Alice 和 Bob 是谁?

Julia 想写张小纸条告诉她的朋友 César “Meet me in the garden,”(在花园和我见面),不过她当然不想让她的同学也知道。

在多个人之间传递信息

(汝有没有在上课的时候给谁传过小纸条咧?)

Julia 的字条会在同学间传递好几次之后才能到 César 的手上。不排除中间有谁会在传递之前悄悄看上一眼, 或者自己抄一份然后记下 Julia 写字条的时间什么的。(汝在期望给汝传字条的同学不会多管闲事嘛?)

信息的传递过程可(ken)能(ding)会被偷窥

Julia 决定用密钥 “3” 来加密她们的消息,每个字母向后移动三位,例如 A->D, B->E 等等。 其实这简单的密钥加密以后的消息看起来像胡言乱语,别有用心的某人还是能够通过尝试所有的组合 的方式来“暴力破解”。换句话说,他们能为了得到解密的答案一直猜下去。

这种移位三个字母的方法其实是一种古老的加密方法,相传是罗马帝国的凯撒大帝发明的,所以也称作凯撒密码。 在这个例子中,字母移位的数量(3)被同时用于加密和解密,这被称作对称加密。

凯撒加密其实挺弱的(按现代的话来说,可以通过频率分析一类的手段破译出密钥(字母移位的数量))。 不过这么长时间过去了,在神奇的算法和电脑的帮助下,加密的密钥可以生成的越来越长,越来越难猜。 对称加密也用在越来越多的场景中。

(汝来想一想

对称加密

有没有问题?)


不过咧,对称加密还剩下一个问题。如果有人在 Julia 和 César 交换密钥的时候窃听,然后窃取密钥怎么办? (毕竟密钥没法再加密的传递出去……) 或者如果她们俩没法在现实见面呢?(例如离得相当远?)

于是让咱们跳过纷繁冗杂的学习过程,就当她们已经熟练掌握公钥加密了吧(大嘘)。 窃听者不太可能得到她俩解密消息的密钥——因为她们根本就不用分享呀。 在公钥加密的过程中,加密和解密的密钥是不同的。

公钥密码:密钥之双城记

拿上汝手边的放大镜(啥?),让咱们离问题更近一些:在有人窃听对话的前提下, 如何只把对称解密的密钥发送给接收者咧,特别是在双方的物理距离非常远的话?

公钥密码学(有时候汝也会看到非对称加密之类的说法,这俩是一样的)解决了这个问题。 它允许每个人在对话中创建两个密钥——通常称作公钥和私钥。两把密钥相互关联,通常是 具有某些特别的数学性质的特别大的数字。如果汝用谁的公钥编码了一段消息,那么他们就 可以用他们的私钥解码。

Julia 和 César 现在要用公钥密码发送加密的消息,于是之前传小纸条的同学们就理所当然的被 一台台电脑取代了呗。它们可能是她们俩之间的电脑,例如 Wi-Fi 接入点,ISP,她们所使用的邮箱的 服务器等等。自然它们也能复制双方传递的消息的内容和记录消息传递的时间。

在互联网上传递信息时,会经过多个中继

虽然她们不介意中间有人会看到她们的消息,不过自然的还是不想让别人看到消息的内容。

首先,Julia 需要 César 的公钥,于是 César 把她的公钥寄给了 Julia (例如通过邮件)。她并不 介意通过不安全的通道传递公钥,因为公钥本来就是她可以自由分享的东西。

需要注意的是,公钥和现实生活中的钥匙还有一些区别的。因为 César 可以把公钥拆分成几部分,然后通过不同的通道 发送出去。

César 发送公钥给 Julia

有了公钥以后,Julia 就能给她发送加密的信息了:

“Meet me in the garden.”

Julia 发出了只给 César 加密的消息。

私钥可以解密由同一人的公钥加密的消息

虽然她们都可以理解这条消息,剩下的人都认为消息的内容是乱码, 不过中间的人(或者电脑)还是能看到元数据,例如主题、时间、发件人和收件人。

元数据在信息的传递过程中不会被加密

因为消息是用 César 的公钥加密的,因此只有 César 和 Julia(因为是发件人)能阅读这条消息。

César 用她的私钥解密消息:

私钥可以解密由同一人的公钥加密的消息

简单的概括:

  • 公钥密码可以让汝(和其他人)在开放和不安全的通道中传递公钥。
  • 有了朋友的公钥以后,汝就可以给他们发送加密的信息。
  • 汝的私钥用来解密发送给汝的加密的信息。
  • 中间人——像是服务和网络提供商,在消息传递的过程中可以看到一些元数据,例如谁写了 这些消息,发送给谁,接收或者发送的时间和消息的主题等等。

(汝觉得现在可以高枕无忧了?)

还有一件事……如何识别假冒?

在上面的例子中,中间人能够看到整个过程中的元数据。

所以让咱们来耍点坏心眼,某个中间人想知道她们在谈论什么,因此在监听她们。

坏人能够用一些手段让 Julia 得到假的公钥。Julia 没注意到这不是 César 的公钥,还是用它加密了要发给 César 的消息。

随后坏人就得到了本该加密给 César 的消息,它可以看到消息的原始内容,然后再发送给 César 。

心怀恶意的中间人可以假冒某人发送消息

甚至可以篡改消息的内容:

心怀恶意的中间人可以假冒某人发送篡改的消息

于是 Julia 和 César 如约在花园见面,没想到这里已经有人守候多时了……

心怀恶意的中间人

这就是所谓的“中间人攻击”啦,其实攻击的家伙也不一定是人……幸运的是,公钥密码学有防御这种攻击的手段。

公钥密码学可以让汝通过一种称作“指纹验证”的方式来确认某把密钥确实属于汝认识的某人,(“指纹” 指的是公钥通过某种摘要算法计算出的一串字母,通常比公钥短的多) 这种过程最好在现实生活中完成。如果汝可以和谁面对面的话,汝可以检查双方拥有的公钥的指纹的每一个字母 是否一致。虽然一个一个的看很枯燥,但为了保证汝手上的某把私钥确实属于汝认识的某人的话,就稍微忍受一下吧……

其它的端到端加密应用也拥有类似的指纹验证机制,虽然可能方法各不相同。有些方式比较传统,汝要小心比较双方屏幕上显示的内容是否一致。 有的方法比较“新鲜”,例如用汝手上的设备扫描对方显示的二维码来“验证”某个设备。

支持扫描二维码验证的端到端加密应用

如果汝没法和对方见面的话( 担心有去无回么…… ),汝可以在其它已知安全的地方交换指纹,例如端到端加密的聊天软件,或者一个 HTTPS 网站。

通过其它方式传递指纹A 通过其它方式传递指纹B

总结一下:

  • 中间人攻击是指有人在某个双方之间拦截传递的消息,攻击者可以修改消息、或是只是简单地监听。
  • 公钥密码让汝通过验证发送者和接收者的身份来防御中间人攻击,这种方法被称作“指纹验证”。
  • 汝(或者汝的朋友)的公钥也包含一些被称作“指纹”的信息,汝可以通过指纹验证公钥持有人的身份。
  • 私钥用于解密发给汝的消息,也用来以汝的名义签名消息。

签名时代

公钥密码让汝不用冒险把解密密钥发送给汝私密消息的接收者,因为他们已经拥有了,就是他们的私钥。 因为公钥只用来加密而不能解密,因此汝(和汝消息的接收者)可以把公钥分享出去,汝发送私密信息所需要 的也只有接收者的公钥而已。

不过只有这样么……咱们已经知道了用特定公钥加密的消息只有用公钥对应的私钥解密。其实反过来也是, 用特定私钥加密的消息只能用私钥对应的公钥来解密。

啥,这有啥用?汝用汝自己的私钥加密一段消息然后让随便什么人用汝的公钥解密来看嘛…… 别那么着急嘛,再想想,例如汝写了一句话,然后用汝的私钥加密。大家都能解密汝的消息, 但只有持有汝私钥的人能写出这条消息。如果汝能小心翼翼的保护汝的私钥的话,多半只有汝 自己有汝自己的私钥,也就只有汝能发出这条消息了是呗~

通过用汝的私钥加密消息,汝确保了这条消息只会是汝所写的,是不是感觉哪里眼熟?没错,就和 实际生活中汝在某些地方签下汝自己的名字一样,汝对汝的数字消息完成了同样的操作。

签名同时也能防止篡改。毕竟大多数情况下,其他人不能用汝的私钥重新签名是吧…… 因此,已签名的消息可确保它来自某个特定来源,并且不会在传输过程中被搞得一团糟。

复习:关于公钥加密

公钥密码让汝可以把消息安全的发给知道对方公钥的人咯。

如果有人知道了汝的公钥:

  • 他们可以发送只有用汝的私钥解密的私密信息。
  • 汝可以用汝的私钥签名消息,而他们可以用公钥验证签名有效。

如果汝知道其他人的公钥:

  • 汝可以解密来自他/她/它的消息,而且可以验证消息确实来自那一位。

所以很明显啦,汝(或其他人)的公钥传播的越广,这个加密方式就越有用处是呗。既然 公钥可以分享,汝也可以以现实中的什么东西类比一下啦,例如通讯录或者电话黄页什么的: 它是公开的,人们知道哪里可以找到,汝也能广泛的分享它。

至于私钥呢,汝还是拿现实中的什么东西类比一下好了,例如开门或者各种锁的钥匙。汝的 私钥用来加密和解密消息,所以务必要像对待手上真正的钥匙一样对待它。

为啥汝的私钥明显的(如此)重要?如果汝丢了钥匙,汝就有可能打不开某处。公钥加密中私钥 也是如此,如果汝弄丢了私钥,汝就没法解密发送给汝的信息了。如果谁拿到了汝的钥匙,他就能 以汝的名义打开某些地方。如果其他人拿到了汝的私钥(例如物理接触到汝的电脑,通过恶意软件, 或者只是因为汝像个大笨驴一样把汝的私钥发了出来等等),他就能读取汝加密的信息,或者冒充汝 做些什么。

也曾经有听说过某些政府试图窃取某些人的私钥的经历(像是强行收缴电脑啊,钓鱼攻击或者想方设法的 安装恶意软件等等)。这当然是对公钥加密安全性的损害。 既然汝大多数时候遇不到那么交迫的情况,那咱们来换个栗子:有扒手在汝停下来休息的时候偷走了汝的 钥匙,然后去配了一把新的,把原来的钥匙悄悄地放回汝的口袋里。汝可能觉得什么都没发生,然后发 现有人光顾了汝的家门……

所以又到了老生常谈的威胁模型时间了:估算汝可能遇到的风险,并设计相应的应对措施。 例如既然汝担心谁觊觎汝的私钥的话,汝大概就不会使用某些浏览器中的端到端加密方案了吧,汝也会想方设法 确保汝的私钥只留在汝自己的电脑上而不是别处(例如其他人的服务器或所谓的云端)。

公钥密码学总结和典型实例: PGP

虽然咱们是分开演示的对称加密和公钥机密,不过公钥加密的过程中也有机会用到对称加密啦。 例如是记得操作中,公钥加密加密的是解密对称加密的消息的密钥。PGP 就是把对称加密和公钥加密 结合起来的典型例子。

(汝有时会看到像是 openPGP 或者 GnuPG 一类的家伙,是因为 PGP 本身是私有的啦,于是不少人开发了 类似的软件,它们都支持 openPGP 规范。而其中最常用的就是 GNU 开发的 GnuPG 咯……)

所以那些密钥到底是什么,它们是怎么结合在一起的?

公钥密码学基于有两把密钥这一前提,一个负责加密,一个负责解密。 基本上,汝可以把其中一把密钥在互联网这样的不安全通道上传递,这把称作公钥。 汝可以把公钥公开到随便哪里去而不会影响到加密信息的安全性。

密钥对的组成

(公钥和私钥构成一对密钥,有时也把它俩一起称作一个密钥对)

既然公钥可以分享,汝也可以以现实中的什么东西类比一下啦,例如通讯录或者电话黄页什么的: 它是公开的,人们知道哪里可以找到,汝也能广泛的分享它。

至于私钥呢,汝还是拿现实中的什么东西类比一下好了,例如开门或者各种锁的钥匙。汝的 私钥用来加密和解密消息,所以务必要像对待手上真正的钥匙一样对待它。

让咱们来看一看一种经常在公钥密码中用于生成密钥的算法,叫做 RSA (以发明这种算法的三位科学家 罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman) 的姓氏命名)。

公钥和私钥实际上是两组大数

(私钥由两组特别大的质数组成)

公钥和私钥实际上是两组大数_

(公钥是私钥的两组数的乘积,因为私钥是两组特别大的质数,所以在相当长的一段时间内很难猜出来)

公钥和私钥同生共死(?只是一起生成啦……),而且都依赖于特别大的秘密质数。私钥由两组特别大的质数组成, 而公钥就是它们的乘积。以公钥推算出私钥的两个质数的尝试十分困难。

为啥咧?因为现在还没有一个又快又稳定的分解质因数的算法啦……随着时间的推进,现代的加密套件也允许用户生成 更大更难猜的质数作为密钥。

(以及随着时间推进,更现代的加密算法也被发明出来,像是椭圆曲线加密算法(ECC)和 ElGamal 等等。)

这就是公钥加密的强大之处了,参加加密信息交换的双方在不安全的通道中交换的信息只有双方的公钥啦! 因为不需要交换私钥,所以(理想的情况下)私钥自然不会泄漏。

铭记:需要双方的公钥让公钥密码工作。

换种说法:公钥和私钥一起生成,像阴阳一样交织在一起。

公钥和私钥阴阳调和(大雾)

公钥可以搜索和分享,汝可以把它随便发送到哪里去。如果汝不介意暴露邮件地址的话,汝可以把它放在社交媒体 或是汝自己的网站上。

私钥需要安全的保护起来。因为越多的话保持安全和秘密就越困难,所以汝肯定只想留着一把(而且不要搞丢), 而不是把它分享出去或者弄得复件到处都是。(?)

PGP 如何工作

所以说了那么多,PGP 到底是如何工作的呢?假设汝要向 Aarav 发送一些私密消息:

  • Aarav 有一对密钥,像一个优秀的公钥密码用户一样,他把他的公钥放在了他有 HTTPS 的网站上。
  • 汝下载了他的公钥,用它加密了一些消息发送给他。
  • 因为只有他有他自己的私钥,所以只有他能解密发送给他的消息。

PGP(Pretty Good Privacy,优良保密协议)就是聚焦于创建和使用公钥和私钥这些细节的结合。 汝可以创建一对密钥,用密码保护私钥,以及用私钥和公钥加密和签名文本。

如果汝一定要总结一下的话:用足够强的密码短语保护汝的私钥,而且放在安全的地方。

元数据:公钥加密不是银弹

公钥加密的一切(秘密,一致性,防止篡改)都是在保护消息的内容。但这绝对不是汝唯一在 隐私上需要关注的问题,就像前面所说的一样,关于消息的信息(有时被称作“元数据”) 也可能暴露消息的内容。

某些情况下,仅仅是交换加密的信息也有可能让汝遇到危险。例如某些政权中的持不同政见者, 或者仅仅是拒绝解密消息。

伪装汝在和某人交流的事实也许会相当困难。在前面的 PGP 中,一种可行的方法是双方使用 通过匿名网络(例如 Tor )访问的匿名邮箱。在这种情况下, PGP 还是十分有用,无论是 在其他人间保护双方的私密消息,还是证明消息的作者和传递过程没被篡改。

说了那么多,不妨亲手试试看一些端到端加密的应用吧, 例如 可用于 iOS 和 Android 的 Signal

by ホロ at November 04, 2019 04:00 PM

November 03, 2019

Alynx Zhou

GNOME 的修改开发与测试

由于时间久远,记录的事物可能已经改变,作者不能保证此时页面内容完全正确,请不要完全参考。

对于一些 GNOME 比较核心的程序比如 GNOME Shell,调试的时候没办法简单的运行,需要构建一个隔离的环境然后替代系统的 WM。GNOME 项目使用 JHBuild 构建这个环境。我的系统是 Arch Linux,介绍一下中间遇到的一些问题。

November 03, 2019 07:05 AM

October 29, 2019

Alynx Zhou

Arch Linux 安装 UnityHub 的临时方案

如果你直接使用 AUR 里 UnityHub 的 PKGBUILD 安装会出一些问题。解决方案也很简单。 首先似乎 PKGBUILD 下载的版本很奇怪,不管你在 Unity 论坛里哪个链接下载的版本其实都是一个,并且和 PKGBUILD 里面的不一样,解决方法就是自己计算一下 m...

October 29, 2019 02:27 PM

Magnolia

我不是很能欣赏花。

我的鼻子并不懂得花的芬芳,我的眼睛也不是很了解花的鲜艳,大部分情况下,我连分辨花种类的兴趣都没有。但我今天还是认出了一株玉兰树,就像我高中楼门口两侧的玉兰树一样,开着白色花瓣。

October 29, 2019 02:27 PM

如何编写 Android 视频壁纸

最近我编写了一个 Android 的视频壁纸应用(GitHub Repo),一开始觉得并没有什么难写的地方,应该很快就可以写出来,但是后来发现我想的太简单了。或许你也看过许多写视频壁纸的教程,但我发现他们都有一些问题,写出来的程序基本不能用,所以我打算在这里写一下如何编写一个可以发布的 Android 视频壁纸,而不是一个 demo。

October 29, 2019 02:27 PM

在中国 Android 环境下传个 APK 有多难

事情的起因是这样的,我写了 一个 Android App,打算把它发给更多的人试用,发给同学什么的都好说,但我想发给我妈用的时候遇到了一系列问题。我觉得恰好可以说一下在国内用 Android 环境是多么痛苦。

October 29, 2019 02:27 PM

Linux 下面常见的代理设置

通常情况下在 Linux 下面配置好的是 socks5 代理(你懂的),但是这个代理并不能让所有程序自动走它,需要手动的做一些转换。

October 29, 2019 02:27 PM