了解操作系统及其内核对系统性能分析至关重要。您经常需要制定并测试有关系统行为的假设,例如系统调用的执行方式、CPU如何调度线程、有限的内存如何影响性能以及文件系统如何处理I/O。这些行为将需要您应用您对操作系统和内核的知识。
本章概述了操作系统和内核,并假定读者已具备相关知识,是本书的基础。如果您错过了操作系统课程,可以将本章视为快速课程。请留意您所了解的知识中是否存在任何漏洞,因为本章末尾将有一个小测验(开玩笑,只是一个小测试)。有关内核内部的更多信息,请参见本章末尾的参考文献和参考书目。
本章分为两个部分:
背景介绍了术语和操作系统基础知识。
内核概述了基于Linux和Solaris的内核。
有关性能的相关领域,包括CPU调度、内存、磁盘、文件系统、网络和许多特定的性能工具,将在接下来的章节中进行更详细的介绍。
3.1 Terminology
供参考,以下是本书中使用的核心操作系统术语:
操作系统:这指的是安装在系统上的软件和文件,使其能够启动和执行程序。它包括内核、管理工具和系统库。
内核:内核是管理系统的程序,包括设备(硬件)、内存和CPU调度。它运行在允许直接访问硬件的特权CPU模式中,称为内核模式。
进程:用于执行程序的OS抽象和环境。程序通常在用户模式下运行,并通过系统调用或陷阱访问内核模式(例如,执行设备I/O)。
线程:可安排在CPU上运行的可执行上下文。内核有多个线程,一个进程包含一个或多个线程。
任务:Linux可运行实体,可以引用单个线程的进程、来自多线程进程的线程或内核线程。
内核空间:内核的内存地址空间。
用户空间:进程的内存地址空间。
用户层:用户级程序和库(/usr/bin、/usr/lib等)。
上下文切换:内核例程,它将CPU切换到不同的进程地址空间(上下文)中进行操作。
系统调用(syscall):用户程序请求内核执行特权操作的一种明确定义的协议,包括设备I/O。
处理器:不要与进程混淆,处理器是一个包含一个或多个CPU的物理芯片。
陷阱:发送到内核的信号,请求系统例程(特权操作)。陷阱类型包括系统调用、处理器异常和中断。
中断:由物理设备发送到内核的信号,通常是请求I/O服务的信号。中断是陷阱的一种类型。
术语表中包括更多本章需要参考的术语,包括地址空间、缓冲区、CPU、文件描述符、POSIX和寄存器。
3.2 Background
以下各节描述操作系统的概念和通用内核内部。这些部分之后将涵盖特定的内核差异。
3.2.1 Kernel
内核管理CPU调度、内存、文件系统、网络协议和系统设备(磁盘、网络接口等)。它通过系统调用提供对设备和基于它们构建的内核服务的访问。图3.1显示了它的示意图。
//Applications如何直接访问System Calls?
还显示了系统库,通常用于提供比仅使用系统调用更丰富和更易于编程的接口。应用程序包括所有正在运行的用户级软件,包括数据库、Web服务器、管理工具和操作系统Shell。
在这里,系统库被描绘为一个断开的环,以显示应用程序可以直接调用系统调用(如果操作系统允许)。传统上,这个图示是用完整的环来绘制的,反映了从中心核心开始的不同特权级别的递减(这个模型起源于Multics[Graham 68],Unix的前身)。
Kernel Execution
内核是一个庞大的程序,通常包含数十万行代码。它主要根据需求执行,当用户级程序发出系统调用或设备发送中断时执行。一些内核线程异步运行以进行日常维护工作,这可能包括内核时钟例程和内存管理任务,但它们试图轻量化并且消耗非常少的CPU资源。频繁进行I/O操作的工作负载(如Web服务器)通常在内核上下文中执行。计算密集型的工作负载尽可能地被内核保持独立,以便在CPU上无间断地运行。也许会认为内核不能影响这些工作负载的性能,但实际上存在许多情况会受到影响。最明显的是CPU争用,当其他线程竞争CPU资源时,内核调度器需要决定哪个线程将运行,哪个线程将等待。内核还会选择在哪个CPU上运行线程,并且可以选择具有较暖的硬件缓存或更好的进程内存局部性的CPU,从而显著提高性能。
//感觉multi-core cpu越来越重要了!
Clock
Unix内核的一个核心组件是clock()例程,它是从定时器中断执行的。它在过去通常以每秒60次、100次或1,000次的频率执行,每次执行被称为一个tick。它的功能包括更新系统时间、处理定时器和线程调度的时间片、维护CPU统计信息以及执行调用(预定的内核例程)。
在早期的内核中,clock存在性能问题,在后续的内核中得到改进,包括:
- Tick延迟:对于100 Hz的时钟,一个定时器可能会等待下一个tick的处理而产生高达10毫秒的额外延迟。通过使用高分辨率实时中断来修复这个问题,使得执行立即进行而无需等待。
- Tick开销:现代处理器具有动态电源特性,可以在空闲期间关闭部分电源。而clock例程会打断这个过程,对于空闲系统来说可能会不必要地消耗电力。Linux实现了动态ticks,因此当系统处于空闲状态时,定时器例程(clock)不会触发。
现代内核将许多功能从clock例程移出到按需中断中,以创建一个无tick的内核。其中包括Linux,其中的clock例程(即系统定时器中断)除了更新系统时钟和jiffies计数器(jiffies是Linux中的一种时间单位,类似于ticks)之外几乎不执行其他工作。
Kernel Mode
内核是在一种称为内核模式的特殊CPU模式下运行的唯一程序,允许对设备进行完全访问并执行特权指令。内核调解设备访问以支持多任务处理,防止进程和用户在没有明确许可的情况下访问彼此的数据。
用户程序(进程)在用户模式下运行,它们通过系统调用向内核请求特权操作,例如进行I/O操作。要执行系统调用,执行将从用户模式切换到内核模式,然后以较高的权限级别执行。这在图3.2中显示。
每个模式都有自己的软件执行状态,包括堆栈和寄存器。在用户模式下执行特权指令会引发异常,然后由内核进行适当处理。
这些模式(用户模式和内核模式)之间的切换需要时间(CPU周期),这为每个I/O操作增加了一小部分开销。一些服务,例如NFS,已被实现为内核模式软件(而不是用户模式守护程序),这样它们可以在不需要切换到用户模式的情况下执行与设备之间的I/O操作。
如果系统调用在执行过程中阻塞,进程可能会从CPU上切换下来,并被另一个进程取代:这称为上下文切换。
3.2.2 Stacks
栈以函数和寄存器的形式包含了线程的执行祖先。CPU使用栈来高效地处理本地软件中的函数执行。
当调用一个函数时,当前CPU寄存器的集合(存储CPU状态)会保存到栈中,并在栈顶为当前线程的当前执行添加一个新的栈帧。函数通过调用“返回”CPU指令来结束执行,该指令会移除当前的栈并将执行返回到先前的栈中,恢复其状态。
栈检查是调试和性能分析的宝贵工具。栈显示了当前执行的调用路径,这通常可以解答为什么会执行某个操作的问题。
How to Read a Stack
下面是一个示例内核栈(来自Linux),显示了TCP传输的路径,由一个调试工具打印出来:
栈的顶部通常显示为第一行。在这个示例中,它包括了正在执行的函数的名称tcp_sendmsg。函数名称的左右两边是调试器通常包含的细节:内核模块位置(`kernel)和指令偏移量(0x1,它指的是函数内的指令地址)。
调用tcp_sendmsg()的函数(其父函数)可以在其下面看到:inet_sendmsg()。而其父函数则在其下面:sock_aio_write()。通过阅读栈,可以看到完整的祖先链:函数、父函数、祖父函数等等。或者,通过自底向上阅读,可以追踪到当前函数的执行路径:我们是如何到达这里的。
由于栈展示了源代码中的内部路径,这些函数通常没有其他文档,除了代码本身。对于这个示例栈来说,这就是Linux内核源代码。一个例外是那些作为API的一部分并且有公共文档的函数。
User and Kernel Stacks
在执行系统调用时,一个进程线程有两个栈:用户级栈和内核级栈。它们的范围如图3.3所示。
在系统调用期间,被阻塞的线程的用户级栈不会改变,因为线程在内核上下文中执行时使用的是一个单独的内核级栈。(一个例外是信号处理程序,根据其配置可能会借用一个用户级栈。)
3.2.3 Interrupts and Interrupt Threads
除了响应系统调用外,内核还会响应来自设备的服务请求。这些被称为中断,因为它们会中断当前的执行。这在图3.4中有所描述。
设备中断会注册一个中断服务例程来处理。这些例程被设计成尽可能快地运行,以减少对活动线程的中断影响。如果一个中断需要执行更多的工作,特别是如果它可能在锁上阻塞,那么可以由内核调度一个中断线程来处理。
这个实现取决于内核版本。在Linux上,设备驱动程序可以被建模为两个部分,顶半部分快速处理中断,并将工作调度给底半部分以便稍后处理[Corbet 05]。快速处理中断很重要,因为顶半部分在禁止中断模式下运行,以推迟新中断的传递,如果运行时间过长,可能会对其他线程造成延迟问题。底半部分可以是任务(let)或工作队列(work queue);后者是由内核调度并在需要时可以睡眠的线程。基于Solaris的系统会将中断提升为中断线程,如果需要执行更多的工作[McDougall 06a]。
从中断到达到服务的时间被称为中断延迟,这取决于具体的实现方式。这是实时或低延迟系统研究的课题。
3.2.4 Interrupt Priority Level
中断优先级(IPL)表示当前活动的中断服务例程的优先级。在传递中断信号期间,它是从处理器中读取的,只有当其优先级高于当前正在执行的中断(如果有的话),中断才会成功;否则,中断将排队等待稍后处理。这样可以防止较低优先级的工作中断较高优先级的工作。
图3.5显示了一个例子中断优先级范围,对于此内核服务而言,IPL1到IPL10是中断线程。
串行I/O具有较高的中断优先级,因为其硬件缓冲区通常较小,需要快速服务以避免溢出。
3.2.5 Processes
进程是执行用户级程序的环境。它由内存地址空间、文件描述符、线程栈和寄存器组成。在某些方面,进程类似于虚拟的早期计算机,只有一个程序正在执行,具有自己的寄存器和堆栈。
内核通过多任务处理进程,通常在单个系统上支持数千个进程的执行。它们分别由它们的进程ID(PID)唯一标识。
进程包含一个或多个线程,它们在进程地址空间中操作并共享相同的文件描述符(表示打开文件的状态)。线程是一个可执行的上下文,由堆栈、寄存器和程序计数器组成。多个线程允许单个进程在多个CPU上并行执行。
Process Creation
通常使用fork()系统调用创建进程。这将创建一个进程的副本,具有自己的进程ID。然后可以调用exec()系统调用来开始执行不同的程序。
图3.6显示了一个示例进程创建过程,shell(sh)执行ls命令。
fork()系统调用可能使用写时复制(COW)策略来提高性能。这会向先前的地址空间添加引用,而不是复制所有内容。一旦任何一个进程修改了多次引用的内存,就会为修改创建一个单独的副本。该策略延迟或消除了复制内存的需要,减少内存和CPU的使用。
Process Life Cycle
进程的生命周期如图3.7所示。这是一个简化的图表;对于现代多线程操作系统,调度和运行的是线程,并且有关如何将这些映射到进程状态的一些额外实现细节(供参考,请参阅内核源代码中的proc.h文件)。
在处理器(CPU)上运行时,进程处于运行状态。准备运行状态是指进程可运行,但正在等待在 CPU 运行队列中轮到它运行。I/O 操作会阻塞进程,将其置于睡眠状态,直到 I/O 完成并唤醒进程。僵尸状态发生在进程终止期间,进程将等待其进程状态被父进程读取,或者直到被内核移除。
Process Environment
进程环境如图3.8所示,它由进程的地址空间中的数据和内核中的元数据(上下文)组成。
//这个图很清晰!!!
内核上下文包括各种进程属性和统计信息:进程ID(PID),所有者的用户ID(UID)以及各种时间。通常可以通过ps(1)命令进行检查。它还具有一组文件描述符,用于引用打开的文件,这些文件描述符(通常)在线程之间共享。
此示例显示了两个线程,每个线程都包含一些元数据,包括内核上下文中的优先级和用户地址空间中的堆栈。该图表没有按比例绘制;与进程地址空间相比,内核上下文非常小。
用户地址空间包含进程的内存段:可执行文件、库和堆。有关更多详细信息,请参阅第7章"内存"。
3.2.6 System Calls
系统调用是请求内核执行特权系统例程的操作。可用的系统调用数量在数百个,但会尽量保持这个数量尽可能小,以保持内核的简洁性(Unix哲学;[Thompson 78])。更复杂的接口可以在用户空间构建在它们之上作为系统库,在那里更容易开发和维护。
需要记住的关键系统调用列在表3.1中。
系统调用都有详细的文档,通常与操作系统一起提供。它们的接口通常简单而一致,其中包括设置一个特殊变量 errno,以指示是否遇到错误及其类型。
许多系统调用有明显的目的。以下是一些常见用途不太明显的系统调用:
ioctl():通常用于向内核请求各种操作,特别是对于系统管理工具,在其他(更明显)的系统调用不适用的情况下使用。可以参考下面的示例。
mmap():通常用于将可执行文件和库文件映射到进程的地址空间中,以及进行内存映射文件。有时它也被用来分配进程的工作内存,而不是基于 brk() 的 malloc(),以减少系统调用的频率并提高性能(这并不总是有效的,因为这涉及到权衡:内存映射管理)。
brk():用于扩展堆指针,定义进程的工作内存大小。通常由系统内存分配库执行,当堆中的现有空间无法满足 malloc()(内存分配)调用时使用。详见第7章"内存"。
如果对某个系统调用不熟悉,可以在其 man page 中了解更多信息(这些位于第2节:syscalls)。
ioctl() 系统调用可能是最难学习的,因为其含义不明确。以 Linux 中的 perf(1) 工具(在第6章"处理器"中介绍)的使用为例,它执行特权操作以协调性能工具。而不是为每个动作添加系统调用,只添加了一个系统调用:perf_event_open(),它返回一个文件描述符,可与 ioctl() 一起使用。然后可以使用不同的参数来调用这个 ioctl() 以执行不同的操作。例如,ioctl(fd, PERF_EVENT_IOC_ENABLE) 可以启用性能工具。在这个示例中,参数 PERF_EVENT_IOC_ENABLE 更容易由开发人员添加和更改。
/*mmap内存映射的内存和malloc()分配的堆内存的区别:
1 malloc() 分配的堆内存来自进程的堆空间,而 mmap() 内存映射的内存通常与文件或设备相关联。
2 对于 malloc() 分配的堆内存,一般只有当前进程可以直接访问。而 mmap() 内存映射的内存可以通过多个进程共享,实现进程间的通信和共享数据。
*/
/*
进程内存分布图:
https://blog.csdn.net/m0_65346989/article/details/130395560
问题:mmap内存映射的内存对应图中哪个位置?
*/
3.2.7 Virtual Memory
虚拟内存是主存的一种抽象,为进程和内核提供了自己的、几乎无限的私有主存视图。它支持多任务处理,允许进程和内核在自己的私有地址空间中运行而无需担心冲突问题。它还支持超额订阅主存,允许操作系统根据需要在主存和二级存储(磁盘)之间透明地映射虚拟内存。虚拟内存的作用如图 3.9 所示。主存是主内存(RAM),次存是存储设备(磁盘)。
虚拟内存的实现依赖于处理器和操作系统的支持。它并非真实的内存,大多数操作系统只在需要时,即内存首次被使用(写入)时,将虚拟内存映射到真实内存中。有关虚拟内存的更多信息,请参阅第7章"内存"。
3.2.8 Memory Management
虚拟内存允许使用辅助存储器扩展主存,但内核努力将最活跃的数据保持在主存中。为此,内核有两种例行程序:
1. 交换(Swapping)将整个进程在主存和辅助存储器之间移动。
2. 分页(Paging)将称为页面(例如,4 KB)的小内存单位移动。
交换是原始 Unix 方法,可能会导致严重的性能损失。分页更为高效,在引入页面式虚拟内存的 BSD 中被添加。在这两种情况下,最近最少使用(或最近未使用)的内存被移动到辅助存储器中,并且只有在需要时才会被移回到主存中。
在 Linux 中,术语 swapping 用于指代分页。Linux 内核不支持(较旧的)Unix 风格的整个线程和进程交换。
关于分页和交换的更多内容,请参阅第7章"内存"。
3.2.9 Schedulers
Unix 及其衍生系统是时间分享系统,通过将执行时间分配给多个进程,使它们能够同时运行。进程在处理器和各个 CPU 上的调度由调度程序完成,调度程序是操作系统内核的关键组件。调度程序的作用如图 3.10 所示,它对线程(在 Linux 中称为任务)进行操作,并将它们映射到 CPU 上。
基本意图是将 CPU 时间分配给活动进程和线程,并保持一定的优先级概念,以便更重要的工作能够更早地执行。调度程序跟踪所有处于就绪状态的线程,传统上是在每个优先级队列上进行,称为运行队列[Bach 86]。现代内核可能会针对每个 CPU 实现这些队列,并且除了队列之外,还可以使用其他数据结构来跟踪线程。当有更多的线程要运行而可用的 CPU 不足时,较低优先级的线程会等待它们的轮到来。大多数内核线程的优先级高于用户级进程。
调度程序可以动态修改进程优先级,以改善特定工作负载的性能。工作负载可以分为以下两类:
1. CPU 密集型:执行大量计算的应用程序,例如科学和数学分析,预计具有长运行时间(几秒、几分钟、几小时)。这些工作负载受 CPU 资源限制。
2. I/O 密集型:执行 I/O 操作很多但计算量很少的应用程序,例如 Web 服务器、文件服务器和交互式 shell,需要低延迟响应。当它们的负载增加时,它们受 I/O 存储或网络资源的限制。
调度程序可以识别出 CPU 密集型工作负载,并降低它们的优先级,从而让 I/O 密集型工作负载(其中低延迟响应更可取)更早地运行。这可以通过计算最近计算时间(在 CPU 上执行的时间)与实际时间(经过的时间)的比率,并降低具有较高(计算)比率的进程的优先级来实现[Thompson 78]。这种机制优先处理运行时间较短的进程,通常是执行 I/O 操作的进程,包括人机交互进程。
现代内核支持多个调度类别,它们应用不同的算法来管理优先级和可运行线程。其中可能包括实时调度类别,该类别使用比所有非关键工作(包括内核线程)更高的优先级。实时调度类别以及抢占支持(稍后将介绍)为实时系统提供了低延迟调度。有关内核调度程序和其他调度类别的更多信息,请参阅第6章“CPU”。
3.2.10 File Systems
文件系统是将数据组织为文件和目录的一种方式。它们具有基于文件的接口,通常基于 POSIX 标准。内核可以支持多种文件系统类型和实例。提供文件系统是操作系统最重要的角色之一,曾被描述为最重要的角色之一[Ritchie 74]。
操作系统提供了一个全局文件命名空间,以自顶向下的树状拓扑结构组织,从根级别(“/”)开始。文件系统通过挂载将其加入到树中,将自己的树连接到一个目录(挂载点)。这使得最终用户可以透明地浏览文件命名空间,而不受底层文件系统类型的限制。
一个典型的操作系统可能按照图3.11所示的方式进行组织。
顶级目录包括etc(用于系统配置文件),usr(用于系统提供的用户级程序和库),dev(用于设备文件),var(用于包含系统日志等可变文件),tmp(用于临时文件)和home(用于用户主目录)。在图示的示例中,var和home可能位于它们自己的文件系统实例和单独的存储设备上;但是,它们可以像树的任何其他组件一样进行访问。
大多数文件系统类型使用存储设备(磁盘)来存储其内容。一些文件系统类型由内核动态创建,例如/proc或/dev。
VFS
虚拟文件系统(VFS)是一个用于抽象文件系统类型的内核接口,最初由Sun Microsystems开发,以便Unix文件系统(UFS)和NFS能够更容易地共存。其角色如图3.12所示。
VFS接口使得向内核添加新的文件系统类型更加容易。它还支持提供之前所述的全局文件命名空间,以便用户程序和应用程序可以透明地访问各种文件系统类型。
I/O Stack
对于基于存储设备的文件系统,从用户级软件到存储设备的路径被称为I/O堆栈。这是之前展示的整个软件堆栈的一个子集。图3.13显示了一个通用的I/O堆栈。
文件系统及其性能在第8章《文件系统》中有详细介绍,而构建在其上的存储设备在第9章《磁盘》中进行了讨论。
3.2.11 Caching
由于磁盘I/O的延迟通常较高,软件堆栈的许多层次尝试通过缓存读取和缓冲写入来避免它。缓存可能包括表3.2中所示的缓存(按照检查的顺序)。
例如,缓冲区缓存是主内存中存储最近使用的磁盘块的区域。如果请求的块存在于缓存中,磁盘读取可以立即从缓存中提供,避免了磁盘I/O的高延迟。(11)
存在的缓存类型会根据系统和环境而异。
3.2.12 Networking
现代内核提供了一系列内置网络协议,使得系统能够在网络上进行通信并参与分布式系统环境。该堆栈被称为TCP/IP堆栈,以常用的TCP和IP协议命名。用户级应用程序通过可编程的端点(套接字)访问网络。
连接到网络的物理设备是网络接口,通常提供在网络接口卡(NIC)上。系统管理员的常见职责之一是将IP地址与网络接口关联起来,以便它可以与网络通信。
网络协议不经常更改,但增强和选项有所变化,例如更新的TCP选项和TCP拥塞控制算法,需要内核支持。另一个变化是支持不同的网络接口卡,这需要内核的新设备驱动程序。
有关网络和网络性能的更多信息,请参见第10章《网络》。
3.2.13 Device Drivers
内核必须与各种物理设备进行通信。使用设备驱动程序实现这种通信:内核软件用于设备管理和I/O。设备驱动程序通常由开发硬件设备的厂商提供。一些内核支持可插拔的设备驱动程序,可以在不需要系统重启的情况下加载和卸载。
设备驱动程序可以为其设备提供字符和/或块接口。字符设备,也称为原始设备,根据设备的不同,提供任何I/O大小的非缓冲顺序访问甚至单个字符的访问。这样的设备包括键盘和串行端口(在原始Unix中,还有纸带和线打印机设备)。
块设备以块为单位执行I/O操作,每个块通常为512字节。可以基于它们的块偏移随机访问这些块,从块设备的开始处开始,偏移量为0。在原始Unix中,块设备接口还提供了块设备缓冲区的缓存,以提高性能,在主内存的缓冲区缓存区域中完成。
/*sensor驱动程序是字符设备还是块设备?
传感器驱动程序通常被视为字符设备。这是因为传感器通常以流式数据的形式输出读数,而不是以固定大小的块进行访问。因此,传感器驱动程序提供的接口是一个字符接口,允许逐个字符或按流读取传感器数据。
*/
3.2.14 Multiprocessor
多处理器支持允许操作系统使用多个CPU实例并行执行工作。通常以对称多处理(SMP)的形式实现,其中所有CPU被平等对待。这在技术上是很难实现的,会导致在并行运行的线程之间访问和共享内存和CPU时出现问题。有关详细信息,请参见第6章“CPU”中的调度和线程同步,以及第7章“内存”中的内存访问和架构。
CPU跨调用
对于多处理器系统,CPU有时需要协调,例如用于内存转换项的缓存一致性(通知其他CPU,如果缓存了一个条目,则现在已经过时)。CPU可以请求其他CPU或所有CPU立即执行此类工作,使用CPU跨调用。跨调用是设计成快速执行的处理器中断,以最小化对其他线程的中断。
跨调用也可以用于抢占。
3.2.15 Preemption
内核抢占支持允许高优先级的用户级线程中断内核并执行。这使得实时系统(具有严格的响应时间要求的系统)成为可能。支持抢占的内核被称为完全可抢占的,尽管实际上它仍然会有一些无法被中断的小型关键代码路径。
Linux支持的一种方法是自愿内核抢占,在内核代码的逻辑停止点可以检查并执行抢占。这避免了支持完全可抢占内核的一些复杂性,并为常见工作负载提供了低延迟的抢占。
3.2.16 Resource Management
操作系统可以提供各种可配置的控制,以微调对系统资源(如CPU、内存、磁盘和网络)的访问。这些是资源控制,可以用于运行不同应用程序或租户(云计算)的系统来管理性能。这些控制可以对每个进程(或进程组)的资源使用设置固定限制,或者采用更灵活的方法,允许它们之间共享闲置使用。
早期的Unix和BSD版本具有基本的针对每个进程的资源控制,包括使用nice(1)进行CPU优先级设置,以及使用ulimit(1)进行一些资源限制。基于Solaris的系统自Solaris 9(2002年)起提供了高级资源控制,并在resources_controls(5)手册页中有相关文档。
对于Linux,已经开发并集成了控制组(cgroups)功能,从2.6.24版本(2008年)开始,并添加了各种附加控制功能。这些功能在内核源码的Documentation/cgroups目录下有详细说明。
适当的章节中会提到特定的资源控制。第11章“云计算”中描述了一个示例用例,用于管理基于操作系统的租户的性能。
3.2.17 Observability
操作系统由内核、库和程序组成。这些程序包括用于观察系统活动和分析性能的工具,通常安装在/usr/bin和/usr/sbin目录下。还可以在系统上安装第三方工具来提供额外的可观察性。
下一章介绍了可观察性工具以及构建它们所依赖的操作系统组件。
3.3 Kernels
本节介绍了基于Solaris和Linux内核(按时间顺序),它们的历史和特点,并重点讨论了它们在性能方面的差异。Unix的起源也作为背景进行了讨论。
现代内核之间的一些明显差异包括它们支持的文件系统(参见第8章“文件系统”)和它们提供的可观察性框架(参见第4章“可观察性工具”)。还存在着它们的系统调用(syscall)接口、网络堆栈架构、实时支持以及CPU、磁盘和网络I/O调度方面的差异。
表3.3显示了最近的内核版本,其中系统调用计数是基于操作系统手册第2节中的条目数。这是一个粗略的比较,但足以看出一些差异。
这些只是有文档记录的系统调用,内核通常还提供了更多供操作系统软件私下使用的系统调用。除了内核之间的差异,随着时间的推移,存在一种模式:Linux一直在增加系统调用,而Solaris则一直在删除系统调用。
UNIX最初只有20个系统调用,而今天直接来自UNIX的Linux已经有超过一千个……我只是担心随之增长的复杂性和规模。
肯·汤普森(Ken Thompson),ACM图灵百年庆典,2012年
这两个内核实际上都在不同的方式中变得越来越复杂,并通过添加新的系统调用或通过其他内核接口将此暴露给用户空间。
3.3.1 Unix
Unix是由肯·汤普森(Ken Thompson)、丹尼斯·里奇(Dennis Ritchie)和贝尔实验室(AT&T Bell Labs)的其他人在1969年及其后几年开发的。它的确切起源在《UNIX分时系统》[Ritchie 74]中有所描述:
第一个版本是由我们其中之一(汤普森)在对现有计算机设备不满意的情况下,发现了一个很少使用的PDP-7,然后着手创建一个更友好的环境。
UNIX的开发者之前曾在多路信息和计算机服务(Multics)操作系统上工作过。UNIX最初被开发为一种轻量级多任务操作系统和内核,最初被命名为UNiplexed Information and Computing Service(UNICS),以对Multics进行双关语的方式命名。从《UNIX实施》[Thompson 78]中可以看出:
内核是唯一不能由用户自己替换的UNIX代码。因此,内核应该尽可能少地做出真正的决策。这并不意味着允许用户通过无数选项来完成相同的事情。相反,它意味着只允许一种方式来完成一件事,但这种方式是所有可能提供的选项的最小公倍数。
尽管内核很小,但它确实提供了一些高性能的功能。进程具有调度器优先级,以降低高优先级工作的运行队列延迟。为了效率,磁盘I/O是按照大块(512字节)进行的,并且在内存中的每个设备缓冲区缓存中进行了缓存。空闲进程可以被交换到存储器中,使得更繁忙的进程可以在主存中运行。当然,该系统是多任务的,允许多个进程同时运行,提高作业吞吐量。
为了支持网络、多个文件系统、分页和其他我们现在认为是标准的功能,内核不得不增长。而且随着包括BSD、SunOS(Solaris)和后来的Linux在内的多个衍生版本的出现,内核性能变得具有竞争力,这推动了更多功能和代码的添加。
3.3.2 Solaris-Based
Solaris内核不仅是Unix衍生的,甚至还保留了一些原始Unix内核的代码。Solaris始于1982年由Sun Microsystems创建的SunOS。基于BSD,SunOS被保持得小巧紧凑,以便在Sun工作站上表现出色。到了1980年代末,Sun已经开发了新的操作系统功能,并与BSD和Xenix的功能一起贡献给了AT&T的Unix System V Release 4(SVR4)。随着SVR4成为新的Unix标准,Sun基于它创建了一个新的内核和操作系统:SunOS 5。Sun市场营销将其称为Solaris 2.0,并将先前的SunOS命名为Solaris 1.0。然而,工程师们在内核中保留了SunOS的名称。
Sun内核的发展,特别是与性能相关的,包括以下内容:
NFS:NFS协议允许文件在网络上共享,并作为全局文件系统树的一部分透明地使用(挂载)。NFS目前广泛使用的是3和4版本,每个版本都引入了许多性能改进。
VFS:虚拟文件系统(VFS)是一个抽象和接口,允许多个文件系统轻松共存。Sun最初创建它是为了使NFS和UFS可以共存。有关VFS的详细信息,请参见第8章,文件系统。
页面缓存:这个缓存虚拟内存页面,并且自从引入以来一直是大多数操作系统的主要文件系统缓存(ZFS ARC是一个例外)。它在SunOS 4中引入,同时也支持共享页面。有关页面缓存的更多信息,请参见第8章,文件系统。
内存映射文件:可用于减少文件I/O的开销,并在SVR4的SunOS虚拟内存重写中引入。
RPC:远程过程调用接口。
NIS:网络信息服务是一个简单的扁平拓扑结构,用于在网络上共享信息,包括passwd和hosts文件。它多年来被广泛使用,但现在正在被LDAP取代。
CacheFS:缓存文件系统在Solaris 2.4(1994年)引入,用于提高访问慢速NFS服务器的性能。此后,NFS服务器的性能已经提高到CacheFS不再常用或被考虑的程度。
完全可抢占内核:Sun最早的一个特点是其完全可抢占内核,确保高优先级工作(包括实时工作)的低延迟。
调度器类:提供多个调度器类来调整不同类工作负载的性能。这些包括时间共享(TS)、交互式(IA)、实时(RT)、系统(SYS)、固定(FX)和公平分享调度器(FSS)。有关详细信息,请参见第6章,CPU。
/*
上面的内存映射文件是指mmap么?
是的,上面提到的内存映射文件就是指mmap。mmap是一种将文件或其他对象映射到内存中的方法,使得应用程序可以像访问内存一样访问这些对象,从而避免了频繁的文件I/O操作带来的开销。在SVR4的SunOS虚拟内存重写中,引入了内存映射文件的概念。
*/
多处理器支持:在上世纪90年代初,Sun大力投资于多处理器操作系统支持,开发了对称和非对称多处理器(ASMP和SMP)的内核支持[Mauro 01]。
Slab分配器:取代了SVR4的伙伴分配器,内核slab分配器通过预先分配缓冲区的每个CPU缓存,提供更好的性能,这些缓冲区可以快速重用。这种分配器类型及其衍生物已成为操作系统的标准。
崩溃分析:Sun开发了一个成熟的内核崩溃转储分析框架,适用于所有系统并默认启用,包括用于崩溃转储、内核和应用程序分析的模块化调试器(mdb(1))。
M:N线程调度:这实现了一个在线程和进程之间增加一个额外的对象,目的是进行高效的线程调度。这个对象被称为轻量级进程(LWP),它可以有自己的用户级调度行为,与内核调度器不同。Sun的实现后来被发现存在问题,并且不值得复杂性[Cantrill 96]。它在Solaris 9中被删除,但术语(LWP)和一些数据结构在Solaris的某些部分中仍然存在。
STREAMS网络堆栈:Sun在AT&T STREAMS接口上构建了其TCP/IP网络堆栈,该接口提供了用户空间和内核空间之间的通信。它最终无法适应更快的网络,到Solaris 10时,许多STREAMS管道已被删除。
64位支持:Solaris 7内核(1998年)提供了对64位处理器的支持。
锁统计:在Solaris 7中引入了锁性能统计。
MPSS:多页大小支持允许操作系统使用由处理器提供的不同大小的内存页面,包括大(或巨大)页面,提高了内存操作的效率。
MPO:在Solaris 9中添加了内存放置优化,以改善与处理器体系结构(局部性)相关的内存分配方式,这可以显着提高内存访问性能。
资源控制:一种限制进程或进程组使用各种资源的设施,称为项目(稍后由区域使用)。
FireEngine:用于Solaris 10的一组高性能TCP/IP堆栈增强功能,包括垂直边界,以改善数据包处理的CPU和内存局部性,以及IP扇出以在CPU之间分散负载。
DTrace:一个静态和动态跟踪框架和工具,实时在生产环境中提供对整个软件堆栈的几乎无限的可观察性。它于2005年发布于Solaris 10,并成为首个广泛成功的动态跟踪实现。它已被移植到其他操作系统,包括Mac OS X和FreeBSD,并目前正在移植到Linux。DTrace在第4章“可观察性工具”中有详细介绍。
Zones:一种基于操作系统的虚拟化技术,允许创建共享同一主机内核的操作系统实例。它于Solaris 10发布,但这个概念最早由FreeBSD的jails在1998年实现。与其他虚拟化技术相比,它们具有轻量级和高性能的特点。详见第11章“云计算”。
Crossbow:一种提供高性能虚拟化网络接口和网络带宽资源控制的架构。这个特性对于构建高性能和可靠的云至关重要。
ZFS:ZFS文件系统提供了企业级功能,并随Solaris 10更新1一起发布,同时也作为开源软件提供。现在它已经适用于其他操作系统,并成为许多文件服务器设备的基础。详见第8章“文件系统”。
其中许多功能已经被移植或重新实现为Linux,并且有些功能仍在开发中。
在面对Linux的压力下,Sun在2005年将Solaris开源为OpenSolaris项目。它一直保持开源状态,直到Oracle于2010年收购Sun并停止发布源代码更新。最后发布的OpenSolaris版本,它镜像了Solaris 11的开发版本,成为开源的illumos内核的基础。今天有几种基于illumos内核的操作系统,包括Joyent的SmartOS,它被用于本书中许多基于Solaris的示例中。
3.3.3 Linux-Based
Linux是由Linus Torvalds于1991年创建的免费操作系统,用于Intel个人计算机。他在Usenet上发布了这个项目的消息:
我正在开发一个(免费的)操作系统(只是一种爱好,不会像GNU那样大而专业),用于386(486)AT克隆机。这个项目从四月份开始酝酿,并且正在逐渐准备就绪。我希望得到关于人们在MINIX中喜欢/不喜欢的事物的任何反馈,因为我的操作系统在某种程度上类似于它(包括文件系统的物理布局相同(出于实际原因)等等)。
这里提到的是MINIX操作系统,它当时正在开发,作为面向小型计算机的免费和精简版Unix。BSD也致力于提供一个免费的Unix版本,尽管当时遇到了法律问题。
Linux内核的开发借鉴了许多前辈的一般性思想,包括:
Unix(和Multics):操作系统层,系统调用,多任务处理,进程,进程优先级,虚拟内存,全局文件系统,文件系统权限,设备文件,缓存
BSD:分页虚拟内存,按需分页,快速文件系统(FFS),TCP/IP网络堆栈,套接字
Solaris:VFS、NFS、页面缓存、统一页面缓存、slab分配器,以及(正在进行中的)ZFS和DTrace
Plan 9:资源叉(rfork),用于创建进程和线程(任务)之间不同级别的共享。
/*
内存管理buddy和slab分配器的关系是?
Buddy分配器和Slab分配器是Linux内核中用于内存管理的两种不同机制。
Buddy分配器是一种用于管理大块连续内存的算法。它将物理内存按照2的幂次方进行划分,并通过合并相邻的空闲块来满足不同大小的内存分配请求。Buddy分配器主要用于处理大于一个页面的内存分配,例如进程堆栈或内核数据结构。
Slab分配器是一种用于管理小块连续内存(称为slabs)的机制。它通过将内存划分为固定大小的slabs来提高内存分配和释放的效率。每个slab被预先分配并用作特定类型的对象的缓存,以减少频繁的内存分配和释放操作。Slab分配器主要用于管理内核数据结构和缓存,例如文件描述符、进程控制块等。
因此,Buddy分配器和Slab分配器是Linux内核中两个不同的内存管理机制,用于处理不同大小和类型的内存分配需求。
*/
Linux内核的特性,特别是与性能相关的特性包括以下内容。其中许多特性包括了它们首次引入的Linux内核版本。
CPU调度类:开发了各种高级CPU调度算法,包括调度域(2.6.7)以更好地决策非均匀存储器访问(NUMA)。参见第6章,CPUs。
I/O调度类:开发了不同的块I/O调度算法,包括deadline(2.5.39)、anticipatory(2.5.75)和完全公平队列(CFQ)(2.6.6)。参见第9章,磁盘。
TCP拥塞控制:Linux内核支持新的TCP拥塞控制算法,允许根据需要选择。还有许多TCP增强功能。参见第10章,网络。
超额提交:与内存不足杀手(OOM)一起,这是一种利用更少主内存进行更多操作的策略。参见第7章,内存。
Futex(2.5.7):快速用户空间互斥量,用于提供高性能的用户级同步原语。
巨大页面(2.5.36):这提供了对由内核和内存管理单元(MMU)预分配的大内存页面的支持。参见第7章,内存。
OProfile(2.5.43):一个系统性能分析器,用于研究CPU使用率和其他事件,适用于内核和应用程序。
RCU(2.5.43):内核提供了一种读-复制-更新同步机制,允许多个读与更新并发进行,从而提高数据的性能和可扩展性。
epoll (2.5.46):一个系统调用,用于有效地等待许多打开文件描述符的I/O操作,提高服务器应用程序的性能。
模块化I/O调度(2.6.10):Linux提供了可插拔的调度算法来调度块设备I/O。参见第9章,磁盘。
DebugFS(2.6.11):内核向用户级别公开数据的简单非结构化接口,被一些性能工具使用。
Cpusets(2.6.12):进程的独占CPU分组。
自愿内核抢占(2.6.13):此过程提供低延迟调度而不需要完全抢占的复杂性。
inotify(2.6.13):用于监视文件系统事件的框架。
blktrace(2.6.17):用于跟踪块I/O事件的框架和工具(后来迁移到tracepoints)。
splice(2.6.17):一种系统调用,用于在文件描述符和管道之间快速移动数据,无需通过用户空间。
延迟账户(2.6.18):跟踪每个任务的延迟状态。参见第4章,可观测性工具。
IO账户(2.6.20):测量各种存储I/O统计信息的进程。
DynTicks(2.6.21):动态滴答允许内核定时器中断(时钟)仅在必要时触发(无滴答),节省CPU资源和电源。
SLUB(2.6.22):一个新的、简化的slab内存分配器版本。
CFS(2.6.23):完全公平调度程序。参见第6章,CPUs。
cgroups(2.6.24):控制组允许对进程组的资源使用进行测量和限制。
latencytop(2.6.25):用于观察操作系统延迟来源的仪器和工具。
Tracepoints(2.6.28):静态内核跟踪点(也称为静态探针),用于仪器化内核中的逻辑执行点,供跟踪工具使用(以前是内核标记)。跟踪工具在第4章可观测性工具中介绍。
perf(2.6.31):Linux性能事件(perf)是一组用于性能可观测性的工具,包括CPU性能计数器分析和静态和动态跟踪。参见第6章,CPUs,进行介绍。
透明大页面(2.6.38):这是一个框架,允许轻松使用巨大(大)内存页面。参见第7章,内存。
Uprobes(3.5):用于动态跟踪用户级软件的基础设施,被其他工具(perf、SystemTap等)使用。
KVM:基于内核的虚拟机(KVM)技术是由Qumranet开发的,后来在2008年被Red Hat收购。KVM允许创建虚拟操作系统实例并运行它们自己的内核。请参见第11章,云计算。
其中一些功能,包括epoll和KVM,已经被移植或重新实现为基于Solaris的系统。
Linux还通过其对设备驱动程序的广泛支持和开源要求,间接地为许多其他操作系统做出了贡献。
3.3.4 Differences
虽然Linux和基于Solaris的内核都是Unix的后代,共享相同的操作系统概念,但它们在许多方面都有不同,无论是大还是小。没有简洁明了的方法来总结这种复杂性。
Linux系统的主要优势主要不来自内核或操作系统本身,而来自应用程序包支持、设备驱动程序支持、庞大的社区以及其开源性。大多数基于Solaris的内核也是开源的(Oracle Solaris目前不是),但它们没有相同广泛的驱动程序支持(这对于笔记本电脑使用可能是一个问题)。
基于Solaris的系统提供了面向企业级的ZFS文件系统和几乎无限的可观测性DTrace。尽管它们正在被移植到Linux上,但它们已经在基于Solaris的系统上可用并成熟,自2003年以来已在生产环境中使用。Linux确实具有许多较新的会计和跟踪框架,提供了扩展的可观测性(在下一章中介绍),但它们可能尚未普遍启用或默认安装。
基于Solaris的系统还默认启用了内核崩溃转储,以便可以从第一次发生时对内核崩溃进行分析和解决。
除了这些主要差异之外,内核之间还有许多许多微小的差异,特别是在性能优化方面。要了解这些差异如何影响您,需要分析所需工作负载,以确定哪些是相关的。
举个微小差异的例子,POSIX的fadvise()调用目前在Linux上实现,但在基于Solaris的内核上被忽略。应用程序可以使用此调用通知内核不缓存与文件描述符相关联的数据,从而允许Linux内核更高效地进行缓存,提高性能。以下是来自MySQL数据库的一个示例用法:
storage/innobase/row/row0merge.c:
/* Each block is read exactly once. Free up the file cache. */
posix_fadvise(fd, ofs, sizeof *buf, POSIX_FADV_DONTNEED);
这样的微小差异可能会迅速改变,而当您阅读本书时,这个特定问题可能已经在基于Solaris的内核中得到解决。
虽然根据工作负载的不同,交付性能存在微小差异,但最大的差异可能是性能可观测性,特别是对动态跟踪的支持。如果一个内核支持您在生产环境中找到10倍以上的优势,那么早期发现的任何10%左右的差异都可能不那么重要。
观测工具将在下一章中介绍。