Add the Simplified Chinese Translation of the FreeBSD Architecture

Handbook.

Obtained from:	The FreeBSD Simplified Chinese Project
Work done by:	intron <intron at intron ac>,
		spellar <spellar at newsmth dot net>,
		delphij
Additional Contributor:
		Qin ZHAO <chaochin at emails bjut edu cn> (language)
This commit is contained in:
Xin LI 2006-03-15 19:54:49 +00:00
parent 639205b60c
commit d6aa1908eb
Notes: svn2git 2020-12-08 03:00:23 +00:00
svn path=/head/; revision=27345
20 changed files with 17858 additions and 0 deletions

View file

@ -0,0 +1,60 @@
#
# Original Revision: 1.25
# $FreeBSD$
#
# Build the FreeBSD Architecture Handbook.
#
MAINTAINER=doc@FreeBSD.org
DOC?= book
FORMATS?= html-split
HAS_INDEX= true
INSTALL_COMPRESSED?= gz
INSTALL_ONLY_COMPRESSED?=
#
# SRCS lists the individual SGML files that make up the document. Changes
# to any of these files will force a rebuild
#
# SGML content
SRCS= book.sgml
SRCS+= boot/chapter.sgml
SRCS+= driverbasics/chapter.sgml
SRCS+= isa/chapter.sgml
SRCS+= jail/chapter.sgml
SRCS+= kobj/chapter.sgml
SRCS+= locking/chapter.sgml
SRCS+= mac/chapter.sgml
SRCS+= newbus/chapter.sgml
SRCS+= pci/chapter.sgml
SRCS+= scsi/chapter.sgml
SRCS+= smp/chapter.sgml
SRCS+= sound/chapter.sgml
SRCS+= pccard/chapter.sgml
SRCS+= sysinit/chapter.sgml
SRCS+= usb/chapter.sgml
SRCS+= vm/chapter.sgml
# Images from the cross-document image library
IMAGES_LIB= callouts/1.png
IMAGES_LIB+= callouts/2.png
IMAGES_LIB+= callouts/3.png
IMAGES_LIB+= callouts/4.png
IMAGES_LIB+= callouts/5.png
IMAGES_LIB+= callouts/6.png
IMAGES_LIB+= callouts/7.png
IMAGES_LIB+= callouts/8.png
IMAGES_LIB+= callouts/9.png
IMAGES_LIB+= callouts/10.png
# Entities
URL_RELPREFIX?= ../../../..
DOC_PREFIX?= ${.CURDIR}/../../..
.include "${DOC_PREFIX}/share/mk/doc.project.mk"

View file

@ -0,0 +1,184 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.49
$FreeBSD$
-->
<!DOCTYPE BOOK PUBLIC "-//FreeBSD//DTD DocBook V4.1-Based Extension//EN" [
<!ENTITY % books.ent PUBLIC "-//FreeBSD//ENTITIES DocBook FreeBSD Books Entity Set//EN">
%books.ent;
<!ENTITY % chapters SYSTEM "chapters.ent">
%chapters;
<!ENTITY % mac-entities SYSTEM "mac.ent">
%mac-entities;
<!ENTITY % chap.index "IGNORE">
]>
<book>
<bookinfo>
<title>&os; 系统结构手册</title>
<corpauthor>The FreeBSD Documentation Project</corpauthor>
<pubdate>2000 年 8 月</pubdate>
<copyright>
<year>2000</year>
<year>2001</year>
<year>2002</year>
<year>2003</year>
<year>2004</year>
<year>2005</year>
<holder>The FreeBSD Documentation Project</holder>
</copyright>
<corpauthor>&cnproj.freebsd.org;</corpauthor>
<pubdate>2005 年 12 月</pubdate>
<copyright>
<year>2004</year>
<year>2005</year>
<holder>&cnproj.freebsd.org;</holder>
</copyright>
&bookinfo.trademarks;
&bookinfo.legalnotice;
<abstract>
<!--
The following two entities "contributing.to.freebsd.doc" and
"getting.freebsd.doc" are defined in zh_CN.GB2312/share/sgml/l10n.ent
-->
<para>欢迎您阅读《&os;系统结构手册》。&cnproj.contributing.to.freebsd.doc;</para>
<para>&cnproj.getting.freebsd.doc;</para>
</abstract>
</bookinfo>
<part id="kernel">
<title>内核</title>
&chap.boot;
&chap.locking;
&chap.kobj;
&chap.jail;
&chap.sysinit;
&chap.mac;
&chap.vm;
&chap.smp;
</part>
<part id="devicedrivers">
<title>设备驱动程序</title>
&chap.driverbasics;
&chap.isa;
&chap.pci;
&chap.scsi;
&chap.usb;
&chap.newbus;
&chap.snd;
&chap.pccard;
</part>
<!-- XXX - finish me
<part id="architectures">
<title>Architectures</title>
<chapter id="i386">
<title>* I386</title>
<para>Talk about <literal>i386</literal> specific &os;
architecture.</para>
</chapter>
<chapter id="alpha">
<title>* Alpha</title>
<para>Talk about the architectural specifics of
FreeBSD/alpha.</para>
</chapter>
<chapter id="ia64">
<title>* IA-64</title>
<para>Talk about the architectural specifics of
FreeBSD/ia64.</para>
</chapter>
<chapter id="sparc64">
<title>* SPARC64</title>
<para>Talk about <literal>SPARC64</literal> specific &os;
architecture.</para>
</chapter>
<chapter id="amd64">
<title>* AMD64</title>
<para>Talk about <literal>AMD64</literal> specific &os;
architecture.</para>
</chapter>
<chapter id="powerpc">
<title>* PowerPC</title>
<para>Talk about <literal>PowerPC</literal> specific &os;
architecture.</para>
</chapter>
</part>
-->
<part id="appendices">
<title>附录</title>
<bibliography>
<biblioentry xreflabel="1">
<authorgroup>
<author>
<firstname>Marshall</firstname>
<othername role="Middle">Kirk</othername>
<surname>McKusick</surname>
</author>
<author>
<firstname>Keith</firstname>
<surname>Bostic</surname>
</author>
<author>
<firstname>Michael</firstname>
<othername role="MI">J</othername>
<surname>Karels</surname>
</author>
<author>
<firstname>John</firstname>
<othername role="MI">S</othername>
<surname>Quarterman</surname>
</author>
</authorgroup>
<copyright><year>1996</year><holder>Addison-Wesley Publishing Company,
Inc.</holder></copyright>
<isbn>0-201-54979-4</isbn>
<publisher>
<publishername>Addison-Wesley Publishing Company, Inc.</publishername>
</publisher>
<title>The Design and Implementation of the 4.4 BSD Operating System</title>
<pagenums>1-2</pagenums>
</biblioentry>
</bibliography>
<![ %chap.index; [ &chap.index; ]]>
</part>
</book>

View file

