代码改变世界

第六章 I/O复用:select 和 poll 函数

2017-11-13 22:07  szn好色仙人  阅读(220)  评论(0编辑  收藏  举报
//1.
阻塞式I/O型


非阻塞式I/O型


I/O复用模型



//2.
select 函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或者多个发生或者经历一段指定时间后才唤醒他
也就是说,我们调用 select 告知内核对哪些描述符(读、写、异常)感兴趣以及等待多长时间。我们感兴趣的描述符不局限于套接字,包含任意描述符
(备注:windows下的select只支持对套接字进行操作)

select 最后一个参数:
(1):永远等待,直到有一个描述符准备好I/O时才返回,此时此参数传 nullptr
(2):等待一段固定时间,在有一个描述符准备好I/O时返回,但是不超过由该参数指定的秒数以及微秒数
(3):根本不等待,此时此参数秒与微秒都应置为0

尽管 select 最后一个参数指定了微秒级的分辨率,但是内核支持的真是分辨率往往粗糙的多,许多UNIX内核把此时间向上舍入成10ms的倍数,另外还存在调度延时
timeval 结构体能表达 select 不支持的值(当 timeval 结构中指定的时间超过1亿秒,有些系统是不支持这种情况下的 select 的)
struct timeval {
    long    tv_sec;         /* seconds */
    long    tv_usec;        /* and microseconds */
};


//3.
select 使用描述符集,相关的数据类型为 fd_set
typedef struct fd_set {
    u_int fd_count;               /* how many are SET? */
    SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

#define FD_SETSIZE      64

#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0)

#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0)

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)

#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))


//4.
select 第一个参数为最大描述符+1,注意此处不是描述符数量+1,而是最大描述符+1。windows下忽略此参数,传0即可
select 函数返回后,我们使用 FD_ISSET 来测试 fd_set 数据类型中的描述符,描述符集内的所有未就绪描述符对应位返回时均为0,
为此每次调用 select 函数时,我们都得再次把所有描述符集内所关心的位置为1
select 返回值表示所有描述符集的已就绪的总位数。如果在任何描述符就绪前定时器到时,那么返回0,返回-1表示出错


//5.
一个套接字准备好读的条件:
(1):该套接字接收缓冲区中的数据字节大于等于套接字接收缓冲区低水平标记的当前大小。对这样的套接字执行读操作不会阻塞并且返回值大于0
    可以使用 SO_RCVLOWAT 套接字选项设置该套接字的低水平标记位。对于TCP和UDP,此值默认为1
(2):该连接的读半部关闭(接收了FIN的TCP连接)。对这样的套接字读操作将不会阻塞并返回0(也就是返回EOF)
(3):该套接字是一个监听套接字并且已完成连接数不为0,对这样的套接字调用 accept 通常不会阻塞
(4):该套接字有错误待处理。对这样的套接字进行读不会阻塞将返回-1。这些待处理错误可指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除

一个套接字准备好写的条件:
(1):该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水平位标记的当前大小,并且或者该套接字已连接,或者该套接字不需连接(UDP套接字)。
    这意味着如果我们把这样的套接字设为非阻塞,写操作将不会阻塞并返回一个正值。我们使用 SO_SNDLOWAT 套接字选项来设置该套接字的低水平位标记。
    对于TCP和UDP而言,其默认值通常为2048
(2):该连接的写半部关闭
(3):使用非阻塞的 connect 的套接字已建立连接,或者 connect 已经以失败告终(此为书中原话,估计是针对UNIX系统下的,在windows下,前半句成立,后半句不成立)
(4):该套接字有一个错误待处理。对这样的套接字的写不会阻塞并且返回-1。这些待处理错误可指定 SO_ERROR 套接字选项调用 getsockopt 获取并清除

一个套接字异常的条件:
(1):一个套接字存在带外数据或仍处于带外标记
(2):某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息
备注:在windows下,当非阻塞的情况下 connect 失败,也会导致 select 异常条件就绪,然而非阻塞下的 connect 失败不会使得套接字可写就绪
注意:当某个套接字上发生错误,他将被
select 标记为既可读也可写 select 返回某个套接字就绪的条件小结:


//5.
shutdown 可以激发TCP的正常连接终止序列

shutdown 关闭一半TCP连接:


#define SD_RECEIVE      0x00
#define SD_SEND         0x01
#define SD_BOTH         0x02
SD_RECEIVE:关闭连接的读的一半,套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据被丢弃。进程不能再对这样的套接字调用任何读取函数。
           对一个TCP套接字这样调用 shutdown 函数后,由该套接字接受的来自对端的任何数据都被确认,但是悄然丢弃
SD_SEND:关闭写的这一半,对于TCP套接字,这称为半关闭。当前留在套接字发送缓冲区的数据将被发送掉,后跟TCP的正常终止序列。
        进程不能再对这样的套接字调用任何写函数
SD_BOTH:连接的读、写半部均被关闭,等效先指定 SD_RECEIVE 指定 SD_SEND


select MSDN参考文档
https://msdn.microsoft.com/query/dev10.query?appId=Dev10IDEF1&l=ZH-CN&k=k(%22WINSOCK2%2fSELECT%22);k(SELECT)&rd=true