第 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 提供的内存映射问题进行更深入的讲解。
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 值来探测出其最高位。在内核计算出最高位后,它开始计算低位并微调其精度:
/* 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 * 250 * 2) / (1000000) |
|