Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
本章将讨论有关在 PCI 总线上为设备编写设备驱动程序的 FreeBSD 机制。
这里的信息是关于 PCI 总线代码如何迭代未连接的设备,并查看新加载的 kld 是否会附加到它们中的任何一个。
如果您将上述源文件和 Makefile 放入一个目录,您可以运行 make 来编译样例驱动程序。此外,您可以运行 make load 将驱动程序加载到当前运行的内核中,运行 make unload 来在加载后卸载驱动程序。
PCI 系统架构,第四版,Tom Shanley 等著
FreeBSD 提供了一种从父总线请求资源的面向对象机制。 几乎所有设备都将是某种总线(PCI、ISA、USB、SCSI 等)的子成员,这些设备需要从其父总线获取资源(如内存段、中断线或 DMA 通道)。
要对 PCI 设备执行任何特别有用的操作,您需要从 PCI 配置空间获取基址寄存器(BARs)。获取 BAR 的 PCI 特定细节在 bus_alloc_resource() 函数中进行了抽象处理。
例如,典型的驱动程序可能在 attach() 函数中有类似以下内容:
每个基址寄存器的句柄都保存在 softc 结构中,以便稍后用于向设备写入。
这些句柄可以用于使用 bus_space_* 函数读取或写入设备寄存器。例如,驱动程序可能包含一个快捷函数来从特定于板的寄存器读取,像这样:
类似地,可以用以下方式写入寄存器:
这些功能存在 8 位、16 位和 32 位版本,您应相应地使用 bus_space_{read|write}_{1|2|4} 。
中断是从面向对象的总线代码中分配的,类似于内存资源。首先,必须从父总线中分配一个 IRQ 资源,然后必须设置中断处理程序来处理此 IRQ。
同样,来自设备 attach() 功能的示例胜过言辞。
驱动程序的分离例程中必须小心处理。您必须使设备的中断流静止,并移除中断处理程序。一旦 bus_teardown_intr() 返回,您就知道您的中断处理程序将不再被调用,并且可能正在执行此中断处理程序的所有线程都已返回。由于此函数可能会休眠,因此在调用此函数时不得持有任何互斥锁。
此部分已过时,仅出于历史原因而存在。处理这些问题的正确方法是改用 bus_space_dma*() 函数。当此部分更新以反映该用法时,可以删除本段。但是,目前 API 处于一种不稳定状态,因此一旦稳定下来,最好更新此部分以反映该状态。
在 PC 上,希望进行总线主控 DMA 的外围设备必须处理物理地址。这是一个问题,因为 FreeBSD 使用虚拟内存并几乎完全处理虚拟地址。幸运的是,有一个名为 vtophys() 的函数来帮助。
然而,在 alpha 上,解决方案有点不同,我们真正想要的是一个名为 vtobus() 的函数。
在 attach() 期间分配的所有资源进行释放非常重要。必须小心释放正确的资源,即使在失败的情况下,以便系统在驱动程序停止运行时仍然可用。
欢迎来到 FreeBSD 架构手册。本手册还在不断完善中,是许多人共同努力的成果。许多部分尚不存在,一些已存在的部分也需要更新。如果您有兴趣参与此项目,请发送电子邮件至 FreeBSD 文档项目邮件列表。
该文档的最新版本始终可从 FreeBSD 全球网络服务器获取。也可以从 FreeBSD 下载服务器或众多镜像站点之一以各种格式和压缩选项下载。
版权 © 2000-2006,2012-2023 FreeBSD 文档项目
本章由 FreeBSD SMP Next Generation 项目维护。
本文概述了 FreeBSD 内核中使用的锁,以允许内核内的有效多处理。可以通过几种方式实现锁定。数据结构可以通过 mutexes 或 lockmgr(9)锁来保护。一些变量只需始终使用原子操作来访问即可。
互斥锁只是用来保证互斥的锁。具体来说,互斥锁一次只能被一个实体拥有。如果另一个实体希望获取已经拥有的互斥锁,它必须等待直到互斥锁被释放。在 FreeBSD 内核中,互斥锁由进程拥有。
互斥锁可以递归获取,但它们旨在短时间内持有。具体来说,在持有互斥锁时不能休眠。如果需要在休眠期间持有锁,请使用 lockmgr(9)锁。
每个互斥锁都有几个值得关注的属性:
内核源码中 struct mtx 变量的名称。
mtx_init 分配给它的互斥体的逻辑名称。此名称显示在 KTR 跟踪消息和见证错误和警告中,并用于在见证代码中区分互斥体。
互斥体的类型,根据 MTX_* 标志。每个标志的含义与其在 mutex(9)中的文档中记录的含义相关。
MTX_DEF 一个睡眠互斥体
MTX_SPIN 一个自旋互斥体
MTX_RECURSE 这个互斥体允许递归。
保护对象一个数据结构或数据结构成员列表,该条目保护这些数据结构或数据结构成员。对于数据结构成员,名称将采用 structure name . member name 的形式。
依赖函数只能在持有此互斥锁时调用的函数。
表 1。互斥锁列表
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
vm86pcb 锁
vm86pcb 锁
MTX_DEF
vm86pcb
vm86_bioscall
巨人
巨人
MTX_DEF
MTX_RECURSE
几乎所有东西
锁定调用
锁定调用
MTX_SPIN
MTX_RECURSE
callfree
, callwheel
, nextsoftcheck
, proc
.p_itcallout
, proc
.p_slpcallout
, softticks
, ticks
这些锁提供基本的读者-写者类型功能,并且可能被正在睡眠的进程持有。目前它们由 lockmgr(9)支持。
共享独占锁列表表 2。
allproc_lock
allproc
zombproc
pidhashtbl
proc
.p_list
proc
.p_hash
nextpid
proctree_lock
proc
.p_children
proc
.p_sibling
原子保护变量是一种特殊变量,不受显式锁保护。 相反,对变量的所有数据访问都使用特殊的原子操作,如 atomic(9)中所述。 很少有变量是这样处理的,尽管其他同步原语,如互斥锁,是用原子保护变量实现的。
mtx
.mtx_lock
内核对象,或 Kobj 为内核提供了一个面向对象的 C 编程系统。因此,被操作的数据携带了如何操作它的描述。这允许在运行时向接口添加和移除操作,而不会破坏二进制兼容性。
对象一组数据 - 数据结构 - 数据分配。
方法-函数操作。
类-一个或多个方法。
接口-一个或多个方法的标准集合。
Kobj 通过生成方法描述来工作。每个描述都包含一个唯一的 id 和一个默认函数。描述的地址用于在类的方法表中唯一标识该方法。
通过创建一个将一个或多个函数与方法描述关联的方法表来构建一个类。在使用之前,对该类进行编译。编译会分配一个缓存并将其与类关联起来。如果另一个引用类的编译尚未完成,那么会为类的方法表中的每个方法描述分配一个唯一的 id。为了使用每个方法,脚本会生成一个函数来验证参数并自动引用方法描述以进行查找。生成的函数通过使用与方法描述关联的唯一 id 作为哈希值到与对象类关联的缓存中查找该方法。如果方法未被缓存,生成的函数会继续使用类的表来查找方法。如果找到方法,则使用类中关联的函数;否则,使用与方法描述关联的默认函数。
这些间接引用可以可视化为以下内容:
使用 Kobj 的第一步是创建一个接口。创建接口涉及创建一个模版,脚本 src/sys/kern/makeobjops.pl 可以用来生成方法声明和方法查找函数的头文件和代码。
在这个模版中,使用以下关键字: #include , INTERFACE , CODE , EPILOG , HEADER , METHOD , PROLOG , STATICMETHOD 和 DEFAULT 。
#include 语句及其后的内容将逐字复制到生成的代码文件的开头。
例如:
INTERFACE 关键字用于定义接口名称。此名称与每个方法名称连接为[接口名称]_[方法名称]。其语法为 INTERFACE [接口名称];。
例如:
CODE 关键字会将其参数逐字复制到代码文件中。其语法是 CODE { [whatever] };
例如:
HEADER 关键字会将其参数逐字复制到头文件中。它的语法是 HEADER { [whatever] };
例如:
METHOD 关键字描述一个方法。它的语法是 METHOD [return type] [method name] { [object [, arguments]] };
例如:
DEFAULT 关键字可以跟在 METHOD 关键字后面。它扩展了 METHOD 关键字以包括方法的默认功能。扩展的语法是 METHOD [return type] [method name] { [object; [other arguments]] }DEFAULT [default function];
例如:
STATICMETHOD 关键字与 METHOD 关键字相似,不同之处在于 kobj 数据不在对象结构的头部,因此将其强制转换为 kobj_t 是不正确的。相反, STATICMETHOD 依赖于 Kobj 数据被引用为“ops”。这也非常有用,可以直接从类的方法表中调用方法。
PROLOG 和 EPILOG 关键字在它们附加的地方之前或之后立即插入代码。此功能主要用于分析情况,以便以其他方式难以获取信息的情况。
其他完整示例:
使用 Kobj 的第二步是创建一个类。一个类包括一个名称、一个方法表和对象大小(如果使用 Kobj 的对象处理功能)。要创建类,请使用宏 DEFINE_CLASS() 。要创建方法表,请创建一个以 NULL 条目结尾的 kobj_method_t 数组。每个非 NULL 条目可以使用宏 KOBJMETHOD() 创建。
例如:
类必须被“编译”。根据类初始化时系统的状态,必须使用静态分配的缓存“操作表”。这可以通过声明一个 struct kobj_ops 并使用 kobj_class_compile_static(); 来实现,否则,应该使用 kobj_class_compile() 。
使用 Kobj 的第三步涉及如何定义对象。Kobj 对象创建例程假设 Kobj 数据位于对象的头部。如果这不合适,你将不得不自己分配对象,然后在其 Kobj 部分上使用 kobj_init() ;否则,你可以使用 kobj_create() 来自动分配和初始化对象的 Kobj 部分。 kobj_init() 也可以用来改变对象使用的类。
将 Kobj 集成到对象中,应使用宏 KOBJ_FIELDS。
例如
Kobj 的最后一步是简单地使用生成的函数来使用对象的类中的所需方法。这就像使用接口名称和方法名称一样简单,只需进行一些修改。应该使用下划线将接口名称与方法名称连接起来,全部使用大写。
例如,如果接口名称是 foo,方法是 bar,那么调用将是:
当通过 kobj_create() 分配的对象不再需要时,可以调用 kobj_delete() ,当类不再被使用时,可以调用 kobj_class_free() 。
本章节是关于引导和系统初始化过程的概述,从 BIOS(固件)POST 开始,到第一个用户进程的创建。由于系统启动的初始步骤非常依赖架构,以 IA-32 架构作为示例。但是,AMD64 和 ARM64 架构是更重要和引人注目的示例,根据本文档的主题,应该在不久的将来进行解释。
FreeBSD 的引导过程可能会出人意料地复杂。在控制权从 BIOS 传递后,必须进行大量的低级配置,然后才能加载和执行内核。这个设置必须以简单灵活的方式完成,允许用户有很多定制的可能性。
引导过程是一个极其依赖机器的活动。不仅每种计算机架构都需要编写代码,而且在同一架构上也可能有多种引导类型。例如,列出 stand 目录显示了大量与架构相关的代码。每种支持的架构都有一个目录。FreeBSD 支持 CSM 引导标准(兼容性支持模块)。因此,CSM 是支持的(同时支持 GPT 和 MBR 分区)并且支持 UEFI 引导(完全支持 GPT,主要支持 MBR)。它还支持从 ext2fs、MSDOS、UFS 和 ZFS 加载文件。FreeBSD 还支持 ZFS 的引导环境功能,使得主机操作系统可以传达有关引导的详细信息,这些信息比过去简单的分区更加复杂。但如今 UEFI 比 CSM 更为重要。下面的例子展示了如何从一个 MBR 分区的硬盘上引导一个 x86 计算机,使用存储在第一个扇区的 FreeBSD boot0 多重引导加载程序。该引导代码启动了 FreeBSD 的三阶段引导过程。
理解这一过程的关键在于它是一个复杂性逐渐增加的阶段系列。这些阶段是 boot1、boot2 和 loader(参见 boot(8) 了解更多详情)。引导系统按顺序执行每个阶段。最后一个阶段,loader,负责加载 FreeBSD 内核。以下部分将详细介绍每个阶段。
这是由不同引导阶段生成的输出示例。实际输出可能因机器而异:
当计算机启动时,处理器的寄存器被设置为一些预定义的值。其中一个寄存器是指令指针寄存器,在开机后的值是明确定义的:它是一个 32 位的值 0xfffffff0 。指令指针寄存器(也称为程序计数器)指向处理器要执行的代码。另一个重要的寄存器是 cr0 32 位控制寄存器,在重新启动后的值是 0 。其中 cr0 的位之一,即 PE(Protection Enabled)位,指示处理器是否在 32 位保护模式或 16 位实模式下运行。由于此位在引导时被清除,处理器以 16 位实模式引导。实模式意味着,线性地址和物理地址是相同的。处理器不立即以 32 位保护模式启动的原因是向后兼容性。特别是,引导过程依赖于 BIOS 提供的服务,而 BIOS 本身在传统的 16 位代码中运行。
0xfffffff0 的值略小于 4 GB,因此,除非计算机具有 4 GB 的物理内存,否则它无法指向有效的内存地址。计算机的硬件会将此地址转换,使其指向 BIOS 内存块。
基本输入输出系统(BIOS)是主板上的一块芯片,具有相对较小的只读存储器(ROM)。该存储器包含各种特定于主板提供的硬件的低级例程。处理器首先会跳转到地址 0xfffffff0,实际上位于 BIOS 存储器中。通常,该地址包含一个跳转指令,指向 BIOS 的 POST 例程。
上电自检(POST)是一组例程,包括内存检查、系统总线检查和其他低级初始化,以便 CPU 可以正确设置计算机。此阶段的重要步骤是确定引导设备。现代 BIOS 实现允许选择引导设备,允许从软盘、CD-ROM、硬盘或其他设备引导。
POST 的最后一步是 INT 0x19 指令。 INT 0x19 处理程序将引导设备的第一个扇区中的 512 字节读入地址 0x7c00 处的内存中。术语“第一个扇区”源自硬盘架构,其中磁盘分为多个圆柱磁道。磁道编号,每个磁道分为多个扇区(通常为 64 个)。磁道编号从 0 开始,但扇区编号从 1 开始。磁道 0 是磁盘的最外层,第一个扇区即第 1 扇区具有特殊用途。它也称为 MBR 或主引导记录。第一个磁道上的其余扇区从不使用。
这个扇区是我们的引导序列起始点。正如我们将看到的,这个扇区包含我们的 boot0 程序的副本。BIOS 跳转到地址 0x7c00 开始执行。
在从 BIOS 接收到控制权并位于内存地址 0x7c00 处后,boot0 开始执行。这是 FreeBSD 控制下的第一段代码。boot0 的任务非常简单:扫描分区表,并让用户选择要从哪个分区引导。分区表是嵌入在 MBR 中的特殊标准数据结构(因此嵌入在 boot0 中),描述了四个标准 PC “分区”。boot0 位于文件系统中的 /boot/boot0。它是一个小的 512 字节文件,它正是 FreeBSD 安装过程在硬盘的 MBR 中写入的内容,如果您在安装时选择了“bootmanager”选项。实际上,boot0 就是 MBR。
如前所述,我们称 BIOS INT 0x19 为在地址 0x7c00 处将 MBR(boot0)加载到内存中。boot0 的源文件可以在 stand/i386/boot0/boot0.S 中找到 - 这是由 Robert Nordier 编写的一段很棒的代码。
从 MBR 中的偏移 0x1be 开始的特殊结构称为分区表。它有四个记录,每个记录 16 字节,称为分区记录,代表硬盘如何被分区,或者在 FreeBSD 的术语中称为切片。这 16 个字节中的一个字节表示分区(切片)是否可引导。必须有一个记录设置了该标志,否则 boot0 的代码将拒绝继续执行。
分区记录有以下字段:
1 字节的文件系统类型
1 字节的可引导标志
CHS 格式中的 6 字节描述符
以 LBA 格式的 8 字节描述符
分区记录描述符包含有关分区在驱动器上的确切位置的信息。 LBA 和 CHS 两种描述符以不同的方式描述相同的信息:LBA(逻辑块寻址)具有分区的起始扇区和分区长度,而 CHS(柱面磁头扇区)具有分区的第一个和最后一个扇区的坐标。 分区表以特殊签名 0xaa55 结束。
MBR 必须适合 512 字节,即单个磁盘扇区。 该程序使用低级“技巧”,如利用某些指令的副作用并重用先前操作的寄存器值,以充分利用尽可能少的指令。 处理嵌入在 MBR 中的分区表时也必须小心。 出于这些原因,在修改 boot0.S 时务必小心。
注意,boot0.S 源文件是按原样汇编的:指令逐条转换为二进制,没有额外的信息(例如没有 ELF 文件格式)。这种低级控制是通过传递给链接器的特殊控制标志在链接时实现的。例如,程序的文本部分被设置为位于地址 0x600 。实际上,这意味着 boot0 必须加载到内存地址 0x600 才能正常运行。
值得查看 boot0 的 Makefile(stand/i386/boot0/Makefile),因为它定义了 boot0 的一些运行时行为。例如,如果连接到串口port(COM1)的终端用于 I/O,则必须定义宏 SIO ( -DSIO )。按下 F6 启用通过 PXE 引导。此外,该程序定义了一组标志,允许进一步修改其行为。所有这些都在 Makefile 中有所体现。例如,查看链接器指令,该指令命令链接器从地址 0x600 开始启动文本部分,并构建输出文件“按原样”(剥离任何文件格式):
stand/i386/boot0/Makefile
现在让我们开始学习 MBR,或者 boot0,从执行开始的地方开始。
stand/i386/boot0/boot0.S
这段代码的第一个块是程序的入口点。这是 BIOS 转移控制的地方。首先,它确保字符串操作自动递增其指针操作数( cld 指令)^[ 2]^。然后,由于它对段寄存器的状态没有假设,它对它们进行初始化。最后,它将堆栈指针寄存器( %sp )设置为($LOAD = 地址 0x7c00 ),这样我们就有一个可用的堆栈。
下一个块负责重定位并跳转到重定位后的代码。
stand/i386/boot0/boot0.S
当 boot0 被 BIOS 加载到地址 0x7C00 时,它将自身复制到地址 0x600 ,然后将控制权转移到那里(回想一下,它被链接以在地址 0x600 处执行)。源地址 0x7c00 被复制到寄存器 %si 。目标地址 0x600 被复制到寄存器 %di 。要复制的字数 256 (程序大小 = 512 字节)被复制到寄存器 %cx 。接下来, rep 指令重复执行后面的指令,即 movsw ,由 %cx 寄存器指示的次数。 movsw 指令将由 %si 指向的字复制到由 %di 指向的地址。这将重复另外 255 次。在每次重复中,源和目标寄存器 %si 和 %di 都会递增一次。因此,在完成 256 个字(512 字节)的复制后, %di 的值为 0x600512 = 0x800 , %si 的值为 0x7c00512 = 0x7e00 ;因此我们完成了代码重定位。自上次更新此文档以来,代码中的复制指令已更改,因此不再使用 movsb 和 stosb,而是引入了 movsw 和 stosw,它们在一次迭代中复制 2 字节(1 个字)。
接下来,目标寄存器 %di 被复制到 %bp 。 %bp 得到值 0x800 。值 8 被复制到 %cl 以准备进行新的字符串操作(类似于我们之前的 movsw )。现在, stosw 被执行 8 次。此指令将一个 0 值复制到由目标寄存器指向的地址( %di ,即 0x800 ),并递增它。这将重复另外 7 次,因此 %di 最终值为 0x810 。实际上,这清除了地址范围 0x800 - 0x80f 。该范围用作(虚假)分区表,用于将 MBR 写回磁盘。最后,给这个虚假分区的 CHS 寻址的扇区字段赋值为 1,并跳转到从重定位代码中的主函数。请注意,在跳转到重定位代码之前,避免了对绝对地址的任何引用。
以下代码块测试 BIOS 提供的驱动器编号是否应该使用,还是存储在 boot0 中的驱动器编号。
stand/i386/boot0/boot0.S
此代码测试标志变量中的 SETDRV 位( 0x20 )。请记住,寄存器 %bp 指向地址位置 0x800 ,因此测试是针对地址 0x800 - 69 = 0x7bb 处的标志变量进行的。这是可以对 boot0 进行的修改类型的示例。 SETDRV 标志默认未设置,但可以在 Makefile 中设置。设置后,将使用 MBR 中存储的驱动器编号,而不是 BIOS 提供的驱动器编号。我们假设使用默认值,并且 BIOS 提供了有效的驱动器编号,因此我们跳转到 save_curdrive 。
下一个块保存 BIOS 提供的驱动器编号,并调用 putn 在屏幕上打印新行。
stand/i386/boot0/boot0.S
请注意,我们假设 TEST 未定义,因此其中的条件代码未被汇编,也不会出现在我们的可执行 boot0 中。
我们的下一个模块实现了对分区表的实际扫描。它会将每个分区表中的四个条目的分区类型打印到屏幕上。它会将每种类型与一组知名操作系统文件系统进行比较。被识别的分区类型的示例有 NTFS(Windows®,ID 0x7)、 ext2fs (Linux®,ID 0x83),当然还有 ffs / ufs2 (FreeBSD,ID 0xa5)。实现相当简单。
stand/i386/boot0/boot0.S
重要的是要注意,每个条目的活动标志都被清除了,因此在扫描之后,我们在 boot0 的内存副本中没有活动分区条目。稍后,将为选定的分区设置活动标志。这确保了如果用户选择将更改写回磁盘,只存在一个活动分区。
下一个块检测其他驱动器。在启动时,BIOS 会将计算机中存在的驱动器数量写入地址 0x475 。如果存在其他驱动器,boot0 会将当前驱动器打印到屏幕上。用户可以随后命令 boot0 扫描另一个驱动器上的分区。
stand/i386/boot0/boot0.S
我们假设只有一个驱动器存在,因此不执行跳转到 print_drive 。我们也假设没有发生奇怪的事情,因此跳转到 print_prompt 。
这个下一块只是打印一个提示,然后是默认选项:
stand/i386/boot0/boot0.S
最后,跳转到 start_input ,这里使用 BIOS 服务启动计时器和从键盘读取用户输入;如果计时器超时,将选择默认选项:
站/i386/boot0/boot0.S
当需要编号为 0x1a 和参数为 0 的中断时,通过 int 指令由应用程序请求由 BIOS 提供的一系列预定义服务,参数存储在寄存器中(在本例中为 %ah )。在这里,特别是我们请求自午夜以来的时钟滴答数;BIOS 通过 RTC(实时时钟)计算此值。此时钟可以在范围从 2 赫兹到 8192 赫兹的频率下运行。BIOS 在启动时将其设置为 18.2 赫兹。当请求得到满足时,BIOS 会在寄存器 %cx 和 %dx 中返回一个 32 位结果(低字节在 %dx 中)。该结果(第 %dx 部分)会被复制到寄存器 %di 中,并且将 TICKS 变量的值加到 %di 中。该变量驻留在 boot0 中,距离寄存器 %bp 的偏移量为 _TICKS (一个负值)(回想一下,该寄存器指向 0x800 )。该变量的默认值为 0xb6 (十进制中为 182)。现在,boot0 的想法是不断向 BIOS 请求时间,当在寄存器 %dx 中返回的值大于存储在 %di 中的值时,时间就到了,并且将进行默认选择。由于 RTC 每秒滴答 18.2 次,此条件将在 10 秒后满足(此默认行为可以在 Makefile 中更改)。在此时间过去之前,boot0 将不断通过 int 0x16 ,参数 1 ,在 %ah 中向 BIOS 请求任何用户输入。
不论是按下了键还是时间到了,后续代码会验证选择。根据选择,寄存器 %si 会被设置为指向分区表中的相应分区条目。这个新选择会覆盖之前的默认选择。事实上,它成为新的默认选择。最后,所选分区的 ACTIVE 标志会被设置。如果在编译时启用了此功能,则这些修改后的值的内存版本会写回磁盘上的 MBR。我们将此实现的细节留给读者。
我们现在以 boot0 程序中的最后一个代码块结束我们的研究:
stand/i386/boot0/boot0.S
回想一下, %si 指向所选分区条目。该条目告诉我们分区在磁盘上的起始位置。当然,我们假设所选的分区实际上是一个 FreeBSD 切片。
传输缓冲区设置为 0x7c00 (寄存器 %bx ),并通过调用 intx13 请求读取 FreeBSD 切片的第一个扇区。我们假设一切顺利,因此不执行跳转到 beep 。特别是,新扇区读取必须以魔术序列 0xaa55 结束。最后, %si 处的值(指向所选分区表的指针)被保留供下一阶段使用,并跳转到地址 0x7c00 ,执行我们的下一阶段(刚读取的块)的执行开始。
到目前为止,我们已经按照以下顺序进行了:
BIOS 进行了一些早期硬件初始化,包括 POST。MBR(boot0)从绝对磁盘扇区一加载到地址 0x7c00 。执行控制权被传递到该位置。
boot0 将自身重定位到链接到执行的位置( 0x600 ),然后跳转到适当位置继续执行。最后,boot0 从 FreeBSD 切片加载第一个磁盘扇区到地址 0x7c00 。执行控制被传递到该位置。
boot1 是引导加载序列中的下一步。它是三个引导阶段中的第一个。请注意,我们一直在处理磁盘扇区。实际上,BIOS 加载绝对的第一个扇区,而 boot0 加载 FreeBSD 切片的第一个扇区。这两个加载都是到地址 0x7c00 。我们可以在概念上将这些磁盘扇区视为分别包含文件 boot0 和 boot1,但实际上对于 boot1 来说这并不完全正确。严格来说,与 boot0 不同,boot1 不是引导块的一部分^[3]。相反,一个单独的完整文件 boot(/boot/boot)最终被写入磁盘。这个文件是 boot1、boot2 和 Boot Extender (或 BTX)的组合。这个单一文件的大小大于一个扇区(大于 512 字节)。幸运的是,boot1 恰好占据这个单一文件的前 512 字节,因此当 boot0 加载 FreeBSD 切片的第一个扇区(512 字节)时,实际上是加载 boot1 并将控制传递给它。
boot1 的主要任务是加载下一个引导阶段。这个下一个阶段稍微复杂一些。它由一个名为“Boot Extender”或 BTX 的服务器和一个名为 boot2 的客户端组成。正如我们将看到的,最后一个引导阶段 loader 也是 BTX 服务器的客户端。
现在让我们详细看看 boot1 到底做了什么,就像我们为 boot0 做的那样,在其入口点开始:
stand/i386/boot2/boot1.S
start 处的入口点简单地跳过一个特殊数据区域到标签 main ,然后看起来像这样:
stand/i386/boot2/boot1.S
就像 boot0 一样, 这段代码将 boot1 重新定位到内存地址 0x700 。 但与 boot0 不同的是, 它不会跳转到那里。 boot1 链接以在地址 0x7c00 处执行, 实际上就是它最初加载的位置。 关于这种重定位的原因很快会讨论。
接下来是一个循环,用于查找 FreeBSD 分区。 虽然 boot0 从 FreeBSD 分区加载了 boot1,但它未传递关于这一点的任何信息 ^[ 4]^,因此 boot1 必须重新扫描分区表以找到 FreeBSD 分区的起始位置。 因此, 它重新读取 MBR:
stand/i386/boot2/boot1.S
在上面的代码中,寄存器 %dl 保存有关引导设备的信息。这是由 BIOS 传递的,并由 MBR 保留。数字 0x80 及更大的数字告诉我们正在处理硬盘,因此会调用 nread ,读取 MBR。参数传递给 nread 通过 %si 和 %dh 。标签 part4 处的内存地址被复制到 %si 。这个内存地址保存着一个供 nread 使用的“虚拟分区”。以下是虚拟分区中的数据:
stand/i386/boot2/boot1.S
特别是,此虚假分区的 LBA 硬编码为零。这用作 BIOS 读取硬盘绝对扇区一的参数。或者,可以使用 CHS 寻址。在这种情况下,虚假分区保留柱面 0、磁头 0 和扇区 1,这相当于绝对扇区一。
现在让我们继续看一下 nread :
stand/i386/boot2/boot1.S
回想一下, %si 指向虚假分区。偏移 0x8 处的字 ^[ 5]^ 被复制到寄存器 %ax ,偏移 0xa 处的字被复制到 %cx 。它们被 BIOS 解释为表示要读取的 LBA 的低 4 字节值(假定高 4 字节为零)。寄存器 %bx 存储着 MBR 将被加载的内存地址。将 %cs 推送到堆栈的指令非常有趣。在这种情况下,它什么也没做。然而,正如我们很快将看到的,boot2 与 BTX 服务器一起,也使用了 xread.1 。这个机制将在下一节中讨论。
xread.1 处的代码进一步调用 read 函数,实际上调用 BIOS 请求磁盘扇区:
stand/i386/boot2/boot1.S
注意此块末尾的长返回指令。此指令弹出 %cs 寄存器,该寄存器是由 nread 推送的,并返回。最后, nread 也返回。
将 MBR 加载到内存后,开始搜索 FreeBSD 分区的实际循环:
stand/i386/boot2/boot1.S
如果检测到 FreeBSD 分区,执行会继续在 main.5 处。请注意,当找到 FreeBSD 分区时, %si 指向分区表中的适当条目, %dh 存储分区号。我们假设已经找到 FreeBSD 分区,所以在 main.5 处继续执行:
stand/i386/boot2/boot1.S
在这一点上,请回忆一下,寄存器 %si 指向 MBR 分区表中的 FreeBSD 分区条目,因此对 nread 的调用将有效地读取该分区开头的扇区。传递给寄存器 %dh 的参数告诉 nread 读取 16 个磁盘扇区。请回忆,前 512 字节,或者 FreeBSD 分区的第一个扇区,与 boot1 程序重合。还请回忆,写入 FreeBSD 分区开头的文件不是/boot/boot1,而是/boot/boot。让我们看一下这些文件在文件系统中的大小:
boot0 和 boot1 各占用 512 个字节,因此它们恰好适合一个磁盘扇区。boot2 要大得多,包含 BTX 服务器和 boot2 客户端。最后,一个名为 boot 的文件比 boot2 大 512 个字节。该文件是 boot1 和 boot2 的串联。正如前面所指出的,boot0 是写入绝对第一个磁盘扇区(MBR)的文件,boot 是写入 FreeBSD 分区的第一个扇区的文件;boot1 和 boot2 并未写入磁盘。将 boot1 和 boot2 串联为单个 boot 的命令仅仅是 cat boot1 boot2 > boot 。
因此,boot1 恰好占用 boot 的前 512 个字节,并且,由于 boot 写入了 FreeBSD 分区的第一个扇区,boot1 正好适合于这个第一个扇区。当 nread 读取 FreeBSD 分区的前 16 个扇区时,实际上已经读取了整个 boot 文件。我们将在下一节详细了解 boot 是如何由 boot1 和 boot2 形成的。
回想一下, nread 使用存储器地址 0x8c00 作为传输缓冲区,用于保存读取的扇区。这个地址选择得很方便。的确,因为 boot1 属于前 512 个字节,它最终落入地址范围 0x8c00 - 0x8dff 内。随后的 512 个字节(范围 0x8e00 - 0x8fff )用于存储 bsdlabel。
从地址 0x9000 开始是 BTX 服务器的起点,紧随其后的是 boot2 客户端。BTX 服务器充当内核,在最高特权级别的保护模式下执行。相比之下,BTX 客户端(例如 boot2)在用户模式下执行。我们将在下一节看到如何实现这一点。在调用 nread 后的代码定位内存缓冲区中 boot2 的起始位置,并将其复制到内存地址 0xc000 。这是因为 BTX 服务器安排 boot2 在从 0xa000 开始的段中执行。我们将在接下来的部分详细探讨这一点。
boot1 的最后一个代码块允许访问 1MB 以上的内存 ^[ 8]^,并以跳转到 BTX 服务器的起始点结束:
stand/i386/boot2/boot1.S
注意,在跳转之前,中断被启用。
我们引导顺序中的下一个步骤是 BTX 服务器。让我们快速回顾一下我们是如何到达这里的:
BIOS 加载绝对扇区一(MBR 或 boot0),跳转到地址 0x7c00 。
boot0 会重新定位到 0x600 ,即链接执行的地址,并跳转到那里。然后,它将读取 FreeBSD 切片的第一个扇区(包含 boot1),加载到地址 0x7c00 ,然后跳转到那里。
boot1 将 FreeBSD 切片的前 16 个扇区加载到地址 0x8c00 。这 16 个扇区,或 8192 字节,是整个文件 boot。该文件是 boot1 和 boot2 的串联。boot2 则包含 BTX 服务器和 boot2 客户端。最后,跳转到地址 0x9010 ,即 BTX 服务器的入口点。
在详细学习 BTX 服务器之前,让我们进一步审查单一的、一体化的引导文件是如何创建的。引导的构建方式是在其 Makefile(stand/i386/boot2/Makefile)中定义的。让我们看一下创建引导文件的规则:
stand/i386/boot2/Makefile
这告诉我们需要 boot1 和 boot2,并且该规则简单地将它们连接起来以生成一个名为 boot 的单一文件。创建 boot1 的规则也很简单:
stand/i386/boot2/Makefile
要应用创建 boot1 的规则,必须解析 boot1.out。这又取决于 boot1.o 文件的存在。最后一个文件只是我们熟悉的 boot1.S 汇编的结果,没有链接。现在,应用创建 boot1.out 的规则。这告诉我们 boot1.o 应该与 start 作为其入口点链接,并从地址 0x7c00 开始。最后,通过将适当的规则应用于 boot1.out 创建 boot1。这个规则是应用于 boot1.out 的 objcopy 命令。注意传递给 objcopy 的标志: -S 告诉它剥离所有重定位和符号信息; -O binary 表示输出格式,即一个简单的、无格式的二进制文件。
有了 boot1,让我们看看 boot2 是如何构建的:
stand/i386/boot2/Makefile
构建 boot2 的机制要复杂得多。让我们指出最相关的事实。依赖列表如下:
stand/i386/boot2/Makefile
请注意,最初没有头文件 boot2.h,但它的创建依赖于我们已经有的 boot1.out。其创建规则有点简洁,但重要的是输出文件 boot2.h 是这样的:
stand/i386/boot2/boot2.h
回忆一下,boot1 被重新定位(即,从 0x7c00 复制到 0x700 )。这种重新定位现在有意义了,因为正如我们将看到的,BTX 服务器回收了一些内存,包括最初加载 boot1 的空间。然而,BTX 服务器需要访问 boot1 的 xread 函数;根据 boot2.h 的输出,这个函数位于 0x725 。确实,BTX 服务器使用 boot1 重新定位代码中的 xread 函数。这个函数现在可以从 boot2 客户端中访问。
下一个规则指示链接器链接各种文件(ashldi3.o、boot2.o 和 sio.o)。请注意,输出文件 boot2.out 被链接以在地址 0x2000 ($ {ORG2})执行。回想一下,boot2 将在用户模式下执行,在 BTX 服务器设置的特殊用户段内。该段从 0xa000 开始。还记得 boot 中的 boot2 部分被复制到地址 0xc000 ,也就是从用户段开始的偏移 0x2000 ,所以当我们转移控制到它时,boot2 将正常工作。接下来,boot2.bin 是通过删除其符号和格式信息从 boot2.out 创建的;boot2.bin 是一个原始二进制文件。现在,请注意一个名为 boot2.ldr 的文件创建为一个全是零的 512 字节文件。这个空间是留给 bsdlabel 的。
现在我们拥有 boot1、boot2.bin 和 boot2.ldr 文件,在创建这个全合一启动文件之前,唯一缺失的是 BTX 服务器。BTX 服务器位于 stand/i386/btx/btx;它有自己的 Makefile,其中包含了用于构建的一套规则。需要注意的重要一点是,它也被编译为原始二进制文件,并被链接以在地址 0x9000 执行。详细信息可以在 stand/i386/btx/btx/Makefile 中找到。
拥有构成启动程序的文件后,最后一步是合并它们。这由一个名为 btxld 的特殊程序完成(源文件位于/usr/src/usr.sbin/btxld)。该程序的一些参数包括输出文件的名称(boot)、其入口点( 0x2000 )和文件格式(原始二进制)。这个实用程序最终将各种文件合并为 boot 文件,其中包括 boot1、boot2、 bsdlabel 和 BTX 服务器。这个文件正好占据 16 个扇区或 8192 字节,实际上是在安装期间写入 FreeBSD 分区的开始部分。现在让我们继续研究 BTX 服务器程序。
BTX 服务器在将控制权交给客户端之前,准备一个简单的环境,并从 16 位实模式切换到 32 位保护模式。这包括初始化和更新以下数据结构:
修改 Interrupt Vector Table (IVT) 。IVT 为实模式代码提供异常和中断处理程序。
创建 Interrupt Descriptor Table (IDT) 。为处理器异常、硬件中断、两个系统调用和 V86 接口提供条目。IDT 为保护模式代码提供异常和中断处理程序。
创建了一个 Task-State Segment (TSS) 。这是必要的,因为处理器在执行客户端(boot2)时工作在最低特权级别,但在执行 BTX 服务器时工作在最高特权级别。
设置了 GDT(全局描述符表)。为监管者代码和数据、用户代码和数据以及实模式代码和数据提供了条目(描述符)。 ^[ 9]^
现在让我们开始研究实际实现。回想一下,boot1 跳转到地址 0x9010 ,即 BTX 服务器的入口点。在那里研究程序执行之前,请注意 BTX 服务器在地址范围 0x9000-0x900f 之前有一个特殊的头。这个头的定义如下:
站/i386/btx/btx/btx.S
注意,前两个字节分别为 0xeb 和 0xe 。在 IA-32 架构中,这两个字节被解释为相对于头部跳转到入口点,因此理论上,boot1 可以跳转到这里(地址 0x9000 )而不是地址 0x9010 。请注意,BTX 头部中的最后一个字段是指向客户端(boot2)入口点 b2 的指针。此字段在链接时被修补。
紧随头部之后是 BTX 服务器的入口点:
stand/i386/btx/btx/btx.S
此代码禁用中断,设置工作堆栈(从地址 0x1800 开始),并清除 EFLAGS 寄存器中的标志位。请注意, popfl 指令从堆栈中弹出一个双字(4 字节)并将其放入 EFLAGS 寄存器中。由于实际弹出的值为 2 ,因此 EFLAGS 寄存器实际上被清除(IA-32 要求 EFLAGS 寄存器的第 2 位始终为 1)。
我们的下一个代码块清除(设置为 0 )内存范围 0x5e00-0x8fff 。这个范围是各种数据结构将被创建的地方:
FreeBSD/i386/btx/btx/btx.S
回想一下,boot1 最初加载到地址 0x7c00 ,因此,随着这个内存初始化,该副本实际上消失了。但是,也要记住,boot1 已经重新定位到 0x700 ,因此该副本仍然在内存中,并且 BTX 服务器将利用它。
接下来,实模式 IVT(中断向量表)被更新。IVT 是一组用于异常和中断处理程序的段/偏移对数组。BIOS 通常将硬件中断映射到中断向量 0x8 到 0xf 和 0x70 到 0x77 ,但是,正如将要看到的,8259A 可编程中断控制器,控制实际将硬件中断映射到中断向量的芯片,被编程为将这些中断向量重新映射从 0x8-0xf 到 0x20-0x27 和从 0x70-0x77 到 0x28-0x2f 。因此,为中断向量 0x20-0x2f 提供了中断处理程序。之所以不直接使用 BIOS 提供的处理程序是因为它们在 16 位实模式下工作,但不在 32 位保护模式下工作。处理器模式将很快切换到 32 位保护模式。但是,BTX 服务器建立了一种机制,有效地使用 BIOS 提供的处理程序:
站/i386/btx/btx/btx.S
下一个块创建 IDT(中断描述符表)。在保护模式中,IDT 类似于实模式中的 IVT。也就是说,在处理器在保护模式下执行时,IDT 描述了使用的各种异常和中断处理程序。实质上,它也由一系列段/偏移对组成,尽管结构略微更加复杂,因为在保护模式下,段与实模式下的段不同,并且各种保护机制适用:
站/i386/btx/btx/btx.S
IDT 中的每个条目都有 8 字节长。除了段/偏移信息外,它们还描述段类型、特权级别以及段是否存在于内存中。构造如下:从 0 到 0xf 的中断向量(异常)由函数 intx00 处理;向量 0x10 (同样是异常)由 intx10 处理;稍后配置为从中断向量 0x20 开始直到中断向量 0x2f 的硬件中断由函数 intx20 处理。最后,用于系统调用的中断向量 0x30 由 intx30 处理,向量 0x31 和 0x32 由 intx31 处理。必须注意的是,只有中断向量 0x30 、 0x31 和 0x32 的描述符被赋予特权级别 3,与 boot2 客户端相同特权级别,这意味着客户端可以通过 int 指令执行对这些向量的软件生成中断而不会失败(这是 boot2 使用 BTX 服务器提供的服务的方式)。此外,请注意,只有软件生成的中断受保护,免受在较低特权级别执行代码的影响。硬件生成的中断和处理器生成的异常始终得到充分处理,无论涉及的实际特权级别为何。
下一步是初始化 TSS(任务状态段)。TSS 是一个硬件特性,通过处理抽象进程,帮助操作系统或执行软件实现多任务处理功能。如果使用多任务处理设施或定义不同的特权级别,IA-32 架构要求创建并使用至少一个 TSS。由于 boot2 客户端在特权级别 3 中执行,但 BTX 服务器在特权级别 0 中运行,因此必须定义一个 TSS:
stand/i386/btx/btx/btx.S
注意,在 TSS 中为特权级 0 的堆栈指针和堆栈段提供了一个值。这是必要的,因为如果在特权级 3 中执行 boot2 时收到中断或异常,处理器会自动执行到特权级 0,因此需要一个新的工作堆栈。最后,TSS 的 I/O 映射基地址字段被赋予一个值,这是从 TSS 开头到 I/O 权限位图和中断重定向位图的 16 位偏移量。
在创建 IDT 和 TSS 之后,处理器已准备好切换到保护模式。这在下一个块中完成:
stand/i386/btx/btx/btx.S
首先,调用 setpic 来编程 8259A PIC(可编程中断控制器)。该芯片连接到多个硬件中断源。当从设备接收到中断时,它会向处理器发出适当的中断向量信号。这可以定制,以便特定中断与特定中断向量相关联,如前所述。接下来,将 IDTR(中断描述符表寄存器)和 GDTR(全局描述符表寄存器)加载为分别为 lidt 和 lgdt 的指令。这些寄存器加载了 IDT 和 GDT 的基地址和限制地址。接下来的三条指令设置 %cr0 寄存器的保护使能(PE)位。这有效地将处理器切换到 32 位保护模式。接下来,通过使用段选择器 SEL_SCODE 进行长跳转到 init.8 。这选择了监控代码段。在此跳转后,处理器有效地在 CPL 0 中执行,即最高特权级别。最后,通过将段选择器 SEL_SDATA 分配给 %ss 寄存器,为堆栈选择了监控数据段。该数据段的特权级别也为 0 。
我们的最后一个代码块负责使用我们之前创建的 TSS 的段选择器将 TR(任务寄存器)加载,并在将执行控制传递给 boot2 客户端之前设置用户模式环境。
stand/i386/btx/btx/btx.S
请注意,客户端的环境包括堆栈段选择器和堆栈指针(寄存器 %ss 和 %esp )。实际上,一旦 TR 加载了适当的堆栈段选择器(指令 ltr ),堆栈指针就会被计算并推送到堆栈上,同时推送堆栈的段选择器。接下来,值 0x202 被推送到堆栈上;这是当控制权传递给客户端时 EFLAGS 将获得的值。此外,用户模式代码段选择器和客户端的入口点也被推送。请记住,此入口点在链接时在 BTX 标头中进行了修补。最后,段选择器(存储在寄存器 %ecx 中)用于段寄存器 %gs, %fs, %ds and %es 也被推送到堆栈上,以及 %edx ( 0xa000 )处的值。请记住已经推送到堆栈上的各种值(它们很快将被弹出)。接下来,剩余通用寄存器的值也被推送到堆栈上(请注意 loop 会将值 0 推送七次)。现在,值将开始从堆栈中弹出。首先, popa 指令从堆栈中弹出最近推送的七个值。它们按顺序存储在通用寄存器中 %edi, %esi, %ebp, %ebx, %edx, %ecx, %eax 。然后,推送的各种段选择器被弹出到各种段寄存器中。堆栈上仍然有五个值。当执行 iret 指令时,它们将被弹出。此指令首先弹出从 BTX 标头推送的值。此值是指向 boot2 入口点的指针。它被放置在寄存器 %eip 中,即指令指针寄存器。接下来,用户代码段的段选择器被弹出并复制到寄存器 %cs 中。请记住,此段的特权级别为 3,是最低特权级别。这意味着我们必须为此特权级别的堆栈提供值。这就是处理器除了进一步从堆栈中弹出 EFLAGS 寄存器的值外,还要从堆栈中弹出两个值的原因。 这些值输入堆栈指针( %esp )和堆栈段( %ss )。现在,执行继续在 boot0 的入口点。
注意用户代码段的定义方式很重要。该段的基址设置为 0xa000 。这意味着代码内存地址是相对于地址 0xa000 的;如果执行的代码是从地址 0x2000 获取的,实际内存地址为 0xa000+0x2000=0xc000 。
boot2 定义了一个重要的结构, struct bootinfo 。这个结构由 boot2 初始化并传递给加载程序,然后进一步传递给内核。这个结构的一些节点由 boot2 设置,其余由加载程序设置。除其他信息外,这个结构包含内核文件名、BIOS 硬盘几何、用于引导设备的 BIOS 驱动器号、可用物理内存、 envp 指针等。它的定义如下:
boot2 进入一个无限循环,等待用户输入,然后调用 load() 。如果用户没有按任何键,循环会因超时而中断,因此 load() 将加载默认文件(/boot/loader)。函数 ino_t lookup(char *filename) 和 int xfsread(ino_t inode, void *buf, size_t nbyte) 用于将文件内容读入内存。/boot/loader 是一个 ELF 二进制文件,但 ELF 头部前面附加了 a.out 的 struct exec 结构。 load() 扫描加载程序的 ELF 头部,将/boot/loader 的内容加载到内存中,并将执行传递给加载程序的入口:
加载程序也是一个 BTX 客户端。我在这里不会详细描述它,有一个由 Mike Smith 编写的全面的手册,加载程序(8)。底层机制和 BTX 在前面已经讨论过。
加载程序的主要任务是引导内核。当内核被加载到内存中时,加载程序会调用它:
让我们看一下链接内核的命令。这将帮助确定加载程序将执行传递给内核的确切位置。这个位置是内核的实际入口点。此命令现已从 sys/conf/Makefile.i386 中排除。我们感兴趣的内容可以在/usr/obj/usr/src/i386.i386/sys/GENERIC/找到。
这里可以看到一些有趣的东西。首先,内核是一个 ELF 动态链接的二进制文件,但内核的动态链接器是/red/herring,这绝对是一个虚假文件。其次,查看文件 sys/conf/ldscript.i386 可以了解在编译内核时使用了哪些 ld 选项。通过阅读前几行,字符串
表明内核的入口点是符号 btext 。这个符号在 locore.s 中定义:
首先,寄存器 EFLAGS 被设置为预定义值 0x00000002。然后初始化所有段寄存器:
btext 调用了在 locore.s 中也定义的例程 recover_bootinfo() , identify_cpu() 。以下是它们的功能描述:
接下来的步骤是启用 VME,如果 CPU 支持的话:
然后,启用分页:
下面的三行代码是因为设置了分页,所以需要跳转才能在虚拟地址空间中继续执行:
函数 init386() 被调用,传递了一个指向第一个空闲物理页的指针,之后 mi_startup() 。 init386 是一个与体系结构相关的初始化函数, mi_startup() 是一个与体系结构无关的函数('mi_'前缀代表 Machine Independent)。内核从不返回 mi_startup() ,通过调用它,内核完成引导:
init386()
init386() 在 sys/i386/i386/machdep.c 中定义,执行特定于 i386 芯片的低级初始化。切换到保护模式是由加载器完成的。加载器创建了第一个任务,在其中内核继续操作。在查看代码之前,请考虑处理器必须完成的任务以初始化保护模式执行:
初始化内核可调参数,从引导程序传递而来。
准备 GDT。
准备 IDT。
初始化系统控制台。
如果编译到内核中,请初始化 DDB。
初始化 TSS。
准备 LDT。
设置 thread0 的 PCB。
init386() 通过设置环境指针(envp)并调用 init_param1() 来初始化从引导加载程序传递的可调参数。envp 指针已经从加载程序传递给了 bootinfo 结构:
init_param1() 定义在 sys/kern/subr_param.c 中。该文件有许多 sysctl,还有两个函数, init_param1() 和 init_param2() ,它们从 init386() 调用:
TUNABLE__FETCH 用于从环境中获取值:
Sysctl kern.hz 是系统时钟滴答。此外,这些 sysctl 由 init_param1() 设置: kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.maxdsiz, kern.dflssiz, kern.maxssiz, kern.sgrowsiz 。
然后 init386() 准备全局描述符表(GDT)。 x86 上的每个任务都运行在自己的虚拟地址空间中,该空间由段:偏移对进行寻址。 例如,处理器即将执行的当前指令位于 CS:EIP,那么该指令的线性虚拟地址将是“代码段 CS 的虚拟地址”+ EIP。 为方便起见,段从虚拟地址 0 开始,并以 4GB 边界结束。 因此,对于此示例,指令的线性虚拟地址将仅为 EIP 的值。 诸如 CS、DS 等段寄存器是选择器,即 GDT 中的索引。 FreeBSD 的 GDT 为每个 CPU 保留了 15 个选择器的描述符:
请注意,这些#define 本身并不是选择器,而只是选择器的字段 INDEX,因此它们恰好是 GDT 的索引。 例如,内核代码(GCODE_SEL)的实际选择器值为 0x20。
下一步是初始化中断描述符表(IDT)。 当软件或硬件中断发生时,处理器将引用此表。 例如,要进行系统调用,用户应用程序发出 INT 0x80 指令。 这是软件中断,因此处理器的硬件会查找 IDT 中索引为 0x80 的记录。 该记录指向处理此中断的例程,在这种特殊情况下,这将是内核的系统调用门。 IDT 最多可以有 256(0x100)个记录。 内核为 IDT 分配 NIDT 记录,其中 NIDT 为最大值(256):
对于每个中断,都设置了适当的处理程序。 INT 0x80 的系统调用门也被设置:
因此,当用户应用程序发出 INT 0x80 指令时,控制权将转移到位于内核代码段中并以监督者特权执行的函数 _Xint0x80_syscall 。
然后初始化控制台和 DDB:
任务状态段是另一个 x86 保护模式结构,TSS 用于在任务切换发生时由硬件存储任务信息。
本地描述符表用于引用用户空间代码和数据。定义了几个选择器指向 LDT,它们是系统调用门和用户代码和数据选择器:
接下来,proc0 的进程控制块( struct pcb )结构被初始化。proc0 是一个 struct proc 结构,描述了一个内核进程。它在内核运行时始终存在,因此与 thread0 相关联:
结构 struct pcb 是 proc 结构的一部分。它在 /usr/include/machine/pcb.h 中定义,包含特定于 i386 架构的进程信息,如寄存器值。
mi_startup()
此函数对所有系统初始化对象执行冒泡排序,然后依次调用每个对象的入口:
尽管 sysinit 框架在开发者手册中有描述,我将讨论其中的内部。
每个系统初始化对象(sysinit 对象)都是通过调用 SYSINIT()宏创建的。让我们以 announce sysinit 对象为例。此对象打印版权信息:
此对象的子系统 ID 为 SI_SUB_COPYRIGHT(0x0800001)。因此,版权信息将在控制台初始化后首先打印出来。
让我们看看宏 SYSINIT() 实际上是做什么的。它扩展为 C_SYSINIT() 宏。然后 C_SYSINIT() 宏扩展为具有另一个 DATA_SET 宏调用的静态 struct sysinit 结构声明:
DATA_SET() 宏扩展为 _MAKE_SET() ,该宏是隐藏所有 sysinit 魔术的地方:
执行这些宏后,在内核中创建了各种部分,包括 set.sysinit_set 。在内核二进制文件上运行 objdump,您可能会注意到这些小节的存在:
此屏幕转储显示 set.sysinit_set 部分的大小为 0x14d8 字节,因此 0x14d8/sizeof(void *) sysinit 对象被编译到内核中。其他部分,如 set.sysctl_set ,代表其他链接器集合。
通过定义一个类型为 struct sysinit 的变量, set.sysinit_set 部分的内容将被"收集"到该变量中:
struct sysinit 定义如下:
回到 mi_startup() 讨论,现在必须清楚了,sysinit 对象是如何被组织的。 mi_startup() 函数对它们进行排序并调用每一个。最后一个对象是系统调度程序:
系统调度程序 sysinit 对象在文件 sys/vm/vm_glue.c 中定义,该对象的入口点是 scheduler() 。该函数实际上是一个无限循环,代表进程 PID 0,即交换进程。之前提到的 thread0 结构用于描述它。
第一个用户进程称为 init,由 sysinit 对象 init 创建:
函数 create_init() 通过调用 fork1() 分配一个新进程,但不标记为可运行。当调度程序安排执行此新进程时,将调用 start_init() 。该函数在 init_main.c 中定义。它尝试加载和执行 init 二进制文件,首先探测/sbin/init,然后是/sbin/oinit,/sbin/init.bak,最后是/rescue/init:
用户在 boot0 阶段选择要引导的操作系统后,如果按键,将出现此提示。
如果有疑问,我们建议读者参考官方英特尔手册,其中描述了每条指令的确切语义。
存在一个文件 /boot/boot1,但它并非写入到 FreeBSD 切片的开头。相反,它与 boot2 连接在一起形成 boot,后者写入到 FreeBSD 切片的开头,并在引导时读取。
实际上,我们确实向寄存器%si 中的切片条目传递了指针。 但是,boot1 不假设它是由 boot0 加载的(也许是其他某个 MBR 加载了它,并且没有传递这些信息),因此它不做任何假设。
在 16 位实模式的背景下,一个字是 2 个字节。
512*16=8192 字节,正好是 boot 的大小。
历史上被称为磁盘标签。如果你曾经想知道 FreeBSD 将这些信息存储在哪里,它就在这个区域 - 参见 bsdlabel(8)
由于传统原因,这是必要的。感兴趣的读者应参阅。
当从保护模式切换回实模式时,根据英特尔手册的建议,实模式代码和数据是必要的。
boot0
F1 FreeBSD
F2 BSD
F5 Disk 2
boot2
^[1]^
>>FreeBSD/x86 BOOT
Default: 0:ad(0p4)/boot/loader
boot:
装载程序
BTX loader 1.00 BTX version is 1.02
Consoles: internal video/keyboard
BIOS drive C: is disk0
BIOS 639kB/2096064kB available memory
FreeBSD/x86 bootstrap loader, Revision 1.1
Console internal video/keyboard
(root@releng1.nyi.freebsd.org, Fri Apr 9 04:04:45 UTC 2021)
Loading /boot/defaults/loader.conf
/boot/kernel/kernel text=0xed9008 data=0x117d28+0x176650 syms=[0x8+0x137988+0x8+0x1515f8]
内核
Copyright (c) 1992-2021 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 is a registered trademark of The FreeBSD Foundation.
FreeBSD 13.0-RELEASE 0 releng/13.0-n244733-ea31abc261f: Fri Apr 9 04:04:45 UTC 2021
root@releng1.nyi.freebsd.org:/usr/obj/usr/src/i386.i386/sys/GENERIC i386
FreeBSD clang version 11.0.1 (git@github.com:llvm/llvm-project.git llvmorg-11.0.1-0-g43ff75f2c3fe)
recover_bootinfo
此例程解析从引导加载程序传递给内核的参数。内核可能以 3 种方式引导:通过上述的加载程序、通过旧的磁盘引导块,或通过旧的无盘引导过程。此函数确定引导方法,并将 struct bootinfo 结构存储到内核内存中。
identify_cpu
此函数尝试查找其正在运行的 CPU,并将找到的值存储在变量 _cpu 中。
SYSINIT 是用于通用调用排序和分派机制的框架。FreeBSD 目前在内核的动态初始化中使用它。SYSINIT 允许重新排序、添加、移除和替换 FreeBSD 的内核子系统,在内核或其模块加载时,无需编辑静态排序的初始化路由并重新编译内核。该系统还允许内核模块,目前称为 KLD,可以在引导时单独编译、链接和初始化,甚至在系统已经运行时加载。这是通过使用“内核链接器”和“链接器集”来实现的。
链接器集是一种链接技术,其中链接器将程序源文件中静态声明的数据收集到一个连续可寻址的数据单元中。
SYSINIT 依赖于链接器能够将程序源代码中在多个位置声明的静态数据作为单个连续数据块进行分组。这种链接器技术称为 "链接器集"。SYSINIT 使用两个链接器集来维护包含每个消费者调用顺序、函数和传递给该函数的数据指针的两个数据集。
SYSINIT 在对函数执行顺序进行排序时使用两个优先级。第一个优先级是子系统 ID,为 SYSINIT 的函数分派提供整体顺序。当前预声明的 ID 在 中的枚举列表中。第二个使用的优先级是子系统内的元素顺序。当前预声明的子系统元素顺序在 中的枚举列表中。
目前 SYSINIT 有两个用途。在系统启动和内核模块加载时进行功能分发,以及在系统关闭和内核模块卸载时进行功能分发。内核子系统经常使用系统启动的 SYSINIT 来初始化数据结构,例如进程调度子系统使用 SYSINIT 来初始化运行队列数据结构。设备驱动程序应避免直接使用 SYSINIT() 。相反,属于总线结构一部分的实际设备的驱动程序应使用 DRIVER_MODULE() 来提供一个检测设备并(如果存在)初始化设备的函数。它将执行一些特定于设备的操作,然后调用 SYSINIT() 本身。对于非属于总线结构的伪设备,请使用 DEV_MODULE() 。
SYSINIT() 宏在 SYSINIT 的启动数据集中创建必要的 SYSINIT 数据,以便 SYSINIT 对系统启动和模块加载时需要排序和分发的函数进行调用。 SYSINIT() 接受一个唯一标识符,该标识符由 SYSINIT 用于识别特定函数分发数据、子系统顺序、子系统元素顺序、要调用的函数以及要传递给函数的数据。所有函数必须接受一个常量指针参数。
示例 1。 SYSINIT() 的示例
请注意, SI_SUB_FOO 和 SI_ORDER_FOO 需要出现在上面提到的枚举类型 sysinit_sub_id 和 sysinit_elem_order 中。可以使用现有枚举类型或将自己的枚举类型添加到中。还可以使用数学方法对 SYSINIT 将运行的顺序进行微调。此示例显示了需要在处理调整内核参数的 SYSINIT 之前运行的 SYSINIT。
示例 2. 调整 SYSINIT() 订单的示例
SYSUNINIT() 宏的行为与 SYSINIT() 宏类似,只是它将 SYSINIT 数据添加到 SYSINIT 的关机数据集中。
示例 3. SYSUNINIT() 的示例
在大多数 UNIX® 系统上, root 拥有无所不能的权力。这促进了不安全性。如果黑客在系统上获得了 root ,他将可以使用手边的每个功能。在 FreeBSD 中,有一些 sysctls 降低了 root 的权限,以最小化黑客造成的破坏。具体来说,其中一个功能被称为 secure levels 。同样地,另一个功能从 FreeBSD 4.0 开始存在,是一个称为 jail(8)的实用程序。Jail 为一个环境创建了 chroot 并对在 jail 内分叉的进程设置了某些限制。例如,一个被 jail 的进程无法影响 jail 外部的进程、利用某些系统调用或对主机环境造成任何破坏。
Jail 正在成为新的安全模型。人们在 jail 内运行潜在易受攻击的服务器,比如 Apache、BIND 和 sendmail,因此,如果黑客在 jail 内获得了 root ,那只是个恼人的大问题,而不是灾难。本文主要关注 jail 的内部(源代码)。关于如何设置 jail 的信息,请参阅有关 jails 的手册条目。
Jail 由两个领域组成:用户空间程序 jail(8)和内核中实现的代码:系统调用 jail(2)及相关限制。我将讨论用户空间程序,然后讨论 jail 如何在内核中实现。
用户空间 jail 的源代码位于/usr/src/usr.sbin/jail,包括一个文件 jail.c。该程序接受这些参数:jail 的路径,主机名,IP 地址和要执行的命令。
在 jail.c 中,我要注意的第一件事是从 /usr/include/sys/jail.h 包含的一个重要结构 struct jail j; 的声明。
jail 结构的定义是:
正如您所看到的,对于传递给 jail(8) 程序的每个参数,确实在其执行期间设置。
传递给 jail(8) 程序的参数之一是一个 IP 地址,通过该 IP 地址可以访问 jail。jail(8) 将给定的 IP 地址转换为主机字节顺序,然后将其存储在 j ( jail 结构体) 中。
inet_aton(3) 函数会“将指定的字符串解释为一个互联网地址,并将该地址放入所提供的结构中。” 当 IP 地址由 inet_aton(3) 放入结构中,并且通过 ntohl(3) 被转换为主机字节顺序时, ip_number 结构中的 jail 成员才会被设置。
最后,用户空间程序 jails 进程。现在,进程成为一个被 jail 的进程,并使用 execv(3) 执行给定的命令。
As you can see, the jail()
function is called, and its argument is the jail
structure which has been filled with the arguments given to the program. Finally, the program you specify is executed. I will now discuss how jail is implemented within the kernel.
We will now be looking at the file /usr/src/sys/kern/kern_jail.c. This is the file where the jail(2) system call, appropriate sysctls, and networking functions are defined.
在 kern_jail.c 中,定义了以下 sysctls:
用户可以通过 sysctl(8)程序访问这些 sysctl 中的每一个。在整个内核中,这些特定 sysctl 通过其名称来识别。例如,第一个 sysctl 的名称是 security.jail.set_hostname_allowed 。
像所有系统调用一样,jail(2) 系统调用接受两个参数, struct thread *td 和 struct jail_args *uap 。 td 是描述调用线程的结构体指针。在这种情况下, uap 是指向用户态传递的 jail 结构体的指针所包含的结构体指针。当我之前描述用户态程序时,你看到 jail(2) 系统调用被赋予一个 jail 结构体作为自己的参数。
因此, uap→jail 可以用来访问传递给系统调用的 jail 结构体。接下来,系统调用使用 copyin(9) 函数将 jail 结构体复制到内核空间。copyin(9) 接受三个参数:要复制到内核空间的数据的地址, uap→jail ,存储位置 j 和存储空间的大小。由 uap→jail 指向的 jail 结构体被复制到内核空间,并存储在另一个 jail 结构体 j 中。
在 jail.h 中定义了另一个重要的结构。这是 prison 结构。 prison 结构专门在内核空间中使用。这是 prison 结构的定义。
然后,jail(2)系统调用为 prison 结构分配内存,并在 jail 和 prison 结构之间复制数据。
接下来,我们将讨论另一个重要的系统调用 jail_attach(2),它实现了将进程放入 jail 的功能。
这个系统调用会进行改变,以区分被 jail 的进程和未被 jail 的进程。 要理解 jail_attach(2) 对我们的作用,需要一些背景信息。
在 FreeBSD 中,每个内核可见线程由其 thread 结构标识,而进程由其 proc 结构描述。 您可以在 /usr/include/sys/proc.h 中找到 thread 和 proc 结构的定义。 例如,任何系统调用中的 td 参数实际上是指向调用线程的 thread 结构的指针,正如前面所述。 td_proc 结构中的 thread 成员,由 td 指向的结构,是指向包含由 td 表示的线程的进程的 proc 结构的指针。 proc 结构包含可以描述所有者身份( p_ucred )、进程资源限制( p_limit )等的成员。 在由 proc 结构中的 p_ucred 成员指向的 ucred 结构中,有一个指向 prison 结构( cr_prison )的指针。
在 kern_jail.c 中,函数 jail() 然后使用给定的 jid 调用函数 jail_attach() 。 jail_attach() 调用函数 change_root() 来更改调用进程的根目录。 然后 jail_attach() 创建一个新的 ucred 结构,并在成功将 prison 结构附加到 ucred 结构之后,将新创建的 ucred 结构附加到调用进程。 从那时起,调用进程被认为是被 jail 的。 当在内核中调用带有新创建的 ucred 结构作为其参数的内核例程 jailed() 时,返回 1 以告知凭证与 jail 连接。 在 jail 中生成的所有进程的公共祖先进程,是运行 jail(8) 的进程,因为它调用 jail(2) 系统调用。 通过 execve(2) 执行程序时,它会继承其父进程的 ucred 结构的被 jail 属性,因此它具有一个被 jail 的 ucred 结构。
当从其父进程派生进程时,fork(2)系统调用使用 crhold() 来维护新派生进程的凭据。它本质上保持新派生子进程的凭据与其父进程一致,因此子进程也被禁闭。
在整个内核中都存在与被禁闭进程相关的访问限制。通常,这些限制只检查进程是否被禁闭,如果是,则返回一个错误。例如:
System V IPC 基于消息。进程可以相互发送这些告诉它们如何行动的消息。处理消息的函数包括:msgctl(3)、msgget(3)、msgsnd(3)和 msgrcv(3)。前面我提到过,您可以打开或关闭某些 sysctl 以影响 jail 的行为。其中一个 sysctl 是 security.jail.sysvipc_allowed 。默认情况下,此 sysctl 设置为 0。如果设置为 1,则会破坏拥有 jail 的整个目的;特权用户可以影响被 jail 化环境之外的进程。消息和信号之间的区别在于消息只包含信号编号。
/usr/src/sys/kern/sysv_msg.c:
msgget(key, msgflg) : msgget 返回(并可能创建)一个消息描述符,指定一个消息队列供其他函数使用。
msgctl(msgid, cmd, buf) :使用此函数,进程可以查询消息描述符的状态。
msgsnd(msgid, msgp, msgsz, msgflg) : msgsnd 向进程发送消息。
msgrcv(msgid, msgp, msgsz, msgtyp, msgflg) :一个进程使用此函数接收消息
在与这些函数对应的每个系统调用中,都有这个条件:
信号量系统调用允许进程通过对一组信号量进行一组原子操作来同步执行。基本上,信号量为进程提供了另一种锁定资源的方式。然而,等待正在使用的信号量的进程将会休眠,直到资源被释放。以下信号量系统调用在一个 jail 内被阻塞:semget(2),semctl(2)和 semop(2)。
/usr/src/sys/kern/sysv_sem.c:
semctl(semid, semnum, cmd, …) : semctl 在由 semid 指示的信号量队列上执行指定的 cmd 。
semget(key, nsems, flag) : semget 创建一个与 key 对应的信号量数组。 key and flag take on the same meaning as they do in msgget.
semop(semid, array, nops) : semop 执行由 array 指示的一组操作,对 semid 标识的信号量集进行操作。
System V IPC 允许进程共享内存。进程可以通过共享它们的虚拟地址空间的部分,直接与彼此通信,然后读取和写入存储在共享内存中的数据。这些系统调用在受限环境中被阻止:shmdt(2)、shmat(2)、shmctl(2)和 shmget(2)。
/usr/src/sys/kern/sysv_shm.c:
shmctl(shmid, cmd, buf) : shmctl 对由 shmid 标识的共享内存区执行各种控制操作。
shmget(key, size, flag) : shmget 访问或创建一个 size 字节的共享内存区。
shmat(shmid, addr, flag) : shmat 将由 shmid 标识的共享内存区附加到进程的地址空间。
shmdt(addr) : shmdt 分离先前附加在 addr 的共享内存区域。
Jail 特殊处理 socket(2) 系统调用和相关的底层套接字函数。为了确定是否允许创建特定套接字,它首先检查 sysctl security.jail.socket_unixiproute_only 是否设置。如果设置,只允许创建指定族为 PF_LOCAL 、 PF_INET 或 PF_ROUTE 的套接字。否则,将返回错误。
伯克利数据包过滤器以协议无关的方式为数据链路层提供原始接口。BPF 现在由 devfs(8)控制,是否可以在受限环境中使用。
有一些非常常见的协议,比如 TCP、UDP、IP 和 ICMP。IP 和 ICMP 在同一层级:网络层 2。为了防止被 jail 的进程绑定协议到特定地址,会采取一些预防措施,只有当设置了 nam 参数时才会生效。 nam 是指向 sockaddr 结构的指针,描述要绑定服务的地址。更精确的定义是 sockaddr “可用作引用每个地址的标识标签和长度的模板”。在函数 in_pcbbind_setup() 中, sin 是指向 sockaddr_in 结构的指针,其中包含 port、地址、长度和套接字的域家族,该套接字将被绑定。基本上,这禁止任何进程从 jail 到能够指定不属于调用进程所在 jail 的地址。
也许你想知道函数 prison_ip() 的作用是什么。 prison_ip() 给出三个参数,一个指向凭证的指针(由 cred 表示),任何标志和一个 IP 地址。如果 IP 地址不属于 jail,则返回 1,否则返回 0。从代码中可以看出,如果确实是一个不属于 jail 的 IP 地址,那么协议就不允许绑定到该地址。
即使在安全等级高于 0 时,内部用户 root 也不被允许取消或修改任何文件标志,例如不可变、追加和不可删除标志。
物理内存是通过 vm_page_t 结构进行逐页管理。 物理内存页面通过将其相应的 vm_page_t 结构放置在几个分页队列之一来进行分类。
页面可以处于有线、活动、非活动、缓存或自由状态。 除了有线状态之外,页面通常放置在表示其状态的双向链接列表队列中。 有线页面不会放置在任何队列上。
FreeBSD 实现了一个更复杂的页面队列,用于缓存和空闲页面,以实现页面着色。每个状态都涉及根据处理器的 L1 和 L2 缓存的大小排列的多个队列。当需要分配新页面时,FreeBSD 试图获得一个相对于 VM 对象而言从 L1 和 L2 缓存的角度合理对齐的页面。
另外,页面可以以引用计数或繁忙计数锁定。VM 系统还使用页面标志中的 PG_BUSY 位实现页面的“终极锁定”状态。
通常情况下,每个分页队列都以 LRU 方式运作。页面通常最初处于有线或活动状态。在有线状态下,页面通常与某个页表关联。VM 系统通过扫描更活跃的页面队列(LRU)中的页面来使页面老化,以便将它们移到不太活跃的分页队列中。移动到缓存中的页面仍然与 VM 对象关联,但是可能立即被重复使用。在空闲队列中的页面是真正空闲的。FreeBSD 试图最小化空闲队列中的页面数量,但必须保持一定数量的真正空闲页面,以便在中断时进行页面分配。
如果进程尝试访问其页表中不存在但存在于其中一个分页队列(如非活动队列或缓存队列)的页面,则会发生相对廉价的页面重新激活故障,导致页面被重新激活。如果页面根本不存在于系统内存中,则进程必须在页面从磁盘中带入时阻塞。
FreeBSD 动态调整其分页队列,并尝试保持各个队列中页面的合理比例,以及保持干净页面与脏页面的合理比例。发生的重新平衡量取决于系统的内存负载。这种重新平衡是由分页守护程序实现的,涉及清洗脏页面(与其后备存储同步),注意页面何时被活动引用(重置其在 LRU 队列中的位置或在队列之间移动它们),在队列不平衡时迁移页面之间的页面等。FreeBSD 的虚拟内存系统愿意接受合理数量的重新激活页面故障,以确定页面实际上是多么活跃或多么空闲。这导致更好的决策何时清洗或交换出页面。
FreeBSD 实现了通用“VM 对象”的概念。VM 对象可以与各种类型的后备存储相关联,包括未支持、交换支持、物理设备支持或文件支持存储。由于文件系统使用相同的 VM 对象来管理与文件相关的内核数据,因此结果是统一的缓冲区缓存。
VM 对象可以被阴影化。也就是说,它们可以堆叠在彼此之上。例如,您可以在文件支持的 VM 对象之上堆叠一个交换支持的 VM 对象,以实现 MAP_PRIVATE mmap()。这种堆叠也用于实现各种共享属性,包括 forked 地址空间的写时复制。
值得注意的是, vm_page_t 只能一次与一个 VM 对象相关联。VM 对象阴影化实现了同一页面在多个实例之间的感知共享。
vnode-backed VM 对象,如文件-backed 对象,通常需要维护其自己的干净/脏信息,独立于 VM 系统的干净/脏概念。例如,当 VM 系统决定将物理页同步到其备份存储时,VM 系统在将页面实际写入备份存储之前需要标记页面为干净。此外,文件系统需要能够将文件的某些部分或文件元数据映射到 KVM 中以对其进行操作。
用于管理这些实体的称为文件系统缓冲区, struct buf 's,或 bp 's。当文件系统需要在 VM 对象的一部分上操作时,通常将对象的部分映射到一个结构 buf 中,然后将结构 buf 中的页面映射到 KVM 中。以相同方式,磁盘 I/O 通常通过将对象的部分映射到缓冲区结构然后在缓冲区结构上发出 I/O 来发出。底层的 vm_page_t's 通常在 I/O 期间很忙碌。文件系统缓冲区还有自己的忙碌概念,对于更愿意操作文件系统缓冲区而不是硬 VM 页面的文件系统驱动程序代码很有用。
FreeBSD 保留了有限数量的 KVM 以保留从结构 bufs 的映射,但应明确指出,这个 KVM 仅用于保存映射,不限制缓存数据的能力。物理数据缓存严格是 vm_page_t 的功能,而不是文件系统缓冲区的功能。然而,由于文件系统缓冲区用于占位 I/O,它们确实限制了可能的并发 I/O 数量。但是,由于通常有几千个文件系统缓冲区可用,这通常不是问题。
FreeBSD 将物理页表拓扑结构与 VM 系统分开。所有硬 per-process 页表都可以动态重建,通常被视为一次性的。特殊的页表,如管理 KVM 的页表通常是永久预分配的。这些页表不是一次性的。
FreeBSD 通过 vm_map_t 和 vm_entry_t 结构将 vm_objects 的部分与虚拟内存中的地址范围关联起来。页面表直接从 vm_map_t / vm_entry_t / vm_object_t 层次结构合成。请记住,我提到物理页面仅与 vm_object 直接关联;这并不完全正确。 vm_page_t 也链接到它们积极关联的页面表中。一个 vm_page_t 可以链接到多个称为页面表的 pmap。然而,层次关联保持不变,因此对同一对象中同一页面的所有引用都引用相同的 vm_page_t ,从而使我们在整个系统中实现了缓冲区缓存统一化。
FreeBSD 使用 KVM 保存各种内核结构。在 KVM 中保存的最大实体是文件系统缓冲区缓存。也就是,与 struct buf 实体相关的映射。
不像 Linux,FreeBSD 不会将所有物理内存映射到 KVM 中。这意味着 FreeBSD 可以处理 32 位平台上高达 4G 的内存配置。事实上,如果 MMU 有能力的话,FreeBSD 理论上可以处理多达 8TB 的内存配置。然而,由于大多数 32 位平台只能映射 4GB 的内存,这一点无关紧要。
KVM 通过几种机制进行管理。用于管理 KVM 的主要机制是区域分配器。区域分配器获取一块 KVM 并将其分割成具有恒定大小的内存块,以便为特定类型的结构分配空间。您可以使用 vmstat -m 来查看按区域分解的当前 KVM 利用情况的概述。
为使 FreeBSD 内核动态调优,已经做出了协同努力。通常情况下,您无需干预除 maxusers 和 NMBCLUSTERS 内核配置选项之外的任何内容。也就是说,内核编译选项通常在/usr/src/sys/i386/conf/CONFIG_FILE 中指定。您可以在/usr/src/sys/i386/conf/LINT 中找到所有可用内核配置选项的描述。
在大型系统配置中,您可能希望增加 maxusers 。通常取值范围为 10 到 128。请注意,将 maxusers 提高过高可能会导致系统溢出可用的 KVM,导致不可预测的操作。最好将 maxusers 保持在某个合理的数字,并添加其他选项,如 NMBCLUSTERS ,以增加特定资源。
如果您的系统将大量使用网络,您可能希望增加 NMBCLUSTERS 。典型值范围为 1024 到 4096。
NBUF 参数也是传统上用于扩展系统的。该参数确定系统可以用于映射 I/O 文件系统缓冲区的 KVA 量。请注意,该参数与统一缓冲区缓存完全无关!该参数在 3.0-CURRENT 及更高版本内核中动态调整,通常不应手动调整。我们建议您不要尝试指定 NBUF 参数。让系统自行选择。值过小可能导致文件系统操作极其低效,而值过大可能通过使太多页面被固定而使页面队列饥饿。
默认情况下,FreeBSD 内核未经过优化。您可以使用内核配置中的 makeoptions 指令设置调试和优化标志。请注意,除非您可以容纳结果为大型(通常为 7 MB 以上)的内核,否则不应使用 -g 。
Sysctl 提供了一种在运行时调整内核参数的方法。通常情况下,您不需要干预任何 sysctl 变量,特别是与 VM 相关的变量。
运行时虚拟机和系统调整相对简单。首先,尽可能在您的 UFS/FFS 文件系统上使用 Soft Updates。 /usr/src/sys/ufs/ffs/README.softupdates 包含了如何配置它的说明(和限制)。
其次,配置足够的交换空间。您应该在每个物理磁盘上配置一个交换分区,最多四个,甚至在您的“工作”磁盘上也是如此。您的交换空间应该至少是主内存的 2 倍,如果您的内存不多甚至可能更多。您还应根据您打算在机器上放置的最大内存配置来设置交换分区的大小,以便以后不必重新分区您的磁盘。如果您想能够容纳崩溃转储,您的第一个交换分区的大小必须至少与主内存一样大,并且 /var/crash 必须具有足够的免费空间来容纳转储。
基于 NFS 的交换在 4.X 或之后的系统上是完全可以接受的,但您必须意识到 NFS 服务器将承受分页负载的大部分。
本章简要介绍了为 FreeBSD 编写设备驱动程序的内容。在这里,设备是指系统中大多数用于硬件相关的东西,如磁盘、打印机或带有键盘的图形显示器。设备驱动程序是操作系统的软件组件,用于控制特定设备。还有所谓的伪设备,其中设备驱动程序在软件中模拟设备的行为,而没有任何特定的底层硬件。设备驱动程序可以静态地编译到系统中,也可以通过动态内核链接器 `kld' 按需加载。
UNIX®-类操作系统中的大多数设备通过设备节点访问,有时也称为特殊文件。这些文件通常位于文件系统层次结构中的 /dev 目录下。
设备驱动程序大致可以分为两类;字符设备驱动程序和网络设备驱动程序。
kld 接口允许系统管理员动态地向运行中的系统添加和删除功能。这允许设备驱动程序编写者将其新更改加载到运行中的内核中,而无需不断重新启动以测试更改。
kld 接口用于:
kldload - 加载一个新的内核模块
kldunload - 卸载一个内核模块
kldstat - 列出加载的模块
内核模块的骨架布局
FreeBSD 提供了一个系统 makefile 来简化编译内核模块的过程。
运行 make ,将使用这个 makefile 创建一个名为 skeleton.ko 的文件,可以通过键入以下命令将其加载到内核中:
一个字符设备驱动程序是直接向用户进程传输数据的驱动程序。这是最常见的设备驱动程序类型,在源树中有许多简单的示例。
这个简单的伪设备示例会记住写入它的任何值,并在读取时将它们回显出来。
示例 1。适用于 FreeBSD 10.X - 12.X 的样本回显伪设备驱动程序示例。
有了这个驱动程序加载,请尝试:
真实的硬件设备在下一章中描述。
其他 UNIX®系统可能支持第二种称为块设备的磁盘设备。块设备是指内核提供缓存的磁盘设备。这种缓存使块设备几乎无法使用,或者至少是非常不可靠的。缓存会重新排序写操作的顺序,使应用程序无法在任一特定时刻知道确切的磁盘内容。
这导致无法预测和可靠地恢复磁盘数据结构(文件系统、数据库等)。由于写入可能会延迟,内核无法向应用程序报告哪个特定的写操作遇到写错误,这进一步加剧了一致性问题。
出于这个原因,没有严肃的应用程序依赖块设备,事实上,几乎所有直接访问磁盘的应用程序都非常努力地指定应始终使用字符(或“原始”)设备。由于将每个磁盘(分区)的别名实现为具有不同语义的两个设备会显著复杂化相关的内核代码,FreeBSD 在现代化磁盘 I/O 基础架构的一部分中放弃了对缓存磁盘设备的支持。
与设备节点不同,网络设备的驱动程序不使用设备节点来进行访问。它们的选择是基于内核内部做出的其他决策,而不是调用 open(),使用网络设备通常是通过使用系统调用 socket(2)来引入的。
更多信息请参见 ifnet(9),回环设备的源代码以及 Bill Paul 的网络驱动程序。
特别感谢 Matthew N. Dodd, Warner Losh, Bill Paul, Doug Rabson, Mike Smith, Peter Wemm 和 Scott Long。
本章详细解释了 Newbus 设备框架。
设备驱动程序是一种软件组件,提供内核对外围设备(例如磁盘、网络适配器)的通用视图与外设的实际实现之间的接口。设备驱动程序接口(DDI)是内核与设备驱动程序组件之间定义的接口。
在 UNIX® 以及 FreeBSD 中,曾经定义过四种类型的设备:
块设备驱动程序
字符设备驱动程序
网络设备驱动程序
伪设备驱动程序
块设备以固定大小的数据块的方式执行。这种类型的驱动程序依赖于所谓的缓冲区缓存,它在内存的一个专用部分中缓存访问的数据块。通常,这种缓冲区缓存基于后台写操作,这意味着当数据在内存中被修改时,它会在系统进行定期磁盘刷新时将数据同步到磁盘,从而优化写入操作。
然而,在 FreeBSD 4.0 及以后的版本中,块设备和字符设备之间的区别变得不存在。
Newbus 是基于抽象层的新总线架构的实现,在 FreeBSD 3.0 中引入了 Alpha port后才开始使用。直到 4.0 版本之前,它才成为设备驱动程序的默认系统。其目标是提供一种更面向对象的方式,用于连接主机系统提供给操作系统的各种总线和设备。
其主要特点包括但不限于:
动态附加
驱动程序的简易模块化
伪总线
最显著的变化之一是从平面和特定系统迁移到设备树布局。
在顶层是“根”设备,它是所有其他设备的父设备。对于每种体系结构,通常有一个名为“root”的子设备,附有诸如主机到 PCI 桥等内容。对于 x86,这个“root”设备是“nexus”设备。对于 Alpha,Alpha 的各种不同型号都有不同的顶层设备,对应不同的硬件芯片组,包括 lca、apecs、cia 和 tsunami。
在 Newbus 上下文中,设备表示系统中的单个硬件实体。例如,每个 PCI 设备都由一个 Newbus 设备表示。系统中的任何设备都可以有子设备;具有子设备的设备通常称为“总线” 。系统中常见总线的示例包括 ISA 和 PCI,它们分别管理连接到 ISA 和 PCI 总线的设备列表。
通常,不同类型总线之间的连接由“桥接”设备表示,通常具有一个连接到附加总线的子设备。一个例子是 PCI-to-PCI 桥接器,它在父 PCI 总线上由一个名称为 pcibN 的设备表示,并在附加总线上有一个名为 pciN 的子设备。这种布局简化了 PCI 总线树的实现,允许对顶层总线和桥接总线使用共同的代码。
新总线架构中的每个设备都要求其父级映射其资源。然后父级继续向其父级请求,直到达到中枢。所以,基本上,中枢是新总线系统中唯一知道所有资源的部分。
资源分配可以在设备树中的任何地方控制。例如,在许多 Alpha 平台上,ISA 中断与 PCI 中断分别管理,并且 ISA 中断的资源分配由 Alpha 的 ISA 总线设备管理。在 IA-32 上,ISA 和 PCI 中断都由顶层中枢设备管理。对于 ports,内存和 port 地址空间由单个实体管理 - IA-32 上的中枢和 Alpha 上的相关芯片组驱动程序(例如,CIA 或海啸)。
为了规范对内存和port映射资源的访问,Newbus 集成了来自 NetBSD 的 bus_space API。这些 API 提供了一个单一的 API,用于替换 inb/outb 和直接内存读取/写入。这样做的好处是,一个驱动程序可以轻松地使用内存映射寄存器或port映射寄存器(一些硬件同时支持两者)。
此支持已集成到资源分配机制中。当分配资源时,驱动程序可以从资源中检索相关的 bus_space_tag_t 和 bus_space_handle_t 。
Newbus 还允许在专门用于此目的的文件中定义接口方法。这些文件是在 src/sys 层级结构下找到的.m 文件。
新总线系统的核心是可扩展的“基于对象的编程”模型。系统中的每个设备都有一个支持的方法表。系统和其他设备使用这些方法来控制设备并请求服务。设备支持的不同方法由多个“接口”定义。一个“接口”只是由设备实现的一组相关方法。
在新总线系统中,设备的方法由系统中的各种设备驱动程序提供。当设备在自动配置期间连接到驱动程序时,它使用驱动程序声明的方法表。设备可以稍后与其驱动程序分离,并重新连接到具有新方法表的新驱动程序。这允许动态替换驱动程序,这对驱动程序开发很有用。
接口由类似于用于定义文件系统的 vnode 操作的语言描述的接口定义语言描述。接口将存储在一个方法文件中(通常命名为 foo_if.m)。
示例 1. Newbus 方法
当编译此接口时,它会生成一个名为 "foo_if.h" 的头文件,其中包含函数声明:
还会创建一个源文件 "foo_if.c",与自动生成的头文件配套;它包含这些函数的实现,这些函数查找对象方法表中相关函数的位置并调用该函数。
系统定义了两个主要接口。第一个基本接口称为 "设备" ,包含所有设备相关的方法。"设备" 接口中的方法包括 "探测","连接" 和 "断开" 用于控制硬件检测,以及 "关机","暂停" 和 "恢复" 用于关键事件通知。
第二个更复杂的接口是 "总线" 。该接口包含适用于具有子设备的设备的方法,包括用于访问特定于总线的每个设备信息^[1]^,事件通知( child_detached , driver_added )和资源管理( alloc_resource , activate_resource , deactivate_resource , release_resource )。
"总线" 接口中的许多方法为总线设备的某个子设备提供服务。这些方法通常使用前两个参数来指定提供服务的总线和请求服务的子设备。为了简化驱动程序代码,这些方法中的许多都有访问器函数,这些函数查找父级并在父级上调用方法。例如,方法 BUS_TEARDOWN_INTR(device_t dev, device_t child, …) 可以使用函数 bus_teardown_intr(device_t child, …) 调用。
系统中的一些总线类型定义了附加接口,以提供对总线特定功能的访问。例如,PCI 总线驱动程序定义了具有两种方法 read_config 和 write_config 用于访问 PCI 设备配置寄存器的 "pci" 接口。
由于 Newbus API 非常庞大,本节尝试对其进行一些文档化。更多信息将在本文档的下一个修订版中提供。
src/sys/[arch]/[arch] - 特定机器架构的内核代码存放在这个目录中。例如, i386 架构,或者 SPARC64 架构。
src/sys/dev/[bus] - 特定 [bus] 的设备支持存放在这个目录中。
该目录包含 PCI 总线支持代码。
PCI/ISA 设备驱动程序位于此目录中。在 FreeBSD 版本 4.0 中,PCI/ISA 总线支持代码曾经存在于此目录中。
devclass_t - 这是指向 struct devclass 的指针类型定义。
device_method_t - 这与 kobj_method_t 相同(请参阅 src/sys/kobj.h)。
device_t - 这是指向 struct device 的指针类型定义。 device_t 代表系统中的设备。这是一个内核对象。有关实现细节,请参阅 src/sys/sys/bus_private.h。
driver_t - 这是引用 struct driver 的类型定义。 driver 结构体是 device 内核对象的一类;它还保存了驱动程序私有的数据。
driver_t Implementation
一个 device_state_t 类型,它是一个枚举, device_state 。它包含 Newbus 设备在自动配置过程之前和之后的可能状态。
设备状态 _device_state_t
bus_generic_read_ivar(9) 和 bus_generic_write_ivar(9)
通用串行总线(USB)是一种将设备连接到个人计算机的新方法。总线架构具有双向通信功能,并作为设备变得更智能并需要与主机进行更多交互的响应而开发。USB 支持已包含在所有当前 PC 芯片组中,因此在所有最近构建的 PC 中都可以使用。苹果推出的仅支持 USB 的 iMac 对硬件制造商生产其设备的 USB 版本起到了重要的推动作用。未来的 PC 规范规定,PC 上的所有传统连接器都应该被一个或多个 USB 连接器取代,提供通用即插即用功能。USB 硬件的支持在 NetBSD 的早期阶段就已经可用,并由 Lennart Augustsson 为 NetBSD 项目开发。该代码已被移植到 FreeBSD,我们目前正在维护一个共享的代码库。为了实现 USB 子系统,USB 的许多特性都是重要的。
Lennart Augustsson 为 NetBSD 项目完成了大部分 USB 支持的实现。非常感谢他做出的大量工作。也要感谢 Ardy 和 Dirk 对本文的评论和校对。
设备直接连接到计算机上的ports,或连接到称为集线器的设备上,形成类似树状的设备结构。
设备可以在运行时连接和断开。
设备可以暂停自身并触发主机系统的恢复。
由于设备可以从总线供电,主机软件必须跟踪每个集线器的功率预算。
不同设备类型对服务质量的不同要求,加上最多可连接到同一总线的 126 个设备,要求在共享总线上进行传输的适当调度,以充分利用可用的 12Mbps 带宽。(USB 2.0 可达 400Mbps 以上)
设备是智能的,并包含有关自身的易于访问的信息
USB 子系统及连接到其上的设备的驱动程序开发受到已开发和将要开发的规范的支持。这些规范可从 USB 主页公开获取。苹果一直在推动基于标准的驱动程序开发,通过在其操作系统 MacOS 中提供通用类的驱动程序,并不鼓励为每个新设备编写单独的驱动程序。本章尝试整理有关在 FreeBSD/NetBSD 中理解 USB 2.0 实现堆栈的基本信息。建议与相关的 2.0 规范和其他开发者资源一起阅读:
USB 2.0 规范 (http://www.usb.org/developers/docs/usb20_docs/)
通用主机控制器接口 (UHCI) 规范 (ftp://ftp.netbsd.org/pub/NetBSD/misc/blymn/uhci11d.pdf)
开放主机控制器接口(OHCI)规范(ftp://ftp.compaq.com/pub/supportinformation/papers/hcir1_0a.pdf)
USB 主页开发人员部分(http://www.usb.org/developers/)
FreeBSD 中的 USB 支持可以分为三个层。最底层包含主机控制器驱动程序,提供与硬件及其调度功能的通用接口。它支持硬件的初始化,传输的调度以及已完成和/或失败的传输的处理。每个主机控制器驱动程序实现了一个虚拟集线器,提供对控制机器背面根处的寄存器的硬件独立访问。
中间层处理设备的连接和断开,设备的基本初始化,驱动程序选择,通信通道(管道)和资源管理。此服务层还控制默认管道和通过它们传输的设备请求。
顶层包含支持特定(类别的)设备的各个驱动程序。这些驱动程序实现了在默认管道之外使用的协议。它们还实现了附加功能,使设备可供内核或用户区的其他部分使用。它们使用服务层提供的 USB 驱动程序接口(USBDI)。
主机控制器(HC)控制总线上数据包的传输。每隔 1 毫秒发送一帧。在每一帧的开始,主机控制器会生成一个帧起始(SOF)数据包。
SOF 数据包用于同步到帧的开始并跟踪帧号。在每帧内,数据包的传输可以是从主机到设备(输出),也可以是从设备到主机(输入)。传输始终由主机发起(轮询式传输)。因此,每个 USB 总线只能有一个主机。每个数据包的传输都有一个状态阶段,在该阶段数据的接收方可以返回 ACK(确认接收)、NAK(重试)、STALL(错误条件)或无回应(数据混乱阶段、设备不可用或已断开连接)。USB 2.0 规范的第 8.5 节更详细地解释了数据包的细节。USB 总线上可以发生四种不同类型的传输:控制传输,块传输,中断传输和等时传输。下面描述了各种传输类型及其特性。
USB 总线上设备与设备驱动程序之间的大型传输被主控制器或主控制器驱动程序分割成多个数据包。
设备请求(控制传输)到默认端点是特殊的。它们由两到三个阶段组成:设置(SETUP)、数据(可选)和状态(STATUS)。设置数据包被发送到设备。如果有数据阶段,数据包的方向在设置数据包中给出。状态阶段中的方向与数据阶段期间的方向相反,或者如果没有数据阶段,则为 IN。主控制器硬件还提供具有根ports当前状态和自上次状态更改寄存器重置以来发生的更改的寄存器。通过 USB 规范建议的虚拟化集线器提供对这些寄存器的访问。虚拟集线器必须符合该规范第 11 章中给出的集线器设备类。它必须提供一个默认管道,通过该管道可以将设备请求发送到它。它返回标准和集线器类别特定的描述符集。它还应提供一个中断管道,报告发生在其ports上的更改。目前有两种可用的主控制器规范:英特尔的通用主控制器接口(UHCI)和康柏、微软和国家半导体的开放主控制器接口(OHCI)。UHCI 规范旨在通过要求主控制器驱动程序为每个帧提供完整的传输计划来减少硬件复杂性。OHCI 类型控制器更加独立,通过提供一个更抽象的接口来自行完成大量工作。
UHCI 主机控制器维护一个包含 1024 个指向每帧数据结构的指针的帧列表。它了解两种不同的数据类型:传输描述符(TD)和队列头(QH)。每个 TD 代表与设备端点进行通信的要传输的数据包。 QH 是将 TD(和 QH)组合在一起的一种方式。
每个传输由一个或多个数据包组成。 UHCI 驱动程序将大型传输拆分为多个数据包。对于每个传输,除了等时传输外,都会分配一个 QH。对于每种传输类型,这些 QH 会被收集到该类型的一个 QH 中。 等时传输必须首先执行,因为有固定的延迟要求,并且直接通过帧列表中的指针引用。最后一个等时 TD 引用了该帧的中断传输的 QH。所有中断传输的 QH 都指向控制传输的 QH,而控制传输的 QH 又指向批量传输的 QH。以下图表提供了这方面的图形概述:
这导致在每个帧中运行以下时间表。在从帧列表中获取当前帧的指针后,控制器首先执行该帧中所有等时数据包的传输描述符(TD)。这些 TD 中的最后一个引用了该帧的中断传输的队列头(QH)。主控制器然后会从该 QH 下降到各个中断传输的 QH。完成该队列后,中断传输的 QH 将引导控制器到所有控制传输的 QH。它将执行那里安排的所有子队列,然后执行所有排队在批量 QH 的传输。为了便于处理已完成或失败的传输,硬件会在每帧结束时生成不同类型的中断。在传输的最后一个 TD 中,HC 驱动程序会设置“传输完成时中断”位,以在传输完成时标记中断。如果 TD 达到其最大错误计数,将标记错误中断。如果在 TD 中设置了短包检测位,并且传输的长度小于设置的数据包长度,则会标记此中断,以通知控制器驱动程序传输已完成。主控制器驱动程序的任务是找出哪个传输已完成或产生错误。调用中断服务例程时,将定位所有已完成的传输并调用它们的回调函数。
请参阅 UHCI 规范以获取更详细的描述。
编程 OHCI 主机控制器要简单得多。控制器假定一组端点可用,并且知道帧中传输类型的调度优先级和顺序。主机控制器使用的主要数据结构是端点描述符(ED),附加到该端点描述符的是传输描述符(TD)队列。ED 包含端点允许的最大数据包大小,控制器硬件进行数据包分割。在每次传输后更新数据缓冲区指针,并且当起始指针和结束指针相同时,TD 将被放入完成队列。四种类型的端点(中断,等时,控制和块)都有自己的队列。控制和块端点各自排队。中断 ED 在树中排队,树中的级别定义了它们运行的频率。
主机控制器每帧运行的调度如下。控制器首先运行非周期性控制和块队列,直到由 HC 驱动程序设置的时间限制。然后运行该帧编号的中断传输,通过使用帧编号的低五位作为中断 ED 树级别 0 的索引。在这个树的末尾连接等时 ED,并依次遍历这些 ED。等时 TD 包含传输应在其中运行的第一帧的帧编号。运行所有周期性传输之后,再次遍历控制和块队列。定期调用中断服务例程来处理完成队列,并为每次传输调用回调函数并重新安排中断和等时端点。
查看 UHCI 规范以获取更详细的描述。中间层以受控方式访问设备,并维护不同驱动程序和服务层正在使用的资源。该层负责以下方面:
设备配置信息
用于与设备通信的管道
探测、连接和分离设备。
每个设备提供不同级别的配置信息。 每个设备都有一个或多个配置,在探测/附加期间选择一个配置。 配置提供功率和带宽要求。 在每个配置中,可以有多个接口。 设备接口是端点的集合。 例如,USB 扬声器可以具有用于音频数据(音频类)的界面和用于旋钮,拨号和按钮(HID 类)的界面。 配置中的所有接口同时处于活动状态,并且可以由不同的驱动程序附加。 每个接口可以有备用项,提供不同的服务质量参数。 例如相机可以用来提供不同的帧大小和每秒帧数。
在每个接口内,可以指定 0 个或更多个端点。端点是与设备通信的单向访问点。它们提供缓冲区,用于临时存储来自设备的传入或传出数据。每个端点在配置内有一个唯一地址,即端点号加上其方向。默认端点,端点 0,不属于任何接口,并且在所有配置中都可用。它由服务层管理,不直接提供给设备驱动程序。
此分层配置信息在设备中由一组标准描述符描述(请参阅 USB 规范的第 9.6 节)。它们可以通过获取描述符请求来请求。服务层会缓存这些描述符,以避免在 USB 总线上进行不必要的传输。通过函数调用提供对描述符的访问。
设备描述符:有关设备的一般信息,如供应商、产品和修订 ID,支持的设备类、子类和协议(如果适用),默认端点的最大包大小等。
配置描述符:此配置中的接口数量、支持的挂起和恢复功能以及电源要求。
接口描述符:接口类、子类和协议(如果适用)、接口的备用设置数量以及端点数量。
端点描述符:端点地址、方向和类型、支持的最大数据包大小以及轮询频率(如果类型为中断端点)。默认端点(端点 0)没有描述符,并且在接口描述符中永远不计数。
字符串描述符:在其他描述符中,为某些字段提供了字符串索引。这些索引可用于检索描述性字符串,可能是多种语言。
类规范可以添加它们自己的描述符类型,通过 GetDescriptor 请求可用。
管道通信:设备上的端点之间的流量通过所谓的管道进行。驱动程序向端点提交传输到管道,并提供在传输完成或失败时调用的回调(异步传输),或等待完成(同步传输)。端点的传输在管道中进行序列化。传输可以完成、失败或超时(如果设置了超时)。传输的超时有两种类型。传输可能由于 USB 总线上的超时(毫秒级)而发生。这些超时被视为失败,可能是由于设备断开连接。第二种形式的超时是在软件中实现的,当传输在指定的时间内未完成时触发(秒级)。这些超时是由设备否定(NAK)传输的数据包而触发的。造成这种情况的原因是设备尚未准备好接收数据,缓冲区下溢或上溢,或协议错误。
如果通过管道的传输大于关联端点描述符中指定的最大数据包大小,则主机控制器(OHCI)或 HC 驱动程序(UHCI)将将传输拆分为最大数据包大小的数据包,最后一个数据包可能小于最大数据包大小。
有时,设备返回的数据量少于请求的数据量并不是问题。例如,对于调制解调器的批量输入传输,可能请求 200 字节的数据,但调制解调器在那时只有 5 字节可用。驱动程序可以设置短数据包(SPD)标志。它允许主机控制器接受一个数据包,即使传输的数据量少于请求的数据量。该标志仅对输入传输有效,因为要发送到设备的数据量始终是预先知道的。如果在传输过程中设备发生不可恢复的错误,则管道将被停止。在接受或发送更多数据之前,驱动程序需要解决停止的原因,并通过在默认管道上发送清除端点停止设备请求来清除端点停止条件。默认端点不应该停止。
有四种不同类型的端点和相应的管道:- 控制管道/默认管道:每个设备有一个控制管道,连接到默认端点(端点 0)。该管道携带设备请求和相关数据。与通过默认管道和其他管道进行的传输之间的区别在于传输的协议在 USB 规范中描述。这些请求用于重置和配置设备。USB 规范的第 9 章提供了每个设备必须支持的基本命令集。在此管道上支持的命令可以通过设备类规范进行扩展,以支持附加功能。
大容量管道: 这是 USB 等效于原始传输媒介。
中断管道: 主机向设备发送数据请求,如果设备没有要发送的数据,它将 NAK 数据包。中断传输的调度频率在创建管道时指定。
等时管道: 这些管道用于等时数据,例如视频或音频流,具有固定的延迟,但无保证交付。当前实现中提供了对这种类型管道的一些支持。如果在传输期间发生错误或设备因例如缺少缓冲区空间而否定数据包(NAK)而设备确认包时,在控制、大容量和中断传输中的数据包会重试。然而,如果传递失败或数据包被否定(NAK)时,等时数据包不会重试,因为这可能会违反时间约束。
在创建 pipe 时计算必要带宽的可用性。传输在 1 毫秒的帧内安排。帧内的带宽分配由 USB 规范第 5.6 节 [2] 规定。等时传输和中断传输允许消耗帧内多达 90% 的带宽。控制传输和批量传输的数据包在所有等时和中断数据包之后安排,并将消耗所有剩余的带宽。
关于传输调度和带宽回收的更多信息,可以在 USB 规范的第 5 章,UHCI 规范的第 1.3 节和 OHCI 规范的第 3.4.2 节中找到。
在中心通知有新设备连接后,服务层打开port,为设备提供 100 毫安的电流。此时,设备处于默认状态,并监听设备地址 0。服务层将通过默认管道继续检索各种描述符。之后,它将发送一个设置地址请求,将设备从默认设备地址(地址 0)移开。多个设备驱动程序可能能够支持该设备。例如,调制解调器驱动程序可能通过 AT 兼容接口支持 ISDN TA。然而,特定型号的 ISDN 适配器的驱动程序可能能够为该设备提供更好的支持。为了支持这种灵活性,探针返回优先级,指示其支持水平。对产品特定版本的支持排名最高,通用驱动程序排名最低。也可能是,如果一个配置中有多个接口,多个驱动程序可以连接到一个设备。每个驱动程序只需要支持接口的一个子集。
当搜索新连接设备的驱动程序时,首先检查设备特定驱动程序。如果未找到,探测代码将会迭代所有支持的配置,直到驱动程序在某个配置中附加。为支持具有不同接口上多个驱动程序的设备,探测程序将迭代配置中所有尚未被驱动程序占用的接口。忽略超出集线器功率预算的配置。在附加过程中,驱动程序应将设备初始化为正确状态,但不应复位设备,因为这将导致设备从总线断开连接并重新开始对其进行探测的过程。为避免占用不必要的带宽,在附加时不应声明中断管道,而应推迟分配管道,直到打开文件并实际使用数据为止。在关闭文件时,管道应再次关闭,即使设备可能仍处于附加状态。
设备驱动程序应当对与设备的任何事务接收到错误有所期望。USB 的设计支持并鼓励随时断开设备。驱动程序应确保在设备消失时采取正确的措施。
此外,已断开并重新连接的设备将不会重新附加到相同的设备实例。当更多设备支持序列号(请参阅设备描述符)或其他定义设备标识的方法被开发出来时,这种情况可能会发生变化。
设备的断开是由集线器在传递给集线器驱动程序的中断数据包中发出的。状态更改信息指示哪个port已经看到连接更改。对于连接在该port上的设备的所有设备驱动程序的设备分离方法将被调用,并清理结构。如果port状态表明在此期间已连接了一个设备到该port,则将启动用于探测和附加设备的过程。设备复位将在集线器上产生一个断开连接的序列,并将如上所述进行处理。
除了默认管道外,USB 规范未定义其他管道上使用的协议。关于此信息可以从各种来源找到。最准确的来源是 USB 主页上的开发者部分。从这些页面上,可以找到越来越多的设备类规范。这些规范指定了符合标准的设备在驱动程序视角下应该是什么样子,需要提供的基本功能以及在通信通道上应该使用的协议。USB 规范包括 Hub 类的描述。已创建了用于键盘、平板、条形码阅读器、按钮、旋钮、开关等的人机界面设备(HID)的类规范。第三个例子是用于大容量存储设备的类规范。有关设备类的完整列表,请参阅 USB 主页上的开发者部分。
然而,对于许多设备,协议信息尚未公开。有关使用的协议的信息可能可以从制造设备的公司获得。一些公司可能要求您在提供规范之前签署保密协议。在大多数情况下,这将阻止将驱动程序开源化。
另一个信息来源是 Linux 驱动程序源代码,因为许多公司已开始为其设备提供 Linux 驱动程序。与那些驱动程序的作者联系以获取信息源始终是一个好主意。
示例:人机接口设备 人机接口设备的规范,如键盘、鼠标、平板、按钮、拨号等,在其他设备类规范中被引用,并在许多设备中使用。
例如,音频扬声器为数字到模拟转换器提供端点,可能还提供一个用于麦克风的额外管道。它们还为设备前面的按钮和拨号提供一个 HID 端点的单独接口。监视器控制类也是如此。通过可用的内核和用户空间库以及 HID 类驱动程序或通用驱动程序,构建对这些接口的支持是直截了当的。作为一个在一个配置中由不同设备驱动程序驱动的接口的示例的另一个设备是带有内置传统鼠标的廉价键盘。为了避免在设备中包含 USB 集线器的成本,制造商将从键盘背面的 PS/2 接收的鼠标数据和键盘的按键合并到同一配置中的两个独立接口中。鼠标和键盘驱动程序分别连接到适当的接口,并为两个独立端点分配管道。
示例:固件下载 许多已开发的设备基于通用处理器,并添加了额外的 USB 核心。由于 USB 设备的驱动程序和固件开发仍然非常新颖,许多设备在连接后需要下载固件。
遵循的过程很简单。设备通过供应商和产品 ID 进行标识。第一个驱动程序探测并附加到设备上,并将固件下载到设备中。之后,设备会软重置自身,并且驱动程序会被分离。在短暂的暂停后,设备会在总线上宣布自己的存在。设备将已更改其供应商/产品/修订 ID,以反映其已被提供固件的事实,因此第二个驱动程序将对其进行探测并附加到其上。
这些类型设备的一个示例是基于 EZ-USB 芯片的 ActiveWire I/O 板。对于这个芯片,有一个通用的固件下载器可用。下载到 ActiveWire 板中的固件会更改修订 ID。然后,它将对 EZ-USB 芯片的 USB 部分执行软重置,以断开与 USB 总线的连接,然后重新连接。
例子:大容量存储设备支持大容量存储设备主要围绕现有协议构建。Iomega USB Zipdrive 基于其驱动器的 SCSI 版本。SCSI 命令和状态消息被包装在块中,并通过批量管道传输到设备,模拟 USB 线上的 SCSI 控制器。ATAPI 和 UFI 命令以类似的方式进行支持。
大容量存储规范支持命令块的两种不同包装方式。 最初的尝试是通过默认管道传送命令和状态,并使用批量传输来移动主机和设备之间的数据。 根据经验,设计了第二种方法,它基于包装命令和状态块,并通过批量输出和输入端点将它们发送出去。 该规范确切地规定了在何时发生以及在遇到错误条件时应该做什么。 在为这些设备编写驱动程序时最大的挑战是将基于 USB 的协议整合到对大容量存储设备的现有支持中。 CAM 提供了一种相当直接的方法来执行此操作。 从历史上看,ATAPI 就没有那么简单了,因为历史上 IDE 接口从来没有太多不同的外观。
对来自 Y-E Data 的 USB 软驱的支持也不那么简单,因为已设计了新的命令集。
本章介绍了编写 ISA 设备驱动程序相关的问题。这里呈现的伪代码非常详细,类似于真实代码,但仍然只是伪代码。它避免了与讨论主题无关的细节。真实驱动程序的源代码中可以找到真实示例。特别是驱动程序 ep 和 aha 是很好的信息来源。
一个典型的 ISA 驱动程序需要以下包含文件:
它们描述了 ISA 和通用总线子系统的特定内容。
总线子系统以面向对象的方式实现,其主要结构通过关联的方法函数访问。
ISA 驱动程序实现的总线方法列表与任何其他总线的方法类似。对于一个名为"xxx"的假设驱动程序,它们将是:
static void xxx_isa_identify (driver_t *, device_t); 通常用于总线驱动程序,而不是设备驱动程序。但对于 ISA 设备,此方法可能具有特殊用途:如果设备提供一些特定于设备的(非即插即用)方式来自动检测设备,则此例程可能会实现它。
static int xxx_isa_probe (device_t dev); 探测已知(或即插即用)位置的设备。此例程还可以适应于部分配置设备的设备特定参数的自动检测。
static int xxx_isa_attach (device_t dev); 附加并初始化设备。
static int xxx_isa_detach (device_t dev); 在卸载驱动程序模块之前分离设备。
static int xxx_isa_shutdown (device_t dev); 在系统关机之前执行设备关机。
static int xxx_isa_suspend (device_t dev); 在系统进入省电状态之前暂停设备。也可能中止进入省电状态的转换。
static int xxx_isa_resume (device_t dev); 在从省电状态返回后恢复设备活动。
xxx_isa_probe() 和 xxx_isa_attach() 是必需的,其余的例程根据设备的需求而定。
驱动程序与系统连接,具有以下一组描述。
这里的 struct xxx_softc 是一个特定于设备的结构,包含私有驱动程序数据和驱动程序资源的描述符。总线代码会根据需要为每个设备自动分配一个 softc 描述符。
如果驱动程序实现为可加载模块,则会调用 load_function() 来执行驱动程序特定的初始化或清理工作,当加载或卸载驱动程序时会将 load_argument 作为其参数之一传递。如果驱动程序不支持动态加载(换句话说,它必须始终链接到内核中),那么这些值应设置为 0,最后一个定义将如下所示:
如果驱动程序用于支持 PnP 的设备,则必须定义支持的 PnP ID 表。该表包括此驱动程序支持的一系列 PnP ID,以及具有这些 ID 的硬件类型和型号的人类可读描述。它看起来像:
如果驱动程序不支持 PnP 设备,则仍然需要一个空的 PnP ID 表,如下所示:
device_t 是设备结构的指针类型。在这里,我们只考虑从设备驱动程序编写者的角度来看感兴趣的方法。用于操作设备结构中的值的方法有:
device_t device_get_parent(dev) 获取设备的父总线。
driver_t device_get_driver(dev) 获取指向其驱动程序结构的指针。
char *device_get_name(dev) 获取驱动程序名称,例如我们的示例中的 "xxx" 。
int device_get_unit(dev) 获取单元号(从 0 开始对每个驱动程序关联的设备进行编号)。
char *device_get_nameunit(dev) 获取包括单元号在内的设备名称,如 "xxx0","xxx1" 等等。
char *device_get_desc(dev) 获取设备描述。通常,它以人类可读的形式描述设备的确切型号。
device_set_desc(dev, desc) 设置描述。这将使设备描述指向字符串 desc,之后可能不会被释放或更改。
device_set_desc_copy(dev, desc) 设置描述。描述被复制到内部动态分配的缓冲区中,因此字符串 desc 可以在之后更改而不会产生不良影响。
void *device_get_softc(dev) 获取与此设备关联的设备描述符指针(结构 xxx_softc )。
u_int32_t device_get_flags(dev) 获取配置文件中指定的设备标志。
便捷功能 device_printf(dev, fmt, …) 可用于打印设备驱动程序中的消息。它会自动将单元名称和冒号前置到消息之前。
device_t 方法在文件 kern/bus_subr.c 中实现。
ISA 设备在内核配置文件中描述如下:
port的值,IRQ 等等,都会转换为与设备关联的资源值。这些是可选的,取决于设备对自动配置的需求和能力。例如,一些设备根本不需要 DRQ,而一些允许驱动程序从设备配置中读取 IRQ 设置ports。如果一台机器有多个 ISA 总线,则可以在配置行中指定确切的总线,比如 isa0 或 isa1 ,否则设备将在所有 ISA 总线上搜索。
sensitive 是一个资源请求,要求在所有非敏感设备之前对此设备进行探测。它得到支持,但似乎在任何当前驱动程序中都没有使用。
对于许多情况下的传统 ISA 设备,驱动程序仍然能够检测到配置参数。但系统中要配置的每个设备都必须有一个配置行。如果系统中安装了两个相同类型的设备,但相应驱动程序只有一条配置行,即:
但对于通过即插即用或某种专有协议支持自动识别的设备,只需一行配置就足以配置系统中的所有设备,就像上面那个或只是简单地:
如果驱动程序同时支持自动识别和传统设备,并且同时在一台机器上安装了这两种设备,则只需要在配置文件中描述传统设备即可。自动识别设备将自动添加。
当 ISA 总线自动配置时,事件发生如下:
所有驱动程序的识别例程(包括识别所有 PnP 设备的 PnP 识别例程)都是随机调用的。当它们识别设备时,它们将设备添加到 ISA 总线的列表中。通常,驱动程序的识别例程会将其驱动程序与新设备关联起来。PnP 识别例程还不知道其他驱动程序,因此它不会将任何驱动程序与它添加的新设备关联起来。
使用 PnP 协议将 PnP 设备置于睡眠状态,以防止它们被探测为传统设备。
标记为 sensitive 的非 PnP 设备的探测例程被调用。如果设备探测成功,则为其调用附加例程。
所有非 PNP 设备的探测和附加例程同样被调用。
PnP 设备从睡眠状态恢复并分配它们请求的资源:I/O 和内存地址范围,IRQ 和 DRQ,所有这些资源都不会与已连接的传统设备发生冲突。
然后,对于每个 PnP 设备,调用所有当前 ISA 驱动程序的探测例程。第一个声称拥有该设备的驱动程序将被附加。可能会有多个驱动程序声称拥有具有不同优先级的设备;在这种情况下,优先级最高的驱动程序获胜。探测例程必须调用 ISA_PNP_PROBE() 来将实际 PnP ID 与驱动程序支持的 ID 列表进行比较,如果 ID 不在表中,则返回失败。这意味着绝对每个驱动程序,即使不支持任何 PnP 设备的驱动程序,也必须调用 ISA_PNP_PROBE() ,至少使用空的 PnP ID 表来在未知的 PnP 设备上返回失败。
探针例程在出错时返回正值(错误代码),成功时返回零或负值。
当 PnP 设备支持多个接口时,使用负的返回值。例如,支持不同驱动程序的旧兼容接口和新高级接口。那么两个驱动程序都会检测到该设备。在探针例程中返回更高值的驱动程序优先(换句话说,返回 0 的驱动程序具有最高优先级,返回 -1 的次之,返回 -2 的再次之,依此类推)。结果是仅支持旧接口的设备将由旧驱动程序处理(该驱动程序应从探针例程中返回 -1),而还支持新接口的设备将由新驱动程序处理(该驱动程序应从探针例程中返回 0)。如果多个驱动程序返回相同值,则先调用哪个获胜。因此,如果驱动程序返回值 0,则可以确定它获胜了优先级仲裁。
设备特定的识别例程还可以为设备分配驱动程序的类别。然后,为该设备探查该类别中的所有驱动程序,就像 PnP 一样。此功能尚未在任何现有驱动程序中实现,并且在本文档中不再考虑。
当探测传统设备时禁用即插即用设备,它们不会被附加两次(一次作为传统设备,一次作为即插即用设备)。但是,在设备相关的识别例程中,驱动程序有责任确保同一设备不会被驱动程序附加两次:一次作为传统用户配置,一次作为自动识别。
对于自动识别的设备(即即插即用和特定设备),另一个实际后果是无法从内核配置文件传递标志给它们。因此,它们要么根本不使用标志,要么对所有自动识别的设备使用设备单元 0 的标志,要么使用 sysctl 接口而不是标志。
其他不寻常的配置可能通过直接访问功能族 resource_query_ () 和 resource_ _value() 的配置资源来实现。它们的实现位于 kern/subr_bus.c 中。旧的 IDE 硬盘驱动程序 i386/isa/wd.c 包含了这种用法的示例。但是,始终应优先使用标准配置手段。将配置资源的解析留给总线配置代码。
用户输入到内核配置文件中的信息将被处理并作为配置资源传递给内核。这些信息将被总线配置代码解析,并转换为结构设备 device_t 的值以及与其关联的总线资源。驱动程序可以直接访问配置资源,对于更复杂的配置情况使用函数 resource_* 。然而,一般情况下这既不需要也不推荐,因此本问题在此不再讨论。
总线资源与每个设备相关联。它们由类型和类型内编号确定。对于 ISA 总线,定义了以下类型:
SYS_RES_IRQ - 中断号
SYS_RES_DRQ - ISA DMA 通道号
SYS_RES_MEMORY - 设备内存范围映射到系统内存空间
SYS_RES_IOPORT - 设备 I/O 寄存器范围
类型内的枚举从 0 开始,因此,如果设备有两个内存区域,则类型为 SYS_RES_MEMORY 的资源将被编号为 0 和 1。资源类型与 C 语言类型无关,所有资源值都具有 C 语言类型 unsigned long ,必要时必须进行转换。资源编号不必连续,尽管对于 ISA 而言,它们通常是连续的。ISA 设备允许的资源编号为:
所有资源都表示为范围,具有起始值和计数。对于 IRQ 和 DRQ 资源,计数通常等于 1。内存的值是指物理地址。
可以对资源执行三种类型的活动:
设置/获取
分配/释放
激活/停用
设置定义了资源使用的范围。分配会保留请求的范围,以防止其他驱动程序能够保留它(并检查没有其他驱动程序已经保留了这个范围)。激活会使资源对驱动程序可访问,通过执行必要的操作(例如,对于内存来说,会映射到内核虚拟地址空间)。
用于操作资源的函数有:
int bus_set_resource(device_t dev, int type, int rid, u_long start, u_long count) 为资源设置范围。如果成功返回 0,否则返回错误代码。通常,只有当 type 、 rid 、 start 或 count 中有一个值超出允许范围时,此函数才会返回错误。
dev - 驱动程序的设备
type - 资源类型,SYS_RES_*
rid - 资源类型内的资源编号(ID)
start, count - 资源范围
int bus_get_resource(device_t dev, int type, int rid, u_long *startp, u_long *countp) 获取资源范围。如果成功返回 0,如果资源尚未定义则返回错误代码。
u_long bus_get_resource_start(device_t dev, int type, int rid) u_long bus_get_resource_count (device_t dev, int type, int rid) 仅获取开始或计数的便利函数。在发生错误时返回 0,因此,如果资源的开始值在合法值中包含 0,则无法确定该值是 0 还是发生了错误。幸运的是,对于附加驱动程序的 ISA 资源,可能不会有一个开始值等于 0。
void bus_delete_resource(device_t dev, int type, int rid) 删除资源,使其未定义。
struct resource * bus_alloc_resource(device_t dev, int type, int *rid, u_long start, u_long end, u_long count, u_int flags) 将资源分配为在开始和结束之间的计数值范围,这些值未被其他任何人分配。遗憾的是,不支持对齐。如果资源尚未设置,则会自动创建。开始值为 0 和结束值为~0(全为 1)的特殊值意味着先前由 bus_set_resource() 设置的固定值必须改为使用:开始和计数本身,以及结束=(开始+计数),在这种情况下,如果资源在之前未定义,则会返回错误。尽管 rid 是通过引用传递的,但 ISA 总线的资源分配代码不会在任何地方设置它。(其他总线可能会使用不同的方法并对其进行修改)。
标志是一个位图,对调用者感兴趣的标志有:
RF_ACTIVE - 导致资源在分配后自动激活。
RF_SHAREABLE - 资源可以同时被多个驱动程序共享。
RF_TIMESHARE - 资源可以被多个驱动程序共享,即可以同时被多个驱动程序分配,但在任何给定时刻只能由一个驱动程序激活。
返回错误时返回 0。分配的值可以通过使用方法 rhand_*() 从返回的句柄中获取。
int bus_release_resource(device_t dev, int type, int rid, struct resource *r)
释放资源,r 是 bus_alloc_resource() 返回的句柄。成功返回 0,否则返回错误代码。
int bus_activate_resource(device_t dev, int type, int rid, struct resource *r) int bus_deactivate_resource(device_t dev, int type, int rid, struct resource *r)
激活或停用资源。成功时返回 0,否则返回错误码。如果资源是共享时间的,并且当前由另一个驱动程序激活,则返回 EBUSY 。
int bus_setup_intr(device_t dev, struct resource *r, int flags, driver_intr_t *handler, void *arg, void **cookiep) int bus_teardown_intr(device_t dev, struct resource *r, void *cookie)
将中断处理程序与设备关联或取消关联。成功时返回 0,否则返回错误码。
r - 已激活的资源处理程序,描述 IRQ 标志 - 中断优先级级别之一:
INTR_TYPE_TTY - 终端和其他类似字符型设备。要对它们进行掩码,请使用 spltty() 。
(INTR_TYPE_TTY | INTR_TYPE_FAST) - 具有小输入缓冲区的终端类型设备,在输入时对数据丢失至关重要(例如老式串行ports)。要对它们进行掩码,请使用 spltty() 。
INTR_TYPE_BIO - 块设备,除了那些在 CAM 控制器上的设备。要对它们进行掩码,请使用 splbio() 。
INTR_TYPE_CAM - CAM(Common Access Method)总线控制器。要屏蔽它们,请使用 splcam() 。
INTR_TYPE_NET - 网络接口控制器。要屏蔽它们,请使用 splimp() 。
INTR_TYPE_MISC - 杂项设备。除了通过屏蔽所有中断的 splhigh() 外,没有其他屏蔽它们的方法。
当中断处理程序执行时,所有其他与其优先级相匹配的中断将被屏蔽。唯一的例外是 MISC 级别,其他中断都不会被屏蔽,也不会被任何其他中断屏蔽。
处理程序 - 指向处理程序函数的指针,类型 driver_intr_t 被定义为 void driver_intr_t(void *)
arg - 传递给处理程序以识别特定设备的参数。在处理程序中,它从 void*强制转换为任何真实类型。对 ISA 中断处理程序的旧惯例是使用单元号作为参数,新(推荐)惯例是使用指向设备 softc 结构的指针。
cookie[p] - 从 setup() 接收到的值用于标识在传递给 teardown() 时要使用的处理程序
定义了许多方法来操作资源处理程序(struct resource *)。 设备驱动程序编写人员感兴趣的是:
u_long rman_get_start(r) u_long rman_get_end(r) 获取分配的资源范围的起始和结束。
获取激活内存资源的虚拟地址。
在许多情况下,数据通过内存在驱动程序和设备之间交换。有两种可能的变体:
(a) 存储器位于设备卡上
(b) 存储器是计算机的主存储器
在情况(a)中,驱动程序始终根据需要在设备卡上的存储器和主存储器之间来回复制数据。要将设备卡上的存储器映射到内核虚拟地址空间,必须定义设备卡上的物理地址和长度作为 SYS_RES_MEMORY 资源。然后可以分配并激活该资源,并使用 rman_get_virtual() 获得其虚拟地址。较旧的驱动程序曾经使用函数 pmap_mapdev() 来实现此目的,不应再直接使用。现在这是资源激活的内部步骤之一。
大多数 ISA 卡的内存配置为物理位置范围在 640KB-1MB 之间。一些 ISA 卡需要更大的内存范围,应放置在 16MB 以下(因为 ISA 总线上存在 24 位地址限制)。在这种情况下,如果计算机的内存多于设备内存的起始地址(换句话说,它们重叠),必须在设备使用的地址范围配置内存空洞。许多 BIOS 允许在 14MB 或 15MB 处启动 1MB 的内存空洞。如果 BIOS 正确报告它们,FreeBSD 可以正确处理内存空洞(此功能可能在旧 BIOS 上失效)。
在情况(b)中,只发送数据的地址给设备,设备使用 DMA 实际访问主内存中的数据。存在两个限制:首先,ISA 卡只能访问 16MB 以下的内存。其次,虚拟地址空间中的连续页面在物理地址空间中可能不连续,因此设备可能需要执行分散/聚集操作。总线子系统为其中一些问题提供了现成的解决方案,其余问题必须由驱动程序自行解决。
用于 DMA 内存分配的两种结构, bus_dma_tag_t 和 bus_dmamap_t 。标签描述了 DMA 内存所需的属性。映射表示根据这些属性分配的内存块。可以将多个映射与同一标签关联。
标签以树状层次结构组织,属性可以继承。子标签继承其父标签的所有要求,并且可以使它们更严格,但绝不更松散。
通常为每个设备单元创建一个顶层标签(没有父级)。如果每个设备需要具有不同要求的多个内存区域,则可以为每个内存区域创建一个父标签的子标签。
标签可以通过两种方式创建地图。
首先,可以分配符合标记要求的一块连续内存(稍后可以释放)。这通常用于为与设备通信的相对长寿命的内存区域分配空间。将这样的内存加载到映射中非常简单:它始终被视为适当物理内存范围内的一个块。
其次,可以将任意虚拟内存区域加载到映射中。将检查该内存的每个页面是否符合映射要求。如果符合,则保留在原始位置。如果不符合,则分配一个新的符合要求的“反弹页面”并用作中间存储。在写入来自非符合原始页面的数据时,它们将首先被复制到其反弹页面,然后从反弹页面传输到设备。在读取数据时,数据将从设备传输到反弹页面,然后复制到其非符合原始页面。在原始页面和反弹页面之间复制的过程称为同步。通常,这是基于每次传输的基础进行的:每次传输都会加载缓冲区,完成传输后卸载缓冲区。
在 DMA 内存上运行的函数有:
int bus_dma_tag_create(bus_dma_tag_t parent, bus_size_t alignment, bus_size_t boundary, bus_addr_t lowaddr, bus_addr_t highaddr, bus_dma_filter_t *filter, void *filterarg, bus_size_t maxsize, int nsegments, bus_size_t maxsegsz, int flags, bus_dma_tag_t *dmat) 创建一个新标签。成功返回 0,否则返回错误代码。
parent - 父标签,或者为 NULL 以创建顶级标签。
alignment - 为分配给此标签的内存区域所需的物理对齐。对于未来的 bus_dmamem_alloc() 而不是 bus_dmamap_create() 调用,请使用值 1 表示“无特定对齐”。
边界 - 分配内存时不能越过的物理地址边界。对于“无边界”,请使用值 0。仅适用于未来的 bus_dmamem_alloc() 而不是 bus_dmamap_create() 调用。必须是 2 的幂。如果计划在非级联 DMA 模式下使用内存(即,DMA 地址将不是由设备本身提供,而是由 ISA DMA 控制器提供),则边界不能大于 64KB(64*1024),因为 DMA 硬件的限制。
lowaddr,highaddr - 这些名称有点误导;这些值用于限制用于分配内存的物理地址范围。具体含义取决于计划的未来用途:
对于 bus_dmamem_alloc() ,所有从 0 到 lowaddr-1 的地址被视为允许的,更高的地址被禁止。
对于 bus_dmamap_create() ,在包含范围 [lowaddr; highaddr] 之外的所有地址被视为可访问。范围内的页面地址传递给过滤器函数,该函数决定它们是否可访问。如果未提供过滤器函数,则认为整个范围都是不可访问的。
对于 ISA 设备,没有过滤器函数的正常值为:lowaddr = BUS_SPACE_MAXADDR_24BIT highaddr = BUS_SPACE_MAXADDR
filter,filterarg - 过滤函数及其参数。如果对于 filter 传递了 NULL,则在进行 bus_dmamap_create() 时将认为整个范围[lowaddr,highaddr]是不可访问的。否则,将范围[lowaddr; highaddr]中每个尝试页面的物理地址传递给过滤函数,该函数决定它是否可以访问。过滤函数的原型是: int filterfunc(void *arg, bus_addr_t paddr) 。如果页面可访问则必须返回 0,否则返回非零值。
maxsize - 通过此标签可以分配的内存的最大大小(以字节为单位)。如果难以估计或可能是任意大的情况,ISA 设备的值将为 BUS_SPACE_MAXSIZE_24BIT 。
nsegments - 设备支持的最大分散-聚集段数。如果不受限制,则应使用值 BUS_SPACE_UNRESTRICTED 。此值建议用于父标签,然后可以为子标签指定实际限制。 nsegments 等于 BUS_SPACE_UNRESTRICTED 的标签不得用于实际加载映射,只能用作父标签。 nsegments 的实际限制似乎约为 250-300,更高的值将导致内核堆栈溢出(硬件通常无法支持那么多分散-聚集缓冲区)。
maxsegsz - 设备支持的分散-聚集段的最大大小。ISA 设备的最大值为 BUS_SPACE_MAXSIZE_24BIT 。
flags - 一组标志位。唯一有趣的标志位是:
BUS_DMA_ALLOCNOW - 在创建标记时请求分配所有潜在需要的反弹页面。
dmat - 指向要返回的新标签存储。
int bus_dma_tag_destroy(bus_dma_tag_t dmat) 销毁一个标签。成功返回 0,否则返回错误代码。 dmat - 要销毁的标签。
int bus_dmamem_alloc(bus_dma_tag_t dmat, void** vaddr, int flags, bus_dmamap_t *mapp) 分配由标签描述的连续内存区域。要分配的内存大小为标签的最大尺寸。成功时返回 0,否则返回错误代码。在使用物理地址获取内存之前,仍需由 bus_dmamap_load() 加载结果。
dmat - 标签
vaddr - 指向用于返回分配区域的内核虚拟地址的存储指针。
标志 - 一组标志位图。唯一有趣的标志是:
BUS_DMA_NOWAIT - 如果内存不可立即使用,则返回错误。如果未设置此标志,则允许例程休眠,直到内存可用为止。
mapp - 指向将返回的新映射的存储指针。
void bus_dmamem_free(bus_dma_tag_t dmat, void *vaddr, bus_dmamap_t map) 释放由 bus_dmamem_alloc() 分配的内存。目前,尚未实现对具有 ISA 限制的内存进行释放。因此,建议的使用模式是尽可能保留并重复使用已分配的区域。不要轻易释放某些区域,然后不久再次分配它。这并不意味着完全不应使用 bus_dmamem_free() :希望它很快能够得到正确实现。
dmat - 标签
vaddr - 内存的内核虚拟地址
地图 - 内存地图(从 bus_dmamem_alloc() 返回)
int bus_dmamap_create(bus_dma_tag_t dmat, int flags, bus_dmamap_t *mapp) 为标签创建一个映射,稍后在 bus_dmamap_load() 中使用。成功时返回 0,否则返回错误代码。
dmat - 标签
标志 - 理论上,表示标志的位图。但是目前没有定义任何标志,因此目前它将始终为 0。
mapp - 指向要返回的新地图的存储指针
int bus_dmamap_destroy(bus_dma_tag_t dmat, bus_dmamap_t map) 销毁地图。成功时返回 0,否则返回错误代码。
dmat - 地图关联的标签
map - 要销毁的地图
int bus_dmamap_load(bus_dma_tag_t dmat, bus_dmamap_t map, void *buf, bus_size_t buflen, bus_dmamap_callback_t *callback, void *callback_arg, int flags) 将缓冲区加载到地图中(地图必须由 bus_dmamap_create() 或 bus_dmamem_alloc() 预先创建)。缓冲区的所有页面都会检查是否符合标签要求,对于不符合要求的页面,将分配跳跃页面。一个物理段描述符数组将被构建并传递给回调例程。然后期望该回调例程以某种方式处理它。系统中的跳跃缓冲区数量有限,因此如果需要但未立即可用时,请求将排队,当跳跃缓冲区可用时将调用回调。立即执行回调返回 0,若请求排队等待执行则返回 EINPROGRESS 。在后者情况下,与排队回调例程的同步是驱动程序的责任。
dmat - 标签
map - 地图
buf - 缓冲区的内核虚拟地址
缓冲区长度
回调, callback_arg - 回调函数及其参数 回调函数的原型是: void callback(void *arg, bus_dma_segment_t *seg, int nseg, int error)
参数 - 与传递给 bus_dmamap_load() 的回调参数相同
seg - 段描述符数组
nseg - 数组中的描述符数量
error - 表示段号溢出:如果设置为 EFBIG ,则缓冲区不适合标签允许的最大段数。在这种情况下,数组中将只包含允许的描述符数量。处理此情况取决于驱动程序:根据所需的语义,它可以将其视为错误或拆分缓冲区并单独处理第二部分。段数组中的每个条目包含以下字段:
ds_addr - 段的物理总线地址
ds_len - 段的长度
void bus_dmamap_unload(bus_dma_tag_t dmat, bus_dmamap_t map) 卸载地图。
dmat - 标签
地图 - 已加载地图
void bus_dmamap_sync (bus_dma_tag_t dmat, bus_dmamap_t map, bus_dmasync_op_t op) 将加载的缓冲区与其弹跳页面在物理传输到或从设备之前和之后进行同步。这是执行原始缓冲区和其映射版本之间所有必要数据复制的功能。必须在执行传输之前和之后同步缓冲区。
dmat - 标签
map - 载入的地图
op - 要执行的同步操作类型:
BUS_DMASYNC_PREREAD - 从设备读取到缓冲区之前
BUS_DMASYNC_POSTREAD - 从设备读取到缓冲区之后
BUS_DMASYNC_PREWRITE - 将缓冲区写入设备之前
BUS_DMASYNC_POSTWRITE - 写入缓冲区到设备后
目前 PREREAD 和 POSTWRITE 是空操作,但将来可能会发生变化,因此驱动程序不得忽略它们。不需要对从 bus_dmamem_alloc() 获取的内存进行同步。
在从 bus_dmamap_load() 调用回调函数之前,段数组存储在堆栈中。并且为标签允许的最大段数进行了预分配。由此导致在 i386 架构上段数的实际限制约为 250-300(内核堆栈为 4KB 减去用户结构的大小,段数组条目的大小为 8 字节,必须留有一些空间)。由于数组是基于最大数目分配的,因此此值不得设置得比实际需要的更高。幸运的是,对于大多数硬件,支持的最大段数要低得多。但是,如果驱动程序希望处理具有非常大数量的分散-聚集段的缓冲区,则应该分批处理:加载缓冲区的一部分,将其传输到设备,加载缓冲区的下一部分,依此类推。
另一个实际后果是,段的数量可能限制缓冲区的大小。如果缓冲区中的所有页面在物理上不连续,则该碎片化情况下支持的最大缓冲区大小将为(nsegments * page_size)。例如,如果支持最大数量的 10 个段,则在 i386 上最大保证支持的缓冲区大小将为 40K。如果需要更大的大小,则驱动程序应使用特殊技巧。
如果硬件根本不支持散射-聚集,或者驱动程序希望支持即使在非常碎片化的情况下也支持某些缓冲区大小,则解决方案是在驱动程序中分配一个连续的缓冲区,并在原始缓冲区不适合时将其用作中间存储。
在使用映射时,典型的调用序列取决于映射的用途。字符→用于显示时间流。
在设备连接和断开期间几乎保持不变的缓冲区:
bus_dmamem_alloc → bus_dmamap_load → …使用缓冲区… → → bus_dmamap_unload → bus_dmamem_free
对于频繁更改并从驱动程序外部传递的缓冲区:
加载由 bus_dmamem_alloc() 创建的地图时,传递的地址和缓冲区大小必须与 bus_dmamem_alloc() 中使用的相同。在这种情况下,保证整个缓冲区将被映射为一个段(因此回调可以基于此假设),并且请求将立即执行(永远不会返回 EINPROGRESS)。在这种情况下,回调所需做的就是保存物理地址。
典型示例如下:
看起来有点长且复杂,但这就是操作方法。实际结果是:如果多个内存区域总是一起分配,则将它们全部组合成一个结构并作为一个单元分配将是一个非常好的主意(如果对齐和边界限制允许的话)。
当将任意缓冲区加载到 bus_dmamap_create() 创建的地图中时,必须采取特殊措施来同步回调,以防延迟。 代码看起来像:
请求处理的两种可能方法是:
如果通过显式标记完成请求(例如 CAM 请求)来完成请求,则将所有进一步处理放入回调驱动程序将更简单,该驱动程序在请求完成时标记请求。 然后,就不需要太多额外的同步。出于流量控制的原因,将请求队列冻结直到该请求完成可能是一个好主意。
如果请求在函数返回时完成(例如在字符设备上的经典读取或写入请求),则应在缓冲区描述符中设置同步标志并调用 tsleep() 。稍后当回调被调用时,它将进行处理并检查此同步标志。如果已设置,则回调应发出唤醒。在这种方法中,回调函数可以执行所有所需的处理(就像前一种情况一样),也可以简单地将段数组保存在缓冲区描述符中。然后在回调完成后,调用函数可以使用此保存的段数组并执行所有处理。
直接内存访问(DMA)通过 DMA 控制器在 ISA 总线上实现(实际上,有两个控制器,但这是一个无关紧要的细节)。为使早期 ISA 设备简单且廉价,总线控制和地址生成的逻辑集中在 DMA 控制器中。幸运的是,FreeBSD 提供了一组函数,大部分隐藏了 DMA 控制器的烦人细节,使设备驱动程序更易于编写。
最简单的情况是对于相当智能的设备。就像 PCI 上的总线主设备一样,它们可以自己生成总线周期和内存地址。它们真正需要的唯一一件事情是来自 DMA 控制器的总线仲裁。因此,为此目的,它们假装是级联的从 DMA 控制器。系统 DMA 控制器唯一需要的是在附加驱动程序时通过调用以下函数来在 DMA 通道上启用级联模式:
void isa_dmacascade(int channel_number)
所有进一步的活动都是通过对设备进行编程来完成的。在卸载驱动程序时,不需要调用任何与 DMA 相关的函数。
对于更简单的设备,情况变得更加复杂。使用的函数包括:
int isa_dma_acquire(int chanel_number) 保留 DMA 通道。成功时返回 0,如果通道已被此驱动程序或其他驱动程序预留,则返回 EBUSY。大多数 ISA 设备无法共享 DMA 通道,因此通常在连接设备时调用此函数。尽管现代总线资源接口使此保留变得多余,但仍必须与后者一起使用。如果不使用,稍后其他 DMA 例程将会崩溃。
int isa_dma_release(int chanel_number) 释放先前保留的 DMA 通道。在释放通道时不得有传输正在进行(此外,设备在通道释放后不得尝试启动传输)。
void isa_dmainit(int chan, u_int bouncebufsize) 为指定通道分配一个弹跳缓冲区。缓冲区的请求大小不能超过 64KB。如果传输缓冲区不是物理上连续的,或者超出 ISA 总线可访问的内存范围,或者跨越 64KB 边界,则稍后将自动使用此弹跳缓冲区。如果传输始终来自符合这些条件的缓冲区(例如由 bus_dmamem_alloc() 分配的具有适当限制的缓冲区),则不必调用 isa_dmainit() 。但是使用 DMA 控制器传输任意数据非常方便。弹跳缓冲区将自动处理分散-聚集问题。
通道号
弹跳缓冲区大小 - 弹跳缓冲区的大小,以字节为单位
void isa_dmastart(int flags, caddr_t addr, u_int nbytes, int chan) 准备开始 DMA 传输。在实际启动设备上的传输之前,必须调用此函数设置 DMA 控制器。它检查缓冲区是否连续并是否在 ISA 内存范围内,如果不是,则自动使用弹跳缓冲区。如果需要弹跳缓冲区但未通过 isa_dmainit() 设置或请求传输大小过大,则系统将发生紧急情况。在使用弹跳缓冲区的写入请求时,数据将自动复制到弹跳缓冲区中。
标志 - 一个位掩码,确定要执行的操作类型。方向位 B_READ 和 B_WRITE 是互斥的。
B_READ - 从 ISA 总线读取到内存中
B_WRITE - 从内存写入到 ISA 总线
如果设置为 B_RAW,则 DMA 控制器将记住缓冲区,并在传输结束后自动重新初始化以再次重复传输相同的缓冲区(当然,在设备中启动另一个传输之前,驱动程序可能会更改缓冲区中的数据)。 如果未设置,则参数仅适用于一个传输,并且在启动下一个传输之前必须再次调用 isa_dmastart() 。仅当不使用跳变缓冲区时,使用 B_RAW 才有意义。
地址 - 缓冲区的虚拟地址
nbytes - 缓冲区的长度。必须小于或等于 64KB。不允许长度为 0:DMA 控制器将其理解为 64KB,而内核代码将其理解为 0,这将导致不可预测的效果。对于通道号在 4 及以上的通道,长度必须是偶数,因为这些通道每次传输 2 字节。在长度为奇数时,最后一个字节将不会被传输。
通道号
void isa_dmadone(int flags, caddr_t addr, int nbytes, int chan) 在设备报告传输完成后同步内存。如果这是一个带有弹簧缓冲区的读操作,则数据将从弹簧缓冲区复制到原始缓冲区。参数与 isa_dmastart() 相同。标记 B_RAW 被允许但不会以任何方式影响 isa_dmadone() 。
int isa_dmastatus(int channel_number) 返回当前传输中剩余的字节数。如果在 isa_dmastart() 中设置了标志 B_READ,则返回的数字永远不会等于零。在传输结束时,它将自动重置为缓冲区的长度。正常用法是在设备信号传输完成后检查剩余字节数。如果字节数不为 0,则该传输可能出现问题。
int isa_dmastop(int channel_number) 中止当前传输并返回未传输的字节数。
此函数探测设备是否存在。如果驱动程序支持某些设备配置的自动检测(例如中断向量或内存地址),则此自动检测必须在此例程中完成。
对于任何其他总线,如果设备无法被检测到或者被检测到但是自检未通过或发生其他问题,则返回一个正值的错误。如果设备不存在,则必须返回值 ENXIO 。其他错误值可能表示其他条件。零或负值表示成功。大多数驱动程序返回零表示成功。
负返回值用于 PnP 设备支持多个接口的情况。例如,一个旧的兼容接口和一个支持不同驱动程序的新的高级接口。然后两个驱动程序都会检测到设备。在探测例程中返回更高值的驱动程序优先(换句话说,返回 0 的驱动程序拥有最高优先级,返回-1 的次之,返回-2 的在其后,依此类推)。结果,只支持旧接口的设备将由旧驱动程序处理(该驱动程序应在探测例程中返回-1),而同时支持新接口的设备将由新驱动程序处理(该驱动程序应在探测例程中返回 0)。
系统在调用探测程序之前通过分配结构 xxx_softc 为设备分配描述符。如果探测程序返回错误,系统将自动释放描述符。因此,如果发生探测错误,驱动程序必须确保在探测过程中使用的所有资源被释放,没有任何东西阻止描述符安全释放。如果探测成功完成,系统将保留描述符并稍后传递给例程 xxx_isa_attach() 。如果驱动程序返回负值,则不能确定它将具有最高优先级并且将调用其附加程序。因此,在这种情况下,在返回之前还必须释放所有资源,如果需要,在附加程序中重新分配资源。当 xxx_isa_probe() 返回 0 时,在返回之前释放资源也是一个好主意,行为良好的驱动程序应该这样做。但是,在释放资源方面存在问题的情况下,允许驱动程序在从探测程序返回 0 到执行附加程序之间保留资源。
典型的探测程序以获取设备描述符和单元开始:
然后检查 PnP 设备。检查通过包含此驱动程序支持的 PnP ID 列表及这些 ID 对应的设备型号的人类可读描述的表格进行。
ISA_PNP_PROBE 的逻辑如下:如果此卡(设备单元)未被检测为 PnP,则将返回 ENOENT。如果它被检测为 PnP,但其检测到的 ID 与表中的任何 ID 都不匹配,则返回 ENXIO。最后,如果它具有 PnP 支持并且与表中的 ID 之一匹配,则返回 0,并由 device_set_desc() 设置表中的适当描述。
如果驱动程序仅支持 PnP 设备,则条件将如下所示:
不需要对不支持 PnP 的驱动程序进行特殊处理,因为它们传递空的 PnP ID 表,如果在 PnP 卡上调用,则始终会收到 ENXIO。
探测例行程序通常至少需要一些最小的资源集,比如输入/输出port号,以便找到卡并对其进行探测。根据硬件,驱动程序可能能够自动发现其他必要的资源。即插即用设备已经由即插即用子系统预设了所有资源,因此驱动程序不需要自己去发现它们。
通常,要访问设备所需的最少信息是输入/输出port号。然后,一些设备允许从设备配置寄存器中获取其余信息(虽然并非所有设备都这样做)。因此,首先我们尝试获取port起始值:
基本的port地址保存在结构 softc 中以供将来使用。如果它会经常被使用,那么每次调用资源函数将会非常慢。如果我们没有得到port,我们只返回一个错误。一些设备驱动程序可以更聪明,尝试探测所有可能的ports,就像这样:
当然,通常应该使用驱动程序的 identify() 例程来执行这样的操作。但有一个有效的理由可能更好的在 probe() 中完成:如果此探针可能会让其他敏感设备发疯。探针例程考虑到 sensitive 标志的顺序:首先对敏感设备进行探测,然后是其余设备。但 identify() 例程是在任何探测之前调用的,因此它们对敏感设备没有尊重,可能会使它们受到影响。
现在,当我们获得起始port后,我们需要设置port计数(除了 PnP 设备),因为内核在配置文件中没有此信息。
最后分配并激活一块port地址空间(开始值和结束值的特殊值表示“使用我们通过 bus_set_resource() 设置的值”):
现在访问port-映射寄存器后,我们可以以某种方式探测设备并检查其反应是否如预期。如果没有反应,那么可能在这个地址上有其他设备或根本没有设备。
通常驱动程序不会在连接例程之前设置中断处理程序。相反,他们使用 DELAY() 函数在轮询模式下进行探测。探测例程绝不能永远挂起,对设备的所有等待都必须设置超时。如果设备在规定时间内没有响应,可能是损坏或配置错误,驱动程序必须返回错误。在确定超时间隔时,给设备一些额外的时间以确保安全:虽然 DELAY() 应该在任何机器上延迟相同的时间,但它有一些误差范围,具体取决于 CPU。
如果探测例程真的想检查中断是否真的有效,它也可以配置和探测中断。但这不推荐。
函数 xxx_probe_ports() 还可以根据发现的设备的确切型号设置设备描述。但是,如果只有一个受支持的设备型号,这也可以以硬编码的方式完成。当然,对于即插即用设备,即插即用支持会自动从表中设置描述。
然后,探测例程应通过读取设备配置寄存器来发现所有资源的范围,或者确保资源范围是由用户明确设置的。我们将以板载内存的示例来考虑它。探测例程应尽可能非侵入性,因此资源的分配和功能检查(除ports之外)最好留给附加例程。
内存地址可以在内核配置文件中指定,或者在某些设备上可能预先配置在非易失性配置寄存器中。如果两个来源都可用且不同,应该使用哪一个?可能如果用户在内核配置文件中明确设置了地址,他们知道自己在做什么,这个应该优先。实现示例可能如下:
通过类比很容易检查 IRQ 和 DRQ 的资源。
如果一切顺利,释放所有资源并返回成功。
最后,在处理棘手的情况。在返回之前,应该释放所有资源。我们利用 softc 结构传递给我们之前被清零的事实,因此我们可以找出是否有一些资源被分配:然后其描述符则为非零。
这将是探测例程的全部内容。释放资源是从多个地方完成的,因此移至一个函数,可能看起来像:
如果探测例程返回成功并且系统选择连接该驱动程序,则连接例程实际上将驱动程序连接到系统。如果探测例程返回 0,则连接例程可能期望接收设备结构 softc 完整,就像探测例程设置的那样。此外,如果探测例程返回 0,则可以期望将来某个时候为此设备调用连接例程。如果探测例程返回负值,则驱动程序可能不做出这些假设中的任何一个。
attach 例程返回 0 表示成功完成,否则返回错误代码。
attach 例程的开始方式与 probe 例程相同,将一些经常使用的数据放入更易访问的变量中。
然后分配和激活所有必要的资源。由于在返回 probe 之前通常会释放port范围,所以必须再次分配它。我们期望 probe 例程已正确设置了所有资源范围,并将它们保存在结构 softc 中。如果 probe 例程留下了一些资源已分配的情况,则不需要再次分配(这将被视为错误)。
DMA 请求通道(DRQ)也是类似分配的。要初始化它,请使用 isa_dma*() 系列的函数。例如:
isa_dmacascade(sc→drq0);
中断请求线(IRQ)有点特殊。除了分配外,驱动程序的中断处理程序应与之关联。在旧的 ISA 驱动程序中,系统传递给中断处理程序的参数是设备单元号。但在现代驱动程序中,惯例建议传递指向结构体 softc 的指针。重要的原因是,当动态分配结构体 softc 时,从 softc 获取单元号很容易,而从单元号获取 softc 很困难。此外,这种惯例使得不同总线的驱动程序看起来更加统一,并允许它们共享代码:每个总线都有自己的探测、附加、分离和其他特定于总线的例程,而驱动程序的大部分代码可以在它们之间共享。
如果设备需要对主内存进行 DMA,则应像之前描述的那样分配该内存:
分配所有必要资源后,应初始化设备。初始化可能包括测试所有预期功能是否可用。
总线子系统将自动在控制台上打印由探测设置的设备描述。但是,如果驱动程序想要打印有关设备的一些额外信息,例如:
如果初始化过程出现任何问题,则建议在返回错误之前打印有关这些问题的消息。
附加程序的最后一步是将设备附加到内核中的功能子系统。这样做的确切方式取决于驱动程序的类型:字符设备、块设备、网络设备、CAM SCSI 总线设备等。
如果一切顺利,就返回成功。
最后,处理棘手的情况。在返回错误之前,应释放所有资源。我们利用这样一个事实:在结构 softc 传递给我们之前,它被清零,因此我们可以查明是否分配了某些资源:在这种情况下,其描述符是非零的。
这将是附加例程的全部内容。
如果此功能存在于驱动程序中,并且驱动程序被编译为可加载模块,则驱动程序具有卸载的能力。如果硬件支持热插拔,则这是一个重要的功能。但是 ISA 总线不支持热插拔,因此对 ISA 设备来说,这个功能并不特别重要。在调试驱动程序时,卸载驱动程序的能力可能是有用的,但在许多情况下,只有在旧版本驱动程序在某种程度上使系统崩溃并且需要重新启动时,才需要安装新版本的驱动程序,因此编写卸载例程所花费的精力可能不值得。另一个论点是卸载将允许在生产机器上升级驱动程序,这似乎大多是理论性的。在生产机器上安装新版本的驱动程序是一项危险的操作,绝不能在生产机器上执行(并且在系统运行在安全模式时是不允许的)。尽管如此,出于完整性考虑,可能会提供卸载例程。
分离例程在成功分离驱动程序时返回 0,否则返回错误代码。
分离的逻辑与附加的逻辑相反。首先要做的是将驱动程序从其内核子系统中分离。如果设备当前正在打开,则驱动程序有两种选择:拒绝分离或强制关闭并继续分离。所选择的方式取决于特定内核子系统执行强制关闭的能力以及驱动程序作者的偏好。通常,强制关闭似乎是首选的替代方案。
接下来,驱动程序可能希望将硬件重置为一致的状态。这包括停止任何正在进行的传输,禁用 DMA 通道和中断,以避免设备引起内存损坏。对于大多数驱动程序来说,这正是关闭例程所做的事情,因此如果它包含在驱动程序中,我们只需调用它。
xxx_isa_shutdown(dev);
最后释放所有资源并返回成功。
当系统即将关闭时,将调用此例程。预计将硬件带到某种一致状态。对于大多数 ISA 设备,不需要特殊操作,因此该函数实际上并不是必需的,因为设备将在重新启动时重新初始化。但是,一些设备必须通过特殊过程关闭,以确保它们在软重启后能够被正确检测到(这对于许多具有专有识别协议的设备尤为重要)。无论如何,在设备寄存器中禁用 DMA 和中断,并停止任何正在进行的传输都是一个好主意。确切的操作取决于硬件,因此我们在这里不会详细考虑。
当接收到中断时,将调用中断处理程序,该中断可能来自该特定设备。ISA 总线不支持中断共享(除非在某些特殊情况下),因此实际上,如果调用中断处理程序,则几乎可以确定中断来自其设备。但是,中断处理程序必须轮询设备寄存器,并确保中断是由其设备生成的。如果不是,则应该直接返回。
ISA 驱动程序的旧约定是将设备单元号作为参数。这已经过时,新驱动程序在调用 bus_setup_intr() 时接收指定给它们的任何参数。根据新约定,它应该是指向结构体 softc 的指针。因此,中断处理程序通常以以下方式开始:
它以 bus_setup_intr() 指定的中断类型参数中断优先级别运行。这意味着所有同一类型的其他中断以及所有软件中断都被禁用。
为了避免竞争,通常写成循环:
中断处理程序只需向设备确认中断,而不需要向中断控制器确认,系统会处理后者。
本文假定读者对 FreeBSD 中的设备驱动程序和 SCSI 协议有一般的了解。本文中的许多信息都是从驱动程序中提取的:
ncr (/sys/pci/ncr.c) 由 Wolfgang Stanglmeier 和 Stefan Esser 编写
sym (/sys/dev/sym/sym_hipd.c) 由 Gerard Roudier 编写
aic7xxx (/sys/dev/aic7xxx/aic7xxx.c) 由 Justin T. Gibbs 编写
以及来自 CAM 代码本身(由 Justin T. Gibbs 编写,参见/sys/cam/*)。当某些解决方案看起来最合乎逻辑,并且基本上是 Justin T. Gibbs 从代码中直接提取出来时,我将其标记为“推荐”。
该文档以伪代码示例进行说明。虽然有时示例具有许多细节并且看起来像真实代码,但它仍然是伪代码。它是为了以一种易于理解的方式演示概念而编写的。对于真实的驱动程序,其他方法可能更模块化和高效。它还抽象出硬件细节,以及可能会混淆演示概念或应该在开发人员手册的其他章节中描述的问题。这些细节通常显示为调用具有描述性名称的函数、注释或伪语句。幸运的是,真实生活中带有所有细节的全尺寸示例可以在真实的驱动程序中找到。
CAM 代表通用访问方法。这是一种以类似 SCSI 的方式寻址 I/O 总线的通用方式。这允许将通用设备驱动程序与控制 I/O 总线的驱动程序分开:例如,磁盘驱动程序能够控制 SCSI、IDE 和/或任何其他总线上的磁盘,因此磁盘驱动程序部分不必为每个新的 I/O 总线重写(或复制和修改)。因此,最重要的两个活动实体是:
外围模块 - 外围设备(磁盘、磁带、CD-ROM 等)的驱动程序。
SCSI 接口模块(SIM)- 用于连接到 I/O 总线(如 SCSI 或 IDE)的主机总线适配器驱动程序。
外围驱动程序接收来自操作系统的请求,将其转换为一系列 SCSI 命令,并将这些 SCSI 命令传递给 SCSI 接口模块。SCSI 接口模块负责将这些命令传递给实际的硬件(或者如果实际的硬件不是 SCSI 而是 IDE,还需要将 SCSI 命令转换为硬件的本机命令)。
由于我们对编写 SCSI 适配器驱动程序感兴趣,在这一点上,我们将从 SIM 的角度考虑一切。
典型的 SIM 驱动程序需要包含以下与 CAM 相关的头文件:
每个 SIM 驱动程序必须做的第一件事是向 CAM 子系统注册自己。这是在驱动程序的 xxx_attach() 函数中完成的(这里和后续使用 xxx_ 来表示唯一的驱动程序名称前缀)。 xxx_attach() 函数本身是由系统总线自动配置代码调用的,我们在这里不进行描述。
这是通过多个步骤实现的: 首先需要为与此 SIM 相关的请求队列分配空间:
这里 SIZE 是要分配的队列大小,它可以包含的最大请求数。这是 SIM 驱动程序在一个 SCSI 卡上可以并行处理的请求数。通常可以计算为:
接下来,我们创建 SIM 的描述符:
请注意,如果我们无法创建 SIM 描述符,我们也会释放 devq ,因为我们无法做其他任何事情,我们希望保留内存。
如果 SCSI 卡上有多个 SCSI 总线,则每个总线都需要自己的 cam_sim 结构。
有趣的问题是,如果 SCSI 卡有多个 SCSI 总线,我们是需要每张卡一个 devq 结构还是每个 SCSI 总线一个?CAM 代码中的评论给出的答案是:无论哪种方式,都取决于驱动程序的作者偏好。
参数为:
action_func - 指向驱动程序的 xxx_action 函数。
poll_func - 指向驱动程序的 xxx_poll()
driver_name - 实际驱动程序的名称,如"ncr"或"wds"。
softc - 指向此 SCSI 卡的驱动程序内部描述符的指针。驱动程序将来会使用此指针获取私有数据。
unit - 控制器单元号,例如对于控制器“mps0”,此编号为 0。
mtx - 与此 SIM 关联的锁。对于不了解锁定的 SIM,请传递 Giant。对于了解的 SIM,请传递用于保护此 SIM 数据结构的锁。当调用 xxx_action 和 xxx_poll 时,将持有此锁。
max_dev_transactions - 非标记模式下每个 SCSI 目标的最大同时事务数。这个值几乎普遍等于 1,只有非 SCSI 卡可能有例外。希望在执行一个事务时准备另一个事务的驱动程序可能将其设置为 2,但这似乎不值得复杂化。
max_tagged_dev_transactions - 相同的概念,但在标记模式下。标记是 SCSI 启动设备上多个事务的方式:每个事务被分配一个唯一的标记,然后将事务发送到设备。当设备完成某个事务时,它会将结果与标记一起发送回来,以便 SCSI 适配器(和驱动程序)可以知道哪个事务已完成。这个参数也被称为最大标记深度。它取决于 SCSI 适配器的能力。
最后,我们注册与我们的 SCSI 适配器关联的 SCSI 总线:
如果每个 SCSI 总线上只有一个 devq 结构(即,我们将具有多个总线的卡视为具有单个总线的多个卡),那么总线编号将始终为 0,否则 SCSI 卡上的每个总线都应该获得一个不同的编号。每个总线都需要有自己单独的结构 cam_sim。
之后,我们的控制器完全连接到 CAM 系统。现在可以丢弃 devq 的值:sim 将在以后所有的 CAM 调用中作为参数传递,并且 devq 可以从中派生出来。
CAM 为这种异步事件提供框架。一些事件源自较低级别(SIM 驱动程序),一些事件源自外围驱动程序,一些事件源自 CAM 子系统本身。任何驱动程序都可以为某些类型的异步事件注册回调,以便在发生这些事件时通知它。
例如此类事件的典型示例是设备重置。每个事务和事件通过“路径”来标识适用于哪些设备。目标特定事件通常发生在与该设备的事务过程中。因此,该事务的路径可能被重复使用以报告此事件(这是安全的,因为事件路径在事件报告例程中被复制但未被释放或传递给其他地方)。动态分配路径在任何时候都是安全的,包括中断例程,尽管这会带来一定的开销,而采用这种方法可能存在的问题是在那时可能没有空闲内存。对于总线重置事件,我们需要定义一个包含总线上所有设备的通配符路径。因此,我们可以提前为未来的总线重置事件创建路径,避免未来的内存短缺问题:
如您所见路径包括:
外设驱动程序的 ID(这里为空,因为我们没有)
SIM 驱动程序的 ID ( cam_sim_path(sim) )
设备的 SCSI 目标编号 (CAM_TARGET_WILDCARD 表示 "所有设备")
子设备的 SCSI LUN 编号 (CAM_LUN_WILDCARD 表示 "所有 LUNs")
如果驱动程序无法分配此路径,则将无法正常工作,因此在这种情况下,我们拆卸那个 SCSI 总线。
然后我们将路径指针保存在 softc 结构中以供将来使用。之后,我们保存 sim 的值(或者如果希望,在 xxx_probe() 退出时也可以将其丢弃)。
这就是最小化初始化的全部内容。要做正确的事情,还有一个问题尚未解决。
对于 SIM 驱动程序,有一个特别有趣的事件:当目标设备被视为丢失时。在这种情况下,重置与该设备的 SCSI 协商可能是一个好主意。因此,我们使用 CAM 为此事件注册回调。请求通过请求 CAM 在此类型请求的 CAM 控制块上执行 CAM 操作来传递:
在 CAM 子系统请求时执行一些操作。Sim 描述了该请求的 SIM,CCB 是请求本身。CCB 代表“CAM 控制块”。它是许多特定实例的联合体,每个实例描述某种类型交易的参数。所有这些实例共享 CCB 头,其中存储了参数的公共部分。
CAM 支持 SCSI 控制器以发起方(“正常”)模式和目标(模拟 SCSI 设备)模式工作。在这里,我们只考虑与发起方模式相关的部分。
有一些函数和宏(换句话说,方法)被定义用来访问结构体 sim 中的公共数据:
cam_sim_path(sim) - 路径标识(参见上文)
cam_sim_name(sim) - sim 的名称
cam_sim_softc(sim) - softc(驱动程序私有数据)结构的指针
cam_sim_unit(sim) - 单元编号
cam_sim_bus(sim) - 公交车 ID
要识别设备, xxx_action() 可以使用这些函数获取单元编号和指向其结构 softc 的指针。
请求类型存储在 ccb→ccb_h.func_code 中。因此,通常 xxx_action() 包含一个大开关:
从默认情况可以看出(如果收到未知命令),命令的返回代码被设置为 ccb→ccb_h.status ,并通过调用 xpt_done(ccb) 将完成的 CCB 返回给 CAM。
xpt_done() 不必从 xxx_action() 调用:例如,I/O 请求可以在 SIM 驱动程序内排队,和/或其 SCSI 控制器内。然后,当设备发出中断信号表明此请求的处理已完成时, xpt_done() 可能会从中断处理例程中调用。
实际上,CCB 状态不仅被分配为返回代码,而且 CCB 始终具有一些状态。在将 CCB 传递给 xxx_action() 例程之前,它会获得状态 CCB_REQ_INPROG,表示它正在进行中。在/sys/cam/cam.h 中定义了令人惊讶的状态值数量,应该能够详细表示请求的状态。更有趣的是,状态实际上是一个枚举状态值(低 6 位)和可能的附加类似标志位(高位)的“按位或”。枚举值将在稍后更详细地讨论。它们的摘要可以在错误摘要部分找到。可能的状态标志包括:
如果 SIM 驱动程序在处理 CCB 时发生严重错误(例如,设备不响应选择或破坏 SCSI 协议),则应通过调用 xpt_freeze_simq() 冻结请求队列,将此设备的其他尚未处理的但已入列的 CCB 返回到 CAM 队列,然后设置此问题 CCB 的标志并调用 xpt_done() 。此标志导致 CAM 子系统在处理错误后解冻队列。
如果设备返回错误条件并且在 CCB 中未设置标志 CAM_DIS_AUTOSENSE,则 SIM 驱动程序必须自动执行 REQUEST SENSE 命令以从设备中提取感觉(扩展错误信息)数据。如果此尝试成功,则应将感觉数据保存在 CCB 中,并设置此标志。
类似于 CAM_DEV_QFRZN,但在 SCSI 控制器本身出现问题(或资源短缺)时使用。然后,所有对控制器的其他请求都应通过 xpt_freeze_simq() 停止。一旦 SIM 驱动程序克服短缺并通过设置此标志返回某个 CCB 通知 CAM 后,控制器队列将重新启动。
CAM_SIM_QUEUED - 当 SIM 将 CCB 放入其请求队列时,应设置此标志(在此 CCB 被返回给 CAM 之前被移除)。此标志目前在 CAM 代码中未被使用,所以其目的纯粹是诊断性的。
CAM_QOS_VALID - QOS 数据现在有效。
函数 xxx_action() 不允许休眠,因此所有资源访问的同步都必须使用 SIM 或设备队列冻结进行。除了前述标志外,CAM 子系统还提供函数 xpt_release_simq() 和 xpt_release_devq() 来直接解冻队列,而无需将 CCB 传递给 CAM。
CCB 头部包含以下字段:
路径 - 请求的路径 ID
target_id - 请求的目标设备 ID
目标 LUN - 目标设备的 LUN ID
超时 - 此命令的超时间隔,以毫秒为单位
超时句柄 - 用于 SIM 驱动程序存储超时句柄的便利位置(CAM 子系统本身不对此做任何假设)
标志 - 有关请求 spriv_ptr0 和 spriv_ptr1 的各种信息-这些字段由 SIM 驱动程序的私有使用保留(例如,链接到 SIM 队列或 SIM 私有控制块);实际上,它们作为联合存在:spriv_ptr0 和 spriv_ptr1 具有类型(void *),spriv_field0 和 spriv_field1 的类型为 unsigned long,sim_priv.entries[0].bytes 和 sim_priv.entries[1].bytes 是具有与联合的其他 incarnations 一致大小的字节数组,sim_priv.bytes 是一个数组,大小是两倍大。
使用 CCB 的 SIM 私有字段的推荐方法是为其定义一些有意义的名称,并在驱动程序中使用这些有意义的名称,如下:
最常见的发起者模式请求是:
联合 ccb 的实例 "struct ccb_scsiio csio" 用于传递参数。它们是:
cdb_io - 指向 SCSI 命令缓冲区或缓冲区本身
cdb_len - SCSI 命令长度
data_ptr - 数据缓冲区的指针(如果使用 scatter/gather 会有点复杂)
dxfer_len - 传输数据的长度
sglist_cnt - 散射/聚集段的计数器
scsi_status - 返回 SCSI 状态的位置
sense_data - 如果命令返回错误,则用于存储 SCSI 感知信息的缓冲区(在这种情况下,如果 CCB 标志 CAM_DIS_AUTOSENSE 未设置,SIM 驱动程序应自动运行请求感知命令)
sense_len - 缓冲区的长度(如果恰巧高于 sense_data 的大小,SIM 驱动程序必须悄悄地假设较小的值)
resid, sense_resid - 如果数据传输或 SCSI sense 返回错误,则这些是残余(未传输)数据的返回计数器。它们似乎并不特别有意义,因此在难以计算的情况下(例如,在 SCSI 控制器的 FIFO 缓冲区中计算字节),大约值也足够。对于成功完成的传输,它们必须设置为零。
tag_action - 要使用的标记类型:
CAM_TAG_ACTION_NONE - 不使用标签进行此事务
MSG_SIMPLE_Q_TAG, MSG_HEAD_OF_Q_TAG, MSG_ORDERED_Q_TAG - 值等于适当的标签消息(请参阅/sys/cam/scsi/scsi_message.h);这只提供标签类型,SIM 驱动程序必须自行分配标签值
处理此请求的一般逻辑如下:
首先要做的是检查可能的竞争条件,确保命令在队列中等待时没有被中止:
同样,我们要检查设备是否被我们的控制器支持:
然后分配我们需要处理此请求的任何数据结构(例如依赖于卡的硬件控制块)。如果我们无法分配,则冻结 SIM 队列并记住我们有一个待处理操作,将 CCB 返回并要求 CAM 重新排队。稍后当资源变得可用时,SIM 队列必须通过返回一个状态中设置了 CAM_SIMQ_RELEASE 位的 ccb 来解冻。否则,如果一切顺利,将 CCB 与硬件控制块(HCB)链接并标记为已排队。
从 CCB 中提取目标数据到硬件控制块。检查是否要求分配标签,如果是,则生成唯一标签并构建 SCSI 标签消息。SIM 驱动程序还负责与设备协商,以设置最大互相支持的总线宽度、同步速率和偏移量。
然后设置 SCSI 命令。命令存储可以以许多有趣的方式在 CCB 中指定,由 CCB 标志指定。命令缓冲区可以包含在 CCB 中或指向,在后一种情况下,指针可以是物理的或虚拟的。由于硬件通常需要物理地址,我们始终将地址转换为物理地址,通常使用 busdma API。
如果请求物理地址,则可以返回带有状态 CAM_REQ_INVALID 的 CCB,当前的驱动程序就是这样做的。如果需要,物理地址也可以转换或映射回虚拟地址,但非常痛苦,所以我们不这样做。
现在是设置数据的时候了。同样,数据存储可以通过 CCB 中的许多有趣方式指定,由 CCB 标志指定。首先,我们得到数据传输的方向。最简单的情况是如果没有数据需要传输:
然后我们检查数据是在一个块中还是在一个分散-聚集列表中,地址是物理还是虚拟的。SCSI 控制器可能只能处理有限数量的有限长度的块。如果请求达到了这个限制,我们会返回一个错误。我们使用一个特殊的函数来返回 CCB,以便在一个地方处理 HCB 资源短缺。添加块的函数取决于驱动程序,这里我们不对其进行详细实现。请参阅 SCSI 命令(CDB)处理的描述以获取有关地址转换问题的详细信息。如果某些变化对于使用特定卡实现来说太困难或不可能,将状态 CAM_REQ_INVALID 返回是可以的。实际上,看起来分散-聚集能力现在在 CAM 代码中没有被使用。但至少要实现非分散虚拟缓冲区的情况,它被 CAM 积极使用。
如果为此 CCB 禁用了断开连接,我们将这些信息传递给 hcb:
如果控制器能够独自运行 REQUEST SENSE 命令,那么 CAM_DIS_AUTOSENSE 标志的值也应该传递给它,以防止 CAM 子系统不需要时自动执行 REQUEST SENSE。
唯一剩下的事情是设置超时,将我们的 hcb 传递给硬件并返回,其余的将由中断处理程序(或超时处理程序)完成。
这里是一个返回 CCB 的函数的可能实现。
在 CCB 中没有传输任何数据,除了头部之外,其中最有趣的参数是目标 ID。根据控制器硬件的不同,可能会构建类似于 XPT_SCSI_IO 请求的硬件控制块(请参阅 XPT_SCSI_IO 请求描述)并发送到控制器,或者 SCSI 控制器可能会立即被编程以向设备发送此复位消息,或者此请求可能根本不受支持(并返回状态 CAM_REQ_INVALID )。此外,在请求完成时,必须中止针对该目标的所有断开的事务(可能在中断例程中)。
此外,重置时所有针对目标的当前协商都会丢失,因此它们可能也会被清除。或者清除可能会被推迟,因为无论如何,目标都会在下一个事务上请求重新协商。
在 CCB 中不传递参数,唯一感兴趣的参数是通过结构 sim 指针指示的 SCSI 总线。
一个简化的实现会忘记总线上所有设备的 SCSI 协商,并返回状态 CAM_REQ_CMP。
实际上,正确的实现将重置 SCSI 总线(可能还会重置 SCSI 控制器),并将所有正在处理的 CCB,包括硬件队列中的和断开连接的,都标记为状态 CAM_SCSI_BUS_RESET,如:
将 SCSI 总线复位实现为一个函数可能是个好主意,因为如果事情出错,它将被超时函数作为最后手段重复使用。
参数被传递给联合 ccb 的实例"struct ccb_abort cab"中。它中唯一的参数字段是:
abort_ccb - 指向要中止的 CCB 的指针
如果中止不受支持,只需返回状态 CAM_UA_ABORT。这也是最简单实现此调用的方法,在任何情况下返回 CAM_UA_ABORT。
用诚实的方式实现这个请求的困难方式。首先检查中止是否适用于 SCSI 事务:
然后需要在我们的队列中找到这个 CCB。这可以通过遍历所有与此 CCB 关联的硬件控制块列表来完成:
现在我们看一下 HCB 的当前处理状态。它可能是坐在队列中等待发送到 SCSI 总线,正在传输中,或者已经断开连接并等待命令的结果,或者实际上已经被硬件完成但尚未被软件标记为完成。为了确保我们不会与硬件发生任何竞争,我们将 HCB 标记为已中止,这样如果这个 HCB 即将被发送到 SCSI 总线,SCSI 控制器将看到这个标志并跳过它。
如果 CCB 正在传输中,我们希望以某种硬件相关的方式向 SCSI 控制器发出我们要中止当前传输的信号。SCSI 控制器将设置 SCSI 注意信号,当目标响应时发送中止消息。我们还重置超时以确保目标不会永远沉睡。如果命令在合理的时间内(如 10 秒)内没有被中止,超时程序将继续重置整个 SCSI 总线。由于在合理的时间内命令将被中止,我们现在可以将中止请求返回为成功完成,并将中止的 CCB 标记为已中止(但尚未标记为已完成)。
如果 CCB 在已断开连接列表中,则将其设置为中止请求,并将其重新排队放在硬件队列的最前面。重置超时并报告中止请求已完成。
关于中止请求就是这些,尽管还有一个问题。由于中止消息清除了 LUN 上的所有正在进行的事务,我们必须将此 LUN 上的所有其他活动事务标记为已中止。这应该在事务被中止后的中断程序中完成。
实现 CCB 中止作为一个函数可能是一个很好的主意,如果 I/O 事务超时,这个函数可以被重复使用。唯一的区别是超时的事务会返回状态 CAM_CMD_TIMEOUT 用于请求超时。然后 XPT_ABORT 情况会很小,就像这样:
参数传递到联合 ccb 的实例“struct ccb_trans_setting cts”中。
有效 - 一个位掩码,显示应更新哪些设置:
CCB_TRANS_SYNC_RATE_VALID - 同步传输速率
CCB_TRANS_SYNC_OFFSET_VALID - 同步偏移
CCB_TRANS_BUS_WIDTH_VALID - 总线宽度
CCB_TRANS_DISC_VALID - 设置启用/禁用断开连接
CCB_TRANS_TQ_VALID - 设置启用/禁用标记排队
标志 - 由两部分组成,二进制参数和子操作的标识。 二进制参数是:
CCB_TRANS_DISC_ENB - 启用断开连接
CCB_TRANS_TAG_ENB - 启用标记排队
子操作如下:
CCB_TRANS_CURRENT_SETTINGS - 更改当前协商
CCB_TRANS_USER_SETTINGS - 记住所需的用户值同步周期,同步偏移 - 不言自明,如果 sync_offset==0,则请求异步模式 bus_width - 总线宽度,以位为单位(不是字节)
支持两组协商参数,用户设置和当前设置。用户设置在 SIM 驱动程序中并没有被广泛使用,这主要只是一个内存空间,上层可以在其中存储(以及稍后调用)有关参数的想法。设置用户参数不会导致传输速率重新协商。但是,当 SCSI 控制器进行协商时,绝不能将值设置得高于用户参数,因此它实质上是顶部边界。
当前设置是,顾名思义,当前的设置。更改它们意味着参数必须在下一次传输时重新协商。同样,这些“新的当前设置”不应该被强加给设备,它们只是协商的初始步骤。此外,它们必须受到 SCSI 控制器实际能力的限制:例如,如果 SCSI 控制器具有 8 位总线,并且请求要求设置 16 位宽传输,则在将其发送到设备之前,此参数必须被静默截断为 8 位传输。
一个注意事项是总线宽度和同步参数是每个目标的,而断开连接和标记启用参数是每个逻辑单元的。
推荐的实现方法是保留 3 组协商的参数(总线宽度和同步传输):
用户 - 与上述相同的用户集
当前 - 实际生效的那些
目标 - 由"current"参数设置请求的那些
代码看起来像:
然后,当下一个 I/O 请求被处理时,它将检查是否需要重新协商,例如通过调用函数 target_negotiated(hcb)。可以这样实现:
在重新协商值之后,必须将得到的值分配给当前参数和目标参数,因此对于将来的 I/O 事务,当前参数和目标参数将是相同的,并且 target_negotiated() 将返回 TRUE。当卡初始化时(在 xxx_attach() 中),当前协商值必须初始化为窄异步模式,目标和当前值必须初始化为控制器支持的最大值。
这些操作是 XPT_SET_TRAN_SETTINGS 的反向操作。使用 CCB_TRANS_CURRENT_SETTINGS 或 CCB_TRANS_USER_SETTINGS 标志根据请求的数据填充 CCB 实例"struct ccb_trans_setting cts"(如果两者都设置,则现有驱动程序将返回当前设置)。将 valid 字段中的所有位设置。
参数传递在联合 ccb 的实例“struct ccb_calc_geometry ccg”中进行:
block_size - 输入,以字节为单位的块(又名扇区)大小
输入, 以字节为单位的卷大小
输出, 逻辑柱面
输出, 逻辑磁头
secs_per_track - output, logical sectors per track
If the returned geometry differs much enough from what the SCSI controller BIOS thinks and a disk on this SCSI controller is used as bootable the system may not be able to boot. The typical calculation example taken from the aic7xxx driver is:
This gives the general idea, the exact calculation depends on the quirks of the particular BIOS. If BIOS provides no way set the "extended translation" flag in EEPROM this flag should normally be assumed equal to 1. Other popular geometries are:
一些系统 BIOS 和 SCSI BIOS 彼此争斗,成功的程度不同,例如 Symbios 875/895 SCSI 和 Phoenix BIOS 的组合在上电后可能会给出几何 128/63,在硬重置或软重启后可能会给出几何 255/63。
这些属性以联合 ccb 的实例"struct ccb_pathinq cpi"的形式返回:
版本号 - SIM 驱动程序版本号,现在所有驱动程序使用 1
hba_inquiry - 控制器支持的特性位掩码:
PI_MDP_ABLE - 支持 MDP 消息(来自 SCSI3 的某些内容?)
支持 32 位宽 SCSI
支持 16 位宽 SCSI
可以协商同步传输速率
PI_LINKED_CDB - 支持链接命令
PI_TAG_ABLE - 支持标记命令
PI_SOFT_RST - 支持软复位替代方案(在 SCSI 总线内,硬复位和软复位是互斥的)
target_sprt - 用于目标模式支持的标志,如果不支持则为 0
hba_misc - 杂项控制器功能:
PIM_SCANHILO - 从高 ID 到低 ID 进行总线扫描
不可删除 - 可移动设备不包含在扫描中
PIM_NOINITIATOR - 发起者角色不支持
PIM_NOBUSRESET - 用户已禁用初始总线复位
hba_eng_cnt - 神秘的 HBA 引擎计数,与压缩有关,现在始终设置为 0
vuhba_flags - 供应商特有标志,现在未使用
max_target - 最大支持的目标 ID(8 位总线为 7,16 位总线为 15,光纤通道为 127)
max_lun - 最大支持的 LUN ID(对于较旧的 SCSI 控制器为 7,对于较新的为 63)
async_flags - 安装的异步处理程序的位掩码,目前未使用
hpath_id - 子系统中最高的路径 ID,目前未使用
单元编号 - 控制器单元编号,cam_sim_unit(模拟器)
总线编号 - 总线编号,cam_sim_bus(模拟器)
发起者编号 - 控制器本身的 SCSI ID
基础传输速度 - 异步窄带传输的标称传输速度,对于 SCSI 等于 3300
sim_vid - SIM 驱动程序的供应商 ID,最大长度为 SIM_IDLEN 的以零结尾的字符串,包括终止零
hba_vid - SCSI 控制器的供应商 ID,最大长度为 HBA_IDLEN 的以零结尾的字符串,包括终止零
dev_name - 设备驱动程序名称,长度最大为 DEV_IDLEN(包括终止零),与 cam_sim_name(sim)相等
设置字符串字段的推荐方法是使用 strncpy,例如:
在设置值后,将状态设置为 CAM_REQ_CMP,并将 CCB 标记为完成。
当中断子系统不起作用时(例如,系统崩溃并正在创建系统转储时),使用轮询功能来模拟中断。CAM 子系统在调用轮询例程之前设置适当的中断级别。因此,它所需做的就是调用中断例程(或者反过来,轮询例程可能正在执行实际操作,而中断例程只会调用轮询例程)。那么为什么要费心使用单独的函数呢?这与不同的调用约定有关。 xxx_poll 例程将结构 cam_sim 指针作为其参数,而根据通用约定,PCI 中断例程会获得指向结构 xxx_softc 的指针,ISA 中断例程只会获得设备单元号。因此,轮询例程通常会如下所示:
或
如果已设置异步事件回调,则应定义回调函数。
callback_arg - 在注册回调时提供的值。
代码 - 识别事件类型
路径 - 标识事件适用的设备
参数 - 事件特定参数
一个类型的事件的实现,AC_LOST_DEVICE,看起来像:
中断例程的确切类型取决于 SCSI 控制器连接的外围总线的类型(PCI、ISA 等)。
SIM 驱动程序的中断例程在中断级别 splcam 上运行。因此,在驱动程序中应该使用 splcam() 来同步中断例程和驱动程序的其他部分(对于一个多处理器感知的驱动程序,情况会更加有趣,但我们在这里忽略这种情况)。本文档中的伪代码快乐地忽略了同步的问题。真实的代码不应该忽略它们。一个简单的方法是在进入其他例程时设置 splcam() ,并在返回时将其重置,从而通过一个大的临界段来保护它们。为了确保中断级别总是会被恢复,可以定义一个包装函数,如:
这种方法简单而稳健,但它的问题在于中断可能会被阻塞相对长的时间,这会对系统的性能产生负面影响。另一方面, spl() 系列的功能开销相当高,因此大量微小的临界段可能也不是一个好主意。
中断例程处理的条件和详细信息非常依赖于硬件。我们考虑“典型”条件集。
首先,我们检查总线上是否遇到了 SCSI 复位(可能是由另一个 SCSI 控制器引起的)。如果是这样,我们将放弃所有已排队和断开连接的请求,报告事件并重新初始化我们的 SCSI 控制器。在此期间很重要的一点是,在此初始化过程中,控制器不会发出另一个复位信号,否则同一条 SCSI 总线上的两个控制器可能会永远地发出复位信号。致命控制器错误/挂起的情况可能也可以在同一位置处理,但可能还需要向 SCSI 总线发送复位信号,以重置与 SCSI 设备的连接状态。
如果中断不是由控制器范围内的条件引起的,那么当前硬件控制块可能发生了一些问题。根据硬件的不同,可能还会发生其他与 HCB 无关的事件,我们在这里不考虑它们。然后,我们分析此 HCB 发生了什么:
首先,我们检查 HCB 是否已完成,如果是,则检查返回的 SCSI 状态。
然后查看此状态是否与 REQUEST SENSE 命令相关,如果是,请以简单方式处理。
否则,命令本身已完成,请更加注意细节。如果未为此 CCB 禁用自动感知,并且命令因感知数据而失败,则运行 REQUEST SENSE 命令以接收该数据。
一个典型的情况可能是协商事件:从 SCSI 目标接收的协商消息(作为回应我们的协商尝试或目标主动发起)或目标无法协商(拒绝我们的协商消息或不回应它们)。
然后,我们以与之前相同的简单方式处理在自动感应过程中可能发生的任何错误。否则,我们再次仔细查看细节。
我们考虑的下一个事件是意外断开连接。在 ABORT 或 BUS DEVICE RESET 消息后被视为正常,其他情况下视为异常。
如果目标拒绝接受标签,我们通知 CAM 并将返回该 LUN 的所有命令。
然后我们检查其他一些条件,处理基本上仅限于设置 CCB 状态:
然后我们检查错误是否严重到需要冻结输入队列直到处理完毕,如果是,则执行:
这总结了通用中断处理,尽管特定控制器可能需要一些补充。
在执行 I/O 请求时,可能会出现许多问题。错误的原因可以在 CCB 状态中详细报告。本文档中散布了使用示例。为了完整起见,这里是典型错误条件的推荐响应摘要:
CAM_RESRC_UNAVAIL - 某些资源暂时不可用,SIM 驱动程序无法在资源可用时生成事件。此资源的一个示例是某些控制器内部硬件资源,控制器在其可用时不生成中断。
CAM_UNCOR_PARITY - 发生未恢复的奇偶校验错误
CAM_DATA_RUN_ERR - 数据超限或意外数据阶段(与 CAM_DIR_MASK 中指定的方向相反)或宽传输的奇数传输长度
CAM_SEL_TIMEOUT - 选择超时发生(目标未响应)
命令超时-发生了命令超时(超时函数运行)
SCSI 状态错误-设备返回错误
自动感知失败-设备返回错误,请求感知命令失败
收到 CAM_MSG_REJECT_REC - 收到 MESSAGE REJECT 消息
收到 CAM_SCSI_BUS_RESET - 收到 SCSI 总线复位
收到 CAM_REQ_CMP_ERR - "不可能的"SCSI 阶段发生或者其他奇怪的情况或者如果没有更多细节可用,则是一般错误
CAM_UNEXP_BUSFREE - 意外断开发生
CAM_BDR_SENT - 总线设备复位消息已发送到目标
CAM_UNREC_HBA_ERROR - 无法恢复的主机总线适配器错误
CAM_REQ_TOO_BIG - 请求对于此控制器太大
CAM_REQUEUE_REQ - 此请求应重新排队以保留事务顺序。当 SIM 识别应该冻结队列并必须将目标的其他排队请求放回 XPT 队列时,通常会发生这种情况。此类错误的典型情况包括选择超时、命令超时和其他类似情况。在这种情况下,有问题的命令返回指示错误的状态,尚未发送到总线的其他命令将被重新排队。
CAM_LUN_INVALID - 请求中的 LUN ID 不受 SCSI 控制器支持
CAM_TID_INVALID - 请求中的目标 ID 不受 SCSI 控制器支持
当 HCB 的超时时间到期时,该请求应该被中止,就像 XPT_ABORT 请求一样。唯一的区别是,中止请求的返回状态应该是 CAM_CMD_TIMEOUT,而不是 CAM_REQ_ABORTED(这就是为什么最好将中止的实现作为一个函数来完成)。但还有一个可能的问题:如果中止请求本身被卡住怎么办?在这种情况下,SCSI 总线应该被重置,就像 XPT_RESET_BUS 请求一样(在这两个地方都调用一个函数的想法同样适用于这里)。此外,如果设备重置请求被卡住,我们也应该重置整个 SCSI 总线。因此,超时函数最终会是这样的:
当我们中止一个请求时,所有其他与同一目标/LUN 断开连接的请求也会被中止。因此,出现了一个问题,我们应该用状态 CAM_REQ_ABORTED 还是 CAM_CMD_TIMEOUT 返回它们?当前的驱动程序使用 CAM_CMD_TIMEOUT。这似乎是合乎逻辑的,因为如果一个请求超时了,那么设备可能发生了真正糟糕的事情,所以如果它们不被打扰,它们会自己超时。
本文介绍了 SMPng 架构的当前设计和实现。首先介绍了基本原语和工具。接下来,概述了 FreeBSD 内核的同步和执行模型的一般架构。然后讨论了特定子系统的锁定策略,记录了为每个子系统引入细粒度同步和并行性所采取的方法。最后,提供了详细的实施说明,以激励设计选择,并使读者了解涉及特定原语使用的重要影响。
本文是一个正在进行的工作,将根据与 SMPng 项目相关的正在进行的设计和实施活动进行更新。许多部分目前仅以概述形式存在,但随着工作的进行将会逐步完善。关于本文档的更新或建议可直接发送给文档编辑。
SMPng 的目标是允许内核并发。内核基本上是一个相当庞大和复杂的程序。为了使内核多线程化,我们使用了一些用于使其他程序多线程化的工具。这些工具包括互斥锁、共享/独占锁、信号量和条件变量。有关这些和其他与 SMP 相关术语的定义,请参阅本文的术语表部分。
存在几种处理内存屏障和原子指令的现有方法,所以本部分不会包含太多细节。简单地说,如果在保护对该变量的写入时使用锁定,则不能在没有锁的情况下读取变量。当你考虑到内存屏障仅仅确定了内存操作的相对顺序时,这一点就变得很明显;它们并不对内存操作的时间作出任何保证。也就是说,内存屏障不会强制 CPU 的本地缓存或存储缓冲区刷新。相反,在释放锁时的内存屏障仅确保对受保护数据的所有写入对其他 CPU 或设备可见,如果用于释放锁的写操作可见的话。CPU 可以自由地将数据保留在其缓存或存储缓冲区中,只要它愿意。然而,如果另一个 CPU 对同一数据执行原子指令,那么第一个 CPU 必须保证更新的值对第二个 CPU 可见,并且还需处理内存屏障可能需要的其他任何操作。
例如,假设一个简单的模型,其中数据在进入主内存(或全局缓存)时被视为可见,当一个 CPU 上触发了原子指令时,其他 CPU 的存储缓冲区和缓存必须刷新对该相同缓存行的任何写入,以及在内存屏障后面等待的任何操作。
使用受原子指令保护的项目时需要特别小心。例如,在睡眠互斥锁实现中,我们必须使用 atomic_cmpset 而不是 atomic_set 来打开 MTX_CONTESTED 位。原因是,我们将 mtx_lock 的值读入一个变量,然后根据该读取做出决定。然而,我们读取的值可能过时,或者在我们做决定时可能会更改。因此,当执行 atomic_set 时,它可能设置另一个值上的位,而不是我们做出决定的值。因此,我们必须使用 atomic_cmpset ,只有当我们做出决定的值是最新的和有效的时,才设置该值。
最后,原子指令只允许更新或读取一个项目。如果需要原子地更新多个项目,则必须改用锁。例如,如果必须读取两个计数器,并且这些计数器的值相对一致,则这些计数器必须受到锁的保护,而不是通过单独的原子指令。
读锁不需要像写锁那样强大。两种类型的锁都需要确保它们正在访问的数据不是陈旧的。但是,只有写访问需要独占访问。多个线程可以安全地读取一个值。可以通过多种方式为读取和写入使用不同类型的锁。
首先,通过在写入时使用独占锁,在读取时使用共享锁可以使用 sx 锁。这种方法非常直接。
第二种方法有点更加隐晦。您可以用多个锁来保护数据。然后,对于读取该数据,您只需要拥有其中一个锁的读取锁。但是,要写入数据,您需要拥有所有锁的写锁。这可能会使写入变得非常昂贵,但在数据以各种方式访问时可能会很有用。例如,父进程指针由 proctree_lock sx 锁和每个进程互斥锁保护。有时 proc 锁会更容易,因为我们只是检查我们已经锁定的进程的父进程是谁。然而,其他地方如 inferior 需要通过父指针遍历进程树,并且锁定每个进程既是困难又是痛苦,无法保证您正在检查的条件对于检查和采取的操作都保持有效。
如果您需要一个锁来检查变量的状态,以便根据您读取的状态采取行动,您不能只在读取变量时保持锁定,然后在对您读取的值采取行动之前放弃锁定。一旦您放弃锁定,变量可能会发生更改,使您的决定无效。因此,您必须在读取变量和执行检验结果的操作时保持锁定。
跟随其他多线程 UNIX®内核的模式,FreeBSD 通过为中断处理程序提供自己的线程上下文来处理中断处理程序。为中断处理程序提供上下文允许它们在锁上阻塞。然而,为了避免延迟,中断线程以实时内核优先级运行。因此,中断处理程序不应该执行很长时间,以避免使其他内核线程饥饿。此外,由于多个处理程序可能共享一个中断线程,中断处理程序不应该休眠或使用可休眠锁,以避免使另一个中断处理程序饥饿。
FreeBSD 中当前的中断线程被称为重量级中断线程。之所以这样称呼是因为切换到中断线程涉及完整的上下文切换。在最初的实现中,内核不是抢占式的,因此中断会在中断了内核线程时等待,直到内核线程阻塞或返回用户空间才有机会运行。
为了解决延迟问题,FreeBSD 内核已经被改为抢占式。目前,我们只在释放睡眠互斥锁或中断到来时抢占内核线程。然而,计划是使 FreeBSD 内核完全抢占式,如下所述。
并非所有中断处理程序都在线程上下文中执行。相反,某些处理程序直接在主中断上下文中执行。这些中断处理程序目前被错误地命名为“快速”中断处理程序,因为内核早期版本中使用的 INTR_FAST 标志用于标记这些处理程序。当前仅使用这些类型的中断处理程序的中断是时钟中断和串行 I/O 设备中断。由于这些处理程序没有自己的上下文,因此它们可能不会获取阻塞锁,因此只能使用自旋互斥体。
最后,在 MD 代码中可以增加一种名为轻量级上下文切换的可选优化。由于中断线程在内核上下文中执行,它可以借用任何进程的 vmspace。因此,在轻量级上下文切换中,切换到中断线程不会切换 vmspaces,而是借用被中断线程的 vmspace。为了确保被中断线程的 vmspace 不会在我们之下消失,不允许被中断线程执行,直到中断线程不再借用其 vmspace。当中断线程阻塞或完成时,这种情况可能发生。如果中断线程阻塞,那么当其再次可运行时,它将使用自己的上下文。因此,它可以释放被中断线程。
这种优化的缺点是它们非常机器特定和复杂,因此只有在有大的性能改进时才值得努力。目前还为时过早,实际上,它可能会降低性能,因为几乎所有中断处理程序将立即在 Giant 上阻塞并在阻塞时需要进行线程修复。此外,Mike Smith 提出了一种另一种中断处理的替代方法,是这样工作的:
每个中断处理程序都有两个部分:在主中断上下文中运行的谓词和在自己的线程上下文中运行的处理程序。
如果中断处理程序有一个谓词,那么当触发中断时,将运行谓词。如果谓词返回 true,则假定中断已完全处理,并且内核从中断返回。如果谓词返回 false 或没有谓词,则计划运行线程处理程序。
将轻量级上下文切换适应这种方案可能会相当复杂。由于我们可能希望在将来某个时候转换到这种方案,最好推迟对轻量级上下文切换的工作,直到我们就最终中断处理架构达成一致,并确定轻量级上下文切换如何适应或不适应其中。
内核抢占相当简单。基本思想是 CPU 应始终执行最高优先级的可用工作。至少这是理想状态。有几种情况下,实现理想状态的成本不值得完美。
实现完整的内核抢占非常简单:当您将一个线程调度到运行队列时,您检查其优先级是否高于当前正在执行的线程。如果是,您会启动到该线程的上下文切换。
虽然锁可以保护大多数数据在抢占的情况下,但并非所有内核都是抢占安全的。例如,如果一个持有自旋互斥锁的线程被抢占,新线程尝试获取相同的自旋互斥锁,那么新线程可能会永远自旋,因为被中断的线程可能永远没有机会执行。此外,一些代码,例如在 Alpha 上为进程分配地址空间号的代码在 exec 期间需要不被抢占,因为它支持实际的上下文切换代码。通过使用临界区,这些代码部分禁用了抢占。
临界区 API 的责任是防止在临界区内发生上下文切换。对于一个完全抢占的内核,当前线程以外的每个线程的 setrunqueue 都是一个抢占点。一种实现方法是 critical_enter 设置一个每线程标志,该标志由其对应的部分清除。如果在设置了此标志的情况下调用 setrunqueue ,则无论新线程相对于当前线程的优先级如何,它都不会抢占。但是,由于临界区用于自旋互斥锁以防止上下文切换,并且可以获取多个自旋互斥锁,临界区 API 必须支持嵌套。为此,当前实现使用嵌套计数而不是单一的每线程标志。
为了尽量减少延迟,临界区内的抢占是被推迟而不是丢弃的。如果一个通常会被抢占的线程在当前线程处于临界区时变为可运行状态,则会设置一个每线程标志,以指示存在待处理的抢占。当最外层的临界区退出时,将检查此标志。如果标志被设置,则当前线程将被抢占以允许更高优先级的线程运行。
中断对于自旋互斥锁构成问题。如果低级中断处理程序需要一个锁,它需要不中断任何需要该锁的代码,以避免可能的数据结构损坏。目前,通过临界区 API 的 cpu_critical_enter 和 cpu_critical_exit 函数实现了这种机制。目前,该 API 在 FreeBSD 的所有平台上禁用和重新启用中断。这种方法可能并非完全最佳,但易于理解且易于正确实现。从理论上讲,第二个 API 只需要用于主中断上下文中使用的自旋互斥锁。然而,为了使代码更简单,它用于所有自旋互斥锁甚至所有临界区。将 MD API 与 MI API 分离并仅在自旋互斥锁实现中与 MI API 一起使用可能是可取的。如果采用这种方法,则很可能需要对 MD API 进行重命名以显示它是一个单独的 API。
正如前面提到的,已经做出了一些权衡,牺牲了完美抢占可能并不总是提供最佳性能的情况。
第一个折衷是抢占代码不考虑其他 CPU。假设我们有两个 CPU A 和 B,A 的线程优先级为 4,B 的线程优先级为 2。如果 CPU B 使优先级为 1 的线程可运行,则从理论上讲,我们希望 CPU A 切换到新线程,这样我们将运行两个最高优先级的可运行线程。然而,确定要在哪个 CPU 强制执行抢占以及实际通过 IPI 信号传递给该 CPU 以及所需的同步成本将是巨大的。因此,当前代码将强制 CPU B 切换到更高优先级的线程。请注意,这仍然使系统处于更好的位置,因为 CPU B 正在执行优先级为 1 的线程,而不是优先级为 2 的线程。
第二个折衷限制立即内核抢占为实时优先级内核线程。在上面定义的抢占的简单情况下,如果一个更高优先级的线程变为可运行状态,线程将立即被抢占(或者在关键部分退出时)。然而,许多在内核中执行的线程只在内核环境中短暂执行一段时间,然后阻塞或返回到用户空间。因此,如果内核抢占这些线程以运行另一个非实时内核线程,那么内核可能会在执行线程准备睡眠或执行之前切换出执行线程。CPU 上的缓存必须调整到新线程。当内核返回到被抢占的线程时,它必须重新填充丢失的所有缓存信息。此外,执行了两次额外的上下文切换,如果内核延迟抢占直到第一个线程阻塞或返回到用户空间,则可以避免这两次额外的上下文切换。因此,默认情况下,如果更高优先级的线程是实时优先级线程,抢占代码将立即抢占。
打开所有内核线程的完全内核抢占对于调试工具具有价值,因为它暴露了更多的竞态条件。在单处理器系统上,这尤其有用,否则很难模拟许多竞态条件。因此,有一个内核选项 FULL_PREEMPTION 可以启用所有内核线程的抢占,可用于调试目的。
简而言之,当线程从一个 CPU 移动到另一个 CPU 时发生线程迁移。在非抢占内核中,这只能发生在明确定义的点,比如调用 msleep 或返回到用户空间时。然而,在抢占内核中,中断可以在任何时候强制执行抢占并可能发生迁移。这可能会对每个 CPU 数据产生负面影响,因为除了 curthread 和 curpcb 之外,在你迁移时数据可能会随时更改。由于你可能随时迁移,这使得未受保护的每个 CPU 数据访问变得相当无用。因此,希望能够禁用对需要每个 CPU 数据保持稳定的代码段的迁移。
当前临界区阻止了迁移,因为它们不允许上下文切换。然而,在某些情况下,这可能是一种太强的要求,因为临界区实际上还会阻止当前处理器上的中断线程。因此,另一个 API 已经提供,允许当前线程指示,如果它被抢占,则不应迁移到另一个 CPU。
此 API 被称为线程固定,由调度程序提供。该 API 包括两个函数: sched_pin 和 sched_unpin 。这些函数管理每个线程的嵌套计数 td_pinned 。当嵌套计数大于零时,线程被固定,线程初始时未固定,嵌套计数为零。每个调度程序实现都需要确保只有在第一次调用 sched_pin 时,固定的线程才会在执行时所在的 CPU 上执行。由于嵌套计数只有线程本身编写,并且只有在固定线程未执行但持有 sched_lock 时由其他线程读取时, td_pinned 不需要任何锁定。 sched_pin 函数增加嵌套计数, sched_unpin 函数减少嵌套计数。请注意,这些函数仅在当前线程上操作,并将当前线程绑定到其在执行时所在的 CPU。要将任意线程绑定到特定 CPU,应改用 sched_bind 和 sched_unbind 函数。
内核设施允许内核服务注册函数以作为软件中断的一部分执行。事件根据所需的时钟滴答数进行调度,并在大约正确的时间调用消费者提供的函数的回调。
挂起超时事件的全局列表由全局自旋互斥锁保护;所有对超时列表的访问都必须在持有此互斥锁的情况下执行。当唤醒时,它扫描挂起超时列表以查找应触发的超时。为避免锁定顺序颠倒,当调用提供的回调函数时,线程将释放互斥锁。如果在注册期间未设置标志,则在调用调用之前将抓取 Giant,然后在之后释放。在继续之前,将重新抓取互斥锁。代码在释放互斥锁时小心地保持列表处于一致状态。如果启用了,则会测量执行每个函数所需的时间,并在超过阈值时生成警告。
struct ucred 是内核的内部凭据结构,通常用作内核内的基础进程驱动访问控制。BSD 派生系统使用“写时复制”模型来处理凭据数据:凭据结构可能存在多个引用,当需要进行更改时,结构会被复制,修改,然后替换引用。由于广泛缓存凭据以实现对打开的访问控制,这导致了大量的内存节省。随着向细粒度 SMP 的转变,这种模型还通过要求仅在未共享的凭据上进行修改,从而避免在使用已知共享凭据时需要显式同步,大大节省了锁操作。
具有单个引用的凭据结构被视为可变的;共享凭据结构不得修改,否则会存在竞争条件。一个互斥体, cr_mtxp 保护 struct ucred 的引用计数,以保持一致性。对结构的任何使用都需要在使用期间具有有效引用,否则结构可能会在非法使用者的情况下被释放。
struct ucred 互斥锁是一个叶子互斥锁,通过互斥锁池实现,以提高性能。
通常,凭据在访问控制决策中以只读方式使用,在这种情况下,通常优先选择 td_ucred ,因为它不需要锁定。当进程的凭据更新时,必须跨检查和更新操作保持 proc 锁,从而避免竞争。进程凭据 p_ucred 必须用于检查和更新操作,以防止时间检查、时间使用竞争。
如果系统调用在更新进程凭据后执行访问控制,那么 td_ucred 的值也必须刷新为当前进程值。这将防止在更改后使用过时的凭据。内核会自动从进程 p_ucred 刷新线程结构中的 td_ucred 指针,每当进程进入内核时,允许使用新的凭据进行内核访问控制。
细节待定。
struct prison 存储与使用 jail(2) API 创建的 jails 的维护相关的管理详细信息。这包括每个 jail 主机名、IP 地址和相关设置。由于结构实例的指针被许多凭据结构共享,因此此结构是引用计数的。一个互斥锁 pr_mtx 保护对引用计数和结构 jail 内所有可变变量的读写访问。一些变量仅在创建 jail 时设置,对 struct prison 的有效引用足以读取这些值。每个条目的精确锁定通过 sys/jail.h 中的注释记录。
TrustedBSD MAC 框架以各种内核对象的形式维护数据,如 struct label 所示。一般来说,内核对象中的标签受到与内核对象的其余部分相同的锁的保护。例如,在 struct vnode 中的 v_label 标签受 vnode 锁的保护。
除了标准内核对象中维护的标签之外,MAC 框架还维护着注册的活跃策略列表。该策略列表受全局互斥锁( mac_policy_list_lock )和一个繁忙计数(也受互斥锁保护)的保护。由于许多访问控制检查可能同时发生,只读访问策略列表需要持有互斥锁同时递增(后来递减)繁忙计数。在 MAC 条目操作期间不必一直持有互斥锁——一些操作,例如对文件系统对象上的标签操作,是长时间的。要修改策略列表,比如在策略注册和注销期间,必须持有互斥锁并且引用计数必须为零,以防止在使用列表时对其进行修改。
控制变量 mac_policy_list_not_busy 可用于需要等待列表变得不繁忙的线程,但只有在调用者未持有其他锁时才能等待此条件变量,否则可能会发生锁顺序违规问题。实际上,繁忙计数充当了对框架访问的一种共享/互斥锁:不同的是,与 sx 锁不同,等待列表变得不繁忙的消费者可能会被饿死,而不是允许锁顺序问题关于繁忙计数和可能在进入(或内部)MAC 框架时持有的其他锁。
对于模块子系统,存在一个用于保护共享数据的单个锁。此锁是一个共享/排他(SX)锁,并且很有可能需要被获取(共享或排他),因此添加了一些宏以使对锁的访问更加容易。这些宏可以在 sys/module.h 中找到,从使用角度来看相当基础。在此锁保护之下的主要结构是 module_t 结构(在共享时)和全局 modulelist_t 结构,模块。应该查看 kern/kern_module.c 中的相关源代码以进一步了解锁定策略。
新总线系统将有一个 SX 锁。读取者将持有共享(读取)锁(sx_slock(9)),写入者将持有排他(写入)锁(sx_xlock(9))。内部函数将根本不进行锁定。外部可见的函数将根据需要进行锁定。那些无论比赛赢还是输都无关紧要的项目将不被锁定,因为它们往往在各个地方被读取(例如 device_get_softc(9))。对新总线数据结构的更改将相对较少,因此一个单独的锁应该足够,并且不会造成性能损失。
…
进程层次结构
进程锁,引用
系统调用期间要冻结的进程条目的线程特定副本,包括 td_ucred
进程间操作
进程组和会话
对 sched_lock 的大量引用,以及指向文档中特定原语和相关魔法的注释。
select 和 poll 函数允许线程阻塞,等待文件描述符上的事件发生,最常见的是文件描述符是否可读或可写。
…
SIGIO 服务允许进程请求在指定文件描述符的读/写状态发生变化时向其进程组传递 SIGIO 信号。在任何给定内核对象上,最多允许有一个进程或进程组注册 SIGIO,该进程或组被称为所有者。支持 SIGIO 注册的每个对象都包含一个指针字段,如果对象未注册,则该字段值为 NULL ,或者指向描述注册的 struct sigio 。该字段受全局互斥锁 sigio_lock 保护。调用 SIGIO 维护函数时,必须按引用传递此字段,以便在未受锁保护时不会生成字段的本地注册副本。
为与任何进程或进程组关联的每个注册对象分配一个 struct sigio ,并包含指向对象、所有者、信号信息、凭证和注册的一般处理的反向指针。每个进程或进程组包含一个注册 struct sigio 结构的列表, p_sigiolst 用于进程, pg_sigiolst 用于进程组。这些列表分别由进程或进程组锁保护。每个 struct sigio 中的大多数字段在注册期间保持不变,但 sio_pgsigio 字段除外,它将 struct sigio 链接到进程或进程组列表中。实现支持 SIGIO 的新内核对象的开发人员通常希望在调用支持 SIGIO 函数时避免持有结构锁,例如 fsetown 或 funsetown ,以避免在结构锁和全局 SIGIO 锁之间定义锁定顺序。通过在结构上使用提升的引用计数,例如在管道操作期间依赖于文件描述符引用到管道上,通常可以实现这一点。
sysctl MIB 服务既可以从内核内部调用,也可以从用户空间应用程序使用系统调用调用。锁定引发至少两个问题:首先,保护维护命名空间的结构,其次,与通过 sysctl 接口访问的内核变量和函数的交互。由于 sysctl 允许直接导出(和修改)内核统计信息和配置参数,因此 sysctl 机制必须了解这些变量的适当锁定语义。目前,sysctl 使用单个全局 sx 锁来串行使用 sysctl ;但是,它被假定在 Giant 下运行,并且不提供其他保护措施。本节的其余部分推测了 sysctl 的锁定和语义更改。
需要更改 sysctl 更新值的操作顺序,从读取旧值、复制进和复制出、写入新值改为复制进、锁定、读取旧值和写入新值、解锁、复制出。普通的 sysctl 只需复制出旧值并设置新值(复制进)的可能仍然能够遵循旧模型。不过,对于所有 sysctl 处理程序使用第二个模型可能更为简洁,以避免锁操作。
为了允许常见情况,一个 sysctl 可以在 SYSCTL_FOO 宏和结构体中嵌入一个互斥体的指针。这对大多数 sysctl 是有效的。对于由 sx 锁、自旋互斥体或其他除了单个睡眠互斥体之外的锁定策略保护的值,可以使用 SYSCTL_PROC 节点来正确获取锁定。
任务队列的接口有两个基本锁与其关联,以保护相关的共享数据。 taskqueue_queues_mutex 旨在作为一个用于保护 taskqueue_queues TAILQ 的锁。与该系统关联的另一个互斥锁是 struct taskqueue 数据结构中的一个。使用同步原语的目的是保护 struct taskqueue 中数据的完整性。值得注意的是,没有单独的宏来帮助用户锁定他/她自己的工作,因为这些锁很可能不会在 kern/subr_taskqueue.c 之外使用。
一个睡眠队列是一个结构,它保存了在等待通道上睡眠的线程列表。每个没有在等待通道上睡眠的线程都携带着一个睡眠队列结构。当一个线程在等待通道上阻塞时,它将自己的睡眠队列结构捐赠给该等待通道。与等待通道关联的睡眠队列存储在哈希表中。
睡眠队列哈希表保存了至少有一个阻塞线程的等待通道的睡眠队列。哈希表中的每个条目称为睡眠队列链。该链包含一个睡眠队列的链表和一个自旋互斥锁。自旋互斥锁保护睡眠队列列表以及列表上睡眠队列结构的内容。一个给定等待通道只关联一个睡眠队列。如果多个线程在等待通道上阻塞,那么除第一个线程外,所有其他线程关联的睡眠队列都存储在主睡眠队列的空闲睡眠队列列表中。当一个线程从睡眠队列中移除时,如果它不是队列上唯一睡眠的线程,则它将从主队列的空闲列表中获得一个睡眠队列结构。当最后一个线程恢复时,它将获得主睡眠队列。由于线程可能以不同于添加顺序的顺序从睡眠队列中移除,因此线程可能以不同的睡眠队列结构离开睡眠队列。
sleepq_lock 函数锁定映射到特定等待通道的睡眠队列链的自旋互斥锁。 sleepq_lookup 函数在哈希表中查找与给定等待通道关联的主睡眠队列。如果找不到主睡眠队列,则返回 NULL 。 sleepq_release 函数解锁与给定等待通道关联的自旋互斥锁。
通过 sleepq_add 函数将线程添加到休眠队列中。此函数接受等待通道、保护等待通道的互斥量的指针、等待消息描述字符串和标志掩码。在调用此函数之前,应该通过 sleepq_lock 锁定休眠队列链。如果没有互斥量保护等待通道(或者由巨人保护),那么互斥量指针参数应该是 NULL 。标志参数包含一个类型字段,指示线程正在添加到哪种类型的休眠队列,以及一个指示休眠是否可中断的标志( SLEEPQ_INTERRUPTIBLE )。当前只有两种类型的休眠队列:通过 msleep 和 wakeup 函数管理的传统休眠队列( SLEEPQ_MSLEEP )和条件变量休眠队列( SLEEPQ_CONDVAR )。休眠队列类型和锁指针参数仅用于内部断言检查。调用 sleepq_add 的代码应该在通过 sleepq_lock 锁定相关休眠队列链并在通过其中一个等待函数阻塞之前,明确解锁任何保护等待通道的互斥量。
通过调用 sleepq_set_timeout 设置休眠的超时。该函数接受等待通道和超时时间(相对滴答计数)作为参数。如果需要通过到达信号中断休眠,则还应调用 sleepq_catch_signals 函数。此函数仅接受等待通道作为其唯一参数。如果这个线程已经有一个挂起的信号,那么 sleepq_catch_signals 将返回一个信号号码;否则,它将返回 0。
一旦线程被添加到睡眠队列中,它就会使用其中一个 sleepq_wait 函数阻塞。根据调用者是否希望使用超时或由捕获的信号或用户线程调度器的中断终止睡眠,有四种等待函数。 sleepq_wait 函数只是等待,直到当前线程被其中一个唤醒函数显式恢复。 sleepq_timedwait 函数等待,直到线程被显式恢复或先前调用 sleepq_set_timeout 设置的超时到期。 sleepq_wait_sig 函数等待,直到线程被显式恢复或其睡眠被终止。 sleepq_timedwait_sig 函数等待,直到线程被显式恢复、先前调用 sleepq_set_timeout 设置的超时到期或线程的睡眠被终止。所有等待函数都接受等待通道作为它们的第一个参数。此外, sleepq_timedwait_sig 函数接受第二个布尔参数,以指示先前调用 sleepq_catch_signals 是否发现挂起的信号。
如果线程被显式恢复或被信号终止,则等待函数返回值为零以表示成功睡眠。如果线程因超时或用户线程调度器的中断而恢复,则返回适当的 errno 值。请注意,由于 sleepq_wait 只能返回 0,因此它不返回任何内容,调用者应假定成功睡眠。此外,如果线程的睡眠超时并同时被终止,则 sleepq_timedwait_sig 将返回一个错误,表示发生了超时。如果返回错误值 0,并且使用了 sleepq_wait_sig 或 sleepq_timedwait_sig 进行阻塞,则应调用 sleepq_calc_signal_retval 函数检查是否有任何挂起的信号,并计算适当的返回值(如果有)。先前调用 sleepq_catch_signals 返回的信号号应作为唯一参数传递给 sleepq_calc_signal_retval 。
被阻塞在等待通道上的线程由 sleepq_broadcast 和 sleepq_signal 函数显式恢复。 这两个函数都接受要恢复线程的等待通道,要提升线程到的优先级以及指示正在恢复哪种类型的睡眠队列的标志参数。 优先级参数被视为最低优先级。 如果正在恢复的线程的优先级(数字较低)已经比优先级参数高,则其优先级不会被调整。 标志参数用于内部断言,以确保睡眠队列不被错误地视为其他类型。 例如,条件变量函数不应该恢复传统睡眠队列上的线程。 sleepq_broadcast 函数恢复所有阻塞在指定等待通道上的线程,而 sleepq_signal 仅恢复阻塞在等待通道上的最高优先级线程。 在调用这些函数之前,应先通过 sleepq_lock 函数锁定睡眠队列链。
睡眠线程可能会被调用 sleepq_abort 函数中断其睡眠。 必须在 sched_lock 保持的情况下调用此函数,并且线程必须排队等候在一个睡眠队列上。 也可以通过 sleepq_remove 函数从特定的睡眠队列中移除线程。 此函数接受线程和等待通道作为参数,并仅在线程位于指定等待通道的睡眠队列上时唤醒线程。 如果线程不在睡眠队列上或者线程位于不同等待通道的睡眠队列上,则此函数不执行任何操作。
与睡眠队列进行比较/对比。
查找/等待/释放。-描述 TDF_TSNOBLOCK 竞争情况。
优先级传播。
我们是否应该要求在调用 mtx_destroy()时拥有互斥锁,因为我们无法安全地断言它们不被其他任何人拥有?
使用临界区…
描述有争议的互斥体竞争
当持有转门链锁时,安全地读取有争议的互斥锁的 mtx_lock 是为什么。
它是做什么的
它是如何工作的
结构 isrc
图 Drivers
我们应该将一个互锁传入 sema_wait 吗?
我们应该有非可休眠 sx 锁吗?
添加有关引用计数正确使用的信息。
如果遵循适当的访问协议,操作是原子的,如果其所有效果对其他 CPU 一起可见。在极端情况下,原子指令直接由机器体系结构提供。在更高的层次上,如果一个结构的几个成员受到锁的保护,那么如果在持有锁的情况下执行所有操作而不在任何操作之间释放锁,则一组操作是原子的。
另请参见操作。
当一个线程在等待锁、资源或条件时,它被阻塞。不幸的是,由于这个原因,这个术语有点多义性。
参见 sleep。
临界区是一段不允许被抢占的代码。使用 critical_enter(9) API 进入和退出临界区。
机器相关。
参见 MI。
内存操作内存操作读取和/或写入内存位置。
机器无关。
参见 MD。
操作请参见内存操作。
主中断上下文主中断上下文是指在中断发生时运行的代码。此代码可以直接运行中断处理程序,也可以调度一个异步中断线程来执行给定中断源的中断处理程序。
实时内核线程高优先级的内核线程。目前,唯一的实时优先级内核线程是中断线程。
另请参见线程。
当线程被阻塞在条件变量或通过 msleep 或 tsleep 阻塞在睡眠队列上时,线程处于睡眠状态。
参见块。
可睡眠锁可睡眠锁是一个可以被处于睡眠状态的线程持有的锁。Lockmgr 锁和 sx 锁目前是 FreeBSD 中唯一的可睡眠锁。最终,一些 sx 锁,如 allproc 和 proctree 锁,可能会变成不可睡眠锁。
参见 sleep。
线程 A 内核线程由结构线程表示。线程拥有锁并持有单个执行上下文。
等待通道一个线程可以在其上休眠的内核虚拟地址。
[1] Marshall Kirk McKusick, Keith Bostic, Michael J Karels, and John S Quarterman. Copyright © 1996 Addison-Wesley Publishing Company, Inc.. 0-201-54979-4. Addison-Wesley Publishing Company, Inc.. The Design and Implementation of the 4.4 BSD Operating System. 1-2.
本章将讨论 FreeBSD 为 PC 卡或 CardBus 设备编写设备驱动程序的机制。然而,目前它只是记录了如何向现有的 pccard 驱动程序添加新设备。
设备驱动程序知道它们支持哪些设备。内核中有一个支持设备的表,驱动程序使用它来连接到设备。
PC 卡可以通过两种方式进行识别,都基于存储在卡上的卡信息结构(CIS)。第一种方法是使用数字制造商和产品编号。第二种方法是使用包含在 CIS 中的可读字符串。PC 卡总线使用集中式数据库和一些宏来促进一种设计模式,以帮助驱动程序编写者将设备与其驱动程序匹配。
原始设备制造商(OEM)经常为 PC 卡产品开发参考设计,然后将该设计出售给其他公司进行营销。这些公司改进设计,将产品推向目标受众或地理区域,并在卡上放上自己的商标。对物理卡的改进通常非常微小,如果有任何更改的话。为了加强其品牌,这些供应商在 CIS 空间的可读字符串中放置其公司名称,但保持制造商和产品 ID 不变。
由于这种做法,FreeBSD 驱动程序通常依赖于设备识别的数字 ID。使用数字 ID 和一个集中的数据库会使系统添加 ID 和对卡的支持变得复杂。必须仔细检查看看到底谁真正制造了这张卡片,特别是当制造这张卡片的厂商在中央数据库中可能已经有不同的制造商 ID 的情况下。Linksys、D-Link 和 NetGear 是一些美国的 LAN 硬件制造商,它们经常销售相同的设计。这些相同的设计在日本可能以 Buffalo 和 Corega 等名称销售。通常,这些设备都会拥有相同的制造商和产品 ID。
PC Card 总线代码保留了卡信息的中央数据库,但未保留与之关联的驱动程序在/sys/dev/pccard/pccarddevs 中。它还提供了一组宏,允许用户轻松构建驱动程序用来索赔设备的表中的简单条目。
最后,一些真正低端的设备根本没有制造商标识。这些设备必须通过匹配可读的 CIS 字符串来检测。虽然如果我们不需要这种方法作为后备是很好的,但对一些非常低端的 CD-ROM 播放器和以太网卡来说是必要的。这种方法通常应该避免使用,但由于在识别 PC Card 业务的 OEM 性质之前添加了许多设备,这里列出了一些设备。在添加新设备时,请优先使用数字方法。
pccarddevs 文件中有四个部分。第一部分列出了使用厂商编号的供应商。这一部分按数字顺序排序。接下来的部分包含所有这些供应商使用的产品,以及它们的产品 ID 号和描述字符串。描述字符串通常不使用(而是基于可读的 CIS 设置设备的描述,即使我们匹配了数字版本)。然后为使用字符串匹配方法的设备重复这两个部分。最后,文件中允许在任何位置使用 / ** 和 ** / 字符括起的 C 样式注释。
文件的第一部分包含供应商 ID。请将此列表按数字顺序排序。此外,请协调对此文件的更改,因为我们与 NetBSD 共享此信息以帮助促进这一信息的共同清理。例如,这里有前几个供应商 ID:
很有可能, NETGEAR_2 这个条目实际上是 NETGEAR 从 OEM 那里购买的卡,支持这些卡的作者当时可能不知道 Netgear 正在使用别人的 ID。这些条目相当直观。厂商关键字表示这是什么类型的行,接着是厂商的名称。这个名称将在后面的 pccarddevs 中重复出现,并且被用在驱动程序的匹配表中,所以保持它简短且是有效的 C 标识符。十六进制的数字 ID 标识制造商。不要添加类似 0xffffffff 或 0xffff 这样的 ID,因为这些是保留的 ID(前者是 "没有设置 ID",而后者有时出现在质量极差的卡中,试图表示 "无")。最后是制作这张卡的公司的描述字符串。这个字符串在 FreeBSD 中除了用于评论目的外并没有其他用途。
文件的第二部分包含了产品。正如本例所示,格式类似于厂商行:
第 product 个关键字后面跟着上面重复的厂商名称。然后是产品名称,驱动程序使用,应该是一个有效的 C 标识符,但也可以以数字开头。与供应商一样,该卡的十六进制产品 ID 遵循相同的约定 0xffffffff 和 0xffff 。最后是设备本身的字符串描述。这个字符串通常在 FreeBSD 中不被使用,因为 FreeBSD 的 pccard 总线驱动程序将从可人类阅读的 CIS 条目构建一个字符串,但在某些极其不足的情况下可以使用。产品按照制造商的字母顺序,然后按产品 ID 的数字顺序排列。在每个制造商的条目之前有一个 C 注释,并在条目之间有一个空行。
第三部分类似于前一个供应商部分,但所有制造商的数字 ID 都设置为 -1 ,意思是在 FreeBSD pccard 总线代码中"匹配任何找到的"。由于这些是 C 标识符,它们的名称必须是唯一的。否则,格式与文件的第一部分相同。
最后一部分包含必须通过字符串条目标识的卡的条目。此部分的格式与通用部分略有不同:
熟悉的 product 关键字后面跟着供应商名称和卡名称,就像文件的第二部分一样。在这里,格式与先前使用的格式有所偏差。出现了{}分组,后面跟着一些字符串。这些字符串对应于在 CIS_INFO 元组中定义的供应商、产品和附加信息。这些字符串由生成 pccarddevs.h 的程序过滤,以将&sp 替换为真实空格。NULL 字符串意味着应忽略条目的相应部分。此处显示的示例包含错误条目。它不应包含版本号,除非对卡的操作至关重要。有时供应商将在字段中具有许多不同版本的卡,所有这些都有效,在这种情况下,该信息只会使使用类似卡的人更难以在 FreeBSD 上使用它。有时候,当供应商希望在相同品牌下销售许多不同的零件时,由于市场考虑(供货能力、价格等),对于将卡消除歧义则是至关重要的。现在不提供正则表达式匹配。
要了解如何将设备添加到受支持设备列表中,必须了解许多驱动程序具有的探测和/或匹配例程。在 FreeBSD 5.x 中,由于还存在与 OLDCARD 的兼容性层,这变得有点复杂。由于只是装饰有所不同,这里将呈现一个理想化的版本。
这里有一个简单的 pccard 探测例程,匹配几个设备。如上所述,名称可能会有所不同(如果不是 foo_pccard_probe() ,那么将是 foo_pccard_match() )。函数 pccard_product_lookup() 是一个通用函数,它遍历表并返回与之匹配的第一个条目的指针。一些驱动程序可能使用此机制向其余驱动程序传递有关某些卡的附加信息,因此表中可能会有一些变化。唯一的要求是表的每一行的第一个元素必须是 struct pccard_product 。
观察表格 wi_pccard_products ,人们会注意到所有条目的形式都是 PCMCIA_CARD(foo, bar, baz) 。foo 部分是来自 pccarddevs 的制造商 ID。bar 部分是产品 ID。baz 是此卡的预期功能号。许多 PCCards 可以具有多个功能,并且需要某种方法来区分功能 1 和功能 0。您可能看到 PCMCIA_CARD_D ,其中包括来自 pccarddevs 的设备描述。您还可能看到 PCMCIA_CARD2 和 PCMCIA_CARD2_D ,在需要匹配 CIS 字符串和制造商编号时使用,分别是“使用默认描述”和“从 pccarddevs 获取描述”。
要添加新设备,必须首先从设备获取标识信息。这样做的最简单方法是将设备插入 PC 卡或 CF 槽并发出 devinfo -v 。示例输出:
manufacturer 和 product 是该产品的数字 ID,而 cisvendor 和 cisproduct 是来自 CIS 的产品描述字符串。
由于我们首先希望优先选择数字选项,因此请首先尝试基于该选项构建条目。上面的卡片为本示例略作虚构。供应商是 BUFFALO,我们看到已经有一个条目:
但没有这张特定卡片的条目。相反,我们发现:
要添加设备,我们只需将此条目添加到 pccarddevs 中:
完成这些步骤后,可以将该卡添加到驱动程序中。这只是简单地添加一行:
请注意,在我添加的行之前的行中我包含了一个“+”,但那只是为了突出显示该行。不要将其添加到实际驱动程序中。添加了该行后,您可以重新编译您的内核或模块并进行测试。如果设备被识别并正常工作,请提交补丁。如果不起作用,请找出需要做什么使其正常工作并提交补丁。如果设备根本没有被识别,那么您做错了什么,应该重新检查每一步。
如果您是一个 FreeBSD src 提交者,并且一切正常,那么您可以将更改提交到树中。但是,有一些小技巧需要考虑。首先必须将 pccarddevs 提交到树中。然后,必须重新生成 pccarddevs.h 并作为第二步提交,确保后一个文件中有正确的$FreeBSD$标记。最后,提交对驱动程序的添加。
请不要直接向作者发送新设备的条目。请将它们作为 PR 提交,并将 PR 编号发送给作者以备记录。这样可以确保条目不会丢失。提交 PR 时,无需在补丁中包含 pccardevs.h 的差异,因为这些将被重新生成。需要包括设备描述以及客户端驱动程序的补丁。如果您不知道名称,请使用 OEM99 作为名称,作者将在调查后相应调整 OEM99。提交者不应提交 OEM99,而应找到最高的 OEM 条目并提交比那更高一个。
FreeBSD 声音子系统清晰地将通用声音处理问题与特定设备问题分开。这使得为新硬件添加支持变得更容易。
pcm(4)框架是声音子系统的核心部分。它主要实现以下元素:
用于数字化声音和混音功能的系统调用接口(读取、写入、ioctls)。ioctl 命令集兼容传统的 OSS 或 Voxware 接口,允许常见多媒体应用程序在无需修改的情况下移植。
处理声音数据的通用代码(格式转换、虚拟通道)。
用于硬件特定音频接口模块的统一软件接口。
为一些常见硬件接口(ac97)提供额外支持,或者共享硬件特定代码(例如:ISA DMA 例程)。
特定声卡的支持是通过硬件特定驱动程序实现的,这些驱动程序提供通道和混音接口,以便插入通用 pcm 代码。
在本章中,pcm 一词将指代声卡驱动程序的中心通用部分,而不是硬件特定模块。
潜在的驱动程序编写人员当然希望从现有模块开始,并将代码用作最终参考。但是,尽管良好的代码很好而且干净,但它也大多没有注释。本文试图概述框架接口,并回答在调整现有代码时可能出现的一些问题。
作为另一种选择,或者除了从现有工作示例开始外,您也可以在 https://people.FreeBSD.org/~cg/template.c 找到一个有注释的驱动程序模板
所有相关的代码都位于 /usr/src/sys/dev/sound/ 中,除了公共的 ioctl 接口定义,可以在 /usr/src/sys/sys/soundcard.h 中找到。
在 /usr/src/sys/dev/sound/ 中,pcm/ 目录包含了核心代码,而 pci/、isa/ 和 usb/ 目录包含了用于 PCI 和 ISA 板卡以及 USB 音频设备的驱动程序。
音频驱动程序的探测和附加方式几乎与任何硬件驱动程序模块相同。您可能希望查看手册中的 ISA 或 PCI 特定部分以获取更多信息。
然而,音频驱动程序在某些方面有所不同:
它们将自己声明为 pcm 类设备,带有一个 struct snddev_info 设备私有结构:
大多数声卡驱动程序需要存储有关其设备的其他私有信息。通常在附加例程中分配私有数据结构。其地址通过调用 pcm_register() 和 mixer_init() 传递给 pcm。稍后,pcm 会在调用声卡接口时将该地址作为参数传回。
声卡驱动程序附加例程应通过调用 mixer_init() 声明其 MIXER 或 AC97 接口到 pcm。对于 MIXER 接口,这将依次调用 xxxmixer_init() 。
声卡驱动程序附加例程通过调用 pcm_register(dev, sc, nplay, nrec) 声明其通道配置到 pcm,其中 sc 是设备数据结构的地址,在 pcm 的进一步调用中使用,而 nplay 和 nrec 是播放和记录通道的数量。
音频驱动程序附加例程通过调用 pcm_addchan() 声明其每个通道对象。这将在 pcm 中设置通道粘合剂,并依次调用 xxxchannel_init() 。
在释放资源之前,音频驱动程序分离例程应调用 pcm_unregister() 。
处理非 PnP 设备有两种可能的方法:
使用 device_identify() 方法(示例:sound/isa/es1888.c)。 device_identify() 方法会在已知地址处探测硬件,如果找到支持的设备,则创建一个新的 pcm 设备,然后传递给探测/附加。
使用定制内核配置,并为 pcm 设备提供适当的提示(示例:sound/isa/mss.c)。
pcm 驱动程序应实现 device_suspend , device_resume 和 device_shutdown 例程,以便正确执行电源管理和模块卸载功能。
PCM 核心和声音驱动程序之间的接口是以内核对象的形式定义的。
通常,声音驱动程序提供两种主要接口:通道和混音器或 AC97。
AC97 接口是由具有 AC97 编解码器的硬件驱动程序实现的非常小的硬件访问(寄存器读/写)接口。在这种情况下,实际的 MIXER 接口由 pcm 中的共享 AC97 代码提供。
声音驱动程序通常具有一个私有数据结构来描述其设备,并为其支持的每个播放和记录数据通道提供一个结构。
对于所有 CHANNEL 接口函数,第一个参数是一个不透明指针。
第二个参数是指向私有通道数据结构的指针,除了 channel_init() ,它具有指向私有设备结构的指针(并返回通道指针以供 pcm 进一步使用)。
为了实现音频数据传输,pcm 核心和音频驱动程序通过一个共享内存区域进行通信,该区域由 struct snd_dbuf 描述。
struct snd_dbuf 对 pcm 是私有的,音频驱动程序通过调用访问器函数( sndbuf_getxxx() )获取感兴趣的值。
共享内存区的大小为 sndbuf_getsize() ,分为大小为 sndbuf_getblksz() 字节的固定大小块。
在播放时,一般的传输机制如下(录制的想法相反):
pcm 最初填充缓冲区,然后调用声卡驱动程序的 xxxchannel_trigger() 函数,参数为 PCMTRIG_START。
声音驱动程序然后安排重复地将整个内存区域 ( sndbuf_getbuf() , sndbuf_getsize() ) 传输到设备,以 sndbuf_getblksz() 字节的块的形式。它会为每个传输的块回调 chn_intr() pcm 函数(这通常会发生在中断时期)。
chn_intr() 安排将新数据复制到已传输到设备的区域(现在空闲),并对 snd_dbuf 结构进行适当更新。
xxxchannel_init() 被调用来初始化每个播放或录制通道。这些调用是从声音驱动程序附加例程启动的。(请参阅探测和附加部分)。
xxxchannel_setformat() 应该为指定的声道设置指定的声音格式。
xxxchannel_setspeed() 设置指定采样速度的通道硬件,并返回可能调整后的速度。
xxxchannel_setblocksize() 设置块大小,这是 pcm 和声卡之间、声卡和设备之间单位事务的大小。通常,这将是在发生中断之前传输的字节数。在传输过程中,声卡应该每次传输这个大小时调用 pcm 的 chn_intr() 。
大多数声卡驱动程序只在这里注意块大小,在实际开始传输时使用。
xxxchannel_trigger()
is called by pcm to control data transfer operations in the driver.
xxxchannel_getptr() 返回传输缓冲区中的当前偏移量。通常会被 chn_intr() 调用,这是 pcm 知道它可以传输新数据的位置。
当驱动程序被卸载时, xxxchannel_free() 被调用来释放通道资源,如果通道数据结构是动态分配的或者未使用 sndbuf_alloc() 进行缓冲区分配,则应该实现它。
channel_reset() , channel_resetdone() 和 channel_notify() 用于特殊目的,不应在未经讨论的情况下在 FreeBSD 多媒体邮件列表上实现驱动程序中。
channel_setdir() 已过时。
xxxmixer_init() 初始化硬件并告诉 pcm 可用于播放和录音的混音设备
混音器位定义可以在 soundcard.h 中找到( SOUND_MASK_XXX 值和 SOUND_MIXER_XXX 位移)。
xxxmixer_set() 设置一个混音设备的音量级别。
xxxmixer_setrecsrc() 设置录音源设备。
xxxmixer_uninit() 应确保所有声音静音,如有可能,混音器硬件应关闭电源。
xxxmixer_reinit() 应确保混音器硬件已上电,并恢复任何非由 mixer_set() 或 mixer_setrecsrc() 控制的设置。
AC97 接口由带有 AC97 编解码器的驱动程序实现。它只有三种方法:
xxxac97_init() 返回找到的 ac97 编解码器数量。
ac97_read() 和 ac97_write() 读取或写入指定的寄存器。
AC97 接口由 pcm 中的 AC97 代码用于执行更高级别的操作。 查看 sound/pci/maestro3.c 或其他许多 sound/pci/下的示例。
该函数应返回一个指向用于控制此通道的私有区域的指针。这将作为参数传递给其他通道接口调用。
go
defines the action for the current call. The possible values are:
由于硬件级别可能不会与输入比例匹配,而且会发生一些四舍五入,所以该例程返回实际级别值(范围为 0-100),如所示。
返回设置为录音的实际设备。一些驱动程序只能设置一个录音设备。如果发生错误,函数应返回-1。
本文档是由 FreeBSD 项目的 Chris Costello 在 Safeport Network Services 和 Network Associates Laboratories 开发的,Network Associates, Inc.的安全研究部门在 DARPA/SPAWAR 合同 N66001-01-C-8035(“CBOSS”)下开发,作为 DARPA CHATS 研究计划的一部分。
在源代码(SGML DocBook)和“编译”形式(SGML,HTML,PDF,PostScript,RTF 等)中重新分发和使用,无论是否经过修改,只要满足以下条件:
源代码( SGML DocBook)的再分发必须保留本文件中的版权声明、这份条件列表以及以下免责声明作为本文件的第一行,不得进行修改。
在编译形式中(转换为其他 DTD、转换为 PDF、PostScript、RTF 和其他格式),必须在文档和/或其他随附分发的材料中复制上述版权声明、此条件列表以及以下免责声明。
FreeBSD 包括对几种强制访问控制策略的实验性支持,以及一个用于内核安全可扩展性的框架,即 TrustedBSD MAC 框架。MAC 框架是一个可插拔的访问控制框架,允许轻松地将新的安全策略链接到内核中,在启动时加载,或在运行时动态加载。该框架提供了各种功能,使实现新的安全策略变得更加容易,包括轻松地将安全标签(如机密信息)附加到系统对象上的能力。
本章介绍了 MAC 策略框架,并为一个示例 MAC 策略模块提供了文档。
TrustedBSD MAC 框架提供了一种机制,允许在编译时或运行时扩展内核访问控制模型。 新的系统策略可以作为内核模块实现,并链接到内核; 如果存在多个策略模块,则它们的结果将被组合。 MAC 框架为协助策略编写人员提供各种访问控制基础设施服务,包括支持瞬态和持久的与策略无关的对象安全标签。 目前,此支持被视为实验性的。
本章提供了适用于策略模块开发者以及潜在的 MAC 启用环境消费者的信息,了解 MAC 框架如何支持内核访问控制扩展。
强制访问控制(MAC)是指操作系统对用户强制执行的一组访问控制策略。MAC 策略可以与用户自主保护对象的自主访问控制(DAC)保护形成对比。在传统的 UNIX 系统中,DAC 保护包括文件权限和访问控制列表;MAC 保护包括防止用户间调试和防火墙的进程控制。操作系统设计者和安全研究人员制定了各种 MAC 策略,包括多级安全(MLS)保密策略,Biba 完整性策略,基于角色的访问控制(RBAC),域和类型强制(DTE),以及类型强制(TE)。每个模型都会基于各种因素做出决策,包括用户身份,角色和安全许可,以及表示数据敏感性和完整性等概念的对象的安全标签。
TrustedBSD MAC 框架能够支持实现所有这些策略的策略模块,以及一类广泛的系统加固策略,这些策略可能使用现有的安全属性,如用户和组 ID,以及文件的扩展属性和其他系统属性。此外,尽管名字叫做 MAC 框架,但策略模块有很大的灵活性,可以实现纯自主策略的授权保护。
TrustedBSD MAC 框架允许内核模块扩展操作系统安全策略,同时提供许多访问控制模块所需的基础功能。如果同时加载多个策略,则 MAC 框架将有用地(某个定义来说)组合这些策略的结果。
MAC 框架包含许多内核元素:
框架管理接口
并发和同步原语。
策略注册
内核对象的可扩展安全标签
策略入口点组合运算符
标签管理基元
内核服务调用的入口点 API
策略模块的入口点 API
入口点实现(策略生命周期,对象生命周期/标签管理,访问控制检查)。
与策略无关的标签管理系统调用
mac_syscall() 多路复用系统调用
实现为 MAC 策略模块的各种安全策略
TrustedBSD MAC 框架可以直接使用 sysctl、加载器可调参数和系统调用进行管理。
在大多数情况下,相同名称的 sysctl 和加载器可调整相同的参数,并控制与各种内核子系统相关的保护执行等行为。此外,如果 MAC 调试支持编译到内核中,将维护多个计数器以跟踪标签分配。通常建议不要使用每个子系统执行控件来控制生产环境中的策略行为,因为它们会广泛影响所有活动策略的操作。相反,应优先使用每个策略控件,因为它们提供了更大的粒度和更大的操作一致性,适用于策略模块。
使用系统模块管理系统调用和其他系统接口,包括引导加载程序变量,执行策略模块的加载和卸载;策略模块将有机会影响加载和卸载事件,包括阻止不希望的策略卸载。
由于活动策略集可能在运行时发生变化,并且入口点的调用是非原子的,因此需要同步以防止在入口点调用正在进行时加载或卸载策略,冻结活动策略集以防止加载或卸载策略,直到入口点调用完成。这是通过一个框架繁忙计数来实现的:每当进入一个入口点时,繁忙计数会递增;每当退出时,繁忙计数会递减。在繁忙计数升高时,不允许策略列表更改,并且试图修改策略列表的线程将休眠,直到列表不再繁忙。繁忙计数受互斥锁保护,并且条件变量用于唤醒等待策略列表修改的线程。这种同步模型的一个副作用是允许从策略模块内部递归进入 MAC 框架,尽管通常不会使用。
采用各种优化措施来减少繁忙计数的开销,包括避免在列表为空或仅包含静态条目(在系统启动前加载的策略,无法卸载)时增加和减少的全部成本。还提供了一个编译时选项,可以防止在运行时更改加载的策略集,从而消除了与支持动态加载和卸载策略相关的互斥锁定成本,因为不再需要同步。
由于 MAC 框架在某些入口点不允许阻塞,因此不能使用普通的睡眠锁;因此,尝试加载或卸载可能会阻塞相当长的时间,等待框架变得空闲。
由于感兴趣的内核对象通常可以同时从多个线程访问,并且允许多个线程同时进入 MAC 框架,因此 MAC 框架维护的安全属性存储被仔细同步。一般情况下,现有的内核对象数据上的内核同步用于保护对象上的 MAC 框架安全标签:例如,套接字上的 MAC 标签使用现有的套接字互斥锁进行保护。同样,对于并发访问的语义通常与容器对象的语义相同:对于凭据,与凭据结构的其余部分一样,标签内容保持写时复制语义。当使用对象引用调用 MAC 框架时,MAC 框架会对对象施加必要的锁定。策略作者必须了解这些同步语义,因为它们有时会限制对标签允许的访问类型:例如,当只读引用传递给策略的凭据时,只允许对附加到凭据的标签状态进行读取操作。
策略模块必须编写,以假定许多内核线程可能同时进入一个或多个策略入口点,这是因为 FreeBSD 内核的并行和抢占性质。如果策略模块使用可变状态,这可能需要在策略内部使用同步原语,以防止对该状态产生不一致的视图,导致策略操作不正确。策略通常可以利用现有的 FreeBSD 同步原语来实现这一目的,包括互斥锁、睡眠锁、条件变量和计数信号量。但是,策略应该小心编写这些原语,尊重现有的内核锁顺序,并认识到一些入口点不允许休眠,限制这些入口点中原语的使用到互斥锁和唤醒操作。
当策略模块调用其他内核子系统时,它们通常需要释放任何策略锁,以避免违反内核锁顺序或冒险锁递归。这将保持策略锁作为全局锁顺序中的叶锁,有助于避免死锁。
MAC 框架维护两个活动策略列表:一个静态列表和一个动态列表。这两个列表在锁定语义上略有不同:使用静态列表不需要提升参考计数。当加载包含 MAC 框架策略的内核模块时,策略模块将使用 SYSINIT 调用注册函数;当卸载策略模块时, SYSINIT 同样会调用注销函数。如果策略模块加载超过一次,注册可能会失败,如果注册所需资源不足(例如,策略可能需要标记,但可用的标记状态不足),或者其他策略前提条件可能无法满足(某些策略可能仅能在引导之前加载)。同样,如果一个策略被标记为不可卸载,注销可能会失败。
内核服务以两种方式与 MAC 框架交互:它们调用一系列 API 来通知框架相关事件,并在安全相关对象中提供一个策略不可知的标签结构指针。标签指针通过标签管理入口点由 MAC 框架维护,并允许框架通过对维护对象的内核子系统进行相对非侵入性的更改为策略模块提供标记服务。例如,标签指针已经添加到进程、进程凭据、套接字、管道、vnodes、Mbufs、网络接口、IP 重组队列以及各种其他安全相关结构中。内核服务还会在执行重要安全决策时调用 MAC 框架,允许策略模块基于自己的标准(可能包括存储在安全标签中的数据)增强这些决策。这些安全关键决策大多是显式访问控制检查;然而,某些影响更广泛决策函数的功能,例如套接字的数据包匹配和程序执行时的标签转换。
当有多个策略模块同时加载到内核中时,策略模块的结果将由框架使用组合运算符组合。该运算符目前是硬编码的,要求所有活动策略必须批准请求才能返回成功。由于策略可能返回各种错误条件(成功、访问被拒绝、对象不存在等),一个优先级运算符从策略返回的错误集合中选择结果错误。一般来说,指示对象不存在的错误会优先于指示对象访问被拒绝的错误。虽然不能保证结果的组合会是有用或安全的,但我们发现对于许多有用的策略选择而言,确实如此。例如,传统的受信任系统通常会附带两个或更多使用类似组合的策略。
由于许多有趣的访问控制扩展依赖于对象上的安全标签,因此 MAC 框架提供了一套基于策略的标签管理系统调用,涵盖各种用户可见对象。常见的标签类型包括分区标识符、敏感性标签、完整性标签、隔间、域、角色和类型。所谓策略不可知是指策略模块能够完全定义与对象关联的元数据的语义。策略模块参与提供给用户应用程序的基于字符串的标签的内部化和外部化,并且如果需要,可以向应用程序公开多个标签元素。
内存中的标签存储在分配的 slab 中,其中包含一个固定长度的联合数组,每个联合数组包含一个指针和一个。注册标签存储的策略将被分配一个“槽”标识符,该标识符可用于取消引用标签存储。存储的语义完全由策略模块决定:模块提供了与内核对象生命周期相关联的各种入口点,包括初始化、关联/创建和销毁。使用这些接口,可以实现引用计数和其他存储模型。通常策略模块不需要直接访问对象结构以检索标签,因为 MAC 框架通常会将对象指针和对象标签的直接指针传递给入口点。这一规则的主要例外是进程凭据,必须手动取消引用才能访问凭据标签。这在 MAC 框架的将来版本中可能会改变。
初始化条目点经常包括一个睡眠倾向标志,指示初始化是否允许睡眠;如果不允许睡眠,则可能返回失败以取消标签(因此对象)的分配。例如,在中断处理期间的网络堆栈中,不允许睡眠,或者在调用者持有互斥锁时可能发生这种情况。由于在飞行中网络数据包(Mbufs)上维护标签的性能成本,策略必须明确声明需要分配 Mbuf 标签。动态加载的策略使用标签时必须能够处理其初始化函数未在对象上调用的情况,因为在加载策略时对象可能已经存在。MAC 框架保证未初始化的标签槽将保存 0 或 NULL 值,策略可以使用这些值来检测未初始化的值。但是,由于 Mbuf 标签的分配是有条件的,策略还必须能够处理 Mbuf 的 NULL 标签指针,如果它们已经被动态加载。
对于文件系统标签,提供了特殊支持,用于在扩展属性中持久存储安全标签。在可用的情况下,扩展属性事务用于允许对 vnodes 上的安全标签进行一致的复合更新 — 目前此支持仅存在于 UFS2 文件系统中。策略作者可以选择使用一个(或多个)扩展属性来实现多标签文件系统对象标签。出于效率原因,vnode 标签( v_label )是任何磁盘上标签的缓存;策略能够在实例化 vnode 时将值加载到缓存中,并根据需要更新缓存。因此,在每次访问控制检查时,不需要直接访问扩展属性。
MAC 框架实现了许多系统调用:其中大多数调用支持面向策略的标签检索和操作 API,这些 API 暴露给用户应用程序。
标签管理调用接受一个标签描述结构, struct mac ,其中包含一系列 MAC 标签元素。每个元素包含一个字符串名称和字符串值。每个策略将有机会声明特定元素名称,允许策略在需要时公开多个独立元素。策略模块通过入口点在内核标签和用户提供的标签之间执行内部化和外部化,允许多种语义。标签管理系统调用通常由用户库函数包装,以执行内存分配和错误处理,简化必须管理标签的用户应用程序。
FreeBSD 内核中存在以下与 MAC 相关的系统调用:
mac_get_proc() 可用于检索当前进程的标签。
mac_set_proc() 可用于请求更改当前进程的标签。
mac_get_fd() 可用于检索由文件描述符引用的对象(文件、套接字、管道等)的标签。
mac_get_file() 可用于检索由文件系统路径引用的对象的标签。
mac_set_fd() 可用于请求更改由文件描述符引用的对象(文件,套接字,管道等)的标签。
mac_set_file() 可用于请求更改由文件系统路径引用的对象的标签。
mac_syscall() 允许策略模块创建新的系统调用,而无需修改系统调用表; 它接受目标策略名称,操作编号和策略使用的不透明参数。
mac_get_pid() 可用于通过进程 id 请求另一个进程的标签。
mac_get_link() 与 mac_get_file() 相同,只是如果它是路径中的最后一个条目,则不会跟随符号链接,因此可用于检索符号链接上的标签。
mac_set_link() 与 mac_set_file() 相同,只是如果它是路径中的最后一个条目,则不会跟随符号链接,因此可用于操作符号链接上的标签。
mac_execve() 等同于 execve() 系统调用,只是它还接受一个请求的标签,在开始执行新程序时将进程标签设置为该标签。执行时标签的变化称为"过渡"。
mac_get_peer() 实际上是通过套接字选项实现的,如果可用,它检索套接字上远程对等体的标签。
除了这些系统调用外, SIOCSIGMAC 和 SIOCSIFMAC 网络接口 ioctls 允许检索和设置网络接口上的标签。
安全策略要么直接链接到内核,要么编译为可在启动时加载的可加载内核模块,要么在运行时使用模块加载系统调用动态加载。策略模块通过一组声明的入口点与系统交互,提供对系统事件流的访问,并允许策略影响访问控制决策。每个策略包含若干元素:
策略的可选配置参数。
策略逻辑和参数的集中实现。
可选实现策略生命周期事件,如初始化和销毁。
可选支持在选定的内核对象上初始化、维护和销毁标签。
为用户进程检查和修改选定对象的标签提供可选支持。
实现与策略相关的选定访问控制入口点。
声明策略身份、模块入口点和策略属性。
模块可以使用 MAC_POLICY_SET() 宏进行声明,该宏命名策略,提供对 MAC 入口点向量的引用,提供确定策略框架应如何处理策略的加载时标志,并可选择请求框架分配标签状态。
MAC 策略入口点向量,在本示例中为 macpolicyops ,将模块中定义的函数与特定入口点关联起来。可在 MAC 入口点参考部分找到所有可用入口点及其原型的完整列表。在模块注册期间特别感兴趣的是.mpo_destroy 和.mpo_init 入口点。.mpo_init 将在成功向模块框架注册策略但在任何其他入口点变为活动之前调用。这允许策略执行任何特定于策略的分配和初始化,例如初始化任何数据或锁。.mpo_destroy 将在卸载策略模块时调用,以允许释放任何已分配的内存和销毁锁。目前,这两个入口点在保持 MAC 策略列表互斥锁的情况下被调用,以防止调用任何其他入口点:这将被更改,但同时,策略应注意调用哪些内核原语,以避免锁定顺序或睡眠问题。
政策声明的模块名称字段存在是为了可以唯一标识模块的依赖关系。应选择一个适当的字符串。政策的完整字符串名称在加载和卸载事件期间通过内核日志向用户显示,并在向用户态进程提供状态信息时导出。
政策声明的标志字段允许模块在模块加载时向框架提供关于其能力的信息。目前定义了三个标志:
此标志表示策略模块可以被卸载。如果没有提供此标志,则策略框架将拒绝卸载模块的请求。这个标志可能被那些在运行时无法释放分配的标签状态的模块使用。
此标志表示策略模块必须在引导过程的早期被加载和初始化。如果指定了该标志,将拒绝在引导后尝试注册模块。这个标志可能被需要对所有系统对象进行普遍标记的策略使用,且不能处理未经正确初始化的对象。
此标志表示策略模块需要对 Mbuf 进行标记,并且内存应始终为 Mbuf 标签的存储分配。默认情况下,MAC 框架不会为 Mbuf 分配标签存储,除非至少加载了一个带有此标志设置的策略。当策略不需要对 Mbuf 进行标记时,这显着提高了网络性能。存在一个内核选项, MAC_ALWAYS_LABEL_MBUF ,用于强制 MAC 框架分配 Mbuf 标签存储,而不考虑此标志的设置,并且在某些环境中可能很有用。
提供了四类与框架注册的策略相关的入口点:与策略的注册和管理相关的入口点,表示内核对象的初始化、创建、销毁和其他生命周期事件的入口点,与策略模块可能影响的访问控制决策相关的事件,以及涉及对象标签管理的调用。此外,还提供了一个 mac_syscall() 入口点,以便策略可以在不注册新系统调用的情况下扩展内核接口。
策略模块编写者应了解内核锁定策略,以及在哪些入口点可以使用哪些对象锁。编写者应尽量避免在入口点内获取非叶锁,同时遵循对象访问和修改的锁定协议以避免死锁情况。特别是,编写者应注意,虽然通常会持有访问对象及其标签所需的锁,但并非所有入口点都具有修改对象或其标签所需的足够锁。参数的锁定信息已记录在 MAC 框架入口点文档中。
策略入口点将传递对象标签的引用以及对象本身。这使得带标签的策略可以不了解对象的内部情况,但仍可根据标签做出决策。唯一的例外是进程凭据,策略假定内核中的策略将其视为第一类安全对象。
mpo_init
conf
MAC 策略定义
策略加载事件。策略列表互斥体被锁定,因此不能执行休眠操作,对其他内核子系统的调用必须谨慎进行。如果在策略初始化期间需要可能会睡眠的内存分配,应使用单独的模块 SYSINIT()进行。
mpo_destroy
conf
MAC 政策定义
策略加载事件。策略列表互斥锁被持有,因此应该谨慎。
mpo_syscall
td
调用线程
call
特定策略系统调用号
arg
系统调用参数指针
此入口点提供策略多路复用系统调用,以便策略可以为用户进程提供附加服务,而无需注册特定系统调用。在注册期间提供的策略名称用于从用户态解多路复用调用,并将参数转发到此入口点。在实现新服务时,安全模块应确保根据需要从 MAC 框架调用适当的访问控制检查。例如,如果策略实现了增强信号功能,应调用必要的信号访问控制检查以调用 MAC 框架和其他已注册策略。
mpo_thread_userret
td
返回线程
此入口点允许策略模块在线程通过系统调用返回、陷阱返回或其他方式返回用户空间时执行与 MAC 相关的事件。对于具有浮动进程标签的策略而言,这是必需的,因为在系统调用处理过程中的任意点获取进程锁并不总是可能的;进程标签可能代表传统认证数据、进程历史信息或其他数据。要使用此机制,对进程凭证标签的预期更改可能存储在由每个策略自旋锁保护的 p_label 中,然后设置每个线程的 TDF_ASTPENDING 标志和每个进程的 PS_MACPENDM 标志以安排调用 userret 入口点。从此入口点,策略可以更轻松地创建替换凭证,而不必过多考虑锁定上下文。策略编写者应当注意,与调度 AST 和执行 AST 相关的事件顺序可能在多线程应用程序中变得复杂且交织在一起。
mpo_init_bpfdesc_label
label
应用新标签
在新实例化的 bpfdesc(BPF 描述符)上初始化标签。允许睡眠。
mpo_init_cred_label
label
初始化新标签
为新实例化的用户凭据初始化标签。允许睡眠。
mpo_init_devfsdirent_label
label
要应用的新标签
初始化新实例化的 devfs 条目上的标签。允许休眠。
mpo_init_ifnet_label
label
新标签应用
在新实例化的网络接口上初始化标签。允许睡眠。
mpo_init_ipq_label
label
申请新标签
flag
睡眠/非睡眠 malloc(9);见下文
在新实例化的 IP 片段重组队列上初始化标签。 flag 字段可以是 M_WAITOK 和 M_NOWAIT 之一,并应在此初始化调用期间避免执行睡眠 malloc(9)。IP 片段重组队列分配经常发生在性能敏感的环境中,实现应谨慎避免睡眠或长时间运行的操作。此入口点允许失败,导致无法分配 IP 片段重组队列。
mpo_init_mbuf_label
flag
睡眠/非睡眠 malloc(9); 请参见下文
label
初始化策略标签
在新实例化的 mbuf 数据包头( mbuf )上初始化标签。 flag 字段可以是 M_WAITOK 和 M_NOWAIT 之一,并且应该在此初始化调用期间避免执行睡眠 malloc(9)。mbuf 分配经常发生在性能敏感的环境中,实现应小心避免睡眠或长时间运行的操作。允许此入口点失败,导致无法分配 mbuf 头。
mpo_init_mount_label
mntlabel
创建要为挂载点本身初始化的策略标签
fslabel
创建要为文件系统初始化的策略标签
在新实例化的挂载点上初始化标签。允许睡眠。
mpo_init_mount_fs_label
label
初始化标签
在新挂载的文件系统上初始化标签。允许睡眠
mpo_init_pipe_label
label
填写标签
为新实例化的管道初始化标签。允许睡眠。
mpo_init_socket_label
label
初始化新标签
flag
malloc(9) 标志
初始化新实例套接字的标签。 flag 字段可以是 M_WAITOK 和 M_NOWAIT 中的一个,并且应该用于避免在此初始化调用期间执行睡眠 malloc(9)。
mpo_init_socket_peer_label
label
初始化新标签
flag
malloc(9) 标志
初始化新实例化套接字的对等标签。 flag 字段可以是 M_WAITOK 和 M_NOWAIT 之一,在此初始化调用期间应使用它们,以避免执行睡眠 malloc(9)。
mpo_init_proc_label
label
初始化新标签
为新实例化的进程初始化标签。允许睡眠。
mpo_init_vnode_label
label
初始化新标签
在新实例化的虚拟节点上初始化标签。允许睡眠。
mpo_destroy_bpfdesc_label
label
bpfdesc 标签
销毁 BPF 描述符上的标签。在此入口点,策略应释放与 label 关联的任何内部存储,以便销毁它。
mpo_destroy_cred_label
label
标签被销毁
销毁凭据上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁。
mpo_destroy_devfsdirent_label
label
被销毁的标签
在 devfs 条目上销毁标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁它。
mpo_destroy_ifnet_label
label
标签被销毁
删除被移除接口上的标签。在这个入口点,策略模块应该释放与 label 相关的任何内部存储,以便它可以被销毁。
mpo_destroy_ipq_label
label
标签被销毁
销毁 IP 片段队列上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁它。
mpo_destroy_mbuf_label
label
被销毁的标签
在 mbuf 标头上销毁标签。在此入口点,策略模块应释放与 label 相关联的任何内部存储,以便销毁它。
mpo_destroy_mount_label
label
挂载点标签被销毁
摧毁挂载点上的标签。在此入口点,策略模块应释放与 mntlabel 关联的内部存储,以便它们可以被销毁。
mpo_destroy_mount_label
mntlabel
挂载点标签被破坏
fslabel
文件系统标签被破坏
摧毁挂载点上的标签。在此入口点,策略模块应释放与 mntlabel 和 fslabel 关联的内部存储,以便可以销毁它们。
mpo_destroy_socket_label
label
销毁套接字标签
销毁套接字上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁。
mpo_destroy_socket_peer_label
peerlabel
销毁套接字对等标签
销毁套接字上的对等标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁它。
mpo_destroy_pipe_label
label
管道标签
销毁管道上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁它。
mpo_destroy_proc_label
label
过程标签
销毁进程上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便可以销毁它。
mpo_destroy_vnode_label
label
进程标签
销毁 vnode 上的标签。在此入口点,策略模块应释放与 label 关联的任何内部存储,以便销毁它。
mpo_copy_mbuf_label
src
源标签
dest
目的地标签
将 src 中的标签信息复制到 dest 。
mpo_copy_pipe_label
src
源标签
dest
目的地标签
将 src 中的标签信息复制到 dest 中。
mpo_copy_vnode_label
src
源标签
dest
目的地标签
将 src 中的标签信息复制到 dest 中。
mpo_externalize_cred_label
label
标签要外部化
element_name
应将标签外部化的策略名称
sb
应填入文本表示形式的字符串缓冲区
claimed
当 element_data 可填入时应递增。
根据传递的标签结构生成外部化标签。外部化标签包括标签内容的文本表示,可供用户空间应用程序使用并供用户阅读。当前将调用所有策略的 externalize 入口点,因此在尝试填充 sb 之前,实现应检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,请简单地返回 0。仅在在外部化标签数据时发生错误时返回非零值。一旦策略填充 element_data ,应递增 *claimed 。
mpo_externalize_ifnet_label
label
标签要外部化
element_name
应该外部化标签的策略的名称
sb
要用文本表示填充的字符串缓冲区
claimed
当 element_data 可以填入时应递增。
根据传递的标签结构生成外部化标签。外部化标签包括标签内容的文本表示,可用于用户空间应用程序,并可被用户阅读。当前,将调用所有策略的 externalize 入口点,因此实现应在尝试填充 sb 之前检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,请简单地返回 0。仅在外部化标签数据时发生错误时返回非零值。一旦策略填充 element_data ,应递增 *claimed 。
mpo_externalize_pipe_label
label
要外部化的标签
element_name
应该外部化标签的策略名称
sb
要填充文本表示的字符串缓冲器
claimed
当 element_data 可以填入时应递增。
根据传递的标签结构生成外部化标签。外部化标签包含一个标签内容的文本表示,可以与用户应用程序一起使用并被用户阅读。当前,所有策略的 externalize 入口点将被调用,因此在尝试填充 sb 之前,实现应检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,只需返回 0。只有在外部化标签数据时发生错误时才返回非零值。一旦策略填写 element_data , *claimed 就应该递增。
mpo_externalize_socket_label
label
标签外部化
element_name
应该外部化标签的策略名称
sb
用于填充文本表示的字符串缓冲区
claimed
当 element_data 可以填充时应递增。
基于传递的标签结构生成外部化标签。外部化标签包括标签内容的文本表示,可与用户空间应用程序一起使用并由用户阅读。当前,将调用所有策略的 externalize 入口点,因此实现应在尝试填充 sb 之前检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,请简单地返回 0。只有在外部化标签数据时发生错误时才返回非零值。一旦策略填写 element_data ,应递增 *claimed 。
mpo_externalize_socket_peer_label
label
应外部化的标签
element_name
应外部化标签的策略名称
sb
要填充为标签的文本表示的字符串缓冲区
claimed
当可以填入 element_data 时应递增
基于传递的标签结构生成外部化标签。外部化标签包括可以与用户端应用一起使用并由用户阅读的标签内容的文本表示。目前,将调用所有策略的 externalize 入口点,因此实现在尝试填充 sb 之前应检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,只需返回 0。仅在在外部化标签数据时发生错误时返回非零值。一旦策略填写 element_data ,应递增 *claimed 。
mpo_externalize_vnode_label
label
要外部化的标签
element_name
应该外部化标签的策略名称
sb
要填充文本表示的标签的字符串缓冲区
claimed
当 element_data 可以填入时应递增。
根据传递的标签结构生成外部化标签。外部化标签包括标签内容的文本表示,可用于用户应用程序并被用户阅读。当前,将调用所有策略的 externalize 入口点,因此在尝试填入 sb 之前,实现应检查 element_name 的内容。如果 element_name 与您的策略名称不匹配,只需返回 0。仅在在外部化标签数据时发生错误时返回非零值。一旦策略填入 element_data ,应递增 *claimed 。
mpo_internalize_cred_label
label
需要填写的标签
element_name
要内部化标签的政策名称
element_data
要内部化的文本数据
claimed
当数据可以成功内部化时应递增。
基于文本格式的外部标签数据生成内部标签结构。目前,当请求内部化时,会调用所有策略的 internalize 入口点,因此实现应该将 element_name 的内容与其自身名称进行比较,以确保它应该内部化 element_data 中的数据。就像在 externalize 入口点中一样,如果 element_name 与其自身名称不匹配,或当数据成功内部化时,入口点应该返回 0,在这种情况下 *claimed 应该递增。
mpo_internalize_ifnet_label
label
要填写的标签
element_name
应将其标签内部化的策略名称
element_data
内部化的文本数据
claimed
当数据可以成功内部化时应递增。
根据文本格式中外部化的标签数据生成内部标签结构。当前,当请求内部化时,会调用所有策略的 internalize 入口点,因此实现应该将 element_name 的内容与自身名称进行比较,以确保应该内部化 element_data 中的数据。就像 externalize 入口点一样,如果 element_name 与自身名称不匹配,入口点应返回 0,或者当数据可以成功内部化时, *claimed 应该递增。
mpo_internalize_pipe_label
label
要填入的标签
element_name
应该内部化标签的策略名称
element_data
要内部化的文本数据
claimed
当数据可以成功内部化时,应递增。
基于文本格式中的外部化标签数据生成内部标签结构。当前,当请求内部化时,所有策略的 internalize 入口点都会被调用,因此实现应该将 element_name 的内容与其自身名称进行比较,以确保应该将 element_data 中的数据内部化。就像 externalize 入口点一样,如果 element_name 与自身名称不匹配,入口点应返回 0,或者当数据可以成功内部化时,应递增 *claimed 。
mpo_internalize_socket_label
label
填写标签
element_name
应内部化标签的策略名称
element_data
要内部化的文本数据
claimed
当数据成功内部化时应递增。
根据文本格式中的外部化标签数据生成内部标签结构。当前,当请求内部化时,所有策略的 internalize 入口点都会被调用,因此实现应该将 element_name 的内容与自身名称进行比较,以确保应该将 element_data 中的数据内部化。就像 externalize 入口点一样,如果 element_name 与自身名称不匹配,入口点应该返回 0,或者当数据可以成功内部化时, *claimed 应该递增。
mpo_internalize_vnode_label
label
要填写的标签
element_name
应内部化其标签的策略名称
element_data
将文字数据内部化
claimed
当数据可以成功内部化时应递增。
根据文本格式中的外部化标签数据生成内部标签结构。当前,当请求内部化时,所有策略的 internalize 入口点都被调用,因此实现应将 element_name 的内容与其自身的名称进行比较,以确保应该在 element_data 中将数据内部化。就像在 externalize 入口点中一样,如果 element_name 不匹配其自身的名称,则入口点应返回 0,或者当数据可以成功内部化时,应递增 *claimed 。
这个入口点类别是 MAC 框架用来允许策略在内核对象上维护标签信息的。对于每个 MAC 策略感兴趣的带标签内核对象,可以注册相关生命周期事件的入口点。所有对象都实现了初始化、创建和销毁挂钩。一些对象还会实现重新标记,允许用户进程更改对象上的标签。一些对象还会实现特定于对象的事件,比如与 IP 重组相关的标签事件。一个典型的带标签对象将具有以下入口点的生命周期:
标签初始化允许策略为对象分配内存并设置标签的初始值,而不需要对象的使用上下文。分配给策略的标签槽默认情况下会被清零,所以一些策略可能不需要执行初始化。
标签创建发生在内核结构与实际内核对象关联时。例如,Mbufs 可能会在池中分配并保持未使用,直到它们被需要。mbuf 分配导致在 mbuf 上进行标签初始化,但 mbuf 创建发生在 mbuf 与数据报相关联时。通常,将为创建事件提供上下文,包括创建的情况,以及创建过程中其他相关对象的标签。例如,当从套接字创建 mbuf 时,套接字及其标签将被呈现给已注册策略,除了新的 mbuf 及其标签。在创建事件中进行内存分配是不鼓励的,因为它可能发生在内核的性能敏感 ports;此外,不允许创建调用失败,因此无法报告内存分配失败。
特定对象事件通常不属于其他广泛类别的标签事件,但通常会提供机会根据额外上下文修改或更新对象上的标签。例如,在 MAC_UPDATE_IPQ 入口点期间,IP 片段重组队列上的标签可能会在接受附加 mbuf 到该队列时进行更新。
访问控制事件将在以下部分详细讨论。
标签销毁许可策略允许在标签与对象关联期间释放与标签关联的存储或状态,以便支持对象的内核数据结构可以被重用或释放。
除了与特定内核对象关联的标签之外,还存在另一类标签:临时标签。这些标签用于存储用户进程提交的更新信息。这些标签与其他类型的标签一样被初始化和销毁,但创建事件是 MAC_INTERNALIZE,它接受一个用户标签,将其转换为内核表示形式。
6.7.3.1.1. mpo_associate_vnode_devfs
mp
Devfs 挂载点
fslabel
Devfs 文件系统标签 ( mp→mnt_fslabel )
de
Devfs 目录条目
delabel
与 de 关联的策略标签
vp
与 de 关联的 vnode
vlabel
与 vp 关联的策略标签
根据传递给定的 devfs 目录条目及其标签,为新创建的 devfs vnode 填写标签( vlabel )。
6.7.3.1.2. mpo_associate_vnode_extattr
mp
文件系统挂载点
fslabel
文件系统标签
vp
Vnode 到标签
vlabel
与 vp 关联的策略标签
尝试从文件系统扩展属性中检索 vp 的标签。成功时,返回值为 0 。如果不支持扩展属性检索,可以接受的替代方法是将 fslabel 复制到 vlabel 。发生错误时,应返回 errno 的适当值。
6.7.3.1.3. mpo_associate_vnode_singlelabel
mp
文件系统挂载点
fslabel
文件系统标签
vp
Vnode 到标签
vlabel
与 vp 关联的策略标签
在非多标签文件系统上,根据文件系统标签 fslabel ,调用此入口点设置 vp 的策略标签。
6.7.3.1.4. mpo_create_devfs_device
dev
与 devfs_dirent 对应的设备
devfs_dirent
要标记的 Devfs 目录条目。
label
用于填写 devfs_dirent 的标签。
填写要为传递的设备创建的 devfs_dirent 上的标签。当设备文件系统被挂载、重新生成或新设备可用时,将调用此函数。
6.7.3.1.5. mpo_create_devfs_directory
dirname
创建目录的名称
namelen
字符串的长度 dirname
devfs_dirent
正在创建的目录的 Devfs 目录条目
填写为传递目录创建的 devfs_dirent 上的标签。当设备文件系统被挂载、重新生成或提供需要特定目录层次结构的新设备时,将调用此函数。
6.7.3.1.6. mpo_create_devfs_symlink
cred
主题凭证
mp
Devfs 挂载点
dd
链接目标
ddlabel
与 dd 相关联的标签
de
符号链接条目
delabel
与 de 关联的标签
为新创建的 devfs(5) 符号链接条目填写标签 ( delabel )。
6.7.3.1.7. mpo_create_vnode_extattr
cred
主体凭证
mount
文件系统挂载点
label
文件系统标签
dvp
父目录 vnode
dlabel
与 dvp 关联的标签
vp
新创建的 vnode
vlabel
与 vp 关联的策略标签
cnp
vp 的组件名称
将 vp 的标签写入适当的扩展属性中。如果写入成功,请使用标签填充 vlabel ,然后返回 0。否则,返回适当的错误。
6.7.3.1.8. mpo_create_mount
cred
主体凭证
mp
被挂载的对象;文件系统
mntlabel
用于填写 mp 的策略标签
fslabel
mp 挂载的策略标签
通过传递的主题凭据填写正在创建的挂载点上的标签。当挂载新文件系统时将调用此函数。
6.7.3.1.9. mpo_create_root_mount
看 mpo_create_mount 。
填写由传递的主体凭证创建的挂载点上的标签。此调用将在根文件系统挂载后进行。
6.7.3.1.10. mpo_relabel_vnode
cred
主体凭证
vp
vnode 重新标记
vnodelabel
vp 的现有策略标签
newlabel
替换 vnodelabel 的新标签,可能是部分的
根据传递的更新 vnode 标签和传递的主体凭证,在传递的 vnode 上更新标签。
6.7.3.1.11. mpo_setlabel_vnode_extattr
cred
主题凭证
vp
写入标签的 Vnode
vlabel
与 vp 相关的策略标签
intlabel
要写出的标签
写出从 intlabel 到扩展属性的策略。这称为从 vop_stdcreatevnode_ea 调用。
6.7.3.1.12. mpo_update_devfsdirent
devfs_dirent
对象;devfs 目录条目
direntlabel
要更新为 devfs_dirent 的策略标签
vp
父 vnode
锁定
vnodelabel
vp 的策略标签
从传递的 devfs vnode 标签更新 devfs_dirent 标签。当 devfs vnode 成功重新标记以提交标签更改时,将调用此调用,以使标签更改持久,即使 vnode 被回收也是如此。当在 devfs 中创建符号链接时,将调用 mac_vnode_create_from_vnode 来初始化 vnode 标签后,也将进行此调用。
6.7.3.2.1. mpo_create_mbuf_from_socket
socket
插座
套接字锁定工作正在进行中
socketlabel
socket 的策略标签
m
对象; mbuf
mbuflabel
用于填写 m 的策略标签
从传递的套接字标签设置新创建的 mbuf 标头上的标签。当套接字生成新的数据报文或消息并存储在传递的 mbuf 中时,会调用此函数。
6.7.3.2.2. mpo_create_pipe
cred
主体凭证
pipe
管道
pipelabel
与 pipe 关联的策略标签
从传递的主体凭据上设置新创建的管道上的标签。在创建新管道时会调用此方法。
6.7.3.2.3. mpo_create_socket
cred
学科证书
不变的
so
对象;套接字进行标记
socketlabel
用于填充 so 的标签
从传递的主体凭据设置新创建套接字上的标签。当套接字被创建时进行此调用。
6.7.3.2.4. mpo_create_socket_from_socket
oldsocket
监听套接字
oldsocketlabel
与 oldsocket 相关联的策略标签
newsocket
新套接字
newsocketlabel
与 newsocketlabel 相关联的策略标签
通过基于 listen(2) 套接字 oldsocket 标记套接字 newsocket , 新接受(2) 的
6.7.3.2.5. mpo_relabel_pipe
cred
主题凭证
pipe
管道
oldlabel
与 pipe 关联的当前策略标签
newlabel
要应用于 pipe 的策略标签更新
将一个新标签 newlabel 应用于 pipe 。
6.7.3.2.6. mpo_relabel_socket
cred
主题凭证
不可变的
so
对象; 套接字
oldlabel
so 的当前标签
newlabel
so 的标签更新
从传递的套接字标签更新更新套接字上的标签
6.7.3.2.7. mpo_set_socket_peer_from_mbuf
mbuf
收到的第一份数据报文
mbuflabel
mbuf 的标签
oldlabel
套接字的当前标签
newlabel
要填写套接字的策略标签
从传递的 mbuf 标签在流套接字上设置对等标签。只有在通过流套接字接收到第一个数据报时,才会调用此调用,Unix 域套接字除外。
6.7.3.2.8. mpo_set_socket_peer_from_socket
oldsocket
本地套接字
oldsocketlabel
策略标签用于 oldsocket
newsocket
对等套接字
newsocketpeerlabel
填写策略标签用于 newsocket
从传递的远程套接字端点为流 UNIX 域套接字设置对等标签。当套接字对连接时,将调用此调用,并将为两个端点都进行调用。
6.7.3.3.1. mpo_create_bpfdesc
cred
主体凭证
不变的
bpf_d
对象; bpf 描述符
bpf
要填写的策略标签 bpf_d
从传递的主体凭证为新创建的 BPF 描述符设置标签。当由具有传递的主体凭证的进程打开 BPF 设备节点时,将调用此函数。
6.7.3.3.2. mpo_create_ifnet
ifnet
网络接口
ifnetlabel
用于填写 ifnet 的策略标签
创建接口的标签。当新的物理接口对系统可用时,或者当通过引导或用户操作实例化伪接口时,可能会发起此调用。
6.7.3.3.3. mpo_create_ipq
fragment
首个接收的 IP 片段
fragmentlabel
fragment 的策略标签
ipq
要为 IP 重组队列标记
ipqlabel
要填写的策略标签为 ipq
从第一个接收到的片段的 mbuf 标头上设置新创建的 IP 片段重组队列的标签。
6.7.3.3.4. mpo_create_datagram_from_ipq
ipq
IP 重组队列
ipqlabel
ipq 的策略标签
datagram
待标记的数据报
datagramlabel
要填写的策略标签为 datagramlabel
设置从生成它的 IP 片段重组队列中重新组装的 IP 数据报上的标签。
6.7.3.3.5. mpo_create_fragment
datagram
数据包
datagramlabel
datagram 的政策标签
fragment
要标记的片段
fragmentlabel
要填写的政策标签 datagram
将新创建的 IP 片段的 mbuf 标签设置为生成该片段的数据报的 mbuf 标头上的标签。
6.7.3.3.6. mpo_create_mbuf_from_mbuf
oldmbuf
现有(源)mbuf
oldmbuflabel
oldmbuf 的策略标签
newmbuf
新的 mbuf 将被标记
newmbuflabel
要为 newmbuf 填写策略标签
在从现有数据报的 mbuf 标头到新创建的数据报的 mbuf 标头上设置标签。可以在许多情况下调用此函数,包括为了对齐目的重新分配 mbuf 时。
6.7.3.3.7. mpo_create_mbuf_linklayer
ifnet
网络接口
ifnetlabel
ifnet 的策略标签
mbuf
新数据报的 mbuf 标头
mbuflabel
用于填写 mbuf 的策略标签
为通过接口传递链路层响应生成的新创建数据报的 mbuf 标头设置标签。 可在多种情况下调用此函数,包括 IPv4 和 IPv6 堆栈中的 ARP 或 ND6 响应。
6.7.3.3.8. mpo_create_mbuf_from_bpfdesc
bpf_d
BPF 描述符
bpflabel
用于 bpflabel 的策略标签
mbuf
新的 mbuf 将被标记
mbuflabel
用于填写 mbuf 的策略标签
使用传递的 BPF 描述符生成新创建的数据报时,在 MBUF 标头上设置标签。当向与传递的 BPF 描述符关联的 BPF 设备执行写操作时,将调用此函数。
6.7.3.3.9. mpo_create_mbuf_from_ifnet
ifnet
网络接口
ifnetlabel
ifnetlabel 的策略标签
mbuf
新数据报的 mbuf 头
mbuflabel
待填入的 Policy 标签 mbuf
在从传递的网络接口生成的新创建的数据报的 mbuf 头上设置标签
6.7.3.3.10. mpo_create_mbuf_multicast_encap
oldmbuf
现有数据报的 mbuf 标头
oldmbuflabel
oldmbuf 的策略标签
ifnet
网络接口
ifnetlabel
用于 ifnet 的策略标签
newmbuf
用于新数据报的 mbuf 标头
newmbuflabel
用于填写的策略标签 newmbuf
在通过传递的组播封装接口处理时,为新创建的数据报文设置从现有传递的数据报文生成的 mbuf 标签。当要使用虚拟接口传递 mbuf 时,进行此调用。
6.7.3.3.11. mpo_create_mbuf_netlayer
oldmbuf
收到数据包
oldmbuflabel
oldmbuf 的策略标签
newmbuf
新创建的数据报
newmbuflabel
newmbuf 的策略标签
在响应现有接收到的数据报时,设置由 IP 栈生成的新创建数据报的 mbuf 标签头( oldmbuf )。可以在许多情况下调用此函数,包括响应 ICMP 请求数据报时。
6.7.3.3.12. mpo_fragment_match
fragment
IP 数据报片段
fragmentlabel
fragment 的策略标签
ipq
IP 片段重组队列
ipqlabel
ipq 的政策标签
确定包含 IP 数据报 ( fragment ) 片段的 mbuf 头部是否与传递的 IP 片段重组队列 ( ipq ) 的标签相匹配。对于成功匹配返回 (1),对于无匹配返回 (0)。在 IP 栈尝试为新收到的片段找到现有的片段重组队列时进行此调用;如果失败,可以为该片段实例化新的片段重组队列。策略可以利用此入口点防止基于标签或其他信息不允许其重新组装的情况下重新组装其他匹配的 IP 片段。
6.7.3.3.13. mpo_relabel_ifnet
cred
主体凭据
ifnet
对象; 网络接口
ifnetlabel
ifnet 的策略标签
newlabel
应用于 ifnet 的标签更新
根据传递的更新标签 newlabel 和传递的主题凭据 cred 更新网络接口 ifnet 的标签。
6.7.3.3.14. mpo_update_ipq
mbuf
IP 片段
mbuflabel
mbuf 的策略标签
ipq
IP 片段重组队列
ipqlabel
要更新的策略标签为 ipq
根据接受传递的 IP 片段 mbuf 标头( mbuf )来更新 IP 片段重组队列( ipq )上的标签。
6.7.3.4.1. mpo_create_cred
parent_cred
父主体凭证
child_cred
子主体凭证
从传递的主体凭证设置新创建的主体凭证的标签。当在新创建的 struct ucred 上调用 crcopy(9) 时将进行此调用。此调用不应与进程复制或创建事件混淆。
6.7.3.4.2. mpo_execve_transition
old
现有主题凭据
不可改变
new
新主体凭证需标记
vp
要执行的文件
已锁定
vnodelabel
vp 的政策标签
根据执行传递的 vnode( vp )引起的标签转换,从已传递的现有主体凭证( old )更新新创建的主体凭证( new )的标签。当一个过程执行传递的 vnode 并且其中一个策略从 mpo_execve_will_transition 入口点返回成功时,将发生此调用。策略可以选择通过调用 mpo_create_cred 并传递两个主体凭证来实现此调用,以便不实现过渡事件。即使策略不实现 mpo_execve_will_transition ,在实现 mpo_create_cred 时也不应使该入口点未实现。
6.7.3.4.3. mpo_execve_will_transition
old
在执行 execve(2)之前的主体凭证
不可变
vp
执行的文件
vnodelabel
vp 的策略标签
确定策略是否希望根据传递的 vnode 的执行结果以及传递的主体凭证执行过渡事件。如果需要过渡,则返回 1,否则返回 0。即使策略返回 0,在出现对 mpo_execve_transition 的意外调用时也应正确执行,因为该调用可能是另一个策略请求过渡的结果。
6.7.3.4.4. mpo_create_proc0
cred
需要填写主体凭据
创建进程 0 的主体凭据,即所有内核进程的父进程。
6.7.3.4.5. mpo_create_proc1
cred
需填写主体凭证
创建进程 1 的主体凭证,所有用户进程的父进程。
6.7.3.4.6. mpo_relabel_cred
cred
学科凭证
newlabel
更新标签以应用于 cred
更新主题凭据上的标签从传递的更新标签。
访问控制条目点允许策略模块影响内核所做的访问控制决策。一般来说,虽然不是总是这样,访问控制条目点的参数将包括一个或多个授权凭证,操作涉及的任何其他对象的信息(可能包括标签)。访问控制条目点可能返回 0 以允许操作,或者一个 errno(2)错误值。调用各个已注册的策略模块中的条目点的结果将组合如下:如果所有模块都允许操作成功,则将返回成功。如果一个或多个模块返回失败,则将返回一个失败。如果多个模块返回失败,则将使用以下优先顺序选择要返回给用户的 errno 值,该优先顺序由 kern_mac.c 中的 error_select() 函数实现:
EINVAL
ESRCH
EACCES
最低优先级
EPERM
如果所有模块返回的错误值都不在优先级图表中列出,则将从集合中任意选择一个值返回。一般情况下,规则按以下顺序对错误赋予优先级:内核故障,无效参数,对象不存在,访问不允许,其他。
mpo_check_bpfdesc_receive
bpf_d
主题; BPF 描述符
bpflabel
bpf_d 的策略标签
ifnet
对象; 网络接口
ifnetlabel
ifnet 的策略标签
确定 MAC 框架是否允许从传递的接口传递数据报文到传递的 BPF 描述符的缓冲区。成功返回(0),失败返回 errno 值。建议的失败原因:标签不匹配为 EACCES,缺乏权限为 EPERM。
mpo_check_kenv_dump
cred
主题凭证
确定是否应允许主体检索内核环境(请参阅 kenv(2))。
mpo_check_kenv_get
cred
主题凭证
name
内核环境变量名称
确定是否应允许主体检索指定内核环境变量的值。
mpo_check_kenv_set
cred
主题凭证
name
内核环境变量名
确定是否应允许主题设置指定的内核环境变量。
mpo_check_kenv_unset
cred
主体凭证
name
内核环境变量名称
确定是否应允许主体取消设置指定的内核环境变量。
mpo_check_kld_load
cred
主题凭证
vp
内核模块 vnode
vlabel
与 vp 关联的标签
确定主题是否应被允许加载指定的模块文件。
mpo_check_kld_stat
cred
主题凭证
确定是否应允许主体检索已加载的内核模块文件列表和相关统计信息。
mpo_check_kld_unload
cred
主体凭证
确定是否应允许主题卸载内核模块。
mpo_check_pipe_ioctl
cred
主体凭证
pipe
管道
pipelabel
与 pipe 关联的策略标签
cmd
ioctl(2) 命令
data
ioctl(2) 数据
确定是否应允许主体进行指定的 ioctl(2)调用。
mpo_check_pipe_poll
cred
主题凭证
pipe
管道
pipelabel
与 pipe 关联的策略标签
确定是否应允许主体轮询 pipe 。
mpo_check_pipe_read
cred
主体凭证
pipe
管道
pipelabel
与 pipe 相关的策略标签
确定是否应允许主题读取 pipe 的访问。
mpo_check_pipe_relabel
cred
凭证
pipe
管道
pipelabel
与 pipe 相关的当前策略标签
newlabel
标签更新为 pipelabel
确定是否应允许主体重新标记 pipe 。
mpo_check_pipe_stat
cred
主体凭据
pipe
管道
pipelabel
与 pipe 相关的策略标签
确定是否应允许主体检索与 pipe 相关的统计信息
mpo_check_pipe_write
cred
主体凭证
pipe
管道
pipelabel
与 pipe 相关联的策略标签
确定主题是否应该被允许写入 pipe 。
mpo_check_socket_bind
cred
主体凭证
socket
要绑定的套接字
socketlabel
socket 的政策标签
sockaddr
socket 的地址
mpo_check_socket_connect
cred
主体凭证
socket
需要连接的插座
socketlabel
socket 的策略标签
sockaddr
socket 的地址
确定主体凭证( cred )是否可以将传递的套接字( socket )连接到传递的套接字地址( sockaddr )。成功返回 0,或者失败返回 errno 值。建议的失败: 标签不匹配的话返回 EACCES,缺乏特权返回 EPERM。
mpo_check_socket_receive
cred
主体凭证
so
套接字
socketlabel
与 so 关联的策略标签
确定是否应允许主体从套接字 so 接收信息。
mpo_check_socket_send
cred
主题凭证
so
套接字
socketlabel
与 so 相关联的策略标签
确定主体是否应该被允许通过套接字 so 发送信息。
mpo_check_cred_visible
u1
主体凭证
u2
客体凭证
确定是否通过传递的主体凭证 u1 可以“看见”具有传递的主体凭证 u2 的其他主体。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,缺乏特权的 EPERM,或者隐藏可见性的 ESRCH。此调用可能在许多情况下进行,包括由 ps 使用的进程间状态 sysctl 和 procfs 查找。
mpo_check_socket_visible
cred
凭证主体
socket
对象; 套接字
socketlabel
socket 的策略标签
mpo_check_ifnet_relabel
cred
主体凭证
ifnet
对象; 网络接口
ifnetlabel
ifnet 的现有策略标签
newlabel
更新策略标签,稍后应用于 ifnet
确定主体凭据是否可以将传递的网络接口重新标记为传递的标签更新。
mpo_check_socket_relabel
cred
主体凭证
socket
对象; 插座
socketlabel
socket 的现有策略标签
newlabel
标签更新,稍后将应用于 socketlabel
确定主体凭据是否可以重新标记传递的套接字以更新传递的标签。
mpo_check_cred_relabel
cred
主题凭证
newlabel
标签更新,稍后将应用于 cred
确定主体凭据是否可以重新标记自身以传递的标签更新。
mpo_check_vnode_relabel
cred
主题凭证
不可变
vp
对象; vnode
已锁定
vnodelabel
vp 的现有策略标签
newlabel
vp 后续应用的策略标签更新
确定主体凭证是否可以将传递的 vnode 重新标记为传递的标签更新
mpo_check_mount_stat
cred
主题凭证
mp
对象;文件系统挂载
mountlabel
mp 的策略标签
确定主体凭据是否可以看到对文件系统执行的 statfs 的结果。成功返回 0,失败返回 errno 值。建议的失败情况包括标签不匹配的 EACCES 或缺乏特权的 EPERM。此调用可以在许多情况下进行,包括在调用 statfs(2)和相关调用时,以及确定要从文件系统列表中排除哪些文件系统时,例如在调用 getfsstat(2)时。
mpo_check_proc_debug
cred
主体凭证
不可变的
proc
对象; 过程
确定主体凭证是否可以调试传递的进程。成功返回 0,失败返回一个 errno 值。建议的失败情况包括:标签不匹配返回 EACCES,缺乏权限返回 EPERM,或者为了隐藏目标的可见性返回 ESRCH。可以在许多情况下进行此调用,包括使用 ptrace(2)和 ktrace(2) API,以及某些类型的 procfs 操作。
mpo_check_vnode_access
cred
主题凭证
vp
对象; 虚结点
label
vp 的策略标签
flags
access(2) 标志
确定当主体凭证对传递的 vnode 使用传递的访问标志执行 access(2)和相关调用时应如何返回。一般应该使用与 mpo_check_vnode_open 相同的语义实现。成功返回 0,失败返回 errno 值。建议的失败:EACCES 用于标签不匹配或 EPERM 用于缺乏权限。
mpo_check_vnode_chdir
cred
主体凭证
dvp
对象;vnode 到 chdir(2) 进入
dlabel
dvp 的策略标签
确定主题凭证是否可以将进程的工作目录更改为传递的 vnode。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者缺乏特权的 EPERM。
mpo_check_vnode_chroot
cred
主体凭证
dvp
目录虚节点
dlabel
与 dvp 关联的策略标签
确定主体是否应被允许 chroot(2) 进入指定目录 ( dvp )。
mpo_check_vnode_create
cred
主题凭证
dvp
对象; vnode
dlabel
dvp 的策略标签
cnp
dvp 的组件名称
vap
vap 的 vnode 属性
确定主题凭证是否可以使用传递的父目录、传递的名称信息和传递的属性信息创建 vnode。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者权限不足的 EPERM。在许多情况下可能会调用此函数,包括作为对使用 O_CREAT 打开(2)、mkfifo(2)和其他调用的结果.
mpo_check_vnode_delete
cred
主体凭证
dvp
父目录 vnode
dlabel
dvp 的策略标签
vp
要删除的对象; vnode
label
vp 的策略标签
cnp
vp 的组件名称
确定主体凭证是否可以从传递的父目录和传递的名称信息中删除一个 vnode。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的情况下为 EACCES,或者权限不足的情况下为 EPERM。可以在许多情况下调用此函数,包括由 unlink(2)和 rmdir(2)调用的结果。实现此入口点的策略还应实现 mpo_check_rename_to 以授权由于成为重命名目标而删除对象。
mpo_check_vnode_deleteacl
cred
凭证
不可变的
vp
对象;虚拟节点
锁定
label
vp 的策略标签
type
ACL 类型
确定主体凭证是否可以从传递的 vnode 中删除传递类型的 ACL。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者权限不足的 EPERM。
mpo_check_vnode_exec
cred
主题凭证
vp
对象;要执行的 vnode
label
vp 的策略标签
确定主体凭证是否可以执行传递的 vnode。执行特权的确定与关于任何过渡事件的决定分开进行。成功返回 0,失败返回 errno 值。建议的失败:标签不匹配为 EACCES,或者权限不足为 EPERM。
mpo_check_vnode_getacl
cred
主题凭证
vp
对象; vnode
label
vp 的策略标签
type
ACL 类型
确定主体凭据是否可以从传递的 vnode 中检索传递类型的 ACL。成功返回 0,或失败返回 errno 值。建议的故障:标签不匹配的 EACCES,或缺乏特权的 EPERM。
mpo_check_vnode_getextattr
cred
主体凭证
vp
对象; vnode
label
vp 的策略标签
attrnamespace
扩展属性命名空间
name
扩展属性名称
uio
I/O 结构指针;参见 uio(9)
确定主题凭证是否可以从传递的 vnode 中检索具有传递命名空间和名称的扩展属性。使用扩展属性实现标记的策略可能对这些扩展属性的操作有特殊处理。返回 0 表示成功,或返回 errno 值表示失败。建议的失败:标签不匹配的 EACCES,或缺乏权限的 EPERM。
mpo_check_vnode_link
cred
主体凭证
dvp
目录 vnode
dlabel
与 dvp 关联的策略标签
vp
链接目标 vnode
label
与 vp 相关的策略标签
cnp
正在创建的链接的组件名称
确定是否允许主体使用由 cnp 指定名称创建到 v 节点 vp 的链接
mpo_check_vnode_mmap
cred
主体凭证
vp
Vnode 映射
label
与 vp 相关联的策略标签
prot
内存映射保护(请参阅 mmap(2))
确定主体是否应该被允许使用指定的保护来映射 vnode vp 。
mpo_check_vnode_mmap_downgrade
cred
查看 mpo_check_vnode_mmap 。
vp
label
prot
降低内存映射保护
根据主体和客体标签降低内存映射保护
mpo_check_vnode_mprotect
cred
主题凭证
vp
映射的 vnode
prot
内存保护
确定主体是否应被允许在从 vnode 映射的内存上设置指定的内存保护 vp 。
mpo_check_vnode_poll
active_cred
主题凭证
file_cred
与结构文件相关的凭证
vp
轮询的 vnode
label
与 vp 关联的策略标签
确定是否应允许主体轮询 vnode vp 。
mpo_check_vnode_rename_from
cred
主题凭证
dvp
目录 vnode
dlabel
与 dvp 关联的策略标签
vp
将要重命名的 vnode
label
与 vp 相关的策略标签
cnp
vp 的组件名称
确定主体是否应被允许将 vnode vp 重命名为其他内容。
mpo_check_vnode_rename_to
cred
主体凭证
dvp
目录 vnode
dlabel
与 dvp 关联的策略标签
vp
覆盖的 vnode
label
与 vp 相关的策略标签
samedir
布尔值;如果源目录和目标目录相同,则为 1
cnp
目的地组件名称
确定是否应允许主题重命名到 vnode vp ,到目录 dvp ,或到 cnp 表示的名称。如果没有现有文件可覆盖, vp 和 label 将为 NULL。
mpo_check_socket_listen
cred
主体凭证
socket
对象; 套接字
socketlabel
socket 的策略标签
确定主体凭证是否可以监听传递的套接字。成功返回 0,或失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者缺乏特权的 EPERM。
mpo_check_vnode_lookup
cred
主题凭证
dvp
对象; vnode
dlabel
dvp 的策略标签
cnp
查找的组件名称
确定主体凭据是否可以在传递的目录 vnode 中查找传递的名称。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的情况下返回 EACCES,或者缺乏权限的情况下返回 EPERM。
mpo_check_vnode_open
cred
主体凭证
vp
对象; vnode
label
vp 的策略标签
acc_mode
open(2) 访问模式
确定主体凭证是否可以对传递的 vnode 执行开放操作,并使用传递的访问模式。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的情况下返回 EACCES,或者缺乏特权的情况下返回 EPERM。
mpo_check_vnode_readdir
cred
主题凭证
dvp
对象; 目录 vnode
dlabel
dvp 的策略标签
确定主体凭证是否可以在传递的目录 vnode 上执行 readdir 操作。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配为 EACCES,权限不足为 EPERM。
mpo_check_vnode_readlink
cred
主题凭证
vp
对象; vnode
label
vp 的策略标签
确定主体凭证是否可以在传递的符号链接 vnode 上执行 readlink 操作。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者权限不足的 EPERM。此调用可以在许多情况下进行,包括用户进程的显式 readlink 调用,或者作为进程进行名称查找期间的隐式 readlink 的结果。
mpo_check_vnode_revoke
cred
证书主题
vp
对象; 虚拟节点
label
vp 的策略标签
确定主体凭证是否可以撤销对传递的 vnode 的访问权限。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者缺乏特权的 EPERM。
mpo_check_vnode_setacl
cred
主题凭证
vp
对象;vnode
label
" vp "的策略标签
type
ACL 类型
acl
ACL
确定主体凭证是否可以在传递的 vnode 上设置传递的 ACL 的类型。成功返回 0,失败返回 errno 值。建议失败情况: 标签不匹配的 EACCES,或缺乏权限的 EPERM。
mpo_check_vnode_setextattr
cred
主体凭证
vp
对象; 虚节点
label
vp 的策略标签
attrnamespace
扩展属性命名空间
name
扩展属性名称
uio
I/O 结构指针;参见 uio(9)
确定主题凭证是否可以在传递的 vnode 上设置传递名称和传递命名空间的扩展属性。 实现安全标签的策略将希望为这些属性提供额外的保护。 此外,策略不应该基于从 uio 引用的数据做出决定,因为在此检查和实际操作之间存在潜在的竞争条件。 如果执行删除操作,则 uio 也可能是 NULL 。 为成功返回 0,失败返回 errno 值。 建议失败:标签不匹配的 EACCES,或者缺乏权限的 EPERM。
mpo_check_vnode_setflags
cred
主题凭据
vp
对象;vnode
label
vp 的策略标签
flags
文件标志; 参见 chflags(2)
确定主体凭证是否可以在传递的 vnode 上设置传递的标志。成功返回 0,失败返回 errno 值。建议的失败原因: 标签不匹配使用 EACCES,或者权限不足使用 EPERM。
mpo_check_vnode_setmode
cred
主题凭证
vp
对象; vnode
label
vp 的策略标签
mode
文件模式;请参阅 chmod(2)
确定主体凭据是否可以在传递的 vnode 上设置传递的模式。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配为 EACCES,或者权限不足为 EPERM。
mpo_check_vnode_setowner
cred
主题凭证
vp
对象; vnode
label
vp 的策略标签
uid
用户 ID
gid
组 ID
确定主体凭据是否可以将传递的 UID 和 GID 设置为文件 UID 和文件 GID 在传递的 vnode 上。这些 ID 可设置为( -1 ),以请求不进行更新。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配为 EACCES,权限不足为 EPERM。
mpo_check_vnode_setutimes
cred
主体凭证
vp
对象; vp
label
vp 的策略标签
atime
访问时间; 请参阅 utimes(2)
mtime
修改时间;请参阅 utimes(2)
确定主体凭证是否可以在传递的 vnode 上设置传递的访问时间戳。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者权限不足的 EPERM。
mpo_check_proc_sched
cred
主体凭证
proc
对象; 进程
确定主体凭据是否可以更改传递进程的调度参数。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,缺乏特权的 EPERM,或限制可见性的 ESRCH。
有关更多信息,请参阅 setpriority(2)。
mpo_check_proc_signal
cred
主体凭证
proc
对象; 进程
signal
信号; 参见 kill(2)
确定主体凭据是否可以将传递的信号传递给传递的进程。成功返回 0,或者失败返回 errno 值。建议失败:标签不匹配使用 EACCES,缺乏特权使用 EPERM,或限制可见性使用 ESRCH。
mpo_check_vnode_stat
cred
主体凭证
vp
对象; vnode
label
vp 的政策标签
确定主体凭证是否可以 stat 传递的 vnode。成功返回 0,或失败返回 errno 值。建议的失败:标签不匹配的 EACCES,或缺乏特权的 EPERM。
查看 stat(2) 以获取更多信息。
mpo_check_ifnet_transmit
cred
主题凭证
ifnet
网络接口
ifnetlabel
对 ifnet 的策略标签
mbuf
要发送的对象;mbuf
mbuflabel
mbuf 的策略标签
确定网络接口是否可以传输传递的 mbuf。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的话返回 EACCES,缺乏权限的话返回 EPERM。
mpo_check_socket_deliver
cred
主体凭证
ifnet
网络接口
ifnetlabel
ifnet 的策略标签
mbuf
对象; 待传递的 mbuf
mbuflabel
mbuf 的策略标签
确定套接字是否可以接收传递的 mbuf 头中存储的数据报。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的 EACCES,或者缺乏特权的 EPERM。
mpo_check_socket_visible
cred
主体凭证
不可改变的
so
对象; 套接字
socketlabel
so 的策略标签
使用系统监控功能(例如 netstat(8)和 sockstat(1)使用的功能)确定主体凭证 cred 是否可以“看到”传递的套接字( socket )。成功返回 0,失败返回 errno 值。建议的失败情况:标签不匹配的情况下为 EACCES,缺乏权限的情况下为 EPERM,隐藏可见性的情况下为 ESRCH。
mpo_check_system_acct
ucred
主体凭证
vp
会计文件; acct(5)
vlabel
与 vp 相关联的标签
根据主体的标签和会计日志文件的标签,确定是否应允许主体启用会计。
mpo_check_system_nfsd
cred
主题凭证
确定是否允许主体调用 nfssvc(2)。
mpo_check_system_reboot
cred
主体凭证
howto
从 reboot(2)中的 howto 参数
确定是否应允许主体以指定的方式重新启动系统。
mpo_check_system_settime
cred
主题凭证
确定用户是否应被允许设置系统时钟。
mpo_check_system_swapon
cred
主题凭证
vp
交换设备
vlabel
与 vp 关联的标签
确定是否应允许主体将 vp 添加为交换设备。
mpo_check_system_sysctl
cred
主体凭证
name
看 sysctl(3)
namelen
old
oldlenp
inkernel
布尔;如果从内核调用,则为 1
new
看 sysctl(3)
newlen
确定是否应允许主体进行指定的 sysctl(3)事务。
当用户进程请求修改对象的标签时,会发生重新标记事件。更新分两个阶段进行:首先,将执行访问控制检查以确定更新是否合法和允许,然后通过一个单独的入口点执行更新。重新标记入口点通常接受对象、对象标签引用和由进程提交的更新标签。在重新标记期间不鼓励内存分配,因为重新标记调用不允许失败(失败应在重新标记检查中提前报告)。
TrustedBSD MAC 框架包括许多与策略无关的元素,包括用于抽象管理标签的 MAC 库接口,对系统凭据管理和登录库的修改以支持给用户分配 MAC 标签,以及一组工具来监视和修改进程、文件和网络接口上的标签。有关用户架构的更多详细信息将在不久的将来添加到本节。
TrustedBSD MAC Framework 提供了许多库和系统调用,允许应用程序使用与策略无关的接口管理对象上的 MAC 标签。这使得应用程序可以在不支持特定策略的情况下操作各种策略的标签。这些接口被通用工具(如 ifconfig(8)、ls(1)和 ps(1))使用,用于查看网络接口、文件和进程上的标签。这些 API 还支持包括 getfmac(8)、getpmac(8)、setfmac(8)、setfsmac(8)和 setpmac(8)在内的 MAC 管理工具。MAC API 的文档在 mac(3)中。
应用程序以两种形式处理 MAC 标签:一种是内部形式,用于返回和设置进程和对象上的标签( mac_t ),另一种是基于 C 字符串的外部形式,适合存储在配置文件中、显示给用户或用户输入。每个 MAC 标签包含多个元素,每个元素由名称和值对组成。内核中的策略模块绑定到特定名称,并以特定于策略的方式解释值。在外部化字符串形式中,标签由逗号分隔的名称和值对列表表示,用 / 字符分隔。标签可以直接使用提供的 API 转换为文本,从内核检索标签时,必须首先准备所需的标签元素集的内部化标签存储。通常有两种方法来完成这个过程:使用 mac_prepare(3)和任意的所需标签元素列表,或者使用从 mac.conf(5)配置文件加载默认元素集的调用变体之一。对象默认值允许应用程序编写者有用地显示与对象关联的标签,而不必了解系统中存在的策略。
标准用户上下文管理接口 setusercontext(3)已经修改,以从 login.conf(5)中检索与用户类别关联的 MAC 标签。当指定 LOGIN_SETALL 或明确指定 LOGIN_SETMAC 时,这些标签将与其他用户上下文一起设置。
TrustedBSD MAC 框架允许内核模块以高度集成的方式增强系统安全策略。它们可以基于现有对象属性或基于在 MAC 框架的帮助下维护的标签数据来执行此操作。该框架具有足够的灵活性,可以实现各种策略类型,包括诸如 MLS 和 Biba 之类的信息流安全策略,以及基于现有 BSD 凭据或文件保护的策略。策略作者在实现新的安全服务时可能希望参考本文档以及现有安全模块。