epoll的ET和LT模式
LT水平触发是默认的模式,只要缓冲区有消息就会触发,如果这次事件没有被处理,那么下一次调用epoll_wait
的时候,事件仍然会被触发;ET边沿触发只会在第一次有消息的时候触发,之后再次调用epoll_wait
的时候,事件不会被再次触发了。Linux的epoll
默认是LT模式。
下面这一段来自于:https://blog.csdn.net/liuxuejiang158blog/article/details/12290725
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。在epoll的ET模式下,正确的读写方式为:读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN;写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
也就是说,ET模式是在不可读写变化到可读写的时候,才会触发。
编程时的应用
如果使用默认的LT处理模式,则意味着socket只要可读或者可写,那么就会一直有事件发生,这样可以防止事件被忽略,但是会造成额外的开销;如果使用ET模式,则要求程序员获取事件后,要一直处理socket,因为socket再次可读或者可写的时候,不会再有事件发生了。
ET或者LT模式更多地是和阻塞与非阻塞模式进行配合。可以这总结:如果使用了ET模式,那么必须使用while
循环处理socket
上所有的数据或者消息,否则可能会存在有数据永远不会被读取的情况。
ET模式的优势在于开销小,不用过度处理消息,但是要求程序员必须一次性处理完一个可读写事件,否则下次不会激发。只有状态再次变化的时候才行。
补充一点:accept
函数不论什么时候,都要设置为非阻塞模式,参考这篇博客,给出理由:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。 解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误.
下面给出一个一般的例子:
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <string.h>
#define MAX_EVENTS 200
#define MAX_BUFFER_SIZE 1000
int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8001);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("bind() error\n");
close(listenfd);
return 1;
}
ret = listen(listenfd, 32);
if (ret < 0) {
perror("listen() error\n");
close(listenfd);
return 1;
}
setnonblocking(listenfd); // 设置为非阻塞模式
int epollfd = epoll_create1(0);
if (epollfd < 0) {
perror("epoll_create1() error\n");
close(listenfd);
return 1;
}
epoll_event events[MAX_EVENTS];
epoll_event ev;
bzero(&ev, sizeof(ev));
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP; // 监听到来的连接、ET模式、用户主动断开连接
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
if (ret < 0) {
perror("epoll_ctl() error\n");
close(listenfd);
close(epollfd);
return 1;
}
while (1) {
int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds < 0) {
perror("epoll_wait error\n");
close(listenfd);
close(epollfd);
return 1;
}
int connfd = -1;
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listenfd) {
struct sockaddr_in client_address;
socklen_t client_addrlen = sizeof(client_address);
// 这里使用while循环处理accept,否则如果并发连接,ET模式的accept无法激发后续的连接事件
while ((connfd = accept(listenfd, (struct sockaddr*)& client_address,
&client_addrlen)) > 0) {
// 在这里注册新的连接
ev.data.fd = connfd;
ev.events &= 0;
ev.events |= EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &ev);
// 下面添加一些新的操作
}
} else if (events[i].events == EPOLLRDHUP) { // 断开连接应该在EPOLLIN之前,以免不确定事件发生
// 有用户断开连接,取消注册
ev.data.fd = connfd;
ev.events &= 0;
ev.events |= EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
// 添加其它自定义操作
} else if (events[i].events == EPOLLIN) { // 有用户添加数据
char buf[MAX_BUFFER_SIZE];
memset(buf, 0, MAX_BUFFER_SIZE);
// ET模式必须是一次性循环接受完毕,MSG_DONTWAIT参数可以更改,在这里是非阻塞模式
while((ret = recv(events[i].data.fd, buf, MAX_BUFFER_SIZE, MSG_DONTWAIT)) > 0) {
// 这里是循环处理数据
}
if (ret < 0) {
perror("recv() error\n");
// 添加错误处理代码
}
} else {
perror("unknown message\n");
break;
}
}
}
return 0;
}