溫故知新-快速理解Linux網絡I/O



摘要

在上一篇頭腦風暴中,我們提到了影響性能的最大就是I/O,本文將從同步、異步、阻塞、非阻塞的概念開始、從I/O模型、I/O多路複用展開、爲下一篇Java I/O模型做鋪墊;

阻塞、非阻塞、同步、異步

  • 阻塞、非阻塞

阻塞 I/O 和非阻塞 I/O 這兩個概念是程序級別的。主要描述的是程序請求操作系統 I/O 操作後,如果 I/O 資源沒有準備好,那麼程序該如何處理的問題:前者等待;後者繼續執行(並且使用線程一直輪詢,直到有 I/O 資源準備好了)。

  • 同步、異步:

同步 I/O 和非同步 I/O,這兩個概念是操作系統級別的。主要描述的是操作系
統在收到程序請求 IO 操作後,如果 I/O 資源沒有準備好,該如何響應程序的問
題:前者不響應,直到 I/O 資源準備好以後;後者返回一個標記(好讓程序和自
己知道以後的數據往哪裏通知),當 I/O資源準備好以後,再用事件機制返回給程
序。

Linux下的I/O模型

Linux的內核將所有的外部設備都看做一個文件來操作,對一個文件的讀寫操作會調用內核提供的系統命令,返回一個file descriptor(fd 文件描述符)。而對一個socket的讀寫也會有相應的描述符,稱爲socketfd(socket描述符)。描述符就是一個數字,它指向內核中的一個結構體(文件路徑,數據區等一些屬性)。

根據UNIX網絡編程對I/O模型的分類,UNIX提供了5種I/O模型,分別如下。

  • 阻塞I/O模型
  • 非阻塞I/O模型
  • I/O複用模型
  • 信號驅動I/O模型
  • 異步I/O

阻塞I/O模型

在進程空間中調用recvfrom,系統調用直到數據包到達且被複制到應用進程的緩衝區中或者發生錯誤時才返回,在此期間一直會等待。進程在從調用recvfrom開始到它返回的整段時間內都是被阻塞的,因此稱爲阻塞IO模型

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

阻塞

非阻塞I/O模型

recvfrom從應用層到內核的時候,如果該緩衝區沒有數據的話,就直接返回一個EWOULDBLOCK錯誤,一般都對非阻塞IO模型進行輪詢檢查這個狀態,看內核是不是有數據到來。
非阻塞

I/O複用模型

Linux提供select/poll,進程通過一個或者多個fd傳遞給select或者poll系統調用,阻塞在select操作上,這樣select/poll可以幫我們偵測多個fd是否處於就緒狀態。select/pool是順序掃描fd是否就緒,而且支持的fd數量有限,因此他的使用限制,所有epoll系統誕生了,epoll使用基於事件驅動的方式代替順序掃描,因此性能更高。當有fd就緒時,立即回調函數roolback;
I/O複用

select

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就緒描述符的數目,超時返回0,出錯返回-1
函數參數介紹如下:
(1)第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(因此把該參數命名爲maxfdp1),描述字012...maxfdp1-1均將被測試。
因爲文件描述符是從0開始的。
(2)中間的三個參數readset、writeset和exceptset指定我們要讓內核測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設爲空指針。struct fd_set可以理解爲一個集合,這個集合中存放的是文件描述符,可通過以下四個宏進行設置:
   void FD_ZERO(fd_set *fdset);       //清空集合
   void FD_SET(int fd, fd_set *fdset);//將一個給定的文件描述符加入集合之中
   void FD_CLR(int fd, fd_set *fdset);//將一個給定的文件描述符從集合中刪除
   int FD_ISSET(int fd, fd_set *fdset);// 檢查集合中指定的文件描述符是否可以讀寫 3)timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。
   struct timeval{
      long tv_sec;   //seconds
      long tv_usec;  //microseconds
  };
