目录
在Linux系统编程中我们已经学习了基础IO的概念,本期开始我们将在基础IO的基础上进一步学习高级IO相关的知识。
五种IO模型
在高级IO中主要有以下五种IO模型,阻塞IO/非阻塞IO/信号驱动IO/IO多路转接/异步IO。
重新认识IO
我们总是说IO,那么能不能准确的给IO下一个定义呢?在TCP传输层中我们学到了接收缓冲区和发送缓冲区的概念。
其实在应用层调用系统调用接口时,这些系统调用接口其实就是起了一个拷贝的作用,要么将应用层的用户数据拷贝到发送缓冲区,要么将接收缓冲区中的数据拷贝到应用层,这一点我们在TCP中其实已经讲过了,不过今天我们要进行一下补充,在用户调用系统调用接口时,不仅仅是拷贝,在拷贝之前还要等待对应的发送缓冲区或者接收缓冲区是否就绪,何为就绪?对于发送缓冲区而言就绪就是发送缓冲区中有空间,对于接收缓冲区就绪就是接收缓冲区中有数据。
以生活中的一个场景举例子,就比如说在一个鱼塘,小明拿着鱼竿钓鱼,钓鱼的整个过程我们分为2步,第一步,等鱼上钩,第二步,把上钩的鱼放进自己的鱼桶里。所以,等鱼上钩就类似于等待缓冲区就绪,把上钩的鱼放进自己的鱼桶就类似于拷贝数据。
综上,我们给出计算机中IO的最终定义:IO=等+拷贝。
阻塞IO
注:五种IO情景统一以钓鱼为情景,且五个角色为张三,李四,王五,赵六,田七。
场景一:张三是一个办事很认真的人,所以张三在钓鱼的时候,会全心全意的投入钓鱼,只要鱼漂没动,张三就一直观察着鱼漂,直到鱼漂动了鱼上钩,最终张三将钓到的鱼放入鱼桶中。
张三的钓鱼场景引入IO模型中,就是一个典型的阻塞IO模型。即调用了对应的系统调用接口之后,就会一直等待文件描述符就绪,也就是所谓的死等,此时死等期间系统调用函数也是无法返回的,就会导致进程无法处理其它的任务,降低效率。
非阻塞IO
场景二:李四是一个办事三心二意的人,所以李四在钓鱼的时候,会边看手机边东张西望,只要鱼漂没动,李四就会一直的看手机和东张西直到鱼漂动了鱼上钩,同时李四也会定期的观察鱼漂是否动了,最终李四将钓到的鱼放入了鱼桶。
李四的钓鱼场景引入IO模型中,就是一个典型的非阻塞IO模型。即调用了对应的系统调用接口之后,只要一看到文件描述符没有就绪,就不会继续等了,系统调用函数就会立即返回,但是进程也会多次的调用系统调用接口(一般是程序员进行设置的),频繁地去判断文件描述符是否准备就绪。因为系统调用函数返回了所以进程可以去执行其它任务,提高了效率,但是频繁的调用系统调用函数,会导致进程在用户态和内核态的来回切换,消耗资源。
信号驱动IO
场景三:王五是一个善于思考的人,所以王五在钓鱼时,会往鱼漂上绑上一个铃铛,所以王五根本不用去关心鱼漂动没动,只关心铃铛响没想,所以王五在等铃铛响的期间也可以去干自己的事情,直到铃声一响,此时王五知道鱼上钩了,所以王五会钓起鱼将鱼放进鱼桶里。
王五的钓鱼场景引入IO模型中,就是一个典型的信号驱动IO模型。即当文件描述符就绪之后,就会给进程发送一个信号,进程捕捉到这个信号之后,就回去执行自定义handler函数,从而调用系统调用接口进行拷贝。此时进程在捕捉信号期间就可以去执行其它任务,提高了效率。
IO多路转接
场景四:赵六是一个小老板,直接准备了10个鱼竿,然后观察鱼漂,只要10个鱼竿中的任意一个鱼竿的鱼漂动了,就证明鱼上钩了,所以赵六就会立即去对应的鱼竿去钓起鱼,然后将钓起来的鱼放进鱼桶里。
赵六的钓鱼场景引入IO模型中,就是一个典型的IO多路转接模型,即可以通过select,poll和epoll一次性等待多个文件描述符就绪,大大的提高了效率。且传统的IO方式,都是在执行系统调用接口时,由对应的系统调用接口有等待文件描述符就绪和拷贝,但是有了IO多路复用之后,是在执行select,poll和epoll时由这些函数负责等待对应的文件描述符就绪,系统调用接口只负责拷贝数据。那么无论是select,poll和epoll还是系统调用接口,是如何知道对应的文件描述符是否就绪呢?其实这都是操作系统的功劳,因为进程每打开一个文件,此时操作系统就会为打开的文件创建一个结构体,然后操作系统会为进程分配一个文件描述符指向了打开的文件所描述的结构体,所以没有人比操作系统更懂得文件描述符,所以操作系统可以知道文件描述符何时就绪。
异步IO
场景四:田七是一个小老板,有自己的助理,田七让自己的助理去帮自己钓鱼,自己去忙其他的事,最终当助理钓完鱼了,最终老板会将助理钓的桶中的鱼提走。
田七钓鱼的场景引入IO模型中,就是一个典型的异步IO。可以理解为进程A既不参与执行系统调用时的等待,也不参与执行系统调用时的拷贝,这些工作全部让另一个进程做了,自己只是去接收了另一个进程IO完之后的数据。
只要一个进程既没有参与IO中的等,又没有参与IO时的拷贝,那么就称这个进程的IO就是一个异步IO。
那么问题来了,以上的五种IO模型,哪个IO模型的IO效率最高呢?
其实如果单纯的从效率角度出发,IO效率就可以简单的成为是单位时间内拷贝数据的多少。以此为依据去分析IO效率的话,其实本质上阻塞IO,非阻塞IO,信号驱动IO,异步IO本质上都是没有区别的,因为总归都是在单位时间内有进程去等的,要么等文件描述符就绪,要么等信号,要么等其它进程IO,它们单位时间内等待的时间大致也是相同的,因为大多数情况下都是等待一个文件描述符的就绪情况,但是对于IO多路转接就不一样了,它可以同时的等待多个文件描述符就绪,也就是说,单位时间内,它就绪的概率是最大的,也就意味着它在单位时间内是有比其他IO方式更大的概率进行数据拷贝,所以说IO多路转接模型是效率最高的模型。
因此在高级IO的五种模型中,我们重点学习IO多路转接模型。
同步通信和异步通信
有的小伙伴可能比较疑惑,在Linux系统编程中,我们学习了线程的同步,那么线程同步和我们的同步通信中的同步有关系吗?
其实,这两者是毫无任何关系的,线程中的同步讲究的是多个线程之间,互斥访问同一临界资源,为了防止竞争能力强的线程一直访问临界资源,导致其它线程长时间无法获取临界资源从而造成线程饥饿问题,所以我们引入了同步机制,保证了多线程访问临界资源的合法性。
那么同步通信中的同步是什么意思呢?
同步通信,关注点在同上,简单来说意味着一个函数调用通过自身调用获取了最终的调用结果,通俗来说就是自己通过自己的努力获取想要的结果。
异步通信,关注点在异上,简单来说意味着一个函数调用不是通过自身的调用来获取了最终的结果,而是在自己参数中,用可调用对象(函数指针,lambda表达式对象,仿函数对象)作为参数,通过这些可调用对象获取最终的调用结果。
阻塞和非阻塞
其实阻塞和非阻塞上文我们已经讲过了,再次给大家总结一下。
- 阻塞:即调用了个函数,这个函数在执行时,先去判断条件是否满足,条件不满足,就会一直等待条件满足,在条件满足之后,函数才会执行,执行完毕之后返回。所以这就会导致如果条件不满足,函数就会一直等待条件满足而不会返回,对于整个进程而言,无法返回,进程就阻塞在了这个函数中,从而不能去执行其它的任务,导致进程执行效率低下。
- 非阻塞:即调用了个函数,这个函数在执行时,先去判断条件是否满足,条件不满足,这个函数不会等待条件满足,而是会直接返回,对于整个进程而言,返回之后就意味着进程可以去执行其它任务,提高了进程的效率,但是有一点,非阻塞一般情况下会轮训的去进行检测,也就意味着,非阻塞的系统调用接口一般会被重复的调用,重复的调用就意味着重复的执行,重复的执行就意味着进程用户态和内核态的来回切换,所以就会造成大量资源的消耗。
非阻塞接口
对于任何一个文件描述符,默认情况下都是阻塞IO类型,如何将一个文件描述符由阻塞类型设置为非阻塞类型呢?
此时就需要用到一个fcntl函数。
fd为文件描述符,cmd表示我们要设置的命令选项,arg为可变参数列表。
我们重点关注,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)。
要设置文件描述符的状态为非阻塞,我们应重点关注第三个cmd命令选项,即设置文件状态标记。
以阻塞的方式进行文件的读写。
cpp
#include<iostream>
#include<unistd.h>
#include<string.h>
int main()
{
char buffer[1024];
while(1)
{
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
write(1,buffer,strlen(buffer));
}
}
return 0;
}
运行结果如下。

