第2章 内核一瞥
在我们开始步入Linux设备驱动的神秘世界之前,让我们先熟悉一些从驱动开发人员应该理解的基本的内核概念。我们将学习到内核定时器、同步机制以及内存分配方法,但是,先让我们从顶层视角开始探索,扫描一下内核发出的启动信息,并在感兴趣的地方设置停下来看一看。
启动过程
图2.1显示了基于x86计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR),接下来MBR中的代码查看分区表并从活动分区读取GRUB、LILO或SYSLINUX等bootloader,之后bootloader会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会将自身解压缩并投入运转。
图2.1 基于x86的硬件上Linux的启动过程

基于x86的处理器有两种操作模式:实模式和保护模式。在实模式下,用户仅可以使用1MB内存,并且没有任何保护。保护模式则更加复杂,用户可以使用更多的高级功能(如分页)。CPU提供了一条由实模式通向保护模式的道路,但是,这条路只允许单向行驶,用户不能从保护模式再切换回实模式。
内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下init/main.c文件(上一章我们修改了这个文件)中的start_kernel()函数。start_kernel()函数首先会初始化CPU子系统,之后让内存管理和进程管理系统就位,接下来启动外部总线和I/O设备,最后的一步是激活所有Linux进程的父亲init。init执行用户空间的脚本以启动必要的内核服务,它最终派生控制台终端程序并显示登录(login)提示。
接下来,每一小节的标题都是图2.2中的一条打印信息,这些信息来源于基于x86的笔记本电脑的Linux启动过程。如果你在启动体系结构上启动Linux,消息以及语义可能会有所改变。如果本节中的一些内容读起来非常晦涩,请不要担心。目前的目的仅是从100英尺的高度给你一个视图,让你初次品尝内核甜点的味道。接下来要提到的许多概念都会在以后的章节中进行更深的论述。
图2.2 内核启动信息
Linux version 2.6.23.1y (root@localhost.localdomain) (gcc version 4.1.1 20061011 (Red Hat 4.1.1-30)) #7 SMP PREEMPT Thu Nov 1 11:39:30 IST 2007 BIOS-provided physical RAM map: BIOS-e820: 0000000000000000 - 000000000009f000 (usable) BIOS-e820: 000000000009f000 - 00000000000a0000 (reserved) ... 758MB LOWMEM available. ... Kernel command line: ro root=/dev/hda1 ... Console: colour VGA+ 80x25 ... Calibrating delay using timer specific routine.. 1197.46 BogoMIPS (lpj=2394935) ... CPU: L1 I cache: 32K, L1 D cache: 32K CPU: L2 cache: 1024K ... Checking 'hlt' instruction... OK. ... Setting up standard PCI resources ... NET: Registered protocol family 2 IP route cache hash table entries: 32768 (order: 5, 131072 bytes) TCP established hash table entries: 131072 (order: 9, 2097152 bytes) ... checking if image is initramfs... it is Freeing initrd memory: 387k freed ... io scheduler noop registered io scheduler anticipatory registered (default) ... 00:0a: ttyS0 at I/O 0x3f8 (irq = 4) is a NS16550A ... Uniform Multi-Platform E-IDE driver Revision: 7.00alpha2 ide: Assuming 33MHz system bus speed for PIO modes; override with idebus=xx ICH4: IDE controller at PCI slot 0000:00:1f.1 Probing IDE interface ide0... hda: HTS541010G9AT00, ATA DISK drive hdc: HL-DT-STCD-RW/DVD DRIVE GCC-4241N, ATAPI CD/DVD-ROM drive ... serio: i8042 KBD port at 0x60,0x64 irq 1 mice: PS/2 mouse device common for all mice ... Synaptics Touchpad, model: 1, fw: 5.9, id: 0x2c6ab1, caps: 0x884793/0x0 ... agpgart: Detected an Intel 855GM Chipset. ... Intel(R) PRO/1000 Network Driver - version 7.3.20-k2 ... ehci_hcd 0000:00:1d.7: EHCI Host Controller ... Yenta: CardBus bridge found at 0000:02:00.0 [1014:0560] ... Non-volatile memory driver v1.2 ... kjournald starting. Commit interval 5 seconds EXT3 FS on hda2, internal journal EXT3-fs: mounted filesystem with ordered data mode. ... INIT: version 2.85 booting ... |
BIOS-provided physical RAM map
内核解析从BIOS中读取到的系统内存映射,并率先将这些信息打印出来:
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码通过使用BIOS的int 0x15服务并执行0xe820号函数来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将使用这些信息创建其可用的内存池。在附录B《Linux和BIOS》的《实模式调用》一节,我们会对BIOS提供的内存映射问题进行更深入的讲解。
758MB LOWMEM Available
896MB以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数kmalloc()就是从该区域分配内存的。高于896MB被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。在启动过程中,内核会计算并显示这些内存zone内总的页数,在本章的稍后,会对这些内存zone进行更深入的分析。
Kernel Command Line: ro root=/dev/hda1
Linux的bootloader通常会给内核传递一个命令行。命令行中的参数类似于传递给C程序中main()函数的argv[]列表,唯一的不同是它们是传递给内核的。你可以在bootloader的配置文件中增加命令行参数,当然,也可以在运行过程中对bootloader的提示行进行修改[1]。如果你正在使用GRUB这个bootloader,归因于发行版的不同,其配置文件可能是/boot/grub/grub.conf或者是/boot/grub/menu.lst。如果你正在使用LILO,配置文件为/etc/lilo.conf。下面给出了一个grub.conf文件的例子(增加了一些注释),阅读了紧接着“title kernel 2.6.23”后的一行之后,你会发现前述打印信息的由来。\
[1] 嵌入式设备上的bootloader通常经过了“瘦身”,并不支持配置文件或类似机制。归因于此,许多非x86体系结构提供了CONFIG_CMDLINE这个内核配置选项,通过它,用户可以在编译内核时提供内核命令行。
default 0 #Boot the 2.6.23 kernel by default
timeout 5 #5 second to alter boot order or parameters
title kernel 2.6.23 #Boot Option 1
#The boot image resides in the first partition of the first disk
#under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
#indicates that the root partition should be mounted read-only.
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#Look under section "Freeing initrd memory:387k freed"
initrd (hd0,0)/boot/initrd
#...
命令行参数将影响启动过程中的代码执行路径。举一个例子,假设某命令行参数为bootmode,如果该参数被设置为1,意味着你希望在启动过程中打印一些调试信息并在启动结束时切换到runlevel的第3级(到我们分析init进程的打印信息时,会学习到runlevel的含义);如果bootmode参数被设置为0,意味着你希望启动过程相对简洁,并且设置runlevel为2。因为你已经熟悉了init/main.c文件,让我们在该文件中增加如下修改:
static unsigned int bootmode = 1;
static int __init
is_bootmode_setup(char *str)
{
get_option(&str, &bootmode);
return 1;
}
/* Handle parameter "bootmode=" */
__setup("bootmode=", is_bootmode_setup);
if (bootmode) {
/* Print verbose output */
/* ... */
}
/* ... */
/* If bootmode is 1, choose an init runlevel of 3, else
switch to a run level of 2 */
if (bootmode) {
argv_init[++args] = "3";
} else {
argv_init[++args] = "2";
}
/* ... */
请重新编译内核并尝试新的修改。另外,本书第18章《嵌入式Linux》的《内存分布》一节也将对命令行参数进行更多的讲解。
Calibrating Delay...1197.46 BogoMIPS (lpj=2394935)
在启动过程中,内核会计算处理器在一个jiffy时间内运行一个内部的delay循环的次数。jiffy的含义是系统定时器2个连续的节拍之间的间隔。如果你所期待的那样,该计算必须被校准到你的CPU的处理速度。校准的结果被存储在称为loops_per_jiffy的内核变量中。使用loops_per_jiffy的一个场合是某设备驱动希望进行小的微妙级别的延迟的时候。
为了理解delay循环校准代码,让我们看一下定义于init/calibrate.c文件中的calibrate_delay()函数。该函数机智地使用整型运算得到了浮点的精度。如下的代码片段(增加了一些注释)显示了该函数的开始部分,这部分用于得到一个粗略的loops_per_jiffy:
loops_per_jiffy = (1 << 12); /* Initial approximation = 4096 */
printk(KERN_DEBUG "Calibrating delay loop... ");
while ((loops_per_jiffy <<= 1) != 0) {
ticks = jiffies; /* As you will find out in the section, "Kernel
Timers," the jiffies variable contains the
number of timer ticks since the kernel
started, and is incremented in the timer
interrupt handler */
while (ticks == jiffies); /* Wait until the start
of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/* Did the wait outlast the current jiffy? Continue if
it didn't */
ticks = jiffies - ticks;
if (ticks) break;
}
loops_per_jiffy >>= 1; /* This fixes the most significant bit and is
the lower-bound of loops_per_jiffy */
上述代码首先假定loops_per_jiffy高于4096,这可以转化为处理器速度大约为每秒100万条指令,即1MIPS。接下来,它等待jiffy被刷新(1个新的节拍的开始),并开始运行delay循环__delay(loops_per_jiffy)。如果这个delay循环持续了1个jiffy以上,将使用以前的loops_per_jiffy值(将当前值右移1位)修复当前loops_per_jiffy的最高位;否则,该函数继续通过左移loops_per_jiffy值来探测出其最高位。在内核计算出最高位后,它开始计算低位并微调其精度:
loopbit = loops_per_jiffy;
/* Gradually work on the lower-order bits */
while (lps_precision-- && (loopbit >>= 1)) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies); /* Wait until the start
of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~loopbit;
}
上述代码计算出了delay循环跨越jiffy边界时loops_per_jiffy的低位值。这个被校准的值可被用于获取BogoMIPS(其实它是一个并非科学的处理器速度指标)。你可以使用BogoMIPS作为衡量处理器运行速度的相对尺度。在1.6Ghz 基于Pentium M的笔记本电脑上,根据前述启动过程的打印信息,delay循环校准的结果趋向于loops_per_jiffy的值为2394935。获得BogoMIPS的方式如下:
| BogoMIPS = loops_per_jiffy * 1秒内的jiffy数 * delay循环消耗的指令数(以百万为单位) |
| | = (2394935 * HZ * 2) / (1 million) |
| | = (2394935 * 250 * 2) / (1000000) |
| | = 1197.46 (与启动过程打印信息中的值一致) |
在本章《内核定时器》一节,将有对jiffy、HZ和loops_per_jiffy更深入的阐述。
Checking HLT Instruction
由于Linux内核支持多种硬件平台,启动代码会检查体系结构相关的bug。其中一项工作就是验证停机(HLT)指令。
x86处理器的HLT指令会将CPU置入一种低功耗睡眠模式,直到下一次硬件中断发生之前维持不变。当内核想让CPU进入空闲状态时(查看arch/x86/kernel/process_32.c 文件中定义的cpu_idle()函数),它会使用HLT指令。对于有问题的CPU而言,命令行参数no-hlt可以禁止HLT指令。如果no-hlt被设置,在空闲的时候,内核会进行忙等待而不是通过HLT给CPU降温。
当init/main.c中的启动代码调用include/asm-your-arch/bugs.h中定义的check_bugs()时,会打印上述信息。
NET: Registered Protocol Family 2
Linux套接字(socket)层是用户空间应用程序访问各种网络协议的统一接口。每个协议通过include/linux/socket.h文件中定义的被分配给它的独一无二的家族(family)号注册自身。上述打印信息中的Family 2代表AF_INET(Internet协议)。启动过程中另一个常见的被打印的信息是AF_NETLINK(Family 16)。Netlink socket提供了用户进程和内核通信的方法。通过netlink socket可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用法列表)。对于此类任务而言,netlink socket比系统调用更合适,因为前者具有采用异步机制、更易于实现和可动态连接的优点。
内核中经常使能的另一个协议家族是AF_UNIX或UNIX-domain套接字。X Windows等程序使用它们在同一个系统在进行进程间通信。
Freeing Initrd Memory: 387k Freed
Initrd是一种由bootloader加载的常住内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分区时所依赖的可动态连接的模块。由于内核可运行于各种各样的存储控制器硬件平台上,把所有可能的磁盘驱动都直接放进基本的内核映像中并非一种灵活的方式。你所使用的系统的存储设备的驱动被打包放入了initrd中,在内核启动后、实际的根文件系统被挂载之前,这些驱动才被加载。使用mkinitrd命令可以创建一个initrd映像。
2.6内核提供了一种称为initramfs的新功能,它在几个方面较initrd更为优秀。后者模拟了一个磁盘(因而被称为initramdisk或initrd),会带来Linux块I/O子系统的开销(如缓冲),然后前者基本上如同一个被挂载的文件系统一样,由自身获取缓冲(因此被称作initramfs)。
不同于initrd,基于页缓冲建立的initramfs如同页缓冲一样会动态地变大和缩小,从而减少了其内存消耗。另外,initrd要求你的内核映像包含了initrd所使用的文件系统(例如,如果你的initrd为EXT2文件系统,内核必须包含EXT2驱动),然而initramfs不需要文件系统支持。再者,由于initramfs只是页缓冲之上的一小层,因此它的代码量很小。
用户可以将初始根文件系统打包为一个cpio压缩包[2],并通过initrd=命令行参数传递给内核。当然,也可以在内核配置过程中通过INITRAMFS_SOURCE选项直接编译进内核。对于后一种方式而言,用户可以提供cpio压缩包的文件名或者包含initramfs的目录树。在启动过程中,内核会将文件解压缩为一个initramfs根文件系统,如果它找到了/init,它就会执行该顶层的程序。这种获取初始根文件系统的方法对于嵌入式系统而言特别有用,因为在嵌入式系统中系统资源非常宝贵。使用mkinitramfs可以创建一个initramfs映像,查看文档Documentation/filesystems/ramfs-rootfs-initramfs.txt可获得更多信息。
[2] cpio是一种UNIX压缩文件格式,从www.gnu.org/software/cpio可以下载到它。
在本例中,我们使用的是通过initrd=命令行参数向内核传递初始根文件系统cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为387K)并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。
在第18章中我们会发现,在嵌入式系统开发过程中,initrd和initramfs有时候也可被用作嵌入式设备上实际的根文件系统。
IO Scheduler Anticipatory Registered (Default)
I/O调度器的主要目标是通过减少磁盘的定位次数以增加系统的吞吐率。在磁盘定位过程中,磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的延迟。2.6内核提供了4种不同的I/O调度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。从上述内核打印信息可以看出,本例将Anticipatory 设置为了缺省的I/O调度器。在第14章《块设备驱动》中,我们将学习I/O调度的知识。
Setting Up Standard PCI Resources
启动过程的下一阶段会初始化I/O总线和外围控制器。内核会通过遍历PCI总线来探测PCI硬件,接下来再初始化其他的I/O子系统。从图2.3中中我们会看到SCSI子系统、USB控制器、视频芯片(855北桥芯片组信息中的一部分)、串口(本例中为8250 UART)、PS/2键盘和鼠标、软驱、ramdisk、loopback设备、IDE控制器(本例中为ICH4南桥芯片集中的一部分)、触控板、以太网控制器(本例中为e1000)以及PCMCIA控制器初始化的启动信息。图2.3中——>符号指向的为I/O设备的标识(ID)。
SCSI subsystem initialized ——>SCSI usbcore: registered new driver hub ——>USB agpgart: Detected an Intel 855 Chipset. ——>Video [drm] Initialized drm 1.0.0 20040925 PS/2 Controller [PNP0303:KBD,PNP0f13:MOU] at 0x60,0x64 irq 1,12 serio: i8042 KBD port ——> |