一、 理解 IO
当我们调用系统接口 write
、read
的时候,本质是把数据从用户层写给操作系统,也就是写入到 OS 的发送缓冲区中,或者从 OS 的接收缓冲区中读取数据,所以它们的本质也就是拷贝函数。
那么在这个过程中,调用 write
的时候只有当发送缓冲区中有足够的空间才能进行拷贝,当发送缓冲区没有空间了,此时 write
只能阻塞等待,不能继续拷贝。而调用 read
的时候,只有当接收缓冲区有数据才能进行读取拷贝,当接收缓冲区没有数据了,此时 read
也只能阻塞等待。上面就是 IO 的过程,所以,IO 的过程被分为两个部分:等待和拷贝!
所以在 IO 的过程中,要进行拷贝,必须先判断条件成立,也就是读写事件是否就绪。那么什么叫做高效的 IO 呢?就是在单位时间内,IO 过程中,等的比重越小,IO 的效率越高!
二、认识五种高级 IO 模型
1. 阻塞 IO
在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
阻塞IO是最常见的IO模型,过程如下:
2. 非阻塞IO
非阻塞 IO 就是,如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码。
非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。
3. 信号驱动 IO
内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作。
4. IO 多路转接
IO 多路转接,虽然从流程图上看起来和阻塞 IO 类似,实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
5. 异步 IO
由内核在数据拷贝完成时,通知应用程序。和信号驱动 IO 的区别在于,信号驱动是告诉应用程序何时可以开始拷贝数据。
以上就是五种高级 IO 的模型的简单介绍。任何 IO 过程中,都包含两个步骤:第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。想要让 IO 更高效,最核心的办法就是让等待的时间尽量少。
三、高级 IO 重要概念
1. 阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
其实阻塞IO和非阻塞IO的效率是差不多的,因为 IO = 等待+拷贝,数据好了大家都要拷贝,只是非阻塞IO在等待的时候可以做其他事情,也就是它们之间等的方式不一样,非阻塞IO在进行非阻塞轮询时可以做自己其它的事情,所以这就导致非阻塞IO在效率上稍微高一点。
2. 同步通信和异步通信
同步和异步关注的是消息通信机制。
- 所谓同步IO,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果。也就是参与了IO中的等待或者拷贝的过程,就是同步IO;
- 异步IO则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。也就是说,异步IO不参与IO,只是发起IO,最后拿结果就行。
四、非阻塞 IO
fcntl
我们以前学过 recv()
这样的接口,其中它的参数如下:
我们知道前三个参数是和 read()
的一模一样,而最后一个参数 flag 设为 0 默认就是阻塞等待。而我们可以将这个参数设为 MSG_DONTWAIT
,就是非阻塞IO,如下:
但是这种选项用起来不方便,更通用的做法是使用 fcntl()
接口。我们知道,文件描述符就是一个数组下标,而我们所有的网络通信、文件等等,都是读写文件描述符,而每一个文件描述符指向的都是内核中的文件对象,文件对象是有关于这个文件的 flags 的,也就是它的标记位。所以我们可以通过 fcntl()
接口来直接设置一个文件描述符的属性!其实就是设置其文件对象中的 flags 标志位,告诉内核这个指定的文件描述符要以非阻塞的方式来操作。系统接口如下:
如上,可以按照指定的 cmd 来对指定的文件描述符来进行可变参数部分的设置。
传入的 cmd 的值不同,后面追加的参数也不相同。fcntl 函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
- 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
- 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)
我们尝试将标准输入设置为非阻塞IO的形式,如下代码:
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];
// 设置标准输入为非阻塞IO
SetNonBlock(0);
while(true){
cout << "Please Enter# ";
ssize_t 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{
cerr << "read error, n = " << n << ", errno code: " << errno << ", errstr: " << strerror(errno) << endl;
break;
}
}
return 0;
}</code></pre></div></div><p>上面的代码其实是跑不通的,因为我们根本没有输入数据,因此 n 是小于0的,我们可以通过打印错误信息观察:</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723277113360499888.png" /></div></div></div></figure><p>我们可以看到,错误码的描述大概意思就是临时资源不可用,因为我们在 else 中 <strong>break</strong> 了,我们应该把 <strong>break</strong> 去掉,改为 <strong>sleep(1)</strong>,我们方便观察。</p><p>此时运行后我们在键盘上输入是可以直接回显的,如下:</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723277113646848174.png" /></div></div></div></figure><p>所以,设置为非阻塞,如果底层 <strong>fd</strong> 数据没有就绪,<strong>recv/read/write/send,返回值会以出错的形式返回</strong>。所以出错就分为两种情况了,一种是真的出错了,另一种是底层没有就绪,这种情况就是返回 11 号错误码,也就是 <strong>EWOULDBLOCK</strong>. 那么我们怎么区分呢?可以通过 <strong>errno</strong> 区分!如果 <strong>errno</strong> 为 11,代表底层没就绪!所以我们对代码稍作修改,如下:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0"> 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];
// 设置标准输入为非阻塞IO
SetNonBlock(0);
while(true){
// cout << "Please Enter# ";
ssize_t 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){
// do other thing...
}
else{
cerr << "read error" << endl;
break;
}
//cerr << "read error, n = " << n << ", errno code: " << errno << ", errstr: " << strerror(errno) << endl;
sleep(1);
}
}
return 0;
}</code></pre></div></div>