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...

[关于EPOLL的阻塞和非阻塞以及LT和ET模式] blog.csdn.net/luseysd/art...

Level Triggered (LT) 水平触发

socket接收缓冲区不为空 有数据可读 读事件一直触发

socket发送缓冲区不满 可以继续写入数据 写事件一直触发

符合思维习惯,epoll_wait返回的事件就是socket的状态

Edge Triggered (ET) 边沿触发

socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件

socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件,仅在状态变化时触发事件

ini 复制代码
 #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;
 }
 ​
 /*
  @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;
 }

客户端发送数据:

LT模式下服务端接收数据:

ET模式下服务端接收数据:

9.3.4 EPOLLONESHOT事件

即使使用ET模式,一个socket上的某个事件还是可能被多次触发。在多线程环境下,容易出现同一个socket上的两次上下文数据被不同的线程读取。我们期望一个socket连接在任何一个时刻都只被一个线程处理,就可以使用EPOLLONESHOT事件实现。

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个事件,且只触发一次。所以每次处理完必须使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。

ini 复制代码
 #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;
 }
 ​
 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 <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 <string.h>
 #include <stdlib.h>
 #include <poll.h>
 #include <fcntl.h>
 ​
 #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 <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 <poll.h>
 ​
 #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...

10.1 Linux信号概述

10.1.1 发送信号

Linux下,一个进程给其它进程发送信号的API是kill函数。

arduino 复制代码
 #include <sys/types.h>
 #include <signal.h>
 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 <signal.h>
 /*使用typedef定义了一个函数指针类型,它指向一个接收int型参数返回void的函数*/
 typedef void(* __sighandler_t) (int);

信号处理函数应该是可重入的,否则容易引发竞态条件。

除了用户自定义信号处理函数外,还定义了信号的两种其它处理方式。

arduino 复制代码
 #include <bits/signum.h>
 #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 <signal.h>
 _sighandler_t signal(int sig, _sighandler_t _handler);
  • sig参数指出捕获的信号类型;
  • _handler参数是 _sighandler_t类型的函数指针,指定信号sig的处理函数。

signal系统调用出错时返回SIG_ERR,并设置errno。

10.2.2 sigaction系统调用

设置信号处理函数的更健壮的接口是如下的系统调用:

arduino 复制代码
 #include <signal.h>
 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 <bits/sigset.h>
 #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 <signal.h>
 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 <signal.h>
 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 <signal.h>
 int sigpending(sigset_t* set);
  • set参数用于保存被挂起的信号集

10.4 统一事件源

通过把信号的主要处理逻辑放到主循环中,当信号处理函数被触发时,它只是简单的把信号传递给主程序。信号处理函数通常通过管道来传输信号给主循环。我们可以使用I/O多路复用来监听信号的到来。这就是统一信号源。

ini 复制代码
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <assert.h>
 #include <stdio.h>
 #include <signal.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
 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;
 }
相关推荐
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠3 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries4 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_4 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平5 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码6 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞7 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb