现在的位置: 首页 > 综合 > 正文

《Essential Linux Device Drivers》linux系统的启动

2013年01月23日 ⁄ 综合 ⁄ 共 6536字 ⁄ 字号 评论关闭

《Essential Linux Device Drivers》翻译笔记002

by muge0913

整装待发

Linux 已经跋涉了许多的地域,成为一门艺术。所以你可以基于它来学习操作系统的概念、处理器体系结构甚至工业领域。当你学习某一设备驱动子系统所使用的技术时,可以更深入地探索其背后潜在的设计由来。

在没有明确说明的情况下,书中都假定为 32 位 x86 体系结构。但是,本书也考虑到了一个事实:你更有可能为嵌入式设备而非传统的 PC 兼容的系统编写驱动程序。因此,对于串口驱动一章,讲解了 2 个设备:一个 PC 衍生器件上的触摸控制器和一个手机上的 UART 。对于 I2C 设备驱动程序一章,则讲解了 PC 系统中的 EEPROM 和嵌入式设备中的实时钟。本书也将介绍内核为大多数设备驱动类所提供的核心基础设施,它们隐藏了设备驱动程序的体系结构相关性。

本书基于 2.6 内核,它包含了对 2.4 内核的大量改变,覆盖了所有的主要的子系统。希望你已经在你的系统中安装了基于 2.6 的 Linux 。

(1) 因为内核中的每个驱动子系统包含成千上万行的源代码,所以本书只可能进行一个相对简单的呈现,查看源代码中与书中例子相关的真实的驱动将为你提供更广阔的视角。

(2) 在开发一个驱动之前,参考 drivers/ 目录中现存的、与你的要求相似的驱动并以之作为起点是一个好主意。

 

 

启动过程

图 2.1 显示了基于 x86 计算机 Linux 系统的启动顺序。第一步是 BIOS 从启动设备中导入主引导记录( MBR ),接下来 MBR 中的代码查看分区表并从活动分区读取 GRUB 、 LILO 或 SYSLINUX 等 bootloader ,之后 bootloader 会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会将自身解压缩并投入运转。



基于 x86 的处理器有两种操作模式:实模式和保护模式。在实模式下,用户仅可以使用 1MB 内存,并且没有任何保护。保护模式则更加复杂,用户可以使用更多的高级功能(如分页)。 CPU 提供了一条由实模式通向保护模式的道路,但是,这条路只允许单向行驶,用户不能从保护模式再切换回实模式。

内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下 init/main.c 文件中的 start_kernel() 函数。 start_kernel() 函数首先会初始化 CPU 子系统,之后让内存管理和进程管理系统就位,接下来启动外部总线和 I/O 设备,最后的一步是激活所有 Linux 进程的父亲 init 。 init 执行用户空间的脚本以启动必要的内核服务,它最终派生控制台终端程序并显示登录(
login )提示。

接下来,每一小节的标题都是图 2.2 中的一条打印信息,这些信息来源于基于 x86 的笔记本电脑的 Linux 启动过程

图BIOS-providedphysical RAM map

内核解析从 BIOS 中读取到的系统内存映射,并率先将这些信息打印出来:       

BIOS-provided physical RAMmap:

BIOS-e820: 0000000000000000 -000000000009f000 (usable)

...

BIOS-e820: 00000000ff800000 -0000000100000000 (reserved)

实模式下的初始化代码通过使用 BIOS 的 int 0x15 服务并执行 0xe820 号函数来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将使用这些信息创建其可用的内存池。。

758MB LOWMEM Available

896MB 以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数kmalloc() 就是从该区域分配内存的。高于 896MB 被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。在启动过程中,内核会计算并显示这些内存 zone 内总的页数,在本章的稍后,会对这些内存 zone 进行更深入的分析。

Kernel CommandLine: 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 kernel2.6.23 ”后的一行之后,你会发现前述打印信息的由来。 /

[1] 嵌入式设备上的 bootloader 通常经过了“瘦身”,并不支持配置文件或类似机制。归因于此,许多非 x86 体系结构提供了 CONFIG_CMDLINE 这个内核配置选项,通过它,用户可以在编译内核时提供内核命令行。

命令行参数将影响启动过程中的代码执行路径。举一个例子,假设某命令行参数为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 runlevelof 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.46BogoMIPS (lpj=2394935)


Checking HLT Instruction

