epoll是I/O复用模型中相对epoll和select更高效的实现对套接字管理的函数。
epoll有两种模式 LT 和 ET
二者的差异在于 level-trigger 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。
LT和ET模式下对于accept和read,write函数的使用有所不同
一、ET模式下(转载)Q1:调用accept时,到底TCP完成队列里有多少个已经建立好的连接?这里又得分情况来说:
- 没有连接。这种情况发生在TCP连接被客户端夭折,即在服务端调用accept之前客户端给出一个RST。该RST导致刚刚建立好的连接从服务器端的TCP完成队列中被移出。源自berkeley的实现会在内核处理该事件,并不会将该事件通知给服务程序,如果套接口被设置为阻塞模式,就会导致accept函数被阻塞,程序挂起,一直要等到下一个连接到来,这也是为什么采用非阻塞的accept的原因;其他的实现通常会返回ECONNABORTED或EPROTO错误。
解决方法:采用非阻塞的accept,在accept返回后处理ECONNABORTED、EPROTO、EINTR错误。
- 一个以上的连接。一个连接很容易理解,多个连接的情况出现在有多个连接请求同时到达的情况,由于是边缘触发,epoll_wait只返回一次,然后调用accept处理一个请求。这会导致TCP完成队列中依然存在连接未被处理。
解决办法:
1):将该套接口设置为非阻塞模式,然后将accept用while循环包住,处理完TCP完成队列中的所有连接再跳出循环。
2):将accept放入一个单独的线程中,这样还可以采用阻塞模式。
Q2:read函数应当如何处理?
read函数有可能不能够一次将内核缓冲的数据读完,由于采用的ET模式,epoll_wait不会因为内核缓冲区还有数据而继续读取。这就会导致内核数据不能及时、完全地读到服务程序。
解决办法:将套接口设置为非阻塞,再用while循环包住read一直读到产生EAGAIN错误,采用非阻塞套接口的原因在于防止read被阻塞住。网上看见有的朋友说也可以采用阻塞套接口,判断read的返回值是否小于期望值,小于就证明数据读完,但是个人认为这里存在一个临界情况,比如内核缓冲有2048字节,期望读1024字节,读完第二次后read返回的是1024,等于期望值,于是再读,但此时内核已经没有数据,于是read被阻塞(自个的思考,不知有务否)。
Q3:写缓冲区什么时候可以导致epoll_wait函数返回可写状态?
个人对这个问题不是很清楚,按资料说法是该缓冲区从不可写变为可写则返回,那么该套接口刚创建的时候处于可写状态,是否会导致一直返回不了可写状态,也就是说第一次永远返回不了,因为没有任何调用会使该套接口从不可写变为可写。
另外,写的时候还有些什么要注意的么?
二、LT模式下(转载)
Q1:accept函数
accept函数同样存在连接被夭折的情况,和ET模式下类似。但这种模式下不会存在处理不完多个请求的情况。因为只要有连接存在TCP完成队列中,epoll_wait就会一直返回。
Q2:read函数
同样可以避免因为一次读不完内核缓冲区的数据的情况。
Q3:当内核写缓冲空闲时,可写状态会一直返回,如何处理?(这个曾是tencent的一个面试题)
A1:不将可写条件加入epoll集中,采用非阻塞I/O,有数据可写时,直接写,如果返回是EAGAIN,则将可写加入epoll集中,写完后移出。
通常都是写失败情况下,才会将写加入epoll中操作。
//此回射服务器用边缘触发的模式使用epoll
#include <sys/epoll.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <fcntl.h> void myerr(char *m) { perror(m); exit(0); } int main(int argc, char** argv) { int listenfd; if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) myerr("socket error"); int flags = fcntl(listenfd, F_GETFL, 0); fcntl(listenfd, F_SETFL, flags | O_NONBLOCK); int on = 1; if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) myerr("setsockopt error"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_port = htons(5188); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) myerr("bind error"); if(listen(listenfd, SOMAXCONN) < 0) myerr("listen error"); int epollfd; epollfd = epoll_create(EPOLL_CLOEXEC); struct epoll_event event; event.data.fd = listenfd; event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event); struct epoll_event events[16]={0}; struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr);; int conn, counter=0; char recvbuf[1024]; int i; int nready; for(;;) { nready = epoll_wait(epollfd, events, 16, -1); if(nready == -1) { if(errno == EINTR) continue; myerr("epoll_wait"); } for(i=0; i<nready; i++) { if(events[i].data.fd == listenfd) { while((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) > 0) { printf("ip: %s port: %d ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); printf("%d ", ++counter); } if (conn == -1) if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); event.data.fd = conn; event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event); } else if(events[i].events & EPOLLIN) { conn = events[i].data.fd; if(conn < 0) continue; memset(recvbuf, 0, sizeof(recvbuf)); int ret = read(conn, recvbuf, sizeof(recvbuf)); if(ret == 0) { printf("client close "); close(conn); event = events[i]; epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event); continue; } write(conn, recvbuf, ret); fputs(recvbuf, stdout); } } } return 0; }