【linux】高级IO,认识五种IO模型,非阻塞IO的实现,fcntl
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
- 前言
- 一、前置知识
- 二、五种IO模型
- 三、深入认识五种IO模型
- 阻塞IO
- 非阻塞IO
- 信号驱动IO
- 多路转接
- 异步IO
- 四、非阻塞IO的实现
- fcntl
- 总结
前言
【linux】网络基础(十八)DNS协议,ICMP协议,NAT技术,内网穿透,代理服务器——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】高级IO,认识五种IO模型,非阻塞IO的实现,fcntl
一、前置知识
- 在正式学习高级IO之前,打好基础是一定的,即我们需要预先学习基础IO才能更好的学习高级IO,关于基础IO的讲解,如下
【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
【linux】linux基础IO(三)(用户缓冲区概念与深刻理解)
【linux】linux基础IO(四)(模拟实现c语言文件标准库fopen,fclose,fwrite,fflush)
【linux】linux基础IO(五)深入理解文件系统
【linux】linux基础IO(六)软硬链接(软链接,硬链接)
【linux】linux基础IO(七)静态库的制作与使用
【linux】linux基础IO(八)动态库的制作与使用
【linux】linux基础IO(九)动态库是如何被加载的 - IO之前我们的通俗理解是文件,或者说是网络通信,例如向文件中进行写入,从文件中读取数据,网络通信的本质也是IO,将数据从计算机写到网卡上,将网卡中的数据读取到计算机上,IO的英文也叫做Input&&Output,即输入和输出,那么对应系统调用则是输入对应read,输出对应write

