多路復用


1、說明

socket編程的demo中使用的都是最基本的,但是一般不會真正用在項目中的代碼。而實際項目中,需要面臨復雜多變的需求環境,比如有多個socket連接,或者服務需要監聽的時候,可能有很多socket連接進來。面對這種情況,最直接最簡單的想法是,一個socket連接創建一個線程去處理。當然,在socket連接數較少的情況下,這種方式無可厚非,但是如果連接數量較大,就會出現意外情況。

我們都知道,linux下一個線程默認所占的內存是8M(可以使用ulimit -s查看),那么加入,1000個socket連接,建立1000線程,光線程的開銷就高達8G多,更遑論其他業務還要使用內存了。

而且,在很多情況下,socket建立連接之后,並不是要一直通信,而是間隔通信,那么占用一個獨立的線程來“照顧”這個連接顯得很不明智。

針對這種情況,就需要采用多路復用機制,所謂多路復用,就是一個進程見識多個socket描述符,一旦某個socket描述符就緒(可讀寫或者異常)了,就會通知應用程序,進行相應的處理。

1.1、多路復用的幾種機制

目前的多路復用機制有三種,select、poll 和 epoll。這三種機制各有優劣

2、函數簡介

2.1、select

頭文件和函數聲明:

#include <sys/select.h>

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

FD_ZERO(int fd, fd_set* fds)   // 清空集合
FD_SET(int fd, fd_set* fds)    // 將給定的描述符加入集合
FD_ISSET(int fd, fd_set* fds)  // 判斷指定描述符是否在集合中 
FD_CLR(int fd, fd_set* fds)    // 將給定的描述符從文件中刪除

描述:

監聽多個文件描述符的屬性變化。

函數返回后,需要便利fd_set來找到就緒的描述符

參數說明:

nfds: 需要監聽的描述符的范圍,一般是最大描述符+1,比如,現在需要監聽 0/1/2/3/4/5 這幾個描述符,則參數設置為6,在linux下,值最大是1024

readfds: 監聽到的可讀的描述符的set,所有可讀的描述符都會存儲到這里

writefds: 監聽到的可寫的描述符的set

exceptfds: 監聽到的異常的描述符set

timeout: select 方法的超時時間,這個參數決定 select 的運行機制,可能有三種值

  1. NULL,設置為空指針,則select阻塞運行
  2. 0,select 非阻塞運行,機制變為輪詢,注意,不是參數傳遞0,參數傳遞0表示NULL
  3. >0,在指定的時間內阻塞,但有事件或者超時之后返回,返回值為有事件的描述符數量

返回值:

select 返回有事件的描述符數量,可以在對應的set中找到具體的描述符,錯誤則返回-1

優點: 跨平台

缺點:

  1. 描述符的數量受到限制,比如linux下最大1024;
  2. 每次調用,都需要將set從用戶態復制到內核態,這在描述符多時,開銷很大;
  3. 采用便利輪詢的方式,多了無意義的損耗,比如,只需要監聽99的描述符,但是內核會遍歷0~100的描述符;

2.2、poll

頭文件和函數聲明:

#include <poll.h>

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

struct pollfd {
    int fd; 	   /* 描述符 */
    short events;  /* 需要監聽的事件 */
    short revents; /* 實際發生的事件 */
};

函數描述: 監聽多個文件描述符的屬性變化,和select類似,但是有很大區別,使用一個 pollfd 指針來替代 select 的三個set的功能

參數描述:

fd: 結構體指針,可以傳入多個結構體,每個結構體都是一個被監聽的描述符

nfds: 指定傳入的結構體的數量

timeout: 超時時間,單位毫秒,-1 表示阻塞

返回值: 返回有事件的描述符數量,函數返回后,需要輪詢來找到發生事件的描述符,錯誤則返回-1

pollfd 結構體:

fd: 表示描述符

events: 需要監聽的事件掩碼,取值如下

revents: 實際發生的事件掩碼,取值如下

宏定義 可作events的值 可作revents的值 說明
POLLIN y y 數據可讀
POLLRDNORM y y 普通數據可讀
POLLRDBAND y y 優先數據可讀
POLLPRI y y 緊迫帶數據可讀
POLLOUT y y 數據可寫,不會阻塞
POLLWRNORM y y 普通數據可寫,不會阻塞
POLLWRBAND y y 優先級帶數據可寫,不會阻塞
POLLMSGSIGPOLL y y 消息可用

