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; } ```

相关推荐
鬼火儿4 小时前
SpringBoot】Spring Boot 项目的打包配置
java·后端
cr7xin4 小时前
缓存三大问题及解决方案
redis·后端·缓存
间彧5 小时前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧5 小时前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧5 小时前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧5 小时前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧5 小时前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧5 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端
间彧5 小时前
如何在实际项目中集成Arthas Tunnel Server实现Kubernetes集群的远程诊断?
后端
brzhang6 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构