linux内核技术文章

Linux 引导过程内幕

从主引导记录到第一个用户空间应用程序的指导
引导 Linux® 系统的过程包括很多阶段。不管您是引导一个标准的 x86 桌面系统,还是引导一台嵌入式


的 PowerPC® 机器,很多流程都惊人地相似。本文将探索 Linux 的引导过程,从最初的引导到启动第一


个用户空间应用程序。在本文介绍的过程中,您将学习到各种与引导有关的主题,例如引导加载程序、


内核解压、初始 RAM 磁盘以及 Linux 引导的其他一些元素。


早期时,启动一台计算机意味着要给计算机喂一条包含引导程序的纸带,或者手工使用前端面板地址/数


据/控制开关来加载引导程序。尽管目前的计算机已经装备了很多工具来简化引导过程,但是这一切并没


有对整个过程进行必要的简化。
让我们先从高级的视角来查看 Linux 引导过程,这样就可以看到整个过程的全貌了。然后将回顾一下在


各个步骤到底发生了什么。在整个过程中,参考一下内核源代码可以帮助我们更好地了解内核源代码树


,并在以后对其进行深入分析。
概述
图 1 是我们在 20,000 英尺的高度看到的视图。
图 1. Linux 引导过程在 20,000 英尺处的视图
Linux 引导过程在 20,000 英尺处的视图
当系统首次引导时,或系统被重置时,处理器会执行一个位于已知位置处的代码。在个人计算机(PC)


中,这个位置在基本输入/输出系统(BIOS)中,它保存在主板上的闪存中。嵌入式系统中的中央处理单


元(CPU)会调用这个重置向量来启动一个位于闪存/ROM 中的已知地址处的程序。在这两种情况下,结


果都是相同的。因为 PC 提供了很多灵活性,BIOS 必须确定要使用哪个设备来引导系统。稍后我们将详


细介绍这个过程。
当找到一个引导设备之后,第一阶段的引导加载程序就被装入 RAM 并执行。这个引导加载程序在大小上


小于 512 字节(一个扇区),其作用是加载第二阶段的引导加载程序。
当第二阶段的引导加载程序被装入 RAM 并执行时,通常会显示一个动画屏幕,并将 Linux 和一个可选


的初始 RAM 磁盘(临时根文件系统)加载到内存中。在加载映像时,第二阶段的引导加载程序就会将控


制权交给内核映像,然后内核就可以进行解压和初始化了。在这个阶段中,第二阶段的引导加载程序会


检测系统硬件、枚举系统链接的硬件设备、挂载根设备,然后加载必要的内核模块。完成这些操作之后


启动第一个用户空间程序(init),并执行高级系统初始化工作。
这就是 Linux 引导的整个过程。现在让我们深入挖掘一下这个过程,并深入研究一下 Linux 引导过程


的一些详细信息。
回页首
系统启动
系统启动阶段依赖于引导 Linux 系统上的硬件。在嵌入式平台中,当系统加电或重置时,会使用一个启


动环境。这方面的例子包括 U-Boot、RedBoot 和 Lucent 的 MicroMonitor。嵌入式平台通常都是与引


导监视器搭配销售的。这些程序位于目标硬件上的闪存中的某一段特殊区域,它们提供了将 Linux 内核


映像下载到闪存并继续执行的方法。除了可以存储并引导 Linux 映像之外,这些引导监视器还执行一定


级别的系统测试和硬件初始化过程。在嵌入式平台中,这些引导监视器通常会涉及第一阶段和第二阶段


的引导加载程序。
提取 MBR 的信息
要查看 MBR 的内容,请使用下面的命令:
# dd if=/dev/hda of=mbr.bin bs=512 count=1 # od -xa mbr.bin
这个 dd 命令需要以 root 用户的身份运行,它从 /dev/hda(第一个 IDE 盘) 上读取前 512 个字节


的内容,并将其写入 mbr.bin 文件中。od 命令会以十六进制和 ASCII 码格式打印这个二进制文件的内


容。
在 PC 中,引导 Linux 是从 BIOS 中的地址 0xFFFF0 处开始的。BIOS 的第一个步骤是加电自检(POST


)。POST 的工作是对硬件进行检测。BIOS 的第二个步骤是进行本地设备的枚举和初始化。
给定 BIOS 功能的不同用法之后,BIOS 由两部分组成:POST 代码和运行时服务。当 POST 完成之后,


它被从内存中清理了出来,但是 BIOS 运行时服务依然保留在内存中,目标操作系统可以使用这些服务



要引导一个操作系统,BIOS 运行时会按照 CMOS 的设置定义的顺序来搜索处于活动状态并且可以引导的


设备。引导设备可以是软盘、CD-ROM、硬盘上的某个分区、网络上的某个设备,甚至是 USB 闪存。
通常,Linux 都是从硬盘上引导的,其中主引导记录(MBR)中包含主引导加载程序。MBR 是一个 512 


