IO多路复用
什么是IO多路复用
I/O :网络 I/O
多路 :多个网络连接
复用:复用同一个线程。
IO多路复用其实就是一种同步IO模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出cpu
I/O主要分为:网络IO(本质是socket文件读取)、磁盘IO
每次IO,都要经由两个阶段:
1. 将数据从文件先加载至内核内存空间(缓冲区),等待数据准备完成,时间较长
2. 第二步:将数据从内核缓冲区复制到用户空间的进程的内存中,时间较短
I/O模型分类
发起系统调用的是运行在系统上的某个应用的进程,对象是磁盘上的数据,获取数据需要通过I/O,整个过程就是应用等待获取磁盘数据。针对整个过程中应用进程的状态不同,可以分为:阻塞型、非阻塞型、复用型、信号驱动型、异步
1、阻塞I/O模型:在等待数据和数据复制两个阶段都处于阻塞状态
1.1、阻塞:blocking,指IO操作需要彻底完成后才返回到用户空间,调用结果返回之前,调用者被挂起
1.2、特点:在等待数据和数据复制两个阶段都处于阻塞状态
1.3、原理:用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。用户需要等待read将数据读取到buffer后,才继续处理接收的数据。
1.4、优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源
1.5、缺点:每次请求需要单独的进程/线程处理,整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够,当并发请求量大时为了维护程序,内存、线程切换开销较大
2、非阻塞IO模型:在等待数据和数据复制两个阶段都处于阻塞状态
2.1、非阻塞:nonblocking,指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成,最终的调用结果返回之前,调用者不会被挂起。进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程
2.2、特点:
轮询机制:用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。
实现难度低、开发应用相对阻塞IO模式较难:比如,轮询的时间不好把握。这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试
2.3、在等待数据和数据复制两个阶段都处于非阻塞状态
2.4、典型应用:socket是非阻塞的方式
3、IO多路复用模型
重点在于select,select可以监控多个IO上是否已有IO操作准备就绪,即可达到在同一个线程内同时处理多个IO请求的目的。而不像阻塞IO那种,一次只能监控一个IO.
3.1、典型应用:select、poll、epoll
3.2、IO多路复用:是一种机制,程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”, 内核一旦发现进程指定的一个或者多个IO条件准备读取,就通知该进程
3.3、实现:用户首先将需要进行IO操作添加到select中,同时等待select系统调用返回。当数据到达时,IO被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行
3.4、问题:IO多路复用是最常使用的IO模型,多个连接共用一个等待机制,但是其异步程度还不够“彻底”,因它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO模型,而非真正的异步IO,但是进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的IO操作上
4、信号驱动IO:signal-driven I/O
当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。特点:回调机制,实现、开发应用难度大
5、异步IO模型
5.1、定义:当进程发起一个IO操作,进程返回(不阻塞),但也不能返回果结;内核把整个IO处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据
5.2、与信号驱动IO的区别:信号驱动IO是由内核通知应用程序何时可以进行IO操作,而异步IO则是由内核告诉用户线程IO操作何时完成。信号驱动IO当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后,内核直接通知用户线程可以进行后续操作了
5.3、优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠
5.4、缺点:要实现真正的异步 I/O,操作系统需要做大量的工作
IO多路复用
select | poll | epoll | |
操作方式 | 遍历 | 回调 | |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | O(n) | O(n) | O(1) |
最大连接数 |
x86:1024 x64:2048 |
无上限 | 无上限 |
I/O多路复用就通过一种机制可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间
select
1、实现
① 使用copy_from_user从用户空间拷贝fd_set到内核空间
② 注册回调函数__pollwait,主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了
③ 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll),poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值
④ 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd
⑤ 把fd_set从内核空间拷贝到用户空间
poll
epoll
1、epoll针对select的三个不足做了优化
① 最大限制为1024
epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子, 在1GB内存的机器上大约是10万左右
② 用轮询方法效率较低
epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在 epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调 函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd
③ 内存拷贝开销大
epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定 EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝 一次
2、实现原理
① 调用epoll_create方法,在内核中创建eventpoll结构体,返回一个描述符作为操作句柄,这个结构体中有两个成员与epoll的使用方式密切相关,epoll的监控事件rab和双向链表rdllist
struct eventpoll{ ..... /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll监控的事件*/ struct rb_root rbr; /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/ struct list_head rdllist; ........ }
② 对需要监控的描述符组织事件结构体(struct epitem), 所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,开启监控后,epoll的监控时一个异步阻塞操作,只需要告诉操作系统哪些描述符需要监控,然后这个监控的过程就由操作系统来完成.操作系统为每一个描述符所需要监控的事件设置了一个回调函数,相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中
struct epitem { ...... //红黑树节点 struct rb_node rbn; //双向链表节点 struct list_head rdllink; //事件句柄等信息 struct epoll_filefd ffd; //指向其所属的eventepoll对象 struct eventpoll *ep; //期待的事件类型 struct epoll_event event ...... };// 这里包含每一个事件对应着的信息。
③ 发起监控后,当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接
3、优点
① 底层用的红黑树存储,监控的描述符数量没有上限
② 所有的描述符事件信息只需要向内核中拷贝一次
③ 监控采用异步阻塞,性能不会随着描述符增多而下降
④ 直接返回就绪描述符事件信息,可以直接对就绪描述符进行操作,不需要像select和poll一样遍历判断
4、不足
① 无法跨平台移植
② 超时等待时间只能精确到毫秒
③ 在活跃连接较多的时候,由于会大量触发回调函数,所以此时epoll的效率未必会比select和poll高,所以epoll适用于连接数量多,但是活跃连接少的情况
5、epoll的两种触发模式
epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式
① LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
② ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞
6、一个特点
epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
参考资料:epoll原理详解及epoll反应堆模型