1、内核事件表
epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都是重复传入描述符或者时间集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用如下的epoll_create函数来创建:
1
2 |
#include<sys/epoll.h>
int epoll_create( int size); |
下面的函数用来操作epoll的内核事件表:
1
2 |
#include<sys/epoll.h>
int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event); |
?EPOLL_CTL_ADD ,往事件表中注册fd上的事件。
?EPOLL_CTL_MOD,修改fd上的注册事件。
?EPOLL_CTL_DEL,删除fd上的注册事件。
event参数指定事件,它是epoll_event结构指针类型。epoll_event的定义如下:
1
2 3 4 5 |
struct epoll_event
{ __uint32_t event; //epoll事件 epoll_data_t data; //用户数量 }; |
1
2 3 4 5 6 7 |
typedef
union epoll_data
{ void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; |
epoll_ctl 成功时返回0,失败则返回-1,并设置errno。
2、epoll_wait函数
epoll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件,其原型如下:
1
2 |
#include<sys/epoll.h>
int epoll_wait( int epfd, struct epoll_event *events, int maxvents, int timeout); |
关于该函数的参数,我们从后往前讨论。timeout参数的含义与poll接口的timeout参数相同。maxevents参数指定最多监听多少个事件,它必须大于0.
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪时间,而不像select和poll的数组参数那样即用于用户注册的事件,又用于输出内核检测的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。
poll和epoll在使用上的差别
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//如何索引poll返回的就绪文件描述 int ret = poll(fds, MAX_EVENT_NUMBER, - 1); //必须遍历所有已注册文件描述符并找到其中的就绪这 //(当然,可以利用ret来稍做优化) for( int i = 0; i < MAX_EVENT_NUMBER; ++i) { if(fds[i].revents & POLLIN) { int sockfd = fds[i].fd; //处理sockfd } } //如何索引epoll返回的就绪文件描述符 int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, - 1); //仅遍历就绪的ret个文件描述符 for( int i = 0; i < ret; i++) { int sockfd = events[i].data.fd; //sockfd肯定就绪,直接处理 } |
3、LT和ET模式
epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)模式和ET(Edge Trigger,边沿触发)模式,LT模式是默认的工作模式,这种模式下epoll相当于效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,知道该事件被处理。而对采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件,可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。
体现LT和ET工作方式的差异代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
#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> #define MAX_EVENT_NUMBER 1024 #define BUFFER_SIZE 10 //将文件描述符设置成非阻塞 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; } //将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数enable_et //指定是否对fd启用ET模式 void addfd( int epollfd, int fd, bool enable_et ) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN; if( enable_et ) { event.events |= EPOLLET; //ET模式 } epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); setnonblocking( fd ); } //LT模式的工作流程 void lt( epoll_event *events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr * )&client_address, &client_addrlength ); addfd( epollfd, connfd, false ); //对connfd禁用ET模式 } else if ( events[i].events & EPOLLIN ) { //只要socket读缓存中还有未读出的数据,这段代码被触发 printf( "event trigger once\n" ); memset( buf, '\0', BUFFER_SIZE ); int ret = recv( sockfd, buf, BUFFER_SIZE - 1, 0 ); if( ret <= 0 ) { close( sockfd ); continue; } printf( "get %d bytes of content: %s\n", ret, buf ); } else { printf( "something else happened \n" ); } } } //ET模式的工作流程 void et( epoll_event *events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr * )&client_address, &client_addrlength ); addfd( epollfd, connfd, true ); } else if ( events[i].events & EPOLLIN ) { //这段代码不会被重复触发,所以我们循环读取数据 //以确保把socket读缓存中的所有数据独处 printf( "event trigger once\n" ); while( 1 ) { memset( buf, '\0', BUFFER_SIZE ); int ret = recv( sockfd, buf, BUFFER_SIZE - 1, 0 ); if( ret < 0 ) { //对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。此后epoll //就能再次触发sockfd上的EPOLLIN时间,以驱动下一次读操作。 if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) ) { printf( "read later\n" ); break; } close( sockfd ); break; } else if( ret == 0 ) { close( sockfd ); } else { printf( "get %d bytes of content: %s\n", ret, buf ); } } } else { printf( "something else happened \n" ); } } } 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 address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( listenfd >= 0 ); ret = bind( listenfd, ( struct sockaddr * )&address, sizeof( address ) ); assert( ret != - 1 ); ret = listen( listenfd, 5 ); assert( ret != - 1 ); epoll_event events[ MAX_EVENT_NUMBER ]; int epollfd = epoll_create( 5 ); assert( epollfd != - 1 ); addfd( epollfd, listenfd, true ); while( 1 ) { int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, - 1 ); //阻塞于epoll事件 if ( ret < 0 ) { printf( "epoll failure\n" ); break; } lt( events, ret, epollfd, listenfd ); //使用LT模式 //et( events, ret, epollfd, listenfd );//使用ET模式 } close( listenfd ); return 0; } |
每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,那么读或者写操作将会因为没有后续的事件而一直处理阻塞状态(饥渴状态)。
4、EPOLLONESHOT事件(防止两个线程操作一个socket)
即使我们使用ET模式,一个socket上的某个时间还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程,下同)在读取完某个socket上色数据后开始处理这些数据,而在数据的处理过程中该socket上又有心数据可读(EPOLLIN再次触发),此时另外一个线程被唤醒来读取这些信的数据。于是就出现了两个线程同时操作一个socket局面。这当然不是我们期望的。我们期望的是一个socket连接在任一时刻都只被一个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个某个socket时,其他线程是不可能有机会操作该socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕后,该线程就应该立即重置这个socket的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。
如下是EPOLLONETSHOT事件的使用。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
#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> #define MAX_EVENT_NUMBER 1024 #define BUFFER_SIZE 1024 struct fds { int epollfd; int sockfd; }; 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; } //将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; //设置oneshot } epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); setnonblocking( fd ); } //重置fd上的事件,这样操作之后,尽管fd上的EPOLLONESHOT事件被注册,但是 //操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次 void reset_oneshot( 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[ BUFFER_SIZE ]; memset( buf, '\0', BUFFER_SIZE ); //循环读取sockfd上的数据,直到遇到EAGAIN错误 while( 1 ) { int ret = recv( sockfd, buf, BUFFER_SIZE - 1, 0 ); if( ret == 0 ) { close( sockfd ); printf( "foreiner closed the connection\n" ); break; } else if( ret < 0 ) { if( errno == EAGAIN ) { reset_oneshot( epollfd, sockfd ); printf( "read later\n" ); break; } } else { printf( "get content: %s\n", buf ); 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 address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( listenfd >= 0 ); ret = bind( listenfd, ( struct sockaddr * )&address, sizeof( address ) ); assert( ret != - 1 ); ret = listen( listenfd, 5 ); assert( ret != - 1 ); epoll_event events[ MAX_EVENT_NUMBER ]; int epollfd = epoll_create( 5 ); assert( epollfd != - 1 ); addfd( epollfd, listenfd, false ); //监听socke listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序只能处理 //一个客户连接,因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件 while( 1 ) { int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, - 1 ); if ( ret < 0 ) { printf( "epoll failure\n" ); break; } for ( int i = 0; i < ret; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr * )&client_address, &client_addrlength ); //对每个非监听文件描述符都注册EPOLLONESHOT事件 addfd( epollfd, connfd, true ); } else if ( events[i].events & EPOLLIN ) { pthread_t thread; fds fds_for_new_worker; fds_for_new_worker.epollfd = epollfd; fds_for_new_worker.sockfd = sockfd; //新启动一个工作线程为sockfd服务 pthread_create( &thread, NULL, worker, ( void * )&fds_for_new_worker ); } else { printf( "something else happened \n" ); } } } close( listenfd ); return 0; } |
由此看来,尽管一个socket在不同时间可能被不同的线程处理,但同一时刻肯定只有一个线程为它服务。这就保证了连接完整性,从而避免了很多可能的竞态条件。
5、I/O复用的应用:同时处理TCP和UDP服务
从bind系统调用的参数来看,一个socket只能与一个socket地址绑定,即一个socket只能用来监听一个端口。因此,服务器如果要同时监听多个端口,就必须创建多个socket,并将它们分别绑定在各个端口上。这样一来,服务器要同时管理多个监听socket,I/O复用就有用武之地了。另外,即使是同一个端口,如果服务器要同时处理该端口上的TCP和UDP请求,则也需要创建两个不同的socket:一个是流socket,另一个是数据报socket,并将它们都绑定到该端口上。代码如下所示:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
#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> #define MAX_EVENT_NUMBER 1024 #define TCP_BUFFER_SIZE 512 #define UDP_BUFFER_SIZE 1024 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; } void addfd( int epollfd, int fd ) { epoll_event event; event.data.fd = fd; //event.events = EPOLLIN | EPOLLET; event.events = EPOLLIN; epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); setnonblocking( fd ); } 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 address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); //创建TCP_socket,并将其绑定到端口port上 int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( listenfd >= 0 ); ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != - 1 ); ret = listen( listenfd, 5 ); assert( ret != - 1 ); //创建UDP_socket,并将其绑定到端口port上 bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int udpfd = socket( PF_INET, SOCK_DGRAM, 0 ); assert( udpfd >= 0 ); ret = bind( udpfd, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != - 1 ); epoll_event events[ MAX_EVENT_NUMBER ]; int epollfd = epoll_create( 5 ); assert( epollfd != - 1 ); //注册TCP socket和UDPsocket到可读事件上 addfd( epollfd, listenfd ); addfd( epollfd, udpfd ); while( 1 ) { int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, - 1 ); //阻塞 if ( number < 0 ) { printf( "epoll failure\n" ); break; } for ( int i = 0; i < number; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) //接收tcp数据 { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength ); addfd( epollfd, connfd ); //设置另外读出来 } else if ( sockfd == udpfd ) //接收udp数据 { char buf[ UDP_BUFFER_SIZE ]; memset( buf, '\0', UDP_BUFFER_SIZE ); struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); ret = recvfrom( udpfd, buf, UDP_BUFFER_SIZE- 1, 0, ( struct sockaddr* )&client_address, &client_addrlength ); if( ret > 0 ) { sendto( udpfd, buf, UDP_BUFFER_SIZE- 1, 0, ( struct sockaddr* )&client_address, client_addrlength ); } } else if ( events[i].events & EPOLLIN ) //读取TCP数据 { char buf[ TCP_BUFFER_SIZE ]; while( 1 ) { memset( buf, '\0', TCP_BUFFER_SIZE ); ret = recv( sockfd, buf, TCP_BUFFER_SIZE- 1, 0 ); if( ret < 0 ) { if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) ) { break; } close( sockfd ); break; } else if( ret == 0 ) { close( sockfd ); } else { send( sockfd, buf, ret, 0 ); } } } else { printf( "something else happened \n" ); } } } close( listenfd ); return 0; } |