Linux IO模型
簡述#
IO操作不外乎讀和寫,但是不同場景對讀寫有不同的需求,例如網絡中同時監控多個檔案句柄,例如關鍵資料希望一路刷到儲存設備而不是扔到cache就傳回。
怎麼讀,怎麼寫,等不等結果傳回,是否等擷取到資料才發傳回,組成了不同的IO模型,分别适用于不同的場景。
根據同步與異步,阻塞與非阻塞,可以把IO模型如下分類:
同步/異步與阻塞/非阻塞怎麼了解呢?
同步與異步,就是IO發起人是否等待資料操作結束。發起一次IO請求後,發起IO的程序死等操作結束才繼續往下跑,就是同步。發起一次IO請求後,不等結束直接往下跑,就是異步。
阻塞與非阻塞,就是沒有資料時是否等到有資料才傳回。如果沒有資料,就讓IO程序幹等,直到有資料再傳回,就是阻塞。如果看到沒有資料,直接就傳回了,就是非阻塞。
什麼情況下會沒有資料呢?一個檔案就這麼大,除非讀到檔案末尾了,不然怎麼會說沒有資料呢?實際上,阻塞與非阻塞并不指磁盤上正常的檔案讀寫,而是指socket或者pipe之類的特殊檔案。這些特殊檔案并沒有明确意義上的大小,就是說,理論上他們的資料是無限大的,隻要有人往裡面寫資料,你就能無限讀出資料。這種特殊檔案有資料的前提,就是有人往裡面”灌“資料。如果你讀的太快,别人寫得太慢,就會出現池子裡面沒有資料的情況。這情況并不表示檔案讀到末端了,隻表示暫時還沒有資料。這時候你等呢?還是不等呢?這就是阻塞與非阻塞。
如果是同步/異步是IO發起人是否主動想等待,阻塞/非阻塞就是沒有資料時讀是否被動等待。
本文并不會嚴格按上圖依次介紹6種IO模型,相信網上有一大堆的資料。本文嘗試從正常讀寫、多路複用的2個常用場景介紹IO模型。
上圖中的信号IO,需要核心在某個時機向使用者空間傳遞SIGIO信号,且使用者空間在捕抓到此信号後進行IO處理。這做法并不常見,本文不會展開介紹。且由于是需要被動通知後才會執行IO操作,是以被我歸類為異步IO。
上圖中的事件驅動依賴于某些特定的庫。其原理類似于為某些事件注冊鈎子函數,由庫函數實作事件監控。在事件觸發時調用鈎子函數解決。這些函數庫屏蔽了平台之間的差異,例如Linux中通過epoll()來監控多個fd。不同庫有不同的使用方法,本文也不會展開介紹。
正常read()和write()#
讀操作#
Copy
/ 同步阻塞 /
fd = open("./test.txt", O_RDONLY); // 正常檔案
ret = read(fd, buf, len);
/ 同步非阻塞 /
int fds[2];
ret = pipe2(fds, O_NONBLOCK); // 無名管道
ret = read(fds[0], buf, len);
fd = open("./fifo", O_RDONLY | O_NONBLOCK); // 有名管道
在大多數場景下,我們系統調用read()正确傳回時就表示已經讀到資料了,此次的IO操作已經結束了。毫無疑問,大多數情況下的讀操作,都是同步的。
是否要阻塞,取決于open()時是否有O_NONBLOCK參數。
總的來說,我們系統調用read(),除非指定O_NONBLOCK,否則都是同步且阻塞的。
寫操作#
/ 異步 /
fd = open("./test.txt", O_WRONLY);
ret = write(fd, buf, len);
/ 同步 /
fd = open("./test.txt", O_WRONLY | O_SYNC);
阻塞與非阻塞主要針對讀資料,寫資料主要區分同步還是異步
由于Linux的IO棧中,有專門為IO準備的Cache層。在正常情況下,寫操作隻是把資料直接扔到了Cache就傳回了,此時資料并沒回刷到磁盤。要不等到系統回刷線程主動回刷,要不應用主動調用fsync(),否則資料一直都在Cache層,此時掉電資料就丢了。
寫操作是同步還是異步,主要看open()時,是否帶O_SYNC參數。帶O_SYNC就是同步寫,否則就是異步寫。
本文開頭就提到,同步/異步是IO發起人是否主動想等待IO結束,這裡的寫IO結束,指的是資料完全寫入到磁盤,而非write()傳回。
在沒有O_SYNC情況下,資料隻是寫到了Cache,需要核心線程定期回刷,是以此時的write是并沒有結束的,是以是異步的。相反,如果有O_SYNC,write()操作會一直等到資料完全寫入到磁盤後再傳回,是以是同步的。
從寫性能角度來說,異步寫會優于同步寫。由核心IO排程算法,對寫請求進行合并與排序,再一次性寫入,效率絕對高于東一塊西一塊的随機寫。是以,除非是擔心掉電丢失的關鍵資料,否則建議使用異步寫
多路複用#
多路複用常用于網絡開發,例如每個用戶端由一個socket與伺服器進行遠端通信,此時這個服務程式需要同時監控多個socket,為了避免資源損耗和提高響應速率,就會使用多路複用。
多路複用是怎麼一回事呢?我們假設一下有100個socket,在某一時間可能隻有個别socket是有資料的,即用戶端向服務端發送的請求資料。此時服務程式怎麼監控這100個socket,找出有資料的socket,并做出響應?有一種做法,就是非阻塞讀每個socket,沒資料直接傳回讀下一個,有資料(請求)就響應,以此實作輪詢。還有一種做法,建立100個程序/線程,每個線程,程序對應一個socket。
對少量的檔案還行,如果檔案數量一多,數百個,上千個socket逐一輪詢,或者建立上千個線程,這效率得多低啊。可不可以批量等待,當哪怕有一個socket有資料時,核心直接告訴應用那個socket來資料了?可以!這就是核心支援的多路複用的系統調用select()和epoll()
/ select 函數原型 /
int select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval timeout);
/ epoll 函數原型 /
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
他們原理大抵相似:
建立一個檔案句柄集合,把要監視的檔案句柄按順序整合到一起
在有資料時置位對應的辨別後傳回
應用通過檢查辨別就可以知道是哪個socket有資料了,此時讀socket即可直接擷取資料
具體的使用方法不在這裡詳細介紹,網上有總多資料,可以參考《UNIX環境進階程式設計》。
本文順便記錄下select()與epoll()的優缺點對比:
select()#
每次調用select,都需要把fd集合從使用者态拷貝到核心态,這個開銷在fd很多時會很大
每次調用select,都需要在核心周遊傳遞進來的所有fd,這個開銷在fd很多時也很大
select支援的檔案描述符數量太小了,預設是1024
epoll()#
epoll不僅僅一個函數,而是切分為3個函數,使得監控新的fd時,不需要拷貝所有的fd集合,隻需要拷貝新的fd到核心即可。
epoll采取回調的形式,當某個fd就緒了,就會調用回調,而在回調中,把就緒的fd加入就緒連結清單
epoll沒有數量,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048
可以發現,epoll()是對select()存在的問題進行針對性的解決。
作者: 廣漠飄羽
出處:
https://www.cnblogs.com/gmpy/p/12652578.html