非法事件

宏定義 可作events的值 可作revents的值 說明
POLLERR y 發生錯誤
POLLHUP y 發生掛起
POLLNVAL y 描述不是打開的文件

POLLIN | POLLPRI 等價於 select 的讀事件,而 POLLIN 等價於 POLLRDNORM | POLLRDBAND

POLLOUT | POLLWRBAND 等價於 select 的寫事件,而 POLLOUT 等價於 POLLWRNORM

這些事件不是互斥的,可以同時設置

優缺點:

和 select 相比,poll 沒有了數量的限制,但是數量太大也會影響效率

poll 同樣有着將傳入的描述符從用戶態復制到內核態的缺點,開銷隨着描述符數量的增大而線性增大

2.3、epoll

epoll是后來提出的,作為 select 和 poll 的增強版本

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

2.3.1、epoll_create

int epoll_create(int size); 

創建一個 epoll 專用的描述符,size 為監聽的數目

size 參數並沒有限制 epoll 監聽的描述符的最大限制,而是作為內部分配數據結構的一個建議,linux自動2.6.8版本之后,該參數被忽略,只要大於0就行

返回一個描述符,使用結束時,需要close(),錯誤則返回-1

2.3.2、epoll_ctl

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

epoll 的事件注冊,告訴 epoll 要監聽哪些事件

返回0表示注冊成功,-1表示失敗

參數:

epfd: epoll 專用的描述符,由epoll_create() 返回

op: 該函數的作用,即注冊(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)和刪除(EPOLL_CTL_DEL)

fd: 需要監聽的描述符

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 */  
};  

epoll_event 中的 events 可以是以下宏定義的集合

宏定義 說明
EPOLLIN 表示對應的文件描述符可以讀(包括對端 SOCKET 正常關閉)
EPOLLOUT 表示對應的文件描述符可以寫
EPOLLPRI 表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來)
EPOLLERR 表示對應的文件描述符發生錯誤
EPOLLHUP 表示對應的文件描述符被掛斷
EPOLLET 將 EPOLL 設為邊緣觸發(Edge Trigger)模式,這是相對於水平觸發(Level Trigger)來說的
EPOLLONESHOT 只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個 socket 的話,需要再次把這個 socket 加入到 EPOLL 隊列里

2.3.3、epoll_wait

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

等待 epoll 監聽下的 IO 事件

參數:

epfd: epoll 描述符

events: epoll 會把發生的事件賦值到events中

maxevents: 表明這個 events 的大小

timeout: 超時時間,單位毫秒,-1表示阻塞

返回值: 返回需要處理的事件數目,0表示超時未有事件,-1表示失敗

2.4、其他方法

FD_ZERO(fd_set *fdset);	//清空一個描述符集合
FD_SET(fd_set *fdset, int fd);	//添加fd到描述符集合中
FD_CLR(fd_set *fdset, int fd);	//從描述符集合中刪除一個fd
FD_ISSET(int fd,fd_set *fdset);	//檢查fd是否在描述符集合中

3、epoll

epoll 的工作模式有兩種:LT(level trigger) 和 ET(edge trigger),默認LT模式,區別如下:

LT模式: 當 epoll_wait 檢測到描述符事件發生,並通知應用程序,應用程序可以不利己處理該事件,下次調用 epoll_wait 時,還是會通知此事件

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

3.1、LT模式和ET模式

LT模式:

默認模式,支持阻塞和非阻塞方式,內核會通知事件發生,若果不做任何操作,則內核下次還是會通知,編程不易出錯,select/poll都是這種模式

ET模式:

告訴你工作模式,只支持非阻塞。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)

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

在 select/poll中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一 個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait() 時便得到通知。(此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。)

3.2、優缺點

  1. 監聽數量不受限制,理論上上限是最大可以打開的文件數目,這個數目一般遠大於2048,linux上可以使用 cat /proc/sys/fs/file-max 命令查看。select的最大缺點就是進程打開的fd是有數量限制的。這對 於連接數量比較大的服務器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache就是這樣實現的),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。
  2. IO的效率不會隨着監視fd的數量的增長而下降。epoll不同於select和poll輪詢的方式,而是通過每個fd定義的回調函數來實現的。只有就緒的fd才會執行回調函數。
  3. 文件描述符只需要復制一次到內核,不需要每一次調用函數都進行文件描述符的內核復制

如果沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle- connection,就會發現epoll的效率大大高於select/poll。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM