FreeBSD 系统结构手册

[复制链接]
查看11 | 回复9 | 2006-4-13 01:08:50 | 显示全部楼层 |阅读模式
目录
第I部分. 内核
第1章 引导过程与内核初始化
第1.1节 概述
第1.2节 总览
第1.3节 BIOS POST
第1.4节 boot0 阶段
第1.5节 boot2 阶段
第1.6节 loader 阶段
第1.7节 内核初始化
第2章 内核中的锁
第2.1节 Mutex
第2.2节 共享互斥锁
第2.3节 原子保护变量
第3章 内核对象
第3.1节 术语
第3.2节 Kobj的工作流程
第3.3节 使用Kobj
第4章 Jail子系统
第4.1节 Jail的系统结构
第4.2节 系统对被囚禁程序的限制
第5章 SYSINIT框架
第5.1节 术语
第5.2节 SYSINIT操作
第5.3节 使用SYSINIT
第6章 The TrustedBSD MAC Framework
第6.1节 MAC Documentation Copyright
第6.2节 Synopsis
第6.3节 Introduction
第6.4节 Policy Background
第6.5节 MAC Framework Kernel Architecture
第6.6节 MAC Policy Architecture
第6.7节 MAC Policy Entry Point Reference
第6.8节 Userland Architecture
第6.9节 Conclusion
第7章 虚拟内存系统
第7.1节 物理内存的管理──vm_page_t
第7.2节 The unified buffer cache──vm_object_t
第7.3节 Filesystem I/O──struct buf
第7.4节 Mapping Page Tables──vm_map_t, vm_entry_t
第7.5节 KVM Memory Mapping
第7.6节 Tuning the FreeBSD VM system
第8章 SMPng 设计文档
第8.1节 绪论
第8.2节 基本工具与上锁的基础知识
第8.3节 General Architecture and Design
第8.4节 Specific Locking Strategies
第8.5节 Implementation Notes
第8.6节 Miscellaneous Topics
Glossary
第II部分. 设备驱动程序
第9章 编写 FreeBSD 设备驱动程序
第9.1节 简介
第9.2节 动态内核链接工具-KLD
第9.3节 访问设备驱动程序
第9.4节 字符设备
第9.5节 块设备(消亡中)
第9.6节 网络设备驱动程序
第10章 ISA设备驱动程序
第10.1节 概述
第10.2节 基本信息
第10.3节 Device_t指针
第10.4节 配置文件与自动配置期间识别和探测的顺序
第10.5节 资源
第10.6节 总线内存映射
第10.7节 DMA
第10.8节 xxx_isa_probe
第10.9节 xxx_isa_attach
第10.10节 xxx_isa_detach
第10.11节 xxx_isa_shutdown
第10.12节 xxx_intr
第11章 PCI设备
第11.1节 探测与连接
第11.2节 总线资源
第12章 通用访问方法SCSI控制器
第12.1节 提纲
第12.2节 通用基础结构
第12.3节 轮询
第12.4节 异步事件
第12.5节 中断
第12.6节 错误总览
第12.7节 超时处理
第13章 USB设备
第13.1节 简介
第13.2节 主控器
第13.3节 USB设备信息
第13.4节 设备的探测和连接
第13.5节 USB驱动程序的协议信息
第14章 Newbus
第14.1节 设备驱动程序
第14.2节 Newbus概览
第14.3节 Newbus API
第15章 声音子系统
第15.1节 简介
第15.2节 文件
第15.3节 探测,连接等
第15.4节 接口
第16章 PC Card
第16.1节 添加设备
第III部分. 附录
参考书目
索引
表格清单
表2-1. Mutex列表
表2-2. 共享互斥锁列表
插图清单
图14-1. driver_t实现
图14-2. 设备状态device_state_t
范例清单
例5-1. SYSINIT()的例子
例5-2. 调整SYSINIT()顺序的例子
例5-3. SYSUNINIT()的例子
例9-1. 适用于FreeBSD 4.X的回显伪设备驱动程序实例
例9-2. 适用于FreeBSD 5.X回显伪设备驱动程序实例
例14-1. Newbus的方法
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
第I部分. 内核
目录
第1章 引导过程与内核初始化
第2章 内核中的锁
第3章 内核对象
第4章 Jail子系统
第5章 SYSINIT框架
第6章 The TrustedBSD MAC Framework
第7章 虚拟内存系统
第8章 SMPng 设计文档
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
1.1 概述
这一章是对引导过程和系统初始化过程的总览。这些过程始于BIOS(固化件) POST, 直到第一个用户进程建立。由于系统启动的最初步骤是与硬件结构相关的、是紧配合的,这里用IA-32(Intel Architecture 32bit)结构作为例子。
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
1.2 总览
一台运行FreeBSD的计算机有多种引导方法。这里讨论其中最通常的方法,也就是从安装了操作系统的硬盘上引导。引导过程分几步完成:
BIOS POST
boot0阶段
boot2阶段
loader阶段
内核初始化
boot0和boot2阶段在手册 boot(8) 中被称为bootstrap stages 1 and 2,是FreeBSD的3阶段引导过程的开始。在每一阶段都有各种各样的信息显示在屏幕上,你可以参考下表识别出这些步骤。请注意实际的显示内容可能随机器的不同而有一些区别:
视不同机器而定
BIOS(固化件)消息

