Amiya · 2023年08月01日 · 北京市海淀区

剖析虚幻渲染体系(18)- 操作系统(1)(上)

18.1 操作系统综述

迄今为止,博主在博客中阐述的内容包含渲染技术、性能优化、图形API、Shader、GPU、游戏引擎架构、图形驱动等等技术范畴的内容,这些内容都仅仅局限于单个应用程序之中,常常让人有”只缘身在此山中“的感叹。现在是时候更进一步了——进入操作系统(Operating System,OS)的范畴,以更高的层次去看待渲染体系,以便我们能够高屋建瓴,练就深厚的技术内功。

image.png

本篇将站在应用层开发者的视角,去阐述Windows、Linux等操作系统的相关技术内幕(如果是操作系统开发者,则博主不认为是很贴切的目标读者),主要包含但不限于以下内容:

  • 操作系统的架构。
  • 操作系统的概念。
  • 操作系统的技术。
  • 操作系统的机制和原理。
  • 操作系统的UE封装和实现。

操作系统经历了数十年的发展历程,从最初的形态,到当前的琳琅满目,无论是技术、体验、应用等方面都有了质的飞跃。

UNIX操作系统最早于20世纪70年代开发,是一个支持多个用户同时使用的多任务操作系统。它的特点是基于命令行,支持同时运行数千个小程序,易于从单个程序创建管道,内置多用户支持和分区。面临的挑战是基于命令行,寻找帮助和文档可能很繁琐,许多不同的变体(下图)。

image.png
Unix系统和它的衍生体。

同样流行且知名的还有Windows、MacOS、Android、Linux等操作系统。

18.1.1 操作系统功能

操作系统位于应用程序和硬件之间,调解访问并抽象出接口,程序通过陷阱或例外请求服务,设备通过中断请求关注等。操作系统执行许多功能,具体而言,有:

  • 实现用户界面。
  • 在用户之间共享硬件。
  • 允许用户之间共享数据。
  • 防止用户相互干扰。
  • 在用户之间调度资源。
  • 促进I/O操作。
  • 从错误中恢复。
  • 资源存储核算。
  • 促进并行操作。
  • 组织数据以实现安全快速访问。
  • 处理网络通信。

image.png

操作系统是一组程序,用于控制应用程序的执行,并充当计算机用户和计算机硬件之间的中介。操作系统是一种管理计算机硬件并为应用程序运行提供环境的软件,示例有Windows、Windows/NT、Linux、OS/2和MacOS。OS涉及的主题常常是以下几方面:

image.png

操作系统提供以下几大类功能:

  • 作业管理:识别作业,确定优先级,确定主内存的可用性,并准备运行作业或程序的时间表。
  • 进程管理:减少计算机系统处理器和输入/输出设备的空闲时间。
  • 输入/输出管理:在各种设备及其驱动程序的帮助下,管理计算机的输入和输出流。
  • 数据管理:跟踪磁盘和其他存储设备上的数据。
  • 文件管理:负责文件相关活动,如:创建、删除、操作文件和目录。
  • 内存管理:根据不同程序的需要分配和释放可用内存。
  • 虚拟存储:在不增加物理大小的情况下增加主内存的容量,使用虚拟内存存储指令和数据。
  • 安全管理:为敏感数据提供密码安全,以阻止未经授权的访问。
  • 在线处理:等待用户的操作指令,并立即执行。

常见的模块或子系统有内存管理、IO系统、文件系统、进程、线程、中断、安全、信息服务等等,它们之间有着错综复杂的关联:

image.png

操作系统的目标是:1、使计算机系统方便用户使用;2、有效地使用计算机硬件;3、执行用户程序并使解决用户问题更容易。

可以分别从用户、系统视角来看待操作系统。

计算机的用户视图取决于使用的界面。一些用户可能使用PC,在这种情况下,系统设计为只有一个用户可以利用资源,并且主要是为了便于使用,其中主要关注的是性能而不是资源利用率。一些用户可能使用连接到大型机或小型计算机的终端,其他用户可以通过其他终端访问同一台计算机,这些用户可以共享资源和交换信息。在这种情况下,操作系统被设计为最大限度地利用资源,以便有效地利用所有可用的CPU时间、内存和I/O。其他用户可以坐在工作站上,连接到其他工作站和服务器的网络,在这种情况下,操作系统被设计为在个人可见性和资源利用率之间进行折衷。

而从系统角度来看,可以将系统视为资源分配器,也就是说,一个计算机系统有许多可用于解决问题的资源。操作系统充当这些资源的管理器,必须决定如何将这些资源分配给程序和用户,以便能够高效、公平地操作计算机系统。操作系统的不同视角是,它需要控制各种I/O设备和用户程序,即操作系统是用于管理用户程序执行的控制程序,以防止错误和不当使用计算机。资源可以是CPU时间、内存空间、文件存储空间、I/O设备等。

操作系统服务是操作系统为程序的执行提供了环境,也为程序提供一些服务,每个操作系统提供的服务可能与其他操作系统不同,使得编程任务更容易。

image.png
操作系统服务概览。

操作系统提供的各种服务如下:

  • 程序执行:系统必须能够将程序加载到主内存分区并运行该程序,程序必须能够正常终止此执行以成功执行,或异常终止以显示错误。
  • I/O操作:完成设备分配和I/O设备控制任务,提供通知设备错误、设备状态等。
  • 文件系统操作:完成打开文件、关闭文件的任务,程序需要按名称创建和删除文件,允许文件操作,如读取文件、写入文件、附加文件。
  • 通信:在同一计算机系统上或在计算机网络上的不同计算机系统之间完成进程间通信的任务,在安全模式下提供消息传递和共享内存访问。
  • 错误检测:操作系统应针对任何类型的溢出(如算术溢出)采取适当的操作;除以零错误、访问非法内存位置和用户CPU时间过大。完成错误检测和恢复任务(如果有),例如打印机卡纸或缺纸。跟踪CPU、内存、I/O设备、存储设备、文件系统、网络等的状态。如果出现致命错误,如RAM奇偶校验错误、功率波动,则中止执行。
  • 资源分配:完成资源分配到多个作业的任务,在资源使用后或作业终止时回收分配的资源,当多个用户登录到系统时,必须将资源分配给每个用户。对于资源在各个进程之间的当前分配,操作系统使用CPU调度运行时间,以确定将为哪个进程分配资源。

image.png

  • 账户:操作系统跟踪哪些用户使用了多少和哪种计算机资源,维护系统活动日志以进行性能分析和错误恢复。
  • 保护:完成保护系统资源免受恶意使用的任务,采用安全方案防止未经授权的访问/用户进行安全计算,使用登录密码和注册对合法用户进行身份验证。操作系统负责硬件和软件保护,操作系统保护存储在多用户计算机系统中的信息。
  • 系统调用:系统调用提供进程和操作系统之间的接口,它们通常以汇编语言指令的形式提供。有些系统允许直接从高级语言程序(如C、BCPL和PERL等)进行系统调用,根据使用的计算机,系统调用以不同的方式进行。系统调用可以大致分为5大类:
  • 进程管理和控制:

    • 结束,中止:运行中的程序需要能够正常(结束)或异常(中止)执行。
    • 加载、执行:执行一个程序的进程或作业可能要加载并执行另一个程序。
    • 创建、终止进程:有一个系统调用指定用于创建新进程或作业(创建流程或提交作业),或者终止创建的作业或进程。
    • 获取或设置进程属性:如果创建新作业或进程,我们应该能够控制其执行,以便确定和重置作业或进程的属性。
    • 等待时间:创建新作业或进程后,我们可能需要等待它们完成执行(等待时间)。
    • 等待事件、信号事件:我们可以等待特定事件发生(等待事件),然后,作业或进程在该事件发生时发出信号(信号事件)。
    • 分配、释放内存。
  • 文件管理/文件操作/文件处理:

    • 创建文件,删除文件:我们首先需要能够创建和删除文件。两个系统调用都需要文件名及其某些属性。
    • 打开文件,关闭文件:创建文件后,我们需要打开并使用它。当不再使用该文件时,我们将其关闭。
    • 读取、写入、重新定位文件:打开后,我们还可以读取、写入或重新定位文件(回放或跳到文件末尾)。
    • 获取文件属性,设置文件属性:对于文件或目录,我们需要能够确定各种属性的值,并在必要时重置它们。需要两个系统调用获取文件属性和设置文件属性
  • 设备管理:

    • 请求设备,释放设备:如果系统有多个用户,首先请求设备,在完成设备后,必须释放它。
    • 读取、写入、重新定位:一旦设备被请求并分配,就可以读取、写入和重新定位设备。
  • 信息维护/管理:

    • 获取时间或日期、设置时间或日期:大多数系统都有一个系统调用来返回当前的日期和时间或设置当前的日期与时间。
    • 获取系统数据,设置系统数据:其他系统调用可能会返回有关系统的信息,如当前用户数、操作系统版本号、可用内存量等。
    • 获取进程属性,设置进程属性:操作系统保留有关其所有进程的信息,并且有系统调用来访问这些信息。
  • 通讯管理:

    • 创建、删除连接。
    • 发送、接收消息。
    • 连接、分离远程设备(安装/远程登录)。
    • 传输状态信息(字节)。

通讯管理有两种模式:

  • 消息传递模式:信息通过操作系统提供的进程间通信设施进行交换,网络中的每台计算机都有一个已知的名称。类似地,每个进程都有一个进程名,该进程名被转换为操作系统可以引用的等效标识符。get-host-id和get-processed系统调用来执行此转换,然后将这些标识符传递给文件系统提供的通用打开和关闭调用,或传递给特定的打开连接系统调用。收件人进程必须授予其权限,才能与接受连接呼叫进行通信。通信源称为客户端和接收方,称为服务器,通过读消息和写消息系统调用交换消息。关闭连接调用终止连接。
  • 共享内存模式:进程使用映射内存系统调用访问其他进程拥有的内存区域,他们通过读写共享区域中的数据来交换信息,这些进程确保它们不会同时写入同一位置。

image.png

18.1.2 操作系统种类

image.png

操作系统可分为以下几类。

18.1.2.1 批处理操作系统

在批处理系统(Batch System)类型的操作系统中,用户定期(如每天、每周、每月)将作业提交到一个中心位置,该位置的系统用户不直接与计算机系统交互。为了加快处理速度,具有类似需求的作业被分批处理,并作为一个组在计算机中运行。因此,程序员将把程序留给操作员,每个作业的输出将发送给适当的程序员。这种类型的主要任务是自动将控制权从一个作业转移到下一个作业。

image.png

此种操作系统的优势是简单、连续的作业调度,尽量减少人为干预,由于作业成批处理,提高了性能和系统吞吐量。缺点是从用户的角度来看,由于批处理,周转时间可能会很长,程序调试困难,作业可以进入无限循环,可能会损坏显示器,由于缺乏保护方案,一项作业可能会影响待定作业。

应用案例是工资系统、银行对账单等。注意,周转时间是指用户提交流程或作业与完成该流程或作业之间所用的时间。

联机的全称是在线同步外围设备操作(Simultaneous Peripheral Operation On-Line,SPOOL),是暂时保存数据以供设备、程序或系统使用和执行的过程,数据被发送到其他易失性(临时存储器)存储器并存储在其中,直到程序或计算机请求执行。

18.1.2.2 分时操作系统

时间共享系统(Time-Sharing)也被称为分时系统、多用户系统,提供用户与系统之间的在线通信,用户直接发出指令并接收中间响应,因此称为交互式系统。它允许多个用户同时共享计算机系统,CPU在几个程序之间快速多路复用,这些程序保存在内存和磁盘上,一个程序在磁盘上交换内存和内存。

CPU通过在多个作业之间切换来执行多个作业,但切换频繁,用户可以在每个程序运行时与之交互。交互式计算机系统提供用户和系统之间的直接通信,用户直接使用键盘或鼠标向操作系统或程序发出指令,并等待即时结果。因此,响应时间将很短。分时系统允许许多用户同时共享计算机,由于此系统中的每个操作都很短,因此每个用户只需要很少的CPU时间。系统可以快速地从一个用户切换到另一个用户,因此每个用户都会感觉整个计算机系统都是专门用于自己的使用的,即使它是由许多用户共享的。

image.png

在上图中,用户5处于活动状态,但用户1、用户2、用户3和用户4处于等待状态,而用户6处于就绪状态。一旦用户5的时间片完成,时间片就移到下一个就绪用户,即用户6。在此状态下,用户2、用户3、用户4和用户5处于等待状态,用户1处于就绪状态。这个过程以同样的方式继续,以此类推。

分时系统的优点是有效共享和利用计算机资源,对许多用户的快速响应时间,CPU空闲时间完全消除,适合在线数据处理和用户对话。

分时系统的缺点是比多道程序操作系统更复杂,系统必须具有内存管理和保护,因为多个作业同时保存在内存中,分时系统还必须提供文件系统,因此需要磁盘管理,它为需要复杂CPU调度方案的并发执行提供了机制。示例:Multics、UNIX等。

18.1.2.3 实时操作系统

实时系统(Real-Time System)的特点是提供即时响应,保证关键任务按时完成。对于要在计算机上执行的每个功能,此类型必须具有已知的最大时间限制。当处理器的操作或数据流有严格的时间要求时,使用实时系统,实时系统可用作专用应用中的控制设备。

当处理器的操作或数据流有严格的时间要求时,使用实时系统,控制科学实验、医学成像系统和一些显示系统的系统是实时系统。传感器将数据传送到计算机,计算机分析数据并调整控件以修改传感器输入。

实时系统的缺点是:只有当实时系统在时间限制内返回正确的结果时,才认为它能够正确运行,辅助存储有限或丢失,而数据通常存储在短期内存或ROM中,缺少高级操作系统功能。

实时系统有两种类型:

  • 硬实时系统:保证关键任务按时完成,例如突如其来的任务发生在一个突然的时刻。
  • 软实时系统:是一种限制较少的实时系统,其中关键任务的优先级高于其他任务,并在计算之前保持该优先级。与硬实时系统相比,这些系统的实用性更为有限。偶尔错过最后期限是可以接受的。

示例:QNX、VX工作,数字音频或多媒体包含在这一类别中,是一种特殊用途的操作系统,其中对处理器的操作有严格的时间要求。实时操作系统有明确定义的固定时间限制,处理必须在时间限制内完成,否则系统将失败。只有在时间限制内返回正确的结果,实时系统才能正常运行。这些系统的特点是将时间作为关键参数。

image.png

实时操作系统的优点:

  • 最大化消费:设备和系统的最大利用率,从而从所有资源中获得更多输出。
  • 任务转移:在这些系统中分配给转移任务的时间非常少。例如,在较旧的系统中,将一个任务转移到另一个任务大约需要10微秒,而在最新的系统中则需要3微秒。
  • 专注于应用程序:专注于运行应用程序,而对队列中的应用程序不那么重要。
  • 嵌入式系统中的实时操作系统:由于程序的大小很小,RTOS也可以用于嵌入式系统,如传输和其他系统。
  • 无错误:这些类型的系统是无错误的。
  • 内存分配:内存分配在这些类型的系统中管理得最好。

实时操作系统的缺点:

  • 有限的任务:同一时间运行的任务很少,并且它们很少集中在少数应用程序上,以避免错误。
  • 使用繁重的系统资源:有时系统资源不是很好,而且也很昂贵。
  • 复杂算法:算法非常复杂,设计者很难写下去。
  • 设备驱动和中断信号:需要特定的设备驱动程序和中断信号来尽早响应中断。
  • 线程优先级:设置线程优先级并不好,因为这些系统不太容易切换任务。

实时操作系统的例子有:科学实验、医学成像系统、工业控制系统、武器系统、机器人、空中交通控制系统等。

image.png

18.1.2.4 多道程序操作系统

多程序设计(Multiprogramming)概念通过组织作业增加CPU利用率,CPU总有一个作业要执行。操作系统同时在内存中保留多个作业,如下图所示。

image.png

这组作业是作业池中保留的作业的子集,操作系统拾取并开始执行内存中的一个作业,当一个作业需要等待时,CPU只需切换到另一个作业,依此类推。

多道程序操作系统很复杂,因为操作系统为用户做出决定,称为作业调度。如果多个作业准备同时运行,系统将从中选择一个,称为CPU调度。其他功能包括内存管理、设备和文件管理。

多道程序系统的优点是有效的资源利用率(CPU、内存、外围设备),消除或最小化浪费的CPU空闲时间,增加的吞吐量(在给定的时间间隔内,相对于提交执行的作业数量,执行的作业数)。

多道程序系统的缺点是在程序执行期间,不提供用户与计算机系统的交互,磁盘技术的引入解决了这些问题,而不是将卡片从读卡器读入磁盘,这种处理形式称为联机,复杂且相当昂贵。

18.1.2.5 多处理器/并行/紧耦合系统

这些系统有多个处理器进行紧密通信,共享计算机总线、时钟、内存和外围设备,例如UNIX、LINUX。多处理器系统有三个主要优点:

  • 增加的吞吐量:每单位时间计算的进程数。通过增加处理器的数量,可以在更短的时间内完成工作。N个处理器的加速比不是N,但小于N。因为在保持所有部件正常工作时会产生一定的开销。
  • 提高可靠性:如果功能可以在多个处理器之间正确分配,那么一个处理器的故障不会停止系统,而是会降低系统速度。这种即使发生故障仍能继续运行的能力使系统具有容错能力。
  • 经济伸缩:多处理器系统可以节省资金,因为它们可以共享外围设备、存储和电源。

多处理系统的类型包括:

  • 对称多处理(Symmetric Multiprocessing,SMP):每个处理器运行操作系统的相同副本,这些副本根据需要彼此通信。例如:Encore版本的UNIX for multi-max计算机。实际上,包括Windows NT、Solaris、Digital UNIX、OS/2和LINUX在内的所有现代操作系统现在都支持SMP。
  • 非对称多处理(主-从处理器,Master – Slave Processors):每个处理器都是为特定任务设计的。主处理器控制系统,并将工作安排和分配给从处理器。Ex-Sun的操作系统SUNOS版本4提供非对称多处理。

18.1.2.6 分布式/松耦合系统

与紧耦合系统相比,处理器不共享内存或时钟,相反,每个处理器都有自己的本地内存。处理器通过各种通信线路(如高速总线或电话线)相互通信,分布式系统的功能依赖于网络。通过能够通信,分布式系统能够共享计算任务并为用户提供丰富的功能集,网络因所使用的协议、节点和传输介质之间的距离而异。

TCP/IP是最常见的网络协议,分布式系统中的处理器大小和功能各不相同,可以是微处理器、工作站、小型计算机和大型通用计算机,网络类型基于节点之间的距离,例如LAN(在房间、楼层或建筑物内)和WAN(在建筑物、城市或国家之间)。
image.png

分布式操作系统的优点:资源共享,计算速度加快,负载共享/负载平衡,可靠性,通讯。

分布式操作系统的缺点:主网络故障将停止整个通信,为了建立分布式系统,所使用的语言还没有很好的定义,这些类型的系统并不容易获得,因为它们非常昂贵。不仅底层软件非常复杂,而且还没有被很好地理解。示例是LOCUS等。

18.1.2.7 网络操作系统

这些系统在服务器上运行,并提供管理数据、用户、组、安全、应用程序和其他网络功能的能力。此类操作系统允许通过小型专用网络共享访问文件、打印机、安全、应用程序和其他网络功能。网络操作系统的另一个重要方面是,所有用户都清楚底层配置、网络中所有其他用户的配置、他们的个人连接等,这就是为什么这些计算机通常被称为紧耦合系统的原因。

image.png

网络操作系统的优点:高度稳定的集中式服务器,安全问题通过服务器处理,新技术和硬件升级很容易集成到系统中,可以从不同位置和类型的系统远程访问服务器。网络操作系统的缺点:服务器成本高昂,用户必须依赖中央位置进行大多数操作,需要定期维护和更新。

网络操作系统的示例有Microsoft Windows Server 2003、Microsoft Windows Server 2008、UNIX、Linux、Mac OS X、Novell NetWare和BSD等。

18.1.3 操作系统结构

操作系统体系结构的设计传统上遵循关注点分离原则,这一原则建议将操作系统结构化为相对独立的部分,这些部分提供简单的单个功能,从而使设计的复杂性保持可控。

18.1.3.1 简单结构

有几个商业系统没有定义良好的结构,例如操作系统开始时是小的、简单的和有限的系统,然后扩展到超出其原始范围。MS-DOS就是这种系统的一个例子,没有仔细地划分成模块。另一个有限结构的例子是UNIX操作系统,没有CPU执行模式(用户和内核),因此应用程序中的错误可能会导致整个系统崩溃。

image.png

18.1.3.2 单片结构

在这个模型中,对于每个系统调用,都有一个服务过程来处理和执行它。实用程序执行多个服务程序所需的操作,例如从用户程序中获取数据。程序分为三层,如下图所示。

image.png

操作系统体系结构的单片设计不适合操作系统的特殊性质。尽管设计遵循关注点分离,但没有尝试限制授予操作系统各个部分的权限,整个操作系统以最大权限执行。

单片操作系统内的通信开销与任何其他软件内的通信开支相同,被认为相对较低。CP/M和DOS是单片操作系统的简单示例,CP/M和DOS都是与应用程序共享单个地址空间的操作系统。在CP/M中,16位地址空间以系统变量和应用程序区域开始,以操作系统的三个部分结束,即CCP(控制台命令处理器)、BDOS(基本磁盘操作系统)和BIOS(基本输入/输出系统)。在DOS中,20位地址空间从中断向量数组和系统变量开始,然后是DOS的常驻部分和应用程序区域,最后是视频卡和BIOS使用的内存块。

image.png

18.1.3.3 分层结构

在分层方法中,操作系统被划分为多个层(级别),每个层都构建在较低层之上。底层(第0层)是硬件,最顶层(第N层)是用户界面。

image.png
操作系统的分层通用架构。

分层方法的主要优点是模块化,层的选择使得每个用户只具有较低层的功能(或操作)和服务。这种方法简化了调试和系统验证,即可以调试第一层,而不必考虑系统的其余部分。一旦调试了第一层,就假定它在调试第二层时正常工作,依此类推。如果在调试特定层的过程中发现错误,则该错误必须位于该层上,因为它下面的层已被调试。

这样,当系统被分解为多个层次时,系统的设计和实现就简化了。每个层仅使用较低层提供的操作来实现,层不需要知道这些操作是如何实现的;它只需要知道这些操作是做什么的。分层方法首先在操作系统中使用。它被定义为六层。

image.png

分层方法的主要缺点:

1、主要困难在于对层的仔细定义,因为一个层只能使用它下面的那些层。例如,虚拟内存算法使用的磁盘空间的设备驱动程序必须低于内存管理例程的级别,因为内存管理需要使用磁盘空间的能力。

2、效率低于非分层系统(每一层都会增加系统调用的开销,最终的结果是系统调用比非分层系统花费的时间更长)。

