Linux高性能服务器编程(三)

9. I/O复用

I/O复用使得程序可以同时监听多个文件描述符。通常,网络程序在下列情况下需要使用I/O复用技术。

  • 客户端程序要同时处理多个socket。比如本章要讨论的非阻塞connect技术。

9.1 select系统调用

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。

9.1.1 select API

arduino 复制代码
 #include <sys/select.h>
 int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  • nfds参数指定被监听的文件描述符的总数

  • readfds、writefds、exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。传入到内核中会被修改

    • 这三个参数都是fd_set结构指针类型,它仅仅包含一个整型数组,每个元素的每1bit标记一个文件描述符

    • 通常使用下列宏来访问fd_set结构体中的位:

      scss 复制代码
       #include <sys/select.h>
       FD_ZERO(fd_set* fdset);                 /*清空fd_set集合*/
       FD_SET(int fd, fd_set* fdset)           /*将给定的文件描述符加入集合之中*/
       FD_CLR(int fd, fd_set* fdset)           /*将给定的文件描述符从集合中删除*/
       int FD_ISSET(int fd, fd_set* fdset)     /*检测fd在fdset集合中的状态是否发生变化*/
  • timeout参数用来设置select函数的超时时间。如果传递给NULL,则select将一直阻塞,直到某个文件描述符就绪

9.1.2 文件描述符就绪条件

9.1.3 处理带外数据

socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态。

scss 复制代码
 #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>
 ​
 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] );
     printf( "ip is %s and port is %d\n", ip, port );
 ​
     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 );
 ​
     struct sockaddr_in client_address;
     socklen_t client_addrlength = sizeof( client_address );
     int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
     if ( connfd < 0 )
     {
         printf( "errno is: %d\n", errno );
         close( listenfd );
     }
 ​
     char remote_addr[INET_ADDRSTRLEN];
     printf( "connected with ip: %s and port: %d\n", inet_ntop( AF_INET, &client_address.sin_addr, remote_addr, INET_ADDRSTRLEN ), ntohs( client_address.sin_port ) );
 ​
     char buf[1024];
     fd_set read_fds;
     fd_set exception_fds;
 ​
     /*给两个fd_set置空*/
     FD_ZERO( &read_fds );
     FD_ZERO( &exception_fds );
 ​
     int nReuseAddr = 1;
     /*设置socket选项*/
     setsockopt( connfd, SOL_SOCKET, SO_OOBINLINE, &nReuseAddr, sizeof( nReuseAddr ) );
     while( 1 )
     {
         memset( buf, '\0', sizeof( buf ) );
         /*将fd加入到两个不同的fd_set集合中*/
         FD_SET( connfd, &read_fds );
         FD_SET( connfd, &exception_fds );
 ​
         ret = select( connfd + 1, &read_fds, NULL, &exception_fds, NULL );
         printf( "select one\n" );
         if ( ret < 0 )
         {
             printf( "selection failure\n" );
             break;
         }
         /*检测对应的文件描述符是否读就绪*/
         if ( FD_ISSET( connfd, &read_fds ) )
         {
             ret = recv( connfd, buf, sizeof( buf )-1, 0 );
             if( ret <= 0 )
             {
                 break;
             }
             printf( "get %d bytes of normal data: %s\n", ret, buf );
         }
         /*检测对应的文件描述符是否发生异常事件*/
         else if( FD_ISSET( connfd, &exception_fds ) )
         {
             ret = recv( connfd, buf, sizeof( buf )-1, MSG_OOB );
             if( ret <= 0 )
             {
                 break;
             }
             printf( "get %d bytes of oob data: %s\n", ret, buf );
         }
 ​
     }
 ​
     close( connfd );
     close( listenfd );
     return 0;
 }

服务端两次实验截图:

9.2 poll系统调用

poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

arduino 复制代码
 #include <poll.h>
 int poll(struct pollfd* fds, nfds_t nfds, int timeout);
  • fds参数一个pollfd结构体类型数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。它定义如下:

    arduino 复制代码
     struct pollfd {
         int fd;           /*文件描述符*/
         short events;     /*注册的事件*/
         short revents;    /*实际发生的事件,由内核填写*/
     }

    pollfd支持的事件类型如表所示:

    事件 描述 是否可作为输入 是否可作为输出
    POLLIN 数据(包括普通数据和优先数据)可读
    POLLRDNORM 普通数据可读
    POLLPRI 高优先级数据可读,比如TCP外带数据
    POLLOUT 数据(包括普通数据和优先数据)可写
    POLLWRNORM 普通数据可写
    POLLWRBAND 优先级数据可写
    POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作
    POLLERR 错误
    POLLHUP 挂起
    POLLNVAL 文件描述符没有打开
  • ndfs参数指定被监听事件集合fds的大小;

  • timeout参数指定poll的超时值,当timeout置为-1时,poll调用将阻塞直到某个事件发生。

