作者: 大呀大帝國 </br>
email:drnijq@126.com
1.Epoll簡介
EPOLL 的API用來執(zhí)行類似poll()的任務。能夠用于檢測在多個文件描述符中任何IO可用的情況。Epoll API可以用于邊緣觸發(fā)(edge-triggered)和水平觸發(fā)(level-triggered), 同時epoll可以檢測更多的文件描述符千元。以下的系統調用函數提供了創(chuàng)建和管理epoll實例:
- epoll_create() 可以創(chuàng)建一個epoll實例并返回相應的文件描述符(epoll_create1() 擴展了epoll_create() 的功能)忧饭。
- 注冊相關的文件描述符使用epoll_ctl()
- epoll_wait() 可以用于等待IO事件。如果當前沒有可用的事件元媚,這個函數會阻塞調用線程疼蛾。
邊緣觸發(fā)(edge-triggered 簡稱ET)和水平觸發(fā)(level-triggered 簡稱LT):
epoll的事件派發(fā)接口可以運行在兩種模式下:邊緣觸發(fā)(edge-triggered)和水平觸發(fā)(level-triggered),兩種模式的區(qū)別請看下面,我們先假設下面的情況:
- 一個代表管道讀取的文件描述符已經注冊到epoll實例上了愉舔。
- 在管道的寫入端寫入了2kb的數據钢猛。
- epoll_wait 返回一個可用的rfd文件描述符。
- 從管道讀取了1kb的數據轩缤。
- 調用epoll_wait 完成命迈。
如果rfd被設置了ET,在調用完第五步的epool_wait 后會被掛起火的,盡管在緩沖區(qū)還有可以讀取的數據壶愤,同時另外一段的管道還在等待發(fā)送完畢的反饋。這是因為ET模式下只有文件描述符發(fā)生改變的時候卫玖,才會派發(fā)事件公你。所以第五步操作,可能會去等待已經存在緩沖區(qū)的數據假瞬。在上面的例子中陕靠,一個事件在第二步被創(chuàng)建,再第三步中被消耗脱茉,由于第四步中沒有讀取完緩沖區(qū)剪芥,第五步中的epoll_wait可能會一直被阻塞下去。
下面情況下推薦使用ET模式:
- 使用非阻塞的IO琴许。
- epoll_wait() 只需要在read或者write返回的時候税肪。
相比之下,當我們使用LT的時候(默認),epoll會比poll更簡單更快速榜田,而且我們可以使用在任何一個地方益兄。
2.API介紹
先簡單的看下EPOLL的API
2.1 創(chuàng)建epoll
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
epoll_create() 可以創(chuàng)建一個epoll實例。在linux 內核版本大于2.6.8 后箭券,這個size 參數就被棄用了净捅,但是傳入的值必須大于0。
在 epoll_create () 的最初實現版本時辩块, size參數的作用是創(chuàng)建epoll實例時候告訴內核需要使用多少個文件描述符蛔六。內核會使用 size 的大小去申請對應的內存(如果在使用的時候超過了給定的size荆永, 內核會申請更多的空間)。現在国章,這個size參數不再使用了(內核會動態(tài)的申請需要的內存)具钥。但要注意的是,這個size必須要大于0液兽,為了兼容舊版的linux 內核的代碼骂删。
epoll_create() 會返回新的epoll對象的文件描述符。這個文件描述符用于后續(xù)的epoll操作抵碟。如果不需要使用這個描述符桃漾,請使用close關閉坏匪。
epoll_create1() 如果flags的值是0拟逮,epoll_create1()等同于epoll_create()除了過時的size被遺棄了。當然flasg可以使用 EPOLL_CLOEXEC适滓,請查看 open() 中的O_CLOEXEC來查看 EPOLL_CLOEXEC有什么用敦迄。
返回值: 如果執(zhí)行成功,返回一個非負數(實際為文件描述符), 如果執(zhí)行失敗凭迹,會返回-1罚屋,具體原因請查看error.
2.2 設置epoll事件
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
這個系統調用能夠控制給定的文件描述符epfd指向的epoll實例,op是添加事件的類型嗅绸,fd是目標文件描述符脾猛。
有效的op值有以下幾種:
- EPOLL_CTL_ADD 在epfd中注冊指定的fd文件描述符并能把event和fd關聯起來。
- EPOLL_CTL_MOD 改變*** fd和evetn***之間的聯系鱼鸠。
- EPOLL_CTL_DEL 從指定的epfd中刪除fd文件描述符猛拴。在這種模式中event是被忽略的,并且為可以等于NULL蚀狰。
event這個參數是用于關聯制定的fd文件描述符的愉昆。它的定義如下:
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這個參數是一個字節(jié)的掩碼構成的。下面是可以用的事件:
- EPOLLIN - 當關聯的文件可以執(zhí)行 read ()操作時麻蹋。
- EPOLLOUT - 當關聯的文件可以執(zhí)行 write ()操作時跛溉。
- EPOLLRDHUP - (從 linux 2.6.17 開始)當socket關閉的時候,或者半關閉寫段的(當使用邊緣觸發(fā)的時候扮授,這個標識在寫一些測試代碼去檢測關閉的時候特別好用)
- EPOLLPRI - 當 read ()能夠讀取緊急數據的時候芳室。
- EPOLLERR - 當關聯的文件發(fā)生錯誤的時候,epoll_wait() 總是會等待這個事件刹勃,并不是需要必須設置的標識堪侯。
- EPOLLHUP - 當指定的文件描述符被掛起的時候。epoll_wait() 總是會等待這個事件深夯,并不是需要必須設置的標識抖格。當socket從某一個地方讀取數據的時候(管道或者socket),這個事件只是標識出這個已經讀取到最后了(EOF)诺苹。所有的有效數據已經被讀取完畢了,之后任何的讀取都會返回0(EOF)雹拄。
- EPOLLET - 設置指定的文件描述符模式為邊緣觸發(fā)收奔,默認的模式是水平觸發(fā)。
- EPOLLONESHOT - (從 linux 2.6.17 開始)設置指定文件描述符為單次模式滓玖。這意味著坪哄,在設置后只會有一次從epoll_wait() 中捕獲到事件,之后你必須要重新調用 epoll_ctl() 重新設置势篡。
返回值:如果成功翩肌,返回0。如果失敗禁悠,會返回-1念祭, errno將會被設置
有以下幾種錯誤:
- EBADF - epfd 或者 fd 是無效的文件描述符。
- EEXIST - op是EPOLL_CTL_ADD碍侦,同時 fd 在之前粱坤,已經被注冊到epoll中了。
- EINVAL - epfd不是一個epoll描述符瓷产≌拘或者fd和epfd相同,或者op參數非法濒旦。
- ENOENT - op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL株旷,但是fd還沒有被注冊到epoll上。
- ENOMEM - 內存不足尔邓。
- EPERM - 目標的fd不支持epoll晾剖。
2.3 等待epoll事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
epoll_wait 這個系統調用是用來等待epfd中的事件。events指向調用者可以使用的事件的內存區(qū)域铃拇。maxevents告知內核有多少個events钞瀑,必須要大于0.
timeout這個參數是用來制定epoll_wait 會阻塞多少毫秒,會一直阻塞到下面幾種情況:
- 一個文件描述符觸發(fā)了事件慷荔。
- 被一個信號處理函數打斷雕什,或者timeout超時。
當timeout等于-1的時候這個函數會無限期的阻塞下去显晶,當timeout等于0的時候贷岸,就算沒有任何事件,也會立刻返回磷雇。
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 */
};
每次epoll_wait() 返回的時候偿警,會包含用戶在epoll_ctl中設置的events。
還有一個系統調用epoll_pwait ()唯笙。epoll_pwait()和epoll_wait ()的關系就像select()和 pselect()的關系螟蒸。和pselect()一樣盒使,epoll_pwait()可以讓應用程序安全的等待知道某一個文件描述符就緒或者捕捉到信號。
下面的 epoll_pwait () 調用:
ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);
在內部等同于:
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll_wait(epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
如果 sigmask為NULL, epoll_pwait()等同于epoll_wait()七嫌。
返回值:有多少個IO事件已經準備就緒少办。如果返回0說明沒有IO事件就緒,而是timeout超時诵原。遇到錯誤的時候英妓,會返回-1,并設置 errno绍赛。
有以下幾種錯誤:
- EBADF - epfd是無效的文件描述符
- EFAULT - 指針events指向的內存沒有訪問權限
- EINTR - 這個調用被信號打斷蔓纠。
- EINVAL - epfd不是一個epoll的文件描述符,或者maxevents小于等于0
3. 官方demo
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
* (socket(), bind(), listen()) omitted */
epollfd = epoll_create1( 0 );
if ( epollfd == -1 )
{
perror( "epoll_create1" );
exit( EXIT_FAILURE );
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, listen_sock, &ev ) == -1 )
{
perror( "epoll_ctl: listen_sock" );
exit( EXIT_FAILURE );
}
for (;; )
{
nfds = epoll_wait( epollfd, events, MAX_EVENTS, -1 );
if ( nfds == -1 )
{
perror( "epoll_wait" );
exit( EXIT_FAILURE );
}
for ( n = 0; n < nfds; ++n )
{
if ( events[n].data.fd == listen_sock )
{
conn_sock = accept( listen_sock,
(struct sockaddr *) &local, &addrlen );
if ( conn_sock == -1 )
{
perror( "accept" );
exit( EXIT_FAILURE );
}
setnonblocking( conn_sock );
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, conn_sock,
&ev ) == -1 )
{
perror( "epoll_ctl: conn_sock" );
exit( EXIT_FAILURE );
}
} else {
do_use_fd( events[n].data.fd );
}
}
}
4.完整可運行的DEMO
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#define MAX_EVENT 20
#define READ_BUF_LEN 256
/**
* 設置 file describe 為非阻塞模式
* @param fd 文件描述
* @return 返回0成功吗蚌,返回-1失敗
*/
static int make_socket_non_blocking (int fd) {
int flags, s;
// 獲取當前flag
flags = fcntl(fd, F_GETFL, 0);
if (-1 == flags) {
perror("Get fd status");
return -1;
}
flags |= O_NONBLOCK;
// 設置flag
s = fcntl(fd, F_SETFL, flags);
if (-1 == s) {
perror("Set fd status");
return -1;
}
return 0;
}
int main() {
// epoll 實例 file describe
int epfd = 0;
int listenfd = 0;
int result = 0;
struct epoll_event ev, event[MAX_EVENT];
// 綁定的地址
const char * const local_addr = "192.168.0.45";
struct sockaddr_in server_addr = { 0 };
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd) {
perror("Open listen socket");
return -1;
}
/* Enable address reuse */
int on = 1;
// 打開 socket 端口復用, 防止測試的時候出現 Address already in use
result = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
if (-1 == result) {
perror ("Set socket");
return 0;
}
server_addr.sin_family = AF_INET;
inet_aton (local_addr, &(server_addr.sin_addr));
server_addr.sin_port = htons(8080);
result = bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr));
if (-1 == result) {
perror("Bind port");
return 0;
}
result = make_socket_non_blocking(listenfd);
if (-1 == result) {
return 0;
}
result = listen(listenfd, 200);
if (-1 == result) {
perror("Start listen");
return 0;
}
// 創(chuàng)建epoll實例
epfd = epoll_create1(0);
if (1 == epfd) {
perror("Create epoll instance");
return 0;
}
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET /* 邊緣觸發(fā)選項腿倚。 */;
// 設置epoll的事件
result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
if(-1 == result) {
perror("Set epoll_ctl");
return 0;
}
for ( ; ; ) {
int wait_count;
// 等待事件
wait_count = epoll_wait(epfd, event, MAX_EVENT, -1);
for (int i = 0 ; i < wait_count; i++) {
uint32_t events = event[i].events;
// IP地址緩存
char host_buf[NI_MAXHOST];
// PORT緩存
char port_buf[NI_MAXSERV];
int __result;
// 判斷epoll是否發(fā)生錯誤
if ( events & EPOLLERR || events & EPOLLHUP || (! events & EPOLLIN)) {
printf("Epoll has error\n");
close (event[i].data.fd);
continue;
} else if (listenfd == event[i].data.fd) {
// listen的 file describe 事件觸發(fā), accpet事件
for ( ; ; ) { // 由于采用了邊緣觸發(fā)模式褪测,這里需要使用循環(huán)
struct sockaddr in_addr = { 0 };
socklen_t in_addr_len = sizeof (in_addr);
int accp_fd = accept(listenfd, &in_addr, &in_addr_len);
if (-1 == accp_fd) {
perror("Accept");
break;
}
__result = getnameinfo(&in_addr, sizeof (in_addr),
host_buf, sizeof (host_buf) / sizeof (host_buf[0]),
port_buf, sizeof (port_buf) / sizeof (port_buf[0]),
NI_NUMERICHOST | NI_NUMERICSERV);
if (! __result) {
printf("New connection: host = %s, port = %s\n", host_buf, port_buf);
}
__result = make_socket_non_blocking(accp_fd);
if (-1 == __result) {
return 0;
}
ev.data.fd = accp_fd;
ev.events = EPOLLIN | EPOLLET;
// 為新accept的 file describe 設置epoll事件
__result = epoll_ctl(epfd, EPOLL_CTL_ADD, accp_fd, &ev);
if (-1 == __result) {
perror("epoll_ctl");
return 0;
}
}
continue;
} else {
// 其余事件為 file describe 可以讀取
int done = 0;
// 因為采用邊緣觸發(fā)猴誊,所以這里需要使用循環(huán)。如果不使用循環(huán)侮措,程序并不能完全讀取到緩存區(qū)里面的數據。
for ( ; ;) {
ssize_t result_len = 0;
char buf[READ_BUF_LEN] = { 0 };
result_len = read(event[i].data.fd, buf, sizeof (buf) / sizeof (buf[0]));
if (-1 == result_len) {
if (EAGAIN != errno) {
perror ("Read data");
done = 1;
}
break;
} else if (! result_len) {
done = 1;
break;
}
write(STDOUT_FILENO, buf, result_len);
}
if (done) {
printf("Closed connection\n");
close (event[i].data.fd);
}
}
}
}
close (epfd);
return 0;
}