- 根据我们之前的理解应read&&write是拷贝函数,read将数据从用户层拷贝给OS,write将数据从OS拷贝到应用层,此时小编要根据这个引入点新的理解了, IO对应的系统调用可以是read和write
- 那么如上图,当我们在应用层想要输入read读取数据的时候,是将操作系统的tcp接收缓冲区的数据拷贝到用户层的缓冲区中,可是如果此时tcp接收缓冲区中没有数据呢?那么此时read就会阻塞,直到tcp接收缓冲区有数据才可以将tcp接收缓冲区的数据拷贝到用户层的缓冲区,所以当read阻塞的时候read就是在等待tcp接收缓冲区有数据
- 同样的道理,当我们在应用层想要输出write写数据的时候,是将用户层缓冲区的数据写到操作系统的tcp的发送缓冲区中,可是如果此时tcp发送缓冲区满了没有空间了呢?那么此时write就会阻塞,直到tcp发送缓冲区有空间,所以此时write就会阻塞,直到tcp发送缓冲区有空间才可以将用户层的数据写到操作系统的tcp发送缓冲区中,由操作系统决定什么时候发送,发多少,出错了怎么办,所以当write阻塞的时候write就是在等待tcp发送缓冲区有数据
- 进行IO对应的系统调用可以是read和write,所以IO不仅仅是单纯的拷贝数据,IO还有等待过程,所以小编此时给IO下一个定义,IO = 等待 + 拷贝,那么对于这个等待也是有条件的,例如read读取的时候要求tcp接收缓冲区不为空,write写的时候要求tcp发送缓冲区不为满,否则如果不满足条件,那么IO对应的系统调用read或write就会阻塞等待
- IO = 等待 + 拷贝,所以要进行拷贝,必须先判断条件成立,我们把这个条件叫做读写事件,所以呢?IO = 等待 + 拷贝,什么才是高效的IO呢?
- 单位时间内,拷贝的数据越多那么IO的效率越高效,可是究竟拷贝多少的数据才算高效呢?我们不好衡量,所以此时我们正难则反一下,既然IO = 等待 + 拷贝,所以高效的IO = 单位时间内,IO过程中,等待的比重越小,IO的效率就越高
- 有了上面的结论,那么几乎所有的提高IO效率的策略就是让等待的比重尽可能的变小,例如,在网络中,单线程等待一个文件描述符进行读写的IO效率较低,那么使用多线程等待多个文件描述符进行读写的IO效率就会变高,为什么?
- 因为多线程等待多个文件描述符的话,单位时间内,等待的时间就会变少,如果让单线程来进行等待单个文件描述符,那么等待的时间就只能是串行的,而让多线程来等待多个文件描述符,那么此时等待的时间就是并行的,有重合的,所以多线程等待多个文件描述符,在单位时间内,等待的时间就会变少,所以单位时间内既然等待的时间变少了,那么拷贝数据的时间就会变多,所以IO的效率自然也就会被提高
- 如果就是认识五种IO模型的前置性知识的讲解,下面我们来认识一下五种IO模型
二、五种IO模型
- IO分为五种,即五种IO模型,既然都是模型了,所以也就意味着无论是何种方式进行的IO都无法逃脱这五种IO模型的范畴,为了让大家更好的理解五种IO模型,下面小编以钓鱼的例子为场景引入五种IO模型,我们知道关于钓鱼,一般情况下要找个位置,鱼竿上放上鱼漂鱼饵之后,所以此时就开始进行钓鱼的过程了,然后陷入进行等待直到鱼漂上下浮动了之后,然后一拉鱼竿将鱼钓上来,所以既然IO = 等待 + 拷贝,同样的我们也可以将IO简单的替换理解为钓鱼,钓鱼 = 等待 + 钓
- 张三是一个钓鱼新手,他拿着钓鱼的装备,去岸边钓鱼,鱼竿上放上鱼漂鱼饵之后,所以此时张三就开始钓鱼了,张三就目不转睛的盯着鱼竿上的鱼漂有没有上下浮动,来了电话张三也忽略,那么张三等待观察许久终于鱼漂开始上下浮动了,所以此时张三拿起鱼竿一提就将鱼钓上来了
- 所以张三这种钓鱼方式是一旦开始钓鱼,那么就目不转睛的盯着鱼漂,观察等待鱼漂上下浮动,在这个过程中张三没有做其它事情,即使如上的电话打给张三,张三也忽略,所以这种钓鱼方式我们成为阻塞式钓鱼,由于IO可以理解为钓鱼,所以第一种IO模型,即阻塞式IO
- 李四是一个有2年钓龄的钓友,他拿着钓鱼的装备去岸边钓鱼,发现了张三,然后喊一下张三,但是此时张三没有理他,别忘了此时张三是阻塞式钓鱼,所以对于除了观察鱼漂等待鱼漂上下浮动的事情张三都会忽略,此时李四想了一下,好吧,你不理我我就在你旁边钓鱼吧
- 所以此时李四在鱼竿上放上鱼漂鱼饵之后,李四也开始等待鱼漂上下浮动,但是李四还拿了书,所以此时他打开书开始看书,那么每隔一会他就观察一下鱼漂有没有上下浮动,如果鱼漂没有上下浮动,那么李四就继续看书,如果鱼漂上下浮动了,那么李四就拉杆把鱼钓上来
- 所以李四看一会儿书,然后观察一下鱼漂,看一会儿书,观察一会儿鱼漂,终于看完书之后,再观察鱼漂,鱼漂此时上下浮动了,所以此时李四就拉杆,也成功的把鱼钓上来了,所以李四这种钓鱼方式是非阻塞式钓鱼,由于IO可以理解为钓鱼,所以第二种IO方式,即非阻塞式IO,也叫做非阻塞轮询
- 王五是一个有5年钓龄的钓友,他拿着钓鱼的装备去岸边钓鱼,那么王五在鱼竿上放上鱼漂鱼饵之后,特别的,王五带了一个独特的铃铛到鱼漂上,只要鱼漂上下浮动,那么铃铛就会响,所以王五也拿了一本书,那么他就不需要像李四一样,间隔一会儿就看一下鱼漂有没有上下浮动
- 王五开始钓鱼后,王五可以一直看书,十分的悠闲,只要铃铛不响,那么就代表没有鱼咬钩,那么王五就开始悠闲的看书了,看了一会儿之后,突然,铃铛响了,所以此时王五就拉杆,成功将鱼钓上来了,所以铃铛的响声对于王五来讲是一种信号,当王五听到这种信号之后已经有了特定的动作,即王五就要拉杆,将鱼钓上来,所以王五这种钓鱼方式是信号驱动式钓鱼,由于IO可以理解为钓鱼,所以第三种钓鱼方式,即信号驱动式IO
- 赵六是县里的首富,赵六也喜欢钓鱼,赵六比较有钱,所以赵六买了100根鱼竿以及对应的装备,所以赵六在鱼竿上放上鱼漂鱼饵之后,赵六也开始钓鱼了,所以赵六就在岸边遍历鱼竿,依次观察鱼漂有没有上下浮动,所以很快,赵六观察到了鱼漂上下浮动了,所以赵六一拉杆,将鱼钓上来了
- 赵六和张三,李四,王五都不同,赵六使用的是100根鱼竿,张三,李四,王五使用的都是一根鱼竿,那么假设一个鱼在一定时间内咬钩的概率是1/1000,所以张三,李四,王五在一定时间内上鱼的概率是1/1000,那么由于赵六使用的是100根鱼竿,所以赵六在一定时间内上鱼的概率是100 * (1/1000) = 1 / 10
- 相对于张三,李四,王五使用的都是一根鱼竿上鱼等待时间是串行的,赵六使用的100根鱼杆上鱼的时间是并发的,是重合的,所以也就注定了赵六的等待时间会变少,赵六的等待时间在整个钓鱼的过程中比重减少了,所以赵六的钓鱼效率就更高,IO也是类似的道理,所以赵六的钓鱼方式叫做多路复用式钓鱼,也叫做多路转接式钓鱼,IO可以理解为钓鱼,所以第五种IO模型,即多路复用,也叫做多路转接
- 田七是整个市的首富,田七是一个公司的大老板,所以注定了田七有很多商务要忙,但是田七喜欢吃鱼,同样的田七也稍微学了一下钓鱼,田七略微喜欢钓鱼,田七有一个司机叫做小王,田七跟小王讲,走,拿着钓鱼的装备放到车上,小王你开着车带着我去钓鱼
- 所以小王就拿着钓鱼的装备放到了车上,开着车去往岸边钓鱼,到了位置之后,田七正要下车,此时田七的公司来电话了,需要田七这个大老板去开一个公司里的会议,田七想了一下,好吧,感觉也不是那么喜欢钓鱼了,我田七只是喜欢吃鱼,所以田七告诉小王,小王呀,你拿着钓鱼的装备,去岸边钓鱼吧,当你钓完鱼之后打电话通知我,我来接你,车我开走了
- 所以此时小王一想,不错呀,上班还可以钓鱼休闲,所以小王就直接答应了老板田七,所以田七开着车走了,小王也是一个钓鱼高手,所以经过几个小时,钓箱已经被掉满了,并且小王也估摸着老板田七的会议应该也差不多开完了,于是小王就给老板田七打电话,老板鱼我已经钓好了,老板说,好的,正好我的会议也开完了,我开着车来接你
- 所以老板田七开着车来了见到了小王,老板田七说,干得不错小王,满满一钓箱都钓满了,所以老板田七就获得了满满一钓箱的鱼,正好大快朵颐吃好多的鱼了,在这个过程过程中,田七没有参与钓鱼的过程,但是收获了满满一钓箱的鱼,所以在这个过程中,田七只是钓鱼行为的发起者,让小王来帮我钓鱼,钓鱼的结果给我田七,田七要的只是鱼
- 所以田七的钓鱼方式叫做异步钓鱼,由于IO可以理解为钓鱼,所以第五种IO模型,即异步IO,此时我们可以想一下,类似的,如果用户想要进行异步IO,那么这里的用户就相当于田七,那么用户告诉操作系统,你帮我进行等待,完成数据的拷贝,拷贝完成后,通知我一下就可以了,所以操作系统就相当于小王
- 既然有异步IO,类似的,同步IO也应该有,同步IO就是前四种模型,即同步IO有阻塞式IO,非阻塞式IO,信号驱动式IO,多路复用(多路转接),所以异步IO和同步IO的区别是什么呢?核心区别在于有没有参与IO的过程
- 对于异步IO来讲,田七只是让小王去钓鱼,田七本身并不参与钓鱼过程,田七只是拿小王钓鱼后满满一钓箱的鱼,异步IO仅仅是发起IO,不参与IO过程,异步IO只需要最后拿IO的结果就可以,同步IO诸如有阻塞式IO,非阻塞式IO,信号驱动式IO,多路复用(多路转接),都需要亲自参与IO的过程,只要进行等待了,那么就是参与了IO的过程,诸如张三,李四,王五,赵六都进行了等待,只不过各自等待的方式不同
- 张三是一直进行等待检查鱼漂有没有上下浮动,李四看一会儿书,然后检查鱼漂有没有上下浮动,王五是看一会书,看书也是一种等待呀,等待的是鱼咬钩,赵六是遍历来回检查检查鱼漂有没有上下浮动也是一种等待,等待鱼漂上下浮动
- 钓鱼 = 等待 + 钓,所以只要等待发现鱼漂上下浮动,那么一拉杆,将鱼钓上来,也是参与了钓鱼的过程,所以张三,李四,王五,赵六参与了钓鱼的过程,IO可以理解为钓鱼,所以阻塞式IO,非阻塞式IO,信号驱动式IO,多路复用(多路转接)参与了IO过程,所以阻塞式IO,非阻塞式IO,信号驱动式IO,多路复用(多路转接)是同步IO
- 阻塞式IO和非阻塞式IO有什么区别呢?IO = 等待 + 拷贝,无论是阻塞式IO还是非阻塞式IO都要进行等待和拷贝,拿钓鱼为例,张三是阻塞式钓鱼,张三会一直等待盯着鱼漂有没有上下浮动,李四是非阻塞式钓鱼,李四会看书,间隔一会,然后观察鱼漂有没有上下浮动,李四看书是等待是方式
- 无论是张三还是李四在看到鱼漂进行了上下浮动之后都会拉杆然后将鱼钓上来,钓鱼 = 等待 + 钓,对于钓来讲都是相同的,所以阻塞式钓鱼和非阻塞式钓鱼的核心区别在于等待的方式不同,IO可以理解为钓鱼,所以阻塞式IO和非阻塞式IO的核心区别在于等待的方式不同,阻塞式IO当读写事件不满足的时候会一直阻塞在调用的系统调用中
- 非阻塞式IO当读写事件不满足的时候,则会立即返回,然后可以做一些其它的事情,例如检测状态,打印日志等,所以对于非阻塞式IO来讲与其阻塞式的等待,我非阻塞式IO直接返回去做其它一些事情,但是虽然这里非阻塞式IO去做了其它的事情,但是非阻塞式IO也进行了等待,只不过等待读写事件就绪的方式是去做一些其它的事情,当做完了其它事情之后,就会再轮询的去系统调用中看一下读写事件是否满足,如果满足了那么就进行拷贝数据,否则继续直接返回然后去做其它事情,这样重复
- 小编,小编我记得之前还学过线程同步,那么线程同步和这里的同步IO有关系吗?没有关系,就像老婆和老婆饼一样,没有关系,线程同步是指两个线程在某些条件下两个线程谁先执行,谁后执行,而这里的同步IO是指参与了IO的过程,所以两个毫无关系,两者只是不同的领域恰好使用了相同的同步这个名词而已
- 所以5种IO模型,分别是阻塞式IO,非阻塞式IO,信号驱动式IO,多路复用(多路转接),异步IO,那么由于这是模型,所以所有的IO方式都逃脱不开这5种IO模型的范畴,即所有的IO方式都是着5种IO模型的一种,那么其中最值得我们学习的,效率最高的IO模型是什么呢?
- 有的读者友友可能会想,应该是异步IO吧,自己不用做事情,交给其它人做事情效率多高,实则不然,在钓鱼的例子中,虽然田七让小王钓鱼,但是小王钓鱼的装备也只是一套,所以效率没有提高,并且在实际使用中,异步IO这种方式编写出来的代码逻辑一般都比较混乱,不易维护
- 所以对于我们来讲,最值得我们学习的,效率最高的IO模型是多路复用(多路转接),多路复用也叫做多路转接,为什么效率最高呢?因为多路复用可以做到并发等待,让等待的时间重合,所以等待的时间在IO过程中的比重就会减小,所以IO的效率就会越高
- 所以多路复用是最值得我们学习的,效率最高的IO模型,那么在正式学习多路复用之前,我们还需要前置知识的学习,这个前置知识的学习就是非阻塞式IO,所以我们会在学习多路复用之前先学习非阻塞式IO,但是我们使用钓鱼的例子帮助大家认识了五种IO模型,但是这五种IO模型实际上了解的还不够,下面我们详细的再来深入认识五种IO模型
三、深入认识五种IO模型
阻塞IO
- 阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有套接字,默认都是阻塞方式

