Linux的I/O复用之epoll:EPOLLONESHOT事件
即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序中就会引起一个问题,比如一个线程在读取某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新的数据可读,此时另外一个线程被唤醒来读取这些新的数据,于是就出现两个线程同时操作一个socket的局面,这当然不是我们期望的,我们期望的是一个socket连接在任一时刻都只被一个线程处理,这就要用到EPOLLONSHORT。
对于注册了EPOLLONSHORT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONSHORT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。注册了EPOLLONSHORT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONSHORT事件,以确保socket下一次可读时,其他EPOLLIN事件能被触发,进而让其他线程有机会继续处理这个socket。
epolloneshot_server.cpp:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <fcntl.h> #include <stdlib.h> #include <sys/epoll.h> #include <pthread.h> struct fds { int epollfd; int sockfd; }; pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; int setnonblocking(int fd) { int oldt = fcntl(fd, F_GETFL); int newt = oldt | O_NONBLOCK; fcntl(fd, F_SETFL, newt); return oldt; } /*将fd上的EPOLLIN和EPOLLET事件注册到epollfd指示的epoll内核事件表中,参数oneshot指定是否注册fd上的EPOLLONESHOT事件*/ void addfd(int epollfd, int fd, bool oneshot) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN | EPOLLET; if (oneshot) { event.events |= EPOLLONESHOT; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); setnonblocking(fd); } /*重置fd上的事件。这样操作之后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次*/ void resetOneshot(int epollfd, int fd) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event); } /*工作线程*/ void *worker(void* arg) { int sockfd = ((fds*)arg)->sockfd; int epollfd = ((fds*)arg)->epollfd; printf("start new thread to receive data on fd: %d\n", sockfd); char buf[1024]; memset(buf, '\0', 1024); //循环读取sockfd上的数据,直到遇到EAGAIN错误 while(1) { int ret = recv(sockfd, buf, 1023, 0); if (ret == 0) { close(sockfd); printf("foreigner closed the connection\n"); break; } else if(ret < 0) { if (errno == EAGAIN) { resetOneshot(epollfd, sockfd); printf("thread1 read later\n"); break; } } else { printf("get content: %s\n", buf); //休眠5秒,模拟数据处理过程 sleep(5); } } printf("end thread receiving data on fd: %d\n", sockfd); } int main(int argc, char* argv[]) { if (argc <= 2) { printf("usage: %s ip_address port_number\n", basename(argv[0])); return 1; } const char* ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; inet_pton(AF_INET, ip, &addr.sin_addr); addr.sin_port = htons(port); int listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(listenfd >= 0); ret = bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)); assert(ret != -1); ret = listen(listenfd, 5); assert(ret != -1); epoll_event events[1024]; int epollfd = epoll_create(5); assert(epollfd != -1); /*注意,监听socket listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序只能处理一个客户连接。 因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件。 */ addfd(epollfd, listenfd, false); while(1) { int ret = epoll_wait(epollfd, events, 1024, -1); if (ret < 0) { printf("epoll failure\n"); break; } int i; for(i=0; i<ret; i++) { int sockfd = events[i].data.fd; if (sockfd == listenfd) { struct sockaddr_in clientAddr; socklen_t clientAddrLen = sizeof(clientAddr); int connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &clientAddrLen); //对每个非监听文件描述符都注册EPOLLONESHOT事件。 addfd(epollfd, connfd, true); } else if (events[i].events & EPOLLIN) { pthread_t thread1,thread2; fds fdsWorker; fdsWorker.epollfd = epollfd; fdsWorker.sockfd = sockfd; //新启动一个工作线程为sockfd服务 pthread_create(&thread, NULL, worker, (void*)&fdsWorker); } else { printf("something else happened\n"); } } } close(listenfd); return 0; }
从工作线程函数worker来看,如果一个工作线程处理完某个socket上的一次请求(用休眠5s来模拟这个过程)之后,又接收到该socket上新的客户请求,则该线程将继续为这个socket服务。并且因为该socket上注册了EPOLLONESHOT事件,其他线程没有机会接触这个socket,如果工作线程等待5s后仍然没收到该socket上的下一批客户数据,则它将放弃为该socket服务。同时,它调用reset_oneshot函数来重置该socket的注册事件,这将使epoll有机会再次检测到该socket的EPOLLIN事件,进而使得其他线程有机会为该socket服务。
由此看来,尽管一个socket在不同时间能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务。这就保证了连接的完整性,从而避免了很多可能的竟态条件。