字节大小的扇区,位于磁盘上的第一个扇区中(0 道 0 柱面 1 扇区)。当 MBR 被加载到 RAM 中之后


,BIOS 就会将控制权交给 MBR。
回页首
第一阶段引导加载程序
MBR 中的主引导加载程序是一个 512 字节大小的映像,其中包含程序代码和一个小分区表(参见图 2)


。前 446 个字节是主引导加载程序,其中包含可执行代码和错误消息文本。接下来的 64 个字节是分区


表,其中包含 4 个分区的记录(每个记录的大小是 16 个字节)。MBR 以两个特殊数字的字节(0xAA55


)结束。这个数字会用来进行 MBR 的有效性检查。
图 2. MBR 剖析
MBR 剖析
主引导加载程序的工作是查找并加载次引导加载程序(第二阶段)。它是通过在分区表中查找一个活动


分区来实现这种功能的。当找到一个活动分区时,它会扫描分区表中的其他分区,以确保它们都不是活


动的。当这个过程验证完成之后,就将活动分区的引导记录从这个设备中读入 RAM 中并执行它。
回页首
第二阶段引导加载程序
次引导加载程序(第二阶段引导加载程序)可以更形象地称为内核加载程序。这个阶段的任务是加载 


Linux 内核和可选的初始 RAM 磁盘。
GRUB 阶段引导加载程序
/boot/grub 目录中包含了 stage1、stage1.5 和 stage2 引导加载程序,以及很多其他加载程序(例如


,CR-ROM 使用的是 iso9660_stage_1_5)。
在 x86 PC 环境中,第一阶段和第二阶段的引导加载程序一起称为 Linux Loader(LILO)或 GRand 


Unified Bootloader(GRUB)。由于 LILO 有一些缺点,而 GRUB 克服了这些缺点,因此下面让我们就


来看一下 GRUB。(有关 GRUB、LILO 和相关主题的更多内容,请参阅本文后面的 参考资料 部分的内容


。)
关于 GRUB,很好的一件事情是它包含了有关 Linux 文件系统的知识。GRUB 不像 LILO 一样使用裸扇区


,而是可以从 ext2 或 ext3 文件系统中加载 Linux 内核。它是通过将两阶段的引导加载程序转换成三


阶段的引导加载程序来实现这项功能的。阶段 1 (MBR)引导了一个阶段 1.5 的引导加载程序,它可以


理解包含 Linux 内核映像的特殊文件系统。这方面的例子包括 reiserfs_stage1_5(要从 Reiser 日志


文件系统上进行加载)或 e2fs_stage1_5(要从 ext2 或 ext3 文件系统上进行加载)。当阶段 1.5 的


引导加载程序被加载并运行时,阶段 2 的引导加载程序就可以进行加载了。
当阶段 2 加载之后,GRUB 就可以在请求时显示可用内核列表(在 /etc/grub.conf 中进行定义,同时


还有几个软符号链接 /etc/grub/menu.lst 和 /etc/grub.conf)。我们可以选择内核甚至修改附加内核


参数。另外,我们也可以使用一个命令行的 shell 对引导过程进行高级手工控制。
将第二阶段的引导加载程序加载到内存中之后,就可以对文件系统进行查询了,并将默认的内核映像和 


initrd 映像加载到内存中。当这些映像文件准备好之后,阶段 2 的引导加载程序就可以调用内核映像


了。


内核
GRUB 中的手工引导
在 GRUB 命令行中,我们可以使用 initrd 映像引导一个特定的内核,方法如下:
grub> kernel /bzImage-2.6.14.2
[Linux-bzImage, setup=0x1400, size=0x29672e]
grub> initrd /initrd-2.6.14.2.img
[Linux-initrd @ 0x5f13000, 0xcc199 bytes]
grub> boot
Uncompressing Linux... Ok, booting the kernel.
如果您不知道要引导的内核的名称,只需使用斜线(/)然后按下 Tab 键即可。GRUB 会显示内核和 


initrd 映像列表。
当内核映像被加载到内存中,并且阶段 2 的引导加载程序释放控制权之后,内核阶段就开始了。内核映


像并不是一个可执行的内核,而是一个压缩过的内核映像。通常它是一个 zImage(压缩映像,小于 


512KB)或一个 bzImage(较大的压缩映像,大于 512KB),它是提前使用 zlib 进行压缩过的。在这个


内核映像前面是一个例程,它实现少量硬件设置,并对内核映像中包含的内核进行解压,然后将其放入


高端内存中,如果有初始 RAM 磁盘映像,就会将它移动到内存中,并标明以后使用。然后该例程会调用


内核,并开始启动内核引导的过程。
当 bzImage(用于 i386 映像)被调用时,我们从 ./arch/i386/boot/head.S 的 start 汇编例程开始


执行(主要流程图请参看图 3)。这个例程会执行一些基本的硬件设置,并调用 