- 进程运行,用户在应用层调用recvfrom从内核的tcp接收缓冲区中读数据,如果此时tcp接收缓冲区中没有数据,那么就会阻塞在系统调用中,进程层面表现为进程被阻塞挂起,即进程的PCB从运行队列中拿下来,将PCB放到等待队列中,那么此时系统调用就会等待数据
- 那么当内核准备好数据时候,即此时tcp接收缓冲区有数据了,那么此时就会将进程唤醒,然后recvfrom这个系统调用将数据从tcp接收缓冲区中拷贝到用户层缓冲区中,拷贝完成之后,recvfrom返回
非阻塞IO
- 非阻塞IO需要程序员以循环的方式反复尝试读取文件描述符,这个过程称为轮询,非阻塞IO也称为非阻塞轮询,但是一昧的循环十分消耗CPU资源,所以一般非阻塞IO在返回之后可以搭配去做其它的事情,在第六点的非阻塞轮询进行的讲解,详情请点击<——

- 进程运行,那么通常默认系统调用接口是阻塞式调用,那么如果想要以非阻塞方式IO,那么需要进行设置,最常用的是使用fcntl将文件描述符设置为非阻塞,然后以read读取数据就是以非阻塞形式IO了,这个最常用的方式小编会在非阻塞IO的实现中进行讲解使用
- 那么上图的方式是通过参数以非阻塞的形式调用recvfrom,并且搭配循环,就可以实现非阻塞IO,所以此时以非阻塞轮询的方式调用recvfrom然后去查看读事件是否就绪,内核中的数据没有准备好,即此时的内核的tcp接收缓冲区中没有数据,所以由于是非阻塞轮询,所以recvfrom就会直接返回,错误码errno设置为EWOULDBLOCK
- 那么返回上层后,可以去做其它的事情,别忘了小编在钓鱼的例子中讲解的李四看书的同时也是一种等待,所以这里的做其它事情也是一种等待,那么做完其它事情之后,然后再继续以非阻塞的形式调用recvfrom,以此循环,直到内核中tcp接收缓冲区中有数据了,那么此时recvfrom就会将数据从tcp接收缓冲区中拷贝到应用层缓冲区中,然后返回,可以去做其它的事情,那么做完其它事情之后,然后再继续以非阻塞的形式调用recvfrom,以此循环
信号驱动IO
- 信号驱动IO,当内核将数据准备好的时候,通过SIGIO信号通知应用程序进行IO操作

