diff options
author | Xin LI <delphij@FreeBSD.org> | 2013-11-14 00:20:51 +0000 |
---|---|---|
committer | Xin LI <delphij@FreeBSD.org> | 2013-11-14 00:20:51 +0000 |
commit | 797a990cf709e43fdcfa0f1b0b8b8e0127076002 (patch) | |
tree | 38ebab2823385023d1fa54018fceb14262c4cf93 | |
parent | 8ff93b9827e6bfbbae625ff0d45eb2ce00ebe029 (diff) | |
download | doc-797a990cf709e43fdcfa0f1b0b8b8e0127076002.tar.gz doc-797a990cf709e43fdcfa0f1b0b8b8e0127076002.zip |
Convert zh_CN from GB2312 to UTF-8. While I'm there, also
fix a bunch of font issues when rendering PDF.
Notes
Notes:
svn path=/head/; revision=43188
518 files changed, 189841 insertions, 189858 deletions
@@ -27,7 +27,7 @@ SUBDIR+= pt_BR.ISO8859-1 SUBDIR+= ru_RU.KOI8-R SUBDIR+= sr_YU.ISO8859-2 SUBDIR+= tr_TR.ISO8859-9 -SUBDIR+= zh_CN.GB2312 +SUBDIR+= zh_CN.UTF-8 SUBDIR+= zh_TW.Big5 .endif diff --git a/en_US.ISO8859-1/books/faq/book.xml b/en_US.ISO8859-1/books/faq/book.xml index 8d3833d4cc..30b8a816fd 100644 --- a/en_US.ISO8859-1/books/faq/book.xml +++ b/en_US.ISO8859-1/books/faq/book.xml @@ -729,9 +729,9 @@ </row> <row> - <entry><literal>zh_CN.GB2312</literal></entry> + <entry><literal>zh_CN.UTF-8</literal></entry> - <entry>Simplified Chinese (China, GB2312 + <entry>Simplified Chinese (China, UTF-8 encoding)</entry> </row> diff --git a/en_US.ISO8859-1/htdocs/Makefile b/en_US.ISO8859-1/htdocs/Makefile index 4266835fd6..85ec9a80b7 100644 --- a/en_US.ISO8859-1/htdocs/Makefile +++ b/en_US.ISO8859-1/htdocs/Makefile @@ -102,7 +102,7 @@ WEB_LANG= da_DK.ISO8859-1 \ nl_NL.ISO8859-1 \ pt_BR.ISO8859-1 \ ru_RU.KOI8-R \ - zh_CN.GB2312 \ + zh_CN.UTF-8 \ zh_TW.Big5 .endif .if !defined(ENGLISH_ONLY) || empty(ENGLISH_ONLY) diff --git a/zh_CN.GB2312/articles/contributing/article.xml b/zh_CN.GB2312/articles/contributing/article.xml deleted file mode 100644 index 2388fa7ff5..0000000000 --- a/zh_CN.GB2312/articles/contributing/article.xml +++ /dev/null @@ -1,488 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - Original Revision: 1.511 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>为 FreeBSD 提供帮助</title> - - - <abstract> - <para>无论是作为个人还是组织机构,如果您希望为FreeBSD项目提供帮助, - 都可以在本文中找到合适的方法。</para> - </abstract> - - <authorgroup> - <author><personname><firstname>Jordan</firstname><surname>Hubbard</surname></personname><contrib>原著</contrib></author> - </authorgroup> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.ieee; - &tm-attrib.general; - </legalnotice> - - <pubdate>$FreeBSD$</pubdate> - - <releaseinfo>$FreeBSD$</releaseinfo> - - </info> - - <indexterm><primary>贡献 </primary></indexterm> - - <para>您希望给 FreeBSD 项目做点什么吗? 太好了, 我们欢迎您。 FreeBSD 正是 - <emphasis>依靠</emphasis> 广大用户的贡献才得以发展壮大的。 - 我们不仅非常感谢您所做的贡献,而且,这些工作对于 FreeBSD 的持续发展也至关重要。</para> - - <para>也许与您想象的不同, 您既不必是一名出色的程序员, 也无须和 - FreeBSD 核心团队成员有很好的私交, 我们会一视同仁的对待您的工作。 - FreeBSD 的开发人员遍布全球, 大家技术专长各异, 年龄分布也非常广泛。 - 每天, 我们都在面对持续增长的工作而苦于没有足够的人手, - 因此我们随时欢迎您的帮助。</para> - - <para>FreeBSD 项目处理的是一个完整的操作系统环境, - 而不只是一个内核或是一些零散的工具包。 因此, 我们的 - <filename>TODO</filename> 任务列表里包含各种各样的工作, - 从文档、用户测试、演示, 到系统安装程序和高度专业的内核开发。 - 因此无论您的技术水平如何, 从事何种领域, 都可以帮助这个项目。</para> - - <para>我们鼓励从事和 FreeBSD 相关工作的企业和我们联系。 - 您需要一些特殊的扩展来使您的产品运转起来么? - 您会发现我们很乐意答应您的请求, 除非是特别稀奇古怪的。 - 您是否正从事相关的增值业务? 让我们来帮助您吧, - 我们也许可以在其中的某些方面相互协作。 - 自由软件世界正在努力打破旧有的关于软件开发、 销售和维护的框框, - 我们希望恳请您至少能给它一次机会。</para> - - <sect1 xml:id="contrib-what"> - <title>我们的需求</title> - - <para>下面列出了一些需要完成的任务和子项目, - 它们基本上可以被等同于 <filename>TODO</filename>(任务列表) - 列表, 以及用户的要求。</para> - - <sect2 xml:id="non-programmer-tasks"> - <title>正在进行中的非开发任务</title> - - <para>很多参加FreeBSD项目的人不是程序员。 - 这个项目里有文档撰写者、 网页设计师、 以及技术支持人员。 - 对于这些志愿者来说, 他们只需要贡献一些时间, - 并且具有学习的意愿。</para> - - <orderedlist> - <listitem> - <para>您可以经常通读FAQ和手册, 如果您发现了繁琐的解释, - 或者是过时的知识, 甚至完全不正确的地方, 都请告诉我们。 - 如果您能顺手把他们改过来那就更好了 - (SGML其实并不难学, 但我们也不反对您直接提交 - ASCII 的版本)。</para> - </listitem> - - <listitem> - <para>帮助我们把 FreeBSD 文档翻译成您的母语。 - 如果您的母语版本已经存在了, - 您也可以翻译一些其他的文档或者检查那些已有的文档是否是最新更新过的。 - 您可以先简单看看 FreeBSD 文档计划中有关 <link xlink:href="&url.books.fdp-primer;/translations.html">翻译的常见问题</link>。 - 参加翻译工作并不是说您要孤军奋战翻译所有的 FreeBSD 文档。 - 作为一个志愿者, 做多少工作完全取决于您的意愿。 一旦某个人开始翻译了, - 其他人几乎一定会参与到这些工作中来。 - 如果您只有有限的时间或者精力去翻译部分文档, - 您可以首先去翻译安装指南。</para> - </listitem> - - <listitem> - <para>阅读 &a.questions; 并偶尔看一看 &ng.misc; - (甚至有规律地这样做)。 与别人分享您的专业知识, - 并帮助他们解决问题是一件令人愉悦的事情; - 有些时候您甚至可以在这个过程中学到一些新东西! - 这些论坛有时也会为您提供一些有价值的主意。</para> - </listitem> - </orderedlist> - </sect2> - - <sect2 xml:id="ongoing-programmer-tasks"> - <title>正在进行的开发任务</title> - <para>列在这里的大部分任务都需要您投入可观的时间, - 或者需要您在 FreeBSD 内核方面有丰富的知识, 或者两者都要。 - 当然这里也有很多重要的任务也许您一个 - <quote>周末开发人员</quote> 就可以干完。</para> - - <orderedlist> - <listitem> - <para>如果您正在运行 FreeBSD-CURRENT 版本并且有一条高速的 - Internet接入线路, 您可以访问 <systemitem class="fqdomainname">current.FreeBSD.org</systemitem>, - 这里每天会有一个新版本 — 如果您有空, - 您可以隔三岔五地下载一份并且安装它, - 其间如果出了什么问题,请告诉我们。</para> - </listitem> - - <listitem> - <para>阅读 &a.bugs;。 您可能会为这些问题提供具有建设性意义的评论, - 或者帮忙测试一些补丁。 此外, - 您甚至可以尝试修正其中的一些问题。</para> - </listitem> - - <listitem> - <para>如果您知道有一些修正已经在 -CURRENT 上成功地进行, - 但在经过一段时间之后仍然没有合并到 -STABLE - (通常是 2周左右), 给相关的 committer 发一封礼貌的提示信。</para> - </listitem> - - <listitem> - <para>将第三方软件加入到源代码中的 - <filename>src/contrib</filename> 目录。</para> - </listitem> - - <listitem> - <para>确保 <filename>src/contrib</filename> - 中的代码是最新的</para> - </listitem> - - <listitem> - <para>以更高的警告级别构建源代码 (或一部分源代码) - 并清理这些警告。</para> - </listitem> - - <listitem> - <para>更新那些在 ports 中使用过时的东西, - 例如 <function>gets()</function> 或包含 - <filename>malloc.h</filename> 所产生的警告。</para> - </listitem> - - <listitem> - <para>如果您制作了 ports, 并进行了一些针对 - &os; 的改动, 将您的补丁发回给原作者 - (这样下次升级时您的工作会变得轻松一些)。</para> - </listitem> - - <listitem> - <para>获取一份正式的标准, 如 &posix; 的副本。 - 您可以在 <link xlink:href="&url.base;/projects/c99/index.html">FreeBSD - C99 & POSIX 标准顺应项目</link> 网站上得到相关的链接。 - 将 FreeBSD 的行为同标准进行比较。 如果与标准不同, - 特别是那些细节地方的微小差异, 请发送一个关于它的 PR (问题报告)。 - 如果可能, 请指出如何修正它, 并随 PR 提交补丁。 - 如果您认为标准有问题, - 请向标准化团体要求对其进行重新的考虑。</para> - </listitem> - - <listitem> - <para>为这份列表建议更多的内容!</para> - </listitem> - </orderedlist> - </sect2> - - <sect2> - <title>查看整个 PR 数据库</title> - - <indexterm><primary>问题报告数据库</primary></indexterm> - - <para><link xlink:href="http://www.FreeBSD.org/cgi/query-pr-summary.cgi">FreeBSD - PR 列表</link> 展示了所有当前处于活跃状态的问题报告, 以及由 - FreeBSD 用户提交的改进建议。 PR - 数据库同时包括了开发人员和非开发人员的任务。 - 查看那些尚未解决的 PR, 并看看是否有您感兴趣的任务。 - 这其中可能有一些是非常简单的问题, - 只需要看一看并确认 PR 是正确的。 另外一些可能会非常复杂, - 或者完全没有包括任何修正。</para> - - <para>首先看一看那些还没有人接手的 PR。 - 如果 PR 已经分配给了其它人, 但看起来是您能够处理的, - 您可以给那个人发信, 并询问您是否可以提供帮助 — - 他们可能已经有了可供测试的补丁, 或有一些可供讨论的意见。</para> - </sect2> - - <sect2> - <title>从 <quote>点子</quote> 网页上认领项目</title> - - <para><link xlink:href="&url.base;/projects/ideas/">&os; - 志愿者项目和点子清单</link> 也是提供给愿意为 - &os; 项目做出贡献的人们的。 - 这张清单一直在被定期更新着, - 包含了对程序员和非程序员有用的每个项目的信息。</para> - </sect2> - </sect1> - - <sect1 xml:id="contrib-how"> - <title>如何提供帮助</title> - - <para>帮助改进系统基本上可以分为 5 类:</para> - - <sect2 xml:id="contrib-general"> - <title>错误报告和一般的注解</title> - - <para>通常, <emphasis>一般意义上的</emphasis> - 技术想法和建议应该发到 &a.hackers;。 - 同样地, 对于这些东西有兴趣的人 (当然, - 他们同时还要能够容忍 <emphasis>大量的</emphasis> 邮件!) - 可以考虑订阅 &a.hackers;。 - 参见 <link xlink:href="&url.books.handbook;/eresources.html#ERESOURCES-MAIL">FreeBSD - 使用手册</link> 以了解关于这个邮件列表, - 以及其它邮件列表的详细情况。</para> - - <para>如果您发现了 bug 或者想要提交某些修改, - 请通过 &man.send-pr.1; 程序或使用 - <link xlink:href="&url.base;/send-pr.html">基于 WEB - 的提交页面</link> 来提交。 请试着填写 bug 报告的每一项。 - 一般来说, 我们建议在 bug 报告中直接附上补丁, 除非它超过了 65KB。 - 如果补丁可以直接应用到源代码上, 则建议您在报告的 - synopsis 一栏写上 <literal>[PATCH]</literal>。 - 在附带补丁时, 请 <emphasis>不要</emphasis> - 通过复制和粘贴来进行, 因为这样做会把 tab 变成空格, - 结果补丁很可能就不能用了。 如果补丁超过 20KB 很多, - 应考虑将其压缩 (例如使用 &man.gzip.1; 或 &man.bzip2.1;) - 之后用 &man.uuencode.1; 进行编码之后再放进您的问题报告中。</para> - - <para>一旦报告被存档, 您会收到一封确认邮件以及一个事件追踪编号。 - 请保留这个编号, 因为您可以在之后使用这个编号, - 发邮件到 &a.bugfollowup; - 来提供关于该事件的进一步信息。 您需要做的是将编号放到邮件的标题中, - 例如 <literal>"Re: - kern/3377"</literal>。 - 关于同一问题更进一步的情况应该通过这种方式来提交。</para> - - <para>如果您在一段时间之后仍然没有收到确认信 (超过 3 - 天甚至 1 周, 这取决于您的邮件服务) - 或者由于某种原因无法使用 &man.send-pr.1; 命令, - 则可以发信给 &a.bugs; 要求别人代您发送它。</para> - - <para>请参见 <link xlink:href="&url.articles.problem-reports;/article.html">这篇文章</link> - 了解如何撰写好的问题报告。</para> - </sect2> - - <sect2> - <title>对于文档的修订</title> - - <indexterm><primary>提交文档</primary></indexterm> - - <para>对于文档的修改由 &a.doc; 来审查。 - 请参见 <link xlink:href="&url.books.fdp-primer;/index.html">FreeBSD - 文档计划初级读本</link> 来获得完整的指导。 - 请按照 <xref linkend="contrib-general"/> 中介绍的方法使用 &man.send-pr.1; - 来发送新的文档或者对于现有文档的完善 (哪怕是很小的改进也是欢迎的!)。</para> - </sect2> - - <sect2> - <title>对于现有源代码的修改</title> - - <indexterm><primary>FreeBSD-CURRENT</primary></indexterm> - - <para>在现有代码上进行修改或增加功能在某种程度上是需要更多技巧的事情, - 并且还和您对于目前 FreeBSD 的开发现状的了解有关。 - 有多种方式可以得到被称作 <quote>FreeBSD-CURRENT</quote> - 的 FreeBSD 开发版本, 您可以通过它来了解最近的开发情况。 - 请参见 <link xlink:href=" &url.books.handbook;/current-stable.html">FreeBSD - 使用手册</link> 来了解使用 FreeBSD-CURRENT 的进一步详情。</para> - - <para>在旧的代码上进行修改, 则通常可能由于代码已经过时, - 或与新的开发版本差异太大而无法被重新集成到 FreeBSD 中。 - 如果您订阅了 &a.announce; 以及 &a.current; 邮件列表, - 则可以通过它们来大体了解目前的开发状态。</para> - - <para>假如说您能够基于尽可能新的代码来完成您的修改, - 则下一步要做的事情就是生成您所进行的修改的差异文件, - 并将它发给 FreeBSD 的维护人员。 这项工作可以通过 &man.diff.1; - 命令来完成。</para> - - <para>提交补丁时推荐的 &man.diff.1; 格式是一致差异 (unified diff), - 它可以通过 <command>diff - -u</command> 来生成。 不过, 如果您修改了大量的代码, - 则使用 <command>diff -c</command> 来生成的上下文格式 (context diff) - 的差异可能更容易阅读, 因而推荐使用。</para> - - <indexterm> - <primary><command>diff</command></primary> - </indexterm> - - <para>例如:</para> - - <screen>&prompt.user; <userinput>diff -c oldfile newfile</userinput></screen> - - <para>或者</para> - - <screen>&prompt.user; <userinput>diff -c -r olddir newdir</userinput></screen> - - <para>将分别生成给定文件或目录结构的 context diff。</para> - - <para>类似地,</para> - - <screen>&prompt.user; <userinput>diff -u oldfile newfile</userinput></screen> - - <para>或</para> - - <screen>&prompt.user; <userinput>diff -u -r olddir newdir</userinput></screen> - - <para>的作用与前面的类似, 但采用的格式是 unified diff。</para> - - <para>请参见 &man.diff.1; 联机手册了解更多细节。</para> - - <para>一旦您使用 &man.diff.1; 生成了差异集 (可以使用 - &man.patch.1; 命令来测试一下), 就可以提交它们, - 以便被 FreeBSD 收录。 通过使用 <xref linkend="contrib-general"/> - 中所介绍的 &man.send-pr.1; 程序就可以完成这项工作。 - <emphasis>不要</emphasis> 只是把差异集发到 &a.hackers;, - 否则它们可能会被丢掉! 我们会非常感激您提交的修改 - (这是一个志愿者项目!); 因为我们都很忙, - 因此有时不一定能够立即修正问题, 但 PR - 数据库将一直保持着这些记录, - 因此只要有人有了时间它们就能被改正了。 - 如果您的问题报告中包括补丁, 一定不要忘了在标题上用 - <literal>[PATCH]</literal> 来强调一下。</para> - - <indexterm> - <primary><command>uuencode</command></primary> - </indexterm> - - <para>如果您认为合适 (例如您添加、 删除或重命名了文件), - 还可以考虑使用 - <command>tar</command> 来将文件打包, 然后用 &man.uuencode.1; - 来编码。 我们也欢迎用 &man.shar.1; 创建的包。</para> - - <para>如果您的修改可能存在潜在的争议, 例如, - 您不确定与之相关的版权问题, 或者感觉需要经过更严格的复审才可以发布它们, - 则应直接发给 &a.core;, 而不是通过 &man.send-pr.1; 来发送。 - &a.core; 是一个小组, 其成员更多的从事 FreeBSD 的日常工作。 - 需要注意的是, 这个小组也因此 <emphasis>很忙</emphasis>, - 因此只有在非常必要的时候才应给他们写信。</para> - - <para>请参考 &man.intro.9; 和 &man.style.9; 以了解关于编码习惯和约定的详情。 - 如果您了解这些约定, 则对我们来说将是极大的帮助。</para> - </sect2> - - <sect2> - <title>新代码或重要的增值软件包</title> - - <para>如果您打算提供规模较大的代码, 或者为 FreeBSD 增加重要的新功能, - 则可能必须将它们通过 uuencode 进行编码, 或传到某个 Web 或 - FTP 站点, 以便更多的人能够得到它。 如果您没有这样的服务器, - 请到相关的 FreeBSD 邮件列表提出, 看看是否有人愿意帮您放置它们。</para> - - <para>对于大量的代码而言, 关于版权的问题肯定会被提出。 - FreeBSD 基本系统中能够使用的版权声明包括:</para> - - <orderedlist> - <listitem> - <para>BSD<indexterm><primary>BSD 版权声明</primary></indexterm> 版权。 我们倾向于使用这类授权的代码, - 因为它 <quote>不附加多余的条件</quote>, 因而更能够吸引商业企业使用。 - FreeBSD 并不反对商业公司使用它的代码, 相反, - 我们积极地鼓励商业公司使用我们的代码, - 当然, 如果它们最终把一部分代码重新捐赠给 FreeBSD - 就更好了。</para> - </listitem> - - <listitem> - <para>GNU General Public License,<indexterm><primary>GPL</primary><see>GNU General Public License</see></indexterm><indexterm><primary>GNU General Public License</primary></indexterm> 或简称 <quote>GPL</quote>。 - 我们并不很欢迎使用这样授权的代码, - 因为商业公司使用它需要做更多的工作。 不过, 由于很多使用 - GPL 授权的代码目前是无法避免的 (编译器、 汇编器, - 文本排版程序等等), 拒绝使用所有采用这样授权的软件是很不明智的。 - 采用 GPL 授权的代码会被放到源代码的一些专门的位置, 例如 - <filename>/sys/gnu</filename> 或 - <filename>/usr/src/gnu</filename>, - 以方便那些使用 GPL 代码可能会给他们带来问题的人识别。</para> - </listitem> - </orderedlist> - - <para>使用其它授权的代码在进入 FreeBSD 之前必须经过慎重的复审和考虑。 - 采用包含严厉限制的商业授权的代码, 一般来说会被拒绝, - 但我们鼓励这些代码的作者通过自己的渠道来发布它们。</para> - - <para>要在您的成果上加入 <quote>BSD式</quote> 的版权, - 请把下列文本放到每一个源文件的最开始部分, - 并用适当的文字替换 <literal>%%</literal> 之间的文字。</para> - - <programlisting>Copyright (c) %%proper_years_here%% - %%your_name_here%%, %%your_state%% %%your_zip%%. - All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer as - the first lines of this file unmodified. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY %%your_name_here%% ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL %%your_name_here%% BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - $Id$</programlisting> - - <para>为了方便您的使用, 在 - <filename>/usr/share/examples/etc/bsd-style-copyright</filename> - 也可以找到此授权的副本。</para> - </sect2> - - <sect2> - <title>资金、 硬件或 Internet 接入</title> - - <para>我们非常愿意接受各种形式的捐赠, 以进一步拓展 FreeBSD - Project 的事业, 因为有您的支持, - 像我们这样的志愿者努力才能够有更大的成就! - 捐赠硬件也非常重要, 因为这样能够帮助我们增加可以支持的硬件种类, - 而我们中的很多人并没有足够的资金来购置这些硬件。</para> - - <sect3 xml:id="donations"> - <title>捐款</title> - - <para>FreeBSD 基金会是一个非营利的、 有课税豁免权的基金会, - 建立这个基金会的目标是为了让 FreeBSD Project 能够达成更加长远的目标。 - 作为 501(c)3 实体, 一般而言基金会可以免予上缴美国联邦收入税, - 以及科罗拉多州收入税。 通常对于课税豁免的实体进行捐赠, - 可以折抵联邦收入中应课税部分的金额。</para> - - <para>您可以把支票寄往: - <address> - The FreeBSD Foundation - <street>7321 Brockway Dr.</street> - <city>Boulder</city>, <state>CO</state> <postcode>80303</postcode> - <country>USA</country> - </address> - </para> - - <para>FreeBSD 基金会现在可以通过 PayPal 从网上接受捐款。 - 如果您想向基金会捐款, 请访问它的 <link xlink:href="http://www.freebsdfoundation.org">web - 站点</link>。</para> - - <para>关于 FreeBSD 基金会的更多详情, 可以在 <link xlink:href="http://people.FreeBSD.org/~jdp/foundation/announcement.html">FreeBSD - 基金会 -- 介绍</link> 找到。 要联络基金会, - 请发送电子邮件到 - <email>bod@FreeBSDFoundation.org</email>。</para> - </sect3> - - <sect3> - <title>捐赠硬件</title> - <indexterm><primary>捐赠</primary></indexterm> - - <para>FreeBSD 计划欢迎任何人捐赠可以使用的硬件。 - 如果您有兴趣捐赠硬件, 请联系 <link xlink:href="&url.base;/donations/">捐赠联络人办公室</link>。</para> - </sect3> - - <sect3> - <title>捐赠 Internet 接入</title> - - <para>我们欢迎新的 FTP、 WWW 或 - <command>cvsup</command> 镜像。 如果您希望成为这样的镜像, - 请参见 <link xlink:href="&url.articles.hubs;/index.html">如何架设 FreeBSD 镜像</link> - 一文, 以了解进一步的情况。</para> - </sect3> - </sect2> - </sect1> - - <index/> - -</article> diff --git a/zh_CN.GB2312/articles/cvs-freebsd/article.xml b/zh_CN.GB2312/articles/cvs-freebsd/article.xml deleted file mode 100644 index 5d25306bb5..0000000000 --- a/zh_CN.GB2312/articles/cvs-freebsd/article.xml +++ /dev/null @@ -1,588 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - From FreeBSD: doc/zh_TW.Big5/articles/cvs-freebsd/article.xml,v 1.2 2006/01/31 01:30:59 vanilla - Original Revision: 1.17 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>建立 CVS 储存库 (repository) - 以 FreeBSD 的方式</title> - - - <author><personname><firstname>Stijn</firstname><surname>Hoop</surname></personname><affiliation> - <address><email>stijn@win.tue.nl</email></address> - </affiliation></author> - - <pubdate>$FreeBSD$</pubdate> - - <copyright> - <year>2001</year> - <year>2002</year> - <year>2003</year> - <holder role="mailto:stijn@win.tue.nl">Stijn Hoop</holder> - </copyright> - - <releaseinfo>$FreeBSD$</releaseinfo> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.general; - </legalnotice> - - <abstract> - <para>这份文件描述了使用和 FreeBSD 项目相同的命令脚本来建立 CVS 储存库的步骤。 - 这和标准 CVS 建立的储存库相较之下有许多优点, - 它提供了更多对于源代码树的细粒度访问控制, - 并能够为每一次的提交生成和发出易读的电子邮件。</para> - </abstract> - </info> - - <sect1 xml:id="introduction"> - <title>简介</title> - - <para>大多数的开放源代码软件项目都使用 <application>CVS</application> - 作为它们的源代码控制系统。 尽管 <application>CVS</application> 有许多的优点, - 但它也有部份的瑕疵和缺点。 - 其中之一的原因是和其它的开发者分享源代码树可能会迅速成为系统管理的恶梦, - 特别是当有人希望保护部份的源代码树免受于一般的存取时。</para> - - <para>FreeBSD 是众多使用 <application>CVS</application> 的项目之一, - 因为基于它进行开发的开发人员遍布于全世界。 - 他们撰写了一些命令脚本使得管理储存库变得更加容易。 - 最近这些命令脚本由 &a.joe; 重新整理过且更标准化, - 使得在其它的项目上再次使用这些命令脚本会更加容易。 - 本文件将描述使用这些新的命令脚本的方法。</para> - - <para>为了使本文件中的信息对您有用, 您需要首先熟悉 <application>CVS</application> - 的基本操作方式。</para> - </sect1> - - <sect1 xml:id="first-setup"> - <title>基本配置</title> - - <warning> - <para>最好的方式是在一个全新的储存库中执行这些步骤, - 并确定你了解所有的后果。 同时, 请确定你有最新且可读的资料备份!</para> - </warning> - - <sect2> - <title>初始化储存库</title> - - <para>首先要做的是建立一个新的储存库, 执行下列命令告诉 - <application>CVS</application> 建立并初始化:</para> - - <screen>&prompt.user; <userinput>cvs -d path-to-repository init</userinput></screen> - - <para>这命令告诉 <application>CVS</application> 建立 - <filename>CVSROOT</filename> 的目录, - 这个目录里放置了所有的配置文件。</para> - </sect2> - - <sect2> - <title>配置储存库的用户组</title> - - <para>现在我们将建立一个拥有该储存库的用户组, - 所有的开发者必须加入这个用户组, 这样他们才能够存取该储存库。 - 我们假设用户组名称是以 FreeBSD 内部所采用的 - <literal>ncvs</literal>。</para> - - <screen>&prompt.root; <userinput>pw groupadd ncvs</userinput></screen> - - <para>接着你需要使用 &man.chown.8; 将目录所有者指定给刚刚新增的用户组:</para> - - <screen>&prompt.root; <userinput>chown -R :ncvs path-to-your-repository</userinput></screen> - - <para>如此一来, 没有适当的用户组许可的用户, 就不再能够写入该储存库。</para> - </sect2> - - <sect2> - <title>取回源文件</title> - - <para>现在你需要从 FreeBSD 储存库中取回 <filename>CVSROOT</filename> 目录, - 从 FreeBSD 匿名的 CVS 镜像站来取回会是最简单的方法。请查阅 <link xlink:href="&url.books.handbook;/anoncvs.html">在 使用手册 中的相关章节</link> - 来获得更多信息。 我们假设取回的文件存放在相同目录下的 - <filename>CVSROOT-freebsd</filename> 目录中。</para> - </sect2> - - <sect2> - <title>复制 FreeBSD 的命令脚本</title> - - <para>接下来我们要复制 FreeBSD <filename>CVSROOT</filename> - 里的文件到你的储存库中。 如果你熟悉 <application>CVS</application>, - 你也许会想你可以直接汇入 (import) 这些命令脚本, - 从而更容易地在未来有新版时进行版本同步; 不过,事实是 - <application>CVS</application> 在这个部份有缺点: 当汇入文件到 - <filename>CVSROOT</filename> 时, 它并不会更新配置文件。 - 为了要认出这些文件, 你还需要在汇入它们后一一重新提交, 这就失去了 - <literal>cvs import</literal> 的价值。 因此, 推荐的方法是直接将这些命令脚本复制过去。</para> - - <para>如果您不了解这些操作也没有关系 — 因为最后的结果都是一样的。 - 首先汇出 (checkout) 你的 <filename>CVSROOT</filename>, - 然后复制刚刚取回的 FreeBSD 文件到本地的目录中(尚未变动过):</para> - - <screen>&prompt.user; <userinput>cvs -d path-to-your-repository checkout CVSROOT</userinput> -&prompt.user; <userinput>cd CVSROOT</userinput> -&prompt.user; <userinput>cp ../CVSROOT-freebsd/* .</userinput> -&prompt.user; <userinput>cvs add *</userinput></screen> - - <para>注意: 你很可能会得到一段关于某些目录没有被复制的警告,这是正常的, - 你并不需要用到这些目录。</para> - </sect2> - - <sect2> - <title>命令脚本说明</title> - - <para>现在你的工作目录中有了完整 FreeBSD 项目在他们的储存库中使用的命令脚本的副本, - 以下是每个文件简单的介绍。</para> - - <itemizedlist> - <listitem> - <para><filename>access</filename> - 此文件在预设的安装中没有被用到。 - 它是在 <link linkend="freebsdspecific">FreeBSD 的专用配置</link> - 中用来控制储存库的存取的。 如果你不希望使用这个配置的话, - 则可以删除这个文件。</para> - </listitem> - - <listitem> - <para><filename>avail</filename> - 此文件控制储存库的存取。 - 在此文件中你可以指定允许存取储存库的用户组, - 也可以针对目录或文件来拒绝提交。 - 你应该调整为在你的储存库中将包含的用户组和目录。</para> - </listitem> - - <listitem> - <para><filename>cfg.pm</filename> - 此文件说明了配置内容,并提供预设的配置。 - 你 <emphasis>不</emphasis> 应修改此文件, 而应将修改的配置放到 - <filename>cfg_local.pm</filename>。</para> - </listitem> - - <listitem> - <para><filename>cfg_local.pm</filename> - 此文件包含所有的系统配置值。 - 你应该配置所有列在此的配置, 例如提交的邮件要寄到哪、 - 在哪些主机上的使用者可以提交等等。 更多的相关信息在稍后会提到。</para> - </listitem> - - <listitem> - <para><filename>checkoutlist</filename> - 此文件列出所有在 - <application>CVS</application> 控制下此目录中的文件, 除了标准在 - <literal>cvs init</literal> 建立出的文件。 你可以删除某些不需要的 - FreeBSD 专用的文件。</para> - </listitem> - - <listitem> - <para><filename>commit_prep.pl</filename> - 此命令脚本执行各种提交前的检查, - 这些检查是否启用, 取决于您在 <filename>cfg_local.pm</filename> - 中所进行的配置。 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>commitcheck</filename> - 此命令脚本会直接影响 - <application>CVS</application>。 首先它会使用 - <filename>cvs_acls.pl</filename> 来检查提交者是否可以存取指定的源代码树, - 然后执行 <filename>commit_prep.pl</filename> 来确认各种提交前的检查。 - 如果一切正常,<application>CVS</application> 将允许此次提交继续执行。 - 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>commitinfo</filename> - 此文件是 - <application>CVS</application> 用来定义在提交前所要执行的程序 — - 在此例中是 <filename>commitcheck</filename>。你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>config</filename> - 储存库的配置选项。 - 你可以修改为你想要的, 但大多数的管理者可能会保留默认值。 - 更多关于可以在此配置的选项信息可以查阅 <application>CVS</application> 手册。</para> - </listitem> - - <listitem> - <para><filename>cvs_acls.pl</filename> - 此命令脚本定义提交者的身分, - 以及他/她是否允许存取源代码树, 这些判断基于 <filename>avail</filename> - 中的配置。 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>cvsignore</filename> - 此文件列出哪些文件 - <application>CVS</application> 不用处理到储存库中, - 你可以修改成你想要的。 更多关于可以此文件的说明可以查阅 <application>CVS</application> - 手册。</para> - </listitem> - - <listitem> - <para><filename>cvswrappers</filename> - 此文件是 - <application>CVS</application> 用来启用或停用关键词展开, - 或者是否应将文件视为二进制文件。 你可以修改成你想要的。 - 更多关于可以此文件的说明可以查阅 <application>CVS</application> 手册。 - 注意 <literal>-t</literal> 和 <literal>-f</literal> 选项在 - <application>CVS</application> 客户端/服务器 并不能够正确地运作。</para> - </listitem> - - <listitem> - <para><filename>edithook</filename> - 此文件已经不再使用了, - 仅为历史原因保留。 你可以安全地删除此文件。</para> - </listitem> - - <listitem> - <para><filename>editinfo</filename> - <application>CVS</application> - 使用这个文件来强迫你使用特定的编辑器。 FreeBSD 没有使用这个功能, - 而对输入的日志信息的检查, 则由 <filename>verifymsg</filename> 和 - <filename>logcheck</filename> 来完成。 这是因为 - <filename>editinfo</filename> 功能在从远程提交或是使用 - <literal>-m</literal> 或 <literal>-F</literal> 选项时不会执行。 - 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>exclude</filename> - 此文件列出被 - <filename>commit_prep.pl</filename> 定义不能包含修正版标头的文件。 - 在 FreeBSD 版本的配置中, 所有在修正版控制下的文件需有一个修正版标头, - (类似 $FreeBSD$ 这样)。 - 此文件逐行列出不应进行检查的文件名字。 - 你可以在此文件中为不需要修正版标头的文件新增一个正则表达式。 - 为了安装这些命令脚本, 最好的方法是将 - <filename>CVSROOT/</filename> 从标头检查中排除。</para> - </listitem> - - <listitem> - <para><filename>log_accum.pl</filename> - 此命令脚本会处理由 - <filename>logcheck</filename> 所提供的日志信息, - 并且将之为备份目的附加于储存库中的记录文件。 - 同时也执行要将邮件寄到你提供的信箱中的程序 (在 <filename>cfg_local.pm</filename> 中)。 - 它和 <application>CVS</application> 之间是由 <filename>loginfo</filename> - 负责沟通。 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>logcheck</filename> - 此文件分析提交者提供的日志信息, - 并试图对其作清理动作。 它和 <application>CVS</application> - 之间是由 <filename>verifymsg</filename> 负责沟通。 你不应更动此文件。</para> - - <note><para>此命令脚本依赖于经过 FreeBSD 修改的 <application>CVS</application>: - FreeBSD 版本在此命令脚本修改过后才读取日志信息; 标准的 - <application>CVS</application> 版本虽然能够检查语法上是否正确, - 但并不会清理日志信息。<application>CVS</application> 1.11.2 可以通过在 - <filename>config</filename> 配置 <literal>RereadLogAfterVerify=always</literal> - 来和 FreeBSD 版本有相同的作用。</para></note> - </listitem> - - <listitem> - <para><filename>loginfo</filename> - 此文件是 - <application>CVS</application> 用来控制日志信息要寄到哪里,而 - <filename>log_accum.pl</filename> 负责处理。你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>modules</filename> - 此文件保留了 - <application>CVS</application> 原始的意义。 你应该删除新增的 FreeBSD - 模块, 并修改为你想要的内容。 更多关于可以此文件的说明可以查阅 - <application>CVS</application> 手册。</para> - </listitem> - - <listitem> - <para><filename>notify</filename> - 此文件为 - <application>CVS</application> 用来控制监看某个文件。 - 在 FreeBSD 的储存库中没有用到此文件, - 你可以修改成你想要的。 更多关于可以此文件的说明, - 可以查阅 <application>CVS</application> 手册。</para> - </listitem> - - <listitem> - <para><filename>options</filename> - 此文件仅限使用于 FreeBSD 和 Debian - 的 <application>CVS</application> 版本。 - 它包含了需要在修正版标头中展开的关键词。 - 你可以修改为符合你在 <filename>cfg_local.pm</filename> 中指定的关键词。</para> - </listitem> - - <listitem> - <para><filename>rcsinfo</filename> - 此文件定义提交时, - 储存库所要使用的日志信息样式模板, 如 <filename>rcstemplate</filename>。 - FreeBSD 预设为所有的储存库使用同一个样式模板, - 你可以加入其它你想要的。</para> - </listitem> - - <listitem> - <para><filename>rcstemplate</filename> - 此文件是提交者在提交时会看到的日志信息样式模板, - 你应该修改为你在 <filename>cfg_local.pm</filename> - 中定义的各种参数。</para> - </listitem> - - <listitem> - <para><filename>tagcheck</filename> - 此文件控制在储存库中贴上标签的存取。 - 标准的 FreeBSD 版本拒绝名为 RELENG* 的标签,因为这是 交付工程组 的工作。 - 你可以根据需要来修改此文件。</para> - </listitem> - - <listitem> - <para><filename>taginfo</filename> - 此文件控制执行在储存库中贴上标签的存取的命令脚本, - 如 <filename>tagcheck</filename>。你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>unwrap</filename> - 此命令脚本可以用来在汇出时自动 <quote>解开</quote> - 二进制文件 (请见 <filename>cvswrappers</filename>)。 目前 FreeBSD - 并没有使用此配置, 因为此功能在远程提交时执行的并不十分完善。 - 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>verifymsg</filename> - 此文件用来执行和日志信息相关的命令脚本, - 如 <filename>logcheck</filename>。你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>wrap</filename> - 此命令脚本可以用来在提交时自动 <quote>包裹</quote> - 二进制文件 (请见 <filename>cvswrappers</filename>)。 目前 FreeBSD - 并没有使用此配置, 因为此功能在远程提交时执行的并不十分完善。 - 你不应更动此文件。</para> - </listitem> - </itemizedlist> - </sect2> - - <sect2> - <title>定制命令脚本</title> - - <para>接下来的步骤要配置这些命令脚本使得它们可以在你的环境中运作。 - 你应该检查所有在目录中的文件, 并修改为符合你的配置。 - 尤其, 你会想要修改下列的文件:</para> - - <procedure> - <step> - <para>如果你不希望使用 <link linkend="freebsdspecific">FreeBSD 的专用配置</link>, - 你可以安全地删除 <filename>access</filename>:</para> - - <screen>&prompt.user; <userinput>cvs rm -f access</userinput></screen> - </step> - - <step> - <para>编辑 <filename>avail</filename> 来包含你想控制存取的各种储存库目录, - 请确定你有保留 <literal>avail||CVSROOT</literal> 这一行, - 否则你将会在下一步把你自己锁在外面。</para> - - <para>另外你可以在此文件中新增开发者的用户组,FreeBSD 预设使用 - <filename>access</filename> 来列出所有的开发者, - 但你可以使用任何你想要用的文件。 如果你想的话也可以新增用户组 (请使用指定在 - <filename>cvs_acls.pl</filename> 前面所介绍的语法)。</para> - </step> - - <step> - <para>编辑 <filename>cfg_local.pm</filename> 来包含你需要的选项。 - 你应该特别检视一下下列的配置项目:</para> - - <itemizedlist> - <listitem> - <para><literal>%TEMPLATE_HEADERS</literal> - 这是用来取得日志信息内容的程序, - 并加入将呈现的邮件项目和提供非空值的信息。 - 你可以删除 <literal>PR</literal> 和 <literal>MFC after</literal> - 叙述,当然也可以加入你想要的。</para> - </listitem> - - <listitem> - <para><literal>$MAIL_BRANCH_HDR</literal> - 如果你想要在每一封提交的邮件中加入描述是在哪一个分支中提交的标头, - 那么请定义为符合你的配置。 如果你不想使用这样的标头, 那么请配置为空值。</para> - </listitem> - - <listitem> - <para><literal>@COMMIT_HOSTS</literal> - 定义使用者能够提交的主机。</para> - </listitem> - - <listitem> - <para><literal>$MAILADDRS</literal> - 配置应该收到提交邮件的邮件地址。</para> - </listitem> - - <listitem> - <para><literal>@LOG_FILE_MAP</literal> - 以你所需要的来修改这个数组, - 每个配置值应该符合被提交的目录,而提交的日志信息会以 - <filename>commitlogs</filename> 的名称储存在每个被配置的目录下。</para> - </listitem> - - <listitem> - <para><literal>$COMMITCHECK_EXTRA</literal> - 如果你不想使用 - <link linkend="freebsdspecific">FreeBSD 专用的存取控制</link> - 功能, 你可以在此文件中删除对 <literal>$COMMITCHECK_EXTRA</literal> - 的定义。</para> - </listitem> - </itemizedlist> - - <note><para>修改 <literal>$IDHEADER</literal> 的功能只有在 FreeBSD - 平台上可以运作, 它依赖于 FreeBSD 专用的 <application>CVS</application> - 配置。</para></note> - - <para>你可以检查 <filename>cfg.pm</filename> 是否有其它的参数可以修改, - 但是修改最好是有原因的。</para> - </step> - - <step> - <para>删除 <filename>exclude</filename> 中关于FreeBSD 的专用配置的叙述 - (如以 <literal>^ports/</literal> 为开头的每一行等)。 此外, - 注释掉以 <literal>^CVSROOT/</literal> 为开头的行列, 然后新增一行只有 - <literal>^CVSROOT/</literal>。 等到关键词展开的命令脚本安装好后, - 你可以在 <filename>CVSROOT</filename> 目录中的文件里加上标头, - 然后再恢复刚刚注释的行列, 但在你还没有提交前则只保持这样。</para> - </step> - - <step> - <para>编辑 <filename>modules</filename>, 并删除所有 FreeBSD 的模块。 - 加入你需要的模块。</para> - </step> - - <step> - <note><para>此步骤只有在你于 <filename>cfg_local.pm</filename> 中指定了 - <literal>$IDHEADER</literal> 才有必要配置 (只有在 FreeBSD 专用的 - <application>CVS</application> 配置上才能够执行)。</para></note> - - <para>编辑 <filename>options</filename> 以符合你在 - <filename>cfg_local.pm</filename> 中配置的标签名称。 - 在所有的文件中搜寻 <literal>FreeBSD</literal> - 并替换为你配置的标签名称。</para> - </step> - - <step> - <para>修改 <filename>rcstemplate</filename> 为和在 - <filename>cfg_local.pm</filename> 中相同的配置。</para> - </step> - - <step> - <para>选择性的删除在 <filename>tagcheck</filename> 中针对 FreeBSD - 检查的配置。你可以仅仅在文件的最上层加上 <literal>exit 0</literal> - 来取消所有标签的检查。</para> - </step> - - <step> - <para>在你完成前的最后一件事是确认 commitlogs 可以正确储存。 - 预设会储存在储存库中 <filename>CVSROOT</filename> 里的 - <filename>commitlogs</filename> 子目录中, - 而这个目录需要事先建立:</para> - - <screen>&prompt.user; <userinput>mkdir commitlogs</userinput> -&prompt.user; <userinput>cvs add commitlogs</userinput></screen> - </step> - </procedure> - - <para>现在, 在细心的检视过后, 你可以提交你的修改了。 确定你先前有在 - <filename>avail</filename> 中允许你自己存取 <filename>CVSROOT</filename> - 目录, 因为如果没有这样做的话你会把你自己锁在外面。 - 完整确认过后请执行下列命令:</para> - - <screen>&prompt.user; <userinput>cvs commit -m '- Initial FreeBSD scripts commit'</userinput></screen> - </sect2> - - <sect2> - <title>测试配置</title> - - <para>你已经准备好做基本的测试了: 强制提交 <filename>avail</filename> - 以确认每件事都如预期的运作。</para> - - <screen>&prompt.user; <userinput>cvs commit -f -m 'Forced commit to test the new CVSROOT scripts' avail</userinput></screen> - - <para>如果一切正常, 那么恭喜了! 你现在已经为你的储存库建立好 FreeBSD - 的命令脚本了。 如果 <application>CVS</application> 仍然有警告什么, - 回头检视上述的步骤是否有正确的执行。</para> - </sect2> - </sect1> - - <sect1 xml:id="freebsdspecific"> - <title>FreeBSD 的专用配置</title> - - <para>FreeBSD 项目自己使用一个有点不同的配置,那就是同时也使用 FreeBSD - <filename>CVSROOT</filename> 中的 <filename>freebsd</filename> 子目录。 - 因为大量的 committer 必须在相同的用户组中, 因此项目写了一个简单的 wrapper - 来确保 committer 可以正确的提交, 并配置储存库的用户组名称。</para> - - <para>如果你的储存库也需要这样的功能,那么下面就会介绍如何建立, - 不过首先要先来看一段复杂的概述。</para> - - <sect2> - <title>FreeBSD 配置中使用的文件</title> - - <itemizedlist> - <listitem> - <para><filename>access</filename> - 此文件用来控制储存库的存取。 - 你应该编辑并加入所有在项目中的成员。</para> - </listitem> - - <listitem> - <para><filename>freebsd/commitmail.pl</filename> - - 此文件已经不再使用了, - 只是因为历史原因而保留。 你不应更动此文件。</para> - </listitem> - - <listitem> - <para><filename>freebsd/cvswrap.c</filename> - 此 CVS wrapper - 源代码是用来建立检查所有存取的工作。更多的信息在稍后会提出。 - 你应该编辑 <literal>ACCESS</literal> 和 <literal>REALCVS</literal> - 的路径以符合你的配置。</para> - </listitem> - - <listitem> - <para><filename>freebsd/mailsend.c</filename> - 此文件是 FreeBSD 设定 - mailing lists 需要的, 你不应更动此文件。</para> - </listitem> - </itemizedlist> - </sect2> - - <sect2> - <title>步骤</title> - - <procedure> - <step> - <para>只把你的用户名加到 <filename>access</filename> 中。</para> - </step> - - <step> - <para>编辑 <filename>cvswrap.c</filename> 的路径以符合你的配置, - 定义在大写的 <literal>ACCESS</literal> 中。 - 同时如果默认值不符合你的情况的话也应该修改本地实际的 <command>cvs</command> - 程序所在位置。 原始的 <filename>cvswrap.c</filename> - 希望替代服务器端的 CVS 程序, - 例如将其改名为 <filename>/usr/bin/ncvs</filename>。</para> - - <para>我的 <filename>cvswrap.c</filename> 是这样:</para> - - <programlisting>#define ACCESS "/local/cvsroot/CVSROOT/access" -#define REALCVS "/usr/bin/ncvs"</programlisting> - </step> - - <step> - <para>接下来是建立 wrapper 来确认你在提交时是在正确的用户组中。 - 在你的 <filename>CVSROOT</filename> 中的 - <filename>cvswrap.c</filename> 要能够使用。</para> - - <para>在你完成编辑并加入正确的路径后我们要来编译源代码:</para> - - <screen>&prompt.user; <userinput>cc -o cvs cvswrap.c</userinput></screen> - - <para>然后进行需要配置(此步骤需要 root 权限):</para> - - <screen>&prompt.root; <userinput>mv /usr/bin/cvs /usr/bin/ncvs</userinput> -&prompt.root; <userinput>mv cvs /usr/bin/cvs</userinput> -&prompt.root; <userinput>chown root:ncvs /usr/bin/cvs /usr/bin/ncvs</userinput> -&prompt.root; <userinput>chmod o-rx /usr/bin/ncvs</userinput> -&prompt.root; <userinput>chmod u-w,g+s /usr/bin/cvs</userinput></screen> - - <para>这会将 wrapper 安装成预设的 <command>cvs</command> 程序, - 请确定任何要使用储存库的人应该有正确的存取权限。</para> - </step> - - <step> - <para>现在你可以删除所有在储存库用户组中的使用者,所有的存取控制会经由 - wrapper 完成,同时 wrapper 会配置存取的正确用户组。</para> - </step> - </procedure> - </sect2> - - <sect2> - <title>测试配置</title> - - <para>你的 wrapper 现在应该已经安装好了,你当然也可以强制提交 - <filename>access</filename> 来测试是否正常:</para> - - <screen>&prompt.user; <userinput>cvs commit -f -m 'Forced commit to test the new CVSROOT scripts' access</userinput></screen> - - <para>同样地,如果有错误,检查是否上述所有步骤都有正确的执行。</para> - </sect2> - </sect1> -</article> diff --git a/zh_CN.GB2312/articles/linux-users/article.xml b/zh_CN.GB2312/articles/linux-users/article.xml deleted file mode 100644 index 366cbed6b3..0000000000 --- a/zh_CN.GB2312/articles/linux-users/article.xml +++ /dev/null @@ -1,572 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - Original Revision: r39170 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>&linux; 用户的 FreeBSD 快速入门向导</title> - - - <authorgroup> - <author><personname><firstname>John</firstname><surname>Ferrell</surname></personname></author> - </authorgroup> - - <pubdate>$FreeBSD$</pubdate> - - <copyright> - <year>2008</year> - <holder>The FreeBSD Documentation Project</holder> - </copyright> - - <releaseinfo>$FreeBSD$</releaseinfo> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.linux; - &tm-attrib.intel; - &tm-attrib.redhat; - &tm-attrib.unix; - &tm-attrib.general; - </legalnotice> - - <abstract> - <para>本文档旨在快速使那些高级 &linux; - 用户熟悉FreeBSD的一些基础知识。</para> - </abstract> - </info> - - <sect1 xml:id="intro"> - <title>简介</title> - - <para>本文档将突出介绍 &os; 与 &linux; 的差别, - 以使得那些 &linux; 高级用户能自己快速熟悉 &os; - 的基础内容。这只是份技术上的快速入门, - 并非是试图描绘这两种操作系统之间的"哲学"上的差异。</para> - - <para>此文档假定认为你已经安装好了 &os;。 - 如果你还没有安装 &os; 或者对 &os; - 的安装过程方面需要帮助,请参考 &os; 手册的 - <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/install.html"> - 安装 FreeBSD</link>一章。</para> - </sect1> - - <sect1 xml:id="shells"> - <title>Shell程序:没有Bash吗?</title> - - <para>那些从 &linux; 转过来的用户经常会惊讶于 - <application>Bash</application> 不是 &os; 的默认 Shell。 - 事实上,<application>Bash</application> 甚至没有包括在 - &os; 的默认安装中。代替的是,&os; 使用 &man.tcsh.1; - 作为自己的默认 Shell,尽管如此,<application>Bash</application> - 和其他你喜爱的 Shell 程序在 &os; 的 - <link xlink:href="article.html#SOFTWARE">Packages 和 Ports 套件</link> - 里都可以找到。</para> - - <para>如果你安装了其他的 Shell 你可以使用 &man.chsh.1; - 来设置一个用户的默认 Shell。 通常情况下, - 强烈建议不要去更改 <systemitem class="username">root</systemitem> - 用户的默认 Shell。原因是这些 Shell - 没有包括在基本系统中,正常情况下它们会被安装到 - <filename>/usr/local/bin</filename> 和 - <filename>/usr/bin</filename> 目录下。万一某天 - <filename>/usr/local/bin</filename> 和 - <filename>/usr/bin</filename> 的文件系统不能被挂载, - 这样情况下 <systemitem class="username">root</systemitem> - 将不能进入自己默认的 Shell,从而 - <systemitem class="username">root</systemitem> 将不能够登录进去。 - 鉴于这个原因,第二个系统管理员帐户 - <systemitem class="username">toor</systemitem> 创建时使用的是非默认的 - Shell。在安全 FAQ 可以查阅到关于 <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/faq/security.html#TOOR-ACCOUNT">toor 帐户</link> - 的信息。</para> - </sect1> - - <sect1 xml:id="software"> - <title>Packages和Ports:在 &os; 中添加软件</title> - - <para>除了经典的 &unix; 安装软件的方法 - (下载源码包,解压,编辑源码,编译)外,&os; - 还提供了另外两种方法来安装应用程序:packages 和 ports。 - 你可以在 <link xlink:href="http://www.freebsd.org/ports/master-index.html">这里</link> - 到一份完整可用的 ports 和 packages 的软件清单。</para> - - <sect2 xml:id="packages"> - <title>Packages</title> - - <para>Packages 是预编译好的应用程序,在 &os; - 中等价于基于 Debian/Ubuntu 的系统中的 - <filename>.deb</filename> 软件包以及基于 - Red Hat/Fedora 的系统中的 - <filename>.rpm</filename> 软件包。 - Packages使用 &man.pkg.add.1; 来进行安装。 - 例如,下面的命令将用来安装 - <application>Apache 2.2</application>:</para> - - <screen>&prompt.root; <userinput>pkg_add /tmp/apache-2.2.6_2.tbz</userinput></screen> - - <para>使用 <option>-r</option> 操作将告诉 &man.pkg.add.1; - 来自动获取并安装一个软件包,以及解决所有的依赖关系:</para> - - <screen>&prompt.root; <userinput>pkg_add -r apache22</userinput> -Fetching ftp://ftp.freebsd.org/pub/FreeBSD/ports/i386/packages-6.2-release/Latest/apache22.tbz... Done. -Fetching ftp://ftp.freebsd.org/pub/FreeBSD/ports/i386/packages-6.2-release/All/expat-2.0.0_1.tbz... Done. -Fetching ftp://ftp.freebsd.org/pub/FreeBSD/ports/i386/packages-6.2-release/All/perl-5.8.8_1.tbz... Done. -[snip] - -To run apache www server from startup, add apache22_enable="YES" -in your /etc/rc.conf. Extra options can be found in startup script.</screen> - - <note> - <para>如果你正运行着 release 版本的 &os; (6.2,6.3,7.0等, - 通常从 CD-ROM 被安装的)<command>pkg_add -r</command> - 会为其下载专门为这些特定版本构建好的软件包。 - 这些软件包 <emphasis>可能</emphasis> 不是当前最新的程序。 - 你可以使用 <envar>PACKAGESITE</envar> 变量来覆盖默认的动作。 - 例如,把 <envar>PACHAGESITE</envar> 设置成 - <uri xlink:href="ftp://ftp.freebsd.org/pub/FreeBSD/ports/i386/packages-6-stable/Latest/">ftp://ftp.freebsd.org/pub/FreeBSD/ports/i386/packages-6-stable/Latest/</uri> - 来下载 6.X 系列最新的包。</para> - </note> - - <para>想了解更多的 packages 信息请查阅 &os; - 手册的 4.4 小节:<link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/packages-using.html">使用 Packages 系统</link>。</para> - </sect2> - - <sect2 xml:id="ports"> - <title>Ports</title> - - <para>&os; 的第二种安装应用程序的方法就是使用 Ports 套件了。 - Ports 套件是 &os; 上的一个利用 <filename>Makefile</filename> - 和一些补丁文件来特定从源码定制安装各种软件程序的框架。 - 当安装一个 port 时系统会获取程序源码, - 应用任何所需要的补丁,编译源码, - 并安装应用程序(并针对依赖关系以同样的方式安装解决)。</para> - - <para>Ports 套件,常被称作 ports 树,可以在 - <filename>/usr/ports</filename> 下找到。 - 假设Ports套件已经在安装 &os; 时安装过了。 - 如果 Ports 套件还没有被安装可以通过 &man.sysinstall.8; - 来进行安装,或者使用 &man.csup.1; 或 &man.portsnap.8; - 来从 &os; 的服务器上面拉下来。在手册的 <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/ports-using.html">4.5.1 小节</link> - 可以找到安装 Ports 套件的详细介绍。</para> - - <para>安装一个 port 就像进入 port - 的目录并开始构建过程一样简单(通常情况下), - 下面是从 Ports 套件安装 - <application>Apache 2.2</application> 的例子:</para> - - <screen>&prompt.root; <userinput>cd /usr/ports/www/apache22</userinput> -&prompt.root; <userinput>make install clean</userinput></screen> - - <para>使用 ports 安装软件的最大好处就是能够自定义安装选项。 - 例如,从 ports 安装 <application>Apache 2.2</application> - 时你可以通过设置 <varname>WITH_LDAP</varname> &man.make.1; - 变量来启用 <application>mod_ldap</application>:</para> - - <screen>&prompt.root; <userinput>cd /usr/ports/www/apache22</userinput> -&prompt.root; <userinput>make WITH_LDAP="YES" install clean</userinput></screen> - - <para>请查看 &os; 手册的 4.5 小节,<link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/ports-using.html"> - 使用 Ports Collection</link>, - 以获取更多关于Ports Collection 的信息。</para> - </sect2> - - <sect2 xml:id="which"> - <title>Ports还是packages,我应该使用哪个?</title> - - <para>Packages 就是预编译好的 ports, - 所以从源码(ports)安装与从二进制 packages - 安装这两者间确实有很大关联。每种方法各有自己的优点:</para> - - <itemizedlist> - <title>Packages(二进制)</title> - - <listitem><simpara>更快速的安装 - (比较大的应用程序编译起来会花很长时间)。</simpara></listitem> - - <listitem><simpara>你不需要知道如何编译软件。</simpara></listitem> - - <listitem><simpara>不需要在操作系统上安装编译器。</simpara></listitem> - </itemizedlist> - - <itemizedlist> - <title>Ports(源码)</title> - - <listitem><simpara>能够定制安装选项。 - (Packages通常都是使用标准选项构建的。使用 ports - 你能够定义各种各样的选项, - 比如类似构建附加的模块或是更改安装路径之类的。)</simpara></listitem> - - <listitem><simpara>如果你喜欢的话还可以使用自己的补丁。</simpara></listitem> - </itemizedlist> - - <para>如果你没有一些特别的需求, packages - 可能刚好最适合你的情况。如果你需要进一步定制, - ports 是最适合的方法了。(请记得, - 如果你需要定制而自己又更倾向于使用 packages, - 你可以使用 <command>make</command> - <buildtarget>package</buildtarget> 从 ports - 构建一个定制的 package,然后复制到其他的服务器。)</para> - </sect2> - </sect1> - - <sect1 xml:id="startup"> - <title>系统启动:运行级别在哪里?</title> - - <para>&linux; 使用 Sysv init 初始化系统,而 &os; - 使用的是传统的 BSD 风格的 &man.init.8;。在 BSD 风格的 - &man.init.8; 中没有运行级别和 <filename>/etc/inittab</filename>, - 代替控制启动的是 &man.rc.8; 实用程序。 - <filename>/etc/rc</filename> 脚本读取 - <filename>/etc/defaults/rc.conf</filename> 和 - <filename>/etc/rc.conf</filename> 文件来决定哪个服务将被启动。 - 特殊服务在此后由处于 <filename>/etc/rc.d/</filename> 和 - <filename>/usr/local/etc/rc.d/</filename> - 下的相应服务初始化脚本文件所启动。 - 这些脚本类似于位于 &linux; 系统中的 - <filename>/etc/init.d/</filename> 目录下的脚本。</para> - - <sidebar> - <para><emphasis>为何会有两个服务初始化脚本的目录呢?</emphasis> - <filename>/etc/rc.d/</filename> 下的脚本是属于 - <quote>基本</quote> 系统一部分的应用程序所使用的。 - (&man.cron.8;,&man.sshd.8;,&man.syslog.3;,以及其他。) - <filename>/usr/local/etc/rc.d/</filename> - 下的脚本是用户安装的应用程序如 <application>Apache</application>, - <application>Squid</application> 等使用的。</para> - - <para><emphasis><quote>基本</quote> - 系统和用户安装的应用程序之间的区别是什么?</emphasis> FreeBSD - 是一套开发出来的完整的操作系统,也就是说,内核,系统类库, - 还有实用应用程序(如 &man.ls.1;,&man.cat.1;,&man.cp.1; 等) - 全部被做为一个整体一起开发并释出。这就是被认为归属于 - <quote>基本</quote>系统的程序。用户安装的程序并不是 - <quote>基本</quote>系统的一部分,如 - <application>Apache</application>,<application>X11</application>, - <application>Moazilla Firefox</application>,等等。这 - 些用户安装的应用程序通常是使用 &os; 的 Packages 和 Ports - 套件安装上去的。为了将这些程序和 <quote>基本</quote> - 系统区分开来,用户安装的应用程序通常被安装到 - <filename>/usr/local/</filename>下。 - 因此用户安装的二进制执行文件存在于 - <filename>/usr/local/bin</filename>下,配置文件在 - <filename>/usr/local/etc</filename>下,以此类推。</para> - </sidebar> - - <para>您可以通过在 - <filename>/etc/rc.conf</filename>(&man.rc.conf.5;) - 文件中增加与之对应的 - <literal>ServiceName_enable="YES"</literal> - 配置来启用服务。 看一下系统默认的 - <filename>/etc/defaults/rc.conf</filename> 文件, 这些默认配置可以使用 - <filename>/etc/rc.conf</filename> 文件来改变。 因此, - 当安装附加应用程序时最好回顾下文档来决定到底该如何启用任何相关的服务。</para> - - <para>下面的一小段内容用来在 <filename>/etc/rc.conf</filename> - 中启用 &man.sshd.8; 和 <application>Apache 2.2</application>。 - 还指定了 <application>Apache</application> 应该通过 - SSL 方式启动。</para> - - <programlisting># enable SSHD -sshd_enable="YES" -# enable Apache with SSL -apache22_enable="YES" -apache22_flags="-DSSL"</programlisting> - - <para>一旦服务已经在 <filename>/etc/rc.conf</filename> - 中启用,服务将能够从命令行启动(不需要重新启动系统):</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/sshd start</userinput></screen> - - <para>如果服务还没有被启用,可以使用 - <option>forcestart</option> 来从命令行启动:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/sshd forcestart</userinput></screen> - </sect1> - - <sect1 xml:id="network"> - <title>网络配置</title> - - <sect2 xml:id="interfaces"> - <title>网络接口</title> - - <para>代替 &linux; 中所使用的标识网络接口所常用的 - <emphasis>ethX</emphasis> 格式的是,&os; - 使用驱动名字后跟一个数字来标识。下面 - &man.ifconfig.8; 的输出显示了两个 &intel; Pro 1000 - 的网络接口(<filename>em0</filename> 和 <filename>em1</filename>):</para> - - <screen>&prompt.user; <userinput>ifconfig</userinput> -em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 - options=b<RXCSUM,TXCSUM,VLAN_MTU> - inet 10.10.10.100 netmask 0xffffff00 broadcast 10.10.10.255 - ether 00:50:56:a7:70:b2 - media: Ethernet autoselect (1000baseTX <full-duplex>) - status: active -em1: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500 - options=b<RXCSUM,TXCSUM,VLAN_MTU> - inet 192.168.10.222 netmask 0xffffff00 broadcast 192.168.10.255 - ether 00:50:56:a7:03:2b - media: Ethernet autoselect (1000baseTX <full-duplex>) - status: active</screen> - </sect2> - - <sect2 xml:id="ipaddress"> - <title>IP配置</title> - - <para>一个 IP 地址可以使用 &man.ifconfig.8; - 来指定到一个网络接口。通常,要保持重启后依然能够使用的 - IP 配置信息需要包含在 <filename>/etc/rc.conf</filename> - 中。下列例子指定了主机名,IP 地址,以及默认网关:</para> - - <programlisting>hostname="server1.example.com" -ifconfig_em0="inet 10.10.10.100 netmask 255.255.255.0" -defaultrouter="10.10.10.1"</programlisting> - - <para>使用下面内容来为网络接口配置DHCP:</para> - - <programlisting>hostname="server1.example.com" -ifconfig_em0="DHCP"</programlisting> - - </sect2> - </sect1> - - <sect1 xml:id="firewall"> - <title>防火墙</title> - - <para>像 &linux; 中的 <application>IPTABLES</application> - 一样, &os; 也提供了一个内核级的防火墙; - 实际上 &os; 提供了三个防火墙:</para> - - <itemizedlist> - <listitem><simpara><link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/firewalls-ipfw.html">IPFIREWALL</link></simpara></listitem> - <listitem><simpara><link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/firewalls-ipf.html">IPFILTER</link></simpara></listitem> - <listitem><simpara><link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/firewalls-pf.html">PF</link></simpara></listitem> - </itemizedlist> - - <para><application>IPFIREWALL</application> 或是 - <application>IPFW</application>(管理 - <application>IPFW</application> 规则的 &man.ipfw.8; 命令) - 是 &os; 开发者开发并维持的。 - <application>IPFW</application> 能够与 &man.dummynet.4; - 配合使用来提供流量图形功能以及模拟不同网络连接类型的功能。</para> - - <para>允许 <application>SSH</application> - 进入的 <application>IPFW</application> 规则样例如下:</para> - - <programlisting>ipfw add allow tcp from any to me 22 in via $ext_if</programlisting> - - <para><application>IPFILTER</application> 是 - Darren Reed 所开发的防火墙程序。不是专门针对 - &os; 的,它已经被移植到 NetBSD,OpenBSD,SunOS,HP/UX, - 还有Solaris等一些操作系统之上。</para> - - <para>允许 <application>SSH</application> - 进入的 <application>IPFILTER</application> - 命令样例如下:</para> - - <programlisting>pass in on $ext_if proto tcp from any to any port = 22</programlisting> - - <para>最后一种防火墙程序,<application>PF</application>, - 是 OpenBSD 项目所开发的。<application>PF</application> - 是被作为 <application>IPFILTER</application> - 的一个替代品而被创建出的。就这点而言, - <application>PF</application> 的语法与 - <application>IPFILTER</application> 的非常相似。 - <application>PF</application> 可以与 &man.altq.4; - 配合来提供 QoS 的特性。</para> - - <para>允许 <application>SSH</application> - 进入的 <application>PF</application> - 命令样例如下:</para> - - <programlisting>pass in on $ext_if inet proto tcp from any to ($ext_if) port 22</programlisting> - </sect1> - - <sect1 xml:id="updates"> - <title>升级 &os;</title> - - <para>共有三种方法来升级 &os; 系统: - 源码,二进制更新,还有安装光盘。</para> - - <para>从源码升级是最复杂的升级方法,但是提供了最棒的总体灵活性。 - 这个过程包含了使用 &os; <application>CVS</application> - (并行版本系统)来同步一个本地的 &os; 源代码。 - 一旦本地源码已经更新到当前最新你便可以构建新版本的内核以及应用程序。 - 关于源码更新的更多信息可见于 &os; 手册 - <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/cutting-edge.html"> - 关于如何更新操作系统的章节</link>。</para> - - <para>二进制更新类似于使用 <command>yum</command> 或 - <command>apt-get</command> 更新 &linux; 系统。 - &man.freebsd-update.8; 命令会获取新的更新并安装它们。 - 这些更新可以通过 &man.cron.8; 使用程序来调度。</para> - - <note> - <para>如果你使用 &man.cron.8; 来预定更新, - 请确信在你的 &man.crontab.1; 中使用了 - <command>freebsd-update cron</command> - 来控制大数目的机器同时获取更新。</para> - - <programlisting>0 3 * * * root /usr/sbin/freebsd-update cron</programlisting> - </note> - - <para>最后一种更新的方法,从安装光盘来升级,是个直接的过程。 - 从安装光盘启动并选择该选项来更新。</para> - </sect1> - - <sect1 xml:id="procfs"> - <title>procfs:已是过去式但仍未被遗忘</title> - - <para>&linux; 中,你可能会通过看一看 - <filename>/proc/sys/net/ipv4/ip_forward</filename> - 来确定 IP 转发是否被启用。在 &os; 中你应该使用 &man.sysctl.8; - 来查看这和其他方面的系统设置,在当前的 &os; 版本中 - &man.procfs.5; 已经不赞成使用了。(虽然 - <command>sysctl</command>在 &os; 也同样可用。)</para> - - <para>在 IP 转发样例中,你应该使用下列内容来确定 &os; - 系统中是否已经开启了 IP 转发:</para> - - <screen>&prompt.user; <userinput>sysctl net.inet.ip.forwarding</userinput> -net.inet.ip.forwarding: 0</screen> - - <para><option>-a</option> 标志用来列出所有的系统设置:</para> - - <screen>&prompt.user; <userinput>sysctl -a</userinput> -kern.ostype: FreeBSD -kern.osrelease: 6.2-RELEASE-p9 -kern.osrevision: 199506 -kern.version: FreeBSD 6.2-RELEASE-p9 #0: Thu Nov 29 04:07:33 UTC 2007 - root@i386-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC - -kern.maxvnodes: 17517 -kern.maxproc: 1988 -kern.maxfiles: 3976 -kern.argmax: 262144 -kern.securelevel: -1 -kern.hostname: server1 -kern.hostid: 0 -kern.clockrate: { hz = 1000, tick = 1000, profhz = 666, stathz = 133 } -kern.posix1version: 200112 -...</screen> - - <note> - <para>某些 <command>sysctl</command> 的参数是只读的。</para> - </note> - - <para>需要 procfs 的情况是,运行一些较老的软件,使用 - &man.truss.1; 来跟踪系统信号,以及 - <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/linuxemu.html">&linux; 二进制兼容</link>. - (尽管,&linux; 二进制兼容性使用其本身的 procfs,&man.linprocfs.5;。) - 如果你需要挂载 procfs 你可以在 - <filename>/etc/fstab</filename> 中加入如下内容:</para> - - <screen>proc /proc procfs rw,noauto 0 0</screen> - - <note> - <para><option>noauto</option> 会防止 - <filename>/proc</filename> 在启动时被自动挂载。</para> - </note> - - <para>然后使用如下命令挂载 procfs:</para> - - <screen>&prompt.root; <userinput>mount /proc</userinput></screen> - </sect1> - - <sect1 xml:id="commands"> - <title>常用命令</title> - - <sect2 xml:id="packageCommands"> - <title>软件包管理</title> - - <para> - <informaltable frame="none" pgwide="1"> - <tgroup cols="3"> - <thead> - <row> - <entry>&linux; 命令 (Red Hat/Debian)</entry> - <entry>&os; 等价命令</entry> - <entry>目的</entry> - </row> - </thead> - - <tbody> - <row> - <entry><command>yum install package</command> / <command>apt-get install package</command></entry> - <entry><command>pkg_add -r package</command></entry> - <entry>从远程仓库安装 <replaceable>package</replaceable></entry> - </row> - - <row> - <entry><command>rpm -ivh package</command> / <command>dpkg -i package</command></entry> - <entry><command>pkg_add -v package</command></entry> - <entry>安装 package</entry> - </row> - - <row> - <entry><command>rpm -qa</command> / <command>dpkg -l</command></entry> - <entry><command>pkg_info</command></entry> - <entry>列出已安装的软件包</entry> - </row> - </tbody> - </tgroup> - </informaltable> - </para> - </sect2> - - <sect2 xml:id="systemCommands"> - <title>系统管理</title> - - <para> - <informaltable frame="none" pgwide="1"> - <tgroup cols="3"> - <thead> - <row> - <entry>&linux; 命令</entry> - <entry>&os; 等价命令</entry> - <entry>目的</entry> - </row> - </thead> - - <tbody> - <row> - <entry><command>lspci</command></entry> - <entry><command>pciconf</command></entry> - <entry>列出 PCI 设备</entry> - </row> - - <row> - <entry><command>lsmod</command></entry> - <entry><command>kldstat</command></entry> - <entry>列出已载入的内核模块</entry> - </row> - - <row> - <entry><command>modprobe</command></entry> - <entry><command>kldload</command> / <command>kldunload</command></entry> - <entry>载入/卸载内核模块</entry> - </row> - - <row> - <entry><command>strace</command></entry> - <entry><command>truss</command></entry> - <entry>跟踪系统调用</entry> - </row> - </tbody> - </tgroup> - </informaltable> - </para> - </sect2> - </sect1> - - <sect1 xml:id="conclusion"> - <title>总结</title> - - <para>非常希望这篇文档能够给予你足够的帮助来开始你的 - &os; 之路。务必要再去看一下 <link xlink:href="&url.base;/doc/en_US.ISO8859-1/books/handbook/index.html">&os; 手册</link> - 所提到的但并没有被包含在本文档中的那些更深入广泛的主题。</para> - </sect1> -</article> diff --git a/zh_CN.GB2312/articles/nanobsd/article.xml b/zh_CN.GB2312/articles/nanobsd/article.xml deleted file mode 100644 index 124180ee1d..0000000000 --- a/zh_CN.GB2312/articles/nanobsd/article.xml +++ /dev/null @@ -1,468 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - Original Revision: 1.7 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>NanoBSD 简介</title> - - - <authorgroup> - <author><personname><firstname>Daniel</firstname><surname>Gerzo</surname></personname></author> - </authorgroup> - - <copyright> - <year>2006</year> - <holder>The FreeBSD Documentation Project</holder> - </copyright> - - <pubdate>$FreeBSD$</pubdate> - - <releaseinfo>$FreeBSD$</releaseinfo> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.general; - </legalnotice> - - <abstract> - <para>这篇文档提供了关于 <application>NanoBSD</application> 工具的介绍信息, - 这一工具可以用来创建用于嵌入式应用的 &os; 系统映像, - 以适应存放到袖珍闪存 (Compact Flash) 卡 (或其它大容量存储介质) 上的需要。</para> - </abstract> - </info> - - <sect1 xml:id="intro"> - <title>NanoBSD 简介</title> - - <indexterm><primary>NanoBSD</primary></indexterm> - - <para><application>NanoBSD</application> 是 &a.phk; 目前正在开发的一项工具。 - 它可以用来创建用于嵌入式应用的 &os; 系统映像, - 以便配合袖珍闪存 (Compact Flash) 卡 (或其他大容量存储介质) 使用。</para> - - <para>这一工具也可以用来构建定制的安装映像, - 以简化通常称为 <quote>计算设备 (computer appliances)</quote> - 的系统的安装和维护工作。 计算设备通常在产品中将捆绑硬件和软件, - 或者换言之, 所有的应用程序都是预先装好的。 - 这些设备可以直接插到暨存的网络中, - 并 (几乎是) 立即投入使用。</para> - - <para><application>NanoBSD</application> 提供的功能包括:</para> - - <itemizedlist> - <listitem> - <para>可以和 &os; 一样使用 Ports 和预编译包— - 所有的应用程序都可以在 <application>NanoBSD</application> 映像中直接使用, - 而方式与 &os; 完全一样。</para> - </listitem> - - <listitem> - <para>不减少功能 — 能够使用 &os; 做的任何工作, 都可以在 - <application>NanoBSD</application> 中使用, - 除非您在创建 <application>NanoBSD</application> 映像时, - 明确地删去它们。</para> - </listitem> - - <listitem> - <para>所有对象在运行时均是只读的 — 可以安全地拔掉电源插销。 - 在系统非正常关闭之后, 无需运行 - &man.fsck.8;。</para> - </listitem> - - <listitem> - <para>便于联编和定制 — 只需使用一个 shell 脚本和一个配置文件, - 您可以很容易地裁减和定制适于任意需求的映像。</para> - </listitem> - </itemizedlist> - </sect1> - - <sect1 xml:id="howto"> - <title>如何使用 NanoBSD</title> - - <sect2 xml:id="design"> - <title>NanoBSD 的设计</title> - - <para>一旦将映像存入介质, 就可以用它来引导 <application>NanoBSD</application> - 了。 默认情况下, 大容量存储器会划分为三个区:</para> - - <itemizedlist> - <listitem> - <para>两个映像区: <literal>code#1</literal> - 和 <literal>code#2</literal>。</para> - </listitem> - - <listitem> - <para>一个配置文件区, 运行环境中, - 可以将其挂接到 <filename>/cfg</filename> 目录下。</para> - </listitem> - </itemizedlist> - - <para>这些分区默认情况下以只读方式挂接。</para> - - <para><filename>/etc</filename> 和 - <filename>/var</filename> 目录均为 - &man.md.4; (malloc) 盘。</para> - - <para>配置文件分区保存在 - <filename>/cfg</filename> 目录。 - 它包含了用于 <filename>/etc</filename> - 目录的文件, 在启动之后暂时以只读方式挂接。 因此, - 在需要从 <filename>/etc</filename> 向 - <filename>/cfg</filename> 目录复制所进行的、 - 希望在重启时保持不变的配置时, 需要进行一些额外的操作。</para> - - <example> - <title>在 <filename>/etc/resolv.conf</filename> 中进行需要保持的修改</title> - - <screen>&prompt.root; <userinput>vi /etc/resolv.conf</userinput> -[...] -&prompt.root; <userinput>mount /cfg</userinput> -&prompt.root; <userinput>cp /etc/resolv.conf /cfg</userinput> -&prompt.root; <userinput>umount /cfg</userinput></screen> - </example> - - <note> - <para>只有在系统启动过程中, 以及需要修改配置文件的场合, 才需要挂接包含 - <filename>/cfg</filename> 的那个分区。</para> - - <para>在任何时候都保持挂接 <filename>/cfg</filename> - 不是一个好主意, 特别是当您把 <application>NanoBSD</application> - 放在不适合进行大量写操作的分区时 - (由于文件系统的同步进程会定期向系统盘写一些数据)。</para> - </note> - </sect2> - - <sect2> - <title>构建 NanoBSD 映像</title> - - <para><application>NanoBSD</application> 映像是通过使用非常简单的 - <filename>nanobsd.sh</filename> shell 脚本来构建的, 这个脚本可以在 - <filename>/usr/src/tools/tools/nanobsd</filename> - 目录中找到。 这个脚本建立的映像文件, 可以用 &man.dd.1; 工具复制到存储介质上。</para> - - <para>构建 - <application>NanoBSD</application> 映像所需的命令是:</para> - - <screen>&prompt.root; <userinput>cd /usr/src/tools/tools/nanobsd</userinput> <co xml:id="nbsd-cd"/> -&prompt.root; <userinput>sh nanobsd.sh</userinput> <co xml:id="nbsd-sh"/> -&prompt.root; <userinput>cd /usr/obj/nanobsd.full</userinput> <co xml:id="nbsd-cd2"/> -&prompt.root; <userinput>dd if=_.disk.full of=/dev/da0 bs=64k</userinput> <co xml:id="nbsd-dd"/></screen> - - <calloutlist> - <callout arearefs="nbsd-cd"> - <para>进入 <application>NanoBSD</application> 构建脚本的主目录。</para> - </callout> - - <callout arearefs="nbsd-sh"> - <para>开始构建过程。</para> - </callout> - - <callout arearefs="nbsd-cd2"> - <para>进入构建好的映像文件所在的目录。</para> - </callout> - - <callout arearefs="nbsd-dd"> - <para>在存储介质上安装 <application>NanoBSD</application>。</para> - </callout> - </calloutlist> - </sect2> - - <sect2> - <title>定制 NanoBSD 映像</title> - - <para>这可能是 <application>NanoBSD</application> 最为重要, - 同时也是您最感兴趣的功能。 同时, 您在开发 - <application>NanoBSD</application> 应用时, - 这也是相当耗时的过程。</para> - - <para>执行下面的命令将使 - <filename>nanobsd.sh</filename> 从当前目录中的 - <filename>myconf.nano</filename> 文件读取配置:</para> - - <screen>&prompt.root; <userinput>sh nanobsd.sh -c myconf.nano</userinput></screen> - - <para>定制过程包含两步:</para> - - <itemizedlist> - <listitem> - <para>配置选项</para> - </listitem> - - <listitem> - <para>定制函数</para> - </listitem> - </itemizedlist> - - <sect3> - <title>配置选项</title> - - <para>通过对配置进行设置, 可以配置用以传递给 - <application>NanoBSD</application> 构建过程中 - <buildtarget>buildworld</buildtarget> - 和 <buildtarget>installworld</buildtarget> 阶段的联编和安装选项, 以及 - <application>NanoBSD</application> 的主构建过程中的选项。 - 通过使用这些选项可以削减系统的尺寸, 使之能够放入 - 64MB 的存储。 您还可以进一步通过这些选项来削减 &os;, - 直到它只包含内核以及两三个用户环境文件为止。</para> - - <para>配置文件中包含用以代替默认值的配置选项。 - 最重要的语句包括:</para> - - <itemizedlist> - <listitem> - <para><literal>NANO_NAME</literal> — 本次构建的名称 - (用于创建工作目录的名字)。</para> - </listitem> - - <listitem> - <para><literal>NANO_SRC</literal> — 用以联编和构建映像的源码树的位置。</para> - </listitem> - - <listitem> - <para><literal>NANO_KERNEL</literal> — 用以联编内核的配置文件的名字。</para> - </listitem> - - <listitem> - <para><literal>CONF_BUILD</literal> — 用于传递给 - <buildtarget>buildworld</buildtarget> 构建阶段的选项。</para> - </listitem> - - <listitem> - <para><literal>CONF_INSTALL</literal> — 用于传递给 - <buildtarget>installworld</buildtarget> 构建阶段的选项。</para> - </listitem> - - <listitem> - <para><literal>CONF_WORLD</literal> — 用以传递给 - <buildtarget>buildworld</buildtarget> 和 - <buildtarget>installworld</buildtarget> 这两个构建阶段的选项。</para> - </listitem> - - <listitem> - <para><literal>FlashDevice</literal> — 定义所用的介质类型。 - 要了解进一步的细节, 请参考 <filename>FlashDevice.sub</filename> - 文件。</para> - </listitem> - </itemizedlist> - </sect3> - - <sect3> - <title>定制函数</title> - - <para>通过在配置文件中使用 shell 函数可以进一步微调 - <application>NanoBSD</application>。 下面的例子展示了定制函数的基本模式:</para> - - <programlisting>cust_foo () ( - echo "bar=baz" > \ - ${NANO_WORLDDIR}/etc/foo -) -customize_cmd cust_foo</programlisting> - - <para>下面是一个更贴近实际的例子, 它将默认的 - <filename>/etc</filename> 目录尺寸, - 从 5MB 调整为 30MB:</para> - - <programlisting>cust_etc_size () ( - cd ${NANO_WORLDDIR}/conf - echo 30000 > default/etc/md_size -) -customize_cmd cust_etc_size</programlisting> - - <para>除此之外, 还有几个默认的预定义定制函数:</para> - - <itemizedlist> - <listitem> - <para><literal>cust_comconsole</literal> — 在 VGA 设备上禁止 - &man.getty.8; - (<filename>/dev/ttyv*</filename> 设备节点) 并启用串口 COM1 - 作为系统控制台。</para> - </listitem> - - <listitem> - <para><literal>cust_allow_ssh_root</literal> — 允许 - <systemitem class="username">root</systemitem> 通过 &man.sshd.8; 登录。</para> - </listitem> - - <listitem> - <para><literal>cust_install_files</literal> — - 从 <filename>nanobsd/Files</filename> - 目录中安装文件, 这包含一些实用的系统管理脚本。</para> - </listitem> - </itemizedlist> - </sect3> - - <sect3> - <title>安装预编译软件包</title> - - <para>通过增加自定义的函数, 可以在 <application>NanoBSD</application> - 增加预编译的软件包。 下面的函数会添加位于 - <filename>/usr/src/tools/tools/nanobsd/packages</filename> - 的全部预编译软件包:</para> - - <programlisting>install_packages () ( -mkdir -p ${NANO_WORLDDIR}/packages -cp /usr/src/tools/tools/nanobsd/packages/* ${NANO_WORLDDIR}/packages -chroot ${NANO_WORLDDIR} sh -c 'cd packages; pkg_add -v *;cd ..;' -rm -rf ${NANO_WORLDDIR}/packages -) -customize_cmd install_packages</programlisting> - </sect3> - - <sect3> - <title>配置文件举例</title> - - <para>下面是一个用于构建定制的 <application>NanoBSD</application> 映像的完整例子:</para> - - <programlisting>NANO_NAME=custom -NANO_SRC=/usr/src -NANO_KERNEL=MYKERNEL -NANO_IMAGES=2 - -CONF_BUILD=' -NO_KLDLOAD=YES -NO_NETGRAPH=YES -NO_PAM=YES -' - -CONF_INSTALL=' -NO_ACPI=YES -NO_BLUETOOTH=YES -NO_CVS=YES -NO_FORTRAN=YES -NO_HTML=YES -NO_LPR=YES -NO_MAN=YES -NO_SENDMAIL=YES -NO_SHAREDOCS=YES -NO_EXAMPLES=YES -NO_INSTALLLIB=YES -NO_CALENDAR=YES -NO_MISC=YES -NO_SHARE=YES -' - -CONF_WORLD=' -NO_BIND=YES -NO_MODULES=YES -NO_KERBEROS=YES -NO_GAMES=YES -NO_RESCUE=YES -NO_LOCALES=YES -NO_SYSCONS=YES -NO_INFO=YES -' - -FlashDevice SanDisk 1G - -cust_nobeastie() ( - touch ${NANO_WORLDDIR}/boot/loader.conf - echo "beastie_disable=\"YES\"" >> ${NANO_WORLDDIR}/boot/loader.conf -) - -customize_cmd cust_comconsole -customize_cmd cust_install_files -customize_cmd cust_allow_ssh_root -customize_cmd cust_nobeastie</programlisting> - </sect3> - </sect2> - - <sect2> - <title>更新 NanoBSD</title> - - <para>更新 <application>NanoBSD</application> 相对而言较为简单:</para> - - <procedure> - <step> - <para>和之前一样构建新的 <application>NanoBSD</application> 映像文件。</para> - </step> - - <step> - <para>将新的映像放入正运行的 - <application>NanoBSD</application> 设备中的一个未用的分区。</para> - - <para>与之前最初安装 <application>NanoBSD</application> 的步骤相比, - 这一步骤最重要的区别在于这次不应使用 <filename>_.disk.full</filename> 文件 - (它包含整个盘的映像), - 而应安装 <filename>_.disk.image</filename> 映像 (这个文件中, - 只包含一个系统分区)。</para> - </step> - - <step> - <para>重新启动, 并从新安装的分区中启动系统。</para> - </step> - - <step> - <para>如果一切顺利的话, 升级工作就完成了。</para> - </step> - - <step> - <para>如果发生了任何问题, 则可以从先前的分区启动 - (其中包含了旧的、 可用的映像), 来尽可能快地恢复系统功能。 - 接下来可以修正新联编的版本中存在的问题, 并重复前述步骤。</para> - </step> - </procedure> - - <para>要在正在运行的 - <application>NanoBSD</application> 系统中安装新的映像, 可以使用位于 - <filename>/root</filename> 目录的 - <filename>updatep1</filename> 或 - <filename>updatep2</filename> 脚本, - 具体使用哪一个脚本, 取决于正在运行的系统位于那个分区。</para> - - <para>随时提供新 <application>NanoBSD</application> 映像所提供的服务, - 以及采用的传输方法的不同, 您可以参考并使用下列三种方式之一:</para> - - <sect3> - <title>使用 &man.ftp.1;</title> - - <para>如果传输速度是第一要务, - 采用下面的例子:</para> - - <screen>&prompt.root; <userinput>ftp myhost -get _.disk.image "| sh updatep1"</userinput></screen> - </sect3> - - <sect3> - <title>使用 &man.ssh.1;</title> - - <para>如果更倾向于安全传输, 应参考下面的例子:</para> - - <screen>&prompt.root; <userinput>ssh myhost cat _.disk.image.gz | zcat | sh updatep1</userinput></screen> - </sect3> - - <sect3> - <title>使用 &man.nc.1;</title> - - <para>如果远程主机既不提供 - &man.ftp.1; 服务, 也不提供 &man.sshd.8; 服务:</para> - - <procedure> - <step> - <para>开始时, 在提供映像的主机上开启 TCP 监听, - 并令其将映像文件发给客户机:</para> - - <screen>myhost&prompt.root; <userinput>nc -l 2222 < _.disk.image</userinput></screen> - - <note> - <para>请确认您所使用的端口没有通过防火墙阻止来自 - <application>NanoBSD</application> 客户机的联接请求。</para> - </note> - </step> - <step> - <para>连接到提供新映像服务的主机, 并执行 - <filename>updatep1</filename> 脚本:</para> - - <screen>&prompt.root; <userinput>nc myhost 2222 | sh updatep1</userinput></screen> - </step> - </procedure> - </sect3> - </sect2> - </sect1> - - <index/> -</article> diff --git a/zh_CN.GB2312/articles/rc-scripting/article.xml b/zh_CN.GB2312/articles/rc-scripting/article.xml deleted file mode 100644 index a76bd721d8..0000000000 --- a/zh_CN.GB2312/articles/rc-scripting/article.xml +++ /dev/null @@ -1,1219 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - Original Revision: 1.14 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>BSD rc.d脚本编程实战</title> - - - <author><personname><firstname>Yar</firstname><surname>Tikhiy</surname></personname><affiliation> - <address><email>yar@FreeBSD.org</email></address> - </affiliation></author> - - <copyright> - <year>2005</year> - <year>2006</year> - <year>2012</year> - - <holder>The FreeBSD Project</holder> - </copyright> - - <pubdate>$FreeBSD$</pubdate> - - <releaseinfo>$FreeBSD$</releaseinfo> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.netbsd; - &tm-attrib.general; - </legalnotice> - - <abstract> - <para>初学者可能会发现,难以通过正式的文档, - 基于 BSD 的 <filename>rc.d</filename> - 框架,编写一些实际任务的 <filename>rc.d</filename> 脚本。 - 本文中,我们采用了一些复杂性不断增加的典型案例, - 来展示适合每个案例的 <filename>rc.d</filename> 特性, - 并探讨其中的工作原理。 - 这样的实验为大家进一步研究设计有效的 - <filename>rc.d</filename> 应用程序提供了一些参考点。</para> - </abstract> - </info> - - <sect1 xml:id="rcng-intro"> - <title>简介</title> - - <para>历史上 BSD 曾有过一个单一的启动脚本, - <filename>/etc/rc</filename>。 该脚本在系统启动的时候被 - &man.init.8; 程序所引导,并执行所有多用户操作所需求的用户级任务: - 检查并挂载文件系统,设置网络,启动守护进程,等等。 - 在每个系统中实际的任务清单也并不相同; - 管理员需要根据需求自定义这样的任务清单。在一些特殊的情况中, - 还不得不去修改 <filename>/etc/rc</filename> 文件, - 一些真正的黑客乐此不疲。</para> - - <para>单一脚本启动方法的真正问题是它没有提供对从 - <filename>/etc/rc</filename> 启动的单个组件的控制。 - 拿一个例子来说吧,<filename>/etc/rc</filename> - 不能够重新启动某个单独的守护进程。 - 系统管理员不得不手动找出守护进程,并杀掉它, - 等待它真正退出后,再通过浏览 <filename>/etc/rc</filename> - 得到该守护进程的标识,最终输入全部命令来再次启动守护进程。 - 如果重新启动的服务包括不止一个守护进程或需要更多动作的话, - 该任务将变得更加困难以及容易出错。简而言之, - 单一脚本在实现我们这样的目的上是不成功的: - 让系统管理员的生活更轻松。</para> - - <para>再后来,为了将最重要的一些子系统独立出来, - 便尝试将部分的内容从 <filename>/etc/rc</filename> 分离出来了。 - 最广为人知的例子就是用来启动联网的 <filename>/etc/netstart</filename> - 文件。它容许从单用户模式访问网络, - 但由于它的部分代码需要和一些与联网完全无关的动作交互, - 所以它并没有完美地结合到自启动的进程中。那便是为何 - <filename>/etc/netstart</filename> 被演变成 - <filename>/etc/rc.network</filename> 的原因了。 - 后者不再是一个普通的脚本;它包括了庞大的,由 - <filename>/etc/rc</filename> 在不同的系统启动级别中调用的凌乱的 - &man.sh.1; 函数。然而,当启动任务变得多样化以及久经更改, - <quote>类模块化</quote> 方法变得比曾经的整体 - <filename>/etc/rc</filename> 更缓慢费事。</para> - - <para>由于没有一个干净和易于设计的框架, - 启动脚本不得不全力更改以满足飞速开发中基于 BSD 的操作系统的需求。 - 它逐渐变得明朗并经过许多必要的步骤最终变成一个具有细密性和扩展性的 - <filename>rc</filename> 系统。BSD <filename>rc.d</filename> - 就这样诞生了。Luke Mewburn 和 NetBSD 社区是公认的 - <filename>rc.d</filename> 之父。再之后它被引入到了 &os; 中。 - 它的名字引用为系统单独的服务脚本的位置,也就是 - <filename>/etc/rc.d</filename>下面的那些脚本。 - 之后我们将学习到更多的 <filename>rc.d</filename> - 系统的组件并看看单个脚本是如何被调用的。</para> - - <para>BSD <filename>rc.d</filename> - 背后的基本理念是 <emphasis>良好</emphasis> 的模块化和代码重用性。 - <emphasis>良好</emphasis> 的模块化意味着每个基本 - <quote>服务</quote> 就象系统守护进程或原始启动任务那样, - 通过属于它们的可启动该服务的 &man.sh.1; 脚本,来停止服务, - 重载服务,检查服务的状态。具体动作由脚本的命令行参数所决定。 - <filename>/etc/rc</filename> 脚本仍然掌管着系统的启动, - 但现在它仅仅是使用 <option>start</option> 参数来一个个调用那些小的脚本。 - 这便于用 <option>stop</option> 来对运行中的同样的脚本很好地执行停止任务, - 这是被 <filename>/etc/rc.shutdown</filename> - 脚本所完成的。看,这是多么好地体现了 Unix 的哲学: - 拥有一组小的专用的工具,每个工具尽可能好地完成自己的任务。 - <emphasis>代码重用</emphasis> 意味着所有的通用操作由 - <filename>/etc/rc.subr</filename> 中的一些 &man.sh.1; 函数所实现。 - 现在一个典型的脚本只需要寥寥几行的 &man.sh.1; 代码。最终, - &man.rcorder.8; 成为了 <filename>rc.d</filename> 框架中重要的一部分, - 它用来帮助 <filename>/etc/rc</filename> - 处理小脚本之间的依赖关系并有次序地运行它们。它同样帮助 - <filename>/etc/rc.shutdown</filename> 做类似的事情, - 因为正确的关闭次序是相对于启动的次序的。</para> - - <para>BSD <filename>rc.d</filename> 的设计在 - <link linkend="lukem"> Luke Mewburn 的原文 </link> 中有记录, - 以及 <filename>rc.d</filename> 组件也被充分详细地记录在各自的 - <link linkend="manpages">联机手册</link> 中。然而, - 它可能没能清晰展现给一个 <filename>rc.d</filename> - 新手,如何将无数的块和片进行关联来为具体的任务创建一个好风格的脚本。 - 因此本文将试着以不同的方式来讲述 <filename>rc.d</filename>。 - 它将展示在某些典型情况中应该使用哪些特性,并阐述了为何如此。 - 注意这并不是一篇 how-to 文档,我们的目的不是给出现成的配方, - 而是在展示一些简单的进入 <filename>rc.d</filename> 的范围的门路。 - 本文也不是相关联机手册的替代品。 - 阅读本文时记得同时参考联机手册以获取更完整正规的文档。</para> - - <para>理解本文需要一些先决条件。首先,你需要熟悉 - &man.sh.1; 脚本编程语言以掌握 <filename>rc.d</filename>, - 还有,你需要知道系统是如何执行用户级的启动和停止任务,这些在 - &man.rc.8; 中都有说明。</para> - - <para>本文关注的是 <filename>rc.d</filename> 的 &os; 分支。 - 不过,它可能对 NetBSD 的开发者也同样有用,因为 BSD - <filename>rc.d</filename> 的两个分支不只是共享了同样的设计, - 还保留了对脚本编写者都可见的类似观点。</para> - </sect1> - - <sect1 xml:id="rcng-task"> - <title>任务描述</title> - - <para>在开始打开 <envar>$EDITOR</envar>(编辑器) - 之前进行小小的思考不是坏事。为了给一个系统服务写一个 - <quote>听话的</quote> <filename>rc.d</filename> 脚本, - 我们首先应该能回答以下问题:</para> - - <itemizedlist> - <listitem> - <para>该服务是必须性的还是可选性的?</para> - </listitem> - - <listitem> - <para>脚本将为单个程序服务,如一个守护进程,还是执行更复杂的动作?</para> - </listitem> - - <listitem> - <para>我们的服务依赖哪些服务?反过来哪些服务依赖我们的服务?</para> - </listitem> - </itemizedlist> - - <para>从下面的例子中我们将看到,为什么说知道这些问题的答案是很重要的。</para> - </sect1> - - <sect1 xml:id="rcng-dummy"> - <title>虚拟的脚本</title> - - <para>下面的脚本是用来在每次系统启动时发出一个信息:</para> - - <informalexample> - <programlisting>#!/bin/sh<co xml:id="rcng-dummy-shebang"/> - -. /etc/rc.subr<co xml:id="rcng-dummy-include"/> - -name="dummy"<co xml:id="rcng-dummy-name"/> -start_cmd="${name}_start"<co xml:id="rcng-dummy-startcmd"/> -stop_cmd=":"<co xml:id="rcng-dummy-stopcmd"/> - -dummy_start()<co xml:id="rcng-dummy-startfn"/> -{ - echo "Nothing started." -} - -load_rc_config $name<co xml:id="rcng-dummy-loadconfig"/> -run_rc_command "$1"<co xml:id="rcng-dummy-runcommand"/></programlisting> - </informalexample> - - <para>需要注意的是:</para> - - <calloutlist> - <callout arearefs="rcng-dummy-shebang"> - <para>一个解释性的脚本应该以一行魔幻的 - <quote>shebang</quote> 行开头。 - 该行指定了脚本的解析程序。由于 shebang 行的作用, - 假如再有可执行位的设置, - 脚本就能象一个二进制程序一样被精确地调用执行。 - (请参考 &man.chmod.1;。) 例如, - 一个系统管理员可以从命令行手动运行我们的脚本:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/dummy start</userinput></screen> - - <note> - <para>为了使 <filename>rc.d</filename> 框架正确地管理脚本, - 它的脚本需要用 &man.sh.1; 语言编写。 - 如果你的某个服务或 port 套件使用了二进制控制程序或是用其它语言编写的例程, - 请将其组件安装到 <filename>/usr/sbin</filename>(相对于系统) - 或 <filename>/usr/local/sbin</filename>(相对于ports), - 然后从合适的 <filename>rc.d</filename> 目录的某个 - &man.sh.1; 脚本调用它。</para> - </note> - - <tip> - <para>如果你想知道为什么 <filename>rc.d</filename> - 脚本必须用 &man.sh.1; 语言编写的细节,先看下 - <filename>/etc/rc</filename> 是如何依靠 - <function>run_rc_script</function> 调用它们, - 然后再去学习 <filename>/etc/rc.subr</filename> - 下 <function>run_rc_script</function> - 的相关实现。</para> - </tip> - </callout> - - <callout arearefs="rcng-dummy-include"> - <para>在 <filename>/etc/rc.subr</filename> 下, - 有许多定义过的 &man.sh.1; 函数可供每个 - <filename>rc.d</filename> 脚本使用。这些函数在 - &man.rc.subr.8; 中都有说明。尽管理论上可以完全不使用 - &man.rc.subr.8; 来编写一个 <filename>rc.d</filename> - 脚本,但它的函数已经证明了它真的很方便, - 并且能使任务更加的简单。所以所有人在编写 - <filename>rc.d</filename> 脚本时都会求助于 - &man.rc.subr.8; 也不足为奇了。当然我们也不例外。</para> - - <para>一个 <filename>rc.d</filename> 脚本在其调用 - &man.rc.subr.8; 函数之前必须先 <quote>source</quote> - <filename>/etc/rc.subr</filename>(用 - <quote><command>.</command></quote>将其包含进去), - 而使 &man.sh.1; 程序有机会来获悉那些函数。 - 首选风格是在脚本的最开始 source - <filename>/etc/rc.subr</filename> 文件。</para> - - <note> - <para>某些有用的与联网有关的函数由另一个被包含进来的文件提供, - <filename>/etc/network.subr</filename> 文件。</para> - </note> - </callout> - - <callout arearefs="rcng-dummy-name"> - <para><anchor xml:id="name-var"/>强制的变量 - <envar>name</envar> 指定我们脚本的名字。 - 这是 &man.rc.subr.8; 所强调的。也就是, - 每个 <filename>rc.d</filename> 脚本在调用 - &man.rc.subr.8; 的函数之前必须设置 - <envar>name</envar> 变量。</para> - - <para>现在是时候来为我们的脚本一次性选择一个独一无二的名字了。 - 在编写这个脚本的时我们将在许多地方用到它。在开始之前, - 我们来给脚本文件也取个相同的名字。</para> - - <note> - <para>当前的 <filename>rc.d</filename> - 脚本风格是把值放在双引号中来给变量赋值。 - 请记住这只是个风格问题,可能并不总是这样。 - 你可以在只是简单的并不包括 &man.sh.1; - 元字符的词句中放心地省略掉引号, - 而在某些情况下你将需要使用单引号以防止 - &man.sh.1; 对任何的变量的解释。 - 程序员是可以灵巧地由风格惯例获悉其语法以及使用的。 - </para> - </note> - </callout> - - <callout arearefs="rcng-dummy-startcmd"> - <para>&man.rc.subr.8; 背后主要的构思是 - <filename>rc.d</filename> 脚本提供处理程序,或者方法,来让 - &man.rc.subr.8; 调用。特别是,<option>start</option>, - <option>stop</option>,以及其它的 <filename>rc.d</filename> - 脚本参数都是这样被处理的。方法是存储在一个以 - <envar><replaceable>argument_cmd</replaceable></envar> - 形式命名的变量中的 &man.sh.1; 表达式,该 - <replaceable>argument</replaceable> - 对应着脚本命令行中所特别指定的参数。我们稍后将看到 - &man.rc.subr.8; 是如何为标准参数提供默认方法的。</para> - - <note> - <para>为了让 <filename>rc.d</filename> 中的代码更加统一, - 常见的是在任何适合的地方都使用 <envar>${name}</envar> 形式。 - 这样一来,可以轻松地将一些代码从一个脚本拷贝到另一个中使用。</para> - </note> - </callout> - - <callout arearefs="rcng-dummy-stopcmd"> - <para>我们应谨记 &man.rc.subr.8; 为标准参数提供了默认的方法。 - 因此,如果希望它什么都不做的话,我们必须使用无操作的 - &man.sh.1; 表达式来改写标准的方法。</para> - </callout> - - <callout arearefs="rcng-dummy-startfn"> - <para>比较复杂的方法主体可以用函数来实现。 - 在能够保证函数名有意义的情况下,这是个很不错的想法。</para> - - <important> - <para>强烈推荐给我们脚本中所定义的所有函数名都添加类似 - <envar>${name}</envar> 这样的前缀,以使它们永远不会和 - &man.rc.subr.8; 或其它公用包含文件中的函数冲突。</para> - </important> - </callout> - - <callout arearefs="rcng-dummy-loadconfig"> - <para>这是在请求 &man.rc.subr.8; 载入 &man.rc.conf.5; 变量。 - 尽管我们这个脚本中使用的变量并没有被其它地方使用,但由于 - &man.rc.subr.8; 自身所控制着的 &man.rc.conf.5; - 变量存在的原因,仍然推荐脚本去装载 &man.rc.conf.5;。</para> - </callout> - - <callout arearefs="rcng-dummy-runcommand"> - <para>通常这是 <filename>rc.d</filename> 脚本的最后一个命令。 - 它调用 &man.rc.subr.8; - 体系使用我们脚本所提供的变量和方法来执行相应的请求动作。</para> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-confdummy"> - <title>可配置的虚拟脚本</title> - - <para>现在我们来给我们的虚拟脚本增加一些控制参数吧。正如你所知, - <filename>rc.d</filename> 脚本是由 &man.rc.conf.5; 所控制的。 - 幸运的是,&man.rc.subr.8; 隐藏了所有复杂化的东西。 - 下面这个脚本使用 &man.rc.conf.5; 通过 &man.rc.subr.8; - 来查看它是否在第一个地方被启用,并获取一条信息在启动时显示。 - 事实上这两个任务是相互独立的。一方面,<filename>rc.d</filename> - 脚本要能够支持启动和禁用它的服务。另一方面, - <filename>rc.d</filename> 脚本必须能具备配置信息变量。 - 我们将通过下面同一脚本来演示这两方面的内容:</para> - - <informalexample> - <programlisting>#!/bin/sh - -. /etc/rc.subr - -name=dummy -rcvar=dummy_enable<co xml:id="rcng-confdummy-rcvar"/> - -start_cmd="${name}_start" -stop_cmd=":" - -load_rc_config $name<co xml:id="rcng-confdummy-loadconfig"/> -eval "${rcvar}=\${${rcvar}:-'NO'}"<co xml:id="rcng-confdummy-enable"/> -dummy_msg=${dummy_msg:-"Nothing started."}<co xml:id="rcng-confdummy-opt"/> - -dummy_start() -{ - echo "$dummy_msg"<co xml:id="rcng-confdummy-msg"/> -} - -run_rc_command "$1"</programlisting> - </informalexample> - - <para>在这个样例中改变了什么?</para> - - <calloutlist> - <callout arearefs="rcng-confdummy-rcvar"> - <para>变量 <envar>rcvar</envar> 指定了 ON/OFF - 开关变量的名字。</para> - </callout> - - <callout arearefs="rcng-confdummy-loadconfig"> - <para>现在 <function>load_rc_config</function> 在任何 - &man.rc.conf.5; 变量被访问之前就在脚本中被预先调用。</para> - - <note> - <para>检查 <filename>rc.d</filename> 脚本时,切记 &man.sh.1; - 会把函数延迟到其被调用时才对其中的表达式进行求值运算。 - 因此尽可能晚地在 <function>run_rc_command</function> - 之前调用 <function>load_rc_config</function>, - 以及仍然访问从方法函数输出到 - <function>run_rc_command</function> 的 &man.rc.conf.5; - 变量并不是一个错误。这是因为方法函数将在 - <function>load_rc_config</function> <emphasis>之后</emphasis>, - 被调用的 <function>run_rc_command</function> 调用。</para> - </note> - </callout> - - <callout arearefs="rcng-confdummy-enable"> - <para>如果自身设置了 <envar>rcvar</envar>, - 但指示开关变量却未被设置,那么 <function>run_rc_command</function> - 将发出一个警告。如果你的 <filename>rc.d</filename> - 脚本是为基本系统所用的,你应当在 - <filename>/etc/defaults/rc.conf</filename> - 中给开关变量添加一个默认的设置并将其标注在 &man.rc.conf.5; 中。 - 否则的话你的脚本应该给开关变量提供一个默认设置。 - 范例中演示了一个可移植接近于后者情况的案例。</para> - - <note> - <para>你可以通过将开关变量设置为 ON 来使 &man.rc.subr.8; 有效, - 使用 <literal>one</literal> 或 <literal>force</literal> - 为脚本的参数加前缀,如 <literal>onestart</literal> 或 - <literal>forcestop</literal> 这样,会忽略其当前的设置。 - 切记 <literal>force</literal> - 在我们下面要提到的情况下有额外的危险后果,那就是当用 - <literal>one</literal> 改写了 ON/OFF 开关变量。例如, - 假定 <envar>dummy_enable</envar> 是 <literal>OFF</literal> - 的,而下面的命令将忽略系统设置而强行运行 - <option>start</option>方法:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/dummy onestart</userinput></screen> - </note> - </callout> - - <callout arearefs="rcng-confdummy-opt"> - <para>现在启动时显示的信息不再是硬编码在脚本中的了。 - 它是由一个命名为 <envar>dummy_msg</envar> 的 - &man.rc.conf.5; 变量所指定的。这就是 &man.rc.conf.5; - 变量如何来控制 <filename>rc.d</filename> - 脚本的一个小例子。</para> - - <important> - <para>我们的脚本所独占使用的所有 &man.rc.conf.5; 变量名, - 都必须具有同样的前缀:<envar>${name}</envar>。 - 例如:<envar>dummy_mode</envar>, - <envar>dummy_state_file</envar>,等等。</para> - </important> - - <note> - <para>当可以内部使用一个简短的名字时,如 <envar>msg</envar> - 这样,为我们的脚本所引进的全部的共用名添加唯一的前缀 - <envar>${name}</envar>,能够避免我们与 &man.rc.subr.8; - 命名空间冲突的可能。</para> - - <para>只要一个 &man.rc.conf.5; 变量与其内部等同值是相同的, - 我们就能够使用一个更加兼容的表达式来设置默认值:</para> - - <programlisting>: ${dummy_msg:="Nothing started."}</programlisting> - - <para>尽管目前的风格是使用了更详细的形式。</para> - - <para>通常,基本系统的 <filename>rc.d</filename> - 脚本不需要为它们的 &man.rc.conf.5; 变量提供默认值, - 因为默认值应该是在 <filename>/etc/defaults/rc.conf</filename> - 设置过了。但另一方面,为 ports 所用的 <filename>rc.d</filename> - 脚本应提供如范例所示的默认设置。</para> - </note> - </callout> - - <callout arearefs="rcng-confdummy-msg"> - <para>这里我们使用 <envar>dummy_msg</envar> - 来实际地控制我们的脚本,即,发一个变量信息。</para> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-daemon"> - <title>启动并停止简单守护进程</title> - - <para>我们早先说过 &man.rc.subr.8; 是能够提供默认方法的。 - 显然,这些默认方法并不是太通用的。 - 它们都是适用于大多数情况下来启动和停止一个简单的守护进程况。 - 我们来假设现在需要为一个叫做 <command>mumbled</command> - 的守护进程编写一个 <filename>rc.d</filename>脚本, - 在这里:</para> - - <informalexample> - <programlisting>#!/bin/sh - -. /etc/rc.subr - -name=mumbled -rcvar=mumbled_enable - -command="/usr/sbin/${name}"<co xml:id="rcng-daemon-basic-cmd"/> - -load_rc_config $name -run_rc_command "$1"</programlisting> - </informalexample> - - <para>感到很简单吧,不是么?我们来检查下我们这个小脚本。 - 只需要注意下面的这些新知识点:</para> - - <calloutlist> - <callout arearefs="rcng-daemon-basic-cmd"> - <para>这个 <envar>command</envar> 变量对于 - &man.rc.subr.8; 来说是有意义的。当它被设置时, - &man.rc.subr.8; 将根据提供传统守护进程的情形而生效。 - 特别是,将为这些参数提供默认的方法: - <option>start</option>,<option>stop</option>, - <option>restart</option>,<option>poll</option>, - 以及 <option>status</option>。</para> - - <para>该守护进程将会由运行中的 <envar>$command</envar> - 配合由 <envar>$mumbled_flags</envar> 所指定的命令行标帜来启动。 - 因此,对默认的 <option>start</option> 方法来说, - 所有的输入数据在我们脚本变量集合中都可用。与 - <option>start</option> 不同的是, - 其他方法可能需要与进程启动相关的额外信息。举个例子, - <option>stop</option> 必须知道进程的 PID 号来终结进程。 - 在目前的情况中,&man.rc.subr.8; 将扫描全部进程的清单, - 查找一个名字等同于 <envar>$procname</envar> 的进程。 - 后者是另一个对 &man.rc.subr.8; 有意义的变量, - 并且默认它的值跟 <envar>command</envar> 一样。 - 换而言之,当我们给 <envar>command</envar> 设置值后, - <envar>procname</envar> 实际上也设置了同样的值。 - 这启动我们的脚本来杀死守护进程并检查它是否正在第一个位置运行。</para> - - <note> - <para>某些程序实际上是可执行的脚本。 - 系统启动脚本的解释器以传递脚本名为命令行参数的形式来运行脚本。 - 然后被映射到进程列表中,这会使 &man.rc.subr.8; 迷惑。因此,当 - <envar>$command</envar> 是一个脚本的时,你应该额外地设置 - <envar>command_interpreter</envar> 来让 &man.rc.subr.8; - 知晓进程的实际名字。</para> - - <para>对每个 <filename>rc.d</filename> 脚本而言, - 有一个可选的 &man.rc.conf.5; 变量给 - <envar>command</envar> 指示其优先级。 - 它的名字是下面这样的形式:<envar>${name}_program</envar>, - <envar>name</envar> 是我们 <link linkend="name-var">之前</link> - 讨论过的必须性变量。如,在这个案例中它应该命名为 - <envar>emumbled_program</envar>。这其实是 &man.rc.subr.8; - 分配 <envar>${name}_program</envar> 来改写 - <envar>command</envar> 的。</para> - - <para>当然,即使 <envar>command</envar> 未被设置, - &man.sh.1; 也将允许你从 &man.rc.conf.5; 或自身来设置 - <envar>${name}_program</envar>。在那种情况下, - <envar>${name}_program</envar> 的特定属性丢失了, - 并且它成为了一个能供你的脚本用于其自身目的的普通变量。 - 然而,单独使用 <envar>${name}_program</envar> - 是并不是我们所寄望的,因为同时使用它和 <envar>command</envar> - 已成为了 <filename>rc.d</filename> 脚本编程的一个惯用的约定。</para> - </note> - - <para>关于默认方法的更详细的信息,请参考 - &man.rc.subr.8;。</para> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-daemon-adv"> - <title>启动并停止高级守护进程</title> - - <para>我们来给之前的 <quote>骨架</quote> - 脚本加点 <quote>血肉</quote>,并让它更复杂更富有特性吧。 - 默认的方法已能够为我们做很好的工作了, - 但是我们可能会需要它们一些方面的调整。 - 现在我们将学习如何调整默认方法来符合我们的需要。</para> - - <informalexample> - <programlisting>#!/bin/sh - -. /etc/rc.subr - -name=mumbled -rcvar=mumbled_enable - -command="/usr/sbin/${name}" -command_args="mock arguments > /dev/null 2>&1"<co xml:id="rcng-daemon-adv-args"/> - -pidfile="/var/run/${name}.pid"<co xml:id="rcng-daemon-adv-pid"/> - -required_files="/etc/${name}.conf /usr/share/misc/${name}.rules"<co xml:id="rcng-daemon-adv-reqfiles"/> - -sig_reload="USR1"<co xml:id="rcng-daemon-adv-sig"/> - -start_precmd="${name}_prestart"<co xml:id="rcng-daemon-adv-precmd"/> -stop_postcmd="echo Bye-bye"<co xml:id="rcng-daemon-adv-postcmd"/> - -extra_commands="reload plugh xyzzy"<co xml:id="rcng-daemon-adv-extra"/> - -plugh_cmd="mumbled_plugh"<co xml:id="rcng-daemon-adv-methods"/> -xyzzy_cmd="echo 'Nothing happens.'" - -mumbled_prestart() -{ - if checkyesno mumbled_smart; then<co xml:id="rcng-daemon-adv-yn"/> - rc_flags="-o smart ${rc_flags}"<co xml:id="rcng-daemon-adv-rcflags"/> - fi - case "$mumbled_mode" in - foo) - rc_flags="-frotz ${rc_flags}" - ;; - bar) - rc_flags="-baz ${rc_flags}" - ;; - *) - warn "Invalid value for mumbled_mode"<co xml:id="rcng-daemon-adv-warn"/> - return 1<co xml:id="rcng-daemon-adv-preret"/> - ;; - esac - run_rc_command xyzzy<co xml:id="rcng-daemon-adv-run"/> - return 0 -} - -mumbled_plugh()<co xml:id="rcng-daemon-adv-plugh"/> -{ - echo 'A hollow voice says "plugh".' -} - -load_rc_config $name -run_rc_command "$1"</programlisting> - </informalexample> - - <calloutlist> - <callout arearefs="rcng-daemon-adv-args"> - <para>附加给 <envar>$command</envar> 的参数在 - <envar>command_args</envar> 中进行传递。它们在 - <envar>$mumbled_flags</envar> 之后将被添加到命令行。 - 其实际的执行便是此后最终的命令行传递给 - <command>eval</command> 运算,输入和输出以及重定向都可以在 - <envar>command_args</envar> 中指定。</para> - - <note> - <para><emphasis>永远不要</emphasis> 在 - <envar>command_args</envar> 包含破折号选项, - 类似 <option>-X</option> 或 <option>--foo</option> - 这样的。<envar>command_args</envar> - 的内容将出现在最终命令行的末尾,因此它们可能是紧接在 - <envar>${name}_flags</envar> 中所列出的参数后面; - 但大多的命令将不能识别出普通参数后的破折号选项。 - 更好的传递附加给 <envar>$command</envar> - 的选项的方式是添加它们到 <envar>${name}_flags</envar> - 的起始处。另一种方法是像后文所示的那样来修改 - <envar>rc_flags</envar>。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-pid"> - <para>一个得体的守护进程会创建一个 - <emphasis>pidfile</emphasis> 进程文件, - 以使其进程能够更容易更可靠地被找到。如果设置了 - <envar>pidfile</envar> 变量,告诉 &man.rc.subr.8; - 哪里能找到供其默认方法所使用的 - <envar>pidfile</envar> 进程文件。</para> - - <note> - <para>事实上,&man.rc.subr.8; - 在启动一个守护进程前还会使用 pidfile - 进程文件来查看它是否已经在运行。使用了 - <option>faststart</option> 参数可以跳过这个检查步骤。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-reqfiles"> - <para>如果守护进程只有在确定的文件存在的情况下才可以运行, - 那就将它们列到 <envar>required_files</envar> 中,而 - &man.rc.subr.8; 将在启动守护进程之前检查那些文件是否存在。 - 还有相关的分别用来检查目录和环境变量的 - <envar>required_dirs</envar> 和 <envar>required_vars</envar> - 可供使用。它们都在 &man.rc.subr.8; 中有详细的说明。</para> - - <note> - <para>来自 &man.rc.subr.8; 的默认方法,通过使用 - <option>forcestart</option> 作为脚本的参数, - 可以强制性地跳过预先需要的检查。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-sig"> - <para>我们可以在守护进程有异常的时候,自定义发送给守护进程的信号。 - 特别是,<envar>sig_reload</envar> - 指定了使守护进程重新装载其配置的信号;默认情况也就是 - <symbol>SIGHUP</symbol> 信号。 - 另一个信号是发送给守护进程以停止该进程;默认情况下是 - <symbol>SIGTERM</symbol> 信号,但这是可以通过设置 - <envar>sig_stop</envar> 来进行适当更改的。</para> - - <note> - <para>信号名称应当以不包含 <literal>SIG</literal> - 前缀的形式指定给 &man.rc.subr.8;,就如范例中所示的那样。 - &os; 版本的 &man.kill.1; 程序能够识别出 - <literal>SIG</literal> 前缀,不过其它系统版本的就不一定了。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-precmd rcng-daemon-adv-postcmd"> - <para>在默认的方法前面或后面执行附加任务是很容易的。 - 对于我们脚本所支持的每条命令参数而言,我们可以定义 - <envar><replaceable>argument</replaceable>_precmd</envar> 和 - <envar><replaceable>argument</replaceable>_postcmd</envar> - 来完成。这些 &man.sh.1; 命令分别在它们各自的方法前后被调用, - 显然,从它们各自的名字就能看出来。</para> - - <note> - <para>如果我们需要的话,用自定义的 - <envar><replaceable>argument</replaceable>_cmd</envar> - 改写默认的方法,并不妨碍我们仍然使用 - <envar><replaceable>argument</replaceable>_precmd</envar> 和 - <envar><replaceable>argument</replaceable>_postcmd</envar>。 - 特别是,前者便于检查自定义的方法, - 以及执行自身命令之前所遇到更严密的条件。于是,将 - <envar><replaceable>argument</replaceable>_precmd</envar> 和 - <envar><replaceable>argument</replaceable>_cmd</envar> - 一起使用,使我们合理地将检查从动作中独立了出来。</para> - - <para>别忘了你可以将任意的有效的 &man.sh.1; - 表达式插入到方法和你定义的 pre- 与 post-commands 命令中。 - 在大部分情况下,调用函数使实际任务有好的风格, - 但千万不要让风格限制了你对其幕后到底是怎么回事的思考。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-extra"> - <para>如果我们愿意实现一些自定义参数, - 这些参数也可被认作为我们脚本的 <emphasis>命令</emphasis>,我们需要在 - <envar>extra_commands</envar> 中将它们列出并提供方法以处理它们。</para> - - <note> - <para><option>reload</option> 是个特别的命令。一方面, - 它有一个在 &man.rc.subr.8; 中预置的方法。另一方面, - <option>reload</option> 命令默认是不被提供的。 - 理由是并非所有的守护进程都使用同样的重载方法, - 并且有些守护进程根本没有任何东西可重载的。所以显而易见, - 我们需要去询问都提供了哪些的内建功能。我们可以通过 - <envar>extra_commands</envar> 来这样做。</para> - - <para>我们从 <option>reload</option> 的默认方法得到了什么呢? - 守护进程常常在收到一个信号后重新载入它们的配置 — - 一般来说,也就是 <symbol>SIGHUP</symbol> 信号。因此 - &man.rc.subr.8; 尝试发送一个信号给守护进程来重载它。 - 该信号一般预设为 <symbol>SIGHUP</symbol>, - 但是如果必要的话可以通过 <envar>sig_reload</envar> - 变量来自定义它。</para> - </note> - </callout> - - <callout arearefs="rcng-daemon-adv-methods rcng-daemon-adv-plugh"> - <para>我们的脚本提供了两个非标准的命令, - <option>plugh</option> 和 <option>xyzzy</option>。 - 我们看到它们在 <envar>extra_commands</envar> 中被列出来了, - 并且现在是时候给它们提供方法了。<option>xyzzy</option> - 的方法是内联的而 <option>plugh</option> 的是以 - <function>mumbled_plugh</function> 形式完成的函数。</para> - - <para>非标准命令在启动或停止的时候不被调用。 - 通常它们是为了系统管理员的方便。它们还能被其它的子系统所使用, - 例如,&man.devd.8;,前提是 &man.devd.conf.5; 中已经指定了。</para> - - <para>全部可用命令的列表,当脚本不加参数地调用时,在 - &man.rc.subr.8; 打印出的使用方法中能够找到。例如, - 这就是供学习的脚本用法的内容:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/mumbled</userinput> -Usage: /etc/rc.d/mumbled [fast|force|one](start|stop|restart|rcvar|reload|plugh|xyzzy|status|poll)</screen> - </callout> - - <callout arearefs="rcng-daemon-adv-run"> - <para>如果脚本需要的话,它可以调用自己的标准或非标准的命令。 - 这可能看起来有点像函数的调用,但我们知道,命令和 shell - 函数并非一直都是同样的东西。举个例子,<command>xyzzy</command> - 在这里不是以函数来实现的。另外,还有应该被有序调用的 - pre-command 预置命令和 post-command 后置命令。 - 所以脚本运行自己命令的合适方式就是利用 &man.rc.subr.8;, - 就像范例中展示的那样。</para> - </callout> - - <callout arearefs="rcng-daemon-adv-yn"> - <para>&man.rc.subr.8; 提供了一个方便的函数叫做 - <function>checkyesno</function>。 - 它以一个变量名作为参数并返回一个为零的退出值, - 当且仅当该变量设置为 <literal>YES</literal>,或 - <literal>TRUE</literal>,或 <literal>ON</literal>,或 - <literal>1</literal>,区分大小写;否则返回一个非零的退出值。 - 在第二种情况中,函数测试变量的设置为 <literal>NO</literal>, - <literal>FALSE</literal>,<literal>OFF</literal>,或 - <literal>0</literal>,区分大小写; - 如果变量包含别的内容的话它打印一条警告信息,例如,垃圾。</para> - - <para>切记对 &man.sh.1; 而言零值意味着真而非零值意味着假。</para> - - <important> - <para><function>checkyesno</function> 函数使用一个 - <emphasis>变量名</emphasis>。不要扩大含义将变量的 - <emphasis>值</emphasis> 传递给它; - 否则它不会如你预期那样的工作。</para> - - <para>下面是 <function>checkyesno</function> - 的合理使用范围:</para> - - <programlisting>if checkyesno mumbled_enable; then - foo -fi</programlisting> - - <para>相反地,以下面的方式调用 <function>checkyesno</function> - 是不会工作的 -- 至少是不会如你预期的那样:</para> - - <programlisting>if checkyesno "${mumbled_enable}"; then - foo -fi</programlisting> - </important> - </callout> - - <callout arearefs="rcng-daemon-adv-rcflags"> - <para>我们可以通过修改 <envar>$start_precmd</envar> 中的 - <envar>rc_flags</envar> 来影响传递到 - <envar>$command</envar> 的标帜。</para> - </callout> - - <callout arearefs="rcng-daemon-adv-warn"> - <para>某种情况下我们可能需要发出一条重要的信息,那样的话 - <application>syslog</application> 可以很好地记录日志。 - 这可以使用下列 &man.rc.subr.8; 函数来轻松完成: - <function>debug</function>,<function>info</function>, - <function>warn</function>,以及 <function>err</function>。 - 后者以指定的代码值退出脚本。</para> - </callout> - - <callout arearefs="rcng-daemon-adv-preret"> - <para>方法的退出值和它们的 pre-commands 预命令不只是默认被忽略掉。如果 - <envar><replaceable>argument</replaceable>_precmd</envar> - 返回了一个非零退出值,主方法将不会被执行。依次地是, - <envar><replaceable>argument</replaceable>_postcmd</envar> - 将不会被调用,除非主方法返回的是一个为零的退出值。</para> - - <note> - <para>然而,当给一个参数使用 <literal>force</literal> - 前缀的时候,如 <option>forcestart</option>,&man.rc.subr.8; - 会听从命令行指示而忽略那些退出值最后仍然调用所有的命令。</para> - </note> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-hookup"> - <title>链接脚本到 rc.d 框架</title> - - <para>当编写好了一个脚本,它需要被整合到 <filename>rc.d</filename> 中去。 - 一个重要的步骤就是安装脚本到 <filename>/etc/rc.d</filename> - (对基本系统而言)或 <filename>/usr/local/etc/rc.d</filename> - (对ports而言)中去。在 <<filename>bsd.prog.mk</filename>> 和 - <<filename>bsd.port.mk</filename>> 中都为此提供了方便的接口, - 通常你不必担心适当的所有权限和模式。系统脚本应当是通过可以在 - <filename>src/etc/rc.d</filename> 找到的 <filename>Makefile</filename> - 安装的。Port 脚本可以像 - <link xlink:href="&url.books.porters-handbook;/rc-scripts.html">Porter's Handbook</link> - 中描述那样通过使用 <varname>USE_RC_SUBR</varname> 来被安装。</para> - - <para>不过,我们应该预先考虑到我们脚本在系统启动顺序中的位置。 - 我们的脚本所处理的服务可能依赖于其它的服务。举个例子, - 没有网络接口和路由选择的启用运行的话,一个网络守护进程是不起作用的。 - 即使一个服务看似什么都不需要,在基本文件系统检查挂载完毕之前也很难启动。</para> - - <para>之前我们曾提到过 &man.rcorder.8;。现在是时候来密切地关注下它了。 - 笼统地说,&man.rcorder.8; 处理一组文件,检验它们的内容, - 并从文件集合打印一个文件列表的依赖顺序到 <varname>stdout</varname> - 标准输出。这点是用于保持文件内部的依赖信息, - 而每个文件只能说明自己的依赖。一个文件可以指定如下信息:</para> - - <itemizedlist> - <listitem> - <para>它 <emphasis>提供</emphasis> 的 <quote>条件</quote> - 的名字(意味着我们服务的名字);</para> - </listitem> - - <listitem> - <para>它 <emphasis>需求</emphasis> 的 - <quote>条件</quote> 的名字;</para> - </listitem> - - <listitem> - <para>应该 <emphasis>先</emphasis> 运行的文件的 - <quote>条件</quote>的名字;</para> - </listitem> - - <listitem> - <para>能用于从全部文件集合中选择一个子集的额外 - <emphasis>关键字</emphasis>( &man.rcorder.8; - 可通过选项而被指定来包括或省去由特殊关键字所列出的文件。)</para> - </listitem> - </itemizedlist> - - <para>并不奇怪的是,&man.rcorder.8; 只能处理接近 &man.sh.1; - 语法的文本文件。&man.rcorder.8; 所解读的特殊行看起来类似 - &man.sh.1; 的注释。这种特殊文本行的语法相当严格地简化了其处理。 - 请查阅 &man.rcorder.8; 以获取更详细的信息。</para> - - <para>除使用 &man.rcorder.8; 的特殊行以外, - 脚本可以坚持将其依赖的其它服务强制性启动。当其它服务是可选的, - 并因系统管理员错误地在 &man.rc.conf.5; - 中禁用掉该服务而使其不能自行启动时,会需要这一点。</para> - - <para>将这些谨记在心,我们来考虑下简单结合了依赖信息增强的守护进程脚本:</para> - - <informalexample> - <programlisting>#!/bin/sh - -# PROVIDE: mumbled oldmumble <co xml:id="rcng-hookup-provide"/> -# REQUIRE: DAEMON cleanvar frotz<co xml:id="rcng-hookup-require"/> -# BEFORE: LOGIN<co xml:id="rcng-hookup-before"/> -# KEYWORD: nojail shutdown<co xml:id="rcng-hookup-keyword"/> - -. /etc/rc.subr - -name=mumbled -rcvar=mumbled_enable - -command="/usr/sbin/${name}" -start_precmd="${name}_prestart" - -mumbled_prestart() -{ - if ! checkyesno frotz_enable && \ - ! /etc/rc.d/frotz forcestatus 1>/dev/null 2>&1; then - force_depend frotz || return 1<co xml:id="rcng-hookup-force"/> - fi - return 0 -} - -load_rc_config $name -run_rc_command "$1"</programlisting> - </informalexample> - - <para>跟前面一样,做如下详细分析:</para> - - <calloutlist> - <callout arearefs="rcng-hookup-provide"> - <para>该行声明了我们脚本所提供的 <quote>条件</quote> 的名字。 - 现在其它脚本可以用那些名字来标明我们脚本的依赖。</para> - - <note> - <para>通常脚本指定一个单独的已提供的条件。然而, - 并没有什么妨碍我们从列出的那些条件中指定,例如, - 为了兼容性的目的。</para> - - <para>在其它情况,主要的名称,或者说唯一的, - <literal>PROVIDE:</literal> 条件应该与 - <envar>${name}</envar> 相同。</para> - </note> - </callout> - - <callout arearefs="rcng-hookup-require rcng-hookup-before"> - <para>因此我们的脚本指示了其依赖于别的脚本所提供的 - <quote>条件</quote>。根据这些行的信息,脚本请示 - &man.rcorder.8; 以将其放在一个或多个提供 - <filename>DAEMON</filename> 和 <filename>cleanvar</filename> - 的脚本后面,但在提供 <filename>LOGIN</filename> 的脚本前面。</para> - - <note> - <para><literal>BEFORE:</literal> - 这一行不可以在其它脚本不完整的依赖关系列表中滥用。 - 适合使用 <literal>BEFORE:</literal> - 的情况是当其它脚本不关心我们的脚本, - 但是我们的脚本如果在另一个之前运行的话能够更好地执行任务。 - 一个典型的实例是网络接口和防火墙: - 虽然接口不依赖防火墙来完成自己的工作, - 但是系统安全将因一切网络流量之前启动的防火墙而受益。</para> - - <para>除了条件相对应的每个单独服务,脚本使用元条件和它们的 - <quote>占位符</quote> 来保证某个操作组在其它之前被执行。 - 这些是由 <filename>UPPERCASE</filename> - 大写名字所表示的。它们的列表和用法可以在 &man.rc.8; 中找到。</para> - - <para>切记将一个服务名称放进 <literal>REQUIRE:</literal> - 行不能保证实际的服务会在我们的脚本启动的时候运行。 - 所需求的服务可能会启动失败或在 &man.rc.conf.5; 中被禁掉了。 - 显然,&man.rcorder.8; 是无法追踪这些细节的,并且 - &man.rc.8; 也不会去追踪。所以, - 脚本启动的应用程序应当能够应付任何所需求的服务的不可用情况。 - 某些情况下,我们可以用 <link linkend="forcedep">下面</link> - 所讨论的方式来协助脚本。</para> - </note> - </callout> - - <callout arearefs="rcng-hookup-keyword"> - <para><anchor xml:id="keywords"/>如我们从上述文字所记起的,&man.rcorder.8; - 关键字可以用来选择或省略某些脚本。即任何 &man.rcorder.8; - 用户可以通过指定 <option>-k</option> 和 <option>-s</option> - 选项来分别指定 <quote>保留清单(keep list)</quote> 和 - <quote>跳过清单(skip list)</quote>。 - 从全部文件到按依赖关系排列的清单,&man.rcorder.8; - 将只是挑出保留清单(除非是空的) - 中那些带关键字的以及从跳过清单中挑出不带关键字的文件。</para> - - <para>在 &os; 中,&man.rcorder.8; 被 - <filename>/etc/rc</filename> 和 - <filename>/etc/rc.shutdown</filename> 所使用。 - 这两个脚本定义了 &os; 中 <filename>rc.d</filename> - 关键字以及它们的意义的标准列表如下:</para> - - <variablelist> - <varlistentry> - <term><literal>nojail</literal></term> - - <listitem> - <para>该服务不适用于 &man.jail.8; 环境。 - 如果是在 jail 的内部的话,自动启动和关闭程序将忽略该脚本。</para> - </listitem> - </varlistentry> - - <varlistentry> - <term><literal>nostart</literal></term> - - <listitem> - <para>该服务只能手动启动否则将不会启动。 - 自动启动程序将忽略此脚本。结合 <literal>shutdown</literal> - 关键字的话,这可以用来编写只在系统关闭时执行一些任务的脚本。</para> - </listitem> - </varlistentry> - - <varlistentry> - <term><literal>shutdown</literal></term> - - <listitem> - <para>这个关键字 <emphasis>明确</emphasis> - 地列出了需要在系统关闭前停止的服务。</para> - - <note> - <para>当系统即将关闭的时候, - <filename>/etc/rc.shutdown</filename> 在运行。 - 它假定认为大部分的 <filename>rc.d</filename> - 脚本在那刻什么都不做。因此, - <filename>/etc/rc.shutdown</filename> - 选择性地调用带有 <literal>shutdown</literal> - 关键字的 <filename>rc.d</filename> 脚本, - 有效地忽略其余的脚本。为了更快的关闭, - <filename>/etc/rc.shutdown</filename> 传递 - <option>faststop</option> 命令给其运行的脚本, - 以跳过预置的检查,例如,进程文件 pidfile 的检查。 - 正如依赖性服务应该在其所依赖的服务之前停止, - <filename>/etc/rc.shutdown</filename> - 以相反的依赖次序来运行这些脚本。</para> - - <para>如果写一个真正的 <filename>rc.d</filename> 脚本的话, - 你应当考虑到其是否与系统关闭时有关系。例如, - 如果你的脚本只通过响应 <option>start</option> - 命令来运行任务,那么你不需要包含这个关键字。然而, - 如果你的脚本管理着一个服务,那么,在系统进入 &man.halt.8; - 中所描述的其本身关闭顺序的最终阶段之前停止该脚本, - 可能是个不错的主意。特别是, - 你显然是应该关闭一个需要相当长时间, - 或需要特定的动作才能干净地关闭的服务。 - 数据库引擎就是这样一个典型的例子。</para> - </note> - </listitem> - </varlistentry> - </variablelist> - </callout> - - <callout arearefs="rcng-hookup-force"> - <para><anchor xml:id="forcedep"/>以 - <function>force_depend</function> - 起始的行应被用于更谨慎的情况。通常,用于修正相互关联的 - <filename>rc.d</filename> - 脚本分层结构的配置文件时会更加稳妥。</para> - - <para>如果你仍不能完成不含 <function>force_depend</function> 的脚本, - 范例提供了一个如何有条件地调用它的习惯用法。在范例中,我们的 - <command>mumbled</command> 守护进程需求另一个以高级方式启动的进程, - <command>frotz</command>。但 <command>frotz</command> 也是可选的; - 而且 &man.rcorder.8; 对这些信息是一无所知的。幸运的是, - 我们的脚本已访问到全部的 &man.rc.conf.5; 变量。如果 - <envar>frotz_enable</envar> 为真,我们希望的最好结果是依靠 - <filename>rc.d</filename> 已经启动了 <command>frotz</command>。 - 否则我们强制检查 <command>frotz</command> 的状态。最终, - 如果 <command>frotz</command> 依赖的服务没有找到或运行的话, - 我们将强制其运行。这时 <function>force_depend</function> - 将发出一条警告信息,因为它只应该在检查到配置信息丢失的情况下被调用。</para> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-args"> - <title>给予 rc.d 脚本更多的灵活性</title> - - <para>当进行启动或停止的调用时,<filename>rc.d</filename> - 脚本应该作用于其所负责的整个子系统。例如, - <filename>/etc/rc.d/netif</filename> 应该启动或停止 - &man.rc.conf.5; 中所描述的全部网络接口。每个任务都唯一地听从一个如 - <option>start</option> 或 <option>stop</option> - 这样的单独命令参数的指示。在启动和停止之间的时间, - <filename>rc.d</filename> 脚本帮助管理员控制运行中的系统, - 并其在需要的时候它将产生更多的灵活性和精确性。举个例子, - 管理员可能想在 &man.rc.conf.5; 中添加一个新网络接口的配置信息, - 然后在不妨碍其它已存在接口的情况下将其启动。 - 在下次管理员可能需要关闭一个单独的网络接口。在魔幻的命令行中, - 对应的 <filename>rc.d</filename> 脚本调用一个额外的参数, - 网络接口名即可。</para> - - <para>幸运的是,&man.rc.subr.8; 允许传递任意多(取决于系统限制)的参数给脚本的方法。 - 由于这个原因,脚本自身的改变可以说是微乎其微。</para> - - <para>&man.rc.subr.8; 如何访问到附加的命令行参数呢?直接获取么? - 并非是无所不用其极的。首先,&man.sh.1; - 函数没有访问到调用者的定位参数,而 &man.rc.subr.8; - 只是这些函数的容器。其次,<filename>rc.d</filename> - 指令的一个好的风格是由主函数来决定将哪些参数传递给它的方法。</para> - - <para>所以 &man.rc.subr.8; 提供了如下的方法: - <function>run_rc_command</function> - 传递其所有参数但将第一个参数逐字传递到各自的方法。首先, - 发出以方法自身为名字的参数:<option>start</option>, - <option>stop</option>,等等。这会被 - <function>run_rc_command</function> 移出, - 这样命令行中原本 <envar>$2</envar> 的内容将作为 - <envar>$1</envar> 来提供给方法,等等。</para> - - <para>为了说明这点,我们来修改原来的虚拟脚本, - 这样它的信息将取决于所提供的附加参数。从这里出发:</para> - - <informalexample> - <programlisting>#!/bin/sh - -. /etc/rc.subr - -name="dummy" -start_cmd="${name}_start" -stop_cmd=":" -kiss_cmd="${name}_kiss" -extra_commands="kiss" - -dummy_start() -{ - if [ $# -gt 0 ]; then<co xml:id="rcng-args-start"/> - echo "Greeting message: $*" - else - echo "Nothing started." - fi -} - -dummy_kiss() -{ - echo -n "A ghost gives you a kiss" - if [ $# -gt 0 ]; then<co xml:id="rcng-args-kiss"/> - echo -n " and whispers: $*" - fi - case "$*" in - *[.!?]) - echo - ;; - *) - echo . - ;; - esac -} - -load_rc_config $name -run_rc_command "$@"<co xml:id="rcng-args-all"/></programlisting> - </informalexample> - - <para>能注意到脚本里发生了那些实质性改变么?</para> - - <calloutlist> - <callout arearefs="rcng-args-start"> - <para>你输入的所有在 <option>start</option> - 之后的参数可以被当作各自方法的定位参数一样被终结。 - 我们可以根据我们的任务、技巧和想法来以任何方式使用他们。 - 在当前的例子中,我们只是以下行中字符串的形式传递参数给 - &man.echo.1; 程序 — 注意 <envar>$*</envar> - 是有双引号的。这里是脚本如何被调用的:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/dummy start</userinput> -Nothing started. -&prompt.root; <userinput>/etc/rc.d/dummy start Hello world!</userinput> -Greeting message: Hello world!</screen> - </callout> - - <callout arearefs="rcng-args-kiss"> - <para>同样用于我们脚本提供的任何方法,并不仅限于标准的方法。 - 我们已经添加了一个自定义的叫做 <option>kiss</option> 的方法, - 并且它给附加参数带来的戏耍决不亚于 <option>start</option>。 - 例如:</para> - - <screen>&prompt.root; <userinput>/etc/rc.d/dummy kiss</userinput> -A ghost gives you a kiss. -&prompt.root; <userinput>/etc/rc.d/dummy kiss Once I was Etaoin Shrdlu...</userinput> -A ghost gives you a kiss and whispers: Once I was Etaoin Shrdlu...</screen> - </callout> - - <callout arearefs="rcng-args-all"> - <para>如果我们只是传递所有附加参数给任意的方法, - 我们只需要在脚本的最后一行我们调用 - <function>run_rc_command</function> 的地方, - 用 <literal>"$@</literal> 代替 <literal>"$1"</literal> 即可。</para> - - <important> - <para>一个 &man.sh.1; 程序员应该是可以理解 - <envar>$*</envar> 和 <envar>$@</envar> - 的微妙区别只是指定全部定位参数的不同方法。 - 关于此更深入的探讨,可以参考这个很好的 &man.sh.1; - 脚本编程手册。在你完全理解这些表达式的意义之前请不要使用它们, - 因为误用它们将给脚本引入缺陷和不安全的弊端。</para> - </important> - - <note> - <para>现在 <function>run_rc_command</function> 可能有个缺陷, - 它将影响保持参数之间的原本边界。也就是, - 带有嵌入空白的参数可能不会被正确处理。该缺陷是由于对 - <envar>$*</envar> 的误用。</para> - </note> - </callout> - </calloutlist> - </sect1> - - <sect1 xml:id="rcng-furthur"> - <title>进一步阅读</title> - - <para><anchor xml:id="lukem"/><link xlink:href="http://www.mewburn.net/luke/papers/rc.d.pdf"> - Luke Mewburn 的原始文章</link> 中讲述了 - <filename>rc.d</filename> 的基本概要, - 并详细阐述了其设计方案的原理。该文章提供了深入了解整个 - <filename>rc.d</filename> 框架以及其所在的现代 BSD - 操作系统的内容。</para> - - <para><anchor xml:id="manpages"/>在 &man.rc.8;,&man.rc.subr.8;, - 还有 &man.rcorder.8; 的联机手册中,对 - <filename>rc.d</filename> 组件做了非常详细的记载。 - 在你写脚本时,如果不去学习和参考这些联机手册的话, - 你是无法完全发挥出 <filename>rc.d</filename> 的能量的。</para> - - <para>工作中实际范例的主要来源就是运行的系统中的 - <filename>/etc/rc.d</filename> 目录。 - 它的内容可读性非常好,因为大部分的枯燥的内容都深藏在 - &man.rc.subr.8; 中了。切记 <filename>/etc/rc.d</filename> - 的脚本也不是神仙写出来的, - 所以它们可能也存在着代码缺陷以及低级的设计方案。 - 但现在你可以来改进它们了!</para> - </sect1> -</article> diff --git a/zh_CN.GB2312/articles/remote-install/article.xml b/zh_CN.GB2312/articles/remote-install/article.xml deleted file mode 100644 index b682458e7e..0000000000 --- a/zh_CN.GB2312/articles/remote-install/article.xml +++ /dev/null @@ -1,512 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!DOCTYPE article PUBLIC "-//FreeBSD//DTD DocBook XML V5.0-Based Extension//EN" - "http://www.FreeBSD.org/XML/share/xml/freebsd50.dtd"> -<!-- - The FreeBSD Documentation Project - The FreeBSD Chinese (Simplified) Documentation Project - - Original Revision: 1.6 ---> -<article xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:lang="zh_cn"> - <info><title>&os; 操作系统在无远程控制台下的远程安装</title> - - - <author><personname><firstname>Daniel</firstname><surname>Gerzo</surname></personname><affiliation> - <address><email>danger@FreeBSD.org</email></address> - </affiliation></author> - - <legalnotice xml:id="trademarks" role="trademarks"> - &tm-attrib.freebsd; - &tm-attrib.general; - </legalnotice> - - <copyright> - <year>2008</year> - <holder>The &os; Documentation Project</holder> - </copyright> - - <pubdate>$FreeBSD$</pubdate> - - <releaseinfo>$FreeBSD$</releaseinfo> - - <abstract> - <para>本文归档了当远程控制台不可用的情况下 &os; 操作系统的远程安装。 - 文章背后的主要灵感归功于和 &a.mm; 还有由 &a.pjd; - 提供的宝贵输入合作的结果。</para> - </abstract> - </info> - - <sect1 xml:id="background"> - <title>背景</title> - - <para>世界上有很多的服务器主机供应商, - 但是他们中只有很少的一部分正式支持 &os;, - 他们通常为他们提供的服务器上安装 &linux; - 发行版提供支持。</para> - - <para>在某些情况下,如果你请求这些公司他们会安装一个你首选的 &linux; - 发行版。有了这个选择,我们将试图安装 &os;。 - 在其他情况下,他们可能提供一个急救系统用于紧急情况。 - 使用这个可能将有利于我们的目的更好的实现。</para> - - <para>本文涵盖了引导一个包含 RAID-1 及 <application>ZFS</application> - 性能的 &os; 系统的远程安装的基本安装配置所必须的步骤。</para> - </sect1> - - <sect1 xml:id="intro"> - <title>简介</title> - - <para>这一节会摘要本文的目的以及更好阐述这里所概括的东西。 - 本文中的这些指令将有益于那些使用不支持 &os; - 的托管设施提供的服务的人。</para> - - <procedure> - <step> - <para>如我们提到过的 <link linkend="background">背景</link> - 的那一节,许多的有声望的服务器主机托管公司提供了各种的急救系统。 - 可以从他们自己的 <acronym>局域网</acronym> 启动并可以通过 - <application>SSH</application> 访问。 - 他们通常提供这种支持目的用于帮助他们的顾客修正损坏的操作系统。 - 如文章将说明的,我们将能够通过这些急救系统的帮助来安装 &os;。</para> - <!-- XXXTR: Solaris has a restore command, something like - sysrestore, FreeBSD Should have one too. --> - </step> - - <step> - <para>文章的下一小节会描述如何配置,并在本地机器上构建最小限度的 - &os;。该版本最终会从随机存储盘运行到远程机器上面去。 - 这将允许我们使用 <application>sysinstall</application> - 实用程序从一个 <acronym>FTP</acronym> - 镜像安装一套完整的 &os; 操作系统。</para> - </step> - - <step> - <para>文章的剩余内容除了描述 <application>ZFS</application> - 文件系统的配置还将描述系统本身的安装步骤。</para> - </step> - </procedure> - - <sect2 xml:id="requirements"> - <title>需求</title> - - <para>想要成功地做下去,你必须:</para> - - <itemizedlist> - <listitem> - <para>拥有一个可通过 <application>SSH</application> - 网络访问的操作系统。</para> - </listitem> - - <listitem> - <para>理解 &os; 的安装过程</para> - </listitem> - - <listitem> - <para>熟悉 &man.sysinstall.8; 实用程序</para> - </listitem> - - <listitem> - <para>拥有 &os; 安张的 <acronym>ISO</acronym> - 镜像文件或者易于使用的 <acronym>CD</acronym></para> - </listitem> - </itemizedlist> - </sect2> - </sect1> - - <sect1 xml:id="preparation"> - <title>准备工作 - <application>mfsBSD</application></title> - - <para>在 &os; 可能安装到目标系统上之前, - 需要先构建一个最小化的从磁盘启动的 &os; 操作系统映像文件。 - 此方法中新系统必须能够从网络访问, - 并且安装的其他过程能够在没有远程访问到系统控制台的情况下完成。</para> - - <para><application>mfsBSD</application> 设置工具能够被用来构建一个微小的 - &os; 映像。如 <application>mfsBSD</application> 名字的含义 - (<quote>mfs</quote> 的意思是 <quote>memory file system</quote> 内存文件系统), - 最后的映像全部从随机存储器运行。多亏了这个特性, - 磁盘的操作将不会有任何限制,因此它能够被用来安装一个完整的 &os; 操作系统。 - <application>mfsBSD</application> 的主页在 - <uri xlink:href="http://people.freebsd.org/~mm/mfsbsd/">http://people.freebsd.org/~mm/mfsbsd/</uri>, - 包含了指向最新释出的设置工具。</para> - - <para>请注意关于 <application>mfsBSD</application> - 内幕以及它所有的适用都超出了本文的内容, - 感兴趣的读者应该去查阅 <application>mfs</application> - 的原始文档得到更多详细内容。</para> - - <para>下载并解压出最新的 <application>mfsBSD</application> - 版本,并改变自己的当前工作目录到存在 <application>mfsBSD</application> - 脚本文件的目录:</para> - - <screen>&prompt.root; <userinput>fetch http://people.freebsd.org/~mm/mfsbsd/mfsbsd-latest.tar.gz</userinput> -&prompt.root; <userinput>tar xvzf mfsbsd-1.0-beta1.tar.gz</userinput> -&prompt.root; <userinput>cd mfsbsd-1.0-beta1/</userinput></screen> - - <sect2 xml:id="mfsbsd-config"> - <title><application>mfsBSD</application> 的配置</title> - - <para>引导 <application>mfsBSD</application> 之前, - 必须设置一些重要的配置选项。 - 最重要的是我们必须有正确地,自然地,网络配置。 - 最适合的方法配置网络选项取决于我们是否事先知道我们会用到的网络接口, - 而且网络接口驱动程序应被系统为我们的硬件载入。 - 我们将看到 <application>mfsBSD</application> - 如何能够在任一种情况下被配置。</para> - - <para>另外一件重要的事情是设置 <systemitem class="username">root</systemitem> 的密码。 - 这将通过编辑 <filename>conf/rootpw.conf</filename> 文件来完成。 - 请记住该文件将把你的密码保存在简单的文本中, - 所以在此我们不推荐你使用真实的密码。然而, - 这只是一个临时使用一次的密码,你可以在随后安装好的系统中更改它。</para> - - <sect3> - <title>编辑 <filename>conf/interfaces.conf</filename> 的方法</title> - - <para>如果我们安装好的网卡是未知类型的, - 我们可以使用 <application>mfsBSD</application> 的自动探测功能。 - <application>mfsBSD</application> 启动脚本能够探测到正确的驱动来使用, - 基于网络接口的 MAC 地址,我们假设在 - <filename>conf/interfaces.conf</filename> 文件中设置如下选项:</para> - - <programlisting>initconf_interfaces="ext1" -initconf_mac_ext1="00:00:00:00:00:00" -initconf_ip_ext1="192.168.0.2" -initconf_netmask_ext1="255.255.255.0"</programlisting> - - <para>别忘了添加 <literal>defaultrouter</literal> - 信息到 <filename>conf/rc.conf</filename> 文件中:</para> - - <programlisting>defaultrouter="192.168.0.1"</programlisting> - </sect3> - - <sect3> - <title>编辑 <filename>conf/rc.conf</filename> 的方法</title> - - <para>当网络接口的驱动是已知类型的,使用 - <filename>conf/rc.conf</filename> 文件添加联网选项会更加方便。 - 该文件的语法跟 &os; 中标准的 &man.rc.conf.5; 文件的语法相同。</para> - - <para>例如,当你知道被使用的将是一个 &man.re.4; 网络接口设备, - 你可以在 <filename>conf/rc.conf</filename> 文件中设置如下选项:</para> - - <programlisting>defaultrouter="192.168.0.1" -ifconfig_re0="inet 192.168.0.2 netmask 255.255.255.0"</programlisting> - </sect3> - </sect2> - - <sect2 xml:id="mfsbsd-build"> - <title>构建一个 <application>mfsBSD</application> 映像</title> - - <para>构建一个 <application>mfsBSD</application> - 映像文件的过程是非常简单明了的。</para> - - <para>第一步是挂载 &os; 的安装 <acronym>CD</acronym>, - 或者挂载安装 <acronym>ISO</acronym> 文件到 - <filename>/cdrom</filename>。 - 因为例子的缘故,在文章中我们将假定你下载的是 &os; 7.0-RELEASE - <acronym>ISO</acronym> 文件。使用 &man.mdconfig.8; - 实用程序挂载 <acronym>ISO</acronym> 映像文件到 - <filename>/cdrom</filename> 目录非常简单:</para> - - <screen>&prompt.root; <userinput>mdconfig -a -t vnode -u 10 -f 7.0-RELEASE-amd64-disc1.iso</userinput> -&prompt.root; <userinput>mount_cd9660 /dev/md10 /cdrom</userinput></screen> - - <para>紧接着,构建可启动的 <application>mfsBSD</application> - 映像:</para> - - <screen>&prompt.root; <userinput>make BASE=/cdrom/7.0-RELEASE</userinput></screen> - - <note> - <para>上面的 <command>make</command> 命令必须在 - <application>mfsBSD</application> 目录树的最高一层运行,也就是: - <filename>~/mfsbsd-1.0-beta1/</filename>。</para> - </note> - </sect2> - - <sect2> - <title>启动 <application>mfsBSD </application></title> - - <para>现在 <application>mfsBSD</application> 映像已经准备好了, - 必须把它上传到远程的一个正在运行的急救系统上或者一个预安装了 - &linux; 发行版的系统上。最适合做这个工作的工具是 - <application>scp</application>:</para> - - <screen>&prompt.root; <userinput>scp disk.img root@192.168.0.2:.</userinput></screen> - - <para>想要正确的引导 <application>mfsBSD</application> 映像, - 必须把它安放在机器的第一块(可启动)设备上。 - 这可能会和使用的例子我们假定的一样,第一块可启动磁盘设备是 - <filename>sda</filename>:</para> - - <screen>&prompt.root; <userinput>dd if=/root/disk.img of=/dev/sda bs=1m</userinput></screen> - - <para>如果一切正常,该映像现在应该存在于第一块设备的 - <acronym>MBR</acronym>(主引导区)而机器也应该能够被启动了。 - 使用工具 &man.ping.8; 来查看机器是否被正确启动。 - 一旦它回复在线状态,就应该能够使用 <systemitem class="username">root</systemitem> - 用户和配置好的密码通过 &man.ssh.1; 来访问它了。</para> - </sect2> - </sect1> - - <sect1 xml:id="installation"> - <title>&os; 操作系统的安装</title> - - <para><application>mfsBSD</application> 成功被引导后它就应该能够通过 - &man.ssh.1; 登入了。这一节会描述如何创建 slices 并标记 slices 的 label, - 为 RAID-1 配置 <application>gmirror</application>, - 还有如何使用 <application>sysinstall</application> - 来安装一个最小的FreeBSD操作系统版本。</para> - - <sect2> - <title>准备磁盘</title> - - <para>首要的任务是为 &os; 分配磁盘空间,也就是, - 创建 slices 和 partitions。很显然, - 当前运行的系统是全部被载入到系统内存中的因此操作磁盘将没有任何问题。 - 要完成这个任务,可以是使用 <application>sysinstall</application> - 或者 &man.fdisk.8; 中的二者任一并结合工具 &man.bsdlabel.8;。</para> - - <para>在开始时,将所有磁盘都标记成空的, - 在每个磁盘上重复如下命令:</para> - - <screen>&prompt.root; <userinput>dd if=/dev/zero of=/dev/ad0 count=2</userinput></screen> - - <para>下面,使用你喜欢的工具创建 slices 并标记磁盘 label。 - 比较简单的方法是使用 <application>sysinstall</application>, - 强大也可能几乎没有漏洞方法是使用标准的基于文本的 &unix; 工具, - 类似于 &man.fdisk.8; 和 &man.bsdlabel.8; - 这些工具的使用也会在这一节中包括。前者已经被包括在 &os; 手册的 - <link xlink:href="&url.books.handbook;/install-steps.html">安装FreeBSD</link> - 一章中了。如本节中刚提到的,这篇文章会展示如何设置一个带有 RAID-1 - 和 <application>ZFS</application> 性能的系统。我们的设置由一个小工具 - &man.gmirror.8; 镜像为 <filename>/</filename> (root), - <filename>/usr</filename> 和 - <filename>/var</filename> 文件系统, - 并把剩余的磁盘空间被分配为 &man.zpool.8; 镜像出的 - <application>ZFS</application> 文件系统。请注意, - <application>ZFS</application> 文件系统将在 &os; - 操作系统成功安装并启动后才会被配置。</para> - - <para>下面的例子会描述如何去创建 slices 和 labels, - 在每个 partition 上初始化 &man.gmirror.8; - 并如何在每个被镜像过的 partition 上创建 - <application>UFS2</application> 文件系统:</para> - - <screen>&prompt.root; <userinput>fdisk -BI /dev/ad0</userinput> <co xml:id="fdisk"/> -&prompt.root; <userinput>fdisk -BI /dev/ad1</userinput> -&prompt.root; <userinput>bsdlabel -wB /dev/ad0s1</userinput> <co xml:id="bsdlabel-writing"/> -&prompt.root; <userinput>bsdlabel -wB /dev/ad1s1</userinput> -&prompt.root; <userinput>bsdlabel -e /dev/ad0s1</userinput> <co xml:id="bsdlabel-editing"/> -&prompt.root; <userinput>bsdlabel /dev/ad0s1 > /tmp/bsdlabel.txt && bsdlabel -R /dev/ad1s1 /tmp/bsdlabel.txt</userinput> <co xml:id="bsdlabel-restore"/> -&prompt.root; <userinput>gmirror label root /dev/ad[01]s1a</userinput> <co xml:id="gmirror1"/> -&prompt.root; <userinput>gmirror label var /dev/ad[01]s1d</userinput> -&prompt.root; <userinput>gmirror label usr /dev/ad[01]s1e</userinput> -&prompt.root; <userinput>gmirror label -F swap /dev/ad[01]s1b</userinput> <co xml:id="gmirror2"/> -&prompt.root; <userinput>newfs /dev/mirror/root</userinput> <co xml:id="newfs"/> -&prompt.root; <userinput>newfs /dev/mirror/var</userinput> -&prompt.root; <userinput>newfs /dev/mirror/usr</userinput></screen> - - <calloutlist> - <callout arearefs="fdisk"> - <para>在整个磁盘上创建一个 slice - 并初始化包含在磁盘第一个扇区启动代码。 - 重复在系统上全部的磁盘上执行此命令。</para> - </callout> - - <callout arearefs="bsdlabel-writing"> - <para>为每块磁盘写入一个包括启动代码的内容的标准 - label。</para> - </callout> - - <callout arearefs="bsdlabel-editing"> - <para>现在,手动去编辑磁盘的 label。可以查阅 &man.bsdlabel.8; - 的联机手册来找到如何建立 partitions 的方法。创建如下 - partions,<literal>a</literal> 为 - <filename>/</filename> (root) 文件系统, - <literal>b</literal> 为 swap 交换空间, - <literal>d</literal> 为 - <filename>/usr</filename> - 还有最后 <literal>f</literal> 被用于 - <application>ZFS</application>。</para> - </callout> - - <callout arearefs="bsdlabel-restore"> - <para>引入你刚才创建的 label 到第二块磁盘, - 所以两块磁盘会使用同样的 label。</para> - </callout> - - <callout arearefs="gmirror1"> - <para>在每个 partition 上初始化 &man.gmirror.8;。</para> - </callout> - - <callout arearefs="gmirror2"> - <para>注意 <option>-F</option> 选项被用在 swap - 交换分区的 partition。 &man.gmirror.8; - 这个指令认为设备处于可靠的状态除非电源系统故障。</para> - </callout> - - <callout arearefs="newfs"> - <para>在每个被镜像的分区上创建一个 - <application>UFS2</application> 的文件系统。</para> - </callout> - </calloutlist> - </sect2> - - <sect2> - <title>系统安装</title> - - <para>这是最重要的一部分。 - 此节将描述如何在我们上一小节已经准备好的磁盘上安装一个最小的 - &os; 版本。要达成这个目的,所有的文件安系统需要被挂载乃至于 - <application>sysinstall</application> 可以把 &os; - 系统的内容写到磁盘上:</para> - - <screen>&prompt.root; <userinput>mount /dev/mirror/root /mnt</userinput> -&prompt.root; <userinput>mkdir /mnt/var /mnt/usr</userinput> -&prompt.root; <userinput>mount /dev/mirror/var /mnt/var</userinput> -&prompt.root; <userinput>mount /dev/mirror/usr /mnt/usr</userinput></screen> - - <para>当你做完这些时,打开 &man.sysinstall.8;。 - 从主菜单选择自定义 <guimenuitem>Custom</guimenuitem> 安装。 - 选中 <guimenuitem>Options</guimenuitem> 选项然后按回车确认。 - 使用方向键获取帮助,移动鼠标指针到 <literal>Install Root</literal> - 选项,按 <keycap>空格</keycap> 更改为 - <filename>/mnt</filename>。 - 按 <keycap>回车</keycap> 提交你的更改并使用 - <keycap>q</keycap> 退出 <guimenuitem>Options</guimenuitem> - (选项)菜单。</para> - - <warning> - <para>注意这一步骤非常重要,如果被跳过了, - <application>sysinstall</application> - 将不能安装 &os;。</para> - </warning> - - <para>到 <guimenuitem>Distributions</guimenuitem>(发行版)菜单选项, - 使用方向键移动鼠标指针到 <option>Minimal</option>(最小化)选项, - 并使用 <keycap>空格键</keycap> 选中该选项。 - 本文使用了最小版本来保存网络联通信息,因为系统本身会通过 - <application>ftp</application> 来安装。使用 - <option>Exit</option>(退出)选项退出这个菜单。</para> - - <note> - <para><guimenuitem>Partition</guimenuitem> 和 - <guimenuitem>Label</guimenuitem> 菜单将被跳过, - 这些没有多少价值了。</para> - </note> - - <para><guimenuitem>Media</guimenuitem>(媒介)菜单, - 选择 <option>FTP</option> 选项。 - 选择一个距离你最近的镜像站点并交给 - <application>sysinstall</application> - 假定网络已经配置完好。你将再回到 - <guimenuitem>Custom</guimenuitem> - (自定义)菜单。</para> - - <para>最后,选择最后的选项来执行系统的安装过程, - <guimenuitem>Commit</guimenuitem>, - 当安装完成后退出 <application>sysinstall</application> - 即可。</para> - </sect2> - - <sect2> - <title>后期安装步骤</title> - - <para>&os; 操作系统现在应该安装完毕了;通常情况下, - 安装过程还没有结束。还需要进行一些安装后期的步骤使得容许 - &os; 在将来启动并能够登入系统。</para> - - <para>你现在必须 &man.chroot.8; - 到刚安装的全新的系统中来完成安装。 - 使用如下命令: </para> - - <screen>&prompt.root; <userinput>chroot /mnt</userinput></screen> - - <para>要达到我们的目的,进行如下步骤:</para> - - <itemizedlist> - <listitem> - <para>拷贝 <literal>GENERIC</literal>(通用)内核到 - <filename>/boot/kernel</filename> - 目录:</para> - - <screen>&prompt.root; <userinput>cp -Rp /boot/GENERIC/* /boot/kernel</userinput></screen> - </listitem> - - <listitem> - <para>创建 <filename>/etc/rc.conf</filename>, - <filename>/etc/resolv.conf</filename> 还有 - <filename>/etc/fstab</filename> 文件。 - 不要忘记正确地设置网络信息并在 - <filename>/etc/rc.conf</filename> - 文件中启用 <application>sshd</application>。 - <filename>/etc/fstab</filename> - 文件内容类似于下面的内容:</para> - - <programlisting># Device Mountpoint FStype Options Dump Pass# -/dev/mirror/swap none swap sw 0 0 -/dev/mirror/root / ufs rw 1 1 -/dev/mirror/usr /usr ufs rw 2 2 -/dev/mirror/var /var ufs rw 2 2 -/dev/cd0 /cdrom cd9660 ro,noauto 0 0</programlisting> - </listitem> - - <listitem> - <para>创建 <filename>/boot/loader.conf</filename> - 文件,并写入如下内容:</para> - - <programlisting>geom_mirror_load="YES" -zfs_load="YES"</programlisting> - </listitem> - - <listitem> - <para>执行下面的命令,使得 - <application>ZFS</application> 在下次启动后可用:</para> - - <screen>&prompt.root; <userinput>echo 'zfs_enable="YES"' >> /etc/rc.conf </userinput></screen> - </listitem> - - <listitem> - <para>可以用 &man.adduser.8; 工具来添加额外的用户。 - 不要忘记添加一个用户到 <systemitem class="groupname">wheel</systemitem> - 组,这样你可以在重新启动后获得 root 权限。</para> - </listitem> - - <listitem> - <para>反复检验你的设置是否正确。</para> - </listitem> - </itemizedlist> - - <para>现在你的系统在下次启动后应该可用了。使用 - &man.reboot.8; 命令重新启动你的系统。</para> - </sect2> - </sect1> - - <sect1 xml:id="zfs"> - <title>ZFS</title> - - <para>如果你的系统重新启动后还完好,现在应该能够登入了。 - 欢迎来到崭新的 &os; 安装,进行远程的不使用远程控制台的安装。</para> - - <para>最后还剩下的步骤是配置 &man.zpool.8; 并创建一些 - &man.zfs.8; 文件系统。建立并管理 - <application>ZFS</application> 非常简单。 - 首先,创建一个镜像的pool:</para> - - <screen>&prompt.root; <userinput>zpool create tank mirror /dev/ad[01]s1f</userinput></screen> - - <para>再接着,创建一些文件系统:</para> - - <screen>&prompt.root; <userinput>zfs create tank/ports</userinput> -&prompt.root; <userinput>zfs create tank/src</userinput> -&prompt.root; <userinput>zfs set compression=gzip tank/ports</userinput> -&prompt.root; <userinput>zfs set compression=on tank/src</userinput> -&prompt.root; <userinput>zfs set mountpoint=/usr/ports tank/ports</userinput> -&prompt.root; <userinput>zfs set mountpoint=/usr/src tank/src</userinput></screen> - - <para>这就是全部步骤了。如果你对 &os; 上的 - <application>ZFS</application> 感兴趣,请查阅 &os; WIKI 中的 - <link xlink:href="http://wiki.freebsd.org/ZFS">ZFS</link> 一节。</para> - </sect1> -</article> diff --git a/zh_CN.GB2312/books/Makefile.inc b/zh_CN.GB2312/books/Makefile.inc deleted file mode 100644 index a4a0382f54..0000000000 --- a/zh_CN.GB2312/books/Makefile.inc +++ /dev/null @@ -1,6 +0,0 @@ -# -# Original Revision: 1.4 -# $FreeBSD$ -# - -DESTDIR?= ${DOCDIR}/zh_CN.GB2312/books/${.CURDIR:T} diff --git a/zh_CN.GB2312/books/arch-handbook/boot/chapter.xml b/zh_CN.GB2312/books/arch-handbook/boot/chapter.xml deleted file mode 100644 index 29f4e4230c..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/boot/chapter.xml +++ /dev/null @@ -1,915 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- -The FreeBSD Documentation Project -The FreeBSD Simplified Chinese Project - -Original Revision: 1.28 - -Copyright (c) 2002 Sergey Lyubka <devnull@uptsoft.com> -All rights reserved -$FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="boot"> - <info><title>引导过程与内核初始化</title> - <authorgroup> - <author><personname><firstname>Sergey</firstname><surname>Lyubka</surname></personname><contrib>&cnproj.contributed.by;</contrib></author> <!-- devnull@uptsoft.com 12 Jun 2002 --> - </authorgroup> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <sect1 xml: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 xml: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>>>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 xml: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 xml: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 xml: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 & RBX_MASK), - MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part), - 0, 0, 0, VTOP(&bootinfo));</programlisting> - </sect1> - - <sect1 xml: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->m_loader]->l_exec(km);</programlisting> - </sect1> - - <sect1 xml: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 \ -<lots of kernel .o files></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及后续产品的资料, - 或者在<link xlink:href="http://www.lib.tsinghua.edu.cn/">清华大学图书馆</link> - 馆藏记录中用"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", &hz);</programlisting> - - <para>TUNABLE_<typename>_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.maxdsiz, 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 = &idt0[0]; /* 中断描述符表 */ -</programlisting> - - <para>每个中断都被设置一个合适的中断处理程序。 - 系统调用关口<literal>INT 0x80</literal>也是如此:</para> - - <programlisting><filename>sys/i386/i386/machdep.c:</filename> - setidt(0x80, &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 & 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)->func))((*sipp)->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 = &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 = - &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(&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)</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> diff --git a/zh_CN.GB2312/books/arch-handbook/driverbasics/chapter.xml b/zh_CN.GB2312/books/arch-handbook/driverbasics/chapter.xml deleted file mode 100644 index e2c1e42c7c..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/driverbasics/chapter.xml +++ /dev/null @@ -1,555 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.37 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="driverbasics"> - <info><title>编写 FreeBSD 设备驱动程序</title> - <authorgroup> - <author><personname><firstname>Murray</firstname><surname>Stokely</surname></personname><contrib>&cnproj.written.by;</contrib></author> - </authorgroup> - <authorgroup> - <author><personname><firstname>Jörg</firstname><surname>Wunsch</surname></personname><contrib>基础性手册intro(4):</contrib></author> - </authorgroup> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <sect1 xml: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 xml:id="driverbasics-kld"> - <title>动态内核链接工具—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 <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);</programlisting> - - - <sect2> - <title>Makefile</title> - - <para>FreeBSD提供了一个makefile包含文件,利用它你可以快速地编译 - 你附加到内核的东西。</para> - - <programlisting>SRCS=skeleton.c -KMOD=skeleton - -.include <bsd.kmod.mk></programlisting> - - <para>简单地用这个makefile运行<command>make</command>就能够创建文件 - <filename>skeleton.ko</filename>,键入如下命令可以把它加载到内核: -<screen>&prompt.root; <userinput>kldload -v ./skeleton.ko</userinput></screen> - </para> - </sect2> - </sect1> - - <sect1 xml: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 xml:id="driverbasics-char"> - <title>字符设备</title> - - <indexterm><primary>character devices(字符设备)</primary></indexterm> - <para>字符设备驱动程序直接从用户进程传输数据,或传输数据到用户进程。 - 这是最普通的一类设备驱动程序,源码树中有大量的简单例子。</para> - - <para>这个简单的伪设备例子会记住你写给它的任何值,并且当你读取它的时候 - 会将这些值返回给你。下面显示了两个版本,一个适用于&os; 4.X, - 一个适用于&os; 5.X。</para> - - <example> - <title>适用于&os; 4.X的回显伪设备驱动程序实例</title> - - <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(<literal>&</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 = 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);</programlisting> - </example> - - <example> - <title>适用于&os; 5.X回显伪设备驱动程序实例</title> - - <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(<literal>&</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 = 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);</programlisting> - </example> - - <para>在&os; 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" > /dev/echo</userinput> -&prompt.root; <userinput>cat /dev/echo</userinput> -Test Data</screen> - - <para>真正的硬件设备在下一章描述。</para> - - <para>补充资源 - <itemizedlist> - <listitem><simpara><link xlink:href="http://ezine.daemonnews.org/200010/blueprints.html">Dynamic - Kernel Linker (KLD) Facility Programming Tutorial</link> - - <link xlink:href="http://www.daemonnews.org/">Daemonnews</link> October 2000</simpara></listitem> - <listitem><simpara><link xlink:href="http://ezine.daemonnews.org/200007/newbus-intro.html">How - to Write Kernel Drivers with NEWBUS</link> - <link xlink:href="http://www.daemonnews.org/">Daemonnews</link> July - 2000</simpara></listitem> - </itemizedlist> - </para> - </sect1> - - <sect1 xml:id="driverbasics-block"> - <title>块设备(消亡中)</title> - - <indexterm><primary>block devices(块设备)</primary></indexterm> - <para>其他&unix;系统支持另一类型的磁盘设备,称为块设备。块设备是内核 - 为它们提供缓冲的磁盘设备。这种缓冲使得块设备几乎没有用,或者说非常 - 不可靠。缓冲会重新安排写操作的次序,使得应用程序丧失了在任何时刻及时 - 知道准确的磁盘内容的能力。这导致对磁盘数据结构(文件系统,数据库等)的 - 可预测的和可靠的崩溃恢复成为不可能。由于写操作被延迟,内核无法向应用 - 程序报告哪个特定的写操作遇到了写错误,这又进一步增加了一致性问题。 - 由于这个原因,真正的应用程序从不依赖于块设备,事实上,几乎所有访问 - 磁盘的应用程序都尽力指定总是使用字符(或<quote>raw</quote>)设备。 - 由于实现将每个磁盘(分区)同具有不同语义的两个设备混为一谈,从而致使 - 相关内核代码极大地复杂化,作为推进磁盘I/O基础结构现代化的一部分,&os; - 抛弃了对带缓冲的磁盘设备的支持。</para> - </sect1> - - <sect1 xml:id="driverbasics-net"> - <title>网络设备驱动程序</title> - - <indexterm><primary>network devices(网络设备)</primary></indexterm> - <para>访问网络设备的驱动程序不需要使用设备节点。选择哪个驱动程序是 - 基于内核内部的其他决定而不是调用open(),对网络设备的使用通常由 - 系统调用socket(2)引入。</para> - - <para>更多细节, 请参见 ifnet(9) 联机手册、 回环设备的源代码, - 以及 Bill Paul 撰写的网络驱动程序。</para> - - </sect1> - -</chapter> diff --git a/zh_CN.GB2312/books/arch-handbook/isa/chapter.xml b/zh_CN.GB2312/books/arch-handbook/isa/chapter.xml deleted file mode 100644 index d231b9e637..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/isa/chapter.xml +++ /dev/null @@ -1,2146 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - - Original Revision: 1.20 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="isa-driver"> - <info><title>ISA设备驱动程序</title> - <authorgroup> - <author><personname><firstname>Sergey</firstname><surname>Babkin</surname></personname><contrib>&cnproj.written.by;</contrib></author> - </authorgroup> - <authorgroup> - <author><personname><firstname>Murray</firstname><surname>Stokely</surname></personname><contrib>&cnproj.modified.for.handbook.by;</contrib></author> - <author><personname><firstname>Valentino</firstname><surname>Vaschetto</surname></personname></author> - <author><personname><firstname>Wylie</firstname><surname>Stilwell</surname></personname></author> - </authorgroup> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - - <sect1 xml:id="isa-driver-synopsis"> - <title>概述</title> - - <indexterm><primary>ISA(Industry Standard Architecture, 工业标准结构)</primary></indexterm> - <indexterm><primary>device driver(设备驱动程序)</primary><secondary>ISA(Industry Standard Architecture, 工业标准结构)</secondary></indexterm> - - <para>本章介绍了编写ISA设备驱动程序相关的一些问题。这儿展示的伪代码 - 相当详细,很容易让人联想到真正的代码,不过这依然仅仅是伪代码。 - 它避免了与所讨论的主题无关的细节。真实的例子可以在实际驱动程序的 - 源代码中找到。<literal>ep</literal>和<literal>aha</literal> - 更是信息的好来源。</para> - </sect1> - - <sect1 xml:id="isa-driver-basics"> - <title>基本信息</title> - - <para>典型的ISA驱动程序需要以下包含文件:</para> - -<programlisting>#include <sys/module.h> -#include <sys/bus.h> -#include <machine/bus.h> -#include <machine/resource.h> -#include <sys/rman.h> - -#include <isa/isavar.h> -#include <isa/pnpvar.h></programlisting> - - <para>它们描述了针对ISA和通用总线子系统的东西。</para> - - <indexterm><primary>object-oriented(面向对象)</primary></indexterm> - <para>总线子系统是以面向对象的方式实现的,其主要结构通过相关联的 - 方法函数来访问。</para> - - <indexterm><primary>bus methods(总线方法)</primary></indexterm> - <para>ISA驱动程序实现的总线方法的列表与任何其他总线的很相似。 - 对于名字为“xxx”的假想驱动程序,它们将是:</para> - - <itemizedlist> - <listitem> - <para><function>static void xxx_isa_identify (driver_t *, - device_t);</function> 通常用于总线驱动程序而不是设备驱动程序。 - 但对于ISA设备,这个方法有特殊用途:如果设备提供某些设备特定的 - (非PnP)方法自动侦测设备,这个例程可以实现它。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_probe (device_t - dev);</function> 在已知(或PnP)位置探测设备。对于已经部分配置的 - 设备,这个例程也能够提供设备特定的对某些参数的自动侦测。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_attach (device_t - dev);</function> 挂接和初始化设备。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_detach (device_t - dev);</function> 卸载设备驱动模块前解挂设备。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_shutdown (device_t - dev);</function> 系统关闭前执行设备的关闭。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_suspend (device_t - dev);</function> 系统进入节能状态前挂起设备。也可以中止 - 切换到节能状态。</para> - </listitem> - - <listitem> - <para><function>static int xxx_isa_resume (device_t - dev);</function> 从节能状态返回后恢复设备的活动状态。</para> - </listitem> - - </itemizedlist> - - <para><function>xxx_isa_probe()</function>和 - <function>xxx_isa_attach()</function>是必须提供的,其余例程根据设备的 - 需要可以有选择地实现。</para> - - <para>使用下面一组描述符将设备驱动链接到系统。</para> - -<programlisting> /* 支持的总线方法表 */ - static device_method_t xxx_isa_methods[] = { - /* 列出驱动程序支持的所有总线方法函数 */ - /* 略去不支持的函数 */ - DEVMETHOD(device_identify, xxx_isa_identify), - DEVMETHOD(device_probe, xxx_isa_probe), - DEVMETHOD(device_attach, xxx_isa_attach), - DEVMETHOD(device_detach, xxx_isa_detach), - DEVMETHOD(device_shutdown, xxx_isa_shutdown), - DEVMETHOD(device_suspend, xxx_isa_suspend), - DEVMETHOD(device_resume, xxx_isa_resume), - - { 0, 0 } - }; - - static driver_t xxx_isa_driver = { - "xxx", - xxx_isa_methods, - sizeof(struct xxx_softc), - }; - - - static devclass_t xxx_devclass; - - DRIVER_MODULE(xxx, isa, xxx_isa_driver, xxx_devclass, - load_function, load_argument);</programlisting> - - <indexterm><primary>softc(硬件设备的软件描述符,相关信息)</primary></indexterm> - - <para>此处的结构<varname remap="structname">xxx_softc</varname>是一个设备 - 特定的结构,它包含私有的驱动程序数据和驱动程序资源的描述符。 - 总线代码会自动按需要为每个设备分配一个softc描述符。</para> - - <indexterm><primary>kernel modules(内核模块)</primary></indexterm> - - <para>如果驱动程序作为可加载模块实现,当驱动程序被加载或卸载时, - 会调用<function>load_function()</function>函数进行驱动程序特定的 - 初始化或清理工作,并将load_argument作为函数的一个参量传递进去。 - 如果驱动程序不支持动态加载(换句话说,它必须被链接到内核中),则 - 这些值应当被设置为0,最后的定义将看起来如下所示:</para> - - <programlisting> DRIVER_MODULE(xxx, isa, xxx_isa_driver, - xxx_devclass, 0, 0);</programlisting> - - <indexterm><primary>PnP(Plug and Play, 即插即用)</primary></indexterm> - - <para>如果驱动程序是为支持PnP的设备而写的,那么就必须定义一个包含 - 所有支持的PnP ID的表。这个表由此驱动程序所支持的PnP ID的列表 - 和以人可读的形式给出的、与这些ID对应的硬件类型和型号的描述 - 组成。看起来如下:</para> - -<programlisting> static struct isa_pnp_id xxx_pnp_ids[] = { - /* 每个所支持的PnP ID占一行 */ - { 0x12345678, "Our device model 1234A" }, - { 0x12345679, "Our device model 1234B" }, - { 0, NULL }, /* 表结束 */ - };</programlisting> - - <para>如果驱动程序不支持PnP设备,它仍然需要一个空的PnP ID表, - 如下所示:</para> - -<programlisting> static struct isa_pnp_id xxx_pnp_ids[] = { - { 0, NULL }, /* 表结束 */ - };</programlisting> - - </sect1> - - <sect1 xml:id="isa-driver-device-t"> - <title>Device_t指针</title> - - <para><varname remap="structname">Device_t</varname>是为设备结构而定义的指针类型, - 这里我们只关心从设备驱动程序编写者的角度看感兴趣的方法。下面的方法 - 用来操纵设备结构中的值:</para> - - <itemizedlist> - - <listitem><para><function>device_t - device_get_parent(dev)</function> 获取设备的父总线。 - </para></listitem> - - <listitem><para><function>driver_t - device_get_driver(dev)</function> 获取指向其驱动程序结构的指针。 - </para></listitem> - - <listitem><para><function>char - *device_get_name(dev)</function> 获取驱动程序的名字,在我们的 - 例子中为<literal>"xxx"</literal>。</para></listitem> - - <listitem><para><function>int device_get_unit(dev)</function> - 获取单元号(与每个驱动程序关联的设备从0开始编号)。 - </para></listitem> - - <listitem><para><function>char - *device_get_nameunit(dev)</function> 获取设备名,包括单元号。 - 例如<quote>xxx0</quote>,<quote>xxx1</quote> - 等。</para></listitem> - - <listitem><para><function>char - *device_get_desc(dev)</function> 获取设备描述。通常它以人可读的 - 形式描述设备的确切型号。</para></listitem> - - <listitem><para><function>device_set_desc(dev, - desc)</function>设置描述信息。这使得设备描述指向desc字符串, - 此后这个字符串就不能被解除分配。</para></listitem> - - <listitem><para><function>device_set_desc_copy(dev, - desc)</function> 设置描述信息。描述被拷贝到内部动态分配的 - 缓冲区,这样desc字符串在以后可以被改变而不会产生有害的结果。 - </para></listitem> - - <listitem><para><function>void - *device_get_softc(dev)</function> 获取指向与设备关联的设备描述符 - (<varname remap="structname">xxx_softc</varname>结构)的指针。 - </para></listitem> - - <listitem><para><function>u_int32_t - device_get_flags(dev)</function> 获取配置文件中特定于设备的 - 标志。</para></listitem> - - </itemizedlist> - - <para>可以使用一个很方便的函数<function>device_printf(dev, fmt, - ...)</function>从设备驱动程序中打印讯息。它自动在讯息前添加 - 单元名和冒号。</para> - - <para>device_t的这些方法在文件<filename>kern/bus_subr.c</filename> - 中实现。</para> - - </sect1> - - <sect1 xml:id="isa-driver-config"> - <title>配置文件与自动配置期间识别和探测的顺序</title> - - <indexterm><primary>ISA(Industry Standard Architecture, 工业标准结构)</primary><secondary>probing(探测)</secondary></indexterm> - - <para>ISA设备在内核配置文件中的描述如下:</para> - - <programlisting>device xxx0 at isa? port 0x300 irq 10 drq 5 - iomem 0xd0000 flags 0x1 sensitive</programlisting> - - <indexterm><primary>IRQ(中断请求)</primary></indexterm> - - <para>端口值、IRQ值和其他值被转换成与设备关联的资源值。根据设备 - 对自动配置需要和支持程度的不同,这些值是可选的。例如, - 某些设备根本不需要读DRQ,而有些则允许设备从设备配置端口读取 - IRQ设置。如果机器有多个ISA总线,可以在配置文件中明确指定哪条 - 总线,如<literal>isa0</literal>或<literal>isa1</literal>, - 否则将在所有ISA总线上搜索设备。</para> - - <para><literal>敏感(sensitive)</literal>是一种资源请求,它指示 - 必须在所有非敏感设备之前探测设备。此特性虽被支持,但似乎从未 - 在目前的任何驱动程序中使用过。</para> - - <para>对于老的ISA设备,很多情况下驱动程序仍然能够侦测配置参数。 - 但是系统中配置的每个设备必须具有一个配置行。如果系统中装有同一 - 类型的两个设备,但对应的驱动程序却只有一个配置行,例如: - <programlisting>device xxx0 at isa?</programlisting> - 那么只有一个设备会被配置。</para> - - <para>但对于支持通过PnP或专有协议进行自动识别的设备,一个配置行 - 就足够配置系统中的所有设备,如上面的配置行,或者简单地:</para> - - <programlisting>device xxx at isa?</programlisting> - - <para>如果设备驱动程序既支持能自动识别的设备又支持老设备,并且 - 两类设备同时安装在一台机器上,那么只要在配置文件中描述老设备 - 就足够了。自动识别的设备将被自动添加。</para> - - <para>如果ISA设备是自动配置的,发生的事件如下:</para> - - <para>所有设备驱动程序的识别例程(包括识别所有PnP设备的PnP识别 - 例程)以随机顺序被调用。他们识别出设备后就把设备添加到ISA总线 - 上的列表中。通常驱动程序的识别例程将新设备与它们的驱动 - 程序关联起来。而PnP识别例程并不知道其他驱动程序,因此不能将 - 驱动程序与它所添加的新设备关联起来。</para> - - <para>使用PnP协议让PnP设备进入睡眠,以防止它们被探测为老设备。</para> - - <para>被标记为<literal>敏感(sensitive)</literal>的非PnP设备的 - 探测例程被调用。如果探测设备成功,那么就为其调用挂接(attach) - 例程。</para> - - <para>所有非PnP设备的探测和连接例程以同样的方式被调用。</para> - - <para>PnP设备从睡眠中恢复过来,并给它们分配所请求的资源:I/O、 - 内存地址范围、IRQ和DRQ,所有这些与已连接的老设备不会冲突。</para> - - <para>对于每个PnP设备,所有ISA设备驱动程序的探测例程都会被调用。 - 第一个要求此设备的驱动程序将被连接。多个驱动程序以不同的优先权 - 要求一个设备的情况是可能的,这种情况下,具有最高优先权的驱动程序 - 将获胜。探测例程必须调用<function>ISA_PNP_PROBE()</function>将 - 真实的PnP ID和驱动程序支持的ID列表作比较,如果ID不在表中则返回 - 失败。这意味着每个驱动程序,包括不支持任何PnP设备的驱动程序, - 都必须对未知的PnP设备无条件调用 - <function>ISA_PNP_PROBE()</function>,对于未知设备, 至少要用一个 - 空的PnP ID表调用并返回失败。</para> - - <para>探测例程遇到错误时会返回一个正值(错误码),成功时返回 - 零或负值。</para> - - <para>负的返回值用于PnP设备支持多个接口的情况。例如,老的兼容接口 - 和新的高级接口通过不同的驱动程序来提供支持。两个驱动程序 - 都侦测设备。在探测例程中返回较高值的驱动程序优先(换句话说, - 返回0的驱动程序具有最高的优先级,返回-1的其次,返回-2的更在 - 其后,如此下去)。如果多个驱动程序返回相同的值,那么最先调用的 - 获胜。因此,如果驱动程序返回0,就基本能够确信它获得优先权仲裁。 - </para> - - <para>设备特定的识别例程也能够将一类而不是单个驱动程序指派给设备。 - 就象使用PnP的情况一样,对于某一设备,会探测这一类中所有的驱动程序。 - 由于这个特性在任何现存的驱动程序中总均未实现,故本文档中不再予以 - 考虑。</para> - - <para>由于探测老设备的时候PnP设备被禁用,它们不会被连接两次 - (一次作为老设备,一次作为PnP)。但如果识别例程设备相关的, - 这种情况下设备驱动程序有责任确保同一设备不会被设备驱动程序 - 连接两次:一次作为老的由用户配置的,一次作为自动识别的。</para> - - <para>对于自动识别的设备(包括PnP和设备特定的)的另一个实践结论是, - 不能从内核配置文件中向它们传递旗标。因此它们必须要么根本不使用 - 旗标,要么为所有自动识别的设备使用单元号为0的设备的旗标,或者 - 使用sysctl接口而不是旗标。</para> - - <para>通过使用函数族<function>resource_query_*()</function>和 - <function>resource_*_value()</function>直接访问配置资源, - 从而可以提供其他不常用的配置。它们的实现位于 - <filename>kern/subr_bus.c</filename>。老的IDE磁盘驱动器 - <filename>i386/isa/wd.c</filename>包含这样使用的例子。 - 但必须优先使用配置的标准方法。将解析配置资源这类事情留给 - 总线配置代码。</para> - - </sect1> - - <sect1 xml:id="isa-driver-resources"> - <title>资源</title> - - <indexterm><primary>resources(资源)</primary></indexterm> - <indexterm><primary>device driver(设备驱动程序)</primary><secondary>resources(资源)</secondary></indexterm> - - <para>用户写入到内核配置文件中的信息被作为配置资源处理,并传递到内核。 - 总线配置代码解析这部分信息并将其转换为结构device_t的值和与之 - 关联的总线资源。对于复杂情况下的配置,驱动程序可以直接使用 - <function>resource_*</function> 函数访问配置资源。 然而, - 通常既不需要也不推荐这样做, - 因此这儿不再进一步讨论这个问题。</para> - - <para>总线资源与每个设备相关联。通过类型和类型中的数字标识它们。 - 对于ISA总线,定义了下面的类型:</para> - - <indexterm><primary>DMA channel(DMA通道)</primary></indexterm> - - <itemizedlist> - <listitem> - <para><emphasis>SYS_RES_IRQ</emphasis> - 中断号</para> - </listitem> - - <listitem> - <para><emphasis>SYS_RES_DRQ</emphasis> - ISA DMA通道号</para> - </listitem> - - <listitem> - <para><emphasis>SYS_RES_MEMORY</emphasis> - 映射到系统内存空间 - 的设备内存的范围 - </para> - </listitem> - - <listitem> - <para><emphasis>SYS_RES_IOPORT</emphasis> - 设备I/O寄存器的范围 - </para> - </listitem> - </itemizedlist> - - <para>类型内的枚举从0开始,因此如果设备有两个内存区域,它的 - <literal>SYS_RES_MEMORY</literal> 类型的资源编号为0和1。 - 资源类型与C语言的类型无关, - 所有资源值具有C语言 <literal>unsigned long</literal> - 类型,并且必要时必须进行类型强制转换 (cast)。资源号不必连续, - 尽管对于ISA它们一般是连续的。ISA设备允许的资源编号为:</para> - - <programlisting> IRQ: 0-1 - DRQ: 0-1 - MEMORY: 0-3 - IOPORT: 0-7</programlisting> - - <para>所有资源被表示为带有起始值和计数的范围。对于IRQ和DRQ资源, - 计数一般等于1。内存的值引用物理地址。</para> - - <para>对资源能够执行三种类型的动作:</para> - - <itemizedlist> - <listitem><para>set/get</para></listitem> - <listitem><para>allocate/release</para></listitem> - <listitem><para>activate/deactivate</para></listitem> - </itemizedlist> - - <para>Set设置资源使用的范围。Allocation保留出请求的范围,使得 - 其它设备不能再占用(并检查此范围没有被其它设备占用)。 - Activation执行必要的动作使得驱动程序可以访问资源(例如,对于 - 内存,它将被映射到内核的虚拟地址空间)。</para> - - <para>操作资源的函数有:</para> - - <itemizedlist> - <listitem> - <para><function>int bus_set_resource(device_t dev, int type, - int rid, u_long start, u_long count)</function></para> - - <para>为资源设置范围。成功则返回0,否则返回错误码。 - 一般此函数只有在<literal>type</literal>, - <literal>rid</literal>,<literal>start</literal>或 - <literal>count</literal>之一的值超出了允许的范围才会 - 返回错误。</para> - - <itemizedlist> - <listitem> - <para> dev - 驱动程序的设备</para> - </listitem> - <listitem> - <para> type - 资源类型,SYS_RES_* </para> - </listitem> - <listitem> - <para> rid - 类型内部的资源号(ID)</para> - </listitem> - <listitem> - <para> start, count - 资源范围 </para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> - <para><function>int bus_get_resource(device_t dev, int type, - int rid, u_long *startp, u_long *countp)</function></para> - - <para>取得资源范围。成功则返回0,如果资源尚未定义则返回错误码。 - </para> - </listitem> - - <listitem> - <para><function>u_long bus_get_resource_start(device_t dev, - int type, int rid) u_long bus_get_resource_count (device_t - dev, int type, int rid)</function></para> - - <para>便捷函数,只用来获取start或count。出错的情况下返回0, - 因此如果0是资源的start合法值之一,将无法区分返回 - 的0是否指示错误。幸运的是,对于附加驱动程序,没有ISA资源的 - start值从0开始。</para> - </listitem> - - <listitem> - <para><function>void bus_delete_resource(device_t dev, int - type, int rid)</function></para> - <para>删除资源,令其未定义。</para> - </listitem> - - <listitem> - <para><function>struct resource * - bus_alloc_resource(device_t dev, int type, int *rid, - u_long start, u_long end, u_long count, u_int - flags)</function></para> - - <para>在start和end之间没有被其它设备占用的地方按count值 - 的范围分配一个资源。不过,不支持对齐。如果资源尚未被设置, - 则自动创建它。start为0,end为~0(全1)的这对特殊值 - 意味着必须使用以前通过 - <function>bus_set_resource()</function>设置的固定值: - start和count就是它们自己,end=(start+count),这种情况下, - 如果以前资源没有定义,则返回错误。尽管rid通过引用传递, - 但它并不被ISA总线的资源分配代码设置(其它总线可能使用不同的 - 方法并可能修改它)。</para> - </listitem> - </itemizedlist> - - <para>旗标是一个位映射,调用者感兴趣的有:</para> - - <itemizedlist> - <listitem> - <para><emphasis>RF_ACTIVE</emphasis> - 使得资源分配后 - 被自动激活。</para> - </listitem> - - <listitem> - <para><emphasis>RF_SHAREABLE</emphasis> - 资源可以同时 - 被多个驱动程序共享。</para> - </listitem> - - <listitem> - <para><emphasis>RF_TIMESHARE</emphasis> - 资源可以被多个驱动 - 程序分时共享,也就是说,被多个驱动程序同时分配,但任何 - 给定时间只能被其中一个激活。</para> - </listitem> -<!-- XXXDONT KNOW IT THESE SHOULD BE TWO SEPARATE LISTS OR NOT --> - <listitem> - <para>出错返回0。被分配的值可以使用 - <function>rhand_*()</function>从返回的句柄获得。</para> - </listitem> - <listitem> - <para><function>int bus_release_resource(device_t dev, int - type, int rid, struct resource *r)</function></para> - </listitem> - - <listitem> - <para>释放资源,r为<function>bus_alloc_resource()</function> - 返回的句柄。成功则返回0,否则返回错误码。</para> - </listitem> - - <listitem> - <para><function>int bus_activate_resource(device_t dev, int - type, int rid, struct resource *r)</function> - <function>int bus_deactivate_resource(device_t dev, int - type, int rid, struct resource *r)</function></para> - </listitem> - - <listitem> - <para>激活或禁用资源。成功则返回0,否则返回错误码。如果 - 资源被分时共享且当前被另一驱动程序激活,则返回 <literal>EBUSY</literal>。 - </para> - </listitem> - - <listitem> - <para><function>int bus_setup_intr(device_t dev, struct - resource *r, int flags, driver_intr_t *handler, void *arg, - void **cookiep)</function> <function>int - bus_teardown_intr(device_t dev, struct resource *r, void - *cookie)</function></para> - </listitem> - - <listitem> - <para>关联/分离中断处理程序与设备。成功则返回0,否则 - 返回错误码。</para> - </listitem> - - <listitem> - <para>r - 被激活的描述IRQ的资源句柄。</para> - <para>flags - 中断优先级,如下之一:</para> - - <itemizedlist> - <listitem> - <para><function>INTR_TYPE_TTY</function> - 终端和其它 - 类似的字符类型设备。使用 - <function>spltty()</function>屏蔽它们。</para> - </listitem> - <listitem> - <para><function>(INTR_TYPE_TTY | - INTR_TYPE_FAST)</function> - 输入缓冲较小的终端类型 - 设备,而且输入上的数据丢失很关键(例如老式串口)。 - 使用<function>spltty()</function>屏蔽它们。</para> - </listitem> - <listitem> - <para><function>INTR_TYPE_BIO</function> - 块类型设备, - 不包括CAM控制器上的。使用 - <function>splbio()</function>屏蔽它们。</para> - </listitem> - <listitem> - <para><function>INTR_TYPE_CAM</function> - CAM (通用访问 - 方法Common Access Method)总线控制器。使用 - <function>splcam()</function>屏蔽它们。</para> - </listitem> - <listitem> - <para><function>INTR_TYPE_NET</function> - 网络接口 - 控制器。使用 - <function>splimp()</function>屏蔽它们。</para> - </listitem> - <listitem> - <para><function>INTR_TYPE_MISC</function> - - 各种其它设备。除了通过 - <function>splhigh()</function>没有其它方法屏蔽它们。 - <function>splhigh()</function>屏蔽所有中断。 - </para> - </listitem> - </itemizedlist> - </listitem> - </itemizedlist> - - <para>当中断处理程序执行时,匹配其优先级的所有其它中断都被屏蔽, - 唯一的例外是MISC级别,它不会屏蔽其它中断,也不会被其它中断 - 屏蔽。</para> - - <itemizedlist> - <listitem> - <para><emphasis>handler</emphasis> - 指向处理程序的指针, - 类型driver_intr_t被定义为<function>void - driver_intr_t(void *)</function></para> - </listitem> - <listitem> - <para><emphasis>arg</emphasis> - 传递给处理程序的参量,标识 - 特定设备。由处理程序将它从void*转换为任何实际类型。ISA - 中断处理程序的旧约定是使用单元号作为参量,新约定(推荐) - 使用指向设备softc结构的指针。</para> - </listitem> - <listitem> - <para><emphasis>cookie[p]</emphasis> - 从 - <function>setup()</function>接收的值,当传递给 - <function>teardown()</function> - 时用于标识处理程序。</para> - </listitem> - </itemizedlist> - - <para>定义了若干方法来操作资源句柄(struct resource *)。设备驱动 - 程序编写者感兴趣的有:</para> - - <itemizedlist> - <listitem> - <para><function>u_long rman_get_start(r) u_long - rman_get_end(r)</function> 取得被分配的资源范围的起始和结束。 - </para> - </listitem> - <listitem> - <para><function>void *rman_get_virtual(r)</function> 取得 - 被激活的内存资源的虚地址。</para> - </listitem> - </itemizedlist> - - </sect1> - - <sect1 xml:id="isa-driver-busmem"> - <title>总线内存映射</title> - - <para>很多情况下设备驱动程序和设备之间的数据交换是通过内存 - 进行的。有两种可能的变体:</para> - - <para>(a) 内存位于设备卡上</para> - <para>(b) 内存为计算机的主内存</para> - - <para>情况(a)中,驱动程序可能需要在卡上的内存与主存之间来回 - 拷贝数据。为了将卡上的内存映射到内核的虚地址空间,卡上内存的 - 物理地址和长度必须被定义为<literal>SYS_RES_MEMORY</literal>资源。 - 然后资源就可以被分配并激活,它的虚地址通过使用 - <function>rman_get_virtual()</function>获取。较老的驱动程序 - 将函数<function>pmap_mapdev()</function>用于此目的,现在 - 不应当再直接使用此函数。它已成为资源激活的一个内部步骤。</para> - - <para>大多数ISA卡的内存配置为物理地位于640KB-1MB范围之间的 - 某个位置。某些ISA卡需要更大的内存范围,位于16M以下的某个 - 位置(由于ISA总线上24位地址限制)。这种情况下,如果机器有 - 比设备内存的起始地址更多的内存(换句话说,它们重叠),则 - 必须在被设备使用的内存起始地址处配置一个内存空洞。许多 - BIOS允许在起始于14MB或15MB处配置1M的内存空洞。如果BIOS - 正确地报告内存空洞,FreeBSD就能够正确处理它们(此特性 - 在老BIOS上可能会出问题)。</para> - - <para>情况(b)中,只是数据的地址被发送到设备,设备使用DMA实际 - 访问主存中的数据。存在两个限制:首先,ISA卡只能访问16MB以下 - 的内存。其次,虚地址空间中连续的页面在物理地址空间中可能不 - 连续,设备可能不得不进行分散/收集操作。总线子系统为这些问题 - 提供现成现成的解决办法,剩下的必须由驱动程序自己完成。</para> - - <para>DMA内存分配使用了两个结构, <varname>bus_dma_tag_t</varname> - 和 <varname>bus_dmamap_t</varname>。 - 标签(tag)描述了DMA内存要求的特性。映射(map)表示按照这些 - 特性分配的内存块。多个映射可以与同一标签关联。</para> - - <para>标签按照对特性的继承而被组织成树型层次结构。子标签继承父 - 标签的所有要求,可以令其更严格,但不允许放宽要求。</para> - - <para>一般地,每个设备单元创建一个顶层标签(没有父标签)。如果 - 每个设备需要不同要求的内存区,则为每个内存区都会创建一个标签,这些 - 标签作为父标签的孩子。</para> - - <para>使用标签创建映射的方法有两种。</para> - - <para>其一,分配一大块符合标签要求的连续内存(以后可以被释放)。 - 这一般用于分配为了与设备通信而存在相对较长时间的那些内存区。 - 将这样的内存加载到映射中非常容易:它总是被看作位于适当物理 - 内存范围的一整块。</para> - - <para>其二,将虚拟内存中的任意区域加载到映射中。这片内存的 - 每一页都被检查,看是否符合映射的要求。如何符合则留在原始位置。 - 如果不符合则分配一个新的符合要求的 - <quote>反弹页面(bounce page)</quote>,用作中间存储。 - 当从不符合的原始页面写入数据时,数据首先被拷贝到反弹页面, - 然后从反弹页面传递到设备。当读取时,数据将会从设备到反弹页面, - 然后被拷贝到它们不符合的原始页面。原始和反弹页面之间的拷贝 - 处理被称作同步。这一般用于单次传输的基础之上:每次传输时 - 加载缓冲区,完成传输,卸载缓冲区。</para> - - <para>工作在DMA内存上的函数有:</para> - - <itemizedlist> - <listitem> - <para><function>int bus_dma_tag_create(bus_dma_tag_t parent, - bus_size_t alignment, bus_size_t boundary, bus_addr_t - lowaddr, bus_addr_t highaddr, bus_dma_filter_t *filter, void - *filterarg, bus_size_t maxsize, int nsegments, bus_size_t - maxsegsz, int flags, bus_dma_tag_t *dmat)</function></para> - - <para>创建新标签。成功则返回0,否则返回错误码。</para> - - <itemizedlist> - <listitem> - <para><emphasis>parent</emphasis> - 父标签或者NULL, - NULL用于创建顶层标签。</para> - </listitem> - - <listitem> - <para><emphasis>alignment</emphasis> - - 对将要分配给标签的内存区的对齐要求。<quote>no specific - alignment</quote>时值为1。仅应用于以后的 - <function>bus_dmamem_alloc()</function>而不是 - <function>bus_dmamap_create()</function>调用。</para> - </listitem> - - <listitem> - <para><emphasis>boundary</emphasis> - 物理地址边界, - 分配内存时不能穿过。对于<quote>no boundary</quote> - 使用0。仅应用于以后的 - <function>bus_dmamem_alloc()</function>而不是 - <function>bus_dmamap_create()</function>调用。 - 必须为2的乘方。如果计划以非层叠DMA方式使用内存(也就是说, - DMA地址由ISA DMA控制器提供而不是设备自身),则由于DMA硬件 - 限制,边界必须不能大于64KB (64*1024)。</para> - </listitem> - - <listitem> - <para><emphasis>lowaddr, highaddr</emphasis> - 名字稍微 - 有些误导。这些值用于限制可用于内存分配的物理地址的允许 - 范围。其确切含义根据以后不同的使用而有所不同。</para> - - <itemizedlist> - <listitem> - <para>对于<function>bus_dmamem_alloc()</function>, - 从0到lowaddr-1的所有地址被视为允许,更高的地址不允许 - 使用。</para> - </listitem> - - <listitem> - <para>对于<function>bus_dmamap_create()</function>, - 闭区间[lowaddr; highaddr]之外的所有地址被视为可访问。 - 范围之内的地址页面被传递给过滤函数,由它决定是否可访问。 - 如果没有提供过滤函数,则整个区间被视为不可访问。</para> - </listitem> - - <listitem> - <para>对于ISA设备,正常值(没有过滤函数)为:</para> - <para>lowaddr = BUS_SPACE_MAXADDR_24BIT</para> - <para>highaddr = BUS_SPACE_MAXADDR</para> - </listitem> - </itemizedlist> - - </listitem> - - <listitem> - <para><emphasis>filter, filterarg</emphasis> - 过滤函数及其 - 参数。如果filter为NULL,则当调用 - <function>bus_dmamap_create()</function>时,整个区间 - [lowaddr, highaddr]被视为不可访问。 - 否则,区间[lowaddr; highaddr]内的每个被试图访问的页面的 - 物理地址被传递给过滤函数,由它决定是否可访问。过滤函数的 - 原型为:<function>int filterfunc(void *arg, - bus_addr_t paddr)</function>。当页面可以被访问时它必须 - 返回0,否则返回非零值。</para> - </listitem> - - <listitem> - <para><emphasis>maxsize</emphasis> - 通过此标签可以分配的 - 最大内存值(以字节计)。有时这个值很难估算,或者可以任意大, - 这种情况下,对于ISA设备这个值可以设为 - <literal>BUS_SPACE_MAXSIZE_24BIT</literal>。</para> - </listitem> - - <listitem> - <para><emphasis>nsegments</emphasis> - 设备支持的分散/收集段 - 的最大数目。如果不加限制,则使用应当使用值 - <literal>BUS_SPACE_UNRESTRICTED</literal>。 - 建议对父标签使用这个值,而为子孙标签指定实际限制。 - nsegments值等于 <literal>BUS_SPACE_UNRESTRICTED</literal> - 的标签不能用于实际加载映射,仅可以将它们作为父标签。 nsetments - 的实际限制大约为250-300,再高的值将导致内核堆栈溢出(硬件 - 无法正常支持那么多的分散/收集缓冲区)。</para> - </listitem> - - <listitem> - <para><emphasis>maxsegsz</emphasis> - 设备支持的分散/收集段 - 的最大尺寸。对于ISA设备的最大值为 - <literal>BUS_SPACE_MAXSIZE_24BIT</literal>。</para> - </listitem> - - <listitem> - <para><emphasis>flags</emphasis> - 旗标的位图。感兴趣的旗标 - 只有:</para> - - <itemizedlist> - <listitem> - <para><emphasis>BUS_DMA_ALLOCNOW</emphasis> - 创建标签时 - 请求分配所有可能用到的反射页面。</para> - </listitem> - - <listitem> - <para><emphasis>BUS_DMA_ISA</emphasis> - 比较神秘的一个标志, - 仅用于Alpha机器。i386机器没有定义它。Alpha机器的所有ISA设备 - 都应当使用这个标志,但似乎还没有这样的驱动程序。</para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> - <para><emphasis>dmat</emphasis> - 指向返回的新标签的存储的 - 指针。</para> - </listitem> - - </itemizedlist> - - </listitem> - - <listitem> <!-- Second entry in list alpha --> - <para><function>int bus_dma_tag_destroy(bus_dma_tag_t - dmat)</function></para> - - <para>销毁标签。成功则返回0,否则返回错误码。</para> - - <para>dmat - 被销毁的标签。</para> - - </listitem> - - <listitem> <!-- Third entry in list alpha --> - <para><function>int bus_dmamem_alloc(bus_dma_tag_t dmat, - void** vaddr, int flags, bus_dmamap_t - *mapp)</function></para> - - <para>分配标签所描述的一块连续内存区。被分配的内存的大小为标签的 - maxsize。成功则返回0,否则返回错误码。调用结果被用于获取内存的 - 物理地址,但在此之前必须用<function>bus_dmamap_load()</function> - 将其加载。</para> - - <itemizedlist> - <listitem> - <para> - <emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>vaddr</emphasis> - 指向存储的指针,该存储空间 - 用于返回的分配区域的内核虚地址。 - </para> - </listitem> - <listitem> - <para> - flags - 旗标的位图。唯一感兴趣的旗标为: - </para> - <itemizedlist> - <listitem> - <para> - <emphasis>BUS_DMA_NOWAIT</emphasis> - 如果内存不能 - 立即可用则返回错误。如果此标志没有设置,则允许例程 - 睡眠,直到内存可用为止。 - </para> - </listitem> - </itemizedlist> - </listitem> - <listitem> - <para> - <emphasis>mapp</emphasis> - 指向返回的新映射的存储的指针。 - </para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> <!-- Fourth entry in list alpha --> - <para> - <function>void bus_dmamem_free(bus_dma_tag_t dmat, void - *vaddr, bus_dmamap_t map)</function> - </para> - <para> - 释放由<function>bus_dmamem_alloc()</function>分配的内存。 - 目前,对分配的带有ISA限制的内存的释放没有实现。因此,建议的 - 使用模型为尽可能长时间地保持和重用分配的区域。不要轻易地 - 释放某些区域,然后再短时间地分配它。这并不意味着不应当使用 - <function>bus_dmamem_free()</function>:希望很快它就会被 - 完整地实现。 - </para> - - <itemizedlist> - <listitem> - <para><emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>vaddr</emphasis> - 内存的内核虚地址 - </para> - </listitem> - <listitem> - <para> - <emphasis>map</emphasis> - 内存的映射(跟 - <function>bus_dmamem_alloc()</function>返回的一样) - </para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> <!-- The fifth entry in list alpha --> - <para> - <function>int bus_dmamap_create(bus_dma_tag_t dmat, int - flags, bus_dmamap_t *mapp)</function> - </para> - <para> - 为标签创建映射,以后用于 - <function>bus_dmamap_load()</function>。成功则返回0,否则 - 返回错误码。 - </para> - <itemizedlist> - <listitem> - <para> - <emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>flags</emphasis> - 理论上是旗标的位图。但还 - 从未定义过任何旗标,因此目前总是0。 - </para> - </listitem> - <listitem> - <para> - <emphasis>mapp</emphasis> - 指向返回的新映射的存储的指针。 - </para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> <!-- Sixth entry in the alpha list --> - <para> - <function>int bus_dmamap_destroy(bus_dma_tag_t dmat, - bus_dmamap_t map)</function> - </para> - <para> - 销毁映射。成功则返回0,否则返回错误码。 - </para> - - <itemizedlist> - <listitem> - <para> - dmat - 与映射关联的标签 - </para> - </listitem> - <listitem> - <para> - map - 将要被销毁的映射 - </para> - </listitem> - </itemizedlist> - </listitem> - - <listitem> <!-- Seventh entry in list alpha --> - <para> - <function>int bus_dmamap_load(bus_dma_tag_t dmat, - bus_dmamap_t map, void *buf, bus_size_t buflen, - bus_dmamap_callback_t *callback, void *callback_arg, int - flags)</function> - </para> - <para> - 加载缓冲区到映射中(映射必须事先由 - <function>bus_dmamap_create()</function>或者 - <function>bus_dmamem_alloc()</function>)创建。缓冲区的所有 - 页面都会被检查,看是否符合标签的要求,并为那些不符合的分配 - 反弹页面。会创建物理段描述符的数组,并将其传递给回调函数。 - 回调函数以某种方式处理这个数组。系统中的反弹缓冲区是受限的, - 因此如果需要的反弹缓冲区不能立即获得,则将请求入队,当反弹 - 缓冲区可用时再调用回调函数。如果回调函数立即执行则返回0, - 如果请求被排队,等待将来执行,则返回 - <errorname>EINPROGRESS</errorname>。后一种情况下, - 与排队的回调函数之间的同步由驱动程序负责。 - </para> - <!--<blockquote>--> - <itemizedlist> - <listitem> - <para> - <emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>map</emphasis> - 映射 - </para> - </listitem> - <listitem> - <para> - <emphasis>buf</emphasis> - 缓冲区的内核虚地址 - </para> - </listitem> - <listitem> - <para> - <emphasis>buflen</emphasis> - 缓冲区的长度 - </para> - </listitem> - <listitem> - <para> - <emphasis>callback</emphasis>,<function> - callback_arg</function> - 回调函数及其参数 - </para> - </listitem> - </itemizedlist> - <!--</blockquote>--> - <para> - 回调函数的原型为: - </para> - <para> - <function>void callback(void *arg, bus_dma_segment_t - *seg, int nseg, int error)</function> - </para> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <emphasis>arg</emphasis> - 与传递给 - <function>bus_dmamap_load()</function>的callback_arg - 相同。 - </para> - </listitem> - <listitem> - <para> - <emphasis>seg</emphasis> - 段描述符的数组 - </para> - </listitem> - <listitem> - <para> - <emphasis>nseg</emphasis> - 数组中的描述符个数 - </para> - </listitem> - <listitem> - <para> - <emphasis>error</emphasis> - 表示段数目溢出:如被设为 - <errorname>EFBIG</errorname>, - 则标签允许的最大数目的段无法容纳缓冲区。 - 这种情况下数组中的描述符的数目只有标签许可的那么多。 - 对这种情况的处理由驱动程序决定:根据希望的语义,驱动 - 程序可以视其为错误,或将缓冲区分为两个并单独处理第二个。 - </para> - </listitem> - </itemizedlist> - <!-- </blockquote> --> - <para> - 段数组中的每一项包含如下字段: - </para> - - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <emphasis>ds_addr</emphasis> - 段物理地址 - </para> - </listitem> - <listitem> - <para> - <emphasis>ds_len</emphasis> - 段长度 - </para> - </listitem> - </itemizedlist> - <!-- </blockquote>--> - </listitem> - - <listitem> <!-- Eighth entry in alpha list --> - <para> - <function>void bus_dmamap_unload(bus_dma_tag_t dmat, - bus_dmamap_t map)</function> - </para> - <para>unload the map. - </para> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>map</emphasis> - 已加载的映射 - </para> - </listitem> - </itemizedlist> - <!-- </blockquote> --> - </listitem> - - <listitem> <!-- Ninth entry list alpha --> - <para> - <function>void bus_dmamap_sync (bus_dma_tag_t dmat, - bus_dmamap_t map, bus_dmasync_op_t op)</function> - </para> - <para> - 与设备进行物理传输前后,将加载的缓冲区与其反弹页面进行同步。 - 此函数完成原始缓冲区与其映射版本之间所有必需的数据拷贝工作。 - 进行传输之前和之后必须对缓冲区进行同步。 - </para> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <emphasis>dmat</emphasis> - 标签 - </para> - </listitem> - <listitem> - <para> - <emphasis>map</emphasis> - 已加载的映射 - </para> - </listitem> - <listitem> - <para> - <emphasis>op</emphasis> - 要执行的同步操作的类型: - </para> - </listitem> - </itemizedlist> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <function>BUS_DMASYNC_PREREAD</function> - 从设备到 - 缓冲区的读操作之前 - </para> - </listitem> - <listitem> - <para> - <function>BUS_DMASYNC_POSTREAD</function> - 从设备到 - 缓冲区的读操作之后 - </para> - </listitem> - <listitem> - <para> - <function>BUS_DMASYNC_PREWRITE</function> - 从缓冲区到 - 设备的写操作之前 - </para> - </listitem> - <listitem> - <para> - <function>BUS_DMASYNC_POSTWRITE</function> - 从缓冲区到 - 设备的写操作之后 - </para> - </listitem> - </itemizedlist> - </listitem> - </itemizedlist> <!-- End of list alpha --> -<!-- </blockquote> -</blockquote> --> - - <para> - 当前PREREAD和POSTWRITE为空操作,但将来可能会改变,因此驱动程序 - 中不能忽略它们。由<function>bus_dmamem_alloc()</function> - 获得的内存不需要同步。 - </para> - <para> - 从<function>bus_dmamap_load()</function>中调用回调函数之前, - 段数组是存储在栈中的。并且是按标签允许的最大数目的段预先分配 - 好的。这样由于i386体系结构上对段数目的实际限制约为250-300 - (内核栈为4KB减去用户结构的大小,段数组条目的大小为8字节,和 - 其它必须留出来的空间)。由于数组基于最大数目而分配,因此这个值 - 必须不能设置成超出实际需要。幸运的是,对于大多数硬件而言, - 所支持的段的最大数目低很多。但如果驱动程序想处理具有非常多 - 分散/收集段的缓冲区,则应当一部分一部分地处理:加载缓冲区的 - 一部分,传输到设备,然后加载缓冲区的下一部分,如此反复。 - </para> - <para> - 另一个实践结论是段数目可能限制缓冲区的大小。如果缓冲区中的 - 所有页面碰巧物理上不连续,则分片情况下支持的最大缓冲区尺寸 - 为(nsegments * page_size)。例如,如果支持的段的最大数目为10, - 则在i386上可以确保支持的最大缓冲区大小为40K。如果希望更大的 - 则需要在驱动程序中使用一些特殊技巧。 - </para> - <para> - 如果硬件根本不支持分散/收集,或者驱动程序希望即使在严重分片的 - 情况下仍然支持某种缓冲区大小,则解决办法是:如果无法容纳下原始 - 缓冲区,就在驱动程序中分配一个连续的缓冲区作为中间存储。 - </para> - <para> - 下面是当使用映射时的典型调用顺序,根据对映射的具体使用而不同。 - 字符->用于显示时间流。 - </para> - <para> - 对于从连接到分离设备,这期间位置一直不变的缓冲区:</para> - <para> - bus_dmamem_alloc -> bus_dmamap_load -> ...use buffer... -> - -> bus_dmamap_unload -> bus_dmamem_free - </para> - - <para>对于从驱动程序外部传递进去,并且经常变化的缓冲区: - - <!-- XXX is this correct? --> - <programlisting> bus_dmamap_create -> - -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> - -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> - ... - -> bus_dmamap_load -> bus_dmamap_sync(PRE...) -> do transfer -> - -> bus_dmamap_sync(POST...) -> bus_dmamap_unload -> - -> bus_dmamap_destroy </programlisting> - - </para> - <para> - 当加载由<function>bus_dmamem_alloc()</function>创建的映射时,传递 - 进去的缓冲区的地址和大小必须和 - <function>bus_dmamem_alloc()</function>中使用的一样。这种情况下就 - 可以保证整个缓冲区被作为一个段而映射(因而回调可以基于此假设), - 并且请求被立即执行(永远不会返回EINPROGRESS)。这种情况下回调函数 - 需要作的只是保存物理地址。 - </para> - <para> - 典型示例如下: - </para> - - <programlisting> static void - alloc_callback(void *arg, bus_dma_segment_t *seg, int nseg, int error) - { - *(bus_addr_t *)arg = seg[0].ds_addr; - } - - ... - int error; - struct somedata { - .... - }; - struct somedata *vsomedata; /* 虚地址 */ - bus_addr_t psomedata; /* 物理总线相关的地址 */ - bus_dma_tag_t tag_somedata; - bus_dmamap_t map_somedata; - ... - - error=bus_dma_tag_create(parent_tag, alignment, - boundary, lowaddr, highaddr, /*filter*/ NULL, /*filterarg*/ NULL, - /*maxsize*/ sizeof(struct somedata), /*nsegments*/ 1, - /*maxsegsz*/ sizeof(struct somedata), /*flags*/ 0, - &tag_somedata); - if(error) - return error; - - error = bus_dmamem_alloc(tag_somedata, &vsomedata, /* flags*/ 0, - &map_somedata); - if(error) - return error; - - bus_dmamap_load(tag_somedata, map_somedata, (void *)vsomedata, - sizeof (struct somedata), alloc_callback, - (void *) &psomedata, /*flags*/0); </programlisting> - - <para> - 代码看起来有点长,也比较复杂,但那是正确的使用方法。实际结果是: - 如果分配多个内存区域,则总将它们组合成一个结构并作为整体分配 - (如果对齐和边界限制允许的话)是一个很好的主意。 - </para> - <para> - 当加载任意缓冲区到由<function>bus_dmamap_create()</function> - 创建的映射时,由于回调可能被延迟,因此必须采取特殊措施与回调 - 函数进行同步。代码看起来像下面的样子: - </para> - - <programlisting> { - int s; - int error; - - s = splsoftvm(); - error = bus_dmamap_load( - dmat, - dmamap, - buffer_ptr, - buffer_len, - callback, - /*callback_arg*/ buffer_descriptor, - /*flags*/0); - if (error == EINPROGRESS) { - /* - * 执行必要的操作以确保与回调的同步。 - * 回调被确保直到我们执行了splx()或tsleep()才会被调用。 - */ - } - splx(s); - } </programlisting> - - <para> - 处理请求的两种方法分别是: - </para> - <para> - 1. 如果通过显式地标记请求已经结束来完成请求(例如CAM请求),则 - 将所有进一步的处理放入回调驱动程序中会比较简单,回调结束后会 - 标记请求。之后不需要太多额外的同步。由于流控制的原因,冻结请求 - 队列直到请求完成才释放可能是个好主意。 - </para> - <para> - 2. 如果请求是在函数返回时完成(例如字符设备上传统的读写请求), - 则需要在缓冲区描述符上设置同步标志,并调用 - <function>tsleep()</function>。后面当回调函数被调用时,它将 - 执行处理并检查同步标志。如果设置了同步标志,它应该发出一个 - 唤醒操作。在这种方法中,回调函数或者进行所由必需的处理(就像 - 前面的情况),或者简单在缓冲区描述符中存储段数组。回调完成 - 后,回调函数就能使用这个存储的段数组并进行所有的处理。 - </para> - </sect1> -<!--_________________________________________________________________________--> -<!--~~~~~~~~~~~~~~~~~~~~END OF SECTION~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--> - - <sect1 xml:id="isa-driver-dma"> - <title>DMA</title> - <!-- Section Marked up by Wylie --> - - <indexterm><primary>Direct Memory Access (DMA, 直接内存访问)</primary></indexterm> - - <para> - ISA总线中Direct Memory Access (DMA)是通过DMA控制器(实际上是它们 - 中的两个,但这只是无关细节)实现的。为了使以前的ISA设备简单便宜, - 总线控制和地址产生的逻辑都集中在DMA控制器中。幸运的是,FreeBSD - 提供了一套函数,这些函数大多把DMA控制器的繁琐细节对设备驱动程序 - 隐藏了起来。 - </para> - - <para> - 最简单情况是那些比较智能的设备。就象PCI上的总线主设备一样, - 它们自己能产生总线周期和内存地址。它们真正从DMA控制器需要的 - 唯一事情是总线仲裁。所以为了此目的,它们假装是级联从DMA控制器。 - 当连接驱动程序时,系统DMA控制器需要做的唯一事情就是通过调用 - 如下函数在一个DMA通道上激活级联模式。 - </para> - - <para> - <function>void isa_dmacascade(int channel_number)</function> - </para> - - <para> - 所有进一步的活动通过对设备编程完成。当卸载驱动程序时,不需要 - 调用DMA相关的函数。 - </para> - - <para> - 对于较简单的设备,事情反而变得复杂。使用的函数包括: - </para> - - <itemizedlist> - - <listitem> - <para> - <function>int isa_dma_acquire(int chanel_number)</function> - </para> - <para> - 保留一个DMA通道。成功则返回0,如果通道已经被保留或被其它 - 驱动程序保留则返回EBUSY。大多数的ISA设备都不能共享DMA通道, - 因此这个函数通常在连接设备时调用。总线资源的现代接口使得 - 这种保留成为多余,但目前仍必须使用。如果不使用,则后面其它 - DMA例程将会panic。 - </para> - </listitem> - - <listitem> - <para> - <function>int isa_dma_release(int chanel_number)</function> - </para> - <para> - 释放先前保留的DMA通道。释放通道时必须不能有正在进行中的 - 传输(另外,释放通道后设备必须不能再试图发起传输)。 - </para> - </listitem> - - <listitem> - <para> - <function>void isa_dmainit(int chan, u_int - bouncebufsize)</function> - </para> - <para> - 分配由特定通道使用的反弹缓冲区。请求的缓冲区大小不能超过 - 64KB。以后,如果传输缓冲区碰巧不是物理连续的,或超出ISA - 总线可访问的内存范围,或跨越64KB的边界,则会自动使用反弹 - 缓冲区。如果传输总是使用符合上述条件的缓冲区(例如,由 - <function>bus_dmamem_alloc()</function>分配的那些),则 - 不需要调用<function>isa_dmainit()</function>。但使用此函数 - 会让通过DMA控制器传输任意数据变得非常方便。 - </para> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - <emphasis>chan</emphasis> - 通道号 - </para> - </listitem> - <listitem> - <para> - <emphasis>bouncebufsize</emphasis> - 以字节计数的反弹 - 缓冲区的大小 - </para> - </listitem> - </itemizedlist> -<!-- </blockquote> --> -<!--</para> --> - </listitem> - - <listitem> - <para> - <function>void isa_dmastart(int flags, caddr_t addr, u_int - nbytes, int chan)</function> - </para> - <para> - 准备启动DMA传输。实际启动设备上的传输之前必需调用此函数 - 来设置DMA控制器。它检查缓冲区是否连续的且在ISA内存范围 - 之内,如果不是则自动使用反弹缓冲区。如果需要反弹缓冲区, - 但反弹缓冲区没有用<function>isa_dmainit()</function> - 设置,或对于请求的传输大小来说太小,则系统将panic。 - 写请求且使用反弹缓冲区的情况下,数据将被自动拷贝到反弹 - 缓冲区。 - </para> - </listitem> - <listitem> - <para>flags - 位掩码,决定将要完成的操作的类型。方向位B_READ和 - B_WRITE互斥。 - </para> - <!-- <blockquote> --> - <itemizedlist> - <listitem> - <para> - B_READ - 从ISA总线读到内存 - </para> - </listitem> - <listitem> - <para> - B_WRITE - 从内存写到ISA总线上 - </para> - </listitem> - <listitem> - <para> - B_RAW - 如果设置则DMA控制器将会记住缓冲区,并在传输结束后 - 自动重新初始化它自己,再次重复传输同一缓冲区(当然,驱动 - 程序可能发起设备的另一个传输之前改变缓冲区中的数据)。 - 如果没有设置,参数只对一次传输有效,在发起下一次传输之前 - 必须再次调用<function>isa_dmastart()</function>。只有在不 - 使用反弹缓冲区时使用B_RAW才有意义。 - </para> - </listitem> - </itemizedlist> -<!-- </blockquote> --> - </listitem> - <listitem> - <para> - addr - 缓冲区的虚地址 - </para> - </listitem> - <listitem> - <para> - nbytes - 缓冲区长度。必须小于等于64KB。不允许长度为0:因为 - DMA控制器将会理解为64KB,而内核代码把它理解为0,那样就会导致 - 不可预测的效果。对于通道号等于和高于4的情况,长度必需为偶数, - 因为这些通道每次传输2字节。奇数长度情况下,最后一个字节不被 - 传输。 - </para> - </listitem> - <listitem> - <para> - chan - 通道号 - </para> - </listitem> - - <listitem> - <para> - <function>void isa_dmadone(int flags, caddr_t addr, int - nbytes, int chan)</function> - </para> - <para> - 设备报告传输完成后,同步内存。如果是使用反弹缓冲区的读操作, - 则将数据从反弹缓冲区拷贝到原始缓冲区。参量与 - <function>isa_dmastart()</function>的相同。允许使用B_RAW标志, - 但它一点也不会影响<function>isa_dmadone()</function>。 - </para> - </listitem> - - <listitem> - <para> - <function>int isa_dmastatus(int channel_number)</function> - </para> - <para> - 返回当前传输中剩余的字节数。在 - <function>isa_dmastart()</function>中设置了B_READ的情况下, - 返回的数字一定不会等于零。传输结束时它会被自动复位到缓冲区的 - 长度。正式的用法是在设备发信号指示传输已完成时检查剩余的字节数。 - 如果字节数不为0,则此次传输可能有问题。 - </para> - </listitem> - - <listitem> - <para> - <function>int isa_dmastop(int channel_number)</function> - </para> - <para> - 放弃当前的传输并返回剩余未传输的字节数。 - </para> - </listitem> - </itemizedlist> - </sect1> - - <sect1 xml:id="isa-driver-probe"> - <title>xxx_isa_probe</title> - <!-- Section marked up by Wylie --> - - <para> - 这个函数探测设备是否存在。如果驱动程序支持自动侦测设备配置的 - 某些部分(如中断向量或内存地址),则自动侦测必须在此例程中完成。 - </para> - - <para> - 对于任意其他总线,如果不能侦测到设备,或者侦测到但自检失败, - 或者发生某些其他问题,则应当返回一个正值的错误。如果设备不 - 存在则必须返回值 <errorname>ENXIO</errorname>。 - 其他错误值可能表示其他条件。零或负值 - 意味着成功。大多数驱动程序返回零表示成功。 - </para> - - <para> - 当PnP设备支持多个接口时使用负返回值。例如,不同驱动程序支持 - 老的兼容接口和较新的高级接口。则两个驱动程序都将侦测设备。 - 在探测例程中返回较高值的驱动程序获得优先(换句话说,返回0的 - 驱动程序具有最高的优先级,返回-1的其次,返回-2的更后,等等)。 - 这样,仅支持老接口的设备将被老驱动程序处理(其应当从探测例程中 - 返回-1),而同时也支持新接口的设备将由新驱动程序处理(其应当从 - 探测例程中返回0)。 - </para> - - <para> - 设备描述符结构xxx_softc由系统在调用探测例程之前分配。如果 - 探测例程返回错误,描述符会被系统自动取消分配。因此如果出现 - 探测错误,驱动程序必须保证取消分配探测期间它使用的所有资源, - 且确保没有什么能够阻止描述符被安全地取消分配。如果探测成功 - 完成,描述符将由系统保存并在以后传递给例程 - <function>xxx_isa_attach()</function>。如果驱动程序返回负值, - 就不能保证它将获得最高优先权且其连接例程会被调用。因此这种 - 情况下它也必须在返回前释放所有的资源,并在需要的时候在连接 - 例程中重新分配它们。当<function>xxx_isa_probe()</function> - 返回0时,在返回前释放资源也是一个好主意,而且中规中矩的驱动 - 程序应当这样做。但在释放资源会存在某些问题的情况下,允许驱动 - 程序在从探测例程返回0和连接例程的执行之间保持资源。 - </para> - - <para> - 典型的探测例程以取得设备描述符和单元号开始: - </para> - - <programlisting> struct xxx_softc *sc = device_get_softc(dev); - int unit = device_get_unit(dev); - int pnperror; - int error = 0; - - sc->dev = dev; /* 链接回来 */ - sc->unit = unit; </programlisting> - - <para> - 然后检查PnP设备。检查是通过一个包含PnP ID列表的表进行的。此表 - 包含这个驱动程序支持的PnP ID和以人工可读形式给出的对应这些ID的 - 设备型号的描述。 - </para> - - <programlisting> - pnperror=ISA_PNP_PROBE(device_get_parent(dev), dev, - xxx_pnp_ids); if(pnperror == ENXIO) return ENXIO; - </programlisting> - - <para> - ISA_PNP_PROBE的逻辑如下:如果卡(设备单元)没有被作为PnP侦测到, - 则返回ENOENT。如果被作为PnP侦测到,但侦测到的ID不匹配表中的 - 任一ID,则返回ENXIO。最后,如果设备能支持PnP且匹配表中的一个 - ID,则返回0,并且由<function>device_set_desc()</function>从 - 表中取得适当的描述进行设置。 - </para> - - <para> - 如果设备驱动程序仅支持PnP设备,则情况看起来如下: - </para> - - <programlisting> if(pnperror != 0) - return pnperror; </programlisting> - - <para> - 对于不支持PnP的驱动程序不需要特殊处理,因为驱动程序会传递空的 - PnP ID表,且如果在PnP卡上调用会得到ENXIO。 - </para> - - <para> - 探测例程通常至少需要某些最少量的资源,如I/O端口号,来发现并探测卡。 - 对于不同的硬件,驱动程序可能会自动发现其他必需的资源。PnP设备的 - 所有资源由PnP子系统预先设置,因此驱动程序不需要自己发现它们。</para> - - <para> - 通常访问设备所需要的最少信息就是端口号。然后某些设备允许从设备 - 配置寄存器中取得其余信息(尽管不是所有的设备都这样)。因此首先 - 我们尝试取得端口起始值: - </para> - - <programlisting> sc->port0 = bus_get_resource_start(dev, - SYS_RES_IOPORT, 0 /*rid*/); if(sc->port0 == 0) return ENXIO; - </programlisting> - - <para> - 基端口地址被保存在softc结构中,以便将来使用。如果需要经常使用 - 端口,则每次都调用资源函数将会慢的无法忍受。如果我们没有得到 - 端口,则返回错误即可。相反,一些设备驱动程序相当聪明,尝试探测 - 所有可能的端口,如下: - </para> - - <programlisting> - /* 此设备所有可能的基I/O端口地址表 */ - static struct xxx_allports { - u_short port; /* 端口地址 */ - short used; /* 旗标:此端口是否已被其他单元使用 */ - } xxx_allports = { - { 0x300, 0 }, - { 0x320, 0 }, - { 0x340, 0 }, - { 0, 0 } /* 表结束 */ - }; - - ... - int port, i; - ... - - port = bus_get_resource_start(dev, SYS_RES_IOPORT, 0 /*rid*/); - if(port !=0 ) { - for(i=0; xxx_allports[i].port!=0; i++) { - if(xxx_allports[i].used || xxx_allports[i].port != port) - continue; - - /* 找到了 */ - xxx_allports[i].used = 1; - /* 在已知端口上探测 */ - return xxx_really_probe(dev, port); - } - return ENXIO; /* 端口无法识别或已经被使用 */ - } - - /* 仅在需要猜测端口的时候才会到达这儿 */ - for(i=0; xxx_allports[i].port!=0; i++) { - if(xxx_allports[i].used) - continue; - - /* 标记为已被使用 - 即使我们在此端口什么也没有发现 - * 至少我们以后不会再次探测 - */ - xxx_allports[i].used = 1; - - error = xxx_really_probe(dev, xxx_allports[i].port); - if(error == 0) /* 在那个端口找到一个设备 */ - return 0; - } - /* 探测过所有可能的地址,但没有可用的 */ - return ENXIO;</programlisting> - - <para> - 当然,做这些事情通常应该使用驱动程序的 - <function>identify()</function>例程。但可能有一个正当的理由来 - 说明为什么在函数<function>probe()</function>中完成更好:如果 - 这种探测会让一些其他敏感设备发疯。探测例程按旗标 - <literal>sensitive</literal>排序:敏感设备首先被探测,然后是 - 其他设备。但<function>identify()</function>例程在所有探测之前 - 被调用,因此它们不会考虑敏感设备并可能扰乱这些设备。 - </para> - - <para> - 现在,我们得到起始端口以后就需要设置端口数(PnP设备除外),因为 - 内核在配置文件中没有这个信息。 - </para> - - <programlisting> - if(pnperror /* 只对非PnP设备 */ - && bus_set_resource(dev, SYS_RES_IOPORT, 0, sc->port0, - XXX_PORT_COUNT)<0) - return ENXIO;</programlisting> - - <para> - 最后分配并激活一片端口地址空间(特殊值start和end意思是说 - <quote>使用我们通过<function>bus_set_resource()</function> - 设置的那些值</quote>): - </para> - - <programlisting> - sc->port0_rid = 0; - sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT, - &sc->port0_rid, - /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); - - if(sc->port0_r == NULL) - return ENXIO;</programlisting> - - <para> - 现在可以访问端口映射的寄存器后,我们就可以以某种方式向设备写入 - 数据并检查设备是否如我们期望的那样作出反应。如果没有,则说明 - 可能其他的设备在这个地址上,或者这个地址上根本没有设备。 - </para> - - <para> - 通常驱动程序直到连接例程才会设置中断处理函数。这之前我们替代以 - 轮询模式进行探测,超时则以<function>DELAY()</function>实现。 - 探测例程必须确保不能永久挂起,设备上的所有等待必须在超时内完成。 - 如果设备不在这段时间内响应,则可能设备出故障或配置错误,驱动程序 - 必须返回错误,当确定超时间隔时,给设备一些额外时间以确保可靠: - 尽管假定<function>DELAY()</function>在任何机器上都延时相同数量的 - 时间,但随具体CPU的不同,此函数还是有一定的误差幅度。 - </para> - - <para> - 如果探测例程真的想检查中断是否真的工作,它可以也配置和探测中断。 - 但不建议这样。 - </para> - - <programlisting> - /* 以严重依赖于具体设备的方式实现 */ - if(error = xxx_probe_ports(sc)) - goto bad; /* 返回前释放资源 */ - </programlisting> - - <para> - 依赖于所发现设备的确切型号,函数 - <function>xxx_probe_ports()</function>也可能设置设备描述。但 - 如果只支持一种设备型号,则也可以硬编码的形式完成。当然,对于 - PnP设备,PnP支持从表中自动设置描述。 - </para> - - - <programlisting> if(pnperror) - device_set_desc(dev, "Our device model 1234"); - </programlisting> - - <para> - 探测例程应当或者通过读取设备配置寄存器来发现所有资源的范围, - 或者确保由用户显式设置。我们将假定一个带板上内存的例子。 - 探测例程应当尽可能是非插入式的,这样分配和检查其余资源功能性 - 的工作就可以更好地留给连接例程来做。 - </para> - - <para> - 内存地址可以在内核配置文件中指定,或者对应某些设备可以在非易失性 - 配置寄存器中预先配置。如果两种做法均可用却不同,那么应当用 - 哪个呢?可能用户厌烦在内核配置文件中明确设置地址,但他们知道 - 自己在干什么,则应当优先使用这个。一个实现的例子可能是这样的: - </para> - <programlisting> - /* 首先试图找出配置地址 */ - sc->mem0_p = bus_get_resource_start(dev, SYS_RES_MEMORY, 0 /*rid*/); - if(sc->mem0_p == 0) { /* 没有,用户没指定 */ - sc->mem0_p = xxx_read_mem0_from_device_config(sc); - - - if(sc->mem0_p == 0) - /* 从设备配置寄存器也到不了这儿 */ - goto bad; - } else { - if(xxx_set_mem0_address_on_device(sc) < 0) - goto bad; /* 设备不支持那地址 */ - } - - /* 就像端口,设置内存大小, - * 对于某些设备,内存大小不是常数, - * 而应当从设备配置寄存器中读取,以适应设备的不同型号 - * 另一个选择是让用户把内存大小设置为“msize”配置资源, - * 由ISA总线自动处理 - */ - if(pnperror) { /*仅对非PnP设备 */ - sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/); - if(sc->mem0_size == 0) /* 用户没有指定 */ - sc->mem0_size = xxx_read_mem0_size_from_device_config(sc); - - if(sc->mem0_size == 0) { - /* 假定这是设备非常老的一种型号,没有自动配置特性, - * 用户也没有偏好设置,因此假定最低要求的情况 - * (当然,真实值将根据设备驱动程序而不同) - */ - sc->mem0_size = 8*1024; - } - - if(xxx_set_mem0_size_on_device(sc) < 0) - goto bad; /*设备不支持那个大小 */ - - if(bus_set_resource(dev, SYS_RES_MEMORY, /*rid*/0, - sc->mem0_p, sc->mem0_size)<0) - goto bad; - } else { - sc->mem0_size = bus_get_resource_count(dev, SYS_RES_MEMORY, 0 /*rid*/); - } </programlisting> - - <para> - 类似, 很容易检查IRQ和DRQ所用的资源。 - </para> - - <para> - 如果一切进行正常,然后就可以释放所有资源并返回成功。 - </para> - - <programlisting> xxx_free_resources(sc); - return 0;</programlisting> - - <para> - 最后,处理棘手情况。所有资源应当在返回前被释放。我们利用这样一个 - 事实:softc结构在传递给我们以前被零化,因此我们能够找出是否分配了 - 某些资源:如果分配则这些资源的描述符非零。 - </para> - - <programlisting> bad: - - xxx_free_resources(sc); - if(error) - return error; - else /* 确切错误未知 */ - return ENXIO;</programlisting> - - <para> - 这是完整的探测例程。资源的释放从多个地方完成,因此将它挪到一个 - 函数中,看起来可能像下面的样子: - </para> - -<programlisting>static void - xxx_free_resources(sc) - struct xxx_softc *sc; - { - /* 检查每个资源,如果非0则释放 */ - - /* 中断处理函数 */ - if(sc->intr_r) { - bus_teardown_intr(sc->dev, sc->intr_r, sc->intr_cookie); - bus_release_resource(sc->dev, SYS_RES_IRQ, sc->intr_rid, - sc->intr_r); - sc->intr_r = 0; - } - - /* 我们分配过的所有种类的内存 */ - if(sc->data_p) { - bus_dmamap_unload(sc->data_tag, sc->data_map); - sc->data_p = 0; - } - if(sc->data) { /* sc->data_map等于0有可能合法 */ - /* the map will also be freed */ - bus_dmamem_free(sc->data_tag, sc->data, sc->data_map); - sc->data = 0; - } - if(sc->data_tag) { - bus_dma_tag_destroy(sc->data_tag); - sc->data_tag = 0; - } - - ... 如果有,释放其他的映射和标签 ... - - if(sc->parent_tag) { - bus_dma_tag_destroy(sc->parent_tag); - sc->parent_tag = 0; - } - - /* 释放所有总线资源 */ - if(sc->mem0_r) { - bus_release_resource(sc->dev, SYS_RES_MEMORY, sc->mem0_rid, - sc->mem0_r); - sc->mem0_r = 0; - } - ... - if(sc->port0_r) { - bus_release_resource(sc->dev, SYS_RES_IOPORT, sc->port0_rid, - sc->port0_r); - sc->port0_r = 0; - } - }</programlisting> - - </sect1> - - <sect1 xml:id="isa-driver-attach"> - <title>xxx_isa_attach</title> - <!-- Section Marked up by Wylie --> - - <para>如果探测例程返回成功并且系统选择连接那个驱动程序,则连接例程 - 负责将驱动程序实际连接到系统。如果探测例程返回0 ,则连接例程期望 - 接收完整的设备结构softc,此结构由探测例程设置。同时,如果探测例程 - 返回0,它可能期望这个设备的连接例程应当在将来的某点被调用。如果 - 探测例程返回负值,则驱动程序可能不会作此假设。 - </para> - - <para>如果成功完成,连接例程返回0,否则返回错误码。 - </para> - - <para>连接例程的启动跟探测例程相似,将一些常用数据取到一些更容易 - 访问的变量中。 - </para> - - <programlisting> struct xxx_softc *sc = device_get_softc(dev); - int unit = device_get_unit(dev); - int error = 0;</programlisting> - - <para>然后分配并激活所需资源。由于端口范围通常在从探测返回前就 - 被释放,因此需要重新分配。我们希望探测例程已经适当地设置了 - 所有的资源范围,并将它们保存在结构softc中。如果探测例程留下了 - 一些被分配的资源,就不需要再次分配(重新分配被视为错误)。 - </para> - - <programlisting> sc->port0_rid = 0; - sc->port0_r = bus_alloc_resource(dev, SYS_RES_IOPORT, &sc->port0_rid, - /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); - - if(sc->port0_r == NULL) - return ENXIO; - - /* 板上内存 */ - sc->mem0_rid = 0; - sc->mem0_r = bus_alloc_resource(dev, SYS_RES_MEMORY, &sc->mem0_rid, - /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); - - if(sc->mem0_r == NULL) - goto bad; - - /* 取得虚地址 */ - sc->mem0_v = rman_get_virtual(sc->mem0_r);</programlisting> - - <para>DMA请求通道(DRQ)以相似方式被分配。使用 - <function>isa_dma*()</function>函数族进行初始化。例如: - </para> - - <para><function>isa_dmacascade(sc->drq0);</function></para> - - <para>中断请求线(IRQ)有点特殊。除了分配以外,驱动程序的中断处理 - 函数也应当与它关联。在古老的ISA驱动程序中,由系统传递给中断处理 - 函数的参量是设备单元号。但在现代驱动程序中,按照约定,建议传递 - 指向结构softc的指针。一个很重要的原因在于当结构softc被动态分配后, - 从softc取得单元号很容易,而从单元号取得softc很困难。同时,这个 - 约定也使得用于不同总线的应用程序看起来统一,并允许它们共享代码: - 每个总线有其自己的探测,连接,分离和其他总线相关的例程,而它们 - 之间可以共享大块的驱动程序代码。 - </para> - - <programlisting> - sc->intr_rid = 0; - sc->intr_r = bus_alloc_resource(dev, SYS_RES_MEMORY, &sc->intr_rid, - /*start*/ 0, /*end*/ ~0, /*count*/ 0, RF_ACTIVE); - - if(sc->intr_r == NULL) - goto bad; - - /* - * 假定对XXX_INTR_TYPE的定义依赖于驱动程序的类型, - * 例如INTR_TYPE_CAM用于CAM的驱动程序 - */ - error = bus_setup_intr(dev, sc->intr_r, XXX_INTR_TYPE, - (driver_intr_t *) xxx_intr, (void *) sc, &sc->intr_cookie); - if(error) - goto bad; - - </programlisting> - - - <para>如果驱动程序需要与内存进行DMA,则这块内存应当按前述方式分配: - </para> - - <programlisting> error=bus_dma_tag_create(NULL, /*alignment*/ 4, - /*boundary*/ 0, /*lowaddr*/ BUS_SPACE_MAXADDR_24BIT, - /*highaddr*/ BUS_SPACE_MAXADDR, /*filter*/ NULL, /*filterarg*/ NULL, - /*maxsize*/ BUS_SPACE_MAXSIZE_24BIT, - /*nsegments*/ BUS_SPACE_UNRESTRICTED, - /*maxsegsz*/ BUS_SPACE_MAXSIZE_24BIT, /*flags*/ 0, - &sc->parent_tag); - if(error) - goto bad; - - /* 很多东西是从父标签继承而来 - * 假设sc->data指向存储共享数据的结构,例如一个环缓冲区可能是: - * struct { - * u_short rd_pos; - * u_short wr_pos; - * char bf[XXX_RING_BUFFER_SIZE] - * } *data; - */ - error=bus_dma_tag_create(sc->parent_tag, 1, - 0, BUS_SPACE_MAXADDR, 0, /*filter*/ NULL, /*filterarg*/ NULL, - /*maxsize*/ sizeof(* sc->data), /*nsegments*/ 1, - /*maxsegsz*/ sizeof(* sc->data), /*flags*/ 0, - &sc->data_tag); - if(error) - goto bad; - - error = bus_dmamem_alloc(sc->data_tag, &sc->data, /* flags*/ 0, - &sc->data_map); - if(error) - goto bad; - - /* 在&sc->data_p的情况下,xxx_alloc_callback()只是将物理地址 - * 保存到作为其参量传递进去的指针中。 - * 参看关于总线内存映射一节中的详细内容。 - * 其实现可以像这样: - * - * static void - * xxx_alloc_callback(void *arg, bus_dma_segment_t *seg, - * int nseg, int error) - * { - * *(bus_addr_t *)arg = seg[0].ds_addr; - * } - */ - bus_dmamap_load(sc->data_tag, sc->data_map, (void *)sc->data, - sizeof (* sc->data), xxx_alloc_callback, (void *) &sc->data_p, - /*flags*/0);</programlisting> - - - <para>分配了所有的资源后,设备应当被初始化。初始化可能包括测试 - 所有特性,确保它们起作用。</para> - - <programlisting> if(xxx_initialize(sc) < 0) - goto bad; </programlisting> - - - <para>总线子系统将自动在控制台上打印由探测例程设置的设备描述。但 - 如果驱动程序想打印一些关于设备的额外信息,也是可能的,例如:</para> - - <programlisting> - device_printf(dev, "has on-card FIFO buffer of %d bytes\n", sc->fifosize); - </programlisting> - - <para>如果初始化例程遇到任何问题,建议返回错误之前打印有关信息。</para> - - <para>连接例程的最后一步是将设备连接到内核中的功能子系统。完成 - 这个步骤的精确方式依赖于驱动程序的类型:字符设备、块设备、网络 - 设备、CAM SCSI总线设备等等。</para> - - <para>如果所有均工作正常则返回成功。</para> - - <programlisting> error = xxx_attach_subsystem(sc); - if(error) - goto bad; - - return 0; </programlisting> - - <para>最后,处理棘手情况。返回错误前,所有资源应当被取消分配。 - 我们利用这样一个事实:结构softc传递给我们之前被零化,因此我们 - 能找出是否分配了某些资源:如果分配则它们的描述符非零。</para> - - <programlisting> bad: - - xxx_free_resources(sc); - if(error) - return error; - else /* exact error is unknown */ - return ENXIO;</programlisting> - - <para>这就是连接例程的全部。</para> - - </sect1> - - - <sect1 xml:id="isa-driver-detach"> - <title>xxx_isa_detach</title> - - <para> - 如果驱动程序中存在这个函数,且驱动程序被编译为可加载模块,则 - 驱动程序具有被卸载的能力。如果硬件支持热插拔,这是一个很重要的 - 特性。但ISA总线不支持热插拔,因此这个特性对于ISA设备不是特别 - 重要。卸载驱动程序的能力可能在调试时有用,但很多情况下只有在 - 老版本的驱动程序莫名其妙地卡住系统的情况下才需要安装新版本的 - 驱动程序,并且无论如何都需要重启,这样使得花费精力写分离例程 - 有些不值得。另一个宣称卸载允许在用于生产的机器上升级驱动程序的 - 论点看起来似乎更多的只是理论而已。升级驱动程序是一项危险的操作, - 决不不应当在用于生产的机器上实行(并且当系统运行于安全模式时这 - 也是不被允许的)。然而,出于完整性考虑,还是会提供分离例程。 - </para> - - <para> - 如果驱动程序成功分离,分离例程返回0,否则返回错误码。 - </para> - - <para> - 分离逻辑是连接的镜像。要做的第一件事情就是将驱动程序从内核 - 子系统分离。如果设备当前正打开着,驱动程序有两个选择:拒绝分离 - 或者强制关闭并继续进行分离。选用哪种方式取决于特定内核子系统 - 执行强制关闭的能力和驱动程序作者的偏好。通常强制关闭似乎是 - 更好的选择。 - <programlisting> struct xxx_softc *sc = device_get_softc(dev); - int error; - - error = xxx_detach_subsystem(sc); - if(error) - return error;</programlisting> - </para> - <para> - 下一步,驱动程序可能希望复位硬件到某种一致的状态。包括停止任何 - 将要进行的传输,禁用DMA通道和中断以避免设备破坏内存。对于大多数 - 驱动程序而言,这正是关闭例程所做的,因此如果驱动程序中包括关闭 - 例程,我们只要调用它就可以了。 - </para> - <para><function>xxx_isa_shutdown(dev);</function></para> - - <para> - 最后释放所有资源并返回成功。 - <programlisting> xxx_free_resources(sc); - return 0;</programlisting> - - </para> - </sect1> - - <sect1 xml:id="isa-driver-shutdown"> - <title>xxx_isa_shutdown</title> - - <para> - 当系统要关闭的时候调用此例程。通过它使硬件进入某种一致的状态。 - 对于大多数ISA设备而言不需要特殊动作,因此这个函数并非真正必需, - 因为不管怎样重启动时设备会被重新初始化。但有些设备必须按特定 - 步骤关闭,以确保在软重启后能被正确地检测到(对于很多使用私有 - 识别协议的设备特别有用)。很多情况下,在设备寄存器中禁用DMA和 - 中断,并停止将要进行的传输是个好主意。确切动作取决于硬件,因此 - 我们无法在此详细讨论。 - </para> - </sect1> - - <sect1 xml:id="isa-driver-intr"> - <title>xxx_intr</title> - - <indexterm><primary>interrupt handler(中断处理程序)</primary></indexterm> - - <para> - 当收到来自特定设备的中断时就会调用中断处理函数。ISA总线不支持 - 中断共享(某些特殊情况例外),因此实际上如果中断处理函数被调用, - 几乎可以确信中断是来自其设备。然而,中断处理函数必须轮询设备 - 寄存器并确保中断是由它的设备产生的。如果不是,中断处理函数应当 - 返回。 - </para> - - <para> - ISA驱动程序的旧约定是取设备单元号作为参量。现在已经废弃,当 - 调用<function>bus_setup_intr()</function>时新驱动程序接收任何 - 在连接例程中为他们指定的参量。根据新约定,它应当是指向结构 - softc的指针。因此中断处理函数通常像下面那样开始: - </para> - - <programlisting> - static void - xxx_intr(struct xxx_softc *sc) - { - - </programlisting> - - <para> - 它运行在由<function>bus_setup_intr()</function>的中断类型参数指定 - 的中断优先级上。这意味着禁用所有其他同类型的中断和所有软件中断。 - </para> - - <para> - 为了避免竞争,中断处理例程通写成循环形式: - </para> - - <programlisting> - while(xxx_interrupt_pending(sc)) { - xxx_process_interrupt(sc); - xxx_acknowledge_interrupt(sc); - } </programlisting> - - <para> - 中断处理函数必须只向设备应答中断,但不能向中断控制器应答,后者由 - 系统负责处理。 - </para> - - </sect1> -</chapter> diff --git a/zh_CN.GB2312/books/arch-handbook/jail/chapter.xml b/zh_CN.GB2312/books/arch-handbook/jail/chapter.xml deleted file mode 100644 index 0150b243f3..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/jail/chapter.xml +++ /dev/null @@ -1,636 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.22 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="jail"> - <info><title>Jail子系统</title> - <author><personname><firstname>Evan</firstname><surname>Sarmiento</surname></personname><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><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <indexterm><primary>security(安全)</primary></indexterm> - <indexterm><primary>Jail(囚禁)</primary></indexterm> - <indexterm><primary>root(根用户,管理员用户)</primary></indexterm> - <para>在大多数&unix;系统中,用户<literal>root</literal>是万能的。这也就增加了许多危险。 - 如果一个攻击者获得了一个系统中的<literal>root</literal>,就可以在他的指尖掌握系统中所有的功能。 - 在FreeBSD里,有一些sysctl项削弱了<literal>root</literal>的权限, - 这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级别。 - 另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是&man.jail.8;。 - <application>Jail</application>将一个运行环境的文件树根切换到某一特定位置, - 并且对这样环境中叉分生成的进程做出限制。例如, - 一个被监禁的进程不能影响这个<application>jail</application>之外的进程、不能使用一些特定的系统调用, - 也就不能对主计算机造成破坏。<tip><title>译者注</title> - <para>英文单词“jail”的中文意思是“囚禁、监禁”。</para></tip></para> - - <para><application>Jail</application>已经成为一种新型的安全模型。 - 人们可以在jail中运行各种可能很脆弱的服务器程序,如<application>Apache</application>、 - <application>BIND</application>和<application>sendmail</application>。 - 这样一来,即使有攻击者取得了<application>jail</application>中的<literal>root</literal>, - 这最多让人们皱皱眉头,而不会使人们惊慌失措。 - 本文主要关注<application>jail</application>的内部原理(源代码)。 - 如果你正在寻找设置<application>Jail</application>的指南性文档, - 我建议你阅读我的另一篇文章,发表在Sys Admin Magazine, May 2001, - 《Securing FreeBSD using <application>Jail</application>》。</para> - - <sect1 xml:id="jail-arch"> - <title>Jail的系统结构</title> - - <para><application>Jail</application>由两部分组成:用户级程序, - 也就是&man.jail.8;;还有在内核中Jail的实现代码:&man.jail.2; - 系统调用和相关的约束。我将讨论用户级程序和<application>jail</application>在内核中的实现原理。</para> - - <sect2> - <title>用户级代码</title> - - <indexterm><primary>Jail(囚禁)</primary> - <secondary>userland program(用户级程序)</secondary></indexterm> - - <para><application>Jail</application>的用户级源代码在<filename>/usr/src/usr.sbin/jail</filename>, - 由一个文件<filename>jail.c</filename>组成。这个程序有这些参数:<application>jail</application>的路径, - 主机名,IP地址,还有需要执行的命令。</para> - - <sect3> - <title>数据结构</title> - - <para>在<filename>jail.c</filename>中,我将最先注解的是一个重要结构体 - <literal>struct jail j;</literal>的声明,这个结构类型的声明包含在 - <filename>/usr/include/sys/jail.h</filename>之中。</para> - - <para><literal>jail</literal>结构的定义是:</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> -char path[PATH_MAX]; -... -if(realpath(argv[0], path) == NULL) - err(1, "realpath: %s", argv[0]); -if (chdir(path) != 0) - err(1, "chdir: %s", path); -memset(&j, 0, sizeof(j)); -j.version = 0; -j.path = path; -j.hostname = argv[1];</programlisting> - - </sect3> - - <sect3> - <title>网络</title> - - <para>传给&man.jail.8;的参数中有一个是IP地址。这是在网络上访问<application>jail</application>时的地址。 - &man.jail.8;将IP地址翻译成网络字节顺序,并存入<literal>j</literal>(<literal>jail</literal>类型的结构体)。</para> - - <programlisting><filename>/usr/src/usr.sbin/jail/jail.c</filename>: -struct in_addr in; -... -if (inet_aton(argv[2], &in) == 0) - errx(1, "Could not make sense of ip-number: %s", argv[2]); -j.ip_number = ntohl(in.s_addr);</programlisting> - - <para>函数&man.inet.aton.3;“将指定的字符串解释为一个Internet地址, - 并将其转存到指定的结构体中”。&man.inet.aton.3;设定了结构体in, - 之后in中的内容再用&man.ntohl.3;转换成主机字节顺序, - 并置入<literal>jail</literal>结构体的<literal>ip_number</literal>成员。</para> - - </sect3> - - <sect3> - <title>囚禁进程</title> - - <para>最后,用户级程序囚禁进程。现在Jail自身变成了一个被囚禁的进程, - 并使用&man.execv.3;执行用户指定的命令。</para> - - <programlisting><filename>/usr/src/usr.sbin/jail/jail.c</filename> -i = jail(&j); -... -if (execv(argv[3], argv + 3) != 0) - err(1, "execv: %s", argv[3]);</programlisting> - - <para>正如你所见,函数<literal>jail()</literal>被调用,参数是结构体<literal>jail</literal>中被填入数据项, - 而如前所述,这些数据项又来自&man.jail.8;的命令行参数。 - 最后,执行了用户指定的命令。下面我将开始讨论<literal>jail</literal>在内核中的实现。</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>。 - 在这里定义了&man.jail.2;的系统调用、相关的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(_security_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW, - &jail_set_hostname_allowed, 0, - "Processes in jail can set their hostnames"); - /* Jail中的进程可设定自身的主机名 */ - -int jail_socket_unixiproute_only = 1; -SYSCTL_INT(_security_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW, - &jail_socket_unixiproute_only, 0, - "Processes in jail are limited to creating UNIX/IPv4/route sockets only"); - /* Jail中的进程被限制只能建立UNIX套接字、IPv4套接字、路由套接字 */ - -int jail_sysvipc_allowed = 0; -SYSCTL_INT(_security_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW, - &jail_sysvipc_allowed, 0, - "Processes in jail can use System V IPC primitives"); - /* Jail中的进程可以使用System V进程间通讯原语 */ - -static int jail_enforce_statfs = 2; -SYSCTL_INT(_security_jail, OID_AUTO, enforce_statfs, CTLFLAG_RW, - &jail_enforce_statfs, 0, - "Processes in jail cannot see all mounted file systems"); - /* jail 中的进程查看系统中挂接的文件系统时受到何种限制 */ - -int jail_allow_raw_sockets = 0; -SYSCTL_INT(_security_jail, OID_AUTO, allow_raw_sockets, CTLFLAG_RW, - &jail_allow_raw_sockets, 0, - "Prison root can create raw sockets"); - /* jail 中的 root 用户是否可以创建 raw socket */ - -int jail_chflags_allowed = 0; -SYSCTL_INT(_security_jail, OID_AUTO, chflags_allowed, CTLFLAG_RW, - &jail_chflags_allowed, 0, - "Processes in jail can alter system file flags"); - /* jail 中的进程是否可以修改系统级文件标记 */ - -int jail_mount_allowed = 0; -SYSCTL_INT(_security_jail, OID_AUTO, mount_allowed, CTLFLAG_RW, - &jail_mount_allowed, 0, - "Processes in jail can mount/unmount jail-friendly file systems"); - /* jail 中的进程是否可以挂载或卸载对jail友好的文件系统 */</programlisting> - - <para>这些sysctl项中的每一个都可以用命令&man.sysctl.8;访问。在整个内核中, - 这些sysctl项按名称标识。例如,上述第一个sysctl项的名字是 - <literal>security.jail.set_hostname_allowed</literal>。</para> - </sect3> - - <sect3> - <title>&man.jail.2;系统调用</title> - - <para>像所有的系统调用一样,系统调用&man.jail.2;带有两个参数, - <literal>struct thread *td</literal>和<literal>struct jail_args *uap</literal>。 - <literal>td</literal>是一个指向<literal>thread</literal>结构体的指针,该指针用于描述调用&man.jail.2;的线程。 - 在这个上下文中,<literal>uap</literal>指向一个结构体,这个结构体中包含了一个指向从用户级 - <filename>jail.c</filename>传送过来的<literal>jail</literal>结构体的指针。 - 在前面我讲述用户级程序时,你已经看到过一个<literal>jail</literal>结构体被作为参数传送给系统调用 - &man.jail.2;。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_jail.c:</filename> -/* - * struct jail_args { - * struct jail *jail; - * }; - */ -int -jail(struct thread *td, struct jail_args *uap)</programlisting> - - <para>于是<literal>uap->jail</literal>可以用于访问被传递给&man.jail.2;的<literal>jail</literal>结构体。 - 然后,&man.jail.2;使用&man.copyin.9;将<literal>jail</literal>结构体复制到内核内存空间中。 - &man.copyin.9;需要三个参数:要复制进内核内存空间的数据的地址 - <literal>uap->jail</literal>,在内核内存空间存放数据的<literal>j</literal>, - 以及数据的大小。<literal>uap->jail</literal>指向的Jail结构体被复制进内核内存空间, - 并被存放在另一个<literal>jail</literal>结构体<literal>j</literal>里。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_jail.c: </filename> -error = copyin(uap->jail, &j, sizeof(j));</programlisting> - - <para>在jail.h中定义了另一个重要的结构体型prison。 - 结构体<literal>prison</literal>只被用在内核空间中。 - 下面是<literal>prison</literal>结构体的定义。</para> - - <programlisting><filename>/usr/include/sys/jail.h</filename>: -struct prison { - LIST_ENTRY(prison) pr_list; /* (a) all prisons */ - int pr_id; /* (c) prison id */ - int pr_ref; /* (p) refcount */ - char pr_path[MAXPATHLEN]; /* (c) chroot path */ - struct vnode *pr_root; /* (c) vnode to rdir */ - char pr_host[MAXHOSTNAMELEN]; /* (p) jail hostname */ - u_int32_t pr_ip; /* (c) ip addr host */ - void *pr_linux; /* (p) linux abi */ - int pr_securelevel; /* (p) securelevel */ - struct task pr_task; /* (d) destroy task */ - struct mtx pr_mtx; - void **pr_slots; /* (p) additional data */ -};</programlisting> - - <para>然后,系统调用&man.jail.2;为一个<literal>prison</literal>结构体分配一块内存, - 并在<literal>jail</literal>和<literal>prison</literal>结构体之间复制数据。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename>: -MALLOC(pr, struct prison *, sizeof(*pr), M_PRISON, M_WAITOK | M_ZERO); -... -error = copyinstr(j.path, &pr->pr_path, sizeof(pr->pr_path), 0); -if (error) - goto e_killmtx; -... -error = copyinstr(j.hostname, &pr->pr_host, sizeof(pr->pr_host), 0); -if (error) - goto e_dropvnref; -pr->pr_ip = j.ip_number;</programlisting> - - <para>下面,我们将讨论另外一个重要的系统调用&man.jail.attach.2;,它实现了将进程监禁的功能。</para> - <programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename> -/* - * struct jail_attach_args { - * int jid; - * }; - */ -int -jail_attach(struct thread *td, struct jail_attach_args *uap)</programlisting> - <para>这个系统调用做出一些可以用于区分被监禁和未被监禁的进程的改变。 - 要理解&man.jail.attach.2;为我们做了什么,我们首先要理解一些背景信息。</para> - <para>在FreeBSD中,每个对内核可见的线程是通过其<literal>thread</literal>结构体来识别的, - 同时,进程都由它们自己的<literal>proc</literal>结构体描述。 - 你可以在<filename>/usr/include/sys/proc.h</filename>中找到<literal>thread</literal>和<literal>proc</literal>结构体的定义。 - 例如,在任何系统调用中,参数<literal>td</literal>实际上是个指向调用线程的<literal>thread</literal>结构体的指针, - 正如前面所说的那样。<literal>td</literal>所指向的<literal>thread</literal>结构体中的<literal>td_proc</literal>成员是一个指针, - 这个指针指向<literal>td</literal>所表示的线程所属进程的<literal>proc</literal>结构体。 - 结构体<literal>proc</literal>包含的成员可以描述所有者的身份 - (<literal>p_ucred</literal>),进程资源限制(<literal>p_limit</literal>), - 等等。在由<literal>proc</literal>结构体的<literal>p_ucred</literal>成员所指向的ucred结构体的定义中, - 还有一个指向<literal>prison</literal>结构体的指针(<literal>cr_prison</literal>)。</para> - <programlisting><filename>/usr/include/sys/proc.h: </filename> -struct thread { - ... - struct proc *td_proc; - ... -}; -struct proc { - ... - struct ucred *p_ucred; - ... -}; -<filename>/usr/include/sys/ucred.h</filename> -struct ucred { - ... - struct prison *cr_prison; - ... -};</programlisting> - - <para>在<filename>kern_jail.c</filename>中,函数<literal>jail()</literal>以给定的<literal>jid</literal> - 调用函数<literal>jail_attach()</literal>。随后<literal>jail_attach()</literal>调用函数<literal>change_root()</literal>以改变 - 调用进程的根目录。接下来,<literal>jail_attach()</literal>创建一个新的<literal>ucred</literal>结构体,并在 - 成功地将<literal>prison</literal>结构体连接到这个<literal>ucred</literal>结构体后,将这个<literal>ucred</literal>结构体连接 - 到调用进程上。从此时起,这个调用进程就会被识别为被监禁的。 - 当我们以新创建的这个<literal>ucred</literal>结构体为参数调用内核路径<literal>jailed()</literal>时, - 它将返回1来说明这个用户身份是和一个<application>jail</application>相连的。 - 在<application>jail</application>中叉分出来的所有进程的的公共祖先进程就是这个执行了&man.jail.2;的进程, - 因为正是它调用了&man.jail.2;系统调用。当一个程序通过&man.execve.2;而被执行时, - 它将从其父进程的<literal>ucred</literal>结构体继承被监禁的属性, - 因而它也会拥有一个被监禁的<literal>ucred</literal>结构体。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_jail.c</filename> -int -jail(struct thread *td, struct jail_args *uap) -{ -... - struct jail_attach_args jaa; -... - error = jail_attach(td, &jaa); - if (error) - goto e_dropprref; -... -} - -int -jail_attach(struct thread *td, struct jail_attach_args *uap) -{ - struct proc *p; - struct ucred *newcred, *oldcred; - struct prison *pr; -... - p = td->td_proc; -... - pr = prison_find(uap->jid); -... - change_root(pr->pr_root, td); -... - newcred->cr_prison = pr; - p->p_ucred = newcred; -... -}</programlisting> - <para>当一个进程被从其父进程叉分来的时候, - 系统调用&man.fork.2;将用<literal>crhold()</literal>来维护其身份凭证。 - 这样,很自然的就保持了子进程的身份凭证于其父进程一致,所以子进程也是被监禁的。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_fork.c</filename>: -p2->p_ucred = crhold(td->td_ucred); -... -td2->td_ucred = crhold(p2->p_ucred);</programlisting> - - </sect3> - </sect2> - </sect1> - - <sect1 xml:id="jail-restrictions"> - <title>系统对被囚禁程序的限制</title> - - <para>在整个内核中,有一系列对被囚禁程序的约束措施。 - 通常,这些约束只对被囚禁的程序有效。如果这些程序试图突破这些约束, - 相关的函数将出错返回。例如:</para> - - <programlisting> -if (jailed(td->td_ucred)) - return EPERM;</programlisting> - - <sect2> - <title>SysV进程间通信(IPC)</title> - - <indexterm><primary>System V IPC(系统V进程间通信)</primary></indexterm> - - <para>System V 进程间通信 (IPC) 是通过消息实现的。 - 每个进程都可以向其它进程发送消息, 告诉对方该做什么。 - 处理消息的函数是: &man.msgctl.3;、&man.msgget.3;、&man.msgsnd.3; 和 - &man.msgrcv.3;。前面已经提到,一些 sysctl 开关可以影响 <application>jail</application> 的行为, - 其中有一个是 <literal>security.jail.sysvipc_allowed</literal>。 在大多数系统上, - 这个 sysctl 项会设成0。 如果将它设为1, 则会完全失去 <application>jail</application> 的意义: - 因为那样在 <application>jail</application> 中特权进程就可以影响被监禁的环境外的进程了。 - 消息与信号的区别是:消息仅由一个信号编号组成。</para> - - <para><filename>/usr/src/sys/kern/sysv_msg.c</filename>:</para> - - <itemizedlist> - <listitem><para><literal>msgget(key, msgflg)</literal>: - <literal>msgget</literal>返回(也可能创建)一个消息描述符, - 以指派一个在其它函数中使用的消息队列。</para></listitem> - - <listitem><para><literal>msgctl(msgid, cmd, buf)</literal>: 通过这个函数, - 一个进程可以查询一个消息描述符的状态。</para></listitem> - - <listitem><para><literal>msgsnd(msgid, msgp, msgsz, msgflg)</literal>: - <literal>msgsnd</literal>向一个进程发送一条消息。</para></listitem> - - <listitem><para><literal>msgrcv(msgid, msgp, msgsz, msgtyp, msgflg)</literal>: - 进程用这个函数接收消息。</para></listitem> - - </itemizedlist> - - <para>在这些函数对应的系统调用的代码中,都有这样一个条件判断:</para> - - <programlisting><filename>/usr/src/sys/kern/sysv_msg.c</filename>: -if (!jail_sysvipc_allowed && jailed(td->td_ucred)) - return (ENOSYS);</programlisting> - - <indexterm><primary>semaphores(信号量)</primary></indexterm> - <para>信号量系统调用使得进程可以通过一系列原子操作实现同步。 - 信号量为进程锁定资源提供了又一种途径。 - 然而,进程将为正在被使用的信号量进入等待状态,一直休眠到资源被释放。 - 在<application>jail</application>中如下的信号量系统调用将会失效: &man.semget.2;, &man.semctl.2; - 和&man.semop.2;。</para> - - <para><filename>/usr/src/sys/kern/sysv_sem.c</filename>:</para> - - <itemizedlist> - <listitem> - <para><literal>semctl(semid, num, cmd, ...)</literal>: - <literal>semctl</literal>对在信号量队列中用<literal>semid</literal>标识的信号量执行<literal>cmd</literal>指定的命令。</para></listitem> - - <listitem> - <para><literal>semget(key, nsems, flag)</literal>: - <literal>semget</literal>建立一个对应于<literal>key</literal>的信号量数组。</para> - - <para><literal>参数key和flag与他们在msgget()的意义相同。</literal></para></listitem> - - <listitem><para><literal>setop(semid, array, nops)</literal>: - <literal>semop</literal>对semid标识的信号量完成一组由array所指定的操作。</para></listitem> - </itemizedlist> - - <indexterm><primary>shared memory(共享内存)</primary></indexterm> - <para>System V IPC使进程间可以共享内存。进程之间可以通过它们虚拟地址空间 - 的共享部分以及相关数据读写操作直接通讯。这些系统调用在被监禁的环境中将会失效: - &man.shmdt.2;、&man.shmat.2;、&man.shmctl.2;和&man.shmget.2;</para> - - <para><filename>/usr/src/sys/kern/sysv_shm.c</filename>:</para> - - <itemizedlist> - <listitem><para><literal>shmctl(shmid, cmd, buf)</literal>: - <literal>shmctl</literal>对<literal>id</literal>标识的共享内存区域做各种各样的控制。</para></listitem> - - <listitem><para><literal>shmget(key, size, flag)</literal>: - <literal>shmget</literal>建立/打开<literal>size</literal>字节的共享内存区域。</para></listitem> - - <listitem><para><literal>shmat(shmid, addr, flag)</literal>: - <literal>shmat</literal>将<literal>shmid</literal>标识的共享内存区域指派到进程的地址空间里。</para></listitem> - - <listitem><para><literal>shmdt(addr)</literal>: - <literal>shmdt</literal>取消共享内存区域的地址指派。</para></listitem> - - </itemizedlist> - </sect2> - - <sect2> - <title>套接字</title> - - <indexterm><primary>sockets(套接字)</primary></indexterm> - <para><application>Jail</application>以一种特殊的方式处理&man.socket.2;系统调用和相关的低级套接字函数。 - 为了决定一个套接字是否允许被创建,它先检查sysctl项 - <literal>security.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(int dom, struct socket **aso, int type, int proto, - struct ucred *cred, struct thread *td) -{ - struct protosw *prp; -... - if (jailed(cred) && jail_socket_unixiproute_only && - prp->pr_domain->dom_family != PF_LOCAL && - prp->pr_domain->dom_family != PF_INET && - prp->pr_domain->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><application>Berkeley包过滤器</application>提供了一个与协议无关的,直接通向数据链路层的低级接口。 - 现在<application>BPF</application>是否可以在监禁的环境中被使用是通过&man.devfs.8;来控制的。</para> - </sect2> - - <sect2> - <title>网络协议</title> - - <indexterm><primary>protocols(协议)</primary></indexterm> - - <para>网络协议TCP, UDP, IP和ICMP很常见。IP和ICMP处于同一协议层次:第二层, - 网络层。当参数<literal>nam</literal>被设置时, - 有一些限制措施会防止被囚禁的程序绑定到一些网络接口上。 - <literal>nam</literal>是一个指向<literal>sockaddr</literal>结构体的指针, - 描述可以绑定服务的地址。一个更确切的定义:<literal>sockaddr</literal>“是一个模板,包含了地址的标识符和地址的长度”。 - 在函数<literal>in_pcbbind_setup()</literal>中<literal>sin</literal>是一个指向<literal>sockaddr_in</literal>结构体的指针, - 这个结构体包含了套接字可以绑定的端口、地址、长度、协议族。 - 这就禁止了在<application>jail</application>中的进程指定不属于这个进程所存在于的<application>jail</application>的IP地址。</para> - <programlisting><filename>/usr/src/sys/kern/netinet/in_pcb.c</filename>: -int -in_pcbbind_setup(struct inpcb *inp, struct sockaddr *nam, in_addr_t *laddrp, - u_short *lportp, struct ucred *cred) -{ - ... - struct sockaddr_in *sin; - ... - if (nam) { - sin = (struct sockaddr_in *)nam; - ... - if (sin->sin_addr.s_addr != INADDR_ANY) - if (prison_ip(cred, 0, &sin->sin_addr.s_addr)) - return(EINVAL); - ... - if (lport) { - ... - if (prison && prison_ip(cred, 0, &sin->sin_addr.s_addr)) - return (EADDRNOTAVAIL); - ... - } - } - if (lport == 0) { - ... - if (laddr.s_addr != INADDR_ANY) - if (prison_ip(cred, 0, &laddr.s_addr)) - return (EINVAL); - ... - } -... - if (prison_ip(cred, 0, &laddr.s_addr)) - return (EINVAL); -... -}</programlisting> - - <para>你也许想知道函数<literal>prison_ip()</literal>做什么。 - <literal>prison_ip()</literal>有三个参数,一个指向身份凭证的指针(用<literal>cred</literal>表示), - 一些标志和一个IP地址。当这个IP地址不属于这个<application>jail</application>时,返回1; - 否则返回0。正如你从代码中看见的,如果,那个IP地址确实不属于这个<application>jail</application>, - 就不再允许向这个网络地址绑定协议。</para> - - <programlisting><filename>/usr/src/sys/kern/kern_jail.c:</filename> -int -prison_ip(struct ucred *cred, int flag, u_int32_t *ip) -{ - u_int32_t tmp; - - if (!jailed(cred)) - return (0); - if (flag) - tmp = *ip; - else - tmp = ntohl(*ip); - if (tmp == INADDR_ANY) { - if (flag) - *ip = cred->cr_prison->pr_ip; - else - *ip = htonl(cred->cr_prison->pr_ip); - return (0); - } - if (tmp == INADDR_LOOPBACK) { - if (flag) - *ip = cred->cr_prison->pr_ip; - else - *ip = htonl(cred->cr_prison->pr_ip); - return (0); - } - if (cred->cr_prison->pr_ip != tmp) - return (1); - return (0); -}</programlisting> - </sect2> - - <sect2> - <title>文件系统</title> - - <indexterm><primary>filesystem(文件系统)</primary></indexterm> - <para>如果完全级别大于0,即便是<application>jail</application>里面的<literal>root</literal>, - 也不允许在Jail中取消或更改文件标志,如“不可修改”、“只可添加”、“不可删除”标志。</para> - <programlisting><filename>/usr/src/sys/ufs/ufs/ufs_vnops.c:</filename> -static int -ufs_setattr(ap) - ... -{ - ... - if (!priv_check_cred(cred, PRIV_VFS_SYSFLAGS, 0)) { - if (ip->i_flags - & (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) { - error = securelevel_gt(cred, 0); - if (error) - return (error); - } - ... - } -} -<filename>/usr/src/sys/kern/kern_priv.c</filename> -int -priv_check_cred(struct ucred *cred, int priv, int flags) -{ - ... - error = prison_priv_check(cred, priv); - if (error) - return (error); - ... -} -<filename>/usr/src/sys/kern/kern_jail.c</filename> -int -prison_priv_check(struct ucred *cred, int priv) -{ - ... - switch (priv) { - ... - case PRIV_VFS_SYSFLAGS: - if (jail_chflags_allowed) - return (0); - else - return (EPERM); - ... - } - ... -}</programlisting> - - </sect2> - - </sect1> - -</chapter> diff --git a/zh_CN.GB2312/books/arch-handbook/kobj/chapter.xml b/zh_CN.GB2312/books/arch-handbook/kobj/chapter.xml deleted file mode 100644 index b5799936a0..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/kobj/chapter.xml +++ /dev/null @@ -1,284 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.7 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="kernel-objects"> - <info><title>内核对象</title> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <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 xml: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>某一种操作—函数</para> - </listitem> - </varlistentry> - <varlistentry> - <term>类</term> - <listitem> - <para>一种或多种方法</para> - </listitem> - </varlistentry> - <varlistentry> - <term>接口</term> - <listitem> - <para>一种或多种方法的一个标准集合</para> - </listitem> - </varlistentry> - </variablelist> - </sect1> - - <sect1 xml:id="kernel-objects-operation"> - <title>Kobj的工作流程</title> - - <tip><title>译者注</title><para>这一小节两段落中原作者的用词有些含混, - 请参考我在括号中的注释阅读。</para></tip> - - <para>Kobj工作时,产生方法的描述。每个描述有一个唯一的标识和一个缺省函数。 - 某个描述的地址被用来在一个类的方法表里唯一的标识方法。</para> - - <para>构建一个类,就是要建立一张方法表,并将这张表关联到一个或多个函数(方法); - 这些函数(方法)都带有方法描述。使用前,类要被编译。编译时要为这个类分配一些缓存。 - 在方法表中的每个方法描述都会被指派一个唯一的标识, - 除非已经被其它引用它的类在编译时指派了标识。对于每个将要被使用的方法, - 都会由脚本生成一个函数(方法查找函数),以解析外来参数, - 并在被查询时给出方法描述的地址。被生成的函数(方法查找函数) - 凭着那个方法描述的唯一标识按Hash的方法查找对象的类的缓存。 - 如果这个方法不在缓存中,函数会查找使用类的方法表。如果这个方法被找到了, - 类里的相关函数(也就是某个方法的实现代码)就会被使用。 - 否则,这个方法描述的缺省函数将被使用。</para> - - <para>这些过程可被表示如下:</para> - - <programlisting>对象->缓存<->类</programlisting> - - </sect1> - - <sect1 xml: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><sys/param.h> -<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 <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>)。这些操作可通过声明一个结构体 - <varname remap="structname">struct kobj_ops</varname>并使用 - <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> diff --git a/zh_CN.GB2312/books/arch-handbook/newbus/chapter.xml b/zh_CN.GB2312/books/arch-handbook/newbus/chapter.xml deleted file mode 100644 index d91a4d26a7..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/newbus/chapter.xml +++ /dev/null @@ -1,328 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.10 - $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 xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="newbus"> - <info><title>Newbus</title> - <authorgroup> - <author><personname><firstname>Jeroen</firstname><surname>Ruigrok van der Werven (asmodai)</surname></personname><affiliation><address><email>asmodai@FreeBSD.org</email></address> - </affiliation><contrib>&cnproj.written.by;</contrib></author> - <author><personname><firstname>Hiten</firstname><surname>Pandya</surname></personname><affiliation><address><email>hiten@uk.FreeBSD.org</email></address> - </affiliation></author> - </authorgroup> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <para><emphasis>特别感谢Matthew N. Dodd, Warner Losh, Bill Paul, - Doug Rabson, Mike Smith, Peter Wemm and Scott Long</emphasis>.</para> - - <para>本章详细解释了Newbus设备框架。</para> - <sect1 xml: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 xml: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>设备,对于Alpha,Alpha的各种 - 不同型号有不同的顶层设备,对应不同的硬件芯片组,包括 - <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><filename>pcibN</filename></emphasis>,而用它的孩子 - <emphasis><filename>pciN</filename></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>这种支持被合并到了资源分配机制中。分配资源时,驱动程序可以从资源 - 中检取关联的<varname remap="structfield">bus_space_tag_t</varname>和 - <varname remap="structfield">bus_space_handle_t</varname>。</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 xml: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> diff --git a/zh_CN.GB2312/books/arch-handbook/pccard/chapter.xml b/zh_CN.GB2312/books/arch-handbook/pccard/chapter.xml deleted file mode 100644 index d7bfcaaa78..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/pccard/chapter.xml +++ /dev/null @@ -1,302 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.12 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="pccard"> - <info><title>PC Card</title> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - <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 xml:id="pccard-adddev"> - <title>添加设备</title> - - <para>向所支持的pccard设备列表中添加新设备的步骤已经与系统在FreeBSD 4 - 中使用的方法不同了。在以前的版本中,需要编辑 - <filename>/etc</filename>中的一个文件来列出设备。从FreeBSD 5.0开始, - 设备驱动程序知道它们支持什么设备。现在内核中有一个受支持设备的表, - 驱动程序用它来连接设备。</para> - - <sect2 xml: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 - 时。Linksys,D-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 xml: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&spWireless&spPCMCIA", "Version&sp01.02", NULL } -product ALLIEDTELESIS WR211PCM { "Allied&spTelesis&spK.K.", "WR211PCM", NULL, NULL } Allied Telesis WR211PCM</programlisting> - - <para>我们已经熟悉了产品关键字,后跟供货商名字,然后再跟卡的名字, - 就象在文件第二节中那样。然而,这之后就与那格式不同了。有一个 - {}分组,后跟几个字符串。这些字符串对应CIS_INFO三元组中定义的 - 供货商,产品和额外信息。这些字符串被产生 - <filename>pccarddevs.h</filename>的程序过滤,将 &sp替换为 - 实际的空格。空条目意味着条目的这部分应当被忽略。在我选择的例子中 - 有一个错误的条目。除非对卡的操作来说至关重要,否则不应当在其中 - 包含版本号。有时供货商在这个字段中会有卡的很多不同版本,这些版本 - 都能工作,这种情况下那些信息只会让那些拥有相似卡的人在FreeBSD中 - 更难以使用。有时当供货商出于市场考虑(可用性,价格等等),希望出售 - 同一品牌下的很多不同部分时,这也是有必要的。如果这样,则在那些 - 供货商仍然保持相同的制造商/产品对的少见情况下,能否区分开卡至关 - 重要. 此时不能使用正则表达式匹配。</para> - - </sect2> - - <sect2 xml: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->pp_name != NULL) - device_set_desc(dev, pp->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>观察一下表<varname remap="structname">wi_pccard_products</varname>就会发现, - 所有条目都是 - <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 xml: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> - 并将它作为另一次提交来提交(这是为了确保正确的 - $FreeBSD$标签会留在后面的文件中)。最后,你需要把 - 其它东西提交到驱动程序。</para> - - </sect2> - - <sect2 xml:id="pccard-pr"> - <title>提交新设备</title> - - <para>很多人直接把新设备的条目发送给作者。请不要那样做。请将它们作为 - PR来提交,并将PR号码发送给作者用于记录。这样确保条目不会丢失。提交 - PR时,补丁中没有必要包含<filename>pccardevs.h</filename>的diff, - 因为那些东西可以重新产生。包含设备的描述和客户驱动程序的补丁是必要 - 的。如果你不知道名字,使用OEM99作为名字,作者将会调查后相应地调整 - OEM99。提交者不应当提交OEM99,而应该找到最高的OEM条目并提交高于那个 - 的一个。</para> - - </sect2> - - </sect1> - -</chapter> diff --git a/zh_CN.GB2312/books/arch-handbook/scsi/chapter.xml b/zh_CN.GB2312/books/arch-handbook/scsi/chapter.xml deleted file mode 100644 index 5fa24cc16e..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/scsi/chapter.xml +++ /dev/null @@ -1,1815 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD Simplified Chinese Project - - Original Revision: 1.18 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="scsi"> - <info><title>通用访问方法SCSI控制器</title> - <authorgroup> - <author><personname><firstname>Sergey</firstname><surname>Babkin</surname></personname><contrib>&cnproj.written.by;</contrib></author> - </authorgroup> - <authorgroup> - <author><personname><firstname>Murray</firstname><surname>Stokely</surname></personname><contrib>&cnproj.modified.for.handbook.by;</contrib></author> - </authorgroup> - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - </info> - - - - <sect1 xml:id="scsi-synopsis"> - <title>提纲</title> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary></indexterm> - <para>本文档假定读者对FreeBSD的设备驱动程序和SCSI协议有大致了解, - 本文档中很多信息是从以下驱动程序中:</para> - - <itemizedlist> - - <listitem><para>ncr (<filename>/sys/pci/ncr.c</filename>) - 由Wolfgang Stanglmeier and Stefan Esser编写</para></listitem> - - <listitem><para>sym (<filename>/sys/dev/sym/sym_hipd.c</filename>) - 由Gerard Roudier编写</para></listitem> - - <listitem><para>aic7xxx - (<filename>/sys/dev/aic7xxx/aic7xxx.c</filename>) - 由Justin T. Gibbs编写</para></listitem> - - </itemizedlist> - - <para>和从CAM的代码本身(作者 Justin T. Gibbs, - 见<filename>/sys/cam/*</filename>)中摘录。当一些解决方法看起来 - 极具逻辑性,并且基本上是从 Justin T. Gibbs 的代码中一字不差地摘录时, - 我将其标记为<quote>recommended</quote>。</para> - - <para>本文档以伪代码例子进行说明。尽管有时例子中包含很多细节,并且 - 看起来很像真正代码,但它仍然只是伪代码。这样写是为了以一种可理解 - 的方式来展示概念。对于真正的驱动程序,其它方法可能更模块化,并且 - 更加高效。文档也对硬件细节进行抽象,对于那些会模糊我们所要展示的 - 概念的问题,或被认为在开发者手册的其他章节中已有描述的问题也做同样 - 处理。这些细节通常以调用具有描述性名字的函数、注释或伪语句的形式展现。 - 幸运的是,具有实际价值的完整例子,包括所有细节,可以在真正的驱动 - 程序中找到。</para> - - </sect1> - - <sect1 xml:id="scsi-general"> - <title>通用基础结构</title> - - <indexterm><primary>Common Access Method (CAM, 通用访问方法)</primary></indexterm> - - <para>CAM代表通用访问方法(Common Access Method)。它以类SCSI方式寻址 - I/O总线。这就允许将通用设备驱动程序和控制I/O总线的驱动程序分离开来: - 例如磁盘驱动程序能同时控制SCSI、IDE、且/或任何其他总线上的磁盘, - 这样磁盘驱动程序部分不必为每种新的I/O总线而重写(或拷贝修改)。 - 这样,两个最重要的活动实体是:</para> - - <indexterm><primary>CD-ROM(只读光盘驱动器)</primary></indexterm> - <indexterm><primary>tape(磁带)</primary></indexterm> - <indexterm><primary>IDE(Intelligent Drive Electronics,智能驱动器电路;或Integrated Drive Electronics,集成驱动器电路)</primary></indexterm> - <itemizedlist> - <listitem><para><emphasis>外围设备模块</emphasis> - 外围设备(磁盘, - 磁带, CD-ROM等)的驱动程序</para></listitem> - <listitem><para><emphasis>SCSI接口模块</emphasis>(SIM) - - 连接到I/O总线,如SCSI或IDE,的主机总线适配器驱动程序。 - </para></listitem> - </itemizedlist> - - <para>外围设备驱动程序从OS接收请求,将它们转换为SCSI命令序列并将 - 这些SCSI命令传递到SCSI接口模块。SCSI接口模块负责将这些命令传递给 - 实际硬件(或者如果实际硬件不是SCSI,而是例如IDE,则也要将这些SCSI - 命令转换为硬件的native命令)。</para> - - <para>由于这儿我们感兴趣的是编写SCSI适配器驱动程序,从此处开始我们 - 将从SIM的角度考虑所有的事情。</para> - - <para>典型的SIM驱动程序需要包括如下的CAM相关的头文件:</para> - -<programlisting>#include <cam/cam.h> -#include <cam/cam_ccb.h> -#include <cam/cam_sim.h> -#include <cam/cam_xpt_sim.h> -#include <cam/cam_debug.h> -#include <cam/scsi/scsi_all.h></programlisting> - - <para>每个SIM驱动程序必须做的第一件事情是向CAM子系统注册它自己。 - 这在驱动程序的<function>xxx_attach()</function>函数(此处和以后的 - xxx_用于指带唯一的驱动程序名字前缀)期间完成。 - <function>xxx_attach()</function>函数自身由系统总线自动配置代码 - 调用,我们在此不描述这部分代码。</para> - - <para>这需要好几步来完成:首先需要分配与SIM关联的请求队列:</para> - -<programlisting> struct cam_devq *devq; - - if(( devq = cam_simq_alloc(SIZE) )==NULL) { - error; /* 一些处理错误的代码 */ - }</programlisting> - - <para>此处 <literal>SIZE</literal> 为要分配的队列的大小, - 它能包含的最大请求数目。 它是 SIM - 驱动程序在 SCSI 卡上能够并行处理的请求的数目。一般可以如下估算:</para> - -<programlisting>SIZE = NUMBER_OF_SUPPORTED_TARGETS * MAX_SIMULTANEOUS_COMMANDS_PER_TARGET</programlisting> - - <para>下一步为我们的SIM创建描述符:</para> - -<programlisting> struct cam_sim *sim; - - if(( sim = cam_sim_alloc(action_func, poll_func, driver_name, - softc, unit, max_dev_transactions, - max_tagged_dev_transactions, devq) )==NULL) { - cam_simq_free(devq); - error; /* 一些错误处理代码 */ - }</programlisting> - - <para>注意如果我们不能创建SIM描述符,我们也释放 - <varname remap="structname">devq</varname>,因为我们对其无法做任何其他事情, - 而且我们想节约内存。</para> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary><secondary>bus(总线)</secondary></indexterm> - <para>如果SCSI卡上有多条SCSI总线,则每条总线需要它自己的 - <varname remap="structname">cam_sim</varname> 结构。</para> - - <para>一个有趣的问题是,如果SCSI卡有不只一条SCSI总线我们该怎么做, - 每个卡需要一个<varname remap="structname">devq</varname>结构还是每条SCSI总线? - 在CAM代码的注释中给出的答案是:任一方式均可,由驱动程序的作者 - 选择。</para> - - <para>参量为: - <itemizedlist> - - <listitem><para><function>action_func</function> - 指向驱动程序 - <function>xxx_action</function> 函数的指针。 - <funcsynopsis><funcprototype> - <funcdef>static void - <function>xxx_action</function> - </funcdef> - <paramdef> - <parameter>struct cam_sim *sim</parameter>, - <parameter>union ccb *ccb</parameter> - </paramdef> - </funcprototype></funcsynopsis> - </para></listitem> - - <listitem><para><function>poll_func</function> - 指向驱动程序 - <function>xxx_poll()</function>函数的指针。 - <funcsynopsis><funcprototype> - <funcdef>static void - <function>xxx_poll</function> - </funcdef> - <paramdef> - <parameter>struct cam_sim *sim</parameter> - </paramdef> - </funcprototype></funcsynopsis> - </para></listitem> - - <listitem><para>driver_name - 实际驱动程序的名字,例如 - <quote>ncr</quote>或<quote>wds</quote>。</para></listitem> - - <listitem><para><varname remap="structname">softc</varname> - 指向这个SCSI卡 - 驱动程序的内部描述符的指针。这个指针以后被驱动程序用来获取 - 私有数据。</para></listitem> - - <listitem><para>unit - 控制器单元号,例如对于控制器 - <quote>wds0</quote>的此数字将为0。</para></listitem> - - <listitem><para>max_dev_transactions - 无标签模式下每个SCSI目标的 - 最大并发(simultaneous)事务数。这个值一般几乎总是等于1,只有非 - SCSI卡才可能例外。此外,如果驱动程序希望执行一个事务的同时准备另 - 一个事务,可以将其设置为2,但似乎不值得增加这种复杂性。 - </para></listitem> - - <listitem><para>max_tagged_dev_transactions - 同样的东西,但是 - 在标签模式下。标签是SCSI在设备上发起多个事务的方式:每个事务 - 被赋予一个唯一的标签,并被发送到设备。当设备完成某些事务,它 - 将结果连同标签一起发送回来,这样SCSI适配器(和驱动程序)就能知道 - 哪个事务完成了。此参量也被认为是最大标签深度。它取决于SCSI - 适配器的能力。</para></listitem> - </itemizedlist> - </para> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary><secondary>adapter(适配器)</secondary></indexterm> - <para>最后我们注册与我们的SCSI适配器关联的SCSI总线。</para> - -<programlisting> if(xpt_bus_register(sim, bus_number) != CAM_SUCCESS) { - cam_sim_free(sim, /*free_devq*/ TRUE); - error; /* 一些错误处理代码 */ - }</programlisting> - - <para>如果每条SCSI总线有一个<varname remap="structname">devq</varname>结构(即, - 我们将带有多条总线的卡看作多个卡,每个卡带有一条总线),则总线号 - 总是为0,否则SCSI卡上的每条总线应当有不同的号。每条总线需要 - 它自己单独的cam_sim结构。</para> - - <para>这之后我们的控制器完全挂接到CAM系统。现在 - <varname remap="structname">devq</varname>的值可以被丢弃:在所有以后从CAM发出的 - 调用中将以sim为参量,devq可以由它导出。</para> - - <para>CAM为这些异步事件提供了框架。有些事件来自底层(SIM驱动程序), - 有些来自外围设备驱动程序,还有一些来自CAM子系统本身。任何驱动 - 程序都可以为某些类型的异步事件注册回调,这样那些事件发生时它就 - 会被通知。</para> - - <para>这种事件的一个典型例子就是设备复位。每个事务和事件以 - <quote>path</quote>的方式区分它们所作用的设备。目标特定的事件 - 通常在与设备进行事务处理期间发生。因此那个事务的路径可以被重用 - 来报告此事件(这是安全的,因为事件路径的拷贝是在事件报告例程中进行的, - 而且既不会被deallocate也不作进一步传递)。在任何时刻,包括中断例程中, - 动态分配路径也是安全的,尽管那样会导致某些额外开销,并且这种方法 - 可能存在的一个问题是碰巧那时可能没有空闲内存。对于总线复位事件, - 我们需要定义包括总线上所有设备在内的通配符路径。这样我们就能提前为 - 以后的总线复位事件创建路径,避免以后内存不足的问题:</para> - -<programlisting> struct cam_path *path; - - if(xpt_create_path(&path, /*periph*/NULL, - cam_sim_path(sim), CAM_TARGET_WILDCARD, - CAM_LUN_WILDCARD) != CAM_REQ_CMP) { - xpt_bus_deregister(cam_sim_path(sim)); - cam_sim_free(sim, /*free_devq*/TRUE); - error; /* 一些错误处理代码 */ - } - - softc->wpath = path; - softc->sim = sim;</programlisting> - - <para>正如你所看到的,路径包括:</para> - - <itemizedlist> - <listitem><para>外围设备驱动程序的ID(由于我们一个也没有,故此处为空) - </para></listitem> - - <listitem><para>SIM驱动程序的ID - (<function>cam_sim_path(sim)</function>)</para></listitem> - - <listitem><para>设备的SCSI目标号(CAM_TARGET_WILDCARD的意思指 - <quote>所有devices</quote>)</para></listitem> - - <listitem><para>子设备的SCSI LUN号(CAM_LUN_WILDCARD的意思指 - <quote>所有LUNs</quote>)</para></listitem> - </itemizedlist> - - <para>如果驱动程序不能分配这个路径,它将不能正常工作,因此那样情况下 - 我们卸除(dismantle)那个SCSI总线。</para> - - <para>我们在<varname remap="structname">softc</varname>结构中保存路径指针以便以后 - 使用。这之后我们保存sim的值(或者如果我们愿意,也可以在从 - <function>xxx_probe()</function>退出时丢弃它)。</para> - - <para>这就是最低要求的初始化所需要做的一切。为了把事情做正确无误, - 还剩下一个问题。</para> - - <para>对于SIM驱动程序,有一个特殊感兴趣的事件:何时目标设备被认为 - 找不到了。这种情况下复位与这个设备的SCSI协商可能是个好主意。因此我们 - 为这个事件向CAM注册一个回调。通过为这种类型的请求来请求CAM控制块上 - 的CAM动作,请求就被传递到CAM:(译注:参看下面示例代码和原文)</para> - -<programlisting> struct ccb_setasync csa; - - xpt_setup_ccb(&csa.ccb_h, path, /*优先级*/5); - csa.ccb_h.func_code = XPT_SASYNC_CB; - csa.event_enable = AC_LOST_DEVICE; - csa.callback = xxx_async; - csa.callback_arg = sim; - xpt_action((union ccb *)&csa);</programlisting> - - <para>现在我们看一下<function>xxx_action()</function> - 和<function>xxx_poll()</function>的驱动程序入口点。</para> - - <para> - <funcsynopsis><funcprototype> - <funcdef>static void - <function>xxx_action</function> - </funcdef> - <paramdef> - <parameter>struct cam_sim *sim</parameter>, - <parameter>union ccb *ccb</parameter> - </paramdef> - </funcprototype></funcsynopsis> - </para> - - <para>响应CAM子系统的请求采取某些动作。Sim描述了请求的SIM,CCB为 - 请求本身。CCB代表<quote>CAM Control Block</quote>。它是很多特定 - 实例的联合,每个实例为某些类型的事务描述参量。所有这些实例共享 - 存储着参量公共部分的CCB头部。(译注:这一段不很准确,请自行参考原文) - </para> - - <para>CAM既支持SCSI控制器工作于发起者(initiator)(<quote>normal</quote>) - 模式,也支持SCSI控制器工作于目标(target)(模拟SCSI设备)模式。这儿 - 我们只考虑与发起者模式有关的部分。</para> - - <para>定义了几个函数和宏(换句话说,方法)来访问结构sim中公共数据: - </para> - - <itemizedlist> - <listitem><para><function>cam_sim_path(sim)</function> - 路径ID - (参见上面)</para></listitem> - - <listitem><para><function>cam_sim_name(sim)</function> - - sim的名字</para></listitem> - - <listitem><para><function>cam_sim_softc(sim)</function> - - 指向softc(驱动程序私有数据)结构的指针</para></listitem> - - <listitem><para><function> cam_sim_unit(sim)</function> - - 单元号</para></listitem> - - <listitem><para><function> cam_sim_bus(sim)</function> - - 总线ID</para></listitem> - </itemizedlist> - - <para>为了识别设备,<function>xxx_action()</function>可以使用这些 - 函数得到单元号和指向它的softc结构的指针。</para> - - <para>请求的类型被存储在 - <varname remap="structfield">ccb->ccb_h.func_code</varname>。因此,通常 - <function>xxx_action()</function>由一个大的switch组成:</para> - -<programlisting> struct xxx_softc *softc = (struct xxx_softc *) cam_sim_softc(sim); - struct ccb_hdr *ccb_h = &ccb->ccb_h; - int unit = cam_sim_unit(sim); - int bus = cam_sim_bus(sim); - - switch(ccb_h->func_code) { - case ...: - ... - default: - ccb_h->status = CAM_REQ_INVALID; - xpt_done(ccb); - break; - }</programlisting> - - <para>从default case语句部分可以看出(如果收到未知命令),命令的返回码 - 被设置到 <varname remap="structfield">ccb->ccb_h.status</varname> 中,并且通过 - 调用<function>xpt_done(ccb)</function>将整个CCB返回到CAM中。</para> - - <para><function>xpt_done()</function>不必从 - <function>xxx_action()</function>中调用:例如I/O请求可以在SIM驱动程序 - 和/或它的SCSI控制器中排队。(译注:它指I/O请求?) - 然后,当设备传递(post)一个中断信号,指示对此请求的处理已结束时, - <function>xpt_done()</function>可以从中断处理例程中被调用。</para> - - <para>实际上,CCB状态不是仅仅被赋值为一个返回码,而是始终有某种状态。 - CCB被传递给<function>xxx_action()</function>例程前,其取得状态 - CCB_REQ_INPROG,表示其正在进行中。<filename>/sys/cam/cam.h</filename> - 中定义了数量惊人的状态值,它们应该能非常详尽地表示请求的状态。 - 更有趣的是,状态实际上是一个枚举状态值(低6位)和一些可能出现的附加 - 类(似)旗标位(高位)的<quote>位或(bitwise or)</quote>。枚举值会在以后 - 更详细地讨论。对它们的汇总可以在错误概览节(Errors Summary section) - 找到。可能的状态旗标为:</para> - - <itemizedlist> - - <listitem><para><emphasis>CAM_DEV_QFRZN</emphasis> - 当处理CCB时, - 如果SIM驱动程序得到一个严重错误(例如,驱动程序不能响应选择或违反 - 了SCSI协议),它应当调用<function>xpt_freeze_simq()</function>冻结 - 请求队列,把此设备的其他已入队但尚未被处理的CCB返回到CAM队列, - 然后为有问题的CCB设置这个旗标并调用 - <function>xpt_done()</function>。这个旗标会使得CAM子系统处理错误后 - 解冻队列。</para></listitem> - - <listitem><para><emphasis>CAM_AUTOSNS_VALID</emphasis> - 如果设备 - 返回错误条件,且CCB中未设置旗标CAM_DIS_AUTOSENSE,SIM驱动程序 - 必须自动执行REQUEST SENSE命令来从设备抽取sense(扩展错误信息) - 数据。如果这个尝试成功,sense数据应当被保存在CCB中且设置此旗标。 - </para></listitem> - - <listitem><para><emphasis>CAM_RELEASE_SIMQ</emphasis> - 类似于 - CAM_DEV_QFRZN,但用于SCSI控制器自身出问题(或资源短缺)的情况。 - 此后对控制器的所有请求会被<function>xpt_freeze_simq()</function> - 停止。SIM驱动程序克服这种短缺情况,并通过返回设置了此旗标的CCB - 通知CAM后,控制器队列将会被重新启动。</para></listitem> - - <listitem><para><emphasis>CAM_SIM_QUEUED</emphasis> - 当SIM将一个 - CCB放入其请求队列时应当设置此旗标(或当CCB出队但尚未返回给CAM时 - 去掉)。现在此旗标还没有在CAM代码的任何地方使用过,因此其目的 - 纯粹用于诊断)。</para></listitem> - - </itemizedlist> - - <para>函数<function>xxx_action()</function>不允许睡眠,因此对资源 - 访问的所有同步必须通过冻结SIM或设备队列来完成。除了前述的旗标外, - CAM子系统提供了函数<function>xpt_release_simq()</function>和 - <function>xpt_release_devq()</function>来直接解冻队列,而不必将 - CCB传递到CAM。</para> - - <para>CCB头部包含如下字段:</para> - - <itemizedlist> - - <listitem><para><emphasis>path</emphasis> - 请求的路径ID - </para></listitem> - - <listitem><para><emphasis>target_id</emphasis> - 请求的目标设备ID - </para></listitem> - - <listitem><para><emphasis>target_lun</emphasis> - 目标设备的LUN ID - </para></listitem> - - <listitem><para><emphasis>timeout</emphasis> - - 这个命令的超时间隔,以毫秒计</para></listitem> - - <listitem><para><emphasis>timeout_ch</emphasis> - 一个为SIM驱动 - 程序存储超时处理函数的方便之所(CAM子系统自身并不对此作任何假设) - </para></listitem> - - <listitem><para><emphasis>flags</emphasis> - 有关请求的各个 - 信息位</para></listitem> - - <listitem><para><emphasis>spriv_ptr0,spriv_ptr1</emphasis> - - SIM驱动程序保留私用的字段 - (例如链接到SIM队列或SIM私有控制块);实际上,它们作为联合存在: - spriv_ptr0和spriv_ptr1具有类型(void *),spriv_field0和 - spriv_field1具有类型unsigned long,sim_priv.entries[0].bytes和 - sim_priv.entries[1].bytes为与联合的其他形式大小一致的字节数组, - sim_priv.bytes为一个两倍大小的数组</para></listitem> - - </itemizedlist> - - <para>使用CCB的SIM私有字段的建议方法是为它们定义一些有意义的名字, - 并且在驱动程序中使用这些有意义的名字,就像下面这样:</para> - -<programlisting>#define ccb_some_meaningful_name sim_priv.entries[0].bytes -#define ccb_hcb spriv_ptr1 /* 用于硬件控制块 */</programlisting> - - <para>最常见的发起者模式的请求是:</para> - <itemizedlist> - <listitem><para><emphasis>XPT_SCSI_IO</emphasis> - 执行I/O事务 - </para> - - <para>联合ccb的<quote>struct ccb_scsiio csio</quote>实例用于传递参量。 - 它们是:</para> - - <itemizedlist> - <listitem><para><emphasis>cdb_io</emphasis> - - 指向SCSI命令缓冲区的指针或缓冲区本身</para></listitem> - - <listitem><para><emphasis>cdb_len</emphasis> - - SCSI命令长度</para></listitem> - - <listitem><para><emphasis>data_ptr</emphasis> - - 指向数据缓冲区的指针(如果使用分散/集中会复杂一点) - </para></listitem> - - <listitem><para><emphasis>dxfer_len</emphasis> - - 待传输数据的长度</para></listitem> - - <listitem><para><emphasis>sglist_cnt</emphasis> - - 分散/集中段的计数</para></listitem> - - <listitem><para><emphasis>scsi_status</emphasis> - - 返回SCSI状态的地方</para></listitem> - - <listitem><para><emphasis>sense_data</emphasis> - - 命令返回错误时保存SCSI sense信息的缓冲区(这种情况下,如果没有 - 设置CCB的旗标CAM_DIS_AUTOSENSE,则假定SIM驱动程序会自动运行 - REQUEST SENSE命令) - </para></listitem> - - <listitem><para><emphasis>sense_len</emphasis> - - 缓冲区的长度(如果碰巧大于sense_data的大小,SIM驱动程序必须 - 悄悄地采用较小值)(译注:一点改动,参考原文及代码) - </para></listitem> - <listitem><para><emphasis>resid, sense_resid</emphasis> - - 如果数据传输或SCSI sense返回错误,则它们 - 就是返回的剩余(未传输)数据的计数。它们看起来并不是特别有意义, - 因此当很难计算的情况下(例如,计数SCSI控制器FIFO缓冲区中的字节 - 数),使用近似值也同样可以。对于成功完成的传输,它们必须被设置 - 为0。</para></listitem> - - <listitem><para><emphasis>tag_action</emphasis> - - 使用的标签的种类有: - - <itemizedlist> - <listitem><para>CAM_TAG_ACTION_NONE - 事务不使用标签 - </para></listitem> - <listitem><para>MSG_SIMPLE_Q_TAG, MSG_HEAD_OF_Q_TAG, - MSG_ORDERED_Q_TAG - 值等于适当的标签信息 - (见/sys/cam/scsi/scsi_message.h);仅给出标签类型,SIM驱动程序 - 必须自己赋标签值</para></listitem> - </itemizedlist> - - </para></listitem> - - </itemizedlist> - - <para>处理请求的通常逻辑如下:</para> - - <para>要做的第一件事情是检查可能的竞争条件,确保命令位于队列中时 - 不会被中止:</para> - -<programlisting> struct ccb_scsiio *csio = &ccb->csio; - - if ((ccb_h->status & CAM_STATUS_MASK) != CAM_REQ_INPROG) { - xpt_done(ccb); - return; - }</programlisting> - - <para>我们也检查我们的控制器完全支持设备:</para> - -<programlisting> if(ccb_h->target_id > OUR_MAX_SUPPORTED_TARGET_ID - || cch_h->target_id == OUR_SCSI_CONTROLLERS_OWN_ID) { - ccb_h->status = CAM_TID_INVALID; - xpt_done(ccb); - return; - } - if(ccb_h->target_lun > OUR_MAX_SUPPORTED_LUN) { - ccb_h->status = CAM_LUN_INVALID; - xpt_done(ccb); - return; - }</programlisting> - - <indexterm><primary>hardware control block(硬件控制块)</primary></indexterm> - - <para>然后分配我们处理请求所需的数据结构(如卡相关的硬件控制块等)。 - 如果我们不能分配则冻结SIM队列,记录下我们有一个挂起的操作,返回 - CCB,请求CAM将CCB重新入队。以后当资源可用时,必须通过返回其 - 状态中设置 <literal>CAM_SIMQ_RELEASE</literal> 位的ccb来解冻SIM队列。否则,如果所有 - 正常,则将CCB与硬件控制块(HCB)链接,并将其标志为已入队。</para> - -<programlisting> struct xxx_hcb *hcb = allocate_hcb(softc, unit, bus); - - if(hcb == NULL) { - softc->flags |= RESOURCE_SHORTAGE; - xpt_freeze_simq(sim, /*count*/1); - ccb_h->status = CAM_REQUEUE_REQ; - xpt_done(ccb); - return; - } - - hcb->ccb = ccb; ccb_h->ccb_hcb = (void *)hcb; - ccb_h->status |= CAM_SIM_QUEUED;</programlisting> - - <para>从CCB中提取目标数据到硬件控制块。检查是否要求我们分配一个 - 标签,如果是则产生一个唯一的标签并构造SCSI标签信息。SIM驱动程序 - 也负责与设备协商设定彼此支持的最大总线宽度、同步速率和偏移。 - </para> - -<programlisting> hcb->target = ccb_h->target_id; hcb->lun = ccb_h->target_lun; - generate_identify_message(hcb); - if( ccb_h->tag_action != CAM_TAG_ACTION_NONE ) - generate_unique_tag_message(hcb, ccb_h->tag_action); - if( !target_negotiated(hcb) ) - generate_negotiation_messages(hcb);</programlisting> - - <para>然后设置SCSI命令。可以在CCB中以多种有趣的方式指定命令的存储, - 这些方式由CCB中的旗标指定。命令缓冲区可以包含在CCB中或者用指针 - 指向,后者情况下指针可以指向物理地址或虚地址。由于硬件通常需要 - 物理地址,因此我们总是将地址转换为物理地址。</para> - - <para>不太相关的提示:通常这是通过调用<function>vtophys()</function>来完成的,但由于 - 特殊的Alpha怪异之处,为了PCI设备(它们现在占SCSI控制器的大多数) - 驱动程序向Alpha架构的可移植性,转换必须替代以 <function>vtobus()</function> 来完成。 - [IMHO 提供两个单独的函数 <function>vtop()</function> 和 <function>ptobus()</function>,而 <function>vtobus()</function> 只是它们的 - 简单叠代,这样做要好得多。] 在请求物理地址的情况下,返回带有状态 - <errorname>CAM_REQ_INVALID</errorname> 的CCB是可以的,当前的驱动程序就是那样做的。但也 - 可能像这个例子(驱动程序中应当有不带条件编译的更直接做法)中那样 - 编译Alpha特定的代码片断。如果需要物理地址也能转换或映射回虚地址, - 但那样代价很大,因此我们不那样做。</para> - -<programlisting> if(ccb_h->flags & CAM_CDB_POINTER) { - /* CDB is a pointer */ - if(!(ccb_h->flags & CAM_CDB_PHYS)) { - /* CDB指针是虚拟的 */ - hcb->cmd = vtobus(csio->cdb_io.cdb_ptr); - } else { - /* CDB指针是物理的 */ -#if defined(__alpha__) - hcb->cmd = csio->cdb_io.cdb_ptr | alpha_XXX_dmamap_or ; -#else - hcb->cmd = csio->cdb_io.cdb_ptr ; -#endif - } - } else { - /* CDB在ccb(缓冲区)中 */ - hcb->cmd = vtobus(csio->cdb_io.cdb_bytes); - } - hcb->cmdlen = csio->cdb_len;</programlisting> - - <para>现在是设置数据的时候了,又一次,可以在CCB中以多种有趣的方式 - 指定数据存储,这些方式由CCB中的旗标指定。首先我们得到数据传输的 - 方向。最简单的情况是没有数据需要传输的情况:</para> - -<programlisting> int dir = (ccb_h->flags & CAM_DIR_MASK); - - if (dir == CAM_DIR_NONE) - goto end_data;</programlisting> - - <para>然后我们检查数据在一个chunk中还是在分散/集中列表中,并且是 - 物理地址还是虚地址。SCSI控制器可能只能处理有限数目有限长度的 - 大块。如果请求到达到这个限制我们就返回错误。我们使用一个特殊 - 函数返回CCB,并在一个地方处理HCB资源短缺。增加chunk的函数是 - 驱动程序相关的,此处我们不进入它们的详细实现。对于地址翻译问题 - 的细节可以参看SCSI命令(CDB)处理的描述。如果某些变体对于特定的卡 - 太困难或不可能实现,返回状态 <errorname>CAM_REQ_INVALID</errorname> - 是可以的。实际上, - 现在的CAM代码中似乎哪儿也没有使用分散/集中能力。但至少必须实现 - 单个非分散虚拟缓冲区的情况,CAM中这种情况用得很多。</para> - -<programlisting> int rv; - - initialize_hcb_for_data(hcb); - - if((!(ccb_h->flags & CAM_SCATTER_VALID)) { - /* 单个缓冲区 */ - if(!(ccb_h->flags & CAM_DATA_PHYS)) { - rv = add_virtual_chunk(hcb, csio->data_ptr, csio->dxfer_len, dir); - } - } else { - rv = add_physical_chunk(hcb, csio->data_ptr, csio->dxfer_len, dir); - } - } else { - int i; - struct bus_dma_segment *segs; - segs = (struct bus_dma_segment *)csio->data_ptr; - - if ((ccb_h->flags & CAM_SG_LIST_PHYS) != 0) { - /* SG列表指针是物理的 */ - rv = setup_hcb_for_physical_sg_list(hcb, segs, csio->sglist_cnt); - } else if (!(ccb_h->flags & CAM_DATA_PHYS)) { - /* SG缓冲区指针是虚拟的 */ - for (i = 0; i < csio->sglist_cnt; i++) { - rv = add_virtual_chunk(hcb, segs[i].ds_addr, - segs[i].ds_len, dir); - if (rv != CAM_REQ_CMP) - break; - } - } else { - /* SG缓冲区指针是物理的 */ - for (i = 0; i < csio->sglist_cnt; i++) { - rv = add_physical_chunk(hcb, segs[i].ds_addr, - segs[i].ds_len, dir); - if (rv != CAM_REQ_CMP) - break; - } - } - } - if(rv != CAM_REQ_CMP) { - /* 如果成功添加了一chunk,我们希望add_*_chunk()函数返回 - * CAM_REQ_CMP,如果请求太大(太多字节或太多chunks) - * 则返回CAM_REQ_TOO_BIG, 其他情况下返回<errorname>CAM_REQ_INVALID</errorname>。 - */ - free_hcb_and_ccb_done(hcb, ccb, rv); - return; - } - end_data:</programlisting> - - <para>如果这个CCB不允许断开连接,我们就传递这个信息到hcb: - </para> - -<programlisting> if(ccb_h->flags & CAM_DIS_DISCONNECT) - hcb_disable_disconnect(hcb);</programlisting> - - <para>如果控制器能够完全自己运行REQUEST SENSE命令,则也应当将旗标 - CAM_DIS_AUTOSENSE的值传递给它,这样可以在CAM子系统不想REQUEST SENSE - 时阻止自动REQUEST SENSE。</para> - - <para>剩下的唯一事情是设置超时,将我们的hcb传递给硬件并返回,余下的 - 由中断处理函数(或超时处理函数)完成。</para> - -<programlisting> ccb_h->timeout_ch = timeout(xxx_timeout, (caddr_t) hcb, - (ccb_h->timeout * hz) / 1000); /* 将毫秒转换为滴答数 */ - put_hcb_into_hardware_queue(hcb); - return;</programlisting> - - <para>这儿是返回CCB的函数的一个可能实现:</para> - -<programlisting> static void - free_hcb_and_ccb_done(struct xxx_hcb *hcb, union ccb *ccb, u_int32_t status) - { - struct xxx_softc *softc = hcb->softc; - - ccb->ccb_h.ccb_hcb = 0; - if(hcb != NULL) { - untimeout(xxx_timeout, (caddr_t) hcb, ccb->ccb_h.timeout_ch); - /* 我们要释放hcb,因此资源短缺问题也就不存在了 */ - if(softc->flags & RESOURCE_SHORTAGE) { - softc->flags &= ~RESOURCE_SHORTAGE; - status |= CAM_RELEASE_SIMQ; - } - free_hcb(hcb); /* 同时从任何内部列表中移除hcb */ - } - ccb->ccb_h.status = status | - (ccb->ccb_h.status & ~(CAM_STATUS_MASK|CAM_SIM_QUEUED)); - xpt_done(ccb); - }</programlisting> - </listitem> - - <listitem><para><emphasis>XPT_RESET_DEV</emphasis> - 发送SCSI - <quote>BUS DEVICE RESET</quote>消息到设备</para> - - <para>除了头部外CCB中没有数据传输,其中最让人感兴趣的参量为target_id。 - 依赖于控制器硬件,硬件控制块就像XPT_SCSI_IO请求中那样被创建 - (参看XPT_SCSI_IO请求的描述)并被发送到控制器,或者立即编程让SCSI - 控制器发送RESET消息到设备,或者这个请求可能只是不被支持 - (并返回状态 <errorname>CAM_REQ_INVALID</errorname>)。而且请求完成时,目标的所有已断开 - 连接(disconnected)的事务必须被中止(可能在中断例程中)。</para> - - <para>而且目标的所有当前协商在复位时会丢失,因此它们也可能被清除。 - 或者清除可能被延迟,因为不管怎样目标将会在下一次事务时请求重新协商。 - </para></listitem> - - <listitem><para><emphasis>XPT_RESET_BUS</emphasis> - - 发送RESET信号到SCSI总线</para> - - <para>CCB中并不传递参量,唯一感兴趣的参量是由指向结构sim的指针标识 - 的SCSI总线。</para> - - <para>最小实现会忘记总线上所有设备的SCSI协商,并返回状态 - CAM_REQ_CMP。</para> - - <para>恰当的实现实际上会另外复位SCSI总线(可能也复位SCSI控制器)并 - 将所有在硬件队列中的和断开连接的那些正被处理的CCB的完成状态标记为 - CAM_SCSI_BUS_RESET。像这样:</para> - -<programlisting> int targ, lun; - struct xxx_hcb *h, *hh; - struct ccb_trans_settings neg; - struct cam_path *path; - - /* SCSI总线复位可能会花费很长时间,这种情况下应当使用中断或超时来检查 - * 复位是否完成。但为了简单,我们这儿假设复位很快。 - */ - reset_scsi_bus(softc); - - /* 丢弃所有入队的CCB */ - for(h = softc->first_queued_hcb; h != NULL; h = hh) { - hh = h->next; - free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET); - } - - /* 协商的(清除操作后的)干净值,我们报告这个值 */ - neg.bus_width = 8; - neg.sync_period = neg.sync_offset = 0; - neg.valid = (CCB_TRANS_BUS_WIDTH_VALID - | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID); - - /* 丢弃所有断开连接的CCB和干净的协商(译注:干净=clean) */ - for(targ=0; targ <= OUR_MAX_SUPPORTED_TARGET; targ++) { - clean_negotiations(softc, targ); - - /* 如果可能报告事件 */ - if(xpt_create_path(&path, /*periph*/NULL, - cam_sim_path(sim), targ, - CAM_LUN_WILDCARD) == CAM_REQ_CMP) { - xpt_async(AC_TRANSFER_NEG, path, &neg); - xpt_free_path(path); - } - - for(lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++) - for(h = softc->first_discon_hcb[targ][lun]; h != NULL; h = hh) { - hh=h->next; - free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET); - } - } - - ccb->ccb_h.status = CAM_REQ_CMP; - xpt_done(ccb); - - /* 报告事件 */ - xpt_async(AC_BUS_RESET, softc->wpath, NULL); - return;</programlisting> - - <para>将SCSI总线复位作为函数来实现可能是个好主意,因为如果事情出了差错, - 它会被超时函数作为最后的报告来重用。</para></listitem> - - <listitem><para><emphasis>XPT_ABORT</emphasis> - 中止指定的CCB - </para> - - <para>参量在联合ccb的实例<quote>struct ccb_abort cab</quote> - 中传输。其中唯一的参量字段为:</para> - - <para><emphasis>abort_ccb</emphasis> - 指向被中止的ccb的指针 - </para> - - <para>如果不支持中断就返回CAM_UA_ABORT。这也是最小化实现这个调用的 - 简易方式,任何情况下都返回CAM_UA_ABORT。</para> - - <para>困难方式则是真正地实现这个请求。首先检查应用到SCSI事务的中止: - </para> - -<programlisting> struct ccb *abort_ccb; - abort_ccb = ccb->cab.abort_ccb; - - if(abort_ccb->ccb_h.func_code != XPT_SCSI_IO) { - ccb->ccb_h.status = CAM_UA_ABORT; - xpt_done(ccb); - return; - }</programlisting> - - <para>然后需要在我们的队列中找到这个CCB。这可以通过遍历我们所有硬件 - 控制块列表,查找与这个CCB关联的控制块来完成:</para> - -<programlisting> struct xxx_hcb *hcb, *h; - - hcb = NULL; - - /* 我们假设softc->first_hcb是与此总线关联的所有HCB的列表头元素, - * 包括那些入队待处理的、硬件正在处理的和断开连接的那些。 - */ - for(h = softc->first_hcb; h != NULL; h = h->next) { - if(h->ccb == abort_ccb) { - hcb = h; - break; - } - } - - if(hcb == NULL) { - /* 我们的队列中没有这样的CCB */ - ccb->ccb_h.status = CAM_PATH_INVALID; - xpt_done(ccb); - return; - } - - hcb=found_hcb;</programlisting> - - <para>现在我们来看一下HCB当前的处理状态。它可能或呆在队列中正等待 - 被发送到SCSI总线,或此时正在传输中,或已断开连接并等待命令结果, - 或者实际上已由硬件完成但尚未被软件标记为完成。为了确保我们不会 - 与硬件产生竞争条件,我们将HCB标记为中止(aborted),这样如果这个 - HCB要被发送到SCSI总线的话,SCSI控制器将会看到这个旗标并跳过它。 - </para> - -<programlisting> int hstatus; - - /* 此处显示为一个函数,有时需要特殊动作才能使得这个旗标对硬件可见 - */ - set_hcb_flags(hcb, HCB_BEING_ABORTED); - - abort_again: - - hstatus = get_hcb_status(hcb); - switch(hstatus) { - case HCB_SITTING_IN_QUEUE: - remove_hcb_from_hardware_queue(hcb); - /* 继续执行 */ - case HCB_COMPLETED: - /* 这是一种简单的情况 */ - free_hcb_and_ccb_done(hcb, abort_ccb, CAM_REQ_ABORTED); - break;</programlisting> - - <para>如果CCB此时正在传输中,我们一般会以某种硬件相关的方式发信号 - 给SCSI控制器,通知它我们希望中止当前的传输。SCSI控制器会设置 - SCSI ATTENTION信号,并当目标对其进行响应后发送ABORT消息。我们也复位 - 超时,以确保目标不会永远睡眠。如果命令不能在某个合理的时间,如 - 10秒内中止,超时例程就会运行并复位整个SCSI总线。由于命令会在某个 - 合理的时间后被中止,因此我们现在可以只将中止请求返回,当作成功完成, - 并将被中止的CCB标记为中止(但还没有将它标记为完成)。</para> - -<programlisting> case HCB_BEING_TRANSFERRED: - untimeout(xxx_timeout, (caddr_t) hcb, abort_ccb->ccb_h.timeout_ch); - abort_ccb->ccb_h.timeout_ch = - timeout(xxx_timeout, (caddr_t) hcb, 10 * hz); - abort_ccb->ccb_h.status = CAM_REQ_ABORTED; - /* 要求控制器中止CCB,然后产生一个中断并停止 - */ - if(signal_hardware_to_abort_hcb_and_stop(hcb) < 0) { - /* 哎呀,我们没有获得与硬件的竞争条件,在我们中止 - * 这个事务之前它就脱离总线,再尝试一次 - * (译注:脱离=getoff)*/ - goto abort_again; - } - - break;</programlisting> - - <para>如果CCB位于断开连接的列表中,则将它设置为中止请求,并在硬件 - 队列的前端将它重新入队。复位超时,并报告中止请求完成。</para> - -<programlisting> case HCB_DISCONNECTED: - untimeout(xxx_timeout, (caddr_t) hcb, abort_ccb->ccb_h.timeout_ch); - abort_ccb->ccb_h.timeout_ch = - timeout(xxx_timeout, (caddr_t) hcb, 10 * hz); - put_abort_message_into_hcb(hcb); - put_hcb_at_the_front_of_hardware_queue(hcb); - break; - } - ccb->ccb_h.status = CAM_REQ_CMP; - xpt_done(ccb); - return;</programlisting> - - <para>这就是关于ABORT请求的全部,尽管还有一个问题。由于ABORT消息 - 清除LUN上所有正在进行中的事务,我们必须将LUN上所有其他活动事务 - 标记为中止。那应当在中断例程中完成,且在中止事务之后。</para> - - <para>将CCB中止作为函数来实现可能是个很好的主意,因为如果I/O事务超时 - 这个函数能够被重用。唯一的不同是超时事务将为超时请求返回状态 - CAM_CMD_TIMEOUT。于是XPT_ABORT的case语句就会很小,像下面这样: - </para> - -<programlisting> case XPT_ABORT: - struct ccb *abort_ccb; - abort_ccb = ccb->cab.abort_ccb; - - if(abort_ccb->ccb_h.func_code != XPT_SCSI_IO) { - ccb->ccb_h.status = CAM_UA_ABORT; - xpt_done(ccb); - return; - } - if(xxx_abort_ccb(abort_ccb, CAM_REQ_ABORTED) < 0) - /* no such CCB in our queue */ - ccb->ccb_h.status = CAM_PATH_INVALID; - else - ccb->ccb_h.status = CAM_REQ_CMP; - xpt_done(ccb); - return;</programlisting> - </listitem> - - <listitem><para><emphasis>XPT_SET_TRAN_SETTINGS</emphasis> - - 显式设置SCSI传输设置的值</para> - - <para>在联合ccb的实例<quote>struct ccb_trans_setting cts</quote> - 中传输的参量:</para> - - <itemizedlist> - <listitem><para><emphasis>valid</emphasis> - - 位掩码,显示应当更新那些设置:</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_SYNC_RATE_VALID</emphasis> - - 同步传输速率</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_SYNC_OFFSET_VALID</emphasis> - - 同步位移</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_BUS_WIDTH_VALID</emphasis> - - 总线宽度</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_DISC_VALID</emphasis> - - 设置启用/禁用断开连接</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_TQ_VALID</emphasis> - - 设置启用/禁用带标签的排队</para></listitem> - - <listitem><para><emphasis>flags</emphasis> - - 由两部分组成,两元参量和子操作标识。两元参量为:</para> - <itemizedlist> - <listitem><para><emphasis>CCB_TRANS_DISC_ENB</emphasis> - - 启用断开连接</para></listitem> - <listitem><para><emphasis>CCB_TRANS_TAG_ENB</emphasis> - - 启用带标签的排队</para></listitem> - </itemizedlist> - </listitem> - - <listitem><para>子操作为:</para> - <itemizedlist> - <listitem><para><emphasis>CCB_TRANS_CURRENT_SETTINGS</emphasis> - - 改变当前的协商</para></listitem> - - <listitem><para><emphasis>CCB_TRANS_USER_SETTINGS</emphasis> - - 记住希望的用户值 </para></listitem> - - <listitem><para><emphasis>sync_period, sync_offset</emphasis> - - 自解释的,如果sync_offset==0则请求同步模式 - </para></listitem> - - <listitem><para><emphasis>bus_width</emphasis> - - 总线带宽,以位计(而不是字节) - <tip><title>译注</title><para>参考原文和源码</para></tip> - </para></listitem> - - </itemizedlist> - </listitem> - - </itemizedlist> - - <para>支持两组协商参数,用户设置和当前设置。用户设置在SIM驱动程序中 - 实际上用得不多,这通常只是一片内存,供上层存储(并在以后恢复)其关于 - 参数的一些主张。设置用户参数并不会导致重新协商传输速率。但当SCSI - 控制器协商时,它必须永远不能设置高于用户参数的值,因此它实质上是 - 上限。</para> - - <para>当前设置,正如其名字所示,指当前的。改变它们意味着下一次传输时 - 必须重新协商参数。又一次,这些<quote>new current settings</quote> - 并没有被假定为强制用于设备上,它们只是用作协商的起始步骤。此外, - 它们必须受SCSI控制器的实际能力限制:例如,如果SCSI控制器有8位总线, - 而请求要求设置16位传输,则在发送给设备前参数必须被悄悄地截取为8位。 - </para> - - <para>一个需要注意的问题就是总线宽度和同步两个参数是针对每目标的而言的, - 而断开连接和启用标签两个参数是针对每lun而言的。</para> - - <para>建议的实现是保持3组协商参数(总线宽度和同步传输): - </para> - - <itemizedlist> - <listitem><para><emphasis>user</emphasis> - 用户的一组,如上 - </para></listitem> - - <listitem><para><emphasis>current</emphasis> - 实际生效的那些 - </para></listitem> - - <listitem><para><emphasis>goal</emphasis> - - 通过设置<quote>current</quote>参数所请求的那些</para></listitem> - </itemizedlist> - - <para>代码看起来像:</para> - -<programlisting> struct ccb_trans_settings *cts; - int targ, lun; - int flags; - - cts = &ccb->cts; - targ = ccb_h->target_id; - lun = ccb_h->target_lun; - flags = cts->flags; - if(flags & CCB_TRANS_USER_SETTINGS) { - if(flags & CCB_TRANS_SYNC_RATE_VALID) - softc->user_sync_period[targ] = cts->sync_period; - if(flags & CCB_TRANS_SYNC_OFFSET_VALID) - softc->user_sync_offset[targ] = cts->sync_offset; - if(flags & CCB_TRANS_BUS_WIDTH_VALID) - softc->user_bus_width[targ] = cts->bus_width; - - if(flags & CCB_TRANS_DISC_VALID) { - softc->user_tflags[targ][lun] &= ~CCB_TRANS_DISC_ENB; - softc->user_tflags[targ][lun] |= flags & CCB_TRANS_DISC_ENB; - } - if(flags & CCB_TRANS_TQ_VALID) { - softc->user_tflags[targ][lun] &= ~CCB_TRANS_TQ_ENB; - softc->user_tflags[targ][lun] |= flags & CCB_TRANS_TQ_ENB; - } - } - if(flags & CCB_TRANS_CURRENT_SETTINGS) { - if(flags & CCB_TRANS_SYNC_RATE_VALID) - softc->goal_sync_period[targ] = - max(cts->sync_period, OUR_MIN_SUPPORTED_PERIOD); - if(flags & CCB_TRANS_SYNC_OFFSET_VALID) - softc->goal_sync_offset[targ] = - min(cts->sync_offset, OUR_MAX_SUPPORTED_OFFSET); - if(flags & CCB_TRANS_BUS_WIDTH_VALID) - softc->goal_bus_width[targ] = min(cts->bus_width, OUR_BUS_WIDTH); - - if(flags & CCB_TRANS_DISC_VALID) { - softc->current_tflags[targ][lun] &= ~CCB_TRANS_DISC_ENB; - softc->current_tflags[targ][lun] |= flags & CCB_TRANS_DISC_ENB; - } - if(flags & CCB_TRANS_TQ_VALID) { - softc->current_tflags[targ][lun] &= ~CCB_TRANS_TQ_ENB; - softc->current_tflags[targ][lun] |= flags & CCB_TRANS_TQ_ENB; - } - } - ccb->ccb_h.status = CAM_REQ_CMP; - xpt_done(ccb); - return;</programlisting> - - <para>此后当下一次要处理I/O请求时,它会检查其是否需要重新协商, - 例如通过调用函数target_negotiated(hcb)。它可以如下实现:</para> - -<programlisting> int - target_negotiated(struct xxx_hcb *hcb) - { - struct softc *softc = hcb->softc; - int targ = hcb->targ; - - if( softc->current_sync_period[targ] != softc->goal_sync_period[targ] - || softc->current_sync_offset[targ] != softc->goal_sync_offset[targ] - || softc->current_bus_width[targ] != softc->goal_bus_width[targ] ) - return 0; /* FALSE */ - else - return 1; /* TRUE */ - }</programlisting> - - <para>重新协商这些值后,结果值必须同时赋给当前和目的(goal)参数, - 这样对于以后的I/O事务当前和目的参数将相同,且 - <function>target_negotiated()</function>会返回TRUE。当初始化卡 - (在<function>xxx_attach()</function>中)当前协商值必须被初始化为 - 最窄同步模式,目的和当前值必须被初始化为控制器所支持的最大值。 - (译注:原文可能有误,此处未改) - </para></listitem> - - <listitem><para><emphasis>XPT_GET_TRAN_SETTINGS</emphasis> - - 获得SCSI传输设置的值</para> - - <para>此操作为XPT_SET_TRAN_SETTINGS的逆操作。用通过旗标 - CCB_TRANS_CURRENT_SETTINGS或CCB_TRANS_USER_SETTINGS(如果同时设置则 - 现有驱动程序返回当前设置)所请求而得的数据填充CCB实例 - <quote>struct ccb_trans_setting cts</quote>. </para></listitem> - - <listitem> - <indexterm><primary>BIOS(基本输入输出系统, Basic Input Output System)</primary></indexterm> - - <para><emphasis>XPT_CALC_GEOMETRY</emphasis> - - 计算磁盘的逻辑(BIOS)结构(geometry)</para> - - <para>参量在联合ccb的实例<quote>struct ccb_calc_geometry ccg</quote> - 中传输:</para> - - <itemizedlist> - - <listitem><para><emphasis>block_size</emphasis> - - 输入,以字节计的块大小(也称为扇区)</para></listitem> - - <listitem><para><emphasis>volume_size</emphasis> - - 输入,以字节计的卷大小</para></listitem> - - <listitem><para><emphasis>cylinders</emphasis> - - 输出,逻辑柱面</para></listitem> - - <listitem><para><emphasis>heads</emphasis> - - 输出,逻辑磁头</para></listitem> - - <listitem><para><emphasis>secs_per_track</emphasis> - - 输出,每磁道的逻辑扇区</para></listitem> - - </itemizedlist> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary><secondary>BIOS(基本输入输出系统, Basic Input Output System)</secondary></indexterm> - <para>如果返回的结构与SCSI控制器BIOS所想象的差别很大,并且SCSI - 控制器上的磁盘被作为可引导的,则系统可能无法启动。从aic7xxx - 驱动程序中摘取的典型计算示例:</para> - -<programlisting> struct ccb_calc_geometry *ccg; - u_int32_t size_mb; - u_int32_t secs_per_cylinder; - int extended; - - ccg = &ccb->ccg; - size_mb = ccg->volume_size - / ((1024L * 1024L) / ccg->block_size); - extended = check_cards_EEPROM_for_extended_geometry(softc); - - if (size_mb > 1024 && extended) { - ccg->heads = 255; - ccg->secs_per_track = 63; - } else { - ccg->heads = 64; - ccg->secs_per_track = 32; - } - secs_per_cylinder = ccg->heads * ccg->secs_per_track; - ccg->cylinders = ccg->volume_size / secs_per_cylinder; - ccb->ccb_h.status = CAM_REQ_CMP; - xpt_done(ccb); - return;</programlisting> - - <para>这给出了一般思路,精确计算依赖于特定BIOS的癖好(quirk)。如果 - BIOS没有提供方法设置EEPROM中的<quote>extended translation</quote> - 旗标,则此旗标通常应当假定等于1。其他流行结构有:</para> - -<programlisting> 128 heads, 63 sectors - Symbios控制器 - 16 heads, 63 sectors - 老式控制器</programlisting> - - <para>一些系统BIOS和SCSI BIOS会相互竞争,胜负不定,例如Symbios - 875/895 SCSI和Phoenix BIOS的结合在系统加电时会给出结构128/63, - 而当冷启动或软启动后会是255/63。</para> - </listitem> - - <listitem><para><emphasis>XPT_PATH_INQ</emphasis> - 路径问询, - 换句话说,获得SIM驱动程序和SCSI控制器(也称为HBA - 主机总线适配器) - 的特性。</para> - - <para>特性在联合ccb的实例<quote>struct ccb_pathinq cpi</quote> - 中返回:</para> - - <itemizedlist> - - <listitem><para>version_num - SIM驱动程序号,当前所有驱动程序使用1 - </para></listitem> - - <listitem><para>hba_inquiry - 控制器所支持特性的位掩码: - </para></listitem> - - <listitem><para>PI_MDP_ABLE - 支持MDP消息(来自SCSI3的一些东西?) - </para></listitem> - - <listitem><para>PI_WIDE_32 - 支持32位宽SCSI - </para></listitem> - - <listitem><para>PI_WIDE_16 - 支持16位宽SCSI - </para></listitem> - - <listitem><para>PI_SDTR_ABLE - 可以协商同步传输速率 - </para></listitem> - - <listitem><para>PI_LINKED_CDB - 支持链接的命令 - </para></listitem> - - <listitem><para>PI_TAG_ABLE - 支持带标签的命令 - </para></listitem> - - <listitem><para>PI_SOFT_RST - 支持软复位选择 - (硬复位和软复位在SCSI总线中是互斥的) - </para></listitem> - - <listitem><para>target_sprt - 目标模式支持的旗标,如果不支持则为0 - </para></listitem> - - <listitem><para>hba_misc - 控制器特性杂项: - </para></listitem> - - <listitem><para>PIM_SCANHILO - 从高ID到低ID的总线扫描 - </para></listitem> - - <listitem><para>PIM_NOREMOVE - 可移除设备不包括在扫描之列 - </para></listitem> - - <listitem><para>PIM_NOINITIATOR - 不支持发起者角色 - </para></listitem> - - <listitem><para>PIM_NOBUSRESET - 用户禁用初始BUS RESET - </para></listitem> - - <listitem><para>hba_eng_cnt - 神秘的HBA引擎计数,与压缩有关的一些 - 东西,当前总是置为0</para></listitem> - - <listitem><para>vuhba_flags - 供应商唯一的旗标,当前未用 - </para></listitem> - - <listitem><para>max_target - 最大支持的目标ID(对8位总线为7, - 16位总线为15,光纤通道为127)</para></listitem> - - <listitem><para>max_lun - 最大支持的LUN ID(对较老的SCSI控制器 - 为7,较新的为63)</para></listitem> - - <listitem><para>async_flags - 安装的异步处理函数的位掩码,当前未用 - </para></listitem> - - <listitem><para>hpath_id - 子系统中最高的路径ID,当前未用 - </para></listitem> - - <listitem><para>unit_number - 控制器单元号,cam_sim_unit(sim) - </para></listitem> - - <listitem><para>bus_id - 总线号,cam_sim_bus(sim) - </para></listitem> - - <listitem><para>initiator_id - 控制器自己的SCSI ID - </para></listitem> - - <listitem><para>base_transfer_speed - 异步窄传输的名义传输速率, - 以KB/s计,对于SCSI等于3300</para></listitem> - - <listitem><para>sim_vid - SIM驱动程序的供应商ID,以0结束的字符串, - 包含结尾0在内的最大长度为SIM_IDLEN</para></listitem> - - <listitem><para>hba_vid - SCSI控制器的供应商ID,以0结束的字符串, - 包含结尾0在内的最大长度为HBA_IDLEN</para></listitem> - - <listitem><para>dev_name - 设备驱动程序名字,以0结尾的字符串, - 包含结尾0在内的最大长度为DEV_IDLEN,等于cam_sim_name(sim) - </para></listitem> - - </itemizedlist> - - <para>设置字符串字段的建议方法是使用strncpy,如:</para> - -<programlisting> strncpy(cpi->dev_name, cam_sim_name(sim), DEV_IDLEN);</programlisting> - - <para>设置这些值后将状态设置为CAM_REQ_CMP,并将CCB标记为完成。</para> - </listitem> - </itemizedlist> - - </sect1> - - <sect1 xml:id="scsi-polling"> - <title>轮询</title> - - <funcsynopsis><funcprototype> - <funcdef>static void - <function>xxx_poll</function> - </funcdef> - <paramdef> - <parameter>struct cam_sim *sim</parameter> - </paramdef> - </funcprototype></funcsynopsis> - - <para>轮询函数用于当中断子系统不起作用时(例如,系统崩溃或正在创建 - 系统转储)模拟中断。CAM子系统在调用轮询函数前设置适当的中断级别。 - 因此它所需做全部的只是调用中断例程(或其他方法,轮询例程来 - 进行实际动作, 而中断例程只是调用轮询例程)。那么为什么要找麻烦 - 弄出一个单独的函数来呢?这是由于不同的调用约定。 - <function>xxx_poll</function>例程取结构cam_sim的指针作为参量, - 而PCI中断例程按照普通约定取的是指向结构 - <varname remap="structname">xxx_softc</varname>的指针,ISA中断例程只是取设备号, - 因此轮询例程一般看起来像:</para> - -<programlisting>static void -xxx_poll(struct cam_sim *sim) -{ - xxx_intr((struct xxx_softc *)cam_sim_softc(sim)); /* for PCI device */ -}</programlisting> - - <para>or</para> - -<programlisting>static void -xxx_poll(struct cam_sim *sim) -{ - xxx_intr(cam_sim_unit(sim)); /* for ISA device */ -}</programlisting> - - </sect1> - - <sect1 xml:id="scsi-async"> - <title>异步事件</title> - - <para>如果建立了异步事件回调,则应当定义回调函数。</para> - -<programlisting>static void -ahc_async(void *callback_arg, u_int32_t code, struct cam_path *path, void *arg)</programlisting> - - <itemizedlist> - <listitem><para>callback_arg - 注册回调时提供的值 - </para></listitem> - - <listitem><para>code - 标识事件类型</para></listitem> - - <listitem><para>path - 标识事件作用于其上的设备 - </para></listitem> - - <listitem><para>arg - 事件特定的参量</para></listitem> - </itemizedlist> - - <para>单一类型事件的实现,AC_LOST_DEVICE,看起来如下:</para> - -<programlisting> struct xxx_softc *softc; - struct cam_sim *sim; - int targ; - struct ccb_trans_settings neg; - - sim = (struct cam_sim *)callback_arg; - softc = (struct xxx_softc *)cam_sim_softc(sim); - switch (code) { - case AC_LOST_DEVICE: - targ = xpt_path_target_id(path); - if(targ <= OUR_MAX_SUPPORTED_TARGET) { - clean_negotiations(softc, targ); - /* send indication to CAM */ - neg.bus_width = 8; - neg.sync_period = neg.sync_offset = 0; - neg.valid = (CCB_TRANS_BUS_WIDTH_VALID - | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID); - xpt_async(AC_TRANSFER_NEG, path, &neg); - } - break; - default: - break; - }</programlisting> - - </sect1> - - <sect1 xml:id="scsi-interrupts"> - <title>中断</title> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary><secondary>interrupts(中断)</secondary></indexterm> - - <para>中断例程的确切类型依赖于SCSI控制器所连接到的外围总线的类型(PCI, - ISA等等)。</para> - - <para>SIM驱动程序的中断例程运行在中断级别splcam上。因此应当在驱动 - 程序中使用<function>splcam()</function>来同步中断例程与驱动程序 - 剩余部分的活动(对于能察觉多处理器的驱动程序,事情更要有趣,但 - 此处我们忽略这种情况)。本文档中的伪代码简单地忽略了同步问题。 - 实际代码一定不能忽略它们。一个较笨的办法就是在进入其他例程的 - 入口点处设<function>splcam()</function>,并在返回时将它复位,从而 - 用一个大的临界区保护它们。为了确保中断级别总是会被恢复,可以定义 - 一个包装函数,如:</para> - -<programlisting> static void - xxx_action(struct cam_sim *sim, union ccb *ccb) - { - int s; - s = splcam(); - xxx_action1(sim, ccb); - splx(s); - } - - static void - xxx_action1(struct cam_sim *sim, union ccb *ccb) - { - ... process the request ... - }</programlisting> - - <para>这种方法简单而且健壮,但它存在的问题是中断可能会被阻塞相对 - 很长的事件,这会对系统性能产生负面影响。另一方面, - <function>spl()</function>函数族有相当高的额外开销,因此大量 - 很小的临界区可能也不好。</para> - - <para>中断例程处理的情况和其中细节严重依赖于硬件。我们考虑 - <quote>典型(typical)</quote>情况。</para> - - <para>首先,我们检查总线上是否遇到了SCSI复位(可能由同一SCSI总线上 - 的另一SCSI控制器引起)。如果这样我们丢弃所有入队的和断开连接的 - 请求,报告事件并重新初始化我们的SCSI控制器。初始化期间控制器 - 不会发出另一个复位,这对我们十分重要,否则同一SCSI总线上的两个控制器 - 可能会一直来回地复位下去。控制器致命错误/挂起的情况可以在同一 - 地方进行处理,但这可能需要发送RESET信号到SCSI总线来复位与SCSI - 设备的连接状态。</para> - -<programlisting> int fatal=0; - struct ccb_trans_settings neg; - struct cam_path *path; - - if( detected_scsi_reset(softc) - || (fatal = detected_fatal_controller_error(softc)) ) { - int targ, lun; - struct xxx_hcb *h, *hh; - - /* 丢弃所有入队的CCB */ - for(h = softc->first_queued_hcb; h != NULL; h = hh) { - hh = h->next; - free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET); - } - - /* 要报告的协商的干净值 */ - neg.bus_width = 8; - neg.sync_period = neg.sync_offset = 0; - neg.valid = (CCB_TRANS_BUS_WIDTH_VALID - | CCB_TRANS_SYNC_RATE_VALID | CCB_TRANS_SYNC_OFFSET_VALID); - - /* 丢弃所有断开连接的CCB和干净协商 */ - for(targ=0; targ <= OUR_MAX_SUPPORTED_TARGET; targ++) { - clean_negotiations(softc, targ); - - /* report the event if possible */ - if(xpt_create_path(&path, /*periph*/NULL, - cam_sim_path(sim), targ, - CAM_LUN_WILDCARD) == CAM_REQ_CMP) { - xpt_async(AC_TRANSFER_NEG, path, &neg); - xpt_free_path(path); - } - - for(lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++) - for(h = softc->first_discon_hcb[targ][lun]; h != NULL; h = hh) { - hh=h->next; - if(fatal) - free_hcb_and_ccb_done(h, h->ccb, CAM_UNREC_HBA_ERROR); - else - free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET); - } - } - - /* 报告事件 */ - xpt_async(AC_BUS_RESET, softc->wpath, NULL); - - /* 重新初始化可能花很多时间,这种情况下应当由另一中断发信号 - * 指示初始化否完成,或在超时时检查 - 但为了简单我们假设 - * 初始化真的很快 - */ - if(!fatal) { - reinitialize_controller_without_scsi_reset(softc); - } else { - reinitialize_controller_with_scsi_reset(softc); - } - schedule_next_hcb(softc); - return; - }</programlisting> - - <para>如果中断不是由控制器范围的条件引起的,则很可能当前硬件控制块 - 出现了问题。依赖于硬件,可能有非HCB相关的事件,此处我们指示不考虑 - 它们。然后我们分析这个HCB发生了什么:</para> - -<programlisting> struct xxx_hcb *hcb, *h, *hh; - int hcb_status, scsi_status; - int ccb_status; - int targ; - int lun_to_freeze; - - hcb = get_current_hcb(softc); - if(hcb == NULL) { - /* 或者丢失(stray)的中断,或者某些东西严重错误, - * 或者这是硬件相关的某些东西 - */ - 进行必要的处理; - return; - } - - targ = hcb->target; - hcb_status = get_status_of_current_hcb(softc);</programlisting> - - <para>首先我们检查HCB是否完成,如果完成我们就检查返回的SCSI状态。 - </para> - -<programlisting> if(hcb_status == COMPLETED) { - scsi_status = get_completion_status(hcb);</programlisting> - - <para>然后看这个状态是否与REQUEST SENSE命令有关,如果有关则简单 - 地处理一下它。</para> - -<programlisting> if(hcb->flags & DOING_AUTOSENSE) { - if(scsi_status == GOOD) { /* autosense成功 */ - hcb->ccb->ccb_h.status |= CAM_AUTOSNS_VALID; - free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_SCSI_STATUS_ERROR); - } else { - autosense_failed: - free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_AUTOSENSE_FAIL); - } - schedule_next_hcb(softc); - return; - }</programlisting> - - <para>否则命令自身已经完成,把更多注意力放在细节上。如果这个CCB - 没有禁用auto-sense并且命令连同sense数据失败,则运行REQUEST SENSE - 命令接收那些数据。</para> - -<programlisting> hcb->ccb->csio.scsi_status = scsi_status; - calculate_residue(hcb); - - if( (hcb->ccb->ccb_h.flags & CAM_DIS_AUTOSENSE)==0 - && ( scsi_status == CHECK_CONDITION - || scsi_status == COMMAND_TERMINATED) ) { - /* 启动auto-SENSE */ - hcb->flags |= DOING_AUTOSENSE; - setup_autosense_command_in_hcb(hcb); - restart_current_hcb(softc); - return; - } - if(scsi_status == GOOD) - free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_REQ_CMP); - else - free_hcb_and_ccb_done(hcb, hcb->ccb, CAM_SCSI_STATUS_ERROR); - schedule_next_hcb(softc); - return; - }</programlisting> - - <para>属于协商事件的一个典型事情:从SCSI目标(回答我们的协商企图或 - 由目标发起的)接收到的协商消息,或目标无法协商(拒绝我们的协商消息 - 或不回答它们)。</para> - -<programlisting> switch(hcb_status) { - case TARGET_REJECTED_WIDE_NEG: - /* 恢复到8-bit总线 */ - softc->current_bus_width[targ] = softc->goal_bus_width[targ] = 8; - /* 报告事件 */ - neg.bus_width = 8; - neg.valid = CCB_TRANS_BUS_WIDTH_VALID; - xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg); - continue_current_hcb(softc); - return; - case TARGET_ANSWERED_WIDE_NEG: - { - int wd; - - wd = get_target_bus_width_request(softc); - if(wd <= softc->goal_bus_width[targ]) { - /* 可接受的回答 */ - softc->current_bus_width[targ] = - softc->goal_bus_width[targ] = neg.bus_width = wd; - - /* 报告事件 */ - neg.valid = CCB_TRANS_BUS_WIDTH_VALID; - xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg); - } else { - prepare_reject_message(hcb); - } - } - continue_current_hcb(softc); - return; - case TARGET_REQUESTED_WIDE_NEG: - { - int wd; - - wd = get_target_bus_width_request(softc); - wd = min (wd, OUR_BUS_WIDTH); - wd = min (wd, softc->user_bus_width[targ]); - - if(wd != softc->current_bus_width[targ]) { - /* 总线宽度改变了 */ - softc->current_bus_width[targ] = - softc->goal_bus_width[targ] = neg.bus_width = wd; - - /* 报告事件 */ - neg.valid = CCB_TRANS_BUS_WIDTH_VALID; - xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg); - } - prepare_width_nego_rsponse(hcb, wd); - } - continue_current_hcb(softc); - return; - }</programlisting> - - <para>然后我们用与前面相同的笨办法处理auto-sense期间可能出现的任何 - 错误。否则,我们再一次进入细节。</para> - -<programlisting> if(hcb->flags & DOING_AUTOSENSE) - goto autosense_failed; - - switch(hcb_status) {</programlisting> - - <para>我们考虑的下一事件是未预期的连接断开,这个事件在ABORT或 - BUS DEVICE RESET消息之后被看作是正常的,其他情况下是非正常的。 - </para> - -<programlisting> case UNEXPECTED_DISCONNECT: - if(requested_abort(hcb)) { - /* 中止影响目标和LUN上的所有命令,因此将那个目标和LUN上的 - * 所有断开连接的HCB也标记为中止 - */ - for(h = softc->first_discon_hcb[hcb->target][hcb->lun]; - h != NULL; h = hh) { - hh=h->next; - free_hcb_and_ccb_done(h, h->ccb, CAM_REQ_ABORTED); - } - ccb_status = CAM_REQ_ABORTED; - } else if(requested_bus_device_reset(hcb)) { - int lun; - - /* 复位影响那个目标上的所有命令,因此将那个目标和LUN上的 - * 所有断开连接的HCB标记为复位 - */ - - for(lun=0; lun <= OUR_MAX_SUPPORTED_LUN; lun++) - for(h = softc->first_discon_hcb[hcb->target][lun]; - h != NULL; h = hh) { - hh=h->next; - free_hcb_and_ccb_done(h, h->ccb, CAM_SCSI_BUS_RESET); - } - - /* 发送事件 */ - xpt_async(AC_SENT_BDR, hcb->ccb->ccb_h.path_id, NULL); - - /* 这是CAM_RESET_DEV请求本身,它完成了 */ - ccb_status = CAM_REQ_CMP; - } else { - calculate_residue(hcb); - ccb_status = CAM_UNEXP_BUSFREE; - /* request the further code to freeze the queue */ - hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN; - lun_to_freeze = hcb->lun; - } - break;</programlisting> - - <para>如果目标拒绝接受标签,我们就通知CAM,并返回此LUN的所有命令: - </para> - -<programlisting> case TAGS_REJECTED: - /* 报告事件 */ - neg.flags = 0 & ~CCB_TRANS_TAG_ENB; - neg.valid = CCB_TRANS_TQ_VALID; - xpt_async(AC_TRANSFER_NEG, hcb->ccb.ccb_h.path_id, &neg); - - ccb_status = CAM_MSG_REJECT_REC; - /* 请求后面的代码冻结队列 */ - hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN; - lun_to_freeze = hcb->lun; - break;</programlisting> - - <para>然后我们检查一些其他情况,处理(processing)基本上仅限于设置CCB状态: - </para> - -<programlisting> case SELECTION_TIMEOUT: - ccb_status = CAM_SEL_TIMEOUT; - /* request the further code to freeze the queue */ - hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN; - lun_to_freeze = CAM_LUN_WILDCARD; - break; - case PARITY_ERROR: - ccb_status = CAM_UNCOR_PARITY; - break; - case DATA_OVERRUN: - case ODD_WIDE_TRANSFER: - ccb_status = CAM_DATA_RUN_ERR; - break; - default: - /*以通用方法处理所有其他错误 */ - ccb_status = CAM_REQ_CMP_ERR; - /* 请求后面的代码冻结队列 */ - hcb->ccb->ccb_h.status |= CAM_DEV_QFRZN; - lun_to_freeze = CAM_LUN_WILDCARD; - break; - }</programlisting> - - <para>然后我们检查是否错误严重到需要冻结输入队列,直到它得到处理方可 - 解冻,如果是这样那么就这样来处理:</para> - -<programlisting> if(hcb->ccb->ccb_h.status & CAM_DEV_QFRZN) { - /* 冻结队列 */ - xpt_freeze_devq(ccb->ccb_h.path, /*count*/1); - -* /* 重新入队这个目标/LUN的所有命令,将它们返回CAM */ - - for(h = softc->first_queued_hcb; h != NULL; h = hh) { - hh = h->next; - - if(targ == h->targ - && (lun_to_freeze == CAM_LUN_WILDCARD || lun_to_freeze == h->lun) ) - free_hcb_and_ccb_done(h, h->ccb, CAM_REQUEUE_REQ); - } - } - free_hcb_and_ccb_done(hcb, hcb->ccb, ccb_status); - schedule_next_hcb(softc); - return;</programlisting> - - <para>这包括通用中断处理,尽管特定处理器可能需要某些附加处理。</para> - - </sect1> - - <sect1 xml:id="scsi-errors"> - <title>错误总览</title> - - <indexterm><primary>SCSI(小型计算机系统接口)</primary><secondary>errors(出错)</secondary></indexterm> - - <para>当执行I/O请求时很多事情可能出错。可以在CCB状态中非常详尽地 - 报告错误原因。使用的例子散布于本文档中。为了完整起见此处给出 - 对典型错误条件的建议响应的一个总览:</para> - - <itemizedlist> - - <listitem><para><emphasis>CAM_RESRC_UNAVAIL</emphasis> - 某些资源 - 暂时不可用,并且当其变为可用时SIM驱动程序不能产生事件。这种资源 - 的一个例子就是某些控制器内部硬件资源,当其可用时控制器不会为其 - 产生中断。</para></listitem> - - <listitem><para><emphasis>CAM_UNCOR_PARITY</emphasis> - - 发生不可恢复的奇偶校验错误</para></listitem> - - <listitem><para><emphasis>CAM_DATA_RUN_ERR</emphasis> - - 数据外溢或未预期的数据状态(phase)(跑在另一个方向上而不是 - CAM_DIR_MASK指定的方向),或对于宽传输出现奇数传输长度 - </para></listitem> - - <listitem><para><emphasis>CAM_SEL_TIMEOUT</emphasis> - - 发生选择超时(目标不响应)</para></listitem> - - <listitem><para><emphasis>CAM_CMD_TIMEOUT</emphasis> - - 发生命令超时(超时函数运行)</para></listitem> - - <listitem><para><emphasis>CAM_SCSI_STATUS_ERROR</emphasis> - - 设备返回的错误</para></listitem> - - <listitem><para><emphasis>CAM_AUTOSENSE_FAIL</emphasis> - - 设备返回的错误且REQUEST SENSE命令失败 - </para></listitem> - - <listitem><para><emphasis>CAM_MSG_REJECT_REC</emphasis> - - 收到MESSAGE REJECT消息</para></listitem> - - <listitem><para><emphasis>CAM_SCSI_BUS_RESET</emphasis> - - 收到SCSI总线复位</para></listitem> - - <listitem><para><emphasis>CAM_REQ_CMP_ERR</emphasis> - - 出现<quote>不可能(impossible)</quote>SCSI状态(phase) - 或者其他怪异事情,或者如果进一步的信息不可用则只是通用错误 - </para></listitem> - - <listitem><para><emphasis>CAM_UNEXP_BUSFREE</emphasis> - - 出现未预期的断开连接</para></listitem> - - <listitem><para><emphasis>CAM_BDR_SENT</emphasis> - - BUS DEVICE RESET消息被发送到目标</para></listitem> - - <listitem><para><emphasis>CAM_UNREC_HBA_ERROR</emphasis> - - 不可恢复的主机总线适配器错误</para></listitem> - - <listitem><para><emphasis>CAM_REQ_TOO_BIG</emphasis> - - 请求对于控制器太大</para></listitem> - - <listitem><para><emphasis>CAM_REQUEUE_REQ</emphasis> - - 此请求应当被重新入队以保持事务的次序性。这典型地出现在下列 - 时刻:SIM识别出了应当冻结队列的错误,并且必须在sim级别上将目标的 - 其他入队请求放回到XPT队列。这些错误的典型情况有选择超时、命令 - 超时和其他类似情况。这些情况下出问题的命令返回状态来指示错误, - 此命令和其他还没有被发送到总线的命令被重新入队。 - </para></listitem> - - <listitem><para><emphasis>CAM_LUN_INVALID</emphasis> - - SCSI控制器不支持请求中的LUN ID</para></listitem> - - <listitem><para><emphasis>CAM_TID_INVALID</emphasis> - - SCSI控制器不支持请求中的目标ID</para></listitem> - </itemizedlist> - </sect1> - - <sect1 xml:id="scsi-timeout"> - <title>超时处理</title> - - <para>当HCB的超时期满时,请求就应当被中止,就像处理XPT_ABORT请求 - 一样。唯一区别在于被中止的请求的返回状态应当为CAM_CMD_TIMEOUT - 而不是CAM_REQ_ABORTED(这就是为什么中止的实现最好由函数来完成)。 - 但还有一个可能的问题:如果中止请求自己出了麻烦怎么办?这种情况下 - 应当复位SCSI总线,就像处理XPT_RESET_BUS请求一样(并且将其实现为 - 函数,从两个地方调用的想法也适用于这儿)。而且如果设备复位请求出了 - 问题,我们应当复位整个SCSI总线。因此最终超时函数看起来像下面样子: - </para> - -<programlisting>static void -xxx_timeout(void *arg) -{ - struct xxx_hcb *hcb = (struct xxx_hcb *)arg; - struct xxx_softc *softc; - struct ccb_hdr *ccb_h; - - softc = hcb->softc; - ccb_h = &hcb->ccb->ccb_h; - - if(hcb->flags & HCB_BEING_ABORTED - || ccb_h->func_code == XPT_RESET_DEV) { - xxx_reset_bus(softc); - } else { - xxx_abort_ccb(hcb->ccb, CAM_CMD_TIMEOUT); - } -}</programlisting> - - <para>当我们中止一个请求时,同一目标/LUN的所有其他断开连接的请求 - 也会被中止。因此出现了一个问题,我们应当返回它们的状态 - CAM_REQ_ABORTED还是CAM_CMD_TIMEOUT?当前的驱动程序使用 - CAM_CMD_TIMEOUT。这看起来符合逻辑,因为如果一个请求超时,则可能 - 设备出现了某些的确很糟的事情,因此如果它们没有被扰乱则它们自己 - 应当超时。</para> - - </sect1> - -</chapter> diff --git a/zh_CN.GB2312/books/arch-handbook/smp/chapter.xml b/zh_CN.GB2312/books/arch-handbook/smp/chapter.xml deleted file mode 100644 index 1815559700..0000000000 --- a/zh_CN.GB2312/books/arch-handbook/smp/chapter.xml +++ /dev/null @@ -1,929 +0,0 @@ -<?xml version="1.0" encoding="gb2312"?> -<!-- - The FreeBSD Documentation Project - The FreeBSD SMP Next Generation Project - - Original Revision: 1.26 - $FreeBSD$ ---> -<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="smp"> - <info><title>SMPng 设计文档</title> - <authorgroup> - <author><personname><firstname>John</firstname><surname>Baldwin</surname></personname><contrib>&cnproj.written.by;</contrib></author> - <author><personname><firstname>Robert</firstname><surname>Watson</surname></personname></author> - </authorgroup> - - <copyright> - <year>2002</year> - <year>2004</year> - <year>2005</year> - <holder>John Baldwin</holder> - <holder>Robert Watson</holder> - </copyright> - - <authorgroup> - <author><personname/><contrib>&cnproj.translated.by;</contrib></author> - </authorgroup> - - </info> - - - - <sect1 xml: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 xml: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> 位。 这样做的原因是, - 我们需要把 <varname remap="structfield">mtx_lock</varname> 的值读到某个变量, - 并据此进行决策。 然而, 我们读到的值可能是过时的, - 也可能在我们进行决策的过程中发生变化。 因此, 当执行 - <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 xml: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 xml:id="smp-lock-strategies"> - <title>特定数据的锁策略</title> - - <sect2> - <title>凭据</title> - - <indexterm><primary>credentials(凭据)</primary></indexterm> - - <para><varname remap="structname">struct ucred</varname> 是内核内部的凭据结构体, - 它通常作为内核中以进程为导向的访问控制的依据。 - BSD-派生的系统采用一种 <quote>写时复制</quote> 的模型来处理凭据数据: - 同一凭据结构体可能存在多个引用, 如果需要对其进行修改, - 则这个结构体将被复制、 修改, 然后替换该引用。 - 由于在打开时用于实现访问控制的凭据快取缓存广泛存在, - 这种做法会极大地节省内存。 在迁移到细粒度的 SMP 时, - 这一模型也省去了大量的锁操作, 因为只有未共享的凭据才能实施修改, - 因而避免了在使用共享凭据时额外的同步操作。</para> - - <para>凭据结构体只有一个引用时, 被认为是可变的; - 不允许改变共享的凭据结构体, 否则将可能导致发生竞态条件。 - <varname remap="structfield">cr_mtxp</varname> mutex 用于保护 - <varname remap="structname">struct ucred</varname> 的引用计数, - 以维护其一致性。 使用凭据结构体时, 必须在使用过程中保持有效的引用, - 否则它就可能在这个不合理的消费者使用过程中被释放。</para> - - <para><varname remap="structname">struct ucred</varname> mutex 是一种叶 - mutex, 出于性能考虑, 它通过 mutex 池实现。</para> - - <para>由于多用于访问控制决策, 凭据通常情况下是以只读方式访问的, 此时一般应使用 - <varname remap="structfield">td_ucred</varname>, 因为它不需要上锁。 - 当更新进程凭据时, 检查和更新过程中必须持有 <literal>proc</literal> - 锁。 检查和更新操作必须使用 <varname remap="structfield">p_ucred</varname>, - 以避免检查时和使用时的竞态条件。</para> - - <para>如果所调系统调用将在更新进程凭据之后进行访问控制检查, 则 - <varname remap="structfield">td_ucred</varname> 也必须刷新为当前进程的值。 - 这样做能够避免修改后使用过时的凭据。 内核会自动在进程进入内核时, - 将线程结构体的 <varname remap="structfield">td_ucred</varname> 指针刷新为进程的 - <varname remap="structfield">p_ucred</varname>, 以保证内核访问控制能用到新的凭据。</para> - </sect2> - - <sect2> - <title>文件描述符和文件描述符表</title> - - <para>详细内容将在稍后增加。</para> - </sect2> - - <sect2> - <title>Jail 结构体</title> - - <indexterm><primary>Jail(囚禁)</primary></indexterm> - - <para><varname remap="structname">struct prison</varname> 保存了用于维护那些通过 - &man.jail.2; API 创建的 jail 所用到的管理信息。 这包括 jail - 的主机名、 IP 地址, 以及一些相关的设置。 这个结构体包含引用计数, - 因为指向这一结构体实例的指针会在多种凭据结构之间共享。 - 用了一个 mutex, <varname remap="structfield">pr_mtx</varname> - 来保护对引用计数以及所有 jail 结构体中可变变量的读写访问。 - 有一些变量只会在创建 jail 的时刻发生变化, 只需持有有效的 - <varname remap="structname">struct prison</varname> 就可以开始读这些值了。 - 关于每个项目具体的上锁操作的文档, - 可以在 <filename>sys/jail.h</filename> 的注释中找到。</para> - </sect2> - - <sect2> - <title>MAC 框架</title> - - <indexterm><primary>MAC(强制访问控制)</primary></indexterm> - - <para>TrustedBSD MAC 框架会以 <varname remap="structname">struct - label</varname> 的形式维护一系列内核对象的数据。 - 一般来说, 内核中的 label (标签) 是由与其对应的内核对象同样的锁保护的。 - 例如, <varname remap="structname">struct vnode</varname> 上的 - <varname remap="structfield">v_label</varname> 标签是由其所在 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> 中找到, - 其用法都非常简单明了。 这个锁保护的主要是 - <varname remap="structname">module_t</varname> (当以共享方式上锁) - 和全局的 <varname remap="structname">modulelist_t</varname> 这两个结构体, - 以及模块。 要更进一步理解这些锁策略, 需要仔细阅读 - <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>, - 否则是一指向描述这一注册的 <varname remap="structname">struct sigio</varname> 的指针。 - 这一字段由一全局 mutex, - <varname>sigio_lock</varname> 保护。 调用 SIGIO 维护函数时, - 必须以 <quote>传引用</quote> 方式传递这一字段, - 以确保本地注册副本的中这个字段不脱离锁的保护。</para> - - <para>每个关联到进程或进程组的注册对象, 都会分配一 - <varname remap="structname">struct sigio</varname> 结构, 并包括指回该对象的指针、 - 属主、 信号信息、 凭据, 以及关于这一注册的一般信息。 - 每个进程或进程组都包含一个已注册 <varname remap="structname">struct sigio</varname> - 结构体的列表, 对进程来说是 - <varname remap="structfield">p_sigiolst</varname>, 而对进程组则是 - <varname remap="structfield">pg_sigiolst</varname>。 这些表由相应的进程或进程组锁保护。 - 除了用以将 - <varname remap="structname">struct sigio</varname> 连接到进程组上的 - <varname remap="structfield">sio_pgsigio</varname> 字段之外, 在 <varname remap="structname">struct - sigio</varname> 中的多数字段在注册过程中都是不变量。 - 一般而言, 开发人员在实现新的支持 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 锁是位于 - <varname remap="structname">struct taskqueue</varname> 结构体上。 - 在此处使用同步原语的目的在于保护 <varname remap="structname">struct - taskqueue</varname> 中数据的完整性。 应注意的是, - 并没有单独的、 帮助用户对其自身的工作进行锁的细化用的宏, - 因为这些锁基本上不会在 - <filename>kern/subr_taskqueue.c</filename> 以外的地方用到。</para> - </sect2> - </sect1> - - <sect1 xml: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 xml: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 xml:id="smp-glossary"> - <title>术语表</title> - - <glossentry xml:id="smp-glossary-atomic"> - <glossterm>原子</glossterm> - <glossdef> - <para>当遵循适当的访问协议时, 如果一操作的效果对其它所有 CPU - 均可见, 则称其为原子操作。 狭义的原子操作是机器直接提供的。 - 就更高的抽象层次而言, 如果结构体的多个成员由一个锁保护, - 则如果对它们的操作都是在上锁后、 解锁前进行的, - 也可以称其为原子操作。</para> - - <glossseealso>操作</glossseealso> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-block"> - <glossterm>阻塞</glossterm> - <glossdef> - <para>线程等待锁、 资源或条件时被阻塞。 - 这一术语也因此被赋予了太多的意涵。</para> - - <glossseealso>休眠</glossseealso> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-critical-section"> - <glossterm>临界区</glossterm> - <glossdef> - <para>不允许发生抢占的代码段。 使用 - &man.critical.enter.9; API 来表示进入和退出临界区。</para> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-MD"> - <glossterm>MD</glossterm> - <glossdef> - <para>表示与机器/平台有关。</para> - - <glossseealso>MI</glossseealso> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-memory-operation"> - <glossterm>内存操作</glossterm> - <glossdef> - <para>内存操作包括读或写内存中的指定位置。</para> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-MI"> - <glossterm>MI</glossterm> - <glossdef> - <para>表示与机器/平台无关。</para> - - <glossseealso>MD</glossseealso> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-operation"> - <glossterm>操作</glossterm> - <glosssee>内存操作</glosssee> - </glossentry> - - <glossentry xml:id="smp-glossary-primary-interrupt-context"> - <glossterm>主中断上下文</glossterm> - <glossdef> - <para>主中断上下文表示当发生中断时所执行的那段代码。 - 这些代码可以直接运行某个中断处理程序, 或调度一异步终端线程, - 以便为给定的中断源执行中断处理程序。</para> - </glossdef> - </glossentry> - - <glossentry> - <glossterm>实时内核线程</glossterm> - <glossdef> - <para>一种高优先级的内核线程。 目前, - 只有中断线程属于实时优先级的内核线程。</para> - - <glossseealso>线程</glossseealso> - </glossdef> - </glossentry> - - <glossentry xml:id="smp-glossary-sleep"> - <glossterm>休眠</glossterm> - <glossdef> - <para>当进程由条件变量或通过 <function>msleep</function> 或 - <function>tsleep&l |