./arch/i386/boot/compressed/head.S 中的 startup_32 例程。此例程会设置一个基本的环境(堆栈等


),并清除 Block Started by Symbol(BSS)。然后调用一个叫做 decompress_kernel 的 C 函数(在 


./arch/i386/boot/compressed/misc.c 中)来解压内核。当内核被解压到内存中之后,就可以调用它了


。这是另外一个 startup_32 函数,但是这个函数在 ./arch/i386/kernel/head.S 中。
在这个新的 startup_32 函数(也称为清除程序或进程 0)中,会对页表进行初始化,并启用内存分页


功能。然后会为任何可选的浮点单元(FPU)检测 CPU 的类型,并将其存储起来供以后使用。然后调用 


start_kernel 函数(在 init/main.c 中),它会将您带入与体系结构无关的 Linux 内核部分。实际上


,这就是 Linux 内核的 main 函数。
图 3. Linux 内核 i386 引导的主要函数流程
Linux 内核 i386 引导的主要函数流程
通过调用 start_kernel,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 


RAM 磁盘。最后,要调用 kernel_thread(在 arch/i386/kernel/process.c 中)来启动 init 函数,


这是第一个用户空间进程(user-space process)。最后,启动空任务,现在调度器就可以接管控制权


了(在调用 cpu_idle 之后)。通过启用中断,抢占式的调度器就可以周期性地接管控制权,从而提供


多任务处理能力。
在内核引导过程中,初始 RAM 磁盘(initrd)是由阶段 2 引导加载程序加载到内存中的,它会被复制


到 RAM 中并挂载到系统上。这个 initrd 会作为 RAM 中的临时根文件系统使用,并允许内核在没有挂


载任何物理磁盘的情况下完整地实现引导。由于与外围设备进行交互所需要的模块可能是 initrd 的一


部分,因此内核可以非常小,但是仍然需要支持大量可能的硬件配置。在内核引导之后,就可以正式装


备根文件系统了(通过 pivot_root):此时会将 initrd 根文件系统卸载掉,并挂载真正的根文件系统



decompress_kernel 输出
函数 decompress_kernel 就是显示我们通常看到的解压消息的地方:
Uncompressing Linux... Ok, booting the kernel.
initrd 函数让我们可以创建一个小型的 Linux 内核,其中包括作为可加载模块编译的驱动程序。这些


可加载的模块为内核提供了访问磁盘和磁盘上的文件系统的方法,并为其他硬件提供了驱动程序。由于


根文件系统是磁盘上的一个文件系统,因此 initrd 函数会提供一种启动方法来获得对磁盘的访问,并


挂载真正的根文件系统。在一个没有硬盘的嵌入式环境中,initrd 可以是最终的根文件系统,或者也可


以通过网络文件系统(NFS)来挂载最终的根文件系统。


Init
当内核被引导并进行初始化之后,内核就可以启动自己的第一个用户空间应用程序了。这是第一个调用


的使用标准 C 库编译的程序。在此之前,还没有执行任何标准的 C 应用程序。
在桌面 Linux 系统上,第一个启动的程序通常是 /sbin/init。但是这不是一定的。很少有嵌入式系统


会需要使用 init 所提供的丰富初始化功能(这是通过 /etc/inittab 进行配置的)。在很多情况下,


我们可以调用一个简单的 shell 脚本来启动必需的嵌入式应用程序。


结束语
与 Linux 本身非常类似,Linux 的引导过程也非常灵活,可以支持众多的处理器和硬件平台。最初,加


载引导加载程序提供了一种简单的方法,不用任何花架子就可以引导 Linux。LILO 引导加载程序对引导


能力进行了扩充,但是它却缺少文件系统的感知能力。最新一代的引导加载程序,例如 GRUB,允许 


Linux 从一些文件系统(从 Minix 到 Reise)上进行引导。
========

Linux slab 分配器剖析

了解 Linux 内存管理的方式
良好的操作系统性能部分依赖于操作系统有效管理资源的能力。在过去,堆内存管理器是实际的规范,


但是其性能会受到内存碎片和内存回收需求的影响。现在,Linux® 内核使用了源自于 Solaris 的一种


方法,但是这种方法在嵌入式系统中已经使用了很长时间了,它是将内存作为对象按照大小进行分配。


本文将探索 slab 分配器背后所采用的思想,并介绍这种方法提供的接口和用法。


动态内存管理
内存管理的目标是提供一种方法,为实现各种目的而在各个用户之间实现内存共享。内存管理方法应该


实现以下两个功能:
最小化管理内存所需的时间
最大化用于一般应用的可用内存(最小化管理开销)
内存管理实际上是一种关于权衡的零和游戏。您可以开发一种使用少量内存进行管理的算法,但是要花


费更多时间来管理可用内存。也可以开发一个算法来有效地管理内存,但却要使用更多的内存。最终,


