Linux網路程式設計之IO模型

小米運維發表於2018-11-21
本文基於IO訪問中存在的兩個階段詳細介紹了Linux產生的五種IO模型。
上篇文章回顧:小米開源監控Open-Falcon收錄汽車之家貢獻的Win版Agent

同步與非同步

同步是指一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成後,依賴的任務才能算完成。

非同步是指不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了,非同步一般使用狀態、通知和回撥。

阻塞與非阻塞

阻塞是指呼叫結果返回之前,當前執行緒會被掛起,一直處於等待訊息通知,不能夠執行其他業務。

非阻塞是指在不能立刻得到結果之前,該函式不會阻塞當前執行緒,而會立刻返回。

五種IO模型

對於一次IO訪問,資料會先被拷貝到核心的緩衝區中,然後才會從核心的緩衝區拷貝到應用程式的地址空間。需要經歷兩個階段:

1)準備資料

2)將資料從核心緩衝區拷貝到程式地址空間

由於存在這兩個階段,Linux產生了下面五種IO模型。

阻塞IO

當使用者程式呼叫了recvfrom呼叫時,核心進入IO的第一個階段:準備資料(核心需要等待足夠的資料再拷貝),這個過程需要等待,使用者程式會被阻塞,等核心將資料準備好,然後拷貝到使用者地址空間,核心返回結果,使用者程式才從阻塞態進入就緒態。
Linux中,預設情況下所有的socket都是阻塞的。

非阻塞IO

當使用者程式發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程式,而是立刻返回一個error。使用者程式判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程式的system call,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

非阻塞IO模式下使用者程式需要不斷地詢問核心的資料準備好了沒有。

Linux下可以通過設定socket使其變為non-blocking。

IO多路複用

通過一種機制,一個程式可以監視多個檔案描述符(套接字描述符),一旦某個檔案描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。這樣就不需要每個使用者程式不斷的詢問核心資料準備好了沒有。

常用的IO多路複用方式有select、poll和epoll。

select

kernel會“監視”所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程式再呼叫read操作,將資料從kernel拷貝到使用者程式。

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);複製程式碼

select 函式監視的檔案描述符分3類,分別是writefds、readfds、和exceptfds。呼叫後select函式會阻塞,直到有描述副就緒(有資料可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來找到就緒的描述符。

select的一個缺點在於單個程式能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024。

poll

poll使用一個 pollfd的指標實現。

int poll (struct pollfd *fds, unsigned int nfds, int timeout);複製程式碼

pollfd結構包含了要監視的event和發生的event

struct pollfd { int fd; /* file descriptor */short events; /* requested events to watch */short revents; /* returned events witnessed */ };複製程式碼

和select函式一樣,poll返回後,需要遍歷pollfd來獲取就緒的描述符。poll沒有監聽最大數量限制。

epoll

epoll使用一個檔案描述符管理多個描述符,將使用者關心的檔案描述符的事件存放到核心的一個事件表中,採用監聽回撥的機制,這樣在使用者空間和核心空間的copy只需一次,避免再次遍歷就緒的檔案描述符列表。

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);複製程式碼

int epoll_create(int size):

建立一個epoll的控制程式碼,size用來告訴核心這個監聽的數目一共有多大。


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):

對指定描述符fd執行op操作。

- epfd:是epoll_create()的返回值。

- op:表示op操作,用三個巨集來表示:新增EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別新增、刪除和修改對fd的監聽事件。

- fd:是需要監聽的fd(檔案描述符)

- epoll_event:是告訴核心需要監聽什麼事,struct epoll_event結構如下:

struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; //events可以是以下幾個巨集的集合:EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉); EPOLLOUT:表示對應的檔案描述符可以寫; EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來); EPOLLERR:表示對應的檔案描述符發生錯誤; EPOLLHUP:表示對應的檔案描述符被結束通話; EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。 EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡複製程式碼


int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

等待epfd上的io事件,最多返回maxevents個事件。

引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函式返回需要處理的事件數目,如返回0表示已超時。

epoll的兩種工作模式

LT(level trigger,水平觸發)模式:當epoll_wait檢測到描述符就緒,將此事件通知應用程式,應用程式可以不立即處理該事件。下次呼叫epoll_wait時,會再次響應應用程式並通知此事件。LT模式是預設的工作模式。

LT模式同時支援阻塞和非阻塞socket。

ET(edge trigger,邊緣觸發)模式:當epoll_wait檢測到描述符就緒,將此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次呼叫epoll_wait時,不會再次響應應用程式並通知此事件。

ET是高速工作方式,只支援非阻塞socket。ET模式減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。

非同步IO

使用者程式發起read操作之後,立刻就可以開始去做其它的事。核心收到一個非同步IO read之後,會立刻返回,不會阻塞使用者程式。核心會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,核心會給使用者程式傳送一個signal,告訴它read操作完成了。

Linux網路程式設計之IO模型

訊號驅動IO

核心檔案描述符就緒後,通過訊號通知使用者程式,使用者程式再通過系統呼叫讀取資料。此方式屬於同步IO,因為實際讀取資料到使用者程式快取的工作仍然是由使用者程式自己負責的。


本文首發於公眾號“小米運維”,點選檢視原文


相關文章