- 进程运行,用户先调用sigaction当进程收到SIGIO信号的时候执行信号处理函数,信号处理函数中应该设置调用recvfrom将数据读取上来,所以此时进程可以做自己的事情,做自己的事情也是一种等待,等待系统给我进程发信号,那么当内核的tcp接收缓冲区有数据了,内核就会给进程发送SIGIO信号,直到收到SIGIO信号
- 那么当进程收到SIGIO信号的时候执行信号处理函数,所以在信号处理函数中就会调用recvfrom将数据读取上来,即将数据从内核的tcp接收缓冲区拷贝到用户层缓冲区中,然后recvfrom返回成功即可
多路转接
- 多路转接的核心在于可以同时等待多个文件描述符的就绪状态

- 我们知道IO = 等待 + 拷贝,那么诸如阻塞IO方式进行IO的过程,其实拷贝数据只占IO过程的1/1000,其余的时间都在等待,那么我们如果可以同时等待多个文件描述符,那么就可以实现并发等待,即让等待的时间重合,所以等待的比重在IO过程中就会变少,进而IO的效率也就会变高
- 所以这里如何实现同时等待多个文件描述符呢?那么我们可以使用系统调用select,select专门用于等待多个文件描述符的就绪状态,所以进程运行,那么就可以使用select同时等待多个文件描述符,如果select等待的多个文件描述符没有一个文件描述符就绪那么select就会阻塞直到有文件描述符就绪
- 那么如何理解文件描述符就绪呢?拿网络来讲,其实就是文件描述符对应的tcp接收缓冲区中有数据了,所以此时select就会返回告诉上层现在这个文件描述符已经就绪了,那么你过来读取数据吧,当你过来读的时候不需要等待,直接就可以读取
- 所以上层就调用recvfrom去文件描述符中的tcp接收缓冲区看了一下,果真有数据,所以recvfrom就将tcp接收缓冲区的数据拷贝到应用层缓冲区中,然后recvfrom返回成功
异步IO
- 异步IO,内核将数据拷贝完成的时候,通知应用层数据已经拷贝完成了,这里的异步IO和信号驱动式IO需要区分开,信号驱动式IO是通知应用层去拷贝数据,所以如何区分?核心在于拷贝数据的工作谁来做,异步IO是让操作系统去做,信号驱动式IO是让用户自己调用系统调用去做

