Linux文件IO(三)高級IO

直接文件IO
與其他現代操作系統內核一樣,Linux內核實現了一個複雜的緩存、緩衝以及設備和應用之間的I/O管理的層次結構。一個高性能應用可能希望越過這些複雜的層次結構並進行獨立的I/O管理,如數據庫系統,比較傾向於使用他們自己的緩存機制,以儘可能的減少操作系統的影響。
系統提供O_DIRECT標誌給open系統調用,會繞過內核的頁面緩存,直接啓動用戶空間的緩衝區與設備之間的IO,所有IO將會同步,直到操作完成後才返回。執行直接IO,請求的長度、緩衝區的調整、文件的偏移量必須是底層設備扇區的大小。

分散聚集I/O
散佈聚集I/O是一種可以在單次系統調用中操作多塊緩衝區的I/O方法,可以將單個數據流的內容寫到多個緩衝區,或者把單個數據流讀到多個緩衝區中。這樣命名是因爲數據被散佈到一個緩衝區向量,或者從一個緩衝區向量聚集。這種方法的另一個名字是向量I/O。相對的,第二章提到的標準讀寫系統調用可以稱作線性I/O。
效率:單個向量I/O 操作能代替多個線性I/O 操作。
性能:除了系統調用次數的降低,由於內部優化,向量I /O 比線性I/O 提供更好的性能。
原子性:不同於多個線性I/O 操作,一個進程可以執行單個向量I/O操作而且避免了與其它進程交叉操作的風險。
ssize_t readv (int fd, const struct iovec iov, int count);
ssize_t writev (int fd, const struct iovec
iov, int count);
除了操作多個緩衝區外, readv()和 writev()的行爲和read(),write() —樣。 每個iovec結構體描述一個獨立的緩衝區,我們稱其爲段。一 組 segm ent的集合稱爲向量(vector)。每個段描述了所要讀寫的緩衝區的
地址和長度。實際上,內核裏的所有I/O 都是向量I/O,read()和 write()是隻有一個向量的向量I/O,且向量中只有一個段。

Memroy Map
存儲映射是內核地址空間與用戶進程地址空間共享物理內存緩衝區,而不在內核和用戶存儲空間之間執行任何複製。mmap()調用請求內核將fd表示的文件中從offset處開始的len個字節數據映射到內存中。Linux系統調用最多6個參數,這是由CPU體系結構和操作系統共同決定的。
void mmap (void addr, size_t len, int prot, int flags, int fd, off_t offset);
addr參數告訴內核映射文件的最佳地址。這僅僅是提示,而不是強制,部分用戶傳遞0。調用返回內存映射區域的開始地址。
prot參數描述了對內存區域所請求的訪問權限。要求的訪存權限不能和打開文件的訪問模式衝突。當權限衝突時,系統發出SIGSEGV信號。
flag 參數描述了映射的類型和一些行爲。比如將addr看做強制性要求、映射區爲進程私有或者共享等

頁是內存中允許具有不同權限和行爲的最小單元。因此,頁是內存映射的基本塊,同時也是進程地址空間的基本塊。所以,映射區域是整數倍個的頁。如果len參數不能按頁對齊(可能因爲需要映射的文件大小不是頁大小的整數倍)映射區域延伸到下個空頁。多出來的內存,即最後一個有效字節和映射區域邊界的區域,用0填充。
sysconf():標準POSIX規定的獲得頁大小方法是通過sysconf(SC_PAGESIZE)。Linux也提供了 getpagesize()函數來獲得頁大小。

int munmap (void *addr, size_t len);
通常, munmap()的參數是上次mmap()調用的返回值和其參數len。當進程試圖訪問一塊已經無效的映射區域時,系統發出SIGBUS 。

