请选择 进入手机版 | 继续访问电脑版

Linux的IO模型进化详解

[复制链接]
小甜心 发表于 2020-12-31 18:11:11 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
根本概念

IO在盘算机世界中职位举足轻重,IO效率一直是码农们孜孜不倦最求的目的。本文我们一起来研究下Linux的IO的工作方式是如何一步步进化到本日的。我们说的IO主要是指应用步伐在工作过程中用到的IO范例,包罗两种IO:文件IO和网络IO,本文主要研究的是网络IO。应用历程和内核之间的数据交互方式一直在演进,下面我们对各种形态的交互方式举行介绍。在这之前,我们先明确几个概念:内核空间和用户空间、同步和异步、阻塞和非阻塞。
内核空间

操作系统单独拥有的内存空间为内核空间,这块内存空间独立于其他的应用内存空间,除了操作系统,其他应用步伐不允许访问这块空间。但操作系统可以同时操作内核空间和用户空间。
用户空间

单独给用户应用历程分配的内存空间,操作系统和应用步伐都可以访问这块内存空间。
同步

调用线程发出同步请求后,在没有得到效果前,该调用就不会返回。所有同步调用都必须是串行的,前面的同步调用处理完了后才气处理下一个同步调用。
异步

调用线程发出异步请求后,在没有得到效果前,该调用就返回了。真正的效果数据会在业务处理完成后通过发送信号大概回调的形式通知调用者。
阻塞

调用线程发出请求后,在没有得到效果前,该线程就会被挂起,此时CPU也不会给此线程分配时间,此线程处于非可执行状态。直到返回效果返回后,此线程才会被唤醒,继承运行。划重点:线程进入阻塞状态不占用CPU资源。
非阻塞

调用线程发出请求后,在没有得到效果前,该调用就返回了,整个过程调用线程不会被挂起。
同步阻塞IO

同步阻塞IO模式是Linux中最常用的IO模子,所有Socket通信默认使用同步阻塞IO模子。同步阻塞IO模子中,应用线程在调用了内核的IO接口后,会一直被阻塞,直到内核将数据准备好,而且复制到应用线程的用户空间内存中。常见的Java的BIO,阻塞模式的Socket网络通信就是使用这种模式举行网络数据传输的。
我们可以用man read下令来检察Linux内核提供的读IO的函数read
  1. ssize_t read(int fd, void *buf, size_t count);
复制代码
当应用线程发起读操作的IO请求时,内核收到请求后进入期待数据阶段,此时应用线程会处于阻塞的状态。当内批准备好数据后,内核会将数据从内核空间拷贝到用户内存空间,然后内核给应用线程返回效果,此时应用线程才解除阻塞的状态。
同步阻塞模式简单直接,没有下面几种模式的线程切换、回调、通知等消耗,在并发量较少的网络通信场景下是最好的选择。
但是在大规模网络通信的场景下,大量的请求和毗连需要处理,线程被阻塞是不可继承的。


  • 优点:并发量较少的网络通信场景较高效,应用步伐开发简单。
  • 缺点:不适合并发量较大的网络通信场景。
同步非阻塞IO

同步非阻塞IO是同步阻塞IO的一种变种IO模式,它和同步阻塞区别在于,应用线程在向内核发送IO请求后,内核的IO数据在没有准备好的时候会立即给应用线程返回一个错误代码(EAGAIN 或 EWOULDBLOCK),在内核的IO数据准备好了之后,应用线程再发起IO操作请求时候,内核会在将IO数据从内核空间复制到用户空间后给应用线程返回正常应答。常见的Non-Blocking模式的Socket网络通信就是同步非阻塞模式。
当用户线程发起读操作时,如果内核的IO数据还没有准备好,那么它不会阻塞掉用户线程,而是会直接返回一个 EAGAIN/EWOULDBLOCK 错误。从用户线程的角度,它发起一个读操作后立即就得到了一个效果,用户历程判断效果是 EAGAIN/EWOULDBLOCK 之后会再次发起读操作。这种利用返回值不断调用被称为轮询(polling),显而易见,这么做会泯灭大量 CPU 时间。一旦内核中的IO数据准备好了,而且又再次收到了用户历程的请求,那么它马上就将数据拷贝到了用户内存,然后返回。


  • 优点:在内核IO数据准备阶段不会阻塞应用线程,适合对线程阻塞敏感的网络应用。
  • 缺点:轮询查询内核IO数据状态,泯灭大量CPU,效率低。
