shell程序设计(三)

2.3.6 shell内置命令

  • trap命令(常被称为陷阱),用于指定在接收到系统信号后所要执行的命令。常用于脚本程序被中断执行时指定需要完成的配置或文件的清理工作。历史上,shell中常用不同的数字来代表不同的信号,现在逐渐使用信号的名字来明确指定信号的类型,这有利于代码的可读性。信号名字在头文件signal.h中定义,使用时需要省略SIG前缀。可以在命令行下使用trap –l查看信号的编号和对应的信号名。(信号是那些被异步发送给程序的事件,默认情况下常会导致程序运行的终止)。trap命令有两个参数,前一个是接受到信号后要执行的命令,后一个是要处理的信号名(或对应的信号值):
trap command signal

          由于脚本程序是从起始到结束循序执行的,所以必须在期望保护的代码段之前指定trap命令内容.可以将command设置为-来重置命令的默认处理方式.如果要忽略某个命令可以将command设置为空字符串’’.如下是比较重要的一些信号:

HUP(1)         挂起,终端掉线或用户退出引发
INT(2)          中断,Ctrl+C时引发
QUIT(3)       退出,常在按下Ctrl+\组合键时引发
ABRT(6)       中止,常为某些严重的执行错误引发
ALRM(14)    报警,常用于处理超时
TREM(15)    终止,常在系统关机时发出

         使用举例如下,在循环执行过程中,按下ctrl+C组合键就会触发INT信号,执行相应的命令,最终临时文件被删除,循环条件不在满足,循环退出:

trap 'rm -f /tmp/my_tmp_file_$$' INT
date > /tmp/my_tmp_file_$$
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo file exists
done

  • unset命令,从环境中删除相应的变量和函数,类似于清除类变量及其释放对应的内存空间。但不能删除shell本身定义的只读变量(如IFS),这个命令并不常用。举例如下:
foo="Hello world"
echo $foo
 
unset foo
echo $foo #执行结果为空

  • find命令,和grep命令虽然都不是shell的一部分,但在编写是常被用到。该命令用于搜索文件(简单的说来就是找你想要的文件及其路径),它的命令的完整内容有选项、测试和动作参数组成,使用较复杂。举例,在本地机器上查找文件名为test的文件:
$ find / -name test -print #请使用root权限执行

该命令的完整语法格式如下:

find [path] [options] [tests] [actions]

path部分:既可以使用绝对路径/bin,也可以使用相对路径.。如果需要还可以指定多个路径,如find /var /home。

options部分有如下参数可选:

-depth                   在查看目录前先搜索目录内容
-follow                   跟随符号链接   
-maxdepths N       最多搜索N层目录
-mount(或-xdev)  不搜索其他文件系统中的目录

tests部分的条件组合很多,但返回的结果只有两种可能:true和false。测试条件将依照定义的顺序依次在搜索到的每个文件上执行,若返回false则跳过该文件继续搜索,为true则继续测试该文件或执行相应的动作。常用的测试如下:

-atime N         文件在N天前最后被访问过
-mtime N         文件在N天前最后被修改过
-name pattern    文件名匹配模式,为确保内容被find处理而不是shell,
                   该内容常被引号括起来
-newer otherfile 文件是否比otherfile要新 
-type c          文件类型是否为c,或d(目录),或f(普通文件)
-user username   文件所有者是否为username

          还可以使用如下操作符与测试条件形成组合测试条件,操作符分短格式和长格式:

!   -not   条件取反
-a  -and   两个都为真时为真
-o  -or    两个测试条件有一个及以上为真时结果为真

         还可以使用圆括号括起测试和操作符来调整测试条件的优先级,但圆括号对shell有特殊意义所以要对齐进行转义。使用举例如下,查找当前路径下以下划线开头的文件或比test文件要新的文件,但这两种情况下的文件都必须是普通文件:

$ find . \( -name "_*" -or -newer while2 \) -type f -print

常见的动作actions部分:匹配到指定的文件后,需要执行的命令:

-exec command 执行一条命令,必须以\;结尾
-ok command   与-exec类似,但是在执行命令前
                        会针对每个文件要求用户进行确认
-print        打印文件名
-ls           对当前文件使用ls -dils命令

应用举例如下,魔术字符串是-exec和-ok命令的一个特殊参数,输出文件的完整路径:

$ find . -newer test -type f -exec ls -l {} \;

  • grep命令,和find一样几乎是Linux用户必须掌握的命令。全称叫做通用正则表达式解析器(Genneral Regular Expression Parser,简写为grep)。find指令搜索到了文件,grep就可以用于在文件中查找字符串。在实践中常用的方法是将grep作为传递给find命令的-exec的一条命令执行。grep完整的命令内容包括一个选项一个要匹配的模式和要搜索的源文件,语法格式如下:
grep [options] PATTERN [FILES]

如果未提供源文件名则grep将在标准输入中进行搜索。主要选项字段如下:

-c    输出匹配行的数目而不是匹配行内容
-E    启用扩展表达式
-h    取消每个输出行的普通前缀,即不列出匹配查询模式的文件名
-i    忽略大小写
-l    只列出存在匹配行的文件的文件名,而不列出匹配行的内容
-v    对匹配模式取反,即搜索不匹配行

应用举例,查找不含echo的文件的匹配行数目:

$ grep -c -v echo test.sh f.sh 1.sh 2.txt 3.c

  • 正则表达式:正则表达式常用于书写复杂的匹配模式,被广泛应用于Linux和许多相关的开源编程语言.可以再Vi和Perl中使用,且由于其通用性很高,在不同地方出现时,其基本原理都相同.在通用正则表达式中,一些字符是以特殊的方式处理的.
^    标记行的开头
$    标记行的结尾
.    指代单个任意字符
[]   括号内包含一个字符范围,其中任一字符均可被匹配例如字符范围a~e
      在字符范围前使用^表示反向不匹配字符的范围

匹配模式:

[:alnum:] 字母或数字字符

[:alpha:] 字母

[:ascii:] ascii字符

[:blank:] 空格或制表符

[:cntrl:] asfii控制字符

[:digit:] 数字

[:graph:] 非控制非空格字符

[:lower:]    小写字符
[:print:]    可打印字符
[:punct:]    标点符号字符
[:space:]    空白字符包括垂直制表符
[:upper:]    大写字母
[:xdigit:]   十六进制数字

可以使用如下选项对正则表达式进行扩展的规则表示,如下字符前都需要加\字符进行转义:

?       匹配是可选的,最多匹配1次
*       匹配0或多次
+       匹配1或多次
{n}     必须匹配n次
{n,}    匹配n或n次以上
{n,m}   匹配n到m次,包括n和m

          应用举例:

$ grep e$ test.txt #匹配以e结尾的行
$ grep a[[:blank:]] test.txt  #匹配以a结尾的单词
$ grep Th.[[:space:]]   #匹配以Th开头的三个字母组成的单词
$ grep -E [a-z]\{10\} test.txt   #匹配10个字符长的小写字母组成的单词

2.3.7 执行结果的获取

      我们常常需要捕获命令的执行结果,并暂存在某个变量以待处理、在shell中$(command)语法即表示了command的执行结果,也可以使用`command`反引号,这样得到的是字符串形式的输出结果而不是命令的退出状态。注意要区分环境变量和shell命令,比如pwd是命令而PWD是环境变量,对于变量不需要加圆括号($(pwd)和$PWD)。可以将$(command)的结果直接赋给某个变量暂存起来。如果期望将某条命令的标准输出转换为其他程序的参数则可以使用xargs。

  • 算术扩展

我们可以使用expr命令,通过它可以处理一些简单的算术命令,但命令需要调用子shell去执行expr命令,效率很慢,这里可以使用$((expression))扩展来实现算法运算,举例如下:

#!/bin/bash
 
x=0
while [ "$x" -ne 10 ]; do
    echo $x
    x=$(($x+1))
done
 
exit 0

注意:x=$(command)和x=((expression))是不同的,两对圆括号是用于算术计算和替换,一对圆括号用于获取命令的执行结果。

  • 参数扩展

当我们期望在变量名后附加额外的字符形成新字符时,为了保护变量名中内置的其他变量,需要特别注意此时需要将变量用花括号括起来。比如:

i=10
echo $i_tmp#出现错误
echo ${i}_tmp #正确

在处理多参数扩展时,如下的参数扩展方法可非常精巧的实现相关功能:

${param:-default}  若param为空则设为default值
${#param}        给出param长度
${param%word}    从param尾部                   最小部分
${param%%word}   从param的尾部                  最长部分
${param#word}     param的头部                     最小部分
${param##word}   从param的头部开始删除与word匹配的最长部分,返回剩余内容

应用举例:

#!/bin/bash
 
unset foo
echo ${foo:bar}
 
foo=fud
echo ${foo:bar}
 
foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}
 
foo=/usr/local/etc/local/networks
echo $ {foo%local*}
echo ${foo%%local*}
 
exit 0

输出结果为:
bar
fud
usr/bin/X11/startx
startx
/usr/local/etc
/usr

${foo:=bar}:foo不存在则赋值为bar否则返回foo的值。${foo:?bar}foo不存在或为空时输出foo:bar并异常终止程序。${foo:+bar}:foo存在且不为空时返回bar。linux下可用cjepg程序将GIF装换为JPEG文件:cjpeg image.gif > image.jpg,如下程序可以实现本路径下的批量处理:

#!/bin/bash
 
for image in *.gif; do
    cjpeg $image > ${image%%gif}jpg
done
exit 0

2.3.8 here文档

        在shell中我们可以向某条命令或程序传递参数,比如:调用Vi启动插入模式,将shell脚本的首行内容从#!/bin/bash改变为#!/bin/sh,这样的完整操作都需要给Vi依照执行顺序传递多个不同参数。此时shell中的here文档就派上了用场。它使传递给命令的参数类似于人们在键盘输入的模式。here文档以<<开始紧跟一个特殊的字符标记序列(常用大写且容易记忆的单词有助于可读性),该序列还需要在结尾处再次出现以标记文档的结束。<<是shell的标签重定向符,在此处,它强制命令的输入是一个here文档

#!/bin/sh
 
cat <<!FUNKY!
hello
this is a here
document
!FUNKY!
输出结果如下:
hello
this is ahere
document

here文档的更常见的用途是输出大量的文本且避免繁复使用echo来输出每一行。在标识符前后使用!!有助于避免混淆。可以通过ed行编辑器来处理文件的多个行,且可以再脚本中直接使用here文档来向它提供命令。

2.3.9 脚本程序的调试

         出现错误时,脚本程序一般都会打印错误的行的行号,如果这个错误不明显还可以使用echo命令来显示相关的变量内容,也可以在shell中交互式的输入代码段进行调试。跟踪脚本程序中复杂错误的主要方法是设置各种shell选项,为此可以再调用shell时加上命令行选项或使用set命令。

sh -n <script>   set -o noexec(set -n)  只检查语法错误
sh -v <script>   set -o verbose(set -v)  命令执行前回显
sh -n <script>   set -o xtrace(set -x)  命令处理完前回显
sh -n <script>   set -o nounset(set -u) 若使用未定义的变量则给出错误信息

可以使用-o选项开启相关设置,+o选项关闭相关设置.如果期望获取更好的调试效果,可以将xtrace标记(启用或关闭命令跟踪)放在脚本程序问题代码前后,执行跟踪功能,让shell在执行每行语句之前先输出改行并对改行变量进行扩展.可以再shell脚本的开始处添加如下语句,来捕获exit信号并查看退出时的程序状态:

trap 'echo Exiting: critical variable=$critical_variable' EXIT

3、图形化:dialog工具

image

     大部分linux用户都应该见过类似如上的图形界面。这就是用linux下的dialog工具命令输出的简单的图形界面,常用于shell的简单图形编程。部分linux发型版可能是依赖gnome用户接口的gdialog,比如ubuntu。dialog主要用于创建对话框。如下对话框类型:

复选框  --checklist 参数:text height width list-height [tag text status]...
信息框  --infobox   参数:text height width
输入框  --inputbox  参数:text height width [initial string]
菜单框  --menu      参数:text height width menu-height [tag item]...
消息框  --msgbox    参数:text height width
单选框  --radiolist 参数:text height width list-height [tag text status]...
文本框  --textbox   参数:filename text height width
是否框  --yesno     参数:text height width

应用举例:

dialog --title "CheckList" --checklist "Pick Numbers" 15 25 3 1"one"
                     "off" 2 "two" "on" 3 "three" "off"

测试结果如下:

image

(完)。更多内容请参看BLP 4th和Linux shell脚本攻略。

,

shell程序设计(二)

2.3.3 控制语句

  • if语句
if condition
then
statements
else
statements
fi

  • elif语句,增加第二个测试条件
if condition; then
statements
elif condition; then
statements
else
statements
fi

  • for语句
for variable in values
do
statements
done

  • while语句
while condition do
statements
done

  • until语句,与while相似,只是测试条件反过来了,将反复执行直到测试条件为真
until condition
do
statements
done

  • case语句,类似于C中的switch case,前面是匹配模式后面是执行路径。注意它的每个路径都是以两个分号;;结尾,因为statements可以是多条语句所以需要;;结尾
case variable in
pattern [ | pattern ] ... ) statements;;
pattern [ | pattern ] ... ) statements;;
...
esac
#!/bin/bash
#case 使用举例 
echo "Boy? Please answer yes or no"
read sex
 
case "$sex" in
     yes | y | Yes | YES ) echo "how about to play basketball?";;
     n* | N* )             echo "how about to have some coffee?";;
     * )                   echo "Sorry, answer not recognized";;
esac
 
exit 0

2.3.4 命令列表

     如果我们希望将多条命令连接成一个序列,则可以使用AND或OR列表,这类似于C中的含义,与操作符(&&)和或操作符(||),且AND和OR列表可以混合使用。

  • AND列表
statement1 && statement2 && ...

使用举例:

#!/bin/bash
 
touch file_one
rm -f file_two
 
if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo "there"
then
    echo "in if"
else
echo "in else"
fi
 
exit 0

  • OR列表
statement1 || statement2 || ...
  • 如果希望某个statement包含多个语句,则可以使用{}来包含这些语句形成语句块
get_confirm && {
    read myname
    echo &name
}

2.3.5 函数

     在shell中定义函数,只需写出它的名字,然后跟一堆圆括号,再把语句块放入一堆花括号中,如下:

funtion_name () {
    statements
}
简单函数举例:
var="global var"
foo () {
    local var="local var"
    echo $var
}
echo $var
 
result="$(foo)"
echo "$result"

     如上,函数中可以使用local定义只能在函数内部使用的局部变量,且函数内部的局部变量会覆盖全局变量.在函数中,我们可以使用return命令让函数返回数字值,同样可以使用echo命令将保存在某个变量的字符串输出作为函数的返回结果。当然直接使用在函数中定义的字符串变量也是一个函数返回字符串的方法(非local变量可以在外部使用)。注意:脚本语言都是定义后才能使用的,所以函数的定义一定要在其使用的语句的前面实现。函数返回值使用举例:

         1.  在shell头定义函数yes_or_no

#! /bin/bash
 
yes_or_no () {
echo "Is your name $* ?"
  while true
  do
    echo -n "Enter yes or no: "
    read x
    case $x in
        y | yes ) return 0;;
        n | no )  return 1;;
        * )       ecoh "Answer yes or no, please"
    esac
  done

        2.  主程序部分:

echo "Original parameters are $*"
 
if yes_or_no "$1"
then
    echo "Hi $1, nice to meet you"
else
    echo "Never mind"
fi
exit 0

         3. 脚本的输出结果

