前言:本节内容进入高级IO, 我们将要讲解高级IO的铺垫性质的知识点, 比如理解高级IO,IO分为什么等等。 下面废话不多说,开始我们的学习吧!!!
ps:本节内容刚刚进入高级IO, 友友们放心观看哦!
目录
重新理解IO
以read为例,我们今天调用read,接收缓冲区没有数据,那么read就阻塞等待了。
以write为例,我们今天调用write, 发送缓冲区被打满了,那么write阻塞等待了。
我们就知道一个基本事实就是,在应用层读写的时候,本质不是从网络当中读数据,或者把数据写到网络里,我们其实是把数据写给操作系统,至于操作系统什么时候发,发多少,由操作系统自己决定。所以read和write其实本质就是拷贝函数,我们平时应用层用的这些读写函数,其实就是拷贝函数。
更重要的是,我们把读写就叫做IO,即Input和Output。
当我们进行IO的时候,其实应用层大部分时间,都是等待的操作。只有当有数据才读,只有当有空位,才写。所以,IO本质 = 等 + 拷贝。那么我们IO的过程中在等什么呢?对于read,在等缓冲区有数据;对于write,在等缓冲区有空位。所以,我们真正IO的时候,要进行拷贝,就要先判断条件成立,这里的条件就叫做读写事件。
所以,我们以后就一句话: IO = 等 + 贝,等什么? 等读写事件就绪。
什么叫做高效的IO呢?单位时间内,IO过程中等的比重越小,IO的效率就越高。几乎所有的提高IO效率的策略,本质就是让等的比重减小。
五种IO模型
五种IO模型分为以下五种:
- 1、阻塞式。我们学过的大部分接口,比如read, write接口。都是阻塞式。
- 2、非阻塞式。就是非阻塞轮询查看是否读写事件就绪。
- 3、信号驱动。就是当读写事件就绪的时候通知我们。
- 4、多路复用,多路转接。
- 5、异步IO
阻塞IOVS非阻塞IO。两者在IO层面没有区别。因为IO = 等 + 拷贝。只不过等的方式不同,所以在非阻塞时候可以做其他事,在其他事上效率更高。
同步IO和异步IO的区别?区别就是有没有参与IO。 参与了,就是同步。没参与,只是发起IO,就是异步。
这里讲的同步IO和线程同步之间,没有关系!!!
上面的这些方法,谁的效率最高?就是多路转接。所以我们接下来几节内容都是要学习多路转接。 但是由于非阻塞也有一些重要, 所以也要了解一下非阻塞,本节内容的任务就是将非阻塞进行讲解完毕。
阻塞式等待
当我们recvfrom向操作系统要数据的时候,操作系统说数据还没有准备好,则等待数据。等待数据,其实就是阻塞在了recvfrom这个系统调用里面了。怎么阻塞?
本质就是把PCB从运行队列里面拿下来,当数据好了之后,再把我们的进程唤醒运行。再把数据从内核当中拷贝到应用层返回。这个时候revfrom才能够返回。所以recvfrom这个函数就是先等,然后再拷贝。
非阻塞式等待
然后recvfrom也可以设置成为非阻塞。设置成非阻塞如果再没有数据,那么不会等待,而是直接返回一个EWOULDBLOCK。如果检测到数据报准备好了,那么我们就可以把数据从内核拷贝到用户层,然后就完成了。
多路转接
上面的recvfrom一个函数,承担了两种任务。第一种是等,第二种是数据拷贝。然后多路转接有自己的函数,其中一个就叫select。select只负责IO当中的等,事件就绪后他会通知上层。然后上层再来读取,这样recvfrom或者read等接口直接调用的话就不需要等了。所以,这里的select,以及我们以后讲解的其他的多路转接接口,最大的特点就是他们会一次性等待多个文件描述符。
异步IO
其实异步IO接口在操作系统层面和应用层面都有。我们调用异步IO接口。接口就直接返回。应用层就能继续做自己的事情。在做自己的事情的时候,操作系统帮我等,等好之后操作系统帮我把数据从内核拷贝到用户。之后再进行数据处理。
非阻塞
我们这里要使用代码来实现一下网络套接字版本的非阻塞IO。首先下面是我们要用到的核心函数fcntl。 其中第一个参数就是文件描述符(可以是网络版本的,也可以是非网络版本的)。
对于上面的cmd,对指定的描述符,进行设置状态。 传入的cmd不同,后面的可变参数就不同。
- 复制一个现有的描述符 (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)。
下面是我们测试非阻塞的代码:
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<cstring>
using namespace std;
//这里之所以要先获得标志位, 是因为担心这个文件描述符已经设置过标记位了。
void SetNoBlock(int fd) //
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
exit(0);
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNoBlock(0);
char buffer[1024];
while (true)
{
cout << "Please Enter# ";
fflush(stdout);
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;
}
else
{
if (errno == EWOULDBLOCK)
{
cout << "not ready, try again" << endl;
sleep(1);
}
else
{
cout << "read error n = " << n << endl;
cout << "error code: " << errno << " " << "strerror: " << strerror(errno) << endl;
}
}
}
return 0;
}
上面的代码, 为什么当n == -1的时候里面还要进行判断一下是否errno是EWOULDBLOCK。 因为如果一旦我们把fd设置成非阻塞, 一旦底层没有数据,那么返回值就会以出错的形式返回。所以非阻塞轮询的时候出错有两种情况:1、真的出错。2、底层没有就绪。
我们如何区分, 就是使用错误码区分, 如果错误码是11,那么就是没有就绪。宏定义名称叫做:EWOULDBLOCK。
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!