1. 为什么需要epoll
select和poll的特点是:用户给内核一张 需要查看的文件描述符表,内核必须处理表中的每一个文件描述符,当这个表变大时,程序性能降低。
epoll将事件监视器的注册和事件监视工作分离,以避开这个问题。
2. api
// @size : 用于提示内核要监控的文件描述符数目,并非最大数目,用于提升性能
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *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 */
};
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
#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 *) &addr, &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);
}
}
}
3. 边缘触发和水平触发
若将epoll_ctl 的 event.events 设置为 EPOLLET,则使用边缘触发。
比如,如下事件
- 生产者将1KB数据写入管道
- 消费者对管道调用epoll_wait,等待管道送来数据并因此而读取。
若使用水平触发,则在调用2立即返回,表示读管道不会阻塞。
若使用边缘触发,则在调用1发生返回,说明 即使 管道已经可读,但epoll_wait也不会返回,除非再次发生写管道事件。
水平触发是默认行为,边缘触发需要使用不同的程序设计方法,通常用于非阻挡IO并且仔细检测EAGAIN.