- 进程启动,用户调用aio_read发起异步IO,即告诉操作系统,等待和拷贝的过程你来做,做好了之后通知我就行,那么此时操作系统就去tcp的接收缓冲区去看了,如果没有数据,那么操作系统就会等待
- 当有数据了之后,操作系统就会调用系统调用将数据从内核的tcp接收缓冲区中拷贝到应用层缓冲区中,当然对于这个应用层缓冲区需要用户在调用aid_read的时候通过参数交给操作系统,那么当数据拷贝完成,那么操作系统就会发起信号告诉用户,IO过程已经完成,数据已经拷贝到你所指定的用户层缓冲区中了,所以用户接下来只需要处理处于用户层缓冲区的数据即可
四、非阻塞IO的实现
- 比较常用的方式如果想要实现非阻塞IO,那么需要对文件描述符进行设置,最常用的是使用fcntl将文件描述符的读写方式设置为非阻塞,然后以read读取数据就是以非阻塞形式IO了
- 文件描述符默认是以阻塞方式进行读写,所以如果我们采用循环加read那么默认就是阻塞,那么对于非阻塞来讲只需要使用fcntl将文件的读写方式设置为非阻塞,那么我们就可以直接实现非阻塞IO
- 下面我们先来实现阻塞的read从标准输入中读取数据,那么首先我们创建一个用户级缓冲区buffer,然后紧接着就是一个while死循环,循环内,先打印字段让用户输入,由于打印的字段没有换行,并且这些字段也不足以打满标准输出,所以如果此时标准输出的缓冲区并不会被刷新,即字段并不会被打印,所以这时候我们再调用一下fflush冲刷标准输出stdout流的缓冲区即可让字段显示
- 紧接着我们调用read从标准输入0号文件描述符中读取数据,默认是阻塞式IO,那么依次传入文件描述符0,缓冲区,缓冲区的大小减一,因为我们要将读取上来的数据当做字符串处理,所以如果出现用户级缓冲区被写满,那么也可以在末尾预留出一个字节的空间放’ ’
- 如果read的返回值大于0,那么代表数据被读取上来了,所以我们就在末尾放0即可,0即对应’ ’,然后打印出数据即可,如果read的返回值等于0,那么代表读取到了文件末尾,如果在网络中代表对方的写端被关闭,接下来跳出循环即可,如果read的返回值小于0表示读取错误,例如文件描述符是错误的等,那么使用标准错误cerr打印错误信息,接下来跳出循环即可
#include
#include
#include
using namespace std;
int main()
{
char buffer[1024];
sleep(1);
while(true)
{
printf("Please Enter# ");
fflush(stdout);
int n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
break;
}
else
{
cerr << "read error" << endl;
break;
}
}
return 0;
}
运行结果如下
- 那么如上,当程序运行,当打印出字段Please Enter#,提示进行输入的时候,由于是阻塞式IO,并且前三秒小编并没有任何输入,所以read读取的底层的标准输入的缓冲区中并没有任何数据,所以进程会被挂起阻塞,那么当小编进行输入的时候,标准输入的缓冲区中才被放入从键盘上获取的数据,此时进程被唤醒,read将标准输入的缓冲区的数据拷贝到用户层,然后在用户层我们将这个数据打印出来回显在屏幕上了
- 那么根据之前的理论,我们只需要将标准输入对应的文件描述符0使用fcntl设置成非阻塞即可进行非阻塞IO,所以下面我们先来学习一下fcntl
fcntl

