在学 NIO 的时候,需要用到一点操作系统和计算机组成原理的知识,于是就又复习了一下计算机组成原理和操作系统。
可能一些程序员心中的程序执行模型是这样的: 在使用 IO 流复制文件的时候,都是认为程序将位于磁盘上的文件读入内存然后在输出的磁盘上指定的位置。这是一个相当粗糙而不精准的模型,在这个模型中 CPU 和操作系统被隐藏,好像根本就没有参与工作一样。程序并不能直接读取文件,它需要向操作系统的申请,操做系统会给程序回应。程序是无法直接接触到硬件的,在现代计算机中,一切资源都必须向操作系统申请。还有一些程序员认为 32 位操作系统最多只能够使用 4GB(232)的 RAM, 而 64 位操作系统则最多只能使用 2 64字节的 RAM。这是一种谬误,要解释清楚这些问题,并没有那么简单。
但是了解这些,将会让你成为一名大牛,你将看到隐藏在海面下的八分之七。
通用硬件组成
这张图是近期 Intel 系统产品族的模型,可能你还不大清楚这张模型图中有些名词的意思。但是不要担心,我将会为你逐个介绍。
- 总线
贯穿这个系统的是一组电子管道,称作总线,它携带信息字节并负责在各个部件间进行传递。通常总线被设计成传送定长的字节块,也就是字。字中的字节数(即字长) 是一个基本的系统参数,各个系统中都不尽相同。现在大多数机器字长要么是 4 个字节(32 位),要么是 8 个字节(64 位)。
我们常说从磁盘加载进内存,那么磁盘和内存是如何交换信息的呢,就是通过 I / O 总线。 - I/ O 设备
I/O(输入 / 输出) 设备是系统与外部与世界的联系通道,是主存和外部设备(例如磁盘驱动器、终端和网络) 之间复制数据的过程。输入操作是从 I / O 设备复制数据到主存,而输出操作是从主存复制到 I / O 设备。I/ O 是一种抽象的概念,网络从某种意义上来说,只是另一种 I / O 设备,是数据源和数据接收方。
每个 I / O 设备都通过一个控制器或适配器与 I / O 总线相连。控制器和适配器之间的区别主要在于它们的封装方式。控制器是 I / O 设备本身或者系统主板上的芯片组。而适配器则是一块插在主板插槽上的卡。无论如何,它们的功能都是在 I / O 总线与 I / O 设备之间传递信息。 - 处理器
中央处理单元(CPU),简称处理器,是执行存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),或称为程序计数器(PC)。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。
从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序程序计数器,使其指向下一条指令。处理器看上去是按照一个非常简单的指令执行模型来操作的,这个模型是由指令集架构决定的。
什么是指令集架构? 粗略的说这个就是我们所说的机器语言,但是要讲清楚指令集体系架构是什么并不容易,这不是本篇的主题,需要结合 CPU 去讲,会专门再开一门博客去讲。
在这个模型中,指令按照严格的顺序执行,而执行一条指令包含一系列的步骤。处理器从程序计数器指向的内存处读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新 PC,使其指向下一条指令,这条并不一定和内存中刚刚执行的指令相邻。
这样的简单操作并不多,它们围绕着主存、寄存器文件(register file) 和算术 / 逻辑单元(ALU) 进行。寄存器文件时一个小的存储设备,由一些单个字长的寄存器租场,每个寄存器都有唯一的名字。ALU 计算新的数据和地址值。下面是一些简单操作的例子,CPU 在指令的要求下可能会执行这些操作。 - 加载: 从主存复制一个字节或者到一个字到寄存器,以覆盖寄存器原来的内容。
- 存储: 从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖掉这个位置原来的内容
- 操作: 把两个寄存器的内容复制到 ALU,ALU 对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖这个位置上原来的内容。
- 跳转: 从指令本身中抽取一个字,并将这个字复制到 PC 中,以覆盖 PC 中原来的值。
顺便提一下 USB 也是总线: 通用串行总线。 - 主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。从物理上来说,主存是由一组动态随机存储器 (DRAM) 芯片组成的。从逻辑上来说,存储器上是一个线性的字节数组,每个字节都有唯一的地址(数组索引),这些地址是从零开始的,突然想起了 C 语言的指针。
操作系统简论与 IO 接口
各位还记得 hello world 吗? 相信这是很多学习高级语言的人,写的第一个程序。在键盘上输入文字在显式器上显示,然后程序被执行,在控制台输出。在这个过程中我们写的程序是没有直接访问显示器、磁盘、内存。取而代之的是,他们依靠操作系统的服务。这也是 java 中读文件的方法都是 native 方法的原因。在现代操作系统中,I/ O 是计算机完成各种功能的一个重要方面。处理器负责执行各种计算任务,并且通过内存总线操纵整个内存空间,但是仅仅这些还是不够的,还要有各种外部设备的参与才可使计算机做到真正 ” 有用 ”。尽管这些设备的功能和用途各式各样,但是处理器利用了标准的接口技术,通过设备的控制器与他们打交道。因此,从硬件层面上,处理器支持的接口技术是计算机系统能够协作运行的基础。
从软件层面上,操作系统提供相应的软件来操作设备的控制器,并且定义对应的软件接口或系统服务。使得应用程序可以方便地操纵或使用外部设备。操作系统的 I / O 系统复杂而精巧的,我们不可能用一篇博客来完全讲清楚,以上引入的概念只是为了介绍程序的执行过程和程序在执行过程中读写文件的时候都发生了些什么。现代的高级语言设计的十分精巧,经常让一些程序员有一种错觉,觉得读写文件时,就是程序将文件读入内存,然后对文件做操作,然后输出到磁盘。但是这种模型十分经不起推敲,已经不能说是粗糙,应该是错误的。
我们可以将操作系统看成应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作尝试必须经过操作系统。再强调一遍,所有应用程序对硬件的操作尝试必须经过操作系统。操作系统有两个基本功能: (1) 防止硬件被失控的程序滥用 (2) 向应用程序提供简单一致的机制来控制复杂而又通常不相同的低级硬件设备。
一个典型的 I / O 请求过程
绝大多数 I / O 系统并不涉及 I / O 系统的所有组件。典型的 I / O 请求首先从 ” 一个应用程序执行一个 I / O 相关的函数开始 ”, 我们可以将其理解为程序调用操作系统提供的 I / O 接口,然后该请求将由 I / O 管理器、一个或多个设备驱动程序和 HAL 来处理。什么,你问什么是 HAL? HAL: Hardware Abstraction Layer 硬件抽象层。最早由微软提出,微软发现,程序直接与硬件通信,是造成系统不稳定的主要原因。硬件抽象层是位于操作系统内核与硬件电路之间的接口层,其目的在于将硬件抽象化。它隐藏了特定平台的硬件接口细节, 为操作系统提供虚拟硬件平台, 使其具有硬件无关性, 可在多种平台上进行移植。
程序读写文件时发生了什么?
这里我不准备介绍 I /O 设备是如何工作的,这是一个庞大的主题。只做概要描述,便于理解程序在执行时发起 I / O 请求,请求读写文件时,CPU 从磁盘读数据时发生的步骤。CPU 使用一种称为内存映射 I /O(memory-mapped I/O)的技术向 I / O 设备发射命令。在使用内存映射 I / O 的系统中,地址空间中有一块地址是为与 I / O 设备通信保留的。每个这样的地址称为一个 I / O 端口(I/O port)。当一个设备连接到总线时,它与一个或多个端口相关联。
有了内存映射 I /O(memory—mapped I/O)这个概念之后,我们接着来介绍 CPU 从磁盘数据的步骤。我们知道 CPU 的执行单位是指令。假设磁盘控制器被映射到某个端口,随后,CPU 可能通过执行三个对地址 0xa0 的存储指令,发起磁盘读: 第一条指令是发送一个命令字,告诉磁盘发起一个读,同时还发送了其他的参数,例如当读完成时,是否中断 CPU(如果你不懂什么是中断的话,没关系,等着我)。第二条指令指明应该读的逻辑块号。第三条指令指明应该存储磁盘山区内容的主存地址。
当 CPU 发出了请求之后,在磁盘执行读的时候,CPU 出于等待状态,磁盘是很慢的,此时让 CPU 一直陷入等待是一种极大的浪费。操作系统的 CPU 调度器通常会让 CPU 去做其他事情。
在磁盘控制器收到来自 CPU 的读命令之后,它将逻辑块号翻译成一个扇区地址,读该扇区的内容,随后将这些内容直接传送到主存,不需要 CPU 的干涉。设备可以自己执行读或者写总线事务而不需要 CPU 的过程,我们称为直接内存访问 (Direct Memory Access,DMA)。这种数据传统称为 DMA 传送(DMA transfer)。写到这里突然想起 java NIO 中的 transferTo 方法,高效的文件拷贝。
试想如果是 CPU 再干涉,那么 CPU 就需要向内存发送写指令,而 CPU 不会被单个进程独占。
在 DMA 传送完成,磁盘扇区的内容被安全地存储在主存中以后,磁盘控制器通过给 CPU 发送一个中断信号来通知 CPU。
操作系统的结构与 CPU 态
结构
接下来我们来讲程序执行模型,也就是进程,程序是如何被执行起来的。要介绍程序是如何执行的,我们首先要介绍操作系统。首先我们来认识现代操作系统的逻辑结构, 那么什么是操作系统的逻辑结构呢? 那就是操作系统的设计和实现思路。一般来说有三种:
- 整体式结构
整体式结构以模块为基本单位构建,将系统拆分成一个一个模块。
特点:
- 模块设计、编码和调试独立
- 模块调用自由
- 模块通信多以全局变量形式完成
缺点:
信息传递随意, 维护和更新困难。
- 层次式结构
层次结构的软件例子: TCP/IP 协议栈
所有功能模块按照调用次序排成若干层,相邻层间只有单向依赖或单向调用。
层次结构的优点:
- 结构清晰,避免循环调用
- 整体问题局部化,系统的正确性容易保证
- 有利于操作系统的维护、扩充、移植。
- 微内核结构(客户 / 服务器结构)
操作系统 = 微内核 + 核外服务器。
微内核: 足够小, 提供 OS 最基本的核心功能和服务。
- 实现与硬件紧密相关的处理
- 实现一些较基本的功能
- 负责客户和服务器间的通信
核外服务器: 完成 OS 的绝大部分服务功能,等待应用程序提出请求。
若干服务器或若干进程共同构成。
例如: 进程 / 线程服务器,设备管理服务器。以进程的形式运行在服务态。而我们熟知的 Linux 则是宏内核,当然你也可以称之为单体内核。windows 则是混合内核。而 Minix OS 是一个典型的微内核结构。Linux、windows、Android 的界面属于外壳,而不是内核,对外提供服务。内核是操作系统的核心,负责管理系统的进程、内存、设备驱动程序、文件和网络系统。
CPU 态
CPU 态 (Mode) 描述了什么:
- CPU 的工作状态
- 对资源和指令使用权限的描述
有一些特权指令,不能随便被随便被应用程序使用。
比如 I /O, 停止 CPU 的工作。只有 CPU 工作在一种权限很高的态下面才能获准调用这些指令。
态的分类:
-
内核态(Kernel mode)
- 能够访问所有资源和执行所有指令
- 管理程序 /OS 内核都工作在内核态下面。
-
用户态(user model 目态)
- 仅能够访问部分资源,其他资源受限
- 用户程序。
-
管态(Supervisor model)
- 介于核态和用户态之间
-
用户态向核态转换:
- 用户进程请求 OS 提供服务
- 发生中断
- 用户进程发生错误(内部中断)
- 用户态企图执行特权指令
-
核态向用户态转换的情形
- 一般是执行中断返回
操作系统按 ” 进程 ” 来区分 CPU 的状态。为什么对 CPU 进行分态呢? 这也是从操作系统的稳定性来讲的,历史经验告诉我们,将硬件开放给程序,会破坏系统的稳定性。我们大致的举一个例子, 程序想要访问磁盘上的某个文件,首先此时程序请求操作系统的接口,也就是请求操作系统的服务,那么此时 CPU 态就从用户态转为内核态,但是你知道磁盘的速度相对于 CPU 来说还是太慢了,可能磁盘还没有找到对应的文件,CPU 在操作系统的调度下,就去执行其他进程了,等到在转向程序,首先要恢复上下文,再接着还要从用户态转向内核态,频繁的从用户态转向内核态是一种巨大的消耗,这也是 DMA 出现的原因,减少 CPU 参与读写的过程。
参考文献:
- 《深入理解计算机系统》
- 《Windows I/ O 系统》南京理工大学 王泽玮
- 计算机操作系统精讲 华中科技大学 软件学院 苏曙光
- 操作系统 清华大学_向勇、陈渝教授