文件系統與異步操做——異步IO那些破事

爲何想起寫這篇文章

前面這篇文章提到,舊的 Linux AIO 只支持直接(Direct)IO,還對讀寫區域大小有限制,可是 Windows 上的 IOCP 就有完整的 AIO 支持。以前真的以爲 Windows 真的很牛B,可是對爲何這樣一直懵懵懂懂。html

直到昨天我看到了這篇討論帖:https://news.ycombinator.com/...,他說微軟的異步IO是用線程模擬的。linux

WTF?這個內核原生支持這麼高大上的東西竟然是模擬的?可是人家還拿了微軟官方的文章佐證git

... so if you issue an asynchronous cached read, and the pages are not in memory, the file system driver assumes that you do not want your thread blocked and the request will be handled by a limited pool of worker threads.

微軟官方的說明總不會錯。但爲何會這樣?緩衝(Buffered)IO實現異步爲何就這麼難?github

還得從硬件提及

回顧一下大學學的計算機硬件知識:https://www.cnblogs.com/jswan...c#

盤面、磁道、扇區

每一個硬盤有多個盤面,每一個盤面都是由一圈圈的同心圓組成的,叫作磁道(track)。每一個磁道又被等比劃分爲若干個弧段,叫作扇區(sector)。扇區有固定的大小和個數,並且從硬盤誕生就被分配在固定位置。通常一個扇區具體的大小視總磁盤大小而定,傳統上512B爲一個扇區(可是也有不一樣)。segmentfault

扇區

注:如今的固態硬盤已經沒有傳統意義上的盤面、磁道概念,可是扇區的概念一直保留了下來。windows

扇區是實際上的磁盤最小讀寫單位,操做系統與磁盤文件系統通訊必須以扇區的整數個進行。這裏的整數個不只表明大小,並且指個體,例如你不能只讀第一個扇區的後半個+第二個扇區的前半個,雖然加起來大小也是一個扇區。api

直接(Direct)IO,最原始的文件IO

這種基於扇區操做磁盤的方式就直接派生了一種文件IO方式——直接(Direct)IO,也叫裸(Raw)IO,也叫非緩存(Unbuffered)IO。在 *nix 系中對應 O_DIRECT,在 Windows 中對應 FILE_FLAG_NO_BUFFERING緩存

對於這種IO方式,讀寫操做都有硬性的要求,全部操做系統都一致:app

  1. 數據傳輸的開始點,即文件和設備的偏移量,必須是扇區大小的整數倍
  2. 待傳遞數據的長度必須是扇區大小的整數倍
  3. 用於傳遞數據的緩衝區,其內存邊界必須對齊爲扇區大小的整數倍

前兩個限制就是爲了保證讀寫的都是一個個完整的扇區。第一個限制要求從一個扇區的開始讀寫,第二個限制要求正好到一個扇區完截止。第三個限制則是爲了保證讀寫一個扇區時不發生頁錯誤(page fault)

直接內存訪問(DMA),現代文件IO機制,異步IO的基礎

這些硬性要求都保證了,那麼操做系統怎樣實施文件IO操做呢?

這裏以讀爲例。就是磁盤說:把從第x號扇區開始的y個扇區的數據寫入到從p地址開始的內存中,寫完了告訴我(觸發中斷)。

這種操做叫作直接內存訪問(Direct memory access),其整個過程都不須要CPU參與。這個過程當中CPU有兩個選擇:等待或者去作其餘事。前者就是同步IO,後者就是異步IO。因此異步IO中實現直接IO是毫無問題的。

緩衝IO,操做方便性的妥協

直接IO環節少速度快。可是限制太多太複雜了,人們不想用。

設想一下,若是用戶想把文件中間的某個部分讀取至內存,使用直接IO須要怎麼作?假設用戶須要讀取 size 個字節,開始位置爲 offset,須要讀取至的內存指針爲 p

// 首先獲取扇區大小
int sectsize;
ioctl(fd, BLKSSZGET, &sectsize);
// 存儲真實須要讀取的數據量
size_t actual_size = size;
// 須要讀取的開始部分
size_t start = offset;
// 須要讀取的第一個扇區可能不完整
if (offset % sectsize != 0) {
    // 須要找到這段數據是從哪一個扇區開始存儲的(相對於文件頭),從扇區的開始位置讀取
    start -= offset % sectsize;
    // 把須要多讀的數據量計算進去
    actual_size += start - offset;
}
// 須要讀的最後一個扇區也可能不完整
if (actual_size % sectsize != 0) {
    // 須要讀滿整個尾扇區
    actual_size += sectsize - actual_size % sectsize;
}
// 終於算出了所須要的全部參數,開闢臨時內存
void *buf = aligned_alloc(sectsize, actual_size); // aligned_alloc 保證申請的內存地址按指定字節數對齊
// 執行讀操做,能夠異步
pread(fd, buf, actual_size, start);
// 將讀出的數據中所須要的部分複製到指定內存
memcpy(p, ((char *)buf + (start - offset)), size);
// 釋放零時內存
free(buf);

這是讀操做,寫操做更復雜。爲了保證填補區域空間不被寫操做沖掉,你要先把填補空間的數據從文件裏讀出來。

爲了簡化用戶端操做,全部的內核都提供基於緩衝機制(相似如上操做)的IO操做方式,這就是緩衝(Buffered)IO

緩衝IO——異步IO的原罪

前面說到異步IO中實現直接IO毫無問題,由於直接IO一旦開始全程不須要CPU參與。可是緩衝IO不同了,仍是以上面的讀操做舉例,申請內存能夠在打開文件時作,釋放內存能夠在關閉文件時作,數據對齊操做是純計算並且自己計算量不大同步作了也無所謂。可是最後的內存複製和此次讀操做強關聯,不管如何省不掉。

那麼怎麼辦呢?內核爲了避免阻斷當前線程運行,必須開闢一個線程池等待所需的直接IO操做完成,而後作如上內存複製操做。線程池中線程的數量是有限的,若是程序同時發起了大量異步的緩存IO請求,致使內核中的線程不夠用,那麼本次異步操做會被放入等待隊列等待線程池空閒,或者乾脆直接改成阻塞執行

Windows、Linux、*BSD,無一例外。舊的 Linux AIO 最直接,遇到緩存IO直接強制阻塞運行,新的io_uring 使用了線程池,它牛B的地方是可讓線程強輪詢IO操做是否完成,而不使用硬件中斷喚醒線程的方式,以減小喚醒線程所須要的額外延時。

微軟的文章還提到了幾個讓異步操做變同步運行的情形,好比數據壓縮加密等等。總而言之,異步IO操做在現現在階段還有很大的侷限性,Linux 新的異步IO特性 io_uring 已經用了線程池,恐怕也不會有什麼大的改觀。在沒有硬件支持的狀況下,哪有什麼黑魔法呢?

注:本文大致基於官方描述,也有部分是筆者的猜測,好比緩衝IO操做也許遠非筆者想象的那麼簡單。若是有錯歡迎提出。

相關文章
相關標籤/搜索