其中,Unix系统的架构如下图所示:

image.png

Unix可分为内核和系统程序。Unix内核包括系统资源管理、接口和设备驱动程序,如CPU调度、文件系统、内存管理和I/O管理。

Linux是针对Intel 386/486/Pentium机器的完整Unix克隆,充当计算机系统的硬件和软件之间的通信服务,其内核包含了任何操作系统中所期望的所有特性,部分功能包括:

  • 多任务处理(一种在多个独立作业之间共享单个处理器的技术)。
  • 虚拟内存(允许重复、扩展使用计算机的RAM以提高性能)。
  • 快速TCP/IP驱动程序(用于快速通信)。
  • 共享库(使应用程序能够共享公共代码)。
  • 多用户能力(意味着数百人可以通过网络、互联网、笔记本电脑/计算机或连接到这些计算机串行端口的终端同时使用计算机)。
  • 保护模式(允许程序访问物理内存,并保护系统的稳定性)。

Windows XP是一个基于增强技术的多任务操作系统,集成了Windows 2000的优点,如基于标准的安全性、可管理性和可靠性,以及Windows 98和Windows Me的最佳功能,如即插即用和易于使用的用户界面。Windows XP的体系结构如下图所示,采用分层结构,由硬件抽象层、内核层、执行层、用户模式层和应用程序组成。

image.png

Windows XP的每个内核实体都被视为一个对象,由执行程序中的对象管理器管理。用户模式应用程序可以通过进程中的对象句柄调用内核对象。使用内核对象来提供基本服务,以及对客户端-服务器计算的支持,使Windows XP能够支持多种应用程序。Windows XP还提供虚拟内存、集成缓存、抢占式调度、更强的安全模式和国际化功能。

image.png
Windows和Windows Vista架构图。

18.1.3.4 微内核结构

通过删除内核的所有不重要部分并将其作为系统级和用户级程序实现来构建操作系统,通常提供最少的进程和内存管理以及通信设施, 操作系统组件之间的通信通过消息传递提供。

微内核的优点是扩展操作系统变得容易得多,对内核的任何更改都会减少,因为内核更小,微内核还提供了更高的安全性和可靠性。主要缺点是由于消息传递增加了系统开销,性能较差。

image.png

MINIX 3微内核只有大约12000行C语言和1400行汇编语言,用于捕捉中断和切换进程等非常低级的功能。C代码管理和调度进程,处理进程间通信(通过在进程之间传递消息),并提供一组大约40个内核调用,以允许操作系统的其余部分完成其工作。这些调用执行诸如将处理程序挂接到中断、在地址空间之间移动数据以及为新进程安装内存映射等功能。MINIX 3的进程结构如下图所示,内核调用处理程序标记为Sys。时钟的设备驱动程序也在内核中,因为调度程序与它紧密交互。其他设备驱动程序作为单独的用户进程运行。

image.png

Solaris结构图如下:

image.png

18.1.3.5 客户端-服务器结构

微内核思想的一个微小变化是区分两类进程,即服务器(每个进程都提供一些服务)和客户端(使用这些服务)。此模型称为客户机-服务器模型,通常最底层是微内核(但非必需),其本质是客户端进程和服务器进程的存在。

客户端和服务器之间的通信通常是通过消息传递进行的。为了获得服务,客户端进程构造一条消息,说明它想要什么,并将其发送到适当的服务。然后,该服务完成工作并返回答案。如果客户机和服务器碰巧在同一台机器上运行,则可以进行某些优化,如消息传递。

这种想法的一个明显的概括是让客户端和服务器运行在不同的计算机上,通过局域网或广域网连接,如下图所示。由于客户端通过发送消息与服务器通信,客户端不需要知道消息是在自己的机器上本地处理的,还是通过网络发送到远程机器上的服务器。就客户而言,在这两种情况下都会发生同样的事情:发送请求,然后回复。因此,客户机-服务器模型是一种抽象,可以用于单个机器或机器网络。

image.png

18.1.3.6 虚拟机

image.png
虚拟机涉及的概念。

虚拟机采用分层方法得出其逻辑结论,将硬件和操作系统内核视为硬件,提供与底层裸硬件相同的接口。操作系统产生了多个进程的错觉,每个进程都使用自己的(虚拟)内存在自己的处理器上执行,共享物理计算机的资源以创建虚拟机。CPU调度可以创建用户拥有自己处理器的外观。联机和文件系统可以提供虚拟读卡器和虚拟行打印机。普通用户分时终端充当虚拟机操作员的控制台。

image.png

虚拟机概念提供了对系统资源的完全保护,因为每个虚拟机都与所有其他虚拟机隔离。然而,这种隔离不允许直接共享资源。虚拟机系统是操作系统研究和开发的完美工具。系统开发是在虚拟机上进行的,而不是在物理机上进行,因此不会中断正常的系统操作。虚拟机概念很难实现,因为需要为底层机器提供精确的副本。它的缺点是虚拟机包括由于大量模拟虚拟机操作而增加的系统开销,VM OS的效率取决于VM监视器必须模拟的操作数。虚拟机可分为进程和系统两个级别:

image.png

VM/370是于1979由Seawright和MacKinnon推出的虚拟机,它基于一个敏锐的观察:分时系统提供(1)多道程序设计和(2)扩展机器,与裸硬件相比,具有更方便的接口。VM/370的本质是将这两个功能完全分离。

系统的核心,即虚拟机监视器,在裸硬件上运行并执行多道程序设计,向上一层提供的不是一个,而是几个虚拟机,如图1-28所示。然而,与所有其他操作系统不同,这些虚拟机不是扩展机,具有文件和其他漂亮的功能。相反,它们是裸硬件的精确副本,包括内核/用户模式、I/O、中断以及真实机器所拥有的所有其他内容。

image.png

虚拟化在Web托管领域也很流行。如果没有虚拟化,Web托管客户就不得不在共享托管和专用托管之间进行选择。当一家网络托管公司提供虚拟机出租时,一台物理机可以运行许多虚拟机,每个虚拟机看起来都是一台完整的机器。租用虚拟机的客户可以运行他们想要的任何操作系统和软件,但成本仅为专用服务器的一小部分(因为同一物理机同时支持多个虚拟机)。

虚拟化的另一个用途是为那些希望能够同时运行两个或更多操作系统(例如Windows和Linux)的最终用户提供的,因为他们喜欢的一些应用程序包在一个上运行,而另一些在另一个上运行。这种情况如图下图(a)所示,其中术语“虚拟机监控器”已重命名为类型1虚拟机监控程序,现在常用的是,因为“虚拟机监控”需要的击键次数比人们现在准备好的要多。

image.png
(a) 1类虚拟机监控程序。(b) 纯类型2管理程序。(c) 一个实用的2型管理程序。

image.png
虚拟计算机的一种架构示例。

使用虚拟机的另一个领域是运行Java程序,但方式有所不同。当Sun Microsystems发明Java编程语言时,它还发明了一种称为JVM(Java Virtual Machine)的虚拟机(即计算机体系结构)。Java编译器为JVM生成代码,然后通常由软件JVM解释器执行。这种方法的优点是,JVM代码可以通过Internet发送到任何具有JVM解释器并在其中运行的计算机,例如,如果编译器生成了SPARC或x86二进制程序,那么它们就不可能如此容易地发布和运行。使用JVM的另一个优点是,如果解释器实现正确(并不是小事),可以检查传入的JVM程序的安全性,然后在受保护的环境中执行,这样它们就不会窃取数据或造成任何损坏。

18.1.4 操作系统组件

游戏开发是一个非常复杂的过程和项目,如果没有模块化架构和高效开发环境的帮助,几乎是不可行的。当前市面上的各种产品共享由以下抽象层次组成的结构,从上到下分别是:游戏应用、游戏引擎、图形API、操作系统、设备驱动、硬件设备。

image.png

下图是更加详细的层级模块,其中操作系统(OS)处于图形API等第三方SDK和驱动之间,充当着承上启下的重要作用和通讯桥梁,是整个计算机层级架构极其重要的组成部分。

image.png

计算机系统可以分为四个部分:硬件、操作系统、应用程序和用户。系统组件的抽象视图如图1所示。

1、硬件:如CPU、内存和I/O设备。

2、操作系统:在计算机系统的操作中提供正确使用硬件的方法,类似于政府。

3、应用程序:解决用户的计算问题,如:编译器、数据库系统和web浏览器。

4、用户:人、机器或其他计算机。

image.png
在顶层,计算机由处理器、内存和I/O组件组成,每种类型有一个或多个模块。这些组件以某种方式互连,以实现计算机的主要功能,即执行程序。有四个主要结构要素:

  • 处理器:控制计算机的操作并执行其数据处理功能。当只有一个处理器时,它通常被称为中央处理单元(CPU)。
  • 主存储器:存储数据和程序。易丢失,当计算机关闭时,内存中的内容会丢失。相反,即使计算机系统关闭,磁盘内存的内容也会保留。主存储器也称为实存储器或主存储器。
  • I/O模块:在计算机及其外部环境之间移动数据外部环境由各种设备组成,包括辅助存储器设备(如磁盘)、通信设备和终端。
  • 系统总线:提供处理器、主存储器和I/O模块之间的通信。

image.png

操作系统所处的层级如下图所示:

image.png

通用系统架构如下图,展示了Windows的总体架构,包含了用户模式和内核模式组件。

image.png
上图中出现的概念的简要说明如下:

  • 用户模式
  • 用户进程。是基于镜像文件(image file)的普通进程,在系统上执行,例如Notepad.exe、cmd.exe、explorer.exe等。
  • 子系统DLL。子系统DLL是实现子系统API的动态链接库(DLL),子系统是内核公开的功能的特定视图。从技术上讲,从Windows 8.1开始,只有一个子系统——Windows子系统。子系统dll包括众所周知的文件,如kernel32.dll、user32.dll、gdi32.dll,advapi32.dll和combase.dll和许多其他dll。它们主要实现了Windows的官方API。
  • NTDLL.DLL。实现Windows本机API的系统范围DLL,是代码的最底层,仍处于用户模式,最重要的作用是将系统调用转换到内核模式,还实现了堆管理器、映像加载器和用户模式线程池的某些部分。
  • 服务进程。服务进程是正常的Windows进程,与服务控制管理器(SCM,在services.exe中实现)通信,并允许对其生命周期进行一些控制。SCM可以启动、停止、暂停、恢复并向服务发送其他消息。
  • 系统进程。系统进程是一个概括术语,用于描述通常“就在那里”的进程,在通常情况下,这些进程不会直接通信。尽管如此,它们仍然很重要,其中一些对系统的功能至关重要,终止其中一些会导致致命的系统崩溃。一些系统进程是原生进程,意味着它们只使用原生API(由NTDLL实现的API)。系统进程的示例包括Smss.exe、Lsass.exe、Winlogon.exe、Services.exe等。
  • 子系统进程。Windows子系统进程,运行镜像Csrss.exe,可视为内核助手,用于管理在Windows系统下运行的进程。它是一个关键的进程,如果被杀死,系统将崩溃。通常有一个CSRS.exe实例,因此在标准系统中存在两个实例——一个用于会话(通常为0),一个用于登录用户会话(通常为1)。尽管CSRS.exe是Windows子系统的“管理器”(目前仅剩的一个),其重要性不仅仅是此角色。
  • 内核模式
  • 执行层(Executive)。执行层是NtOskrnl.exe(“内核”)的上层,承载了内核模式下的大部分代码。主要包括各种管理器:对象管理器、内存管理器、I/O管理器、即插即用管理器、电源管理器、配置管理器等,远远大于底层的内核层。
  • 内核。内核层实现内核模式操作系统代码的最基本和时间敏感部分,包括线程调度、中断和异常调度以及各种内核原语(如互斥和信号量)的实现。一些内核代码是用CPU特定的机器语言编写的,以提高效率并直接访问CPU特定的细节。
  • 设备驱动程序。设备驱动程序是可加载的内核模块,其代码以内核模式执行,因此拥有内核的全部功能。经典设备驱动程序提供了硬件设备和操作系统其余部分之间的粘合剂,其他类型的驱动程序提供过滤功能。
  • Win32k.sys。Windows子系统的内核模式组件,本质上是一个内核模块(驱动程序),用于处理Windows的用户界面部分和经典的图形设备接口(GDI)API。意味着所有窗口操作都由该组件处理,系统的其余部分对UI几乎一无所知。
  • 硬件抽象层(HAL)。HAL是最接近CPU的硬件上的抽象层,允许设备驱动程序使用不需要中断控制器或DMA控制器等详细和特定知识的API。当然,这一层对于为处理硬件设备而编写的设备驱动程序非常有用。目标是将特定于硬件的例程从“核心”操作系统中分离出来,提供便携性,提高可读性。
  • Hyper-V管理程序。如果支持基于虚拟化的安全性(VBS),则Hyper-V管理程序存在于Windows 10和server 2016(及更高版本)系统上。VBS提供了额外的安全层,其中实际的机器实际上是由Hyper-V控制的虚拟机。

