epoll 是 linux 特有的 I/O 復用函數。它是把用戶關心的文件描述符事件放在內核的一個事件列表中,故而,無須像select和poll一樣每次調用都重復傳入文件描述符或事件集。但是, epoll 需要一個額外的文件描述符,來唯一標識內核中的這個事件表。這個文件描述符由 epoll_create 函數來創建:
#include <sys/epoll.h>
int epoll_create(int size);
size 參數現在是被忽略的,但是,為了兼容性,需要傳入一個大於0的數。
epoll_ctl 函數來操作epoll的內核事件表:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd是epoll_create返回的文件描述符,op指定操作類型,有如下三種:
/* Valid opcodes ( "op" parameter ) to issue to epoll_ctl(). */
#define EPOLL_CTL_ADD 1 /* Add a file descriptor to the interface. */
#define EPOLL_CTL_DEL 2 /* Remove a file descriptor from the interface. */
#define EPOLL_CTL_MOD 3 /* Change file descriptor epoll_event structure. */
event 參數指定事件,它是 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 是成員描述符事件類型。事件類型也定義在 sys/epoll.h 文件中
enum EPOLL_EVENTS
{
EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
EPOLLEXCLUSIVE = 1u << 28,
#define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
EPOLLET = 1u << 31
#define EPOLLET EPOLLET
};
data 是 epoll_data_t 聯合體類型。可以用fd 表示文件描述符,或者用ptr指針指向更多的用戶數據。
epoll 系列系統調用的主要接口是epoll_wait函數,它在一段超時時間內等待一組文件描述符上的事件,定義:
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
成功時,返回就緒文件描述符的個數,失敗返回-1,並設置errno
其中,timeout指定超時時間,單位毫秒。-1表示永遠等待直到有文件描述符就緒。
maxevents 指定最多監聽多少個事件,它必須大於0
epoll_wait 函數如果檢測到時間,就將事件從內核事件表中復制到第二個參數events指向的數組中。這個數組只輸出epoll_wait檢測到的就緒事件。
epoll 對文件描述符的操作有兩種模式:LT(level trigger 電平觸發)和 ET(edge trigger 邊沿觸發)。LT是默認的工作模式。在這種模式下,文件描述符會一直被檢測到直到應用程序處理它。ET模式下,文件描述符就緒,被通知給應用程序,之后,就不再通知該同一事件了。ET模式降低了同一個epoll時間被重復觸發的次數,因此效率較高。
EPOLLONESHOT事件
對於注冊了EPOLLONESHOT事件的文件描述符,操作系統最多觸發其上注冊的一個可讀、可寫、異常事件,且只觸發一次,除非我們使用 epoll_ctl 函數重置該文件描述符上注冊的 EPOLLONESHOT 事件。這樣就不會用多並發的問題。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <signal.h>
#include <map>
#include <string>
using namespace std;
#define CLIENTSIZE 5000
#define BUFSIZE 4000
int createSocket()
{
struct sockaddr_in servaddr;
int listenfd = -1;
if (-1 == (listenfd = socket(PF_INET, SOCK_STREAM, 0)))
{
fprintf(stderr, "socket: %d, %s\n", errno, strerror(errno));
exit(1);
}
int reuseaddr = 1;
if (-1 == setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)))
{
fprintf(stderr, "setsockopt: %d, %s\n", errno, strerror(errno));
exit(1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = PF_INET;
servaddr.sin_port = htons(8008);
inet_pton(PF_INET, "0.0.0.0", &servaddr.sin_addr);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)))
{
fprintf(stderr, "bind: %d, %s\n", errno, strerror(errno));
exit(1);
}
if (-1 == listen(listenfd, 5))
{
fprintf(stderr, "listen: %d, %s\n", errno, strerror(errno));
exit(1);
}
return listenfd;
}
int setnoblock(int fd)
{
int oldopt = fcntl(fd, F_GETFL);
int newopt = oldopt | O_NONBLOCK;
fcntl(fd, F_SETFL, newopt);
return oldopt;
}
void ErrExit(const char* reason)
{
fprintf(stderr, "%s: %d, %s\n", reason, errno, strerror(errno));
exit(1);
}
void addsig(int sig, void (*handler)(int))
{
int olderrno = errno;
struct sigaction ac;
memset(&ac, 0, sizeof(ac));
ac.sa_handler = handler;
ac.sa_flags |= SA_RESTART;
sigfillset(&ac.sa_mask);
if (-1 == sigaction(sig, &ac, NULL))
{
ErrExit("sigaction");
}
errno = olderrno;
}
void addfd(int epfd, int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET | EPOLLERR;
ev.data.fd = fd;
if (-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev))
{
ErrExit("epoll_ctl");
}
setnoblock(fd);
}
void delfd(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;
if (-1 == epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &ev))
{
ErrExit("epoll_ctl");
}
}
int main(int argc, char const *argv[])
{
int listenfd = createSocket();
int epfd = -1;
map<int, string> mapdata;
if (-1 == (epfd = epoll_create(CLIENTSIZE)))
{
ErrExit("epoll_create");
}
struct epoll_event evs[CLIENTSIZE];
addfd(epfd, listenfd);
while (1)
{
int connnum = epoll_wait(epfd, evs, CLIENTSIZE, -1);
for (int i = 0; i < connnum; ++i)
{
if (evs[i].events & EPOLLERR)
{
printf("%d exit\n", evs[i].data.fd);
delfd(epfd, evs[i].data.fd);
close(evs[i].data.fd);
mapdata.erase(evs[i].data.fd);
}
else if (evs[i].data.fd == listenfd && (evs[i].events & EPOLLIN))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(listenfd, (struct sockaddr*)&client, &len);
if (cfd == -1)
{
ErrExit("accept");
}
printf("get connection: %d\n", cfd);
addfd(epfd, cfd);
}
else if (evs[i].events & EPOLLIN)
{
char buf[BUFSIZE] = {0};
int len = recv(evs[i].data.fd, buf, BUFSIZE-1, 0);
if (len > 0)
{
mapdata[evs[i].data.fd] = buf;
evs[i].events &= (~EPOLLIN);
evs[i].events |= EPOLLOUT;
if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, evs[i].data.fd, &evs[i]))
{
ErrExit("epoll_ctl");
}
}
else if (len == 0)
{
printf("%d exit\n", evs[i].data.fd);
delfd(epfd, evs[i].data.fd);
close(evs[i].data.fd);
mapdata.erase(evs[i].data.fd);
}
else
{
ErrExit("recv");
}
}
else if (evs[i].events & EPOLLOUT)
{
if (send(evs[i].data.fd, mapdata[evs[i].data.fd].c_str(), mapdata[evs[i].data.fd].size(), 0) < 0)
{
if (errno == 104)
{
continue;
}
ErrExit("send");
}
evs[i].events &= (~EPOLLOUT);
evs[i].events |= EPOLLIN;
if (-1 == epoll_ctl(epfd, EPOLL_CTL_MOD, evs[i].data.fd, &evs[i]))
{
ErrExit("epoll_ctl");
}
}
}
}
close(listenfd);
close(epfd);
return 0;
}