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 的運行機制,可能有三種值
- NULL,設置為空指針,則select阻塞運行
- 0,select 非阻塞運行,機制變為輪詢,注意,不是參數傳遞0,參數傳遞0表示NULL
- >0,在指定的時間內阻塞,但有事件或者超時之后返回,返回值為有事件的描述符數量
返回值:
select 返回有事件的描述符數量,可以在對應的set中找到具體的描述符,錯誤則返回-1
優點: 跨平台
缺點:
- 描述符的數量受到限制,比如linux下最大1024;
- 每次調用,都需要將set從用戶態復制到內核態,這在描述符多時,開銷很大;
- 采用便利輪詢的方式,多了無意義的損耗,比如,只需要監聽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、優缺點
- 監聽數量不受限制,理論上上限是最大可以打開的文件數目,這個數目一般遠大於2048,linux上可以使用 cat /proc/sys/fs/file-max 命令查看。select的最大缺點就是進程打開的fd是有數量限制的。這對 於連接數量比較大的服務器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache就是這樣實現的),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。
- IO的效率不會隨着監視fd的數量的增長而下降。epoll不同於select和poll輪詢的方式,而是通過每個fd定義的回調函數來實現的。只有就緒的fd才會執行回調函數。
- 文件描述符只需要復制一次到內核,不需要每一次調用函數都進行文件描述符的內核復制
如果沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle- connection,就會發現epoll的效率大大高於select/poll。