下图是微内核结构示意图:

image.png

OS的组件通常包含控制程序、系统服务程序、工具类程序等。其中,控制程序创建环境以运行其它程序,通过图形接口,控制和维护计算机操作,例如windows环境的GUI。系统服务程序无需用户干预即可执行特定功能,可以手动启动,也可以配置为在启动操作系统时自动启动,在运行操作系统时在后台运行,可能会影响系统性能、响应能力、能效和安全性,例如任务调度程序、windows更新、信使服务、即插即用、索引服务等。工具类程序执行与管理系统资源相关的非常具体的任务,关注各种计算机组件的操作方式,常用实用程序是磁盘格式化实用程序、防病毒实用程序、备份实用程序、文件管理器、磁盘清理等。

操作系统提供由进程通过涉及环转换的机制访问的服务,以将控制转移到执行所需功能的内核。这有一个显著的缺点,即每个服务调用都涉及上下文切换的开销,其中保存处理器状态并执行保护域传输。然而,正如A High Performance Kernel-Less Operating System Architecture所发现的,在支持分段的处理器体系结构上,通过不执行环转换,可以在访问操作系统提供的服务时获得显著的性能提升。KLOS是基于这种设计构建的无内核操作系统,它的服务调用机制比当前广泛实施的服务或系统调用机制快了一个数量级,比传统陷阱/中断提高了4倍,比Intel SYSENTER/SYSEXIT快速系统调用模型提高了2倍。

image.png
KLOS的架构图。

18.1.5 操作系统高性能开发

常见的OS高性能开发技术包含:

  • 在线和离线操作。为每个I/O设备编写了一个称为设备控制器的特殊子程序,一些I/O设备已配备用于在线操作(它们连接到处理器)或离线操作(它们由控制单元运行)。
  • 缓冲。缓冲区是一个主存储器区域,用于在I/O传输期间保存数据。输入时,数据通过I/O通道放入缓冲区,当传输完成时,处理器可以访问数据。可以是单缓冲或双缓冲。
  • 联机(同时进行外围设备在线操作)。联机将磁盘用作非常大的缓冲区,它很有用,因为设备访问不同速率的数据。缓冲区提供了一个等待站,当较慢的设备赶上时,数据可以在这里暂存。联机允许一个作业的计算和另一个作业I/O之间的重叠。
  • 多道程序处理。在多道程序设计中,几个程序同时保存在主存中,CPU在它们之间切换,因此CPU总是有一个要执行的程序。操作系统开始从内存执行一个程序,如果该程序需要等待,例如I/O操作,操作系统将切换到另一个程序。多程序设计提高了CPU利用率。多道程序设计系统提供了一种环境,在这种环境中,各种系统资源得到了有效利用,但它们不提供用户与计算机系统的交互。优势是CPU利用率高,似乎许多程序几乎同时分配CPU。缺点是需要CPU调度,要在内存中容纳许多作业,需要内存管理。
  • 并行系统。系统中的处理器上有2个及以上,这些处理器共享计算机总线、时钟、内存和I/O设备。优点是提高吞吐量(以时间单位完成的程序数)。
  • 分布式系统。在几个物理处理器之间分配计算,涉及通过通信链路连接2个或多个独立的计算机系统。因此,每个处理器都有自己的OS和本地内存,处理器通过各种通信线路(如高速总线或电话线)相互通信。

image.png

分布式系统的优点:

  • 资源共享,可以共享文件和打印机。
  • 计算速度加快。可以对作业进行分区,以便每个处理器可以并发执行一部分任务(负载共享)。
  • 可靠性。如果一个处理器出现故障,其余处理器仍然可以正常工作。
  • 通信。如电子邮件、ftp。
  • 个人计算机。专用于单个用户的计算机系统,PC操作系统既不是多用户系统,也不是多任务系统。PC操作系统的目标是最大限度地提高用户的便利性和响应能力,而不是最大限度地利用CPU和I/O。比如Microsoft Windows和Apple Macintosh。

A caching model of operating system kernel functionality描述了操作系统功能的缓存模型,可以缓存内核缓存线程和地址空间等操作系统对象,就像传统硬件缓存内存数据一样。用户模式应用程序内核处理这些对象的加载和写回,实现特定于应用程序的管理策略和机制。在多处理器上实现缓存内核及其性能测量的经验表明,该缓存模型可以提供与传统单片操作系统相比具有竞争力的性能,同时还可以提供系统资源的应用程序级控制、更好的模块化、更好的可伸缩性、更小的大小以及故障控制的基础。

如下图所示,各种应用程序、服务器内核和操作系统仿真器可以在同一硬件上同时执行。一个称为系统资源管理器(SRM)的特殊应用程序内核,每个缓存内核/MPM复制一个,管理其他应用程序内核之间的资源共享,以便它们可以同时共享相同的硬件,而不会产生不合理的干扰。例如,它可以防止运行大型模拟的恶意应用程序内核中断提供在同一ParaDiGM配置上运行的分时服务的UNIX模拟器的执行。

image.png
软件架构一览。

下图显示了Cache Kernel对象之间的依赖关系。图中的箭头表示从箭头尾部的对象到头部的对象的引用,因此是缓存依赖项。例如,物理内存映射中的信号映射引用一个线程,该线程引用一个引用其所属内核对象的地址空间。因此,当线程、地址空间或内核被卸载时,必须卸载信号映射。

image.png
缓存数据架构。

18.2 计算机硬件概览

操作系统与运行它的计算机的硬件紧密相连,它扩展了计算机的指令集并管理其资源。一台简单的个人计算机可以抽象为类似于下图的模型,CPU、内存和I/O设备都通过系统总线连接,并通过它彼此通信。现代个人电脑的结构更复杂,涉及多条总线。

image.png
image.png
计算机硬件架构抽象图。

image.png
image.png
计算机硬件组成图。

image.png
Intel Core i7结构图。

18.2.1 CPU
计算机的“大脑”是CPU,它从内存中获取指令并执行它们。每个CPU的基本周期是从内存中获取第一条指令,对其进行解码以确定其类型和操作数,然后执行,然后获取、解码和执行后续指令。循环重复,直到程序结束。以这种方式执行程序。

每个CPU都有一组特定的指令可以执行。因此,x86处理器无法执行ARM程序,而ARM处理器无法执行x86程序。因为访问内存以获取指令或数据字比执行指令需要更长的时间,所以所有CPU都包含一些寄存器来保存关键变量和临时结果。因此,指令集通常包含将字从内存加载到寄存器,并将字从寄存器存储到内存的指令。其他指令将来自寄存器、内存或两者的两个操作数组合成一个结果,例如添加两个字并将结果存储在寄存器或内存中。

除了用于保存变量和临时结果的通用寄存器外,大多数计算机还具有程序员可见的几个专用寄存器。其中之一是程序计数器,它包含要提取的下一条指令的内存地址。获取该指令后,程序计数器将更新为指向其后续指令。另一个寄存器是堆栈指针,它指向内存中当前堆栈的顶部。堆栈包含每个已进入但尚未退出的过程的一个帧。过程的堆栈框架保存那些不保存在寄存器中的输入参数、局部变量和临时变量。另一个寄存器是PSW(程序状态字)。该寄存器包含由比较指令、CPU优先级、模式(用户或内核)和各种其他控制位设置的条件代码位。用户程序通常可以读取整个PSW,但通常只能写入其部分字段。PSW在系统调用和I/O中起着重要作用。

操作系统必须完全了解所有寄存器。当对CPU进行多路复用时,操作系统通常会停止正在运行的程序以(重新)启动另一个程序。每次停止运行的程序时,操作系统必须保存所有寄存器,以便在程序稍后运行时恢复。

为了提高性能,CPU设计者早已放弃了一次获取、解码和执行一条指令的简单模型。许多现代CPU都具有同时执行多条指令的功能。例如,CPU可能有单独的提取、解码和执行单元,因此在执行指令n时,它也可能是解码指令n+1和提取指令n+2。这种组织称为管道,如下图(a)所示,用于三级管道。较长的管道是常见的。在大多数管道设计中,一旦指令被提取到管道中,就必须执行它,即使前面的指令是执行的条件分支。管道给编译器编写者和操作系统编写者带来了极大的麻烦,因为它们向他们暴露了底层机器的复杂性,并且他们必须处理它们。

image.png
比管道设计更先进的是超标量(superscalar)CPU,如上图(b)所示。在这种设计中,有多个执行单元,例如一个用于整数运算,一个用于浮点运算,另一个用于布尔运算。两个或多个指令被同时获取、解码并转储到一个保持缓冲区,直到它们可以执行为止。一旦执行单元可用,它就会查看保持缓冲区中是否有它可以处理的指令,如果有,就从缓冲区中删除该指令并执行它。这种设计的一个含义是,程序指令经常乱序执行。在大多数情况下,要由硬件来确保产生的结果与顺序实现所产生的结果相同,但正如我们将看到的那样,操作系统上强加了令人讨厌的复杂性。

如前所述,除嵌入式系统中使用的非常简单的CPU外,大多数CPU都有两种模式,内核模式和用户模式。通常,PSW中的一个位控制模式。在内核模式下运行时,CPU可以执行其指令集中的每一条指令,并使用硬件的每一项功能。在台式机和服务器上,操作系统通常以内核模式运行,从而可以访问整个硬件。在大多数嵌入式系统上,一小部分以内核模式运行,其余操作系统以用户模式运行。

用户程序总是在用户模式下运行,该模式只允许执行指令的子集和访问功能的子集。通常,在用户模式下,所有涉及I/O和内存保护的指令都是不允许的。当然,也禁止将PSW模式位设置为进入内核模式。

要从操作系统获取服务,用户程序必须进行系统调用,该调用会进入内核并调用操作系统。TRAP指令从用户模式切换到内核模式,并启动操作系统。工作完成后,根据系统调用后的指令将控制权返回给用户程序。可以将其视为一种特殊的进程调用,它具有从用户模式切换到内核模式的附加属性。