$ ./my_name Qiu Liang
Original parameters are Qiu Liang
Is you name Qiu ?
Enter yes or no: yes
Hi Qiu, nice to meet you

      可以看到脚本把第一个参数传递给了函数yes_or_no成为函数自己的参数(函数有自己的独立的位置参数,对应的$*,还有相应的$1和$2等位置参数),并且调用后返回相应的值。

2.3.6 shell内置命令

     shell脚本中可以执行两类命令:可以再命令行提示符下执行的普通命令也称为外部命令,在shell内部实现不能被外部程序调用的内置命令也称为内部命令。但大多数的内部命令都提供了独立运行的程序版本——这是POSIX程序规范的一部分。内部命令的执行效率更高。

     如下将主要介绍在编写shell程序是可能用到的主要命令,不细分内外部也不罗列所有指令。

  • break命令,在控制条件为满足前,挑出for、while或until循环。可以为其提供额外参数表明跳出的循环层数,但这是不建议的行为。break默认跳出一层循环。
  • :命令,冒号命令,是一个空命令,常被用于简化条件逻辑,相当于true的别名,是内置命令处理速度较true快。例如使用while :代替while true.它也会被用于变量的条件设置中,在旧时shell中冒号也被用于行首表示注释的开始,但现在都用#(效率更高),举例如下:
${var : =value}
#这条命令检查var是否为空,若为空则把value赋值给var并返回这个值,否则返回var的值。如果没有:,shell将试图将$var当做一条命令处理

  • continue命令非常类似于C语言中的continue,相关设定可以参考它的兄弟break。
  • .命令,用于执行当前路径下的脚本,标记了当前路径(默认情况下不包含在PATH中),常被用于设定命令执行的开发环境。
  • echo命令,用于输出字符串,它的字符串带有默认的换行符。若希望不换行可以使用如下方法:
echo -n "Enter your name: "

  • eval命令,允许对参数进行求值,是内部命令。举例如下
foo=10
x=foo
y='$'$x
echo $y
输出为:$foo,而:
foo=10
x=foo
eval y='$'$x
echo $y
输出结果为10
  • exec命令,它有两种不同的用法,最常见的用法是将当前的shell替换为一个不同的程序,例如:
exec wall "Thanks for you love"

脚本中exec用wall替换了当前的shell,其后面的代码不会被执行,因为执行这个脚本的shell已经不存在。exec的第二个功能是修改当前文件描述符:

exec 3< afile

#使得文件描述符3被打开以便从afile文件读取数据,这种用法很少见

  • exit命令,使脚本程序以指定的退出码结束运行,在当前shell命令行输入则推出系统。如果脚本程序在退出时未指定退出状态则该脚本的最后一条命令的状态将被用作为返回值。在脚本中定义退出码是个良好的编程习惯。在shell中退出码0表示成功,退出码1~125是用户可自定义的错误代码,其余数字具有保留意义,例如:126表示文件不可执行,127表示命令未找到,128及以上表示出现一个信号。所以这里也涉及了一个真理“幸福的家庭都是相似的,不幸的家庭各有各的不幸.^_^”
  • export命令,将作为其参数的变量导出到子shell中,使其在子shell中有效。默认情况下,在shell中创建的变量在子shell中是不可见的。export把自己的参数设置成环境变量后就可以被当前程序调用的其他脚本或程序看见,被导出的变量构成从该shell衍生的任何子进程的环境变量。
#!/bin/bash
 
#脚本程序export2
echo "$foo"
echo "$bar"
 
#!/bin/bash
 
#脚本程序export1
 
foo="The first var"
export bar="The second var"
 
export2
 
执行结果:
$./export1
 
The second var
$foo在export2中未定义所以输出null变量成为一个空行。一旦一个变量被shell导出,
它就可以被该shell调用的任意脚本使用,也可以被依次调用的脚本使用
  • expr命令,将其参数作为表达式来求值,常用于简单的数学表达式运算,举例如下:
x=`expr $x + 1`或者x=$(expr $x + 1)

反引号字符(“)使x取值为命令expr $x + 1的执行结果,也可以使用语法$()替换反引号“。主要的求值运算如下:

  1. expr1 | expr2:   若expr1为零则结果为expr2,否则为expr1
  2. expr1 & expr2: 只有有表达式为0则为0,否则为expr1
  3. expr1 = expr2:  等于
  4. expr1> expr2:   大于
  5. expr1 >= expr2:大于或等于
  6. expr1 < expr2:   小于
  7. expr1 <= expr2:小于等于
  8. expr1 != expr2: 不等于
  9. expr1 + expr2:  加法
  10. expr1 – expr2:   减法
  11. expr1 * expr2:   乘法
  12. expr1 / expr2:   整除
  13. expr1 % expr2: 取余
  • printf命令,类似于C中的printf,但不支持浮点数输出,因为shell中只有整数运算。在X/Open规范中被建议用于替换echo命令。格式字符串包含可打印字符、转义序列和字符转换限定符。语法如下:
printf "format string" parameter1 parameter2...

使用举例:

$ printf "%s\n" hello

hello

$printf "%s %d\t%s" "Hi there" 15 people

Hi there 15     people