多路复用

多路复用是现在大型互联网应用中最常见的一种IO模子,简单说就是应用历程中有一个IO状态管理器,多个网络IO注册到这个管理器上,管理器使用一个线程调用内核API来监听所有注册的网络IO的状态变革情况,一旦某个毗连的网络IO状态发生变革,可以或许通知应用步伐举行相应的读写操作。多路网络IO复用这个状态管理器,所以叫多路复用模式。多路复用本质上是同步阻塞,但与传统的同步阻塞多线程模子相比,IO 多路复用的最大优势是在处理IO高并发场景时只使用一个线程就完成了大量的网络IO状态的管理工作,系统资源开销小。Java的NIO,Nginx都是用的多路复用模式举行网络传输。多路复用的根本工作流程:

  • 应用步伐将网络IO注册到状态管理器;
  • 状态管理器通过调用内核API来确认所管理的网络IO的状态;
  • 状态管理器探知到网络IO的状态发生变革后,通知应用步伐举行实质的同步阻塞读写操作。
现在Linux主要有三种状态管理器:select,poll,epoll。epoll是Linux现在大规模网络并发步伐开发的首选模子,在绝大多数情况下性能远超select和poll。现在盛行的高性能Web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发毗连不高的情况下,多线程+阻塞I/O方式大概性能更好。他们也是差异汗青时期的产物:


  • select出现是1984年在BSD里面实现的;
  • 14年之后也就是1997年才实现了poll,实在拖那么久也不是效率问题, 而是谁人时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求;
  • 2002, 大神 Davide Libenzi 实现了epoll;
这三种状态管理器都是通过差异的内核API来监督网络毗连的状态,差异的API提供差异的本领,导致了性能上的差异,下面我们逐个分析下。
select

select是最古老的多路复用模子,Linux在2.6版本之前仅提供select模式,一度是主流的网络IO模式。select接纳定期轮询的方式将自己管理的所有网络IO对应的文件句柄发送给内核,举行状态查询,下面是内核系统对应用步伐提供的API:man select
  1. int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
复制代码
fd_set是一个Long的数组的数据布局,用于存放的是文件句柄(file descriptor)。这个API有三个关键参数,即readset/writeset/exceptset,前两个参数是注册到select,所有需要监听的网络IO文件句柄数组,第三个参数是一个空数组,由内核轮询所有网络IO文件句柄后,将状态有变革的文件句柄值写入到exceptset数组中,也就是说readset/writeset是输入数据,exceptset是输出数据。最后,内核将变革的句柄数数量返回给调用者。
select的工作流程:

  • 应用线程将需要监督的网络IO文件句柄注册到select状态监督器;
  • select状态监督器工作线程定期调用内核API,将自己所有管理的文件句柄通过(readset/writeset)两个参数传给内核;
  • 内核轮询所有传进来的文件句柄的网络IO状态,将有变革的文件句柄值写入exceptset数组中,而且将变革的句柄数数量返回给调用者;
  • select工作线程通知应用步伐举行实质的同步阻塞读写操作。
select机制的特性分析:

  • 每次调用select,都需要把readset/writeset聚集从用户空间态拷贝到内核空间,如果readset/writeset聚集很大时,那这个开销很大;
  • 每次调用select都需要在内核遍历通报进来的所有文件句柄,每次调用都举行线性遍历,时间复杂度为O(n),文件句柄聚集很大时,那这个开销也很大;
  • 内查对被监控的文件句柄聚集巨细做了限制,X86为1024,X64为2048。