值得注意的是,除了执行系统调用的指令之外,计算机还有陷阱(trap),大多数其它陷阱是由硬件警告异常情况(如试图除以0或浮点下溢)引起的。在所有情况下,操作系统都会得到控制,并且必须决定要做什么。有时程序必须因错误而终止,其他时候可以忽略错误(下溢数字可以设置为0)。最后,当程序事先宣布要处理某些类型的条件时,可以将控制权传递回程序,让它处理问题。

除了多线程之外,现在许多CPU芯片上都有四个、八个或更多完整的处理器或内核。下图中的多核芯片有效地携带了四个微型芯片,每个芯片都有自己的独立CPU,一些处理器(如Intel Xeon Phi和Tilera TilePro)已经在单个芯片上运行了60多核。使用这种多核芯片肯定需要多处理器操作系统。

顺便说一句,就绝对数量而言,没有什么能比得上现代GPU(图形处理单元),GPU是一个拥有数千个微内核的处理器,它们对于许多并行完成的小计算非常有用,比如在图形应用程序中渲染多边形。他们不太擅长连续任务,也很难编程。虽然GPU对操作系统很有用(例如,加密或处理网络流量),但操作系统本身不太可能在GPU上运行。

image.png
(a) 具有共享二级缓存的四核芯片。(b) 具有独立二级缓存的四核芯片。

18.2.2 内存

任何计算机的第二个主要部件是内存。理想情况下,内存应该非常快(比执行指令快,这样CPU就不会被内存占用),非常大且非常便宜。目前没有任何技术能够满足所有这些目标,因此采取了不同的方法。内存系统被构造为一个层次结构,如下图所示。与低层相比,顶层具有更高的速度、更小的容量和更高的每比特成本,通常是10亿或更多倍。

image.png
image.png
内存层级架构和速度示意图。

顶层由CPU内部的寄存器组成,它们由与CPU相同的材料制成,因此与CPU一样快,因此,访问它们没有任何延误。它们的可用存储容量通常是32位CPU上的32×32位,64位CPU上为64×64位,两种情况下都小于1 KB。程序必须在软件中自行管理寄存器(即决定在其中保存什么)。

高速缓存主要由硬件控制。主内存被划分为缓存行,通常为64字节,缓存线0中的地址为0到63,缓存线1中的地址是64到127,依此类推。最常用的缓存行保存在CPU内部或非常靠近CPU的高速缓存中,当程序需要读取内存字时,缓存硬件会检查所需的行是否在缓存中。如果是,称为缓存命中,则请求从缓存中得到满足,并且没有内存请求通过总线发送到主内存。缓存命中通常需要大约两个时钟周期,缓存未命中必须转移到内存中,会导致大量时间损失。由于高速缓存的成本较高,其大小受到限制,有些机器有两级甚至三级缓存,每级缓存都比前一级缓存更慢、更大。

image.png
缓存和内存结构。

缓存在计算机科学的许多领域都扮演着重要角色,不仅仅是缓存RAM行。每当一个资源可以划分为多个部分时,其中一些部分的使用量比其他部分大得多,缓存通常用于提高性能。操作系统一直在使用它。例如,大多数操作系统将频繁使用的文件(片段)保存在主内存中,以避免重复从磁盘获取它们。类似地,转换长路径名的结果如下:

/home/ast/projects/minix3/src/kernel/clock.c

可以缓存到文件所在的磁盘地址,以避免重复查找。最后,当网页(URL)的地址转换为网络地址(IP地址)时,可以缓存结果以供将来使用。还有许多其他用途。在任何缓存系统中,很快就会出现几个问题,包括:

1、何时将新项目放入缓存。

2、将新项目放入哪个缓存行。

3、需要插槽时要从缓存中删除的项。

4、将新收回的项目放在较大内存中的何处。

并非每个问题都与每个缓存情况相关。对于CPU缓存中的主内存缓存行,通常在每次缓存未命中时都会输入一个新项。要使用的缓存行通常是通过使用引用的内存地址的一些高位来计算的。例如,对于4096条64字节和32位地址的缓存行,可以使用位6到17来指定缓存行,其中位0到5是缓存行内的字节。在这种情况下,要删除的项与新数据进入的项相同,但在其他系统中可能不是。最后,当缓存行被重写到主内存时(如果缓存行在被缓存后已被修改),内存中要重写它的位置是由所讨论的地址唯一确定的。

缓存是一个好主意,现代CPU有两个缓存。第一级缓存或L1缓存始终位于CPU内部,通常将解码的指令送入CPU的执行引擎。大多数芯片都有第二个L1缓存,用于存储大量使用的数据字,一级缓存通常每个为16 KB。此外,通常还有一个称为L2缓存的第二个缓存,它保存了几兆字节的最近使用的内存字。一级缓存和二级缓存的区别在于定时,对一级缓存的访问不会有任何延迟,而对二级缓存的存取会延迟一个或两个时钟周期。

在多核芯片上,设计者必须决定缓存的位置。在下图(a),所有核心共享一个二级缓存。这种方法用于Intel多核芯片。相反,在下图(b)中,每个内核都有自己的二级缓存,AMD采用了这种方法。每种策略都有其优缺点,例如,Intel共享的二级缓存需要更复杂的缓存控制器,但AMD方法使保持二级缓存一致性更加困难。

image.png

在上上图的层次结构中,主内存紧随其后,是内存系统的主力,通常称为RAM(随机存取存储器),更早之前称之为磁芯存储器,因为20世纪50年代和60年代的计算机使用微小的可磁化铁氧体磁芯作为主存储器,所有无法从缓存中满足的CPU请求都会转到主内存。

除了主存储器之外,许多计算机还有少量非易失性随机存取存储器。与RAM不同,非易失性存储器在电源关闭时不会丢失其内容。ROM(只读存储器)是在工厂编程的,以后不能更改。它既快又便宜,在一些计算机上,用于启动计算机的引导加载程序包含在ROM中。此外,一些I/O卡附带ROM,用于处理低级设备控制。

EEPROM(电可擦除PROM)和闪存也是非易失性的,但与ROM相反,它们可以擦除和重写。然而,写它们比写RAM需要更多数量级的时间,因此它们的使用方式与ROM相同,只是有了一个额外的功能,即现在可以通过在现场重写它们来纠正程序中的错误。

闪存也常用作便携式电子设备的存储介质,在数码相机中用作胶片,在便携式音乐播放器中用作磁盘,仅举两个用途。闪存的速度介于RAM和磁盘之间。此外,与磁盘内存不同,如果擦除次数太多,它就会磨损。

另一种存储器是CMOS,它是易失性的。许多计算机使用CMOS存储器来保存当前时间和日期,CMOS存储器和增加时间的时钟电路由一个小电池供电,因此即使拔下电脑插头,时间也能正确更新。CMOS存储器还可以保存配置参数,例如从哪个磁盘启动。之所以使用CMOS,是因为它耗电很少,以至于原厂安装的电池通常可以使用几年。然而,当它开始出现故障时,计算机可能会出现阿尔茨海默病,忘记它多年来知道的事情,比如从哪个硬盘启动。

18.2.3 磁盘

磁盘存储每比特比RAM便宜两个数量级,通常也要大两个数量级别,唯一的问题是随机访问数据的时间慢了近三个数量级,原因是磁盘是一种机械装置,如下图所示。

image.png

磁盘由一个或多个旋转速度为5400、7200、10800 RPM或以上的金属盘组成。一个机械臂从角落绕着盘片旋转,类似于老式33-RPM留声机上播放乙烯基唱片的拾取臂,信息以一系列同心圆写入磁盘。在任何给定的手臂位置,每个头部都可以读取一个称为轨道的环形区域,给定手臂位置的所有轨道一起构成一个圆柱体。

每个磁道被划分为若干扇区,通常每个扇区512字节。在现代磁盘上,外圆柱体包含的扇区比内圆柱体多。将臂从一个圆柱体移动到另一个圆柱体大约需要1毫秒。根据驱动器的不同,将其移动到随机圆柱体通常需要5到10毫秒。一旦臂位于正确的轨道上,驱动器必须等待所需扇区在磁头下旋转,根据驱动器的RPM,额外延迟5毫秒到10毫秒。一旦扇区位于磁头之下,低端磁盘的读取或写入速度为50 MB/秒,而高速磁盘的读取和写入速度为160 MB/秒。

有时,我们会听到人们谈论实际上根本不是磁盘的磁盘,如SSD(固态磁盘)。SSD没有移动部件,不包含磁盘形状的盘片,并将数据存储在(闪存)内存中。它们与磁盘相似的唯一方式是,存储了大量数据,这些数据在断电时不会丢失。

许多计算机支持一种称为虚拟内存的方案,该方案通过将程序放在磁盘上并将主内存用作执行最频繁的部分的缓存,使运行大于物理内存的程序成为可能。此方案需要动态重新映射内存地址,以将程序生成的地址转换为字所在的RAM中的物理地址。此映射由CPU的一部分MMU(内存管理单元)完成。

缓存和MMU的存在会对性能产生重大影响。在多道程序设计系统中,当从一个程序切换到另一个程序(有时称为上下文切换)时,可能需要从缓存中清除所有修改过的块,并更改MMU中的映射寄存器。这两种操作昂贵很大,应该被开发者注意并规避。

18.2.4 I/O设备

IO存储设备根据速度和介质,有着以下的层级关系:

image.png

Linux中的网络分层如下:

image.png

CPU和内存不是操作系统必须管理的唯一资源,I/O设备还与操作系统进行大量交互,通常由两部分组成:控制器和设备本身。

控制器是物理控制设备的一个或一组芯片,接受来自操作系统的命令,例如从设备读取数据,并执行这些命令。在许多情况下,设备的实际控制是复杂和详细的,因此控制器的工作是为操作系统提供一个更简单(但仍然非常复杂)的接口。例如,磁盘控制器可能会接受从磁盘2读取扇区11206的命令。然后,控制器必须将此线性扇区编号转换为圆柱体、扇区和磁头。由于外部圆柱体的扇区比内部圆柱体的多,并且一些坏扇区已重新映射到其他扇区,因此这种转换可能会变得复杂。然后,控制器必须确定磁盘臂在哪个圆柱体上,并向其发出命令,以移入或移出所需数量的圆柱体。它必须等到正确的扇区在磁头下旋转,然后开始读取和存储从驱动器上下来的位,删除前导码并计算校验和。最后,它必须将输入的位组合成单词并存储在内存中。为了完成所有这些工作,控制器通常包含小型嵌入式计算机,这些计算机被编程来完成它们的工作。

另一部分是实际设备本身。设备有相当简单的接口,既是因为它们不能做很多事情,也是为了使它们成为标准。例如,需要后者,以便任何SATA磁盘控制器都可以处理任何SATA盘。SATA代表串行ATA,ATA代表AT附件,是围绕当时功能极为强大的6-MHz 80286处理器而构建的。SATA目前是许多计算机上的标准磁盘类型。由于实际的设备接口隐藏在控制器后面,操作系统看到的只是控制器的接口,可能与设备的接口有很大的不同。

因为每种类型的控制器都不同,所以需要不同的软件来控制每种控制器,与控制器对话、发出命令并接受响应的软件称为设备驱动程序,每个控制器制造商必须为其支持的每个操作系统提供一个驱动程序。例如扫描仪可能附带OS X、Windows 7、Windows 8和Linux的驱动程序。

要使用该驱动程序,必须将其放入操作系统中,以便它可以在内核模式下运行。驱动程序实际上可以在内核外运行,现在像Linux和Windows这样的操作系统确实提供了一些支持,绝大多数驱动程序仍在内核边界以下运行。目前只有极少数系统(如MINIX 3)在用户空间中运行所有驱动程序,必须允许用户空间中的驱动程序以受控方式访问设备,但并不简单。

