比较 Solaris、Linux 和 FreeBSD 内核
作者 Max Bruning
2005 年 10 月 14 日
我大部分时间都在教授 Solaris 内部实现、设备驱动程序以及内核崩溃转储分析和调试方面的课程。向学生解释如何在 Solaris 中实现各个子系统时,他们常常会问“在 Linux 中是怎样实现的呢?”或“在 FreeBSD 中是这样实现的,但在 Solaris中呢?”。本文对内核的三个基本子系统进行了探讨,并对它们在 Solaris 10、Linux 2.6 和 FreeBSD 5.3 中的实现进行了比较。
本文探讨的三个子系统是调度、内存管理和文件系统体系结构。我之所以选择这些子系统,是因为它们是所有操作系统(不仅是 Unix 和类似于 Unix 的系统)所共有的,而且可能是操作系统中最容易理解的组件。
本文不会对这些子系统进行深入的研究。要进行深入研究,请参考源代码、各种 Web 站点和有关这方面主题的书籍。以下是相关书籍:
如果您在 Web 中搜索 Linux、FreeBSD 和 Solaris 的比较,大多数匹配的网页讨论的都是这些操作系统的旧版本(有时是 Solaris 2.5、Linux 2.2 等)。许多“事实”对于最新版本来说都是错误的,有些“事实”对于它们要介绍的版本来说也是不正确的。当然,大多数网页对于这些常常为大家所讨论的操作系统的优点还是做出了一些有价值的判断,但是对这些内核本身进行比较的信息却很少。以下站点中的信息相对而言可能是最新的:
这三个操作系统颇有意思的一点是它们具有很多的相似点。除了命名约定不同,这些操作系统在实现各种概念时都采用了非常相似的方法。每个操作系统都支持线程的分时调度、支持通过“最近未使用页面替换算法”执行请求调页、支持可以实现不同文件系统体系结构的虚拟文件系统层。源于一个操作系统的概念经常被应用到其他操作系统中。例如,Linux 也采用了 Solaris 的 Slab 内存分配器概念。FreeBSD 源代码中的许多术语也出现在 Solaris 中。随着 Sun 开放 Solaris 源代码活动的开展,我希望能够看到各种操作系统更多地相互取长补短。目前,LXR 计划针对 FreeBSD、Linux 和其他与 Unix 相关的操作系统提供了一个源代码相互参照浏览器,您可以从 fxr.watson.org 站点访问该浏览器。如果在该站点也能看到 OpenSolaris 源代码,那将是一件令人高兴的事情。
调度和调度程序
Solaris、FreeBSD 和 Linux 中的基本调度单元分别是 kthread_t、thread 和 task_struct。Solaris 将每个进程表示为一个 proc_t,而进程中的每个线程都有一个 kthread_t。Linux 用 task_struct 结构来表示进程(和线程)。Linux 中的单线程进程有一个 task_struct。Solaris 中的单线程进程有一个 proc_t、一个 kthread_t 和一个 klwp_t。klwp_t 为在用户和内核模式之间切换的线程提供一个存储区域。FreeBSD 中的单线程进程有一个 proc 结构、一个 thread 结构和一个 ksegrp 结构。ksegrp 是“内核调度实体组”。实际上,这三个操作系统都支持线程调度,其中线程在 Solaris 中为 kthread_t,在 FreeBSD 中为 thread 结构,在 Linux 中为 task_struct。
调度决策基于优先级。在 Linux 和 FreeBSD 中,优先级值越小,代表的优先级越高。这是一种倒置;越接近 0 的值代表的优先级越高。在 Solaris 中,值越大,代表的优先级越高。表 1 说明了不同操作系统的优先级值。
表 1. Solaris、Linux 和 FreeBSD 中的调度优先级
| Solaris |
| 优先级 |
调度类 |
| 0-59 |
分时、交互、固定、公平份额调度器 |
| 60-99 |
系统类 |
| 100-159 |
实时(请注意实时调度类的优先级高于系统线程) |
| 160-169 |
低级别中断 |
| Linux |
优先级 |
调度类 |
| 0-99 |
系统线程、实时(SCHED_FIFO、SCHED_RR) |
| 100-139 |
用户优先级 (SCHED_NORMAL) |
| FreeBSD |
优先级 |
调度类 |
| 0-63 |
中断 |
| 64-127 |
上半部分内核 |
| 128-159 |
实时用户(系统线程具有更高的优先级) |
| 160-223 |
分时用户 |
| 224-255 |
空闲用户 |
这三个操作系统都优先考虑交互式线程/进程。交互式线程的优先级高于与计算机绑定的线程,但是交互式线程获得的时间片较短。Solaris、FreeBSD 和 Linux 都使用每 CPU“运行队列”。FreeBSD 和 Linux 使用“活动”队列和“过期”队列。系统从活动队列中按优先级调度线程。如果某个线程用完其时间片(也可能是为了避免资源匮乏),则它会从活动队列移到过期队列。如果活动队列为空,内核将交换活动队列和过期队列。FreeBSD 还有一个用来容纳“空闲”线程的队列。仅当其他两个队列为空时,才会调度该队列中的线程。Solaris 使用每 CPU“分发队列”。如果线程用完其时间片,内核会为其分配一个新优先级,然后将其放回分发队列。这三个操作系统的“运行队列”对于不同优先级的可运行线程都有单独的链接表。(虽然在 FreeBSD 中每四种优先级使用一个列表,但在 Solaris 和 Linux 中都是每种优先级使用一个单独的列表。)
Linux 和 FreeBSD 基于运行时间和休眠时间的比(“交互性”度量方法)使用算术运算计算线程的优先级。Solaris 执行表查找。这三个操作系统都不支持“组调度”。实际上,每个操作系统都调度下一个线程而不是调度 n 个线程来运行。这三个操作系统都有利用高速缓存(热关联)和负载平衡的机制。对于超线程 CPU,FreeBSD 具有将多个线程保持在同一 CPU 节点(尽管可能是不同的超线程)上的机制。Solaris 也有类似的机制,不过是在用户和应用程序的控制下,而且并不限于超线程(在 Solaris 中称为“处理器集”,在 FreeBSD 中称为“处理器组”)。
与其他两个操作系统最大的不同点在于,Solaris 可以同时在系统中支持多个“调度类”。这三个操作系统都支持 Posix SCHED_FIFO、SCHED_RR 和 SCHED_OTHER(或 SCHED_NORMAL)。SCHED_FIFO 和 SCHED_RR 通常导致“实时”线程。(请注意,Solaris 和 Linux 为了支持实时线程都支持内核抢占。)Solaris 支持“固定优先级”类、系统线程(如页出线程)的“系统类”、用于在 X 服务器控制下在窗口环境中运行的线程的“交互式”类,以及用于支持资源管理的公平份额调度器。有关如何使用这些类的信息以及各种类的功能概述,请参见 priocntl(1)。有关特定于公平份额调度器的概述,请参见 FSS(7)。FreeBSD 的调度程序是在编译时选择的,而 Linux 的调度程序取决于 Linux 的版本。
支持将新调度类添加到系统中是要付出代价的。内核中每个可以制订调度决策(选择要运行的线程的实际操作除外)的地方都有一个用于调用特定于调度类代码的间接函数。例如,当线程将要休眠时,它将调用调度类相关代码,执行该类中线程休眠需要完成的工作。在 Linux 和 FreeBSD 中,调度代码即可执行所需操作,而不再需要一个间接调用。在 Solaris 中,额外的层意味着要花费更多一些的系统开销用于调度(不过提供了更多的功能)。
内存管理和分页
在 Solaris 中,每个进程都有一个由称为“段”的逻辑节分区组成的“地址空间”。可通过 pmap(1) 来查看进程地址空间的段。Solaris 将内存管理代码和数据结构分为平台无关和平台相关两个部分。内存管理的平台相关部分位于 HAT(即硬件地址转换)层。FreeBSD 用 vmspace 来描述其进程地址空间(划分为称为区域的逻辑节)。硬件相关部分位于 "pmap"(物理映射)模块中,而 "vmap" 例程处理硬件无关部分和数据结构。Linux 使用内存描述符将进程地址空间划分为称为“存储区”的逻辑节,使用逻辑节来描述进程地址空间。Linux 也使用 pmap 命令来检查进程地址空间。
Linux 将计算机相关层与软件中较高级别的计算机无关层分开。例如,在 Solaris 和 FreeBSD 中,用来处理页故障的代码大都与计算机无关。在 Linux 中,用来处理页故障的代码(从故障处理开始)绝大多数都与计算机相关。这样 Linux 便可以更快地处理许多分页代码,因为代码中的数据抽象(分层)较少。但是,代价是更改底层硬件或模型时需要对代码进行更多的更改。Solaris 和 FreeBSD 则分别把这些更改隔离到 HAT 层和 pmap 层。
段、区域和存储区由以下各项来限定:
- 区域开头的虚拟地址。
- 它们在由段/区域/存储区映射的对象/文件中的位置。
- 权限。
- 映射的大小。
例如,程序文本位于段/区域/存储区。这三个操作系统中用于管理地址空间的机制十分相似,但是数据结构的名称却完全不同。此外,与其他两个操作系统相比,有更多 Linux 代码依赖于计算机。
分页
这三个操作系统都使用最近最少使用算法的变体进行页偷取/替换。它们都有一个用于执行页替换的守护进程/线程。在 FreeBSD 中,会定期或当可用内存不多时唤醒 vm_pageout 守护进程。当可用内存小于某些阈值时,vm_pageout 将运行一个例程 (vm_pageout_scan) 来扫描内存,以尝试释放一些页。vm_pageout_scan 例程在释放已修改的页之前,可能需要先将这些页异步写入磁盘。无论有多少个 CPU,这样的守护进程只有一个。Solaris 有一个 pageout 守护进程,它也会定期运行并且会在可用内存较少时做出响应。在 Solaris 中,分页阈值是在系统启动时自动校准的,这样守护进程便不会由于页出请求而过度使用 CPU 或对磁盘进行泛洪攻击。大多数情况下,FreeBSD 守护进程使用硬编码值或可调值来确定分页阈值。Linux 也使用运行时动态调整的 LRU(Least Recently Used,最近最少使用)算法。在 Linux 中,可以有多个 kswapd 守护进程(每个 CPU 最多一个)。这三个操作系统都使用全局工作集策略(与每进程工作集相对)。
FreeBSD 有多个页列表用来跟踪最近使用的页。这些列表跟踪“活动”页、“非活动”页、“高速缓存”页和“空闲”页。根据使用情况,将在这些链接表间移动页。经常访问的页将在活动列表中。退出的进程的数据页会立即放入空闲列表。如果 vm_pageout_scan 无法跟上负载变化(例如,如果系统内存不足),FreeBSD 可能会将所有进程全部换出。如果内存严重不足,vm_pageout_scan 将中止系统中最大的进程。
Linux 也使用不同的页链接表以简化 LRU 样式的算法。Linux 将物理内存分为三个“区域 (zone)”(可能为多组):一个用于 DMA 页,一个用于正常页,一个用于动态分配的内存。这些区域很可能是由 x86 体系结构约束产生的实现详细信息。页在“热”列表、“冷”列表和“空闲”列表间移动。页在这些列表间移动的机制与 FreeBSD 中的非常相似。经常访问的页将在“热”列表中。空闲页将在“冷”列表或“空闲”列表中。
Solaris 使用空闲列表、散列表和 vnode 页列表来维护其 LRU 替换算法的变体。Solaris 使用“双指针时钟”算法(在《Solaris 内部实现》及其他地方进行了介绍)扫描所有页,而不是扫描 vnode 页列表或散列页列表(相当于 FreeBSD/Linux 实现中的“活动”/“热”列表)。这两个指针之间保持固定的距离。前面的指针通过清除页的引用位使页过期。如果前面的指针访问该页后没有进程引用该页,后面的指针将释放该页(如果该页已被修改,则先将该页异步写入磁盘)。
这三个操作系统在分页过程中都会考虑 NUMA 本地性。在这三个操作系统中,I/O 高速缓存存储区和虚拟内存页高速缓存都合并到一个系统页高速缓存中。该系统页高速缓存用于读/写文件以及经过 mmap 处理的文件、文本和应用程序数据。
文件系统
这三个操作系统都使用数据抽象层来向应用程序隐藏文件系统实现的详细信息。在这三个操作系统中,无论文件数据的底层实现和组织如何,都可以使用 open、close、read、write、stat 等系统调用来访问文件。Solaris 和 FreeBSD 将该机制称为 VFS(virtual file system,虚拟文件系统),其基本数据结构为 vnode(即“虚拟节点”)。在 Solaris 或 FreeBSD 中,会为每个被访问的文件分配一个 vnode。除通用文件信息外,vnode 还包含指向文件系统特定信息的指针。Linux 也使用类似的机制,该机制也称为 VFS(即虚拟文件切换)。在 Linux 中,独立于文件系统的数据结构是 inode。该结构与 Solaris/FreeBSD 中的 vnode 类似。(请注意,在 Solaris/FreeBSD 中也有 inode 结构,但对于 UFS 文件系统,这是与文件系统相关的数据)。Linux 有两个不同的结构,一个用于文件操作,另一个用于 inode 操作。Solaris 和 FreeBSD 将这两种操作合并为“vnode 操作”。
VFS 允许在系统中实现多种文件系统类型。这意味着这些操作系统可以相互访问对方的文件系统。当然,这要求将相关文件系统例程和数据结构导入相关操作系统的 VFS。这三个操作系统都允许堆叠文件系统。表 2 列出了在各个操作系统中实现的文件系统类型,但未列出所有文件系统类型。
表 2. 部分文件系统类型列表
| Solaris |
ufs |
缺省本地文件系统(基于 BSD 快速文件系统) |
| nfs |
远程文件 |
| proc |
/proc 文件;请参见 proc(4) |
| namefs |
名称文件系统;允许将门/流以文件形式打开 |
| ctfs |
用于服务管理工具的合同文件系统 |
| tmpfs |
对临时文件使用匿名空间(内存/交换) |
| swapfs |
跟踪匿名空间(数据、堆、栈等) |
| objfs |
跟踪内核模块,请参见 objfs(7FS) |
| devfs |
跟踪 /devices 文件;请参见 devfs(7FS) |
| FreeBSD |
ufs |
缺省本地文件系统(ufs2,基于 BSD 快速文件系统) |
| defvs |
跟踪 /dev 文件 |
| ext2 |
Linux ext2 文件系统(基于 GNU) |
| nfs |
远程文件 |
| ntfs |
Windows NT 文件系统 |
| smbfs |
Samba 文件系统 |
| portalfs |
将进程挂载到目录中 |
| kernfs |
包含各种系统信息的文件 |
| Linux |
ext3 |
日志记录,源自 ext2 的基于扩展的文件系统 |
| ext2 |
基于扩展的文件系统 |
| afs |
AFS 客户机支持远程文件共享 |
| nfs |
远程文件 |
| coda |
另一个网络文件系统 |
| procfs |
进程、处理器、总线和平台的特定信息 |
| reiserfs |
日志记录文件系统 |
结论
Solaris、FreeBSD 和 Linux 显然彼此都取长补短。随着 Solaris 开放其源代码,这种相互促进有望更快。我个人感觉 Linux 的变化最快。其好处就是新技术能够快速地结合到该系统中。不过,文档(和健壮性)有时相对滞后。Linux 有(有时看上去有)很多开发者。FreeBSD 则大概(在某种意义上)是这三个系统中历史最长的一个。Solaris 来自 BSD Unix 和 AT&T Bell Labs Unix 的结合。Solaris 使用了更多数据抽象分层,因此通常能更容易地支持更多功能。但是,内核中大多数这样的分层都没有记录。随着源代码的开放这一点可能会有所改善。
突出说明这三个操作系统间差异的简单示例就是页故障处理。在 Solaris 中,发生页故障时,将开始执行特定于平台的陷阱处理程序中的代码,然后调用通用 as_fault() 例程。该例程确定发生故障的段,然后调用“段驱动程序”处理故障。段驱动程序调用文件系统代码。文件系统代码再调用设备驱动程序以换入所需页。页入完成后,段驱动程序调用 HAT 层以更新页表项(或其等效内容)。在 Linux 中,发生页故障时,内核会调用代码来处理故障。系统将立即执行特定于平台的代码。这意味着在 Linux 中可更快地执行故障处理代码,但是 Linux 代码可能不易扩展或导入。
内核可见性和调试工具对于正确理解系统行为非常重要。您可以阅读源代码,但是相信您很容易误解代码。使用工具验证您对代码工作原理的猜测非常重要。在这一点上,我认为 Solaris 毫无疑问是个赢家,它拥有 kmdb、mdb 和 Dtrace。我对 Solaris 进行了多年的“反向工程”。我发现,解决问题时使用工具常常比阅读源代码要快。至于 Linux,在这方面就不能提供如此多的选择。FreeBSD 允许使用 gdb 来调试内核崩溃转储。gdb 可以设置断点、单步,检查和修改数据以及代码。至于 Linux,下载并安装相应工具后也可以执行这些操作。
Max Bruning 目前教授 Solaris 内部实现、设备驱动程序、内核(和应用程序)崩溃分析和调试、网络内部实现以及各种专题,并提供咨询。请通过 max@bruningsystems.com 或 http://mbruning.blogspot.com/ 与他联系。