epoll是event poll的意思。因为涉及到用户态wait获取到内核返回的读写就绪事件之后、去主动到内核缓冲区获取数据、所以本质上是属于同步非阻塞io模型。linux上边真正的异步非阻塞io模型还没提供、windows倒是有了,但是服务器端仍然是linux的天下,所以现在真正事实上的标准高性能网络io模型仍然是epoll。它也是解决c10k问题的关键突破。
一、epoll在linux中的定义和系统调用
//用户数据载体、用来用户态和内核态交换数据
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
//fd装载入内核的载体
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//三个主要api
int epoll_create(int size);
//在内核空间创建epoll句柄epfd,就是新建一个多路复用器(struct eventpoll对象)然后返回它的文件描述符。可以类比java nio里的selector.open()方法。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//向epfd指向的内核空间里边的多路复用器epoll添加/删除(int op)一个fd以及感兴趣的事件event。这个event是epoll_event类型,里边封装了epoll_data用户态的数据、以及事件类型。可以类比一下java nio里边的register()方法。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//这个就是需要用户态程序去轮询的方法了,可以查到就绪的事件、内核会通过这个方法告知用户态程序就绪的events。一般来说用户态程序拿到一批就绪的事件以后会遍历、然后逐个判断属于哪种事件、比如是connect还是read/write,然后执行不同的逻辑。类比java nio里的selector.selectedKeys()。
//epoll官方例子
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Set up listening socket, 'listen_sock' (socket(),bind(), listen()) */
epollfd = epoll_create(10); //创建多路复用器epollfd
if(epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { //向创建好的多路复用器epollfd添加server socket, 然后指定其event为EPOLLIN即监听客户端连接请求
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for(;;) { //自旋
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //从多路复用器epollfd中查询已就绪的events
if (nfds == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
//主监听socket有新连接,connect事件
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 {
//已建立连接的可读写句柄,read/write事件
do_use_fd(events[n].data.fd);
}
}
}
二、LT模式和ET模式的简单理解
level triggered水平触发和edge triggered边缘触发,指的是epoll在socket读写上的两种通知方式。
1、LT 是默认的模式,支持阻塞和非阻塞socket、epoll_wait获取到内核拷贝来的就绪事件之后、用户态程序如果没有处理完、下次仍然会在调用epoll_wait的时候通知给用户态程序、数据不会丢失因为反复提醒、更加安全。
这里边read的话只要有数据就通知就绪可读了,但是write的话,一般来说socket空闲了、写缓冲区不满就会提醒写就绪、也就是反复的提醒某个socket可写。但此时用户态程序可能并没有对这个socket的写的需求,大量连接的时候这也是个不小的开销,所以一般是在没有数据要发送的时候,由用户态程序把对应的fd写事件从epoll列表里去掉,需要的时候再加进去。
2、ET模式是高速模式,只支持非阻塞socket,如果用户态程序对事件没有处理完,那下一次epoll_wait调用就不会继续通知了。对用户态读写处理的逻辑容错提出了更高的要求、但因为没有反复通知、所以性能更高。简单来说ET模式只在socket的读写状态发生变化的时候通知、状态不变则不通知,比如读缓冲区由无数据到有数据通知read事件、写缓冲区由满到未满通知可写write事件。
通过前面的对比可以看到LT模式比较安全并且代码编写也更清晰,但是ET模式属于高速模式,在处理大高并发场景使用得当效果更好,具体选择什么根据自己实际需要和团队代码能力来选择。