- 那么我们来看一下fcntl,首先第一个参数的要传入文件描述符,那么这里我们填写标准输入的文件描述符0即可,第二个参数是cmd命令,然后紧跟着的是一个…可变参数(可变参数可以没有,也可以为1个至多个),那么对于fcntl的命令介绍,我们目前了解两个即可,分为是获取文件状态标记F_GETFL和设置文件状态标记F_SETFL
- 所以对于第一个命令cmd = F_GETFL获得文件状态标记,文件状态标记该如何理解呢?那么对于文件描述符来讲,一个文件描述符在打开的时候必然要对应很多的状态,例如以读O_RDONLY方式打开,以写O_WEONLY方式打开等
- 那么这些状态本质上就是宏,所以如果第二个参数是F_GETFL那么可以采用位图的方式进行获取状态,那么操作系统会将这些状态设置在位图中,然后以返回值的形式进行返回这些文件状态
- 那么对于第二个命令cmd = F_SETFL设置文件标记,这个要基于第一个命令cmd = F_GETFL获得文件标记 fl 的前提下进行使用,要设置文件标记,即新增一个文件标记,所以就是在原本的文件标记 fl 的基础上按位或上新增的一个文件标记传入给可变参数即可,因为这些文件标记是以位图的形式存在,关于具体原理请点击后方蓝字链接,第三点的比特位方式传递标志位进行的讲解,详情请点击<——
- 所以此时如果要给文件描述符设置非阻塞,那么应该使用fcntl第二个命令cmd = F_SETFL,那么非阻塞的宏是O_NONBLOCK,所以将 fl 按位或上O_NONBLOCK即可实现,所以此时我们封装一下,将给文件描述符设置非阻塞封装成一个函数SetNonBlock
- 那么首先我们使用fcntl的第二个参数cmd对应的命令F_GETFL从fcntl的返回值中获得文件状态标记fl,由于文件状态标记fl是一个位图,所以fl默认应该是大于等于0的,所以如果fd小于0,那么代表获取文件状态描述失败,此时我们就perror打印错误,然后返回即可
- 接下来我们使用fcntl的第二个参数cmd对应的命令F_SETFL设置文件状态标记,那么应该是在文件描述符对应的文件状态标记fl的基础上进行新增非阻塞O_NONBLOCK这个状态,所以应该是 fl 按位或上O_NONBLOCK即可实现,最后我们打印消息设置文件描述符的非阻塞成功
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
cout << "set " << fd << " nonblock done" << endl;
}

