4. 非阻塞i/o(nonblocking i/o)上文花了太多的笔墨描述bio,接下来的非阻塞io我们只抓主要矛盾,其余参考bio即可。
如果你看过其他介绍非阻塞io的文章,下面这个图片你多少会有点眼熟。
nio模型
非阻塞io指的是进程发起系统调用之后,内核不会将进程投入睡眠,而是会立即返回一个结果,这个结果可能恰好是我们需要的数据,又或者是某些错误。
你可能会想,这种非阻塞带来的轮询有什么用呢?大多数都是空轮询,白白浪费cpu而已,还不如让进程休眠来的合适。
4.1 java的非阻塞实现这个问题暂且搁置一下,我们先看java在语法层面是如何提供非阻塞功能的,细节慢慢聊。
public class noblockingserver { public static list channellist = new arraylist(); public static void main(string[] args) throws interruptedexception { try { // 相当于serversocket serversocketchannel serversocketchannel = serversocketchannel.open(); // 将监听socket设置为非阻塞 serversocketchannel.configureblocking(false); serversocketchannel.socket().bind(new inetsocketaddress(8099)); while (true) { // 这里将不再阻塞 socketchannel socketchannel = serversocketchannel.accept(); if (socketchannel != null) { // 将连接socket设置为非阻塞 socketchannel.configureblocking(false); channellist.add(socketchannel); } else { system.out.println(没有客户端连接!!!); } for (socketchannel client : channellist) { bytebuffer bytebuffer = bytebuffer.allocate(1024); // read也不阻塞 int num = client.read(bytebuffer); if (num > 0) { system.out.println(收到客户端【 + client.socket().getport() + 】数据: + new string(bytebuffer.array())); } else { system.out.println(等待客户端【 + client.socket().getport() + 】写数据); } } // 加个睡眠是为了避免strace产生大量日志,否则不好追踪 thread.sleep(1000); } } catch (ioexception e) { e.printstacktrace(); } }}java提供了新的api,serversocketchannel以及socketchannel,相当于bio中的serversocket和socket。此外,通过下面两行的配置,将监听socket和连接socket设置为非阻塞。
// 将监听socket设置为非阻塞serversocketchannel.configureblocking(false);// 将连接socket设置为非阻塞socketchannel.configureblocking(false);我们上文强调过, java自身并没有将socket设置为非阻塞的本事,一定是在某个时间点上,操作系统内核提供了这个功能,才使得java设计出了新的api来提供非阻塞功能 。
之所以需要上面两行代码的显式设置,也恰好说明了内核是默认将socket设置为阻塞状态的,需要非阻塞,就得额外调用其他系统调用。我们通过man命令查看一下socket()这个方法(截图的中间省略了一部分内容):
man 2 socket
image-20221225144028751
我们可以看到socket()函数提供了sock_nonblock这个类型,可以通过fcntl()这个方法将socket从默认的阻塞修改为非阻塞,不管是对监听socket还是连接socket都是一样的。
4.2 java的非阻塞解释现在解释上面提到的问题:这种非阻塞带来的轮询有什么用?观察一下上面的代码就可以发现,我们全程只使用了1个main线程就解决了所有客户端的连接以及所有客户端的读写操作。
serversocketchannel.accept();会立即返回调用结果。
返回的结果如果是一个socketchannel对象(系统调用底层就是个socket描述符),说明有客户端连接,这个socketchannel就表示了这个连接;然后利用socketchannel.configureblocking(false);将这个连接socket设置为非阻塞。这个设置非常重要,设置之后对连接socket所有的读写操作都变成了非阻塞,因此接下来的client.read(bytebuffer);并不会阻塞while循环,导致新的客户端无法连接。再之后将该连接socket加入到channellist队列中。
如果返回的结果为空(底层系统调用返回了错误),就说明现在还没有新的客户端要连接监听socket,因此程序继续向下执行,遍历channellist队列中的所有连接socket,对连接socket进行读操作。而读操作也是非阻塞的,会理解返回一个整数,表示读到的字节数,如果>0,则继续进行下一步的逻辑处理;否则继续遍历下一个连接socket。
下面给出一张accept()返回一个连接socket情况下的动图,希望对大家理解整个流程有帮助。
4.3 掀开非阻塞io的底裤我将上面的程序在centos下再次用strace程序追踪一下,具体步骤不再赘述,下面是out日志文件的内容(我忽略了绝大多数没用的)。
非阻塞io的系统调用分析
4.4 非阻塞io总结
nio模型
再放一遍这个图,有一个细节需要大家注意,系统调用向内核要数据时,内核的动作分成两步:
等待数据(从网卡缓冲区拷贝到内核缓冲区)拷贝数据(数据从内核缓冲区拷贝到用户空间)只有在第1步时,系统调用是非阻塞的,第2步进程依然需要等待这个拷贝过程,然后才能返回,这一步是阻塞的。
非阻塞io模型仅用一个线程就能处理所有操作,对比bio的一个客户端需要一个线程而言进步还是巨大的。但是他的致命问题在于会不停地进行系统调用,不停的进行accept(),不停地对连接socket进行read()操作,即使大部分时间都是白忙活。要知道,系统调用涉及到用户空间和内核空间的多次转换,会严重影响整体性能。
所以,一个自然而言的想法就是,能不能别让进程瞎轮询。
比如有人告诉进程监听socket是不是被连接了,有的话进程再执行accept();比如有人告诉进程哪些连接socket有数据从客户端发送过来了,然后进程只对有数据的连接socket进行read()。
这个方案就是 i/o多路复用 。
电脑的机箱风扇不联网也能泄露机密信息
Netmiko+excle定时检测接口状态
宇凡微Y53R 433MHz合封接收芯片,集成MCU和433接收功能
iOS10.3Beta6时隔3天!iOS10.3Beta7来了苹果你是疯了还是搞事情
手环vs手表,谁将更强?
探究Redis网络模型究竟有多强大(下)
区块链技术能否推动经济社会的发展繁荣
需要了解的Linux模块编程框架
一文详解51单片机的存储器组织结构
人工智能在教育行业的应用
国产大飞机C919距离交付又近一步:再次成功试飞
空间隔离操作系统µC/OS-MPU中段的定位
用C语言实现状态机设计模式
AI时代应该具备怎样的嵌入式思维
西门子MES平台在湖北卷烟厂中的应用
自制万用表升压电路(三款万用表升压电路设计方案详解)
基于有限状态机(FSM)的SiC MOSFET开关瞬态建模
为何对区块链技术如此重视
特斯拉车型的需求旺盛而交不了车?
C语言中指针变量简述