有三种方法可以将驱动程序放入内核。第一种方法是用新的驱动程序重新链接内核,然后重新启动系统,例如许多较旧的UNIX系统。第二种方法是在操作系统文件中输入一个条目,告诉它需要驱动程序,然后重新启动系统。在引导时,操作系统会找到所需的驱动程序并加载它们,如Windows。第三种方法是让操作系统能够在运行时接受新的驱动程序,并动态安装它们,而无需重新启动。这种方式过去很少见,但现在越来越普遍了。热插拔设备,如USB和IEEE 1394设备,始终需要动态加载的驱动程序。

每个控制器都有少量用于与其通信的寄存器。例如,最小磁盘控制器可能具有指定磁盘地址、内存地址、扇区计数和方向(读或写)的寄存器。为了激活控制器,驱动程序从操作系统获取命令,然后将其转换为适当的值,写入设备寄存器。所有设备寄存器的集合构成I/O端口空间。

在某些计算机上,设备寄存器被映射到操作系统的地址空间(它可以使用的地址),因此它们可以像普通内存字一样读取和写入。在这类计算机上,不需要特殊的I/O指令,用户程序可以通过不将这些内存地址放在其可及范围内而远离硬件(例如,通过使用基址和限制寄存器)。在其他计算机上,设备寄存器放在一个特殊的I/O端口空间中,每个寄存器都有一个端口地址。在这些机器上,内核模式下有特殊的IN和OUT指令,允许驱动程序读取和写入寄存器。前一种方案不需要特殊的I/O指令,但会占用一些地址空间。后者不使用地址空间,但需要特殊说明。这两种系统都被广泛使用。

输入和输出可以用三种不同的方式完成。在最简单的方法中,用户程序发出一个系统调用,然后内核将其转换为对相应驱动程序的过程调用。然后,驱动程序启动I/O并在一个紧密的循环中持续轮询设备,以查看是否完成了操作(通常有一些位表示设备仍在忙)。当I/O完成时,驱动程序将数据(如果有)放在需要的位置并返回。然后,操作系统将控制权返回给调用者。这种方法称为繁忙等待,其缺点是占用CPU轮询设备直到完成。

第二种方法是驱动程序启动设备,并在完成时要求其中断。此时,驱动返回。然后,如果需要,操作系统会阻止调用者,并寻找其他工作。当控制器检测到传输结束时,它会生成一个中断以完成信号。

中断在操作系统中非常重要,所以让我们更仔细地研究一下这个想法。在下图(a)中,我们看到I/O的三步过程。在步骤1中,驱动程序通过写入其设备寄存器来告诉控制器要做什么。然后,控制器启动设备。当控制器完成读取或写入它被要求传输的字节数时,它在步骤2中使用某些总线向中断控制器芯片发送信号。如果中断控制器准备接受中断(如果它忙于处理更高优先级的中断,则可能不会接受),它在CPU芯片上断言一个管脚,在步骤3中告诉它。在步骤4中,中断控制器将设备的编号放在总线上,这样CPU就可以读取它,并知道哪个设备刚刚完成(许多设备可能同时运行)。

一旦CPU决定接受中断,程序计数器和PSW通常被推到当前堆栈上,CPU切换到内核模式。设备编号可用作内存部分的索引,以查找该设备的中断处理程序的地址。这部分内存称为中断向量。一旦中断处理程序(中断设备驱动程序的一部分)启动,它将删除堆叠程序计数器和PSW并保存它们,然后查询设备以了解其状态。当处理程序全部完成时,它返回到以前运行的用户程序,返回到尚未执行的第一条指令。这些步骤如下图(b)所示。

image.png
(a)启动I/O设备并获得中断的步骤。(b) 中断处理包括接受中断、运行中断处理程序和返回用户程序。

执行I/O的第三种方法使用特殊的硬件:DMA(直接内存访问)芯片,可以控制内存和某些控制器之间的比特流,而无需持续的CPU干预。CPU设置DMA芯片,告诉它要传输多少字节、涉及的设备和内存地址以及方向,然后让它执行。当DMA芯片完成时,它会触发中断,如上文所述进行处理。

例如,当另一个中断处理程序正在运行时,中断可能(而且经常)发生在非常不方便的时刻。因此,CPU有一种方法可以禁用中断,然后稍后重新启用它们。当中断被禁用时,任何完成的设备都会继续断言其中断信号,但CPU不会中断,直到再次启用中断。如果多个设备在中断被禁用时完成,中断控制器通常根据分配给每个设备的静态优先级决定首先让哪个设备通过。优先级最高的设备获胜并首先得到服务。其他必须等待。

18.2.5 总线

随着处理器和内存的速度越来越快,单个总线(当然还有IBM PC总线)处理所有流量的能力已经到了极限。为了更快的I/O设备和CPU到内存的通信量,添加了额外的总线。由于这一演变,一个大型x86系统目前看起来类似于下图。

image.png
大型x86系统的结构。

该系统有许多总线(例如,缓存、内存、PCIe、PCI、USB、SATA和DMI),每个总线具有不同的传输速率和功能。操作系统必须了解所有这些信息,以便进行配置和管理。

主总线是PCIe(外围组件互连高速)总线,PCIe总线是Intel作为旧PCI总线的继承者而发明的,而旧PCI总线又是原始ISA(行业标准体系结构)总线的替代品。PCIe能够每秒传输数十Gb的数据,比其前代产品快得多,它的性质也非常不同。直到2004年创建,大多数总线都是并行共享的,共享总线架构意味着多个设备使用相同的线路传输数据。因此,当多个设备有数据要发送时,需要一个仲裁器(arbiter)来确定谁可以使用总线。相反,PCIe使用专用的点到点连接,传统PCI中使用的并行总线架构意味着可以通过多条导线发送每个字的数据。例如,在常规PCI总线中,单个32位数字通过32条并行线发送。与此相反,PCIe使用串行总线体系结构,并通过单个连接(称为通道)发送消息中的所有位,就像网络数据包一样。这种方式简单得多,因为不必确保所有32位都在同一时间到达目的地。仍然使用平行性,因为可以有多条平行通道(lane),例如可以使用32条通道并行传输32条消息。随着网卡和图形适配器等外围设备速度的快速增长,PCIe标准每3-5年升级一次。例如,16通道PCIe 2.0提供每秒64 Gb的速率,升级到PCIe 3.0将使速度提高一倍,而PCIe 4.0将使速度再次提高一倍。

与此同时,仍有许多适用于旧PCI标准的传统设备,如上图所示,这些设备连接到单独的集线器处理器。未来,当我们认为PCI不再仅仅是老式的,而是老式的时,所有PCI设备都有可能连接到另一个集线器,而该集线器又将它们连接到主集线器上,从而形成总线树。

在此配置中,CPU通过快速DDR3总线与内存通信,通过PCIe与外部图形设备通信,并通过DMI(直接媒体接口)总线上的集线器与所有其他设备通信。集线器依次连接所有其他设备,使用通用串行总线与USB设备通信,使用SATA总线与硬盘和DVD驱动器交互,使用PCIe传输以太网帧。

此外,每个核心都有一个专用缓存和一个更大的缓存,在它们之间共享,每个缓存都引入另一条总线。

发明USB(通用串行总线)是为了将所有低速I/O设备(如键盘和鼠标)连接到计算机上。然而,对于以8-Mbps ISA作为第一台IBM PC的主总线而成长起来的一代人来说,将以5 Gbps“低速”运行的现代USB 3.0设备称为“低速”可能不是自然而然的。USB使用一个带有四到十一根线(取决于版本)的小连接器,其中一些线为USB设备供电或接地。USB是一种集中式总线,其中根设备每1毫秒轮询一次所有I/O设备,以查看它们是否有流量。USB 1.0可以处理12 Mbps的总负载,USB 2.0将速度提高到480 Mbps,USB 3.0最高不低于5 Gbps。任何USB设备都可以连接到计算机上,将立即运行,而无需重新启动计算机,这是USB设备之前所需的,让一代受挫的用户非常震惊。

SCSI(小型计算机系统接口)总线是一种高性能总线,用于快速磁盘、扫描仪和其他需要大量带宽的设备。如今,它们大多在服务器和工作站上,可以以高达640 MB/秒的速度运行。

要在上图所示的环境中工作,操作系统必须知道哪些外围设备连接到计算机并对其进行配置。这一要求促使英特尔和微软基于苹果Macintosh首次实现的类似概念,设计了一种称为即插即用的PC系统。在即插即用之前,每个I/O卡都有一个固定的中断请求级别和其I/O寄存器的固定地址。例如,键盘为中断1并使用I/O地址0x60至0x64,软盘控制器为中断6并使用I/O位置0x3F0至0x3F7,打印机为中断7并使用I/O位址0x378至0x37A,依此类推。

到目前为止,一切都很好。当用户购买了一张声卡和一张调制解调器卡,而这两张卡都碰巧使用了同一中断(如中断4)时,问题就出现了——会发生冲突,不能一起工作。解决方案是在每个I/O卡上包括DIP开关或跳线,并指示用户请将其设置为选择一个中断级别和I/O设备地址,该中断级别和输入/输出设备地址不会与用户系统中的任何其他地址冲突。不幸的是,极少人能做到,导致混乱。

即插即用所做的是让系统自动收集有关I/O设备的信息,集中分配中断级别和I/O地址,然后告诉每个卡的编号。这项工作与启动计算机密切相关,且不是简单的小事。

18.3 内核

下面两图显示了主流的内核总体架构。

image.png
内核架构1。

image.png
内核架构2。

图的底部显示了内核的一部分,该部分(以及从那里调用的函数)在监督者模式下执行。在监督器模式下执行的所有代码都是用汇编程序编写的,并包含在文件crt0.S中。crt0.S中的代码分为启动代码、访问硬件的函数、中断服务例程、任务开关(调度程序)和出于性能原因而用汇编程序写的信号量函数。

图的中间部分显示了在用户模式下执行的内核的其余部分。对crt0.S中代码的任何调用都需要更改为监控模式,即从中间到下部的每个箭头都与一个或多个TRAP指令相关,这些指令会导致监控模式的更改。类os包含一组带有TRAP指令的包装函数,使应用程序能够访问某些硬件部件。SerialIn和SerialOut类称为串行I/O,需要硬件访问,也可以从中断服务例程访问。Class Task包含与任务管理相关的任何内容,并使用内核的supervisor部分进行(显式)任务切换。任务切换也由中断服务例程引起。Semaphore类提供包装函数,使其成员函数的实现在用户模式下可用。内核内部使用了几个Queue类,并且应用程序也可以使用这些类;他们中的大多数使用Semaphore类。

通常,应用程序与内部内核接口无关,与内核相关的接口是在类os、SerialIn、SerialOut、Task、Queue和Semaphore中定义的接口。

18.3.1 内核概述

下图给出了内核的主要组件的简单概述。可以看到底部的硬件,硬件由芯片、电路板、磁盘、键盘、显示器和类似的物理对象组成。硬件之上是软件,大多数计算机有两种操作模式:内核模式和用户模式。操作系统是软件中最基本的部分,它以内核模式(也称为管理器模式)运行。在这种模式下,它可以完全访问所有硬件,并可以执行机器能够执行的任何指令。软件的其余部分以用户模式运行,在该模式下,只有机器指令的一个子集可用。特别是,那些影响机器控制或进行I/O输入/输出的指令“被禁止用于用户模式程序。本文反复讨论内核模式和用户模式之间的区别,它在操作系统的工作方式中起着至关重要的作用。