- 其实这里我们看到这个非阻塞的宏O_NONBLOCK,不光在fcntl中可以进行设置,其实在最初open打开文件描述符的时候,我们就可以以非阻塞形式打开,只不过最常用的方式是fcntl设置非阻塞的这种方式
- 那么在我们的代码中新增非阻塞IO,所以我们只需要在原来的基础上进入while死循环之前调用一下SetNonBlock即可,那么此时是非阻塞轮询,所以我们不需要提示用户输入了,那么我们将最开始打印的字段Please Enter#以及后面的fflush冲刷标准输出stdout的缓冲区的代码注释掉即可
#include
#include
#include
#include
using namespace std;
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
cout << "set " << fd << " nonblock done" << endl;
}
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while(true)
{
// printf("Please Enter# ");
// fflush(stdout);
int n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
break;
}
else
{
cerr << "read error" << endl;
break;
}
}
return 0;
}
运行结果如下
- 那么结果如上,很奇怪,为什么程序运行之后,设置了0号文件描述符为非阻塞之后,最开始的时候,小编并没有使用键盘进行输入,所以也就意味着标准输入对应的0号文件描述符的缓冲区中是没有数据的
- 没有数据就没有数据,此时不满足读事件,由于是非阻塞那么就直接返回了,那么由于没有数据,所以自然不可能返回值大于0,因为read的返回值是读取到的字节数的个数,由于此时没有数据所以返回值不可能大于0,那么返回值等于0呢?
- 返回等于0表示读取到文件的结尾,此时由于0号文件描述符的缓冲区中根本没有数据,所以不满足读事件,所以自然不会开始读取文件,进而也就不可能读取到文件的结尾,所以返回值等于0也不可能
- 可是虽然0号文件描述符的缓冲区中根本没有数据,所以不满足读事件,那么就直接返回了-1,表示读取错误了,可是我们觉得仅仅是由于0号文件描述符的缓冲区中根本没有数据,所以不满足读事件不足以直接报错,那么如何进行区分究竟是真正的报错还是事件不就绪呢?这里的事件是读事件不就绪,在其它场景有可能是写事件不就绪

