epoll_create
#include <sys/epoll.h>
int epoll_create(int size);
返回值:
success:返回一个非0 的未使用过的最小的文件描述符
error:-1 errno被设置
参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。
需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下查看/proc/进程id/fd/,是能够看到这个fd的( eg: ls /proc/$(ps -aux | grep './main' | awk 'NR==1 { print $2 }')/fd
),所以在使用完 epoll 后,必须调用close()
关闭,否则可能导致 fd 被耗尽。
- epoll_create1
int epoll_create1(int flags); - flags: - 如果这个参数是0,这个函数等价于epoll_create(0) - EPOLL_CLOEXEC:这是这个参数唯一的有效值,如果这个参数设置为这个。 那么当进程替换映像的时候会关闭这个文件描述符,这样新的映像中就无法对这个文件描述符操作, 适用于多进程编程+映像替换的环境里
epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数
第一个参数是 epoll_create() 的创建的 epoll 实例。
第二个参数表示动作,用3个宏表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是内核需要监听什么事件,struct epoll_event 结构如下:
#include <sys/epoll.h>
struct epoll_event {
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
- EPOLLIN
表示对应的文件描述符可以读(包括对端SOCKET正常关闭) - EPOLLOUT
表示对应的文件描述符可以写 - EPOLLPRI
表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) - EPOLLERR
表示对应的文件描述符发生错误 - EPOLLHUP
表示对应的文件描述符被挂断 - EPOLLET
将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
等待事件的产生。
参数 events 用来从内核得到事件的集合。
参数 maxevents 告之内核这个events有多大(数组成员的个数)。
参数 timeout 是超时时间,单位毫秒(0会立即返回,-1将是永久阻塞)。
该函数返回需要处理的事件数目,返回的事件集合在 events 数组中,如返回 0 表示已超时。
- maxevents 参数设置多少合适?
muduo 中的思路是动态扩张:开始是 n 个,当发现有事件的 fd 数量已经到达 n 个后,将 struct epoll_event 数量调整成 2n 个,下次如果还不够,则变成 4n 个,以此类推。// 初始化代码 std::vector<struct epoll_event> events_(16); // 线程循环里面的代码 while (m_bExit) { int numEvents = ::epoll_wait(epollfd_, &*events_.begin(), static_cast<int>(events_.size()), 1); if (numEvents > 0) { if (static_cast<size_t>(numEvents) == events_.size()) { events_.resize(events_.size() * 2); } } }
- timeout 超时时间设置多少合适?
muduo 中epoll_wait 的超时事件设置为 1 毫秒。
代码示例
服务器
#include "lib/common.h"
#define MAXEVENTS 128
char rot13_char(char c)
{
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
int main(int argc, char **argv)
{
int listen_fd, socket_fd;
int n, i;
int efd;
struct epoll_event event;
struct epoll_event *events;
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
efd = epoll_create1(0);
if (efd == -1) {
error(1, errno, "epoll create failed");
}
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
error(1, errno, "epoll_ctl add listen fd failed");
}
/* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof(event));
while (1) {
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("epoll_wait wakeup\n");
for (i = 0; i < n; i++) {
if ((events[i].events & EPOLLERR) ||
(events[i].events & EPOLLHUP) ||
(!(events[i].events & EPOLLIN))) {
fprintf(stderr, "epoll error\n");
close(events[i].data.fd);
continue;
} else if (listen_fd == events[i].data.fd) {
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
} else {
make_nonblocking(fd);
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET; //edge-triggered
if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1) {
error(1, errno, "epoll_ctl add connection fd failed");
}
}
continue;
} else {
socket_fd = events[i].data.fd;
printf("get event on socket fd == %d \n", socket_fd);
while (1) {
char buf[512];
if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
if (errno != EAGAIN) {
error(1, errno, "read error");
close(socket_fd);
}
break;
} else if (n == 0) {
close(socket_fd);
break;
} else {
for (i = 0; i < n; ++i) {
buf[i] = rot13_char(buf[i]);
}
if (write(socket_fd, buf, n) < 0) {
error(1, errno, "write error");
}
}
}
}
}
}
free(events);
close(listen_fd);
}
LT和ET 工作模式
epoll对文件描述符的操作有2种模式:LT和ET。
LT模式(水平触发):
LT(水平触发)是默认的工作模式,并且同时支持阻塞和非阻塞socket。
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有时间发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次相应应用程序通告此事件,直到该事件被处理。-
ET模式(边沿触发):
ET是一种高效的模式,只支持非阻塞socket。
对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此时间通知应用程序以后,应用程序可以不立即处理该事件,但是后续的epoll_wait将不再向应用程序通知这一事件。- 为什么将ET称为高效的工作方式了?
因为ET不用多次触发,减少了每次epoll_wait可能需要返回的fd数量,在并发event数量极多的情况下能够加快epoll_wait的处理速度。
注意 epoll 工作在 ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的 阻塞读/阻塞写 操作把处理多个文件描述符的任务饿死。
- 为什么将ET称为高效的工作方式了?
如果对于一个非阻塞 socket,如果使用 epoll 边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把 socket 上的数据收取干净才行,也就是一定要循环调用 recv 函数直到 recv 出错,错误码是 EWOULDBLOCK
或者 EAGAIN
。
如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。边缘模式下收取数据的代码示例如下:
参考资料
[1]《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff
[2] http://www.cnblogs.com/ajianbeyourself/p/5859989.html
[3] https://blog.csdn.net/hnlyyk/article/details/50946194