特定应用程序的需求将促使对这种权衡作出选择。
每个内存管理器都使用了一种基于堆的分配策略。在这种方法中,大块内存(称为 堆)用来为用户定义


的目的提供内存。当用户需要一块内存时,就请求给自己分配一定大小的内存。堆管理器会查看可用内


存的情况(使用特定算法)并返回一块内存。搜索过程中使用的一些算法有 first-fit(在堆中搜索到


的第一个满足请求的内存块 )和 best-fit(使用堆中满足请求的最合适的内存块)。当用户使用完内


存后,就将内存返回给堆。
这种基于堆的分配策略的根本问题是碎片(fragmentation)。当内存块被分配后,它们会以不同的顺序


在不同的时间返回。这样会在堆中留下一些洞,需要花一些时间才能有效地管理空闲内存。这种算法通


常具有较高的内存使用效率(分配需要的内存),但是却需要花费更多时间来对堆进行管理。
另外一种方法称为 buddy memory allocation,是一种更快的内存分配技术,它将内存划分为 2 的幂次


方个分区,并使用 best-fit 方法来分配内存请求。当用户释放内存时,就会检查 buddy 块,查看其相


邻的内存块是否也已经被释放。如果是的话,将合并内存块以最小化内存碎片。这个算法的时间效率更


高,但是由于使用 best-fit 方法的缘故,会产生内存浪费。
本文将着重介绍 Linux 内核的内存管理,尤其是 slab 分配提供的机制。
slab 缓存
Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 


的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分


配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间


。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状


态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始


化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构


之后,它已经处于所需的状态中了。
Linux slab 分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配


器。
图 1 给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。


这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。


cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理


的给定大小的对象池。
图 1. slab 分配器的主要结构
图 1. slab 分配器的主要结构
每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种 slab:
slabs_full
完全分配的 slab
slabs_partial
部分分配的 slab
slabs_empty
空 slab,或者没有对象被分配
注意 slabs_empty 列表中的 slab 是进行回收(reaping)的主要备选对象。正是通过此过程,slab 所


使用的内存被返回给操作系统供其他用户使用。
slab 列表中的每个 slab 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个对象。这


些对象是从特定缓存中进行分配和释放的基本元素。注意 slab 是 slab 分配器进行操作的最小分配单


位,因此如果需要对 slab 进行扩展,这也就是所扩展的最小值。通常来说,每个 slab 被分配为多个


对象。
由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当


一个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一


个 slab 完全被分配并且有对象被释放后,就从 slabs_full 列表中移动到 slabs_partial 列表中。当


所有对象都被释放之后,就从 slabs_partial 列表移动到 slabs_empty 列表中。
slab 背后的动机
与传统的内存管理模式相比, slab 缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分


配,它们会在系统生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供


这种功能,从而避免了常见的碎片问题。slab 分配器还支持通用对象的初始化,从而避免了为同一目而


对一个对象重复进行初始化。最后,slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的


对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。
回页首
API 函数
现在来看一下能够创建新 slab 缓存、向缓存中增加内存、销毁缓存的应用程序接口(API)以及 slab 


中对对象进行分配和释放操作的函数。
第一个步骤是创建 slab 缓存结构,您可以将其静态创建为:
struct struct kmem_cache *my_cachep;
然后其他 slab 缓存函数将使用该引用进行创建、删除、分配等操作。kmem_cache 结构包含了每个中央


处理器单元(CPU)的数据、一组可调整的(可以通过 proc 文件系统访问)参数、统计信息和管理 


slab 缓存所必须的元素。
kmem_cache_create
内核函数 kmem_cache_create 用来创建一个新缓存。这通常是在内核初始化时执行的,或者在首次加载


内核模块时执行。其原型定义如下:
struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,
                       unsigned long flags;
                       void (*ctor)(void*, struct kmem_cache *, unsigned long),
                       void (*dtor)(void*, struct kmem_cache *, unsigned long));
name 参数定义了缓存名称,proc 文件系统(在 /proc/slabinfo 中)使用它标识这个缓存。 size 参


数指定了为这个缓存创建的对象的大小, align 参数定义了每个对象必需的对齐。 flags 参数指定了


为缓存启用的选项。这些标志如表 1 所示。
表 1. kmem_cache_create 的部分选项(在 flags 参数中指定)
选项 说明
SLAB_RED_ZONE 在对象头、尾插入标志,用来支持对缓冲区溢出的检查。
SLAB_POISON 使用一种己知模式填充 slab,允许对缓存中的对象进行监视(对象属对象所有,不过


可以在外部进行修改)。
SLAB_HWCACHE_ALIGN 指定缓存对象必须与硬件缓存行对齐。
ctor 和 dtor 参数定义了一个可选的对象构造器和析构器。构造器和析构器是用户提供的回调函数。当