F1FreeBSD
F2BSD
F5Disk 2

boot0

>>FreeBSD/i386 BOOT
Default: 1:ad(1,a)/boot/loader
boot:

boot2a

BTX loader 1.0 BTX version is 1.01
BIOS drive A: is disk0
BIOS drive C: is disk1
BIOS 639kB/64512kB available memory
FreeBSD/i386 bootstrap loader, Revision 0.8
Console internal video/keyboard
([email protected], Mon Nov 20 11:41:23 GMT 2000)
/kernel text=0x1234 data=0x2345 syms=[0x4+0x3456]
Hit [Enter] to boot immediately, or any other key for command prompt
Booting [kernel] in 9 seconds..._

loader

Copyright (c) 1992-2002 The FreeBSD Project.
Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
The Regents of the University of California. All rights reserved.
FreeBSD 4.6-RC #0: Sat May4 22:49:02 GMT 2002
devnull@kukas:/usr/obj/usr/src/sys/DEVNULL
Timecounter "i8254"frequency 1193182 Hz

内核

表注:
a. 的提示符仅在boot0阶段用户选择 操作系统后仍按住键盘上某一键时才出现。
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
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]。
备注
[1] 有些工具如 disklabel(8) 会使用这一区域存储信息,主要是在第二扇区里。
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
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。
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
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 # Interrupt number,中断号
#
# System call: exit
#
__exit: xorl %eax,%eax
# BTX system
int $INT_SYS
#call 0x0
#
# System call: exec
#
__exec: movl $0x1,%eax
# BTX system
int $INT_SYS
#call 0x1
BTX建立全局描述符表(Global Descriptor Table, GDT):
sys/boot/i386/btx/btx/btx.s:
gdt:.word 0x0,0x0,0x0,0x0 # Null entry,以空为入口
.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# Segment selector, 段选择符
init.2: shr %bx
# Handle this int? 是否处理这个中断?
jnc init.3
# No, 否
mov %ax,(%di) # Set handler offset, 设置处理程序偏移量
mov %dh,0x2(%di)#and selector, 设置处理程序选择符
mov %dl,0x5(%di)# Set P

PL:type, 设置 P

