介绍
在谈及网络IO的时候总避不开阻塞、非阻塞、同步、异步、IO多路复用、select、poll、epoll等这几个词语。在面试的时候也会被经常问到这几个的区别。本文就来讲一下这几个词语的含义、区别以及使用方式。
Unix网络编程一书中作者给出了五种IO模型:
1、BlockingIO - 阻塞IO
2、NoneBlockingIO - 非阻塞IO
3、IO multiplexing - IO多路复用
4、signal driven IO - 信号驱动IO
5、asynchronous IO - 异步IO
这五种IO模型中前四个都是同步的IO,只有最后一个是异步IO。信号驱动IO使用的比较少,重点介绍其他几种IO以及在Java中的应用。
阻塞、非阻塞、同步、异步以及IO多路复用
在进行网络IO的时候会涉及到用户态和内核态,并且在用户态和内核态之间会发生数据交换,从这个角度来说我们可以把IO抽象成两个阶段:1、用户态等待内核态数据准备好,2、将数据从内核态拷贝到用户态。之所以会有同步、异步、阻塞和非阻塞这几种说法就是根据程序在这两个阶段的处理方式不同而产生的。
同步阻塞
当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待kernel把数据拷贝到用户态。
在Java中同步阻塞的实现对应的是传统的文件IO操作以及Socket的accept的过程。在Socket调用accept的时候,程序会一直等待知道有描述符就绪,并且把就绪的数据拷贝到用户态,然后程序中就可以拿到对应的数据。
同步非阻塞
对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待。
IO多路复用
IO多路复用也是同步的。
IO多路复用的方式看起来跟同步阻塞是一样的,两个阶段都是阻塞的,但是IO多路复用可以实现以较小的代价同时监听多个IO。通常情况下是通过一个线程来同时监听多个描述符,只要任何一个满足就绪条件,那么内核态就返回。IO多路复用使得传统的每请求每线程的处理方式得到解耦,一个线程可以同时处理多个IO请求,然后交到后面的线程池里处理,这也是netty等框架的处理方式,所谓的reactor模式。IO多路复用的实现依赖于操作系统的select、poll和epoll,后面会详细介绍这几个系统调用。
IO多路复用在Java中的实现方式是在Socket编程中使用非阻塞模式,然后配置感兴趣的事件,通过调用select函数来实现。select函数就是对应的第一个阶段。如果给select配置了超时参数,在指定时间内没有感兴趣事件发生的话,select调用也会返回,这也是为什么要做非阻塞模式下运行。
异步IO
异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。
IO多路复用
现在用Java开发的网络服务器通常采用IO多路复用的方式来加快网络IO操作,例如Netty、Tomcat等。IO多路复用的基础是select、poll和epoll。这三个函数是从操作系统的角度上支持的IO多路复用的操作,下面就分别来看一下这三个函数。
select
函数签名如下:
|
int select(int maxfdp1, fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
|
maxfdp1为指定的待监听的描述符的个数,因为描述符是从0开始的,所以需要加1
readset为要监听的读描述符
writeset为要监听的写描述符
exceptset为要监听的异常描述符
timeout监听没有准备好的描述符的话,多久可以返回,支持按照秒或者毫秒来配置时间
select操作的逻辑是首先将要监听的读、写以及异常描述符拷贝到内核空间,然后遍历所有的描述符,如果有感兴趣的事件发生,那么就返回。
select在使用的过程中有三个问题:
1、被监控的fds(描述符)集合限制为1024,1024太小了
2、需要将描述符集合从用户空间拷贝到内核空间
3、当有描述符可操作的时候都需要遍历一下整个描述符集合才能知道哪个是可操作的,效率很低。
poll
函数签名如下:
|
int poll(struct pollfd[] fds, unsigned int nfds, int timeout);
|
poll操作与select操作类似,仍旧避免不了描述符从用户空间拷贝到内核空间,但是poll不再有1024个描述符的限制。对于事件的触发通知还是使用遍历所有描述符的方式,因此在大量连接的情况下也存在遍历低效的问题。poll函数在传递参数的时候统一的将要监听的描述符和事件封装在了pollfd结构体数组中。
epoll
epoll有三个方法:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 通过这三个方法epoll解决了select的三个问题。
1、1024数量限制的问题
通过epoll_create方法来创建一个epoll句柄,这个句柄监听的描述符的数量不再有限制。
2、文件描述符频繁从用户空间拷贝到内核空间的问题
通过观察select的操作会发现描述符从用户空间到内核空间拷贝发生在调用select方法的时候,只要没有注册新的事件或者取消注册事件,每次拷贝的描述符都是一样的。因此epoll引入了epoll_ctl调用,该方法用于注册新事件和取消注册事件。而在epoll_wait的时候并不会拷贝描述符,描述符始终存在于内核空间,当需要修改的时候只要调用epoll_ctl修改一下内核的描述符即可。如此一来便省去了描述符来回拷贝的开销。
3、文件描述符可操作的时候遍历整个描述符集合的问题
在调用epoll_ctl注册感兴趣的事件的时候,实际上会为设置的事件添加一个回调函数,当对应的感兴趣的事件发生的时候,回调函数就会触发,然后将自己加到一个链表中。epoll_wait函数的作用就是去查看这个链表中有没有已经准备就绪的事件,如果有的话就通知应用程序处理,如此操作epoll_wait只需要遍历就绪的事件描述符即可。
epoll在Java中的使用
目前针对Java服务器的非阻塞编程基本都是基于epoll的。在进行非阻塞编程的时候有两个步骤:1、注册感兴趣的事情;2、调用select方法,查找感兴趣的事件。
注册感兴趣的事件
我们在编写Socket的非阻塞代码的时候需要在Selector上注册感兴趣的事情,通常写法是serverSocketChannel.register(selector, SelectionKey.XXX)。来看一下这行代码背后的执行逻辑是什么样的。
注册的时候实际执行的是EPollSelectorImp。该方法主要有以下三步:
1、implRegister方法。在fdToKey的Map中插入channel对应的文件描述法和SelectionKey的映射,当做注册Channel、关闭Channel、取消注册等操作是都是操作此Map。
2、往pollWrapper[Epoll实例]中放入channel实例。
3、往keys[HashSet]中放入SelectionKey
select方法
通过Java的Selector.select方法来获取准备好的键的时候实际执行的代码如下:
首先调用EPollArrayWrapper的poll方法,该方法做两件事:1、调用epollCtl方法向epoll中注册感兴趣的事件;2、调用epollWait方法返回已就绪的文件描述符集合
然后调用updateSelectedKeys方法调用把epoll中就绪的文件描述符加到ready队列中等待上层应用处理, updateSelectedKeys通过fdToKey查找文件描述符对应的SelectionKey,并在SelectionKey对应的channel中添加对应的事件到ready队列。
水平触发LT与边缘触发ET
epoll支持两种触发模式,分别是水平触发和边缘触发。
LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核会通知你一次,并且除非你做了某些操作导致那个文件描述符不再为就绪状态了,否则不会再次发送通知。
可以看到,本来内核在被DMA中断,捕获到IO设备来数据后,只需要查找这个数据属于哪个文件描述符,进而通知线程里等待的函数即可,但是,LT要求内核在通知阶段还要继续再扫描一次刚才所建立的内核fd和io对应的那个数组,因为应用程序可能没有真正去读上次通知有数据后的那些fd,这种沟通方式效率是很低下的,只是方便编程而已;
JDK并没有实现边缘触发,关于边缘触发和水平触发的差异简单列举如下,边缘触发的性能更高,但编程难度也更高,netty就重新实现了Epoll机制,采用边缘触发方式;另外像nginx等也采用的是边缘触发。
----------------------------------------------------------------
欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
相关推荐
仓颉编程语言:一文读懂,什么是仓颉编程语言? 仓颉编程语言:一文读懂,什么是仓颉编程语言? 仓颉编程语言:一文读懂,什么是仓颉编程语言? 仓颉编程语言:一文读懂,什么是仓颉编程语言? 仓颉编程语言:一文读...
### 一文理解异步同步阻塞与非阻塞 #### 同步与异步 在探讨异步和同步的概念前,我们首先需要明确这两者的区别。同步与异步主要关注的是消息通信机制,即synchronous communication 和 asynchronous communication...
吐血整理,一文读懂中国金融衍生品市场的前世今生和未来.doc
银行业:一文读懂LPR全部故事,LPR改革22问-20190828-光大证券-12页.pdf
非银行业新业态系列报告(三)一文读懂keep招股书:以内容和社区为核心打造一体化线上健身生态圈
一文读懂工业互联网标识解析体系.docx
一文读懂重点报表项目——重新认识Roe(中) 正常情况下,如果你是一家企业的管理层,董事会对你的考核指标是提高我们这家企业的ROE 你直接根据最简单的公式:净资产收益率=净利润/所有者权益 只要提高分子或者减少...
20210614-国泰君安-社服行业:一文读懂BOSS直聘(BZ.O)招股书,开创全新模式,直面招聘难题.pdf
20210620-华西证券-城投新论(十三):一文读懂城市更新.pdf
电感是电子电路中重要的基础元件,其作用主要表现在能够将电能转换成磁能并储存起来。电感器具有特殊的特性,它能够阻止交流电的流通,而允许直流电顺利通过,这种特性在电路中有着广泛的应用。...
一文读懂5G无线通信与4G的典型区别一文读懂5G无线通信与4G的典型区别一文读懂5G无线通信与4G的典型区别一文读懂5G无线通信与4G的典型区别一文读懂5G无线通信与4G的典型区别一文读懂5G无线通信与4G的典型区别一文读懂...
一文读懂重点报表项目——货币资金(下) 从客观上来说,货币资金这个项目的造假难度相对较高,因为审计时一般会向银行发询证函,要求所有的账户必须取得银行回函,除非你有本事让银行协助你造假,不然的话分分钟就...
随着时间推移,企业在面对快速变化的市场需求时发现,孤立的数据孤岛导致信息同步困难,不同部门间的数据标准不一致等问题愈发明显。这促使企业寻求一种能够整合并统一管理数据的方法,从而更好地支持业务决策和运营...
一文读懂GNSS的RTK
一文读懂领域迁移与领域适应的常见方法,附文档链接: https://mp.weixin.qq.com/s/TJjgyL4dR2cMfbk8DUmbfw。在使用机器学习、人工智能中的技术手段解决实际问题的时候,常常会遇到领域迁移(domain shift)问题。
边缘计算是一种计算模式,它将数据处理和应用程序的执行从中央云数据中心转移到网络的边缘,更接近终端设备,如智能手机、物联网(IoT)设备或传感器。这种技术旨在减少延迟,提高响应速度,同时减轻核心网络和云计算...
SR(Segment Routing)技术是从传统的MPLS(Multi-Protocol Label Switching,多协议标签交换)技术发展而来的,它继承了MPLS的标签转发机制,并在控制面上进行了创新和优化。MPLS是一种广泛应用于广域网的数据传输...
可解释机器学习(IML)是当前人工智能和机器学习领域中的一个重要研究方向,它主要关注机器学习模型的解释性和透明度。随着机器学习技术在各个领域中的广泛应用,模型的可解释性变得越来越重要。...
中国绿色债券市场的发展与实践是全球可持续发展的重要组成部分,它不仅体现了金融创新在推动环境保护和气候变化缓解方面的积极作用,也代表了中国在全球金融市场中不断增长的影响力和责任担当。...
一文读懂“国企改革职业经理人制度”.docx