从缓存中分配新对象时,可以通过构造器进行初始化。
在创建缓存之后, kmem_cache_create 函数会返回对它的引用。注意这个函数并没有向缓存分配任何内


存。相反,在试图从缓存(最初为空)分配对象时,refill 操作将内存分配给它。当所有对象都被使用


掉时,也可以通过相同的操作向缓存添加内存。
kmem_cache_destroy
内核函数 kmem_cache_destroy 用来销毁缓存。这个调用是由内核模块在被卸载时执行的。在调用这个


函数时,缓存必须为空。
void kmem_cache_destroy( struct kmem_cache *cachep );
kmem_cache_alloc
要从一个命名的缓存中分配一个对象,可以使用 kmem_cache_alloc 函数。调用者提供了从中分配对象


的缓存以及一组标志:
void kmem_cache_alloc( struct kmem_cache *cachep, gfp_t flags );
这个函数从缓存中返回一个对象。注意如果缓存目前为空,那么这个函数就会调用 cache_alloc_refill 


向缓存中增加内存。 kmem_cache_alloc 的 flags 选项与 kmalloc 的 flags 选项相同。表 2 给出了


标志选项的部分列表。
表 2. kmem_cache_alloc 和 kmalloc 内核函数的标志选项
标志 说明
GFP_USER 为用户分配内存(这个调用可能会睡眠)。
GFP_KERNEL 从内核 RAM 中分配内存(这个调用可能会睡眠)。
GFP_ATOMIC 使该调用强制处于非睡眠状态(对中断处理程序非常有用)。
GFP_HIGHUSER 从高端内存中分配内存。
kmem_cache_zalloc
内核函数 kmem_cache_zalloc 与 kmem_cache_alloc 类似,只不过它对对象执行 memset 操作,用来在


将对象返回调用者之前对其进行清除操作。
kmem_cache_free
要将一个对象释放回 slab,可以使用 kmem_cache_free。调用者提供了缓存引用和要释放的对象。
void kmem_cache_free( struct kmem_cache *cachep, void *objp );
kmalloc 和 kfree
内核中最常用的内存管理函数是 kmalloc 和 kfree 函数。这两个函数的原型如下:
void *kmalloc( size_t size, int flags );
void kfree( const void *objp );
注意在 kmalloc 中,惟一两个参数是要分配的对象的大小和一组标志(请参看 表 2 中的部分列表)。


但是 kmalloc 和 kfree 使用了类似于前面定义的函数的 slab 缓存。kmalloc 没有为要从中分配对象


的某个 slab 缓存命名,而是循环遍历可用缓存来查找可以满足大小限制的缓存。找到之后,就(使用 


__kmem_cache_alloc)分配一个对象。要使用 kfree 释放对象,从中分配对象的缓存可以通过调用 


virt_to_cache 确定。这个函数会返回一个缓存引用,然后在 __cache_free 调用中使用该引用释放对


象。
其他函数
slab 缓存 API 还提供了其他一些非常有用的函数。 kmem_cache_size 函数会返回这个缓存所管理的对


象的大小。您也可以通过调用 kmem_cache_name 来检索给定缓存的名称(在创建缓存时定义)。缓存可


以通过释放其中的空闲 slab 进行收缩。这可以通过调用 kmem_cache_shrink 实现。注意这个操作(称


为回收)是由内核定期自动执行的(通过 kswapd)。
unsigned int kmem_cache_size( struct kmem_cache *cachep );
const char *kmem_cache_name( struct kmem_cache *cachep );
int kmem_cache_shrink( struct kmem_cache *cachep );
回页首
slab 缓存的示例用法
下面的代码片断展示了创建新 slab 缓存、从缓存中分配和释放对象然后销毁缓存的过程。首先,必须


要定义一个 kmem_cache 对象,然后对其进行初始化(请参看清单 1)。这个特定的缓存包含 32 字节


的对象,并且是硬件缓存对齐的(由标志参数 SLAB_HWCACHE_ALIGN 定义)。
清单 1. 创建新 slab 缓存
static struct kmem_cache *my_cachep;


static void init_my_cache( void )
{


   my_cachep = kmem_cache_create( 
                  "my_cache",            /* Name */
                  32,                    /* Object Size */
                  0,                     /* Alignment */
                  SLAB_HWCACHE_ALIGN,    /* Flags */
                  NULL, NULL );          /* Constructor/Deconstructor */


   return;
}
使用所分配的 slab 缓存,您现在可以从中分配一个对象了。清单 2 给出了一个从缓存中分配和释放对


象的例子。它还展示了两个其他函数的用法。
清单 2. 分配和释放对象
int slab_test( void )
{
  void *object;


  printk( "Cache name is %s\n", kmem_cache_name( my_cachep ) );
  printk( "Cache object size is %d\n", kmem_cache_size( my_cachep ) );


  object = kmem_cache_alloc( my_cachep, GFP_KERNEL );


  if (object) {


    kmem_cache_free( my_cachep, object );


  }


  return 0;
}
最后,清单 3 演示了 slab 缓存的销毁。调用者必须确保在执行销毁操作过程中,不要从缓存中分配对


