重新理解IO模型

在进行Linux网络编程开发的时候,免不了会涉及到IO模型的讨论。《Unix网络编程》一书中讨论的几种IO模型,我们在开发过程中,讨论最多的应该就是三种: 阻塞IO非阻塞IO以及异步IO

本文试图理清楚几种IO模型的根本性区别,同时分析了为什么在Linux网络编程中最好要用非阻塞式IO?

网络IO概念准备

在讨论网络IO之前,一定要有一个概念上的准备前提: 不要用操作磁盘文件的经验去看待网络IO。  具体的原因我们在下文中会介绍到。

相比于传统的网络IO来说,一个普通的文件描述符的操作可以分为两部分。以read为例,我们利用read函数从socket中同步阻塞的读取数据,整个流程如下所示:

read调用示意图
  1. 调用read后,该调用会转入内核调用
  2. 内核会等待该socket的可读事件,直到远程向socket发送了数据。可读事件成立(这里还需要满足TCP的低水位条件,但是不做太详细的讨论)
  3. 数据包到达内核,接着内核将数据拷贝到用户进程中,也就是read函数指定的buffer参数中。至此,read调用结束。

可以看到除了转入内核调用,与传统的磁盘IO不同的是,网络IO的读写大致可以分为两个阶段:

  1. 等待阶段:等待socket的可读或者可写事件成立
  2. 拷贝数据阶段:将数据从内核拷贝到用户进程,或者从用户进程拷贝到内核中,

三种IO模型的区别

我们日常开发遇到最多的三种IO模型分别是:同步阻塞IO、同步非阻塞IO、异步IO。

这些名词非常容易混淆,为什么一个IO会有两个限定词:同步和阻塞?同步和阻塞分别代表什么意思?

简单来说:

  1. 等待阻塞: 在socket操作的第一个阶段,也就是用户等待socket可读可写事件成立的这个阶段。如果一直等待下去,直到成立后,才进行下个阶段,则称为阻塞式IO;如果发现socket非可读可写状态,则直接返回,不等待,也不进行下个阶段,则称为非阻塞式IO。
  2. 拷贝同步:从内核拷贝到用户空间的这个阶段,如果直到从开始拷贝直到拷贝结束,read函数才返回,则称为同步IO。如果在调用read的时候就直接返回了,等到数据拷贝结束,才通过某种方式(例如回调)通知到用户,这种被称为异步IO。

所谓异步,实际上就是非同步非阻塞。

同步阻塞IO

read(fd, buffer, count)

Linux下面如果直接不对fd进行特殊处理,直接调用read,就是同步阻塞IO。同步阻塞IO的两个阶段都需要等待完成后,read才会返回。

也就是说,如果远程一直没有发送数据,则read一直就不会返回,整个线程就会阻塞到这里了。

同步非阻塞IO

对于同步非阻塞IO来说,如果没有可读可写事件,则直接返回;如果有,则进行第二个阶段,复制数据。

在linux下面,需要使用fcntl将fd变为非阻塞的。

int flags = fcntl(socket, F_GETFL, 0); 
fcntl(socket, F_SETFL, flags | O_NONBLOCK);

同时,如果read的时候,fd不可读,则read调用会触发一个EWOULDBLOCK错误。用户只要检查下errno == EWOULDBLOCK, 即可判断read是否返回正常。

基本在Linux下进行网络编程,非阻塞IO都是不二之选。

异步IO

Linux开发者应该很少使用纯粹的异步IO。因为目前来说,Linux并没有一个完美的异步IO的解决方案。pthread虽然提供了aio的接口,但是这里不做太具体的讨论了。

我们平常接触到的异步IO库或者框架都是在代码层面把操作封装成了异步。但是在具体调用read或者write的时候,一般还是用的非阻塞式IO。

不能用操作磁盘IO的经验看待网络IO

为什么不能用操作磁盘IO的经验看待网络IO。实际上在磁盘IO中,等待阶段是不存在的,因为磁盘文件并不像网络IO那样,需要等待远程传输数据。

所以有的时候,习惯了操作磁盘IO的开发者会无法理解同步阻塞IO的工作过程,无法理解为什么read函数不会返回。

