Linux 網絡編程 — IO 模型

目錄

基本概念

同步與異步

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

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

阻塞與非阻塞

  • 阻塞是指調用結果返回之前,當前線程會被掛起,一直處於等待消息通知,不能夠執行其他業務。
  • 非阻塞是指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。

五種 IO 模型

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

  1. 準備數據。
  2. 將數據從內核緩衝區拷貝到進程地址空間。

由於存在這兩個階段,Linux 具有下面五種 I/O 模型。

阻塞 IO

當用戶進程調用了 recvfrom() 時,內核進入 IO 的第一個階段:準備數據(內核需要等待足夠多的數據再拷貝),這個過程需要等待,用戶進程會被阻塞,等內核將數據準備好,然後拷貝到用戶地址空間,內核返回結果,用戶進程才從阻塞態進入就緒態。

Linux 中,默認情況下所有的 Socket 都是阻塞的。

非阻塞 IO

當用戶進程發出 read() 調用時,如果 Kernel 中的數據還沒有準備好,那麼它並不會阻塞用戶進程,而是立刻返回一個 Error。用戶進程判斷結果是一個 Error 時,它就知道數據還沒有準備好,於是它可以再次發送 read() 調用。一旦 Kernel 中的數據準備好了,並且又再次收到了用戶進程的系統調用,那麼它馬上就將數據拷貝到了用戶內存,然後返回。

非阻塞 IO 模式下用戶進程需要不斷地詢問內核的數據準備好了沒有,如果沒有準備好,那麼在某些場景中,用戶進程可以去做別的事情而不需要一直等待。

Linux 下可以通過設置 Socket 爲 non-blocking 模式。

同步 IO(信號驅動)

內核文件描述符就緒後,通過 Signal(信號)通知用戶進程,用戶進程再通過系統調用讀取數據。此方式屬於同步 IO,因爲實際讀取數據到用戶進程緩存的工作仍然是由用戶進程自己負責的。

異步 IO

用戶進程發起 read() 調用之後,立刻就可以開始去做其它的事。內核收到一個異步 IO read 之後,會立刻返回,不會阻塞用戶進程。內核會等待數據準備完成,然後將數據拷貝到用戶內存,當這一切都完成之後,內核會給用戶進程發送一個 Signal(信號),告訴它 read() 完成了。用戶進程再從用戶內存讀取數據。

在這裏插入圖片描述

IO 多路複用

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

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 類:

  1. writefds
  2. readfds
  3. 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 使用一個文件描述符來管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,採用監聽回調的機制,這樣在用戶空間和內核空間的數據拷貝只需要進行一次,避免再次遍歷就緒的文件描述符列表,從而提升了性能。

epoll 的操作過程需要三個接口:

  1. 創建一個 epoll 的句柄,形參 size 用來告訴內核這個監聽的數目一共有多大。
int epoll_create(int size)
  1. 對指定描述符 fd 執行 op 操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
  • epfd:是 epoll_create() 的返回值。
  • 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 隊列裏。
  1. 等待 epfd 上的 IO 事件,最多返回 maxevents 個事件。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • events:用來從內核得到事件的集合
  • maxevents:告之內核這個 events 有多大,這個 maxevents 的值不能大於創建 epoll_create() 時指定的 size
  • timeout:超時時間,單位毫秒,0 表示立即返回,-1 表示不確定,也有說法說是永久阻塞

該函數返回需要處理的事件數目,如返回 0 表示已超時。

epoll 的兩種工作模式:

  1. LT(Level Trigger,水平觸發)模式:當 epoll_wait 檢測到描述符就緒,將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序並通知此事件。LT 模式是默認的工作模式,同時支持阻塞和非阻塞 Socket。
  2. ET(Edge Trigger,邊緣觸發)模式:當 epoll_wait 檢測到描述符就緒,將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序並通知此事件。ET 是高速工作方式,只支持非阻塞 Socket。ET 模式減少了 epoll 事件被重複觸發的次數,因此效率要比 LT 模式高。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章