实现IO多路复用
通过IO多路复用技术, 不用通过多线程就可以实现, 多个客户端与服务器建立连接
多线程/多进程实现并发与IO多路复用实现并发的区别:
主要区别就是在3个函数的执行方面
accept()
read()
write()
多线程实现并发的过程, 当调用这个函数时有可能会发生阻塞, 比如当调用accept时, 如果没有新的客户端连接请求, 那么accept就会阻塞, 调用read时, 如果对应文件描述符的读缓冲区没有数据, 也会阻塞, 调用write时, 如果对应的文件描述符的写缓冲区已满, 还是会阻塞
而IO多路转接, 是将文件描述符的读写缓冲区委托给内核来进行检测, 如果内核检测所有的文件描述符都处于未就绪状态, 那么就会导致内核阻塞, 直到检测到有文件描述符处于就绪状态, 那么阻塞就会解除, 并将就绪态的文件描述符信息传出
后续就可以根据内核传出来的文件描述符进行判断
再调用accept(), read(), write() 就不会发生阻塞, 因为内核已经帮忙判断过, 这些对应的文件描述符都是就绪的
与多进程/多线程技术相比, IO多路复用的优势就是不需要频繁的创建和销毁线程, 从而减少系统的开销
select
首先是select
#include<sys/select.h> struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
文件描述符的数量:
nfds: 下面集合中最大的文件描述符+1(因为文件描述符是从0开始的, 可以理解为内核所要检测的文件描述符的数量)
通过nfds给内核设置一个检测范围(到nfds就结束检测), 在select中内核是通过线性遍历挨个来进行检测每个文件描述符的标志位
三个集合:(传入传出参数) 本质上都是数组
readfds:
文件描述符的集合, 内核只检测这个集合中文件描述符的读缓冲区
writefds:
内核只检测这个集合中文件描述符的写缓冲区
exceptfds:
内核只检测这个集合中的文件描述符是否发生异常
超时机制
timeout: 设置超时的时长, 可以用来解除阻塞
NULL: 没有文件描述符处于就绪状态, select函数就处于阻塞
通过设置timeval结构体中的参数, 来设置阻塞的时长
返回值:
>0
成功, 返回已就绪的文件描述符的个数
==-1
函数调用失败
==0
函数超时,也没有检测到就绪的文件描述符
初始化fd_set 的函数
// 将fd从set中删掉, 其实就是将set中fd对应的标志位置为0 void FD_CLR(int fd,fd_set* set); // 判断fd是否在set中, 其实就是判断set中的fd的标志位是否为1 int FD_ISSET(int fd,fd_set* set); // 将fd加入到set中, 其实就是将set中fd对应的标志位置为1 void FD_SET(int fd,fd_set* set); // 情况set中的fd, 其实就是将set中的所有标志位置为0 void FD_ZERO(fd_set* set)
进一步理解 fd_set
fd_set 本质上是一个位图, 有1024位(正好对应1024个文件描述符,select所能检测的最大文件描述符的个数就是1024个),每一位只有2种状态(0和1), 就是通过0和1来判断每个位置的文件描述符是否需要内核进行检测
sizeof(fd_set)=128Byte=128*8=1024bit; // int[32]
select函数在内核中的执行过程:
以读集合为例
首先, 将要检测的fd_set(读集合,也可以理解为一张文件描述符的表)拷贝到内核中, 由内核来遍历fd_set中的每一位, 如果fd_set中对应文件描述符的标志位为0, 则说明该处的文件描述符不需要检测, 内核就会跳过这个文件描述符, 如果fd_set中的文件描述符的标志位为1, 则说明该处的文件描述符需要进行检测, 内核就会检测这个文件描述符的读缓冲区, 如果读缓冲区有数据, 则该处的状态不变,标志位还为1, 如果读缓冲区没有数据, 那么该处的标志位就会被内核置为0
最后内核遍历到设置的最大文件描述符+1的位置处停止
然后内核将修改后的文件描述符表再拷贝给内存中的fd_set, 这样fd_set中标志位为1的文件描述符 就是处于就绪状态的文件描述符, 这样就可以通过遍历内核传出来的fd_set集合,来找到就绪的文件描述符, 基于这个文件描述符来进行后续的建立连接或通信的操作,就不会再发生阻塞。
select 并发流程:
-
创建监听的套接字
-
绑定ip和端口
-
设置监听
-
创建FD_SET 读集合
-
通过FD_ZERO , 将FD_SET 的标志位初始化为0
-
将监听的套接字通过加入到FD_SET, 也就是将监听的套接字标志位置为1
-
循环调用select, 将FD_SET集合的拷贝放入到select函数中, 而不是将FD_SET本身放入到select函数中,因为内核会改变传入的FD_SET的值, 内核检测到文件描述符的读缓冲区没有数据,就会将其标志位置为0
-
如果检测的文件描述符有处于就绪状态, 那么select就会解除阻塞,并返回处于就绪状态的文件描述符的个数
-
遍历maxfd之前的所有文件描述符,通过FD_ISSET函数来判断文件描述符是否处于就绪态(就是看文件描述符对应的标志位是否为1,就是读缓冲区是否有数据)
-
如果有数据, 进一步判断该文件描述符是属于监听的文件描述符还是通信的文件描述符
-
如果是监听的文件描述符, 就调用accept函数, 创建通信的文件描述符, 并将新创建的通信的文件描述符加入到FD_SET集合中
-
如果是通信的文件描述符,就进行通信, 如果客户端和服务器断开了连接, 就调用FD_CLR函数将对应的文件描述符从FD_SET集合中移除(其实就是将对应的文件描述符的标志位置为0)
select 局限性:
1.select 需要将待检测的集合从用户去拷贝到内核, 来让内核进行检测, 当内核检测完毕后, 还要将修改后的待检测集合再拷贝到用户去
这样在用户区和内核去频繁的拷贝, 效率太低
2.内核检测传入的读写集合, 是线性检测的, 如果待检测的文件描述符太多, 检测的效率就会比较低 3.select检测的文件描述符是有上限的, 默认是1024个, 在内核中是写死的