IO多路复用
IO多路复用:这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:select、poll、epoll。
select函数原型:
入参:
(1)nfds:委托内核检测的这三个集合中最大的文件描述符 + 1
内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
在Window中这个参数是无效的,指定为-1即可
(2)readfds(读集合):文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区
传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
(3)writefds(写集合):内核只检测这个集合中文件描述符对应的写缓冲区
如果不需要使用这个参数可以指定为NULL
(4)exceptfds(异常集合):内核检测集合中文件描述符是否有异常状态
(5)timeout:超时时长,用来强制解除select()函数的阻塞的
NULL:函数检测不到就绪的文件描述符会一直阻塞。
等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回0
不等待:函数不会阻塞,直接将该参数对应的结构体初始化为0即可。
返回值:
大于0:成功,返回集合中已就绪的文件描述符的总个数
等于-1:函数调用失败
等于0:超时,没有检测到就绪的文件描述符
注:select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。
也就是说select检测的文件描述符有限制1024
fd_set中存储了要委托内核检测读缓冲区的文件描述符集合
如果集合中的标志位为0代表不检测这个文件描述符状态
如果集合中的标志位为1代表检测这个文件描述符状态
内核检测读集合中文件描述符对应的读缓冲区若无数据,内核将该文件描述符对应标志位改为0,有数据标志位不变为1。
当select()函数解除阻塞之后,被内核修改过的读集合通过参数传出,此时集合中只要标志位的值为1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。
服务器端基于select实现并发:
(1)创建监听套接字lfd = socket();
(2) 监听套接字lfd与本底IP及端口进行绑定,bind();
(3)监听套接字设置监听 listen();
(4)创建文件描述符集合fd_set,用于检测读事件;
通过 FD_ZERO() 初始化
通过 FD_SET() 将监听的文件描述符放入检测的读集合中
(5)循环调用select(),周期性检测
(6)select解除阻塞返回,得到满足就绪的文件描述符集合
通过FD_ISSET() 判断集合中的标志位是否为 1
如果这个文件描述符是监听的文件描述符,调用 accept() 和客户端建立连接
将得到的新的通信的文件描述符,通过FD_SET() 放入到检测集合中
如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
如果客户端和服务器断开了连接,使用FD_CLR()将这个文件描述符从检测集合中删除
如果没有断开连接,正常通信即可。
(7)
poll函数
入参:(1)fds: struct pollfd类型的数组,存储待检测的文件描述符信息,三个成员
1.fd:检测的文件描述符
2.Events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
3.revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
(2)nfds: 这是第一个参数数组中最后一个有效元素的下标 + 1(也可以指定参数1数组的元素总个数)
(3)timeout:poll函数阻塞时长
-1:一直阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
大于0:阻塞指定的毫秒(ms)数之后,解除阻塞
返回值:失败: 返回-1
成功:返回一个大于0的整数,表示检测的集合中已就绪的文件描述符的总个数
缺点:
1.文件描述符数量有限,32位1024,64位2048;
2.采用轮询的方式,效率低
3.用户态与内核态频繁复制传递fd数据,开销大
poll与select区别:
三者对比:
共同点:一个线程管理和检测IO是否准备就绪。
三者相关API:
Select
Poll
epoll
区别