Linux socket通信--select
1 为什么要有select
将socket fd设置为nonblock(非阻塞)是在服务器编程中常见的做法,采用blocking IO并为每一个client创建一个线程的模式开销巨大且可扩展性不佳(带来大量的切换开销),更为通用的做法是采用线程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。这也就是为什么存在io复用的原因
2 什么是select
系统提供select函数来实现多路复用输入/输出模型.select系统调用是用来让我们程序监视多个文件描述符的状态变化;程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
2.1 select 函数
-
接口形式
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
头文件:
#include <sys/select.h>
-
参数说明
- 参数nfds是需要监视的最大的文件描述符值+1;
- fd_set:描述符集合 这个结构体中有一个数组,作用是用于向数组中添加描述符,将描述符添加到集合中,实际上是将描述符这个数字对应的比特位置1;而这个位图中能够添加多少描述符取决于一个宏:_FD_SETSIZE=1024,因此select模型所能够监控的描述符是有最大数量限制的;readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合;为什么是1024,原因是fd_set 数组定义如下:
__fds_bits 是一个long int数组,使用bit位记录fd的事件,0:表示无事件,1:表示有事件,数组长度为16。因此一共可以表示 8 * 8 * 16 = 1024个fd的状态。这是为什么select函数支持的最大fd数量。typedef struct { long int __fds_bits[16]; //可以看作是128bit 的数组 } fd_set;
- timeout: 结构timeval,用来设置select()的等待时间
- 返回值
- ( >0 ) 表示当前集合中多少描述符就绪了
- ( ==0 ) 表示等待超时了(在阻塞的时间段内,一直没有就绪的描述符)
- ( <0 ) 表示监控出错了(select本次监控出错)
2.2 fd_set类型变量相关宏定义
- FD_ZERO(fd_set *fdset):清空fdset与所有文件描述符的联系。
- FD_SET(int fd, fd_set *fdset):建立文件描述符fd与fdset的联系。
- FD_CLR(int fd, fd_set *fdset):清除文件描述符fd与fdset的联系。
- FD_ISSET(int fd, fd_set *fdset):检查fd_set联系的文件描述符fd是否可读写,>0表示可读写。
2.3 select 原理
- 用户进程向内核发起select函数的调用,并携带socket描述符集合从用户空间复制到内核空间,由内核对socket集合进行可读状态的监控。
- 其次当前内核没有数据可达的时候,将注册的socket集合分别以entry节点的方式添加到链表结构的等待队列中等待数据报可达。
- 这个时候网卡设备接收到网络发起的数据请求数据,内核接收到数据报,就会通过轮询唤醒的方式(内核并不知道是哪个socket可读)逐个进行唤醒通知,直到当前socket描述符有可读状态的时候就退出轮询然后从等待队列移除对应的socket节点entry,并且这个时候内核将会更新fd集合中的描述符的状态,以便于用户进程知道是哪些socket是具备可读性从而方便后续进行数据读取操作。
- 同时在轮询唤醒的过程中,如果有对应的socket描述符是可读的,那么此时会将read_process加入到cpu就绪队列中,让cpu能够调度执行read_process任务。
- 最后是用户进程调用select函数返回成功,此时用户进程会在socket描述符结合中进行轮询遍历具备可读的socket,此时也就意味着数据此时在内核已经准备就绪,用户进程可以向内核发起数据读取操作,也就是执行上述的read_process任务操作。
2.4 select 事件
select函数用于检测在一组socket中是否有事件就绪,主要分为以下三类:
- 读事件就绪
- 在socket 内核中,接收缓冲区中的字节数大于或者等于低水位标记 SO_RCVLOWAT,此时调用recv或者read函数可以无阻塞地读该文件描述符,并且返回值大于0。
- TCP 连接的对端关闭连接,此时本端调用recv或read函数对socket进行读操作,recv或read函数会返回0值。
- 在监听socket上有新的连接请求。
- 在socket上有未处理的错误。
- 写事件就绪
- 在socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小)大于或者等于低水位标记SO_SNDLOWAT时,可以无阻塞地写,并且返回值大于0。
- socket 的写操作被关闭(调用了close或shutdown函数)时,对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
- socket使用非阻塞connect连接成功或失败时。
- 异常事件就绪