什么是操作系统内核?
内核是计算机操作系统核心的计算机程序,对系统中所有事务具有控制权。内核是操作系统代码的一部分,始终驻留在内存中,有助于硬件和软件组件之间的交互。一个完整的内核通过设备驱动控制所有的硬件资源(例如I/O,内存,加密)、仲裁有关此类资源的进程之间的冲突、优化公共资源(如CPU、缓存、文件系统)的利用、以及优化网络。
在大多数系统上,内核是启动时(在引导加载程序之后)加载的第一批程序。它处理启动的其余部分以及来自软件的内存、外围设备和输入/输出 (I/O) 请求,将它们转换为CPU的数据处理指令。
内核的关键代码通常被加载到单独的内存区域中,该区域受到保护,不会被应用软件或操作系统的其他不太关键的部分访问。内核在这个受保护的内核空间中执行其任务,例如运行进程、管理硬盘等硬件设备以及处理中断。相比之下,浏览器、文字处理器、音视频播放器等应用程序使用单独的内存区域,称为用户空间。这种分离可以防止用户数据和内核数据相互干扰并导致不稳定和缓慢,以及防止出现故障的应用程序影响其他应用程序或使整个操作系统崩溃。即使在内核被包含在用户程序空间的系统中,也需要内存保护来防止未经授权的应用程序修改内核。
内核的接口是一个低级抽象层。当一个进程向内核请求一个服务时,它必须调用一个系统调用,通常是通过一个包装函数。
不同的内核架构
计算机系统的内核负责执行程序,决定将许多正在运行的程序中的哪一个分配给一个或多个处理器。
Monolithic kernel 单片内核
单片内核完全在单个地址空间中运行,CPU 以超级用户模式执行,主要是为了提高速度。(Linux kernel)
Microkernels 微内核
微内核在用户空间中运行大部分但不是全部服务,就像用户进程一样,主要是为了弹性和模块化。(MINIX 3)
RAM(random-access memory)
随机存取存储器 (RAM) 用于存储程序指令和数据。通常,两者都需要存于内存中才能执行程序。 通常有多个程序需要访问内存,经常需要使用比计算机可用的内存更多的内存。 内核负责决定每个进程可以使用哪些内存,并决定在没有足够内存时做什么。
I/O devices
I/O设备包含键盘、鼠标、磁盘、打印机、USB设备、网络设备、显示设备等外围设备。内核将来自应用程序的设备请求发送至对应的设备,并提供便捷的方法来使用这些设备,通常通过将应用程序无需了解的底层细节抽象化。
Resource management
资源管理所需的关键方面是定义执行域(地址空间)和用于调解域内资源访问的保护机制。内核还提供了用于同步(synchronization)和进程间通信 (inter-process communication IPC) 的方法。这些实现可能位于内核本身,或者内核也可以依赖于它正在运行的其他进程。尽管内核必须提供 IPC 以提供对彼此提供的设施的访问,但内核还必须为正在运行的程序提供一种方法来请求访问这些设施。内核还负责进程或线程之间的上下文切换。
Memory management
内核可以访问系统所有的内存并且必须使进程安全的访问所需的内存。通常这样做的第一步是虚拟寻址,通常通过分页和/或分段来实现。虚拟寻址允许内核使给定的物理地址看起来是另一个地址,即虚拟地址。一个程序在特定虚拟地址访问的内存可能与另一个进程在同一地址访问的内存不同,允许每个程序都表现得好像它是唯一运行的程序(除了内核),从而防止应用程序相互崩溃。在许多系统上,程序的虚拟地址可能指的是当前不在内存中的数据。虚拟寻址提供的间接层允许操作系统使用其他数据存储(如硬盘驱动器)来存储原本必须保留在主存储器 ( RAM ) 中的数据。因此,操作系统可以允许程序使用比系统实际可用的内存更多的内存。当程序需要当前不在 RAM 中的数据时,CPU 会向内核发出信号,告知内核已经发生了这种情况,内核通过将非活动内存块的内容写入磁盘(如果需要)并将其替换为请求的数据来响应该程序。然后可以从停止的点恢复程序。这种方案通常被称为需求寻呼。
虚拟寻址还允许在两个不相交的区域中创建内存的虚拟分区,一个为内核(内核空间)保留,另一个为应用程序(用户空间)保留。 处理器不允许应用程序寻址内核内存,从而防止应用程序损坏正在运行的内核。 这种基本的内存空间划分对实际通用内核的当前设计做出了很大贡献,并且在此类系统中几乎是通用的,尽管一些研究内核(例如,Singularity)采用其他方法。
Device management
进程有时需要访问外围设备,这些外围设备由内核通过相应的驱动程序控制。驱动程序通过其硬件/软件接口代表操作系统封装、监视和控制硬件设备。它为操作系统提供了一个 API、程序和有关如何控制某个硬件并与之通信的信息。设备驱动程序是所有操作系统及其应用程序的重要且至关重要的依赖项。驱动程序的设计目标是抽象;驱动程序的功能是将操作系统授权的抽象函数调用(编程调用)转换为特定于设备的调用。理论上,设备应该与合适的驱动程序一起正常工作。设备驱动程序用于例如视频卡、声卡、打印机、扫描仪、调制解调器和网卡。
例如,为了在屏幕上向用户显示某些东西,应用程序会向内核发出请求,内核会将请求转发给其显示驱动程序,然后由后者负责实际绘制字符/像素。
内核必须维护可用设备的列表。该列表可能是预先知道的(例如,在嵌入式系统上,如果可用的硬件发生变化,内核将被重写),由用户配置(通常在较旧的 PC 和非为个人使用而设计的系统上)或由用户检测到运行时的操作系统(通常称为即插即用)。在即插即用系统中,设备管理器首先在不同的外围总线上执行扫描,例如外围组件互连(PCI) 或通用串行总线(USB),以检测已安装的设备,然后搜索适当的驱动程序。
由于设备管理是一个非常特定于操作系统的主题,因此每种内核设计对这些驱动程序的处理方式不同,但在每种情况下,内核都必须提供I/O以允许驱动程序通过某些端口)或内存物理访问其设备地点。在设计设备管理系统时必须做出重要决定,因为在某些设计中访问可能涉及上下文切换,这使得操作非常占用 CPU 并容易导致显着的性能开销。
System calls
进程通过系统调用从操作系统内核请求通常情况下无权运行的服务。系统调用提供进程和操作系统之间的接口。大多数与系统交互的操作都需要用户级进程不可用的权限,例如,使用系统上存在的设备执行的 I/O,或与其他进程的任何形式的通信都需要使用系统调用。
系统调用是应用程序用来向操作系统请求服务的机制。他们使用机器代码指令导致处理器改变模式。一个例子是从监督模式到保护模式。这是操作系统执行访问硬件设备或内存管理单元等操作的地方。通常,操作系统提供了一个位于操作系统和普通用户程序之间的库。通常它是一个C 库),例如Glibc或 Windows API。该库处理将信息传递给内核和切换到主管模式的低级细节。系统调用包括关闭、打开、读取、等待和写入。
要真正执行有用的工作,进程必须能够访问内核提供的服务。每个内核的实现方式不同,但大多数都提供了C 库或API,它们依次调用相关的内核函数。调用内核函数的方法因内核而异。如果使用内存隔离,用户进程就不可能直接调用内核,因为这会违反处理器的访问控制规则。几种可能性是:
- 使用软件模拟中断。这种方法在大多数硬件上都可用,因此非常常见。
- 使用呼叫门。调用门是内核存储在内核内存列表中处理器已知位置的特殊地址。当处理器检测到对该地址的调用时,它会重定向到目标位置,而不会导致访问冲突。这需要硬件支持,但它的硬件很常见。
- 使用特殊的系统调用指令。这种技术需要特殊的硬件支持,而常见的架构(尤其是x86)可能缺乏这种支持。但是,最近的 x86 处理器型号中已添加了系统调用指令,并且一些用于 PC 的操作系统在可用时会使用它们。
- 使用基于内存的队列。发出大量请求但不需要等待每个请求的结果的应用程序可能会将请求的详细信息添加到内核定期扫描以查找请求的内存区域。
内核设计决策
保护
内核设计中的一个重要考虑因素是它为防止故障(容错)和恶意行为(安全性)提供的支持。 这两个方面通常没有明确区分,在内核设计中采用这种区分会导致拒绝使用分层结构进行保护。
内核提供的机制或策略可以根据几个标准进行分类,包括:静态(在编译时强制执行)或动态(在运行时强制执行); 先发制人或后检测; 根据他们满足的保护原则(例如,Denning); 它们是硬件支持的还是基于语言的; 它们是更多的开放机制还是具有约束力的政策。
对分层保护域的支持通常使用 CPU 模式实现。
许多内核提供了“功能”的实现,即提供给用户代码的对象,它们允许对由内核管理的底层对象的有限访问。一个常见的例子是文件处理:文件是存储在永久存储设备上的信息表示。内核能够执行许多不同的操作,包括读、写、删除或执行,但是用户级应用程序可能只允许执行其中的一些操作(例如,它可能只允许读取文件)。常见实现是内核为应用程序提供一个对象(通常称为“文件句柄”),然后应用程序可以调用该操作,内核在请求操作时检查该操作的有效性。这样的系统可以扩展到覆盖内核管理的所有对象,实际上也可以覆盖由其他用户应用程序提供的对象。
提供功能硬件支持的一种有效简单的方法是将检查每个内存访问的访问权限的责任委派给内存管理单元(MMU),这种机制称为基于能力的寻址。大多数商用计算机架构缺乏对功能的MMU支持。
另一种方法是使用通常支持的层次结构域来模拟功能。在这种方法中,每个受保护的对象必须驻留在应用程序无法访问的地址空间中;内核还在这些内存中维护一个功能列表。当应用程序需要访问受功能保护的对象时,它执行系统调用,然后内核检查应用程序的功能是否授予它执行请求操作的权限,以及是否允许它执行访问(直接,或将请求委托给另一个用户级进程)。地址空间切换的性能成本限制了这种方法在对象之间具有复杂交互的系统中的实用性,但在当前的操作系统中,它用于不频繁访问或期望不会快速执行的对象。
一个重要的内核设计决策是选择应该实现安全机制和策略的抽象级别。内核安全机制在支持更高级别的安全性方面发挥着关键作用。
一种方法是使用固件和内核支持来实现容错(见上文),并在此基础上构建针对恶意行为的安全策略(必要时添加加密机制等特性),并将一些责任委托给编译器。将安全策略的执行委托给编译器和/或应用程序级别的方法通常称为基于语言的安全性。
进程合作
Edsger Dijkstra证明,从逻辑的角度来看,在二进制信号量上操作的原子锁和解锁操作是表达它的任何过程协作功能的充分原语。然而,这种方法通常被认为在安全性和效率方面缺乏缺陷,而消息传递方法更灵活。还有许多其他方法(较低级别或较高级级别)可用,许多现代内核都支持共享内存(shared memory)和远程过程调用(RPC)等系统。
I/O设备管理
与物理内存类似,允许应用程序直接访问控制器端口和寄存器可能会导致控制器故障或系统崩溃。有了这个,根据设备的复杂性,一些设备的编程可能会变得异常复杂,并使用几个不同的控制器。因此,提供更抽象的接口来管理设备很重要。此接口通常由设备驱动程序或硬件抽象层完成。通常,应用程序需要访问这些设备。内核必须通过以某种方式查询系统来维护这些设备的列表。这可以通过 BIOS或通过各种系统总线之一(例如IPC)。
内核常见的设计方法
机制和策略分离的原则是micro kernel和monolithic kernel之间的重大差别。机制支持众多不同策略的实现,策略是一种特定的操作模式。例如:
- Mechanism:用户的登录尝试被路由到授权服务器;
- Policy:授权服务器需要密码,该密码已经根据数据库中存储的密码进行验证;
因为策略和机制是分开的,所以策略可以很容易地改变,例如使用安全令牌。
在最小的微内核中只包含了一些非常基本的策略,它的机制允许在内核之上运行操作系统其余的部分以及其他应用来决定采取哪种策略(例如内存管理、高级进程调度、文件系统管理)。单体内核(monolithic kernel)倾向于包含众多策略。
单体内核在同一地址空间(内核空间)中执行所有代码,而微内核尝试在用户空间中运行大部分服务,旨在提高代码库的可维护性和模块化。大多数内核并不完全适合这些类别之一,而是介于这两种设计之间。这些被称为混合内核。更奇特的设计例如纳米内核和外内核可以被使用,但很少用于生产系统。例如,Xen管理程序是一个外内核。
Monolithic kernel 单体内核
在单片内核中,所有操作系统服务都与主内核线程一起运行,因此也驻留在同一内存区域中。这种方法提供了丰富而强大的硬件访问。一些开发人员,例如 UNIX 开发人员 Ken Thompson,坚持认为它比微内核“更容易实现单体内核”。单体内核的主要缺点是系统组件之间的依赖关系——设备驱动程序中的错误可能会使整个系统崩溃——以及大内核可能变得非常难以维护的事实。
传统上由类 Unix 操作系统使用的单体内核包含所有操作系统核心功能和设备驱动程序。这也是Unix系统的传统设计方式。单体内核是一个单独的程序,其中包含执行每个与内核相关的任务所需的所有代码。大多数程序要访问但不能放入库中的部分都在内核空间中:设备驱动程序、调度程序、内存处理、文件系统和网络堆栈。许多系统调用被提供给应用程序,以允许它们访问所有这些服务。
虽然单体内核最初可能加载不需要的子系统,但是从一般意义上说,可以被优化到与针对硬件进行设计的系统一样快甚至更快。现代的单体内核例如Linux(GNU操作系统内核之一)以及FreeBSD内核都是类Unix操作系统,具有在运行时加载模块的能力,从而允许根据需要轻松扩展内核的功能,同时有助于最大限度地减少内核空间中运行的代码量。单体内核优势主要有:
- 由于涉及的软件较少,因此速度更快。
- 由于它是一个单一的软件,它的源代码和编译形式都应该更小。
- 更少的代码通常意味着更少的错误可以转化为更少的安全问题。
单体内核中的大部分工作都是通过系统调用完成的。这些接口通常保存在表格结构中,用于访问内核中的某些子系统,例如磁盘操作。本质上调用是在程序中进行的,请求的检查副本通过系统调用传递。单片 Linux 内核可以做得非常小,不仅因为它能够动态加载模块,还因为它易于定制。这种将内核小型化的能力也导致 Linux 在嵌入式系统中的使用迅速增长。
这些类型的内核由操作系统的核心功能和能够在运行时加载模块的设备驱动程序组成。它们提供了丰富而强大的底层硬件抽象。它们提供一小组简单的硬件抽象,并使用称为服务器的应用程序来提供更多功能。这种特殊的方法在硬件上定义了一个高级虚拟接口,用一组系统调用来实现操作系统服务,例如进程管理、并发和内存管理在几个以超级用户模式运行的模块中。这种设计也有一些限制:
- 在内核中编码可能具有挑战性,部分原因是无法使用通用库(如功能齐全的 libc),并且需要使用源级调试器,如 gdb。 通常需要重新启动计算机。 这不仅仅是开发人员方便的问题。 当调试变得更难时,随着难度变得越来越大,代码就越有可能变得“buggier”。
- 内核某一部分的错误具有很强的副作用;因为内核中的每个函数都拥有所有的权限,一个函数中的错误可能会破坏另一个完全不相关的内核部分或任何正在运行的程序的数据结构。
- 内核经常变得非常大并且难以维护。
- 即使服务这些操作的模块与整体分离,代码集成也很紧密,很难正确完成。
- 由于模块在相同的地址空间中运行,因此一个错误可能会导致整个系统瘫痪。
- 单体内核不可移植;因此,必须为要使用操作系统的每个新架构重写它们。
单体内核有 AIX 内核、HP-UX 内核和 Solaris 内核。
Micro kernels 微内核
微内核(也缩写为 μK 或 uK)是描述操作系统设计方法的术语,通过该方法将系统的功能从传统的“内核”移出,进入到一组通过“最小”内核通信的“服务器” ,使得内核尽可能少的留在“系统空间”,尽可能多的留在“用户空间”。
为特定平台或设备设计的微内核只会拥有它需要运行的东西。微内核方法包括在硬件上定义一个简单的抽象,使用一组原语或系统调用来实现最小的操作系统服务,例如内存管理、多任务处理和进程间通信。其他服务,包括通常由内核提供的服务,例如网络,在用户空间程序中实现,称为服务器。微内核比单片内核更容易维护,但大量的系统调用和上下文切换可能会减慢系统速度,因为它们通常比普通函数调用产生更多的开销。
只有真正需要处于特权模式的部分位于内核空间中:IPC(进程间通信)、基本调度程序或调度原语、基本内存处理、基本 I/O 原语。许多关键部分现在都在用户空间中运行:完整的调度程序、内存处理、文件系统和网络堆栈。
微内核与传统的单体内核具有鲜明的不同,单体内核所有的系统功能都被放在一个静态的程序中,该程序运行在处于特殊系统模式的处理器中。在微内核设计模式下,只有最基本的任务,例如访问基础硬件、管理内存和协调进程间消息传递才会被执行。有些系统使用的是微内核例如:QNX以及Hurd。微内核的主要优势为:
- 更容易维护
- 补丁可以在单独的实例中进行测试,然后换入以接管生产实例
- 无需重新启动内核即可测试快速开发时间和新软件
- 一般来说,更多的持久性,如果一个实例出现故障,通常可以用一个操作镜像代替它
大多数微内核使用消息传递系统来处理从一台服务器到另一台服务器的请求。消息传递系统通常以端口为基础与微内核一起运行。例如,如果发送更多内存的请求,则使用微内核打开一个端口并通过该请求发送。一旦进入微内核,这些步骤类似于系统调用。尽管微内核本身非常小,但结合它们所需的所有辅助代码,实际上它们通常比单片内核大。单片内核的拥护者还指出,微内核系统的两层结构中大多数操作系统不直接与硬件交互,在系统效率方面产生了不小的成本。这些类型的内核通常只提供最低限度的服务,例如定义内存地址空间、进程间通信 (IPC) 和进程管理。其他功能(例如运行硬件进程)不直接由微内核处理。微内核的支持者指出,那些单片内核的缺点是内核中的错误会导致整个系统崩溃。但是,对于微内核,如果内核进程崩溃,仍然可以通过重新启动导致错误的服务来防止整个系统崩溃。
内核提供的其他服务(例如网络)在称为服务器的用户空间程序中实现。服务器允许通过简单地启动和停止程序来修改操作系统。例如,对于没有网络支持的机器,网络服务器不会启动。进出内核以在各种应用程序和服务器之间移动数据的任务会产生开销,与单片内核相比,这不利于微内核的效率。微内核也存在一些不足,例如:
- 更大的运行内存占用
- 需要更多用于接口的软件,可能会降低性能
- 与单片内核中的一次性副本相比,消息错误可能更难修复,因为它们必须花费更长的时间
- 一般来说,流程管理可能非常复杂
微内核的缺陷有时需要实际考虑。对于小型系统因为不需要运行太多的程序微内核往往工作的很好,进程管理的复杂度也可以很好的缓解。
微内核允许将操作系统的其余部分实现为用高级语言编写的普通应用程序,并在相同的未更改内核之上使用不同的操作系统。还可以在操作系统之间动态切换并同时激活多个操作系统。
Monolithic kernel vs Micro kernels
随着计算机内核的发展,其大小和安全需求也在增加。内核的不断增长不仅带来了安全隐患还增加了内存占用。通过虚拟内存技术在一定程度上可以缓解上述问题,但是并非所有的计算机架构都支持虚拟内存技术。为了降低内核的内存占用,开发人员往往需要进行额外的修改来删除不需要的代码,对于各组件依赖关系并不明显的具有数百万行代码的内核来说这一优化非常困难。1990s,因为与微内核相比存在诸多缺点,几乎所有的操作系统研究人员都认为单体内核已经过时。Linux系统应该被设计为单体内核还是微内核在Linus Torvalds and Andrew Tanenbaum之间引发了一场著名的辩论,双方各据优点。
性能
单体内核将其所有的代码放入相同的地址空间(内核空间),一些开发者认为这对于提升系统性能来说是必要的,还有一些开发者认为单体内核系统在好好编写的情况下是及其高效的。通过共享内核内存共享取代微内核中基于消息传递的IPC,单体内核模型可以更加高效。
微内核在1980s和1990s时期性能表现低下,但是研究只是在经验上统计了这些微内核的性能,并没有微内核性能低下的原因。对此的解释逐渐称为约定俗称的经验,一般将微内核的低性能归结于内核模式和用户模式的频繁切换、进程内部通信的增加、以及上下文切换的增加。1995年微内核性能不足的原因主要被总结为:1)微内核这种方法就是性能低下的;2)微内核中实现的某些概念是性能低下的;3)某些概念的具体实现是性能低下的。因此和以往的尝试不同,为了构建高效的微内核是否应该采用正确的构造技术也在被研究。
另一方面,对于导致单体内核产生的分层保护域架构,每当不同保护层级之间需要交互的时候具有性能缺陷,因为需要值的消息复制(如当一个进程需要同时在用户模式和超级模式下修改一个数据结构时)。
Hybrid (or modular) kernels 混合内核
混合内核在大多数商用操作系统,例如Microsoft Windows NT 3.1, NT 3.5, NT 3.51, NT 4.0, 2000, XP, Vista, 7, 8, 8.1 and 10. Apple Inc‘s macOS都采用了混合内核。混合内核力求将单体内核的运行高速、结构简单与微内核的模块化、执行安全性相结合。混合内核与微内核相似,区别在于其在内核空间增加了额外的代码以提升性能。混合内核其实是开发人员参考了单体内核以及微内核两者优势之后,采取的折衷方案,混合内核是微内核的扩展,融合了某些单体内核的特性。与单体内核不同,混合内核无法在运行时自行加载模块。混合内核是微内核,在内核空间中有一些“非必要”代码,将这些代码放在内核空间为不是用户空间可以使代码运行更快。混合内核是单体内核与微内核设计之间的折衷,该设计为了降低传统微内核的性能开销会在内核空间运行一些服务(例如网络栈、文件系统),但是仍然会将内核代码(例如设备驱动程序)作为服务器运行在用户空间。
很多传统的单体内核目前都添加了模块化能力,其中最著名的是Linux内核。模块化内核本质上可以将其中的一部分内置到核心内核二进制文件中,或者按需加载到内存中的二进制文件中。但是也有人担心损坏的模块会破坏正在运行的操作系统。当一个内核模块被加载,其访问单体内核的内存空间并添加自己所需的部分,这也会导致可能出现的污染。
总的来说混合内核的优势在于:
- 对于可在模块内运行的驱动程序来说开发时间更快。在内核稳定的情况下不需要为了测试重启系统;
- 不需要为了添加新驱动或子系统花费时间重编译整个内核;
- 更快速地集成第三方技术;
通常来说模块通过模块接口与内核进行通信。接口相对于给定的操作系统来说是通用的,所以模块有时并不通用。设备驱动器通常需要提供比模块更多的灵活性。本质上说此时需要两次系统调用,在单体内核中仅需一次的安全检查现在需要两次。模块化的缺点有:
- 随着更多的接口要通过,存在增加错误的可能性(这意味着更多的安全漏洞)。
- 在处理符号差异等问题时,维护模块可能会让一些管理员感到困惑。
Nanokernels 纳米内核
超微内核将几乎所有服务(包括中断控制器或计时器等最基本的服务)委托给设备驱动程序,以使内核内存需求比传统微内核更小。