為什麼內核設計了 Direct I/O
以 Linux 內核為例,默認狀態下大部分讀寫邏輯使用 Buffered I/O,所以當我們寫入某文件時,並不會直接寫入物理設備,而是寫入 kernel 的 page cache,然後返回「寫成功」。page cache 中的記錄何時寫到物理設備上由 kernel 控制,用戶空間程序看不到這一過程。
Buffered I/O 狀態下,當用戶空間程序讀取此文件時,kernel 會先在 page cache 中查詢,如果命中則直接返回,未命中則先從物理設備上讀取到 page cache,再從 page cache 返回結果。這個過程很像我們在開發數據應用程序時會給 postgresql 上加一層 redis 緩存,主要目的為提高讀寫效率。
而 Direct I/O 則會繞開 page cache,允許用戶空間程序直接讀寫物理設備。Direct I/O 之所以有重大意義,正因為 Buffered I/O 在特殊情境下無法滿足用戶空間程序的需求。
舉個例子,LSM-Tree 的 level 0 tree 活躍於內存上,是一個有固定最大容量限制的結構(實際上不一定是 Tree),也扮演了緩存的角色。level 0 tree 在容量達標後會與物理設備上的下一個 level 的文件進行合併。從算法的角度來看,LSM-Tree 自己在用戶空間實現了一套高效可靠的緩衝邏輯,再在中間加一層 kernel 的 Buffered I/O 不僅無法提高效率,甚至可能有反作用,於是我們便需要 Direct I/O 來繞過 page cache。
Direct I/O 的目的並非抹除 cache 邏輯,而是將 cache 邏輯的控制權由內核轉移到用戶空間程序。如果用戶空間程序上設計的 cache 算法低效,使用 Direct I/O 反而會大大降低 I/O 效率。
Direct I/O 也帶來了問題:Buffered I/O 除了自動 cache 邏輯,還有自動對齊數據,避免讀寫衝突等功能;而 Direct I/O 則沒有後兩者,由此引來吐槽:Direct I/O 的設計與 kernel 格格不入——kernel 存在的目的,就是讓用戶空間程序不要關心、不要操縱這些細節。
使用 Direct I/O,務必先在用戶空間對數據進行對齊,這是因為數據要想寫入物理設備,必須與扇區空間對齊。而 Buffered I/O 則會在內存空間自動幫我們做這件事。LSM-Tree 之所以適合 Direct I/O,一是因為前文提到的它的 level 0 tree 天然是個緩存結構,二是因為 LSM-Tree 各層級的 tree 的大小確定,並不需要過於複雜的邏輯就能滿足對齊需求。