一座記憶體金字塔,而不只是一塊
在記憶體那一階,你認識了一個舒服的謊言:每個行程都擁有一整片扁平的 RAM;你也看到作業系統如何維持那個謊言——靠翻譯位址,以及在 RAM 用光時,悄悄把睡著的分頁挪到磁碟上的置換空間裡。但那個磁碟,到底是什麼東西?而 RAM 自己又為什麼夾在快速的暫存器和慢速的儲存裝置之間?這一階要回答的,正是記憶體那一階一直指向、卻略過不談的問題:你能命名的每個位元組底下,都有一個有實體代價的實體裝置,而那個代價,幾乎形塑了作業系統處理大容量儲存的一切。
電腦並非只有一種記憶體;它有一整座儲存階層,一座金字塔。最頂端坐著 CPU 的暫存器——少少幾個格子,處理器能在不到一奈秒的瞬間讀到。它們底下是快取,再來是主記憶體(RAM),然後是磁碟這類儲存裝置,最底層是可移除或網路上的儲存。當你往金字塔下走,三件事一起增長:容量變大、每位元組的成本變便宜,而——這是代價——存取變得戲劇性地慢。整門儲存階層的手藝,就是盡可能把你即將要碰的資料,留在金字塔越高的地方越好。
上層易失,下層持久
金字塔上還畫著第二條線,它的重要性不亞於速度。從暫存器往下、一路到 RAM,全都是「易失的」(volatile):一斷電就立刻全忘光。從磁碟往下,則全是「非易失的」、也就是持久的:拔掉插頭、明天再回來,那些位元組仍在原地。這正是為什麼一份你只編輯、卻還沒存檔的文件,會在當機時人間蒸發——你的編輯活在易失的 RAM 裡,唯有存檔才會把它們往下複製到持久的儲存裝置上。「快但健忘」與「慢但忠實」之間的這條線,正是大容量儲存之所以自成一層的理由。
留意這如何漂亮地與虛擬記憶體接合。當作業系統把一個行程換出去,它就把分頁從快速、易失的 RAM 往下複製到慢速、持久的磁碟上;當行程再次碰到它們,一次分頁錯誤又把它們拉回上層。這座階層不是投影片上一張靜止的圖——它是作業系統整天不停地把資料往上、往下搬動的對象,努力把你「此刻需要的」留在靠近頂端的地方。從這裡開始,我們不再含糊地說「磁碟」,而是來看一個磁碟到底是什麼。
把硬碟撬開來看
把一台經典的硬碟打開,你會看到某種近乎機械的東西,像一台迷你唱機。一疊堅硬的金屬碟片(platter)共用一根主軸轉動,每分鐘數千轉。每片碟面都鍍著磁性材料,資料則以一個個被磁化的小點,記錄在一圈圈細細的同心環上。一個讀寫磁頭(head)懸浮在每個碟面上方、僅一髮之隔,固定在一支臂的末端;所有的臂像一把梳子一樣一起移動。要讀到一個位元組,正確的磁頭必須在正確的小點轉到它底下的那一刻,剛好位在正確的環上方。光是這最後一句話,就已經包含了磁碟之所以慢的全部理由。
磁碟幾何的術語,命名的正是這些零件。一個碟面上的一圈同心環叫一條磁軌(track)。一條磁軌被切成一段段固定大小的弧,叫磁區(sector)——以前每個 512 位元組,現在通常是 4 KB——而磁區是磁碟讀或寫的最小單位;你沒辦法取半個磁區。同一半徑上、垂直貫穿每片碟片疊起來的那一組磁軌,叫一個磁柱(cylinder):也就是磁頭梳不必移動就能搆到的所有磁軌,因為那些磁頭是同步動的。所以一個老式磁碟上的實體位置,曾經是以一組三元組來表示:第幾磁柱、第幾磁頭(碟面)、第幾磁區。
side view of the platter stack top view of one platter
(heads move together, in/out) (it spins under the head)
====[ head ]====> platter 0 .--------. <- outer track
====[ head ]====> platter 1 / ______ \
====[ head ]====> platter 2 | / \ | <- a sector is
^ | | spindle| | one arc of
arm comb moves in / out | \______/ | a track
= SEEK (slow, mechanical) \ /
'--------'
one track, all platters, same radius = a CYLINDER (no seek needed)為什麼一次讀取要等三種不同的時間
現在想像處理一個請求:「把磁柱 800、磁頭 2、磁區 17 的那個磁區給我。」這個代價乾淨俐落地拆成三部分,而把它們看成三件事,正是關於轉動式磁碟最有用的一個觀念。第一,磁頭梳必須往內或往外滑,直到磁頭坐在磁柱 800 上方。這一下機械式的擺動,就是尋軌時間,而因為它是一個實體物件在加速與停止,它佔了大頭——通常好幾毫秒,對 CPU 來說是一個永恆。那支臂,是整台機器裡最慢、最重、最不情願的東西。
第二,就算磁頭已經在正確的磁軌上方,你要的那個磁區大概還在環上別的地方——所以你得等碟片把它轉到磁頭底下。這段等待,就是旋轉延遲。平均而言你會等半圈;以 7200 RPM 計,轉一整圈約 8.3 毫秒,所以平均延遲大約 4 毫秒。第三、也是最後,正確的磁區一旦來到磁頭下方,位元就得在碟片持續轉動時真正地串流出來:這是傳輸時間(transfer time)。對單一磁區而言,這是三者中最小的,常常只是不到一毫秒的一小部分。
把它們加起來,就得到總存取時間:尋軌 + 旋轉 + 傳輸。烙印在每一個「懂磁碟」的設計裡的教訓是:前兩項——那些會動、要等的部分——通常把第三項淹沒。讀取隨機散落各處的 4 KB,代價幾乎和讀一個位元組一樣,因為兩種情況你都得付出完整的尋軌與旋轉;但讀取剛好彼此相鄰的 4 KB,便宜得不得了,因為你只尋軌、只等一次,接著一路串流下去。這就是參考局部性的實體根源:在磁碟上,靠在一起的東西,一起讀就便宜。本階下一篇導覽,會用實際的數字把這三個成分逐一拆開。
把幾何藏起來:邏輯區塊定址
老式的「磁柱/磁頭/磁區」三元組,拿來寫程式簡直是惡夢,而且更糟——它會說謊:現代磁碟在較長的外圈磁軌上塞的磁區,比短短的內圈磁軌多,所以磁碟回報出來的幾何,本來就是虛構的。於是硬碟改提供一個親切得多的介面,叫邏輯區塊定址,簡稱 LBA。作業系統把整顆磁碟看成一個長長的、扁平的固定大小區塊陣列,從 0、1、2、3 一路編號上去。要讀取,你只要問「第 42,891 號區塊」——沒有磁柱、沒有磁頭。這跟記憶體那一階給你的禮物是同一份:一組簡單、線性的數字,把底下一團混亂的實體現實藏了起來。
在硬碟內部,一台叫磁碟控制器的小電腦,保管著一張從每個邏輯區塊號碼對映到真實實體位置的祕密地圖,而它通常會把連續的區塊號碼,實際擺在彼此相鄰的位置。最後這一點,正是讓作業系統有辦法去推理局部性的關鍵:它看不到磁柱,但它可以信任「相鄰的區塊號碼大概在碟片上也相鄰」,所以依序讀取第 100 到第 110 號區塊就很便宜。作業系統把一個像 read(fd, buf, n) 的請求往下交,穿過檔案系統各層,直到它變成「讀這幾個邏輯區塊」,而控制器再把它變回那些你永遠看不到的尋軌與旋轉。
這一階要蓋的東西
你現在握住了後面一切的承重事實:一座在速度與持久之間權衡的金字塔、一顆存取代價是「尋軌加旋轉加傳輸」的硬碟,以及一個把所有那些機構變成一張扁平編號區塊清單的 LBA 介面。第二篇導覽會用真正的算術把這三個成分拆開,讓你能預測為什麼某種存取模式慢如爬行、另一種卻飛快。第三篇接著問那個顯而易見的後續:如果尋軌是頭號殺手、而許多請求正在排隊,作業系統能不能重新排序它們、好少尋一點軌?那就是磁碟排程,你會在那裡認識電梯演算法——一個像電梯依樓層服務、朝同一方向掃過去的磁頭,而不是來回亂衝。
接著地基會位移。第四篇導覽打開固態硬碟,那裡的快閃記憶體沒有會動的零件,因此根本沒有尋軌時間——這既讓一切變快,又出人意料地讓電梯排程幾乎變得沒意義,同時引進了磁碟從未有過的全新問題,像是抹除區塊與磨損。最後,第五篇導覽往上一層談到 RAID:把好幾顆硬碟黏在一起,以求更快(條帶化)、求在故障中存活(鏡像與同位元),或兩者兼得——以及那些誠實的取捨,包括一顆故障硬碟在重建時那段令人膽寒的空窗。這裡每一篇導覽,都是同一個問題的再次提問:我們要怎麼跟一種比記憶體慢上百萬倍、有時會說謊、有時會掛掉的儲存裝置共處?