2007年8月30日星期四
2007年8月26日星期日
QEMU简单模拟Plan9
2007年8月18日星期六
2007年8月15日星期三
2007年8月13日星期一
About Plan9 Shell-rc
I plan to translate some information of Plan9,I use the key of "Plan9翻译“ from Google search,that I found "寒蝉退士" had translated "rc-the Plan9 Shell".
1 介绍
rc 在精神上类似于 UNIX 的 Bourne shell 但在细节上有很多不同。本文使用很多小例子和一些大点的例子来描述 rc 的首要特征。假定读者熟悉 Bourne shell。
2 简单命令
对于最简单的使用,rc 有着 Bourne-shell 用户很熟悉的语法。下列命令都会如愿而行:
datecat /lib/news/buildwho >user.nameswho >>user.nameswc fileecho [a-f]*.cwho wcwho; datevc *.c &mk && v.out /*/bin/fb/*rm -r junk echo rm failed!
3 引用
包含一个空格或 rc 的其他语法字符之一的参数必须包围在单引号(')之中:
rm 'odd file name'
在已被引号括起来的参数中的单引号必须是双重的:
echo 'How''s your father?'
4 模式
包含字符 * ? [ 中任何一个的未被引号括起来的参数都是同文件名字匹配的模式。字符 * 匹配字符的任意序列,字符 ? 匹配任何单一字符,而 [class] 匹配在 class 中的任意字符,除非 class 的第一个字符是 ~,在这种情况下取这个类的补集。class 也可以包含用 - 分隔的成对的字符,它表示词法上在两者之间的所有字符。字符 / 在模式中必须显式的出现,路径名分量 . 和 .. 也是如此。模式被替代为一个参数的列表,每个匹配的路径名字都是一个参数,作为例外,没有匹配的名字的模式不被替代为空列表;而是保持为自身。
5 变量
UNIX 的 Bourne shell 提供字符串值的变量。rc 提供的变量值是参数的列表 - 就是说,字符串的数组。这是在 rc 和传统的 UNIX 命令解释器之间的原则性区别。变量可以通过键入来给出值,例如:
path=(. /bin)user=tdfont=/lib/font/bit/pelm/ascii.9.font
圆括号指示赋予 path 的值是两个字符串的一个列表。变量 user 和 font 被赋予包含一个单一字符串的列表。
通过在变量的名字前面前导一个 $ 可以把变量的值替换到一个命令中,比如:
echo $path
如果 path 已经做如上设置,它将等价于
echo . /bin
变量可以有数字或数字的列表作为下标,比如:
echo $path(2)echo $path(2 1 2)
它们等价于
echo /binecho /bin . /bin
在左圆括号和变量名字之间必须没有空格分隔;否则,这些下标将被当作一个独立的圆括号中的列表。
在变量中的字符串的数目可以用 $# 操作符确定。例如,
echo $#path
对于上面的例子将输出 2。
下面的两个赋值有着微妙的不同:
empty=()null=''
第一个设置 empty 为不包含字符串的一个列表。第二个设置 null 为包含一个单一的字符串的一个列表,但是这个字符串不包含字符。
尽管它们或多或少是同样的东西(在 Bourne 的 shell 中,它们是不可区分的),它们在几乎所有情况下都表现的很不同。在其他东西当中
echo $#empty
打印 0,而
echo $#null
打印 1。
所有未设置的变量都有值 ()。
偶尔的,把变量的值作为一个单一的字符串是方便的。通过 $" 操作符把字符串的所有元素联接到一个单一字符串中,在元素之间带有空格。这样,如果我们设置
list=(How now brown cow)string=$"list
则
echo $list
和
echo $string
二者导致相同的输出,就是:
How now brown cow
而
echo $#list $#string
将输出
4 1
因为 $list 有四个成员,而 $string 有一个单一的成员,带有三个空格分隔它的单字。
6 参数
当 rc 从一个文件读取它的输入的时候,这个文件有权访问在 rc 的命令行上提供的参数。变量 $* 最初被赋值为参数的列表。名字 $1、$2 等是 $*(1)、$*(2) 等的同义字。此外,$0 是一个文件的名字,rc 的输入从其中读入。
7 联接
rc 有一个字符串联接操作符,插入符 ^,用来建造由多个片断组成的参数。
echo hully^gully
完全等价于
echo hullygully
假定变量 i 包含命令的名字。则
vc $i^.cvl -o $1 $i^.v
可以编译这个命令的源代码,把结果留在适当的文件中。
联接可以应用于列表之上。下面的
echo (a b c)^(1 2 3)src=(main subr io)cc $src^.c
等价于
echo a1 b2 c3cc main.c subr.c io.c
在细节上,规则是: 如果 ^ 的两个操作数(operand)是有相同的非零个字符串的列表,则成对的连接它们。否则,如果操作数之一是一个单一的字符串,它依次与另一个操作数的每个参数联接。操作数的任何其他组合都是错误的。
8 无约束的插入符
用户需求决定了 rc 在特定位置插入插入符,来使语法看起来更像 Bourne shell。例如,下面的:
cc -$flags $stems.c
等价于
cc -^$flags $stems^.c
一般的,rc 会在不由空白分隔的两个参数之间插入 ^。特别是,只要 $ ' ` 跟随着一个被引号括起来的或未被引号括起来的字,或者一个未被引号括起来的字跟随着一个被引号括起来的字并且无空白或 Tab 介于其间,就在二者之间插入一个隐含的 ^。如果立即跟随着一个 $ 的一个未被引号括起来的字包含不是一个字母下划线或 * 的字符,在第一个这样的字符之前插入 ^。
9 命令替换
通常用来从一个命令的输出建造一个参数列表。rc 允许一个命令,包围在花括号中并前导着一个反引号 `{...},出现在需要参数的任何地方。执行这个命令并捕获它的标准输出。使用存储在变量 ifs 中的字符来把输入分解到参数中。例如,
cat
`{ls -trsed 10q}
将按时间次序连接当前目录中十个最旧的文件,假定缺省 ifs 设置为空格、Tab 和换行符。
10 管道线分支
一般的管道表示法对于几乎所有情况都是足够用了。有时后拥有非线性的管道线是有用的。比树状结构更一般化的管道线拓扑结构可能要求任意大的管道缓冲区,甚至更坏、导致死锁。rc 拥有某种非线性的树状管道线的语法。例如,
cmp <{old} <{new} 将回归测试一个命令的新版本。跟随着在花括号中的命令的 < 或 >,导致运行这个命令并把它的标准输出或输入连结到一个管道上。父命令(本例中的 cmp)开始于连结到某个文件描述符或其他东西上的管道线的另一端,并带有在打开的时候会连接到管道的一个参数(例如,/dev/fd/6)(译注:一个命名管道的名字)。一些命令不准备处理关闭就不可搜寻的输入文件。例如 diff 需要读取它的输入两次。
11 退出状态
在命令退出的时候,它向执行它的进程返回一个状态。在 Plan 9 中,状态是描述错误状况的一个字符串。在正常终止的使用它是空的。
rc 把命令退出状态捕获到变量 $status 中。对于一个简单命令 $status 的值就如同上面描述的一样。对于一个管道线 $status 被设置为管道线构件的状态的联接、并带有 字符作为分隔符。
rc 有多种控制流,其中很多控制流以前面执行的命令返回的状态作为条件。所有只包含 0 和 的 $status 拥有逻辑值 true。任何其他状态都是 false。
12 命令组合
包围在 {} 中的命令序列可以用在需要命令的任何地方。例如:
{sleep 3600;echo 'Time''s up!'}&
将在后台等待一小时,接着打印一个消息。不带花括号,
sleep 3600;echo 'Time''s up!'&
将锁住终端一个小时,接着在后台打印这个消息。
13 控制流 - for
为键入的列表中的每个成员执行一次命令,例如:
for(i in printf scanf putchar) look $i /usr/td/lib/dw.dat
它在给定文件中查找单字 printf、scanf 和 putchar 中的每一个。一般形式是
for(name in list) command
或
for(name) command
在第一种情况下为 list 的每个成员执行一次 command,并把这个成员赋予变量 name。如果缺少子句“in list”,假定为“in $*”。
14 条件执行 - if
rc 还提供一个通用 if 语句。例如:
for(i in *.c) if(cpp $i >/tmp/$i) vc /tmp/$i
在 cpp 处理而没有错误的每个 C 源程序上运行 C 编译器。‘if not’语句提供双尾(two-tailed)条件。例如:
for(i){ if(test -f /tmp/$i) echo $i already in /tmp if not cp $i /tmp}
它是在 $* 中的每个文件之上的循环,把不存在于 /tmp 中的文件复制到这里,并对那些已经存在于这里的文件打印一个消息。
15 控制流 - while
rc 的 while 语句看起来如下:
while(newer subr.v subr.c) sleep 5
它等待直到 subr.v 比 subr.c 新,原因大概是 C 编译处理完了它。
如果控制命令是空的,循环将不会终止。这样,
while() echo y
将模拟 yes 命令。
16 控制流 - switch
rc 提供一个 switch 语句来在任意字符串上做模式匹配。它的一般形式是
switch(word){case pattern ... commands case pattern ... commands ...}
Rc 依次尝试对在每个 case 语句中的模式去匹配这个 word。除了 / 和 . 和 .. 不需要被显式的匹配之外,模式与用于文件名匹配的一样。
如果有任何模式匹配,执行紧随这个 case 之后直到下一个 case 之前(或 switch 的结束处)的命令,并且这个 switch 的执行完成。例如,
switch($#*){case 1 cat >>$1case 2 cat >>$2 <$1case * echo 'Usage: append [from] to'} 是一个 append 命令。调用时带有一个文件参数,它把它的标准输入添加到指名的文件中。带有两个参数时,把第一个文件添加到第二个文件中。任何其他参数数目引发一个错误消息。 内置 ~ 命令也匹配模式,并且经常比 switch 更加简洁。它的参数是一个字符串和一个模式的列表。只有在所有模式都匹配这个字符串的时候,设置 $status 为真。下列例子为 man(1) 命令处理选项参数: opt=()while(~ $1 -* [1-9] 10){ switch($1){ case [1-9] 10 sec=$1 secn=$1 case -f c=f s=f case -[qwnt] cmd=$1 case -T* T=$1 case -* opt=($opt $1) } shift} 17 函数
可以通过如下键入来定义函数
fn name { commands }
此后,在遇到叫做 name 的命令的时候,命令的参数列表的余下部分将赋给 $* ,接着 rc 将执行 commands。在完成的时候恢复 $* 的值。例如:
fn g { grep $1 *.[hcyl]}
定义 g pattern (用来查找在当前目录中的所有程序源文件中查找 pattern 的出现)。
通过写
fn name
而没有函数体来删除函数定义。
18 命令执行
rc 做许多事情来执行一个简单命令。如果这个命令名字是使用 fn 定义的一个函数的名字,则执行这个函数。否则,如果它是一个内置命令的名字,rc 直接执行这个内置命令。否则,查找在变量 $path 提及的目录,直到找到一个可执行文件。在 Plan 9 中不鼓励 $path 变量的广泛使用。转而,使用缺省(. /bin) 和把你需要的绑定入 /bin。
19 内置命令
许多命令由 rc 内部执行,原因是不这样就难于实现。
. [-i] file ...
执行来自 file 的命令。在此期间 $* 设置为紧随 file 之后的参数列表的余下部分。使用 $path 来查找 file。选项 -i 指示交互式输入 在读取每个命令之前打印一个提示(在 $prompt 找到)。
builtin command ...
除了忽略叫做 command 的任何函数之外,正常的执行 command。例如,
fn cd{ builtin cd $* && pwd}
定义 cd 内置命令(见后)的一个替代者,它宣布新目录的全名。
cd [dir]
改变当前目录到 dir。缺省参数是 $home。$cdpath 是在其中查找 dir 的位置的一个列表。
eval [arg ...]
参数被联接(由空格分隔)到一个字符串中,读为给 rc 的输入并执行它。例如,
x='$y'y=Doodyeval echo Howdy, $x
将回显
Howdy, Doody
因为在替换了 $x 之后,eval 的参数是
echo Howdy, $y
exec [command ...]
Rc 用给定命令替代自身。有点像一个 goto - rc 不等待命令退出,并且不回来读更多的命令。
exit [status]
Rc 立即退出并返回给定状态。如果未给出,则使用 $status 的当前的值。
flag f [+-]
这个命令操纵并测试命令行标志(后面描述).
flag f +
设置标志 f。
flag f -
清除标志 f。
flag f
测试标志 f,恰当的设置 $status。所以
if(flag x) flag v +
如果已经设置了 -x 标志则设置 -v 标志。
rfork [nNeEsfF]
它使用 Plan 9 rfork 系统入口来把 rc 放置到带有下列属性的新进程组之中:
标志
名字
功能
n
RFNAMEG
制做父名字空间的一个复本
N
RFCNAMEG
开始于一个新的空名字空间
e
RFENVG
制做父环境的一个复本
E
RFCENVG
开始于一个新的空环境
s
RFNOTEG
制做一个新的通知组
f
RFFDG
制做父文件描述符空间的一个复本
F
RFCFDG
开始于一个新的空文件描述符空间
程序员手册的 fork(2) 章节详细描述这些属性。
shift [n]
删除 $* 的前 n(缺省为 1)个元素。
wait [pid]
等待有给定 pid 的进程退出。如果未给出 pid, 等待所有未完结的进程。
whatis name ...
以适合输入到 rc 的形式打印每个名字的值。输出的是到变量的赋值,函数的定义,到对一个内置命令的 builtin 的调用,或一个二进制程序的路径名字。例如,
whatis path g cd who
可能打印
path=(. /bin)fn g {gre -e $1 *.[hycl]}builtin cd/bin/who
~ subject pattern ...
依次针对每个 pattern 匹配 subject。在匹配时,$status 设置为真。否则,设置为‘不匹配’。模式同于文件名匹配的。在执行 ~ 命令之前模式不受文件名替代的支配,所以它们不需要被包围在引号之中,当然,除非想要的是对 * [ or ? 的一个文字上的匹配。例如
~ $1 ?
匹配任何单一字符,而
~ $1 '?'
只匹配一个文字问号。
20 高级 I/O 重定向
rc 允许在 < 或 > 之后的方括号 [ ] 中指定文件描述符,来做不是 0 和 1(标准输入和输出)的文件描述符的重定向。例如,
vc junk.c >[2]junk.diag
把编译器的诊断从标准错误保存到 junk.diag 中。
通过如下键入,这些文件描述符可以被替代为一个已经打开了的文件的 dup(2) 意义上的复制:
vc junk.c >[2=1]
它用文件描述符 1 的一个复件替换文件描述符 2。在与其它重定向联合时非常有用,比如
vc junk.c >junk.out >[2=1]
从左至右求值重定向,所以重定向文件描述符 1 到 junk.out,接着把文件描述符 2 指向相同的文件。与之相对,
vc junk.c >[2=1] >junk.out
重定向文件描述符 2 到文件描述符 1(大概是终端), 并接着定向文件描述符 1 到一个文件。在第一种情况下,标准和诊断输出将被混合到 junk.out 中。在第二种情况下,诊断输出将出现在终端上,而标准输出将发送到文件。
可以使用带有右手边为空的复制记号(notation)关闭文件描述符。例如,
vc junk.c >[2=]
将丢弃来自编译的诊断。
通过如下键入,可以派遣(send)任意的文件描述符经过管道,
vc junk.c [2] grep -v '^$'
它从 C 编译器的错误输出中删除空行。注意 grep 的输出仍出现在文件描述符 1 上。
有时你可能希望把一个管道的输入端连接到不是 0 的某个文件描述符。记号
cmd1 [5=19] cmd2
建立一个管道线,并且 cmd1'的文件描述符 5 通过管道连接到 cmd2'的文件描述符 19。
21 立即文档
rc 过程可以包含叫做“立即文档”数据,它被提供为给命令的输入,比如在下面这版本的 tel 命令中
for(i) grep $i <22>捕获通知
在接收到来自终端的中断的时候,rc 脚本通常终止。以通常的方式定义带有小写的一个 UNIX 信号名字的函数,但在 rc 收到对应的通知的时候调用。程序员手册的 notify(2) 章节详细的讨论了通知。有意思的通知有:
sighup
通知是‘hangup’。Plan 9 在终端从 rc 断开的时候发送这个通知。
sigint
通知是‘interrupt’,通常在终端上键入中断字符(ASCII DEL)的时候发出。
sigterm
通知是‘kill’,通常由 kill(1) 发送。
sigexit
在 rc 将要退出的时候发送的人为通知。
作为一个例子,
fn sigint{ rm /tmp/junk exit}
为键盘中断设置一个陷入来在退出之前删除临时文件。
如果通知例程被设置为 {} 则忽略通知。在删除它的处理器(handler)定义的时候信号恢复它们的缺省行为。
23 环境
环境是对于执行的二进制程序可以获得的名字-值对的一个列表。在 Plan 9 上,环境存储在叫做 #e 的文件系统中,它通常挂装在 /env 上。每个变量的值都保存在一个单独的文件中,都带有终止于 0 字节的分量(component)。(文件系统完全维持在内存中,所以不涉及磁盘或网络。) /env 的内容在一个每进程(per-process)组基础上共享 - 在建立一个新进程组的时候,把 /env 有效的连结到一个新文件系统上,它被初始化为旧文件系统的复件。这种组织的一个结果是命令可以改变环境条目并看到改变反射到 rc 中。
函数也出现在环境中,通过对它们的名字前导 fn# 来命名,如 /env/fn#roff。
24 局部变量
在一个单一命令期间设置一个变量经常是有用的。一个赋值紧随一个命令有这种效果。例如
a=globala=local echo $aecho $a
将打印
localglobal
这对复合命令也有效,如
f=/fairly/long/file/name { { wc $f; spell $f; diff $f.old $f } pr -h 'Facts about '$f lp -dfn}
25 例子 - cd, pwd
下面的这对函数提供标准 cd 和 pwd 命令的增强版本。(为此感谢 Rob Pike。)
ps1='% ' # default prompttab=' ' # a tab characterfn cd{ builtin cd $1 && switch($#*){ case 0 dir=$home prompt=($ps1 $tab) case * switch($1) case /* dir=$1 prompt=(`{basename `{pwd}}^$ps1 $tab) case */* ..* dir=() prompt=(`{basename `{pwd}}^$ps1 $tab) case * dir=() prompt=($1^$ps1 $tab) } }}fn pwd{ if(~ $#dir 0) dir=`{/bin/pwd} echo $dir}
函数 pwd 是标准 pwd 的一个增强版本,它在变量 $dir 中缓冲它的值,因为真正的 pwd 可能执行起来非常慢。(最新版本的 Plan 9 已经有了非常快的 pwd 实现,这减低了 pwd 函数的优势。)
函数 cd 调用 cd 内置命令,并检查它是否成功。如果成功,它设置 $dir 和 $prompt。提示将包含当前目录的最后的成员(除非在 home 目录中,在这里它将是空),并且 $dir 将被重置为正确的值或者是 (),这样 pwd 函数将正确的工作。
26 例子 - man
man 命令打印程序员手册的页面。例如,下面的调用
man 2 sinhman rcman -t cat
在第一种情况,打印章节 2 中的 sinh 页面。在第二种情况,打印 rc 的手册页。因为未指定章节,在所有章节中查找这个页面,在章节 1 中找到了它。在第三种情况,排版 cat 的页面(使用了 -t 选项)。
cd /sys/man { echo $0: No manual! >[1=2] exit 1}NT=n # default nroffs='*' # section, default try allfor(i) switch($i){case -t NT=tcase -n NT=ncase -* echo Usage: $0 '[-nt] [section] page ...' >[1=2] exit 1case [1-9] 10 s=$icase * eval 'pages='$s/$i for(page in $pages){ if(test -f $page) $NT^roff -man $page if not echo $0: $i not found >[1=2] }}
注意使用了 eval 来制作候选手册页的一个列表。不使用 eval,存储在 $s 中的 * 将不触发文件名匹配 - 它被包围在引号中,并且即使不这样,它将在赋值给 $s 的时候被展开。 eval 导致它的参数被 rc 的分析器和解释器重新处理,有效的延迟 * 的求值直到赋值给 $pages.
27 例子 - holmdel
下列 rc 脚本玩一个假装的简单游戏 holmdel,在其中游戏者交替的指名 Bell Labs 的位置,第一个提及 Holmdel 的获胜。
t=/tmp/holmdel$pidfn read{$1=`{awk '{print;exit}'}}ifs='' # just a newlinefn sigexit sigint sigquit sighup{rm -f $texit}cat <<'!' >$tAllentown AtlantaCedar CrestChesterColumbusElmhurstFullertonHolmdelIndian HillMerrimack ValleyMorristownNeptunePiscatawayReadingShort HillsSouth PlainfieldSummitWhippanyWest Long Branch!while(){ lab=`{fortune $t} echo $lab if(~ $lab Holmdel){ echo You lose. exit } while(read lab; ! grep -i -s $lab $t) echo No such location. if(~ $lab [hH]olmdel){ echo You win. exit }}
这个脚本值得详细描述(至少它不蠢。)
变量 $t 是对临时文件名字的一个简写。在这个脚本的多个实例同时运行的情况下,在临时文件名字包括 $pid 能确保它们的名字不会冲突,rc 把 $pid 初始化为它的进程-id。
函数 read 的参数是一个变量的名字,把从的标准输入读取来的行汇集到其中。$ifs 被设置未只有一个换行符。这样 read 的输入不会在空格处被分离,但是终止的换行被删除了。
设置了一个处理器来捕获 sigint、sigquit 和 sighup,还有一个人工的 sigexit 信号。它们只是删除临时文件并退出。
从包含 Bell Labs 位置的一个列表的立即文档来初始化临时文件,接着主循环开始。
首先,程序使用 fortune 程序从位置列表中挑出随机的一行来猜测一个位置(在 $lab 中),如果它猜到 Holmdel,打印一个消息并退出。
接着它使用 read 函数来从标准输入得到一些行,并对它们做有效性检查直到得到一个合法的名字。注意 while 的条件部分可以是一个复合命令。只检查在序列中最后一个命令的退出状态。
最后,如果结果是 Holmdel,打印一个消息并退出。否则它回到循环的顶部。
28 设计原理
Rc 在很大程度上吸取了 Steve Bourne 的 /bin/sh。任何 Bourne shell 的后继者都纳入此类比较。我尝试着去修补它最周知的缺点,并通过省略无关紧要的特征近可能的去简化事情。只在遇到不可抗拒的诱惑的时候我才介入新颖的想法。明显的,我广泛的修补了 Bourne 的语法。
在 rc 的设计中的最重要的原理是它不是一个宏处理器。词法和语法分析代码从不扫描输入多于一次(当然 eval 命令是个例外,它的存在就是为了打破这个规则)。
通过把包含空格的参数传递给 Bourne shell 脚本,它们经常被粗暴的运行。经常在不恰当的时候使用 $IFS 把它们分割到多个参数中。在 rc 中,变量的值,包括命令行参数,在替换到命令中的时候是不重新扫描的。参数大概已经在父进程中扫描过了,并且不应当被重新扫描。
为什么 Bourne 在变量替换之后重新扫描命令? 它需要能够在值是字符串的变量中存储参数列表。如果我们除去了重新扫描,我们必须改变变量的类型,这样它们可以显式的承载字符串的列表。
这介入了一些概念上的复杂性。我们需要对字的列表的表示法。有两种不同的联接,对字符串的 - $a^$b, 和对列表的 - ($a $b)。 在 () 和 '' 之间的区别对初学者是容易混淆的,尽管差别是确切明显的 - 一个空参数和没有参数是不一样的。
Bourne 在做命令替换的时候也需要重新扫描输入。这是因为包围在反引号之中的文本不是字符串而是一个命令。严格的说,在包围着命令是嵌套的命令替换时候必须分析它,但是这使处理它很困难,比如:
size=`wc -l \`ls -tsed 1q\``
内部的反引号必须被转义来避免终止外部的命令。比上面的例子更糟的是,需要的转义数目是嵌套深度的指数。rc 通过把后引用改为参数是命令的一个一元操作符来修正这个问题,例如:
size=`{wc -l `{ls -tsed 1q}}
不需要转义,并且在一遍之中分析所有事情。
出于类似的原因,rc 定义信号处理器如同它们是函数,而不是如同 Bourne 那样为每个信号关联上一个字符串,伴有在键入中断字符时得到语法错误作为响应的可能性。因为 rc 在键入的时候分析输入,它在你制作它们的时候就报告错误。
解决了所有这些麻烦,我们获得了实质的语义上的简化。不需要区分 $* 和 $@。不需要四种类型的引用,没有支配它们的非常复杂的规则。在 rc 中你在想让一个语法字符出现在参数中、或者参数是空串的时候,而不在其他时候使用引号。IFS 不再使用,除了在绝对必须的一种情况下: 在命令替换期间把命令输出转换到参数列表中。
这也避免了一个重要的 UNIX 安全漏洞。在 UNIX 中,system() 和 popen() 函数调用 /bin/sh 来执行一个命令。不可能使用任何一个这种例程而且保证指定的命令会执行,即使 system() 或 popen() 的调用者为这个命令指定了全路径名字。如果这发生在 set-userid 程序中将是破坏性的。问题在于使用 IFS 把命令分解到一组字中,所以攻击者在运行特权程序之前,只需要在他的环境中设置 IFS=/ 并在当前目录下留下一个叫做 usr 或 bin 的木马程序。rc 通过永不为任何原因而重新扫描输入来解决修正问题。
在 rc 和 Bourne shell 之间的多数其他区别都是不严重的。我去除了 Bourne 变量替换的怪异形式,如
echo ${a=b} ${c-d} ${e?error}
因为它们很少使用,多余和易于用不深奥的术语表达。我删除了内置的 export、readonly、break、continue、read、return、set、times 和 unset,因为他们好象是多余的或只有少量的使用。
在 Bourne 出自 Algol 68 语法的地方,rc 转而基于 C 或 Awk。这是很难辩护的。我相信,比如
if(test -f junk) rm junk
优于
if test -f junk; then rm junk; fi
因为它更少受关键字的困扰。它避免了 Bourne 在奇怪的地方需要的分号,并且语法字符更好的突出了命令的起作用的部分。
Bourne 无可争议的比 rc 好的一个大范围的语法是带有 else 子句的 if 语句。rc 的 if 没有终止的 fi 式的括号(bracket)。结果是,分析器不预先查看它的输入就不能断定是否预期有一个 else 子句。问题出在读取之后,例如
if(test -f junk) echo junk found
在交互模式下,rc 不能确定是立即执行它并打印 $prompt(1),还是打印 $prompt(2) 并等待键入 else。在 Bourne shell 中,这不是个问题,因为 if 语句必须以 fi 结束,不管它是否包含 else。
Rc 公认的薄弱的解决方案是在一个单独的语句中声明 else 子句,带有它必须立即跟随在 if 之后的语义附加条件,并把它叫做 if not 而不是 else,作为快要接近怪物的警示。它唯一值得注意的结果是在下面的构造中需要花括号。
for(i){ if(test -f $i) echo $i found if not echo $i not found}
rc 解决“摇摆的 else”二义性的方法与多数人的预期相反。
值得注意的是在 UNIX 系统程序员手册的四个最新的版本中,手册页中描述的 Bourne shell 文法不认可命令 whowc。这的确是一个疏漏,但它暗示了更黑暗的事情: 没有人真正知道 Bourne shell 的语法是什么。即使是查看源代码也少有帮助。分析器是以递归下降方式实现的,但是对应于文法类属(category)的例程都有一个标志参数,依据上下文巧妙的改变它们的操作。rc 的分析器是使用 yacc 实现的,所以我们可以精确的说出它的语法。
29 致谢
Rob Pike、Howard Trickey 和其他 Plan 9 用户是好想法和批评的持久的、无尽的来源。本文档中的一些例子提取自 [Bourne],这些是 rc 最好的特征。
30 引用
S. R. Bourne, UNIX Time-Sharing System: The UNIX Shell, Bell System Technical Journal, Volume 57 number 6, July-August 1978 Copyright © 2000 Lucent Technologies Inc. All rights reserved.
1 介绍
rc 在精神上类似于 UNIX 的 Bourne shell 但在细节上有很多不同。本文使用很多小例子和一些大点的例子来描述 rc 的首要特征。假定读者熟悉 Bourne shell。
2 简单命令
对于最简单的使用,rc 有着 Bourne-shell 用户很熟悉的语法。下列命令都会如愿而行:
datecat /lib/news/buildwho >user.nameswho >>user.nameswc fileecho [a-f]*.cwho wcwho; datevc *.c &mk && v.out /*/bin/fb/*rm -r junk echo rm failed!
3 引用
包含一个空格或 rc 的其他语法字符之一的参数必须包围在单引号(')之中:
rm 'odd file name'
在已被引号括起来的参数中的单引号必须是双重的:
echo 'How''s your father?'
4 模式
包含字符 * ? [ 中任何一个的未被引号括起来的参数都是同文件名字匹配的模式。字符 * 匹配字符的任意序列,字符 ? 匹配任何单一字符,而 [class] 匹配在 class 中的任意字符,除非 class 的第一个字符是 ~,在这种情况下取这个类的补集。class 也可以包含用 - 分隔的成对的字符,它表示词法上在两者之间的所有字符。字符 / 在模式中必须显式的出现,路径名分量 . 和 .. 也是如此。模式被替代为一个参数的列表,每个匹配的路径名字都是一个参数,作为例外,没有匹配的名字的模式不被替代为空列表;而是保持为自身。
5 变量
UNIX 的 Bourne shell 提供字符串值的变量。rc 提供的变量值是参数的列表 - 就是说,字符串的数组。这是在 rc 和传统的 UNIX 命令解释器之间的原则性区别。变量可以通过键入来给出值,例如:
path=(. /bin)user=tdfont=/lib/font/bit/pelm/ascii.9.font
圆括号指示赋予 path 的值是两个字符串的一个列表。变量 user 和 font 被赋予包含一个单一字符串的列表。
通过在变量的名字前面前导一个 $ 可以把变量的值替换到一个命令中,比如:
echo $path
如果 path 已经做如上设置,它将等价于
echo . /bin
变量可以有数字或数字的列表作为下标,比如:
echo $path(2)echo $path(2 1 2)
它们等价于
echo /binecho /bin . /bin
在左圆括号和变量名字之间必须没有空格分隔;否则,这些下标将被当作一个独立的圆括号中的列表。
在变量中的字符串的数目可以用 $# 操作符确定。例如,
echo $#path
对于上面的例子将输出 2。
下面的两个赋值有着微妙的不同:
empty=()null=''
第一个设置 empty 为不包含字符串的一个列表。第二个设置 null 为包含一个单一的字符串的一个列表,但是这个字符串不包含字符。
尽管它们或多或少是同样的东西(在 Bourne 的 shell 中,它们是不可区分的),它们在几乎所有情况下都表现的很不同。在其他东西当中
echo $#empty
打印 0,而
echo $#null
打印 1。
所有未设置的变量都有值 ()。
偶尔的,把变量的值作为一个单一的字符串是方便的。通过 $" 操作符把字符串的所有元素联接到一个单一字符串中,在元素之间带有空格。这样,如果我们设置
list=(How now brown cow)string=$"list
则
echo $list
和
echo $string
二者导致相同的输出,就是:
How now brown cow
而
echo $#list $#string
将输出
4 1
因为 $list 有四个成员,而 $string 有一个单一的成员,带有三个空格分隔它的单字。
6 参数
当 rc 从一个文件读取它的输入的时候,这个文件有权访问在 rc 的命令行上提供的参数。变量 $* 最初被赋值为参数的列表。名字 $1、$2 等是 $*(1)、$*(2) 等的同义字。此外,$0 是一个文件的名字,rc 的输入从其中读入。
7 联接
rc 有一个字符串联接操作符,插入符 ^,用来建造由多个片断组成的参数。
echo hully^gully
完全等价于
echo hullygully
假定变量 i 包含命令的名字。则
vc $i^.cvl -o $1 $i^.v
可以编译这个命令的源代码,把结果留在适当的文件中。
联接可以应用于列表之上。下面的
echo (a b c)^(1 2 3)src=(main subr io)cc $src^.c
等价于
echo a1 b2 c3cc main.c subr.c io.c
在细节上,规则是: 如果 ^ 的两个操作数(operand)是有相同的非零个字符串的列表,则成对的连接它们。否则,如果操作数之一是一个单一的字符串,它依次与另一个操作数的每个参数联接。操作数的任何其他组合都是错误的。
8 无约束的插入符
用户需求决定了 rc 在特定位置插入插入符,来使语法看起来更像 Bourne shell。例如,下面的:
cc -$flags $stems.c
等价于
cc -^$flags $stems^.c
一般的,rc 会在不由空白分隔的两个参数之间插入 ^。特别是,只要 $ ' ` 跟随着一个被引号括起来的或未被引号括起来的字,或者一个未被引号括起来的字跟随着一个被引号括起来的字并且无空白或 Tab 介于其间,就在二者之间插入一个隐含的 ^。如果立即跟随着一个 $ 的一个未被引号括起来的字包含不是一个字母下划线或 * 的字符,在第一个这样的字符之前插入 ^。
9 命令替换
通常用来从一个命令的输出建造一个参数列表。rc 允许一个命令,包围在花括号中并前导着一个反引号 `{...},出现在需要参数的任何地方。执行这个命令并捕获它的标准输出。使用存储在变量 ifs 中的字符来把输入分解到参数中。例如,
cat
`{ls -trsed 10q}
将按时间次序连接当前目录中十个最旧的文件,假定缺省 ifs 设置为空格、Tab 和换行符。
10 管道线分支
一般的管道表示法对于几乎所有情况都是足够用了。有时后拥有非线性的管道线是有用的。比树状结构更一般化的管道线拓扑结构可能要求任意大的管道缓冲区,甚至更坏、导致死锁。rc 拥有某种非线性的树状管道线的语法。例如,
cmp <{old} <{new} 将回归测试一个命令的新版本。跟随着在花括号中的命令的 < 或 >,导致运行这个命令并把它的标准输出或输入连结到一个管道上。父命令(本例中的 cmp)开始于连结到某个文件描述符或其他东西上的管道线的另一端,并带有在打开的时候会连接到管道的一个参数(例如,/dev/fd/6)(译注:一个命名管道的名字)。一些命令不准备处理关闭就不可搜寻的输入文件。例如 diff 需要读取它的输入两次。
11 退出状态
在命令退出的时候,它向执行它的进程返回一个状态。在 Plan 9 中,状态是描述错误状况的一个字符串。在正常终止的使用它是空的。
rc 把命令退出状态捕获到变量 $status 中。对于一个简单命令 $status 的值就如同上面描述的一样。对于一个管道线 $status 被设置为管道线构件的状态的联接、并带有 字符作为分隔符。
rc 有多种控制流,其中很多控制流以前面执行的命令返回的状态作为条件。所有只包含 0 和 的 $status 拥有逻辑值 true。任何其他状态都是 false。
12 命令组合
包围在 {} 中的命令序列可以用在需要命令的任何地方。例如:
{sleep 3600;echo 'Time''s up!'}&
将在后台等待一小时,接着打印一个消息。不带花括号,
sleep 3600;echo 'Time''s up!'&
将锁住终端一个小时,接着在后台打印这个消息。
13 控制流 - for
为键入的列表中的每个成员执行一次命令,例如:
for(i in printf scanf putchar) look $i /usr/td/lib/dw.dat
它在给定文件中查找单字 printf、scanf 和 putchar 中的每一个。一般形式是
for(name in list) command
或
for(name) command
在第一种情况下为 list 的每个成员执行一次 command,并把这个成员赋予变量 name。如果缺少子句“in list”,假定为“in $*”。
14 条件执行 - if
rc 还提供一个通用 if 语句。例如:
for(i in *.c) if(cpp $i >/tmp/$i) vc /tmp/$i
在 cpp 处理而没有错误的每个 C 源程序上运行 C 编译器。‘if not’语句提供双尾(two-tailed)条件。例如:
for(i){ if(test -f /tmp/$i) echo $i already in /tmp if not cp $i /tmp}
它是在 $* 中的每个文件之上的循环,把不存在于 /tmp 中的文件复制到这里,并对那些已经存在于这里的文件打印一个消息。
15 控制流 - while
rc 的 while 语句看起来如下:
while(newer subr.v subr.c) sleep 5
它等待直到 subr.v 比 subr.c 新,原因大概是 C 编译处理完了它。
如果控制命令是空的,循环将不会终止。这样,
while() echo y
将模拟 yes 命令。
16 控制流 - switch
rc 提供一个 switch 语句来在任意字符串上做模式匹配。它的一般形式是
switch(word){case pattern ... commands case pattern ... commands ...}
Rc 依次尝试对在每个 case 语句中的模式去匹配这个 word。除了 / 和 . 和 .. 不需要被显式的匹配之外,模式与用于文件名匹配的一样。
如果有任何模式匹配,执行紧随这个 case 之后直到下一个 case 之前(或 switch 的结束处)的命令,并且这个 switch 的执行完成。例如,
switch($#*){case 1 cat >>$1case 2 cat >>$2 <$1case * echo 'Usage: append [from] to'} 是一个 append 命令。调用时带有一个文件参数,它把它的标准输入添加到指名的文件中。带有两个参数时,把第一个文件添加到第二个文件中。任何其他参数数目引发一个错误消息。 内置 ~ 命令也匹配模式,并且经常比 switch 更加简洁。它的参数是一个字符串和一个模式的列表。只有在所有模式都匹配这个字符串的时候,设置 $status 为真。下列例子为 man(1) 命令处理选项参数: opt=()while(~ $1 -* [1-9] 10){ switch($1){ case [1-9] 10 sec=$1 secn=$1 case -f c=f s=f case -[qwnt] cmd=$1 case -T* T=$1 case -* opt=($opt $1) } shift} 17 函数
可以通过如下键入来定义函数
fn name { commands }
此后,在遇到叫做 name 的命令的时候,命令的参数列表的余下部分将赋给 $* ,接着 rc 将执行 commands。在完成的时候恢复 $* 的值。例如:
fn g { grep $1 *.[hcyl]}
定义 g pattern (用来查找在当前目录中的所有程序源文件中查找 pattern 的出现)。
通过写
fn name
而没有函数体来删除函数定义。
18 命令执行
rc 做许多事情来执行一个简单命令。如果这个命令名字是使用 fn 定义的一个函数的名字,则执行这个函数。否则,如果它是一个内置命令的名字,rc 直接执行这个内置命令。否则,查找在变量 $path 提及的目录,直到找到一个可执行文件。在 Plan 9 中不鼓励 $path 变量的广泛使用。转而,使用缺省(. /bin) 和把你需要的绑定入 /bin。
19 内置命令
许多命令由 rc 内部执行,原因是不这样就难于实现。
. [-i] file ...
执行来自 file 的命令。在此期间 $* 设置为紧随 file 之后的参数列表的余下部分。使用 $path 来查找 file。选项 -i 指示交互式输入 在读取每个命令之前打印一个提示(在 $prompt 找到)。
builtin command ...
除了忽略叫做 command 的任何函数之外,正常的执行 command。例如,
fn cd{ builtin cd $* && pwd}
定义 cd 内置命令(见后)的一个替代者,它宣布新目录的全名。
cd [dir]
改变当前目录到 dir。缺省参数是 $home。$cdpath 是在其中查找 dir 的位置的一个列表。
eval [arg ...]
参数被联接(由空格分隔)到一个字符串中,读为给 rc 的输入并执行它。例如,
x='$y'y=Doodyeval echo Howdy, $x
将回显
Howdy, Doody
因为在替换了 $x 之后,eval 的参数是
echo Howdy, $y
exec [command ...]
Rc 用给定命令替代自身。有点像一个 goto - rc 不等待命令退出,并且不回来读更多的命令。
exit [status]
Rc 立即退出并返回给定状态。如果未给出,则使用 $status 的当前的值。
flag f [+-]
这个命令操纵并测试命令行标志(后面描述).
flag f +
设置标志 f。
flag f -
清除标志 f。
flag f
测试标志 f,恰当的设置 $status。所以
if(flag x) flag v +
如果已经设置了 -x 标志则设置 -v 标志。
rfork [nNeEsfF]
它使用 Plan 9 rfork 系统入口来把 rc 放置到带有下列属性的新进程组之中:
标志
名字
功能
n
RFNAMEG
制做父名字空间的一个复本
N
RFCNAMEG
开始于一个新的空名字空间
e
RFENVG
制做父环境的一个复本
E
RFCENVG
开始于一个新的空环境
s
RFNOTEG
制做一个新的通知组
f
RFFDG
制做父文件描述符空间的一个复本
F
RFCFDG
开始于一个新的空文件描述符空间
程序员手册的 fork(2) 章节详细描述这些属性。
shift [n]
删除 $* 的前 n(缺省为 1)个元素。
wait [pid]
等待有给定 pid 的进程退出。如果未给出 pid, 等待所有未完结的进程。
whatis name ...
以适合输入到 rc 的形式打印每个名字的值。输出的是到变量的赋值,函数的定义,到对一个内置命令的 builtin 的调用,或一个二进制程序的路径名字。例如,
whatis path g cd who
可能打印
path=(. /bin)fn g {gre -e $1 *.[hycl]}builtin cd/bin/who
~ subject pattern ...
依次针对每个 pattern 匹配 subject。在匹配时,$status 设置为真。否则,设置为‘不匹配’。模式同于文件名匹配的。在执行 ~ 命令之前模式不受文件名替代的支配,所以它们不需要被包围在引号之中,当然,除非想要的是对 * [ or ? 的一个文字上的匹配。例如
~ $1 ?
匹配任何单一字符,而
~ $1 '?'
只匹配一个文字问号。
20 高级 I/O 重定向
rc 允许在 < 或 > 之后的方括号 [ ] 中指定文件描述符,来做不是 0 和 1(标准输入和输出)的文件描述符的重定向。例如,
vc junk.c >[2]junk.diag
把编译器的诊断从标准错误保存到 junk.diag 中。
通过如下键入,这些文件描述符可以被替代为一个已经打开了的文件的 dup(2) 意义上的复制:
vc junk.c >[2=1]
它用文件描述符 1 的一个复件替换文件描述符 2。在与其它重定向联合时非常有用,比如
vc junk.c >junk.out >[2=1]
从左至右求值重定向,所以重定向文件描述符 1 到 junk.out,接着把文件描述符 2 指向相同的文件。与之相对,
vc junk.c >[2=1] >junk.out
重定向文件描述符 2 到文件描述符 1(大概是终端), 并接着定向文件描述符 1 到一个文件。在第一种情况下,标准和诊断输出将被混合到 junk.out 中。在第二种情况下,诊断输出将出现在终端上,而标准输出将发送到文件。
可以使用带有右手边为空的复制记号(notation)关闭文件描述符。例如,
vc junk.c >[2=]
将丢弃来自编译的诊断。
通过如下键入,可以派遣(send)任意的文件描述符经过管道,
vc junk.c [2] grep -v '^$'
它从 C 编译器的错误输出中删除空行。注意 grep 的输出仍出现在文件描述符 1 上。
有时你可能希望把一个管道的输入端连接到不是 0 的某个文件描述符。记号
cmd1 [5=19] cmd2
建立一个管道线,并且 cmd1'的文件描述符 5 通过管道连接到 cmd2'的文件描述符 19。
21 立即文档
rc 过程可以包含叫做“立即文档”数据,它被提供为给命令的输入,比如在下面这版本的 tel 命令中
for(i) grep $i <22>捕获通知
在接收到来自终端的中断的时候,rc 脚本通常终止。以通常的方式定义带有小写的一个 UNIX 信号名字的函数,但在 rc 收到对应的通知的时候调用。程序员手册的 notify(2) 章节详细的讨论了通知。有意思的通知有:
sighup
通知是‘hangup’。Plan 9 在终端从 rc 断开的时候发送这个通知。
sigint
通知是‘interrupt’,通常在终端上键入中断字符(ASCII DEL)的时候发出。
sigterm
通知是‘kill’,通常由 kill(1) 发送。
sigexit
在 rc 将要退出的时候发送的人为通知。
作为一个例子,
fn sigint{ rm /tmp/junk exit}
为键盘中断设置一个陷入来在退出之前删除临时文件。
如果通知例程被设置为 {} 则忽略通知。在删除它的处理器(handler)定义的时候信号恢复它们的缺省行为。
23 环境
环境是对于执行的二进制程序可以获得的名字-值对的一个列表。在 Plan 9 上,环境存储在叫做 #e 的文件系统中,它通常挂装在 /env 上。每个变量的值都保存在一个单独的文件中,都带有终止于 0 字节的分量(component)。(文件系统完全维持在内存中,所以不涉及磁盘或网络。) /env 的内容在一个每进程(per-process)组基础上共享 - 在建立一个新进程组的时候,把 /env 有效的连结到一个新文件系统上,它被初始化为旧文件系统的复件。这种组织的一个结果是命令可以改变环境条目并看到改变反射到 rc 中。
函数也出现在环境中,通过对它们的名字前导 fn# 来命名,如 /env/fn#roff。
24 局部变量
在一个单一命令期间设置一个变量经常是有用的。一个赋值紧随一个命令有这种效果。例如
a=globala=local echo $aecho $a
将打印
localglobal
这对复合命令也有效,如
f=/fairly/long/file/name { { wc $f; spell $f; diff $f.old $f } pr -h 'Facts about '$f lp -dfn}
25 例子 - cd, pwd
下面的这对函数提供标准 cd 和 pwd 命令的增强版本。(为此感谢 Rob Pike。)
ps1='% ' # default prompttab=' ' # a tab characterfn cd{ builtin cd $1 && switch($#*){ case 0 dir=$home prompt=($ps1 $tab) case * switch($1) case /* dir=$1 prompt=(`{basename `{pwd}}^$ps1 $tab) case */* ..* dir=() prompt=(`{basename `{pwd}}^$ps1 $tab) case * dir=() prompt=($1^$ps1 $tab) } }}fn pwd{ if(~ $#dir 0) dir=`{/bin/pwd} echo $dir}
函数 pwd 是标准 pwd 的一个增强版本,它在变量 $dir 中缓冲它的值,因为真正的 pwd 可能执行起来非常慢。(最新版本的 Plan 9 已经有了非常快的 pwd 实现,这减低了 pwd 函数的优势。)
函数 cd 调用 cd 内置命令,并检查它是否成功。如果成功,它设置 $dir 和 $prompt。提示将包含当前目录的最后的成员(除非在 home 目录中,在这里它将是空),并且 $dir 将被重置为正确的值或者是 (),这样 pwd 函数将正确的工作。
26 例子 - man
man 命令打印程序员手册的页面。例如,下面的调用
man 2 sinhman rcman -t cat
在第一种情况,打印章节 2 中的 sinh 页面。在第二种情况,打印 rc 的手册页。因为未指定章节,在所有章节中查找这个页面,在章节 1 中找到了它。在第三种情况,排版 cat 的页面(使用了 -t 选项)。
cd /sys/man { echo $0: No manual! >[1=2] exit 1}NT=n # default nroffs='*' # section, default try allfor(i) switch($i){case -t NT=tcase -n NT=ncase -* echo Usage: $0 '[-nt] [section] page ...' >[1=2] exit 1case [1-9] 10 s=$icase * eval 'pages='$s/$i for(page in $pages){ if(test -f $page) $NT^roff -man $page if not echo $0: $i not found >[1=2] }}
注意使用了 eval 来制作候选手册页的一个列表。不使用 eval,存储在 $s 中的 * 将不触发文件名匹配 - 它被包围在引号中,并且即使不这样,它将在赋值给 $s 的时候被展开。 eval 导致它的参数被 rc 的分析器和解释器重新处理,有效的延迟 * 的求值直到赋值给 $pages.
27 例子 - holmdel
下列 rc 脚本玩一个假装的简单游戏 holmdel,在其中游戏者交替的指名 Bell Labs 的位置,第一个提及 Holmdel 的获胜。
t=/tmp/holmdel$pidfn read{$1=`{awk '{print;exit}'}}ifs='' # just a newlinefn sigexit sigint sigquit sighup{rm -f $texit}cat <<'!' >$tAllentown AtlantaCedar CrestChesterColumbusElmhurstFullertonHolmdelIndian HillMerrimack ValleyMorristownNeptunePiscatawayReadingShort HillsSouth PlainfieldSummitWhippanyWest Long Branch!while(){ lab=`{fortune $t} echo $lab if(~ $lab Holmdel){ echo You lose. exit } while(read lab; ! grep -i -s $lab $t) echo No such location. if(~ $lab [hH]olmdel){ echo You win. exit }}
这个脚本值得详细描述(至少它不蠢。)
变量 $t 是对临时文件名字的一个简写。在这个脚本的多个实例同时运行的情况下,在临时文件名字包括 $pid 能确保它们的名字不会冲突,rc 把 $pid 初始化为它的进程-id。
函数 read 的参数是一个变量的名字,把从的标准输入读取来的行汇集到其中。$ifs 被设置未只有一个换行符。这样 read 的输入不会在空格处被分离,但是终止的换行被删除了。
设置了一个处理器来捕获 sigint、sigquit 和 sighup,还有一个人工的 sigexit 信号。它们只是删除临时文件并退出。
从包含 Bell Labs 位置的一个列表的立即文档来初始化临时文件,接着主循环开始。
首先,程序使用 fortune 程序从位置列表中挑出随机的一行来猜测一个位置(在 $lab 中),如果它猜到 Holmdel,打印一个消息并退出。
接着它使用 read 函数来从标准输入得到一些行,并对它们做有效性检查直到得到一个合法的名字。注意 while 的条件部分可以是一个复合命令。只检查在序列中最后一个命令的退出状态。
最后,如果结果是 Holmdel,打印一个消息并退出。否则它回到循环的顶部。
28 设计原理
Rc 在很大程度上吸取了 Steve Bourne 的 /bin/sh。任何 Bourne shell 的后继者都纳入此类比较。我尝试着去修补它最周知的缺点,并通过省略无关紧要的特征近可能的去简化事情。只在遇到不可抗拒的诱惑的时候我才介入新颖的想法。明显的,我广泛的修补了 Bourne 的语法。
在 rc 的设计中的最重要的原理是它不是一个宏处理器。词法和语法分析代码从不扫描输入多于一次(当然 eval 命令是个例外,它的存在就是为了打破这个规则)。
通过把包含空格的参数传递给 Bourne shell 脚本,它们经常被粗暴的运行。经常在不恰当的时候使用 $IFS 把它们分割到多个参数中。在 rc 中,变量的值,包括命令行参数,在替换到命令中的时候是不重新扫描的。参数大概已经在父进程中扫描过了,并且不应当被重新扫描。
为什么 Bourne 在变量替换之后重新扫描命令? 它需要能够在值是字符串的变量中存储参数列表。如果我们除去了重新扫描,我们必须改变变量的类型,这样它们可以显式的承载字符串的列表。
这介入了一些概念上的复杂性。我们需要对字的列表的表示法。有两种不同的联接,对字符串的 - $a^$b, 和对列表的 - ($a $b)。 在 () 和 '' 之间的区别对初学者是容易混淆的,尽管差别是确切明显的 - 一个空参数和没有参数是不一样的。
Bourne 在做命令替换的时候也需要重新扫描输入。这是因为包围在反引号之中的文本不是字符串而是一个命令。严格的说,在包围着命令是嵌套的命令替换时候必须分析它,但是这使处理它很困难,比如:
size=`wc -l \`ls -tsed 1q\``
内部的反引号必须被转义来避免终止外部的命令。比上面的例子更糟的是,需要的转义数目是嵌套深度的指数。rc 通过把后引用改为参数是命令的一个一元操作符来修正这个问题,例如:
size=`{wc -l `{ls -tsed 1q}}
不需要转义,并且在一遍之中分析所有事情。
出于类似的原因,rc 定义信号处理器如同它们是函数,而不是如同 Bourne 那样为每个信号关联上一个字符串,伴有在键入中断字符时得到语法错误作为响应的可能性。因为 rc 在键入的时候分析输入,它在你制作它们的时候就报告错误。
解决了所有这些麻烦,我们获得了实质的语义上的简化。不需要区分 $* 和 $@。不需要四种类型的引用,没有支配它们的非常复杂的规则。在 rc 中你在想让一个语法字符出现在参数中、或者参数是空串的时候,而不在其他时候使用引号。IFS 不再使用,除了在绝对必须的一种情况下: 在命令替换期间把命令输出转换到参数列表中。
这也避免了一个重要的 UNIX 安全漏洞。在 UNIX 中,system() 和 popen() 函数调用 /bin/sh 来执行一个命令。不可能使用任何一个这种例程而且保证指定的命令会执行,即使 system() 或 popen() 的调用者为这个命令指定了全路径名字。如果这发生在 set-userid 程序中将是破坏性的。问题在于使用 IFS 把命令分解到一组字中,所以攻击者在运行特权程序之前,只需要在他的环境中设置 IFS=/ 并在当前目录下留下一个叫做 usr 或 bin 的木马程序。rc 通过永不为任何原因而重新扫描输入来解决修正问题。
在 rc 和 Bourne shell 之间的多数其他区别都是不严重的。我去除了 Bourne 变量替换的怪异形式,如
echo ${a=b} ${c-d} ${e?error}
因为它们很少使用,多余和易于用不深奥的术语表达。我删除了内置的 export、readonly、break、continue、read、return、set、times 和 unset,因为他们好象是多余的或只有少量的使用。
在 Bourne 出自 Algol 68 语法的地方,rc 转而基于 C 或 Awk。这是很难辩护的。我相信,比如
if(test -f junk) rm junk
优于
if test -f junk; then rm junk; fi
因为它更少受关键字的困扰。它避免了 Bourne 在奇怪的地方需要的分号,并且语法字符更好的突出了命令的起作用的部分。
Bourne 无可争议的比 rc 好的一个大范围的语法是带有 else 子句的 if 语句。rc 的 if 没有终止的 fi 式的括号(bracket)。结果是,分析器不预先查看它的输入就不能断定是否预期有一个 else 子句。问题出在读取之后,例如
if(test -f junk) echo junk found
在交互模式下,rc 不能确定是立即执行它并打印 $prompt(1),还是打印 $prompt(2) 并等待键入 else。在 Bourne shell 中,这不是个问题,因为 if 语句必须以 fi 结束,不管它是否包含 else。
Rc 公认的薄弱的解决方案是在一个单独的语句中声明 else 子句,带有它必须立即跟随在 if 之后的语义附加条件,并把它叫做 if not 而不是 else,作为快要接近怪物的警示。它唯一值得注意的结果是在下面的构造中需要花括号。
for(i){ if(test -f $i) echo $i found if not echo $i not found}
rc 解决“摇摆的 else”二义性的方法与多数人的预期相反。
值得注意的是在 UNIX 系统程序员手册的四个最新的版本中,手册页中描述的 Bourne shell 文法不认可命令 whowc。这的确是一个疏漏,但它暗示了更黑暗的事情: 没有人真正知道 Bourne shell 的语法是什么。即使是查看源代码也少有帮助。分析器是以递归下降方式实现的,但是对应于文法类属(category)的例程都有一个标志参数,依据上下文巧妙的改变它们的操作。rc 的分析器是使用 yacc 实现的,所以我们可以精确的说出它的语法。
29 致谢
Rob Pike、Howard Trickey 和其他 Plan 9 用户是好想法和批评的持久的、无尽的来源。本文档中的一些例子提取自 [Bourne],这些是 rc 最好的特征。
30 引用
S. R. Bourne, UNIX Time-Sharing System: The UNIX Shell, Bell System Technical Journal, Volume 57 number 6, July-August 1978 Copyright © 2000 Lucent Technologies Inc. All rights reserved.
2007年8月10日星期五
2007年8月9日星期四
James Blunt - Goodbye My Lover
Did I disappoint you or let you down?
Should I be feeling guilty or let the judges frown?
'Cause I saw the end before we'd begun,
Yes I saw you were blinded and I knew I had won.
So I took what's mine by eternal right.
Took your soul out into the night.
It may be over but it won't stop there,
I am here for you if you'd only care.
You touched my heart you touched my soul.
You changed my life and all my goals.
And love is blind and that I knew when,
My heart was blinded by you.
I've kissed your lips and held your head.
Shared your dreams and shared your bed.
I know you well, I know your smell.
I've been addicted to you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I am a dreamer but when I wake,
You can't break my spirit
it's my dreams you take.
And as you move on, remember me,
Remember us and all we used to be
I've seen you cry, I've seen you smile.
I've watched you sleeping for a while.
I'd be the father of your child.
I'd spend a lifetime with you.
I know your fears and you know mine.
We've had our doubts but now we're fine,
And I love you, I swear that's true.
I cannot live without you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
And I still hold your hand in mine.
In mine when I'm asleep.
And I will bear my soul in time,
When I'm kneeling at your feet.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I'm so hollow, baby, I'm so hollow.
so, I'm so, I'm so hollow.
Should I be feeling guilty or let the judges frown?
'Cause I saw the end before we'd begun,
Yes I saw you were blinded and I knew I had won.
So I took what's mine by eternal right.
Took your soul out into the night.
It may be over but it won't stop there,
I am here for you if you'd only care.
You touched my heart you touched my soul.
You changed my life and all my goals.
And love is blind and that I knew when,
My heart was blinded by you.
I've kissed your lips and held your head.
Shared your dreams and shared your bed.
I know you well, I know your smell.
I've been addicted to you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I am a dreamer but when I wake,
You can't break my spirit
it's my dreams you take.
And as you move on, remember me,
Remember us and all we used to be
I've seen you cry, I've seen you smile.
I've watched you sleeping for a while.
I'd be the father of your child.
I'd spend a lifetime with you.
I know your fears and you know mine.
We've had our doubts but now we're fine,
And I love you, I swear that's true.
I cannot live without you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
And I still hold your hand in mine.
In mine when I'm asleep.
And I will bear my soul in time,
When I'm kneeling at your feet.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I'm so hollow, baby, I'm so hollow.
so, I'm so, I'm so hollow.
2007年8月5日星期日
《嵌入式设计及Linux驱动开发指南——基于ARM9处理器》读书笔记
第一章 嵌入式系统基础
1、 嵌入式系统定义:
“嵌入式系统是用来控制或者监视机器、装置、工厂等大规模系统的设备。”
——电气工程师协会
“嵌入到对象体系中的专用计算机系统”
——北京航空航天大学 何立民教授
“嵌入性”、“专用性”与“计算机系统”是嵌入式系统的三个基本要素。
2、 嵌入式操作系统:
硬实时系统有一个刚性的、不可改变的时间限制,它不允许任何超出时限的错误超时错误会带来损害甚至导致系统失败、或者导致系统不能实现它的预期目标。
软实时系统的时限是柔性灵活的,它可以容忍偶然的超时错误。失败造成的后果并不严重,仅仅是轻微地降低了系统的吞吐量。
我们可以认为至少嵌入式系统都是软实时系统,所有的嵌入式系统都是实时系统,但并不是所有的实时系统都是嵌入式系统。
常用的嵌入式操作系统有:Linux, uC/OS, Windows CE, VxWorks, Palm OS, QNX等。
3、 选择Embedded OS的原则:
系统成本;
市场进入时间及技术支持;
可移植性;
可利用资源;
系统定制能力。
第二章 基于ARM9处理器的硬件开发平台
1、 ARM的历史:
ARM(Advanced RISC Machine)公司于1990年11月在英国剑桥成立。
1991年,ARM推出第一个嵌入式RISC核心——ARM6系列处理器,VLSI、夏普、GEC Plessey、德州仪器、Cirrus Logic等公司相继同ARM公司签署了授权协议。
1998年4月,ARM在伦敦证券交易所和纳斯达克交易所上市。
ARM中国安谋咨询上海有限公司于2002年7月在中国上海成立。
目前基于ARM核的处理器有以下几类:
ARM7家族;
ARM9家族;
ARM9E家族;
ARM10E家族;
ARM11家族;
SecurCore家族;
OptimoDE数据引擎内核;
MPCore多处理器家族;
Intel公司的StrongARM/XScale。
2、ARM7和ARM9处理器的主要区别:
指令流水线:
ARM7:三级,(取指令,译码,执行);
ARM9:五级,(取指,译码,执行,缓冲/数据,回写)。
3、三星S3C2410X处理器:
基于ARM920T核(由ARM9TDMI、存储管理单元MMU和高速缓存三部分组成),片上资源包括:
1个LCD控制器(支持STN和TFT带有触摸屏的液晶显示屏);
SDRAM控制器;
3个通道的UART;
4个通道的DMA;
4个具有PWM功能的计时器和1个内部时钟;
8通道的10位ADC;
触摸屏接口;
I2S总线接口;
2个USB主机接口,1个USB设备接口;
2个SPI接口;
SD接口和MMC卡接口;
看门狗计数器;
117位通用I/O口和24位外部中断源;
第三章 调试嵌入式系统程序
1、 嵌入式系统调试方法:
1) 实时在线仿真(In-Circuit Emulator, ICE)
优点:功能非常强大,软硬件均可做到完全实时在线调试。
缺点:价格昂贵。
2) 模拟调试
优点:简单方便,不需要目标板,成本低。
缺点:功能非常有限,无法实时调试。
3) 软件调试
优点:纯软件,价格较低,简单,软件调试能力较强。
缺点:需要事先烧制监控程序(Monitor)(往往需多次实验才能成功)且目标板工作正常,功能有限,特别是硬件调试能力较差。
4) JTAG调试
优点:方便、简单,无需制作Monitor,软硬件均可调适。
缺点:需要工作基本正常(至少CPU工作正常)的目标板,仅适用于有调试接口的芯片。
2、 ARM仿真器工作原理:
利用高速JTAG(Joint Test Action Group)串行扫描链,通过调试通信通道(Debug Communication Channel, DCC)连接ARM核心内嵌的名为“Embedded-ICE”的调试逻辑,调试逻辑实时监测ARM核心的寄存器、数据总线和地址总线。调试器设置Breakpoint及Watchpoint后,程序在ARM内核全速运行,调试程序实时监测地址与数据总线并与预设值比较,在吻合时产生异常中断通知内核并把控制权交给调试器。这样,在程序全速运行时,可以在断点处停止,可以设置条件断点、条件观测断点等,而又不占用CPU时间及内存资源。
3、 JTAG接口:
1985年制定的检测PCB和IC的一个标准,1990年被修改后成为IEEE的一个标准,及IEEE1149.1-1990。通过这个标准,可对具有JTAG接口芯片的电路进行边界扫描和故障检测。
第四章 创建嵌入式系统开发环境
1、交叉编译步骤:
(1) 创建编译环境。在这个过程中,将设置一些环境变量,创建安装目录,安装内核源代码和头文件等。
(2) 创建binutils。这个过程结束后,会创建类似arm-linux-ld等工具。
Binutils是一组开发工具,包括链接器、汇编器以及其他用于目标文件和档案的工具
首先要安装的软件包使binutils。这非常重要,因为glibc和gcc会针对可用的连接器和汇编器进行多种测试,以决定打开某些特性。
(3) 创建一个交叉编译版本的gcc。注意:在这个过程中只能编译C程序,而不能编译C++程序。
创建交叉编译版本的gcc,需要交叉编译版本的glibc及其头文件,而交叉编译版本的glibc是通过交叉编译版本的gcc创建的。面对这个先有鸡还是先有蛋的问题,解决办法是先只编译对C语言的支持,并禁止支持线程。
(4) 创建一个交叉编译版本的glibc。这里最容易出现问题。
glibc是一个提供系统调用和基本函数的C语言库,比如open,malloc和printf等,所有动态链接的程序都要用到它。创建glibc需要的时间更长。
(5) 创建一个交叉编译版本的gdb。在这个过程结束后,会创建ARM-Linux-gdb。
(6) 重新创建gcc。前面创建gcc的过程没有编译C++编译器,现在glibc已经准备好了,所以这个步骤将完善gcc的交叉编译。
(7) 重新创建glibc。如果成功执行了这个过程,那么你就拥有了一套属于自己的交叉编译工具链。
2、如果在交叉编译过程中出现错误,那么请检查:
版本选择是否正确,以及是否安装了相应的补丁;
库文件路径是否正确;
系统环境变量是否设置正确。
第五章 Bootloader
1、 Bootloader(引导加载程序)是系统加电后运行的第一段代码。一般它只在系统启动时运行非常短的一段时间,但对于嵌入式系统来说,这是一个非常重要的系统组成部分。
2、 嵌入式Linux系统从软件的角度看通常可以分成4个层次:
(1) 引导加载程序。包括固化在固件(Firmware)中的启动代码(可选)和Bootloader两大部分。
(2) 内核。特定于嵌入式板子的定制内核以及控制内核引导系统的参数。
(3) 文件系统。包括根文件系统和建立于Flash内存设备之上的文件系统。通常用Ramdisk作为根文件系统。它是提供管理系统的各种配置文件以及系统执行用户应用程序的良好的运行环境的载体。
(4) 用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。
3、 Bootloader包含两种不同的操作模式:
1) 启动加载(Bootloading)模式;
2) 下载(Downloading)模式。
4、 Bootloader的启动流程:
第一阶段:主要包含依赖于CPU的体系结构硬件初始化的代码,通常都用汇编实现。这个阶段的任务有:
基本的硬件设备初始化(屏蔽所有的中断、关闭处理器内部指令/数据Cache等)
为第二阶段准备RAM空间
如果是从某个固态存储媒质中,则复制Bootloader的第二段代码到RAM
设置堆栈
跳转到第二阶段的程序入口点
第二阶段:通常用C语言完成,以便实现更复杂的功能,也使程序有更好的可读性和可移植性。这个阶段的任务有:
初始化本阶段要使用到的硬件设备
检测系统内存映射
将内核映像和根文件系统映像从FLASH读到RAM
为内核设置启动参数
调用内核
5、 常用U-Boot命令:
命令名
功能
help / ?
帮助命令。用于查询U-Boot支持的命令并列出简单说明,和“?”是同一个命令
bdinfo
察看目标系统参数和变量、目标板的硬件配置、各种变量参数
setenv
设置环境变量。比较常用的有:
setenv ipaddr *.*.*.*
setenv severip *.*.*.*
setenv gatewayip *.*.*.*
setenv ethaddr *.*.*.*.*.*
printenv
查看环境变量
saveenv
保存设置的环境变量到Flash
mw
写内存
md
察看内存
mm
修改内存
flinfo
察看Flash的信息
erase [起始地址 结束地址]
搽除Flash内容,必须以扇区为单位进行搽除
cp [源地址 目标地址 大小]
内存复制,可以在Flash和ram中交换数据
imi [起始地址]
察看内核映像文件
bootm [起始地址]
从某个地址启动内核
tftpboot [起始地址 镜像名]
通过ftp从主机系统下载内核映像文件
reset
复位
第六章 Linux系统在ARM平台的移植
1、 使某一个平台的代码运行在其它平台上的过程就叫做移植。
2、 Linux内核结构:
/arch包含了所有硬件结构特定的内核代码。
Linux系统能支持如此多平台的部分原因是因为内河把原程序代码清晰的划分为体系结构无关部分和体系结构相关部分。对于任何平台,都必须包含以下几个目录:
u boot:包括启动内核所使用的部分或全部平台特有代码。
u kernel:存放支持体系结构特有的(如信号处理和SMP)特征的实现。
u lib:存放高速体系结构特有的(如strlen和memcpy)同用函数的实现。
u mm:存放体系结构特有的内存管理程序的实现。
u math-emu:模拟FPU的代码。对于ARM处理器来说,此目录用mach-xxx代替。
显然,移植工作的重点就是移植arch目录下的文件。
/drivers包含了内核中所有的设备驱动程序。
/fs包含了所有的文件系统的代码。
/include包含了建立内核代码时所需的大部分库文件,这个模块利用其他模块重建内核。该目录也包含了不同平台需要的库文件。比如,asm-arm是arm平台需要的库文件。
/init包含了内核的初始化代码,内核从此处工作。
不是系统的引导代码,由main.c和version.c两个文件。这是研究核心如何工作的好起点。
/ipc包含了进程间通信代码。
/kernel包含了主内核代码。
/mm包含了所有内存管理代码。
/net包含了和网络相关的代码。
3、 在移植过程中,定时器、中断、CACHE管理、MMU等和硬件密切相关的地方都是要相关平台的底层代码支持的,要特别注意。
4、 编译内核:
1) 配置内核
make ARCH=arm CROSS_COMPILE=arm-Linux- menuconfig
2) 创建内核依赖关系
make dep
3) 创建内核镜像文件
make zImage
4) 创建内核模块
make modules
make modules_install
对每一个配置来说,内核生成以后包括4个文件:没有压缩的内核镜像(zImage或bzImage),压缩的内核镜像(vmlinux),内核符号映射文件(System.map)以及配置文件(config)。
第七章 Linux设备驱动程序开发
1、 设备驱动的任务包括:
1) 自动配置和初始化子程序。这部分程序仅在初始化的时候被调用一次。
2) 服务于I/O请求的子程序。这部分是系统调用的结果。在执行这部分程序的时候,系统仍认为和进行调用的进程属于同一个进程,只是由用户态变成了核心态,并具有进行此系统调用的用户程序的运行环境,所以可以在其中调用sleep()等与进程运行环境有关的函数。
2、 设备类型分类:
1) 字符设备(char device)。字符设备是Linux最简单的设备,可以向文件一样访问。
初始化字符设备时,它的设备驱动程序向Linux登记,并在字符设备向量表中增加一个device_struct数据结构条目,这个设备的主设备标识符用作这个向量表的索引。一个设备的主设备标识符是固定的。chrdevs向量表中的每一个条目,一个device_struct数据结构,包括两个元素:一个登记的设备驱动程序的名称的指针和一个指向一组文件操作的指针。参见include/linux/major.h。
2) 块设备(block device)。是文件系统的物质基础,它也支持像文件一样被访问。
Linux用blkdevs向量表维护已经登记的块设备文件。它像chrdevs向量表一样,使用设备的主设备号作为索引。它的条目也是device_struct数据结构。与字符设备不同的是,块设备分为SCSI类和IDE类。类向Linux内核登记并向河心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。参见fs/devices.c。
每一个块设备驱动程序必须提供普通的文件操作接口和对于buffer cache的接口。每一个块设备驱动程序填充blk_dev向量表中的blk_dev_struct数据结构。这个向量表的索引还是设备的主设备号。这个blk_dev_struct数据结构包括一个请求例程的地址和一个指针,指向一个request数据结构的列表,每一个都表达buffer cache向设备读/写一块数据的一个请求。参见drivers/block/ll_rw_blk.c和include/linux/blkdev.h。
当buffer_cache从一个已登记的设备读/写一块数据,或者希望读写一块数据到其他位置时,他救灾blk_dev_struct中增加一个request数据结构。每个request数据结构都有一个指向一个或多个buffer_head数据结构的指针,每一个都是读/写一块数据的请求。如果buffer_head数据结构被锁定(buffer_cache),可能会有一个进程在等待这个缓冲区的阻塞进成完成。每一个request数据结构都是从all_request表中分配的。如果request增加到空的request列表,就调用驱动程序的request函数处理这个request队列,否则驱动程序只是简单的处理request队列中的每一个请求。
块设备驱动程序和字符设备驱动程序的主要区别是:在对字符设备发出读写请求时,实际的硬件I/O一般紧接着就发生了,块设备则不然,它利用一块系统内存作为缓冲区,当用户进程对设备请求能满足用户的要求时,就返回请求的数据,如果不能就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备的,以免耗费过多地CPU时间来等待。
3) 网络设备。
3、 设备驱动中关键数据结构:
1) file_operations:内核内部通过file结构识别设备,通过file_operations数据结构提供文件系统的入口点函数。file_operations定义在中的函数指针表。这个结构的每一个成员的名字都对应着一个系统调用。从某种意义上说,写驱动程序的任务之一就是完成file_operations中的函数指针。如果在2.4版本内核下开发的驱动很可能在2.6版本中无法使用,需要进行移植。
2) inode:文件系统处理的文件所需要的信息在inode(索引节点)中。
3) file:主要用于与文件系统对应的设备驱动程序使用。
第八章 网络设备驱动程序开发
1、 网络设备使用网络接口管理表dev_base。dev_base是一个指向device结构的指针,因为网络设备是通过device数据结构来表示的。
2、 网络驱动程序必须解决的两个问题:
1) 不是所有建立在Linux内核的网络设备驱动程序都会有控制的设备。
2) 系统中的以太网设备总是叫做/dev/eth0到/dev/eth7,而不管底层的设备驱动程序是什么。当驱动程序找到它的以太网设备时,它就填充它现在拥有的ethn的device数据结构。这时网络驱动程序也要初始化它控制的物理硬件,并找出它使用的IRQ、DMA等。一旦所有的8个标准的/dev/ethn都分配了,就不会再探测更多的以太网设备。
3、 两个重要的数据结构:
1) device:这个数据结构是在系统中每一个设备的代表,它提供了多个设备方法,供操作系统或协议层使用。这其中就包括设备初始化时调用的init函数、打开和关闭设备的open和stop函数、处理数据包发送的hard_start_xmit函数等。
2) sk_buff:Linux网络各层之间的数据传送都是通过sk_buff(套接字缓冲区)完成的。每个sk_buff包括一些控制方法和一块数据缓冲区,这个区域存放了网络传输的数据包。控制方法按功能分为两种类型,一种是控制整个buffer链的方法,另一种是控制数据缓冲区的方法。sk_buff组成双向链表的形式,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff数据结构再include/linux/skbuff.h文件中定义。
4、 内核的驱动程序接口:
1) 打开函数net_open:执行“ifconfig eth0 up”命令时网络层调用它,把设备连到线路上并启用来接受/发送数据。open函数在网络设备驱动程序里是网络设备被激活的时候被调用的,即设备状态由down至up。所以实际上很多在初始化的工作可以放到这里来做。
2) 关闭函数net_close:执行“ifconfig eth0 down”时使网卡进入一个清醒的状态。如果硬件许可那么它会释放中断和DMA通道,并完全关闭以节约能源。
3) 探测函数probe/probe1:在启动时调用以检测网卡存在与否。如果可以通过读取内存等非强制手段进行检查最好,也可以从I/O端口读取。在探测函数的最后它会填充device结构各域。
4) 发送函数:它与dev->hard_start_xmit()连接,在内核想通过设备传送数据时调用它。如果发送成功,hard_start_xmit()方法里释放sk_buff,返回0,否则返回1。如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0后才会再次发送;也可以不置dev->tbusy为非0,这样系统会不断尝试重发。tbusy的置0任务一般由中断完成,然后用mark_bh()调用通知系统可以再次发送。
5) 接收函数:它把数据从硬件移出,放在sk_buff结构中,执行netif_rx(sk_buff)告诉内核数据所在位置。真正的处理是在中断返回之后,这样可以减少中断时间。在协议层,接收数据包的流程控制分两个层次:首先,netif_rx()函数限制了从物理层到协议层的数据帧的数量;然后,每一个套接字都有一个队列,限制从协议层到套接字层的数据帧的数量。
6) 中断处理函数:需要了解相关的中断状态位以进行相应的操作。
7) 其他函数。
第九章 USB驱动程序开发
1、 OHCI简介:
OHCI规范定义了两个主机控制器(HC)与主机控制器驱动(HDC)的通信通道(Communication Channel)。第一个为主机控制器操作寄存器,第二个为主机控制器通信域(Host Controller Communications Area, HCCA)。
OHCI规范支持USB四种数据通信方式,并根据数据传输特定,将中断数据传输和等时数据传输归为同一类周期性数据传输方式。在HCCA中定义了4个链表。其中除完成数据链表外,其他的周期性数据链表、控制传输数据链表和批量传输数据链表都是二维链表。每个ED(Endpoint Descriptor)描述USB设备的一个端点的所有的数据传输,所有的ED被连接在一起,而TD(Transfer Descriptor)描述的才是最终要在USB总线上传输的数据包。属于同一个USB设备的端点的TD被连接在一起,并挂在相应的ED上。
主机控制器硬件通过寄存器访问该链表来得到相关的USB传输数据包,并将其发送到USB总线上。主机控制器驱动程序则根据实际的数据传输需要,将要发送的数据包添加到相应的链表上。
OHCI定义了两类TD:通用TD(General TD)和等时TD(Isochronous TD)。通用TD被用来支持USB中断、批量和控制三类传输方式,而等时TD被用来支持USB等时数据传输。用一个单独类型的TD来实现USB等时数据传输的目的是为了方便实现DMA数据传输功能。
2、 Linux下USB系统文件节点:同其他外设一样,上层应用软件对连接在系统地USB设备访问是通过文件系统的形式进行的。每个连接到系统总线上的USB设备可以同时对应有一个或者多个驱动程序。即一个USB设备可以在Linux系统上形成一个或多个设备节点,以供应用程序使用。在Linux系统上,每个设备节点都有其相关的主设备号和次设备号。
3、 USB主机驱动结构:Linux USB主机驱动由三部分组成:
1) USB主机控制器驱动(HCD):是USB主机驱动程序中直接与硬件交互的软件模块,其主要功能有:主机控制器硬件初始化;为USBD层提供相应的接口函数;提供根HUB(ROOT HUB)设备配置、控制功能;完成4种类型的数据传输等。
2) USB驱动(USBD):是整个USB主机驱动的核心,其主要实现的功能有:USB总线管理、USB总线设备、USB总线带宽管理、USB的4种类型数据传输、USB HUB驱动、为USB设备类驱动提供相关接口、提供应用程序访问的USB系统的文件接口等。
3) USB设备类驱动:是最终与应用程序交互的软件模块,其主要实现的功能有:访问特定的USB设备、为应用程序提供访问接口等。
应用程序首先通过文件系统(POSIX)接口来访问相应的USB设备类驱动程序和USBD;USB设备类驱动程序则通过USBD提供的相关接口将数据请求包传递给USBD;USBD通过HCD提供的接口,进一步将数据包传递给HCD;HCD最终将数据发送到USB总线上。Linux定义了通用的数据结构URB用来在USB设备类驱动和USBD,USBD和HCD间进行数据传输。统一的URB(Universal Request Block)结构为usb主机驱动程序的开发带来了很大方便。
4、 USB时序:
1) 数据传输时序:在USB总线上,所有的数据传输都是由USB HOST发起的。每个USB设备通过地址过滤出自己要接受的数据包,并根据数据包请求的类型与USB HOST进行数据传输。由于数据传输的时序和总线带宽问题,当应用程序通过设备类提供一个URB时,该数据包并不能立即被送到USB总线上,而只能在USB总线上有足够带宽的情况下,该数据请求才会被传输。因而,HCD层为不同类型的数据传输维护了相应的数据链,当数据链上的数据包传输结束后,HCD通过调用与该数据包相关联的回调函数来通知设备类驱动程序。
2) 设备连接:当一个USB设备连接到USB总线上时,USB HUB驱动程序首先通过中断数据传输获得设备连接信息,然后通过调用USB内核模块所提供的相关函数来完成对USB设备的配置工作。同时USBD不但要为新设备分配在USBD层所需要的资源,同时也要为USB设备分配在HCD层所需要的资源。接着,USBD通过调用所有USB设备类驱动程序提供的Probe函数来查找适合该USB设备的驱动程序。
3) 设备断开:当一个USB设备断开时,USB HUB驱动程序首先通过中断数据传输获得设备断开信息,然后通过调用USB内核模块所提供的相关函数来释放USBD层和HCD层为该设备分配资源。同时通过调用该设备的相关驱动程序提供的Disconnect函数来通知设备驱动程序该设备已经断开。
第十章 图形用户接口
1、 嵌入式系统中几种流行的GUI:
1) MicroWindows;
2) MiniGUI:是我国为数不多的在国际比较知名的自由软件之一。几乎所有的MiniGUI代码都采用C语言开发,提供了完备的多窗口机制和消息传递机制以及众多空间和其他GUI元素。其引人注目的特性和技术创新主要有:
是一个轻量级的图形系统;
完善的对中日韩文字、输入法的多字体和多字符集支持
提供图形抽象层(GAL)以及输入抽象层(IAL)以适应嵌入式系统各种显示和输入设备
提供MiniGUI-Threads、MiniGUI-Lite、MiniGUI-standone三种不同架构的版本以满足不同的嵌入式系统
提供了丰富的应用软件,其商业版本提供了手机、PDA类产品、媒体及机顶盒类产品以及工业控制方面的诸多程序;
3) Qt/Embedded:是挪威Trolltech软件公司的产品,Linux桌面系统的KDE就是基于Qt库(不是QtE)开发的。使用QtE苦,开发者可以:
当移植QtE程序到不同平台时,只需要重新编译代码,而不需要对代码进行修改;
随意设置程序界面的外观;
方便地为程序连接数据库;
使程序本地化;
将程序与Java集成
同Qt一样,QtE也是用C++写的,虽然这样会增加系统资源消耗,但是却为开发者提供了清晰的程序框架,使开发者能够迅速上手,并且可以方便的编写自定义的用户界面程序。
2、 MiniGUI编程:
它采用类似WINDOWS SDK的窗口和事件驱动机制,MiniGUI的存储空间占用情况如下:
项目
容量
备注
Linux内核
300~500KB
由系统决定
MiniGUI支持库
500~700KB
由编译选项确定
MiniGUI字体、位图等资源
400KB
由应用程序确定,可缩小到200KB以内
GB2312输入法码表
200KB
不是必需的,由应用程序确定
应用程序
1~2MB
由应用程序决定
MiniGUI层次:
FrameWork, MMI, Key Apps
MiniGUI
ANSIC Library
Portable Layer
Devices
Linux/uCLinux, eCos, uC/OS-II, VxWorks, Psos
ix86, ARM, MIPS, PowerPC, M68K
MiniGUI是一个典型的消息驱动的GUI系统,每一个MiniGUI程序都从MiniGUIMain函数开始。应用程序先以CreatMainWindow函数创建一个主窗口,由于窗口创建的时候默认是不可见的,所以我们通过ShowWindow函数显现该窗口,最后通过GetMessage(&Msg, hMainWnd)进入消息循环。
3、 Qt/Embedded编程:
Qt是以工具开发包的形式提供给开发者的,这些工具开发包包括了图形设计器、Makefile制作工具、字体国际化工具和Qr的C++类库等。如果不考虑X窗口系统的需要,基于QrE的应用程序可以直接对缓冲帧进行写操作。QtE提供了大约200个可配置的特征,由此再Intel X86平台上库的大小范围为700KB~5MB。大部分客户选择的配置使得库的大小为1.5~4MB。Qt/Embedded与Qt/X11的Linux版本架构比较如下:
应用源代码
Qt API
Qt/Embedded
Qt/X11
Qt/XLib
X Window sever
帧缓冲
Linux内核
信号与插槽机制提供了对象间的通信机制。Qt的窗口在事件发生后会激发信号,程序员通过建立一个函数(称作插槽)然后调用connect()函数把这个插槽和一个信号连接起来,这样就完成了一个事件和响应代码的连接。信号与插槽机制不需要类之间互相知道细节,这样就可以相对容易地开发出代码可高度重用的类。信号与插槽机制是类型安全的,它以警告的方式报告类型错误,而不会使系统产生崩溃。
Qt拥有丰富的满足不同需求的窗体(如按钮、滚动条等)。窗体是QWidget类或它子类的实例,客户自己的窗体类需要从QWidget的子类继承。一个窗体可以包含任意数量的子窗体,子窗体可以显示在父窗体的客户区,一个没有父窗体的窗体称为顶级窗体(一个“窗口”),一个窗体通常有一个边框和标题栏作为装饰。在父窗体无效、隐藏或被删除后,它的子窗体都会进行同样的操作。
第十一章 系统设计开发
1、 按照嵌入式系统的工程设计方法,嵌入式系统的设计可以分成7个阶段:产品定义、硬件与软件划分、迭代与实现、详细的硬件与软件设计、硬件与软件集成、接受测试和维护与升级。前三个阶段确定要解决的问题及需要完成的目标,也常被称为“需求阶段”;第四阶段主要解决如何在给定的约束条件下完成用户的需求;后三个阶段主要解决如何在所选择的硬件和软件基础上进行整个软/硬件系统的协调实现。
2、 硬件设计主要有以下五个关键步骤:
1) 功能定义;
2) 原理图设计:应尽量做到标准化、通用化、模块化、可扩展化;
3) PCB设计:是硬件设计的难点;
4) 硬件调试;
5) 产品化调整:一个系统的设计样机到产品化是个漫长的过程,单从硬件角度讲,至少要满足功能性、稳定性、可靠性、工艺性等最低要求。
3、 原理图设计中的几点建议:
1) 开关电源和线性电源的优缺点:
优点
缺点
开关电源
输入电压范围宽
效率高
输出功率大
应用比较灵活
电路相对复杂,外围器件较多
对电容电感的要求很高,布线也很讲究
开关频率会给系统带来干扰
纹波比较难控制
线性电源
电路简单,外围器件很少
输出精度高,有很好的负载曲线
工作在低频状态,不会给系统带来麻烦
输入范围比较受限制
效率低,这是由于线性电源自身的损耗造成的
输出功率相对较小
2) NAND和NOR Flash的速度差异:
NOR得读速度比NAND稍快一些;
NAND的写入速度比NOR快很多;
NAND的4ms擦除速度远比NOR的5s快;
大多数写入操作需要先进行擦除操作;
NAND的擦除单元更小,相应的擦除电路更少。
4、 PCB设计要点:
1) 元件封装的准备: 尽量调用标准封装库中的文件;
严密按照所选器件的数据手册上的规范制作封装,不能忽略累计误差;
注意二极管、三极管等极性元件及一些非对称元件的引脚定义不能搞错。
2) 合理布局:
尽量依照参考板的模式进行布局;
模块化布局;
要求模拟电路与数字电路分开;
输入模块与输出模块隔离;
去隅电容尽量靠近元件的电源/地;
电源等发热元件要考虑散热,主发热元件靠近出风口,大体积元件的放置避开风路;
元件分布均匀,避免电流过于密集;
板上的跳线或按键考虑易操作性;
元件的排列尽量整齐美观;
考虑机械尺寸,不要超过结构所允许的范围。
3) PCB分层:
如果有参考板,按照参考板进行分层;
多层板安排:顶层和底层为元件面,第二层为地平面,倒数第二层为电源层;
在不影响性能的情况下,减少PCB层数,降低成本。
4) 电源考虑:
系统电源入口做高频和低频滤波处理;
功率较高的器件配备大容量电容去除低频干扰;
每个器件配备0.1Uf电容过滤高频干扰;
高频器件电源管脚和电容之间串联磁珠达到更好的效果;
去耦电容的引线不能过长,特别是高频旁路电容不能带引线。
5) 时钟考虑:
时钟电路尽量靠近芯片;
晶体下方不要走线;
晶体外科接地,增加抗电磁干扰能力;
频率大于20MHz的时钟信号有地线护送;
时钟线宽大于10mil;
时钟输出端串联22~220Ω的阻尼电阻。
6) 高速信号:
采用手工布线;
高速总线走线尽量等长,并且在靠近数据输出端串联22~300Ω的阻尼电阻;
高速信号远离时钟芯片和晶体;
高速信号远离外部输入输出端口,或地线隔离。
7) 差分信号:
差分信号要平行等长;
信号之间不能走其他信号线;
信号要求在同一层上。
8) 走线规范:
不同层的信号垂直走线;
地线和电源层不要走线,否则要保证平面的完整性;
导线宽度不要突变;
导线变向时导角要大于90度;
定位孔周围0.5mm范围不要走线。
5、 硬件调试:
优先调试电源:保证系统可靠地供电;
分模块调试:可以分清模块间的问题,不至于混淆;
结合软件调试:对于复杂的接口,单纯硬件角度不易调试,结合软件从不同的角度测试,能起到更好的效果。
正确、合理的使用示波器,提高工作效率;
对比调试:有条件用评估板或功能相似的电路板作参考,比较差异并找出问题所在;
系统时钟受干扰或晶体震荡不正常导致系统工作故障;
复位不可靠,造成个单元未进入预期状态而出现问题;
因焊接问题引发的各种问题,如方向焊错、虚焊、错焊等;
因时序不匹配引发的通信故障,如时钟信号通过逻辑器件后产生延时,与读写信号时序搭配不上导致读写错误。
6、 嵌入式文件系统:
目前支持闪存的文件系统技术有以下几种:
JFFS2和Yaffs。这些文件系统可以使用在没有初始化的NAND Flash和有CFI接口的NOR Flash中。JFFS2的特点包括:
支持数据压缩;
提供了“写平衡”支持;
支持多种结点类型;
提高了对闪存的利用率,降低了闪存的消耗。
JFFS2中最重要的数据结构是jffs2_sb_info,这个数据结构用来管理所有的结点链表和闪存块。它在/src/include/Linux/jffs2_fs_sb.h中定义。
Yaffs/Yaffs2(Yet Another Flash File System)和JFFS相比,它减少了一些功能,因此速度更快、占用内存更少。
TrueFFS。该文件系统相当于Linux中的MTD层,必须配合其他文件系统。
FTL/NTFL。它是一种中间层解决方案的统称,为上层文件系统提供接口。
RAMFS、CRAMFS和ROMFS。这些文件系统用于早期的小容量闪存设备,系统功能比较简单,仅提供基本接口,只属于只读的闪存文件系统。适合存储空间小的系统。
7、 MTD简介:
无论JFFS2还是Yaffs,都需要MTD(Memory Technology Devices,内存技术设备)的支持。MTD是对Flash操作的接口,提供了一系列的标准函数,将硬件驱动设计和系统程序设计分开,硬件驱动人员不用了解存储设备的组织方法,只需提供标准的函数调用,如读、写等。
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备注册进mtd_table,mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,
1、 嵌入式系统定义:
“嵌入式系统是用来控制或者监视机器、装置、工厂等大规模系统的设备。”
——电气工程师协会
“嵌入到对象体系中的专用计算机系统”
——北京航空航天大学 何立民教授
“嵌入性”、“专用性”与“计算机系统”是嵌入式系统的三个基本要素。
2、 嵌入式操作系统:
硬实时系统有一个刚性的、不可改变的时间限制,它不允许任何超出时限的错误超时错误会带来损害甚至导致系统失败、或者导致系统不能实现它的预期目标。
软实时系统的时限是柔性灵活的,它可以容忍偶然的超时错误。失败造成的后果并不严重,仅仅是轻微地降低了系统的吞吐量。
我们可以认为至少嵌入式系统都是软实时系统,所有的嵌入式系统都是实时系统,但并不是所有的实时系统都是嵌入式系统。
常用的嵌入式操作系统有:Linux, uC/OS, Windows CE, VxWorks, Palm OS, QNX等。
3、 选择Embedded OS的原则:
系统成本;
市场进入时间及技术支持;
可移植性;
可利用资源;
系统定制能力。
第二章 基于ARM9处理器的硬件开发平台
1、 ARM的历史:
ARM(Advanced RISC Machine)公司于1990年11月在英国剑桥成立。
1991年,ARM推出第一个嵌入式RISC核心——ARM6系列处理器,VLSI、夏普、GEC Plessey、德州仪器、Cirrus Logic等公司相继同ARM公司签署了授权协议。
1998年4月,ARM在伦敦证券交易所和纳斯达克交易所上市。
ARM中国安谋咨询上海有限公司于2002年7月在中国上海成立。
目前基于ARM核的处理器有以下几类:
ARM7家族;
ARM9家族;
ARM9E家族;
ARM10E家族;
ARM11家族;
SecurCore家族;
OptimoDE数据引擎内核;
MPCore多处理器家族;
Intel公司的StrongARM/XScale。
2、ARM7和ARM9处理器的主要区别:
指令流水线:
ARM7:三级,(取指令,译码,执行);
ARM9:五级,(取指,译码,执行,缓冲/数据,回写)。
3、三星S3C2410X处理器:
基于ARM920T核(由ARM9TDMI、存储管理单元MMU和高速缓存三部分组成),片上资源包括:
1个LCD控制器(支持STN和TFT带有触摸屏的液晶显示屏);
SDRAM控制器;
3个通道的UART;
4个通道的DMA;
4个具有PWM功能的计时器和1个内部时钟;
8通道的10位ADC;
触摸屏接口;
I2S总线接口;
2个USB主机接口,1个USB设备接口;
2个SPI接口;
SD接口和MMC卡接口;
看门狗计数器;
117位通用I/O口和24位外部中断源;
第三章 调试嵌入式系统程序
1、 嵌入式系统调试方法:
1) 实时在线仿真(In-Circuit Emulator, ICE)
优点:功能非常强大,软硬件均可做到完全实时在线调试。
缺点:价格昂贵。
2) 模拟调试
优点:简单方便,不需要目标板,成本低。
缺点:功能非常有限,无法实时调试。
3) 软件调试
优点:纯软件,价格较低,简单,软件调试能力较强。
缺点:需要事先烧制监控程序(Monitor)(往往需多次实验才能成功)且目标板工作正常,功能有限,特别是硬件调试能力较差。
4) JTAG调试
优点:方便、简单,无需制作Monitor,软硬件均可调适。
缺点:需要工作基本正常(至少CPU工作正常)的目标板,仅适用于有调试接口的芯片。
2、 ARM仿真器工作原理:
利用高速JTAG(Joint Test Action Group)串行扫描链,通过调试通信通道(Debug Communication Channel, DCC)连接ARM核心内嵌的名为“Embedded-ICE”的调试逻辑,调试逻辑实时监测ARM核心的寄存器、数据总线和地址总线。调试器设置Breakpoint及Watchpoint后,程序在ARM内核全速运行,调试程序实时监测地址与数据总线并与预设值比较,在吻合时产生异常中断通知内核并把控制权交给调试器。这样,在程序全速运行时,可以在断点处停止,可以设置条件断点、条件观测断点等,而又不占用CPU时间及内存资源。
3、 JTAG接口:
1985年制定的检测PCB和IC的一个标准,1990年被修改后成为IEEE的一个标准,及IEEE1149.1-1990。通过这个标准,可对具有JTAG接口芯片的电路进行边界扫描和故障检测。
第四章 创建嵌入式系统开发环境
1、交叉编译步骤:
(1) 创建编译环境。在这个过程中,将设置一些环境变量,创建安装目录,安装内核源代码和头文件等。
(2) 创建binutils。这个过程结束后,会创建类似arm-linux-ld等工具。
Binutils是一组开发工具,包括链接器、汇编器以及其他用于目标文件和档案的工具
首先要安装的软件包使binutils。这非常重要,因为glibc和gcc会针对可用的连接器和汇编器进行多种测试,以决定打开某些特性。
(3) 创建一个交叉编译版本的gcc。注意:在这个过程中只能编译C程序,而不能编译C++程序。
创建交叉编译版本的gcc,需要交叉编译版本的glibc及其头文件,而交叉编译版本的glibc是通过交叉编译版本的gcc创建的。面对这个先有鸡还是先有蛋的问题,解决办法是先只编译对C语言的支持,并禁止支持线程。
(4) 创建一个交叉编译版本的glibc。这里最容易出现问题。
glibc是一个提供系统调用和基本函数的C语言库,比如open,malloc和printf等,所有动态链接的程序都要用到它。创建glibc需要的时间更长。
(5) 创建一个交叉编译版本的gdb。在这个过程结束后,会创建ARM-Linux-gdb。
(6) 重新创建gcc。前面创建gcc的过程没有编译C++编译器,现在glibc已经准备好了,所以这个步骤将完善gcc的交叉编译。
(7) 重新创建glibc。如果成功执行了这个过程,那么你就拥有了一套属于自己的交叉编译工具链。
2、如果在交叉编译过程中出现错误,那么请检查:
版本选择是否正确,以及是否安装了相应的补丁;
库文件路径是否正确;
系统环境变量是否设置正确。
第五章 Bootloader
1、 Bootloader(引导加载程序)是系统加电后运行的第一段代码。一般它只在系统启动时运行非常短的一段时间,但对于嵌入式系统来说,这是一个非常重要的系统组成部分。
2、 嵌入式Linux系统从软件的角度看通常可以分成4个层次:
(1) 引导加载程序。包括固化在固件(Firmware)中的启动代码(可选)和Bootloader两大部分。
(2) 内核。特定于嵌入式板子的定制内核以及控制内核引导系统的参数。
(3) 文件系统。包括根文件系统和建立于Flash内存设备之上的文件系统。通常用Ramdisk作为根文件系统。它是提供管理系统的各种配置文件以及系统执行用户应用程序的良好的运行环境的载体。
(4) 用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。
3、 Bootloader包含两种不同的操作模式:
1) 启动加载(Bootloading)模式;
2) 下载(Downloading)模式。
4、 Bootloader的启动流程:
第一阶段:主要包含依赖于CPU的体系结构硬件初始化的代码,通常都用汇编实现。这个阶段的任务有:
基本的硬件设备初始化(屏蔽所有的中断、关闭处理器内部指令/数据Cache等)
为第二阶段准备RAM空间
如果是从某个固态存储媒质中,则复制Bootloader的第二段代码到RAM
设置堆栈
跳转到第二阶段的程序入口点
第二阶段:通常用C语言完成,以便实现更复杂的功能,也使程序有更好的可读性和可移植性。这个阶段的任务有:
初始化本阶段要使用到的硬件设备
检测系统内存映射
将内核映像和根文件系统映像从FLASH读到RAM
为内核设置启动参数
调用内核
5、 常用U-Boot命令:
命令名
功能
help / ?
帮助命令。用于查询U-Boot支持的命令并列出简单说明,和“?”是同一个命令
bdinfo
察看目标系统参数和变量、目标板的硬件配置、各种变量参数
setenv
设置环境变量。比较常用的有:
setenv ipaddr *.*.*.*
setenv severip *.*.*.*
setenv gatewayip *.*.*.*
setenv ethaddr *.*.*.*.*.*
printenv
查看环境变量
saveenv
保存设置的环境变量到Flash
mw
写内存
md
察看内存
mm
修改内存
flinfo
察看Flash的信息
erase [起始地址 结束地址]
搽除Flash内容,必须以扇区为单位进行搽除
cp [源地址 目标地址 大小]
内存复制,可以在Flash和ram中交换数据
imi [起始地址]
察看内核映像文件
bootm [起始地址]
从某个地址启动内核
tftpboot [起始地址 镜像名]
通过ftp从主机系统下载内核映像文件
reset
复位
第六章 Linux系统在ARM平台的移植
1、 使某一个平台的代码运行在其它平台上的过程就叫做移植。
2、 Linux内核结构:
/arch包含了所有硬件结构特定的内核代码。
Linux系统能支持如此多平台的部分原因是因为内河把原程序代码清晰的划分为体系结构无关部分和体系结构相关部分。对于任何平台,都必须包含以下几个目录:
u boot:包括启动内核所使用的部分或全部平台特有代码。
u kernel:存放支持体系结构特有的(如信号处理和SMP)特征的实现。
u lib:存放高速体系结构特有的(如strlen和memcpy)同用函数的实现。
u mm:存放体系结构特有的内存管理程序的实现。
u math-emu:模拟FPU的代码。对于ARM处理器来说,此目录用mach-xxx代替。
显然,移植工作的重点就是移植arch目录下的文件。
/drivers包含了内核中所有的设备驱动程序。
/fs包含了所有的文件系统的代码。
/include包含了建立内核代码时所需的大部分库文件,这个模块利用其他模块重建内核。该目录也包含了不同平台需要的库文件。比如,asm-arm是arm平台需要的库文件。
/init包含了内核的初始化代码,内核从此处工作。
不是系统的引导代码,由main.c和version.c两个文件。这是研究核心如何工作的好起点。
/ipc包含了进程间通信代码。
/kernel包含了主内核代码。
/mm包含了所有内存管理代码。
/net包含了和网络相关的代码。
3、 在移植过程中,定时器、中断、CACHE管理、MMU等和硬件密切相关的地方都是要相关平台的底层代码支持的,要特别注意。
4、 编译内核:
1) 配置内核
make ARCH=arm CROSS_COMPILE=arm-Linux- menuconfig
2) 创建内核依赖关系
make dep
3) 创建内核镜像文件
make zImage
4) 创建内核模块
make modules
make modules_install
对每一个配置来说,内核生成以后包括4个文件:没有压缩的内核镜像(zImage或bzImage),压缩的内核镜像(vmlinux),内核符号映射文件(System.map)以及配置文件(config)。
第七章 Linux设备驱动程序开发
1、 设备驱动的任务包括:
1) 自动配置和初始化子程序。这部分程序仅在初始化的时候被调用一次。
2) 服务于I/O请求的子程序。这部分是系统调用的结果。在执行这部分程序的时候,系统仍认为和进行调用的进程属于同一个进程,只是由用户态变成了核心态,并具有进行此系统调用的用户程序的运行环境,所以可以在其中调用sleep()等与进程运行环境有关的函数。
2、 设备类型分类:
1) 字符设备(char device)。字符设备是Linux最简单的设备,可以向文件一样访问。
初始化字符设备时,它的设备驱动程序向Linux登记,并在字符设备向量表中增加一个device_struct数据结构条目,这个设备的主设备标识符用作这个向量表的索引。一个设备的主设备标识符是固定的。chrdevs向量表中的每一个条目,一个device_struct数据结构,包括两个元素:一个登记的设备驱动程序的名称的指针和一个指向一组文件操作的指针。参见include/linux/major.h。
2) 块设备(block device)。是文件系统的物质基础,它也支持像文件一样被访问。
Linux用blkdevs向量表维护已经登记的块设备文件。它像chrdevs向量表一样,使用设备的主设备号作为索引。它的条目也是device_struct数据结构。与字符设备不同的是,块设备分为SCSI类和IDE类。类向Linux内核登记并向河心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。参见fs/devices.c。
每一个块设备驱动程序必须提供普通的文件操作接口和对于buffer cache的接口。每一个块设备驱动程序填充blk_dev向量表中的blk_dev_struct数据结构。这个向量表的索引还是设备的主设备号。这个blk_dev_struct数据结构包括一个请求例程的地址和一个指针,指向一个request数据结构的列表,每一个都表达buffer cache向设备读/写一块数据的一个请求。参见drivers/block/ll_rw_blk.c和include/linux/blkdev.h。
当buffer_cache从一个已登记的设备读/写一块数据,或者希望读写一块数据到其他位置时,他救灾blk_dev_struct中增加一个request数据结构。每个request数据结构都有一个指向一个或多个buffer_head数据结构的指针,每一个都是读/写一块数据的请求。如果buffer_head数据结构被锁定(buffer_cache),可能会有一个进程在等待这个缓冲区的阻塞进成完成。每一个request数据结构都是从all_request表中分配的。如果request增加到空的request列表,就调用驱动程序的request函数处理这个request队列,否则驱动程序只是简单的处理request队列中的每一个请求。
块设备驱动程序和字符设备驱动程序的主要区别是:在对字符设备发出读写请求时,实际的硬件I/O一般紧接着就发生了,块设备则不然,它利用一块系统内存作为缓冲区,当用户进程对设备请求能满足用户的要求时,就返回请求的数据,如果不能就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备的,以免耗费过多地CPU时间来等待。
3) 网络设备。
3、 设备驱动中关键数据结构:
1) file_operations:内核内部通过file结构识别设备,通过file_operations数据结构提供文件系统的入口点函数。file_operations定义在
2) inode:文件系统处理的文件所需要的信息在inode(索引节点)中。
3) file:主要用于与文件系统对应的设备驱动程序使用。
第八章 网络设备驱动程序开发
1、 网络设备使用网络接口管理表dev_base。dev_base是一个指向device结构的指针,因为网络设备是通过device数据结构来表示的。
2、 网络驱动程序必须解决的两个问题:
1) 不是所有建立在Linux内核的网络设备驱动程序都会有控制的设备。
2) 系统中的以太网设备总是叫做/dev/eth0到/dev/eth7,而不管底层的设备驱动程序是什么。当驱动程序找到它的以太网设备时,它就填充它现在拥有的ethn的device数据结构。这时网络驱动程序也要初始化它控制的物理硬件,并找出它使用的IRQ、DMA等。一旦所有的8个标准的/dev/ethn都分配了,就不会再探测更多的以太网设备。
3、 两个重要的数据结构:
1) device:这个数据结构是在系统中每一个设备的代表,它提供了多个设备方法,供操作系统或协议层使用。这其中就包括设备初始化时调用的init函数、打开和关闭设备的open和stop函数、处理数据包发送的hard_start_xmit函数等。
2) sk_buff:Linux网络各层之间的数据传送都是通过sk_buff(套接字缓冲区)完成的。每个sk_buff包括一些控制方法和一块数据缓冲区,这个区域存放了网络传输的数据包。控制方法按功能分为两种类型,一种是控制整个buffer链的方法,另一种是控制数据缓冲区的方法。sk_buff组成双向链表的形式,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff数据结构再include/linux/skbuff.h文件中定义。
4、 内核的驱动程序接口:
1) 打开函数net_open:执行“ifconfig eth0 up”命令时网络层调用它,把设备连到线路上并启用来接受/发送数据。open函数在网络设备驱动程序里是网络设备被激活的时候被调用的,即设备状态由down至up。所以实际上很多在初始化的工作可以放到这里来做。
2) 关闭函数net_close:执行“ifconfig eth0 down”时使网卡进入一个清醒的状态。如果硬件许可那么它会释放中断和DMA通道,并完全关闭以节约能源。
3) 探测函数probe/probe1:在启动时调用以检测网卡存在与否。如果可以通过读取内存等非强制手段进行检查最好,也可以从I/O端口读取。在探测函数的最后它会填充device结构各域。
4) 发送函数:它与dev->hard_start_xmit()连接,在内核想通过设备传送数据时调用它。如果发送成功,hard_start_xmit()方法里释放sk_buff,返回0,否则返回1。如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0后才会再次发送;也可以不置dev->tbusy为非0,这样系统会不断尝试重发。tbusy的置0任务一般由中断完成,然后用mark_bh()调用通知系统可以再次发送。
5) 接收函数:它把数据从硬件移出,放在sk_buff结构中,执行netif_rx(sk_buff)告诉内核数据所在位置。真正的处理是在中断返回之后,这样可以减少中断时间。在协议层,接收数据包的流程控制分两个层次:首先,netif_rx()函数限制了从物理层到协议层的数据帧的数量;然后,每一个套接字都有一个队列,限制从协议层到套接字层的数据帧的数量。
6) 中断处理函数:需要了解相关的中断状态位以进行相应的操作。
7) 其他函数。
第九章 USB驱动程序开发
1、 OHCI简介:
OHCI规范定义了两个主机控制器(HC)与主机控制器驱动(HDC)的通信通道(Communication Channel)。第一个为主机控制器操作寄存器,第二个为主机控制器通信域(Host Controller Communications Area, HCCA)。
OHCI规范支持USB四种数据通信方式,并根据数据传输特定,将中断数据传输和等时数据传输归为同一类周期性数据传输方式。在HCCA中定义了4个链表。其中除完成数据链表外,其他的周期性数据链表、控制传输数据链表和批量传输数据链表都是二维链表。每个ED(Endpoint Descriptor)描述USB设备的一个端点的所有的数据传输,所有的ED被连接在一起,而TD(Transfer Descriptor)描述的才是最终要在USB总线上传输的数据包。属于同一个USB设备的端点的TD被连接在一起,并挂在相应的ED上。
主机控制器硬件通过寄存器访问该链表来得到相关的USB传输数据包,并将其发送到USB总线上。主机控制器驱动程序则根据实际的数据传输需要,将要发送的数据包添加到相应的链表上。
OHCI定义了两类TD:通用TD(General TD)和等时TD(Isochronous TD)。通用TD被用来支持USB中断、批量和控制三类传输方式,而等时TD被用来支持USB等时数据传输。用一个单独类型的TD来实现USB等时数据传输的目的是为了方便实现DMA数据传输功能。
2、 Linux下USB系统文件节点:同其他外设一样,上层应用软件对连接在系统地USB设备访问是通过文件系统的形式进行的。每个连接到系统总线上的USB设备可以同时对应有一个或者多个驱动程序。即一个USB设备可以在Linux系统上形成一个或多个设备节点,以供应用程序使用。在Linux系统上,每个设备节点都有其相关的主设备号和次设备号。
3、 USB主机驱动结构:Linux USB主机驱动由三部分组成:
1) USB主机控制器驱动(HCD):是USB主机驱动程序中直接与硬件交互的软件模块,其主要功能有:主机控制器硬件初始化;为USBD层提供相应的接口函数;提供根HUB(ROOT HUB)设备配置、控制功能;完成4种类型的数据传输等。
2) USB驱动(USBD):是整个USB主机驱动的核心,其主要实现的功能有:USB总线管理、USB总线设备、USB总线带宽管理、USB的4种类型数据传输、USB HUB驱动、为USB设备类驱动提供相关接口、提供应用程序访问的USB系统的文件接口等。
3) USB设备类驱动:是最终与应用程序交互的软件模块,其主要实现的功能有:访问特定的USB设备、为应用程序提供访问接口等。
应用程序首先通过文件系统(POSIX)接口来访问相应的USB设备类驱动程序和USBD;USB设备类驱动程序则通过USBD提供的相关接口将数据请求包传递给USBD;USBD通过HCD提供的接口,进一步将数据包传递给HCD;HCD最终将数据发送到USB总线上。Linux定义了通用的数据结构URB用来在USB设备类驱动和USBD,USBD和HCD间进行数据传输。统一的URB(Universal Request Block)结构为usb主机驱动程序的开发带来了很大方便。
4、 USB时序:
1) 数据传输时序:在USB总线上,所有的数据传输都是由USB HOST发起的。每个USB设备通过地址过滤出自己要接受的数据包,并根据数据包请求的类型与USB HOST进行数据传输。由于数据传输的时序和总线带宽问题,当应用程序通过设备类提供一个URB时,该数据包并不能立即被送到USB总线上,而只能在USB总线上有足够带宽的情况下,该数据请求才会被传输。因而,HCD层为不同类型的数据传输维护了相应的数据链,当数据链上的数据包传输结束后,HCD通过调用与该数据包相关联的回调函数来通知设备类驱动程序。
2) 设备连接:当一个USB设备连接到USB总线上时,USB HUB驱动程序首先通过中断数据传输获得设备连接信息,然后通过调用USB内核模块所提供的相关函数来完成对USB设备的配置工作。同时USBD不但要为新设备分配在USBD层所需要的资源,同时也要为USB设备分配在HCD层所需要的资源。接着,USBD通过调用所有USB设备类驱动程序提供的Probe函数来查找适合该USB设备的驱动程序。
3) 设备断开:当一个USB设备断开时,USB HUB驱动程序首先通过中断数据传输获得设备断开信息,然后通过调用USB内核模块所提供的相关函数来释放USBD层和HCD层为该设备分配资源。同时通过调用该设备的相关驱动程序提供的Disconnect函数来通知设备驱动程序该设备已经断开。
第十章 图形用户接口
1、 嵌入式系统中几种流行的GUI:
1) MicroWindows;
2) MiniGUI:是我国为数不多的在国际比较知名的自由软件之一。几乎所有的MiniGUI代码都采用C语言开发,提供了完备的多窗口机制和消息传递机制以及众多空间和其他GUI元素。其引人注目的特性和技术创新主要有:
是一个轻量级的图形系统;
完善的对中日韩文字、输入法的多字体和多字符集支持
提供图形抽象层(GAL)以及输入抽象层(IAL)以适应嵌入式系统各种显示和输入设备
提供MiniGUI-Threads、MiniGUI-Lite、MiniGUI-standone三种不同架构的版本以满足不同的嵌入式系统
提供了丰富的应用软件,其商业版本提供了手机、PDA类产品、媒体及机顶盒类产品以及工业控制方面的诸多程序;
3) Qt/Embedded:是挪威Trolltech软件公司的产品,Linux桌面系统的KDE就是基于Qt库(不是QtE)开发的。使用QtE苦,开发者可以:
当移植QtE程序到不同平台时,只需要重新编译代码,而不需要对代码进行修改;
随意设置程序界面的外观;
方便地为程序连接数据库;
使程序本地化;
将程序与Java集成
同Qt一样,QtE也是用C++写的,虽然这样会增加系统资源消耗,但是却为开发者提供了清晰的程序框架,使开发者能够迅速上手,并且可以方便的编写自定义的用户界面程序。
2、 MiniGUI编程:
它采用类似WINDOWS SDK的窗口和事件驱动机制,MiniGUI的存储空间占用情况如下:
项目
容量
备注
Linux内核
300~500KB
由系统决定
MiniGUI支持库
500~700KB
由编译选项确定
MiniGUI字体、位图等资源
400KB
由应用程序确定,可缩小到200KB以内
GB2312输入法码表
200KB
不是必需的,由应用程序确定
应用程序
1~2MB
由应用程序决定
MiniGUI层次:
FrameWork, MMI, Key Apps
MiniGUI
ANSIC Library
Portable Layer
Devices
Linux/uCLinux, eCos, uC/OS-II, VxWorks, Psos
ix86, ARM, MIPS, PowerPC, M68K
MiniGUI是一个典型的消息驱动的GUI系统,每一个MiniGUI程序都从MiniGUIMain函数开始。应用程序先以CreatMainWindow函数创建一个主窗口,由于窗口创建的时候默认是不可见的,所以我们通过ShowWindow函数显现该窗口,最后通过GetMessage(&Msg, hMainWnd)进入消息循环。
3、 Qt/Embedded编程:
Qt是以工具开发包的形式提供给开发者的,这些工具开发包包括了图形设计器、Makefile制作工具、字体国际化工具和Qr的C++类库等。如果不考虑X窗口系统的需要,基于QrE的应用程序可以直接对缓冲帧进行写操作。QtE提供了大约200个可配置的特征,由此再Intel X86平台上库的大小范围为700KB~5MB。大部分客户选择的配置使得库的大小为1.5~4MB。Qt/Embedded与Qt/X11的Linux版本架构比较如下:
应用源代码
Qt API
Qt/Embedded
Qt/X11
Qt/XLib
X Window sever
帧缓冲
Linux内核
信号与插槽机制提供了对象间的通信机制。Qt的窗口在事件发生后会激发信号,程序员通过建立一个函数(称作插槽)然后调用connect()函数把这个插槽和一个信号连接起来,这样就完成了一个事件和响应代码的连接。信号与插槽机制不需要类之间互相知道细节,这样就可以相对容易地开发出代码可高度重用的类。信号与插槽机制是类型安全的,它以警告的方式报告类型错误,而不会使系统产生崩溃。
Qt拥有丰富的满足不同需求的窗体(如按钮、滚动条等)。窗体是QWidget类或它子类的实例,客户自己的窗体类需要从QWidget的子类继承。一个窗体可以包含任意数量的子窗体,子窗体可以显示在父窗体的客户区,一个没有父窗体的窗体称为顶级窗体(一个“窗口”),一个窗体通常有一个边框和标题栏作为装饰。在父窗体无效、隐藏或被删除后,它的子窗体都会进行同样的操作。
第十一章 系统设计开发
1、 按照嵌入式系统的工程设计方法,嵌入式系统的设计可以分成7个阶段:产品定义、硬件与软件划分、迭代与实现、详细的硬件与软件设计、硬件与软件集成、接受测试和维护与升级。前三个阶段确定要解决的问题及需要完成的目标,也常被称为“需求阶段”;第四阶段主要解决如何在给定的约束条件下完成用户的需求;后三个阶段主要解决如何在所选择的硬件和软件基础上进行整个软/硬件系统的协调实现。
2、 硬件设计主要有以下五个关键步骤:
1) 功能定义;
2) 原理图设计:应尽量做到标准化、通用化、模块化、可扩展化;
3) PCB设计:是硬件设计的难点;
4) 硬件调试;
5) 产品化调整:一个系统的设计样机到产品化是个漫长的过程,单从硬件角度讲,至少要满足功能性、稳定性、可靠性、工艺性等最低要求。
3、 原理图设计中的几点建议:
1) 开关电源和线性电源的优缺点:
优点
缺点
开关电源
输入电压范围宽
效率高
输出功率大
应用比较灵活
电路相对复杂,外围器件较多
对电容电感的要求很高,布线也很讲究
开关频率会给系统带来干扰
纹波比较难控制
线性电源
电路简单,外围器件很少
输出精度高,有很好的负载曲线
工作在低频状态,不会给系统带来麻烦
输入范围比较受限制
效率低,这是由于线性电源自身的损耗造成的
输出功率相对较小
2) NAND和NOR Flash的速度差异:
NOR得读速度比NAND稍快一些;
NAND的写入速度比NOR快很多;
NAND的4ms擦除速度远比NOR的5s快;
大多数写入操作需要先进行擦除操作;
NAND的擦除单元更小,相应的擦除电路更少。
4、 PCB设计要点:
1) 元件封装的准备: 尽量调用标准封装库中的文件;
严密按照所选器件的数据手册上的规范制作封装,不能忽略累计误差;
注意二极管、三极管等极性元件及一些非对称元件的引脚定义不能搞错。
2) 合理布局:
尽量依照参考板的模式进行布局;
模块化布局;
要求模拟电路与数字电路分开;
输入模块与输出模块隔离;
去隅电容尽量靠近元件的电源/地;
电源等发热元件要考虑散热,主发热元件靠近出风口,大体积元件的放置避开风路;
元件分布均匀,避免电流过于密集;
板上的跳线或按键考虑易操作性;
元件的排列尽量整齐美观;
考虑机械尺寸,不要超过结构所允许的范围。
3) PCB分层:
如果有参考板,按照参考板进行分层;
多层板安排:顶层和底层为元件面,第二层为地平面,倒数第二层为电源层;
在不影响性能的情况下,减少PCB层数,降低成本。
4) 电源考虑:
系统电源入口做高频和低频滤波处理;
功率较高的器件配备大容量电容去除低频干扰;
每个器件配备0.1Uf电容过滤高频干扰;
高频器件电源管脚和电容之间串联磁珠达到更好的效果;
去耦电容的引线不能过长,特别是高频旁路电容不能带引线。
5) 时钟考虑:
时钟电路尽量靠近芯片;
晶体下方不要走线;
晶体外科接地,增加抗电磁干扰能力;
频率大于20MHz的时钟信号有地线护送;
时钟线宽大于10mil;
时钟输出端串联22~220Ω的阻尼电阻。
6) 高速信号:
采用手工布线;
高速总线走线尽量等长,并且在靠近数据输出端串联22~300Ω的阻尼电阻;
高速信号远离时钟芯片和晶体;
高速信号远离外部输入输出端口,或地线隔离。
7) 差分信号:
差分信号要平行等长;
信号之间不能走其他信号线;
信号要求在同一层上。
8) 走线规范:
不同层的信号垂直走线;
地线和电源层不要走线,否则要保证平面的完整性;
导线宽度不要突变;
导线变向时导角要大于90度;
定位孔周围0.5mm范围不要走线。
5、 硬件调试:
优先调试电源:保证系统可靠地供电;
分模块调试:可以分清模块间的问题,不至于混淆;
结合软件调试:对于复杂的接口,单纯硬件角度不易调试,结合软件从不同的角度测试,能起到更好的效果。
正确、合理的使用示波器,提高工作效率;
对比调试:有条件用评估板或功能相似的电路板作参考,比较差异并找出问题所在;
系统时钟受干扰或晶体震荡不正常导致系统工作故障;
复位不可靠,造成个单元未进入预期状态而出现问题;
因焊接问题引发的各种问题,如方向焊错、虚焊、错焊等;
因时序不匹配引发的通信故障,如时钟信号通过逻辑器件后产生延时,与读写信号时序搭配不上导致读写错误。
6、 嵌入式文件系统:
目前支持闪存的文件系统技术有以下几种:
JFFS2和Yaffs。这些文件系统可以使用在没有初始化的NAND Flash和有CFI接口的NOR Flash中。JFFS2的特点包括:
支持数据压缩;
提供了“写平衡”支持;
支持多种结点类型;
提高了对闪存的利用率,降低了闪存的消耗。
JFFS2中最重要的数据结构是jffs2_sb_info,这个数据结构用来管理所有的结点链表和闪存块。它在/src/include/Linux/jffs2_fs_sb.h中定义。
Yaffs/Yaffs2(Yet Another Flash File System)和JFFS相比,它减少了一些功能,因此速度更快、占用内存更少。
TrueFFS。该文件系统相当于Linux中的MTD层,必须配合其他文件系统。
FTL/NTFL。它是一种中间层解决方案的统称,为上层文件系统提供接口。
RAMFS、CRAMFS和ROMFS。这些文件系统用于早期的小容量闪存设备,系统功能比较简单,仅提供基本接口,只属于只读的闪存文件系统。适合存储空间小的系统。
7、 MTD简介:
无论JFFS2还是Yaffs,都需要MTD(Memory Technology Devices,内存技术设备)的支持。MTD是对Flash操作的接口,提供了一系列的标准函数,将硬件驱动设计和系统程序设计分开,硬件驱动人员不用了解存储设备的组织方法,只需提供标准的函数调用,如读、写等。
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备注册进mtd_table,mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,
订阅:
博文 (Atom)