9.3 epoll系列系统调用

epoll使用一组函数来完成任务,而不是单个函数。

9.3.1 内核事件表

epoll把用户关心的文件描述符放到内核的一个内核事件表,避免像select和poll那样每次使用都需要重复传入文件描述符集和事件集。但它需要使用一个额外的文件描述符,来标识内核中的这个事件表。这个文件描述符使用下列函数传递:

arduino 复制代码
 #include <sys/epoll.h>
 int epoll_create(int size)
  • size参数只是对内核的一个提示
  • 该函数的返回值将用作epoll系列其他函数的第一个参数,以指定内核事件表

下面的函数用来操作内核事件表:

arduino 复制代码
 #include <sys/epoll.h>
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
  • fd参数是要操作的文件描述符

  • op参数则指定操作类型

    • EPOLL_CTL_ADD, 往事件表上注册fd上的事件
    • EPOLL_CTL_MOD,修改fd上的注册事件
    • EPOLL_CTL_DEL,删除fd上的注册事件
  • event参数指定事件类型,它是epoll_event结构指针类型。epoll_event的定义如下:

    arduino 复制代码
     struct epoll_event {
         _uint32_t events;      /*epoll事件类型*/
         epoll_data_t data;     /*用户数据*/
     }

    其中的events成员描述事件类型。epoll支持的事件类和poll基本相同,在poll对应的宏前加上'E'即可。其中epoll_data_t的定义如下:

    arduino 复制代码
     typedef union epoll_data {
         void* ptr;
         int fd;                      /*指定事件所从属的目标文件描述符*/
         uint32_t u32;
         uint64_t u64;
     } epoll_data_t;

9.3.2 epoll_wait函数

epoll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件。

arduino 复制代码
 #include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。

9.3.3 LT和ET模式

