Farseerfc的小窝//farseerfc.me/zhs/2020-10-06T13:45:00+09:00关于 swap 的一些补充2020-10-06T13:45:00+09:002020-10-06T13:45:00+09:00farseerfctag:farseerfc.me,2020-10-06:/zhs/followup-about-swap.html<p>上周翻译完 <a class="reference external" href="//farseerfc.me/zhs/in-defence-of-swap.html">【译】替 swap 辩护:常见的误解</a>
之后很多朋友们似乎还有些疑问和误解,于是写篇后续澄清一下。事先声明我不是内核开发者,
这里说的只是我的理解,
<a class="reference external" href="https://www.kernel.org/doc/gorman/html/understand/understand005.html">基于内核文档中关于物理内存的描述</a>
,新的内核代码的具体行为可能和我的理解有所出入,欢迎踊跃讨论。</p>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://www.youtube.com/watch?v=7aONIVSXiJ8">Introduction to Memory Management in Linux</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/7aONIVSXiJ8"></iframe></div></div>
</div>
<div class="section" id="id3">
<h2>误解1: swap 是虚拟内存,虚拟内存肯定比物理内存慢嘛</h2>
<p>这种误解进一步的结论通常是:「使用虚拟内存肯定会减慢系统运行时性能,如果物理内存足够为什么还要用虚拟的?」
这种误解是把虚拟内存和交换区的实现方式类比于「虚拟磁盘」或者「虚拟机」等同的方式,
也隐含「先用物理内存,用完了之后用虚拟内存」也即下面的「误解3」的理解。</p>
<p>首先,交换区(swap) <strong>不是</strong> 虚拟内存。操作系统中说「物理内存」还是「虚拟内存」的时候在指程序代码
寻址时使用的内存地址方式,使用物理地址空间时是在访问物理内存,使用虚拟地址空间时是在访问虚拟内存 …</p></div><p>上周翻译完 <a class="reference external" href="//farseerfc.me/zhs/in-defence-of-swap.html">【译】替 swap 辩护:常见的误解</a>
之后很多朋友们似乎还有些疑问和误解,于是写篇后续澄清一下。事先声明我不是内核开发者,
这里说的只是我的理解,
<a class="reference external" href="https://www.kernel.org/doc/gorman/html/understand/understand005.html">基于内核文档中关于物理内存的描述</a>
,新的内核代码的具体行为可能和我的理解有所出入,欢迎踊跃讨论。</p>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://www.youtube.com/watch?v=7aONIVSXiJ8">Introduction to Memory Management in Linux</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/7aONIVSXiJ8"></iframe></div></div>
</div>
<div class="section" id="id3">
<h2>误解1: swap 是虚拟内存,虚拟内存肯定比物理内存慢嘛</h2>
<p>这种误解进一步的结论通常是:「使用虚拟内存肯定会减慢系统运行时性能,如果物理内存足够为什么还要用虚拟的?」
这种误解是把虚拟内存和交换区的实现方式类比于「虚拟磁盘」或者「虚拟机」等同的方式,
也隐含「先用物理内存,用完了之后用虚拟内存」也即下面的「误解3」的理解。</p>
<p>首先,交换区(swap) <strong>不是</strong> 虚拟内存。操作系统中说「物理内存」还是「虚拟内存」的时候在指程序代码
寻址时使用的内存地址方式,使用物理地址空间时是在访问物理内存,使用虚拟地址空间时是在访问虚拟内存。
现代操作系统在大部分情况下都在使用虚拟地址空间寻址, <strong>包括</strong> 在执行内核代码的时候。</p>
<p>并且,交换区 <strong>不是</strong> 实现虚拟内存的方式。操作系统使用内存管理单元(MMU,Memory Management
Unit)做虚拟内存地址到物理内存地址的地址翻译,现代架构下 MMU 通常是 CPU
的一部分,配有它专用的一小块存储区叫做地址转换旁路缓存(TLB,Translation Lookaside Buffer),
只有在 TLB 中没有相关地址翻译信息的时候 MMU 才会以缺页中断的形式调用操作系统内核帮忙。
除了 TLB 信息不足的时候,大部分情况下使用虚拟内存都是硬件直接实现的地址翻译,没有软件模拟开销。
实现虚拟内存不需要用到交换区,交换区只是操作系统实现虚拟内存后能提供的一个附加功能,
即便没有交换区,操作系统大部分时候也在用虚拟内存,包括在大部分内核代码中。</p>
</div>
<div class="section" id="id4">
<h2>误解2: 但是没有交换区的话,虚拟内存地址都有物理内存对应嘛</h2>
<p>很多朋友也理解上述操作系统实现虚拟内存的方式,但是仍然会有疑问:「我知道虚拟内存和交换区的区别,
但是没有交换区的话,虚拟内存地址都有物理内存对应,不用交换区的话就不会遇到读虚拟内存需要读写磁盘
导致的卡顿了嘛」。</p>
<p>这种理解也是错的,禁用交换区的时候,也会有一部分分配给程序的虚拟内存不对应物理内存,
比如使用 <code class="code">
mmap</code>
调用实现内存映射文件的时候。实际上即便是使用 <code class="code">
read/write</code>
读写文件, Linux 内核中(可能现代操作系统内核都)在底下是用和 <code class="code">
mmap</code>
相同的机制建立文件
到虚拟地址空间的地址映射,然后实际读写到虚拟地址时靠缺页中断把文件内容载入页面缓存(page cache
)。内核加载可执行程序和动态链接库的方式也是通过内存映射文件。甚至可以进一步说,
用户空间的虚拟内存地址范围内,除了匿名页之外,其它虚拟地址都是文件后备(backed by file
),而匿名页通过交换区作为文件后备。上篇文章中提到的别的类型的内存,比如共享内存页面(shm
)是被一个内存中的虚拟文件系统后备的,这一点有些套娃先暂且不提。于是事实是无论有没有交换区,
缺页的时候总会有磁盘读写从慢速存储加载到物理内存,这进一步引出上篇文章中对于交换区和页面缓存这两者的讨论。</p>
</div>
<div class="section" id="id5">
<h2>误解3: 不是内存快用完的时候才会交换的么?</h2>
<p>简短的答案可以说「是」,但是内核理解的「内存快用完」和你理解的很可能不同。
也可以说「不是」,就算按照内核理解的「内存快用完」的定义,内存快用完的时候内核的行为是去回收内存,
至于回收内存的时候内核会做什么有个复杂的启发式经验算法,实际上真的内存快满的时候根本来不及做
swap ,内核可能会尝试丢弃 page cache 甚至丢弃 vfs cache (dentry cache / inode cache)
这些不需要磁盘I/O就能更快获取可用内存的动作。</p>
<p>深究这些内核机制之前,我在思考为什么很多朋友会问出这样的问题。可能大部分这么问的人,学过编程,
稍微学过基本的操作系统原理,在脑海里对内核分配页面留着这样一种印象(C伪代码):</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="c1">//////////////////// userspace space ////////////////</span></span>
<span class="code-line"><span class="kt">void</span><span class="o">*</span> <span class="nf">malloc</span><span class="p">(</span><span class="kt">int</span> <span class="n">size</span><span class="p">){</span></span>
<span class="code-line"> <span class="kt">void</span><span class="o">*</span> <span class="n">pages</span> <span class="o">=</span> <span class="n">mmap</span><span class="p">(...);</span> <span class="c1">// 从内核分配内存页</span></span>
<span class="code-line"> <span class="k">return</span> <span class="n">alloc_from_page</span><span class="p">(</span><span class="n">pages</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span> <span class="c1">// 从拿到的内存页细分</span></span>
<span class="code-line"><span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="c1">//////////////////// kernel space //////////////////</span></span>
<span class="code-line"><span class="kt">void</span> <span class="o">*</span> <span class="n">SYSCALL</span> <span class="nf">do_mmap</span><span class="p">(...){</span></span>
<span class="code-line"> <span class="c1">//...</span></span>
<span class="code-line"> <span class="k">return</span> <span class="n">kmalloc_pages</span><span class="p">(</span><span class="n">nr_page</span><span class="p">);</span></span>
<span class="code-line"><span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="kt">void</span><span class="o">*</span> <span class="nf">kmalloc_pages</span><span class="p">(</span><span class="kt">int</span> <span class="n">size</span><span class="p">){</span></span>
<span class="code-line"> <span class="k">while</span> <span class="p">(</span> <span class="n">available_mem</span> <span class="o"><</span> <span class="n">size</span> <span class="p">)</span> <span class="p">{</span></span>
<span class="code-line"> <span class="c1">// 可用内存不够了!尝试搞点内存</span></span>
<span class="code-line"> <span class="n">page_frame_info</span><span class="o">*</span> <span class="n">least_accessed</span> <span class="o">=</span> <span class="n">lru_pop_page_frame</span><span class="p">();</span> <span class="c1">// 找出最少访问的页面</span></span>
<span class="code-line"> <span class="k">switch</span> <span class="p">(</span> <span class="n">least_accessed</span> <span class="o">-></span> <span class="n">pf_type</span> <span class="p">){</span></span>
<span class="code-line"> <span class="k">case</span> <span class="nl">PAGE_CACHE</span><span class="p">:</span> <span class="n">drop_page_cache</span><span class="p">(</span><span class="n">least_accessed</span><span class="p">);</span> <span class="k">break</span><span class="p">;</span> <span class="c1">// 丢弃文件缓存</span></span>
<span class="code-line"> <span class="k">case</span> <span class="nl">SWAP</span><span class="p">:</span> <span class="n">swap_out</span><span class="p">(</span><span class="n">least_accessed</span><span class="p">);</span> <span class="k">break</span><span class="p">;</span> <span class="c1">// <- 写磁盘,所以系统卡了!</span></span>
<span class="code-line"> <span class="c1">// ... 别的方式回收 least_accessed</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"> <span class="n">append_free_page</span><span class="p">(</span><span class="n">free_page_list</span><span class="p">,</span> <span class="n">least_accessed</span><span class="p">);</span> <span class="c1">// 回收到的页面加入可用列表</span></span>
<span class="code-line"> <span class="n">available_mem</span> <span class="o">+=</span> <span class="n">least_accessed</span> <span class="o">-></span> <span class="n">size</span><span class="p">;</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"> <span class="c1">// 搞到内存了!返回给程序</span></span>
<span class="code-line"> <span class="n">available_mem</span> <span class="o">-=</span> <span class="n">size</span><span class="p">;</span></span>
<span class="code-line"> <span class="kt">void</span> <span class="o">*</span> <span class="n">phy_addr</span> <span class="o">=</span> <span class="n">take_from_free_list</span><span class="p">(</span><span class="n">free_page_list</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span></span>
<span class="code-line"> <span class="k">return</span> <span class="n">assign_virtual_addr</span><span class="p">(</span><span class="n">phy_addr</span><span class="p">);</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
<p>这种逻辑隐含三层 <strong>错误的</strong> 假设:</p>
<ol class="arabic simple">
<li>分配物理内存是发生在从内核分配内存的时候的,比如 <code class="code">
malloc/mmap</code>
的时候。</li>
<li>内存回收是发生在进程请求内存分配的上下文里的,换句话说进程在等内核的内存回收返回内存,
不回收到内存,进程就得不到内存。</li>
<li>交换出内存到 swap 是发生在内存回收的时候的,会阻塞内核的内存回收,进而阻塞程序的内存分配。</li>
</ol>
<p>这种把内核代码当作「具有特权的库函数调用」的看法,可能很易于理解,
甚至早期可能的确有操作系统的内核是这么实现的,但是很可惜现代操作系统都不是这么做的。
上面三层假设的错误之处在于:</p>
<ol class="arabic simple">
<li>在程序请求内存的时候,比如 <code class="code">
malloc/mmap</code>
的时候,内核只做虚拟地址分配,
记录下某段虚拟地址空间对这个程序是可以合法访问的,但是不实际分配物理内存给程序。
在程序第一次访问到虚拟地址的时候,才会实际分配物理内存。这种叫 <strong>惰性分配(lazy allocation)</strong> 。</li>
<li>在内核感受到内存分配压力之后,早在内核内存用尽之前,内核就会在后台慢慢扫描并回收内存页。
内存回收通常不发生在内存分配的时候,除非在内存非常短缺的情况下,后台内存回收来不及满足当前分配请求,
才会发生 <strong>直接回收(direct reclamation)</strong> 。</li>
<li>同样除了直接回收的情况,大部分正常情况下换出页面是内存管理子系统调用 DMA 在后台慢慢做的,
交换页面出去不会阻塞内核的内存回收,更不会阻塞程序做内存分配(malloc
)和使用内存(实际访问惰性分配的内存页)。</li>
</ol>
<p>也就是说,现代操作系统内核是高度并行化的设计,内存分配方方面面需要消耗计算资源或者 I/O
带宽的场景,都会尽量并行化,最大程度利用好计算机所有组件(CPU/MMU/DMA/IO)的吞吐率,
不到紧要关头需要直接回收的场合,就不会阻塞程序的正常执行流程。</p>
</div>
<div class="section" id="id6">
<h2>惰性分配有什么好处?</h2>
<p>或许会有人问:「我让你分配内存,你给我分配了个虚拟的,到用的时候还要做很多事情才能给我,这不是骗人嘛」,
或者会有人担心惰性分配会对性能造成负面影响。</p>
<p>这里实际情况是程序从分配虚拟内存的时候,「到用的时候」,这之间有段时间间隔,可以留给内核做准备
。程序可能一下子分配一大片内存地址,然后再在执行过程中解析数据慢慢往地址范围内写东西。
程序分配虚拟内存的速率可以是「突发」的,比如一个系统调用中分配 1GiB 大小,而实际写入数据的速率会被
CPU 执行速度等因素限制,不会短期内突然写入很多页面。
这个分配速率导致的时间差内内核可以完成很多后台工作,比如回收内存,
比如把回收到的别的进程用过的内存页面初始化为全0,这部分后台工作可以和程序的执行过程并行,
从而当程序实际用到内存的时候,需要的准备工作已经做完了,大部分场景下可以直接分配物理内存出来。</p>
<p>如果程序要做实时响应,想避免因为惰性分配造成的性能不稳定,可以使用 <code class="code">
mlock/mlockall</code>
将得到的虚拟内存锁定在物理内存中,锁的过程中内核会做物理内存分配。不过要区分「性能不稳定」和「低性能」,
预先分配内存可以避免实际使用内存时分配物理页面的额外开销,但是会拖慢整体吞吐率,所以要谨慎使用。</p>
<p>很多程序分配了很大一片地址空间,但是实际并不会用完这些地址,直到程序执行结束这些虚拟地址也一直
处于没有对应物理地址的情况。惰性分配可以避免为这些情况浪费物理内存页面,使得很多程序可以无忧无虑地
随意分配内存地址而不用担心性能损失。这种分配方式也叫「超额分配(overcommit)」。飞机票有超售,
VPS 提供商划分虚拟机有超售,操作系统管理内存也同样有这种现象,合理使用超额分配能改善整体系统效率。</p>
<p>内核要高效地做到惰性分配而不影响程序执行效率的前提之一,在于程序真的用到内存的时候,
内核能不做太多操作就立刻分配出来,也就是说内核需要时时刻刻在手上留有一部分空页,
满足程序执行时内存分配的需要。换句话说,内核需要早在物理内存用尽之前,就开始回收内存。</p>
</div>
<div class="section" id="id7">
<h2>那么内核什么时候会开始回收内存?</h2>
<p>首先一些背景知识:物理内存地址空间并不是都平等,因为一些地址范围可以做
<a class="reference external" href="https://en.wikipedia.org/wiki/Direct_memory_access">DMA</a> 而另一些不能,以及
<a class="reference external" href="https://en.wikipedia.org/wiki/Non-uniform_memory_access">NUMA</a>
等硬件环境倾向于让 CPU 访问其所在 NUMA 节点内存范围。在 32bit
系统上内核的虚拟地址空间还有低端内存和高端内存的区分,他们会倾向于使用不同属性的物理内存,到
64bit 系统上已经没有了这种限制。</p>
<p>硬件限制了内存分配的自由度,于是内核把物理内存空间分成多个 Zone
,每个 Zone 内各自管理可用内存, Zone 内的内存页之间是相互平等的。</p>
<div class="panel panel-default">
<div class="panel-heading">
zone 内水位线</div>
<div class="panel-body">
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/c995609e.png"/>
</div>
</div>
<p>一个 Zone 内的页面分配情况可以右图描绘。
除了已用内存页,剩下的就是空闲页(free pages),空闲页范围中有三个水位线(watermark
)评估当前内存压力情况,分别是高位(high)、低位(low)、最小位(min)。</p>
<p>当内存分配使得空闲页水位低于低位线,内核会唤醒 <code class="code">
kswapd</code>
后台线程, <code class="code">
kswapd</code>
负责扫描物理页面的使用情况并挑选一部分页面做回收,直到可用页面数量恢复到水位线高位(high)以上。
如果 <code class="code">
kswapd</code>
回收内存的速度慢于程序执行实际分配内存的速度,
可用空闲页数量可能进一步下降,降至低于最小水位(min)之后,内核会让内存分配进入
<strong>直接回收(direct reclamation)</strong> 模式,在直接回收模式下,程序分配某个物理页的请求(
第一次访问某个已分配虚拟页面的时候)会导致在进程上下文中阻塞式地调用内存回收代码。</p>
<p>除了内核在后台回收内存,进程也可以主动释放内存,比如有程序退出的时候就会释放一大片内存页,
所以可用页面数量可能会升至水位线高位以上。有太多可用页面浪费资源对整体系统运行效率也不是好事,
所以系统会积极缓存文件读写,所有 page cache 都留在内存中,直到可用页面降至低水位以下触发
<code class="code">
kswapd</code>
开始工作。</p>
<p>设置最小水位线(min)的原因在于,内核中有些硬件也会突然请求大量内存,比如来自网卡接收到的数据包,
预留出最小水位线以下的内存给内核内部和硬件使用。</p>
<p>设置高低两个控制 <code class="code">
kswapd</code>
开关的水位线是基于控制理论。唤醒 <code class="code">
kswapd</code>
扫描内存页面本身有一定计算开销,于是每次唤醒它干活的话就让它多做一些活( high - low
),避免频繁多次唤醒。</p>
<p>因为有这些水位线,系统中根据程序请求内存的「速率」,整个系统的内存分配在宏观的一段时间内可能处于以下几种状态:</p>
<ol class="arabic simple">
<li><strong>不回收:</strong> 系统中的程序申请内存速度很慢,或者程序主动释放内存的速度很快,
(比如程序执行时间很短,不怎么进行文件读写就马上退出,)此时可用页面数量可能一直处于低水位线以上,
内核不会主动回收内存,所有文件读写都会以页面缓存的形式留在物理内存中。</li>
<li><strong>后台回收:</strong> 系统中的程序在缓慢申请内存,比如做文件读写,
比如分配并使用匿名页面。系统会时不时地唤醒 <code class="code">
kswapd</code>
在后台做内存回收,
不会干扰到程序的执行效率。</li>
<li><strong>直接回收:</strong> 如果程序申请内存的速度快于 <code class="code">
kswapd</code>
后台回收内存的速度,
空闲内存最终会跌破最小水位线,随后的内存申请会进入直接回收的代码路径,从而极大限制内存分配速度。
在直接分配和后台回收的同时作用下,空闲内存可能会时不时回到最小水位线以上,
但是如果程序继续申请内存,空闲内存量就会在最小水位线附近上下徘徊。</li>
<li><strong>杀进程回收:</strong> 甚至直接分配和后台回收的同时作用也不足以拖慢程序分配内存的速度的时候,
最终空闲内存会完全用完,此时触发 OOM 杀手干活杀进程。</li>
</ol>
<p>系统状态处于 <strong>1. 不回收</strong> 的时候表明分配给系统的内存量过多,比如系统刚刚启动之类的时候。
理想上应该让系统长期处于 <strong>2. 后台回收</strong> 的状态,此时最大化利用缓存的效率而又不会因为内存回收
减缓程序执行速度。如果系统引导后长期处于 <strong>1. 不回收</strong> 的状态下,那么说明没有充分利用空闲内存做
文件缓存,有些 unix 服务比如 <a class="reference external" href="https://wiki.archlinux.org/index.php/preload">preload</a> 可用来提前填充文件缓存。</p>
<p>如果系统频繁进入 <strong>3. 直接回收</strong> 的状态,表明在这种工作负载下系统需要减慢一些内存分配速度,
让 <code class="code">
kswapd</code>
有足够时间回收内存。就如前一篇翻译中 Chris
所述,频繁进入这种状态也不一定代表「内存不足」,可能表示内存分配处于非常高效的利用状态下,
系统充分利用慢速的磁盘带宽,为快速的内存缓存提供足够的可用空间。
<strong>直接回收</strong> 是否对进程负载有负面影响要看具体负载的特性。
此时选择禁用 swap 并不能降低磁盘I/O,反而可能缩短 <strong>2. 后台回收</strong> 状态能持续的时间,
导致更快进入 <strong>4. 杀进程回收</strong> 的极端状态。</p>
<p>当然如果系统长期处于 <strong>直接回收</strong> 的状态的话,则说明内存总量不足,需要考虑增加物理内存,
或者减少系统负载了。如果系统进入 <strong>4. 杀进程回收</strong> 的状态,不光用空间的进程会受影响,
并且还可能导致内核态的内存分配受影响,产生网络丢包之类的结果。</p>
</div>
<div class="section" id="id8">
<h2>微调内存管理水位线</h2>
<p>可以看一下运行中的系统中每个 Zone 的水位线在哪儿。比如我手上这个 16GiB 的系统中:</p>
<div class="highlight"><pre><span class="code-line"><span></span>$ cat /proc/zoneinfo</span>
<span class="code-line">Node <span class="m">0</span>, zone DMA</span>
<span class="code-line"> pages free <span class="m">3459</span></span>
<span class="code-line"> min <span class="m">16</span></span>
<span class="code-line"> low <span class="m">20</span></span>
<span class="code-line"> high <span class="m">24</span></span>
<span class="code-line"> spanned <span class="m">4095</span></span>
<span class="code-line"> present <span class="m">3997</span></span>
<span class="code-line"> managed <span class="m">3975</span></span>
<span class="code-line">Node <span class="m">0</span>, zone DMA32</span>
<span class="code-line"> pages free <span class="m">225265</span></span>
<span class="code-line"> min <span class="m">3140</span></span>
<span class="code-line"> low <span class="m">3925</span></span>
<span class="code-line"> high <span class="m">4710</span></span>
<span class="code-line"> spanned <span class="m">1044480</span></span>
<span class="code-line"> present <span class="m">780044</span></span>
<span class="code-line"> managed <span class="m">763629</span></span>
<span class="code-line">Node <span class="m">0</span>, zone Normal</span>
<span class="code-line"> pages free <span class="m">300413</span></span>
<span class="code-line"> min <span class="m">13739</span></span>
<span class="code-line"> low <span class="m">17173</span></span>
<span class="code-line"> high <span class="m">20607</span></span>
<span class="code-line"> spanned <span class="m">3407872</span></span>
<span class="code-line"> present <span class="m">3407872</span></span>
<span class="code-line"> managed <span class="m">3328410</span></span>
</pre></div>
<p>因为不是 NUMA 系统,所以只有一个 NUMA node,其中根据 DMA 类型共有 3 个 Zone 分别叫 DMA,
DMA32, Normal 。三个 Zone 的物理地址范围(spanned)加起来大概有
<span class="math">\(4095+1044480+3407872\)</span> 大约 17GiB 的地址空间,而实际可访问的地址范围(present
)加起来有 <span class="math">\(3997+780044+3407872\)</span> 大约 16GiB 的可访问物理内存。</p>
<p>其中空闲页面有 <span class="math">\(3459+762569+1460218\)</span> 大约 8.5GiB ,三条水位线分别在:
<span class="math">\(\texttt{high} = 24+4710+20607 = 98\texttt{MiB}\)</span> ,
<span class="math">\(\texttt{low} = 20+3925+17173 = 82\texttt{MiB}\)</span> ,
<span class="math">\(\texttt{min} = 16+3140+13739 = 65\texttt{MiB}\)</span> 的位置。</p>
<p>具体这些水位线的确定方式基于几个 sysctl 。首先 min 基于 <code class="code">
vm.min_free_kbytes</code>
默认是基于内核低端内存量的平方根算的值,并限制到最大 64MiB 再加点余量,比如我这台机器上
<code class="code">
vm.min_free_kbytes = 67584</code>
,于是 min 水位线在这个位置。
其它两个水位线基于这个计算,在 min 基础上增加总内存量的 <code class="code">
vm.watermark_scale_factor / 10000</code>
比例(在小内存的系统上还有额外考虑),默认 <code class="code">
vm.watermark_scale_factor = 10</code>
在大内存系统上意味着 low 比 min 高 0.1% , high 比 low 高 0.1% 。</p>
<p>可以手动设置这些值,以更早触发内存回收,比如将 <code class="code">
vm.watermark_scale_factor</code>
设为 100:</p>
<div class="highlight"><pre><span class="code-line"><span></span>$ <span class="nb">echo</span> <span class="m">100</span> <span class="p">|</span> sudo tee /proc/sys/vm/watermark_scale_factor</span>
<span class="code-line">$ cat /proc/zoneinfo</span>
<span class="code-line">Node <span class="m">0</span>, zone DMA</span>
<span class="code-line"> pages free <span class="m">3459</span></span>
<span class="code-line"> min <span class="m">16</span></span>
<span class="code-line"> low <span class="m">55</span></span>
<span class="code-line"> high <span class="m">94</span></span>
<span class="code-line"> spanned <span class="m">4095</span></span>
<span class="code-line"> present <span class="m">3997</span></span>
<span class="code-line"> managed <span class="m">3975</span></span>
<span class="code-line"> Node <span class="m">0</span>, zone DMA32</span>
<span class="code-line"> pages free <span class="m">101987</span></span>
<span class="code-line"> min <span class="m">3149</span></span>
<span class="code-line"> low <span class="m">10785</span></span>
<span class="code-line"> high <span class="m">18421</span></span>
<span class="code-line"> spanned <span class="m">1044480</span></span>
<span class="code-line"> present <span class="m">780044</span></span>
<span class="code-line"> managed <span class="m">763629</span></span>
<span class="code-line"> Node <span class="m">0</span>, zone Normal</span>
<span class="code-line"> pages free <span class="m">61987</span></span>
<span class="code-line"> min <span class="m">13729</span></span>
<span class="code-line"> low <span class="m">47013</span></span>
<span class="code-line"> high <span class="m">80297</span></span>
<span class="code-line"> spanned <span class="m">3407872</span></span>
<span class="code-line"> present <span class="m">3407872</span></span>
<span class="code-line"> managed <span class="m">3328410</span></span>
</pre></div>
<p>得到的三条水位线分别在 <span class="math">\(\texttt{min} = 16+3149+13729 = 66\texttt{MiB}\)</span>
, <span class="math">\(\texttt{low} = 55+10785+47013 = 226\texttt{MiB}\)</span>
, <span class="math">\(\texttt{high} = 94+18421+80297 = 386\texttt{MiB}\)</span> ,
从而 low 和 high 分别比 min 提高 160MiB 也就是内存总量的 1% 左右。</p>
<p>在 swap 放在 HDD 的系统中,因为换页出去的速度较慢,除了上篇文章说的降低
<code class="code">
vm.swappiness</code>
之外,还可以适当提高 <code class="code">
vm.watermark_scale_factor</code>
让内核更早开始回收内存,这虽然会稍微降低缓存命中率,但是另一方面可以在进入直接回收模式之前
有更多时间做后台换页,也将有助于改善系统整体流畅度。</p>
</div>
<div class="section" id="id9">
<h2>只有 0.1% ,这不就是说内存快用完的时候么?</h2>
<p>所以之前的「误解3」我说答案可以说「是」或者「不是」,但是无论回答是或不是,都代表了认为「swap
就是额外的慢速内存」的错误看法。当有人在强调「swap 是内存快用完的时候才交换」的时候,
隐含地,是在把系统总体的内存分配看作是一个静态的划分过程:打个比方这就像在说,我的系统里存储空间有快速
128GiB SSD 和慢速 HDD 的 1TiB ,同样内存有快速的 16GiB RAM 和慢速 16GiB 的 swap 。
这种静态划分的类比是错误的看待方式,因为系统回收内存进而做页面交换的方式是动态平衡的过程,
需要考虑到「时间」和「速率」而非单纯看「容量」。</p>
<p>假设 swap 所在的存储设备可以支持 5MiB/s 的吞吐率( HDD 上可能更慢, SSD
上可能更快,这里需要关注数量级),相比之下 DDR3 大概有 10GiB/s 的吞吐率,DDR4 大概有 20GiB/s
,无论多快的 SSD 也远达不到这样的吞吐(可能 Intel Optane 这样的
<a class="reference external" href="https://lwn.net/Articles/717953/">DAX</a> 设备会改变这里的状况)。从而把 swap
当作慢速内存的视角来看的话,加权平均的速率是非常悲观的,「 16G 的 DDR3 + 16G 的 swap 会有
<span class="math">\(\frac{16 \times 10 \times 1024 + 16 \times 5}{16+16} = 5 \texttt{GiB/s}\)</span>
的吞吐?所以开 swap 导致系统速度降了一半?」显然不能这样看待。</p>
<p>动态的看待方式是, swap 设备能提供 5MiB/s 的吞吐,这意味着:如果我们能把未来 10
分钟内不会访问到的页面换出到 swap ,那么就相当于有
<span class="math">\(10 \times 60 \texttt{s} \times 5 \texttt{MiB/s} = 3000 \texttt{MiB}\)</span>
的额外内存,用来放那 10 分钟内可能会访问到的页面缓存。
10 分钟只是随口说的一段时间,可以换成 10 秒或者 10 小时,重要的是只要页面交换发生在后台,
不阻塞前台程序的执行,那么 swap 设备提供的额外吞吐率相当于一段时间内提供了更大的物理内存,
总是能提升页面缓存的命中,从而改善系统性能。</p>
<p>当然系统内核不能预知「未来 10 分钟内需要的页面」,只能根据历史上访问内存的情况预估之后可能会访问的情况,
估算不准的情况下,比如最近10分钟内用过的页面缓存在之后10分钟内不再被使用的时候,
为了把最近这10分钟内访问过的页面留在物理内存中,可能会把之后10分钟内要用到的匿名页面换出到了交换设备上。
于是会有下面的情况:</p>
</div>
<div class="section" id="swap-swap">
<h2>但是我开了 swap 之后,一旦复制大文件,系统就变卡,不开 swap 不会这样的</h2>
<p>大概电脑用户都经历过这种现象,不限于 Linux 用户,包括 macOS 和 Windows 上也是。
在文件管理器中复制了几个大文件之后,切换到别的程序系统就极其卡顿,复制已经结束之后的一段时间也会如此。
复制的过程中系统交换区的使用率在上涨,复制结束后下降,显然 swap 在其中有重要因素,并且禁用
swap 或者调低 swappiness 之后就不会这样了。于是网上大量流传着解释这一现象,并进一步建议禁用
swap 或者调低 swappiness 的文章。我相信不少关心系统性能调优的人看过这篇「
<a class="reference external" href="https://rudd-o.com/linux-and-free-software/tales-from-responsivenessland-why-linux-feels-slow-and-how-to-fix-that">Tales from responsivenessland: why Linux feels slow, and how to fix that</a>
」或是它的转载、翻译,用中文搜索的话还能找到更多错误解释 swappiness 目的的文章,比如
<a class="reference external" href="http://blog.itpub.net/29371470/viewspace-1250975">这篇将 swappiness 解释成是控制内存和交换区比例的参数</a> 。</p>
<p>除去那些有技术上谬误的文章,这些网文中描述的现象是有道理的,不单纯是以讹传讹。
桌面环境中内存分配策略的不确定性和服务器环境中很不一样,复制、下载、解压大文件等导致一段时间内
大量占用页面缓存,以至于把操作结束后需要的页面撵出物理内存,无论是交换出去的方式还是以丢弃页面缓存的方式,
都会导致桌面响应性降低。</p>
<p>不过就像前文 Chris 所述,这种现象其实并不能通过禁止 swap 的方式缓解:禁止 swap 或者调整
swappiness 让系统尽量避免 swap 只影响回收匿名页面的策略,不影响系统回收页面的时机,
也不能避免系统丢弃将要使用的页面缓存而导致的卡顿。</p>
<p>以前在 Linux 上也没有什么好方法能避免这种现象。 macOS 转用 APFS 作为默认文件系统之后,
从文件管理器(Finder)复制文件默认启用 file clone 快速完成,这操作不实际复制文件数据,
一个隐含优势在不需要读入文件内容,从而不会导致大量页面缓存失效。 Linux 上同样可以用支持
reflink 的文件系统比如 btrfs 或者开了 reflink=1 的 xfs 达到类似的效果。
不过 reflink 也只能拯救复制文件的情况,不能改善解压文件、下载文件、计算文件校验等情况下,
一次性处理大文件对内存产生的压力。</p>
<p>好在最近几年 Linux 有了 cgroup ,允许更细粒度地调整系统资源分配。进一步现在我们有了 cgroup
v2 ,前面 Chris 的文章也有提到 cgroup v2 的 <code class="code">
memory.low</code>
可以某种程度上建议内存子系统
尽量避免回收某些 cgroup 进程的内存。</p>
<p>于是有了 cgroup 之后,另一种思路是把复制文件等大量使用内存而之后又不需要保留页面缓存的程序单独放入
cgroup 内限制它的内存用量,用一点点复制文件时的性能损失换来整体系统的响应流畅度。</p>
<div class="panel panel-default">
<div class="panel-heading">
关于 cgroup v1 和 v2</div>
<div class="panel-body">
<p>稍微跑题说一下 cgroup v2 相对于 v1 带来的优势。这方面优势在
<a class="reference external" href="https://www.youtube.com/watch?v=ikZ8_mRotT4">Chris Down 另一个关于 cgroup v2 演讲</a>
中有提到。老 cgroup v1 按控制器区分 cgroup 层级,从而内存控制器所限制的东西和 IO
控制器所限制的东西是独立的。在内核角度来看,页面写回(page writeback)和交换(swap)正是
夹在内存控制器和IO控制器管理的边界上,从而用 v1 的 cgroup 难以同时管理。 v2
通过统一控制器层级解决了这方面限制。具体见下面 Chris Down 的演讲。</p>
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/ikZ8_mRotT4"></iframe></div></div>
</div>
</div>
<div class="section" id="cgroup-v2">
<h2>用 cgroup v2 限制进程的内存分配</h2>
<p>实际上有了 cgroup v2 之后,还有更多控制内存分配的方案。 <a class="reference external" href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#memory">cgroup v2 的内存控制器</a>
可以对某个 cgroup 设置这些阈值:</p>
<ul class="simple">
<li><strong>memory.min</strong> : 最小内存限制。内存用量低于此值后系统不会回收内存。</li>
<li><strong>memory.low</strong> : 低内存水位。内存用量低于此值后系统会尽量避免回收内存。</li>
<li><strong>memory.high</strong> : 高内存水位。内存用量高于此值后系统会积极回收内存,并且会对内存分配节流(throttle)。</li>
<li><strong>memory.max</strong> : 最大内存限制。内存用量高于此值后系统会对内存分配请求返回 ENOMEM,或者在 cgroup 内触发 OOM 。</li>
</ul>
<p>可见这些设定值可以当作 per-cgroup 的内存分配水位线,作用于某一部分进程而非整个系统。
针对交换区使用情况也可设置这些阈值:</p>
<ul class="simple">
<li><strong>memory.swap.high</strong> : 高交换区水位,交换区用量高于此值后会对交换区分配节流。</li>
<li><strong>memory.swap.max</strong> : 最大交换区限制,交换区用量高于此值后不再会发生匿名页交换。</li>
</ul>
<p>到达这些 cgroup 设定阈值的时候,还可以设置内核回调的处理程序,从用户空间做一些程序相关的操作。</p>
<p>Linux 有了 cgroup v2 之后,就可以通过对某些程序设置内存用量限制,避免他们产生的页面请求把别的
程序所需的页面挤出物理内存。使用 systemd 的系统中,首先需要
<a class="reference external" href="https://wiki.archlinux.org/index.php/Cgroups#Switching_to_cgroups_v2">启用 cgroup v2</a>
,在内核引导参数中加上 <code class="code">
systemd.unified_cgroup_hierarchy=1</code>
。然后开启用户权限代理:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="c1"># systemctl edit user@1000.service</span></span>
<span class="code-line"><span class="o">[</span>Service<span class="o">]</span></span>
<span class="code-line"><span class="nv">Delegate</span><span class="o">=</span>yes</span>
</pre></div>
<p>然后可以定义用户会话的 slice (slice 是 systemd 术语,用来映射 cgroup ),比如创建一个叫
<code class="code">
limit-mem</code>
的 slice :</p>
<div class="highlight"><pre><span class="code-line"><span></span>$ cat ~/.config/systemd/user/limit-mem.slice</span>
<span class="code-line"><span class="o">[</span>Slice<span class="o">]</span></span>
<span class="code-line"><span class="nv">MemoryHigh</span><span class="o">=</span>3G</span>
<span class="code-line"><span class="nv">MemoryMax</span><span class="o">=</span>4G</span>
<span class="code-line"><span class="nv">MemorySwapMax</span><span class="o">=</span>2G</span>
</pre></div>
<p>然后可以用 systemd-run 限制在某个 slice 中打开一个 shell:</p>
<div class="highlight"><pre><span class="code-line"><span></span>$ systemd-run --user --slice<span class="o">=</span>limit-mem.slice --shell</span>
</pre></div>
<p>或者定义一个 shell alias 用来限制任意命令:</p>
<div class="highlight"><pre><span class="code-line"><span></span>$ <span class="nb">type</span> limit-mem</span>
<span class="code-line">limit-mem is an <span class="nb">alias</span> <span class="k">for</span> /usr/bin/time systemd-run --user --pty --same-dir --wait --collect --slice<span class="o">=</span>limit-mem.slice</span>
<span class="code-line">$ limit-mem cp some-large-file dest/</span>
</pre></div>
<p>实际用法有很多,可以参考 systemd 文档
<a class="reference external" href="http://www.jinbuguo.com/systemd/systemd.resource-control.html">man systemd.resource-control</a>
, <a class="reference external" href="/links.html#xuanwo">xuanwo</a> 也 <a class="reference external" href="https://xuanwo.io/2018/10/30/tips-of-systemd/">有篇博客介绍过 systemd 下资源限制</a>
, <a class="reference external" href="/links.html#lilydjwg">lilydjwg</a> 也 <a class="reference external" href="https://blog.lilydjwg.me/2019/3/2/use-cgroups-to-limit-memory-usage-for-specific-processes.214196.html">写过用 cgroup 限制进程内存的用法</a>
和 <a class="reference external" href="https://blog.lilydjwg.me/2020/5/11/priority-and-nice-value-in-linux.215304.html">用 cgroup 之后对 CPU 调度的影响</a>
。</p>
</div>
<div class="section" id="id12">
<h2>未来展望</h2>
<p>最近新版的 gnome 和 KDE 已经开始为桌面环境下用户程序的进程创建 systemd scope 了,
可以通过 <code class="code">
systemd-cgls</code>
观察到,每个通过桌面文件(.desktop)开启的用户空间程序
都有个独立的名字叫 <code class="code">
app-APPNAME-HASH.scope</code>
之类的 systemd scope 。
有了这些 scope 之后,事实上用户程序的资源分配某种程度上已经相互独立,
不过默认的用户程序没有施加多少限制。</p>
<p>今后可以展望,桌面环境可以提供用户友好的方式对这些桌面程序施加公平性的限制。
不光是内存分配的大小限制,包括 CPU 和 IO 占用方面也会更公平。
值得一提的是传统的 ext4/xfs/f2fs 之类的文件系统虽然支持
<a class="reference external" href="https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#writeback">cgroup writeback 节流</a>
但是因为他们有额外的 journaling 写入,难以单独针对某些 cgroup 限制 IO
写入带宽(对文件系统元数据的写入难以统计到具体某组进程)。
而 btrfs 通过 CoW 避免了 journaling ,
<a class="reference external" href="https://facebookmicrosites.github.io/btrfs/docs/btrfs-facebook.html#io-control-with-cgroup2">在这方面有更好的支持</a>
。相信不远的将来,复制大文件之类常见普通操作不再需要手动调用加以限制,
就能避免单个程序占用太多资源影响别的程序。</p>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>【译】替 swap 辩护:常见的误解2020-09-30T13:45:00+09:002020-09-30T13:45:00+09:00farseerfctag:farseerfc.me,2020-09-30:/zhs/in-defence-of-swap.html<p>这篇翻译自 Chris Down 的博文
<a class="reference external" href="https://chrisdown.name/2018/01/02/in-defence-of-swap.html">In defence of swap: common misconceptions</a>
。 <a class="reference external" href="https://github.com/cdown/chrisdown.name/blob/master/LICENSE">原文的协议</a>
是 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
,本文翻译同样也使用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> 。其中加入了一些我自己的理解作为旁注,所有译注都在侧边栏中。</p>
<p>翻译这篇文章是因为经常看到朋友们(包括有经验的程序员和 Linux 管理员)对 swap 和 swappiness
有诸多误解,而这篇文章正好澄清了这些误解,也讲清楚了 Linux 中这两者的本质。值得一提的是本文讨论的
swap 针对 Linux 内核,在别的系统包括 macOS/WinNT 或者 Unix 系统中的交换文件可能有不同一样的行为,
需要不同的调优方式。比如在 <a class="reference external" href="https://www.freebsd.org/doc/handbook/bsdinstall-partitioning.html#configtuning-initial">FreeBSD …</a></p><p>这篇翻译自 Chris Down 的博文
<a class="reference external" href="https://chrisdown.name/2018/01/02/in-defence-of-swap.html">In defence of swap: common misconceptions</a>
。 <a class="reference external" href="https://github.com/cdown/chrisdown.name/blob/master/LICENSE">原文的协议</a>
是 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
,本文翻译同样也使用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> 。其中加入了一些我自己的理解作为旁注,所有译注都在侧边栏中。</p>
<p>翻译这篇文章是因为经常看到朋友们(包括有经验的程序员和 Linux 管理员)对 swap 和 swappiness
有诸多误解,而这篇文章正好澄清了这些误解,也讲清楚了 Linux 中这两者的本质。值得一提的是本文讨论的
swap 针对 Linux 内核,在别的系统包括 macOS/WinNT 或者 Unix 系统中的交换文件可能有不同一样的行为,
需要不同的调优方式。比如在 <a class="reference external" href="https://www.freebsd.org/doc/handbook/bsdinstall-partitioning.html#configtuning-initial">FreeBSD handbook</a>
中明确建议了 swap 分区通常应该是两倍物理内存大小,这一点建议对 FreeBSD 系内核的内存管理可能非常合理,
而不一定适合 Linux 内核,FreeBSD 和 Linux 有不同的内存管理方式尤其是 swap 和 page cache 和
buffer cache 的处理方式有诸多不同。</p>
<p>经常有朋友看到系统卡顿之后看系统内存使用状况观察到大量 swap 占用,于是觉得卡顿是来源于 swap
。就像文中所述,相关不蕴含因果,产生内存颠簸之后的确会造成大量 swap 占用,也会造成系统卡顿,
但是 swap 不是导致卡顿的原因,关掉 swap 或者调低 swappiness 并不能阻止卡顿,只会将 swap
造成的 I/O 转化为加载文件缓存造成的 I/O 。</p>
<p>以下是原文翻译:</p>
<hr class="docutils"/>
<p>这篇文章也有 <a class="reference external" href="https://chrisdown.name/ja/2018/01/02/in-defence-of-swap.html">日文</a>
和 <a class="reference external" href="https://softdroid.net/v-zashchitu-svopa-rasprostranennye-zabluzhdeniya">俄文</a>
翻译。</p>
<a aria-controls="a01b6caa" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a01b6caa" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="a01b6caa">
<p>tl;dr:</p>
<ol class="arabic simple">
<li>Having swap is a reasonably important part of a well functioning system.
Without it, sane memory management becomes harder to achieve.</li>
<li>Swap is not generally about getting emergency memory, it's about making
memory reclamation egalitarian and efficient. In fact, using it as
"emergency memory" is generally actively harmful.</li>
<li>Disabling swap does not prevent disk I/O from becoming a problem
under memory contention, it simply shifts the disk I/O thrashing from
anonymous pages to file pages. Not only may this be less efficient,
as we have a smaller pool of pages to select from for reclaim, but it
may also contribute to getting into this high contention state in
the first place.</li>
<li>The swapper on kernels before 4.0 has a lot of pitfalls,
and has contributed to a lot of people's negative perceptions about
swap due to its overeagerness to swap out pages. On kernels >4.0,
the situation is significantly better.</li>
<li>On SSDs, swapping out anonymous pages and reclaiming file pages are
essentially equivalent in terms of performance/latency.
On older spinning disks, swap reads are slower due to random reads,
so a lower <code class="code">
vm.swappiness</code>
setting makes sense there
(read on for more about <code class="code">
vm.swappiness</code>
).</li>
<li>Disabling swap doesn't prevent pathological behaviour at near-OOM,
although it's true that having swap may prolong it. Whether the
system global OOM killer is invoked with or without swap, or was invoked
sooner or later, the result is the same: you are left with a system in an
unpredictable state. Having no swap doesn't avoid this.</li>
<li>You can achieve better swap behaviour under memory pressure and prevent
thrashing using <code class="code">
memory.low</code>
and friends in cgroup v2.</li>
</ol>
</blockquote>
<p>太长不看:</p>
<ol class="arabic simple">
<li>对维持系统的正常功能而言,有 swap 是相对挺重要的一部分。没有它的话会更难做到合理的内存管理。</li>
<li>swap 的目的通常并不是用作紧急内存,它的目的在于让内存回收能更平等和高效。
事实上把它当作「紧急内存」来用的想法通常是有害的。</li>
<li>禁用 swap 在内存压力下并不能避免磁盘I/O造成的性能问题,这么做只是让磁盘I/O颠簸的范围从
匿名页面转化到文件页面。这不仅更低效,因为系统能回收的页面的选择范围更有限了,
而且这种做法还可能是加重了内存压力的原因之一。</li>
<li>内核 4.0 版本之前的交换进程(swapper)有一些问题,导致很多人对 swap 有负面印象,
因为它太急于(overeagerness)把页面交换出去。在 4.0 之后的内核上这种情况已经改善了很多。</li>
<li>在 SSD 上,交换出匿名页面的开销和回收文件页面的开销基本上在性能/延迟方面没有区别。
在老式的磁盘上,读取交换文件因为属于随机访问读取所以会更慢,于是设置较低的 <code class="code">
vm.swappiness</code>
可能比较合理(继续读下面关于 <code class="code">
vm.swappiness</code>
的描述)。</li>
<li>禁用 swap 并不能避免在接近 OOM 状态下最终表现出的症状,尽管的确有 swap
的情况下这种症状持续的时间可能会延长。在系统调用 OOM 杀手的时候无论有没有启用 swap
,或者更早/更晚开始调用 OOM 杀手,结果都是一样的:整个系统留在了一种不可预知的状态下。
有 swap 也不能避免这一点。</li>
<li>可以用 cgroup v2 的 <code class="code">
memory.low</code>
相关机制来改善内存压力下 swap 的行为并且
避免发生颠簸。</li>
</ol>
<hr class="docutils"/>
<a aria-controls="053c68ad" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#053c68ad" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="053c68ad">
As part of my work improving kernel memory management and cgroup v2,
I've been talking to a lot of engineers about attitudes towards memory
management, especially around application behaviour under pressure and
operating system heuristics used under the hood for memory management.</blockquote>
<p>我的工作的一部分是改善内核中内存管理和 cgroup v2 相关,所以我和很多工程师讨论过看待内存管理的态度,
尤其是在压力下应用程序的行为和操作系统在底层内存管理中用的基于经验的启发式决策逻辑。</p>
<a aria-controls="8dedecec" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#8dedecec" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="8dedecec">
A repeated topic in these discussions has been swap.
Swap is a hotly contested and poorly understood topic,
even by those who have been working with Linux for many years.
Many see it as useless or actively harmful: a relic of a time where
memory was scarce, and disks were a necessary evil to provide much-needed
space for paging. This is a statement that I still see being batted
around with relative frequency in recent years, and I've had many
discussions with colleagues, friends, and industry peers to help them
understand why swap is still a useful concept on modern computers with
significantly more physical memory available than in the past.</blockquote>
<p>在这种讨论中经常重复的话题是交换区(swap)。交换区的话题是非常有争议而且很少被理解的话题,甚至包括那些在
Linux 上工作过多年的人也是如此。很多人觉得它没什么用甚至是有害的:它是历史遗迹,从内存紧缺而
磁盘读写是必要之恶的时代遗留到现在,为计算机提供在当年很必要的页面交换功能作为内存空间。
最近几年我还经常能以一定频度看到这种论调,然后我和很多同事、朋友、业界同行们讨论过很多次,
帮他们理解为什么在现代计算机系统中交换区仍是有用的概念,即便现在的电脑中物理内存已经远多于过去。</p>
<a aria-controls="b1c86306" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#b1c86306" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="b1c86306">
There's also a lot of misunderstanding about the purpose of swap –
many people just see it as a kind of "slow extra memory" for use in emergencies,
but don't understand how it can contribute during normal load to the healthy
operation of an operating system as a whole.</blockquote>
<p>围绕交换区的目的还有很多误解——很多人觉得它只是某种为了应对紧急情况的「慢速额外内存」,
但是没能理解在整个操作系统健康运作的时候它也能改善普通负载的性能。</p>
<a aria-controls="3603c7fa" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#3603c7fa" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="3603c7fa">
Many of us have heard most of the usual tropes about memory:
" <a class="reference external" href="https://www.linuxatemyram.com/">Linux uses too much memory</a> ",
" <a class="reference external" href="https://superuser.com/a/111510">swap should be double your physical memory size</a>
", and the like. While these are either trivial to dispel,
or discussion around them has become more nuanced in recent years,
the myth of "useless" swap is much more grounded in heuristics and
arcana rather than something that can be explained by simple analogy,
and requires somewhat more understanding of memory management to reason about.</blockquote>
<p>我们很多人也听说过描述内存时所用的常见说法: 「 <a class="reference external" href="https://www.linuxatemyram.com/">Linux 用了太多内存</a>
」,「 <a class="reference external" href="https://superuser.com/a/111510">swap 应该设为物理内存的两倍大小</a> 」,或者类似的说法。
虽然这些误解要么很容易化解,或者关于他们的讨论在最近几年已经逐渐变得琐碎,但是关于「无用」交换区
的传言有更深的经验传承的根基,而不是一两个类比就能解释清楚的,并且要探讨这个先得对内存管理有
一些基础认知。</p>
<a aria-controls="ff09462c" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#ff09462c" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="ff09462c">
This post is mostly aimed at those who administrate Linux systems and
are interested in hearing the counterpoints to running with
undersized/no swap or running with vm.swappiness set to 0.</blockquote>
<p>本文主要目标是针对那些管理 Linux 系统并且有兴趣理解「让系统运行于低/无交换区状态」或者「把
<code class="code">
vm.swappiness</code>
设为 0 」这些做法的反论。</p>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id23">背景</a></h2>
<a aria-controls="283c5b39" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#283c5b39" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="283c5b39">
It's hard to talk about why having swap and swapping out pages are good
things in normal operation without a shared understanding of some of
the basic underlying mechanisms at play in Linux memory management,
so let's make sure we're on the same page.</blockquote>
<p>如果没有基本理解 Linux 内存管理的底层机制是如何运作的,就很难讨论为什么需要交换区以及交换出页面
对正常运行的系统为什么是件好事,所以我们先确保大家有讨论的基础。</p>
<div class="section" id="id7">
<h3><a class="toc-backref" href="#id24">内存的类型</a></h3>
<a aria-controls="b93d3e73" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#b93d3e73" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="b93d3e73">
There are many different types of memory in Linux, and each type has its
own properties. Understanding the nuances of these is key to understanding
why swap is important.</blockquote>
<p>Linux 中内存分为好几种类型,每种都有各自的属性。想理解为什么交换区很重要的关键一点在于理解这些的细微区别。</p>
<a aria-controls="ea5c81cf" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#ea5c81cf" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="ea5c81cf">
For example, there are <strong>pages ("blocks" of memory, typically 4k)</strong>
responsible for holding the code for each process being run on your computer.
There are also pages responsible for caching data and metadata related to
files accessed by those programs in order to speed up future access.
These are part of the <strong>page cache</strong> , and I will refer to them as file memory.</blockquote>
<p>比如说,有种 <strong>页面(「整块」的内存,通常 4K)</strong> 是用来存放电脑里每个程序运行时各自的代码的。
也有页面用来保存这些程序所需要读取的文件数据和元数据的缓存,以便加速随后的文件读写。
这些内存页面构成 <strong>页面缓存(page cache)</strong>,后文中我称他们为文件内存。</p>
<a aria-controls="a8e582f2" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a8e582f2" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="a8e582f2">
There are also pages which are responsible for the memory allocations
made inside that code, for example, when new memory that has been allocated
with <code class="code">
malloc</code>
is written to, or when using <code class="code">
mmap</code>
's
<code class="code">
MAP_ANONYMOUS</code>
flag. These are "anonymous" pages –
so called because they are not backed by anything –
and I will refer to them as anon memory.</blockquote>
<p>还有一些页面是在代码执行过程中做的内存分配得到的,比如说,当代码调用 <code class="code">
malloc</code>
能分配到新内存区,或者使用 <code class="code">
mmap</code>
的 <code class="code">
MAP_ANONYMOUS</code>
标志分配的内存。
这些是「匿名(anonymous)」页面——之所以这么称呼它们是因为他们没有任何东西作后备——
后文中我称他们为匿名内存。</p>
<a aria-controls="38d36a1d" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#38d36a1d" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="38d36a1d">
There are other types of memory too –
shared memory, slab memory, kernel stack memory, buffers, and the like –
but anonymous memory and file memory are the most well known and
easy to understand ones, so I will use these in my examples,
although they apply equally to these types too.</blockquote>
<p>还有其它类型的内存——共享内存、slab内存、内核栈内存、文件缓冲区(buffers),这种的——
但是匿名内存和文件内存是最知名也最好理解的,所以后面的例子里我会用这两个说明,
虽然后面的说明也同样适用于别的这些内存类型。</p>
</div>
<div class="section" id="id8">
<h3><a class="toc-backref" href="#id25">可回收/不可回收内存</a></h3>
<a aria-controls="896d577c" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#896d577c" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="896d577c">
One of the most fundamental questions when thinking about a particular type
of memory is whether it is able to be reclaimed or not.
"Reclaim" here means that the system can, without losing data,
purge pages of that type from physical memory.</blockquote>
<p>考虑某种内存的类型时,一个非常基础的问题是这种内存是否能被回收。
「回收(Reclaim)」在这里是指系统可以,在不丢失数据的前提下,从物理内存中释放这种内存的页面。</p>
<a aria-controls="c8915ce3" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#c8915ce3" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="c8915ce3">
For some page types, this is typically fairly trivial. For example,
in the case of clean (unmodified) page cache memory,
we're simply caching something that we have on disk for performance,
so we can drop the page without having to do any special operations.</blockquote>
<p>对一些内存类型而言,是否可回收通常可以直接判断。比如对于那些干净(未修改)的页面缓存内存,
我们只是为了性能在用它们缓存一些磁盘上现有的数据,所以我们可以直接扔掉这些页面,
不需要做什么特殊的操作。</p>
<a aria-controls="543b6f9b" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#543b6f9b" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="543b6f9b">
For some page types, this is possible, but not trivial. For example,
in the case of dirty (modified) page cache memory, we can't just drop the page,
because the disk doesn't have our modifications yet.
As such we either need to deny reclamation or first get our changes back to
disk before we can drop this memory.</blockquote>
<p>对有些内存类型而言,回收是可能的,但是不是那么直接。比如对脏(修改过)的页面缓存内存,
我们不能直接扔掉这些页面,因为磁盘上还没有写入我们所做的修改。这种情况下,我们可以选择拒绝回收,
或者选择先等待我们的变更写入磁盘之后才能扔掉这些内存。</p>
<a aria-controls="5111480d" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#5111480d" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="5111480d">
For some page types, this is not possible. For example,
in the case of the anonymous pages mentioned previously,
they only exist in memory and in no other backing store,
so they have to be kept there.</blockquote>
<p>对还有些内存类型而言,是不能回收的。比如前面提到的匿名页面,它们只存在于内存中,没有任何后备存储,
所以它们必须留在内存里。</p>
</div>
</div>
<div class="section" id="id9">
<h2><a class="toc-backref" href="#id26">说到交换区的本质</a></h2>
<a aria-controls="99551744" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#99551744" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="99551744">
<p>If you look for descriptions of the purpose of swap on Linux,
you'll inevitably find many people talking about it as if it is merely
an extension of the physical RAM for use in emergencies. For example,
here is a random post I got as one of the top results from typing
"what is swap" in Google:</p>
<blockquote>
Swap is essentially emergency memory; a space set aside for times
when your system temporarily needs more physical memory than you
have available in RAM. It's considered "bad" in the sense that
it's slow and inefficient, and if your system constantly needs
to use swap then it obviously doesn't have enough memory. […]
If you have enough RAM to handle all of your needs, and don't
expect to ever max it out, then you should be perfectly safe
running without a swap space.</blockquote>
</blockquote>
<p>如果你去搜 Linux 上交换区的目的的描述,肯定会找到很多人说交换区只是在紧急时用来扩展物理内存的机制。
比如下面这段是我在 google 中输入「什么是 swap」 从前排结果中随机找到的一篇:</p>
<blockquote>
交换区本质上是紧急内存;是为了应对你的系统临时所需内存多余你现有物理内存时,专门分出一块额外空间。
大家觉得交换区「不好」是因为它又慢又低效,并且如果你的系统一直需要使用交换区那说明它明显没有足够的内存。
[……]如果你有足够内存覆盖所有你需要的情况,而且你觉得肯定不会用满内存,那么完全可以不用交换区
安全地运行系统。</blockquote>
<a aria-controls="fe4c8bc1" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#fe4c8bc1" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="fe4c8bc1">
To be clear, I don't blame the poster of this comment at all for the content
of their post – this is accepted as "common knowledge" by a lot of
Linux sysadmins and is probably one of the most likely things that you will
hear from one if you ask them to talk about swap. It is unfortunately also,
however, a misunderstanding of the purpose and use of swap, especially on
modern systems.</blockquote>
<p>事先说明,我不想因为这些文章的内容责怪这些文章的作者——这些内容被很多 Linux 系统管理员认为是「常识」,
并且很可能你问他们什么是交换区的时候他们会给你这样的回答。但是也很不幸的是,
这种认识是使用交换区的目的的一种普遍误解,尤其在现代系统上。</p>
<a aria-controls="77407587" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#77407587" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="77407587">
Above, I talked about reclamation for anonymous pages being "not possible",
as anonymous pages by their nature have no backing store to fall back to
when being purged from memory – as such, their reclamation would result in
complete data loss for those pages. What if we could create such a
store for these pages, though?</blockquote>
<p>前文中我说过回收匿名页面的内存是「不可能的」,因为匿名内存的特点,把它们从内存中清除掉之后,
没有别的存储区域能作为他们的备份——因此,要回收它们会造成数据丢失。但是,如果我们为这种内存页面创建
一种后备存储呢?</p>
<a aria-controls="75be861f" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#75be861f" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="75be861f">
Well, this is precisely what swap is for. Swap is a storage area for these
seemingly "unreclaimable" pages that allows us to page them out to
a storage device on demand. This means that they can now be considered as
equally eligible for reclaim as their more trivially reclaimable friends,
like clean file pages, allowing more efficient use of available physical memory.</blockquote>
<p>嗯,这正是交换区存在的意义。交换区是一块存储空间,用来让这些看起来「不可回收」的内存页面在需要的时候
可以交换到存储设备上。这意味着有了交换区之后,这些匿名页面也和别的那些可回收内存一样,
可以作为内存回收的候选,就像干净文件页面,从而允许更有效地使用物理内存。</p>
<a aria-controls="aa3951ca" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#aa3951ca" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="aa3951ca">
<strong>Swap is primarily a mechanism for equality of reclamation,</strong>
<strong>not for emergency "extra memory". Swap is not what makes your application</strong>
<strong>slow – entering overall memory contention is what makes your application slow.</strong></blockquote>
<p><strong>交换区主要是为了平等的回收机制,而不是为了紧急情况的「额外内存」。使用交换区不会让你的程序变慢——</strong>
<strong>进入内存竞争的状态才是让程序变慢的元凶。</strong></p>
<a aria-controls="526740fe" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#526740fe" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="526740fe">
So in what situations under this "equality of reclamation"
scenario would we legitimately choose to reclaim anonymous pages?
Here are, abstractly, some not uncommon scenarios:</blockquote>
<p>那么在这种「平等的可回收机遇」的情况下,让我们选择回收匿名页面的行为在何种场景中更合理呢?
抽象地说,比如在下述不算罕见的场景中:</p>
<a aria-controls="a12ad6df" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a12ad6df" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="a12ad6df">
<ol class="arabic simple">
<li>During initialisation, a long-running program may allocate and
use many pages. These pages may also be used as part of shutdown/cleanup,
but are not needed once the program is "started" (in an
application-specific sense). This is fairly common for daemons which
have significant dependencies to initialise.</li>
<li>During the program's normal operation, we may allocate memory which is
only used rarely. It may make more sense for overall system performance
to require a <strong>major fault</strong> to page these in from disk on demand,
instead using the memory for something else that's more important.</li>
</ol>
</blockquote>
<ol class="arabic simple">
<li>程序初始化的时候,那些长期运行的程序可能要分配和使用很多页面。这些页面可能在最后的关闭/清理的
时候还需要使用,但是在程序「启动」之后(以具体的程序相关的方式)持续运行的时候不需要访问。
对后台服务程序来说,很多后台程序要初始化不少依赖库,这种情况很常见。</li>
<li>在程序的正常运行过程中,我们可能分配一些很少使用的内存。对整体系统性能而言可能比起让这些内存页
一直留在内存中,只有在用到的时候才按需把它们用 <strong>缺页异常(major fault)</strong> 换入内存,
可以空出更多内存留给更重要的东西。</li>
</ol>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://www.youtube.com/watch?v=ikZ8_mRotT4">cgroupv2: Linux's new unified control group hierarchy (QCON London 2017)</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/ikZ8_mRotT4"></iframe></div></div>
</div>
</div>
<div class="section" id="id10">
<h2><a class="toc-backref" href="#id27">考察有无交换区时会发生什么</a></h2>
<a aria-controls="fc974b28" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#fc974b28" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="fc974b28">
Let's look at typical situations, and how they perform with and without
swap present. I talk about metrics around "memory contention" in my
<a class="reference external" href="https://www.youtube.com/watch?v=ikZ8_mRotT4">talk on cgroup v2</a> .</blockquote>
<p>我们来看一些在常见场景中,有无交换区时分别会如何运行。
在我的 <a class="reference external" href="https://www.youtube.com/watch?v=ikZ8_mRotT4">关于 cgroup v2 的演讲</a>
中探讨过「内存竞争」的指标。</p>
<div class="section" id="id11">
<h3><a class="toc-backref" href="#id28">在无/低内存竞争的状态下</a></h3>
<a aria-controls="a577bbc2" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a577bbc2" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="a577bbc2">
<ul class="simple">
<li><strong>With swap:</strong> We can choose to swap out rarely-used anonymous memory that
may only be used during a small part of the process lifecycle,
allowing us to use this memory to improve cache hit rate,
or do other optimisations.</li>
<li><strong>Without swap</strong> We cannot swap out rarely-used anonymous memory,
as it's locked in memory. While this may not immediately
present as a problem, on some workloads this may represent
a non-trivial drop in performance due to stale,
anonymous pages taking space away from more important use.</li>
</ul>
</blockquote>
<ul class="simple">
<li><strong>有交换区:</strong> 我们可以选择换出那些只有在进程生存期内很小一部分时间会访问的匿名内存,
这允许我们空出更多内存空间用来提升缓存命中率,或者做别的优化。</li>
<li><strong>无交换区:</strong> 我们不能换出那些很少使用的匿名内存,因为它们被锁在了内存中。虽然这通常不会直接表现出问题,
但是在一些工作条件下这可能造成卡顿导致不平凡的性能下降,因为匿名内存占着空间不能给
更重要的需求使用。</li>
</ul>
<div class="panel panel-default">
<div class="panel-heading">
译注:关于 <strong>内存热度</strong> 和 <strong>内存颠簸(thrash)</strong></div>
<div class="panel-body">
<p>讨论内核中内存管理的时候经常会说到内存页的 <strong>冷热</strong> 程度。这里冷热是指历史上内存页被访问到的频度,
内存管理的经验在说,历史上在近期频繁访问的 <strong>热</strong> 内存,在未来也可能被频繁访问,
从而应该留在物理内存里;反之历史上不那么频繁访问的 <strong>冷</strong> 内存,在未来也可能很少被用到,
从而可以考虑交换到磁盘或者丢弃文件缓存。</p>
<p><strong>颠簸(thrash)</strong> 这个词在文中出现多次但是似乎没有详细介绍,实际计算机科学专业的课程中应该有讲过。
一段时间内,让程序继续运行所需的热内存总量被称作程序的工作集(workset),估算工作集大小,
换句话说判断进程分配的内存页中哪些属于 <strong>热</strong> 内存哪些属于 <strong>冷</strong> 内存,是内核中
内存管理的最重要的工作。当分配给程序的内存大于工作集的时候,程序可以不需要等待I/O全速运行;
而当分配给程序的内存不足以放下整个工作集的时候,意味着程序每执行一小段就需要等待换页或者等待
磁盘缓存读入所需内存页,产生这种情况的时候,从用户角度来看可以观察到程序肉眼可见的「卡顿」。
当系统中所有程序都内存不足的时候,整个系统都处于颠簸的状态下,响应速度直接降至磁盘I/O的带宽。
如本文所说,禁用交换区并不能防止颠簸,只是从等待换页变成了等待文件缓存,
给程序分配超过工作集大小的内存才能防止颠簸。</p>
</div>
</div>
</div>
<div class="section" id="id12">
<h3><a class="toc-backref" href="#id29">在中/高内存竞争的状态下</a></h3>
<a aria-controls="266f0b15" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#266f0b15" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="266f0b15">
<ul class="simple">
<li><strong>With swap:</strong> All memory types have an equal possibility of being reclaimed.
This means we have more chance of being able to reclaim pages
successfully – that is, we can reclaim pages that are not quickly
faulted back in again (thrashing).</li>
<li><strong>Without swap</strong> Anonymous pages are locked into memory as they have nowhere to go.
The chance of successful long-term page reclamation is lower,
as we have only some types of memory eligible to be reclaimed
at all. The risk of page thrashing is higher. The casual
reader might think that this would still be better as it might
avoid having to do disk I/O, but this isn't true –
we simply transfer the disk I/O of swapping to dropping
hot page caches and dropping code segments we need soon.</li>
</ul>
</blockquote>
<ul class="simple">
<li><strong>有交换区:</strong> 所有内存类型都有平等的被回收的可能性。这意味着我们回收页面有更高的成功率——
成功回收的意思是说被回收的那些页面不会在近期内被缺页异常换回内存中(颠簸)。</li>
<li><strong>无交换区:</strong> 匿名内存因为无处可去所以被锁在内存中。长期内存回收的成功率变低了,因为我们成体上
能回收的页面总量少了。发生缺页颠簸的危险性更高了。缺乏经验的读者可能觉得这某时也是好事,
因为这能避免进行磁盘I/O,但是实际上不是如此——我们只是把交换页面造成的磁盘I/O变成了扔掉热缓存页
和扔掉代码段,这些页面很可能马上又要从文件中读回来。</li>
</ul>
</div>
<div class="section" id="id13">
<h3><a class="toc-backref" href="#id30">在临时内存占用高峰时</a></h3>
<a aria-controls="bf276ac9" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#bf276ac9" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="bf276ac9">
<ul class="simple">
<li><strong>With swap:</strong> We're more resilient to temporary spikes, but in cases of
severe memory starvation, the period from memory thrashing beginning
to the OOM killer may be prolonged. We have more visibility into the
instigators of memory pressure and can act on them more reasonably,
and can perform a controlled intervention.</li>
<li><strong>Without swap</strong> The OOM killer is triggered more quickly as anonymous
pages are locked into memory and cannot be reclaimed. We're more likely to
thrash on memory, but the time between thrashing and OOMing is reduced.
Depending on your application, this may be better or worse. For example,
a queue-based application may desire this quick transfer from thrashing
to killing. That said, this is still too late to be really useful –
the OOM killer is only invoked at moments of severe starvation,
and relying on this method for such behaviour would be better replaced
with more opportunistic killing of processes as memory contention
is reached in the first place.</li>
</ul>
</blockquote>
<ul class="simple">
<li><strong>有交换区:</strong> 我们对内存使用激增的情况更有抵抗力,但是在严重的内存不足的情况下,
从开始发生内存颠簸到 OOM 杀手开始工作的时间会被延长。内存压力造成的问题更容易观察到,
从而可能更有效地应对,或者更有机会可控地干预。</li>
<li><strong>无交换区:</strong> 因为匿名内存被锁在内存中了不能被回收,所以 OOM 杀手会被更早触发。
发生内存颠簸的可能性更大,但是发生颠簸之后到 OOM 解决问题的时间间隔被缩短了。
基于你的程序,这可能更好或是更糟。比如说,基于队列的程序可能更希望这种从颠簸到杀进程的转换更快发生。
即便如此,发生 OOM 的时机通常还是太迟于是没什么帮助——只有在系统极度内存紧缺的情况下才会请出
OOM 杀手,如果想依赖这种行为模式,不如换成更早杀进程的方案,因为在这之前已经发生内存竞争了。</li>
</ul>
</div>
<div class="section" id="id14">
<h3><a class="toc-backref" href="#id31">好吧,所以我需要系统交换区,但是我该怎么为每个程序微调它的行为?</a></h3>
<a aria-controls="7f9321c9" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#7f9321c9" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="7f9321c9">
You didn't think you'd get through this entire post without me plugging cgroup v2, did you? ;-)</blockquote>
<p>你肯定想到了我写这篇文章一定会在哪儿插点 cgroup v2 的安利吧? ;-)</p>
<a aria-controls="3fcbadba" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#3fcbadba" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="3fcbadba">
Obviously, it's hard for a generic heuristic algorithm to be right all the time,
so it's important for you to be able to give guidance to the kernel.
Historically the only tuning you could do was at the system level,
using <code class="code">
vm.swappiness</code>
. This has two problems: <code class="code">
vm.swappiness</code>
is incredibly hard to reason about because it only feeds in as
a small part of a larger heuristic system, and it also is system-wide
instead of being granular to a smaller set of processes.</blockquote>
<p>显然,要设计一种对所有情况都有效的启发算法会非常难,所以给内核提一些指引就很重要。
历史上我们只能在整个系统层面做这方面微调,通过 <code class="code">
vm.swappiness</code>
。这有两方面问题:
<code class="code">
vm.swappiness</code>
的行为很难准确控制,因为它只是传递给一个更大的启发式算法中的一个小参数;
并且它是一个系统级别的设置,没法针对一小部分进程微调。</p>
<a aria-controls="dc0990d3" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#dc0990d3" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="dc0990d3">
You can also use <code class="code">
mlock</code>
to lock pages into memory, but this requires
either modifying program code, fun with <code class="code">
LD_PRELOAD</code>
, or doing
horrible things with a debugger at runtime.
In VM-based languages this also doesn't work very well, since you
generally have no control over allocation and end up having to
<code class="code">
mlockall</code>
, which has no precision towards the pages
you actually care about.</blockquote>
<p>你可以用 <code class="code">
mlock</code>
把页面锁在内存里,但是这要么必须改程序代码,或者折腾
<code class="code">
LD_PRELOAD</code>
,或者在运行期用调试器做一些魔法操作。对基于虚拟机的语言来说这种方案也不能
很好工作,因为通常你没法控制内存分配,最后得用上 <code class="code">
mlockall</code>
,而这个没有办法精确指定你实际上想锁住的页面。</p>
<a aria-controls="ee392737" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#ee392737" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="ee392737">
cgroup v2 has a tunable per-cgroup in the form of <code class="code">
memory.low</code>
, which allows us to tell the kernel to prefer other applications for
reclaim below a certain threshold of memory used. This allows us to not
prevent the kernel from swapping out parts of our application,
but prefer to reclaim from other applications under memory contention.
Under normal conditions, the kernel's swap logic is generally pretty good,
and allowing it to swap out pages opportunistically generally increases
system performance. Swap thrash under heavy memory contention is not ideal,
but it's more a property of simply running out of memory entirely than
a problem with the swapper. In these situations, you typically want to
fail fast by self-killing non-critical processes when memory pressure
starts to build up.</blockquote>
<p>cgroup v2 提供了一套可以每个 cgroup 微调的 <code class="code">
memory.low</code>
,允许我们告诉内核说当使用的内存低于一定阈值之后优先回收别的程序的内存。这可以让我们不强硬禁止内核
换出程序的一部分内存,但是当发生内存竞争的时候让内核优先回收别的程序的内存。在正常条件下,
内核的交换逻辑通常还是不错的,允许它有条件地换出一部分页面通常可以改善系统性能。在内存竞争的时候
发生交换颠簸虽然不理想,但是这更多地是单纯因为整体内存不够了,而不是因为交换进程(swapper)导致的问题。
在这种场景下,你通常希望在内存压力开始积攒的时候通过自杀一些非关键的进程的方式来快速退出(fail fast)。</p>
<a aria-controls="d789770a" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#d789770a" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="d789770a">
You can not simply rely on the OOM killer for this. The OOM killer is
only invoked in situations of dire failure when we've already entered
a state where the system is severely unhealthy and may well have been
so for a while. You need to opportunistically handle the situation yourself
before ever thinking about the OOM killer.</blockquote>
<p>你不能依赖 OOM 杀手达成这个。 OOM 杀手只有在非常急迫的情况下才会出动,那时系统已经处于极度不健康的
状态了,而且很可能在这种状态下保持了一阵子了。需要在开始考虑 OOM 杀手之前,积极地自己处理这种情况。</p>
<a aria-controls="fe0515e4" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#fe0515e4" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="fe0515e4">
Determination of memory pressure is somewhat difficult using traditional
Linux memory counters, though. We have some things which seem somewhat related,
but are merely tangential – memory usage, page scans, etc – and from these
metrics alone it's very hard to tell an efficient memory configuration
from one that's trending towards memory contention. There is a group of us
at Facebook, spearheaded by <a class="reference external" href="https://patchwork.kernel.org/project/LKML/list/?submitter=45">Johannes</a>
, working on developing new metrics that expose memory pressure more easily
that should help with this in future. If you're interested in hearing more
about this,
<a class="reference external" href="https://youtu.be/ikZ8_mRotT4?t=2145">I go into detail about one metric being considered in my talk on cgroup v2</a>.</blockquote>
<p>不过,用传统的 Linux 内存统计数据还是挺难判断内存压力的。我们有一些看起来相关的系统指标,但是都
只是支离破碎的——内存用量、页面扫描,这些——单纯从这些指标很难判断系统是处于高效的内存利用率还是
在滑向内存竞争状态。我们在 Facebook 有个团队,由
<a class="reference external" href="https://patchwork.kernel.org/project/LKML/list/?submitter=45">Johannes</a>
牵头,努力开发一些能评价内存压力的新指标,希望能在今后改善目前的现状。
如果你对这方面感兴趣, <a class="reference external" href="https://youtu.be/ikZ8_mRotT4?t=2145">在我的 cgroup v2 的演讲中介绍到一个被提议的指标</a>
。</p>
</div>
</div>
<div class="section" id="id16">
<h2><a class="toc-backref" href="#id32">调优</a></h2>
<div class="section" id="id17">
<h3><a class="toc-backref" href="#id33">那么,我需要多少交换空间?</a></h3>
<a aria-controls="5b5c883c" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#5b5c883c" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="5b5c883c">
In general, the minimum amount of swap space required for optimal
memory management depends on the number of anonymous pages pinned into
memory that are rarely reaccessed by an application, and the value of
reclaiming those anonymous pages. The latter is mostly a question of
which pages are no longer purged to make way for these infrequently
accessed anonymous pages.</blockquote>
<p>通常而言,最优内存管理所需的最小交换空间取决于程序固定在内存中而又很少访问到的匿名页面的数量,
以及回收这些匿名页面换来的价值。后者大体上来说是问哪些页面不再会因为要保留这些很少访问的匿名页面而
被回收掉腾出空间。</p>
<a aria-controls="f070dec0" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#f070dec0" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="f070dec0">
If you have a bunch of disk space and a recent (4.0+) kernel,
more swap is almost always better than less. In older kernels <code class="code">
kswapd</code>
,
one of the kernel processes responsible for managing swap, was historically
very overeager to swap out memory aggressively the more swap you had.
In recent times, swapping behaviour when a large amount of swap space is
available has been significantly improved. If you're running kernel 4.0+,
having a larger swap on a modern kernel should not result in overzealous
swapping. As such, if you have the space, having a swap size of a few GB
keeps your options open on modern kernels.</blockquote>
<p>如果你有足够大的磁盘空间和比较新的内核版本(4.0+),越大的交换空间基本上总是越好的。
更老的内核上 <code class="code">
kswapd</code>
, 内核中负责管理交换区的内核线程,在历史上倾向于有越多交换空间就
急于交换越多内存出去。在最近一段时间,可用交换空间很大的时候的交换行为已经改善了很多。
如果在运行 4.0+ 以后的内核,即便有很大的交换区在现代内核上也不会很激进地做交换。因此,
如果你有足够的容量,现代内核上有个几个 GB 的交换空间大小能让你有更多选择。</p>
<a aria-controls="d981b13d" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#d981b13d" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="d981b13d">
If you're more constrained with disk space, then the answer really
depends on the tradeoffs you have to make, and the nature of the environment.
Ideally you should have enough swap to make your system operate optimally
at normal and peak (memory) load. What I'd recommend is setting up a few
testing systems with 2-3GB of swap or more, and monitoring what happens
over the course of a week or so under varying (memory) load conditions.
As long as you haven't encountered severe memory starvation during that week
– in which case the test will not have been very useful – you will probably
end up with some number of MB of swap occupied. As such, it's probably worth
having at least that much swap available, in addition to a little buffer for
changing workloads. <code class="code">
atop</code>
in logging mode can also show you which applications
are having their pages swapped out in the <code class="code">
SWAPSZ</code>
column, so if you don't
already use it on your servers to log historic server state you probably
want to set it up on these test machines with logging mode as part of this
experiment. This also tells you when your application started swapping out
pages, which you can tie to log events or other key data.</blockquote>
<p>如果你的磁盘空间有限,那么答案更多取决于你愿意做的取舍,以及运行的环境。理想上应该有足够的交换空间
能高效应对正常负载和高峰(内存)负载。我建议先用 2-3GB 或者更多的交换空间搭个测试环境,
然后监视在不同(内存)负载条件下持续一周左右的情况。只要在那一周里没有发生过严重的内存不足——
发生了的话说明测试结果没什么用——在测试结束的时候大概会留有多少 MB 交换区占用。
作为结果说明你至少应该有那么多可用的交换空间,再多加一些以应对负载变化。用日志模式跑 <code class="code">
atop</code>
可以在 <code class="code">
SWAPSZ</code>
栏显示程序的页面被交换出去的情况,所以如果你还没用它记录服务器历史日志的话
,这次测试中可以试试在测试机上用它记录日志。这也会告诉你什么时候你的程序开始换出页面,你可以用这个
对照事件日志或者别的关键数据。</p>
<a aria-controls="c81cfdce" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#c81cfdce" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="c81cfdce">
Another thing worth considering is the nature of the swap medium.
Swap reads tend to be highly random, since we can't reliably predict
which pages will be refaulted and when. On an SSD this doesn't matter much,
but on spinning disks, random I/O is extremely expensive since it requires
physical movement to achieve. On the other hand, refaulting of file pages
is likely less random, since files related to the operation of a single
application at runtime tend to be less fragmented. This might mean that on
a spinning disk you may want to bias more towards reclaiming file pages
instead of swapping out anonymous pages, but again, you need to test and
evaluate how this balances out for your workload.</blockquote>
<p>另一点值得考虑的是交换空间所在存储设备的媒介。读取交换区倾向于很随机,因为我们不能可靠预测什么时候
什么页面会被再次访问。在 SSD 上这不是什么问题,但是在传统磁盘上,随机 I/O 操作会很昂贵,
因为需要物理动作寻道。另一方面,重新加载文件缓存可能不那么随机,因为单一程序在运行期的文件读操作
一般不会太碎片化。这可能意味着在传统磁盘上你想更多地回收文件页面而不是换出匿名页面,但仍旧,
你需要做测试评估在你的工作负载下如何取得平衡。</p>
<div class="panel panel-default">
<div class="panel-heading">
译注:关于休眠到磁盘时的交换空间大小</div>
<div class="panel-body">
原文这里建议交换空间至少是物理内存大小,我觉得实际上不需要。休眠到磁盘的时候内核会写回并丢弃
所有有文件作后备的可回收页面,交换区只需要能放下那些没有文件后备的页面就可以了。
如果去掉文件缓存页面之后剩下的已用物理内存总量能完整放入交换区中,就可以正常休眠。
对于桌面浏览器这种内存大户,通常有很多缓存页可以在休眠的时候丢弃。</div>
</div>
<a aria-controls="34173e8b" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#34173e8b" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="34173e8b">
For laptop/desktop users who want to hibernate to swap, this also needs to
be taken into account – in this case your swap file should be at least
your physical RAM size.</blockquote>
<p>对笔记本/桌面用户如果想要休眠到交换区,这也需要考虑——这种情况下你的交换文件应该至少是物理内存大小。</p>
</div>
<div class="section" id="swappiness">
<h3><a class="toc-backref" href="#id34">我的 swappiness 应该如何设置?</a></h3>
<a aria-controls="02421253" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#02421253" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="02421253">
First, it's important to understand what <code class="code">
vm.swappiness</code>
does.
<code class="code">
vm.swappiness</code>
is a sysctl that biases memory reclaim either towards
reclamation of anonymous pages, or towards file pages. It does this using two
different attributes: <code class="code">
file_prio</code>
(our willingness to reclaim file pages)
and <code class="code">
anon_prio</code>
(our willingness to reclaim anonymous pages).
<code class="code">
vm.swappiness`plays into this, as it becomes the default value for
:code:`anon_prio</code>
, and it also is subtracted from the default value of 200
for <code class="code">
file_prio</code>
, which means for a value of <code class="code">
vm.swappiness = 50</code>
,
the outcome is that <code class="code">
anon_prio</code>
is 50, and <code class="code">
file_prio</code>
is 150
(the exact numbers don't matter as much as their relative weight compared to the other).</blockquote>
<p>首先很重要的一点是,要理解 <code class="code">
vm.swappiness</code>
是做什么的。
<code class="code">
vm.swappiness</code>
是一个 sysctl 用来控制在内存回收的时候,是优先回收匿名页面,
还是优先回收文件页面。内存回收的时候用两个属性: <code class="code">
file_prio</code>
(回收文件页面的倾向)
和 <code class="code">
anon_prio</code>
(回收匿名页面的倾向)。 <code class="code">
vm.swappiness</code>
控制这两个值,
因为它是 <code class="code">
anon_prio</code>
的默认值,然后也是默认 200 减去它之后 <code class="code">
file_prio</code>
的默认值。
意味着如果我们设置 <code class="code">
vm.swappiness = 50</code>
那么结果是 <code class="code">
anon_prio</code>
是 50,
<code class="code">
file_prio</code>
是 150 (这里数值本身不是很重要,重要的是两者之间的权重比)。</p>
<div class="panel panel-default">
<div class="panel-heading">
译注:关于 SSD 上的 swappiness</div>
<div class="panel-body">
<p>原文这里说 SSD 上 swap 和 drop page cache 差不多开销所以 <code class="code">
vm.swappiness = 100</code>
。我觉得实际上要考虑 swap out 的时候会产生写入操作,而 drop page cache 可能不需要写入(
要看页面是否是脏页)。如果负载本身对I/O带宽比较敏感,稍微调低 swappiness 可能对性能更好,
内核的默认值 60 是个不错的默认值。以及桌面用户可能对性能不那么关心,反而更关心 SSD
的写入寿命,虽然说 SSD 写入寿命一般也足够桌面用户,不过调低 swappiness
可能也能减少一部分不必要的写入(因为写回脏页是必然会发生的,而写 swap 可以避免)。
当然太低的 swappiness 会对性能有负面影响(因为太多匿名页面留在物理内存里而降低了缓存命中率)
,这里的权衡也需要根据具体负载做测试。</p>
<p>另外澄清一点误解, swap 分区还是 swap 文件对系统运行时的性能而言没有差别。或许有人会觉得
swap 文件要经过文件系统所以会有性能损失,在译文之前译者说过 Linux 的内存管理子系统基本上独立于文件系统。
实际上 Linux 上的 swapon 在设置 swap 文件作为交换空间的时候会读取一次文件系统元数据,
确定 swap 文件在磁盘上的地址范围,随后运行的过程中做交换就和文件系统无关了。关于 swap
空间是否连续的影响,因为 swap 读写基本是页面单位的随机读写,所以即便连续的 swap 空间(swap
分区)也并不能改善 swap 的性能。希疏文件的地址范围本身不连续,写入希疏文件的空洞需要
文件系统分配磁盘空间,所以在 Linux 上交换文件不能是希疏文件。只要不是希疏文件,
连续的文件内地址范围在磁盘上是否连续(是否有文件碎片)基本不影响能否 swapon 或者使用 swap 时的性能。</p>
</div>
</div>
<a aria-controls="b2897dd9" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#b2897dd9" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="b2897dd9">
This means that, in general, <code class="code">
vm.swappiness</code>
<strong>is simply a ratio of how</strong>
<strong>costly reclaiming and refaulting anonymous memory is compared to file memory</strong>
<strong>for your hardware and workload</strong>. The lower the value, the more you tell the
kernel that infrequently accessed anonymous pages are expensive to swap out
and in on your hardware. The higher the value, the more you tell the kernel
that the cost of swapping anonymous pages and file pages is similar on your
hardware. The memory management subsystem will still try to mostly decide
whether it swaps file or anonymous pages based on how hot the memory is,
but swappiness tips the cost calculation either more towards swapping or
more towards dropping filesystem caches when it could go either way.
On SSDs these are basically as expensive as each other, so setting
<code class="code">
vm.swappiness = 100</code>
(full equality) may work well.
On spinning disks, swapping may be significantly more expensive since
swapping in generally requires random reads, so you may want to
bias more towards a lower value.</blockquote>
<p>这意味着,通常来说 <code class="code">
vm.swappiness</code>
<strong>只是一个比例,用来衡量在你的硬件和工作负载下,</strong>
<strong>回收和换回匿名内存还是文件内存哪种更昂贵</strong> 。设定的值越低,你就是在告诉内核说换出那些不常访问的
匿名页面在你的硬件上开销越昂贵;设定的值越高,你就是在告诉内核说在你的硬件上交换匿名页和
文件缓存的开销越接近。内存管理子系统仍然还是会根据实际想要回收的内存的访问热度尝试自己决定具体是
交换出文件还是匿名页面,只不过 swappiness 会在两种回收方式皆可的时候,在计算开销权重的过程中左右
是该更多地做交换还是丢弃缓存。在 SSD 上这两种方式基本上是同等开销,所以设成
<code class="code">
vm.swappiness = 100</code>
(同等比重)可能工作得不错。在传统磁盘上,交换页面可能会更昂贵,
因为通常需要随机读取,所以你可能想要设低一些。</p>
<a aria-controls="916d89d2" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#916d89d2" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="916d89d2">
The reality is that most people don't really have a feeling about which
their hardware demands, so it's non-trivial to tune this value based on
instinct alone – this is something that you need to test using different
values. You can also spend time evaluating the memory composition of your
system and core applications and their behaviour under mild memory reclamation.</blockquote>
<p>现实是大部分人对他们的硬件需求没有什么感受,所以根据直觉调整这个值可能挺困难的 ——
你需要用不同的值做测试。你也可以花时间评估一下你的系统的内存分配情况和核心应用在大量回收内存的时候的行为表现。</p>
<a aria-controls="1716806c" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#1716806c" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="1716806c">
When talking about <code class="code">
vm.swappiness</code>
, an extremely important change to
consider from recent(ish) times is
<a class="reference external" href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/patch/?id=fe35004fbf9eaf67482b074a2e032abb9c89b1dd">this change to vmscan by Satoru Moriya in 2012</a>
, which changes the way that <code class="code">
vm.swappiness = 0</code>
is handled
quite significantly.</blockquote>
<p>讨论 <code class="code">
vm.swappiness</code>
的时候,一个极为重要需要考虑的修改是(相对)近期在
<a class="reference external" href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/patch/?id=fe35004fbf9eaf67482b074a2e032abb9c89b1dd">2012 年左右 Satoru Moriya 对 vmscan 行为的修改</a>
,它显著改变了代码对 <code class="code">
vm.swappiness = 0</code>
这个值的处理方式。</p>
<a aria-controls="0d73ddd8" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#0d73ddd8" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="0d73ddd8">
Essentially, the patch makes it so that we are extremely biased against
scanning (and thus reclaiming) any anonymous pages at all with
<code class="code">
vm.swappiness = 0</code>
, unless we are already encountering severe
memory contention. As mentioned previously in this post, that's generally
not what you want, since this prevents equality of reclamation prior to
extreme memory pressure occurring, which may actually lead to this
extreme memory pressure in the first place. <code class="code">
vm.swappiness = 1</code>
is the lowest you can go without invoking the special casing for
anonymous page scanning implemented in that patch.</blockquote>
<p>基本上来说这个补丁让我们在 <code class="code">
vm.swappiness = 0</code>
的时候会极度避免扫描(进而回收)匿名页面,
除非我们已经在经历严重的内存抢占。就如本文前面所属,这种行为基本上不会是你想要的,
因为这种行为会导致在发生内存抢占之前无法保证内存回收的公平性,这甚至可能是最初导致发生内存抢占的原因。
想要避免这个补丁中对扫描匿名页面的特殊行为的话, <code class="code">
vm.swappiness = 1</code>
是你能设置的最低值。</p>
<a aria-controls="8283fab5" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#8283fab5" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="8283fab5">
The kernel default here is <code class="code">
vm.swappiness = 60</code>
. This value is
generally not too bad for most workloads, but it's hard to have a
general default that suits all workloads. As such, a valuable extension
to the tuning mentioned in the "how much swap do I need" section above
would be to test these systems with differing values for <code class="code">
vm.swappiness</code>
, and monitor your application and system metrics under heavy (memory) load.
Some time in the near future, once we have a decent implementation of
<a class="reference external" href="https://youtu.be/ikZ8_mRotT4?t=2145">refault detection</a> in the kernel,
you'll also be able to determine this somewhat workload-agnostically by
looking at cgroup v2's page refaulting metrics.</blockquote>
<p>内核在这里设置的默认值是 <code class="code">
vm.swappiness = 60</code>
。这个值对大部分工作负载来说都不会太坏,
但是很难有一个默认值能符合所有种类的工作负载。因此,对上面「 <a class="reference internal" href="#id17">那么,我需要多少交换空间?</a>
」那段讨论的一点重要扩展可以说,在测试系统中可以尝试使用不同的 <code class="code">
vm.swappiness</code>
,然后监视你的程序和系统在重(内存)负载下的性能指标。在未来某天,如果我们在内核中有了合理的
<a class="reference external" href="https://youtu.be/ikZ8_mRotT4?t=2145">缺页检测</a> ,你也将能通过 cgroup v2 的页面缺页
指标来以负载无关的方式决定这个。</p>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://www.youtube.com/watch?v=beefUhRH5lU">SREcon19 Asia/Pacific - Linux Memory Management at Scale: Under the Hood</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/beefUhRH5lU"></iframe></div></div>
</div>
</div>
<div class="section" id="id19">
<h3><a class="toc-backref" href="#id35">2019年07月更新:内核 4.20+ 中的内存压力指标</a></h3>
<a aria-controls="2cbb410f" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#2cbb410f" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="2cbb410f">
The refault metrics mentioned as in development earlier are now in the
kernel from 4.20 onwards and can be enabled with <code class="code">
CONFIG_PSI=y</code>
. See my talk at SREcon at around the 25:05 mark:</blockquote>
<p>前文中提到的开发中的内存缺页检测指标已经进入 4.20+ 以上版本的内核,可以通过
<code class="code">
CONFIG_PSI=y</code>
开启。详情参见我在 SREcon 大约 25:05 左右的讨论。</p>
</div>
</div>
<div class="section" id="id20">
<h2><a class="toc-backref" href="#id36">结论</a></h2>
<a aria-controls="01142bf6" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#01142bf6" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><blockquote class="collapse" id="01142bf6">
<ul class="simple">
<li>Swap is a useful tool to allow equality of reclamation of memory pages,
but its purpose is frequently misunderstood, leading to its negative
perception across the industry. If you use swap in the spirit intended,
though – as a method of increasing equality of reclamation – you'll
find that it's a useful tool instead of a hindrance.</li>
<li>Disabling swap does not prevent disk I/O from becoming a problem under
memory contention, it simply shifts the disk I/O thrashing from anonymous
pages to file pages. Not only may this be less efficient, as we have
a smaller pool of pages to select from for reclaim, but it may also
contribute to getting into this high contention state in the first place.</li>
<li>Swap can make a system slower to OOM kill, since it provides another,
slower source of memory to thrash on in out of memory situations – the
OOM killer is only used by the kernel as a last resort, after things have
already become monumentally screwed. The solutions here depend on your system:<ul>
<li>You can opportunistically change the system workload depending on
cgroup-local or global memory pressure. This prevents getting into these
situations in the first place, but solid memory pressure metrics are
lacking throughout the history of Unix. Hopefully this should be
better soon with the addition of refault detection.</li>
<li>You can bias reclaiming (and thus swapping) away from certain processes
per-cgroup using memory.low, allowing you to protect critical daemons
without disabling swap entirely.</li>
</ul>
</li>
</ul>
</blockquote>
<ul class="simple">
<li>交换区是允许公平地回收内存的有用工具,但是它的目的经常被人误解,导致它在业内这种负面声誉。如果
你是按照原本的目的使用交换区的话——作为增加内存回收公平性的方式——你会发现它是很有效的工具而不是阻碍。</li>
<li>禁用交换区并不能在内存竞争的时候防止磁盘I/O的问题,它只不过把匿名页面的磁盘I/O变成了文件页面的
磁盘I/O。这不仅更低效,因为我们回收内存的时候能选择的页面范围更小了,而且它可能是导致高度内存竞争
状态的元凶。</li>
<li>有交换区会导致系统更慢地使用 OOM 杀手,因为在缺少内存的情况下它提供了另一种更慢的内存,
会持续地内存颠簸——内核调用 OOM 杀手只是最后手段,会晚于所有事情已经被搞得一团糟之后。
解决方案取决于你的系统:<ul>
<li>你可以预先更具每个 cgroup 的或者系统全局的内存压力改变系统负载。这能防止我们最初进入内存竞争
的状态,但是 Unix 的历史中一直缺乏可靠的内存压力检测方式。希望不久之后在有了
<a class="reference external" href="https://youtu.be/ikZ8_mRotT4?t=2145">缺页检测</a> 这样的性能指标之后能改善这一点。</li>
<li>你可以使用 <code class="code">
memory.low</code>
让内核不倾向于回收(进而交换)特定一些 cgroup 中的进程,
允许你在不禁用交换区的前提下保护关键后台服务。</li>
</ul>
</li>
</ul>
<hr class="docutils"/>
<p>感谢在撰写本文时 <a class="reference external" href="https://github.com/rahulg">Rahul</a> ,
<a class="reference external" href="https://github.com/htejun">Tejun</a> 和
<a class="reference external" href="https://patchwork.kernel.org/project/LKML/list/?submitter=45">Johannes</a>
提供的诸多建议和反馈。</p>
</div>
系统中的大多数文件有多大?2020-06-11T15:45:00+09:002020-06-11T15:45:00+09:00farseerfctag:farseerfc.me,2020-06-11:/zhs/file-size-histogram.html
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id8"><strong>你觉得,你的系统中大多数文件大概有多大?</strong></a></h2>
<p>这是一个很有意思的问题,你可以试着先猜一下。</p>
<p>基于对系统中保存文件的了解,可能有这样的思考过程:</p>
<ul class="simple">
<li>我收藏了好多照片,每个有 2~5MiB 吧。</li>
<li>我下载了好多漫画,每个 100KiB 左右,这些大概占了不少比例。</li>
<li>我还收藏了不少动画电影电视剧,虽然这些文件总数量可能不多?</li>
<li>我下载了 Linux 的源码,那里面每个 C 代码文件都几千行,每行 100 字宽,平均也得有 30KiB
吧,有几万个源码文件呢,占比应该挺大的……</li>
</ul>
<p>问题中「大多数」其实是个挺不精确的称呼,换个精确点的问法:你觉得你的系统中 <strong>文件大小的中位数</strong>
大概在什么范围内?或者说,文件系统中 <strong>文件大小的分布情况</strong> 一般是怎样的曲线?</p>
<p>这个问题其实还有多种别的问法,比如:一个常见的桌面或者服务器系统中,多大的文件算大文件,
多小的文件算小文件,什么范围内的大小算是普通呢?</p>
<p>经历过基本的科学教育的人 …</p></div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id8"><strong>你觉得,你的系统中大多数文件大概有多大?</strong></a></h2>
<p>这是一个很有意思的问题,你可以试着先猜一下。</p>
<p>基于对系统中保存文件的了解,可能有这样的思考过程:</p>
<ul class="simple">
<li>我收藏了好多照片,每个有 2~5MiB 吧。</li>
<li>我下载了好多漫画,每个 100KiB 左右,这些大概占了不少比例。</li>
<li>我还收藏了不少动画电影电视剧,虽然这些文件总数量可能不多?</li>
<li>我下载了 Linux 的源码,那里面每个 C 代码文件都几千行,每行 100 字宽,平均也得有 30KiB
吧,有几万个源码文件呢,占比应该挺大的……</li>
</ul>
<p>问题中「大多数」其实是个挺不精确的称呼,换个精确点的问法:你觉得你的系统中 <strong>文件大小的中位数</strong>
大概在什么范围内?或者说,文件系统中 <strong>文件大小的分布情况</strong> 一般是怎样的曲线?</p>
<p>这个问题其实还有多种别的问法,比如:一个常见的桌面或者服务器系统中,多大的文件算大文件,
多小的文件算小文件,什么范围内的大小算是普通呢?</p>
<p>经历过基本的科学教育的人,大概会做这样的基于科学假设的猜测:</p>
<ul class="simple">
<li>统计学上说,大量独立随机事件的累积概率满足正态分布(常态分布)曲线。假设我们把某个特定文件的大小增长
1字节看作是一次独立随机事件,那么文件大小在文件系统中应该是满足正态分布的?</li>
<li>正态分布的前提下,平均数接近中位数,文件系统的已占用大小除以文件数量大概就是大部分文件的大小了吧。</li>
<li>根据我现在文件系统的占用大小和文件数量,平均数大概是 500KiB 左右?</li>
<li>虽然我还存了几个非常大,上 GiB 的文件,但是看起来似乎也有很多很多非常小的文件,
平均一下的话应该会把平均数拉大,大于中位数吧。那么中位数应该在 100KiB 这样的量级附近?</li>
</ul>
<p>你说为什么要关心这个?因为我经常在网上看到这样的讨论:</p>
<p>「我有个仓库盘要存很多下载到的漫画,每个漫画都是一个文件夹里面一堆 <strong>小 JPG</strong> ,每个就几十 KiB
。网上看到的说法是 XFS 对 <strong>小文件</strong> 的性能不那么好,我是不是该换 EXT4 ?我还想在 Windows
上能读写,是不是 ExFAT 这种简单的文件系统更合适一点?」</p>
<p>「软件源的镜像服务器需要存的都是些 <strong>小文件</strong> 吧,大多数软件包压缩后也就是几个 KiB 到几个
MiB 的量级,这种需求是不是适合用对 <strong>小文件</strong> 优化比较好的文件系统?」</p>
<p>「我的程序需要分析的数据是大量几百K的 <strong>小文件</strong> ,该怎么存合适呢,直接用文件系统还是应该上数据库?
我还想多线程并发分析,是不是 SQL 数据库的并发能力强一些?又或者 MongoDB 的 GridFS
看起来似乎能结合文件系统和数据库的特点,选它应该还不错?」</p>
<p>有没有觉得上面这些讨论和直觉有些出入?如果你的直觉告诉你,上面的讨论似乎很自然的话,
那说明你需要继续看下去了。</p>
<p>好了写了这么多废话给大家思考时间,现在请回答一下我标题中那个问题,
<strong>你觉得,你的系统中大多数文件大概有多大?</strong> ,接下来我要揭晓答案了。</p>
</div>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id9">统计实际系统中文件大小的学术研究</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://youtu.be/lTE26gkeVUs?t=452">A Study of Practical Deduplication</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/lTE26gkeVUs"></iframe></div></div>
</div>
<p>最近看到一个挺早以前的研究报告,是 <a class="reference external" href="https://www.usenix.org/legacy/events/fast11/">FAST'11</a>
的最优秀论文奖,研究的课题叫 <a class="reference external" href="https://www.usenix.org/legacy/event/fast11/tech/full_papers/Meyer.pdf">《A Study of Practical Deduplication》</a>
。这个研究原本是想考察一下在桌面文件系统中「去重」(deduplication)的可行性和潜在收益,作为背景调查,
他们收集了一个挺大的调查样本,记录文件大小和校验和之类的。从论文摘要看,他们在微软公司内,
通过邮件的形式让微软员工在各自的工作机上执行他们的调查程序,大概在1个月左右的时间内收集到了 857
份调查结果。关于去重的研究结果这里我们这里先不深究,只看这个背景调查,他们对收集到的文件大小画了个图表:</p>
<img alt="file-histogram-4k.jpg" class="img-responsive" src="//farseerfc.me/zhs/images/file-histogram-4k.jpg"/>
<p>他们结果显示最常见的文件大小是 <strong>4K</strong> !</p>
<p>注意上图里的横轴座标,是按2的指数来给文件大小分类的。比如 128~256 字节的算一类, 4K~8K
字节的算一类,分类之后统计每一类里面文件的数量所占比例,也就是说横轴座标是指数增长的。
在指数增长的横轴座标上,画出的曲线才看起来像是正态分布的曲线,如果把横轴座标画成线性的话,
中位数会出现在非常靠近左侧小文件的地方。</p>
<p>也就是说根据他们的统计,文件系统中大部分文件都是大概 2K 到 8K 这样的范围,最常见 4K 大小。
非常大的比如 8M 以上的文件只是极个别,位于图表右侧非常长的尾巴中。</p>
<p>其实我对这个结果还不是很惊讶,因为我记得在 2000 年左右,当我家的电脑还在用 Windows 98 跑在
40G 的 FAT32 文件系统中的时候,读到过一篇介绍 NTFS 的「新」特性的文章。那篇文章讲到 FAT32
的簇大小随着分区大小增长,越来越大的簇大小对保存大量小文件极其浪费,而 NTFS 用固定的 4K
簇大小可避免这样的浪费,并且 1K MFT 记录甚至能「内联(inline)」存储非常小的文件。
为了证明大量小文件对文件系统是个现实存在的问题,那篇文章也提到了常见系统中的文件大小分布曲线,
提到了大部分文件都是 4K 大小这有点反直觉的结论。</p>
<p>这次这个研究让我觉得吃惊的是,文件大小分布并没有随着硬盘大小的增加而增加,稳定在了 4K 这个数字上。
他们以前还进行过两次类似的统计,分别在 2000 年和 2004 年,图中的点线画出了历史上的统计分布,实线是
2009 年的最新统计。三年获得的统计结果的曲线基本吻合,这意味着随着存储容量增长,文件大小的分布几乎没有变化。</p>
<p>正当我疑惑,这种文件大小不变的趋势,是否是因为微软公司内特定的操作系统和工作内容,
在别的系统上或者在更长的时间跨度上是否有类似的趋势呢?这时演讲的幻灯片翻了一页:</p>
<img alt="file-histogram-4k-since1981.jpg" class="img-responsive" src="//farseerfc.me/zhs/images/file-histogram-4k-since1981.jpg"/>
<p>从早在 1981 年起,有研究表明文件系统中文件大小中位数就稳定在了 <strong>4K</strong> !</p>
<p>在他们论文的参考文献中,能找到 <a class="reference external" href="https://www.cs.cmu.edu/~satya/docdir/satya-sosp-1981.pdf">这个 1981 年的研究</a>
。这篇早年的调查是在 DEC 的 PDP-10 机器上,使用 TOPS-10 操作系统。从现在的视点来看,被调查的
TOPS-10 的文件系统已经可以说非常初级了,没法支持很大的文件或者很多的文件,
然而即便如此常见文件大小也还是非常符合现代系统中得到的结果。</p>
<p>微软的研究者们还回顾了计算机科学领域多年的相关研究,结论是常见文件大小这个值在 1981 到 2009
这近 30 年中都非常稳定。演讲的原文中这么评价:</p>
<blockquote>
<p>…… 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 ……</p>
<p>文件大小中位数是 4K 。在前几年的两次研究中它也是 4K 。其实我们回顾了既往的学术研究,发现在过去30
年中每个研究都说它是 4K 这个值。这是个好消息,我们终于有了一个堪比物理学家的结论:我们有我们自己的
宇宙基本常数了,是文件大小中位数。</p>
</blockquote>
<p>这个结论很有意思,文件大小中位数在计算机科学领域的稳定程度堪比宇宙基本常数: <strong>4K</strong> !</p>
<p>很明显这是在调侃,文件大小这种变化很大的数字显然和文件系统内存储的内容直接相关,
存游戏的可能不同于存音乐的。但是这调侃的背后也有一定真实性:文件系统中保存的文件,
除了用户直接使用的那些视频、文档、代码,还有大量文件是程序内部创建使用的,比如浏览器的缓存和
cookie ,这类不被用户知晓的文件可能在数量上反而占据绝大多数。
于是从文件系统这边来看,大多数文件都是在 <strong>4K</strong> 左右的数量级,更大的文件是少数。</p>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id10">不信?你可以测一下自己的文件系统</a></h2>
<p>我也想测一下我的文件系统中文件大小的分布情况,于是稍微写了点代码测量和画图。如果你也想知道你的系统中
文件大小的分布,那么可以像我这样测。</p>
<p>首先用 <code class="code">
find</code>
命令统计一下每个文件的大小,输出到一个文件里:</p>
<div class="highlight"><pre><span class="code-line"><span></span>find /home -type f -printf <span class="s2">"%s %p\n"</span> > myhome.txt</span>
</pre></div>
<p>上述命令对 <code class="code">
/home</code>
中的所有普通文件而忽略文件夹和符号链接之类的( <code class="code">
-type f</code>
),输出文件大小字节数和文件路径( <code class="code">
-printf "%s %p\n"</code>
)。
如果文件名路径中有特殊符号可能之后比较难处理,那么可以 <code class="code">
-printf "%s\n"</code>
忽略路径。</p>
<p>然后用 Python 的 Matplotlib 和 NumPy 对收集到的文件大小数据画个直方图(histogram)。
以下 <a class="reference external" href="https://github.com/farseerfc/dotfiles/blob/master/zsh/.local/bin/filesizehistogram.py">filesizehistogram.py 脚本在这儿</a>
能下载到。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="ch">#!/usr/bin/python3</span></span>
<span class="code-line"><span class="kn">import</span> <span class="nn">argparse</span></span>
<span class="code-line"><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="nn">plt</span></span>
<span class="code-line"><span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="nn">np</span></span>
<span class="code-line"><span class="kn">import</span> <span class="nn">sys</span></span>
<span class="code-line"><span class="kn">from</span> <span class="nn">math</span> <span class="kn">import</span> <span class="o">*</span></span>
<span class="code-line"><span class="kn">from</span> <span class="nn">bisect</span> <span class="kn">import</span> <span class="n">bisect_left</span></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">def</span> <span class="nf">numfmt</span><span class="p">(</span><span class="n">s</span><span class="p">):</span></span>
<span class="code-line"> <span class="n">marks</span> <span class="o">=</span> <span class="s2">"KMGTP"</span></span>
<span class="code-line"> <span class="n">m</span> <span class="o">=</span> <span class="mi">0</span></span>
<span class="code-line"> <span class="n">f</span> <span class="o">=</span> <span class="nb">type</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="ow">is</span> <span class="nb">float</span></span>
<span class="code-line"> <span class="k">while</span> <span class="n">s</span> <span class="o">>=</span> <span class="mi">1024</span> <span class="ow">and</span> <span class="n">m</span> <span class="o"><</span> <span class="nb">len</span><span class="p">(</span><span class="n">marks</span><span class="p">):</span></span>
<span class="code-line"> <span class="k">if</span> <span class="n">f</span><span class="p">:</span></span>
<span class="code-line"> <span class="n">s</span> <span class="o">/=</span> <span class="mf">1024.0</span></span>
<span class="code-line"> <span class="k">else</span><span class="p">:</span></span>
<span class="code-line"> <span class="n">s</span> <span class="o">//=</span><span class="mi">1024</span></span>
<span class="code-line"> <span class="n">m</span> <span class="o">+=</span> <span class="mi">1</span></span>
<span class="code-line"> <span class="k">if</span> <span class="n">f</span><span class="p">:</span></span>
<span class="code-line"> <span class="k">return</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">s</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}{</span><span class="n">marks</span><span class="p">[</span><span class="n">m</span><span class="o">-</span><span class="mi">1</span><span class="p">:</span><span class="n">m</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span></span>
<span class="code-line"> <span class="k">else</span><span class="p">:</span></span>
<span class="code-line"> <span class="k">return</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">s</span><span class="si">}{</span><span class="n">marks</span><span class="p">[</span><span class="n">m</span><span class="o">-</span><span class="mi">1</span><span class="p">:</span><span class="n">m</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span></span>
<span class="code-line"> <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span></span>
<span class="code-line"> <span class="n">prog</span> <span class="o">=</span> <span class="s2">"filesizehistogram"</span><span class="p">,</span></span>
<span class="code-line"> <span class="n">description</span> <span class="o">=</span> <span class="s2">"""</span></span>
<span class="code-line"><span class="s2"> can use "-" as input filename, indicate input is taken from stdin.</span></span>
<span class="code-line"><span class="s2"> otherwise input file should be a result of "find -printf </span><span class="se">\'</span><span class="si">%s</span><span class="s2"> %p</span><span class="se">\\</span><span class="s2">n</span><span class="se">\'</span><span class="s2">"</span></span>
<span class="code-line"><span class="s2"> """</span></span>
<span class="code-line"> <span class="p">)</span></span>
<span class="code-line"> <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'-o'</span><span class="p">,</span> <span class="s1">'--output'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"output filename, will recognize common extensions by matplot"</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'input'</span><span class="p">,</span> <span class="n">nargs</span><span class="o">=</span><span class="s1">'+'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"input filenames"</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="n">filenames</span> <span class="o">=</span> <span class="p">[</span><span class="n">x</span> <span class="k">if</span> <span class="n">x</span> <span class="o">!=</span> <span class="s1">'-'</span> <span class="k">else</span> <span class="s1">'/dev/stdin'</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">args</span><span class="o">.</span><span class="n">input</span><span class="p">]</span></span>
<span class="code-line"> <span class="n">data</span><span class="o">=</span><span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s1">' '</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span> <span class="k">for</span> <span class="n">fn</span> <span class="ow">in</span> <span class="n">filenames</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">open</span><span class="p">(</span><span class="n">fn</span><span class="p">)])</span></span>
<span class="code-line"> <span class="n">mindatalog2</span> <span class="o">=</span> <span class="mi">5</span> <span class="c1"># cut from 32</span></span>
<span class="code-line"> <span class="n">maxdatalog2</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">ceil</span><span class="p">(</span><span class="n">log2</span><span class="p">(</span><span class="n">data</span><span class="o">.</span><span class="n">max</span><span class="p">())),</span> <span class="mi">31</span><span class="p">)</span> <span class="c1"># cut at 1G and above</span></span>
<span class="code-line"> <span class="c1"># bins [0, 1, 32, 64, 128, 256, ... , 1G, 2G] , last bin is open range</span></span>
<span class="code-line"> <span class="n">bins</span><span class="o">=</span><span class="p">[</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">,]</span> <span class="o">+</span> <span class="p">[</span><span class="mi">2</span><span class="o">**</span><span class="n">x</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">mindatalog2</span><span class="p">,</span> <span class="n">maxdatalog2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)]</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="n">median</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">median</span><span class="p">(</span><span class="n">data</span><span class="p">))</span></span>
<span class="code-line"> <span class="n">mean</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">data</span><span class="o">.</span><span class="n">mean</span><span class="p">())</span></span>
<span class="code-line"> <span class="n">bmedian</span> <span class="o">=</span> <span class="n">bisect_left</span><span class="p">(</span><span class="n">bins</span><span class="p">,</span> <span class="n">median</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span></span>
<span class="code-line"> <span class="n">bmean</span> <span class="o">=</span> <span class="n">bisect_left</span><span class="p">(</span><span class="n">bins</span><span class="p">,</span> <span class="n">mean</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span></span>
<span class="code-line"> <span class="n">files</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">total</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="n">hist</span><span class="p">,</span> <span class="n">bin_edges</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">histogram</span><span class="p">(</span><span class="n">data</span><span class="p">,</span><span class="n">bins</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">fig</span><span class="p">,</span><span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">20</span><span class="p">,</span><span class="mi">8</span><span class="p">))</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">bar</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">hist</span><span class="p">)),</span> <span class="n">hist</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="mf">0.9</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">set_xticks</span><span class="p">([</span><span class="n">i</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">hist</span><span class="p">))])</span></span>
<span class="code-line"> <span class="n">tickbar</span> <span class="o">=</span> <span class="s2">"┊</span><span class="se">\n</span><span class="s2">"</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">set_xticklabels</span><span class="p">([</span><span class="sa">f</span><span class="s1">'</span><span class="si">{</span><span class="n">tickbar</span><span class="o">*</span><span class="p">(</span><span class="n">i</span><span class="o">%</span><span class="mi">3</span><span class="p">)</span><span class="si">}{</span><span class="n">numfmt</span><span class="p">(</span><span class="n">bins</span><span class="p">[</span><span class="n">i</span><span class="p">])</span><span class="si">}</span><span class="s1">~</span><span class="si">{</span><span class="n">numfmt</span><span class="p">(</span><span class="n">bins</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">])</span><span class="si">}</span><span class="s1">'</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">hist</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">)]</span> <span class="o">+</span></span>
<span class="code-line"> <span class="p">[</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">numfmt</span><span class="p">(</span><span class="n">bins</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">hist</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span><span class="si">}</span><span class="s2">~"</span><span class="p">])</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">axvline</span><span class="p">(</span><span class="n">bmean</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s1">'k'</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="s1">'dashed'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">axvline</span><span class="p">(</span><span class="n">bmedian</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s1">'r'</span><span class="p">,</span> <span class="n">linestyle</span><span class="o">=</span><span class="s1">'dashed'</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">min_ylim</span><span class="p">,</span> <span class="n">max_ylim</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">ylim</span><span class="p">()</span></span>
<span class="code-line"> <span class="n">min_xlim</span><span class="p">,</span> <span class="n">max_xlim</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">xlim</span><span class="p">()</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="n">bmean</span> <span class="o">+</span> <span class="mf">0.5</span> <span class="p">,</span> <span class="n">max_ylim</span> <span class="o">*</span> <span class="mf">0.9</span><span class="p">,</span> <span class="sa">f</span><span class="s1">'Mean: </span><span class="si">{</span><span class="n">numfmt</span><span class="p">(</span><span class="n">mean</span><span class="p">)</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="n">bmedian</span> <span class="o">+</span> <span class="mf">0.5</span> <span class="p">,</span> <span class="n">max_ylim</span> <span class="o">*</span> <span class="mf">0.9</span><span class="p">,</span> <span class="sa">f</span><span class="s1">'Median: </span><span class="si">{</span><span class="n">numfmt</span><span class="p">(</span><span class="n">median</span><span class="p">)</span><span class="si">}</span><span class="s1">'</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s1">'r'</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="n">max_xlim</span> <span class="o">*</span> <span class="mf">0.8</span><span class="p">,</span> <span class="n">max_ylim</span> <span class="o">*</span> <span class="mf">0.9</span><span class="p">,</span> <span class="sa">f</span><span class="s1">'Files: </span><span class="si">{</span><span class="n">files</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="n">max_xlim</span> <span class="o">*</span> <span class="mf">0.9</span><span class="p">,</span> <span class="n">max_ylim</span> <span class="o">*</span> <span class="mf">0.9</span><span class="p">,</span> <span class="sa">f</span><span class="s1">'Total: </span><span class="si">{</span><span class="n">numfmt</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">total</span><span class="p">))</span><span class="si">}</span><span class="s1">'</span><span class="p">)</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">hist</span><span class="p">)):</span></span>
<span class="code-line"> <span class="n">ax</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="n">i</span> <span class="o">-</span> <span class="mf">0.5</span><span class="p">,</span> <span class="n">hist</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">files</span> <span class="o">/</span> <span class="mi">400</span><span class="p">,</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">hist</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="si">:</span><span class="s2">5</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="c1"># label on top of every bar, uplefted a little</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">output</span><span class="p">:</span></span>
<span class="code-line"> <span class="n">plt</span><span class="o">.</span><span class="n">savefig</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">output</span><span class="p">)</span></span>
<span class="code-line"> <span class="k">else</span><span class="p">:</span></span>
<span class="code-line"> <span class="n">plt</span><span class="o">.</span><span class="n">show</span><span class="p">()</span></span>
</pre></div>
<p>然后就能 <code class="code">
./filesizehistogram.py myhome.txt</code>
这样画出一张图。以下是我一台机器上根目录
<code class="code">
/</code>
和家目录 <code class="code">
/home</code>
放在一起的结果:</p>
<img alt="myroot.png" class="img-responsive" src="//farseerfc.me/zhs/images/myroot.png"/>
<p>图中我用点线标出了中位数(median)和平均数(mean)大小的位置,可见在我的文件系统中,
文件大小中位数在 2.24K ,平均数是 88.09K ,512~8K
范围内的文件数量加在一起超过了文件总数一半。文件数量最多的范围是 1K~2K
,大概因为我家里存了好多源代码。还有一个小突起在 64K~128K ,这堆主要是我收藏的漫画 JPG 文件。</p>
<p>图的横座标和上面微软的研究类似,用2倍增长的bin统计文件数量。
不过稍微修改了一下,因为我想知道 0 大小文件的个数,还想把 1~32 和 1G~
以上这两个曲线底端的尾巴放在一起统计。图的纵座标是文件数。</p>
<p>也可以用这个来画你感兴趣的文件夹的文件大小分布,比如用 linux 内核代码树画出来的图大概这样:</p>
<img alt="linux-filesize.png" class="img-responsive" src="//farseerfc.me/zhs/images/linux-filesize.png"/>
<p>linux 代码树的文件大部分比我猜的 30K 要小呢,主要在 1K~16K ,中位数 3.28K
。而且意外得在代码树里有好几个 0 大小的文件,看了几个文件路径确认了一下,它们的确是 0
大小的头文件,并不是我的文件系统丢了文件内容。</p>
</div>
<div class="section" id="id7">
<h2><a class="toc-backref" href="#id11">结论</a></h2>
<p>有没有觉得「文件大小的中位数是 4K 」这个结论出乎意料呢?</p>
<p>你在用的系统中文件大小的分布曲线又是什么样的呢?欢迎留言告诉我。(贴图可以用
<a class="reference external" href="https://fars.ee/f">https://fars.ee/f</a> 图床呀)</p>
<p>知道了文件大小分布的规律,就会发现设计文件系统的时候,需要考虑两个极端情况:
既要照顾到文件系统中数量很少而大小超大的那些文件,又要考虑到这么多数量众多而大小只有数 K
的文件。也会发现,对于文件系统而言,超过 16K 的文件就绝不会被算作是「小文件」了,而文件系统设计中说的
「小文件优化」针对的通常是更小的文件大小。并且这一趋势并不会随着存储设备容量增加而改变,
不能妄图通过随着容量逐步增加文件分配「簇」大小的方式,来简化文件系统设计。</p>
<p>那么众多文件系统实际是如何满足这些极端情况的呢?待我有空再细聊……</p>
</div>
SSD 就是大U盘?聊聊闪存类存储的转换层2020-03-18T15:45:00+09:002020-03-18T15:45:00+09:00farseerfctag:farseerfc.me,2020-03-18:/zhs/flash-storage-ftl-layer.html
<p>上篇 <a class="reference external" href="//farseerfc.me/zhs/history-of-chs-addressing.html">「柱面-磁头-扇区寻址的一些旧事」</a>
整理了一下我对磁盘类存储设备(包括软盘、硬盘,不包括光盘、磁带)的一些理解,
算是为以后讨论文件系统作铺垫;这篇整理一下我对闪存类存储设备的理解。</p>
<p>这里想要讨论的闪存类存储是指 SSD 、SD卡、U盘、手机内置闪存等基于 NAND
又有闪存转换层的存储设备(下文简称闪存盘),但不包括裸 NAND 设备、3D Xpoint (Intel
Optane)等相近物理结构但是没有类似的闪存转换层的存储设备。
闪存类存储设备这几年发展迅猛,SD卡和U盘早就替代软盘成为数据交换的主流, SSD 大有替代硬盘的趋势。
因为发展迅速,所以其底层技术变革很快,不同于磁盘类存储技术有很多公开资料可以获取,
闪存类存储的技术细节通常是厂商们的秘密,互联网上能找到很多外围资料,
但是关于其如何运作的细节却很少提到。所以我想先整理一篇笔记,记下我搜集到的资料,加上我自己的理解。
本文大部分信息来源是 <a class="reference external" href="https://lwn.net/Articles/428584/">Optimizing Linux with cheap flash drives</a>
和 <a class="reference external" href="https://accelazh.github.io/ssd/A-Summary-On-SSD-And-FTL">A Summary on …</a></p>
<p>上篇 <a class="reference external" href="//farseerfc.me/zhs/history-of-chs-addressing.html">「柱面-磁头-扇区寻址的一些旧事」</a>
整理了一下我对磁盘类存储设备(包括软盘、硬盘,不包括光盘、磁带)的一些理解,
算是为以后讨论文件系统作铺垫;这篇整理一下我对闪存类存储设备的理解。</p>
<p>这里想要讨论的闪存类存储是指 SSD 、SD卡、U盘、手机内置闪存等基于 NAND
又有闪存转换层的存储设备(下文简称闪存盘),但不包括裸 NAND 设备、3D Xpoint (Intel
Optane)等相近物理结构但是没有类似的闪存转换层的存储设备。
闪存类存储设备这几年发展迅猛,SD卡和U盘早就替代软盘成为数据交换的主流, SSD 大有替代硬盘的趋势。
因为发展迅速,所以其底层技术变革很快,不同于磁盘类存储技术有很多公开资料可以获取,
闪存类存储的技术细节通常是厂商们的秘密,互联网上能找到很多外围资料,
但是关于其如何运作的细节却很少提到。所以我想先整理一篇笔记,记下我搜集到的资料,加上我自己的理解。
本文大部分信息来源是 <a class="reference external" href="https://lwn.net/Articles/428584/">Optimizing Linux with cheap flash drives</a>
和 <a class="reference external" href="https://accelazh.github.io/ssd/A-Summary-On-SSD-And-FTL">A Summary on SSD & FTL</a>
,加上我的理解,文中一些配图也来自这两篇文章。</p>
<div class="section" id="nand-flash">
<h2><a class="toc-backref" href="#id15">1 NAND Flash 原理</a></h2>
<p>比 NAND Flash 更早的 <a class="reference external" href="https://en.wikipedia.org/wiki/EEPROM">EEPROM</a> 等存储技术
曾经用过 NOR Flash cell ,用于存储主板配置信息等少量数据已经存在 PC 中很久了。后来
NAND Flash 的微型化使得 NAND Flash 可以用于存储大量数据,急剧降低了存储成本,所以以 NAND
Flash 为基础的存储技术能得以替代硬盘等存储设备。</p>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://openiotelceurope2016.sched.com/event/7rsF/tutorial-why-nand-flash-breaks-down-arnout-vandecappelle-essensiummind">Tutorial: Why NAND Flash Breaks Down</a></div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/VajB8vCsZ3s"></iframe></div></div>
</div>
<p>这里不想涉及太多 NAND Flash 硬件细节,有个演讲
<a class="reference external" href="https://openiotelceurope2016.sched.com/event/7rsF/tutorial-why-nand-flash-breaks-down-arnout-vandecappelle-essensiummind">Tutorial: Why NAND Flash Breaks Down</a>
和 <a class="reference external" href="https://youtu.be/VajB8vCsZ3s">YouTube 视频</a>
介绍了其原理,感兴趣的可以参考一下。只罗列一下视频中提到的一些 NAND Flash 的特点:</p>
<ul class="simple">
<li>NAND Flash 使用 floating gate 中束缚电子来保存二进制数据,对这些 Cell 有读取(Read)、
写入(Programming)、擦除(Erase)的操作。擦写次数叫 P/E cycle。</li>
<li>电子的量导致的电势差可以区别 1 和 0 ,这是 Single Level Cell (SLC) 的存储方式。
或者可以用不同的电势差区分更多状态保存更多二进制位,从而有 Multi-Level Cell (MLC),
TLC, QLC 等技术。可以对 MLC 的 Flash Cell 使用类似 SLC 的写入模式,物理区别只是参考电压,
只是 SLC 模式写入下容量减半。</li>
<li>高密度设计下,一组 NAND Flash Cell 可以同时并发读写。所以有了读写页 2KiB/4KiB 这样的容量。
页面越大,存储密度越高,为了降低成本厂商都希望提高读写页的大小。</li>
<li>为了避免添加额外导线,NAND Flash Cell 是使用基板上加负电压的方式擦除 floating gate
中的二进制位的,所以擦除操作没法通过地址线选择特定 Cell 或者读写页,于是整块擦除有块大小。</li>
<li>写入操作对 SLC 单个 Cell 而言,就是把 1 置 0 ,而擦除操作则是把整块置 1 。SLC
可以通过地址线单独选择要写入的 Cell ,MLC 则把不同页的二进制放入一个 Cell ,放入时有顺序要求,
先写处于高位的页,再写低位的。所以 MLC 中不同页面地址的页面是交错在同一组 Cell 中的。</li>
<li>SLC 其实并没有特别要求擦除块中的写入顺序,只是要求仅写一次(从 1 到 0)。
MLC 则有先写高位页再写低位页的要求。厂商规格中的要求更严格,擦除块中必须满足按页面编号顺序写入。</li>
<li>写入和擦除操作是通过量子隧道效应把电子困在 floating gate 中的,所以是个概率事件。通过多次脉冲
可以缩小发生非预期概率事件的可能性,但是没法完全避免,所以需要 ECC 校验纠错。</li>
<li>根据 ECC 强度通常有三种 ECC 算法,强度越强需要越多算力:<ul>
<li><a class="reference external" href="https://zh.wikipedia.org/zh-hans/%E6%B1%89%E6%98%8E%E7%A0%81">汉民码</a>
可根据 n bit 探测 <span class="math">\(2^n - n -1\)</span> 中的 2 bit 错误,修正 1 bit 错误。</li>
<li><a class="reference external" href="https://zh.wikipedia.org/wiki/BCH%E7%A0%81">BCH码</a> 可根据 <span class="math">\(n*m\)</span> bit
纠错 <span class="math">\(2^n\)</span> bit 中的 <span class="math">\(m\)</span> bit 错误。</li>
<li><a class="reference external" href="https://zh.wikipedia.org/wiki/%E4%BD%8E%E5%AF%86%E5%BA%A6%E5%A5%87%E5%81%B6%E6%AA%A2%E6%9F%A5%E7%A2%BC">LDPC</a>
原理上类似扩展的汉民码,能做到使用更少校验位纠错更多错误。</li>
</ul>
</li>
<li>因为 ECC 的存在,所以读写必须至少以 ECC 整块为单位,比如 256 字节或者整个页面。</li>
<li>也因为 ECC 的存在, <span class="math">\(ECC(\texttt{0xFF}) \ne \texttt{0xFF}\)</span>
,空页(擦除后全1的页面)必须特殊处理。所以需要区分写了数据全 1 的页和空页。</li>
<li>ECC校验多次失败的页面可以被标记为坏页,出厂时就可能有一些坏页,这些由转换层隐藏起来。</li>
<li>断电后,也有小概率下束缚的电子逃逸出 floating gate ,时间越长越可能发生可以探测到的位反转。
所以基于 NAND Flash 的存储设备应该避免作为存档设备离线保存。</li>
<li>电子逃逸的概率也和温度有关,温度越高越容易逃逸,所以高温使用下会有更高的校验错误率。</li>
<li>读取时,因为用相对较高的电压屏蔽没有读取的地址线,有一定概率影响到没被读取的页面中存储的数据。
控制器可能考虑周期性地刷新这些写入后多次读取的页面,这可能和后文的静态擦写均衡一起做。</li>
<li>正在写入或者擦除中突然断电的话下,写入中的一整页数据可能并不稳定,比如短期内能正常读取但是难以持续很长时间。</li>
</ul>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://openiotelceurope2016.sched.com/event/7rsF/tutorial-why-nand-flash-breaks-down-arnout-vandecappelle-essensiummind">MLC 擦写次数与错误率</a></div>
<div class="panel-body">
<img alt="ssd-pe-cycles.png" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-pe-cycles.png"/>
</div>
</div>
<p>上篇讲硬盘的笔记中提到过,硬盘物理存储也有越来越强的校验机制,不过相比之下 NAND Flash
出现临时性校验失败的可能性要高很多,需要控制器对校验出错误的情况有更强的容忍能力。
厂商们制作存储设备的时候,有一个需要达到的错误率目标(比如平均 <span class="math">\(10^{14}\)</span> bit
出现一次位反转),针对这个目标和实际物理错误率,相应地设计纠错强度。校验太强会浪费存储密度和算力,
从而提升成本,这里会根据市场细分找折衷点。</p>
</div>
<div class="section" id="id5">
<h2><a class="toc-backref" href="#id16">2 封装结构</a></h2>
<p>从外部来看,一个闪存盘可能有这样的结构:</p>
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/ssd-enclosure.svg" type="image/svg+xml">
ssd-enclosure.svg</object>
<p>从上往下,我们买到的一个闪存盘可能一层层分级:</p>
<ol class="arabic simple">
<li>整个闪存盘有个控制器,其中含有一部分 RAM 。然后是一组 NAND Flash 封装芯片(chip)。</li>
<li>每个封装芯片可能还分多个 Device ,每个 Device 分多个 Die ,这中间有很多术语我无法跟上,大概和本文想讨论的事情关系不大。</li>
<li>每个 Die 分多个平面(Plane),平面之间可以并行控制,每个平面相互独立。从而比如在一个平面内
做某个块的擦除操作的时候,别的平面可以继续读写而不受影响。</li>
<li>每个平面分成多个段(Segment),段是擦除操作的基本单位,一次擦除一整个段。</li>
<li>每个段分成多个页面(Page),页面是读写操作的基本单位,一次可以读写一整页。</li>
<li>页面内存有多个单元格(Cell),单元格是存储二进制位的基本单元,对应 SLC/MLC/TLC/QLC 这些,
每个单元格可以存储一个或多个二进制位。</li>
</ol>
<p>以上这些名字可能不同厂商不同文档的称法都各有不同,比如可能有的文档把擦除块叫 page 或者叫
eraseblock 。随着容量不断增大,厂商们又新造出很多抽象层次,比如 chip device die 这些,
不过这些可能和本文关系不大。如果看别的文档注意区别术语所指概念,本文中我想统一成以上术语。
重要的是有并行访问单元的平面(Plane)、擦除单元的段(Segment)、读写单元的页(Page)这些概念。
抽象地列举概念可能没有实感,顺便说一下这些概念的数量级:</p>
<ol class="arabic simple">
<li>每个 SSD 可以有数个封装芯片。</li>
<li>每个芯片有多个 Die 。</li>
<li>每个 Die 有多个平面。</li>
<li>每个平面有几千个段。比如 2048 个。</li>
<li>每个段有数百个页到几千页,比如 128~4096 页,可能外加一些段内元数据。</li>
<li>每个页面是 2KiB~8KiB 这样的容量,外加几百字节的元数据比如 ECC 校验码。</li>
</ol>
<p>和硬盘相比,一个闪存页面大概对应一个到数个物理扇区大小,现代硬盘也逐渐普及 4KiB 物理扇区,
文件系统也基本普及 4KiB 或者更大的逻辑块(block)或者簇(cluster)大小,可以对应到一个闪存页面。
每次读写都可以通过地址映射直接对应到某个闪存页面,这方面没有硬盘那样的寻址开销。
闪存盘的一个页面通常配有比硬盘扇区更强的 ECC 校验码,因为 NAND
单元格丧失数据的可能性比磁介质高了很多。</p>
<p>闪存有写入方式的限制,每次写入只能写在「空」的页面上,不能覆盖写入已有数据的页面。
要重复利用已经写过的页面,需要对页面所在段整个做擦除操作,每个段是大概 128KiB 到 8MiB
这样的数量级。每个擦除段需要统计校验失败率或者跟踪擦除次数,以进行擦写均衡(Wear Leveling)。</p>
</div>
<div class="section" id="wear-leveling-flash-translation-layer">
<h2><a class="toc-backref" href="#id17">3 擦写均衡(Wear Leveling)和映射层(Flash Translation Layer)</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://lwn.net/Articles/428793/">Animation: wear leveling on SSD drives</a></div>
<div class="panel-body">
<img alt="ssd-segment.gif" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-segment.gif"/>
</div>
</div>
<p>擦除段的容量大小是个折衷,更小的擦除段比如 128KiB 更适合随机读写,
因为每随机修改一部分数据时需要垃圾回收的粒度更小;而使用更大的擦除段可以减少元数据和地址映射的开销。
从擦除段的大小这里,已经开始有高端闪存和低端闪存的差异,比如商用 SSD 可能比 U 盘和 SD
卡使用更小的擦除段大小。</p>
<p>闪存盘中维护一个逻辑段地址到物理段地址的映射层,叫闪存映射层(Flash Translation Layer
)。每次写一个段的时候都新分配一个空段,
写完后在映射表中记录其物理地址。映射表用来在读取时做地址转换,所以映射表需要保存在闪存盘控制器的
RAM 中,同时也需要记录在闪存内。具体记录方式要看闪存盘控制器的实现,可能是类似日志的方式记录的。</p>
<p>「段地址映射表」的大小可以由段大小和存储设备容量推算出来。比如对一个 64GiB 的 SD
卡,如果使用 4MiB 的段大小,那么需要至少 16K 个表项。假设映射表中只记录 2B 的物理段地址,
那么需要 32KiB 的 RAM 存储段地址映射表。对一个 512GiB 的 SSD ,如果使用 128KiB 的段大小,
那么至少需要 4M 个表项。记录 4B 的物理段地址的话,需要 16MiB 的 RAM 存储地址映射,
或者需要动态加载的方案只缓存一部分到 RAM 里。控制器中的
RAM 比 NAND 要昂贵很多,这里可以看出成本差异。</p>
<p>除了地址映射表,每个物理段还要根据擦除次数或者校验错误率之类的统计数据,做擦写均衡。有两种擦写均衡:</p>
<ul class="simple">
<li>动态擦写均衡(Dynamic Wear Leveling):每次写入新段时选择擦除次数少的物理段。</li>
<li>静态擦写均衡(Static Wear Leveling):空闲时,偶尔将那些许久没有变化的逻辑段搬运到
多次擦除的物理段上。</li>
</ul>
<p>低端闪存比如 SD 卡和 U 盘可能只有动态擦写均衡,更高端的 SSD 可能会做静态擦写均衡。
静态擦写均衡想要解决的问题是:盘中写入的数据可以根据写入频率分为冷热,
总有一些冷数据写入盘上就不怎么变化了,它们占用着的物理段有比较低的擦除计数。
只做动态擦写均衡的话,只有热数据的物理段被频繁擦写,加速磨损,
通过静态擦写均衡能将冷数据所在物理段释放出来,让整体擦写更平均。
但是静态擦写均衡搬运数据本身也会磨损有限的擦写次数,这需要优秀的算法来折衷。</p>
<p>除了擦写均衡用的统计数据外, FTL 也要做坏块管理。闪存盘出厂时就有一定故障率,可能有一部分坏块。
随着消耗擦写周期、闲置时间、环境温度等因素影响,也会遇到一些无法再保证写入正确率的坏块。
NAND Flash 上因为量子隧道效应,偶尔会有临时的校验不一致,遇到这种情况,除了根据 ECC
校验恢复数据, FTL 也负责尝试对同一个物理段多次擦除和读写,考察它的可用性。排除了临时故障后,
如果校验不一致的情况仍然持续,那么需要标注它为坏块,避免今后再写入它。</p>
<p>出厂时,闪存盘配有的物理段数量就高于标称的容量,除了出厂时的坏块之外,剩余的可用物理段可以用于
擦写均衡,这种行为称作 <a class="reference external" href="https://zh.wikipedia.org/wiki/%E5%86%99%E5%85%A5%E6%94%BE%E5%A4%A7#%E9%A2%84%E7%95%99%E7%A9%BA%E9%97%B4">Over Provisioning</a>
。除了盘内预留的这些空间,用户也可以主动通过分区的方式或者文件系统 TRIM 的方式预留出更多可用空间,
允许 FTL 更灵活地均衡擦写。</p>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id18">4 段内写入顺序与垃圾回收策略</a></h2>
<p>段是闪存盘的擦写单元,考虑到段是 128KiB ~ 8MiB 这样的数量级,现实中要求每次连续写入一整段的话,
这样的块设备接口不像硬盘的接口,不方便普通文件系统使用。所以在段的抽象之下有了更小粒度的页面抽象,
页面对应到文件系统用的逻辑块大小,是 2KiB~8KiB 这样的数量级,每次以页面为单位读写。</p>
<p>写入页面时有段内连续写入的限制,于是需要段内映射和垃圾回收算法,提供对外的随机写入接口。
写入操作时, FTL 控制器内部先「打开(open)」一个段,等写入完成,再执行垃圾回收「关闭(close)」一个段。
写入过程中处于打开状态的段需要一些额外资源(RAM等)跟踪段内的写入状况,所以闪存盘同时能「打开」
的段数量有限。并且根据不同的垃圾回收算法,需要的额外资源也不尽相同,在
<a class="reference external" href="https://lwn.net/Articles/428584/">Optimizing Linux with cheap flash drives</a> 一文中介绍几种可能的垃圾回收算法:</p>
<div class="section" id="id7">
<h3><a class="toc-backref" href="#id19">4.1 线性写入优化</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://lwn.net/Articles/428796/">Animations: linear-access optimized</a></div>
<div class="panel-body">
<img alt="ssd-linear.gif" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-linear.gif"/>
</div>
</div>
<p>假设写入请求大部分都是连续写入,很少有地址跳转,那么可以使用线性优化算法。</p>
<ul class="simple">
<li>Open:当第一次打开一个段,写入其中一页时,分配一个新段。如果要写入的页不在段的开头位置,那么搬运写入页面地址之前的所有页面到新段中。</li>
<li>Write: 在 RAM 中跟踪记录当前写入位置,然后按顺序写下新的页面。</li>
<li>Close: 最后搬运同段中随后地址上的页面,并关闭整段,调整段映射表。</li>
</ul>
<p>如果在段内写入了几页之后,又跳转到之前的位置,那需要在跳转时关闭当前段写入(并完整搬运剩下的页面),
然后重新打开这一段,搬运调转地址之前的页面,从跳转的页面位置开始写入。</p>
<p>线性优化算法的好处在于:没有复杂的页面地址映射,段内的逻辑页面地址就是物理页面地址。
读一页的时候根据页面偏移和当前写入位置就能判断读新物理段还是老物理段。遇到突然断电之类的情况,
即使丢失最近写入的新物理段,老物理段的数据仍然还在,所以没必要保存 RAM 中的地址映射到闪存元数据中。</p>
<p>线性优化算法的坏处是:每遇到一次乱序的写入,都要整段执行一次搬运,造成
<a class="reference external" href="https://zh.wikipedia.org/zh-hans/%E5%86%99%E5%85%A5%E6%94%BE%E5%A4%A7">写入放大(Write Amplification)</a>
。</p>
<p>一些文档中,将这种地址映射垃圾回收方式叫做「段映射(Segment Mapping)」,因为从 FTL
全局来看只维护了擦写段的地址映射关系。</p>
</div>
<div class="section" id="id8">
<h3><a class="toc-backref" href="#id20">4.2 段内地址映射</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://lwn.net/Articles/428831/">Animations: block remapping</a></div>
<div class="panel-body">
<img alt="ssd-random.gif" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-random.gif"/>
</div>
</div>
<p>对需要随机乱序写入的数据,可以使用段内地址映射。方式是额外在段外的别的闪存区域维护一张段内地址映射表,
像段地址一样,通过查表间接访问页面地址。</p>
<ul class="simple">
<li>Open: 分配一块新的段,同时分配一个新的段内映射表。</li>
<li>Write: 每写入一页,在段内映射表记录页面的在新段中的物理地址。</li>
<li>Close: 复制老段中没有被覆盖写入的页到新段,并记录在段内映射表中,然后释放老段和老的段内映射表。</li>
</ul>
<p>也就是说同时维护两块不同大小的闪存空间,一块是记录段数据的,一块是记录段内地址映射表的,
两块闪存空间有不同的写入粒度。可以在每个物理段内额外留出一些空间记录段内地址映射表,也可以在 FTL
全局维护一定数量的段内地址映射表。
每次读取段内的数据时,根据映射表的内容,做地址翻译。新段中页面的排列顺序将是写入的顺序,
而不是地址顺序。</p>
<p>根据实现细节,段内地址映射可以允许覆盖写入老段中的页面,但是可能不允许覆盖写入新段(正在写入的段)
中已经写入的页面,遇到一次连续的写请求中有重复写入某一页面的时候,就需要关闭这一段的写入,然后重新打开。</p>
<p>段内地址映射的优点是:支持随机写入,并且只要段处于打开状态,随机写入不会造成写入放大(Write Amplification)。</p>
<p>缺点是:首先地址映射这层抽象有性能损失。其次遇到突然断电之类的情况,
下次上电后需要扫描所有正打开的段并完成段的关闭操作。</p>
<p>和「段映射」术语一样,在一些文档中,将这种段内地址映射的方式叫做「页面映射(Page Mapping)」,因为从
FTL 全局来看跳过了擦写段这一层,直接映射了页面的地址映射。</p>
</div>
<div class="section" id="id9">
<h3><a class="toc-backref" href="#id21">4.3 日志式写入</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://lwn.net/Articles/428831/">Animations: data logging</a></div>
<div class="panel-body">
<img alt="ssd-logging.gif" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-logging.gif"/>
</div>
</div>
<p>除了大量随机写入和大量连续写入这两种极端情况,大部分文件系统的写入方式可能会是对某个地址空间
进行一段时间的随机写入,然后就长时间不再修改,这时适合日志式的写入方式。</p>
<p>日志式的写入方式中写入一段采用三个物理段:老物理段,用于日志记录的新物理段,和垃圾回收后的段。</p>
<ul class="simple">
<li>Open: 分配一块新的段。可能额外分配一个用于记录日志的段,或者将日志信息记录在数据段内。</li>
<li>Write:每写入一页,同时记录页面地址到日志。</li>
<li>Close:再分配一个新段执行垃圾回收。按日志中记录的地址顺序将数据段中(新写入)的页面或者老段中
没有被覆盖的页面复制到垃圾回收结束的新段中。</li>
</ul>
<p>日志式写入在写入过程中像段内地址映射的方式一样,通过日志记录维护页面地址映射关系,
在写入结束执行垃圾回收之后,则像线性写入的方式一样不再需要维护页面映射。
可以说日志式写入某种程度上综合了前面两种写入方式的优点。</p>
<p>日志式写入的优点:允许随机顺序写入,并且在执行垃圾回收之后,不再有间接访问的地址转换开销。</p>
<p>日志式写入的缺点:触发垃圾回收的话,可能比段地址映射有更大的写入放大(Write Amplification)。</p>
<p>在一些文档中,将这种日志式写入方式称作「混合映射(Hybrid Mapping)」,因为在段开启写入期间行为像页面映射,
在段关闭写入后行为像段映射。</p>
</div>
</div>
<div class="section" id="id10">
<h2><a class="toc-backref" href="#id22">5 针对特定写入模式的优化</a></h2>
<p>上述三种地址映射和垃圾回收方式,各有不同的优缺点,根据数据块的写入模式可能需要挑选相应的策略。
并且「全局段地址映射表」、「段内页面地址映射表」、「写入页面地址日志」之类的元数据因为频繁修改,
FTL 也可能需要用不同的策略来记录这些元数据。这里面向不同使用场景的闪存设备可能有不同的 FTL
策略,并且 FTL 可能根据逻辑地址来选择哪种策略。</p>
<div class="section" id="id11">
<h3><a class="toc-backref" href="#id23">5.1 混合垃圾回收策略</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="https://lwn.net/Articles/428592/">Performance measurements on a class 10 SDHC card</a></div>
<div class="panel-body">
<img alt="ssd-extrememory-results.png" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-extrememory-results.png"/>
<img alt="ssd-panasonic-plot.png" class="img-responsive" src="//farseerfc.me/zhs/images/ssd-panasonic-plot.png"/>
</div>
</div>
<p>用来记录照片、视频等的 SD 卡、microSD、U盘等设备可能根据数据的逻辑地址,为特定文件系统布局优化,
这里特定文件系统主要是指 FAT32 和 exFAT 这两个 FAT 系文件系统。 FAT 系文件系统的特点在于,
地址前端有一块空间被用来放置 <a class="reference external" href="https://zh.wikipedia.org/wiki/FAT">文件分配表(File Allocation Table)</a>
,可以根据文件系统簇大小和设备存储容量推算出 FAT 表占用大小,这块表内空间需要频繁随机读写。
对 FTL 自身的元数据,和 FAT 表的逻辑地址空间,需要使用「段内地址映射」来保证高效的随机读写,
而对随后的数据空间可使用「线性写入优化」的策略。</p>
<p>右侧上图有张性能曲线,测量了一个 class 10 SDHC 卡上,不同读写块大小时,顺序读取、顺序写入、随机写入、
对 FAT 区域的写入之类的性能差异。下图是测量的读取延迟。可以看出 FAT
区域的随机写入和其余逻辑地址上有明显不同的性能表现。</p>
<p>为容纳普通操作系统设计的 eMMC 和 SSD 难以预测文件系统的读写模式,可能需要使用更复杂的地址映射和垃圾回收策略。
比如一开始假定写入会是顺序写入,采用「线性优化」方式;当发生乱序写入时,转变成类似「日志式写入」
的方式记录写入地址并做地址映射;关闭段时,再根据积累的统计数据判断,可能将记录的日志与乱序的数据
合并(merge)成顺序的数据块,也可能保持页面映射转变成类似「段内地址映射」的策略。</p>
</div>
<div class="section" id="id12">
<h3><a class="toc-backref" href="#id24">5.2 利用 NAND Flash 物理特性的优化</a></h3>
<p>再考虑 NAND Flash 的物理特性,因为 MLC 要不断调整参考电压做写入, MLC 的写入比 SLC
慢一些,但是可以对 MLC Flash 使用 SLC 式的写入, FTL 控制器也可能利用这一点,让所有新的写入处于
SLC 模式,直到关闭整段做垃圾回收时把积攒的 SLC 日志段回收成 MLC 段用于长期保存。
一些网页将这种写入现象称作「SLC 缓存」甚至称之为作弊,需要理解这里并不是用单独的 SLC Flash
芯片做 writeback 缓存,更不是用大 RAM 做缓存,处于 SLC 模式的写入段也是持久存储的。</p>
</div>
<div class="section" id="id13">
<h3><a class="toc-backref" href="#id25">5.3 同时打开段数</a></h3>
<p>上述地址映射和垃圾回收策略都有分别的打开(open)、写入(write)、关闭(close)时的操作,
闪存盘通常允许同时打开多个段,所以这三种操作不是顺序进行的,某一时刻可能同时有多个段处在打开的状态,
能接受写入。不过一个平面(Plane)通常只能进行一种操作(读、写、擦除),所以打开写入段时,
FTL 会尽量让写入分部在不同的平面上。还可能有更高层次的抽象比如 Device、 Chip 、 Die
等等,可能对应闪存盘内部的 RAID 层级。</p>
<p>闪存盘能同时打开的段不光受平面之类的存储结构限制,还受控制器可用内存(RAM)限制之类的。
为 FAT 和顺序写入优化的 FTL ,可能除了 FAT 区域之外,只允许少量(2~8)个并发写入段,
超过了段数之后就会对已经打开的段触发关闭操作(close),执行垃圾回收调整地址映射,进而接受新的写入。
更高端的 SSD 的 FTL 如果采用日志式记录地址的话,同时打开的段数可能不再局限于可用内存限制,
连续的随机写入下按需动态加载段内地址映射到内存中,在空闲时或者剩余空间压力下才触发垃圾回收。</p>
</div>
<div class="section" id="id14">
<h3><a class="toc-backref" href="#id26">5.4 预格式化</a></h3>
<p>FTL 可能为某种文件系统的写入模式做优化,同时如果文件系统能得知 FTL 的一些具体参数(比如擦除段大小、
读写页大小、随机写入优化区域),那么可能更好地安排数据结构,和 FTL 相互配合。
F2FS 和 exFAT 这些文件系统都在最开头的文件系统描述中包含了一些区域,记录这些闪存介质的物理参数。
闪存盘出厂时,可能预先根据优化的文件系统做好格式化,并写入这些特定参数。</p>
</div>
<div class="section" id="trim-discard">
<h3><a class="toc-backref" href="#id27">5.5 TRIM 和 discard</a></h3>
<p>另一种文件系统和 FTL 相互配合的机制是 TRIM 指令。TRIM 由文件系统发出,告诉底层闪存盘(
或者别的类型的 thin provisioning 块设备)哪些空间已经不再使用, FTL 接受 TRIM
指令之后可以避免一些数据搬运时的写入放大。关于 TRIM 指令在 Linux 内核中的实现,有篇
<a class="reference external" href="https://lwn.net/Articles/417809/">The best way to throw blocks away</a>
介绍可以参考。</p>
<p>考虑到 FTL 的上述地址映射原理, TRIM 一块连续空间对 FTL 而言并不总是有帮助的。
如果被 TRIM 的地址位于正在以「段内地址映射」或「日志式映射」方式打开的写入段中,那么
TRIM 掉一些页面可能减少垃圾回收时搬运的页面数量。但是如果 TRIM 的地址发生在已经垃圾回收结束的段中,
此时如果 FTL 选择立刻对被 TRIM 的段执行垃圾回收,可能造成更多写入放大,
如果选择不回收只记录地址信息,记录这些地址信息也需要耗费一定的 Flash 写入。
所以 FTL 的具体实现中,可能只接受 TRIM 请求中,整段擦除段的 TRIM ,而忽略细小的写入页的 TRIM 。</p>
<p>可见 FTL 对 TRIM 的实现是个黑盒操作,并且 TRIM 操作的耗时也非常难以预测,可能立刻返回,
也可能需要等待垃圾回收执行结束。</p>
<p>对操作系统和文件系统实现而言,有两种方式利用 TRIM :</p>
<ol class="arabic simple">
<li>通过 discard 挂载选项,每当释放一些数据块时就执行 TRIM 告知底层块设备。</li>
<li>通过 fstrim 等外部工具,收集连续的空块并定期发送 TRIM 给底层设备。</li>
</ol>
<p>直觉来看可能 discard 能让底层设备更早得知 TRIM 区域的信息并更好利用,但是从实现角度来说,
discard 不光影响文件系统写入性能,还可能发送大量被设备忽略掉的小块 TRIM 区域。可能 fstrim
方式对连续大块的区间执行 TRIM 指令更有效。</p>
</div>
</div>
<div class="section" id="tl-dr-vs">
<h2><a class="toc-backref" href="#id28">6 TL;DR 低端 vs 高端</a></h2>
<p>标题中的疑问「SSD就是大U盘?」相信看到这里已经有一些解答了。
即使 SSD 和U盘中可以采用类似的 NAND Flash 存储芯片,由于他们很可能采用不同的 FTL
策略,导致在读写性能和可靠性方面都有不同的表现。(何况他们可能采用不同品质的 Flash )。</p>
<p>如果不想细看全文,这里整理一张表,列出「高端」闪存盘和「低端」闪存盘可能采取的不同策略。
实际上大家买到的盘可能处于这些极端策略中的一些中间点,市场细分下并不是这么高低端分明。
比如有些标明着「为视频优化」之类宣传标语的「外置SSD」,对消费者来说可能会觉得为视频优化的话一定性能好,
但是理解了 FTL 的差异后就可以看出这种「优化」只针对线性写入,不一定适合放系统文件根目录的文件系统。</p>
<table border="0" class="table docutils borderless">
<colgroup>
<col width="34%"/>
<col width="34%"/>
<col width="31%"/>
</colgroup>
<thead valign="bottom">
<tr><th class="head">参数</th>
<th class="head">低端</th>
<th class="head">高端</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>段大小</td>
<td>8MiB</td>
<td>128KiB</td>
</tr>
<tr><td>段地址映射</td>
<td>静态段映射</td>
<td>日志式映射</td>
</tr>
<tr><td>随机写入范围</td>
<td>FTL元数据与FAT表区域</td>
<td>全盘</td>
</tr>
<tr><td>同时打开段数</td>
<td>4~8</td>
<td>全盘</td>
</tr>
<tr><td>物理段统计信息</td>
<td>无(随机挑选空闲段)</td>
<td>擦除次数、校验错误率等</td>
</tr>
<tr><td>擦写均衡</td>
<td>动态均衡(仅写入时分配新段考虑)</td>
<td>静态均衡(空闲时考虑搬运)</td>
</tr>
<tr><td>写入单元模式</td>
<td>TLC</td>
<td>长期存储 MLC, 模拟 SLC 日志</td>
</tr>
</tbody>
</table>
<p>介绍完闪存类存储,下篇来讲讲文件系统的具体磁盘布局,考察一下常见文件系统如何使用 HDD/SSD
这些不同读写特性的设备。</p>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>柱面-磁头-扇区寻址的一些旧事2020-03-06T15:45:00+09:002020-03-06T15:45:00+09:00farseerfctag:farseerfc.me,2020-03-06:/zhs/history-of-chs-addressing.html
<p>在 SSD 这种新兴存储设备普及之前,很长一段时间硬盘是个人计算机的主要存储设备。
更往前的磁带机不常见于个人计算机,软盘的地位很快被硬盘取代,到 SSD 出现为止像
<a class="reference external" href="https://en.wikipedia.org/wiki/MiniDisc">MiniDisc</a> 、
<a class="reference external" href="https://en.wikipedia.org/wiki/DVD-RAM">DVD-RAM</a>
等存储设备也从未能挑战过硬盘的地位。硬盘作为主要存储设备,自然也影响了文件系统的设计。</p>
<p>这篇笔记稍微聊一聊硬盘这种存储设备的寻址方式对早期文件系统设计的一些影响,特别是
柱面-磁头-扇区寻址(Cylinder-head-sector addressing, 简称CHS寻址)的起源和发展。
大部分内容来自维基百科 <a class="reference external" href="https://en.wikipedia.org/wiki/Cylinder-head-sector">Cylinder-head-sector 词条</a>
这里只是记录笔记。现今的硬盘已经不再采用 CHS 寻址,其影响却还能在一些文件系统设计中看到影子。</p>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id7">柱面、磁头、扇区以及相关术语</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
磁盘示意图(来自维基百科 <a class="reference external" href="https://en.wikipedia.org/wiki/Cylinder-head-sector">Cylinder-head-sector 词条</a> )</div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/chs-illustrate-trans.svg" type="image/svg+xml">
chs-illustrate-trans.svg</object>
</div>
</div>
<p>如右图所示,一块硬盘(Hard Disk Drive, HDD)是一个圆柱体转轴上套着一些磁碟片(platter),
然后有一条磁头臂(actuator arm)插入磁碟片间的位置 …</p></div>
<p>在 SSD 这种新兴存储设备普及之前,很长一段时间硬盘是个人计算机的主要存储设备。
更往前的磁带机不常见于个人计算机,软盘的地位很快被硬盘取代,到 SSD 出现为止像
<a class="reference external" href="https://en.wikipedia.org/wiki/MiniDisc">MiniDisc</a> 、
<a class="reference external" href="https://en.wikipedia.org/wiki/DVD-RAM">DVD-RAM</a>
等存储设备也从未能挑战过硬盘的地位。硬盘作为主要存储设备,自然也影响了文件系统的设计。</p>
<p>这篇笔记稍微聊一聊硬盘这种存储设备的寻址方式对早期文件系统设计的一些影响,特别是
柱面-磁头-扇区寻址(Cylinder-head-sector addressing, 简称CHS寻址)的起源和发展。
大部分内容来自维基百科 <a class="reference external" href="https://en.wikipedia.org/wiki/Cylinder-head-sector">Cylinder-head-sector 词条</a>
这里只是记录笔记。现今的硬盘已经不再采用 CHS 寻址,其影响却还能在一些文件系统设计中看到影子。</p>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id7">柱面、磁头、扇区以及相关术语</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
磁盘示意图(来自维基百科 <a class="reference external" href="https://en.wikipedia.org/wiki/Cylinder-head-sector">Cylinder-head-sector 词条</a> )</div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/chs-illustrate-trans.svg" type="image/svg+xml">
chs-illustrate-trans.svg</object>
</div>
</div>
<p>如右图所示,一块硬盘(Hard Disk Drive, HDD)是一个圆柱体转轴上套着一些磁碟片(platter),
然后有一条磁头臂(actuator arm)插入磁碟片间的位置,加上一组控制芯片(controller)。
每个磁碟片有上下两面涂有磁性材质,磁头臂上有一组磁头(head),每个磁头对应磁盘的一个面,
所以比如一个 3 碟的硬盘会有 6 个磁头。</p>
<p>每个磁碟片上定义了很多同心圆的磁头轨道,叫做磁道(track),磁道位于盘面上不同半径的位置,
通过旋转磁碟臂能让磁头移动到特定的半径上,从而让读写磁头在不同的磁道间跳转。
不同磁头上同磁道的同心圆共同组成一个柱面(cylinder),或者说移动磁碟臂能选定磁盘中的一个柱面。
磁道上按等角度切分成多个小段,叫做扇区(sector),每个扇区是读写数据时采用的最小单元。
早期在 IBM 大型机之类上使用的硬盘的扇区大小比较小,到 IBM PC
开始个人计算机用的硬盘扇区基本被统一到 512 字节。现代硬盘内部可能采用 Advanced Format
使用 4K 字节扇区。</p>
<p>在早期软盘和硬盘的寻址方式被称作「柱面-磁头-扇区寻址」,简称 CHS 寻址,
是因为这三个参数是软件交给硬件定位到某个具体扇区单元时使用的参数。
首先柱面参数让磁头臂移动到某个半径上,寻址到某个柱面,然后激活某个磁头,然后随着盘面旋转,
磁头定位到某个扇区上。</p>
<p>「柱面-磁头-扇区」这个寻址方式,听起来可能不太符合直觉,尤其是柱面的概念。直觉上,
可能更合理的寻址方式是「盘片-盘面-磁道-扇区」,而柱面在这里是同磁道不同盘片盘面构成的一个集合。
不过理解了磁盘的机械结构的话,柱面的概念就比较合理了,寻址时先驱动磁头臂旋转,
磁头臂上多个磁头一起飞到某个磁道上,从而运动磁头臂的动作定义了一个柱面。
柱面和磁头(CH)组合起来能定位到某个特定的磁道,画张图大概如下图所示:</p>
<object class="tikz embed-responsive-item" data="//farseerfc.me/uml/790bfd1c.svg" type="image/svg+xml">
tikz diagram</object>
<p>上图中值得注意的是磁道的编号方式,我用相同的颜色画出了相同的磁道。因为按照 CHS
的顺序寻址,所以先定位柱面,然后选定磁头。磁盘上按半径从外向内定义柱面的编号,最外圈的磁道位于
0号柱面,由0号磁头开始。随着柱面编号增加,逐步从外圈定位到内圈。</p>
</div>
<div class="section" id="chs">
<h2><a class="toc-backref" href="#id8">物理 CHS 寻址</a></h2>
<p>以上术语中,柱面号和磁头号直接对应了硬盘上的物理组成部分,所以在物理 CHS
寻址方式下,通过扇区地址的写法能对应到扇区的具体物理位置。之所以这样描述扇区,
是因为早期的软盘和硬盘驱动器没有内置的控制芯片,可以完全由宿主系统执行驱动程序驱动。</p>
<p>在 IBM PC 上,驱动软盘和硬盘的是 CPU 执行位于主板
<a class="reference external" href="https://zh.wikipedia.org/wiki/BIOS">BIOS (Basic Input/Output System)</a>
中的程序,具体来说操作系统(比如DOS)和应用程序调用 <a class="reference external" href="https://en.wikipedia.org/wiki/INT_13H">INT 13H</a>
中断,通过 AH=02H/03H 选择读/写操作,BIOS 在中断表中注册的 13H 中断处理程序执行在 CPU
上完成读写请求。调用 INT 13H 读写扇区的时候,CPU 先通过 INT 13H AH=0CH
控制硬盘的磁头臂旋转到特定柱面上,然后选定具体磁头,让磁头保持在磁道上读数据,
通过忙轮训的方式等待要读写的扇区旋转到磁头下方,从而读到所需扇区的数据。在 DOS 之后的操作系统,
比如早期的 Windows 和 Linux 和 BSD 能以覆盖中断程序入口表的方式提供升级版本的这些操作替代
BIOS 的程序。</p>
<p>以上过程中可以看出两点观察:</p>
<ol class="arabic simple">
<li>CHS 寻址下,跨磁道的寻址(不同 CH 值),和磁道内的寻址(同 CH 不同 S
),是本质上不同的操作。跨磁道的寻址有移动磁头臂的动作,会比磁道内寻址花费更多时间。</li>
<li>通过扇区号的磁道内寻址是个忙轮训操作,需要占用完整 CPU
周期。这也隐含扇区号在一个磁道内的物理排列不必是连续的。</li>
</ol>
<p>实际上扇区号的物理排列的确不是连续的,每个物理扇区中除了用512字节记录扇区本身的数据,
还有扇区的开始记录和结束记录,写有扇区编号和扇区校验码。每读到一个扇区, CPU
可能需要做一些额外操作(比如计算比对校验、写入内存缓冲区、调整内存段页映射)
后才能继续读下一个扇区,如果物理排列上连续编号扇区,可能等 CPU
做完这些事情后磁头已经旋转到之后几个扇区上了。所以出厂时做磁盘低级格式化的时候,
会跳跃着给扇区编号,给 CPU 留足处理时间。比如下图:</p>
<object class="tikz embed-responsive-item" data="//farseerfc.me/uml/adb6edc3.svg" type="image/svg+xml">
tikz diagram</object>
<p>上图中假设有3个柱面,每个柱面6个磁头,每个磁道内11个扇区,并且画出了三种不同的扇区编号跳转情况,
分别是磁道内的扇区跳转(+3),柱面内的磁头跳转(+5),以及柱面间跳转(+10)。
实际磁盘上的柱面数、扇区数要多很多,寻址时需要跳转的距离也可能更长,这里只是举例说明。
图中和实际情况相同的是,柱面号和磁头号从 0 开始编号,而扇区号从 1 开始编号,
所以做逻辑地址换算的时候要考虑编号差异。</p>
<p>早期 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 字节的容量。</p>
<p>如此跳跃编号扇区之后,不是总能给磁道中所有扇区编号,可能在磁道的末尾位置留几个没有使用的扇区空间,
这些是磁道内的保留扇区,可以在发现坏扇区后使用这些隐藏扇区作为替代扇区。当然读写替代扇区的时候
因为扇区寻址不连续可能会有一定性能损失。</p>
<p>因为物理 CHS 寻址下,磁盘由 CPU 执行驱动程序来驱动,所以以上扇区跳跃的长短实际是由 CPU
的速度等因素决定的,理论上 CPU 越快,跳跃间隔可以越短,从而磁盘读写速度也能加快。磁盘出厂时,
厂商并不知道使用磁盘的计算机会是怎样的性能,所以只能保守地根据最慢的 CPU 比如 IBM 初代 PC 搭配的
8086 的速度来决定跳跃间隔。所以在当年早期玩家们流传着这样一个操作:买到新硬盘,
或者升级了电脑配置之后,对硬盘做一次 <a class="reference external" href="https://en.wikipedia.org/wiki/Disk_formatting#Low-level_formatting_(LLF)_of_hard_disks">低级格式化(Low level formating)</a>
,聪明的低级格式化程序能智能安排扇区编号,提升硬盘读写速度,也能跳过已知坏道位置继续编号,
甚至可能将更多保留扇区暴露成可用扇区。这对现代有硬盘控制器的硬盘而言已经没有意义了。</p>
</div>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id9">逻辑 CHS 寻址</a></h2>
<p>随着硬盘容量不断增加, BIOS 中用来 CHS 寻址的地址空间逐渐不够用了。早期 24bit 地址按 <cite>C H S</cite>
的顺序分为 <cite>10 8 6</cite> 的位数,用 8bit 来寻址磁头最多可以有 256 个磁头,而只有 10bit
来寻址柱面,就只能有 1024 个柱面。最初 IBM 这么划分是因为早期用于 IBM 大型机之类的硬盘可以有
厚厚一叠的盘片组,同样的寻址方式就直接用于了 IBM PC 。而 PC 用的硬盘迫于硬盘仓空间大小,
有厚度限制,硬盘中物理盘面可能只有四五个盘片,硬盘容量增加主要是增加盘片表面的数据密度而非增加盘片数量。</p>
<p>于是逐渐地,硬盘厂商开始对 CHS 寻址的地址空间做一些手脚。比如最初的简单想法是重新定义 CH
,将一些磁头数挪用做柱面数。从而有了逻辑 CHS 寻址,其中 CH 是固定一组,通过简单换算从 CH
值找到物理的柱面和磁头数。结合 CH 而不映射 S 的优势在于,从操作系统和文件系统来看依然能根据逻辑
CHS 地址估算出地址跳转所需大概的时间,只是原本一次切换磁头的动作可能变成一次短距离的切换柱面。</p>
<p>此时的操作系统和文件系统已经开始出现针对 CHS 寻址特点的优化方式,
尽量减少跨磁道的寻址能一定程度提升读写速度,跨磁道时的磁道间距离也会影响寻道时间,
文件系统可能会根据CHS地址来安排数据结构,优化这些寻址时间。</p>
<p>即便使用没有针对 CHS 寻址方式优化过的操作系统和文件系统,比如局限在早期 Windows 和 FAT
系文件系统上,早期这些桌面系统用户们仍然能自己优化磁盘读写性能:通过分区。
分区是硬盘上连续的一段空间,早期由于 BIOS 和 bootloader 的一些技术限制,
每个分区必须对齐到柱面大小上。早期 PC 玩家们通过把一个大硬盘切分成多个小分区,
使用时尽量保持近期读写针对同一个分区,就可以减少寻址时的额外开销,改善读写速度。</p>
<p>于是隐含地,CHS 寻址导致底层硬盘和上层操作系统之间有一层性能约定: <strong>连续读写保证最快的读写速度</strong>
。硬盘实现 CHS 寻址时,调整扇区编号方式让连续的 CHS 地址有最快读写速度,文件系统也根据这个约定,
按照 CHS 地址的跳跃来估算读写速度耗时并针对性优化。</p>
</div>
<div class="section" id="zone-bit-recoding-zbr">
<h2><a class="toc-backref" href="#id10">区位记录(Zone bit recoding, ZBR)</a></h2>
<p>以上物理 CHS 寻址,其实依赖一个假设: <strong>每个磁道上有同样数量的扇区</strong> 。早期硬盘上也的确遵循这个假设,
所以我们上面的图示里才能把一个盘面上的扇区展开成一张长方形的表格,因为每个磁道的扇区数是一样的。
实际上当时的硬盘都是恒定角速度(constant angular velocity, CAV)的方式读写,无论磁头在哪儿,
盘片都旋转保持恒定的转速,所以对磁头来说在单位时间内转过的角度影响读写二进制位的数量,
而磁头扫过的面积在这里没有影响。</p>
<div class="panel panel-default">
<div class="panel-heading">
区位记录(来自维基百科 <a class="reference external" href="https://en.wikipedia.org/wiki/Zone_bit_recording">Zone bit recording 词条</a> )</div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/DiskStructure.svg" type="image/svg+xml">
DiskStructure.svg</object>
</div>
</div>
<p>不过随着硬盘容量增加,盘面的数据密度也随之增加,单位面积中理论能容纳的二进制位数量有限。
理论上,如果保持相同密度的话,盘片外圈能比内圈容纳更多数据。因此硬盘厂商们开始在盘面上将磁道划分出
区块(zone),外圈区块中的磁道可以比内圈区块中的磁道多放入一些扇区。这种方式下生产出的硬盘叫
区位记录硬盘(Zone bit recoding, ZBR),相对的传统固定磁道中扇区数的硬盘就被叫做恒定角速度(CAV)
硬盘。</p>
<p>如右图所示,区位记录在硬盘上将多个柱面组合成一个区块,区块内的磁道有相同数量的扇区,
而不同区块的磁道可以有不同数量的扇区,外圈区块比内圈区块有更多扇区。</p>
<p>显然要支持 ZBR ,物理 CHS 寻址方式不再有效,于是 ZBR
硬盘将原本简单的地址换算电路升级为更复杂的磁盘控制器芯片,替代 CPU
来驱动硬盘,把来自文件系统的逻辑 CHS 地址通过换算转换到物理 CHS 地址,并且驱动磁头做跳转和寻址。
从而有了独立的控制芯片之后,硬盘读写扇区的速度不再受 CPU 速度影响。有了完整的逻辑-物理地址转换后,
逻辑扇区编号不再对应物理扇区编号,上述编号跳转和坏扇区处理之类的事情都由磁盘控制芯片代为完成。
从而 CHS 地址已经丧失了物理意义,只留下 <strong>连续读写保证最快的读写速度</strong> 这样的性能约定。</p>
<p>有了 ZBR 之后,硬盘读写速度也不再恒定,虽然仍然保持恒定转速,但是读写外圈磁道时单位时间扫过的扇区
多于读写内圈磁道时扫过的扇区。所以 ZBR 硬盘的低端地址比高端地址有更快的读写速度,
通过硬盘测速软件能观察到阶梯状的「掉速」现象。</p>
<p>逻辑地址转换也会造成逻辑 CHS 寻址能访问到的扇区数少于物理 CHS 寻址的现象,
磁盘中扇区被重新编号后可能有一些扇区剩余,于是 ZBR 硬盘的出厂低级格式化可能会均分这些访问不到的扇区
给每个磁道作为保留扇区,留作坏扇区后备。</p>
<p>另外有了独立磁盘控制器芯片之后,扇区内的校验算法也不再受制于 BIOS INT 13H 接口。
原本 BIOS 的 INT 13H 接口定义了每个扇区 512 字节,额外配有 4 字节校验, 32bit
的校验码对 4096bit 的数据来说,只能允许一些简单的校验算法,比如 32bit CRC ,或者比如
<a class="reference external" href="https://en.wikipedia.org/wiki/Hamming_code">汉明码</a> 对 4096bit 的数据需要 13bit
的校验。突破了校验算法限制后硬盘可以在物理扇区中放更多校验位,使用更复杂的
<a class="reference external" href="https://en.wikipedia.org/wiki/Error_correction_code">ECC</a> 算法,提供更强的容错性。
IDE/SATA 接口的硬盘由内部控制器负责计算和比对校验,而 SAS 接口的硬盘(主要用于服务器)可以读取
520/528 字节长度的扇区,包含额外校验位。</p>
<p>通过 ZBR ,逻辑 CHS 寻址不再局限在具体每磁道扇区数等物理限制上,但是仍然局限在 CHS 总位数。
24bit 的 CHS 地址能寻址 <span class="math">\(1024*256*63 = 16515072\)</span> 个扇区,也就是 8064MiB 的空间。
于是早期很多操作系统有 7.8G 硬盘大小的限制。后来 ATA/IDE 标准提升了 CHS 寻址数量,从 24bit
到 28bit 到 32bit ,不过在系统引导早期仍然依赖 BIOS 最基本的 24bit CHS
寻址能力,于是那时候安装系统时要求引导程序装在前 8G 范围内也是这个原因。</p>
</div>
<div class="section" id="chs-lba">
<h2><a class="toc-backref" href="#id11">从 CHS 到 LBA</a></h2>
<p>随着硬盘大小不断提升,无论是操作系统软件层,还是硬盘厂商硬件层,都逐渐意识到逻辑 CHS
寻址是两边相互欺骗对方的骗局:文件系统根据假的 CHS 地址的提示苦苦优化,而硬盘控制器又要把物理
CHS 模拟到假的 CHS 地址上以兼容 BIOS 和操作系统。和 CS 领域太多别的事情一样,
CHS 寻址过早地暴露出太多底层抽象细节,而上层软件又转而依赖于这些暴露出的细节进行优化,
底层细节的变动使得上层优化不再是有意义的优化。</p>
<p>于是 <a class="reference external" href="https://en.wikipedia.org/wiki/Parallel_ATA">ATA 标准</a> 引入了
<a class="reference external" href="https://en.wikipedia.org/wiki/Logical_block_addressing">逻辑块寻址(Logical Block Addressing, LBA)</a>
来替代 CHS 寻址,解决其中的混乱。LBA 的思路其实就是逻辑 CHS 寻址的简单换算,因为
CHS 寻址下 S 从 1 开始计算,而 LBA 使用连续扇区编号,从 0 开始编号,所以换算公式如下:</p>
<div class="math">
\begin{equation*}
LBA 地址 = ( C \times 磁头数 + H ) \times 扇区数 + ( S - 1 )
\end{equation*}
</div>
<p>使用 LBA 寻址,操作系统和文件系统直接寻址一个连续地址空间中的扇区号,
不应该关心柱面和磁头之类的物理参数,将这些物理细节交由磁盘控制器。
对操作系统和文件系统这些上层软件而言,LBA寻址的抽象仍然保证了 <strong>连续读写提供最快的读写速度</strong>
,文件系统仍然会尝试根据 LBA 地址优化,尽量连续读写从而减少寻道时间。</p>
<p>从 CHS 寻址切换到 LBA 寻址,需要硬盘和操作系统两方面的努力,所以很长一段时间,
硬盘同时支持两种寻址方式,在控制器内部做转换。最后需要放弃支持的是深植了 CHS 寻址的 BIOS
,使用 BIOS 引导的 MBR 引导程序还在用 CHS 寻址方式读取数据加载操作系统,直到大家都切换到 UEFI 。</p>
<p>并且随着硬盘使用 LBA 寻址,导致上层软件很难预测底层硬件实际切换柱面切换磁头之类的时机,
潜在地导致一些性能不确定性。于是硬盘控制器在除了负责实际驱动物理磁盘之外,
还开始负责维护一块盘内缓冲区,实现盘内的 IO 队列。缓冲区的存在允许磁盘控制器同时接收更多来自上层软件
的读写请求,转换成实际物理布局参数,并根据磁盘物理布局来调整读写顺序,增加总体吞吐率。
比如 <a class="reference external" href="https://en.wikipedia.org/wiki/Tagged_Command_Queuing">ATA TCQ</a> 和
<a class="reference external" href="https://en.wikipedia.org/wiki/Native_Command_Queuing">SATANCQ</a>
就是这样的盘内队列协议。</p>
<p>当然有缓冲区和盘内队列的存在也使得突然断电之类的情况下更难保证数据一致性,于是 SCSI/SATA
标准开始约定特殊的请求,从操作系统能发送命令让底层设备清空自己的读写队列。</p>
</div>
<div class="section" id="shingled-magnetic-recording-smr">
<h2><a class="toc-backref" href="#id12">叠瓦磁记录(Shingled Magnetic Recording, SMR)</a></h2>
<p>逐渐从历史讲到了现在,随着硬盘记录密度的不断增加,硬盘厂商们也在不断发明新技术尝试突破磁盘记录的物理极限。
因为有了在硬盘上独立的控制器,并且切换到了逻辑块地址(LBA)的寻址方式,
操作系统大部分时候不用再关心底层硬盘的物理技术革新,比如垂直写入技术(perpendicular magnetic
recording, PMR)将磁头记录方式从水平转换成垂直记录,增加了记录密度,但不影响寻址方式。</p>
<div class="panel panel-default">
<div class="panel-heading">
叠瓦磁记录(来自 <a class="reference external" href="https://ieeexplore.ieee.org/document/4782114">The Feasibility of Magnetic Recording at 10 Terabits Per Square Inch on Conventional Media</a> )</div>
<div class="panel-body">
<img alt="smr.png" class="img-responsive" src="//farseerfc.me/zhs/images/smr.png"/>
</div>
</div>
<p>不过技术革新中也有影响寻址方式的技术,比如
<a class="reference external" href="https://en.wikipedia.org/wiki/Shingled_magnetic_recording">叠瓦磁记录技术(Shingled Magnetic Recording, SMR)</a>
。 SMR 技术基于一个技术事实:物理上磁头的写入头(write head)需要比读取头(read head
)占用更大面积,如果按照写入头的物理极限放置磁记录,那么对于读取头会有很多空间浪费。从而
SMR 试图让相邻磁道的写入有部分重叠,从而增加记录密度。即便重叠了相邻磁道,读取磁道还是能随机定位,
而写入磁道会覆盖它后面叠加上的磁道,所以写入磁道必须严格按地址顺序写入。为了满足随机顺序写入的需要,
SMR 硬盘把连续的几个磁道组织成区块(zone),在一个区块内必须按顺序写入。
这里的区块可以和区位记录(ZBR)是同样的区块,也可以独立于 ZBR 做不同大小的区块分割。</p>
<p>这种区块内连续写入的要求,很像是 SSD 这种基于闪存介质的记录方式, SMR 硬盘也同样像 SSD
一样在磁盘控制器内引入 <a class="reference external" href="//farseerfc.me/zhs/btrfs-vs-zfs-difference-in-implementing-snapshots.html#id21">日志结构式的记录方式,采用类似的 GC 算法</a>
,收到随机写入请求的时候,在区块间执行 GC 搬运数据块,对操作系统提供可以任意写入的抽象接口。</p>
<p>当然这种类似闪存介质的 FTL 的抽象有对读写性能的直接影响。SMR 硬盘可以将这些细节完全隐藏起来(
Device Managed),或者完全暴露给宿主系统(Host Managed
),或者在读写时隐藏细节的同时在宿主想查询的时候提供接口查询(Host Aware)。和 SSD
一样,消费级的 SMR 硬盘通常选择隐藏细节只在被询问时暴露,完全暴露细节的设备通常只在企业服务器级别
的产品中看到。</p>
<p>可以期待,随着 SMR 硬盘的逐渐普及,文件系统设计中也将更多考虑 SMR 的特性加以优化。这些优化可能参考
对 SSD 的优化(比如尽量连续写入),但是又不能完全照搬(比如 SSD 需要考虑写平衡而 SMR
硬盘不需要,比如 SSD 不用担心随机寻道时间而 SMR 硬盘需要)。这些对现在和未来文件系统的设计提供了更多挑战。</p>
</div>
<div class="section" id="kib">
<h2><a class="toc-backref" href="#id13">4KiB 扇区大小</a></h2>
<p>不局限于硬盘,存储设备发展中另一个方向是增加扇区大小。如前所述,在应用于 PC
之前的硬盘设计也曾有过比 512 字节更小的扇区大小,而自从 PC 普及之后 512 字节扇区逐渐成为主流,
甚至到了挥之不去的地步。随着硬盘容量提升,直接寻址 512 字节的扇区显得不再那么高效,
文件系统内部也早已把多个扇区合并成一个逻辑簇(cluster)或者块(block),按簇或块的粒度管理。
在底层硬件同样也是按照 512 字节大小划分扇区,每个扇区都要独立计算校验,如果能增大扇区大小到比如
4KiB,将能更经济地安排扇区校验码,从而得到更多可用容量。可见 512 字节扇区大小这一设计,和
CHS 寻址一样,逐渐成为了操作系统和硬盘厂商彼此间互相努力维护的谎言。</p>
<p>硬盘物理扇区提升为 4KiB 大小的设计,叫做「
<a class="reference external" href="https://en.wikipedia.org/wiki/Advanced_Format">先进格式化(Advanced Format)</a>
」,这样的硬盘叫做先进格式化硬盘(AFD)。在此基础上,硬盘控制器可以提供模拟 512 字节扇区的模拟层,
叫做 512e ,也可以直接提供 4K 大小的扇区给操作系统,叫做 4K native (4Kn)。
操作系统和文件系统要尽量避免依赖 512e 以提供最优性能,支持 4Kn 扇区寻址也是现在和未来
文件系统设计中一个重要挑战。</p>
</div>
<div class="section" id="dual-actuator">
<h2><a class="toc-backref" href="#id14">双磁头臂(Dual Actuator)</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
双磁头臂(来自 <a class="reference external" href="http://www.digitalpreservation.gov/meetings/DSA2018/Day_1/5_TO_P2-2_Trantham_Disk_Trends.pdf">Seagate Storage Update - LOC Designing Storage Architecture for Digital Collections</a> )</div>
<div class="panel-body">
<img alt="DualActuator-Opposed.jpeg" class="img-responsive" src="//farseerfc.me/zhs/images/DualActuator-Opposed.jpeg"/>
<img alt="DualActuator-Split.jpeg" class="img-responsive" src="//farseerfc.me/zhs/images/DualActuator-Split.jpeg"/>
</div>
</div>
<p>除了提升容量,硬盘发展的另一个方向是提升读写速度。通过上述 CHS 寻址方式可见,
传统方式下提升硬盘读写速度有两种方式:</p>
<ol class="arabic simple">
<li>提升磁记录密度</li>
<li>提升(磁头臂和盘片)转速</li>
</ol>
<p>第一种方式提升记录密度,在增加容量的同时也能提升硬盘读写速度,所以是长久以来硬盘厂商的主要方式。
第二种方式提升转速则很快就遇到了物理瓶颈,硬盘以前是 5400rpm 现在最高能到 15000rpm
附近,高速旋转的盘片就像一个螺旋桨一样,外圈线速度已经到了接近声速,很难再往上提升。
以及盘片转速影响连续读写速度,而磁头臂转速影响寻道速度,高速寻道对磁头臂旋转有极高精度要求。</p>
<p>所以长久以来,衡量硬盘速度有两项指标:连续读写速度和每秒操作数(IOPS),随着容量提升,
也在提升连续读写速度,但是很难提升 IOPS ,相对而言随机寻道所需的开销越来越昂贵。</p>
<p>目前硬盘厂商们在尝试一种新的方式提升硬盘 IOPS :增加一条磁头臂。一个硬盘驱动器内封入两组甚至多组
磁头臂,每个磁头臂能独立旋转,从而能独立寻址定位。这样的硬盘叫双/多磁头臂(Dual/Multi Actuator)硬盘。</p>
<p>从操作系统角度来看,双磁头臂硬盘更像是一根连接线上接有等容量的两个独立驱动器,
可以在盘内控制器上组 RAID0 ,或者把两个磁头臂都暴露给操作系统,由操作系统组 RAID0
或更智能地使用独立寻址的能力。</p>
</div>
<div class="section" id="tl-dr">
<h2><a class="toc-backref" href="#id15">结论(TL;DR)和预告</a></h2>
<p>软件层面的优化与硬件层面的革新一直是一组矛盾。长久以来文件系统和硬盘设备在关于寻址方式的磨合中,
逐渐演化出一条真理,也是我文中一直在强调的: <strong>连续读写提供最快的读写速度</strong>
。文件系统总是能根据底层设备暴露出的一些抽象泄漏,比如物理 CHS 布局,比如 512 字节扇区大小,
针对性做更多优化,但是随着底层设备的技术革新这些优化也随之成为泡影。</p>
<p>从 SMR 技术中也能看出, 硬盘的读写接口也在逐渐向 SSD 的接口靠拢,从而文件系统的「优化」也在逐渐
向这种「倾向顺序写入」的方向优化。关于这些发展趋势待我有空再谈。</p>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>Btrfs vs ZFS 实现 snapshot 的差异2020-02-19T15:45:00+09:002020-02-19T15:45:00+09:00farseerfctag:farseerfc.me,2020-02-19:/zhs/btrfs-vs-zfs-difference-in-implementing-snapshots.html
<!-- -->
<blockquote>
<div class="line-block">
<div class="line">zfs 这个东西倒是名不符实。叫 z storage stack 明显更符合。 叫 fs 但不做 fs 自然确实会和 btrfs 有很大出入。</div>
<div class="line">我反而以前还好奇为什么 btrfs 不弄 zvol , 直到我意识到这东西真是一个 fs ,名符奇实。</div>
<div class="line">—— 某不愿透露姓名的 Ext2FSD 开发者</div>
</div>
</blockquote>
<p>Btrfs 和 ZFS 都是开源的写时拷贝(Copy on Write, CoW)文件系统,都提供了相似的子卷管理和
快照(snapshot)的功能。网上有不少文章都评价 ZFS 实现 CoW FS 的创新之处,进而想说「 Btrfs
只是 Linux/GPL 阵营对 ZFS …</p>
<!-- -->
<blockquote>
<div class="line-block">
<div class="line">zfs 这个东西倒是名不符实。叫 z storage stack 明显更符合。 叫 fs 但不做 fs 自然确实会和 btrfs 有很大出入。</div>
<div class="line">我反而以前还好奇为什么 btrfs 不弄 zvol , 直到我意识到这东西真是一个 fs ,名符奇实。</div>
<div class="line">—— 某不愿透露姓名的 Ext2FSD 开发者</div>
</div>
</blockquote>
<p>Btrfs 和 ZFS 都是开源的写时拷贝(Copy on Write, CoW)文件系统,都提供了相似的子卷管理和
快照(snapshot)的功能。网上有不少文章都评价 ZFS 实现 CoW FS 的创新之处,进而想说「 Btrfs
只是 Linux/GPL 阵营对 ZFS 的拙劣抄袭」。或许(在存储领域人尽皆知而在领域外)鲜有人知,在
ZFS 之前就有 <a class="reference external" href="https://en.wikipedia.org/wiki/NetApp">NetApp</a> 的商业产品
<a class="reference external" href="https://en.wikipedia.org/wiki/Write_Anywhere_File_Layout">WAFL (Write Anywhere File Layout)</a>
实现了 CoW 语义的文件系统,并且集成了快照和卷管理之类的功能。描述 btrfs 原型设计的
<a class="reference external" href="https://btrfs.wiki.kernel.org/images-btrfs/6/68/Btree_TOS.pdf">论文</a>
和 <a class="reference external" href="https://btrfs.wiki.kernel.org/images-btrfs/6/63/LinuxFS_Workshop.pdf">发表幻灯片</a>
也明显提到 WAFL 比提到 ZFS 更多一些,应该说 WAFL 这样的企业级存储方案才是 ZFS 和 btrfs
共同的灵感来源,而无论是 ZFS 还是 btrfs 在其设计中都汲取了很多来自 WAFL 的经验教训。</p>
<p>我一开始也带着「 Btrfs 和 ZFS
都提供了类似的功能,因此两者必然有类似的设计」这样的先入观念,尝试去使用这两个文件系统,
却经常撞上两者细节上的差异,导致使用时需要不尽相同的工作流,
或者看似相似的用法有不太一样的性能表现,又或者一边有的功能,比如 ZFS 的在线去重(in-band dedup)
, Btrfs 的 reflink ,在另一边没有的情况,进而需要不同细粒度的子卷划分方案。后来看到了
<a class="reference external" href="https://lwn.net/Articles/342892/">LWN 的这篇 《A short history of btrfs》</a>
让我意识到 btrfs 和 ZFS 虽然表面功能上看起来类似,但是实现细节上完全不一样,
所以需要不一样的用法,适用于不一样的使用场景。</p>
<p>为了更好地理解这些差异,我四处搜罗这两个文件系统的实现细节,于是有了这篇笔记,
记录一下我查到的种种发现和自己的理解。<del>(或许会写成一个系列?还是先别乱挖坑不填。)</del>
只是自己的笔记,所有参阅的资料文档都是二手资料,没有深挖过源码,还参杂了自己的理解,
于是难免有和事实相违的地方,如有写错,还请留言纠正。</p>
<div class="section" id="btrfs">
<h2><a class="toc-backref" href="#id34">1 Btrfs 的子卷和快照</a></h2>
<p>关于写时拷贝(CoW)文件系统的优势,我们为什么要用 btrfs/zfs 这样的写时拷贝文件系统,
而不是传统的文件系统设计,或者写时拷贝文件系统在使用时有什么区别之类的,网上同样也能找到很多介绍
,这里不想再讨论。这里假设你用过 btrfs/zfs 至少一个的快照功能,知道它该怎么用,
并且想知道更多细节,判断怎么用那些功能才合理。</p>
<p>先从两个文件系统中(表面上看起来)比较简单的 btrfs 的子卷(subvolume)和快照(snapshot)说起。
关于子卷和快照的常规用法、推荐布局之类的话题就不细说了,网上能找到很多不错的资料,比如
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Subvolumes">btrfs wiki 的 SysadminGuide 页</a>
和 Arch wiki 上 <a class="reference external" href="https://wiki.archlinux.org/index.php/Btrfs#Subvolumes">Btrfs#Subvolumes</a> 页都有不错的参考价值。</p>
<div class="section" id="id4">
<h3><a class="toc-backref" href="#id35">1.1 子卷和快照的术语</a></h3>
<p>在 btrfs 中,存在于存储媒介中的只有「子卷」的概念,「快照」只是个创建「子卷」的方式,
换句话说在 btrfs 的术语里,子卷(subvolume)是个名词,而快照(snapshot)是个动词。
如果脱离了 btrfs 术语的上下文,或者不精确地称呼的时候,也经常有文档把 btrfs
的快照命令创建出的子卷叫做一个快照,所以当提到快照的时候,根据上下文判断这里是个动词还是名词,
把名词的快照当作用快照命令创建出的子卷就可以了。或者我们可以理解为,
<strong>互相共享一部分元数据(metadata)的子卷互为彼此的快照(名词)</strong> ,
那么按照这个定义的话,在 btrfs 中创建快照(名词)的方式其实有两种:</p>
<ol class="arabic simple">
<li>用 <code class="code">
btrfs subvolume snapshot</code>
命令创建快照</li>
<li>用 <code class="code">
btrfs send</code>
命令并使用 <code class="code">
-p</code>
参数发送快照,并在管道另一端接收</li>
</ol>
<div class="panel panel-default">
<div class="panel-heading">
<code class="code">
btrfs send</code>
命令的 <code class="code">
-p</code>
与 <code class="code">
-c</code>
</div>
<div class="panel-body">
<p>这里也顺便提一下 <code class="code">
btrfs send</code>
命令的 <code class="code">
-p</code>
参数和 <code class="code">
-c</code>
参数的差异。
只看 <a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/Manpage/btrfs-send#DESCRIPTION">btrfs-send(8)</a> 的描述的话:</p>
<blockquote>
<div class="line-block">
<div class="line">-p <parent></div>
<div class="line-block">
<div class="line">send an incremental stream from parent to subvol</div>
<div class="line"><br/></div>
</div>
<div class="line">-c <clone-src></div>
<div class="line-block">
<div class="line">use this snapshot as a clone source for an incremental send (multiple allowed)</div>
</div>
</div>
</blockquote>
<p>看起来这两个都可以用来生成两个快照之间的差分,只不过 -p 只能指定一个「parent」,
而 -c 能指定多个「clone source」。在
<a class="reference external" href="https://unix.stackexchange.com/a/490857">unix stackexchange 上有人写明了这两个的异同</a>
。使用 -p 的时候,产生的差分首先让接收端用 subvolume snapshot 命令对 parent 子卷创建一个快照,
然后发送指令将这个快照修改成目标子卷的样子,而使用 -c 的时候,首先在接收端用 subvolume create
创建一个空的子卷,随后发送指令在这个子卷中填充内容,其数据块尽量共享 clone source 已有的数据。
所以 <code class="code">
btrfs send -p</code>
在接收端产生是有共享元数据的快照,而 <code class="code">
btrfs send -c</code>
在接收端产生的是仅仅共享数据而不共享元数据的子卷。</p>
</div>
</div>
<p>定义中「互相共享一部分 <strong>元数据</strong> 」比较重要,因为除了快照的方式之外, btrfs
的子卷间也可以通过 reflink 的形式共享数据块。我们可以对一整个子卷(甚至目录)执行
<code class="code">
cp -r --reflink=always</code>
,创建出一个副本,副本的文件内容通过 reflink
共享原本的数据,但不共享元数据,这样创建出的就不是快照。</p>
<p>说了这么多,其实关键的只是 btrfs 在传统 Unix 文件系统的「目录/文件/inode」
这些东西之外只增加了一个「子卷」的新概念,而子卷间可以共享元数据或者数据,
用快照命令创建出的子卷就是共享一部分元数据。</p>
</div>
<div class="section" id="id5">
<h3><a class="toc-backref" href="#id36">1.2 于是子卷在存储介质中是如何记录的呢?</a></h3>
<p>首先要说明, btrfs 中大部分长度可变的数据结构都是
<a class="reference external" href="https://www.usenix.org/legacy/events/lsf07/tech/rodeh.pdf">CoW B-tree</a>
,一种经过修改适合写时拷贝的B树结构,所以在
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/On-disk_Format">on-disk format</a>
中提到了很多个树。这里的树不是指文件系统中目录结构树,而是写时拷贝B树(CoW B-tree,下文简称B树)
,如果不关心B树细节的话可以把 btrfs 所说的一棵树理解为关系数据库中的一个表,
和数据库的表一样 btrfs 的树的长度可变,然后表项内容根据一个 key 排序。</p>
<p>B树结构由索引 key 、中间节点和叶子节点构成。每个 key
是一个 <code class="code">
(uint64_t object_id, uint8_t item_type, uint64_t item_extra)</code>
这样的三元组,三元组每一项的具体含义由 item_type 定义。 key
三元组构成了对象的概念,每个对象(object)在树中用一个或多个表项(item)描述,同 object_id
的表项共同描述一个对象。B树中的 key 只用来比较大小而不必连续,从而 object_id
也不必连续,只是按大小排序。有一些预留的 object_id 不能用作别的用途,他们的编号范围是
-255ULL 到 255ULL,也就是表中前 255 和最后 255 个编号预留。</p>
<p>B树中间节点和叶子节点结构大概像是这个样子:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: btree_nodes Pages: 1 -->
<svg class="svg-responsive" height="389pt" viewbox="0.00 0.00 529.00 389.00" width="529pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 385)">
<title>btree_nodes</title>
<polygon fill="#ffffff" points="-4,4 -4,-385 525,-385 525,4 -4,4" stroke="transparent"></polygon>
<!-- btree_node -->
<g class="node" id="node1">
<title>btree_node</title>
<polygon fill="none" points="0,-242.5 0,-380.5 166,-380.5 166,-242.5 0,-242.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-365.3">header TREE_NODE</text>
<polyline fill="none" points="0,-357.5 166,-357.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-342.3">key0: address</text>
<polyline fill="none" points="0,-334.5 166,-334.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-319.3">key10: address</text>
<polyline fill="none" points="0,-311.5 166,-311.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-296.3">key20: address</text>
<polyline fill="none" points="0,-288.5 166,-288.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-273.3">...</text>
<polyline fill="none" points="0,-265.5 166,-265.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="83" y="-250.3">free space</text>
</g>
<!-- btree_leaf1 -->
<g class="node" id="node2">
<title>btree_leaf1</title>
<polygon fill="none" points="238,-.5 238,-322.5 403,-322.5 403,-.5 238,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-307.3">header LEAF_NODE</text>
<polyline fill="none" points="238,-299.5 403,-299.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-284.3">key0: offset, size</text>
<polyline fill="none" points="238,-276.5 403,-276.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-261.3">key1: offset, size</text>
<polyline fill="none" points="238,-253.5 403,-253.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-238.3">key2: offset, size</text>
<polyline fill="none" points="238,-230.5 403,-230.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-215.3">...</text>
<polyline fill="none" points="238,-207.5 403,-207.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-192.3">keyN offset, size</text>
<polyline fill="none" points="238,-184.5 403,-184.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-169.3"> </text>
<polyline fill="none" points="238,-161.5 403,-161.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-146.3">free space</text>
<polyline fill="none" points="238,-138.5 403,-138.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-123.3"> </text>
<polyline fill="none" points="238,-115.5 403,-115.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-100.3">dataN</text>
<polyline fill="none" points="238,-92.5 403,-92.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-77.3">...</text>
<polyline fill="none" points="238,-69.5 403,-69.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-54.3">data2</text>
<polyline fill="none" points="238,-46.5 403,-46.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-31.3">data1</text>
<polyline fill="none" points="238,-23.5 403,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="320.5" y="-8.3">data0</text>
</g>
<!-- btree_node->btree_leaf1 -->
<g class="edge" id="edge1">
<title>btree_node:key00->btree_leaf1:label</title>
<path d="M166.2568,-311.5C186.1234,-311.5 207.4684,-311.5 227.7961,-311.5" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="228,-315.0001 238,-311.5 228,-308.0001 228,-315.0001" stroke="#000000"></polygon>
</g>
<!-- btree_leaf1->btree_leaf1 -->
<g class="edge" id="edge2">
<title>btree_leaf1:e->btree_leaf1:e</title>
<path d="M403.5,-288.5C442.3333,-297.5 521,-297.5 521,-150 521,-14.3115 454.4278,-3.4458 413.5345,-9.5821" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="412.6651,-6.1849 403.5,-11.5 413.9793,-13.0604 412.6651,-6.1849" stroke="#000000"></polygon>
</g>
<!-- btree_leaf1->btree_leaf1 -->
<g class="edge" id="edge3">
<title>btree_leaf1:w->btree_leaf1:w</title>
<path d="M237.5,-265.5C198.6667,-274.5 120,-274.5 120,-150 120,-35.4697 186.5722,-26.2984 227.4655,-32.5703" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="227.0189,-36.0485 237.5,-34.5 228.3409,-29.1745 227.0189,-36.0485" stroke="#000000"></polygon>
</g>
<!-- btree_leaf1->btree_leaf1 -->
<g class="edge" id="edge4">
<title>btree_leaf1:e->btree_leaf1:e</title>
<path d="M403.5,-242.5C442.3333,-251.5 521,-251.5 521,-150 521,-56.6279 454.4278,-49.1509 413.5345,-55.5585" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="412.6531,-52.1641 403.5,-57.5 413.9828,-59.0366 412.6531,-52.1641" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>由此,每个中间节点保存一系列 key 到叶子节点的指针,而叶子节点内保存一系列 item ,每个 item
固定大小,并指向节点内某个可变大小位置的 data 。从而逻辑上一棵B树可以包含任何类型的
item ,每个 item 都可以有可变大小的附加数据。通过这样的B树结构,可以紧凑而灵活地表达很多数据类型。</p>
<p>有这样的背景之后,比如在
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Flat">SysadminGuide 这页的 Flat 布局</a>
有个子卷布局的例子。</p>
<pre class="code literal-block">
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)
</pre>
<p>用圆柱体表示子卷的话画成图大概是这个样子:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: Flat_layout Pages: 1 -->
<svg class="svg-responsive" height="206pt" viewbox="0.00 0.00 287.00 206.00" width="287pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 202)">
<title>Flat_layout</title>
<polygon fill="#ffffff" points="-4,4 -4,-202 283,-202 283,4 -4,4" stroke="transparent"></polygon>
<!-- toplevel -->
<g class="node" id="node1">
<title>toplevel</title>
<path d="M74,-113.7273C74,-115.5331 57.416,-117 37,-117 16.584,-117 0,-115.5331 0,-113.7273 0,-113.7273 0,-84.2727 0,-84.2727 0,-82.4669 16.584,-81 37,-81 57.416,-81 74,-82.4669 74,-84.2727 74,-84.2727 74,-113.7273 74,-113.7273" fill="none" stroke="#000000"></path>
<path d="M74,-113.7273C74,-111.9214 57.416,-110.4545 37,-110.4545 16.584,-110.4545 0,-111.9214 0,-113.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="37" y="-95.3">toplevel</text>
</g>
<!-- root -->
<g class="node" id="node2">
<title>root</title>
<path d="M176.5,-194.7273C176.5,-196.5331 164.3982,-198 149.5,-198 134.6018,-198 122.5,-196.5331 122.5,-194.7273 122.5,-194.7273 122.5,-165.2727 122.5,-165.2727 122.5,-163.4669 134.6018,-162 149.5,-162 164.3982,-162 176.5,-163.4669 176.5,-165.2727 176.5,-165.2727 176.5,-194.7273 176.5,-194.7273" fill="none" stroke="#000000"></path>
<path d="M176.5,-194.7273C176.5,-192.9214 164.3982,-191.4545 149.5,-191.4545 134.6018,-191.4545 122.5,-192.9214 122.5,-194.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="149.5" y="-176.3">root</text>
</g>
<!-- toplevel->root -->
<g class="edge" id="edge1">
<title>toplevel->root</title>
<path d="M60.5079,-116.7348C74.7361,-127.3823 93.2999,-141.1218 110,-153 111.8699,-154.33 113.7963,-155.6874 115.7451,-157.0508" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="114.115,-160.1795 124.3276,-163 118.1029,-154.4265 114.115,-160.1795" stroke="#000000"></polygon>
</g>
<!-- home -->
<g class="node" id="node3">
<title>home</title>
<path d="M177.5,-140.7273C177.5,-142.5331 164.95,-144 149.5,-144 134.05,-144 121.5,-142.5331 121.5,-140.7273 121.5,-140.7273 121.5,-111.2727 121.5,-111.2727 121.5,-109.4669 134.05,-108 149.5,-108 164.95,-108 177.5,-109.4669 177.5,-111.2727 177.5,-111.2727 177.5,-140.7273 177.5,-140.7273" fill="none" stroke="#000000"></path>
<path d="M177.5,-140.7273C177.5,-138.9214 164.95,-137.4545 149.5,-137.4545 134.05,-137.4545 121.5,-138.9214 121.5,-140.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="149.5" y="-122.3">home</text>
</g>
<!-- toplevel->home -->
<g class="edge" id="edge2">
<title>toplevel->home</title>
<path d="M74.1485,-107.9156C86.1488,-110.7957 99.4652,-113.9916 111.5245,-116.8859" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="110.7613,-120.302 121.302,-119.2325 112.395,-113.4953 110.7613,-120.302" stroke="#000000"></polygon>
</g>
<!-- var -->
<g class="node" id="node4">
<title>var</title>
<polygon fill="none" points="176.5,-90 173.5,-94 152.5,-94 149.5,-90 122.5,-90 122.5,-54 176.5,-54 176.5,-90" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="149.5" y="-68.3">var</text>
</g>
<!-- toplevel->var -->
<g class="edge" id="edge3">
<title>toplevel->var</title>
<path d="M74.1485,-90.0844C86.4488,-87.1323 100.1319,-83.8484 112.4265,-80.8976" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="113.4596,-84.2492 122.3666,-78.512 111.8259,-77.4425 113.4596,-84.2492" stroke="#000000"></polygon>
</g>
<!-- postgres -->
<g class="node" id="node6">
<title>postgres</title>
<path d="M189,-32.7273C189,-34.5331 171.2955,-36 149.5,-36 127.7045,-36 110,-34.5331 110,-32.7273 110,-32.7273 110,-3.2727 110,-3.2727 110,-1.4669 127.7045,0 149.5,0 171.2955,0 189,-1.4669 189,-3.2727 189,-3.2727 189,-32.7273 189,-32.7273" fill="none" stroke="#000000"></path>
<path d="M189,-32.7273C189,-30.9214 171.2955,-29.4545 149.5,-29.4545 127.7045,-29.4545 110,-30.9214 110,-32.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="149.5" y="-14.3">postgres</text>
</g>
<!-- toplevel->postgres -->
<g class="edge" id="edge5">
<title>toplevel->postgres</title>
<path d="M60.5079,-81.2652C74.7361,-70.6177 93.2999,-56.8782 110,-45 111.5738,-43.8806 113.1875,-42.7418 114.821,-41.5965" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="116.8432,-44.4534 123.0588,-35.8733 112.8492,-38.7046 116.8432,-44.4534" stroke="#000000"></polygon>
</g>
<!-- www -->
<g class="node" id="node5">
<title>www</title>
<path d="M279,-86.7273C279,-88.5331 266.8982,-90 252,-90 237.1018,-90 225,-88.5331 225,-86.7273 225,-86.7273 225,-57.2727 225,-57.2727 225,-55.4669 237.1018,-54 252,-54 266.8982,-54 279,-55.4669 279,-57.2727 279,-57.2727 279,-86.7273 279,-86.7273" fill="none" stroke="#000000"></path>
<path d="M279,-86.7273C279,-84.9214 266.8982,-83.4545 252,-83.4545 237.1018,-83.4545 225,-84.9214 225,-86.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="252" y="-68.3">www</text>
</g>
<!-- var->www -->
<g class="edge" id="edge4">
<title>var->www</title>
<path d="M176.699,-72C188.2828,-72 201.9866,-72 214.5073,-72" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="214.6739,-75.5001 224.6739,-72 214.6739,-68.5001 214.6739,-75.5001" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>上图例子中的 Flat 布局在 btrfs 中大概是这样的数据结构,
其中实线箭头是B树一系列中间节点和叶子节点,逻辑上指向一棵B树,虚线箭头是根据
inode 号之类的编号的引用:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: Flat_layout_on_disk Pages: 1 -->
<svg class="svg-responsive" height="499pt" viewbox="0.00 0.00 1130.00 499.00" width="1130pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 495)">
<title>Flat_layout_on_disk</title>
<polygon fill="#ffffff" points="-4,4 -4,-495 1126,-495 1126,4 -4,4" stroke="transparent"></polygon>
<!-- superblock -->
<g class="node" id="node1">
<title>superblock</title>
<polygon fill="none" points="0,-315.5 0,-407.5 123,-407.5 123,-315.5 0,-315.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-392.3">SUPERBLOCK</text>
<polyline fill="none" points="0,-384.5 123,-384.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-369.3">...</text>
<polyline fill="none" points="0,-361.5 123,-361.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-346.3">root_tree</text>
<polyline fill="none" points="0,-338.5 123,-338.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-323.3">...</text>
</g>
<!-- roottree -->
<g class="node" id="node2">
<title>roottree</title>
<polygon fill="none" points="195,-62 195,-361 504,-361 504,-62 195,-62" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-345.8">ROOT_TREE</text>
<polyline fill="none" points="195,-338 504,-338 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-322.8">2: extent_tree</text>
<polyline fill="none" points="195,-315 504,-315 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-299.8">3: chunk_tree</text>
<polyline fill="none" points="195,-292 504,-292 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-276.8">4: dev_tree</text>
<polyline fill="none" points="195,-269 504,-269 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-253.8">5: fs_tree</text>
<polyline fill="none" points="195,-246 504,-246 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-230.8">6: root_dir "default" -> ROOT_ITEM 256</text>
<polyline fill="none" points="195,-223 504,-223 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-207.8">10: free_space_tree</text>
<polyline fill="none" points="195,-200 504,-200 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-184.8">256: fs_tree "root"</text>
<polyline fill="none" points="195,-177 504,-177 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-161.8">257: fs_tree "home"</text>
<polyline fill="none" points="195,-154 504,-154 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-138.8">258: fs_tree "www"</text>
<polyline fill="none" points="195,-131 504,-131 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-115.8">259: fs_tree "postgres"</text>
<polyline fill="none" points="195,-108 504,-108 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-92.8">-7: tree_log_tree</text>
<polyline fill="none" points="195,-85 504,-85 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-69.8">-5: orphan_root</text>
</g>
<!-- superblock->roottree -->
<g class="edge" id="edge1">
<title>superblock:sn_root->roottree:label</title>
<path d="M123,-349.5C151.375,-349.5 160.8795,-349.5 184.9792,-349.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="185,-353.0001 195,-349.5 185,-346.0001 185,-353.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- roottree->roottree -->
<g class="edge" id="edge8">
<title>roottree:e->roottree:e</title>
<path d="M504.5,-234.5C552,-243.5 648,-243.5 648,-211.5 648,-181.625 564.3267,-179.6411 514.5622,-186.8328" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="513.7934,-183.4124 504.5,-188.5 514.9376,-190.3183 513.7934,-183.4124" stroke="#000000"></polygon>
</g>
<!-- toplevel -->
<g class="node" id="node3">
<title>toplevel</title>
<polygon fill="none" points="576,-195.5 576,-425.5 923,-425.5 923,-195.5 576,-195.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-410.3">FS_TREE "toplevel"</text>
<polyline fill="none" points="576,-402.5 923,-402.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-387.3"> </text>
<polyline fill="none" points="576,-379.5 923,-379.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-364.3">256: inode_item DIR</text>
<polyline fill="none" points="576,-356.5 923,-356.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-341.3">256: dir_item: "root" -> ROOT_ITEM 256</text>
<polyline fill="none" points="576,-333.5 923,-333.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-318.3">256: dir_item: "home" -> ROOT_ITEM 257</text>
<polyline fill="none" points="576,-310.5 923,-310.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-295.3">256: dir_item: "var" -> INODE_ITEM 257</text>
<polyline fill="none" points="576,-287.5 923,-287.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-272.3">256: dir_item: "postgres" -> ROOT_ITEM 259</text>
<polyline fill="none" points="576,-264.5 923,-264.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-249.3"> </text>
<polyline fill="none" points="576,-241.5 923,-241.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-226.3">257: inode_item DIR</text>
<polyline fill="none" points="576,-218.5 923,-218.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-203.3">257: dir_item: "www" -> ROOT_ITEM 258</text>
</g>
<!-- roottree->toplevel -->
<g class="edge" id="edge7">
<title>roottree:root_fs->toplevel:label</title>
<path d="M504,-257.5C577.0172,-257.5 506.8191,-399.5423 565.8504,-413.4136" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="565.6843,-416.9157 576,-414.5 566.4293,-409.9555 565.6843,-416.9157" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- root -->
<g class="node" id="node4">
<title>root</title>
<polygon fill="none" points="667.5,-444.5 667.5,-490.5 831.5,-490.5 831.5,-444.5 667.5,-444.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-475.3">FS_TREE "root"</text>
<polyline fill="none" points="667.5,-467.5 831.5,-467.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-452.3">256: inode_item DIR</text>
</g>
<!-- roottree->root -->
<g class="edge" id="edge9">
<title>roottree:root_sub_root->root:label</title>
<path d="M504,-188.5C617.92,-188.5 502.6779,-347.3126 576,-434.5 602.8896,-466.4744 619.4489,-477.7394 657.3774,-479.3044" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.4342,-482.8061 667.5,-479.5 657.5695,-475.8074 657.4342,-482.8061" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- home -->
<g class="node" id="node5">
<title>home</title>
<polygon fill="none" points="667.5,-130.5 667.5,-176.5 831.5,-176.5 831.5,-130.5 667.5,-130.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-161.3">FS_TREE "home"</text>
<polyline fill="none" points="667.5,-153.5 831.5,-153.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-138.3">256: inode_item DIR</text>
</g>
<!-- roottree->home -->
<g class="edge" id="edge10">
<title>roottree:root_sub_home->home:label</title>
<path d="M504,-165.5C573.1185,-165.5 592.9293,-165.5 657.2326,-165.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.5,-169.0001 667.5,-165.5 657.5,-162.0001 657.5,-169.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- www -->
<g class="node" id="node6">
<title>www</title>
<polygon fill="none" points="667.5,-65.5 667.5,-111.5 831.5,-111.5 831.5,-65.5 667.5,-65.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-96.3">FS_TREE "www"</text>
<polyline fill="none" points="667.5,-88.5 831.5,-88.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-73.3">256: inode_item DIR</text>
</g>
<!-- roottree->www -->
<g class="edge" id="edge11">
<title>roottree:root_sub_www->www:label</title>
<path d="M504,-142.5C537.4605,-142.5 543.6289,-128.9684 576,-120.5 612.8107,-110.8702 623.9964,-102.1347 657.4378,-100.7044" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.5732,-104.2024 667.5,-100.5 657.431,-97.2039 657.5732,-104.2024" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- postgres -->
<g class="node" id="node7">
<title>postgres</title>
<polygon fill="none" points="667,-.5 667,-46.5 832,-46.5 832,-.5 667,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-31.3">FS_TREE "postgres"</text>
<polyline fill="none" points="667,-23.5 832,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-8.3">256: inode_item DIR</text>
</g>
<!-- roottree->postgres -->
<g class="edge" id="edge12">
<title>roottree:root_sub_postgres->postgres:label</title>
<path d="M504,-119.5C546.8146,-119.5 537.8681,-74.9691 576,-55.5 609.5346,-38.3781 622.9605,-35.847 656.4796,-35.5384" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="656.5135,-39.0384 666.5,-35.5 656.4866,-32.0384 656.5135,-39.0384" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge2">
<title>toplevel:toplevel_dir_root->roottree:root_sub_root</title>
<path d="M576,-345.5C502.9828,-345.5 573.1809,-203.4577 514.1496,-189.5864" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514.3157,-186.0843 504,-188.5 513.5707,-193.0445 514.3157,-186.0843" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge3">
<title>toplevel:toplevel_dir_home->roottree:root_sub_home</title>
<path d="M576,-322.5C502.9828,-322.5 573.1809,-180.4577 514.1496,-166.5864" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514.3157,-163.0843 504,-165.5 513.5707,-170.0445 514.3157,-163.0843" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge5">
<title>toplevel:toplevel_dir_postgres->roottree:root_sub_postgres</title>
<path d="M576,-275.5C503.3669,-275.5 572.8531,-134.3624 514.0988,-120.5795" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514.3154,-117.0828 504,-119.5 513.5713,-124.0431 514.3154,-117.0828" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge6">
<title>toplevel:toplevel_dir_www->roottree:root_sub_www</title>
<path d="M576,-206.5C537.0321,-206.5 544.8212,-153.4834 514.2549,-143.957" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514.3929,-140.4415 504,-142.5 513.4082,-147.3719 514.3929,-140.4415" stroke="#000000"></polygon>
</g>
<!-- toplevel->toplevel -->
<g class="edge" id="edge4">
<title>toplevel:e->toplevel:e</title>
<path d="M923.5,-298.5C989.3333,-307.5 1122,-307.5 1122,-264 1122,-222.624 1001.9728,-220.6037 933.5984,-228.25" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="932.9943,-224.798 923.5,-229.5 933.8542,-231.745 932.9943,-224.798" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>上图中已经隐去了很多和本文无关的具体细节,所有这些细节都可以通过
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/Manpage/btrfs-inspect-internal">btrfs inspect-internal 的 dump-super 和 dump-tree</a>
查看到。</p>
<p>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
,也就是那个顶级子卷,之后可以通过命令 <code class="code">
btrfs subvolume set-default</code>
修改它指向别的子卷,这里它被改为指向 ROOT_ITEM 256 亦即那个名叫 "root" 的子卷。</p>
<p>每一个子卷都有一棵自己的 FS_TREE (有的文档中叫 file tree),一个 FS_TREE 相当于传统 Unix
文件系统中的一整个 inode table ,只不过它除了包含 inode 信息之外还包含所有文件夹内容。在
FS_TREE 中, object_id 同时也是它所描述对象的 inode 号,所以 btrfs
的 <strong>子卷有互相独立的 inode 编号</strong> ,不同子卷中的文件或目录可以拥有相同的 inode 。
或许有人不太清楚子卷间 inode 编号独立意味着什么,简单地说,这意味着你不能跨子卷创建
hard link ,不能跨子卷 mv 移动文件而不产生复制操作。不过因为 reflink 和 inode 无关,
可以跨子卷创建 reflink ,也可以用 reflink + rm 的方式快速「移动」文件(这里移动加引号是因为
inode 变了,传统上不算移动)。</p>
<p>FS_TREE 中一个目录用一个 inode_item 和多个 dir_item 描述, inode_item 是目录自己的 inode
,那些 dir_item 是目录的内容。 dir_item 可以指向别的 inode_item 来描述普通文件和子目录,
也可以指向 root_item 来描述这个目录指向一个子卷。有人或许疑惑,子卷就没有自己的 inode
么?其实如果看 <a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/Data_Structures#btrfs_root_item">数据结构定义</a>
的话 <code class="code">
struct btrfs_root_item</code>
结构在最开头的地方包含了一个
<code class="code">
struct btrfs_inode_item</code>
所以 root_item 也同时作为子卷的 inode
,不过用户通常看不到这个子卷的 inode ,因为子卷在被(手动或自动地)挂载到目录上之后,
用户会看到的是子卷的根目录的 inode 。</p>
<p>比如上图 FS_TREE toplevel 中,有两个对象,第一个 256 是(子卷的)根目录,第二个 257
是 "var" 目录,256 有4个子目录,其中 "root" "home" "postgres" 这三个指向了 ROOT_TREE
中的对应子卷,而 "var" 指向了 inode 257 。然后 257 有一个子目录叫 "www" 它指向了
ROOT_TREE 中 object_id 为 258 的子卷。</p>
</div>
<div class="section" id="id7">
<h3><a class="toc-backref" href="#id37">1.3 那么快照又是如何记录的呢?</a></h3>
<p>以上是子卷、目录、 inode 在 btrfs 中的记录方式,你可能想知道,如何记录一个快照呢?
特别是,如果对一个包含子卷的子卷创建了快照,会得到什么结果呢?如果我们在上面的布局基础上执行:</p>
<div class="highlight"><pre><span class="code-line"><span></span>btrfs subvolume snapshot toplevel toplevel/toplevel@s1</span>
</pre></div>
<p>那么产生的数据结构大概如下所示:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: Flat_layout_on_disk Pages: 1 -->
<svg class="svg-responsive" height="781pt" viewbox="0.00 0.00 1162.00 781.00" width="1162pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 777)">
<title>Flat_layout_on_disk</title>
<polygon fill="#ffffff" points="-4,4 -4,-777 1158,-777 1158,4 -4,4" stroke="transparent"></polygon>
<!-- superblock -->
<g class="node" id="node1">
<title>superblock</title>
<polygon fill="none" points="0,-680.5 0,-772.5 123,-772.5 123,-680.5 0,-680.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-757.3">SUPERBLOCK</text>
<polyline fill="none" points="0,-749.5 123,-749.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-734.3">...</text>
<polyline fill="none" points="0,-726.5 123,-726.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-711.3">root_tree</text>
<polyline fill="none" points="0,-703.5 123,-703.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-688.3">...</text>
</g>
<!-- roottree -->
<g class="node" id="node2">
<title>roottree</title>
<polygon fill="none" points="195,-403.5 195,-725.5 504,-725.5 504,-403.5 195,-403.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-710.3">ROOT_TREE</text>
<polyline fill="none" points="195,-702.5 504,-702.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-687.3">2: extent_tree</text>
<polyline fill="none" points="195,-679.5 504,-679.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-664.3">3: chunk_tree</text>
<polyline fill="none" points="195,-656.5 504,-656.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-641.3">4: dev_tree</text>
<polyline fill="none" points="195,-633.5 504,-633.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-618.3">5: fs_tree</text>
<polyline fill="none" points="195,-610.5 504,-610.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-595.3">6: root_dir "default" -> ROOT_ITEM 256</text>
<polyline fill="none" points="195,-587.5 504,-587.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-572.3">10: free_space_tree</text>
<polyline fill="none" points="195,-564.5 504,-564.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-549.3">256: fs_tree "root"</text>
<polyline fill="none" points="195,-541.5 504,-541.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-526.3">257: fs_tree "home"</text>
<polyline fill="none" points="195,-518.5 504,-518.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-503.3">258: fs_tree "www"</text>
<polyline fill="none" points="195,-495.5 504,-495.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-480.3">259: fs_tree "postgres"</text>
<polyline fill="none" points="195,-472.5 504,-472.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-457.3">260: fs_tree "toplevel@s1"</text>
<polyline fill="none" points="195,-449.5 504,-449.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-434.3">-7: tree_log_tree</text>
<polyline fill="none" points="195,-426.5 504,-426.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-411.3">-5: orphan_root</text>
</g>
<!-- superblock->roottree -->
<g class="edge" id="edge1">
<title>superblock:sn_root->roottree:label</title>
<path d="M123,-714.5C151.375,-714.5 160.8795,-714.5 184.9792,-714.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="185,-718.0001 195,-714.5 185,-711.0001 185,-718.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- roottree->roottree -->
<g class="edge" id="edge10">
<title>roottree:e->roottree:e</title>
<path d="M504.5,-599.5C552,-608.5 648,-608.5 648,-576 648,-545.6582 564.3267,-543.6433 514.5622,-550.833" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="513.7934,-547.4125 504.5,-552.5 514.9376,-554.3184 513.7934,-547.4125" stroke="#000000"></polygon>
</g>
<!-- toplevel -->
<g class="node" id="node3">
<title>toplevel</title>
<polygon fill="none" points="576,-380 576,-633 948,-633 948,-380 576,-380" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-617.8">FS_TREE "toplevel"</text>
<polyline fill="none" points="576,-610 948,-610 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-594.8"> </text>
<polyline fill="none" points="576,-587 948,-587 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-571.8">256: inode_item DIR</text>
<polyline fill="none" points="576,-564 948,-564 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-548.8">256: dir_item: "root" -> ROOT_ITEM 256</text>
<polyline fill="none" points="576,-541 948,-541 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-525.8">256: dir_item: "home" -> ROOT_ITEM 257</text>
<polyline fill="none" points="576,-518 948,-518 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-502.8">256: dir_item: "var" -> INODE_ITEM 257</text>
<polyline fill="none" points="576,-495 948,-495 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-479.8">256: dir_item: "postgres" -> ROOT_ITEM 259</text>
<polyline fill="none" points="576,-472 948,-472 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-456.8">256: dir_item: "toplevel@s1" -> ROOT_ITEM 260</text>
<polyline fill="none" points="576,-449 948,-449 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-433.8"> </text>
<polyline fill="none" points="576,-426 948,-426 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-410.8">257: inode_item DIR</text>
<polyline fill="none" points="576,-403 948,-403 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-387.8">257: dir_item: "www" -> ROOT_ITEM 258</text>
</g>
<!-- roottree->toplevel -->
<g class="edge" id="edge8">
<title>roottree:root_fs->toplevel:label</title>
<path d="M504,-622.5C532.3777,-622.5 541.8777,-621.7137 565.9785,-621.5356" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="566.0125,-625.0356 576,-621.5 565.9876,-618.0356 566.0125,-625.0356" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- toplevels1 -->
<g class="node" id="node4">
<title>toplevels1</title>
<polygon fill="none" points="588.5,-.5 588.5,-230.5 935.5,-230.5 935.5,-.5 588.5,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-215.3">FS_TREE "toplevel@s1"</text>
<polyline fill="none" points="588.5,-207.5 935.5,-207.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-192.3"> </text>
<polyline fill="none" points="588.5,-184.5 935.5,-184.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-169.3">256: inode_item DIR</text>
<polyline fill="none" points="588.5,-161.5 935.5,-161.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-146.3">256: dir_item: "root" -> ROOT_ITEM 256</text>
<polyline fill="none" points="588.5,-138.5 935.5,-138.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-123.3">256: dir_item: "home" -> ROOT_ITEM 257</text>
<polyline fill="none" points="588.5,-115.5 935.5,-115.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-100.3">256: dir_item: "var" -> INODE_ITEM 257</text>
<polyline fill="none" points="588.5,-92.5 935.5,-92.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-77.3">256: dir_item: "postgres" -> ROOT_ITEM 259</text>
<polyline fill="none" points="588.5,-69.5 935.5,-69.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-54.3"> </text>
<polyline fill="none" points="588.5,-46.5 935.5,-46.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-31.3">257: inode_item DIR</text>
<polyline fill="none" points="588.5,-23.5 935.5,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-8.3">257: dir_item: "www" -> ROOT_ITEM 258</text>
</g>
<!-- roottree->toplevels1 -->
<g class="edge" id="edge9">
<title>roottree:root_sub_s1->toplevels1:label</title>
<path d="M504,-460.5C613.7754,-460.5 483.5108,-234.7829 577.8149,-220.2347" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="578.2778,-223.7105 588,-219.5 577.7741,-216.7287 578.2778,-223.7105" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- root -->
<g class="node" id="node5">
<title>root</title>
<polygon fill="none" points="680,-717.5 680,-763.5 844,-763.5 844,-717.5 680,-717.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-748.3">FS_TREE "root"</text>
<polyline fill="none" points="680,-740.5 844,-740.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-725.3">256: inode_item DIR</text>
</g>
<!-- roottree->root -->
<g class="edge" id="edge11">
<title>roottree:root_sub_root->root:label</title>
<path d="M504,-552.5C580.3617,-552.5 521.1427,-655.3795 576,-708.5 609.5199,-740.9586 627.2238,-751.0945 669.8767,-752.3594" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="669.9523,-755.8607 680,-752.5 670.0496,-748.8614 669.9523,-755.8607" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- home -->
<g class="node" id="node6">
<title>home</title>
<polygon fill="none" points="680,-652.5 680,-698.5 844,-698.5 844,-652.5 680,-652.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-683.3">FS_TREE "home"</text>
<polyline fill="none" points="680,-675.5 844,-675.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-660.3">256: inode_item DIR</text>
</g>
<!-- roottree->home -->
<g class="edge" id="edge12">
<title>roottree:root_sub_home->home:label</title>
<path d="M504,-529.5C563.5506,-529.5 530.6816,-603.867 576,-642.5 611.6322,-672.8757 627.3697,-685.6659 669.8761,-687.3144" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="669.9375,-690.816 680,-687.5 670.0659,-683.8172 669.9375,-690.816" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- www -->
<g class="node" id="node7">
<title>www</title>
<polygon fill="none" points="680,-314.5 680,-360.5 844,-360.5 844,-314.5 680,-314.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-345.3">FS_TREE "www"</text>
<polyline fill="none" points="680,-337.5 844,-337.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-322.3">256: inode_item DIR</text>
</g>
<!-- roottree->www -->
<g class="edge" id="edge13">
<title>roottree:root_sub_www->www:label</title>
<path d="M504,-506.5C572.3925,-506.5 521.2069,-411.4298 576,-370.5 610.9748,-344.3743 629.9117,-348.6235 669.9457,-349.3981" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="669.965,-352.8984 680,-349.5 670.036,-345.8988 669.965,-352.8984" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- postgres -->
<g class="node" id="node8">
<title>postgres</title>
<polygon fill="none" points="679.5,-249.5 679.5,-295.5 844.5,-295.5 844.5,-249.5 679.5,-249.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-280.3">FS_TREE "postgres"</text>
<polyline fill="none" points="679.5,-272.5 844.5,-272.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="762" y="-257.3">256: inode_item DIR</text>
</g>
<!-- roottree->postgres -->
<g class="edge" id="edge14">
<title>roottree:root_sub_postgres->postgres:label</title>
<path d="M504,-483.5C589.7501,-483.5 509.8697,-359.0882 576,-304.5 609.2237,-277.075 629.1685,-283.2528 668.7818,-284.3531" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="668.9507,-287.8558 679,-284.5 669.0514,-280.8565 668.9507,-287.8558" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge2">
<title>toplevel:toplevel_dir_root->roottree:root_sub_root</title>
<path d="M576,-552.5C547.625,-552.5 538.1205,-552.5 514.0208,-552.5" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514,-549.0001 504,-552.5 514,-556.0001 514,-549.0001" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge3">
<title>toplevel:toplevel_dir_home->roottree:root_sub_home</title>
<path d="M576,-529.5C547.625,-529.5 538.1205,-529.5 514.0208,-529.5" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514,-526.0001 504,-529.5 514,-533.0001 514,-526.0001" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge5">
<title>toplevel:toplevel_dir_postgres->roottree:root_sub_postgres</title>
<path d="M576,-483.5C547.625,-483.5 538.1205,-483.5 514.0208,-483.5" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514,-480.0001 504,-483.5 514,-487.0001 514,-480.0001" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge6">
<title>toplevel:toplevel_dir_toplevels1->roottree:root_sub_s1</title>
<path d="M576,-460.5C547.625,-460.5 538.1205,-460.5 514.0208,-460.5" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="514,-457.0001 504,-460.5 514,-464.0001 514,-457.0001" stroke="#000000"></polygon>
</g>
<!-- toplevel->roottree -->
<g class="edge" id="edge7">
<title>toplevel:toplevel_dir_www->roottree:root_sub_www</title>
<path d="M576,-391.5C519.4668,-391.5 558.652,-492.5742 514.0836,-505.2085" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="513.4743,-501.7579 504,-506.5 514.3637,-508.7011 513.4743,-501.7579" stroke="#000000"></polygon>
</g>
<!-- toplevel->toplevel -->
<g class="edge" id="edge4">
<title>toplevel:e->toplevel:e</title>
<path d="M948,-506.5C1016.6667,-515.5 1154,-515.5 1154,-460.5 1154,-408.0781 1029.2399,-405.6208 958.0877,-413.2984" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="none" points="957.5158,-409.8417 948,-414.5 958.3438,-416.7926 957.5158,-409.8417" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>在 ROOT_TREE 中增加了 260 号子卷,其内容复制自 toplevel 子卷,然后 FS_TREE toplevel
的 256 号 inode 也就是根目录中增加一个 dir_item 名叫 <cite>toplevel@s1</cite> 它指向 ROOT_ITEM
的 260 号子卷。这里看似是完整复制了整个 FS_TREE 的内容,这是因为 CoW b-tree
当只有一个叶子节点时就复制整个叶子节点。如果子卷内容再多一些,除了叶子之外还有中间节点,
那么只有被修改的叶子和其上的中间节点需要复制。从而创建快照的开销基本上是
O( level of FS_TREE ),而B树的高度一般都能维持在很低的程度,所以快照创建速度近乎是常数开销。</p>
<p>从子卷和快照的这种实现方式,可以看出: <strong>虽然子卷可以嵌套子卷,但是对含有嵌套子卷的子卷做快照的语义有些特别</strong>
。上图中我没有画 <cite>toplevel@s1</cite> 下的各个子卷到对应 ROOT_ITEM 之间的虚线箭头,
是因为这时候如果你尝试直接跳过 <cite>toplevel</cite> 挂载 <cite>toplevel@s1</cite> 到挂载点,
会发现那些子卷没有被自动挂载,更奇怪的是那些子卷的目录项也不是个普通目录,
尝试往它们中放东西会得到无权访问的错误,对它们能做的唯一事情是手动将别的子卷挂载在上面。
推测原因在于这些子目录并不是真的目录,没有对应的目录的 inode ,试图查看它们的 inode
号会得到 2 号,而这是个保留号不应该出现在 btrfs 的 inode 号中。
每个子卷创建时会记录包含它的上级子卷,用 <code class="code">
btrfs subvolume list</code>
可以看到每个子卷的
top level subvolid ,猜测当挂载 A 而 A 中嵌套的 B 子卷记录的上级子卷不是 A 的时候,
会出现上述奇怪行为。嵌套子卷的快照还有一些别的奇怪行为,大家可以自己探索探索。</p>
<div class="panel panel-default">
<div class="panel-heading">
建议用平坦的子卷布局</div>
<div class="panel-body">
<p>因为上述嵌套子卷在做快照时的特殊行为,
我个人建议是 <strong>保持平坦的子卷布局</strong> ,也就是说:</p>
<ol class="arabic simple">
<li>只让顶层子卷包含其它子卷,除了顶层子卷之外的子卷只做手工挂载,不放嵌套子卷</li>
<li>只在顶层子卷对其它子卷做快照,不快照顶层子卷</li>
<li>虽然可以在顶层子卷放子卷之外的东西(文件或目录),不过因为想避免对顶层子卷做快照,
所以避免在顶层子卷放普通文件。</li>
</ol>
</div>
</div>
<p>btrfs 的子卷可以设置「可写」或者「只读」,在创建一个快照的时候也可以通过 <code class="code">
-r</code>
参数创建出一个只读快照。通常只读快照可能比可写的快照更有用,因为 <code class="code">
btrfs send</code>
命令只接受只读快照作为参考点。子卷可以有两种方式切换它是否只读的属性,可以通过
<code class="code">
btrfs property set <subvol> ro</code>
直接修改是否只读,也可以对只读子卷用
<code class="code">
btrfs subvolume snapshot</code>
创建出可写子卷,或者反过来对可写子卷创建出只读子卷。</p>
<p>只读快照也有些特殊的限制,在 <a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/SysadminGuide#Special_Cases">SysadminGuide#Special_Cases</a>
就提到一例,你不能把只读快照用 mv 移出包含它的目录,虽然你能用 mv 给它改名或者移动包含它的目录
到别的地方。 btrfs wiki 上给出这个限制的原因是子卷中记录了它的上级,
所以要移动它到别的上级需要修改这个子卷,从而只读子卷没法移动到别的上级(
不过我还没搞清楚子卷在哪儿记录了它的上级,记录的是上级目录还是上级子卷)。不过这个限制可以通过
对只读快照在目标位置创建一个新的只读快照,然后删掉原位置的只读快照来解决。</p>
</div>
</div>
<div class="section" id="zfs">
<h2><a class="toc-backref" href="#id38">2 ZFS 的文件系统、快照、克隆及其它</a></h2>
<p>Btrfs 给传统文件系统只增加了子卷的概念,相比之下 ZFS 中类似子卷的概念有好几个,据我所知有这些:</p>
<ul class="simple">
<li>数据集(dataset)</li>
<li>文件系统(filesystem)</li>
<li>快照(snapshot)</li>
<li>克隆(clone)</li>
<li>书签(bookmark):从 ZFS on Linux v0.6.4 开始</li>
<li>检查点(checkpoint):从 ZFS on Linux v0.8.0 开始</li>
</ul>
<p>梳理一下这些概念之间的关系也是最初想写下这篇笔记的初衷。先画个简图,随后逐一讲讲这些概念:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/f0cd8a5f.png"/>
<p>上图中,假设我们有一个 pool ,其中有 3 个文件系统叫 fs1~fs3 和一个 zvol 叫 zv1
,然后文件系统 fs1 有两个快照 s1 和 s2 ,和两个书签 b1 和 b2。pool 整体有两个检查点 cp1 和
cp2 。这个简图将作为例子在后面介绍这些概念。</p>
<div class="section" id="id8">
<h3><a class="toc-backref" href="#id39">2.1 ZFS 设计中和快照相关的一些术语和概念</a></h3>
<div class="section" id="id9">
<h4><a class="toc-backref" href="#id40">数据集</a></h4>
<p>ZFS 中把文件系统、快照、克隆、zvol 等概念统称为数据集(dataset)。
一些文档和介绍中把文件系统叫做数据集,大概因为在 ZFS 中,文件系统是最先创建并且最有用的数据集。</p>
<p>在 ZFS 的术语中,把底层管理和释放存储设备空间的叫做 ZFS 存储池(pool),
简称 zpool ,其上可以容纳多个数据集,这些数据集用类似文件夹路径的语法
<code class="code">
pool_name/dataset_path@snapshot_name</code>
这样来称呼。
存储池中的数据集一同共享可用的存储空间,每个数据集单独跟踪自己所消耗掉的存储空间。</p>
<p>数据集之间有类似文件夹的层级父子关系,这一点有用的地方在于可以在父级数据集上设定一些 ZFS 参数,
这些参数可以被子级数据集继承,从而通过层级关系可以方便地微调 ZFS 参数。在 btrfs
中目前还没有类似的属性继承的功能。</p>
<p>zvol 的概念和本文关系不大,可以参考我上一篇 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#ZVOL">ZFS 子系统笔记中 ZVOL 的说明</a>
。用 zvol 能把 ZFS 当作一个传统的卷管理器,绕开 ZFS
的 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#ZPL">ZPL(ZFS Posix filesystem Layer)</a>
层。在 Btrfs 中可以用 loopback 块设备某种程度上模拟 zvol 的功能。</p>
</div>
<div class="section" id="id10">
<h4><a class="toc-backref" href="#id41">文件系统</a></h4>
<p>创建了 ZFS 存储池后,首先要在其中创建文件系统(filesystem),才能在文件系统中存储文件。
容易看出 ZFS 文件系统的概念直接对应 btrfs 中的子卷。文件系统(filesystem)这个术语,
从命名方式来看或许是想要和(像 Solaris 的 SVM 或者 Linux 的 LVM 这样的)传统的卷管理器
与其上创建的多个文件系统(Solaris UFS 或者 Linux ext)这样的上下层级做类比。
从 btrfs 的子卷在内部结构中叫作 FS_TREE 这一点可以看出,至少在 btrfs
早期设计中大概也是把子卷称为 filesystem 做过类似的类比的。
和传统的卷管理器与传统文件系统的上下层级不同的是, ZFS 和 btrfs 中由存储池跟踪和管理可用空间,
做统一的数据块分配和释放,没有分配的数据块算作整个存储池中所有 ZFS 文件系统或者 btrfs
子卷的可用空间。</p>
<p>与 btrfs 的子卷不同的是, ZFS 的文件系统之间是完全隔离的,(除了后文会讲的 dedup
方式之外)不可以共享任何数据或者元数据。一个文件系统还包含了隶属于其中的快照(snapshot)、
克隆(clone)和书签(bookmark)。在 btrfs 中一个子卷和对其创建的快照之间虽然有父子关系,
但是在 ROOT_TREE 的记录中属于平级的关系。</p>
<p>上面简图中 pool 里面包含 3 个文件系统,分别是 fs1~3 。</p>
</div>
<div class="section" id="id11">
<h4><a class="toc-backref" href="#id42">快照</a></h4>
<p>ZFS 的快照对应 btrfs 的只读快照,是标记数据集在某一历史时刻上的只读状态。
和 btrfs 的只读快照一样, ZFS 的快照也兼作 send/receive 时的参考点。
快照隶属于一个数据集,这说明 ZFS 的文件系统或者 zvol 都可以创建快照。</p>
<p>ZFS 中快照是排列在一个时间线上的,因为都是只读快照,它们是数据集在历史上的不同时间点。
这里说的时间不是系统时钟的时间,而是 ZFS 中事务组(TXG, transaction group)的一个序号。
整个 ZFS pool 的每次写入会被合并到一个事务组,对事务组分配一个严格递增的序列号,
提交一个事务组具有类似数据库中事务的语义:要么整个事务组都被完整提交,要么整个 pool
处于上一个事务组的状态,即使中间发生突然断电之类的意外也不会破坏事务语义。
因此 ZFS 快照就是数据集处于某一个事务组时的状态。</p>
<p>如果不满于对数据集进行的修改,想把整个数据集恢复到之前的状态,那么可以回滚(rollback
)数据集到一个快照。回滚操作会撤销掉对数据集的所有更改,并且默认参数下只能回滚到最近的一个快照。
如果想回滚到更早的快照,可以先删掉最近的几个,或者可以使用 <code class="code">
zfs rollback -r</code>
参数删除中间的快照并回滚。</p>
<p>除了回滚操作,还可以直接只读访问到快照中的文件。 ZFS 的文件系统中有个隐藏文件夹叫 ".zfs"
,所以如果只想回滚一部分文件,可以从 ".zfs/snapshots/SNAPSHOT-NAME" 中把需要的文件复制出来。</p>
<p>比如上面简图中 fs1 就有 <code class="code">
pool/fs1@s1</code>
和 <code class="code">
pool/fs1@s2</code>
这两个快照,
那么可以在 fs1 挂载点下 <code class="code">
.zfs/snapshots/s1</code>
的路径直接访问到 s1 中的内容。</p>
</div>
<div class="section" id="id12">
<h4><a class="toc-backref" href="#id43">克隆</a></h4>
<p>ZFS 的克隆(clone)有点像 btrfs 的可写快照。因为 ZFS 的快照是只读的,如果想对快照做写入,那需要先用
<code class="code">
zfs clone</code>
从快照中建出一个克隆,创建出的克隆和快照共享元数据和数据,
然后对克隆的写入不影响数据集原本的写入点。
创建了克隆之后,作为克隆参考点的快照会成为克隆的依赖,克隆存在期间无法删除掉作为其依赖的快照。</p>
<p>一个数据集可以有多个克隆,这些克隆都独立于数据集当前的写入点。使用 <code class="code">
zfs promote</code>
命令可以把一个克隆「升级」成为数据集的当前写入点,从而数据集原本的写入点会调转依赖关系,
成为这个新写入点的一个克隆,被升级的克隆原本依赖的快照和之前的快照会成为新数据集写入点的快照。</p>
<p>比如上面简图中 fs1 有 c1 的克隆,它依赖于 s2 这个快照,从而 c1 存在的时候就不能删除掉 s2 。</p>
</div>
<div class="section" id="id13">
<h4><a class="toc-backref" href="#id44">书签</a></h4>
<p>这是 ZFS 一个比较新的特性,ZFS on Linux 分支从 v0.6.4 开始支持创建书签的功能。</p>
<p>书签(bookmark)特性存在的理由是基于这样的事实:原本 ZFS 在 send 两个快照间的差异的时候,比如 send S1 和
S2 之间的差异,在发送端实际上只需要 S1 中记录的时间戳(TXG id),而不需要 S1 快照的数据,
就可以计算出 S1 到 S2 的差异。在接收端则需要 S1 的完整数据,在其上根据接收到的数据流创建 S2 。
因此在发送端,可以把快照 S1 转变成书签,只留下时间戳元数据而不保留任何目录结构或者文件内容。
书签只能作为增量 send 时的参考点,并且在接收端需要有对应的快照,这种方式可以在发送端节省很多存储。</p>
<p>通常的使用场景是,比如你有一个笔记本电脑,上面有 ZFS 存储的数据,然后使用一个服务器上 ZFS
作为接收端,定期对笔记本上的 ZFS 做快照然后 send 给服务器。在没有书签功能的时候,
笔记本上至少得保留一个和服务器上相同的快照,作为 send 的增量参考点,
而这个快照的内容已经在服务器上,所以笔记本中存有相同的快照只是在浪费存储空间。
有了书签功能之后,每次将定期的新快照发送到服务器之后,就可以把这个快照转化成书签,节省存储开销。</p>
</div>
<div class="section" id="id14">
<h4><a class="toc-backref" href="#id45">检查点</a></h4>
<p>这也是 ZFS 的新特性, ZFS on Linux 分支从 v0.8.0 开始支持创建检查点。</p>
<p>简而言之,检查点(checkpoint)可以看作是整个存储池级别的快照,使用检查点能快速将整个存储池都恢复到上一个状态。
这边有篇文章介绍 <a class="reference external" href="https://sdimitro.github.io/post/zpool-checkpoint/">ZFS checkpoint 功能的背景、用法和限制</a>
,可以看出当存储池中有检查点的时候很多存储池的功能会受影响(比如不能删除 vdev 、不能处于
degraded 状态、不能 scrub 到当前存储池中已经释放而在检查点还在引用的数据块),
于是检查点功能设计上更多是给系统管理员准备的用于调整整个 ZFS pool 时的后悔药,
调整结束后日用状态下应该删除掉所有检查点。</p>
</div>
</div>
<div class="section" id="zfs-btrfs">
<h3><a class="toc-backref" href="#id46">2.2 ZFS 的概念与 btrfs 概念的对比</a></h3>
<p>先说书签和检查点,因为这是两个 btrfs 目前完全没有的功能。</p>
<p>书签功能完全围绕 ZFS send 的工作原理,而 ZFS send 位于
<a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#DSL">ZFS 设计中的 DSL</a>
层面,甚至不关心它 send 的快照的数据是来自文件系统还是 zvol
。在发送端它只是从目标快照递归取数据块,判断 TXG
是否老于参照点的快照,然后把新的数据块全部发往 send stream ;在接收端也只是完整地接收数据块,
不加以处理,。与之不同的是 btrfs 的 send 的工作原理是工作在文件系统的只读子卷层面,
发送端在内核代码中根据目标快照的 b 树和参照点快照的 generation 生成一个 diff
(可以通过 <code class="code">
btrfs subvolume find-new</code>
直接拿到这个 diff ),然后在用户态代码中根据
diff 和参照点、目标快照的两个只读子卷的数据产生一连串修改文件系统的指令,
指令包括创建文件、删除文件、让文件引用数据块(保持 reflink )等操作;在接收端则完全工作在用户态下,
根据接收到的指令重建目标快照。可见 btrfs send 需要在发送端读取参照点快照的数据(比如找到
reflink 引用),从而 btrfs 没法(或者很难)实现书签功能。</p>
<p>检查点也是 btrfs 目前没有的功能。 btrfs 目前不能对顶层子卷做递归的 snapshot ,btrfs
的子卷也没有类似 ZFS 数据集的层级关系和可继承属性,从而没法实现类似检查点的功能。</p>
<p>除了书签和检查点之外,剩下的概念可以在 ZFS 和 btrfs 之间有如下映射关系:</p>
<table border="0" class="docutils table field-list" frame="void" rules="none">
<col class="field-name"/>
<col class="field-body"/>
<tbody valign="top">
<tr class="field"><th class="field-name">ZFS 文件系统:</th><td class="field-body">btrfs 子卷</td>
</tr>
<tr class="field"><th class="field-name">ZFS 快照:</th><td class="field-body">btrfs 只读快照</td>
</tr>
<tr class="field"><th class="field-name">ZFS 克隆:</th><td class="field-body">btrfs 可写快照</td>
</tr>
</tbody>
</table>
<p>对 ZFS 数据集的操作,大部分也可以找到对应的对 btrfs 子卷的操作。</p>
<table border="0" class="docutils table field-list" frame="void" rules="none">
<col class="field-name"/>
<col class="field-body"/>
<tbody valign="top">
<tr class="field"><th class="field-name">zfs list:</th><td class="field-body"><code class="code">
btrfs subvolume list</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs create:</th><td class="field-body"><code class="code">
btrfs subvolume create</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs destroy:</th><td class="field-body"><code class="code">
btrfs subvolume delete</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs rename:</th><td class="field-body"><code class="code">
mv</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs snapshot:</th><td class="field-body"><code class="code">
btrfs subvolume snapshot -r</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs rollback:</th><td class="field-body">这个在 btrfs 需要对只读快照创建出可写的快照(用 snapshot 命令,或者直接修改读写属性),然后改名或者调整挂载点</td>
</tr>
<tr class="field"><th class="field-name">zfs diff:</th><td class="field-body"><code class="code">
btrfs subvolume find-new</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs clone:</th><td class="field-body"><code class="code">
btrfs subvolume snapshot</code>
</td>
</tr>
<tr class="field"><th class="field-name">zfs promote:</th><td class="field-body">和 rollback 类似,可以直接调整 btrfs 子卷的挂载点</td>
</tr>
</tbody>
</table>
<p>可见虽然功能上类似,但是至少从管理员管理的角度而言, zfs 对文件系统、快照、克隆的划分更为清晰,
对他们能做的操作也更为明确。这也是很多从 ZFS 迁移到 btrfs ,或者反过来从 btrfs 换用 zfs
时,一些人困惑的起源(甚至有人据此说 ZFS 比 btrfs 好在 cli 设计上)。</p>
<p>不过 btrfs 子卷的设计也使它在系统管理上有了更大的灵活性。比如在 btrfs
中删除一个子卷不会受制于别的子卷是否存在,而在 zfs 中要删除一个快照必须先保证先摧毁掉依赖它的克隆。
再比如 btrfs 的可写子卷没有主次之分,而 zfs 中一个文件系统和其克隆之间有明显的区别,所以需要
promote 命令调整差异。还有比如 ZFS 的文件系统只能回滚到最近一次的快照,
要回滚到更久之前的快照需要删掉中间的快照,并且回滚之后原本的文件系统数据和快照数据就被丢弃了;
而 btrfs 中因为回滚操作相当于调整子卷的挂载,所以不需要删掉快照,
并且回滚之后原本的子卷和快照还可以继续保留。</p>
<p>加上 btrfs 有 reflink ,这给了 btrfs 在使用中更大的灵活性,可以有一些 zfs 很难做到的用法。
比如想从快照中打捞出一些虚拟机镜像的历史副本,而不想回滚整个快照的时候,在
btrfs 中可以直接 <code class="code">
cp --reflink=always</code>
将镜像从快照中复制出来,此时的复制将和快照共享数据块;
而在 zfs 中只能用普通 cp 复制,会浪费很多存储空间。</p>
</div>
<div class="section" id="id15">
<h3><a class="toc-backref" href="#id47">2.3 ZFS 中是如何存储这些数据集的呢</a></h3>
<p>要讲到存储细节,首先需要 了解一下 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html">ZFS 的分层设计</a>
。不像 btrfs 基于现代 Linux 内核,有许多现有文件系统已经实现好的基础设施可以利用,
并且大体上只用到一种核心数据结构(CoW的B树); ZFS 则脱胎于 Solaris 的野心勃勃,
设计时就分成很多不同的子系统,逐步提升抽象层次,
并且每个子系统都发明了许多特定需求下的数据结构来描述存储的信息。 在这里和本文内容密切相关的是
<a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#zpl">ZPL</a> 、 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#DSL">DSL</a> 、 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#dmu">DMU</a> 这些 ZFS 子系统。</p>
<p>Sun 曾经写过一篇 ZFS 的 <a class="reference external" href="http://www.giis.co.in/Zfs_ondiskformat.pdf">On disk format</a>
对理解 ZFS 如何存储在磁盘上很有帮助,虽然这篇文档是针对 Sun 还在的时候 Solaris 的 ZFS
,现在 ZFS 的内部已经变化挺大,不过对于理解本文想讲的快照的实现方式还具有参考意义。这里借助这篇
ZFS On Disk Format 中的一些图示来解释 ZFS 在磁盘上的存储方式。</p>
<div class="section" id="id18">
<h4><a class="toc-backref" href="#id48">ZFS 的块指针</a></h4>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="//farseerfc.me/zhs/images/zfs-block-pointer.svg">ZFS 中用的 128 字节块指针</a></div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/zfs-block-pointer.svg" type="image/svg+xml">
zfs-block-pointer.svg</object>
</div>
</div>
<p>要理解 ZFS 的磁盘结构首先想介绍一下 ZFS 中的块指针(block pointer, <code class="code">
blkptr_t</code>
),结构如右图所示。 ZFS 的块指针用在 ZFS 的许多数据结构之中,当需要从一个地方指向任意另一个地址的时候都会
插入这样的一个块指针结构。大多数文件系统中也有类似的指针结构,比如 btrfs
中有个8字节大小的逻辑地址(logical address),一般也就是个 4字节 到 16字节
大小的整数写着扇区号、块号或者字节偏移,在 ZFS 中的块指针则是一个巨大的128字节(不是
128bit !)的结构体。</p>
<p>128字节块指针的开头是3个数据虚拟地址(DVA, Data Virtual Address),每个 DVA 是 128bit
,其中记录这块数据在什么设备(vdev)的什么偏移(offset)上占用多大(asize),有 3个
DVA 槽是用来存储最多3个不同位置的副本。然后块指针还记录了这个块用什么校验算法( cksum
)和什么压缩算法(comp),压缩前后的大小(PSIZE/LSIZE),以及256bit的校验和(checksum)。</p>
<p>当需要间接块(indirect block)时,块指针中记录了间接块的层数(lvl),和下层块指针的数量(fill)。
一个间接块就是一个数据块中包含一个块指针的数组,当引用的对象很大需要很多块时,间接块构成一棵树状结构。</p>
<p>块指针中还有和本文关系很大的一个值 birth txg ,记录这个块指针诞生时的整个 pool 的 TXG id
。一次 TXG 提交中写入的数据块都会有相同的 birth txg ,这个相当于 btrfs 中 generation 的概念。
实际上现在的 ZFS 块指针似乎记录了两个 birth txg ,分别在图中的9行和a行的位置,
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSBlockPointers">一个 physical 一个 logical ,用于 dedup 和 device removal</a>
。值得注意的是块指针里只有 birth txg ,没有引用计数或者别的机制做引用,这对后面要讲的东西很关键。</p>
</div>
<div class="section" id="id19">
<h4><a class="toc-backref" href="#id49">DSL 的元对象集</a></h4>
<p>理解块指针和 ZFS 的子系统层级之后,就可以来看看 ZFS 存储在磁盘上的具体结构了。
因为涉及的数据结构种类比较多,所以先来画一张逻辑上的简图,其中箭头只是某种引用关系不代表块指针,
方框也不是结构体细节:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: zfs_layout_simple Pages: 1 -->
<svg class="svg-responsive" height="262pt" viewbox="0.00 0.00 1128.00 262.00" width="1128pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 258)">
<title>zfs_layout_simple</title>
<polygon fill="#ffffff" points="-4,4 -4,-258 1124,-258 1124,4 -4,4" stroke="transparent"></polygon>
<!-- uberblock -->
<g class="node" id="node1">
<title>uberblock</title>
<polygon fill="none" points="0,-184.5 0,-253.5 114,-253.5 114,-184.5 0,-184.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="57" y="-238.3">UBERBLOCK</text>
<polyline fill="none" points="0,-230.5 114,-230.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="57" y="-215.3">...</text>
<polyline fill="none" points="0,-207.5 114,-207.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="57" y="-192.3">mos_blkptr</text>
</g>
<!-- mos -->
<g class="node" id="node2">
<title>mos</title>
<polygon fill="none" points="186,-115 186,-207 320,-207 320,-115 186,-115" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="253" y="-191.8">Meta Object Set</text>
<polyline fill="none" points="186,-184 320,-184 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="253" y="-168.8">root dataset</text>
<polyline fill="none" points="186,-161 320,-161 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="253" y="-145.8">config</text>
<polyline fill="none" points="186,-138 320,-138 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="253" y="-122.8">...</text>
</g>
<!-- uberblock->mos -->
<g class="edge" id="edge1">
<title>uberblock:ub_rootbp->mos:mos_label</title>
<path d="M114,-196C142.375,-196 151.8795,-196 175.9792,-196" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="176,-199.5001 186,-196 176,-192.5001 176,-199.5001" stroke="#000000"></polygon>
</g>
<!-- root_dataset -->
<g class="node" id="node3">
<title>root_dataset</title>
<polygon fill="none" points="392,-92 392,-184 541,-184 541,-92 392,-92" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="466.5" y="-168.8">ROOT dataset</text>
<polyline fill="none" points="392,-161 541,-161 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="466.5" y="-145.8">dataset1 directory</text>
<polyline fill="none" points="392,-138 541,-138 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="466.5" y="-122.8">dataset2 directory</text>
<polyline fill="none" points="392,-115 541,-115 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="466.5" y="-99.8">...</text>
</g>
<!-- mos->root_dataset -->
<g class="edge" id="edge2">
<title>mos:mos_root_dataset->root_dataset:rd_label</title>
<path d="M320,-173C348.375,-173 357.8795,-173 381.9792,-173" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="382,-176.5001 392,-173 382,-169.5001 382,-176.5001" stroke="#000000"></polygon>
</g>
<!-- ds1_directory -->
<g class="node" id="node4">
<title>ds1_directory</title>
<polygon fill="none" points="613,-.5 613,-161.5 805,-161.5 805,-.5 613,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-146.3">DSL Directory</text>
<polyline fill="none" points="613,-138.5 805,-138.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-123.3">ds1 property ZAP object</text>
<polyline fill="none" points="613,-115.5 805,-115.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-100.3">ds1 child ZAP object</text>
<polyline fill="none" points="613,-92.5 805,-92.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-77.3">ds1 dataset (active)</text>
<polyline fill="none" points="613,-69.5 805,-69.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-54.3">ds1 snapshot1</text>
<polyline fill="none" points="613,-46.5 805,-46.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-31.3">ds1 snapshot2</text>
<polyline fill="none" points="613,-23.5 805,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="709" y="-8.3">...</text>
</g>
<!-- root_dataset->ds1_directory -->
<g class="edge" id="edge3">
<title>root_dataset:rd_ds1->ds1_directory:ds1_label</title>
<path d="M541,-150C569.375,-150 578.8795,-150 602.9792,-150" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="603,-153.5001 613,-150 603,-146.5001 603,-153.5001" stroke="#000000"></polygon>
</g>
<!-- ds1_dataset -->
<g class="node" id="node5">
<title>ds1_dataset</title>
<polygon fill="none" points="916,-67 916,-113 1081,-113 1081,-67 916,-67" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="998.5" y="-97.8">ds1 DMU Object Set</text>
<polyline fill="none" points="916,-90 1081,-90 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="998.5" y="-74.8">...</text>
</g>
<!-- ds1_directory->ds1_dataset -->
<g class="edge" id="edge4">
<title>ds1_directory:ds1_dataset->ds1_dataset:ds1_ds_label</title>
<path d="M805,-81C851.3776,-81 863.7833,-99.0745 905.2509,-101.6868" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="905.3977,-105.1929 915.5,-102 905.6116,-98.1961 905.3977,-105.1929" stroke="#000000"></polygon>
</g>
<!-- ds1_snapshot1 -->
<g class="node" id="node6">
<title>ds1_snapshot1</title>
<polygon fill="none" points="877,-2 877,-48 1120,-48 1120,-2 877,-2" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="998.5" y="-32.8">ds1 snapshot1 DMU Object Set</text>
<polyline fill="none" points="877,-25 1120,-25 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="998.5" y="-9.8">...</text>
</g>
<!-- ds1_directory->ds1_snapshot1 -->
<g class="edge" id="edge5">
<title>ds1_directory:ds1_s1->ds1_snapshot1:ds1_s1_label</title>
<path d="M805,-58C834.6875,-58 842.165,-41.3425 866.9941,-37.6987" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="867.2681,-41.1882 877,-37 866.7804,-34.2052 867.2681,-41.1882" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>如上简图所示,首先 ZFS pool 级别有个 uberblock ,具体每个 vdev 如何存储和找到这个 uberblock
今后有空再聊,这里认为整个 zpool 有唯一的一个 uberblock 。从 uberblock
有个指针指向元对象集(MOS, Meta Object Set),它是个 DMU 的对象集,它包含整个 pool
的一些配置信息,和根数据集(root dataset)。根数据集再包含整个 pool
中保存的所有顶层数据集,每个数据集有一个 DSL Directory 结构。然后从每个数据集的
DSL Directory 可以找到一系列子数据集和一系列快照等结构。最后每个数据集有个 active
的 DMU 对象集,这是整个文件系统的当前写入点,每个快照也指向一个各自的 DMU 对象集。</p>
<p>DSL 层的每个数据集的逻辑结构也可以用下面的图表达(来自 ZFS On Disk Format ):</p>
<div class="figure">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/zfs-dsl-infrastructure.svg" type="image/svg+xml">
zfs-dsl-infrastructure.svg</object>
<p class="caption">ZFS On Disk Format 中 4.1 节的 DSL infrastructure</p>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<a class="reference external" href="//farseerfc.me/zhs/images/zfs-metaobjectset.svg">ZFS On Disk Format 中 4.2 节的 Meta Object Set</a></div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/zhs/images/zfs-metaobjectset.svg" type="image/svg+xml">
zfs-metaobjectset.svg</object>
</div>
</div>
<p>需要记得 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 为止。</p>
<p>看到这里,大概可以理解在 ZFS 中创建一个 ZFS 快照的操作其实很简单:找到数据集的 DSL Directory
中当前 active 的 DMU 对象集指针,创建一个表示 snapshot 的 DSL dataset 结构,指向那个
DMU 对象集,然后快照就建好了。因为今后对 active 的写入会写时复制对应的 DMU 对象集,所以
snapshot 指向的 DMU 对象集不会变化。</p>
</div>
</div>
</div>
<div class="section" id="id20">
<h2><a class="toc-backref" href="#id50">3 创建快照这么简单么?那么删除快照呢?</a></h2>
<p>按上面的存储格式细节来看, btrfs 和 zfs 中创建快照似乎都挺简单的,利用写时拷贝,创建快照本身没什么复杂操作。</p>
<p>如果你也听到过别人介绍 CoW 文件系统时这么讲,是不是会觉得似乎哪儿少了点什么。创建快照是挺简单的,
<strong>直到你开始考虑如何删除快照</strong> ……</p>
<p>或者不局限在删除单个快照上, CoW 文件系统因为写时拷贝,每修改一个文件内容或者修改一个文件系统结构,
都是分配新数据块,然后考虑是否要删除这个数据替换的老数据块,此时如何决定老数据块能不能删呢?
删除快照的时候也是同样,快照是和别的文件系统有共享一部分数据和元数据的,
所以显然不能把快照引用到的数据块都直接删掉,要考察快照引用的数据块是否还在别的地方被引用着,
只能删除那些没有被引用的数据。</p>
<p>深究「如何删快照」这个问题,就能看出 WAFL 、 btrfs 、 ZFS 甚至别的 log-structured
文件系统间的关键区别,从而也能看到另一个问题的答案:
<strong>为什么 btrfs 只需要子卷的抽象,而 zfs 搞出了这么多抽象概念?</strong>
带着这两个疑问,我们来研究一下这些文件系统的块删除算法。</p>
<div class="section" id="id21">
<h3><a class="toc-backref" href="#id51">3.1 日志结构文件系统中用的垃圾回收算法</a></h3>
<p>讲 btrfs 和 zfs 用到的删除算法之前,先讲一下日志结构(log-structured)文件系统中的垃圾回收(
GC, Garbage Collection)算法。对熟悉编程的人来说,讲到空间释放算法,大概首先会想到 GC
,因为这里要解决的问题乍看起来很像编程语言的内存管理中 GC
想要解决的问题:有很多指针相互指向很多数据结构,找其中没有被引用的垃圾然后释放掉。</p>
<p>首先要澄清一下 <a class="reference external" href="https://en.wikipedia.org/wiki/Log-structured_file_system">日志结构文件系统(log-structured file system)</a>
的定义,因为有很多文件系统用日志,而用了日志的不一定是日志结构文件系统。
在维基百科上有个页面介绍 <a class="reference external" href="https://en.wikipedia.org/wiki/Log-structured_file_system">日志结构文件系统</a>
,还有个 <a class="reference external" href="https://en.wikipedia.org/wiki/List_of_log-structured_file_systems">列表列出了一些日志结构文件系统</a>
。通常说,整个文件系统的存储结构都组织成一个大日志的样子,就说这个文件系统是日志结构的,
这包括很多早期学术研究的文件系统,以及目前 <a class="reference external" href="https://en.wikipedia.org/wiki/Log-structured_File_System_(BSD)">NetBSD 的 LFS</a>
、Linux 的 <a class="reference external" href="https://en.wikipedia.org/wiki/NILFS">NILFS</a>
,用在光盘介质上的 <a class="reference external" href="https://en.wikipedia.org/wiki/Universal_Disk_Format">UDF</a>
,还有一些专门为闪存优化的 <a class="reference external" href="https://en.wikipedia.org/wiki/JFFS">JFFS</a> 、
<a class="reference external" href="https://en.wikipedia.org/wiki/YAFFS">YAFFS</a> 以及
<a class="reference external" href="https://en.wikipedia.org/wiki/F2FS">F2FS</a>
。日志结构文件系统不包括那些用额外日志保证文件系统一致性,但文件系统结构不在日志中的 ext4 、 xfs
、 ntfs 、 hfs+ 。</p>
<p>简单来说,日志结构文件系统就是把存储设备当作一个大日志,每次写入数据时都添加在日志末尾,
然后用写时复制重新写入元数据,最后提交整个文件系统结构。因为这里用了写时复制,原本的数据块都还留着,
所以可以很容易实现快照之类的功能。从这个特征上来说,写时拷贝文件系统(CoW
FS)像 btrfs/zfs 这些在一些人眼中也符合日志结构文件系统的特征,
所以也有人说写时拷贝文件系统算是日志结构文件系统的一个子类。不过日志结构文件系统的另一大特征是利用
GC 回收空间,这里是本文要讲的区别,所以在我看来不用 GC 的 btrfs 和 zfs 不算是日志结构文件系统。</p>
<p>举个例子,比如下图是一个日志结构文件系统的磁盘占用,其中绿色是数据,蓝色是元数据(比如目录结构和
inode),红色是文件系统级关键数据(比如最后的日志提交点),一开始可能是这样,有9个数据块,
2个元数据块,1个系统块:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/c8f452c3.png"/>
<p>现在要覆盖 2 和 3 的内容,新写入 n2 和 n3 ,再删除 4 号的内容 ,然后修改 10 里面的 inode 变成 n10
引用这些新数据,然后写入一个新提交 n12 ,用黄色表示不再被引用的垃圾,提交完大概是这样:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/a0eb592f.png"/>
<p>日志结构文件系统需要 GC 比较容易理解,写日志嘛,总得有一个「添加到末尾」的写入点,比如上面图中的
n12 就是当前的写入点。空盘上连续往后写而不 GC 总会遇到空间末尾,这时候就要覆盖写空间开头,
就很难判断「末尾」在什么地方,而下一次写入需要在哪里了。
这时文件系统也不知道需要回收哪些块(图中的 o2 o3 o4 o10 和 o12),因为这些块可能被别的地方还继续
引用着,需要等到 GC 时扫描元数据来判断。</p>
<p>和内存管理时的 GC 不同的一点在于,文件系统的 GC 肯定不能停下整个世界跑 GC
,也不能把整个地址空间对半分然后 Mark-and-Sweep
,这些在内存中还尚可的简单策略直接放到文件系统中绝对是性能灾难。所以文件系统的 GC
需要并行的后台 GC ,并且需要更细粒度的分块机制能在 Mark-and-Sweep
的时候保持别的地方可以继续写入数据而维持文件系统的正常职能。</p>
<p>通常文件系统的 GC 是这样,先把整个盘分成几个段(segment)或者区域(zone),术语不同不过表达的概念类似,
然后 GC 时挑一个老段,扫描文件系统元数据找出要释放的段中还被引用的数据块,搬运到日志末尾,最后整个释放一段。
搬运数据块时,也要调整文件系统别的地方对被搬运的数据块的引用。</p>
<p>物理磁盘上一般有扇区的概念,通常是 512B 或者 4KiB 的大小,在文件系统中一般把连续几个物理块作为一个数据块,
大概是 4KiB 到 1MiB 的数量级,然后日志结构文件系统中一个段(segment)通常是连续的很多块,数量级来看大约是
4MiB 到 64MiB 这样的数量级。相比之下 ufs/ext4/btrfs/zfs 的分配器通常还有 block group 的概念,
大概是 128MiB 到 1GiB 的大小。可见日志结构文件系统的段,是位于数据块和其它文件系统 block group
中间的一个单位。段大小太小的话,会显著增加空间管理需要的额外时间空间开销,而段大小太大的话,
又不利于利用整个可用空间,这里的抉择有个平衡点。</p>
<p>继续上面的例子,假设上面文件系统的图示中每一列的4块是一个段,想要回收最开头那个段,
那么需要搬运还在用的 1 到空闲空间,顺带修改引用它的 n10 ,最后提交 n12 :</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/ba5b5955.png"/>
<p>要扫描并释放一整段,需要扫描整个文件系统中别的元数据(图中的 n12 和 n10 和
11)来确定有没有引用到目标段中的地址,可见释放一个段是一个 <span class="math">\(O(N)\)</span> 的操作,其中 N
是元数据段的数量,按文件系统的大小增长,
于是删除快照之类可能要连续释放很多段的操作在日志文件系统中是个 <span class="math">\(O(N^2)\)</span> 甚至更昂贵的操作。
在文件系统相对比较小而系统内存相对比较大的时候,比如手机上或者PC读写SD卡,大部分元数据块(
其中包含块指针)都能放入内存缓存起来的话,这个扫描操作的开销还是可以接受的。
但是对大型存储系统显然扫描并释放空间就不合适了。</p>
<p>段的抽象用在闪存类存储设备上的一点优势在于,闪存通常也有擦除块的概念,比写入块的大小要大,
是连续的多个写入块构成,从而日志结构的文件系统中一个段可以直接对应到闪存的一个擦除块上。
所以闪存设备诸如U盘或者 SSD 通常在底层固件中用日志结构文件系统模拟一个块设备,来做写入平衡。
大家所说的 SSD 上固件做的 GC ,大概也就是这样一种操作。</p>
<p>基于段的 GC 还有一个显著缺陷,需要扫描元数据,复制搬运仍然被引用到的块,这不光会增加设备写入,
还需要调整现有数据结构中的指针,调整指针需要更多写入,同时又释放更多数据块,
F2FS 等一些文件系统设计中把这个问题叫 Wandering Tree Problem ,在 F2FS
设计中是通过近乎「作弊」的 <a class="reference external" href="https://www.kernel.org/doc/html/latest/filesystems/f2fs.html#id1">NAT 转换表</a>
放在存储设备期待的 FAT 所在位置,不仅能让需要扫描的元数据更集中,还能减少这种指针调整导致的写入。</p>
<p>不过基于段的 GC 也有一些好处,它不需要复杂的文件系统设计,不需要特殊构造的指针,
就能很方便地支持大量快照。一些日志结构文件系统比如 NILFS 用这一点支持了「连续快照(continuous
snapshots)」,每次文件系统提交都是自动创建一个快照,用户可以手动标记需要保留哪些快照,
GC 算法则排除掉用户手动标记的快照之后,根据快照创建的时间,先从最老的未标记快照开始回收。
即便如此, GC 的开销(CPU时间和磁盘读写带宽)仍然是 NILFS
最为被人诟病的地方,是它难以被广泛采用的原因。 为了加快 NILFS 这类日志文件系统的 GC
性能让他们能更适合于普通使用场景,也有许多学术研究致力于探索和优化 GC
,使用更先进的数据结构和算法跟踪数据块来调整 GC 策略,比如这里有一篇
<a class="reference external" href="https://www.complang.tuwien.ac.at/Diplomarbeiten/rohner18.pdf">State-of-the-art Garbage Collection Policies for NILFS2</a>
。</p>
</div>
<div class="section" id="wafl">
<h3><a class="toc-backref" href="#id52">3.2 WAFL 早期使用的可用空间位图数组</a></h3>
<p>从日志结构文件系统使用 GC 的困境中可以看出,文件系统级别实际更合适的,
可能不是在运行期依赖扫描元数据来计算空间利用率的 GC
,而是在创建快照时或者写入数据时就预先记录下快照的空间利用情况,
从而可以细粒度地跟踪空间和回收空间,这也是 WAFL 早期实现快照的设计思路。</p>
<p>WAFL 早期记录快照占用数据块的思路从表面上来看也很「暴力」,传统文件系统一般有个叫做「位图(bitmap
)」的数据结构,用一个二进制位记录一个数据块是否占用,靠扫描位图来寻找可用空间和已用空间。 WAFL
的设计早期中考虑既然需要支持快照,那就把记录数据块占用情况的位图,变成快照的数组。
于是整个文件系统有个 256 大小的快照利用率数组,数组中每个快照记录自己占用的数据块位图,
文件系统中最多能容纳 255 个快照。</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/5dfeb8e2.png"/>
<p>上面每个单元格都是一个二进制位,表示某个快照有没有引用某个数据块。有这样一个位图的数组之后,
就可以直接扫描位图判断出某个数据块是否已经占用,可以找出尚未被占用的数据块用作空间分配,
也可以方便地计算每个快照引用的空间大小或者独占的空间大小,估算删除快照后可以释放的空间。</p>
<p>需要注意的是,文件系统中可以有非常多的块,从而位图数组比位图需要更多的元数据来表达。
比如估算一下传统文件系统中一块可以是 4KiB 大小,那么跟踪空间利用的位图需要 1bit/4KiB
, 1TiB 的盘就需要 32MiB 的元数据来存放位图;
而 WAFL 这种位图数组即便限制了快照数量只能有255个,仍需要 256bit/4KiB 的空间开销,
1TiB 的盘需要的元数据开销陡增到 8GiB ,这些还只是单纯记录空间利用率的位图数组,不包括别的元数据。</p>
<p>使用这么多元数据表示快照之后,创建快照的开销也相应地增加了,需要复制整个位图来创建一个新的快照,
按上面的估算 1TiB 的盘可能需要复制 32MiB 的位图,这不再是一瞬能完成的事情,
期间可能需要停下所有对文件系统的写入等待复制完成。
位图数组在存储设备上的记录方式也很有讲究,当删除快照时希望能快速读写上图中的一整行位图,
于是可能希望每一行位图的存储方式在磁盘上都尽量连续,
而在普通的写入操作需要分配新块时,想要按列的方式扫描位图数组,找到没有被快照占用的块,
从而上图中按列的存储表达也希望在磁盘上尽量连续。
WAFL 的设计工程师们在位图数组的思路下,实现了高效的数据结构让上述两种维度的操作都能快速完成,
但是这绝不是一件容易的事情。</p>
<p>位图数组的表达方式也有其好处,比如除了快照之外,也可以非常容易地表达类似 ZFS
的克隆和独立的文件系统这样的概念,这些东西和快照一样,占用仅有的 256 个快照数量限制。
这样表达的克隆可以有数据块和别的文件系统共享,文件系统之间也可以有类似
reflink 的机制共享数据块,在位图数组的相应位置将位置1即可。</p>
<p>使用位图数组的做法,也只是 WAFL 早期可能采用的方式,由于 WAFL 本身是闭源产品,
难以获知它具体的工作原理。哈佛大学和 NetApp 的职员曾经在 FAST10
(USENIX Conference on File and Storage Technologies) 上发表过一篇讲解高效跟踪和使用
back reference 的论文,叫
<a class="reference external" href="https://www.usenix.org/legacy/event/fast10/tech/full_papers/macko.pdf">Tracking Back References in a Write-Anywhere File System</a>
,可以推测在新一代 WAFL 的设计中可能使用了类似 btrfs backref 的实现方式,接下来会详细介绍。</p>
</div>
<div class="section" id="id24">
<h3><a class="toc-backref" href="#id53">3.3 ZFS 中关于快照和克隆的空间跟踪算法</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
How ZFS snapshots really work And why they perform well (usually)</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/NXg86uBDSqI"></iframe></div><p><a class="reference external" href="https://www.bsdcan.org/2019/schedule/attachments/500_How%20ZFS%20Snapshots%20Really%20Work.pdf">幻灯片可以从这里下载</a></p>
<p><embed class="embed-responsive-item" src="//farseerfc.me/zhs/images/500_How_ZFS_Snapshots_Really_Work.pdf" style="width:90%;height:480px"/></p>
</div>
</div>
<p>OpenZFS 的项目领导者,同时也是最初设计 ZFS 中 DMU 子系统的作者 Matt Ahrens 在 DMU
和 DSL
中设计并实现了 ZFS 独特的快照的空间跟踪算法。他也在很多地方发表演讲,讲过这个算法的思路和细节,
比如右侧就是他在 BSDCan 2019 做的演讲
<a class="reference external" href="https://youtu.be/NXg86uBDSqI">How ZFS snapshots really work And why they perform well (usually)</a>
的 YouTube 视频。</p>
<p>其中 Matt 讲到了三个删除快照的算法,分别可以叫做「🐢乌龟算法」、「🐰兔子算法」、「🐆豹子算法」,
接下来简单讲讲这些算法背后的思想和实现方式。</p>
<div class="section" id="id26">
<h4><a class="toc-backref" href="#id54">🐢乌龟算法:概念上 ZFS 如何删快照</a></h4>
<p>乌龟算法没有实现在 ZFS 中,不过方便理解 ZFS 在概念上如何考虑快照删除这个问题,从而帮助理解
后面的🐰兔子算法和🐆豹子算法。</p>
<p>要删除一个快照, ZFS 需要找出这个快照引用到的「独占」数据块,也就是那些不和别的数据集或者快照共享的
数据块。 ZFS 删除快照基于这几点条件:</p>
<ol class="arabic simple">
<li>ZFS 快照是只读的。创建快照之后无法修改其内容。</li>
<li>ZFS 的快照是严格按时间顺序排列的,这里的时间指 TXG id ,即记录文件系统提交所属事务组的严格递增序号。</li>
<li>ZFS 不存在 reflink 之类的机制,从而在某个时间点删除掉的数据块,不可能在比它更后面的快照中「复活」。</li>
</ol>
<p>第三点关于 reflink 造成的数据复活现象可能需要解释一下,比如在(支持 reflink 的) btrfs 中有如下操作:</p>
<div class="highlight"><pre><span class="code-line"><span></span>btrfs subvolume snapshot -r fs s1</span>
<span class="code-line">rm fs/somefile</span>
<span class="code-line">btrfs subvolume snapshot -r fs s2</span>
<span class="code-line">cp --reflink<span class="o">=</span>always s1/somefile fs/somefile</span>
<span class="code-line">btrfs subvolume snapshot -r fs s3</span>
</pre></div>
<p>我们对 fs 创建了 s1 快照,删除了 fs 中某个文件,创建了 s2 快照,然后用 reflink
把刚刚删除的文件从 s1 中复制出来,再创建 s3 。如此操作之后,按时间顺序有 s1、s2、s3 三个快照:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/f91316de.png"/>
<p>其中只有 s2 不存在 somefile ,而 s1 、 s3 和当前的 fs 都有,并且都引用到了同一个数据块。
于是从时间线来看, somefile 的数据块在 s2 中「死掉」了,又在 s3 中「复活」了。</p>
<p>而 ZFS (目前还)不支持 reflink ,所以没法像这样让数据块复活。一旦某个数据块在某个快照中「死」了,
就意味着它在随后的所有快照中都不再被引用到了。</p>
<p>ZFS 的快照具有的上述三点条件,使得 ZFS 的快照删除算法可以基于 birth time
。回顾上面 <a class="reference internal" href="#id18">ZFS 的块指针</a> 中讲到, ZFS 的每个块指针都有一个 birth txg
属性,记录这个块诞生时 pool 所在的 txg 。于是可以根据这个 birth txg
找到快照所引用的「独占」数据块然后释放掉它们。</p>
<p>具体来说,🐢乌龟算法可以这样删除一个快照:</p>
<ol class="arabic simple">
<li>在 DSL 层找出要删除的快照(我们叫他 s ),它的前一个快照(叫它 ps ),后一个快照(叫它 ns
),分别有各自的 birth txg 叫 s.birth, ps.birth, ns.birth 。</li>
<li>遍历 s 的 DMU 对象集指针所引出的所有块指针。
这里所有块指针在逻辑上构成一个由块指针组成的树状结构,可以有间接块组成的指针树,可以有对象集的
dnode 保存的块指针,这些都可以看作是树状结构的中间节点。<ol class="arabic">
<li>每个树节点的指针 bp,考察如果 bp.birth <= ps.birth
,那么这个指针和其下所有指针都还被前一个快照引用着,需要保留这个 bp 引出的整个子树。</li>
<li>按定义 bp.birth 不可能 > s.birth 。</li>
<li>对所有满足 ps.birth < bp.birtu <= s.birth 的 bp ,需要去遍历 ns
的相应块指针(同样文件的同样偏移位置),看是否还在引用 bp 。<ul>
<li>如果存在,继续递归往下考察树状结构中 bp 的所有子节点指针。因为可能共享了这个 bp 但
CoW 了新的子节点。</li>
<li>如果不存在,说明下一个快照中已经删了 bp 。这时可以确定地说 bp 是 s 的「独占」数据块。</li>
</ul>
</li>
</ol>
</li>
<li>释放掉所有找到的 s 所「独占」的数据块。</li>
</ol>
<p>上述算法的一些边角情况可以自然地处理,比如没有后一个快照时使用当前数据集的写入点,
没有前一个快照时那么不被后一个快照引用的数据块都是当前要删除快照的独占数据块。</p>
<p>分析一下乌龟算法的复杂度的话,算法需要分两次,读 s 和 ns 中引用到的所有 ps
之后创建的数据块的指针,重要的是这些读都是在整个文件系统范围内的随机读操作,所以速度非常慢……</p>
</div>
<div class="section" id="id27">
<h4><a class="toc-backref" href="#id55">🐰兔子算法:死亡列表算法(ZFS早期)</a></h4>
<p>可以粗略地认为🐢乌龟算法算是用 birth txg 优化代码路径的 GC 算法,利用了一部分元数据中的 birth txg
信息来避免扫描所有元数据,但是概念上仍然是在扫描元数据找出快照的独占数据块,
而非记录和跟踪快照的数据块,在最坏的情况下仍然可能需要扫描几乎所有元数据。</p>
<p>🐰兔子算法基于🐢乌龟算法的基本原理,在它基础上跟踪快照所引用数据块的一些信息,
从而很大程度上避免了扫描元数据的开销。ZFS 在早期使用这个算法跟踪数据集和快照引用数据块的情况。</p>
<p>🐰兔子算法为每个数据集(文件系统或快照)增加了一个数据结构,叫死亡列表(dead list),
记录 <strong>前一个快照中还活着,而当前数据集中死掉了的数据块指针</strong>
,换句话说就是在本数据集中「杀掉」的数据块。举例画图大概是这样</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/ab76244a.png"/>
<p>上图中有三个快照和一个文件系统,共 4 个数据集。每个数据集维护自己的死亡列表,
死亡列表中是那些在该数据集中被删掉的数据块。于是🐰兔子算法把🐢乌龟算法所做的操作分成了两部分,
一部分在文件系统删除数据时记录死亡列表,另一部分在删除快照时根据死亡列表释放需要释放的块。</p>
<p>在当前文件系统删除数据块(不再被当前文件系统引用)时,负责比对 birth txg
维护当前文件系统的死亡列表。每删除一个数据块,指针为 bp 时,判断 bp.birth
和文件系统最新的快照(上图为 s3)的 birth:</p>
<ul class="simple">
<li>bp.birth <= s3.birth: 说明 bp 被 s3 引用,于是将 bp 加入 fs1 的 deadlist</li>
<li>bp.birth > s3.birth:说明 bp 指向的数据块诞生于 s3 之后,可以直接释放 bp 指向的块。</li>
</ul>
<p>创建新快照时,将当前文件系统(图中 fs1)的死亡列表交给快照,文件系统可以初始化一个空列表。</p>
<p>删除快照时,我们有被删除的快照 s 和前一个快照 ps 、后一个快照 ns ,需要读入当前快照 s
和后一个快照 ns 的死亡列表:</p>
<ol class="arabic simple">
<li>对 s.deadlist 中的每个指针 bp<ul>
<li>复制 bp 到 ns.deadlist</li>
</ul>
</li>
<li>对 ns.deadlist 中的每个指针 bp (其中包含了上一步复制来的)<ul>
<li>如果 bp.birth > ps.birth ,释放 bp 的空间</li>
<li>否则保留 bp</li>
</ul>
</li>
</ol>
<p>换个说法的话, <strong>死亡列表记录的是每个数据集需要负责删除,但因为之前的快照还引用着所以不能删除的数据块列表</strong>
。从当前文件系统中删除一个数据块时,这个职责最初落在当前文件系统身上,随后跟着创建新快照职责被转移到新快照上。
每个负责的数据集根据数据块的出生时间是否早于之前一个快照来判断现在是否能立刻释放该块,
删除一个快照时则重新评估自己负责的和下一个快照负责的数据块的出生时间。</p>
<p>从所做的事情来看,🐰兔子算法并没有比🐢乌龟算法少做很多事情。🐢乌龟算法删除一个快照,
需要遍历当前快照和后一个快照两组数据块指针中,新写入的部分;
🐰兔子算法则需要遍历当前快照和后一个快照两个死亡列表中,新删除的块指针。
但是实际🐰兔子算法能比🐢乌龟算法快不少,因为维护死亡列表的操作只在文件系统删除数据时和删除快照时,
顺序写入,并且删除快照时也只需要顺序读取死亡列表。在磁盘这种块设备上,顺序访问能比随机访问有数量级的差异。</p>
<p>不过记录死亡列表也有一定存储开销。最差情况下,比如把文件系统写满之后,创建一个快照,
再把所有数据都删掉,此时文件系统引用的所有数据块的块指针都要保存在文件系统的死亡列表中。
按 ZFS 默认的 128KiB 数据块大小,每块需要 128 字节的块指针,存储这些死亡列表所需开销可能要
整个文件系统大小的 1/1024 。如果用 4KiB 的数据块大小,所需开销则是 1/32 , 1TiB
的盘会有 32GiB 拿来存放这些块指针,将高于用位图数组所需的存储量。</p>
</div>
<div class="section" id="id28">
<h4><a class="toc-backref" href="#id56">🐆豹子算法:死亡列表的子列表</a></h4>
<p>🐆豹子算法是 ZFS 后来在 2009 年左右实现的算法。在🐰兔子算法中就可以看到,每次删除快照操作死亡列表的时候,
都需要扫描死亡列表中的块指针,根据指针中记录的 birth txg 做判断是否能直接释放或是需要保留到另一个快照的死亡列表。
于是🐆豹子算法的思路是,在死亡列表中记录块指针时,就把其中的块指针按 birth txg 分成子列表(sublist)。</p>
<p>比如上面🐰兔子算法中那4个死亡列表,可以这样拆成子列表:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/34b39b01.png"/>
<p>这样拆成子列表之后,每次从死亡列表中释放数据块都能根据出生时间找到对应的子列表,
然后连续释放整个子列表。每次合并死亡列表时,也能直接用单链表穿起需要合并的子列表,不需要复制块指针。</p>
<p>死亡列表并不在跟踪快照的独占大小,而是在跟踪快照所需负责删除的数据块大小,
从这个数值可以推算出快照的独占大小之类的信息。
有了按出生时间排列的死亡列表子列表之后,事实上给任何一个出生时间到死亡时间的范围,
都可以找出对应的几个子列表,从而根据子列表的大小可以快速计算出每个快照范围的「独占」数据块、
「共享」数据块等大小,这不光在删除快照时很有用,也可以用来根据大小估算 zfs send
或者别的基于快照操作时需要的时间。</p>
<p>从直觉上理解,虽然 ZFS 没有直接记录每个数据块属于哪个数据集,但是 ZFS
跟踪记录了每个数据块的归属信息,也就是说由哪个数据集负责释放这个数据块。
在文件系统中删除数据块或者快照时,这个归属信息跟着共享数据块转移到别的快照中,直到最终被释放掉。</p>
</div>
<div class="section" id="id29">
<h4><a class="toc-backref" href="#id57">生存日志:ZFS 如何管理克隆的空间占用</a></h4>
<div class="panel panel-default">
<div class="panel-heading">
Fast Clone Deletion by Sara Hartse</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/GLABJRWwGMk"></iframe></div></div>
</div>
<p>以上三种算法负责在 ZFS 中跟踪快照的空间占用,它们都基于数据块的诞生时间,所以都假设 ZFS
中对数据块的分配是位于连续的快照时间轴上。但是明显 ZFS 除了快照和文件系统,
还有另一种数据集可能分配数据块,那就是 <a class="reference internal" href="#id12">克隆</a>
,于是还需要在克隆中使用不同的算法单独管理因克隆而分配的数据块。
OpenZFS Summit 2017 有个演讲 <a class="reference external" href="https://www.youtube.com/watch?v=GLABJRWwGMk">Fast Clone Deletion by Sara Hartse</a>
解释了其中的细节。</p>
<p>首先克隆的存在本身会锁住克隆引用到的快照,不能删除这些被依赖的快照,
所以克隆无须担心靠快照共享的数据块的管理问题。因此克隆需要管理的,是从快照分离之后,
新创建的数据块。</p>
<p>和🐢乌龟算法一样,原理上删除克隆的时候可以遍历克隆引用的整个 DMU
对象集,找出其中晚于快照的诞生时间的数据块,然后释放它们。也和🐢乌龟算法一样,
这样扫描整个对象集的开销很大,所以使用一个列表来记录数据块指针。
克隆管理新数据块的思路和快照的🐰兔子算法维持死亡列表的思路相反,
记录所有新诞生的数据块,这个列表叫做「生存日志(livelist)」。</p>
<p>克隆不光要记录新数据块的诞生,还要记录新数据块可能的死亡,所以磁盘上保存的生存日志虽然叫 livelist
,但不像死亡列表那样是列表的形式,而是日志的形式,而内存中保存的生存日志则组织成了棵
<a class="reference external" href="https://zh.wikipedia.org/wiki/AVL%E6%A0%91">自平衡树(AVLTree)</a> 来加速查找。</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/7f244754.png"/>
<p>磁盘上存储的生存日志如上图,每个表项记录它是分配(A)或者删除(F)一个数据块,同时记录数据块的地址。
这些记录在一般情况下直接记录在日志末尾,随着对克隆的写入操作而不断增长,长到一定程度则从内存中的
AVL Tree 直接输出一个新的生存日志替代掉旧的,合并其中对应的分配和删除操作。</p>
<p>生存日志可以无限增长,从而要将整个生存列表载入内存也有不小的开销,这里的解决方案有点像快照管理中用
🐆豹子算法改进🐰兔子算法的思路,把一个克隆的整个生存日志也按照数据块的诞生时间拆分成子列表。
Sara Hartse 的演讲 Fast Clone Deletion 中继续解释了其中的细节和优化方案,感兴趣的可以看看。</p>
</div>
</div>
<div class="section" id="id30">
<h3><a class="toc-backref" href="#id58">3.4 btrfs 的空间跟踪算法:引用计数与反向引用</a></h3>
<p>理解了 ZFS 中根据 birth txg 管理快照和克隆的算法之后,可以发现它们基于的假设难以用于 WAFL
和 btrfs 。 ZFS 严格区分文件系统、快照、克隆,并且不存在 reflink ,从而可以用 birth txg
判断数据块是否需要保留,而 WAFL 和 btrfs 中不存在 ZFS 的那些数据集分工,又想支持 reflink
,可见单纯基于 birth txg 不足以管理 WAFL 和 btrfs 子卷。</p>
<p>让我们回到一开始日志结构文件系统中基于垃圾回收(GC)的思路上来,作为程序员来看,
当垃圾回收的性能不足以满足当前需要时,大概很自然地会想到:引用计数(reference counting)。
编程语言中用引用计数作为内存管理策略的缺陷是:强引用不能成环,
这在文件系统中看起来不是很严重的问题,文件系统总体上看是个树状结构,或者就算有共享的数据也是个
上下层级分明的有向图,很少会使用成环的指针,以及文件系统记录指针的时候也都会区分指针的类型,
根据指针类型可以分出强弱引用。</p>
<div class="section" id="extent-tree">
<h4><a class="toc-backref" href="#id59">EXTENT_TREE 和引用计数</a></h4>
<p>btrfs 中就是用引用计数的方式跟踪和管理数据块的。引用计数本身不能保存在 FS_TREE
或者指向的数据块中,因为这个计数需要能够变化,对只读快照来说整个 FS_TREE 都是只读的。
所以这里增加一层抽象, btrfs 中关于数据块的引用计数用一个单独的 CoW B树来记录,叫做
EXTENT_TREE ,保存于 ROOT_TREE 中的 2 号对象位置。</p>
<p>btrfs 中每个块都是按 <a class="reference external" href="https://en.wikipedia.org/wiki/Extent_(file_systems)">区块(extent)</a>
的形式分配的,区块是一块连续的存储空间,而非 zfs 中的固定大小。每个区块记录存储的位置和长度,
以及这里所说的引用计数。所以本文最开始讲 <a class="reference internal" href="#btrfs">Btrfs 的子卷和快照</a> 中举例的那个平坦布局,如果画上
EXTENT_TREE 大概像是下图这样,其中每个粗箭头是一个区块指针,指向磁盘中的逻辑地址,细箭头则是对应的
EXTENT_TREE 中关于这块区块的描述:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: Flat_layout_extents_on_disk Pages: 1 -->
<svg class="svg-responsive" height="599pt" viewbox="0.00 0.00 1280.00 598.88" width="1280pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 594.8766)">
<title>Flat_layout_extents_on_disk</title>
<polygon fill="#ffffff" points="-4,4 -4,-594.8766 1276,-594.8766 1276,4 -4,4" stroke="transparent"></polygon>
<!-- superblock -->
<g class="node" id="node1">
<title>superblock</title>
<polygon fill="none" points="0,-315.5 0,-407.5 123,-407.5 123,-315.5 0,-315.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-392.3">SUPERBLOCK</text>
<polyline fill="none" points="0,-384.5 123,-384.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-369.3">...</text>
<polyline fill="none" points="0,-361.5 123,-361.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-346.3">root_tree</text>
<polyline fill="none" points="0,-338.5 123,-338.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="61.5" y="-323.3">...</text>
</g>
<!-- roottree -->
<g class="node" id="node2">
<title>roottree</title>
<polygon fill="none" points="195,-62 195,-361 504,-361 504,-62 195,-62" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-345.8">ROOT_TREE</text>
<polyline fill="none" points="195,-338 504,-338 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-322.8">2: extent_tree</text>
<polyline fill="none" points="195,-315 504,-315 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-299.8">3: chunk_tree</text>
<polyline fill="none" points="195,-292 504,-292 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-276.8">4: dev_tree</text>
<polyline fill="none" points="195,-269 504,-269 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-253.8">5: fs_tree</text>
<polyline fill="none" points="195,-246 504,-246 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-230.8">6: root_dir "default" -> ROOT_ITEM 256</text>
<polyline fill="none" points="195,-223 504,-223 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-207.8">10: free_space_tree</text>
<polyline fill="none" points="195,-200 504,-200 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-184.8">256: fs_tree "root"</text>
<polyline fill="none" points="195,-177 504,-177 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-161.8">257: fs_tree "home"</text>
<polyline fill="none" points="195,-154 504,-154 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-138.8">258: fs_tree "www"</text>
<polyline fill="none" points="195,-131 504,-131 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-115.8">259: fs_tree "postgres"</text>
<polyline fill="none" points="195,-108 504,-108 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-92.8">-7: tree_log_tree</text>
<polyline fill="none" points="195,-85 504,-85 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="349.5" y="-69.8">-5: orphan_root</text>
</g>
<!-- superblock->roottree -->
<g class="edge" id="edge1">
<title>superblock:sn_root->roottree:label</title>
<path d="M123,-349.5C151.375,-349.5 160.8795,-349.5 184.9792,-349.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="185,-353.0001 195,-349.5 185,-346.0001 185,-353.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- toplevel -->
<g class="node" id="node3">
<title>toplevel</title>
<polygon fill="none" points="576,-260.5 576,-490.5 923,-490.5 923,-260.5 576,-260.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-475.3">FS_TREE "toplevel"</text>
<polyline fill="none" points="576,-467.5 923,-467.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-452.3"> </text>
<polyline fill="none" points="576,-444.5 923,-444.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-429.3">256: inode_item DIR</text>
<polyline fill="none" points="576,-421.5 923,-421.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-406.3">256: dir_item: "root" -> ROOT_ITEM 256</text>
<polyline fill="none" points="576,-398.5 923,-398.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-383.3">256: dir_item: "home" -> ROOT_ITEM 257</text>
<polyline fill="none" points="576,-375.5 923,-375.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-360.3">256: dir_item: "var" -> INODE_ITEM 257</text>
<polyline fill="none" points="576,-352.5 923,-352.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-337.3">256: dir_item: "postgres" -> ROOT_ITEM 259</text>
<polyline fill="none" points="576,-329.5 923,-329.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-314.3"> </text>
<polyline fill="none" points="576,-306.5 923,-306.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-291.3">257: inode_item DIR</text>
<polyline fill="none" points="576,-283.5 923,-283.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-268.3">257: dir_item: "www" -> ROOT_ITEM 258</text>
</g>
<!-- roottree->toplevel -->
<g class="edge" id="edge2">
<title>roottree:root_fs->toplevel:label</title>
<path d="M504,-257.5C604.0795,-257.5 481.5022,-464.165 565.9262,-478.6961" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="565.7533,-482.1934 576,-479.5 566.3101,-475.2156 565.7533,-482.1934" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- root -->
<g class="node" id="node4">
<title>root</title>
<polygon fill="none" points="667.5,-195.5 667.5,-241.5 831.5,-241.5 831.5,-195.5 667.5,-195.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-226.3">FS_TREE "root"</text>
<polyline fill="none" points="667.5,-218.5 831.5,-218.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-203.3">256: inode_item DIR</text>
</g>
<!-- roottree->root -->
<g class="edge" id="edge3">
<title>roottree:root_sub_root->root:label</title>
<path d="M504,-188.5C575.5091,-188.5 591.078,-226.6548 657.3427,-230.2318" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.4111,-233.7347 667.5,-230.5 657.5959,-226.7372 657.4111,-233.7347" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- home -->
<g class="node" id="node5">
<title>home</title>
<polygon fill="none" points="667.5,-130.5 667.5,-176.5 831.5,-176.5 831.5,-130.5 667.5,-130.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-161.3">FS_TREE "home"</text>
<polyline fill="none" points="667.5,-153.5 831.5,-153.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-138.3">256: inode_item DIR</text>
</g>
<!-- roottree->home -->
<g class="edge" id="edge4">
<title>roottree:root_sub_home->home:label</title>
<path d="M504,-165.5C573.1185,-165.5 592.9293,-165.5 657.2326,-165.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.5,-169.0001 667.5,-165.5 657.5,-162.0001 657.5,-169.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- www -->
<g class="node" id="node6">
<title>www</title>
<polygon fill="none" points="667.5,-65.5 667.5,-111.5 831.5,-111.5 831.5,-65.5 667.5,-65.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-96.3">FS_TREE "www"</text>
<polyline fill="none" points="667.5,-88.5 831.5,-88.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-73.3">256: inode_item DIR</text>
</g>
<!-- roottree->www -->
<g class="edge" id="edge5">
<title>roottree:root_sub_www->www:label</title>
<path d="M504,-142.5C537.3333,-142.5 543.742,-129.8984 576,-121.5 612.9079,-111.8909 623.9319,-102.3027 657.4191,-100.7257" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="657.5809,-104.2231 667.5,-100.5 657.4241,-97.2248 657.5809,-104.2231" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- postgres -->
<g class="node" id="node7">
<title>postgres</title>
<polygon fill="none" points="667,-.5 667,-46.5 832,-46.5 832,-.5 667,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-31.3">FS_TREE "postgres"</text>
<polyline fill="none" points="667,-23.5 832,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="749.5" y="-8.3">256: inode_item DIR</text>
</g>
<!-- roottree->postgres -->
<g class="edge" id="edge6">
<title>roottree:root_sub_postgres->postgres:label</title>
<path d="M504,-119.5C546.5206,-119.5 538.1561,-75.8865 576,-56.5 609.5913,-39.292 622.8882,-35.9967 656.4598,-35.5574" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="656.5202,-39.0572 666.5,-35.5 656.4801,-32.0573 656.5202,-39.0572" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- extent_tree -->
<g class="node" id="node8">
<title>extent_tree</title>
<polygon fill="none" points="995,-338.5 995,-568.5 1272,-568.5 1272,-338.5 995,-338.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-553.3">EXTENT_TREE</text>
<polyline fill="none" points="995,-545.5 1272,-545.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-530.3"> </text>
<polyline fill="none" points="995,-522.5 1272,-522.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-507.3">0x2000 len=0x1000 : ref=1 gen=8</text>
<polyline fill="none" points="995,-499.5 1272,-499.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-484.3">0x3000 len=0x1000 : ref=1 gen=8</text>
<polyline fill="none" points="995,-476.5 1272,-476.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-461.3">0x11000 len=0x1000 : ref=1 gen=8</text>
<polyline fill="none" points="995,-453.5 1272,-453.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-438.3">0x12000 len=0x1000 : ref=1 gen=6</text>
<polyline fill="none" points="995,-430.5 1272,-430.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-415.3">0x13000 len=0x1000 : ref=1 gen=6</text>
<polyline fill="none" points="995,-407.5 1272,-407.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-392.3">0x14000 len=0x1000 : ref=1 gen=6</text>
<polyline fill="none" points="995,-384.5 1272,-384.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-369.3">0x15000 len=0x1000 : ref=1 gen=7</text>
<polyline fill="none" points="995,-361.5 1272,-361.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="1133.5" y="-346.3">...</text>
</g>
<!-- roottree->extent_tree -->
<g class="edge" id="edge7">
<title>roottree:root_extent->extent_tree:label</title>
<path d="M504,-326.5C601.4064,-326.5 498.2586,-474.812 576,-533.5 722.1806,-643.8537 805.4304,-560.628 984.9901,-557.5848" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="985.0301,-561.0847 995,-557.5 984.9707,-554.085 985.0301,-561.0847" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- roottree->extent_tree -->
<g class="edge" id="edge8">
<title>roottree:label->extent_tree:extent_roottree</title>
<path d="M504,-349.5C578.3499,-349.5 513.6489,-460.0002 576,-500.5 651.4417,-549.5028 885.1193,-514.1107 984.9203,-511.6341" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="985.0475,-515.1328 995,-511.5 984.9543,-508.1334 985.0475,-515.1328" stroke="#000000"></polygon>
</g>
<!-- toplevel->extent_tree -->
<g class="edge" id="edge10">
<title>toplevel:label->extent_tree:extent_toplevel</title>
<path d="M923,-479.5C951.9064,-479.5 960.5287,-468.4922 984.8395,-465.9983" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="985.1835,-469.4857 995,-465.5 984.8405,-462.4941 985.1835,-469.4857" stroke="#000000"></polygon>
</g>
<!-- root->extent_tree -->
<g class="edge" id="edge11">
<title>root:label->extent_tree:extent_root</title>
<path d="M831.5,-230.5C873.224,-230.5 891.5516,-224.0794 923,-251.5 988.2728,-308.413 911.5209,-430.9023 984.7571,-440.8546" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="984.7996,-444.3641 995,-441.5 985.2399,-437.3779 984.7996,-444.3641" stroke="#000000"></polygon>
</g>
<!-- home->extent_tree -->
<g class="edge" id="edge12">
<title>home:label->extent_tree:extent_home</title>
<path d="M831.5,-165.5C873.224,-165.5 892.4068,-158.1284 923,-186.5 999.5328,-257.4751 894.4682,-408.0627 984.9685,-417.9844" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="984.8335,-421.482 995,-418.5 985.1929,-414.4912 984.8335,-421.482" stroke="#000000"></polygon>
</g>
<!-- www->extent_tree -->
<g class="edge" id="edge13">
<title>www:label->extent_tree:extent_www</title>
<path d="M831.5,-100.5C873.224,-100.5 893.0377,-92.463 923,-121.5 1010.8577,-206.6445 877.0747,-385.0229 984.9412,-395.058" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="984.856,-398.5576 995,-395.5 985.1633,-391.5643 984.856,-398.5576" stroke="#000000"></polygon>
</g>
<!-- postgres->extent_tree -->
<g class="edge" id="edge14">
<title>postgres:label->extent_tree:extent_postgres</title>
<path d="M832.5,-35.5C873.7909,-35.5 893.8612,-27.2448 923,-56.5 1022.0703,-155.9659 859.5991,-361.7078 984.6327,-372.0916" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="984.8699,-375.6035 995,-372.5 985.1456,-368.609 984.8699,-375.6035" stroke="#000000"></polygon>
</g>
<!-- extent_tree->extent_tree -->
<g class="edge" id="edge9">
<title>extent_tree:extent_extent->extent_tree:label</title>
<path d="M994.6922,-483.8069C898.1723,-484.611 803,-494.4106 803,-523 803,-550.5843 891.5981,-560.6768 984.5214,-562.0745" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="984.652,-565.5762 994.6922,-562.1931 984.7337,-558.5767 984.652,-565.5762" stroke="#000000"></polygon>
</g>
</g>
</svg>
<div class="panel panel-default">
<div class="panel-heading">
btrfs 中关于 <code class="code">
chattr +C</code>
关闭了 CoW 的文件的处理</div>
<div class="panel-body">
<div class="label label-warning">
<strong>2020年2月20日补充</strong></div>
<p>这里从 EXTENT_TREE 的记录可以看出,每个区块都有引用计数记录。对用 <code class="code">
chattr +C</code>
关闭了 CoW 的文件而言,文件数据同样还是有引用计数,可以和别的文件或者快照共享文件数据的。
这里的特殊处理在于,每次写入一个 nocow 的文件的时候,考察这个文件指向区块的引用计数,
如果引用计数 >1 ,表示这个文件的区块发生过 reflink ,那会对文件内容做一次 CoW 断开
reflink 并写入新位置;如果引用计数 =1 ,那么直接原地写入文件内容而不 CoW 。于是
nocow 的文件仍然能得到 reflink 和 snapshot 的功能,
使用这些功能仍然会造成文件碎片并伴随性能损失,只是在引用计数为 1 的时候不发生 CoW 。</p>
</div>
</div>
<p>包括 ROOT_TREE 和 EXTENT_TREE 在内,btrfs 中所有分配的区块(extent)都在 EXTENT_TREE
中有对应的记录,按区块的逻辑地址索引。从而给定一个区块,能从 EXTENT_TREE 中找到 ref
字段描述这个区块有多少引用。不过 ROOT_TREE 、 EXTENT_TREE 和别的一些 pool-wide
数据结构本身不依赖引用计数的,这些数据结构对应的区块的引用计数总是 1 ,不会和别的树共享区块;从 FS_TREE
开始的所有树节点都可以共享区块,这包括所有子卷的元数据和文件数据,这些区块对应的引用计数可以大于
1 表示有多处引用。</p>
<p>EXTENT_TREE 按区块的逻辑地址索引,记录了起始地址和长度,所以 EXTENT_TREE 也兼任 btrfs
的空间利用记录,充当别的文件系统中 block bitmap 的职责。比如上面例子中的 extent_tree 就表示
<code class="code">
[0x2000,0x4000) [0x11000,0x16000)</code>
这两段连续的空间是已用空间,
剩下的空间按定义则是可用空间。为了加速空间分配器, btrfs 也有额外的
free space cache 记录在 ROOT_TREE 的 10 号位置 free_space_tree 中,不过在 btrfs
中这个 free_space_tree 记录的信息只是缓存,必要时可以通过
<code class="code">
btrfs check --clear-space-cache</code>
扔掉这个缓存重新扫描 extent_tree 并重建可用空间记录。</p>
<p>比如我们用如下命令创建了两个文件,通过 reflink 让它们共享区块,然后创建两个快照,
然后删除文件系统中的 file2 :</p>
<div class="highlight"><pre><span class="code-line"><span></span>write fs/file1</span>
<span class="code-line">cp --reflink<span class="o">=</span>always fs/file1 fs/file2</span>
<span class="code-line">btrfs subvolume snapshot fs sn1</span>
<span class="code-line">btrfs subvolume snapshot fs sn2</span>
<span class="code-line">rm fs/file2</span>
</pre></div>
<p>经过以上操作之后,整个 extent_tree 的结构中记录的引用计数大概如下图所示:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: btrfs_reflink_backref Pages: 1 -->
<svg class="svg-responsive" height="290pt" viewbox="0.00 0.00 823.00 290.00" width="823pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 286)">
<title>btrfs_reflink_backref</title>
<polygon fill="#ffffff" points="-4,4 -4,-286 819,-286 819,4 -4,4" stroke="transparent"></polygon>
<!-- root -->
<g class="node" id="node1">
<title>root</title>
<polygon fill="none" points="0,-90.5 0,-182.5 110,-182.5 110,-90.5 0,-90.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-167.3">ROOT_TREE</text>
<polyline fill="none" points="0,-159.5 110,-159.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-144.3">sn1</text>
<polyline fill="none" points="0,-136.5 110,-136.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-121.3">sn2</text>
<polyline fill="none" points="0,-113.5 110,-113.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-98.3">fs</text>
</g>
<!-- sn1 -->
<g class="node" id="node2">
<title>sn1</title>
<polygon fill="none" points="182,-154.5 182,-200.5 297,-200.5 297,-154.5 182,-154.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-185.3">FS_TREE sn1</text>
<polyline fill="none" points="182,-177.5 297,-177.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-162.3">leaf_node</text>
</g>
<!-- root->sn1 -->
<g class="edge" id="edge1">
<title>root:sn1->sn1:label</title>
<path d="M110,-148.5C143.0846,-148.5 145.1136,-181.5948 171.89,-188.3172" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="171.661,-191.8142 182,-189.5 172.4745,-184.8616 171.661,-191.8142" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2 -->
<g class="node" id="node3">
<title>sn2</title>
<polygon fill="none" points="182,-89.5 182,-135.5 297,-135.5 297,-89.5 182,-89.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-120.3">FS_TREE sn2</text>
<polyline fill="none" points="182,-112.5 297,-112.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-97.3">leaf_node</text>
</g>
<!-- root->sn2 -->
<g class="edge" id="edge2">
<title>root:sn2->sn2:label</title>
<path d="M110,-124.5C138.375,-124.5 147.8795,-124.5 171.9792,-124.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="172,-128.0001 182,-124.5 172,-121.0001 172,-128.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs -->
<g class="node" id="node4">
<title>fs</title>
<polygon fill="none" points="188.5,-24.5 188.5,-70.5 290.5,-70.5 290.5,-24.5 188.5,-24.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-55.3">FS_TREE fs</text>
<polyline fill="none" points="188.5,-47.5 290.5,-47.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-32.3">leaf_node</text>
</g>
<!-- root->fs -->
<g class="edge" id="edge3">
<title>root:fs->fs:label</title>
<path d="M110,-101.5C145.8591,-101.5 148.6975,-67.0059 178.3652,-60.5382" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="178.9088,-64.0009 188.5,-59.5 178.1954,-57.0374 178.9088,-64.0009" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- extent -->
<g class="node" id="node7">
<title>extent</title>
<polygon fill="none" points="599,-97.5 599,-281.5 815,-281.5 815,-97.5 599,-97.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-266.3">EXTENT_TREE extent_tree</text>
<polyline fill="none" points="599,-258.5 815,-258.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-243.3">root_tree : ref 1</text>
<polyline fill="none" points="599,-235.5 815,-235.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-220.3">sn1 fs_tree : ref 1</text>
<polyline fill="none" points="599,-212.5 815,-212.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-197.3">sn2 fs_tree : ref 1</text>
<polyline fill="none" points="599,-189.5 815,-189.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-174.3">sn1 sn2 leaf_node: ref 2</text>
<polyline fill="none" points="599,-166.5 815,-166.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-151.3">fs fs_tree : ref 1</text>
<polyline fill="none" points="599,-143.5 815,-143.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-128.3">fs leaf_node : ref 1</text>
<polyline fill="none" points="599,-120.5 815,-120.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="707" y="-105.3">file1 : ref 3</text>
</g>
<!-- root->extent -->
<g class="edge" id="edge7">
<title>root:label->extent:root</title>
<path d="M110,-171.5C146.3929,-171.5 147.3645,-199.3276 182,-210.5 355.7912,-266.56 410.8974,-248.22 588.778,-247.5202" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.007,-251.0198 599,-247.5 588.9931,-244.0198 589.007,-251.0198" stroke="#000000"></polygon>
</g>
<!-- snleaf -->
<g class="node" id="node5">
<title>snleaf</title>
<polygon fill="none" points="369,-104 369,-173 527,-173 527,-104 369,-104" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-157.8">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-150 527,-150 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-134.8">file1</text>
<polyline fill="none" points="369,-127 527,-127 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-111.8">file2</text>
</g>
<!-- sn1->snleaf -->
<g class="edge" id="edge4">
<title>sn1:leaf->snleaf:label</title>
<path d="M297,-165.5C325.4188,-165.5 334.8506,-162.3549 358.9677,-161.6424" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.0507,-165.1416 369,-161.5 358.9513,-158.1423 359.0507,-165.1416" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn1->extent -->
<g class="edge" id="edge8">
<title>sn1:label->extent:sn1</title>
<path d="M297,-189.5C331.0225,-189.5 335.608,-208.9802 369,-215.5 465.9727,-234.434 494.7483,-225.1896 588.7672,-224.5355" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.0122,-228.0348 599,-224.5 588.9879,-221.0348 589.0122,-228.0348" stroke="#000000"></polygon>
</g>
<!-- sn2->snleaf -->
<g class="edge" id="edge5">
<title>sn2:leaf->snleaf:label</title>
<path d="M297,-100.5C335.1725,-100.5 328.7599,-151.0314 358.921,-160.1113" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="358.6158,-163.6023 369,-161.5 359.5713,-156.6678 358.6158,-163.6023" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2->extent -->
<g class="edge" id="edge9">
<title>sn2:label->extent:sn2</title>
<path d="M297,-124.5C338.0913,-124.5 330.8441,-167.2481 369,-182.5 460.9878,-219.27 494.1557,-202.7369 588.7239,-201.5638" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.022,-205.0621 599,-201.5 588.9784,-198.0622 589.022,-205.0621" stroke="#000000"></polygon>
</g>
<!-- fsleaf -->
<g class="node" id="node6">
<title>fsleaf</title>
<polygon fill="none" points="369,-.5 369,-46.5 527,-46.5 527,-.5 369,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-31.3">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-23.5 527,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-8.3">file1</text>
</g>
<!-- fs->fsleaf -->
<g class="edge" id="edge6">
<title>fs:leaf->fsleaf:label</title>
<path d="M290.5,-35.5C321.7092,-35.5 331.9802,-35.5 358.7727,-35.5" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359,-39.0001 369,-35.5 359,-32.0001 359,-39.0001" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs->extent -->
<g class="edge" id="edge11">
<title>fs:label->extent:fs</title>
<path d="M290.5,-59.5C396.7559,-59.5 430.015,-51.0924 527,-94.5 561.6044,-109.9879 558.3556,-146.986 588.8984,-153.5091" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="588.7061,-157.007 599,-154.5 589.3895,-150.0404 588.7061,-157.007" stroke="#000000"></polygon>
</g>
<!-- snleaf->extent -->
<g class="edge" id="edge10">
<title>snleaf:label->extent:snleaf</title>
<path d="M527,-161.5C556.0672,-161.5 564.4226,-174.0803 588.7972,-176.9306" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="588.8205,-180.4372 599,-177.5 589.2106,-173.4481 588.8205,-180.4372" stroke="#000000"></polygon>
</g>
<!-- snleaf->extent -->
<g class="edge" id="edge13">
<title>snleaf:f1->extent:f1</title>
<path d="M527,-138.5C558.0104,-138.5 563.4149,-114.4944 589.024,-109.4307" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.3684,-112.9139 599,-108.5 588.7181,-105.9442 589.3684,-112.9139" stroke="#000000"></polygon>
</g>
<!-- snleaf->extent -->
<g class="edge" id="edge14">
<title>snleaf:f2->extent:f1</title>
<path d="M527,-115.5C555.5088,-115.5 564.7912,-109.9961 588.944,-108.7491" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.0898,-112.2467 599,-108.5 588.9163,-105.2488 589.0898,-112.2467" stroke="#000000"></polygon>
</g>
<!-- fsleaf->extent -->
<g class="edge" id="edge12">
<title>fsleaf:label->extent:fsleaf</title>
<path d="M527,-35.5C576.5833,-35.5 550.1066,-118.4746 588.994,-130.1429" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="588.6203,-133.6242 599,-131.5 589.5611,-126.6877 588.6203,-133.6242" stroke="#000000"></polygon>
</g>
<!-- fsleaf->extent -->
<g class="edge" id="edge15">
<title>fsleaf:f1->extent:f1</title>
<path d="M527,-11.5C576.9145,-11.5 549.8453,-95.3389 588.934,-107.1288" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="588.6191,-110.6181 599,-108.5 589.564,-103.6822 588.6191,-110.6181" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>上图简化了一些细节,实际上每个文件可以引用多个区块(文件碎片),
其中每个对区块的引用都可以指明引用到具体某个区块记录的某个地址偏移和长度,
也就是说文件引用的区块可以不是区块记录中的一整个区块,而是一部分内容。</p>
<p>图中可见,整个文件系统中共有5个文件路径可以访问到同一个文件的内容,分别是
<code class="code">
sn1/file1, sn1/file2, sn2/file1, sn2/file2, fs/file1</code>
,
在 extent_tree 中, sn1 和 sn2 可能共享了一个 B树 叶子节点,这个叶子节点的引用计数为 2
,然后每个文件的内容都指向同一个 extent ,这个 extent 的引用计数为 3 。</p>
<p>删除子卷时,通过引用计数就能准确地释放掉子卷所引用的区块。具体算法挺符合直觉的:</p>
<ol class="arabic simple">
<li>从子卷的 FS_TREE 往下遍历<ul>
<li>遇到引用计数 >1 的区块,减小该块的计数即可,不需要再递归下去</li>
<li>遇到引用计数 =1 的区块,就是子卷独占的区块,需要释放该块并递归往下继续扫描</li>
</ul>
</li>
</ol>
<p>大体思路挺像上面介绍的 <a class="reference external" href="🐢乌龟算法:概念上ZFS如何删快照">ZFS 快照删除的🐢乌龟算法</a>
,只不过根据引用计数而非 birth txg 判断是否独占数据块。性能上说, btrfs
的B树本身内容就比较紧凑,FS_TREE 一个结构就容纳了文件 inode 和引用的区块信息,
EXTENT_TREE 按地址排序也比较紧凑,所以删除算法的随机读写不像 ZFS 的🐢乌龟算法那么严重,
实际实现代码里面也可能通过 btrfs generation 做一些类似基于 birth txg 优化的快速代码路径。
即便如此,扫描 FS_TREE 仍然可能需要耗时良久,这个递归的每一步操作都会记录在 ROOT_TREE
中专门的结构,也就是说删除一个子卷的操作可以执行很长时间并跨越多个 pool commit 。
<code class="code">
btrfs subvolume delete</code>
命令默认也只是记录下这个删除操作,然后就返回一句类似:
<code class="code">
Delete subvolume (no-commit): /subvolume/path</code>
的输出,不会等删除操作执行结束。
相比之下 ZFS 那边删除一个快照或文件系统必须在一个 txg 内执行完,没有中间过程的记录,
所以如果耗时很久会影响整个 pool 的写入,于是 ZFS 那边必须对这些操作优化到能在一个 txg
内执行完的程度(摧毁克隆方面
<a class="reference external" href="https://www.delphix.com/blog/delphix-engineering/performance-zfs-destroy">ZFS 还有 async_destroy 优化</a>
可能有些帮助)。</p>
<p>只需要引用计数就足够完成快照的创建、删除之类的功能,也能支持 reflink 了(仔细回想,
reflink 其实就是 reference counted link 嘛),普通读写下也只需要引用计数。
但是只有引用计数不足以知道区块的归属,不能用引用计数统计每个子卷分别占用多少空间,
独占多少区块而又共享多少区块。上面的例子就可以看出,所有文件都指向同一个区块,该区块的引用计数为
3 ,而文件系统中一共有 5 个路径能访问到该文件。可见从区块根据引用计数反推子卷归属信息不是那么一目了然的。</p>
</div>
<div class="section" id="back-reference">
<h4><a class="toc-backref" href="#id60">反向引用(back reference)</a></h4>
<p>单纯从区块的引用计数难以看出整个文件系统所有子卷中有多少副本。
也就是说单有引用计数的一个数字还不够,需要记录具体反向的从区块往引用源头指的引用,这种结构在
btrfs 中叫做「反向引用(back reference,简称 backref)」。所以在上图中每一个指向 EXTENT_TREE
的单向箭头,在 btrfs 中都有记录一条反向引用,通过反向引用记录能反过来从被指针指向的位置找回到记录指针的地方。</p>
<p>反向引用(backref)是 btrfs 中非常关键的机制,在 btrfs kernel wiki 专门有一篇页面
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/Resolving_Extent_Backrefs">Resolving Extent Backrefs</a>
解释它的原理和实现方式。</p>
<p>对上面的引用计数的例子画出反向引用的指针大概是这样:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: btrfs_reflink_backref Pages: 1 -->
<svg class="svg-responsive" height="546pt" viewbox="0.00 0.00 890.00 546.46" width="890pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 542.4604)">
<title>btrfs_reflink_backref</title>
<polygon fill="#ffffff" points="-4,4 -4,-542.4604 886,-542.4604 886,4 -4,4" stroke="transparent"></polygon>
<!-- root -->
<g class="node" id="node1">
<title>root</title>
<polygon fill="none" points="0,-299 0,-391 110,-391 110,-299 0,-299" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-375.8">ROOT_TREE</text>
<polyline fill="none" points="0,-368 110,-368 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-352.8">sn1</text>
<polyline fill="none" points="0,-345 110,-345 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-329.8">sn2</text>
<polyline fill="none" points="0,-322 110,-322 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-306.8">fs</text>
</g>
<!-- sn1 -->
<g class="node" id="node2">
<title>sn1</title>
<polygon fill="none" points="182,-343 182,-389 297,-389 297,-343 182,-343" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-373.8">FS_TREE sn1</text>
<polyline fill="none" points="182,-366 297,-366 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-350.8">leaf_node</text>
</g>
<!-- root->sn1 -->
<g class="edge" id="edge1">
<title>root:sn1->sn1:label</title>
<path d="M110,-357C139.6875,-357 147.165,-373.6575 171.9941,-377.3013" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="171.7804,-380.7948 182,-378 172.2681,-373.8118 171.7804,-380.7948" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2 -->
<g class="node" id="node3">
<title>sn2</title>
<polygon fill="none" points="182,-278 182,-324 297,-324 297,-278 182,-278" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-308.8">FS_TREE sn2</text>
<polyline fill="none" points="182,-301 297,-301 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-285.8">leaf_node</text>
</g>
<!-- root->sn2 -->
<g class="edge" id="edge2">
<title>root:sn2->sn2:label</title>
<path d="M110,-333C139.5791,-333 147.2379,-317.1357 172.0219,-313.6654" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="172.2551,-317.1578 182,-313 171.7892,-310.1733 172.2551,-317.1578" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs -->
<g class="node" id="node4">
<title>fs</title>
<polygon fill="none" points="188.5,-213 188.5,-259 290.5,-259 290.5,-213 188.5,-213" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-243.8">FS_TREE fs</text>
<polyline fill="none" points="188.5,-236 290.5,-236 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-220.8">leaf_node</text>
</g>
<!-- root->fs -->
<g class="edge" id="edge3">
<title>root:fs->fs:label</title>
<path d="M110,-310C150.6377,-310 145.4268,-258.1984 178.1842,-249.295" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="179.0138,-252.7184 188.5,-248 178.1419,-245.7729 179.0138,-252.7184" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- extent -->
<g class="node" id="node7">
<title>extent</title>
<polygon fill="none" points="599,-.5 599,-529.5 882,-529.5 882,-.5 599,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-514.3">EXTENT_TREE extent_tree</text>
<polyline fill="none" points="599,-506.5 882,-506.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-491.3">root_tree : ref 1</text>
<polyline fill="none" points="599,-483.5 882,-483.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-468.3"> </text>
<polyline fill="none" points="599,-460.5 882,-460.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-445.3">sn1 fs_tree : ref 1</text>
<polyline fill="none" points="599,-437.5 882,-437.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-422.3">backref ROOT_TREE sn1</text>
<polyline fill="none" points="599,-414.5 882,-414.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-399.3"> </text>
<polyline fill="none" points="599,-391.5 882,-391.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-376.3">sn2 fs_tree : ref 1</text>
<polyline fill="none" points="599,-368.5 882,-368.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-353.3">backref ROOT_TREE sn2</text>
<polyline fill="none" points="599,-345.5 882,-345.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-330.3"> </text>
<polyline fill="none" points="599,-322.5 882,-322.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-307.3">sn1 sn2 leaf_node: ref 2</text>
<polyline fill="none" points="599,-299.5 882,-299.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-284.3">backref sn1 FS_TREE node</text>
<polyline fill="none" points="599,-276.5 882,-276.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-261.3">backref sn2 FS_TREE node</text>
<polyline fill="none" points="599,-253.5 882,-253.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-238.3"> </text>
<polyline fill="none" points="599,-230.5 882,-230.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-215.3">fs fs_tree : ref 1</text>
<polyline fill="none" points="599,-207.5 882,-207.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-192.3">backref ROOT_TREE fs</text>
<polyline fill="none" points="599,-184.5 882,-184.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-169.3"> </text>
<polyline fill="none" points="599,-161.5 882,-161.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-146.3">fs leaf_node : ref 1</text>
<polyline fill="none" points="599,-138.5 882,-138.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-123.3">backref fs FS_TREE node</text>
<polyline fill="none" points="599,-115.5 882,-115.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-100.3"> </text>
<polyline fill="none" points="599,-92.5 882,-92.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-77.3">file1 : ref 3</text>
<polyline fill="none" points="599,-69.5 882,-69.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-54.3">backref sn1 FS_TREE leaf_node file1</text>
<polyline fill="none" points="599,-46.5 882,-46.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-31.3">backref sn1 FS_TREE leaf_node file2</text>
<polyline fill="none" points="599,-23.5 882,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-8.3">backref fs FS_TREE leaf_node file1</text>
</g>
<!-- root->extent -->
<!-- snleaf -->
<g class="node" id="node5">
<title>snleaf</title>
<polygon fill="none" points="369,-92.5 369,-161.5 527,-161.5 527,-92.5 369,-92.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-146.3">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-138.5 527,-138.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-123.3">file1</text>
<polyline fill="none" points="369,-115.5 527,-115.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-100.3">file2</text>
</g>
<!-- sn1->snleaf -->
<g class="edge" id="edge4">
<title>sn1:leaf->snleaf:label</title>
<path d="M297,-354C389.3923,-354 281.9198,-165.6262 358.6977,-150.9095" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.3466,-154.3659 369,-150 358.7309,-147.3931 359.3466,-154.3659" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn1->extent -->
<!-- sn2->snleaf -->
<g class="edge" id="edge5">
<title>sn2:leaf->snleaf:label</title>
<path d="M297,-289C362.7688,-289 306.3617,-164.7874 358.7675,-151.2017" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.4765,-154.6425 369,-150 358.66,-147.6903 359.4765,-154.6425" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2->extent -->
<!-- fsleaf -->
<g class="node" id="node6">
<title>fsleaf</title>
<polygon fill="none" points="369,-27 369,-73 527,-73 527,-27 369,-27" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-57.8">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-50 527,-50 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-34.8">file1</text>
</g>
<!-- fs->fsleaf -->
<g class="edge" id="edge6">
<title>fs:leaf->fsleaf:label</title>
<path d="M290.5,-224C366.7574,-224 296.2794,-76.8315 358.7804,-63.0345" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.4034,-66.4894 369,-62 358.6983,-59.525 359.4034,-66.4894" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs->extent -->
<!-- snleaf->extent -->
<!-- snleaf->extent -->
<!-- snleaf->extent -->
<!-- fsleaf->extent -->
<!-- fsleaf->extent -->
<!-- extent->root -->
<g class="edge" id="edge7">
<title>extent:br1->root:label</title>
<path d="M599,-426C561.4977,-426 563.1005,-459.8426 527,-470 379.398,-511.5302 320.1998,-536.4223 182,-470 139.2574,-449.4568 157.8497,-389.8142 120.2,-381.0742" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="120.3116,-377.5667 110,-380 119.5784,-384.5282 120.3116,-377.5667" stroke="#000000"></polygon>
</g>
<!-- extent->root -->
<g class="edge" id="edge8">
<title>extent:br2->root:label</title>
<path d="M599,-357C554.3693,-357 568.1678,-409.7629 527,-427 470.5865,-450.6205 230.539,-447.6953 182,-432 147.9627,-420.9939 148.904,-387.2293 120.0001,-380.9952" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="120.2975,-377.5076 110,-380 119.6042,-384.4732 120.2975,-377.5076" stroke="#000000"></polygon>
</g>
<!-- extent->root -->
<g class="edge" id="edge11">
<title>extent:br5->root:label</title>
<path d="M599,-196C437.2729,-196 451.6981,-351.8392 297,-399 248.1103,-413.9044 232.6864,-405.5752 182,-399 152.8975,-395.2247 144.817,-383.2055 120.2855,-380.5309" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="120.1671,-377.0202 110,-380 119.8063,-384.0109 120.1671,-377.0202" stroke="#000000"></polygon>
</g>
<!-- extent->sn1 -->
<g class="edge" id="edge9">
<title>extent:br3->sn1:label</title>
<path d="M599,-288C532.2547,-288 382.0082,-369.7603 307.4346,-377.4253" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="306.7924,-373.9553 297,-378 307.1774,-380.9447 306.7924,-373.9553" stroke="#000000"></polygon>
</g>
<!-- extent->sn2 -->
<g class="edge" id="edge10">
<title>extent:br4->sn2:label</title>
<path d="M599,-265C466.5438,-265 434.5081,-310.5934 307.1513,-312.9087" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="306.9681,-309.4101 297,-313 307.0311,-316.4099 306.9681,-309.4101" stroke="#000000"></polygon>
</g>
<!-- extent->fs -->
<g class="edge" id="edge12">
<title>extent:br6->fs:label</title>
<path d="M599,-127C561.4977,-127 561.4368,-156.1499 527,-171 460.1724,-199.8178 437.6774,-189.9207 369,-214 336.6245,-225.3513 329.7175,-243.9007 300.743,-247.4153" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="300.2843,-243.9357 290.5,-248 300.6832,-250.9244 300.2843,-243.9357" stroke="#000000"></polygon>
</g>
<!-- extent->snleaf -->
<g class="edge" id="edge13">
<title>extent:br7->snleaf:label</title>
<path d="M599,-58C550.9315,-58 574.6566,-136.8505 537.245,-148.5549" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="536.4131,-145.1375 527,-150 537.3909,-152.0689 536.4131,-145.1375" stroke="#000000"></polygon>
</g>
<!-- extent->snleaf -->
<g class="edge" id="edge14">
<title>extent:br8->snleaf:label</title>
<path d="M599,-35C542.4668,-35 581.652,-136.0742 537.0836,-148.7085" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="536.4743,-145.2579 527,-150 537.3637,-152.2011 536.4743,-145.2579" stroke="#000000"></polygon>
</g>
<!-- extent->fsleaf -->
<g class="edge" id="edge15">
<title>extent:br9->fsleaf:label</title>
<path d="M599,-12C563.8453,-12 565.2313,-52.7112 537.2135,-60.6626" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="536.4609,-57.2312 527,-62 537.3698,-64.1719 536.4609,-57.2312" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>EXTENT_TREE 中每个 extent 记录都同时记录了引用到这个区块的反向引用列表。反向引用有两种记录方式:</p>
<ol class="arabic simple">
<li>普通反向引用(Normal back references)。记录这个指针来源所在是哪颗B树、 B树中的对象 id 和对象偏移。<ul>
<li>对文件区块而言,就是记录文件所在子卷、inode、和文件内容的偏移。</li>
<li>对子卷的树节点区块而言,就是记录该区块的上级树节点在哪个B树的哪个位置开始。</li>
</ul>
</li>
<li>共享反向引用(Shared back references)。记录这个指针来源区块的逻辑地址。<ul>
<li>无论对文件区块而言,还是对子卷的树节点区块而言,都是直接记录了保存这个区块指针的上层树节点的逻辑地址。</li>
</ul>
</li>
</ol>
<p>有两种记录方式是因为它们各有性能上的优缺点:</p>
<table border="0" class="docutils table field-list" frame="void" rules="none">
<col class="field-name"/>
<col class="field-body"/>
<tbody valign="top">
<tr class="field"><th class="field-name">普通反向引用:</th><td class="field-body">因为通过对象编号记录,所以当树节点 CoW 改变了地址时不需要调整地址,
从而在普通的读写和快照之类的操作下有更好的性能,
但是在解析反向引用时需要额外一次树查找。
同时因为这个额外查找,普通反向引用也叫间接反向引用。</td>
</tr>
<tr class="field"><th class="field-name">共享反向引用:</th><td class="field-body">因为直接记录了逻辑地址,所以当这个地址的节点被 CoW 的时候也需要调整这里记录的地址。
在普通的读写和快照操作下,调整地址会增加写入从而影响性能,但是在解析反向引用时更快。</td>
</tr>
</tbody>
</table>
<p>通常通过普通写入、快照、 reflink 等方式创建出来的引用是普通反向引用,
由于普通反向引用记录了包含它的B树,从而可以说绑在了某棵树比如某个子卷上,
当这个普通反向引用指向的对象不再存在,而这个反向引用还在通过别的途径共享时,
这个普通反向引用会转换共享反向引用;共享反向引用在存在期间不会变回普通反向引用。</p>
<p>比如上图反向引用的例子中,我们先假设所有画出的反向引用都是普通反向引用,于是图中标为 file1
引用数为 3 的那个区块有 3 条反向引用记录,其中前两条都指向 sn1 里面的文件,分别是 sn1/file1
和 sn1/file2 ,然后 sn1 和 sn2 共享了 FS_TREE 的叶子节点。</p>
<p>假设这时我们删除 sn1/file2,执行了代码 <code class="code">
rm sn1/file2</code>
之后:</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: btrfs_reflink_shared_backref Pages: 1 -->
<svg class="svg-responsive" height="614pt" viewbox="0.00 0.00 890.00 613.50" width="890pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 609.5)">
<title>btrfs_reflink_shared_backref</title>
<polygon fill="#ffffff" points="-4,4 -4,-609.5 886,-609.5 886,4 -4,4" stroke="transparent"></polygon>
<!-- root -->
<g class="node" id="node1">
<title>root</title>
<polygon fill="none" points="0,-375.5 0,-467.5 110,-467.5 110,-375.5 0,-375.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-452.3">ROOT_TREE</text>
<polyline fill="none" points="0,-444.5 110,-444.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-429.3">sn1</text>
<polyline fill="none" points="0,-421.5 110,-421.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-406.3">sn2</text>
<polyline fill="none" points="0,-398.5 110,-398.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="55" y="-383.3">fs</text>
</g>
<!-- sn1 -->
<g class="node" id="node2">
<title>sn1</title>
<polygon fill="none" points="182,-419.5 182,-465.5 297,-465.5 297,-419.5 182,-419.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-450.3">FS_TREE sn1</text>
<polyline fill="none" points="182,-442.5 297,-442.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-427.3">leaf_node</text>
</g>
<!-- root->sn1 -->
<g class="edge" id="edge1">
<title>root:sn1->sn1:label</title>
<path d="M110,-433.5C139.6875,-433.5 147.165,-450.1575 171.9941,-453.8013" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="171.7804,-457.2948 182,-454.5 172.2681,-450.3118 171.7804,-457.2948" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2 -->
<g class="node" id="node3">
<title>sn2</title>
<polygon fill="none" points="182,-354.5 182,-400.5 297,-400.5 297,-354.5 182,-354.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-385.3">FS_TREE sn2</text>
<polyline fill="none" points="182,-377.5 297,-377.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-362.3">leaf_node</text>
</g>
<!-- root->sn2 -->
<g class="edge" id="edge2">
<title>root:sn2->sn2:label</title>
<path d="M110,-409.5C139.5791,-409.5 147.2379,-393.6357 172.0219,-390.1654" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="172.2551,-393.6578 182,-389.5 171.7892,-386.6733 172.2551,-393.6578" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs -->
<g class="node" id="node4">
<title>fs</title>
<polygon fill="none" points="188.5,-250.5 188.5,-296.5 290.5,-296.5 290.5,-250.5 188.5,-250.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-281.3">FS_TREE fs</text>
<polyline fill="none" points="188.5,-273.5 290.5,-273.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="239.5" y="-258.3">leaf_node</text>
</g>
<!-- root->fs -->
<g class="edge" id="edge3">
<title>root:fs->fs:label</title>
<path d="M110,-386.5C163.0775,-386.5 135.9169,-298.4687 178.3378,-286.777" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="179.0145,-290.2196 188.5,-285.5 178.1416,-283.2742 179.0145,-290.2196" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- extent -->
<g class="node" id="node8">
<title>extent</title>
<polygon fill="none" points="599,-30 599,-605 882,-605 882,-30 599,-30" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-589.8">EXTENT_TREE extent_tree</text>
<polyline fill="none" points="599,-582 882,-582 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-566.8">root_tree : ref 1</text>
<polyline fill="none" points="599,-559 882,-559 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-543.8"> </text>
<polyline fill="none" points="599,-536 882,-536 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-520.8">sn1 fs_tree : ref 1</text>
<polyline fill="none" points="599,-513 882,-513 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-497.8">backref ROOT_TREE sn1</text>
<polyline fill="none" points="599,-490 882,-490 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-474.8"> </text>
<polyline fill="none" points="599,-467 882,-467 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-451.8">sn2 fs_tree : ref 1</text>
<polyline fill="none" points="599,-444 882,-444 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-428.8">backref ROOT_TREE sn2</text>
<polyline fill="none" points="599,-421 882,-421 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-405.8"> </text>
<polyline fill="none" points="599,-398 882,-398 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-382.8">sn1 sn2 leaf_node: ref 2</text>
<polyline fill="none" points="599,-375 882,-375 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-359.8">backref sn1 FS_TREE node</text>
<polyline fill="none" points="599,-352 882,-352 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-336.8">backref sn2 FS_TREE node</text>
<polyline fill="none" points="599,-329 882,-329 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-313.8"> </text>
<polyline fill="none" points="599,-306 882,-306 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-290.8">fs fs_tree : ref 1</text>
<polyline fill="none" points="599,-283 882,-283 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-267.8">backref ROOT_TREE fs</text>
<polyline fill="none" points="599,-260 882,-260 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-244.8"> </text>
<polyline fill="none" points="599,-237 882,-237 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-221.8">fs leaf_node : ref 1</text>
<polyline fill="none" points="599,-214 882,-214 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-198.8">backref fs FS_TREE node</text>
<polyline fill="none" points="599,-191 882,-191 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-175.8"> </text>
<polyline fill="none" points="599,-168 882,-168 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-152.8">file1 : ref 4</text>
<polyline fill="none" points="599,-145 882,-145 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-129.8">backref FS_TREE leaf_node file1</text>
<polyline fill="none" points="599,-122 882,-122 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-106.8">backref FS_TREE leaf_node file2</text>
<polyline fill="none" points="599,-99 882,-99 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-83.8">backref fs FS_TREE leaf_node file1</text>
<polyline fill="none" points="599,-76 882,-76 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-60.8">backref sn1 FS_TREE leaf_node file1</text>
<polyline fill="none" points="599,-53 882,-53 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="740.5" y="-37.8"> </text>
</g>
<!-- root->extent -->
<!-- sn1leaf -->
<g class="node" id="node5">
<title>sn1leaf</title>
<polygon fill="none" points="369,-.5 369,-46.5 527,-46.5 527,-.5 369,-.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-31.3">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-23.5 527,-23.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-8.3">file1</text>
</g>
<!-- sn1->sn1leaf -->
<g class="edge" id="edge4">
<title>sn1:leaf->sn1leaf:label</title>
<path d="M297,-430.5C471.7886,-430.5 202.0422,-51.535 358.5979,-35.9916" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.1764,-39.4682 369,-35.5 358.8459,-32.476 359.1764,-39.4682" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn1->extent -->
<!-- snleaf -->
<g class="node" id="node6">
<title>snleaf</title>
<polygon fill="none" points="369,-131 369,-200 527,-200 527,-131 369,-131" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-184.8">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-177 527,-177 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-161.8">file1</text>
<polyline fill="none" points="369,-154 527,-154 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-138.8">file2</text>
</g>
<!-- sn2->snleaf -->
<g class="edge" id="edge5">
<title>sn2:leaf->snleaf:label</title>
<path d="M297,-365.5C378.2769,-365.5 292.1456,-203.3841 359.0359,-189.4523" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.3784,-192.9356 369,-188.5 358.7123,-185.9674 359.3784,-192.9356" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- sn2->extent -->
<!-- fsleaf -->
<g class="node" id="node7">
<title>fsleaf</title>
<polygon fill="none" points="369,-65.5 369,-111.5 527,-111.5 527,-65.5 369,-65.5" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-96.3">FS_TREE leaf_node</text>
<polyline fill="none" points="369,-88.5 527,-88.5 " stroke="#000000"></polyline>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="448" y="-73.3">file1</text>
</g>
<!-- fs->fsleaf -->
<g class="edge" id="edge6">
<title>fs:leaf->fsleaf:label</title>
<path d="M290.5,-261.5C366.3764,-261.5 296.6069,-115.24 358.8289,-101.5281" fill="none" stroke="#000000" stroke-width="2"></path>
<polygon fill="#000000" points="359.4027,-104.988 369,-100.5 358.6987,-98.0235 359.4027,-104.988" stroke="#000000" stroke-width="2"></polygon>
</g>
<!-- fs->extent -->
<!-- snleaf->extent -->
<!-- snleaf->extent -->
<!-- snleaf->extent -->
<!-- fsleaf->extent -->
<!-- fsleaf->extent -->
<!-- extent->root -->
<g class="edge" id="edge7">
<title>extent:br1->root:label</title>
<path d="M599,-501.5C464.7417,-501.5 431.2493,-506.9443 297,-508.5 245.8923,-509.0922 230.6319,-524.2253 182,-508.5 147.9627,-497.4939 148.904,-463.7293 120.0001,-457.4952" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="120.2975,-454.0076 110,-456.5 119.6042,-460.9732 120.2975,-454.0076" stroke="#000000"></polygon>
</g>
<!-- extent->root -->
<g class="edge" id="edge8">
<title>extent:br2->root:label</title>
<path d="M599,-432.5C412.6839,-432.5 366.7679,-499.4688 182,-475.5 152.8975,-471.7247 144.817,-459.7055 120.2855,-457.0309" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="120.1671,-453.5202 110,-456.5 119.8063,-460.5109 120.1671,-453.5202" stroke="#000000"></polygon>
</g>
<!-- extent->root -->
<g class="edge" id="edge11">
<title>extent:br5->root:label</title>
<path d="M599,-271.5C551.9428,-271.5 221.2192,-319.4954 182,-345.5 136.0546,-375.9645 164.6578,-446.8667 119.9606,-455.6104" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="119.649,-452.1242 110,-456.5 120.2717,-459.0965 119.649,-452.1242" stroke="#000000"></polygon>
</g>
<!-- extent->sn1 -->
<g class="edge" id="edge9">
<title>extent:br3->sn1:label</title>
<path d="M599,-363.5C547.0832,-363.5 416.9848,-384.6803 369,-404.5 336.5079,-417.9206 335.7513,-447.8324 307.2897,-453.5466" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="306.6344,-450.0922 297,-454.5 307.2803,-457.0624 306.6344,-450.0922" stroke="#000000"></polygon>
</g>
<!-- extent->sn2 -->
<g class="edge" id="edge10">
<title>extent:br4->sn2:label</title>
<path d="M599,-340.5C496.1267,-340.5 469.5943,-344.9663 369,-366.5 339.744,-372.7627 332.1354,-386.1139 307.089,-388.9595" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="306.7984,-385.47 297,-389.5 307.1729,-392.4599 306.7984,-385.47" stroke="#000000"></polygon>
</g>
<!-- extent->fs -->
<g class="edge" id="edge12">
<title>extent:br6->fs:label</title>
<path d="M599,-202.5C546.6951,-202.5 418.875,-236.743 369,-252.5 336.4361,-262.7879 329.5387,-281.3788 300.696,-284.9117" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="300.2817,-281.4297 290.5,-285.5 300.685,-288.418 300.2817,-281.4297" stroke="#000000"></polygon>
</g>
<!-- extent->sn1leaf -->
<g class="edge" id="edge16">
<title>extent:br10->sn1leaf:label</title>
<path d="M599,-64.5C568.2751,-64.5 562.532,-41.4968 537.2718,-36.4649" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="537.2835,-32.9507 527,-35.5 536.6288,-39.92 537.2835,-32.9507" stroke="#000000"></polygon>
</g>
<!-- extent->snleaf -->
<g class="edge" id="edge13">
<title>extent:br7->snleaf:label</title>
<path d="M599,-133.5C562.5069,-133.5 566.0966,-178.6709 537.119,-187.1404" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="#000000" points="536.4449,-183.6995 527,-188.5 537.377,-190.6371 536.4449,-183.6995" stroke="#000000"></polygon>
</g>
<!-- extent->snleaf -->
<g class="edge" id="edge14">
<title>extent:br8->snleaf:label</title>
<path d="M599,-110.5C555.6919,-110.5 570.9782,-176.228 537.2833,-187.0115" fill="none" stroke="#000000" stroke-dasharray="5,2"></path>
<polygon fill="#000000" points="536.3954,-183.6035 527,-188.5 537.3983,-190.5313 536.3954,-183.6035" stroke="#000000"></polygon>
</g>
<!-- extent->fsleaf -->
<g class="edge" id="edge15">
<title>extent:br9->fsleaf:label</title>
<path d="M599,-87.5C570.1662,-87.5 561.4234,-97.7215 537.1414,-100.0373" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="536.8301,-96.5478 527,-100.5 537.1492,-103.5405 536.8301,-96.5478" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>那么 sn1 会 CoW 那个和 sn2 共享的叶子节点,有了新的属于 sn1 的叶子,从而断开了原本 file1
中对这个共享叶子节点的两个普通反向引用,转化成共享反向引用(图中用虚线箭头描述),
并且插入了一个新的普通反向引用指向新的 sn1 的叶子节点。</p>
</div>
<div class="section" id="backref-walking">
<h4><a class="toc-backref" href="#id61">遍历反向引用(backref walking)</a></h4>
<p>有了反向引用记录之后,可以给定一个逻辑地址,从 EXTENT_TREE 中找到地址的区块记录,
然后从区块记录中的反向引用记录一步步往回遍历,直到遇到 ROOT_TREE
,最终确定这个逻辑地址的区块在整个文件系统中有多少路径能访问它。
这个遍历反向引用的操作,在 btrfs 文档和代码中被称作 backref walking 。</p>
<p>比如还是上面的反向引用图例中 sn1 和 sn2 完全共享叶子节点的那个例子,通过 backref walking
,我们能从 file1 所记录的 3 个反向引用,推出全部 5 个可能的访问路径。</p>
<p>backref walking 作为很多功能的基础设施,从 btrfs 相当早期(3.3内核)就有,很多 btrfs
的功能实际依赖 backref walking 的正确性。列举一些需要 backref walking 来实现的功能:</p>
<ol class="arabic">
<li><p class="first">qgroup</p>
<p>btrfs 的子卷没有记录子卷的磁盘占用开销,靠引用计数来删除子卷,
所以也不需要详细统计子卷的空间占用情况。不过对一些用户的使用场景,可能需要统计子卷空间占用。由于
可能存在的共享元数据和数据,子卷占用不能靠累计加减法的方式算出来,所以 btrfs 有了
qgroup 和 quota 功能,用来统计子卷或者别的管理粒度下的占用空间情况。为了实现 qgroup
,需要 backref walking 来计算区块共享的情况。</p>
</li>
<li><p class="first">send</p>
<p>btrfs send 在计算子卷间的差异时,也通过 backref walking 寻找能靠 reflink
共享的区块,从而避免传输数据。</p>
</li>
<li><p class="first">balance/scrub</p>
<p>balance 和 scrub 都会调整区块的地址,通过 backref walking
能找到所有引用到这个地址的位置并正确修改地址。</p>
</li>
<li><p class="first">check</p>
<p>当需要打印诊断信息的时候,除了提供出错的数据所在具体地址之外,通过 backref walking
也能提供受影响的文件路径之类的信息。</p>
</li>
</ol>
<div class="panel panel-default">
<div class="panel-heading">
btrfs 的 reflink-aware defrag</div>
<div class="panel-body">
这里想提一下 btrfs 一直计划中,但是还没有成功实现的 reflink-aware defrag
。文件碎片一直是 CoW 文件系统的大问题,对 btrfs 和对 ZFS 都是同样。ZFS 完全不支持碎片整理,
而 btrfs 目前只提供了文件级别的碎片整理,这会切断现有的 reflink 。计划中的
reflink-aware defrag 也是基于 backref walking ,根据区块引用的碎片程度,整理碎片而某种程度上保持
reflink 。btrfs 曾经实现了这个,但是因为 bug 太多不久就取消了相关功能,目前这个工作处于停滞阶段。</div>
</div>
<p>可见 backref walking 的能力对 btrfs 的许多功能都非常重要(不像 ZPL 的
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSPathLookupTrick">dnode 中记录的 parent dnode 那样只用于诊断信息</a>
)。不过 backref walking
根据区块共享的情况的不同,也可能导致挺大的运行期开销,包括算法时间上的和内存占用方面的开销。
比如某个子卷中有 100 个文件通过 reflink 共享了同一个区块,然后对这个子卷做了 100 个快照,
那么对这一个共享区块的 backref walking 结果可能解析出 10000 个路径。可见随着使用 reflink
和快照, backref walking 的开销可能爆炸式增长。最近 btrfs 邮件列表也有一些用户汇报,在大量子卷
和通过 reflink 做过 dedup 的 btrfs 文件系统上 send 快照时,可能导致内核分配大量内存甚至
panic 的情形,在 5.5 内核中 btrfs send 试图控制 send 时 clone reference
的数量上限来缓解这种边角问题。</p>
<p>值得再强调的是,在没有开启 qgroup 的前提下,正常创建删除快照或 reflink
,正常写入和覆盖区块之类的文件系统操作,只需要引用计数就足够,虽然可能需要调整反向引用记录(
尤其是共享反向引用的地址),但是不需要动用 backref walking 这样的重型武器。</p>
</div>
</div>
</div>
<div class="section" id="zfs-vs-btrfs-dedup">
<h2><a class="toc-backref" href="#id62">4 ZFS vs btrfs 的 dedup 功能现状</a></h2>
<p>上面讨论 ZFS 的快照和克隆如何跟踪数据块时,故意避开了 ZFS 的 dedup 功能,因为要讲 dedup
可能需要先理解引用计数在文件系统中的作用,而 btrfs 正好用了引用计数。
于是我们再回来 ZFS 这边,看看 ZFS 的 dedup 是具体如何运作的。</p>
<p>稍微了解过 btrfs 和 ZFS 两者的人,或许有不少 btrfs 用户都眼馋 ZFS 有 in-band dedup
的能力,可以在写入数据块的同时就去掉重复数据,而 btrfs 只能「退而求其次」地选择第三方 dedup
方案,用外部工具扫描已经写入的数据,将其中重复的部分改为 reflink 。又或许有不少 btrfs
用户以为 zfs 的 dedup 就是在内存和磁盘中维护一个类似
<a class="reference external" href="https://en.wikipedia.org/wiki/Bloom_filter">Bloom filter</a>
的结构,然后根据结果对数据块增加 reflink ,从而 zfs 内部大概一定有类似 reflink
的设施,进一步质疑为什么 btrfs 还迟迟没有实现这样一个 Bloom filter 。
或许还有从 btrfs 转移到 ZFS 的用户有疑惑,
<a class="reference external" href="https://github.com/zfsonlinux/zfs/issues/405">为什么 ZFS 还没有暴露出 reflink 的用户空间接口</a>
,或者既然 ZFS 已经有了 dedup ,
<a class="reference external" href="https://github.com/zfsonlinux/zfs/issues/2554">能不能临时开关 dedup 来提供类似 reflink 式的共享数据块</a>
而避免 ZFS 长期开 dedup 导致的巨大性能开销。</p>
<p>看过上面 <a class="reference internal" href="#id24">ZFS 中关于快照和克隆的空间跟踪算法</a> 之后我们会发现,其实 ZFS 中并没有
能对应 btrfs reflink 的功能,而是根据数据块指针中的 birth txg
来跟踪快照和克隆的共享数据块的。这引来更多疑惑:</p>
<div class="section" id="zfs-dedup">
<h3><a class="toc-backref" href="#id63">4.1 ZFS 是如何实现 dedup 的?</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
Dedup Performance by Matt Ahrens</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/PYxFDBgxFS8"></iframe></div></div>
</div>
<p>ZFS 是在 Sun/OpenSolaris 寿命相当晚期的 2009 年获得的 dedup 功能,就在 Oracle 收购 Sun
,OpenSolaris 分裂出 Illumos 从而 ZFS 分裂出 Oracle ZFS 和 OpenZFS 的时间点之前。因此
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSDedupBadDocumentation">关于 ZFS dedup 如何实现的文档相对匮乏</a>
,大部分介绍 ZFS 的文档或者教程会讲到 ZFS dedup 的用法,但是对 dedup
的实现细节、性能影响、乃至使用场景之类的话题就很少提了(甚至很多教程讲了一堆用法之后说类似,
「我评估之后觉得我不需要开 dedup ,你可以自己评估一下」这样的建议)。</p>
<p>OpenZFS Summit 2017 上 Matt 有个演讲,主要内容关于今后如何改进 dedup
性能的计划,其中讲到的计划还没有被具体实现,不过可以窥探一下 dedup 现在在 ZFS 中是如何工作的。
Chris 的博客也有两篇文章《
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSDedupStorage">What I can see about how ZFS deduplication seems to work on disk</a>
》和《
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSDedupStorageII">An important addition to how ZFS deduplication works on the disk</a>
》介绍了他对此的认识,在这里我也尝试来总结一下 ZFS dedup 特性如何工作。</p>
<p>ZFS dedup 是存储池级别(pool-wide)开关的特性,所以大概在 MOS 之类的地方有存储一个特殊的数据结构,
叫 DeDup Table 简称 DDT 。DDT 目前是存储设备上的一个 hash table ,因为是存储池级别的元数据,
所以在 ZFS 中存储了三份完全一样的 DDT ,DDT 的内容是大概如下结构:</p>
<table border="0" class="table docutils borderless">
<colgroup>
<col width="24%"/>
<col width="54%"/>
<col width="22%"/>
</colgroup>
<thead valign="bottom">
<tr><th class="head">Checksum</th>
<th class="head">DVA(Data Virtual Address)</th>
<th class="head">Refcount</th>
</tr>
</thead>
<tbody valign="top">
<tr><td>0x12345678</td>
<td>vdev=1 addr=0x45671234</td>
<td>3</td>
</tr>
<tr><td>0x5678efab</td>
<td>vdev=2 addr=0x37165adb</td>
<td>0</td>
</tr>
<tr><td>0x98765432</td>
<td>vdev=1 addr=0xac71be12</td>
<td>1</td>
</tr>
<tr><td>0xabcd1234</td>
<td>vdev=0 addr=0xc1a2231d</td>
<td>5</td>
</tr>
<tr><td>... ...</td>
<td>... ...</td>
<td>... ...</td>
</tr>
</tbody>
</table>
<p>DDT 中对每个数据块存有3个东西:数据块的 checksum 、DVA (就是
<a class="reference internal" href="#id18">ZFS 的块指针</a> 中的 DVA)和引用计数。在存储池开启 dedup
特性之后,每次新写入一个数据块,都会先计算出数据块的 checksum ,然后查找 DDT
,存在的话增加 DDT 条目的引用计数,不存在的话插入 DDT 条目。每次释放一个数据块,同样需要查找
DDT 调整引用计数。</p>
<p>除了 DDT 之外,文件系统中记录的块指针中也有个特殊标志位记录这个块是否经过了 DDT
。读取数据不需要经过 DDT ,但是子卷、克隆或者文件系统正常删除数据块的时候,
需要根据块指针中的标志位判断是否需要检查和调整 DDT 。</p>
<p>从而关于 dedup 的实现可以得知以下一些特点:</p>
<ul class="simple">
<li>开启 dedup 之后,每个写入操作放大成 3+1 个随机位置的写入操作,每个删除操作变成 1 个写入操作。没有
dedup 时删除块并不需要立刻写入,只需要记录在内存中并在 MOS 提交的时候调整磁盘占用情况即可。</li>
<li>只有开启 dedup 期间写入的数据块才会参与 dedup 。对已经有数据的存储池,后来开启的 dedup
不会影响已经写好的数据,从而即使后来新的写入与之前的写入有重复也得不到 dedup 效果。
DDT 中没有记录的数据块不会参与 dedup 。换句话说 DDT 中那些引用计数为 1
的记录也是必须存在的,否则这些数据块没有机会参与 dedup 。</li>
<li>关闭 dedup 之后,只要 DDT 中还存有数据,那么对这些数据的删除操作仍然有性能影响。</li>
</ul>
<p>从直觉上可以这样理解:在 ZFS 中每个数据块都有其「归属」,没有 dedup
的时候,数据块归属于某个数据集(文件系统、快照、克隆),
该数据集需要负责释放该数据块或者把从属信息转移到别的数据集(快照)上。
而在开启 dedup 期间,产生的写入的数据块实际归属于 DDT
而不是任何一个数据集,数据集需要查询和调整 DDT 中记录的引用计数来决定是否能释放数据块。</p>
<p>乍看起来 DDT 貌似挺像 btrfs 的 EXTENT_TREE ,但是本质上 EXTENT_TREE 是根据区块地址排序的,
而 DDT 因为是个 hashtable 所以是根据 checksum 排序的。并且 EXTENT_TREE
中记录的区块可以是任意大小,而 DDT 中记录的数据块是固定大小的,所以碎片不严重的情况下 DDT 要比
EXTENT_TREE 多记录很多数据块。这些区别都非常影响操作 DDT 时的性能。</p>
<p>DDT 本身是个 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#dmu">DMU</a> 对象,所以对 DDT 的读写也是经过 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#dmu">DMU</a> 的 CoW 读写,从而也经过 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#arc">ARC</a>
的缓存。想要有比较合理的 dedup 性能,需要整个 DDT 都尽量保持在内存 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#arc">ARC</a> 或者 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#l2arc">L2ARC</a> 缓存中,
于是 dedup 特性也有了非常占用内存的特点。每个 DDT 表项需要大概 192 字节来描述一个(
默认 128KiB 大小的)数据块,由此可以估算一下平均每 2TiB 的数据需要 3GiB 的内存来支持 dedup 的功能。</p>
<p>Matt 的视频中后面讲到优化 ZFS dedup 的一些思路,大体上未来 ZFS 可以做这些优化:</p>
<ol class="arabic simple">
<li>DDT 在内存中仍然是 hashtable ,在存储介质上则换成类似 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#zil">ZIL</a> 的日志结构,让 DDT
尽量保持在内存中,并且绕过 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#dmu">DMU</a> 减少写入放大。</li>
<li>给 DDT 表项瘦身,从192字节缩减到接近64字节。</li>
<li>当遇到内存压力时,从 DDT 中随机剔除掉引用计数为 1 的表项。被剔除的表项没有了未来参与 dedup
的可能性,但是能减轻内存压力。剔除引用计数为 1 的表项仍然可以维持数据块的归属信息(
处理上当作是没有 dedup 的形式),但是引用计数更高的表项没法剔除。</li>
</ol>
<p>这些优化策略目的是想让 dedup 的性能损失能让更多使用场景接受。不过因为缺乏开发者意愿,
目前这些策略还只是计划,没有实现在 ZFS 的代码中。</p>
<p>因为以上特点, ZFS 目前 dedup 特性的适用场景极为有限,只有在 IO 带宽、内存大小都非常充裕,
并且可以预见到很多重复的数据的时候适合。听说过的 ZFS dedup
的成功案例是,比如提供虚拟机服务的服务商,在宿主文件系统上用 ZFS 的 zvol
寄宿虚拟机的磁盘镜像,客户在虚拟机内使用其它文件系统。大部分客户可能用类似版本的操作系统,
从而宿主机整体来看有很多 dedup 的潜质。不过这种应用场景下,服务商很可能偏向选择 CephFS
这样的分布式文件系统提供虚拟机镜像存储,而不是 ZFS 这样局限在单系统上的本地文件系统。</p>
</div>
<div class="section" id="btrfs-dedup">
<h3><a class="toc-backref" href="#id64">4.2 btrfs 的 dedup</a></h3>
<p>btrfs 目前没有内建的 dedup 支持,但是因为有 reflink 所以可以通过第三方工具在事后扫描文件块来实现
dedup 。这一点乍看像是某种将就之策,实际上了解了 ZFS dedup 的实现之后可以看出这个状况其实更灵活。</p>
<p>在 btrfs 中实现 in-band dedup 本身不算很复杂,增加一个内存中的 bloom filter 然后按情况插入
reflink 的正常思路就够了。在
<a class="reference external" href="https://btrfs.wiki.kernel.org/index.php/User_notes_on_dedupe">btrfs kernel wiki 中有篇笔记</a>
提到已经有了实验性的 in-band dedup 内核支持的实现。这个实现已经越来越成熟,虽然还有诸多使用限制,
不过实现正确性上问题不大,迟迟没有办法合并进主线内核的原因更多是性能上的问题。</p>
<p>如果 btrfs 有了 in-band dedup 这样系统性的 dedup 方案,那么不可避免地会增加文件系统中使用
reflink 的数量。这将会暴露出 backref walking 这样的基础设施中许多潜在的边角情况下的性能瓶颈。
前面解释过 backref walking 操作是个挺大开销的操作,并且开销随着快照和 reflink
的使用而爆炸式增长。直到最近的 btrfs 更新仍然在试图优化和改善现有 backref walking
的性能问题,可以预测 btrfs 的内建 dedup 支持将需要等待这方面更加成熟。</p>
</div>
</div>
<div class="section" id="id33">
<h2><a class="toc-backref" href="#id65">5 结论和展望</a></h2>
<p>不知不觉围绕 btrfs 和 zfs 的快照功能写了一大篇,前前后后写了一个半月,
文中提及的很多细节我自己也没有自信,如果有错误还请指出。</p>
<p>稍微列举一些我觉得比较重要的结论,算是 TL;DR 的 takeaway notes 吧:</p>
<ul class="simple">
<li>ZFS 的快照非常轻量。完全可以像 NILFS2 的连续快照那样,每小时一个快照,每天24小时,每年
365天不间断地创建快照,实际似乎也有公司是这样用的。如此频繁的快照不同于 NILFS2
等文件系统提供的连续快照,但是也没有那些日志结构文件系统实现连续快照所需承担的巨大 GC 开销。
并且 ZFS 可以没有额外开销地算出快照等数据集的空间占用之类的信息。</li>
<li>btrfs 的快照相对也很轻量,比 LVM 和 dm-thin 的快照轻便很多,但是不如 ZFS 的快照轻,因为
btrfs 有维护反向引用的开销。 btrfs 要得知子卷的空间占用情况需要开启 qgroup
特性,这会对一些需要 backref walking 的操作有一些额外性能损失。</li>
<li>btrfs 对快照和 reflink 没有限制,日常桌面系统下使用也不太会遇到性能问题。
不过系统性地(自动化地)大量使用快照和 reflink ,在一些操作下可能会有性能问题,值得注意。</li>
<li>因为没有 reflink , ZFS 的数据集划分需要一些前期计划。 ZFS 中共享元数据的方式只有快照,
所以要尽量多细分文件系统,方便以后能利用到快照特性,划分的粒度大致按照可能要回滚快照的粒度来。
btrfs 有 reflink ,于是这里有很多自由度,即便前期计划不够详细也可以通过 reflink
相对快速调整子卷结构。</li>
<li>dedup 在 zfs 和 btrfs 都是个喜忧参半的特性,开启前要仔细评估可能的性能损失。ZFS dedup
的成功案例是,比如虚拟机服务的服务商,在宿主文件系统上用 ZFS
寄宿虚拟机的磁盘镜像,客户在虚拟机可能用类似版本的操作系统,从而宿主机整体来看有很多 dedup
的潜质。一般桌面场景下 dedup 的收益不明显,反而有巨大内存和IO带宽开销。</li>
<li>相比 btrfs ,ZFS 更严格地遵守 CoW 文件系统「仅写一次」的特点,甚至就算遇到了数据块损坏,
修复数据块的时候也只能在原位写入。 btrfs 因为有反向引用所以在这方面灵活很多。</li>
<li>ZFS 不支持对单个文件关闭 CoW ,所有文件(以及所有 zvol)都经过 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#dmu">DMU</a> 层有 CoW
语义,这对一些应用场景有性能影响。btrfs 可以对单个文件关闭 CoW ,但是关闭 CoW
同时也丢失了写文件的事务性语义。</li>
<li>ZFS 不支持碎片整理,靠 ARC 加大缓存来解决碎片带来的性能问题。 btrfs 有 defrag
,但是目前的实现会切断 reflink 。</li>
</ul>
<p>最后关于 ZFS 没有 reflink 也没有反向引用的情况,想引用几段话。</p>
<p>FreeBSD 的发起人之一,FreeBSD 的 FFS 维护者,
<a class="reference external" href="https://youtu.be/IQp_FglfzUQ?t=2619">Kirk McKusick 曾经在 OpenZFS developer summit 2015</a>
这么说过:</p>
<blockquote>
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.</blockquote>
<p>Kirk McKusick 呼吁 ZFS 开发者们考虑在 ZFS 中实现类似 backref
的基础设施,从而可能在未来获得更多有用的特性。</p>
<p>和 ZFS 实现 backref 相关的一点是目前 ZFS 的块指针的组织结构。对此
ZFS 的 <a class="reference external" href="//farseerfc.me/zhs/zfs-layered-architecture-design.html#zpl">ZPL</a> 层原作者之一的
<a class="reference external" href="https://youtu.be/xMH5rCL8S2k?t=1037">Mark Shellenbaum 在 OpenZFS developer summit 2016</a>
也曾说过这样的话:</p>
<blockquote>
(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.</blockquote>
<p>以及 ZFS 的最初的作者 <a class="reference external" href="https://youtu.be/yNKZQBsTX08?t=2">Jeff 在 OpenZFS developer summit 2015</a>
也曾说过:</p>
<blockquote>
... 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.</blockquote>
<p>ZFS 这些开发者元老们都希望 ZFS 能有某种类似 backref 的机制,或者让块指针记录的地址更抽象的机制。</p>
<p>关于这一点,ZFS 最重要的作者 Matt 如何看的呢? Matt 近期似乎没有发表过看法,但是熟悉 ZFS
的人可能听到过 Matt 一直在计划的另一项 ZFS 特性中看出些端倪,叫 BP rewrite
,或者 BP virtualization 。从 Matt 还在 Sun 的时候开始,就试图在 ZFS 中实现
BP rewrite 特性,提供某种系统性的基础设施,能够快速地找到并改写大量数据块指针。
在网上搜索很多 ZFS 功能的实现细节,最终都会带到关于 BP rewrite 的讨论(甚至可以说论战)中。
Matt 最近给 OpenZFS 实现的两项功能,
<a class="reference external" href="https://www.youtube.com/watch?v=Njt82e_3qVo">toplevel vdev removal 和 raidz expansion</a>
如果有 BP rewrite 将会容易很多,而他们目前是在没有 BP rewrite 的前提下,通过一连串额外抽象实现的。</p>
<p>从 BP rewrite 这个兔子洞中,还能引出更多 btrfs 和 ZFS 关于设备管理的差异,这个有待今后再谈。</p>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>ZFS 分层架构设计2020-02-04T16:59:00+09:002020-02-04T16:59:00+09:00farseerfctag:farseerfc.me,2020-02-04:/zhs/zfs-layered-architecture-design.html
<div class="label label-warning">
<strong>2020年2月9日更新过</strong></div>
<p>ZFS 在设计之初源自于 Sun 内部多次重写 UFS 的尝试,背负了重构 Solaris
诸多内核子系统的重任,从而不同于 Linux 的文件系统只负责文件系统的功能而把其余功能(比如内存脏页管理,
IO调度)交给内核更底层的子系统, ZFS 的整体设计更层次化并更独立,很多部分可能和 Linux/FreeBSD
内核已有的子系统有功能重叠。</p>
<p>似乎很多关于 ZFS 的视频演讲和幻灯片有讲到子系统架构,但是找了半天也没找到网上关于这个的说明文档。
于是写下这篇笔记试图从 ZFS 的早期开发历程开始,记录一下 ZFS 分层架构中各个子系统之间的分工。
也有几段 OpenZFS Summit 视频佐以记录那段历史。</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id4">早期架构</a></h2>
<p>早期 ZFS 在开发时大体可以分为上下三层,分别是 ZPL, DMU 和 SPA ,这三层分别由三组人负责。</p>
<p>最初在 Sun 内部带领 ZFS …</p></div>
<div class="label label-warning">
<strong>2020年2月9日更新过</strong></div>
<p>ZFS 在设计之初源自于 Sun 内部多次重写 UFS 的尝试,背负了重构 Solaris
诸多内核子系统的重任,从而不同于 Linux 的文件系统只负责文件系统的功能而把其余功能(比如内存脏页管理,
IO调度)交给内核更底层的子系统, ZFS 的整体设计更层次化并更独立,很多部分可能和 Linux/FreeBSD
内核已有的子系统有功能重叠。</p>
<p>似乎很多关于 ZFS 的视频演讲和幻灯片有讲到子系统架构,但是找了半天也没找到网上关于这个的说明文档。
于是写下这篇笔记试图从 ZFS 的早期开发历程开始,记录一下 ZFS 分层架构中各个子系统之间的分工。
也有几段 OpenZFS Summit 视频佐以记录那段历史。</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id4">早期架构</a></h2>
<p>早期 ZFS 在开发时大体可以分为上下三层,分别是 ZPL, DMU 和 SPA ,这三层分别由三组人负责。</p>
<p>最初在 Sun 内部带领 ZFS 开发的是 <a class="reference external" href="https://blogs.oracle.com/bonwick/">Jeff Bonwick</a>
,他首先有了对 ZFS 整体架构的构思,然后游说 Sun 高层,亲自组建起了 ZFS
开发团队,招募了当时刚从大学毕业的 <a class="reference external" href="http://open-zfs.org/wiki/User:Mahrens">Matt Ahrens</a>
。作为和 Sun 高层谈妥的条件, Jeff 也必须负责 Solaris 整体的 Storage & Filesystem Team
,于是他又从 Solaris 的 Storage Team 抽调了 UFS 部分的负责人 Mark Shellenbaum 和
Mark Maybee 来开发 ZFS 。而如今昔日升阳已然日落, Jeff
成立了独立公司继续开拓服务器存储领域, Matt 是 OpenZFS 项目的负责人,两位 Mark 则留在了
Sun/Oracle 成为了 Oracle ZFS 分支的维护者。</p>
<div class="panel panel-default">
<div class="panel-heading">
The Birth of ZFS by Jeff Bonwick</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/dcV2PaMTAJ4"></iframe></div></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
Story Time (Q&A) with Matt and Jeff</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/yNKZQBsTX08"></iframe></div></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
ZFS First Mount by Mark Shellenbaum</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/xMH5rCL8S2k"></iframe></div></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
ZFS past & future by Mark Maybee</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/c1ek1tFjhH8"></iframe></div></div>
</div>
<p>在开发早期,作为分工, Jeff 负责 ZFS 设计中最底层的 SPA ,提供多个存储设备组成的存储池抽象;
Matt 负责 ZFS 设计中最至关重要的 DMU 引擎,在块设备基础上提供具有事务语义的对象存储;
而两位 Mark 负责 ZFS 设计中直接面向用户的 ZPL ,在 DMU 基础上提供完整 POSIX 文件系统语义。
ZFS 设计中这最初的分工也体现在了 ZFS 现在子系统分层的架构上,继续影响(增强或者限制) ZFS
今后发展的方向。</p>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id5">子系统整体架构</a></h2>
<p>首先 ZFS 整体架构如下图,其中圆圈是 ZFS 给内核层的外部接口,方框是 ZFS 内部子系统(
我给方框的子系统加上了超链接):</p>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
-->
<!-- Title: ZFS_Layer_Architecture Pages: 1 -->
<svg class="svg-responsive" height="626pt" viewbox="0.00 0.00 806.50 626.00" width="807pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 622)">
<title>ZFS_Layer_Architecture</title>
<polygon fill="#ffffff" points="-4,4 -4,-622 802.5,-622 802.5,4 -4,4" stroke="transparent"></polygon>
<g class="cluster" id="clust4">
<title>clusterTOL</title>
<g id="a_clust4"><a xlink:href="#tol" xlink:title="TOL">
<polygon fill="none" points="493.5,-355 493.5,-574 583.5,-574 583.5,-355 493.5,-355" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="538.5" y="-558.8">TOL</text>
</a>
</g>
</g>
<g class="cluster" id="clust9">
<title>clusterSPA</title>
<g id="a_clust9"><a xlink:href="#spa" xlink:title="SPA">
<polygon fill="none" points="415.5,-64 415.5,-283 649.5,-283 649.5,-64 415.5,-64" stroke="#000000"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="532.5" y="-267.8">SPA</text>
</a>
</g>
</g>
<!-- Filesystem API -->
<g class="node" id="node1">
<title>Filesystem API</title>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="62.5" y="-596.3">Filesystem API</text>
</g>
<!-- VFS -->
<g class="node" id="node5">
<title>VFS</title>
<ellipse cx="62.5" cy="-525" fill="none" rx="30.5947" ry="18" stroke="#000000"></ellipse>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="62.5" y="-521.3">VFS</text>
</g>
<!-- Filesystem API->VFS -->
<g class="edge" id="edge1">
<title>Filesystem API->VFS</title>
<path d="M62.5,-581.8446C62.5,-573.3401 62.5,-563.0076 62.5,-553.4964" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="66.0001,-553.2481 62.5,-543.2482 59.0001,-553.2482 66.0001,-553.2481" stroke="#000000"></polygon>
</g>
<!-- Block device API -->
<g class="node" id="node2">
<title>Block device API</title>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="437.5" y="-596.3">Block device API</text>
</g>
<!-- /dev/zvol/... -->
<g class="node" id="node6">
<title>/dev/zvol/...</title>
<ellipse cx="421.5" cy="-525" fill="none" rx="63.8893" ry="18" stroke="#000000"></ellipse>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="421.5" y="-521.3">/dev/zvol/...</text>
</g>
<!-- Block device API->/dev/zvol/... -->
<g class="edge" id="edge2">
<title>Block device API->/dev/zvol/...</title>
<path d="M433.6268,-581.8446C431.7795,-573.1849 429.5277,-562.6301 427.4684,-552.9768" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="430.8469,-552.0379 425.3375,-542.9882 424.001,-553.4984 430.8469,-552.0379" stroke="#000000"></polygon>
</g>
<!-- ZFS Management API (libzfs) -->
<g class="node" id="node3">
<title>ZFS Management API (libzfs)</title>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="684.5" y="-596.3">ZFS Management API (libzfs)</text>
</g>
<!-- /dev/zfs ioctl -->
<g class="node" id="node7">
<title>/dev/zfs ioctl</title>
<ellipse cx="684.5" cy="-525" fill="none" rx="69.5877" ry="18" stroke="#000000"></ellipse>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="684.5" y="-521.3">/dev/zfs ioctl</text>
</g>
<!-- ZFS Management API (libzfs)->/dev/zfs ioctl -->
<g class="edge" id="edge3">
<title>ZFS Management API (libzfs)->/dev/zfs ioctl</title>
<path d="M684.5,-581.8446C684.5,-573.3401 684.5,-563.0076 684.5,-553.4964" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="688.0001,-553.2481 684.5,-543.2482 681.0001,-553.2482 688.0001,-553.2481" stroke="#000000"></polygon>
</g>
<!-- NFS/Samba API (libshare) -->
<g class="node" id="node4">
<title>NFS/Samba API (libshare)</title>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="246.5" y="-596.3">NFS/Samba API (libshare)</text>
</g>
<!-- NFS/CIFS vop_rwlock -->
<g class="node" id="node8">
<title>NFS/CIFS vop_rwlock</title>
<ellipse cx="225.5" cy="-525" fill="none" rx="114.2798" ry="18" stroke="#000000"></ellipse>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="225.5" y="-521.3">NFS/CIFS vop_rwlock</text>
</g>
<!-- NFS/Samba API (libshare)->NFS/CIFS vop_rwlock -->
<g class="edge" id="edge4">
<title>NFS/Samba API (libshare)->NFS/CIFS vop_rwlock</title>
<path d="M241.4165,-581.8446C238.966,-573.0928 235.9735,-562.4053 233.2473,-552.669" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="236.6034,-551.6741 230.5367,-542.9882 229.8627,-553.5616 236.6034,-551.6741" stroke="#000000"></polygon>
</g>
<!-- VFS->NFS/CIFS vop_rwlock -->
<g class="edge" id="edge5">
<title>VFS->NFS/CIFS vop_rwlock</title>
<path d="M93.0625,-525C95.6653,-525 98.2682,-525 100.871,-525" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="100.9106,-528.5001 110.9105,-525 100.9105,-521.5001 100.9106,-528.5001" stroke="#000000"></polygon>
</g>
<!-- ZPL -->
<g class="node" id="node9">
<title>ZPL</title>
<g id="a_node9"><a xlink:href="#zpl" xlink:title="ZPL">
<polygon fill="none" points="367.5,-471 313.5,-471 313.5,-435 367.5,-435 367.5,-471" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="340.5" y="-449.3">ZPL</text>
</a>
</g>
</g>
<!-- VFS->ZPL -->
<g class="edge" id="edge6">
<title>VFS->ZPL</title>
<path d="M86.3025,-513.3546C91.5594,-511.0472 97.1562,-508.7886 102.5,-507 171.7912,-483.808 255.7337,-467.3568 303.1418,-459.0894" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="303.9854,-462.4959 313.2493,-457.3547 302.8013,-455.5968 303.9854,-462.4959" stroke="#000000"></polygon>
</g>
<!-- ZVOL -->
<g class="node" id="node10">
<title>ZVOL</title>
<g id="a_node10"><a xlink:href="#zvol" xlink:title="ZVOL">
<polygon fill="none" points="483,-471 424,-471 424,-435 483,-435 483,-471" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="453.5" y="-449.3">ZVOL</text>
</a>
</g>
</g>
<!-- /dev/zvol/...->ZVOL -->
<g class="edge" id="edge7">
<title>/dev/zvol/...->ZVOL</title>
<path d="M429.575,-506.8314C433.1102,-498.8771 437.336,-489.369 441.2456,-480.5723" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="444.4532,-481.9729 445.3163,-471.4133 438.0565,-479.1299 444.4532,-481.9729" stroke="#000000"></polygon>
</g>
<!-- DSL -->
<g class="node" id="node11">
<title>DSL</title>
<g id="a_node11"><a xlink:href="#dsl" xlink:title="DSL">
<polygon fill="none" points="557.5,-543 503.5,-543 503.5,-507 557.5,-507 557.5,-543" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="530.5" y="-521.3">DSL</text>
</a>
</g>
</g>
<!-- /dev/zfs ioctl->DSL -->
<g class="edge" id="edge10">
<title>/dev/zfs ioctl->DSL</title>
<path d="M614.5895,-525C598.9983,-525 583.4071,-525 567.8159,-525" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="567.5703,-521.5001 557.5703,-525 567.5703,-528.5001 567.5703,-521.5001" stroke="#000000"></polygon>
</g>
<!-- VDEV -->
<g class="node" id="node20">
<title>VDEV</title>
<g id="a_node20"><a xlink:href="#vdev" xlink:title="VDEV">
<polygon fill="none" points="546,-108 487,-108 487,-72 546,-72 546,-108" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="516.5" y="-86.3">VDEV</text>
</a>
</g>
</g>
<!-- /dev/zfs ioctl->VDEV -->
<g class="edge" id="edge29">
<title>/dev/zfs ioctl->VDEV</title>
<path d="M689.2656,-506.9623C696.0025,-479.8344 707.5,-426.8236 707.5,-381 707.5,-381 707.5,-381 707.5,-234 707.5,-186.6526 686.8759,-174.3079 650.5,-144 623.0063,-121.0926 584.5356,-106.9458 555.8638,-98.9099" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="556.7296,-95.5186 546.165,-96.3181 554.9223,-102.2812 556.7296,-95.5186" stroke="#000000"></polygon>
</g>
<!-- DMU -->
<g class="node" id="node13">
<title>DMU</title>
<g id="a_node13"><a xlink:href="#dmu" xlink:title="DMU">
<polygon fill="none" points="555.5,-399 501.5,-399 501.5,-363 555.5,-363 555.5,-399" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="528.5" y="-377.3">DMU</text>
</a>
</g>
</g>
<!-- NFS/CIFS vop_rwlock->DMU -->
<g class="edge" id="edge15">
<title>NFS/CIFS vop_rwlock->DMU</title>
<path d="M285.6968,-509.563C314.41,-500.7092 348.5163,-487.9433 376.5,-471 396.6786,-458.7825 395.9728,-448.2337 415.5,-435 439.4387,-418.7766 469.0038,-404.835 491.8877,-395.1832" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="493.3442,-398.3685 501.2501,-391.3152 490.6713,-391.8989 493.3442,-398.3685" stroke="#000000"></polygon>
</g>
<!-- ZAP -->
<g class="node" id="node12">
<title>ZAP</title>
<g id="a_node12"><a xlink:href="#zap" xlink:title="ZAP">
<polygon fill="none" points="555.5,-471 501.5,-471 501.5,-435 555.5,-435 555.5,-471" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="528.5" y="-449.3">ZAP</text>
</a>
</g>
</g>
<!-- ZPL->ZAP -->
<g class="edge" id="edge8">
<title>ZPL->ZAP</title>
<path d="M367.5472,-469.1304C396.6265,-484.3281 444.0019,-502.8552 483.5,-489 490.6397,-486.4955 497.4881,-482.3291 503.5943,-477.7013" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="505.9521,-480.293 511.4185,-471.2173 501.4855,-474.9032 505.9521,-480.293" stroke="#000000"></polygon>
</g>
<!-- ZPL->DMU -->
<g class="edge" id="edge12">
<title>ZPL->DMU</title>
<path d="M367.7478,-440.7581C372.3191,-438.7848 377.0292,-436.8006 381.5,-435 418.7731,-419.9886 462.0037,-404.312 491.8902,-393.7414" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="493.2174,-396.9848 501.4861,-390.3607 490.8913,-390.3825 493.2174,-396.9848" stroke="#000000"></polygon>
</g>
<!-- ZIL -->
<g class="node" id="node15">
<title>ZIL</title>
<g id="a_node15"><a xlink:href="#zil" xlink:title="ZIL">
<polygon fill="none" points="480.5,-327 426.5,-327 426.5,-291 480.5,-291 480.5,-327" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="453.5" y="-305.3">ZIL</text>
</a>
</g>
</g>
<!-- ZPL->ZIL -->
<g class="edge" id="edge16">
<title>ZPL->ZIL</title>
<path d="M354.8115,-434.7623C374.5947,-409.5518 410.4107,-363.9103 433.0226,-335.0952" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="435.8847,-337.1173 439.3047,-327.0896 430.3778,-332.7959 435.8847,-337.1173" stroke="#000000"></polygon>
</g>
<!-- ZVOL->DMU -->
<g class="edge" id="edge13">
<title>ZVOL->DMU</title>
<path d="M472.4257,-434.8314C481.4857,-426.1337 492.4809,-415.5783 502.3265,-406.1265" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="504.7904,-408.613 509.5804,-399.1628 499.9426,-403.5633 504.7904,-408.613" stroke="#000000"></polygon>
</g>
<!-- DSL->ZAP -->
<g class="edge" id="edge9">
<title>DSL->ZAP</title>
<path d="M529.9953,-506.8314C529.7814,-499.131 529.5271,-489.9743 529.2894,-481.4166" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="532.7879,-481.3122 529.0115,-471.4133 525.7906,-481.5066 532.7879,-481.3122" stroke="#000000"></polygon>
</g>
<!-- DSL->DMU -->
<g class="edge" id="edge14">
<title>DSL->DMU</title>
<path d="M545.614,-506.7768C552.8549,-496.8586 560.7524,-483.9858 564.5,-471 568.9365,-455.6274 569.1365,-450.3135 564.5,-435 561.5436,-425.2355 556.1748,-415.6229 550.47,-407.2762" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="553.2129,-405.0973 544.4905,-399.0831 547.5586,-409.2239 553.2129,-405.0973" stroke="#000000"></polygon>
</g>
<!-- ZAP->DMU -->
<g class="edge" id="edge11">
<title>ZAP->DMU</title>
<path d="M528.5,-434.8314C528.5,-427.131 528.5,-417.9743 528.5,-409.4166" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="532.0001,-409.4132 528.5,-399.4133 525.0001,-409.4133 532.0001,-409.4132" stroke="#000000"></polygon>
</g>
<!-- ARC -->
<g class="node" id="node14">
<title>ARC</title>
<g id="a_node14"><a xlink:href="#arc" xlink:title="ARC">
<polygon fill="none" points="555.5,-327 501.5,-327 501.5,-291 555.5,-291 555.5,-327" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="528.5" y="-305.3">ARC</text>
</a>
</g>
</g>
<!-- DMU->ARC -->
<g class="edge" id="edge17">
<title>DMU->ARC</title>
<path d="M528.5,-362.8314C528.5,-355.131 528.5,-345.9743 528.5,-337.4166" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="532.0001,-337.4132 528.5,-327.4133 525.0001,-337.4133 532.0001,-337.4132" stroke="#000000"></polygon>
</g>
<!-- MetaSlab -->
<g class="node" id="node19">
<title>MetaSlab</title>
<g id="a_node19"><a xlink:href="#metaslab" xlink:title="MetaSlab">
<polygon fill="none" points="641,-180 556,-180 556,-144 641,-144 641,-180" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="598.5" y="-158.3">MetaSlab</text>
</a>
</g>
</g>
<!-- DMU->MetaSlab -->
<g class="edge" id="edge23">
<title>DMU->MetaSlab</title>
<path d="M555.6383,-368.3974C585.2203,-352.9755 630.9457,-323.7894 650.5,-283 663.3726,-256.1483 661.6355,-243.6173 650.5,-216 646.1775,-205.2796 638.5956,-195.4983 630.5361,-187.2948" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="632.7442,-184.5652 623.0801,-180.2228 627.927,-189.644 632.7442,-184.5652" stroke="#000000"></polygon>
</g>
<!-- ZIO -->
<g class="node" id="node16">
<title>ZIO</title>
<g id="a_node16"><a xlink:href="#zio" xlink:title="ZIO">
<polygon fill="none" points="555.5,-252 501.5,-252 501.5,-216 555.5,-216 555.5,-252" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="528.5" y="-230.3">ZIO</text>
</a>
</g>
</g>
<!-- ARC->ZIO -->
<g class="edge" id="edge20">
<title>ARC->ZIO</title>
<path d="M528.5,-290.8446C528.5,-282.3401 528.5,-272.0076 528.5,-262.4964" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="532.0001,-262.2481 528.5,-252.2482 525.0001,-262.2482 532.0001,-262.2481" stroke="#000000"></polygon>
</g>
<!-- L2ARC -->
<g class="node" id="node17">
<title>L2ARC</title>
<g id="a_node17"><a xlink:href="#l2arc" xlink:title="L2ARC">
<polygon fill="none" points="641.5,-252 573.5,-252 573.5,-216 641.5,-216 641.5,-252" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="607.5" y="-230.3">L2ARC</text>
</a>
</g>
</g>
<!-- ARC->L2ARC -->
<g class="edge" id="edge21">
<title>ARC->L2ARC</title>
<path d="M554.9972,-290.9209C558.2877,-288.3716 561.5341,-285.6973 564.5,-283 572.1249,-276.0657 579.7661,-267.8645 586.4472,-260.1753" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="589.4807,-262.0092 593.2739,-252.1166 584.1396,-257.4845 589.4807,-262.0092" stroke="#000000"></polygon>
</g>
<!-- ZIL->ZIO -->
<g class="edge" id="edge19">
<title>ZIL->ZIO</title>
<path d="M480.869,-292.5464C484.9519,-289.5928 488.9697,-286.3728 492.5,-283 499.3026,-276.501 505.7319,-268.5881 511.2235,-261.0336" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="514.3671,-262.6436 517.2054,-252.436 508.6211,-258.6456 514.3671,-262.6436" stroke="#000000"></polygon>
</g>
<!-- SLOG -->
<g class="node" id="node18">
<title>SLOG</title>
<g id="a_node18"><a xlink:href="#slog" xlink:title="SLOG">
<polygon fill="none" points="483,-252 424,-252 424,-216 483,-216 483,-252" stroke="#0000ff"></polygon>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="453.5" y="-230.3">SLOG</text>
</a>
</g>
</g>
<!-- ZIL->SLOG -->
<g class="edge" id="edge18">
<title>ZIL->SLOG</title>
<path d="M453.5,-290.8446C453.5,-282.3401 453.5,-272.0076 453.5,-262.4964" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="457.0001,-262.2481 453.5,-252.2482 450.0001,-262.2482 457.0001,-262.2481" stroke="#000000"></polygon>
</g>
<!-- ZIO->MetaSlab -->
<g class="edge" id="edge24">
<title>ZIO->MetaSlab</title>
<path d="M546.164,-215.8314C554.4732,-207.2848 564.5264,-196.9443 573.5918,-187.6198" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="576.1369,-190.023 580.5982,-180.4133 571.1179,-185.1435 576.1369,-190.023" stroke="#000000"></polygon>
</g>
<!-- ZIO->VDEV -->
<g class="edge" id="edge28">
<title>ZIO->VDEV</title>
<path d="M526.9802,-215.7623C524.9334,-191.201 521.2706,-147.2474 518.8628,-118.3541" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="522.3259,-117.7644 518.0075,-108.0896 515.3501,-118.3458 522.3259,-117.7644" stroke="#000000"></polygon>
</g>
<!-- L2ARC->ZIO -->
<g class="edge" id="edge22">
<title>L2ARC->ZIO</title>
<path d="M573.4775,-234C570.8786,-234 568.2797,-234 565.6807,-234" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="565.6563,-230.5001 555.6563,-234 565.6562,-237.5001 565.6563,-230.5001" stroke="#000000"></polygon>
</g>
<!-- L2ARC->VDEV -->
<g class="edge" id="edge26">
<title>L2ARC->VDEV</title>
<path d="M581.9503,-215.8625C570.0905,-206.3385 556.6295,-193.7883 547.5,-180 535.0104,-161.137 527.0138,-136.7121 522.2764,-118.0676" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="525.6279,-117.032 519.9132,-108.1105 518.8171,-118.6485 525.6279,-117.032" stroke="#000000"></polygon>
</g>
<!-- SLOG->VDEV -->
<g class="edge" id="edge25">
<title>SLOG->VDEV</title>
<path d="M461.479,-215.7623C472.3192,-190.9846 491.7938,-146.4714 504.4277,-117.5939" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="507.7841,-118.6541 508.5858,-108.0896 501.371,-115.8483 507.7841,-118.6541" stroke="#000000"></polygon>
</g>
<!-- MetaSlab->VDEV -->
<g class="edge" id="edge27">
<title>MetaSlab->VDEV</title>
<path d="M577.8079,-143.8314C567.8052,-135.0485 555.645,-124.3712 544.8001,-114.8489" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="547.0092,-112.1308 537.1854,-108.1628 542.3906,-117.3909 547.0092,-112.1308" stroke="#000000"></polygon>
</g>
<!-- physical storage devices -->
<g class="node" id="node21">
<title>physical storage devices</title>
<path d="M613,-32.7273C613,-34.5331 569.7472,-36 516.5,-36 463.2528,-36 420,-34.5331 420,-32.7273 420,-32.7273 420,-3.2727 420,-3.2727 420,-1.4669 463.2528,0 516.5,0 569.7472,0 613,-1.4669 613,-3.2727 613,-3.2727 613,-32.7273 613,-32.7273" fill="none" stroke="#000000"></path>
<path d="M613,-32.7273C613,-30.9214 569.7472,-29.4545 516.5,-29.4545 463.2528,-29.4545 420,-30.9214 420,-32.7273" fill="none" stroke="#000000"></path>
<text fill="#000000" font-family="Times,serif" font-size="14.00" text-anchor="middle" x="516.5" y="-14.3">physical storage devices</text>
</g>
<!-- VDEV->physical storage devices -->
<g class="edge" id="edge30">
<title>VDEV->physical storage devices</title>
<path d="M516.5,-71.8314C516.5,-64.131 516.5,-54.9743 516.5,-46.4166" fill="none" stroke="#000000"></path>
<polygon fill="#000000" points="520.0001,-46.4132 516.5,-36.4133 513.0001,-46.4133 520.0001,-46.4132" stroke="#000000"></polygon>
</g>
</g>
</svg>
<p>接下来从底层往上介绍一下各个子系统的全称和职能。</p>
</div>
<div class="section" id="spa">
<h2><a class="toc-backref" href="#id6">SPA</a></h2>
<p>Storage Pool Allocator</p>
<p>从内核提供的多个块设备中抽象出存储池的子系统。 SPA 进一步分为 ZIO 和 VDEV 两大部分和其余一些小的子系统。</p>
<p>SPA 对 DMU 提供的接口不同于传统的块设备接口,完全利用了 CoW 文件系统对写入位置不敏感的特点。
传统的块设备接口通常是写入时指定一个写入地址,把缓冲区写到磁盘指定的位置上,而 DMU 可以让 SPA
做两种操作:</p>
<ol class="arabic simple">
<li><code class="code">
write</code>
, DMU 交给 SPA 一个数据块的内存指针, SPA
负责找设备写入这个数据块,然后返回给 DMU 一个 block pointer 。</li>
<li><code class="code">
read</code>
,DMU 交给 SPA 一个 block pointer ,SPA 读取设备并返回给 DMU
完整的数据块。</li>
</ol>
<p>也就是说,在 DMU 让 SPA 写数据块时, DMU 还不知道 SPA 会写入的地方,这完全由 SPA 判断,
这一点通常被称为 Write Anywhere ,在别的 CoW 文件系统比如 Btrfs 和 WAFL 中也有这个特点。
反过来 SPA 想要对一个数据块操作时,也完全不清楚这个数据块用于什么目的,属于什么文件或者文件系统结构。</p>
</div>
<div class="section" id="vdev">
<h2><a class="toc-backref" href="#id7">VDEV</a></h2>
<p>Virtual DEVice</p>
<p>VDEV 在 ZFS 中的作用相当于 Linux 内核的 Device Mapper 层或者 FreeBSD GEOM 层,提供
Stripe/Mirror/RAIDZ 之类的多设备存储池管理和抽象。 ZFS 中的 vdev
形成一个树状结构,在树的底层是从内核提供的物理设备,
其上是虚拟的块设备。每个虚拟块设备对上对下都是块设备接口,除了底层的物理设备之外,位于中间层的
vdev 需要负责地址映射、容量转换等计算过程。</p>
<p>除了用于存储数据的 Stripe/Mirror/RAIDZ 之类的 VDEV ,还有一些特殊用途的 VDEV
,包括提供二级缓存的 L2ARC 设备,以及提供 ZIL 高速日志的 SLOG 设备。</p>
</div>
<div class="section" id="zio">
<h2><a class="toc-backref" href="#id8">ZIO</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
ZIO Pipeline by George Wilson</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/qkA5HhfzsvM"></iframe></div></div>
</div>
<p>ZFS I/O</p>
<p>作用相当于内核的 IO scheduler 和 pagecache write back 机制。
OpenZFS Summit 有个演讲整理了 ZIO 流水线的工作原理。
ZIO 内部使用流水线和事件驱动机制,避免让上层的 ZFS 线程阻塞等待在 IO 操作上。
ZIO 把一个上层的写请求转换成多个写操作,负责把这些写操作合并到
transaction group 提交事务组。 ZIO 也负责将读写请求按同步还是异步分成不同的读写优先级并实施优先级调度,
在 <a class="reference external" href="https://github.com/zfsonlinux/zfs/wiki/ZIO-Scheduler">OpenZFS 项目 wiki 页有一篇描述 ZIO 调度</a>
的细节。</p>
<p>除了调度之外, ZIO 层还负责在读写流水线中拆解和拼装数据块。上层 DMU 交给 SPA 的数据块有固定大小,
目前默认是 128KiB ,pool 整体的参数可以调整块大小在 4KiB 到 8MiB 之间。ZIO
拿到整块大小的数据块之后,在流水线中可以对数据块做诸如以下操作:</p>
<ol class="arabic simple">
<li>用压缩算法,压缩/解压数据块。</li>
<li>查询 dedup table ,对数据块去重。</li>
<li>加密/解密数据块。</li>
<li>计算数据块的校验和。</li>
<li>如果底层分配器不能分配完整的 128KiB (或 zpool 设置的大小),那么尝试分配多个小块,然后用多个
512B 的指针间接块连起多个小块的,拼装成一个虚拟的大块,这个机制叫
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSGangBlocks">gang block</a>
。通常 ZFS 中用到 gang block 时,整个存储池处于极度空间不足的情况,由 gang block
造成严重性能下降,而 gang block 的意义在于在空间接近要满的时候也能 CoW
写入一些元数据,释放亟需的存储空间。</li>
</ol>
<p>可见经过 ZIO 流水线之后,数据块不再是统一大小,这使得 ZFS 用在 4K 对齐的磁盘或者 SSD
上有了一些新的挑战。</p>
</div>
<div class="section" id="metaslab">
<h2><a class="toc-backref" href="#id9">MetaSlab</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
MetaSlab Allocation Performance by Paul Dagnelie</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/LZpaTGNvalE"></iframe></div></div>
</div>
<p>MetaSlab 是 ZFS 的块分配器。 VDEV 把存储设备抽象成存储池之后, MetaSlab
负责实际从存储设备上分配数据块,跟踪记录可用空间和已用空间。</p>
<p>叫 MetaSlab 这个名字是因为 Jeff 最初同时也给 Solaris 内核写过
<a class="reference external" href="https://en.wikipedia.org/wiki/Slab_allocation">slab 分配器</a>
,一开始设计 SPA 的时候 Jeff 想在 SPA 中也利用 Solaris 的 slab
分配器对磁盘空间使用类似的分配算法。后来 MetaSlab 逐渐不再使用 slab 算法,只有名字留了下来。</p>
<p>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
。</p>
<p>OpenZFS Summit 也有一个对 MetaSlab 分配器性能的介绍,可以看到很多分配器内的细节。</p>
</div>
<div class="section" id="arc">
<h2><a class="toc-backref" href="#id10">ARC</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
ELI5: ZFS Caching Explain Like I'm 5: How the ZFS Adaptive Replacement Cache works</div>
<div class="panel-body">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/1Wo3i2gkAIk"></iframe></div></div>
</div>
<p>Adaptive Replacement Cache</p>
<p>ARC 的作用相当于 Linux/Solaris/FreeBSD 中传统的 page/buffer cache 。
和传统 pagecache 使用 LRU (Least Recently Used) 之类的算法剔除缓存页不同, ARC
算法试图在 LRU 和 LFU(Least Frequently Used) 之间寻找平衡,从而复制大文件之类的线性大量
IO 操作不至于让缓存失效率猛增。最近 FOSDEM 2019 有一个介绍 ZFS ARC 工作原理的视频。</p>
<p>不过 ZFS 采用它自有的 ARC 一个显著缺点在于,不能和内核已有的 pagecache 机制相互配合,尤其在
系统内存压力很大的情况下,内核与 ZFS 无关的其余部分可能难以通知 ARC 释放内存。所以 ARC
是 ZFS 消耗内存的大户之一(另一个是可选的 dedup table),也是
<a class="reference external" href="http://open-zfs.org/wiki/Performance_tuning#Adaptive_Replacement_Cache">ZFS 性能调优</a>
的重中之重。</p>
<p>当然, ZFS 采用 ARC 不依赖于内核已有的 pagecache 机制除了 LFU 平衡的好处之外,也有别的有利的一面。
系统中多次读取因 snapshot 或者 dedup 而共享的数据块的话,在 ZFS 的 ARC 机制下,同样的
block pointer 只会被缓存一次;而传统的 pagecache 因为基于 inode 判断是否有共享,
所以即使这些文件有共享页面(比如 btrfs/xfs 的 reflink 形成的),也会多次读入内存。 Linux
的 btrfs 和 xfs 在 VFS 层面有共用的 reflink 机制之后,正在努力着手改善这种局面,而 ZFS
因为 ARC 所以从最初就避免了这种浪费。</p>
<p>和很多传言所说的不同, ARC 的内存压力问题不仅在 Linux 内核会有,在 FreeBSD 和
Solaris/Illumos 上也是同样,这个在
<a class="reference external" href="https://youtu.be/xMH5rCL8S2k?t=997">ZFS First Mount by Mark Shellenbaum 的问答环节 16:37 左右有提到</a>
。其中 Mark Shellenbaum 提到 Matt 觉得让 ARC 并入现有 pagecache
子系统的工作量太大,难以实现。</p>
<p>因为 ARC 工作在 ZIO 上层,所以 ARC 中缓存的数据是经过 ZIO
从存储设备中读取出来之后解压、解密等处理之后的,原始的数据。最近 ZFS 的版本有支持一种新特性叫
<a class="reference external" href="https://www.illumos.org/issues/6950">Compressed ARC</a>
,打破 ARC 和 VDEV 中间 ZIO 的壁垒,把压缩的数据直接缓存在 ARC
中。这么做是因为压缩解压很快的情况下,压缩的 ARC 能节省不少内存,让更多数据保留在 ARC
中从而提升缓存利用率,并且在有 L2ARC 的情况下也能增加 L2ARC 能存储的缓存。</p>
</div>
<div class="section" id="l2arc">
<h2><a class="toc-backref" href="#id11">L2ARC</a></h2>
<p>Level 2 Adaptive Replacement Cache</p>
<p>这是用 ARC 算法实现的二级缓存,保存于高速存储设备上。常见用法是给 ZFS pool 配置一块 SSD
作为 L2ARC 高速缓存,减轻内存 ARC 的负担并增加缓存命中率。</p>
</div>
<div class="section" id="slog">
<h2><a class="toc-backref" href="#id12">SLOG</a></h2>
<p>Separate intent LOG</p>
<p>SLOG 是额外的日志记录设备。 SLOG 之于 ZIL 有点像 L2ARC 之余 ARC , L2ARC 是把内存中的
ARC 放入额外的高速存储设备,而 SLOG 是把原本和别的数据块存储在一起的 ZIL
放到额外的高速存储设备。</p>
</div>
<div class="section" id="tol">
<h2><a class="toc-backref" href="#id13">TOL</a></h2>
<p>Transactional Object Layer</p>
<p>这一部分子系统在数据块的基础上提供一个事务性的对象语义层,这里事务性是指,
对对象的修改处于明确的状态,不会因为突然断电之类的原因导致状态不一致。TOL
中最主要的部分是 DMU 层。</p>
</div>
<div class="section" id="dmu">
<h2><a class="toc-backref" href="#id14">DMU</a></h2>
<p>Data Management Unit</p>
<p>在块的基础上提供「对象(object)」的抽象。每个「对象」可以是一个文件,或者是别的 ZFS 内部需要记录的东西。</p>
<p>DMU 这个名字最初是 Jeff 想类比于操作系统中内存管理的 MMU(Memory Management Unit),
Jeff 希望 ZFS 中增加和删除文件就像内存分配一样简单,增加和移除块设备就像增加内存一样简单,
由 DMU 负责从存储池中分配和释放数据块,对上提供事务性语义,管理员不需要管理文件存储在什么存储设备上。
这里事务性语义指对文件的修改要么完全成功,要么完全失败,不会处于中间状态,这靠 DMU 的 CoW
语义实现。</p>
<p>DMU 实现了对象级别的 CoW 语义,从而任何经过了 DMU 做读写的子系统都具有了 CoW 的特征,
这不仅包括文件、文件夹这些 ZPL 层需要的东西,也包括文件系统内部用的 spacemap 之类的设施。
相反,不经过 DMU 的子系统则可能没法保证事务语义。这里一个特例是 ZIL ,一定程度上绕过了 DMU
直接写日志。说一定程度是因为 ZIL 仍然靠 DMU 来扩展长度,当一个块写满日志之后需要等 DMU
分配一个新块,在分配好的块内写日志则不需要经过 DMU 。所有经过 DMU 子系统的对象都有 CoW
语义,也意味着 ZFS 中不能对某些文件可选地关闭 CoW ,不能提供数据库应用的 direct IO 之类的接口。</p>
<p>「对象(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 提供的流水线对延迟写入避免碎片有至关重要的帮助。</p>
<p>有了「对象(object)」的抽象之后, DMU 进一步实现了「对象集(objectset)」的抽象,
一个对象集中保存一系列按顺序编号的 dnode ( ZFS 中类似 inode 的数据结构),每个 dnode 有足够空间
指向一个对象的最多三个块指针,如果对象需要更多数据块可以使用间接块,如果对象很小也可以直接压缩进
dnode 。随后 DSL 又进一步用对象集来实现数据集(dataset)抽象,提供比如文件系统(filesystem
)、快照(snapshot)、克隆(clone)之类的抽象。一个对象集中的对象可以通过 dnode 编号相互引用,
就像普通文件系统的硬链接引用 inode 编号那样。</p>
<p>上面也提到因为 SPA 和 DMU 分离, SPA 完全不知道数据块用于什么目的;这一点其实对 DMU 也是类似,
DMU 虽然能从某个对象找到它所占用的数据块,但是 DMU 完全不知道这个对象在文件系统或者存储池中是
用来存储什么的。当 DMU 读取数据遇到坏块(block pointer 中的校验和与 block pointer
指向的数据块内容不一致)时,它知道这个数据块在哪儿(具体哪个设备上的哪个地址),
但是不知道这个数据块是否和别的对象共享,不知道搬动这个数据块的影响,也没法从对象反推出文件系统路径,
(除了明显开销很高地扫一遍整个存储池)。所以 DMU 在遇到读取错误(普通的读操作或者 scrub/resilver
操作中)时,只能选择在同样的地址,原地写入数据块的备份(如果能找到或者推算出备份的话)。</p>
<p>或许有人会疑惑,既然从 SPA 无法根据数据地址反推出对象,在 DMU 也无法根据对象反推出文件,那么
zfs 在遇到数据损坏时是如何在诊断信息中给出损坏的文件路径的呢?这其实基于 ZPL 的一个黑魔法:
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSPathLookupTrick">在 dnode 记录父级 dnode 的编号</a>
。因为是个黑魔法,这个记录不总是对的,所以只能用于诊断信息,不能基于这个实现别的文件系统功能。</p>
</div>
<div class="section" id="zap">
<h2><a class="toc-backref" href="#id15">ZAP</a></h2>
<p>ZFS Attribute Processor</p>
<p>在 DMU 提供的「对象」抽象基础上提供紧凑的 name/value 映射存储,
从而文件夹内容列表、文件扩展属性之类的都是基于 ZAP 来存。 ZAP 在内部分为两种存储表达:
microZAP 和 fatZAP 。</p>
<p>一个 microZAP 占用一整块数据块,能存 name 长度小于 50 字符并且 value 是 uint64_t 的表项,
每个表项 64 字节。 <del>fatZAP 则是个树状结构,能存更多更复杂的东西。</del><ins>fatZAP 是个 on disk 的散利表,指针表中是 64bit 对 name 的 hash ,指向单链表的子节点列表,子节点中的 value 可以是任意类型的数据(不光是 uint64_t )。</ins></p>
<p>可见 microZAP 非常适合表述一个普通大小的文件夹里面包含到很多普通文件 inode (ZFS 是 dnode)的引用。
<del></del><ins>fatZAP 则不光可以用于任意大小的文件夹,还可以表达 ZFS 的配置属性之类的东西,非常灵活。</ins></p>
<p>在 <a class="reference external" href="https://youtu.be/xMH5rCL8S2k?t=526">ZFS First Mount by Mark Shellenbaum 的8:48左右</a>
提到,最初 ZPL 中关于文件的所有属性(包括访问时间、权限、大小之类所有文件都有的)都是基于
ZAP 来存,也就是说每个文件都有个 ZAP ,其中有叫做 size 呀 owner
之类的键值对,就像是个 JSON 对象那样,这让 ZPL 一开始很容易设计原型并且扩展。然后文件夹内容列表有另一种数据结构
ZDS(ZFS Directory Service),后来常见的文件属性在 ZPL 有了专用的紧凑数据结构,而 ZDS 则渐渐融入了 ZAP 。
<del></del><ins>这些变化详见下面 ZPL 。</ins></p>
</div>
<div class="section" id="dsl">
<h2><a class="toc-backref" href="#id16">DSL</a></h2>
<p>Dataset and Snapshot Layer</p>
<p>数据集和快照层,负责创建和管理快照、克隆等数据集类型,跟踪它们的写入大小,最终删除它们。
由于 DMU 层面已经负责了对象的写时复制语义和对象集的概念,所以 DSL 层面不需要直接接触写文件之类来自 ZPL
的请求,无论有没有快照对 DMU 层面一样采用写时复制的方式修改文件数据。
不过在删除快照和克隆之类的时候,则需要 DSL 参与计算没有和别的数据集共享的数据块并且删除它们。</p>
<p>DSL 管理数据集时,也负责管理数据集上附加的属性。ZFS 每个数据集有个属性列表,这些用 ZAP 存储,
DSL 则需要根据数据集的上下级关系,计算出继承的属性,最终指导 ZIO 层面的读写行为。</p>
<p>除了管理数据集, 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
的壁垒,支持了直接发送已压缩或加密的数据块的能力。</p>
</div>
<div class="section" id="zil">
<h2><a class="toc-backref" href="#id17">ZIL</a></h2>
<p>ZFS Intent Log</p>
<p>记录两次完整事务语义提交之间的日志,用来加速实现 fsync 之类的文件事务语义。</p>
<p>原本 CoW 的文件系统不需要日志结构来保证文件系统结构的一致性,在 DMU
保证了对象级别事务语义的前提下,每次完整的 transaction group commit
都保证了文件系统一致性,挂载时也直接找到最后一个 transaction group 从它开始挂载即可。
不过在 ZFS 中,做一次完整的 transaction group commit 是个比较耗时的操作,
在写入文件的数据块之后,还需要更新整个 object set ,然后更新 meta-object set
,最后更新 uberblock ,为了满足事务语义这些操作没法并行完成,所以整个 pool
提交一次需要等待好几次磁盘写操作返回,短则一两秒,长则几分钟,
如果事务中有要删除快照等非常耗时的操作可能还要等更久,在此期间提交的事务没法保证一致。</p>
<p>对上层应用程序而言,通常使用 fsync 或者 fdatasync 之类的系统调用,确保文件内容本身的事务一致性。
如果要让每次 fsync/fdatasync 等待整个 transaction group commit
完成,那会严重拖慢很多应用程序,而如果它们不等待直接返回,则在突发断电时没有保证一致性。
从而 ZFS 有了 ZIL ,记录两次 transaction group 的 commit 之间发生的 fsync
,突然断电后下次 import zpool 时首先找到最近一次 transaction group ,在它基础上重放
ZIL 中记录的写请求和 fsync 请求,从而满足 fsync API 要求的事务语义。</p>
<p>显然对 ZIL 的写操作需要绕过 DMU 直接写入数据块,所以 ZIL 本身是以日志系统的方式组织的,每次写
ZIL 都是在已经分配的 ZIL 块的末尾添加数据,分配新的 ZIL 块仍然需要经过 DMU
的空间分配。</p>
<p>传统日志型文件系统中对 data 开启日志支持会造成每次文件系统写入操作需要写两次到设备上,
一次写入日志,再一次覆盖文件系统内容;在
ZIL 实现中则不需要重复写两次, DMU 让 SPA 写入数据之后 ZIL 可以直接记录新数据块的
block pointer ,所以使用 ZIL 不会导致传统日志型文件系统中双倍写入放大的问题。</p>
</div>
<div class="section" id="zvol">
<h2><a class="toc-backref" href="#id18">ZVOL</a></h2>
<p>ZFS VOLume</p>
<p>有点像 loopback block device ,暴露一个块设备的接口,其上可以创建别的
FS 。对 ZFS 而言实现 ZVOL 的意义在于它是比文件更简单的接口,所以在实现完整 ZPL
之前,一开始就先实现了 ZVOL ,而且
<a class="reference external" href="https://youtu.be/xMH5rCL8S2k?t=298">早期 Solaris 没有 thin provisioning storage pool 的时候可以用 ZVOL 模拟很大的块设备,当时 Solaris 的 UFS 团队用它来测试 UFS 对 TB 级存储的支持情况</a>
。</p>
<p>因为 ZVOL 基于 DMU 上层,所以 DMU 所有的文件系统功能,比如 snapshot / dedup / compression
都可以用在 ZVOL 上,从而让 ZVOL 上层的传统文件系统也具有类似的功能。并且 ZVOL 也具有了 ARC
缓存的能力,和 dedup 结合之下,非常适合于在一个宿主机 ZFS
上提供对虚拟机文件系统镜像的存储,可以节省不少存储空间和内存占用开销。</p>
</div>
<div class="section" id="zpl">
<h2><a class="toc-backref" href="#id19">ZPL</a></h2>
<p>ZFS Posix Layer</p>
<p>提供符合 POSIX 文件系统语义的抽象,也就是包括文件、目录、软链接、套接字这些抽象以及
inode 访问时间、权限那些抽象,ZPL 是 ZFS 中对一个普通 FS 而言用户直接接触的部分。
ZPL 可以说是 ZFS 最复杂的子系统,也是 ZFS 作为一个文件系统而言最关键的部分。</p>
<p>ZPL 的实现中直接使用了 ZAP 和 DMU 提供的抽象,比如每个 ZPL 文件用一个 DMU 对象表达,每个
ZPL 目录用一个 ZAP 对象表达,然后 DMU 对象集对应到 ZPL 下的一个文件系统。
也就是说 ZPL 负责把操作系统 VFS 抽象层的那些文件系统操作接口,翻译映射到基于 DMU 和 ZAP
的抽象上。传统 Unix 中的管道、套接字、软链接之类的没有什么数据内容的东西则在 ZPL 直接用 dnode
实现出来。 ZPL 也需要进一步实现文件权限、所有者、访问日期、扩展属性之类杂七杂八的文件系统功能。</p>
<div class="label label-warning">
<strong>2020年2月9日添加</strong></div>
<p>继续上述 ZAP 格式变化的讨论,在 ZPL 抛弃早期用 ZAP 的设计之后, ZPL 中 znode (ZPL 扩展的 dnode)
保存文件属性的机制成为了一个小的子系统,叫
<a class="reference external" href="https://github.com/illumos/illumos-gate/blob/master/usr/src/uts/common/fs/zfs/sa.c">ZFS System Attributes</a>
。 SA 的设计照顾了旧版 ZPL znode 格式兼容问题,有新旧两代格式。旧版 znode
格式是固定偏移位置存取属性的 SA ,因此透过预先注册好的描述旧版 znode 格式的固定映射表,
SA 依然能用同样的代码路径存取旧版的 znode 。而后来
<a class="reference external" href="https://utcc.utoronto.ca/~cks/space/blog/solaris/ZFSSystemAttributes">灵活的新设计下的 SA 更有意思</a>
,ZFS 认识到,大部分 znode 的属性都可以用有限的几种属性集来表达,
比如普通文件有一组类似的属性(权限、所有者之类的), zvol 有另一组(明显 zvol 不需要很多 ZPL
文件的属性),整个 ZFS dataset 可以「注册」几种属性布局,然后让每个 znode 引用其中一种布局,
这样 znode 保存的属性仍然是可以任意变化的,又不需要在每个 znode 中都记录所有属性的名字。
SA 的出现提升了 ZPL 的可扩展性。 ZPL 为了应付不同的操作系统之间文件系统 API 的差异,可以使用
SA 在 znode 之中加入针对不同操作系统和应用场景的属性。例如,在支持 NFSv4 ACL 的操作系统上,ZFS
既可以用现有方式把 DACL ACEs 放在独立于文件对象的单独对象中,也可以把 DACL ACEs 放在 SA 内。</p>
<p><a class="reference external" href="https://youtu.be/xMH5rCL8S2k?t=456">在 ZFS First Mount by Mark Shellenbaum</a>
中介绍了很多在最初实现 ZPL 过程中的坎坷, ZPL 的困难之处在于需要兼容现有应用程序对传统文件系统
API 的使用方式,所以他们需要大量兼容性测试。视频中讲到非常有意思的一件事是, ZFS
在设计时不想重复 Solaris UFS 设计中的很多缺陷,于是实现 VFS API 时有诸多取舍和再设计。
其中他们遇到了 <code class="code">
VOP_RWLOCK</code>
,这个是 UFS 提供的文件级别读写锁。对一些应用尤其是
NFS 而言,文件读写锁能保证应用层的一致性,而对另一些应用比如数据库而言,
文件锁的粒度太大造成了性能问题。在设计 ZPL 的时候他们不想在 ZFS 中提供 <code class="code">
VOP_RWLOCK</code>
,这让 NFS 开发者们很难办(要记得 NFS 也是 Solaris 对 Unix 世界贡献的一大发明)。
最终 ZFS 把 DMU 的内部细节也暴露给了 NFS ,让 NFS 基于 DMU 的对象创建时间( TXG id
)而不是文件锁来保证 NFS 的一致性。结果是现在 ZFS 中也有照顾 NFS 的代码,后来也加入了
Samba/CIFS 的支持,从而在 ZFS 上设置 NFS export 时是通过 ZFS 的机制而非系统原生的 NFS
export 机制。</p>
</div>
和萌狼交换问题2020-01-05T17:51:00+09:002020-01-05T17:51:00+09:00farseerfctag:farseerfc.me,2020-01-05:/zhs/question-exchange-horo.html<p>很抱歉萌狼很早就提过交换问题的事,被我一直咕咕了许久。 <del>拖延症晚期有药么</del></p>
<div class="section" id="id2">
<h2>我的提问和萌狼的回答</h2>
<p><a class="reference external" href="https://blog.yoitsu.moe/life/question_exchange_farseerfc.html">可以去萌狼的博客上看呀</a></p>
</div>
<div class="section" id="q1">
<h2>Q1:除了博客的「关于」页面以外,还愿意再向咱介绍一下自己嘛?</h2>
<p>介绍自己啊。 <del>写了删删了写,不知道该介绍点啥</del> 就说点自己的兴趣?</p>
<p>喜欢自由开源软件,喜欢 Arch Linux 。喜欢这些倒不是出于 RMS 和 FSF 那样道义上的原因,
我觉得商业软件公司要赚钱吃饭也是无可厚非的。</p>
<p>喜欢自由软件是因为,当我需要知道它到底怎么工作的时候,有可能去挖代码,必要的话能去改代码。
当然我一个人肯定不能读所有在用的软件,但是我知道我有读和修改代码的权利的话,
那么我认识的朋友们也同样有这样的权利,我不认识的广大社区有千千万万的人也同样有这样的权利,
从而我相信当我遇到问题的时候不至于卡在某些人某些公司某些集体的决策上而无法解决。</p>
<p>基于这个理由,我对开源社区也同样有公开全部细节的期待。我喜欢 Arch Linux
因为即便它的内部决策只是一小波人,但是导致决策的讨论以及决策的执行方式全是公开的,可以在网上翻阅,
可以追根溯源,这让我有种安心感。就像我不喜欢 Manjaro 的一点是它有太多细节是翻阅不到的,
虽然它也是开源社区,但是打包细节翻阅不到,包列表翻阅不到,决策的制定和执行的过程也翻阅不到 …</p></div><p>很抱歉萌狼很早就提过交换问题的事,被我一直咕咕了许久。 <del>拖延症晚期有药么</del></p>
<div class="section" id="id2">
<h2>我的提问和萌狼的回答</h2>
<p><a class="reference external" href="https://blog.yoitsu.moe/life/question_exchange_farseerfc.html">可以去萌狼的博客上看呀</a></p>
</div>
<div class="section" id="q1">
<h2>Q1:除了博客的「关于」页面以外,还愿意再向咱介绍一下自己嘛?</h2>
<p>介绍自己啊。 <del>写了删删了写,不知道该介绍点啥</del> 就说点自己的兴趣?</p>
<p>喜欢自由开源软件,喜欢 Arch Linux 。喜欢这些倒不是出于 RMS 和 FSF 那样道义上的原因,
我觉得商业软件公司要赚钱吃饭也是无可厚非的。</p>
<p>喜欢自由软件是因为,当我需要知道它到底怎么工作的时候,有可能去挖代码,必要的话能去改代码。
当然我一个人肯定不能读所有在用的软件,但是我知道我有读和修改代码的权利的话,
那么我认识的朋友们也同样有这样的权利,我不认识的广大社区有千千万万的人也同样有这样的权利,
从而我相信当我遇到问题的时候不至于卡在某些人某些公司某些集体的决策上而无法解决。</p>
<p>基于这个理由,我对开源社区也同样有公开全部细节的期待。我喜欢 Arch Linux
因为即便它的内部决策只是一小波人,但是导致决策的讨论以及决策的执行方式全是公开的,可以在网上翻阅,
可以追根溯源,这让我有种安心感。就像我不喜欢 Manjaro 的一点是它有太多细节是翻阅不到的,
虽然它也是开源社区,但是打包细节翻阅不到,包列表翻阅不到,决策的制定和执行的过程也翻阅不到,
通常就只是在他们的论坛上发个通知了事,这我很不喜欢。</p>
<p>除了喜欢自由开源软件之外,可能我在网上比较有特点的地方是用繁体字了吧,
也曾经年幼时在水木社区和别人因为这个吵过嘴,也在
<a class="reference external" href="https://zhuanlan.zhihu.com/p/24586802">知乎上写过篇「在知乎用繁体字是怎样一种体验」</a> 。
致力于在我存在的地方为繁体字爱好者们提供一个安逸的环境,不过好像最近也不见很多反对的声音了。</p>
<p>除了网上之外,现实中的自己嘛,特点可能算是不知道自己属于哪儿了……一个漂泊的人。
小时候8岁前在陕西长大,把自己当作陕西人,但是身边的邻里街坊们却以河南人和江浙人居多。
厂办环境,好几个大型重工都从江浙搬到了陕西秦川一带,加上国共内战的时候河南黄河缺口造成的难民慌西逃,
构成了当时厂办的主要人口拿着城市户口,反而是当地的陕西人都是农民户口,
于是和厂办子弟们形成了鲜明的隔阂。我对社会主义,对苏式厂办,对整个国家结构的理解大概也是从那儿来的。
跟着邻里们学会了河南话,在家里说普通话,从老一辈们身上又学会了江浙的语调。
都说一个厂办是一个社会的缩影,那时候的环境可能算聚集了全国东南西北的样子吧。
8、9岁左右随父母到了上海,因为不会说上海话受同学们排挤,倒也不是很在意,渐渐和同学们学起了上海话,
可能还参杂点爷爷奶奶的江苏方言。十多年后考入大学,五湖四海的同学都有,就不算是在上海了。
大学毕业来了日本,一晃又是7年过去。至此我大概比起同龄人接触到更多全国各地的人,
也分不清自己的归属地了。但有一条,我知道自己是个中国人,为自己是个中国人自豪,觉得虽在他乡,
该为中国做点自己的贡献。</p>
</div>
<div class="section" id="q2">
<h2>Q2:现在这个名字是怎么想到的呢?</h2>
<p>farseerfc 这个名字嘛,来自 firechild 这个更早的网名,和魔兽争霸里面 farseer
这个英雄。 farseer 本算是 <a class="reference external" href="https://en.wikipedia.org/wiki/Linguistic_purism_in_English">Anglish</a>
,以日耳曼语系的构词法再造的英语词,对应拉丁构词法的话 far = tele , seer = visioner
,于是 farseer 也就是 tele-visioner ,看得远的人,电视一词 television 的原本的词干的衍生词。
不过说为什么选 farseer 这个名字,更多是为了符合 fc 这个缩写,而 fc 来自 firechild 这个词。
再深挖黑历史也不再有什么意义了, farseerfc 作为网名只是一直以来的习惯吧。</p>
</div>
<div class="section" id="q3">
<h2>Q3:觉得咱俩之间最令汝印象深刻的时候是什么?</h2>
<p>近期来看,印象最深刻的可能算是起草 <a class="reference external" href="https://fars.ee/~readme.html">Arch Linux 中文社区交流群指引</a>
吧,看得出萌狼对社区发展的热心和好意。</p>
<p>再往前,印象深刻的时候可能是萌狼用 Pelican 搭博客吧,最初认识萌狼的时候觉得是 MediaWiki
方面的行家,还以为博客也会继续用 MediaWiki 打造,没想到能吃了 Pelican
的安利,外加萌狼写博文的产量着实让人望尘莫及。</p>
<p>然后 ArchWiki 上 Beginner's Guide 被删除之后,萌狼的博客多了一篇为新人们写的入门安装手册,
配有完整截图指引,详尽程度令人感叹。感觉得到萌狼作为一个「过来人」对新人们的照顾。
每次群中闹起争执,老用户们对新人发起调侃的时候,也是萌狼站出来为新人们解围,
帮助有能力的人适应群里的讨论环境。或许最初写交流群指引的时候也是出于这样的良苦用心吧。</p>
</div>
<div class="section" id="q4">
<h2>Q4:对咱的印象怎么样?</h2>
<p>最早来 Arch Linux CN 的时候,似乎萌狼还不叫萌狼?不记得那时候用的名字了。只记得来自 AOSC
,和那边一众谈笑风声,着实令人羡慕,经常跑他们的聚会也令人羡慕。</p>
<p>后来有了萌狼的名字,群里的狼们也渐渐多了起来,一时间都分不清哪个狼是哪个了。
不过萌狼的口癖和说话方式总是在狼群中非常有标志性。</p>
<p>后来似乎发生了好多事情,我不知道的事情,也不敢妄加揣测。萌狼开始变身音游大佬,
群里的别的狼们渐渐也各忙东西。不知道什么原因,萌狼会偶尔退群,想问下前因后果,
又觉得自己不该多管闲事。不过无论萌狼退群多少次,总是在默默关心着社区发展,
关心着新人融入社区的环境。</p>
<p>似乎萌狼加入了 FSF ?玩起了 Parabola ,玩起了 linux-libre 。有能跑起完全自由的发行版的设备,
这一点也非常令人羡慕。似乎有很多设备,但是似乎又很不满于现状。看得出萌狼为了理想放弃了很多东西,
或许大家都是如此吧,也或许只是我多心。</p>
<p>还有就是萌狼用 Gnome ,感觉 AOSC 那边很多人都用 Gnome ,给 Gnome 贡献翻译之类的,
萌狼或许也是其中一员。DE 党争是水群久胜不衰的话题,或许我也有些责任,但是我觉得以发行版角度而言 DE
多样性非常重要,萌狼在社区中的作用也不可或缺。</p>
</div>
<div class="section" id="q5-gnu-linux">
<h2>Q5:在汝用过的 GNU/Linux 发行版之间汝最喜欢的是哪一个,为啥咧?</h2>
<p>最喜欢的当然是 Arch Linux 啦,喜欢的理由前面 Q1 算是提到了一些。其实别的发行版的很多特性也很眼馋,
眼馋 Fedora Silverblue 的 A/B 更新机制,眼馋 Fedora 的 SELinux 和诸多企业级特性支援,眼馋
openSUSE 的 OBS 和 btrfs 支持,眼馋 debian 的小巧和细化打包,眼馋 NixOS 的函数式包管理,
眼馋 Gentoo 的可定制性,眼馋 Parabola / GuixSD 的完全自由。</p>
<p>但是总得来说, Arch Linux 提供的基础足够让我折腾系统成自己喜欢的方式,足够顺手,
也在需要软件的时候足够自己打包使用,不需要等待某些远在天边的议会做决策,或许是让我留在
Arch Linux 的原因吧(当然更大原因可能是因为惯性)。发行版之间的技术区别可能并不那么重要,
重要的是该干活的时候能找到干活的人,这一点 Arch Linux 还是有很多人在认真做事情的。
没有繁琐的议会投票表决,没有细碎的打包步骤,用最快的方式把活干了,这在我看来是最重要的。</p>
<p>或许有一天,干活的人没了,或者我想要的特殊特性因为太复杂没人想带头干,而别的发行版有,
那时可能我会换去别的发行版吧。又或许我会自己干,谁知道呢。</p>
<p>比起发行版之争,甚至比起 Linux/Windows/macOS 的桌面系统地位之争,可能日后更关键的是别的平台
比如 Android 在手持设备甚至物联网设备上的兴起导致的 PC 桌面的衰落。虽然这些新设备大多都是跑着
Linux 的内核,但是其上的生态环境不能说像 GNU/Linux 那样自由。这一点上,自由软件该如何发挥优势
争取用户和生态可能是更关键的。</p>
<p>当然这些都于我而言过于遥远,一人之力难挽狂澜……我只希望自己和朋友们所在的自由的土地能保持下去,
或许我也仅能做到这些。</p>
</div>
<div class="section" id="q6-arch-linux-trusted-users">
<h2>Q6:在 Arch Linux 做 Trusted Users 时有没有什么心得?</h2>
<p>说来非常惭愧,做 TU 这么4年了,实际做的事情着实有限,只能隔几天打打包而已。要做的事情太多,
而自己上面也说了有干活的人最重要,设身处地深刻体会到在开源社区的诸位志愿者们大家都不容易。</p>
<p>TU 应该做的事情,细数一下除了给 community 打包之外,还有处理包的 bug ,处理 AUR 的争议,
测试新包给反馈,以及沟通和反馈上游。反观自己做的事情,真的太少了。比起肥猫和其他 TU 们的辛勤,
总觉得自己不够格。「精力有限,凭着志愿者热情」,什么的说辞可以说很多,
但是良心上对着自己热爱的事情却不能百分百扑上去做,真的没有颜面腆着脸说……</p>
<p>打包和沟通上游之类的心得倒是有不少,也一直想写点笔记记录一下,挖坑却没时间填上。该说,
或许应该换个本职工作了,又想,孰重孰轻哪边是本行需要自己掂量。</p>
</div>
<div class="section" id="q7">
<h2>Q7:有什么话要对咱说嘛?</h2>
<p>不知何时起,不知萌狼经历了什么,有时候感觉萌狼傲娇的性格让人看不透,不过事后能看出萌狼都是本着好心。
或许,如果能更坦诚一些的话,也能更融入大家吧。虽然我也没资格这么说。</p>
<p>像前面写的,隐约能感觉到萌狼似乎为了理想放弃了很多,孰重孰轻是每个人自己的权衡。</p>
<p>以及还有感谢,感谢萌狼把我当作朋友,感谢萌狼的耐心。</p>
<p>最后还有抱歉,这篇拖了太久,是该治治我的拖延症了。</p>
</div>
东方歌词翻译迁移至 sak.uy2019-02-24T08:37:00+09:002019-02-24T08:37:00+09:00farseerfctag:farseerfc.me,2019-02-24:/zhs/move-lyrics-to-sakuy.html<p>最近几个月在这个博客发了不少歌词翻译 <del>似乎有要转型成音乐博主的趋势</del> ,前段时间买了个新域名
<a class="reference external" href="https://sak.uy/">sak.uy</a> ,准备专门用来放这些东方歌曲的歌词翻译,于是分设了单独的博客「
<a class="reference external" href="https://sak.uy/">Sakuya的音乐盒</a> 」。主博客这边右侧边栏会有到音乐盒的链接。</p>
<p>曾经在这边的那些歌尽量保持 URL 跳转过去,新的歌词翻译会发到那边去,还想继续听歌的话请继续订阅那边的 RSS 呀。</p>
<p>主博客这边还是像往常一样保持记录生活点滴和技术经验好了。说道介绍技术,
有人问过我那些日语歌词上给汉字标注的假名都是我一个个手输的么?
一开始是手输的,后来发现了不错的自动化方案,于是这里介绍一下。</p>
<div class="section" id="python-furigana">
<h2>首先是 python-furigana</h2>
<p>这是个 python 写的小程序(严格说是库),可以把一段日文转换成标准的 HTML 形式的
<code class="code">
<ruby></code>
标签的振假名( <ruby><rb>振</rb><rp>(</rp><rt>ふ</rt><rp>)</rp></ruby> り <ruby><rb>仮名</rb><rp>(</rp><rt>かな</rt><rp>)</rp></ruby> )。
它本身只是个方便的格式化库,实际工作是用 python-mecab 这个 binding 去查询 mecab
这个著名的日语语料分析库。要用它还得配合一些开源的 mecab 词典,这些在 …</p></div><p>最近几个月在这个博客发了不少歌词翻译 <del>似乎有要转型成音乐博主的趋势</del> ,前段时间买了个新域名
<a class="reference external" href="https://sak.uy/">sak.uy</a> ,准备专门用来放这些东方歌曲的歌词翻译,于是分设了单独的博客「
<a class="reference external" href="https://sak.uy/">Sakuya的音乐盒</a> 」。主博客这边右侧边栏会有到音乐盒的链接。</p>
<p>曾经在这边的那些歌尽量保持 URL 跳转过去,新的歌词翻译会发到那边去,还想继续听歌的话请继续订阅那边的 RSS 呀。</p>
<p>主博客这边还是像往常一样保持记录生活点滴和技术经验好了。说道介绍技术,
有人问过我那些日语歌词上给汉字标注的假名都是我一个个手输的么?
一开始是手输的,后来发现了不错的自动化方案,于是这里介绍一下。</p>
<div class="section" id="python-furigana">
<h2>首先是 python-furigana</h2>
<p>这是个 python 写的小程序(严格说是库),可以把一段日文转换成标准的 HTML 形式的
<code class="code">
<ruby></code>
标签的振假名( <ruby><rb>振</rb><rp>(</rp><rt>ふ</rt><rp>)</rp></ruby> り <ruby><rb>仮名</rb><rp>(</rp><rt>かな</rt><rp>)</rp></ruby> )。
它本身只是个方便的格式化库,实际工作是用 python-mecab 这个 binding 去查询 mecab
这个著名的日语语料分析库。要用它还得配合一些开源的 mecab 词典,这些在 <code class="code">
[archlinuxcn]</code>
都有打好的包了,直接安装:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> sudo pacman -Syu python-furigana mecab-git python-mecab mecab-ipadic</span>
</pre></div>
<p>装好之后用法也很直接,甚至没有 binary 直接调用 python 的 module 就可以:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> python -m furigana.furigana <span class="s2">"振り仮名の例"</span></span>
<span class="code-line"><span class="go"><ruby><rb>振</rb><rt>ふ</rt></ruby>り<ruby><rb>仮名</rb><rt>かめい</rt></ruby>の<ruby><rb>例</rb><rt>れい</rt></ruby></span></span>
</pre></div>
<p>就是提供日语作为输入,然后输出 HTML 形式的 <code class="code">
<ruby></code>
标签而已。
像上面的例子中出现的错误(「振り仮名」完整的一个词中「仮名」意思是「平仮名」应该发音「がな」而非意为「假的人名」的「かめい」)
可以看出其实标注的准确率还是有些问题的。嘛日语作为一个非常依赖上下文判断的语言,
经常日本人都会搞错某些汉字的发音,这些也不能强求机械化的算法能 100% 正确实现。
好在单纯的词典匹配也能满足大部分标注的需要了,用这个标注总体来说 95%
以上的情况都是正确的(歌词的话正确率低一些,毕竟歌词中古语啦当て字啦训読み这些情况很常见)。</p>
</div>
<div class="section" id="id2">
<h2>把输出插入我的博客</h2>
<p>然后我的博客用 reStructuredText 语法写,不能直接用 HTML 标签(虽然我加了 <code class="code">
:html:</code>
这个 <ruby><rb>行内角色</rb><rp>(</rp><rt>inline role</rt><rp>)</rp></ruby> 但是大量用也不方便)。这个博客一开始用
<a class="reference external" href="//farseerfc.me/zhs/redesign-pelican-theme.html#bootstrapify-twitter-bootstrap-rst-directives">Pelican 重写主题的时候</a>
我就实现了个自己的 <code class="code">
:ruby:</code>
<ruby><rb>行内角色</rb><rp>(</rp><rt>inline role</rt><rp>)</rp></ruby> 用来标发音,于是一段
sed 就能把 python-furigana 的输出转换成我用的 rst 语法:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> which clipboard Co Ci Ct</span>
<span class="code-line"><span class="go">clipboard: aliased to xclip -selection clipboard</span></span>
<span class="code-line"><span class="go">Co: aliased to clipboard -o</span></span>
<span class="code-line"><span class="go">Ci: aliased to clipboard -i</span></span>
<span class="code-line"><span class="go">Ct () {</span></span>
<span class="code-line"><span class="go"> t=$(mktemp /tmp/furigana-XXXX)</span></span>
<span class="code-line"><span class="go"> python -m furigana.furigana $(Co) | sed 's@<ruby><rb>@ :ruby:`@g;s@</rb><rt>@|@g;s@</rt></ruby>@` @g' | sponge $t</span></span>
<span class="code-line"><span class="go"> cat $t | tee /dev/tty | perl -pe 'chomp if eof' | Ci</span></span>
<span class="code-line"><span class="go">}</span></span>
</pre></div>
<p>上面这些 alias 在我的 <a class="reference external" href="https://git.io/fczsh">.bashrc</a> 中。有了这些之后,
我只要把需要标注的日语文本放入剪切版,执行 Ct ,再粘帖结果就好了。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> <span class="nb">echo</span> <span class="s2">"振り仮名の例"</span> <span class="p">|</span> Ci</span>
<span class="code-line"><span class="gp">$</span> Ct</span>
<span class="code-line"><span class="go">:ruby:`振|ふ` り :ruby:`仮名|かめい` の :ruby:`例|れい`</span></span>
</pre></div>
<p>然后所有那些歌词上标注的假名都是这样一句一句标注好之后,再手动校对修改的。</p>
</div>
用 usbip 转发 raspberry pi 的 USB 键盘鼠标给 Arch Linux 的 PC2019-02-07T02:14:00+09:002019-02-07T02:14:00+09:00farseerfctag:farseerfc.me,2019-02-07:/zhs/usbip-forward-raspberrypi.html<p>惠狐 <a class="reference external" href="/links.html#megumifox">megumifox</a> 写了篇 <a class="reference external" href="https://blog.megumifox.com/public/2019/02/06/%E7%94%A8pulseaudio%E5%B0%86%E7%94%B5%E8%84%91%E7%9A%84%E5%A3%B0%E9%9F%B3%E7%94%A8%E6%89%8B%E6%9C%BA%E6%94%BE%E5%87%BA%E6%9D%A5/">用PulseAudio将电脑的声音用手机放出来</a>
,文末提到想知道我怎么用树莓派转发 USB 的,于是写篇文章记录一下。</p>
<div class="section" id="id1">
<h2>起因</h2>
<p>家里有个装了 Arch Linux ARM 的树莓派3B 闲置着,装了 Arch Linux ARM 偶尔上电更新一下,
不过因为性能实在不适合做别的事情于是一直在吃灰。某日 <del>给老婆安利幻想万华镜</del><ins>和老婆看片</ins>
的时候, <del>老婆不吃安利于是迁怒键盘鼠标</del><ins>键盘鼠标被长长的 USB 线扯着感觉很难受</ins>
,于是偶发奇想,能不能利用一下树莓派的多达 4 个 USB 2.0 端口接鼠标键盘呢,
这样鼠标键盘就可以跟着树莓派来回走,不用拖着长长的 USB 线了。</p>
<p>上网搜了一下, Linux 环境有个 usbip 工具正好能做到这个。原理也很直观, usbip 能把 USB …</p></div><p>惠狐 <a class="reference external" href="/links.html#megumifox">megumifox</a> 写了篇 <a class="reference external" href="https://blog.megumifox.com/public/2019/02/06/%E7%94%A8pulseaudio%E5%B0%86%E7%94%B5%E8%84%91%E7%9A%84%E5%A3%B0%E9%9F%B3%E7%94%A8%E6%89%8B%E6%9C%BA%E6%94%BE%E5%87%BA%E6%9D%A5/">用PulseAudio将电脑的声音用手机放出来</a>
,文末提到想知道我怎么用树莓派转发 USB 的,于是写篇文章记录一下。</p>
<div class="section" id="id1">
<h2>起因</h2>
<p>家里有个装了 Arch Linux ARM 的树莓派3B 闲置着,装了 Arch Linux ARM 偶尔上电更新一下,
不过因为性能实在不适合做别的事情于是一直在吃灰。某日 <del>给老婆安利幻想万华镜</del><ins>和老婆看片</ins>
的时候, <del>老婆不吃安利于是迁怒键盘鼠标</del><ins>键盘鼠标被长长的 USB 线扯着感觉很难受</ins>
,于是偶发奇想,能不能利用一下树莓派的多达 4 个 USB 2.0 端口接鼠标键盘呢,
这样鼠标键盘就可以跟着树莓派来回走,不用拖着长长的 USB 线了。</p>
<p>上网搜了一下, Linux 环境有个 usbip 工具正好能做到这个。原理也很直观, usbip 能把 USB
端口上的数据封装成 IP 协议通过网络转发出去,从而两个网络间相互联通的电脑就可以远程转发 USB 了。
设置好的话,就像是一台 PC 多了几个位于树莓派上的 USB 端口,插上树莓派的 USB 设备统统作为 PC
的设备。</p>
<p>这篇文章假设有一个装了 Arch Linux 的 PC ,和一个装了 Arch Linux ARM 的树莓派,
并且两者间能通过网络互相访问到。别的发行版上大概也可以这么做,只是我没有试过。 usbip
工具似乎普遍被发行版打包了,除此之外需要的也只是 Linux 内核提供好的功能而已。</p>
</div>
<div class="section" id="arch-linux-arm">
<h2>设置 Arch Linux ARM 的树莓派端</h2>
<p>假设树莓派上面网络已经设置妥当,开机插电就能自动联网。接下来安装 usbip 工具:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> sudo pacman -Syu usbip</span>
</pre></div>
<p>然后需要记录一下树莓派的 IP 地址:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> ip addr</span>
<span class="code-line"><span class="go">3: wlan0: ......</span></span>
<span class="code-line"><span class="go">inet 192.168.0.117/24 brd 192.168.0.255 scope global noprefixroute wlan0</span></span>
<span class="code-line"><span class="go">......</span></span>
</pre></div>
<p>接下来给 udev 添加一个规则,当插入 usb 设备的时候,执行我的脚本 usbipall.sh
把 usb 设备通过 usbip 共享出去:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> cat /etc/udev/rules.d/usbipall.rules</span>
<span class="code-line"><span class="go">ACTION=="add", SUBSYSTEM=="usb", RUN+="/usr/bin/bash /usr/local/bin/usbipall.sh"</span></span>
</pre></div>
<p>这个 rules 文件 <a class="reference external" href="https://github.com/farseerfc/dotfiles/blob/master/usbiprpi/usbipall.rules">可以在我的 dotfiles 里面找到</a> 。</p>
<p>然后规则调用的 usbipall.sh 我这么写的, <a class="reference external" href="https://github.com/farseerfc/dotfiles/blob/master/usbiprpi/usbipall.sh">文件同样在我的 dotfiles 里面</a> :</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="ch">#!/bin/sh</span></span>
<span class="code-line"><span class="o">(</span></span>
<span class="code-line"><span class="nv">allusb</span><span class="o">=</span><span class="k">$(</span>usbip list -p -l<span class="k">)</span></span>
<span class="code-line"><span class="k">for</span> usb in <span class="nv">$allusb</span></span>
<span class="code-line"><span class="k">do</span></span>
<span class="code-line"> <span class="nv">busid</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$usb</span><span class="s2">"</span> <span class="p">|</span> sed <span class="s2">"s|#.*||g;s|busid=||g"</span><span class="k">)</span></span>
<span class="code-line"> <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"1-1.1"</span> <span class="o">]</span></span>
<span class="code-line"> <span class="k">then</span></span>
<span class="code-line"> <span class="c1"># ignoring usb ethernet</span></span>
<span class="code-line"> <span class="k">continue</span></span>
<span class="code-line"> <span class="k">fi</span></span>
<span class="code-line"> <span class="nb">echo</span> <span class="s2">"</span><span class="k">$(</span>date -Iseconds<span class="k">)</span><span class="s2">: Exporting </span><span class="nv">$busid</span><span class="s2">"</span></span>
<span class="code-line"> usbip <span class="nb">bind</span> --busid<span class="o">=</span><span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span></span>
<span class="code-line"><span class="k">done</span></span>
<span class="code-line"><span class="o">)</span> >>/var/log/usbipall.log <span class="m">2</span>><span class="p">&</span><span class="m">1</span></span>
</pre></div>
<p>这个脚本做了这样几件事。</p>
<ol class="arabic simple">
<li>调用 <code class="code">
usbip list --local</code>
列出本地所有 usb 设备。</li>
<li>针对每个设备<ol class="arabic">
<li>取出它的 busid</li>
<li>判断是不是树莓派的 USB 以太网卡,不是的话继续</li>
<li>通过 <code class="code">
usbip bind --busid=</code>
命令把这个 usb 设备导出到网上</li>
</ol>
</li>
<li>最后把所有输出记录到 /var/log/usbipall.log 日志里面</li>
</ol>
<p>树莓派这边设置就完成了。从此之后插入的 usb 设备就会统统导出出去。</p>
<p>这里需要注意一下,启用了 udev 规则之后,就没法插键盘鼠标到树莓派上控制它了……我都是从另一端 ssh
上树莓派操作的。如果有什么地方设置错误,可能需要把树莓派的 SD 卡拔下来插到电脑上,删除掉 rules
文件……</p>
<p>仔细检查设置正确了之后,重新载入 udev 规则,或者重启树莓派:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">#</span> systemctl restart systemd-udevd</span>
</pre></div>
<p>这样树莓派这边就设置好了。</p>
</div>
<div class="section" id="arch-linux-pc">
<h2>设置 Arch Linux 的 PC 端</h2>
<p>同样假设 PC 这边也已经联网。接下来同样安装 usbip 工具:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> sudo pacman -Syu usbip</span>
</pre></div>
<p>然后我写了个小脚本去链接树莓派端, <a class="reference external" href="https://github.com/farseerfc/dotfiles/blob/master/usbiprpi/usbiprpi3.sh">这个文件 usbiprpi3.sh 也在我的 dotfiles</a>:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="ch">#!/bin/sh</span></span>
<span class="code-line"><span class="nv">rpi3</span><span class="o">=</span><span class="s2">"192.168.0.117"</span></span>
<span class="code-line"></span>
<span class="code-line">modprobe vhci-hcd</span>
<span class="code-line"></span>
<span class="code-line"><span class="nv">allusb</span><span class="o">=</span><span class="k">$(</span>usbip list -p -r <span class="nv">$rpi3</span> <span class="p">|</span> cut -d<span class="s2">":"</span> -f1 -s <span class="p">|</span> sed <span class="s1">'s|^[ \t]*||;/^$/d'</span><span class="k">)</span></span>
<span class="code-line"><span class="k">for</span> busid in <span class="nv">$allusb</span></span>
<span class="code-line"><span class="k">do</span></span>
<span class="code-line"> <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"1-1.1"</span> <span class="o">]</span></span>
<span class="code-line"> <span class="k">then</span></span>
<span class="code-line"> <span class="c1"># ignoring usb ethernet</span></span>
<span class="code-line"> <span class="k">continue</span></span>
<span class="code-line"> <span class="k">fi</span></span>
<span class="code-line"> <span class="nb">echo</span> <span class="s2">"Attaching </span><span class="nv">$busid</span><span class="s2">"</span></span>
<span class="code-line"> usbip attach --remote<span class="o">=</span><span class="nv">$rpi3</span> --busid<span class="o">=</span><span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span></span>
<span class="code-line"><span class="k">done</span></span>
</pre></div>
<p>其中脚本第一行填入上面记录下来的树莓派的 IP 地址,接下来脚本做了这么几件事:</p>
<ol class="arabic simple">
<li>用 modprobe 确认加载 vhci-hcd 通用虚拟键鼠驱动</li>
<li>用 <code class="code">
usbip list --remote=</code>
列出远程设备上已经导出了的 USB 设备,取出他们的 busid</li>
<li>对每个设备用 <code class="code">
usbip attach</code>
接上该设备</li>
</ol>
<p>然后就已经准备妥当,接下来是见证奇迹的时刻:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> sleep <span class="m">10</span><span class="p">;</span> sudo ./usbiprpi3.sh</span>
<span class="code-line"><span class="go">Attaching 1-1.4.3</span></span>
<span class="code-line"><span class="go">Attaching 1-1.4.1</span></span>
</pre></div>
<p>因为只有一套键盘鼠标,所以先 sleep 个 10 秒,在此期间快速把键鼠拔下来插到树莓派的 USB 口上去。
如果对自己手速没自信也可以把时间设长一点。然后用 root 权限执行 usbiprpi3.sh 。</p>
<p>一切正常的话,先能观测插上树莓派的键盘鼠标被树莓派初始化了一下,比如键盘灯会亮,
然后这些设备会被导出出去,从而键盘灯灭掉,然后 10 秒等待结束后他们被远程接到了 PC 端,
又会被初始化一下,同时 PC 端这边会有上述 Attaching 的输出。然后键盘鼠标就能像平常一样用啦。</p>
</div>
<div class="section" id="id3">
<h2>使用体验</h2>
<p>因为就是通过 IP 转发 USB 嘛,所以就和普通地接 USB 的体验差不多,当然前提是网络环境足够稳定。
在我家间隔 5 米到无线路由器的环境下,基本感觉不到网络延迟的影响。
通过这种方式聊天上网应该和直接接 USB 设备完全一样。本文就是在通过树莓派转发的前提下用键盘打字写的。</p>
<p>不过如果网络负载本身就很大的话,可能会一些延迟,比如我开着 OBS 直播打东方的时候,原本就手残
的我感觉更加手残了……</p>
<p>试过拿着树莓派在房间到处走,走到无线信号覆盖不到的地方, usbip 会断掉,PC 上的现象就像是 USB
设备被拔下来了……所以如果无线网络不稳的话,可能需要对上面脚本做个循环?不过那样可能会用起来很别扭吧。</p>
<p>以及,上述操作 usbip 是走 TCP 3240 端口,数据包大概完全没有加密,所以考虑安全性的话,
最好还是在内网环境使用。不过转念一想,万一有别人接上了我导出出去的 USB ,也就是截获我的键盘,
PC 这边没法 attach 设备了,应该马上会发现吧。我敲打 sudo 之类命令的时候 shell 里面没有回显,
就不会再继续敲密码了。而且似乎对攻击者也没有什么好处?要是他 usb attach 到了我的设备上,
我就能控制他的键盘了耶~</p>
</div>
【听译】君さえいなけりゃよかった2018-12-23T23:04:00+09:002018-12-23T23:04:00+09:00farseerfctag:farseerfc.me,2018-12-23:/zhs/kimisaeinakerya.html<div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/-KOeQapXsx8"></iframe></div><table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君さえいなけりゃよかった</td>
<td>如果你从未出现过该多好</td>
</tr>
<tr><td>降り出した雨の中で 君に出会った时から</td>
<td>下起雨的那一刻 从遇到你那时起</td>
</tr>
<tr><td>君がいないということが 当たり前じゃなくなった</td>
<td>身边没有你的情况 就已经不再是平常</td>
</tr>
<tr><td>ああ こんなはずじゃない</td>
<td>啊 不应该是这样的</td>
</tr>
<tr><td>ずっと自分胜手にさ 过ごせたはずなのに</td>
<td>明明一直是散漫地过着自己的日子</td>
</tr>
<tr><td>まるで仆じゃないような仆が さらけ出されてくよ</td>
<td>就像是带出了不是我的另一面的我</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君さえいなけりゃよかった こんな気持ちは知らないから</td>
<td>如果你从未出现过该多好 就不会知道这种心情</td>
</tr>
<tr><td>やらなくちゃいけないことが 手つかずのまま积もってく</td>
<td>一堆不得不做的事情 堆在手头越积越多</td>
</tr>
<tr><td>仆じゃなくてもいいのなら こっちを见て笑わないでよ</td>
<td>如果不是我也可以的话 就别看着我这边笑啊</td>
</tr>
<tr><td>大袈裟じゃなくてそれだけで 忘れられなくなるの</td>
<td>甚至那些不重要的事情 都变得难以忘记了</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君の适当な话も 全部心に刺さります</td>
<td>你无意间随口说的话 全都刺在心头</td>
</tr>
<tr><td>気にしなけりゃいいのにな 残らずかき集めちゃうの</td>
<td>虽说只要不在意就可以了 却一句不剩全收集了起来</td>
</tr>
<tr><td>ああ こんなはずじゃない こんなはずじゃない</td>
<td>啊 不应该是这样的 不应该是这样的 …</td></tr></tbody></table><div align="left" class="youtube embed-responsive embed-responsive-16by9"><iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/-KOeQapXsx8"></iframe></div><table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君さえいなけりゃよかった</td>
<td>如果你从未出现过该多好</td>
</tr>
<tr><td>降り出した雨の中で 君に出会った时から</td>
<td>下起雨的那一刻 从遇到你那时起</td>
</tr>
<tr><td>君がいないということが 当たり前じゃなくなった</td>
<td>身边没有你的情况 就已经不再是平常</td>
</tr>
<tr><td>ああ こんなはずじゃない</td>
<td>啊 不应该是这样的</td>
</tr>
<tr><td>ずっと自分胜手にさ 过ごせたはずなのに</td>
<td>明明一直是散漫地过着自己的日子</td>
</tr>
<tr><td>まるで仆じゃないような仆が さらけ出されてくよ</td>
<td>就像是带出了不是我的另一面的我</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君さえいなけりゃよかった こんな気持ちは知らないから</td>
<td>如果你从未出现过该多好 就不会知道这种心情</td>
</tr>
<tr><td>やらなくちゃいけないことが 手つかずのまま积もってく</td>
<td>一堆不得不做的事情 堆在手头越积越多</td>
</tr>
<tr><td>仆じゃなくてもいいのなら こっちを见て笑わないでよ</td>
<td>如果不是我也可以的话 就别看着我这边笑啊</td>
</tr>
<tr><td>大袈裟じゃなくてそれだけで 忘れられなくなるの</td>
<td>甚至那些不重要的事情 都变得难以忘记了</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君の适当な话も 全部心に刺さります</td>
<td>你无意间随口说的话 全都刺在心头</td>
</tr>
<tr><td>気にしなけりゃいいのにな 残らずかき集めちゃうの</td>
<td>虽说只要不在意就可以了 却一句不剩全收集了起来</td>
</tr>
<tr><td>ああ こんなはずじゃない こんなはずじゃない</td>
<td>啊 不应该是这样的 不应该是这样的</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>君に出会わなきゃよかった こんなに寂しくなるのなら</td>
<td>如果没遇到过你该多好 就不会变得如此寂寞</td>
</tr>
<tr><td>君じゃなくてもいいことが もう见つからないの</td>
<td>已经找不到 和你无关也可以的情况了</td>
</tr>
<tr><td>忘れられないから 君じゃなかったら</td>
<td>无法忘记了 要不是你的话</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>いっそ见损なってしまうような そんなひとだったらなあ</td>
<td>干脆变成根本看不起的人 如果是那种人的话</td>
</tr>
<tr><td>でもそれでも どうせ无理そう 嫌いになれないや</td>
<td>但是即使如此 大概反正也不可能 无法变得讨厌</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>仆がいなくてもいいなら いっそ不幸になってしまえ</td>
<td>如果不是我也可以的话 干脆变得不幸吧</td>
</tr>
<tr><td>最后にまた仆の元に 泣きついてくればいい</td>
<td>最后还是会回到我身边 哭着凑过来的话就可以</td>
</tr>
<tr><td>君さえいなけりゃよかった こんな気持ちは知らないから</td>
<td>如果没有你该多好 就不会知道这种心情</td>
</tr>
<tr><td>やらなくちゃいけないことが 手つかずのまま积もってく</td>
<td>一堆不得不做的事情 堆在手头越积越多</td>
</tr>
<tr><td>仆じゃなくてもいいのなら こっちを见て笑わないでよ</td>
<td>如果不是我也可以的话 就别看着我这边笑啊</td>
</tr>
<tr><td>大袈裟じゃなくてそれだけで</td>
<td>甚至那些不重要的事情</td>
</tr>
<tr><td>君のこと 间违いなく</td>
<td>对你 毫无疑问</td>
</tr>
<tr><td>苦しいほど 好きになっちゃうよ</td>
<td>刻骨铭心地 变得喜欢上了啊</td>
</tr>
</tbody>
</table>
<hr class="docutils"/>
<table border="0" class="table docutils borderless translate-paragraph">
<colgroup>
<col width="50%"/>
<col width="50%"/>
</colgroup>
<tbody valign="top">
<tr></tr>
<tr><td>忘れられないから 君じゃなかったら</td>
<td>因为无法忘记 如果不是你的话</td>
</tr>
<tr><td>君に出会わなきゃ 仆じゃなかったら</td>
<td>要是没遇到过你 如果不是我的话</td>
</tr>
<tr><td>君さえいなけりゃよかった</td>
<td>如果你从未出现过该多好</td>
</tr>
</tbody>
</table>
【译】使用 GNU stow 管理你的点文件2018-12-08T03:35:00+09:002018-12-08T03:35:00+09:00farseerfctag:farseerfc.me,2018-12-08:/zhs/using-gnu-stow-to-manage-your-dotfiles.html<div class="section" id="id1">
<h2>译注</h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>这篇是翻译自 <a class="reference external" href="http://brandon.invergo.net/news/2012-05-26-using-gnu-stow-to-manage-your-dotfiles.html">Brandon Invergo 的博客的英文文章 Using GNU Stow to manage your dotfiles</a> 。
Brandon Invergo 的博客采用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/3.0/deed.en_US">CC-BY-SA 3.0</a>
授权,因此本文也同样采用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/3.0/deed.en_US">CC-BY-SA 3.0</a>
,不同于其它我写的文章是 <a class="reference external" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA 4.0</a>
授权。</p>
<p>我自己已经使用此文中介绍的方案管理 <a class="reference external" href="https://git.io/fcdots">我自己的 dotfiles</a>
快 3 年了。最早想采用这样的管理方案是为了方便在多台 Arch Linux 系统之间同步配置,
后来逐渐主力系统也更新换代了一次,又同步到了自己的 vps 上去,目前管理多个 Arch Linux
上都多少都有这套配置。甚至装好 Arch Linux 添加好用户最初做的事情就是安装 …</p></div><div class="section" id="id1">
<h2>译注</h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>这篇是翻译自 <a class="reference external" href="http://brandon.invergo.net/news/2012-05-26-using-gnu-stow-to-manage-your-dotfiles.html">Brandon Invergo 的博客的英文文章 Using GNU Stow to manage your dotfiles</a> 。
Brandon Invergo 的博客采用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/3.0/deed.en_US">CC-BY-SA 3.0</a>
授权,因此本文也同样采用 <a class="reference external" href="https://creativecommons.org/licenses/by-sa/3.0/deed.en_US">CC-BY-SA 3.0</a>
,不同于其它我写的文章是 <a class="reference external" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA 4.0</a>
授权。</p>
<p>我自己已经使用此文中介绍的方案管理 <a class="reference external" href="https://git.io/fcdots">我自己的 dotfiles</a>
快 3 年了。最早想采用这样的管理方案是为了方便在多台 Arch Linux 系统之间同步配置,
后来逐渐主力系统也更新换代了一次,又同步到了自己的 vps 上去,目前管理多个 Arch Linux
上都多少都有这套配置。甚至装好 Arch Linux 添加好用户最初做的事情就是安装 stow git
然后 clone 了我自己的 dotfiles repo 下来,然后按需取想要的配置,快捷方便有效。</p>
<!-- PELICAN_END_SUMMARY -->
<p>废话不多说,下面是原文和翻译。与之前的翻译一样,正文部分给出原文引用以便对照参考。</p>
</div>
<div class="section" id="id2">
<h2>使用 GNU stow 管理你的点文件</h2>
<a aria-controls="8b1882fc" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#8b1882fc" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="8b1882fc">
<blockquote>
I accidentally stumbled upon something yesterday that I felt like sharing,
which fell squarely into the "why the hell didn't I know about this before?"
category. In this post, I'll describe how to manage the various
configuration files in your GNU/Linux home directory
(aka "dotfiles" like .bashrc) using GNU Stow.</blockquote>
</div>
<p>我昨天偶然间发现一些我觉得值得分享的经验,就是那种「为毛我没有早点知道这个?」那一类的。
我将在这篇文章中介绍如何使用 GNU Stow 管理你的 GNU/Linux 系统中位于用户家目录里的各种配置文件
(通常又叫「点文件(dotfiles)」比如 .bashrc)。</p>
<a aria-controls="71af1930" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#71af1930" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="71af1930">
<blockquote>
The difficulty is that it would be helpful to manage one's configuration
files with a version control system like Git, Mercurial or Bazaar, but
many/most dotfiles reside at the top-level of your home directory,
where it wouldn't be a good idea to initialize a VCS repository.
Over time I've come across various programs which aim to manage this
for you by keeping all the files in a subdirectory and then installing or
linking them into their appropriate places. None of those programs ever
really appealed to me. They would require a ton of dependencies
(like Ruby and a ton of libraries for it) or they would require me to
remember how to use them, which is difficult when really for such a task
you rarely use the program.</blockquote>
</div>
<p>这件事的困难之处在于,如果能用版本管理系统(VCS, Version Control System)比如
Git, Mercurial(hg), Bazaar(bzr)
管理点文件的话会非常方便,但是这些点文件大部分都位于家目录的顶级目录下,
在这个位置不太适合初始化一个版本管理仓库。这些年下来我试过很多程序,设计目的在于解决这个问题,
帮你把这些配置文件安置在某个下级目录中,然后安装或者链接这些文件到它们应该在的位置。
尝试下来这些程序没有一个真正能打动我。它们要么有很多依赖(比如 Ruby 和一大坨库),
要么需要我记住如何用它,考虑到同步配置这种不算经常使用的场合,要记住用法真的挺难。</p>
<a aria-controls="f79eff90" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#f79eff90" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="f79eff90">
<blockquote>
Lately I've been using GNU Stow to manage programs I install from source to
/usr/local/. Basically, in this typical usage, you install locally built
packages to /usr/local/stow/${PKGNAME}-{PKGVERSION} and then
from /usr/local/stow/ you run # stow ${PKGNAME}-${PKGVERSION} and the
program generates symbolic links to all the programs' files into the
appropriate places under /usr/local/. Then, when you uninstall a program
via Stow, you don't have to worry about any stray files that you or a
provide Makefile may have missed. It also makes handling alternate versions
of a program quite easy (i.e. when I'm experimenting with different
configurations of dwm or st).</blockquote>
</div>
<p>最近我在用 GNU Stow 来管理我从源代码在本地编译安装到 <code class="code">
/usr/local/</code>
中的一些程序。
基本上说,在这种常见用法下,是你把这些本地编译的包配置安装到
<code class="code">
/usr/local/stow/${PKGNAME}-{PKGVERSION}</code>
这样的位置,然后在
<code class="code">
/usr/local/stow/</code>
目录中执行 <code class="code">
# stow ${PKGNAME}-${PKGVERSION}</code>
,然后它就会为程序所有的文件创建符号链接放在 <code class="code">
/usr/local</code>
中合适的地方。然后当你想用 Stow 卸载这个程序的时候,就不必再考虑会留下什么垃圾文件,
或者找不到安装时用的 Makefile 了。这种安装方式下也可以非常容易地切换一个程序的不同版本
(比如我想尝试不同配置选项下的 <a class="reference external" href="https://dwm.suckless.org/">dwm</a> 或者
<a class="reference external" href="https://st.suckless.org/">st</a> 的时候)。</p>
<a aria-controls="de09a7d8" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#de09a7d8" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="de09a7d8">
<blockquote>
Some time ago I happened across a mailing list posting where someone
described using Stow to manage the installation of their dotfiles.
I didn't pay much attention to it but my brain must have filed it away
for later. Yesterday I decided to give it a try and I have to say that
it is so much more convenient than those other dedicated dotfile-management
programs, even if it wasn't an immediately obvious option.</blockquote>
</div>
<p>前段时间在我扫邮件列表的时候,看到某个帖子中某人在说使用 Stow 管理安装他的点文件。
当时我没特别在意这个帖子,但是大概我大脑潜意识把它归档保存为今后阅读了。
昨天我想起来试试这种用法,试过后我不得不说,这比那些专门设计用来做这任务的点文件管理器要方便太多了,
虽然表面上看起来这种用法没那么显而易见。</p>
<a aria-controls="2e827562" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#2e827562" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="2e827562">
<blockquote>
The procedure is simple. I created the ${HOME}/dotfiles directory and then
inside it I made subdirectories for all the programs whose cofigurations
I wanted to manage. Inside each of those directories, I moved in all the
appropriate files, maintaining the directory structure of my home directory.
So, if a file normally resides at the top level of your home directory,
it would go into the top level of the program's subdirectory.
If a file normally goes in the default ${XDG_CONFIG_HOME}/${PKGNAME}
location (${HOME}/.config/${PKGNAME}), then it would instead go in
${HOME}/dotfiles/${PKGNAME}/.config/${PKGNAME} and so on.
Finally, from the dotfiles directory, you just run $ stow $PKGNAME and
Stow will symlink all the package's configuration files to the appropriate
locations. It's then easy to make the dotfiles a VCS repository so you can
keep track of changes you make (plus it makes it so much easier to share
configurations between different computers, which was my main reason to
do it).</blockquote>
</div>
<p>方法很简单。我建了个 <code class="code">
${HOME}/dotfiles</code>
文件夹,然后在里面为我想管理的每个程序配置都
创建一个子文件夹。然后我把这些程序的配置从原本的家目录移动到这每一个对应的子文件夹中,
并保持它们在家目录中的文件夹结构。比如,如果某个文件原本应该位于家目录的顶层文件夹里,
那它现在应该放在这个程序名子目录的顶层文件夹。如果某个配置文件通常应该位于默认的
<code class="code">
${XDG_CONFIG_HOME}/${PKGNAME}</code>
位置 (<code class="code">
${HOME}/.config/${PKGNAME}</code>
),
那么现在它应该放在 <code class="code">
${HOME}/dotfiles/${PKGNAME}/.config/${PKGNAME}</code>
,如此类推。然后在那个 dotfiles 文件夹里面,直接运行 <code class="code">
$ stow $PKGNAME</code>
命令,
Stow 就会为你自动创建这些配置文件的符号链接到合适的位置。接下来就很容易为这个 dotfiles
目录初始化版本管理仓库,从而记录你对这些配置文件做的修改(并且这也可以极度简化在不同电脑之间
共享配置,这也是我想要这么做的主要原因)。</p>
<a aria-controls="5e0e831d" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#5e0e831d" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="5e0e831d">
<blockquote>
For example, let's say you want to manage the configuration for Bash,
VIM and Uzbl. Bash has a couple files in the top-level directory;
VIM typically has your .vimrc file on the top-level and a .vim directory;
and Uzbl has files in ${XDG_CONFIG_HOME}/uzbl and ${XDG_DATA_HOME}/uzbl.
So, your home directory looks like this:</blockquote>
</div>
<p>举个例子,比如说你想管理 Bash, VIM, Uzbl 这三个程序的配置文件。Bash 会在家目录的顶层文件夹
放几个文件; VIM 通常会有在顶层文件夹的 .vimrc 文件和 .vim 目录;然后 Uzbl 的配置位于
<code class="code">
${XDG_CONFIG_HOME}/uzbl</code>
以及 <code class="code">
${XDG_DATA_HOME}/uzbl</code>
。于是在迁移配置前,你的家目录的文件夹结构应该看起来像这样:</p>
<div class="highlight"><pre><span class="code-line"><span></span>home/</span>
<span class="code-line"> brandon/</span>
<span class="code-line"> .config/</span>
<span class="code-line"> uzbl/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> .local/</span>
<span class="code-line"> share/</span>
<span class="code-line"> uzbl/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> .vim/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> .bashrc</span>
<span class="code-line"> .bash_profile</span>
<span class="code-line"> .bash_logout</span>
<span class="code-line"> .vimrc</span>
</pre></div>
<a aria-controls="8c2a2e91" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#8c2a2e91" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="8c2a2e91">
<blockquote>
You would then create a dotfiles subdirectory and move all the files there:</blockquote>
</div>
<p>然后迁移配置的方式是,应该建一个 dotfiles 子目录,然后像这样移动所有配置文件:</p>
<div class="highlight"><pre><span class="code-line"><span></span>home/</span>
<span class="code-line"> /brandon/</span>
<span class="code-line"> .config/</span>
<span class="code-line"> .local/</span>
<span class="code-line"> .share/</span>
<span class="code-line"> dotfiles/</span>
<span class="code-line"> bash/</span>
<span class="code-line"> .bashrc</span>
<span class="code-line"> .bash_profile</span>
<span class="code-line"> .bash_logout</span>
<span class="code-line"> uzbl/</span>
<span class="code-line"> .config/</span>
<span class="code-line"> uzbl/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> .local/</span>
<span class="code-line"> share/</span>
<span class="code-line"> uzbl/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> vim/</span>
<span class="code-line"> .vim/</span>
<span class="code-line"> [...some files]</span>
<span class="code-line"> .vimrc</span>
</pre></div>
<a aria-controls="a3ea20b7" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a3ea20b7" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="a3ea20b7">
<blockquote>
Then, perform the following commands:</blockquote>
</div>
<p>然后执行以下命令:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> <span class="nb">cd</span> ~/dotfiles</span>
<span class="code-line"><span class="gp">$</span> stow bash</span>
<span class="code-line"><span class="gp">$</span> stow uzbl</span>
<span class="code-line"><span class="gp">$</span> stow vim</span>
</pre></div>
<a aria-controls="6a03f8ee" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#6a03f8ee" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="6a03f8ee">
<blockquote>
And, voila, all your config files (well, symbolic links to them) are all
in the correct place, however disorganized that might be, while the actual
files are all neatly organized in your dotfiles directory, which is easily
turned into a VCS repo. One handy thing is that if you use multiple
computers, which may not have the same software installed on them,
you can pick and choose which configurations to install when you need them.
All of your dotfiles are always available in your dotfiles directory,
but if you don't need the configuration for one program, you simply don't
Stow it and thus it does not clutter your home directory.</blockquote>
</div>
<p>然后,瞬间,所有你的配置文件(的符号链接)就安安稳稳地放入了它们该在的地方,无论原本这些目录结构
有多么错综复杂,这样安排之后的 dotfiles 文件夹内的目录结构立刻整理得有条有理,
并且可以很容易地转换成版本控制仓库。非常有用的一点是,如果你有多台电脑,可能这些电脑并没有
安装完全一样的软件集,那么你可以手选一些你需要的软件配置来安装。在你的 dotfiles 文件夹中总是
可以找到所有的配置文件,但是如果你不需要某个程序的某份配置,那你就不对它执行 stow
命令,它就不会扰乱你的家目录。</p>
<a aria-controls="68a93e12" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#68a93e12" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="68a93e12">
<blockquote>
Well, that's all there is to it. Hopefully someone else out there finds
this useful! I know I've found it to be a huge help.</blockquote>
</div>
<p>嗯,以上就是整个用法介绍。希望能有别人觉得这个用法有用!我知道对我来说这个非常有帮助。</p>
</div>
为什么 Linus Torvalds 不愿意将 Linux 变成 GPLv3 授权?2016-08-08T16:15:00+09:002016-08-08T16:15:00+09:00farseerfctag:farseerfc.me,2016-08-08:/zhs/why-linus-torvalds-undermine-gplv3.html<p>从 <a class="reference external" href="https://www.zhihu.com/question/48884264/answer/113454129">知乎</a> 转载</p>
<p>和上篇文章一样,这篇也是来自一个知乎上我回答的问题。</p>
<p>原问题:为什么 Linus Torvalds 不愿意将 Linux 变成 GPLv3 授权?</p>
<div class="panel panel-default">
<div class="panel-heading">
DebConf 14: Q&A with Linus Torvalds</div>
<div class="panel-body">
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_1Mg5_gxNXTo">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/1Mg5_gxNXTo"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XMTY3NjIzNDU0NA">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XMTY3NjIzNDU0NA" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_1Mg5_gxNXTo">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XMTY3NjIzNDU0NA">Youku</a></li>
</ul>
</div>
</div>
</div>
<p>我的回答:</p>
<blockquote>
<p>这里有段 Linus Torvalds 在 DebConf 14 上的 Q&A:
<a class="reference external" href="https://youtu.be/1Mg5_gxNXTo?t=47m20s">https://youtu.be/1Mg5_gxNXTo?t=47m20s</a></p>
<p>其中关于 GPLv3 和协议的那一段在47:20开始到57:00左右。
里面 Linus 对自己的观点澄清得很清楚了。
看u2b或者听英语有困难的请留评论,我抽空可以试着翻译一下。</p>
</blockquote>
<div class="section" id="id2">
<h2>然后接下来就是我承诺的翻译了 …</h2></div><p>从 <a class="reference external" href="https://www.zhihu.com/question/48884264/answer/113454129">知乎</a> 转载</p>
<p>和上篇文章一样,这篇也是来自一个知乎上我回答的问题。</p>
<p>原问题:为什么 Linus Torvalds 不愿意将 Linux 变成 GPLv3 授权?</p>
<div class="panel panel-default">
<div class="panel-heading">
DebConf 14: Q&A with Linus Torvalds</div>
<div class="panel-body">
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_1Mg5_gxNXTo">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/1Mg5_gxNXTo"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XMTY3NjIzNDU0NA">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XMTY3NjIzNDU0NA" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_1Mg5_gxNXTo">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XMTY3NjIzNDU0NA">Youku</a></li>
</ul>
</div>
</div>
</div>
<p>我的回答:</p>
<blockquote>
<p>这里有段 Linus Torvalds 在 DebConf 14 上的 Q&A:
<a class="reference external" href="https://youtu.be/1Mg5_gxNXTo?t=47m20s">https://youtu.be/1Mg5_gxNXTo?t=47m20s</a></p>
<p>其中关于 GPLv3 和协议的那一段在47:20开始到57:00左右。
里面 Linus 对自己的观点澄清得很清楚了。
看u2b或者听英语有困难的请留评论,我抽空可以试着翻译一下。</p>
</blockquote>
<div class="section" id="id2">
<h2>然后接下来就是我承诺的翻译了</h2>
<a aria-controls="ebbd22ff" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#ebbd22ff" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="ebbd22ff">
<blockquote>
Q: Do you agree that you undermine GPLv3? and ...</blockquote>
</div>
<p>问:你是否同意说你贬低了 GPLv3 ? 以及……</p>
<a aria-controls="505c14ba" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#505c14ba" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="505c14ba">
<blockquote>
L: Yes</blockquote>
</div>
<p>L: 是的</p>
<a aria-controls="97e91d0f" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#97e91d0f" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="97e91d0f">
<blockquote>
Q: How can we get you to stop?</blockquote>
</div>
<p>问:我们如何才能让你别这么做?</p>
<a aria-controls="77f7155c" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#77f7155c" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="77f7155c">
<blockquote>
L: What?</blockquote>
</div>
<p>L: 什么?</p>
<a aria-controls="d3591d99" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#d3591d99" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="d3591d99">
<blockquote>
Q: ...How can we get you to stop?</blockquote>
</div>
<p>问:……我们如何才能让你别这么做?</p>
<a aria-controls="7b4e5b8a" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#7b4e5b8a" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="7b4e5b8a">
<blockquote>
L: Oh I hate GPLv3. I undermined it on purpose.
I actually thought the GPLv3 extensions were horrible.
I understand why people would want to do them but I think it should have
been a completely new license.</blockquote>
</div>
<p>L: 哦我讨厌 GPLv3 ,我是在故意贬低它。实际上我觉得 GPLv3 的扩展非常可怕。
我能理解为什么人们想要做这个,但是我觉得它本应是一个全新的协议。</p>
<a aria-controls="20696190" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#20696190" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="20696190">
<blockquote>
Emm my argument for liking version 2, and I still think version 2 is a
great license, was that, "I give you source code, you give me your
changes back, we are even." Right? That's my take on GPL version 2, right,
it's that simple.</blockquote>
</div>
<p>嗯我喜欢版本 2 的那些理由,并且我仍然觉得版本 2 是一个非常棒的协议,
理由是:「我给你源代码,你给我你对它的修改,我们就扯平了」
对吧?这是我用 GPL 版本 2 的理由,就是这么简单。</p>
<a aria-controls="7565a2bc" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#7565a2bc" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="7565a2bc">
<blockquote>
And version 3 extended that in ways that I personally am really
uncomfortable with, namely "I give you source code, that means that if
you use that source code, you can't use it on your device unless you
follow my rules." And to me that's, that's a violation of everything
version 2 stood for. And I understand why the FSF did it because I know
what the FSF wants. But to me it's not the same license at all.</blockquote>
</div>
<p>然后版本 3 的扩展在某些方面让我个人觉得非常不舒服,也就是说「我给你源代码,
这意味着你必须服从我的一些规则,否则你不能把它用在你的设备上。」
对我来说,这是违反了版本 2 协议所追求的所有目的。然而我理解为什么 FSF 要这么做,
因为我知道 FSF 想要达成什么,但是对我来说这完全是不同的协议了。</p>
<a aria-controls="f0b4e1fb" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#f0b4e1fb" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="f0b4e1fb">
<blockquote>
So I was very upset and made it very clear, and this was months before
version 3 was actually published. There was a discussion about this
long before... There was an earlier version of version 3, years before
actually, where I said "No, this is not gonna fly."
And during that earlier discussion I had already added to the kernel that,
"Hey, I don't have the version 2 or later". And there was no...
And I was really happy then when version 3 came out, that I have done that
something like 5 years before, because there was ever never any question
about what the license for the kernel was.</blockquote>
</div>
<p>所以我当时非常不安,并且表明了自己的观点,并且这是在版本 3 发布的数月之前。
在那很久之前曾经有过一场讨论……在版本 3 之前有一个早期的版本,
事实上几年之前,那时我就说过:「不,这不可能工作」。
并且在那个早期的讨论阶段我已经在内核里写好了「嘿,我可没有写过版本 2
或者更高版本」。所以之后也没有过(争议)……随后版本 3 出来的时候我非常开心,
因为我早在大概 5 年前做了预防,之后也就再也没有过关于内核的协议究竟是哪个
版本的讨论。</p>
<a aria-controls="32e9bcca" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#32e9bcca" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="32e9bcca">
<blockquote>
But I actually thought that version 3 is ... Uh, no ... I actually think
version 3 is a <strong>FINE</strong> license, right. I'm a firm believer in,
"If you write your code, it is your choice to pick a license."
And version 3 is a fine license. Version 3 was not a good ...
"Here we give you version 2, and then we tried to sneak in these new rules,
and tried to force everybody to upgrade." That was the part I disliked.
And the FSF did some really sneaky stuff, downright immoral in my opinion.</blockquote>
</div>
<p>不过事实上我觉得版本 3 是……呃不……我事实上觉得版本 3 是个 <strong>不错</strong> 的协议,
对吧。我坚定地相信「如果是你写的代码,那么你有权利决定它应该用什么协议」。
并且版本 3 是个不错的选择。版本 3 不好的地方在……「我们给你了版本 2
,然后我们试图偷偷混入这些新的规则,并且想逼着所有人都跟着升级」这是我不喜欢版本
3 的地方。并且 FSF 在其中做了很多见不得人的事情,我觉得做得很不道德。</p>
<a aria-controls="318f390b" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#318f390b" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="318f390b">
<blockquote>
Q: So you are talking about <a class="reference external" href="https://en.wikipedia.org/wiki/Tivoization">Tivoization</a>?</blockquote>
</div>
<p>问:所以你在说 <a class="reference external" href="https://en.wikipedia.org/wiki/Tivoization">Tivoization</a> 的事情么?</p>
<div class="panel panel-default">
<div class="panel-heading">
译注: 关于 <a class="reference external" href="https://en.wikipedia.org/wiki/Tivoization">Tivoization</a></div>
<div class="panel-body">
Tivoization 是 FSF 发明的一个词,表示 TiVo 的做法。 TiVo
是一个生产类似电视机顶盒之类的设备的厂商,他们在他们的设备中用到了 Linux
内核和很多别的开源组件,并且他们根据 GPLv2 协议开放了他们使用的组件的源代码。
然而他们在他们出售的设备中增加了数字签名,验证正在执行的系统和软件是他们自己
编制的软件,从而限制了用户修改运行软件的自由。这种做法在 FSF 看来是钻了 GPLv2
的法律上的空子,所以 FSF 提出了 GPLv3 封堵这种做法。</div>
</div>
<a aria-controls="db80a74e" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#db80a74e" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="db80a74e">
<blockquote>
L: Ehmm, yeah the Tivoization is always my main, eh dislike of version 3.
And, the FSF was being very dishonest thing. "Hey, we actually allow you
to invalidate the Tivoization clause" and they tried to, they literally
lied to people, and say "Hey, so that means that you can use GPLv3 without
the Tivoization part", right. This is ... How many people heard this
particular statement from the FSF? (Please raise your hands)</blockquote>
</div>
<p>L: 没错,Tivoization 的事情一直是我反对版本 3 的主要根据。并且,FSF
在这件事上表现得极不诚实。「嘿,其实我们允许你无效化 Tivoization 条款」,这样他们试图,
应该说他们是在明白着欺骗别人,并且说「嘿,这意味着你可以使用除去 Tivoization 部分的 GPLv3」。
这很……在场的诸位中有谁从 FSF 那儿听过这个说法?(请举手)</p>
<a aria-controls="d4a2f0a6" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#d4a2f0a6" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="d4a2f0a6">
<blockquote>
Ok, maybe they only tried to convince me with that one.
But they did try. And it was like, "I'm not stupid", right. Yes, you can
... The GPLv3 allows you to say "Ok, Tivoization is not an issue for us".
But it allows somebody else to take the project, and say "Hey, I ... The
GPLv3 without Tivoization is compatible with the full GPLv3, so I will now
make my own fork of this, and I will start doing drivers that use the full
version of version 3" And where am I stuck then? I am stuck saying "Hey I
give you the source code, and now I can't take it back your changes".
That's completely against the whole point of the license in the first
place.</blockquote>
</div>
<p>好吧,或许他们只试过对我用这套说辞,但是他们真的试过。我的反应是「我可不傻」,对吧。是的,
的确你可以…… GPLv3 允许你说「好, Tivoization 的事情对我们来说不是问题」,
但是它同时又允许别人接过这个项目,并且说「嘿,我觉得……去掉了 Tivoization 的 GPLv3
是兼容完整的 GPLv3 的,所以我可以 fork 这个项目,然后我将在自己的 fork 上用完整的
GPLv3 写驱动。」然后我就囧了。我的困境在于说「嘿,我给了你我的源代码,现在我却不能拿回你对它
的修改了」。这是彻底违背了我用这个协议最初的目的了。</p>
<a aria-controls="2f77aec5" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#2f77aec5" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="2f77aec5">
<blockquote>
So the FSF was, I mean the kind of stuff that was going on behind the
scenes, ah, made me once and for all to decide to never had any thing to
do with the FSF again. So if you wanted to give money to an organization
that does good? Give it to the EFF. The FSF is full of crazy bittered
people. That's just mine opinion. Uh, actually I have ... Ah ...
I overstated that a bit, right. The FSF has a lot of nice people in it,
but some of them are bit too extreme.</blockquote>
</div>
<p>所以 FSF 是,我是说那时他们暗地里做的那些事情,让我当下决定永远不再和 FSF 有任何瓜葛。
所以如果你想捐钱给一个行善的组织,那就捐给 EFF 吧。FSF 充满了疯狂难处的人。这只是我的观点。
呃其实我……嗯……我说得有点过分了。FSF 里有很多不错的人,不过其中有些人有点过激。</p>
<a aria-controls="51874275" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#51874275" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="51874275">
<blockquote>
Q: Well I wish the EFF care more about software freedom. But, uh,
can you ... Do you think that Tivoization benefits me as a user somehow?</blockquote>
</div>
<p>问: 嗯我也希望 EFF 能更多的关注于软件的自由方面。但是你能……你觉得 Tivoization
这种行为也能在某种方式上让我作为用户获益么?</p>
<a aria-controls="489be787" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#489be787" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="489be787">
<blockquote>
L: No, no I don't. I mean that ... But that was never my argument. That
was not why I selected the GPLv2. This is my whole point. It's not that
I think Tivoization is necessarily something that you should strive for.
But it is something that in my world view, it's your decision.
If you make hardware that locks down the software, that's your decision
as a hardware maker. That has no impact on my decision as a software maker
to give you the software. Do you see where I am coming from? I don't like
the locked down hardware, but at the same time that was never the social
contract I intended with Linux.</blockquote>
</div>
<p>L: 不,我不觉得。我的意思是……这从来都不是我的论据,这不是我选择了 GPLv2 的理由。
并不是说我觉得 Tivoization 是某种值得你去争取的权利,而是说在我的世界观中,这是你的决定。
如果你生产硬件去锁住了其中的软件,这是你作为一个硬件提供者的决定。
这完全不影响我作为一个软件提供者给你软件的决定。你能看出我的立场在哪儿了么?
我不喜欢上锁的硬件,但是同时这也从来不是我想要给 Linux 加上的的社会契约。</p>
<a aria-controls="d4934e2e" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#d4934e2e" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="d4934e2e">
<blockquote>
To me, umm, I mean, people may or may not
realize GPLv2 wasn't even the first license for Linux.
To me the important part was always "I give you software, you can do
whatever you want with it. If you making improvements, you have to give
them back." That was the first version of the license. It also had a
completely broken clause which was completely insane and I was stupid.
Hey it happened. My origin license says that you can't make money
change hands. And that was a mistake. That was clearly just wrong and bad
because it really didn't have anything to do with what I wanted. But I
was young, I was poor, I didn't realize that the whole money thing wasn't
the important part. And I have saw the errors in my ways, I saw the GPLv2
and said "Hey, that's the perfect license". And I saw the GPLv3 and I said
"No, that's overreaching a lot, that's not what I wanted". And so I made
Linux GPLv2 only, right.</blockquote>
</div>
<p>对我来说,呃我想说,大家可能知道或者不知道, GPLv2 并不是 Linux 的最初的协议。
对我来说重要的部分一直是「我给你软件,你可以用它做任何你想要做的事情。如果你做了任何改进,
你需要把它交还给我。」这是协议最初的样子。最早的协议还有一条完全错误的条款,写得完全不合理,
那时我很傻。嘿我也傻过。我最初的协议说你不能用它赚钱。这是失策,这明显是不对的不好的,
因为它和我真正想要做的事情没有任何关系。但是那时我很傻很天真,
我没意识到钱的事情在其中完全不重要。然后我发现了其中的问题,我看到了 GPLv2 然后说「嘿,
这是个完美的协议」。然后我看到了 GPLv3 我说「不,这做得过分了,这不是我想要的」
所以我让 Linux 成为了仅限 GPLv2 ,对吧。</p>
<a aria-controls="9e4f3a4f" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#9e4f3a4f" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="9e4f3a4f">
<blockquote>
Q: So do you think getting the patches back is as useful even if you can't
modify the device that it is used on?</blockquote>
</div>
<p>问: 所以你是否认为,即使你不能修改跑着这个软件的设备,拿回对软件的修改也还是同样重要的?</p>
<a aria-controls="1eafb32a" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#1eafb32a" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="1eafb32a">
<blockquote>
L: Yeah, absolutely. And I mean TiVo itself is actually an example of this.
Their patches were kind of crafty but I mean they were basically running
on a, originally a fairly standard MIPS thing. And their patches were
working around bugs in the chipsets they used. And they were valid patches.
The fact that they then felt that their hardware had to be locked down
someway. I didn't like it. But as I have mentioned, I felt that that was
their decision.</blockquote>
</div>
<p>L: 是的,当然。我想说 TiVo 它自己实际上就是一个例子。他们的修改有点复杂,但是我想说他们基本
是,一开始基本是运行在一套相当标准的 MIPS 设备上。然后他们的修改是想绕开他们用到的芯片上的
一些问题,并且这些是合格的修改。之后的事情是他们觉得他们需要锁住他们的硬件,我不喜欢这个。
但是就像我已经说的,我觉得这是他们的决定。</p>
<a aria-controls="93f9460e" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#93f9460e" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="93f9460e">
<blockquote>
And they had real reasons for that. That's something people sometimes
missed. There are sometimes reasons to do what TiVo did. Sometimes it's
imposed on you by, wireless carriers. Sometimes it's imposed on you by
Disney. Uh sometimes it's imposed on you by laws. The GPLv3 actually
accepts the last one when it comes to things like medical equipment
I think. But the point is that the whole Tivoization thing is, sometimes
it's, there is a reason for it. And if you make ... I mean I am not a
hardware designer. I think FPGA and stuff like that is really cool.
But I always ... I mean I really don't want to impose my world view on
anybody else. You don't have to use Linux. If you do use Linux, the only
thing I asked for is source code back. And there is all these other
verbiages in the GPLv2 about exact details, those aren't important.
And that was always my standpoint.</blockquote>
</div>
<p>并且他们有真正的理由去这么做。这是有时人们忽视的地方。有时是真的有理由去做 TiVo
他们做的事情。有时强加给你这种限制的是,无线运营商。有时强加给你的是迪士尼。
有时强加给你限制的甚至是法律。 GPLv3 在医疗设备之类的场合其实允许最后一种情况,我记得。
我的观点是,整个 Tivoization 的事情有时是有理由去这么做的。如果你生产……
我是说我不是硬件设计者,我觉得 FPGA 之类的东西很酷,但是我……我的意思是我真的不想把我对世界的
看法强加给别人。你不是非得要用 Linux ,如果你想要用 Linux
,那么我唯一要求你做的事情是把源代码(变更)还给我。然后在 GPLv2
中还有很多繁文缛节规定了详细的细节,这些都不重要。这是我一直以来的观点。</p>
<a aria-controls="1bed26a3" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#1bed26a3" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="1bed26a3">
<blockquote>
Q: Ok, well I will stop my non-point of making noise now.</blockquote>
</div>
<p>问: 好吧那我就不浪费时间了。</p>
<div class="panel panel-default">
<div class="panel-heading">
译注: 关于 <a class="reference external" href="https://zh.wikipedia.org/wiki/ISC%E8%A8%B1%E5%8F%AF%E8%AD%89">ISC 协议</a></div>
<div class="panel-body">
ISC 协议是一个开源软件协议,和两句的 BSD 协议功能相同。OpenBSD 项目选择尽量用 ISC
协议公开他们新写的代码。</div>
</div>
<a aria-controls="a8a95a78" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#a8a95a78" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="a8a95a78">
<blockquote>
L: I mean don't get me ... I mean I like other licenses too. I have used
like the four, emmm... Which BSD license is the acceptable one?
One of the BSD license is actually really nice. And it's actually the...
What?</blockquote>
</div>
<p>L: 我的意思是别误解……我也喜欢别的协议。我用过……到底是哪个 BSD 协议是可以接受的?
有一个 BSD 协议实际上非常不错。它实际上是……什么?</p>
<a aria-controls="4624145d" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#4624145d" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="4624145d">
<blockquote>
A: ISC</blockquote>
</div>
<p>观众: ISC</p>
<a aria-controls="b1cbe308" aria-expanded="false" class="translate-collapse-btn" data-toggle="collapse" href="#b1cbe308" role="button">
<span class="badge badge-pill badge-light"><i class="fa fa-language"></i> </span></a><div class="collapse" id="b1cbe308">
<blockquote>
L: ISC? And I actually encourage people who don't care about the giving
code back but care about the "Hey, I did something cool, please use it".
I encourage people to use the BSD license for that. And I mean the BSD
license is wonderful for that. It so happens that I thought that for my
project the giving back is equally important so I, for me BSD is bad.
But the point is <strong>for me</strong>. The GPLv3 maybe the perfect license for what
you guys want to do. And that's fine. And then it's the license you should
use. It's just that when somebody else wrote the code you don't get that
choice.</blockquote>
</div>
<p>L: ISC?并且事实上我在鼓励那些不在意拿回修改但是在意「嘿,我做了一个很酷的东西,请用它」。
我鼓励这些人去用 BSD 协议做这些事情。我想说 BSD 协议在这种场合是完美的。
只是碰巧我觉得对于我的项目,拿回修改也同样重要,所以对我而言 BSD 不好。但是重点是
<strong>对我而言</strong> 。 GPLv3 可能对你们想要做的事情而言是完美的协议,这很好,并且这时你就应该去用
GPLv3 。只是当代码是别人写的时候,你没有这个选择权。</p>
</div>
C语言中“.”与“->”有什么区别?2016-08-08T00:02:00+09:002016-08-08T00:02:00+09:00farseerfctag:farseerfc.me,2016-08-08:/zhs/dot-and-arrow-in-c.html<p>从 <a class="reference external" href="https://www.zhihu.com/question/49164544/answer/114584541">知乎</a> 转载</p>
<p>转载几篇知乎上我自己的回答,因为不喜欢知乎的排版,所以在博客里重新排版一遍。</p>
<div class="section" id="id2">
<h2>原问题:C语言中“.”与“->”有什么区别?</h2>
<p>除了表达形式有些不同,功能可以说完全一样阿。那为何又要构造两个功能一样的运算符? 效率有差异?可是现在编译器优化都那么强了,如果真是这样岂不是有些多此一举</p>
<hr class="docutils"/>
<p>刚刚翻了下书,说早期的C实现无法用结构直接当作参数在函数间传递,只能用指向结构的指针在函数间进行传递!我想这应该也是最直观的原因吧。</p>
</div>
<div class="section" id="id3">
<h2>我的回答</h2>
<p>首先 <code class="code">
a->b</code>
的含义是 <code class="code">
(*a).b</code>
,所以他们是不同的,不过的确 <code class="code">
-></code>
可以用 <code class="code">
*</code>
和 <code class="code">
.</code>
实现,不需要单独一个运算符。
嗯,我这是说现代的标准化的 C 语义上来说, <code class="code">
-></code>
可以用 <code class="code">
*</code>
和 <code class="code">
.</code>
的组合实现。</p>
<p>早期的 C 有一段时间的语义和现代的 C 的语义不太一样。</p>
<p>稍微有点汇编的基础的同学可能知道,在机器码和汇编的角度来看,不存在变量,不存在 struct …</p></div><p>从 <a class="reference external" href="https://www.zhihu.com/question/49164544/answer/114584541">知乎</a> 转载</p>
<p>转载几篇知乎上我自己的回答,因为不喜欢知乎的排版,所以在博客里重新排版一遍。</p>
<div class="section" id="id2">
<h2>原问题:C语言中“.”与“->”有什么区别?</h2>
<p>除了表达形式有些不同,功能可以说完全一样阿。那为何又要构造两个功能一样的运算符? 效率有差异?可是现在编译器优化都那么强了,如果真是这样岂不是有些多此一举</p>
<hr class="docutils"/>
<p>刚刚翻了下书,说早期的C实现无法用结构直接当作参数在函数间传递,只能用指向结构的指针在函数间进行传递!我想这应该也是最直观的原因吧。</p>
</div>
<div class="section" id="id3">
<h2>我的回答</h2>
<p>首先 <code class="code">
a->b</code>
的含义是 <code class="code">
(*a).b</code>
,所以他们是不同的,不过的确 <code class="code">
-></code>
可以用 <code class="code">
*</code>
和 <code class="code">
.</code>
实现,不需要单独一个运算符。
嗯,我这是说现代的标准化的 C 语义上来说, <code class="code">
-></code>
可以用 <code class="code">
*</code>
和 <code class="code">
.</code>
的组合实现。</p>
<p>早期的 C 有一段时间的语义和现代的 C 的语义不太一样。</p>
<p>稍微有点汇编的基础的同学可能知道,在机器码和汇编的角度来看,不存在变量,不存在 struct 这种东西,只存在寄存器和一个叫做内存的大数组。</p>
<p>所以变量,是 C 对内存地址的一个抽象,它代表了一个位置。举个例子,C 里面我们写:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">a</span> <span class="o">=</span> <span class="n">b</span></span>
</pre></div>
<p>其实在汇编的角度来看更像是</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="o">*</span><span class="n">A</span> <span class="o">=</span> <span class="o">*</span><span class="n">B</span></span>
</pre></div>
<p>其中 A 和 B 各是两个内存地址,是指针。</p>
<p>好,以上是基本背景。</p>
<p>基于这个背景我们讨论一下 struct 是什么,以及 struct 的成员是什么。
假设我们有</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="k">struct</span> <span class="n">Point</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kt">int</span> <span class="n">x</span><span class="p">;</span></span>
<span class="code-line"> <span class="kt">int</span> <span class="n">y</span><span class="p">;</span></span>
<span class="code-line"><span class="p">};</span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">Point</span> <span class="n">p</span><span class="p">;</span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">Point</span> <span class="o">*</span><span class="n">pp</span> <span class="o">=</span> <span class="o">&</span><span class="n">p</span><span class="p">;</span></span>
</pre></div>
<p>从现代语义上讲 <code class="code">
p</code>
就是一个结构体对象, <code class="code">
x</code>
和 <code class="code">
y</code>
各是其成员,嗯。</p>
<p>从汇编的语义上讲, <code class="code">
p</code>
是一个不完整的地址,或者说,半个地址,再或者说,一个指向的东西是虚构出来的地址。而 <code class="code">
x</code>
和 <code class="code">
y</code>
各是在 Point 结构中的地址偏移量。也就是说,必须有 <code class="code">
p</code>
和 <code class="code">
x</code>
或者 <code class="code">
p</code>
和 <code class="code">
y</code>
同时出现,才形成一个完整的地址,单独的一个 <code class="code">
p</code>
没有意义。</p>
<p>早期的 C 就是在这样的模型上建立的。所以对早期的 C 而言, <code class="code">
*pp</code>
没有意义,你取得了一个 struct ,而这个 struct 不能塞在任何一个寄存器里,编译器和 CPU 都无法表达这个东西。</p>
<p>这时候只有 <code class="code">
p.x</code>
和 <code class="code">
p.y</code>
有意义,它们有真实的地址。</p>
<p>早期的 C 就是这样一个看起来怪异的语义,而它更贴近机器的表达。
所以对早期的 C 而言,以下的代码是对的:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">p</span><span class="p">.</span><span class="n">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span></span>
<span class="code-line"><span class="kt">int</span> <span class="o">*</span><span class="n">a</span><span class="p">;</span></span>
<span class="code-line"><span class="n">a</span> <span class="o">=</span> <span class="o">&</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">x</span><span class="p">);</span></span>
</pre></div>
<p>而以下代码是错的:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="p">(</span><span class="o">*</span><span class="n">pp</span><span class="p">).</span><span class="n">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span></span>
</pre></div>
<p>因为作为这个赋值的目标地址表达式的一部分, <code class="code">
*pp</code>
,这个中间结果没法直译到机器码。</p>
<p>所以对早期的 C 而言,对 pp 解引用的操作,必须和取成员的偏移的操作,这两者紧密结合起来变成一个单独的操作,其结果才有意义。</p>
<p>所以早期的 C 就发明了 -> ,表示这两个操作紧密结合的操作。于是才能写:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">pp</span><span class="o">-></span><span class="n">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span></span>
</pre></div>
<p>嗯,这就是它存在的历史原因。
而这个历史原因现在已经不重要了,现代的符合标准的 C 编译器都知道 <code class="code">
(*pp).x</code>
和 <code class="code">
pp->x</code>
是等价的了。</p>
<p>说句题外话, C++ 里面还发明了 <code class="code">
.*</code>
和 <code class="code">
->*</code>
这两个运算符(注意 <code class="code">
->*</code>
不是单独的 <code class="code">
-></code>
和 <code class="code">
*</code>
并排放的意思),关于为什么要发明这两个运算符,而不能直接说 <code class="code">
a ->* b</code>
的意思就是 <code class="code">
a ->(*b)</code>
,这个就作为课堂作业吧。</p>
</div>
启用 GitHub Issue 作为博客留言系统2016-08-07T16:28:00+09:002016-08-07T16:28:00+09:00farseerfctag:farseerfc.me,2016-08-07:/zhs/github-issues-as-comments.html<p>从今天起本博客将启用 GitHub Issue 作为留言系统。
原本使用的 Disqus 将继续保留一段时间,目前没有关闭的计划。</p>
<p>换用 GitHub Issue 是计划了好久的事情了,最初重做这个主题的时候就有考虑过。
这个想法的契机是看到了这篇
<a class="reference external" href="http://ivanzuzak.info/2011/02/18/github-hosted-comments-for-github-hosted-blogs.html">GitHub hosted comments for GitHub hosted blogs</a>
,然后立马觉得这个想法很符合寄宿在 GitHub Pages 上的博客。
一个限制是要求评论者必须有 GitHub
账户,考虑到我的博客的受众这个要求估计不算太过分。
使用 GitHub Issue 的好处么,比如自带的 GFMD
富文本格式,邮件通知,还有订阅和取消订阅通知,邮件回复,
这些方面都不比第三方留言系统逊色。</p>
<p>换用 GitHub Issue 另一方面原因是最近听说 Disqus
被部分墙了,想必以后墙也会越来越高。之前曾经试过在这个博客换上多说,
然而效果我并不喜欢,多说喜欢侵入页面加很多奇怪的东西 …</p><p>从今天起本博客将启用 GitHub Issue 作为留言系统。
原本使用的 Disqus 将继续保留一段时间,目前没有关闭的计划。</p>
<p>换用 GitHub Issue 是计划了好久的事情了,最初重做这个主题的时候就有考虑过。
这个想法的契机是看到了这篇
<a class="reference external" href="http://ivanzuzak.info/2011/02/18/github-hosted-comments-for-github-hosted-blogs.html">GitHub hosted comments for GitHub hosted blogs</a>
,然后立马觉得这个想法很符合寄宿在 GitHub Pages 上的博客。
一个限制是要求评论者必须有 GitHub
账户,考虑到我的博客的受众这个要求估计不算太过分。
使用 GitHub Issue 的好处么,比如自带的 GFMD
富文本格式,邮件通知,还有订阅和取消订阅通知,邮件回复,
这些方面都不比第三方留言系统逊色。</p>
<p>换用 GitHub Issue 另一方面原因是最近听说 Disqus
被部分墙了,想必以后墙也会越来越高。之前曾经试过在这个博客换上多说,
然而效果我并不喜欢,多说喜欢侵入页面加很多奇怪的东西,比如用户的头像通常是
http 的……也试过结合新浪微博的评论,而新浪微博越来越封闭,API 也越来越不靠谱。</p>
<p>使用 GitHub Issue 作为评论的方式比较简单,上面那篇博客里面提到了,代码量不比
加载 Disqus 多多少,而且没有了 iframe 的困扰,唯一麻烦的地方就是要稍微设计一下布局方式让它融入
现有的页面布局。
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/blob/2ea6c9f3227275fe86ddaa75d8fc6496b3b03d8c/templates/includes/comments.html#L32">我参考上面的实现在这里</a> 。
这个加载代码使用两个变量加载 Issue Comments ,一个是在 pelicanconf.py 里的
<code class="code">
GITHUB_REPO</code>
,可以指向任何 Repo ,我指向 farseerfc/farseerfc.github.io
的这个 GitHub Page repo ,另一个变量是每篇文章里需要加上 <code class="code">
issueid</code>
的元数据,关连文章到每个 Issue 上。</p>
<p>还有一个稍微麻烦的事情是现在每写一篇文章之后都要新建一个 issue 了。
手动操作有点累人,于是我 <a class="reference external" href="https://github.com/farseerfc/farseerfc/blob/master/createissue.py">写了个脚本</a>
自动搜索 pelican 的 content 文件夹里面文章的 slug 并且对没有 issueid 关连的
文章创建 issue 。</p>
<p>好啦新的留言系统的外观样式还在测试中,希望大家多留言帮我测试一下!</p>
<div class="label label-warning">
<strong>2016年8月7日19:30更新</strong></div>
<p>新增了对 GitHub Issue comments 里面
<a class="reference external" href="https://developer.github.com/v3/issues/comments/#reactions-summary">reactions</a>
的支持,套用 font-awesome 的图标(似乎没 GitHub 上的图标好看)。这个还属于 GitHub API
的实验性功能,要加入 <code class="code">
Accept: application/vnd.github.squirrel-girl-preview</code>
HTTP 头才能拿到。</p>
<div class="label label-warning">
<strong>2016年8月7日23:16更新</strong></div>
<p>感谢 @iovxw 的测试让我发现 github 的高亮回复和邮件回复是需要特殊处理的。
高亮回复用上了 <a class="reference external" href="https://github.com/sindresorhus/github-markdown-css">这里的 CSS</a>
邮件引言的展开事件直接用 jQuery 做了:</p>
<div class="highlight"><pre><span class="code-line"><span></span> <span class="nx">$</span><span class="p">(</span><span class="s2">".email-hidden-toggle > a"</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="s2">"click"</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">){</span></span>
<span class="code-line"> <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span></span>
<span class="code-line"> <span class="nx">$</span><span class="p">(</span><span class="s2">".email-hidden-reply"</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">parent</span><span class="p">).</span><span class="nx">toggle</span><span class="p">();</span></span>
<span class="code-line"><span class="p">});</span></span>
</pre></div>
<p>还得注意邮件的回复需要 CSS 里面 <code class="code">
white-space: pre-wrap</code>
。</p>
PacVis: 可视化 pacman 本地数据库2016-07-31T03:52:00+09:002016-07-31T03:52:00+09:00farseerfctag:farseerfc.me,2016-07-31:/zhs/pacvis.html
<div class="panel panel-default">
<div class="panel-heading">
PacVis</div>
<div class="panel-body">
<img alt="Demo of PacVis" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-first.png"/>
</div>
</div>
<div class="section" id="pacvis">
<h2><a class="toc-backref" href="#id11">我为什么要做 PacVis</a></h2>
<p>我喜欢 Arch Linux ,大概是因为唯有 Arch Linux 能给我对整个系统「了如指掌」的感觉。
在 Arch Linux 里我能清楚地知道我安装的每一个包,能知道系统里任何一个文件是来自哪个包,
以及我为什么要装它。或许对 Debian/Fedora/openSUSE 足够熟悉了之后也能做到这两点,
不过他们的细致打包的结果通常是包的数量比 Arch 要多个 3 到 10 倍,并且打包的细节也比 Arch
Linux 简单的 PKGBUILD 要复杂一个数量级。</p>
<p>每一个装过 Arch Linux 的人大概都知道,装了 Arch Linux 之后得到的系统非常朴素,按照
ArchWiki 上的流程一路走下来的话,最关键的一条命令就是 <code class="code">
pacstrap /mnt …</code></p></div>
<div class="panel panel-default">
<div class="panel-heading">
PacVis</div>
<div class="panel-body">
<img alt="Demo of PacVis" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-first.png"/>
</div>
</div>
<div class="section" id="pacvis">
<h2><a class="toc-backref" href="#id11">我为什么要做 PacVis</a></h2>
<p>我喜欢 Arch Linux ,大概是因为唯有 Arch Linux 能给我对整个系统「了如指掌」的感觉。
在 Arch Linux 里我能清楚地知道我安装的每一个包,能知道系统里任何一个文件是来自哪个包,
以及我为什么要装它。或许对 Debian/Fedora/openSUSE 足够熟悉了之后也能做到这两点,
不过他们的细致打包的结果通常是包的数量比 Arch 要多个 3 到 10 倍,并且打包的细节也比 Arch
Linux 简单的 PKGBUILD 要复杂一个数量级。</p>
<p>每一个装过 Arch Linux 的人大概都知道,装了 Arch Linux 之后得到的系统非常朴素,按照
ArchWiki 上的流程一路走下来的话,最关键的一条命令就是 <code class="code">
pacstrap /mnt base</code>
,
它在 <code class="code">
/mnt</code>
里作为根调用 <code class="code">
pacman -S base</code>
装上了整个 base 组,
然后就没有然后了。这个系统一开始空无一物,你需要的任何东西都是后来一点点用
<code class="code">
pacman</code>
手动装出来的,没有累赘,按你所需。</p>
<p>然而时间长了,系统中难免会有一些包,是你装过用过然后忘记了,
然后这些包就堆在系统的角落里,就像家里陈年的老家具,占着地,落着灰。虽然
<code class="code">
pacman -Qtd</code>
能方便地帮你找出所有
<strong>曾经作为依赖被装进来,而现在不被任何包依赖</strong> 的包,但是对于那些你手动指定的包,
它就无能为力了。</p>
<p>于是我就一直在找一个工具能帮我梳理系统中包的关系,方便我:</p>
<ol class="arabic simple">
<li>找出那些曾经用过而现在不需要的包</li>
<li>找出那些体积大而且占地方的包</li>
<li>厘清系统中安装了的包之间的关系</li>
</ol>
<div class="figure">
<img alt="Android System Architecture" class="img-responsive" src="//farseerfc.me/zhs/images/Android-System-Architecture.jpg"/>
<p class="caption"><a class="reference external" href="https://en.wikipedia.org/wiki/Android_(operating_system)">Android 系统架构</a></p>
</div>
<p>关于最后一点「厘清包的关系」,我曾经看到过
<a class="reference external" href="https://en.wikipedia.org/wiki/Architecture_of_OS_X">macOS 系统架构图</a>
和 Android 的系统架构图,对其中的层次化架构印象深刻,之后就一直在想,是否能画出现代
Linux 桌面系统上类似的架构图呢?又或者 Linux 桌面系统是否会展现完全不同的样貌?
从维基百科或者别的渠道能找到 Linux 内核、或者 Linux 图形栈,
或者某个桌面环境的架构,但是没有找到覆盖一整个发行版的样貌的。
于是我便想,能不能从包的依赖关系中自动生成这样一张图呢。</p>
</div>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id12">PacVis的老前辈们</a></h2>
<p>在开始写 PacVis 之前,我试过一些类似的工具,他们都或多或少能解决一部分我的需要,
又在某些方面有所不足。这些工具成为了 PacVis 的雏形,启发了 PacVis
应该做成什么样子。</p>
<div class="section" id="pactree">
<h3><a class="toc-backref" href="#id13">pactree</a></h3>
<p>pactree 曾经是一个
<a class="reference external" href="https://bbs.archlinux.org/viewtopic.php?id=51795">独立的项目</a> ,现在则是
<a class="reference external" href="https://www.archlinux.org/pacman/pactree.8.html">pacman 的一部分</a> 了。
从手册页可以看出, pactree 的输出是由某个包开始的依赖树。
加上 <code class="code">
--graph</code>
参数之后 pactree 还能输出
<a class="reference external" href="http://www.graphviz.org/">dot</a> 格式的矢量图描述,然后可以用 dot 画出依赖图:</p>
<div class="panel panel-default">
<div class="panel-heading">
<code class="code">
pactree pacvis-git -d3 --graph | dot -Tpng >pacvis-pactree.png</code>
</div>
<div class="panel-body">
<img alt="pactree --graph" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-pactree.png"/>
</div>
</div>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> pactree pacvis-git -d3</span>
<span class="code-line"><span class="go">pacvis-git</span></span>
<span class="code-line"><span class="go">├─python-tornado</span></span>
<span class="code-line"><span class="go">│ └─python</span></span>
<span class="code-line"><span class="go">│ ├─expat</span></span>
<span class="code-line"><span class="go">│ ├─bzip2</span></span>
<span class="code-line"><span class="go">│ ├─gdbm</span></span>
<span class="code-line"><span class="go">│ ├─openssl</span></span>
<span class="code-line"><span class="go">│ ├─libffi</span></span>
<span class="code-line"><span class="go">│ └─zlib</span></span>
<span class="code-line"><span class="go">├─pyalpm</span></span>
<span class="code-line"><span class="go">│ ├─python</span></span>
<span class="code-line"><span class="go">│ └─pacman</span></span>
<span class="code-line"><span class="go">│ ├─bash</span></span>
<span class="code-line"><span class="go">│ ├─glibc</span></span>
<span class="code-line"><span class="go">│ ├─libarchive</span></span>
<span class="code-line"><span class="go">│ ├─curl</span></span>
<span class="code-line"><span class="go">│ ├─gpgme</span></span>
<span class="code-line"><span class="go">│ ├─pacman-mirrorlist</span></span>
<span class="code-line"><span class="go">│ └─archlinux-keyring</span></span>
<span class="code-line"><span class="go">└─python-setuptools</span></span>
<span class="code-line"><span class="go"> └─python-packaging</span></span>
<span class="code-line"><span class="go"> ├─python-pyparsing</span></span>
<span class="code-line"><span class="go"> └─python-six</span></span>
<span class="code-line"><span class="gp"> $</span> pactree pacvis-git -d3 --graph <span class="p">|</span> dot -Tpng >pacvis-pactree.png</span>
</pre></div>
<p>从画出的图可以看出,因为有共用的依赖,所以从一个包开始的依赖关系已经不再是一棵
<a class="reference external" href="https://zh.wikipedia.org/wiki/%E6%A8%B9%E7%8B%80%E7%B5%90%E6%A7%8B">图论意义上的树(Tree)</a>
了。最初尝试做 PacVis 的早期实现的时候,就是试图用 bash/python 脚本解析 pactree 和
pacman 的输出,在 pactree 的基础上把整个系统中所有安装的包全都包含到一张图里。
当然后来画出的结果并不那么理想,首先由于图非常巨大,导致 dot
的自动布局要耗费数小时,最后画出的图也过于巨大基本上没法看。</p>
<p>然而不得不说没有 pactree 就不会有 PacVis ,甚至 pacman 被分离出 alpm
库也和 pactree 用 C 重写的过程有很大关系,而 PacVis 用来查询 pacman 数据库的库
pyalpm 正是 alpm 的 Python 绑定。因为 pactree 的需要而增加出的 alpm 库奠定了 PacVis
实现的基石。</p>
</div>
<div class="section" id="pacgraph">
<h3><a class="toc-backref" href="#id14">pacgraph</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
pacgraph 的输出</div>
<div class="panel-body">
<img alt="pacgraph" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-pacgraph.png"/>
</div>
</div>
<p><a class="reference external" href="http://kmkeen.com/pacgraph/index.html">pacgraph</a> 是一位 Arch Linux 的
Trusted User <a class="reference external" href="http://kmkeen.com/">keenerd</a> 写的程序,和
PacVis 一样也是用 Python 实现的。
比起 pactree , pacgraph 明显更接近我的需求,它默认绘制整个系统的所有安装包,
并且用聪明的布局算法解决了 dot 布局的性能问题。</p>
<p>pacgraph 的输出是一个富有艺术感的依赖图,图中用不同的字体大小表示出了每个包占用
的磁盘空间。通过观察 pacgraph 的输出,我们可以清楚地把握系统全局的样貌,
比如一眼看出这是个桌面系统还是个服务器系统,并且可以很容易地发现那些占用磁盘空间
巨大的包,考虑清理这些包以节约空间。</p>
<p>更棒的是 pacgraph 还提供了一个交互性的 GUI 叫做 pacgraph-tk ,显然通过 tk 实现。
用这个 GUI 可以缩放观察整幅图的细节,或者选中某个包观察它和别的包的依赖关系。</p>
<p>pacgraph 还支持通过参数指定只绘制个别包的依赖关系,就像 pactree 那样。</p>
<p>不过 pacgraph 也不是完全满足我的需要。如我前面说过,我希望绘制出的图能反应
<strong>这个发行版的架构面貌</strong> ,而 pacgraph 似乎并不区别「该包依赖的包」和「依赖该包的包」
这两种截然相反的依赖关系。换句话说 pacgraph 画出的是一张无向图,
而我更想要一张有向图,或者说是 <strong>有层次结构的依赖关系图</strong> 。</p>
</div>
</div>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id15">于是就有了 PacVis</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
PacVis 刚打开的样子</div>
<div class="panel-body">
<img alt="PacVis on startup" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-second.png"/>
</div>
</div>
<p>总结了老前辈们的优势与不足,我便开始利用空余时间做我心目中的 PacVis 。
前后断断续续写了两个月,又分为两个阶段,第一阶段做了基本的功能和雏形,
第二阶段套用上 <a class="reference external" href="https://getmdl.io/">https://getmdl.io/</a> 的模板,总算有了能拿得出手给别人看的样子。</p>
<p>于是乎前两天在 AUR 上给 pacvis 打了个
<a class="reference external" href="https://aur.archlinux.org/packages/pacvis-git/">pacvis-git</a>
包,现在想在本地跑 pacvis 应该很方便了,用任何你熟悉的 aurhelper
就可以安装,也可以直接从 aur 下载 PKGBUILD 打包:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="go">~$ git clone aur@aur.archlinux.org:pacvis-git.git</span></span>
<span class="code-line"><span class="go">~$ cd pacvis-git</span></span>
<span class="code-line"><span class="go">~/pacvis-git$ makepkg -si</span></span>
<span class="code-line"><span class="go">~/pacvis-git$ pacvis</span></span>
<span class="code-line"><span class="go">Start PacVis at http://localhost:8888/</span></span>
</pre></div>
<p>按照提示说的,接下来打开浏览器访问 <a class="reference external" href="http://localhost:8888/">http://localhost:8888/</a> 就能看到 PacVis
的样子了。仅仅作为尝试也可以直接打开跑在我的服务器上的 demo:
<a class="reference external" href="https://pacvis.farseerfc.me/">https://pacvis.farseerfc.me/</a> ,这个作为最小安装的服务器载入速度大概比普通的桌面系统快一点。</p>
<div class="panel panel-default">
<div class="panel-heading">
在 Windows msys2 跑 PacVis</div>
<div class="panel-body">
<img alt="PacVis on Windows msys2" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-msys2.png"/>
</div>
</div>
<p>另外补充一下,因为 PacVis 只依赖 pyalpm 和 tornado ,所以在别的基于 pacman
的系统上跑它应该也没有任何问题,包括
<a class="reference external" href="https://msys2.github.io/">Windows 上的 msys2</a> 里(尽管在 msys2 上编译
tornado 的包可能要花些功夫)。</p>
</div>
<div class="section" id="id5">
<h2><a class="toc-backref" href="#id16">PacVis 的图例和用法</a></h2>
<p>操作上 PacVis 仿照地图程序比如 Google Maps 的用法,可以用滚轮或者触摸屏的手势
缩放、拖拽,右上角有个侧边栏,不需要的话可以点叉隐藏掉,右下角有缩放的按钮和
回到全局视图的按钮,用起来应该还算直观。</p>
<div class="figure">
<img alt="PacVis showing pacvis-git" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-pacvis-git.png"/>
<p class="caption">pacvis-git 包的依赖</p>
</div>
<p>先解释图形本身,整张图由很多小圆圈的节点,以及节点之间的箭头组成。
一个圆圈就代表一个软件包,而一条箭头代表一个依赖关系。缩放到细节的话,
能看到每个小圆圈的下方标注了这个软件包的名字,鼠标悬浮在圆圈上也会显示相应信息。
还可以点开软件包,在右侧的边栏里会有更详细的信息。</p>
<p>比如图例中显示了 pacvis-git 自己的依赖,它依赖 pyalpm, python-tornado 和
python-setuptools ,其中 pyalpm 又依赖 pacman 。图中用
<span class="label label-primary">紫色</span> 表示手动安装的包,
<span class="label label-warning">橙色</span> 表示被作为依赖安装的包,
箭头的颜色也随着包的颜色改变。</p>
<p>值得注意的是图中大多数箭头都是由下往上指的,这是因为 PacVis 按照包的依赖关系做
了拓扑排序,并且给每个包赋予了一个拓扑层级。比如 pacvis-git 位于 39
层,那么它依赖的 pyalpm 就位于 38 层,而 pyalpm 依赖的 pacman 就位于 37
层。根据层级关系排列包是 PacVis 于 pacgraph 之间最大的不同之处。</p>
<p>除了手动缩放, PacVis 还提供了搜索框,根据包名快速定位你感兴趣的包。
以及在右侧边栏中的 Dep 和 Req-By 等页中,包的依赖关系也是做成了按钮的形式,
可以由此探索包和包之间的关联。</p>
<p>最后稍微解释一下两个和实现相关的参数:</p>
<div class="label label-info">
Max Level</div>
<p>这是限制 PacVis 载入的最大拓扑层。系统包非常多的时候 PacVis
的布局算法会显得很慢,限制层数有助于加快载入,特别是在调试 PacVis 的时候比较有用。</p>
<div class="label label-info">
Max Required-By</div>
<p>这是限制 PacVis 绘制的最大被依赖关系。稍微把玩一下 PacVis 就会发现系统内绝大多数
的包都直接依赖了 glibc 或者 gcc-libs 等个别的几个包,而要绘制这些依赖的话会导致
渲染出的图中有大量长直的依赖线,不便观察。于是可以通过限制这个值,使得 PacVis
不绘制被依赖太多的包的依赖关系,有助于让渲染出的图更易观察。</p>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id17">从 PacVis 能了解到的一些事实</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
一个 KDE 桌面的 PacVis 结果全图, <a class="reference external" href="//farseerfc.me/zhs/images/pacvis-16384.png">放大(17M)</a></div>
<div class="panel-body">
<img alt="A normal KDE desktop in PacVis" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-4096-anno.png"/>
</div>
</div>
<p>稍微玩一下 PacVis 就能发现不少有趣现象,上述「绝大多数包依赖 glibc 」就是一例。
除此之外还有不少值得玩味的地方。</p>
<div class="section" id="id7">
<h3><a class="toc-backref" href="#id18">依赖层次</a></h3>
<p>系统中安装的包被明显地分成了这样几个层次:</p>
<ul class="simple">
<li>glibc 等 C 库</li>
<li>Bash/Perl/Python 等脚本语言</li>
<li>coreutils/gcc/binutils 等核心工具</li>
<li>pacman / systemd 等较大的系统工具</li>
<li>gtk{2,3}/qt{4,5} 等 GUI toolkit</li>
<li>chromium 等 GUI 应用</li>
<li>Plasma/Gnome 等桌面环境</li>
</ul>
<p>大体上符合直观的感受,不过细节上有很多有意思的地方,比如 zsh 因为 gdbm
间接依赖了 bash,这也说明我们不可能在系统中用 zsh 完全替代掉 bash。
再比如 python (在 Arch Linux 中是 python3)和 python2 和 pypy
几乎在同一个拓扑层级。</p>
<div class="figure">
<img alt="zsh depends on bash because of gdbm" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-zsh-bash.png" style="width: 45%;"/>
<p class="caption">zsh 因为 gdbm 间接依赖了 bash</p>
</div>
<p>不过偶尔显示的依赖层级不太符合直观,比如 qt5-base < qt4 < gtk2 < gtk3 。
qt5 因为被拆成了数个包所以比 qt4 更低级这可以理解,而 gtk 系比 qt
系更高级这一点是很多人(包括我)没有预料到的吧。</p>
</div>
<div class="section" id="id8">
<h3><a class="toc-backref" href="#id19">循环依赖</a></h3>
<p>有些包的依赖关系形成了循环依赖,一个例子是 freetype2 和 harfbuzz,freetype2
是绘制字体的库,harfbuzz 是解析 OpenType 字形的库,两者对对方互相依赖。
另一个例子是 KDE 的 kio 和 kinit,前者提供类似 FUSE 的资源访问抽象层,
后者初始化 KDE 桌面环境。</p>
<div class="figure">
<img alt="freetype2 harfbuzz" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-freetype2-harfbuzz.png" style="width: 45%;"/>
<p class="caption">freetype2 和 harfbuzz 之间的循环依赖</p>
</div>
<p>因为这些循环依赖的存在,使得 PacVis 在实现时不能直接拓扑排序,我采用环探测
算法找出有向图中所有的环,并且打破这些环,然后再使用拓扑排序。
因此我在图中用红色的箭头表示这些会导致环的依赖关系。</p>
</div>
<div class="section" id="id9">
<h3><a class="toc-backref" href="#id20">有些包没有依赖关系</a></h3>
<div class="figure">
<img alt="PacVis Level 0" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-level0.png" style="width: 45%;"/>
<p class="caption">man-pages 和 licenses 没有依赖关系</p>
</div>
<p>有些包既不被别的包依赖,也不依赖别的包,而是孤立在整张图中,比如
man-pages 和 licenses 。这些包在图中位于最顶端,拓扑层级是 0 ,我用
<span class="label label-info">蓝色</span> 正方形特别绘制它们。</p>
</div>
<div class="section" id="linux">
<h3><a class="toc-backref" href="#id21">只看依赖关系的话 Linux 内核完全不重要</a></h3>
<p>所有用户空间的程序都依赖着 glibc ,而 glibc 则从定义良好的 syscall 调用内核。
因此理所当然地,如果只看用户空间的话, glibc 和别的 GNU 组件是整个 GNU/Linux
发行版的中心,而 Linux 则是位于依赖层次中很深的位置,甚至在我的 demo 服务器上
Linux 位于整个图中的最底端,因为它的安装脚本依赖 mkinitcpio
而后者依赖了系统中的众多组件。</p>
</div>
<div class="section" id="pacman-qtd">
<h3><a class="toc-backref" href="#id22">pacman -Qtd 不能找到带有循环依赖的孤儿包</a></h3>
<div class="figure">
<img alt="pacman -Qtd cannot find packages with circle dependency" class="img-responsive" src="//farseerfc.me/zhs/images/pacvis-circledeps-Qtd.png" style="width: 45%;"/>
<p class="caption">msys2 中带有循环依赖的孤儿包</p>
</div>
<p>这是我在 msys2 上测试 PacVis 的时候发现的,我看到在渲染的图中有一片群岛,
没有连上任何手动安装的包。这种情况很不正常,因为我一直在我的所有系统中跑
<code class="code">
pacman -Qtd</code>
找出孤儿包并删掉他们。放大之后我发现这些包中有一条循环依赖,
这说明 <code class="code">
pacman -Qtd</code>
不能像语言的垃圾回收机制那样找出有循环依赖的孤儿包。</p>
</div>
</div>
<div class="section" id="id10">
<h2><a class="toc-backref" href="#id23">PacVis 的未来</a></h2>
<p>目前的 PacVis 基本上是我最初开始做的时候设想的样子,随着开发逐渐又增加了不少功能。
一些是迫于布局算法的性能而增加的(比如限制层数)。</p>
<p>今后准备再加入以下这些特性:</p>
<ol class="arabic simple">
<li>更合理的 optdeps 处理。目前只是把 optdeps 关系在图上画出来了。</li>
<li>更合理的 <strong>依赖关系抉择</strong> 。有时候包的依赖关系并不是直接根据包名,而是
<code class="code">
provides</code>
由一个包提供另一个包的依赖。目前 PacVis 用 alpm
提供的方式抉择这种依赖,于是这种关系并没有记录在图上。</li>
<li>目前的层级关系没有考虑包所在的仓库 (core/extra/community/...) 或者包所属的组。
加入这些关系能更清晰地表达依赖层次。</li>
<li>目前没有办法只显示一部分包的关系。以后准备加入像 pactree/pacgraph 一样显示部分包。</li>
</ol>
<p>如果你希望 PacVis 出现某些有趣的用法和功能,也
<a class="reference external" href="https://github.com/farseerfc/pacvis/issues">请给我提 issue</a> 。</p>
</div>
X 中的混成器与 Composite 扩展2015-03-19T17:45:00+09:002015-03-19T17:45:00+09:00farseerfctag:farseerfc.me,2015-03-19:/zhs/compositor-in-X-and-compositext.html
<p>在上篇文章 <a class="reference external" href="//farseerfc.me/zhs/brief-history-of-compositors-in-desktop-os.html">「桌面系统的混成器简史」</a>
中我介绍了其它桌面系统中的混成器的发展史和工作原理,
话题回到我们的正题 Linux 系统上,来说说目前 X 中混成器是如何工作的。
这篇文章将比上一篇深入更多技术细节,不想看太多细节的可以直接跳过看 <a class="reference external" href="#id6">结论</a> 。</p>
<div class="section" id="x">
<h2><a class="toc-backref" href="#id8">原始的 X 的绘图模型</a></h2>
<p>首先,没有混成器的时候 X 是这样画图的:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/e06d011e.png"/>
<p>X 的应用程序没有统一的绘图 API 。GTK+ 在 3.0 之后统一用 <a class="reference external" href="http://cairographics.org/">Cairo</a> 绘图,
而 Cairo 则是基于 PDF 1.4 的绘图模型构建的,
GTK 的 2.0 和之前的版本中也有很大一部分的绘图是用 Cairo 进行,
其余则通过 xlib 或者 xcb 调用 X 核心协议提供的绘图原语绘图 …</p></div>
<p>在上篇文章 <a class="reference external" href="//farseerfc.me/zhs/brief-history-of-compositors-in-desktop-os.html">「桌面系统的混成器简史」</a>
中我介绍了其它桌面系统中的混成器的发展史和工作原理,
话题回到我们的正题 Linux 系统上,来说说目前 X 中混成器是如何工作的。
这篇文章将比上一篇深入更多技术细节,不想看太多细节的可以直接跳过看 <a class="reference external" href="#id6">结论</a> 。</p>
<div class="section" id="x">
<h2><a class="toc-backref" href="#id8">原始的 X 的绘图模型</a></h2>
<p>首先,没有混成器的时候 X 是这样画图的:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/e06d011e.png"/>
<p>X 的应用程序没有统一的绘图 API 。GTK+ 在 3.0 之后统一用 <a class="reference external" href="http://cairographics.org/">Cairo</a> 绘图,
而 Cairo 则是基于 PDF 1.4 的绘图模型构建的,
GTK 的 2.0 和之前的版本中也有很大一部分的绘图是用 Cairo 进行,
其余则通过 xlib 或者 xcb 调用 X 核心协议提供的绘图原语绘图。
QT 的情况也是类似,基本上用 QPaint 子系统绘制成位图然后交给 X 的显示服务器。
显示服务器拿到这些绘制请求之后,再在屏幕上的相应位置绘制整个屏幕。
当然还有很多老旧的不用 GTK 或者 QT 的程序,他们则直接调用 X 核心协议提供的绘图原语。</p>
<p>值得注意一点是 X 上除了没有统一的绘图模型,也没有统一的矢量图格式。
X 核心协议的绘图原语提供的是像素单位的绘图操作,没有类似 GDI+ 或者 Quartz
提供的 <ruby><rb>设备无关</rb><rp>(</rp><rt>Device Independence</rt><rp>)</rp></ruby> 的「点」的抽象。所以只用 X
的绘图原语的话,我们可以把 (1,1) 这个像素点涂黑,但是不能把 (0.5, 0.5)
这个点涂黑,这一设计缺陷在
<a class="reference external" href="http://web.mit.edu/~simsong/www/ugh.pdf">Unix Hater's Handbook</a>
中已经被吐槽过了。因为这个缺陷,所以直接用 X 绘图原语绘制的图像不能像
矢量图那样进行无损缩放。同样的缺陷导致 X 绘图原语绘制的字符不能做到
<ruby><rb>子像素级</rb><rp>(</rp><rt>subpixel-level</rt><rp>)</rp></ruby> <ruby><rb>抗锯齿</rb><rp>(</rp><rt>anti-aliasing</rt><rp>)</rp></ruby>
(这解释了默认配置下的 xterm 和
<a class="reference external" href="http://arch.acgtyrant.com/2015/01/05/I-do-not-recommend-urxvt-again-now/">urxvt 中的字体渲染为什么难看</a>
)。相比之下 GDI 有对应的 WMF 矢量图格式, Quartz 有对应的 PDF 矢量图格式,
而 X 中没有这样的格式对应。因为没有统一的矢量图格式,所以无论是 Cairo 、QPaint
还是没有用这些绘图库但是同样在意字体和曲线渲染效果的程序(比如 Firefox 和
Chromium)都需要首先渲染到内部的 <a class="reference external" href="http://en.wikipedia.org/wiki/X_PixMap">XPixMap</a>
位图格式,做好子像素渲染和矢量缩放,然后再把渲染好的位图转交给 X 图形服务器。</p>
</div>
<div class="section" id="composite">
<h2><a class="toc-backref" href="#id9">通过 Composite 扩展重定向窗口输出</a></h2>
<p>2004年发布的 X11R6.8 版本的 Xorg 引入了
<a class="reference external" href="http://freedesktop.org/wiki/Software/CompositeExt/">Composite 扩展</a>
。这个扩展背后的动机以及前因后果在一篇文章
<a class="reference external" href="http://keithp.com/~keithp/talks/xarch_ols2004/xarch-ols2004-html/">The (Re)Architecture of the X Window System</a>
中有详细的表述。Composite 扩展允许某个 X 程序做这几件事情:</p>
<ol class="arabic simple">
<li>通过 <code class="code">
RedirectSubwindows</code>
调用将一个窗口树中的所有窗口渲染重定向到
<ruby><rb>内部存储</rb><rp>(</rp><rt>off-screen storage</rt><rp>)</rp></ruby> 。重定向的时候可以指定让 X
自动更新窗口的内容到屏幕上或者由混成器手动更新。</li>
<li>通过 <code class="code">
NameWindowPixmap</code>
取得某个窗口的内部存储。</li>
<li>通过 <code class="code">
GetOverlayWindow</code>
获得一个特殊的用于绘图的窗口,
在这个窗口上绘制的图像将覆盖在屏幕的最上面。</li>
<li>通过 <code class="code">
CreateRegionFromBorderClip</code>
取得某个窗口的边界剪裁区域(不一定是矩形)。</li>
</ol>
<p>有了 Composite 扩展,一个 X 程序就可以调用这些 API 实现混成器。
这里有篇 <a class="reference external" href="http://www.talisman.org/~erlkonig/misc/x11-composite-tutorial/">教学解释如何使用 Composite 扩展</a> 。开启了混成的 X 是这样绘图的:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/18f7774d.png"/>
<p>整个 X 的混成器模型与 Mac OS X 的混成器模型相比,有如下几点显著的区别:</p>
<ol class="arabic simple">
<li>混成的部分是交由外部的程序完成的,对混成的绘制方式和绘制普通窗口一样。
出于效率考虑,绝大多数 X 上的混成器额外使用了 XRender 扩展或者
OpenGL/EGL 来加速绘制贴图。不过即使如此,还是不能避免同样的位图(内容不一定完全一致,
比如 X 可以在窗口交给它的位图上加上边框然后再返还给混成器) <strong>在不同的三个程序之间来回传递</strong> 。</li>
<li><code class="code">
RedirectSubwindows</code>
调用针对的是一个窗口树,换句话说是一个窗口
及其全部子窗口,不同于 Mac OS X 中混成器会拿到全部窗口的输出。
这个特点其实并不算是限制,因为 X 中每个虚拟桌面都有一个根窗口,只要指定这个根窗口
就可以拿到整个虚拟桌面上的全部可见窗口输出了。
反而这个设计提供了一定的自由度,比如我们可以用这个调用实现一个截图程序,
拿到某个特定窗口的输出,而不用在意别的窗口。</li>
<li>为了让窗口有输出,窗口必须显示在当前桌面上,不能处于最小化
状态或者显示在别的虚拟桌面,用 X 的术语说就是窗口必须处于 <ruby><rb>被映射</rb><rp>(</rp><rt>mapped</rt><rp>)</rp></ruby>
的状态。因此直接用上述方法 <strong>不能得到没有显示的窗口的输出</strong> ,比如不能对最小化的窗口
直接实现 Windows 7 中的 Aero Peak 之类的效果。这个限制可以想办法绕开,
比如在需要窗口输出的时候临时把窗口映射到桌面上,拿到输出之后再隐藏起来,
不过要实现这一点需要混成器和窗口管理器相互配合。</li>
<li>不像 Mac OS X 的基于 OpenGL Surface 的绘图模型是 <ruby><rb>设备无关</rb><rp>(</rp><rt>device independent</rt><rp>)</rp></ruby>
的,这里 X 的绘图模型是 <ruby><rb>设备相关</rb><rp>(</rp><rt>device dependent</rt><rp>)</rp></ruby> 的。
这既是优点也是缺点。从缺点方面而言,显示到 X 的位图输出因为设备相关性,
所以严格对应显示器的点阵,并不适合作为文档格式打印出来。当然无论是 Cairo
还是 QPaint 都提供了到 PostScript 或者 PDF 后端的输出,所以实用层面这个并不构成问题。
设备相关这一点的优点在于,绘制到 XPM 位图的时候,程序和绘图库是能拿到输出设备(显示器)
的特殊属性的,从而绘图库能考虑不同的色彩、分辨率、 DPI 或者
<ruby><rb>子像素布局</rb><rp>(</rp><rt>subpixel layout</rt><rp>)</rp></ruby> 这些属性以提供最好的渲染效果。
Mac OS X 10.4 在设计的时候也曾考虑过提供无极缩放的支持,而这种支持到了 Mac OS X
10.5 中就缩水变成了 Retina 的固定 2 倍缩放。这种局面在 X
上没有发生正是因为 X 的绘图模型的这种设备相关性,而 Mac OS X 的混成器采用的
OpenGL Surface 则无视了这些设备相关的属性。</li>
</ol>
</div>
<div class="section" id="id5">
<h2><a class="toc-backref" href="#id10">输入事件的重定向,这可能做到么?</a></h2>
<p>通过上述 Composite 扩展提供的 API ,混成器可以把窗口的 <strong>输出</strong> 重定向到自己的窗口上。
但是仅仅重定向输出,整个 X 还不处于可用状态,因为 <strong>没有重定向输入</strong> 。
考虑一下用户试图用鼠标点击某个按钮或者文本框,这时鼠标处于的位置是在 OverlayWindow
上绘制的位置,这个鼠标事件会交给 OverlayWindow ,而用户期待这个事件被发送给他看到的按钮上。</p>
<p>需要重定向的事件主要有键盘和鼠标事件两大类(暂时先不考虑触摸屏之类的额外输入)。
由于 Composite 扩展并没有直接提供这方面的重定向 API ,这使得输入事件处理起来都比较麻烦,</p>
<p>假设要重定向键盘事件,混成器需要效仿输入法框架(fcitx, ibus, scim)
那样处理一部分按键事件并把其余事件转给具有输入焦点的程序。
看看现有的输入法框架和诸多程序间的问题,我们就能知道这里的坑有多深。
于是 <strong>大部分 X 的混成器都不处理键盘事件重定向</strong>
。再来看重定向鼠标事件,这边的坑比重定向键盘事件的坑更多,
因为不像重定向窗口输出那样只需要考虑 <ruby><rb>顶层</rb><rp>(</rp><rt>top-level</rt><rp>)</rp></ruby> 窗口,
重定向鼠标输入的时候要考虑所有子窗口(它们有独立的事件队列),
以及要准确记录输入事件事件发生时的键盘组合键状态,还要正确实现 ICCCM/EWMH
中描述的转交窗口焦点的复杂规则,所有这些都已经在 X 中实现过的事情需要重新实现一遍。</p>
<p>由于坑太多难以实现,所以所有 X 下的混成器的实现方式都是直接忽略这个繁重的任务,
<strong>不重定向输入事件</strong> 而把它交给 X 处理。具体的实现方式就是通过
<a class="reference external" href="http://freedesktop.org/wiki/Software/FixesExt/">XFixes</a>
扩展提供的 <code class="code">
SetWindowShapeRegion</code>
API 将 OverlayWindow 的 <strong>输入区域</strong>
<code class="code">
ShapeInput</code>
设为空区域,从而忽略对这个 OverlayWindow 的一切鼠标键盘事件。
这样一来对 OverlayWindow 的点击会透过 OverlayWindow 直接作用到底下的窗口上。</p>
<p>因为选择了不重定向输入事件, X 下的混成器通常会处于以下两种状态:</p>
<ol class="arabic simple">
<li>选择状态下可以缩放窗口的大小,扭曲窗口的形状,并且可以把窗口绘制在任意想要绘制的位置上
(并不是移动窗口的位置), <strong>但是不能让用户与窗口的内容交互</strong> 。</li>
<li>正常状态下可以让用户与窗口的内容交互,但是
<strong>绘制的窗口位置、大小和形状必须严格地和 X 记录的窗口的位置、大小和形状保持一致</strong>
。持续时间短暂的动画效果可以允许位置和形状稍有偏差,但是在动画的过程中如果用户点击了
变形缩放过的窗口,那么鼠标事件将发往错误的( X 记录中的而非显示出的)窗口元素上。</li>
</ol>
<p>可以发现这两种状态就直接对应了 Gnome 3 的普通状态和缩略图状态(点击 <ruby><rb>活动</rb><rp>(</rp><rt>Activity</rt><rp>)</rp></ruby>
或者戳画面左上角之后显示的状态),这也解释了为什么尽管 Gnome 3
的窗口有硕大的关闭按钮,但是在缩略图状态下 Gnome 3 仍然需要给窗口加上额外的关闭按钮:
<strong>因为处于缩略状态下的窗口只是一张画而不能点</strong> 。</p>
<p>Composite 扩展的这些限制使得 X 下的混成器目前只能实现 Mac OS X 那样的 Exposé
效果,而不能实现 <a class="reference external" href="//farseerfc.me/zhs/brief-history-of-compositors-in-desktop-os.html#project-looking-glass-3d">LG3D</a> 那样直接在 3D 空间中操纵窗口内容。</p>
<p>解决重定向问题曾经的一缕曙光是 <ruby><rb>升阳公司</rb><rp>(</rp><rt>Sun Microsystems</rt><rp>)</rp></ruby> 在开发 <a class="reference external" href="//farseerfc.me/zhs/brief-history-of-compositors-in-desktop-os.html#project-looking-glass-3d">LG3D</a> 的过程中同时提议过另一个 X
扩展叫做 Event Interception 或者简称 <a class="reference external" href="http://freedesktop.org/wiki/Software/XEvIE/">XEvIE</a> ,这个扩展的设计目的就是提供 API
让某个程序接收并操纵全部的键盘和鼠标事件。可惜这个扩展随着升阳公司本身的陨落而
处于无人维护的状态,这一点也在它的官方网页上说明了:</p>
<blockquote>
It has been suggested that this extension should not be used
because it is broken and maintainerless.</blockquote>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id11">Composite 扩展的不足</a></h2>
<p>通过上面的介绍,我们就已经可以看到 Composite 扩展的不足之处了。
总结起来说,主要有两大不足:</p>
<ol class="arabic">
<li><p class="first">绘图效率低。因为同样的位图从应用程序传到 Xorg ,再从 Xorg 传到混成器,
最后从混成器再绘制到屏幕上,绕了一个大弯。这就是为什么 Wayland 的开发者在他的slide
<a class="reference external" href="http://people.freedesktop.org/~daniels/lca2013-wayland-x11.pdf">the real story behind Wayland and X</a>
里这么说:</p>
<blockquote>
<p>and what's the X server? really bad IPC</p>
<p>那么 X 服务器到底做了什么呢? 非常糟糕的进程间通讯</p>
</blockquote>
</li>
<li><p class="first">没有重定向输入事件。如果我们要在 X 的混成器里做这个事情,
基本上我们要全部重写一遍 X 已经写好的窗口事件分发逻辑。</p>
</li>
</ol>
<p>既然同样要重写,为什么不直接重写一遍 X 呢,扔掉那些历史负担,扔掉那些无用的 API
,重新设计可扩展的 API ,做好快速安全的 IPC —— 嗯,重写 X 就是 Wayland 的目的。</p>
<p>不过这么重写了的 Wayland 还是我们熟悉可爱的 X 么?它有哪些地方变样了?
这将是我下一篇文章的内容。</p>
</div>
<div class="section" id="id7">
<h2><a class="toc-backref" href="#id12">附录:扩展阅读</a></h2>
<p>我自己没有写过窗口管理器,没有写过混成器,没有写过 Wayland
程序,以上说的都是我从互联网上看到的整理出来的内容。写下本文的过程中我参考了这些文章:</p>
<p><a class="reference external" href="http://keithp.com/~keithp/talks/xarch_ols2004/xarch-ols2004-html/">The (Re)Architecture of the X Window System</a> 这篇2004年写的文章描述了 Composite
扩展出现的动机和历史,介绍了绘图库的实现情况,涉及了上面所说的那些 X 扩展被用到的情况和可能。
同时这篇文章还展望了很多现在的 X 已然实现了的功能,比如 OpenGL 和 X 的结合方面我们有了 <a class="reference external" href="http://en.wikipedia.org/wiki/GLX">GLX</a> 和 <a class="reference external" href="http://en.wikipedia.org/wiki/AIGLX">AIGLX</a>
,比如内核的显卡支持方面我们有了 <a class="reference external" href="http://en.wikipedia.org/wiki/Direct_Rendering_Infrastructure">DRI</a> 和 <a class="reference external" href="http://en.wikipedia.org/wiki/Mode_setting">KMS</a> 。总之这是一篇描述 Linux
桌面未来的发展轨迹的非常有阅读价值的历史文献。</p>
<p><a class="reference external" href="http://wingolog.org/archives/2008/07/26/so-you-want-to-build-a-compositor">so you want to build a compositor</a> 这是一篇 2008 年写的博文,介绍如何用 Clutter
实现一个最简单的混成器。</p>
<p><a class="reference external" href="http://www.talisman.org/~erlkonig/misc/x11-composite-tutorial/">Composite tutorial</a> 这是另一篇介绍如何实现一个简单的混成器的博文,用 Qt 实现,但是同样很底层。</p>
<p><a class="reference external" href="http://projects.mini-dweeb.org/projects/unagi">unagi</a> 这是一个可用的(但是已经长期没有开发的)类似 xcompmgr 的混成器。这个项目貌似
是一位研究生的硕士毕业设计,同时他公开了硕士学位的毕业论文
<a class="reference external" href="http://projects.mini-dweeb.org/attachments/download/3/report.pdf">Master thesis: Writing an X compositing manager</a>
其中也对实现一个简单的混成器做了详尽描述,包括介绍了相关的 X 扩展和调用。</p>
</div>
桌面系统的混成器简史2015-03-19T13:45:00+09:002015-03-19T13:45:00+09:00farseerfctag:farseerfc.me,2015-03-19:/zhs/brief-history-of-compositors-in-desktop-os.html
<p>(原本是想写篇关于 Wayland 的文章,后来越写越长感觉能形成一个系列,
于是就先把这篇背景介绍性质的部分发出来了。)</p>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>Linux 系统上要迎来 Wayland 了,或许大家能从各种渠道打听到 Wayland
是一个混成器,替代 X 作为显示服务器。
那么 <strong>混成器</strong> 是个什么东西,桌面系统为什么需要它呢?
要理解为什么桌面系统需要 <strong>混成器</strong> (或者它的另一个叫法,
<ruby><rb>混成窗口管理器</rb><rp>(</rp><rt>Compositing Window Manager</rt><rp>)</rp></ruby>
),在这篇文章中我想回顾一下历史,
了解一下混成器出现的前因后果。</p>
<p>首先介绍一下混成器出现前主要的一类窗口管理器,也就是
<ruby><rb>栈式窗口管理器</rb><rp>(</rp><rt>Stacking Window Manager</rt><rp>)</rp></ruby> 的实现方式。</p>
<!-- PELICAN_END_SUMMARY -->
<div class="label label-warning">
本文中所有桌面截图来自维基百科,不具有著作权保护。</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id11">早期的栈式窗口管理器</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
栈式窗口管理器的例子,Windows 3.11 的桌面</div>
<div class="panel-body">
<img alt="栈式窗口管理器的例子,Windows 3.11 的桌面" class="img-responsive" src="//farseerfc.me/zhs/images/Windows_3.11_workspace.png"/>
</div>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>我们知道最初图形界面的应用程序是全屏的,独占整个显示器(现在很多游戏机和手持设备的实现仍旧如此)。
所有程序都全屏并且任何时刻只能看到一个程序的输出,这个限制显然不能满足人们使用计算机的需求,
于是就有了 <a class="reference external" href="http://en.wikipedia.org/wiki/WIMP_(computing)">窗口 …</a></p></div>
<p>(原本是想写篇关于 Wayland 的文章,后来越写越长感觉能形成一个系列,
于是就先把这篇背景介绍性质的部分发出来了。)</p>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>Linux 系统上要迎来 Wayland 了,或许大家能从各种渠道打听到 Wayland
是一个混成器,替代 X 作为显示服务器。
那么 <strong>混成器</strong> 是个什么东西,桌面系统为什么需要它呢?
要理解为什么桌面系统需要 <strong>混成器</strong> (或者它的另一个叫法,
<ruby><rb>混成窗口管理器</rb><rp>(</rp><rt>Compositing Window Manager</rt><rp>)</rp></ruby>
),在这篇文章中我想回顾一下历史,
了解一下混成器出现的前因后果。</p>
<p>首先介绍一下混成器出现前主要的一类窗口管理器,也就是
<ruby><rb>栈式窗口管理器</rb><rp>(</rp><rt>Stacking Window Manager</rt><rp>)</rp></ruby> 的实现方式。</p>
<!-- PELICAN_END_SUMMARY -->
<div class="label label-warning">
本文中所有桌面截图来自维基百科,不具有著作权保护。</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id11">早期的栈式窗口管理器</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
栈式窗口管理器的例子,Windows 3.11 的桌面</div>
<div class="panel-body">
<img alt="栈式窗口管理器的例子,Windows 3.11 的桌面" class="img-responsive" src="//farseerfc.me/zhs/images/Windows_3.11_workspace.png"/>
</div>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>我们知道最初图形界面的应用程序是全屏的,独占整个显示器(现在很多游戏机和手持设备的实现仍旧如此)。
所有程序都全屏并且任何时刻只能看到一个程序的输出,这个限制显然不能满足人们使用计算机的需求,
于是就有了 <a class="reference external" href="http://en.wikipedia.org/wiki/WIMP_(computing)">窗口</a>
的概念,有了 <a class="reference external" href="http://en.wikipedia.org/wiki/Desktop_metaphor">桌面隐喻</a> 。</p>
<p>在 <ruby><rb>桌面隐喻</rb><rp>(</rp><rt>Desktop Metaphor</rt><rp>)</rp></ruby> 中每个窗口只占用显示面积的一小部分,
有其显示的位置和大小,可以互相遮盖。于是栈式窗口管理器就是在图形界面中实现桌面隐喻的核心功能,
其实现方式大体就是:给每个窗口一个相对的“高度”或者说“远近”,比较高的窗口显得距离用户比较近,
会覆盖其下比较低的窗口。绘图的时候窗口管理器会从把窗口按高低排序,按照从低到高的顺序使用
<a class="reference external" href="http://zh.wikipedia.org/wiki/%E7%94%BB%E5%AE%B6%E7%AE%97%E6%B3%95">画家算法</a>
绘制整个屏幕。</p>
<!-- PELICAN_END_SUMMARY -->
<p>这里还要补充一点说明,在当时图形界面的概念刚刚普及的时候,绘图操作是非常“昂贵”的。
可以想象一下 800x600 像素的显示器输出下,每帧
<a class="reference external" href="http://zh.wikipedia.org/wiki/%E7%9C%9F%E5%BD%A9%E8%89%B2">真彩色</a>
位图就要占掉 <span class="math">\(800 \times 600 \times 3 \approx 1.4 \text{MiB}\)</span> 的内存大小,30Hz
的刷新率(也就是30FPS)下每秒从 CPU 传往绘图设备的数据单单位图就需要
<span class="math">\(1.4 \times 30 = 41 \text{MiB}\)</span> 的带宽。对比一下当时的
<a class="reference external" href="http://en.wikipedia.org/wiki/VESA_Local_Bus">VESA 接口</a> 总的数据传输能力也就是
<span class="math">\(25 \text{MHz} \times 32 \text{bits} = 100 \text{MiB/s}\)</span> 左右,
而 Windows 3.1 的最低内存需求是 1MB,对当时的硬件而言无论是显示设备、内存或是CPU,
这无疑都是一个庞大的负担。</p>
<p>于是在当时的硬件条件下采用栈式窗口管理器有一个巨大 <strong>优势</strong> :如果正确地采用画家算法,
并且合理地控制重绘时 <strong>只绘制没有被别的窗口覆盖的部分</strong> ,那么无论有多少窗口互相
遮盖,都可以保证每次绘制屏幕的最大面积不会超过整个显示器的面积。
同样因为实现方式栈式窗口管理器也有一些难以回避的 <strong>限制</strong> :</p>
<ol class="arabic simple">
<li>窗口必须是矩形的,不能支持不规则形状的窗口。</li>
<li>不支持透明或者半透明的颜色。</li>
<li>为了优化效率,在缩放窗口和移动窗口的过程中,窗口的内容不会得到重绘请求,
必须等到缩放或者移动命令结束之后窗口才会重绘。</li>
</ol>
<p>以上这些限制在早期的 X11 窗口管理器比如 twm 以及 XP 之前经典主题的 Windows
或者经典的 Mac OS 上都能看到。
在这些早期的窗口环境中,如果你拖动或者缩放一个窗口,那么将显示变化后的窗口边界,
这些用来预览的边界用快速的位图反转方式绘制。当你放开鼠标的时候才会触发窗口的
重绘事件。
虽然有很多方法或者说技巧能绕过这些限制,比如 Windows XP 上就支持了实时的
重绘事件和不规则形状的窗口剪裁,不过这些技巧都是一连串的 hack ,难以扩展。</p>
</div>
<div class="section" id="nextstep-mac-os-x">
<h2><a class="toc-backref" href="#id12">NeXTSTEP 与 Mac OS X 中混成器的发展</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
NeXTSTEP 桌面</div>
<div class="panel-body">
<img alt="NeXTSTEP 桌面" class="img-responsive" src="//farseerfc.me/zhs/images/NeXTSTEP_desktop.png"/>
</div>
</div>
<p>转眼进入了千禧年, Windows 称霸了 PC 产业,苹果为重振 Macintosh 请回了 Jobs 基于 <a class="reference external" href="http://en.wikipedia.org/wiki/NeXTSTEP">NeXTSTEP</a>
开发 Mac OSX 。</p>
<p>NeXTSTEP 在当时提供的 GUI 界面技术相比较于同年代的 X 和 Windows 有一个很特别的地方:
拖动滚动条或者移动窗口的时候,窗口的内容是 <strong>实时更新</strong> 的,这比只显示一个缩放大小的框框来说被认为更直观。
而实现这个特性的基础是在 NeXTSTEP 中运用了
<a class="reference external" href="http://en.wikipedia.org/wiki/Display_PostScript">Display PostScript (DPS)</a>
技术,简单地说,就是每个窗口并非直接输出到显示设备,而是把内容输出到 (Display) PostScript
格式交给窗口管理器,然后窗口管理器再在需要的时候把 PostScript 用软件解释器解释成位图显示在屏幕上。</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/8d293c10.png"/>
<p>比起让窗口直接绘制,这种方案在滚动和移动窗口的时候不需要重新渲染保存好的 DPS ,
所以能实现实时渲染。到了实现 Mac OS X 的时候,为了同时兼容老的 Mac 程序 API (carbon)
以及更快的渲染速度,以及考虑到 Adobe 对苹果收取的高昂的 Display PostScript 授权费,
Mac OS X 的 Quartz 技术在矢量图的 PDF 描述模型和最终渲染之间又插入了一层抽象:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/5807c26a.png"/>
<div class="panel panel-default">
<div class="panel-heading">
Mission Control</div>
<div class="panel-body">
<img alt="Mission Control" class="img-responsive" src="//farseerfc.me/zhs/images/Mac_OS_X_Lion_Preview_-_Mission_Control.jpg"/>
</div>
</div>
<p>也就是说在 Mac OS X 中无论窗口用何种方式绘图,都会绘制输出成一副内存中的位图交给混成器,
而后者再在需要的时候将位图混成在屏幕上。这种设计使得 2001年3月发布的 Mac OS X v10.0
成为了第一个广泛使用的具有软件混成器的操作系统。</p>
<p>到了 Mac OS X v10.2 的时候,苹果又引入了 Quartz Extreme 让最后的混成渲染这一步发生在
显卡上。然后在 2003年1月公开亮相的 Mac OS X v10.3 中,他们公布了 Exposé (后来改名为
Mission Control) 功能,把窗口的缩略图(而不是事先绘制的图标)并排显示在桌面上,
方便用户挑选打开的窗口。</p>
<p>由于有了混成器的这种实现方式,使得可能把窗口渲染的图像做进一步加工,添加阴影、三维和动画效果。
这使得 Mac OS X 有了美轮美奂的动画效果和 Exposé 这样的方便易用的功能。
或许对于乔布斯而言,更重要的是因为有了混成器,窗口的形状终于能显示为他
<a class="reference external" href="http://www.folklore.org/StoryView.py?story=Round_Rects_Are_Everywhere.txt">梦寐以求</a>
的 <a class="reference external" href="http://www.uiandus.com/blog/2009/7/26/realizations-of-rounded-rectangles.html">圆角矩形</a>
了!</p>
</div>
<div class="section" id="project-looking-glass-3d">
<h2><a class="toc-backref" href="#id13">插曲:昙花一现的 Project Looking Glass 3D</a></h2>
<p>在苹果那边刚刚开始使用混成器渲染窗口的 2003 年,昔日的 <ruby><rb>升阳公司</rb><rp>(</rp><rt>Sun Microsystems</rt><rp>)</rp></ruby>
则在 Linux 和 Solaris 上用 Java3D 作出了另一个炫酷到没有朋友的东西,被他们命名为
<a class="reference external" href="http://en.wikipedia.org/wiki/Project_Looking_Glass">Project Looking Glass 3D</a>
(缩写LG3D,别和 Google 的 Project Glass 混淆呀)。这个项目的炫酷实在难以用言语描述,
好在还能找到两段视频展示它的效果。</p>
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_JXv8VlpoK_g">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/JXv8VlpoK_g"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XOTEzMzM3MTY0">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XOTEzMzM3MTY0" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_JXv8VlpoK_g">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XOTEzMzM3MTY0">Youku</a></li>
</ul>
</div>
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_zcPIEMvyPy4">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/zcPIEMvyPy4"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XOTEzMzQwMjky">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XOTEzMzQwMjky" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_zcPIEMvyPy4">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XOTEzMzQwMjky">Youku</a></li>
</ul>
</div>
<div class="panel panel-default">
<div class="panel-heading">
LG3D</div>
<div class="panel-body">
<img alt="LG3D" class="img-responsive" src="//farseerfc.me/zhs/images/LG3D.jpg"/>
</div>
</div>
<p>如视频中展示的那样, LG3D 完全突破了传统的栈式窗口管理方式,
在三维空间中操纵二维的窗口平面,不仅像传统的窗口管理器那样可以缩放和移动窗口,
还能够旋转角度甚至翻转到背面去。从视频中难以体会到的一点是, LG3D 在实现方式上与
Mac OS X 中的混成器有一个本质上的不同,那就是处于(静止或动画中)缩放或旋转状态
下的窗口是 <strong>可以接受输入事件</strong> 的。这一重要区别在后面 Wayland 的说明中还会提到。
LG3D 项目展示了窗口管理器将如何突破传统的栈式管理的框架,可以说代表了窗口管理器的未来发展趋势。</p>
<p>LG3D 虽然以 GPL 放出了实现的源代码,不过整个项目已经停滞开发许久了。
官方曾经放出过一个
<a class="reference external" href="http://sourceforge.net/projects/lg3d-livecd/">预览版的 LiveCD</a>
。可惜时隔久远(12年前了)在我的 VirtualBox 上已经不能跑起来这个 LiveCD 了……</p>
<p>更为可惜的是,就在这个项目刚刚公开展示出来的时候,乔布斯就致电升阳,
说如果继续商业化这个产品,升阳公司将涉嫌侵犯苹果的知识产权
(时间顺序上来看,苹果最初展示 Exposé 是在 2003年6月23日的
Apple Worldwide Developers Conference ,而升阳最初展示
LG3D 是在 2003年8月5日的 LinuxWorld Expo)。
虽然和乔布斯的指控无关,升阳公司本身的业务也着重于服务器端的业务,
后来随着升阳的财政困难,这个项目也就停止开发并不了了之了。</p>
</div>
<div class="section" id="windows">
<h2><a class="toc-backref" href="#id14">Windows 中的混成器</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
Longhorn 中的 Wobbly 效果</div>
<div class="panel-body">
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_X0idaN0MY1U">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/X0idaN0MY1U"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XOTEzMzY5NjQ0">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XOTEzMzY5NjQ0" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_X0idaN0MY1U">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XOTEzMzY5NjQ0">Youku</a></li>
</ul>
</div>
</div>
</div>
<p>上面说到, Windows 系列中到 XP 为止都还没有使用混成器绘制窗口。
看着 Mac OS X 上有了美轮美奂的动画效果, Windows 这边自然不甘示弱。
于是同样在 2003 年展示的 Project Longhorn 中就演示了 wobbly 效果的窗口,
并且跳票推迟多年之后的 Windows Vista 中实现了完整的混成器
<a class="reference external" href="http://en.wikipedia.org/wiki/Desktop_Window_Manager">Desktop Window Manager (DWM)</a>
。整个 DWM 的架构和 Mac OS X 上看到的很像:</p>
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/1763baf6.png"/>
<p>和 Mac OS X 的情况类似, Windows Vista 之后的应用程序有两套主要的绘图库,一套是从早期
Win32API 就沿用至今的 GDI(以及GDI+),另一套是随着 Longhorn 计划开发出的 WPF 。
WPF 的所有用户界面控件都绘制在 DirectX 贴图上,所以使用了 WPF 的程序也可以看作是
DirectX 程序。而对老旧的 GDI 程序而言,它们并不是直接绘制到 DirectX 贴图的。首先每一个
GDI 的绘图操作都对应一条
<a class="reference external" href="http://en.wikipedia.org/wiki/Windows_Metafile">Windows Metafile (WMF)</a>
记录,所以 WMF 就可以看作是 Mac OS X 的 Quartz 内部用的 PDF 或者 NeXTSTEP 内部用的
DPS,它们都是矢量图描述。随后,这些 WMF 绘图操作被通过一个
Canonical Display Driver (cdd.dll) 的内部组建转换到 DirectX 平面,并且保存起来交给
DWM。最后, DWM 拿到来自 CDD 或者 DirectX 的平面,把它们混合起来绘制在屏幕上。</p>
<p>值得注意的细节是,WPF 底层的绘图库几乎肯定有 C/C++ 绑定对应, Windows 自带的不少应用程序
和 Office 2007 用了 Ribbon 之后的版本都采用这套绘图引擎,不过微软没有公开这套绘图库的
C/C++ 实现的底层细节,而只能通过 .Net 框架的 WPF 访问它。这一点和 OS X 上只能通过
Objective-C 下的 Cocoa API 调用 Quartz 的情况类似。</p>
<p>另外需要注意的细节是 DirectX 的单窗口限制在 Windows Vista 之后被放开了,或者严格的说是
基于 WDDM 规范下的显卡驱动支持了多个 DirectX 绘图平面。
在早期的 Windows 包括 XP 上,整个桌面上同一时刻只能有一个程序的窗口处于 DirectX 的
<strong>直接绘制</strong> 模式,而别的窗口如果想用 DirectX 的话,要么必须改用软件渲染要么就不能工作。
这种现象可以通过打开多个播放器或者窗口化的游戏界面观察到。
而在 WDDM 规范的 Vista 中,所有窗口最终都绘制到 DirectX 平面上,换句话说每个窗口都是
DirectX 窗口。又或者我们可以认为,整个界面上只有一个真正的窗口也就是 DWM 绘制的全屏窗口,
只有 DWM 处于 DirectX 的直接渲染模式下,而别的窗口都输出到 DirectX 平面里(可能通过了硬件加速)。</p>
<p>由 DWM 的这种实现方式,可以解释为什么
<a class="reference external" href="http://gaming.stackexchange.com/questions/13066/why-is-windowed-mode-always-slower-in-games">窗口模式下的游戏总是显得比较慢</a>
,原因是整个桌面有很多不同的窗口都需要 DWM 最后混成,而如果在全屏模式下,只有游戏
处于 DirectX 的直接渲染方式,从而不会浪费对游戏而言宝贵的 GPU 资源。</p>
<p>由于 DWM 实现了混成器,使得 Vista 和随后的 Windows 7 有了
<a class="reference external" href="http://en.wikipedia.org/wiki/Windows_Aero">Aero Glass</a> 的界面风格,
有了 Flip 3D 、Aero Peek 等等的这些辅助功能和动画效果。
这套渲染方式延续到 Windows 8 之后,虽然 Windows 8 还提出了 Modern UI
不过传统桌面上的渲染仍旧是依靠混成器来做的。</p>
</div>
<div class="section" id="linux">
<h2><a class="toc-backref" href="#id15">这就结束了? Linux 桌面呢?</a></h2>
<p>别急,我写这些文章的目的是想聊聊 Linux 中的混成器,尤其是 X 下现有的混成器和 Wayland
,这篇文章只是个背景介绍。关于 X 中混成器的实现方式和限制,且听我下回分解。</p>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>避免在博文中写「简单地」2015-03-11T22:00:00+09:002015-03-11T22:00:00+09:00farseerfctag:farseerfc.me,2015-03-11:/zhs/stop-write-simply.html<p>我的 RSS 订阅着一个博客叫 <a class="reference external" href="http://blogs.msdn.com/b/oldnewthing/">The Old New Thing</a>
,作者是Windows开发者之一的 Raymond Chen ,记录 Windows 中的很多有趣的技术细节。
这个博客中的一些精彩内容还被他写成了一本书,中文名叫《Windows编程启示录》
(ISBN: <a class="reference external" href="http://www.amazon.cn/dp/B0011C1ZEG/">978-7-111-21919-4</a>) 而英文书名就叫
<em>The Old New Thing — Practical Development Throughout the Evolution of Windows</em>
(ISBN: <a class="reference external" href="http://www.amazon.com/gp/product/0321440307">978-0-321-44030-3</a>)。</p>
<p>今天看到这个博客的一篇文章说
<a class="reference external" href="http://blogs.msdn.com/b/oldnewthing/archive/2015/03/10/10598846.aspx">你用「简单地」次数越多我越怀疑你不懂这个词的意思</a> , 描述他看到某个博客上指导读者打开命令行、执行某条魔法命令、从命令输出抽取参数、
改写配置文件、用魔法命令重启服务,并把这些工作描述为「简单地」。</p>
<p>的确正如 Raymond 指出,一个人觉得简单的事情对别人并不一定是简单的。
搜了一下我自己写的东西,的确很多地方写了「简单 …</p><p>我的 RSS 订阅着一个博客叫 <a class="reference external" href="http://blogs.msdn.com/b/oldnewthing/">The Old New Thing</a>
,作者是Windows开发者之一的 Raymond Chen ,记录 Windows 中的很多有趣的技术细节。
这个博客中的一些精彩内容还被他写成了一本书,中文名叫《Windows编程启示录》
(ISBN: <a class="reference external" href="http://www.amazon.cn/dp/B0011C1ZEG/">978-7-111-21919-4</a>) 而英文书名就叫
<em>The Old New Thing — Practical Development Throughout the Evolution of Windows</em>
(ISBN: <a class="reference external" href="http://www.amazon.com/gp/product/0321440307">978-0-321-44030-3</a>)。</p>
<p>今天看到这个博客的一篇文章说
<a class="reference external" href="http://blogs.msdn.com/b/oldnewthing/archive/2015/03/10/10598846.aspx">你用「简单地」次数越多我越怀疑你不懂这个词的意思</a> , 描述他看到某个博客上指导读者打开命令行、执行某条魔法命令、从命令输出抽取参数、
改写配置文件、用魔法命令重启服务,并把这些工作描述为「简单地」。</p>
<p>的确正如 Raymond 指出,一个人觉得简单的事情对别人并不一定是简单的。
搜了一下我自己写的东西,的确很多地方写了「简单」二字,这的确对读者不友好。</p>
<p>从今往后避免用「简单」来描述。</p>
用 Travis-CI 生成 Github Pages 博客2015-02-20T11:10:00+09:002015-02-20T11:10:00+09:00farseerfctag:farseerfc.me,2015-02-20:/zhs/travis-push-to-github-pages-blog.html
<div class="label label-warning">
<strong>2015年2月21日更新</strong></div>
<p>上次介绍过 <a class="reference external" href="//farseerfc.me/zhs/redesign-pelican-theme.html">这个博客改换了主题</a> ,
本以为这个话题可以告一段落了,没想到还能继续写呢。</p>
<p>寄宿在 Github Pages 上的静态博客通常有两种方案,其一是使用 <a class="reference external" href="http://jekyllrb.com/">Jekyll</a> 方式撰写,这可以利用
Github Pages 原本就有的
<a class="reference external" href="https://help.github.com/articles/using-jekyll-with-pages/">Jekyll支持</a>
生成静态网站。另一种是在 <strong>本地</strong> 也就是自己的电脑上生成好,然后把生成的 HTML 网站 push
到 Github Pages ,这种情况下 Github Pages 就完全只是一个静态页面宿主环境。</p>
<p>我用 <a class="reference external" href="http://getpelican.com/">Pelican</a> 生成博客,当然就只能选择后一种方式了。这带来一些不便,比如本地配置 pelican
还是有一点点复杂的,所以不能随便找台电脑就开始写博客。有的时候只是想修正一两个错别字,
这时候必须打开某台特定的电脑才能编辑博客就显得不太方便了。再比如 pelican 本身虽然是 python
写的所以跨平台,但是具体到博客的配置方面, Windows …</p>
<div class="label label-warning">
<strong>2015年2月21日更新</strong></div>
<p>上次介绍过 <a class="reference external" href="//farseerfc.me/zhs/redesign-pelican-theme.html">这个博客改换了主题</a> ,
本以为这个话题可以告一段落了,没想到还能继续写呢。</p>
<p>寄宿在 Github Pages 上的静态博客通常有两种方案,其一是使用 <a class="reference external" href="http://jekyllrb.com/">Jekyll</a> 方式撰写,这可以利用
Github Pages 原本就有的
<a class="reference external" href="https://help.github.com/articles/using-jekyll-with-pages/">Jekyll支持</a>
生成静态网站。另一种是在 <strong>本地</strong> 也就是自己的电脑上生成好,然后把生成的 HTML 网站 push
到 Github Pages ,这种情况下 Github Pages 就完全只是一个静态页面宿主环境。</p>
<p>我用 <a class="reference external" href="http://getpelican.com/">Pelican</a> 生成博客,当然就只能选择后一种方式了。这带来一些不便,比如本地配置 pelican
还是有一点点复杂的,所以不能随便找台电脑就开始写博客。有的时候只是想修正一两个错别字,
这时候必须打开某台特定的电脑才能编辑博客就显得不太方便了。再比如 pelican 本身虽然是 python
写的所以跨平台,但是具体到博客的配置方面, Windows 环境和 Linux/OSX/Unix-like
环境下还是有
<a class="reference external" href="http://pelican.readthedocs.org/en/latest/settings.html#date-format-and-locale">些许出入</a>
的。还有就是没有像 wordpress 那样的基于 web
的编辑环境,在手机上就不能随便写一篇博客发表出来(不知道有没有勇士尝试过在
Android 的 <a class="reference external" href="https://code.google.com/p/android-scripting/">SL4A</a> 环境下的 python 中跑 pelican ,还要配合一个
<a class="reference external" href="https://play.google.com/store/apps/details?id=com.romanenco.gitt">Android 上的 git 客户端</a> )。</p>
<p>当然并不是因此就束手无策了,感谢 <a class="reference external" href="https://travis-ci.org/">Travis-CI</a> 提供了免费的
<ruby><rb>持续整合</rb><rp>(</rp><rt>Continuous integration</rt><rp>)</rp></ruby> 虚拟机环境,
通过它全自动生成静态博客成为了可能。</p>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id10">关于 Travis-CI</a></h2>
<p><a class="reference external" href="http://zh.wikipedia.org/wiki/%E6%8C%81%E7%BA%8C%E6%95%B4%E5%90%88">持续整合</a>
原本是 <ruby><rb>敏捷开发</rb><rp>(</rp><rt>Agile Development</rt><rp>)</rp></ruby>
或者 <ruby><rb>极限编程</rb><rp>(</rp><rt>Extreme Programming</rt><rp>)</rp></ruby> 中提到的概念,大意就是说在开发的过程中,
一旦有微小的变更,就全自动地 <strong>持续</strong> 合并到主线中, <strong>整合</strong> 变更的内容到发布版本里。
这里的 <strong>整合</strong> 实际上可以理解为 <strong>全自动测试</strong> 加上 <strong>生成最终产品</strong> 。
可以看到 <strong>持续整合</strong> 实际强调 <strong>全自动</strong> ,于是需要有一个服务器不断地监听主线开发的变更内容,
一旦有任何变更(可以理解为 git commit )就自动调用测试和部署脚本。</p>
<p>于是要用持续整合就需要一个整合服务器,幸而 Travis-CI 对 github 上的公开 repo
提供了免费的整合服务器虚拟机服务,和 github 的整合非常自然。所以我们就可以用它提供的虚拟机
为博客生成静态网站。</p>
</div>
<div class="section" id="id6">
<h2><a class="toc-backref" href="#id11">启用 Travis-CI 自动编译</a></h2>
<p>这一步很简单,访问 <a class="reference external" href="https://travis-ci.org/">https://travis-ci.org/</a> 并用你的 Github 账户登录,
授权它访问你的账户信息就可以了。然后在 <a class="reference external" href="https://travis-ci.org/repositories">https://travis-ci.org/repositories</a> 里开启
需要编译的 repo ,这样 Travis-CI 就会监视对这个 repo 的所有 push 操作,并且对
每个 push 调用测试了。</p>
<div class="figure">
<img alt="在 Travis-CI 中开启对 Github Repo 的持续整合" class="img-responsive" src="//farseerfc.me/zhs/images/travis-repo-enable.png"/>
<p class="caption">在 Travis-CI 中开启对 Github Repo 的持续整合</p>
</div>
<p>然后在 repo 的根目录放一个 <code class="code">
.travis.yml</code>
文件描述编译的步骤。
<strong>暂时</strong> 测试的目的下我写的 <code class="code">
.travis.yml</code>
大概是下面这样。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="nt">language</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">python</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nt">python</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="s">"2.7"</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nt">before_install</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo apt-add-repository ppa:chris-lea/node.js -y</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo apt-get update</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo apt-get install nodejs ditaa doxygen parallel</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nt">install</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo pip install pelican</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo pip install jinja2</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo pip install babel</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo pip install beautifulsoup4</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo pip install markdown</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo npm install -g less</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">wget "http://downloads.sourceforge.net/project/plantuml/plantuml.jar?r=&ts=1424308684&use_mirror=jaist" -O plantuml.jar</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo mkdir -p /opt/plantuml</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo cp plantuml.jar /opt/plantuml</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">echo "#! /bin/sh" > plantuml</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">echo 'exec java -jar /opt/plantuml/plantuml.jar "$@"' >> plantuml</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo install -m 755 -D plantuml /usr/bin/plantuml</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">wget https://bintray.com/artifact/download/byvoid/opencc/opencc-1.0.2.tar.gz</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">tar xf opencc-1.0.2.tar.gz</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">cd opencc-1.0.2 && make && sudo make install && cd ..</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo locale-gen zh_CN.UTF-8</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo locale-gen zh_HK.UTF-8</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo locale-gen en_US.UTF-8</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">sudo locale-gen ja_JP.UTF-8</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nt">script</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">mkdir output</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">env SITEURL="farseerfc.me" make publish</span></span>
</pre></div>
<p>Travis-CI 提供的虚拟机是比较标准的 Ubuntu 12.04 LTS ,打上了最新的补丁,并且根据你指定的
语言选项会把相应的解释器和编译器升级到最新版(或者指定的版本)。这里用 python 语言的配置,
所以 python 是 2.7 的最新版并且有 pip 可以直接用。
配置中的 before_install 和 install 的区别其实不大,其中任何一个失败的话算作
build errored 而不是 build fail ,而如果在 script 里失败的话算作 build fail 。</p>
<p>为了编译我的模板,还需要比较新的 less.js ,所以添加了 ppa 装了个最新的 nodejs
并用它装上了 less 。
还从源码编译安装上了最新版的 opencc 1.0.2 ,因为 Ubuntu 源里的 opencc 的版本比较老(0.4),
然后 doxygen 作为 opencc 的编译依赖也装上了。
其它安装的东西么,除了 pelican 之外都是插件们需要的。以及我还需要生成 4 个语言的 locale
所以调用了 4 次 locale-gen 。由于是比较标准的 Ubuntu 环境,所以基本上编译的步骤和在本地
Linux 环境中是一样的,同样的这套配置应该可以直接用于本地 Ubuntu 下编译我的博客。</p>
<p>写好 <code class="code">
.travis.yml</code>
之后把它 push 到 github ,然后 travis 这边就会自动 clone
下来开始编译。 travis 上能看到编译的完整过程和输出,一切正常的话编译结束之后
build 的状态就会变成 passing ,比如
<a class="reference external" href="https://travis-ci.org/farseerfc/farseerfc/builds/51344614">我的这次的build</a> 。</p>
</div>
<div class="section" id="travis-ci-github">
<h2><a class="toc-backref" href="#id12">从 Travis-CI 推往 Github</a></h2>
<p>上面的测试编译通过了之后,下一步就是让 travis-ci 编译的结果自动推到 Github Pages
并发布出来。要推往 Github 自然需要设置 Github 用户的身份,在本地设置的时候是把
ssh key 添加到 github 账户就可以了,在编译细节都通过 github repo 公开了的 travis 上
当然不能放推送用的私有 key ,所以我们需要另外一种方案传递密码。</p>
<div class="panel panel-default">
<div class="panel-heading">
Github 上创建 Personal Access Token</div>
<div class="panel-body">
<img alt="Github 上创建 Personal Access Token" class="img-responsive" src="//farseerfc.me/zhs/images/travis-blog-push.png"/>
</div>
</div>
<p>好在 Github 支持通过 <a class="reference external" href="https://github.com/settings/applications">Personal Access Token</a>
的方式验证,这个和 App Token 一样可以随时吊销,同时完全是个人创建的。另一方面 Travis-CI
支持加密一些私密数据,通过环境变量的方式传递给编译脚本,避免公开密码这样的关键数据。</p>
<p>首先创建一个 <a class="reference external" href="https://github.com/settings/applications">Personal Access Token</a>
,这里需要勾选一些给这个 Token 的权限,我只给予了最小的 public_repo 权限,如侧边里的图。
生成之后会得到一长串 Token 的散列码。</p>
<div class="panel panel-default">
<div class="panel-heading">
如果你不能使用 travis 命令</div>
<div class="panel-body">
<div class="label label-warning">
<strong>2015年2月21日更新</strong></div>
<p>使用 <code class="code">
travis encrypt</code>
命令来加密重要数据最方便,不过如果有任何原因,
比如 ruby 版本太低或者安装不方便之类的,那么不用担心,我们直接通过
<a class="reference external" href="http://docs.travis-ci.com/api/#repository-keys">travis api</a>
也能加密数据。</p>
<p>第一步用这个命令得到你的repo的 pubkey :</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="go">curl -H "Accept: application/vnd.travis-ci.2+json" https://api.travis-ci.org/repos/<github-id/repo>/key | python2 -m json.tool | grep key | sed 's/.*"key": "\(.*\)"/\1/' | xargs -0 echo -en | sed 's/ RSA//' > travis.pem</span></span>
</pre></div>
<p>其中的 <github-id/repo> 替换成 github 上的 用户名/repo名, 比如我的是
farseerfc/farseer 。travis api 获得的结果是一个 json ,所以还用 python 的
json 模块处理了一下,然后把其中包含 key 的行用 <code class="code">
grep</code>
提取出来,用
<code class="code">
sed</code>
匹配出 key 的字符串本身,然后 <code class="code">
xargs -0 echo -en</code>
解释掉转义字符,然后删掉其中的 "<空格>RSA" 几个字(否则 openssl 不能读),
最后保存在名为 travis.pem 的文件里。</p>
<p>有了 pubkey 之后用 openssl 加密我们需要加密的东西并用 base64 编码:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="go">echo -n 'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>' | openssl rsautl -encrypt -pubin -inkey travis.pem | base64 -w0</span></span>
</pre></div>
<p>替换了相应的身份信息和token之后,这行得到的结果就是 secure 里要写的加密过的内容。</p>
</div>
</div>
<p>然后我们需要 <code class="code">
travis</code>
命令来加密这个 token , archlinux 用户可以安装
<code class="code">
aur/ruby-travis</code>
,其它用户可以用 gems 安装:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> gem install travis</span>
</pre></div>
<p>装好之后,在设定了 Travis-CI 的 repo 的目录中执行一下 <code class="code">
travis status</code>
,
命令会指导你登录 Travis-CI 并验证 repo 。正常的话会显示最新的 build 状态。
然后同样在这个 repo 目录下执行:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> travis encrypt <span class="s1">'GIT_NAME="Jiachen Yang" GIT_EMAIL=farseerfc@gmail.com GH_TOKEN=<Personal Access Token>'</span></span>
</pre></div>
<p>当然上面一行里的相应信息替换为个人的信息,作为这个命令的执行结果会得到另一长串散列码,
把这串散列写入刚才的 <code class="code">
.travis.yml</code>
文件:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="nt">env</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="nt">secure</span><span class="p">:</span> <span class="s">"long</span><span class="nv"> </span><span class="s">secure</span><span class="nv"> </span><span class="s">base64</span><span class="nv"> </span><span class="s">string"</span></span>
</pre></div>
<p>有了这段声明之后, Travis-CI 就会在每次编译之前,设置上面加密的环境变量。
然后在编译脚本中利用这些环境变量来生成博客:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="nt">script</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git config --global user.email "$GIT_EMAIL"</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git config --global user.name "$GIT_NAME"</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git config --global push.default simple</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git clone --depth 1 https://github.com/farseerfc/pelican-plugins plugins</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git clone --depth 1 https://github.com/farseerfc/pelican-bootstrap3 theme</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git clone --depth 1 https://$GH_TOKEN@github.com/farseerfc/farseerfc.github.io output</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">env SITEURL="farseerfc.me" make publish</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nt">after_success</span><span class="p">:</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">cd output</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git add -A .</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git commit -m "update from travis"</span></span>
<span class="code-line"> <span class="p p-Indicator">-</span> <span class="l l-Scalar l-Scalar-Plain">git push --quiet</span></span>
</pre></div>
<div class="alert alert-warning compound">
<p>这里要注意最后 <code class="code">
git push</code>
的时候一定要加上 <code class="code">
--quiet</code>
,因为默认不加的时候会把
代入了 <code class="code">
$GH_TOKEN</code>
的 URL 显示出来,从而上面的加密工作就前功尽弃了……</p>
</div>
<p>根据 <a class="reference external" href="http://docs.travis-ci.com/user/build-lifecycle/">travis 的文档</a>
, after_success 里写的步骤只有在 script 里的全都完全无错执行完之后才会执行,这正是我们
push 的条件。目前 after_success 的成功与否不会影响到 build 的状态。
具体我用的配置见
<a class="reference external" href="https://github.com/farseerfc/farseerfc/blob/master/.travis.yml">这里的最新版</a> 。
在我的 <code class="code">
make github</code>
中
<a class="reference external" href="https://github.com/farseerfc/farseerfc/blob/master/Makefile#L102">调用了</a>
<code class="code">
git push</code>
命令,从而执行了 <code class="code">
make github</code>
之后就会自动部署到 github 上。</p>
</div>
<div class="section" id="web">
<h2><a class="toc-backref" href="#id13">用 Web 编辑并发布静态博客</a></h2>
<p>经过以上设置之后,一切正常的话,每次对主 repo 推送更新的同时, Travis-CI 就会自动
拉来更新然后编译并发布了。可以放置这样的图标 <img alt="travisIcon" class="img-responsive no-responsive" src="https://travis-ci.org/farseerfc/farseerfc.svg?branch=master"/> 在项目的 <code class="code">
Readme.md</code>
中显示编译状态。</p>
<p>这样设置之后的另一个好处就在于可以利用 Github 的 Web 界面编辑文章内容。在 Github 里
编辑和保存之后会自动作为一个 commit 提交,所以也会触发 Travis-CI 的自动编译。</p>
<div class="figure">
<img alt="在 Github 的 Web 界面中直接编辑文章内容" class="img-responsive" src="//farseerfc.me/zhs/images/travis-edit-github-web.png"/>
<p class="caption">在 Github 的 Web 界面中直接编辑文章内容</p>
</div>
<p>以及虽然目前还没有好用的 Github 的手机客户端,不过直接用 Android/iPhone 的浏览器登录
github 并编辑文章的可用性也还不错,所以同样的方式也可以直接在手机上发布博文了。</p>
<p>That is all, happy blogging ~</p>
</div>
从天气预报谈谈日本的学术氛围2015-02-18T21:00:00+09:002015-02-18T21:00:00+09:00farseerfctag:farseerfc.me,2015-02-18:/zhs/weather-forcast-academic-in-japan.html<p>最近 <a class="reference external" href="/links.html#mazk">mazk</a> 说我 <a class="reference external" href="//farseerfc.me/zhs/pages/about.html#comment-1856339316">life 分类里的文章太少</a>
,所以想了想写了这篇。</p>
<p>很多人问过我为什么要来日本留学,嘛原因之一是我英语太差了,相对而言日语比较好。
另一方面,我比较喜欢日本的学术氛围。这个当然是主观体会,而不是客观的评价,只是我
觉得相对于 <strong>欧美喜欢研究基础架构技术</strong> , <strong>日本则偏向实用层面</strong> 。</p>
<p>说个具体一点例子,最近看到这篇新闻说
<a class="reference external" href="http://www.solidot.org/story?sid=43079">卢布贬值影响中央气象台预报准确率?</a>
,其中提到:</p>
<blockquote>
因为卢布贬值,天气预报的准确率会有所降低</blockquote>
<p>也说道:</p>
<blockquote>
不过经我多年的观察,中国中央气象台的预报准确率实在是不怎么样,具体到我生活的地区,
实际天气状况和中国中央气象台预报的出入较大……</blockquote>
<p>相信不少人也有类似的体会。</p>
<p>天气预报是事关人们生活的重要信息,其准确度对生产生活当然有很大影响。
说到增加天气预报的准确度,人们自然会想到高性能的超级计算机比如
<a class="reference external" href="https://zh.wikipedia.org/wiki/%E5%A4%A9%E6%B2%B3%E4%BA%8C%E5%8F%B7">天河二号</a>
,想到环绕在地球高空的 <a class="reference external" href="https://zh.wikipedia.org/wiki/%E6%B0%A3%E8%B1%A1%E8%A1%9B%E6%98%9F">气象卫星</a>
,想到遍布世界各地的气象站观测台。想想这么多耗资不菲的高尖端项目被国家投入,
用来改善天气预报的准确程度,看起来这的确是一个困难的科研课题。</p>
<p>话说回来,准确预测气温、气压、湿度、降水概率等等这些事情对于生产生活固然重要,
不过对一般民众而言,天气预报最重要的作用就只是回答 <strong>明天我该穿多厚的衣服,出门是否需要打伞</strong>
这种问题 …</p><p>最近 <a class="reference external" href="/links.html#mazk">mazk</a> 说我 <a class="reference external" href="//farseerfc.me/zhs/pages/about.html#comment-1856339316">life 分类里的文章太少</a>
,所以想了想写了这篇。</p>
<p>很多人问过我为什么要来日本留学,嘛原因之一是我英语太差了,相对而言日语比较好。
另一方面,我比较喜欢日本的学术氛围。这个当然是主观体会,而不是客观的评价,只是我
觉得相对于 <strong>欧美喜欢研究基础架构技术</strong> , <strong>日本则偏向实用层面</strong> 。</p>
<p>说个具体一点例子,最近看到这篇新闻说
<a class="reference external" href="http://www.solidot.org/story?sid=43079">卢布贬值影响中央气象台预报准确率?</a>
,其中提到:</p>
<blockquote>
因为卢布贬值,天气预报的准确率会有所降低</blockquote>
<p>也说道:</p>
<blockquote>
不过经我多年的观察,中国中央气象台的预报准确率实在是不怎么样,具体到我生活的地区,
实际天气状况和中国中央气象台预报的出入较大……</blockquote>
<p>相信不少人也有类似的体会。</p>
<p>天气预报是事关人们生活的重要信息,其准确度对生产生活当然有很大影响。
说到增加天气预报的准确度,人们自然会想到高性能的超级计算机比如
<a class="reference external" href="https://zh.wikipedia.org/wiki/%E5%A4%A9%E6%B2%B3%E4%BA%8C%E5%8F%B7">天河二号</a>
,想到环绕在地球高空的 <a class="reference external" href="https://zh.wikipedia.org/wiki/%E6%B0%A3%E8%B1%A1%E8%A1%9B%E6%98%9F">气象卫星</a>
,想到遍布世界各地的气象站观测台。想想这么多耗资不菲的高尖端项目被国家投入,
用来改善天气预报的准确程度,看起来这的确是一个困难的科研课题。</p>
<p>话说回来,准确预测气温、气压、湿度、降水概率等等这些事情对于生产生活固然重要,
不过对一般民众而言,天气预报最重要的作用就只是回答 <strong>明天我该穿多厚的衣服,出门是否需要打伞</strong>
这种问题。一年四季换衣服的时机其实并不那么频繁,气温提升五度或者降低两度这种程度下人们估计也
不能感觉得到,大体上只要根据「昨天穿什么衣服,昨天觉得冷不冷」就能作出判断。另一方面,
<strong>出门是否需要打伞</strong> 这样的问题的确只能依靠天气预报来回答。</p>
<p>那么解决 <strong>出门是否需要打伞</strong> 这个问题需要那么高尖端的技术么?</p>
<p>我所在的大阪大学情报科学研究科有个已经毕业的学长 <ruby><rb>今城 健太郎</rb><rp>(</rp><rt>いまじょう けんたろう</rt><rp>)</rp></ruby>
就对此作出了解答。他的专业不是气象预测,而是图像分析处理,纯粹的计算机科学学科。
而他的本科毕业设计就着眼于「仅仅分析气象云图,能否高精度预测降水概率」,
其研究成果,就是一个叫 <a class="reference external" href="http://blog.imoz.jp/post/7316967132/ninetan-forecast">ないんたん 的降水概率预测系统</a> 。</p>
<p>这个系统有数个会卖萌的Twitter机器人 <a class="reference external" href="//twitter.com/ninetan">@ninetan</a> ,每时每刻对
其预测地区的降水情况做播报,同时也有详细的降水概率曲线图对
<a class="reference external" href="http://sx9.jp/weather/osaka.html">大阪</a> ( <a class="reference external" href="//twitter.com/ninetan_osaka">@ninetan_osaka</a> ),
<a class="reference external" href="http://sx9.jp/weather/kyoto.html">京都</a> ( <a class="reference external" href="//twitter.com/ninetan_kyoto">@ninetan_kyoto</a> ),
<a class="reference external" href="http://sx9.jp/weather/tokyo.html">东京</a> ( <a class="reference external" href="//twitter.com/ninetan_tokyo">@ninetan_tokyo</a> ),
<a class="reference external" href="http://sx9.jp/weather/hyogo.html">兵库</a> ( <a class="reference external" href="//twitter.com/ninetan_hyogo">@ninetan_hyogo</a> ),
<a class="reference external" href="http://sx9.jp/weather/wakayama.html">和歌山</a> ( <a class="reference external" href="//twitter.com/ninetan_wakayam">@ninetan_wakayam</a> ) 的各个大学所在校区
两个半小时内做精确的降水概率预测。比如今天晚上大阪大学三个校区的降水概率图如下:</p>
<div class="figure">
<img alt="今天晚上大阪大学三个校区的降水概率图" class="img-responsive" src="//farseerfc.me/zhs/images/forcast-osaka.png"/>
<p class="caption">今天晚上大阪大学三个校区的降水概率图</p>
</div>
<p>从上面的图可以看出这个系统的预测精度是以 <strong>分为单位</strong> 的,可以看到
两个半小时内各地的降水量的大小。比如我可以根据这张图看出,我所在的吹田校区
将在 <strong>21时35分</strong> 开始有微弱的概率下起 0.1mm/h~1mm/h 的毛毛雨,到 <strong>22时05分</strong> 左右这个降水概率
爬升到最高大约45%,从而作出判断:
我最好在晚上九点左右离开学校回家,避免淋雨。</p>
<p>自从研究室的前辈给我介绍这个天气预报系统开始,我用了它两三年了,直观感觉是
这个系统的预测精度惊人得准确,基本上能接近
<a class="reference external" href="http://zh.wikipedia.org/wiki/%E9%AD%94%E6%B3%95%E7%A6%81%E6%9B%B8%E7%9B%AE%E9%8C%84%E7%94%A8%E8%AA%9E%E5%88%97%E8%A1%A8#.E8.A3.9D.E7.BD.AE.E3.80.81.E5.85.B5.E5.99.A8.E3.80.81.E6.8A.80.E8.A1.93">《魔法的禁书目录》中的「树形图设计者」</a>
能做的天气预报的程度,
它说何时会下雨就一定下雨,它说何时雨停就一定雨停。同学们出门和回家的时候一般都会
看一眼这个天气预报然后决定是否出门。「啊今天晚上9点开始下雨所以早点回家」
或者「啊还有30分钟雨就停了,再在研究室里留一会儿」。</p>
<p>这只是一个本科生的毕业设计,所以覆盖面小(只有5所大学的十几个校区,只能预测
未来两个多小时的降水概率),不过仅此而已能做到如此的精度以至于实用,实在让我
惊讶。系统的测试之初就有人说:</p>
<blockquote class="twitter-tweet" lang="zh-tw"><p>最近ないんたん予报あたりすぎてないんたんが雨降らせてるんじゃないかという疑惑</p>— すみのネコ歩き (@sumi_eee) <a href="https://twitter.com/sumi_eee/status/88530793407852544">2011 7月 6日</a></blockquote>
<script async="" charset="utf-8" src="//platform.twitter.com/widgets.js"></script><!-- -->
<blockquote>
最近ないんたん预告实在太准了,甚至让人怀疑是不是ないんたん把雨招来的。</blockquote>
<p>不过最近身边的日本人似乎已经把这个系统的准确当作习以为常了,就像日本的电车
掐着秒表准点到站一样,理所当然。
把天气预报这种高尖端的技术做到如此实用的地步,这基本上可以代表我对
日本学术界研究方式和研究目的的总体印象了。</p>
<p>嗯今天就写这么多,9点到了,我要按照天气预报的预测,准时回家了。</p>
<p>——写于2015羊年除夕夜,9点。</p>
archlinux 上用 chrome 实现 透明计算 远程登录2015-02-13T20:39:00+09:002015-02-13T20:39:00+09:00farseerfctag:farseerfc.me,2015-02-13:/zhs/arch-chrome-remote-desktop.html
<p><a class="reference external" href="http://news.sciencenet.cn/htmlnews/2015/1/311393.shtm">透明计算</a>
具体是什么,因为他们没有公开技术细节所以我并不知道,只是看
<a class="reference external" href="http://v.qq.com/page/h/v/q/h0145ebh1vq.html">公开出来的演示视频</a>
,感觉似乎只要能从手机上远程登录系统桌面,就能算是透明计算了。
如果透明计算真是这个意思,那么我似乎已经用着这个技术很多年了嘛。</p>
<p>Xorg 上常用的远程桌面工具有很多,基于 VNC 协议的、基于NX的和基于 RDP 协议的都能找到,
直接 ssh X forwarding 效果也不错。只是这些方案的一个 <strong>不太易用</strong> 的地方在于,需要
通过 ip 访问到远程的电脑,所以在跨越 NAT 之类的情况下不太容易使用。</p>
<p>于是今天介绍一个使用方便设置也简单的方法: 通过 chrome-remote-desktop 在 archlinux
上使用远程桌面。这个方案的优势在于,借助 Google 的云端服务器(内部貌似是XMPP协议下的握手)
方便地实现了 NAT 穿透,无论什么网络环境基本都能使用。当然,要支持远程登录,
位于远端的登录的计算机必须一直开着 …</p>
<p><a class="reference external" href="http://news.sciencenet.cn/htmlnews/2015/1/311393.shtm">透明计算</a>
具体是什么,因为他们没有公开技术细节所以我并不知道,只是看
<a class="reference external" href="http://v.qq.com/page/h/v/q/h0145ebh1vq.html">公开出来的演示视频</a>
,感觉似乎只要能从手机上远程登录系统桌面,就能算是透明计算了。
如果透明计算真是这个意思,那么我似乎已经用着这个技术很多年了嘛。</p>
<p>Xorg 上常用的远程桌面工具有很多,基于 VNC 协议的、基于NX的和基于 RDP 协议的都能找到,
直接 ssh X forwarding 效果也不错。只是这些方案的一个 <strong>不太易用</strong> 的地方在于,需要
通过 ip 访问到远程的电脑,所以在跨越 NAT 之类的情况下不太容易使用。</p>
<p>于是今天介绍一个使用方便设置也简单的方法: 通过 chrome-remote-desktop 在 archlinux
上使用远程桌面。这个方案的优势在于,借助 Google 的云端服务器(内部貌似是XMPP协议下的握手)
方便地实现了 NAT 穿透,无论什么网络环境基本都能使用。当然,要支持远程登录,
位于远端的登录的计算机必须一直开着 Chrome Remote Desktop 的后台服务。</p>
<div class="panel panel-default">
<div class="panel-heading">
Chrome Remote Desktop 插件</div>
<div class="panel-body">
<img alt="Chrome Remote Desktop 插件" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-plugin.png"/>
</div>
</div>
<div class="section" id="chrome-remote-desktop">
<h2><a class="toc-backref" href="#id5">Chrome Remote Desktop 的客户端</a></h2>
<p>虽然可能有很多人不知道,不过 Chrome 内包括远程桌面的功能很久了。只是这个功能的界面默认
没有提供界面,要使用它需要安装 Google 官方出品的
<a class="reference external" href="https://chrome.google.com/webstore/detail/chrome-remote-desktop/gbchcmhmhahfdphkhkmpfmihenigjmpp">remote-desktop 插件</a> 。
装好之后远程桌面的客户端就准备好,可以用来远程访问别的计算机桌面了(无论是 Windows/OS X
还是 Linux 都支持)。并且不光可以自己远程访问自己账户的桌面,还可以远程协助朋友的桌面。</p>
</div>
<div class="section" id="archlinux">
<h2><a class="toc-backref" href="#id6">Archlinux 上设置远程登录的服务器</a></h2>
<p>有了客户端之后还要设置一下才能让桌面作为远程登录的服务器。Windows 和 OS X 上 Chrome
会自动下载需要的安装包,无脑下一步就能装好了。Linux上由于发行版众多,桌面配置各异,
所以需要一点手动配置。官方的设置步骤记载在 <a class="reference external" href="https://support.google.com/chrome/answer/1649523">这里</a>
其中给出了 debian 用的二进制包和 Ubuntu 12.10 上的设置方式,以下设置是参考官方步骤。</p>
<p>首先要安装 chrome-remote-desktop 这个包,这个包实际上对应了 Windows/OS X 上用安装程序
安装的 Remote Desktop Host Controller。 archlinux 上开启了
<a class="reference external" href="https://github.com/archlinuxcn/repo">[archlinuxcn]</a>
仓库的话,可以直接安装打好的包。或者可以从
<a class="reference external" href="https://aur.archlinux.org/packages/chrome-remote-desktop/">AUR</a> 装。</p>
<pre><span class="code-line">$ pacman -Ss chrome-remote-desktop<br/><span style="color:purple;font-weight:bold;">archlinuxcn/</span><span style="font-weight:bold;">chrome-remote-desktop </span><span style="color:green;font-weight:bold;">40.0.2214.44-1</span><br/>Allows you to securely access your computer over the Internet through Chrome.</span></pre><p>装好之后从会说这么一段话:</p>
<blockquote>
<p>groupadd:无效的组 ID “chrome-remote-desktop”</p>
<p>Please create ~/.config/chrome-remote-desktop folder manually, if it doesn't exist, or else you can't use CRD.
The needed files are created by the Chrome app, inside the chrome-remote-desktop folder, after Enabling Remote Connections.
To {enable,start} the service use systemctl --user {enable,start} chrome-remote-desktop</p>
<p>You may need to create a ~/.chrome-remote-desktop-session file with commands to start your session</p>
<p>Go to <a class="reference external" href="https://support.google.com/chrome/answer/1649523">https://support.google.com/chrome/answer/1649523</a> for more information.</p>
</blockquote>
<p>那句报错是 AUR 里打的包还没跟上上游 Google 的更改导致的错误,
首先我们需要把远程登录的用户添加入 chrome-remote-desktop 这个用户组里。
新版本的 chrome remote desktop 提供了一个命令做这个事情,所以执行以下命令就可以了:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> /opt/google/chrome-remote-desktop/chrome-remote-desktop --add-user</span>
</pre></div>
<p>然后我们需要手动创建 <code class="code">
~/.config/chrome-remote-desktop</code>
这个文件夹,内容是空的
就好了,随后 chrome 会往这里面放 <code class="code">
host#.json</code>
文件用于身份验证。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> mkdir ~/.config/chrome-remote-desktop</span>
</pre></div>
<p>然后我们要创建一个 shell 脚本 <code class="code">
~/.chrome-remote-desktop-session</code>
,这是远程
登录时的 .xinitrc ,内容么就是启动你想在远程登录时用的桌面环境。
这里可以指定一个和你正在登录的 WM/DE 不同的桌面,比如我启动 xfce4:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> cat ~/.chrome-remote-desktop-session</span>
<span class="code-line"><span class="gp">#</span>!/bin/bash</span>
<span class="code-line"><span class="go">startxfce4</span></span>
<span class="code-line"><span class="gp">$</span> chmod <span class="m">755</span> .chrome-remote-desktop-session</span>
</pre></div>
<p>接下来需要从 Chrome 的插件里启用远程桌面。打开 Chrome 的 Remote Desktop 插件,这时
应该可以看到一个「启用远程链接」的按钮。</p>
<div class="figure">
<img alt="Chrome Remote Desktop 插件中「启用远程链接」的按钮" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-enable-button.png"/>
<p class="caption">Chrome Remote Desktop 插件中「启用远程链接」的按钮</p>
</div>
<div class="alert alert-warning compound">
<p>在撰写本文的时候, Archlinux 官方源里的 chromium 的版本和 aur/google-chrome
的版本尚且还是 40.0.2214.111 ,而 Chrome Web Store 中提供的 Chrome Remote
Desktop 的插件的版本是 41.0.2272.41 。虽然通常并不要求两者版本一致,不过貌似最近
Chrome 内部的 Remoting 功能更改了 API 导致可能出问题。如果你找不到
「启用远程链接」的按钮,请尝试一下新版本的 Chrome 比如 google-chrome-dev 。
在这一步启用之后,老版本的 chrome 应该也就能使用远程桌面了。</p>
</div>
<div class="alert alert-warning compound">
<p>在32位的 Linux 版本上,最近更新的 Chrome Remote Desktop 插件可能无法正确识别 Host
的版本,具体 <a class="alert-link reference external" href="https://code.google.com/p/chromium/issues/detail?id=332930">参考这个 bug</a> 。</p>
</div>
<p>点击「启用远程链接」,设定一个 PIN 密码(不需要很复杂,这里首先有 Google 帐号验证保证只有
你才能访问),然后就能看到这套电脑的 hostname 出现在「我的电脑」列表里。</p>
<div class="figure">
<img alt="启用远程链接之后的样子" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-after-enabled.png"/>
<p class="caption">启用远程链接之后的样子</p>
</div>
<p>同时,启用了远程链接之后,可以在刚刚创建的 ~/.config/chrome-remote-desktop
文件夹中找到记录了验证信息的文件。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> ls .config/chrome-remote-desktop</span>
<span class="code-line"><span class="go">chrome-profile host#8cfe7ecfd6bb17955c1ea22f77d0d800.json pulseaudio#8cfe7ecfd6</span></span>
</pre></div>
<p>然后就可以启动对应的 systemd 用户服务了,如果想自动启动服务要记得 <code class="code">
systemctl --user enable</code>
:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> systemctl --user start chrome-remote-desktop.service</span>
</pre></div>
<p>如果上面的设置一切正常,就可以看到 chrome-remote-desktop 启动了另外一个 Xorg 执行你
刚刚指定的桌面环境:</p>
<div class="figure">
<img alt="htop 中看到的 chrome-remote-desktop 启动的另外一个 Xorg" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-htop.png"/>
<p class="caption">htop 中看到的 chrome-remote-desktop 启动的另外一个 Xorg</p>
</div>
<p>然后就可以试着通过 Remote Desktop 插件登录到这个新开的 Xorg 了:</p>
<div class="figure">
<img alt="「远程」登录到新的 XFCE4" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-xfce4.png"/>
<p class="caption">「远程」登录到新的 XFCE4</p>
</div>
</div>
<div class="section" id="linux-chrome-windows-os-x">
<h2><a class="toc-backref" href="#id7">Linux 版本的 Chrome远程桌面 和 Windows/ OS X 上的区别</a></h2>
<p>通过上面的设置步骤也可以看出,Linux版本的远程桌面会在后台开一个独立的 X 会话,而不能
复用现在已有的 X 会话。对远程登录的用法而言这还能接受,对远程协助的功能而言有点问题,
因为正在使用的人不能观察协助者做了什么,协助者也不能继续请求协助的人的操作。</p>
<p>当然目前 Chrome 远程桌面的 Linux Host Controller 还只是 beta 版本,官方只测试支持
Ubuntu 12.04 和 12.10 (14.04之后似乎有
<a class="reference external" href="https://code.google.com/p/chromium/issues/detail?id=366432">Bug</a>
),所以不能要求太多。希望以后能改善吧。</p>
</div>
<div class="section" id="bonus">
<h2><a class="toc-backref" href="#id8">Bonus: 手机远程登录</a></h2>
<div class="panel panel-default">
<div class="panel-heading">
手机上的 Chrome 远程桌面 App</div>
<div class="panel-body">
<img alt="手机上的 Chrome 远程桌面 App" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-android.png"/>
</div>
</div>
<p>通过上面的设置就可以从任何一个 Chrome 远程桌面客户端登录刚刚设置的这台电脑了。
因为 Chrome 在三大桌面系统 Windows / OS X / Linux 上都有,所以应该能覆盖大多数桌面
系统了。</p>
<p>除了桌面的 Chrome 之外还有一个客户端是 Android 上的
<a class="reference external" href="https://play.google.com/store/apps/details?id=com.google.chromeremotedesktop">Chrome 远程桌面 App</a> 经过上面的设置之后,从这个 App 也能看到并登录:</p>
<div class="figure">
<img alt="手机远程登录" class="img-responsive" src="//farseerfc.me/zhs/images/chrome-remote-desktop-android-logined.png"/>
<p class="caption">手机远程登录</p>
</div>
<p>好啦,开始享受国家自然科学一等奖的透明计算技术吧!</p>
</div>
换到 farseerfc.me 域名2015-01-26T23:32:00+09:002015-01-26T23:32:00+09:00farseerfctag:farseerfc.me,2015-01-26:/zhs/switch-to-farseerfc-dot-me-domain.html<p>上个月就在 <ruby><rb>狗爹</rb><rp>(</rp><rt>godaddy</rt><rp>)</rp></ruby> 上买了个自己的域名 <code class="code">
farseerfc.me</code>
准备用在这个
博客上,当时试着转到过这个域名,发现 <ruby><rb>自定义域名</rb><rp>(</rp><rt>custom domain</rt><rp>)</rp></ruby>
只支持 http 不支持 https ,想着还要买自己的证书,于是就扔在了一旁。不用自定义域名的话,
放在 github.io 上是可以用 HTTPS 的。
今天在 <a class="reference external" href="//webchat.freenode.net/?channels=archlinux-cn">#archlinux-cn</a> 上受大牛 <a class="reference external" href="/links.html#quininer">quininer</a> 和 <a class="reference external" href="/links.html#lilydjwg">lilydjwg</a> 点播,
发现 cloudflare 有提供
<a class="reference external" href="https://blog.cloudflare.com/introducing-universal-ssl/">免费的支持 SSL 的 CDN 服务</a>
赶快去申请了一个,感觉非常赞,于是就换过来了。</p>
<p>设置的方法按照 <a class="reference external" href="https://me.net.nz/blog/github-pages-secure-with-cloudflare/">这篇博文</a>
说的一步步做下来,如它所述,用 CloudFlare …</p><p>上个月就在 <ruby><rb>狗爹</rb><rp>(</rp><rt>godaddy</rt><rp>)</rp></ruby> 上买了个自己的域名 <code class="code">
farseerfc.me</code>
准备用在这个
博客上,当时试着转到过这个域名,发现 <ruby><rb>自定义域名</rb><rp>(</rp><rt>custom domain</rt><rp>)</rp></ruby>
只支持 http 不支持 https ,想着还要买自己的证书,于是就扔在了一旁。不用自定义域名的话,
放在 github.io 上是可以用 HTTPS 的。
今天在 <a class="reference external" href="//webchat.freenode.net/?channels=archlinux-cn">#archlinux-cn</a> 上受大牛 <a class="reference external" href="/links.html#quininer">quininer</a> 和 <a class="reference external" href="/links.html#lilydjwg">lilydjwg</a> 点播,
发现 cloudflare 有提供
<a class="reference external" href="https://blog.cloudflare.com/introducing-universal-ssl/">免费的支持 SSL 的 CDN 服务</a>
赶快去申请了一个,感觉非常赞,于是就换过来了。</p>
<p>设置的方法按照 <a class="reference external" href="https://me.net.nz/blog/github-pages-secure-with-cloudflare/">这篇博文</a>
说的一步步做下来,如它所述,用 CloudFlare 的优点如下:</p>
<ol class="arabic simple">
<li>CDN 加速</li>
<li>SSL (HTTPS) 加密</li>
<li>支持 SPDY 协议</li>
<li>支持 IPv6</li>
</ol>
<div class="label label-warning">
<strong>2015年12月29日更新</strong></div>
<p>现在不光支持 SPDY 而且支持 HTTP/2 了。</p>
<p>然后 <strong>免费账户</strong> 的一些缺点有:</p>
<ol class="arabic simple">
<li>CloudFlare 和 github.io 之间的数据不是加密的,因为 github
<ruby><rb>自定义域名</rb><rp>(</rp><rt>custom domain</rt><rp>)</rp></ruby> 还不支持使用自己的证书。这也是一开始我没用
自定义域名的原因嘛,这没有办法……</li>
<li>CloudFlare 给免费账户签名的 SSL 证书比较新,不支持一些老的设备和浏览器,比如不支持
老的 XP 系统的 IE 或者 2.x 的 Android。这种情况下没办法只能用没有加密的 HTTP 了。</li>
<li>不支持 <a class="reference external" href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HSTS 头</a>
,所以不能从服务器这边强制浏览器用 HTTPS。当然可以放个 javascript 跳转,
也可以用 <a class="reference external" href="https://www.eff.org/https-everywhere">HTTPSEverywhere</a> 这种方案。</li>
</ol>
<div class="label label-warning">
<strong>2015年12月29日更新</strong></div>
<p>如评论中 <a class="reference external" href="http://farseerfc.me/switch-to-farseerfc-dot-me-domain.html#comment-2015037231">提到的</a>
现在支持 HSTS 了。</p>
<div class="section" id="id3">
<h2>设置步骤</h2>
<p>基本按照默认的选项下一步就可以了。</p>
<ol class="arabic simple">
<li>和那个博主一样我把 <ruby><rb>安全级别</rb><rp>(</rp><rt>Security profile</rt><rp>)</rp></ruby> 降到了 Low ,即使是可疑流量也
不会要求输入 CAPTCHA 。</li>
<li>把 SSL 方式开在 Flexible SSL,访客到 CloudFlare 是加密的,而 CloudFlare 到
github.io 是不加密的。</li>
<li>把 CDN 开到了 CDT+Full Optimization ,可以对访问加速。由于是完全静态的博客,没有
动态变化的内容,所以应该比较安全。</li>
<li>服务器设置的一步需要将 <ruby><rb>域名解析服务器</rb><rp>(</rp><rt>DNS nameservers</rt><rp>)</rp></ruby> 从狗爹的服务器改到
CloudFlare 的,如下图:</li>
</ol>
<div class="figure">
<img alt="更改狗爹的域名服务器" class="img-responsive" src="//farseerfc.me/zhs/images/godaddy.png"/>
<p class="caption">更改狗爹的域名服务器</p>
</div>
<p>申请好之后就由 CloudFlare 接管域名解析了,接下来在 CloudFlare 的 DNS 设置添加一条
<a class="reference external" href="https://help.github.com/articles/tips-for-configuring-an-a-record-with-your-dns-provider/">A 类规则指向 github pages 的 IP</a> 。</p>
<div class="figure">
<img alt="更改CloudFlare的DNS规则" class="img-responsive" src="//farseerfc.me/zhs/images/cloudflaredns.png"/>
<p class="caption">更改CloudFlare的DNS规则</p>
</div>
<p>等一切都反映到 DNS 服务器上就设置完成了,接下来给
<a class="reference external" href="https://help.github.com/articles/adding-a-cname-file-to-your-repository/">farseerfc.github.io push 一个 CNAME 文件</a>
写上我的域名就可以了。我用 Makefile 配合我的 pelican 配置做这个:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="nf">publish</span><span class="o">:</span> <span class="n">rmdrafts</span> <span class="n">cc</span> <span class="n">clean</span> <span class="n">theme</span></span>
<span class="code-line"> <span class="o">[</span> ! -d <span class="k">$(</span>OUTPUTDIR<span class="k">)</span> <span class="o">]</span> <span class="o">||</span> find <span class="k">$(</span>OUTPUTDIR<span class="k">)</span> -mindepth <span class="m">1</span> -not -wholename <span class="s2">"*/.git*"</span> -delete</span>
<span class="code-line"> rm -rf cache</span>
<span class="code-line"> <span class="nb">echo</span> <span class="k">$(</span>SITEURL<span class="k">)</span> > content/static/CNAME</span>
<span class="code-line"> <span class="k">$(</span>PELICAN<span class="k">)</span> <span class="k">$(</span>INPUTDIR<span class="k">)</span> -o <span class="k">$(</span>OUTPUTDIR<span class="k">)</span> -s <span class="k">$(</span>PUBLISHCONF<span class="k">)</span> <span class="k">$(</span>PELICANOPTS<span class="k">)</span></span>
<span class="code-line"> <span class="k">$(</span>MAKE<span class="k">)</span> rsthtml</span>
<span class="code-line"></span>
<span class="code-line"><span class="nf">github</span><span class="o">:</span></span>
<span class="code-line"> <span class="o">(</span><span class="nb">cd</span> <span class="k">$(</span>OUTPUTDIR<span class="k">)</span> <span class="o">&&</span> git checkout master<span class="o">)</span></span>
<span class="code-line"> env <span class="nv">SITEURL</span><span class="o">=</span><span class="s2">"farseerfc.me"</span> <span class="k">$(</span>MAKE<span class="k">)</span> publish</span>
<span class="code-line"> <span class="o">(</span><span class="nb">cd</span> <span class="k">$(</span>OUTPUTDIR<span class="k">)</span> <span class="o">&&</span> git add . <span class="o">&&</span> git commit -m <span class="s2">"update"</span> <span class="o">&&</span> git push<span class="o">)</span></span>
</pre></div>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">SITEURL</span> <span class="o">=</span> <span class="s1">'//'</span> <span class="o">+</span> <span class="n">getenv</span><span class="p">(</span><span class="s2">"SITEURL"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">'localhost:8000'</span><span class="p">)</span></span>
<span class="code-line"><span class="n">STATIC_PATHS</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'static'</span><span class="p">,</span> <span class="s1">'images'</span><span class="p">,</span> <span class="s1">'uml'</span><span class="p">,</span> <span class="s1">'images/favicon.ico'</span><span class="p">,</span> <span class="s1">'static/CNAME'</span><span class="p">]</span></span>
<span class="code-line"><span class="n">EXTRA_PATH_METADATA</span> <span class="o">=</span> <span class="p">{</span></span>
<span class="code-line"> <span class="s1">'images/favicon.ico'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'path'</span><span class="p">:</span> <span class="s1">'favicon.ico'</span><span class="p">},</span></span>
<span class="code-line"> <span class="s1">'static/CNAME'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'path'</span><span class="p">:</span> <span class="s1">'CNAME'</span><span class="p">}</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
<p>然后把生成的静态网站 push 到 github 之后可以从项目设置里看到域名的变化:</p>
<div class="figure">
<img alt="Github 配置好自定义域名之后的变化" class="img-responsive" src="//farseerfc.me/zhs/images/githubdomain.png"/>
<p class="caption">Github 配置好自定义域名之后的变化</p>
</div>
<p>最后把Disqus的评论也迁移到新的域名,disqus有方便的迁移向导,一直下一步就可以了。</p>
<p>这样就一切都设置妥当了。</p>
</div>
<div class="section" id="id4">
<h2>致谢</h2>
<p>最后要感谢提供消息的 <a class="reference external" href="/links.html#quininer">quininer</a> 和 <a class="reference external" href="/links.html#lilydjwg">lilydjwg</a> ,感谢撰写设置步骤的
<em>Jonathan J Hunt</em> , 感谢 CloudFlare 提供免费 SSL CDN 服务,感谢 Github 提供
方便免费的 Pages 托管。</p>
</div>
重新设计了 Pelican 的主题与插件2015-01-25T22:45:00+09:002015-01-25T22:45:00+09:00farseerfctag:farseerfc.me,2015-01-25:/zhs/redesign-pelican-theme.html
<!-- PELICAN_BEGIN_SUMMARY -->
<div class="label label-warning">
<strong>2015年2月14日更新</strong></div>
<!-- PELICAN_END_SUMMARY -->
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id38">前言: 新天新地,将一切都更新了 <a class="footnote-reference" href="#id37" id="id1">[1]</a></a></h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>不知不觉间放任这边长草很久了,从上次
<a class="reference external" href="//farseerfc.me/zhs/try-pelican.html">折腾主题</a> 到现在都快三年了,
而从上次 <a class="reference external" href="//farseerfc.me/zhs/marry-me.html">写了篇告白信</a> 到现在也有快两年了。
这期间曾经把主题配色从 <a class="reference external" href="http://getbootstrap.com/2.3.2/">Bootstrap 2</a> 默认的
白底黑字改成了让眼睛更舒适的黑底白字,也不过是用 drop-in 的配色方案而已,没有本质上的改进。</p>
<p>洞中一日世上千载,两年里 Bootstrap 已经升上 <a class="reference external" href="http://getbootstrap.com/">v3.3</a> ,
而 Pelican 则已经升到 <a class="reference external" href="https://github.com/getpelican/pelican/releases/tag/3.5.0">3.5</a> 了。
早就眼馋 Bootstrap 和 Pelican 中的诸多新功能新设计,不过无奈于时间有限只能饱饱眼福。</p>
<p>近日想写的东西越积越多,终于下定决心花了前前后后 <strong>两个月</strong> 的时间重新设计了一遍
Pelican 的主题,配合一些我觉得有用的插件。于是本博客就变成你们现在看到的样子了。
(以及本篇博文也用了两个月的时间写完,其间还发了几篇别的短文,算是恢复写博客的尝试吧 …</p></div>
<!-- PELICAN_BEGIN_SUMMARY -->
<div class="label label-warning">
<strong>2015年2月14日更新</strong></div>
<!-- PELICAN_END_SUMMARY -->
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id38">前言: 新天新地,将一切都更新了 <a class="footnote-reference" href="#id37" id="id1">[1]</a></a></h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>不知不觉间放任这边长草很久了,从上次
<a class="reference external" href="//farseerfc.me/zhs/try-pelican.html">折腾主题</a> 到现在都快三年了,
而从上次 <a class="reference external" href="//farseerfc.me/zhs/marry-me.html">写了篇告白信</a> 到现在也有快两年了。
这期间曾经把主题配色从 <a class="reference external" href="http://getbootstrap.com/2.3.2/">Bootstrap 2</a> 默认的
白底黑字改成了让眼睛更舒适的黑底白字,也不过是用 drop-in 的配色方案而已,没有本质上的改进。</p>
<p>洞中一日世上千载,两年里 Bootstrap 已经升上 <a class="reference external" href="http://getbootstrap.com/">v3.3</a> ,
而 Pelican 则已经升到 <a class="reference external" href="https://github.com/getpelican/pelican/releases/tag/3.5.0">3.5</a> 了。
早就眼馋 Bootstrap 和 Pelican 中的诸多新功能新设计,不过无奈于时间有限只能饱饱眼福。</p>
<p>近日想写的东西越积越多,终于下定决心花了前前后后 <strong>两个月</strong> 的时间重新设计了一遍
Pelican 的主题,配合一些我觉得有用的插件。于是本博客就变成你们现在看到的样子了。
(以及本篇博文也用了两个月的时间写完,其间还发了几篇别的短文,算是恢复写博客的尝试吧。)</p>
<!-- PELICAN_END_SUMMARY -->
<div class="panel panel-default">
<div class="panel-heading">
在迈阿密参加 <a class="reference external" href="http://icsr2015.ipd.kit.edu/">ICSR 2015</a> 的时候
拍到的街边一家叫 Pelican 的旅馆</div>
<div class="panel-body">
<img alt="Pelican Hotel" class="img-responsive" src="//farseerfc.me/zhs/images/pelican.jpg"/>
</div>
</div>
<div class="section" id="bootstrap-3">
<h3><a class="toc-backref" href="#id39">Bootstrap 3 的新设计</a></h3>
<ul class="simple">
<li>全新的 <ruby><rb>优先移动设备</rb><rp>(</rp><rt>mobile-first</rt><rp>)</rp></ruby> <ruby><rb>响应式</rb><rp>(</rp><rt>responsive</rt><rp>)</rp></ruby> 设计。
原本Bootstrap 2虽然有响应式设计,
不过诸多细节不能符合我的需求,最终还是得手工 hack <code class="code">
@media</code>
查询去微调。
现在的 <ruby><rb>优先移动设备</rb><rp>(</rp><rt>mobile-first</rt><rp>)</rp></ruby> <ruby><rb>响应式</rb><rp>(</rp><rt>responsive</rt><rp>)</rp></ruby>
<ruby><rb>栅格系统</rb><rp>(</rp><rt>grid system</rt><rp>)</rp></ruby> 则相对显得科学很多了,也终于能在手持
设备上看起来舒服一些。诸位可以尝试改变窗口宽度,或者在不同的手持设备上打开这个
blog ,体验一下这个页面在不同显示器大小中的效果。如果仍有问题欢迎
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/issues">发 Issue 给我</a> 。</li>
<li>科学的 <ruby><rb>导航栏</rb><rp>(</rp><rt>Navbar</rt><rp>)</rp></ruby> 。
比 Bootstrap 2 那个科学很多了。无论是 <ruby><rb>保持</rb><rp>(</rp><rt>sticky</rt><rp>)</rp></ruby> 在上端还是跟着浮动,
或者像这边这样 <a class="reference external" href="http://www.virtuosoft.eu/code/bootstrap-autohidingnavbar/">自动隐藏</a> 都很简单。</li>
</ul>
<p>更多细节参考 <a class="reference external" href="http://getbootstrap.com/">Bootstrap 3 主页</a> 。</p>
</div>
<div class="section" id="pelican-3-5">
<h3><a class="toc-backref" href="#id40">Pelican 3.5 的新功能</a></h3>
<ul class="simple">
<li>Python 2 和 Python 3 统一代码:
再没有恼人的 unicode 相关的问题了。这对 blog 系统来说相当重要啊。
而且还能方便切换 pypy 等不同的解释器。</li>
<li>全新的插件系统:非常多功能强大的 <a class="reference external" href="https://github.com/getpelican/pelican-plugins">插件</a> 等着你。</li>
<li>增强了导入系统:嗯总算可以导入我的中文的 wordpress 博客了。(虽然那边长草更久了……)</li>
<li><a class="reference external" href="http://pelican.readthedocs.org/en/latest/content.html#linking-to-internal-content">站内链接</a>
:不用 <ruby><rb>硬编码</rb><rp>(</rp><rt>hard code</rt><rp>)</rp></ruby> 目标页面的链接了,可以直接写源文件的位置然后让 pelican
处理,这样能简化各种 <ruby><rb>插件</rb><rp>(</rp><rt>plugin</rt><rp>)</rp></ruby> 和 <ruby><rb>主题</rb><rp>(</rp><rt>theme</rt><rp>)</rp></ruby> 的实现。</li>
</ul>
<p>更多细节参考 <a class="reference external" href="http://pelican.readthedocs.org/en/latest/">Pelican 文档</a> 。</p>
</div>
<div class="section" id="id11">
<h3><a class="toc-backref" href="#id41">新的文件夹布局</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
Pelican 的新文件夹布局</div>
<div class="panel-body">
<pre><span class="code-line"></span>
<span class="code-line"><span style="color:blue;font-weight:bold;">.</span></span>
<span class="code-line">├── <span style="color:blue;font-weight:bold;">cache</span> 生成页面的 pickle 缓存</span>
<span class="code-line">├── <span style="color:blue;font-weight:bold;">content</span> 读取的全部内容</span>
<span class="code-line">│ ├── <span style="color:blue;font-weight:bold;"><categories></span> 按分类存放的文章</span>
<span class="code-line">│ ├── <span style="color:blue;font-weight:bold;">pages</span> 像 About 这样的固定页面</span>
<span class="code-line">│ └── <span style="color:blue;font-weight:bold;">static</span> 文章内用到的静态内容</span>
<span class="code-line">├── <span style="color:blue;font-weight:bold;">drafts</span> 文章的草稿箱</span>
<span class="code-line">├── <span style="color:green;font-weight:bold;">Makefile</span> 生成用的 makefile</span>
<span class="code-line">├── <span style="color:green;font-weight:bold;">pelicanconf.py</span> 测试时用的快速 Pelican 配置</span>
<span class="code-line">├── <span style="color:green;font-weight:bold;">publishconf.py</span> 部署时用的耗时 Pelican 配置</span>
<span class="code-line">├── <span style="color:teal;font-weight:bold;">output</span> -> <span style="color:blue;font-weight:bold;">../farseerfc.github.io</span></span>
<span class="code-line">├── <span style="color:teal;font-weight:bold;">plugins</span> -> <span style="color:blue;font-weight:bold;">../pelican-plugins</span></span>
<span class="code-line">└── <span style="color:teal;font-weight:bold;">theme</span> -> <span style="color:blue;font-weight:bold;">../pelican-bootstrap3</span></span>
</pre></div>
</div>
<p><a class="reference external" href="https://github.com/farseerfc/farseerfc.github.com">之前的博客</a> 仍然留在
github 上,其中的内容完全搬过来了。开始写老博客的时候 Pelican 版本较早,没有形成好的
文件夹布局,导致生成的文章、使用的模板和撰写的内容全都混在一起,非常难以管理,
于是趁改版之际用了新的文件夹布局方式,并分为 4 个 git repo 分别管理历史。</p>
<p>首先是存放 <a class="reference external" href="https://github.com/farseerfc/farseerfc">总的博客内容的 repo</a> ,
其布局是如图那样的。这样将生成的静态网站和生成网站用的配置啦内容啦分开之后,顿时清晰了很多。</p>
<p>然后这个内容 repo 中的三个符号链接分别指向三个子 repo(没用 <code class="code">
git submodule</code>
管理纯粹是因为偷懒)。 theme 指向
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3">pelican-bootstrap3</a>
,是我修改过的 pelican 主题。
plugins 指向 <a class="reference external" href="https://github.com/farseerfc/pelican-plugins">pelican-plugins</a>
,由于 plugins 的质量有些参差不齐,其中不少 plugin
都按我的需要做了些许修改,一些是功能改进,另一些则是修bug(比如不少plugin只支持 python 2)。
最后 output 指向
<a class="reference external" href="https://github.com/farseerfc/farseerfc.github.io">farseerfc.github.io</a>
也就是发布的静态网站啦。</p>
<p>接下来从 <strong>主题</strong> 和 <strong>插件</strong> 两个方面介绍一下改版的细节。</p>
</div>
</div>
<div class="section" id="material-design-bootstrap-3">
<h2><a class="toc-backref" href="#id42">主题: Material Design 风格的 Bootstrap 3</a></h2>
<p>上篇 <a class="reference external" href="//farseerfc.me/zhs/summarize-material-design-css-framework.html">博文</a>
就总结了我为了这个博客寻找了一堆 CSS 框架,并且最终决定用
<a class="reference external" href="http://fezvrasta.github.io/bootstrap-material-design/">bootstrap-material-design</a>
, <a class="reference external" href="https://github.com/DandyDev/pelican-bootstrap3">DandyDev/pelican-bootstrap3</a>
和 <a class="reference external" href="http://getbootstrap.com/">Bootstrap 3</a> 这三个项目结合的方式实现这个模板的主题。
这三个项目都或多或少经过了我的修改,修改后的项目以 pelican-bootstrap3 为基础放在
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3">这里</a> ,包括 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/tree/master/static/bootstrap">Bootstrap3 样式</a>
和 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/tree/master/static/material">Material 样式</a>。</p>
<div class="section" id="id16">
<h3><a class="toc-backref" href="#id43">对 Bootstrap 3 的定制</a></h3>
<p>由于架构完善,修改 Bootstrap 3 感觉非常简单。另一方面我在 Web 前端技术上的技能点也不多,
所以修改的地方非常有限,只能按我自己的需求定制而已。</p>
<div class="section" id="id17">
<h4><a class="toc-backref" href="#id44">响应式设备的大小</a></h4>
<div class="panel panel-default">
<div class="panel-heading">
修改了 Bootstrap 3 响应式设备的大小</div>
<div class="panel-body">
<div class="highlight"><pre><span class="code-line"><span></span><span class="p">@</span><span class="k">screen-xs</span><span class="o">:</span> <span class="nt">320px</span><span class="p">;</span></span>
<span class="code-line"><span class="p">@</span><span class="k">screen-sm</span><span class="o">:</span> <span class="nt">598px</span><span class="p">;</span> <span class="c">/* 768px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">screen-md</span><span class="o">:</span> <span class="nt">952px</span><span class="p">;</span> <span class="c">/* 992px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">screen-lg</span><span class="o">:</span> <span class="nt">1350px</span><span class="p">;</span> <span class="c">/* 1200px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">screen-xl</span><span class="o">:</span> <span class="nt">2030px</span><span class="p">;</span></span>
<span class="code-line"><span class="p">@</span><span class="k">container-sm</span><span class="o">:</span> <span class="nt">582px</span><span class="p">;</span> <span class="c">/* 750px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">container-md</span><span class="o">:</span> <span class="nt">930px</span><span class="p">;</span> <span class="c">/* 970px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">container-lg</span><span class="o">:</span> <span class="nt">1320px</span><span class="p">;</span> <span class="c">/* 1170px; */</span></span>
<span class="code-line"><span class="p">@</span><span class="k">container-xl</span><span class="o">:</span> <span class="nt">1990px</span><span class="p">;</span></span>
</pre></div>
</div>
</div>
<p>首先把 Bootstrap 3 默认适配的几个 <a class="reference external" href="http://getbootstrap.com/css/#grid">响应式设备的大小</a>
改成了我需要的大小。 <code class="code">
xs</code>
和 <code class="code">
sm</code>
的大小分别按照我的手机屏幕 <strong>竖屏</strong> 和
<strong>横屏</strong> 时候的浏览器页面宽度来算, <code class="code">
md</code>
是想兼容 Nexus 7 横屏 960 的宽度以及
一个常见上网本 1024 的宽度。 <code class="code">
lg</code>
的大小则按照常见的笔记本 1366 宽的屏幕来适配。</p>
<p>这里 Bootstrap 3 支持的设备大小的一个问题是,它最多考虑到 1200 像素宽的显示器,而更宽的
比如 1600、 2048 甚至 2560 像素宽的显示器现在也并不少见,其结果就是页面中左右两侧
有很大的空间被浪费掉了。作为深受这一问题困扰的用户之一,我用
<a class="reference external" href="http://stackoverflow.com/a/25644266">这里介绍的方法</a>
给 bootstrap 增加了一类「 <ruby><rb>比大更大</rb><rp>(</rp><rt>bigger than bigger</rt><rp>)</rp></ruby> 」的
<code class="code">
xl</code>
响应式设备尺寸,宽度设为支持 2048 像素宽的显示器,具体的修改反映在
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/blob/master/static/bootstrap/variables.less">variables.less</a>
文件里。</p>
</div>
<div class="section" id="id20">
<h4><a class="toc-backref" href="#id45">根据宽度自动分栏和瀑布式布局</a></h4>
<p>接下来目标是让主页的文章列表像 Google+ 主页那样根据显示器宽度自动调整分栏,使得宽度不同的
显示器上每个分栏的宽度接近。想要达到的效果是,根据上面定义的屏幕宽度尺寸:</p>
<table border="0" class="table docutils borderless">
<colgroup>
<col width="26%"/>
<col width="17%"/>
<col width="18%"/>
<col width="39%"/>
</colgroup>
<tbody valign="top">
<tr><td><code class="code">
xs</code>
用单栏 <ruby><rb>流动</rb><rp>(</rp><rt>fluid</rt><rp>)</rp></ruby> 布局</td>
<td colspan="2"><code class="code">
sm</code>
用上方单栏文章列表、下方双栏 <ruby><rb>侧边栏</rb><rp>(</rp><rt>sidebar</rt><rp>)</rp></ruby> 固定布局</td>
<td><code class="code">
md</code>
用单栏文章列表、单栏 侧边栏 固定布局</td>
</tr>
<tr><td><table border="0" class="table first docutils last borderless">
<colgroup>
<col width="100%"/>
</colgroup>
<tbody valign="top">
<tr><td><ruby><rb>导航栏</rb><rp>(</rp><rt>Navbar</rt><rp>)</rp></ruby></td>
</tr>
<tr><td>文章</td>
</tr>
<tr><td>侧边栏</td>
</tr>
<tr><td>底栏</td>
</tr>
</tbody>
</table>
</td>
<td colspan="2"><table border="0" class="table first docutils last borderless">
<colgroup>
<col width="53%"/>
<col width="47%"/>
</colgroup>
<tbody valign="top">
<tr><td colspan="2">导航栏</td>
</tr>
<tr><td colspan="2">文章</td>
</tr>
<tr><td>侧边栏 1</td>
<td>侧边栏 2</td>
</tr>
<tr><td colspan="2"><ruby><rb>底栏</rb><rp>(</rp><rt>footer</rt><rp>)</rp></ruby></td>
</tr>
</tbody>
</table>
</td>
<td><table border="0" class="table first docutils last borderless">
<colgroup>
<col width="53%"/>
<col width="47%"/>
</colgroup>
<tbody valign="top">
<tr><td colspan="2">导航栏</td>
</tr>
<tr><td>文章 1</td>
<td>侧边栏 1</td>
</tr>
<tr><td>文章 2</td>
<td>侧边栏 2</td>
</tr>
<tr><td colspan="2"><ruby><rb>底栏</rb><rp>(</rp><rt>footer</rt><rp>)</rp></ruby></td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr><td colspan="2"><code class="code">
lg</code>
用双栏文章列表、单栏 侧边栏 固定布局</td>
<td colspan="2"><code class="code">
xl</code>
用三栏文章列表、双栏 侧边栏 固定布局</td>
</tr>
<tr><td colspan="2"><table border="0" class="table first docutils last borderless">
<colgroup>
<col width="35%"/>
<col width="35%"/>
<col width="31%"/>
</colgroup>
<tbody valign="top">
<tr><td colspan="3">导航栏</td>
</tr>
<tr><td>文章 1</td>
<td>文章 3</td>
<td>侧边栏 1</td>
</tr>
<tr><td>文章 2</td>
<td>文章 4</td>
<td>侧边栏 2</td>
</tr>
<tr><td colspan="3"><ruby><rb>底栏</rb><rp>(</rp><rt>footer</rt><rp>)</rp></ruby></td>
</tr>
</tbody>
</table>
</td>
<td colspan="2"><table border="0" class="table first docutils last borderless">
<colgroup>
<col width="26%"/>
<col width="26%"/>
<col width="26%"/>
<col width="23%"/>
</colgroup>
<tbody valign="top">
<tr><td colspan="4">导航栏</td>
</tr>
<tr><td>文章 1</td>
<td>文章 3</td>
<td>文章 5</td>
<td>侧边栏 1</td>
</tr>
<tr><td>文章 2</td>
<td>文章 4</td>
<td>文章 6</td>
<td>侧边栏 2</td>
</tr>
<tr><td colspan="4"><ruby><rb>底栏</rb><rp>(</rp><rt>footer</rt><rp>)</rp></ruby></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p>一开始纯粹用 Bootstrap3 的响应式栅格实现这个分栏布局,结果发现效果不太理想,
因为文章列表和侧边栏的高度是变化的,会导致栅格间留下大片空白。后来改用
<a class="reference external" href="http://cssdeck.com/labs/pinterest-like-waterfall-design-purely-css">这里示范的纯CSS瀑布式布局</a>
实现文章和侧边栏的布局,具体的实现代码在
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/blob/master/static/bootstrap/waterfall.less">waterfall.less</a>
,总算达到了想要的布局了。</p>
</div>
<div class="section" id="id21">
<h4><a class="toc-backref" href="#id46">正文的样式</a></h4>
<p>最最重要的是文章正文的样式。这里我想要达到的效果是,在大屏幕上用更大的字号,让读者
看起来更舒适,同时在小屏幕上用比较小的字号,最终保证基本上「一行」的文字数接近。这个修改
主要针对 <code class="code">
.jumbotron</code>
,
用了 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/blob/master/static/bootstrap/jumbotron.less">不太科学的方式</a>
代码太长就不贴全了。</p>
</div>
<div class="section" id="id23">
<h4><a class="toc-backref" href="#id47">一些细微的定制</a></h4>
<p>把主题配色改成了现在这样的淡紫色
<code class="code">
@brand-primary: darken(#6B5594, 6.5%);</code>
,配合我的头像风格, 这个修改只需要一行。
接着删掉了 <code class="code">
.btn</code>
的 <code class="code">
white-space: nowrap;</code>
让按钮的文字可以换行,
这也只是一行修改。</p>
<div class="label label-warning">
<strong>2015年1月29日更新</strong></div>
<p>另外我也不太喜欢 Bootstrap 3 默认在手机上的 <ruby><rb>折叠导航栏</rb><rp>(</rp><rt>collapsed navbar</rt><rp>)</rp></ruby> ,
折叠之后的操作不够直观方便而且依赖 javascript 所以有 bug …… 于是我把它关掉了,
具体方式是在 variables.less 把 <code class="code">
@grid-float-breakpoint</code>
和
<code class="code">
@grid-float-breakpoint-max</code>
都设为0就可以了。</p>
</div>
</div>
<div class="section" id="id24">
<h3><a class="toc-backref" href="#id48">对 bootstrap-material-design 的定制</a></h3>
<p>这里定制的地方不多。原样式中一个不太科学的做法是所有 <code class="code">
.btn</code>
都强制加上了阴影
效果,这在已经有阴影的环境里用的话非常碍眼,像是 Win9x 风格的厚重睫毛膏。既然可以单独
给每个样式加阴影,于是就把 <code class="code">
.btn</code>
强制的阴影去掉了,只保留鼠标悬停之后强调的阴影。</p>
<p>其它定制的细节么就是统一配色风格,修补漏洞错误,微调响应式效果而已,这里不细说。</p>
</div>
<div class="section" id="id25">
<h3><a class="toc-backref" href="#id49">将以上两者整合在 pelican-bootstrap3 里</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
Pelican 实现显示源代码按钮</div>
<div class="panel-body">
<p>显示源代码按钮借用了 Pelican 配置中自带的 <code class="code">
OUTPUT_SOURCES</code>
选项将源文件复制到输出文件夹:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">OUTPUT_SOURCES</span> <span class="o">=</span> <span class="kc">True</span></span>
<span class="code-line"><span class="n">OUTPUT_SOURCES_EXTENSION</span> <span class="o">=</span> <span class="s1">'.rst'</span></span>
</pre></div>
<p>然后在 Makefile 里用 pygmentize 把所有源代码文件着色:</p>
<div class="highlight"><pre><span class="code-line"><span></span>find -iname <span class="s2">"*.rst"</span> <span class="p">|</span> parallel -I@ pygmentize -f html -o @.html @</span>
</pre></div>
<p>最后在按钮按下的时候用 jQuery 载入源代码:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="p"><</span><span class="nt">a</span> <span class="na">onclick</span><span class="o">=</span><span class="s">"$.get('{{SITEURL}}/{{article.slug}}.rst.html', function(data){$('#source-code').html(data)});$('#article-content').toggle();$('#source-content').toggle();"</span><span class="p">></span></span>
</pre></div>
<p>虽然难看的 hack 比较多,但是能用!</p>
</div>
</div>
<p>虽说 pelican-bootstrap3 是我 fork 出来的,不过由于我修改的地方实在太多,代码看来基本上
接近重写了一份。好在之前有给 pelican 写 bootstrap 2 主题的经验,这次修改算得上驾轻就熟。
可以对比一下 <a class="reference external" href="http://dandydev.net/">上游作者的博客</a> 和这里的样子体会一下感觉。
具体修改过的地方包括:</p>
<ol class="arabic simple">
<li>套用 bootstrap-material-design 的各个元素样式。</li>
<li>在文章列表模板应用上面提到的 Bootstrap 3 的栅格布局和瀑布式布局。</li>
<li>翻译到多个语言,这里在后面的 i18n-subsite 插件里详述。</li>
<li>套用后面会介绍到的各种插件。</li>
<li>统一侧边栏的样式到一个模板里。</li>
<li>添加 Atom 订阅按钮和 breadcrumb 条。</li>
<li>对正文中出现的插图,添加点击放大的功能,通过 Bootstrap 的 <code class="code">
modal</code>
实现。</li>
<li>上面提到的用 <a class="reference external" href="http://www.virtuosoft.eu/code/bootstrap-autohidingnavbar/">这个bootstrap插件</a>
让导航栏自动隐藏。</li>
<li><strong>显示源代码按钮</strong> ,也就是每篇文章信息栏中的
<button class="btn btn-primary"><i class="fa fa-code"></i></button> 按钮。</li>
</ol>
</div>
</div>
<div class="section" id="pelican-restructuredtext">
<h2><a class="toc-backref" href="#id50">插件: 发挥 Pelican 和 reStructuredText 的优势</a></h2>
<p>先列举一下我目前用到的所有插件:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">PLUGINS</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"i18n_subsites"</span><span class="p">,</span></span>
<span class="code-line"> <span class="s2">"plantuml"</span><span class="p">,</span></span>
<span class="code-line"> <span class="s2">"youku"</span><span class="p">,</span></span>
<span class="code-line"> <span class="s2">"youtube"</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'tipue_search'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'neighbors'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'series'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'bootstrapify'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'twitter_bootstrap_rst_directives'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s2">"render_math"</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'extract_toc'</span><span class="p">,</span></span>
<span class="code-line"> <span class="s1">'summary'</span><span class="p">]</span></span>
</pre></div>
<p>嗯其实不算多。接下来逐一介绍一下这些各具特色的插件。</p>
<div class="section" id="i18n-subsites">
<h3><a class="toc-backref" href="#id51">i18n-subsites</a></h3>
<p>这个插件的目的是创建 <ruby><rb>国际化</rb><rp>(</rp><rt>internationalization</rt><rp>)</rp></ruby>
<ruby><rb>子站</rb><rp>(</rp><rt>subsite</rt><rp>)</rp></ruby> 。</p>
<p>之前介绍 Pelican 配置的时候就提到过,
原本的 Pelican 就支持一篇文章用多种语言书写,有 <code class="code">
lang</code>
属性注明这篇文章使用的
语言,以及 <code class="code">
slug</code>
属性注明多语言的翻译之间的关联,换句话说同一篇文章的多个语言
版本应该有相同的 <code class="code">
slug</code>
和不同的 <code class="code">
lang</code>
。然后原本 Pelican 里对多语言的
实现方式是,首先有一个 <strong>主语言</strong> 是模板和大部分文章采用的语言,文章列表中会优先列出
用 <strong>主语言</strong> 撰写的文章,然后从 <strong>主语言</strong> 的文章链接到别的翻译版本。
很多博客系统和CMS对多语言的支持都是这样的,这种处理方式的缺点也显而易见:作为 <strong>主语言</strong>
的语言必须足够通用,才能让进来的人找到合适的翻译版本,所以通常 <strong>主语言</strong> 都是英语。</p>
<p>而这个插件做的事情描述起来很简单:将文章按语言属性分到多个子站,每个子站独立放在各自的文件夹。
比如主站是 <a class="reference external" href="https://farseerfc.github.io/">https://farseerfc.github.io/</a> 的话,那么英语的子站就可以是
<a class="reference external" href="https://farseerfc.github.io/en/">https://farseerfc.github.io/en/</a> 。
然后分别对多个子站生成静态页面。具体的实现方式是对 pelican 的页面生成步骤做了拆分:</p>
<ol class="arabic simple">
<li>pelican 按正常情况读入文章,生成元信息。</li>
<li>i18n-subsites 针对每个语言,覆盖掉 pelican 的一些选项设置比如路径和 URL ,
分别调用 pelican 的页面生成器按模板生成文章。</li>
<li>对共用的静态内容比如模板的 js 和 css 文件,只在主站中生成,子站中的相应链接全部链回主站。</li>
</ol>
<p>虽然描述起来简单,但是这个插件可以说最大化利用了 Pelican 的插件系统,实现细节相对比较
复杂,大概是我用的这些插件里面最复杂的了。不夸张的说 Pelican 3.4 支持的新插件 API 和
站内链接功能基本上就是为了配合这个插件的。至于具体它会覆盖哪些 Pelican 的配置,请参阅它的
<a class="reference external" href="https://github.com/farseerfc/pelican-plugins/blob/master/i18n_subsites/README.rst">README.md文件</a> 。</p>
<p>按内容拆分多语言子站的做法只解决了问题的一半,还留下另一半的问题,也即对模板的翻译。
对这个问题, i18n-subsites 提供了两套方案供选择:</p>
<ol class="arabic simple">
<li>用覆盖配置路径的方式让每个子站套用不同的模板。这配置起来简单,但是对模板维护起来有点困难。</li>
<li>用 jinja2 的 i18n 插件,配合 Python 的 gettext 库实现内容翻译。这个方案
<a class="reference external" href="https://github.com/farseerfc/pelican-plugins/blob/master/i18n_subsites/localizing_using_jinja2.rst">配置起来比较复杂</a> ,但是配置好之后用起来就很方便了。
只是要记得每次修改了模板都要更新翻译,处理 *.po 和 *.mo 文件等等琐碎事宜。</li>
</ol>
<p>这里我用 jinja2 的 i18n 插件的方式实现了模板的翻译,
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/tree/master/translations">各个语言的翻译在这里</a> ,
然后用 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/blob/master/SConstruct">这里的 SCons 脚本</a>
根据内容是否变化自动更新 po 和 mo 文件。</p>
<p>配置好这一套方案之后,还要注意在模板和文章中处理好链接。用 Pelican 3.4 之后推荐的
新的文章间链接的写法以及将 <code class="code">
SITEURL</code>
设置为实际 URL 并且关闭 <code class="code">
RELATIVE_URLS</code>
之后,应该就不会出没什么问题了(可能还要考虑使用的模板和插件的兼容性,大部分都是写死了 URL 的问题)。</p>
</div>
<div class="section" id="plantuml">
<h3><a class="toc-backref" href="#id52">plantuml</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
嵌入 PlantUML 的示例</div>
<div class="panel-body">
<img alt="uml diagram" class="uml img-responsive" src="//farseerfc.me/uml/8e4b1ee3.png"/>
</div>
</div>
<p><a class="reference external" href="http://plantuml.sourceforge.net/">PlantUML</a> 是一个Java实现的,
用接近文字描述的语言绘制 UML 图或者 GUI 界面图的工具,非常适合嵌入在
Markdown、 reStructuredText、 AsciiDoc 等这种轻量级标记语言里。
然后么这个 plantuml 插件就是定义了一个新的 reStructuredText
<ruby><rb>指示符</rb><rp>(</rp><rt>directive</rt><rp>)</rp></ruby> <code class="code">
.. uml::</code>
,把嵌入的内容提取出来调用 plantuml 命令处理
成图像然后再插入到文章中。</p>
<p>比如示例里的这个 UML 图就是用这样一段简单的文字描述生成的:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="p">..</span> <span class="ow">uml</span><span class="p">::</span></span>
<span class="code-line"></span>
<span class="code-line"> Object <|-- ArrayList</span>
<span class="code-line"></span>
<span class="code-line"> Object : equals()</span>
<span class="code-line"> ArrayList : Object[] elementData</span>
<span class="code-line"> ArrayList : size()</span>
</pre></div>
<p>实际用起来这个插件实现上稍微有点小问题:首先它只支持 python2,所以我把它改写成了 python
2 和 3 都通用的语法;其次它原本输出的文件夹似乎会被 pelican 删掉,所以把它改了个位置;
然后它输出的 URL 也和 i18n-subsites 插件间有不兼容的问题,也顺带修掉了。
<a class="reference external" href="https://github.com/farseerfc/pelican-plugins/tree/master/plantuml">修改之后的代码在这里</a> 。</p>
<div class="label label-warning">
<strong>2015年1月30日更新</strong></div>
<div class="panel panel-default">
<div class="panel-heading">
嵌入 Ditaa 的示例</div>
<div class="panel-body">
<img alt="ditaa diagram" class="ditaa img-responsive" src="//farseerfc.me/uml/973a8424.png"/>
</div>
</div>
<p>plantuml 是绘制UML的,除此之外还有一个类似的工具是绘制一般的 <ruby><rb>流程图</rb><rp>(</rp><rt>diagram</rt><rp>)</rp></ruby>
的,叫 <a class="reference external" href="http://ditaa.sourceforge.net/">ditaa</a> ,和 plantuml 非常像,也比较像
reStructuredText 的表格。
于是我也照猫画虎实现了一个 ditaa 的 <ruby><rb>指示符</rb><rp>(</rp><rt>directive</rt><rp>)</rp></ruby> ,用起来类似这样:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="p">..</span> <span class="ow">ditaa</span><span class="p">::</span></span>
<span class="code-line"></span>
<span class="code-line"> +-------------+</span>
<span class="code-line"> <span class="o">|</span> ditaa |-------+</span>
<span class="code-line"> <span class="o">|</span> Diagram | |</span>
<span class="code-line"> +-------------+ | PNG out</span>
<span class="code-line"> ^ |</span>
<span class="code-line"> <span class="o">|</span> ditaa in |</span>
<span class="code-line"> <span class="o">|</span> v</span>
<span class="code-line"> +--------+ +--------+----+ /----------------\</span>
<span class="code-line"> <span class="o">|</span> | --+ Pelican +--> | |</span>
<span class="code-line"> <span class="o">|</span> Text | +-------------+ | Beautiful Blog |</span>
<span class="code-line"> |Document| | !magic! | | |</span>
<span class="code-line"> <span class="o">|</span> {d}| | | | |</span>
<span class="code-line"> +---+----+ +-------------+ \----------------/</span>
<span class="code-line"> : ^</span>
<span class="code-line"> <span class="o">|</span> Lots of work |</span>
<span class="code-line"> +-----------------------------------+</span>
</pre></div>
</div>
<div class="section" id="render-math">
<h3><a class="toc-backref" href="#id53">render-math</a></h3>
<div class="panel panel-default">
<div class="panel-heading">
嵌入公式的示例</div>
<div class="panel-body">
<p>示范行内公式 <span class="math">\(A_\text{c} = (\pi/4) d^2\)</span>.</p>
<p>整行公式</p>
<div class="math">
\begin{equation*}
\alpha{}_t(i) = P(O_1, O_2, \ldots O_t, q_t = S_i \lambda{})
\end{equation*}
</div>
</div>
</div>
<p>这个插件提供在 reStructuredText 中用 LaTeX 语法插入数学公式的能力,定义了
<code class="code">
:math:</code>
<ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> 和 <code class="code">
.. math::</code>
<ruby><rb>指示符</rb><rp>(</rp><rt>directive</rt><rp>)</rp></ruby> 。
实际工作的渲染库当然是大名鼎鼎的 <a class="reference external" href="http://www.mathjax.org/">MathJax</a> ,这个插件
会用 MathJax 的 CDN 载入,所以也没有额外的依赖文件。(只是不知道是否会被国内墙掉,
如果公式显示不正常请 <strong>务必</strong> 告诉我。)</p>
</div>
<div class="section" id="youtube-youku">
<h3><a class="toc-backref" href="#id54">youtube 和 youku</a></h3>
<p>顾名思义,这两个插件分别实现嵌入 youtube 和 youku 视频。其中 youtube 是原本就有的插件,
youku 是我照猫画虎抄的。
之前写了一篇
<a class="reference external" href="//farseerfc.me/zhs/jumping-kde5-plasma-activities-button.html">KDE5 Plasma 之跳动卖萌的活动按钮</a>
用到了这两个插件。</p>
</div>
<div class="section" id="tipue-search">
<h3><a class="toc-backref" href="#id55">tipue_search</a></h3>
<p><a class="reference external" href="http://www.tipue.com/search/">Tipue search</a> 是一个非常有意思也很强大的搜索工具,
通过 jQuery 实现静态博客的站内搜索功能。实现方式是,它需要你写一个 json 文件,包含
整个网站的 <strong>全部</strong> 文章的标题和文字内容,然后在搜索的时候读入这个 json 做搜索(是不是有点耍赖)。
虽然听起来会有性能问题,但是应用在小型的静态博客上效果意外很不错,比如本站的所有文章内容
放在一起的 json 也只有 300KiB 左右。</p>
<p>这个插件就是自动在 pelican 输出完全部静态网页之后,调用 beautifulsoup4 从所有网页中抽取出
纯文本,产生这个 json 给 Tipue 用。</p>
</div>
<div class="section" id="neighbors-series">
<h3><a class="toc-backref" href="#id56">neighbors 和 series</a></h3>
<p>这两个插件比较类似也都比较简单, neighbors 提供一篇文章的前后文章信息,
在主题模板里可以用来制作 <strong>上一篇</strong> 和 <strong>下一篇</strong> 按钮。
series 提供将多篇文章归类为一个 <strong>系列</strong> 的支持,当然也需要在
主题模板中定义显示「文章系列」的列表。这两个插件的效果都能在本文末尾,评论区上方的部分看到。</p>
</div>
<div class="section" id="bootstrapify-twitter-bootstrap-rst-directives">
<h3><a class="toc-backref" href="#id57">bootstrapify 和 twitter_bootstrap_rst_directives</a></h3>
<p>这两个插件让文章的 <strong>正文</strong> 套用上 Bootstrap 的样式。</p>
<p>bootstrapify 这个插件实现得比较简单,用 beautifulsoup4 在静态网页的结果里面过滤元素,
对 <code class="code">
table</code>
, <code class="code">
img</code>
, <code class="code">
embed</code>
, <code class="code">
iframe</code>
,
<code class="code">
video</code>
, <code class="code">
object</code>
这几个标签套用上
<a class="reference external" href="http://getbootstrap.com/components/#responsive-embed">响应式嵌入对象的类</a>
让他们更美观。</p>
<p>twitter_bootstrap_rst_directives 这个插件则是增加了几个 reStructuredText 的
<ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> 和 <ruby><rb>指示符</rb><rp>(</rp><rt>directive</rt><rp>)</rp></ruby> 。
它实现的 <ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> 包括:
用 <code class="code">
:kbd:</code>
实现如 <kbd class="kbd">
Ctrl+C</kbd>
这样的键盘快捷键,
用 <code class="code">
:code:</code>
嵌入代码片段,用 <code class="code">
:glyph:</code>
嵌入字符图标。
它实现的 <ruby><rb>指示符</rb><rp>(</rp><rt>directive</rt><rp>)</rp></ruby> 包括:
<a class="reference external" href="http://getbootstrap.com/components/#labels">labels 行内标签</a> ,
<a class="reference external" href="http://getbootstrap.com/components/#alerts">alerts 提示段落</a> ,
<a class="reference external" href="http://getbootstrap.com/components/#panels">panels 嵌入面板</a> ,
以及还有一个 <a class="reference external" href="http://getbootstrap.com/components/#media">media 混排图标</a> 。</p>
<p>对其中的 <code class="code">
panel</code>
我改写了它在文章正文中的样式,在 <code class="code">
lg</code>
或者 <code class="code">
xl</code>
的屏幕宽度下,分别用 <span class="math">\(\frac{1}{2}\)</span> 和 <span class="math">\(\frac{1}{3}\)</span> 大小的嵌入面板,
简单实现和正文文字的图文混排。</p>
<p>除此以外我还在 twitter_bootstrap_rst_directives 这个插件里套用它的框架实现了两个额外
的 <ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> , 分别是 <code class="code">
:ruby:</code>
:通过 html5 的 <code class="code">
<ruby></code>
标签实现文字上方的注音(firefox下
<a class="reference external" href="https://bugzilla.mozilla.org/show_bug.cgi?id=33339">不支持</a>
,会使用文字后的括号显示), 以及 <code class="code">
:html:</code>
:在
行内插入 <ruby><rb>裸</rb><rp>(</rp><rt>raw</rt><rp>)</rp></ruby> html 标签(这属于 Markdown 的基本功能,在 reStructuredText
这边由于要考虑多种输出格式于是就比较麻烦了)。这两个 <ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> 的
<a class="reference external" href="https://github.com/farseerfc/pelican-plugins/blob/master/twitter_bootstrap_rst_directives/bootstrap_rst_directives.py#L140">实现代码在这里</a> 。</p>
<div class="label label-warning">
<strong>2015年2月3日更新</strong></div>
<p>今天又在 twitter_bootstrap_rst_directives 里增加了两个 <ruby><rb>行内角色</rb><rp>(</rp><rt>role</rt><rp>)</rp></ruby> 。
一个是 <code class="code">
:twi:</code>
用来写 twitter 用户的链接,比如 <a class="reference external" href="//twitter.com/farseerfc">@farseerfc</a> ,另一个是
<code class="code">
:irc:</code>
用来指向 freenode 的 channel ,比如 <a class="reference external" href="//webchat.freenode.net/?channels=yssyd3">#yssyd3</a> 。</p>
<div class="label label-warning">
<strong>2015年2月14日更新</strong></div>
<p>今天增加了 <code class="code">
.. friend::</code>
用来写好友链接,以及 <code class="code">
fref</code>
用来引用好友,
比如 <a class="reference external" href="/links.html#lqymgt">LQYMGT</a> 这样。</p>
</div>
<div class="section" id="extract-toc-summary">
<h3><a class="toc-backref" href="#id58">extract_toc 和 summary</a></h3>
<p>最后是这两个有点「名不副实」的插件。</p>
<p>reStructuredText 原本就有自动生成
<ruby><rb>目录</rb><rp>(</rp><rt>toc</rt><rp>)</rp></ruby> 的功能,用起来也非常简单,只需要在想要插入目录的地方写一行
<code class="code">
.. contents::</code>
,剩下的都由 docutils 自动生成了。
只是当然这样生成的目录肯定会插入在文章的正文里,而 extract_toc 这个插件的作用就是简单地
把这个目录抽取出来,让模板能在别的地方放置这个目录。比如我这里就把目录放在了一个
<code class="code">
panel</code>
里。</p>
<p>然后 Pelican 也原本就有从文章中抽取 <ruby><rb>总结</rb><rp>(</rp><rt>summary</rt><rp>)</rp></ruby> 显示在文章列表的功能。
Pelican 原始的实现似乎是按照文字数抽取前半段,不总是适合作为总结。 于是这个 summary
插件的作用其实是允许在正文中以特殊的注释的方式标注哪些部分应该被抽出来作为总结。
summary 这个插件原本的实现只允许抽取一段文字,我又对它的实现做了少许扩充,允许标注多段
文字合并起来作为总结。</p>
<div class="label label-warning">
<strong>2015年1月29日更新</strong></div>
<p>今天在 extract_toc 插件的帮助下,在侧边栏里放了一个 Bootstrap affix 的目录,
它保持在页面的右侧位置不变,方便导航到文章的各个地方。具体实现方法除了 Bootstrap 3 的
<a class="reference external" href="http://getbootstrap.com/javascript/#affix">Affix 文档</a> ,还参考了
<a class="reference external" href="http://tutsme-webdesign.info/bootstrap-3-affix/">这篇更详细的说明</a> 。</p>
</div>
</div>
<div class="section" id="id36">
<h2><a class="toc-backref" href="#id59">结语</a></h2>
<p>这个博客的配置都可以在
<a class="reference external" href="https://github.com/farseerfc/farseerfc/blob/master/pelicanconf.py">github 上找到</a>
,包括用来
<a class="reference external" href="https://github.com/farseerfc/farseerfc/blob/master/Makefile">自动生成整个博客的 Makefile</a>
,由于比较长,这里就不再贴了。</p>
<p>折腾这个主题前后历时两个月,期间学会了不少东西,也算是不错的收获吧。
现在既然基础打好了,接下来就要开始多写博客了。(希望拖延症不会再犯……)</p>
<p>最近发现除了我的博客之外还有一个网站
<a class="reference external" href="http://www.kansaslinuxfest.us/">Kansas Linux Fest</a> fork
了我的主题,不过他们用了我修改的早期版本,还是原本的 Bootstrap 3 和
bootstrap-material-design 样式。自己草草修改的东西被别人用到果然还是有点小激动呢,
以及接下来不能马马虎虎地写 commit 消息了。</p>
<table border="0" class="docutils table footnote" frame="void" id="id37" rules="none">
<colgroup><col class="label"/><col/></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td>赛65:17「看哪!我造新天新地」启21:5「我将一切都更新了。」</td></tr>
</tbody>
</table>
</div>
<script type='text/javascript'>if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
var align = "center",
indent = "0em",
linebreak = "false";
if (false) {
align = (screen.width < 768) ? "left" : align;
indent = (screen.width < 768) ? "0em" : indent;
linebreak = (screen.width < 768) ? 'true' : linebreak;
}
var mathjaxscript = document.createElement('script');
var location_protocol = (false) ? 'https' : document.location.protocol;
if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:';
mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
mathjaxscript.type = 'text/javascript';
mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
mathjaxscript[(window.opera ? "innerHTML" : "text")] =
"MathJax.Hub.Config({" +
" config: ['MMLorHTML.js']," +
" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } }," +
" jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
" displayAlign: '"+ align +"'," +
" displayIndent: '"+ indent +"'," +
" showMathMenu: true," +
" messageStyle: 'normal'," +
" tex2jax: { " +
" inlineMath: [ ['\\\\(','\\\\)'] ], " +
" displayMath: [ ['$$','$$'] ]," +
" processEscapes: true," +
" preview: 'TeX'," +
" }, " +
" 'HTML-CSS': { " +
" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
" linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
" }, " +
"}); " +
"if ('default' !== 'default') {" +
"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
"VARIANT['normal'].fonts.unshift('MathJax_default');" +
"VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
"VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
"});" +
"}";
(document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
</script>总结一下 Material Design 的 CSS 框架2015-01-16T03:27:00+09:002015-01-16T03:27:00+09:00farseerfctag:farseerfc.me,2015-01-16:/zhs/summarize-material-design-css-framework.html<!-- PELICAN_BEGIN_SUMMARY -->
<p>现在这里的界面风格要从 Google 在 <a class="reference external" href="https://www.google.com/events/io">I/O 2014 大会</a>
上公布Android L 也即 后来的 Lollipop 说起。 他们在谈论界面设计的时候公布了他们的
设计准则: <a class="reference external" href="http://www.google.com/design/spec/material-design/introduction.html">Material Design</a> (<a class="reference external" href="http://wcc723.gitbooks.io/google_design_translate/">中文非官方翻译</a> )。
当然这只是一些准则,总结并描述了之前在 Web 设计和移动端 App 界面设计方面的一些规范,
并且用材料的类比来形象化的比喻这个准则。关于 Material Design 的更多中文资料可
<a class="reference external" href="http://www.ui.cn/Material/">参考这里</a> 。</p>
<p>看到 Material Design 之后就觉得这个设计风格非常符合直觉,于是想在这边也用上
Material Design。 但是我在 Web 前端科技树上没点多少技能点,所以想找找别人实现好的模板
或者框架直接套用上。在网络上搜索数日找到了这几个:</p>
<div class="section" id="polymer-paper-elements">
<h2>Polymer Paper Elements</h2>
<!-- PELICAN_END_SUMMARY -->
<div class="panel panel-default">
<div class="panel-heading">
Polymer</div>
<div class="panel-body">
<object class="embed-responsive-item" data="https://www.polymer-project.org/images/logos/p-logo.svg" type="image/svg+xml">
Polymer logo</object>
</div>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>Google …</p></div><!-- PELICAN_BEGIN_SUMMARY -->
<p>现在这里的界面风格要从 Google 在 <a class="reference external" href="https://www.google.com/events/io">I/O 2014 大会</a>
上公布Android L 也即 后来的 Lollipop 说起。 他们在谈论界面设计的时候公布了他们的
设计准则: <a class="reference external" href="http://www.google.com/design/spec/material-design/introduction.html">Material Design</a> (<a class="reference external" href="http://wcc723.gitbooks.io/google_design_translate/">中文非官方翻译</a> )。
当然这只是一些准则,总结并描述了之前在 Web 设计和移动端 App 界面设计方面的一些规范,
并且用材料的类比来形象化的比喻这个准则。关于 Material Design 的更多中文资料可
<a class="reference external" href="http://www.ui.cn/Material/">参考这里</a> 。</p>
<p>看到 Material Design 之后就觉得这个设计风格非常符合直觉,于是想在这边也用上
Material Design。 但是我在 Web 前端科技树上没点多少技能点,所以想找找别人实现好的模板
或者框架直接套用上。在网络上搜索数日找到了这几个:</p>
<div class="section" id="polymer-paper-elements">
<h2>Polymer Paper Elements</h2>
<!-- PELICAN_END_SUMMARY -->
<div class="panel panel-default">
<div class="panel-heading">
Polymer</div>
<div class="panel-body">
<object class="embed-responsive-item" data="https://www.polymer-project.org/images/logos/p-logo.svg" type="image/svg+xml">
Polymer logo</object>
</div>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>Google 官方提供的参考实现应该是 <a class="reference external" href="https://www.polymer-project.org/">Polymer</a> 中的
<a class="reference external" href="https://www.polymer-project.org/docs/elements/paper-elements.html">Paper Elements</a> 。</p>
<!-- PELICAN_END_SUMMARY -->
<p>由于是 <strong>官方参考实现</strong> ,这个框架的确非常忠实地实现了 Material Design 的设计,但是同时
由于它基于 <a class="reference external" href="http://webcomponents.org/">HTML5 Web Components</a> 构建,相关技术我还
不太懂,浏览器兼容性和其余 HTML 技术的兼容性也还不太完善的样子……</p>
<p>并且对于我这个 Web 开发的半吊子来说,Polymer 只是提供了一组设计组建,没有完善的
<strong>响应式</strong> (responsive) 布局支持,也没有 Navbar 这种常见的框架组建,真的要用起来的话还
需要手工实现不少东西。于是口水了半天之后只好放弃……以后可能真的会换用这个,只是目前需要学
的东西太多了。</p>
</div>
<div class="section" id="angular-material-design">
<h2>Angular Material Design</h2>
<div class="panel panel-default">
<div class="panel-heading">
AngularJS</div>
<div class="panel-body">
<img alt="AngularJS logo" class="img-responsive" src="https://angularjs.org/img/AngularJS-large.png"/>
</div>
</div>
<p><a class="reference external" href="https://angularjs.org/">AngularJS</a> 是 Google 对 Web Components 技术的另一个
尝试。而这额 <a class="reference external" href="https://material.angularjs.org/">Angular Material Design</a> 项目
就是基于 AngularJS 构建的Material Design 库啦,同样是 Google 出品所以应该算得上半个
官方实现吧。 相比于 Polymer, AngularJS 算是实用了很多,提供了基于
<a class="reference external" href="http://www.w3.org/TR/css3-flexbox/">CSS Flexbox</a> 的布局。有人对这两者的评价是,
如果说 Polymer 代表了 <strong>未来趋势</strong> ,那么 AngularJS 就是 <strong>眼下可用</strong> 的 Web
Components 实现了。</p>
<p>只不过同样是因为它是 Components 的框架,对 WebApp 的支持很丰富,大量采用 Ajax 等
JavaScript 技术, 对于我这个静态博客来说仍然稍显高级了……非常担心还不支持 HTML5 的浏览器
比如 w3m 甚至 cURL 对它的支持程度。 于是最终也没有使用它。</p>
</div>
<div class="section" id="materialize">
<h2>Materialize</h2>
<div class="panel panel-default">
<div class="panel-heading">
Materialize</div>
<div class="panel-body">
<img alt="Materialize logo" class="img-responsive" src="https://raw.githubusercontent.com/Dogfalo/materialize/master/images/materialize.gif"/>
</div>
</div>
<p><a class="reference external" href="http://materializecss.com/">Materialize</a> 这是一批(自称?)熟悉 Android 上
Material Design 的设计师们新近出炉的框架,试图提供一个接近 Bootstrap 的方案。
最早是在 <a class="reference external" href="http://www.reddit.com/r/web_design/comments/2lt4qy/what_do_you_think_of_materialize_a_responsive/">Reddit</a> 上看到对它的讨论的,立刻觉得这个想法不错。</p>
<p>体验一下官网的设计就可以看出,他们的动画效果非常接近 Polymer 的感觉,响应式设计的布局
也还不错。 只是同样体验一下他们现在的官网就可以看出,他们目前的
<a class="reference external" href="https://github.com/Dogfalo/materialize/issues">bug 还比较多</a> ,甚至一些 bug
在他们自己的主页上也有显现。 虽然不想给这个新出炉的项目泼凉水,不过看来要达到他们声称的接近
Bootstrap 的易用度还任重而道远……</p>
</div>
<div class="section" id="bootstrap-material-design-bootstrap3">
<h2>bootstrap-material-design + bootstrap3</h2>
<p>这是我最终选择的方案。这个方案将三个项目组合在了一起,分别是
<a class="reference external" href="http://fezvrasta.github.io/bootstrap-material-design/">bootstrap-material-design</a>
, <a class="reference external" href="https://github.com/DandyDev/pelican-bootstrap3">pelican-bootstrap3</a>
和 <a class="reference external" href="http://getbootstrap.com/">Bootstrap 3</a> 。
Bootstrap 3 想必不用再介绍了,很多网站都在使用这套框架,定制性很高。
bootstrap-material-design 是在 Bootstrap 3 的基础上套用 Material Design 风格
制作的一套 CSS 库,当然也不是很完善并且在不断改进中,一些细节其实并不是很符合我的要求。
最后 pelican-bootstrap3 是用 Bootstrap 3 做的 pelican 模板。
这三个项目或多或少都有点不合我的口味,于是嘛就把 pelican-bootstrap3 fork了一套放在
<a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3">这里</a> ,其中还包括我自己改
过的 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/tree/master/static/bootstrap">Bootstrap3 样式</a>
和 <a class="reference external" href="https://github.com/farseerfc/pelican-bootstrap3/tree/master/static/material">Material 样式</a>
,需要的可以自取。</p>
<p>至于细节上我定制了哪些地方,敬请听下回分解……</p>
</div>
从非缓冲输入流到 Linux 控制台的历史2014-12-12T17:06:00+09:002014-12-12T17:06:00+09:00farseerfctag:farseerfc.me,2014-12-12:/zhs/from-unbuffered-stdin-to-history-of-linux-tty.html
<p>这篇也是源自于水源C板上板友的一个问题,涉及Linux上的控制台的实现方式和历史原因。因为内容比较长,所以在这里再排版一下发出来。
<a class="reference external" href="http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1418138991,file,M.1418138991.A.html">原帖在这里</a> 。</p>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id7">可以设置不带缓冲的标准输入流吗?</a></h2>
<p>WaterElement(UnChanged) 于 2014年12月09日23:29:51 星期二 问到:</p>
<blockquote>
<p>请问对于标准输入流可以设置不带缓冲吗?比如以下程序</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="cp">#include</span> <span class="cpf"><stdio.h></span><span class="cp"></span></span>
<span class="code-line"><span class="cp">#include</span> <span class="cpf"><unistd.h></span><span class="cp"></span></span>
<span class="code-line"></span>
<span class="code-line"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fp</span> <span class="o">=</span> <span class="n">fdopen</span><span class="p">(</span><span class="n">STDIN_FILENO</span><span class="p">,</span> <span class="s">"r"</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">setvbuf</span><span class="p">(</span><span class="n">fp</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">_IONBF</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span></span>
<span class="code-line"> <span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="mi">20</span><span class="p">];</span></span>
<span class="code-line"> <span class="n">buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"> <span class="n">fgets</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="n">fp</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">printf</span><span class="p">(</span><span class="s">"buffer …</span></span></pre></div></blockquote></div>
<p>这篇也是源自于水源C板上板友的一个问题,涉及Linux上的控制台的实现方式和历史原因。因为内容比较长,所以在这里再排版一下发出来。
<a class="reference external" href="http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1418138991,file,M.1418138991.A.html">原帖在这里</a> 。</p>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id7">可以设置不带缓冲的标准输入流吗?</a></h2>
<p>WaterElement(UnChanged) 于 2014年12月09日23:29:51 星期二 问到:</p>
<blockquote>
<p>请问对于标准输入流可以设置不带缓冲吗?比如以下程序</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="cp">#include</span> <span class="cpf"><stdio.h></span><span class="cp"></span></span>
<span class="code-line"><span class="cp">#include</span> <span class="cpf"><unistd.h></span><span class="cp"></span></span>
<span class="code-line"></span>
<span class="code-line"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fp</span> <span class="o">=</span> <span class="n">fdopen</span><span class="p">(</span><span class="n">STDIN_FILENO</span><span class="p">,</span> <span class="s">"r"</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">setvbuf</span><span class="p">(</span><span class="n">fp</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">_IONBF</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span></span>
<span class="code-line"> <span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="mi">20</span><span class="p">];</span></span>
<span class="code-line"> <span class="n">buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"> <span class="n">fgets</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="n">fp</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">printf</span><span class="p">(</span><span class="s">"buffer is:%s"</span><span class="p">,</span> <span class="n">buffer</span><span class="p">);</span></span>
<span class="code-line"> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
<p>似乎还是需要在命令行输入后按回车才会让 <code class="code">
fgets</code>
返回,不带缓冲究竟体现在哪里?</p>
</blockquote>
<div class="section" id="id3">
<h3><a class="toc-backref" href="#id8">这和缓存无关,是控制台的实现方式的问题。</a></h3>
<p>再讲细节一点,这里有很多个程序和设备。以下按 linux 的情况讲:</p>
<ol class="arabic simple">
<li>终端模拟器窗口(比如xterm)收到键盘事件</li>
<li>终端模拟器(xterm)把键盘事件发给虚拟终端 pty1</li>
<li>pty1 检查目前的输入状态,把键盘事件转换成 stdin 的输入,发给你的程序</li>
<li>你的程序的 c 库从 stdin 读入一个输入,处理</li>
</ol>
<p>标准库说的输入缓存是在 4 的这一步进行的。而行输入是在 3 的这一步被缓存起来的。</p>
<p>终端pty有多种状态,一般控制台程序所在的状态叫「回显行缓存」状态,这个状态的意思是:</p>
<ol class="arabic simple">
<li>所有普通字符的按键,会回显到屏幕上,同时记录在行缓存区里。</li>
<li>处理退格( <kbd class="kbd">
BackSpace</kbd>
),删除( <kbd class="kbd">
Delete</kbd>
)按键为删掉字符,左右按键移动光标。</li>
<li>收到回车的时候把整个一行的内容发给stdin。</li>
</ol>
<p>参考: <a class="reference external" href="http://en.wikipedia.org/wiki/Cooked_mode">http://en.wikipedia.org/wiki/Cooked_mode</a></p>
<p>同时在Linux/Unix下可以发特殊控制符号给pty让它进入「raw」状态,这种状态下按键
不会被回显,显示什么内容都靠你程序自己控制。
如果你想得到每一个按键事件需要用raw状态,这需要自己控制回显自己处理缓冲,
简单点的方法是用 readline 这样的库(基本就是「回显行缓存」的高级扩展,支持了
Home/End,支持历史)或者 ncurses 这样的库(在raw状态下实现了一个简单的窗口/
事件处理框架)。</p>
<p>参考: <a class="reference external" href="http://en.wikipedia.org/wiki/POSIX_terminal_interface#History">http://en.wikipedia.org/wiki/POSIX_terminal_interface#History</a></p>
<p>除此之外, <kbd class="kbd">
Ctrl-C</kbd>
转换到 SIGINT , <kbd class="kbd">
Ctrl-D</kbd>
转换到 EOF 这种也是在 3 这一步做的。</p>
<p>以及,有些终端模拟器提供的 <kbd class="kbd">
Ctrl-Shift-C</kbd>
表示复制这种是在 2 这一步做的。</p>
<p>以上是 Linux/unix 的方式。 Windows的情况大体类似,只是细节上有很多地方不一样:</p>
<ol class="arabic simple">
<li>窗口事件的接收者是创建 cmd 窗口的 Win32 子系统。</li>
<li>Win32子系统接收到事件之后,传递给位于 命令行子系统 的 cmd 程序</li>
<li>cmd 程序再传递给你的程序。</li>
</ol>
<p>Windows上同样有类似行缓存模式和raw模式的区别,只不过实现细节不太一样。</p>
</div>
<div class="section" id="strace">
<h3><a class="toc-backref" href="#id9">strace查看了下</a></h3>
<p>WaterElement(UnChanged) 于 2014年12月10日21:53:54 星期三 回复:</p>
<blockquote>
<p>感谢FC的详尽解答。</p>
<p>用strace查看了下,设置标准输入没有缓存的话读每个字符都会调用一次 <code class="code">
read</code>
系统调用,
比如输入abc:</p>
<div class="highlight"><pre><span class="code-line"><span></span>read(0, abc</span>
<span class="code-line">"a", 1) = 1</span>
<span class="code-line">read(0, "b", 1) = 1</span>
<span class="code-line">read(0, "c", 1) = 1</span>
<span class="code-line">read(0, "\n", 1) = 1</span>
</pre></div>
<p>如果有缓存的话就只调用一次了 <code class="code">
read</code>
系统调用了:</p>
<div class="highlight"><pre><span class="code-line"><span></span>read(0, abc</span>
<span class="code-line">"abc\n", 1024) = 4</span>
</pre></div>
</blockquote>
</div>
<div class="section" id="raw-mode">
<h3><a class="toc-backref" href="#id10">如果想感受一下 raw mode</a></h3>
<p>没错,这个是你的进程内C库做的缓存,tty属于字符设备所以是一个一个字符塞给你的
程序的。</p>
<p>如果想感受一下 raw mode 可以试试下面这段程序(没有检测错误返回值)</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="cp">#include</span> <span class="cpf"><stdio.h></span><span class="cp"></span></span>
<span class="code-line"><span class="cp">#include</span> <span class="cpf"><unistd.h></span><span class="cp"></span></span>
<span class="code-line"><span class="cp">#include</span> <span class="cpf"><termios.h></span><span class="cp"></span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">static</span> <span class="kt">int</span> <span class="n">ttyfd</span> <span class="o">=</span> <span class="n">STDIN_FILENO</span><span class="p">;</span></span>
<span class="code-line"><span class="k">static</span> <span class="k">struct</span> <span class="n">termios</span> <span class="n">orig_termios</span><span class="p">;</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="cm">/* reset tty - useful also for restoring the terminal when this process</span></span>
<span class="code-line"><span class="cm"> wishes to temporarily relinquish the tty</span></span>
<span class="code-line"><span class="cm">*/</span></span>
<span class="code-line"><span class="kt">int</span> <span class="nf">tty_reset</span><span class="p">(</span><span class="kt">void</span><span class="p">){</span></span>
<span class="code-line"> <span class="cm">/* flush and reset */</span></span>
<span class="code-line"> <span class="k">if</span> <span class="p">(</span><span class="n">tcsetattr</span><span class="p">(</span><span class="n">ttyfd</span><span class="p">,</span><span class="n">TCSAFLUSH</span><span class="p">,</span><span class="o">&</span><span class="n">orig_termios</span><span class="p">)</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span></span>
<span class="code-line"> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"><span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"><span class="cm">/* put terminal in raw mode - see termio(7I) for modes */</span></span>
<span class="code-line"><span class="kt">void</span> <span class="nf">tty_raw</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span></span>
<span class="code-line"><span class="p">{</span></span>
<span class="code-line"> <span class="k">struct</span> <span class="n">termios</span> <span class="n">raw</span><span class="p">;</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="n">raw</span> <span class="o">=</span> <span class="n">orig_termios</span><span class="p">;</span> <span class="cm">/* copy original and then modify below */</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* input modes - clear indicated ones giving: no break, no CR to NL,</span></span>
<span class="code-line"><span class="cm"> no parity check, no strip char, no start/stop output (sic) control */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_iflag</span> <span class="o">&=</span> <span class="o">~</span><span class="p">(</span><span class="n">BRKINT</span> <span class="o">|</span> <span class="n">ICRNL</span> <span class="o">|</span> <span class="n">INPCK</span> <span class="o">|</span> <span class="n">ISTRIP</span> <span class="o">|</span> <span class="n">IXON</span><span class="p">);</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* output modes - clear giving: no post processing such as NL to CR+NL */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_oflag</span> <span class="o">&=</span> <span class="o">~</span><span class="p">(</span><span class="n">OPOST</span><span class="p">);</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* control modes - set 8 bit chars */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_cflag</span> <span class="o">|=</span> <span class="p">(</span><span class="n">CS8</span><span class="p">);</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* local modes - clear giving: echoing off, canonical off (no erase with</span></span>
<span class="code-line"><span class="cm"> backspace, ^U,...), no extended functions, no signal chars (^Z,^C) */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_lflag</span> <span class="o">&=</span> <span class="o">~</span><span class="p">(</span><span class="n">ECHO</span> <span class="o">|</span> <span class="n">ICANON</span> <span class="o">|</span> <span class="n">IEXTEN</span> <span class="o">|</span> <span class="n">ISIG</span><span class="p">);</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* control chars - set return condition: min number of bytes and timer */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VMIN</span><span class="p">]</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VTIME</span><span class="p">]</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span> <span class="cm">/* after 5 bytes or .8 seconds</span></span>
<span class="code-line"><span class="cm"> after first byte seen */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VMIN</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VTIME</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="cm">/* immediate - anything */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VMIN</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VTIME</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="cm">/* after two bytes, no timer */</span></span>
<span class="code-line"> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VMIN</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">raw</span><span class="p">.</span><span class="n">c_cc</span><span class="p">[</span><span class="n">VTIME</span><span class="p">]</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span> <span class="cm">/* after a byte or .8 seconds */</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="cm">/* put terminal in raw mode after flushing */</span></span>
<span class="code-line"> <span class="n">tcsetattr</span><span class="p">(</span><span class="n">ttyfd</span><span class="p">,</span><span class="n">TCSAFLUSH</span><span class="p">,</span><span class="o">&</span><span class="n">raw</span><span class="p">);</span></span>
<span class="code-line"><span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span> <span class="p">{</span></span>
<span class="code-line"> <span class="n">atexit</span><span class="p">(</span><span class="n">tty_reset</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">tty_raw</span><span class="p">();</span></span>
<span class="code-line"> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fp</span> <span class="o">=</span> <span class="n">fdopen</span><span class="p">(</span><span class="n">ttyfd</span><span class="p">,</span> <span class="s">"r"</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">setvbuf</span><span class="p">(</span><span class="n">fp</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="n">_IONBF</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span></span>
<span class="code-line"> <span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="mi">20</span><span class="p">];</span></span>
<span class="code-line"> <span class="n">buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"> <span class="n">fgets</span><span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="mi">20</span><span class="p">,</span> <span class="n">fp</span><span class="p">);</span></span>
<span class="code-line"> <span class="n">printf</span><span class="p">(</span><span class="s">"buffer is:%s"</span><span class="p">,</span> <span class="n">buffer</span><span class="p">);</span></span>
<span class="code-line"> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
</div>
</div>
<div class="section" id="id4">
<h2><a class="toc-backref" href="#id11">终端上的字符编程</a></h2>
<p>vander(大青蛙) 于 2014年12月12日08:52:20 星期五 问到:</p>
<blockquote>
<p>学习了!</p>
<p>进一步想请教一下fc大神。如果我在Linux上做终端上的字符编程,是否除了用ncurses库
之外,也可以不用该库而直接与终端打交道,就是你所说的直接在raw模式?
另外,终端类型vt100和linux的差别在哪里?为什么Kevin Boone的KBox配置手册里面说必
须把终端类型设成linux,而且要加上terminfo文件,才能让终端上的vim正常工作?term
info文件又是干什么的?</p>
</blockquote>
<div class="section" id="id5">
<h3><a class="toc-backref" href="#id12">Linux控制台的历史</a></h3>
<p>嗯理论上可以不用 ncurses 库直接在 raw 模式操纵终端。</p>
<p>这里稍微聊一下terminfo/termcap的历史,详细的历史和吐槽参考
<a class="reference external" href="http://web.mit.edu/~simsong/www/ugh.pdf">Unix hater's Handbook</a>
第6章 Terminal Insanity。</p>
<p>首先一个真正意义上的终端就是一个输入设备(通常是键盘)加上一个输出设备(打印
机或者显示器)。很显然不同的终端的能力不同,比如如果输出设备是打印机的话,显
示出来的字符就不能删掉了(但是能覆盖),而且输出了一行之后就不能回到那一行了
。再比如显示器终端有的支持粗体和下划线,有的支持颜色,而有的什么都不支持。
早期Unix工作在电传打字机(TeleTYpe)终端上,后来Unix被port到越来越多的机器上
,然后越来越多类型的终端会被连到Unix上,很可能同一台Unix主机连了多个不同类型
的终端。由于是不同厂商提供的不同的终端,能力各有不同,自然控制他们工作的方式
也是不一样的。所有终端都支持回显行编辑模式,所以一般的面向行的程序还比较好写
,但是那时候要撰写支持所有终端的「全屏」程序就非常痛苦,这种情况就像现在浏览
器没有统一标准下写HTML要测试各种浏览器兼容性一样。
通常的做法是</p>
<ol class="arabic simple">
<li>使用最小功能子集</li>
<li>假设终端是某个特殊设备,不管别的设备。</li>
</ol>
<p>水源的代码源头 Firebird2000 就是那样的一个程序,只支持固定大小的vt102终端。</p>
<p>这时有一个划时代意义的程序出现了,就是 vi,试图要做到「全屏可视化编辑」。这在
现在看起来很简单,但是在当时基本是天方夜谭。
vi 的做法是提出一层抽象,记录它所需要的所有终端操作,然后有一个终端类型数据库
,把那些操作映射到终端类型的具体指令上。当然并不是所有操作在所有终端类型上都
支持,所以会有一堆 fallback,比如要「强调」某段文字,在彩色终端上可能
fallback 到红色,在黑白终端上可能 fallback 到粗体。</p>
<p>vi 一出现大家都觉得好顶赞,然后想要写更多类似 vi 这样的全屏程序。然后 vi 的作
者就把终端抽象的这部分数据库放出来形成一个单独的项目,叫 termcap (Terminal
Capibility),对应的描述终端的数据库就是 termcap 格式。然后 termcap 只是一个
数据库(所以无状态)还不够方便易用,所以后来又有人用 termcap 实现了 curses 。</p>
<p>再后来大家用 curses/termcap 的时候渐渐发现这个数据库有一点不足:它是为 vi 设
计的,所以只实现了 vi 需要的那部分终端能力。然后对它改进的努力就形成了新的
terminfo 数据库和 pcurses 和后来的 ncurses 。 然后 VIM 出现了自然也用
terminfo 实现这部分终端操作。</p>
<p>然后么就是 X 出现了, xterm 出现了,大家都用显示器了,然后 xterm 为了兼容各种
老程序加入了各种老终端的模拟模式。不过因为最普及的终端是 vt100 所以 xterm 默
认是工作在兼容 vt100 的模式下。然后接下来各种新程序(偷懒不用*curses的那些)
都以 xterm/vt100 的方式写。</p>
<p>嗯到此为止是 Unix 世界的黑历史。</p>
<p>知道这段历史的话就可以明白为什么需要 TERM 变量配合 terminfo 数据库才能用一些
Unix 下的全屏程序了。类比一下的话这就是现代浏览器的 user-agent。</p>
<p>然后话题回到 Linux 。 大家知道 Linux 早期代码不是一个 OS, 而是 Linus 大神想
在他的崭新蹭亮的 386-PC 上远程登录他学校的 Unix 主机,接收邮件和逛水源(咳咳
)。于是 Linux 最早的那部分代码并不是一个通用 OS 而只是一个 bootloader 加一个
终端模拟器。所以现在 Linux 内核里还留有他当年实现的终端模拟器的部分代码,而这
个终端模拟器的终端类型就是 linux 啦。然后他当时是为了逛水源嘛所以 linux 终端
基本上是 vt102 的一个接近完整子集。</p>
<p>说到这里脉络大概应该清晰了, xterm终端类型基本模拟 vt100,linux终端类型基本模
拟 vt102。这两个的区别其实很细微,都是同一个厂商的两代产品嘛。有差别的地方差
不多就是 <kbd class="kbd">
Home</kbd>
/ <kbd class="kbd">
End</kbd>
/ <kbd class="kbd">
PageUp</kbd>
/ <kbd class="kbd">
PageDown</kbd>
/ <kbd class="kbd">
Delete</kbd>
这些不在 ASCII 控制字符表里的按键的映射关系不同。</p>
<p>嗯这也就解释了为什么在linux环境的图形界面的终端里 telnet 上水源的话,上面这些
按键会错乱…… 如果设置终端类型是 linux/vt102 的话就不会乱了。在 linux 的
TTY 里 telnet 也不会乱的样子。</p>
<p>写到这里才发现貌似有点长…… 总之可以参考
<a class="reference external" href="http://web.mit.edu/~simsong/www/ugh.pdf">Unix hater's Handbook</a>
里的相关历史评论和吐槽,那一段非常有意思。</p>
</div>
</div>
KDE5 Plasma 之跳动卖萌的活动按钮2014-12-09T01:54:00+09:002014-12-09T01:54:00+09:00farseerfctag:farseerfc.me,2014-12-09:/zhs/jumping-kde5-plasma-activities-button.html<!-- PELICAN_BEGIN_SUMMARY -->
<p>今天尝试 KDE5 Plasma 的活动的时候无意间发现这个现象。
只要把活动按钮拖出桌面,它就会在桌面边缘来回跳动。
视频如下:</p>
<!-- PELICAN_END_SUMMARY -->
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_SSbf97jGSpI">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/SSbf97jGSpI"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XODQ0NjM2MzQ4">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XODQ0NjM2MzQ4" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_SSbf97jGSpI">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XODQ0NjM2MzQ4">Youku</a></li>
</ul>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>当然你可以把它再拖回来,所以这个问题还无伤大雅,只是卖萌。</p>
<p>比比之前 Gnome3 那个跳动的界面真是好太多了:</p>
<!-- PELICAN_END_SUMMARY -->
<div class="well" style="padding: 0">
<div class="tab-content" id="youtubeku">
<div class="tab-pane fade active in" id="youtube_TRQJdRHYwrw">
<div align="left" class="youtube embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" src="https://www.youtube.com/embed/TRQJdRHYwrw"></iframe> </div>
</div>
<div class="tab-pane fade" id="youku_XNjc4MjQ5NjE2">
<div align="left" class="youku embed-responsive embed-responsive-16by9"> <iframe allow="fullscreen" class="embed-responsive-item" frameborder="0" height="498" src="https://player.youku.com/embed/XNjc4MjQ5NjE2" width="510"></iframe> </div>
</div>
</div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#youtube_TRQJdRHYwrw">Youtube</a></li>
<li><a data-toggle="tab" href="#youku_XNjc4MjQ5NjE2">Youku</a></li>
</ul>
</div>
<!-- PELICAN_BEGIN_SUMMARY -->
<p>顺便,今天还看到一个卖萌的 KDE5 Plasma 静音图标的翻译:</p>
<!-- PELICAN_END_SUMMARY -->
<blockquote class="twitter-tweet" lang="zh-tw"><p>KDE5のミュート画面の中国语翻訳、「静音」のはずだが「镜音」になっている。Vocaloidファンのネタだか、単なる入力ミスだか分からない。 <a href="http://t.co/ipyHjXMscR">pic.twitter.com/ipyHjXMscR</a></p>— Jiachen YANG (@farseerfc) <a href="https://twitter.com/farseerfc/status/541944351270518784">2014 12月 8日</a></blockquote>嫁给我好么2013-02-20T20:42:00+09:002013-02-20T20:42:00+09:00farseerfctag:farseerfc.me,2013-02-20:/zhs/marry-me.html<div class="panel panel-default">
<div class="panel-heading">
渲染的样子</div>
<div class="panel-body">
<blockquote>
<!-- PELICAN_BEGIN_SUMMARY -->
<img alt="嫁给我好么" class="img-responsive" src="/images/marry-me.png"/>
<!-- PELICAN_END_SUMMARY -->
</blockquote>
</div>
</div>
<p>可以玩的是下面这个:</p>
<script src="/static/three.min.js" type="text/javascript"></script>
<script src="/static/FirstPersonControls.js" type="text/javascript"></script>
<script src="/static/helvetiker_regular.typeface.js" type="text/javascript"></script>
<script src="/static/214game.js" type="text/javascript"></script>
<div id="game_area" style="width: 600px; height: 450px; margin-left: 10px;clear:both">
</div>
<p style="margin-left: 100px; margin-top: 10px; ">* 用 WASD←→ 移动,需要 WebGL 支持</p>ICSE 20122012-06-06T10:42:00+09:002012-06-06T10:42:00+09:00farseerfctag:farseerfc.me,2012-06-06:/zhs/icse2012.html
<div class="section" id="june-6">
<h2><a class="toc-backref" href="#id1">June 6</a></h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<div class="section" id="keynote-1">
<h3><a class="toc-backref" href="#id2">Keynote 1</a></h3>
<p>没怎么听懂,只记得讲到了finance is not money但是没听懂这个和软件有什么关系。</p>
</div>
<div class="section" id="cost-estimation-for-distributed-software-project">
<h3><a class="toc-backref" href="#id3">Cost Estimation for Distributed Software Project</a></h3>
<p>讲到他们试图改善现有的模型去更精确地评估软件开发的开销。</p>
<p>他们会给PM建议之前的项目的历史数据,然后对于新项目,他们建议历史上已有
的项目的数据,从而帮助PM得到更精确的评估。他们试图尽量减少项目评估对PM
的经验的需求,从而帮助即使经验很少的PM也能准确评估项目的开销。</p>
<!-- PELICAN_END_SUMMARY -->
<p>他们的观点:</p>
<blockquote>
<p>Context-specfic solutions needed!</p>
<p>我们需要更上下文相关的解决方案!</p>
<p>Early user paticipation is key!</p>
<p>早期用户的参与是关键</p>
</blockquote>
</div>
<div class="section" id="characterizing-logging-practices-in-open-source-software">
<h3><a class="toc-backref" href="#id4">Characterizing Logging Practices in Open-Source Software</a></h3>
<p>Common mistakes in logging messages</p>
<p>在日志记录中容易犯的错误</p>
<p>他们学习了历史上的log记录,然后试图找到重复修改的输出log的语句,确定log …</p></div></div>
<div class="section" id="june-6">
<h2><a class="toc-backref" href="#id1">June 6</a></h2>
<!-- PELICAN_BEGIN_SUMMARY -->
<div class="section" id="keynote-1">
<h3><a class="toc-backref" href="#id2">Keynote 1</a></h3>
<p>没怎么听懂,只记得讲到了finance is not money但是没听懂这个和软件有什么关系。</p>
</div>
<div class="section" id="cost-estimation-for-distributed-software-project">
<h3><a class="toc-backref" href="#id3">Cost Estimation for Distributed Software Project</a></h3>
<p>讲到他们试图改善现有的模型去更精确地评估软件开发的开销。</p>
<p>他们会给PM建议之前的项目的历史数据,然后对于新项目,他们建议历史上已有
的项目的数据,从而帮助PM得到更精确的评估。他们试图尽量减少项目评估对PM
的经验的需求,从而帮助即使经验很少的PM也能准确评估项目的开销。</p>
<!-- PELICAN_END_SUMMARY -->
<p>他们的观点:</p>
<blockquote>
<p>Context-specfic solutions needed!</p>
<p>我们需要更上下文相关的解决方案!</p>
<p>Early user paticipation is key!</p>
<p>早期用户的参与是关键</p>
</blockquote>
</div>
<div class="section" id="characterizing-logging-practices-in-open-source-software">
<h3><a class="toc-backref" href="#id4">Characterizing Logging Practices in Open-Source Software</a></h3>
<p>Common mistakes in logging messages</p>
<p>在日志记录中容易犯的错误</p>
<p>他们学习了历史上的log记录,然后试图找到重复修改的输出log的语句,确定log
中存在的问题。他们首先确定修改是事后修改。</p>
<p>通常的修改的比例(9027个修改)</p>
<table border="0" class="table docutils borderless">
<colgroup>
<col width="10%"/>
<col width="90%"/>
</colgroup>
<tbody valign="top">
<tr><td>45%</td>
<td>静态文本</td>
</tr>
<tr><td>27%</td>
<td>打印出的变量</td>
</tr>
<tr><td>26%</td>
<td>调试等级verbosity</td>
</tr>
<tr><td>2%</td>
<td>日志输出的位置</td>
</tr>
</tbody>
</table>
<p>他们发现有调试等级的变化,是因为安全漏洞之类的原因,或者在开销和数据
之间的权衡。</p>
<p>大多数对log的变量的修改都是为了增加一个参数。他们之前的LogEnhancer是为了
解决这个问题而提出的,通过静态检查,提醒程序员是否忘记了某个参数</p>
<p>对text的修改是因为要改掉过时的代码信息,避免误导用户。</p>
<p>他们的实验是采用了基于code clone 的技术,找到所有log语句,然后找不一致
的clone,然后自动提出建议。</p>
</div>
<div class="section" id="combine-functional-and-imperative-pgrm-for-multicore-sw-scala-java">
<h3><a class="toc-backref" href="#id5">Combine Functional and Imperative Pgrm for Multicore Sw: Scala & Java</a></h3>
<p>趋势:到处都是多核,但是并发程序呢?</p>
<p>他们研究的对象是Scala和Java,因为可以编译后确认JVM字节码的语义。</p>
<ul class="simple">
<li><dl class="first docutils">
<dt>Java:</dt>
<dd><ul class="first last">
<li>共享内存</li>
<li>显示创建的线程</li>
<li>手动同步</li>
<li>Wait/Notify机制</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>Scala:</dt>
<dd><ul class="first last">
<li>高阶函数</li>
<li>Actors, 消息传递</li>
<li>lists, filters, iterators</li>
<li>while</li>
<li>共享状态, OO</li>
<li>import java.* 能从java导入任何库</li>
<li>auto type inferance 自动类型推导</li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>实验的参与者都经过4周的训练,实验项目是工业等级的开发项目</p>
<p>结果:</p>
<p>scala 的项目平均比java多花38%的时间,主要都是花在Test和debug上的时间。</p>
<p>程序员的经验和总体时间相关,但是对test和debug没有显著影响。</p>
<p>scala的为了让编程更有效率的设计,导致debug更困难。比如类型推导,debug
的时候需要手动推导,来理解正在发生什么。</p>
<p>scala的程序比java小,中位数2.6%,平均15.2%</p>
<ul class="simple">
<li><dl class="first docutils">
<dt>性能比较:</dt>
<dd><ul class="first last">
<li>单核:scala的线性程序的性能比java好</li>
<li><dl class="first docutils">
<dt>4核:</dt>
<dd><ul class="first last">
<li>scala 7s @ 4 threads</li>
<li>java 4si @ 8 threads</li>
<li><dl class="first docutils">
<dt>median</dt>
<dd><ul class="first last">
<li>83s scala</li>
<li>98s java</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
<li>32core: best scala 34s @ 64 threads</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>结论</dt>
<dd><ul class="first last">
<li>java有更好的scalability</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>scala类型推导</dt>
<dd><ul class="first last">
<li>45%说对携带码有帮助</li>
<li>85%说导致程序错误</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>调试</dt>
<dd><ul class="first last">
<li>23%认为scala简单</li>
<li>77%认为java简单</li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>multi-paradigram are better</p>
</div>
<div class="section" id="sound-empirical-evidence-in-software-testing">
<h3><a class="toc-backref" href="#id6">Sound Empirical Evidence in Software Testing</a></h3>
<p>Test data generation 测试数据自动生成</p>
<p>Large Empirical Studies - not always possible</p>
<p>For open source software - big enough</p>
</div>
<div class="section" id="identifing-linux-bug-fixing-patch">
<h3><a class="toc-backref" href="#id7">Identifing Linux Bug Fixing Patch</a></h3>
<ul class="simple">
<li><dl class="first docutils">
<dt>current practice:</dt>
<dd><ul class="first last">
<li>manual</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>Current research:</dt>
<dd><ul class="first last">
<li>keywords in commits</li>
<li>link bug reports in bugzilla</li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>Try to solve classification problem</p>
<ul class="simple">
<li><dl class="first docutils">
<dt>issue</dt>
<dd><ul class="first last">
<li>pre-identified</li>
<li>post-identified</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>data</dt>
<dd><ul class="first last">
<li>from commit log</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>feature extraction</dt>
<dd><ul class="first last">
<li>text pre-process stemmed non-stop words</li>
</ul>
</dd>
</dl>
</li>
<li>model learning</li>
</ul>
<p>research questions</p>
</div>
<div class="section" id="active-refinement-of-clone-anomaly-reports">
<h3><a class="toc-backref" href="#id8">Active Refinement of Clone Anomaly Reports</a></h3>
<p>motivating</p>
<ul class="simple">
<li>code clones, clone groups</li>
<li>clone used to detect bugs</li>
<li>anomaly : inconsistent clone group
many anomaly clone are note bug, high false positive</li>
</ul>
<dl class="docutils">
<dt>approach</dt>
<dd><ul class="first last simple">
<li>reorder by sorted bug reports</li>
</ul>
</dd>
</dl>
</div>
</div>
<hr class="docutils"/>
<div class="section" id="june7">
<h2><a class="toc-backref" href="#id9">June7</a></h2>
<div class="section" id="keynotes-2-sustainability-with-software-an-industrial-perspective">
<h3><a class="toc-backref" href="#id10">Keynotes 2: Sustainability with Software - An Industrial Perspective</a></h3>
<p>Sustainability</p>
<ul class="simple">
<li><dl class="first docutils">
<dt>Classic View: Idenpendent view with overlap</dt>
<dd><ul class="first last">
<li>Social</li>
<li>Environment</li>
<li>Economic</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>Nested viw</dt>
<dd><ul class="first last">
<li><dl class="first docutils">
<dt>Environment</dt>
<dd><ul class="first last">
<li><dl class="first docutils">
<dt>Social</dt>
<dd><ul class="first last">
<li>Economic</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
</ul>
<dl class="docutils">
<dt>Triple bottom line</dt>
<dd><ul class="first last simple">
<li><dl class="first docutils">
<dt>economic</dt>
<dd>-global business, networks , global econ</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>env</dt>
<dd><ul class="first last">
<li>natural res, climate change, population grow</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>social</dt>
<dd><ul class="first last">
<li>awareness, connectivity, accountability</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
<div class="section" id="green-it">
<h4><a class="toc-backref" href="#id11">Green IT</a></h4>
<ul class="simple">
<li><dl class="first docutils">
<dt>reduce IT energy</dt>
<dd><ul class="first last">
<li>more than 50% cooling - doing nothing</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>mini e-waste: not properly recycled</dt>
<dd><ul class="first last">
<li>80% in EU</li>
<li>75% in US</li>
</ul>
</dd>
</dl>
</li>
<li>foster dematerialization</li>
</ul>
<p>In-Memory Technology: Expected Sustainable Benefits</p>
</div>
<div class="section" id="what-can-we-do">
<h4><a class="toc-backref" href="#id12">What can we do?</a></h4>
<blockquote>
<ul class="simple">
<li>consider all software lifecycle phases in your design</li>
<li>avoid energy expensive behavior in your codes</li>
<li>design lean architectures</li>
</ul>
</blockquote>
</div>
<div class="section" id="green-by-it">
<h4><a class="toc-backref" href="#id13">Green by IT</a></h4>
<blockquote>
<ul class="simple">
<li>2% green IT</li>
<li>98% green IT</li>
</ul>
</blockquote>
</div>
</div>
<div class="section" id="on-how-often-code-is-cloned-across-repositories">
<h3><a class="toc-backref" href="#id14">On How Often code is cloned across repositories</a></h3>
<p>Line based hashing code clone detection</p>
<p>never do anything harder than sorting</p>
<p>hashing a window of 5 lines of normalized (tokenized) code, dropping
3/4 of the hashing</p>
<p>把ccfinder一个月的工作缩短到了3, 4天。没有比较presion和recall。</p>
<table border="0" class="table docutils borderless">
<colgroup>
<col width="11%"/>
<col width="89%"/>
</colgroup>
<tbody valign="top">
<tr><td>14%</td>
<td>type1</td>
</tr>
<tr><td>16%</td>
<td>type2</td>
</tr>
<tr><td>17%</td>
<td>type3 (not really type2)</td>
</tr>
</tbody>
</table>
</div>
<div class="section" id="graph-based-analysis-and-prediction-for-sw-evolution">
<h3><a class="toc-backref" href="#id15">Graph-based analysis and prediction for sw evolution</a></h3>
<div class="section" id="graph-are-everywhere">
<h4><a class="toc-backref" href="#id16">graph are everywhere</a></h4>
<ul class="simple">
<li>internet topology</li>
<li>social net</li>
<li>chemistry</li>
<li>biology</li>
</ul>
<p>in sw
- func call graph
- module dependency graph</p>
<p>developer interaction graph
- commit logs
- bug reports</p>
<p>experiment 11 oss, 27~171 release, > 9 years</p>
</div>
<div class="section" id="predictors">
<h4><a class="toc-backref" href="#id17">predictors</a></h4>
<ul class="simple">
<li><dl class="first docutils">
<dt>NodeRank</dt>
<dd><ul class="first last">
<li>similar to pagerank of google</li>
<li>measure relative importance of each node</li>
<li><dl class="first docutils">
<dt>func call graph with noderank</dt>
<dd><ul class="first last">
<li>compare rank with severity scale on bugzilla</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>correlation between noderank and BugSeverity</dt>
<dd><ul class="first last">
<li>func level 0.48 ~ 0.86 varies among projects.</li>
<li>model level > func level</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>ModularityRatio</dt>
<dd><ul class="first last">
<li>cohesion/coupling ratio: IntraDep(M)/InterDep(M)</li>
<li>forecast mantencance effort</li>
<li><dl class="first docutils">
<dt>use for</dt>
<dd><ul class="first last">
<li>identify modules that need redesign or refactoring</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>EditDistance</dt>
<dd><ul class="first last">
<li>bug-based developer collaboration graphs</li>
<li>ED(G1,G2)=|V1|+|V2|-2|V1交V2|+|E1|+|E2|-2|E1交E2|</li>
<li><dl class="first docutils">
<dt>use for</dt>
<dd><ul class="first last">
<li>release planning</li>
<li>resource allocation</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>graph metrics</p>
<ul class="simple">
<li><dl class="first docutils">
<dt>graph diameter</dt>
<dd><ul class="first last">
<li>average node degree indicates reuse</li>
</ul>
</dd>
</dl>
</li>
<li>clustering coefficient</li>
<li>assortativity</li>
<li>num of cycles</li>
</ul>
</div>
<div class="section" id="conclusion">
<h4><a class="toc-backref" href="#id18">Conclusion</a></h4>
<p>"Actionable intelligence" from graph evolution</p>
<ul class="simple">
<li>studie 11 large long-live projs</li>
<li>predictors</li>
<li>identify pivotal moments in evolution</li>
</ul>
</div>
</div>
<div class="section" id="what-make-long-term-contributors-willingness-and-opportunity-in-oss">
<h3><a class="toc-backref" href="#id19">What make long term contributors: willingness and opportunity in OSS</a></h3>
<p>OSS don't work without contributors form community</p>
<p>mozilla (2000-2008)</p>
<p>10^2.2 LTC <- 2 order -> 10^4.2 new contributors <- 3.5 order -> 10^7.7 users</p>
<p>gnome (1999-2007)</p>
<p>10^2.5 LTC <- 1.5 order -> 10^4.0 new contributors <- 3.5 order -> 10^6.5 users</p>
<div class="section" id="approach">
<h4><a class="toc-backref" href="#id20">approach</a></h4>
<ul class="simple">
<li>read issues of 20 LTC and 20 non-LTC</li>
<li>suvery 56 (36 non-LTC and 20 LTC)</li>
<li>extract practices published on project web sites</li>
</ul>
</div>
<div class="section" id="summeray">
<h4><a class="toc-backref" href="#id21">summeray</a></h4>
<ul class="simple">
<li>Ability/Willingness distinguishes LTCs</li>
<li><dl class="first docutils">
<dt>Environment</dt>
<dd><ul class="first last">
<li><dl class="first docutils">
<dt>macro-climate</dt>
<dd><ul class="first last">
<li>popularity</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>micro-climate</dt>
<dd><ul class="first last">
<li>attention</li>
<li>bumber of peers</li>
<li>performance of peers</li>
</ul>
</dd>
</dl>
</li>
</ul>
</dd>
</dl>
</li>
</ul>
<p>regression model</p>
<p>newcomers to LTC conversion drops</p>
<dl class="docutils">
<dt>actions in first month predicts LTCs</dt>
<dd><ul class="first last simple">
<li>24% recall</li>
<li>37% precision</li>
</ul>
</dd>
</dl>
</div>
</div>
<div class="section" id="develop-of-auxiliary-functions-should-you-be-agile">
<h3><a class="toc-backref" href="#id22">develop of auxiliary functions: should you be agile?</a></h3>
<p>a empirial assessment of pair programming and test-first programming</p>
<p>can agile help auxiliary functions?</p>
<div class="section" id="experiment">
<h4><a class="toc-backref" href="#id23">experiment</a></h4>
<ul class="simple">
<li>pair vs solo</li>
<li>test-first vs test-last</li>
<li>students vs professors</li>
</ul>
</div>
<div class="section" id="research-questions">
<h4><a class="toc-backref" href="#id24">research questions</a></h4>
<ul class="simple">
<li>r1: can pair help obtain more correct impl</li>
<li>r2: can test-first</li>
<li>r3: dst test1 encourage the impl or more test cases?</li>
<li>r4: does test1 course more coverage</li>
</ul>
</div>
<div class="section" id="result">
<h4><a class="toc-backref" href="#id25">result</a></h4>
<ul class="simple">
<li><dl class="first docutils">
<dt>test-first</dt>
<dd><ul class="first last">
<li>higher coverage</li>
<li>non change with correctness</li>
</ul>
</dd>
</dl>
</li>
<li><dl class="first docutils">
<dt>pair</dt>
<dd><ul class="first last">
<li>improve on correctness</li>
<li>longer total programming time</li>
</ul>
</dd>
</dl>
</li>
</ul>
</div>
</div>
<div class="section" id="static-detection-of-resource-contention-problems-in-server-side-script">
<h3><a class="toc-backref" href="#id26">Static Detection of Resource Contention Problems in Server-side script</a></h3>
<p>Addressed the race condition of accessing database or filesystem of PHP</p>
</div>
<div class="section" id="amplifying-tests-to-validate-exception-handling-code">
<h3><a class="toc-backref" href="#id27">Amplifying Tests to Validate Exception Handling Code</a></h3>
<p>异常处理的代码不但难写,而且难以验证。各种组合情况难以估计,尤其是手机
系统上。</p>
</div>
<div class="section" id="a-tactic-centric-approach-automating-traceability-of-quality-concerns">
<h3><a class="toc-backref" href="#id28">A tactic-centric approach automating traceability of quality concerns</a></h3>
<p>tactic traceability information models</p>
</div>
</div>
MSR 2012 @ ICSE2012-06-02T10:42:00+09:002012-06-02T10:42:00+09:00farseerfctag:farseerfc.me,2012-06-02:/zhs/msr2012.html
<div class="section" id="mining-software-repository-2012-icse">
<h2><a class="toc-backref" href="#id3">Mining Software Repository 2012 @ ICSE</a></h2>
<p>参加了今年的MSR,会场在University of Zurich。一大早来到大学,注册有点
小插曲,显然瑞士人搞不清楚中国人的名字,3个杨(Yang)姓的中国人的名牌
被搞错了。然后堀田学长的所属被写作了“Japan, Japan”,成为了全日本的代表。</p>
<div class="section" id="msr-microsoft-research-talk-msr-mining-software-repositories">
<h3><a class="toc-backref" href="#id4">MSR(MicroSoft Research) talk @ MSR(Mining Software Repositories)</a></h3>
<p>首先是来自微软亚洲研究院(MicroSoft Research @ Asia, MSR Asia)的Keynots,
于是就变成了MSR在MSR的演讲。MSR的张冬梅(Dongmei Zhang)女士的演讲
分为关于Software Analysis和XIAO的两部分。XIAO是MSRA开发的Code Clone
Detector,似乎我要给井上研做的就是这个。想更多了解Xiao的细节,不过张女士
演讲结束的时候的鼓掌导致了话筒的小故障 …</p></div></div>
<div class="section" id="mining-software-repository-2012-icse">
<h2><a class="toc-backref" href="#id3">Mining Software Repository 2012 @ ICSE</a></h2>
<p>参加了今年的MSR,会场在University of Zurich。一大早来到大学,注册有点
小插曲,显然瑞士人搞不清楚中国人的名字,3个杨(Yang)姓的中国人的名牌
被搞错了。然后堀田学长的所属被写作了“Japan, Japan”,成为了全日本的代表。</p>
<div class="section" id="msr-microsoft-research-talk-msr-mining-software-repositories">
<h3><a class="toc-backref" href="#id4">MSR(MicroSoft Research) talk @ MSR(Mining Software Repositories)</a></h3>
<p>首先是来自微软亚洲研究院(MicroSoft Research @ Asia, MSR Asia)的Keynots,
于是就变成了MSR在MSR的演讲。MSR的张冬梅(Dongmei Zhang)女士的演讲
分为关于Software Analysis和XIAO的两部分。XIAO是MSRA开发的Code Clone
Detector,似乎我要给井上研做的就是这个。想更多了解Xiao的细节,不过张女士
演讲结束的时候的鼓掌导致了话筒的小故障。</p>
</div>
<div class="section" id="towards-improving-bts-with-game-mechanisms">
<h3><a class="toc-backref" href="#id5">Towards Improving BTS with Game Mechanisms</a></h3>
<p>感觉这篇的内容基本上就是关于</p>
<p><a class="reference external" href="http://www.joelonsoftware.com/items/2008/09/15.html">http://www.joelonsoftware.com/items/2008/09/15.html</a></p>
<p>这里写到的东西,然后说同样的理论是否可以用于Issue Tracking之类的事情上。
个人感觉这个意义不大,stackoverflow之所以成功是因为它把开源社区本身就
具有的名誉体系具现化了,本着大家都喜欢被别人奉为大牛的心态,就如同
wikipedia一样。同样的理论如果用于公司内部的Issue Tracking系统上,会得到
完全不同的东西吧。就像MSDN的组织方式虽然和wikipedia是一样的,但是在MSDN
里找信息的感觉和在wikipedia完全不一样。个人不太看好这个方向。</p>
</div>
<div class="section" id="ghtorrent">
<h3><a class="toc-backref" href="#id6">GHTorrent</a></h3>
<p>这篇的slide在这里可以看到:<a class="reference external" href="http://www.slideshare.net/gousiosg/ghtorrent-githubs-data-from-a-firehose-13184524">http://www.slideshare.net/gousiosg/ghtorrent-githubs-data-from-a-firehose-13184524</a></p>
<p>Data exporter for github. Github的主要数据,代码,已经可以通过git接口
获得了,wiki是git的形式保存的。所以这个项目的目的就是暴露别的数据,主要
是issue tracking,code comments,这种。代码访问github api,然后用分布式
实现以克服api的限制,然后提供torrents形式的history下载。github api获得
的json数据以bson的形式保存在MongoDB里,解析过的有了Schema之后的数据保存
在MySQL里并可以导出SQL。</p>
<p>个人的想法,觉得数据如果能够更统一,全部存在Git里或许更好,像Wiki一样。
同样是要暴露全部历史记录的目的,用Torrent自己实现的历史远不如用Git的
接口实现的历史记录方便吧,git blame之类的也更方便追踪code comment之类的
作者信息。当然对git的raw date直接读写,需要对git的内部原理有足够的理解,
或许只有github的人有这种能力了。</p>
</div>
<div class="section" id="topic-mining">
<h3><a class="toc-backref" href="#id7">Topic Mining</a></h3>
<p>用得两个参数, DE 和 AIC,完全不能理解,过后研究。实验针对了Firefox,
Mylyn, Eclipse三个软件。试图从Repo中分析源代码的identifier和comments,
找到topic和bug之间的关系,比如怎样的topic更容易导致bug。得出的结论似乎
也很暧昧,只是说核心功能被报告的bug更多,但是不知道原因。这只能表示核心
功能受到更多关注和更多测试吧,并不能说明核心功能就容易产生bug。</p>
<p>不过这个的Slide做得很漂亮,很容易理解。</p>
</div>
<div class="section" id="secold">
<h3><a class="toc-backref" href="#id8">SeCold</a></h3>
<p>A linked data platform for mining software repositories</p>
<p>没听懂这个项目的目的。</p>
</div>
<div class="section" id="the-evolution-of-software">
<h3><a class="toc-backref" href="#id9">The evolution of software</a></h3>
<p>第二天的Keynotes,关于将Social Media和Software Development相结合的想法。
或许就是Github赖以成功的基础。讲到代码中的comment, Tags, uBlog, blog之类
的social的特性和IDE的融合的趋势。</p>
</div>
<div class="section" id="do-faster-releases-imporve-software-quality">
<h3><a class="toc-backref" href="#id10">Do Faster Releases Imporve Software Quality?</a></h3>
<p>使用Firefox作为例子。</p>
<p>结论是快速发布导致bug更多,更容易crash,但是bug更快得到修复,并且用户
更快转向新的发布。</p>
</div>
<div class="section" id="security-vs-performance-bugs-in-firefox">
<h3><a class="toc-backref" href="#id11">Security vs Performance Bugs in Firefox</a></h3>
<p>Performance bugs are regression, blocks release.</p>
</div>
<hr class="docutils"/>
<div class="section" id="id1">
<h3><a class="toc-backref" href="#id12">一些感想</a></h3>
<div class="section" id="commit">
<h4><a class="toc-backref" href="#id13">基于自然语义分析的commit分割</a></h4>
<p>经常工具(比如git)的使用者并没有按照工具设计者的意图使用工具,这给MSR
带来很多困难。举个例子,git有非常完美的branch系统,通常期望git的使用者
能够在一次commit里commit一个功能,比如一个bug的修复,或者一个feature的
添加,但是事实上经常有很多逻辑上的commit被合并在一个里面了。</p>
<p>或许这不是使用者的错,而是工具仍然不够人性的表现。或许我们可以自动把
一次的commit按照语义分割成多个。</p>
<p>分割之后,可以更容易地把issue和commit关联,也更容易组织更多的研究。</p>
</div>
<div class="section" id="slides">
<h4><a class="toc-backref" href="#id14">关于这次发表中大家用的slides系统</a></h4>
<p>题目为``Incorporating Version Histories in Information Retrieval Based
Bug Localization''的人用的slide是beamer的。公式很多,overlay很多,列表
很多,图片很少,典型的beamer做出的slide。思维导图用得很不错。今天一天
有至少3个slide是用beamer做的。</p>
<p>题目为``Towards Improving Bug Tracking Systems with Game Mechanisms''
的人用了prezi,图片很多,过度很多。但是比如没有页号没有页眉页脚,正式
会议的场合不太方便。</p>
<p>至少有六个以上用了Apple Keynotes,Keynotes做出来的东西真的和Powerpoint
做出来的很难区别,其中两个人用了初始的主题所以才看出来。</p>
<p>剩下的自然是PPT。MSRA的张女士做的虽然是PPT,倒是有很多beamer的感觉,
比如页眉页脚和overlay的用法。这些如果都是PPT做出来的,会多很多额外的
人力吧。</p>
<p>值得一提的是有一个题目为``Green Mining: A Methodology of Relating
Software Change to Power Consumption''的人的slide全是``劣质''的手绘漫画,
效果意外地好,很低碳很环保很绿色很可爱。具体效果可以参考下面的动画,虽然
现场看到的不是一个版本:</p>
<p><a class="reference external" href="http://softwareprocess.es/a/greenmining-presentatation-at-queens-20120522.ogv">http://softwareprocess.es/a/greenmining-presentatation-at-queens-20120522.ogv</a></p>
</div>
<div class="section" id="id2">
<h4><a class="toc-backref" href="#id15">微软是个腹黑娘!</a></h4>
<p>嘛虽然这也不是什么新闻了。MSR2012的Mining Challenge的赞助商是微软,管理
组织者来自微软研究院,奖品是Xbox和Kinect。然后今年的题目是:</p>
<pre class="literal-block">
Mining Android Bug
</pre>
<p>我看到了微软满满的怨气……</p>
</div>
</div>
</div>
Pyssy 项目2012-04-02T12:42:00+09:002012-04-02T12:42:00+09:00farseerfctag:farseerfc.me,2012-04-02:/zhs/pyssy.html<div class="section" id="id1">
<h2>简介</h2>
<p>Pyssy 是用于 <a class="reference external" href="https://bbs.sjtu.edu.cn">上海交通大学 饮水思源站</a> 的一系列 Python 脚本和工具。</p>
<p>Pyssy 被有意设计为既可以托管寄宿在 SAE <a class="footnote-reference" href="#sae" id="id3">[1]</a> 上,也可以在单机上独立使用。</p>
<p>项目地址: <a class="reference external" href="http://pyssy.sinaapp.com/">http://pyssy.sinaapp.com/</a></p>
<p>Github上的源代码地址: <a class="reference external" href="https://github.com/yssy-d3/pyssy">https://github.com/yssy-d3/pyssy</a></p>
<table border="0" class="docutils table footnote" frame="void" id="sae" rules="none">
<colgroup><col class="label"/><col/></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id3">[1]</a></td><td><a class="reference external" href="http://sae.sina.com.cn/">Sina App Engine</a> ,新浪云平台,类似 <a class="reference external" href="https://appengine.google.com/">Google App Engine</a> 的东西。</td></tr>
</tbody>
</table>
</div>
<div class="section" id="id4">
<h2>依赖关系</h2>
<p>Pyssy 使用 <a class="reference external" href="http://flask.pocoo.org/">Flask</a> 作为网页服务器,
并且使用 Memcached 或者 Redis 作为抓取 <em>水源Web</em> 的缓存。</p>
<p>SAE Python 环境下请开启 …</p></div><div class="section" id="id1">
<h2>简介</h2>
<p>Pyssy 是用于 <a class="reference external" href="https://bbs.sjtu.edu.cn">上海交通大学 饮水思源站</a> 的一系列 Python 脚本和工具。</p>
<p>Pyssy 被有意设计为既可以托管寄宿在 SAE <a class="footnote-reference" href="#sae" id="id3">[1]</a> 上,也可以在单机上独立使用。</p>
<p>项目地址: <a class="reference external" href="http://pyssy.sinaapp.com/">http://pyssy.sinaapp.com/</a></p>
<p>Github上的源代码地址: <a class="reference external" href="https://github.com/yssy-d3/pyssy">https://github.com/yssy-d3/pyssy</a></p>
<table border="0" class="docutils table footnote" frame="void" id="sae" rules="none">
<colgroup><col class="label"/><col/></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id3">[1]</a></td><td><a class="reference external" href="http://sae.sina.com.cn/">Sina App Engine</a> ,新浪云平台,类似 <a class="reference external" href="https://appengine.google.com/">Google App Engine</a> 的东西。</td></tr>
</tbody>
</table>
</div>
<div class="section" id="id4">
<h2>依赖关系</h2>
<p>Pyssy 使用 <a class="reference external" href="http://flask.pocoo.org/">Flask</a> 作为网页服务器,
并且使用 Memcached 或者 Redis 作为抓取 <em>水源Web</em> 的缓存。</p>
<p>SAE Python 环境下请开启 Memcached 支持。</p>
<p>本地环境下请安装 Redis-py 并运行 redis-server 服务器程序。</p>
</div>
PyRuby2012-03-02T23:09:00+09:002012-03-02T23:09:00+09:00farseerfctag:farseerfc.me,2012-03-02:/zhs/mix-ruby.html<p>今天在GitHub上闲逛的时候看到一个叫做 <a class="reference external" href="https://github.com/danielfm/pyruby">PyRuby</a> 的项目。项目的Readme说得很好:</p>
<pre class="literal-block">
PyRuby - Some Ruby for your Python!
PyRuby is a simple way to leverage the power of Ruby to make your Python code more readable and beautiful.
Usage
All you have to do is import the ruby module:
import ruby
From now on you should be able …</pre><p>今天在GitHub上闲逛的时候看到一个叫做 <a class="reference external" href="https://github.com/danielfm/pyruby">PyRuby</a> 的项目。项目的Readme说得很好:</p>
<pre class="literal-block">
PyRuby - Some Ruby for your Python!
PyRuby is a simple way to leverage the power of Ruby to make your Python code more readable and beautiful.
Usage
All you have to do is import the ruby module:
import ruby
From now on you should be able to write Ruby code within a regular Python module. An example:
1.upto(10) { |n| puts n }
</pre>
<p>甚至 <a class="reference external" href="http://pypi.python.org/pypi/pyruby/1.0.0">PyPI</a> 上还有这个项目的包。</p>
<p>一开始我还以为这又是一个野心勃勃的基于PyPy的Ruby实现,或者某种trick在Python里面直接调用Ruby解释器。</p>
<div class="section" id="id2">
<h2>然后我想看看这个的源代码</h2>
<p>只有一个ruby.py文件,内容是:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="c1"># -*- coding: utf-8 -*-</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="nb">print</span><span class="p">(</span><span class="s2">"""</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="s2"> `.-:/+ossyhhddmmmmNNNNNNNmmmmmdddddhhhyyyyhhhyo:`</span></span>
<span class="code-line"><span class="s2"> .:+sydNNNmmdhhysso++/+++++++////::::::-.```......--/oymms.</span></span>
<span class="code-line"><span class="s2"> `:ohmdys+//::/::--::::////:-.```......`````.://:-` `/dNs.</span></span>
<span class="code-line"><span class="s2"> .+hNds:`-:-:///::------::///++///:--....--::///::-`.///. `oMm/</span></span>
<span class="code-line"><span class="s2"> /hNmo.` `` `....``````````` ...------:::-:/+/-.:/:` /NMs</span></span>
<span class="code-line"><span class="s2"> oMd/` `::::--.---://+` //` `````-:::::+/-`::.` :NM+</span></span>
<span class="code-line"><span class="s2"> yN` -+.` `/` o. ``::.-:. `` :NN:</span></span>
<span class="code-line"><span class="s2"> :Nm - ./ : `.-://///:-. `-` `` :NN-</span></span>
<span class="code-line"><span class="s2"> /NM/ .-:::-.` `/ `:sdmdhyMMMMMMNNmy/` :mNo`</span></span>
<span class="code-line"><span class="s2"> :hMd: /dmddddNNmdy+-. `smmy/-```hMMMMMMMhydm/ `-.`` `...:mMm+.</span></span>
<span class="code-line"><span class="s2"> -hNd/-/o/-..-::`.ydmmmmNMMMMMMNh:/+- dMN-`-+hmmmmdhhhhdddmMN-`-/o: .-::::/oydms-</span></span>
<span class="code-line"><span class="s2"> oNMo:+/::. ``...--:/+ohNMNhs- :hNmmdyo:..``yo-```.--. `-`-+shdddhs+-` `.//yms.</span></span>
<span class="code-line"><span class="s2"> .MMo:/`o:.:+sso+:-` sM+ ./-` /mNh+-....-/ymNNdo::--/shd+` -`:mm:</span></span>
<span class="code-line"><span class="s2"> /MM-o ./ ohhsooohNmy::sh. `yM/ `:oyyyyyyhys+:.` hy `/Nh` : -NN.</span></span>
<span class="code-line"><span class="s2"> -MM// -: `` y: odddhh+ -omNh- `--.` `` ```` .:ohMMs. +Ms / yMo</span></span>
<span class="code-line"><span class="s2"> hMoo .+. :Mh ```` `/hNd/.` ohdddy::...`..` `-/sdmdyo+NMNh+- :Mh / sMs</span></span>
<span class="code-line"><span class="s2"> .mmh:..:. :NMm `-/dMNM+ ./+++/:`.hM:`.````.` `-/shmNmh+-` /Mmooso.hM/ .: `mM/</span></span>
<span class="code-line"><span class="s2"> .mNs://: .NMNMs- -:-.`/+-sms. ` `shyyyhy`sNd` `.:+sdmmmdMM-. .oNM+ :m/ `s``yMh</span></span>
<span class="code-line"><span class="s2"> -mMo . sMNdMNNh+-. .ydyoyy` ``+o::+shdddhs+:-.:MM.`.-+hNMMh- `.`-/::dNs`</span></span>
<span class="code-line"><span class="s2"> -NM- mMMMh:MMdNmhs+:-..```-ohs-`...-:/+syhddmMMs:-.` `/mMMdmmddNMm+` ..-/hNh-</span></span>
<span class="code-line"><span class="s2"> sMy NMMM`:Mh`-/mMmmmdddddddddhhhdNNdhyo+:--.yMs `..:+ymMMMMd+--yNh. `+hNh:</span></span>
<span class="code-line"><span class="s2"> -Mm NMMM/yMh -NM-`..--:NMo:--.`+My :MNoydmNMMNmhdMh` -dNs` `yMd:</span></span>
<span class="code-line"><span class="s2"> `MN mMMMMMMMyshMN+:---.-MN-.....+My...-:/oyhdMMMMNmdy+-` +Mh:sNm/ yMy`</span></span>
<span class="code-line"><span class="s2"> MN yMMMMMMMMMMMMMMMMMNMMMMNNNNNMMMNNNMMMMMNmhMM/-. `yMMNs. /My</span></span>
<span class="code-line"><span class="s2"> `MN :MMmMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmdy+:-``NM- ./hNNy- /Nd`</span></span>
<span class="code-line"><span class="s2"> -Mh dMydMmsNMNdNNMMmmmNMMMdddhys+yMo`` /Nm: `:yNNdo. .sNd.</span></span>
<span class="code-line"><span class="s2"> +Ms .mMsMN::NN:.:MN: `.+NM. +Mo +Mm+ymNdo- .omm+`</span></span>
<span class="code-line"><span class="s2"> yM: .hNMd+:sMN. oMm. oMo +Mh ```.:+shMNmy+-``.-:-..-//-`:yNmo`</span></span>
<span class="code-line"><span class="s2"> mM. :ohmNNMMdhyMMdo//+Mm//////sMNhyhhdmNNmhs/-``./+/:--+so/-:smNy/`</span></span>
<span class="code-line"><span class="s2"> .Mm `` .-:/+osyyhhddddddddddhhyysoo+/:-. `./+//--+oo/--+ymmy/.</span></span>
<span class="code-line"><span class="s2"> :Mh .: `+:` `.------------` ```-////:/++/:../ydNdo:`</span></span>
<span class="code-line"><span class="s2"> +Ms `/` :+o+:-``` ``..-::///++///:-.`-+ydNdo:`</span></span>
<span class="code-line"><span class="s2"> oMs :/:.`` `..---.``` ````````..-:/:::---.` `-ohmmh+:`</span></span>
<span class="code-line"><span class="s2"> /Mh .://///:::-----.-----.......` `-+hmmy+-</span></span>
<span class="code-line"><span class="s2"> sMy` ``````-+ydmy+-</span></span>
<span class="code-line"><span class="s2"> /mNs-` `./ohmNMNNNmy+-</span></span>
<span class="code-line"><span class="s2"> /yNmho/:.``````````.-:/+syhdNmdyso+/-.`</span></span>
<span class="code-line"><span class="s2"> `:+ydmNMNNNNNNNNNmdhys+/:.`</span></span>
<span class="code-line"><span class="s2"> ``.....`</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="s2"> LOL U MAD?</span></span>
<span class="code-line"><span class="s2">"""</span><span class="p">)</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="kn">import</span> <span class="nn">sys</span></span>
<span class="code-line"><span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span></span>
</pre></div>
<p>是的……的确……这种尝试把Python和Ruby放在一起的想法绝对是疯了……</p>
</div>
关于C++模板的类型转换的讨论2012-02-26T05:54:57+09:002012-02-26T05:54:57+09:00farseerfctag:farseerfc.me,2012-02-26:/zhs/discuss-cpp-template-downcast.html
<p>这两天在饮水思源的C板,关于C++模板的类型转换的一个讨论,后面是我的解答。</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id4">讨论地址</a></h2>
<p><a class="reference external" href="http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1330078933,file,M.1330078933.A.html">http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1330078933,file,M.1330078933.A.html</a></p>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id5">原问题</a></h2>
<p>今天在书上看到模板演绎的时候可以允许cast-down,于是我写了个东西:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="k">template</span> <span class="o"><</span><span class="kt">bool</span> <span class="n">_Test</span><span class="p">,</span> <span class="k">class</span> <span class="nc">_Type</span> <span class="o">=</span> <span class="kt">void</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">enable_if</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span><span class="o"><</span><span class="k">class</span> <span class="nc">_Type</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">enable_if</span><span class="o"><</span><span class="nb">true</span><span class="p">,</span> <span class="n">_Type</span><span class="o">></span> <span class="p">{</span></span>
<span class="code-line"> <span class="k">typedef</span> <span class="n">_Type</span> <span class="n">type</span><span class="p">;</span></span>
<span class="code-line"><span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">class</span> <span class="nc">A</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"><span class="k">class</span> <span class="nc">B</span> <span class="o">:</span> <span class="n">A</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct …</span></span></pre></div></div>
<p>这两天在饮水思源的C板,关于C++模板的类型转换的一个讨论,后面是我的解答。</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id4">讨论地址</a></h2>
<p><a class="reference external" href="http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1330078933,file,M.1330078933.A.html">http://bbs.sjtu.edu.cn/bbstcon,board,C,reid,1330078933,file,M.1330078933.A.html</a></p>
</div>
<div class="section" id="id2">
<h2><a class="toc-backref" href="#id5">原问题</a></h2>
<p>今天在书上看到模板演绎的时候可以允许cast-down,于是我写了个东西:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="k">template</span> <span class="o"><</span><span class="kt">bool</span> <span class="n">_Test</span><span class="p">,</span> <span class="k">class</span> <span class="nc">_Type</span> <span class="o">=</span> <span class="kt">void</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">enable_if</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span><span class="o"><</span><span class="k">class</span> <span class="nc">_Type</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">enable_if</span><span class="o"><</span><span class="nb">true</span><span class="p">,</span> <span class="n">_Type</span><span class="o">></span> <span class="p">{</span></span>
<span class="code-line"> <span class="k">typedef</span> <span class="n">_Type</span> <span class="n">type</span><span class="p">;</span></span>
<span class="code-line"><span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">class</span> <span class="nc">A</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"><span class="k">class</span> <span class="nc">B</span> <span class="o">:</span> <span class="n">A</span> <span class="p">{</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">traits</span> <span class="p">{</span> <span class="k">static</span> <span class="kt">int</span> <span class="k">const</span> <span class="n">value</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><></span></span>
<span class="code-line"><span class="k">struct</span> <span class="n">traits</span><span class="o"><</span><span class="n">A</span><span class="o">></span> <span class="p">{</span> <span class="k">static</span> <span class="kt">int</span> <span class="k">const</span> <span class="n">value</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="p">};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span></span>
<span class="code-line"><span class="kt">void</span> <span class="n">f</span><span class="p">(</span><span class="n">T</span><span class="p">,</span> <span class="k">typename</span> <span class="n">enable_if</span><span class="o"><</span><span class="n">traits</span><span class="o"><</span><span class="n">T</span><span class="o">>::</span><span class="n">value</span><span class="o">>::</span><span class="n">type</span><span class="o">*</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><></span></span>
<span class="code-line"><span class="kt">void</span> <span class="n">f</span><span class="o"><</span><span class="n">A</span><span class="o">></span><span class="p">(</span><span class="n">A</span><span class="p">,</span> <span class="n">enable_if</span><span class="o"><</span><span class="n">traits</span><span class="o"><</span><span class="n">A</span><span class="o">>::</span><span class="n">value</span><span class="o">>::</span><span class="n">type</span><span class="o">*</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span></span>
<span class="code-line"><span class="k">class</span> <span class="nc">BB</span> <span class="p">{};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span></span>
<span class="code-line"><span class="k">class</span> <span class="nc">DD</span> <span class="o">:</span> <span class="k">public</span> <span class="n">BB</span><span class="o"><</span><span class="n">T</span><span class="o">></span> <span class="p">{};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="k">template</span> <span class="o"><</span><span class="k">typename</span> <span class="n">T</span><span class="o">></span> <span class="kt">void</span> <span class="n">ff</span><span class="p">(</span><span class="n">BB</span><span class="o"><</span><span class="n">T</span><span class="o">></span><span class="p">)</span> <span class="p">{};</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span> <span class="n">argv</span><span class="p">[])</span></span>
<span class="code-line"><span class="p">{</span></span>
<span class="code-line"> <span class="n">A</span> <span class="n">a</span><span class="p">;</span> <span class="n">B</span> <span class="n">b</span><span class="p">;</span></span>
<span class="code-line"> <span class="n">DD</span><span class="o"><</span><span class="kt">long</span><span class="o">></span> <span class="n">dd</span><span class="p">;</span></span>
<span class="code-line"> <span class="c1">//f(b);</span></span>
<span class="code-line"> <span class="n">ff</span><span class="p">(</span><span class="n">dd</span><span class="p">);</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
<p>奇怪的是重载决议的时候, <code class="code">
f</code>
的情况下它就不让我特化的 <code class="code">
f<A></code>
进来。</p>
<p>但是在 <code class="code">
ff</code>
的情况下, <code class="code">
ff<BB<long>></code>
却进来了。</p>
<p>在VC10和GCC3.4下测试</p>
</div>
<div class="section" id="id3">
<h2><a class="toc-backref" href="#id6">我的解答</a></h2>
<p>我们来设身处地地作为编译器,看一遍到底发生了什么。</p>
<p>约定符号 <code class="code">
#</code>
: <code class="code">
A#B</code>
是把 <code class="code">
B</code>
带入 <code class="code">
A<T></code>
的参数 <code class="code">
T</code>
之后实例化得到的结果。</p>
<div class="section" id="ff">
<h3><a class="toc-backref" href="#id7">首先看ff的情况。</a></h3>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">DD</span><span class="o"><</span><span class="kt">long</span><span class="o">></span> <span class="n">dd</span><span class="p">;</span></span>
</pre></div>
<p>处理到这句的时候,编译器看到了 <code class="code">
DD<long></code>
的实例化,于是去实例化 <code class="code">
DD#long</code>
,继而实例
化了 <code class="code">
BB#long</code>
。</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">ff</span><span class="p">(</span><span class="n">dd</span><span class="p">);</span></span>
</pre></div>
<p>这句,首先计算重载函数集合。</p>
<p>第一步,需要从参数 <code class="code">
DD#long -> BB<T></code>
推断 <code class="code">
ff<T></code>
的 <code class="code">
T</code>
。根据函数模板参数推断规则:</p>
<pre class="literal-block">
:code:`class_template_name<T>` 类型的参数,可以用于推断 :code:`T` 。
</pre>
<p>于是编译器推断 <code class="code">
T</code>
为 <code class="code">
long</code>
。这里就算不是 <code class="code">
BB</code>
而是完全无关的 <code class="code">
CC</code>
都可以推断成功,只要 <code class="code">
CC</code>
也
是一个 <code class="code">
CC<T></code>
形式的模板。</p>
<p>第二步,模板特化匹配。因为只有一个模板,所以匹配了最泛化的 <code class="code">
ff<T></code>
。</p>
<p>第三步,模板实例化。</p>
<p>推断了 <code class="code">
long -> T</code>
之后,编译器实例化 <code class="code">
ff#long</code>
。</p>
<p>重载函数集合: <code class="code">
{ff#long}</code>
</p>
<p>然后重载抉择找到唯一的可匹配的实例 <code class="code">
ff#long</code>
,检查实际参数 <code class="code">
DD#long</code>
可以隐式转换到
形式参数 <code class="code">
BB#long</code>
,从而生成了这次函数调用。</p>
</div>
<div class="section" id="f">
<h3><a class="toc-backref" href="#id8">再来看f的情况。</a></h3>
<div class="highlight"><pre><span class="code-line"><span></span><span class="n">f</span><span class="p">(</span><span class="n">b</span><span class="p">);</span></span>
</pre></div>
<p>计算候选重载函数集合。</p>
<p>第一步,对所有 <code class="code">
f</code>
模板推断实参。根据函数模板参数推断规则:</p>
<pre class="literal-block">
带有 :code:`T` 类型的参数,可以用于推断 :code:`T` 。
</pre>
<p>于是 <code class="code">
B -> T</code>
被推断出来了。</p>
<p>第二步,模板特化匹配。</p>
<p>这里 <code class="code">
B</code>
不是 <code class="code">
A</code>
,所以不能用 <code class="code">
f<A></code>
特化,只能用 <code class="code">
f<T></code>
模板。</p>
<p>第三步,模板实例化。</p>
<p><code class="code">
B</code>
带入 <code class="code">
f<T></code>
实例化成 <code class="code">
f#B</code>
的过程中,实例化 <code class="code">
traits#B</code>
。</p>
<p>由于没有针对 <code class="code">
B</code>
的特化,所以用 <code class="code">
traits<T></code>
模板, <code class="code">
traits#B::value=false</code>
,进而 <code class="code">
enable_if#false</code>
没有 <code class="code">
type</code>
,出错。</p>
<p>唯一的模板匹配出错,重载函数集合为空,SFINAE原则不能找到合适的匹配,于是报错。</p>
</div>
</div>
尝试一下 Pelican2012-02-24T17:33:00+09:002012-02-24T17:33:00+09:00farseerfctag:farseerfc.me,2012-02-24:/zhs/try-pelican.html<p>似乎一夜之间所有的
<a class="reference external" href="http://blog.yxwang.me/2011/11/migrated-to-octopress/">极客们</a>
<a class="reference external" href="http://xoyo.name/2012/02/migrate-to-octopress/">都</a>
<a class="reference external" href="http://blog.xdite.net/posts/2011/10/07/what-is-octopress/">有了</a>
<a class="reference external" href="http://www.yangzhiping.com/tech/octopress.html">自己</a>
的 <a class="reference external" href="http://pages.github.com/#user__organization_pages">Github主页</a>
和 <a class="reference external" href="http://octopress.org/">Octopress</a> 博客。就像所有人在他们的博客中指出的,静态博客的确比传统的WordPress方式具有更多优势。 自从看到这些
我就一直在想着自己搭一个 <a class="reference external" href="http://octopress.org/">Octopress</a> 。</p>
<div class="section" id="id6">
<h2>但是似乎 <a class="reference external" href="http://octopress.org/">Octopress</a> 不适合我</h2>
<p>一上手就被 <a class="reference external" href="http://octopress.org/docs/setup/">Octopress的搭建步骤</a> 烦到了。 <a class="reference external" href="http://beginrescueend.com/">RVM</a> 是什么? <a class="reference external" href="https://github.com/sstephenson/rbenv">rbenv</a> 又是什么?
看来 Ruby 社区的快节奏发展已经超过了我的想象,他们似乎需要一套发行版管理器来调和不同版本之间的 Ruby 的兼容性问题。
虽然同样的兼容性问题在 Python 社区也有 <a class="footnote-reference" href="#id10" id="id8">[1]</a> ,不过总觉得 Python 至少还没到需要一个发行版管理器的程度 <a class="footnote-reference" href="#id11" id="id9">[2]</a> 。</p>
<p>真正的问题是我手上还没有一个可以让我随便玩的 Linux 环境(真的想要……)。 而无论是 <a class="reference external" href="http://beginrescueend.com/">RVM</a> 还是 <a class="reference external" href="https://github.com/sstephenson/rbenv">rbenv</a> 似乎都只支持 Unix/Linux …</p></div><p>似乎一夜之间所有的
<a class="reference external" href="http://blog.yxwang.me/2011/11/migrated-to-octopress/">极客们</a>
<a class="reference external" href="http://xoyo.name/2012/02/migrate-to-octopress/">都</a>
<a class="reference external" href="http://blog.xdite.net/posts/2011/10/07/what-is-octopress/">有了</a>
<a class="reference external" href="http://www.yangzhiping.com/tech/octopress.html">自己</a>
的 <a class="reference external" href="http://pages.github.com/#user__organization_pages">Github主页</a>
和 <a class="reference external" href="http://octopress.org/">Octopress</a> 博客。就像所有人在他们的博客中指出的,静态博客的确比传统的WordPress方式具有更多优势。 自从看到这些
我就一直在想着自己搭一个 <a class="reference external" href="http://octopress.org/">Octopress</a> 。</p>
<div class="section" id="id6">
<h2>但是似乎 <a class="reference external" href="http://octopress.org/">Octopress</a> 不适合我</h2>
<p>一上手就被 <a class="reference external" href="http://octopress.org/docs/setup/">Octopress的搭建步骤</a> 烦到了。 <a class="reference external" href="http://beginrescueend.com/">RVM</a> 是什么? <a class="reference external" href="https://github.com/sstephenson/rbenv">rbenv</a> 又是什么?
看来 Ruby 社区的快节奏发展已经超过了我的想象,他们似乎需要一套发行版管理器来调和不同版本之间的 Ruby 的兼容性问题。
虽然同样的兼容性问题在 Python 社区也有 <a class="footnote-reference" href="#id10" id="id8">[1]</a> ,不过总觉得 Python 至少还没到需要一个发行版管理器的程度 <a class="footnote-reference" href="#id11" id="id9">[2]</a> 。</p>
<p>真正的问题是我手上还没有一个可以让我随便玩的 Linux 环境(真的想要……)。 而无论是 <a class="reference external" href="http://beginrescueend.com/">RVM</a> 还是 <a class="reference external" href="https://github.com/sstephenson/rbenv">rbenv</a> 似乎都只支持 Unix/Linux/MacOSX 。 身为极客就注定不能用 Windows 么?(或许是的……)。</p>
<p>剩下的问题就是 Ruby 和 Python 两大阵营的对立问题了。我不熟悉 <a class="reference external" href="http://daringfireball.net/projects/markdown/">Markdown</a> , 相对来说比较喜欢 <a class="reference external" href="http://docutils.sourceforge.net/rst.html">ReST</a> 。 似乎无论哪边都要
依赖 <a class="reference external" href="http://pygments.org/">Pygments</a> 作为代码着色器,那么其实 Rubyist 也至少需要安装 Python 。 我倾向于不依赖任何 Ruby 组件,最好没有 C 扩展
的纯 Python 实现。</p>
<p>于是我开始在 Github 上找 Python 的静态博客引擎。 <a class="reference external" href="http://flask.pocoo.org/">Flask</a> 的作者 <a class="reference external" href="https://github.com/mitsuhiko">mitsuhiko</a> 写的 <a class="reference external" href="https://github.com/mitsuhiko/rstblog">rstblog</a> 看起来不错,不过似乎没有多少人在用。 <a class="reference external" href="http://ringce.com/hyde">Hyde</a> 似乎很完善,不过默认的标记语言是 MarkDown , 又依赖于几个 Ruby 组建,而且官方网站的设计实在太前卫。 最终我看到了 <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 。</p>
<table border="0" class="docutils table footnote" frame="void" id="id10" rules="none">
<colgroup><col class="label"/><col/></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id8">[1]</a></td><td>比如 Python 2.x 与 3.x 之间看似难以跨越的鸿沟,以及 <a class="reference external" href="http://pypy.org/">PyPy</a> 、 <a class="reference external" href="http://python.org/">CPython</a> 、 <a class="reference external" href="http://www.stackless.com/">Stackless</a> 、 <a class="reference external" href="http://cython.org/">Cython</a> 等各个实现之间的微妙差别。</td></tr>
</tbody>
</table>
<table border="0" class="docutils table footnote" frame="void" id="id11" rules="none">
<colgroup><col class="label"/><col/></colgroup>
<tbody valign="top">
<tr><td class="label"><a class="fn-backref" href="#id9">[2]</a></td><td>是的,我们有 <a class="reference external" href="http://packages.python.org/distribute/easy_install.html">easy_install</a> ,我们有 <a class="reference external" href="http://www.pip-installer.org/en/latest/index.html">pip</a> , 不过这些都是包管理器,都是装好特定的Python实现之后的事情。 Python实现本身还不需要包管理器来管理。 Python 的版本问题基本上也只需要 <a class="reference external" href="http://docs.python.org/release/3.0.1/library/2to3.html">2to3.py</a> 和 <a class="reference external" href="http://www.startcodon.com/wordpress/?cat=8">3to2.py</a> 这样的轻量级转换器就可以了,你不需要为了安装多个软件而在硬盘里留下多个不同版本的 Python 。 如果为了引用的稳定性,你可以用 <a class="reference external" href="http://pypi.python.org/pypi/virtualenv">virtualenv</a> ,不过这又是另一回事情了。</td></tr>
</tbody>
</table>
</div>
<div class="section" id="id12">
<h2>那么就 <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 吧</h2>
<p>对我而言, <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 相比于 <a class="reference external" href="http://octopress.org/">Octopress</a> 有几个好处:</p>
<blockquote>
<ol class="arabic simple">
<li>纯 Python 实现。 这意味着我可以换用任何 Python 解释器而不必担心兼容性问题。比如我就换成了 <a class="reference external" href="http://pypy.org/">PyPy</a>。</li>
<li>多语言支持。因为 <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 的作者似乎是个法国人。不过这个似乎大部分人不需要…… 我是想尽量把一篇博客写成三种语言作为锻炼吧。</li>
<li><a class="reference external" href="http://docutils.sourceforge.net/rst.html">ReST</a> 。这样我就可以用 <a class="reference external" href="http://webpages.charter.net/edreamleo/front.html">Leo</a> 的 @auto-rst 直接写 ReST了。简单方便快捷有效。</li>
</ol>
</blockquote>
<p>不过似乎 <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 的关注度不如 <a class="reference external" href="http://octopress.org/">Octopress</a> 那么高,现在一些部分还有细微的问题:</p>
<blockquote>
<ol class="arabic simple">
<li>pelican-import 从 WordPress 导入的时候对中文、日文的支持似乎很成问题。</li>
<li>日期格式、时区、字符集、和多语言功能的结合度还不够。 <strong>我在尝试改善它。</strong></li>
<li>模板还不够丰富。</li>
<li>插件也不够多……</li>
</ol>
</blockquote>
<p>希望这么优秀的工具能够受到更多关注,以上这些问题都是增加关注度之后很快就能解决的问题。</p>
</div>
<div class="section" id="settings-py">
<h2>我的设置 settings.py</h2>
<p>安装 <a class="reference external" href="http://pelican.notmyidea.org/en/latest/">Pelican</a> 很容易,一句话就够了:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> pip install pelican</span>
</pre></div>
<p>然后把文章写成ReST的格式,放在`pages`文件夹里面。(重新)生成只要:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> pelican -s settings.py</span>
</pre></div>
<p>上传到 Github:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="gp">$</span> git commit -am <span class="s2">"Commit message"</span></span>
<span class="code-line"><span class="gp">$</span> git push</span>
</pre></div>
<p>就这么简单。附上我的配置文件:</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="c1"># -*- coding: utf-8 -*-</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">TIMEZONE</span> <span class="o">=</span> <span class="s1">'Asia/Tokyo'</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">DATE_FORMATS</span> <span class="o">=</span> <span class="p">{</span></span>
<span class="code-line"> <span class="s1">'en'</span><span class="p">:(</span><span class="s1">'usa'</span><span class="p">,</span><span class="s1">'</span><span class="si">%a</span><span class="s1">, </span><span class="si">%d</span><span class="s1"> %b %Y'</span><span class="p">),</span></span>
<span class="code-line"> <span class="s1">'zh'</span><span class="p">:(</span><span class="s1">'chs'</span><span class="p">,</span><span class="s1">'%Y-%m-</span><span class="si">%d</span><span class="s1">, </span><span class="si">%a</span><span class="s1">'</span><span class="p">),</span></span>
<span class="code-line"> <span class="s1">'jp'</span><span class="p">:(</span><span class="s1">'jpn'</span><span class="p">,</span><span class="s1">'%Y/%m/</span><span class="si">%d</span><span class="s1"> (</span><span class="si">%a</span><span class="s1">)'</span><span class="p">),</span></span>
<span class="code-line"><span class="p">}</span></span>
<span class="code-line"><span class="c1"># windows locale: http://msdn.microsoft.com/en-us/library/cdax410z%28VS.71%29.aspx</span></span>
<span class="code-line"><span class="n">LOCALE</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'usa'</span><span class="p">,</span> <span class="s1">'chs'</span><span class="p">,</span> <span class="s1">'jpn'</span><span class="p">,</span> <span class="c1"># windows</span></span>
<span class="code-line"> <span class="s1">'en_US'</span><span class="p">,</span> <span class="s1">'zh_CN'</span><span class="p">,</span> <span class="s1">'ja_JP'</span><span class="p">]</span> <span class="c1"># Unix/Linux</span></span>
<span class="code-line"><span class="n">DEFAULT_LANG</span> <span class="o">=</span> <span class="s1">'zh'</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">SITENAME</span> <span class="o">=</span> <span class="s1">'Farseerfc Blog'</span></span>
<span class="code-line"><span class="n">AUTHOR</span> <span class="o">=</span> <span class="s1">'Jiachen Yang'</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">DISQUS_SITENAME</span> <span class="o">=</span> <span class="s1">'farseerfcgithub'</span></span>
<span class="code-line"><span class="n">GITHUB_URL</span> <span class="o">=</span> <span class="s1">'https://github.com/farseerfc'</span></span>
<span class="code-line"><span class="n">SITEURL</span> <span class="o">=</span> <span class="s1">'http://farseerfc.github.com'</span></span>
<span class="code-line"><span class="n">TAG_FEED</span> <span class="o">=</span> <span class="s1">'feeds/</span><span class="si">%s</span><span class="s1">.atom.xml'</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">SOCIAL</span> <span class="o">=</span> <span class="p">((</span><span class="s1">'twitter'</span><span class="p">,</span> <span class="s1">'http://twitter.com/farseerfc'</span><span class="p">),</span></span>
<span class="code-line"> <span class="p">(</span><span class="s1">'github'</span><span class="p">,</span> <span class="s1">'https://github.com/farseerfc'</span><span class="p">),</span></span>
<span class="code-line"> <span class="p">(</span><span class="s1">'facebook'</span><span class="p">,</span> <span class="s1">'http://www.facebook.com/farseerfc'</span><span class="p">),</span></span>
<span class="code-line"> <span class="p">(</span><span class="s1">'weibo'</span><span class="p">,</span> <span class="s1">'http://weibo.com/farseerfc'</span><span class="p">),</span></span>
<span class="code-line"> <span class="p">(</span><span class="s1">'renren'</span><span class="p">,</span> <span class="s1">'http://www.renren.com/farseer'</span><span class="p">),</span></span>
<span class="code-line"> <span class="p">)</span></span>
<span class="code-line"></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">TWITTER_USERNAME</span> <span class="o">=</span> <span class="s1">'farseerfc'</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">THEME</span><span class="o">=</span><span class="s1">'notmyidea'</span></span>
<span class="code-line"><span class="n">CSS_FILE</span> <span class="o">=</span> <span class="s2">"wide.css"</span></span>
<span class="code-line"></span>
<span class="code-line"><span class="n">DEFAULT_CATEGORY</span> <span class="o">=</span><span class="s1">'Others'</span></span>
<span class="code-line"><span class="n">OUTPUT_PATH</span> <span class="o">=</span> <span class="s1">'.'</span></span>
<span class="code-line"><span class="n">PATH</span> <span class="o">=</span> <span class="s1">'posts'</span></span>
</pre></div>
</div>
关于我的Blogs2011-09-27T02:35:00+09:002011-09-27T02:35:00+09:00farseerfctag:farseerfc.me,2011-09-27:/zhs/about-my-blogs.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>很久没有写过blog或者之类的东西了。这边一直荒废着。</p>
<p>由于国内被墙的原因,另一个wordpress: <a class="reference external" href="http://fchome.sinaapp.com/">http://fchome.sinaapp.com/</a>
应该会同步更新这里的内容。</p>
<p>抽空写点什么吧。</p>
“…if we do this work … ” --Bill Gates2011-03-14T20:34:00+09:002011-03-14T20:34:00+09:00farseerfctag:farseerfc.me,2011-03-14:/zhs/if-we-do-this-work.html<p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/716517729">renren</a></p>
<p>From: Bill Gates</p>
<p>’-- Sent: Sunday, January 24, 1999 8:41 AM</p>
<p>Jeff Westorinon; Ben Fathi ;</p>
<p>TO: Carl Stork (Exchange); Nathan Myhrvofd; Eric Rudder</p>
<p>Subject: ACPI extensions</p>
<p>One thing I find myself wondering about is whether we shouldn’t try and
make the "ACPI" extensions somehow Windows specific.</p>
<p>It …</p><p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/716517729">renren</a></p>
<p>From: Bill Gates</p>
<p>’-- Sent: Sunday, January 24, 1999 8:41 AM</p>
<p>Jeff Westorinon; Ben Fathi ;</p>
<p>TO: Carl Stork (Exchange); Nathan Myhrvofd; Eric Rudder</p>
<p>Subject: ACPI extensions</p>
<p>One thing I find myself wondering about is whether we shouldn’t try and
make the "ACPI" extensions somehow Windows specific.</p>
<p>It seems unfortunate if we do this work and get our partners to do the
work and the result is that <strong>Linux works great without having to do the work</strong>.</p>
<p><strong>Maybe there is no way to avoid this problem but it does bother me.</strong></p>
<p>Maybe we could define the APIs so that they work well with NT and not
the others even if they are open.</p>
<p>Or maybe we could patent something relaled to this.</p>
<p>From:</p>
<p><a class="reference external" href="http://antitrust.slated.org/www.iowaconsumercase.org/011607/3000/PX03020.pdf">http://antitrust.slated.org/www.iowaconsumercase.org/011607/3000/PX03020.pdf</a></p>
<p>如果这就是我至今在Xen4.0上得不到ACPI 3.0的完善支持的原因,那么我诅咒Bill Gates!</p>
[zz]“西厢计划”原理小解2010-03-17T09:40:00+09:002010-03-17T09:40:00+09:00farseerfctag:farseerfc.me,2010-03-17:/zhs/zz-introducing-scholarzhang.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>好神奇的想法,先存着,以后慢慢研究</p>
<p>原文: <a class="reference external" href="http://blog.youxu.info/2010/03/14/west-chamber/">http://blog.youxu.info/2010/03/14/west-
chamber/</a></p>
<p>待月西厢下,迎风户半开。隔墙花影动,疑是玉人来。</p>
<p>最近推上最流行的一个关键词是”西厢计划”,
这个计划名字取得很浪漫,客户端叫做张生,对,就是西厢记里面那个翻墙去见崔莺莺小姐的张生;显然,服务器端必然叫做崔莺莺。客户端的张生是最重要的部件,可以不依赖于服务端工作。因为西厢计划的作者只是简要的介绍了一下原理,其他报道又语焉不详,我当时就觉得很好奇,花了昨天一个晚上详细读了一下源代码,终于知道怎么回事了,觉得原理非常漂亮,所以写篇文章介绍总结一下。</p>
<p>先说大方向。大家都知道,连接被重置的本质,是因为收到了破坏连接的一个 TCP
Reset 包。以前剑桥大学有人实验过,客户端和服务器都忽略 Reset,
则通信可以不受影响 …</p><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>好神奇的想法,先存着,以后慢慢研究</p>
<p>原文: <a class="reference external" href="http://blog.youxu.info/2010/03/14/west-chamber/">http://blog.youxu.info/2010/03/14/west-
chamber/</a></p>
<p>待月西厢下,迎风户半开。隔墙花影动,疑是玉人来。</p>
<p>最近推上最流行的一个关键词是”西厢计划”,
这个计划名字取得很浪漫,客户端叫做张生,对,就是西厢记里面那个翻墙去见崔莺莺小姐的张生;显然,服务器端必然叫做崔莺莺。客户端的张生是最重要的部件,可以不依赖于服务端工作。因为西厢计划的作者只是简要的介绍了一下原理,其他报道又语焉不详,我当时就觉得很好奇,花了昨天一个晚上详细读了一下源代码,终于知道怎么回事了,觉得原理非常漂亮,所以写篇文章介绍总结一下。</p>
<p>先说大方向。大家都知道,连接被重置的本质,是因为收到了破坏连接的一个 TCP
Reset 包。以前剑桥大学有人实验过,客户端和服务器都忽略 Reset,
则通信可以不受影响。但是这个方法其实只有理论价值,因为绝大多数服务器都不可能忽略
Reset 的 (比如 Linux, 需要 root 权限配置iptables, 而且这本身也把正常的
Reset 给忽略了)。只要服务器不忽略 Reset,
客户端再怎么弄都没用,因为服务器会停止发送数据,Reset
这条连接。所以,很多报道说西厢计划是忽略 Reset,
我从源代码来看应该不是这样。在我看来,西厢计划是利用了墙的一个可能的弱点–墙只在连接发起的时候把一个
TCP
连接加入监听序列,如果墙认为这个连接终止了,就会从监听序列中去掉这条记录,这样,这条连接上后续的包就不会被监听。西厢计划就是让墙“认为”这个连接终止的一个绝妙的方法。只要墙认为这个连接两端都是死老虎,墙就不会触发关键词检测,其后所有的数据,都不存在连接被重置的问题了。</p>
<p>如何让一个连接置之死地而后生,就是西厢计划那帮黑客神奇的地方了。这也不是一日之功。
首先,这帮牛人发现,墙的是一个入侵检测系统,把含有关键字的包当成一种“入侵”来对待。采取这种设计有很多好处,但缺点是入侵检测系统可能具有的问题,墙都可能有。西厢计划主页上那篇著名的论文就是讲这些七七八八的漏洞的。可以说处理这些七七八八的漏洞是非常困难的,迫使墙的设计者“拆东墙,补西墙”。这样补来补去,外表看起来好像很牛逼的墙,其实有很多本质上无法简单修补的漏洞,其中有一个致命的,就是
TCP 连接状态的判定问题。
出于入侵检测系统这种设计的局限,墙没有,也没办法准确判定一条 TCP
连接的状态,而只是根据两边收到的数据来“推测”连接的状态。而所有的关键词检测功能,都是基于“连接还活着”的这个推测的结果的。因为墙的规则是在连接发起的时候开始对这条连接的检测,在连接终止的时候停止对这条连接的检测,所以,一旦对连接的状态推测错误,把还活着的连接当成已经关闭的连接,墙就会放弃对这条连接上随后所有的包的检测,他们都会都透明的穿过墙的入侵检测。</p>
<p>上面只是想法,具体到 TCP
协议实现这一层,就要只迷惑墙,还不能触及我要通信的服务器。最理想的情况下,在任何有效通信之前,就能让墙出现错误判断,这些,就需要对
TCP 协议有深刻理解了。西厢计划的那帮黑客,居然真的去读 TCP 几百页的
RFC,还居然就发现了方法(这里我假设读者都知道 TCP
的三次握手过程和序列号每次加一的规则)。
我们都知道,三次握手的时候,在收到服务器的 SYN/ACK
的时候,客户端如果发送 ACK 并且序列号+1
就算建立连接了,但是客户端如果发送一个序列号没 +1 的 FIN
(表示连接终止,但是服务器知道,这时候连接还没建立呢, FIN
这个包状态是错的,加上序列号也是错的,服务器自己一判断,就知道这个包是坏包,按照标准协议,服务器随手丢弃了这个包),
但这个包,过墙的时候,在墙看来,是表示连接终止的(墙是 ma de in china,
是比较山寨的,不维护连接状态,并且,墙并没有记下刚才服务器出去的 SYN/ACK
的序列号,所以墙不知道序列号错了)。所以,墙很高兴的理解为连接终止,舒了一口气去重置其他连接了,
而这个连接,就成了僵尸,墙不管你客户端了,而这时候,好戏才刚刚开始。</p>
<p>事实上,墙是双向检测的(或者说对每个包都检测的),因此,对服务器和客户端实现相同的对待方法,所以,墙不管客户端还不行,假如服务端有关键词传给客户端,墙还是有可能要发飙的(这里说有可能,因为我也不知道)。所以,最好的办法就是,让服务端也给墙一个终止连接的标志就好了。可是这个说起来简单,做起来难,怎么能让不受自己控制的服务器发一个自己想要的包呢?
西厢计划的那帮黑客,再次去读几百页的 RFC, 令人惊讶的发现,他们居然在 RFC
上发现了一个可以用的特性。我们上面说了,三次握手的时候,在收到 SYN/ACK
后,客户端要给服务器发送一个序列号+1 的ACK,可是,假如我不+1呢,直接发
ACK 包给服务器。
墙已经认为你客户端是死老虎了,不理你了,不知道你搞什么飞机,让这个 ACK
过了。可是服务器一看,不对啊,你给我的不是我期待的那个序列号, RFC
上说了,TCP 包如果序列号错了的话,就回复一个 Reset.
所以,服务器就回复了一个 Reset。这个 Reset
过墙的时候,墙一看乐了,服务器也终止连接了,好吧,两边都是死老虎了,我就不监听这条连接了。而至于客户端,这个服务器过来的
Reset 非常好识别,忽略就是。随后,客户端开始正确的发送 ACK,
至此,三次握手成功,真正的好戏开始,而墙则认为客户端和服务器都是死老虎,直接放过。所以,张生就这样透明的过了墙。
至于过墙以后所有的事情,《西厢记》里面都有记载,各位读者自行买书学习。</p>
<p>现在的西厢计划客户端,即“张生”模块的防连接重置的原理就是这样,服务器端,即莺莺模块的实现也是类似的。防DNS那个,不懂
DNS
协议,所以看不懂。我猜想,因为开发人员都是黑客,所以自然喜欢用最经得起折腾和高度定制的
Linux 开发。 现在看西厢计划的实现,因为依赖于 Linux 内核模块 netfilter,
在 Linux 上如鱼得水,但往其他平台的移植可能是个亟待解决的问题。
我觉得,在其他平台上,可以通过 libpcap 和 libnet
,在用户态实现相同的功能,就是有点麻烦而已,有兴趣的懂网络的可以照西厢计划原理,在家自行做出此功能;当然,全中国人民都用
Linux 最好 :)</p>
<div class="line-block">
<div class="line">PS 1: 据说是西厢计划一个作者画的原理图:<a class="reference external" href="http://img.ly/DIi">http://img.ly/DIi</a></div>
<div class="line-block">
<div class="line">PS 2: 我对 TCP 的理解仅限于课本,如果上面的对技术的理解有错,请大家指出。</div>
<div class="line">PS 3: 有些漏洞,可能是设计上本质缺陷,不是那么容易修复的。</div>
<div class="line">PS 4: 除了最后一个图,本文没有其他相关链接,如需相关资料,自行Google。</div>
</div>
</div>
写程序让CPU占用率保持正弦函数2008-06-02T23:27:00+09:002008-06-02T23:27:00+09:00farseerfctag:farseerfc.me,2008-06-02:/zhs/sine-cpu.html<p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/298871889">renren</a></p>
<p>据说是一道微软的面试题。如题,写程序,让Windows的任务管理器中的性能监视器呈现正弦曲线。</p>
<img alt="正弦曲线" class="img-responsive align-center" src="http://fm531.img.xiaonei.com/pic001/20080602/23/14/large_10019p67.jpg"/>
<img alt="正弦曲线" class="img-responsive align-center" src="http://fm541.img.xiaonei.com/pic001/20080602/23/14/large_9935o67.jpg"/>
<!-- PELICAN_END_SUMMARY -->
<p>潜心钻研良久,得代码:(java)</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="kd">public</span> <span class="kd">class</span> <span class="nc">sincpu</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">cycle</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span><span class="n">tick</span> <span class="o">=</span> <span class="mi">256</span><span class="p">;</span></span>
<span class="code-line"> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span> <span class="p">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="p">)</span> <span class="kd">throws</span> <span class="n">InterruptedException</span> <span class="p">{</span></span>
<span class="code-line"> <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;;</span><span class="n">i</span><span class="o">++</span><span class="p">){</span></span>
<span class="code-line"> <span class="n">work</span><span class="p">(</span><span class="n">calcNextSleep</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="n">cycle</span><span class="p">));</span></span>
<span class="code-line"> <span class="n">sleep</span><span class="p">(</span><span class="n">tick</span> <span class="o">-</span> <span class="n">calcNextSleep</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="n">cycle</span><span class="p">));</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="nf">calcNextSleep</span><span class="p">(</span><span class="kt">long</span> <span class="n">i</span><span class="p">){</span></span>
<span class="code-line"> <span class="k">return</span> <span class="p">(</span><span class="kt">int …</span></span></pre></div><p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/298871889">renren</a></p>
<p>据说是一道微软的面试题。如题,写程序,让Windows的任务管理器中的性能监视器呈现正弦曲线。</p>
<img alt="正弦曲线" class="img-responsive align-center" src="http://fm531.img.xiaonei.com/pic001/20080602/23/14/large_10019p67.jpg"/>
<img alt="正弦曲线" class="img-responsive align-center" src="http://fm541.img.xiaonei.com/pic001/20080602/23/14/large_9935o67.jpg"/>
<!-- PELICAN_END_SUMMARY -->
<p>潜心钻研良久,得代码:(java)</p>
<div class="highlight"><pre><span class="code-line"><span></span><span class="kd">public</span> <span class="kd">class</span> <span class="nc">sincpu</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">cycle</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span><span class="n">tick</span> <span class="o">=</span> <span class="mi">256</span><span class="p">;</span></span>
<span class="code-line"> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span> <span class="p">(</span><span class="n">String</span><span class="o">[]</span> <span class="n">args</span><span class="p">)</span> <span class="kd">throws</span> <span class="n">InterruptedException</span> <span class="p">{</span></span>
<span class="code-line"> <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;;</span><span class="n">i</span><span class="o">++</span><span class="p">){</span></span>
<span class="code-line"> <span class="n">work</span><span class="p">(</span><span class="n">calcNextSleep</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="n">cycle</span><span class="p">));</span></span>
<span class="code-line"> <span class="n">sleep</span><span class="p">(</span><span class="n">tick</span> <span class="o">-</span> <span class="n">calcNextSleep</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="n">cycle</span><span class="p">));</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="nf">calcNextSleep</span><span class="p">(</span><span class="kt">long</span> <span class="n">i</span><span class="p">){</span></span>
<span class="code-line"> <span class="k">return</span> <span class="p">(</span><span class="kt">int</span><span class="p">)(</span><span class="n">Math</span><span class="p">.</span><span class="na">sin</span><span class="p">((</span><span class="kt">double</span><span class="p">)</span><span class="n">i</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">Math</span><span class="p">.</span><span class="na">PI</span> <span class="o">/</span> <span class="n">cycle</span><span class="p">)</span> <span class="o">*</span> <span class="n">tick</span> <span class="o">+</span> <span class="n">tick</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">sleep</span> <span class="p">(</span><span class="kt">long</span> <span class="n">sleepTime</span><span class="p">)</span> <span class="kd">throws</span> <span class="n">InterruptedException</span></span>
<span class="code-line"> <span class="p">{</span></span>
<span class="code-line"> <span class="k">if</span><span class="p">(</span><span class="n">sleepTime</span> <span class="o"><</span> <span class="mi">2</span><span class="p">)</span></span>
<span class="code-line"> <span class="n">Thread</span><span class="p">.</span><span class="na">yield</span><span class="p">();</span></span>
<span class="code-line"> <span class="k">else</span></span>
<span class="code-line"> <span class="n">Thread</span><span class="p">.</span><span class="na">sleep</span><span class="p">(</span><span class="n">sleepTime</span><span class="p">);</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"></span>
<span class="code-line"> <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">work</span> <span class="p">(</span><span class="kt">long</span> <span class="n">period</span><span class="p">)</span> <span class="p">{</span></span>
<span class="code-line"> <span class="kt">long</span> <span class="n">start</span> <span class="o">=</span> <span class="n">System</span><span class="p">.</span><span class="na">currentTimeMillis</span><span class="p">();</span></span>
<span class="code-line"> <span class="k">for</span><span class="p">(;;){</span></span>
<span class="code-line"> <span class="n">Math</span><span class="p">.</span><span class="na">sin</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span></span>
<span class="code-line"> <span class="k">if</span><span class="p">(</span><span class="n">System</span><span class="p">.</span><span class="na">currentTimeMillis</span><span class="p">()</span> <span class="o">-</span> <span class="n">start</span> <span class="o">>=</span> <span class="n">period</span><span class="p">)</span></span>
<span class="code-line"> <span class="k">break</span><span class="p">;</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"> <span class="p">}</span></span>
<span class="code-line"><span class="p">}</span></span>
</pre></div>
<p>多核CPU上测试时要注意关掉一个CPU:</p>
<img alt="多核CPU上测试" class="img-responsive align-center" src="http://fm411.img.xiaonei.com/pic001/20080602/23/14/large_9946k67.jpg"/>
关于神创论的一些见解2008-05-12T11:16:00+09:002008-05-12T11:16:00+09:00farseerfctag:farseerfc.me,2008-05-12:/zhs/some-thought-on-creationism.html<p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/292274803">renren</a></p>
<p>看到陈骉同学很有感想的一篇神创论与命运日志,觉得近日很久没有看到这样的评论了。想说几句自己的观点。</p>
<p>首先我认为,神创论与宿命论没有多少关联,甚至进化论者相较于神创论者更容易接受宿命论的观点。因为神创论主张意志的存在,人所具有的个体意志与神的意志,因此在神创论者的眼中事件的结果是可以通过意志来改变的,亦即如果我从物理楼11楼跳下,那么我就可以改变自己死亡时间的宿命。上帝的意志同样可以左右事件的结果,也就是所谓的宿命不复存在。而进化论者不承认意志独立于物质世界的存在,你我的思考、行为,都受到物理学法则诸如量子力学的约束,这就引出了北大物理系教授的那句“宇宙中的一切都是可以计算的”,亦即宿命论。如我我选择现在从物理楼上跳下,我这一行为并不是处于个人的独立意志,乃是想证明这一点,亦即我跳楼这一举动是有其背后的动机与原因的,就如同计算机的输入必然导致了输出,宿命的必然终结于此。</p>
<p>其次,关于事件的复杂度所导致的随机化,在大量混沌随机中也存在着如统计学和随机分形学这样的规律,并不是否认宿命的充分理由。</p>
<p>关于神创论的合理性问题。我认为是否相信神的存在只是一个boolean二值问题,它为true为false本身并不重要,重要的是确定它的取值之后得到的推论与结果。如果否认神的存在,如现代数学这样的完美又何以存在,进化论者的解释是事物最终会向着更好更高级的方向发展,产生现代数学乃至现代科学是发展的必然。而这种论调显然有悖于物理中以热力学第二定律为首的,预言事物会随时间推演愈发混乱的论断。更进一步,甚至整个人类、整个生物系统的存在都是有悖于热力学推论的现象,是某种理论只能以“小概率事件”解释的现象。</p>
<p>神创论的核心观点之一,是神的唯一存在性 …</p><p>导入自
<a class="reference external" href="http://blog.renren.com/blog/230263946/292274803">renren</a></p>
<p>看到陈骉同学很有感想的一篇神创论与命运日志,觉得近日很久没有看到这样的评论了。想说几句自己的观点。</p>
<p>首先我认为,神创论与宿命论没有多少关联,甚至进化论者相较于神创论者更容易接受宿命论的观点。因为神创论主张意志的存在,人所具有的个体意志与神的意志,因此在神创论者的眼中事件的结果是可以通过意志来改变的,亦即如果我从物理楼11楼跳下,那么我就可以改变自己死亡时间的宿命。上帝的意志同样可以左右事件的结果,也就是所谓的宿命不复存在。而进化论者不承认意志独立于物质世界的存在,你我的思考、行为,都受到物理学法则诸如量子力学的约束,这就引出了北大物理系教授的那句“宇宙中的一切都是可以计算的”,亦即宿命论。如我我选择现在从物理楼上跳下,我这一行为并不是处于个人的独立意志,乃是想证明这一点,亦即我跳楼这一举动是有其背后的动机与原因的,就如同计算机的输入必然导致了输出,宿命的必然终结于此。</p>
<p>其次,关于事件的复杂度所导致的随机化,在大量混沌随机中也存在着如统计学和随机分形学这样的规律,并不是否认宿命的充分理由。</p>
<p>关于神创论的合理性问题。我认为是否相信神的存在只是一个boolean二值问题,它为true为false本身并不重要,重要的是确定它的取值之后得到的推论与结果。如果否认神的存在,如现代数学这样的完美又何以存在,进化论者的解释是事物最终会向着更好更高级的方向发展,产生现代数学乃至现代科学是发展的必然。而这种论调显然有悖于物理中以热力学第二定律为首的,预言事物会随时间推演愈发混乱的论断。更进一步,甚至整个人类、整个生物系统的存在都是有悖于热力学推论的现象,是某种理论只能以“小概率事件”解释的现象。</p>
<p>神创论的核心观点之一,是神的唯一存在性,按照邹恒明的比喻,这就如同数学中集合中元素的的唯一性一般至关重要。数学乃至近代科学的发展,其起源在于这种对神性的探求,而不仅仅是好奇心就可以解释的。反观东方文化中数学的发展,开始时领先于西方科学千余每年,但是始终作为一种craft-oriented的实用主义学科。可以说没有了神的唯一性支持,人们就不能确信自己能找到这样一种完美高效的学科,只能在实用的基础上发展其基础算数。可以想象,没有神的完美与唯一性,数学必将发展成现代化学或者微软软件这样,庞大而充满特例,到处都是修补与查表,怎么会像现在的完美、简洁与和谐。</p>
<p>神创论者并不是将难题推与“神”然后放任不管,他们相信神是最为理智的存在,创人时人同样得到了神的智慧和理智,也就是神可以用人的理智来理解。</p>
<p>引用牛顿《自然哲学的数学原理》中终章的话“太阳、恒星、行星的这个极精致的结构不可能存在,除非通过一个有理智的和有权能的存在的设计和主宰……他不是作为宇宙的灵魂,而是作为一切的主宰而统治所有……”</p>
<p>以上……</p>
<p>(发现最近的哲理思维果然慢了不少,写作思绪也一片混乱^_^)</p>
由记忆棒误差故障引发的关于面向对象设计的九点思考2007-09-20T14:38:00+09:002007-09-20T14:38:00+09:00farseerfctag:farseerfc.me,2007-09-20:/zhs/9-thoughts-about-oop-from-wrongly-insert-memory-stick.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>故障描述:
MMC Memory Stick Duo记忆棒未经Adapter适配器,直接插入SD Reader,致使MMC卡入SD Reader中。</p>
<p>栈展开:
某日下午,无课。
忙于数分作业,想查询用手机拍摄的板书照片。
取出手机中的MMC。
未经装配Adapter,直接插入SD Reader。
(A runtime exception was thrown.)
尝试翻转笔记本机身,倒出MMC,未果。(rethrow)
尝试用手指甲取出,未果。(rethrow)
考虑到有“推入反弹”机制,尝试将MMC推入更深,反弹机制由于类型不匹配而失效,未果。(rethrow)
(The exception spread across the border of the …</p><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>故障描述:
MMC Memory Stick Duo记忆棒未经Adapter适配器,直接插入SD Reader,致使MMC卡入SD Reader中。</p>
<p>栈展开:
某日下午,无课。
忙于数分作业,想查询用手机拍摄的板书照片。
取出手机中的MMC。
未经装配Adapter,直接插入SD Reader。
(A runtime exception was thrown.)
尝试翻转笔记本机身,倒出MMC,未果。(rethrow)
尝试用手指甲取出,未果。(rethrow)
考虑到有“推入反弹”机制,尝试将MMC推入更深,反弹机制由于类型不匹配而失效,未果。(rethrow)
(The exception spread across the border of the model.)
电脑维修技师接手(catch)
技师未能发现问题所在,由我解说原委。
(Because the exception lose the information, RTTI was asked to recall the information)
技师发现问题,尝试用镊子镊出MMC,未果。
技师开解机箱(expose the data structure)
技师制作钩子,勾出MMC(hooker link to the structure)
取出MMC,故障解除</p>
<p>故障总结
1.接收到没有完全了解、或没有适当工具解决的exception时,不要尝试用不成熟的技术解决,应尽快寻求能解决它的代码。否则,被反复rethrow的exception,尤其是通过模块边界的exception,有可能由subclass退化为superclass,并因此而丧失一些信息。尽量不要让exception丢失信息,必要时,通过RTTI机制寻回信息。</p>
<p>2.超负荷运转,多线程执行,这种种复杂性都有可能导致错误,应避免。无论你有多么信任你的代码或能力。</p>
<p>3.在设计class的interface时,相匹配的interface应该满足is-a的关系。因此,任何能插入SD Reader的object,即任何实现了SD interface的object,都应该is-a SD card。这次故障中,interface接受了MMC,但MMC不是SD。即使这种情况下throw an exception,都不能使事态缓和。能提供compile-time error时,尽量让错误以compile-time error的形式展现,并在事先解决。类型匹配问题是应该能在事先解决的问题。</p>
<p>4.Design patterns中的Adapter pattern应该只是迫不得已情况之下的解决方案。只有当你无权改变现状时,才能使用Adapter。如果能改变现状,应该改变设计以符合interface。</p>
<p>5.因为上条,所有相似功能的对象应具有相同的interface,不同的interface是本次故障的根源所在。</p>
<p>6.特殊情况下,破坏封装机制并expose the data structure是必要的,应该有方法支持这种做法。C的指针和C#的Reflection技术都以不同的方式支持这种做法。其他的一些语言机制,比如serializing(序列化)或streaming(流化),也可以以某种方式间接支持这一做法。当然,机制还应避免这种做法被滥用。</p>
<p>7.相反功能具有相同操作的设计,容易造成使用的混乱,应适当避免。比如SD Reader的推入反弹设计,即插入和弹出使用同一个向里推的操作的设计。同样的设计还包括,C++中的setNewHandle使用同一个函数,同时设置和返回handle。以及有些书中提倡的,使用同名函数重载的方式,实现setter/getter的设计。</p>
<p>8.特殊工具(hooker)对于解决特定问题,通常比手工解决有效。不要嫌麻烦而不愿意构造特殊工具。</p>
<p>9.栈语义,即FILO顺序,总在不知不觉中影响我们。违反了FILO顺序的操作极易造成混乱。本故障发生时正确的处理顺序为:
装配Adapter
插入SD Reader
读取数据
停用设备
拔出SD Reader
拆解Adapter
本次故障的原因就是违反了FILO顺序,违反了栈语义。</p>
Program Development in Java Preface2007-09-16T13:26:00+09:002007-09-16T13:26:00+09:00farseerfctag:farseerfc.me,2007-09-16:/zhs/program-development-in-java-preface.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>程序开发原理</h2>
<div class="section" id="id2">
<h3>——抽象、规格与面向对象设计</h3>
<p>Barbara Liskov 、John Guttag 著</p>
<div class="line-block">
<div class="line">杨嘉晨 等译</div>
</div>
</div>
<div class="section" id="id3">
<h3>关于翻译风格:</h3>
<div class="line-block">
<div class="line">多年来阅读计算机类的著作及译作,感觉总体的困难在于一大堆没有标准译名的技术术语。由于通行于工业界和学术界的还是英文原名和术语,我决定保留大量的英文术语。这样的翻译风格借鉴于台湾著名的译者和作者侯捷先生。对于译与不译的权衡,主要考虑阅读的流畅,以及读者的理解能力,或许难免带有一些主观色彩。</div>
</div>
</div>
</div>
<div class="section" id="preface">
<h2>前言 Preface</h2>
<p>构建产品级质量的程序——可以在很长一段时间内使用的程序——众所周知是极其困难的。本书的目标就是改善程序员解决这项任务的效率。我希望读者在阅读本书之后成为一名好程序员。我相信本书的成功在于改善编程技巧,因为我的学生告诉我这已经发生在他们身上。</p>
<p>怎么才算是一名好程序员?是产生整个程序产品的效率。关键是要在每一阶段减少浪费掉的努力。解决的方法包括:在开始编写代码之前就仔细考虑你的实现方案,通过未雨绸缪的方法来编写代码,使用严格的测试在早期发现错误,以及仔细注意模块化编程,这样当错误出现时,只需要改动极少数代码就可以修正整个程序。本书涉及所有这些领域的技术。</p>
<p>模块化编程(Modularity)是编写好程序的关键。把程序分解成许多小模块,每一个模块通过良好定义的狭窄接口和别的模块交互作用 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>程序开发原理</h2>
<div class="section" id="id2">
<h3>——抽象、规格与面向对象设计</h3>
<p>Barbara Liskov 、John Guttag 著</p>
<div class="line-block">
<div class="line">杨嘉晨 等译</div>
</div>
</div>
<div class="section" id="id3">
<h3>关于翻译风格:</h3>
<div class="line-block">
<div class="line">多年来阅读计算机类的著作及译作,感觉总体的困难在于一大堆没有标准译名的技术术语。由于通行于工业界和学术界的还是英文原名和术语,我决定保留大量的英文术语。这样的翻译风格借鉴于台湾著名的译者和作者侯捷先生。对于译与不译的权衡,主要考虑阅读的流畅,以及读者的理解能力,或许难免带有一些主观色彩。</div>
</div>
</div>
</div>
<div class="section" id="preface">
<h2>前言 Preface</h2>
<p>构建产品级质量的程序——可以在很长一段时间内使用的程序——众所周知是极其困难的。本书的目标就是改善程序员解决这项任务的效率。我希望读者在阅读本书之后成为一名好程序员。我相信本书的成功在于改善编程技巧,因为我的学生告诉我这已经发生在他们身上。</p>
<p>怎么才算是一名好程序员?是产生整个程序产品的效率。关键是要在每一阶段减少浪费掉的努力。解决的方法包括:在开始编写代码之前就仔细考虑你的实现方案,通过未雨绸缪的方法来编写代码,使用严格的测试在早期发现错误,以及仔细注意模块化编程,这样当错误出现时,只需要改动极少数代码就可以修正整个程序。本书涉及所有这些领域的技术。</p>
<p>模块化编程(Modularity)是编写好程序的关键。把程序分解成许多小模块,每一个模块通过良好定义的狭窄接口和别的模块交互作用(interact)。有了模块化,可以修正一部分程序中的错误而不考虑程序的其他部分,而且可以仅仅理解一部分程序而不必理解整个程序。没有模块化,程序是一大堆有着错综复杂的相互关系的部分的拼凑。很难去领悟和修改这样一个程序,同样也很难让它正常工作。</p>
<p>因此本书的重点在于创建模块化的程序:怎样把程序组织成一系列精心挑选的模块。本书认为模块化就是抽象(abstraction)。每一个模块意味着一个抽象,比如说指引一系列文档中的关键字的目录,或者在文档中使用目录来查找匹配某个问题的文档的过程。着重强调面向对象编程思想——在程序中使用数据抽象和对象的思想。</p>
<div class="line-block">
<div class="line">这本书使用Java作为它的编程示例的语言。我们没有假定读者已经熟悉Java。尽管可能没什么价值,但是本书中的思想是语言无关的,并且可以在任何语言的编程中使用。</div>
</div>
<div class="section" id="how-can-the-book-be-used">
<h3>怎样使用这本书? How Can the Book Be Used</h3>
<p>本书《程序开发原理》有两种使用方法。其一是作为课本教材,讲述如何用面向对象的方法来设计和实现复杂系统;其二是编程专家使用,帮助他们改善编程技能,增进他们的关于模块化和Object-Oriented(面向对象)设计的知识。</p>
<p>作为教材使用时,本书一般作为第二或第三门程序设计课程。我们已经在MIT使用本书很多年,给大一大二的本科生教授第二门编程课。在这一阶段,学生们已经知道怎样编写小程序。课程在两方面利用这一点:让学生更仔细地思考小程序,以及教他们如何利用小程序作为组件构建大型程序。这本书也可以在专业(如软件工程)后期教学中使用。</p>
<div class="line-block">
<div class="line">建立在本书基础上的课程适合于所有计算机科学专业。尽管许多学生可能永远不会成为真正的大型程序的设计师,他们可以在开发部门工作,在那儿他们负责设计和实现能与整个结构耦合的子系统。模块化设计的子系统是这种任务中心,这对那些从事大型程序设计任务的人来说也同样重要。</div>
</div>
</div>
<div class="section" id="what-is-this-book-about">
<h3>这本书讲什么?What Is This Book About</h3>
<div class="line-block">
<div class="line">通观全篇三分之二的书致力于讨论在构建独立的程序模块时产生的问题,剩下的部分讨论怎样运用这些模块构建大型程序。</div>
</div>
<div class="section" id="program-modules">
<h4>程序模块Program Modules</h4>
<p>这一部分的书集中讨论抽象机制(abstraction
mechanism)。它讨论procedure(子程序)和exception(异常),数据抽象,遍历(iteration)抽象,数据抽象系列(family)以及多态(polymorphic)抽象。</p>
<p>在对抽象的讨论中,三个步骤是重要的。首先是决定被抽象的东西到底是什么:它提供给它的用户哪些行为。创造抽象是设计的关键,因此本书讨论如何在众多选择中挑选,以及怎样才能创造出好的抽象。</p>
<p>第二步是通过为一个抽象制定一个规格(specification)来获取它的意义。如果没有一些描述,一个抽象就会含糊不清,而变得没有使用价值。specification则提供了需要的描述。本书定义了一种specification的格式,讨论了一份好的specification应有的属性,并且提供了许多示例。</p>
<p>第三步是实现抽象。本书讨论怎样设计一份实现,以及在简洁性和执行性能之间怎样权衡利弊。书中强调封装(encapsulation)的重要性以及在一份实现中履行规格中定义的行为的重要性。书中同样提供一些技术——尤其是不变式断言(representation
invariant)和抽象函数(abstraction
function)——来帮助读者理解代码和它的原因。不变式断言和抽象函数都实现到尽可能的程度,这对于除错和调试很有用。</p>
<p>关于类型层次(type
hierarchy)的材料注重讨论使用它作为抽象的技术——一种把相关联的一组数据抽象归入同一系列的技术。这里很重要的一点是,是否应当将一个类型作为另一个类型的子类。本书定义了替换原则——通过比较子类和父类的specification,来决定是否建立子类关系的方法<a class="reference external" href="#_ftn1">[1]</a>。</p>
<div class="line-block">
<div class="line">本书同样涉及除错和调试。书中讨论怎样得到足够数量的测试情况,来准备通过黑箱和白箱测试,它同样强调了复查(regression)测试的重要性。</div>
</div>
</div>
<div class="section" id="programming-in-the-large">
<h4>编写大型程序 Programming in the Large</h4>
<p>本书的其后部分讲解怎样用模块化的方法设计和实现大型程序。它建立在前文有关abstraction和specification的材料的基础之上。</p>
<p>编写大型程序涵盖四个主要议题。首先讲解需求分析——怎样才能领悟程序中需要什么。本书讨论怎样实施需求分析,也讨论书写产生的需求规格的方式,通过使用一种描述程序的抽象阶段的数据模型。使用这种模型将产生一份更为正式的specification,同时它也使需求检查更加严格,这样可以更好的领悟需求。</p>
<p>编写大型程序的第二项议题是程序设计,这通常是一个循序渐进的过程。设计过程围绕构建有用的抽象来组织,这些抽象作为整个程序之中理想的构建组建。这些抽象在设计时被仔细的编写规格,这样当程序实现时,那些实现抽象的模块可以独立地开发。这种设计使用设计笔记编写文档,包括描述整个程序结构的模块间依赖性的图示。</p>
<p>第三项议题是实现和测试。本书讨论了前置设计分析对于实现的必要性,以及怎样进行设计复审。它同样讨论了设计和实现的顺序。这一部分比较了自顶而下与自底而上的组织方式,讨论如何使用驱动程序和占位程序<a class="reference external" href="#_ftn2">[2]</a>(stub),并且强调了制定一个事先的顺序策略的必要性,以满足开发组织和客户的需求。</p>
<div class="line-block">
<div class="line">本书以一章设计模式(design pattern)结束。一些模式在前面的章节介绍过,比如遍历抽象是算法的主要组建。最后的章节讨论前文中没有涉及到的模式。希望它作为这一教材的介绍。有兴趣的读者可以继续阅读其它书中更完善的讨论<a class="reference external" href="#_ftn3">[3]</a>。</div>
</div>
<hr class="docutils"/>
<p><a class="reference external" href="#_ftnref1">[1]</a>
译注:如果子类的specification包括了所有父类的specification,就是说父类的要求也是子类的要求,或者子类的要求更为严格,那么可以建立父子关系。而替换原则的说法是,对于具有父子关系的类,任何需要一个父类对象的地方,都可以替换为一个子类对象。</p>
<p><a class="reference external" href="#_ftnref2">[2]</a>
译注:在测试某一组建时,由于其余组建还未实现,这一组建与其余组建的接口衔接部分无法工作。此时可以针对这一组建编写其余组建的占位程序(stub),预留出接口的衔接代码。占位代码通常不做任何有价值的事情,只报告组建的衔接部位工作正常。</p>
<p><a class="reference external" href="#_ftnref3">[3]</a> 译注:作者指的是设计模式的开山之作——《Design
Patterns—Elements of Reusable Object-Oriented
Software》,作者为设计模式界著名的“四人帮”GoF(Gang of
Four)。此书详尽讨论了三大类共23个广泛使用的设计模式的适用范围、依存关系、实现细节以及已有的应用领域等问题。书中以C++和Smalltalk为示例语言,不过书中所涉及的模式适用于所有面向对象的语言。</p>
</div>
</div>
</div>
C++ Tricks 3.2 标号、goto,以及switch的实现2007-09-16T13:08:00+09:002007-09-16T13:08:00+09:00farseerfctag:farseerfc.me,2007-09-16:/zhs/c-tricks-3-2-label-goto-and-implementation-of-switch.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="goto-switch">
<h2>3.2 标号、goto,以及switch的实现</h2>
<p>goto语句及标号(label)是最古老的C语言特性,也是最早被人们抛弃的语言特性之一。像汇编语言中的jmp指令一样,goto语句可以跳转到同一函数体中任何标号位置:</p>
<p>void f()</p>
<p>{int i=0;</p>
<p>Loop: //A label</p>
<p>++i;</p>
<p>if(i<10)goto Loop; //Jump to the label</p>
<p>}</p>
<p>在原始而和谐的早期Fortran和Basic时代,我们没有if then
else,没有for和while,甚至没有函数的概念,一切控制结构都靠goto(带条件的或无条件的)构件。软件工程师将这样的代码称作“意大利面条”代码。实践证明这样的代码极容易造成混乱。</p>
<p>自从证明了结构化的程序可以做意大利面条做到的任何事情,人们就开始不遗余力地推广结构化设计思想,将goto像猛兽一般囚禁在牢笼 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="goto-switch">
<h2>3.2 标号、goto,以及switch的实现</h2>
<p>goto语句及标号(label)是最古老的C语言特性,也是最早被人们抛弃的语言特性之一。像汇编语言中的jmp指令一样,goto语句可以跳转到同一函数体中任何标号位置:</p>
<p>void f()</p>
<p>{int i=0;</p>
<p>Loop: //A label</p>
<p>++i;</p>
<p>if(i<10)goto Loop; //Jump to the label</p>
<p>}</p>
<p>在原始而和谐的早期Fortran和Basic时代,我们没有if then
else,没有for和while,甚至没有函数的概念,一切控制结构都靠goto(带条件的或无条件的)构件。软件工程师将这样的代码称作“意大利面条”代码。实践证明这样的代码极容易造成混乱。</p>
<p>自从证明了结构化的程序可以做意大利面条做到的任何事情,人们就开始不遗余力地推广结构化设计思想,将goto像猛兽一般囚禁在牢笼,标号也因此消失。</p>
<p>标号唯一散发余热的地方,是在switch中控制分支流程。</p>
<p>很多人不甚了解switch存在的意义,认为它只是大型嵌套if then
else结构的缩略形式,并且比if语句多了很多“不合理”的限制。如果你了解到switch在编译器内部的实现机制,就不难理解强加在switch之上的诸多限制,比如case后只能跟一个编译期整型常量,比如用break结束每一个case。首先看一个switch实例:</p>
<p>switch (shape.getAngle())</p>
<p>{</p>
<p>case 3: cout<<”Triangle”;break;</p>
<p>case 4: cout<<”Square”;break;</p>
<p>case 0:case1: cout<<”Not a sharp!”;break;</p>
<p>default: cout<<”Polygon”;</p>
<p>}</p>
<p>任何程序员都可以写出与之对应的if结构:</p>
<p>int i= getAngle(shape);</p>
<p>if (i==3) cout<<”Triangle”;</p>
<p>else if(i==4) cout<<”Square”;</p>
<p>else if(i==0||i==1) cout<<”Not a sharp!”;</p>
<p>else cout<<”Polygon”;</p>
<p>看起来这两段代码在语义上是完全一样的,不是么?</p>
<p>不!或许代码的执行结果完全一样,但是就执行效率而言,switch版本的更快!</p>
<p>要了解为什么switch的更快,我们需要知道编译器是怎样生成switch的实现代码的:</p>
<p>首先,保留switch之后由{}括起来的语具体,仅将其中case、default和break替换为真正的标号:</p>
<p>switch (getAngle(shape))</p>
<p>{</p>
<p>_case_3: cout<<”Triangle”;goto _break;</p>
<p>_case_4: cout<<”Square”; goto _break;</p>
<p>_case_0:_case_1: cout<<”Not a sharp!”; goto _break;</p>
<p>_default: cout<<”Polygon”;</p>
<p>_break:</p>
<p>}</p>
<p>随后,对于所有出现在case之后的常量,列出一张只有goto的跳转表,其顺序按case后的常量排列:</p>
<p>goto _case_0;</p>
<p>goto _case_1;</p>
<p>goto _case_3;</p>
<p>goto _case_4;</p>
<p>然后,计算case之后的常量与跳转表地址之间的关系,如有需要,在跳转表中插入空缺的项目:</p>
<p>100105: goto _case_0;</p>
<p>100110: goto _case_1;</p>
<p>100115: goto _default; //因为没有case 2,所以插入此项以条转到default</p>
<p>100120: goto _case_3;</p>
<p>100125: goto _case_4;</p>
<p>假设一个goto语句占用5个字节,那么在本例中,goto的地址=case后的常量*5+100105</p>
<p>之后,生成跳转代码,在其余条件下跳转至default,在已知范围内按照公式跳转,全部的实现如下:</p>
<p>{</p>
<p>int i= getAngle(shape);</p>
<p>if (i<0||i>=5)goto _default;</p>
<p>i=i*5+100105; //按照得出的公式算出跳转地址</p>
<p>goto i; //伪代码,C中不允许跳转到整数,但是汇编允许</p>
<p>100105: goto _case_0;</p>
<p>100110: goto _case_1;</p>
<p>100115: goto _default;</p>
<p>100120: goto _case_3;</p>
<p>100125: goto _case_4;</p>
<p>_case_3: cout<<”Triangle”;goto _break;</p>
<p>_case_4: cout<<”Square”; goto _break;</p>
<p>_case_0:_case_1: cout<<”Not a sharp!”; goto _break;</p>
<p>_default: cout<<”Polygon”;</p>
<p>_break:</p>
<p>}</p>
<p>经过这样处理整个switch结构,使得无论switch后的变量为何值,都可以通过最多两次跳转到达目标代码。相比之下if版本的代码则采用线性的比较和跳转,在case语句很多的情况下效率极低。</p>
<p>由此,我们也可以知道,为什么case后跟的一定是编译期整型常数,因为编译器需要根据这个值制作跳转表。我们可以明白为什么case与case之间应该用break分隔,因为编译器不改变switch语句体的结构,case其本身只是一个具有语义的标号而已,要想跳出switch,就必须用break语句。</p>
</div>
C++ Tricks 3.1 左值右值与常量性(lvalue,rvalue & constant)2007-09-16T13:07:00+09:002007-09-16T13:07:00+09:00farseerfctag:farseerfc.me,2007-09-16:/zhs/c-tricks-3-1-lvalue-rvalue-constant.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="lvalue-rvalue-constant">
<h2>3.1 左值右值与常量性(lvalue,rvalue & constant)</h2>
<p>首先要搞清楚的是,什么是左值,什么是右值。这里给出左值右值的定义:</p>
<p>1、左值是可以出现在等号(=)左边的值,右值是只能出现在等号右边的值。</p>
<p>2、左值是可读可写的值,右值是只读的值。</p>
<p>3、左值有地址,右值没有地址。</p>
<p>根据左值右值的第二定义,值的左右性就是值的常量性——常量是右值,非常量是左值。比如:</p>
<p>1=1;//Error</p>
<p>这个复制操作在C++中是语法错误,MSVC给出的错误提示为“error C2106: '=' :
left operand must be
l-value”,就是说’=’的左操作数必须是一个左值,而字面常数1是一个右值。可见,严格的区分左值右值可以从语法分析的角度找出程序的逻辑错误。</p>
<p>根据第二定义,一个左值也是一个右值 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="lvalue-rvalue-constant">
<h2>3.1 左值右值与常量性(lvalue,rvalue & constant)</h2>
<p>首先要搞清楚的是,什么是左值,什么是右值。这里给出左值右值的定义:</p>
<p>1、左值是可以出现在等号(=)左边的值,右值是只能出现在等号右边的值。</p>
<p>2、左值是可读可写的值,右值是只读的值。</p>
<p>3、左值有地址,右值没有地址。</p>
<p>根据左值右值的第二定义,值的左右性就是值的常量性——常量是右值,非常量是左值。比如:</p>
<p>1=1;//Error</p>
<p>这个复制操作在C++中是语法错误,MSVC给出的错误提示为“error C2106: '=' :
left operand must be
l-value”,就是说’=’的左操作数必须是一个左值,而字面常数1是一个右值。可见,严格的区分左值右值可以从语法分析的角度找出程序的逻辑错误。</p>
<p>根据第二定义,一个左值也是一个右值,因为左值也可读,而一个右值不是一个左值,因为右值不可写。</p>
<p>通常情况下,声明的变量是一个左值,除非你指定const将它变成一个右值:</p>
<p>int lv=1;</p>
<p>const int rv=lv;</p>
<p>由于右值的值在程序执行期间不能改变,所以必须用另一个右值初始化它。</p>
<p>一个普通变量只能用右值初始化,如果你想传递左值,必须声明一个引用或一个指针:</p>
<p>int & ref=lv;//用引用传递左值</p>
<p>int * plv=&lv;//传递指针以间接传递左值</p>
<p>必须用左值初始化引用,然而,可以用右值初始化常量引用:</p>
<p>int & r1=1; //Error!</p>
<p>const int & r2=1; //OK</p>
<p>这实际上相当于:</p>
<p>int _r2=1;</p>
<p>const int & r2=_r2;</p>
<p>这样的写法在函数体内没什么作用,但是在传递函数参数时,它可以避免潜在的(传递左值时的)复制操作,同时又可以接受右值。</p>
<p>通常情况下,函数的参数和返回值都只传回右值,除非你明确的通过引用传递左值。</p>
<p>明确了左值与右值的区别,有助于我们写函数时确定什么时候应该有const,什么时候不该有。比如,我们写了一个代表数学中复数的类Complex:</p>
<p>class Complex;</p>
<p>然后,我们写针对Complex的运算符重载:operator+和operator=。问题在于,参数和返回值应该是什么类型,可选类型有四种:
Complex、const Complex、Complex&、const Complex&。</p>
<p>对于operator+,我们不会改变参数的值,所以可以通过const
Complex&传递参数。至于返回值类型,由于int类型的加法返回右值,所以根据Do
as the ints do的原则,返回值类型为const Complex:</p>
<p>const Complex operator+(const Complex&,const Complex&);</p>
<p>对于operator=,同样要思考这些问题。我们写入第一个参数,所以第一个参数为Complex&,我们只读取第二个参数,所以第二个参数为const
Complex&。至于返回值,还是Do as the ints
do。int的赋值返回左值,不信你可以试一试:</p>
<p>int i;</p>
<p>(i=1)=2;</p>
<p>虽然比较傻,先将i赋为1,再将其改为2,但是这是被C++语法支持的做法,我们就理应遵守。所以返回第一个参数的左值:</p>
<p>Complex& operator=(Complex&,const Complex&);</p>
<p>const是C++引入的语言特性,也被ANSI
C99借鉴,在经典版本的C语言中是没有的。关于const的历史,有几点值得玩味。最初Bjarne
Stroustrup引入const时,可写性是和可读性分开的。那时使用关键字readonly和writeonly。这个特点被首先提交到C的ANSI标准化委员会(当时还没有C++标准化的计划),但是ANSI
C标准只接受了readonly的概念,并将其命名为const。随后,有人发现在多线程同步的环境下,有些变量的值会在编译器的预料之外改变,为了防止过度优化破坏这些变量,C++又引入关键字violate。从语义特点来看,violate是const的反义词,因为const表示不会变的量,而violate表示会不按照预期自行变化的量。从语法特点而言,violate与const是极为相似的,适用于const的一切语法规则同样适用于violate。</p>
<p>值的常量性可以被划分为两种:编译期常量和运行期常量。C++语法并没有严格区分这两种常量,导致了少许混乱:</p>
<p>const int i=5;const int * pi=&i;</p>
<p>const_cast<int&>i=1;//对于运行期常量,在需要时可以去除它的常量性</p>
<p>int a[i];//对于编译期常量,可以用它来指定数组大小</p>
<p>cout<<i<<sizeof(a)/sizeof(a[0])<<*pi;</p>
<p>这种将编译期与运行期常量的特性混用的方法,势必导致语义的混乱。数组a的大小最终是5,因为采用了i的编译期值,而不管i在运行期是否被改变了值。最后一句代码将(有可能)输出551,第一个i的值作为一种优化在编译期绑定,第二个值标明了a的大小,第三个值通过指针显示地输出i的运行期真实值。</p>
<p>在C++的近亲C#的语法中,这两种常量被严格地区分开:编译期常量由const指定,只能是内建类型变量;运行期常量由readonly指定,可以是任何类型。永远不会改变的常量,如圆周率pi的值,应该用const声明;而其它有可能改变的常量,皆由readonly声明。</p>
<p>C++中的const的特点更倾向于C#中的readonly,虽然语法上允许使用const的编译期常量性,但正如上文所展示的,这容易造成混乱。为了得到C#中const的语义,在C++中,我们不必回归恶魔#define的怀抱,可以使用所谓“匿名enum技巧”。当匿名声明一个enum类型时,其中的枚举值就是一个int类型的编译期常量,比如:</p>
<p>enum{Size=5;};</p>
<p>int a[Size];</p>
<p>这种使用匿名enum来声明编译期常量的做法,被广泛应用于STL、boost等模板库的实现代码中。</p>
</div>
C++ Tricks 2.2 I386平台的内存布局2007-08-28T14:30:00+09:002007-08-28T14:30:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-2-i386-memory-layout.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386">
<h2>2.2 I386平台的内存布局</h2>
<p>众所周知,I386是32位体系结构。因此对于绝大多数I386平台的C++编译器而言,sizeof(int)=sizeof(long)=sizeof(void*)=4。当然C++标准对此没有任何保证,我们也不应该试图编写依赖于此的代码。</p>
<div class="line-block">
<div class="line">32位指针的可寻址空间为4GB。为充分利用这么大的寻址空间,也是为了支持其它更先进的技术比如多任务技术或者动态链接库技术,WinNT使用虚拟内存技术,给与每个应用程序全部4GB的内存空间。4GB的地址被一分为二,前2GB供应用程序自己使用,后2GB由系统内核分配和管理。这2GB的内存地址,通常被划分成3种内存区使用:</div>
</div>
<div class="section" id="id1">
<h3>1 代码及静态数据区</h3>
<p>由代码加载器从动态链接库镜像(通常是exe或dll文件)加载,通常定位到镜像文件中指定的基址开始的内存区。如果基址所在内存已被占用,动态连接器会将代码或数据重定向到其它可用地址。</p>
<div class="line-block">
<div class="line">在C++中,静态数据包括:名字空间(namespace)和全局(global)对象、函数的static对象、类的static数据成员 …</div></div></div></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386">
<h2>2.2 I386平台的内存布局</h2>
<p>众所周知,I386是32位体系结构。因此对于绝大多数I386平台的C++编译器而言,sizeof(int)=sizeof(long)=sizeof(void*)=4。当然C++标准对此没有任何保证,我们也不应该试图编写依赖于此的代码。</p>
<div class="line-block">
<div class="line">32位指针的可寻址空间为4GB。为充分利用这么大的寻址空间,也是为了支持其它更先进的技术比如多任务技术或者动态链接库技术,WinNT使用虚拟内存技术,给与每个应用程序全部4GB的内存空间。4GB的地址被一分为二,前2GB供应用程序自己使用,后2GB由系统内核分配和管理。这2GB的内存地址,通常被划分成3种内存区使用:</div>
</div>
<div class="section" id="id1">
<h3>1 代码及静态数据区</h3>
<p>由代码加载器从动态链接库镜像(通常是exe或dll文件)加载,通常定位到镜像文件中指定的基址开始的内存区。如果基址所在内存已被占用,动态连接器会将代码或数据重定向到其它可用地址。</p>
<div class="line-block">
<div class="line">在C++中,静态数据包括:名字空间(namespace)和全局(global)对象、函数的static对象、类的static数据成员。这些静态数据由编译器分配地址(但可能被重定向),由静态连接器写入代码文件(通常是exe或dll)的静态数据区段。所以标准说,这些静态数据在编译期就已经具有地址。</div>
</div>
</div>
<div class="section" id="stack">
<h3>2 栈(Stack)</h3>
<p>栈是最常用的动态数据存储区,所有函数的non-static对象和函数参数都在程序运行期在栈上分配内存。在数据结构中,术语“栈(Stack)”意指先进后出(FILO,First
In Last
Out),与“队列(Queue)”所指的FIFO相对。相对于基于堆的对象分配技术,默认使用栈的对象分配有两点优势:</p>
<p>一、栈的FILO与人的思维方式相同</p>
<p>现实生活中有许多事例都使用FILO的方式,比如人们必须先提起话筒再拨打号码,而后挂断电话之后再放下话筒。使用FILO的栈,可以保证事物的销毁顺序以其诞生顺序相反的顺序进行,不会产生在挂断电话之前就放下话筒的尴尬。</p>
<p>二、栈的分配管理仅需要两个额外指针:栈顶(esp)和栈底(ebp)指针</p>
<p>从实现的技术层面而言,栈的管理比其它动态分配技术要简单很多。I386平台上的动态栈管理,仅需要栈顶和栈底两个指针。这两个指针的存储显然不能放置于栈中,置于静态数据区又有损效率。I386平台为管理动态栈专门预留了两个通用寄存器变量esp与ebp,分别代表栈顶(esp,Extended
Stack Pointer)与栈底(Extended Bottom
Pointer)指针。其中的extended代表它们是32位指针,以区分16位的sp和bp寄存器。</p>
<div class="line-block">
<div class="line">栈是动态存储区的特点,表明它的内存占用将随着程序的运行而变化。I386平台上WinNT将应用程序的栈置于程序空间,向下增长。程序初始化时,由操作系统将esp指向系统分配的栈空间的顶部。当程序需要在栈上分配变量时,就将esp减去变量所需字节数,这被称作“压栈(Push)”;随后又要销毁变量时,就将esp加上变量所需字节数,这被称作“弹出(Pop)”。esp与ebp两者之间所夹的空间,就是当前函数正在使用的栈空间。由于栈向下增长,esp(栈顶)的值总是小于ebp(栈底)的值,新分配的变量地址总是小于旧变量的地址。</div>
</div>
</div>
<div class="section" id="heap">
<h3>3 堆(Heap)和自由存储区</h3>
<p>栈中的变量对于分配与释放的顺序有特定要求,这在一定程度上限制了栈的适用范围。面向对象(OO,Object
Oriented)的程序设计思想也要求能自由地控制变量的分配与销毁。由此,现代操作系统都提供了被称作“堆(Heap)”的自由存储区,以允许由程序员控制的对象创建和销毁过程。C标准库函数malloc和free则是对操作系统提供的堆操作的封装。C++提供的自由存储区运算符new和delete则通常是malloc和free的又一层封装。</p>
<p>操作系统经由malloc和free控制对堆的访问。堆的存储管理技术各不相同,简单的使用双链表管理,复杂的可以比拟一个完整的文件系统。</p>
<p>由于额外的管理需求,使用系统提供的通用分配器在堆上分配和销毁变量的代价,无论从空间角度还是效率角度而言,都比在栈上分配对象要高昂很多。对于sizeof上百的大型对象,这样的高昂代价还是可以接受的,但是对于sizeof只有个位数的小对象,这样的代价通常是一个数量级的差距。正因为这个原因,STL不使用new和delete,转而使用分配子(alllocor)分配对象。</p>
</div>
</div>
C++ Tricks2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p><strong>C++ Tricks</strong></p>
<p><strong>By FarseerFc</strong></p>
<p>从今天起,我再将在<a class="reference external" href="http://firechildren.spaces.live.com/">Live
Space</a>和<a class="reference external" href="http://76635424.qzone.qq.com/">QQZone</a>同时发表一系列文章,暂定名为“C++Tricks”。</p>
<p>本文旨在记录和阐述一些本人学习C++时所得的心得、技巧。总体来看,本文涉及的内容是每一个C++程序员都应该知道的,但是很少见诸C++教材。希望对各位同仁学习C++有所帮助。</p>
<div class="line-block">
<div class="line">也可以通过QQ或MSN向我索要此文的DOC版或PDF版,会比网页上的更新的快一点。</div>
</div>
<div class="section" id="lexical-problems">
<h2>1 词法问题(Lexical Problems)</h2>
<div class="line-block">
<div class="line"><br/></div>
</div>
<div class="section" id="id1">
<h3><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!139.entry">1.1 条件运算符(?:)</a></h3>
</div>
<div class="section" id="id2">
<h3><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!140.entry">1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱</a></h3>
</div>
<div class="section" id="x86">
<h3>2 X86体系结构</h3>
<div class="section" id="id3">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!146.entry">2.1 X86概述</a></h4>
</div>
<div class="section" id="i386">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!147.entry">2.2 …</a></h4></div></div></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p><strong>C++ Tricks</strong></p>
<p><strong>By FarseerFc</strong></p>
<p>从今天起,我再将在<a class="reference external" href="http://firechildren.spaces.live.com/">Live
Space</a>和<a class="reference external" href="http://76635424.qzone.qq.com/">QQZone</a>同时发表一系列文章,暂定名为“C++Tricks”。</p>
<p>本文旨在记录和阐述一些本人学习C++时所得的心得、技巧。总体来看,本文涉及的内容是每一个C++程序员都应该知道的,但是很少见诸C++教材。希望对各位同仁学习C++有所帮助。</p>
<div class="line-block">
<div class="line">也可以通过QQ或MSN向我索要此文的DOC版或PDF版,会比网页上的更新的快一点。</div>
</div>
<div class="section" id="lexical-problems">
<h2>1 词法问题(Lexical Problems)</h2>
<div class="line-block">
<div class="line"><br/></div>
</div>
<div class="section" id="id1">
<h3><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!139.entry">1.1 条件运算符(?:)</a></h3>
</div>
<div class="section" id="id2">
<h3><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!140.entry">1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱</a></h3>
</div>
<div class="section" id="x86">
<h3>2 X86体系结构</h3>
<div class="section" id="id3">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!146.entry">2.1 X86概述</a></h4>
</div>
<div class="section" id="i386">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!147.entry">2.2 I386平台的内存布局</a></h4>
</div>
<div class="section" id="i386c">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!148.entry">2.3 I386平台C函数内部的栈分配</a></h4>
</div>
<div class="section" id="id4">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!149.entry">2.4 I386平台C函数调用边界的栈分配</a></h4>
</div>
<div class="section" id="i386-align">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!150.entry">2.5 I386平台的边界对齐(Align)</a></h4>
</div>
<div class="section" id="i386c-variable-arguments">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!151.entry">2.6 I386平台C函数的可变参数表(Variable Arguments)</a></h4>
</div>
<div class="section" id="id5">
<h4> <a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!152.entry">2.7 I386平台的其它函数调用模型</a></h4>
<div class="section" id="id6">
<h5>3 过程式编程</h5>
</div>
</div>
<div class="section" id="lvalue-rvalue-constant">
<h4><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!154.entry">3.1 左值右值与常量性(lvalue,rvalue & constant)</a></h4>
</div>
<div class="section" id="goto-switch">
<h4><a class="reference external" href="http://firechildren.spaces.live.com/blog/cns!1D8C01E7131A5AD4!155.entry">3.2 标号、goto,以及switch的实现</a></h4>
</div>
</div>
</div>
C++ Tricks 2.3 I386平台C函数内部的栈分配2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-3-i386-stack-allocation-in-c-functions.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c">
<h2>2.3 I386平台C函数内部的栈分配</h2>
<p>函数使用栈来保存局部变量,传递函数参数。进入函数时,函数在栈上为函数中的变量统一预留栈空间,将esp减去相应字节数。当函数执行流程途径变量声明语句时,如有需要就调用相应构造函数将变量初始化。当执行流程即将离开声明所在代码块时,以初始化的顺序的相反顺序逐一调用析构函数。当执行流程离开函数体时,将esp加上相应字节数,归还栈空间。</p>
<p>为了访问函数变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp的值保存在ebp中,并事先将ebp的值压栈。随后,在函数体中通过ebp减去偏移量来访问变量。以一个最简单的函数为例:</p>
<p>void f()</p>
<p>{</p>
<p>int a=0; //a的地址被分配为ebp-4</p>
<p>char c=1; //c的地址被分配为ebp-8</p>
<p>}</p>
<p>产生的汇编代码为:</p>
<p>push ebp ;将ebp压栈</p>
<p>mov ebp,esp ;ebp=esp 用栈底备份栈顶指针</p>
<p>sub …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c">
<h2>2.3 I386平台C函数内部的栈分配</h2>
<p>函数使用栈来保存局部变量,传递函数参数。进入函数时,函数在栈上为函数中的变量统一预留栈空间,将esp减去相应字节数。当函数执行流程途径变量声明语句时,如有需要就调用相应构造函数将变量初始化。当执行流程即将离开声明所在代码块时,以初始化的顺序的相反顺序逐一调用析构函数。当执行流程离开函数体时,将esp加上相应字节数,归还栈空间。</p>
<p>为了访问函数变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp的值保存在ebp中,并事先将ebp的值压栈。随后,在函数体中通过ebp减去偏移量来访问变量。以一个最简单的函数为例:</p>
<p>void f()</p>
<p>{</p>
<p>int a=0; //a的地址被分配为ebp-4</p>
<p>char c=1; //c的地址被分配为ebp-8</p>
<p>}</p>
<p>产生的汇编代码为:</p>
<p>push ebp ;将ebp压栈</p>
<p>mov ebp,esp ;ebp=esp 用栈底备份栈顶指针</p>
<p>sub esp,8 ;esp-=8,为a和c预留空间,包括边界对齐</p>
<p>mov dword ptr[ebp-4],0 ;a=0</p>
<p>mov byte ptr[ebp-8],1 ;c=1</p>
<p>add esp,8 ;esp+=8,归还a和c的空间</p>
<p>mov esp,ebp ;esp=ebp 从栈底恢复栈顶指针</p>
<p>pop ebp ;恢复ebp</p>
<p>ret ;返回</p>
<p>相应的内存布局是这样:</p>
<p>09992:c=1 <-esp</p>
<p>09996:a=0</p>
<p>10000:旧ebp <-ebp</p>
<p>10004:……</p>
<p>注:汇编中的pop、push、call、ret语句是栈操作指令,其功能可以用普通指令替换</p>
<p>push ebp相当于:</p>
<p>add esp,4</p>
<p>mov dword ptr[esp],ebp</p>
<p>pop ebp相当于:</p>
<p>mov ebp,dword ptr[esp]</p>
<p>sub esp,4</p>
<p>call fun_address相当于:</p>
<p>push eip</p>
<p>jmp fun_address</p>
<p>ret相当于</p>
<p>add esp,4</p>
<p>jmp dword ptr[esp-4]</p>
<p>带参数的ret</p>
<p>ret 8相当于</p>
<p>add esp,12</p>
<p>jmp dword ptr[esp-4]</p>
<p>所有局部变量都在栈中由函数统一分配,形成了类似逆序数组的结构,可以通过指针逐一访问。这一特点具有很多有趣性质,比如,考虑如下函数,找出其中的错误及其造成的结果:</p>
<p>void f()</p>
<p>{</p>
<p>int i,a[10];</p>
<p>for(i=0;i<=10;++i)a[i]=0;/An error occurs here!</p>
<p>}</p>
<p>这个函数中包含的错误,即使是C++新手也很容易发现,这是老生常谈的越界访问问题。但是这个错误造成的结果,是很多人没有想到的。这次的越界访问,并不会像很多新手预料的那样造成一个“非法操作”消息,也不会像很多老手估计的那样会默不作声,而是导致一个,呃,死循环!</p>
<p>错误的本质显而易见,我们访问了a[10],但是a[10]并不存在。C++标准对于越界访问只是说“未定义操作”。我们知道,a[10]是数组a所在位置之后的一个位置,但问题是,是谁在这个位置上。是i!</p>
<p>根据前面的讨论,i在数组a之前被声明,所以在a之前分配在栈上。但是,I386上栈是向下增长的,所以,a的地址低于i的地址。其结果是在循环的最后,a[i]引用到了i自己!接下来的事情就不难预见了,a[i],也就是i,被重置为0,然后继续循环的条件仍然成立……这个循环会一直继续下去,直到在你的帐单上产生高额电费,直到耗光地球电能,直到太阳停止燃烧……呵呵,或者直到聪明的你把程序Kill了……</p>
</div>
C++ Tricks 2.4 I386平台C函数调用边界的栈分配2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-4-i386-stack-allocation-accross-function-invocation.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c">
<h2>2.4 I386平台C函数调用边界的栈分配</h2>
<p>当调用一个函数时,主调函数将参数以声明中相反的顺序压栈,然后将当前的代码执行指针(eip)压栈,然后跳转到被调函数的入口点。在被调函数中,通过将ebp加上一个偏移量来访问函数参数,以声明中的顺序(即压栈的相反顺序)来确定参数偏移量。被调函数返回时,弹出主调函数压在栈中的代码执行指针,跳回主调函数。再由主调函数恢复到调用前的栈。</p>
<p>函数的返回值不同于函数参数,通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,通过eax寄存器传递。如果返回值类型是64位变量,如_int64,同过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是用户自定义结构,或C++类类型,通过修改函数签名,以引用型参数的形式传回。</p>
<p>同样以最简单的函数为例:</p>
<p>void f(){</p>
<p>int i …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c">
<h2>2.4 I386平台C函数调用边界的栈分配</h2>
<p>当调用一个函数时,主调函数将参数以声明中相反的顺序压栈,然后将当前的代码执行指针(eip)压栈,然后跳转到被调函数的入口点。在被调函数中,通过将ebp加上一个偏移量来访问函数参数,以声明中的顺序(即压栈的相反顺序)来确定参数偏移量。被调函数返回时,弹出主调函数压在栈中的代码执行指针,跳回主调函数。再由主调函数恢复到调用前的栈。</p>
<p>函数的返回值不同于函数参数,通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,通过eax寄存器传递。如果返回值类型是64位变量,如_int64,同过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是用户自定义结构,或C++类类型,通过修改函数签名,以引用型参数的形式传回。</p>
<p>同样以最简单的函数为例:</p>
<p>void f(){</p>
<p>int i=g(1,2);</p>
<p>}</p>
<p>int g(int a,int b){</p>
<p>int c=a+b;</p>
<p>return c;</p>
<p>}</p>
<p>产生的汇编代码如下:</p>
<p>f:</p>
<p>push ebp ;备份ebp</p>
<p>mov ebp,esp ;建立栈底</p>
<p>sub esp,4 ;为i分配空间</p>
<p>mov eax,2 ;准备参数b的值2</p>
<p>push eax ;将b压栈</p>
<p>mov eax,1 ;准备参数a的值1</p>
<p>push eax ;将a压栈</p>
<p>call g ;调用g</p>
<p>add esp,8 ;将a和b一起弹出,恢复调用前的栈</p>
<p>mov dword ptr[ebp-4],eax ;将返回值保存进变量i</p>
<p>mov esp,ebp ;恢复栈顶</p>
<p>pop ebp ;恢复栈底</p>
<p>g:</p>
<p>push ebp ;备份ebp</p>
<p>mov ebp,esp ;建立栈底</p>
<p>sub esp,4 ;为局部变量c在栈中分配内存</p>
<p>mov eax,dword ptr[ebp+8] ;通过ebp间接读取参数a的值</p>
<p>mov ebx,dword ptr[ebp+12] ;通过ebp间接读取参数b的值</p>
<p>add eax,ebx ;将a和b的值相加,之和存在eax中</p>
<p>mov dword ptr[ebp-4],eax ;将和存入变量c</p>
<p>mov eax,dword ptr[ebp-4] ;将c作为返回值,代码优化后会删除此句</p>
<p>add esp,4 ;销毁c的内存</p>
<p>mov esp,ebp ;恢复栈顶</p>
<p>pop ebp ;恢复栈底</p>
<p>ret ;返回函数f</p>
<p>栈的内存布局如下:</p>
<p>100076:c <- g的esp</p>
<p>100080:f的ebp=100100 <- g的ebp</p>
<p>100084:f的eip</p>
<p>100088:a=1</p>
<p>100092:b=2</p>
<p>100096:i</p>
<p>100100:旧ebp <-f的ebp</p>
<p>100104:……</p>
<p>注意在函数g的汇编代码中,访问函数的局部变量和访问函数参数的区别。局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。</p>
<p>由于函数返回值通过寄存器返回,不需要空间分配等操作,所以返回值的代价很低。基于这个原因,旧的C语法约定,不写明返回值类型的函数,返回值类型为int。这一规则与现行的C++语法相违背,因为C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。这种语法不兼容性是为了加强C++的类型安全,但同时也带来了一些问题。</p>
</div>
C++ Tricks 2.5 I386平台的边界对齐(Align)2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-5-address-alignment.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386-align">
<h2>2.5 I386平台的边界对齐(Align)</h2>
<p>首先提问,既然I386上sizeof(int)==4、sizeof(char)==1,那么如下结构(struct)A的sizeof是多少?</p>
<p>struct A{int i;char c;};</p>
<p>答案是sizeof(A)==8……1+5=8?</p>
<p>呵呵,这就是I386上的边界对齐问题。我们知道,I386上有整整4GB的地址空间,不过并不是每一个字节上都可以放置任何东西的。由于内存总线带宽等等的技术原因,很多体系结构都要求内存中的变量被放置于某一个边界的地址上。如果违反这个要求,重则导致停机出错,轻则减慢运行速度。对于I386平台而言,类型为T的变量必须放置在sizeof(T)的整数倍的地址上,char可以随便放置,short必须放在2的整数倍的地址上,int必须放在4的整数倍的地址上,double必须放在8的整数倍的地址上。如果违反边界对齐要求 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386-align">
<h2>2.5 I386平台的边界对齐(Align)</h2>
<p>首先提问,既然I386上sizeof(int)==4、sizeof(char)==1,那么如下结构(struct)A的sizeof是多少?</p>
<p>struct A{int i;char c;};</p>
<p>答案是sizeof(A)==8……1+5=8?</p>
<p>呵呵,这就是I386上的边界对齐问题。我们知道,I386上有整整4GB的地址空间,不过并不是每一个字节上都可以放置任何东西的。由于内存总线带宽等等的技术原因,很多体系结构都要求内存中的变量被放置于某一个边界的地址上。如果违反这个要求,重则导致停机出错,轻则减慢运行速度。对于I386平台而言,类型为T的变量必须放置在sizeof(T)的整数倍的地址上,char可以随便放置,short必须放在2的整数倍的地址上,int必须放在4的整数倍的地址上,double必须放在8的整数倍的地址上。如果违反边界对齐要求,从内存中读取数据必须进行两次,然后将独到的两半数据拼接起来,这会严重影响效率。</p>
<p>由于边界对齐问题的要求,在计算struct的sizeof的时候,编译器必须算入额外的字节填充,以保证每一个变量都能自然对齐。比如如下声明的struct:</p>
<p>struct WASTE</p>
<p>{</p>
<p>char c1;</p>
<p>int i;</p>
<p>char c2;</p>
<p>}</p>
<p>实际上相当于声明了这样一个结构:</p>
<p>struct WASTE</p>
<p>{</p>
<p>char c1;</p>
<p>char _filling1 [3];//三个字节填充,保证下一个int的对齐</p>
<p>int i;</p>
<p>char c2;</p>
<p>char _filling2 [3];//又三个字节填充</p>
<p>}</p>
<p>值得注意的是尾部的3个字节填充,这是为了可以在一个数组中声明WASTE变量,并且每一个都自然对齐。因为有了这些填充,所以sizeof(WASTE)==12。这是一种浪费,因为只要我们重新安排变量的声明,就可以减少sizeof:</p>
<p>struct WASTE</p>
<p>{</p>
<p>int i;</p>
<p>char c1,c2;</p>
<p>}</p>
<p>像这样的安排,sizeof就减少到8,只有2个字节的额外填充。为了与汇编代码相兼容,C语言语法规定,编译器无权擅自安排结构体内变量的布局顺序,必须从左向右逐一排列。所以,妥当安排成员顺序以避免内存空间的浪费,就成了我们程序员的责任之一。一般的,总是将结构体的成员按照其sizeof从大到小排列,double在最前,char在最后,这样总可以将结构的字节填充降至最小。</p>
<p>C++继承了C语言关于结构体布局的规定,所以以上的布局准则也适用于C++的class的成员变量。C++进一步扩展了布局规定,同一访问区段(private、public、protected)中的变量,编译器无权重新排列,不过编译器有权排列访问区段的前后顺序。基于这个规则,C++中有的程序员建议给每一个成员变量放在单独区段,在每一个成员声明之前都加上private:、public:、protected:标志,这可以最大限度的利用编译器的决策优势。</p>
<p>在栈中按顺序分配的变量,其边界也受到对齐要求的限制。与在结构中不同的是,栈中的变量还必须保证其后续变量无论是何种类型都可以自由对齐,所以在栈中的变量通常都有平台相关的对齐最小值。在MSVC编译器上,这个最小值可以由宏_INTSIZEOF(T)查询:</p>
<p>#define _INTSIZEOF(T) ( (sizeof(T) + sizeof(int) - 1) & ~(sizeof(int) -
1) )</p>
<p>_INTSIZEOF(T)会将sizeof(T)进位到sizeof(int)的整数倍。</p>
<p>由于在栈中分配变量使用_INTSIZEOF而不是sizeof,在栈上连续分配多个小变量(sizeof小于int的变量)会造成内存浪费,不如使用结构(struct)或数组。也就是说:</p>
<p>char c1,c2,c3,c4;//使用16字节</p>
<p>char c[4];//使用4字节</p>
<p>当然,使用数组的方法在访问数组变量(比如c[1])时有一次额外的指针运算和提领(dereference)操作,这会有执行效率的损失。这又是一种空间(内存占用)vs时间(执行效率)的折中,需要程序员自己根据情况权衡利弊。</p>
<p>sizeof的大小可能比我们预期的大,也可能比我们预期的小。对于空类:</p>
<p>class Empty {};</p>
<p>在通常情况下,sizeof(Empty)至少为1。这是因为C++语法规定,对于任何实体类型的两个变量,都必须具有不同的地址。为了符合语法要求,编译器会给Empty加入1字节的填充。所以sizeof()的值不可能出现0的情况。可是对于以下的类声明:</p>
<p>class A:public Empty{vitual ~A(){}};</p>
<p>sizeof(A)有可能是6,也有可能是5,也有可能是4!必不可少的四个字节是一个指向虚函数表的指针。一个可能有的字节是Empty的大小,这是是因为编译器在特定情况下会将Empty视作一个“空基类”,从而实施“空基类优化”,省掉那毫无作用的一字节填充。另一个字节是A的一字节填充,因为从语法上讲,A没有成员声明,理应有1字节填充,而从语义上讲,编译器给A的声明加入了一个指向虚函数表的指针,从而A就不再是一个“空类”,是否实施这个优化,要看编译器作者对语法措词的理解。也就是说,sizeof也会出现4+1+1=4的情况。具体要看编译器有没有实施“空基类优化”和“含虚函数表的空类优化”。</p>
<p>结构和类的空间中可能有填充的字节,这意味着填充字节中可能有数值,虽然这数值并不影响结构的逻辑状态,但是它也可能不知不觉中影响到你。比如说,你手头正好有一组依赖于底层硬件(比如多处理器)的函数,他们在操纵连续字节时比手动编码要快很多,而你想充分利用这种硬件优势:</p>
<p>bool BitCompare(void* begin,void* end,void* another);</p>
<p>这个函数将区间[begin,end)之间的字节与another开始的字节相比较,如果有一位不同就返回false,否则返回true。</p>
<p>比如你想将这个函数用于你自己的类的operator==中,这样可以利用硬件加快速度。不过你在动手前要充分考虑,你的class是否真的要比较每一位。如果在类的成员中存在编译器填充的字节数,那么应用以上的函数就是不正确的,因为填充的字节中可以有不同的值。为了保证你可以用Bitwise
Compare,你必须确保填充的字节中的值也是相同的。这不仅要求你在类的构造函数中初始化类的每一bit而不是每一个成员,也要求你在复制初始化和复制赋值函数中也同时保证bitwise
copy语义,而不是编译器默认产生的memberwise语义。当然,你可能通过与BitCompare一同提供的BitCopy来完成这个艰巨的任务。</p>
</div>
C++ Tricks 2.6 I386平台C函数的可变参数表(Variable Arguments)2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-6-i386-variable-arguments.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c-variable-arguments">
<h2>2.6 I386平台C函数的可变参数表(Variable Arguments)</h2>
<p>基于前文(2.4节)分析,我们可以不通过函数签名,直接通过指针运算,来得到函数的参数。由于参数的压栈和弹出操作都由主调函数进行,所以被调函数对于参数的真实数量不需要知晓。因此,函数签名中的变量声明不是必需的。为了支持这种参数使用形式,C语言提供可变参数表。可变参数表的语法形式是在参数表末尾添加三个句点形成的省略号“...”:</p>
<p>void g(int a,char* c,...);</p>
<p>省略号之前的逗号是可选的,并不影响词法语法分析。上面的函数g可以接受2个或2个以上的参数,前两个参数的类型固定,其后的参数类型未知,参数的个数也未知。为了知道参数个数,我们必须通过其他方法,比如通过第一个参数传递:</p>
<p>g(3,”Hello”,2,4,5);//调用g并传递5个参数,其中后3个为可变参数。</p>
<p>在函数的实现代码中,可以通过2.4节叙述的 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386c-variable-arguments">
<h2>2.6 I386平台C函数的可变参数表(Variable Arguments)</h2>
<p>基于前文(2.4节)分析,我们可以不通过函数签名,直接通过指针运算,来得到函数的参数。由于参数的压栈和弹出操作都由主调函数进行,所以被调函数对于参数的真实数量不需要知晓。因此,函数签名中的变量声明不是必需的。为了支持这种参数使用形式,C语言提供可变参数表。可变参数表的语法形式是在参数表末尾添加三个句点形成的省略号“...”:</p>
<p>void g(int a,char* c,...);</p>
<p>省略号之前的逗号是可选的,并不影响词法语法分析。上面的函数g可以接受2个或2个以上的参数,前两个参数的类型固定,其后的参数类型未知,参数的个数也未知。为了知道参数个数,我们必须通过其他方法,比如通过第一个参数传递:</p>
<p>g(3,”Hello”,2,4,5);//调用g并传递5个参数,其中后3个为可变参数。</p>
<p>在函数的实现代码中,可以通过2.4节叙述的,参数在栈中的排列顺序,来访问位于可变参数表的参数。比如:</p>
<p>void g(int a,char* c...){</p>
<p>void *pc=&c;int* pi=static_cast<int*>(pc)+1;//将pi指向首个可变参数</p>
<p>for(int i=0;i<a;i++)std::cout<<pi[i]<<” ”;</p>
<p>std::cout<<c<<std::endl;</p>
<p>}</p>
<p>我们甚至可以让一个函数的所有参数都是可变参数,只要有办法获知参数的数量即可。比如,我们约定,在传递给addAll的参数都是int,并且最后一个以0结束:</p>
<p>int addAll(...);</p>
<p>int a=f(1,4,2,5,7,0);</p>
<p>那么addAll可以这样实现:</p>
<p>int addAll(...){</p>
<p>int sum=0;int *p=&sum; //p指向第一个局部变量</p>
<p>p+=3; //跳过sum,ebp,eip,现在p指向第一个参数</p>
<p>for(;*p;++p) //如果p不指向0就继续循环</p>
<p>sum+=*p;</p>
<p>return sum;</p>
<p>}</p>
<p>可变参数表的最广泛应用是C的标准库函数中的格式化输入输出:printf和scanf。</p>
<p>void printf(char *c,...);</p>
<p>void scanf(char *c,...);</p>
<p>两者都通过它的首个参数指出后续参数表中的参数类型和参数数量。</p>
<p>如果可变参数表中的参数类型不一样,那么操纵可变参数表就需要复杂的指针运算,并且还要时刻注意边界对齐(align)问题,非常令人头痛。好在C标准库提供了用于操纵可变参数表的宏(macro)和结构(struct),他们被定义在库文件stdarg.h中:</p>
<p>typedef struct {char *p;int offset;} va_list;</p>
<p>#define va_start(valist,arg)</p>
<p>#define va_arg(valist,type)</p>
<p>#define va_end(valist)</p>
<p>其中结构va_list用于指示参数在栈中的位置,宏va_start接受一个va_list和函数的可变参数表之前的参数,通过第一个参数初始化va_list中的相应数据,因此要使用stdarg.h中的宏,你的可变参数表的函数必须至少有一个具名参数。va_arg返回下一个类型为type的参数,va_end结束可变参数表的使用。还是以上文的addAll为例,这次写出它的使用标准宏的版本:</p>
<p>int addAll(int i,...)</p>
<p>{</p>
<p>va_list vl; //定义一个va_list结构</p>
<p>va_start(vl,i); //用省略号之前的参数初始化vl</p>
<p>if(i=0)return 0; //如果第一个参数就是0,返回</p>
<p>int sum=i; //将第一个参数加入sum</p>
<p>for(;;){</p>
<p>i=va_arg(vl,int); //取得下一个参数,类型是sum</p>
<p>if(i==0)break; //如果参数是0,跳出循环</p>
<p>sum+=i;</p>
<p>}</p>
<p>va_end(vl);</p>
<p>return sum;</p>
<p>}</p>
<p>可以看出,如果参数类型一致,使用标准库要多些几行代码。不过如果参数类型不一致或者未知(printf的情况),使用标准库就要方便很多,因为我们很难猜出编译器处置边界对齐(align)等汇编代码的细节。使用标准库的代码是可以移植的,而使用上文所述的其它方法操纵可变参数表都是不可移植的,仅限于在I386平台上使用。</p>
<p>纵使可变参数表有使用上的便利性,它的缺陷也有很多,不可移植性和平台依赖性只是其一,最大的问题在于它的类型不安全性。使用可变参数表就意味着编译器不对参数作任何类型检查,这在C中算是一言难尽的历史遗留问题,在C++中就意味着恶魔reinterpret_cast被你唤醒。C的可变参数表是C++代码错误频发的根源之一,以至于C++标准将可变参数表列为即将被废除的C语言遗留特性。C++语法中的许多新特性,比如重载函数、默认参数值、模板,都可以一定程度上替代可变参数表,并且比可变参数表更加安全。</p>
<p>可变参数表在C++中惟一值得嘉奖的贡献,是在模板元编程(TMP)的SFINAE技术中利用可变参数表制作最差匹配重载。根据C++标准中有关函数重载决议的规则,具有可变参数表的函数总是最差匹配,编译器在被逼无奈走头无路时才会选择可变参数表。利用这一点,我们可以精心制作重载函数来提取类型信息。比如,要判断一个通过模板传递来的类型是不是int:</p>
<p>long isIntImp(int);</p>
<p>char isIntImp(...);</p>
<p>template<typename T></p>
<p>struct isInt</p>
<p>{</p>
<p>enum{value=sizeof(isIntImp(T()))==sizeof(long);}</p>
<p>}</p>
<p>然后,在一个具有模板参数T的函数中,我们就可以写</p>
<p>if(isInt<T>::value)//...</p>
<p>在这个(不怎么精致的)例子中,如果T是int,那么isIntImp的第一个重载版本就会被选中,返回值类型就是long,这样value就为1。否则,编译器只能选中第二个具有可变参数表的重载版本,返回值类型成为char,这样value就为0。把它说得再明白一些,上文的代码所表达的意思是:如果类型T是int,那它就是int,否则它就不是int,呵呵简单吧。这种通过重载决议规则来提取类型信息的技术,在模板元编程中被称作SFINAE,它和其它模板元编程技术被广泛运用于STL、Boost等模板库的开发实现之中。</p>
<p>值得注意的是,在上文SFINAE的运用中,isIntImp并没有出现定义而只提供了声明,因为我们并没有实际调用isIntImp函数,而只是让它参与重载决议并用sizeof判断其返回值类型。这是C++的一个设计准则的完美体现:不需要的东西可以不出现。由于这一准则,我们避免了在C++中调用具有可变参数表的函数这一危险举动,而仅仅利用了可变参数表在语法分析过程中的特殊地位,这种对于危险语言特性的巧妙利用是善意而无害的。</p>
</div>
C++ Tricks 2.7 I386平台的其它函数调用模型2007-08-28T14:28:00+09:002007-08-28T14:28:00+09:00farseerfctag:farseerfc.me,2007-08-28:/zhs/c-tricks-2-7-i386-calling-conventions.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386">
<h2>2.7 I386平台的其它函数调用模型</h2>
<div class="line-block">
<div class="line">上文介绍的只是I386平台上C函数调用的标准模型,被称作__cdecl。事实上,Microsoft Visual C++编译器还支持其它一些函数调用模型,所有调用模型名称皆以双下划线开头,下面列出所有函数调用模型的异同:</div>
</div>
<div class="section" id="cdecl">
<h3>1 __cdecl</h3>
<p>参数压栈顺序:逆序(从右至左)</p>
<p>参数堆栈恢复者:主调函数(caller)</p>
<div class="line-block">
<div class="line">__cdecl明确地指出函数使用C函数调用模型,这是默认的调用模型。</div>
</div>
</div>
<div class="section" id="stdcall">
<h3>2 __stdcall</h3>
<p>参数压栈顺序:逆序(从右至左)</p>
<p>参数堆栈恢复者:被调函数(callee)</p>
<div class="line-block">
<div class="line">__stdcall是微软所谓的标准调用模型。可惜的是它与__cdecl不兼容。几乎所有的Win32API函数使用这种函数调用模型,希望在DLL之间,或者在程序和WinNT操作系统之间传递函数指针的函数也应该使用这种模型。与__cdecl模型的不同之处在于,__stdcall模型下由被调函数恢复堆栈。主调函数在call语句之后,不需要再加上add语句。而被调函数的ret语句则被添加一个参数,代表函数参数堆栈的长度。因此,被调函数需要明确的知晓函数参数的数量和类型,所以在__stdcall模型下不支持可变参数表,所有参数必须写明 …</div></div></div></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="i386">
<h2>2.7 I386平台的其它函数调用模型</h2>
<div class="line-block">
<div class="line">上文介绍的只是I386平台上C函数调用的标准模型,被称作__cdecl。事实上,Microsoft Visual C++编译器还支持其它一些函数调用模型,所有调用模型名称皆以双下划线开头,下面列出所有函数调用模型的异同:</div>
</div>
<div class="section" id="cdecl">
<h3>1 __cdecl</h3>
<p>参数压栈顺序:逆序(从右至左)</p>
<p>参数堆栈恢复者:主调函数(caller)</p>
<div class="line-block">
<div class="line">__cdecl明确地指出函数使用C函数调用模型,这是默认的调用模型。</div>
</div>
</div>
<div class="section" id="stdcall">
<h3>2 __stdcall</h3>
<p>参数压栈顺序:逆序(从右至左)</p>
<p>参数堆栈恢复者:被调函数(callee)</p>
<div class="line-block">
<div class="line">__stdcall是微软所谓的标准调用模型。可惜的是它与__cdecl不兼容。几乎所有的Win32API函数使用这种函数调用模型,希望在DLL之间,或者在程序和WinNT操作系统之间传递函数指针的函数也应该使用这种模型。与__cdecl模型的不同之处在于,__stdcall模型下由被调函数恢复堆栈。主调函数在call语句之后,不需要再加上add语句。而被调函数的ret语句则被添加一个参数,代表函数参数堆栈的长度。因此,被调函数需要明确的知晓函数参数的数量和类型,所以在__stdcall模型下不支持可变参数表,所有参数必须写明。</div>
</div>
</div>
<div class="section" id="thiscall">
<h3>3 __thiscall</h3>
<p>参数压栈顺序:逆序(从右至左),this用ecx传递。</p>
<p>参数堆栈恢复者:被调函数(callee)</p>
<div class="line-block">
<div class="line">__thiscall是VC编译器中类的非静态成员函数(non-static member functon)的默认调用模型。但是如果此成员函数有可变参数表,VC编译器会使用__cdecl。和__stdcall一样,__thiscall由被调函数恢复堆栈。比较独特的是__thiscall会通过ecx寄存器传递成员函数的this指针,而__cdecl下this指针是通过在参数表最前面增加一个函数参数来传递的。__thiscall是VC编译器对this指针的使用的一种优化,大大提高了面向对象程序的效率。在VC2003及之前的编译器上__thiscall不是一个关键字,不能被显式指定。但可以给成员函数显式指定__cdecl来避免使用__thiscall。</div>
</div>
</div>
<div class="section" id="fastcall">
<h3>4 __fastcall</h3>
<p>参数压栈顺序:逆序(从右至左),前两个32位函数参数放入ecx和edx中</p>
<p>参数堆栈恢复者:被调函数(callee)</p>
<div class="line-block">
<div class="line">快速函数调用模型,将前两个32位函数参数放入ecx和edx中,其余参数再逆序压栈。使用的是和__thiscall类似的优化技术,加快函数调用,适合运用在小型inline函数上。同样使用__stdcall形式的被调函数恢复堆栈,所以不支持可变参数表。</div>
</div>
</div>
<div class="section" id="pascal">
<h3>5 __pascal</h3>
<p>参数压栈顺序:正序(从左至右)</p>
<p>参数堆栈恢复者:被调函数(callee)</p>
<div class="line-block">
<div class="line">过程式编程语言Pascal所使用的函数调用模型,由此得名。也是16位版本的Windows使用的API模型,过时的模型,现在已经废弃且禁止使用。你会看到有些书本仍会不时提到它,所以需要注意。__pascal是正序压栈,这与大部分I386函数模型都不相同。与__stdcall一样,由被调者恢复堆栈,不支持可变参数表。历史上曾有过的别名PASCAL、pascal、_pascal(单下划线),现在都改成了__stdcall的别名,与__pascal(双下划线)不同。</div>
</div>
</div>
<div class="section" id="id1">
<h3>6 其它函数调用模型,以及模型别名。</h3>
<p>__syscall:操作系统内部使用的函数调用模型,由用户模式向核心模式跳转时使用的模型。由于用户模式和核心模式使用不同的栈,所以没办法使用栈来传递参数,所有参数通过寄存器传递,这限制了参数的数量。用户模式编程中不允许使用。</p>
<p>__fortran:数学运算语言fortran使用的函数模型,由此得名。在C中调用由fortran编译的函数时使用。</p>
<p>__clrcall:微软.Net框架使用的函数模型,托管(Managed)C++默认使用,也可以从非托管代码调用托管函数时使用。参数在托管栈上正序(从左至右)压栈,不使用普通栈。</p>
<p>CALLBACK、PASCAL、WINAPI、APIENTRY、APIPRIVATE:I386平台上是__stdcall的别名</p>
<div class="line-block">
<div class="line">WINAPIV:I386平台上是__cdecl的别名</div>
</div>
</div>
<div class="section" id="id2">
<h3>7 函数调用模型的指定</h3>
<p>函数调用模型的指定方式和inline关键字的指定方式相同,事实上,inline可以被看作是C++语言内建的一种函数调用模型。唯一不同的是,声明函数指针时,也要指明函数调用模型,而inline的指针是不能指明的,根本不存在指向inline函数的指针。比如:</p>
<p>int CALLBACK GetVersion();</p>
<p>int (CALLBACK * pf)()=GetVersion;</p>
</div>
</div>
C++ Tricks 2.1 X86概述2007-08-27T16:33:00+09:002007-08-27T16:33:00+09:00farseerfctag:farseerfc.me,2007-08-27:/zhs/c-tricks-2-1-x86-architecture.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="x86">
<h2>2.1 X86概述</h2>
<p>所谓X86体系结构,是指以Intel
8086芯片为首的芯片所沿袭的CPU结构,一些文档中又被称作IA32体系结构。包括的芯片有但不限于:Intel
8086至
80486,奔腾(Pentium)系列处理器1至4,赛扬系列处理器,酷睿系列处理器,以及AMD的相应型号产品。X86体系结构在早期属于16位处理器,自80386之后扩展为32位处理器,所以一些文档中又把80386之后的32位处理器体系称作I386。自Pentium4后期,AMD的Athlon64开始,I386被进一步扩充为64位处理器,含有64位寻址能力的X86体系结构被称作X86-64或IA32-64。总之,市售的个人电脑用CPU,除苹果的Macintosh之外,全部采用X86体系结构芯片。</p>
<p>在X86早期,16位的寻址能力只支持64KB(2^16=64K)内存,这显然是不够的。Intel采用分段寻址的方法,用4位段位+16位偏移量,提供了总共1MB(2^20=1M)的寻址能力。所以在X86的16位编程中,有两种指针类型 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="x86">
<h2>2.1 X86概述</h2>
<p>所谓X86体系结构,是指以Intel
8086芯片为首的芯片所沿袭的CPU结构,一些文档中又被称作IA32体系结构。包括的芯片有但不限于:Intel
8086至
80486,奔腾(Pentium)系列处理器1至4,赛扬系列处理器,酷睿系列处理器,以及AMD的相应型号产品。X86体系结构在早期属于16位处理器,自80386之后扩展为32位处理器,所以一些文档中又把80386之后的32位处理器体系称作I386。自Pentium4后期,AMD的Athlon64开始,I386被进一步扩充为64位处理器,含有64位寻址能力的X86体系结构被称作X86-64或IA32-64。总之,市售的个人电脑用CPU,除苹果的Macintosh之外,全部采用X86体系结构芯片。</p>
<p>在X86早期,16位的寻址能力只支持64KB(2^16=64K)内存,这显然是不够的。Intel采用分段寻址的方法,用4位段位+16位偏移量,提供了总共1MB(2^20=1M)的寻址能力。所以在X86的16位编程中,有两种指针类型:长指针(lp,long
pointer)和短指针(sp,short
pointer),长指针(20位)提供整个内存空间寻址能力,短指针(16位)仅支持同一段中的寻址。在“古代”DOS及Win3.x编程过程中,两种类型的指针,以及总共1MB的内存大小,常常把程序员们折腾得焦头烂额。</p>
<p>自I386之后,CPU才开始提供32位的寻址能力。有了整整4GB(2^32=4G)的寻址空间,所有指针统一为长指针(32位)。时至今日,我们仍可以看到微软文档中指针变量的lp前缀。由于内存管理的需要,分段机制被保留下来,但这一次不是因为地址空间太小,而是因为地址空间远大于实际内存容量,从而采用了虚拟内存机制。</p>
<p>在从16位结构向32位结构转变的过程中,由于向下兼容的历史原因,曾一度长时间出现硬件32位(I386)、软件16位(Win3.x)的情况。同样也是为了兼容16位软件,Win9x操作系统(Win95、Win98、WinME)保留了16位代码和32位代码。混合代码的设计使得Win9x及其混乱和不稳定。直到完全32位内核的操作系统WinNT(以及构建于其上的Win2000,WinXP,Win2003)的出现,X86平台上内存布局混乱的局面才得以改善。有了从16位至32位移植的经验和准备,现今的从32位到64位的操作系统移植显得平稳顺利很多。WinXP和WinVista系统都同时发布了32位版本和64位版本,并且其x86-64系统都实现了对32位软件的无缝衔接支持。</p>
</div>
C++ Tricks 1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱2007-08-22T18:06:00+09:002007-08-22T18:06:00+09:00farseerfctag:farseerfc.me,2007-08-22:/zhs/c-tricks-1-2-trap-in-comma-logical-operator.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱</h2>
<p>很多人甚至不知道逗号(,)也是个C++运算符。与语法上要求出现的逗号(比如分隔函数参数的逗号)不同的是,出现在表达式中的逗号运算符在语义上表示多个表达式操作的连续执行,类似于分隔多语句的分号。比如:</p>
<p><strong>for</strong>(<strong>int</strong>i=0,j=9;i<10;++i<strong>,</strong>--j)std::cout<<i<<”+”<<j<<”=9\n”;</p>
<p>在这句语句中,出现了两个逗号,其中前者是语法上用来分隔声明的变量的,并非逗号运算符,而后者则是一个逗号运算符。根据C++标准,逗号运算符的执行顺序为从左到右依次执行,返回最后一个子表达式的结果。由于只有最后一个表达式返回结果,所以对于一个语义正常的逗号表达式而言,前几个子表达式必须具有副作用。同时,从语言的定义中也可以看出,逗号表达式对求值的顺序有严格要求 …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>1.2 逗号运算符(,)、逻辑运算符(&&,||)与运算符重载的陷阱</h2>
<p>很多人甚至不知道逗号(,)也是个C++运算符。与语法上要求出现的逗号(比如分隔函数参数的逗号)不同的是,出现在表达式中的逗号运算符在语义上表示多个表达式操作的连续执行,类似于分隔多语句的分号。比如:</p>
<p><strong>for</strong>(<strong>int</strong>i=0,j=9;i<10;++i<strong>,</strong>--j)std::cout<<i<<”+”<<j<<”=9\n”;</p>
<p>在这句语句中,出现了两个逗号,其中前者是语法上用来分隔声明的变量的,并非逗号运算符,而后者则是一个逗号运算符。根据C++标准,逗号运算符的执行顺序为从左到右依次执行,返回最后一个子表达式的结果。由于只有最后一个表达式返回结果,所以对于一个语义正常的逗号表达式而言,前几个子表达式必须具有副作用。同时,从语言的定义中也可以看出,逗号表达式对求值的顺序有严格要求。</p>
<p>对求值顺序有要求的,除了逗号表达式和条件表达式(参见1.1),在C++中还有逻辑运算符(&&和||)。逻辑运算相较于数学运算和位运算而言,有个显著的不同点:逻辑运算在计算到一半时,就有可能已经得到结果,这样继续运算另一半就不是必需的。对于A&&B,如果A=false,那么无论B为何值,整个的结果都是false;同样的A||B,如果A=true,那么不考虑B,结果一定是true。</p>
<p>C++标准规定,如果逻辑运算到一半(算出A)时,就已经可以确定运算的结果,那么就不运算剩下的另一半(B)。这种执行语义被称作“短路”。在其它一些编程语言中,短路语义是可以选择的:在Ada里非短路的逻辑运算符为and和or,短路的逻辑运算符为and_then和or_else。但是在C++中,逻辑运算符的短路语义是语法上强制的,我们没有非短路版本的运算符。如果确实需要非短路语义,我们总是可以通过增加一个bool中间变量加以解决。有时,短路对于保证正确执行是必须的,比如:</p>
<p><strong>char</strong>*p=getString();</p>
<p><strong>if</strong>(p<strong>&&</strong>*p)std::cout<<p;</p>
<p>这段代码在得到了一个字符串后,在字符串不为空时输出它。在C++中判断一个字符串不为空需要两个步骤:判断指针是否为0,以及指针不为0时判断指针指向的内容是否为’’。就像条件表达式中讨论到的(参见1.1),在p为空时提领p是个极其危险的操作。逻辑运算符的短路语义则避免了这种危险。</p>
<p>以上对逗号运算符与逻辑运算符的讨论,仅限于C++标准所定义的运算符语义。为什么这样说呢?这是因为在C++中,运算符的语义是可以由程序员自行定义的,这种机制叫做运算符重载(operator
overload)。运算符重载可以将人们熟悉的运算符表达式转换成函数调用,使编程灵活而直观,是个方便的语言特性。不过有时运算符重载也会使人困扰,那就是当运算符重载遇到求值顺序问题时。</p>
<p>C++中,并不是所有合法运算符都可以被合法地重载。条件运算符虽然对求值顺序有要求,但它并不在可重载运算符之列,所以运算符重载机制对它没有影响。问题在于,逗号运算符和逻辑运算符都可以被合法地重载:</p>
<p><strong>class</strong> BadThing{/* Some Bad and Stupid Thing*/};</p>
<p>BadThing& <strong>operator</strong>,(BadThing&, BadThing&);//重载了逗号运算符</p>
<p><strong>bool</strong> <strong>operator</strong>&&(BadThing&, BadThing&);//重载了&&</p>
<p>BadThing b1,b2;</p>
<p><strong>if</strong>(b1&&b2)b1,b2;//被替换成如下形式:</p>
<p><strong>if</strong>(<strong>operator</strong>&&(b1,b2))<strong>operator</strong>,(b1,b2);</p>
<p>可以看到,重载了运算符之后,对运算符的使用被替换为相应的函数调用形式。因此,旧有的运算符的执行顺序不再适用,取而代之的是函数参数的压栈顺序。</p>
<p>根据C++标准规定,任何参数必须在进入函数之前压栈,所以在进入<strong>operator</strong>&&之前,b1、b2就会被求值,这里不再有短路规则,任何依赖于短路语义的不知不觉间操作BadThing的代码(可能通过模板)都会混乱。</p>
<p>短路语义只是一个方面,更重要的在于压栈顺序。鉴于执行效率和旧代码兼容性等细节问题,C++标准在压栈顺序上给编译器的开发者留有很大自主性。标准的说辞是,编译器可能以任何它觉得方便的顺序将参数压栈,从左到右,从右到左,甚至从中间到两边,在这一点上我们不能安全地做任何假设。在上面的例子中,编译器生成的代码可能先计算b1再计算b2,也可能是相反的顺序。再看看编译器的实际情况,在我试过的所有基于X86体系结构的编译器中,参数都是以逆向压栈,即从右到左,有悖于大多数人的阅读习惯和直觉(别说你是来自伊斯兰的……)。</p>
<p>在C时代使用函数调用时,压栈顺序并不是什么大问题,毕竟大多数人会在函数调用的边界稍稍小心一些。但是到了C++中,事情变得有些复杂,因为简单如a+b的使用,就有可能被运算符重载机制替换为函数调用。更何况有模板参与之后,我们写代码时不能确定对象的真实类型,也就无法预知一个运算符是否真的被重载过,唯一稳妥的方法是,假定任何有可能被重载的运算符的使用都是函数调用。</p>
<blockquote>
<p style="margin:0;"></blockquote>
<p>回到上文的示例中,由于,和&&都被替换为函数调用,程序的执行顺序将成为压栈顺序,在X86上很有可能是从右到左,与标准定义的运算符的顺序正好相反。逗号运算符原本就含有“先…后…”的语义,这种颠倒的执行顺序势必造成程序和程序员的混乱。以我的经验而言,含有<strong>operator</strong>,的类,完全没有办法和STL或者iostream相互协作,反而会导致巨量的错误报告(什么叫巨量的错误报告有概念么?如果没有,那说明你还没玩过范式编程(GP,
Generic
Programming)。去玩玩GP吧,看看你的编译器对巨量的定义。在我手头,针对3.5KB的代码文件倾泻出3.8<strong>MB</strong>的错误信息的编译器不在少数……)。有鉴于此,我的结论是,除非你有充足的依据支持你这么做(比如你的粗暴上司的键盘上只剩下逗号能用),并且你清楚的了解这么做的后果的严重性(比如至少要看过此文),否则我奉劝你,永远不要碰<strong>operator</strong>,、<strong>operator</strong>&&以及<strong>operator</strong>||!</p>
</div>
C++ Tricks 1.1 条件运算符(?:)2007-08-22T18:05:00+09:002007-08-22T18:05:00+09:00farseerfctag:farseerfc.me,2007-08-22:/zhs/c-tricks-1-1-conditional-operator.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>1.1 条件运算符(?:)</h2>
<p>条件运算符(?:)是C++中唯一的三目运算符(trinary
operator),用于在表达式中作条件判断,通常可以替换if语句,与Visual
Basic中的iif函数、Excel中的if函数有同样的作用。语法形式如下:</p>
<p><em>condition</em> ? <em>true_value</em> : <em>false_value</em></p>
<p>其中<em>condition
*条件是任何可以转换为bool类型的表达式,包括但不仅限于**bool*</em>、<strong>int</strong>、指针。与<strong>if</strong>和<strong>while</strong>的条件部分稍显不同的是,这里不能定义变量,否则会导致语法错误。</p>
<p>另外,条件语句会切实地控制执行流程,而不仅仅是控制返回值。也就是说,两个返回值表达式中永远只有一个会被求值,在表达式的执行顺序很重要时,这点尤为值得注意。比如:</p>
<p><strong>int</strong> *pi=getInt();</p>
<p><strong>int</strong> i=pi …</p></div><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<div class="section" id="id1">
<h2>1.1 条件运算符(?:)</h2>
<p>条件运算符(?:)是C++中唯一的三目运算符(trinary
operator),用于在表达式中作条件判断,通常可以替换if语句,与Visual
Basic中的iif函数、Excel中的if函数有同样的作用。语法形式如下:</p>
<p><em>condition</em> ? <em>true_value</em> : <em>false_value</em></p>
<p>其中<em>condition
*条件是任何可以转换为bool类型的表达式,包括但不仅限于**bool*</em>、<strong>int</strong>、指针。与<strong>if</strong>和<strong>while</strong>的条件部分稍显不同的是,这里不能定义变量,否则会导致语法错误。</p>
<p>另外,条件语句会切实地控制执行流程,而不仅仅是控制返回值。也就是说,两个返回值表达式中永远只有一个会被求值,在表达式的执行顺序很重要时,这点尤为值得注意。比如:</p>
<p><strong>int</strong> *pi=getInt();</p>
<p><strong>int</strong> i=pi<strong>?</strong>*pi<strong>:</strong>0;</p>
<p>这里,只有当pi的值不为0时,它才会被提领(dereference)。这种语义保证了程序的正确性,因为提领一个空指针将导致致命的运行期错误(通常是非法操作的警告)。同时,正因为条件运算符控制运算流程的特点,使得它不能用类似iif的普通函数来模拟:</p>
<p><strong>int</strong> iif(<strong>int</strong> con,<strong>int</strong> t,<strong>int</strong>f){<strong>if</strong>(c)<strong>return</strong> t;<strong>return</strong> f;}//试图模拟?:</p>
<p>…//in some function</p>
<p><strong>int</strong> *pi=getInt();</p>
<p><strong>int</strong> i=iif(pi,*pi,0);//Error!</p>
<p>这段代码会导致上文提到的致命运行期错误。C/C++标准规定,参数在被传递给函数之前求值,因此无论pi为何值,都会被提领。又因为函数传回一个空指针的情况比较少见,所以这样的错误在调试时很难被发现,一旦发生又势必造成重大灾难。这样的代码在实践中应尽量避免。</p>
<p>有时,条件运算符控制流程的特点会不知不觉影响我们的代码。在C时代,最大值MAX通常用宏实现:</p>
<p><strong>#define</strong>MAX(a,b) ((a)>(b)<strong>?</strong>(a)<strong>:</strong>(b))</p>
<p>需要用额外的括号将宏参数和宏本体保护起来,以免运算符优先级扰乱逻辑,这是宏丑陋的特点之一,这里暂且不提。矛盾在于,用具有副作用的表达式调用宏时,会出现问题:</p>
<p><strong>int</strong> i=5,j=6;//…</p>
<p><strong>int</strong> a=MAX(++i,++j);</p>
<p>代码的作者原意显然是想先将i,j分别递增,再将其中较大的一个赋给a。执行这段代码,当i=5,j=6时,a=8,知道为什么吗?通过宏展开,赋值语句成这样:</p>
<p><strong>int</strong> a=(++i)>(++j)<strong>?</strong>(++i)<strong>:</strong>(++j);//删除了多余括号</p>
<p>在判断之前,i、j被分别自增一次,然后舍弃:之前的部分,j又被自增一次。执行之后,i=6,j=8。</p>
<p>MAX的更正确更安全的实现,是利用模板将类型参数化。STL标准算法中就有一个这样的工具级模版函数std::max。</p>
<p>条件运算符是表达式而不是语句,这使得它可以出现在任何需要表达式的地方,这扩大了它的适用范围。在那些语法上只能出现表达式而不能出现语句的地方(比如变量初始化),条件运算符有着不可替代的作用。</p>
<p>条件运算符优于<strong>if</strong>语句的另一个场合是“模板元编程”(TMP, Template
MetaProgramming)。在TMP这个古怪奇异的编译期运算编程技术中,一切旧有的技术和法则被全线击破,我们所能仰仗的工具,只有模板特化(Specialization)、<strong>typedef</strong>s、函数声明(无法调用它们)、以及编译期常量运算。已经有人很深入地论证过,仅有以上这些,就已经形成了一个“图灵完善”的计算机语言。我们可以用模板特化技术,来模拟条件分支,循环迭代等一系列复杂的语言结构。由于可以参与编译期常量运算,条件运算符在TMP世界中很自然地扮演起重要角色。</p>
<p>比如,给与类型T的一个变量t,我们想声明一个缓冲区存放t和一个int,缓冲区的大小不小于sizeof(T)也不小于sizeif(int),我们可以这样写:</p>
<p>char buffer[sizeof(T)>sizeof(int)? sizeof(T): sizeof(int)];</p>
<p>我们不能用一个if语句替换这个运算:</p>
<p>int i;</p>
<p>if(sizeof(T)>sizeof(int))i=sizeof(T);</p>
<p>else i=sizeof(int);</p>
<p>char buffer[i];//语法错误!</p>
<p>原因在于数组声明中的下标必须是一个编译期常量,而不是一个运行期的值,条件表达式的运算可以在编译期进行,if语句就只能在执行期执行。</p>
</div>
填补信仰、唤醒良知2006-08-07T21:36:00+09:002006-08-07T21:36:00+09:00farseerfctag:farseerfc.me,2006-08-07:/zhs/filling-believings-calling-conscience.html<p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>填补信仰、唤醒良知</p>
<p>我们听尽了呼吁与号召,对于良知,我不必谴责丧失它的国人,不必盛赞良知的美好。我只想讨论,丧失了良知的原因——空缺的信仰。</p>
<p>一、空缺信仰丧失良知</p>
<p>现代的国人缺少信仰,以至于丧失良知。曾几何时,中华民族由良好的信仰凝聚而成。三皇五帝时,族民们以炎黄为信仰;春秋战国时,士大夫之族以周制礼乐为信仰;汉代以后,百姓延习孔孟之说、老聃之道,以儒家学说为信仰;自大唐起,以佛教为首的现代宗教纷纷传入中原,人民开始以它们作为信仰。</p>
<p>直至鸦片战争、五四运动,西方文化入侵中华,国人开始抛弃国学,转而去研究科学;文化大革命,十年文化浩劫,人们批判旧的信仰,却没有合适的新的信仰前来填补。从此,国人的信仰出现空缺,国人的良知也被一块块蚕食殆尽。</p>
<p>二、信仰、科学、迷信</p>
<p>在许多国人的心目中,信仰就等于迷信。从小到大的教育告诉我们 …</p><p>从 <a class="reference external" href="http://farseerfc.wordpress.com/">farseerfc.wordpress.com</a> 导入</p>
<p>填补信仰、唤醒良知</p>
<p>我们听尽了呼吁与号召,对于良知,我不必谴责丧失它的国人,不必盛赞良知的美好。我只想讨论,丧失了良知的原因——空缺的信仰。</p>
<p>一、空缺信仰丧失良知</p>
<p>现代的国人缺少信仰,以至于丧失良知。曾几何时,中华民族由良好的信仰凝聚而成。三皇五帝时,族民们以炎黄为信仰;春秋战国时,士大夫之族以周制礼乐为信仰;汉代以后,百姓延习孔孟之说、老聃之道,以儒家学说为信仰;自大唐起,以佛教为首的现代宗教纷纷传入中原,人民开始以它们作为信仰。</p>
<p>直至鸦片战争、五四运动,西方文化入侵中华,国人开始抛弃国学,转而去研究科学;文化大革命,十年文化浩劫,人们批判旧的信仰,却没有合适的新的信仰前来填补。从此,国人的信仰出现空缺,国人的良知也被一块块蚕食殆尽。</p>
<p>二、信仰、科学、迷信</p>
<p>在许多国人的心目中,信仰就等于迷信。从小到大的教育告诉我们,信奉宗教是愚昧而又无知的表现,科学与信仰是矛盾的。是么?</p>
<p>我们无法保证社会上的每一个人都接受过良好的教育,我们无法确信最前沿的科学素养能在民众中普及。在科普与教育力不从心的社会死角,在科学技术尚不能及的文化盲区,我们依旧需要信仰的规范与限制,我们的良知需要信仰!</p>
<p>信仰不等于迷信。信仰本身无所谓谜与不迷,迷信是持有信仰的人误解了信仰,盲目遵从的结果。以为烧过香就可以免遭祸患,以为捐了钱就可以升入天堂,以为引火自焚就可以功德圆满,这便是迷信了。希特勒曾经的人类完善计划,依照遗传学的原理,将科学家与运动员强行结为夫妇孕育生命,希望得到最优秀的人类种族,这便是对科学这种信仰的迷信!</p>
<p>由此可见,科学与信仰并不是矛盾的硬币的两面,从某种意义而言科学本身也是信仰的一种。虽然历史上宗教往往作为科学发展的阻碍,可信奉真理的信念一直是推动科学发展的动力。牛顿就曾说过,对自然规律的探询是为了更接近上帝。由此可见,信仰与真理,与良知毫无矛盾。</p>
<p>三、信仰唤醒良知</p>
<p>很少有人仔细思考过,良知的缺失是由信仰的缺失造成的。信仰是人思想的寄托与依靠,是人行动处世的准则。没有了信仰的人,思想行为就缺少了约束的标准,人就更容易因为一时不成熟的冲动,背叛良知、铸成错误。</p>
<p>泰国人以佛教为信仰,泰国的寺庙每天都会有成千上万人顶礼膜拜。寺庙有一个人尽皆知的不成文规定:不得穿鞋进入。于是在寺庙之外,游客们可以看到千百双各式的鞋子有序的摆放在门口。国人每每看到此景,总会诧异地问:没有人会偷鞋么?得到的答案极为简单:庙前偷鞋会遭报应。由于拥有信仰,泰国人作了坏事会受到良知的谴责,泰国商人售出假货会彻夜难眠。二战期间,无数犹太难民被天主教会收留藏匿从而侥幸逃生,这同样是出于,天主教徒们被自己信奉的教义“众生生来平等”,所唤醒的良知。</p>
<p>天下无贼的世界,不能仅靠科普说教来营造。如果脱离了信仰,纵使是教育也无法培养良知。我问过许多修化学的同学,学习化学的意义,结论竟是为了考试。如果没有对科学的信仰,我们可以牢记公式定理,却质疑它们是真理;如果没有对社会公德的信仰,我们可以熟背交通规则,却正大光明地闯红灯;如果没有对医疗道德的信仰,医生可以放任伤口发炎,从而留住病人继续治疗……</p>
<p>国人需要信仰的约束,需要填补信仰的空白,从而唤醒那深埋于每个国人内心深处的良知!</p>