象。
清单 3. 销毁 slab 缓存
static void remove_my_cache( void )
{


  if (my_cachep) kmem_cache_destroy( my_cachep );


  return;
}
回页首
slab 的 proc 接口
proc 文件系统提供了一种简单的方法来监视系统中所有活动的 slab 缓存。这个文件称为 


/proc/slabinfo,它除了提供一些可以从用户空间访问的可调整参数之外,还提供了有关所有 slab 缓


存的详细信息。当前版本的 slabinfo 提供了一个标题,这样输出结果就更具可读性。对于系统中的每


个 slab 缓存来说,这个文件提供了对象数量、活动对象数量以及对象大小的信息(除了每个 slab 的


对象和页面之外)。另外还提供了一组可调整的参数和 slab 数据。
要调优特定的 slab 缓存,可以简单地向 /proc/slabinfo 文件中以字符串的形式回转 slab 缓存名称


和 3 个可调整的参数。下面的例子展示了如何增加 limit 和 batchcount 的值,而保留 shared 


factor 不变(格式为 “cache name limit batchcount shared factor”):
# echo "my_cache 128 64 8" > /proc/slabinfo
limit 字段表示每个 CPU 可以缓存的对象的最大数量。 batchcount 字段是当缓存为空时转换到每个 


CPU 缓存中全局缓存对象的最大数量。 shared 参数说明了对称多处理器(Symmetric MultiProcessing


,SMP)系统的共享行为。
注意您必须具有超级用户的特权才能在 proc 文件系统中为 slab 缓存调优参数。
回页首
SLOB 分配器
对于小型的嵌入式系统来说,存在一个 slab 模拟层,名为 SLOB。这个 slab 的替代品在小型嵌入式 


Linux 系统中具有优势,但是即使它保存了 512KB 内存,依然存在碎片和难于扩展的问题。在禁用 


CONFIG_SLAB 时,内核会回到这个 SLOB 分配器中。更多信息请参看 参考资料 一节。
回页首
结束语
slab 缓存分配器的源代码实际上是 Linux 内核中可读性较好的一部分。除了函数调用的间接性之外,


源代码也非常直观,总的来说,具有很好的注释。如果您希望了解更多有关 slab 缓存分配器的内容,


建议您从源代码开始,因为它是有关这种机制的最新文档。 下面的 参考资料 一节提供了介绍 slab 缓


存分配器的参考资料,但是不幸的是就目前的 2.6 实现来说,这些文档都已经过时了。
========

Linux 调度器内幕

内核中这个非常重要的组件的最新版本改进了可伸缩性
Linux® 内核继续不断发展并采用新技术,在可靠性、可伸缩性和性能方面获得了长足的发展。2.6 版本


的内核最重要的特性之一是由 Ingo Molnar 实现的调度器。这个调度器是动态的,可以支持负载均衡,


并以恒定的速度进行操作 —— O(1)。本文将介绍 Linux 2.6 调度器的这些属性以及更多内容。


本文将回顾一下 Linux 2.6 的任务调度器及其最重要的一些属性。在深入介绍调度器的详细信息之前,


让我们先来理解一下调度器的基本目标。
什么是调度器?
通常来说,操作系统是应用程序和可用资源之间的媒介。典型的资源有内存和物理设备。但是 CPU 也可


以认为是一个资源,调度器可以临时分配一个任务在上面执行(单位是时间片)。调度器使得我们同时


执行多个程序成为可能,因此可以与具有各种需求的用户共享 CPU。
调度器的一个重要目标是有效地分配 CPU 时间片,同时提供很好的用户体验。调度器还需要面对一些互


相冲突的目标,例如既要为关键实时任务最小化响应时间,又要最大限度地提高 CPU 的总体利用率。下


面我们来看一下 Linux 2.6 调度程序是如何实现这些目标的,并与以前的调度器进行比较。
回页首
早期 Linux 调度器的问题
O-notation 的重要性
O-notation 可以告诉我们一个算法会占用多少时间。一个 O(n) 算法所需要的时间依赖于输入的多少(


与 n 是线性关系),而 O(n^2) 则是输入数量的平方。O(1) 与输入无关,可以在固定的时间内完成操


作。
在 2.6 版本的内核之前,当很多任务都处于活动状态时,调度器有很明显的限制。这是由于调度器是使


用一个复杂度为 O(n) 的算法实现的。在这种调度器中,调度任务所花费的时间是一个系统中任务个数


的函数。换而言之,活动的任务越多,调度任务所花费的时间越长。在任务负载非常重时,处理器会因


调度消耗掉大量的时间,用于任务本身的时间就非常少了。因此,这个算法缺乏可伸缩性。
在对称多处理系统(SMP)中,2.6 版本之前的调度器对所有的处理器都使用一个运行队列。这意味着一