相對於read(),write(),使用mmap()處理文件有很多優點。其中包括:
1,使用read()或write()系統調用需要從用戶緩衝區進行數據讀寫,而使用映射文件進行操作,可以避免多餘的數據拷貝。
2,除了潛在的頁錯誤,讀寫映射文件不會帶來系統調用和上下文切換的開銷。就像直接操作內存一樣簡單。,
3,當多個進程映射同一個對象到內存中,數據在進程間共享。只讀和寫共享的映射在全體中都是共享的;私有可寫的尚未進行寫時拷貝的頁是共享的。
4,在映射對象中搜索只需要一般的指針操作。而不必使用lseek()。基於以上理由,mmap()是很多應用的明智選擇。

使用mmap()時需要注意以下幾點:
1,映射區域的大小通常是頁大小的整數倍。因此,映射文件大小與頁大小的整數倍之間有空間浪費。對於小文件,較大比重的空間被浪費。例如對於4kb的頁,一個7字節的映射浪費了4089字節。
2,存儲映射區域必須在進程地址空間內。對於32位的地址空間,大量的大小各異的映射會導致大量的碎片出現,使得很難找到連續的大片空內存。這個問題在64位地址空間明顯減少。
3,創建和維護映射以及相關的內核數據結構有一定的開銷。通過上節提到的消除讀寫時的不必要拷貝的,這些開銷可以忽略,對於大文件和頻繁訪問的文件更是如此。基於以上理由,處理大文件(浪費的空間只佔很小的比重),或者在文件大小恰好被Page大小整除時(沒有空間浪費)優勢很明顯。

Linux提供了 mremap()來擴大或減少已有映射的大小。這個函數是Linux特有的:
void mremap (void addr, size_t old_size, size_t new_size, unsigned long flags);

POSIX 定義了 mprotect() , 允許程序改變已有內存區域的權限。在一些系統上, mprotect()只能操作之前由mmap()創建的區域。在 Linux下, mprotect()可以操作任意區域的內存。
int mprotect (const void *addr, size_t len, int prot);

POSIX 提供了一個使用存儲映射機制並與fsync()等價的系統調用: 調用 msync()可以將mmap()生成的映射在內存中的任何修改回寫到磁盤,達到同步內存中的映射和被映射的文件的目的。
int msync (void *addr, size_t len, int flags);

madvise()系統調用,可以讓進程在如何訪問映射區域上給內核一定的提示。內核會據此優化自己的行爲,儘量更好的利用映射區域。
int madvise (void *addr, size_t len, int advice);

預讀當Linux內核訪問磁盤上的文件時,通常會採用衆所周知的預讀(read a-head)來優化自己的操作。也就是說,當文件的某塊內容被加載時,內核也會讀取這塊內容之後的塊。如果隨後有對該塊的訪問請求(例如連續訪問某個文件時)內核可以馬上返回數據。因爲磁盤有緩衝區(磁盤自己也會有預讀行爲),而且文件通常是連續分佈在磁盤的,這個優化的開銷是很低的。預讀通常是有好處的,但是具體的優化效果依賴於預讀的程度。很大的預讀窗口在連續訪問文件時很有效,而對隨機訪問來講,預讀則是無用的開銷。正如我們在第二章的“內核內幕”一節所討論的,內核會動態的調整預讀窗口,以保證在預讀窗口中一定的命中率。高命中率則意味着最好使用再大一點的預讀窗口,反之則提示使用小一點的預讀窗口。應用程序可以通過madvise()系統調用來影響預讀窗口的大小。

普通文件I/O提示
在普通文件I/O 時,給內核提供操作提示。Linux提供了兩個滿足要求的函數: posix_fadvise()和 readahead()。
int posix_fadvise (int fd, off_t offset, off_t len, int advice);
advice告訴內核,文件的操作是順序的還是隨機的、是一次訪問、多次訪問還是不訪問等等。內核會根據提示作出優化,或者讓用戶更快地獲取數據,或者是讓系統少做無用的優化。

