第六章 IO复用:select和poll函数

1. 概述

I/O复用使用场合:

    • 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须要使用I/O复用;
    • 一个客户同时处理多个套接字(比较少见); 
    • 如果一个TCP服务器既要处理监听套接字,又要处理已连接的套接字,一般要使用I/O复用;
    • 如果一个服务器要处理多个服务或者多个协议,一般要使用I/O复用

另外I/O复用并非只限于网络编程,还有许多重要程序也会用到这项技术。

2. I/O模型

  • 阻塞式I/O模型
  • 非阻塞式I/O模型
  • I/O复用模型
  • 信号驱动I/O模型
  • 异步I/O模型

3. select函数

1)函数作用:允许进程指示内核等待多个事件中的一个发生, 并只在有一个或多个事件发生,或通过定时唤醒它。(前面说的同时处理socket描述符和等待用户输入就符合这个情况)(Berkeley的实现允许任何描述符的I/O复用)

2)函数定义

#include <sys/time.h>
#include <sys/select.h>
int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set exceptset, const struct timeval *timeout);

struct timeval
{
long tv_sec;
long tv_usec;
}

参数介绍:

timeout:告知内核等待所指定的描述符中任何一个就绪的时间。其有三种可能:

    • 永远等下去,此时值设置为NULL;
    • 等一段固定的时间。即等待时间不超过所指定的timeout时间值;
    • 根本不等待。此时称为轮询,timeout结构总的秒、微妙都设置为0;

exceptset:目前支持的异常条件只有两个

    • 某个套接字的带外数据到达(24章讨论);
    • 某个已置为分组模式的伪终端,存在可以从其主终端读取的控制状态信息(本书不涉及)

readset,writeset:我们要让内核读和写的描述符;

maxfdp1: 指定待测描述符的个数,具体值从0开始到maxfdp1-1(FD_SETSIZE常值为fd_set描述符的总数)。

返回值:若有就绪描述符则为其个数,超时返回0,出错为-1.

select可以作为定时器,此时中间三个描述符集设置为NULL,这个定时器比sleep还有精确,sleep以秒为单位,其以微妙为单位。

 

3)fd_set类型的相关操作

fd_set rset; //注意新定义变量一定要用FD_ZERO初始化,其自动分配的值不可意料,会导致不可意料的后果。
void FD_ZERO(fd_set *fdset); //initialize the set: all bits off
void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set *fdset); //turn off the bits for fd in fdset
int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset

4) 描述符的就绪条件

    • 有数据可读(读);
    • 关闭连接的读一半(TCP收到FIN)(读);
    • 给监听套接口准备好新连接(读);
    • 有待处理的错误(读写)
    • 有可用的写空间(写);
    • 关闭连接写一半(写);
    • tcp带外数据(异常)

5)select的最大描述符数:不同系统可能不同,使用大描述符集时,要考虑移植性。

6)例子:第五章中说到的同时处理fputs、read的情况

View Code
void str_cli_1(FILE *fp, int sockfd)
{
    char sendline[MAXLINE];
    char readline[MAXLINE];
    
    fd_set rset;
    FD_ZERO(&rset);
    
    while(1)
    {
        FD_SET(fileno(fp), &rset);
        FD_SET(sockfd, &rset);
        int maxfdp1 = MAX(fileno(fp), sockfd) + 1;
        
        if( select(maxfdp1, &rset, NULL, NULL, NULL) <=0 )
        {
            printf("%s:%d select error\n", __FUNCTION__, __LINE__);
            return;
        }
        
        if( FD_ISSET(sockfd, &rset) )
        {
            if( read(sockfd, readline, MAXLINE) == 0)
            {
                printf("%s:%d recv data error\n", __FUNCTION__, __LINE__);
                return;
            }
            fputs(readline,stdout);
        }
        
        if( FD_ISSET(fileno(fp), &rset) )
        {
            if( NULL == fgets(sendline, MAXLINE, fp) )
            {
                printf("%s:%d fgets error\n", __FUNCTION__, __LINE__);
                return;
            }
            if( write(sockfd, sendline, strlen(sendline)) != strlen(sendline) )
            {
                printf("%s:%d send data error\n", __FUNCTION__, __LINE__);
                return ;
            }
        }
    }
}

例子中有了select当服务器进程退出后,客户端就不会阻塞于fgets函数,客户端收到FIN后read会马上返回错误。(此例子还有问题)

4. 批量输入

    如果客户端持续向服务器发送数据,发送完数据后客户端马上调用close关闭连接。此时客户端可能仍有请求去往服务器的路上,或是仍有服务器的应答在返回客户段的路上。这样如果调用close全关闭就会造成问题。我们需要一个半关闭TCP的一种方法。即下面将说到的shutdown。

    混合使用select和stdio比较容易发生错误,要非常小心。

5. shutdown函数

1)close函数的限制

    • close把描述符的引用计数减一,要到计数为0时才关闭套接字。shutdown可以不管引用计数就激发TCP的正常连接终止。
    • close终止读和写两个方向的数据传送。但是有时我们需要告知对方我们已经把数据发送完毕,即使我们还有数据需要接收。