个任务可以在任何处理器上进行调度 —— 这对于负载均衡来说是好事,但是对于内存缓存来说却是个


灾难。例如,假设一个任务正在 CPU-1 上执行,其数据在这个处理器的缓存中。如果这个任务被调度到 


CPU-2 上执行,那么数据就需要先在 CPU-1 使其无效,并将其放到 CPU-2 的缓存中。
以前的调度器还使用了一个运行队列锁;因此在 SMP 系统中,选择一个任务执行就会阻碍其他处理器操


作这个运行队列。结果是空闲处理器只能等待这个处理器释放出运行队列锁,这样会造成效率的降低。
最后,在早期的内核中,抢占是不可能的;这意味着如果有一个低优先级的任务在执行,高优先级的任


务只能等待它完成。
回页首
Linux 2.6 调度器简介
2.6 版本的调度器是由 Ingo Molnar 设计并实现的。Ingo 从 1995 年开始就一直参与 Linux 内核的开


发。他编写这个新调度器的动机是为唤醒、上下文切换和定时器中断开销建立一个完全 O(1) 的调度器


。触发对新调度器的需求的一个问题是 Java™ 虚拟机(JVM)的使用。Java 编程模型使用了很多执行线


程,在 O(n) 调度器中这会产生很多调度负载。O(1) 调度器在这种高负载的情况下并不会受到太多影响


,因此 JVM 可以有效地执行。
2.6 版本的调度器解决了以前调度器中发现的 3 个主要问题(O(n) 和 SMP 可伸缩性的问题),还解决


了其他一些问题。现在我们将开始探索一下 2.6 版本的调度器的基本设计。
主要的调度结构
首先我们来回顾一下 2.6 版本的调度器结构。每个 CPU 都有一个运行队列,其中包含了 140 个优先级


列表,它们是按照先进先出的顺序进行服务的。被调度执行的任务都会被添加到各自运行队列优先级列


表的末尾。每个任务都有一个时间片,这取决于系统允许执行这个任务多长时间。运行队列的前 100 个


优先级列表保留给实时任务使用,后 40 个用于用户任务(参见图 1)。我们稍后将来看一下为什么这


种区别非常重要。
图 1. Linux 2.6 调度器的运行队列结构
Linux 2.6 调度器的运行队列结构
除了 CPU 的运行队列(称为活动运行队列(active runqueue))之外,还有一个过期运行队列。当活


动运行队列中的一个任务用光自己的时间片之后,它就被移动到过期运行队列(expired runqueue) 中


。在移动过程中,会对其时间片重新进行计算(因此会体现其优先级的作用;稍后会更详细地介绍)。


如果活动运行队列中已经没有某个给定优先级的任务了,那么指向活动运行队列和过期运行队列的指针


就会交换,这样就可以让过期优先级列表变成活动优先级的列表。
调度器的工作非常简单:它在优先级最高的队列中选择一个任务来执行。为了使这个过程的效率更高,


内核使用了一个位图来定义给定优先级列表上何时存在任务。因此,在大部分体系架构上,会使用一条 


find-first-bit-set 指令在 5 个 32 位的字(140 个优先级)中哪一位的优先级最高。查找一个任务


来执行所需要的时间并不依赖于活动任务的个数,而是依赖于优先级的数量。这使得 2.6 版本的调度器


成为一个复杂度为 O(1) 的过程,因为调度时间既是固定的,而且也不会受到活动任务个数的影响。
更好地支持 SMP 系统
那么什么是 SMP 呢?SMP 是一种体系架构,其中多个 CPU 可以用来同时执行各个任务,它与传统的非


对称处理系统不同,后者使用一个 CPU 来执行所有的任务。SMP 体系架构对多线程的应用程序非常有益



尽管优先级调度在 SMP 系统上也可以工作,但是它这种大锁体系架构意味着当一个 CPU 选择一个任务


进行分发调度时,运行队列会被这个 CPU 加锁,其他 CPU 只能等待。2.6 版本的调度器不是使用一个


锁进行调度;相反,它对每个运行队列都有一个锁。这样允许所有的 CPU 都可以对任务进行调度,而不


会与其他 CPU 产生竞争。
另外,由于每个处理器都有一个运行队列,因此任务通常都是与 CPU 密切相关的,可以更好地利用 CPU 


的热缓存。
任务抢占
Linux 2.6 版本调度器的另外一个优点是它允许抢占。这意味着当高优先级的任务准备运行时低优先级


的任务就不能执行了。调度器会抢占低优先级的进程,并将这个进程放回其优先级列表中,然后重新进


行调度。
回页首
但是请等一下,还有更多功能呢!
似乎 2.6 版本调度器的 O(1) 特性和抢占特性还不够,这个调度器还提供了动态任务优先级和 SMP 负