由于 Linux 内核支持多种硬件平台,启动代码会检查体系结构相关的 bug 。其中一项工作就是验证停机( HLT )指令。

当 init/main.c 中的启动代码调用 include/asm-your-arch/bugs.h中定义的 check_bugs() 时,会打印上述信息。

NET: Registered ProtocolFamily 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 InitrdMemory: 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 只是页缓冲之上的一小层,因此它的代码量很小。

在本例中,我们使用的是通过 initrd= 命令行参数向内核传递初始根文件系统cpio 压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为 387K )并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。

在第 18 章中我们会发现,在嵌入式系统开发过程中, initrd 和 initramfs 有时候也可被用作嵌入式设备上实际的根文件系统。

IO SchedulerAnticipatory Registered (Default)

I/O 调度器的主要目标是通过减少磁盘的定位次数以增加系统的吞吐率。在磁盘定位过程中,磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的延迟。 2.6 内核提供了 4 种不同的 I/O 调度器: Deadline 、 Anticipatory 、 Complete Fair Queuing 以及
NOOP 。 从上述内核打印信息可以看出,本例将 Anticipatory 设置为了缺省的 I/O 调度器。在第 14 章《块设备驱动》中,我们将学习 I/O 调度的知识。

Setting Up Standard PCIResources

启动过程的下一阶段会初始化 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 )。


EXT3-fs: Mounted Filesystem

EXT3 文件系统已经成为 Linux 事实上的文件系统。 EXT3 在退役的 EXT2 文件系统基础上增添了日志层,该层可用于崩溃后文件系统的快速恢复。它的目标是不经由耗时的文件系统检查( fsck )操作即可获得一个一致的文件系统。 EXT2 仍然是新文件系统的工作引擎,但是 EXT3 层会在进行实际的磁盘改变之前记录文件交互的日志。 EXT3 向后兼容于 EXT2 ,因此,你可以在你现存的 EXT2
文件系统上批上 EXT3 的大衣或者脱去 EXT3 的大衣以回归到 EXT2 文件系统。


EXT3 会启动一个称为 kjournald 的内核辅助线程(在接下来的一章中将深入讨论内核线程)来完成日志功能。在 EXT3 投入运转以后,内核挂载根文件系统并做好 “业务”上的 准备:

EXT3-fs: mounted filesystemwith ordered data mode

kjournald starting. Commitinterval 5 seconds

VFS: Mounted root (ext3filesystem).

INIT: Version 2.85 Booting

所有 Linux 进程的父进程 init 是内核完成启动序列后运行的第 1 个程序。在init/main.c 的最后几行,内核会搜索一个不同的位置以定位到 init :

if (ramdisk_execute_command){ /* Look for /init in initramfs */

 run_init_process(ramdisk_execute_command);

}

 

if (execute_command) { /* Youmay 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 shin 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 中的系统初始化脚本,该脚本的一项最重要的职责就是激活交换( swap )分区,这会导致如下启动信息被打印:

Adding 1552384k swap on/dev/hda6

让我们来仔细看看上述这段话的意思。 Linux 用户进程拥有 3GB 的虚拟地址空间(见《内存分配》一节),构成“工作集”的页被保存在 RAM 中。但是,如果有太多程序需要内存资源,内核会释放一些被使用了的 RAM 页面并将其存储到称为交换空间( swap space )的磁盘分区中。根据经验法则,交换分区的大小应该是 RAM 的 2 倍。在本例中,交换空间位于 /dev/hda6
这个磁盘分区,其大小为 1552384K 字节。

接下来, init 开始运行/etc/rc.d/rcX.d/ 目录中的脚本, X 是 inittab 中定义的运行级别。 Runlevel 是根据预期的工作模式所进入的执行状态。例如,多用户文本模式意味着 runlevel 为 3 , X Windows 则意味着 runlevel 为 5 。因此,当你看到“ INIT: Enteringrunlevel 3” 这条信息的时候,
init 就已经开始执行 /etc/rc.d/rc3.d/ 目录中的脚本了。这些脚本会启动动态设备命名子系统(第 4 章《打下基础》中将讨论 udev ),并加载网络、音频、存储设备等驱动所对应的内核模块:

Starting udev: [ OK ]

Initializing hardware...network audio storage [Done]

...

最后, init 发起虚拟控制台终端 ,你现在就可以登录了。

抱歉!评论已关闭.