- 那么如上的红色框内关于read的返回值的为-1的解读,噢噢,原来不仅仅是返回值是-1,还一并设置了错误码,所以下面我们将错误码打印一下,并且使用strerror打印一下错误码对应的错误信息
#include
#include
#include
#include
#include
#include
using namespace std;
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
cout << "set " << fd << " nonblock done" << endl;
}
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while(true)
{
// printf("Please Enter# ");
// fflush(stdout);
int n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
break;
}
else
{
cerr << "read error, n = " << n << ", err code: " << errno
<< ", err str: " << strerror(errno) << endl;
break;
}
}
return 0;
}
运行结果如下
- 所以如上,ret的返回值是-1,通过返回值我们无法区分究竟是真的出错还是底层的读事件未就绪,但是我们继续观察,错误码被设置了,错误码是11,表示临时资源不可用,即表示read想要读取0号文件描述符对应的缓冲区,但是此时缓冲区中由于小编没有敲键盘进行输入,所以此时标准输入对应的缓冲区也就不会有数据

- 那么既然此时要读取的0号文件描述符对应的缓冲区中没有数据,即此时读事件未就绪,即表示此时临时资源不可用,所以此时错误码是11也对应上面的EWOULDBOLCK这个宏,恰好是11
- 所以我们将文件描述符设置成非阻塞,那么如果底层文件描述符fd中的缓冲区的数据或者空间没有就绪,read/recv/write/send就会以出错形式返回,出错形式有两种,第一种是真的出错,第二种是底层的读写事件没有就绪,所以我们该如何进行区分呢?

- 根据错误码errno进行区分,所以此时我们再来深入看一下EWOULDBLOCK这个宏,那么它的宏定义是一个宏EAGAIN,那么对于这个宏EAGAIN它的对应的值是11,意义是此时并没有出错,此时是资源未就绪,请再尝试一次,所以EWOULDBLOCK这个宏对应的值是恰好是11,意义和EAGAIN一致
- 所以我们就可以基于非阻塞,然后根据read的返回值n中的小于0的出错的情况中,根据错误码将真的出错和底层读写事件未就绪区分开来,对于读写事件未就绪的情况我们不应该退出循环,而是应该继续非阻塞轮询的去查看读写事件是否就绪,即对应这里是标准输入0号文件描述符对应的缓冲区中的数据是否就绪,如果有数据那么就是就绪,此时就将数据从文件描述符的缓冲区中拷贝到用户层缓冲区中,返回大于0的值,然后打印数据即可
- 如果不是读写事件未就绪的情况,那么就代表此时是真的出错的情况,那么打印错误信息之后,直接退出循环,即退出非阻塞轮询即可,这里如果是读写事件未就绪的情况,那么就打印信息即可,并且要休眠2秒,否则由于这里小编并没有让非阻塞轮询做其它的事情,进而会导致非阻塞轮询的循环速度很快,那么一瞬间就会把屏幕打满,也不利于我们演示,所以这里要休眠2秒
- 并且打印信息的话,为了区分是我们自己输出的还是进程输出的,那么我们在进程打印用户级缓冲区buffer之前加一个echo进行区分
#include
#include
#include
#include
#include
#include
using namespace std;
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
cout << "set " << fd << " nonblock done" << endl;
}
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while(true)
{
// printf("Please Enter# ");
// fflush(stdout);
int n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
cout << "echo: " << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
break;
}
else
{
if(errno == EWOULDBLOCK)
{
cout << "0 fd data not ready, please try again" << endl;
// 由于是非阻塞轮询,所以这里可以去做其它的事情,这里小编就不举例了
sleep(2);
}
else
{
cerr << "read error, n = " << n << ", err code: " << errno
<< ", err str: " << strerror(errno) << endl;
break;
}
}
}
return 0;
}
运行结果如下,无误
- 所以如上,进程运行,如果小编不进行输入,那么非阻塞IO就会循环式的一直去检查有没有底层fd数据是否就绪,接下来小编在键盘上进行输入,所以此时标准输入的0号文件描述符对应的缓冲区就会有数据
- 即此时fd数据就绪,即满足读条件,所以此时read就会将数据从文件描述符的缓冲区拷贝到用户级缓冲区buffer中,那么拷贝完成返回大于0的数据,进而就会将数据以字符串的形式进行打印
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
