@ -0,0 +1,930 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.25
$FreeBSD$
Copyright (c) 2002 Sergey Lyubka <devnull@uptsoft.com>
All rights reserved
-->
<chapter id="boot">
<chapterinfo>
<authorgroup>
<author>
<firstname>Sergey</firstname>
<surname>Lyubka</surname>
<contrib>&cnproj.contributed.by;</contrib>
</author> <!-- devnull@uptsoft.com 12 Jun 2002 -->
</authorgroup>
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>引导过程与内核初始化</title>
<sect1 id="boot-synopsis">
<title>概述</title>
<indexterm><primary>BIOS(基本输入输出系统, Basic Input Output System)</primary></indexterm>
<indexterm><primary>fireware(固件)</primary></indexterm>
<indexterm><primary>POST(加电自检, Power On Self Test)</primary></indexterm>
<indexterm><primary>IA-32</primary></indexterm>
<indexterm><primary>booting(引导)</primary></indexterm>
<indexterm><primary>system initialization(系统初始化)</primary></indexterm>
<para>这一章是对引导过程和系统初始化过程的总览。这些过程始于BIOS(固件)POST,
直到第一个用户进程建立。由于系统启动的最初步骤是与硬件结构相关的、是紧配合的,
这里用IA-32(Intel Architecture 32bit)结构作为例子。</para>
</sect1>
<sect1 id="boot-overview">
<title>总览</title>
<para>一台运行FreeBSD的计算机有多种引导方法。这里讨论其中最通常的方法
也就是从安装了操作系统的硬盘上引导。引导过程分几步完成:</para>
<itemizedlist>
<listitem><para>BIOS POST</para></listitem>
<listitem><para><literal>boot0</literal>阶段</para></listitem>
<listitem><para><literal>boot2</literal>阶段</para></listitem>
<listitem><para>loader阶段</para></listitem>
<listitem><para>内核初始化</para></listitem>
</itemizedlist>
<indexterm><primary>BIOS POST</primary></indexterm>
<indexterm><primary>boot0</primary></indexterm>
<indexterm><primary>boot2</primary></indexterm>
<indexterm><primary>loader</primary></indexterm>
<para><literal>boot0</literal>和<literal>boot2</literal>阶段在手册
&man.boot.8;中被称为<emphasis>bootstrap stages 1 and 2</emphasis>
是FreeBSD的3阶段引导过程的开始。在每一阶段都有各种各样的信息显示在屏幕上
你可以参考下表识别出这些步骤。请注意实际的显示内容可能随机器的不同而有一些区别:
</para>
<informaltable frame="none" pgwide="1">
<tgroup cols="2">
<tbody>
<row>
<entry><para>视不同机器而定</para></entry>
<entry><para>BIOS(固件)消息</para></entry>
</row>
<row>
<entry><para>
<screen>F1 FreeBSD
F2 BSD
F5 Disk 2</screen>
</para></entry>
<entry><para><literal>boot0</literal></para></entry>
</row>
<row>
<entry><para>
<screen>&gt;&gt;FreeBSD/i386 BOOT
Default: 1:ad(1,a)/boot/loader
boot:</screen>
</para></entry>
<entry><para><literal>boot2</literal><footnote><para>
这种提示仅在<literal>boot0</literal>阶段用户选择操作系统后
仍按住键盘上某一键时才出现。</para></footnote></para></entry>
</row>
<row>
<entry><para>
<screen>BTX loader 1.0 BTX version is 1.01
BIOS drive A: is disk0
BIOS drive C: is disk1
BIOS 639kB/64512kB available memory
FreeBSD/i386 bootstrap loader, Revision 0.8
Console internal video/keyboard
(jkh@bento.freebsd.org, Mon Nov 20 11:41:23 GMT 2000)
/kernel text=0x1234 data=0x2345 syms=[0x4+0x3456]
Hit [Enter] to boot immediately, or any other key for command prompt
Booting [kernel] in 9 seconds..._</screen>
</para></entry>
<entry><para>loader</para></entry>
</row>
<row>
<entry><para>
<screen>Copyright (c) 1992-2002 The FreeBSD Project.
Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
The Regents of the University of California. All rights reserved.
FreeBSD 4.6-RC #0: Sat May 4 22:49:02 GMT 2002
devnull@kukas:/usr/obj/usr/src/sys/DEVNULL
Timecounter "i8254" frequency 1193182 Hz</screen></para></entry>
<entry><para>内核</para></entry>
</row>
</tbody>
</tgroup>
</informaltable>
</sect1>
<sect1 id="boot-bios">
<title>BIOS POST</title>
<para>当PC加电后处理器的寄存器被设为某些特定值。在这些寄存器中
<emphasis>指令指针</emphasis>寄存器被设为32位值0xfffffff0。
指令指针寄存器指向处理器将要执行的指令代码。<literal>cr1</literal>
一个32位控制寄存器在刚启动时值被设为0。cr1的PE(Protected Enabled
保护模式使能)位用来指示处理器是处于保护模式还是实地址模式。
由于启动时该位被清位,处理器在实地址模式中引导。在实地址模式中,
线性地址与物理地址是等同的。</para>
<para>值0xfffffff0略小于4G,因此计算机没有4G字节物理内存
这就不会是一个有效的内存地址。计算机硬件将这个地址转指向BIOS存储块。
</para>
<para>BIOS表示<emphasis>Basic Input Output System</emphasis>
(基本输入输出系统)。在主板上,它被固化在一个相对容量较小的
只读存储器(Read-Only Memory, ROM)。BIOS包含各种各样为主板硬件
定制的底层例程。就这样处理器首先指向常驻BIOS存储器的地址
0xfffffff0。通常这个位置包含一条跳转指令指向BIOS的POST例程。</para>
<para>POST表示<emphasis>Power On Self Test</emphasis>(加电自检)。
这套程序包括内存检查,系统总线检查和其它底层工具,
从而使得CPU能够初始化整台计算机。这一阶段中有一个重要步骤
就是确定引导设备。现在所有的BIOS都允许手工选择引导设备。
你可以从软盘、光盘驱动器、硬盘等设备引导。</para>
<para>POST的最后一步是执行<literal>INT 0x19</literal>指令。
这个指令从引导设备第一个扇区读取512字节装入地址0x7c00。
<emphasis>第一个扇区</emphasis>的说法最早起源于硬盘的结构,
硬盘面被分为若干圆柱形轨道。给轨道编号,同时又将轨道分为
一定数目(通常是64)的扇形。0号轨道是硬盘的最外圈1号扇区
第一个扇区(轨道、柱面都从0开始编号而扇区从1开始编号)
有着特殊的作用,它又被称为主引导记录(Master Boot Record, MBR)。
第一轨剩余的扇区常常不使用<footnote><para>有些工具如&man.disklabel.8;
会使用这一区域存储信息,主要是在第二扇区里。</para></footnote>。</para>
</sect1>
<sect1 id="boot-boot0">
<title><literal>boot0</literal>阶段</title>
<indexterm><primary>MBR(主引导记录)</primary></indexterm>
<para>让我们看一下文件<filename>/boot/boot0</filename>。
这是一个仅512字节的小文件。如果在FreeBSD安装过程中选择
<quote>bootmanager</quote>这个文件中的内容将被写入硬盘MBR</para>
<para>如前所述,<literal>INT 0x19</literal>指令装载MBR
也就是<filename>boot0</filename>的内容至内存地址0x7c00。
再看文件<filename>sys/boot/i386/boot0/boot0.s</filename>
可以猜想这里面发生了什么 - 这是引导管理器,
一段由 Robert Nordier书写的令人起敬的程序片段。</para>
<para>MBR里也就是<filename>boot0</filename>里,
从偏移量0x1be开始有一个特殊的结构称为
<emphasis>分区表</emphasis>。其中有4条记录
(称为<emphasis>分区记录</emphasis>)每条记录16字节。
分区记录表示硬盘如何被划分在FreeBSD的术语中
这被称为slice(d)。16字节中有一个标志字节决定这个分区是否可引导。
有仅只能有一个分区可设定这一标志。否则,
<filename>boot0</filename>的代码将拒绝继续执行。</para>
<para>一个分区记录有如下域:</para>
<itemizedlist>
<listitem>
<para>1字节 文件系统类型</para>
</listitem>
<listitem>
<para>1字节 可引导标志</para>
</listitem>
<listitem>
<para>6字节 CHS格式描述符</para>
</listitem>
<listitem>
<para>8字节 LBA格式描述符</para>
</listitem>
</itemizedlist>
<para>一个分区记录描述符包含某一分区在硬盘上的确切位置信息。
LBA和CHS两种描述符指示相同的信息但是指示方式有所不同LBA
(逻辑块寻址Logical Block Addressing)指示分区的起始扇区和分区长度,
而CHS(柱面 磁头 扇区)指示首扇区和末扇区</para>
<para>引导管理器扫描分区表,并在屏幕上显示菜单,以便用户可以
选择用于引导的磁盘和分区。在键盘上按下相应的键后,
<filename>boot0</filename>进行如下动作:</para>
<itemizedlist>
<listitem>
<para>标记选中的分区为可引导,清除以前的可引导标志</para>
</listitem>
<listitem>
<para>记住本次选择的分区以备下次引导时作为缺省项</para>
</listitem>
<listitem>
<para>装载选中分区的第一个扇区,并跳转执行之</para>
</listitem>
</itemizedlist>
<para>什么数据会存在于一个可引导扇区(这里指FreeBSD扇区)的第一扇区里呢?
正如你已经猜到的,那就是<filename>boot2</filename>。</para>
</sect1>
<sect1 id="boot-boot2">
<title><literal>boot2</literal>阶段</title>
<para>也许你想知道,为什么<literal>boot2</literal>是在
<literal>boot0</literal>之后而不是在boot1之后。事实上
也有一个512字节的文件<filename>boot1</filename>存放在目录
<filename>/boot</filename>里,那是用来从一张软盘引导系统的。
从软盘引导时,<filename>boot1</filename>起着
<filename>boot0</filename>对硬盘引导相同的作用:它找到
<filename>boot2</filename>并运行之。</para>
<para>你可能已经看到有一文件<filename>/boot/mbr</filename>。
这是<filename>boot0</filename>的简化版本。
<filename>mbr</filename>中的代码不会显示菜单让用户选择,
而只是简单的引导被标志的分区。</para>
<para>实现<filename>boot2</filename>的代码存放在目录
<filename>sys/boot/i386/boot2/</filename>里,对应的可执行文件在
<filename>/boot</filename>里。在<filename>/boot</filename>里的文件
<filename>boot0</filename>和<filename>boot2</filename>不会在引导过程中使用,
只有<application>boot0cfg</application>这样的工具才会使用它们。
<filename>boot0</filename>的内容应在MBR中才能生效。
<filename>boot2</filename>位于可引导的FreeBSD分区的开始。
这些位置不受文件系统控制,所以它们不可用<application>ls</application>
之类的命令查看。</para>
<para><literal>boot2</literal>的主要任务是装载文件
<filename>/boot/loader</filename>,那是引导过程的第三阶段。
在<literal>boot2</literal>中的代码不能使用诸如
<function>open()</function>和<function>read()</function>
之类的例程函数,因为内核还没有被加载。而应当扫描硬盘,
读取文件系统结构,找到文件<filename>/boot/loader</filename>
用BIOS的功能将它读入内存然后从其入口点开始执行之。</para>
<para>除此之外,<literal>boot2</literal>还可提示用户进行选择,
loader可以从其它磁盘、系统单元、分区装载。</para>
<para><literal>boot2</literal> 的二进制代码用特殊的方式产生:</para>
<programlisting><filename>sys/boot/i386/boot2/Makefile</filename>
boot2: boot2.ldr boot2.bin ${BTX}/btx/btx
btxld -v -E ${ORG2} -f bin -b ${BTX}/btx/btx -l boot2.ldr \
-o boot2.ld -P 1 boot2.bin</programlisting>
<indexterm><primary>BTX</primary></indexterm>
<para>这个Makefile片断表明&man.btxld.8;被用来链接二进制代码。
BTX表示引导扩展器(BooT eXtender)是给程序(称为客户(client)
提供保护模式环境、并与客户程序相链接的一段代码。所以
<literal>boot2</literal>是一个BTX客户使用BTX提供的服务。</para>
<indexterm><primary>linker(链接器)</primary></indexterm>
<para>工具<application>btxld</application>是链接器,
它将两个二进制代码链接在一起。&man.btxld.8;和&man.ld.1;
的区别是<application>ld</application>通常将两个目标文件
链接成一个动态链接库或可执行文件,而<application>btxld</application>
则将一个目标文件与BTX链接起来产生适合于放在分区首部的二进制代码
以实现系统引导。</para>
<para><literal>boot0</literal>执行跳转至BTX的入口点。
然后BTX将处理器切换至保护模式并准备一个简单的环境
然后调用客户。这个环境包括:</para>
<indexterm><primary>virtual 8086 mode(虚拟8086模式)</primary></indexterm>
<itemizedlist>
<listitem><para>虚拟8086模式。这意味着BTX是虚拟8086的监视程序。
实模式指令如pushf, popf, cli, sti, if均可被客户调用。</para></listitem>
<listitem><para>建立中断描述符表(Interrupt Descriptor Table, IDT)
使得所有的硬件中断可被缺省的BIOS程序处理。
建立中断0x30这是系统调用关口。</para></listitem>
<listitem><para>两个系统调用<function>exec</function>和
<function>exit</function>的定义如下:</para>
<programlisting><filename>sys/boot/i386/btx/lib/btxsys.s:</filename>
.set INT_SYS,0x30 # 中断号
#
# System call: exit
#
__exit: xorl %eax,%eax # BTX系统调用0x0
int $INT_SYS #
#
# System call: exec
#
__exec: movl $0x1,%eax # BTX系统调用0x1
int $INT_SYS # </programlisting></listitem>
</itemizedlist>
<para>BTX建立全局描述符表(Global Descriptor Table, GDT):</para>
<programlisting><filename>sys/boot/i386/btx/btx/btx.s:</filename>
gdt: .word 0x0,0x0,0x0,0x0 # 以空为入口
.word 0xffff,0x0,0x9a00,0xcf # SEL_SCODE
.word 0xffff,0x0,0x9200,0xcf # SEL_SDATA
.word 0xffff,0x0,0x9a00,0x0 # SEL_RCODE
.word 0xffff,0x0,0x9200,0x0 # SEL_RDATA
.word 0xffff,MEM_USR,0xfa00,0xcf# SEL_UCODE
.word 0xffff,MEM_USR,0xf200,0xcf# SEL_UDATA
.word _TSSLM,MEM_TSS,0x8900,0x0 # SEL_TSS</programlisting>
<para>客户的代码和数据始于地址MEM_USR(0xa000),选择符(selector)
SEL_UCODE指向客户的数据段。选择符 SEL_UCODE 拥有第3级描述符权限
(Descriptor Privilege Level, DPL),这是最低级权限。但是
<literal>INT 0x30</literal> 指令的处理程序存储于另一个段里,
这个段的选择符SEL_SCODE (supervisor code)由有着管理级权限。
正如代码建立IDT(中断描述符表)时进行的操作那样:</para>
<programlisting> mov $SEL_SCODE,%dh # 段选择符
init.2: shr %bx # 是否处理这个中断?
jnc init.3 # 否
mov %ax,(%di) # 设置处理程序偏移量
mov %dh,0x2(%di) # 设置处理程序选择符
mov %dl,0x5(%di) # 设置 P:DPL:type
add $0x4,%ax # 下一个中断处理程序</programlisting>
<para>所以,当客户调用 <function>__exec()</function>时,代码将被以最高权限执行。
这使得内核可以修改保护模式数据结构,如分页表(page tables)、全局描述符表(GDT)、
中断描述符表(IDT)等。</para>
<para><literal>boot2</literal> 定义了一个重要的数据结构:
<literal>struct bootinfo</literal>。这个结构由
<literal>boot2</literal> 初始化然后被转送到loader之后又被转入内核。
这个结构的部分项目由<literal>boot2</literal>设定其余的由loader设定。
这个结构中的信息包括内核文件名、BIOS提供的硬盘柱面/磁头/扇区数目信息、
BIOS提供的引导设备的驱动器编号可用的物理内存大小<literal>envp</literal>
指针(环境指针)等。定义如下:</para>
<programlisting><filename>/usr/include/machine/bootinfo.h</filename>
struct bootinfo {
u_int32_t bi_version;
u_int32_t bi_kernelname; /* 用一个字节表示 * */
u_int32_t bi_nfs_diskless; /* struct nfs_diskless * */
/* 以上为常备项 */
#define bi_endcommon bi_n_bios_used
u_int32_t bi_n_bios_used;
u_int32_t bi_bios_geom[N_BIOS_GEOM];
u_int32_t bi_size;
u_int8_t bi_memsizes_valid;
u_int8_t bi_bios_dev; /* 引导设备的BIOS单元编号 */
u_int8_t bi_pad[2];
u_int32_t bi_basemem;
u_int32_t bi_extmem;
u_int32_t bi_symtab; /* struct symtab * */
u_int32_t bi_esymtab; /* struct symtab * */
/* 以下项目仅高级bootloader提供 */
u_int32_t bi_kernend; /* 内核空间末端 */
u_int32_t bi_envp; /* 环境 */
u_int32_t bi_modulep; /* 预装载的模块 */
};</programlisting>
<para><literal>boot2</literal> 进入一个循环等待用户输入,然后调用
<function>load()</function>。如果用户不做任何输入,循环将在一段时间后结束,
<function>load()</function> 将会装载缺省文件(<filename>/boot/loader</filename>)。
函数 <function>ino_t lookup(char *filename)</function>和
<function>int xfsread(ino_t inode, void *buf, size_t nbyte)</function>
用来将文件内容读入内存。<filename>/boot/loader</filename>是一个ELF格式二进制文件
不过它的头部被换成了a.out格式中的<literal>struct exec</literal>结构。
<function>load()</function>扫描loader的ELF头部装载<filename>/boot/loader</filename>
至内存,然后跳转至入口执行之:</para>
<programlisting><filename>sys/boot/i386/boot2/boot2.c:</filename>
__exec((caddr_t)addr, RB_BOOTINFO | (opts &amp; RBX_MASK),
MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part),
0, 0, 0, VTOP(&amp;bootinfo));</programlisting>
</sect1>
<sect1 id="boot-loader">
<title><application>loader</application>阶段</title>
<para><application>loader</application>也是一个 BTX 客户,在这里不作详述。
已有一部内容全面的手册 &man.loader.8; 由Mike Smith书写。
比loader更底层的BTX的机理已经在前面讨论过。</para>
<para>loader 的主要任务是引导内核。当内核被装入内存后即被loader调用:</para>
<programlisting><filename>sys/boot/common/boot.c:</filename>
/* 从loader中调用内核中对应的exec程序 */
module_formats[km-&gt;m_loader]-&gt;l_exec(km);</programlisting>
</sect1>
<sect1 id="boot-kernel">
<title>内核初始化</title>
<para>loader跳转至哪里呢那就是内核的入口点。让我们来看一下链接内核的命令</para>
<programlisting><filename>sys/conf/Makefile.i386:</filename>
ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic \
-dynamic-linker /red/herring -o kernel -X locore.o \
&lt;lots of kernel .o files&gt;</programlisting>
<indexterm><primary>ELF(可执行可链接格式)</primary></indexterm>
<para>在这一行中有一些有趣的东西。首先内核是一个ELF动态链接二进制文件
可是动态链接器却是<filename>/red/herring</filename>,一个莫须有的文件。
其次,看一下文件<filename>sys/conf/ldscript.i386</filename>
可以对理解编译内核时<application>ld</application>的选项有一些启发。
阅读最前几行,字符串</para>
<programlisting><filename>sys/conf/ldscript.i386:</filename>
ENTRY(btext)</programlisting>
<para>表示内核的入口点是符号 `btext'。这个符号在<filename>locore.s</filename>
中定义:</para>
<programlisting><filename>sys/i386/i386/locore.s:</filename>
.text
/**********************************************************************
*
* This is where the bootblocks start us, set the ball rolling...
* 入口
*/
NON_GPROF_ENTRY(btext)</programlisting>
<para>首先将寄存器EFLAGS设为一个预定义的值0x00000002
然后初始化所有段寄存器:</para>
<programlisting><filename>sys/i386/i386/locore.s</filename>
/* 不要相信BIOS给出的EFLAGS值 */
pushl $PSL_KERNEL
popfl
/*
* 不要相信BIOS给出的%fs、%gs值。相信引导过程中设定的%cs、%ds、%es、%ss值
*/
mov %ds, %ax
mov %ax, %fs
mov %ax, %gs</programlisting>
<para>btext调用例程<function>recover_bootinfo()</function>,
<function>identify_cpu()</function>,<function>create_pagetables()</function>。
这些例程也定在<filename>locore.s</filename>之中。这些例程的功能如下:</para>
<informaltable frame="none" pgwide="1">
<tgroup cols="2" align="left">
<tbody>
<row>
<entry><function>recover_bootinfo</function></entry>
<entry>这个例程分析由引导程序传送给内核的参数。引导内核有3种方式:
由loader引导(如前所述), 由老式磁盘引导块引导,无盘引导方式。
这个函数决定引导方式,并将结构<literal>struct bootinfo</literal>
存储至内核内存。</entry>
</row>
<row>
<entry><function>identify_cpu</function></entry>
<entry>这个函数侦测CPU类型将结果存放在变量
<varname>_cpu</varname>中。</entry>
</row>
<row>
<entry><function>create_pagetables</function></entry>
<entry>这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容</entry>
</row>
</tbody>
</tgroup>
</informaltable>
<para>下一步是开启VME(如果CPU有这个功能):</para>
<programlisting> testl $CPUID_VME, R(_cpu_feature)
jz 1f
movl %cr4, %eax
orl $CR4_VME, %eax
movl %eax, %cr4</programlisting>
<para>然后,启动分页模式:</para>
<programlisting>/* Now enable paging */
movl R(_IdlePTD), %eax
movl %eax,%cr3 /* load ptd addr into mmu */
movl %cr0,%eax /* get control word */
orl $CR0_PE|CR0_PG,%eax /* enable paging */
movl %eax,%cr0 /* and let's page NOW! */</programlisting>
<para>由于分页模式已经启动,原先的实地址寻址方式随即失效。
随后三行代码用来跳转至虚拟地址:</para>
<programlisting> pushl $begin /* jump to high virtualized address */
ret
/* 现在跳转至KERNBASE那里是操作系统内核被链接后真正的入口 */
begin:</programlisting>
<para>函数<function>init386()</function>被调用;随参数传递的是一个指针,
指向第一个空闲物理页。随后执行<function>mi_startup()</function>。
<function>init386</function>是一个与硬件系统相关的初始化函数,
<function>mi_startup()</function>是个与硬件系统无关的函数
(前缀'mi_'表示Machine Independent不依赖于机器)。
内核不再从<function>mi_startup()</function>里返回;
调用这个函数后,内核完成引导:</para>
<programlisting><filename>sys/i386/i386/locore.s:</filename>
movl physfree, %esi
pushl %esi /* 送给init386()的第一个参数 */
call _init386 /* 设置386芯片使之适应UNIX工作 */
call _mi_startup /* 自动配置硬件,挂接根文件系统,等 */
hlt /* 不再返回到这里! */</programlisting>
<sect2>
<title><function>init386()</function></title>
<para><function>init386()</function>定义在
<filename>sys/i386/i386/machdep.c</filename>中,
它针对Intel 386芯片进行低级初始化。loader已将CPU切换至保护模式。
loader已经建立了最早的任务。<tip><title>译者注</title>
<para>每个"任务"都是与其它“任务”相对独立的执行环境。
任务之间可以分时切换,这为并发进程/线程的实现提供了必要基础。
对于Intel 80x86任务的描述详见Intel公司关于80386 CPU及后续产品的资料
或者在<ulink url="http://www.lib.tsinghua.edu.cn/">清华大学图书馆</ulink>
馆藏记录中用"80386"作为关键词所查找到的系统结构方面的书目。</para></tip>
在这个任务中,内核将继续工作。在讨论其代码前,
我将处理器对保护模式必须完成的一系列准备工作一并列出:</para>
<itemizedlist>
<listitem>
<para>初始化内核的可调整参数,这些参数由引导程序传来</para>
</listitem>
<listitem>
<para>准备GDT(全局描述符表)</para>
</listitem>
<listitem>
<para>准备IDT(中断描述符表)</para>
</listitem>
<listitem>
<para>初始化系统控制台</para>
</listitem>
<listitem>
<para>初始化DDB(内核的点调试器),如果它被编译进内核的话</para>
</listitem>
<listitem>
<para>初始化TSS(任务状态段)</para>
</listitem>
<listitem>
<para>准备LDT(局部描述符表)</para>
</listitem>
<listitem>
<para>建立proc0(0号进程即内核的进程)的pcb(进程控制块)</para>
</listitem>
</itemizedlist>
<indexterm><primary>parameters(参数)</primary></indexterm>
<para><function>init386()</function>首先初始化内核的可调整参数,
这些参数由引导程序传来。先设置环境指针(environment pointer, envp)调用,
再调用<function>init_param1()</function>。
envp指针已由loader存放在结构<literal>bootinfo</literal>中:</para>
<programlisting><filename>sys/i386/i386/machdep.c:</filename>
kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE;
/* 初始化基本可调整项,如hz等 */
init_param1();</programlisting>
<para><function>init_param1()</function>定义在
<filename>sys/kern/subr_param.c</filename>之中。
这个文件里有一些sysctl项还有两个函数
<function>init_param1()</function>和<function>init_param2()</function>。
这两个函数从<function>init386()</function>中调用:</para>
<programlisting><filename>sys/kern/subr_param.c</filename>
hz = HZ;
TUNABLE_INT_FETCH("kern.hz", &amp;hz);</programlisting>
<para>TUNABLE_&lt;typename&gt;_FETCH用来获取环境变量的值:</para>
<programlisting><filename>/usr/src/sys/sys/kernel.h</filename>
#define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var))
</programlisting>
<para>Sysctl<literal>kern.hz</literal>是系统时钟频率。同时,
这些sysctl项被<function>init_param1()</function>设定:
<literal>kern.maxswzone, kern.maxbcache, kern.maxtsiz,
kern.dfldsiz, kern.dflssiz, kern.maxssiz,
kern.sgrowsiz</literal>。</para>
<indexterm><primary>Global Descriptors Table (GDT)(全局描述符表)</primary></indexterm>
<para>然后<function>init386()</function> 准备全局描述符表
(Global Descriptors Table, GDT)。在x86上每个任务都运行在自己的虚拟地址空间里
这个空间由"段址:偏移量"的数对指定。举个例子,当前将要由处理器执行的指令在
CS:EIP那么这条指令的线性虚拟地址就是<quote>代码段虚拟段地址CS</quote> + EIP。
为了简便段起始于虚拟地址0终止于界限4G字节。所以在这个例子中
指令的线性虚拟地址正是EIP的值。段寄存器如CS、DS等是选择符
即全局描述符表中的索引(更精确的说,索引并非选择符的全部,
而是选择符中的INDEX部分)。<tip><title>译者注</title><para>对于80386
选择符有16位INDEX部分是其中的高13位。</para></tip>
FreeBSD的全局描述符表为每个CPU保存着15个选择符:</para>
<programlisting><filename>sys/i386/i386/machdep.c:</filename>
union descriptor gdt[NGDT * MAXCPU]; /* 全局描述符表 */
<filename>sys/i386/include/segments.h:</filename>
/*
* 全局描述符表(GDT)中的入口
*/
#define GNULL_SEL 0 /* 空描述符 */
#define GCODE_SEL 1 /* 内核代码描述符 */
#define GDATA_SEL 2 /* 内核数据描述符 */
#define GPRIV_SEL 3 /* 对称多处理(SMP)每处理器专有数据 */
#define GPROC0_SEL 4 /* Task state process slot zero and up, 任务状态进程 */
#define GLDT_SEL 5 /* 每个进程的局部描述符表 */
#define GUSERLDT_SEL 6 /* 用户自定义的局部描述符表 */
#define GTGATE_SEL 7 /* 进程任务切换关口 */
#define GBIOSLOWMEM_SEL 8 /* BIOS低端内存访问(必须是这第8个入口) */
#define GPANIC_SEL 9 /* 会导致全系统异常中止工作的任务状态 */
#define GBIOSCODE32_SEL 10 /* BIOS接口(32位代码) */
#define GBIOSCODE16_SEL 11 /* BIOS接口(16位代码) */
#define GBIOSDATA_SEL 12 /* BIOS接口(数据) */
#define GBIOSUTIL_SEL 13 /* BIOS接口(工具) */
#define GBIOSARGS_SEL 14 /* BIOS接口(自变量,参数) */</programlisting>
<para>请注意,这些#defines并非选择符本身而只是选择符中的INDEX域
因此它们正是全局描述符表中的索引。
例如,内核代码的选择符(GCODE_SEL)的值为0x08。</para>
<indexterm><primary>Interrupt Descriptor Table (IDT)(中断描述符表)</primary></indexterm>
<para>下一步是初始化中断描述符表(Interrupt Descriptor Table, IDT)。
这张表在发生软件或硬件中断时会被处理器引用。例如,执行系统调用时,
用户应用程序提交<literal>INT 0x80</literal> 指令。这是一个软件中断,
处理器用索引值0x80在中断描述符表中查找记录。这个记录指向处理这个中断的例程。
在这个特定情形中,这是内核的系统调用关口。<tip><title>译者注</title>
<para>Intel 80386支持“调用门”可以使得用户程序只通过一条call指令
就调用内核中的例程。可是FreeBSD并未采用这种机制
也许是因为使用软中断接口可免去动态链接的麻烦吧。另外还有一个附带的好处:
在仿真Linux时当遇到FreeBSD内核不支持的而又并非关键性的系统调用时
内核只会显示一些出错信息,这使得程序能够继续运行;
而不是在真正执行程序之前的初始化过程中就因为动态链接失败而不允许程序运行。</para></tip>
中断描述符表最多可以有256 (0x100)条记录。内核分配NIDT条记录的内存给中断描述符表
这里NIDT=256是最大值</para>
<programlisting><filename>sys/i386/i386/machdep.c:</filename>
static struct gate_descriptor idt0[NIDT];
struct gate_descriptor *idt = &amp;idt0[0]; /* 中断描述符表 */
</programlisting>
<para>每个中断都被设置一个合适的中断处理程序。
系统调用关口<literal>INT 0x80</literal>也是如此:</para>
<programlisting><filename>sys/i386/i386/machdep.c:</filename>
setidt(0x80, &amp;IDTVEC(int0x80_syscall),
SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));</programlisting>
<para>所以当一个用户应用程序提交<literal>INT 0x80</literal>指令时,
全系统的控制权会传递给函数<function>_Xint0x80_syscall</function>
这个函数在内核代码段中,将被以管理员权限执行。</para>
<para>然后控制台和DDB(调试器)被初始化:</para>
<indexterm><primary>DDB</primary></indexterm>
<programlisting><filename>sys/i386/i386/machdep.c:</filename>
cninit();
/* 以下代码可能因为未定义宏DDB而被跳过 */
#ifdef DDB
kdb_init();
if (boothowto &amp; RB_KDB)
Debugger("Boot flags requested debugger");
#endif</programlisting>
<para>任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时
任务状态段用来让硬件存储任务现场信息。</para>
<para>局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符,
指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符:</para>
<programlisting><filename>/usr/include/machine/segments.h</filename>
#define LSYS5CALLS_SEL 0 /* Intel BCS强制要求的 */
#define LSYS5SIGR_SEL 1
#define L43BSDCALLS_SEL 2 /* 尚无 */
#define LUCODE_SEL 3
#define LSOL26CALLS_SEL 4 /* Solaris >=2.6版系统调用关口 */
#define LUDATA_SEL 5
/* separate stack, es,fs,gs sels ? 分别的栈、es、fs、gs选择符 */
/* #define LPOSIXCALLS_SEL 5*/ /* notyet, 尚无 */
#define LBSDICALLS_SEL 16 /* BSDI system call gate, BSDI系统调用关口 */
#define NLDT (LBSDICALLS_SEL + 1)
</programlisting>
<para>然后proc0(0号进程即内核所处的进程)的进程控制块(Process Control Block)
(<literal>struct pcb</literal>)结构被初始化。proc0是一个
<literal>struct proc</literal> 结构,描述了一个内核进程。
内核运行时,该进程总是存在,所以这个结构在内核中被定义为全局变量:</para>
<programlisting><filename>sys/kern/kern_init.c:</filename>
struct proc proc0;</programlisting>
<para>结构<literal>struct pcb</literal>是proc结构的一部分
它定义在<filename>/usr/include/machine/pcb.h</filename>之中,
内含针对i386硬件结构专有的信息如寄存器的值。</para>
</sect2>
<sect2>
<title><function>mi_startup()</function></title>
<para>这个函数用冒泡排序算法,将所有系统初始化对象,然后逐个调用每个对象的入口:</para>
<programlisting><filename>sys/kern/init_main.c:</filename>
for (sipp = sysinit; *sipp; sipp++) {
/* ... 省略 ... */
/* 调用函数 */
(*((*sipp)-&gt;func))((*sipp)-&gt;udata);
/* ... 省略 ... */
}</programlisting>
<para>尽管sysinit框架已经在《FreeBSD开发者手册》中有所描述
我还是在这里讨论一下其内部原理。</para>
<indexterm><primary>sysinit对象</primary></indexterm>
<para>每个系统初始化对象(sysinit对象)通过调用宏建立。
让我们以<literal>announce</literal> sysinit对象为例。
这个对象打印版权信息:</para>
<programlisting><filename>sys/kern/init_main.c:</filename>
static void
print_caddr_t(void *data __unused)
{
printf("%s", (char *)data);
}
SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)</programlisting>
<para>这个对象的子系统标识是SI_SUB_COPYRIGHT(0x0800001)
数值刚好排在SI_SUB_CONSOLE(0x0800000)后面。
所以,版权信息将在控制台初始化之后就被很早的打印出来。</para>
<para>让我们看一看宏<literal>SYSINIT()</literal>到底做了些什么。
它展开成宏<literal>C_SYSINIT()</literal>。
宏<literal>C_SYSINIT()</literal>然后展开成一个静态结构
<literal>struct sysinit</literal>。结构里申明里调用了另一个宏
<literal>DATA_SET</literal>:</para>
<programlisting><filename>/usr/include/sys/kernel.h:</filename>
#define C_SYSINIT(uniquifier, subsystem, order, func, ident) \
static struct sysinit uniquifier ## _sys_init = { \ subsystem, \
order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ##
_sys_init);
#define SYSINIT(uniquifier, subsystem, order, func, ident) \
C_SYSINIT(uniquifier, subsystem, order, \
(sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)</programlisting>
<para>宏<literal>DATA_SET()</literal>展开成<literal>MAKE_SET()</literal>
宏<literal>MAKE_SET()</literal>指向所有隐含的sysinit幻数:</para>
<programlisting><filename>/usr/include/linker_set.h</filename>
#define MAKE_SET(set, sym) \
static void const * const __set_##set##_sym_##sym = &amp;sym; \
__asm(".section .set." #set ",\"aw\""); \
__asm(".long " #sym); \
__asm(".previous")
#endif
#define TEXT_SET(set, sym) MAKE_SET(set, sym)
#define DATA_SET(set, sym) MAKE_SET(set, sym)</programlisting>
<para>回到我们的例子中,经过宏的展开过程,将会产生如下声明:</para>
<programlisting>static struct sysinit announce_sys_init = {
SI_SUB_COPYRIGHT,
SI_ORDER_FIRST,
(sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t,
(void *) copyright
};
static void const *const __set_sysinit_set_sym_announce_sys_init =
&amp;announce_sys_init;
__asm(".section .set.sysinit_set" ",\"aw\"");
__asm(".long " "announce_sys_init");
__asm(".previous");</programlisting>
<para>第一个<literal>__asm</literal>指令在内核可执行文件中建立一个ELF节(section)。
这发生在内核链接的时候。这一节将被命令为<literal>.set.sysinit_set</literal>。
这一节的内容是一个32位值——announce_sys_init结构的地址这个结构正是第二个
<literal>__asm</literal>指令所定义的。第三个<literal>__asm</literal>指令标记节的结束。
如果前面有名字相同的节定义语句,节的内容(那个32位值)将被填加到已存在的节里,
这样就构造出了一个32位指针数组。</para>
<para>用<application>objdump</application>察看一个内核二进制文件,
也许你会注意到里面有这么几个小的节:</para>
<screen>&prompt.user; <userinput>objdump -h /kernel</userinput>
7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2
CONTENTS, ALLOC, LOAD, DATA
8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2
CONTENTS, ALLOC, LOAD, DATA
9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2
CONTENTS, ALLOC, LOAD, DATA
10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2
CONTENTS, ALLOC, LOAD, DATA
11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2
CONTENTS, ALLOC, LOAD, DATA
12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2
CONTENTS, ALLOC, LOAD, DATA</screen>
<para>这一屏信息显示表明节.set.sysinit_set有0x664字节的大小
所以<literal>0x664/sizeof(void *)</literal>个sysinit对象被编译进了内核。
其它节,如<literal>.set.sysctl_set</literal>表示其它链接器集合。</para>
<para>通过定义一个类型为<literal>struct linker_set</literal>的变量,
节<literal>.set.sysinit_set</literal>将被<quote>收集</quote>到那个变量里:</para>
<programlisting><filename>sys/kern/init_main.c:</filename>
extern struct linker_set sysinit_set; /* XXX */</programlisting>
<para><literal>struct linker_set</literal>定义如下:</para>
<programlisting><filename>/usr/include/linker_set.h:</filename>
struct linker_set {
int ls_length;
void *ls_items[1]; /* ls_length个项的数组, 以NULL结尾 */
};</programlisting>
<para><tip><title>译者注</title><para>实际上是说,
用C语言结构体linker_set来表达那个ELF节。</para></tip>
第一项是sysinit对象的数量第二项是一个以NULL结尾的数组
数组中是指向那些对象的指针。</para>
<para>回到对<function>mi_startup()</function>的讨论,
我们清楚了sysinit对象是如何被组织起来的。
函数<function>mi_startup()</function>将它们排序,
并调用每一个对象。最后一个对象是系统调度器:</para>
<programlisting><filename>/usr/include/sys/kernel.h:</filename>
enum sysinit_sub_id {
SI_SUB_DUMMY = 0x0000000, /* 不被执行,仅供链接器使用 */
SI_SUB_DONE = 0x0000001, /* 已被处理*/
SI_SUB_CONSOLE = 0x0800000, /* 控制台*/
SI_SUB_COPYRIGHT = 0x0800001, /* 最早使用控制台的对象 */
...
SI_SUB_RUN_SCHEDULER = 0xfffffff /* 调度器:不返回 */
};</programlisting>
<para>系统调度器sysinit对象定义在文件<filename>sys/vm/vm_glue.c</filename>中,
这个对象的入口点是<function>scheduler()</function>。
这个函数实际上是个无限循环,它表示那个进程标识(PID)为0的进程——swapper进程。
前面提到的proc0结构正是用来描述这个进程。</para>
<para>第一个用户进程是<emphasis>init</emphasis>
由sysinit对象<literal>init</literal>建立:</para>
<programlisting><filename>sys/kern/init_main.c:</filename>
static void
create_init(const void *udata __unused)
{
int error;
int s;
s = splhigh();
error = fork1(&amp;proc0, RFFDG | RFPROC, &amp;initproc);
if (error)
panic("cannot fork init: %d\n", error);
initproc-&gt;p_flag |= P_INMEM | P_SYSTEM;
cpu_set_fork_handler(initproc, start_init, NULL);
remrunqueue(initproc);
splx(s);
}
SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)</programlisting>
<para><function>create_init()</function>通过调用<function>fork1()</function>
分配一个新的进程,但并不将其标记为可运行。当这个新进程被调度器调度执行时,
<function>start_init()</function>将会被调用。
那个函数定义在<filename>init_main.c</filename>中。
它尝试装载并执行二进制代码<filename>init</filename>
先尝试<filename>/sbin/init</filename>,然后是<filename>/sbin/oinit</filename>
<filename>/sbin/init.bak</filename>,最后是<filename>/stand/sysinstall</filename>:</para>
<programlisting><filename>sys/kern/init_main.c:</filename>
static char init_path[MAXPATHLEN] =
#ifdef INIT_PATH
__XSTRING(INIT_PATH);
#else
"/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall";
#endif</programlisting>
</sect2>
</sect1>
</chapter>
<!--
Local Variables:
mode: sgml
sgml-declaration: "../chapter.decl"
sgml-indent-data: t
sgml-omittag: nil
sgml-always-quote-attributes: t
sgml-parent-document: ("../book.sgml" "part" "chapter")
End:
-->

View file

@ -0,0 +1,34 @@
<!--
Creates entities for each chapter in the FreeBSD Architecture
Handbook. Each entity is named chap.foo, where foo is the value
of the id attribute on that chapter, and corresponds to the name of
the directory in which that chapter's .sgml file is stored.
Chapters should be listed in the order in which they are referenced.
Original Revision: 1.21
$FreeBSD$
-->
<!-- Part one - Kernel -->
<!ENTITY chap.boot SYSTEM "boot/chapter.sgml">
<!ENTITY chap.kobj SYSTEM "kobj/chapter.sgml">
<!ENTITY chap.sysinit SYSTEM "sysinit/chapter.sgml">
<!ENTITY chap.locking SYSTEM "locking/chapter.sgml">
<!ENTITY chap.vm SYSTEM "vm/chapter.sgml">
<!ENTITY chap.jail SYSTEM "jail/chapter.sgml">
<!ENTITY chap.mac SYSTEM "mac/chapter.sgml">
<!ENTITY chap.smp SYSTEM "smp/chapter.sgml">
<!-- Part Two - Device Drivers -->
<!ENTITY chap.driverbasics SYSTEM "driverbasics/chapter.sgml">
<!ENTITY chap.isa SYSTEM "isa/chapter.sgml">
<!ENTITY chap.pci SYSTEM "pci/chapter.sgml">
<!ENTITY chap.scsi SYSTEM "scsi/chapter.sgml">
<!ENTITY chap.usb SYSTEM "usb/chapter.sgml">
<!ENTITY chap.newbus SYSTEM "newbus/chapter.sgml">
<!ENTITY chap.snd SYSTEM "sound/chapter.sgml">
<!ENTITY chap.pccard SYSTEM "pccard/chapter.sgml">
<!-- Part three - Appendices -->
<!ENTITY chap.index SYSTEM "index.sgml">

View file

@ -0,0 +1,581 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.34
$FreeBSD$
-->
<chapter id="driverbasics">
<chapterinfo>
<authorgroup>
<author>
<firstname>Murray</firstname>
<surname>Stokely</surname>
<contrib>&cnproj.written.by;</contrib>
</author>
</authorgroup>
<authorgroup>
<author>
<firstname>J&ouml;rg</firstname>
<surname>Wunsch</surname>
<contrib>基础性手册intro(4)</contrib>
</author>
</authorgroup>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>编写 FreeBSD 设备驱动程序</title>
<sect1 id="driverbasics-intro">
<title>简介</title>
<indexterm><primary>device driver(设备驱动程序)</primary></indexterm>
<indexterm><primary>pseudo-device(伪设备)</primary></indexterm>
<para>本章简要介绍了如何为FreeBSD编写设备驱动程序。术语设备在
这儿的上下文中多用于指代系统中硬件相关的东西,如磁盘,打印机,
图形显式器及其键盘。设备驱动程序是操作系统中用于控制特定设备的
软件组件。也有所谓的伪设备,即设备驱动程序用软件模拟设备的行为,
而没有特定的底层硬件。设备驱动程序可以被静态地编译进系统,或者
通过动态内核链接工具kld在需要时加载。</para>
<indexterm><primary>device nodes(设备节点)</primary></indexterm>
<indexterm><primary>MAKEDEV</primary></indexterm>
<para>类&unix;操作系统中的大多数设备都是通过设备节点来访问的,有时也
被称为特殊文件。这些文件在文件系统的层次结构中通常位于
<filename>/dev</filename>目录下。在FreeBSD 5.0-RELEASE以前的
发行版中, 对&man.devfs.5;的支持还没有被集成到FreeBSD中每个设备
节点必须要静态创建,并且独立于相关设备驱动程序的存在。系统中大
多数设备节点是通过运行<command>MAKEDEV</command>创建的。</para>
<para>设备驱动程序可以粗略地分为两类,字符和网络设备驱动程序。</para>
</sect1>
<sect1 id="driverbasics-kld">
<title>动态内核链接工具&mdash;KLD</title>
<indexterm><primary>kernel linking(内核链接)</primary><secondary>dynamic(动态)</secondary></indexterm>
<indexterm><primary>kernel loadable modules (KLD, 内核可装载模块)</primary></indexterm>
<para>kld接口允许系统管理员从运行的系统中动态地添加和删除功能。
这允许设备驱动程序的编写者将他们的新改动加载到运行的内核中,
而不用为了测试新改动而频繁地重启。</para>
<para>kld接口通过下面的特权命令使用
<indexterm><primary>kernel modules(内核模块)</primary><secondary>loading(装载)</secondary></indexterm>
<indexterm><primary>kernel modules(内核模块)</primary><secondary>unloading(卸载)</secondary></indexterm>
<indexterm><primary>kernel modules(内核模块)</primary><secondary>listing(列清单)</secondary></indexterm>
<itemizedlist>
<listitem><simpara><command>kldload</command> - 加载新内核模块
</simpara></listitem>
<listitem><simpara><command>kldunload</command> - 卸载内核模块
</simpara></listitem>
<listitem><simpara><command>kldstat</command> - 列举当前加载的模块
</simpara></listitem>
</itemizedlist>
</para>
<para>内核模块的程序框架</para>
<programlisting>/*
* KLD程序框架
* 受Andrew Reiter在Daemonnews上的文章所启发
*/
#include &lt;sys/types.h&gt;
#include &lt;sys/module.h&gt;
#include &lt;sys/systm.h&gt; /* uprintf */
#include &lt;sys/errno.h&gt;
#include &lt;sys/param.h&gt; /* kernel.h中用到的定义 */
#include &lt;sys/kernel.h&gt; /* 模块初始化中使用的类型 */
/*
* 加载处理函数负责处理KLD的加载和卸载。
*/
static int
skel_loader(struct module *m, int what, void *arg)
{
int err = 0;
switch (what) {
case MOD_LOAD: /* kldload */
uprintf("Skeleton KLD loaded.\n");
break;
case MOD_UNLOAD:
uprintf("Skeleton KLD unloaded.\n");
break;
default:
err = EINVAL;
break;
}
return(err);
}
/* 向内核其余部分声明此模块 */
static moduledata_t skel_mod = {
"skel",
skel_loader,
NULL
};
DECLARE_MODULE(skeleton, skel_mod, SI_SUB_KLD, SI_ORDER_ANY);</programlisting>
<sect2>
<title>Makefile</title>
<para>FreeBSD提供了一个makefile包含文件利用它你可以快速地编译
你附加到内核的东西。</para>
<programlisting>SRCS=skeleton.c
KMOD=skeleton
.include &lt;bsd.kmod.mk&gt;</programlisting>
<para>简单地用这个makefile运行<command>make</command>就能够创建文件
<filename>skeleton.ko</filename>,键入如下命令可以把它加载到内核:
<screen>&prompt.root; <userinput>kldload -v ./skeleton.ko</userinput></screen>
</para>
</sect2>
</sect1>
<sect1 id="driverbasics-access">
<title>访问设备驱动程序</title>
<para>&unix; 提供了一套公共的系统调用供用户的应用程序使用。当用户访问
设备节点时,内核的上层将这些调用分发到相应的设备驱动程序。脚本
<command>/dev/MAKEDEV</command>为你的系统生成了大多数的设备节点,
但如果你正在开发你自己的驱动程序,可能需要用
<command>mknod</command>创建你自己的设备节点。
</para>
<sect2>
<title>创建静态设备节点</title>
<indexterm><primary>device nodes(设备节点)</primary><secondary>static(静态)</secondary></indexterm>
<indexterm><primary>mknod</primary></indexterm>
<para><command>mknod</command>命令需要四个参数来创建设备节点。
你必须指定设备节点的名字,设备的类型,设备的主号码和设备的从号码。
</para>
</sect2>
<sect2>
<title>动态设备节点</title>
<indexterm><primary>device nodes(设备节点)</primary><secondary>dynamic(动态)</secondary></indexterm>
<indexterm><primary>devfs</primary></indexterm>
<para>设备文件系统或者说devfs在全局文件系统名字空间中提供对
内核设备名字空间的访问。这消除了由于有设备驱动程序而没有静态
设备节点,或者有设备节点而没有安装设备驱动程序而带来的潜在问题。
Devfs仍在进展中但已经能够工作得相当好了。</para>
</sect2>
</sect1>
<sect1 id="driverbasics-char">
<title>字符设备</title>
<indexterm><primary>character devices(字符设备)</primary></indexterm>
<para>字符设备驱动程序直接从用户进程传输数据,或传输数据到用户进程。
这是最普通的一类设备驱动程序,源码树中有大量的简单例子。</para>
<para>这个简单的伪设备例子会记住你写给它的任何值,并且当你读取它的时候
会将这些值返回给你。下面显示了两个版本,一个适用于&os;&nbsp;4.X
一个适用于&os;&nbsp;5.X。</para>
<example>
<title>适用于&os;&nbsp;4.X的回显伪设备驱动程序实例</title>
<programlisting>/*
* 简单echo伪设备KLD
*
* Murray Stokely
*/
#define MIN(a,b) (((a) &lt; (b)) ? (a) : (b))
#include &lt;sys/types.h&gt;
#include &lt;sys/module.h&gt;
#include &lt;sys/systm.h&gt; /* uprintf */
#include &lt;sys/errno.h&gt;
#include &lt;sys/param.h&gt; /* kernel.h中用到的定义 */
#include &lt;sys/kernel.h&gt; /* 模块初始化中使用的类型 */
#include &lt;sys/conf.h&gt; /* cdevsw结构 */
#include &lt;sys/uio.h&gt; /* uio结构 */
#include &lt;sys/malloc.h&gt;
#define BUFFERSIZE 256
/* 函数原型 */
d_open_t echo_open;
d_close_t echo_close;
d_read_t echo_read;
d_write_t echo_write;
/* 字符设备入口点 */
static struct cdevsw echo_cdevsw = {
echo_open,
echo_close,
echo_read,
echo_write,
noioctl,
nopoll,
nommap,
nostrategy,
"echo",
33, /* 为lkms保留 - /usr/src/sys/conf/majors */
nodump,
nopsize,
D_TTY,
-1
};
struct s_echo {
char msg[BUFFERSIZE];
int len;
} t_echo;
/* 变量 */
static dev_t sdev;
static int len;
static int count;
static t_echo *echomsg;
MALLOC_DECLARE(M_ECHOBUF);
MALLOC_DEFINE(M_ECHOBUF, "echobuffer", "buffer for echo module");
/*
* 这个函数被kld[un]load(2)系统调用来调用,
* 以决定加载和卸载模块时需要采取的动作。
*/
static int
echo_loader(struct module *m, int what, void *arg)
{
int err = 0;
switch (what) {
case MOD_LOAD: /* kldload */
sdev = make_dev(<literal>&amp;</literal>echo_cdevsw,
0,
UID_ROOT,
GID_WHEEL,
0600,
"echo");
/* kmalloc分配供驱动程序使用的内存 */
MALLOC(echomsg, t_echo *, sizeof(t_echo), M_ECHOBUF, M_WAITOK);
printf("Echo device loaded.\n");
break;
case MOD_UNLOAD:
destroy_dev(sdev);
FREE(echomsg,M_ECHOBUF);
printf("Echo device unloaded.\n");
break;
default:
err = EINVAL;
break;
}
return(err);
}
int
echo_open(dev_t dev, int oflags, int devtype, struct proc *p)
{
int err = 0;
uprintf("Opened device \"echo\" successfully.\n");
return(err);
}
int
echo_close(dev_t dev, int fflag, int devtype, struct proc *p)
{
uprintf("Closing device \"echo.\"\n");
return(0);
}
/*
* read函数接受由echo_write()存储的buf并将其返回到用户空间
* 以供其他函数访问。
* uio(9)
*/
int
echo_read(dev_t dev, struct uio *uio, int ioflag)
{
int err = 0;
int amt;
/*
* 这个读操作有多大?
* 与用户请求的大小一样,或者等于剩余数据的大小。
*/
amt = MIN(uio-&gt;uio_resid, (echomsg-&gt;len - uio-&gt;uio_offset &gt; 0) ?
echomsg-&gt;len - uio-&gt;uio_offset : 0);
if ((err = uiomove(echomsg-&gt;msg + uio-&gt;uio_offset,amt,uio)) != 0) {
uprintf("uiomove failed!\n");
}
return(err);
}
/*
* echo_write接受一个字符串并将它保存到缓冲区用于以后的访问。
*/
int
echo_write(dev_t dev, struct uio *uio, int ioflag)
{
int err = 0;
/* 将字符串从用户空间的内存拷贝到内核空间 */
err = copyin(uio-&gt;uio_iov-&gt;iov_base, echomsg-&gt;msg,
MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE - 1));
/* 现在需要以null结束字符串并记录长度 */
*(echomsg-&gt;msg + MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE - 1)) = 0;
echomsg-&gt;len = MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE);
if (err != 0) {
uprintf("Write failed: bad address!\n");
}
count++;
return(err);
}
DEV_MODULE(echo,echo_loader,NULL);</programlisting>
</example>
<example>
<title>适用于&os;&nbsp;5.X回显伪设备驱动程序实例</title>
<programlisting>/*
* 简单echo伪设备 KLD
*
* Murray Stokely
*
* 此代码由S&oslash;ren (Xride) Straarup转换到5.X
*/
#include &lt;sys/types.h&gt;
#include &lt;sys/module.h&gt;
#include &lt;sys/systm.h&gt; /* uprintf */
#include &lt;sys/errno.h&gt;
#include &lt;sys/param.h&gt; /* kernel.h中用到的定义 */
#include &lt;sys/kernel.h&gt; /* 模块初始化中使用的类型 */
#include &lt;sys/conf.h&gt; /* cdevsw结构 */
#include &lt;sys/uio.h&gt; /* uio结构 */
#include &lt;sys/malloc.h&gt;
#define BUFFERSIZE 256
/* 函数原型 */
static d_open_t echo_open;
static d_close_t echo_close;
static d_read_t echo_read;
static d_write_t echo_write;
/* 字符设备入口点 */
static struct cdevsw echo_cdevsw = {
.d_version = D_VERSION,
.d_open = echo_open,
.d_close = echo_close,
.d_read = echo_read,
.d_write = echo_write,
.d_name = "echo",
};
typedef struct s_echo {
char msg[BUFFERSIZE];
int len;
} t_echo;
/* 变量 */
static struct cdev *echo_dev;
static int count;
static t_echo *echomsg;
MALLOC_DECLARE(M_ECHOBUF);
MALLOC_DEFINE(M_ECHOBUF, "echobuffer", "buffer for echo module");
/*
* 这个函数被kld[un]load(2)系统调用来调用,
* 以决定加载和卸载模块时需要采取的动作.
*/
static int
echo_loader(struct module *m, int what, void *arg)
{
int err = 0;
switch (what) {
case MOD_LOAD: /* kldload */
echo_dev = make_dev(<literal>&amp;</literal>echo_cdevsw,
0,
UID_ROOT,
GID_WHEEL,
0600,
"echo");
/* kmalloc分配供驱动程序使用的内存 */
echomsg = malloc(sizeof(t_echo), M_ECHOBUF, M_WAITOK);
printf("Echo device loaded.\n");
break;
case MOD_UNLOAD:
destroy_dev(echo_dev);
free(echomsg, M_ECHOBUF);
printf("Echo device unloaded.\n");
break;
default:
err = EINVAL;
break;
}
return(err);
}
static int
echo_open(struct cdev *dev, int oflags, int devtype, struct thread *p)
{
int err = 0;
uprintf("Opened device \"echo\" successfully.\n");
return(err);
}
static int
echo_close(struct cdev *dev, int fflag, int devtype, struct thread *p)
{
uprintf("Closing device \"echo.\"\n");
return(0);
}
/*
* read函数接受由echo_write()存储的buf并将其返回到用户空间
* 以供其他函数访问。
* uio(9)
*/
static int
echo_read(struct cdev *dev, struct uio *uio, int ioflag)
{
int err = 0;
int amt;
/*
* 这个读操作有多大?
* 等于用户请求的大小,或者等于剩余数据的大小。
*/
amt = MIN(uio-&gt;uio_resid, (echomsg-&gt;len - uio-&gt;uio_offset &gt; 0) ?
echomsg-&gt;len - uio-&gt;uio_offset : 0);
if ((err = uiomove(echomsg-&gt;msg + uio-&gt;uio_offset,amt,uio)) != 0) {
uprintf("uiomove failed!\n");
}
return(err);
}
/*
* echo_write接受一个字符串并将它保存到缓冲区, 用于以后的访问.
*/
static int
echo_write(struct cdev *dev, struct uio *uio, int ioflag)
{
int err = 0;
/* 将字符串从用户空间的内存拷贝到内核空间 */
err = copyin(uio-&gt;uio_iov-&gt;iov_base, echomsg-&gt;msg,
MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE));
/* 现在需要以null结束字符串并记录长度 */
*(echomsg-&gt;msg + MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE)) = 0;
echomsg-&gt;len = MIN(uio-&gt;uio_iov-&gt;iov_len,BUFFERSIZE);
if (err != 0) {
uprintf("Write failed: bad address!\n");
}
count++;
return(err);
}
DEV_MODULE(echo,echo_loader,NULL);</programlisting>
</example>
<para>在&os;&nbsp;4.X上安装此驱动程序你将首先需要用如下命令在
你的文件系统上创建一个节点:</para>
<screen>&prompt.root; <userinput>mknod /dev/echo c 33 0</userinput></screen>
<para>驱动程序被加载后,你应该能够键入一些东西,如:</para>
<screen>&prompt.root; <userinput>echo -n "Test Data" &gt; /dev/echo</userinput>
&prompt.root; <userinput>cat /dev/echo</userinput>
Test Data</screen>
<para>真正的硬件设备在下一章描述。</para>
<para>补充资源
<itemizedlist>
<listitem><simpara><ulink
url="http://ezine.daemonnews.org/200010/blueprints.html">Dynamic
Kernel Linker (KLD) Facility Programming Tutorial</ulink> -
<ulink url="http://www.daemonnews.org/">Daemonnews</ulink> October 2000</simpara></listitem>
<listitem><simpara><ulink
url="http://ezine.daemonnews.org/200007/newbus-intro.html">How
to Write Kernel Drivers with NEWBUS</ulink> - <ulink
url="http://www.daemonnews.org/">Daemonnews</ulink> July
2000</simpara></listitem>
</itemizedlist>
</para>
</sect1>
<sect1 id="driverbasics-block">
<title>块设备(消亡中)</title>
<indexterm><primary>block devices(块设备)</primary></indexterm>
<para>其他&unix;系统支持另一类型的磁盘设备,称为块设备。块设备是内核
为它们提供缓冲的磁盘设备。这种缓冲使得块设备几乎没有用,或者说非常
不可靠。缓冲会重新安排写操作的次序,使得应用程序丧失了在任何时刻及时
知道准确的磁盘内容的能力。这导致对磁盘数据结构(文件系统,数据库等)的
可预测的和可靠的崩溃恢复成为不可能。由于写操作被延迟,内核无法向应用
程序报告哪个特定的写操作遇到了写错误,这又进一步增加了一致性问题。
由于这个原因,真正的应用程序从不依赖于块设备,事实上,几乎所有访问
磁盘的应用程序都尽力指定总是使用字符(或<quote>raw</quote>)设备。
由于实现将每个磁盘(分区)同具有不同语义的两个设备混为一谈,从而致使
相关内核代码极大地复杂化作为推进磁盘I/O基础结构现代化的一部分&os;
抛弃了对带缓冲的磁盘设备的支持。</para>
</sect1>
<sect1 id="driverbasics-net">
<title>网络设备驱动程序</title>
<indexterm><primary>network devices(网络设备)</primary></indexterm>
<para>访问网络设备的驱动程序不需要使用设备节点。选择哪个驱动程序是
基于内核内部的其他决定而不是调用open(),对网络设备的使用通常由
系统调用socket(2)引入。</para>
<para>更多细节, 请参见 ifnet(9) 联机手册、 回环设备的源代码,
以及 Bill Paul 撰写的网络驱动程序。</para>
</sect1>
</chapter>
<!--
Local Variables:
mode: sgml
sgml-declaration: "../chapter.decl"
sgml-indent-data: t
sgml-omittag: nil
sgml-always-quote-attributes: t
sgml-parent-document: ("../book.sgml" "part" "chapter")
End:
-->

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,540 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.17
$FreeBSD$
-->
<chapter id="jail">
<chapterinfo>
<author>
<firstname>Evan</firstname>
<surname>Sarmiento</surname>
<affiliation>
<address><email>evms@cs.bu.edu</email></address>
</affiliation>
</author>
<copyright>
<year>2001</year>
<holder role="mailto:evms@cs.bu.edu">Evan Sarmiento</holder>
</copyright>
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>Jail子系统</title>
<indexterm><primary>security(安全)</primary></indexterm>
<indexterm><primary>Jail(囚禁)</primary></indexterm>
<indexterm><primary>root(根用户,管理员用户)</primary></indexterm>
<para>在大多数&unix;系统中用户root是万能的。这也就增加了许多危险。
如果一个攻击者获得了一个系统中的root就可以在他的指尖掌握系统中所有的功能。
在FreeBSD里有一些sysctl项削弱了root的权限
这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级。
另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是&man.jail.8;。
<application>Jail</application>将一个运行环境的文件树根切换到某一特定位置,
并且对这样环境中叉分生成的进程做出限制。例如,
一个被jail控制的进程不能影响这个jail之外的进程、不能使用一些特定的系统调用
也就不能对主计算机造成破坏。<tip><title>译者注</title>
<para>英文单词“jail”的中文意思是“囚禁、监禁”。</para></tip></para>
<para><application>Jail</application>已经成为一种新型的安全模型。
人们可以在jail中运行各种可能很脆弱的服务器程序如Apache、BIND和sendmail。
这样一来,即使有攻击者取得了<application>Jail</application>中的root
这最多让人们皱皱眉头,而不会使人们惊慌失措。
本文聚焦<application>Jail</application>的内部原理(源代码)
同时对于改进现役的jail代码提出建议。如果你正在寻找设置
<application>Jail</application>的指南性文档,我建议你阅读我的另一篇文章,
发表在Sys Admin Magazine, May 2001,
《Securing FreeBSD using <application>Jail</application>》。</para>
<sect1 id="jail-arch">
<title>Jail的系统结构</title>
<para><application>Jail</application>由两部分组成:用户级程序,
也就是&man.jail.8;还有在内核中Jail的实现代码&man.jail.2;
系统调用和相关的约束。我将讨论用户级程序和Jail在内核中的实现原理。</para>
<sect2>
<title>用户级代码</title>
<indexterm><primary>Jail(囚禁)</primary>
<secondary>userland program(用户级程序)</secondary></indexterm>
<para>jail的用户级源代码在<filename>/usr/src/usr.sbin/jail</filename>
由一个文件<filename>jail.c</filename>组成。这个程序有这些参数jail的路径
主机名IP地址还有需要执行的命令。</para>
<sect3>
<title>数据结构</title>
<para>在<filename>jail.c</filename>中,我将最先关注的是一个重要结构体
<literal>struct jail j</literal>的申明;结构类型的申明包含在
<filename>/usr/include/sys/jail.h</filename>之中。</para>
<para>jail结构的定义是</para>
<programlisting><filename>/usr/include/sys/jail.h</filename>:
struct jail {
u_int32_t version;
char *path;
char *hostname;
u_int32_t ip_number;
};</programlisting>
<para>正如你能看见的,传送给命令&man.jail.8;的每个参数都在这里有对应的一项。
事实上,当命令&man.jail.8;被执行时,这些参数才由命令行真正传入:</para>
<programlisting><filename>/usr/src/usr.sbin/jail.c</filename>
j.version = 0;
j.path = argv[1];
j.hostname = argv[2];</programlisting>
</sect3>
<sect3>
<title>网络</title>
<para>传给&man.jail.8;的参数中有一个是IP地址。这是在网络上访问jail时的地址。
&man.jail.8;将IP地址翻译成网络字节顺序并存入j (jail类型的结构体)。</para>
<programlisting><filename>/usr/src/usr.sbin/jail/jail.c</filename>:
struct in.addr in;
...
i = inet_aton(argv[3], <![CDATA[&in]]>);
...
j.ip_number = ntohl(in.s_addr);</programlisting>
<para>函数<citerefentry><refentrytitle>inet_aton</refentrytitle>
<manvolnum>3</manvolnum></citerefentry>“将指定的字符串当成一个Internet地址
并将其转存到指定的结构体中”。inet_aton设定了结构体in
之后in中的内容再用<function>ntohl()</function>翻译成主机字节顺序。</para>
</sect3>
<sect3>
<title>囚禁进程</title>
<para>最后用户级程序囚禁进程执行指定的命令。现在Jail自身变成了一个被囚禁的进程
叉分生成一个子进程。这个子进程用&man.execv.3;执行用户指定的命令。</para>
<programlisting><filename>/usr/src/sys/usr.sbin/jail/jail.c</filename>
i = jail(<![CDATA[&j]]>);
...
i = execv(argv[4], argv + 4);</programlisting>
<para>正如你能看见的函数jail被调用参数是结构体jail中被填入数据项
而如前所述,这些数据项又来自&man.jail.8;的命令行参数。
最后执行了用户指定的命令。下面我将开始讨论Jail在内核中的实现。</para>
</sect3>
</sect2>
<sect2>
<title>相关的内核源代码</title>
<indexterm><primary>Jail(囚禁)</primary>
<secondary>kernel architecture(内核系统结构)</secondary></indexterm>
<para>现在我们来看文件<filename>/usr/src/sys/kern/kern_jail.c</filename>。
在这里定义了jail的系统调用、相关的sysctl项还有网络函数。</para>
<sect3>
<title>sysctl项</title>
<indexterm><primary>sysctl(系统控制项)</primary></indexterm>
<para>在<filename>kern_jail.c</filename>里定义了如下sysctl项:</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c:</filename>
int jail_set_hostname_allowed = 1;
SYSCTL_INT(_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW,
<![CDATA[&jail]]>_set_hostname_allowed, 0,
"Processes in jail can set their hostnames");
/* Jail中的进程可设定自身的主机名 */
int jail_socket_unixiproute_only = 1;
SYSCTL_INT(_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW,
<![CDATA[&jail]]>_socket_unixiproute_only, 0,
"Processes in jail are limited to creating &unix;/IPv4/route sockets only
");
/* Jail中的进程被限制只能建立UNIX套接字、IPv4套接字、路由套接字 */
int jail_sysvipc_allowed = 0;
SYSCTL_INT(_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW,
<![CDATA[&jail]]>_sysvipc_allowed, 0,
"Processes in jail can use System V IPC primitives");
/* Jail中的进程可以使用System V进程间通讯原语 */</programlisting>
<para>这些sysctl项中的每一个都可以用命令sysctl访问。在整个内核中
这些sysctl项按名称标识。例如上述第一个sysctl项的名字是
<literal>jail.set.hostname.allowed</literal>.</para>
</sect3>
<sect3>
<title>&man.jail.2;系统调用</title>
<para>像所有的系统调用一样,系统调用&man.jail.2;带有两个参数,
<literal>struct proc *p</literal>和<literal>struct jail_args *uap</literal>。
<literal>p</literal>是一个指向proc结构体的指针描述调用这个系统调用的进程。
此时uap指向一个结构体这个结构体指定了从用户级程序
<filename>jail.c</filename>要传送给&man.jail.2;的参数。
在前面我讲述用户级程序时你已经看见一个jail结构体被作为参数传送给系统调用
&man.jail.2;。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c:</filename>
int
jail(p, uap)
struct proc *p;
struct jail_args /* {
syscallarg(struct jail *) jail;
} */ *uap;</programlisting>
<para><literal>uap-&gt;jail</literal>包含了传递给系统调用的jail结构体。
然后,系统调用使用<literal>copyin()</literal>将jail结构体复制到内核内存空间中。
<literal>copyin()</literal>有三个参数:要复制进内核内存空间的数据
<literal>uap-&gt;jail</literal>,在内核内存空间存放数据的<literal>j</literal>
以及数据的大小。Jail结构体<literal>uap-&gt;jail</literal>被复制进内核内存空间,
并被存放在另一个jail结构体<literal>j</literal>里。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c: </filename>
error = copyin(uap-&gt;jail, <![CDATA[&j]]>, sizeof j);</programlisting>
<para>在jail.h中定义了另一个重要的结构体型prison(<literal>pr</literal>)。
结构体prison只被用在内核空间中。系统调用&man.jail.2;把jail结构体中的
所有内容复制到prison结构体中。这里是prison结构体的定义</para>
<programlisting><filename>/usr/include/sys/jail.h</filename>:
struct prison {
int pr_ref;
char pr_host[MAXHOSTNAMELEN];
u_int32_t pr_ip;
void *pr_linux;
};</programlisting>
<para>然后系统调用jail()为一个prison结构体分配一块内存
由一个指针指向这块内存,再将数据复制进去。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename>:
MALLOC(pr, struct prison *, sizeof *pr , M_PRISON, M_WAITOK);
bzero((caddr_t)pr, sizeof *pr);
error = copyinstr(j.hostname, <![CDATA[&pr-&gt;pr_host]]>, sizeof pr-&gt;pr_host, 0);
if (error)
goto bail;</programlisting>
<indexterm><primary>chroot(切换逻辑根)</primary></indexterm>
<para>最后系统调用jail将切换文件系统逻辑根(chroot)至指定路径。
函数chroot()有两个参数。第一个是p, 表示调用它的进程,
第二个是指向结构体chroot的指针。结构体chroot包含了新的文件系统逻辑根。
正如你看见的结构体jail中指定的路径被复制到结构体chroot中
并在后续操作中被使用。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename>:
ca.path = j.path;
error = chroot(p, <![CDATA[&ca]]>);</programlisting>
<para>这随后的三行在源代码中非常重要,因为他们指定了内核如何将一个
进程判别为被囚禁的进程。在&unix;系统中每一个进程都由它自己的proc结构体描述。
你可以在<filename>/usr/include/sys/proc.h</filename>中看见整个proc结构体。
例如在任何系统调用中参数p实际上是个指向进程的proc结构体的指针
正如前面所说的那样。结构体proc包含的成员可以描述所有者的身份
(<literal>p_cred</literal>),进程资源限制(<literal>p_limit</literal>)
等等。在进程结构体的定义中还有一个指向prison结构体的指针
(<literal>p_prison</literal>)。</para>
<programlisting><filename>/usr/include/sys/proc.h: </filename>
struct proc {
...
struct prison *p_prison;
...
};</programlisting>
<para>在<filename>kern_jail.c</filename>中函数然后复制pr结构体到
<literal>p-&gt;p_prison</literal>中。pr结构体里填充了来自原始jail
结构体中的所有信息。随后,将<literal>p-&gt;p_flag</literal>与恒量
<literal>P_JAILED</literal>进行按位或运算,
这指明调用进程现在被认为是被囚禁的。每个进程的父进程,
都曾在Jail中进行了叉分(fork)。这父进程正是程序jail本身
它调用了&man.jail.2;系统调用。当其它程序通过execve()执行时,
就从父进程那里继承proc结构体因而其<literal>p-&gt;p_flag</literal>
中Jail的标志位被置位并且<literal>p-&gt;p_prison</literal>
结构体中被填有内容。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename>
p-&gt;p.prison = pr;
p-&gt;p.flag |= P.JAILED;</programlisting>
<para>当一个进程被从其父进程叉分来的时候,系统调用&man.fork.2;
将用不同的方式处理被囚禁的进程。在系统调用fork中用到两个指向
<literal>proc</literal>结构体的指针<literal>p1</literal>和
<literal>p2</literal>。<literal>p1</literal>指向父进程的
<literal>proc</literal>结构体,<literal>p2</literal>
指向子进程的尚未被填充的<literal>proc</literal>结构体。
在结构体间复制完所有相关数据之后,&man.fork.2;
检查<literal>p2</literal>指向的结构体成员
<literal>p_prison</literal>是否已被填充。如果已被填充,
就将<literal>pr.ref</literal>的值增加1
并给子进程的<literal>p_flag</literal>设上Jail标记。</para>
<programlisting><filename>/usr/src/sys/kern/kern_fork.c</filename>:
if (p2-&gt;p_prison) {
p2-&gt;p_prison-&gt;pr_ref++;
p2-&gt;p_flag |= P_JAILED;
}</programlisting>
</sect3>
</sect2>
</sect1>
<sect1 id="jail-restrictions">
<title>系统对被囚禁程序的限制</title>
<para>在整个内核中,有一系列对被囚禁程序的约束措施。
通常,这些约束只对被囚禁的程序有效。如果这些程序试图突破这些约束,
相关的函数将出错返回。例如:</para>
<programlisting>if (p-&gt;p_prison)
return EPERM;</programlisting>
<sect2>
<title>SysV进程间通信(IPC)</title>
<indexterm><primary>System V IPC(系统V进程间通信)</primary></indexterm>
<para>System V进程间通信(IPC)是通过消息实现的。
每个进程都可以向其它进程发送消息,告诉对方该做什么。
处理消息的函数是:<literal>msgsys</literal>, <literal>msgctl</literal>,
<literal>msgget</literal>, <literal>msgsend</literal>和
<literal>msgrcv</literal>。前面我提到一些sysctl项开关可以影响Jail的行为
其中有一个是<literal>jail_sysvipc_allowed</literal>。在大多数系统上,
这个sysctl项被设成0。如此它被设为1它将使Jail完全失去意义
在Jail中有权限的进程就可以影响Jail环境外的进程了。
消息与信号的区别是:消息仅由一个信号编号组成。</para>
<para><filename>/usr/src/sys/kern/sysv_msg.c</filename>:</para>
<itemizedlist>
<listitem><para>&man.msgget.3;: msgget返回(也可能创建)一个消息描述符,
以指派一个在其它系统调用中使用的消息队列。</para></listitem>
<listitem><para>&man.msgctl.3;: 通过这个函数,
一个进程可以查询一个消息描述符的状态。</para></listitem>
<listitem><para>&man.msgsnd.3;: msgsnd向一个进程发送一条消息。</para></listitem>
<listitem><para>&man.msgrcv.3;: 进程用这个函数接收消息。</para></listitem>
</itemizedlist>
<para>在这些系统调用的代码中,都有这样一个条件判断:</para>
<programlisting><filename>/usr/src/sys/kern/sysv msg.c</filename>:
if (!jail.sysvipc.allowed &amp;&amp; p-&gt;p_prison != NULL)
return (ENOSYS);</programlisting>
<indexterm><primary>semaphores(信号量)</primary></indexterm>
<para>信号量系统调用使得进程可以通过一系列操作实现同步。
信号量为进程锁定资源提供了又一种途径。
然而,进程将为正在被使用的信号量进入等待状态,一直休眠到资源被释放。
在Jail中如下的信号量系统调用将会失效: <literal>semsys</literal>,
<literal>semget</literal>, <literal>semctl</literal>和
<literal>semop</literal>。</para>
<para><filename>/usr/src/sys/kern/sysv_sem.c</filename>:</para>
<itemizedlist>
<listitem>
<para>&man.semctl.2;<literal>(id, num, cmd, arg)</literal>:
Semctl对在信号量队列中用id标识的信号量执行cmd指定的命令。</para></listitem>
<listitem>
<para>&man.semget.2;<literal>(key, nsems, flag)</literal>:
Semget建立一个对应于key的信号量数组</para>
<para><literal>参数Key和flag与msgget()的意义相同。</literal></para></listitem>
<listitem><para>&man.semop.2;<literal>(id, ops, num)</literal>:
Semop在结构体数组ops中对id标识的信号量完成一系列操作。</para></listitem>
</itemizedlist>
<indexterm><primary>shared memory(共享内存)</primary></indexterm>
<para>System V IPC使进程间可以共享内存。进程之间可以通过它们虚拟地址空间
的共享部分以及相关数据读写操作直接通讯。这些系统调用在Jail环境中将会失效:
<literal>shmdt, shmat, oshmctl, shmctl, shmget</literal>,
和<literal>shmsys</literal>。</para>
<para><filename>/usr/src/sys/kern/sysv shm.c</filename>:</para>
<itemizedlist>
<listitem><para>&man.shmctl.2;<literal>(id, cmd, buf)</literal>:
shmctl对id标识的共享内存区域做各种各样的控制。</para></listitem>
<listitem><para>&man.shmget.2;<literal>(key, size,
flag)</literal>: shmget建立/打开size字节的共享内存区域。</para></listitem>
<listitem><para>&man.shmat.2;<literal>(id, addr, flag)</literal>:
shmat将id标识的共享内存区域指派到进程的地址空间里。</para></listitem>
<listitem><para>&man.shmdt.2;<literal>(addr)</literal>:
shmdt取消共享内存区域的地址指派。</para></listitem>
</itemizedlist>
</sect2>
<sect2>
<title>套接字</title>
<indexterm><primary>sockets(套接字)</primary></indexterm>
<para>Jail以一种特殊的方式处理&man.socket.2;系统调用和相关的低级套接字函数。
为了决定一个套接字是否允许被创建它先检查sysctl项
<literal>jail.socket.unixiproute.only</literal>是否被设置为1。
如果被设为1套接字建立时将只能指定这些协议族
<literal>PF_LOCAL</literal>, <literal>PF_INET</literal>,
<literal>PF_ROUTE</literal>。否则,&man.socket.2;将会返回出错。</para>
<programlisting><filename>/usr/src/sys/kern/uipc_socket.c</filename>:
int socreate(dom, aso, type, proto, p)
...
register struct protosw *prp;
...
{
if (p-&gt;p_prison &amp;&amp; jail_socket_unixiproute_only &amp;&amp;
prp-&gt;pr_domain-&gt;dom_family != PR_LOCAL &amp;&amp; prp-&gt;pr_domain-&gt;dom_family != PF_INET
&amp;&amp; prp-&gt;pr_domain-&gt;dom_family != PF_ROUTE)
return (EPROTONOSUPPORT);
...
}</programlisting>
</sect2>
<sect2>
<title>Berkeley包过滤器</title>
<indexterm><primary>Berkeley Packet Filter(伯克利包过滤器)</primary></indexterm>
<indexterm><primary>data link layer(数据链路层)</primary></indexterm>
<para>Berkeley包过滤器提供了一个与协议无关的直接通向数据链路层的低级接口。
函数<literal>bpfopen()</literal>打开一个以太网设备。
代码中有一个条件判断禁止所有被囚禁的进程打开Berkeley包过滤器设备。</para>
<programlisting><filename>/usr/src/sys/net/bpf.c</filename>:
static int bpfopen(dev, flags, fmt, p)
...
{
if (p-&gt;p_prison)
return (EPERM);
...
}</programlisting>
</sect2>
<sect2>
<title>网络协议</title>
<indexterm><primary>protocols(协议)</primary></indexterm>
<para>网络协议TCP, UDP, IP和ICMP很常见。IP和ICMP处于同一协议层次第二层
网络层。当参数<literal>nam</literal>被设置时,
有一些限制措施会防止被囚禁的程序绑定到一些网络接口上。
nam是一个指向sockaddr结构体的指针描述可以绑定服务的地址。
一个更确切的定义sockaddr“是一个模板包含了地址的标识符和地址的长度”[2]。
在函数中,<literal>pcbbind</literal>, <literal>sin</literal>
里有一个指向sockaddr的指针。结构体包含了套接字可以绑定的端口、地址、
长度、协议族。这就禁止了在Jail中的进程指定协议族。</para>
<programlisting><filename>/usr/src/sys/kern/netinet/in_pcb.c</filename>:
int in.pcbbind(int, nam, p)
...
struct sockaddr *nam;
struct proc *p;
{
...
struct sockaddr.in *sin;
...
if (nam) {
sin = (struct sockaddr.in *)nam;
...
if (sin-&gt;sin_addr.s_addr != INADDR_ANY)
if (prison.ip(p, 0, <![CDATA[&sin]]>-&gt;sin.addr.s_addr))
return (EINVAL);
....
}
...
}</programlisting>
<para>你也许想知道函数<literal>prison_ip()</literal>做什么。
prison.ip有三个参数当前进程(用<literal>p</literal>表示)
一些标志(flag)和一个IP地址。当这个IP地址属于一个Jail时返回1
否则返回0。正如你从代码中看见的如果那个IP地址真的属于一个Jail
就不再允许向一个网络接口绑定协议。</para>
<programlisting><filename>/usr/src/sys/kern/kern_jail.c:</filename>
int prison_ip(struct proc *p, int flag, u_int32_t *ip) {
u_int32_t tmp;
if (!p-&gt;p_prison)
return (0);
if (flag)
tmp = *ip;
else tmp = ntohl (*ip);
if (tmp == INADDR_ANY) {
if (flag)
*ip = p-&gt;p_prison-&gt;pr_ip;
else *ip = htonl(p-&gt;p_prison-&gt;pr_ip);
return (0);
}
if (p-&gt;p_prison-&gt;pr_ip != tmp)
return (1);
return (0);
}</programlisting>
<para>被囚禁的用户不能对一个不属于这个Jail的IP地址绑定服务。
这个限制在函数<literal>in_pcbbind</literal>中也有所体现:</para>
<programlisting><filename>/usr/src/sys/net inet/in_pcb.c</filename>
if (nam) {
...
lport = sin-&gt;sin.port;
... if (lport) {
...
if (p &amp;&amp; p-&gt;p_prison)
prison = 1;
if (prison &amp;&amp;
prison_ip(p, 0, <![CDATA[&sin]]>-&gt;sin_addr.s_addr))
return (EADDRNOTAVAIL);</programlisting>
</sect2>
<sect2>
<title>文件系统</title>
<indexterm><primary>filesystem(文件系统)</primary></indexterm>
<para>如此完全级大于0即便是root也不允许在Jail中设置文件标志
如“不可修改”、“添加”、“不可删除”标志。</para>
<programlisting>/usr/src/sys/ufs/ufs/ufs_vnops.c:
int ufs.setattr(ap)
...
{
if ((cred-&gt;cr.uid == 0) &amp;&amp; (p-&gt;prison == NULL)) {
if ((ip-&gt;i_flags
&amp; (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) &amp;&amp;
securelevel &gt; 0)
return (EPERM);
}</programlisting>
</sect2>
</sect1>
</chapter>

View file

@ -0,0 +1,298 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.7
$FreeBSD$
-->
<chapter id="kernel-objects">
<chapterinfo>
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>内核对象</title>
<indexterm><primary>Kernel Objects(内核对象)</primary></indexterm>
<indexterm><primary>Object-Oriented(面向对象)</primary></indexterm>
<indexterm><primary>binary compatibility(二进制兼容性)</primary></indexterm>
<para>内核对象,也就是<firstterm>Kobj</firstterm>,为内核提供了一种面向对象
的C语言编程方式。被操作的数据也承载操作它的方法。
这使得在不破坏二进制兼容性的前提下,某一个接口能够增/减相应的操作。</para>
<sect1 id="kernel-objects-term">
<title>术语</title>
<indexterm><primary>object(对象)</primary></indexterm>
<indexterm><primary>method(方法)</primary></indexterm>
<indexterm><primary>class(类)</primary></indexterm>
<indexterm><primary>interface(接口)</primary></indexterm>
<variablelist>
<varlistentry>
<term>对象</term>
<listitem><para>数据集合-数据结构-数据分配的集合</para>
</listitem>
</varlistentry>
<varlistentry>
<term>方法</term>
<listitem>
<para>某一种操作&mdash;函数</para>
</listitem>
</varlistentry>
<varlistentry>
<term>类</term>
<listitem>
<para>一种或多种方法</para>
</listitem>
</varlistentry>
<varlistentry>
<term>接口</term>
<listitem>
<para>一种或多种方法的一个标准集合</para>
</listitem>
</varlistentry>
</variablelist>
</sect1>
<sect1 id="kernel-objects-operation">
<title>Kobj的工作流程</title>
<tip><title>译者注</title><para>这一小节两段落中原作者的用词有些含混,
请参考我在括号中的注释阅读。</para></tip>
<para>Kobj工作时产生方法的描述。每个描述有一个唯一的标识和一个缺省函数。
某个描述的地址被用来在一个类的方法表里唯一的标识方法。</para>
<para>构建一个类,就是要建立一张方法表,并将这张表关联到一个或多个函数(方法)
这些函数(方法)都带有方法描述。使用前,类要被编译。编译时要为这个类分配一些缓存。
在方法表中的每个方法描述都会被指派一个唯一的标识,
除非已经被其它引用它的类在编译时指派了标识。对于每个将要被使用的方法,
都会由脚本生成一个函数(方法查找函数),以解析外来参数,
并在被查询时给出方法描述的地址。被生成的函数(方法查找函数)
凭着那个方法描述的唯一标识按Hash的方法查找对象的类的缓存。
如果这个方法不在缓存中,函数会查找使用类的方法表。如果这个方法被找到了,
类里的相关函数(也就是某个方法的实现代码)就会被使用。
否则,这个方法描述的缺省函数将被使用。</para>
<para>这些过程可被表示如下:</para>
<programlisting>对象-&gt;缓存&lt;-&gt;类</programlisting>
</sect1>
<sect1 id="kernel-objects-using">
<title>使用Kobj</title>
<sect2>
<title>结构</title>
<programlisting>struct kobj_method</programlisting>
</sect2>
<sect2>
<title>函数</title>
<programlisting>void kobj_class_compile(kobj_class_t cls);
void kobj_class_compile_static(kobj_class_t cls, kobj_ops_t ops);
void kobj_class_free(kobj_class_t cls);
kobj_t kobj_create(kobj_class_t cls, struct malloc_type *mtype, int mflags);
void kobj_init(kobj_t obj, kobj_class_t cls);
void kobj_delete(kobj_t obj, struct malloc_type *mtype);</programlisting>
</sect2>
<sect2>
<title>宏</title>
<programlisting>KOBJ_CLASS_FIELDS
KOBJ_FIELDS
DEFINE_CLASS(name, methods, size)
KOBJMETHOD(NAME, FUNC)</programlisting>
</sect2>
<sect2>
<title>头文件</title>
<programlisting>&lt;sys/param.h>
&lt;sys/kobj.h></programlisting>
</sect2>
<sect2>
<title>建立一个接口的模板</title>
<indexterm><primary>Kernel Objects(内核对象)</primary>
<secondary>interface(接口)</secondary></indexterm>
<para>使用Kobj的第一步是建立一个接口。建立接口包括建立模板的工作。
建立模板可用脚本<filename>src/sys/kern/makeobjops.pl</filename>完成,
它会产生申明方法的头文件和代码,脚本还会生成方法查找函数。</para>
<para>在这个模板中如下关键词会被使用:
<literal>#include</literal>, <literal>INTERFACE</literal>,
<literal>CODE</literal>, <literal>METHOD</literal>,
<literal>STATICMETHOD</literal>, 和
<literal>DEFAULT</literal>.</para>
<para><literal>#include</literal>语句的整行内容将被一字不差的
复制到被生成的代码文件的头部。</para>
<para>例如:</para>
<programlisting>#include &lt;sys/foo.h></programlisting>
<para>关键词<literal>INTERFACE</literal>用来定义接口名。
这个名字将与每个方法名接合在一起,形成 [interface name]_[method name]。
语法是INTERFACE [接口名];</para>
<para>例如:</para>
<programlisting>INTERFACE foo;</programlisting>
<para>关键词<literal>CODE</literal>会将它的参数一字不差的复制到代码文件中。
语法是<literal>CODE { [任何代码] };</literal></para>
<para>例如:</para>
<programlisting>CODE {
struct foo * foo_alloc_null(struct bar *)
{
return NULL;
}
};</programlisting>
<para>关键词<literal>METHOD</literal>用来描述一个方法。语法是:
<literal>METHOD [返回值类型] [方法名] { [对象 [,
参数若干]] };</literal></para>
<para>例如:</para>
<programlisting>METHOD int bar {
struct object *;
struct foo *;
struct bar;
};</programlisting>
<para>关键词<literal>DEFAULT</literal>跟在关键词<literal>METHOD</literal>之后,
是对关键词<literal>METHOD</literal>的补充。它给这个方法补充上缺省函数。语法是:
<literal>METHOD [返回值类型] [方法名] {
[对象; [其它参数]] }DEFAULT [缺省函数];
</literal></para>
<para>例如:</para>
<programlisting>METHOD int bar {
struct object *;
struct foo *;
int bar;
} DEFAULT foo_hack;</programlisting>
<para>关键词<literal>STATICMETHOD</literal>类似关键词<literal>METHOD</literal>。
对于每个Kobj对象一般其头部都有一些Kobj专有的数据。
<literal>METHOD</literal>定义的方法就假设这些专有数据位于对象头部;
假如对象头部没有这些专有数据,这些方法对这个对象的访问就可能出错。
而<literal>STATICMETHOD</literal>定义的对象可以不受这个限制:
这样描述出的方法,其操作的数据不由这个类的某个对象实例给出,
而是全都由调用这个方法时的操作数(译者注:即参数)给出。
这也对于在某个类的方法表之外调用这个方法有用。
<tip><title>译者注</title><para>这一段的语言与原文相比调整很大。
静态方法是不依赖于对象实例的方法。
参看C++类中的“静态函数”的概念。</para></tip></para>
<para>其它完整的例子:</para>
<programlisting>src/sys/kern/bus_if.m
src/sys/kern/device_if.m</programlisting>
</sect2>
<sect2>
<title>建立一个类</title>
<indexterm><primary>Kernel Objects(内核对象)</primary>
<secondary>class(类)</secondary></indexterm>
<para>使用Kobj的第二步是建立一个类。一个类的组有名字、方法表
假如使用了Kobj的“对象管理工具”(Object Handling Facilities)
类中还包含对象的大小。建立类时使用宏<function>DEFINE_CLASS()</function>。
建立方法表时须建立一个kobj_method_t数组用NULL项结尾。
每个非NULL项可用宏<function>KOBJMETHOD()</function>建立。</para>
<para>例如:</para>
<programlisting>DEFINE_CLASS(fooclass, foomethods, sizeof(struct foodata));
kobj_method_t foomethods[] = {
KOBJMETHOD(bar_doo, foo_doo),
KOBJMETHOD(bar_foo, foo_foo),
{ NULL, NULL}
};</programlisting>
<para>类须被<quote>编译</quote>。根据该类被初始化时系统的状态,
将要用到一个静态分配的缓存和<quote>操作数表</quote>(ops table
译者注:即<quote>参数表</quote>)。这些操作可通过声明一个结构体
<structname>struct kobj_ops</structname>并使用
<function>kobj_class_compile_static()</function>
或是只使用<function>kobj_class_compile()</function>来完成。</para>
</sect2>
<sect2>
<title>建立一个对象</title>
<indexterm><primary>Kernel Objects(内核对象)</primary>
<secondary>object(对象)</secondary></indexterm>
<para>使用Kobj的第三步是定义对象。Kobj对象建立程序假定Kobj
专有数据在一个对象的头部。如果不是如此,应当先自行分配对象,
再使用<function>kobj_init()</function>初始化对象中的Kobj专有数据
其实可以使用<function>kobj_create()</function>分配对象,
并自动初始化对象中的Kobj专有内容。<function>kobj_init()</function>
也可以用来改变一个对象所使用的类。</para>
<para>将Kobj的数据集成到对象中要使用宏KOBJ_FIELDS。</para>
<para>例如</para>
<programlisting>struct foo_data {
KOBJ_FIELDS;
foo_foo;
foo_bar;
};</programlisting>
</sect2>
<sect2>
<title>调用方法</title>
<para>使用Kobj的最后一部就是通过生成的函数调用对象类中的方法。
调用时,接口名与方法名用'_'接合,而且全部使用大写字母。</para>
<para>例如接口名为foo方法为bar调用就是:</para>
<programlisting>[返回值 = ] FOO_BAR(对象 [, 其它参数]);</programlisting>
</sect2>
<sect2>
<title>善后处理</title>
<para>当一个用<function>kobj_create()</function>不再需要被使用时,
可对这个对象调用<function>kobj_delete()</function>。
当一个类不再需要被使用时,
可对这个类调用<function>kobj_class_free()</function>。</para>
</sect2>
</sect1>
</chapter>
<!--
Local Variables:
mode: sgml
sgml-declaration: "../chapter.decl"
sgml-indent-data: t
sgml-omittag: nil
sgml-always-quote-attributes: t
sgml-parent-document: ("../book.sgml" "part" "chapter")
End:
-->

View file

@ -0,0 +1,337 @@
<!--
The FreeBSD Documentation Project
The FreeBSD SMP Next Generation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.11
$FreeBSD$
-->
<chapter id="locking">
<chapterinfo>
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>内核中的锁</title>
<indexterm><primary>SMP Next Generation Project(下一代对称多处理工程)</primary></indexterm>
<para><emphasis>这一章由 FreeBSD SMP Next Generation Project 维护。
请将评论和建议发送给&a.smp;.</emphasis></para>
<indexterm><primary>locking(锁)</primary></indexterm>
<indexterm><primary>multi-processing(多处理)</primary></indexterm>
<indexterm><primary>mutexes(同时/独占, mutual exclusion)</primary></indexterm>
<indexterm><primary>lockmgr(锁管理器)</primary></indexterm>
<indexterm><primary>atomic operations(原子操作)</primary></indexterm>
<para>这篇文档提纲挈领的讲述了在FreeBSD内核中的锁这些锁使得有效的多处理成为可能。
锁可以用几种方式获得。数据结构可以用mutex或&man.lockmgr.9;保护。
对于为数不多的若干个变量,假如总是使用原子操作访问它们,这些变量就可以得到保护。
<tip><title>译者注</title><para>仅读本章内容,还不足以找出<quote>mutex</quote>
和<quote>共享互斥锁</quote>的区别。似乎它们的功能有重叠之处,
前者比后者的功能选项更多。它们似乎都是&man.lockmgr.9;的子集。<para></tip></para>
<sect1 id="locking-mutexes">
<title>Mutex</title>
<para>Mutex就是一种用来解决共享/排它矛盾的锁。
一个mutex在一个时刻只可以被一个实体拥有。如果另一个实体要获得已经被拥有的mutex
就会进入等待直到这个mutex被释放。在FreeBSD内核中mutex被进程所拥有。</para>
<para>Mutex可以被递归的索要但是mutex一般只被一个实体拥有较短的一段时间
因此一个实体不能在持有mutex时睡眠。如果你需要在持有mutex时睡眠
可使用一个 &man.lockmgr.9; 的锁。</para>
<para>每个mutex有几个令人感兴趣的属性:</para>
<variablelist>
<varlistentry>
<term>变量名</term>
<listitem>
<para>在内核源代码中<type>struct mtx</type>变量的名字</para>
</listitem>
</varlistentry>
<varlistentry>
<term>逻辑名</term>
<listitem>
<para>由函数<function>mtx_init</function>指派的mutex的名字。
这个名字显示在KTR跟踪消息和witness出错与警告信息里。
这个名字还用于区分标识在witness代码中的各个mutex</para>
</listitem>
</varlistentry>
<varlistentry>
<term>类型</term>
<listitem>
<para>Mutex的类型用标志<constant>MTX_*</constant>表示。
每个标志的意义在&man.mutex.9;有所描述。</para>
<variablelist>
<varlistentry>
<term><constant>MTX_DEF</constant></term>
<listitem>
<para>一个睡眠mutex</para>
</listitem>
</varlistentry>
<varlistentry>
<term><constant>MTX_SPIN</constant></term>
<listitem>
<para>一个循环mutex</para>
</listitem>
</varlistentry>
<varlistentry>
<term><constant>MTX_RECURSE</constant></term>
<listitem>
<para>这个mutex允许递归</para>
</listitem>
</varlistentry>
</variablelist>
</listitem>
</varlistentry>
<varlistentry>
<term>保护对象</term>
<listitem>
<para>这个入口所要保护的数据结构列表或数据结构成员列表。
对于数据结构成员,将按照
<structname>结构名</structname>.<structfield>成员名</structfield>的形式命名。</para>
</listitem>
</varlistentry>
<varlistentry>
<term>依赖函数</term>
<listitem>
<para>仅当mutex被持有时才可以被调用的函数</para>
</listitem>
</varlistentry>
</variablelist>
<table frame="all" colsep="1" rowsep="1" pgwide="1">
<title>Mutex列表</title>
<indexterm><primary>locks(锁)</primary>
<secondary>sched_lock(调度器锁)</secondary></indexterm>
<indexterm><primary>locks(锁)</primary>
<secondary>vm86pcb_lock(虚拟8086模式进程控制块锁)</secondary></indexterm>
<indexterm><primary>locks(锁)</primary>
<secondary>Giant(巨锁)</secondary></indexterm>
<indexterm><primary>locks(锁)</primary>
<secondary>callout_lock(延时调用锁)</secondary></indexterm>
<tgroup cols="5">
<thead>
<row>
<entry>变量名</entry>
<entry>逻辑名</entry>
<entry>类型</entry>
<entry>保护对象</entry>
<entry>依赖函数</entry>
</row>
</thead>
<!-- The scheduler lock -->
<tbody>
<row>
<entry>sched_lock</entry>
<entry><quote>sched lock</quote>(调度器锁)</entry>
<entry>
<constant>MTX_SPIN</constant> |
<constant>MTX_RECURSE</constant>
</entry>
<entry>
<varname>_gmonparam</varname>,
<varname>cnt.v_swtch</varname>,
<varname>cp_time</varname>,
<varname>curpriority</varname>,
<structname>mtx</structname>.<structfield>mtx_blocked</structfield>,
<structname>mtx</structname>.<structfield>mtx_contested</structfield>,
<structname>proc</structname>.<structfield>p_procq</structfield>,
<structname>proc</structname>.<structfield>p_slpq</structfield>,
<structname>proc</structname>.<structfield>p_sflag</structfield>,
<structname>proc</structname>.<structfield>p_stat</structfield>,
<structname>proc</structname>.<structfield>p_estcpu</structfield>,
<structname>proc</structname>.<structfield>p_cpticks</structfield>
<structname>proc</structname>.<structfield>p_pctcpu</structfield>,
<structname>proc</structname>.<structfield>p_wchan</structfield>,
<structname>proc</structname>.<structfield>p_wmesg</structfield>,
<structname>proc</structname>.<structfield>p_swtime</structfield>,
<structname>proc</structname>.<structfield>p_slptime</structfield>,
<structname>proc</structname>.<structfield>p_runtime</structfield>,
<structname>proc</structname>.<structfield>p_uu</structfield>,
<structname>proc</structname>.<structfield>p_su</structfield>,
<structname>proc</structname>.<structfield>p_iu</structfield>,
<structname>proc</structname>.<structfield>p_uticks</structfield>,
<structname>proc</structname>.<structfield>p_sticks</structfield>,
<structname>proc</structname>.<structfield>p_iticks</structfield>,
<structname>proc</structname>.<structfield>p_oncpu</structfield>,
<structname>proc</structname>.<structfield>p_lastcpu</structfield>,
<structname>proc</structname>.<structfield>p_rqindex</structfield>,
<structname>proc</structname>.<structfield>p_heldmtx</structfield>,
<structname>proc</structname>.<structfield>p_blocked</structfield>,
<structname>proc</structname>.<structfield>p_mtxname</structfield>,
<structname>proc</structname>.<structfield>p_contested</structfield>,
<structname>proc</structname>.<structfield>p_priority</structfield>,
<structname>proc</structname>.<structfield>p_usrpri</structfield>,
<structname>proc</structname>.<structfield>p_nativepri</structfield>,
<structname>proc</structname>.<structfield>p_nice</structfield>,
<structname>proc</structname>.<structfield>p_rtprio</structfield>,
<varname>pscnt</varname>,
<varname>slpque</varname>,
<varname>itqueuebits</varname>,
<varname>itqueues</varname>,
<varname>rtqueuebits</varname>,
<varname>rtqueues</varname>,
<varname>queuebits</varname>,
<varname>queues</varname>,
<varname>idqueuebits</varname>,
<varname>idqueues</varname>,
<varname>switchtime</varname>,
<varname>switchticks</varname>
</entry>
<entry>
<function>setrunqueue</function>,
<function>remrunqueue</function>,
<function>mi_switch</function>,
<function>chooseproc</function>,
<function>schedclock</function>,
<function>resetpriority</function>,
<function>updatepri</function>,
<function>maybe_resched</function>,
<function>cpu_switch</function>,
<function>cpu_throw</function>,
<function>need_resched</function>,
<function>resched_wanted</function>,
<function>clear_resched</function>,
<function>aston</function>,
<function>astoff</function>,
<function>astpending</function>,
<function>calcru</function>,
<function>proc_compare</function>
</entry>
</row>
<!-- The vm86 pcb lock -->
<row>
<entry>vm86pcb_lock</entry>
<entry><quote>vm86pcb lock</quote>(虚拟8086模式进程控制块锁)</entry>
<entry>
<constant>MTX_DEF</constant>
</entry>
<entry>
<varname>vm86pcb</varname>
</entry>
<entry>
<function>vm86_bioscall</function>
</entry>
</row>
<!-- Giant -->
<row>
<entry>Giant</entry>
<entry><quote>Giant</quote>(巨锁)</entry>
<entry>
<constant>MTX_DEF</constant> |
<constant>MTX_RECURSE</constant>
</entry>
<entry>几乎可以是任何东西</entry>
<entry>许多</entry>
</row>
<!-- The callout lock -->
<row>
<entry>callout_lock</entry>
<entry><quote>callout lock</quote>(延时调用锁)</entry>
<entry>
<constant>MTX_SPIN</constant> |
<constant>MTX_RECURSE</constant>
</entry>
<entry>
<varname>callfree</varname>,
<varname>callwheel</varname>,
<varname>nextsoftcheck</varname>,
<structname>proc</structname>.<structfield>p_itcallout</structfield>,
<structname>proc</structname>.<structfield>p_slpcallout</structfield>,
<varname>softticks</varname>,
<varname>ticks</varname>
</entry>
<entry>
</entry>
</row>
</tbody>
</tgroup>
</table>
</sect1>
<sect1 id="locking-sx">
<title>共享互斥锁</title>
<para>这些锁提供基本的读/写类型的功能,可以被一个正在睡眠的进程持有。
现在它们被统一到&man.lockmgr.9;之中。</para>
<indexterm><primary>locks(锁)</primary>
<secondary>shared exclusive(共享互斥)</secondary></indexterm>
<table>
<title>共享互斥锁列表</title>
<indexterm><primary>locks(锁)</primary>
<secondary>allproc_lock(全进程锁)</secondary></indexterm>
<indexterm><primary>locks(锁)</primary>
<secondary>proctree_lock(进程树锁)</secondary></indexterm>
<tgroup cols="2">
<thead>
<row>
<entry>变量名</entry>
<entry>保护对象</entry>
</row>
</thead>
<tbody>
<row>
<entry><varname>allproc_lock</varname></entry>
<entry>
<varname>allproc</varname>
<varname>zombproc</varname>
<varname>pidhashtbl</varname>
<structname>proc</structname>.<structfield>p_list</structfield>
<structname>proc</structname>.<structfield>p_hash</structfield>
<varname>nextpid</varname>
</entry>
</row>
<row>
<entry><varname>proctree_lock</varname></entry>
<entry>
<structname>proc</structname>.<structfield>p_children</structfield>
<structname>proc</structname>.<structfield>p_sibling</structfield>
</entry>
</row>
</tbody>
</tgroup>
</table>
</sect1>
<sect1 id="locking-atomic">
<title>原子保护变量</title>
<indexterm><primary>atomically protected variables(原子保护变量)</primary></indexterm>
<para>原子保护变量并非由一个显在的锁保护的特殊变量,而是:
对这些变量的所有数据访问都要使用特殊的原子操作(&man.atomic.9;)。
尽管其它的基本同步机制(例如mutex)就是用原子保护变量实现的,
但是很少有变量直接使用这种处理方式。</para>
<itemizedlist>
<listitem>
<para><structname>mtx</structname>.<structfield>mtx_lock</structfield></para>
</listitem>
</itemizedlist>
</sect1>
</chapter>

View file

@ -0,0 +1,97 @@
<!-- Original Revision: 1.4
$FreeBSD$ -->
<!ENTITY mac.mpo "mpo">
<!ENTITY mac.thead '
<colspec colname="first" colwidth="0">
<colspec colwidth="0">
<colspec colname="last" colwidth="0">
<thead>
<row>
<entry>参数</entry>
<entry>说明</entry>
<entry>锁定</entry>
</row>
</thead>
'>
<!ENTITY mac.externalize.paramdefs '
<paramdef>struct label *<parameter>label</parameter></paramdef>
<paramdef>char *<parameter>element_name</parameter></paramdef>
<paramdef>struct sbuf *<parameter>sb</parameter></paramdef>
<paramdef>int <parameter>*claimed</parameter></paramdef>
'>
<!ENTITY mac.externalize.tbody '
<tbody>
<row>
<entry><parameter>label</parameter></entry>
<entry>将用外部形式表示的标记</entry>
</row>
<row>
<entry><parameter>element_name</parameter></entry>
<entry>需要外部表示标记的策略的名字</entry>
</row>
<row>
<entry><parameter>sb</parameter></entry>
<entry>用来存放标记的文本表示形式的字符buffer</entry>
</row>
<row>
<entry><parameter>claimed</parameter></entry>
<entry>如果可以填充element_data 域,则其数值递增</entry>
</row>
</tbody>
'>
<!ENTITY mac.externalize.para "
<para>根据传入的标记结构,产生一个以外部形式表示的标记。
一个外部形式标记,是标记内容的文本表示,它由用户级的应用程序使用,是用户可读的。
目前的MAC实现方案将依次调用策略的相应入口函数因此
具体策略的实现代码需要在填写sb之前先检查element_name中指定的名字。
如果element_name中的内容与你的策略名字不相符则直接返回0。
仅当转换标记数据的过程中出现错误时才返回非0值。
一旦策略决定填写element_data递增*claim的数值。</para>
">
<!ENTITY mac.internalize.paramdefs '
<paramdef>struct label *<parameter>label</parameter></paramdef>
<paramdef>char *<parameter>element_name</parameter></paramdef>
<paramdef>char *<parameter>element_data</parameter></paramdef>
<paramdef>int *<parameter>claimed</parameter></paramdef>
'>
<!ENTITY mac.internalize.tbody '
<tbody>
<row>
<entry><parameter>label</parameter></entry>
<entry>将被填充的标记</entry>
</row>
<row>
<entry><parameter>element_name</parameter></entry>
<entry>需要内部表示标记的策略的名字</entry>
</row>
<row>
<entry><parameter>element_data</parameter></entry>
<entry>需要被转换的文本数据</entry>
</row>
<row>
<entry><parameter>claimed</parameter></entry>
<entry>如果数据被正确转换,则其数值递增</entry>
</row>
</tbody>
'>
<!ENTITY mac.internalize.para "
<para>根据一个文本形式的外部表示标记数据,创建一个内部形式的标记结构。
目前的MAC方案将依次调用所有策略的相关入口函数来响应标记的内部转换请求
因此实现代码必须首先通过比较element_name中的内容和自己的策略名字
来确定是否需要转换element_data中存放的数据。
类似的如果名字不匹配或者数据转换操作成功该函数返回0并递增*claimed的值。</para>
">

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,339 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.9
$FreeBSD$
Originally by: Jeroen Ruigrok van der Warven
Date: newbus-draft.txt,v 1.8 2001/01/25 08:01:08
Copyright (c) 2000 Jeroen Ruigrok van der Warven (asmodai@wxs.nl)
Copyright (c) 2002 Hiten Mahesh Pandya (hiten@uk.FreeBSD.org)
Future Additions:
o Expand the information about device_t
o Add information about the bus_* functions.
o Add information about bus specific (e.g. PCI) functions.
o Add a reference section for additional information.
o Add more newbus related structures and typedefs.
o Add a 'Terminology' section.
o Add information on resource manager functions, busspace
manager functions, newbus events related functions.
o More cleanup ... !
Provided under the FreeBSD Documentation License.
-->
<chapter id="newbus">
<chapterinfo>
<authorgroup>
<author>
<firstname>Jeroen</firstname>
<surname>Ruigrok van der Werven (asmodai)</surname>
<affiliation><address><email>asmodai@FreeBSD.org</email></address>
</affiliation>
<contrib>&cnproj.written.by;</contrib>
</author>
<author>
<firstname>Hiten</firstname>
<surname>Pandya</surname>
<affiliation><address><email>hiten@uk.FreeBSD.org</email></address>
</affiliation>
</author>
</authorgroup>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>Newbus</title>
<para><emphasis>特别感谢Matthew N. Dodd, Warner Losh, Bill Paul,
Doug Rabson, Mike Smith, Peter Wemm and Scott Long</emphasis>.</para>
<para>本章详细解释了Newbus设备框架。</para>
<sect1 id="newbus-devdrivers">
<title>设备驱动程序</title>
<sect2>
<title>设备驱动程序的目的</title>
<indexterm><primary>device driver(设备驱动程序)</primary></indexterm>
<indexterm><primary>device driver(设备驱动程序)</primary><secondary>introduction(介绍)</secondary></indexterm>
<para>设备驱动程序是软件组件,它在内核关于外围设备(例如,磁盘、网络
适配卡)的通用视图和外围设备的实际实现之间提供了接口。
<emphasis>设备驱动程序接口(DDI)</emphasis>是内核与设备驱动程序组件
之间定义的接口。
</para>
</sect2>
<sect2>
<title>设备驱动程序的类型</title>
<para>在&unix;那个时代FreeBSD也从中延续而来定义了四种类型的
设备:</para>
<itemizedlist>
<listitem><para>块设备驱动程序</para></listitem>
<listitem><para>字符设备驱动程序</para></listitem>
<listitem><para>网络设备驱动程序</para></listitem>
<listitem><para>伪设备驱动程序</para></listitem>
</itemizedlist>
<indexterm><primary>block devices(块设备)</primary></indexterm>
<para><emphasis>块设备</emphasis>以使用固定大小的[数据]块的方式运行。
这种类型的驱动程序依赖所谓的
<emphasis>缓冲区缓存(buffer cache)</emphasis>,其目的
是在内存中的专用区域缓存访问过的数据块。这种缓冲区缓存常常基于后台写
(write-behind),这意味着数据在内存中被修改后,当系统进行其周期性
磁盘刷新时才会被同步到磁盘,从而优化写操作。</para>
</sect2>
<sect2>
<title>字符设备</title>
<indexterm><primary>character devices(字符设备)</primary></indexterm>
<para>然而在FreeBSD 4.0版本以及后续版本中,
块设备和字符设备的区别变得不存在了。</para>
</sect2>
</sect1>
<sect1 id="newbus-overview">
<!--
Real title:
Newbus, Busspace and the Resource Manager, an Explanation of the Possibilities
-->
<title>Newbus概览</title>
<indexterm><primary>Newbus</primary></indexterm>
<para><emphasis>Newbus</emphasis>实现了一种基于抽象层的新型总线结构,
可以在FreeBSD 3.0中看到这种总线结构的介绍当时Alpha的移植被导入到
代码树中。直到4.0它才成为设备驱动程序使用的默认系统。其目的是为主机
系统提供给<emphasis>操作系统</emphasis>的各种总线和设备的互连提供更加
面向对象的方法。</para>
<para>其主要特性包括:</para>
<itemizedlist>
<listitem><para>动态连接</para></listitem>
<listitem><para>驱动程序容易模块化</para></listitem>
<listitem><para>伪总线</para></listitem>
</itemizedlist>
<para>最显著的改变之一是从平面和特殊系统演变为设备树布局。</para>
<para>顶层驻留的是<emphasis><quote>根</quote></emphasis>设备,它作为
父设备,所有其他设备挂接在它上面。对于每个结构,通常<quote>根</quote>
只有单个孩子,其上连接着诸如<emphasis>host-to-PCI桥</emphasis>
等东西。对于x86这种<quote>根</quote>设备为
<emphasis><quote>nexus</quote></emphasis>设备对于AlphaAlpha的各种
不同型号有不同的顶层设备,对应不同的硬件芯片组,包括
<emphasis>lca</emphasis><emphasis>apecs</emphasis>
<emphasis>cia</emphasis>和<emphasis>tsunami</emphasis>。</para>
<para>Newbus上下文中的设备表示系统中的单个硬件实体。例如每个PCI设备被
表示为一个Newbus设备。系统中的任何设备可以有孩子有孩子的设备通常被
称为<emphasis><quote>bus</quote></emphasis>。系统中常用总线的例子就是
ISA和PCI他们各自管理连接到ISA和PCI总线上的设备列表。</para>
<para>通常,不同类型的总线之间的连接被表示为
<emphasis><quote>桥</quote></emphasis>设备,它的孩子就是它所连接的
总线。一个例子就是<emphasis>PCI-to-PCI桥</emphasis>它在父PCI总线上被
表示为<emphasis><devicename>pcibN</devicename></emphasis>,而用它的孩子
<emphasis><devicename>pciN</devicename></emphasis>表示连接在它上面的
总线。这种布局简化了PCI总线树的实现允许公共代码同时用于顶层和桥接的
总线。</para>
<para>Newbus结构中的每个设备请求它的父设备来为其映射资源。父设备接着请求
它的父设备直到到达nexus。因此基本上nexus是Newbus系统中唯一知道所有
资源的部分。</para>
<tip><para>ISA设备可能想在<literal>0x230</literal>映射其IO端口因此它向其
父设备请求这种情况下是ISA总线。ISA总线将它交给PCI-to-ISA桥PCI-to-ISA
桥接着请求PCI总线PCI总线到达host-to-PCI桥最后到达nexus。这种向上
过渡的优美之处在于可以有空间来变换请求。对<literal>0x230</literal>IO端口
的请求在<acronym>MIPS</acronym>机器上可以被PCI桥变成
<literal>0xb0000230</literal>处的内存映射。</para></tip>
<para>资源分配可以在设备树的任何地方加以控制。例如在很多Alpha平台上
ISA中断与PCI中断是单独管理的对ISA中断的资源分配是由Alpha的ISA总线设备
管理的。在IA-32上ISA和PCI中断都由顶层的nexus设备管理。对于两种移植
内存和端口地址空间由单个实体管理 - 在IA-32上是nexus在Alpha例如CIA
或tsunami上是相关的芯片组驱动程序。</para>
<para>为了规范化对内存和端口映射资源的访问Newbus整合了NetBSD的
<literal>bus_space</literal> API。他们提供了单一的API来代替inb/outb
和直接内存读写。这样做的优势在于单个驱动程序就可以使用内存映射寄存器
或端口映射寄存器(有些硬件支持两者)。</para>
<para>这种支持被合并到了资源分配机制中。分配资源时,驱动程序可以从资源
中检取关联的<structfield>bus_space_tag_t</structfield>和
<structfield>bus_space_handle_t</structfield>。</para>
<para>Newbus也允许在专用于此目的的文件中定义接口方法。这些是
<filename>.m</filename>文件,可以在<filename>src/sys</filename>
目录树中找到。</para>
<para>Newbus系统的核心是可扩展的<quote>基于对象编程(object-based
programming)</quote>的模型。系统中的每个设备具有它所支持的一个方法表。
系统和其他设备使用这些方法来控制设备并请求服务。设备所支持的不同方法
被定义为多个<quote>接口</quote>。<quote>接口</quote>只是
设备实现的一组相关的方法。</para>
<para>在Newbus系统中设备方法是通过系统中的各种设备驱动程序提供的。当
<emphasis>自动配置(auto-configuration)</emphasis>期间设备被连接(attach)
到驱动程序,它使用驱动程序声明的方法表。以后设备可以从其驱动程序
<emphasis>分离(detach)</emphasis>,并
<emphasis>重新连接(re-attach)</emphasis>到具有新方法表的新驱动程序。这就
允许驱动程序的动态替换,而动态替换对于驱动程序的开发非常有用。</para>
<para>接口通过与文件系统中用于定义vnode操作的语言相似的接口定义语言来
描述。接口被保存在方法文件中(通常命名为<filename>foo_if.m</filename>)。
</para>
<example>
<title>Newbus的方法</title>
<programlisting>
# Foo 子系统/驱动程序(注释...
INTERFACE foo
METHOD int doit {
device_t dev;
};
# 如果没有通过DEVMETHOD()提供一个方法则DEFAULT为将会被使用的方法
METHOD void doit_to_child {
device_t dev;
driver_t child;
} DEFAULT doit_generic_to_child;
</programlisting>
</example>
<para>当接口被编译后,它产生一个头文件
<quote><filename>foo_if.h</filename></quote>,其中包含函数声明:</para>
<programlisting>
int FOO_DOIT(device_t dev);
int FOO_DOIT_TO_CHILD(device_t dev, device_t child);
</programlisting>
<para>伴随自动产生的头文件,也会创建一个源文件
<quote><filename>foo_if.c</filename></quote>;其中包含一些函数的实现,
这些函数用于在对象方法表中查找相关函数的位置并调用那个函数。</para>
<para>系统定义了两个主要接口。第一个基本接口被称为
<emphasis><quote>设备(device)</quote></emphasis>,并包括与所有设备相关
的方法。<emphasis><quote>设备(device)</quote></emphasis>接口中的方法
包括<emphasis><quote>探测(probe)</quote></emphasis>
<emphasis><quote>连接(attach)</quote></emphasis>和
<emphasis><quote>分离(detach)</quote></emphasis>,他们用来控制硬件的侦测,
以及<emphasis><quote>关闭(shutdown)</quote></emphasis>
<emphasis><quote>挂起(suspend)</quote></emphasis>和
<emphasis><quote>恢复(resume)</quote></emphasis>,他们用于关键事件通知。
</para>
<para>另一个,更加复杂接口是<emphasis><quote>bus</quote></emphasis>。
此接口包含的方法适用于带有孩子的设备,包括访问总线特定的每设备信息
<footnote><para>&man.bus.generic.read.ivar.9; and
&man.bus.generic.write.ivar.9;</para></footnote>,事件通知
<emphasis><literal>child_detached</literal></emphasis>
<emphasis><literal>driver_added</literal></emphasis>)和响应管理
<emphasis><literal>alloc_resource</literal></emphasis>
<emphasis><literal>activate_resource</literal></emphasis>
<emphasis><literal>deactivate_resource</literal></emphasis>
<emphasis><literal>release_resource</literal></emphasis>)。</para>
<para><quote>bus</quote>接口中的很多方法为总线设备的某些孩子执行服务。
这些方法通常使用前两个参量指定提供服务的总线和请求服务的子设备。为了
简化设备驱动程序代码,这些方法中的很多都有访问者(accessor)函数,访问者
函数用来查找父设备并调用父设备上的方法。例如,方法
<literal>BUS_TEARDOWN_INTR(device_t dev, device_t child, ...)</literal>
可以使用函数
<literal>bus_teardown_intr(device_t child, ...)</literal>来调用。</para>
<para>系统中的某些总线类型提供了额外接口以提供对总线特定功能的访问。
例如PCI总线驱动程序定义了<quote>pci</quote>接口,此接口有两个方法
<emphasis><literal>read_config</literal></emphasis>和
<emphasis><literal>write_config</literal></emphasis>用于访问PCI设备
的配置寄存器。</para>
</sect1>
<sect1 id="newbus-api">
<title>Newbus API</title>
<para>由于Newbus API非常庞大本节努力将它文档化。本文档的下一版本会
带来更多信息。</para>
<sect2>
<title>源代码目录树中的重要位置</title>
<para><filename>src/sys/[arch]/[arch]</filename> - 特定机器结构的
内核代码位于这个目录。例如<literal>i386</literal>结构或
<literal>SPARC64</literal>结构。</para>
<para><filename>src/sys/dev/[bus]</filename> - 支持特定
<literal>[bus]</literal>的设备位于这个目录。</para>
<para><filename>src/sys/dev/pci</filename> - PCI总线支持代码位于
这个目录。</para>
<para><filename>src/sys/[isa|pci]</filename> - PCI/ISA设备驱动程序
位于这个目录。FreeBSD<literal>4.0</literal>版本中PCI/ISA支持代码
过去存在于这个目录中。</para>
</sect2>
<sect2>
<title>重要结构和类型定义</title>
<para><literal>devclass_t</literal> - 这是指向
<literal>struct devclass</literal>的指针的类型定义。</para>
<para><literal>device_method_t</literal> - 与
<literal>kobj_method_t</literal>相同(参看
<filename>src/sys/kobj.h</filename>)。</para>
<para><literal>device_t</literal> - 这是指向
<literal>struct device</literal>的指针的类型定义。
<literal>device_t</literal> 表示系统中的设备。它是内核对象。
实现细节参看<filename>src/sys/sys/bus_private.h</filename>。</para>
<para><literal>driver_t</literal> - 这是一个类型定义,它引用
<literal>struct driver</literal>。
<literal>driver</literal>结构是一类
<literal>device(设备)</literal>内核对象;它也保存着驱动程序的私有数据。
</para>
<figure>
<title><emphasis>driver_t</emphasis>实现</title>
<programlisting>
struct driver {
KOBJ_CLASS_FIELDS;
void *priv; /* 驱动程序私有数据 */
};
</programlisting>
</figure>
<para><literal>device_state_t</literal>是一个枚举类型,即
<literal>device_state</literal>。它包含Newbus设备在自动配置前后
可能的状态。</para>
<figure>
<title>设备状态<emphasis>device_state_t</emphasis></title>
<programlisting>
/*
* src/sys/sys/bus.h
*/
typedef enum device_state {
DS_NOTPRESENT, /* 未探测或探测失败 */
DS_ALIVE, /* 探测成功 */
DS_ATTACHED, /* 调用了连接方法 */
DS_BUSY /* 设备已打开 */
} device_state_t;
</programlisting>
</figure>
</sect2>
</sect1>
</chapter>

View file

@ -0,0 +1,305 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.12
$FreeBSD$
-->
<chapter id="pccard">
<chapterinfo>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>PC Card</title>
<indexterm><primary>PC卡(PCMCIA卡, Personal Computer Memory Card International Association)</primary></indexterm>
<indexterm><primary>CardBus</primary></indexterm>
<para>本章将讨论FreeBSD为编写PC Card或CardBus设备的驱动程序而提供的机制。
但目前本文只记录了如何向现有的pccard驱动程序中添加驱动程序。</para>
<sect1 id="pccard-adddev">
<title>添加设备</title>
<para>向所支持的pccard设备列表中添加新设备的步骤已经与系统在FreeBSD 4
中使用的方法不同了。在以前的版本中,需要编辑
<filename>/etc</filename>中的一个文件来列出设备。从FreeBSD 5.0开始,
设备驱动程序知道它们支持什么设备。现在内核中有一个受支持设备的表,
驱动程序用它来连接设备。</para>
<sect2 id="pccard-overview">
<title>概览</title>
<indexterm><primary>CIS</primary></indexterm>
<para>可以有两种方法来识别PC Card他们都基于卡上的
<acronym>CIS</acronym>信息。第一种方法是使用制造商和产品的数字编号。
第二种方法是使用人可读的字符串字符串也是包含在CIS中。PC Card总线
使用集中式数据库和一些宏来提供一个易用的设计模式,让驱动程序的编写
者很容易地确定匹配其驱动程序的设备。</para>
<para>一个很普遍的实际情况是某个公司为一款PC Card产品开发出参考
设计,然后把这个设计卖给另外的公司,以便在市场上出售。那些公司改进
原设计,把向他们的目标客户群或地理区域出售产品,并将他们自己的名字
放到卡中。然而所谓的对现有卡的改进,即使做过任何修改,这些修改通常
也微乎其微。然而,为了强化他们自己版本的品牌,这些供货商常常会把他们
公司的名字放入CIS空间的可读字符串中却不会改动制造商和产品的ID。
</para>
<indexterm><primary>NetGear</primary></indexterm>
<indexterm><primary>Linksys</primary></indexterm>
<indexterm><primary>D-Link</primary></indexterm>
<para>鉴于以上情况对于FreeBSD来说使用数字ID可以减小工作量。同时也
会减小将ID加入到系统的过程中所带来的复杂性。必须仔细检查谁是卡的
真正制造者尤其当提供原卡的供货商在中心数据库中已经有一个不同的ID
时。LinksysD-Link和NetGear是经常出售相同设计的几个美国制造商。
相同的设计可能在日本以诸如Buffalo和Corega的名字出售。然而这些
设备常常具有相同的制造商和产品ID。</para>
<para>PC Card总线在其中心数据库
<filename>/sys/dev/pccard/pccarddevs</filename>中保存了卡的信息,
但不包含哪个驱动程序与它们关联的信息。它也提供了一套宏,以允许在
驱动程序用来声明设备的表中容易地创建简单条目。</para>
<para>最后,某些非常低端的设备根本不包含制造商标识。这些设备需要使用
可读CIS字符串来匹配它们。如果我们不需要这种应急办法该有多好但对于
某些非常低端却非常流行的CD-ROM播放器来说却是必需的。通常应当避免
使用这种方法但本节中还是列出了很多设备因为它们是在认识到PC
Card商业的<acronym>OEM</acronym>本质之前加入的,应当优先使用
数字方法。</para>
</sect2>
<sect2 id="pccard-pccarddevs">
<title><filename>pccarddevs</filename>的格式</title>
<para><filename>pccarddevs</filename>文件有四节。第一节为使用
它们的那些供货商列出了制造商号码。本节按数字排序。下一节包含了
这些供货商使用的所有产品包括他们的产品ID号码和描述字符串。
描述字符串通常不会被使用(相反,即使我们可以匹配数字版本号,我们
仍然基于人可读的CIS设置设备的描述。然后为使用字符串匹配方法的
那些设备重复这两节的东西。最后文件任何地方可以使用C风格的注释。
</para>
<para>文件的第一节包含供货商ID。请保持列表按数字排序。此外为了
能有一个通用清晰的保存地来方便地保存这些信息我们与NetBSD共享此
文件,因此请协调对此文件的任何更改。例如:</para>
<programlisting>vendor FUJITSU 0x0004 Fujitsu Corporation
vendor NETGEAR_2 0x000b Netgear
vendor PANASONIC 0x0032 Matsushita Electric Industrial Co.
vendor SANDISK 0x0045 Sandisk Corporation</programlisting>
<para>显示了几个供货商ID。很凑巧的是<literal>NETGEAR_2</literal>
实际上是NETGEAR从其购买卡的OEM对那些卡提供支持的作者那时并不知道
NETgear使用的是别人的ID。这些条目相当直接易懂。每行上都有供货商
关键字来指示本行的类别。也有供货商的名字。名字将会在pccarddevs文件
的后面重复出现,名字也会用在驱动程序的匹配表中,因此保持它的短小
并且是有效的C标识符。还有一个给供货商的十六进制数字ID。不要添加
<literal>0xffffffff</literal>或<literal>0xffff</literal>形式的ID
因为它们是保留ID前者是'空ID集合',而后者有时会在质量极其差的卡中
看到用来指示none。最后还有关于制卡公司的描述字符串。这个字符串
在FreeBSD中除了用于注释目的外并没有被使用过。</para>
<para>文件的第二节包含产品. 如你在下面例子中看到的: </para>
<programlisting>/* Allied Telesis K.K. */
product ALLIEDTELESIS LA_PCM 0x0002 Allied Telesis LA-PCM
/* Archos */
product ARCHOS ARC_ATAPI 0x0043 MiniCD</programlisting>
<para>格式与供货商的那些行相似。其中有产品关键字。然后是供货商名字,
由上面重复而来。后面跟着产品名字,此名字在驱动程序中使用,且应当
是一个有效C标识符但可以以数字开头。然后是卡的十六进制产品ID。
供货商通常对<literal>0xffffffff</literal>和
<literal>0xffff</literal>有相同的约定。最后是关于设备自身的字符串
描述。由于FreeBSD的pccard总线驱动程序会从人可读的CIS条目创建一个
字符串因此这个字符串在FreeBSD中通常不被使用但某些CIS条目不能
满足要求的情况下还可能使用。产品按制造商的字母顺序排序,然后再按
产品ID的数字排序。每个制造商条目前有一条C注释条目之间有一个空行。
</para>
<para>第三节很象前面的供货商一节但所由的制造商ID为
<literal>-1</literal>。<literal>-1</literal>在FreeBSD pccard总线
代码中意味着<quote>匹配发现的任何东西</quote>。由于它们是C标识符
它们的名字必须唯一。除此之外格式等同于文件的第一节。</para>
<para>最后一节包含那些必须用字符串匹配的卡。这一节的格式与通用
节的格式有点不同:</para>
<programlisting>product ADDTRON AWP100 { "Addtron", "AWP-100&amp;spWireless&amp;spPCMCIA", "Version&amp;sp01.02", NULL }
product ALLIEDTELESIS WR211PCM { "Allied&amp;spTelesis&amp;spK.K.", "WR211PCM", NULL, NULL } Allied Telesis WR211PCM</programlisting>
<para>我们已经熟悉了产品关键字,后跟供货商名字,然后再跟卡的名字,
就象在文件第二节中那样。然而,这之后就与那格式不同了。有一个
{}分组后跟几个字符串。这些字符串对应CIS_INFO三元组中定义的
供货商,产品和额外信息。这些字符串被产生
<filename>pccarddevs.h</filename>的程序过滤,将 &amp;sp替换为
实际的空格。空条目意味着条目的这部分应当被忽略。在我选择的例子中
有一个错误的条目。除非对卡的操作来说至关重要,否则不应当在其中
包含版本号。有时供货商在这个字段中会有卡的很多不同版本,这些版本
都能工作这种情况下那些信息只会让那些拥有相似卡的人在FreeBSD中
更难以使用。有时当供货商出于市场考虑(可用性,价格等等),希望出售
同一品牌下的很多不同部分时,这也是有必要的。如果这样,则在那些
供货商仍然保持相同的制造商/产品对的少见情况下,能否区分开卡至关
重要. 此时不能使用正则表达式匹配。</para>
</sect2>
<sect2 id="pccard-probe">
<title>探测例程样例</title>
<indexterm><primary>PC卡(PCMCIA卡, Personal Computer Memory Card International Association)</primary><secondary>probe(探测)</secondary></indexterm>
<para>要懂得如何向所支持的设备列表中添加设备,就必须懂得很多驱动程序
都有的探测和/或匹配例程。由于也为老卡提供了一个兼容层,这在
FreeBSD 5.x中有一点复杂。由于只是window-dressing不同这儿给出了
一个理想化的版本。</para>
<programlisting>static const struct pccard_product wi_pccard_products[] = {
PCMCIA_CARD(3COM, 3CRWE737A, 0),
PCMCIA_CARD(BUFFALO, WLI_PCM_S11, 0),
PCMCIA_CARD(BUFFALO, WLI_CF_S11G, 0),
PCMCIA_CARD(TDK, LAK_CD011WL, 0),
{ NULL }
};
static int
wi_pccard_probe(dev)
device_t dev;
{
const struct pccard_product *pp;
if ((pp = pccard_product_lookup(dev, wi_pccard_products,
sizeof(wi_pccard_products[0]), NULL)) != NULL) {
if (pp-&gt;pp_name != NULL)
device_set_desc(dev, pp-&gt;pp_name);
return (0);
}
return (ENXIO);
}</programlisting>
<para>这儿我们有一个可以匹配少数几个设备的简单pccard探测例程。如上面
所提到,名字可能不同(如果不是
<function>foo_pccard_probe()</function>则就是
<function>foo_pccard_match()</function>)。函数
<function>pccard_product_lookup()</function>是一个通用函数,它遍历
表并返回指向它所匹配的第一项的指针。一些驱动程序可能使用这个机制来
将某些卡的附加信息传递到驱动程序的其它部分,因此表中可能有些变体。
唯一的要求就是如果你有一个不同的表,则让表的结构的第一个元素为
结构pccard_product。</para>
<para>观察一下表<structname>wi_pccard_products</structname>就会发现,
所有条目都是
<function>PCMCIA_CARD(<replaceable>foo</replaceable>
<replaceable>bar</replaceable>
<replaceable>baz</replaceable>)</function>的形式。
<replaceable>foo</replaceable>部分为来自
<filename>pccarddevs</filename>的制造商ID。
<replaceable>bar</replaceable>部分为产品。
<replaceable>baz</replaceable>为此卡所期望的功能号。许多pccards
可以有多个功能需要有办法区分开功能1和功能0。你可以看一下
<literal>PCMCIA_CARD_D</literal>,它包括了来自
<filename>pccarddevs</filename>文件的设备描述。你也可以看看
<literal>PCMCIA_CARD2</literal>和
<literal>PCMCIA_CARD2_D</literal>,当你需要按
<quote>使用默认描述</quote>和<quote>从pccarddevs中取得</quote>
做法同时匹配CIS字符串和制造商号码时就会用到它们。</para>
</sect2>
<sect2 id="pccard-add">
<title>将它合在一起</title>
<para>因此,为了一个增加新设备,必须进行下面步骤。首先,必须从设备
获得标识信息。完成这个最容易的方法就是将设备插入到PC Card或CF槽中
并发出<command>devinfo -v</command>。你可能会看到一些类似下面的
东西:</para>
<programlisting> cbb1 pnpinfo vendor=0x104c device=0xac51 subvendor=0x1265 subdevice=0x0300 class=0x060700 at slot=10 function=1
cardbus1
pccard1
unknown pnpinfo manufacturer=0x026f product=0x030c cisvendor="BUFFALO" cisproduct="WLI2-CF-S11" function_type=6 at function=0</programlisting>
<para>作为输出的一部分。制造商和产品为产品的数字ID。而cisvender和
cisproduct为CIS中提供的描述本产品的字符串。</para>
<para>由于我们首先想优先使用数字选项,因此首先尝试创建基于此的条目。
为了示例上面的卡已经被稍稍虚构化了。我们看到的供货商为BUFFALO
它已经有一个条目了:</para>
<programlisting>vendor BUFFALO 0x026f BUFFALO (Melco Corporation)</programlisting>
<para>这样我们就可以了。为这个卡查找一个条目,但我们没有发现。但我们
发现:</para>
<programlisting>/* BUFFALO */
product BUFFALO WLI_PCM_S11 0x0305 BUFFALO AirStation 11Mbps WLAN
product BUFFALO LPC_CF_CLT 0x0307 BUFFALO LPC-CF-CLT
product BUFFALO LPC3_CLT 0x030a BUFFALO LPC3-CLT Ethernet Adapter
product BUFFALO WLI_CF_S11G 0x030b BUFFALO AirStation 11Mbps CF WLAN</programlisting>
<para>我们就可以向<filename>pccarddevs</filename>中添加:</para>
<programlisting>product BUFFALO WLI2_CF_S11G 0x030c BUFFALO AirStation ultra 802.11b CF</programlisting>
<para>目前,需要一个手动步骤来
重新产生<filename>pccarddevs.h</filename>,用来将这些标识符转换
到客户驱动程序。你在驱动程序中使用它们之前必须完成下面步骤:
</para>
<screen>&prompt.root; <userinput>cd src/sys/dev/pccard</userinput>
&prompt.root; <userinput>make -f Makefile.pccarddevs</userinput>
</screen>
<para>一旦完成了这些步骤,你就可以向驱动程序中添加卡了。这只是一个
添加一行的简单操作:</para>
<programlisting>static const struct pccard_product wi_pccard_products[] = {
PCMCIA_CARD(3COM, 3CRWE737A, 0),
PCMCIA_CARD(BUFFALO, WLI_PCM_S11, 0),
PCMCIA_CARD(BUFFALO, WLI_CF_S11G, 0),
+ PCMCIA_CARD(BUFFALO, WLI_CF2_S11G, 0),
PCMCIA_CARD(TDK, LAK_CD011WL, 0),
{ NULL }
};</programlisting>
<para>注意,我在我添加的行前面包含了'<literal>+</literal>',但这只是
用来强调这一行。不要把它添加到实际驱动程序中。一旦你添加了这行,就
可以重新编译内核或模块,并试着看它是否能识别设备。如果它识别出设备
并能工作,请提交补丁。如果它不工作,请找出让它工作所需要的东西并
提交一个补丁。如果它根本不识别设备,那么你可能做错了什么,应当重新
检查每一步。</para>
<para>如果你是一个FreeBSD源代码的committer并且所有东西看起来都
正常工作,则你应当把这些改变提交到树中。然而有些小技巧的东西你
需要考虑。首先,你必须提交<filename>pccarddevs</filename>文件到
树中。完成后,你必须重新产生<filename>pccarddevs.h</filename>
并将它作为另一次提交来提交(这是为了确保正确的
&dollar;FreeBSD&dollar;标签会留在后面的文件中)。最后,你需要把
其它东西提交到驱动程序。</para>
</sect2>
<sect2 id="pccard-pr">
<title>提交新设备</title>
<para>很多人直接把新设备的条目发送给作者。请不要那样做。请将它们作为
PR来提交并将PR号码发送给作者用于记录。这样确保条目不会丢失。提交
PR时补丁中没有必要包含<filename>pccardevs.h</filename>的diff
因为那些东西可以重新产生。包含设备的描述和客户驱动程序的补丁是必要
的。如果你不知道名字使用OEM99作为名字作者将会调查后相应地调整
OEM99。提交者不应当提交OEM99而应该找到最高的OEM条目并提交高于那个
的一个。</para>
</sect2>
</sect1>
</chapter>

View file

@ -0,0 +1,424 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.23
$FreeBSD$
-->
<chapter id="pci">
<chapterinfo>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>PCI设备</title>
<indexterm><primary>PCI总线</primary></indexterm>
<para>本章将讨论FreeBSD为了给PCI总线上的设备编写驱动程序而提供的机制。</para>
<sect1 id="pci-probe">
<title>探测与连接</title>
<para>这儿的信息是关于PCI总线代码如何迭代通过未连接的设备并查看新
加载的kld是否会连接其中一个。</para>
<sect2>
<title>示例驱动程序源代码(<filename>mypci.c</filename>)</title>
<programlisting>/*
* 与PCI函数进行交互的简单KLD
*
* Murray Stokely
*/
#include &lt;sys/param.h&gt; /* kernel.h中使用的定义 */
#include &lt;sys/module.h&gt;
#include &lt;sys/systm.h&gt;
#include &lt;sys/errno.h&gt;
#include &lt;sys/kernel.h&gt; /* 模块初始化中使用的类型 */
#include &lt;sys/conf.h&gt; /* cdevsw结构 */
#include &lt;sys/uio.h&gt; /* uio结构 */
#include &lt;sys/malloc.h&gt;
#include &lt;sys/bus.h&gt; /* pci总线用到的结构、原型 */
#include &lt;machine/bus.h&gt;
#include &lt;sys/rman.h&gt;
#include &lt;machine/resource.h&gt;
#include &lt;dev/pci/pcivar.h&gt; /* 为了使用get_pci宏! */
#include &lt;dev/pci/pcireg.h&gt;
/* softc保存我们每个实例的数据。 */
struct mypci_softc {
device_t my_dev;
struct cdev *my_cdev;
};
/* 函数原型 */
static d_open_t mypci_open;
static d_close_t mypci_close;
static d_read_t mypci_read;
static d_write_t mypci_write;
/* 字符设备入口点 */
static struct cdevsw mypci_cdevsw = {
.d_version = D_VERSION,
.d_open = mypci_open,
.d_close = mypci_close,
.d_read = mypci_read,
.d_write = mypci_write,
.d_name = "mypci",
};
/*
* 在cdevsw例程中我们通过结构体cdev中的成员si_drv1找出我们的softc。
* 当我们建立/dev项时在我们的已附着的例程中
* 我们设置这个变量指向我们的softc。
*/
int
mypci_open(struct cdev *dev, int oflags, int devtype, d_thread_t *td)
{
struct mypci_softc *sc;
/* Look up our softc. */
sc = dev-&gt;si_drv1;
device_printf(sc-&gt;my_dev, "Opened successfully.\n");
return (0);
}
int
mypci_close(struct cdev *dev, int fflag, int devtype, d_thread_t *td)
{
struct mypci_softc *sc;
/* Look up our softc. */
sc = dev-&gt;si_drv1;
device_printf(sc-&gt;my_dev, "Closed.\n");
return (0);
}
int
mypci_read(struct cdev *dev, struct uio *uio, int ioflag)
{
struct mypci_softc *sc;
/* Look up our softc. */
sc = dev-&gt;si_drv1;
device_printf(sc-&gt;my_dev, "Asked to read %d bytes.\n", uio-&gt;uio_resid);
return (0);
}
int
mypci_write(struct cdev *dev, struct uio *uio, int ioflag)
{
struct mypci_softc *sc;
/* Look up our softc. */
sc = dev-&gt;si_drv1;
device_printf(sc-&gt;my_dev, "Asked to write %d bytes.\n", uio-&gt;uio_resid);
return (err);
}
/* PCI支持函数 */
/*
* 将某个设置的标识与这个驱动程序支持的标识相比较。
* 如果相符,设置描述字符并返回成功。
*/
static int
mypci_probe(device_t dev)
{
device_printf(dev, "MyPCI Probe\nVendor ID : 0x%x\nDevice ID : 0x%x\n",
pci_get_vendor(dev), pci_get_device(dev));
if (pci_get_vendor(dev) == 0x11c1) {
printf("We've got the Winmodem, probe successful!\n");
device_set_desc(dev, "WinModem");
return (BUS_PROBE_DEFAULT);
}
return (ENXIO);
}
/* 只有当探测成功时才调用连接函数 */
static int
mypci_attach(device_t dev)
{
struct mypci_softc *sc;
printf("MyPCI Attach for : deviceID : 0x%x\n",pci_get_vendor(dev));
/* Look up our softc and initialize its fields. */
sc = device_get_softc(dev);
sc-&gt;my_dev = dev;
/*
* Create a /dev entry for this device. The kernel will assign us
* a major number automatically. We use the unit number of this
* device as the minor number and name the character device
* "mypci&lt;unit&gt;".
*/
sc-&gt;my_cdev = make_dev(<literal>&amp;</literal>mypci_cdevsw, device_get_unit(dev),
UID_ROOT, GID_WHEEL, 0600, "mypci%u", device_get_unit(dev));
printf("Mypci device loaded.\n");
return (ENXIO);
}
/* 分离设备。 */
static int
mypci_detach(device_t dev)
{
struct mypci_softc *sc;
/* Teardown the state in our softc created in our attach routine. */
sc = device_get_softc(dev);
destroy_dev(sc-&gt;my_cdev);
printf("Mypci detach!\n");
return (0);
}
/* 系统关闭期间在sync之后调用。 */
static int
mypci_shutdown(device_t dev)
{
printf("Mypci shutdown!\n");
return (0);
}
/*
* 设备挂起例程。
*/
static int
mypci_suspend(device_t dev)
{
printf("Mypci suspend!\n");
return (0);
}
/*
* 设备恢复(重新开始)例程。
*/
static int
mypci_resume(device_t dev)
{
printf("Mypci resume!\n");
return (0);
}
static device_method_t mypci_methods[] = {
/* 设备接口 */
DEVMETHOD(device_probe, mypci_probe),
DEVMETHOD(device_attach, mypci_attach),
DEVMETHOD(device_detach, mypci_detach),
DEVMETHOD(device_shutdown, mypci_shutdown),
DEVMETHOD(device_suspend, mypci_suspend),
DEVMETHOD(device_resume, mypci_resume),
{ 0, 0 }
};
static devclass_t mypci_devclass;
DEFINE_CLASS_0(mypci, mypci_driver, mypci_methods, sizeof(struct mypci_softc));
DRIVER_MODULE(mypci, pci, mypci_driver, mypci_devclass, 0, 0);</programlisting>
</sect2>
<sect2>
<title>示例驱动程序的<filename>Makefile</filename></title>
<programlisting># 驱动程序mypci的Makefile
KMOD= mypci
SRCS= mypci.c
SRCS+= device_if.h bus_if.h pci_if.h
.include &lt;bsd.kmod.mk&gt;</programlisting>
<para>如果你将上面的源文件和
<filename>Makefile</filename>放入一个目录,你可以运行
<command>make</command>编译示例驱动程序。
还有,你可以运行<command>make load</command>
将驱动程序装载到当前正在运行的内核中,而<command>make
unload</command>可在装载后卸载驱动程序。
</para>
</sect2>
<sect2>
<title>更多资源</title>
<itemizedlist>
<listitem><simpara><ulink url="http://www.pcisig.org/">PCI
Special Interest Group</ulink></simpara></listitem>
<listitem><simpara>PCI System Architecture, Fourth Edition by
Tom Shanley, et al.</simpara></listitem>
</itemizedlist>
</sect2>
</sect1>
<sect1 id="pci-bus">
<title>总线资源</title>
<indexterm><primary>PCI总线</primary><secondary>resources(资源)</secondary></indexterm>
<para>FreeBSD为从父总线请求资源提供了一种面向对象的机制。几乎所有设备
都是某种类型的总线PCI, ISA, USB, SCSI等等的孩子成员并且这些设备
需要从他们的父总线获取资源(例如内存段, 中断线, 或者DMA通道。</para>
<sect2>
<title>基地址寄存器</title>
<indexterm><primary>PCI总线</primary><secondary>Base Address Registers(基地址寄存器)</secondary></indexterm>
<para>为了对PCI设备做些有用的事情你需要从PCI配置空间获取
<emphasis>Base Address Registers</emphasis> (BARs)。获取BAR时的
PCI特定的细节被抽象在函数<function>bus_alloc_resource()</function>中。
</para>
<para>例如,一个典型的驱动程序可能在<function>attach()</function>
函数中有些类似下面的东西:</para>
<programlisting> sc-&gt;bar0id = PCIR_BAR(0);
sc-&gt;bar0res = bus_alloc_resource(dev, SYS_RES_MEMORY, &amp;(sc-&gt;bar0id),
0, ~0, 1, RF_ACTIVE);
if (sc-&gt;bar0res == NULL) {
printf("Memory allocation of PCI base register 0 failed!\n");
error = ENXIO;
goto fail1;
}
sc-&gt;bar1id = PCIR_BAR(1);
sc-&gt;bar1res = bus_alloc_resource(dev, SYS_RES_MEMORY, &amp;(sc-&gt;bar1id),
0, ~0, 1, RF_ACTIVE);
if (sc-&gt;bar1res == NULL) {
printf("Memory allocation of PCI base register 1 failed!\n");
error = ENXIO;
goto fail2;
}
sc-&gt;bar0_bt = rman_get_bustag(sc-&gt;bar0res);
sc-&gt;bar0_bh = rman_get_bushandle(sc-&gt;bar0res);
sc-&gt;bar1_bt = rman_get_bustag(sc-&gt;bar1res);
sc-&gt;bar1_bh = rman_get_bushandle(sc-&gt;bar1res);</programlisting>
<para>每个基址寄存器的句柄被保存在<structname>softc</structname>
结构中,以便以后可以使用它们向设备写入。</para>
<para>然后就能使用这些句柄与<function>bus_space_*</function>函数一起
读写设备寄存器。例如,驱动程序可能包含如下的快捷函数,用来读取板子
特定的寄存器:</para>
<programlisting>uint16_t
board_read(struct ni_softc *sc, uint16_t address) {
return bus_space_read_2(sc-&gt;bar1_bt, sc-&gt;bar1_bh, address);
}
</programlisting>
<para>类似的,可以用下面的函数写寄存器:</para>
<programlisting>void
board_write(struct ni_softc *sc, uint16_t address, uint16_t value) {
bus_space_write_2(sc-&gt;bar1_bt, sc-&gt;bar1_bh, address, value);
}
</programlisting>
<para>这些函数以8位16位和32位的版本存在你应当相应地使用
<function>bus_space_{read|write}_{1|2|4}</function>。</para>
</sect2>
<sect2>
<title>中断</title>
<indexterm><primary>PCI总线</primary><secondary>interrupts(中断)</secondary></indexterm>
<para>中断按照和分配内存资源相似的方式从面向对象的总线代码分配。首先,
必须从父总线分配IRQ资源然后必须设置中断处理函数来处理这个IRQ。
</para>
<para>再一次,来自设备<function>attach()</function>函数的例子比文字
更具说明性。</para>
<programlisting>/* 取得IRQ资源 */
sc-&gt;irqid = 0x0;
sc-&gt;irqres = bus_alloc_resource(dev, SYS_RES_IRQ, &amp;(sc-&gt;irqid),
0, ~0, 1, RF_SHAREABLE | RF_ACTIVE);
if (sc-&gt;irqres == NULL) {
printf("IRQ allocation failed!\n");
error = ENXIO;
goto fail3;
}
/* 现在我们应当设置中断处理函数 */
error = bus_setup_intr(dev, sc-&gt;irqres, INTR_TYPE_MISC,
my_handler, sc, &amp;(sc-&gt;handler));
if (error) {
printf("Couldn't set up irq\n");
goto fail4;
}
sc-&gt;irq_bt = rman_get_bustag(sc-&gt;irqres);
sc-&gt;irq_bh = rman_get_bushandle(sc-&gt;irqres);
</programlisting>
<para>在设备的分离例程中必须注意一些问题。你必须停顿设备的中断流,
并移除中断处理函数。一旦<function>bus_teardown_intr()</function>
返回,你知道你的中断处理函数不会再被调用,并且所有可能已经执行了
这个中断处理函数的线程都已经返回。由于此函数可以睡眠,调用此函数时
你必须不能拥有任何互斥体。</para>
</sect2>
<sect2>
<title>DMA</title>
<indexterm><primary>PCI总线</primary><secondary>DMA(直接内存访问)</secondary></indexterm>
<para>本节已废弃,只是由于历史原因而给出。处理这些问题的适当方法是
使用<function>bus_space_dma*()</function>函数。当更新这一节以反映
那样用法时这段就可能被去掉。然而目前API还不断有些变动因此一旦
它们固定下来后,更新这一节来反映那些改动就很好了。</para>
<para>在PC上想进行总线主控DMA的外围设备必须处理物理地址由于
FreeBSD使用虚拟内存并且只处理虚地址这仍是个问题。幸运的是有个
函数,<function>vtophys()</function>可以帮助我们。</para>
<programlisting>#include &lt;vm/vm.h&gt;
#include &lt;vm/pmap.h&gt;
#define vtophys(virtual_address) (...)
</programlisting>
<para>然而这个解决办法在alpha上有点不一样并且我们真正想要的是一个
称为<function>vtobus()</function>的函数。</para>
<programlisting>#if defined(__alpha__)
#define vtobus(va) alpha_XXX_dmamap((vm_offset_t)va)
#else
#define vtobus(va) vtophys(va)
#endif
</programlisting>
</sect2>
<sect2>
<title>取消分配资源</title>
<para>取消<function>attach()</function>期间分配的所有资源非常重要。
必须小心谨慎,即使在失败的条件下也要保证取消分配那些正确的东西,
这样当你的驱动程序去掉后系统仍然可以使用。</para>
</sect2>
</sect1>
</chapter>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,938 @@
<!--
The FreeBSD Documentation Project
The FreeBSD SMP Next Generation Project
Original Revision: 1.26
$FreeBSD$
-->
<chapter id="smp">
<chapterinfo>
<authorgroup>
<author>
<firstname>John</firstname>
<surname>Baldwin</surname>
<contrib>&cnproj.written.by;</contrib>
</author>
<author>
<firstname>Robert</firstname>
<surname>Watson</surname>
</author>
</authorgroup>
<copyright>
<year>2002</year>
<year>2004</year>
<year>2005</year>
<holder>John Baldwin</holder>
<holder>Robert Watson</holder>
</copyright>
<authorgroup>
<author>
&author.cn.delphij;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>SMPng 设计文档</title>
<sect1 id="smp-intro">
<title>绪论</title>
<indexterm><primary>SMP Next Generation Project(下一代对称多处理工程)</primary></indexterm>
<indexterm><primary>kernel synchronization(内核同步)</primary></indexterm>
<para>这份文档对目前 SMPng 架构的设计与实现进行了介绍。
它首先介绍了基本的原语和相关工具, 其后是关于
FreeBSD 内核的同步与执行模型, 接下来讨论了具体系统中的锁策略,
并描述了在各个子系统中引入细粒度的同步和实现并行化的步骤,
最后是详细的实现说明, 用以解释最初做出某些设计决策的动机,
并使读者了解使用特定的原语所可能产生的重大影响。</para>
<para>这份文档仍在撰写当中, 并将不断更新以反映与 SMPng
项目有关的最新设计与实现的情况。 其中有许多小节目前还只是提纲,
但我们会逐渐为其充实内容。 关于这份文档的更新和建议,
请发给文档编辑。</para>
<indexterm><primary>concurrency(并发)</primary></indexterm>
<para>SMPng 的目标是使内核能够并发执行。 基本上,
内核是一个很大而复杂的程序。 要让内核能够多线程地执行,
我们需要使用某些其它多线程程序在实现时所用到的工具,
这包括互斥体(mutex)、 共享/排他锁(shared/exclusive lock)、
信号量(semaphores) 和条件变量(condition variable)。
如果希望了解它们以及其它 SMP 术语,
请参阅本文的 <xref linkend="smp-glossary"> 一节。</para>
</sect1>
<sect1 id="smp-lock-fundamentals">
<title>基本工具与上锁的基础知识</title>
<sect2>
<title>原子操作指令和内存栅</title>
<indexterm><primary>atomic instructions(原子操作指令)</primary></indexterm>
<indexterm><primary>memory barriers(内存栅)</primary></indexterm>
<para>关于内存栅和原子操作指令已经有很多介绍材料,
因此这一节并不打算对其进行详尽的介绍。 简而言之, 如果有对某一变量上写锁,
就不能在不获得相应的锁时对其进行读取操作。 也就是说,
内存栅的作用在于保证内存操作的相对顺序, 但并不保证内存操作的严格时序。
换言之, 内存栅并不保证 CPU 将本地快取缓存或存储缓冲的内容刷写回内存,
而是在锁释放时确保其所保护的数据, 对于能看到刚释放的那个锁的 CPU
或设备可见。 持有内存栅的 CPU
可以在其快取缓存或存储缓冲中将数据保持其所希望的、 任意长的时间,
但如果其它 CPU 在同一数据元上执行原子操作, 则第一个 CPU 必须保证,
其所更新的数据值, 以及内存栅所要求的任何其它操作, 对第二个 CPU 可见。</para>
<para>例如, 假设在一简单模型中, 认为在主存 (或某一全局快取缓存)
中的数据是可见的, 当某一 CPU 上触发原子操作时, 其它 CPU
的存储缓冲和快取缓存就必须对同一快取缓存线上的全部写操作,
以及内存栅之后的全部未完成操作进行刷写。</para>
<para>这样一来, 在使用由原子操作保护的内存单元时就需要特别小心。
例如, 在实现 sleep mutex 时, 我们就必须使用
<function>atomic_cmpset</function> 而不是
<function>atomic_set</function> 来打开
<constant>MTX_CONTESTED</constant> 位。 这样做的原因是,
我们需要把 <structfield>mtx_lock</structfield> 的值读到某个变量,
并据此进行决策。 然而, 我们读到的值可能是过时的,
也可能在我们进行决策的过程中发生变化。 因此, 当执行
<function>atomic_set</function> 时, 最终可能会对另一值进行置位,
而不是我们进行决策的那一个。 这就必须通过
<function>atomic_cmpset</function> 来保证只有在我们的决策依据是最新的时,
才对相应的变量进行置位。</para>
<para>最后, 原子操作只允许一次更新或读一个内存单元。
需要原子地更新多个单元时, 就必须使用锁来代替它了。
例如, 如果需要更新两个相互关联的计数器时,
就必须使用锁, 而不是两次单独的原子操作了。</para>
</sect2>
<sect2>
<title>读锁与写锁</title>
<indexterm><primary>read locks(读锁)</primary></indexterm>
<indexterm><primary>write locks(写锁)</primary></indexterm>
<para>读锁并不需要像写锁那样强。 这两种类型的锁,
都需要确保通过它们访问的不是过时的数据。 然而,
只有写操作必须是排他的, 而多个线程则可以安全地读同一变量的值。
使用不同类型的锁用于读和写操作有许多各自不同的实现方式。</para>
<para>第一种方法是用 sx 锁, 它可以用于实现写时使用的排他锁,
而读时则作为共享锁。 这种方法十分简单明了。</para>
<para>第二种方法则略显晦涩。 可以用多个锁来保护同一数据元。
读时, 只需锁其中的一个读锁即可。 然而, 如果要写数据的话,
则需要首先上所有的写锁。 这会大大提高写操作的代价,
但当可能以多种方式访问数据时却可能非常有用。 例如,
父进程指针是同时受
<varname>proctree_lock</varname> sx 锁和进程 mutex 保护的。
在只希望检查已锁进程的父进程时, 用 proc 锁更为方便。
但是, 其它一些地方, 例如
<function>inferior</function> 这类需要通过父指针在进程树上进行搜索,
并对每个进程上锁的地方就不能这样做了,
否则, 将无法保证在对我们所获得的结果执行操作时,
之前检查时的状况依旧有效。</para>
</sect2>
<sect2>
<title>上锁状态和结果</title>
<para>如果您需要使用锁来保持所检查变量的状态, 并据此执行某些操作时,
是不能仅仅在读变量之前对其上锁, 并在执行操作之前解锁的。
过早解锁将使变量再次可变, 这可能会导致之前所做的决策失效。
因此, 在所做检测引发的动作结束之前, 必须继续保持上锁状态。</para>
</sect2>
</sect1>
<sect1 id="smp-design">
<title>架构与设计概览</title>
<sect2>
<title>对中断的处理</title>
<indexterm><primary>interrupt handling(中断处理)</primary></indexterm>
<para>与许多其它多线程 &unix; 内核所采取的模式类似, FreeBSD
会赋予中断处理程序独立的线程上下文,
这样做能够让中断线程在遇到锁时阻塞。 但为了避免不必要的延迟,
中断线程在内核中, 是以实时线程的优先级运行的。 因此,
中断处理程序不应执行过久, 以免饿死其它内核线程。 此外,
由于多个处理程序可以分享同一中断线程, 中断处理程序不应休眠,
或使用可能导致休眠的锁, 以避免将其它中断处理程序饿死。</para>
<indexterm><primary>interrupt threads(中断线程)</primary></indexterm>
<para>目前在 FreeBSD 中的中断线程是指重量级中断线程。
这样称呼它们的原因在于, 转到中断线程需要执行一次完整的上下文切换操作。
在最初的实现中, 内核不允许抢占, 因此中断在打断内核线程之前,
必须等待内核线程阻塞或返回用户态之后才能执行。</para>
<indexterm><primary>latency(响应时间)</primary></indexterm>
<indexterm><primary>preemption(抢占)</primary></indexterm>
<para>为了解决响应时间问题, FreeBSD 内核现在采用了抢占式调度策略。
目前, 只有释放休眠 mutex 或发生中断时才能抢断内核线程,
但最终目标是在 FreeBSD 上实现下面所描述的全抢占式调度策略。</para>
<para>并非所有的中断处理程序都在独立的线程上下文中执行。
相反, 某些处理程序会直接在主中断上下文中执行。 这些中断处理程序,
现在被错误地命名为
<quote>快速</quote> 中断处理程序, 因为早期版本的内核中使用了
<constant>INTR_FAST</constant> 标志来标记这些处理程序。
目前只有时钟中断和串口 I/O 设备中断采用这一类型。
由于这些处理程序没有独立的上下文, 因而它们都不能获得阻塞性锁,
因此也就只能使用自旋 mutex。</para>
<indexterm><primary>context switches(上下文切换现场切换CPU运行环境切换)</primary></indexterm>
<para>最后, 还有一种称为轻量级上下文切换的优化,
可以在 MD 代码中使用。 因为中断线程都是在内核上下文中执行的,
所以它可以借用任意进程的 vmspace (虚拟内存地址空间)。 因此,
在轻量级上下文切换中, 切换到中断线程并不切换对应的 vmspace
而是借用被中断线程的 vmspace。 为确保被中断线程的 vmspace
不在中断处理过程中消失, 被中断线程在中断线程不再借用其 vmspace
之前是不允许执行的。 刚才提到的情况可能在中断线程阻塞或完成时发生。
如果中断线程发生阻塞, 则它再次进入可运行状态时将使用自己的上下文,
这样一来, 就可以释放被中断的线程了。</para>
<para>这种优化的坏处在于它们和硬件紧密相关, 而且实现比较复杂,
因此只有在这样做能带来大幅性能改善时才应采用。
目前这样说可能还为时过早, 而且事实上可能会反而导致性能下降,
因为几乎所有的中断处理程序都会立即被全局锁 (Giant) 阻塞,
而这种阻塞将进而需要线程修正。 另外, Mike Smith
提议采用另一种方式来处理中断线程:</para>
<orderedlist>
<listitem>
<para>每个中断处理程序分为两部分, 一个在主中断上下文中运行的主体
(predicate) 和一个在自己的线程上下文中执行的处理程序 (handler)。</para>
</listitem>
<listitem>
<para>如果中断处理程序拥有主体, 则当触发中断时, 执行该主体。
如果主体返回真, 则认为该中断被处理完毕, 内核从中断返回。
如果主体返回假, 或者中断没有主体, 则调度运行线程式处理程序。</para>
</listitem>
</orderedlist>
<para>在这一模式中适当地采用轻量级上下文切换可能是非常复杂的。
因为我们可能会希望在未来改变这一模式, 因此现在最好的方案,
应该是暂时推迟在轻量级上下文切换之上的工作,
以便进一步完善中断处理架构, 随后再考察轻量级上下文切换是否适用。</para>
</sect2>
<sect2>
<title>内核抢占与临界区</title>
<sect3>
<title>内核抢占简介</title>
<para>内核抢占的概念很简单, 其基本思想是 CPU 总应执行优先级最高的工作。
当然, 至少在理想情况下是这样。 有些时候,
达成这一理想的代价会十分高昂, 以至于在这些情况下抢占会得不偿失。</para>
<para>实现完全的内核抢占十分简单: 在调度将要执行的线程并放入运行队列时,
检查它的优先级是否高于目前正在执行的线程。 如果是这样的话,
执行一次上下文切换并立即开始执行该线程。</para>
<para>尽管锁能够在抢占时保护多数数据, 但内核并不是可以安全地处处抢占的。
例如, 如果持有自旋 mutex 的线程被抢占, 而新线程也尝试获得同一自旋
mutex 新线程就可能一直自旋下去,
因为被中断的线程可能永远没有机会运行了。 此外, 某些代码, 例如在 Alpha 上的
<function>exec</function> 对进程地址空间编号进行赋值的代码也不能被抢断,
因为它被用来支持实际的上下文切换操作。 在这些代码段中,
会通过使用临界区来临时禁用抢占。</para>
</sect3>
<sect3>
<title>临界区</title>
<indexterm><primary>critical sections(临界区)</primary></indexterm>
<para>临界区 API 的责任是避免在临界区内发生上下文切换。
对于完全抢占式内核而言, 除了当前线程之外的其它线程的每个
<function>setrunqueue</function> 都是抢断点。
<function>critical_enter</function> 的一种实现方式是设置一线程私有标记,
并由其对应方清除。 如果调用
<function>setrunqueue</function> 时设置了这个标志,
则无论新线程和当前线程相比其优先级高低, 都不会发生抢占。
然而, 由于临界区会在自旋 mutex 中用于避免上下文切换,
而且能够同时获得多个自旋 mutex 因此临界区 API 必须支持嵌套。
由于这个原因, 目前的实现中采用了嵌套计数,
而不仅仅是单个的线程标志。</para>
<para>为了尽可能缩短响应时间, 在临界区中的抢占被推迟,
而不是直接丢弃。 如果线程应被抢断, 并被置为可运行,
而当前线程处于临界区, 则会设置一线程私有标志,
表示有一个尚未进行的抢断操作。 当最外层临界区退出时,
会检查这一标志, 如果它被置位, 则当前线程会被抢断,
以允许更高优先级的线程开始运行。</para>
<indexterm><primary>spin mutexes(自旋 mutex)</primary></indexterm>
<indexterm><primary>mutexes(同时/独占, mutual exclusion)</primary><secondary>spin(自旋)</secondary></indexterm>
<para>中断会引发一个和自旋 mutex 有关的问题。
如果低级中断处理程序需要锁, 它就不能中断任何需要该锁的代码,
以避免可能发生的损坏数据结构的情况。 目前,这一机制是透过临界区 API
以 <function>cpu_critical_enter</function> 和
<function>cpu_critical_exit</function> 函数的形式实现的。
目前这一 API 会在所有 FreeBSD
所支持的平台上禁用和重新启用中断。 这种方法并不是最优的,
但它更易理解, 也更容易正确地实现。 理论上, 这一辅助 API
只需要配合在主中断上下文中的自旋 mutex 使用。 然而,
为了让代码更为简单, 它被用在了全部自旋 mutex
甚至包括所有临界区上。 将其从 MI API 中剥离出来放入 MD API
并只在需要使用它的 MI API 的自旋 mutex 实现中使用可能会有更好的效果。
如果我们最终采用了这种实现方式, 则 MD API
可能需要改名, 以彰显其为一单独 API 这一事实。</para>
</sect3>
<sect3>
<title>设计折衷</title>
<para>如前面提到的, 当完全抢占并非总能提供最佳性能时,
采取了一些折衷的措施。</para>
<para>第一处折衷是, 抢占代码并不考虑其它 CPU 的存在。
假设我们有两个 CPU A
和 B 其中 A 上线程的优先级为 4
而 B 上线程的优先级是 2。 如果 CPU B 令一优先级为 1
的线程进入可运行状态, 则理论上, 我们希望 CPU A 切换至这一新线程,
这样就有两个优先级最高的线程在运行了。 然而, 确定哪个
CPU 在抢占时更合适, 并通过 IPI 向那个 CPU 发出信号,
并完成相关的同步工作的代价十分高昂。 因此, 目前的代码会强制 CPU B
切换至更高优先级的线程。 请注意这样做仍会让系统进入更好的状态,
因为 CPU B 会去执行优先级为 1 而不是 2 的那个线程。</para>
<para>第二处折衷是限制对于实时优先级的内核线程的立即抢占。
在前面所定义的抢占操作的简单情形中, 低优先级总会被立即抢断
(或在其退出临界区后被抢断)。 然而, 许多在内核中执行的线程,
有很多只会执行很短的时间就会阻塞或返回用户态。 因此,
如果内核抢断这些线程并执行其它非实时的内核线程,
则内核可能会在这些线程马上要休眠或执行完毕之前切换出去。
这样一来, CPU 就必须调整快取缓存以配合新线程的执行。
当内核返回到被抢断的线程时, 它又需要重新填充之前丢失的快取缓存信息。
此外, 如果内核能够将对将阻塞或返回用户态的那个线程的抢断延迟到这之后的话,
还能够免去两次额外的上下文切换。 因此, 默认情况下,
只有在优先级较高的线程是实时线程时, 抢占代码才会立即执行抢断操作。</para>
<para>启用针对所有内核线程的完全抢占对于调试非常有帮助,
因为它会暴露出更多的竞态条件 (race conditions)。
在难以模拟这些竞态条件的单处理器系统中, 这显得尤其有用。
因此, 我们提供了内核选项 <literal>FULL_PREEMPTION</literal>
来启用针对所有内核线程的抢占, 这一选项主要用于调试目的。</para>
</sect3>
</sect2>
<sect2>
<title>线程迁移</title>
<indexterm><primary>thread migration(线程迁移)</primary></indexterm>
<para>简单地说, 线程从一个 CPU 移动到另一个上的过程称作迁移。
在非抢占式内核中, 这只会在明确定义的点, 例如调用
<function>msleep</function> 或返回至用户态时才会发生。
但是, 在抢占式内核中, 中断可能会在任何时候强制抢断,
并导致迁移。 对于 CPU 私有的数据而言这可能会带来一些负面影响, 因为除
<varname>curthread</varname> 和 <varname>curpcb</varname>
以外的数据都可能在迁移过程中发生变化。 由于存在潜在的线程迁移,
使得未受保护的 CPU 私有数据访问变得无用。 这就需要在某些代码段禁止迁移,
以获得稳定的 CPU 私有数据。</para>
<indexterm><primary>critical sections(临界区)</primary></indexterm>
<para>目前我们采用临界区来避免迁移, 因为它们能够阻止上下文切换。
但是, 这有时可能是一种过于严厉的限制,
因为临界区实际上会阻止当前处理器上的中断线程。 因而,
提供了另一个 API 用以指示当前进程在被抢断时,
不应迁移到另一 CPU。</para>
<para>这组 API 也叫线程牵制, 它由调度器提供。 这组 API 包括两个函数:
<function>sched_pin</function> 和
<function>sched_unpin</function>。 这两个函数用于管理线程私有的计数
<varname>td_pinned</varname>。 如果嵌套计数大于零, 则线程将被锁住,
而线程开始运行时其嵌套计数为零, 表示处于未牵制状态。 所有的调度器实现中,
都要求保证牵制线程只在它们首次调用 <function>sched_pin</function>
时所在的 CPU 上运行。 由于只有线程自己会写嵌套计数,
而只有其它线程在受牵制线程没有执行, 且持有
<varname>sched_lock</varname> 锁时才会读嵌套计数, 因此访问
<varname>td_pinned</varname> 不必上锁。
<function>sched_pin</function> 函数会使嵌套计数递增,
而 <function>sched_unpin</function> 则使其递减。
注意, 这些函数只操作当前线程, 并将其绑定到其执行它时所处的 CPU 上。
要将任意线程绑定到指定的 CPU 上, 则应使用 <function>sched_bind</function> 和
<function>sched_unbind</function>。</para>
</sect2>
<sect2>
<title>调出 (Callout)</title>
<para>内核机制 <function>timeout</function> 允许内核服务注册函数,
以作为 <function>softclock</function> 软件中断的一部分来执行。
事件将基于所希望的时钟嘀嗒的数目进行, 并在大约指定的时间回调用户提供的函数。</para>
<para>未决 timeout (超时) 事件的全局表是由一全局 mutex
<varname>callout_lock</varname> 保护的; 所有对 timeout 表的访问,
都必须首先拿到这个 mutex。 当 <function>softclock</function>
唤醒时, 它会扫描未决超时表, 并找出应启动的那些。 为避免锁逆序,
<function>softclock</function> 线程会在调用所提供的
<function>timeout</function> 回调函数时首先释放
<varname>callout_lock</varname> mutex。
如果在注册时没有设置 <constant>CALLOUT_MPSAFE</constant> 标志,
则在调用调出函数之前, 还会抓取全局锁, 并在之后释放。 其后,
<varname>callout_lock</varname> mutex 会在继续处理前再次获得。
<function>softclock</function> 代码在释放这个 mutex
时会非常小心地保持表的一致状态。 如果启用了 <constant>DIAGNOSTIC</constant>
则每个函数的执行时间会被记录, 如果超过了某一阈值, 则会产生警告。</para>
</sect2>
</sect1>
<sect1 id="smp-lock-strategies">
<title>特定数据的锁策略</title>
<sect2>
<title>凭据</title>
<indexterm><primary>credentials(凭据)</primary></indexterm>
<para><structname>struct ucred</structname> 是内核内部的凭据结构体,
它通常作为内核中以进程为导向的访问控制的依据。
BSD-派生的系统采用一种 <quote>写时复制</quote> 的模型来处理凭据数据:
同一凭据结构体可能存在多个引用, 如果需要对其进行修改,
则这个结构体将被复制、 修改, 然后替换该引用。
由于在打开时用于实现访问控制的凭据快取缓存广泛存在,
这种做法会极大地节省内存。 在迁移到细粒度的 SMP 时,
这一模型也省去了大量的锁操作, 因为只有未共享的凭据才能实施修改,
因而避免了在使用共享凭据时额外的同步操作。</para>
<para>凭据结构体只有一个引用时, 被认为是可变的;
不允许改变共享的凭据结构体, 否则将可能导致发生竞态条件。
<structfield>cr_mtxp</structfield> mutex 用于保护
<structname>struct ucred</structname> 的引用计数,
以维护其一致性。 使用凭据结构体时, 必须在使用过程中保持有效的引用,
否则它就可能在这个不合理的消费者使用过程中被释放。</para>
<para><structname>struct ucred</structname> mutex 是一种叶
mutex 出于性能考虑, 它通过 mutex 池实现。</para>
<para>由于多用于访问控制决策, 凭据通常情况下是以只读方式访问的, 此时一般应使用
<structfield>td_ucred</structfield> 因为它不需要上锁。
当更新进程凭据时, 检查和更新过程中必须持有 <literal>proc</literal>
锁。 检查和更新操作必须使用 <structfield>p_ucred</structfield>
以避免检查时和使用时的竞态条件。</para>
<para>如果所调系统调用将在更新进程凭据之后进行访问控制检查, 则
<structfield>td_ucred</structfield> 也必须刷新为当前进程的值。
这样做能够避免修改后使用过时的凭据。 内核会自动在进程进入内核时,
将线程结构体的 <structfield>td_ucred</structfield> 指针刷新为进程的
<structfield>p_ucred</structfield> 以保证内核访问控制能用到新的凭据。</para>
</sect2>
<sect2>
<title>文件描述符和文件描述符表</title>
<para>详细内容将在稍后增加。</para>
</sect2>
<sect2>
<title>Jail 结构体</title>
<indexterm><primary>Jail(囚禁)</primary></indexterm>
<para><structname>struct prison</structname> 保存了用于维护那些通过
&man.jail.2; API 创建的 jail 所用到的管理信息。 这包括 jail
的主机名、 IP 地址, 以及一些相关的设置。 这个结构体包含引用计数,
因为指向这一结构体实例的指针会在多种凭据结构之间共享。
用了一个 mutex <structfield>pr_mtx</structfield>
来保护对引用计数以及所有 jail 结构体中可变变量的读写访问。
有一些变量只会在创建 jail 的时刻发生变化, 只需持有有效的
<structname>struct prison</structname> 就可以开始读这些值了。
关于每个项目具体的上锁操作的文档,
可以在 <filename>sys/jail.h</filename> 的注释中找到。</para>
</sect2>
<sect2>
<title>MAC 框架</title>
<indexterm><primary>MAC(强制访问控制)</primary></indexterm>
<para>TrustedBSD MAC 框架会以 <structname>struct
label</structname> 的形式维护一系列内核对象的数据。
一般来说, 内核中的 label (标签) 是由与其对应的内核对象同样的锁保护的。
例如, <structname>struct vnode</structname> 上的
<structfield>v_label</structfield> 标签是由其所在 vnode 上的
vnode 锁保护的。</para>
<para>除了嵌入到标准内核对象中的标签之外, MAC
框架也需要维护一组包含已注册的和激活策略的列表。 策略表和忙计数由一个全局
mutex (<varname>mac_policy_list_lock</varname>) 保护。
由于能够同时并行地进行许多访问控制检查, 对策略表的只读访问,
在增减忙计数时, 框架的入口处需要首先持有这个 mutex。
MAC 入口操作的过程中并不需要长时间持有此 mutex -- 有些操作,
例如文件系统对象上的标签操作 -- 是持久的。 要修改策略表,
例如在注册和解除注册策略时, 需要持有此 mutex 而且要求引用计数为零,
以避免在用表时对其进行修改。</para>
<para>对于需要等待表进入闲置状态的线程, 提供了一个条件变量
<varname>mac_policy_list_not_busy</varname>
但这一条件变量只能在调用者没有持有其它锁时才能使用,
否则可能会引发锁逆序问题。 忙计数在整个框架中事实上还扮演了某种形式的
共享/排他 锁的作用: 与 sx 锁不同的地方在于,
等待列表进入闲置状态的线程可以饿死, 而不是允许忙计数和其它在 MAC
框架入口 (或内部) 的锁之间的逆序情况。</para>
</sect2>
<sect2>
<title>模块</title>
<indexterm><primary>kernel modules(内核模块)</primary></indexterm>
<para>对于模块子系统, 用于保护共享数据使用了一个单独的锁, 它是一个 共享/排他
(SX) 锁, 许多情况需要获得它 (以共享或排他的方式)
因此我们提供了几个方便使用的宏来简化对这个锁的访问,
这些宏可以在 <filename>sys/module.h</filename> 中找到,
其用法都非常简单明了。 这个锁保护的主要是
<structname>module_t</structname> (当以共享方式上锁)
和全局的 <structname>modulelist_t</structname> 这两个结构体,
以及模块。 要更进一步理解这些锁策略, 需要仔细阅读
<filename>kern/kern_module.c</filename> 的源代码。</para>
</sect2>
<sect2>
<title>Newbus 设备树</title>
<indexterm><primary>Newbus</primary></indexterm>
<para>newbus 系统使用了一个 sx 锁。 读的一方应持有共享 (读)
锁 (&man.sx.slock.9;) 而写的一方则应持有排他 (写) 锁
(&man.sx.xlock.9;)。 内部函数一般不需要进行上锁,
而外部可见的则应根据需要上锁。 有些项目不需上锁,
因为这些项目在全程是只读的,
(例如 &man.device.get.softc.9;) 因而并不会产生竞态条件。
针对 newbus 数据结构的修改相对而言非常少, 因此单个的锁已经足够使用,
而不致造成性能折损。</para>
</sect2>
<sect2>
<title>管道</title>
<para>...</para>
</sect2>
<sect2>
<title>进程和线程</title>
<para>- 进程层次结构</para>
<para>- proc 锁及其参考</para>
<para>- 在系统调用过程中线程私有的 proc 项副本,
包括 td_ucred</para>
<para>- 进程间操作</para>
<para>- 进程组和会话</para>
</sect2>
<sect2>
<title>调度器</title>
<indexterm><primary>scheduler(调度器)</primary></indexterm>
<para>本文在其它地方已经提供了很多关于 <varname>sched_lock</varname>
的参考和注释。</para>
</sect2>
<sect2>
<title>Select 和 Poll</title>
<para><function>select</function> 和
<function>poll</function> 这两个函数允许线程阻塞并等待文件描述符上的事件 --
最常见的情况是文件描述符是否可读或可写。</para>
<para>...</para>
</sect2>
<sect2>
<title>SIGIO</title>
<para>SIGIO 服务允许进程请求在特定文件描述符的读/写状态发生变化时,
将 SIGIO 信号群发给其进程组。 任意给定内核对象上,
只允许一进程或进程组注册 SIGIO 这个进程或进程组称为属主 (owner)。
每一支持 SIGIO 注册的对象, 都包含一指针字段, 如果对象未注册则为
<constant>NULL</constant>
否则是一指向描述这一注册的 <structname>struct sigio</structname> 的指针。
这一字段由一全局 mutex
<varname>sigio_lock</varname> 保护。 调用 SIGIO 维护函数时,
必须以 <quote>传引用</quote> 方式传递这一字段,
以确保本地注册副本的中这个字段不脱离锁的保护。</para>
<para>每个关联到进程或进程组的注册对象, 都会分配一
<structname>struct sigio</structname> 结构, 并包括指回该对象的指针、
属主、 信号信息、 凭据, 以及关于这一注册的一般信息。
每个进程或进程组都包含一个已注册 <structname>struct sigio</structname>
结构体的列表, 对进程来说是
<structfield>p_sigiolst</structfield> 而对进程组则是
<structfield>pg_sigiolst</structfield>。 这些表由相应的进程或进程组锁保护。
除了用以将
<structname>struct sigio</structname> 连接到进程组上的
<structfield>sio_pgsigio</structfield> 字段之外, 在 <structname>struct
sigio</structname> 中的多数字段在注册过程中都是不变量。
一般而言, 开发人员在实现新的支持 SIGIO 的内核对象时,
会希望避免在调用 SIGIO 支持函数, 例如 <function>fsetown</function>
或 <function>funsetown</function> 持有结构体锁,
以免去需要在结构体锁和全局 SIGIO 锁之间定义锁序。
通常可以通过提高结构体上的引用计数来达到这样的目的,
例如, 在进行管道操作时, 使用引用某个管道的文件描述符这样的操作,
就可以照此办理。</para>
</sect2>
<sect2>
<title>Sysctl</title>
<para><function>sysctl</function> MIB 服务会从内核内部,
以及用户态的应用程序以系统调用的方式触发。
这会引发至少两个和锁有关的问题: 其一是对维持命名空间的数据结构的保护,
其二是与那些通过 sysctl 接口访问的内核变量和函数之间的交互。
由于 sysctl 允许直接导出 (甚至修改) 内核统计数据以及配置参数, sysctl
机制必须知道这些变量相应的上锁语义。 目前, sysctl 使用一个全局 sx
锁来实现对 <function>sysctl</function> 操作的串行化;
然而, 这些是假定用全局锁保护的, 并且没有提供其它保护机制。
这一节的其余部分将详细介绍上锁和 sysctl 相关变动的语义。</para>
<para>- 需要将 sysctl 更新值所进行的操作的顺序, 从原先的读旧值、
copyin 和 copyout、 写新值, 改为 copyin、 上锁、 读旧值、 写新值、
解锁、 copyout。 一般的 sysctl 只是 copyout 旧值并设置它们 copyin
所得到的新值, 仍然可以采用旧式的模型。 然而,
对所有 sysctl 处理程序采用第二种模型并避免锁操作方面,
第二种方式可能更规矩一些。</para>
<para>- 对于通常的情况, sysctl 可以内嵌一个 mutex 指针到 SYSCTL_FOO
宏和结构体中。 这对多数 sysctl 都是有效的。 对于使用 sx
锁、 自旋 mutex 或其它除单一休眠 mutex 之外的锁策略,
可以用 SYSCTL_PROC 节点来完成正确的上锁。</para>
</sect2>
<sect2>
<title>任务队列 (Taskqueue)</title>
<para>任务队列 (taskqueue) 的接口包括两个与之关联的用于保护相关数据的锁。
<varname>taskqueue_queues_mutex</varname> 是用于保护
<varname>taskqueue_queues</varname> TAILQ 的锁。
与这个系统关联的另一个 mutex 锁是位于
<structname>struct taskqueue</structname> 结构体上。
在此处使用同步原语的目的在于保护 <structname>struct
taskqueue</structname> 中数据的完整性。 应注意的是,
并没有单独的、 帮助用户对其自身的工作进行锁的细化用的宏,
因为这些锁基本上不会在
<filename>kern/subr_taskqueue.c</filename> 以外的地方用到。</para>
</sect2>
</sect1>
<sect1 id="smp-implementation-notes">
<title>实现说明</title>
<sect2>
<title>休眠队列</title>
<para>休眠队列是一种用于保存同处一个等待通道 (wait channel)
上休眠线程列表的数据结构。 在等待通道上,
每个处于非睡眠状态的线程都会携带一个休眠队列结构。
当线程在等待通道上发生阻塞时, 它会将休眠队列结构体送给那个等待通道。
与等待通道关联的休眠队列则保存在一个散列表中。</para>
<para>休眠队列散列表中保存了包含至少一个阻塞线程的等待通道上的休眠队列。
这个散列表上的项称作 sleepqueue (休眠队列) 链。 它包含了一个休眠队列的链表,
以及一个自旋 mutex。 此处的自旋 mutex 用于保护休眠队列表,
以及其上休眠队列结构的内容。 一个等待通道上只会关联一个休眠队列。
如果有多个线程在同一等待通道上阻塞,
则休眠队列中将关联除第一个线程之外的全部线程。 当从休眠队列中删除线程时,
如果它不是唯一的阻塞的休眠线程, 则会获得主休眠队列的空闲表上的休眠队列结构。
最后一个线程会在恢复运行时获得主休眠队列。
由于线程有可能以和加入休眠队列不同的次序从其中删除,
因此, 线程离开队列时可能会携带与其进入时不同的休眠队列。</para>
<para><function>sleepq_lock</function> 函数会锁住指定等待通道上休眠队列链的自旋
mutex。 <function>sleepq_lookup</function>
函数会在主休眠队列散列表中查找给定的等待通道。 如果没有找到主休眠队列,
它会返回 <constant>NULL</constant>。
<function>sleepq_release</function> 函数会对给定等待通道所关联的自旋
mutex 进行解锁。</para>
<para>将线程加入休眠队列是通过
<function>sleepq_add</function> 来完成的。
这个函数的参数包括等待通道、 指向保护等待通道的 mutex 的指针、
等待消息描述串, 以及一个标志掩码。 调用此函数之前, 应通过
<function>sleepq_lock</function> 为休眠队列链上锁。
如果等待通道不是通过 mutex 保护的 (或者它由全局锁保护)
则应将 mutex 指针设置为
<constant>NULL</constant>。 而 flags (标志) 参数则包括了一个类型字段,
用以表示线程即将加入到的休眠队列的类型,
以及休眠是否是可中断的 (<constant>SLEEPQ_INTERRUPTIBLE</constant>)。
目前只有两种类型的休眠队列: 通过
<function>msleep</function> 和 <function>wakeup</function>
函数管理的传统休眠队列 (<constant>SLEEPQ_MSLEEP</constant>)
以及基于条件变量的休眠队列 (<constant>SLEEPQ_CONDVAR</constant>)。
休眠队列类型和锁指针这两个参数完全是用于内部的断言检查。 调用
<function>sleepq_add</function> 的代码, 应明示地在关联的 sleepqueue 链透过
<function>sleepq_lock</function> 进行上锁之后, 并使用等待函数在休眠队列上阻塞之前解锁所有用于保护等待通道的
interlock。</para>
<para>通过使用
<function>sleepq_set_timeout</function> 可以为休眠设置超时。
这个函数的参数包括等待通道, 以及以相对时钟嘀嗒数为单位的超时时间。
如果休眠应被某个到来的信号打断, 则还应调用
<function>sleepq_catch_signals</function> 函数,
这个函数唯一的参数就是等待通道。 如果此线程已经有未决信号,
则 <function>sleepq_catch_signals</function> 将返回信号编号;
其它情况下, 其返回值则是 0。</para>
<para>一旦将线程加入到休眠队列中,
就可以使用 <function>sleepq_wait</function> 函数族之一将其阻塞了。
目前总共提供了四个等待函数, 使用哪个取决于调用这是否希望允许使用超时、
收到信号, 或用户态线程调度器打断休眠状态。
其中, <function>sleepq_wait</function> 函数简单地等待,
直到当前线程通过某个唤醒 (wakeup) 函数显式地恢复运行;
<function>sleepq_timedwait</function> 函数则等待,
直到当前线程被显式地唤醒, 或者达到早前使用 <function>sleepq_set_timeout</function>
设置的超时; <function>sleepq_wait_sig</function> 函数会等待显式地唤醒,
或者其休眠被中断; 而
<function>sleepq_timedwait_sig</function> 函数则等待显式地唤醒、
达到用 <function>sleepq_set_timeout</function>
设置的超时, 或线程的休眠被中断这三种条件之一。
所有这些等待函数的第一个参数都是等待通道。
除此之外, <function>sleepq_timedwait_sig</function>
的第二个参数是一个布尔值, 表示之前调用 <function>sleepq_catch_signals</function>
时是否有发现未决信号。</para>
<para>如果线程被显式地恢复运行, 或其休眠被信号终止,
则等待函数会返回零, 表示休眠成功。
如果线程的休眠被超时或用户态线程调度器打断, 则会返回相应的 errno 数值。
需要注意的是, 因为 <function>sleepq_wait</function> 只能返回 0
因此调用者不能指望它返回什么有用信息, 而应假定它完成了一次成功的休眠。
同时, 如果线程的休眠时间超时, 并同时被终止, 则
<function>sleepq_timedwait_sig</function> 将返回一个表示发生超时的错误代码。
如果返回错误代码是
0 而且使用 <function>sleepq_wait_sig</function>
或 <function>sleepq_timedwait_sig</function> 来执行阻塞, 则应调用
<function>sleepq_calc_signal_retval</function> 来检查是否有未决信号,
并据此选择合适的返回值。 较早前调用
<function>sleepq_catch_signals</function> 得到的信号编号,
应作为参数传给
<function>sleepq_calc_signal_retval</function>。</para>
<para>在同一休眠通道上休眠的线程,
可以由 <function>sleepq_broadcast</function> 或
<function>sleepq_signal</function> 函数来显式地唤醒。
这两个函数的参数均包括希望唤醒的等待通道、
将唤醒线程的优先级 (priority) 提高到多少,
以及一个标志 (flags) 参数表示将要恢复运行的休眠队列类型。
优先级参数将作为最低优先级, 如果将恢复的线程的优先级比此参数更高
(数值更低) 则其优先级不会调整。 标志参数主要用于函数内部的断言,
用以确认休眠队列没有被当做错误的类型对待。 例如,
条件变量函数不应恢复传统休眠队列的执行。 <function>sleepq_broadcast</function>
函数将恢复所有指定休眠通道上的阻塞线程,
而 <function>sleepq_signal</function> 则只恢复在等待通道上优先级最高的阻塞线程。
在调用这些函数之前, 应首先使用
<function>sleepq_lock</function> 对休眠队列上锁。</para>
<para>休眠线程也可以通过调用 <function>sleepq_abort</function> 函数来中断其休眠状态。
这个函数只有在持有 <varname>sched_lock</varname> 时才能调用,
而且线程必须处于休眠队列之上。 线程也可以通过使用
<function>sleepq_remove</function> 函数从指定的休眠队列中删除。
这个函数包括两个参数, 即休眠通道和线程,
它只在线程处于指定休眠通道的休眠队列之上时才将其唤醒。
如果线程不在那个休眠队列之上, 或同时处于另一等待通道的休眠队列上,
则这个函数将什么都不做而直接返回。</para>
</sect2>
<sect2>
<title>十字转门 (turnstile)</title>
<indexterm><primary>turnstiles(十字转门)</primary></indexterm>
<para>- 与休眠队列的比较和不同。</para>
<para>- 查询/等待/释放 (lookup/wait/release)
- 介绍 TDF_TSNOBLOCK 竞态条件。</para>
<para>- 优先级传播。</para>
</sect2>
<sect2>
<title>关于 mutex 实现的一些细节</title>
<para>- 我们是否应要求 mtx_destroy() 持有 mutex
因为无法安全地断言它们没有被其它对象持有?</para>
<sect3>
<title>自旋 mutex</title>
<indexterm><primary>mutexes(同时/独占, mutual exclusion)</primary><secondary>spin(自旋)</secondary></indexterm>
<para>- 使用一临界区...</para>
</sect3>
<sect3>
<title>休眠 mutex</title>
<indexterm><primary>mutexes(同时/独占, mutual exclusion)</primary><secondary>sleep(休眠)</secondary></indexterm>
<para>- 描述 mutex 冲突时的竞态条件</para>
<para>- 为何在持有十字转门链锁时, 可以安全地读冲突 mutex 的 mtx_lock。</para>
</sect3>
</sect2>
<sect2>
<title>Witness</title>
<indexterm><primary>witness</primary></indexterm>
<para>- 它能做什么</para>
<para>- 它如何工作</para>
</sect2>
</sect1>
<sect1 id="smp-misc">
<title>其它话题</title>
<sect2>
<title>中断源和 ICU 抽象</title>
<para>- struct isrc</para>
<para>- pic 驱动</para>
</sect2>
<sect2>
<title>其它问题/话题</title>
<para>- 是否应将 interlock 传给
<function>sema_wait</function></para>
<para>- 是否应提供非休眠式 sx 锁?</para>
<para>- 增加一些关于正确使用引用计数的介绍。</para>
</sect2>
</sect1>
<glossary id="smp-glossary">
<title>术语表</title>
<glossentry id="smp-glossary-atomic">
<glossterm>原子</glossterm>
<glossdef>
<para>当遵循适当的访问协议时, 如果一操作的效果对其它所有 CPU
均可见, 则称其为原子操作。 狭义的原子操作是机器直接提供的。
就更高的抽象层次而言, 如果结构体的多个成员由一个锁保护,
则如果对它们的操作都是在上锁后、 解锁前进行的,
也可以称其为原子操作。</para>
<glossseealso>操作</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-block">
<glossterm>阻塞</glossterm>
<glossdef>
<para>线程等待锁、 资源或条件时被阻塞。
这一术语也因此被赋予了太多的意涵。</para>
<glossseealso>休眠</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-critical-section">
<glossterm>临界区</glossterm>
<glossdef>
<para>不允许发生抢占的代码段。 使用
&man.critical.enter.9; API 来表示进入和退出临界区。</para>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-MD">
<glossterm>MD</glossterm>
<glossdef>
<para>表示与机器/平台有关。</para>
<glossseealso>MI</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-memory-operation">
<glossterm>内存操作</glossterm>
<glossdef>
<para>内存操作包括读或写内存中的指定位置。</para>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-MI">
<glossterm>MI</glossterm>
<glossdef>
<para>表示与机器/平台无关。</para>
<glossseealso>MD</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-operation">
<glossterm>操作</glossterm>
<glosssee>内存操作</glosssee>
</glossentry>
<glossentry id="smp-glossary-primary-interrupt-context">
<glossterm>主中断上下文</glossterm>
<glossdef>
<para>主中断上下文表示当发生中断时所执行的那段代码。
这些代码可以直接运行某个中断处理程序, 或调度一异步终端线程,
以便为给定的中断源执行中断处理程序。</para>
</glossdef>
</glossentry>
<glossentry>
<glossterm>实时内核线程</glossterm>
<glossdef>
<para>一种高优先级的内核线程。 目前,
只有中断线程属于实时优先级的内核线程。</para>
<glossseealso>线程</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-sleep">
<glossterm>休眠</glossterm>
<glossdef>
<para>当进程由条件变量或通过 <function>msleep</function> 或
<function>tsleep</function> 阻塞并进入休眠队列时, 称其进入休眠状态。</para>
<glossseealso>阻塞</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-sleepable-lock">
<glossterm>可休眠锁</glossterm>
<glossdef>
<para>可休眠锁是一种在进程休眠时仍可持有的锁。
锁管理器 (lockmgr) 锁和 sx 锁是目前 FreeBSD 中仅有的可休眠锁。
最终, 某些 sx 锁, 例如 allproc (全部进程) 和 proctree (进程树)
锁将成为不可休眠锁。</para>
<glossseealso>休眠</glossseealso>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-thread">
<glossterm>线程</glossterm>
<glossdef>
<para>由 struct thread 所表达的内核线程。 线程可以持有锁,
并拥有独立的执行上下文。</para>
</glossdef>
</glossentry>
<glossentry id="smp-glossary-wait-channel">
<glossterm>等待通道</glossterm>
<glossdef>
<para>线程可以在其上休眠的内核虚拟地址。</para>
</glossdef>
</glossentry>
</glossary>
</chapter>

View file

@ -0,0 +1,643 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.10
$FreeBSD$
-->
<chapter id="oss">
<chapterinfo>
<authorgroup>
<author>
<firstname>Jean-Francois</firstname>
<surname>Dockes</surname>
<contrib>&cnproj.contributed.by;</contrib>
</author>
</authorgroup>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
<!-- 23 November 2001 -->
</chapterinfo>
<title>声音子系统</title>
<sect1 id="oss-intro">
<title>简介</title>
<indexterm><primary>sound subsystem(声音子系统)</primary></indexterm>
<para>FreeBSD声音子系统清晰地将通用声音处理问题与设备特定的问题分离
开来。这使得更容易加入对新设备的支持。</para>
<para> &man.pcm.4;框架是声音子系统的中心部分。它主要实现下面的组件:
</para>
<indexterm><primary>system call interface(系统调用接口)</primary></indexterm>
<itemizedlist>
<listitem>
<para>一个到数字化声音和混音器函数的系统调用接口read, write,
ioctls。ioctl命令集合兼容老的<emphasis>OSS</emphasis>
或<emphasis>Voxware</emphasis>接口,允许一般多媒体应用程序
不加修改地移植。</para>
</listitem>
<listitem>
<para>处理声音数据的公共代码(格式转换,虚拟通道)。</para>
</listitem>
<listitem>
<para>一个统一的软件接口,与硬件特定的音频接口模块接口
</para>
</listitem>
<listitem>
<para>对某些通用硬件接口ac97或共享的硬件特定代码
例如ISA DMA例程的额外支持。</para>
</listitem>
</itemizedlist>
<para>对特定声卡的支持是通过硬件特定的驱动程序来实现的,这些驱动程序
提供通道和混音器接口,插入到通用<devicename>pcm</devicename>代码中。
</para>
<para>本章中,术语<devicename>pcm</devicename>将指声音驱动程序的
中心,通用部分,这是对比硬件特定的模块而言的。</para>
<para>预期的驱动程序编写者当然希望从现有模块开始,并使用那些代码作为
最终参考。但是,由于声音代码十分简洁漂亮,这也基本上免除了注释。
本文档试图给出框架接口的一个概览,并回答改写现有代码时可能出现的
一些问题。</para>
<para>作为另外的途径,或者说除了从一个可工作的范例开始的办法之外,
你可以从<ulink url="http://people.FreeBSD.org/~cg/template.c">
http://people.FreeBSD.org/~cg/template.c</ulink>找到一个注释过的
驱动程序模板。</para>
</sect1>
<sect1 id="oss-files">
<title>文件</title>
<para>除<filename>/usr/src/sys/sys/soundcard.h</filename>中的公共
ioctl接口定义外所有的相关代码当前(FreeBSD 4.4)位于
<filename>/usr/src/sys/dev/sound/</filename>。</para>
<para>在<filename>/usr/src/sys/dev/sound/</filename>下面,
<filename>pcm/</filename>目录中保存着中心代码,
而<filename>isa/</filename>和<filename>pci/</filename>目录中有
ISA和PCI板的驱动程序。</para>
</sect1>
<sect1 id="pcm-probe-and-attach">
<title>探测,连接等</title>
<para>声音驱动程序使用与任何硬件驱动程序模块相同的方法探测和连接(设备)。
你可能希望浏览一下手册中<link linkend="isa-driver">ISA</link>或<link
linkend="pci">PCI</link>章节的内容来获取更多信息。</para>
<para>然而,声音驱动程序在某些方面又有些不同:</para>
<itemizedlist>
<listitem>
<para>他们将自己声明为<devicename>pcm</devicename>类设备,带有一个
设备私有结构<structname>struct snddev_info</structname></para>
<programlisting> static driver_t xxx_driver = {
"pcm",
xxx_methods,
sizeof(struct snddev_info)
};
DRIVER_MODULE(snd_xxxpci, pci, xxx_driver, pcm_devclass, 0, 0);
MODULE_DEPEND(snd_xxxpci, snd_pcm, PCM_MINVER, PCM_PREFVER,PCM_MAXVER);</programlisting>
<indexterm><primary>device driver(设备驱动程序)</primary><secondary>sound(声音)</secondary></indexterm>
<para>大多数声音驱动程序需要存储关于其设备的附加私有信息。私有数据
结构通常在连接例程中分配。其地址通过调用
<function>pcm_register()</function>和
<function>mixer_init()</function>传递给
<devicename>pcm</devicename>。后面<devicename>pcm</devicename>
将此地址作为调用声音驱动程序接口时的参数传递回来。</para>
</listitem>
<listitem>
<para>声音驱动程序的连接例程应当通过调用<function>mixer_init()
</function>向<devicename>pcm</devicename>声明它的MIXER或AC97
接口。对于MIXER接口这会接着引起调用
<link linkend="xxxmixer-init">
<function>xxxmixer_init()</function></link>。</para>
</listitem>
<listitem>
<para>声音驱动程序的连接例程通过调用
<function>pcm_register(dev, sc, nplay, nrec)</function>
向<devicename>pcm</devicename>声明其通用CHANNEL配置其中
<varname>sc</varname>是设备数据结构的地址,
在<devicename>pcm</devicename>以后的调用中将会用到它,
<varname>nplay</varname>和<varname>nrec</varname>是播放和录音
通道的数目。</para>
</listitem>
<listitem>
<para>声音驱动程序的连接例程通过调用
<function>pcm_addchan()</function>声明它的每个通道对象。这会在
<devicename>pcm</devicename>中建立起通道合成,并接着会引起调用
<link linkend="xxxchannel-init">
<function>xxxchannel_init()</function></link>
(译注:请参考原文)。</para>
</listitem>
<listitem>
<para>声音驱动程序的分离例程在释放其资源之前应当调用
<function>pcm_unregister()</function>。</para>
</listitem>
</itemizedlist>
<para>有两种可能的方法来处理非PnP设备</para>
<itemizedlist>
<listitem>
<para>使用<function>device_identify()</function>方法
(范例:<filename>sound/isa/es1888.c</filename>)。
<function>device_identify()</function>方法在已知地址探测硬件,
如果发现支持的设备就会创建一个新的pcm设备这个pcm设备接着
会被传递到probe/attach。</para>
</listitem>
<listitem>
<para>使用定制内核配置的方法为pcm设备设置适当的hints范例
<filename>sound/isa/mss.c</filename>)。</para>
</listitem>
</itemizedlist>
<para><devicename>pcm</devicename>驱动程序应当实现
<function>device_suspend</function>
<function>device_resume</function>和
<function>device_shutdown</function>例程,这样电源管理和模块卸载就能
正确地发挥作用。</para>
</sect1>
<sect1 id="oss-interfaces">
<title>接口</title>
<para><devicename>pcm</devicename>核心与声音驱动程序之间的接口以术语
<link linkend="kernel-objects">内核对象</link>的叫法来定义。</para>
<para>声音驱动程序通常提供两种主要的接口:
<emphasis>CHANNEL</emphasis>以及
<emphasis>MIXER</emphasis>或<emphasis>AC97</emphasis>。</para>
<para><emphasis>AC97</emphasis>是一个很小的硬件访问(寄存器读/写)
接口由驱动程序为带AC97编码解码器的硬件来实现。这种情况下实际的
MIXER接口由<devicename>pcm</devicename>中共享的AC97代码提供。
</para>
<sect2>
<title>CHANNEL接口</title>
<sect3>
<title>函数参数的通常注意事项</title>
<para>声音驱动程序通常用一个私有数据结构来描述他们的设备,驱动
程序所支持的播放和录音数据通道各有一个。</para>
<para>对于所有的CHANNEL接口函数第一个参数是一个不透明的指针。
</para>
<para>第二个参数是指向私有的通道数据结构的指针,
<function>channel_init()</function>是个例外,它的指针指向私有
设备结构(并返回由<devicename>pcm</devicename>以后使用的通道指针)。
</para>
</sect3>
<sect3>
<title>数据传输操作概览</title>
<para>对于声音数据传输,<devicename>pcm</devicename>核心与声音驱动
程序是通过一个由<structname>struct snd_dbuf</structname>描述的
共享内存区域进行通信的。</para>
<para><structname>struct snd_dbuf</structname>是
<devicename>pcm</devicename>私有的,声音驱动程序通过调用访问者
函数(<function>sndbuf_getxxx()</function>)来获得感兴趣的值。
</para>
<para>共享内存区域的大小等于
<function>sndbuf_getsize()</function>,并被分割为大小固定,且等于
<function>sndbuf_getblksz()</function>字节的很多块。</para>
<para>当播放时,常规的传输机制如下(将意思反过来就是录音):
</para>
<itemizedlist>
<listitem>
<para><devicename>pcm</devicename>开始时填充缓冲区,然后以
参数PCMTRIG_START调用声音驱动程序的<link
linkend="channel-trigger">
<function>xxxchannel_trigger()</function></link>
。</para>
</listitem>
<listitem>
<para>声音驱动程序接着安排以
<function>sndbuf_getblksz()</function>字节大小为块,重复将
整个内存区域(<function>sndbuf_getbuf()</function>
<function>sndbuf_getsize()</function>)传输到设备。对于每个
传输块回调<devicename>pcm</devicename>函数
<function>chn_intr()</function>(这通常在中断时间发生)。
</para>
</listitem>
<listitem>
<para><function>chn_intr()</function>安排将新数据拷贝到那些
数据已传输到设备(现在空闲)的区域,并对
<structname>snd_dbuf</structname>结构进行适当的更新。</para>
</listitem>
</itemizedlist>
</sect3>
<sect3 id="xxxchannel-init">
<title>channel_init</title>
<para>调用<function>xxxchannel_init()</function>来初始化每个播放
和录音通道。这个调用从声音驱动程序的连接例程中发起。(参看
<link linkend="pcm-probe-and-attach">探测和连接</link>一节)。</para>
<programlisting> static void *
xxxchannel_init(kobj_t obj, void *data,
struct snd_dbuf *b, struct pcm_channel *c, int dir)<co id="co-chinit-params">
{
struct xxx_info *sc = data;
struct xxx_chinfo *ch;
...
return ch;<co id="co-chinit-return">
}</programlisting>
<calloutlist>
<callout arearefs="co-chinit-params">
<para><varname>b</varname>为通道
<structname>struct snd_dbuf</structname>的地址。它应当在
函数中通过调用<function>sndbuf_alloc()</function>来初始化。
所用的缓冲区大小通常是设备'典型'传输大小的一个较小的倍数。
</para>
<para><varname>c</varname>为
<devicename>pcm</devicename>通道控制结构的指针。这是个不透明
指针。函数应当将它保存到局部通道结构中,在后面调用
<devicename>pcm</devicename>函数(例如:
<function>chn_intr(c)</function>)时会使用它。</para>
<para><varname>dir</varname>指示通道方向
<literal>PCMDIR_PLAY</literal>或
<literal>PCMDIR_REC</literal>)。</para>
</callout>
<callout arearefs="co-chinit-return">
<para>函数应当返回一个指针,此指针指向用于控制此通道的私有
区域。它将作为参数被传递到对其他通道接口的调用。
</para>
</callout>
</calloutlist>
</sect3>
<sect3>
<title>channel_setformat</title>
<para><function>xxxchannel_setformat()</function>应当按特定通道,
特定声音格式设置硬件。</para>
<programlisting> static int
xxxchannel_setformat(kobj_t obj, void *data, u_int32_t format)<co id="co-chsetformat-params">
{
struct xxx_chinfo *ch = data;
...
return 0;
}</programlisting>
<calloutlist>
<callout arearefs="co-chsetformat-params">
<para><varname>format</varname>为
<literal>AFMT_XXX value</literal>值之一
<filename>soundcard.h</filename>)。</para>
</callout>
</calloutlist>
</sect3>
<sect3>
<title>channel_setspeed</title>
<para><function>xxxchannel_setspeed()</function>按指定的取样速度
设置通道硬件,并返回返回可能调整过的速度。</para>
<programlisting> static int
xxxchannel_setspeed(kobj_t obj, void *data, u_int32_t speed)
{
struct xxx_chinfo *ch = data;
...
return speed;
}</programlisting>
</sect3>
<sect3>
<title>channel_setblocksize</title>
<para><function>xxxchannel_setblocksize()</function>设置块大小,
这是<devicename>pcm</devicename>与声音驱动程序,以及声音驱动
程序与设备之间的传输单位的大小。传输期间,每次传输这样大小的
数据后,声音驱动程序都应当调用<devicename>pcm</devicename>的
<function>chn_intr()</function>。</para>
<para>大多数驱动程序只注意这儿的块大小,因为当实际传输开始时应该
使用这个值。</para>
<programlisting> static int
xxxchannel_setblocksize(kobj_t obj, void *data, u_int32_t blocksize)
{
struct xxx_chinfo *ch = data;
...
return blocksize;<co id="co-chsetblocksize-return">
}</programlisting>
<calloutlist>
<callout arearefs="co-chsetblocksize-return">
<para>函数返回可能调整过的块大小。如果块大小真的变化了,
这种情况下应当调用<function>sndbuf_resize()</function>调整
缓冲区的大小。</para>
</callout>
</calloutlist>
</sect3>
<sect3 id="channel-trigger">
<title>channel_trigger</title>
<para><function>xxxchannel_trigger()</function>由
<devicename>pcm</devicename>来控制驱动程序中的实际传输操作。
</para>
<programlisting> static int
xxxchannel_trigger(kobj_t obj, void *data, int go)<co id="co-chtrigger-params">
{
struct xxx_chinfo *ch = data;
...
return 0;
}</programlisting>
<calloutlist>
<callout arearefs="co-chtrigger-params">
<para><varname>go</varname>定义当前调用的动作。可能的值为:
</para>
<itemizedlist>
<listitem>
<para><literal>PCMTRIG_START</literal>:驱动程序应当
启动从/到通道缓冲区的数据传输。如果需要,应当通过
<function>sndbuf_getbuf()</function>和
<function>sndbuf_getsize()</function>检取缓冲区的
基地址和大小。</para>
</listitem>
<listitem>
<para><literal>PCMTRIG_EMLDMAWR</literal> /
<literal>PCMTRIG_EMLDMARD</literal>:告诉驱动程序,
输入或输出缓冲区可能已被更新过了。大多数驱动程序只是
忽略这些调用。</para>
</listitem>
<listitem>
<para><literal>PCMTRIG_STOP</literal> /
<literal>PCMTRIG_ABORT</literal>:驱动程序应当停止当前
的传输。</para>
</listitem>
</itemizedlist>
</callout>
</calloutlist>
<note><para>如果驱动程序使用ISA DMA则应当在设备上执行动作前
调用<function>sndbuf_isadma()</function>并处理DMA芯片一方的
事情。</para>
</note>
</sect3>
<sect3>
<title>channel_getptr</title>
<para><function>xxxchannel_getptr()</function>返回传输缓冲区中
当前的缓冲。它通常由<function>chn_intr()</function>调用,而且
这也是为什么<devicename>pcm</devicename>知道它应当往哪儿传送
新数据。</para>
</sect3>
<sect3>
<title>channel_free</title>
<para>调用<function>xxxchannel_free()</function>来释放通道资源,
例如当驱动程序卸载时,并且如果通道数据结构是动态分配的,或者
如果不使用<function>sndbuf_alloc()</function>进行缓冲区分配,
则应当实现这个函数。</para>
</sect3>
<sect3>
<title>channel_getcaps</title>
<programlisting> struct pcmchan_caps *
xxxchannel_getcaps(kobj_t obj, void *data)
{
return &amp;xxx_caps;<co id="co-chgetcaps-return">
}</programlisting>
<calloutlist>
<callout arearefs="co-chgetcaps-return">
<para>这个例程返回指向(通常静态定义的)
<structname>pcmchan_caps</structname>结构的指针(在
<filename>sound/pcm/channel.h</filename>中定义)。这个结构
保存着最小和最大采样频率和被接受的声音格式。任何声音驱动
程序都可以作为一个范例。</para>
</callout>
</calloutlist>
</sect3>
<sect3>
<title>更多函数</title>
<para><function>channel_reset()</function>,
<function>channel_resetdone()</function>和
<function>channel_notify()</function>用于特殊目的,未与权威人士
(&a.cg;)进行探讨之前不应当在驱动程序中实现它。</para>
<para>不赞成使用<function>channel_setdir()</function>.</para>
</sect3>
</sect2>
<sect2>
<title>MIXER接口</title>
<sect3 id="xxxmixer-init">
<title>mixer_init</title>
<para><function>xxxmixer_init()</function>初始化硬件,并告诉
<devicename>pcm</devicename>什么混音器设备可用来播放和录音。
</para>
<programlisting> static int
xxxmixer_init(struct snd_mixer *m)
{
struct xxx_info *sc = mix_getdevinfo(m);
u_int32_t v;
[初始化硬件]
[为播放混音器设置v中适当的位]<co id="co-mxini-sd">
mix_setdevs(m, v);
[为录音混音器设置v中适当的位]
mix_setrecdevs(m, v)
return 0;
}</programlisting>
<calloutlist>
<callout arearefs="co-mxini-sd">
<para>设置一个整数值中的位,并调用
<function>mix_setdevs()</function>和
<function>mix_setrecdevs()</function>来告诉
<devicename>pcm</devicename>存在什么设备。</para>
</callout>
</calloutlist>
<para>混音器的位定义可以在<filename>soundcard.h</filename>中
找到。(<literal>SOUND_MASK_XXX</literal>值和
<literal>SOUND_MIXER_XXX</literal>移位)。</para>
</sect3>
<sect3>
<title>mixer_set</title>
<para><function>xxxmixer_set()</function>为混音器设备设置音量级别
(level)。</para>
<programlisting> static int
xxxmixer_set(struct snd_mixer *m, unsigned dev,
unsigned left, unsigned right)<co id="co-mxset-params">
{
struct sc_info *sc = mix_getdevinfo(m);
[设置音量级别(level)]
return left | (right &lt;&lt; 8);<co id="co-mxset-return">
}</programlisting>
<calloutlist>
<callout arearefs="co-mxset-params">
<para>设备被指定为 <literal>SOUND_MIXER_XXX</literal> 值</para>
<para>在范围[0-100]之间指定音量值。零值应当让设备静音。</para>
</callout>
<callout arearefs="co-mxset-return">
<para>由于硬件(音量)级别(level)可能不匹配输入比例,会出现
某些圆整例程返回如上面所示的实际级别值范围0-100内。</para>
</callout>
</calloutlist>
</sect3>
<sect3>
<title>mixer_setrecsrc</title>
<para><function>xxxmixer_setrecsrc()</function>设定录音源设备。
</para>
<programlisting> static int
xxxmixer_setrecsrc(struct snd_mixer *m, u_int32_t src)<co id="co-mxsr-params">
{
struct xxx_info *sc = mix_getdevinfo(m);
[查看src中的非零位, 设置硬件]
[更新src反映实际动作]
return src;<co id="co-mxsr-return">
}</programlisting>
<calloutlist>
<callout arearefs="co-mxsr-params">
<para>期望的录音设备由一个位域指定. </para>
</callout>
<callout arearefs="co-mxsr-return">
<para>返回设置用来录音的实际设备。一些驱动程序只能设置一个
录音设备。如果出现错误,函数应当返回-1。</para>
</callout>
</calloutlist>
</sect3>
<sect3>
<title>mixer_uninit, mixer_reinit</title>
<para><function>xxxmixer_uninit()</function>应当确保不会发出任何
声音,并且如果可能则应当让混音器硬件断电。</para>
<para><function>xxxmixer_reinit()</function>应当确保混音器硬件
加电,并且恢复所有不受<function>mixer_set()</function>或
<function>mixer_setrecsrc()</function>控制的设置。</para>
</sect3>
</sect2>
<sect2>
<title>AC97接口</title>
<indexterm><primary>AC97</primary></indexterm>
<para><emphasis>AC97</emphasis>由带有AC97编码解码器的驱动程序实现。
它只有三个方法:</para>
<itemizedlist>
<listitem><para><function>xxxac97_init()</function>返回找到的
ac97编码解码器的数目。</para>
</listitem>
<listitem><para><function>ac97_read()</function>与
<function>ac97_write()</function>读写指定的寄存器。</para>
</listitem>
</itemizedlist>
<para>The <emphasis>AC97</emphasis>接口由
<devicename>pcm</devicename>中的AC97代码来执行高层操作。参看
<filename>sound/pci/maestro3.c</filename>或
<filename>sound/pci/</filename>下很多其他内容作为范例。</para>
</sect2>
</sect1>
</chapter>
<!--
Local Variables:
mode: sgml
sgml-declaration: "../chapter.decl"
sgml-indent-data: t
sgml-omittag: nil
sgml-always-quote-attributes: t
sgml-parent-document: ("../book.sgml" "part" "chapter")
End:
-->

View file

@ -0,0 +1,199 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.12
$FreeBSD$
-->
<chapter id="sysinit">
<chapterinfo>
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>SYSINIT框架</title>
<indexterm><primary>SYSINIT(系统初始化)</primary></indexterm>
<indexterm><primary>dynamic initialization(动态初始化)</primary></indexterm>
<indexterm><primary>kernel initialization(内核初始化)</primary>
<secondary>dynamic(动态)</secondary></indexterm>
<indexterm><primary>kernel modules(内核模块)</primary></indexterm>
<indexterm><primary>kernel linker(内核链接器)</primary></indexterm>
<para>SYSINIT是一个通用的调用排序与分别执行机制的框架。
FreeBSD目前使用它来进行内核的动态初始化。
SYSINIT使得FreeBSD的内核各子系统可以在内核或模块动态加载链接时被重整、
添加、删除、替换,这样,内核和模块加载时就不必去修改一个静态的有序初始化
安排表甚至重新编译内核。这个体系也使得内核模块
(现在称为<firstterm>KLD</firstterm>可以与内核不同时编译、链接、
在引导系统时加载,甚至在系统运行时加载。这些操作是通过
<quote>内核链接器</quote>(kernel linker)和<quote>链接器集合</quote>
(linker set)完成的。</para>
<sect1 id="sysinit-term">
<title>术语</title>
<variablelist>
<varlistentry>
<term>链接器集合(Linker Set)</term>
<listitem>
<para>一种链接方法。这种方法将整个程序源文件中静态申明的数据收集到
一个可邻近寻址的数据单元中。</para>
</listitem>
</varlistentry>
</variablelist>
</sect1>
<sect1 id="sysinit-operation">
<title>SYSINIT操作</title>
<indexterm><primary>linker sets(链接器集合)</primary></indexterm>
<para>SYSINIT要依靠链接器获取遍布整个程序源代码多处申明的静态数据
并把它们组成一个彼此相邻的数据块。这种链接方法被称为
<quote>链接器集合</quote>(linker set)。
SYSINIT使用两个链接器集合以维护两个数据集合
包含每个数据条目的调用顺序、函数、一个会被提交给该函数的数据指针。</para>
<para>SYSINIT按照两类优先级标识对函数排序以便执行。
第一类优先级的标识是子系统的标识,
给出SYSINIT分别执行子系统的函数的全局顺序
定义在<filename>&lt;sys/kernel.h&gt;</filename>中的枚举
<literal>sysinit_sub_id</literal>内。第二类优先级标识在子系统中的元素的顺序,
定义在<filename>&lt;sys/kernel.h&gt;</filename>中的枚举
<literal>sysinit_elem_order</literal>内。</para>
<indexterm><primary>pseudo-device(伪设备)</primary></indexterm>
<para>有两种时刻需要使用SYSINIT系统启动或内核模块加载时
系统析构或内核模块卸载时。内核子系统通常在系统启动时使用SYSINIT
的定义项以初始化数据结构。例如进程调度子系统使用一个SYSINIT
定义项来初始化运行队列数据结构。设备驱动程序应避免直接使用
<literal>SYSINIT()</literal>,对于总线结构上的物理真实设备应使用
<literal>DRIVER_MODULE()</literal>调用的函数先侦测设备的存在,
如果存在,再进行设备的初始化。这一系统过程中,
会做一些专门针对设备的事情,然后调用<literal>SYSINIT()</literal>本身。
对于非总线结构一部分的虚设备,应改用<literal>DEV_MODULE()</literal>。</para>
</sect1>
<sect1 id="sysinit-using">
<title>使用SYSINIT</title>
<sect2>
<title>接口</title>
<sect3>
<title>头文件</title>
<programlisting>&lt;sys/kernel.h&gt;</programlisting>
</sect3>
<sect3>
<title>宏</title>
<programlisting>SYSINIT(uniquifier, subsystem, order, func, ident)
SYSUNINIT(uniquifier, subsystem, order, func, ident)</programlisting>
</sect3>
</sect2>
<sect2>
<title>启动</title>
<para>宏<literal>SYSINIT()</literal>在SYSINIT启动数据集合中
建立一个SYSINIT数据项以便SYSINIT在系统启动或模块加载时排序
并执行其中的函数。<literal>SYSINIT()</literal>有一个参数uniquifier
SYSINIT用它来标识数据项随后是子系统顺序号、子系统元素顺序号、
待调用函数、传递给函数的数据。所有的函数必须有一个恒量指针参数。</para>
<example>
<title><literal>SYSINIT()</literal>的例子</title>
<programlisting>#include &lt;sys/kernel.h&gt;
void foo_null(void *unused)
{
foo_doo();
}
SYSINIT(foo, SI_SUB_FOO, SI_ORDER_FOO, foo_null, NULL);
struct foo foo_voodoo = {
FOO_VOODOO;
}
void foo_arg(void *vdata)
{
struct foo *foo = (struct foo *)vdata;
foo_data(foo);
}
SYSINIT(bar, SI_SUB_FOO, SI_ORDER_FOO, foo_arg, &amp;foo_voodoo);
</programlisting>
</example>
<para>注意,<literal>SI_SUB_FOO</literal>和<literal>SI_ORDER_FOO</literal>
应当分别在上面提到的枚举<literal>sysinit_sub_id</literal>和
<literal>sysinit_elem_order</literal>之中。既可以使用已有的枚举项,
也可以将自己的枚举项添加到这两个枚举的定义之中。
你可以使用数学表达式微调SYSINIT的执行顺序。
以下的例子示例了一个需要刚好要在内核参数调整的SYSINIT之前执行的SYSINIT。</para>
<example>
<title>调整<literal>SYSINIT()</literal>顺序的例子</title>
<programlisting>static void
mptable_register(void *dummy __unused)
{
apic_register_enumerator(&amp;mptable_enumerator);
}
SYSINIT(mptable_register, SI_SUB_TUNABLES - 1, SI_ORDER_FIRST,
mptable_register, NULL);</programlisting>
</example>
</sect2>
<sect2>
<title>析构</title>
<para>宏<literal>SYSUNINIT()</literal>的行为与<literal>SYSINIT()</literal>的相当,
只是它将数据项填加至SYSINIT的析构数据集合。</para>
<example>
<title><literal>SYSUNINIT()</literal>的例子</title>
<programlisting>#include &lt;sys/kernel.h&gt;
void foo_cleanup(void *unused)
{
foo_kill();
}
SYSUNINIT(foobar, SI_SUB_FOO, SI_ORDER_FOO, foo_cleanup, NULL);
struct foo_stack foo_stack = {
FOO_STACK_VOODOO;
}
void foo_flush(void *vdata)
{
}
SYSUNINIT(barfoo, SI_SUB_FOO, SI_ORDER_FOO, foo_flush, &amp;foo_stack);
</programlisting>
</example>
</sect2>
</sect1>
</chapter>
<!--
Local Variables:
mode: sgml
sgml-declaration: "../chapter.decl"
sgml-indent-data: t
sgml-omittag: nil
sgml-always-quote-attributes: t
sgml-parent-document: ("../book.sgml" "part" "chapter")
End:
-->

View file

@ -0,0 +1,462 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.8
$FreeBSD$
-->
<chapter id="usb">
<chapterinfo>
<authorgroup>
<author>
<firstname>Nick</firstname>
<surname>Hibma</surname>
<contrib>&cnproj.written.by;</contrib>
</author>
</authorgroup>
<authorgroup>
<author>
<firstname>Murray</firstname>
<surname>Stokely</surname>
<contrib>&cnproj.modified.for.handbook.by;</contrib>
</author>
</authorgroup>
<authorgroup>
<author>
&author.cn.spellar;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>USB设备</title>
<sect1 id="usb-intro">
<title>简介</title>
<indexterm><primary>Universal Serial Bus (USB, 通用串行总线)</primary></indexterm>
<indexterm><primary>NetBSD</primary></indexterm>
<para>通用串行总线(USB)是将设备连接到个人计算机的一种新方法。总线
结构突出了双向通信的特色,并且其开发充分考虑到了设备正逐渐智能化
和需要与host进行更多交互的现实。对USB的支持包含在当前所有芯片中,
因此在新近制造的PC中都可用。苹果(Apple)引入仅带USB的iMac对硬件
制造商生产他们USB版本的设备是一个很大的激励。未来的PC规范指定
PC上的所有老连接器应当由一个或多个USB连接器取代提供通用的
即插即用能力。对USB硬件的支持在NetBSD的相当早期就有了它是由
Lennart Augustsson为NetBSD项目开发的。代码已经被移植到FreeBSD上
我们目前维护着一个底层共享代码。对USB子系统的实现来说许多USB的
特性很重要。</para>
<para><emphasis>Lennart Augustsson已经完成了NetBSD项目中USB支持的
大部分实现。十分感谢这项工作量惊人的工作。也十分感谢Ardy和Dirk
对本文稿的评论和校对。</emphasis></para>
<itemizedlist>
<listitem><para>设备直接连接到计算机上的端口,或者连接到称为
集中器的设备,形成树型设备结构。</para></listitem>
<listitem><para>设备可在运行时连接或断开。</para></listitem>
<listitem><para>设备可以挂起自身并触发host系统的重新投入运行。
</para></listitem>
<listitem><para>由于设备可由总线供电因此host软件必须跟踪每个
集中器的电源预算。</para></listitem>
<listitem><para>不同设备类型需要不同的服务质量,并且同一总线
可以连接最多126个设备这就需要恰当地调度总线上的传输以充分
利用12Mbps的可用带宽。USB 2.0超过400Mbps</para></listitem>
<listitem><para>设备智能化并包含很容易访问到的关于自身的信息。
</para></listitem>
</itemizedlist>
<para>为USB子系统以及连接到它的设备开发驱动程序受已开发或将要开发的
规范的支持。这些规范可以从USB主页公开获得。苹果(Apple)通过使得
通用类驱动程序可从其操作系统MacOS中获得而且不鼓励为每种新设备
使用单独的驱动程序来强烈推行基于标准的驱动程序。本章试图整理基本
信息以便对FreeBSD/NetBSD中USB栈的当前实现有个基本的了解。然而
建议将下面参考中提及的相关规范与本章同时阅读。</para>
<sect2>
<title>USB栈的结构</title>
<para>FreeBSD中的USB支持可被分为三层。最底层包含主控器向硬件
及其调度设施提供一个通用接口。它支持硬件初始化,对传输进行调度,
处理已完成/失败的传输。每个主控器驱动程序实现一个虚拟hub
以硬件无关方式提供对控制机器背面根端口的寄存器的访问。</para>
<para>中间层处理设备连接和断开,设备的基本初始化,驱动程序的选择,
通信通道(管道)和资源管理。这个服务层也控制默认管道和其上传输的
设备请求。</para>
<para>顶层包含支持特定(类)设备的各个驱动程序。这些驱动程序实现
除默认管道外的其他管道上使用的协议。他们也实现额外功能,使得设备
对内核或用户空间是可见的。他们使用服务层暴露出的USB驱动程序接口
(USBDI)。</para>
</sect2>
</sect1>
<sect1 id="usb-hc">
<title>主控器</title>
<indexterm><primary>USB(通用串行总线)</primary><secondary>host controllers(主控制器)</secondary></indexterm>
<para>主控器HC控制总线上包的传输。使用1毫秒的帧。在每帧开始
主控器产生一个帧开始SOF, Start of Frame包。</para>
<para>SOF包用于同步帧的开始和跟踪帧的数目。包在帧中被传输或由host
到设备out或由设备到hostin。传输总是由host发起轮询传输
因此每条USB总线只能有一个host。每个包的传输都有一个状态阶段
数据接收者可以在其中返回ACK应答接收NAK重试STALL错误
条件或什么也没有混乱数据阶段设备不可用或已断开。USB规范
<ulink url="http://www.usb.org/developers/docs.html">USB
specification</ulink>的第8.5节更详细地解释了包的细节。USB总线
上可以出现四中不同类型的传输:控制(control) 大块(bulk) 中断
(interrupt)和同步(isochronous)。传输的类型和他们的特性在下面
描述(`管道'子节中)。</para>
<para>USB总线上的设备和设备驱动程序间的大型传输被主控器或HC
驱动程序分割为多个包。</para>
<para>到默认端点的设备请求(控制传输)有些特殊。它们由两或三个阶段
组成启动SETUP数据DATA可选和状态STATUS。设置set-up
包被发送到设备。如果存在数据阶段,数据包的方向在设置包中给出。
状态阶段中的方向与数据阶段期间的方向相反,或者当没有数据阶段时
为IN。主控器硬件也提供寄存器用于保存根端口的当前状态和自从
状态改变寄存器最后一次复位以来所发生的改变。USB规范[2]建议使用一个
虚拟hub来提供对这些寄存器的访问。虚拟hub必须符合规范第11章中给出的
hub设备类。它必须提供一个默认管道使得设备请求可以发送给它。它返回
标准和hub类特定的一组描述符。它也应当提供一个中断管道用来报告其
端口发生的变化。当前可用的主控器规范有两个:
<ulink url="http://developer.intel.com/design/USB/UHCI11D.htm">
通用主控器接口</ulink>UHCI英特尔和<ulink
url="http://www.compaq.com/productinfo/development/openhci.html">
开放主控器接口</ulink>OHCI康柏微软国家半导体
UHCI规范的设计通过要求主控器驱动程序为每帧的传输提供完整的调度
从而减少了硬件复杂性。OHCI类型的控制器自身提供一个更抽象的接口来
完成很多工作,从而更加独立。</para>
<sect2>
<title>UHCI</title>
<indexterm><primary>USB(通用串行总线)</primary><secondary>UHCI(通用主控制器接口)</secondary></indexterm>
<para>UHCI主控器维护着带有1024个指向每帧数据结构的帧列表。
它理解两种不同的数据类型传输描述符TD和队列头QH。每个
TD表示表示与设备端点进行通信的一个包。QH是将一些TD和QH划分
成组的一种方法。</para>
<para>每个传输由一个或多个包组成。UHCI驱动程序将大的传输分割成
多个包。除同步传输外每个传输都会分配一个QH。对于每种类型的
传输都有一个与此类型对应的QH所有这些QH都会被集中到这个QH上。
由于有固定的时延需求,同步传输必须首先执行,它是通过帧列表中的
指针直接引用的。最后的同步TD传输引用那一帧的中断传输的QH。中断
传输的所有QH指向控制传输的QH控制传输的QH又指向大块传输的QH。
下面的图表给出了一个图形概览:</para>
<para>这导致下面的调度会在每帧中运行。控制器从帧列表中取得当前帧
的指针后,首先为那一帧中的所有的同步(isochronous)包执行TD。
这些TD的最后一个
引用那一帧的中断传输的QH。然后主控器将从那个QH下行到各个
中断传输的QH。完成那一队列后中断传输的QH会将控制器指向到所有
控制传输的QH。它将执行在那儿等待调度的所有子队列然后是在大块QH中
排队的所有传输。为了方便处理已完成或失败的传输,硬件会在每帧末尾
产生不同类型的中断。在传输的最后一个TD中HC驱动程序设置
Interrupt-On-Completion位来标记传输完成时的一个中断。如果TD达到了
其最大错误数就标记错误中断。如果在TD中设置短包侦测位且传输了
小于所设置的包长度(的包),就会标记此中断以通知控制器驱动程序传输
已完成。找出哪个传输已完成或产生错误是主控器驱动程序的任务。
当中断服务例程被调用时,它将定位所有已完成的传输并调用它们的回调。
</para>
<para>更详尽的描述请看 <ulink
url="http://developer.intel.com/design/USB/UHCI11D.htm">UHCI
specification。</ulink></para>
</sect2>
<sect2>
<title>OHCI</title>
<indexterm><primary>USB(通用串行总线)</primary><secondary>OHCI(开放主控制器接口)</secondary></indexterm>
<para>对OHCI主控器进行编程要容易得多。控制器假设有一组端点(endpoint)可用,
并知道帧中不同传输类型的调度优先级和排序。主控器使用的主要
数据结构是端点描述符ED它上面连接着一个传输描述符TD的队列。
ED包含端点所允许的最大的包大小控制器硬件完成包的分割。每次传输
后都会更新指向数据缓冲区的指针当起始和终止指针相等时TD就退归
到完成队列(done-queue)。四种类型的端点各有其自己的队列。控制和
大块(bulk)端点分别在它们自己的队列排队。中断ED在树中排队在树中的深度
定义了它们运行的频度。</para>
<para>帧列表 中断 同步(isochronous) 控制 大块(bulk)</para>
<para>主控器在每帧中运行的调度看起来如下。控制器首先运行非
周期性控制和大块队列最长可到HC驱动程序设置的一个时间限制。
然后以帧编号低5位作为中断ED树上深度为0的那一层中的索引运行
那个帧编号的中断传输。在这个树的末尾同步ED被连接并随后被
遍历。同步TD包含了传输应当运行其中的第一个帧的帧编号。所有周期
性的传输运行过以后,控制和大块队列再次被遍历。中断服务例程会被
周期性地调用,来处理完成的队列,为每个传输调用回调,并重新调度
中断和同步端点。</para>
<para>更详尽的描述请看 <ulink
url="http://www.compaq.com/productinfo/development/openhci.html">
OHCI specification</ulink>。服务层,即中间层,提供了以可控的方式
对设备进行访问,并维护着由不同驱动程序和服务层所使用的资源。
此层处理下面几方面:</para>
<itemizedlist>
<listitem><para>设备配置信息</para></listitem>
<listitem><para>与设备进行通信的管道</para></listitem>
<listitem><para>探测和连接设备,以及从设备分离(detach)。
</para></listitem>
</itemizedlist>
</sect2>
</sect1>
<sect1 id="usb-dev">
<title>USB设备信息</title>
<sect2>
<title>设备配置信息</title>
<para>每个设备提供了不同级别的配置信息。每个设备具有一个或多个
配置,探测/连接期间从其中选定一个。配置提供功率和带宽要求。
每个配置中可以有多个接口。设备接口是端点的汇集(collection)。
例如USB扬声器可以有一个音频接口音频类和对旋钮(knob)、
拨号盘(dial)和按钮的接口HID类
一个配置中的所有接口可以同时有效,并可被不同的
驱动程序连接。每个接口可以有备用接口,以提供不同质量的服务参数。
例如,在照相机中,这用来提供不同的帧大小以及每秒帧数。</para>
<para>每个接口中可以指定0或多个端点。端点是与设备进行通信的单向
访问点。它们提供缓冲区来临时存储从设备而来的,或外出到设备的数据。
每个端点在配置中有唯一地址,即端点号加上其方向。默认端点,即
端点0不是任何接口的一部分并且在所有配置中可用。它由服务层
管理,并且设备驱动程序不能直接使用。</para>
<para>Level 0 Level 1 Level 2 Slot 0</para>
<para>Slot 3 Slot 2 Slot 1</para>
<para>(只显示了32个槽中的4个)</para>
<para>这种层次化配置信息在设备中通过标准的一组描述符来描述(参看
USB规范[2]第9.6节。它们可以通过Get Descriptor Request来请求。
服务层缓存这些描述符以避免在USB总线上进行不必要的传输。对这些
描述符的访问是通过函数调用来提供的。</para>
<itemizedlist>
<listitem><para>设备描述符:关于设备的通用信息,如供应商,产品
和修订ID支持的设备类、子类和适用的协议默认端点的最大包大小
等。</para></listitem>
<listitem><para>配置描述符:此配置中的接口数,支持的挂起和
恢复能力,以及功率要求。</para></listitem>
<listitem><para>接口描述符:接口类、子类和适用的协议,接口备用
配置的数目和端点数目。</para></listitem>
<listitem><para>端点描述符:端点地址、方向和类型,支持的最大包
大小如果是中断类型的端点则还包括轮询频率。默认端点端点0
没有描述符,而且从不被计入接口描述符中。</para></listitem>
<listitem><para>字符串描述符:在其他描述符中会为某些字段提供
字符串索引。它们可被用来检取描述性字符串,可能以多种语言
的形式提供。</para></listitem>
</itemizedlist>
<para>类说明(specification)可以添加它们自己的描述符类型,这些描述符
也可以通过GetDescriptor Request来获得。</para>
<para>管道与设备上端点的通信,流经所谓的管道。驱动程序将到端点的
传输提交到管道,并提供传输(异步传输)失败或完成时调用的回调,
或等待完成(同步传输)。到端点的传输在管道中被串行化。传输或者完成,
或者失败,或者超时(如果设置了超时)。对于传输有两种类型的超时。
超时的发生可能由于USB总线上的超时毫秒。这些超时被视为失败
可能是由于设备断开连接引起的。另一种超时在软件中实现,当传输没有
在指定的时间(秒)内完成时触发。这是由于设备对传输的包否定应答引起的。
其原因是由于设备还没有准备好接收数据,缓冲区欠载或超载,或协议错误。
</para>
<para>如果管道上的传输大于关联的端点描述符中指定的最大包大小,主
控器OHCI或HC驱动程序UHCI将按最大包大小分割传输并且最后
一个包可能小于最大包的大小。</para>
<para>有时候对设备来说返回少于所请求的数据并不是个问题。例如,
到调制解调器的大块in传输可能请求200字节的数据但调制解调器
那时只有5个字节可用。驱动程序可以设置短包(SPD)标志。它允许主
控器即使在传输的数据量少于所请求的数据量的情况下也接受包。
这个标志只在in传输中有效因为将要被发送到设备的数据量总是事先
知道的。如果传输过程中设备出现不可恢复的错误,管道会被停顿。
接受或发送更多数据以前,驱动程序需要确定停顿的原因,并通过在
默认管道上发送清除端点挂起设备请求(clear endpoint halt device
request)来清除端点停顿条件。</para>
<para>有四种不同类型的端点和对应的管道: - </para>
<itemizedlist>
<listitem><para>
控制管道/默认管道:
每个设备有一个控制管道连接到默认端点端点0。此管道运载设备
请求和关联的数据。默认管道和其他管道上的传输的区别在于传输所
使用的协议协议在USB规范[2]中描述。这些请求用于复位和配置设备。
每个设备必须支持USB规范[2]的第9章中提供的一组基本命令。管道上
支持的命令可以通过设备类规范扩展,以支持额外的功能。
</para></listitem>
<listitem><para>大块(bulk)管道这是USB与原始传输媒体对应的等价物。
</para></listitem>
<listitem><para>中断管道host向设备发送数据请求如果设备没有
东西发送则将NAK否定应答数据包。中断传输按创建管道时指定的
频率被调度。</para></listitem>
<listitem><para>同步管道:这些管道用于具有固定时延的同步数据,
例如视频或音频流,但不保证一定传输。当前实现中已经有对这种类型
管道的某些支持。当传输期间出现错误,或者由于,例如缺乏缓冲区空间
来存储进入的数据而引起的设备否定应答包NAK控制、大块和中断
管道中的包会被重试。而同步包在传递失败或对包NAK时不会重试因为
那样可能违反同步约束。</para></listitem>
</itemizedlist>
<para>所需带宽的可用性在管道的创建期间被计算。传输在1毫秒的帧内
进行调度。帧中的带宽分配由USB规范的第5.6节规定。同步和中断传输被
允许消耗帧中多达90%的带宽。控制和大块传输的包在所有同步和中断包
之后进行调度,并将消耗所有剩余带宽。</para>
<para>关于传输调度和带宽回收的更多信息可以在USB规范[2]的第5章
UHCI规范[3]的的第1.3节OHCI规范[4]的3.4.2节中找到。</para>
</sect2>
</sect1>
<sect1 id="usb-devprobe">
<title>设备的探测和连接</title>
<indexterm><primary>USB(通用串行总线)</primary><secondary>probe(探测)</secondary></indexterm>
<para>集中器(hub)通知新设备已连接后,服务层给端口加电(switch on)
为设备提供100mA的电流。
此时设备处于其默认状态并监听设备地址0。服务层会通过默认
管道继续检取各种描述符。此后它将向设备发送Set Address请求将设备
从默认设备地址(地址0)移开。可能有多个设备驱动程序支持此设备。例如,
一个调制解调器可能通过AT兼容接口支持ISDN TA。然而特定型号的ISDN
适配器的驱动程序可能提供对此设备的更好支持。为了支持这样的灵活性,
探测会返回优先级,指示他们的支持级别。支持产品的特定版本会具有最高
优先级,通用驱动程序具有最低优先级。如果一个配置内有多个接口,也可能
多个驱动程序会连接到一个设备。每个驱动程序只需支持所有接口的一个子集。
</para>
<para>为新连接的设备探测驱动程序时,首先探测设备特定的驱动程序。
如果没有发现,则探测代码在所有支持的配置上重复探测过程,直到
在一个配置中连接到一个驱动程序。为了支持不同接口上使用多个驱动
程序的设备,探测会在一个配置中的所有尚未被驱动程序声明(claim)的
接口上重复进行。超出集中器功率预算的配置会被忽略。连接期间,驱动
程序应当把设备初始化到适当状态,但不能复位,因为那样会使得设备将
它自己从总线上断开,并重新启动探测过程。为了避免消耗不必要的带宽,
不应当在连接时声明中断管道,而应当延迟分配管道,直到打开文件并真的
使用数据。当关闭文件时,管道也应当被再次关闭,尽管设备可能仍然
连接着。</para>
<sect2>
<title>设备断开连接(disconnect)和分离(detach)</title>
<indexterm><primary>USB(通用串行总线)</primary><secondary>disconnect(断开)</secondary></indexterm>
<para>设备驱动程序与设备进行任何事务期间,应当预期会接收到错误。
USB的设计支持并鼓励设备在任何点及时断开连接。驱动程序应当确保
当设备不在时做正确的事情。</para>
<para>此外,断开连接(disconnect)后又重新连接(reconnect)的设备不会
被重新连接(reattach)为相同的设备实例。
将来当更多的设备支持序列号(参看设备描述符),
或开发出其他定义设备标识的方法的时候,这种情况可能会改变。
</para>
<para>设备断开连接是由集中器在传递到集中器驱动程序的中断包中发
信号通知(signal)的。状态改变信息指示哪个端口发现了连接改变。
连接到那个端口上的设备的所有设备驱动程序共用的设备分离方法被调用,
结构被彻底清理。如果端口状态指示同时一个设备已经连接(connect)到那个
端口,则探测和连接设备的过程将被启动。设备复位将在集中器上产生
一个断开-连接序列,并将按上面所述进行处理。</para>
</sect2>
</sect1>
<sect1 id="usb-protocol">
<title>USB驱动程序的协议信息</title>
<para>USB规范没有定义除默认管道外其他管道上使用的协议。这方面的信息
可以从各种来源获得。最准确的来源是USB主页[1]上的开发者部分。从这些
页面上可以得到数目不断增长的设备类的规范。这些规范指定从驱动程序
角度看起来兼容设备应当怎样,它需要提供的基本功能和通信通道上使用的
协议。USB规范[2]包括了集中器类的描述。人机界面设备(HID)的类规范已经
创建出来,以迎合对键盘、数字输入板、条形码阅读器、按钮、旋钮(手柄knob)、
开关等的要求。另一个例子是用于大容量存储设备的类规范。设备类的完整列表
参看USB主页[1]的开发者部分。</para>
<para>然而, 许多设备的协议信息还没有被公布。关于所用协议的信息
可能可以从制造设备的公司获得。一些公司会在给你规范之前要求你签署
保密协议(Non-Disclosure Agreement, NDA)。大多数情况下,这会阻止
将驱动程序开放源代码。</para>
<para>另一个信息的很好来源是Linux驱动程序源代码因为很多公司已经
开始为他们的设备提供Linux下的驱动程序。联系那些驱动程序作者询问
他们的信息来源总是一个好主意。</para>
<para>例子:人机界面设备。人机界面设备,如键盘、鼠标、数字输入板、
按钮、拨号盘等的规范被其他设备类规范引用,并在很多设备中使用。
</para>
<para>例如,音频扬声器提供到数模转换器的端点,可能还提供额外管道
用于麦克风。它们也为设备前面的按钮和拨号盘在单独的接口中提供HID
端点。监视器控制类也是如此。通过可用的内核和用户空间的库与HID
类驱动程序或通用驱动程序一起可以简单直接地创建对这些接口的支持。
另一个设备可以作为在一个配置中的多个接口由不同的设备驱动程序驱动
的例子,这个设备是一种便宜的键盘,带有老的鼠标接口。为了避免在
设备中为USB集中器包括一个硬件而导致的成本上升制造商将从键盘背面的
PS/2端口接收到的鼠标数据与来自键盘的按键组合成在同一个配置中的
两个单独的接口。鼠标和键盘驱动程序各自连接到适当的接口,并分配到
两个独立端点的管道. </para>
<indexterm><primary>USB(通用串行总线)</primary><secondary>firmware(固件)</secondary></indexterm>
<para>例子:固件下载。已经开发出来的许多设备是基于通用目的处理器,
并将额外的USB核心加入其中。由于驱动程序的开发和USB设备的固件仍然
非常新,许多设备需要在连接(connect)之后下载固件。</para>
<para>下面的步骤非常简明直接。设备通过供应商和产品ID标识自身。第一
个驱动程序探测并连接到它,并将固件下载到其中。此后设备自己软复位,
驱动程序分离。短暂的暂停之后设备宣布它在总线上的存在。设备将改变
其供应商/产品/版本的ID以反映其提供有固件的事实因此另一个驱动程序
将探测它并连接(attach)到它。</para>
<para>这些类型的设备的一个例子是基于EZ-USB的ActiveWire I/O板。这个
芯片有一个通用固件下载器。下载到ActiveWire板子上的固件改变版本ID。
然后它将执行EZ-USB芯片的USB部分的软复位从USB总线上断开并再次
重新连接。</para>
<para>例子:大容量存储设备。对大容量存储设备的支持主要围绕现有的
协议构建。Iomega USB Zip驱动器是基于SCSI版本的驱动器。SCSI命令和
状态信息被包装到块中,在大块(bulk)管道上传输到/来自设备在USB线
上模拟SCSI控制器。ATAPI和UFI命令以相似的方式被支持。</para>
<indexterm><primary>ATAPI(AT Attachment Packet Interface, (IBM PC)AT附属包接口)</primary></indexterm>
<para>大容量存储规范支持两种不同类型的对命令块的包装。最初的尝试
基于通过默认管道发送命令和状态信息使用大块传输在host和设备之间
移动数据。在经验基础上设计出另一种方法,这种方法基于包装命令和
状态块并在大块out和in端点上发送它们。规范精确地指定了何时必须
发生什么,以及在碰到错误条件的情况下应该做什么。为这些设备编写
驱动程序的最大挑战是协调基于USB的协议让它适合已有的对大容量存储设备
的支持。CAM提供了钩子以相当直接了当的方式来完成这个。ATAPI就
没有这么简单了因为历史上IDE接口从未有过多种不同的表现方式。
</para>
<para>来自Y-E Data的对USB软盘的支持也不是那么直观因为设计了一套
新的命令集。</para>
</sect1>
</chapter>

View file

@ -0,0 +1,241 @@
<!--
The FreeBSD Documentation Project
The FreeBSD Simplified Chinese Project
Original Revision: 1.10
$FreeBSD$
-->
<chapter id="vm">
<chapterinfo>
<authorgroup>
<author>
<firstname>Matthew</firstname>
<surname>Dillon</surname>
<contrib>&cnproj.contributed.by;</contrib>
</author>
</authorgroup>
<!-- 6 Feb 1999 -->
<authorgroup>
<author>
&author.cn.intron;
<contrib>&cnproj.translated.by;</contrib>
</author>
</authorgroup>
</chapterinfo>
<title>虚拟内存系统</title>
<sect1 id="vm-physmem">
<title>物理内存的管理&mdash;<literal>vm_page_t</literal></title>
<indexterm><primary>virtual memory(虚拟内存)</primary></indexterm>
<indexterm><primary>physical memory(物理内存)</primary></indexterm>
<indexterm><primary><literal>vm_page_t</literal>结构体</primary></indexterm>
<para>物理内存通过结构体<literal>vm_page_t</literal>以页为基础进行管理。
物理内存的页由它们各自对应的结构体<literal>vm_page_t</literal>所代表,
这些结构体存放在若干个页管理队列中的一个里面。</para>
<para>一页可以处于在线(wired)、活动(active),去活(inactive)、缓存(cache)、
自由(free)状态。除了在线状态,页一般被放置在一个双向链表队列里,
代表了它所处的状态。在线页不放置在任何队列里。</para>
<para>FreeBSD为缓存页和自由页实现了一个更为复杂的页队列机制
以实现对页的分类管理。每一种状态都对应着多个队列,
队列的安排对应着处理器的一级、二级缓存。当需要分配一个新页时,
FreeBSD会试图把一个按一级、二级缓存对齐的页面分配给虚拟内存对象。</para>
<para>此外,一个页可以有一个引用计数,可以被一个忙计数锁定。
虚拟内存系统也实现了<quote>终极锁定</quote>(ultimate locked)状态,
一个页可以用页标志PG_BUSY表示这一状态。</para>
<para>总之每个页队列都按照LRU(Least-Recently Used)的原则工作。
<tip><title>译者注</title><para>短语Least-Recently Used有两种理解方式
1.将“least-recently”理解为反向比较级意义为“最早”整个短语理解为
“最近的使用时间最早”2.将“least”和“recently”理解为副词
都修饰“used”整个短语理解为“最近最少使用”。
这两种理解方式的实际意义基本相同。</para></tip>
一个页常常最初处于在线或活动状态。在线时,页常常关联于某处的页表。
虚拟内存系统通过扫描在一个较活跃的页队列(LRU)确定页的年龄,
以便将他们移到一个较不活跃的页队列中。
移动到缓存中的页依然与一个VM对象关联但被作为立即再用的候选。
在自由对列中的页是真正未被使用的。FreeBSD尽量不将页放在自由队列中
但是必须保持一定数量的自由页,以便响应中断时分配。</para>
<para>如果一个进程试图访问一个不在页表中而在某一队列中的页
(例如去活队列或缓存队列),一个相对耗费资源少的页错误发生,
导致页被重激活。如果页根本不存在于系统内存之中,进程必须被阻塞,
此时页被从磁盘中载入。<tip><title>译者注</title>
<para>Intel等厂商的CPU工作在保护模式时可用来实现虚拟内存。
当寻址的地址空间对应着真实内存时,则正常读写;
当寻址的地址空间没有对应的真实内存时CPU会产生一个“错误”
通知操作系统与磁盘等设备进行交换,读寻址则调入存储内容,
写寻址则写出存储内容。这个“错误”
并非操作系统或应用程序开发人员犯下的错误,
尽管在CPU硬件实现中这与应用程序或操作系统内核崩溃的错误的发生机制相同。
参见Intel的CPU保护模式开发手册。</para></tip></para>
<indexterm><primary>paging queues(页队列)</primary></indexterm>
<para>FreeBSD动态的调整页队列试图将各个队列中的页数维护在一个适当的比例上
同时管理程序崩溃的已清理和未清理页。重新平衡的比例数值决定于系统内存的负担。
这种重新平衡由pageout守护进程实现包括清理未清理页(与他们的后备存储同步)、
监视页被引用的活跃程度
(重置它们在LRU队列中的位置或在不同活跃程度的页队列间移动)、
当比例不平衡时在队列间迁移页,如此等等。
FreeBSD的VM系统会将重激活页而产生的错误频率调低到一个合理的数值
由此确定某一页活跃/闲置的实际程度。
这可以为更好的决定何时清理/分配一个页做出决策。</para>
</sect1>
<sect1 id="vm-cache">
<title>统一的缓存信息结构体&mdash;<literal>vm_object_t</literal></title>
<indexterm><primary>unified buffer cache(统一缓存)</primary></indexterm>
<indexterm><primary><literal>vm_object_t</literal>结构体</primary></indexterm>
<para>FreeBSD实现了统一的<quote>虚拟内存对象</quote>(VM对象)的设计思想。
VM对象可以与各种类型的内存使用方式相结合&mdash;直接使用(unbacked)、
交换(swap)、物理设备、文件。
由于文件系统使用相同的VM对象管理核内数据&mdash;文件的缓存,
所以这些缓存的结构也是统一的。</para>
<para>VM对象可以被<emphasis>影复制</emphasis>(shadowed)。
它们可以被堆放到其它类别VM对象堆栈的顶端。例如可以有一个交换VM对象
放置在文件VM对象堆栈的顶端以实现MAP_PRIVATE的mmap()操作。
这样的入栈操作也可以用来实现各种各样的共享特性,
包括写入时复制(copy-on-write用于日志文件系统),以派生出地址空间。</para>
<para>应当注意,一个<literal>vm_page_t</literal>
结构体在任一个时刻只能与一个VM对象相关联。
VM对象影复本可以实现跨实例的共享相同的页。</para>
</sect1>
<sect1 id="vm-fileio">
<title>文件系统输入/输出&mdash;<literal>buf</literal>结构体</title>
<indexterm><primary>vnode</primary></indexterm>
<para>vnode VM对象比如文件VM对象一般需要维护它们自己的清理(clean)/
未清理(dirty)信息,而不依赖于文件系统的清理/未清理维护。
例如当VM系统要同步一个物理页和其对应的实际存储器
VM系统就需要在写入到实际存储器前将该页标记为已清理。
另外,文件系统要能够将文件或文件元数据的各部分映射到内核虚拟内存
(KVM)中以便操作。</para>
<para>用来进行这些管理的实体就是众所周知的文件系统缓存,
<literal>struct buf</literal>或<literal>bp</literal>。
当文件系统需要对一个VM对象的一部分操作时
它常会将这个对象的这部分映射到struct buf
并且将struct buf中页映射到内核虚拟内存(KVM)中。
同样的,磁盘输入/输出通常要先将VM对象的各部分映射到buf结构体中
然后对buf结构体进行输入/输出操作。
下层的vm_page_t在输入/输出期间通常被标记为“忙”。
文件系统缓存也会“忙”,这对于文件系统驱动程序非常有用,
对文件系统缓存操作比对VM真实页(hard)操作更好。</para>
<para>FreeBSD保留一定数量的内核虚拟内存来存放struct buf的映射
但是这些buf结构体应该是被清理过的。这些内核虚拟内存仅用来存放映射
并不限制缓存数据的能力。严格的说,物理数据缓存是
<literal>vm_page_t</literal>的一个功能,不是文件系统缓存的功能。
然而,由于文件系统缓存被用来处理输入/输出,
他们固有的限制了同时进行输入/输出可能的数量。
但是,由于通常有数千文件系统缓存可供使用,所以这并不会造成问题。</para>
</sect1>
<sect1 id="vm-pagetables">
<title>映射页表&mdash;<literal>vm_map_t, vm_entry_t</literal></title>
<indexterm><primary>page tables(页表)</primary></indexterm>
<para>FreeBSD将物理页表结构从VM系统中分离了出来。各进程的所有页表可以脱离进程
(on the fly)重建,并且通常被认为是一次性的。特殊的页表,如内核虚拟内存(KVM)
常常是被永久性预分配的;这些页表不是一次性的。</para>
<para>FreeBSD通过<literal>vm_map_t</literal>和<literal>vm_entry_t</literal>
结构将虚拟内存中<literal>vm_objects</literal>的各地址范围部分关联起来。
页表被直接的从
<literal>vm_map_t</literal>/<literal>vm_entry_t</literal>/<literal>vm_object_t</literal>
中有层次的合成出来。这里需要重申一下,我曾提到的“物理页仅直接与
<literal>vm_object</literal>相关联”并不很正确。<literal>vm_page_t</literal>
也被会被链接到正在与之相关联的页表中。当页表被调用时,
一个<literal>vm_page_t</literal>结构体可以被链接到几个<emphasis>pmaps</emphasis>。
然而,由于有了层次的关联,因此在对象中所有对同一页的引用会引用同一
<literal>vm_page_t</literal>结构体,这样就实现了跨区域(board)的缓存的统一。</para>
</sect1>
<sect1 id="vm-kvm">
<title>KVM存储映射</title>
<para>FreeBSD使用KVM存放各种各样的内核结构体。在KVM中最大的单个实体是文件系统缓存。
那是与<literal>struct buf</literal>实体有关的映射。</para>
<para>不像LinuxFreeBSD<emphasis>不</emphasis>将所有的物理内存映射到KVM中。
这意味着FreeBSD可以在32位平台上管理超过4GB的内存配置。事实上
如果mmu(译者注可能是指“内存管理单元”“Memory Management Unit”)
有足够的能力FreeBSD理论上可以在32位平台上管理最多8TB的内存配置。
然而大多数32平台只能映射4GB内存这只能是一个争论点。</para>
<para>有几种机制可以管理KVM。管理KVM的主要机制是<emphasis>区域分配器</emphasis>
(zone allocator)。区域分配器管理着KVM的大块再将大块切分为恒定大小的小块
以便按照某一种类型的结构体分配。你可以使用命令<command>vmstat -m</command>
一览当前KVM分区使用情况。</para>
</sect1>
<sect1 id="vm-tuning">
<title>调整FreeBSD的虚拟内存系统</title>
<para>开发者的协同努力使得FreeBSD可以自行动态调整内核。一般来说
除了内核配置选项<option>maxusers</option>和<option>NMBCLUSTERS</option>
你不需要做任何杂乱的事情。这些内核编译配置选项(一般)被指定在
<filename>/usr/src/sys/i386/conf/<replaceable>CONFIG_FILE</replaceable></filename>
之中。所有可用内核配置选项的描述可在
<filename>/usr/src/sys/i386/conf/LINT</filename>中找到。</para>
<para>在一个大系统的配置中,你可能需要增加<option>maxusers</option>的值。
数值范围通常在10到128。注意过度增加<option>maxusers</option>
的值可能导致系统从实际可用的KVM中溢出从而引起无法预知的操作。
最好将<option>maxusers</option>设为一个合理的数值,并且添加其它选项,
如<option>NMBCLUSTERS</option>,来增加特定的资源。</para>
<para>如果你的系统要被重负荷的使用网络,你需要增加
<option>NMBCLUSTERS</option>的值。数值范围通常在1024到4096。</para>
<para><literal>NBUF</literal>也是传统的规划系统的参数。
这个参数决定系统可用来映射文件系统输入/输出缓存的KVM的数量。
注意这个参数与统一的缓存没有任何关系。这个参数可在3.0-CURRENT
和以后的内核中被动态的调节,通常不应当被手动的调节。
我们推荐你<emphasis>不要</emphasis>指定<literal>NBUF</literal>。
让系统自行确定它。太小的值会导致非常低效的文件系统操作;
太大的值会使用页队列中缺少页面,而大量的页处于在线状态。</para>
<para>缺省情况下FreeBSD内核编译时是不被优化的。
你可以在内核配置文件中用<literal>makeoptions</literal>
指定排错(debugging)和优化标志。注意,你一般不应使用<option>-g</option>
除非你能够应付由此产生的大内核(典型的是7MB或更多)。</para>
<programlisting>makeoptions DEBUG="-g"
makeoptions COPTFLAGS="-O -pipe"</programlisting>
<para>Sysctl提供了在运行时调整内核的方式。你通常不需要指定任何sysctl变量
尤其是与VM相关的那些变量。</para>
<para>运行时VM和系统调整的影响相对直接一些。
首先应当尽可能在UFS/FFS文件系统上使用Soft Updates。
在<filename>/usr/src/sys/ufs/ffs/README.softupdates</filename>
里有关于如何配置的指示。</para>
<indexterm><primary>swap partition(交换分区)</primary></indexterm>
<para>其次,应当配置足够多的交换空间。
你应当在每个物理磁盘上配置一个交换分区最多4个
甚至在你的<quote>工作</quote>磁盘上。你应当有至少2倍于主内存的交换空间
假如你没有足够内存的话,交换分区还应更多。
你也应当按照你期望中的最大内存配置决定交换分区的大小,
这样以后就不再需要重新给磁盘分区了。
如果你处理系统崩溃后的内存倾倒(crash dump)
第一个交换分区必须至少与主内存一样大,
<filename>/var/crash</filename>必须有足够的空间来承装倾倒。</para>
<para>NFS上的交换分区可以很好的被4.X或后来的系统使用
但是你必须明白NFS服务器将要经受页装载操作很强的冲击。</para>
</sect1>
</chapter>