Essential Linux Device 第二章 1
时间:2009-06-08 来源:walker_wu
booting up
图2.1说明了LINUX在x-86计算机的启动顺序。基于x-86硬件的LINUX启动,第一步BIOS从启动设备中加载主引导纪录(MBR);然后主引导纪录(MBR)中的代码查看分区表,并且从活动分区中读取GRUB, LILO和 SYSLINUX等 bootloader;最后一步是加载压缩的内核映像,并将控制权转交给它。内核自我解压开始运行。
图2.1 LINUX在x-86计算机的启动顺序
基于x-86的处理器有两种操作模式,实模式和保护模式。在实模式里,你只能使用1MB的内存,而且是没有任何的保护。保护模式要更加的复杂,让你可以使用许多的处理器高级功能,比如:分页。CPU必须提供一个从实模式到保护模式的路径。这是一条单向的路径。你不能从保护模式转换回实模式。
第一步的内核初始化在实模式汇编时完成。下一步是init/main.c中定义的start_kernel()函数在保护模式下完成的,上一章你已经修改过了源文件。start_kernel()先初始化CPU的子系统。之后是加载内存和进程管理器,接下来是开始外围总线和I/O设备。启动的最后一步是init程序—所有LINUX进程的父进程。init运行用户空间脚本以开始必要的内核服务。最后派生出控制台终端,并显示登录提示。
接下里每一节的题目是图2.2中的一个消息,它们是在基于x-86笔记本中引导过程中产生的。如果你在其他体系下加载内核,语义和消息可能改变。如果在这一节有些解释听起来比较隐秘,不要担心;这里只是给你提供一个100英尺上的一张图片,让你初次体验一下内核的好处。接下来这里提到的许多概念将会更深入的讨论。
图 2.2.内核引导程序.
Code View:
Linux version 2.6.23.1y ([email protected]) (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函数号(这就是先前消息中的BIOS-e820字符串)来获得系统内存映射。内存映射指示了保留(reserved)和可用(usable)的内存范围,内核将在创建可用内存池时只用它们。我们将在附录B中“Linux and the BIOS”的 “Real Mode Calls”一节对BIOS提供的内存映射进行进一步的讨论,
758MB LOWMEM Available
通常可寻址的内核内存区(< 896MB)被称之为低地址。内核内存分配器kmalloc()从这个区域返回内存。高于896MB的内存(被称为高地址)只有只用特殊的映射才能访问。
在引导时,内核计算和显示当时在内存区中页的总数。我们可以在这一章接下来的部分对内存区经行更深入的理解。
Kernel Command Line: ro root=/dev/hda1
LINUX bootloaders通常传送一个命令行到内核。命令行的参数类似于C程序中传递给main()函数的argv[]表,不同的是它们是传递给内核。你可能在bootloader的配置文件中增加命令行参数或者是在运行时通过bootloader提示来提供。[1] 如果你使用的是GRUB bootloader,配置文件是/boot/grub/grub.conf或者/boot/grub/menu.lst中是取决于你的发行版。如果你是LILO使用者,配置文件是/etc/lilo.con。下面是一个grub.conf文件的例子(添加了一些注释)。阅读了title kernel 2.6.23后的一行之后,你会发现前述打印信息的由来:
[1]嵌入设备的Bootloaders通常都被消减了,不支持配置文件或者是类似的机制。因此,许多非x-86体系支持一种内核的配置选项,叫做CONFIG_CMDLINE。你可以在bulid时通过它来提供内核命令行。
default 0 #默认引导2.6.23内核
timeout 5 #5秒后修改引导命令或参数
title kernel 2.6.23 #Boot Option 1
#引导映像在下第一个硬盘的第一个分区的/boot/ directory目录
#且命名为vmlinuz-2.6.23。
#'ro'指示根分区时只读的
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#看下面 "Freeing initrd memory:387k freed一节"
initrd (hd0,0)/boot/initrd
#...
命令行参数在引导时影响代码执行路径。一个简单的例子,假设命令行参数为bootmode。如果它被置为1,你将在引导时打印调试消息,在引导结束时转至runlevel的级别3.(等到init进程打印引导消息时,我们再学习runlevel的含义。)如果bootmode被置为0,你可能喜欢相对精简的引导过程,且runleve被置为2。因为你早就熟悉init/main.c了,我们就给它添加下列修改:
Code View:
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";
}
/* ... */
尽早的重新编译内核并找出改变。我们将在"Embedding Linux"18章"Memory Layout"一节对内核命令行参数做更多的讨论。
Calibrating Delay...1197.46 BogoMIPS (lpj=2394935)
引导时,内核计算一个jiffy内处理器能够运行内部延迟循环的次数,jiffy是系统定时器2个连续的节拍之间的间隔。正如你做期望的,该计算是校准与你CPU的处理速度。这个校准的结果存储在名为loops_per_jiffy的内核变量中。当一个设备驱动希望进行小的微妙级别的延迟的时候,内核会使用loops_per_jiffy。
为了理解延迟循环校准代码,我们先看一看定义在init/calibrate.c中的calibrate_delay()。该函数智慧地使用整型运算得到了浮点的精度。下面的片段(添加了一些注释)显示了函数的初始部分,用于得到loops_per_jiffy的大略值:
loops_per_jiffy = (1 << 12); /* 初始近似值 = 4096 */
printk(KERN_DEBUG "Calibrating delay loop... ");
while ((loops_per_jiffy <<= 1) != 0) {
ticks = jiffies; /* 像是你将要在本节发现的, "Kernel Timers,"
变量包含了内核开始后的时间节拍数,
在定时器中断时被增加 */
while (ticks == jiffies); /* 等到下个 jiffy开始 */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/*等到当前jiffy继续(outlast)?如果不是,则继续 */
ticks = jiffies - ticks;
if (ticks) break;
}
loops_per_jiffy >>= 1; /* 修复最高位和loops_per_jiffy的低位 */
上述代码以假设loops_per_jiffy比4096大开始,这可以转化为处理器速度大约为每秒100万条指令,即1MIPS。然后等待一个新的jiffy开始运行延迟循环,__delay(loops_per_jiffy)。如果这个延迟循环持续了一个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;
}
以上代码指出了当延迟循环越过一个jiffy边界时,loops_per_jiffy的低位的精密组合。这个校准值被用来获得BogoMIPS(一个非科学的处理器速度指标)。你可以使用BogoMIPS去描述一个CPU可以跑多快。在1.6Ghz 基于Pentium M的笔记本电脑上,根据前述启动过程的打印信息,delay循环校准的结果趋向于loops_per_jiffy的值为2394935。
BogoMIPS = loops_per_jiffy * 1 秒内的jiffies数 * 内部延迟循环消耗的指令数,以百万位单位
= (2394935 * HZ * 2) / (1 million)
= (2394935 * 250 * 2) / (1000000)
= 1197.46 (像是先前引导消息显示的)
我们将在这章的"Kernel Timers"一节对jiffies, HZ, 和 loops_per_jiffy做更加深入的讨论。
Checking HLT Instruction
因为LINUX内核被多种硬件平台支持,引导代码检查体系相关的bug。其中一个检查就是修改停机(HLT)指令。
T HLT指令被x-86处理器支持,它可以让CPU处于一个低电量的睡眠模式直到下次硬件中断发生。内核想让CPU进入空闲状态时,它会使用HLT指令(查看定义在arch/x86/kernel/process_32.c中的cpu_idle() 函数)。
对于存在问题的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)层是用户空间应用程序访问各种网络协议的统一接口。每个协议通过被分配的独一无二的家族(family)号(定义在include/linux/socket.h)文件中定义的注册自身。上述打印信息中的Family 2代表AF_INET(Internet协议)。
其他的经常在引导消息中看到的注册协议家族(family)是AF_NETLINK (Family 16)。Netlink sockets提供一个在用户进程和内核间通信的方法。通过netlink socket可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用法列表)。Netlink sockets比起系统调用更适合完成这种任务,因为netlink socket采用的同步机制、易于实现而且动态链接。
其他通常在内核中可用的协议家族(family)是AF_UNIX or UNIX-domain sockets。程序,比如X Windows使用他们在相同系统中进行进程间通信。
Freeing Initrd Memory: 387k Freed
Initrd是bootloader加载的常驻内存的虚拟磁盘映像。在内核引导后,它会被挂载为初始根文件系统,用来存放额外的动态可加载模块,该模块用来挂载实际根文件系统的磁盘分区。因为内核运行在不同的硬件平台,它们使用不同的存储控制器,所以把所有可能的磁盘驱动都直接放进基本的内核映像中并不是一种灵活的方式。。适合你系统存储设备的驱动被打包在initrd,在内核引导后被加载,但是要在根文件系统挂载前。创建一个initrd映像,使用mkinitrd命令。
2.6内核包含了一种叫做initramfs的特征,它比起initrd具有更多的优点。后者模拟了一个磁盘(因而被称为initramdisk或initrd),会带来Linux块I/O子系统的开销(如缓冲),然后前者基本上像是挂载一个文件系统一样,由自身获取缓冲(因此被称作initramfs)。
initramfs是基于页缓冲建立的,不像initrd它会如同页缓冲一样会动态地变大和缩小,以减少内存消耗。另外,initrd要求你的内核映像包含了相关的文件系统驱动(例如,如果你有一个EXT2文件系统在你的 initrd,则必须有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章"Block Drivers"中详细讨论。
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控制器初始化的启动信息。I/O设备相应的身份(identity)被再次标号
图 2.3. 在引导时初始化总线和外围控制器
Code View:
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 Keyboard
serial8250: ttyS0 at I/O 0x3f8 (irq = 4)
is a NS16550A Serial Port
Floppy drive(s): fd0 is 1.44M Floppy
RAMDISK driver initialized: 16 RAM disks
of 4096K size 1024 blocksize Ramdisk
loop: loaded (max 8 devices) Loop back
ICH4: IDE controller at PCI slot
0000:00:1f.1 Hard Disk
...
input: SynPS/2 Synaptics TouchPad as
/class/input/input1 Touchpad
e1000: eth0: e1000_probe: Intel® PRO/1000
Network Connection Ethernet
Yenta: CardBus bridge found at
0000:02:00.0 [1014:0560] PCMCIA/CardBus
...
本书会以单独的章节讨论了许多个上述的驱动子系统。请注意如果驱动作为可加载的模块被动态连接到内核,其中的一些消息也许只有在内核启动后才会被显示。
EXT3-fs: Mounted Filesystem
EXT3文件系统已经成为LINUX事实上的文件系统。EXT3在退役的EXT2文件系统基础上增添了日志层使得崩溃后文件系统可以快速恢复。它的目标是不经由耗时的文件系统检查(fsck)操作即可获得一个一致的文件系统。EXT2仍然是新文件系统的工作引擎,但是EXT3层会在进行实际的磁盘改变之前记录文件交互的日志。EXT3向后兼容于EXT2,因此,你可以在你现存的EXT2文件系统上批上EXT3的大衣或者脱去EXT3的大衣以回到EXT2文件系统。
EXT4
EXT文件系统的最新版本是EXT4,自2.6.19内核以来,EXT4已经被增加到了主线Linux内核中,但是被注明为“experimental”,名称为ext4dev。EXT4很大程度上向后兼容于EXT3,其主页为www.bullopensource.org/ext4。
EXT3会启动一个称为kjournald的内核辅助线程(在接下来的一章中将深入讨论内核线程)来完成日志功能。在EXT3投入运转以后,内核挂载根文件系统并做好业务(business)上的准备:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).
INIT: Version 2.85 Booting
init是所有LUNIX进程的父进程,是内核在完成自身引导序列后运行的第一个程序。在init/main.c的最后几行,内核在尝试定位init时会找到几个不同的位置:
if (ramdisk_execute_command) { /* Look for /init in initramfs */
run_init_process(ramdisk_execute_command);
}
if (execute_command) { /* You may override init and ask the kernel
to execute a custom program using the
"init=" kernel command-line argument. If
you do that, execute_command points to the
specified program */
run_init_process(execute_command);
}
/* Else search for init or sh in the usual places .. */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
Init 从 /etc/inittab收到一个目录。它一开始在/etc/rc.sysinit中运行系统初始化脚本。这个脚本一个重要的责任就是激活交换分区,该分区可以发出一个这样的引导消息:
Adding 1552384k swap on /dev/hda6
我们更进一步看看它的意义。LINUX用户进程拥有一个3GB的虚拟地址空间(查看"Allocating Memory"一节)。除了这个,构成”工作集”的页被保存在RAM中。尽管有太多的程序会请求内存资源,内核会通过把它们存储在一个叫做交换空间的磁盘分区来释放许多使用过的RAM页。根据经验,交换分区的大小应该为RAM的两倍。因此,交换空间存在于磁盘分区/dev/hda6,大小为。
Init继续运行/etc/rc.d/rcX.d/下的脚本, X是inittab中的runlevel。runlevel是所期望的引导模式的运行状态。例如,多用户文本模式的runlevel 为3,而X Windows的为5. 所以,如果你看到消息,INIT: Entering runlevel 3,说明init 已经在/etc/rc.d/rc3.d/目录开始运行脚本。这些脚本开始动态设备命名子系统udev (我们将在第4章"Laying the Groundwork"中讨论) 并且加载网络、音频、存储设备等驱动所对应的内核模块:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
Init最后发起虚拟控制终端,你现在可以登陆了。