Farseerfc的小窩//farseerfc.me/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:/followup-about-swap.html<p>上週翻譯完 <a class="reference external" href="//farseerfc.me/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/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:/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:/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/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/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/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/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:/flash-storage-ftl-layer.html
<p>上篇 <a class="reference external" href="//farseerfc.me/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/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/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/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/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/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/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/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/images/ssd-extrememory-results.png"/>
<img alt="ssd-panasonic-plot.png" class="img-responsive" src="//farseerfc.me/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:/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/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/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/780dfc96.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/8d15ed13.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/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/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/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/images/DualActuator-Opposed.jpeg"/>
<img alt="DualActuator-Split.jpeg" class="img-responsive" src="//farseerfc.me/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:/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/zfs-layered-architecture-design.html#ZVOL">ZFS 子系統筆記中 ZVOL 的說明</a>
。用 zvol 能把 ZFS 當作一個傳統的卷管理器,繞開 ZFS
的 <a class="reference external" href="//farseerfc.me/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/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/zfs-layered-architecture-design.html">ZFS 的分層設計</a>
。不像 btrfs 基於現代 Linux 內核,有許多現有文件系統已經實現好的基礎設施可以利用,
並且大體上只用到一種核心數據結構(CoW的B樹); ZFS 則脫胎於 Solaris 的野心勃勃,
設計時就分成很多不同的子系統,逐步提升抽象層次,
並且每個子系統都發明了許多特定需求下的數據結構來描述存儲的信息。 在這裏和本文內容密切相關的是
<a class="reference external" href="//farseerfc.me/zfs-layered-architecture-design.html#zpl">ZPL</a> 、 <a class="reference external" href="//farseerfc.me/zfs-layered-architecture-design.html#DSL">DSL</a> 、 <a class="reference external" href="//farseerfc.me/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/images/zfs-block-pointer.svg">ZFS 中用的 128 字節塊指針</a></div>
<div class="panel-body">
<object class="embed-responsive-item" data="//farseerfc.me/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/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/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/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/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/zfs-layered-architecture-design.html#dmu">DMU</a> 對象,所以對 DDT 的讀寫也是經過 <a class="reference external" href="//farseerfc.me/zfs-layered-architecture-design.html#dmu">DMU</a> 的 CoW 讀寫,從而也經過 <a class="reference external" href="//farseerfc.me/zfs-layered-architecture-design.html#arc">ARC</a>
的緩存。想要有比較合理的 dedup 性能,需要整個 DDT 都儘量保持在內存 <a class="reference external" href="//farseerfc.me/zfs-layered-architecture-design.html#arc">ARC</a> 或者 <a class="reference external" href="//farseerfc.me/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/zfs-layered-architecture-design.html#zil">ZIL</a> 的日誌結構,讓 DDT
儘量保持在內存中,並且繞過 <a class="reference external" href="//farseerfc.me/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/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/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:/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:/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:/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/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:/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:/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:/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:/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:/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:/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:/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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:/compositor-in-X-and-compositext.html
<p>在上篇文章 <a class="reference external" href="//farseerfc.me/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/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/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/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:/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/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/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/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/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/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:/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:/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/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/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/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/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/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:/weather-forcast-academic-in-japan.html<p>最近 <a class="reference external" href="/links.html#mazk">mazk</a> 說我 <a class="reference external" href="//farseerfc.me/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/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/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:/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/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/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/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/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/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/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/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:/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/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/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/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:/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/try-pelican.html">折騰主題</a> 到現在都快三年了,
而從上次 <a class="reference external" href="//farseerfc.me/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/try-pelican.html">折騰主題</a> 到現在都快三年了,
而從上次 <a class="reference external" href="//farseerfc.me/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/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/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/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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:/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>