1. 程式人生 > >LinuxI/O多路復用

LinuxI/O多路復用

linux

I/O多路復用通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。I/O 多路復用技術是為了解決進程或線程阻塞到某個I/O系統調用而出現的技術,使進程不阻塞於某個特定的 I/O 系統調用。

I/O多路復用select

該函數準許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。

select函數

1.1 需要頭文件

#include <sys/select.h>

#include <sys/time.h>

#include <sys/types.h>

#include <unistd.h/>

1.2 聲明和返回值

1. 聲明

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2. 返回值

成功:就緒描述符的數目,超時返回 0。

出錯:-1。

1.3 功能

監視並等待多個文件描述符的屬性變化(可讀、可寫或錯誤異常)。select()函數監視的文件描述符分 3 類,分別是writefds、readfds、和exceptfds。調用後select() 函數會阻塞,直到有描述符就緒(有數據可讀、可寫、或者有錯誤異常),或者超時( timeout 指定等待時間),函數才返回。當select()函數返回後,可以通過遍歷 fdset,來找到就緒的描述符。

1.4 參數

1. nfds: 要監視的文件描述符的範圍,一般取監視的描述符數的最大值+1,如這裏寫 10, 這樣的話,描述符 0,1, 2 …… 9 都會被監視,在 Linux 上最大值一般為1024。

2. readfd: 監視的可讀描述符集合,只要有文件描述符即將進行讀操作,這個文件描述符就存儲到這。

3. writefds: 監視的可寫描述符集合。

4. exceptfds: 監視的錯誤異常描述符集合。

5. timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。 其timeval結構用於指定這段時間的秒數和微秒數。

struct timeval{

long tv_sec;   //seconds

long tv_usec;  //microseconds

};

timeout可以設置的值:

1、把該參數設置為空指針NULL。表示永遠等待下去,當有一個描述字準備好I/O時才返回。

2、把該參數設置為指定了timeval結構中的秒數和微秒數的值。表示等待指定了超時時間,當超時後還沒有描述字準備好I/O時直接返回。

3、把該參數設置為指定了timeval結構中的秒數和微秒數的值,而且秒數和微秒都為0。表示不檢查描述字是否準備好I/O後立即返回,這稱為輪詢。

1.5 fd_set

fd_set可以理解為一個集合,這個集合中存放的是文件描述符,可通過以下四個宏進行設置:

1.  void FD_ZERO(fd_set *fdset);       //清空集合

2.  void FD_SET(int fd, fd_set *fdset);   //將一個給定的文件描述符加入集合之中

3.  void FD_CLR(int fd, fd_set *fdset);   //將一個給定的文件描述符從集合中刪除

4.  int FD_ISSET(int fd, fd_set *fdset);   //檢查集合中指定的文件描述符是否可以讀寫

select優點和缺點

2.1 優點

select()目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點。

2.2 缺點

1、每次調用 select(),都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大,同時每次調用select()都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大。

2、單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低。

I/O多路復用poll

select()和poll()系統調用的本質一樣,前者在BSD UNIX中引入的,後者在System V中引入的。poll()的機制與 select() 類似,與 select() 在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是 poll() 沒有最大文件描述符數量的限制(但是數量過大後性能也是會下降)。poll() 和 select() 同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制於用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。

poll函數

1.1 需要頭文件

#include <poll.h/>

1.2 聲明和返回值

1. 聲明

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

2. 返回值

成功時,poll()返回結構體中revents域不為0的文件描述符個數;如果在超時前沒有任何事件發生,poll()返回 0;

失敗時,poll()返回 -1,並設置 errno 為下列值之一:

EBADF:一個或多個結構體中指定的文件描述符無效。

EFAULT:fds 指針指向的地址超出進程的地址空間。

EINTR:請求的事件之前產生一個信號,調用可以重新發起。

EINVAL:nfds參數超出 PLIMIT_NOFILE 值。

ENOMEM:可用內存不足,無法完成請求。

1.3 功能

監視並等待多個文件描述符的屬性變化。

1.4 參數

1. fds 不同與select()使用三個位圖來表示三個 fdset 的方式,poll()使用一個pollfd的指針實現。一個pollfd 結構體數組,其中包括了你想測試的文件描述符和事件, 事件由結構中事件域 events 來確定,調用後實際發生的時間將被填寫在結構體的revents 域。

struct pollfd{

int fd;         //文件描述符

short events;   //等待的事件

short revents;  //實際發生了的事件

};

fd 每一個 pollfd 結構體指定了一個被監視的文件描述符,可以傳遞多個結構體,指示poll()監視多個文件描述符。

events:每個結構體的 events 域是監視該文件描述符的事件掩碼,由用戶來設置這個域。events 等待事件的掩碼取值如下:

處理輸入:

POLLIN 普通或優先級帶數據可讀

POLLRDNORM 普通數據可讀

POLLRDBAND 優先級帶數據可讀

POLLPRI 高優先級數據可讀

處理輸出:

POLLOUT 普通或優先級帶數據可寫

POLLWRNORM 普通數據可寫

POLLWRBAND 優先級帶數據可寫

處理錯誤:

POLLERR發生錯誤

POLLHUP發生掛起

POLLVAL 描述字不是一個打開的文件