此时的进程是阻塞住的。因为我们一开始在使用read接口时,是从标准输入对应的0号文件描述符中读取数据,但是0号文件描述符是阻塞的,所以read接口在进行读取时,会在内核态等待0号文件描述符就绪,即标准输入中输入数据,此时才会在内核态执行read函数最终返回。如果没有往标准输入中输入数据,那么此时0号文件描述符就一直没有就绪,所以read接口就会一直在内核态阻塞住,不会返回,所以此时进程也是阻塞住的。
以非阻塞的方式进行文件的读写。
cpp
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
// 设置文件描述符为非阻塞状态
void SetNonBlock(int fd)
{
// 获取文件描述符的状态
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
std::cout << "fcntl erro" <<" "<<std::endl;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
// 将0号文件描述符设为非阻塞状态
SetNonBlock(0);
char buffer[1024];
while (1)
{
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
write(1, buffer, strlen(buffer));
std::cout << "read success!" << s << std::endl;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 读取失败
std::cout << "read failed,文件描述符未就绪!" << errno << std::endl;
sleep(1);
continue;
}
}
}
return 0;
}

此时的0号文件描述符被我们通过fcntl接口设置成了非阻塞状态。当调用read接口从0号文件描述符进行读取时,进程切换到内核态执行时发现0号文件描述符没有就绪,此时就会即返回,且是出错返回,返回之后会多次的轮训调用read接口。虽然读取时没有读取到数据本身并不是read接口的问题,而是文件描述符本身没有就绪的问题,但是在非阻塞状态下,read的只要是在执行时发现文件描述符没有就绪,此时返回我们认为是read的错误,且此时的错误码为EAGEIN或者EWOULDBLOCK,EAGAIN是一个宏,大小为11。
以上便是本期高级IO的所有内容。
本期内容到此结束^_^