poll

poll模子和select模子非常雷同,状态监督器同样管理一批网络IO状态,内核同样对传输过来的所有网络IO文件句柄举行线性轮询来确认状态,唯一区别是应用线程传输给内核的文件句柄数组不限制巨细,管理了select中说到的第三个问题,其他两个问题依然存在。
  1. int poll(struct pollfd *fds, nfds_t nfds, int timeout);typedef struct pollfd {        int fd;                         // 需要被检测或选择的文件形貌符        short events;                   // 对文件形貌符fd上感兴趣的事件        short revents;                  // 文件形貌符fd上当前实际发生的事件} pollfd_t;
复制代码
这个是内核提供的poll的API,fds是一个struct pollfd范例的数组,用于存放需要检测其状态的网络IO文件句柄,而且调用poll函数之后fds数组不会被清空;pollfd布局体体现一个被监督的文件形貌符。此中,布局体的events域是监督该文件形貌符的事件掩码,由用户来设置这个字段,布局体的revents字段是文件句柄的操作效果事件掩码,内核在调用返回时设置这个字段。
epoll

epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有文件句柄个数限制,将应用步伐关心的网络IO文件句柄的事件存放到内核的一个事件表中,在用户空间和内核空间的copy只需一次。epoll内核和网络设备创建了订阅回调机制,一旦注册到内核事件表中的网络毗连状态发生了变革,内核会收到网络设备的通知,订阅回调机制替换了select/poll的轮询查询机制,将时间复杂度从原来的O(n)低落为O(1),大幅提升IO效率,特别是在大量并发毗连中只有少量活泼的场景。
Linux提供的三个epoll的API:
  1. int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
复制代码
epoll将select/poll的一个大API变成了三个API,目的就是先把需要监听的句柄数据通过epoll_ctl接口在内核注册了,不消每次查询网络IO状态的时候传一大堆数据已往。epoll在内核内存里建了一个红黑树用于存储epoll_ctl传来的毗连,epoll内核还会创建一个rdllist双向链表,用于存储网络状态发生变革的文件句柄,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可,有数据就返回,没有数据就让epoll_wait睡眠,比及timeout时间到后纵然链表没数据也返回。因为epoll不像select/poll那样接纳轮询每个毗连来确认状态的方法,而是监听一个双向链表,在毗连数许多的情况下,epoll_wait非常高效。
所有添加到epoll中的网络毗连都会与设备(如网卡)驱动步伐创建回调关系,也就是说相应毗连状态的发生时网络设备会调用回调方法通知内核,这个回调方法在内核中叫做ep_poll_callback,它会把网络状态发生变革的变动事件放到上面的rdllist双向链表中。
当调用epoll_wait检查是否有状态变动事件的毗连时,只是检查eventpoll对象中的rdllist双向链表是否有数据而已,如果rdllist链表不为空,则将链表中的事件复制到用户态内存(使用MMAP提高效率)中,同时将事件数量返回给用户。epoll_ctl在向epoll对象中添加、修改、删除监听的网络IO文件句柄时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发毗连。
epoll的epoll_wait调用,有EPOLLLT和EPOLLET两种触发返回模式,LT是默认的模式,更加安全,ET是“高速”模式,更加高效:


  • **水平触发(LT):**默认工作模式,即当epoll_wait检测到某网络毗连状态发生变革并通知应用步伐时,应用步伐可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件;
  • 边沿触发(ET): 当epoll_wait检测到某网络毗连状态发生变革并通知应用步伐时,应用步伐必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。
epoll机制的特性分析:

  • 没有最大并发毗连的限制,能打开的FD的上限远大于1024(1G的内存上能监听凌驾10万个毗连);
  • 通过epoll_ctl方法将网络毗连注册到内核,不消每次查询毗连状态时将所有网络文件句柄传输传输给内核,大幅提高效率;
  • 内核和网络设备创建事件订阅机制,监听毗连网络状态不使用轮询的方式,不会随着文件句柄数目的增加效率下降,只有活泼可用的文件句柄才会触发回调函数;Epoll最大的优点就在于它只管你“活泼”的毗连,而跟毗连总数无关;
  • 利用MMAP内存映射技术加速用户空间与内核空间的消息通报,淘汰复制开销。
现在大部门主流的应用都是基于此种IO模子构建的,好比Nginx,NodeJS,Netty框架等,总结一下多路复用的特点:


  • 优点:使用一个线程监控多个网络毗连状态,性能好,特别是进化最终形态epoll模式,适合大量毗连业务场景
  • 缺点:较复杂,应用开发难度大
信号驱动

信号驱动模式利用linux信号机制,通过sigaction函数将sigio读写信号以及handler回调函数注册到内核队列中,注册后应用历程不堵塞,可以去干别的工作。当网络IO状态发生变革时触发SIGIO中断,通过调用应用步伐的handler通知应用步伐网络IO停当了。信号驱动的前半部门操作是异步行为,反面的网络数据操作仍然属于同步阻塞行为。


  • 优点:这种异步回调方式避免用户或内核主动轮询设备造成的资源浪费
  • 缺点:handler是在中断情况下运行,多线程不稳定,而且平台兼容性欠好,不是一个完善可靠的管理方案,实际应用场景少
异步IO

异步IO通过一些列异步API实现,是五种IO模式中唯一个真正的异步模式,现在Java的AIO使用的就是本模式。异步模式的读操作通过调用内核的aio_read函数来实现。应用线程调用aio_read,递交给内核一个用户空间下的缓冲区。内核收到请求后立即返回,不阻塞应用线程。当网络设备的数据到来后,内核会自动把数据从内核空间拷贝到aio_read函数递交的用户态缓存。拷贝完成后以信号的方式通知用户线程,用户线程拿到数据后就可以执行后续操作。
异步IO模式与信号驱动IO的区别在于:信号驱动IO由内核通知应用步伐什么时候可以开始IO操作,异步IO则由内核告诉应用步伐IO操作何时完成。异步IO主动把数据拷贝到用户空间,不需要调用recvfrom方法把数据从内核空间拉取到用户态空间。异步IO是一种推数据的机制,相比于信号处理IO拉数据的机制效率会更高。
异步IO照旧属于比力新的IO模式,需要操作系统支持,Linux2.5版本首次提供异步IO,Linux2.6及以后版本,异步IO的API都属于标准提供。异步IO现在没有太多的应用场景。


  • 优点:纯异步,高效率高性能。
  • 缺点:效率比多路复用模式没有质的提升,成熟应用迁移模式的动力不足,一直没有大规模成熟应用来支撑。
总结

五种Linux的IO模式各有特色,存在即合理,各有自己的应用场景。现在,各人在写一些简单的低并发Socket通信时大多数照旧使用多线程加同步阻塞的方式,效率和其他模式差不多,实现起来会简单许多。
现在市面上盛行的高并发网络通信框架,Nginx、基于Java的NIO的Netty框架和NodeJS等都是使用使用的多路复用模子,颠末大量实际项目验证,多路复是现在最成熟的高并发网络通信IO模子。多路复用模子中的epoll是最优秀的,现在Linux2.6以上的系统提供标准的epoll的API,Java的NIO在Linux2.6及以上版本都会默认提供epoll的实现,在低于Linux2.6版本上会提供poll的实现。Windows现在还不支持epoll,只支持select,不外也什么,根本上没什么人用Windows来做网络应用服务器。
信号驱动IO感觉不太成熟,根本上没有见过使用场景。纯异步模式,内核把所有事情做了,看起来很美好,Java也提供了响应的实现,但由于效率比多路复用模式没有质的提升,成熟应用迁移模式的动力不足,一直没有大规模成熟应用来支撑。
参考:
《深入分析 Java I/O 的工作机制》
《IO多路复用的三种机制Select,Poll,Epoll》

来源:https://blog.csdn.net/u013277209/article/details/111997907
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题

专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )