博客首页 注册 建议与交流 排行榜 加入友情链接
推荐 投诉 搜索: 帮助

九月枫叶

人生有如枫叶在秋风里也能红的灿烂Unix is simple. It just takes a genius to understand its simplicity
  w3g8.cublog.cn

关于作者
姓名:*****
职业:计算机网络
年龄:25
位置:山西★运城
个性介绍:人生有如枫叶在秋风里也能红的灿烂。工具,而不是策略。
|| << >> ||
我的分类


FreeBSD 系统结构手册

The FreeBSD Documentation Project

FreeBSD 中文计划

  欢迎您阅读《FreeBSD系统结构手册》。 这本手册还在不断由许多人继续书写。许多章节还是空白,有的章节亟待更新。 如果您对这个项目感兴趣并愿意有所贡献,请发信给 FreeBSD 文档计划邮件列表

   本文档的最新英文原始版本可从 FreeBSD Web 站点获得, 最新中文译本可从FreeBSD 中文计划 Web 站点获得。也可以各种格式和压缩形式从 FreeBSD FTP 服务器 或众多的 镜像站点 得到。

FreeBSD是FreeBSD基金会的注册商标。

UNIX是Open Group在美国和其它国家的注册商标。

Sun, Sun Microsystems, SunOS, Solaris, and Java是Sun Microsystems, Inc. 在美国和其它国家的商标或注册商标。

Apple and QuickTime是Apple Computer, Inc.的商标, 在美国和其它国家注册。

Macromedia and Flash是Macromedia, Inc. 在美国和/或其它国家的商标或注册商标。

Microsoft, Windows, and Windows Media是Microsoft Corporation 在美国和/或其它国家的商标或注册商标。

PartitionMagic是PowerQuest Corporation在美国和/或其它国家的注册商标。

许多制造商和经销商使用一些称为商标的图案或文字设计来彰显自己的产品。 本文档中出现的,为 FreeBSD Project 所知晓的商标,后面将以 '™' 或 '®' 符号来标注。

重要: 本文中许可证的非官方中文翻译仅供参考,不作为判定任何责任的依据。如与英文原文有出入,则以英文原文为准。

在满足下列许可条件的前提下, 允许再分发或以源代码 (SGML DocBook) 或 “编译” (SGML, HTML, PDF, PostScript, RTF 等) 的经过修改或未修改的形式:

  1. 再分发源代码 (SGML DocBook) 必须不加修改的保留上述版权告示、本条件清单和下述弃权书作为该文件的最先若干行。

  2. 再分发编译的形式 (转换为其它DTD、 PDF、 PostScript、 RTF 或其它形式),必须将上述版权告示、本条件清单和下述弃权书复制到与分发品一同提供的文件,以及其它材料中。

重要: 本文档由 FREEBSD DOCUMENTATION PROJECT “按现状条件” 提供,并在此明示不提供任何明示或暗示的保障, 包括但不限于对商业适销性、对特定目的的适用性的暗示保障。 任何情况下, FREEBSD DOCUMENTATION PROJECT 均不对任何直接、 间接、 偶然、 特殊、 惩罚性的, 或必然的损失 (包括但不限于替代商品或服务的采购、 使用、 数据或利益的损失或营业中断) 负责,无论是如何导致的并以任何有责任逻辑的, 无论是否是在本文档使用以外以任何方式产生的契约、严格责任或是民事侵权行为(包括疏忽或其它)中的, 即使已被告知发生该损失的可能性。

