让我们来看一下链接内核的命令。 这能帮助我们了解 loader 传递给内核的准确位置。 这个位置就是内核真实的入口点。
在这一行中有一些有趣的东西。首先,内核是一个ELF动态链接二进制文件,
可是动态链接器却是/red/herring
,一个莫须有的文件。
其次,看一下文件sys/conf/ldscript.i386
,
可以对理解编译内核时ld的选项有一些启发。
阅读最前几行,字符串
表示内核的入口点是符号 `btext'。这个符号在locore.s
中定义:
首先将寄存器EFLAGS设为一个预定义的值0x00000002, 然后初始化所有段寄存器:
btext调用例程recover_bootinfo()
,
identify_cpu()
,create_pagetables()
。
这些例程也定在locore.s
之中。这些例程的功能如下:
recover_bootinfo | 这个例程分析由引导程序传送给内核的参数。引导内核有3种方式:
由loader引导(如前所述), 由老式磁盘引导块引导,无盘引导方式。
这个函数决定引导方式,并将结构struct bootinfo
存储至内核内存。 |
identify_cpu | 这个函数侦测CPU类型,将结果存放在变量
_cpu 中。 |
create_pagetables | 这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容 |
下一步是开启VME(如果CPU有这个功能):
然后,启动分页模式:
由于分页模式已经启动,原先的实地址寻址方式随即失效。 随后三行代码用来跳转至虚拟地址:
函数init386()
被调用;随参数传递的是一个指针,
指向第一个空闲物理页。随后执行mi_startup()
。
init386
是一个与硬件系统相关的初始化函数,
mi_startup()
是个与硬件系统无关的函数
(前缀'mi_'表示Machine Independent,不依赖于机器)。
内核不再从mi_startup()
里返回;
调用这个函数后,内核完成引导:
init386()
定义在
sys/i386/i386/machdep.c
中,
它针对Intel 386芯片进行低级初始化。loader已将CPU切换至保护模式。
loader已经建立了最早的任务。
每个"任务"都是与其它“任务”相对独立的执行环境。 任务之间可以分时切换,这为并发进程/线程的实现提供了必要基础。 对于Intel 80x86任务的描述,详见Intel公司关于80386 CPU及后续产品的资料, 或者在清华大学图书馆 馆藏记录中用"80386"作为关键词所查找到的系统结构方面的书目。
在这个任务中,内核将继续工作。在讨论其代码前, 我将处理器对保护模式必须完成的一系列准备工作一并列出:
初始化内核的可调整参数,这些参数由引导程序传来
准备GDT(全局描述符表)
准备IDT(中断描述符表)
初始化系统控制台
初始化DDB(内核的点调试器),如果它被编译进内核的话
初始化TSS(任务状态段)
准备LDT(局部描述符表)
建立proc0(0号进程,即内核的进程)的pcb(进程控制块)
init386()
首先初始化内核的可调整参数,
这些参数由引导程序传来。先设置环境指针(environment pointer, envp)调用,
再调用init_param1()
。
envp指针已由loader存放在结构bootinfo
中:
init_param1()
定义在
sys/kern/subr_param.c
之中。
这个文件里有一些sysctl项,还有两个函数,
init_param1()
和init_param2()
。
这两个函数从init386()
中调用:
TUNABLE_<typename>_FETCH用来获取环境变量的值:
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个选择符:
请注意,这些#defines并非选择符本身,而只是选择符中的INDEX域, 因此它们正是全局描述符表中的索引。 例如,内核代码的选择符(GCODE_SEL)的值为0x08。
下一步是初始化中断描述符表(Interrupt Descriptor Table, IDT)。
这张表在发生软件或硬件中断时会被处理器引用。例如,执行系统调用时,
用户应用程序提交INT 0x80
指令。这是一个软件中断,
处理器用索引值0x80在中断描述符表中查找记录。这个记录指向处理这个中断的例程。
在这个特定情形中,这是内核的系统调用关口。
Intel 80386支持“调用门”,可以使得用户程序只通过一条call指令 就调用内核中的例程。可是FreeBSD并未采用这种机制, 也许是因为使用软中断接口可免去动态链接的麻烦吧。另外还有一个附带的好处: 在仿真Linux时,当遇到FreeBSD内核不支持的而又并非关键性的系统调用时, 内核只会显示一些出错信息,这使得程序能够继续运行; 而不是在真正执行程序之前的初始化过程中就因为动态链接失败而不允许程序运行。
中断描述符表最多可以有256 (0x100)条记录。内核分配NIDT条记录的内存给中断描述符表, 这里NIDT=256,是最大值:
每个中断都被设置一个合适的中断处理程序。
系统调用关口INT 0x80
也是如此:
所以当一个用户应用程序提交INT 0x80
指令时,
全系统的控制权会传递给函数_Xint0x80_syscall
,
这个函数在内核代码段中,将被以管理员权限执行。
然后,控制台和DDB(调试器)被初始化:
任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时, 任务状态段用来让硬件存储任务现场信息。
局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符, 指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符:
然后,proc0(0号进程,即内核所处的进程)的进程控制块(Process Control Block)
(struct pcb
)结构被初始化。proc0是一个
struct proc
结构,描述了一个内核进程。
内核运行时,该进程总是存在,所以这个结构在内核中被定义为全局变量:
结构struct pcb
是proc结构的一部分,
它定义在/usr/include/machine/pcb.h
之中,
内含针对i386硬件结构专有的信息,如寄存器的值。
这个函数用冒泡排序算法,将所有系统初始化对象,然后逐个调用每个对象的入口:
尽管sysinit框架已经在《FreeBSD开发者手册》中有所描述, 我还是在这里讨论一下其内部原理。
每个系统初始化对象(sysinit对象)通过调用宏建立。
让我们以announce
sysinit对象为例。
这个对象打印版权信息:
这个对象的子系统标识是SI_SUB_COPYRIGHT(0x0800001), 数值刚好排在SI_SUB_CONSOLE(0x0800000)后面。 所以,版权信息将在控制台初始化之后就被很早的打印出来。
让我们看一看宏SYSINIT()
到底做了些什么。
它展开成宏C_SYSINIT()
。
宏C_SYSINIT()
然后展开成一个静态结构
struct sysinit
。结构里申明里调用了另一个宏
DATA_SET
:
宏DATA_SET()
展开成MAKE_SET()
,
宏MAKE_SET()
指向所有隐含的sysinit幻数:
回到我们的例子中,经过宏的展开过程,将会产生如下声明:
第一个__asm
指令在内核可执行文件中建立一个ELF节(section)。
这发生在内核链接的时候。这一节将被命令为.set.sysinit_set
。
这一节的内容是一个32位值――announce_sys_init结构的地址,这个结构正是第二个
__asm
指令所定义的。第三个__asm
指令标记节的结束。
如果前面有名字相同的节定义语句,节的内容(那个32位值)将被填加到已存在的节里,
这样就构造出了一个32位指针数组。
用objdump察看一个内核二进制文件, 也许你会注意到里面有这么几个小的节:
这一屏信息显示表明节.set.sysinit_set有0x664字节的大小,
所以0x664/sizeof(void *)
个sysinit对象被编译进了内核。
其它节,如.set.sysctl_set
表示其它链接器集合。
通过定义一个类型为struct linker_set
的变量,
节.set.sysinit_set
将被“收集”到那个变量里:
struct linker_set
定义如下:
实际上是说, 用C语言结构体linker_set来表达那个ELF节。
第一项是sysinit对象的数量,第二项是一个以NULL结尾的数组, 数组中是指向那些对象的指针。
回到对mi_startup()
的讨论,
我们清楚了sysinit对象是如何被组织起来的。
函数mi_startup()
将它们排序,
并调用每一个对象。最后一个对象是系统调度器:
系统调度器sysinit对象定义在文件sys/vm/vm_glue.c
中,
这个对象的入口点是scheduler()
。
这个函数实际上是个无限循环,它表示那个进程标识(PID)为0的进程――swapper进程。
前面提到的proc0结构正是用来描述这个进程。
第一个用户进程是init,
由sysinit对象init
建立:
create_init()
通过调用fork1()
分配一个新的进程,但并不将其标记为可运行。当这个新进程被调度器调度执行时,
start_init()
将会被调用。
那个函数定义在init_main.c
中。
它尝试装载并执行二进制代码init
,
先尝试/sbin/init
,然后是/sbin/oinit
,
/sbin/init.bak
,最后是/stand/sysinstall
:
本文档和其它文档可从这里下载: ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
如果对于FreeBSD有问题,请先阅读
文档,如不能解决再联系
<questions@FreeBSD.org>.
关于本文档的问题请发信联系
<doc@FreeBSD.org>.