diff --git a/documentation/content/zh-cn/books/arch-handbook/_index.adoc b/documentation/content/zh-cn/books/arch-handbook/_index.adoc index 2c20857249..0228d5fd49 100644 --- a/documentation/content/zh-cn/books/arch-handbook/_index.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/_index.adoc @@ -1,84 +1,56 @@ --- title: FreeBSD 系统结构手册 authors: - author: The FreeBSD Documentation Project - author: FreeBSD 中文计划 copyright: Copyright © 2000-2006, 2012-2013 The FreeBSD Documentation Project trademarks: ["freebsd", "apple", "microsoft", "unix", "general"] +next: books/arch-handbook/parti +add_single_page_link: true isIndex: true --- = FreeBSD 系统结构手册 :doctype: book :toc: macro -:toclevels: 2 +:toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :partnums: :source-highlighter: rouge :experimental: -:book: true -:pdf: false +:images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] :chapters-path: content/{{% lang %}}/books/arch-handbook/ endif::[] ifdef::backend-pdf,backend-epub3[] :chapters-path: include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] :chapters-path: include::../../../../../shared/asciidoctor.adoc[] endif::[] [.abstract-title] 摘要 欢迎您阅读 FreeBSD系统结构手册。 这本手册还在不断由许多人_继续书写_。 许多章节还是空白,有的章节亟待更新。 如果您对这个项目感兴趣并愿意有所贡献,请发信给 {freebsd-doc}。 本文档的最新英文原始版本可从 link:https:://www.FreeBSD.org[FreeBSD Web 站点] 获得, 由 link:http://www.FreeBSD.org.cn/[http://www.FreeBSD.org.cn] 维护的最新译本可以在 link:http://cnsnap.cn.FreeBSD.org/zh-cn/[http://www.FreeBSD.org.cn 快照 Web 站点] 和 link:http://cnsnap.cn.FreeBSD.org/doc/zh_CN.UTF-8/[http://www.FreeBSD.org.cn 文档快照] 处获得, 这一译本会不断向主站同步。 此外, 您也可以从 link:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/[FreeBSD FTP 服务器] 或众多的 extref:{handbook}mirrors/[镜像站点, mirrors-ftp] 得到这份文档的各种其他格式以及压缩形式的版本。 ''' -toc::[] - -[[kernel]] -= 内核 - -include::{chapters-path}boot/chapter.adoc[leveloffset=+1] -include::{chapters-path}locking/chapter.adoc[leveloffset=+1] -include::{chapters-path}kobj/chapter.adoc[leveloffset=+1] -include::{chapters-path}jail/chapter.adoc[leveloffset=+1] -include::{chapters-path}sysinit/chapter.adoc[leveloffset=+1] -include::{chapters-path}mac/chapter.adoc[leveloffset=+1] -include::{chapters-path}vm/chapter.adoc[leveloffset=+1] -include::{chapters-path}smp/chapter.adoc[leveloffset=+1] - -[[devicedrivers]] -= 设备驱动程序 - -include::{chapters-path}driverbasics/chapter.adoc[leveloffset=+1] -include::{chapters-path}isa/chapter.adoc[leveloffset=+1] -include::{chapters-path}pci/chapter.adoc[leveloffset=+1] -include::{chapters-path}scsi/chapter.adoc[leveloffset=+1] -include::{chapters-path}usb/chapter.adoc[leveloffset=+1] -include::{chapters-path}newbus/chapter.adoc[leveloffset=+1] -include::{chapters-path}sound/chapter.adoc[leveloffset=+1] -include::{chapters-path}pccard/chapter.adoc[leveloffset=+1] - -[[appendices]] -= 附录 - -include::{chapters-path}bibliography/chapter.adoc[leveloffset=+1] +include::{chapters-path}toc.adoc[] diff --git a/documentation/content/zh-cn/books/arch-handbook/bibliography/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/bibliography/_index.adoc similarity index 97% rename from documentation/content/zh-cn/books/arch-handbook/bibliography/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/bibliography/_index.adoc index 53c29d8403..78e23c7a39 100644 --- a/documentation/content/zh-cn/books/arch-handbook/bibliography/chapter.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/bibliography/_index.adoc @@ -1,45 +1,45 @@ --- title: 参考书目 -prev: books/arch-handbook/pccard +prev: books/arch-handbook/partiii --- [bibliography] [[bibliography]] = 参考书目 :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: A :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] [1] __Marshall Kirk McKusick、Keith Bostic、Michael J Karels和John S Quarterman. 版权 © 1996 Addison-Wesley Publishing Company, Inc.. 0-201-54979-4. Addison-Wesley Publishing Company, Inc.. The Design and Implementation of the 4.4 BSD Operating System. 1-2.__ diff --git a/documentation/content/zh-cn/books/arch-handbook/_index.adoc b/documentation/content/zh-cn/books/arch-handbook/book.adoc similarity index 63% copy from documentation/content/zh-cn/books/arch-handbook/_index.adoc copy to documentation/content/zh-cn/books/arch-handbook/book.adoc index 2c20857249..0eaba06ce3 100644 --- a/documentation/content/zh-cn/books/arch-handbook/_index.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/book.adoc @@ -1,84 +1,78 @@ --- title: FreeBSD 系统结构手册 authors: - author: The FreeBSD Documentation Project - author: FreeBSD 中文计划 copyright: Copyright © 2000-2006, 2012-2013 The FreeBSD Documentation Project trademarks: ["freebsd", "apple", "microsoft", "unix", "general"] -isIndex: true +add_split_page_link: true --- = FreeBSD 系统结构手册 :doctype: book :toc: macro :toclevels: 2 :icons: font :sectnums: :sectnumlevels: 6 :partnums: :source-highlighter: rouge :experimental: :book: true :pdf: false ifdef::env-beastie[] ifdef::backend-html5[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] :chapters-path: content/{{% lang %}}/books/arch-handbook/ endif::[] ifdef::backend-pdf,backend-epub3[] :chapters-path: include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] :chapters-path: include::../../../../../shared/asciidoctor.adoc[] endif::[] [.abstract-title] 摘要 欢迎您阅读 FreeBSD系统结构手册。 这本手册还在不断由许多人_继续书写_。 许多章节还是空白,有的章节亟待更新。 如果您对这个项目感兴趣并愿意有所贡献,请发信给 {freebsd-doc}。 本文档的最新英文原始版本可从 link:https:://www.FreeBSD.org[FreeBSD Web 站点] 获得, 由 link:http://www.FreeBSD.org.cn/[http://www.FreeBSD.org.cn] 维护的最新译本可以在 link:http://cnsnap.cn.FreeBSD.org/zh-cn/[http://www.FreeBSD.org.cn 快照 Web 站点] 和 link:http://cnsnap.cn.FreeBSD.org/doc/zh_CN.UTF-8/[http://www.FreeBSD.org.cn 文档快照] 处获得, 这一译本会不断向主站同步。 此外, 您也可以从 link:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/[FreeBSD FTP 服务器] 或众多的 extref:{handbook}mirrors/[镜像站点, mirrors-ftp] 得到这份文档的各种其他格式以及压缩形式的版本。 ''' toc::[] -[[kernel]] -= 内核 +include::{chapters-path}parti.adoc[lines=15..16] +include::{chapters-path}boot/_index.adoc[leveloffset=+1] +include::{chapters-path}locking/_index.adoc[leveloffset=+1] +include::{chapters-path}kobj/_index.adoc[leveloffset=+1] +include::{chapters-path}jail/_index.adoc[leveloffset=+1] +include::{chapters-path}sysinit/_index.adoc[leveloffset=+1] +include::{chapters-path}mac/_index.adoc[leveloffset=+1] +include::{chapters-path}vm/_index.adoc[leveloffset=+1] +include::{chapters-path}smp/_index.adoc[leveloffset=+1] -include::{chapters-path}boot/chapter.adoc[leveloffset=+1] -include::{chapters-path}locking/chapter.adoc[leveloffset=+1] -include::{chapters-path}kobj/chapter.adoc[leveloffset=+1] -include::{chapters-path}jail/chapter.adoc[leveloffset=+1] -include::{chapters-path}sysinit/chapter.adoc[leveloffset=+1] -include::{chapters-path}mac/chapter.adoc[leveloffset=+1] -include::{chapters-path}vm/chapter.adoc[leveloffset=+1] -include::{chapters-path}smp/chapter.adoc[leveloffset=+1] +include::{chapters-path}partii.adoc[lines=15..16] +include::{chapters-path}driverbasics/_index.adoc[leveloffset=+1] +include::{chapters-path}isa/_index.adoc[leveloffset=+1] +include::{chapters-path}pci/_index.adoc[leveloffset=+1] +include::{chapters-path}scsi/_index.adoc[leveloffset=+1] +include::{chapters-path}usb/_index.adoc[leveloffset=+1] +include::{chapters-path}newbus/_index.adoc[leveloffset=+1] +include::{chapters-path}sound/_index.adoc[leveloffset=+1] +include::{chapters-path}pccard/_index.adoc[leveloffset=+1] -[[devicedrivers]] -= 设备驱动程序 - -include::{chapters-path}driverbasics/chapter.adoc[leveloffset=+1] -include::{chapters-path}isa/chapter.adoc[leveloffset=+1] -include::{chapters-path}pci/chapter.adoc[leveloffset=+1] -include::{chapters-path}scsi/chapter.adoc[leveloffset=+1] -include::{chapters-path}usb/chapter.adoc[leveloffset=+1] -include::{chapters-path}newbus/chapter.adoc[leveloffset=+1] -include::{chapters-path}sound/chapter.adoc[leveloffset=+1] -include::{chapters-path}pccard/chapter.adoc[leveloffset=+1] - -[[appendices]] -= 附录 - -include::{chapters-path}bibliography/chapter.adoc[leveloffset=+1] +include::{chapters-path}partiii.adoc[lines=15..16] +include::{chapters-path}bibliography/_index.adoc[leveloffset=+1] diff --git a/documentation/content/zh-cn/books/arch-handbook/boot/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/boot/_index.adoc similarity index 99% rename from documentation/content/zh-cn/books/arch-handbook/boot/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/boot/_index.adoc index 431ac89f38..faadabd081 100644 --- a/documentation/content/zh-cn/books/arch-handbook/boot/chapter.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/boot/_index.adoc @@ -1,749 +1,749 @@ --- title: 第 1 章 引导过程与内核初始化 -prev: books/arch-handbook/ +prev: books/arch-handbook/parti next: books/arch-handbook/locking --- [[boot]] = 引导过程与内核初始化 :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: 1 :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] [[boot-synopsis]] == 概述 这一章是对引导过程和系统初始化过程的总览。这些过程始于BIOS(固件)POST, 直到第一个用户进程建立。由于系统启动的最初步骤是与硬件结构相关的、是紧配合的, 这里用IA-32(Intel Architecture 32bit)结构作为例子。 [[boot-overview]] == 总览 一台运行FreeBSD的计算机有多种引导方法。这里讨论其中最通常的方法, 也就是从安装了操作系统的硬盘上引导。引导过程分几步完成: * BIOS POST * ``boot0``阶段 * ``boot2``阶段 * loader阶段 * 内核初始化 ``boot0``和``boot2``阶段在手册 man:boot[8]中被称为__bootstrap stages 1 and 2__, 是FreeBSD的3阶段引导过程的开始。在每一阶段都有各种各样的信息显示在屏幕上, 你可以参考下表识别出这些步骤。请注意实际的显示内容可能随机器的不同而有一些区别: [.informaltable] [cols="90%,10%", frame="none"] |=== |视不同机器而定 |BIOS(固件)消息 a| [source,shell] .... F1 FreeBSD F2 BSD F5 Disk 2 .... |`boot0` a| [source,shell] .... >>FreeBSD/i386 BOOT Default: 1:ad(1,a)/boot/loader boot: .... |`boot2` a| [source,shell] .... BTX loader 1.0 BTX version is 1.01 BIOS drive A: is disk0 BIOS drive C: is disk1 BIOS 639kB/64512kB available memory FreeBSD/i386 bootstrap loader, Revision 0.8 Console internal video/keyboard (jkh@bento.freebsd.org, Mon Nov 20 11:41:23 GMT 2000) /kernel text=0x1234 data=0x2345 syms=[0x4+0x3456] Hit [Enter] to boot immediately, or any other key for command prompt Booting [kernel] in 9 seconds..._ .... |loader a| [source,shell] .... 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 .... |内核 |=== [[boot-bios]] == BIOS POST 当PC加电后,处理器的寄存器被设为某些特定值。在这些寄存器中, __指令指针__寄存器被设为32位值0xfffffff0。 指令指针寄存器指向处理器将要执行的指令代码。`cr1`, 一个32位控制寄存器,在刚启动时值被设为0。cr1的PE(Protected Enabled, 保护模式使能)位用来指示处理器是处于保护模式还是实地址模式。 由于启动时该位被清位,处理器在实地址模式中引导。在实地址模式中, 线性地址与物理地址是等同的。 值0xfffffff0略小于4G,因此计算机没有4G字节物理内存, 这就不会是一个有效的内存地址。计算机硬件将这个地址转指向BIOS存储块。 BIOS表示__Basic Input Output System__ (基本输入输出系统)。在主板上,它被固化在一个相对容量较小的 只读存储器(Read-Only Memory, ROM)。BIOS包含各种各样为主板硬件 定制的底层例程。就这样,处理器首先指向常驻BIOS存储器的地址 0xfffffff0。通常这个位置包含一条跳转指令,指向BIOS的POST例程。 POST表示__Power On Self Test__(加电自检)。 这套程序包括内存检查,系统总线检查和其它底层工具, 从而使得CPU能够初始化整台计算机。这一阶段中有一个重要步骤, 就是确定引导设备。现在所有的BIOS都允许手工选择引导设备。 你可以从软盘、光盘驱动器、硬盘等设备引导。 POST的最后一步是执行``INT 0x19``指令。 这个指令从引导设备第一个扇区读取512字节,装入地址0x7c00。 __第一个扇区__的说法最早起源于硬盘的结构, 硬盘面被分为若干圆柱形轨道。给轨道编号,同时又将轨道分为 一定数目(通常是64)的扇形。0号轨道是硬盘的最外圈,1号扇区, 第一个扇区(轨道、柱面都从0开始编号,而扇区从1开始编号) 有着特殊的作用,它又被称为主引导记录(Master Boot Record, MBR)。 第一轨剩余的扇区常常不使用。 [[boot-boot0]] == ``boot0``阶段 让我们看一下文件[.filename]##/boot/boot0##。 这是一个仅512字节的小文件。如果在FreeBSD安装过程中选择 "bootmanager",这个文件中的内容将被写入硬盘MBR 如前所述, `INT 0x19` 指令装载 MBR, 也就是 [.filename]#boot0# 的内容至内存地址 0x7c00。 再看文件 [.filename]#sys/boot/i386/boot0/boot0.S#, 可以猜想这里面发生了什么 - 这是引导管理器, 一段由 Robert Nordier书写的令人起敬的程序片段。 MBR里,也就是[.filename]##boot0##里, 从偏移量0x1be开始有一个特殊的结构,称为 _分区表_。其中有4条记录 (称为__分区记录__),每条记录16字节。 分区记录表示硬盘如何被划分,在FreeBSD的术语中, 这被称为slice(d)。16字节中有一个标志字节决定这个分区是否可引导。 有仅只能有一个分区可设定这一标志。否则, [.filename]##boot0##的代码将拒绝继续执行。 一个分区记录有如下域: * 1字节 文件系统类型 * 1字节 可引导标志 * 6字节 CHS格式描述符 * 8字节 LBA格式描述符 一个分区记录描述符包含某一分区在硬盘上的确切位置信息。 LBA和CHS两种描述符指示相同的信息,但是指示方式有所不同:LBA (逻辑块寻址,Logical Block Addressing)指示分区的起始扇区和分区长度, 而CHS(柱面 磁头 扇区)指示首扇区和末扇区 引导管理器扫描分区表,并在屏幕上显示菜单,以便用户可以 选择用于引导的磁盘和分区。在键盘上按下相应的键后, [.filename]##boot0##进行如下动作: * 标记选中的分区为可引导,清除以前的可引导标志 * 记住本次选择的分区以备下次引导时作为缺省项 * 装载选中分区的第一个扇区,并跳转执行之 什么数据会存在于一个可引导扇区(这里指FreeBSD扇区)的第一扇区里呢? 正如你已经猜到的,那就是[.filename]##boot2##。 [[boot-boot2]] == ``boot2``阶段 也许你想知道,为什么``boot2``是在 ``boot0``之后,而不是在boot1之后。事实上, 也有一个512字节的文件[.filename]##boot1##存放在目录 [.filename]##/boot##里,那是用来从一张软盘引导系统的。 从软盘引导时,[.filename]##boot1##起着 [.filename]##boot0##对硬盘引导相同的作用:它找到 [.filename]##boot2##并运行之。 你可能已经看到有一文件[.filename]##/boot/mbr##。 这是[.filename]##boot0##的简化版本。 [.filename]##mbr##中的代码不会显示菜单让用户选择, 而只是简单的引导被标志的分区。 实现[.filename]##boot2##的代码存放在目录 [.filename]##sys/boot/i386/boot2/##里,对应的可执行文件在 [.filename]##/boot##里。在[.filename]##/boot##里的文件 [.filename]##boot0##和[.filename]##boot2##不会在引导过程中使用, 只有boot0cfg这样的工具才会使用它们。 [.filename]##boot0##的内容应在MBR中才能生效。 [.filename]##boot2##位于可引导的FreeBSD分区的开始。 这些位置不受文件系统控制,所以它们不可用ls 之类的命令查看。 ``boot2``的主要任务是装载文件 [.filename]##/boot/loader##,那是引导过程的第三阶段。 在``boot2``中的代码不能使用诸如 ``open()``和``read()`` 之类的例程函数,因为内核还没有被加载。而应当扫描硬盘, 读取文件系统结构,找到文件[.filename]##/boot/loader##, 用BIOS的功能将它读入内存,然后从其入口点开始执行之。 除此之外,``boot2``还可提示用户进行选择, loader可以从其它磁盘、系统单元、分区装载。 `boot2` 的二进制代码用特殊的方式产生: [.programlisting] .... sys/boot/i386/boot2/Makefile boot2: boot2.ldr boot2.bin ${BTX}/btx/btx btxld -v -E ${ORG2} -f bin -b ${BTX}/btx/btx -l boot2.ldr \ -o boot2.ld -P 1 boot2.bin .... 这个Makefile片断表明man:btxld[8]被用来链接二进制代码。 BTX表示引导扩展器(BooT eXtender)是给程序(称为客户(client) 提供保护模式环境、并与客户程序相链接的一段代码。所以 ``boot2``是一个BTX客户,使用BTX提供的服务。 工具btxld是链接器, 它将两个二进制代码链接在一起。man:btxld[8]和man:ld[1] 的区别是ld通常将两个目标文件 链接成一个动态链接库或可执行文件,而btxld 则将一个目标文件与BTX链接起来,产生适合于放在分区首部的二进制代码, 以实现系统引导。 ``boot0``执行跳转至BTX的入口点。 然后,BTX将处理器切换至保护模式,并准备一个简单的环境, 然后调用客户。这个环境包括: * 虚拟8086模式。这意味着BTX是虚拟8086的监视程序。 实模式指令,如pushf, popf, cli, sti, if,均可被客户调用。 * 建立中断描述符表(Interrupt Descriptor Table, IDT), 使得所有的硬件中断可被缺省的BIOS程序处理。 建立中断0x30,这是系统调用关口。 * 两个系统调用``exec``和 ``exit``的定义如下: + [.programlisting] .... sys/boot/i386/btx/lib/btxsys.s: .set INT_SYS,0x30 # 中断号 # # System call: exit # __exit: xorl %eax,%eax # BTX系统调用0x0 int $INT_SYS # # # System call: exec # __exec: movl $0x1,%eax # BTX系统调用0x1 int $INT_SYS # .... BTX建立全局描述符表(Global Descriptor Table, GDT): [.programlisting] .... sys/boot/i386/btx/btx/btx.s: gdt: .word 0x0,0x0,0x0,0x0 # 以空为入口 .word 0xffff,0x0,0x9a00,0xcf # SEL_SCODE .word 0xffff,0x0,0x9200,0xcf # SEL_SDATA .word 0xffff,0x0,0x9a00,0x0 # SEL_RCODE .word 0xffff,0x0,0x9200,0x0 # SEL_RDATA .word 0xffff,MEM_USR,0xfa00,0xcf# SEL_UCODE .word 0xffff,MEM_USR,0xf200,0xcf# SEL_UDATA .word _TSSLM,MEM_TSS,0x8900,0x0 # SEL_TSS .... 客户的代码和数据始于地址MEM_USR(0xa000),选择符(selector) SEL_UCODE指向客户的数据段。选择符 SEL_UCODE 拥有第3级描述符权限 (Descriptor Privilege Level, DPL),这是最低级权限。但是 `INT 0x30` 指令的处理程序存储于另一个段里, 这个段的选择符SEL_SCODE (supervisor code)由有着管理级权限。 正如代码建立IDT(中断描述符表)时进行的操作那样: [.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 # 下一个中断处理程序 .... 所以,当客户调用 ``__exec()``时,代码将被以最高权限执行。 这使得内核可以修改保护模式数据结构,如分页表(page tables)、全局描述符表(GDT)、 中断描述符表(IDT)等。 `boot2` 定义了一个重要的数据结构: `struct bootinfo`。这个结构由 `boot2` 初始化,然后被转送到loader,之后又被转入内核。 这个结构的部分项目由``boot2``设定,其余的由loader设定。 这个结构中的信息包括内核文件名、BIOS提供的硬盘柱面/磁头/扇区数目信息、 BIOS提供的引导设备的驱动器编号,可用的物理内存大小,`envp` 指针(环境指针)等。定义如下: [.programlisting] .... /usr/include/machine/bootinfo.h struct bootinfo { u_int32_t bi_version; u_int32_t bi_kernelname; /* 用一个字节表示 * */ u_int32_t bi_nfs_diskless; /* struct nfs_diskless * */ /* 以上为常备项 */ #define bi_endcommon bi_n_bios_used u_int32_t bi_n_bios_used; u_int32_t bi_bios_geom[N_BIOS_GEOM]; u_int32_t bi_size; u_int8_t bi_memsizes_valid; u_int8_t bi_bios_dev; /* 引导设备的BIOS单元编号 */ u_int8_t bi_pad[2]; u_int32_t bi_basemem; u_int32_t bi_extmem; u_int32_t bi_symtab; /* struct symtab * */ u_int32_t bi_esymtab; /* struct symtab * */ /* 以下项目仅高级bootloader提供 */ u_int32_t bi_kernend; /* 内核空间末端 */ u_int32_t bi_envp; /* 环境 */ u_int32_t bi_modulep; /* 预装载的模块 */ }; .... ``boot2`` 进入一个循环等待用户输入,然后调用 ``load()``。如果用户不做任何输入,循环将在一段时间后结束, ``load()`` 将会装载缺省文件([.filename]##/boot/loader##)。 函数 ``ino_t lookup(char *filename)``和 ``int xfsread(ino_t inode, void *buf, size_t nbyte)`` 用来将文件内容读入内存。[.filename]##/boot/loader##是一个ELF格式二进制文件, 不过它的头部被换成了a.out格式中的``struct exec``结构。 ``load()``扫描loader的ELF头部,装载[.filename]##/boot/loader## 至内存,然后跳转至入口执行之: [.programlisting] .... sys/boot/i386/boot2/boot2.c: __exec((caddr_t)addr, RB_BOOTINFO | (opts RBX_MASK), MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part), 0, 0, 0, VTOP(bootinfo)); .... [[boot-loader]] == loader阶段 loader也是一个 BTX 客户,在这里不作详述。 已有一部内容全面的手册 man:loader[8] ,由Mike Smith书写。 比loader更底层的BTX的机理已经在前面讨论过。 loader 的主要任务是引导内核。当内核被装入内存后,即被loader调用: [.programlisting] .... sys/boot/common/boot.c: /* 从loader中调用内核中对应的exec程序 */ module_formats[km-m_loader]-l_exec(km); .... [[boot-kernel]] == 内核初始化 让我们来看一下链接内核的命令。 这能帮助我们了解 loader 传递给内核的准确位置。 这个位置就是内核真实的入口点。 [.programlisting] .... sys/conf/Makefile.i386: ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic \ -dynamic-linker /red/herring -o kernel -X locore.o \ lots of kernel .o files .... 在这一行中有一些有趣的东西。首先,内核是一个ELF动态链接二进制文件, 可是动态链接器却是[.filename]##/red/herring##,一个莫须有的文件。 其次,看一下文件[.filename]##sys/conf/ldscript.i386##, 可以对理解编译内核时ld的选项有一些启发。 阅读最前几行,字符串 [.programlisting] .... sys/conf/ldscript.i386: ENTRY(btext) .... 表示内核的入口点是符号 ``btext``。这个符号在[.filename]##locore.s## 中定义: [.programlisting] .... sys/i386/i386/locore.s: .text /********************************************************************** * * This is where the bootblocks start us, set the ball rolling... * 入口 */ NON_GPROF_ENTRY(btext) .... 首先将寄存器EFLAGS设为一个预定义的值0x00000002, 然后初始化所有段寄存器: [.programlisting] .... sys/i386/i386/locore.s /* 不要相信BIOS给出的EFLAGS值 */ pushl $PSL_KERNEL popfl /* * 不要相信BIOS给出的%fs、%gs值。相信引导过程中设定的%cs、%ds、%es、%ss值 */ mov %ds, %ax mov %ax, %fs mov %ax, %gs .... btext调用例程``recover_bootinfo()``, `identify_cpu()`,`create_pagetables()`。 这些例程也定在[.filename]##locore.s##之中。这些例程的功能如下: [.informaltable] [cols="1,1", frame="none"] |=== |`recover_bootinfo` |这个例程分析由引导程序传送给内核的参数。引导内核有3种方式: 由loader引导(如前所述), 由老式磁盘引导块引导,无盘引导方式。 这个函数决定引导方式,并将结构``struct bootinfo`` 存储至内核内存。 |`identify_cpu` |这个函数侦测CPU类型,将结果存放在变量 ``_cpu``中。 |`create_pagetables` |这个函数为分页表在内核内存空间顶部分配一块空间,并填写一定内容 |=== 下一步是开启VME(如果CPU有这个功能): [.programlisting] .... testl $CPUID_VME, R(_cpu_feature) jz 1f movl %cr4, %eax orl $CR4_VME, %eax movl %eax, %cr4 .... 然后,启动分页模式: [.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] .... pushl $begin /* jump to high virtualized address */ ret /* 现在跳转至KERNBASE,那里是操作系统内核被链接后真正的入口 */ begin: .... 函数``init386()``被调用;随参数传递的是一个指针, 指向第一个空闲物理页。随后执行``mi_startup()``。 ``init386``是一个与硬件系统相关的初始化函数, ``mi_startup()``是个与硬件系统无关的函数 (前缀'mi_'表示Machine Independent,不依赖于机器)。 内核不再从``mi_startup()``里返回; 调用这个函数后,内核完成引导: [.programlisting] .... sys/i386/i386/locore.s: movl physfree, %esi pushl %esi /* 送给init386()的第一个参数 */ call _init386 /* 设置386芯片使之适应UNIX工作 */ call _mi_startup /* 自动配置硬件,挂接根文件系统,等 */ hlt /* 不再返回到这里! */ .... === `init386()` ``init386()``定义在 [.filename]##sys/i386/i386/machdep.c##中, 它针对Intel 386芯片进行低级初始化。loader已将CPU切换至保护模式。 loader已经建立了最早的任务。 [TIP] .译者注 ==== 每个"任务"都是与其它"``任务``"相对独立的执行环境。 任务之间可以分时切换,这为并发进程/线程的实现提供了必要基础。 对于Intel 80x86任务的描述,详见Intel公司关于80386 CPU及后续产品的资料, 或者在link:http://www.lib.tsinghua.edu.cn/[清华大学图书馆] 馆藏记录中用"80386"作为关键词所查找到的系统结构方面的书目。 ==== 在这个任务中,内核将继续工作。在讨论其代码前, 我将处理器对保护模式必须完成的一系列准备工作一并列出: * 初始化内核的可调整参数,这些参数由引导程序传来 * 准备GDT(全局描述符表) * 准备IDT(中断描述符表) * 初始化系统控制台 * 初始化DDB(内核的点调试器),如果它被编译进内核的话 * 初始化TSS(任务状态段) * 准备LDT(局部描述符表) * 建立proc0(0号进程,即内核的进程)的pcb(进程控制块) ``init386()``首先初始化内核的可调整参数, 这些参数由引导程序传来。先设置环境指针(environment pointer, envp)调用, 再调用``init_param1()``。 envp指针已由loader存放在结构``bootinfo``中: [.programlisting] .... sys/i386/i386/machdep.c: kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE; /* 初始化基本可调整项,如hz等 */ init_param1(); .... ``init_param1()``定义在 [.filename]##sys/kern/subr_param.c##之中。 这个文件里有一些sysctl项,还有两个函数, ``init_param1()``和``init_param2()``。 这两个函数从``init386()``中调用: [.programlisting] .... sys/kern/subr_param.c hz = HZ; TUNABLE_INT_FETCH("kern.hz", hz); .... TUNABLE_typename_FETCH用来获取环境变量的值: [.programlisting] .... /usr/src/sys/sys/kernel.h #define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var)) .... Sysctl``kern.hz``是系统时钟频率。同时, 这些sysctl项被``init_param1()``设定: ``kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.maxdsiz, kern.dflssiz, kern.maxssiz, kern.sgrowsiz``。 然后``init386()`` 准备全局描述符表 (Global Descriptors Table, GDT)。在x86上每个任务都运行在自己的虚拟地址空间里, 这个空间由"段址:偏移量"的数对指定。举个例子,当前将要由处理器执行的指令在 CS:EIP,那么这条指令的线性虚拟地址就是"代码段虚拟段地址CS" + EIP。 为了简便,段起始于虚拟地址0,终止于界限4G字节。所以,在这个例子中, 指令的线性虚拟地址正是EIP的值。段寄存器,如CS、DS等是选择符, 即全局描述符表中的索引(更精确的说,索引并非选择符的全部, 而是选择符中的INDEX部分)。 [TIP] .译者注 ==== 对于80386, 选择符有16位,INDEX部分是其中的高13位。 ==== FreeBSD的全局描述符表为每个CPU保存着15个选择符: [.programlisting] .... sys/i386/i386/machdep.c: union descriptor gdt[NGDT * MAXCPU]; /* 全局描述符表 */ sys/i386/include/segments.h: /* * 全局描述符表(GDT)中的入口 */ #define GNULL_SEL 0 /* 空描述符 */ #define GCODE_SEL 1 /* 内核代码描述符 */ #define GDATA_SEL 2 /* 内核数据描述符 */ #define GPRIV_SEL 3 /* 对称多处理(SMP)每处理器专有数据 */ #define GPROC0_SEL 4 /* Task state process slot zero and up, 任务状态进程 */ #define GLDT_SEL 5 /* 每个进程的局部描述符表 */ #define GUSERLDT_SEL 6 /* 用户自定义的局部描述符表 */ #define GTGATE_SEL 7 /* 进程任务切换关口 */ #define GBIOSLOWMEM_SEL 8 /* BIOS低端内存访问(必须是这第8个入口) */ #define GPANIC_SEL 9 /* 会导致全系统异常中止工作的任务状态 */ #define GBIOSCODE32_SEL 10 /* BIOS接口(32位代码) */ #define GBIOSCODE16_SEL 11 /* BIOS接口(16位代码) */ #define GBIOSDATA_SEL 12 /* BIOS接口(数据) */ #define GBIOSUTIL_SEL 13 /* BIOS接口(工具) */ #define GBIOSARGS_SEL 14 /* BIOS接口(自变量,参数) */ .... 请注意,这些#defines并非选择符本身,而只是选择符中的INDEX域, 因此它们正是全局描述符表中的索引。 例如,内核代码的选择符(GCODE_SEL)的值为0x08。 下一步是初始化中断描述符表(Interrupt Descriptor Table, IDT)。 这张表在发生软件或硬件中断时会被处理器引用。例如,执行系统调用时, 用户应用程序提交``INT 0x80`` 指令。这是一个软件中断, 处理器用索引值0x80在中断描述符表中查找记录。这个记录指向处理这个中断的例程。 在这个特定情形中,这是内核的系统调用关口。 [TIP] .译者注 ==== Intel 80386支持"``调用门``",可以使得用户程序只通过一条call指令 就调用内核中的例程。可是FreeBSD并未采用这种机制, 也许是因为使用软中断接口可免去动态链接的麻烦吧。另外还有一个附带的好处: 在仿真Linux时,当遇到FreeBSD内核不支持的而又并非关键性的系统调用时, 内核只会显示一些出错信息,这使得程序能够继续运行; 而不是在真正执行程序之前的初始化过程中就因为动态链接失败而不允许程序运行。 ==== 中断描述符表最多可以有256 (0x100)条记录。内核分配NIDT条记录的内存给中断描述符表, 这里NIDT=256,是最大值: [.programlisting] .... sys/i386/i386/machdep.c: static struct gate_descriptor idt0[NIDT]; struct gate_descriptor *idt = idt0[0]; /* 中断描述符表 */ .... 每个中断都被设置一个合适的中断处理程序。 系统调用关口``INT 0x80``也是如此: [.programlisting] .... sys/i386/i386/machdep.c: setidt(0x80, IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); .... 所以当一个用户应用程序提交``INT 0x80``指令时, 全系统的控制权会传递给函数``_Xint0x80_syscall``, 这个函数在内核代码段中,将被以管理员权限执行。 然后,控制台和DDB(调试器)被初始化: [.programlisting] .... sys/i386/i386/machdep.c: cninit(); /* 以下代码可能因为未定义宏DDB而被跳过 */ #ifdef DDB kdb_init(); if (boothowto RB_KDB) Debugger("Boot flags requested debugger"); #endif .... 任务状态段(TSS)是另一个x86保护模式中的数据结构。当发生任务切换时, 任务状态段用来让硬件存储任务现场信息。 局部描述符表(LDT)用来指向用户代码和数据。系统定义了几个选择符, 指向局部描述符表,它们是系统调用关口和用户代码、用户数据选择符: [.programlisting] .... /usr/include/machine/segments.h #define LSYS5CALLS_SEL 0 /* Intel BCS强制要求的 */ #define LSYS5SIGR_SEL 1 #define L43BSDCALLS_SEL 2 /* 尚无 */ #define LUCODE_SEL 3 #define LSOL26CALLS_SEL 4 /* Solaris =2.6版系统调用关口 */ #define LUDATA_SEL 5 /* separate stack, es,fs,gs sels ? 分别的栈、es、fs、gs选择符? */ /* #define LPOSIXCALLS_SEL 5*/ /* notyet, 尚无 */ #define LBSDICALLS_SEL 16 /* BSDI system call gate, BSDI系统调用关口 */ #define NLDT (LBSDICALLS_SEL + 1) .... 然后,proc0(0号进程,即内核所处的进程)的进程控制块(Process Control Block) (`struct pcb`)结构被初始化。proc0是一个 `struct proc` 结构,描述了一个内核进程。 内核运行时,该进程总是存在,所以这个结构在内核中被定义为全局变量: [.programlisting] .... sys/kern/kern_init.c: struct proc proc0; .... 结构``struct pcb``是proc结构的一部分, 它定义在[.filename]##/usr/include/machine/pcb.h##之中, 内含针对i386硬件结构专有的信息,如寄存器的值。 === `mi_startup()` 这个函数用冒泡排序算法,将所有系统初始化对象,然后逐个调用每个对象的入口: [.programlisting] .... sys/kern/init_main.c: for (sipp = sysinit; *sipp; sipp++) { /* ... 省略 ... */ /* 调用函数 */ (*((*sipp)-func))((*sipp)-udata); /* ... 省略 ... */ } .... 尽管sysinit框架已经在《FreeBSD开发者手册》中有所描述, 我还是在这里讨论一下其内部原理。 每个系统初始化对象(sysinit对象)通过调用宏建立。 让我们以``announce`` sysinit对象为例。 这个对象打印版权信息: [.programlisting] .... sys/kern/init_main.c: static void print_caddr_t(void *data __unused) { printf("%s", (char *)data); } SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright) .... 这个对象的子系统标识是SI_SUB_COPYRIGHT(0x0800001), 数值刚好排在SI_SUB_CONSOLE(0x0800000)后面。 所以,版权信息将在控制台初始化之后就被很早的打印出来。 让我们看一看宏``SYSINIT()``到底做了些什么。 它展开成宏``C_SYSINIT()``。 宏``C_SYSINIT()``然后展开成一个静态结构 ``struct sysinit``。结构里申明里调用了另一个宏 ``DATA_SET``: [.programlisting] .... /usr/include/sys/kernel.h: #define C_SYSINIT(uniquifier, subsystem, order, func, ident) \ static struct sysinit uniquifier ## _sys_init = { \ subsystem, \ order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ## _sys_init); #define SYSINIT(uniquifier, subsystem, order, func, ident) \ C_SYSINIT(uniquifier, subsystem, order, \ (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident) .... 宏``DATA_SET()``展开成``MAKE_SET()``, 宏``MAKE_SET()``指向所有隐含的sysinit幻数: [.programlisting] .... /usr/include/linker_set.h #define MAKE_SET(set, sym) \ static void const * const __set_##set##_sym_##sym = sym; \ __asm(".section .set." #set ",\"aw\""); \ __asm(".long " #sym); \ __asm(".previous") #endif #define TEXT_SET(set, sym) MAKE_SET(set, sym) #define DATA_SET(set, sym) MAKE_SET(set, sym) .... 回到我们的例子中,经过宏的展开过程,将会产生如下声明: [.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 = announce_sys_init; __asm(".section .set.sysinit_set" ",\"aw\""); __asm(".long " "announce_sys_init"); __asm(".previous"); .... 第一个``\__asm``指令在内核可执行文件中建立一个ELF节(section)。 这发生在内核链接的时候。这一节将被命令为``.set.sysinit_set``。 这一节的内容是一个32位值――announce_sys_init结构的地址,这个结构正是第二个 ``\__asm``指令所定义的。第三个``\__asm``指令标记节的结束。 如果前面有名字相同的节定义语句,节的内容(那个32位值)将被填加到已存在的节里, 这样就构造出了一个32位指针数组。 用objdump察看一个内核二进制文件, 也许你会注意到里面有这么几个小的节: [source,shell] .... % objdump -h /kernel 7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2 CONTENTS, ALLOC, LOAD, DATA 8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2 CONTENTS, ALLOC, LOAD, DATA 9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2 CONTENTS, ALLOC, LOAD, DATA 10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2 CONTENTS, ALLOC, LOAD, DATA 11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2 CONTENTS, ALLOC, LOAD, DATA 12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2 CONTENTS, ALLOC, LOAD, DATA .... 这一屏信息显示表明节.set.sysinit_set有0x664字节的大小, 所以``0x664/sizeof(void *)``个sysinit对象被编译进了内核。 其它节,如``.set.sysctl_set``表示其它链接器集合。 通过定义一个类型为``struct linker_set``的变量, 节``.set.sysinit_set``将被"收集"到那个变量里: [.programlisting] .... sys/kern/init_main.c: extern struct linker_set sysinit_set; /* XXX */ .... ``struct linker_set``定义如下: [.programlisting] .... /usr/include/linker_set.h: struct linker_set { int ls_length; void *ls_items[1]; /* ls_length个项的数组, 以NULL结尾 */ }; .... [TIP] .译者注 ==== 实际上是说, 用C语言结构体linker_set来表达那个ELF节。 ==== 第一项是sysinit对象的数量,第二项是一个以NULL结尾的数组, 数组中是指向那些对象的指针。 回到对``mi_startup()``的讨论, 我们清楚了sysinit对象是如何被组织起来的。 函数``mi_startup()``将它们排序, 并调用每一个对象。最后一个对象是系统调度器: [.programlisting] .... /usr/include/sys/kernel.h: enum sysinit_sub_id { SI_SUB_DUMMY = 0x0000000, /* 不被执行,仅供链接器使用 */ SI_SUB_DONE = 0x0000001, /* 已被处理*/ SI_SUB_CONSOLE = 0x0800000, /* 控制台*/ SI_SUB_COPYRIGHT = 0x0800001, /* 最早使用控制台的对象 */ ... SI_SUB_RUN_SCHEDULER = 0xfffffff /* 调度器:不返回 */ }; .... 系统调度器sysinit对象定义在文件[.filename]##sys/vm/vm_glue.c##中, 这个对象的入口点是``scheduler()``。 这个函数实际上是个无限循环,它表示那个进程标识(PID)为0的进程――swapper进程。 前面提到的proc0结构正是用来描述这个进程。 第一个用户进程是_init_, 由sysinit对象``init``建立: [.programlisting] .... sys/kern/init_main.c: static void create_init(const void *udata __unused) { int error; int s; s = splhigh(); error = fork1(proc0, RFFDG | RFPROC, initproc); if (error) panic("cannot fork init: %d\n", error); initproc-p_flag |= P_INMEM | P_SYSTEM; cpu_set_fork_handler(initproc, start_init, NULL); remrunqueue(initproc); splx(s); } SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL) .... ``create_init()``通过调用``fork1()`` 分配一个新的进程,但并不将其标记为可运行。当这个新进程被调度器调度执行时, ``start_init()``将会被调用。 那个函数定义在[.filename]##init_main.c##中。 它尝试装载并执行二进制代码[.filename]##init##, 先尝试[.filename]##/sbin/init##,然后是[.filename]##/sbin/oinit##, [.filename]##/sbin/init.bak##,最后是[.filename]##/stand/sysinstall##: [.programlisting] .... sys/kern/init_main.c: static char init_path[MAXPATHLEN] = #ifdef INIT_PATH __XSTRING(INIT_PATH); #else "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall"; #endif .... diff --git a/documentation/content/zh-cn/books/arch-handbook/chapters-order.adoc b/documentation/content/zh-cn/books/arch-handbook/chapters-order.adoc index af38e8536e..ab4d8f3079 100644 --- a/documentation/content/zh-cn/books/arch-handbook/chapters-order.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/chapters-order.adoc @@ -1,17 +1,20 @@ -boot/chapter.adoc -locking/chapter.adoc -kobj/chapter.adoc -jail/chapter.adoc -sysinit/chapter.adoc -mac/chapter.adoc -vm/chapter.adoc -smp/chapter.adoc -driverbasics/chapter.adoc -isa/chapter.adoc -pci/chapter.adoc -scsi/chapter.adoc -usb/chapter.adoc -newbus/chapter.adoc -sound/chapter.adoc -pccard/chapter.adoc -bibliography/chapter.adoc +parti.adoc +boot/_index.adoc +locking/_index.adoc +kobj/_index.adoc +jail/_index.adoc +sysinit/_index.adoc +mac/_index.adoc +vm/_index.adoc +smp/_index.adoc +partii.adoc +driverbasics/_index.adoc +isa/_index.adoc +pci/_index.adoc +scsi/_index.adoc +usb/_index.adoc +newbus/_index.adoc +sound/_index.adoc +pccard/_index.adoc +partiii.adoc +bibliography/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/driverbasics/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/driverbasics/_index.adoc similarity index 99% rename from documentation/content/zh-cn/books/arch-handbook/driverbasics/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/driverbasics/_index.adoc index 9eeb83eb86..bec7d8bb40 100644 --- a/documentation/content/zh-cn/books/arch-handbook/driverbasics/chapter.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/driverbasics/_index.adoc @@ -1,509 +1,509 @@ --- title: 第 9 章 编写 FreeBSD 设备驱动程序 -prev: books/arch-handbook/smp +prev: books/arch-handbook/partii next: books/arch-handbook/isa --- [[driverbasics]] = 编写 FreeBSD 设备驱动程序 :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: 9 :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] [[driverbasics-intro]] == 简介 本章简要介绍了如何为FreeBSD编写设备驱动程序。术语设备在 这儿的上下文中多用于指代系统中硬件相关的东西,如磁盘,打印机, 图形显式器及其键盘。设备驱动程序是操作系统中用于控制特定设备的 软件组件。也有所谓的伪设备,即设备驱动程序用软件模拟设备的行为, 而没有特定的底层硬件。设备驱动程序可以被静态地编译进系统,或者 通过动态内核链接工具'``kld``'在需要时加载。 类UNIX(R)操作系统中的大多数设备都是通过设备节点来访问的,有时也 被称为特殊文件。这些文件在文件系统的层次结构中通常位于 [.filename]##/dev##目录下。在FreeBSD 5.0-RELEASE以前的 发行版中, 对man:devfs[5]的支持还没有被集成到FreeBSD中,每个设备 节点必须要静态创建,并且独立于相关设备驱动程序的存在。系统中大 多数设备节点是通过运行``MAKEDEV``创建的。 设备驱动程序可以粗略地分为两类,字符和网络设备驱动程序。 [[driverbasics-kld]] == 动态内核链接工具-KLD kld接口允许系统管理员从运行的系统中动态地添加和删除功能。 这允许设备驱动程序的编写者将他们的新改动加载到运行的内核中, 而不用为了测试新改动而频繁地重启。 kld接口通过下面的特权命令使用: * `kldload` - 加载新内核模块 * `kldunload` - 卸载内核模块 * `kldstat` - 列举当前加载的模块 内核模块的程序框架 [.programlisting] .... /* * KLD程序框架 * 受Andrew Reiter在Daemonnews上的文章所启发 */ #include sys/types.h #include sys/module.h #include sys/systm.h /* uprintf */ #include sys/errno.h #include sys/param.h /* kernel.h中用到的定义 */ #include sys/kernel.h /* 模块初始化中使用的类型 */ /* * 加载处理函数,负责处理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 = EOPNOTSUPP; break; } return(err); } /* 向内核其余部分声明此模块 */ static moduledata_t skel_mod = { "skel", skel_loader, NULL }; DECLARE_MODULE(skeleton, skel_mod, SI_SUB_KLD, SI_ORDER_ANY); .... === Makefile FreeBSD提供了一个makefile包含文件,利用它你可以快速地编译 你附加到内核的东西。 [.programlisting] .... SRCS=skeleton.c KMOD=skeleton .include bsd.kmod.mk .... 简单地用这个makefile运行``make``就能够创建文件 [.filename]#skeleton.ko#,键入如下命令可以把它加载到内核: [source,shell] .... # kldload -v ./skeleton.ko .... [[driverbasics-access]] == 访问设备驱动程序 UNIX(R) 提供了一套公共的系统调用供用户的应用程序使用。当用户访问 设备节点时,内核的上层将这些调用分发到相应的设备驱动程序。脚本 ``/dev/MAKEDEV``为你的系统生成了大多数的设备节点, 但如果你正在开发你自己的驱动程序,可能需要用 ``mknod``创建你自己的设备节点。 === 创建静态设备节点 ``mknod``命令需要四个参数来创建设备节点。 你必须指定设备节点的名字,设备的类型,设备的主号码和设备的从号码。 === 动态设备节点 设备文件系统,或者说devfs,在全局文件系统名字空间中提供对 内核设备名字空间的访问。这消除了由于有设备驱动程序而没有静态 设备节点,或者有设备节点而没有安装设备驱动程序而带来的潜在问题。 Devfs仍在进展中,但已经能够工作得相当好了。 [[driverbasics-char]] == 字符设备 字符设备驱动程序直接从用户进程传输数据,或传输数据到用户进程。 这是最普通的一类设备驱动程序,源码树中有大量的简单例子。 这个简单的伪设备例子会记住你写给它的任何值,并且当你读取它的时候 会将这些值返回给你。下面显示了两个版本,一个适用于FreeBSD 4.X, 一个适用于FreeBSD 5.X。 .适用于FreeBSD 4.X的回显伪设备驱动程序实例 [example] ==== [.programlisting] .... /* * 简单‘echo’伪设备KLD * * Murray Stokely */ #define MIN(a,b) (((a) (b)) ? (a) : (b)) #include sys/types.h #include sys/module.h #include sys/systm.h /* uprintf */ #include sys/errno.h #include sys/param.h /* kernel.h中用到的定义 */ #include sys/kernel.h /* 模块初始化中使用的类型 */ #include sys/conf.h /* cdevsw结构 */ #include sys/uio.h /* uio结构 */ #include sys/malloc.h #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 }; typedef struct s_echo { char msg[BUFFERSIZE]; int len; } t_echo; /* 变量 */ static dev_t sdev; 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(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 = EOPNOTSUPP; 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-uio_resid, (echomsg-len - uio-uio_offset 0) ? echomsg-len - uio-uio_offset : 0); if ((err = uiomove(echomsg-msg + uio-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-uio_iov-iov_base, echomsg-msg, MIN(uio-uio_iov-iov_len, BUFFERSIZE - 1)); /* 现在需要以null结束字符串,并记录长度 */ *(echomsg-msg + MIN(uio-uio_iov-iov_len, BUFFERSIZE - 1)) = 0; echomsg-len = MIN(uio-uio_iov-iov_len, BUFFERSIZE); if (err != 0) { uprintf("Write failed: bad address!\n"); } count++; return(err); } DEV_MODULE(echo,echo_loader,NULL); .... ==== .适用于FreeBSD 5.X回显伪设备驱动程序实例 [example] ==== [.programlisting] .... /* * 简单‘echo’伪设备 KLD * * Murray Stokely * * 此代码由Søren (Xride) Straarup转换到5.X */ #include sys/types.h #include sys/module.h #include sys/systm.h /* uprintf */ #include sys/errno.h #include sys/param.h /* kernel.h中用到的定义 */ #include sys/kernel.h /* 模块初始化中使用的类型 */ #include sys/conf.h /* cdevsw结构 */ #include sys/uio.h /* uio结构 */ #include sys/malloc.h #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(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 = EOPNOTSUPP; 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-uio_resid, (echomsg-len - uio-uio_offset 0) ? echomsg-len - uio-uio_offset : 0); if ((err = uiomove(echomsg-msg + uio-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-uio_iov-iov_base, echomsg-msg, MIN(uio-uio_iov-iov_len, BUFFERSIZE - 1)); /* 现在需要以null结束字符串,并记录长度 */ *(echomsg-msg + MIN(uio-uio_iov-iov_len, BUFFERSIZE - 1)) = 0; echomsg-len = MIN(uio-uio_iov-iov_len, BUFFERSIZE); if (err != 0) { uprintf("Write failed: bad address!\n"); } count++; return(err); } DEV_MODULE(echo,echo_loader,NULL); .... ==== 在FreeBSD 4.X上安装此驱动程序,你将首先需要用如下命令在 你的文件系统上创建一个节点: [source,shell] .... # mknod /dev/echo c 33 0 .... 驱动程序被加载后,你应该能够键入一些东西,如: [source,shell] .... # echo -n "Test Data" > /dev/echo # cat /dev/echo Test Data .... 真正的硬件设备在下一章描述。 补充资源 * http://ezine.daemonnews.org/200010/blueprints.html[Dynamic Kernel Linker (KLD) Facility Programming Tutorial] - http://www.daemonnews.org/[Daemonnews] October 2000 * http://ezine.daemonnews.org/200007/newbus-intro.html[How to Write Kernel Drivers with NEWBUS] - http://www.daemonnews.org/[Daemonnews] July 2000 [[driverbasics-block]] == 块设备(消亡中) 其他UNIX(R)系统支持另一类型的磁盘设备,称为块设备。块设备是内核 为它们提供缓冲的磁盘设备。这种缓冲使得块设备几乎没有用,或者说非常 不可靠。缓冲会重新安排写操作的次序,使得应用程序丧失了在任何时刻及时 知道准确的磁盘内容的能力。这导致对磁盘数据结构(文件系统,数据库等)的 可预测的和可靠的崩溃恢复成为不可能。由于写操作被延迟,内核无法向应用 程序报告哪个特定的写操作遇到了写错误,这又进一步增加了一致性问题。 由于这个原因,真正的应用程序从不依赖于块设备,事实上,几乎所有访问 磁盘的应用程序都尽力指定总是使用字符(或"raw")设备。 由于实现将每个磁盘(分区)同具有不同语义的两个设备混为一谈,从而致使 相关内核代码极大地复杂化,作为推进磁盘I/O基础结构现代化的一部分,FreeBSD 抛弃了对带缓冲的磁盘设备的支持。 [[driverbasics-net]] == 网络设备驱动程序 访问网络设备的驱动程序不需要使用设备节点。选择哪个驱动程序是 基于内核内部的其他决定而不是调用open(),对网络设备的使用通常由 系统调用socket(2)引入。 更多细节, 请参见 ifnet(9) 联机手册、 回环设备的源代码, 以及 Bill Paul 撰写的网络驱动程序。 diff --git a/documentation/content/zh-cn/books/arch-handbook/isa/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/isa/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/isa/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/isa/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/jail/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/jail/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/jail/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/jail/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/kobj/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/kobj/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/kobj/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/kobj/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/locking/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/locking/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/locking/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/locking/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/mac/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/mac/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/mac/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/mac/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/newbus/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/newbus/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/newbus/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/newbus/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/parti.adoc b/documentation/content/zh-cn/books/arch-handbook/parti.adoc new file mode 100644 index 0000000000..c4ade3e3a2 --- /dev/null +++ b/documentation/content/zh-cn/books/arch-handbook/parti.adoc @@ -0,0 +1,18 @@ +--- +title: Part I. 内核 +prev: books/arch-handbook +next: books/arch-handbook/boot +--- + +ifdef::env-beastie+backend-html5[] +:chapters-path: content/{{% lang %}}/books/arch-handbook/ +endif::[] + +ifndef::env-beastie[] +:chapters-path: +endif::[] + +[[kernel]] += 内核 + +include::{chapters-path}toc-1.adoc[] diff --git a/documentation/content/zh-cn/books/arch-handbook/partii.adoc b/documentation/content/zh-cn/books/arch-handbook/partii.adoc new file mode 100644 index 0000000000..cf1d799e46 --- /dev/null +++ b/documentation/content/zh-cn/books/arch-handbook/partii.adoc @@ -0,0 +1,18 @@ +--- +title: Part II. 设备驱动程序 +prev: books/arch-handbook/smp +next: books/arch-handbook/driverbasics +--- + +ifdef::env-beastie+backend-html5[] +:chapters-path: content/{{% lang %}}/books/arch-handbook/ +endif::[] + +ifndef::env-beastie[] +:chapters-path: +endif::[] + +[[devicedrivers]] += 设备驱动程序 + +include::{chapters-path}toc-2.adoc[] diff --git a/documentation/content/zh-cn/books/arch-handbook/partiii.adoc b/documentation/content/zh-cn/books/arch-handbook/partiii.adoc new file mode 100644 index 0000000000..ab4deab9b7 --- /dev/null +++ b/documentation/content/zh-cn/books/arch-handbook/partiii.adoc @@ -0,0 +1,18 @@ +--- +title: Part III. 附录 +prev: books/arch-handbook/pccard +next: books/arch-handbook/bibliography +--- + +ifdef::env-beastie+backend-html5[] +:chapters-path: content/{{% lang %}}/books/arch-handbook/ +endif::[] + +ifndef::env-beastie[] +:chapters-path: +endif::[] + +[[appendices]] += 附录 + +include::{chapters-path}toc-3.adoc[] diff --git a/documentation/content/zh-cn/books/arch-handbook/pccard/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/pccard/_index.adoc similarity index 99% rename from documentation/content/zh-cn/books/arch-handbook/pccard/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/pccard/_index.adoc index 939141d559..459a28f986 100644 --- a/documentation/content/zh-cn/books/arch-handbook/pccard/chapter.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/pccard/_index.adoc @@ -1,212 +1,212 @@ --- title: 第 16 章 PC Card prev: books/arch-handbook/sound -next: books/arch-handbook/bibliography +next: books/arch-handbook/partiii --- [[pccard]] = PC Card :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: 16 :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] 本章将讨论FreeBSD为编写PC Card或CardBus设备的驱动程序而提供的机制。 但目前本文只记录了如何向现有的pccard驱动程序中添加驱动程序。 [[pccard-adddev]] == 添加设备 向所支持的pccard设备列表中添加新设备的步骤已经与系统在FreeBSD 4 中使用的方法不同了。在以前的版本中,需要编辑 [.filename]##/etc##中的一个文件来列出设备。从FreeBSD 5.0开始, 设备驱动程序知道它们支持什么设备。现在内核中有一个受支持设备的表, 驱动程序用它来连接设备。 [[pccard-overview]] === 概览 可以有两种方法来识别PC Card,他们都基于卡上的 CIS信息。第一种方法是使用制造商和产品的数字编号。 第二种方法是使用人可读的字符串,字符串也是包含在CIS中。PC Card总线 使用集中式数据库和一些宏来提供一个易用的设计模式,让驱动程序的编写 者很容易地确定匹配其驱动程序的设备。 一个很普遍的实际情况是,某个公司为一款PC Card产品开发出参考 设计,然后把这个设计卖给另外的公司,以便在市场上出售。那些公司改进 原设计,把向他们的目标客户群或地理区域出售产品,并将他们自己的名字 放到卡中。然而所谓的对现有卡的改进,即使做过任何修改,这些修改通常 也微乎其微。然而,为了强化他们自己版本的品牌,这些供货商常常会把他们 公司的名字放入CIS空间的可读字符串中,却不会改动制造商和产品的ID。 鉴于以上情况,对于FreeBSD来说使用数字ID可以减小工作量。同时也 会减小将ID加入到系统的过程中所带来的复杂性。必须仔细检查谁是卡的 真正制造者,尤其当提供原卡的供货商在中心数据库中已经有一个不同的ID 时。Linksys,D-Link和NetGear是经常出售相同设计的几个美国制造商。 相同的设计可能在日本以诸如Buffalo和Corega的名字出售。然而,这些 设备常常具有相同的制造商和产品ID。 PC Card总线在其中心数据库 [.filename]##/sys/dev/pccard/pccarddevs##中保存了卡的信息, 但不包含哪个驱动程序与它们关联的信息。它也提供了一套宏,以允许在 驱动程序用来声明设备的表中容易地创建简单条目。 最后,某些非常低端的设备根本不包含制造商标识。这些设备需要使用 可读CIS字符串来匹配它们。如果我们不需要这种应急办法该有多好,但对于 某些非常低端却非常流行的CD-ROM播放器来说却是必需的。通常应当避免 使用这种方法,但本节中还是列出了很多设备,因为它们是在认识到PC Card商业的OEM本质之前加入的,应当优先使用 数字方法。 [[pccard-pccarddevs]] === [.filename]##pccarddevs##的格式 [.filename]##pccarddevs##文件有四节。第一节为使用 它们的那些供货商列出了制造商号码。本节按数字排序。下一节包含了 这些供货商使用的所有产品,包括他们的产品ID号码和描述字符串。 描述字符串通常不会被使用(相反,即使我们可以匹配数字版本号,我们 仍然基于人可读的CIS设置设备的描述)。然后为使用字符串匹配方法的 那些设备重复这两节的东西。最后,文件任何地方可以使用C风格的注释。 文件的第一节包含供货商ID。请保持列表按数字排序。此外,为了 能有一个通用清晰的保存地来方便地保存这些信息,我们与NetBSD共享此 文件,因此请协调对此文件的任何更改。例如: [.programlisting] .... vendor FUJITSU 0x0004 Fujitsu Corporation vendor NETGEAR_2 0x000b Netgear vendor PANASONIC 0x0032 Matsushita Electric Industrial Co. vendor SANDISK 0x0045 Sandisk Corporation .... 显示了几个供货商ID。很凑巧的是``NETGEAR_2`` 实际上是NETGEAR从其购买卡的OEM,对那些卡提供支持的作者那时并不知道 NETgear使用的是别人的ID。这些条目相当直接易懂。每行上都有供货商 关键字来指示本行的类别。也有供货商的名字。名字将会在pccarddevs文件 的后面重复出现,名字也会用在驱动程序的匹配表中,因此保持它的短小 并且是有效的C标识符。还有一个给供货商的十六进制数字ID。不要添加 ``0xffffffff``或``0xffff``形式的ID, 因为它们是保留ID(前者是'空ID集合',而后者有时会在质量极其差的卡中 看到,用来指示none)。最后还有关于制卡公司的描述字符串。这个字符串 在FreeBSD中除了用于注释目的外并没有被使用过。 文件的第二节包含产品. 如你在下面例子中看到的: [.programlisting] .... /* Allied Telesis K.K. */ product ALLIEDTELESIS LA_PCM 0x0002 Allied Telesis LA-PCM /* Archos */ product ARCHOS ARC_ATAPI 0x0043 MiniCD .... 格式与供货商的那些行相似。其中有产品关键字。然后是供货商名字, 由上面重复而来。后面跟着产品名字,此名字在驱动程序中使用,且应当 是一个有效C标识符,但可以以数字开头。然后是卡的十六进制产品ID。 供货商通常对``0xffffffff``和 ``0xffff``有相同的约定。最后是关于设备自身的字符串 描述。由于FreeBSD的pccard总线驱动程序会从人可读的CIS条目创建一个 字符串,因此这个字符串在FreeBSD中通常不被使用,但某些CIS条目不能 满足要求的情况下还可能使用。产品按制造商的字母顺序排序,然后再按 产品ID的数字排序。每个制造商条目前有一条C注释,条目之间有一个空行。 第三节很象前面的供货商一节,但所由的制造商ID为 ``-1``。``-1``在FreeBSD pccard总线 代码中意味着"匹配发现的任何东西"。由于它们是C标识符, 它们的名字必须唯一。除此之外格式等同于文件的第一节。 最后一节包含那些必须用字符串匹配的卡。这一节的格式与通用 节的格式有点不同: [.programlisting] .... product ADDTRON AWP100 { "Addtron", "AWP-100spWirelessspPCMCIA", "Versionsp01.02", NULL } product ALLIEDTELESIS WR211PCM { "AlliedspTelesisspK.K.", "WR211PCM", NULL, NULL } Allied Telesis WR211PCM .... 我们已经熟悉了产品关键字,后跟供货商名字,然后再跟卡的名字, 就象在文件第二节中那样。然而,这之后就与那格式不同了。有一个 {}分组,后跟几个字符串。这些字符串对应CIS_INFO三元组中定义的 供货商,产品和额外信息。这些字符串被产生 [.filename]##pccarddevs.h##的程序过滤,将 sp替换为 实际的空格。空条目意味着条目的这部分应当被忽略。在我选择的例子中 有一个错误的条目。除非对卡的操作来说至关重要,否则不应当在其中 包含版本号。有时供货商在这个字段中会有卡的很多不同版本,这些版本 都能工作,这种情况下那些信息只会让那些拥有相似卡的人在FreeBSD中 更难以使用。有时当供货商出于市场考虑(可用性,价格等等),希望出售 同一品牌下的很多不同部分时,这也是有必要的。如果这样,则在那些 供货商仍然保持相同的制造商/产品对的少见情况下,能否区分开卡至关 重要. 此时不能使用正则表达式匹配。 [[pccard-probe]] === 探测例程样例 要懂得如何向所支持的设备列表中添加设备,就必须懂得很多驱动程序 都有的探测和/或匹配例程。由于也为老卡提供了一个兼容层,这在 FreeBSD 5.x中有一点复杂。由于只是window-dressing不同,这儿给出了 一个理想化的版本。 [.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-pp_name != NULL) device_set_desc(dev, pp-pp_name); return (0); } return (ENXIO); } .... 这儿我们有一个可以匹配少数几个设备的简单pccard探测例程。如上面 所提到,名字可能不同(如果不是 ``foo_pccard_probe()``则就是 ``foo_pccard_match()``)。函数 ``pccard_product_lookup()``是一个通用函数,它遍历 表并返回指向它所匹配的第一项的指针。一些驱动程序可能使用这个机制来 将某些卡的附加信息传递到驱动程序的其它部分,因此表中可能有些变体。 唯一的要求就是如果你有一个不同的表,则让表的结构的第一个元素为 结构pccard_product。 观察一下表``wi_pccard_products``就会发现, 所有条目都是 ``PCMCIA_CARD(_foo_, _bar_, _baz_)``的形式。 __foo__部分为来自 [.filename]##pccarddevs##的制造商ID。 __bar__部分为产品。 __baz__为此卡所期望的功能号。许多pccards 可以有多个功能,需要有办法区分开功能1和功能0。你可以看一下 ``PCMCIA_CARD_D``,它包括了来自 [.filename]##pccarddevs##文件的设备描述。你也可以看看 ``PCMCIA_CARD2``和 ``PCMCIA_CARD2_D``,当你需要按 "使用默认描述"和"从pccarddevs中取得" 做法,同时匹配CIS字符串和制造商号码时就会用到它们。 [[pccard-add]] === 将它合在一起 因此,为了一个增加新设备,必须进行下面步骤。首先,必须从设备 获得标识信息。完成这个最容易的方法就是将设备插入到PC Card或CF槽中, 并发出``devinfo -v``。你可能会看到一些类似下面的 东西: [.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 .... 作为输出的一部分。制造商和产品为产品的数字ID。而cisvender和 cisproduct为CIS中提供的描述本产品的字符串。 由于我们首先想优先使用数字选项,因此首先尝试创建基于此的条目。 为了示例,上面的卡已经被稍稍虚构化了。我们看到的供货商为BUFFALO, 它已经有一个条目了: [.programlisting] .... vendor BUFFALO 0x026f BUFFALO (Melco Corporation) .... 这样我们就可以了。为这个卡查找一个条目,但我们没有发现。但我们 发现: [.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 .... 我们就可以向[.filename]##pccarddevs##中添加: [.programlisting] .... product BUFFALO WLI2_CF_S11G 0x030c BUFFALO AirStation ultra 802.11b CF .... 目前,需要一个手动步骤来 重新产生[.filename]##pccarddevs.h##,用来将这些标识符转换 到客户驱动程序。你在驱动程序中使用它们之前必须完成下面步骤: [source,shell] .... # cd src/sys/dev/pccard # make -f Makefile.pccarddevs .... 一旦完成了这些步骤,你就可以向驱动程序中添加卡了。这只是一个 添加一行的简单操作: [.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 } }; .... 注意,我在我添加的行前面包含了``+``,但这只是 用来强调这一行。不要把它添加到实际驱动程序中。一旦你添加了这行,就 可以重新编译内核或模块,并试着看它是否能识别设备。如果它识别出设备 并能工作,请提交补丁。如果它不工作,请找出让它工作所需要的东西并 提交一个补丁。如果它根本不识别设备,那么你可能做错了什么,应当重新 检查每一步。 如果你是一个FreeBSD源代码的committer,并且所有东西看起来都 正常工作,则你应当把这些改变提交到树中。然而有些小技巧的东西你 需要考虑。首先,你必须提交[.filename]##pccarddevs##文件到 树中。完成后,你必须重新产生[.filename]##pccarddevs.h## 并将它作为另一次提交来提交(这是为了确保正确的 $FreeBSD$标签会留在后面的文件中)。最后,你需要把 其它东西提交到驱动程序。 [[pccard-pr]] === 提交新设备 很多人直接把新设备的条目发送给作者。请不要那样做。请将它们作为 PR来提交,并将PR号码发送给作者用于记录。这样确保条目不会丢失。提交 PR时,补丁中没有必要包含[.filename]##pccardevs.h##的diff, 因为那些东西可以重新产生。包含设备的描述和客户驱动程序的补丁是必要 的。如果你不知道名字,使用OEM99作为名字,作者将会调查后相应地调整 OEM99。提交者不应当提交OEM99,而应该找到最高的OEM条目并提交高于那个 的一个。 diff --git a/documentation/content/zh-cn/books/arch-handbook/pci/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/pci/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/pci/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/pci/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/scsi/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/scsi/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/scsi/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/scsi/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/smp/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/smp/_index.adoc similarity index 99% rename from documentation/content/zh-cn/books/arch-handbook/smp/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/smp/_index.adoc index 3fa37c019f..ce8fa9f980 100644 --- a/documentation/content/zh-cn/books/arch-handbook/smp/chapter.adoc +++ b/documentation/content/zh-cn/books/arch-handbook/smp/_index.adoc @@ -1,338 +1,338 @@ --- title: 第 8 章 SMPng 设计文档 prev: books/arch-handbook/vm -next: books/arch-handbook/driverbasics +next: books/arch-handbook/partii --- [[smp]] = SMPng 设计文档 :doctype: book :toc: macro :toclevels: 1 :icons: font :sectnums: :sectnumlevels: 6 :sectnumoffset: 8 :partnums: :source-highlighter: rouge :experimental: :images-path: books/arch-handbook/ ifdef::env-beastie[] ifdef::backend-html5[] :imagesdir: ../../../../images/{images-path} endif::[] ifndef::book[] include::shared/authors.adoc[] include::shared/mirrors.adoc[] include::shared/releases.adoc[] include::shared/attributes/attributes-{{% lang %}}.adoc[] include::shared/{{% lang %}}/teams.adoc[] include::shared/{{% lang %}}/mailing-lists.adoc[] include::shared/{{% lang %}}/urls.adoc[] toc::[] endif::[] ifdef::backend-pdf,backend-epub3[] include::../../../../../shared/asciidoctor.adoc[] endif::[] endif::[] ifndef::env-beastie[] toc::[] include::../../../../../shared/asciidoctor.adoc[] endif::[] [[smp-intro]] == 绪论 这份文档对目前 SMPng 架构的设计与实现进行了介绍。 它首先介绍了基本的原语和相关工具, 其后是关于 FreeBSD 内核的同步与执行模型, 接下来讨论了具体系统中的锁策略, 并描述了在各个子系统中引入细粒度的同步和实现并行化的步骤, 最后是详细的实现说明, 用以解释最初做出某些设计决策的动机, 并使读者了解使用特定的原语所可能产生的重大影响。 这份文档仍在撰写当中, 并将不断更新以反映与 SMPng 项目有关的最新设计与实现的情况。 其中有许多小节目前还只是提纲, 但我们会逐渐为其充实内容。 关于这份文档的更新和建议, 请发给文档编辑。 SMPng 的目标是使内核能够并发执行。 基本上, 内核是一个很大而复杂的程序。 要让内核能够多线程地执行, 我们需要使用某些其它多线程程序在实现时所用到的工具, 这包括互斥体(mutex)、 共享/排他锁(shared/exclusive lock)、 信号量(semaphores) 和条件变量(condition variable)。 如果希望了解它们以及其它 SMP 术语, 请参阅本文的 <> 一节。 [[smp-lock-fundamentals]] == 基本工具与上锁的基础知识 === 原子操作指令和内存栅 关于内存栅和原子操作指令已经有很多介绍材料, 因此这一节并不打算对其进行详尽的介绍。 简而言之, 如果有对某一变量上写锁, 就不能在不获得相应的锁时对其进行读取操作。 也就是说, 内存栅的作用在于保证内存操作的相对顺序, 但并不保证内存操作的严格时序。 换言之, 内存栅并不保证 CPU 将本地快取缓存或存储缓冲的内容刷写回内存, 而是在锁释放时确保其所保护的数据, 对于能看到刚释放的那个锁的 CPU 或设备可见。 持有内存栅的 CPU 可以在其快取缓存或存储缓冲中将数据保持其所希望的、 任意长的时间, 但如果其它 CPU 在同一数据元上执行原子操作, 则第一个 CPU 必须保证, 其所更新的数据值, 以及内存栅所要求的任何其它操作, 对第二个 CPU 可见。 例如, 假设在一简单模型中, 认为在主存 (或某一全局快取缓存) 中的数据是可见的, 当某一 CPU 上触发原子操作时, 其它 CPU 的存储缓冲和快取缓存就必须对同一快取缓存线上的全部写操作, 以及内存栅之后的全部未完成操作进行刷写。 这样一来, 在使用由原子操作保护的内存单元时就需要特别小心。 例如, 在实现 sleep mutex 时, 我们就必须使用 `atomic_cmpset` 而不是 `atomic_set` 来打开 MTX_CONTESTED 位。 这样做的原因是, 我们需要把 `mtx_lock` 的值读到某个变量, 并据此进行决策。 然而, 我们读到的值可能是过时的, 也可能在我们进行决策的过程中发生变化。 因此, 当执行 `atomic_set` 时, 最终可能会对另一值进行置位, 而不是我们进行决策的那一个。 这就必须通过 `atomic_cmpset` 来保证只有在我们的决策依据是最新的时, 才对相应的变量进行置位。 最后, 原子操作只允许一次更新或读一个内存单元。 需要原子地更新多个单元时, 就必须使用锁来代替它了。 例如, 如果需要更新两个相互关联的计数器时, 就必须使用锁, 而不是两次单独的原子操作了。 === 读锁与写锁 读锁并不需要像写锁那样强。 这两种类型的锁, 都需要确保通过它们访问的不是过时的数据。 然而, 只有写操作必须是排他的, 而多个线程则可以安全地读同一变量的值。 使用不同类型的锁用于读和写操作有许多各自不同的实现方式。 第一种方法是用 sx 锁, 它可以用于实现写时使用的排他锁, 而读时则作为共享锁。 这种方法十分简单明了。 第二种方法则略显晦涩。 可以用多个锁来保护同一数据元。 读时, 只需锁其中的一个读锁即可。 然而, 如果要写数据的话, 则需要首先上所有的写锁。 这会大大提高写操作的代价, 但当可能以多种方式访问数据时却可能非常有用。 例如, 父进程指针是同时受 `proctree_lock` sx 锁和进程 mutex 保护的。 在只希望检查已锁进程的父进程时, 用 proc 锁更为方便。 但是, 其它一些地方, 例如 `inferior` 这类需要通过父指针在进程树上进行搜索, 并对每个进程上锁的地方就不能这样做了, 否则, 将无法保证在对我们所获得的结果执行操作时, 之前检查时的状况依旧有效。 === 上锁状态和结果 如果您需要使用锁来保持所检查变量的状态, 并据此执行某些操作时, 是不能仅仅在读变量之前对其上锁, 并在执行操作之前解锁的。 过早解锁将使变量再次可变, 这可能会导致之前所做的决策失效。 因此, 在所做检测引发的动作结束之前, 必须继续保持上锁状态。 [[smp-design]] == 架构与设计概览 === 对中断的处理 与许多其它多线程 UNIX(R) 内核所采取的模式类似, FreeBSD 会赋予中断处理程序独立的线程上下文, 这样做能够让中断线程在遇到锁时阻塞。 但为了避免不必要的延迟, 中断线程在内核中, 是以实时线程的优先级运行的。 因此, 中断处理程序不应执行过久, 以免饿死其它内核线程。 此外, 由于多个处理程序可以分享同一中断线程, 中断处理程序不应休眠, 或使用可能导致休眠的锁, 以避免将其它中断处理程序饿死。 目前在 FreeBSD 中的中断线程是指重量级中断线程。 这样称呼它们的原因在于, 转到中断线程需要执行一次完整的上下文切换操作。 在最初的实现中, 内核不允许抢占, 因此中断在打断内核线程之前, 必须等待内核线程阻塞或返回用户态之后才能执行。 为了解决响应时间问题, FreeBSD 内核现在采用了抢占式调度策略。 目前, 只有释放休眠 mutex 或发生中断时才能抢断内核线程, 但最终目标是在 FreeBSD 上实现下面所描述的全抢占式调度策略。 并非所有的中断处理程序都在独立的线程上下文中执行。 相反, 某些处理程序会直接在主中断上下文中执行。 这些中断处理程序, 现在被错误地命名为 "快速" 中断处理程序, 因为早期版本的内核中使用了 INTR_FAST 标志来标记这些处理程序。 目前只有时钟中断和串口 I/O 设备中断采用这一类型。 由于这些处理程序没有独立的上下文, 因而它们都不能获得阻塞性锁, 因此也就只能使用自旋 mutex。 最后, 还有一种称为轻量级上下文切换的优化, 可以在 MD 代码中使用。 因为中断线程都是在内核上下文中执行的, 所以它可以借用任意进程的 vmspace (虚拟内存地址空间)。 因此, 在轻量级上下文切换中, 切换到中断线程并不切换对应的 vmspace, 而是借用被中断线程的 vmspace。 为确保被中断线程的 vmspace 不在中断处理过程中消失, 被中断线程在中断线程不再借用其 vmspace 之前是不允许执行的。 刚才提到的情况可能在中断线程阻塞或完成时发生。 如果中断线程发生阻塞, 则它再次进入可运行状态时将使用自己的上下文, 这样一来, 就可以释放被中断的线程了。 这种优化的坏处在于它们和硬件紧密相关, 而且实现比较复杂, 因此只有在这样做能带来大幅性能改善时才应采用。 目前这样说可能还为时过早, 而且事实上可能会反而导致性能下降, 因为几乎所有的中断处理程序都会立即被全局锁 (Giant) 阻塞, 而这种阻塞将进而需要线程修正。 另外, Mike Smith 提议采用另一种方式来处理中断线程: . 每个中断处理程序分为两部分, 一个在主中断上下文中运行的主体 (predicate) 和一个在自己的线程上下文中执行的处理程序 (handler)。 . 如果中断处理程序拥有主体, 则当触发中断时, 执行该主体。 如果主体返回真, 则认为该中断被处理完毕, 内核从中断返回。 如果主体返回假, 或者中断没有主体, 则调度运行线程式处理程序。 在这一模式中适当地采用轻量级上下文切换可能是非常复杂的。 因为我们可能会希望在未来改变这一模式, 因此现在最好的方案, 应该是暂时推迟在轻量级上下文切换之上的工作, 以便进一步完善中断处理架构, 随后再考察轻量级上下文切换是否适用。 === 内核抢占与临界区 ==== 内核抢占简介 内核抢占的概念很简单, 其基本思想是 CPU 总应执行优先级最高的工作。 当然, 至少在理想情况下是这样。 有些时候, 达成这一理想的代价会十分高昂, 以至于在这些情况下抢占会得不偿失。 实现完全的内核抢占十分简单: 在调度将要执行的线程并放入运行队列时, 检查它的优先级是否高于目前正在执行的线程。 如果是这样的话, 执行一次上下文切换并立即开始执行该线程。 尽管锁能够在抢占时保护多数数据, 但内核并不是可以安全地处处抢占的。 例如, 如果持有自旋 mutex 的线程被抢占, 而新线程也尝试获得同一自旋 mutex, 新线程就可能一直自旋下去, 因为被中断的线程可能永远没有机会运行了。 此外, 某些代码, 例如在 Alpha 上的 `exec` 对进程地址空间编号进行赋值的代码也不能被抢断, 因为它被用来支持实际的上下文切换操作。 在这些代码段中, 会通过使用临界区来临时禁用抢占。 ==== 临界区 临界区 API 的责任是避免在临界区内发生上下文切换。 对于完全抢占式内核而言, 除了当前线程之外的其它线程的每个 `setrunqueue` 都是抢断点。 `critical_enter` 的一种实现方式是设置一线程私有标记, 并由其对应方清除。 如果调用 `setrunqueue` 时设置了这个标志, 则无论新线程和当前线程相比其优先级高低, 都不会发生抢占。 然而, 由于临界区会在自旋 mutex 中用于避免上下文切换, 而且能够同时获得多个自旋 mutex, 因此临界区 API 必须支持嵌套。 由于这个原因, 目前的实现中采用了嵌套计数, 而不仅仅是单个的线程标志。 为了尽可能缩短响应时间, 在临界区中的抢占被推迟, 而不是直接丢弃。 如果线程应被抢断, 并被置为可运行, 而当前线程处于临界区, 则会设置一线程私有标志, 表示有一个尚未进行的抢断操作。 当最外层临界区退出时, 会检查这一标志, 如果它被置位, 则当前线程会被抢断, 以允许更高优先级的线程开始运行。 中断会引发一个和自旋 mutex 有关的问题。 如果低级中断处理程序需要锁, 它就不能中断任何需要该锁的代码, 以避免可能发生的损坏数据结构的情况。 目前,这一机制是透过临界区 API 以 `cpu_critical_enter` 和 `cpu_critical_exit` 函数的形式实现的。 目前这一 API 会在所有 FreeBSD 所支持的平台上禁用和重新启用中断。 这种方法并不是最优的, 但它更易理解, 也更容易正确地实现。 理论上, 这一辅助 API 只需要配合在主中断上下文中的自旋 mutex 使用。 然而, 为了让代码更为简单, 它被用在了全部自旋 mutex, 甚至包括所有临界区上。 将其从 MI API 中剥离出来放入 MD API, 并只在需要使用它的 MI API 的自旋 mutex 实现中使用可能会有更好的效果。 如果我们最终采用了这种实现方式, 则 MD API 可能需要改名, 以彰显其为一单独 API 这一事实。 ==== 设计折衷 如前面提到的, 当完全抢占并非总能提供最佳性能时, 采取了一些折衷的措施。 第一处折衷是, 抢占代码并不考虑其它 CPU 的存在。 假设我们有两个 CPU, A 和 B, 其中 A 上线程的优先级为 4, 而 B 上线程的优先级是 2。 如果 CPU B 令一优先级为 1 的线程进入可运行状态, 则理论上, 我们希望 CPU A 切换至这一新线程, 这样就有两个优先级最高的线程在运行了。 然而, 确定哪个 CPU 在抢占时更合适, 并通过 IPI 向那个 CPU 发出信号, 并完成相关的同步工作的代价十分高昂。 因此, 目前的代码会强制 CPU B 切换至更高优先级的线程。 请注意这样做仍会让系统进入更好的状态, 因为 CPU B 会去执行优先级为 1 而不是 2 的那个线程。 第二处折衷是限制对于实时优先级的内核线程的立即抢占。 在前面所定义的抢占操作的简单情形中, 低优先级总会被立即抢断 (或在其退出临界区后被抢断)。 然而, 许多在内核中执行的线程, 有很多只会执行很短的时间就会阻塞或返回用户态。 因此, 如果内核抢断这些线程并执行其它非实时的内核线程, 则内核可能会在这些线程马上要休眠或执行完毕之前切换出去。 这样一来, CPU 就必须调整快取缓存以配合新线程的执行。 当内核返回到被抢断的线程时, 它又需要重新填充之前丢失的快取缓存信息。 此外, 如果内核能够将对将阻塞或返回用户态的那个线程的抢断延迟到这之后的话, 还能够免去两次额外的上下文切换。 因此, 默认情况下, 只有在优先级较高的线程是实时线程时, 抢占代码才会立即执行抢断操作。 启用针对所有内核线程的完全抢占对于调试非常有帮助, 因为它会暴露出更多的竞态条件 (race conditions)。 在难以模拟这些竞态条件的单处理器系统中, 这显得尤其有用。 因此, 我们提供了内核选项 `FULL_PREEMPTION` 来启用针对所有内核线程的抢占, 这一选项主要用于调试目的。 === 线程迁移 简单地说, 线程从一个 CPU 移动到另一个上的过程称作迁移。 在非抢占式内核中, 这只会在明确定义的点, 例如调用 `msleep` 或返回至用户态时才会发生。 但是, 在抢占式内核中, 中断可能会在任何时候强制抢断, 并导致迁移。 对于 CPU 私有的数据而言这可能会带来一些负面影响, 因为除 `curthread` 和 `curpcb` 以外的数据都可能在迁移过程中发生变化。 由于存在潜在的线程迁移, 使得未受保护的 CPU 私有数据访问变得无用。 这就需要在某些代码段禁止迁移, 以获得稳定的 CPU 私有数据。 目前我们采用临界区来避免迁移, 因为它们能够阻止上下文切换。 但是, 这有时可能是一种过于严厉的限制, 因为临界区实际上会阻止当前处理器上的中断线程。 因而, 提供了另一个 API, 用以指示当前进程在被抢断时, 不应迁移到另一 CPU。 这组 API 也叫线程牵制, 它由调度器提供。 这组 API 包括两个函数: `sched_pin` 和 `sched_unpin`。 这两个函数用于管理线程私有的计数 `td_pinned`。 如果嵌套计数大于零, 则线程将被锁住, 而线程开始运行时其嵌套计数为零, 表示处于未牵制状态。 所有的调度器实现中, 都要求保证牵制线程只在它们首次调用 `sched_pin` 时所在的 CPU 上运行。 由于只有线程自己会写嵌套计数, 而只有其它线程在受牵制线程没有执行, 且持有 `sched_lock` 锁时才会读嵌套计数, 因此访问 `td_pinned` 不必上锁。 `sched_pin` 函数会使嵌套计数递增, 而 `sched_unpin` 则使其递减。 注意, 这些函数只操作当前线程, 并将其绑定到其执行它时所处的 CPU 上。 要将任意线程绑定到指定的 CPU 上, 则应使用 `sched_bind` 和 `sched_unbind`。 === 调出 (Callout) 内核机制 `timeout` 允许内核服务注册函数, 以作为 `softclock` 软件中断的一部分来执行。 事件将基于所希望的时钟嘀嗒的数目进行, 并在大约指定的时间回调用户提供的函数。 未决 timeout (超时) 事件的全局表是由一全局 mutex, `callout_lock` 保护的; 所有对 timeout 表的访问, 都必须首先拿到这个 mutex。 当 `softclock` 唤醒时, 它会扫描未决超时表, 并找出应启动的那些。 为避免锁逆序, `softclock` 线程会在调用所提供的 `timeout` 回调函数时首先释放 `callout_lock` mutex。 如果在注册时没有设置 CALLOUT_MPSAFE 标志, 则在调用调出函数之前, 还会抓取全局锁, 并在之后释放。 其后, `callout_lock` mutex 会在继续处理前再次获得。 `softclock` 代码在释放这个 mutex 时会非常小心地保持表的一致状态。 如果启用了 DIAGNOSTIC, 则每个函数的执行时间会被记录, 如果超过了某一阈值, 则会产生警告。 [[smp-lock-strategies]] == 特定数据的锁策略 === 凭据 `struct ucred` 是内核内部的凭据结构体, 它通常作为内核中以进程为导向的访问控制的依据。 BSD-派生的系统采用一种 "写时复制" 的模型来处理凭据数据: 同一凭据结构体可能存在多个引用, 如果需要对其进行修改, 则这个结构体将被复制、 修改, 然后替换该引用。 由于在打开时用于实现访问控制的凭据快取缓存广泛存在, 这种做法会极大地节省内存。 在迁移到细粒度的 SMP 时, 这一模型也省去了大量的锁操作, 因为只有未共享的凭据才能实施修改, 因而避免了在使用共享凭据时额外的同步操作。 凭据结构体只有一个引用时, 被认为是可变的; 不允许改变共享的凭据结构体, 否则将可能导致发生竞态条件。 `cr_mtxp` mutex 用于保护 `struct ucred` 的引用计数, 以维护其一致性。 使用凭据结构体时, 必须在使用过程中保持有效的引用, 否则它就可能在这个不合理的消费者使用过程中被释放。 `struct ucred` mutex 是一种叶 mutex, 出于性能考虑, 它通过 mutex 池实现。 由于多用于访问控制决策, 凭据通常情况下是以只读方式访问的, 此时一般应使用 `td_ucred`, 因为它不需要上锁。 当更新进程凭据时, 检查和更新过程中必须持有 `proc` 锁。 检查和更新操作必须使用 `p_ucred`, 以避免检查时和使用时的竞态条件。 如果所调系统调用将在更新进程凭据之后进行访问控制检查, 则 `td_ucred` 也必须刷新为当前进程的值。 这样做能够避免修改后使用过时的凭据。 内核会自动在进程进入内核时, 将线程结构体的 `td_ucred` 指针刷新为进程的 `p_ucred`, 以保证内核访问控制能用到新的凭据。 === 文件描述符和文件描述符表 详细内容将在稍后增加。 === Jail 结构体 `struct prison` 保存了用于维护那些通过 man:jail[2] API 创建的 jail 所用到的管理信息。 这包括 jail 的主机名、 IP 地址, 以及一些相关的设置。 这个结构体包含引用计数, 因为指向这一结构体实例的指针会在多种凭据结构之间共享。 用了一个 mutex, `pr_mtx` 来保护对引用计数以及所有 jail 结构体中可变变量的读写访问。 有一些变量只会在创建 jail 的时刻发生变化, 只需持有有效的 `struct prison` 就可以开始读这些值了。 关于每个项目具体的上锁操作的文档, 可以在 [.filename]#sys/jail.h# 的注释中找到。 === MAC 框架 TrustedBSD MAC 框架会以 `struct label` 的形式维护一系列内核对象的数据。 一般来说, 内核中的 label (标签) 是由与其对应的内核对象同样的锁保护的。 例如, `struct vnode` 上的 `v_label` 标签是由其所在 vnode 上的 vnode 锁保护的。 除了嵌入到标准内核对象中的标签之外, MAC 框架也需要维护一组包含已注册的和激活策略的列表。 策略表和忙计数由一个全局 mutex (`mac_policy_list_lock`) 保护。 由于能够同时并行地进行许多访问控制检查, 对策略表的只读访问, 在增减忙计数时, 框架的入口处需要首先持有这个 mutex。 MAC 入口操作的过程中并不需要长时间持有此 mutex -- 有些操作, 例如文件系统对象上的标签操作 -- 是持久的。 要修改策略表, 例如在注册和解除注册策略时, 需要持有此 mutex, 而且要求引用计数为零, 以避免在用表时对其进行修改。 对于需要等待表进入闲置状态的线程, 提供了一个条件变量 `mac_policy_list_not_busy`, 但这一条件变量只能在调用者没有持有其它锁时才能使用, 否则可能会引发锁逆序问题。 忙计数在整个框架中事实上还扮演了某种形式的 共享/排他 锁的作用: 与 sx 锁不同的地方在于, 等待列表进入闲置状态的线程可以饿死, 而不是允许忙计数和其它在 MAC 框架入口 (或内部) 的锁之间的逆序情况。 === 模块 对于模块子系统, 用于保护共享数据使用了一个单独的锁, 它是一个 共享/排他 (SX) 锁, 许多情况需要获得它 (以共享或排他的方式), 因此我们提供了几个方便使用的宏来简化对这个锁的访问, 这些宏可以在 [.filename]#sys/module.h# 中找到, 其用法都非常简单明了。 这个锁保护的主要是 `module_t` (当以共享方式上锁) 和全局的 `modulelist_t` 这两个结构体, 以及模块。 要更进一步理解这些锁策略, 需要仔细阅读 [.filename]#kern/kern_module.c# 的源代码。 === Newbus 设备树 newbus 系统使用了一个 sx 锁。 读的一方应持有共享 (读) 锁 (man:sx_slock[9]) 而写的一方则应持有排他 (写) 锁 (man:sx_xlock[9])。 内部函数一般不需要进行上锁, 而外部可见的则应根据需要上锁。 有些项目不需上锁, 因为这些项目在全程是只读的, (例如 man:device_get_softc[9]), 因而并不会产生竞态条件。 针对 newbus 数据结构的修改相对而言非常少, 因此单个的锁已经足够使用, 而不致造成性能折损。 === 管道 ... === 进程和线程 - 进程层次结构 - proc 锁及其参考 - 在系统调用过程中线程私有的 proc 项副本, 包括 td_ucred - 进程间操作 - 进程组和会话 === 调度器 本文在其它地方已经提供了很多关于 `sched_lock` 的参考和注释。 === Select 和 Poll `select` 和 `poll` 这两个函数允许线程阻塞并等待文件描述符上的事件 -- 最常见的情况是文件描述符是否可读或可写。 .. === SIGIO SIGIO 服务允许进程请求在特定文件描述符的读/写状态发生变化时, 将 SIGIO 信号群发给其进程组。 任意给定内核对象上, 只允许一进程或进程组注册 SIGIO, 这个进程或进程组称为属主 (owner)。 每一支持 SIGIO 注册的对象, 都包含一指针字段, 如果对象未注册则为 NULL, 否则是一指向描述这一注册的 `struct sigio` 的指针。 这一字段由一全局 mutex, `sigio_lock` 保护。 调用 SIGIO 维护函数时, 必须以 "传引用" 方式传递这一字段, 以确保本地注册副本的中这个字段不脱离锁的保护。 每个关联到进程或进程组的注册对象, 都会分配一 `struct sigio` 结构, 并包括指回该对象的指针、 属主、 信号信息、 凭据, 以及关于这一注册的一般信息。 每个进程或进程组都包含一个已注册 `struct sigio` 结构体的列表, 对进程来说是 `p_sigiolst`, 而对进程组则是 `pg_sigiolst`。 这些表由相应的进程或进程组锁保护。 除了用以将 `struct sigio` 连接到进程组上的 `sio_pgsigio` 字段之外, 在 `struct sigio` 中的多数字段在注册过程中都是不变量。 一般而言, 开发人员在实现新的支持 SIGIO 的内核对象时, 会希望避免在调用 SIGIO 支持函数, 例如 `fsetown` 或 `funsetown` 持有结构体锁, 以免去需要在结构体锁和全局 SIGIO 锁之间定义锁序。 通常可以通过提高结构体上的引用计数来达到这样的目的, 例如, 在进行管道操作时, 使用引用某个管道的文件描述符这样的操作, 就可以照此办理。 === Sysctl `sysctl` MIB 服务会从内核内部, 以及用户态的应用程序以系统调用的方式触发。 这会引发至少两个和锁有关的问题: 其一是对维持命名空间的数据结构的保护, 其二是与那些通过 sysctl 接口访问的内核变量和函数之间的交互。 由于 sysctl 允许直接导出 (甚至修改) 内核统计数据以及配置参数, sysctl 机制必须知道这些变量相应的上锁语义。 目前, sysctl 使用一个全局 sx 锁来实现对 `sysctl` 操作的串行化; 然而, 这些是假定用全局锁保护的, 并且没有提供其它保护机制。 这一节的其余部分将详细介绍上锁和 sysctl 相关变动的语义。 - 需要将 sysctl 更新值所进行的操作的顺序, 从原先的读旧值、 copyin 和 copyout、 写新值, 改为 copyin、 上锁、 读旧值、 写新值、 解锁、 copyout。 一般的 sysctl 只是 copyout 旧值并设置它们 copyin 所得到的新值, 仍然可以采用旧式的模型。 然而, 对所有 sysctl 处理程序采用第二种模型并避免锁操作方面, 第二种方式可能更规矩一些。 - 对于通常的情况, sysctl 可以内嵌一个 mutex 指针到 SYSCTL_FOO 宏和结构体中。 这对多数 sysctl 都是有效的。 对于使用 sx 锁、 自旋 mutex, 或其它除单一休眠 mutex 之外的锁策略, 可以用 SYSCTL_PROC 节点来完成正确的上锁。 === 任务队列 (Taskqueue) 任务队列 (taskqueue) 的接口包括两个与之关联的用于保护相关数据的锁。 `taskqueue_queues_mutex` 是用于保护 `taskqueue_queues` TAILQ 的锁。 与这个系统关联的另一个 mutex 锁是位于 `struct taskqueue` 结构体上。 在此处使用同步原语的目的在于保护 `struct taskqueue` 中数据的完整性。 应注意的是, 并没有单独的、 帮助用户对其自身的工作进行锁的细化用的宏, 因为这些锁基本上不会在 [.filename]#kern/subr_taskqueue.c# 以外的地方用到。 [[smp-implementation-notes]] == 实现说明 === 休眠队列 休眠队列是一种用于保存同处一个等待通道 (wait channel) 上休眠线程列表的数据结构。 在等待通道上, 每个处于非睡眠状态的线程都会携带一个休眠队列结构。 当线程在等待通道上发生阻塞时, 它会将休眠队列结构体送给那个等待通道。 与等待通道关联的休眠队列则保存在一个散列表中。 休眠队列散列表中保存了包含至少一个阻塞线程的等待通道上的休眠队列。 这个散列表上的项称作 sleepqueue (休眠队列) 链。 它包含了一个休眠队列的链表, 以及一个自旋 mutex。 此处的自旋 mutex 用于保护休眠队列表, 以及其上休眠队列结构的内容。 一个等待通道上只会关联一个休眠队列。 如果有多个线程在同一等待通道上阻塞, 则休眠队列中将关联除第一个线程之外的全部线程。 当从休眠队列中删除线程时, 如果它不是唯一的阻塞的休眠线程, 则会获得主休眠队列的空闲表上的休眠队列结构。 最后一个线程会在恢复运行时获得主休眠队列。 由于线程有可能以和加入休眠队列不同的次序从其中删除, 因此, 线程离开队列时可能会携带与其进入时不同的休眠队列。 `sleepq_lock` 函数会锁住指定等待通道上休眠队列链的自旋 mutex。 `sleepq_lookup` 函数会在主休眠队列散列表中查找给定的等待通道。 如果没有找到主休眠队列, 它会返回 NULL。 `sleepq_release` 函数会对给定等待通道所关联的自旋 mutex 进行解锁。 将线程加入休眠队列是通过 `sleepq_add` 来完成的。 这个函数的参数包括等待通道、 指向保护等待通道的 mutex 的指针、 等待消息描述串, 以及一个标志掩码。 调用此函数之前, 应通过 `sleepq_lock` 为休眠队列链上锁。 如果等待通道不是通过 mutex 保护的 (或者它由全局锁保护), 则应将 mutex 指针设置为 NULL。 而 flags (标志) 参数则包括了一个类型字段, 用以表示线程即将加入到的休眠队列的类型, 以及休眠是否是可中断的 (SLEEPQ_INTERRUPTIBLE)。 目前只有两种类型的休眠队列: 通过 `msleep` 和 `wakeup` 函数管理的传统休眠队列 (SLEEPQ_MSLEEP), 以及基于条件变量的休眠队列 (SLEEPQ_CONDVAR)。 休眠队列类型和锁指针这两个参数完全是用于内部的断言检查。 调用 `sleepq_add` 的代码, 应明示地在关联的 sleepqueue 链透过 `sleepq_lock` 进行上锁之后, 并使用等待函数在休眠队列上阻塞之前解锁所有用于保护等待通道的 interlock。 通过使用 `sleepq_set_timeout` 可以为休眠设置超时。 这个函数的参数包括等待通道, 以及以相对时钟嘀嗒数为单位的超时时间。 如果休眠应被某个到来的信号打断, 则还应调用 `sleepq_catch_signals` 函数, 这个函数唯一的参数就是等待通道。 如果此线程已经有未决信号, 则 `sleepq_catch_signals` 将返回信号编号; 其它情况下, 其返回值则是 0。 一旦将线程加入到休眠队列中, 就可以使用 `sleepq_wait` 函数族之一将其阻塞了。 目前总共提供了四个等待函数, 使用哪个取决于调用这是否希望允许使用超时、 收到信号, 或用户态线程调度器打断休眠状态。 其中, `sleepq_wait` 函数简单地等待, 直到当前线程通过某个唤醒 (wakeup) 函数显式地恢复运行; `sleepq_timedwait` 函数则等待, 直到当前线程被显式地唤醒, 或者达到早前使用 `sleepq_set_timeout` 设置的超时; `sleepq_wait_sig` 函数会等待显式地唤醒, 或者其休眠被中断; 而 `sleepq_timedwait_sig` 函数则等待显式地唤醒、 达到用 `sleepq_set_timeout` 设置的超时, 或线程的休眠被中断这三种条件之一。 所有这些等待函数的第一个参数都是等待通道。 除此之外, `sleepq_timedwait_sig` 的第二个参数是一个布尔值, 表示之前调用 `sleepq_catch_signals` 时是否有发现未决信号。 如果线程被显式地恢复运行, 或其休眠被信号终止, 则等待函数会返回零, 表示休眠成功。 如果线程的休眠被超时或用户态线程调度器打断, 则会返回相应的 errno 数值。 需要注意的是, 因为 `sleepq_wait` 只能返回 0, 因此调用者不能指望它返回什么有用信息, 而应假定它完成了一次成功的休眠。 同时, 如果线程的休眠时间超时, 并同时被终止, 则 `sleepq_timedwait_sig` 将返回一个表示发生超时的错误代码。 如果返回错误代码是 0 而且使用 `sleepq_wait_sig` 或 `sleepq_timedwait_sig` 来执行阻塞, 则应调用 `sleepq_calc_signal_retval` 来检查是否有未决信号, 并据此选择合适的返回值。 较早前调用 `sleepq_catch_signals` 得到的信号编号, 应作为参数传给 `sleepq_calc_signal_retval`。 在同一休眠通道上休眠的线程, 可以由 `sleepq_broadcast` 或 `sleepq_signal` 函数来显式地唤醒。 这两个函数的参数均包括希望唤醒的等待通道、 将唤醒线程的优先级 (priority) 提高到多少, 以及一个标志 (flags) 参数表示将要恢复运行的休眠队列类型。 优先级参数将作为最低优先级, 如果将恢复的线程的优先级比此参数更高 (数值更低) 则其优先级不会调整。 标志参数主要用于函数内部的断言, 用以确认休眠队列没有被当做错误的类型对待。 例如, 条件变量函数不应恢复传统休眠队列的执行。 `sleepq_broadcast` 函数将恢复所有指定休眠通道上的阻塞线程, 而 `sleepq_signal` 则只恢复在等待通道上优先级最高的阻塞线程。 在调用这些函数之前, 应首先使用 `sleepq_lock` 对休眠队列上锁。 休眠线程也可以通过调用 `sleepq_abort` 函数来中断其休眠状态。 这个函数只有在持有 `sched_lock` 时才能调用, 而且线程必须处于休眠队列之上。 线程也可以通过使用 `sleepq_remove` 函数从指定的休眠队列中删除。 这个函数包括两个参数, 即休眠通道和线程, 它只在线程处于指定休眠通道的休眠队列之上时才将其唤醒。 如果线程不在那个休眠队列之上, 或同时处于另一等待通道的休眠队列上, 则这个函数将什么都不做而直接返回。 === 十字转门 (turnstile) - 与休眠队列的比较和不同。 - 查询/等待/释放 (lookup/wait/release) - 介绍 TDF_TSNOBLOCK 竞态条件。 - 优先级传播。 === 关于 mutex 实现的一些细节 - 我们是否应要求 mtx_destroy() 持有 mutex, 因为无法安全地断言它们没有被其它对象持有? ==== 自旋 mutex - 使用一临界区... ==== 休眠 mutex - 描述 mutex 冲突时的竞态条件 - 为何在持有十字转门链锁时, 可以安全地读冲突 mutex 的 mtx_lock。 === Witness - 它能做什么 - 它如何工作 [[smp-misc]] == 其它话题 === 中断源和 ICU 抽象 - struct isrc - pic 驱动 === 其它问题/话题 - 是否应将 interlock 传给 `sema_wait`? - 是否应提供非休眠式 sx 锁? - 增加一些关于正确使用引用计数的介绍。 :sectnums!: [glossary] [[smp-glossary]] == 术语表 [.glosslist] 原子:: 当遵循适当的访问协议时, 如果一操作的效果对其它所有 CPU 均可见, 则称其为原子操作。 狭义的原子操作是机器直接提供的。 就更高的抽象层次而言, 如果结构体的多个成员由一个锁保护, 则如果对它们的操作都是在上锁后、 解锁前进行的, 也可以称其为原子操作。 阻塞:: 线程等待锁、 资源或条件时被阻塞。 这一术语也因此被赋予了太多的意涵。 临界区:: 不允许发生抢占的代码段。 使用 man:critical_enter[9] API 来表示进入和退出临界区。 MD:: 表示与机器/平台有关。 内存操作:: 内存操作包括读或写内存中的指定位置。 MI:: 表示与机器/平台无关。 操作:: 主中断上下文:: 主中断上下文表示当发生中断时所执行的那段代码。 这些代码可以直接运行某个中断处理程序, 或调度一异步终端线程, 以便为给定的中断源执行中断处理程序。 实时内核线程:: 一种高优先级的内核线程。 目前, 只有中断线程属于实时优先级的内核线程。 休眠:: 当进程由条件变量或通过 msleep 或 tsleep 阻塞并进入休眠队列时, 称其进入休眠状态。 可休眠锁:: 可休眠锁是一种在进程休眠时仍可持有的锁。 锁管理器 (lockmgr) 锁和 sx 锁是目前 FreeBSD 中仅有的可休眠锁。 最终, 某些 sx 锁, 例如 allproc (全部进程) 和 proctree (进程树) 锁将成为不可休眠锁。 线程:: 由 struct thread 所表达的内核线程。 线程可以持有锁, 并拥有独立的执行上下文。 等待通道:: 线程可以在其上休眠的内核虚拟地址。 diff --git a/documentation/content/zh-cn/books/arch-handbook/sound/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/sound/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/sound/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/sound/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/sysinit/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/sysinit/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/sysinit/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/sysinit/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/usb/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/usb/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/usb/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/usb/_index.adoc diff --git a/documentation/content/zh-cn/books/arch-handbook/vm/chapter.adoc b/documentation/content/zh-cn/books/arch-handbook/vm/_index.adoc similarity index 100% rename from documentation/content/zh-cn/books/arch-handbook/vm/chapter.adoc rename to documentation/content/zh-cn/books/arch-handbook/vm/_index.adoc