PL:type
add $0x4,%ax
# Next handler, 下一个中断处理程序
所以,当客户调用 __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;/* represents a char, 一个字节 * */
u_int32_t bi_nfs_diskless;/* struct nfs_diskless * */
/* End of fields that are always present. 以上信息为常备项,总是存在 */
#define bi_endcommonbi_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_tbi_memsizes_valid;
u_int8_tbi_bios_dev;/* bootdev BIOS unit number, BIOS单元编号 */
u_int8_tbi_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 * */
/* Items below only from advanced bootloader, 以下项目仅高级bootloader提供 */
u_int32_t bi_kernend; /* end of kernel space, 内核空间结束 */
u_int32_t bi_envp;/* environment, 环境 */
u_int32_t bi_modulep; /* preloaded modules, 预装载的模块 */
};
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));
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
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);
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
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 \
在这一行中有一些有趣的东西。首先,内核是一个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
/* Don't trust what the BIOS gives for eflags. 不要相信BIOS给出的EFLAGS值 */
pushl $PSL_KERNEL
popfl
/*
* Don't trust what the BIOS gives for %fs and %gs.Trust the bootstrap
* to set %cs, %ds, %es and %ss.
* 不要相信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之中。这些例程的功能如下:
recover_bootinfo 这个例程分析由引导程序传送给内核的参数。 引导内核有3种方式: 由loader引导(如前所述), 由老式磁盘引导块引导, 无盘引导方式。这个函数决定引导方式,并将结构struct bootinfo 存储至内核内存。
identify_cpu 这个函数侦测CPU类型,将结果存放在变量_cpu中。
create_pagetables 这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容
下一步是开启VME(如果CPU有这个功能):
testl $CPUID_VME, R(_cpu_feature)
jz1f
movl%cr4, %eax
orl $CR4_VME, %eax
movl%eax, %cr4
然后,启动分页模式:
/* Now enable paging */
movlR(_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
/* now running relocated at KERNBASE where the system is linked to run
* 现在跳转至KERNBASE,那里是操作系统内核被链接后真正的入口 */
begin:
函数 init386() 被调用;随参数传递的是一个指针,指向第一个空闲物理页。随后执行mi_startup()。 init386是一个与硬件系统相关的初始化函数, mi_startup() 是个与硬件系统无关的函数 (前缀'mi_'表示Machine Independent,不依赖于机器)。 内核不再从 mi_startup()里返回;调用这个函数后, 内核完成引导:
sys/i386/i386/locore.s:
movlphysfree, %esi
pushl %esi/* value of first for init386(first), 送给init386()的参数 */
call_init386/* wire 386 chip for unix operation, 设置386芯片使之适应UNIX工作 */
call_mi_startup /* autoconfiguration, mountroot etc, 自动配置硬件,挂接根文件系统,等 */
hlt /* never returns to here, 不再返回到这里 */
1.7.1 init386()
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中:
sys/i386/i386/machdep.c:
kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE;
/* Init basic tunables, hz etc, 初始化基本可调整项,如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__FETCH 用来获取环境变量的值:
/usr/src/sys/sys/kernel.h
#define TUNABLE_INT_FETCH(path, var)getenv_int((path), (var))
Sysctl kern.hz 是系统时钟频率。 同时,这些sysctl项被init_param1()设定: kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, 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];/* global descriptor table */
sys/i386/include/segments.h:
/*
* Entries in the Global Descriptor Table (GDT)
*/
#define GNULL_SEL 0 /* Null Descriptor, 空描述符 */
#define GCODE_SEL 1 /* Kernel Code Descriptor, 内核代码描述符 */
#define GDATA_SEL 2 /* Kernel Data Descriptor, 内核数据描述符 */
#define GPRIV_SEL 3 /* SMP Per-Processor Private Data, 对称多处理每处理器专有数据 */
#define GPROC0_SEL4 /* Task state process slot zero and up, 任务状态进程 */
#define GLDT_SEL5 /* LDT - eventually one per process, 每个进程的局部描述符表 */
#define GUSERLDT_SEL6 /* User LDT, 用户自定义的局部描述符表 */
#define GTGATE_SEL7 /* Process task switch gate, 进程任务切换关口 */
#define GBIOSLOWMEM_SEL 8 /* BIOS low memory access (must be entry 8), BIOS低端内存访问(入口值必须是8) */
#define GPANIC_SEL9 /* Task state to consider panic from, 会导致全系统异常中止工作的任务状态 */
#define GBIOSCODE32_SEL 10/* BIOS interface (32bit Code), BIOS接口(32位代码) */
#define GBIOSCODE16_SEL 11/* BIOS interface (16bit Code), BIOS接口(16位代码) */
#define GBIOSDATA_SEL 12/* BIOS interface (Data), BIOS接口(数据) */
#define GBIOSUTIL_SEL 13/* BIOS interface (Utility), BIOS接口(工具) */
#define GBIOSARGS_SEL 14/* BIOS interface (Arguments), BIOS接口(自变量,参数) */
请注意,这些 #defines 并非选择符本身,而只是选择符中的 INDEX域,因此它们正是全局描述符表中的索引。例如,内核代码的选择符(GCODE_SEL)的值为0x08。
下一步是初始化中断描述符表(Interrupt Descriptor Table, IDT)。这张表在发生软件或硬件中断时会被处理器引用。例如,执行系统调用时,用户应用程序提交INT 0x80 指令。这是一个软件中断,处理器用索引值0x80在中断描述符表中查找记录。这个记录指向处理这个中断的例程。在这个特定情形中,这是内核的系统调用关口。
译者注: Intel 80386 支持“调用门”,可以使得用户程序只通过一条call指令就调用内核中的例程。可是FreeBSD并未采用这种机制,也许是因为使用软中断接口 可免去动态链接的麻烦吧。另外还有一个附带的好处:在仿真Linux时,当遇到FreeBSD内核不支持的而又并非关键性的系统调用时,内核只会显示一些出错信息,这使得程序能够继续运行;而不是在真正执行程序之前的初始化过程中就因为动态链接失败而不允许程序运行。
中断描述符表最多可以有256 (0x100)条记录。内核分配NIDT条记录的内存给中断描述符表,这里NIDT=256,是最大值:

sys/i386/i386/machdep.c:
static struct gate_descriptor idt0[NIDT];
struct gate_descriptor *idt = &idt0[0]; /* interrupt descriptor table, 中断描述符表 */
每个中断都被设置一个合适的中断处理程序。系统调用关口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();
/* skipped */
#ifdef DDB
kdb_init();
if (boothowto & RB_KDB)
Debugger("Boot flags requested debugger&quot

;
#endif
任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时,任务状态段用来让硬件存储任务现场信息。
局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符,指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符:
/usr/include/machine/segments.h
#define LSYS5CALLS_SEL0 /* forced by intel BCS, Intel BCS强制要求的 */
#define LSYS5SIGR_SEL 1
#define L43BSDCALLS_SEL 2 /* notyet, 尚无 */
#define LUCODE_SEL3
#define LSOL26CALLS_SEL 4 /* Solaris >= 2.6 system call gate, Solaris >=2.6版系统调用关口 */
#define LUDATA_SEL5
/* separate stack, es,fs,gs sels ? 分别的栈、es、fs、gs选择符? */
/* #defineLPOSIXCALLS_SEL 5*/ /* notyet, 尚无 */
#define LBSDICALLS_SEL16/* 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:
structproc 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++) {
/* ... skipped ... */
/* Call function */
(*((*sipp)->func))((*sipp)->udata);
/* ... skipped ... */
}
尽管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\"&quot

;
\
__asm(".long " #sym);
\
__asm(".previous&quot

#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\"&quot

;
__asm(".long " "announce_sys_init&quot

;
__asm(".previous&quot

;
第一个__asm指令在内核可执行文件中建立一个ELF节(section)。这发生在内核链接的时候。这一节将被命令为.set.sysinit_set。这一节的内容是一个32位值——announce_sys_init结构的地址,这个结构正是第二个__asm 指令所定义的。第三个__asm指令标记节的结束。如果前面有名字相同的节定义语句,节的内容(那个32位值)将被填加到已存在的节里,这样就构造出了一个32位指针数组。
用objdump察看一个内核二进制文件,也许你会注意到里面有这么几个小的节:
% objdump -h /kernel
7 .set.cons_set 00000014c03164c0c03164c0002154c02**2

CONTENTS, ALLOC, LOAD, DATA
8 .set.kbddriver_set 00000010c03164d4c03164d4002154d42**2

CONTENTS, ALLOC, LOAD, DATA
9 .set.scrndr_set 00000024c03164e4c03164e4002154e42**2

CONTENTS, ALLOC, LOAD, DATA
10 .set.scterm_set 0000000cc0316508c0316508002155082**2

CONTENTS, ALLOC, LOAD, DATA
11 .set.sysctl_set 0000097cc0316514c0316514002155142**2

CONTENTS, ALLOC, LOAD, DATA
12 .set.sysinit_set 00000664c0316e90c0316e9000215e902**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]; /* really ls_length of them, trailing NULL; */

/* 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,/* not executed; for linker 不被执行,仅供链接器使用 */
SI_SUB_DONE = 0x0000001,/* processed, 已被处理*/
SI_SUB_CONSOLE= 0x0800000,/* console, 控制台*/
SI_SUB_COPYRIGHT= 0x0800001,/* first use of console, 最早使用控制台的对象 */
...
SI_SUB_RUN_SCHEDULER= 0xfffffff /* scheduler: no return, 调度器:不返回 */
};
系统调度器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] =
#ifdefINIT_PATH
__XSTRING(INIT_PATH);
#else
"/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall";
#endif
回复

使用道具 举报

千问 | 2006-4-13 01:08:50 | 显示全部楼层
第2章 内核中的锁
目录
第2.1节 Mutex
第2.2节 共享互斥锁
第2.3节 原子保护变量
这一章由 FreeBSD SMP Next Generation Project 维护。 请将评论和建议发送给 FreeBSD 对称多处理 (SMP) 邮件列表.
这篇文档提纲挈领的讲述了在FreeBSD内核中的锁,这些锁使得有效的多处理成为可能。锁可以用几种方式获得。数据结构可以用 mutex或 lockmgr(9) 保护。对于为数不多的若干个变量,假如总是使用原子操作访问它们,这些变量就可以得到保护。
译者注: 仅读本章内容,还不足以找出“mutex”和“共享互斥锁”的区别。似乎它们的功能有重叠之处,前者比后者的功能选项更多。它们似乎都是lockmgr(9)的子集。


2.1 Mutex
Mutex就是一种用来解决共享/排它矛盾的锁。一个mutex在一个时刻只可以被一个实体拥有。如果另一个实体要获得已经被拥有的mutex,就会进入等待,直到这个mutex被释放。在FreeBSD内核中,mutex被 进程所拥有。
Mutex可以被递归的索要,但是mutex一般只被一个实体拥有较短的一段时间,因此一个实体不能在持有mutex时睡眠。如果你需要在持有 mutex时睡眠,可使用一个 lockmgr(9) 的锁。
每个mutex有几个令人感兴趣的属性:
变量名
在内核源代码中struct mtx变量的名字
逻辑名
由函数mtx_init指派的mutex的名字。这个名字显示在KTR跟踪消息和witness出错与警告信息里。这个名字还用于区分标识在witness代码中的各个mutex
类型
mutex的类型,用标志MTX_*表示。 每个标志的意义在 mutex(9) 有所描述。
MTX_DEF
一个睡眠mutex
MTX_SPIN
一个循环mutex
MTX_RECURSE
这个mutex允许递归
保护对象
这个入口所要保护的数据结构列表或数据结构成员列表。 对于数据结构成员,将按照结构名.成员名的形式命名。
依赖函数
仅当mutex被持有时才可以被调用的函数
表 2-1. Mutex列表
变量名 逻辑名 类型 保护对象 依赖函数
sched_lock “sched lock”(调度器锁) MTX_SPIN | MTX_RECURSE_gmonparam, cnt.v_swtch, cp_time, curpriority, mtx.mtx_blocked, mtx.mtx_contested, proc.p_procq, proc.p_slpq, proc.p_sflag proc.p_stat, proc.p_estcpu, proc.p_cpticks proc.p_pctcpu, proc.p_wchan, proc.p_wmesg, proc.p_swtime, proc.p_slptime, proc.p_runtime, proc.p_uu, proc.p_su, proc.p_iu, proc.p_uticks, proc.p_sticks, proc.p_iticks, proc.p_oncpu, proc.p_lastcpu, proc.p_rqindex, proc.p_heldmtx, proc.p_blocked, proc.p_mtxname, proc.p_contested, proc.p_priority, proc.p_usrpri, proc.p_nativepri, proc.p_nice, proc.p_rtprio, pscnt, slpque, itqueuebits, itqueues, rtqueuebits, rtqueues, queuebits, queues, idqueuebits, idqueues, switchtime, switchtickssetrunqueue, remrunqueue, mi_switch, chooseproc, schedclock, resetpriority, updatepri, maybe_resched, cpu_switch, cpu_throw, need_resched, resched_wanted, clear_resched, aston, astoff, astpending, calcru, proc_compare
vm86pcb_lock “vm86pcb lock”(虚拟8086模式进程控制块锁) MTX_DEFvm86pcbvm86_bioscall
Giant “Giant”(巨锁) MTX_DEF | MTX_RECURSE几乎可以是任何东西 许多
callout_lock “callout lock”(延时调用锁) MTX_SPIN | MTX_RECURSEcallfree, callwheel, nextsoftcheck, proc.p_itcallout, proc.p_slpcallout, softticks, ticks
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题

0

回帖

4882万

积分

论坛元老

Rank: 8Rank: 8

积分
48824836
热门排行