EPOLL模型详解\] [blog.csdn.net/luseysd/art...](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fluseysd%2Farticle%2Fdetails%2F120996812 "https://blog.csdn.net/luseysd/article/details/120996812") \[关于EPOLL的阻塞和非阻塞以及LT和ET模式\] [blog.csdn.net/luseysd/art...](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fluseysd%2Farticle%2Fdetails%2F120995783 "https://blog.csdn.net/luseysd/article/details/120995783") **Level Triggered (LT) 水平触发** socket接收缓冲区不为空 有数据可读 读事件一直触发 socket发送缓冲区不满 可以继续写入数据 写事件一直触发 符合思维习惯,epoll_wait返回的事件就是socket的状态 **Edge Triggered (ET) 边沿触发** socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件 socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件,仅在状态变化时触发事件 ```ini #include #include #include #include #include #include #include #include #include #include #include #include #include ​ #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; } ​ /* @epollfd:内核时间表的文件描述符 @fd:被操作的目标文件描述符 @enable:指定是否对fd启用ET模式 func:把事件-目标文件描述符添加到内核事件表中 */ 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; } epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); /*设置fd文件描述符为非阻塞的*/ setnonblocking( fd ); } ​ /* @events:就绪的事件-目标文件描述符数组 @number:events数组个数 @epollfd:内核事件表的文件描述符 @listenfd:监听socket文件描述符 */ void lt( epoll_event* events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { /*事件所从属的socket文件描述符*/ int sockfd = events[i].data.fd; /*用监听socket去完成TCP连接,同时为连接socket注册读事件并设置为LT模式*/ 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 ); /*注意:不管是LT模式还是ET模式,connfd这个连接socket文件描述符总是非阻塞的*/ addfd( epollfd, connfd, false ); } /*LT模式下连接socket数据可读事件发生,则读取之*/ else if ( events[i].events & EPOLLIN ) { 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" ); } } } ​ /* @events:就绪的事件-目标文件描述符数组 @number:events数组个数 @epollfd:内核事件表的文件描述符 @listenfd:监听socket文件描述符 */ void et( epoll_event* events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { /*事件所从属的socket文件描述符*/ int sockfd = events[i].data.fd; /*用监听socket去完成TCP连接,同时为连接socket注册读事件并设置为ET模式*/ 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 ); } /*ET模式下连接socket上有读事件发生*/ else if ( events[i].events & EPOLLIN ) { printf( "event trigger once\n" ); while( 1 ) { memset( buf, '\0', BUFFER_SIZE ); int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 ); if( ret < 0 ) { /*非阻塞的recv,当无数据可读时产生的错误; ET模式下最好使用非阻塞式的IO,如果使用阻塞式I/O读取函数 这就会导致在数据读完之后,最后一次read阻塞,因为所有数据已经读取完了*/ 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 ); ​ /*监听socket*/ 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 ); /*监听socket文件描述符加入到内核事件表中,并设置为ET模式*/ addfd( epollfd, listenfd, true ); ​ while( 1 ) { int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 ); if ( ret < 0 ) { printf( "epoll failure\n" ); break; } lt( events, ret, epollfd, listenfd ); //et( events, ret, epollfd, listenfd ); } ​ close( listenfd ); return 0; } ``` **客户端发送数据:** ![image](https://file.jishuzhan.net/article/1767001269500645377/d1b3b79450295b2f29f057034bc1a6f5.webp) **LT模式下服务端接收数据:** ![image](https://file.jishuzhan.net/article/1767001269500645377/23117d4bc6d748f627c94682cbcaf3ac.webp) **ET模式下服务端接收数据:** ![image](https://file.jishuzhan.net/article/1767001269500645377/cc33a47d3ef6696653a73baa94ba1550.webp) #### 9.3.4 EPOLLONESHOT事件 即使使用ET模式,一个socket上的某个事件还是可能被多次触发。在多线程环境下,容易出现同一个socket上的两次上下文数据被不同的线程读取。**我们期望一个socket连接在任何一个时刻都只被一个线程处理,就可以使用EPOLLONESHOT事件实现。** 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个事件,且只触发一次。所以**每次处理完必须使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。** ```ini #include #include #include #include #include #include #include #include #include #include #include #include #include ​ #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; } ​ 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 ); } ​ 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 ); 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 ); ​ 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 ); 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; pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker ); } else { printf( "something else happened \n" ); } } } ​ close( listenfd ); return 0; } ``` ### 9.4 三组I/O复用函数的比较 ### 9.5 I/O复用的高级应用一:非阻塞connect ### 9.6 I/O复用的高级应用二:聊天室程序 客户端:把标准输入零拷贝到发送服务器中 ```ini #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include ​ #define BUFFER_SIZE 64 ​ 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] ); ​ struct sockaddr_in server_address; bzero( &server_address, sizeof( server_address ) ); server_address.sin_family = AF_INET; inet_pton( AF_INET, ip, &server_address.sin_addr ); server_address.sin_port = htons( port ); ​ int sockfd = socket( PF_INET, SOCK_STREAM, 0 ); assert( sockfd >= 0 ); if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 ) { printf( "connection failed\n" ); close( sockfd ); return 1; } ​ pollfd fds[2]; /*标准输入*/ fds[0].fd = 0; fds[0].events = POLLIN; fds[0].revents = 0; fds[1].fd = sockfd; fds[1].events = POLLIN | POLLRDHUP; fds[1].revents = 0; char read_buf[BUFFER_SIZE]; int pipefd[2]; int ret = pipe( pipefd ); assert( ret != -1 ); ​ while( 1 ) { ret = poll( fds, 2, -1 ); if( ret < 0 ) { printf( "poll failure\n" ); break; } /*TCP连接被对方关闭*/ if( fds[1].revents & POLLRDHUP ) { printf( "server close the connection\n" ); break; } /*在终端打印服务器发送过来的数据*/ else if( fds[1].revents & POLLIN ) { memset( read_buf, '\0', BUFFER_SIZE ); recv( fds[1].fd, read_buf, BUFFER_SIZE-1, 0 ); printf( "%s\n", read_buf ); } /*将标准输入的数据经过零拷贝发送给服务端socket*/ if( fds[0].revents & POLLIN ) { /*为什么要通过管道文件中转?*/ ret = splice( 0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); ret = splice( pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); } } close( sockfd ); return 0; } ``` 服务端:接收客户数据,并把数据发送给每一个登录到该服务器上的客户端(发送者除外) ```ini #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include ​ #define USER_LIMIT 5 #define BUFFER_SIZE 64 #define FD_LIMIT 65535 ​ struct client_data { sockaddr_in address; char* write_buf; char buf[ BUFFER_SIZE ]; }; ​ /*将文件描述符设置为非阻塞*/ 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 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 ); ​ /*监听socket*/ 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 ); ​ client_data* users = new client_data[FD_LIMIT]; pollfd fds[USER_LIMIT+1]; /*记录监听事件集合的大小*/ int user_counter = 0; /*初始化*/ for( int i = 1; i <= USER_LIMIT; ++i ) { fds[i].fd = -1; fds[i].events = 0; } /*把监听socket放到事件表中*/ fds[0].fd = listenfd; /*监听读就绪事件和错误事件*/ fds[0].events = POLLIN | POLLERR; fds[0].revents = 0; ​ while( 1 ) { ret = poll( fds, user_counter+1, -1 ); if ( ret < 0 ) { printf( "poll failure\n" ); break; } for( int i = 0; i < user_counter+1; ++i ) { /*监听socket读就绪*/ if( ( fds[i].fd == listenfd ) && ( fds[i].revents & POLLIN ) ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength ); if ( connfd < 0 ) { printf( "errno is: %d\n", errno ); continue; } /*超出用户数限制*/ if( user_counter >= USER_LIMIT ) { const char* info = "too many users\n"; printf( "%s", info ); send( connfd, info, strlen( info ), 0 ); close( connfd ); continue; } user_counter++; users[connfd].address = client_address; setnonblocking( connfd ); fds[user_counter].fd = connfd; fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR; fds[user_counter].revents = 0; printf( "comes a new user, now have %d users\n", user_counter ); } else if( fds[i].revents & POLLERR ) { printf( "get an error from %d\n", fds[i].fd ); char errors[ 100 ]; memset( errors, '\0', 100 ); socklen_t length = sizeof( errors ); /*获取并清除socket错误状态*/ if( getsockopt( fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length ) < 0 ) { printf( "get socket option failed\n" ); } continue; } /*TCP连接被对方关闭*/ else if( fds[i].revents & POLLRDHUP ) { users[fds[i].fd] = users[fds[user_counter].fd]; close( fds[i].fd ); fds[i] = fds[user_counter]; i--; user_counter--; printf( "a client left\n" ); } /*连接socket读就绪*/ else if( fds[i].revents & POLLIN ) { int connfd = fds[i].fd; memset( users[connfd].buf, '\0', BUFFER_SIZE ); ret = recv( connfd, users[connfd].buf, BUFFER_SIZE-1, 0 ); printf( "get %d bytes of client data %s from %d\n", ret, users[connfd].buf, connfd ); /*返回-1表示出错*/ if( ret < 0 ) { if( errno != EAGAIN ) { close( connfd ); users[fds[i].fd] = users[fds[user_counter].fd]; fds[i] = fds[user_counter]; i--; user_counter--; } } else if( ret == 0 ) { printf( "code should not come to here\n" ); } else { for( int j = 1; j <= user_counter; ++j ) { if( fds[j].fd == connfd ) { continue; } fds[j].events |= ~POLLIN; fds[j].events |= POLLOUT; users[fds[j].fd].write_buf = users[connfd].buf; } } } else if( fds[i].revents & POLLOUT ) { int connfd = fds[i].fd; if( ! users[connfd].write_buf ) { continue; } ret = send( connfd, users[connfd].write_buf, strlen( users[connfd].write_buf ), 0 ); users[connfd].write_buf = NULL; fds[i].events |= ~POLLOUT; fds[i].events |= POLLIN; } } } ​ delete [] users; close( listenfd ); return 0; } ``` ### 9.7 I/O复用的高级应用三:同时处理TCP和UDP服务 ## 10. 信号 \[信号使用\] [blog.csdn.net/m0_49476241...](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fm0_49476241%2Farticle%2Fdetails%2F131681077 "https://blog.csdn.net/m0_49476241/article/details/131681077") ### 10.1 Linux信号概述 #### 10.1.1 发送信号 Linux下,一个进程给其它进程发送信号的API是kill函数。 ```arduino #include #include int kill(pid_t pid, int sig); ``` 该函数把信号sig发送给目标进程pid。pid可能取值如下: | PID参数 | 含义 | |-----------|--------------------------------| | pid \> 0 | 信号发送给PID为pid的进程 | | pid = 0 | 信号发送给本进程组内的其它进程 | | pid = -1 | 信号发送给除init进程外的所有进程,但需要发送者有相应权限 | | pid \< -1 | 信号发送给组ID为-pid的进程组中的所有成员 | * Linux中信号值大于0,如果sig为0,则kill函数不发送任何信号 #### 10.1.2 信号处理方式 目标进程在收到信号时,需要定义一个接收函数来处理。信号处理函数的原型如下: ```arduino #include /*使用typedef定义了一个函数指针类型,它指向一个接收int型参数返回void的函数*/ typedef void(* __sighandler_t) (int); ``` *信号处理函数应该是可重入的,否则容易引发竞态条件。* 除了用户自定义信号处理函数外,还定义了信号的两种其它处理方式。 ```arduino #include #define SIG_DFL ((__sighandler_t) 0) #define SIG_IGN ((__sighandler_t) 1) ``` * SIG_IGN表示忽略目标信号; * SIG_DFL表示使用信号的默认处理方式,默认处理方式有以下几种: * 结束进程(Term) * 忽略信号(Ign) * 结束进程并生成核心转储文件(Core) * 暂停进程(Stop) * 继续进程(Cont) #### 10.1.3 Linux信号 Linux的可用信号都定义在bits/signum.h头文件中,仅仅介绍几个与网络编程相关的: | 信号 | 起源 | 默认行为 | 含义 | |---------|-------|------|------------------| | SIGHUP | POSIX | Term | 控制终端挂起 | | SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或暂停) | #### 10.1.4 中断系统调用 **如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置成EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用**。 ### 10.2 信号函数 #### 10.2.1 signal系统调用 设置信号处理函数,可以使用下面的signal系统调用: ```arduino #include _sighandler_t signal(int sig, _sighandler_t _handler); ``` * sig参数指出捕获的信号类型; * _handler参数是 _sighandler_t类型的函数指针,指定信号sig的处理函数。 signal系统调用出错时返回SIG_ERR,并设置errno。 #### 10.2.2 sigaction系统调用 设置信号处理函数的更健壮的接口是如下的系统调用: ```arduino #include int sigaction(int sig, const struct sigaction* act, struct sigaction* oact); ``` * sig参数指出要捕获的信号类型; * act参数指定新的信号处理方式; * oact参数输出先前的处理方式。 * act和oact都是sigaction结构体类型的指针,定义如下: ```c struct sigaction { #ifdef _USE_POSIX199309 union { _sighandler_t sa_handler; void (*sa_sigaction) (int, siginfo_t*, void*) } _sigaction_handler; /*定义类型和变量*/ #define sa_handler __sigaction_handler.sa_handler #define sa_sigaction __sigaction_handler.sa_sigaction ​ #else _sighandler_t sa_handler; #endif _sigset_t sa_mask; int sa_flags; void (*sa_restorer) (void); } ``` * sa_handler指定信号处理函数; * sa_mask成员设置进程的信号掩码,**以指定哪些信号不能发送给本进程**; * **sa_flags用于设置程序收到信号时的行为**。 sig_action成功时返回0,失败时候返回-1并设置errno。 ### 10.3 信号集 #### 10.3.1 信号集函数 LInux使用数据结构sigset_t来表示一组信号。定义如下: ```arduino #include #define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } __sigset_t; ``` 可见,**sigset_t是一个长整型数组,每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。** Linux提供了如下一组函数来设置、修改、删除和查询信号集: ```arduino #include int sigemptyset(sigset_t* _set) /*清空信号集*/ int sigfillset(sigset_t* _set) /*为信号集添加所有信号*/ int sigaddset(sigset_t* _set, int _signo) /*将信号_signo添加到信号集中*/ int sigdelset(sigset_t* _set, int _signo) /*将信号_signo从信号集中删除*/ int sigismember(_const sigset_t* _set, int _signo) /*测试_signo是否在信号集中*/ ``` #### 10.3.2 进程信号掩码 我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。如下函数也可以用于设置或查看进程的信号掩码: ```arduino #include int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset); ``` * _set参数指定新的信号掩码; * _oset参数则输出原来的信号掩码; * _how参数指定设置进程信号掩码的方式,可选如下表: | _HOW参数 | 含义 | |-------------|------------------------------------| | SIG_BLOCK | 新的进程信号掩码是当前值和_set指定信号集的**并集** | | SIG_UNBLOCK | 新的进程信号掩码是当前值和 **!_set** 信号集的**交集** | | SIG_SETMASK | 直接将进程信号掩码设置为_set | **如果_set为NULL,则进程信号掩码不变,此时仍可以利用 _oset参数获得进程当前的信号掩码**。 #### 10.3.3 被挂起的信号 设置进程信号掩码后,被屏蔽的信号将不能被进程接收。**如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号** 。如果取消对被挂起信号的屏蔽,则它能立即被进程接收到。**如下函数可以获得进程当前被挂起的信号集:** ```arduino #include int sigpending(sigset_t* set); ``` * set参数用于保存被挂起的信号集 ### 10.4 统一事件源 通过把信号的主要处理逻辑放到主循环中,当信号处理函数被触发时,它只是简单的把信号传递给主程序。**信号处理函数通常通过管道来传输信号给主循环。我们可以使用I/O多路复用来监听信号的到来。这就是统一信号源。** ```ini #include #include #include #include #include #include #include #include #include #include #include #include #include #include ​ #define MAX_EVENT_NUMBER 1024 static int pipefd[2]; ​ 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; epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); setnonblocking( fd ); } ​ void sig_handler( int sig ) { int save_errno = errno; int msg = sig; send( pipefd[1], ( char* )&msg, 1, 0 ); errno = save_errno; } ​ ​ /* 为什么sigaction sa是一个局部变量,或者说为什么一个信号拥有一个sigset? */ void addsig( int sig ) { struct sigaction sa; memset( &sa, '\0', sizeof( sa ) ); /*设置信号处理函数和处理方式*/ sa.sa_handler = sig_handler; /*重新调用被该信号终止的系统调用*/ sa.sa_flags |= SA_RESTART; /*设置信号掩码*/ sigfillset( &sa.sa_mask ); assert( sigaction( sig, &sa, NULL ) != -1 ); } ​ 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 ) ); if( ret == -1 ) { printf( "errno is %d\n", errno ); return 1; } ​ ​ ret = listen( listenfd, 5 ); assert( ret != -1 ); ​ /*创建内核事件表的文件描述符*/ epoll_event events[ MAX_EVENT_NUMBER ]; int epollfd = epoll_create( 5 ); assert( epollfd != -1 ); /*为监听socket注册读事件,且为ET模式*/ addfd( epollfd, listenfd ); ​ /* 匿名管道pipe和有名管道mkfifo是单工的,而socketpair是全双工的 */ ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd ); assert( ret != -1 ); /*下面两句,全双工管道的两侧都是非阻塞的*/ setnonblocking( pipefd[1] ); /*注册读事件,非阻塞*/ addfd( epollfd, pipefd[0] ); ​ // add all the interesting signals here addsig( SIGHUP ); addsig( SIGCHLD ); addsig( SIGTERM ); addsig( SIGINT ); bool stop_server = false; ​ while( !stop_server ) { int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 ); if ( ( number < 0 ) && ( errno != EINTR ) ) { printf( "epoll failure\n" ); break; } 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 ); } else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) ) { int sig; char signals[1024]; ret = recv( pipefd[0], signals, sizeof( signals ), 0 ); if( ret == -1 ) { continue; } else if( ret == 0 ) { continue; } else { for( int i = 0; i < ret; ++i ) { //printf( "I caugh the signal %d\n", signals[i] ); switch( signals[i] ) { /*子进程状态发生变化*/ case SIGCHLD: /*控制中断挂起*/ case SIGHUP: { continue; } /*终止进程*/ case SIGTERM: /*键盘输入以中断进程*/ case SIGINT: { stop_server = true; } } } } } else { } } } ​ printf( "close fds\n" ); close( listenfd ); close( pipefd[1] ); close( pipefd[0] ); return 0; } ```

相关推荐
uhakadotcom3 分钟前
Mypy入门:Python静态类型检查工具
后端·面试·github
喵个咪8 分钟前
开箱即用的GO后台管理系统 Kratos Admin - 定时任务
后端·微服务·消息队列
Asthenia041210 分钟前
ArrayList与LinkedList源码分析及面试应对策略
后端
Asthenia041240 分钟前
由浅入深解析Redis事务机制及其业务应用-电商场景解决超卖
后端
Asthenia041241 分钟前
Redis详解:从内存一致性到持久化策略的思维链条
后端
Asthenia041241 分钟前
深入剖析 Redis 持久化:RDB 与 AOF 的全景解析
后端
Apifox1 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
掘金一周1 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
uhakadotcom1 小时前
构建高效自动翻译工作流:技术与实践
后端·面试·github
Asthenia04121 小时前
深入分析Java中的AQS:从应用到原理的思维链条
后端