poll() 處理三個級別的數據,普通normal,優先級帶priority band,高優先級high priority,這些都是出於流的實現。

POLLIN | POLLPRI 等價於select()的讀事件。

POLLOUT | POLLWRBAND等價於select() 的寫事件。

POLLIN等價於POLLRDNORM | POLLRDBAND。

POLLOUT等價於POLLWRNORM。

例如,要同時監視一個文件描述符是否可讀和可寫,我們可以設置events為 POLLIN | POLLOUT。

revents域是文件描述符的操作結果事件掩碼,內核在調用返回時設置這個域。events 域中請求的任何事件都可能在revents域中返回。每個結構體的 events 域是由用戶來設置,告訴內核我們關註的是什麽,而revents域是返回時內核設置的,以說明對該描述符發生了什麽事件。

2. nfds 用來指定第一個參數數組元素個數。

3. timeout: 指定等待的毫秒數。

如果timeout設置為等待的毫秒數,無論I/O是否準備好,poll()都會返回。

如果timeout設置為 0時,poll() 函數立即返回。

如果timeout設置為 -1時,poll()一直阻塞到一個指定事件發生。

I/O多路復用epoll

epoll是在2.6內核中提出的,是之前的 select()和 poll()的增強版本。相對於 select()和 poll()來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

需要頭文件

#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);

epoll_create 函數

int epoll_create(int size);
3.1 功能

該函數生成一個 epoll 專用的文件描述符(創建一個 epoll 的句柄)。
3.2 參數

size 用來告訴內核這個監聽的數目一共有多大,參數size並不是限制了 epoll 所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。

自從linux 2.6.8之後,size 參數是被忽略的,也就是說可以填只有大於0 的任意值。需要註意的是,當創建好epoll句柄後,它就是會占用一個fd值,在linux下如果查看 /proc/ 進程 id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。

3.3 返回值

成功:epoll專用的文件描述符

失敗:-1

epoll_ctl函數

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

4.1 功能

epoll的事件註冊函數,它不同於select()是在監聽事件時告訴內核要監聽什麽類型的事件,而是在這裏先註冊要監聽的事件類型。

4.2 參數

1. epfd epoll 專用的文件描述符,epoll_create()的返回值

2. op 表示動作,用三個宏來表示:

EPOLL_CTL_ADD:註冊新的 fd 到 epfd 中;

EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;

EPOLL_CTL_DEL:從 epfd 中刪除一個 fd;

3. fd 需要監聽的文件描述符

4. event 告訴內核要監聽什麽事件,struct epoll_event 結構如下:

// 保存觸發事件的某個文件描述符相關的數據(與具體使用方式有關)

typedef union epoll_data {

void *ptr;

int fd;

__uint32_t u32;

__uint64_t u64;

} epoll_data_t;


// 感興趣的事件和被觸發的事件

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 隊列裏

4.3 返回值

成功:0

失敗:-1

epoll_wait函數

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

5.1 功能

等待事件的產生,收集在 epoll 監控的事件中已經發送的事件,類似於 select() 調用。

5.2 參數

1. epfd epoll 專用的文件描述符,epoll_create()的返回值

2. events 分配好的 epoll_event 結構體數組,epoll 將會把發生的事件賦值到events 數組中(events 不可以是空指針,內核只負責把數據復制到這個 events 數組中,不會去幫助我們在用戶態中分配內存)。

3. maxevents maxevents 告之內核這個 events 有多大 。

4. timeout 超時時間。

如果timeout設置為等待的毫秒數,無論I/O是否準備好,都會返回。

如果timeout設置為 0時,函數立即返回。

如果timeout設置為 -1時,一直阻塞到一個指定事件發生。

5.3 返回值

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

失敗:-1

LT模式與ET模式

epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默認模式。

6.1 LT模式

當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序並通知此事件。

6.2 ET模式

當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序並通知此事件。

6.3 LT模式與ET模式比較

ET模式在很大程度上減少了epoll 事件被重復觸發的次數,因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

epoll 的優點

1、在 select/poll中,進程只有在調用一定的方法後,內核才對所有監視的文件描述符進行掃描,而epoll()事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制(軟件中斷 ),迅速激活這個文件描述符,當進程調用 epoll_wait()時便得到通知。

2、監視的描述符數量不受限制,它所支持的 FD 上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在 1GB 內存的機器上大約是 10 萬左右,具體數目可以 cat /proc/sys/fs/file-max 察看,一般來說這個數目和系統內存關系很大。select()的最大缺點就是進程打開的 fd 是有數量限制的。這對於連接數量比較大的服務器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache 就是這樣實現的),不過雖然 Linux 上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。

3、I/O 的效率不會隨著監視 fd 的數量的增長而下降。select(),poll() 實現需要自己不斷輪詢所有 fd 集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而 epoll 其實也需要調用 epoll_wait() 不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒 fd 放入就緒鏈表中,並喚醒在 epoll_wait() 中進入睡眠的進程。雖然都要睡眠和交替,但是 select() 和 poll() 在“醒著”的時候要遍歷整個fd集合,而 epoll 在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的 CPU 時間。這就是回調機制帶來的性能提升。

4、select(),poll() 每次調用都要把 fd 集合從用戶態往內核態拷貝一次,而epoll只要一次拷貝,這也能節省不少的開銷。


LinuxI/O多路復用