ssize_t readahead (int fd, off64_t offset, size_t count);
readahead()是Linux 所獨有的,用以完成posix_fadvise()使用 POSIX_FADV_WILLNEED選項時同樣的功能。

同步化(Synchronized),同步(Synchronous)及異步(Asyn¬chronous)操作
同步(synchronous)寫操作在數據全寫到內核緩衝區之前是不會返回的;同步(synchronous)讀操作在數據寫到應用程序在用戶空間的緩衝區之前是不會返回的。
異步(asynchronous)寫操作在用戶空間數據未全部寫入內核時可能就返回了;異步(asynchronous)讀操作在數據準備好之前可能就返回了。
同步化(synchronized) 寫操作把數據寫回硬盤,確保硬盤上的數據和內核緩衝區中的是同步的;同步化(synchronized) 的讀操作總是返回最新的數據(有可能從硬盤中讀取)。

同步阻塞 I/O:最常用的一個模型是同步阻塞 I/O 模型。在這個模型中,用戶空間的應用程序執行一個系統調用,這會導致應用程序阻塞。這意味着應用程序會一直阻塞,直到系統調用完成爲止(數據傳輸完成或發生錯誤)。
同步非阻塞 I/O:同步阻塞 I/O 的一種效率稍低的變種是同步非阻塞 I/O。在這種模型中,設備是以非阻塞的形式打開的。這意味着 I/O 操作不會立即完成,read 操作可能會返回一個錯誤代碼,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)
異步阻塞 I/O:另外一個阻塞解決方案是帶有阻塞通知的非阻塞 I/O。在這種模型中,配置的是非阻塞 I/O,然後使用阻塞 select 系統調用來確定一個 I/O 描述符何時有操作。使 select 調用非常有趣的是它可以用來爲多個描述符提供通知,而不僅僅爲一個描述符提供通知。對於每個提示符來說,我們可以請求這個描述符可以寫數據、有讀數據可用以及是否發生錯誤的通知。
異步非阻塞 I/O(AIO):異步非阻塞 I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會立即返回,說明 read 請求已經成功發起了。在後臺完成讀操作時,應用程序然後會執行其他處理操作。當 read 的響應到達時,就會產生一個信號或執行一個基於線程的回調函數來完成這次 I/O 處理過程。參考: https://www.ibm.com/developerworks/cn/linux/l-async/

執行異步(asynchronous)I/O 需要內核在底層的支持。POSIX 1003.1-2003定義了 aio 接口,幸運的是Linux實現了 aio。 aio 庫提供了一系列函數來實現異步I/O提交以及在完成時收到通知。

Linux只支持使用O_DIRECT標誌打開的文件上的aio。要想在沒有設置O_DIRECT標誌的普通文件上使用aio,我們必須自己來實現。沒有內核的支持,我們只能希望近似實現異步I/O,在實際應用中達到相似的效果。
第一,我們將看到爲什麼應用程序的開發者需要異步I/O:
1,實現非阻塞I/O
2,爲了分離內核的I/O排隊,I/O請求提交,在操作完成時收到通知。
第一點是基於性能的考慮。如果I/O操作永遠不會阻塞,就不會出現I/O超負荷的情況,進程也不必被I/O所束縛。第二點是基於過程的考慮,只是另一種處理I/O的方式。

達到這些目的最常用的方式是線程(調度將在第五六章講到)。這種方法需要完成下列任務:
1,創建一個線程池來處理所有的I/O。
2,實現將I/O操作加入工作隊列的一系列函數。
3,使這些函數返回唯一的I/O描述符,來區分相關的I/O操作。每個工作線程響應隊列首的I/O請求,提交到內核,等待它們完成。
4,完成後,把操作的結果(返回值,錯誤碼,所有讀取的數據)加入到一個結果隊列中。
5,實現一系列從結果隊列中獲取狀態信息的函數,使用最初返回的I/O描述符區分每個操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章