2)shutdown函数定义

#include <sys/socket.h>
//返回值:成功0,出错-1
int shutdown(int sockfd, int howto);

howto参数值说明:

    • SHUT_RD:关闭连接的读这一半,套接字没有数据可以接收了,而且套接字接收缓冲区中现有的数据将被丢弃;
    • SHUT_WR:关闭连接的写这一半,对于TCP这叫半关闭,当前留在套接字发送缓冲区的数据将被发送完,紧跟TCP正常的连接终止系列。
    • SHUT_RW:关闭整个连接。

6. 修正3中最后面的str_cli_1例子

View Code
void str_cli_2(FILE *fp, int sockfd)
{
    char sendline[MAXLINE];
    char readline[MAXLINE];
    
    fd_set rset;
    FD_ZERO(&rset);
    
    int stdineof = 0;
    while(1)
    {
        if(stdineof == 0)
        {
            FD_SET(fileno(fp), &rset);
        }
        FD_SET(sockfd, &rset);
        int maxfdp1 = MAX(fileno(fp), sockfd) + 1;
        
        if( select(maxfdp1, &rset, NULL, NULL, NULL) <=0 )
        {
            printf("%s:%d select error\n", __FUNCTION__, __LINE__);
            return;
        }
        
        if( FD_ISSET(sockfd, &rset) )
        {
            int rn = 0;
            if( (rn = read(sockfd, readline, MAXLINE)) == 0)
            {
                if(stdineof == 1)
                {
                    return;
                }
                printf("%s:%d recv data error\n", __FUNCTION__, __LINE__);
                return;
            }
            write(fileno(stdout), readline, rn);
        }
        
        if( FD_ISSET(fileno(fp), &rset) )
        {
            int rn = 0;
            if( (rn = read(fileno(fp), sendline, MAXLINE)) == 0 )
            {
                stdineof = 1;
                shutdown(sockfd, SHUT_WR); //send fin
                FD_CLR(fileno(fp), &rset);
                printf("%s:%d read fileno(fp) error\n", __FUNCTION__, __LINE__);
                continue;
            }
            if( write(sockfd, sendline, rn) != rn )
            {
                printf("%s:%d send data error\n", __FUNCTION__, __LINE__);
                return ;
            }
        }
    }
}

7. 使用select的TCP服务器程序

View Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SRV_PORT 8888
#define MAXLINE 4096

void str_echo(int fd);
void sig_chld(int signo);

int main(int argc, char **argv)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        perror("create socket error.");
    }

    struct sockaddr_in srvaddr;
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    srvaddr.sin_port = htons(SRV_PORT);
    
    if(bind(listenfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)) < 0)
    {
        perror("bind error.");
    }
    
    if(listen(listenfd, 1023) < 0)
    {
        perror("listen error.");
    }

    int maxfd = listenfd;
    int maxi = -1;
    
    int client[FD_SETSIZE];
    int i=0;
    for(i=0; i<FD_SETSIZE; i++)
    {
        client[i] = -1;
    }
    
    fd_set rset, allset;
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);
    
    struct sockaddr_in cliaddr;
    
    for(; ;)
    {
        rset = allset;
        int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if(FD_ISSET(listenfd, &rset))
        {
            socklen_t clilen = sizeof(cliaddr);
            int connfd;
            if((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0)
            {
                if(errno == EINTR)
                {
                    continue;
                }
                else
                {
                    perror("accept error.");
                }
            }
            for(i=0; i < FD_SETSIZE; i++)
            {
                if(client[i] < 0)
                {
                    client[i] = connfd;
                    break;
                }
            }
            if(i == FD_SETSIZE)
            {
                printf("too many connect.");
                exit(-1);
            }
            
            FD_SET(connfd, &allset);
            
            maxfd = maxfd > connfd ? maxfd : connfd;
            
            maxi = i > maxi ? i : maxi;
            
            if(--nready <= 0)
            {
                continue;
            }
        }
        
        //get client data
        for(i=0; i <= maxi; ++i)
        {
            if(client[i] < 0)
            {
                continue;
            }

            if(FD_ISSET(client[i], &rset))
            {
                int byteN;
                char buf[MAXLINE];
                if( (byteN = read(client[i], buf, MAXLINE)) == 0)
                {
                    close(client[i]);
                    FD_CLR(client[i], &allset);
                    client[i] = -1;
                }
                else
                {
                    write(client[i], buf, byteN);
                    if(--nready <= 0)
                    {
                        break;
                    }
                }
            }
        }
    }

    return 0;
}

8. pselect函数

#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, 
            const struct timespec *timeout, const sigset_t *sigmask);
此函数比select函数多了timespec和sigmask-不细说了

9. poll函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events */  //入参
    short revents; /* returned events */  //出参
};

说明:与select提供的功能累世,处理流设备时能提供额外的信息。不限定最大描述符数量。

原文地址:https://www.cnblogs.com/4tian/p/2624353.html