7.1 l/O 系统概述
I/O 系统是解决如何将所需要的信息(文字、图像、声音、视频等)通过不同外设输入到计算机中,或者计算机内部处理的结果如何通过相应的外设输出给用户。I/O 系统包含I/O硬件与I/O 软件两大部分。I/O 子系统的层次结构如图7.1所示。
图7.1 I/O 系统的层次结构
如图7.1所示,I/O 硬件由计算机系统中所有I/O 设备以及相应的I/O 接口电路组成,是I/O 系统的基础。I/O 软件通常是指用I/O 指令编制的、对I/O 接口及设备进行管理和访问的程序,也称为I/O 驱动程序。设备无关软件层是指实现用户应用程序与设备驱动层的统一接口,负责设备命令、设备保护以及设备分配与释放等功能,同时为设备管理和数据传送提供必要的存储空间。设备驱动层是根据设备的硬件实现以及应用程序需求,实现对设备进行操作的底层函数。中断服务程序用来控制主机与设备进行具体的数据交换。
如图7.2所示,用户应用程序发出I/O 请求后,通过系统调用(位于设备无关软件层),由I/O 接口判断I/O 请求是否有结果,如果有,就可以直接与进程交换数据,并返回完成结果或错误信息。如果没有,就向设备驱动程序发送I/O 请求,并等待结果。当接收到I/O 请求后,设备驱动程序将启动外设做好准备工作。当外设准备好后发出中断请求,CPU 响应中断后,就调出中断服务程序执行,由中断服务程序控制主机与设备进行具体的数据交换。完成中断请求后,处理结果将发送给中断处理例程,中断处理例程将保存结果并通知设备驱动,设备驱动再通知I/O 子系统,再将完成的结果或错误信息返回给用户进程。
下面以打印操作为主线来说明系统调用的代码实现以及系统调用的全过程,其他系统调用的处理过程实际上道理是一样的。以hello程序在Linux系统运行说明:
图7.2 I/O 请求流程
该hello程序中需要向屏幕输入“hello,world”并按Enter键,即需要向外设显示器输出信息,此时用户程序使用C 语言标准I/O 库函数printf来提出的I/O 请求。当调用了printf()后,则会转到C 语言函数库中对应的I/O 标准库函数printf()中去执行,该函数定义在文件printf.c中。当我们继续查看printf()函数时,我们发现通过一系列函数调用,最终转到了write()函数,而write()函数就是一个系统调用。
在write函数里面实际做了两件事情,一是将write所对应的系统调用号4 存放在EAX 寄存器中,然后通过int$0x80 指示处理器去做系统调用操作。在IA-32 中,INT 指令是陷阱指令,也称为软中断指令。在早期IA-32 架构中,int $0x80 指令是用作系统调用,系统调用号放在EAX 寄存器中,可根据系统调用号选择执行一个系统调用服务例程。
然后,当CPU 遇到陷阱指令时,就会调用Linux中的系统调用的统一入口:即系统调用处理程序system_call。看到在system_call函数中,根据系统调用号4跳转到相应的系统调用服务例程sys_write()函数,该函数将完成字符串输出的功能。
sys_write()的实现方式通常又有三种不同的I/O 接口的输入、输出控制方法,包括:查询控制方式、中断控制方式、DMA 控制方式(这些控制方式将在7.2.4节介绍)。在sys_write的执行过程中可能需要调用具体设备的驱动程序;在设备驱动程序中启动外设工作,外设准备好后发出中断请求,CPU 响应中断后,再调用中断服务程序执行,在中断服务程序中控制主机与设备进行具体的数据交换,如图7.3所示。
图7.3 用户程序、I/O 标准库函数和内核函数之间的关系