IO多路复用
1. IO分类
1.1 两阶段拷贝
假设client已经连接到server,那么在client发出read调用时,分为两个阶段的拷贝
- 网卡缓冲区 -> 内核缓冲区
- 内核缓冲区 -> 用户缓冲区
1.2 分类
1.2.1 阻塞/非阻塞、同步/异步
那么对于两阶段拷贝,根据不同的IO类型,有如下情况
-
阻塞
client发起阻塞read调用后,如果内核缓冲区为空,则会一直等待到网卡缓冲区拷贝数据到内核
-
非阻塞
client发出非阻塞read调用后,如果内核缓冲区为空,则会立即返回,不会等待网卡缓冲区的数据拷贝到内核
-
同步
那么client会一直阻塞等待内核缓冲区数据拷贝到用户缓冲区
-
异步
那么client会为此事件保留一个callback,不会发生阻塞,当数据拷贝到用户空间后,调用callback通知client来开始读取传来的数据
1.2.2 举例
下面列举一些实际的IO
-
阻塞read调用
同步阻塞
-
非阻塞read调用
同步非阻塞
-
IO多路复用
-
从用户角度看
阻塞IO,由于发起select等调用会一直阻塞到有socket返回,一般为同步
-
从内核角度看
非阻塞IO,由于内核调用的是非阻塞的read调用,检查哪些socket就绪,一般为同步
-
-
信号驱动IO
调用时向内核注册一个回调,当数据拷贝到内核空间时返回,一般为同步
-
异步IO
在调用后注册回调并返回,直到数据拷贝到用户空间触发回调,也即在两个阶段均不阻塞
2. IO多路复用
2.1 accept+用户态轮询
使用accpet监听,将返回的socket放入队列,之后创建线程周期性的发起非阻塞调用,检查哪些socket就绪
2.2 IO多路复用
将1)中用户态执行的轮询检查放入内核态执行
2.2.1 select
流程如下
-
将进程挂载到fd的wait-queue
-
将fd放入到用户态监听队列
-
用户态监听队列拷贝到内核监听队列
-
内核轮询监听队列,检查是否有就绪的fd
-
如果有,则返回就绪的个数,唤醒调用进程
-
进程轮询用户态监听队列,找出其中就绪的fd,进行处理
可见,进程只能得到就绪的fd的个数,不知道具体哪些fd就绪
2.2.2 poll
类似于select,采用链表方式构建等待队列,去掉了select中1024的监听队列限制
2.2.3 epoll
1)流程
epoll时一个存在于内核的结构体,其包含三个关键成员
-
rbr
红黑树,用于快速插入、查询、删除fd
-
rdlist
就绪队列,所有就绪的socket会被注册到这里
-
wait-queue
等待队列,用于触发回调
epoll执行有三个关键函数
-
epoll_create
创建epoll对象
-
epoll_ctl
创建epitem并插入epoll对象中的红黑树中
-
epoll_wait
检查epoll中的rdlist是否为空,如果为空则阻塞,不为空则返回
那么,一般的执行流程如下
-
epoll_create创建epoll对象,将其挂载到进程的fdlist中
-
调用epoll_ctl,创建epitem,将(epitem,ep_poll_callback)插入socket的waitqueue中,并将epitem插入rbr
-
发起epoll_wait,首次检查发现rdlist为空,创建(proc,default_wake_function)放入到epoll的wait-queue中
-
进程开始阻塞
-
......
-
当数据抵达网卡时
-
socket被唤醒,之后检查waitqueue,调用ep_poll_callback,
- 将epitem插入rdlist
- 根据proc,调用epoll的waitqueue上对应的default_wake_function,唤醒被阻塞进程
-
epoll_wait返回可用的fd的个数,并将rdlist拷贝到用户空间
-
被唤醒的进程遍历用户空间的rdlist,分别进行处理
可见,进程不仅可以得到就绪的fd个数,还是直到是哪些fd就绪
2)触发方式
epoll对于发送、接收内核缓冲区的状态检查,有两种触发方式
-
水平
-
读操作
只要接收缓冲区不空,就一直触发
-
写操作
只要发送缓冲区不满,就一直触发
-
-
边沿
-
读操作
当接受缓冲区收到数据时,只触发一次,如果未读完,则不会再次触发
-
写操作
当发送缓冲区空出空间时,只触发一次
那么在此状态下,只要可读/写,就要一直读/写,直到返回EAGAIN错误(读缓冲区无数据可读/发缓冲区无空间可写)
-
3. 应用
3.1 NIO-selector
Java的NIO中的selector即Reactor模式中的多路选择(解复用)器,用户进程需要将socket(channel)和对应的事件注册到selector上,之后selector所在线程调用select()
,其会阻塞到注册的事件中至少有一个被激活,当select()
返回之后调用selectedKeys()
用于返回所有的就绪事件对应的key,之后用户进程只需要遍历返回的就绪时间队列,判断对应的事件类型,执行对应的方法即可(如read返回,则开始读数据)
3.2 Redis
redis中由于采用单线程模型,为了应对并发的IO请求,同样采用IO多路复用来处理,将收到的任务交由文件事件分派器来处理
# 参考
Epoll在LT和ET模式下的读写方式 - 平凡的世界 (kimi.it)
Redis 到底是单线程还是多线程?我要吊打面试官! - Java技术栈 - 博客园 (cnblogs.com)
图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!