image.png

更详细的结构图如下:

image.png

传统的Unix内核架构图如下:

image.png

现代Unix内核已经进化成如下架构:

image.png

Linux内核模块列表样例如下:

image.png

Linux内核组件如下所示:

image.png

18.3.2 内核对象

Windows内核公开各种类型的对象,供用户模式进程、内核本身和内核模式驱动程序使用。这些类型的实例是系统(内核)空间中的数据结构,当用户或内核模式代码请求时,由对象管理器(执行程序的一部分)创建和管理。内核对象使用了引用计数,因此只有当对象的最后一个引用被释放时,对象才会被销毁并从内存中释放。

Windows内核支持很多对象类型,可从Sysinternals运行WinObj工具,并找到ObjectTypes目录(下图)。可以根据其可见性和用途进行分类:

  • 通过Windows API导出到用户模式的类型。例如:互斥、信号量、文件、进程、线程和计时器。
  • 未导出到用户模式,但记录在Windows驱动程序工具包(WDK)中供设备驱动程序编写者使用的类型。例如设备、驱动程序和回调。
  • 即使在WDK中也没有记录的类型(至少在编写时),仅由内核本身使用。例如分区、键控事件和核心消息传递。

image.png

内核对象的主要属性如下图所示:

image.png

某些类型的对象可以具有基于字符串的名称,这些名称可用于使用适当的打开函数按名称打开对象。注意,并非所有对象都有名称,例如进程和线程没有名称——它们有ID。这就是为什么OpenProcess和OpenThread函数需要进程/线程标识符(数字)而不是基于string的名称。

在用户模式代码中,如果不存在具有名称的对象,则调用具有名称的创建函数将创建具有该名称的对象;如果存在,则只打开现有对象。

提供给创建函数的名称不是对象的最终名称。在经典(桌面)进程中,它前面有\Sessions\x\BaseNamedObjects\其中x是调用方的会话ID。如果会话为零,则名称前面只加上\BaseNamedObjects\。如果调用方碰巧在AppContainer(通常是通用Windows平台进程)中运行,则前缀字符串更复杂,由唯一的AppContainerSID: \Sessions\x\AppContaineerNameObjects\组成。

18.3.3 内核对象共享

内核对象的句柄是进程私有的,但在某些情况下,进程可能希望与另一个进程共享内核对象。这样的进程不能简单地将句柄的值传递给其他进程,因为在其他进程的句柄表中,句柄值可能指向其他对象或为空。显然,必须有某种机制来允许这种分享。事实上,有三种分享机制:

  • 按名称共享。如果可用,是最简单的方法。“可用”表示所讨论的对象可以有名称,并且确实有名称。典型的场景是,协作进程(2个或更多)将使用相同的对象名调用相应的Create函数。进行调用的第一个进程将创建对象,其他进程的后续调用将为同一对象打开其他句柄。
  • 通过句柄继承共享。通常是父进程创建子进程时,传入继承属性和数据而达成。
  • 通过复制句柄共享。句柄复制没有固有的限制(除了安全性),几乎可以在任何内核对象上工作,无论是有命字的还是没有名字的,并且可以在任何时间点工作。然而,有一个缺陷,是实践中最困难的分享方式(后面会提及)。Windows通过调用DuplicateHandle复制句柄。

image.png
Windows复制句柄应用案例图示。

18.3.4 句柄

由于内核对象驻留在系统空间中,因此无法直接从用户模式访问它们。应用程序必须使用间接机制来访问内核对象,称为句柄(Handle)。句柄至少具有以下优点:

  • 在未来的Windows版本中,对象类型数据结构的任何更改都不会影响任何客户端。
  • 可通过安全访问检查控制对对象的访问。
  • 句柄是进程私有的,因此在一个进程中拥有特定对象的句柄在另一个进程上下文中没有意义。

内核对象是引用计数的。对象管理器维护句柄计数和指针计数,其和是对象的总引用计数(直接指针可以从内核模式获得)。一旦不再需要用户模式客户端使用的对象,客户端代码应通过调用CloseHandle关闭用于访问该对象的句柄。之后句柄将无效,尝试通过关闭句柄访问对象将失败。在一般情况下,客户端不知道对象是否已被销毁。如果对象的引用降至零,则对象管理器将删除该对象。

句柄值是4的倍数,其中第一个有效句柄是4;零永远不是有效的句柄值,在64位系统上亦是如此。句柄间接指向内核空间中的一个小数据结构,该结构包含句柄的一些信息。下图描述了32位和64位系统的数据结构。

image.png

在32位系统上,该句柄条目的大小为8字节,在64位系统上为16字节(从技术上而言,12字节已足够,但为了对齐目的,会扩展为16字节)。每个条目包含以下成分:

  • 指向实际对象的指针。由于低位用于标记,并通过地址对齐提高CPU访问时间,因此在32位系统上,对象的地址是8的倍数,在64位系统上是16的倍数。
  • 访问掩码,指示可使用此手柄执行的操作。换句话说,访问掩码是句柄的力量。
  • 三个标志:继承、关闭时保护和关闭时核验。

访问掩码是位掩码,其中每个“1”位表示可以使用该句柄执行的特定操作。当通过创建对象或打开现有对象创建句柄时,将设置访问掩码。如果创建了对象,则调用者通常具有对该对象的完全访问权。但是,如果对象被打开,调用方需要指定所需的访问掩码,它可能会得到,也可能不会得到。

某些句柄具有特殊值,不可关闭,被称为伪句柄(Pseudo Handles),尽管它们在需要时与任何其他句柄一样使用。在伪句柄上调用CloseHandle总是失败。

当不再需要句柄时,关闭句柄非常重要。如果应用程序未能正确执行此操作,则可能会出现“句柄泄漏”,即如果应用程序打开句柄但“忘记”关闭它们,则句柄的数量将无法控制地增长。帮助代码管理句柄而不忘记关闭它们的一种方法是使用C++实现一个众所周知的习惯用法,称为资源获取即初始化(Resource Acquisition is Initialization,RAII)。其思想是对包装在类型中的句柄使用析构函数,以确保在包装对象被销毁时关闭句柄。下面是一个简单的句柄RAII封装器:

struct Handle
{
    explicit Handle(HANDLE h = nullptr) 
        :_h(h) // 初始化
    {}
    // 析构函数关闭句柄。
    ~Handle() { Close(); }
    // 删除拷贝构造和拷贝赋值
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;

    // 允许移动(所有权转移)
    Handle(Handle&& other) : _h(other._h) 
    {
        other._h = nullptr;
    }
    Handle& operator=(Handle&& other) 
    {
        if (this != &other) 
        {
            Close();
            _h = other._h;
            other._h = nullptr;
        }
        return *this;
    }

    operator bool() const 
    {
        return _h != nullptr && _h != INVALID_HANDLE_VALUE;
    }
    HANDLE Get() const 
    {
        return _h;
    }
    void Close() 
    {
        if (_h) 
        {
            ::CloseHandle(_h);
            _h = nullptr;
        }
    }

private:
    HANDLE _h;
};

18.3.5 其它内核对象

Windows中还有其他常用内核对象,即用户对象和GDI对象。以下是这些对象的简要描述以及这些对象的句柄。

  • 任务管理器。可以通过添加“用户对象”和“GDI对象”列来显示每个进程的此类对象的数量。
  • 用户对象。用户对象是窗口(HWND)、菜单(HNU)和挂钩(HHOOK)。这些对象的句柄具有以下属性:
  • 无引用计数。第一个销毁用户对象的调用方——它已经消失了。
  • 句柄值的范围在窗口工作站(Window Station)下。窗口工作站包含剪贴板、桌面和原子表。例如,意味着这些对象的句柄可以在共享桌面的所有应用程序之间自由传递。
  • GDI对象。图形设备接口(GDI)是Windows中的原始图形API,即使有更丰富和更好的API(例如Direct2D)。例如设备上下文(HDC)、笔(HPEN)、画笔(HBRUSH)、位图(HBITMAP)等。以下是它们的属性:
  • 无引用计数。
  • 句柄仅在创建过程中有效。
  • 不能在进程之间共享。

18.3.6 中断

任何操作系统内核的核心职责是管理连接到机器硬盘驱动器和蓝光光盘、键盘和鼠标、3D处理器和无线收音机的硬件。为了履行这一职责,内核需要与机器的各个设备进行通信。考虑到处理器的速度可能比它们与之对话的硬件快几个数量级,内核发出请求并等待明显较慢的硬件的响应是不理想的。相反,由于硬件的响应速度相对较慢,内核必须能够自由地去处理其他工作,只有在硬件实际完成工作之后才处理硬件。

处理器如何在不影响机器整体性能的情况下与硬件一起工作?这个问题的一个答案是轮询(polling),内核可以定期检查系统中硬件的状态并做出相应的响应。然而,轮询会产生开销,因为无论硬件是活动的还是就绪的,轮询都必须重复进行。更好的解决方案是提供一种机制,让硬件在需要注意时向内核发出信号,这种机制称为中断(interrupt)。在本节中,我们将讨论中断以及内核如何响应它们,并使用称为中断处理程序(interrupt handler)的特殊函数。

image.png
无中断和有中断的程序控制流程。

18.3.6.1 中断概述

中断使硬件能够向处理器发送信号,例如,当用键盘键入时,键盘控制器(管理键盘的硬件设备)向处理器发出电信号,以提醒操作系统新可用的按键,这些电信号就是中断。处理器接收中断并向操作系统发送信号以使操作系统能够响应新数据,硬件设备相对于处理器时钟异步地生成中断,它们可以在任何时间发生。因此,内核可以随时中断以处理中断。

中断是由来自硬件设备的电子信号物理产生的,并被引导到中断控制器的输入引脚,中断控制器是一个简单的多线程芯片,将多条中断线组合成一条到处理器的单线。在接收到中断时,中断控制器向处理器发送信号,处理器检测到该信号并中断其当前执行以处理该中断。然后,处理器可以通知操作系统发生了中断,操作系统可以适当地处理中断。

不同的设备可以通过与每个中断相关联的唯一值与不同的中断相关联,来自键盘的中断与来自硬盘的中断是不同的,使得操作系统能够区分中断并知道哪个硬件设备导致了哪个中断。反过来,操作系统可以使用相应的处理程序为每个中断提供服务。

这些中断值通常称为中断请求(interrupt request,IRQ)线(line)。每个IRQ行都分配了一个数值,例如,在经典PC上,IRQ 0是计时器中断,IRQ 1是键盘中断。然而,并非所有的中断号都是如此严格地定义的,例如,与PCI总线上的设备相关的中断通常是动态分配的。其他非PC架构对中断值具有类似的动态分配,重要的是,一个特定的中断与一个特定设备相关联,内核知道这一点。然后硬件发出中断以引起内核的注意:“嘿,我有新的按键在等待!读取并处理这些坏孩子!”

image.png
带中断的指令周期。

18.3.6.2 中断请求级别

每个硬件中断都与一个优先级相关联,称为中断请求级别(Interrupt Request Level,IRQL)(注意,不要与称为IRQ的中断物理线混淆),由HAL确定。每个处理器的上下文都有自己的IRQL,就像任何寄存器一样。IRQL可以由CPU硬件实现,也可以不由CPU硬件来实现,但本质上并不重要,IRQL应该像其他CPU寄存器一样对待。