载均衡功能。下面就让我们来讨论一下这些功能都是什么,以及它们分别提供了哪些优点。
动态任务优先级
为了防止任务独占 CPU 从而会饿死其他需要访问 CPU 的任务,Linux 2.6 版本的调度器可以动态修改


任务的优先级。这是通过惩罚 CPU 绑定的任务而奖励 I/O 绑定的任务实现的。I/O 绑定的任务通常使


用 CPU 来设置 I/O,然后就睡眠等待 I/O 操作完成。这种行为为其他任务提供了 CPU 的访问能力。
用户响应能力更好
与用户进行通信的任务都是交互型的,因此其响应能力应该比非交互式任务更好。由于与用户的通信(


不管是向标准输出上发送数据,还是通过标准输入等待输入数据)都是 I/O 绑定型的,因此提高这些任


务的优先级可以获得更好的交互式响应能力。
由于 I/O 绑定型的任务对于 CPU 访问来说是无私的,因此其优先级减少(奖励)最多 5 个优先级。


CPU 绑定的任务会通过将其优先级增加最多 5 个优先级进行惩罚。
任务到底是 I/O 绑定的还是 CPU 绑定的,这是根据交互性 原则确定的。任务的交互性指标是根据任务


执行所花费的时间与睡眠所花费的时间的对比程度进行计算的。注意,由于 I/O 任务先对 I/O 进行调


度,然后再进行睡眠,因此 I/O 绑定的任务会在睡眠和等待 I/O 操作完成上面花费更多的时间。这会


提高其交互性指标。
有一点值得注意,优先级的调整只会对用户任务进行,对于实时任务来说并不会对其优先级进行调整。
SMP 负载均衡
在 SMP 系统中创建任务时,这些任务都被放到一个给定的 CPU 运行队列中。通常来说,我们无法知道


一个任务何时是短期存在的,何时需要长期运行。因此,最初任务到 CPU 的分配可能并不理想。
为了在 CPU 之间维护任务负载的均衡,任务可以重新进行分发:将任务从负载重的 CPU 上移动到负载


轻的 CPU 上。Linux 2.6 版本的调度器使用负载均衡(load balancing) 提供了这种功能。每隔 


200ms,处理器都会检查 CPU 的负载是否不均衡;如果不均衡,处理器就会在 CPU 之间进行一次任务均


衡操作。
这个过程的一点负面影响是新 CPU 的缓存对于迁移过来的任务来说是冷的(需要将数据读入缓存中)。
记住 CPU 缓存是一个本地(片上)内存,提供了比系统内存更快的访问能力。如果一个任务是在某个 


CPU 上执行的,与这个任务有关的数据都会被放到这个 CPU 的本地缓存中,这就称为热的。如果对于某


个任务来说,CPU 的本地缓存中没有任何数据,那么这个缓存就称为冷的。
不幸的是,保持 CPU 繁忙会出现 CPU 缓存对于迁移过来的任务为冷的情况。
回页首
挖掘更多潜能
2.6 版本调度器的源代码都很好地封装到了 /usr/src/linux/kernel/sched.c 文件中。我们在表 1 中


对在这个文件中可以找到的一些有用的函数进行了总结。
表 1. Linux 2.6 调度器的功能
函数名 函数说明
schedule 调度器主函数。调度优先级最高的任务执行。
load_balance 检查 CPU,查看是否存在不均衡的情况,如果不均衡,就试图迁移任务。
effective_prio 返回任务的有效优先级(基于静态策略,但是可以包含任何奖励和惩罚)。
recalc_task_prio 根据任务的空闲时间确定对任务的奖励或惩罚。
source_load 适当地计算源 CPU(任务从中迁移出的 CPU)的负载。
target_load 公平地计算目标 CPU(任务可能迁移到的 CPU)的负载。
migration_thread 在 CPU 之间迁移任务的高优先级的系统线程。
运行队列的结构也可以在 /usr/src/linux/kernel/sched.c 文件中找到。2.6 版本的调度器还可以提供


一些统计信息(如果启用了 CONFIG_SCHEDSTATS)。这些统计信息可以从 /proc 文件系统中的 


/proc/schedstat 看到,它为系统中的每个 CPU 都提供了很多数据,包括负载均衡和进程迁移的统计信


息。
回页首
展望
Linux 2.6 调度器从早先的 Linux 调度器已经跨越了一大步。它极大地改善了最大化利用 CPU 的能力


,同时还为用户提供了很好的响应体验。抢占和对多处理器体系架构的更好支持使整个系统更接近于多


桌面和实时系统都非常有用的操作系统。Linux 2.8 版本的内核现在谈论还为时尚早,但是从 2.6 版本


的变化中,我们可以期望会有更多的好东西。
========

相关链接

http://www.ibm.com/developerworks/cn/linux/l-linux-kernel/

你可能感兴趣的:(linux,linux,kernel)