个人主页:小则又沐风
个人专栏:
• [数据结构]
• [竞赛专栏]
• [C语言]
• [C++]
• [Linux]
• [OJ项目]
• [MySQL]•[GIT]
目录
前言
一、阻塞式IO
二、非阻塞式IO
三、信号驱动IO
四、IO多路转接
五、异步IO
文件描述符的非阻塞的设置
fcntl函数
前言
在我们日常通信的时候服务器做的实际上就是一个个的IO操作,将客户端的需求传递到服务器,将服务器的应答传递到客户端。但是这些IO操作默认都是阻塞的,在应答没有到来之前都是在等待的状态所以在这个环境下我们想要提高服务器的效率我们的操作就是大大的进行创建线程,但是这并不是一个很好的方法。今天我们就以钓鱼的例子来了解Linux下的五种IO模型。
一、阻塞式IO
阻塞IO时最常见的一种IO方式,阻塞IO是在数据没有到来之前会一直等待数据的到来。
用钓鱼的例子来讲是这样的:
张三来到河边,抛竿然后目不转睛的盯着自己的鱼竿,等待鱼儿上钩(鱼漂沉入),鱼来了就会收杆。
在这里张三就是应用进程,鱼竿就是文件描述符,鱼儿就是数据,鱼漂就是就绪事件。
这样在数据没有到来之前会一直等待的IO方式就是阻塞式IO。
二、非阻塞式IO
与阻塞式IO相对应的就是非阻塞IO。
在我们相同的场景下,李四来到了张三在的河边,非常不屑的看了一眼张三,他将自己的鱼竿抛在了河里然后他不是一直在等待鱼的上钩而是在玩自己的手机,时不时会观察是否有鱼上钩。
现在李四和张三不同的就是李四会在鱼还没有到来的时候处理别的事情,换句话来说就是李四并不是只做了等待这一件事而是在等待的同时做了别的事情。但是这样的是有一个弊端就是李四需要频繁的看是否有鱼儿的到来非常浪费李四的精力(玩手机都不可以安生)。
但是这样的情况下是由一个让人头痛的就是在数据就绪的时候我们式并不知道的,所以我们需要频繁的进行查看,也就是轮询操作,这样式非常的消耗资源的。
三、信号驱动IO
王五也来到了这个钓鱼的河边,他也是非常不屑的看了以上的两个人,他在自己的鱼竿上挂上了一个铃铛,在鱼儿上钩的时候这个铃铛就会通知王五。由于这个铃铛的存在,王五就可以安心的做他的其他的事情了。
那么这样在数据就绪的时候就会有信号来通知的机制就是信号驱动IO,由以上的就可以看到的就是王五节省了频繁询问的过程,这样就会大量的使CPU的利用率提高,但是信号驱动当然是离不开信号的,信号驱动的底层实现就是通过进行信号的回调函数进行重写。
那么这样就会出现一个问题:
在通常的情况下是所有的鱼钩共用一个铃铛的,但是在LInux下我们可以将每个鱼钩都挂上铃铛,但是仅仅限于鱼钩的数量小。(信号的数量是有限的)
所以这个IO策略的缺点是这样的:
倘若是公用一个铃铛,我们有很多的鱼钩铃铛一响我们并不知道是哪一个的鱼钩上了钩。倘若我们每个鱼钩都有一个铃铛,因为我们信号的数量是有限的,所以当鱼钩的数量太大的时候有的鱼钩是没有铃铛挂的。
所以这个策略是无法很好的解决高并发的。
四、IO多路转接
赵六也来到这个钓鱼的地方,赵六钓鱼的方法是这样的:他将许许多多的鱼钩都抛在了池塘里面,然后看那个鱼钩的鱼漂有动静就会进行对那个鱼钩进行收钩。
所以赵六忙的不亦乐乎,因为其他人的鱼钩的数量极少,他的鱼钩数目极大,就按照概率鱼儿选择的鱼钩大概率是赵六的。
在以上的几个IO模型中,IO多路复用这个策略的效率是最高的一个。
为什么这个策略的效率这么高呢?
因为在以上的策略中,无论是谁他们都没有减少等待数据到来的时间,相反他们是尽可能的想要利用起来这个等待的时间,但是多路转接这个策略因为我们是一个广撒网,所以等待的时间是被大幅度的压缩的。
那么这个策略是怎么进行实现的呢?
- 进程调用select/poll/epoll这些多路复用的函数
- 进行监控需要监控的文件描述符
- 在文件描述符有事件就绪的时候进行数据处理
- 随后进行继续监控
五、异步IO
田七是一个名副其实的富豪,他对他的手下说”小王啊,今天我有点想吃鱼了,你去给我钓一桶鱼,我去开会了。”
这样的策略就是异步IO,我们只关心钓鱼的结果,并不关心是怎么得到的鱼,我们在下达任务之后就去忙自己的事情了,这真是结结实实的非阻塞IO。全程我们并不参与IO的过程。我们得到的是IO数据拷贝结束的结果。
文件描述符的非阻塞的设置
fcntl函数
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );首先我们通过这个函数得到对应的文件描述符的属性。
int fl = fcntl(fd,F_GETFL);然后对文件描述符进行操作。
fcntl(fd,F_SETFL,fl|O_NONBLOCK);这样就完成了文件描述符的非阻塞的设置。
下面是我们的演示代码:
#include <iostream> #include <unistd.h> #include <fcntl.h> void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); if (fl < 0) { perror("fcntl error"); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); } int main() { SetNoBlock(0); while (1) { char buf[1024] = {0}; ssize_t read_size = read(0, buf, sizeof(buf) - 1); if (read_size > 0) { std::cout << "读取成功:" << buf << std::endl; } else if (read_size == 0) { std::cout << "写端关闭" << std::endl; break; } else { if (errno == EWOULDBLOCK || errno == EAGAIN) { std::cout << "数据还没有就绪\n"; } else { perror("read error"); break; } } sleep(1); } return 0; }在这里需要进行解释的是read的返回值:
成功读取的到数据的时候就会返回读取到的数据的数量。
在读到了文件的结尾或者对端关闭的时候就会返回0。
在读取有错误的时候就会返回-1;
但是因为我们这次的读取是一个非阻塞的所以错误可能是没有数据造成的所以我们要进行判断。