瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】824412014(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第四篇-高级字符设备进阶_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
第四篇 高级字符设备进阶
第25章 IO模型引入实验
我们经常提到 IO、NIO 这些名词。那么,到底什么是 IO 呢?什么又是 NIO 呢?另外,我们平时又会听到两组很相似的概念:阻塞/非阻塞、同步/异步。那么,阻塞和非阻塞有什么区别呢?同步和异步的差别又在哪里呢?
为了更好的理解IO模型,在本章节将对IO的概念、IO的执行过程及IO模型的分类进行详细分析,下面就让我们一起进入IO的世界吧!
25.1 IO的概念
IO是英文Input和Output的首字母,代表了输入和输出,当然这样的描述有一点点抽象,更直观的意思是计算机的输入与输出。在冯.诺依曼结构中,将计算机分成了5个部分,分别是运算器,控制器,存储器,输入设备,输出设备。如下图(图 25-1)所示:
上图中的输入设备指的是鼠标和键盘等向计算机输入数据和信息的设备,输出设备指的是电脑显示器等用于计算机信息输出的设备,下面对计算机输入输出过程进行实际举例,当敲击键盘(输入设备)任意按键后,按键的数据会传递给计算机,计算机CPU会对数据进行运算,运算完成之后会将数据输出到显示器(输出设备)上,整个过程如下图(图 25-2)所示:
上述事例中,鼠标、显示器只是输入输出的直观表现形式,而在计算机架构层面上,IO是涉及计算机核心与其他设备间数据迁移的过程。以磁盘IO为例,内存读取磁盘数据和将内存数据写入磁盘,就是一对输入输出的过程。
至此,对于IO的概念就讲解完成了,在下一小节中将对IO执行过程进行分析。
25.2 IO执行过程
操作系统(Linux)负责对计算机的资源进行管理和对进程进行调度,应用程序运行在操作系统上,处于用户空间。应用程序不能直接对硬件进行操作,只能通过操作系统提供的API来操作硬件。需要将进程切换到内核空间,才能进行IO操作,并且应用程序不能直接操作内核空间的数据,需要把内核空间的数据拷贝到用户空间。
应用程序运行在用户空间,它不存在实质的IO过程,真正的IO是在操作系统执行的。那么应用程序操作IO就会有两个动作:IO调用和IO执行。IO调用是应用程序向操作系统内核发起调用,IO执行是操作系统内核完成的IO操作。
一个完整的IO过程需要包含以下三个步骤,如下图(图25-3)所示:
(1) 用户空间的应用程序向内核发起IO调用请求(系统调用)
(2) 内核操作系统准备数据,把IO设备的数据加载到内核缓冲区
(3) 操作系统拷贝数据,把内核缓冲区的数据拷贝到用户进程缓冲区
25.3 IO模型的分类
假设有这样一个场景,从磁盘中循环读取100M的数据并处理,磁盘读取100M需要花费20秒的时间,CPU同样也需要20秒的时间处理完这些数据。如果采用传统的模式编写代码:读数据->等待数据读取完毕->数据处理,可以发现,数据的读取花费了一半的时间,而这就导致该任务的效率极其低下,那么能不能在等待数据的同时对数据进行处理呢?当然可以!这时候就轮到IO编程模型来出场了。
IO模型根据实现的功能可以划分为为阻塞IO、非阻塞IO、信号驱动IO, IO多路复用和异步IO。根据等待IO的执行结果进行划分,前四个IO模型又被称为同步IO,如下图(图 25-3)所示:
所谓同步,即发出一个功能调用后,只有得到结果该调用才会返回。异步的概念和同步相对。当一个异步过程调用发出后,调用者并不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
以现实生活去餐馆吃饭为例,根据菜单进行点餐之后,这时会存在两个选择,第一个选择是在餐馆等待饭菜制作完毕,这就是同步IO的具体表现。第二个选择是,离开餐馆去做其他的事情,工作人员会在饭菜制作完成之后提醒你回餐馆取餐,这就是异步IO的具体表现。
下面让我们来认识一下这五种IO模型。
1 阻塞IO
以阻塞读为例:进程进行IO操作时(如read操作),首先会发起一个系统调用,从而转到内核空间进行处理,内核空间的数据没有准备就绪时,进程会被阻塞,不会继续向下执行,直到内核空间的数据准备完成后,数据才会从内核空间拷贝到用户空间,最后返回用户进程,由用户空间进行数据的处理,如下图(图 25-5)所示:
以现实生活中的钓鱼为例,在做好相应准备抛下鱼钩之后,需要耐心等待鱼儿的上钩,等待的过程中必须聚精会神的关注鱼竿的状态,鱼儿上钩之后立刻扬竿,这就是阻塞IO在实际生活中的事例。
通过上述例子可以总结出阻塞IO的优势与不足,首先可以及时的获取结果,并立刻对获取到的结果进行处理,然而在获取结果之前,无法去处理其他任务,需要时刻对结果进行监听。
阻塞IO比较有代表性的是C语言中的scanf()函数。编写好的io.c文件,如下所示:
#include <stdio.h>
int main(void){
int i;
scanf("%d",&i);
printf("i = %d\n",i);
return 0;
}
在以上代码中,scanf函数用于从键盘上接收数据,如果键盘不进行数据的输入,该任务会持续阻塞,只有键盘输入数据之后,才会有相应的输入值打印到系统终端上。输入以下命令进行可执行文件的编译,如下(图 25-7)所示
gcc io.c -o io
编译完成之后,输入“./io”运行可执行文件,如下所示,键盘没有输入数据时,该任务会持续阻塞,当在键盘上输入“123”之后,输入的值才会被打印出来,如下(图 25-9)所示:
2 非阻塞IO
和阻塞IO模型不同,非阻塞IO进行IO操作时,如果内核数据没有准备好,内核会立即向进程返回err,不会进行阻塞;如果内核空间数据准备就绪,内核会立即把数据返回给用户空间的进程,如下图(图 25-10)所示:
仍旧以现实生活中钓鱼为例,在做好相应准备抛下鱼钩之后,这次并没有持续不断的关注鱼竿的状态,而是去做其他的事情(不阻塞等待结果),每隔几分钟对鱼竿的状态进行检查,如果没有鱼儿上钩,就继续去做其他事情,如果上钩了就把鱼钓上来,这就是非阻塞IO在实际生活中的事例。
从上述案例中可以看出非阻塞IO的优点是效率高,同样的时间可以做更多的事。但是缺点也很明显,需要不断对结果进行轮询查看,从而导致结果获取不及时(结果可能在两次轮询之间就已经准备完毕,但是只能在发起轮询的时候才能知道),如果要增加非阻塞IO的实时性,就要加快轮询的频率,但这样无疑也会增加CPU的负担。
3 IO多路复用
通常情况下使用select()、poll()、epoll()函数实现IO多路复用。这里以select函数为例进行讲解,使用时可以对select传入多个描述符,并设置超时时间。当执行select的时候,系统会发起一个系统调用,内核会遍历检查传入的描述符是否有事件发生(如可读、可写事件)。如有,立即返回,否则进入睡眠状态,使进程进入阻塞状态,直到任何一个描述符事件产生后(或者等待超时)立刻返回。此时用户空间需要对全部描述符进行遍历,以确认具体是哪个发生了事件,这样就能使用一个进程对多个IO进行管理,如下图(图 25-11)所示:
继续以现实生活中的钓鱼为例,和之前案例只有一个鱼竿不同,这次会在十个不同的地方做好相应准备抛下鱼钩,并把十个鱼竿连在了一个铃铛上,这样只要铃铛响了就表示有鱼上钩,只需挨个检查到底是哪个鱼竿有鱼上钩即可。
这样的优点是一个进程/线程可以同时监听和处理多路IO,效率成倍提高。但是IO多路复用并不是能医治百病的良药,虽然IO多路复用可以监听多个IO,但是实际上对结果的处理也只能依次进行,比较适合IO密集但是每一路IO数据量不多且到达时间分散的场合(如网络聊天)。
另外select监听的描述符有上限(一般描述符最大不超过1024),而且需要遍历究竟是哪一个IO产生了数据。因此IO较多时,效率不高(这个问题被epoll解决,感兴趣的读者可以自行了解)。
4 信号驱动
信号驱动IO顾名思义与信号相关。系统在一些事件发生之后,会对进程发出特定的信号,而信号与处理函数相绑定,当信号产生时就会调用绑定的处理函数。例如在Linux系统任务执行的过程中可以按下ctrl+C来对任务进行终止,系统实际上是对该进程发送一个SIGINT信号,该信号的默认处理函数就是退出当前程序。
具体到IO模型上,可以对SIGIO信号注册相应的信号处理函数,并打开对应描述符的信号驱动。每当有IO数据产生时,系统就会发送一个SIGIO信号,进而调用相应的信号处理函数,从而在这个处理函数中对数据进行读取,如下图(图 25-12)所示:
仍旧以现实生活中的钓鱼为例,在做好相应准备抛下鱼钩之后,这次同样没有持续不断的关注鱼竿的状态,而是去做其他的事情(不阻塞等待结果),与之前不同的是,在鱼竿处绑定了一个提醒铃铛,当鱼咬钩之后,铃铛就会响(有SIGIO信号),进而得知到鱼儿上钩的消息之,这样就可以及时把鱼钓上来了(调用处理函数)。
5 异步IO
aio_read函数常常用于异步IO,当进程使用aio_read读取数据时,如果数据尚未准备就绪就立即返回,不会阻塞。若数据准备就绪就会把数据从内核空间拷贝到用户空间的缓冲区中,然后执行定义好的回调函数对接收到的数据进行处理。
最后,还是以钓鱼为例。小马同学喜欢吃新鲜的鱼,但是不想自己钓,所以他请了一个助手来帮他钓鱼,他自己去忙其他的事情(进程不阻塞,立即返回)。如果有鱼上钩助手会帮忙钓上来(将数据拷贝到指定的缓冲区),并立即通知小马同学回来把鱼取走(处理数据)。