转义序列:
\"    双引号
\\    反斜线字符
\a   响铃
\b   退格字符
\c    取消进一步的输出
\f    换页符
\n   换行符
\r    回车符
\t    制表符
\v    垂直制表符
\ooo   八进制ooo表示的单个字符
\xHH  十六进制值HH表示的值
字符转换限定符:
d    十进制
c    单个字符
s    字符串
%   输出一个%
  • return命令,前面有过介绍和举例不在细说,如果未指定参数,则return默认返回最后一条命令的退出码。
  • set命令为shell设置参数变量,许多命令的输出结果是以空格分隔的值,如果需要提取出结果中的某个域,则这个命令非常有用。举例:提取当前系统日期的月份,系统本身有date命令输出字符串形式的日月小时年等参数,但是需要将月份和其他区域分开。此时,可以使用set和$()结构并结合date的执行结果,详细命令如下:
#!/bin/bash
 
echo the date is $(date)
set $(date)
echo The month is $2
 
exit 0
当然依照相同的原理,我们可以将date的结果通过赋值为函数的参数
实现隐式的参数变量设置来实现我们的目的:
#!/bin/bash
 
test_code () {
    echo $2
}
 
test_code $(date)
exit 0

  • shift命令,将所有参数变量左移一个位置,类似于将参数起始指针向右移一个参数位置,使$1指向$2的值,$2指向$3的值依此类推。原$1的值被丢弃,$0(脚本名字)不变。常用于扫描脚本程序的参数,特别是在脚本参数10个及以上时需要使用shift来访问或许参数。

 

2.3.7 执行结果的获取

 

2.3.8 here文档

,

shell程序设计(一)

        经过一个多月的工作和论文的煎熬,再次回到了blog,Good bless,活着真好。这次将简要的总结一下shell编程知识。

       什么是shell?shell就是操作系统内核的外壳,提供了许多方便用户调用操作系统接口的指令并简化和功能化,能够实现几乎全部的操作系统和应用软件的管理操作。shell类似于windows下的CMD,但其功能强大指令简单之处远远超过cmd。大体的说来,几乎所有Unix系的操作系统都提供了这样一个脚本程序用于调用系统接口和通信。

       shell编程具有语法和建构简单、入门和熟练快速、解释执行不需编译、开发周期短,非常适合与构建小巧的或时间性能要求高的程序,但shell语言作为一种解释性的低效的脚本语言,性能上必然有硬伤。常见的做法是使用shell简单迅速的构建模型,再用C/C++、Perl、Python等更加快速的语言来重新实现。从最初的Unix的Bourne shell开始,现在已经演化出了多种不同的shell,有sh(即为Bourne shell)、csh及其变体tcsh和zsh、商业版本的ksh和pdksh、Linux下默认配置也是最兴旺的bash等。shell语言大多遵循X/Open4.2和POSIX 1003.2规范。当然,我们这里接触到的就是最流行的bash,介绍的也是Linux系统下的bash,bash开源且很容易就可以移植到unix系统中。操作系统这一块,我使用的是open suse12.1,德国人的linux发行版,德国的品质值得信赖,稳定性好,工具齐全,国内很多公司都将其产品用于搭建服务器(比如:某china wei,还有疼逊等,据说是国内Linux服务器占有量最大的发行版),suse厂商已经和高端前沿机构合作将suse用于高端和超级计算机的操作系统。

1、管道和重定向

      在正式进入shell编程前,需要了解一下Linux程序(不仅是shell程序)的输入输出重定向的操作。

1.1 输出重定向

      简单举例:
$ ls -l > lsoutput.txt

这条命令使用>操作符把ls -l命令的输出保存到文件lsoutput.txt中,默认情况下,若当前路径存在同名文件,则新产生的lsoutput.txt会将其覆盖。可以使用set –o noclobber(或set -C)命令阻止自动覆盖,同时可以使用set +o noclobber命令取消该设置。
      还可以使用>>操作符将输出的内容附件到某个文件的尾部:
$ ps >> lsoutput.txt

当需要丢弃错误信息并阻止它显示在屏幕上是可以重定向标准错误输出到某个文件,标准错误输出的文件描述编号为2,所以使用2>操作符。举例如下:
$ kill -HUP 1234 >killout.txt 2>killerr.txt
此时标准输出重定向到了killout.txt而标准错误输出重定向到了killerr.txt文件。当然也可以将标准输出定向到某个文件然后将标准错误输出到与标准输出相同的文件,举例如下:
$ kill –1 1234 >killouterr.txt 2>&1
如果不需要标准输出和标准错误输出的内容可以把它们重定向到Linux的回收站中:
$ kill –1 1234 >/dev/null 2>&1

1.2 重定向输入

    重定向标准输入的操作符为<,举例如下:

$ more < killout.txt

1.3 管道

     管道允许多个进程(命令)通过管道操作符|连接到一起同时运行,且数据流在他们之间的传递可以自动协调。举例:使用sort命令对ps执行的输出进行排序,如果不使用管道,则命令如下:

$ ps > psout.txt
$ sort psout.txt > pssort.txt
使用管道,则:
$ ps | more > pssort.txt
若希望结果在界面分页显示可以再连接more命令:
$ ps | sort | more
允许连接在一起的指令数目没有限制。例如,你可以使用如下指令查看除shell本身之外的所有进程的名字且这些进程都是按照字母顺序输出:
$ ps –xo comm | sort | uniq | grep –v sh | more
uniq命令去除了名字相同的进程。

使用管道需要注意:不要在命令流中使用相同的文件名,否则输出文件在命令创建的时候理解被创建和写入,文件会被相互覆盖。举例:

$ cat mydata.txt | sort | uniq > mydata.txt
最终得到的是空文件,因为读取mydata.txt之前已被覆盖。

2、shell程序设计

2.1 命令行上的交互式程序

      对于短小的测试命令,完全可以直接在命令行下直接输出。这里主要需要注意的是通配符*,单字符匹配?和[ab]匹配其中的任一字符(以及[^ab]取反)和{ac,bc}匹配任一字符串.grep命令使用举例:

$ grep -l POSIX * | more

2.2 shell脚本创建和设置

可以使用>命令来创建文件然后用文本编辑器打开并编辑,例如:创建一个名为first的文件:

    $ > first

    $ vi first

或者直接vi first后编辑并保存。下面是一个最经典的hello world程序:

#!/bin/bash
 
#first
#My first shell script
 
echo "hello world"
exit 0

shell中的注释用#开始直到该行结束。第一行#!/bin/bash是特殊形式的注释,#!字符告诉系统使用该行后面的程序来执行本文件的程序。在shell中0表示成功。

创建好第一个脚本文件后,需要将该文件加可执行权限(在unix系下的文件为了保证安全性刚创建时均无可执行权限)后才能执行:

$ chmod u+x first

$ first (PATH不一定包含当前路径,最好使用./first指明可执行文件的全路径)

2.3 shell语法

shell的语法内容包括如下部分:

  • 变量:字符串、数字、环境和参数
  • 测试条件:shell中的bool值
  • 程序控制语句:if、elif、for、while、until、case
  • 命令列表
  • 函数
  • shell内置命令
  • 命令执行结果的获取
  • here文档

2.3.1 变量

    在shell中,使用变量不用事先声明直接赋值并使用即可,且没有变量类型的烦恼,因为在shell的默认情况下,所有内容优势看做字符串的形式存储并处理(这也可以看出shell的简便和低效),即使变量被赋值为数值亦是如此。shell和其他工具程序会在需要的时候将数值型字符串转换为对应的数值并执行对应的操作。Linux系统下,字符的大小写是区分的,所以这一点在编程和命令行执行命令是需要注意。变量使用举例:

foo=hello
echo $foo

    可以再变量前使用$符号来实现对变量的内容的访问.当变量被赋值时,变量根据需要自动创建.在shell编程中常常在变量名前加上$,然后使用echo命令将其内容输出到终端显示.注意将字符串赋给某个变量时,如果字符串内容包含空格则必须使用引号括起来,且等号两边不能有空格.

  • 引号的使用

       shell脚本中的参数一般以空白字符分隔(空格、制表符或换行符)。若希望在一个参数中包含一个或多个空白字符则必须给参数加上引号。引号分单引号和双引号,在单引号中$foo不会被扩展,在双引号中则会被扩展,可以再$前使用\符号消除其特殊含义将其看成普通字符。

#!/bin/bash
 
myvar="Hi, Qiu"
 
echo $myvar
echo "$myvar"
echo '$myvar'
echo "\$myvar"
 
echo Enter some words
read myvar
 
echo '$myvar' now equals $myvar
exit 0
 
输出结果如下:
$ ./var
Hi, Qiu
Hi, Qiu
$myvar
$myvar
Enter some words
Good Job!
$myvar now equals Good job!

      shell中可以使用read命令将用户的输入保存到某个变量中。

  • 环境变量

    在shell脚本开始执行时,shell环境的一些环境变量的值就会进行初始化,这些变量通常使用大写字母表示,而用户自定义的变量名按惯例都是用小写字母,具体创建的变量及其内容与个人的配置相关。主要的环境变量如下:

  1. $HOME:当前用户的家目录
  2. $PATH: 命令所在的目录的列表,以冒号分隔
  3. $PS1 :   命令提示符,通常为$,当然也可以是更复杂的字符串,例如[\u@\u \W]$,给出当前的用户名和主机名以及当前的目录还有$
  4. $PS2:    二级提示符,用于提示后续的输入,通常为>字符
  5. $IFS:     输入域分隔符,在shell读取输入时,输入的内容的单词分隔字符,通常为空格、制表符和换行符或逗号
  6. $0:        shell脚本的名字
  7. $#:        传递给脚本的额参数个数
  8. $$:        shell脚本的进程号,常被用于生成唯一的临时文件,如/tmp/tmpfile_$$
  • 参数变量

    如果脚本在调用时被附加了参数,则一些额外的变量将会被创建。即使不传递参数给脚本,环境变量$#依然存在但为0。

  1. $1, $2,…,$k:脚本程序的第k个参数
  2. $*:                脚本的所有参数暂存的变量,它们被当前的IFS环境变量隔开
  3. $@:              是$*的精巧变体,但不使用IFS环境变量

2.3.2 测试条件

      可以说所有程序设计语言的基础都是对条件进行测试然后确定执行路径。在shell中,shell能对任意可以从命令行上调用的命令的退出码进行测试,包括用户自己编写的脚本,这就是脚本最后都有包括exit返回值的命令。测试程序举例如下:

if test -f file.txt
then
...
fi

      如上脚本检测了file.txt文件是否存在,shell中test后跟测试条件,或者将测试条件放在[后。大多数的脚本语言都会广泛的使用shell的bool判断命令[或者test,为了增强可读性使用[后还需要使用]符号来结尾。

if [ -f file.txt ]; then
...
fi

       test命令的退出码决定了是否需要执行后续操作,需要注意的是对于[符号,需要在它和被测试的条件之间留出空格,[类似于test,所以之后需要有空格来区分。test命令的条件类型可以归为3类:字符串比较、算数比较和文件相关的测试。

  • 字符串比较
  • string1 = string2:  如果两个字符串相等则为真
  • string1 != string2: 两个字符串不同则为真
  • -n string:                 如果字符串不为空则为真
  • -z string:                  如果字符串为空串则为真
  • 算法比较
    • expression1 –eq expression2 :两个表达式相等则为真
    • expression1 –ne expression2 :不等则为真
    • expression1 –gt expression2 :前者比后者大则为真
    • expression1 –ge expression2 :前者大于等于后者则为真
    • expression1 –lt expression2 :前者小于后者则为真
    • expression1 –le expression2 :前者小于等于后者则为真
    • ! expression:表达式为假则为真
  • 文件测试
    • -d file:文件为目录则为真
    • -e file:文件存在则为真,该选项不可移植
    • -f file:文件为普通文件则为真
    • -g file:文件的set-group-id被设置则为真
    • -r file:文件可读则为真
    • -s file:文件大小不为0则为真
    • -u file:文件的set-user-id被设置则为真
    • -w file:文件可写则为真
    • -x file:文件可执行则为真
    #!/bin/bash
     
    if [ -f /bin/csh ]; then
    echo "file /bin/csh exists"
    fi
     
    if [ -d /bin/csh ]; then
    echo "/bin/csh is a dir"
    else
    echo "/bin/csh is NOT a dir"
    fi
    exit 0

    2.3.3 控制语句

    2.3.4 命令列表

    2.3.5 函数

    2.3.6 shell内置命令

    2.3.7 执行结果的获取

    2.3.8 here文档

    , , ,

    一、进程和线程管理

           进程是Unix操作系统最基本的抽象之一(另一个是文件)。进程是一个处于执行期间的程序,包含存放全局变量的数据段(data section)、存放程序指令的可执行代码段(code segment)、打开的文件和挂起的信号等,当然还包含地址空间及一个或多个执行线程(threads of execution)。

           执行线程常简称为线程,它是进程中活动的对象。每个线程都有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而非进程。传统的Unix中一个进程就一个线程,现在的系统大多支持多线程的应用程序。对于Linux来说,线程和进程不做特别区分。

           进程存在两种虚拟机制:虚拟处理器和虚拟内存。虚拟器技术给进程一种假象,让这些进程觉得自己在独享处理器,同理,虚拟内存技术使得进程在获取和使用内存时,觉得自己拥有整个操作系统的内存资源。同一个线程之间可以共享虚拟内存,但都拥有各自的虚拟处理器。

           进程和程序是有区别的,进程是处于执行期的程序以及它所包含的资源的总称,实际上完全可能存在两个或者多个不同的进程实际上执行的是同一个的程序。多个进程之间往往可以共享诸如打开的文件和地址空间之类的资源。进程在它被创建的时刻开始存活,在执行完退出。

           Linux系统中,进程的创建通常通过调用fork()系统调用实现,它通过复制一个现有的进程来创建一个全新的进程。调用fork()的进程被称为父进程,新产生的进程被称为子进程。调用结束后,父进程从原来位置恢复执行,子进程开始执行。通常创建新的进程的目的都是为了执行新的不同的程序,子进程创建完之后,接着调用exec()族函数创建新的地址空间并载入新的程序执行。

           最终程序将通过调用exit()系统调用退出执行,这个函数会终结程序并将其占用的资源释放。父进程通过wait4()系统调用查询子进程是否终结,使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置成为僵死状态,直到其父进程调用wait()或waitpid()为止。

           进程的另一个名字是任务(task)。Linux内核通常把进程称为任务,一般的,内核中运行的程序常被称为任务,用户空间运行的程序叫做进程。

    二、任务队列和进程描述符

           内核把进程存放在任务队列中(task list),这是一个双向循环链表(某些系统可能把它成为任务数组)。队列中的每一项都为task_struct(被称为进程描述符(process description)的结构),该结构定义在include/linux/sched.h中,进程描述符中包含了一个具体进程的所有信息。

           task_struct在32维机器中的大小为1.7KB。进程描述符中包含的数据完整的描述了一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态以及其他信息。

    image

     

           Linux通过slab分配器分配task_struct结构,这样能达到对象的复用和缓存着色的目的,通过预分配和复用task_struct,避免了动态分配和释放带来的资源消耗,实现Unix类系统进程能够快速创建。在2.6以前的内核中,各个进程的task_struct存放在他们内核栈的尾部,这样让类似于X86这类寄存器较少的硬件体系结构只需要通过栈顶指针就能计算出它的位置,避免需要额外的寄存器来专门存储指向task_struct的指针。现在的内核用slab分配器动态生成task_struct,所以只需要在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info。

    image

        X86上,struct thread_info在文件<asm/thread_info.h>中的定义:

    struct thread_info {
        struct task_struct *task;
        struct exec_domain *exec_domain;
        unsigned long flags;
        __u32 cpu;
        __s32 preempt_count;
        mm_segment_t addr_limit;
        u8 supervisor_stack[0];
    };

        每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。

    1、进程描述符内容和如何获取进程描述符

           内核通过一个唯一的进程标识值PID(process identification value)来标识每个进程,PID是一个pid_t隐含类型的数,实际上是一个int值。为了保持Unix和Linux的兼容,PID的最大值默认为32767(short int的最大值)。内核把每个进程的PID存放在各自的进程描述符中。这个值标记了系统允许同时存在的进程的最大数目。对于大型服务器,如有必要,可以通过系统管理员修改/proc/sys/kernel/pid_max来提高上限。

           内核访问任务时通常需要获取指向其task_struct的指针。内核大部分处理进程的代码都是直接通过task_struct实现的,因此,通过current宏查找到当前正在执行的进程描述符的效率就显得尤为重要。而且该宏的具体实现依硬件体系结构的不同而不同,有必要专门针对特定的硬件体系结构做处理。有的硬件体系结构拿出了一个特定的寄存器用于存放指向当前进程的task_struct的指针用于加快访问速度。但X86系统寄存器不丰富,只能在内核栈的尾端创建thread_info结构,通过计算偏移地址来间接地得到task_struct结构的地址。

           X86系统中,current把栈指针的后13个有效位屏蔽了,用来计算出thread_info的偏移。该操作通过current_thread_info()函数完成。汇编代码如下:

    movl $-8192, %eax
    andl %esp, %eax

           最后,current再从thread_info的task域中提取并返回task_struct的地址:

    current_thread_info()->task;

           在PowerPC上,我们可以发现当前task_struct的地址保存在一个寄存器中,current宏只需要把r2寄存器中的值返回就行了。访问进程描述符是一个重要且频繁的操作,所以PPC的内核开发者觉得完全有必要为此使用一个特定的寄存器。

    2、进程的状态和转换

           进程描述符中的state域描述了进程的当前状态(如下图)。系统中的每个进程都必然处于五种进程状态中的一种。该域的值也必为下列五种状态标志之一:

    • TASK_RUNNING(运行)——进程是可执行的。此时进程或者正在执行或者处在运行队列中等待执行。
    • TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(即被阻塞),等待某些条件达成后,内核会把进程状态设置为运行。处于此状态的进程也会因为接受到信号而被提前唤醒投入运行。
    • TASK_UNINTERRUPTIBLE(不可中断)——出了不会因为接收到信号而被唤醒从而投入运行,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会出现时出现。由于处于此状体啊的任务对信号不做响应,所以较可打断状态,使用的稍少。
    • TASK_ZOMBIE(僵死)——该进程已经调用exit()退出执行,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符依然被保留。直到父进程调用wait4()后才被释放。
    • TASK_STOP PED(停止)——进程停止执行,进程没有投入运行也不能投入运行。通常在接收到SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号后进程会处于这种状态。此外,调试期间接收到任何信号都会使进程处于这种状态。

    image

           内核经常需要调整某个进程的装填,此时可以使用set_task_state(task, state)函数,该函数将指定的进程设置为指定的状态,必要时,它会设置内存屏障来强制其他处理器作重新排序,也就是说再SMP系统中有此必要。否则它等价于:

             task->state = state;

    set_current_state(state)和set_task_state(current, state)含义等同。

    3、进程上下文

           可执行程序代码是进程的重要组成部分之一,这些代码从可执行文件中被载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调用系统调用或者触发某个异常时,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中,current宏有效。除非此间隙有高优先级的进程需要执行并由调度器做出了相应的调整,否则在内核退出的时候,程序恢复在用户空间继续向下执行。

           系统调用和异常处理是实现内核调用的明确定义的接口,进程只有通过这些接口才能陷入到内核执行——对内核的所有访问都必须通过这些接口。

           Linux进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本(initscripts)并执行其他的相关程序,最终完成系统的启动过程。

           系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有一个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向父进程task_struct的parent指针,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面的代码获得其父进程的进程描述符:

    struct task_struct *task = current->parent;

           同样,也可以按一下方式依次访问子进程:

    struct task_struct *task;
    struct list_head *list;
     
    list_for_each(list, &current->children) {
        task = list_entry(list, struct task_struct,sibling);
        /* task现在指向当前的某个子进程 */
    }

           init进程的进程描述符是作为init——task静态分配的。下面的代码可以很好到演示所有进程之间的关系:

    struct task_struct *task;
     
    for (task = current; task != &init_task; task = task->parent)
        NULL;
    /* task现在指向init */

           实际上,我们可以通过这种继承体系从系统的任何一个子进程出发找到任意的其他进程。大多数情况下,只需要简单的重复循环就可以遍历系统中的所有进程。任务队列是双向循环链表的特性决定了这个实现非常简单。对于给定的进程,获取链表中的下一个进程的代码如下:

               list_entry(task->tasks.next, struct task_struct, tasks)

           获取前一个进程的方法相似:

               list_entry(task->tasks.prev, struct task_struct, tasks)

           这两个例程通过next_task(task)宏和pre_task(task)宏实现。实际上,for_each_process(task)宏提供了一次访问整个任务队列的能力。每次方位,任务指针都指向链表中的下一个元素:

    struct task_struct *task;
     
    for_each_process (task)  {
        /* 打印出每个任务的名称和PID */
        printk("%s[%d]\n", task->comm, task->pid);
    }

           需要注意的是,在一个拥有大量进程的系统中通过重复遍历来访问所有进程是非常耗时的.因此呢,没有足够理由(或者别无他法)千万别这么做。

    , , ,