這個參數有三種可能:
(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。爲此,把該參數設置爲空指針NULL。
(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。
(3)根本不等待:檢查描述字後立即返回,這稱爲輪詢。爲此,該參數必須指向一個timeval結構,而且其中的定時器值必須爲0

select
select的缺點:

  • 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
  • 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
  • select支持的文件描述符數量太小了,默認是1024

poll

  • 特點:poll的實現和select非常相似,只是描述fd集合的方式不同,pollfd結構包含了要監視的event和發生的event,不再使用select“參數-值”傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後性能也是會下降)。和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
  • 函數
# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd結構體定義如下:
struct pollfd {
  int fd;        /* 文件描述符 */
  short events;  /* 等待的事件 */
  short revents; /* 實際發生了的事件 */
} ; 
  每一個pollfd結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示poll()監視多個文件描述符。每個結構體的events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:
  POLLIN         有數據可讀。
  POLLRDNORM       有普通數據可讀。
  POLLRDBAND      有優先數據可讀。
  POLLPRI         有緊迫數據可讀。
  POLLOUT            寫數據不會導致阻塞。
  POLLWRNORM       寫普通數據不會導致阻塞。
  POLLWRBAND        寫優先數據不會導致阻塞。
  POLLMSGSIGPOLL     消息可用。
  此外,revents域中還可能返回下列事件:
  POLLER     指定的文件描述符發生錯誤。
  POLLHUP   指定的文件描述符掛起事件。
  POLLNVAL  指定的文件描述符非法。
這些事件在events域中無意義,因爲它們在合適的時候總是會從revents中返回。
  使用poll()select()不一樣,你不需要顯式地請求異常情況報告。
  POLLIN | POLLPRI等價於select()的讀事件,POLLOUT |POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM |POLLRDBAND,而POLLOUT則等價於POLLWRNORM。例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置 events爲POLLIN |POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於文件描述符請求的events結構體。如果POLLIN事件被設置,則文件描述符可以被讀取而不阻塞。如果POLLOUT被設置,則文件描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操作都會正常返回而不阻塞。
  timeout參數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定爲負數值表示無限超時,使poll()一直掛起直到一個指定事件發生;timeout爲0指示poll調用立即返回並列出準備好I/O的文件描述符,但並不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。
  返回值和錯誤代碼
  成功時,poll()返回結構體中revents域不爲0的文件描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設置errno爲下列值之一:
  EBADF         一個或多個結構體中指定的文件描述符無效。
  EFAULTfds   指針指向的地址超出進程的地址空間。
  EINTR      請求的事件之前產生一個信號,調用可以重新發起。
  EINVALnfds  參數超出PLIMIT_NOFILE值。
  ENOMEM       可用內存不足,無法完成請求。

epoll

  • 優點: 在 select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一 個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait() 時便得到通知。(此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。)
  • 函數
#include <sys/epoll.h>
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);
1int epoll_create(int size);
  創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。

(2int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件epoll的事件註冊函數,它不同與select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd,第四個參數是告訴內核需要監聽什麼事,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隊列裏
(3int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似於select()調用。參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。

信號驅動I/O模型

I/O信號

  • 開啓套接信號驅動I/O功能,通過系統調用sigaction執行一個信號處理函數,系統調用立即返回,進程繼續工作;當數據準備就緒時,就爲該進程生產一個SIGIO信號,通過信號回調通知應用程序調用recvfrom來讀取數據,並通知主循環函數處理數據;

異步I/O

  • 告知內核某個操作,並讓內核在整個操作完成後(包括講數據從內核複製到用戶自己的緩存)通知我們。
  • 這種模型與信號驅動模型的主要區別是:信號驅動I/O由內核通知我們何時可以開始一個I/O操作;異步I/O由內核通知我們I/O操作何時已經完成
    -異步I/O

參考

IO多路複用之select總結
IO多路複用之poll總結
IO多路複用之epoll總結
《netty權威指南 第二版》李林峯


你的鼓勵是我創作的最大動力

打賞

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