FreeBSD 系统结构手册

FreeBSD文档工程

intron@SMTH、spellar@SMTH、delphij

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

本文档的最新版本可从FreeBSD WWW服务器获得,也可以各种格式和压缩方式从FreeBSD FTP服务器 或众多的 镜像站点 得到。

FreeBSD is a registered trademark of The FreeBSD Foundation.

UNIX is a registered trademark of The Open Group in the US and other countries.

Sun, Sun Microsystems, SunOS, Solaris, and Java are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.

Apple and QuickTime are trademarks of Apple Computer, Inc., registered in the U.S. and other countries.

Macromedia and Flash are trademarks or registered trademarks of Macromedia, Inc. in the United States and/or other countries.

Microsoft, Windows, and Windows Media are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.

PartitionMagic is a registered trademark of PowerQuest Corporation in the United States and/or other countries.

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the FreeBSD Project was aware of the trademark claim, the designations have been followed by the '™' symbol.

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章 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的方法

第I部分. 内核


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

英文原始版Sergey Lyubka.

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       # 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:DPL:type, 设置 P:DPL: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_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;        /* bootdev BIOS unit number, 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 * */
        /* 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));

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
/* 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)
    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

/* 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:
    movl    physfree, %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_<typename>_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_SEL  4   /* Task state process slot zero and up, 任务状态进程 */
#define GLDT_SEL    5   /* LDT - eventually one per process, 每个进程的局部描述符表 */
#define GUSERLDT_SEL    6   /* User LDT, 用户自定义的局部描述符表 */
#define GTGATE_SEL  7   /* Process task switch gate, 进程任务切换关口 */
#define GBIOSLOWMEM_SEL 8   /* BIOS low memory access (must be entry 8), BIOS低端内存访问(入口值必须是8) */
#define GPANIC_SEL  9   /* 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");
#endif

任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时, 任务状态段用来让硬件存储任务现场信息。

局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符, 指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符:

/usr/include/machine/segments.h
#define LSYS5CALLS_SEL  0   /* forced by intel BCS, Intel BCS强制要求的 */
#define LSYS5SIGR_SEL   1
#define L43BSDCALLS_SEL 2   /* notyet, 尚无 */
#define LUCODE_SEL  3
#define LSOL26CALLS_SEL 4   /* Solaris >= 2.6 system call gate, 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++) {

        /* ... 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\"");             \
    __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];       /* 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] =
#ifdef  INIT_PATH
    __XSTRING(INIT_PATH);
#else
    "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall";
#endif

第2章 内核中的锁

这一章由 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章 内核对象

内核对象,也就是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

在大多数 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 *uap. p 是一个指向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, semctl and semop.

/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框架

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


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()


5.3 使用SYSINIT

5.3.1 接口

5.3.1.1 头文件

<sys/kernel.h>

5.3.1.2 宏

SYSINIT(uniquifier, subsystem, order, func, ident)
SYSUNINIT(uniquifier, subsystem, order, func, ident)

5.3.2 启动

SYSINIT()在SYSINIT数据集合中建立一个SYSINIT数据项,以便SYSINIT在系统启动或模块加载时排序并执行其中的函数。SYSINIT()有一个参数uniquifier,SYSINIT用它来标识数据项,随后是子系统顺序号、子系统元素顺序号、待调用函数、传递给函数的数据。所有的函数必须有一个恒量指针参数。

例 5-1. SYSINIT()的例子

#include <sys/kernel.h>

void foo_null(void *unused)
{
        foo_doo();
}
SYSINIT(foo, SI_SUB_FOO, SI_ORDER_FOO, foo_null, NULL);

struct foo foo_voodoo = {
        FOO_VOODOO;
}

void foo_arg(void *vdata)
{
        struct foo *foo = (struct foo *)vdata;
        foo_data(foo);
}
SYSINIT(bar, SI_SUB_FOO, SI_ORDER_FOO, foo_arg, &foo_voodoo);
   

注意,SI_SUB_FOOSI_ORDER_FOO应当分别在上面提到的枚举sysinit_sub_idsysinit_elem_order之中。既可以使用已有的枚举项,也可以将自己的枚举项添加到这两个枚举的定义之中。你可以使用数学表达式微调SYSINIT的执行顺序。以下的例子示例了一个需要刚好要在内核参数调整的SYSINIT之前执行的SYSINIT。

例 5-2. 调整SYSINIT()顺序的例子

static void
mptable_register(void *dummy __unused)
{

    apic_register_enumerator(&mptable_enumerator);
}

SYSINIT(mptable_register, SI_SUB_TUNABLES - 1, SI_ORDER_FIRST,
    mptable_register, NULL);

5.3.3 析构

SYSUNINIT()的行为与SYSINIT()的相当,只是它将数据项填加至SYSINIT的析构数据集合。

例 5-3. SYSUNINIT()的例子

#include <sys/kernel.h>

void foo_cleanup(void *unused)
{
        foo_kill();
}
SYSUNINIT(foobar, SI_SUB_FOO, SI_ORDER_FOO, foo_cleanup, NULL);

struct foo_stack foo_stack = {
        FOO_STACK_VOODOO;
}

void foo_flush(void *vdata)
{
}
SYSUNINIT(barfoo, SI_SUB_FOO, SI_ORDER_FOO, foo_flush, &foo_stack);
   

第6章 The TrustedBSD MAC Framework

Chris Costello 和 Robert Watson.

6.1 MAC Documentation Copyright

This documentation was developed for the FreeBSD Project by Chris Costello at Safeport Network Services and Network Associates Laboratories, the Security Research Division of Network Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035 (“CBOSS”), as part of the DARPA CHATS research program.

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 NETWORKS ASSOCIATES TECHNOLOGY, INC "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 NETWORKS ASSOCIATES TECHNOLOGY, INC 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.


6.2 Synopsis

FreeBSD includes experimental support for several mandatory access control policies, as well as a framework for kernel security extensibility, the TrustedBSD MAC Framework. The MAC Framework is a pluggable access control framework, permitting new security policies to be easily linked into the kernel, loaded at boot, or loaded dynamically at run-time. The framework provides a variety of features to make it easier to implement new security policies, including the ability to easily tag security labels (such as confidentiality information) onto system objects.

This chapter introduces the MAC policy framework and provides documentation for a sample MAC policy module.


6.3 Introduction

The TrustedBSD MAC framework provides a mechanism to allow the compile-time or run-time extension of the kernel access control model. New system policies may be implemented as kernel modules and linked to the kernel; if multiple policy modules are present, their results will be composed. The MAC Framework provides a variety of access control infrastructure services to assist policy writers, including support for transient and persistent policy-agnostic object security labels. This support is currently considered experimental.

This chapter provides information appropriate for developers of policy modules, as well as potential consumers of MAC-enabled environments, to learn about how the MAC Framework supports access control extension of the kernel.


6.4 Policy Background

Mandatory Access Control (MAC), refers to a set of access control policies that are mandatorily enforced on users by the operating system. MAC policies may be contrasted with Discretionary Access Control (DAC) protections, by which non-administrative users may (at their discretion) protect objects. In traditional UNIX systems, DAC protections include file permissions and access control lists; MAC protections include process controls preventing inter-user debugging and firewalls. A variety of MAC policies have been formulated by operating system designers and security researches, including the Multi-Level Security (MLS) confidentiality policy, the Biba integrity policy, Role-Based Access Control (RBAC), Domain and Type Enforcement (DTE), and Type Enforcement (TE). Each model bases decisions on a variety of factors, including user identity, role, and security clearance, as well as security labels on objects representing concepts such as data sensitivity and integrity.

The TrustedBSD MAC Framework is capable of supporting policy modules that implement all of these policies, as well as a broad class of system hardening policies, which may use existing security attributes, such as user and group IDs, as well as extended attributes on files, and other system properties. In addition, despite the name, the MAC Framework can also be used to implement purely discretionary policies, as policy modules are given substantial flexibility in how they authorize protections.


6.5 MAC Framework Kernel Architecture

The TrustedBSD MAC Framework permits kernel modules to extend the operating system security policy, as well as providing infrastructure functionality required by many access control modules. If multiple policies are simultaneously loaded, the MAC Framework will usefully (for some definition of useful) compose the results of the policies.


6.5.1 Kernel Elements

The MAC Framework contains a number of kernel elements:

  • Framework management interfaces

  • Concurrency and synchronization primitives.

  • Policy registration

  • Extensible security label for kernel objects

  • Policy entry point composition operators

  • Label management primitives

  • Entry point API invoked by kernel services

  • Entry point API to policy modules

  • Entry points implementations (policy life cycle, object life cycle/label management, access control checks).

  • Policy-agnostic label-management system calls

  • mac_syscall() multiplex system call

  • Various security policies implemented as MAC policy modules


6.5.2 Framework Management Interfaces

The TrustedBSD MAC Framework may be directly managed using sysctl's, loader tunables, and system calls.

In most cases, sysctl's and loader tunables of the same name modify the same parameters, and control behavior such as enforcement of protections relating to various kernel subsystems. In addition, if MAC debugging support is compiled into the kernel, several counters will be maintained tracking label allocation. It is generally advisable that per-subsystem enforcement controls not be used to control policy behavior in production environments, as they broadly impact the operation of all active policies. Instead, per-policy controls should be preferred, as they provide greater granularity and greater operational consistency for policy modules.

Loading and unloading of policy modules is performed using the system module management system calls and other system interfaces, including boot loader variables; policy modules will have the opportunity to influence load and unload events, including preventing undesired unloading of the policy.


6.5.3 Policy List Concurrency and Synchronization

As the set of active policies may change at run-time, and the invocation of entry points is non-atomic, synchronization is required to prevent loading or unloading of policies while an entry point invocation is in progress, freezing the set of active policies for the duration. This is accomplished by means of a framework busy count: whenever an entry point is entered, the busy count is incremented; whenever it is exited, the busy count is decremented. While the busy count is elevated, policy list changes are not permitted, and threads attempting to modify the policy list will sleep until the list is not busy. The busy count is protected by a mutex, and a condition variable is used to wake up sleepers waiting on policy list modifications. One side effect of this synchronization model is that recursion into the MAC Framework from within a policy module is permitted, although not generally used.

Various optimizations are used to reduce the overhead of the busy count, including avoiding the full cost of incrementing and decrementing if the list is empty or contains only static entries (policies that are loaded before the system starts, and cannot be unloaded). A compile-time option is also provided which prevents any change in the set of loaded policies at run-time, which eliminates the mutex locking costs associated with supporting dynamically loaded and unloaded policies as synchronization is no longer required.

As the MAC Framework is not permitted to block in some entry points, a normal sleep lock cannot be used; as a result, it is possible for the load or unload attempt to block for a substantial period of time waiting for the framework to become idle.


6.5.4 Label Synchronization

As kernel objects of interest may generally be accessed from more than one thread at a time, and simultaneous entry of more than one thread into the MAC Framework is permitted, security attribute storage maintained by the MAC Framework is carefully synchronized. In general, existing kernel synchronization on kernel object data is used to protect MAC Framework security labels on the object: for example, MAC labels on sockets are protected using the existing socket mutex. Likewise, semantics for concurrent access are generally identical to those of the container objects: for credentials, copy-on-write semantics are maintained for label contents as with the remainder of the credential structure. The MAC Framework asserts necessary locks on objects when invoked with an object reference. Policy authors must be aware of these synchronization semantics, as they will sometimes limit the types of accesses permitted on labels: for example, when a read-only reference to a credential is passed to a policy via an entry point, only read operations are permitted on the label state attached to the credential.


6.5.5 Policy Synchronization and Concurrency

Policy modules must be written to assume that many kernel threads may simultaneously enter one more policy entry points due to the parallel and preemptive nature of the FreeBSD kernel. If the policy module makes use of mutable state, this may require the use of synchronization primitives within the policy to prevent inconsistent views on that state resulting in incorrect operation of the policy. Policies will generally be able to make use of existing FreeBSD synchronization primitives for this purpose, including mutexes, sleep locks, condition variables, and counting semaphores. However, policies should be written to employ these primitives carefully, respecting existing kernel lock orders, and recognizing that some entry points are not permitted to sleep, limiting the use of primitives in those entry points to mutexes and wakeup operations.

When policy modules call out to other kernel subsytems, they will generally need to release any in-policy locks in order to avoid violating the kernel lock order or risking lock recursion. This will maintain policy locks as leaf locks in the global lock order, helping to avoid deadlock.


6.5.6 Policy Registration

The MAC Framework maintains two lists of active policies: a static list, and a dynamic list. The lists differ only with regards to their locking semantics: an elevated reference count is not required to make use of the static list. When kernel modules containing MAC Framework policies are loaded, the policy module will use SYSINIT to invoke a registration function; when a policy module is unloaded, SYSINIT will likewise invoke a de-registration function. Registration may fail if a policy module is loaded more than once, if insufficient resources are available for the registration (for example, the policy might require labeling and insufficient labeling state might be available), or other policy prerequisites might not be met (some policies may only be loaded prior to boot). Likewise, de-registration may fail if a policy is flagged as not unloadable.


6.5.7 Entry Points

Kernel services interact with the MAC Framework in two ways: they invoke a series of APIs to notify the framework of relevant events, and they provide a policy-agnostic label structure pointer in security-relevant objects. The label pointer is maintained by the MAC Framework via label management entry points, and permits the Framework to offer a labeling service to policy modules through relatively non-invasive changes to the kernel subsystem maintaining the object. For example, label pointers have been added to processes, process credentials, sockets, pipes, vnodes, Mbufs, network interfaces, IP reassembly queues, and a variety of other security-relevant structures. Kernel services also invoke the MAC Framework when they perform important security decisions, permitting policy modules to augment those decisions based on their own criteria (possibly including data stored in security labels). Most of these security critical decisions will be explicit access control checks; however, some affect more general decision functions such as packet matching for sockets and label transition at program execution.


6.5.8 Policy Composition

When more than one policy module is loaded into the kernel at a time, the results of the policy modules will be composed by the framework using a composition operator. This operator is currently hard-coded, and requires that all active policies must approve a request for it to return success. As policies may return a variety of error conditions (success, access denied, object doesn't exist, ...), a precedence operator selects the resulting error from the set of errors returned by policies. In general, errors indicating that an object does not exist will be preferred to errors indicating that access to an object is denied. While it is not guaranteed that the resulting composition will be useful or secure, we've found that it is for many useful selections of policies. For example, traditional trusted systems often ship with two or more policies using a similar composition.


6.5.9 Labeling Support

As many interesting access control extensions rely on security labels on objects, the MAC Framework provides a set of policy-agnostic label management system calls covering a variety of user-exposed objects. Common label types include partition identifiers, sensitivity labels, integrity labels, compartments, domains, roles, and types. By policy agnostic, we mean that policy modules are able to completely define the semantics of meta-data associated with an object. Policy modules participate in the internalization and externalization of string-based labels provides by user applications, and can expose multiple label elements to applications if desired.

In-memory labels are stored in slab-allocated struct label, which consists of a fixed-length array of unions, each holding a void * pointer and a long. Policies registering for label storage will be assigned a "slot" identifier, which may be used to dereference the label storage. The semantics of the storage are left entirely up to the policy module: modules are provided with a variety of entry points associated with the kernel object life cycle, including initialization, association/creation, and destruction. Using these interfaces, it is possible to implement reference counting and other storage models. Direct access to the object structure is generally not required by policy modules to retrieve a label, as the MAC Framework generally passes both a pointer to the object and a direct pointer to the object's label into entry points. The primary exception to this rule is the process credential, which must be manually dereferenced to access the credential label. This may change in future revisions of the MAC Framework.

Initialization entry points frequently include a sleeping disposition flag indicating whether or not an initialization is permitted to sleep; if sleeping is not permitted, a failure may be returned to cancel allocation of the label (and hence object). This may occur, for example, in the network stack during interrupt handling, where sleeping is not permitted, or while the caller holds a mutex. Due to the performance cost of maintaining labels on in-flight network packets (Mbufs), policies must specifically declare a requirement that Mbuf labels be allocated. Dynamically loaded policies making use of labels must be able to handle the case where their init function has not been called on an object, as objects may already exist when the policy is loaded. The MAC Framework guarantees that uninitialized label slots will hold a 0 or NULL value, which policies may use to detect uninitialized values. However, as allocation of Mbuf labels is conditional, policies must also be able to handle a NULL label pointer for Mbufs if they have been loaded dynamically.

In the case of file system labels, special support is provided for the persistent storage of security labels in extended attributes. Where available, extended attribute transactions are used to permit consistent compound updates of security labels on vnodes--currently this support is present only in the UFS2 file system. Policy authors may choose to implement multilabel file system object labels using one (or more) extended attributes. For efficiency reasons, the vnode label (v_label) is a cache of any on-disk label; policies are able to load values into the cache when the vnode is instantiated, and update the cache as needed. As a result, the extended attribute need not be directly accessed with every access control check.

注意: Currently, if a labeled policy permits dynamic unloading, its state slot cannot be reclaimed, which places a strict (and relatively low) bound on the number of unload-reload operations for labeled policies.


6.5.10 System Calls

The MAC Framework implements a number of system calls: most of these calls support the policy-agnostic label retrieval and manipulation APIs exposed to user applications.

The label management calls accept a label description structure, struct mac, which contains a series of MAC label elements. Each element contains a character string name, and character string value. Each policy will be given the chance to claim a particular element name, permitting policies to expose multiple independent elements if desired. Policy modules perform the internalization and externalization between kernel labels and user-provided labels via entry points, permitting a variety of semantics. Label management system calls are generally wrapped by user library functions to perform memory allocation and error handling, simplifying user applications that must manage labels.

The following MAC-related system calls are present in the FreeBSD kernel:

  • mac_get_proc() may be used to retrieve the label of the current process.

  • mac_set_proc() may be used to request a change in the label of the current process.

  • mac_get_fd() may be used to retrieve the label of an object (file, socket, pipe, ...) referenced by a file descriptor.

  • mac_get_file() may be used to retrieve the label of an object referenced by a file system path.

  • mac_set_fd() may be used to request a change in the label of an object (file, socket, pipe, ...) referenced by a file descriptor.

  • mac_set_file() may be used to request a change in the label of an object referenced by a file system path.

  • mac_syscall() permits policy modules to create new system calls without modifying the system call table; it accepts a target policy name, operation number, and opaque argument for use by the policy.

  • mac_get_pid() may be used to request the label of another process by process id.

  • mac_get_link() is identical to mac_get_file(), only it will not follow a symbolic link if it is the final entry in the path, so may be used to retrieve the label on a symlink.

  • mac_set_link() is identical to mac_set_file(), only it will not follow a symbolic link if it is the final entry in a path, so may be used to manipulate the label on a symlink.

  • mac_execve() is identical to the execve() system call, only it also accepts a requested label to set the process label to when beginning execution of a new program. This change in label on execution is referred to as a "transition".

  • mac_get_peer(), actually implemented via a socket option, retrieves the label of a remote peer on a socket, if available.

In addition to these system calls, the SIOCSIGMAC and SIOCSIFMAC network interface ioctls permit the labels on network interfaces to be retrieved and set.


6.6 MAC Policy Architecture

Security policies are either linked directly into the kernel, or compiled into loadable kernel modules that may be loaded at boot, or dynamically using the module loading system calls at runtime. Policy modules interact with the system through a set of declared entry points, providing access to a stream of system events and permitting the policy to influence access control decisions. Each policy contains a number of elements:

  • Optional configuration parameters for policy.

  • Centralized implementation of the policy logic and parameters.

  • Optional implementation of policy life cycle events, such as initialization and destruction.

  • Optional support for initializing, maintaining, and destroying labels on selected kernel objects.

  • Optional support for user process inspection and modification of labels on selected objects.

  • Implementation of selected access control entry points that are of interest to the policy.

  • Declaration of policy identity, module entry points, and policy properties.


6.6.1 Policy Declaration

Modules may be declared using the MAC_POLICY_SET() macro, which names the policy, provides a reference to the MAC entry point vector, provides load-time flags determining how the policy framework should handle the policy, and optionally requests the allocation of label state by the framework.

static struct mac_policy_ops mac_policy_ops =
{                                   
        .mpo_destroy = mac_policy_destroy,
        .mpo_init = mac_policy_init,
        .mpo_init_bpfdesc_label = mac_policy_init_bpfdesc_label,  
        .mpo_init_cred_label = mac_policy_init_label,
/* ... */
        .mpo_check_vnode_setutimes = mac_policy_check_vnode_setutimes,
        .mpo_check_vnode_stat = mac_policy_check_vnode_stat,
        .mpo_check_vnode_write = mac_policy_check_vnode_write,
};

The MAC policy entry point vector, mac_policy_ops in this example, associates functions defined in the module with specific entry points. A complete listing of available entry points and their prototypes may be found in the MAC entry point reference section. Of specific interest during module registration are the .mpo_destroy and .mpo_init entry points. .mpo_init will be invoked once a policy is successfully registered with the module framework but prior to any other entry points becoming active. This permits the policy to perform any policy-specific allocation and initialization, such as initialization of any data or locks. .mpo_destroy will be invoked when a policy module is unloaded to permit releasing of any allocated memory and destruction of locks. Currently, these two entry points are invoked with the MAC policy list mutex held to prevent any other entry points from being invoked: this will be changed, but in the mean time, policies should be careful about what kernel primitives they invoke so as to avoid lock ordering or sleeping problems.

The policy declaration's module name field exists so that the module may be uniquely identified for the purposes of module dependencies. An appropriate string should be selected. The full string name of the policy is