Copyright(C) 2001~2006 Nathan Boeger 和Mana Tominaga 版权所有,简体中文版由ChinaUnix论坛提供翻译。
目录
第一章: FreeBSD的Make
第二章: BSD自举
第三章: 进程和内核服务
第四章: 高级进程控制和信号
第五章: 基本I/O
第六章: 高级I/O
第七章: 进程资源和系统限制
第八章: FreeBSD 5.x
第一章 FreeBSD的make
译者:雨丝风片@chinaunix.net
1.1 FreeBSD的make 作为常用的和基本的Unix软件开发工具,make是一个可以跟踪全部的文件依赖关系的非常好的簿记工具程序。要管理依赖关系这样的项目细节常常需要花费很多的时间,甚至会拖延开发进度。当多个开发人员合作一个项目的时候,依赖关系的跟踪就可能变得相当困难了。事实上,正确地使用make可以帮助我们加快应用程序的开发,从而提高生产效率。
虽然make最初的设计是用来对应用程序版本构建的维护过程进行管理的,我们实际上还可以通过创建一系列的基于目标依赖关系的Unix shell命令来让make完成多种多样的额外工作。这些依赖关系可以用很多种方式定义——包括需要进行编译的源文件、所需的库文件、shell命令以及其它的目标。
make有多种风格的版本,其中包括GNU make和System V make。并不是在每个make版本中都有我们接下来讨论的那些特性,具体使用哪个版本完全取决于你的个人喜好。我们将主要关注跟随FreeBSD一起发布的make(也叫做bmake或pmake),尤其是如何通过它来编译和更新FreeBSD系统,也就是所谓的make world。虽然我们关注的是FreeBSD make,但我们在这里讨论的所有东西对于各种BSD版本来说都是适用的。
我们首先会讲述一个Makefile的基本文件布局和语法。如果这对于你来说太简单了,那你可以直接跳到本章结束处的示例部分去阅读。(注意,我们给出的代码示例只用于演示我们关于make目标和依赖关系的讨论,它们并不一定是可以运行的代码。)
当然,和其它工具程序一样,最开始应该先去看看man page,以对make提供的命令行选项的概要和细节有一个正式的了解。同时,和其它工具程序一样,学习make的最好方法就是使用它。创建一些小型的源文件(可以使用任何语言),然后尝试一些下面给出的例子。我们希望读完本章之后你除了理解make的语法规则之外,还知道它是如何工作的。
1.2 Makefile布局总的说来,你使用make的方式就是让它去读一个Makefile,你需要在Makefile里指定一个目标及其依赖关系。在运行的时候,make会按顺序搜索名字为Makefile或makefile的文件。这个Makefile通常是放在一个工程的根目录下的,如果想指定其它的Makefile,可以在命令行上用-f (filename)的选项给出。
make -f OtherMakefile
1.3 语法一个Makefile的结构由四个基本行组成,它们都可以通过在行尾添加‘\’字符来扩展到下一行(和shell编程相似)。注释是以‘#’号开始的,至行尾结束。
########################################
# Simple Makefile with comment example #
########################################
# when run, it will just echo hello
all:
echo "hello"
要使用make来编译一个工程,首先需要确定在你的当前工作目录中已有一个正确的Makefile,然后再通过下列命令之一来使用make:
bash$ make
bash$ make all
bash$ make <target name>
1.4 目标用来指定目标的方式有很多种,不过最常用的就是用目标文件或一个工程的名字。工程名字不应当包含有空格或标点符号,不过这只是个惯例而已;少量的空格和标点符号也是允许的。这个名字必须写在一个新行的开头,必须以单冒号(:)、双冒号(::)或感叹号(!)三者之一结束。
myprog:
<some commands to the compile myprog target>
another::
<some commands to the compile another target>
sample!
<some commands to the compile sample target>
在这些目标名字之后是所需的依赖条件,包括名字、变量以及其它的目标等等。如果你的依赖条件太多的话,可以用一个‘\’和一个newline来将它们分开。所有的依赖条件都必须Makefile内定义或者存在于某个外部文件中,否则make将无法知道如何去完成依赖操作。
一些示例如下:
all: driver.cpp network_class.cpp file_io_class.cpp network_libs.cpp file_io_libs.cpp
all: myprog.o
myprog.o:
上例中,all和myprog.o是要make的目标。注意myprog.o既是一个目标又是一个依赖条件。make首先会到myprog.o那儿,执行它的命令,然后返回到all那儿,再执行它的命令。这种操作序列是make的功能基础。
按照惯例,all:目标是你的目标中的最高者,这意味着make将从这儿开始去寻找要完成all:目标都需要哪些东西。不过all:目标并不是必需的,如果没有的话,make就会简单地选择所有列出的目标中的第一个,只对其实施操作,除非你在命令行上指定了某个目标。对于那些有一个核心的应用程序需要维护和构建的工程来说,我们建议你使用all:目标;这是一个通用的惯例,有助于避免错误和不必要的任务。
上例所示的依赖序列只是很简单的一个。下面是一个更为复杂和灵活的依赖序列,我们没有给出用于具体目标的命令:
all: myprog.o lib
lib: lex
lex:
myprog.o: app.h
注意,在这个例子中,all:目标有两个依赖条件:myprog.o和lib。这两个依赖条件本身又都是目标,make将首先去编译myprog.o。在make编译myprog.o的时候,会发现有一个和app.h的依赖关系。app.h并没有在这个makefile里定义,但app.h却是一个在当前目录中的头文件。
用于myprog.o的命令完成之后,make即返回到all:处,继续处理下一个依赖条件,在此例中是lib。依赖条件lib本身也有一个依赖条件lex,所以make在完成lib之前会先去完成lex:。
注意:正如你所看到的,这些依赖关系可能会非常长,或者嵌套得很深。如果你有一个很大的Makefile,那一定要好好地组织一下,把目标的顺序弄好。
1.5 求值规则依赖关系是按照依赖于目标名字结束符号的严格规则来求值的。一旦make认为满足规则,它将通过执行相应的命令来创建特定的目标(比如编译该目标)。例如,使用单冒号:可以让你对需要进行编译的目标进行更为精细的控制。也就是说,你可以指定某个特定的目标文件每次都需要重新编译或者仅当它的源文件过时之后才编译。这些规则都是基于目标名字的结束符号的,如下:
如果目标名字以单冒号(:)结束,它将根据以下两个规则来创建:
- 如果目标尚未存在,就像我们在上面举的例子里的all:一样,make就会创建它。
- 如果任意一个源文件具有比当前目标更新的时间戳。在上例中如果app.h或myprog.c具有更新的时间戳,myprog.o就会被make。这种情况只需简单地用一下touch命令即可出现
touch myprog.c
如果目标名字以双冒号(::)结束,它将根据以下三个规则来创建:
- 如果任意一个源文件具有比当前目标更新地时间戳。
- 该目标不存在。
- 该目标没有与之关联的源文件。
如果目标名字以感叹号(!)结束,只要make把它所需的全部依赖条件都创建完毕就会来创建它。
你只能在目标或源文件的最后一个组成部分中使用通配表达式?、*和[],而且只能用于描述已经存在的文件。比如:
myprog.[oc]
而使用花括号{}的表达式则不一定非得描述已经存在的文件。比如:
{mypgog,test}.o
# the expression above would match myprog.o test.o only
最后需要注意一点:可变表达式是按照目录顺序来处理的,而非字母顺序,就跟在shell表达式中一样。例如,如果你的目标有某些基于字母顺序的依赖条件,下面这个表达式可能就不对了:
{dprog,aprog,bprog,cprog}.cpp
1.6 变量make能够使用变量这一点是非常重要的。例如,你有一个名字为a.c的源文件,由于某种原因你想把它的名字改成b.c。通常情况下,你得把你的makefile里的每个a.c的实例都改成b.c。但是,如果你写成以下方式:
MYSRC = a.c
你只需要把这一行更新成新的名字即可,如下:
MYSRC = b.c
你因此而节省了时间,这也正是make的首要角色:项目管理。
变量可用$(<变量名字>)或就用一个$来引用,但后者未被广泛使用,因此建议别那样写。
$(GCC) = /usr/local/bin/gcc
make有四种不同类型的变量,下面将按照被搜索的顺序列出它们。(make的搜索将一直进行到发现某个数值的第一个实例为止。)
- 局部变量:这些是赋给特定目标的数值。
- 命令行:命令行变量是在命令行上传给make的。
- 全局变量:全局变量是在该Makefile或任何所包含的Makefile内赋值的。你在一个Makefile内最常看到的就是这些变量。
- 环境变量:环境变量是在Makefile之外,也就是运行make的shell里设置的。
这些变量可以在Makefile里用以下五种操作符进行定义:
- 等号“=”是最常用的操作符,这和shell的情况是类似的。数值被直接赋给了变量。例如:
VAR = < value >
- 加号等号“+=”的意思是附加赋值,通过把所赋数值附加到当前数值的后面来完成对变量的赋值。例如:
VAR += < value to append >
- 问号等号“?=”的意思是条件赋值,仅当该变量从未赋值时才进行赋值。条件赋值在向一个字符串数值添加前缀的时候非常有用。例如:
VAR ?= < value if unset >
- 冒号等号“:=”的意思是扩展赋值,在赋值前会对所赋数值进行扩展;通常这种扩展是在所赋变量被引用的时候才进行的。例如:
VAR := < value to expand >
- 感叹号等号“!=”的意思是shell命令赋值。在命令被扩展并发给shell执行完毕之后,将命令结果赋给变量。结果中的newline字符将被空格取代。例如:
VAR != < shell command to execute >
注意:有些变量是在外部的系统级的Makefile内定义的(在/etc/make.conf或/etc/defaults/make.conf中),如果你在设置环境变量方面遇到了问题,就去检查一下系统级文件里的设置。
一个完整的例子如下:
#####################
# Example make file #
#####################
CURDIR != pwd
CFLAGS ?= -g
CFLAGS += -Wall -O2
all:
echo $CFLAGS
#######################
bash$ CFLAGS="-g -Wall" make
在上例中,CURDIR被设置成了shell命令pwd的结果。(注意,这种赋值并不需要backtick命令(即' ')。)CFLAGS如果从未设置,会首先被设置成-g,然后不管它的当前值是什么,都会被附加上-Wall -O2。
1.7 命令如果没有命令,那make什么都不是,只有把命令告诉make它才能完成它的工作。make只能运行这些命令,然后基于shell的退出状态判断这些命令是否成功。因此,如果命令失败,shell返回一个错误,make将会因错误而退出并于该处终止。make可以就被看作是另外一个命令——除了运行其它命令之外,它和那些命令并没有什么实际的交互。
命令必须与一个目标相关联,任何一个目标都可以有多个命令。例如:
# example for an all-install target
all-install:
$(CC) $(CFLAGS) $(MYSRC)
cp $(MYPROG) $(INSTALL_DIR)
echo "Finished the build and install"
每个命令都必须在一个目标之后以新行开始,在实际命令起始位置之前必须要有一个tab键,如上所示。
对于大多数情况而言,Makefile里的命令只要是个有效的shell命令就行,命令还经常会包括变量。例如:
CPP = -g++
CFLAGS = -Wall -O2
myprog.o:
$(CPP) $(CFLAGS) -c myprog.c
下面这个例子告诉make使用给定值编译myprog.c。这些命令可能会比一行要长,它们也可以被写来完成其它的任务。这是非常重要的,因为编译器可以接受相当多的命令行选项、环境设置以及定义等等,比如:
CLFAGS = $(LINK_FLAGS) $(LINK_LIBS) $(OTHER_LIBS) \
$(OPTIMIZER_FLAGS) $(DEFINES) $(NO_KERNEL) $(OBJS) \
$(CPU_TYPE_FLAGS) $(USE_MY_MALLOC) $(UDEFINES) \
$(SRC_FILE) $(OTHER_SRC_FILES)
下面这个例子告诉make删除所有的object文件、core文件以及应用程序本身,然后把log文件移走,这些操作都可以很方便的完成。
CPP = -g++
CFLAGS = -Wall -O2
APP = myapp
DATE != date +%m%d%y_%H_%M
LOG = debug
myprog.o:
$(CPP) $(CFLAGS) -c myprog.c
rm -f *.o *.core $(APP)
mv $(LOG).log $(LOG)_$(DATE).log
clean:
rm -f *.o *.core $(APP)
mv $(LOG).log $(LOG)_$(DATE).log
但是,如果并不存在log文件,make就会因错误而退出。为了避免这种情况,可以在命令之前加上“-”号。通过在命令前面加上减号,你就告诉了make忽略执行命令时遇到的错误。(不过make仍然会打印出错误信息。)因此,在某个命令出现错误之后make仍将继续。例如:
clean:
-rm -f *.o *.core $(APP)
-mv $(LOG).log $(LOG)_$(DATE).log
这将使得make忽略掉rm和mv命令可能遇到的错误。
你还可以让make禁止掉“echo”之类的命令的输出。echo首先告诉make打印出包括echo语句在内的整个命令,然后再执行命令并将字符串打印到屏幕上:
echo $(SOME_DEFINED_STRING)
要避免这种情况,可以在echo命令之前加上“@”符号,这将告诉make只打印字符串,比如:
@echo $(SOME_DEFINED_STRING)
“-”号和“@”号都既可用于变量,又可用于形为字符串的命令,但需要确定你对变量命令的引用是正确的。下例演示了如何对命令使用@操作符:
ECHO = echo
MESSAGE = "Print this message"
msg::
@$(ECHO) $(MESSAGE)
1.8 条件语句(#if,#ifndef等等)如果你对C和C++比较熟悉,那你肯定知道条件预处理命令。功能繁多的make也有一个类似的特性。条件语句使你可以选择Makefile里的哪个部分需要被处理。这些条件语句最多可以嵌套30层,并且可以放在Makefile里的任何地方。每条语句都必须以一个圆点(.)开始,而且条件语句块必须以.endif结束。
条件语句允许使用逻辑操作符,比如逻辑AND“&&”、逻辑OR“||”,整条语句还可以用“!”操作符取反。“!”操作符具有最高优先级,其后依次是逻辑AND和逻辑OR。括号可以被用来指定优先级顺序。关系运算符也是可以使用的,比如“>”、“>=”、“<”、“<=”、“==”和“!=”。这些操作符可被用于十进制和十六进制的数值。对于字符串则可以使用“==”和“!=”操作符。如果没有给出操作符,那么将会把数值和0进行比较。
在下面的例子中,在对VER变量进行赋值之后对条件进行测试。注意,如果VER变量未被赋值,则最后的.else条目将被求值为真,TAG将被赋成2.4_stable的值。
.if $(VER) >= 2.4
TAG = 2.4_current
.elif $(VER) == 2.3
TAG = 2.3_release
.else
TAG = 2.4_stable
.endif
条件语句可用于测试变量,也可以用于下面这种函数风格的表达式。
这些用法中有些是有简写形式的。出于兼容性方面的考虑,我们列出了简写形式。非简写形式意思更为确定,也更容易被理解,但需要你敲入更多的字符。
在使用简写形式的时候是不必使用括号的。此外,简写形式还可以和if/else语句以及其它的简写形式混合在一起:
make( < arg > ) short hand [ .ifmake, .ifnmake, .elifmake, .elifnmake ] 在上例中,make将以一个目标名字作为它的参数。如果这个目标已在命令行上给出,或者它就是缺省进行make的目标,那么该值就为真。下例中将根据make()表达式的规则给CFLAGS赋值:
.if make(debug)
CFLAGS += -g
.elif make(production)
CFLAGS += -O2
.endif
下面是用简写形式表示的相同代码:
.ifmake debug
CFLAGS += -g
.elifmake production
CFLAGS += -O2
.endif
target( < arg > )这种形式将以一个目标名字作为参数。仅当该target已被定义时该值才为真。这个表达式没有简写形式。例如:
.if target(debug)
FILES += $(DEBUG_FILES)
.endif
在上例中,如果debug目标返回真的话就会对FILES变量进行附加操作。
empty ( < arg > )这种形式以一个变量为参数,并允许使用修饰符。当变量被扩展之后是一个空字符串时该值为真。这个表达式没有简写形式。此外需要注意的是,你在使用这个表达式的时候并不需要对数值进行引用,记住是VAR而不是$(VAR)。例如:
.if empty (CFLAGS)
CFLAGS = -Wall -g
.endif
defined( < arg > ) short hand [ .ifdef , .ifndef , .elifdef, elifndef ]下面这个例子采用一个变量作为参数。仅当变量已被定义时该值为真。
.if defined(OS_VER)
.if $(OS_VER) == 4.4
DIRS += /usr/local/4.4-STABLE_src
.endif
.else
DIRS += /usr/src
.endif
下面是简写形式:
.ifdef OS_VER
. if $(OS_VER) == 4.4
DIRS += /usr/local/4.4-STABLE_src
. endif
.else
OS_VER = 3.2
DIRS += /usr/src
.endif
正如你所看到的,make允许嵌套的条件和define表达式。和C不同的是,你不能对if语句和变量赋值语句进行缩进。如果想让你的条件语句块更清晰一点的话,你可以在圆点之后、if之前加一些空格。示例如下:
.if $(DEBUG) == 1
$(CFLAGS) = -g
. ifndef $(DEBUG_FLAGS)
$(FLAGS) = $(DEBUG_FLAGS)
. endif
.endif
exists( < arg > )下面的例子演示了如何使用exists,以及如何给一个目标添加条件语句。如果存在tmp目录,make将运行-rm tmp/*.o命令。正如你在这个例子中看到的,.if语句仅在clean目标中被求值;所运行的命令必须遵从常规的命令语法。
clean:
-rm -f *.o *.core
.if exists(tmp)
-rm tmp/*.o
.endif
1.9 系统Makefiles,模板以及.include指令C的一个重要特性就是它的简单明了的预处理指令,也就我们常说的#include。这个特性也在make中实现了。所不同的是,如果你想包含另外一个Makefile,那你得在文件末尾包含它,而不是像C那样在文件头包含,这是因为变量是按顺序进行赋值的。Makefile中又有Makefile可能会造成混淆,不过包含一个Makefile的基本语法却是很简单的。注意,在include单词前面必须要加一个圆点(.)。基本语法如下:
.include file name
如果要指定一个位于系统make目录中的Makefile,可以使用尖括号:
.include <file name>
如果所指定的文件位于当前目录或者是由-I命令行选项指定的目录中,则使用双引号,这和C的#include是类似的:
.include "file name"
在下面的例子中,如果已在project.mk中定义了CFLAGS变量,它将被这里的新声明所取代。当包含多个Makefile时这就可能造成一些麻烦。
.include "../project.mk"
CFLAGS = -g
FreeBSD系统有很多的系统Makefile可供包含,与之对应的是完成各种任务的例程。在大多数的FreeBSD系统中,这些Makefile位于/usr/share/mk/目录里。除此之外,还有一个/etc/defaults/make.conf,它是可以被/etc/make.conf取代的。(细节请参见man make.conf。)
下面是一个常用的Makefile的简单列表。如果你正准备进行大量的内核编程或应用程序移植,那就好好利用Makefile吧。这些Makefile的形式为bsd.<type>.mk,其中<type>表示这些Makefile的用途。
- bsd.dep.mk:这是一个用来处理Makefile依赖关系的非常有用的包含文件。
- bsd.port.mk:这是一个在构建应用程序的ports的时候使用的包含文件。
使用.include指令的好处就是你可以把你的工程的Makefile分成很多的片断。比如,你的工程可以有一个主Makefile,由所有其它的子Makefile进行包含。这个主Makefile可以包含编译器变量及其所需的选项。这样一来,就不必在每个Makefile里都指定编译器和所需选项了,对编译器名字的引用也因此而得到了简化。这些公共使用的片断之后还可以用于其它的Makefile,对编译程序等例程的修改也将随之在所有的Makefile中生效。
1.10 高级选项高级的make选项主要为了增加灵活性。我们建议你仔细阅读一下make的man page,以便对make有一个深入的了解。下面这些选项是我们最常使用的。
局部变量make可以使用专门定义的局部变量,其范围仅限于当前目标。下面列出了七个这样的局部变量,同时还给出了它们的System V兼容的老的表示形式。(不建议使用System V的老的表示形式,之所以列出它们,只是出于向后兼容的考虑。)
这个变量就是目标的名字:
.TARGET
old style notation: '@'
这个变量包含了当前目标的全部源文件的列表:
.ALLSRC
old style notation: '>'
这个变量是当前目标所隐含的源文件。它的数值是这个目标的源文件的名字和路径。(示例见下面的.SUFFIX部分。)
.IMPSRC
old style notation: '<'
这个变量保存的是已经被确定为过期的源文件的列表:
.OODATE
old style notation: '?'
这个变量的值是不包括后缀和路径的文件名:
.PREFIX
old style notation: '*'
这个变量的值是档案文件的名字:
.ARCHIVE
old style notation: '!'
这个变量的值是档案成员的名字:
.MEMBER
old style notation: '%'
当在依赖关系行中使用这些局部变量的时候,只有.TARGET、.PREFIX、.ARCHIVE和.MEMBER具有该目标的值。
另外还有一个非常不错的指令:
.undef <variable>
这会让你非常方便地取消对一个变量的定义。注意,只有全局变量才能取消定义。例如:
.ifdef DEBUG
.undef RELEASE
.endif
1.11 转换规则(后缀规则)转换规则规定了如何去创建一个目标。你可以利用这些规则,省下为每个目标文件编写规则的时间。语法很简单:.SUFFIXES: (suffix list)
注意,可能有多个不同的后缀共享相同的转换后缀(.cpp、.cc、.c都可以转换成a.o)。如果没有列出后缀,make将把之前的所有后缀都删除。这可能会在你有多个.SUFFIXES规则的时候造成很大的混乱。我们建议你只使用一个规则,并把它放在Makefile的底部。把.SUFFIXES语句块中列出的东西和target进行比较,你就容易理解它的结构了。
.SUFFIXES: .cpp .c .o .sh
.c.o:
$(CC) $(CFLAGS) -c ${.IMPSRC}
.cpp.o:
$(CPP) $(CXXFLAGS) -c ${.IMPSRC}
.sh:
cp ${.IMPSRC} ${.TARGET}
chmod +x ${.TARGET}
上面给出的这些规则将会编译C和C++源文件。不过对于.sh:规则而言,它也可以告诉make去创建相应的shell脚本。注意,如果想在这个例子中列出某个shell脚本作为目标,那就不能添加.sh扩展名,不过那个shell脚本本身却必须是带有.sh后缀的。
如果我们把不带.sh后缀的install_script列出来作为一个依赖条件,那就必须存在一个带有.sh后缀的shell脚本,名字为install_script.sh。也就是说,如果某个文件可以被列出来作为一个目标,那么在这个文件的存在期间,仅当make认为那个目标和该文件比起来过了期之后才会去创建它,make并不会去创建这个文件。示例见下;更多的信息请参见apps.h的例子:
all: install_script $(OBJS)
$(CPP) $(CFLAGS) -o $(APP) $(OBJS) $(LINK)
1.12 有用的命令行选项下面给出了一些非常容易学习和使用的命令行选项。这并不是一个完整的列表,要想知道其它的选项还得去看man page。
-D <variable name to define> 这个选项将在命令行上定义一个变量,如果你的Makefile里面有.ifdef语句,那么使用这个选项就非常方便了。例如:
.ifdef DEBUG
CFLAGS += -g -D__DEBUG__
.endif
于是,当你运行命令make -D DEBUG的时候,make就会正确的设置CFLAGS,在编译你的应用程序的时候也把调试语句编进去。
-E < variable name to override > 这个选项将用环境变量的值取代Makefile中给变量赋的值。在使用这个选项之前,需要先设置你的shell中的环境变量。例如:
bash $ CFLAGS="-O2 -Wall" make -E CFLAGS
-e和大写形式类似,-e将用环境变量取代Makefile中的所有变量。如果某个变量没有定义相应的环境变量,则按正常方式赋值。
-f <makefile to use>这个选项使你可以在命令行上指定Makefile,这一点在你需要多个Makefile时很有用。例如:
bash$ make -f Makefile.BSD
-j < number of max_jobs > 这个选项使你可以指定make能够派生出多少个job来。一般来说make只会派生出一个,但对于一个非常大的工程而言,如果想让你的硬件物尽其用的话,那就指定4个吧,如下:
make -j 4 world
如果超过了4个,有时反而会延长执行时间,不过有些人倒是可能觉得CPU被六个或更多的job折腾的样子很有趣。
-n这个选项对于Makefile的调试很有用,它可以让你看到make究竟会执行哪些命令,但又不会实际去执行它们。对于有很多命令的大型工程,最好是把输出重定向到一个外部文件中,否则就会有浩如烟海的命令显示出来。示例如下:
bash $ make -n >> make_debug.log 2>&1
-V < variable name >这个选项将基于全局的上下文打印变量的值。与前一个选项类似,make也不会去构建任何目标。你可以在命令行上指定多个-V选项,如下:
make -V CFLAGS -V LINK -V OBJS
1.13 一个最后的例子下面列出的Makefile是一个可以重复使用的Makefile的例子。在你包含了它之后,它就知道从列出的.SUFFIXES规则中得知如何去编译C++源文件。它还知道如何去安装应用程序和清除开发目录。这显然并不是一个很好理解的Makefile,但它却是一个很好的创建通用模板风格的Makefile的范例,这种Makefile包含了那些用于开发的公共例程。这不只是节省了在创建每个Makefile时重复输入这些公共规则的时间,它还能让开发人员重复使用已知的好例程。
########################################################
#
# FILE: Makefile
#
# AUTHOR: Nathan Boeger
#
# NOTES:
# This is a generic Makefile for *BSD make, you will
# need to customize the listed variables below inside
# the Makefile for your application.
#
# INSTALL_DIR = name of the directory that you want to install
# this applicaion (Ex: /usr/local/bin/ )
#
# APP = name of the application
#
# C_SRC = C source files (Ex: pstat.c )
#
# CPP_SRC = CPP source files (Ex: node.cpp)
#
#
# $Id: ch01.html,v 1.5 2004/08/10 14:41:39 nathan Exp $
#########################################################
# Make the OBJ's from our defined C & C++ files
.ifdef CPP_SRC
OBJS = ${CPP_SRC:.cpp=.o}
.endif
.ifdef C_SRC
OBJS += ${C_SRC:.c=.o}
.endif
# define the Compiler. The compiler flags will be appended to
# if defined, else they are just assigned the values below
CPP = g++
CFLAGS += -Wall -Wmissing-prototypes -O
LINK += -lc
# Add a debug flag.
.ifdef DEBUG
CFLAGS += -g
.endif
# Targets
all: ${OBJS}
$(CPP) $(CFLAGS) -o $(APP) ${OBJS} $(LINK)
depend:
$(CPP) -E -MM ${C_SRC} ${CPP_SRC} > .depend
#######################################################
#
# INSTALL SECTION
#
# install will copy the defined application (APP) into the
# directory INSTALL_DIR and chmod it 0755
# for more information on install read MAN(1) install
########################################################
install: all
install -b -s $(APP) $(INSTALL_DIR)
clean
rm -f $(APP) *.o *.core
# SUFFIX RULES
.SUFFIXES: .cpp .c .o
.c.o:
$(CPP) $(CFLAGS) -c ${.IMPSRC}
.cpp.o:
$(CPP) $(CFLAGS) -c ${.IMPSRC}
下面给出的Makefile是需要你在工程目录内部创建的:
#######################################################
# PROJECT Makefile
#
# This is what the programs makefile would look like
# These are the only variables you will need to define
######################################################
APP = myapp
C_SRC = debug_logger.c
CPP_SRC = myapp.cpp base_classes.cpp
INSTALL_DIR = /usr/local/bin/
# And include the template Makefile, make sure its
# path is correct.
.include "../../bsd_make.mk"
第二章 自举BSD
翻译(meilincore@chinaunix)
2.1 自举BSD自举:在很少或者没有协助的情况下主动,努力的提升和发展(把她自己自举到顶点)
Bootstrap : to promote or develop by initiative and effort with little or no assistance <bootstrapped herself to the top> www.m-w.com
自举计算机是指加载操作系统的过程,该过程为:初始化硬件,读取一小部分代码到内存并执行.这一点代码接着加载一个大的操作系统.一旦操作系统被加载,它就需要创建自己的整个环境.这个过程,称做自举计算机,是一个复杂的,高度平台相关的过程.
本章我们将从细节上探索FreeBSD在i386平台上的自举过程.相关的概念和过程与NetBSD和OpenBSD在i386平台上的自举程序相类似.注意一些汇编代码对于从实际上完成i386基本系统的启动任务是必须的.然而,我们不会从细节上回顾汇编代码而是主要集中在高层概念上,因此就算你不是专家讨论也是有意义的.
注意:虽然本章中讨论的一些概念,特别是"实模式"和"保护模式",并不存在与现代象PPC或Alpha这些硬件构架当中.但是i386 BSD基本系统是目前为止最为广泛的系统并会继续如此(一个值得注意的例外是Mac OS X),而且适应很多情况.如果你对启动系统细节有兴趣,你可能需要自定义内核,自定义文件系统,设备驱动.同时.i386构架还广泛应用与嵌入式系统.给定一个安装平台(Given the install base),i386平台及其成果将继续在未来一些年里适用.甚至新的64位CPU到目前我们所知道的为止还是一样的启动过程.
2.2 FreeBSD的自举过程FreeBSD使用一种三段启动过程,当你打开计算机电源或者重新启动.一旦BIOS完成系统检测.它将从DISK0加载第一个轨道到内存中.(每一个过程使用512字节的程序.刚好占用硬盘的一个块)第一轨道就是我们熟知的主引导记录(MBR),也就是boot0,第一个被计算机加载执行的程序.第二个程序,boot1,又是固定大小为512字节并知道如何读取slice信息和加载boot2.一旦boot2被加载它就有能力直接启动系统或者加载加载器(loader program),加载器固定大小为512字节.相当精致并被设计来允许对系统精确启动作更多的控制.
boot0从BIOS加载的第一个程序,boot0,是一个固定大小为512字节的小程序,位于主引导记录(MBR).你可以在/usr/src/sys/boot/i386/boot找到该程序的源代码.当然现代计算机的BIOS可以设置成从各种不同的驱动器包括光驱,软驱和IDE硬盘启动.对于本章来说,我们将假设计算机是从第一个硬盘启动的.也就是磁盘驱动器0,C:,或者,对BIOS而言,0X80.
从第一个磁盘的第一个扇区,512字节被读到内存位置0X7C00.然后,BIOS会检验内存位置0X7DFE的数字0XAA55(启动块代码的最后两个字节).这个位置索引数字0XAA55对i386是如此重要而被赋予了一个适当的名字----魔数.这意味着,只有在这个数字存在于内存位置0X7DFE的情况下BIOS才会把控制权转移给boot0被安置的内存位置0X7C00.
这提出了在Intel i386系统上编写启动代码的一个要点:记着你代码中的第一个内存位置处(0X0)必须是一条指令.并且,当BIOS转移控制权给内存位置0X7C00的时候该位置必须包含一条指令.这可以是一个简单的跳转到其他位置或者启动程序主过程的入口.另外当启动代码被执行时你对CPU到底在干什么是完全没有控制的.因为这时候寄存器的状态也是未知的,你别指望已经设置好了适当的段和栈寄存器.这个小工作必须由启动代码来完成.因为目前操作系统还没有加载起来;所有的I/O必须通过BIOS过程来完成(Intel CPU 文档有一个完整列表)
在boot0被加载并获得控制权以后,它将设置自己的寄存器和栈信息.接着,boot0重定位自己到一个低的内存位置并跳转到它主过程的新的偏移地址.这时候boot0必须还能够定位和启动其他磁盘和分区,在它的最后,boot0代码有一个小的常见的可启动类型列表,它们都必须在其最后两个字节包含魔数才可启动.
最终,当boot0完成搜索可启动磁盘和分区以后,将提示用户作一个选择.如果在一个短的时间段里面没有任何选择或者某个键被按下,boot0将加载下一个启动块到内存并转移控制权给它.还是那样,这可以是任何操作系统的启动代码----你可以设置它加载Linux的自举代码或者甚至DOS.对BSD而言,下阶段的启动程序是boot1.
boot1与boot0相似,boot1是一个非常简单且总共才512字节的程序;它也必须在最后两个字节包含魔数.其目的是定位和读取FreeBSD磁盘分区,接着定位和加载boot2.
虽然在大多数情况下,boot1是被boot0加载的,但是这个顺序不必是唯一存在的选项.由于FreeBSD的灵活性你可以使用被称为专用磁盘的选项(说的就是那个更为声名狼籍的危险的专用磁盘)一个专用磁盘就是整个磁盘,或者BIOS的每一个扇区都属于FreeBSD.通常,你会在PC机磁盘上发现一个fdisk表,或者一个slice表,用来允许多个操作系统从单个PC机磁盘上启动.你可以选择使用一个专用磁盘并且直接从boot1启动;boot0完全没有必要安装在磁盘上.不管你采用那种实现,boot1都是非常重要的启动块并需要被加载
Boot1被加载到内存位置0X7C00并操作在实模式;环境没有设置,寄存器也在未知状态.boot1必须设置好栈,定义所有的寄存器,并使用BIOS接口来做所有的I/O.一旦boot1被加载到内存并且控制权已经转移过来.它必须在自己的第一个内存位置(0X0)包含一条指令.所有这些都成功以后,boot1将读取系统磁盘搜索boot2.
一旦boot2被加载.程序必须设置好boot2的环境;boot2是一个BTX客户端(boot Extender 启动扩展器)并且比前面的boot0和boot1稍微复杂一些.boot1需要加载boot2到内存位置0X9000并且它的入口是0X9010.然后,甚至在boot1加载并转移控制权给boot2以后,还有一个被boot2使用的过程存在于boot1.如果你阅读boot1的源代码你会注意到一个函数呼叫xread.这个函数用来通过BIOS从磁盘读取数据.因此,boot1在内存中的位置是非常重要的并且boot2必须知道它的位置才能正常工作.
boot2目前为止我们已经加载了两个启动块和一个大的程序到内存中.转移了两次控制权且每次都重新设置了一个小的环境(栈,段寄存器等等...),并通过BIOS执行了一些有限的I/O.我们依然没有触及到加载操作系统的点子上来.如果你看过FreeBSD启动过程中的屏幕,到目前为止你可能看见F1和可爱的ASCII螺旋线.你可能印象当中不会觉得这都是,但他的却是原原本本,精确原生的汇编代码让这个启动过程看起来如此幽雅而轻巧
现在到最后一个自举过程了,boot2.最后阶段很简单并且可以发生两件事中的一件:boot2加载加载器(我们将在下一节讨论这个)或者,boot2加载内核并直接启动而完全不使用加载器.如果你曾经打断boot2的加载过程,你可能已经看过这个boot2输出到屏幕的东西:
>>FreeBSD/i386 BOOT
Defualt: 0:ad(0,a)/boot/loader
boot:
如果你按下回车,boot2会简单的加载默认加载器,象列出的那样.当然,如果你只是键入"boot kernel"接着它就会加载内核(/kernel)并启动.你可以,当然,改变这些默认值.如果你想找到更多信息请阅读boot( 8 )的文档
早先我们提到过boot2是一个BTX客户端(Boot Extender 启动扩展器).这留下什么呢?BTX提供者是一个基础的虚拟86寻址环境.该来讨论一下Intel硬件内存寻址的历史了.
目前为止我们都避免提及内存寻址方式.这可能会比较混淆因为Intel CPU要忍受历史问题,并且启动代码设计通常是留给那些非写不可的开发人员.除非你要移植一个系统到新的构架因而你的代码必须完全是平台依赖的,通常,一个程序员永远也不会被派去写一个启动加载器.然而启动过程对于需要编写设备驱动代码或者内核相关编程的开发人员来说是非常重要的.这就是一些开发人员会在这里碰到BTX加载器.
大约从8088到80186,Intel处理器只有一种方式进行内存寻址,叫做实模式.这些早期的CPU有巨大的16位寄存器和20位内存地址.然后问题来了,你如何在16位的寄存器中构成一个20位的地址呢?答案就是,用两个16位寄存器,一个提供基地址而另一个提供这个基地址的偏移量.基地址寄存器被左移4位这样当两个合并的时候一个令人惊异的20位地址就被算出来了.用所有这些灵巧的段寄存器和位移,早期的Intel处理器可以寻址总共1M空间.今天这甚至还不够容纳一个Word文档,作为一个傲慢的例子.
一旦80386席卷而来,寻址1M就不够了;用户需要更多的内存并且程序开始使用更多的内存.一种新的寻址模式叫做保护模式被发明了出来.新的保护模式允许寻址高达4G内存.
新方式的另一个好处就是对于汇编程序员来说更容易实现.主要的区别就是你的扩展寄存器(它们还是那些16为寄存器不过386现在是32位了)可以包含一个完整的32位地址.甚至你以前的段寄存器现在都被保护了.程序不能写入或者读取他们.这些段寄存器现在被用来定位你的内存中的真实地址,这个过程包括权限验校位(读写等等...)并引入了MMU(memory management unit内存管理单元)
现在回到BTX客户端的问题.我们使用BTX程序有什么好处呢?很简单:灵活性.BTX提供了足够的服务因此一个小的,拥有漂亮接口的程序可以被写出并能在加载内核的过程中非常灵活.在FreeBSD系统中这就是加载器.从下一节有将看到加载器真的是多么的漂亮和灵活.因此本节余下的部分我们将涵盖基本的BTX服务.
BTX服务可以被归类到两个基本组.第一组是由直接函数呼叫(类似于系统呼叫)提供的系统服务.另外一组是不由客户端直接呼叫的环境服务.这些服务类似于一个操作系统,然而BTX程序是象单任务环境那样操作的.
直接呼叫提供的BTX服务由两个系统呼叫组成.exit 和 exec.当exit被呼叫的时候BTX加载器结束并且系统重启.最后的一个系统呼叫exec将转移控制权给提供的地址.在个控制权移交是在超级用户模式下完成,并且新的程序可以离开被保护的CPU模式
BTX加载器提供的环境服务是非常基础的.BTX加载器处理所有的中断.这些中断被送到合适的BIOS处理器.BTX也模拟BIOS扩展内存传输呼叫.并且最后还模拟一些汇编指令,它们是pushf,popf,cli,sti.iret和hlt.
最后注意:所有编写来运行在BTX环境中的程序都必须被编译并连接到btxld.更多信息请阅读BTX加载器联机手册
2.3加载器最后的启动阶段是由加载器组成的.加载器程序是一个标准命令(引用为"内建命令")和一个Forth解释器的联合(基于ficl).加载器将允许用户同系统启动进行交互或者允许系统恢复和维护.通过加载器用户可以选择载入或者卸载内核模块.用户也可以设置或者取消设置相关变量,比如rootdevice(根设备)和module_path(模块路径).这些也可以在/boot/loader.conf里面改变.加载器读取的默认文件在/boot/defaults/loader.conf.默认文件也包含了许多可用的选项.这写文件都被构造得和/etc/rc.conf类似.
加载器对于内核和设备驱动除错是非常有用的.通过加载器你可以通知内核在除错器起用的状态下启动.或者,你可以为设备驱动测试加载相关的内核模块.如果你准备编写任何内核模块或者设备驱动的话你最好是阅读加载器的所有文档.首先从联机手册开始,然后复习/boot/loader.conf中的所有选项.一路上加载器在你需要扩展BSD或者诊断内核崩溃的时候将非常有用.
2.4开始内核服务我们终于到了内核已经被载入内存,CPU的控制权的也被传给它的阶段了.一旦内核被加载,它需要从初始化开始运行并准备系统进入多任务模式.该初始化包括三个主要组成部分.前两个是机器相关的,由c和汇编混合编写成.前两个阶段准备系统和初始化CPU内存管理单元(MMU,如果它存在)并处理硬件初始化.最后一个阶段继续建立基本内核环境并让系统准备好运行进程1(/sbin/init).前两个阶段是高度平台依赖的.因为每一种构架都有相关需要.我们将提供一个对这两个阶段的高层次浏览并在本书稍后当我们涵盖到设备驱动的时候从细节上来了解这些概念.
阶段 1 & 2 内核装配和c起始过程虽然内核一旦被加载就不再对系统做任何假设,加载器还是会传送一些信息给内核,比如启动设备和启动标记.另外,内核必须创建自己的环境并让系统准备好运行进程0(下面解释).这些任务包括CPU检测,运行时刻任务建立,和内存数量检测.
CPU识别是非常重要的一步,因为每个平台可以有多种不同的CPU(i386是其中之一),不是所有的CPU都会支持同样的特性.比如,拿MMX指令集来说,虽然它对于内核来说不是一个重要特性,但是对于浮点单元就是,因此如果这个特性不被该CPU所支持的话就必须用软件来模拟.这对于所有其他不被支持的特性和已知错误或者CPU的固有特性来说都是成立的
第二个阶段会初始化系统硬件和内存.这包括探测硬件,初始化I/O设备,为内核结构分配内存,建立系统消息缓存.该阶段你会看到启动屏幕中闪过很多硬件列表.按照BSD传统,这个阶段是通过呼叫cpu_startup()函数来初始化.
第3阶段 和 进程 0一旦cpu_startup()函数返回,内核就需要建立进程0.进程0通常称为交换器,如果你运行ps命令会看到它在活动.然而从感官上来说并没有这样一个名为swapper的二进制文件附属于该进程.这对于其他四个在现代FreeBSD系统上找到进程都是成立的:pagedaemon,vmdaemon,bufdaemon和syncer.为避免复杂化,我们只说这些进程是VM子系统的一部分;我们在进程一章中再讨论它们.着重理解他们是在启动过程中由内核创建而不是文件系统中的二进制程序,并且是相当平台依赖性的.他们是用c语言写成并在起始平台环境初始化完毕以后开始的
init和系统shell脚本在所有的汇编和平台依赖性代码执行完毕以后,内核终于执行第一个真正的程序:/sbin/init.该程序是相当简短的(在FreeBSD上,总共约1,700行).象我们在BSD进程一章所讨论的那样,这就是那个所有进程都遗传自的进程.这样设计的实力在于,因为/sbin/init只是文件系统中的一个二进制程序,你可以自己写一个自定义版的.对于/sbin/init在启动时的主要目标就是运行系统启动脚本并为系统进入多用户模式做准备.留意信号:/sbin/init应当能优雅的处理信号,否则你的系统可能结束在/sbin/init程序的一个奇怪的状态并且在系统启动时崩溃掉.同时运行中/sbin/init可以接受信号来执行某些任务.比如,如果你想让系统不为某个相关的终端生产进程.象列在/etc/ttys里面的.你可以标记想要的终端为关闭状态并执行下面的命令使init读取/etc/ttys并只为列出的标记状态为开的终端生产进程.
bash$kill -HUP 1
注意除非你很小心,否则可能会以一个你不能登录的系统告终(到关于信号的一章里面查看详细信息.)
init程序会在启动过程中设置系统以进入多用户模式.这是很灵巧的.引入了象开始每一个守护进程并设置网络信息这样枯燥的任务.在UNIX系统中.有一些这样的途径,主要引入shell脚本.在某些版本的Linux和System V系统中,对应与某个运行级别的可启动脚本位于/etc/rc<n>.d.然而BSD使用了简单得多的办法.这就是在/etc/里面找到的的rc脚本
这些脚本通常是不能被编辑的,而是,在/etc/fc.conf里面设置变量(来改变行为).象PicoBSD的自定义安装你可能需要建立自己的脚本;PicoBSD是高度磁盘空间敏感的,并有相关的文件系统需要.一个重要的提示,/usr/local/etc/rc.d/文件夹是特别的.该文件夹的特别意义在于其中找到的每个.sh可执行文件都会在/etc/rc脚本之后被执行.为了方便系统管理这个文件夹替代了老的/etc/rc.local文件(/etc/rc.local是以前在系统启动尾声的时候启动自定义脚本或者程序的办法)
BSD rc脚本包括值得注意的条目列表如下:
/etc/rc - 主脚本并且第一个被呼叫,挂接文件系统并运行所有需要的rc脚本
/etc/rc.atm - 用来配置ATM网络.
/etc/rc.devfs - 设置/dev/权限和连接
/etc/rc.diskless1 - 第一个diskless客户端脚本
/etc/rc.diskless2 - 第二个diskless客户端脚本
/etc/rc.firewall - 用于防火墙规则
/etc/rc.firewall6 - 用于IPV6防火墙规则
/etc/rc.i386 - intel系统相关需要
/etc/rc.isdn - isdn网络设置
/etc/rc.network - IPV4网络设置
/etc/rc.network6 - IPV6网络设置
/etc/rc.pccard - 膝上电脑pc card控制器
/etc/rc.serial - 设置串口设备
/etc/rc.sysctl - 在系统启动时设置sysctl选项
/usr/local/etc/rc.d/ - 存有自定义启动脚本的普通文件夹
这里有一个例子:
如果你想让rsync作为守护进程这个脚本会起作用:
#!/bin/sh
if [ -x /usr/local/bin/rsync ]; then
/usr/local/bin/rsync --deamon
fi
这将首先检测rsync是否存在然后以守护模式运行它
第三章: 进程和内核服务
译者:孙轩@chinaunix.net
该章节的例程:
http://www.khmere.com/freebsd_book/src/03/index.html3.1 进程和内核服务process:一系列用于实现目标的行为和操作;特别指:制造中指定的连续的工作和处理。
前面的章节有提到BSD系统的启动过程。现在让我们看看一旦内核被导入并运行会发生什么。像大部分其他的现代操作系统,BSD是一个多用户多任务操作系统,这意味着它支持多用户运行不同的多进程来使用系统资源。进程的概念提出了一个对操作系统管理的所有活动的有力的提取。这个概念作为在多道程序设计环境中面向工作的一个特定术语是在19世纪60年代由Multics(一个失败的分时系统计划)的设计者提出的。
3.2 调度 一个进程是一个运行程序的单独实例。例如,当你在BSD系统中运行Netscape,它将在被运行时创建一个进程。
如果有3个用户登陆BSD系统并全部同时运行相同的Netscape程序,每一个用户将拥有区别于其他用户的自己的
Netscape实例。BSD系统可以支持同时多个这样的进程。每一个进程将和一个进程标志号(PID)关联。这些进程
需要资源并且可能不得不使用一些设备比如外部存储。
当多个程序在一个系统上运行的时候,操作系统操控出他们都在运行的假象。操作系统特定的调度算法将管理
程序的优先权指派。该领域的计算科学是广阔的,和十分专业的。更多的信息请看资源章节。
操作系统实际上将运行的多进程不断调入或调出CPU(s),通过这种方法,每个进程得到一段特定的CPU(s)运行
时间。这段时间被叫做时间片。时间片的长短基本上由内核的调度算法决定。该算法有一个可以调整的值叫‘nice’,它提供给进程修改运行优先权的程序能力。这些优先权的值如下:
Nice values Priority
-20 - 0 Higher priority for execution
0 - 20 Lower priority for execution
注意:一个高的nice值导致低的运行级别,这可能看上去有点怪异。然而,考虑到下面这个计算:
(scheduling algo calculation) + (nice value)
这个简单的算术表明:添加一个负值到一个正值将得到一个较小的数,而当这些数被排序的时候,较小的数将排在
运行队列的前面。
所有进程默认的nice值是0,运行的进程可以提高自己的nice值(就是降低自己的优先权),但是只有以root运行的
进程可以降低nice(就是提高优先权)。BSD系列提供了两个基本的借口用于改变和获得这些值。它们是:
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
和
int nice(int incr);
nice函数会把调用它的进程的nice值设置成传给它的参数incr的值。可以看到nice函数是比较不好用的,因为它不是
十分灵活,实际上nice函数不过是对前两个函数的封装。首选的方法是使用setpriority()。
因为可能的的优先权值可以为-1,所以getpriority的返回值不能作为程序运行成功与否的判断,而应该检查errno(错误号)是否被设置。因此在调用getpriority的时候要清空该值(errno)。(更多的关于使用errno信息,可以使用man(2)察看关于erron的页面和读例程:
http://www.khmere.com/freebsd_book/src/03/nice_code.c.html)setpriority 和 getpriority可以通过设置‘which’和‘who’的值作用于外部进程的优先级,这以后在论述。
例程nice_code.c示范如何获取和设置当前进程的nice值。如果它被root以-20的命令行参数运行,系统将看上去没有反应。这是因为这个程序拥有了最高的运行级别,将会比系统优先。所以当设置丢了小于0的值时,需要谨慎使用和更多的耐心。取决于CPU,完全运行完可能要消耗20分钟。建议使用time指令来运行程序,如下:
bash$ time nice_code 10
然后,调整参数的值。这样进程消耗了多少运行时间就很明显了。比如调整参数使之小于0:
$ time nice_code -1
下面把参数调大:
bash$ time nice_code 20
同时,尝试用其他非root用户运行程序,并使用小于0的参数。系统将会拒绝操作。只有root运行的进程可以降低他们的nice值。(因为Unix系统时多用户系统,每个进程应该获得合理的CPU时间片,只有root可以改变进程的优先级到0以下,这样可以避免用户通过降低优先级来独占CPU资源.)
3.3 系统进程保护模式的概念在前一章已有介绍,简单的说,它是现代CPU系列支持的一个特定的模式,通过该模式操作系统可以保护内存.据此,现在有两种这样的操作模式。一是内核态,意味着进程将运行在内核的内存空间,并因此,以有内核特权的保护模式运行于CPU中。二是用户态,即运行并且不是运作在内核保护模式的一切。
这区别是十分重要的,因为所有的给定进程都在内核态和用户态使用资源。这些资源有多种格式,就如用来标志进程的内核结构,进程指派的内存,打开的文件,和其他执行段。
在一个FreeBSD系统中,有一些关键的进程帮助内核来执行任务。这些进程中的一部份是完全运行于内核空间,而有些运行于用户态。这些进程如下:
PID Name
0 swapper
1 init
2 pagedaemon
3 vmdaemon
4 bufdaemon
5 syncer
以上所有的进程除了init,都是运行于内核。这意味着它们不存在相应的二进制程序。这些进程有点类似用户态进程,并且因为它们在内核内运行,他们运作于内核特权模式。这种体系结构是出于多种设计考虑。例如,pagedaemon进程,为了减少负载,只在系统缺乏内存时被唤醒。因此如果系统许多空闲的内存,它就没有被唤醒的必要。如此,比这个进程运行于用户态的优点就是除非真正需要,可以避免使用CPU。但是,它在系统增加了一个需要被调度的进程。不过这些调度计算消耗如此小几乎可以忽略不计。
init进程是所有进程的宗主进程。除了运行于内核态特权模式的进程,每个进程都是init的后裔。并且,僵尸或孤儿进程都会被init接管。它也运行一些管理任务,比如调用产生系统ttys的gettys,和运行系统的有序关机。
3.4 进程创建和进程ID系统就像上面所说,当一个进程被运行,内核指派给它一个唯一PID。这个PID是个正整数并且它的值在0到PID_MAX之间,由系统决定。(对于FreeBSD,在/usr/include/sys/proc.h中设定PID_MAX为99999。)内核将用下一个相续的可用的值来指派PID。因此,当PID_MAX到达的时候,值将会循环。这个循环是重要的,当使用PID于当前进程统计的时候。
每一个在系统上运行的进程都是由其他进程产生的。这是有一些系统调用完成的,将在下一章论述。当一个进程创建一个新的进程,原进程就成为父进程,新进程就是子进程。这种父子关系提供了很好的类推-每一个父进程可以有很多子进程而父进程们是由另外一个进程派生的。进程可以通过getpid/getppid函数得到自己或他们父进程的PID。
进程也可以用进程组来分组。这些进程组可以通过一个唯一的grpID来标志。进程组作为一个机制被引入BSD是为了使shells能够控制工作。看下面这个例子:
bash$ ps auwx | grep root | awk {'print $2' }
这些程序,ps,grep,awk和print都归属于一个相同的组,这允许所有的这些指令可以通过一个单一的工作来控制。
一个进程可以获得它的组ID/和父ID通过调用getpgrp或getppid:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgrp(void);
所有以上例出的函数都使绝对可以运行成功的。然而,FreeBSD man pages 强烈建议,PID不应该用来创建一个唯一文件,因为这个PID只在创建的时候唯一。一旦进程退出,PID值会回到未使用PID池,将会被接着被使用(当然是在系统一直运行情况下)。
一个获得这些值的源程序代码在 proc_ids.c中列出。
http://www.khmere.com/freebsd_book/src/03/proc_ids.c.html
当你运行下面程序:
bash$ sleep 10 | ps auwx | awk {'print $2'} | ./proc_ids
同时在另一个终端运行:
bash$ ps -j
只有这些指令将被一个shell运行并且每个都有相同的PPID和PGID。
3.5 子进程
进程可以由其他进程创建,在BSD中有3个方法来实现这个目的。他们是:fork,vfork 和rfork 。其他的调用例如system不过是对这3个的封装。
fork
当一个进程调用frok,一个新的复制父进程的进程奖被创建:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
不像其他的函数调用,在成功的时候,fork会反馈两次-一次在父进程,返回的是新创建的子进程的PID,而第二次在子进程,返回将是0。这样一来,你就可以区分两个进程。当一个新的进程被fork创建,它几乎是父进程的精确复制。他们的共同点包括一下:(不是创建的顺序)
Controlling terminal
Working directory
Root directory
Nice value
Resource limits
Real, effective and saved user ID
Real, effective and saved group ID
Process group ID
Supplementary group ID
The set-user-id bits
The set-group-id bits
All saved environment variables
All open file descriptors and current offsets
Close-on-exec flags
File mode creation (umask)
Signals handling settings (SIG_DFL, SIG_IGN, addresses)
Signals mask
在子进程中不同的是它的PID,而PPID被设置成父进程的PID,进程的资源利用值是0,并且拷贝了父进程的文件描述符。子进程可以关闭文件描述符而不干扰父进程。如果子进程希望读写它们,将会保持父进程的偏移量。注意一个潜在的问题,如果父子进程同时试图读写相同的文件描述符会导致输出异常,或程序崩溃。
当一个新的进程被创建后,运行的顺序将不可知。最好控制运行顺序的方法是使用semiphores,pipes,或signals,这以后论述。通过这些,读写就可以控制,使得一个进程可以避免扰乱其他进程导致一起崩溃。
wait
父进程应该使用下面wait系统调用的一种来搜集子进程的退出状态:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
#include <sys/time.h>
#include <sys/resource.h>
pid_t waitpid(pid_t wpid, int *status, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t wpid, int *status, int options, struct rusage *rusage);
在调用wait的时候,options参数是一个位值,或者是以下的一个:
WNOHANG -在调用的时候不阻塞程序,他会导致wait马上反馈即使没有子进程被终止。
WUNTRACED -当你想要等待停止并且不可跟踪的子进程(可能由SIGTTIN, SIGTTOU, SIGTSTP或SIGSTOP信号导致)的状态时设置该值。
WLINUXCLONE -如果你想等待通过linux_clone产生的内核线程。
当使用wait4和waitpid调用时,要注意wpid参数是要等待的PID值。如果将其设置成-1将会导致该调用等待所有可能的子进程的终止。在调用时使用rusage结构,如果结构不是指向NULL将返回终止进程的资源使用统计。当然如果进程已经停止,那么这些使用信息就没有什么用处。
wait函数调用提供给父进程一个方法去获取它子进程退出的信息。一当调用,调用它的进程将被阻塞知道一个子进程终止或接收到一个信号。这可以通过设置WNOHANG 值来避免。当调用成功时,status参数将包含结束进程的信息。如果调用进程对退出状态没有兴趣,可以将status设置成NULL。关于使用WNOHANG 的更多细节将在信号章节描述。
如果对退出状态信息有兴趣,它们的宏定义在/usr/include/sys/wait.h中。最好使用它们来获得更好的跨平台兼容性。下面列出它们3个和使用说明:
WIFEXITED(status) -如果返回true(也就是返回非0值)那么进程是通过调用exit()或_exit()正常终止的。
WIFSIGNALED(status) -如果返回true那么进程是由信号终止的。
WIFSTOPPED(status) -如果返回true那么进程已经停止并且可以重新开始。这个宏应该和WUNTRACED一起使用,或当子进程被跟踪的时候(就像使用ptrace)
如果有必要,以下的宏可以进一步提取保存的状态中信息
WEXITSTATUS(status) - 这只能在WIFEXITED(status) 宏评估为true时使用,他会评估exit()和_exit()传递参数的低8位。
WTERMSIG(status) -这只能在WIFSIGNALED(status)评估为true时使用,他会得到导致进程终止的信号的值。
WCOREDUMP(status) -这只能在WIFSIGNALED(status)评估为true时使用,如果终止的进程在收到信号的点创建了core dump文件那么该宏会返回true。
WSTOPSIG(status) -这只能在WIFSTOPPED(status) 评估为true时使用。该红会得到导致进程停止的信号。
如果父进程没有收集子进程的退出状态,或父进程在子进程结束前就已结束,init将会接管子进程并收集它们的退出状态。
vfork 和 rfork
vfork函数和fork相似,是在2.9BSD被引入的。它们两者的区别是,vfork会停止父进程的运行并且使用父进程的运行线程。这是为了调用execv函数设计的,这样可以避免低效的复制父进程的地址空间。实际上vfork是不被建议使用的,因为它不是跨平台的。例如Irix的5.x就没有vfork.下面是vfork的调用例子:
#include <sys/types.h>
#include <unistd.h>
int vfork(void);
rfork函数也和fork与vfork相似.他是在Plan9引入的。它的主要目的是增加更成熟的方法来控制进程的创建和创建内核线程。FreeBSD/OpenBSD支持伪线程和Linux clone调用。换句话说,rfork允许比fork更快更小的进程创建。这个调用可以设置子进程可以和它们共享的资源。下面是一个rfork调用例程:
#include <unistd.h>
int rfork(int flags);
rfork可以选择的资源如下:
RFPROC -设置该值表示你想创建一个新的进程;而其他的标志将只影响当前进程。该值是默认值。
RFNOWAIT -设置该值表示你希望创建一个和父进程分离的子进程。一当该子进程被退出,他不会留下状态等待父进程收集。
RFFDG -设置该值表示你希望复制父进程的文件描述符表。否则父子进程将会共享一个文件描述符表。
RFCFDG -该标志和RFDG标志互斥。设置该值表示你希望子进程拥有一个干净的文件描述符表。
RFMEM -设置该值表示你想强制内核共享完整的地址空间。这是直接共享硬件页面表的典型做法。这不能直接在C中调用,因为子进程会返回和父进程相同堆栈。如果你想这么做,最好使用下面列出的rfork_thread函数:
#include <unistd.h>
int rfork_thread(int flags, void *stack, int (*func)(void*arg), void *arg);
这将创建一个新的进程运行于指定的堆栈,并且调用参数指定的函数。和fork不同,成功的时候只会返回新创建的进程PID给父进程,因为子进程将直接运行指定的函数。如果调用失败将返回-1,并且设置errno的值。
RFSIGSHARE -这是一个FreeBSD特有的标志并且最近众所周知地被用于FreeBSD4.3的缓冲区溢出。该值将允许子进程和父进程共享信号。(通过共享sigacts结构)。
RFLINUXTHPN -这是另一个FreeBSD特有的值。这将导致内核在子进程退出时使用信号SIGUSR1代替SIGCHILD。我们会在下一章论述它,现在可以把它看作rfork模仿linux clone调用。
rfork的返回值和fork相似。子进程获得0值而父进程获得子进程的PID。一个微小的区别是-rfork会在需要时休眠直到必要的系统资源可用。而一个fork调用和使用RFFDG | RFPROC来调用rfork相似。但是他不设计向后兼容。如果rfork调用失败会返回-1并且设置errno。(更多的error 标号信息可以看man page和头部文件。)
显然,rfork调用提供了一个较小消耗的fork版本。缺点是有一些众所周知的安全漏洞。并且他即使在跨BSD系列平台上也不是很兼容,在不同的Unix版本中是独一无二的。比如当前的NetBSD-1.5就不支持rfork,而且不是所有FreeBSD可用的标志都可以用于OpenBSD。正因为如此,推荐的线程接口是使用pthreads。
3.6 运行二进制程序
一个进程如果仅仅是父进程的拷贝是没有很大用途的。因此,需要使用exec函数。这些是设计来使用新的进程镜像代替当前进程的镜像。好,举个例子,shell运行ls指令。首先shell会按照执行运行fork或vfork接着它会调用exec函数。一旦exec被成功调用一个新的进程将由ls代替运行,并且exec自己没有返回。如果像shell或perl这样的脚本是目标程序,这个过程就像二进制程序运行。那里有一个附加的程序调用解析器。那就是,头两个字符将是#! 例如,下面展示了一个带参数的解析器调用:
#! interpreter [arg]
下面的指令将使用-w参数调用Perl解析器:
#!/usr/bin/perl -w
下面列出了所有的exec调用。源代码 exec.c http://www.khmere.com/freebsd_book/src/03/exec.c.html
调用它们的基本结构是(目标程序),(参数),(必要的环境变量)。
#include <sys/types.h>
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg0, ... const char *argn, NULL);
int execle(const char *path, const char *arg0, ... const char *argn, NULL, char *const envp[]);
int execlp(const char *file, const char *arg0, ... const char *argn , NULL );
int exect(const char *path, char *const argv[], char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
注意;在使用带有参数arg的exec时,arg参数必须是以NULL截止的字符串,就如在arg0,arg1,arg2 ..argn中必须以NULL或0结尾。如果没有确定结束符调用会失败。这字符串将成为目标程序运行的参数。
调用exec使用*argv[]参数时其必须是一组包含结束符的字符串的数组,它们将成为目标程序运行的参数。
另一个区分二进制或脚本运行exec的是目标程序是如何指定的。如果目标没有带路径,函数execlp和execvp会搜寻你的环境目录来查找目标程序。而其他函数调用将要求绝对路径。
简索:
数据参数和序列参数
array: exect, execv, execvp
sequence: execl, execle, eseclp
路径搜索和直接文件
path: execl, execle, exect, execv
file: execlp, exevp
指定环境和继承环境
specify: execle, exect
inherit: execl, execlp, execv, execvp
system
#include <stdlib.h>
int system(const char *string);
另外一个关键的运行函数调用是system调用。这个函数十分直观。提供的参数将直接传送给shell。如果指定的参数
为NULL,如果shell可用函数会返回1否则返回0。一旦调用,进城会等待shell结束并返回shell的结束状态。如果返回-1表示fork或waitpid失败,127表示shell运行失败。
一旦调用,一些信号例如SIGINT和SIGQUIT将被忽略,并且会阻塞SIGCHILD。同样如果子进程写输出到标准错误输出可能会使调用进程崩溃。
显然,BSD结构提供了简单同时丰富的进程创建接口。这种成熟的设计可以和任何一个现在操作系统媲美。下一章将包括信号和进程管理,包含资源使用,线程和进程限制。
第四章 高级进程控制和信号
翻译: gvim@chinaunix/bsd
QUOTE:
一些说明:
1 很高兴有这个机会参与到这项活动中来。
2 由于英语语言的习惯,文中有不少复杂长句,按照字面翻译出来并不适合中国人的阅读习惯。我在保留文章原意的基础上作了一些语言组织上的调整,将大部分长句子组织成中文擅长的短句型,所以你会发现翻译品一部分内容不是按照原文字面翻译的。
3 考虑到该书的入门引导作用,在原文中一些比较少见的词后面我添加了少许译注,希望减少入门兄弟的负担。
4 由于2中的语言调整和3中的译注,是我的个人行为,可能会有我对 原文理解模糊,概念不清,或是语言组织不顺畅的地方,还请大家一定多多指出来。谢谢。
个人声明:
本翻译品受控于chinaunix BSD翻译小组。如需单独转载,请保留“翻译:gvim@chinaunix/bsd”和以上几点说明。
4.1 高级进程控制和信号信号:4 a:一种对象,用于传输或负载人类声音之外的信息。
到目前为止,我们已经讨论了进程的创建和其他系统调用。现在是讨论下面这些问题的时候了:你要在多个进程间通讯以获得更好的进程控制粒度,或者要其他程序或操作者用信号通知你的程序。例如,你可能希望你的程序重新读取它的配置文件。或者,你的数据库程序需要在退出之前将事务从主存写入后备存储器,然后再退出。这两个例子可能只是使用信号的很小一部分。虽然已经有套接字,先入先出队列,管道,信号量等多种方式来完成类似的任务,但是我们将把讨论的焦点放在信号和其它进程控制机制上。在现实中,信号和进程控制机制可以提供大部分你所需要的特性和功能。
4.2 信号信号与硬件中断很相似。当设备需要中断服务的时候它可以产生一个硬件中断来通知CPU。与硬件中断类似的,当进程需要将一些事件通知给其他进程的时候可以使用信号来完成。
大多数Unix系统管理员会比较熟悉SIGHUP信号。当你通过kill命令向后台服务进程发出SIGHUP信号后,大多数进程要么重新读入他们的配置文件要么重新启动。这些信号之中,一些与硬件有直接关系,如SIGFPE(浮点异常),SIGILL(非法指令);其它则是与软件相关,如SIGSYS(未实现的系统调用被调用)。
一旦进程接收到信号之后,该信号的行为与信号本身和进程对它的使用目的两个因素有关。一些信号可以被阻塞,忽略,或者捕获,而另外一些则不可以。如果进程需要捕获一个信号并履行一些相关操作,你可以为进程设定这个特定信号的信号处理句柄。处理句柄仅仅是一个函数,在这个信号被进程接收之后调用。或者更确切的说,处理句柄是一个函数调用,你可以对它进行指派(specify)。
当信号没有指定处理句柄时,将会执行操作系统默认的行为。这些缺省行为可以是从终止进程到完全核心转储等不同的操作。注意,有两个信号不能被捕获或忽略:SIGSTOP和SIGKILL,下面会解释。
在BSD系统中定义的有许多信号;我们讨论在/usr/include/sys/signals.h(译注:在我的FB5.2.1中是signal.h)文件中定义的标准信号。注意, NetBSD系统中定义的信号数量稍微多一点,并且我们没有将它们的讨论放在这里。所以如果需要使用某个下面没有涉及到的信号的时候,请查阅你的系统的头文件。
#define SIGHUP 1 /* hangup */
SIGHUP是Unix系统管理员很常用的一个信号。许多后台服务进程在接受到该信号后将会重新读取它们的配置文件。然而,该信号的实际功能是通知进程它的控制终端被断开。缺省行为是终止进程。
#define SIGINT 2 /* interrupt */
对于Unix使用者来说,SIGINT是另外一个常用的信号。许多shell的CTRL-C组合使得这个信号被大家所熟知。该信号的正式名字是中断信号。缺省行为是终止进程。
#define SIGQUIT 3 /* quit */
SIGQUIT信号被用于接收shell的CTRL-/组合。另外,它还用于告知进程退出。这是一个常用信号,用来通知应用程序从容的(译注:即在结束前执行一些退出动作)关闭。缺省行为是终止进程,并且创建一个核心转储。
#define SIGILL 4 /* illegal instr. (not reset when caught) */
如果正在执行的进程中包含非法指令,操作系统将向该进程发送SIGILL信号。如果你的程序使用了线程,或者pointer functions,那么可能的话可以尝试捕获该信号来协助调试。(
注意:原文这句为:“If your program makes use of use of threads, or pointer functions, try to catch this signal if possible for aid in debugging.”。中间的两个use of use of,不知是原书排版的瑕疵还是我确实没有明白其意义;另外,偶经常听说functions pointer,对于pointer functions,google了一下,应该是fortran里面的东西,不管怎样,还真不知道,确切含义还请知道的兄弟斧正。)缺省行为是终止进程,并且创建一个核心转储。
#define SIGTRAP 5 /* trace trap (not reset when caught) */
SIGTRAP这个信号是由POSIX标准定义的,用于调试目的。当被调试进程接收到该信号时,就意味着它到达了某一个调试断点。一旦这个信号被交付,被调试的进程就会停止,并且它的父进程将接到通知。缺省行为是终止进程,并且创建一个核心转储。
#define SIGABRT 6 /* abort() */
SIGABRT提供了一种在异常终止(abort)一个进程的同时创建一个核心转储的方法。然而如果该信号被捕获,并且信号处理句柄没有返回,那么进程不会终止。缺省行为是终止进程,并且创建一个核心转储。
#define SIGFPE 8 /* floating point exception */
当进程发生一个浮点错误时,SIGFPE信号被发送给该进程。对于那些处理复杂数学运算的程序,一般会建议你捕获该信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
SIGKILL是这些信号中最难对付的一个。正如你在它旁边的注释中看到的那样,这个信号不能被捕获或忽略。一旦该信号被交付给一个进程,那么这个进程就会终止。然而,会有一些极少数情况SIGKILL不会终止进程。这些罕见的情形在处理一个“非中断操作”(比如磁盘I/O)的时候发生。虽然这样的情形极少发生,然而一旦发生的话,会造成进程死锁。唯一结束进程的办法就只有重新启动了。缺省行为是终止进程。
#define SIGBUS 10 /* bus error */
如同它的名字暗示的那样,CPU检测到数据总线上的错误时将产生SIGBUS信号。当程序尝试去访问一个没有正确对齐的内存地址时就会产生该信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGSEGV 11 /* segmentation violation */
SIGSEGV是另一个C/C++程序员很熟悉的信号。当程序没有权利访问一个受保护的内存地址时,或者访问无效的虚拟内存地址(脏指针,dirty pointers,译注:由于没有和后备存储器中内容进行同步而造成。关于野指针,可以参见http://en.wikipedia.org/wiki/Wild_pointer 的解释。)时,会产生这个信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGSYS 12 /* non-existent system call invoked */
SIGSYS信号会在进程执行一个不存在的系统调用时被交付。操作系统会交付该信号,并且进程会被终止。缺省行为是终止进程,并且创建一个核心转储。
#define SIGPIPE 13 /* write on a pipe with no one to read it */
管道的作用就像电话一样,允许进程之间的通信。如果进程尝试对管道执行写操作,然而管道的另一边却没有回应者时,操作系统会将SIGPIPE信号交付给这个讨厌的进程(这里就是那个打算写入的进程)。缺省行为是终止进程。
#define SIGALRM 14 /* alarm clock */
在进程的计时器到期的时候,SIGALRM信号会被交付(delivered)给进程。这些计时器由本章后面将会提及的setitimer和alarm调用设置。缺省行为是终止进程。
#define SIGTERM 15 /* software termination signal from kill */
SIGTERM信号被发送给进程,通知该进程是时候终止了,并且在终止之前做一些清理活动。SIGTERM信号是Unix的kill命令发送的缺省信号,同时也是操作系统关闭时向进程发送的缺省信号。缺省行为是终止进程。
#define SIGURG 16 /* urgent condition on IO channel */
在进程已打开的套接字上发生某些情况时,SIGURG将被发送给该进程。如果进程不捕获这个信号的话,那么将被丢弃。缺省行为是丢弃这个信号。
#define SIGSTOP 17 /* sendable stop signal not from tty */
本信号不能被捕获或忽略。一旦进程接收到SIGSTOP信号,它会立即停止(stop),直到接收到另一个SIGCONT信号为止。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。
#define SIGTSTP 18 /* stop signal from tty */
SIGSTP与SIGSTOP类似,它们的区别在于SIGSTP信号可以被捕获或忽略。当shell从键盘接收到CTRL-Z的时候就会交付(deliver)这个信号给进程。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。
#define SIGCONT 19 /* continue a stopped process */
SIGCONT也是一个有意思的信号。如前所述,当进程停止的时候,这个信号用来告诉进程恢复运行。该信号的有趣的地方在于:它不能被忽略或阻塞,但可以被捕获。这样做很有意义:因为进程大概不愿意忽略或阻塞SIGCONT信号,否则,如果进程接收到SIGSTOP或SIGSTP的时候该怎么办?缺省行为是丢弃该信号。
#define SIGCHLD 20 /* to parent on child stop or exit */
SIGCHLD是由Berkeley Unix引入的,并且比SRV 4 Unix上的实现有更好的接口。(如果信号是一个没有追溯能力的过程(not a retroactive process),那么BSD的SIGCHID信号实现会比较好。在system V Unix的实现中,如果进程要求捕获该信号,操作系统会检查是否存在有任何未完成的子进程(这些子进程是已经退出(exit)的子进程,并且在等待调用wait的父进程收集它们的状态)。如果子进程退出的时候附带有一些终止信息(terminating information),那么信号处理句柄就会被调用。所以,仅仅要求捕获这个信号会导致信号处理句柄被调用(译注:即是上面说的“信号的追溯能力”),而这是却一种相当混乱的状况。)
一旦一个进程的子进程状态发生改变,SIGCHLD信号就会被发送给该进程。就像我在前面章节提到的,父进程虽然可以fork出子进程,但没有必要等待子进程退出。一般来说这是不太好的,因为这样的话,一旦进程退出就可能会变成一个僵尸进程。可是如果父进程捕获SIGCHLD信号的话,它就可以使用wait系列调用中的某一个去收集子进程状态,或者判断发生了什么事情。当发送SIGSTOP,SIGSTP或SIGCONF信号给子进程时,SIGCHLD信号也会被发送给父进程。缺省行为是丢弃该信号。
#define SIGTTIN 21 /* to readers pgrp upon background tty read */
当一个后台进程尝试进行一个读操作时,SIGTTIN信号被发送给该进程。进程将会阻塞直到接收到SIGCONT信号为止。缺省行为是停止进程,直到接收到SIGCONT信号。
#define SIGTTOU 22 /* like TTIN if (tp->t_local<OSTOP) */
SIGTTOU信号与SIGTTIN很相似,不同之处在于SIGTTOU信号是由于后台进程尝试对一个设置了TOSTOP属性的tty执行写操作时才会产生。然而,如果tty没有设置这个属性,SIGTTOU就不会被发送。缺省行为是停止进程,直到接收到SIGCONT信号。
#define SIGIO 23 /* input/output possible signal */
如果进程在一个文件描述符上有I/O操作的话,SIGIO信号将被发送给这个进程。进程可以通过fcntl调用来设置。缺省行为是丢弃该信号。
#define SIGXCPU 24 /* exceeded CPU time limit */
如果一旦进程超出了它可以使用的CPU限制(CPU limit),SIGXCPU信号就被发送给它。这个限制可以使用随后讨论的setrlimit设置。缺省行为是终止进程。
#define SIGXFSZ 25 /* exceeded file size limit */
如果一旦进程超出了它可以使用的文件大小限制,SIGXFSZ信号就被发送给它。稍后我们会继续讨论这个信号。缺省行为是终止进程。
#define SIGVTALRM 26 /* virtual time alarm */
如果一旦进程超过了它设定的虚拟计时器计数时,SIGVTALRM信号就被发送给它。缺省行为是终止进程。
#define SIGPROF 27 /* profiling time alarm */
当设置了计时器时,SIGPROF是另一个将会发送给进程的信号。缺省行为是终止进程。
#define SIGWINCH 28 /* window size changes */
当进程调整了终端的行或列时(比如增大你的xterm的尺寸),SIGWINCH信号被发送给该进程。缺省行为是丢弃该信号。
#define SIGUSR1 29 /* user defined signal 1 */
#define SIGUSR2 30 /* user defined signal 2 */
SIGUSR1和SIGUSR2这两个信号被设计为用户指定。它们可以被设定来完成你的任何需要。换句话说,操作系统没有任何行为与这两个信号关联。缺省行为是终止进程。(译注:按原文的意思翻译出来似乎这两句话有点矛盾。)
4.3 系统调用那么,你该如何使用信号呢?有时候甚至拿不准是否应该使用信号。例如,当信号被交付的时候,一方面你可以在行为发生之前,分析当前情况,找出信号发生的原因,或者找到这些信号是从哪里发出来的;另一方面,其他一些时候你也可以只是希望简单的退出程序,并且在清除之后创建一个核心转储文件。参见最后部分的简单代码可以获得这些函数的较详细的例子。
Kill函数kill函数对于那些经常在命令行使用kill命令杀死进程的人来说是再熟悉不过的。基本语法是:
int kill(pid_t pid, int sig);
Kill函数将指定的信号发送给进程号为pid的进程。只有当进程符合下面几点情况的时候信号才会被交付:
QUOTE:
• 发送与接收进程有相同的有效用户ID(UID);
• 发送进程有适当的权限(例如:setuid命令);
• 发送进程有超级用户(root)的UID。
注意:SIGCONT信号是一个特例,它可以由当前进程发送给任何一个该进程的派生进程。
使用不同的调用参数使得kill函数的行为差别非常大。这些行为如下所述:
(译注:下面的PID应该指的是上面kill函数原型中的那个pid,我在这里做出说明并保留原文)
QUOTE:
• 如果PID大于0,并且发送进程有适当的权限,那么参数sig指定的信号将被交付。
• 如果PID等于0,那么sig信号将被交付给所有那些与发送进程有相同组ID的进程。(发送进程同样需要满足权限需求。)
• 如果PID是 -1,那么信号将被发送给所有那些与发送进程有相同有效用户ID的进程(不包含发送进程在内)。然而,如果发送进程的有效用户ID与超级用户(root)的相同,那么信号被交付给除了系统进程(由它们的proc结构中的p_flag域是否是P_SYSTEM来定义)之外的所有进程。在这个特殊的例子中,如果某些进程不能被发送(could not be sent)sig信号,kill函数并不返回一个错误。
• 如果sig是0,kill函数只检查错误(例如,无效权限,不存在的进程等)。该用法有时候用来检查一个指定进程是否存在。
• 如果成功的话kill函数返回0,否则返回-1。kill调用失败时会在errno全局变量中设置相应的错误值。
kill的另一个版本是raise函数:
int raise(int sig);
raise函数会向当前进程发送sig信号。该函数用处不是很大,因为它只能够给当前进程发送信号。raise函数调用成功时返回0,否则返回-1。调用失败时会在errno全局变量中设置相应的错误值,效果和signal函数的返回类似:(译注:原文只有两个单词“as in:”,我并不知道作者把signal列在这里所要表达的意思,所以我按照我的理解+猜测来翻译的。如果大家有什么建议,或是需要纠正的话,请一定告诉我。)
void (*signal(int sig, void (*func)(int)))(int);
4.4 信号处理
现在我们知道何时会产生信号,也知道如何发送信号,那么我们怎么处理它们呢?
signal函数signal系统函数调用提供了一种最简单的范例。然而,由于C原形声明的缘故使它看起来比实际复杂。signal函数将一个给定的函数和一个特定的信号联系。这里是FreeBSD中的定义(和一个typedef一起):
QUOTE:
typedef void (*sig_t) (int);
sig_t signal(int sig, sig_t func);
第一个参数是目标信号,可以是上面列举的所有信号中的任何一个。func参数是一个指针,指向某个处理该信号的函数。这个处理信号函数带有一个int型参数,并应返回void。signal函数中的func参数也可以设定为下面的一些值:
QUOTE:
SIG_IGN: 如果func参数被设置为SIG_IGN,该信号将被忽略。
SIG_DFL: 如果func参数被设置为SIG_DFL,该信号会按照确定行为处理。
sigaction函数sigaction函数是一个比signal更通用的方案。第一个参数是目标信号。下一个名为act的参数(指向)sigaction结构,该结构包含一些用于信号处理的信息。最后一个参数oact是一个指针,指向一个可以存储上一次设置信号处理的信息的地方。
QUOTE:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
sigaction结构有下面这些个成员:
QUOTE:
void (*sa_handler)(int);
这个结构成员是一个指向函数的指针,该函数带有一个简单的整形参数,并返回(void)。这与signal函数的func参数相同,也可以被设置为SIG_IGN和SIG_DFL,并且与调用signal得到的效果也一样。
QUOTE:
void (*sa_sigaction)(int, siginfo_t *, void *);
该结构成员是一个指向函数的指针,返回(void)并需要三个参数。这些参数依次为:一个整形参数指定信号发送;一个指向siginfo_t结构的指针用来保存关于信号的信息;最后一个也是一个指针,指向信号交付时的特定上下文(context)空间。
QUOTE:
sigset_t sa_mask;
该结构成员是一个位掩码(bitwise mask),用来指示信号交付时哪些信号会被阻塞。阻塞SIGKILL和SIGSTOP信号的做法会被忽略。接下来,被阻塞的信号将被推迟,直到它们被开启(unblock)。参见sigprocmask获得更多关于全局掩码(global masks)的信息。
QUOTE:
int sa_flags;
该数据成员是一个拥有下面这些标志的位掩码:
QUOTE:
SA_NOCLDSTOP: 如果SA_NOCLDSTOP位被置位并且目标信号是SIGCHLD,除非子进程退出,而在子进程停止(stop)时父进程将不会收到通知。
SA_NOCLDWAIT: SA_NOCLDWAIT标志会阻止子进程成为僵尸进程。在目标信号是SIGCHLD的时候使用。如果进程设置了这个标志,接着调用某个wait系统调用,进程将被阻塞直到子进程全部终止,最后返回-1(译注:此处在APUE2ed中的解释是返回1),设置errno全局变量为ECHILD。
SA_ONSTACK: 一些时候需要在特定的堆栈上进行信号的处理。sigaction系统调用提供了这个方式。如果该位被置位,那么信号将会被交付到指定的堆栈上。
SA_NODEFER: 如果SA_NODEFER位被置位,那么当前信号正被处理时,系统不会屏蔽该信号以后的交付。
SA_RESETHAND: 如果SA_RESETHAND被置位,一旦信号被交付,信号处理句柄将被置为SIG_DEF。
SA_SIGINFO: 被置位时,由结构体sigaction 的成员sa_sigaction指向的函数被使用。注意:使用SIG_IGN或SIG_DFL时不应该设置这个标志。成功调用sigaction之后,返回0或-1,并且将error设置成相关错误值。
4.5信号掩码(阻塞与开启信号)
进程可以阻塞或设置某个信号。一旦该信号被阻塞,关于它的交付将被推迟,直到进程重新开启它。在这样的情况下是非常有用的:进程进入代码中某个部分,不能被中断但仍希望可以接受、处理可能丢失的信号。可靠交付信号的能力直到4.2BSD引入之后(不久被SVR3采用),操作系统才拥有该能力。
随着可靠信号的出现,信号的生命和交付(life and delivery)都有所改变。信号可以在之前产生和交付。现在,一旦信号是挂起的(pending),进程可以在接收它之前决定怎么处理。进程可能会去处理它,也可能设置为缺省行为,或者丢弃信号
注意:如果许多信号都挂起,系统将会首先交付会改变进程状态的信号,例如SIGBUS。
sigprocmask任何进程可以使用sigprocmask函数来阻塞信号。语法如下:
QUOTE:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
sigprocmask函数会修改或检查(modify or examine)当前信号掩码。当set参数不是null的时候,sigprocmask的行为和第一个参数how有关。函数行为和相关意义列举如下:
QUOTE:
SIG_BLOCK: 在set参数中指定的信号被阻塞,并且添加进阻塞信号列表。
SIG_UNBLOCK: 在set参数中指定的信号会从信号掩码中移除。
SIG_SETMASK: set参数将完全替代当前信号掩码。如果oset参数不为null,则会被设置为前一个信号掩码。如果set值是null,how参数被忽略并且信号掩码保持不变。所以,为了检查信号掩码,我们可以将传入set null值,oset为非null值来调用sigprocmask函数。一旦掩码得到之后,你可能需要对他进行检查或操作。可以使用下面的过程(routine)。注意当前这些过程是宏的实现。
int sigemptyset(sigset_t *set)
如果调用这个过程,set参数将被初始化指向一个空信号集。
int sigfillset(sigset_t *set)
如果调用这个过程,set参数将被初始化指向一个包括所有信号的信号集。
int sigaddset(sigset_t *set, int signo)
如果调用这个过程,signo指定的信号将被添加进set参数指定的信号集。
int sigdelset(sigset_t *set, int signo)
如果调用这个过程,signo指定的信号将从set参数指定的信号集中移除。
int sigismember(const sigset_t *set, int signo)
如果调用这个过程,如果由signo指定的信号存在于set参数指定的信号集中时,返回1,否则返回0。
int sigpending(sigset_t *set);
进程可以使用sigpending函数去查出当前那些信号被挂起。sigpending函数会返回一个包含所有挂起信号的掩码。该掩码可以使用上面介绍的过程去检查。sigpending成功时返回0,否则返回-1,并且设置errno为相应错误值。
4.6 自定义行为一些时候程序要求信号处理句柄运行在一个特定的堆栈上。为了实现这个目的,一个备用(alternate)堆栈区间必须用signaltstack函数指出来。这个函数使用的数据结构为signaltstack:
int sigaltstack(const struct sigaltstack *ss, struct sigaltstack *oss);
它的结构成员解释如下。
char *ss_sp;
该成员指向一个被用作堆栈的区域。系统中有个MINSIGSTKSZ常量,它定义了进行信号处理时所需的最小内存空间。系统中还有一个SIGSTKSZ常量,它定义了通常情况下处理时所需内存空间。该内存空间需要在调用signaltstack函数之间分配。
size_t ss_size;
数据成员ss_size指出新堆栈的大小。如果这个值是错误的(inaccurate),当信号处理句柄执行时,它的行为就变得不可预知(你不能明确知道系统怎样处理这个信号)。
int ss_flags;
根据调用环境(calling circumstances),数据成员ss_flags可以具有少数几个不同的值。首先,当进程希望停用备用堆栈的时候,ss_flags会被设为SS_DISABLE。在这个情况下,ss_sp和ss_size被忽略,备用堆栈被禁止。注意,备用堆栈只能在当前句柄没有处理时禁止。
如果使用一个non-null值作为oss的实参去调用signaltstack,ss_flags将包含指示当前状态的信息。它们是:
SS_DISABLE: 备用堆栈被停用。
SS_ONSTACK: 备用堆栈当前正在被使用,并且现在不可以停用。
如果调用signaltstack 的oss实参不是null,会返回当前状态。调用成功返回0,否则返回-1。如果调用失败,errno也会相应的被设置。因为信号可以在任何一点被交付,所以很难被预测。出于这个原因,4.2BSD的缺省行为是:重新开始被中断的系统调用,重新提供还没有被转送的数据。在大多数时候这个行为是很不错的,并且也是所有BSD系统采用的缺省行为。可是,也有一些罕见的情况,你可能需要将这个特性关掉。你可以使用siginterrupt函数完成需求。使用很简单:
int siginterrupt(int sig, int flag);
将sig参数设置为目标信号,并且设置flag为真(在这个情况下是1)。如果flag参数被设为假(在这个情况下是0),那么缺省行为是重新启动系统调用。
4.7 等待信号
sigsuspend函数可以暂时将当前阻塞信号集改变为由sigmask指定的信号集。改变后,sigsuspend会等待,直到一个信号被交付。一旦一个信号被交付后,原先的信号集被恢复。由于sigsuspend调用在信号交付之后总是被终止,它的返回值总是-1,errno总是EINTR。下面是它的语法:
int sigsuspend(const sigset_t *sigmask);
sigwait函数用set参数指定的信号集作为信号掩码。它会检查包含在这个特定集合内的是否有任何挂起信号,如果有的话,它将清除这个挂起的信号,并在sig参数中返回这个被清除信号的数值。如果没有信号挂起,sigwait将一直等待直到指定信号集合中的任何一个信号产生。下面是它的语法:
int sigwait(const sigset_t *set, int *sig);
当信号被交付给进程(该进程安装了相应信号的处理句柄)时,进程将会切换到信号处理句柄中执行。例如,假设你的程序监听一个由配置文件设定的端口。你的进程安装了一个捕获SIGHUP信号的处理句柄来重新读取配置文件。一旦SIGHUP信号被交付给你的程序,进程将会执行信号处理句柄来重新读取配置文件。这里存在一个问题,你没有办法知道在进程执行过程中信号被交付的确切地点。你虽然可以使用一些下面列出的函数来将范围缩小,但是如果碰到像打开套接字,打开链接,或者其它那些首先需要清理后才能在新端口上监听 这些情况的时候呢?你怎样确定清理活动在那里开始,什么时候开始?如果你的程序正在等到输入,并且没有数据被传进来的时候,系统调用将被重启(system call will be restarted),所以从SIGHUP的返回将会继续等待。
这是使用setjmp和longjmp函数的一些情况,这些函数提供非本地分支(non-local branching)。为了使用这些setjmp函数,需要提供一个evn参数,如下:
jmp_buff env;
int sigsetjmp(sigjmp_buf env, int savemask);
void siglongjmp(sigjmp_buf env, int val);
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
int _setjmp(jmp_buf env);
void _longjmp(jmp_buf env, int val);
void longjmperror(void);
首先,调用setjmp的返回为0,当前的环境将被保存在env中。接着,你可以在信号处理句柄内部调用对应的longjmp。一旦调用了longjmp,它将把执行环境恢复为env中保存的环境,并返回到最初setjmp被调用时的环境中。最初的setjmp调用返回那个传递给longjmp的va参数的值。
关于setjmp和longjmp函数一些说明:首先,这两个是不能混杂使用。也就是说,调用setjmp时保存的env变量不能传递给_longjmp调用。另外,调用setjmp的函数返回后,接下来调用longjmp将会发生错误。
The different calls have specific actions that they take. These actions are listed below:
不同的调用有特定的行为。这些行为在下面列出来:
jmp和longjmp: 他们会保存(恢复)信号掩码,寄存器组和堆栈。
_setjmp和_longjmp: 他们只保存(恢复)寄存器组和堆栈。
sigsetjmp和siglongjmp: 只要savemask参数不是0,他们就保存(恢复)寄存器组,堆栈和信号掩码。
由于一些原因,如果env参数保存的东西被破坏,或者调用setjmp的函数返回了,longjmp函数将会调用longjmperror函数。如果longjmperror也返回了,程序将被异常终止。你可以使用与longjmperror有相同原形的函数来自定义longjmperror函数。缺省的longjmperror会在标准错误上输出”longjmp botch”,然后返回。
4.8 Alarms
unsigned int alarm(unsigned int seconds);
alarm函数基本上是一个简单的闹钟时钟(alarm clock),同时也是一个很有用的函数。它允许进程在经过指定秒数之后收到一个通知。一旦闹钟时间到,进程将收到一个SIGALRM信号。任何随后的alarm调用都会覆盖原先的调用设定。alarm不像sleep函数,它不会被阻塞。
它有一些返回值需要值得你注意:首先,如果进程没有设定定时器,那么返回值是0。其次,如果有一个定时器被设定但还没有超时的话,那么会返回前一个调用到现在还有的剩余时间。
现在可以设定的最大时间是100,000,000秒 --- 已经是相当长的时间了。
int getitimer(int which, struct itimerval *value);
getitimer函数会检索由第一个参数(which参数)描述的itimerval结构。第一个参数可选的选项将在下面说明:
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
setitmer比先前的alarm调用提供了更稳定的接口。在BSD系统上,每个进程可以提供三种不同时间间隔的定时器。他们在下面讲述:
#define ITIMER_REAL 0
实时时钟实时的递减而不管进程在CPU上的实际花销时间(换句话说,它追踪自然时间natural time)。这允许进程设置一个基于自然实时时间(based on atural real time)的定时器。当实时定时器超时的时候,进程会收到SIGALRM信号。
#define ITIMER_VIRTUAL 1
虚拟定时器仅只递减进程在CPU上的执行时间,允许进程设定一个基于CPU使用率的定时器。当虚拟定时器超时的时候,进程收到SIGVTALRM信号
#define ITIMER_PROF 2
Profile定时器递减在CPU上的执行时间和代表进程执行的系统调用的时间。这对于那些要求静态剖析的解释程序是很有帮助的。当profile定时器超时的时候,进程收到SIGPROF信号。然而并不像实时和虚拟定时器那样,SIGPROF可以在系统调用的时候被发送;进程应该准备好重新执行被中断的系统调用。
本章将焦点放在了信号库上。这些信号及他们的使用方法对于系统编程是很重要的。信号允许系统管理员通知应用程序重新读取配置文件,从而使程序更稳定。其它重要的信号处理挂起在打开的文件描述符上的I/O操作。下一章讲述怎样利用这些I/O相关的信号。
第五章 基本输入输出
译者:horseman
(多谢我的同事HUGH CHEN翻译5.6,5.7跟部分5.4,5.5段落)
本章示范(示例)代码:
http://www.khmere.com/freebsd_book/src/05/index.html5.1 基本输入输出一般来说,Unix信奉简单设计的哲理。"一切都是文件"是个很强大的特征--这就意味着你所编辑的文本文件具有和调制解调器、打印机或网卡相同的编程接口,就像编辑文本文件一样,你应该能够对它们(被编辑文件)执行基本的读、写操作等等。尽管这个想法现行的实现不完美,BSD Unix实际上做到非常接近了,这也正是BSD又一个强大的地方--简洁而优雅。有些不是真实的文件而是设备,它们的入口在/dev目录下,有些设备只能用于特殊的操作,如块的读、写,一个极端的例子是以太网设备,甚至它(以太网设备)在FreeBSD 5之前在/dev下面没有入口。
操作系统看待每件事都像文件,一个好的示范是Plan 9,Plan 9用文件实现一切,甚至以太网和网络协议;更详细的信息可以参考Plan 9的主页
http://www.cs.bell-labs.com/plan9dist/一般来说,文件是计算机上数据最基本、最初级的表现形式,本质上是数据一位一位的线性序列。当用exec命令执行编译过的程序时,系统将把二进制文件读入内存,代码将在分配到内存地址空间后被执行,程序被定位在什么位置跟exec命令有关,它可以在软磁盘,硬盘,光驱,甚至是加载的另一半还分布在世界其他角落的网络文件系统上;跟基于网络链路发送数据一样,内容被顺序地一位一位地读入。当一个程序发送数据的时候,数据本身是一位一位的线性序列,有些时候叫做“流”,程序不关心它是否基于网络链路发送数据,它仅仅写数据;这两种最基本的操作,读跟写,是计算鸡用到的最多的。
本章将涉及最基本的I/O子系统跟进程资源。
5.2 基本输入输出UNIX进程打开文件的时候,会保存文件描述符的参考值,该值是一个整数,不论何时在UNIX系统上创建一个进程,都会给它3个文件描述符:
0 标准输入
1 标准输出
2 标准错误
这些值能够用于描述终端、文件的读写,甚至设置其他进程的描述。使shell重定向, cat /etc/hosts >> hosts.out, shell将打开文件hosts.out并且将cat /etc/hosts做为自己的参数执行。不管怎样,当cat进程写到标准输出后(1),结果不会被tty得到,而是输出到hosts.out文件。(cat程序根本不知道写入了文件系统的文件,还是写入到标准输出的文件描述符,我们将在这章的后面一点看到究竟是怎么实现的)
最基本的两个操作描述符是Open和Close函数
Open函数
int open(const char *path, int flags, /* mode */ );,
成功地调用这个函数后,Open函数将返回文件及参数的描述符,这个整型描述符在进程文件描述表内生成索引。该描述符的结构能够让内核知道如何操作这个文件。在BSD上,这个结构叫做filedesc,并且能够在/usr/include/sys/filedesc.h这个头文件中找到。当进程要在这个描述符上执行一些操作的时候,它将要求文件描述符为读、写、可执行操作指定一个整数值。
内核保存了所有文件描述符的基准值,这些基准值将在时进程打开、复制它或者已经处于打开状态的文件描述符执行exec调用、跨越fork的过程中增加。当这个参考值为0的时候,文件关闭。意思是,如果你有一个程序执行fork或者exec调用,并且close-on-exec位没有指定,基准值将增加;并且当一个新的程序执行了fork或者exec调用,基准值将继续增加。所以文件一直处于打开状态,直到基准值被设置为0,或者直到所有的进程关闭它,或者退出。
Close函数
int close(int fd);
当进程想移除或者关闭一个打开的文件描述符的时候,它将调用close函数。这将关闭指定的文件描述符,并且减少文件描述符的参考值。这个过程很像 exit————当一个进程执行exit,所有打开的文件描述符跟进程一起被自动地减少,自从文件参考值设为0,内核将释放所有的文件入口的副本.
getdtablesize函数
int getdtablesize();
getdtablesize函数返回文件描述表的大小,它能用作检查系统限制,你也能够用下面的命令做相同的事情。
bash$ sysctl kern.maxfilesperproc
kern.maxfilesperproc: 3722
依赖于你的系统,你能够在系统运行的时候调整它,或者从新编译你的内核,这个函数不能检查当前进程打开了多少文件,(跟getdtablesize一样)仅仅返回你的进程能够打开文件的最大个数。
fcntl函数
int fcntl(int fd, int cmd, ...);
fcntl 函数允许进程操作文件描述符,fcntl函数至少需要指定两个参数,一个有效的文件描述符,一个命令。根据使用的命令,决定fnctl是否需要第三个参数,下面为命令定参数义了一些值。在FreeBSD上,你能在/usr/include/fcntl.h头文件中找到它们。
#define F_DUPFD 0
F_DUPFD用作创建一个新的很像原型的文件描述符,(你能够用dup调用做到相同的事情,将在晚些时候涉及到),当成功执行带F_DUPFD标记的fcntl函数,fcntl将返回下面属性之一的新的文件描述符:
- 假如指定了第三个参数,描述符返回的值比最小可用的描述符,该值等于或者比给定的第三个参数的值大一点,返回的描述符将参照指定给fcntl第一个参数的文件描述符
- 假如fcntl指定的文件描述符是一个文件,新文件描述符将有相同的文件偏移量,并且新文件描述符将有相同的访问方式(如:O_RDONLY,O_RDWR,o_WRONLY)
- 新文件描述符将共享文件状态标记
- 新文件的“close-on-exec”标志将被关闭,即新的文件描述符在exec调用之后仍将保持打开。
F_GETFD命令
#define F_GETFD 1
F_GETFD 命令用作获取"close-on-exec"标记的状态,跟FD_CLOSEXEC相与后返回值要么为0,要么为1。如果返回0,close-on- exec标记没有被设置过,那么文件描述符将保持调用交叉执行调用(so the file descriptor will remain open across exec calls.),假如是1,close-on-exec标志被设置过,文件描述符将在成功调用一个exec函数后被关闭。
F_SETFD命令
#define F_SETFD 2
F_SETFD命令用作设置文件描述符的close-on-exec标志位。第三个参数要么是FD_CLOEXEC,设置close-on-exec标记,要么是0,取消close-on-exec标记的设置。
F_GETFL和F_SETFL命令
#define F_GETFL 3
#define F_SETFL 4
F_GETFL命令将使fcntl返回当前文件描述符状态标记,当返回值加上O_ACCMODE(#define O_ACCMODE 0x0003)能够获取到打开的方式,F_SETFL命令将根据第三个参数设置文件状态标记。
公共的标记下面这些标记也用作调用open,并且只能被跟上期望的标记调用open函数设置,这是最常见的,检查你系统的头文件能够查到那些值
#define O_RDONLY 0x0000
如果O_RDONLY标记被设置,那么文件只能以只读方式打开。注意这个O_RDONLY标记只能在打开的时候被设置,它不能被fnctl加F_SETFL命令设置。
#define O_WRONLY 0x0001
如果O_WRONLY标记被设置,那么文件只能以只写方式打开。这个标记只能被open设置,不能被fcntl加F_SETFL命令设置。
#define O_RDWR 0x0002
如果O_RDWR标记被设置,那么文件以可读可写方式打开。这个标记也只能被open调用设置。
#define O_NONBLOCK 0x0004
如果O_NONBLOCK标记被设置,文件描述符将不被阻塞而被直接返回替代。一个例子是打开tty。如果用户不在终端调用里输入任何东西,read将被阻塞,直到用户有输入,当O_NONBLOCK标记被设置,read调用将直接返回设置到EAGAIN的值
#define O_APPEND 0x0008
如果O_APPEND标记被设置,文件将以追加方式打开,并且将从文件末尾开始写入。
#define O_SHLOCK 0x0010
如果O_SHLOCK标记被设置,文件描述符将在文件上生成一个共享锁,在文件上设置了共享锁,多个进程能在同一个文件够执行操作,文件共享锁的详细信息,可用看fnctl函数的F_GETLK跟F_SETL命令。
#define O_EXLOCK 0x0020
如果O_EXLOCK标记被设置,文件描述符将在文件上生成一个可执行锁,一样,更详细的描述可以参照fcntl函数的F_SETLK跟F_GETLK命令。
#define O_ASYNC 0x0040
如果O_ASYNC标记被设置,进程集将被发送的SIGIO信号通知,在文件描述符号的IO是可用的。详细的描述请参照信号那一章。
#define O_FSYNC 0x0080
如果O_FSYNC标记被设置,所有写到文件描述符的写操作将不被内核缓存,取而代之的是将被写到介质,并且所有的写调用都将被阻塞,直到内核完成(写操作)。
#define O_NOFOLLOW 0x0100
如果O_NOFOLLOW标记被设置,假如文件是一个符号连接,open调用将会失败。如果在一个有效的文件描述符上设置了这个标志,那么当前文件就不是一个符号连接。
#define O_CREAT 0x0200
如果O_CREAT标记被设置,假如执行open调用的时候文件不存在,那么文件可用被创建。(这个错误的拼写很有趣;when one of the original creators of C was asked "What one thing would you change about C?" he replied, "I would change O_CREAT to O_CREATE!", or at least how the rumor goes)
#define O_TRUNC 0x0400
如果O_TRUNC标记被设置,成功地调用open后文件将被截除。
#define O_EXCL 0x0800
当O_EXCL标记被设置,假如文件已经存在,open调用将产生一个错误。
#define F_GETOWN 5
F_GETOWN命令用于描述符获取当前进程或者进程集收到的SIGIO信号。如果这个值是一个正数,它表示一个进程,负数表示一个进程集。
#define F_SETOWN 6
当IO就绪的时候,F_SETOWN命令用作设置进程或者进程集使其接收SIGIO信号,指定一个进程,用一个正数(一个进程ID)作为fcntl的第三个参数,否则,用一个负数作为fcntl的第三个参数指定进程集。
5.3文件上锁当多个进程试图写同一个文件,将发生什么?它们相互冲突,已知的事情像文件上锁。结果就是每个文件描都有自己的描述符跟偏移量,当每个进程写自己的文件时,偏移量预先独立导致没有进程知道其他的进程也正在执行写操作。最后的文件将因为多个独立写文件的操作使混合后的文件变得相当于垃圾,直接给文件上锁是解决这个问题的一种方式。在任意时刻只能让一个进程能够写到文件,另一种办法是允许在一个叫做高级文件锁的scheme里的文件内部进行区域锁定。 fcntl函数能够提供这个功能,通常来说,锁有两种,一种是写,另一种是写,不同之处在于读锁不会干扰其它进程读取文件,但是特定的区域只能一个写锁存在。
当使用顾问锁的时候,下面的结构用作fcntl的第三个参数。
struct flock {
off_t l_start; /* starting offset */
off_t l_len; /* len = 0 means until end of file */
pid_t l_pid; /* lock owner */
short l_type; /* lock type: */
short l_whence; /* type of l_start */
};
让我们继续讨论每个元素的细节。
l_start这是一个相对于l_whence,单位为字节的偏移量,换句话说,要求的位置实际上是l_whence + l_start
l_len需设置为期望位置的长度,单位为字节,锁将从l_whence + l_start开始锁定l_len字节,如果你想整个文件用一把锁,那么设定l_len的值为0,如果l_len的值是一个负数,结果是不可预测的。
l_pid需要设置为工作在锁上的进程的ID
l_type需要设置为期望的锁的类型,下面是能够使用的值
* F_RDLCK - 读锁定
* F_WRLCK - 写锁定
* F_UNLCK - 用作清除锁定
l_whence这是这个系统调用里面最混乱的部分,这个字段将决定l_start位置的偏移量,需要设为:
* SEEK_CUR - 在当前位置
* SEEK_SET - 在文件开始
* SEEK_END - 在文件末尾
fcntl的命令下面的可用作fcntl的命令
#define F_GETLK 7
F_GETLK 命令尝试检查否能上锁,当使用这个命令,fnctl将检查是否有相冲突的锁,如果存在相冲突的锁,fnctl将改写flock结构,用冲突锁消息通过检查,如果没有相冲突的锁,那么在flock结构最初的信息将被保留,除非l_type字段被设成F_UNLCK
#define F_SETLK 8
F_SETLK命令试图获得flock结构描述的锁,如果锁不被承认,本次调用将不被阻塞。不管怎样,fcntl将直接返回EAGAIN,同时将设置相应的errno,当flock结构的l_type被设置为F_UNLCK时,你能够使用这个命令清除一个锁。
#define F_SETLKW 9
F_GETLK命令试图获得flock结构描述的锁,它将命令fnctl阻塞,直到赋予一个锁
5.4为什么用FLOCK 对大部分情况来说,高级文件锁定机制是有好处的。然而,POSIX.1接口有几个缺点。第一,当一个文件的任何一个文件描述符被关闭时,与该文件关联的所有锁定必须被删除。换句话说,如果你有一个进程打开一个文件,接着它调用一个函数打开同一个文件,读取然后关闭它,这样先前你对这个文件的所有锁定都会被删除。如果你并不确定一个库例程会做什么,这将会引起严重问题。第二,锁定是不会传递给子进程的,所以一个子进程必须独立地创建属于它自己的锁定。第三,所有在一个exec调用之前获得的锁定在由exec启动的进程释放,关闭这个文件或结束之前不会被释放。所以,如果你需要锁定文件的某一部分,那么调用exec不需要释放锁定或者关闭文件描述符,那部分区域将被锁定直到进程结束,你想要的或许不是预期的结果,不论如何,BSD的设计者用许多flock创建了非常简单的优先级文件上锁的接口.
flock用于锁定整个文件,是BSD优先选择的方法,跟fcntl高级锁定相反,flock机制允许锁定进入子进程,使用flock调用的其他好处是可以在文件级并且不在文件描述符级别完成锁定,在某些情况下可用优先选择,意味着多个参照同一个文件的文件描述符,例如DUP()调用,或者多个OPEN()调用,希望每一个参照相同的文件锁,带flock的文件锁跟一个写多个读的fcntl锁很类似,不论如何,当下面的操作被定义,调用flock时,锁的优先级能够被提升:
#define LOCK_SH 0x01 /* shared file lock */
LOCK_SH操作用于在文件上(类似fcntl的读锁)创建共享锁,在一个文件上,多个进程能够共享一把锁。
#define LOCK_EX 0x02 /* exclusive file lock */
LOCK_EX操作用于在文件上创建互斥锁,当互斥锁生效时,文件上不能存在其他的共享锁,包括已共享的锁。
#define LOCK_NB 0x04 /* don't block when locking */
使用这个,flock的调用将阻塞,直到锁定生效,不论如何,假如期望的操作LOCK_NB是ORed,flock的调用将向EWOULDBLOCK返回错误号成功(0)或者失败(1).
#define LOCK_UN 0x08 /* unlock file */
LOCK_UN用于移除文件上的锁
通过调用期望的flock,flock锁的优先级能够被提升或者降低。新的成功的调用将使最近生效的锁替换先前的锁。
DUP函数
int dup(int old);
像fcntl调用能够用作为现有的文件描述符复制描述符一样,dup函数也能复制文件描述符,dup调用能够返回一个跟old参数无法区别的新的文件描述符,这就意味着所有的read(),write()跟lseek()调用都会操作两个描述符(复制出来的新的跟原来的描述符),同时,所有fcntl的选项将保留,close on exec位出外, close on exec位会被关闭,所以你可以复制一个文件描述符,然后允许子进程去调用一个exec函数,这是dup函数非常普遍的一个用法。old参数用作复制,并且必须是一个有效的参数,描述目标描述符,新文件符至少应该是没使用过的文件描述符,它由成功调用dup函数后返回。它意味着如果你关闭STDIN_FILENO(该值为0)那么dup直接调用的新文件描述符的值将是STDIN_FILENO,假如dup函数调用因一些原因失败,将返回-1并且相应地设置errno.
DUP2函数
int dup2(int old, int new);
除了new参数是希望的目标值,dup2函数跟dup函数很相似。假如新参数已经参照了一个有效打开的描述符,并且它的值跟老参数的值不一样,那么新文件描述符将先被关闭。假如新参数等于老参数,那么函数不会做任何操作。成功调用DUP2的返回值将等于新参数。假如DUP2调用失败,将返回-1并且相应地设置errno.
5.5 进程间通信来自于System V的主要的功能是基本的进程间通信,或者说IPC,这些一直在BSD里面被非常广泛地使用。IPC机制允许程序之间相互共享数据。这个很像我们已经介绍过的重定向,但是重定向是单向处理而不是双向处理。在示范程序中,重定向可以跟设置过STDIN_FILENO参数的CAT命令共享数据,但是有一个问题:cat命令不能跟重定向的程序共享数据,假设一个算法是从其他文件描述符读但是用open不灵活的时候,我们可以修改他们使其可以双向地相互共享数据。BSD提供了很多更好的用于进程间通信的方法。
PIPE函数
int pipe(int *array);
通过给定一个有效的二维数组调用pipe函数(如:int array[2]),pipe函数将分配两个文件描述符,假如成功,数组将包含两个有区别的并且允许单向通信的文件描述符, 打开的第一个文件描述符(array[0])用于读,打开的另一(array[1])个用于写,所以,自从成功调用pipe函数后,你能在这两个描述符之间得到一个单向通信通道,当写入其中一个时,你也将能够从另一个读取输出,管道函数比重定向好的地方是你不必使用文件,
这些文件描述符的行为与普通的文件描述符完全一样,然而它们没有任何文件与之关联,管道功能使unix shell加到其他命令的管道变得很有用,例如
bash$ find / -user frankie | grep -i jpg | more
这个例子里面,find命令将其输出通过管道传送给grep命令,grep再将其输出通过管道传送给more命令。当建立这个顺序的时候,shell将处理管道实际的设置,这些程序本身没有写到其他程序的想法,因为他们真的不需要知道。从这个例子你能看见自从调用了管道,正常来说进程就会fork, 之后进程就能通信,假如要达到双向通信的目标,你需要创建两个管道,一个用于父进程到子进程的通信,另一个用作子进程到父进程的通信。
管道有下面两条通信规则:
1.如果管道读的这边关闭了,试图写到这个管道将会导致一个SIGPIPE信号发送给试图写的进程。
2.如果管道写的这边关闭了,试图从这个管道读将导致读返回0或文件结束。关闭写端是发送文件结束到该管道读端的唯一方法。
成功地调用pipe函数后将返回0,假如调用失败,将返回-1并且errno将被相应地设置,
备注:在更多的现代的BSD上,单个描述符上pipe函数支持双向通信,不论如何,这个特性不是很轻便,也因为如此,这个方式不建议使用。
Mkfifo函数
int mkfifo(const char *path, mode_t mode);
在相关的进程间通信时,管道是有用的,然而在没有关联关系的进程间通信时,使用mkfifo函数。Mkfifo函数实际上在文件系统创建一个文件。这个文件只是其他文件在通信时使用的一个标志。这些文件就叫做FIFO管道(先进先出管道)。当一个进程创建一个FIFO管道,写到这个FIFO管道并不会写到这个文件,而是被另一个进程读取。这种行为与管道非常相似,所以FIFO也被叫做命名管道。Mkfifo函数有两个参数。第一个参数是一个以null结尾的字符串,声明路径和文件名。第二个参数是该文件的存取模式。该存取模式是标准的unix文件所有者读写权限(参考/usr/include/sys/stat.h中的S_IRUSR, S_IRGRP等)。
一旦mkfifo函数成功调用,需要用open函数打开创建的fifo管道进行读写。如果调用失败,会返回-1而且错误代码会被相应地设置。
创建fifo管道与创建文件相类似,进程也必须有足够的权限来创建fifo管道,因为该fifo的用户ID会被设置为该进程的有效用户ID,而组ID会被设置为该进程的有效组ID。
关于FIFO重要的一点是,在缺省情况下,它们是阻塞的。因此,读取一个fifo管道会被阻塞,直到另一端把数据写进来,反之亦然。为了避免这种情况,可以使用O_NOBLOCK参数来打开。在这种情况下,你会获得以下行为:对读的调用会立即返回,返回值为0;或者对写的调用会导致一个SIGPIPE信号。
5.6消息队列另一种进程间通信机制,消息队列,为进程间通信提供另一种方式。然而和我提到的其他通信机制不同,你应该尽量避免使用这种方式。如果你的程序使用消息队列,试着用fifo或者甚至Unix Domain Socket重新实现它。在我讨论原因之前,以下是一个快速概述。
消息队列与FIFO相似,但它们使用一个键而不是一个文件作为引用。这个键是一个无符号整数。一旦一个消息队列被创建,发送到这个消息队列的数据会被内核缓存。内核分配给消息队列的内存是有限的。一旦一个消息队列被填满,那么在一个进程从这个消息队列把数据读走之前不能再往这个队列发送数据。在这种情况下,如果两个进程以不同的速度读写,队列是可靠的而且在大多数情况下是非堵塞的。这与FIFO不同。在FIFO的机制中,一个慢的读进程事实上是会拖慢一个比较快的写进程的(除非设置了O_NONBLOCK参数)。另一个好处是即使写进程退出了,写到消息队列中的数据也会被保存起来直到另一个进程读取它。而在FIFO的机制下,如果写进程退出了,这个FIFO会被关闭,而读进程会收到一个文件结束的标志。
以上这些所有的消息队列的好处看起来不错,但让我们再仔细看看。假定一个进程打开一个消息队列,往里面写了一大块数据,填满了内核的缓冲区然后退出,内核将不得不保存这些数据直到另一个进程来读取它,而其他任何想创建和写消息队列的进程将会被拒绝。这种情况将会持续直到一个进程读走这些数据或者系统重启。在这种情况下,创建一个简单的针对消息队列的拒绝服务是可能的。
另一个问题是键并不确保是唯一的。换句话说,一个进程无法确立一种方式来确定它是否是使用一个特定消息队列的唯一进程。而在FIFO机制中,当一个进程创建一个FIFO时,它有比较好的机会知道这个FIFO是否唯一因为可以声明一个针对文件路径的预先约定(比如:/usr/local/myapp/fifo_dir)。你的应用程序可以在安装时创建一个唯一的目录,这样几乎可以确保一个唯一的FIFO路径。有效的消息队列键值可以通过调用一个函数ftok来生成以帮助减少相同的键值,但并不能确保唯一。这个问题的副作用难于确定—你的程序可能读到它并不想读的数据,或者你的程序写入的数据正被其他并不想读取的进程读取。简短来说,当你使用消息队列时,可能导致难于调试的奇怪行为和错误。
如果你仍然坚持使用消息队列,参考以下的指南页:ftok(3), msgget(3), msgctl(3), msgrcv(3), 和msgsnd(3)。
5.7 结论这一章介绍了几个操作打开文件描述符的系统调用,包含在我们fork和exec之前必须关闭文件描述符的场景。我们也讨论了文件锁定,设置和移除文件锁定,以及一些特殊的文件描述符如FIFO和Queue等,在那种情况下根本不在文件系统上存储数据。这些系统调用为BSD增加了很大的可编程性和灵活性。但当一个进程有多个打开的文件描述符时会怎么样?下一章将会讨论高效地处理多个文件描述符。
第六章 高级I/O
翻译:雨丝风片@chinaunix.net
6.1 高级I/O和进程资源正如我们在前面章节中看到的,程序可以同时打开多个文件描述符。这些文件描述符并不一定就是文件,还可以是fifo、pipe或者socket。于是,如何复用这些打开的描述符就很重要了。例如,考虑一个简单的邮件阅读程序,比如pine。它显然应当允许用户在读写email的同时也能去检查是否有新邮件。这就意味着在任一给定时刻都至少能够接收两个来源的输入:一个来源是用户,另一个是用来检查新邮件的描述符。处理描述符的复用是个复杂的问题。一种方法是把所有打开的描述符都标记为非阻塞的(O_NONBLOCK),然后在它们之中循环,直到找到一个可以进行I/O操作的描述符为止。这种方法的问题是程序会一直在循环,如果长时间内没有I/O可用,进程就会一直占据CPU。当有多个进程在一组很少的描述符上循环时,你的CPU的负载就会恶化。
另一种方法就是设置信号处理器去捕获I/O变为可用的事件,然后就让进程进入休眠状态。如果你只打开了少量的描述符,而且并不经常请求I/O的话,这种方法从理论上看倒是不错。由于进程已经休眠,就不会再占用CPU,仅当I/O可用时它才恢复执行。然而,这种方法的问题在于信号处理的开销有点大。比如一个web服务器,每分钟收到100个请求,那就几乎一直都在捕获信号。每秒钟捕获上百个信号的开销是相当大的,不单是进程,对于内核发送信号的开销而言也是一样的。
到目前为止,我们看到的两种选择都有限制,效率也不高,它们需要解决的共同问题就是进程需要知道I/O究竟什么时候能用?然而,这个信息实际上只有内核才能事先知道,因为是内核在最终处理系统中的所有打开的描述符。例如,当一个进程通过fifo向另一个进程发送数据的时候,发送进程会调用write,这是一个系统调用,因此会进入内核。在发送方的write系统调用执行完毕之前接收方对此是一无所知的。于是就引出了一个更好的复用文件描述符的方法:由内核来替进程管理描述符。换句话说,就是把一个打开描述符的链表发送给内核,然后等待,直到内核发现某个或多个描述符已经准备好了或者已经超时了为止。
这就是select()、poll()和kqueue()接口采用的方法。通过这些接口,内核就会管理文件描述符,当I/O可用时就去唤醒进程。这些接口巧妙地处理了上述问题。进程不必再在打开的文件描述符中循环,也不必再去设置信号了。但进程在使用这些函数的时候还是会产生一点小问题。这是因为I/O操作是在从这些接口返回之后才去执行的。所以它至少需要两个系统调用才能完成其操作。例如,你的程序有两个用于读的描述符。你对它们使用select,然后等待它们直至有数据可读。这就需要进程首先调用select,在select返回之后,就对该描述符调用read。更妙的是,你还可以对所有打开的描述符执行一个整体的read。一旦其中有某个描述符准备好读之后,read就会返回,并把数据放在缓冲区中,同时还会给出一个标识,用来指示这个数据是从哪个描述符读进来的。
6.2 select我首先要讲的接口是select()。格式如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
传给select的第一个参数已经造成了多年的混乱。nfds参数的正确用法是把它设成文件描述符的最大值加1。换句话说,如果你有一组文件描述符{0,1,8},nfds参数就应当被设置成9,因为你的描述符的最大值为8。有些人错误地以为这个参数的意思是文件描述符的总数加1,对于我们的例子而言就是4。记住,一个文件描述符只是一个整数而已,所以你的程序就需要指出你所想要在其上select的最大的描述符值。
select接下来会按顺序针对所有尚未完成的读、写以及异常条件检查其余的三个参数,readfds、writefds和exceptfds。(详细信息请参见man(2) select)。注意,如果readfds、writefds和execptfds中没有设置描述符,那么传给select的对应参数应当被设置成NULL。
readfds、writefds和execptfds参数通过以下4个宏进行设置。
FD_ZERO(&fdset);FD_ZERO宏用来对指定的描述符集合中的bit进行清零。有一点需要特别注意:只要使用select,就应当调用这个宏;否则select的行为将是不可预知的。
FD_SET(fd, &fdset); FD_SET宏用于向一组激活的描述符中添加一个描述符。
FD_CLR(fd, &fdset); FD_CLR宏用于从一组激活的描述符中删除一个描述符。
FD_ISSET(fd, &fdset);FD_ISSET宏是在select返回之后使用的,用于测试某个描述符是否已准备好进行I/O操作。
select的最后的参数是一个超时值。如果超时值被设置为NULL,则对select的调用将以不确定的方式被阻塞,直至某个操作已准备好为止。如果你需要一个确定的超时时间,那么超时值就得是一个非空的timeval结构体。timeval结构体如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
如果select调用成功,将返回准备好的描述符的数目。如果select因为超时而返回,则返回值为0。如果有错误发生,则返回-1,同时会相应地设置errno。
6.3 poll我们在这里对I/O的讨论主要是针对BSD的。System V支持一种特殊类型的I/O,即所谓的STREAMS。和socket一样,STREAMS也具有优先级属性,这种属性有时也被成为数据带。数据带可用来给STREAMS中的特定数据设置较高的优先级。BSD最初并不支持这一特性,不过有些人添加了System V仿真功能,可以对某些类型提供支持。由于我们并不关注System V,因此我们只会引用数据带或数据优先级带的概念。详细信息请参见System V STREAMS。
poll函数和select很相似:
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
和原产于BSD的select不同,poll是由System V Unix创建的,在早期的BSD版本中并不支持它。目前主流BSD系统中都已经支持poll了。
和select相似,poll也是在一组给定的文件描述符上进行复用。在指定这些描述符的时候,你必须使用一个结构体数组,其中每个结构体代表一个文件描述符。和select相比,poll的好处就是你可以判断一些很罕见的条件,而select则无法做到。这些条件是POLLERR、POLLHUP和POLLNVAL,我们稍后讨论。尽管对于选择select还是poll的问题已经有了相当多的讨论,但这在很大程度上还是取决于你的个人爱好。poll所使用的结构体是pollfd结构体,如下:
struct pollfd {
int fd; /* which file descriptor to poll */
short events; /* events we are interested in */
short revents; /* events found on return */
};
fdfd成员用于指定你想要poll的文件描述符。如果你想删除一个描述符,那就把那个描述符的fd成员设置成-1。通过这种方法,你可以避免对整个数组进行混洗,同时还可以清除revents成员中列出的所有事件。
events, revents events成员是一个bit掩码,用于指定针对指定描述符所关心的事件。revents成员也是一个bit掩码,但它的值是由poll设置的,用于记录在指定描述符上发生的事件。这些事件的定义如下:
#define POLLIN 0x0001
POLLIN事件表明你的程序将选择该描述符上的可读数据事件。注意,此处的数据不包括高优先级数据,比如socket上的带外数据。
#define POLLPRI 0x0002
POLLPRI事件表明你的程序准备选择该描述符上的任何高优先级事件。
#define POLLOUT 0x0004
#define POLLWRNORM POLLOUT
POLLOUT和POLLWRNOMR事件表明你的程序想知道什么时候可以对一个描述符执行写操作了。在FreeBSD和OpenBSD上这两个事件是相同的;你可以在你的系统头文件(/usr/include/poll.h)中查证这一点。从技术角度来说,它们之间的区别在于POLLWRNOMR仅当数据优先带等于0的时候才去检测是否可以进行写操作。
#define POLLRDNORM 0x0040
POLLRDNORM事件表明你的程序准备选择该描述符上的常规数据。注意,在某些系统上,这个事件指定的操作和POLLIN完全一样。但在NetBSD和FreeBSD上,这个事件和POLLIN并不相同。同样,请去查看你的系统头文件(/usr/include/poll.h)。严格地说,POLLRDNORM仅当数据优先带等于0的时候采取检测是否可以进行读操作。
#define POLLRDBAND 0x0080
POLLRDBAND事件表明你的程序想知道什么时候能够以一个非0的数据带值从该描述符读数据。
#define POLLWRBAND 0x0100
POLLWRBAND事件表明你的程序想知道什么时候能够以一个非0的数据带值向该描述符写数据。
专用于FreeBSD的选项下面的选项是专用于FreeBSD的,知道的和使用的人都不是太多。但它们还是值得提一下,因为它们可以提供更多的灵活性。这些都是新的选项,poll并不保证能够检测这些条件,而且它们只能用于UFS文件系统。如果你的程序需要检测这些类型的事件,那最好使用kqueue接口,我们将在稍后介绍。
#define POLLEXTEND 0x0200
如果文件已经被执行,则设置POLLEXTEND事件。
#define POLLATTRIB 0x0400
如果有任一文件属性发生改变,则设置POLLATTIB事件。
#define POLLNLINK 0x0800
如果文件被重命名、删除或解除链接,则设置POLLNLINK事件。
#define POLLWRITE 0x1000
如果文件内容被修改,则设置POLLWRITE事件。
下面的事件并不是pollfd events成员的有效标志,poll也将忽略它们。它们是在pollfd revents中返回的,用于表明发生了某个事件。
#define POLLERR 0x0008
POLLERR事件表明有错误发生。
#define POLLHUP 0x0010
POLLHUP表明在对应的STREAMS上发生了挂起事件。POLLHUP和POLLOUT是互斥事件,因为一个发生了挂起的STREAMS就不再是可写的了。
#define POLLNVAL 0x0020
POLLNVAL表明对poll的请求是无效的。
poll的最后一个参数是超时值。可以通过这个参数告诉poll一个以微秒为单位的超时值。如果把超时值设置为-1,poll就会阻塞,直至所请求的事件发生为止。如果超时值设置为0,则poll将立即返回。
如果对poll的调用成功,则返回一个正整数。这个正整数的值表示有多少个描述符发生了事件。如果超时,poll将返回0。如果有错误发生,poll则会返回-1。
6.4 kqueue到目前为止,poll和select已经是相当不错的复用文件描述符的方法了。但为了使用这两个函数,你需要创建一个描述符的链表,然后把它们发送给内核,在返回的时候又要再次查看这个链表。这看上去有点效率低下。一个更好一些的模型是把描述符链表交给内核,然后就等待。一旦有某个或多个事件发生,内核就把一个只包含有发生了事件的描述符的链表通知给进程,由此避免了每次函数返回的时候都要去遍历整个链表。尽管对于只打开了几个描述符的进程而言这点改进算不得什么,但对于那些打开了几千个文件描述符的程序来说,这种性能改进就相当显著了。这就是kqueue诞生背后的主要目的。同时,设计者还希望进程能够检测更多类型的事件,比如文件修改、文件删除、信号交付或者子进程退出,并提供一个包含了其它任务的灵活的函数调用。处理信号、复用文件描述符、以及等待子进程等操作都可以封装到这个单一的kqueue接口中,因为它们都是在等待某个事件的发生。
另一个设计考虑就是如何让一个进程毫无干扰地使用多个kqueue实例。如你所见,进程可以设置一个信号处理器,但是,当代码中的其它部分也想捕获那个指定信号的时候该怎么办?或者考虑更坏的情况,比如一个库函数对你的程序想要捕获的信号设置了信号处理器的时候?要想通过调试来找出你的程序为什么没有执行你所设置的信号处理器可能要花费几个小时的时间。不过一般说来,这些情况并不会经常发生。好的程序员应该避免在库函数中设置信号处理器。对于大型的、复杂的程序来说,这些情况就很难避免了,所以为了更完美一点,我们应当能够检测这些事件,而kqueue就可以。
kqueue API由两个函数调用和一个辅助设置事件的宏组成。这些函数将在下面进行简要介绍。
int kqueue(void);
kqueue函数启动一个新的kqueue。如果调用成功,返回值将是一个用来和新创建的kqueue交互的描述符。每个kqueue都有一个与之关联的唯一的描述符。因此,一个程序可以同时打开多个kqueue。kqueue描述符的行为和常规文件描述符类似:它们也可以被复用。
最后一点,这些描述符是不能被fork创建的子进程继承的。如果子进程是通过rfork调用创建的,那就需要设置RFFDG标志,以免这些描述符被子进程共享。如果kqueue函数失败,将返回-1,同时相应的设置errno。
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);
kevent函数用于和kqueue的交互。第一个参数是kqueue返回的描述符。changelist参数是一个大小为nchanges的kevent结构体数组。changelist参数用于注册或修改事件,并且将在从kqueue读出事件之前得到处理。
eventlist参数是一个大小为nevents的kevent结构体数组。kevent通过把事件放在eventlist参数中来向调用进程返回事件。如果需要的话,eventlist和changelist参数可以指向同一个数组。最后一个参数是kevent所期待的超时时间。如果超时参数被指定为NULL,kevent将阻塞,直至有事件发生为止。如果超时参数不为NULL,则kevent将阻塞到超时为止。如果超时参数指定的是一个内容为0的结构体,kevent将立即返回所有当前尚未处理的事件。
kevent的返回值指定了放在eventlist数组中的事件的数目。如果事件数目超过了eventlist的大小,可以通过后续的kevent调用来获得它们。在处理事件的过程中发生的错误也会在还有空间的前提下被放到eventlist参数中。带有错误的事件会设置EV_ERROR位,系统错误也会被放到data成员中。对于其它的所有错误都将返回-1,并相应地设置errno。
kevent结构体用于和kqueue的通信。FreeBSD上的头文件位于/usr/include/sys/event.h。在这个文件中有对kevent结构体的声明,以及其它的一些选项和标志。和select和poll比起来,kqueue还相当的年轻,所以它一直都在发展和添加新的特性。请查看你的系统头文件以确定任何新的或者特定于系统的选项。
原始的kevent结构体的声明如下:
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags;
intptr_t data;
void *udata;
};
现在,让我们来看看各个成员:
identident成员用于存储kqueue的唯一标识。换句话说,如果你想给一个事件添加一个文件描述符的话,ident成员就应当被设置成目标描述符的值。
filterfilter成员用于指定你希望内核用于ident成员的过滤器。
flagsflags成员将告诉内核应当对该事件完成哪些操作和处理哪些必要的标志。在返回的时候,flags成员可用于保存错误条件。
fflagsfflags成员用于指定你想让内核使用的特定于过滤器的标志。在返回的时候,fflags成员可用于保存特定于过滤器的返回值。
datadata成员用于保存任何特定于过滤器的数据。
udataudata成员并不由kqueue使用,kqueue会把它的值不加修改地透传。这个成员可被进程用来发送信息甚至是一个函数给它自己,用于一些依赖于事件检测的场合。
kqueue 过滤器下面列出的是kqueue使用的过滤器。某些过滤器会有专用于它的标志。这些标志是在kevent结构体的fflags成员中设置的。
#define EVFILT_READ (-1)
EVFILT_READ过滤器用于检测什么时候数据可读。kevent的ident成员应当被设成一个有效的描述符。尽管这个过滤器的行为和select或这poll很像,但它返回的事件将是特定于所使用的描述符的类型的。
如果描述符引用的打开文件是一个vnode,该事件就表明读取偏移量尚未到达文件末尾。data成员保存的是当前距文件末尾的偏移量,这可以是负值。如果描述符引用的是一个pipe或者fifo,那么过滤器将在有实际数据可读时返回。data成员保存的是可供读取的字节数目。EV_EOF bit用于表示是哪个写入者关闭了连接。(关于使用socket时EVFILT_READ的行为细节请参见kqueue的手册页。)
#define EVFILT_WRITE (-2)
EVFILT_WRITE过滤器用于检测是否可以对描述符执行写操作。如果描述符引用的是一个pipe、fifo或者socket,则data成员将存有写缓冲区中可用的字节数目。EV_EOF bit表示读取方已经关闭了连接。这个标志对于打开的文件描述符无效。
#define EVFILT_AIO (-3)
EVFILT_AIO用于异步I/O操作,用于检测和aio_error系统调用相似的条件。
#define EVFILT_VNODE (-4)
EVFILT_VNODE过滤器用于检测对文件系统上一个文件的某种改动。把ident成员设置成一个有效的打开文件描述符,用fflags成员指定所关心的事件。返回时,fflags成员将含有所发生事件的比特掩码。这些事件如下:
#define NOTE_DELETE 0x0001
NOTE_DELETE fflag表示进程想知道该文件何时被删。
#define NOTE_WRITE 0x0002
NOTE_WRITE fflag表示进程想知道该文件内容何时被改变。
#define NOTE_EXTEND 0x0004
NOTE_EXTEND fflag表示进程想知道该文件何时被扩展。
#define NOTE_ATTRIB 0x0008
NOTE_ATTRIB fflag表示进程想知道该文件属性何时被改变。
#define NOTE_LINK 0x0010
NOTE_LINK fflag表示进程想知道该文件的链接计数何时被改变。当文件通过link函数调用进行硬链接的时候,它的链接计数就会改变。(详情请参见man(2) link。)
#define NOTE_RENAME 0x0020
NOTE_RENAME fflag表示进程想知道该文件是否被重新命名了。
#define NOTE_REVOKE 0x0040
NOTE_REVOKE fflag表示对文件的访问被revoke了。详情请见man(2) revoke。
#define EVFILT_PROC (-5) /* attached to struct proc */
EVLILT_PROC过滤器被进程用来检测发生在另外一个进程里的事件。所关心进程的PID存储在ident成员中,fflags成员则被设成所关心的事件。返回时,事件将被放在fflags成员中。这些事件由下列事件按比特OR的方式设置:
#define NOTE_EXIT 0x80000000
NOTE_EXIT fflag用于检测该进程何时退出。
#define NOTE_FORK 0x40000000
NOTE_FORK fflag用于检测该进程何时调用fork。
#define NOTE_EXEC 0x20000000
NOTE_EXEC fflag用于检测该进程何时调用exec函数。
#define NOTE_TRACK 0x00000001
NOTE_TRACK fflag让kqueue去跟踪一个跨越fork调用的进程。子进程返回时将设置fflags中的NOTE_CHILD标志,父进程的PID将放在data成员中。
#define NOTE_TRACKERR 0x00000002
当在跟踪子进程的过程中有错误发生时,就会设置NOTE_TRACKERR fflag。这是一个仅用于返回的fflag。
#define NOTE_CHILD 0x00000004
NOTE_CHILD fflag在子进程内设置。这是一个仅用于返回的fflag。
#define EVFILT_SIGNAL (-6)
EVFILT_SIGNAL过滤器用于检测是否有信号发送给该进程。每当有信号发送时这个过滤器就会检测到,并把计数值放在data成员中。这包括设置了SIG_IGN标志的信号。事件将在执行完常规的信号处理过程之后放到kqueue上。注意,这个过滤器将在内部设置EV_CLEAR标志。
#define EVFILT_TIMER (-7)
EVFILT_TIMER过滤器会给kqueue创建一个定时器,用于记录消逝的事件。如果需要一个一次性的定时器,可以设置EV_ONESHOT标志。这个定时器是在ident成员中指定的,data成员用来指定以毫秒为单位的超时时间。返回值放在data成员中。注意,这个过滤器将在内部设置EV_CLEAR标志。
kqueue操作kqueue操作由所需的操作和标志以比特OR的方式进行设置。
#define EV_ADD 0x0001
EV_ADD操作向kqueue添加事件。由于kqueue中不允许出现重复,所以如果你想添加一个已经存在的事件的话,现有事件将被新的添加操作覆盖。注意,在添加事件的时候,它们已经被默认激活了,除非你设置了EV_DISABLE标志。
#define EV_DELETE 0x0002
EV_DELETE操作从kqueue中删除事件。
#define EV_ENABLE 0x0004
EV_ENABLE用于激活kqueue中的事件。注意,新添加的事件默认就是激活的。
#define EV_DISABLE 0x0008
EV_DISABLE禁止kqueue返回某个事件的信息。注意,kqueue并不会删除过滤器。
kqueue操作标志kqueue的操作标志定义如下。它们和上面列出的操作结合使用。它们是通过和所需操作进行比特OR来设置的。
#define EV_ONESHOT 0x0010
EV_ONESHOT标志用于通知kqueue只返回第一个。
#define EV_CLEAR 0x0020
EV_CLEAR标志用于通知kqueue,一旦进程从kqueue中获取到了该事件就将该事件的状态复位。
kqueue返回值仅用于返回的值是放在kevent结构体的flags成员中的。这些值的定义如下:
#define EV_EOF 0x8000
EV_EOF用于表示文件结束的情况。
#define EV_ERROR 0x4000
EV_ERROR用于表示有错误发生了。系统错误将被放到data成员中。
6.5 结论本章研究了BSD中的描述符复用。作为一个程序员,你可以选择三个接口:select、poll和kqueue。对于小数量的描述符来说,这三者的性能差不多,但是当描述符数量很大时,kqueue则是最好的选择。除此之外,kqueue还可以检测比I/O事件更为丰富的条件。它可以检测信号、文件修改以及子进程相关的事件。在下一章中,我们将针对FreeBSD 5.x中的新特性,研究其它的获取子进程信息和当前进程统计信息的方法。
第七章 进程资源和系统限制 译者:
FinalBSD7.1 进程资源和系统限制 为了支持多用户同时登录以及多个应用连接,BSD UNIX系统给系统管理员提供了控制系统资源的许多方法。这种资源限制包括CPU时间、内存使用量以及磁盘使用量。资源控制允许你调整系统到最佳的使用率。UNIX的早期版本中,一些在编译时设置的系统限制如果需要修改,则需要重新编译整个系统。然而,如果并非所有的运行中的系统资源都需要重新编译整个系统,那么现代的BSD系统可以调整大多数这些资源的限制。
本章阐述和进程相关的限制,包括系统端和用户使用的。我们将会看到如何发现这些限制以及怎么修改之,还将阐述进程是如何查询它的资源使用率。
7.2 确定系统限制getrlimit,setrlimit getrlimit允许一个进程查询所受的的系统限制.这些系统限制通过一对硬/软限制对来指定。当一个软限制被超过时,进程还可以继续,当然这取决于限制的类型,同时一个信号会发送给进程。另一方面,进程不可以超过它的硬限制。软限制值可以被进程设置在位于0和最大硬限制间的任意值。硬限制值不能被任何进程降低,仅仅超级用户可以增加之。
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlp);
int setrlimit(int resource, const struct rlimit *rlp);
getrlimit和setrlimit都使用下面的数据结构:
struct rlimit {
rlim_t rlim_cur;
rlim_t rlim_max;
};
我们来看每个成员变量。
rlim_cur为指定的资源指定当前的系统软限制。
rlim_max将为指定的资源指定当前的系统硬限制。
getrlimit和setrlimit函数的第一个参数是资源参数。这个参数用来指定进程获取信息的那个资源。可能的资源值列于下面。你也可以在/usr/include/sys/resource.h中找到它们:
#define RLIMIT_CPU 0 /* cpu time in milliseconds */
RLIMIT_CPU资源限制指定一个进程可以取得CPU执行任务的毫秒数。一般地,一个进程仅仅有一个软限制而没有硬限制。如果超出软限制,进程会收到一个SIGXCPU信号。
#define RLIMIT_FSIZE 1 /* maximum file size */
RLIMIT_FSIZE限制指定一个进程可以创建的最大文件大小,以字节为单位。比如,如果RLIMIT_FSIZE设置为0,那么进程将根本不能创建文件。如果进程超出此限制,就会发出SIGFSZ信号。
#define RLIMIT_DATA 2 /* data size */
RLIMIT_DATA 限制指定一个进程数据段可占据的最大字节值。一个进程的数据段就是放置动态内存的一个区域(C/C++中用malloc()分配的内存)。如果超出限制,分配新内存的操作将会遭到失败。
#define RLIMIT_STACK 3 /* stack size */
RLIMIT_STACK限制指定进程栈可占据的最大字节数。一旦超出硬限制,进程会收到SIGSEV信号。
#define RLIMIT_CORE 4 /* core file size */
RLIMIT_CORE限制指定了进程可以创建的最大core文件的大小。如果此限制设为0,将不能创建。另外,当达到此限制时,所有正在写core文件的进程都将被中断。
#define RLIMIT_RSS 5 /* resident set size */
RMIMIT_RSS限制了进程的常驻集大小(resident set size)可占据的最大字节数.这个进程的常驻集和进程所使用的物理内存数有关。
#define RLIMIT_MEMLOCK 6 /* locked-in-memory address space */
RLIMIT_MEMLOCK限制指定了进程可以使用系统调用到mlock进行锁定的最大字节数。
#define RLIMIT_NPROC 7 /* number of processes */
RLIMIT_NPROC 限制指定了一个指定用户可以开启的最多并发进程数。这里的用户是通过进程来确定的有效用户ID.
#define RLIMIT_NOFILE 8 /* number of open files */
RLIMIT_NOFILE 限制指定了进程可以打开的最多文件数。
#define RLIMIT_SBSIZE 9 /* maximum size of all socket buffers */
RLIMIT_SBSIZE限制指定用户在任何时刻可使用的mbufs数。可以查看socket man页来获得mbufs的定义。
#define RLIMIT_VMEM 10 /* virtual process size (inclusive of mmap) */
RLIMIT_VMEM限制说明一个进程的映射地址空间可以占据的字节数。如果超出限制,分配动态内存和到mmap的调用会失败。
#define RLIM_INFINITY
RLIM_INFINITY宏用来去除对一个资源的限制。换句话说,将一个资源的硬限制设置为RLIM_INFINITY将会导致此种资源的使用没有任何系统限制。 将软限制设置为RLIM_INFINITY将会阻止进程收到任何软限制警告。如果你的进程不想为那些会导致进程在超过软限制时发送信号的资源设置一个信号处理器,这个参数将变得非常有用。
如果使用了getrlimit参数,那么第二个参数需要设置为一个到rlimit结构的有效指针。然后getrlimit会将适当的限制值放入此结构。另外,在改变限制时,setrlimit会使用在第二个参数中设置值。将值设置为0将会阻止使用此资源。将值设置为RLIM_INFINITY会除去对该资源的所有限制。这些函数都在执行成功后都返回0,反之为-1.有任何错误产生,这些函数会相应的设置errno。
getpagesize函数
#include <unistd.h>
int getpagesize(void);
在介绍getrusage函数前,我们需要讨论一下getpagesize函数。一些进程状态 是根据使用的分页(pages)来显示的。分页的内存仅仅是一段内存,通常为4096字节左右。但是分页大小却千差万别,并且它不会固定编入(hard-coded)你的系统。取而代之的是,要确定本地系统的分页大小(pagesize)需要使用getpagesezi函数。getpagesize的返回值就是每个分页使用的字节数。
7.3 确定进程资源使用量getrusage函数 现在我们知道如何查看系统限制,我们还需要知道如何确定当前进程资源的使用量。getrusage函数就是用于此目的。此函数很容易被理解。一个进程可以确定它的内存使用量、CPU执行时间、甚至可获得其子进程的相关信息。因此,getrusage函数的一个用途就是帮助系统避免逃逸进程(runaway)的出现。逃逸进程指的是那些不受系统控制的进程,它们或者使用了过多的CPU(比如循环调用)、或者是超过了内存使用量的限制(导致内存泄漏)。
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#define RUSAGE_SELF 0
#define RUSAGE_CHILDREN -1
int getrusage(int who, struct rusage *rusage);
getrusage函数有两个参数。第一个参数可以设置为RUSAGE_SELF或者RUSAGE_CHILDREN。如果设置成RUSAGE_SELF,那么将会以当前进程的相关信息来填充rusage(数据)结构。反之,如果设置成RUSAGE_CHILDREN,那么rusage结构中的数据都将是当前进程的子进程的信息。
rusage(数据)结构定义在/usr/include/sys/resource.h中。它含有以下成员变量:
struct rusage {
struct timeval ru_utime; /* user time used */
struct timeval ru_stime; /* system time used */
long ru_maxrss; /* max resident set size */
long ru_ixrss; /* integral shared memory size */
long ru_idrss; /* integral unshared data */
long ru_isrss; /* integral unshared stack */
long ru_minflt; /* page reclaims */
long ru_majflt; /* page faults */
long ru_nswap; /* swaps */
long ru_inblock; /* block input operations */
long ru_oublock; /* block output operations */
long ru_msgsnd; /* messages sent */
long ru_msgrcv; /* messages received */
long ru_nsignals; /* signals received */
long ru_nvcsw; /* voluntary context switches */
long ru_nivcsw; /* involuntary " */
};
我们来详细分析每一个成员变量。
ru_utime,ru_stime ru_utime和ru_stime成员变量包含了在用户模式和系统模式中执行时间的总和。它们都使用timeval结构(请查看前一章来了解此结构。)
ru_maxrss ru_maxrss保存了常驻集的内存使用数。其值根据内存分页的使用来确定。
ru_ixrss ru_ixrss值指文本段(text segment)使用的内存数乘以执行滴答数。
ru_idrss ru_idrss 值指进程所使用的私有内存数(KB)乘以执行滴答数来。
ru_isrss ru_isrss 指栈使用的内存数(KB为单位)乘以执行滴答数。
ru_minflt ru_minflt值指不需要I/O的页缺失数。页缺失发生在内核需要得到一个内存页以供进程访问时。
ru_majflt ru_majflt值指需要I/O的页缺失数。页缺失发生在内核需要得到一个内存页以供进程访问时。
ru_nswap 有时,一个进程会被调出内存,以提供空间给其他进程使用。ru_nswap指的就是一个进程将要调出内存的次数。
ru_inblock ru_inblock 指文件系统需要为一个读请求执行输入操作的次数。
ru_oublock ru_oublock指文件系统需要为一个写入请求执行输出操作的次数。
ru_msgsndru_msgsnd指发送的IPC信息总数
ru_msgrcv ru_msgrcv指收到的IPC信息总数。
ru_nsignals ru_nsignals指进程收到的信号总数。
ru_nvcsw 一个进程主动上下文且混总数。主动上下文切换发生在一个进程放弃它的CPU分时时。通常发生在一个进程等待某可用资源时。
ru_nivcsw ru_nivcsw包含了因高优先级进程运行导致的上下文切换总数。
7.4 小结本章主要阐述了程序如何得到系统限制。这些限制不应该固定写入你的代码。取而代之的是,你的程序应该使用这些接口。这是因为这些限制是与平台相关的,甚至不同系统间都会有差别。比如,系统管理员可以调整允许打开的最大文件数,因此如果你需要增加或者减小这个值,就不能将值4096固定写入程序中。另一个例子是分页大小。一些64位系统使用8096作为默认的分页大小,但是大多数的32位系统将此默认值设置位4096。再次强调,这是个可调整的参数,不应该使用一个固定的值。
我们还学习了一个程序如何得到其资源的当前使用量,或者查看其子进程的资源使用状况。使用这些接口可以帮助我们检测甚至避免出现失控的进程。它们用在程序试图超越其软/硬限制时调试错误也非常不错。
第八章 FreeBSD 5.x 译者:
FinalBSD8.1 FreeBSD 5.x 2003年1月发布的FreeBSD-5.x 分支,是FreeBSD项目的一个重要的里程碑。在近3年的开发中,FreeBSD不管是在内核还是基本系统上都有了许多的变化。大部分的这些变化会影响到系统管理员,FreeBSD 编程者则不会,因此也不会影响到此书中讨论的任何一部分。一些例外将在下面进行阐述。
8.2 启动布局(Boot Layout) 第一个变化是启动文件的组织方式。FreeBSD 5.x系统已将所有的模块和内核文件都移到/boot目录下。和老版本的FreeBSD一样,一些和系统启动相关的配置文件也位于此目录下。这个看起来很小的变化实际上提供了更多的便利,因为现在,在不同的设备间分离/和/boot(包含所有的内核和内核模块)十分容易。
8.3 Devfs FreeBSD 5.x中,我们最喜欢的特性是devfs.之前的版本,/dev被塞满了超过1000个文件。设备节点和大多数支持的设备都有一项作为文件保存在该目录下。如你所想,这个目录变得非常的大并且包含了许多不必要的文件。比如,一个有IDE设备的系统会在此目录下包含SCSI设备文件,哪怕是该系统没有任何SCSI设备。devfs中这一切弊病都之前将不复存在.
现在/dev下仅仅包含那些真实存在的设备项。实际上,FreeBSD 5.x并不和之前版本一样将/dev当作到文件系统的一个挂载点,并称之为devfs。
devfs文件系统和proc文件系统相近。二者都是挂载点,该挂载点包含在硬盘上不存在的文件。这些文件由内核创建并且仅以文件方式出现。事实上,它们是在系统启动后被建立的。Devfs提供了更多的便利性,因为你能支持多个devfs挂载点。比如,如果你想chroot或者jail一个进程,你不需要手动创建/dev目录,取而代之的是,你可以简单的为你的进程创建一个新的挂载点并挂载devfs.
devfs的另一个优点是可以告诉你系统真实存在哪些设备。你要做的仅仅是cd到/dev目录然后列出那些文件。这就很方便用户得到系统上所有的设备列表,更重要的是,检测到哪些设备。
8.4 a.out FreeBSD 5.x系列已经在基本系统中去除了对a.out二进制格式的支持。但是你仍然可以加入a.out二进制支持。这是因为a.out是种相当老的格式,并且现在都会优先选择新的ELF格式。ELF格式更灵活而且目前被广泛使用。
8.5 gcc-3.2工具链 FreeBSD 5.x 现在使用gcc-3.2 工具链作为基本系统。这是个很重要的改变:gcc-3.x更接近ISO,并且它的C++ ABI更稳定。然而,这或许会给一些人带来麻烦。他们编写的一些程序在使用gcc-3.x进行编译前也许需要进行更新。如果你使用flex或者yacc,请确定你使用的是最新的版本,或者为你当前的版本打好补丁,因为已经确认知道他们会导致问题出现。
8.6 SMPng FreeBSD 5.x 已经改进为支持SMP的系统了,这一改进都来自于我们常说的SMPng(下一代SMP).尽管之前版本也支持SMP,但是性能有待提高。
8.7 内核调度实体(KSE) 另一个新的特性是内核调度实体(KSE).KSE是个内核支持的线程系统,和Scheduler Activations在概念概念上很接近。特别的,在内核端,KSE在于对FreeBSD的调度的修改。并且在用户端使用的是POSIX线程实现方式,这种方式会利用内核提供的额外工具。然而,你不需要配置任何特殊的内核参数,就可以编译得到一个具有KSE相关修改的内核。
为了在应用程序中使用KSE,你可以使用libpthreads来链入之。libpthreads默认并没编译进系统,所以你首先需要在系统上安装好libpthreads。然后,在它的makefile中,将-pthread选项改为-lpthread并重新链入(relink).
8.8 小结 FreeBSD在几年的发展中已经变得成熟,并且现在是一个可用的非常稳定的操作系统了。有了SMP的增强支持和内核线程,FreeBSD会一如既往的提供强稳定性和高性能。已经加入了对一些新平台的支持,比如Sparc64和ia64.这些新的平台会帮助BSD发行版多年来继续提供高质量的开源选择。
公元二00六年二月十三日