关于磁盘IO与同步非阻塞的讨论,在知乎上有一篇帖子为什么书上说同步非阻塞io在对磁盘io上不起作用? 讨论了这个问题。

为什么在Linux网络编程中最好要用非阻塞式IO?

上文说到,在linux网络编程中,如果使用阻塞式的IO,假如某个fd长期不可读,那么一个线程相应将会被长期阻塞,那么线程资源就会被白白浪费。

那么,如果我们使用IO多路复用例如epoll去监听fd的可读事件呢? 因为如果使用epoll监听了fd的可读事件,在epoll_wait之后调用read,此时fd一定是可读的, 那么此时非阻塞IO相比于阻塞IO的优势不就没了。

实际上,并不是这样的。epoll也必须搭配非阻塞IO使用。

这个帖子为什么 IO 多路复用要搭配非阻塞 IO? 详细讨论了这个问题?

总结来说,原因有二:

  1. fd在read之前有可能会重新进入不可读的状态。要么被其他人读走了(参考惊群问题), 还有可能被内核抛弃了,总的来说,fd因为在read之前,数据被其他方式读走,fd重新变为不可读。此时,用阻塞式IO的read函数就会阻塞整个线程。
  2. epoll只是返回了可读事件,但是并没有返回可以读多少数据量。因此,非阻塞IO的做法是读多次,直到不能读。而阻塞io却只能读一次,因为万一一次就读完了缓冲区所有数据,第二次读的时候,read就会又阻塞了。但是对于epoll的ET模式来说,缓冲区的数据只会在改变的通知一次,如果此次没有消费完,在下次数据到来之前,可读事件再也不会通知了。那么对于只能调用一次read的阻塞式IO来说,未读完的数据就有可能永远读不到了。

因此,在Linux网络编程中最好使用非阻塞式IO。

本文首发于腾讯云+社区,稍后同步于个人博客www.cyhone.com

参考

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏酷玩时刻

H5仿微信支付键盘

视频地址:https://github.com/Javen205/IJPay-Demo/blob/master/doc/pay_keyboard.m4v

1182
来自专栏老安的博客

docker 容积硬盘扩容小坑一个

1333
来自专栏后端技术探索

分布式系统一致性问题解决实战

商户提交表单数据至旺铺(deco项目,以下皆称为deco),deco需要接入poi系统进行装修内容的人工审核,详细流程见下图。

1012
来自专栏张戈的专栏

Linux操作系统DNS解析(nameserver)监控脚本

一、起因 昨天,开发组兄弟发现 resin 日志出现不少支付宝业务报错信息,用户用支付宝购买了产品,钱到账后公司系统却未返回支付成功状态! 这还得了?用户明明支...

3934
来自专栏hbbliyong

运行yum时出现/var/run/yum.pid已被锁定,PID为xxxx的另一个程序正在运行的问题解决

删除文件后再次运行yum可用。 --------------------- 作者:黯淡荣耀 来源:CSDN 原文:https://blog.csdn.ne...

1412
来自专栏ytkah

dedecms自定义表单发布成功后返回当前页面

  dedecms的自定义表单非常的灵活,无论是用户留言、在线报名、信息收集统统都可以通过自定义表单完成。自定义表单发布成功后会跳转到表单列表页,我们又不想让别...

3384
来自专栏彭湖湾的编程世界

【npm】利用npm安装/删除/发布/更新/撤销发布包

什么是npmnpm是javascript的包管理工具,是前端模块化下的一个标志性产物 简单地地说,就是通过npm下载模块,复用已有的代码,提高工作效率 1.从社...

5158
来自专栏Java帮帮-微信公众号-技术文章全总结

Linux查看日志命令【面试+工作】

2484
来自专栏marsggbo

coursera 视频总是缓冲或者无法观看的解决办法

注意!!!该方法针对Windows用户,亲测有效。 1.用管理员权限记事本打开host文件 2.将如下内容复制到文件末尾 52.84.246.90 ...

2105
来自专栏企鹅号快讯

某CMS注入分析及注入点总结

Bluecms是一个地方网站的开源的cms,在很多地方性的网站上应用还是不少的,今天在逛seebug的时候看到了一个漏洞的公告。 ? 有公告但是这里还没有详情,...

1939

扫码关注云+社区