Redistribution and use in source (SGML DocBook) and 'compiled' forms (SGML, HTML, PDF, PostScript, RTF and so forth) with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code (SGML DocBook) must retain the above copyright notice, this list of conditions and the following disclaimer as the first lines of this file unmodified.

  2. Redistributions in compiled form (transformed to other DTDs, converted to PDF, PostScript, RTF and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

重要: THIS DOCUMENTATION IS PROVIDED BY THE FREEBSD DOCUMENTATION PROJECT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD DOCUMENTATION PROJECT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


目录
第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章 TrustedBSD MAC 框架
6.1 MAC 文档版权声明
6.2 术语解析
6.3 概述
6.4 安全策略背景知识
6.5 MAC 框架的内核体系结构
6.6 MAC策略模块体系结构
6.7 MAC策略入口函数参考
6.8 应用层体系结构
6.9 小结
第7章 虚拟内存系统
7.1 物理内存的管理──vm_page_t
7.2 统一的缓存信息结构体──vm_object_t
7.3 文件系统输入/输出──buf结构体
7.4 映射页表──vm_map_t, vm_entry_t
7.5 KVM存储映射
7.6 调整FreeBSD的虚拟内存系统
第8章 SMPng 设计文档
8.1 绪论
8.2 基本工具与上锁的基础知识
8.3 架构与设计概览
8.4 特定数据的锁策略
8.5 实现说明
8.6 其它话题
术语表
第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

第I部分. 内核

目录
第1章 引导过程与内核初始化
第2章 内核中的锁
第3章 内核对象
第4章 Jail子系统
第5章 SYSINIT框架
第6章 TrustedBSD MAC 框架
第7章 虚拟内存系统
第8章 SMPng 设计文档

第1章  引导过程与内核初始化

供稿:Sergey Lyubka. 翻译:intron @NewSMTH.

1.1 概述

  这一章是对引导过程和系统初始化过程的总览。这些过程始于BIOS(固件)POST, 直到第一个用户进程建立。由于系统启动的最初步骤是与硬件结构相关的、是紧配合的,这里用IA-32(Intel Architecture 32bit)结构作为例子。


1.2 总览

  一台运行FreeBSD的计算机有多种引导方法。这里讨论其中最通常的方法,也就是从安装了操作系统的硬盘上引导。引导过程分几步完成:

  • BIOS POST

  • boot0阶段

  • boot2阶段

  • loader阶段

  • 内核初始化

  boot0boot2阶段在手册 boot(8)中被称为bootstrap stages 1 and 2,是FreeBSD的3阶段引导过程的开始。在每一阶段都有各种各样的信息显示在屏幕上,你可以参考下表识别出这些步骤。请注意实际的显示内容可能随机器的不同而有一些区别:

视不同机器而定

BIOS(固件)消息

F1    FreeBSD
F2    BSD
F5    Disk 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
(jkh@bento.freebsd.org, 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 May  4 22:49:02 GMT 2002
    devnull@kukas:/usr/obj/usr/src/sys/DEVNULL
Timecounter "i8254"  frequency 1193182 Hz


内核

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

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.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里的文件 boot0boot2不会在引导过程中使用, 只有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,这是系统调用关口。

  • 两个系统调用execexit的定义如下:

    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之中。这些例程的功能如下:

recover_bootinfo 这个例程分析由引导程序传送给内核的参数。引导内核有3种方式: 由loader引导(如前所述), 由老式磁盘引导块引导,无盘引导方式。 这个函数决定引导方式,并将结构struct bootinfo 存储至内核内存。
identify_cpu 这个函数侦测CPU类型,将结果存放在变量 _cpu中。
create_pagetables 这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容

  下一步是开启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已经建立了最早的任务。

译者注: 每个"任务"都是与其它“任务”相对独立的执行环境。任务之间可以分时切换,这为并发进程/线程的实现提供了必要基础。 对于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;

    /* 初始化基本可调整项,如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.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支持“调用门”,可以使得用户程序只通过一条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]; /* 中断描述符表 */

  每个中断都被设置一个合适的中断处理程序。 系统调用关口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

第2章  内核中的锁

翻译:intron @NewSMTH.

  这一章由 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, switchticks setrunqueue, 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_DEF vm86pcb vm86_bioscall
Giant “Giant”(巨锁) MTX_DEF | MTX_RECURSE 几乎可以是任何东西 许多
callout_lock “callout lock”(延时调用锁) MTX_SPIN | MTX_RECURSE callfree, callwheel, nextsoftcheck, proc.p_itcallout, proc.p_slpcallout, softticks, ticks  

2.2 共享互斥锁

  这些锁提供基本的读/写类型的功能,可以被一个正在睡眠的进程持有。 现在它们被统一到lockmgr(9)之中。

表 2-2. 共享互斥锁列表

变量名 保护对象
allproc_lock allproc zombproc pidhashtbl proc.p_list proc.p_hash nextpid
proctree_lock proc.p_children proc.p_sibling

2.3 原子保护变量

  原子保护变量并非由一个显在的锁保护的特殊变量,而是:对这些变量的所有数据访问都要使用特殊的原子操作(atomic(9))。尽管其它的基本同步机制(例如mutex)就是用原子保护变量实现的,但是很少有变量直接使用这种处理方式。

  • mtx.mtx_lock


第3章  内核对象

翻译:intron @NewSMTH.

  内核对象,也就是Kobj,为内核提供了一种面向对象的C语言编程方式。被操作的数据也承载操作它的方法。这使得在不破坏二进制兼容性的前提下,某一个接口能够增/减相应的操作。


3.1 术语

对象

数据集合-数据结构-数据分配的集合

方法

某一种操作──函数

一种或多种方法

接口

一种或多种方法的一个标准集合


3.2 Kobj的工作流程

译者注: 这一小节两段落中原作者的用词有些含混, 请参考我在括号中的注释阅读。

  Kobj工作时,产生方法的描述。每个描述有一个唯一的标识和一个缺省函数。某个描述的地址被用来在一个类的方法表里唯一的标识方法。

  构建一个类,就是要建立一张方法表,并将这张表关联到一个或多个函数(方法);这些函数(方法)都带有方法描述。使用前,类要被编译。编译时要为这个类分配一些缓存。在方法表中的每个方法描述都会被指派一个唯一的标识,除非已经被其它引用它的类在编译时指派了标识。对于每个将要被使用的方法,都会由脚本生成一个函数(方法查找函数),以解析外来参数,并在被查询时给出方法描述的地址。被生成的函数(方法查找函数) 凭着那个方法描述的唯一标识按Hash的方法查找对象的类的缓存。如果这个方法不在缓存中,函数会查找使用类的方法表。如果这个方法被找到了,类里的相关函数(也就是某个方法的实现代码)就会被使用。否则,这个方法描述的缺省函数将被使用。

  这些过程可被表示如下:

对象->缓存<->类

3.3 使用Kobj

3.3.1 结构

struct kobj_method

3.3.2 函数

void kobj_class_compile(kobj_class_t cls);
void kobj_class_compile_static(kobj_class_t cls, kobj_ops_t ops);
void kobj_class_free(kobj_class_t cls);
kobj_t kobj_create(kobj_class_t cls, struct malloc_type *mtype, int mflags);
void kobj_init(kobj_t obj, kobj_class_t cls);
void kobj_delete(kobj_t obj, struct malloc_type *mtype);

3.3.3 宏

KOBJ_CLASS_FIELDS
KOBJ_FIELDS
DEFINE_CLASS(name, methods, size)
KOBJMETHOD(NAME, FUNC)

3.3.4 头文件

<sys/param.h>
<sys/kobj.h>

3.3.5 建立一个接口的模板

  使用Kobj的第一步是建立一个接口。建立接口包括建立模板的工作。 建立模板可用脚本src/sys/kern/makeobjops.pl完成,它会产生申明方法的头文件和代码,脚本还会生成方法查找函数。

  在这个模板中如下关键词会被使用: #include, INTERFACE, CODE, METHOD, STATICMETHOD, 和 DEFAULT.

  #include语句的整行内容将被一字不差的复制到被生成的代码文件的头部。

  例如:

#include <sys/foo.h>

  关键词INTERFACE用来定义接口名。这个名字将与每个方法名接合在一起,形成 [interface name]_[method name]。 语法是:INTERFACE [接口名];

  例如:

INTERFACE foo;

  关键词CODE会将它的参数一字不差的复制到代码文件中。语法是CODE { [任何代码] };

  例如:

CODE {
    struct foo * foo_alloc_null(struct bar *)
    {
        return NULL;
}
};

  关键词METHOD用来描述一个方法。语法是: METHOD [返回值类型] [方法名] { [对象 [, 参数若干]] };

  例如:

METHOD int bar {
    struct object *;
    struct foo *;
    struct bar;
};

  关键词DEFAULT跟在关键词METHOD之后, 是对关键词METHOD的补充。它给这个方法补充上缺省函数。语法是: METHOD [返回值类型] [方法名] { [对象; [其它参数]] }DEFAULT [缺省函数];

  例如:

METHOD int bar {
    struct object *;
    struct foo *;
    int bar;
} DEFAULT foo_hack;

  关键词STATICMETHOD类似关键词METHOD。 对于每个Kobj对象,一般其头部都有一些Kobj专有的数据。 METHOD定义的方法就假设这些专有数据位于对象头部;假如对象头部没有这些专有数据,这些方法对这个对象的访问就可能出错。 而STATICMETHOD定义的对象可以不受这个限制:这样描述出的方法,其操作的数据不由这个类的某个对象实例给出,而是全都由调用这个方法时的操作数(译者注:即参数)给出。这也对于在某个类的方法表之外调用这个方法有用。

译者注: 这一段的语言与原文相比调整很大。 静态方法是不依赖于对象实例的方法。参看C++类中的“静态函数”的概念。



  其它完整的例子:

src/sys/kern/bus_if.m
src/sys/kern/device_if.m

3.3.6 建立一个类

  使用Kobj的第二步是建立一个类。一个类的组有名字、方法表;假如使用了Kobj的“对象管理工具”(Object Handling Facilities),类中还包含对象的大小。建立类时使用宏DEFINE_CLASS()。建立方法表时,须建立一个kobj_method_t数组,用NULL项结尾。 每个非NULL项可用宏KOBJMETHOD()建立。

  例如:

DEFINE_CLASS(fooclass, foomethods, sizeof(struct foodata));

kobj_method_t foomethods[] = {
    KOBJMETHOD(bar_doo, foo_doo),
    KOBJMETHOD(bar_foo, foo_foo),
    { NULL, NULL}
};

  类须被“编译”。根据该类被初始化时系统的状态,将要用到一个静态分配的缓存和“操作数表”(ops table,译者注:即“参数表”)。这些操作可通过声明一个结构体 struct kobj_ops并使用 kobj_class_compile_static(),或是只使用kobj_class_compile()来完成。


3.3.7 建立一个对象

  使用Kobj的第三步是定义对象。Kobj对象建立程序假定Kobj 专有数据在一个对象的头部。如果不是如此,应当先自行分配对象, 再使用kobj_init()初始化对象中的Kobj专有数据; 其实可以使用kobj_create()分配对象, 并自动初始化对象中的Kobj专有内容。kobj_init() 也可以用来改变一个对象所使用的类。

  将Kobj的数据集成到对象中要使用宏KOBJ_FIELDS。

  例如

struct foo_data {
    KOBJ_FIELDS;
    foo_foo;
    foo_bar;
};

3.3.8 调用方法

  使用Kobj的最后一部就是通过生成的函数调用对象类中的方法。调用时,接口名与方法名用'_'接合,而且全部使用大写字母。

  例如,接口名为foo,方法为bar,调用就是:

[返回值 = ] FOO_BAR(对象 [, 其它参数]);

3.3.9 善后处理

  当一个用kobj_create()不再需要被使用时,可对这个对象调用kobj_delete()。 当一个类不再需要被使用时,可对这个类调用kobj_class_free()


第4章  Jail子系统

Evan Sarmiento版权 © 2001 Evan Sarmiento翻译:intron @NewSMTH.

  在大多数UNIX®系统中,用户root是万能的。这也就增加了许多危险。如果一个攻击者获得了一个系统中的root,就可以在他的指尖掌握系统中所有的功能。在FreeBSD里,有一些sysctl项削弱了root的权限,这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级。另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是jail(8)Jail将一个运行环境的文件树根切换到某一特定位置,并且对这样环境中叉分生成的进程做出限制。例如,一个被jail控制的进程不能影响这个jail之外的进程、不能使用一些特定的系统调用,也就不能对主计算机造成破坏。

译者注: 英文单词“jail”的中文意思是“囚禁、监禁”。



  Jail已经成为一种新型的安全模型。人们可以在jail中运行各种可能很脆弱的服务器程序,如Apache、BIND和sendmail。这样一来,即使有攻击者取得了Jail中的root,这最多让人们皱皱眉头,而不会使人们惊慌失措。 本文聚焦Jail的内部原理(源代码),同时对于改进现役的jail代码提出建议。如果你正在寻找设置 Jail的指南性文档,我建议你阅读我的另一篇文章, 发表在Sys Admin Magazine, May 2001, 《Securing FreeBSD using Jail》。


4.1 Jail的系统结构

  Jail由两部分组成:用户级程序, 也就是jail(8);还有在内核中Jail的实现代码:jail(2) 系统调用和相关的约束。我将讨论用户级程序和Jail在内核中的实现原理。


4.1.1 用户级代码

  jail的用户级源代码在/usr/src/usr.sbin/jail,由一个文件jail.c组成。这个程序有这些参数:jail的路径,主机名,IP地址,还有需要执行的命令。


4.1.1.1 数据结构

  在jail.c中,我将最先关注的是一个重要结构体 struct jail j的申明;结构类型的申明包含在 /usr/include/sys/jail.h之中。

  jail结构的定义是:

/usr/include/sys/jail.h: 

struct jail {
        u_int32_t       version;
        char            *path;
        char            *hostname;
        u_int32_t       ip_number;
};

  正如你能看见的,传送给命令jail(8)的每个参数都在这里有对应的一项。事实上,当命令jail(8)被执行时,这些参数才由命令行真正传入:

/usr/src/usr.sbin/jail.c
j.version = 0; 
j.path = argv[1];
j.hostname = argv[2];

4.1.1.2 网络

  传给jail(8)的参数中有一个是IP地址。这是在网络上访问jail时的地址。 jail(8)将IP地址翻译成网络字节顺序,并存入j (jail类型的结构体)。

/usr/src/usr.sbin/jail/jail.c:
struct in.addr in; 
... 
i = inet_aton(argv[3], &in); 
... 
j.ip_number = ntohl(in.s_addr);

  函数inet_aton(3)“将指定的字符串当成一个Internet地址,并将其转存到指定的结构体中”。inet_aton设定了结构体in, 之后in中的内容再用ntohl()翻译成主机字节顺序。


4.1.1.3 囚禁进程

  最后,用户级程序囚禁进程,执行指定的命令。现在Jail自身变成了一个被囚禁的进程,叉分生成一个子进程。这个子进程用execv(3)执行用户指定的命令。

/usr/src/sys/usr.sbin/jail/jail.c
i = jail(&j); 
... 
i = execv(argv[4], argv + 4);

  正如你能看见的,函数jail被调用,参数是结构体jail中被填入数据项,而如前所述,这些数据项又来自jail(8)的命令行参数。最后,执行了用户指定的命令。下面我将开始讨论Jail在内核中的实现。


4.1.2 相关的内核源代码

  现在我们来看文件/usr/src/sys/kern/kern_jail.c。在这里定义了jail的系统调用、相关的sysctl项,还有网络函数。


4.1.2.1 sysctl项

  在kern_jail.c里定义了如下sysctl项:

/usr/src/sys/kern/kern_jail.c:

int     jail_set_hostname_allowed = 1;
SYSCTL_INT(_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW,
    &jail_set_hostname_allowed, 0,
    "Processes in jail can set their hostnames");
    /* Jail中的进程可设定自身的主机名 */

int     jail_socket_unixiproute_only = 1;
SYSCTL_INT(_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW,
    &jail_socket_unixiproute_only, 0,
    "Processes in jail are limited to creating UNIX/IPv4/route sockets only
");
    /* Jail中的进程被限制只能建立UNIX套接字、IPv4套接字、路由套接字 */

int     jail_sysvipc_allowed = 0;
SYSCTL_INT(_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW,
    &jail_sysvipc_allowed, 0,
    "Processes in jail can use System V IPC primitives");
    /* Jail中的进程可以使用System V进程间通讯原语 */

  这些sysctl项中的每一个都可以用命令sysctl访问。在整个内核中,这些sysctl项按名称标识。例如,上述第一个sysctl项的名字是 jail.set.hostname.allowed.


4.1.2.2 jail(2)系统调用

  像所有的系统调用一样,系统调用jail(2)带有两个参数, struct proc *pstruct jail_args *uapp是一个指向proc结构体的指针,描述调用这个系统调用的进程。此时,uap指向一个结构体,这个结构体指定了从用户级程序 jail.c要传送给jail(2)的参数。在前面我讲述用户级程序时,你已经看见一个jail结构体被作为参数传送给系统调用 jail(2)

/usr/src/sys/kern/kern_jail.c:
int
jail(p, uap)
        struct proc *p;
        struct jail_args /* {
                syscallarg(struct jail *) jail;
        } */ *uap;

  uap->jail包含了传递给系统调用的jail结构体。然后,系统调用使用copyin()将jail结构体复制到内核内存空间中。 copyin()有三个参数:要复制进内核内存空间的数据 uap->jail,在内核内存空间存放数据的j,以及数据的大小。Jail结构体uap->jail被复制进内核内存空间,并被存放在另一个jail结构体j里。

/usr/src/sys/kern/kern_jail.c: 
error = copyin(uap->jail, &j, sizeof j);

  在jail.h中定义了另一个重要的结构体型prison(pr)。结构体prison只被用在内核空间中。系统调用jail(2)把jail结构体中的所有内容复制到prison结构体中。这里是prison结构体的定义:

/usr/include/sys/jail.h:
struct prison {
        int             pr_ref;
        char            pr_host[MAXHOSTNAMELEN];
        u_int32_t       pr_ip;
        void            *pr_linux;
};

  然后,系统调用jail()为一个prison结构体分配一块内存,由一个指针指向这块内存,再将数据复制进去。

/usr/src/sys/kern/kern_jail.c:
 MALLOC(pr, struct prison *, sizeof *pr , M_PRISON, M_WAITOK);
 bzero((caddr_t)pr, sizeof *pr);
 error = copyinstr(j.hostname, &pr->pr_host, sizeof pr->pr_host, 0);
 if (error) 
         goto bail;

  最后,系统调用jail将切换文件系统逻辑根(chroot)至指定路径。函数chroot()有两个参数。第一个是p, 表示调用它的进程,第二个是指向结构体chroot的指针。结构体chroot包含了新的文件系统逻辑根。正如你看见的,结构体jail中指定的路径被复制到结构体chroot中, 并在后续操作中被使用。

/usr/src/sys/kern/kern_jail.c:
ca.path = j.path; 
error = chroot(p, &ca);

  这随后的三行在源代码中非常重要,因为他们指定了内核如何将一个进程判别为被囚禁的进程。在UNIX系统中,每一个进程都由它自己的proc结构体描述。 你可以在/usr/include/sys/proc.h中看见整个proc结构体。例如,在任何系统调用中,参数p实际上是个指向进程的proc结构体的指针,正如前面所说的那样。结构体proc包含的成员可以描述所有者的身份 (p_cred),进程资源限制(p_limit),等等。在进程结构体的定义中,还有一个指向prison结构体的指针 (p_prison)。

/usr/include/sys/proc.h: 
struct proc { 
...
struct prison *p_prison; 
...
};

  在kern_jail.c中,函数然后复制pr结构体到 p->p_prison中。pr结构体里填充了来自原始jail 结构体中的所有信息。随后,将p->p_flag与恒量 P_JAILED进行按位或运算,这指明调用进程现在被认为是被囚禁的。每个进程的父进程,都曾在Jail中进行了叉分(fork)。这父进程正是程序jail本身, 它调用了jail(2)系统调用。当其它程序通过execve()执行时,就从父进程那里继承proc结构体,因而其p->p_flag 中Jail的标志位被置位,并且p->p_prison 结构体中被填有内容。

/usr/src/sys/kern/kern_jail.c
p->p.prison = pr; 
p->p.flag |= P.JAILED;

  当一个进程被从其父进程叉分来的时候,系统调用fork(2) 将用不同的方式处理被囚禁的进程。在系统调用fork中用到两个指向 proc结构体的指针p1p2p1指向父进程的 proc结构体,p2 指向子进程的尚未被填充的proc结构体。 在结构体间复制完所有相关数据之后,fork(2) 检查p2指向的结构体成员 p_prison是否已被填充。如果已被填充, 就将pr.ref的值增加1, 并给子进程的p_flag设上Jail标记。

/usr/src/sys/kern/kern_fork.c:
if (p2->p_prison) {
        p2->p_prison->pr_ref++;
    p2->p_flag |= P_JAILED;
}

4.2 系统对被囚禁程序的限制

  在整个内核中,有一系列对被囚禁程序的约束措施。通常,这些约束只对被囚禁的程序有效。如果这些程序试图突破这些约束,相关的函数将出错返回。例如:

if (p->p_prison) 
        return EPERM;

4.2.1 SysV进程间通信(IPC)

  System V进程间通信(IPC)是通过消息实现的。每个进程都可以向其它进程发送消息,告诉对方该做什么。 处理消息的函数是:msgsys, msgctl, msgget, msgsendmsgrcv。前面我提到一些sysctl项开关可以影响Jail的行为,其中有一个是jail_sysvipc_allowed。在大多数系统上,这个sysctl项被设成0。如此它被设为1,它将使Jail完全失去意义:在Jail中有权限的进程就可以影响Jail环境外的进程了。消息与信号的区别是:消息仅由一个信号编号组成。

  /usr/src/sys/kern/sysv_msg.c:

  • msgget(3): msgget返回(也可能创建)一个消息描述符, 以指派一个在其它系统调用中使用的消息队列。

  • msgctl(3): 通过这个函数, 一个进程可以查询一个消息描述符的状态。

  • msgsnd(3): msgsnd向一个进程发送一条消息。

  • msgrcv(3): 进程用这个函数接收消息。

  在这些系统调用的代码中,都有这样一个条件判断:

/usr/src/sys/kern/sysv msg.c:
if (!jail.sysvipc.allowed && p->p_prison != NULL)
        return (ENOSYS);

  信号量系统调用使得进程可以通过一系列操作实现同步。信号量为进程锁定资源提供了又一种途径。然而,进程将为正在被使用的信号量进入等待状态,一直休眠到资源被释放。在Jail中如下的信号量系统调用将会失效: semsys, semget, semctlsemop

  /usr/src/sys/kern/sysv_sem.c:

  • semctl(2)(id, num, cmd, arg): Semctl对在信号量队列中用id标识的信号量执行cmd指定的命令。

  • semget(2)(key, nsems, flag): Semget建立一个对应于key的信号量数组

    参数Key和flag与msgget()的意义相同。

  • semop(2)(id, ops, num): Semop在结构体数组ops中对id标识的信号量完成一系列操作。

  System V IPC使进程间可以共享内存。进程之间可以通过它们虚拟地址空间的共享部分以及相关数据读写操作直接通讯。这些系统调用在Jail环境中将会失效: shmdt, shmat, oshmctl, shmctl, shmget, 和shmsys

  /usr/src/sys/kern/sysv shm.c:

  • shmctl(2)(id, cmd, buf): shmctl对id标识的共享内存区域做各种各样的控制。

  • shmget(2)(key, size, flag): shmget建立/打开size字节的共享内存区域。

  • shmat(2)(id, addr, flag): shmat将id标识的共享内存区域指派到进程的地址空间里。

  • shmdt(2)(addr): shmdt取消共享内存区域的地址指派。


4.2.2 套接字

  Jail以一种特殊的方式处理socket(2)系统调用和相关的低级套接字函数。为了决定一个套接字是否允许被创建,它先检查sysctl项 jail.socket.unixiproute.only是否被设置为1。如果被设为1,套接字建立时将只能指定这些协议族: PF_LOCAL, PF_INET, PF_ROUTE。否则,socket(2)将会返回出错。

/usr/src/sys/kern/uipc_socket.c:
int socreate(dom, aso, type, proto, p) 
... 
register struct protosw *prp; 
... 
{
        if (p->p_prison && jail_socket_unixiproute_only &&
            prp->pr_domain->dom_family != PR_LOCAL && prp->pr_domain->dom_family != PF_INET 
            && prp->pr_domain->dom_family != PF_ROUTE)
                return (EPROTONOSUPPORT); 
...
}

4.2.3 Berkeley包过滤器

  Berkeley包过滤器提供了一个与协议无关的,直接通向数据链路层的低级接口。 函数bpfopen()打开一个以太网设备。代码中有一个条件判断禁止所有被囚禁的进程打开Berkeley包过滤器设备。

/usr/src/sys/net/bpf.c: 
static int bpfopen(dev, flags, fmt, p) 
... 
{
        if (p->p_prison) 
                return (EPERM);
...
}

4.2.4 网络协议

  网络协议TCP, UDP, IP和ICMP很常见。IP和ICMP处于同一协议层次:第二层,网络层。当参数nam被设置时,有一些限制措施会防止被囚禁的程序绑定到一些网络接口上。 nam是一个指向sockaddr结构体的指针,描述可以绑定服务的地址。一个更确切的定义:sockaddr“是一个模板,包含了地址的标识符和地址的长度”[2]。在函数中,pcbbind, sin 里有一个指向sockaddr的指针。结构体包含了套接字可以绑定的端口、地址、长度、协议族。这就禁止了在Jail中的进程指定协议族。

/usr/src/sys/kern/netinet/in_pcb.c: 
int in.pcbbind(int, nam, p) 
...
        struct sockaddr *nam; 
        struct proc *p; 
{
        ... 
        struct sockaddr.in *sin; 
        ... 
        if (nam) {
                sin = (struct sockaddr.in *)nam; 
                ... 
                if (sin->sin_addr.s_addr != INADDR_ANY) 
                       if (prison.ip(p, 0, &sin->sin.addr.s_addr)) 
                              return (EINVAL); 
                ....
        }
...
}

  你也许想知道函数prison_ip()做什么。 prison.ip有三个参数,当前进程(用p表示),一些标志(flag)和一个IP地址。当这个IP地址属于一个Jail时,返回1;否则返回0。正如你从代码中看见的,如果,那个IP地址真的属于一个Jail,就不再允许向一个网络接口绑定协议。

/usr/src/sys/kern/kern_jail.c:
int prison_ip(struct proc *p, int flag, u_int32_t *ip) {
        u_int32_t tmp;

       if (!p->p_prison) 
              return (0); 
       if (flag) 
              tmp = *ip; 
       else tmp = ntohl (*ip); 

       if (tmp == INADDR_ANY) {
              if (flag) 
                     *ip = p->p_prison->pr_ip; 
              else *ip = htonl(p->p_prison->pr_ip); 
              return (0); 
       }

       if (p->p_prison->pr_ip != tmp) 
              return (1); 
       return (0); 
}

  被囚禁的用户不能对一个不属于这个Jail的IP地址绑定服务。 这个限制在函数in_pcbbind中也有所体现:

/usr/src/sys/net inet/in_pcb.c
        if (nam) {
               ... 
               lport = sin->sin.port; 
               ... if (lport) { 
                          ... 
                         if (p && p->p_prison)
                                prison = 1; 
                         if (prison &&
                             prison_ip(p, 0, &sin->sin_addr.s_addr))
                        return (EADDRNOTAVAIL);

4.2.5 文件系统

  如此完全级大于0,即便是root,也不允许在Jail中设置文件标志,如“不可修改”、“添加”、“不可删除”标志。

/usr/src/sys/ufs/ufs/ufs_vnops.c:
int ufs.setattr(ap) 
        ... 
{
        if ((cred->cr.uid == 0) && (p->prison == NULL)) {
            if ((ip->i_flags 
                     & (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) && 
                     securelevel > 0)
               return (EPERM);
}

第5章  SYSINIT框架

翻译:intron @NewSMTH.

  SYSINIT是一个通用的调用排序与分别执行机制的框架。 FreeBSD目前使用它来进行内核的动态初始化。 SYSINIT使得FreeBSD的内核各子系统可以在内核或模块动态加载链接时被重整、添加、删除、替换,这样,内核和模块加载时就不必去修改一个静态的有序初始化安排表甚至重新编译内核。这个体系也使得内核模块 (现在称为KLD可以与内核不同时编译、链接、在引导系统时加载,甚至在系统运行时加载。这些操作是通过 “内核链接器”(kernel linker)和“链接器集合” (linker set)完成的。


5.1 术语

链接器集合(Linker Set)

一种链接方法。这种方法将整个程序源文件中静态申明的数据收集到一个可邻近寻址的数据单元中。


5.2 SYSINIT操作

  SYSINIT要依靠链接器获取遍布整个程序源代码多处申明的静态数据并把它们组成一个彼此相邻的数据块。这种链接方法被称为 “链接器集合”(linker set)。 SYSINIT使用两个链接器集合以维护两个数据集合,包含每个数据条目的调用顺序、函数、一个会被提交给该函数的数据指针。

  SYSINIT按照两类优先级标识对函数排序以便执行。 第一类优先级的标识是子系统的标识,给出SYSINIT分别执行子系统的函数的全局顺序, 定义在<sys/kernel.h>中的枚举 sysinit_sub_id内。第二类优先级标识在子系统中的元素的顺序, 定义在<sys/kernel.h>中的枚举 sysinit_elem_order内。

  有两种时刻需要使用SYSINIT:系统启动或内核模块加载时,系统析构或内核模块卸载时。内核子系统通常在系统启动时使用SYSINIT 的定义项以初始化数据结构。例如,进程调度子系统使用一个SYSINIT 定义项来初始化运行队列数据结构。设备驱动程序应避免直接使用 SYSINIT(),对于总线结构上的物理真实设备应使用 DRIVER_MODULE()调用的函数先侦测设备的存在,如果存在,再进行设备的初始化。这一系统过程中, 会做一些专门针对设备的事情,然后调用SYSINIT()本身。 对于非总线结构一部分的虚设备,应改用DEV_MODULE()