目录
一、I/O的理解
二、五种IO模型
1.阻塞式IO
2.非阻塞式IO
3.信号驱动IO
4.多路复用IO
5.异步IO
一、I/O的理解
I/O的本质就是输入输出,C语言的stdio,C++的iostream,添加了这两个库,我们才能够进行printf、scanf、cin、cout。
在I/O中,我们大部分时间是在等,比如你调用了scanf(),当前进程就在等待你的输入,你输入完毕后,把输入数据拷贝到内存中,才会继续执行。因此,I/O = 等 + 拷贝。拷贝的效率是硬件决定的,但是等待的时间,是可以由程序来优化的。单位时间内,减少了等的比重,这也就提高了I/O效率。
在网络中,我们经常需要去recv和send,recv是在等待对方发送消息,我来接受,如果此时对方还没有发送消息,我们就会在recv这里阻塞;send则是我发送消息,对方接受消息,当对方的接受缓冲区满了,也会造成阻塞,直到对方接收缓冲区有空间了,才可以发送过去。
如果现在我可以去干自己的事情,等我收到的对方通知(他的条件满足),我们再去调用recv或者send函数,这样就大大提高了效率。
二、五种IO模型
为了理解五种IO模型,我们来看个钓鱼的例子,钓鱼 = 等 + 钓 ,这里可以把钓看做是拷贝,钓都是一样的,但是等的过程不相同。
- 张三:永远看着鱼漂,谁叫他都没有 ——阻塞式IO
- 李四:一会看看鱼漂,一会看看书、刷书视频 ——非阻塞轮询式IO
- 王五:铃铛绑在的鱼竿顶部(铃铛响了,就开始抬杆) ——信号驱动式IO
- 赵六:100个鱼竿一起钓,哪个鱼漂动了就去哪里 ——多路复用IO
- 田七:发起了钓鱼,让助手小王去钓鱼,自己去干其他事情 ——异步IO
其中,阻塞、非阻塞、信号驱动、多路复用都是同步IO,他们都参与了IO的过程,要么等了、要么拷贝了、或者都做了。
而田七只是发起了钓鱼,然后就当甩手掌柜,交给小王去等去拷贝,自己并没有参与,他是异步IO。
在这个故事中,河流是操作系统,鱼是数据,鱼漂是数据就绪条件,鱼竿是文件描述符。
1.阻塞式IO
阻塞IO: 在内核将数据准备好之前,系统调用会一直等待。 所有的套接字,默认都是阻塞方式
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
int main()
{
while (true)
{
char buff[1024];
ssize_t s = read(0, buff, sizeof(buff - 1));
if(s>0)
{
buff[s] = '\0';
std::cout<<"echo# "<<buff<<std::endl;
}
else if (s==0)
{
std::cout<<"end stdin"<<std::endl;
break;
}
else
{
//TODO
}
}
}
2.非阻塞式IO
阻塞IO:如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdlib>
//使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
//然后再使用F_SETFL将文件描述符设置回去, 设置回去的同时, 加上一个O_NONBLOCK参数
void SetNoBlock(int fd)
{
// 获取fd文件描述符的状态标志
int f1 = fcntl(fd, F_GETFL);
if (f1 < 0)
{
std::cerr << "fcntl error" << std::endl;
exit(0);
}
// 设置fd文件描述符的状态标志 f1为老标志的内容,O_NONBLOCK为非阻塞
fcntl(fd, F_SETFL, f1 | O_NONBLOCK);
}
int main()
{
SetNonBlock(0);
while (true)
{
char buff[1024];
ssize_t s = read(0, buff, sizeof(buff) - 1);
if (s > 0)
{
buff[s] = '\0';
std::cout << "echo# " << buff << std::endl;
}
else if (s == 0)
{
std::cout << "end stdin" << std::endl;
break;
}
else
{
// 非阻塞等待,数据没有准备好,返回值仍然是 -1 但不认为是出错
// 因此我们需要查看错误码,看具体原因
if (errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "OS的底层数据还没有就绪,errno: " << errno << std::endl;
}
else if(errno == EINTR)
{
std::cout << "IO 被信号中断,请再次尝试"<< std::endl;
}
else
{
//这里才是read出错
break;
}
}
sleep(1);
}
}
3.信号驱动IO
信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
4.多路复用IO
IO多路复用:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路复用能够同时等待多个文件描述符的就绪状态。
操作系统给我们提供了select系统调用,我们可以将很多文件描述符交给select管理。多个文件描述符就绪的概率是要比前面IO模式单个文件描述符就绪的概率高得多,当有文件描述符准备就绪,select不进行拷贝,而是通知应用程序,让应用程序去进行相应的IO处理,此时,是不会出现IO阻塞的情况,因为是收到的通知才进行的IO。大大提高了效率。
select的使用
5.异步IO
异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
异步IO,只是发起的IO请求,由操作系统去完成IO等+拷贝的操作,最后吃现成的就行了。