IO多路复用

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多路复用来处理,将收到的任务交由文件事件分派器来处理

IO多路复用

IO多路复用

# 参考

你管这破玩意叫 IO 多路复用? (qq.com)

Epoll在LT和ET模式下的读写方式 - 平凡的世界 (kimi.it)

JavaGuide-redis总结

Redis 到底是单线程还是多线程?我要吊打面试官! - Java技术栈 - 博客园 (cnblogs.com)

图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!

上一篇:转载:五种I/O模式


下一篇:再次针对BIO NIO Reactor模式进行总结(项目迭代)