基本规则是处理器执行具有最高IRQL的代码。例如,如果某个CPU的IRQL在某个时刻为零,并且出现了一个IRQL为5的中断,它将在当前线程的内核堆栈中保存其状态(上下文),将其IRQL提升到5,然后执行与该中断相关联的ISR。一旦ISR完成,IRQL将下降到其先前的级别,继续执行先前执行的代码,就像中断不存在一样。当ISR执行时,IRQL为5或更低的其他中断无法中断此处理器。另一方面,如果新中断的IRQL高于5,CPU将再次保存其状态,将IRQL提升到新级别,执行与第二个中断相关联的第二个ISR,完成后,将返回到IRQL 5,恢复其状态并继续执行原始ISR。本质上,提升IRQL会暂时阻止IRQL等于或低于IRQL的代码。中断发生时的基本事件序列如下图所示,下下图显示了中断嵌套的样子。

image.png
基本中断调度流程。

image.png
嵌套中断流程。

上两图所示场景的一个重要事实是,所有ISR的执行都是由最初被中断的同一线程完成的。Windows没有处理中断的特殊线程,它们由当时在中断的处理器上运行的任何线程处理。我们很快就会发现,当处理器的IRQL为2或更高时,上下文切换是不可能的,因此在这些ISR执行时,不可能有其他线程潜入。

由于这些“中断”,被中断的线程不会减少其数量。可以说,这不是它的错。

当执行用户模式代码时,IRQL始终为0,这就是为什么在任何用户模式文档中都没有提到IRQL这个术语的原因之一——它总是为0,不能更改。大多数内核模式代码也使用IRQL 0运行,在内核模式下,可以在当前处理器上提升IRQL。

Windows使用API提升或降低IRQL的示例:

// assuming current IRQL <= DISPATCH_LEVEL
KIRQL oldIrql; // typedefed as UCHAR

// 提升IRQL
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);

NT_ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);

// 在IRQL的DISPATCH_LEVEL执行工作。

// 降低IRQ
KeLowerIrql(oldIrql);

18.3.6.3 线程优先级和IRQL

IRQL是处理器的一个属性,优先级是线程的属性,线程优先级仅在IRQL<2时才有意义。一旦执行线程将IRQL提升到2或更高,它的优先级就不再有任何意义了,理论上它拥有无限量程——会一直执行,直到IRQL降低到2以下。

当然,在IRQL>=2上花费大量时间不是一件好事,用户模式代码肯定没有运行,这只是在这些级别上执行代码的能力受到严格限制的原因之一。

Windows任务管理器显示使用称为系统中断的伪进程在IRQL2或更高版本中花费的CPU时间,Process Explorer将其称为“中断”。下图上半部分显示了Task Manager的屏幕截图,下半部分显示了Process Explorer中的相同信息。

image.png

18.3.6.4 中断处理器

内核响应特定中断而运行的函数称为中断处理程序或中断服务例程(interrupt service routine,ISR),每个产生中断的设备都有一个相关的中断处理程序。例如,一个函数处理来自系统计时器的中断,而另一个函数则处理键盘产生的中断。设备的中断处理程序是设备驱动程序(管理设备的内核代码)的一部分。

在Linux中,中断处理程序是正常的C函数。它们匹配一个特定的原型,这使得内核能够以标准方式传递处理程序信息,但除此之外,它们都是普通函数。中断处理程序与其他内核函数的区别在于,内核在响应中断时调用它们,并且它们在一个称为中断上下文(interrupt context)的特殊上下文中运行,这个特殊的上下文有时被称为原子上下文(atomic context),在此上下文中执行的代码不能被阻塞。

因为中断可以在任何时候发生,所以中断处理程序可以在任何时间执行,处理程序必须快速运行,以便尽快恢复被中断代码的执行。因此,虽然操作系统无延迟地为中断提供服务对硬件很重要,但对系统的其余部分来说,中断处理程序在尽可能短的时间内执行也是很重要的。

至少,中断处理程序的工作是向硬件确认中断的接收:“嘿,硬件,我听到了;现在回去工作吧!”然而,中断处理程序通常要执行大量的工作,例如,考虑网络设备的中断处理程序。除了响应硬件之外,中断处理器还需要将网络数据包从硬件复制到内存中,对其进行处理,并将数据包向下推送到适当的协议栈或应用程序。显然,可能需要大量工作,尤其是今天的千兆和10千兆以太网卡。

image.png
通过中断传输控制权。

在Linux的驱动程序中,请求中断行并安装处理程序是通过request_irq()完成的:

if(request_irq(irqn, my_interrupt, IRQF_SHARED, "my_device", my_dev)) 
{
    printk(KERN_ERR "my_device: cannot register IRQ %d\n", irqn);
    return -EIO;
}

18.3.6.5 中断上下文

执行中断处理程序时,内核处于中断上下文中,进程上下文是内核代表进程执行时的操作模式,例如,执行系统调用或运行内核线程。在进程上下文中,当前宏指向关联的任务。此外,由于进程在进程上下文中耦合到内核,进程上下文可以休眠或以其他方式调用调度器。

另一方面,中断上下文与进程无关,当前宏不相关(尽管它指向被中断的进程)。如果没有后备进程,中断上下文将无法休眠,它将如何重新调度?因此,不能从中断上下文中调用某些函数,如果函数处于休眠状态,则不能从中断处理程序中使用它,这限制了从中断处理函数中调用的函数。

中断上下文是时间关键的,因为中断处理程序会中断其他代码。代码应该快速简单。繁忙的循环是可能的,但不鼓励。请记住,中断处理程序中断了其他代码(甚至可能是另一行上的另一个中断处理程序!)。由于这种异步特性,所有中断处理程序都必须尽可能快和简单。尽可能地,工作应该从中断处理程序中推出,并在下半部分中执行,在更方便的时间运行。

中断处理程序堆栈的设置是一个配置选项,从历史上看,中断处理程序没有收到自己的堆栈,相反,他们将共享中断的进程堆栈。内核堆栈大小为两页,通常在32位体系结构上为8KB,在64位体系结构中为16KB。因为在这种设置中,中断处理程序共享堆栈,所以它们在分配数据时必须格外节约。当然,内核堆栈一开始是有限的,因此所有内核代码都应该谨慎。

在内核进程的早期,可以将堆栈大小从两页减少到一页,在32位系统上只提供4KB的堆栈,此举减少了内存压力,因为系统上的每个进程以前都需要两页连续的、不可扩展的内核内存。为了处理减少的堆栈大小,中断处理程序被赋予了自己的堆栈,每个处理器一个堆栈,一个页面大小,此堆栈称为中断堆栈(interrupt stack)。尽管中断堆栈的总大小是原始共享堆栈的一半,但可用的平均堆栈空间更大,因为中断处理程序可以自己获取整个内存页。

中断处理程序不应该关心正在使用的堆栈设置或内核堆栈的大小,应该始终使用绝对最小的堆栈空间。

18.3.6.6 实现中断处理程序

Linux中中断处理系统的实现依赖于体系结构,实现取决于处理器、使用的中断控制器类型以及体系结构和机器的设计。下图是中断通过硬件和内核的路径图。

image.png

设备通过其总线向中断控制器发送电信号来发出中断,如果中断线被启用,中断控制器将中断发送到处理器。在大多数架构中,通过特殊引脚发送到处理器的电信号来实现。除非中断在处理器中被禁用,否则处理器会立即停止它正在做的事情,禁用中断系统,并跳转到内存中的一个预定义位置并执行位于该位置的代码。这个预定义点由内核设置,是中断处理程序的入口点。

中断在内核中的过程从这个预定义的入口点开始,就像系统调用通过预定义的异常处理程序进入内核一样。对于每个中断行,处理器跳转到内存中的一个唯一位置并执行位于该位置的代码。通过这种方式,内核知道传入中断的IRQ号,初始入口点简单地保存该值并将当前寄存器值(属于中断的任务)存储在堆栈上,则内核调用do_IRQ()。从这里开始,大多数中断处理代码都是用C编写的,然而,它仍然依赖于体系结构。下面是处理IRQ的代码:

/**
* handle_IRQ_event - irq action chain handler
* @irq: the interrupt number
* @action: the interrupt action chain for this irq
*
* Handles the action chain of an irq event
*/
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction* action)
{
    irqreturn_t ret, retval = IRQ_NONE;
    unsigned int status = 0;
    if (!(action->flags & IRQF_DISABLED))
        local_irq_enable_in_hardirq();
    do {
        trace_irq_handler_entry(irq, action);
        ret = action->handler(irq, action->dev_id);
        trace_irq_handler_exit(irq, action, ret);
        switch (ret) {
        case IRQ_WAKE_THREAD:
            /*
            * Set result to handled so the spurious check
            * does not trigger.
            */
            ret = IRQ_HANDLED;
            /*
            * Catch drivers which return WAKE_THREAD but
            * did not set up a thread function
            */
            if (unlikely(!action->thread_fn)) {
                www.it - ebooks.info
                warn_no_thread(irq, action);
                break;
            }
            /*
            * Wake up the handler thread for this
            * action. In case the thread crashed and was
            * killed we just pretend that we handled the
            * interrupt. The hardirq handler above has
            * disabled the device interrupt, so no irq
            * storm is lurking.
            */
            if (likely(!test_bit(IRQTF_DIED,
                &action->thread_flags))) {
                set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
                wake_up_process(action->thread);
            }
            /* Fall through to add to randomness */
        case IRQ_HANDLED:
            status |= action->flags;
            break;
        default:
            break;
        }
        retval |= ret;
        action = action->next;
    } while (action);
    if (status & IRQF_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    local_irq_disable();
    return retval;
}

18.3.6.7 延迟过程调用

下图显示了客户端调用某些I/O操作时的典型事件序列,在图中,用户模式线程打开文件的句柄,并使用ReadFile函数发出读取操作。由于线程可以进行异步调用,因此它几乎立即重新获得控制权,并可以执行其他工作。接收到此请求的驱动程序将调用文件系统驱动程序(例如NTFS),该驱动程序可能会调用其下面的其他驱动程序,直到请求到达磁盘驱动程序,该磁盘驱动程序将在实际磁盘硬件上启动操作。在这一点上,没有代码需要执行,因为硬件“做它的事情”。

image.png
image.png

当硬件完成读取操作时,它发出一个中断,导致与中断相关联的中断服务例程在设备IRQL上执行(请注意,处理请求的线程是任意的,因为中断是异步到达的)。典型的ISR访问设备的硬件以获得操作结果,它的最终行动应该是完成最初的请求。

image.png
简单的中断处理过程。

18.3.6.8 异步过程调用

在Windows中,延迟过程调用(DPC)是封装在IRQLDISPATCH_LEVEL调用的函数的对象。就DPC而言,调用线程并不重要。异步过程调用(APC)也是封装要调用的函数的数据结构。但与DPC相反,APC的目标是特定线程,因此只有该线程才能执行该函数,意味着每个线程都有一个与其关联的APC队列。APC有三种类型:

  • 用户模式APC。仅当线程进入可报警状态时,这些APC在IRQL PASSIVE_LEVEL的用户模式下执行,通常通过调用API(如SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx和类似API)来完成。这些函数的最后一个参数可以设置为TRUE,以使线程处于可报警状态。在此状态下,它查看其APC队列,如果不是空的,则APC现在执行,直到队列为空。
  • 正常内核模式APC。这些APC在IRQL PASSIVE_LEVEL的内核模式下执行,并抢占用户模式代码和用户模式APC。
  • 特殊内核APC。这些APC在IRQL APC_LEVEL(1)的内核模式下执行,并抢占用户模式代码、正常内核APC和用户模式APC。I/O系统使用这些APC来完成I/O操作。
作者:向往
文章来源:https://zhuanlan.zhihu.com/p/579029158
推荐阅读
关注数
1667
文章数
213
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息