当我们调用系统接口 write
、read
的时候,本质是把数据从用户层写给操作系统,也就是写入到 OS 的发送缓冲区中,或者从 OS 的接收缓冲区中读取数据,所以它们的本质也就是拷贝函数。
那么在这个过程中,调用 write
的时候只有当发送缓冲区中有足够的空间才能进行拷贝,当发送缓冲区没有空间了,此时 write
只能阻塞等待,不能继续拷贝。而调用 read
的时候,只有当接收缓冲区有数据才能进行读取拷贝,当接收缓冲区没有数据了,此时 read
也只能阻塞等待。上面就是 IO 的过程,所以,IO 的过程被分为两个部分:等待和拷贝!
所以在 IO 的过程中,要进行拷贝,必须先判断条件成立,也就是读写事件是否就绪。那么什么叫做高效的 IO 呢?就是在单位时间内,IO 过程中,等的比重越小,IO 的效率越高!
在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
阻塞IO是最常见的IO模型,过程如下:
非阻塞 IO 就是,如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码。
非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。
内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作。
IO 多路转接,虽然从流程图上看起来和阻塞 IO 类似,实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
由内核在数据拷贝完成时,通知应用程序。和信号驱动 IO 的区别在于,信号驱动是告诉应用程序何时可以开始拷贝数据。
以上就是五种高级 IO 的模型的简单介绍。任何 IO 过程中,都包含两个步骤:第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。想要让 IO 更高效,最核心的办法就是让等待的时间尽量少。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
其实阻塞IO和非阻塞IO的效率是差不多的,因为 IO = 等待+拷贝,数据好了大家都要拷贝,只是非阻塞IO在等待的时候可以做其他事情,也就是它们之间等的方式不一样,非阻塞IO在进行非阻塞轮询时可以做自己其它的事情,所以这就导致非阻塞IO在效率上稍微高一点。
同步和异步关注的是消息通信机制。
我们以前学过 recv()
这样的接口,其中它的参数如下:
我们知道前三个参数是和 read()
的一模一样,而最后一个参数 flag 设为 0 默认就是阻塞等待。而我们可以将这个参数设为 MSG_DONTWAIT
,就是非阻塞IO,如下:
但是这种选项用起来不方便,更通用的做法是使用 fcntl()
接口。我们知道,文件描述符就是一个数组下标,而我们所有的网络通信、文件等等,都是读写文件描述符,而每一个文件描述符指向的都是内核中的文件对象,文件对象是有关于这个文件的 flags 的,也就是它的标记位。所以我们可以通过 fcntl()
接口来直接设置一个文件描述符的属性!其实就是设置其文件对象中的 flags 标志位,告诉内核这个指定的文件描述符要以非阻塞的方式来操作。系统接口如下:
如上,可以按照指定的 cmd 来对指定的文件描述符来进行可变参数部分的设置。
传入的 cmd 的值不同,后面追加的参数也不相同。fcntl 函数有5种功能:
我们尝试将标准输入设置为非阻塞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;
}
上面的代码其实是跑不通的,因为我们根本没有输入数据,因此 n 是小于0的,我们可以通过打印错误信息观察:
我们可以看到,错误码的描述大概意思就是临时资源不可用,因为我们在 else 中 break 了,我们应该把 break 去掉,改为 sleep(1),我们方便观察。
此时运行后我们在键盘上输入是可以直接回显的,如下:
所以,设置为非阻塞,如果底层 fd 数据没有就绪,recv/read/write/send,返回值会以出错的形式返回。所以出错就分为两种情况了,一种是真的出错了,另一种是底层没有就绪,这种情况就是返回 11 号错误码,也就是 EWOULDBLOCK. 那么我们怎么区分呢?可以通过 errno 区分!如果 errno 为 11,代表底层没就绪!所以我们对代码稍作修改,如下:
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;
}