1. I/O软件分层
I/O 层次结构分为五层:
- 用户层 I/O 软件
- 设备独立性软件
- 设备驱动程序
- 中断处理程序
- 硬件
其中,设备独立性软件、设备驱动程序、中断处理程序属于操作系统的内核部分,即“I/O 系统”,或称“I/O 核心子系统”。
2.用户层 I/O软件
主要是分为三部分:
- 系统调用
- 库函数
- 假脱机系统(Spooling)
2.1 系统调用
为使诸进程能有条不紊的使用 I/O 设备,因此不允许在用户态的应用进程去直接调用运行在核心态的 OS 过程,而应用进程在运行时,又必须取得OS所提供的服务
于是,OS 在用户层中引入了系统调用,应用程序可以通过它,间接调用 OS 中的 I/O 过程,对 I/O 设备进行操作
当应用程序需要咨询某种 I/O 操作时,在应用程序中必须执行相应的系统调用,当 OS 捕获到系统调用命令后,会从用户态转为核心态,执行系统调用。执行完毕后,再返回用户程序
2.1.1 字符设备接口
- get/put 系统调用:向字符设备缓冲区读/写一个字符。
2.1.2 块设备接口
- read/write 系统调用:向块设备的读/写指针位置读/写多个字符。
- seek 系统调用:修改读/写指针位置。
2.1.3 网络设备(网络套接字)接口
- socket 系统调用:创建一个网络套接字,需指明网络协议。
- bind 系统调用:将套接字绑定在某个本地端口。
- connect 系统调用:将套接字连接到远程地址。
- read/write 系统调用:从套接字读/写数据。
2.1.4 阻塞/非阻塞 I/O
- 阻塞 I/O:应用程序发出 I/O 系统调用,进程需转为阻塞态等待。例如,字符设备接口的 get 调用(从键盘读一个字符);C 语言中的 scanf 函数。
- 非阻塞 I/O:应用程序发出 I/O 系统调用,系统调用可迅速返回,进程无需阻塞等待。例如,块设备接口的 write 调用(往磁盘写数据);C 语言中的 printf 函数。
2.2 库函数
1. 库函数必归于系统调用(底层调用):所有资源操作终将穿透用户态壁垒(例如内存分配器 malloc 落地为 mmap 系统调用)
2. 系统调用经库函数封装:原始接口经标准库包装方成可移植组件(如 Linux clone() 被封装为跨平台的 pthread_create)
Linux下的C库 GNU C library
“
GNU C 库项目为 GNU 系统和 GNU/Linux 系统,以及许多其他 使用 Linux 作为内核。这些库提供关键 API 包括 ISO C11、POSIX.1-2008、BSD、特定于作系统的 API 等。这些 API 包括 open、read、write、malloc、printf、getaddrinfo、dlopen、pthread_create、crypt、login、exit 等基础工具。
GNU C 库被设计为向后兼容、 便携式和高性能 ISO C 库。它旨在遵循所有 相关标准包括 ISO C11、POSIX.1-2008 和 IEEE 754-2008。
”
2.3 假脱机(SPOOLing)技术
2.3.1 虚拟化的引入
虚拟性是操作系统的一大重要特性,它通过某种技术将物理实体转化为多个逻辑实体,两种技术是:时分复用和空分复用,不做过多赘述。而虚拟化,举个例子,(单处理机系统下)在生活中,我们同时在计算机上运行多个进程,比如:一边玩游戏一边使用微信聊天一边听音乐,感觉像是拥有多个CPU一样,但是实际是单处理器,正式通过虚拟化使得用户在逻辑上感觉有多个CPU使得多个进程在同时运行,而事实是同一时刻CPU只能运行一个进程。这种虚拟化技术极大地提高了我们生活的便利性和CPU的利用率。
2.3.2 SPOOLing 技术
SPOOLing 技术是用于将 I\O 设备进行虚拟化的技术,这个技术可不像 CPU 的虚拟化能欺骗我们人类,它是专门用于欺骗进程的。就拿打印机举栗吧,我就买了一台打印机,但此时我打开了 word 和 pdf,想要打印 word 和 pdf 中的内容;此时计算机中有2个进程,word 进程和 pdf 进程,这两个进程都认为自己拥有一个打印机,那么是否此时我作为计算机的主人就拥有2台打印机了呢?当然不是啊,我又不是睁眼瞎,我就看到了一台打印机啊这就是通过虚拟化技术欺骗了2个无知的进程。
SPOOLing技术也就是通过使用一个进程模拟脱机任务,以此实现独享设备到共享设备的虚拟化。
2.3.3 SPOOLing的组成
SPOOLing 技术系统由预输入程序、井管理程序(本质上可以理解成目录)和缓输出程序组成。预输入程序、井管理程序和缓输出程序合起来也叫守护进程。
- 输入井:模拟脱机输入时的磁带,用于收容 I/O 设备输入的数据,是在磁盘上开辟出的存储区域。
- 输出井:模拟脱机输出时的磁带,用于收容用户进程输出的数据,是在磁盘上开辟出的存储区域。
- 输入缓冲区:位于内存,在输入进程的控制下,“输入缓冲区”用于暂存从输入设备输入的数据,之后再转存到输入井中。
- 输出缓冲区:位于内存,在输出进程的控制下,“输出缓冲区”用于暂存从输出井送来的数据,之后再传送到输出设备上。
2.3.4 SPOOLing 假脱机管理进程的工作原理
用户进程提出输出打印的请求时,由假脱机管理进程为每个进程做两件事:
- 在磁盘输出井中为进程申请一个空闲缓冲区,并将要打印的数据送入其中;
- 为用户进程申请一张空白的打印请求表,并将用户的打印请求填入表中,再将该表挂到假脱机文件队列上。
打印机空闲时,输出进程会从文件队列的队头取出一张打印请求表,并根据表中的要求将要打印的数据从输出井传送到输出缓冲区,再输出到打印机进行打印。
3.设备驱动程序,中断处理程序
设备驱动程序是一种软件,用于控制计算机上的硬件设备(如打印机、键盘、鼠标、音频/视频设备等),以便它们能够与计算机系统协调工作。设备驱动程序通常由设备制造商提供,并与操作系统紧密集成,以便系统可以识别和使用设备。设备驱动程序提供与设备交互所需的指令,允许设备与计算机通信,从而使设备能够正常工作。
虽然设备控制器屏蔽了设备的众多细节,但每种设备的控制器的寄存器、缓冲区等使用模式都是不同的,所以为了屏蔽「设备控制器」的差异,引入了设备驱动程序。
‘设备完成了事情,则会发送中断来通知操作系统。那操作系统就需要有一个地方来处理这个中断,这个地方也就是在设备驱动程序里,它会及时响应控制器发来的中断请求,并根据这个中断的类型调用响应的中断处理程序进行处理。 通常,设备驱动程序初始化的时候,要先注册一个该设备的中断处理函数。
中断处理程序的处理流程:
- 在 I/O 时,设备控制器如果已经准备好数据,则会通过中断控制器向 CPU 发送中断请求;
- 保护被中断进程的 CPU 上下文;
- 转入相应的设备中断处理函数;
- 进行中断处理;
- 恢复被中断进程的上下文;
4.设备控制器
为了屏蔽设备之间的差异,每个设备都有一个设备控制器的组件,作为计算机主机和外部设备之间的桥梁,比如:硬盘由硬盘控制器、显示器由视频控制器。设备控制器是硬件组件,负责管理该类型设备的所有通信。
每个设备控制器都会有一个应用程序与之对应,设备控制器通过应用程序的接口通过中断与操作系统进行通信。设备控制器是硬件,而设备驱动程序是软件。 CPU通过设备控制器来和设备连接,设备控制器里有芯片,它可以执行自己的逻辑,也有自己的寄存器,用来和CPU进行通信,比如:
- 通过写入这些寄存器,操作系统可以命令设备发送数据、接受数据、开启或关闭、或者执行某些其他操作。
- 通过读取这些寄存器,操作系统可以了解设备的状态,是否准备号接受一个新的命令等。
控制器有三类寄存器,分别是状态寄存器、命令寄存器以及数据寄存器,这三个寄存器的作用:
- 数据寄存器:CPU向I/O设备写入需要传输的数据
- 命令寄存器:CPU发送一个命令,通知I/O设备,要继续输入/输出操作,于是就交给I/O设备去工作,任务完成后,会把状态寄存器里面的状态标记为完成
- 状态寄存器:通知CPU设备的状态,直到前面的工作已经完成,状态寄存标记或已完成,CPU才能发送下一个字符和命令。
设备控制器是处理 CPU 传入和传出信号的系统。设备通过插头和插座连接到计算机,并且插座连接到设备控制器。设备控制器从连接的设备处接收数据,并将其存储在控制器内部的一些特殊目的寄存器(special purpose registers) 也就是本地缓冲区中。
1.特殊用途寄存器——仅为一项任务而设计的寄存器,例如:cs,ds,ss,es,fs,gs,eip,flag
2.通用目的寄存器——可以无限制使用的寄存器,例如:eax,ecx,edx,ebx,esi,edi,ebp,esp
5. 设备驱动程序和设备控制器的区别联系
联系:
- 通信中介:设备驱动程序充当着操作系统和设备控制器之间的通信中介,通过驱动程序,操作系统可以发出指令给控制器,同时也能接受到来自控制器状态更新或结果反馈。
- 抽象封装:控制器对驱动隐藏物理细节:硬盘驱动无需知道碟片转速→控制器处理马达控制;显卡驱动输出渲染指令→GPU控制器管理晶体管开关
- 协同工作:为使设备正常工作,设备驱动程序必须准确地理解和响应设备控制器产生的行为,反之亦然。意味着驱动程序需要针对特定型号的控制器编写,以便充分利用其特性并保证稳定性
6.Linux下的内存映射I/O
每个控制器都会有几个寄存器来和CPU进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接受数据、开启或者关闭设备等。通过从这些寄存器中读取信息,操作系统能够知道设备的状态,是否准备接受一个新命令等。
为了控制寄存器 ,许多设备都会有数据 缓冲区(data buffer),来提供读写。例如:在屏幕 上显示一个像素的常规方法是使用一个视频RAM,这一RAM基本上是一个数据缓冲区,用来供程序和操作系统进行写入。
CPU与设备寄存器和设备数据缓冲区进行通信的方式有两种:
-
每个控制寄存器都被分配一个I/O端口号(I/O port),这是一个8位或16位的整数。所有I/O端口的集合形成了受保护的I/O端口空间,只有操作系统才能进行访问,使用特殊的I/O指令如:
IN REG,PORT --CPU可以读取控制寄存器PORT中的内容并将结果放在CPU寄存器REG中 OUT PORT,REG --CPU可以将REG的内容写到控制寄存器中.
在这一方式中,内存地址空间的I/O地址空间是不相同的,如图:
-
使用PDP-11,它将所有控制寄存器映射到内存空间中
内存映射I/O的优点和缺点
以上两种寻址控制器的方式具有不同的优缺点,优点:
- 如果需要特殊的I/O指令进行读写设备控制器,那么访问这些寄存器需要使用汇编代码,因为在C或C++中不存在执行IN和OUT指令的方式,调用这样的过程增加了I/O的开销。在内存映射中,控制寄存器只是内存中的变量,在C语言中可以可以和其他变量一样进行寻址。
- 对于内存映射I/O,无需特殊的保护机制就能够阻止用户进程执行I/O操作。操作系统需要保证的是禁止把控制寄存器的地址空间放在用户的虚拟地址中即可。
- 对于内存映射I/O,可以引用内存的每一条指令也可以引用控制寄存器,便于引用
缺点:
- 大部分计算机现在都会有一些对于内存字的缓存。缓存一个设备控制器的代价是很大的。为了避免这种内存映射I/O的情况,硬件必须有选择性的禁用缓存。
- 如果仅仅只有一个地址空间,那么所有的内存模块(memory modules, 在计算机中,存储器模块是其上安装有存储器集成电路的印刷电路板)和所有的I/O设备都必须检查所有的内存引用来推断谁来进行响应。
7.直接内存访问(DMA)
无论一个CPU是否具有内存映射I/O,它都需要寻址设备以便进行数据交换。CPU可以从I/O控制器每次请求一个字节的数据,但是会浪费CPU时间,所以经常使用的方式是——直接内存访问(DMA)。如图,为了简化,假设CPU通过单一的系统总线访问所有设备和内存,该总线连接CPU、内存和I/O设备。
不论DMA控制器的物理地址在哪,它都能够独立于CPU从而访问系统总线,它包含几个可由CPU读写的寄存器,其中一个包括一个内存地址寄存器,字节计数寄存器和一个或多个控制寄存器。控制寄存器指定要使用的I/O端口,传送方向(从I/O设备读或写到I/O设备)、传送单位(每次一个字节或者一个字)以及在以此突发传送中的要传送的字节数。
DMA的原理:如果按数据块进行I/O,即需要传输大量数据时,就无须CPU的介入
。在这种情况下,我们可以让I/O设备与计算机内存进行直接数据交换。而CPU则可以去忙别的事情。这种将CPU的介入减少的I/O模式称为直接内存访问。
参考引用:
设备控制器 | 小白的编程之路
操作系统中 设备驱动程序和设备控制器之间的关系 - guanyubo - 博客园
linux_kernel_wiki/文章/Linux操作系统IO机制原理(流程图详解).md at main · 0voice/linux_kernel_wiki