五种IO模型基本概念
什么是高效的IO?
我们常说的IO,站在体系结构的角度上来看!把数据从内存拷贝到外设就是output,把数据从外设搬到内存就是input!——确实没有错!但是理解还不够!
当我们在网络中,我们通过write进行发送,read进行读取!但是当我们write的时候我们实际不是发送数据!而是将数据拷贝到发送缓冲区里面,所以write的本质就是拷贝!而read进行读取的本质是从TCP的接收缓冲区里面把数据从内核中拷贝到应用层!
可是你想拷贝就要能拷贝呢?
例如:你想write,有没有可能发送缓冲区就已经流量控制的问题,发送缓冲区已经被写满了!没有空间让我们write了!那么此时write操作就会阻塞!直到发送缓冲区空出位置!
如果我们想要read,万一接收缓冲区里面压根就没有数据呢?那么read不就会阻塞住吗?就像在进程间通信,使用管道对方不写入数据!我们读取就会被阻塞! 写网络套接字的时候,如果对方不发送数据!那么服务端就会阻塞卡住!——==read,write的本质是拷贝!而拷贝是有条件的!读取的是要求接收缓冲区有数据!写入的时候要求发送缓冲区有空间!==
==阻塞的本质就是等待某种资源就绪,当操作系统发现接收队列没有数据/发送队列没有空间的时候,就会把进程的PCB放入等待队列里面进行等待!——等待到某种资源就绪,等到有数据之后,就会将数据拷贝,拷贝完毕后就进行返回==
所以我们对于read/recv/write等接口我们不仅要知道本质是拷贝!还要明白是要等待的!
==IO = 等待 + 数据拷贝!==
在系统中,我们对于等待的情况感知并不明确!像是进行文件操作,我们是没有资格知道文件的写入的!只是调用了系统接口!然后系统给我们将数据刷新到文件里面,对于网络,也是一样!也是操作系统来替我们发送的!所以系统和网络在IO的处理上是一致的!
但是在系统上,我们对于等待的情况是不直观的!我们访问一个本地文件,很快就访问完成,写入完成了!看起来就是一瞬间!没有等待的样子!但是其实还是有等待的!
==但对于网络通信,距离长了之后,还有拥塞控制,流量控制的策略之后,等待时间的比重就变得十分的明显了!==
在整个冯诺依曼体系中,IO也是影响整个计算机系效率最重要的一个环节没有之一!
如果一个大型计算机集群,如果吞吐有问题!一定是IO的问题!效率低下也一定跟IO有关!
主要原因也不是因为数据拷贝慢!而是等待的时间长!
那么什么是高效的IO呢?
既然我们已经知道IO = 等待 + 拷贝数据!
那么我们就首先得明白拷贝数据就是一个固定的时间是从硬件到硬件!如果硬件不更换,那么该是多少时间就是多少时间!软件层面是很难进行干涉的!只要我们能保证拷贝的时候不中断!那么其实效率就已经到上限了!(例如磁盘把数据拷贝到内存,上限其实就是磁盘的读写速度!可能有一些优化!但是主要还是取决于硬盘本身!)
所以我们说的高效的IO本质是在说——==减少等待时间的比重!==
就像是read用了1s那么肯定是99%以上的时间都是在等待!只有极少的时间在进行拷贝!那么这种就是不高效的!
如果反过来99%在拷贝,1%时间在等待那么这就是一个高效的IO
==所以所有有关IO类的话题都是围绕着提供拷贝速度,降低等待时间这两点==
五种IO模型概念
我们先用一个例子来引入
钓鱼分几步呢?——不考虑打窝,抛诱饵,撒网之类的
其实就是两步!我们常常看到钓鱼佬都是在哪里等待!然后鱼上钩了钓起来!
所以钓鱼 = 等待 + 钓起
在一个鱼塘,张三就坐在他的位置上,扔下鱼钩后,就死死的盯着那个鱼漂,什么事情都不管!什么人都不理会,就看着那个鱼漂,直到鱼漂动了!然后终于钓上一条鱼上上来!然后重复上述的行为
随后一个李四也来钓鱼了,只不过他还带了一本小说和一部手机过来,李四根张三打招呼,但是张三一直死盯着鱼漂没有反应,于是李四也不自讨没趣,也开始坐下钓鱼
但是李四不像张三,会死死的盯着鱼漂!**只是偶尔抬头看一下,发现鱼漂没有动作!**那么就继续低头看书,或者时不时再看一下手机,直到鱼漂动了,李四就放下手里的手机和书本,拉起鱼竿,钓上来了一条鱼,然后继续重复上面的动作!
在李四看来检查鱼漂和看书,玩手机是不冲突的!
这时候就出现了一个情况张三一直不动死死的盯着鱼漂,李四,一直再动做各种事情!
然后王五也来了钓鱼了,但是王五,比张三,李四都多带了一个东西——铃铛
他把铃铛绑在了鱼竿的顶部,然后扔杆,把鱼竿放在鱼竿支架上,然后就干自己的事情去了!看看小说,和李四聊天,听音乐,整个过程中王五就不去关系自己的鱼竿!
直到王五的铃铛响了!王五就直接把鱼竿拿起来!把鱼钓上来!——王五就使用这种钓鱼方式
过了一会,赵六来了,赵六有钱,他不像前面几个人单人单杆,他带了很多根鱼竿,他条路一个空阔的地方,把每个鱼竿弄上鱼饵,然后一一扔下去,每一个鱼竿都放在鱼竿支架上,放了一大排,然后赵六就来来回回进行遍历检查,看看有哪一个鱼竿的鱼漂动了,就去把鱼钓起来!然后把鱼放在桶里,继续把鱼钩甩进水里,然后继续遍历检测
田七是一个懒人,喜欢吃鱼,但是懒得自己去钓,于是他请了小王,把鱼竿等设备交给他,告诉小王说,你把桶里面装满鱼后,就直接给我打电话,我就去取鱼,然后给你报酬
所以小王就也去鱼塘里面钓鱼了!田七就继续做自己事情。
小王钓了一天的鱼,钓满了,打电话给田七,田七一听就开车去接小王!这样田七就在一定意义上的完成了自己的钓鱼工作!
==如上这就是五种不同的人的钓鱼方式——那么当我们看到什么情况的时候,什么样的人钓鱼的效率是最高的呢?很简单如果一小时只钓上一只鱼,那么钓鱼效率就很低!如果每几分钟就一只那么效率就很高!==
==钓鱼效率高的本质——钓鱼人,等的比重时间低,单位时间,钓鱼的效率就高!==
那么上面五个人中谁的钓鱼效率最高呢?——肯定是赵六!因为其他人都是一人一杆!只有赵六是一人多杆!因为鱼竿越多,鱼咬钩的概率就越高!从旁观者的角度,来看就是赵六更经常的就去把鱼竿拉起来,等的时间的比重就更低!效率更高!
==我们把赵六这种一次性可以等待多个鱼竿的钓鱼方式我们就称之为——多路转接/多路复用!==
==张三的钓鱼方式——就是阻塞式IO==
==李四的钓鱼方式——就是非阻塞式IO==
==王五的钓鱼方式——信号驱动式IO==
==田七只是发起了钓鱼这件事!但是既没有去等,也没有去钓,而是让小王去做!而只有小王准备好了田七才去拿的这种方式——异步IO==
==这些人,就是我们系统中对应的进程/线程!田七的小王其实就是指操作系统!,我们说的鱼其实就是数据,鱼塘就是内核空间,鱼漂就是数据就绪的事情!鱼竿就是文件描述符!==
==钓鱼的动作就是recv/read调用!==
当李四这个进程,它拿着自己的文件描述符钓鱼的时候,只要鱼没有就绪,李四不会因为read/recv的调用阻塞住!read/recv就会立马返回!李四会在自己的while中轮询的同时做其他事情!例如帮用户计算一下数据,管理一下连接什么之类,做一些资源的释放
王五这个进程,在就绪的时候操作系统,会给对应的进程推送==SIGIO信号(需要被设置)==——就是在调用recv之前,注册一下SIGIO信号的方法!然后就继续向下执行!做自己的事情!一旦IO就绪!王五的信号捕抓方法里面就调用recv/read,然后把信号拷贝到用户空间
赵六就是一个进程持有多个文件描述符!
田这个进程就是通过异步IO的接口!直接将数据读取的工作交给操作系统!把任务交给操作系统后!还给操作系统一个缓冲区(装鱼用的桶)和一个通知自己的回调方法或者通知策略!然后当操作系统帮田七读取数据将“桶”放满后,再去使用田七的给操作系统的方法去通知田七!让田七直接使用就可以了!——这就是异步IO
==上面的这几种IO方式我们就称之为五种IO模型!所有的IO都隶属于上面的这五种模型中的一种!==
我们现在最常见的文件接口使用的都是阻塞式IO
但是我们平时最常用的还是阻塞是IO
而五种模型中潜在的比较高效的情况就是==多路转接!==
五种IO模型的差别
阻塞式IO和非阻塞式IO,以及信号驱动式IO在IO的效率上有差别么?——没有!因为并没有减少等待的时间!
非阻塞式IO只是允许我们,能在等待的时候去做一些其他的事情!但是没有减少等待的时间!
==比如一个IO需要100s!阻塞式IO就傻等100s!非阻塞式IO则可以去做一些其他事情!——但是也要等待100s!在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: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用
比如:说当调用recvfrom没有数据准备好直接返回的时候,我们先不着急调用recvfrom先做其他事情,例如说打打日志,进行资源管理,获取新的连接,关闭长期没有活动的连接之类的,然后再去调用recvfrom
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
就是让进程提前对SIGIO信号进行捕抓,注册新方法!
然后当数据准备好的时候操作系统会自动的向该进程抵达信号!
通知进程准备就行吗,然后在SIGIO函数里面使用拷贝函数,将数据拷贝上来!
信号IO虽然看起来很优雅,但是其实用的并不多!——因为SIGIO是一个普通信号!当多个数据同时就绪的时候!可能会出现数据丢失的问题!
==上面我们说的三种模型调用拷贝函数都是身兼多职的!既要有等待也要有拷贝!==
IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件 描述符的就绪状态
==像是read/recv/recvfrom这样的接口传参的时候只能传入一个文件描述符!就注定了只能等一个文件描述符!多路转接的原理就要要等待多个文件描述符!——所以操作系统就必须单独提供其他的接口用于支持这些功能!——select/poll/epoll==
而向select/poll/epoll的接口,不像上面的拷贝函数一样,身兼两职,同时负责拷贝和等待!——==select/poll/epoll的接口只负责等待那一步!==
==select/poll/epoll只负责检查是否就绪,一旦数据准备好了select/poll/epoll是不具备拷贝能力的!也不需要拷贝能力!当数据就绪的时候,我们只需要调用一次或者多次recv/recvfrom/read的接口,就可以把数据从内核拷贝到用户空间!因为等待的过程已经被完成!所以这些接口也就不用阻塞等待了!直接拷贝即可,这样子IO的过程就被分成了两步!一个只负责等待!另一个只负责拷贝!==
==其实如果我们系统里面有10个文件描述符,我们创建10个线程一个线程等待一个和多路转接的方案同时等待10个文件描述符,IO就绪的概率是一样的!也就是说效率是一样的!但是多进程多线程方案的成本更高!而多路转接将所有的IO情况收拢在一起!我们就可以用一个多路转接技术,将所有的文件描述符管理起来!这样成本就十分的低!而且只需要一个执行流就能办到等待多个文件描述符!==
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
aio_read这个接口,只要传用户进程的用户级缓冲区和数据就绪的通知方法!
只要调用了这个接口就会返回——虽然接口名字叫做aio_read但是根本不是读取!只是发起了IO,告诉操作系统,请帮我读取一下特定文件描述符下的数据!把数据读好后放入传入了缓冲区里面!然后通过传入的方法告诉我!所以就有操作系统自己来等待数据!由操作系统自己来拷贝到传入的缓冲区!——而进程本身是不会参与进入这个过程的!
总结
==任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝.==
==而且在实际的应用场景中, 等待消耗的时间往 往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少==
高级IO重要概念
同步通信 vs 异步通信
同步和异步关注的是消息通信机制.
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得 到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
多进程多线程也有同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概 念.
进程/线程同步也是进程/线程之间直接的制约关系
是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、 传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在==等待调用结果(消息,返回值)时的状态.==
阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射 IO(mmap,这个可以去了解),这些统称为高级IO
非阻塞IO
fcntl函数
一个文件描述符, 默认都是阻塞IO——所以想要非阻塞IO,我们首先就要将文件描述符设置为非阻塞的!
在我们使用read/send之类接口的时候,都一个一个flag参数选项!
让我们能以非阻塞的方式进行读写!
我们使用open打开文件的时候,也能直接设置为非阻塞文件
但是这都不是最常用的方式!
==最常用的做法就是直接修改文件描述符属性!——变成非阻塞!==
==我们可以使用fcntl系统调用接口来实现这个操作!!==
fd参数
是指我们要操作的文件的文件描述符!
cmd参数
我们要对该文件描述符进行怎么样的操作!后面还有可变参数
==随着cmd的指的不同,后续要追加的参数也不尽相同!==
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
返回值取决于我们使用的那个参数!
但是出错一律返回-1,且设置错误码!
==我们要使用的就是第三个功能h,获取和设置文件状态标记==
返回一个文件描述符的标志位!后续参数被忽略
根据传入的arg参数设置文件描述符的标志位
在参数中O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_EXCL,O_NOCTTY,O_TRUNC都会被忽略
命令行在只有O_APPEND,O_ASYNC,O_DIRECT,O_NOATIME,O_NONBLOCK的时候标志位可以被修改!
void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL);//首先获取该该文件的文件标志位 //其实就是原来该文件的设置选项! if (fl < 0) { perror("fcntl"); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK);//新的标志位和旧的标志位一起设置进文件描述符里面!这样子这个文件描述符就变成了非阻塞 }
使用轮询的方式进行读取
#include"Util.hpp" #include<cstdio> int main() { while (true) { printf(">>>>> "); fflush(stdout); // 从0号文件描述符(键盘)里面进行读取! char buffer[1024]; ssize_t s = read(0, buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout <<"echo: " << buffer; } else if(s == 0)//读取到文件尾 { std::cout <<"read end" <<std::endl; break; //命令行输入的文件可以用ctrl+ d 来表示文件尾 } else //为负数 { } } return 0; }
==这就是一个阻塞的过程!——如果这是一个套接字的话,对方不把数据通过网络发送也会卡主!==
//Util.hpp #pragma once #include<iostream> #include<unistd.h> #include<cstring> #include<fcntl.h> #include<cerrno> void setNoBlock(int fd) { int fl = fcntl(fd,F_GETFL); if(fl == -1) { std::cerr << "fcntl: " << strerror(errno)<<std::endl; return; } fcntl(fd,F_SETFL,fl|O_NONBLOCK); }
//main.cc #include<cstdio> int main() { setNoBlock(0);//设置为非阻塞 while (true) { printf(">>>>> "); fflush(stdout); sleep(1); // 从0号文件描述符(键盘)里面进行读取! char buffer[1024]; ssize_t s = read(0, buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout <<"echo: " << buffer ; } else if(s == 0)//读取到文件尾 { std::cout <<"read end" <<std::endl; break; } else //为负数 { } } return 0; }
如果不加上sleep
==在非阻塞等下,我们可以看到代码会一直不断的进行死循环!而不会因为我们没有输入数据从而导致阻塞等待!read函数会立马返回!==
那么这有什么用呢?——我们可以在我们代码空闲的时候做一些其他的工作!
//Util.hpp #pragma once #include<iostream> #include<unistd.h> #include<cstring> #include<fcntl.h> #include<functional> void setNoBlock(int fd) { int fl = fcntl(fd,F_GETFL); if(fl == -1) { std::cerr << "fcntl: " << strerror(errno)<<std::endl; return; } fcntl(fd,F_SETFL,fl|O_NONBLOCK); } using func_t = std::function<void()>; void printlog() { std::cout << "this is a log" <<std::endl; } void download() { std::cout << "this is a download" <<std::endl; } void executeSql() { std::cout << "this is a executeSql" <<std::endl; }
//main.cc #include"Util.hpp" #include<cstdio> #include<vector> #define INIT(v) do{ \ v.push_back(printlog);\ v.push_back(download);\ v.push_back(executeSql);\ }while(0) #define EXEX_OTHER(v) do{ \ for(auto const & cb:v) cb();\ }while(0) int main() { std::vector<func_t> callback; INIT(callback); setNoBlock(0);//设置为非阻塞 while (true) { printf(">>>>> "); fflush(stdout); sleep(1); // 从0号文件描述符(键盘)里面进行读取! char buffer[1024]; ssize_t s = read(0, buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout <<"echo: " << buffer ; } else if(s == 0)//读取到文件尾 { std::cout <<"read end" <<std::endl; break; } else //为负数 { } EXEX_OTHER(callback); } return 0; }
==非阻塞的时候对应的返回值是什么呢?==
#include"Util.hpp" #include<cstdio> #include<vector> int main() { INIT(callback); setNoBlock(0);//设置为非阻塞 while (true) { printf(">>>>> "); fflush(stdout); sleep(1); // 从0号文件描述符(键盘)里面进行读取! char buffer[1024]; ssize_t s = read(0, buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout <<"echo: " << buffer ; } else if(s == 0)//读取到文件尾 { std::cout <<"read end" <<std::endl; break; } else //为负数 { std::cout << "result: " << s <<std::endl; } } return 0; }
==我们可以看到返回值就是-1==
为什么是-1呢?read函数的返回值对于错误的处理就是-1!
那么当我们不输入的时候——即底层没有数据,算错误么?
==不算是错误!只不过以错误的形式返回了!因为 >0 和 等于0的含义都被占用了!==
==那么我们如何区别是真的错了!还是底层没有数据呢?此时单纯返回值是无法区分的!==
==但是read对于错误的处理除了返回-1,还会设置错误码!我们可以查看错误码!==
std::cout << "result: " << s << " "<< "errno: "<< errno<< " " <<strerror(errno)<<std::endl;
==这个错误码是11就是表达底层资源没有就绪!——我们可以用strerror打印出来就是资源暂时不可用!==
==但是我们总不能直接在代码里面写数字11,这样属于硬编码不好!所以read里面早就给我们提供了关于各种错误的宏!==
**我们可以看到第一个EAGAIN(Error Again)宏——就是文件描述符指向的是一个文件而不是套接字!且被设置为非阻塞,当读取操作本来应该是被阻塞的时候,就会立刻返回,同时设置这个错误码! **
同时还有EWOULDBLOCK(Operation Would Block)表示的也是同样的原因!
EINTR(Interrupted System Call)这个错误码是指,我们读取到一半的时候,被信号中断了!但是这个也不算是错误!
std::cout << "result: " << s << " "<< "errno: "<< errno<< " " <<strerror(errno)<<std::endl; std::cout << "EAGAIN " << EAGAIN << std::endl; std::cout << "EWOULDBLOCK " <<EWOULDBLOCK<<std::endl;
==所以其实我们要在加一些判断!==
#include"Util.hpp" #include<cstdio> #include<vector> #define INIT(v) do{ \ v.push_back(printlog);\ v.push_back(download);\ v.push_back(executeSql);\ }while(0) #define EXEX_OTHER(v) do{ \ for(auto const & cb:v) cb();\ }while(0) int main() { std::vector<func_t> callback; INIT(callback); setNoBlock(0);//设置为非阻塞 while (true) { printf(">>>>> "); fflush(stdout); sleep(1); // 从0号文件描述符(键盘)里面进行读取! char buffer[1024]; ssize_t s = read(0, buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout <<"echo: " << buffer ; } else if(s == 0)//读取到文件尾 { std::cout <<"read end" <<std::endl; break; } else //为负数 { if(errno == EAGAIN || errno == EWOULDBLOCK) { std::cout << "no reeor!no data!" <<std::endl; EXEX_OTHER(callback); } else if(errno == EINTR) { continue;//继续重新读取 }//除了这两种剩下大部分都是报错! else//真正出错了 { std::cout << "result: " << s << " " << "errno: " << errno << " " << strerror(errno) << std::endl; break; } } } return 0; }
==这才是非阻塞的正确写法!==