本文档是《Linux2.4 内核说明文档》中的第一部分。以下是整个文档大致目录:
1,启动
2,进程和中断管理
3,虚拟文件系统
4,Linux 页缓冲
5,IPC机制
本篇文档目录:
1.1. 创建Linux内核镜像
1.2. 引导:概述
1.3. 引导:BOIS POST
1.4. 引导:bootsector和setup
1.5. 采用LILO引导器
1.6. 高级初始化
1.7. SMP机在x86系统上引导
1.8. 释放初始化数据和代码
1.9. 处理内核命令
以下是正文:
1. 引导
1.1. 创建Linux内核镜像
本部分解释了编译内核时每个步骤以及每个步骤的输出。这个创建过程依赖于不同的体系结构,这里强调一下我们仅考虑创建一个Linux x86的内核。当用户输入“make zImage”或者“make bzImage”时,输出的可启动内核镜像就分别存放为arch/i386/boot/zImage或者arch/i386/boot/bzImage。下面来看看这个镜像是怎么创建的:
1) 首先C和汇编源文件被编译成ELF中间文件(.o),其中一部分按照逻辑分组打包成压缩文件(.a)。
2) 调用ld指令将以上的.o和.a文件被链接成一个静态的80386可执行文件vmlinux。
3) 接着调用nm vmlinux 指令剔除不相关和不感兴趣的符号并创建系统关系图。
4) 进入arch/i386/boot目录。
5) Bootsect.S文件按照目标是bzImage(zImage)在定义(不定义) –D_BIG_KERBEL_ 宏 下进行预处理,结果分别存为bbootsect.s(bootsect.s)。
6) Bbootsect.s文件被编译并转换成“raw binary”格式的bbootsect文件(bootsect.s 被转换成“raw”格式文件bootsect)。
7) setup.s(setup.s 包含了video.s文件)被预处理成bzImage需要的bsetup.s或者zImage需要的setup.s文件。这个过程和bootsector一样,bzImage镜像需要定义.D__BIG_KERNEL__宏,结果被转换成“raw binary”格式的bsetup,zImage镜像则被转换成“raw”格式的setup。
8) 进入arch/i386/boot/compressed目录,移出/usr/src/linux/vmlinux文件中ELF标识节.note和.comment ,并将其转换成raw binary格式存放到临时文件$tmppiggy。
9) 将$tmppiggy压缩成$tmppiggy.gz
10) .将$tmppiggy.gz链接成重定向文件piggy.o
11) 编译压缩程序head.S 和misc.c文件
12) 将head.o、misc.o和piggy.o链接成bvmlinux(或者vmlinux),注意vmlinux的标号.Ttext 0x1000和bvmlinux的标号.Ttext 0x100000的不同,这是由于bzImage压缩装载器是从高位装载的。
13) 将bvmlinux转换成“raw binary”文件bvmlinux.out,移出ELF节.note 和.comment。
14) 回到arch/i386/boot目录并调用tools/build将bbootsect、bsetup和压缩后的bvmlinux链接成bzImage。这个过程将向bootsector末尾添加重要的变量例如setup_sects和root_dev。
bootsector的大小总是512字节,setup的大小必须大于4个分节且受限于12K,这个规则如下:
0x4000 bytes >= 512 + setup_sects*512 + 运行bootsector/setup所需堆栈空间
在后面将说明是那个部分造成了这种限制。
BzImage文件大小的上限采用LILO启动时为2.5M,采用冷启动如软盘或者光盘等则为1048560字节。
注意,tools/build工具检验了boot段的大小、内核镜像的大小和setup的低范围地址,并没有检测setup的高范围地址。因此,在setup.S文件末尾的“.space”节增加一个大的地址数值就会很容易创建一个无法使用的内核。
1.2. 引导:概述
启动过程是和体系结构相关的,这里我们仅关注IBM PC/IA32体系。由于旧有设计以及向前兼容,PC机采用了以前流行的风格启动操作系统。这个过程可以被分为一下六个逻辑步骤:
1) BOIS选择启动设备。
2) 从启动设备装载bootsector。
3) Bootsector装载setup,解压缩程序和内核镜像。
4) 在保护模式下解压内核。
5) 汇编代码执行低级初始化。
6) 执行上层初始化。
1.3. 引导:BOIS POST
1) 电源启动时钟发生器并在总线上产生一个#POWERGOOD的中断。
2) 产生CPU的RESET中断(此时CPU处于8086工作模式)。
3) %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (ROM BIOS POST code).
4) 在中断无效状态下执行所有POST检查。
5) 在地址0初始化中断向量表IVT。
6) 0x19中断以启动设备号为参数调用BIOS启动装载程序。这个程序从0扇面1扇区物理位置0x7C00开始装载。
1.4. 引导:bootsector和setup
用来启动内核的bootsector可以是以下几种:
Linux bootsector
LILO等bootsector
无bootsector
以下详细解释linux bootsector。下面一些代码,负责初始化用作段变量的宏定义。
29 SETUPSECS = 4 /* default nr of setup.sectors */
30 BOOTSEG = 0x07C0 /* original address of boot.sector */
31 INITSEG = DEF_INITSEG /* we move boot here . out of the way */
32 SETUPSEG = DEF_SETUPSEG /* setup starts here */
33 SYSSEG = DEF_SYSSEG /* system loaded at 0x10000 (65536) */
34 SYSSIZE = DEF_SYSSIZE /* system size: # of 16.byte clicks */
(左边的数值是代码的行号) 在文件include/asm/boot.h中定义了DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG和DEF_SYSSIZE的值。
/* Don't touch these, unless you really know what you're doing. */
#define DEF_INITSEG 0x9000
#define DEF_SYSSEG 0x1000
#define DEF_SETUPSEG 0x9020
#define DEF_SYSSIZE 0x7F00
以下来看看bootsect.S的源代码:
54 movw $BOOTSEG, %ax
55 movw %ax, %ds
56 movw $INITSEG, %ax
57 movw %ax, %es
58 movw $256, %cx
59 subw %si, %si
60 subw %di, %di
61 cld
62 rep
63 movsw
64 ljmp $INITSEG, $go
65 # bde . changed 0xff00 to 0x4000 to use debugger at 0x6400 up (bde). We
66 # wouldn't have to worry about this if we checked the top of memory. Also
67 # my BIOS can be configured to put the wini drive tables in high memory
68 # instead of in the vector table. The old stack might have clobbered the
69 # drive table.
70 go: movw $0x4000.12, %di # 0x4000 is an arbitrary value >=
71 # length of bootsect + length of
72 # setup + room for stack;
73 # 12 is disk parm size.
74 movw %ax, %ds # ax and es already contain INITSEG
75 movw %ax, %ss
76 movw %di, %sp # put stack at INITSEG:0x4000.12.
代码54行~63行将bootsector从地址0x7C00移动到0x90000,由以下过程完成:
1) set %ds:%si to $BOOTSEG:0 (0x7C0:0 = 0x7C00)
2) set %es:%di to $INITSEG:0 (0x9000:0 = 0x90000)
3) set the number of 16bit words in %cx (256 words = 512 bytes = 1 sector)
4) clear DF (direction) flag in EFLAGS to auto.increment addresses (cld)
5) go ahead and copy 512 bytes (rep movsw)
The reason this code does not use rep movsd is intentional (hint . .code16).
64行跳转到标号go:(一个最新创建的bootsector的拷贝,也就是在0x9000段)。这和接下来的三段指令(64~76行)在$INITSEG:0x4000.0xC 段初始化一个堆栈,也就是指令%ss = $INITSEG (0x9000) 和 %sp = 0x3FF4 (0x4000.0xC)。这就是我们前面提到的setup大小限制的来历(见1.1节)。
77~103行代码建立了第一个磁盘的参数表,以便允许多扇区读操作。
77 # Many BIOS's default disk parameter tables will not recognise
78 # multi.sector reads beyond the maximum sector number specified
79 # in the default diskette parameter tables . this may mean 7
80 # sectors in some cases.
81 #
82 # Since single sector reads are slow and out of the question,
83 # we must take care of this by creating new parameter tables
84 # (for the first disk) in RAM. We will set the maximum sector
85 # count to 36 . the most we will encounter on an ED 2.88.
86 #
87 # High doesn't hurt. Low does.
88 #
89 # Segments are as follows: ds = es = ss = cs . INITSEG, fs = 0,
90 # and gs is unused.
91 movw %cx, %fs # set fs to 0
92 movw $0x78, %bx # fs:bx is parameter table address
93 pushw %ds
94 ldsw %fs:(%bx), %si # ds:si is source
95 movb $6, %cl # copy 12 bytes
96 pushw %di # di = 0x4000.12.
97 rep # don't need cld .> done on line 66
98 movsw
99 popw %di
100 popw %ds
101 movb $36, 0x4(%di) # patch sector count
102 movw %di, %fs:(%bx)
103 movw %es, %fs:2(%bx)
通过0x13BOIS服务0号函数重置软盘管理器,并且在bootsector完成后立即载入setup部分。也就是说在物理地址0x90200 ($INITSEG:0x200)处再次调用0x13BOIS服务2号函数。这个过程发生在107~124行。
107 load_setup:
108 xorb %ah, %ah # reset FDC
109 xorb %dl, %dl
110 int $0x13
111 xorw %dx, %dx # drive 0, head 0
112 movb $0x02, %cl # sector 2, track 0
113 movw $0x0200, %bx # address = 512, in INITSEG
114 movb $0x02, %ah # service 2, "read sector(s)"
115 movb setup_sects, %al # (assume all on head 0, track 0)
116 int $0x13 # read it
117 jnc ok_load_setup # ok . continue
118 pushw %ax # dump error code
119 call print_nl
120 movw %sp, %bp
121 call print_hex
122 popw %ax
123 jmp load_setup
124 ok_load_setup:
如果由于某些原因出错,例如无法使用的软盘或者在运行过程中有人弹出了磁盘等,装载过程将输出错误代码并且无限循环尝试本过程。除非重试成功(这通常不会发生,如果出现其他错误后果将更严重),唯一退出这个循环的办法就是重新启动机器。
如果成功装载配置代码部分,流程将跳转到ok_load_setup标签。
紧接着,启动程序就在物理地址0x10000装载压缩后的内核。这样做是为了保护低位(0~64K)内存的固件数据区。在内核装载后,启动程序跳转到地址$SETUPSEG:0。一旦这些固件数据不再需要的时候,它们会被从0x10000移动到0x1000地址的完整内核镜像覆盖。这个过程由setup.S完成,它主要设置保护模式下的状态,并跳转到压缩内核的起始物理地址0x1000,也就是arch/386/boot/compressed/{head.S,misc.c}文件。它设置堆栈,调用decompress_kernel()解压缩内核到0x100000并跳转到该地址。
注意以前的装载器(如老版本的LILO)仅能装载setup的前4节,这就是setup里面有必要时候装载自身重置的代码的原因。同样,setup代码必须注意多种不同类型版本的装载器与zImage/bzImage的结合,它也因此非常复杂。
让我们分析一下bootsector代码里允许装载大内核(即bzImage)的组装部分(kludge)。首先setup部分像往常一样装载到地址0x90200,但是采用调用BIOS服务将数据从低位内存移动到高位内存的辅助程序,内核一次可装载64K。这个辅助程序在bootsect.S中的bootsect_kludge曾提到,并在setup.S中定义为bootsect_helper。Setup.S中的bootsect_kludge标签段包含了setup段的代码以及其中bootsect_helper代码的偏移量,这样bootsector可以调用lcall指令跳转到bootsect_helper。bootsect_helper包含在setup.S文件里的原因很简单,因为bootsect.S没有剩余的空间了。这个程序调用0x15号BIOS服务以便移动到高位内存并复位%es,使其总是指向0x10000。这保证了bootsect.S里的代码在从磁盘拷贝数据时不会溢出。
1.5. 采用LILO引导器
在原始的linux bootsector上采用专门的引导器(LILO)有很多好处:
1) 可在多个操作系统或多个内核之间选择。
2) 可以输入内核指令参数。
3) 可以装载更大(2.5M甚至1M)的内核文件。
老版本的LILO不能装载bzImage内核,新版本采用了bootsect+setup这样的技术,通过BIOS服务将数据从低位内存拷贝到高位。一些人在为是否移出对zImage支持而争论,这个主要原因是在已损坏的BIOS服务时,zImage可以正常装载而bzImage却不能。
LILO所做的最后一件事情就是跳转到setup.S,然后就是正常的处理过程。
1.6. 高级初始化
对于“高级初始化”我们认为这不是直接和引导过程相关,即使它的部分实现代码也是用汇编语言编写的。也就是arch/i386/kernel/head.S文件,它是未压缩内核的最初部分。整个过程如以下部分:
1) 初始化段数值(%ds = %es = %fs = %gs = __KERNEL_DS = 0x18)。
2) 初始化内存页表。
3) 设置%cr0的PG位,使内存分页机制有效。
4) 将BSS清零(在SMP机上,仅第一个CPU会执行此操作)。
5) 拷贝内核引导指令的前2k。
6) 利用EFLAGS检测CPU类型,如果可能,还有cpuid,以便探测386或者更高型号。
7) 第一个CPU调用start_kernel函数,如果ready等于1,其他CPU则调用arch/i386/kernel/smpboot.c文件的:initialize_secondary()函数,这个函数重新装载esp/eip且不再返回。
init/main.c文件的start_kernel函数是用c语言编写的,并完成以下工作:
1) 获得一个全局的内核锁(这在单CPU完成初始化的时候需要)。
2) 执行细节配置(内存页布局分析,再次拷贝引导命令等)。
3) 输出带有版本号的内核标语,编译器通常将这些创建到内核保存消息的ring缓冲中。版本号来自于init/version.c的linux_banner变量,在使用cat /proc/version时也会显示同样的字符串。
4) 初始化trap。
5) 初始化IRQ。
6) 初始化调度所需数据。
7) 初始化时钟数据。
8) 初始化软中断子系统。
9) 处理引导命令。
10) 初始化控制台。
11) 如果模块支持功能被编译到内核,初始化动态模块装载器。
12) 如果支持“profile=”命令,初始化配置文件缓冲区。
13) 调用kmem_cache_init()函数,初始化大多数的块分配算法。
14) 中断有效。
15) 为当前CPU计算BogoMips值。
16) 调用mem_init()函数计算max_mapnr、totalram_pages和high_memory,并输出“Memory……”。
17) 调用kmem_cache_sizes_init()函数完成块分配算法初始化。
18) 初始化procfs使用的数据结构。
19) 调用for_init(),创建uid_cache进程,基于内存可用总量初始化max_threads,并将init_task 结构的RLIMIT_NPROC设置为max_threads的1/2。
20) 创建VFS、VM、高速缓冲等所需的多种块缓冲器。
21) 如果IPC支持功能被编译到内核,初始化IPC子系统,注意系统V的shm,它包含了挂载一个shmfs文件系统的内部实例。
22) 如果配额功能被编译到内核,则为其创建并初始化一个特殊的块缓冲。
23) 执行bug检查,可能在任何时候激活processor/bus/etc 工作区下的bugs,比较各种结构以发现“ia64没有bug”或者“ia32有相当多的bug”。一个较好的例子就是“f00f bug”,它仅在内核编译为686以前版本并再此环境下工作时产生。
24) 设置一个标记,标识调度表在“next opportunity”时调用,如果提供了“init=”引导命令,则执行execute_command函数以创建一个内核线程init,否则尝试执行/sbin/init, /etc/init, /bin/init, /bin/sh, 如果所有的过程都失败了,则提示使用“init=”参数。
25) .进入到idle循环,这是一个pid为0的idle进程。
注意,init内核进程调用了do_basic_setup 函数,这个函数依次调用do_initcalls函数遍历由_initcall或者module_init宏注册的函数列表,并调用他们。这些函数不是相互倚赖,就是他们的依赖关系已经在Makefile文件里面固定了。这意味着依靠目录树的位置和Makefile的结构,调用初始化函数的指令是可以更改的。有时,这很重要。因为你可以想像两个子系统A和B,其中B依赖于A的初始化晚完成。如果A被静态编译,并且B是一个模块,则在A准备号所有必须环境后调用B实体的入口;如果A是一个模块,B也同样必须是一个模块,这样才没有问题。但如果A和B都被静态编译到内核时呢?调用他们的指令依赖于内核的.initcall.init节的相对入口偏移量。Rogier Wolff 提议引进一种分等级(“优先权”)的基础下部组织,模块通过它让连接器知道在何种关联指令下这些模块可以被链接,但迄今为止都没有一个内核可接收的具备一流风格的可用的实现补丁。因此,确定你的连接指令是正确的。如果像上面的例子,倘若在同一个Makefile中相继列出A和B,一旦在静态编译时他们能够正常工作,他们将会一直如此。如果他们不能工作,更改列举有他们目标文件的指令。
另外一件事是linux的通过“init=”引导命令的含义执行多选择性的初始化程序的能力。这个在意外覆盖/sbin/init或者手工调试初始化脚本和/etc/inittab,并在某时执行其中一个时非常有用。
1.7. SMP机在x86系统上引导
SMP机的引导过程在到达start_kernel入口前和普通顺序一样执行bootsector,setup等等,然后调用smp_init和smp_boot_cpus(位于src/i386/kernel/smpboot.c)函数。smp_boot_cpus函数依次遍历每个apicid直到NR_CPUS数,并针对每个apicid调用do_boot_cpu函数。do_boot_cpu函数的功能就是为目标CPU创建一个空闲任务并记录到一个众所周知的位置,该位置由trampoline.S文件提供的Intel MP规范EIP代码定义;然后,它为该CPU产生让引导程序执行trampoline.S 代码的STARTUP IPI。
引导CPU在低位内存为每个CPU创建一份trampoline代码的拷贝,这个启动程序在自身写入一个可供引导程序判断是否正在执行trampoline代码的随机数。Intel MP 标准限定了trampoline代码必须在低位内存。Trampoline代码仅仅将%bx登记为1,进入到保护模式,并跳转到arch/i386/kernel/head.S的主入口startup_32。
现在,引导程序开始执行head.S并发觉这并不是引导过程,于是它跳过清楚BSS的代码,进入到当前CPU的空任务的入口函数initialize_secondary()并重新调用引导过程已经初始化成功的init_tasks[cpu]。
注意,虽然init_task可以被共享但是每个空任务都有自己的时间分配系统,这就是为何init_tss[NR_CPUS]是一个队列。
1.8. 释放初始化数据和代码
当操作系统完成对自身的初始化,大多数的代码和数据结构将不再需要。多数的操作系统(BSD,FreeBSD等)不会释放这些不需要的数据,因而浪费了宝贵的物理内核内存。他们的理由是“相关代码在多种子系统中都有关联,所以释放不是切实可行的”。当然,Linux系统不能以这些为托词,因为在linux下,如果某事在理论上是可行的,那么它多半已经被实现了或者某人正在为之而努力。
那么,像我在前面所说的Linux内核仅能被编译成ELF二进制格式,现在我们来找出这个原因(或者是其中之一的原因)。这个原因和linux提供的两个用于清楚初始化代码和数据的宏有关系:
初始化代码的__init宏
初始化数据的__initdata宏
在include/linux/init.h中,gcc特殊分类符定义了他们的含义。
#ifndef MODULE
#define __init __attribute__ ((__section__ (".text.init")))
#define __initdata __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif
这意味着如果代码是静态编译到内核的,那么它就放在执行的ELF节.text 和.init,这两个节都在arch/i386/vmlinux.lds文件的连接关联图中都有定义;否则这两个宏就没有任何含义。
引导时将出现 init内核任务调用free_initmem函数释放从地址__init_begin到地址__init_end的所有页。在一个典型系统上(如工作终端),这个结果将释放260k左右的内存。
静态编译情况时,所有经由module_init注册的函数被放置到同样会被释放的.initcall.init这里。当设计一个子系统时,当今Linux的趋势是从旧有版本提供一个init/exit入口点,这样将来有争议的子系统在需要时可以组件化。以pipefs为例,见fs/pipe.c文件。即使给定的子系统不会是一个模块(如fs/buffer.c 中的bdflush),通过module_init宏它依然正常精简地实现了初始化功能,并在函数正确调用时正常运行。
还有两个宏__exit 和__exitdata也以类似的风格工作,但他们和模块支持有更加直接的关联,因此在稍后将详加讨论。
1.9. 处理内核命令
让我们回想在引导期间向内核传递控制命令会发生什么:
1) LILO通过BIOS键盘服务接收命令,保存到物理内存明显的位置,这就像发出一个信号说这里有一条可用的命令。
2) arch/i386/kernel/head.S拷贝命令的前2k到内存页。
3) arch/i386/kernel/setup.c文件的parse_mem_cmdline函数(start_kernel-> setup_arch-> parse_mem_cmdline)从内存页拷贝256字节到saved_command_line,这将显示到/proc/cmdline。如果出现“mem=”项也以同样流程处理,并为VM参数做适量调整。
4) 我们回到parse_options函数(由start_kernel调用),它处理一些内核参数(当前的“init=”以及init所需的环境变量)并将每个命令提交到checksetup函数。
5) Checksetup函数在.setup.init节中搜索并调用对应的函数,传递命令。注意经由__setup注册的函数返回值0的使用,这有可能出现提交“变量=值”命令,而不是一个函数在“值”时无效另一个无效。Jeff Garzik评论说“这样做的黑客在快马加鞭”为什么?因为这明显就是ld旧有的细节。也就是说一个命令连接内核时让函数A先于B调用,而另一个则是相反的命令,结果就依赖于这条命令。
那么怎样写处理引导命令的程序呢?我们使用定义在include/linux/init.h里的__setup()宏:
/*
* Used for kernel command line parameter setup
*/
struct kernel_param {
const char *str;
int (*setup_func)(char *);
};
extern struct kernel_param __setup_start, __setup_end;
#ifndef MODULE
#define __setup(str, fn) \
static char __setup_str_##fn[] __initdata = str; \
static struct kernel_param __setup_##fn __initsetup = \
{ __setup_str_##fn, fn }
#else
#define __setup(str,func) /* nothing */
endif
你可以在自己的代码里仿效这个做法(下面是逻辑总线HBA驱动的示例)。
static int __init
BusLogic_Setup(char *str)
{
int ints[3];
(void)get_options(str, ARRAY_SIZE(ints), ints);
if (ints[0] != 0) {
BusLogic_Error("BusLogic: Obsolete Command Line Entry " "Format Ignored\n", NULL);
return 0;
}
if (str == NULL || *str == '\0')
return 0;
return BusLogic_ParseDriverOptions(str);
}
__setup("BusLogic=", BusLogic_Setup);
注意__setup()不会对模块进行任何操作,所以这段代码希望处理引导命令并在模块初始化过程中,模块化连接或者静态连接时都能调用自身的处理函数。这也同样意味着可以编译一个仅处理以模块化编译的内核的参数,而不处理静态编译或其他情况。
阅读(2385) | 评论(0) | 转发(0) |