什么是IO多路复用?
- IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程。
为什么有IO多路复用机制?
没有IO多路复用时,有其它几种方式,但是都存在一些问题(无法处理高并发,浪费CPU资源等)
同步阻塞IO
- 资源不可用的情况下,IO请求一直被阻塞,直到资源可用。
- 如:钓鱼的时候,鱼钩抛入到水中,眼睛一直盯着鱼漂,直到钓上鱼。
同步阻塞IO的特点: - 阻塞IO的方式,等待的时长取决于内核
- 在等待的过程当中执行流是被挂起的,对CPU的利用率很低
- 在IO就绪到拷贝之间,实时性比较高
- 代码编写的流程比较简单
同步非阻塞IO
- 在资源不可用的情况下,IO请求不会被阻塞,而是直接返回,返回当前资源不可用。(如果返回资源不可用,也就是意味着IO请求并没有真正完成)
- 在非阻塞的情况下,需要判断返回值,来判断IO请求是否被真正完成,需要搭配循环一直判断,直到IO请求完成。
- 如:钓鱼的时候,鱼钩抛入水中,看一眼,鱼漂没有动,看一会手机,再看一眼鱼漂,再看一会手机,循环往复,直到鱼漂动钓上鱼。
同步非阻塞IO的特点:
- 非阻塞IO对CPU的利用率比阻塞IO高
- 代码复杂,流程控制复杂
- 循环调用,直到IO请求完成
- IO准备就绪到拷贝数据之间不够实时性
信号驱动IO
- 自定义一个IO信号的处理函数,处理其中发起的IO调用。
程序收到IO信号–>内核调用IO信号的自定义处理函数–>即在自定义处理函数中发起了IO调用 - 如:在鱼竿上绑上了一个铃铛,当鱼儿咬钩的时候,铃铛一响,就将鱼儿钓上来。如果铃铛没有响,则渔夫就可以做其它事情了。
信号驱动IO特点:
- IO准备就绪到拷贝数据之间的实时性加强了
- 代码更加复杂,流程控制更难
- 不需要重复发起IO调用,但是需要在代码中增加自定义信号的逻辑
异步IO
原理:
- 自定义信号的处理函数—>作用是通知数据拷贝完成的
- 发起一个异步调用,并且异步调用直接返回
- 对于异步调用返回之后,执行流可以执行用户代码,由操作系统内核帮助我们等待IO就绪和拷贝数据
- 当拷贝数据都完成了,内核通过信号来通知调用者
异步IO特点:
- 异步IO模型当中,数据拷贝过程也被内核完成了
- 自定义处理函数,导致代码复杂,流程复杂
- 同阻塞IO一样,实时性没有受到影响
总结
阻塞和非阻塞
只需要判断资源不可用的时候,发起IO调用,当前IO调用是否返回
- IO调用返回—>则认为是非阻塞
- IO调用没有返回—>则认为是阻塞
同步和异步
线程当中的同步和互斥中—>同步:让多个执行流合理的访问临界资源
同步和异步IO中—>同步:数据拷贝过程是否由程序员完成
- 是程序员完成:则为同步
- 是内核完成:则为异步
只需要判断,是否由调用者去等待调用结果
- 如果是,则是同步
- 如果不是,则是异步
IO多路复用
- 服务器端采用单线程通过select/epoll等系统调用获取fd列表,遍历有事件的fd进行accept/recv/send,使其能支持更多的并发连接请求。
- IO多路转接可以完成大量描述符的监控,监控的事件:可读事件、可写事件、异常事件。
- 当使用多路转接的时候,多路转接接口发现了某一个文件描述符就绪的时候,就会通知进程,让进程针对某一个描述符进行操作,其它描述符继续监控。
- 优点:避免了进程对其它没有就绪的文件描述符进行操作,从而陷入阻塞的情况。
IO多路复用的三种实现方式
select
1.作用:对于大量的描述符进行用户关心事件的监控,如果描述符就绪,则返回描述符,让用户对描述符进行操作
- 将用户关心的描述符拷贝到内核,内核帮助用户进行监控
- 如果内核监控到某个文件描述符事件就绪,则返回该描述符
- 用户针对描述符进行操作
2.接口
- nfds:取值为监控的最大描述符数量+1,作用:提高select的监控效率
- fd_set:本质是一个结构体,结构体内部是一个数组fds_bits(__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];)
结论:比特位的大小取决于宏__FD_SETSIZE,fd_set的使用方式是按照位图的方式来进行使用的
提供4个操作fd_set位图的函数:
-
从事件集合当中删除某一个文件描述符
-
判断fd描述符,是否在set集合当中
0:表示没有在集合当中 非0:表示在集合当中
-
设置fd到集合set当中
-
清空集合
timeout:超时等待时间
select返回值:
- >0:则返回就绪的文件描述符的个数
- ==0:等待超时了
- <0:监控出错
注意:select返回的时候,会将没有就绪的文件描述符从集合当中去除掉,只返回就绪的文件描述符!!!
select优缺点:
优点
- select遵循的是posix标准,可以跨平台移植
- select的超时时间可以精确到微秒
缺点
- select是轮询遍历的,监控的效率会随着文件描述符的增多而下降
- select所能监控的文件描述符是有上限的,上限为1024,取决于内核当中的__FD_SETSIZE宏的值
- select监控文件描述符的时候,需要将集合拷贝到内核当中,监控到文件描述符上有事件就绪的时候,同样需要从内核当中拷贝到用户空间,效率会受到影响
- select在返回就绪文件描述符的时候,会将未就绪的文件描述符移除掉,导致第二次在去监控的时候,需要重新添加
- select没有直接告诉程序员哪一个文件描述符就绪了,需要程序员自己在返回的事件集合当中去判断
poll
1.跨平台移植性不如select,poll只能在linux环境下使用,也是采用轮询遍历的方式,所以效率没有明显的增加
2.相较于select改进的地方
- 不限制监控的文件描述符个数
- 文件描述符对应一个事件结构,告诉poll两件事件(要监控的文件描述符、关心的文件描述符的事件)
3.接口
-
fds:事件结构数组
-
fd:关心的文件描述符是什么
-
events:关心的文件描述符产生什么事件
POLLIN:可读事件
POLLOUT:可写事件
如何让一个文件描述符即关心可读事件,也关心可写事件,应将两者进行按位或连接,采用的是位图的方式。
eg:events = POLLIN | POLLOUT -
revents:当关心的文件描述符产生对应关心的事件时,将发生的事件放到revents中返回给调用者,revents在每次poll监控之初,就会被初始化为空。
-
如:程序员需要在代码当中先定义一个事件结构数组
struct pollfd fd_arr[10];
fd_arr[0].fd = 3;
fd_arr[0].events = POLLIN -
nfds:指fd_arr数组当中的有效元素个数
-
timeout:超时等待时间
> 0:带有超时时间的监控,单位为毫秒
= 0:非阻塞
< 0:阻塞 -
返回值
< 0:poll出错了
= 0:监控超时
> 0:就绪的文件描述符的个数
poll优缺点
优点
- poll采用了事件结构的方式,简化了代码的编写
- poll不限制文件描述符的个数
- poll不需要添加文件描述符到事件结构数组中
缺点
- poll也需要轮询遍历事件结构数组,随着文件描述符的增多,性能会下降
- poll不支持跨平台
- poll没有告诉用户哪一个具体的文件描述符就绪了,需要程序员自己判断
- poll也需要将事件结构数组从用户空间拷贝到内核,再从内核拷贝到用户空间
epoll:目前公认的Linux下性能最高的IO多路转接模型
1.创建epoll操作句柄
- size:本来的含义是定义epoll最大能够监控的文件描述符的个数。但是Linux内核2.6.8之后,该参数size就已经被弃用了,内存现在采用的是扩容的方式。size是不可以传入负数的!
- 从内核角度分析:在内核当中创建一个结构体,struct eventpoll结构体(有红黑树和双向链表两个成员)
- 返回值:返回epoll的操作句柄,即就是操作struct eventpoll结构体的钥匙
2.操作epoll
- epfd:epoll的操作句柄
- op:想让epoll_ctl函数做什么事情
EPOLL_CTL_ADD:添加一个文件描述符对应的事件结构到红黑树中
EPOLL_CTL_MOD:修改一个已经存在红黑树当中的事件结构
EPOLL_CTL_DEL:从红黑树中删除一个文件描述符对应的事件结构 - fd:告诉epoll用户关心的文件描述符
- event:类型是struct epoll_event结构体,也是epoll的事件结构
- events:用户关心的文件描述符对应的事件
EPOLLIN:可读事件
EPOLLOUT:可写事件 - epoll_data_t:联合体
- ptr:可以传递一些信息,当epoll监控的文件描述符就绪的时候,返回后,程序员也可以拿到这些信息
- fd:用户关心的文件描述符,可以当作文件描述符对应的事件就绪后,返回给程序员看的
因为是联合体,对于ptr和fd共用一块内存,两者在使用的时候,只能任选其一 - ptr:传入一个结构体 struct my_epoll_data{ int fd }; 必须在结构体中包含一个文件描述符
- fd:fd的取值为文件描述符数值,ptr不能再使用
3.监控
- epfd:epoll的操作句柄
- events:epoll的事件结构数组,是一个出参,返回就绪的事件结构(每一个事件结构都对应一个文件描述符)
- maxevents:最大能够拷贝多少个事件结构
- timeout:
> 0:带有超时时间的监控,单位为毫秒
= 0:非阻塞
< 0:阻塞 - 返回值
< 0:epoll监控出错了
= 0:监控超时
> 0:就绪的文件描述符的个数
4.epoll工作原理
5.epoll对文件描述符就绪事件的触发方式
水平触发
- EPOLLLT:epoll默认工作方式,select和poll都是水平触发
- 可读事件:只要接收缓冲区当中的数据大于低水位标记(1字节),就会一直触发可读事件就绪,直到接收缓冲区当中没有数据可读
- 可写事件:只要发送缓冲区当中的数据空间大小大于低水位标记(1字节),就会一直触发可写事件就绪,直到发送缓冲区当中没有空间可写
边缘触发
- EPOLLET:只有epoll支持
- 使用方式:只需要在文件描述符对应的事件结构中的关心事件中 按位或上 EPOLLET就可以了
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET - 可读事件:只有当新数据到来的时候,才会触发可读。即每次都来一个新的数据,只会通知一次,如果应用程序没有将接收缓冲区的数据读走或者读完,也不会在通知,直到新的数据到来,才会触发可读事件。
- 可写事件:只有发送缓冲区剩余空间从不可写变成可写才会触发一次可写事件就绪
小结
对于ET模式,在就绪事件发生的时候,只会通知一次,如果没有将数据读完,则不会再去通知了。所以,对于ET模式的使用,需要循环的将数据一次性读完。
select、poll、epoll代码测试用例以及相关TCP服务中的使用
GitHub:https://github.com/achen228/Linux/tree/master/high_io