《UNIX网络编程》笔记 - select和poll

1,823 阅读6分钟

简介

IO复用:让进程等待一系列IO条件而不是一个IO条件

通过selectpoll函数我们可以同时监听多个描述符,在描述符就绪时进行对应的操作。

select

定义:

//maxfdpl: 待测试的描述符个数
//返回就绪描述符的个数,若超时则为0, 若出错则为-1
int select(int maxfdpl, 
    fd_set *readset, 
    fd_set *writeset, 
    fd_set *exceptset, 
    struct timeval *timeout);
 
//超时选项    
//NULL:wait forever;0:don't wait
struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* and microseconds */
};

//每个fds_bit的每一位对应一个描述符
typedef struct fd_set {
	int     fds_bits[FD_SETSIZE/sizeof(int)/NBBY]; /* NBBY=bits in a byte ; usually 8*/
} fd_set;

#define FD_SETSIZE  1024        /* fd_set中描述符的总数 */
void FD_ZERO(fd_set *fdset);            /* clear all bits in fdset */
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 bit for fd in fdset */
void FD_ISSET(int fd, fd_set *fdset);   /* is the bit for fd on in fdset ? */

select的使用方法:

    int fds[FD_SETSIZE]; 保存当前所有描述符
    fd_set rset, wset, eset; //定义读、写、异常对应的fd_set
    //初始化fd_set,非常重要且不能省略,因为如果不初始化可能会影响FD_ISSET的调用结果
    FD_ZERO(&rset); 
    FD_ZERO(&wset);
    FD_ZERO(&eset);
    for (;;) {
        //在循环中调用select
        select(FD_SETSIZE, &rset, &wset, &eset, NULL);
        //遍历当前所有的fd,处理就绪的fd
        for (int i = 0; i < FD_SETSIZE; i++)
        {
            if (FD_ISSET(fds[i], &rset))
            {
                //handle read
            }
            if (FD_ISSET(fds[i], &wset))
            {
                //handle write
            }
            if (FD_ISSET(fds[i], &eset))
            {
                //handle exception
            }
        }
    }

fd_set的限制

在很早之前就看到网上的介绍说select在描述符个数上是有限制的,现在终于知道这个限制是从哪来的了,这实际上跟fd_set的实现机制有关。

fd_set中使用int数组中的各个位来保存多个描述符的状态,这个数组称为描述符集,比如数组的第一个数有32位,那么第一个数的每一位就表示第0~31个描述符的状态,这样一来当我们调用FD_ISSET来判断某一个描述符状态时,我们只需要找到其对应的位判断其是0或者1就行了;同理当我们需要设置某个描述符状态时,只需要设置对应的位的状态即可。而fd_set中数组的大小是通过FD_SETSIZE这个值算出来的,FD_SETSIZE是一个宏定义,通常它的默认值比较小,在我的mac上查看其默认值是1024,也就是说在我的mac上select能够支持的最大的描述符数量是1024个。当然FD_SETSIZE也可以重新定义,但如果要调整需要重新编译内核。

描述符读就绪条件

  1. 接收缓冲区数据字节数大于低水位(默认是1),这时读取操作返回大于0
  2. 读半关闭,也就是对端发来了FIN,这时返回0,也就是EOF
  3. 当前套接字是监听套接字,而且已完成连接数不为0,这时可以进行accept操作
  4. 描述符上有套接字错误需要处理

描述符写就绪条件

  1. 发送缓冲区数据字节数大于低水位(通常为2048);
  2. 套接字已连接或不需要连接(UDP)
  3. 写半关闭,这时如果再写会收到SIGPIPE信号
  4. 使用非阻塞式connect的套接字已建立连接或者connect失败
  5. 描述符上有套接字错误需要处理

shutdown&close

有两个函数可以关闭套接字:shutdownclose,它们的区别如下:

  1. close会将引用计数减1,当计数为0时关闭套接字;shutdown可以直接触发关闭。
  2. close会终止读和写两个方向;shutdown可以通过参数howto指定关闭某个方向
int close(int fd);
int shutdown(int fd, int howto);

/*
 * howto arguments for shutdown(2), specified by Posix.1g.
 */
#define SHUT_RD         0               /* shut down the reading side */
#define SHUT_WR         1               /* shut down the writing side */
#define SHUT_RDWR       2               /* shut down both sides */

poll

pollselect的功能类似,也支持IO复用,但是poll没有使用描述符集,而是使用pollfd这种结构来表示描述符的状态。

//nfds:array的长度,受进程能打开的最大文件数限制
//返回就绪描述符的个数,若超时则为0, 若出错则为-1
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

struct pollfd {
	int     fd;         /* descriptor to check */
	short   events;     /* events of intrests on fd */
	short   revents;    /* events that occurred on fd */
};

poll的使用方法:

    struct pollfd pollfds[OPEN_MAX]; //定义pollfd数组,将需要监听的描述符保存起来
    for (;;)
    {
        //在循环中调用poll
        poll(pollfds, OPEN_MAX, INFTIM);
        for (int i = 0; i < OPEN_MAX; i++)
        {
            //遍历pollfd数组处理就绪的描述符
            struct pollfd pfd = pollfds[i];
            if (pfd.revents & POLLIN) {
                //handle read
            }
            if (pfd.revents & POLLOUT) {
                //handle write
            }
        }
    }

poll识别的数据类型:普通(normal)、优先级带(priority band)、高优先级(high priority); 这些术语来自基于流的实现。(没太明白,先标记下)

events常量列举:

常量 出现在events 出现在revents 说明
POLLIN y y 普通或优先级带数据可读
POLLRDNORM y y 普通数据可读
POLLRDBAND y y 优先级带数据可读
POLLPRI y y 高优先级带数据可读
POLLOUT y y 普通数据可写
POLLWRNORM y y 普通数据可写
POLLWRBAND y y 优先级带数据可写
POLLERR n y 发生错误
POLLHUP n y 发生挂起
POLLNVAL n y 描述符不是一个打开的文件

poll的就绪条件:

  • 所有正规TCP数据和所有UDP数据视为普通数据
  • TCP带外数据视为优先级带数据
  • 当TCP读半关闭时(收到对端传来的FIN),也视为普通数据,随后的读操作将返回0
  • TCP连接存在错误既可以视为普通数据,也可以视为错误(POLLERR)。随后的读操作将返回-1,并设置全局的errno变量。
  • 监听套接字上有新的连接既可以视为普通数据,也可以视为优先级数据。
  • 非阻塞式的connect的完成视为使对应的套接字可写。

总结

selectpoll都支持IO复用,其思路都是调用函数监听多个描述符,当有描述符就绪或者超时的时候函数调用就会返回,对应的描述符集合状态也会改变,这时候再遍历描述符集合,处理其中就绪的部分即可。

这种方式在需要监听的描述符比较小,或者是每次就绪的描述符很多的情况下比较有效;但当描述符很多而且每次只有少数描述符就绪时,效率就比较低了。后面出现的epoll就避免了这种线性遍历的问题。

另外select还受FD_SETSIZE的限制,只能处理较少的描述符,而poll则没有这个限制。poll监听的集合大小只受进程能打开的文件数量(RLIMIT_NOFILE)的限制。