F1 FreeBSD
F2 BSD
F5 Disk 2
第 1 章 引导过程与内核初始化
This translation may be out of date. To help with the translations please access the FreeBSD translations instance.
Table of Contents
1.1. 概述
这一章是对引导过程和系统初始化过程的总览。这些过程始于BIOS(固件)POST, 直到第一个用户进程建立。由于系统启动的最初步骤是与硬件结构相关的、是紧配合的, 这里用IA-32(Intel Architecture 32bit)结构作为例子。
1.2. 总览
一台运行FreeBSD的计算机有多种引导方法。这里讨论其中最通常的方法, 也就是从安装了操作系统的硬盘上引导。引导过程分几步完成:
BIOS POST
boot0
阶段boot2
阶段loader阶段
内核初始化
boot0
和boot2
阶段在手册 boot(8)中被称为bootstrap stages 1 and 2, 是FreeBSD的3阶段引导过程的开始。在每一阶段都有各种各样的信息显示在屏幕上, 你可以参考下表识别出这些步骤。请注意实际的显示内容可能随机器的不同而有一些区别:
视不同机器而定 | BIOS(固件)消息 |
| |
|
|
| loader |
| 内核 |
1.3. BIOS POST
当PC加电后,处理器的寄存器被设为某些特定值。在这些寄存器中, 指令指针寄存器被设为32位值0xfffffff0。 指令指针寄存器指向处理器将要执行的指令代码。cr1
, 一个32位控制寄存器,在刚启动时值被设为0。cr1的PE(Protected Enabled, 保护模式使能)位用来指示处理器是处于保护模式还是实地址模式。 由于启动时该位被清位,处理器在实地址模式中引导。在实地址模式中, 线性地址与物理地址是等同的。
值0xfffffff0略小于4G,因此计算机没有4G字节物理内存, 这就不会是一个有效的内存地址。计算机硬件将这个地址转指向BIOS存储块。
BIOS表示Basic Input Output System (基本输入输出系统)。在主板上,它被固化在一个相对容量较小的 只读存储器(Read-Only Memory, ROM)。BIOS包含各种各样为主板硬件 定制的底层例程。就这样,处理器首先指向常驻BIOS存储器的地址 0xfffffff0。通常这个位置包含一条跳转指令,指向BIOS的POST例程。
POST表示Power On Self Test(加电自检)。 这套程序包括内存检查,系统总线检查和其它底层工具, 从而使得CPU能够初始化整台计算机。这一阶段中有一个重要步骤, 就是确定引导设备。现在所有的BIOS都允许手工选择引导设备。 你可以从软盘、光盘驱动器、硬盘等设备引导。
POST的最后一步是执行INT 0x19
指令。 这个指令从引导设备第一个扇区读取512字节,装入地址0x7c00。 第一个扇区的说法最早起源于硬盘的结构, 硬盘面被分为若干圆柱形轨道。给轨道编号,同时又将轨道分为 一定数目(通常是64)的扇形。0号轨道是硬盘的最外圈,1号扇区, 第一个扇区(轨道、柱面都从0开始编号,而扇区从1开始编号) 有着特殊的作用,它又被称为主引导记录(Master Boot Record, MBR)。 第一轨剩余的扇区常常不使用。
1.4. boot0
阶段
让我们看一下文件/boot/boot0。 这是一个仅512字节的小文件。如果在FreeBSD安装过程中选择 "bootmanager",这个文件中的内容将被写入硬盘MBR
如前所述, INT 0x19
指令装载 MBR, 也就是 boot0 的内容至内存地址 0x7c00。 再看文件 sys/boot/i386/boot0/boot0.S, 可以猜想这里面发生了什么 - 这是引导管理器, 一段由 Robert Nordier书写的令人起敬的程序片段。
MBR里,也就是boot0里, 从偏移量0x1be开始有一个特殊的结构,称为 分区表。其中有4条记录 (称为分区记录),每条记录16字节。 分区记录表示硬盘如何被划分,在FreeBSD的术语中, 这被称为slice(d)。16字节中有一个标志字节决定这个分区是否可引导。 有仅只能有一个分区可设定这一标志。否则, boot0的代码将拒绝继续执行。
一个分区记录有如下域:
1字节 文件系统类型
1字节 可引导标志
6字节 CHS格式描述符
8字节 LBA格式描述符
一个分区记录描述符包含某一分区在硬盘上的确切位置信息。 LBA和CHS两种描述符指示相同的信息,但是指示方式有所不同:LBA (逻辑块寻址,Logical Block Addressing)指示分区的起始扇区和分区长度, 而CHS(柱面 磁头 扇区)指示首扇区和末扇区
引导管理器扫描分区表,并在屏幕上显示菜单,以便用户可以 选择用于引导的磁盘和分区。在键盘上按下相应的键后, boot0进行如下动作:
标记选中的分区为可引导,清除以前的可引导标志
记住本次选择的分区以备下次引导时作为缺省项
装载选中分区的第一个扇区,并跳转执行之
什么数据会存在于一个可引导扇区(这里指FreeBSD扇区)的第一扇区里呢? 正如你已经猜到的,那就是boot2。
1.5. boot2
阶段
也许你想知道,为什么boot2
是在 boot0
之后,而不是在boot1之后。事实上, 也有一个512字节的文件boot1存放在目录 /boot里,那是用来从一张软盘引导系统的。 从软盘引导时,boot1起着 boot0对硬盘引导相同的作用:它找到 boot2并运行之。
你可能已经看到有一文件/boot/mbr。 这是boot0的简化版本。 mbr中的代码不会显示菜单让用户选择, 而只是简单的引导被标志的分区。
实现boot2的代码存放在目录 sys/boot/i386/boot2/里,对应的可执行文件在 /boot里。在/boot里的文件 boot0和boot2不会在引导过程中使用, 只有boot0cfg这样的工具才会使用它们。 boot0的内容应在MBR中才能生效。 boot2位于可引导的FreeBSD分区的开始。 这些位置不受文件系统控制,所以它们不可用ls 之类的命令查看。
boot2
的主要任务是装载文件 /boot/loader,那是引导过程的第三阶段。 在boot2
中的代码不能使用诸如 open()
和read()
之类的例程函数,因为内核还没有被加载。而应当扫描硬盘, 读取文件系统结构,找到文件/boot/loader, 用BIOS的功能将它读入内存,然后从其入口点开始执行之。
除此之外,boot2
还可提示用户进行选择, loader可以从其它磁盘、系统单元、分区装载。
boot2
的二进制代码用特殊的方式产生:
sys/boot/i386/boot2/Makefile boot2: boot2.ldr boot2.bin ${BTX}/btx/btx btxld -v -E ${ORG2} -f bin -b ${BTX}/btx/btx -l boot2.ldr \ -o boot2.ld -P 1 boot2.bin
这个Makefile片断表明btxld(8)被用来链接二进制代码。 BTX表示引导扩展器(BooT eXtender)是给程序(称为客户(client) 提供保护模式环境、并与客户程序相链接的一段代码。所以 boot2
是一个BTX客户,使用BTX提供的服务。
工具btxld是链接器, 它将两个二进制代码链接在一起。btxld(8)和ld(1) 的区别是ld通常将两个目标文件 链接成一个动态链接库或可执行文件,而btxld 则将一个目标文件与BTX链接起来,产生适合于放在分区首部的二进制代码, 以实现系统引导。
boot0
执行跳转至BTX的入口点。 然后,BTX将处理器切换至保护模式,并准备一个简单的环境, 然后调用客户。这个环境包括:
虚拟8086模式。这意味着BTX是虚拟8086的监视程序。 实模式指令,如pushf, popf, cli, sti, if,均可被客户调用。
建立中断描述符表(Interrupt Descriptor Table, IDT), 使得所有的硬件中断可被缺省的BIOS程序处理。 建立中断0x30,这是系统调用关口。
两个系统调用
exec
和exit
的定义如下:sys/boot/i386/btx/lib/btxsys.s: .set INT_SYS,0x30 # 中断号 # # System call: exit # __exit: xorl %eax,%eax # BTX系统调用0x0 int $INT_SYS # # # System call: exec # __exec: movl $0x1,%eax # BTX系统调用0x1 int $INT_SYS #
BTX建立全局描述符表(Global Descriptor Table, GDT):
sys/boot/i386/btx/btx/btx.s: gdt: .word 0x0,0x0,0x0,0x0 # 以空为入口 .word 0xffff,0x0,0x9a00,0xcf # SEL_SCODE .word 0xffff,0x0,0x9200,0xcf # SEL_SDATA .word 0xffff,0x0,0x9a00,0x0 # SEL_RCODE .word 0xffff,0x0,0x9200,0x0 # SEL_RDATA .word 0xffff,MEM_USR,0xfa00,0xcf# SEL_UCODE .word 0xffff,MEM_USR,0xf200,0xcf# SEL_UDATA .word _TSSLM,MEM_TSS,0x8900,0x0 # SEL_TSS
客户的代码和数据始于地址MEM_USR(0xa000),选择符(selector) SEL_UCODE指向客户的数据段。选择符 SEL_UCODE 拥有第3级描述符权限 (Descriptor Privilege Level, DPL),这是最低级权限。但是 INT 0x30
指令的处理程序存储于另一个段里, 这个段的选择符SEL_SCODE (supervisor code)由有着管理级权限。 正如代码建立IDT(中断描述符表)时进行的操作那样:
mov $SEL_SCODE,%dh # 段选择符 init.2: shr %bx # 是否处理这个中断? jnc init.3 # 否 mov %ax,(%di) # 设置处理程序偏移量 mov %dh,0x2(%di) # 设置处理程序选择符 mov %dl,0x5(%di) # 设置 P:DPL:type add $0x4,%ax # 下一个中断处理程序
所以,当客户调用 __exec()
时,代码将被以最高权限执行。 这使得内核可以修改保护模式数据结构,如分页表(page tables)、全局描述符表(GDT)、 中断描述符表(IDT)等。
boot2
定义了一个重要的数据结构: struct bootinfo
。这个结构由 boot2
初始化,然后被转送到loader,之后又被转入内核。 这个结构的部分项目由boot2
设定,其余的由loader设定。 这个结构中的信息包括内核文件名、BIOS提供的硬盘柱面/磁头/扇区数目信息、 BIOS提供的引导设备的驱动器编号,可用的物理内存大小,envp
指针(环境指针)等。定义如下:
/usr/include/machine/bootinfo.h struct bootinfo { u_int32_t bi_version; u_int32_t bi_kernelname; /* 用一个字节表示 * */ u_int32_t bi_nfs_diskless; /* struct nfs_diskless * */ /* 以上为常备项 */ #define bi_endcommon bi_n_bios_used u_int32_t bi_n_bios_used; u_int32_t bi_bios_geom[N_BIOS_GEOM]; u_int32_t bi_size; u_int8_t bi_memsizes_valid; u_int8_t bi_bios_dev; /* 引导设备的BIOS单元编号 */ u_int8_t bi_pad[2]; u_int32_t bi_basemem; u_int32_t bi_extmem; u_int32_t bi_symtab; /* struct symtab * */ u_int32_t bi_esymtab; /* struct symtab * */ /* 以下项目仅高级bootloader提供 */ u_int32_t bi_kernend; /* 内核空间末端 */ u_int32_t bi_envp; /* 环境 */ u_int32_t bi_modulep; /* 预装载的模块 */ };
boot2
进入一个循环等待用户输入,然后调用 load()
。如果用户不做任何输入,循环将在一段时间后结束, load()
将会装载缺省文件(/boot/loader)。 函数 ino_t lookup(char *filename)
和 int xfsread(ino_t inode, void *buf, size_t nbyte)
用来将文件内容读入内存。/boot/loader是一个ELF格式二进制文件, 不过它的头部被换成了a.out格式中的struct exec
结构。 load()
扫描loader的ELF头部,装载/boot/loader 至内存,然后跳转至入口执行之:
sys/boot/i386/boot2/boot2.c: __exec((caddr_t)addr, RB_BOOTINFO | (opts RBX_MASK), MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part), 0, 0, 0, VTOP(bootinfo));
1.6. loader阶段
loader也是一个 BTX 客户,在这里不作详述。 已有一部内容全面的手册 loader(8) ,由Mike Smith书写。 比loader更底层的BTX的机理已经在前面讨论过。
loader 的主要任务是引导内核。当内核被装入内存后,即被loader调用:
sys/boot/common/boot.c: /* 从loader中调用内核中对应的exec程序 */ module_formats[km-m_loader]-l_exec(km);
1.7. 内核初始化
让我们来看一下链接内核的命令。 这能帮助我们了解 loader 传递给内核的准确位置。 这个位置就是内核真实的入口点。
sys/conf/Makefile.i386: ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic \ -dynamic-linker /red/herring -o kernel -X locore.o \ lots of kernel .o files
在这一行中有一些有趣的东西。首先,内核是一个ELF动态链接二进制文件, 可是动态链接器却是/red/herring,一个莫须有的文件。 其次,看一下文件sys/conf/ldscript.i386, 可以对理解编译内核时ld的选项有一些启发。 阅读最前几行,字符串
sys/conf/ldscript.i386: ENTRY(btext)
表示内核的入口点是符号 btext
。这个符号在locore.s 中定义:
sys/i386/i386/locore.s: .text /********************************************************************** * * This is where the bootblocks start us, set the ball rolling... * 入口 */ NON_GPROF_ENTRY(btext)
首先将寄存器EFLAGS设为一个预定义的值0x00000002, 然后初始化所有段寄存器:
sys/i386/i386/locore.s /* 不要相信BIOS给出的EFLAGS值 */ pushl $PSL_KERNEL popfl /* * 不要相信BIOS给出的%fs、%gs值。相信引导过程中设定的%cs、%ds、%es、%ss值 */ mov %ds, %ax mov %ax, %fs mov %ax, %gs
btext调用例程recover_bootinfo()
, identify_cpu()
,create_pagetables()
。 这些例程也定在locore.s之中。这些例程的功能如下:
| 这个例程分析由引导程序传送给内核的参数。引导内核有3种方式: 由loader引导(如前所述), 由老式磁盘引导块引导,无盘引导方式。 这个函数决定引导方式,并将结构 |
| 这个函数侦测CPU类型,将结果存放在变量 |
| 这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容 |
下一步是开启VME(如果CPU有这个功能):
testl $CPUID_VME, R(_cpu_feature) jz 1f movl %cr4, %eax orl $CR4_VME, %eax movl %eax, %cr4
然后,启动分页模式:
/* Now enable paging */ movl R(_IdlePTD), %eax movl %eax,%cr3 /* load ptd addr into mmu */ movl %cr0,%eax /* get control word */ orl $CR0_PE|CR0_PG,%eax /* enable paging */ movl %eax,%cr0 /* and let's page NOW! */
由于分页模式已经启动,原先的实地址寻址方式随即失效。 随后三行代码用来跳转至虚拟地址:
pushl $begin /* jump to high virtualized address */ ret /* 现在跳转至KERNBASE,那里是操作系统内核被链接后真正的入口 */ begin:
函数init386()
被调用;随参数传递的是一个指针, 指向第一个空闲物理页。随后执行mi_startup()
。 init386
是一个与硬件系统相关的初始化函数, mi_startup()
是个与硬件系统无关的函数 (前缀’mi_'表示Machine Independent,不依赖于机器)。 内核不再从mi_startup()
里返回; 调用这个函数后,内核完成引导:
sys/i386/i386/locore.s: movl physfree, %esi pushl %esi /* 送给init386()的第一个参数 */ call _init386 /* 设置386芯片使之适应UNIX工作 */ call _mi_startup /* 自动配置硬件,挂接根文件系统,等 */ hlt /* 不再返回到这里! */
1.7.1. init386()
init386()
定义在 sys/i386/i386/machdep.c中, 它针对Intel 386芯片进行低级初始化。loader已将CPU切换至保护模式。 loader已经建立了最早的任务。
译者注 每个"任务"都是与其它" |
在这个任务中,内核将继续工作。在讨论其代码前, 我将处理器对保护模式必须完成的一系列准备工作一并列出:
初始化内核的可调整参数,这些参数由引导程序传来
准备GDT(全局描述符表)
准备IDT(中断描述符表)
初始化系统控制台
初始化DDB(内核的点调试器),如果它被编译进内核的话
初始化TSS(任务状态段)
准备LDT(局部描述符表)
建立proc0(0号进程,即内核的进程)的pcb(进程控制块)
init386()
首先初始化内核的可调整参数, 这些参数由引导程序传来。先设置环境指针(environment pointer, envp)调用, 再调用init_param1()
。 envp指针已由loader存放在结构bootinfo
中:
sys/i386/i386/machdep.c: kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE; /* 初始化基本可调整项,如hz等 */ init_param1();
init_param1()
定义在 sys/kern/subr_param.c之中。 这个文件里有一些sysctl项,还有两个函数, init_param1()
和init_param2()
。 这两个函数从init386()
中调用:
sys/kern/subr_param.c hz = HZ; TUNABLE_INT_FETCH("kern.hz", hz);
TUNABLE_typename_FETCH用来获取环境变量的值:
/usr/src/sys/sys/kernel.h #define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var))
Sysctlkern.hz
是系统时钟频率。同时, 这些sysctl项被init_param1()
设定: kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.maxdsiz, kern.dflssiz, kern.maxssiz, kern.sgrowsiz
。
然后init386()
准备全局描述符表 (Global Descriptors Table, GDT)。在x86上每个任务都运行在自己的虚拟地址空间里, 这个空间由"段址:偏移量"的数对指定。举个例子,当前将要由处理器执行的指令在 CS:EIP,那么这条指令的线性虚拟地址就是"代码段虚拟段地址CS" + EIP。 为了简便,段起始于虚拟地址0,终止于界限4G字节。所以,在这个例子中, 指令的线性虚拟地址正是EIP的值。段寄存器,如CS、DS等是选择符, 即全局描述符表中的索引(更精确的说,索引并非选择符的全部, 而是选择符中的INDEX部分)。
译者注 对于80386, 选择符有16位,INDEX部分是其中的高13位。 |
FreeBSD的全局描述符表为每个CPU保存着15个选择符:
sys/i386/i386/machdep.c: union descriptor gdt[NGDT * MAXCPU]; /* 全局描述符表 */ sys/i386/include/segments.h: /* * 全局描述符表(GDT)中的入口 */ #define GNULL_SEL 0 /* 空描述符 */ #define GCODE_SEL 1 /* 内核代码描述符 */ #define GDATA_SEL 2 /* 内核数据描述符 */ #define GPRIV_SEL 3 /* 对称多处理(SMP)每处理器专有数据 */ #define GPROC0_SEL 4 /* Task state process slot zero and up, 任务状态进程 */ #define GLDT_SEL 5 /* 每个进程的局部描述符表 */ #define GUSERLDT_SEL 6 /* 用户自定义的局部描述符表 */ #define GTGATE_SEL 7 /* 进程任务切换关口 */ #define GBIOSLOWMEM_SEL 8 /* BIOS低端内存访问(必须是这第8个入口) */ #define GPANIC_SEL 9 /* 会导致全系统异常中止工作的任务状态 */ #define GBIOSCODE32_SEL 10 /* BIOS接口(32位代码) */ #define GBIOSCODE16_SEL 11 /* BIOS接口(16位代码) */ #define GBIOSDATA_SEL 12 /* BIOS接口(数据) */ #define GBIOSUTIL_SEL 13 /* BIOS接口(工具) */ #define GBIOSARGS_SEL 14 /* BIOS接口(自变量,参数) */
请注意,这些#defines并非选择符本身,而只是选择符中的INDEX域, 因此它们正是全局描述符表中的索引。 例如,内核代码的选择符(GCODE_SEL)的值为0x08。
下一步是初始化中断描述符表(Interrupt Descriptor Table, IDT)。 这张表在发生软件或硬件中断时会被处理器引用。例如,执行系统调用时, 用户应用程序提交INT 0x80
指令。这是一个软件中断, 处理器用索引值0x80在中断描述符表中查找记录。这个记录指向处理这个中断的例程。 在这个特定情形中,这是内核的系统调用关口。
译者注 Intel 80386支持" |
中断描述符表最多可以有256 (0x100)条记录。内核分配NIDT条记录的内存给中断描述符表, 这里NIDT=256,是最大值:
sys/i386/i386/machdep.c: static struct gate_descriptor idt0[NIDT]; struct gate_descriptor *idt = idt0[0]; /* 中断描述符表 */
每个中断都被设置一个合适的中断处理程序。 系统调用关口INT 0x80
也是如此:
sys/i386/i386/machdep.c: setidt(0x80, IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
所以当一个用户应用程序提交INT 0x80
指令时, 全系统的控制权会传递给函数_Xint0x80_syscall
, 这个函数在内核代码段中,将被以管理员权限执行。
然后,控制台和DDB(调试器)被初始化:
sys/i386/i386/machdep.c: cninit(); /* 以下代码可能因为未定义宏DDB而被跳过 */ #ifdef DDB kdb_init(); if (boothowto RB_KDB) Debugger("Boot flags requested debugger"); #endif
任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时, 任务状态段用来让硬件存储任务现场信息。
局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符, 指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符:
/usr/include/machine/segments.h #define LSYS5CALLS_SEL 0 /* Intel BCS强制要求的 */ #define LSYS5SIGR_SEL 1 #define L43BSDCALLS_SEL 2 /* 尚无 */ #define LUCODE_SEL 3 #define LSOL26CALLS_SEL 4 /* Solaris =2.6版系统调用关口 */ #define LUDATA_SEL 5 /* separate stack, es,fs,gs sels ? 分别的栈、es、fs、gs选择符? */ /* #define LPOSIXCALLS_SEL 5*/ /* notyet, 尚无 */ #define LBSDICALLS_SEL 16 /* BSDI system call gate, BSDI系统调用关口 */ #define NLDT (LBSDICALLS_SEL + 1)
然后,proc0(0号进程,即内核所处的进程)的进程控制块(Process Control Block) (struct pcb
)结构被初始化。proc0是一个 struct proc
结构,描述了一个内核进程。 内核运行时,该进程总是存在,所以这个结构在内核中被定义为全局变量:
sys/kern/kern_init.c: struct proc proc0;
结构struct pcb
是proc结构的一部分, 它定义在/usr/include/machine/pcb.h之中, 内含针对i386硬件结构专有的信息,如寄存器的值。
1.7.2. mi_startup()
这个函数用冒泡排序算法,将所有系统初始化对象,然后逐个调用每个对象的入口:
sys/kern/init_main.c: for (sipp = sysinit; *sipp; sipp++) { /* ... 省略 ... */ /* 调用函数 */ (*((*sipp)-func))((*sipp)-udata); /* ... 省略 ... */ }
尽管sysinit框架已经在《FreeBSD开发者手册》中有所描述, 我还是在这里讨论一下其内部原理。
每个系统初始化对象(sysinit对象)通过调用宏建立。 让我们以announce
sysinit对象为例。 这个对象打印版权信息:
sys/kern/init_main.c: static void print_caddr_t(void *data __unused) { printf("%s", (char *)data); } SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)
这个对象的子系统标识是SI_SUB_COPYRIGHT(0x0800001), 数值刚好排在SI_SUB_CONSOLE(0x0800000)后面。 所以,版权信息将在控制台初始化之后就被很早的打印出来。
让我们看一看宏SYSINIT()
到底做了些什么。 它展开成宏C_SYSINIT()
。 宏C_SYSINIT()
然后展开成一个静态结构 struct sysinit
。结构里申明里调用了另一个宏 DATA_SET
:
/usr/include/sys/kernel.h: #define C_SYSINIT(uniquifier, subsystem, order, func, ident) \ static struct sysinit uniquifier ## _sys_init = { \ subsystem, \ order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ## _sys_init); #define SYSINIT(uniquifier, subsystem, order, func, ident) \ C_SYSINIT(uniquifier, subsystem, order, \ (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)
宏DATA_SET()
展开成MAKE_SET()
, 宏MAKE_SET()
指向所有隐含的sysinit幻数:
/usr/include/linker_set.h #define MAKE_SET(set, sym) \ static void const * const __set_##set##_sym_##sym = sym; \ __asm(".section .set." #set ",\"aw\""); \ __asm(".long " #sym); \ __asm(".previous") #endif #define TEXT_SET(set, sym) MAKE_SET(set, sym) #define DATA_SET(set, sym) MAKE_SET(set, sym)
回到我们的例子中,经过宏的展开过程,将会产生如下声明:
static struct sysinit announce_sys_init = { SI_SUB_COPYRIGHT, SI_ORDER_FIRST, (sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t, (void *) copyright }; static void const *const __set_sysinit_set_sym_announce_sys_init = announce_sys_init; __asm(".section .set.sysinit_set" ",\"aw\""); __asm(".long " "announce_sys_init"); __asm(".previous");
第一个__asm
指令在内核可执行文件中建立一个ELF节(section)。 这发生在内核链接的时候。这一节将被命令为.set.sysinit_set
。 这一节的内容是一个32位值――announce_sys_init结构的地址,这个结构正是第二个 \__asm
指令所定义的。第三个\__asm
指令标记节的结束。 如果前面有名字相同的节定义语句,节的内容(那个32位值)将被填加到已存在的节里, 这样就构造出了一个32位指针数组。
用objdump察看一个内核二进制文件, 也许你会注意到里面有这么几个小的节:
% objdump -h /kernel
7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2
CONTENTS, ALLOC, LOAD, DATA
8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2
CONTENTS, ALLOC, LOAD, DATA
9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2
CONTENTS, ALLOC, LOAD, DATA
10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2
CONTENTS, ALLOC, LOAD, DATA
11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2
CONTENTS, ALLOC, LOAD, DATA
12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2
CONTENTS, ALLOC, LOAD, DATA
这一屏信息显示表明节.set.sysinit_set有0x664字节的大小, 所以0x664/sizeof(void *)
个sysinit对象被编译进了内核。 其它节,如.set.sysctl_set
表示其它链接器集合。
通过定义一个类型为struct linker_set
的变量, 节.set.sysinit_set
将被"收集"到那个变量里:
sys/kern/init_main.c: extern struct linker_set sysinit_set; /* XXX */
struct linker_set
定义如下:
/usr/include/linker_set.h: struct linker_set { int ls_length; void *ls_items[1]; /* ls_length个项的数组, 以NULL结尾 */ };
译者注 实际上是说, 用C语言结构体linker_set来表达那个ELF节。 |
第一项是sysinit对象的数量,第二项是一个以NULL结尾的数组, 数组中是指向那些对象的指针。
回到对mi_startup()
的讨论, 我们清楚了sysinit对象是如何被组织起来的。 函数mi_startup()
将它们排序, 并调用每一个对象。最后一个对象是系统调度器:
/usr/include/sys/kernel.h: enum sysinit_sub_id { SI_SUB_DUMMY = 0x0000000, /* 不被执行,仅供链接器使用 */ SI_SUB_DONE = 0x0000001, /* 已被处理*/ SI_SUB_CONSOLE = 0x0800000, /* 控制台*/ SI_SUB_COPYRIGHT = 0x0800001, /* 最早使用控制台的对象 */ ... SI_SUB_RUN_SCHEDULER = 0xfffffff /* 调度器:不返回 */ };
系统调度器sysinit对象定义在文件sys/vm/vm_glue.c中, 这个对象的入口点是scheduler()
。 这个函数实际上是个无限循环,它表示那个进程标识(PID)为0的进程――swapper进程。 前面提到的proc0结构正是用来描述这个进程。
第一个用户进程是_init_, 由sysinit对象init
建立:
sys/kern/init_main.c: static void create_init(const void *udata __unused) { int error; int s; s = splhigh(); error = fork1(proc0, RFFDG | RFPROC, initproc); if (error) panic("cannot fork init: %d\n", error); initproc-p_flag |= P_INMEM | P_SYSTEM; cpu_set_fork_handler(initproc, start_init, NULL); remrunqueue(initproc); splx(s); } SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)
create_init()
通过调用fork1()
分配一个新的进程,但并不将其标记为可运行。当这个新进程被调度器调度执行时, start_init()
将会被调用。 那个函数定义在init_main.c中。 它尝试装载并执行二进制代码init, 先尝试/sbin/init,然后是/sbin/oinit, /sbin/init.bak,最后是/stand/sysinstall:
sys/kern/init_main.c: static char init_path[MAXPATHLEN] = #ifdef INIT_PATH __XSTRING(INIT_PATH); #else "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall"; #endif
Last modified on: March 9, 2024 by Danilo G. Baio