作者: falcon 发表日期: 2006-04-14 08:21
复制链接
在开始网络学习之前,有必要掌握一些常用的网络工具的使用方法
1,ifconfig
命令ifconfig是Linux系统配置网络设备接口的工具,也可以用它来 查看已经配置好的网络设备接口信息。
如:
#ifconfig eth0
将显示第一块以太网卡的硬件地址以及这个网络接口的ip地址等信息。
2,netstat
命令netstat显示网络连接、路由表、和接口统计等网络信息。它有很多命令选项。
无选项时,显示完了国连接状态,列出打开的套接字。
-a 选项显示所有套接字的状态
-r 选项显示路由表的内容,一般同时指定-n选项,这样可以得到数字格式的地址,也显示默认路由的ip地址。
-i 显示所有网络接口或者指定接口的信息
如:
#netstat -ni
3,ping
命令ping可以检查远程机器的网络可达到情况,命令ping通过向远程机器发送icmp消息,可以获知能否达到远程机器。
如:
#ping 219.246.177.174
4,telnet
命令telnet是标准的internet程序,一般用它来执行远程登录。但是这个程序也可以作为一个通用客户机使用,用来测试服务器程序。
比如。我们可以用telnet来连接xxxy.lzu.edu.cn的21端口
#telnet xxxy.lzu.edu.cn 21
5,tcpdump
命令tcpdump是调试网络程序时的一个 极好的工具,rethat6.0提供了这个程序,但是其他版本可能没有
需要到ftp://ftp.ee.lbl.gov/上获得
该程序直接读取数据链路(data link)层的数据包(packet), 输出指定条件的数据包的信息。
如
#tcp dump 'tcp and port 80 adn tcp[13:1]&2!=0'
6,其他常用工具
如ftp,lftp,ssh,sniffit(网络端口探测器),nmap(端口扫描工具),route(路由配置命令)等
作者: falcon 发表日期: 2006-04-03 18:32
复制链接
这里只是我自己的个人学习笔记哦,要是想系统的学shell,应该找一些比较系统的资料
这里有一篇好的:http://www.chinaunix.net/jh/24/628472.html
shell概述、Shell变量、位置参数、特殊符号、别名、各种控制语句、函数等Shell编程知识
执行Shell脚本的方式
1)输入重定向到bash: bash <exl
可以读入exl中的程序,并执行
2)以脚本名作为参数
其一般形式是:
$ bash 脚本名 [参数]
例如:
$ bash ex2 /usr/meng /usr/zhang
其执行过程与上一种方式一样,但这种方式的好处是能在脚本名后面带有参数,从而将参数值传递给程序中的命令,使一个Shell脚本可以处理多种情况,就如同函数调用时可根据具体问题给定相应的实参。
如果以目前Shell(以·表示)执行一个Shell脚本,则可以使用如下简便形式:
$ · 脚本名 [参数]
以Shell脚本作为Shell的命令行参数,这种方式可用来进行程序调试。
3)将Shell脚本的权限设置为可执行,然后在提示符下直接执行它。
具体办法:chmod 777 脚本名
Shell的执行原理
Shell
接收用户输入的命令(脚本名),并进行分析。如果文件被标记为可执行的,但不是被编译过的程序,Shell就认为它是一个Shell脚本。Shell将读
取其中的内容,并加以解释执行。所以,从用户的观点看,执行Shell脚本的方式与执行一般的可执行文件的方式相似。
因此,用户开发的Shell脚本可以驻留在命令搜索路径的目录之下(通常是“/bin”、 “/usr/bin”等),像普通命令一样使用。这样,也就开发出自己的新命令。如果打算反复使用编好的Shell脚本,那么采用这种方式就比较方便。
变量赋值
可以将一个命令的执行结果赋值给变量。有两种形式的命令替换:一种是使用倒引号引用命令,其一般形式是: `命令表`。
例如:将当前工作目录的全路径名存放到变量dir中,输入以下命令行:
$ dir=`pwd`
另一种形式是:$(命令表)。上面的命令行也可以改写为:
$ dir=$(pwd)
数组
bash只提供一维数组,并且没有限定数组的大小。类似与C语言,数组元素的下标由0开始编号。获取数组中的元素要利用下标。下标可以是整数或算术表达式,其值应大于或等于0。用户可以使用赋值语句对数组变量赋值。对数组元素赋值的一般形式是:数组名[下标]=值例如:
$ city[0]=Beijing
$ city[1]=Shanghai
$ city[2]=Tianjin
$
也可以用declare命令显式声明一个数组,一般形式是:
declare -a 数组名
读取数组元素值的一般格式是:
${数组名[下标]}
例如:
$ echo ${city[0]}
Beijing
一个数组的各个元素可以利用上述方式一个元素一个元素地赋值,也可以组合赋值。定义一个数组并为其赋初值的一般形式是:
数组名=(值1 值2 ... 值n)
其中,各个值之间以空格分开。
例如:
$ A=(this is an example of shell script)
$ echo ${A[0]} ${A[2]} ${A[3]} ${A[6]}
this an example script
$ echo ${A[8]}
$
由于值表中初值共有7个,所以A的元素个数也是7。A[8]超出了已赋值的数组A的范围,就认为它是一个新元素,由于预先没有赋值,所以它的值是空串。
若没有给出数组元素的下标,则数组名表示下标为0的数组元素,如city就等价于city[0]。
使用*或@做下标,则会以数组中所有元素取代
或[@]。
2006.8.19
最近在写一个拆分字符串的代码的时候用到了数组,我把实例写一下
问题大概是这个样子:我需要把一个存放有ip段的字符分开,分成开始地址和结束地址。
下面是解决办法
Code:
[Ctrl+A Select All]
参数传递
假如我要编写一个shell来求两个数的和,可以怎么实现呢?
为了介绍参数传递的使用
我们用vi add编写一个这样的脚本:
let sum=$1+$2
echo $sum
保存后,我们执行一下:
#./add 5 10
15
可以看出5和10分别传给了$1和$2
其实这是shell自己某人的参数顺序,其实我们可以自己先定义好变量,然后传递进去
例如:
我们先修改上面的add文件:
let sum=$X+$Y
echo $sum
现在我们,这样执行
#X=5 Y=10 ./add
15
我们同样可以得到正确结果哦
设置环境变量
将shell程序的变量输出到环境变量export
#opid=True
#export=opid
这样子就可以拉
不过还可以直接到/etc/profile里头去添加哦
具体操作类似
在里头的export命令之前先增加
opid=True
然后在export命令后面增加opid项
键盘读起变量值
我们可以通过read来读取变量值哦
下面我们来等待用户输入一个值并且显示出来
Quote: |
falcon@falcon:~$ read -p "请输入一个值 : " input ; echo "你输入了一个值为 :" $input 请输入一个值 : 21500 你输入了一个值为 : 21500
|
设置变量的只读属性
有些重要的shell变量,赋值后不应该修改,那么我们可设置他为readonly
具体操作:
#oracle_home=/usr/oracle7/bin
#readonly oracle_home
条件测试命令test
语法:test 表达式
如果表达式为真,则返回真,否则,返回假
1)数值比较
先给出数值比较时常见的比较符:
-eg =
-ne !=
-gt >
-ge >=
-lt <
-le <=
#test var1 -gt var2
2)文件属性
文件的可读、可写、可执行,是否为普通文件,是否为目录分别对应
-r -w -x -f -d
#test -r filename
3)字符传属性以及比较
串的长度为零:-z 非零:-n
如:
#test -z s1
如果串s1长度为零,返回真
串比较:相等s1=s2 不相等 s1!=s2
我们还有一种比较串的方法(可以按字典序来比较哦):
Quote: |
falcon@falcon:~$ if [[ 'abcde' <'abcdf' ]]; then echo "yeah,果然是诶"; fi yeah,果然是诶
|
整数算术或关系运算expr
可用该命令进行的运算有
算术运算:+ - * / %
逻辑运算:= ! < <= > >=
如:
#i=5;expr $i+5
另外,bc是linux下的一个计算器,可以进行一些算术计算
控制执行流程命令
(1)条件分支命令if ,case
if命令举例:
下面的shell作用:判断输入的第一个参数的值是一个普通文件名,那么分页打印该文件;否则,判断它是否为目录名,若是则进入该目录并打印该目录下的所有文件,如果也不是目录名,那么提示相关信息。
if test -f $1
then
pr $1>/dev/lp0
elif
test-d $1
then
(cd $1;pr *>/dev/lp0)
else
echo $1 is neither a file nor a directory
fi
case命令举例:
case命令是一个基于模式匹配的多路分之命令
下面的shell将根据用户键盘输入情况决定下一步将执行那一组命令。
while [ $reply!="y" ] && [ $reply!="Y" ] #下面将学习的循环语句
do
echo "\nAre you want to continue?(Y/N)\c"
read reply #读取键盘
case $replay in
(y|Y) break;; #退出循环
(n|N) echo "\n\nTerminating\n"
exit 0;;
*) echo "\n\nPlease answer y or n"
continue; #直接返回内层循环开始出继续
esac
done
(2)循环语句
条件循环语句while,until
语法:
while/until 命令表1
do
命令表2
done
区别是,前者在执行完命令表1后,如果出口状态为零,那么执行do后面的命令表2,然后回到起始处,而后者执行命令表1后,如果出口状态非零,才执行类似操作。
[例子同上]
有限循环命令for
语法:
for 变量名 in 字符串表
do
命令表
done
举例:
FILE="test1.c myfile1.f pccn.h"
for i in $FILE
do
cd ./tmp
cp $i $i.old
echo "$i copied"
done
函数
先在我们来看看shell里头的函数怎么用
先看个例子:我们些一个函数,然后调用它显示"Hello,World!"
#函数定义
function show
{
echo $1$2;
}
H="Hello,"
W="World!"
show $H $W
保存为show
修改执行权限chmod 770 show
执行:
$./show
Hello,World!
呵呵,看出什么蹊跷了吗?
show $H $W
我们可以通过直接在函数明后面跟实参哦
实参顺序对应“虚参”的$1,$2,$3..........
注意一个地方哦,假如我要传进去一个参数,这个参数中间带空格,怎么办呢?
你先试试看
我们来显示"Hello World"(两个单词之间有个空格哦)
function show
{
echo $1
}
HW="Hello World"
show "$HW"
看看,是不是ok拉,如果你 直接show $HW
肯定是不行的,因为$1只接受到了Hello,所以结果之显示Hello
原因是字符串变量必须用"包含起来
感兴趣的你继续学习哦!
还有好多强大的东西值得你去学习呢,呵呵,比如cut, expr, sed, awk等等。
linux下c语言学习笔记——操作mysql
By falcon 2006年3月30日晚上完成
版权声明:可以自由转载,但是必须保留原作者名和本站地址,谢谢
[由于最近比较忙,没有时间来得及整理,下面所有代码的具体编译方法和原理请参考11楼以后的回复,谢谢]
今天上数据库的时候刚上到嵌入式sql,感觉非常有意思,上课的时候就想反正做搜索引擎要用到c连接mysql数据库的,到底是怎么实现?想入非非,呵呵.
下来的时候赶紧找资料,刚才搜索了几下,终于找到一些:
1,[比较详细]在 C 里嵌入 SQL:http://www.pgsqldb.org/pgsqldoc-7.4/ecpg.html
2,[在MySQL数据库中使用C执行SQL语句]:http://www.dvbbs.net/tech/data/2006031818989.asp
3,MySQL客户工具和API:http://www.yesky.com/imagesnew/software/mysql/manual_Clients.html
4,基于mysql的高性能数据库应用开发:http://cache.baidu.com/c?word=mysql%3B%5F%3Breal%3B%5F%3Bconnect%2C%B2%CE%CA%FD&url=http%3A//www%2Edaima%2Ecom%2Ecn/Info/76/Info27780/&b=0&a=2&user=baidu
大家一起来开始练习罗
注:下面的所有例子在mandriva linux下测试通过
1,使用c语言操作mysql之前,先在mysql里头创建一个数据库,一个表,在表里头添加数据如下:
创建数据库,库名为cusemysql:
mysql>create database cusemysql;
创建表,表名为:
mysql>use cusemysql;
mysql>create table children(childno int not null unique,fname varchar(20),age int);
添加一点数据哦:
mysql>insert into children values(5,"花儿",10);
对拉,为了方便起见,把表的大致样子给大家看看
childno fname age
1 小星 9
2 大量 15
2 ,下面进行具体的操作
插入:insert
好的,我们现编辑一段c代码,取名为insert.c
Code:
[Ctrl+A Select All]
代码写完了,要编译哦
#gcc -o insert insert.c -L /usr/local/mysql/lib/mysql/*.a -lz
ok,现在我们执行看看
#./insert
Connection Success
Inserted 1 rows
year,果然可以,呵呵
不信到mysql下看看表children中是否多了刚才插入的那一行数据
注:也许你会问上面gcc的命令参数是什么意思阿,其实,我也不太清楚,呵呵
大概是要把mysql下的某个特定库包含进来,可是我不知道具体是个什么库,所以用*.a全部包含进来拉
其实只要包含mysqlclient.a就可以,你试试看
更新:update
我们只要把上面的代码中的
res = mysql_query(&my_connection, "insert into children values(10,'Ann',5)");
换成
res = mysql_query(&my_connection, "update children set age=20 where childno<5 ");
即可
上面语句实现的功能是,把编号小于5的所有孩子的年龄全部改成20岁
检索:select
看代码之前,最好是先看蓝色字体的部分[介绍了代码中用到的一些函数的作用]
Code:
[Ctrl+A Select All]
上面语句实现的功能是:检索出年龄小于20岁的小孩的信息,不过没有对信息进行任何处理哦
下次我们对数据进行一定的处理
这里介绍上面用到的几个函数:
可以从SELECT语句(或其他返回数据的语句)中检索完所有数据,在单一调用中,使用mysql_store_result:
MYSQL_RES *mysql_store_result(MYSQL *connection);
必须在mysql_query检索数据后才能调用这个函数,以在结果集中存储该数据。这个函数从服务器中检索所有数据并立即将它存储在客户机中。它返回一个指向以前我们从未遇到过的结构(结果集结构)的指针。如果语句失败,则返回NULL。
使用等价的PostgreSQL时,应该知道返回NULL意味着已经发生了错误,并且这与未检索到数据的情况不同。即使,返回值不是NULL,也不意味着当前有数据要处理。
如果未返回NULL,则可以调用mysql_num_rows并且检索实际返回的行数,它当然可能是0。
my_ulonglong mysql_num_rows(MYSQL_RES *result);
它从mysql_store_result取得返回的结果结构,并且在该结果集中返回行数,行数可能为0。如果mysql_store_result成功,则mysql_num_rows也总是成功的。
这
种mysql_store_result和mysql_num_rows的组合是检索数据的一种简便并且直接的方法。一旦
mysql_store_result成功返回,则所有查询数据都已经存储在客户机上并且我们知道可以从结果结构中检索它,而不用担心会发生数据库或网络
错误,因为对于程序所有数据都是本地的。还可以立即发现返回的行数,它可以使编码更简便。如前所述,它将所有结果立即地发送回客户机。对于大结果集,它可
能耗费大量的服务器、网络和客户机资源。由于这些原因,使用更大的数据集时,最好仅检索需要的数据。不久,我们将讨论如何使用
mysql_use_result函数来完成该操作。
一旦检索了数据,则可以使用mysql_fetch_row来检索它,并且使用mysql_data_seek、mysql_row_seek、mysql_row_tell操作结果集。在开始检索数据阶段之前,让我们先讨论一下这些函数。
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);
这个函数采用从存储结果中获取的结果结构,并且从中检索单一行,在行结构中返回分配给您的数据。当没有更多数据或者发生错误时,返回NULL。稍后,我们将回来处理这一行中的数据。
void mysql_data_seek(MYSQL_RES *result, my_ulonglong offset);
这个函数允许您进入结果集,设置将由下一个获取操作返回的行。offset是行号,它必须在从0到结果集中的行数减 1 的范围内。传递0将导致在下一次调用mysql_fetch_row时返回第一行。
MYSQL_ROW_OFFEST mysql_row_tell(MYSQL_RES *result);
这个函数返回一个偏移值,它表示结果集中的当前位置。它不是行号,不能将它用于mysql_data_seek。但是,可将它用于:
MYSQL_ROW_OFFSET mysql_row_seek(MYSQL_RES *result, MYSQL_ROW_OFFSET offset);
它移动结果集中的当前位置,并返回以前的位置。
有时,这一对函数对于在结果集中的已知点之间跳转很有用。请注意,不要将row tell和row seek使用的偏移值与data_seek使用的行号混淆。这些是不可交换的,结果将是您所希望看到的。
void mysql_free_result(MYSQL_RES *result);
完成结果集时, 必须总是调用这个函数,以允许MySQL库整理分配给它的对象。
|
|
检索并处理[比较全面哦,呵呵]:select
下面是详细的代码:
Code:
[Ctrl+A Select All]
到此一些基本的操作都已经实现拉,呵呵,怎一个爽自了得
大家快点来阿
by falcon<zhangjinw@gmail.com>
2007-10-30
前言:
从这个帖子开始,打算结合自己平时的积累和进一步的实践,通过一些范例来介绍shell编程。因为范例往往能够给人以学有所用的感觉,而且给人以动手实践
的机会,从而激发人的学习热情。考虑到易读性,这里的范例将非常简单,但是实用,希望它们能够成为你解决常规问题的参照物或者是“茶余饭后”的小点心,当
然这些“点心”肯定还有值得探讨、优化的地方。更复杂有趣的例子请参考《高级Bash脚本编程指南》(一本深入学习shell脚本艺术的书籍)。
该计划是上一篇帖子《在Linux下更高效的工作》的续。
写这些东西的
Quote: |
目的:1)享受用shell解决问题的乐趣 2)和朋友们一起交流和探讨 初步计划:先零散地写些东西,之后再不断补充,最后整理成册。 适合读者:已经熟悉linux基本知识,比如文件系统结构、常用命令行工具、shell编程基础等。 建议:大家在看这些范例的时候,参考网络中流传的《shell基础十二篇》和《shell十三问》,见ChinaUnix Shell讨论区。 环境:如果没有特别说明,以后"shell编程范例"系列使用的shell将特指bash,版本在3.1.17以上。 说明:本序列的组织不是依据Shell语法,而是面向某些潜在的操作对象和操作本身,它们反应了现实应用。当然,在这个过程当中肯定会涉及到Shell的语法。另外,欢迎您对帖子里头存在的问题进行批评指正,也欢迎您对一些范例进行改进。
|
shell编程范例之数值运算
这一篇打算讨论一下shell编程中的基本数值运算,这类运算包括数值(包括整数和浮点数)间的加减乘除幂求模等,产生指定范围的随机数,产生指定范围的数列等。
貌似shell本身(shell本身是一个解释程序,你可以在命令行打印SHELL变量找到当前的shell程序)只可以完成整数运算,一些复杂的运算可
以通过外部命令实现,比如expr,bc,awk等。至于随机数,shell可以通过RANDOM环境变量产生一个从0到32767的随机数,一些外部工
具,比如awk可以通过rand()函数产生随机数。而seq命令可以用来产生一个数列。
下面分别进行介绍:
1、整数运算
1.1 概要示例:对某个数加一
Quote: |
$ i=0; $ ((i++)) $ echo $i 1 $ let i++ $ echo $i 2 $ expr $i + 1 3 $ echo $i 2 $ echo $i 1 | awk '{printf $1+$2}' 3
|
说明:expr之后的$i,+,1之间有空格分开;awk后面的$1和$2分别指$i和1,即从左往右的第1个和第二个数。
用shell的内置命令查看各个命令的类型如下:
Quote: |
$ type type type is a shell builtin $ type let let is a shell builtin $ type expr expr is hashed (/usr/bin/expr) $ type bc bc is hashed (/usr/bin/bc) $ type awk awk is /usr/bin/awk
|
从上面的演示可以看出:let是shell内置命令,其他几个是外部命令,都在/usr/bin目录下。而expr和bc因为我刚用过,已经加载在内存的hash表中。这个结果将有助于我们理解下面范例的结果。
补充:如果查看不同命令的帮助
对于let和type等shell内置命令,可以通过shell的一个内置命令help来查看相关帮助,而一些外部命令可以通过shell的一个外部命令man来查看帮助,用法诸如help let,man expr等。
1.2 范例演示:从1加到某个数值。
代码:
Code:
[Ctrl+A Select All]
说明:这里通过while [ 条件表达式 ]; do .... done循环来实现。-lt是小于号(<),具体见test命令的用法:man test。
如何执行该脚本?
第一种办法直接把脚本文件当成子shell(bash)的一个参数传入。
Quote: |
$ bash calc.sh $ type bash bash is hashed (/bin/bash)
|
第二种办法是通过bash的内置命令.或source执行。
Quote: |
$ . ./calc.sh 或 $ source ./calc.sh $ type . . is a shell builtin $ type source source is a shell builtin
|
第三种办法是修改文件为可执行,直接在当前shell下执行。
Quote: |
$ chmod ./calc.sh $ ./calc.sh
|
下面,逐一演示用其他方法计算变量加一,即把((i++))行替换成下面的某一个:
let i++;
i=$(expr $i + 1)
i=$(echo $i+1|bc)
i=$(echo "$i 1" | awk '{printf $1+$2;}')
比较计算时间如下:
Quote: |
$ time calc.sh 10000
real 0m1.319s user 0m1.056s sys 0m0.036s $ time calc_let.sh 10000
real 0m1.426s user 0m1.176s sys 0m0.032s $ time calc_expr.sh 1000
real 0m27.425s user 0m5.060s sys 0m14.177s $ time calc_bc.sh 1000
real 0m56.576s user 0m9.353s sys 0m24.618s $ time ./calc_awk.sh 100
real 0m11.672s user 0m2.604s sys 0m2.660s
|
说明:time命令可以用来统计命令执行时间,这部分时间包括总的运行时间,用户空间执行时间,内核空间执行时间,它通过ptrace系统调用实现。
总
结:通过上面的比较,我们发现(())的运算效率最高。而let作为shell内置命令,效率也很高,但是expr,bc,awk的计算效率就比较低。所
以,在shell本身能够完成相关工作的情况下,建议优先使用shell本身提供的功能。但是shell本身好像无法完成浮点运算,所以就需要外部命令的
帮助。
补充:let,expr,bc都可以用来求模,运算符都是%,而let和bc可以用来求幂,运算符不一样,前者是**,后者是^。例如:
Quote: |
//求模 $ expr 5 % 2 1 $ let i=5%2 $ echo $i 1 $ echo 5 % 2 | bc 1 $ ((i=5%2)) $ echo $i 1 //求幂 $ let i=5**2 $ echo $i 25 $ ((i=5**2)) $ echo $i 25 $ echo "5^2" | bc 25
|
2. 浮点运算
let和expr都无法进行浮点运算,但是bc和awk可以。
2.1 概要示例:求1除以13,保留3位有效数字。
Quote: |
$ echo "scale=3; 1/13" | bc .076 $ echo "1 13" | awk '{printf("%0.3f\n",$1/$2)}' 0.077
|
说明:bc在进行浮点运算的时候需要指定小数点位数,否则默认为0,即进行浮点运算的时候,默认求出的结果只保留整数。而awk在控制小数位数的时候非常灵活,仅仅通过printf的格式控制就可以实现。
补充:在用bc进行运算的时候,如果不指定scale,而在bc后加上-l选项,也可以进行浮点运算,只不过这时的浮点运算的小数点默认是20位。例如:
Quote: |
$ echo 1/13100 | bc -l .00007633587786259541
|
2.2 范例演示:假如有这样一组数据,存放有某个村庄所有家庭的人数和月总收入,要求找出人均月收入最高的家庭。
在这里我随机产生了一组数据,文件名为income。
Quote: |
1 3 4490 2 5 3896 3 4 3112 4 4 4716 5 4 4578 6 6 5399 7 3 5089 8 6 3029 9 4 6195 10 5 5145
|
说明:上面的三列数据分别是家庭编号、家庭人数、家庭月总收入。
分析:为了求出月均收入最高的家庭,我们需要对后面两列数进行除法运算,即求出每个家庭的月均收入,然后按照月均收入排序,找出收入最高的家庭。
实现:
Code:
[Ctrl+A Select All]
说明:
[ $# -lt 1 ]: 要求用户至少收入一个参数,$#是shell中传入参数的个数
[ ! -f $1 ] : 要求用户传入的参数是一个文件,-f的用法见test命令,man test
income=$1:把用户传入的参数赋值给income变量,并在后面作为awk的参数,即需要处理的文件
awk....:用文件中的第三列除以第二列,求出月均收入,考虑到精确性,保留了两位有效数字。
sort -k 2 -n -r: 这里对结果的awk结果的第二列(-k 2),即月均收入进行排序,按照数字排序(-n),并按照递减的顺序排序(-r)。
演示:
Quote: |
$ ./gettopfamily.sh income 7 1696.33 9 1548.75 1 1496.67 4 1179.00 5 1144.50 10 1029.00 6 899.83 2 779.20 3 778.00 8 504.83
|
补充:之前的income数据是随机产生的。在做一些实验时,往往需要随机产生一些数据,在下一小节,我们将详细介绍它。这里是产生income数据的脚本:
Code:
[Ctrl+A Select All]
说明:上述脚本中还用到seq命令产生从1到10的一列数,这个命令的详细用法在该篇最后一节也会进一步介绍。
3. 随机数
环境变量RANDOM产生0到32767的随机数,而awk的rand函数可以产生0到1之间的随机数。
3.1 概要示例:打印一个随机数
Quote: |
$ echo $RANDOM 81 $ echo "" | awk '{srand(); printf("%f", rand());}' 0.237788
|
说明:srand在无参数时,采用当前时间作为rand随机数产生器的一个seed。
3.2 范例演示:随机产生一个从0到255之间的数字
3.2.1 可以通过RANDOM变量的缩放和awk中rand的放大来实现。
Quote: |
$ expr $RANDOM / 128 $ echo "" | awk '{srand(); printf("%d\n", rand()*255);}'
|
思考:如果要随机产生某个IP段的IP地址,该如何做呢?
3.2.2 友善地获取一个可用的IP地址
这个脚本我在兰大开源社区的讨论区发过,具体的分析过程见《貌似IP地址老被抢,写个脚本自动换个可用的(非破坏性)》
代码:
Code:
[Ctrl+A Select All]
说明:如果网关地址不是1,那么用ifconfig配置地址时不能配置为网关地址,否则你的IP地址将和网关一样,导致整个网络出现问题。
4. 产生一序列数
其实我们通过一个循环就可以产生一序列数,但是有相关的小工具为什么不用呢!seq就是这么一个小工具,它可以产生一序列数,你可以指定数的递增间隔,也可以指定相邻两个数之间的分割符。
4.1 概要示例:演示seq,打印一序列数
Quote: |
$ seq 5 1 2 3 4 5 $ seq 1 5 1 2 3 4 5 $ seq 1 2 5 1 3 5 $ seq -s: 1 2 5 1:3:5 $ seq 1 2 14 1 3 5 7 9 11 13 $ seq -w 1 2 14 01 03 05 07 09 11 13 $ $ seq -s: -w 1 2 14 01:03:05:07:09:11:13
|
补充:在bash版本3中,在for循环的in后面,可以直接通过{1..5}更简洁地产生自1到5的数字(注意,1和5之间只有两个点),例如:
Quote: |
$ for i in {1..5}; do echo -n "$i "; done 1 2 3 4 5
|
4.2 统计指定字符串(这里以单词为例)的个数
这个灵感来自《高级Bash脚本编程指南》“混杂命令”seq的实例之“字母统计”和CU上一篇统计字母和数字个数的帖子。
4.2.1 首先,我们统计某个文件中所有单词的个数。这里的单词我定义为:由字母组成的单个或者多个字符序列。所以,可以这样实现。
说明:为了方便演示,这里采用我的上一篇转载的日志happiness quotations里头的内容,请把内容复制下来保存为text文件。
Quote: |
//统计每个单词出现的次数 $ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c //统计出出现频率最高的前10个单词 $ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | uniq -c | sort -n -k 1 -r | head -10 45 is 40 to 32 of 31 a 28 and 27 the 25 happiness 19 happy 18 it 18 in
|
说明:
cat text: 显示text文件里的内容
sed -e "s/[^a-zA-Z]/\n/g": 把非字母的字符全部替换成空格,这样整个文本只剩下字母字符
grep -v ^$:去掉空行
sort: 排序
uniq -c:统计相同行的个数,即每个单词的个数
sort -n -k 1 -r:按照第一列(-k 1)的数字(-n)逆序(-r)排序
head -10:取出前十行
4.2.2 接着我们统计指定单词的个数,即输入需要统计的单词,并返回每个单词的个数。
可以考虑采取两种办法:
第一种:只统计那些需要统计的单词
第二种:用上面的算法把所有单词的个数都统计出来,然后再返回那些需要统计的单词给用户
不过,这两种办法都可以通过下面的结构来实现。
Code:[Ctrl+A Select All]说明:
if 条件部分:要求用户输入至少两个参数,第一个是需要统计单词的文件名,第二之后的所有参数是需要统计的单词。
FILE=$1: 获取文件名,即脚本之后的第一个字符串。
((WORDS_NUM=$#-1)):获取单词个数,即总的参数个数($#)减去那个文件名参数(1个)
for
循环部分:首先通过seq产生需要统计的单词个数序列,shift是shell内置变量(请通过help
shift获取帮助),它把用户从命令行中传入的参数依次往后移动位置,并把当前参数作为第一个参数即$1,这样通过$1就可以遍历用户所有输入的单词
(仔细一想,这里貌似有数组下标的味道)。你可以考虑把shift之后的那句替换成echo $1测试shift的用法。
演示:
Quote: |
$ chmod +x statistic_words.sh $ ./statistic_words.sh text is Action happy 45 is 1 Action 19 happy
|
采用第二种办法,我们只需要修改shift之后的那句即可。
Code:[Ctrl+A Select All]演示:
Quote: |
$ ./statistic_words.sh text is Action happy 45 is 1 Action 19 happy
|
说明:很明显,采用第一种办法效率要高很多,因为第一种办法提前找出了需要统计的单词,然后再统计,而后者则不然。实际上,如果使用grep的-E选项,我们无须引入循环,而用一条命令就可以搞定:
Quote: |
$ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | grep -E "^Action$|^is$" | uniq -c 或者 $ cat text | sed -e "s/[^a-zA-Z]/\n/g" | grep -v ^$ | sort | egrep "^Action$|^is$" | uniq
-c [/code] 所以,可见这些命令sed,grep,uniq,sort是多么有用,它们本身虽然只完成简单的功能,但是通过一定的组合,就可以实现你想要实现的功能啦。对了,统计单词还有个非常有用的命令wc -w,需要用到的时候也可以用它。
补充:在《高级Bash脚本编程指南》一书中还提到jot命令和factor命令,由于我机器上没有,所以没有测试,factor命令可以产生某个数的所有素数。如: [quote] $ factor 100 100: 2 2 5 5
|
5. 总结到这里,shell编程范例之数值计算就结束啦。该篇主要介绍了:
* shell编程中的整数运算、浮点运算、随机数的产生、数列的产生
* shell的内置命令、外部命令的区别,以及如何查看他们的类型和帮助,关于内置命令和外部命令的比较,请参考:
http://www.linuxpk.com/doc/abs/internal.html#READPIPEREF* shell脚本的几种执行办法
* 几个常用的shell外部命令:sed,awk,grep,uniq,sort等
* 范例:数字递增;求月均收入;自动获取IP地址;统计单词个数
* 其他:相关的用法,比如命令列表,条件测试等,在上述范例中都已经涉及,请认真阅读之
如果您有时间,请温习之。
6. 参考资料和推荐资料[1] 高级Bash脚本编程指南
http://www.linuxpk.com/doc/abs/[2] shell十三问
http://bbs.chinaunix.net/thread-218853-1-1.html[3] shell基础十二篇
http://bbs.chinaunix.net/thread-452942-1-1.html[4] 在linux下学习和工作
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=775&forum=6[5] 在linux下更高效的工作
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=1074&forum=6[6] SED手册
http://phi.sinica.edu.tw/aspac/reports/96/96005/[7] AWK使用手册
http://www.chinaunix.net/jh/7/16985.htmlhttp://phi.sinica.edu.tw/aspac/reports/94/94011/[8] 几个shell讨论区
兰大开源社区:
http://oss.lzu.edu.cn/modules/newbb/viewforum.php?forum=26LinuxSir.org:
http://www.linuxsir.org/bbs/forumdisplay.php?f=60ChinaUnix.net:
http://bbs.chinaunix.net/forum-24-1.html如果合适,建议直接找对应的英文原版阅读!
后记:[1] 大概花了3个多小时才写完,目前是23:33,该回宿舍睡觉啦,明天起来修改错别字和补充一些内容,朋友们晚安!
[2] 10月31号,修改部分措辞,增加一篇统计家庭月均收入的范例,添加总结和参考资料,并用附录所有代码。
[3] SHELL编程是一件非常有趣的事情,如果您想一想:上面计算家庭月均收入的例子,然后和用M$ Excel来做这个工作比较,你会发现前者是那么简单和省事,而且给您以运用自如的感觉。
描述:shell_examples_calculate
附件:
shell_examples_calculate.tar.gz
(5 K)
再附一个简单范例(问题见XBW linux&unix版)
问题:有这么两个文件,第一列是坐标点,第二列是对应的值,要求把两文件中相同坐标处的值求和,结果格式和原文件一致。
分析:这个问题如果用shell做,用awk最合适不过,当然,还用到sort进行排序预处理。
$ cat A
0.000000 -393.339844
1.000000 -403.556091
2.000000 -408.335876
3.000000 -391.387726
4.000000 -406.563660
5.000000 -413.982544
$ cat B
0.000000 -20.100649
1.000000 -9.304893
2.000000 -7.830594
3.000000 -29.411428
4.000000 -9.393303
5.000000 -23.742157
$ sort A B | awk 'BEGIN{oldpoint=-1;}{ if(oldpoint==$1){ printf("%f %f\n", $1, $2+oldvalue); } oldpoint=$1; oldvalue=$2; }'
0.000000 -413.440493
1.000000 -412.860984
2.000000 -416.166470
3.000000 -420.799154
4.000000 -415.956963
5.000000 -437.724701
关于余弦值转角度
Quote: |
//用bc -l计算,可以获得高精度 $ export cos=0.996293; echo "scale=100; a(sqrt(1-$cos^2)/$cos)*180/(a(1)*4)" |bc -l 4.934954755411383632719834036931840605159706398655243875372764917732\ 5495504159766011527078286004072131 //用awk $ echo 0.996293 | awk '{ printf("%-10s %-10s", $1, atan2(sqrt(1-$1^2),$1)*180/3.1415926535);}'
|
把一个文件中第2列的所有余弦值转换为角度
Quote: |
$ cat data 2000 1 0 2005 0.996293 4.93515 2010 0.999609 1.60303 2015 0.98501 9.9332 2020 0.999488 1.83398 2025 0.999409 1.9702 2030 0.99943 1.93381 2035 0.999267 2.19348 2040 0.99679 4.59235 $
awk '{ printf("%-10s%-10s%-10s", $1, $2, $3); system("echo \"scale=10;
a(sqrt(1-"$2"^2)/"$2")*180/3.1415926535\" | bc -l ");}' data 2000 1 0 0 2005 0.996293 4.93515 4.9349547602 2010 0.999609 1.60303 1.6022865295 2015 0.98501 9.9332 9.9330460660 2020 0.999488 1.83398 1.8335432105 2025 0.999409 1.9702 1.9699389820 2030 0.99943 1.93381 1.9346200645 2035 0.999267 2.19348 2.1938966333 2040 0.99679 4.59235 4.5920476729 $ awk '{ printf("%-10s%-10s%-10s %f\n", $1, $2, $3, atan2(sqrt(1-$2^2), $2)*180/3.1415926535)}' data 2000 1 0 0.000000 2005 0.996293 4.93515 4.934955 2010 0.999609 1.60303 1.602286 2015 0.98501 9.9332 9.933046 2020 0.999488 1.83398 1.833543 2025 0.999409 1.9702 1.969939 2030 0.99943 1.93381 1.934620 2035 0.999267 2.19348 2.193897 2040 0.99679 4.59235 4.592048
|
详细解答过程请参考:
http://bbs.lzu.edu.cn/wForum/disparticle.php?boardName=LinuxUnix&ID=28597&pos=2
感兴趣的朋友看看这些范例:
1. 《Shell 编程实例集锦》
http://www.lupaworld.com/35714/viewspace_21170.html
另外,通过这篇可以深入学习一下AWK的实际应用价值:
2. 巧用AWK处理二进制数据文件
http://www.ibm.com/developerworks/cn/linux/shell/awk/binary/
关于不同类型的数值常量(如八进制、16进制的表示等)的表示,请参考 《高级Bash脚本编程指南》http://www.linuxpk.com/doc/abs/numerical-constants.html#NUMBERS
东西总是学不完,大伙继续,这里是几个好去处,一天看上几篇,保证受益不少:
[1] linuxsir.org Shell版精华
http://www.linuxsir.org/bbs/forum60--1-desc-goodnees.html
[2] chinaunix.net Shell版综合水平测试
http://bbs.chinaunix.net/thread-476260-27-1.html
[3] linuxsir.org Shell技巧交流区
http://www.linuxsir.org/bbs/thread173263.html
[4] linuxsir.org Shell脚本欣赏区
http://www.linuxsir.org/bbs/showthread.php?threadid=29701
用bc计算器计算“Unix高级编程”第一章课后系统最后两个题目的计算过程:
Quote: |
// 1.5 若日历存放在带符号的32位整数中,那么到哪一年它将溢出? $ echo "(2^31)/(360*24*60*60)+1970-1" | bc 2038 // 1.6 若进程时间存放在带符号的32位整数中,而且每秒为100滴答,那么经过多少天后,该时间值将会溢出。 $ echo "2^31/(100*24*60*60)" | bc 248
|
以上两道题需要明白两个概念:
第一就是Unix时间存放的是从1970年1月1日到现在的秒数,第二格式进程时间存放的是进程运行到现在的滴答数。
如果有时间,这里头的一些资料还是值得您仔细阅读的:
"developerWorks 中国 | Shell、Shell 脚本编写、命令行、相关工具及技巧"
http://www.ibm.com/developerworks/cn/linux/shell/index.html
需要补充的是,日期和时间是一个很好的随机数,它是一个在永恒变化的东西,所以,你无须担心存在重复,它很适合用于生成一些临时文件。具体用法见man date,常用的有date +%s
刚从http://www.linuxpk.com/doc/abs/special-chars.html看到有趣的一点,用(())加#还可以转换进制。
例如:
Quote: |
# echo "$(( 8#11 ))" 9
|
即1*8^0 + 1*8^1 = 9
关于间接变量的引用问题:
例如:
Quote: |
$ a=b $ b=c $ echo $a b $ echo $b c $ eval echo \$$a c $ echo ${!a} c
|
${!a}提供了一种非常方便的间接变量引用办法,参考:
http://www.linuxpk.com/doc/abs/othertypesv.html
上个礼拜介绍了Shell编程范例之数值运算,对Shell下基本数值运算方法做了简单的介绍,这周将一起探讨布尔运算,即如何操作“真假值”。
在bash里有这样的常量(实际上是两个内置命令,在这里我们姑且这么认为,后面将介绍),即true和false,一个表示真,一个表示假。对它们可以
进行与、或、非运算等常规的逻辑运算,在这一节,我们除了讨论这些基本逻辑运算外,还将讨论SHELL编程中的条件测试和命令列表,并介绍它们和布尔运算
的关系。
1. 常规的布尔运算
这里主要介绍bash里头常规的逻辑运算,与、或、非。
1.1 概要示例:在shell下如何进行逻辑运算
Quote: |
// 单独测试true和false,可以看出true是真值,false为假 $ if true;then echo "YES"; else echo "NO"; fi YES $ if false;then echo "YES"; else echo "NO"; fi NO // 与运算 $ if true && true;then echo "YES"; else echo "NO"; fi YES $ if true && false;then echo "YES"; else echo "NO"; fi NO $ if false && false;then echo "YES"; else echo "NO"; fi NO $ if false && true;then echo "YES"; else echo "NO"; fi NO // 或运算 $ if true || true;then echo "YES"; else echo "NO"; fi YES $ if true || false;then echo "YES"; else echo "NO"; fi YES $ if false || true;then echo "YES"; else echo "NO"; fi YES $ if false || false;then echo "YES"; else echo "NO"; fi NO // 非运算,即取反 $ if ! false;then echo "YES"; else echo "NO"; fi YES $ if ! true;then echo "YES"; else echo "NO"; fi NO
|
可以看出true和false按照我们对逻辑运算的理解进行着,但是为了能够更好的理解shell对逻辑运算的实现,我们还得弄清楚,true和false是怎么工作的?
1.2 范例演示:bash里头的true和false是我们通常认为的1和0么?
回答是:否。
Quote: |
// true和false它们本身并非逻辑值,它们是shell内置命令,返回了“逻辑值” $ true $ echo $? 0 $ false $ echo $? 1 // 看看true和false帮助和类型 $ help true false true: true Return a successful result. false: false Return an unsuccessful result. $ type true false true is a shell builtin false is a shell builtin
|
说明:$?是一个特殊的变量,存放有上一个程序的结束状态(退出状态码)。
从
上面的操作不难联想到在C语言程序设计中为什么会强调在main函数前面加上int,并在末尾加上return
0。因为在shell里头,将把0作为程序是否成功结束的标志,这就是shell里头true和false的实质,它们用以反应某个程序是否正确结束,而
并非传统的真假值(1和0),相反的,它们返回的是0和1。不过庆幸的是,我们在做逻辑运算时,无须关心这些。
2. 条件测试
从上一节中,我们已经清楚的了解了shell下的“逻辑值”是什么:是程序结束后的返回值,如果成功返回,则为真,如果不成功返回,则为假。
而条件测试正好使用了test这么一个指令,它用来进行数值测试(各种数值属性测试)、字符串测试(各种字符串属性测试)、文件测试(各种文件属性测试),我们通过判断对应的测试是否成功,从而完成各种常规工作,在加上各种测试的逻辑组合后,将可以完成更复杂的工作。
2.1 概要示例:条件测试基本使用和各种测试的逻辑组合
Quote: |
// 数值测试 $ if test 5 -eq 5;then echo "YES"; else echo "NO"; fi YES $ if test 5 -ne 5;then echo "YES"; else echo "NO"; fi NO // 字符串测试 $ if test -n "not empty";then echo "YES"; else echo "NO"; fi YES $ if test -z "not empty";then echo "YES"; else echo "NO"; fi NO $ if test -z "";then echo "YES"; else echo "NO"; fi YES $ if test -n "";then echo "YES"; else echo "NO"; fi NO // 文件测试 $ if test -f /boot/System.map; then echo "YES"; else echo "NO"; fi YES $ if test -d /boot/System.map; then echo "YES"; else echo "NO"; fi NO // 各种测试的组合 // 如果a,b,c都等于下面对应的值,那么打印YES,这里通过-a进行"与"测试 $ a=5;b=4;c=6; $ if test $a -eq 5 -a $b -eq 4 -a $c -eq 6; then echo "YES"; else echo "NO"; fi YES // 测试某个“东西”是文件或者目录,这里通过-o进行“或”运算 $ if test -f /etc/profile -o -d /etc/profile;then echo "YES"; else echo "NO"; fi YES // 测试非运算 $ if test ! -f /etc/profile; then echo "YES"; else echo "NO"; fi NO
|
上
面仅仅演示了test命令一些非常简单的测试,你可以通过help
test获取test的更多使用方法。在这里需要注意的是,test命令内部的逻辑运算和shell的逻辑运算符有一些区别,对应的为-a和&
&,-o与||,这两者不能混淆使用。而非运算都是!,下面对它们进行比较。
2.2 范例演示:-a与&&, -o与||,!与!
Quote: |
// 要求某个文件有可执行权限并且有内容,用-a和&&分别实现 $ vim test.sh $ cat test.sh
#!/bin/bash
echo "test" $ chmod +x test.sh $ if test -s test.sh -a -x test.sh; then echo "YES"; else echo "NO"; fi YES $ if test -s test.sh && test -x test.sh; then echo "YES"; else echo "NO"; fi YES // 要求某个字符串要么为空,要么和某个字符串相等 $ str1="test" $ str2="test" $ if test -z "$str2" -o "$str2" == "$str1"; then echo "YES"; else echo "NO"; fi YES $ if test -z "$str2" || test "$str2" == "$str1"; then echo "YES"; else echo "NO"; fi YES // 测试某个数字不满足指定的所有条件 $ i=5 $ if test ! $i -lt 5 -a $i -ne 6; then echo "YES"; else echo "NO"; fi YES $ if ! test $i -lt 5 -a $i -eq 6; then echo "YES"; else echo "NO"; fi YES
|
很容易找出它们的区别,-a和-o使用在测试命令的内部,作为测试命令的参数,而&&和||是用来运算测试的返回值,!为两者通用。需要关注的是:
1)有时候我们可以不用!运算符,比如-eq和-ne刚好是相反的,用来测试两个数值是否相等;-z与-n也是对应的,用来测试某个字符串是否为空。
2)在bash里,test命令可以用[ ]运算符取代,但是需要注意,[之后与]之前需要加上额外的空格。
3)在测试字符串的时候,所有变量建议用双引号包含起来,以防止变量内容为空的时候出现仅有测试参数,没有测试内容的情况。
下面我们用实例来演示上面三个注意事项:
Quote: |
// -ne和-eq对应的,我们有时候可以免去!运算 $ i=5 $ if test $i -eq 5; then echo "YES"; else echo "NO"; fi YES $ if test $i -ne 5; then echo "YES"; else echo "NO"; fi NO $ if test ! $i -eq 5; then echo "YES"; else echo "NO"; fi NO // 用[ ]可以取代test,这样看上去会“美观”很多 $ if [ $i -eq 5 ]; then echo "YES"; else echo "NO"; fi YES $ if [ $i -gt 4 ] && [ $i -lt 6 ]; then echo "YES"; else echo "NO"; fi YES // 记得给一些字符串变量加上"",记得[之后与]之前多加一个空格 $ str="" $ if [ "$str" = "test"]; then echo "YES"; else echo "NO"; fi -bash: [: missing `]' NO $ if [ $str = "test" ]; then echo "YES"; else echo "NO"; fi -bash: [: =: unary operator expected NO $ if [ "$str" = "test" ]; then echo "YES"; else echo "NO"; fi NO
|
到
这里,条件测试就介绍完了,下面我们将介绍“命令列表”,实际上在上面我们似乎已经使用过了,即多个test命令的组合,通过&&,||
和!组合起来的命令序列。这个命令序列可以有效替换if/then的条件分支结构。这不难想到我们在C语言程序设计中经常做的如下的选择题(很无聊的例
子,但是有意义):下面是否会打印j,如果打印,将打印什么?
Code:
[Ctrl+A Select All]
很
容易知道将打印数字5,因为i==5这个条件成立,而且随后是&&,要判断整个条件是否成立,我们得进行后面的判断,可是这个判断并非常
规的判断,而是先把j修改为5,再转换为真值,所以条件为真,打印出5。因此,这句可以解释为:如果i等于5,那么把j赋值为5,如果j大于1(因为之前
已经为真),那么打印出j的值。这样用&&连结起来的判断语句替代了两个if条件分支语句。
正是基于逻辑运算特有的性质,我们可以通过&&,||来取代if/then等条件分支结构,这样就产生了命令列表。
3. 命令列表
3.1 概要示例:命令列表的执行规律
命令列表的执行规律符合逻辑运算的运算规律,用&&连接起来的命令,如果前者成功返回,将执行后面的命令,反之不然;用||连接起来的命令,如果前者成功返回,将不执行后续命令,反之不然。
Quote: |
// 如果ping通www.lzu.edu.cn,那么打印连通信息 $ ping -c 1 www.lzu.edu.cn -W 1 && echo "=======connected=======" // 这种情况下,只执行第一条命令 $ echo "iiii" || echo "jjjj" iiii
|
非常有趣的问题出来了,即我们上面已经提到的:为什么要让C程序在main函数的最后返回0?如果不这样,把这种程序放入命令列表会有什么样的结果?你自己写个简单的C程序看看,然后放入命令列表看看。
3.2 范例演示:命令列表的作用
在有些时候取代if/then等条件分支结构,这样可以省略一些代码,而且使得程序比较美观、易读,例如:
在脚本里判断程序的参数个数,和参数类型
Code:
[Ctrl+A Select All]
上例要求参数个数为1并且类型为数字。
再加上exit 1,我们将省掉if/then结构
Code:
[Ctrl+A Select All]
这样处理后,对程序参数的判断仅仅需要简单的一行代码,而且变得更美观。
4. 总结
这一节介绍了shell编程中的逻辑运算,条件测试和命令列表。但是貌似没介绍实用一点的范例,是不是得综合起来写一个,或者分析一个现成的程序呢?
还是写一个吧:获取某个ftp服务器根目录下文件信息?
Code:
[Ctrl+A Select All]
说明:
刚开始检查参数个数,至少需要一个,接着检查系统中是否安装有ftp,awk,iconv命令,然后处理参数,最后用ftp命令登录到服务器上下载根目录下的文件信息,最后分离出文件名,并处理编码。关于iconv的用法,请参考man iconv。
演示:
Quote: |
$ chmod +x ftpls.sh $ ./ftpls.sh mirror.lzu.edu.cn about.html cldp doc gentoo gnu.org index.html kernel.org knoppix os rfc slackware slind software tldp tmp ubuntu ubuntu-cn welcome.msg $ ./ftpls.sh xxxy.lzu.edu.cn ftp ftp gb2312 incoming pub xxxy.lzu.edu.cn.url 管理员的信箱是xxxylzu@gmail.com 近期由于大量非校园网用户连接FTP,导致正常用户无法访问,故暂时关闭外网访问
|
再
看看上面的代码,对参数的判断没有用到一个条件分支语句,用命令列表就非常简单的实现了。另外,你是不是想到对这个代码进行改写呢?实现ftp匿名站点的
扫描,结合数据库和CGI实现一个简单的ftp搜索引擎?试试看。不过做这些工作实际上还有很多其他潜在的方法呢,比如用专门的扫描工具(比如nmap,
hping之类),或者自己用C写一个(参考该blog里头的相关帖子)。
关于SHELL编程之布尔运算就到此结束了,欢迎您指出不足、提出意见。很多后续例子将直接回帖发布。
在下一节中,我们将讨论shell编程中非常有趣的字符串操作。
shell编程范例之字符串操作
falcon<zhangjinw@gmail.com>
2007-11-17
忙活了一个礼拜,终于等到周末,可以空下来写点东西。
这次介绍 _字符串操作_ 了,这里先得明白两个东西,什么是字符串,对字符串有哪些操作?
下面是"在线新华字典"的解释:
Quote: |
字符串: 简称“串”。有限字符的序列。数据元素为字符的线性表,是一种数据的逻辑结构。在计算机中可有不同的存储结构。在串上可进行求子串、插入字符、删除字符、置换字符等运算。
|
而字符呢?
Quote: |
字符: 计算机程序设计及操作时使用的符号。包括字母、数字、空格符、提示符及各种专用字符等。
|
照
这样说,之前介绍的数值操作中的数字,逻辑运算中的真假值,都是以字符的形式呈现出来的,是一种特别的字符,对它们的运算只不过是字符操作的特例罢了。而
这里将研究一般字符的运算,它具有非常重要的意义,因为对我们来说,一般的工作都是处理字符而已。这些运算实际上将围绕上述两个定义来做。
第一、找出字符或者字符串的类型,是数字、字母还是其他特定字符,是可打印字符,还是不可打印字符(一些控制字符)。
第二、找出组成字符串的字符个数和字符串的存储结构(比如数组)。
第三、对串的常规操作:求子串、插入字符、删除字符、置换字符、字符串的比较等。
第四、对串的一些比较复杂而有趣的操作,这里将在最后介绍一些有趣的范例。
1. 字符串的属性
1.1 字符串的类型
字符有可能是数字、字母、空格、其他特殊字符,而字符串有可能是它们任何一种或者多种的组合,在组合之后还可能形成一个具有特定意义的字符串,诸如邮件地址,URL地址等。
概要示例: 下面我们来看看如何判断字符的类型。
Quote: |
// 数字或者数字组合(能够返回结果,即程序提出状态是0,说明属于这种类型,反之不然) $ i=5;j=9423483247234; $ echo $i | grep [0-9]* 5 $ echo $j | grep [0-9]* 9423483247234 $ echo $j | grep [0-9]* >/dev/null $ echo $? 0 // 字符组合(小写字母、大写字母、两者的组合) $ c="A"; d="fwefewjuew"; e="fewfEFWefwefe" $ echo $c | grep [A-Z] A $ echo $d | grep "[a-z]*" fwefewjuew $ echo $e | grep "[a-zA-Z]*" fewfEFWefwefe // 字母和数字的组合 $ ic="432fwfwefeFWEwefwef" $ echo $ic | grep "[0-9a-zA-Z]*" 432fwfwefeFWEwefwef // 空格或者Tab键等 $ echo " " | grep " " $ echo -e "\t" | grep "[[:space:]]" #[[:space:]]会同时匹配空格和TAB键
$ echo -e " \t" | grep "[[:space:]]"
$ echo -e "\t" | grep "<tab>" #<tab>为在键盘上按下TAB键,而不是字符<tab> // 匹配邮件地址 $ echo "test2007@lzu.cn" | grep "[0-9a-zA-Z\.]*@[0-9a-zA-Z\.]" test2007@lzu.cn // 匹配URL地址(以http链接为例) $ echo "http://news.lzu.edu.cn/article.jsp?newsid=10135" | grep "http://[0-9a-zA-Z\./=?]*" http://news.lzu.edu.cn/article.jsp?newsid=10135
|
说明:
[1] /dev/null和/dev/zero是非常有趣的两个设备,它们都犹如一个黑洞,什么东西掉进去都会消失殆尽;后者则是一个能源箱,你总能从那里取到0,直到你退出。两者的部分用法见:关于zero及NULL设备的一些问题
[2] [[:space:]]是grep用于匹配空格或者TAB键类型字符串的一种标记,其他类似的标记请查看grep的帮助,man grep。
[3] 上面都是用grep来进行模式匹配,实际上sed, awk都可以用来做模式匹配,关于匹配中用到的正则匹配模式知识,大家可以参考正则匹配模式,更多相关资料请看参考资料。
[4] 如果仅仅想判断字符串是否为空,即判断字符串的长度是否为零,那么可以简单的通过test命令的-z选项来判断,具体用法见test命令,man test.
概要示例: 判断字符是否可打印?如何控制字符在终端的显示。
Quote: |
// 用grep判断某个字符是否为可打印字符 $ echo "\t\n" | grep "[[:print:]]" \t\n $ echo $? 0 $ echo -e "\t\n" | grep "[[:print:]]" $ echo $? 1 // 用echo的-e选项在屏幕控制字符显示位置、颜色、背景等 $ echo -e "\33[31;40m" #设置前景色为黑色,背景色为红色 $ echo -e "\33[11;29H Hello, World\!" #在屏幕的第11行,29列开始打印字符串Hello,World! // 在屏幕的某个位置动态显示当前系统时间 $ while :; do echo -e "\33[11;29H "$(date "+%Y-%m-%d %H:%M:%S"); done // 用col命令过滤掉某些控制字符,在处理诸如script,screen等截屏命令的输出结果时,很有用 $ screen -L $ cat /bin/cat $ exit $ cat screenlog.0 | col -b # 把一些控制字符过滤后,就可以保留可读的操作日志
|
更多关于字符在终端的显示控制方法,请参考资料[20]和字符显示实例[21]:用shell实现的一个动态时钟。
1.2 字符串的长度
概要示例: 除了组成字符串的字符类型外,字符串还有哪些属性呢?组成字符串的字符个数。下面我们来计算字符串的长度,即所有字符的个数,并简单介绍几种求字符串中指定字符个数的方法。
Quote: |
// 计算某个字符串的长度,即所有字符的个数[这计算方法是五花八门,择其优着而用之] $ var="get the length of me" $ echo ${var} # 这里等同于$var get the length of me $ echo ${#var} 20 $ expr length "$var" 20 $ echo $var | awk '{printf("%d\n", length($0));}' 20 $ echo -n $var | wc -c 20 // 计算某些指定一个字符或者多个字符的个数 $ echo $var | tr -cd g | wc -c 2 $ echo -n $var | sed -e 's/[^g]//g' | wc -c 2 $ echo -n $var | sed -e 's/[^gt]//g' | wc -c 5 // 如果要统计单词个数,更多相关信息见《shell编程之数值计算》之 _单词统计_ 实例。 $ echo $var | wc -w 5 $ echo "$var" | tr " " "\n" | grep get | uniq -c 1 $ echo "$var" | tr " " "\n" | grep get | wc -l 1
|
说明:
${}操作符在Bash里头一个“大牛”,能胜任相当多的工作,具体就看看网中人的《shell十三问》之《Shell十三问》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。
1.3 字符串的存储
在
我们看来,字符串是一连串的字符而已,但是为了操作方便,我们往往可以让字符串呈现出一定的结构。在这里,我们不关心字符串在内存中的实际存储结构,仅仅
关系它呈现出来的逻辑结构。比如,这样一个字符串:"get the length of me",我们可以从不同的方面来呈现它。
1.3.1 通过字符在串中的位置来呈现它
这
样我们就可以通过指定位置来找到某个子串。这在c语言里头通常可以利用指针来做。而在shell编程中,有很多可用的工具,诸如expr,awk都提供了
类似的方法来实现子串的查询动作。两者都几乎支持模式匹配(match)和完全匹配(index)。这在后面的字符串操作中将详细介绍。
1.3.2 根据某个分割符来取得字符串的各个部分
这
里最常见的就是行分割符、空格或者TAB分割符了,前者用来当行号,我们似乎已经司空见惯了,因为我们的编辑器就这样“莫名”地处理着行分割符(在
unix下为\n,在其他系统下有一些不同,比如windows下为\r\n)。而空格或者TAB键经常用来分割数据库的各个字段,这似乎也是司空见惯的
事情。
正是因为这样,所以产生了大量优秀的行编辑工具,诸如grep,awk,sed等。在“行内”(姑且这么说吧,就是处理单行,即字符串里头不再包含行分割符)的字符串分割方面,cut和awk提供了非常优越的“行内”(处理单行)处理能力。
1.3.3 更方便地处理用分割符分割好的各个部分
同样是用到分割符,但为了更方便的操作分割以后的字符串的各个部分,我们抽象了“数组”这么一个数据结构,从而让我们更加方便地通过下标来获取某个指定的部分。bash提供了这么一种数据结构,而优秀的awk也同样提供了它,我们这里将简单介绍它们的用法。
概要示例:利用数组存放"get the length of me"的用空格分开的各个部分。
Quote: |
//1. bash提供的数组数据结构,它是以数字为下标的,和C语言从0开始的下标一样 $ var="get the length of me" $ var_arr=($var) #这里把字符串var存放到字符串数组var_arr中了,默认以空格作为分割符 $ echo ${var_arr[0]} ${var_arr[1]} ${var_arr[2]} ${var_arr[3]} ${var_arr[4]} get the length of me $ echo ${var_arr[@]} #这个就是整个字符串所有部分啦,这里可以用*代替@,下同 get the length of me $ echo ${#var_arr[@]} #记得上面求某个字符串的长度么,#操作符,如果想求某个数组元素的字符串长度,那么就把@换成下标吧 5 // 你也可以直接给某个数组元素赋值 $ var_arr[5]="new_element" $ echo ${var_arr[5]} 6 $ echo ${var_arr[5]} new_element // bash里头实际上还提供了一种类似于“数组”的功能,即"for i in 用指定分割符分开的字符串" 的用法 // 即,你可以很方便的获取某个字符串的某个部分 $ for i in $var; do echo -n $i" "; done; get the length of me
//2. awk里头的数组,注意比较它和bash提供的数组的异同 // split把一行按照空格分割,存放到数组var_arr中,并返回数组的长度。注意:这里的第一个元素下标不是0,而是1 $ echo $var | awk '{printf("%d %s\n", split($0, var_arr, " "), var_arr[1]);}' 5 get // 实际上,上面的操作很类似awk自身的行处理功能:awk默认把一行按照空格分割为多个域,并可以通过$1,$2,$3...来获取,$0表示整行 // 这里的NF是该行的域的总数,类似于上面数组的长度,它同样提供了一种通过“下标”访问某个字符串的功能 $ echo $var | awk '{printf("%d | %s %s %s %s %s | %s\n", NF, $1, $2, $3, $4, $5, $0);}' 5 | get the length of me | get the length of me // awk的“数组”功能何止于此呢,看看它的for引用吧,注意,这个和bash里头的for不太一样,i不是元素本身,而是下标 $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",var_arr);}' get the length of me $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",i);}' 1 2 3 4 5 // awk还有更“厉害”的处理能力,它的下标可以不是数字,而可以是字符串,从而变成了“关联”数组,这种“关联”的作用在某些方便将让我们非常方便 // 比如,我们这里就实现一个非凡的应用,把某个文件中的某个系统调用名替换成地址,如果你真正用起它,你会感慨它的“鬼斧神工”的。 // 这就是我在一个场合最好才发现的随好的实现方案:有兴趣看看awk手册帖子中我在3楼回复的实例吧。 $ cat symbol sys_exit sys_read sys_close $ ls /boot/System.map* $ awk '{if(FILENAME ~ "System.map") map[$3]=$1; else {printf("%s\n", map[$1])}}' /boot/System.map-2.6.20-16-generic symbol c0129a80 c0177310 c0175d80 // 另外,awk还支持删除某个数组元素,如果你不用了就可以用delete函数给删除掉。如果某些场合有需要的话,别忘了awk还支持二维数组。
|
okay,就介绍到这里啦。为什么要介绍这些内容?再接着看下面的内容,你就会发现,那些有些的工具是怎么产生和发展起来的了,如果累了,看看最后一篇参考资料吧,它介绍了一些linux命令名字的由来,说不定可以帮助你理解本节下面的部分呢。
2. 字符串常规操作
字符串操作包括取子串、查询子串、插入子串、删除子串、子串替换、子串比较、子串排序、子串进制转换、子串编码转换等。
2.1 取子串
概要示例:取子串的方法主要有:直接到指定位置求子串,字符匹配求子串。
Quote: |
// 按照位置取子串,比如从什么位置开始,取多少个字符 $ var="get the length of me" $ echo ${var:0:3} get $ echo ${var:(-2)} # 方向相反呢 me $ echo `expr substr "$var" 5 3` #记得把$var引起来,否则expr会因为空格而解析错误 the $ echo $var | awk '{printf("%s\n", substr($0, 9, 6))}' length
// 匹配字符求子串 $ echo ${var%% *} #从右边开始计算,删除最左边的空格右边的所有字符 get $ echo ${var% *} #从右边开始计算,删除第一个空格右边的所有字符 get the length of $ echo ${var##* } #从左边开始计算,删除最右边的空格左边的所有字符 me $ echo ${var#* } #从左边开始计算,删除第一个空格左边的所有字符 the length of me
$ echo $var | awk '{printf("%s\n", $1);}' # awk把$var按照空格分开为多个变量,依次为$1,$2,$3,$4,$5 get $ echo $var | awk '{printf("%s\n", $5);}' me
$ echo $var | cut -d" " -f 5 #差点把cut这个小东西忘记啦,用起来和awk类似, -d指定分割符,如同awk用-F指定分割符一样,-f指定“域”,如同awk的$数字。
$ echo $var | sed 's/ [a-z]*//g' #删除所有 空格+字母串 的字符串,所以get后面的全部被删除了 get $ echo $var | sed 's/[a-z]* //g' me
$ echo $var | tr " " "\n" | sed -n 1p #sed有按地址(行)打印(p)的功能,记得先用tr把空格换成行号 get $ echo $var | tr " " "\n" | sed -n 5p me
// tr也可以用来取子串哦,它也可以类似#和%来“拿掉”一些字符串来实现取子串 $ echo $var | tr -d " " getthelengthofme $ echo $var | tr -cd "[a-z]" #把所有的空格都拿掉了,仅仅保留字母字符串,注意-c和-d的用法 getthelengthofme
|
说明:
[1] %和#的区别是,删除字符的方向不一样,前者在右,后者在左,%%和%,##和#的方向是前者是最大匹配,后者是最小匹配。(好的记忆方法见网中人的键盘记忆法:#$%是键盘依次从左到右的三个键)
[2] tr的-c选项是complement的缩写,即invert,而-d选项是删除的意思,tr -cd "[a-z]"这样一来就变成保留所有的字母啦。
对于字符串的截取,实际上还有一些命令,如果head,tail等可以实现有意思的功能,可以截取某个字符串的前面、后面指定的行数或者字节数。例如:
Quote: |
$ echo "abcdefghijk" | head -c 4 abcd $ echo -n "abcdefghijk" | tail -c 4 hijk
|
2.2. 查询子串
概要示例:子串查询包括:返回符合某个模式的子串本身和返回子串在目标串中的位置。
准备:在进行下面的操作之前,请把http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1385.html链接中的内容复制到一个文本text里头,用于下面的操作。
Quote: |
// 查询子串在目标串中的位置 $ var="get the length of me" $ expr index "$var" t #貌似仅仅可以返回某个字符或者多个字符中第一个字符出现的位置 3 $ echo $var | awk '{printf("%d\n", match($0,"the"));}' #awk却能找出字串,match还可以匹配正则表达式 5
// 查询子串,返回包含子串的行(awk,sed都可以实现这些功能,但是grep最擅长) $ grep "consists of" text # 查询text文件包含consists of的行,并打印这些行 $ grep "consists[[:space:]]of" -n -H text # 打印文件名,子串所在行的行号和该行的内容 $ grep "consists[[:space:]]of" -n -o text # 仅仅打印行号和匹配到的子串本身的内容 $ awk '/consists of/{ printf("%s:%d:%s\n",FILENAME, FNR, $0)}' text #看到没?和grep的结果一样 $ sed -n -e '/consists of/=;/consists of/p' text #同样可以打印行号
|
说明:
[1] awk,grep,sed都能通过模式匹配查找指定的字符串,但是它们各有擅长的领域,我们将在后续的章节中继续使用和比较它们,从而发现各自的优点。
[2] 在这里我们姑且把文件内容当成了一个大的字符串,在后面的章节中我们将专门介绍文件的操作,所以对文件内容中存放字符串的操作将会有更深入的分析和介绍。
2.3. 子串替换
子
串替换就是把某个指定的子串替换成其他的字符串,实际上这里就蕴含了“插入子串”和“删除子串”的操作。例如,你想插入某个字符串到某个子串之前,就可以
把原来的子串替换成”子串+新的字符串“,如果想删除某个子串,就把子串替换成空串。不过有些工具提供了一些专门的用法来做插入子串和删除子串的操作,所
以呆伙还是会专门介绍的。另外,要想替换掉某个子串,一般都是先找到子串(查询子串),然后再把它替换掉的,实质上很多工具在使用和设计上都体现了这么一
点。
概要示例:下面我们把变量var中的空格替换成下划线看看。
Quote: |
// 用{}运算符,还记得么?网中人的教程。 $ var="get the length of me" $ echo ${var/ /_} #把第一个空格替换成下划线 get_the length of me $ echo ${var// /_} #把所有空格都替换成了下划线了 get_the_length_of_me
// 用awk,awk提供了转换的最小替换函数sub和全局替换函数gsub,类似/和// $ echo $var | awk '{sub(" ", "_", $0); printf("%s\n", $0);}' get_the length of me $ echo $var | awk '{gsub(" ", "_", $0); printf("%s\n", $0);}' get_the_length_of_me
// 用sed了,子串替换可是sed的特长 $ echo $var | sed -e 's/ /_/' #s <= substitude get_the length of me $ echo $var | sed -e 's/ /_/g' #看到没有,简短两个命令就实现了最小匹配和最大匹配g <= global get_the_length_of_me
// 有忘记tr命令么?可以用替换单个字符的 $ echo $var | tr " " "_" get_the_length_of_me $ echo $var | tr '[a-z]' '[A-Z]' #这个可有意思了,把所有小写字母都替换为大写字母 GET THE LENGTH OF ME
|
说明:sed还有很有趣的标签用法呢,下面再介绍吧。
有一种比较有意思的字符串替换是,整个文件行的倒置,这个可以通过tac命令实现,它会把文件中所有的行全部倒转过来。在一定意义上来说,排序实际上也是一个字符串替换。
2.4. 插入子串
插入子串:就是在指定的位置插入子串,这个位置可能是某个子串的位置,也可能是从某个文件开头算起的某个长度。通过上面的练习,我们发现这两者之间实际上是类似的。
公式:插入子串=把"old子串"替换成"old子串+new子串"或者"new子串+old子串"
概要示例::下面在var字符串的空格之前或之后插入一个下划线
Quote: |
// 用{} $ var="get the length of me" $ echo ${var/ /_ } #在指定字符串之前插入一个字符串 get_ the length of me $ echo ${var// /_ } get_ the_ length_ of_ me $ echo ${var/ / _} #在指定字符串之后插入一个字符串 get _the length of me $ echo ${var// / _} get _the _length _of _me
// 其他的还用演示么?这里主要介绍sed怎么用来插入字符吧,因为它的标签功能很有趣 $ echo $var | sed -e 's/\( \)/_\1/' #\(和\)将不匹配到的字符串存放为一个标签,按匹配顺序为\1,\2... get_ the length of me $ echo $var | sed -e 's/\( \)/_\1/g' get_ the_ length_ of_ me $ echo $var | sed -e 's/\( \)/\1_/' get _the length of me $ echo $var | sed -e 's/\( \)/\1_/g' get _the _length _of _me
// 看看sed的标签的顺序是不是\1,\2....,看到没?\2和\1掉换位置后,the和get的位置掉换了 $ echo $var | sed -e 's/\([a-z]*\) \([a-z]*\) /\2 \1 /g' the get of length me // sed还有专门的插入指令,a和i,分别表示在匹配的行后和行前插入指定字符 $ echo $var | sed '/get/a test' get the length of me test $ echo $var | sed '/get/i test' test get the length of me
|
2.5. 删除子串
删除子串:应该很简单了吧,把子串替换成“空”(什么都没有)不就变成了删除么。还是来简单复习一下替换吧。
概要示例::把var字符串中所有的空格给删除掉。
鼓励: 这样一替换不知道变成什么单词啦,谁认得呢?但是中文却是连在一起的,所以中文有多难,你想到了么?原来你也是个语言天才,而英语并不可怕,你有学会它的天赋,只要你有这个打算。
Quote: |
// 再用{} $ echo ${var// /} getthelengthofme // 再用awk $ echo $var | awk '{gsub(" ","",$0); printf("%s\n", $0);}' // 再用sed $ echo $var | sed 's/ //g' getthelengthofme // 还有更简单的tr命令,tr也可以把" "给删除掉,看 $ echo $var | tr -d " " getthelengthofme
|
如
果要删除掉第一个空格后面所有的字符串该怎么办呢?还记得{}的#和%用法么?如果不记得,回到这一节的还头开始复习吧。(实际上删除子串和取子串未尝不
是两种互补的运算呢,删除掉某些不想要的子串,也就同时取得另外那些想要的子串——这个世界就是一个“二元”的世界,非常有趣)
2.6. 子串比较
这
个很简单:还记得test命令的用法么?man
test。它可以用来判断两个字符串是否相等的。另外,你发现了“字符串是否相等”和“字符串能否跟另外一个字符串匹配"两个问题之间的关系吗?如果两个
字符串完全匹配,那么这两个字符串就相等了。所以呢,上面用到的字符串匹配方法,也同样可以用到这里。
2.7. 子串排序
差点忘记这个重要的内容了,子串排序可是经常用到的,常见的有按字母序、数字序等正序或反序排列。sort命令可以用来做这个工作,它和其他行处理命令一样,是按行操作的,另外,它类似cut和awk,可以指定分割符,并指定需要排序的列。
Quote: |
$ var="get the length of me" $ echo $var | tr ' ' '\n' | sort #正序排 get length me of the $ echo $var | tr ' ' '\n' | sort -r #反序排 the of me length get $ cat data.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 41 45 44 44 26 44 42 20 20 38 37 25 45 45 45 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 44 20 30 39 35 38 38 28 25 30 36 20 24 32 33 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 41 33 51 39 20 20 44 37 38 39 42 40 37 50 50 46 47 48 49 50 51 52 53 54 55 56 42 43 41 42 45 42 19 39 75 17 17 $ cat data.txt | sort -k 2 -n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 44 20 30 39 35 38 38 28 25 30 36 20 24 32 33 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 41 33 51 39 20 20 44 37 38 39 42 40 37 50 50 42 43 41 42 45 42 19 39 75 17 17 41 45 44 44 26 44 42 20 20 38 37 25 45 45 45 46 47 48 49 50 51 52 53 54 55 56
|
2.7. 子串进制转换
如果字母和数字字符用来计数,那么就存在进制转换的问题。在数值计算一节的回复资料里,我们已经介绍了bc命令,这里再简单的复习一下。
Quote: |
$ echo "ibase=10;obase=16;10" | bc A
|
说明:ibase指定输入进制,obase指出输出进制,这样通过调整ibase和obase,你想怎么转就怎么转啦!
2.7. 子串编码转换
什
么是字符编码?这个就不用介绍了吧,看过那些乱七八糟显示的网页么?大多是因为浏览器显示时的”编码“和网页实际采用的”编码“不一致导致的。字符编码通
常是指把一序列”可打印“字符转换成二进制表示,而字符解码呢则是执行相反的过程,如果这两个过程不匹配,则出现了所谓的”乱码“。
为了
解决”乱码“问题呢?就需要进行编码转换。在linux下,我们可以使用iconv这个工具来进行相关操作。这样的情况经常在不同的操作系统之间移动文
件,不同的编辑器之间交换文件的时候遇到,目前在windows下常用的汉字编码是gb2312,而在linux下则大多采用utf8。
Quote: |
$ nihao_gb2312=$(echo "你好" | iconv -f utf8 -t gb2312) $ echo $nihao_gb2312 ���� $ nihao_utf8=$(echo $nihao_gb2312 | iconv -f gb2312 -t utf8) $ PS1="$ " $ echo $nihao_utf8 你好
|
说明:我的终端默认编码是utf8,所以结果如上。
3. 字符串操作范例
实际上,在用Bash编程时,大部分时间都是在处理字符串,因此把这一节熟练掌握非常重要。
3.1 处理一个非常有意义的字符串:URL地址
范例演示:处理URL地址
URL
地址(URL(Uniform Resoure
Locator:统一资源定位器)是WWW页的地址)几乎是我们日常生活的玩伴,我们已经到了无法离开它的地步啦,对它的操作很多,包括判断URL地址的
有效性,截取地址的各个部分(服务器类型、服务器地址、端口、路径等)并对各个部分进行进一步的操作。
下面我们来具体处理这个URL地址:
ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz
Quote: |
$ url="ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz" // 匹配URL地址,判断URL地址的有效性 $ echo $url | grep "ftp://[a-z]*:[a-z]*@[a-z\./-]*" // 截取服务器类型 $ echo ${url%%:*} ftp $ echo $url | cut -d":" -f 1 ftp // 截取域名 $ tmp=${url##*@} ; echo ${tmp%%/*} mirror.lzu.edu.cn // 截取路径 $ tmp=${url##*@} ; echo ${tmp%/*} mirror.lzu.edu.cn/software // 截取文件名 $ basename $url scim-1.4.7.tar.gz $ echo ${url##*/} scim-1.4.7.tar.gz // 截取文件类型(扩展名) $ echo $url | sed -e 's/.*[0-9].\(.*\)/\1/g' tar.gz
|
有了上面的知识,我们就可以非常容易地进行这些工作啦:修改某个文件的文件名,比如调整它的编码,下载某个网页里头的所有pdf文档等。这些就作为练习自己做吧,如果遇到问题,可以在回帖交流。相应地可以参考这个例子:
[1] 用脚本下载某个网页中的英文原著(pdf文档)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1228.html
3.2 处理格式化的文本:/etc/passwd
平时做工作,大多数时候处理的都是一些“格式化”的文本,比如类似/etc/passwd这样的有固定行和列的文本,也有类似tree命令输出的那种具有树形结构的文本,当然还有其他具有特定结构的文本。
关于树状结构的文本的处理,可以考虑看看这两个例子:
[1] 用AWK转换树形数据成关系表
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1260.html
[2] 用Graphviz进行可视化操作──绘制函数调用关系图
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1425.html
实际上,只要把握好特性结构的一些特点,并根据具体的应用场合,处理起来就不会困难。
下面我们来介绍具体有固定行和列的文本的操作,以/etc/passwd文件为例。关于这个文件的帮忙和用户,请通过man 5 passwd查看。下面我们对这个文件以及相关的文件进行一些有意义的操作。
Quote: |
// 选取/etc/passwd文件中的用户名和组ID两列 $ cat /etc/passwd | cut -d":" -f1,4 // 选取/etc/group文件中的组名和组ID两列 $ cat /etc/group | cut -d":" -f1,3 // 如果想找出所有用户所在的组,怎么办? $ join -o 1.1,2.1 -t":" -1 4 -2 3 /etc/passwd /etc/group root:root bin:bin daemon:daemon adm:adm lp:lp pop:pop nobody:nogroup falcon:users //
先解释一下:join命令用来连接两个文件,有点类似于数据库的两个表的连接。-t指定分割符,"-1 4 -2
3"指定按照第一个文件的第4列和第二个文件的第3列,即组ID进行连接,"-o
1.1,2.1"表示仅仅输出第一个文件的第一列和第二个文件的第一列,这样就得到了我们要的结果,不过,可惜的是,这个结果并不准确,再进行下面的操
作,你就会发现: $ cat /etc/passwd | sort -t":" -n -k 4 > /tmp/passwd $ cat /etc/group | sort -t":" -n -k 3 > /tmp/group $ join -o 1.1,2.1 -t":" -1 4 -2 3 /tmp/passwd /tmp/group halt:root operator:root root:root shutdown:root sync:root bin:bin daemon:daemon adm:adm lp:lp pop:pop nobody:nogroup falcon:users games:users // 可以看到这个结果才是正确的,所以以后使用join千万要注意这个问题,否则采取更保守的做法似乎更能保证正确性,更多关于文件连接的讨论见参考资料[14]
|
上
面涉及到了处理某格式化行中的指定列,包括截取(如SQL的select用法),连接(如SQL的join用法),排序(如SQL的order
by用法),都可以通过指定分割符来拆分某个格式化的行,另外,“截取”的做法还有很多,不光是cut,awk,甚至通过IFS指定分割符的read命令
也可以做到,例如:
Quote: |
$ IFS=":"; cat /etc/group | while read C1 C2 C3 C4; do echo $C1 $C3; done
|
因此,熟悉这些用法,我们的工作将变得非常灵活有趣。
到这里,需要做一个简单的练习,如何把按照列对应的用户名和用户ID转换成按照行对应的,即把类似下面的数据:
Quote: |
$ cat /etc/passwd | cut -d":" -f1,3 --output-delimiter=" " root 0 bin 1 daemon 2
|
转换成:
Quote: |
$ cat a root bin daemon 0 1 2
|
并转换回去,有什么办法呢?记得诸如tr,paste,split等命令都可以使用。
参考方法:
*正转换:先截取用户名一列存入文件user,再截取用户ID存入id,再把两个文件用paste -s命令连在一起,这样就完成了正转换。
*逆转换:先把正转换得到的结果用split -1拆分成两个文件,再把两个拆分后的文件用tr把分割符"\t"替换成"\n",只有用paste命令把两个文件连在一起,这样就完成了逆转换。
更多有趣的例子,可以参考该序列第一部分的回复,即参考资料[16]的回复,以及兰大开源社区镜像站用的镜像脚本,即参考资料[17],另外,参考资料[18]关于用Shell实现一个五笔反查小工具也值得阅读和改进。
*更多例子将逐步补充和完善。
参考和推荐资料:
[1] 《高级Bash脚本编程指南》之操作字符串
http://www.linuxpk.com/doc/abs/string-manipulation.html
[2] 《高级Bash脚本编程指南》之指定变量的类型
http://www.linuxpk.com/doc/abs/declareref.html
[3] 《Shell十三问》之$(( )) 與 $( ) 還有${ } 差在哪?
http://bbs.chinaunix.net/viewthread.php?tid=218853&extra=&page=7#pid1617953
[4] Regular Expressions - User guide
http://www.zytrax.com/tech/web/regex.htm
[5] Regular Expression Tutorial
http://analyser.oli.tudelft.nl/regex/index.html.en
[6] Grep Tutorial
http://www.panix.com/~elflord/unix/grep.html
[7] Sed Tutorial
http://www.panix.com/~elflord/unix/sed.html
[8] awk Tutorial
http://www.gnulamp.com/awk.html
[9] sed Tutorial
http://www.gnulamp.com/sed.html
[10] An awk Primer
http://www.vectorsite.net/tsawk.html
[11] 一些奇怪的 unix 指令名字的由来
http://www.linuxsir.org/bbs/showthread.php?t=24264
[12] 磨练构建正则表达式模式的技能
http://www.ibm.com/developerworks/cn/aix/library/au-expressions.html
[13] 实用正则表达式
http://www.linuxlong.com/forum/bbs-27-1.html
[14] AWK使用手册 3 楼的回复帖
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=1006&forum=26
[15] 基础11:文件分类、合并和分割(sort,uniq,join,cut,paste,split)
http://blog.chinaunix.net/u/9465/showart_144700.html
[16] Shell编程范例之数值运算
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1391.html
[17] 兰大Mirror镜像站的镜像脚本
http://oss.lzu.edu.cn/blog/article.php?tid_1236.html
[18] 一个用Shell写的五笔反查小工具
http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_1017.html
[19] 使用Linux 文本工具简化数据的提取
http://linux.chinaunix.net/docs/2006-09-22/2803.shtml
[20] 如何控制终端:光标位置,字符颜色,背景,清屏...
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=962&forum=13
[21] 在终端动态显示时间
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=964&forum=26
后记:
[1] 这一节本来是上个礼拜该弄好的,但是这些天太忙了,到现在才写好一个“初稿”,等到有时间再补充具体的范例。这一节的范例应该是最最有趣的,所有得好好研究一下几个有趣的范例。
[2] 写完[1]貌似是1点多,刚check了一下错别字和语法什么的,再添加了一节,即“字符串的存储结构”,到现在已经快half past 2啦,晚安,朋友们。
[3] 26号,添加“子串进制转换”和“子串编码转换”两小节以及一个处理URL地址的范例。
刚找资料搜到《Bash 实例》,有时间可以看看:
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-1/
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-2/
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-3/
刚去linuxsir.org,看到有人写了一篇《Bash字符串处理》,比较简洁,推荐看看:http://www.linuxsir.org/bbs/showthread.php?threadid=76402
补充例1: 调整文件名的编码
如果在Linux系统下,从 windows的ftp服务器或者系统上复制文件,经常遇到文件名的编码问题,即原文件名的编码是gb2312,而linux下的文件名编码则为utf8。怎么办呢?修改文件名?手动修改么?非也,非也,写个脚本来做。
先来分析一下,假如原来的文件名是 $FROM,要修改为 $TO,则可以:
$ mv $FROM $TO
原来的文件名是gb2312的编码,而现在的编码则是utf8,即
TO=$(echo $FROM | iconv -f gb2312 -t utf8)
这样,如果执行mv操作,就可以把某个文件名的编码调整啦。
但是,如果要调整某个目录下的所有文件的编码呢?
也很简单,可以先用ls命令把某个目录下的文件列出来,然后传递给xargs命令来扩展命令,比如这样:
我们先把上面的修改单个文件名的过程放入一个脚本:
cn.sh
[code]
#!/bin/sh
FROM=$1
TO=$(echo $FROM | iconv -f gb2312 -t utf8)
mv $FROM $TO
[/code]
然后呢,在需要修改文件名的目录下,运行:
$ chmod +x cn.sh
$ ls | xargs -i ./cn.sh {}
而如果要修改某个目录下包括子目录中的所有文件名呢?也很简单,还记得find命令么?
$ find | xargs -i ./cn.sh {}
是不是相当简单,这种解决问题的方式我们在以后会继续深入讨论,这完全体现了Unix的K.I.S.S的哲学,请仔细体会 :-)
当然,在实际解决问题的时候,需要在cn.sh引入一些出错的处理机制,比如,如果原编码并非gb2312,在转换的时候就可能出错,这个时候,可以考虑采用一些办法:
1、比如如果出错,不改变这个文件名,即在TO=...之后加入一句
[ $? -ne 0 ] && exit 1
2、如果出错,那么继续找,直到找出正确的原编码为止
可以把TO=...所在句修改为一个循环,不断判断知道转换正确为止,这样比地一种办法更有效。
sed单行脚本快速参考:
http://sed.sourceforge.net/sed1line_zh-CN.html
需要提到的是这些工具可以用来进行比较复杂有意思的文本处理,包括格式化,过滤字符等:fold, fmt, column, col,
colrm, nl, pr, gettext, msgfmt, tex, groff,
lex等,更多资料请查看相关手册或者是《高级Bash脚本编程指南》
关于范围匹配的实例:通过sed或者awk等命令匹配某个文件中的特定范围的行。(它有着非常重要的意义,比如,你要用脚本把某个C语言程序中的某个符合要求的函数以及定义找出来,这种用法就起作用啦。)
这里是一个测试文件:一本书第7章的课后答案。文件名叫README吧。
Code:[Ctrl+A Select All]实例演示:
Quote: |
// 打印出答案前指定行范围:第7行到第9行,刚好找出了第2题的答案 $ sed -n 7,9p README 7.2 it will depend on the exection mode, interactive or redirection to a file, if interactive, the "output" action will accur after the \n char with the line buffer mode, else, it will be really "printed" after all of the strings have // 其实,因为这个文件内容格式很有特色,有更简单的办法 $ awk '/7.2/,/^$/ {printf("%s\n", $0);}' README 7.2 it will depend on the exection mode, interactive or redirection to a file, if interactive, the "output" action will accur after the \n char with the line buffer mode, else, it will be really "printed" after all of the strings have been stayed in the buffer.
|
by falcon
2007-11-5
这一周我们来探讨文件操作。在日常学习和工作中,我们总是在不断的和各种文件打交道,这些文件包括普通的文本文件,可以执行的程序文件,带有控制字符的文
档、存放各种文件的目录文件、网络套接字文件、设备文件等。这些文件又具有诸如属主、大小、创建和修改日期等各种属性。文件对应文件系统的一些数据块,对
应磁盘等存储设备的一片连续空间,对应于显示设备却是一些具有不同形状的字符集。
在这一节,为了把关注点定位在文件本身,我们不会深入探讨文件系统以及存储设备是如何组织文件的(在后续章节再深入探讨,如果有兴趣也可以提前看参考资料
[1][2][3][4][5]),而是探讨我们对它最熟悉的一面,即把文件当成是一序列的字符(一个byte)集合看待。因此之前介绍的“shell编
程范例之字符串操作”在这里将会得到广泛的应用,关于普通文件的读写操作我想我们已经用得非常熟练啦,那就是“重定向”,在这里,我们会把这部分独立出来
介绍。关于文件在Linux下的“数字化”(文件描述符)高度抽象,“一切皆为文件”的哲学在shell编程里也得到了深刻的体现。
下面我们先来介绍文件的各种属性,然后介绍普通文件的一般操作。
1、文件的各种属性
首先,我们通过文件的结构体来看看文件到底有哪些属性:
Code:
[Ctrl+A Select All]
下面逐次来了解这些属性,如果需要查看某个文件的属性,用stat命令就可以,会按照上面的结构体把信息列出来。另外,ls命令在跟上一定的参数后也可以显示文件的相关属性,比如-l参数。
1.1 文件类型
文件类型对应于上面的st_mode, 文件类型有很多,比如常规文件、符号链接(硬链接、软链接)、管道文件、设备文件(符号设备、块设备)、socket文件等,不同的文件类型对应不同的功能和作用。
1.1.1 在命令行简单地区分各类文件:
Quote: |
$ ls -l total 12 drwxr-xr-x 2 root root 4096 2007-12-07 20:08 directory_file prw-r--r-- 1 root root 0 2007-12-07 20:18 fifo_pipe brw-r--r-- 1 root root 3, 1 2007-12-07 21:44 hda1_block_dev_file crw-r--r-- 1 root root 1, 3 2007-12-07 21:43 null_char_dev_file -rw-r--r-- 2 root root 506 2007-12-07 21:55 regular_file -rw-r--r-- 2 root root 506 2007-12-07 21:55 regular_file_hard_link lrwxrwxrwx 1 root root 12 2007-12-07 20:15 regular_file_soft_link -> regular_file $ stat directory_file/ File: `directory_file/' Size: 4096 Blocks: 8 IO Block: 4096 directory Device: 301h/769d Inode: 521521 Links: 2 Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2007-12-07 20:08:18.000000000 +0800 Modify: 2007-12-07 20:08:18.000000000 +0800 Change: 2007-12-07 20:08:18.000000000 +0800 $ stat null_char_dev_file File: `null_char_dev_file' Size: 0 Blocks: 0 IO Block: 4096 character special file Device: 301h/769d Inode: 521240 Links: 1 Device type: 1,3 Access: (0644/crw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2007-12-07 21:43:38.000000000 +0800 Modify: 2007-12-07 21:43:38.000000000 +0800 Change: 2007-12-07 21:43:38.000000000 +0800
|
说
明:
在ls命令结果每行的第一个字符我们可以看到,它们之间都不相同,这正好反应了不同文件的类型。d表示目录,-表示普通文件(或者硬链接),l表示符号链
接,p表示管道文件,b和c分别表示块设备和字符设备(另外s表示socket文件)。在stat命令的结果中,我们可以在第二行的最后找到说明,从上面
的操作可以看出,directory_file是目录,stat命令的结果中用directory表示,而null_char_dev_file它则用
character special file说明。
1.1.2 简单比较它们的异同
通常,我们只会用碰到目录、普通文
件、以及符号链接,很少碰到其他类型的文件,不过这些文件还是各有用处的,如果要做嵌入式开发或者进程通信等,你可能会涉及到设备文件、有名管道
(FIFO)。下面我们通过简单的操作来反应它们之间的区别(具体的原理可能会在下一节“shell编程范例之文件系统”介绍,如果感兴趣,也可以提前到
网上找找设备文件的作用、快设备和字符设备的区别、以及驱动程序中如何编写相关设备驱动等,可参考资料[12])。
Quote: |
// 对于普通文件:就是一序列字符的集合,所以可以读、写等 $ echo "hello, world" > regular_file $ cat regular_file hello, world // 目录文件下,我们可以创建新的文件,所以目录文件还有叫法:文件夹,到后面我们会分析目录文件的结构体,它实际上存放了它下面的各个文件的文件名。 $ cd directory_file $ touch file1 file2 file3 // 对于有名管道,操作起来比较有意思:如果你要读它,除非有内容,否则阻塞;如果你要写它,除非有人来读,否则阻塞。它常用于进程通信中。你可以打开两个终端terminal1和terminal2,试试看: terminal1$ cat fifo_pipe #刚开始阻塞在这里,直到下面的写动作发生,才打印test字符串 terminal2$ echo "test" > fifo_pipe // 关于块设备,字符设备,上面的两个设备文件对应于/dev/hda1和/dev/null,如果你用过u盘,或者是写过简单的脚本的话,这样的用法你应该用过 :-) $ mount hda1_block_dev_file /mnt #挂载硬盘的第一个分区到/mnt下(关于挂载的原理,我们在下一节讨论) $ echo "fewfewfef" > /dev/null #/dev/null像个黑洞,什么东西丢进去都消失殆尽 //
最后两个文件分别是regular_file文件的硬链接和软链接,你去读写它们,他们的内容是相同的,不过你去删除它们,他们去互不相干,硬链接和软链
接又有什么不同呢?前者可以说就是原文件,后者呢只是有那么一个inode,但没有实际的存储空间,建议用stat命令查看它们之间的区别,包括它们的
Blocks,inode等值,也可以考虑用diff比较它们的大小。 $ ls regular_file* ls regular_file* -l -rw-r--r-- 2 root root 204800 2007-12-07 22:30 regular_file -rw-r--r-- 2 root root 204800 2007-12-07 22:30 regular_file_hard_link lrwxrwxrwx 1 root root 12 2007-12-07 20:15 regular_file_soft_link -> regular_file $ rm regular_file #删除原文件 $ cat regular_file_hard_link #硬链接还在,而且里头的内容还有呢 fefe $ cat regular_file_soft_link cat: regular_file_soft_link: No such file or directory #虽然软链接文件本身还在,不过因为它本身不存储内容,所以读不大东西拉,这就是软链接和硬链接的区别,该知道则么用它们了吧。
|
1.1.3 普通文件再分类
文
件类型从Linux文件系统那么一个级别分了以上那么多类型,不过普通文件还是可以再分的(根据文件内容的”数据结构“分),比如常见的文本文件,可执行
的ELF文件,odt文档,jpg图片格式,swap分区文件,pdf文件。除了文件文件外,它们大多是二进制文件,有特定的结构,因此需要有专门的工具
来创建和编辑它们。关于各类文件的格式,可以参考相关文档标准。不过如果能够了解Linux下可执行的ELF文件的工作原理,可能对你非常有用。所以,如
果有兴趣,建议阅读一下参考资料中和ELF文件相关部分。
虽然各类普通文件都有专属的操作工具,但是我们还是可以直接读、写它们。具体的操作我们在“文件的基本操作部分深入介绍”,这里先提到这么几个工具。
od:以八进制或者其他格式“导出”文件内容。
strings:读出文件中的字符(可打印的字符)
gcc,gdb,readelf,objdump等:ELF文件分析、处理工具(gcc编译器、gdb调试器、readelf分析elf文件,objdump反编译工具)
这
里补充一个非常重要的命令,file,这个命令用来查看各类文件的属性。和stat命令相比,它可以进一步识别普通文件,即stat命令显示的
regular file。因为regular
file可以有各种不同的结构,因此在操作系统的支持下得到不同的解释,执行不同的动作。虽然,Linux下,文件也会加上特定的后缀以便用户能够方便地
识别文件的类型,但是Linux操作系统根据文件头识别各类文件,而不是文件后缀(请参考资料[23]),这样,在解释相应的文件时就更不容易出错。下面
我们简单介绍file命令的用法。
Quote: |
$ file ./ ./: directory $ file /etc/profile /etc/profile: ASCII English text $ file /lib/libc-2.5.so /lib/libc-2.5.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped $ file /bin/test /bin/test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), stripped $ file /dev/hda /dev/hda: block special (3/0) $ file /dev/console /dev/console: character special (5/1) $ cp /etc/profile . $ tar zcf profile.tar.gz profile $ file profile.tar.gz profile.tar.gz: gzip compressed data, from Unix, last modified: Tue Jan 4 18:53:53 2000 $ mkfifo fifo_test $ file fifo_test fifo_test: fifo (named pipe)
|
更多用法见file命令的手册,关于file命令的实现原理,请参考magic的手册(看看/etc/file/magic文件,了解什么是文件的magic number等)。
1.2 文件属主
Linux
作为一个多用户系统,为多用户使用同一个系统提供了极大的方便,比如对于系统上的文件,它通过属主来区分不同的用户,以便分配它们对不同文件的操作权限。
为了更方便地管理,文件属主包括该文件所属用户,以及该文件所属的用户组,因为用户可以属于多个组。我们先来简单介绍Linux下用户和组的管理(更多信
息请参考资料[15])。
Linux下提供了一组命令用于管理用户和组,比如用户创建它们的useradd和groupadd,用户删除
它们的userdel和groupdel,另外,passwd命令用于修改用户密码。当然,Linux还提供了两个相应的配置,即/etc/passwd
和/etc/group,另外,有些系统还把密码单独放到了配置文件/etc/shadow中。关于它们的详细用法请参考资料[15],在这里我们不介绍
了,仅介绍文件和用户之间的一些关系。
比如,修改文件的属主:
Quote: |
$ chown 用户名:组名 文件名
|
如果要递归的修改某个目录下所有用户属主,可以添加-R选项。
在本节开头我们列的文件结构体中,可以看到仅仅有用户ID和组ID的信息,但ls -l的结果却显示了用户名和组名信息,这个是怎么实现的呢?下面先看看-n的结果:
Quote: |
$ ls -n regular_file -rw-r--r-- 1 0 0 115 2007-12-07 23:45 regular_file $ ls -l regular_file -rw-r--r-- 1 root root 115 2007-12-07 23:45 regular_file
|
可
以看到,ls -n显示了用户ID和组ID,而ls
-l显示了它们的名字。还记得上面提到的两个配置文件/etc/passwd和/etc/group文件么?它们分别存放了用户ID和用户名,组ID和组
名的对应关系,因此很容易想到ls -l命令在实现时是如何通过文件结构体的ID信息找到它们对应的名字信息的。如果想对ls
-l命令的实现有更进一步的了解,可以用strace跟踪看看它是否读取了/etc/passwd和/etc/group这个两个文件。
Quote: |
$ strace -f -o strace.log ls -l regular_file $ cat strace.log | egrep "passwd|group|shadow" 2989 open("/etc/passwd", O_RDONLY) = 3 2989 open("/etc/group", O_RDONLY) = 3
|
说明:strace是一个非常有用的工具,可以用来跟踪系统调用和信号。如同gdb等其他强大的工具一样,它基于系统的ptrace系统调用实现,所以如果你感兴趣,可以好好研究一下这个工具。
实际上,把属主和权限分开介绍实际上不太好,因为只有它们两者结合才使得多用户系统成为可能,否则无法隔离不同用户对某个文件的操作,所以下面来介绍文件操作权限。
1.3 不同属主的文件操作权限
从ls
-l命令的结果的第一列的后9个字符中,我们可以看到类似这样的信息rwxr-xr-x,它们对应于文件结构体的st_mode部分(st_mode包含
文件类型信息和文件权限信息两部分)。这类信息可以分成三部分,即rwx,r-x,r-x,分别对应该文件所属用户,所属组,其他组对该文件的操作权限,
如果有rwx中任何一个表示可读、可写、可执行,如果为-表示没有这个权限。对应的,可以用八进制来表示它,比如rwxr-xr-x就可表示成二进制
111101101,对应的八进制则为755。正是因为这样,我们要修改文件的操作权限时,也可以有多种方式来实现,它们都可通过chmod命令来修改。
比如,把regular_file的文件权限修改为所有用户都可读、可写、可执行,即rwxrwxrwx,也可表示为111111111,翻译成八进制,则为777。这样就可以通过两中方式修改这个权限。
Quote: |
$ chmod a+rwx regular_file 或 $ chmod 777 regular_file
|
说明:a指所用用户,如果只想给用户本身可读可写可执行权限,那么可以把a换成u;而+就是添加权限,相反的,如果想去掉某个权限,用-,而rwx则对应可读、可写、可执行。更多用法见chmod命令的帮助。
实际上除了这些权限外,还有两个涉及到安全方面的权限,即setuid/setgid和只读控制等。
如果设置了文件(程序或者命令)的setuid/setgid权限,那么用户将可以root身份去执行该文件,因此,这将可能带来安全隐患;如果设置了文件的只读权限,那么用户将仅仅对该文件将仅有可读权限,这为避免诸如rm -rf的“可恶”操作带来一定的庇佑。
setuid/setgid示例:通过普通用户执行passwd命令。默认情况下,系统是不允许普通用户执行它的。
Quote: |
$ ls -l /usr/bin/passwd -rwx--x--x 1 root root 36092 2007-06-19 14:59 /usr/bin/passwd $ su #切换到root用户,给程序或者命令添加“粘着位” $ chmod +s /usr/bin/passwd $ ls -l /usr/bin/passwd -rws--s--x 1 root root 36092 2007-06-19 14:59 /usr/bin/passwd $ exit $ passwd #普通用户通过执行该命令,修改自己的密码
|
说
明:“setuid
和setgid位是让普通用户可以以root用户的角色运行只有root帐号才能运行的程序或命令。”虽然这在一定程度上为管理提供了方便,比如上面的操
作让普通用户可以修改自己的帐号,而不是要root帐号去为每个用户做这些工作。关于setuid/setgid的更多详细解释,请参考资料[16]。
只读权限示例:给重要文件加锁(添加不可修改位[immutable])),以避免各种误操作带来的灾难性后果(例如: rm -rf)
Quote: |
$ chattr +i regular_file $ lsattr regular_file ----i-------- regular_file $ rm regular_file #加了immutable位以后,你无法对文件进行任何“破坏性”的活动啦 rm: remove write-protected regular file `regular_file'? y rm: cannot remove `regular_file': Operation not permitted $ chattr -i regular_file #如果想对它进行常规操作,那么可以把这个位去掉 $ rm regular_file
|
说明:chattr可以用设置文件的特殊权限,更多用法请参考chattr的帮助。
1.4 文件大小
普
通文件是文件内容的大小,而目录作为一个特殊的文件,它存放的内容是以目录结构体组织的各类文件信息,所以目录的大小一般都是固定的,它存放的文件个数自
然也就有上限,即少于它的大小除以文件名的长度。设备文件的文件大小则对应设备的主、次设备号,而有名管道文件因为特殊的读写性质,所以大小常是0。硬链
接(目录文件不能创建硬链接)实质上是原文件的一个完整的拷比,因此,它的大小就是原文件的大小。而软链接只是一个inode,存放了一个指向原文件的指
针,因此它的大小仅仅是原文件名的字节数。下面我们通过演示增加记忆。
原文件,链接文件文件大小的示例:
Quote: |
$ echo -n "abcde" > regular_file #往regular_file写入5字节 $ ls -l regular_file* -rw-r--r-- 2 root root 5 2007-12-08 15:28 regular_file -rw-r--r-- 2 root root 5 2007-12-08 15:28 regular_file_hard_file lrwxrwxrwx 1 root root 12 2007-12-07 20:15 regular_file_soft_link -> regular_file lrwxrwxrwx 1 root root 22 2007-12-08 15:21 regular_file_soft_link_link -> regular_file_soft_link $ i="regular_file" $ j="regular_file_soft_link" $ echo ${#i} ${#j} #可以参考,软链接存放的刚好是它们指向的原文件的文件名的字节数 12 22
|
设备号对应的文件大小:主、次设备号
Quote: |
$ ls -l hda1_block_dev_file brw-r--r-- 1 root root 3, 1 2007-12-07 21:44 hda1_block_dev_file $ ls -l null_char_dev_file crw-r--r-- 1 root root 1, 3 2007-12-07 21:43 null_char_dev_file
|
补
充:主(major)、次(minor)设备号的作用有不同。当一个设备文件被打开时,内核会根据主设备号(major
number)去查找在内核中已经以主设备号注册的驱动(可以cat
/proc/devices查看已经注册的驱动号和主设备号的对应情况),而次设备号(minor
number)则是通过内核传递给了驱动本身(参考“The Linux
Primer第十章”)。因此,对于内核而言,通过主设备号就可以找到对应的驱动去识别某个设备,而对于驱动而言,为了能够更复杂地访问设备,比如访问设
备的不同部分(如硬件通过分区分成不同部分,而出现hda1,hda2,hda3等),比如产生不同要求的随机数(如/dev/random和
/dev/urandom等)。
目录文件的大小,为什么是这样呢?看看下面的目录结构体的大小,目录文件的Block中存放的该目录下所有文件名的入口(文件名)。
Quote: |
$ ls -ld directory_file/ drwxr-xr-x 2 root root 4096 2007-12-07 23:14 directory_file/
|
目录的结构体如下:
Code:
[Ctrl+A Select All]
1.5 文件访问、更新、修改时间
文件的时间属性可以记录用户对文件的操作信息,在系统管理、判断文件版本信息等情况下将为管理员提供参考。因此,在阅读文件时,建议用cat等阅读工具,不要用编辑工具vim去阅读,因为即使你没有做任何修改操作,一旦你执行了保存命令,你将修改文件的时间戳信息。
1.6 文件名
文件名并没有存放在文件结构体里,而是存放在它所在的目录结构体中。所以,在目录的同一级别中,文件名必须是唯一的。
2、文件的基本操作
对于文件,常见的操作包括创建、删除、修改、读、写等。
[关于各种操作对应的“背后动作”我们将在下一节(shell编程范例之文件系统操作)详细分析]
2.1 创建以上各类文件
socket文件是一类特殊的文件,可以通过C语言创建,这里不做介绍(暂时不知道是否可以用命令直接创建),其他文件我们将通过命令创建。
Quote: |
$ touch regular_file #创建普通文件 $ mkdir directory_file #创建目录文件,目录文件里头可以包含更多文件 $ ln regular_file regular_file_hard_link #硬链接,是原文件的一个完整拷比 $ ln -s regular_file regular_file_soft_link #类似一个文件指针,指向原文件 $ mkfifo fifo_pipe #或者通过 "mknod fifo_pipe p" 来创建,FIFO满足先进先出的特点 $ mknod hda1_block_dev_file b 3 1 #块设备 $ mknod null_char_dev_file c 1 3 #字符设备
|
创
建一个文件实际上是在文件系统中添加了一个节点(inode),该节点信息将保存到文件系统的节点表中。更形象地说,就是在一颗树上长了一颗新的叶子(文
件)或者枝条(目录文件,上面还可以长叶子的那种),这些可以通过tree命令或者ls命令形象地呈现出来。文件系统从日常使用的角度,完全可以当成一颗
倒立的树来看,因为它们太像了,太容易记忆啦(关于文件系统中一些常见的节点请参考资料[17])。
Quote: |
$ tree 当前目录 或者 $ ls 当前目录
|
2.2 删除
删
除文件最直接的印象是这个文件再也不存在啦,这同样可以通过ls或者tree命令呈现出来,就像树木被砍掉一个分支或者摘掉一片叶子一样。实际上,这些文
件删除之后,并不是立即消失了,而是仅仅做了删除标记,因此,如果删除之后,没有相关的磁盘写操作把相应的磁盘空间“覆盖”,那么原理上是可以恢复的(虽
然如此,但是这样的工作往往是很麻烦的,所以在删除一些重要数据时,请务必三思而后行,比如做好备份工作),相应的做法可以参考资料[18]到[21]。
具体删除文件的命令有rm,如果要删除空目录,可以用rmdir命令。例如:
Quote: |
$ rm regular_file $ rmdir directory_file $ rm -r directory_file_not_empty
|
rm
有两个非常重要的参数,一个是-f,这个命令是非常“野蛮的”(关于rm -rf的那些钟爱者轻强烈建议您阅读<unix
haters>的相应章节),它估计给很多linux
user带来了痛苦,另外一个是-i,这个命令是非常“温柔的”,它估计让很多用户感觉烦躁不已过。用哪个还是根据您的“心情”吧,如果做好了充分的备份
工作,或者采取了一些有效避免灾难性后果的动作的话,您在做这些工作的时候就可以放心一些啦。
2.3 复制
文件的复制通常是指文件内容的“临时”复制。通过这一节开头的介绍,我们应该了解到,文件的硬链接和软链接在某种意义上说也是“文件的复制”,前者同步复制文件内容,后者在读写的情况下同步“复制”文件内容。例如:
Quote: |
// 用cp命令常规地复制文件(复制目录需要-r选项) $ cp regular_file regular_file_copy $ cp -r diretory_file directory_file_copy // 创建硬链接(link和copy不同之处是后者是同步更新,前者则不然,复制之后两者不再相关) $ ln regular_file regular_file_hard_link // 创建软链接 $ ln -s regular_file regluar_file_soft_link
|
2.4 修改文件名
修改文件名实际上仅仅修改了文件名标识符。我们可以通过mv命令来实现修改文件名操作(即重命名)。
Quote: |
$ mv regular_file regular_file_new_name
|
2.5 编辑文件
编
辑文件实际上是操作文件的内容,对应普通文本文件的编辑,这里主要涉及到文件内容的读、写、追加、删除等。这些工作通常会通过专门的编辑器来做,这类编辑
器有命令行下的vim、emacs和图形界面下的gedit,kedit等。如果是一些特定的文件,会有专门的编辑和处理工具,比如图像处理软件
gimp,文档编辑软件OpenOffice等。这些工具一般都会有专门的教程。关于vim的用法,建议阅读这篇帖子:VIM高级命令集锦
下面主要简单介绍Linux下通过重定向来实现文件的这些常规的编辑操作(更多有意思的用法见参考资料[6])。
Quote: |
// 创建一个文件并写入abcde $ echo "abcde" > new_regular_file // 再往上面的文件中追加一行abcde $ echo "abcde" >> new_regular_file // 按行读一个文件 $ while read LINE; do echo $LINE; done < test.sh
|
提示:如果要把包含重定向的字符串变量当作命令来执行,请使用eval命令,否则无法解释重定向。例如,
Quote: |
$ redirect="echo \"abcde\" >test_redirect_file" $ $redirect #这里会把>当作字符 > 打印出来,而不会当作 重定向 解释 "abcde" >test_redirect_file $ eval $redirect #这样才会把 > 解释成 重定向 $ cat test_redirect_file abcde
|
2.6 压缩/解压缩文件
压缩和解压缩文件在一定意义上来说是为了方便文件内容的传输,不过也可能有一些特定的用途,比如内核和文件系统的映像文件等(更多相关的知识请参考资料[22])。
这里仅介绍几种常见的压缩和解压缩方法:
Quote: |
// tar, 这在某种意义上来不是文件的压缩,仅仅是“把一堆东西放在一起,当成一个文件来看”。 $ tar -cf file.tar file #压缩 $ tar -xf file.tar #解压 // gz $ gzip -9 file $ gunzip file // tar.gz $ tar -zcf file.tar.gz file $ tar -zxf file.tar.gz // bz2 $ bzip2 file $ bunzip2 file // tar.bz2 $ tar -jcf file.tar.bz2 file $ tar -jxf file.tar.bz2
|
通过上面的演示,我们应该已经非常清楚tar命令,bzip2/bunzip2,gzip/gunzip命令的角色了吧?如果还不清楚,多操作和比较一些上面的命令,并查看它们的手册:man tar...
2.7 文件搜索(文件定位)
文
件搜索是指在某个目录层次中找出具有某些属性的文件在文件系统中的位置,这个位置如果扩展到整个网络,那么可以表示为一个URL地址,对于本地的地址,可
以表示为file://+本地路径。本地路径在Linux系统下是以/开头,例如,每个用户的家目录可以表示为:file:///home/。下面仅仅介
绍本地文件搜索的一些办法。
find命令提供了一种“及时的”搜索办法,它根据用户的请求,在指定的目录层次中遍历所有文件直到找到需要
的文件为止。而updatedb+locate提供了一种“快速的”的搜索策略,updatedb更新并产生一个本地文件数据库,而locate通过文件
名检索这个数据库以便快速找到相应的文件。前者支持通过各种文件属性进行搜索,并且提供了一个接口(-exec选项)用于处理搜索后的文件。因此为“单条
命令”脚本的爱好者提供了极大的方便,不过对于根据文件名的搜索而言,updatedb+locate的方式在搜索效率上会有明显提高。下面简单介绍这两
种方法:
Quote: |
// find命令基本使用演示 $ find ./ -name "*.c" -o -name "*.h" #找出所有的C语言文件,-o是或者 $ find ./ \( -name "*.c" -o -name "*.h" \) -exec mv '{}' ./c_files/ \; #把找到的文件移到c_files下,这种用法非常有趣,不过她可以用xargs命令替代,例如 $ find ./ -name "*.c" -o -name "*.h" | xargs -i mv '{}' ./c_files/ #如果要对文件做更复杂的操作,可以考虑把mv改写为你自己的处理命令,例如,我需要修改所有的文件名后缀为大写。 $ find ./ -name "*.c" -o -name "*.h" | xargs -i ./toupper.sh '{}' ./c_files/ #toupper.sh就是我们需要实现的转换小写为大写的一个处理文件,具体实现如下: $ cat toupper.sh #!/bin/bash
# the {} will be expended to the current line and becomen the first argument of this script FROM=$1 BASENAME=${FROM##*/}
BASE=${BASENAME%.*} SUFFIX=${BASENAME##*.}
TOSUFFIX="$(echo $SUFFIX | tr '[a-z]' '[A-Z]')" TO=$2/$BASE.$TOSUFFIX COM="mv $FROM $TO" echo $COM eval $COM // updatedb+locate基本使用演示 $ updatedb #更新库 $ locate find*.gz #查找包含find字符串的所有gz压缩包
|
实际上,除了上面两种命令外,Linux下还有命令查找工具:which和whereis,前者用于返回某个命令的全路径,而后者用于返回某个命令、源文件、man文件的路径。例如,我们需要查找find命令的绝对路径:
Quote: |
$ which find /usr/bin/find $ whereis find find:
/usr/bin/find /usr/X11R6/bin/find /usr/bin/X11/find /usr/X11/bin/find
/usr/man/man1/find.1.gz /usr/share/man/man1/find.1.gz
/usr/X11/man/man1/find.1.gz
|
需
要提到的是,如果想根据文件的内容搜索文件,那么find和updatedb+locate以及which,whereis都无能为例啦,可选的方法是
grep,sed等命令,前者在加上-r参数以后可以在指定目录下文件中搜索指定的文件内容,后者再使用-i参数后,可以对文件内容进行替换。它们的基本
用法在前面的章节中我们已经详细介绍了,所以这里就不叙述。
值得强调的是,这些命令对文件的操作是非常有意义的。它们在某个程度上把文件
系统结构给抽象了,使得对整个文件系统的操作简化为对单个文件的操作,而单个文件如果仅仅考虑文本部分,那么最终却转化成了我们之前的字符串操作,即我们
上一节讨论过的内容。为了更清楚的了解文件的组织结构,文件之间的关系,在下一节我们将深入探讨文件系统。
参考资料
[1] Linux 系统的基本组成和文件系统结构
http://forum.ubuntu.org.cn/weblog_entry.php?e=332&sid=3ceee92718a77d5eef867497470ecc7b
[2] 从文件 I/O 看 Linux 的虚拟文件系统
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
[3] Linux 文件系统剖析
http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/index.html?ca=drs-cn
[4] 《Linux 核心》第九章 文件系统
http://man.chinaunix.net/tech/lyceum/linuxK/fs/filesystem.html
[5] Linux Device Drivers, 3rd Edition
http://linuxdriver.co.il/ldd3
[6] 技巧:Linux I/O重定向的一些小技巧
http://www.ibm.com/developerworks/cn/linux/l-iotips/index.html
[7] Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析
http://www.ibm.com/developerworks/cn/linux/l-elf/part1/index.html
http://www.ibm.com/developerworks/cn/linux/l-elf/part2/index.html
[8] Shell脚本调试技术
http://www.ibm.com/developerworks/cn/linux/l-cn-shell-debug/index.html
[9] ELF文件格式及程序加载执行过程总汇
http://linuxsir.org/bbs/showthread.php?p=1196195
[10] Linux下C语言编程——文件的操作
http://fanqiang.chinaunix.net/a4/b2/20010508/113315.html
[11] 创建设备文件简单例程
http://www.erya.cn/archives/200604/38.html
[12] Linux Device Driver, 3rd Version
http://linuxdriver.co.il/ldd3/
[13] "Linux下C语言编程" 的 文件操作 部分
http://www.mwjx.com/aboutfish/private/book/linux_c.txt
[14] Linux的inode是什么?
http://bbs.51cto.com/archiver/next-415377.html
[15] Linux 用户管理工具介绍
http://fedora.linuxsir.org/main/?q=node/106
[16] Linux 文件和目录的属性
http://www.linuxsir.org/main/?q=node/196#5.1
[17] Filesystem Hierarchy Standard
http://www.pathname.com/fhs/pub/fhs-2.3.html#INTRODUCTION
[18] 学会恢复 Linux系统里被删除的 Ext3文件
http://tech.ccidnet.com/art/237/20070720/1150559_1.html
[19] reiserfs文件系统反删除(Undelete)操作的实践
http://www.linuxsir.org/main/?q=node/118#2
[20] 使用mc恢复被删除文件
http://bbs.tech.ccidnet.com/read.php?tid=48372
[21] Linux ext3 删除文件原理
http://www.pet100.cn/blog/html/57/1057-1235.html
[22] Linux压缩/解压缩方式大全
http://www.chinaitlab.com/www/techspecial/tar/
[23] Everything is a byte
http://www.reteam.org/papers/e56.pdf
后记:
[1]
考虑到文件和文件系统的重要性,我们将把它分成三个小节来介绍:文件、文件系统、程序与进程。在文件这一部分,我们主要介绍文件的基本属性和常规操作,在
文件系统那部分,将深入探讨Linux 文件系统的各个部分(包括Linux
文件系统的结构、具体某个文件系统的大体结构分析、底层驱动的工作原理),在程序与进程一节将专门讨论可执行文件的相关内容(包括不同的程序类型、加载执
行过程、不同进程之间的交互[命令管道和无名管道、信号通信]、对进程的控制等)。
[2] 有必要讨论清楚 目录大小 的含义,另外,最好把一些常规的文件操作全部考虑到,包括文件的读、写、执行、删除、修改、复制、压缩/解压缩等。
[3] 下午刚从上海回来,比赛结果很“糟糕”,不过到现在已经不重要了,关键是通过决赛发现了很多不足,发现了设计在系统开发中的关键角色,并且发现了上海是个美丽的城市,上交也是个美丽的大学。回来就开始整理这个因为比赛落下了两周的blog。
[4] 12月15日,添加文件搜索部分内容。
关于文件系统、文件相关的章节,建议参考《Unix环境高级编程》第3-6章。
补充一个有趣的例子,来自《unix环境高级编程》P75
Quote: |
//创建一个符号链接,虽然前面的文件不存在,但是符号链接还是创建了 $ ln -s /path/to/a/file/notexist notexist //不过,试图访问这个符号链接时却试图访问前面那个链接,所以找不到那个文件,但提示的时候却提示符号链接名指定的文件不存在,所以要千万注意这个有趣的情况 $ cat notexist cat: notexist: No such file or directory
|
另外一个例子是,即使之前那个符号链接指向一个正确的文件(这里以目录为例),但是那个文件可能会被删除。
Quote: |
$ mkdir -p /tmp/path/to/a/file/notexist $ cat notexist1/ cat: notexist1/: Is a directory $ rmdir /tmp/path/to/a/file/notexist $ cat notexist1 cat: notexist1: No such file or directory
|
所以,在试图访问一个符号链接的时候,如果提示:"文件名: 没有这个文件或目录"的话,可能是该符号链接指向的文件或者目录已经被删除了,或者在创建该符号链接的时候它们就根本不存在。
在上面的操作中,我们发现符号链接文件的长度刚好是该符号链接指向的文件或者目录名字字符串的长度,原因是符号链接文件存放的内容刚好是它们指向的文件或目录名。
符号链接还会有一个有趣的问题:虽然我们可以创建一个目录的符号链接,但是当你试图从该符号链接返回到原有目录的上级时是不可能的(cd
..),因为它的父目录是符号链接文件本身所在的父目录,但是通过getcwd函数获取的却是该符号链接指向的目录的全路径,而并不是它实际所在目录加上
它本身。因此,如果不注意这个细节,容易在编写shell脚本时出问题。
由于内核一般都设有内核缓冲区,所以在写一个文件时可能并没有立即写入磁盘等存储设备,需要特别注意这个问题。不过可以使用sync命令调用sync函数来强制理解写入磁盘,不过这个貌似也会有30s左右的延迟,因为它通过一个sync精灵进程来检查。
--摘自《unix环境高级编程》P89
演示,
做嵌入式开发时,把文件系统烧写到开发板以后最后是执行一下sync,确保确实写入了。
Quote: |
$ lilo -C /path/to/lilo.conf $ sync
|
描述: 图[1] Filesystem Structure
图片:
描述: 图[2] MBR Structure
图片:
by falcon<zhangjinw@gmail.com>
2007-12-20
前言
准备了很久,找了好多天的资料,还不知道应该如何开始动笔写:因为担心“拿捏”不住,所以一方面继续查找资料,一方面思考如何来写。作为“shell编程
范例”序列的一部分,希望它能够很好地帮助shell程序员理解如何用shell命令来完成和Linux系统关系非常之大的文件系统的各种操作,希望让
Shell程序员中对文件系统"混沌"的状态从此消失,希望文件系统以一种更为清晰的样子呈现在我们的眼前。
正文
-1 文件系统在Linux操作系统中的位置
如何来认识文件系统呢?从shell程序员的角度来看,文件系统就是一个用来组织各种文件的方法。但是文件系统无法独立于硬件存储设备和操作系统而独立存
在,因此我们还是有必要来弄清楚硬件存储设备、分区、操作系统、逻辑卷、文件系统等各种概念之间的联系,以便理解我们对文件系统的常规操作的一些“细
节”。这个联系或许(也许会有一些问题)可以通过这样一种方式来呈现,如附录图[1]。
从该图中,我们可以清晰地看到各个“概念”之间的关系,它们以不同层次分布,覆盖硬件设备、系统内核空间、系统用户空间。在用户空间,用户可以不管内核是
如何操作具体硬件设备的,仅仅使用程序员设计的各种界面就可以拉,而普通程序员也仅仅需要利用内核提供的各种接口(system
call)或者一些C库来和内核进行交互,而无须关心具体的实现细节。不过对于操作系统开发人员,他们需要在内核空间设计特定的数据结构来管理和组织底层
的硬件设备。
下面我们从下到上的方式(即从底层硬件开始),用工具来分析和理解图中几个重要的概念。(如果有兴趣,可以先看看下面的几则资料)
参考资料:
[1] Linux 系统的基本组成和文件系统结构
http://forum.ubuntu.org.cn/weblog_entry.php?e=332&sid=3ceee92718a77d5eef867497470ecc7b
[2] 从文件 I/O 看 Linux 的虚拟文件系统
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
[3] Linux 文件系统剖析 http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/index.html?ca=drs-cn
[4] 第九章 文件系统
http://man.chinaunix.net/tech/lyceum/linuxK/fs/filesystem.html
[5] Linux逻辑盘卷管理LVM详解
http://unix-cd.com/vc/www/28/2007-06/1178.html
0 硬件管理和设备驱动
Linux系统通过不同的设备驱动模块管理不同的硬件设备。如果添加了新的硬件设备,那么需要编写相应的硬件驱动模块来管理它。对于一些常见的硬件设备,
系统已经自带了相应的驱动,编译内核时,选中它们,可以把它们编译成内核的一部分,也可以以模块的方式编译。如果以模块的方式编译,那么可以在系统的
/lib/modules/`uname -r`目录下找到对应的模块文件。
比如,可以这样找到相应的scsi驱动和usb驱动的模块:
Quote: |
// 更新系统中文件索引数据库(有点慢,不耐烦就按下CTRL+C取消掉) $ updatedb // 查找scsi相关的驱动 $ locate scsi*.ko // 查找usb相关的驱动 $ locate usb*.ko
|
这些驱动的名字以.ko为后缀,在安装系统时默认编译为了模块。实际上可以把它们编译为内核的一部分,仅仅需要在编译内核时选择为
即可(更多内核模块编译的资料请参考资料[5])。但是,很多情况下会以模块的方式编译它们,这样可以减少内核的大小,并根据需要灵活地加载和卸载它们。下面简单地演示如何查看已加载模块的状态,卸载模块,加载模块。
可通过查看/proc文件系统的modules文件检查内核中已加载的各个模块的状态,也可以通过lsmod命令直接查看它们。
Quote: |
$ cat /proc/modules 或者 $ lsmod // 例如,查看scsi和usb相关驱动模块如下,结果各列为模块名、模块大小、被其他模块的引用情况(引用次数、引用它们的模块) $ lsmod | egrep "scsi|usb" usbhid 29536 0 hid 28928 1 usbhid usbcore 138632 4 usbhid,ehci_hcd,ohci_hcd scsi_mod 147084 4 sg,sr_mod,sd_mod,libata
|
下面卸载usbhid模块看看(呵呵,小心卸载scsi的驱动哦!因为你的系统就跑在上面,如果确实想玩玩,卸载前记得保存数据),通过rmmod命令就可以实现。
Quote: |
// 先切换到root用户 $ rmmod usbhid // 再查看该模块的信息,已经看不到了吧 $ lsmod | grep ^usbhid
|
如果你有个usb鼠标,那么移动一下,是不是发现动不了啦?因为设备驱动都没有了,设备自然就没法用罗。不过不要紧张,既然知道是什么原因,那么把设备驱动重新加载上就可以啦,下面用insmod把usbhid模块重新加载上。
Quote: |
// 也要root用户的 $ insmod `locate usbhid.ko` // `locate usbhid.ko`是为了找出usbhid.ko模块的路径,如果你之前没有updatedb,估计用它是找不到了,不过你可以直接到/lib/modules目录下把usbhid.ko文件找到。
|
okay,现在鼠标又可以用啦,不信再动一下鼠标 :-)
到这里,硬件设备和设备驱动之间关系应该是比较清楚了吧。如果没有,那么继续下面的内容。
在Linux下,设备驱动关联着相应的设备文件,而设备文件则和硬件设备一一对应(更多细节请参考资料[8][9][10])。这些设备文件都统一存放在系统的/dev/目录下。
例如,scsi设备对应的/dev/sda,/dev/sda1,/dev/sda2...下面查看这些设备文件的信息。
Quote: |
$ ls -l /dev/sda* brw-rw---- 1 root disk 8, 0 2007-12-28 22:49 /dev/sda brw-rw---- 1 root disk 8, 1 2007-12-28 22:50 /dev/sda1 brw-rw---- 1 root disk 8, 3 2007-12-28 22:49 /dev/sda3 brw-rw---- 1 root disk 8, 4 2007-12-28 22:49 /dev/sda4 brw-rw---- 1 root disk 8, 5 2007-12-28 22:50 /dev/sda5 brw-rw---- 1 root disk 8, 6 2007-12-28 22:50 /dev/sda6 brw-rw---- 1 root disk 8, 7 2007-12-28 22:50 /dev/sda7 brw-rw---- 1 root disk 8, 8 2007-12-28 22:50 /dev/sda8
|
可以看到第一列第一个字符都是b,第五列都是数字8。b表示该文件是一个块设备文件,对应地,如果是c则表示字符设备(例如/dev/ttyS0,关于块设备和字符设备的区别,可以看这里[摘自网络])。
Quote: |
字符设备:字符设备就是能够像字节流一样访问的设备,字符终端和串口就属于字符设备。
块设备:块设备上可以容纳文件系统。与字符设备不同,在读写操作时,块设备每次只能传输一个或多个完整的块。在Linux操作系统中,应用程序可以像访问
字符设备一样读写块设备(一次读取或写入任意的字节数据)。因此,块设备和字符设备的区别仅仅是在内核中对于数据的管理不同。
|
数字8则是该硬件设备在内核中对应的设备编号,可以在内核的Documentation/devices.txt文件中找到设备号分配情况。但是为什么同
一个设备会对应不同的设备文件(/dev/sda后面为什么还有不同的数字,而且ls结果中的第6列貌似和它们对应起来的)。这实际上是为了区分不同设备
的不同部分。对于硬盘,这样可以处理硬盘内部的不同分区。就内核而言,它仅仅需要通过第5列的设备号就可以找到对应的硬件设备,但是对于驱动模块来说,它
还需要知道如何处理不同的分区,于是就多了一个辅设备号,即第6列对应的内容。这样一个设备就有了主设备号(第5列)和辅设备号(第6列),从而方便的实
现对各种硬件设备的管理。
因为设备文件和硬件是对应的,这样我们可以直接从/dev/sda(如果是IDE的硬盘,那么对应的设备就是/dev/hda啦)设备中读出硬盘的信息,例如:
Quote: |
// 用dd命令复制出硬盘的前512个字节,要root用户哦 $ dd if=/dev/sda of=mbr.bin bs=512 count=1 // 用file命令查看相应的信息 $ file mbr.bin mbr.bin:
x86 boot sector, LInux i386 boot LOader; partition 3: ID=0x82,
starthead 254, startsector 19535040, 1959930 sectors; partition 4:
ID=0x5, starthead 254, startsector 21494970, 56661255 sectors, code
offset 0x48 // 也可以用od命令以16进制的形式读取并进行分析 $ od -x mbr.bin // bs是块的大小(以字节bytes为单位),count是块数
|
因为这些信息并不是很直观(而且下面我们会进一步深入的分析),那么我们来看看另外一个设备文件,将可以非常直观的演示设备文件和硬件的对应关系。还是以鼠标为例吧,下面来读取鼠标对应的设备文件的信息。
Quote: |
// 同样需要root用户 $ cat /dev/input/mouse1 | od -x // 你的鼠标驱动可能不太一样,所以设备文件可能是其他的,但是都会在/dev/input下
|
移动鼠标看看,是不是发现有不同信息输出。(基于这一原理,我们经常通过在一端读取设备文件/dev/ttyS0中的内容,而在另一端往设备文件/dev/ttyS0中写入内容来检查串口线是否被损坏。)
到这里,对设备驱动、设备文件和硬件设备之间的关联应该是印象更深刻了。如果想深入了解设备驱动的工作原理和设备驱动模块的编写,那么看看参考资料[10],开始你的设备驱动模块的编写历程吧。
参考资料:
[5] Compile linux kernel 2.6
http://www.cyberciti.biz/tips/compiling-linux-kernel-26.html
[6] Linux系统的硬件驱动程序编写原理
http://www.blue1000.com/bkhtml/2001-02/2409.htm
[7] Linux下USB设备的原理、配置、 常见问题
http://soft.zdnet.com.cn/software_zone/2007/1108/617545.shtml
[8] Linux 核心--9.设备驱动
http://www.bitscn.com/linux/driver/200604/6788.html
[9] The Linux Kernel Module Programming Guide
http://www.dirac.org/linux/writing/lkmpg/2.6/lkmpg-2.6.0.html
[10] Linux设备驱动开发
http://linuxdriver.co.il/ldd3/
1 理解、查看磁盘分区
实际上内存、u盘等都可以作为文件系统底层的“存储”设备,但是这里我们仅用硬盘作为实例来介绍磁盘和分区的关系。
目前Linux的分区依然采用第一台PC硬盘所使用的分区原理(见该部分的参考资料[1]),下面逐步分析和演示这一分区原理。
先来看看几个概念:
A. 设备管理和分区
在Linux下,每一个存储设备对应一个系统的设备文件,对于硬盘等IDE和SCSI设备,在系统的/dev目录下可以找到对应的包含字符hd和sd的设
备文件。而根据硬盘连接的主板设备接口和数据线接口的不同,在hd或者sd字符后面可以添加一个从a到z的字符,例如hda,hdb,hdc和sda,
sdb,sdc等,另外为了区别同一个硬件设备的不同分区,在后面还可以添加了一个数字,例如hda1,hda2,hda3...和sda1,sda2,
sda3,所以你在/dev目录下,可以看到很多类似的设备文件。
B. 各分区的作用
在分区的时候常遇到主分区和逻辑分区的问题,这实际上是为了方便扩展分区,正如后面的逻辑卷的引入是为了更好地管理多个硬盘一样,引入主分区和逻辑分区可以方便地进行分区的管理。
在Linux系统中,每一个硬盘设备最多由4个主分区(包括扩展分区)构成。
主分区的作用是计算机用来进行启动操作系统的,因此每一个操作系统的启动程序或者称作是引导程序,都应该存放在主分区上。Linux规定主分区(或者扩展
分区)占用分区编号中的前4个。所以你会看到主分区对应的设备文件为/dev/hda1-4或者/dev/sda1-4,而不会是hda5或者sda5。
扩展分区则是为了扩展更多的逻辑分区的,在Linux下,逻辑分区占用了hda5-16或者sda5-16等12个编号。
C. 分区类型
它规定了这个分区上的文件系统的类型。Linux支持诸如msdoc,vfat,ext2,ext3等诸多的文件系统类型,更多信息在下一小节进行进一步的介绍。
下面通过分析硬盘的前512个字节(即MBR)来分析和理解分区。
先来看看这张表(见附图2),它用来描述MBR的结构。MBR包括引导部分、分区表、以及结束标记(55AAH),分别占用了512字节中446字节、
64字节和2字节。这里仅仅关注分区表部分,即中间的64字节以及图中左边的部分。(如果你对引导部分感兴趣,请参考资料[10][11][12])
由于我用的是SCSI的硬盘,下面从/dev/sda设备中把硬盘的前512个字节拷贝到文件mbr.bin中。
Quote: |
// 先切换到root用户 $ dd if=/dev/sda of=mbr.bin bs=512 count=1
|
下面用file,od,fdisk等命令来分析这段MBR的数据,并对照附图[2]以便加深理解。
Quote: |
$ file mbr.bin mbr.bin:
x86 boot sector, LInux i386 boot LOader; partition 3: ID=0x82,
starthead 254, startsector 19535040, 1959930 sectors; partition 4:
ID=0x5, starthead 254, startsector 21494970, 56661255 sectors, code
offset 0x48 $ od -x mbr.bin | tail -6 #仅关注中间的64字节,所以截取了结果中后6行 0000660 0000 0000 0000 0000 a666 a666 0000 0180 0000700 0001 fe83 ffff 003f 0000 1481 012a 0000 0000720 0000 0000 0000 0000 0000 0000 0000 fe00 0000740 ffff fe82 ffff 14c0 012a e7fa 001d fe00 0000760 ffff fe05 ffff fcba 0147 9507 0360 aa55 // 先切换到root用户 $ fdisk -l | grep ^/ #仅分析MBR相关的部分,不分析逻辑分区部分 /dev/sda1 * 1 1216 9767488+ 83 Linux /dev/sda3 1217 1338 979965 82 Linux swap / Solaris /dev/sda4 1339 4865 28330627+ 5 Extended
|
file命令的结果显示,刚拷比的512字节是启动扇区,用分号分开的几个部分分别是boot
loader,分区3和分区4。分区3的类型是82,即swap分区(可以通过fdisk命令的l命令列出相关信息),它对应fdisk的结果中
/dev/sda3所在行的第5列,分区3的扇区数是1959930,转换成字节数是1959930*512(目前,硬盘的默认扇区大小是512字节),
而swap分区的默认块大小是1024字节,这样块数就是:
Quote: |
$ echo 1959930*512/1024 | bc 979965
|
正好是fdisk结果中/dev/sda3所在行的第四列对应的块数,同样地,可以对照fdisk和file的结果分析分区4。
再来看看od命令以十六进制显示的结果,同样考虑分区3,计算一下发现,分区3对应的od命令的结果为:
Quote: |
fe00 ffff fe82 ffff 14c0 012a e7fa 001d
|
首先是分区标记,00H,从图[2]中,看出它就不是引导分区(80H标记的才是引导分区),而分区类型呢?为82H,和file显示结果一致,现在再来关注一下分区大小,即file结果中的扇区数。
Quote: |
$ echo "ibase=10;obase=16;1959930" | bc 1DE7FA
|
刚好对应e7fa 001d,同样地考虑引导分区的结果:
Quote: |
0180 0001 fe83 ffff 003f 0000 1481 012a
|
分区标记:80H,正好反应了这个分区是引导分区,随后是引导分区所在的磁盘扇区情况,010100,即1面0道1扇区。其他内容可以对照分析。
考虑到时间关系,更多细节请参考下面的资料或者查看看系统的相关手册。
补充:安装系统时,可以用fdisk,cfdisk等命令进行分区。如果要想从某个分区启动,那么需要打上80H标记,例如可通过cfdisk把某个分区设置为bootable来实现。
参考资料:
[1] 解析磁盘、分区、文件系统
http://www.linuxpk.com/37190.html
[2] 硬盘分区表详解
http://www.linuxpk.com/5378.html
[3] 深入理解Linux的硬盘分区
http://www.linuxpk.com/39733.html
[4] 什么是硬件分区表
http://www.pc-web.cn/pc/basic/465.asp
[5] Linux指导第6部分 使用分区和文件系统
http://www.pass100.net/jisuanji/linux/zhidao/80974.html
[10] 硬盘MBR全面分析
http://www.pc120.net.cn/home/datcb/05101223070444577.htm
[11] Inside the linux boot process
http://www-128.ibm.com/developerworks/linux/library/l-linuxboot/
[12] Develop your own OS: booting
http://docs.huihoo.com/gnu_linux/own_os/booting.htm
[13] Redhat 9磁盘分区简介
http://www.topstudy.com/info/default.aspx?guid=eeac2894-3588-4b1a-9607-1ad377caa03f
[14] Linux partition HOWTO
http://mirror.lzu.edu.cn/tldp/HOWTO/Partition/
2 分区和文件系统的关系
在没有引入逻辑卷之前,分区类型和文件系统类型几乎可以同等对待,设置分区类型的过程就是格式化分区,建立相应的文件系统类型的过程。
下面主要介绍如何建立分区和文件系统类型的联系,即如何格式化分区为指定的文件系统类型。
先来看看Linux下文件系统的常见类型(如果要查看所有Linux支持的文件类型,可以用fdisk命令的l命令查看,或者通过man fs查看,也可通过/proc/filesystems查看到当前内核支持的文件系统类型)
ext2,ext3:这两个是Linux根文件系统通常采用的类型
swap:这个是具体实现Linux虚拟内存时采用的一种文件系统,安装时一般需要建立一个专门的分区,并格式化为swap文件系统(如果想添加更多的
swap分区,那么可以参考本节的资料[1],熟悉dd,mkswap,swapon,swapoff等命令的用法)
proc:这是一种比较特别的文件系统,作为内核和用户之间的一个接口存在,建立在内存中(你可以通过cat命令查看/proc系统下的文件,甚至可以通
过修改/proc/sys下的文件实时调整内核的配置,当前前提是你需要把proc文件系统挂载上[mount -t proc proc
/proc])
除了这三个最常见的文件系统类型外,Linux支持包括vfat,iso,xfs,nfs在内各种常见的文件系统类型,在linux下,你可以自由地查看和操作windows等其他操作系统使用的文件系统。
那么如何建立磁盘和这些文件系统类型的关联呢?格式化。
格式化的过程实际上就是重新组织分区的过程,可通过mkfs命令来实现,当然也可以通过fdisk等命令来实现。这里仅介绍mkfs,mkfs可用来对一
个已有的分区进行格式化,不能实现分区操作(如果要对一个磁盘进行分区和格式化,那么可以用fdisk就可以啦)。格式化后,相应的分区上的数据就通过某
种特别的文件系统类型进行组织了。
例如:把/dev/sda9分区格式化为ext3的文件系统。
Quote: |
// 先切换到root用户 $ mkfs -t ext3 /dev/sda9
|
如果要列出各个分区的文件系统类型,那么可以用fdisk -l命令。
更多信息请参考下列资料。
参考资料:
[1] Linux下加载swap分区的步骤
http://soft.zdnet.com.cn/software_zone/2007/1010/545261.shtml
[2] 光碟的标准
http://www.edisc.com.cn/bike/viewnews.btml?id=274
[3] Linux下ISO镜像文件的制作与刻录
http://www.examda.com/linux/fudao/20071212/113445321.html
[4] RAM磁盘分区解释
http://oldlinux.org/oldlinux/viewthread.php?tid=2677
http://www.ibm.com/developerworks/cn/linux/l-initrd.html
[5] 高级文件系统实现者指南
http://www-128.ibm.com/developerworks/search/searchResults.jsp?searchType=1&searchSite=dWChina&pageLang=zh&langEncoding=UTF8&searchScope=dW&query=%E9%AB%98%E7%BA%A7%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E5%AE%9E%E7%8E%B0%E8%80%85%E6%8C%87%E5%8D%97&Search.x=42&Search.y=9&Search=%E6%90%9C%E7%B4%A2
(有必要突出解释swap,RAM文件系统的工作原理等)
3 分区、逻辑卷和文件系统的关系
在上一节中,我们直接把分区格式化为某种文件系统类型,但是考虑到扩展新的存储设备的需要,开发人员在文件系统和分区之间引入了逻辑卷。考虑到时间关系,这里不再详述,请参考资料[1]。
参考资料:
[1] Linux逻辑卷管理详解
http://unix-cd.com/vc/www/28/2007-06/1178.html
[2] 见1.2的最后一个参考资料的最后一节
4 文件系统的可视化结构
文件系统最终呈现出来的是一种可视化的结构,我们可用ls,find,tree等命令把它呈现出来。它就像一颗倒挂的“树”,在树的节点上还可以挂载新的“树”。(如果想把目录结构以图表的方式呈现出来,那么可以使用我之前写的一个脚本,即参考资料[3])。
下面简单介绍文件系统的挂载。
一个文件系统可以通过一个设备挂载(mount)到某个目录下(具体的实现请参考资料[2]和[1]),这个目录被称为挂载点。有趣的是,在Linux
下,一个目录本身还可以挂载到另外一个目录下,一个格式化了的文件也可以通过一个特殊的设备/dev/loop进行挂载(如iso文件)。另外,就文件系
统而言,Linux不仅支持本地文件系统,还支持远程文件系统(如nfs)。
下面简单介绍文件系统挂载的几个实例。
A. 根文件系统的挂载
Quote: |
// 挂载需要root权限,先切换到root用户 // 挂载系统根文件系统/dev/sda1到一个新的目录下 $ mount -t ext3 /dev/sda1 /mnt/ // 查看/dev/sda1的挂载情况,可以看到,一个设备可以多次挂载 $ mount | grep sda1 /dev/sda1 on / type ext3 (rw,errors=remount-ro) /dev/sda1 on /mnt type ext3 (rw) // 对于一个已经挂载的文件系统,为支持不同的属性可以重新挂载 $ mount -n -o remount, rw /
|
B. 挂载一个新的设备
如果内核已经支持了USB接口,那么在插入u盘的时候,我们可以通过dmesg命令查看它对应的设备号,并挂载它。
Quote: |
// 查看dmesg结果中的最后几行内容,找到类似/dev/sdN的信息,找出u盘对应的设备号 $ dmesg // 这里假设u盘是vfat格式的,以便在一些打印店里的windows上也可使用 $ mount -t vfat /dev/sdN /path/to/mountpoint_directory
|
C. 挂载一个iso文件或者是光盘
对于一些iso文件或者是iso格式的光盘,同样可以通过mount命令挂载。
Quote: |
// 对于iso文件 $ mount -t iso9660 /path/to/isofile /path/to/mountpoint_directory // 对于光盘 $ mount -t iso9660 /dev/cdrom /path/to/mountpoint_directory
|
D. 挂载一个远程文件系统
Quote: |
$ mount -t nfs remote_ip:/path/to/share_directory /path/to/local_directory [quote] E. 挂载一个proc文件系统 [quote] $ mount -t proc proc /proc
|
proc文件系统组织在内存中,但是你可以把它挂载到某个目录下。通常把它挂载在/proc目录下,以便一些系统管理和配置工具使用它。例如top命令用
它分析内存的使用情况(读取/proc/meminfo和/proc/stat等文件中的内容),lsmod命令通过它获取内核模块的状态(读取
/proc/modules),netstat命令通过它获取网络的状态(读取/proc/net/dev等文件),当然,你也可以编写自己的相关工具。
除此之外,通过调整/proc/sys目录下的文件,你可以动态的调整系统的配置,比如通过往
/proc/sys/net/ipv4/ip_forward文件中写入数字1就可以让内核支持数据包的转发。(更多信息请参考proc的帮助,man
proc)
F. 挂载一个目录
Quote: |
$ mount --bind /path/to/needtomount_directory /path/to/mountpoint_directory
|
这个非常有意思,比如你可以把某个目录挂载到ftp服务的根目录下,而无须把内容复制过去,就可以把相应目录中的资源提供给别人共享。
以上都只提到了挂载,那怎么卸载呢?用umount命令跟上挂载的源地址或者挂载点(设备,文件,远程目录等)就可以。例如:
Quote: |
$ umount /path/to/mountpoint_directory 或者 $ umount /path/to/mount_source
|
如果想管理大量的或者经常性的挂载服务,那么每次手动挂载是很糟糕的事情。这个时候就可以利用mount的配置文件/etc/fstab,把mount对
应的参数写到/etc/fstab文件对应的列中即可实现批量挂载(mount -a)和卸载(umount
-a)。/etc/fstab中各列分别为文件系统、挂载点、类型、相关选项。更多信息可参考fstab的帮助(man fstab)。
参考资料:
[1] Linux硬盘分区以及其挂载原理
http://www.xxlinux.com/linux/article/accidence/technique/20070521/8493.html
[2] 从文件I/O看linux的虚拟文件系统
http://www.ibm.com/developerworks/cn/linux/l-cn-vfs/
[3] 用Graphviz进行可视化操作──绘制函数调用关系图
http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_1425.html
5 如何制作一个文件系统
Linux的文件系统下有一些最基本的目录,不同的目录下存放着不同作用的各类文件。最基本的目录有/etc, /lib, /dev,
/bin等,它们分别存放着系统配置文件,库文件,设备文件和可执行程序。这些目录一般情况下是必须的,在做嵌入式开发的时候,我们需要手动或者是用
busybox等工具来创建这样一个基本的文件系统。如何来制作一个这样的文件系统呢?请参考资料[1]和[2]。这里我们制作仅制作一个非常简单的文件
系统,并对该文件系统进行各种常规的操作,以便加深对文件系统的理解。
首先,创建一个固定大小的文件。
Quote: |
// 还记得dd命令么?我们就用它来产生一个固定大小的文件,这个为1M(1024*1024 bytes)的文件 $ dd if=/dev/zero of=minifs bs=1024 count=1024 // 查看文件类型,这里的minifs是一个充满\0的文件,没有任何特定的数据结构 $ file minifs minifs: data
|
说明:/dev/zero是一个非常特殊的设备,如果读取它,可以获取任意多个\0。
接着把该文件格式化为某个指定文件类型的文件系统。(是不是觉得不可思议,文件也可以格式化?是的,不光是设备可以,文件也可以以某种文件系统类型进行组织,但是需要注意的是,某些文件系统(如ext3)要求被格式化的目标最少有64M的空间)。
Quote: |
// 格式化文件 $ mkfs.ext2 minifs // 查看此时的文件类型,这个时候文件minifs就以ext2文件系统的格式组织了 $ file minifs minifs: Linux rev 1.0 ext2 filesystem data
|
因为该文件以文件系统的类型组织了,那么可以用mount命令挂载并使用它。
Quote: |
// 请切换到root用户挂载它,并通过-o loop选项把它关联到一个特殊设备/dev/loop $ mount minifs /mnt/ -o loop // 查看该文件系统的信息,仅可以看到一个目录文件lost+found $ ls /mnt/ lost+found
|
在该文件系统下进行各种常规操作,包括读、写、删除等。(每次操作前先把minifs文件保存一份,以便比较,结合相关资料就可以深入地分析各种操作对文件系统的改变情况,从而深入理解文件系统作为一种组织数据的方式的实现原理等)
Quote: |
$ cp minifs minifs.bak $ cd /mnt $ touch hello $ cd - $ cp minifs minifs-touch.bak $ od -x minifs.bak > orig.od $ od -x minifs-touch.bak > touch.od // 创建一个文件后,比较此时文件系统和之前文件系统的异同 $ diff orig.od touch.od diff orig.od touch.od 61,63c61,64 < 0060020 000c 0202 2e2e 0000 000b 0000 03e8 020a < 0060040 6f6c 7473 662b 756f 646e 0000 0000 0000 < 0060060 0000 0000 0000 0000 0000 0000 0000 0000 --- > 0060020 000c 0202 2e2e 0000 000b 0000 0014 020a > 0060040 6f6c 7473 662b 756f 646e 0000 000c 0000 > 0060060 03d4 0105 6568 6c6c 006f 0000 0000 0000 > 0060100 0000 0000 0000 0000 0000 0000 0000 0000 // 通过比较发现:添加一个文件后,文件系统的相应位置发生了明显的变化 $ echo "hello, world" > /mnt/hello // 执行sync命令,确保缓存中的数据已经写入磁盘(还记得附图[1]的buffer cache吧,这里就是把cache中的数据写到磁盘中) $ sync $ cp minifs minifs-echo.bak $ od -x minifs-echo.bak > echo.od // 写入文件内容后,比较文件系统和之前的异同 $ diff touch.od echo.od // 查看文件系统中的字符串 $ strings minifs lost+found hello hello, world // 删除hello文件,查看文件系统变化 $ rm /mnt/hello $ cp minifs minifs-rm.bak $ od -x minifs-rm.bak > rm.od $ diff echo.od rm.od // 通过查看文件系统的字符串们发现:删除文件时并没有覆盖文件的内容,所以从理论上说内容此时还是可恢复的 $ strings minifs lost+found hello hello, world
|
上面仅仅演示了一些分析文件系统的常用工具,并分析了几个常规的操作,如果你想非常深入地理解文件系统的实现原理,请熟悉使用上述工具并阅读相关资料,比如1节的参考资料[2][3][4]。
参考资料:
[1] Build a mini filesystem in linux from scratch
http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_1211.html
[2] Build a mini filesystem in linux with BusyBox
http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_1212.html
[3] ext2 文件系统
http://man.chinaunix.net/tech/lyceum/linuxK/fs/filesystem.html
6 如何开发自己的文件系统
随着fuse的出现,在用户空间开发文件系统成为可能,如果想开发自己的文件系统,那么阅读下面的参考资料吧。
参考资料:
[1] 使用fuse开发自己的文件系统
后记:
[1] 2007年12月22日,收集了很多资料,写了整体的框架。
[2] 2007年12月28日下午,完成初稿,考虑到时间关系,很多细节也没有进一步分析,另外有些部分可能存在理解上的问题,欢迎批评指正。
[3] 2007年12月28日晚,修改部分资料,并正式公开该篇文档。
[4] 29号,添加设备驱动和硬件设备一小节。
by falcon<zhangjinw@gmail.com>
2008-02-21
这一小节写了很久,到现在才写完。本来关注的内容比较多,包括程序开发过程的细节、ELF格式的分析、进程的内存映像等,后来搞得“雪球越滚越大”,甚至
脱离了shell编程关注的内容。所以呢,想了个小办法,“大事化小,小事化了”,把涉及到的内容分成如下几个部分:
1、把VIM打造成源代码编辑器(源代码编辑过程:用VIM编辑代码的一些技巧)
2、GCC编译的背后 第一部分:预处理和编译 第二部分:汇编和链接(编译过程:预处理、编译、汇编、链接)
3、程序执行的那一刹那 (执行过程:当我们从命令行输入一个命令之后)
4、进程的内存映像 (进程加载过程:程序在内存里是个什么样子)
5、动态符号链接的细节(动态链接过程:函数puts/printf的地址在哪里)
6、代码测试、调试与优化小结(程序开发过后:内存溢出了吗?有缓冲区溢出?代码覆盖率如何测试呢?怎么调试汇编代码?有哪些代码优化技巧和方法呢?)
7、为你的可执行文件“减肥”(从"减肥"的角度一层一层剖开ELF文件)
8、进程和进程的基本操作(本小节)
呵呵,好多。终于可以一部分一部分地完成了,不会再有一种对着一个大蛋糕却不知道如何下口的尴尬了。
进程作为程序真正发挥作用时的“形态”,我们有必要对它的一些相关操作非常熟悉,这一节主要描述进程相关的概念和操作,将介绍包括程序、进程、作业等基本概念以及进程状态查询、进程通信等相关的基本操作等。
1、什么是程序,什么又是进程
程序是指令的集合,而进程则是程序执行的基本单元。为了让程序完成它的工作,我们必须让程序运行起来成为进程,进而利用处理器资源,内存资源,进行各种I/O操作,从而完成某项指定的工作。
在这个意思上说,程序是静态的,而进程则是动态的。
而进程有区别于程序的地方还有,进程除了包含程序文件中的指令数据意外,还需要在内核中有一个数据结构用以存放特定进程的相关属性,以便内核更好的管理和调度进程,从而完成多进程协作的任务。因此,从这个意义上可以说“高于”程序,超出了程序指令本身。
如果进行过多进程程序的开发,你又会发现,一个程序可能创建多个进程,通过多个进程的交互完成任务。在Linux下,多进程的创建通常是通过fork系统调用实现的。从这个意义上来说程序则”包含”了进程。
另外一个需要明确的是,程序可以由多种不同的程序语言描述,包括C语言程序、汇编语言程序和最后编译产生的机器指令等。
下面我们简单讨论一下Linux下面如何通过shell进行进程的相关操作。
2、进程的创建
通常在命令行键入某个程序文件名以后,一个进程就被创建了。例如,
Quote: |
$ sleep 100 & #让sleep程序在后台运行 [1] 9298 $ pidof sleep #用pidof可以查看指定程序名的进程ID 9298 $ cat /proc/9298/maps #查看进程的内存映像 08048000-0804b000 r-xp 00000000 08:01 977399 /bin/sleep 0804b000-0804c000 rw-p 00003000 08:01 977399 /bin/sleep 0804c000-0806d000 rw-p 0804c000 00:00 0 [heap] b7c8b000-b7cca000 r--p 00000000 08:01 443354 ... bfbd8000-bfbed000 rw-p bfbd8000 00:00 0 [stack] ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
|
当一个程序被执行以后,程序被加载到内存中,成为了一个进程。上面显示了该进程的内存映像(虚拟内存),包括程序指令、数据,以及一些用于存放程命令行参数、环境变量的栈空间,用于动态内存申请的堆空间都被分配好了。
关于程序在命令行执行过程的细节,请参考《Linux命令行下程序执行的那一刹那》。
实际上,创建一个进程,也就是说让程序运行,还有其他的办法,比如,通过一些配置让系统启动时自动启动我们的程序(具体参考"man
init"),或者是通过配置crond(或者at)让它定时启动我们的程序。除此之外,还有一个方式,那就是编写shell脚本,把程序写入一个脚本文
件,当执行脚本文件时,文件中的程序将被执行而成为进程。这些方式的细节就不介绍了,下面介绍如何查看进程的属性。
需要补充一点的是,在命令行下执行程序时,我们可以通过ulimit内置命令来设置进程可以利用的资源,比如进程可以打开的最大文件描述符个数,最大的栈空间,虚拟内存空间等。具体用法见"help ulimit"。
3、查看进程的属性和状态
我们可以通过ps命令查看进程的相关属性和状态,这些信息包括进程所属用户,进程对应的程序,进程对cpu和内存的使用情况等信息。熟悉如何查看它们有助于我们进行相关的统计分析和进一步的操作。
Quote: |
$ ps -ef #查看系统所有当前进程的属性 $ ps -C init #查看命令中包含某个指定字符的程序对应的进程,进程ID是1,TTY为?表示和终端没有关联 PID TTY TIME CMD 1 ? 00:00:01 init $ ps -U falcon #选择某个特定用户启动的进程 $ ps -e -o "%C %c" #可以按照指定格式输出指定内容,这里会输出命令名和cpu使用率 $ ps -e -o "%C %c" | sort -u -k1 -r | head -5 #这样则会打印cpu使用率最高的前4个程序 7.5 firefox-bin 1.1 Xorg 0.8 scim-panel-gtk 0.2 scim-bridge $ ps -e -o "%z %c" | sort -n -k1 -r | head -5 #使用虚拟内存最大的5个进程 349588 firefox-bin 96612 xfce4-terminal 88840 xfdesktop 76332 gedit 58920 scim-panel-gtk
|
由于系统所有进程之间都有“亲缘”关系,所以可以通过pstree查看这种关系,
Quote: |
$ pstree #打印系统进程调用树,可以非常清楚地看到当前系统中所有活动进程之间的调用关系
|
动态查看进程信息,
该命令最大的特点是可以动态的查看进程的信息,当然,它还提供了一些有用的参数,比如-S可以按照累计执行时间的大小排序查看,也可以通过-u查看指定用户启动的进程等。
感觉有上面几个命令来查看进程的信息就差不多了,下面来讨论一个有趣的问题:如何让一个程序在同一时间只有一个在运行。
这意味着当一个程序正在被执行时,它将不能再被启动。那该怎么做呢?
假如一份相同的程序被复制成了很多份,并且具有不同的文件名被放在不同的位置,这个将比较糟糕,所以我们考虑最简单的情况,那就是这份程序在整个系统上是唯一的,而且名字也是唯一的。这样的话,我们有哪些办法来回答上面的问题呢?
总的机理是:在这个程序的开头检查自己有没有执行,如果执行了则停止否则继续执行后续代码。
策略则是多样的,由于前面的假设已经保证程序文件名和代码的唯一性,所以
通过ps命令打印找出当前的所有进程对应的程序名,逐个与自己的程序名比较,如果已经有,那么说明自己已经运行了。
Code:
[Ctrl+A Select All]
每次运行时先在指定位置检查是否存在一个保存自己进程ID的文件,如果不存在,那么继续执行,如果存在,那么查看该进程ID是否正在运行,如果在,那么退出,否则往该文件重新写的新的进程ID,并继续。
Code:
[Ctrl+A Select All]
更多实现策略自己尽情的发挥吧!
4、调整进程的优先级
在保证每个进程都能够顺利执行外,为了让某些任务优先完成,那么系统在进行进程调度时就会采用一定的调度办法,比如常见的有按照优先级的时间片轮转的调度算法。这种情况下,我们可以通过renice调整正在运行的程序的优先级,例如,
Quote: |
$ ps -e -o "%p %c %n" | grep xfs #打印的信息分别是进程ID,进程对应的程序名,优先级 5089 xfs 0 $ renice 1 -p 5089 renice: 5089: setpriority: Operation not permitted $ sudo renice 1 -p 5089 #需要权限才行 [sudo] password for falcon: 5089: old priority 0, new priority 1 $ ps -e -o "%p %c %n" | grep xfs #再看看,优先级已经被调整过来了 5089 xfs 1
|
5、结束进程
既然可以通过命令行执行程序,创建进程,那么也有办法结束它。我们可以通过kill命令给用户自己启动的进程发送一定信号让进程终止,当然“万能”的root几乎可以kill所有进程(除了init之外)。例如,
Quote: |
$ sleep 50 & #启动一个进程 [1] 11347 $ kill 11347
|
kill命令默认会发送终止信号(SIGTERM)给程序,让程序退出,但是kill还可以发送其他的信号,这些信号的定义我们可以通过man 7 signal查看到,也可以通过kill -l列出来。
Quote: |
$ man 7 signal $ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
|
例如,我们用kill命令发送SIGSTOP信号给某个程序,让它暂停,然后发送SIGCONT信号让它继续运行。
Quote: |
$ sleep 50 & [1] 11441 $ jobs [1]+ Running sleep 50 & $ kill -s SIGSTOP 11441 #这个等同于我们对一个前台进程执行CTRL+Z操作 $ jobs [1]+ Stopped sleep 50 $ kill -s SIGCONT 11441 #这个等同于之前我们使用bg %1操作让一个后台进程运行起来 $ jobs [1]+ Running sleep 50 & $ kill %1 #在当前会话(session)下,也可以通过作业号控制进程 $ jobs [1]+ Terminated sleep 50
|
可见kill命令为我们提供了非常好的功能,不过kill命令只能根据进程的ID或者作业来控制进程,所以pkill和killall给我们提供了更多选择,它们扩展了通过程序名甚至是进程的用户名来控制进程的方法。更多用法请参考它们的手册。
当一个程序退出以后,如何判断这个程序是正常退出还是异常退出呢?还记得Linux下,那个经典"hello,world"程序吗?在代码的最后总是有条
“return 0”语句。这个“return
0”实际上是为了让程序员来检查进程是否正常退出的。如果进程返回了一个其他的数值,那么我们可以肯定的说这个进程异常退出了,因为它都没有执行到
“return 0”这条语句就退出了。
那怎么检查进程退出的状态,即那个返回的数值呢?
在shell程序中,我们可以检查这个特殊的变量$?,它存放了上一条命令执行后的退出状态。
Quote: |
$ test1 bash: test1: command not found $ echo $? 127 $ cat ./test.c | grep hello $ echo $? 1 $ cat ./test.c | grep hi printf("hi, myself!\n"); $ echo $? 0
|
貌似返回0成为了一个潜规则,虽然没有标准明确规定,不过当程序正常返回时,我们总是可以从$?中检测到0,但是异常时,我们总是检测到一个非0的值。这
就告诉我们在程序的最后我们最好是跟上一个exit
0以便任何人都可以通过检测$?确定你的程序是否正常结束。如果有一天,有人偶尔用到你的程序,试图检查你的程序的退出状态,而你却在程序的末尾莫名的返
回了一个-1或者1,那么他将会很苦恼,会怀疑自己的程序哪个地方出了问题,检查半天却不知所措,因为他太信任你了,竟然从头至尾都没有怀疑你的编程习惯
可能会与众不同。
6、进程通信
为了便于设计和实现,通常一个大型的任务都被划分成较小的模块。不同模块之间启动后成为进程,它们之间如何通信以便交互数据,协同工作呢?在《UNIX环
境高级编程》一书中提到很多方法,诸如管道(无名管道和有名管道)、信号(signal)、报文(Message)队列(消息队列)、共享内存
(mmap/munmap)、信号量(semaphore,主要是同步用,进程之间,进程的不同线程之间)、套接口(Socket,支持不同机器之间的进
程通信)等,而在shell编程里头,我们通常直接用到的就有管道和信号等。下面主要介绍管道和信号机制在shell编程时候的一些用法。
无名管道(pipe):
在Linux下,你可以通过"|"连接两个程序,这样就可以用它来连接后一个程序的输入和前一个程序的输出,因此被形象地叫做个管道。在C语言里头,创建
无名管道非常简单方便,用pipe函数,传入一个具有两个元素的int型的数组就可以。这个数组实际上保存的是两个文件描述符,父进程往第一个文件描述符
里头写入东西后,子进程可以从第一个文件描述符中读出来。
如果用多了命令行,这个管子"|"应该会经常用。比如我们在上面的演示中把ps命令的输出作为grep命令的输入,从而可以过滤掉一些我们感兴趣的信息:
Quote: |
$ ps -ef | grep init
|
也许你会觉得这个“管子”好有魔法,竟然真地能够链接两个程序的输入和输出,它们到底是怎么实现的呢?实际上当我们输入这样一组命令的时候,当前解释程序
会进行适当的解析,把前面一个进程的输出关联到管道的输出文件描述符,把后面一个进程的输入关联到管道的输入文件描述符,这个关联过程通过输入输出重定向
函数dup(或者fcntl)来实现。
有名管道(named pipe):
有名管道实际上是一个文件(无名管道也像一个文件,虽然关系到两个文件描述符,不过只能一边读另外一边写),不过这个文件比较特别,操作时要满足先进先
出,而且,如果试图读一个没有内容的有名管道,那么就会被阻塞,同样地,如果试图往一个有名管道里头写东西,而当前没有程序试图读它,也会被阻塞。下面看
看效果。
Quote: |
$ mkfifo fifo_test #通过mkfifo命令可以创建一个有名管道 $ echo "fewfefe" > fifo_test #试图往fifo_test文件中写入内容,但是被阻塞,要另开一个终端继续下面的操作 $ cat fifo_test #另开一个终端,记得,另开一个。试图读出fifo_test的内容 fewfefe
|
在这里echo和cat是两个不同的程序,在这种情况下,通过echo和cat启动的两个进程之间并没有父子关系。不过它们依然可以通过有名管道通信。这
样一种通信方式非常适合某些情况:例如有这样一个架构,这个架构由两个应用程序构成,其中一个通过一个循环不断读取fifo_test中的内容,以便判
断,它下一步要做什么。如果这个管道没有内容,那么它就会被阻塞在那里,而不会死循环而耗费资源,另外一个则作为一个控制程序不断地往fifo_test
中写入一些控制信息,以便告诉之前的那个程序该做什么。下面写一个非常简单的例子。我们可以设计一些控制码,然控制程序不断的往fifo_test里头写
入,然后应用程序根据这些控制码完成不同的动作。当然,也可以往fifo_test传入除控制码外的不同的数据。
Quote: |
$ cat app.sh #应用程序的代码 #!/bin/bash
FIFO=fifo_test while :; do CI=`cat $FIFO` #CI --> Control Info case $CI in 0) echo "The CONTROL number is ZERO, do something ..." ;; 1) echo "The CONTROL number is ONE, do something ..." ;; *) echo "The CONTROL number not recognized, do something else..." ;; esac done $ cat control.sh #控制程序的代码 #!/bin/bash
FIFO=fifo_test CI=$1
[ -z "$CI" ] && echo "the control info should not be empty" && exit
echo $CI > $FIFO $ chmod +x app.sh control.sh #修改这两个程序的可执行权限,以便用户可以执行它们 $ ./app.sh #在一个终端启动这个应用程序,在通过./control.sh发送控制码以后查看输出 The CONTROL number is ONE, do something ... #发送1以后 The CONTROL number is ZERO, do something ... #发送0以后 The CONTROL number not recognized, do something else... #发送一个未知的控制码以后 $ ./control.sh 1 #在另外一个终端,发送控制信息,控制应用程序的工作 $ ./control.sh 0 $ ./control.sh 4343
|
这样一种应用架构非常适合本地的多程序任务的设计,如果结合web cgi,那么也将适合远程控制的要求。引入web
cgi的唯一改变是,要把控制程序./control.sh放到web的cgi目录下,并对它作一些修改,以使它符合CGI的规范,这些规范包括文档输出
格式的表示(在文件开头需要输出content-tpye:
text/html以及一个空白行)和输入参数的获取(web输入参数都存放在QUERY_STRING环境变量里头)。因此一个非常简单的CGI形式控
制程序将类似下面。
Code:
[Ctrl+A Select All]
在实际使用的时候,请确保control.sh能够访问到fifo_test管道,并且有写权限。这样我们在浏览器上就可以这样控制app.sh了。
http://ipaddress_or_dns/cgi-bin/control.sh?0
问号(?)后面的内容即QUERY_STRING,类似之前的$1。
这样一种应用对于远程控制,特别是嵌入式系统的远程控制很有实际意义。在去年的暑期课程上,我们就通过这样一种方式来实现马达的远程控制。首先,我们实现
了一个简单的应用程序以便控制马达的转动,包括转速,方向等的控制。为了实现远程控制,我们设计了一些控制码,以便控制马达转动相关的不同属性。
在C语言中,如果要用有名管道,和shell下的类似,只不过在读写数据的时候用read,write调用,在创建fifo的时候用mkfifo函数调用。
信号(Signal):
信号是软件中断,在Linux下面用户可以通过kill命令给某个进程发送一个特定的信号,也可以通过键盘发送一些信号,比如CTRL+C可能触发
CGIINT信号,而CTRL+\可能触发SGIQUIT信号等,除此之外,内核在某些情况下也会给进程发送信号,比如在访问内存越界时产生
SGISEGV信号,当然,进程本身也可以通过kill,raise等函数给自己发送信号。对于Linux下支持的信号类型,大家可以通过"man 7
signal"或者"kill -l"查看到相关列表和说明。
对于有些信号,进程会有默认的响应动作,而有些信号,进程可能直接会忽略,当然,用户还可以对某些信号设定专门的处理函数。在shell程序中,我们可以
通过trap命令(shell的内置命令)来设定响应某个信号的动作(某个命令或者是你定义的某个函数),而在C语言里头可以通过signal调用注册某
个信号的处理函数。这里仅仅演示trap命令的用法。
Quote: |
$ function signal_handler { #定一个signal_handler的函数,>是按下换行符号自动出现的 > echo "hello, world" > } $ trap signal_handle SIGINT #执行该命令设定:当发生SIGINT信号时将打印hello。 $ hello, world #按下CTRL+C,可以看到屏幕上输出了hello, world字符串
|
类似地,如果设定信号0的响应动作,那么就可以用trap来模拟C语言程序中的atexit程序终止函数的登记,即通过trap
signal_handler
SIGQUIT设定的signal_handler函数将在程序退出的时候被执行。信号0是一个特别的信号,在POSIX.1中把信号编号0定义为空信
号,这将常被用来确定一个特定进程是否仍旧存在。当一个程序退出时会触发该信号。
Quote: |
$ cat sigexit.sh #!/bin/bash
function signal_handler { echo "hello, world" } trap signal_handler 0 $ chmod +x sigexit.sh $ ./sigexit.sh #实际上在shell编程时,会用这种方式在程序退出时来做一些清理临时文件的收尾工作 hello, world
|
7、作业和作业控制
当我们为完成一些复杂的任务而将多个命令通过|,>,<, ;, (,
)等组合在一起的时候,通常这样个命令序列会启动多个进程,它们之间通过管道等进行通信。而有些时候,我们在执行一个任务的同时,还有其他的任务需要处
理,那么就经常会在命令序列的最后加上一个&,或者在执行命令以后,按下CTRL+Z让前一个命令暂停。以便做其他的任务。等做完其他一些任务以
后,再通过fg命令把后台的任务切换到前台。这样一种控制过程通常被成为作业控制,而那些命令序列则被成为作业,这个作业可能涉及一个或者多个程序,一个
或者多个进程。下面演示一下几个常用的作业控制操作。
Quote: |
$ sleep 50 & #让程序在后台运行,将打印[作业号]和进程号 [1] 11137 $ fg %1 #使用shell内置命令fg把作业1调到前台运行,然后按下CTRL+Z让该进程暂停 sleep 50
[1]+ Stopped sleep 50 $ jobs #查看当前作业情况,有一个作业停止 [1]+ Stopped sleep 50 $ sleep 100 & #让另外一个作业在后台运行 [2] 11138 $ jobs #查看当前作业情况,一个正在运行,一个停止 [1]+ Stopped sleep 50 [2]- Running sleep 100 & $ bg %1 #启动停止的进程并让它在后台运行 [2]+ sleep 50 &
|
不过,要在命令行下使用作业控制,需要当前shell,内核终端驱动等对作业控制支持才行。
参考资料
<UNIX环境高级编程>相关章节
by falcon <zhangjinw@gmail.com>
2008-02-23
这篇blog从减少可执行文件大小的角度分析了ELF文件,期间通过经典的"Hello World"实例逐步演示如何通过各种常用工具来分析ELF文件,并逐步精简代码。
为了能够尽量减少可执行文件的大小,我们必须了解可执行文件的格式,以及链接生成可执行文件时的后台细节(即最终到底有哪些内容被链接到了目标代码中)。
通过选择合适的可执行文件格式并剔除对可执行文件的最终运行没有影响的内容,就可以实现目标代码的裁减。因此,通过探索减少可执行文件大小的方法,就相当
于实践性地去探索了可执行文件的格式以及链接过程的细节。
当然,算法的优化和编程语言的选择可能对目标文件的大小有很大的影响,在这篇blog的后面我们会跟参考资料[1]的作者那样去探求一个打印“Hello World”的可执行文件能够小到什么样的地步。
1、可执行文件格式的选取
可执行文件格式的选择要满足的一个基本条件是:目标系统支持该可执行文件格式,资料[2]分析和比较了UNIX平台下的三种可执行文件格式,这三种格式实际上代表着可执行文件的一个发展过程。
a.out文件格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section的顺序是固定的。
coff文件格式虽然引入了一个节区表以支持更多节区信息,从而提高了可扩展性,但是这种文件格式的重定位在链接时就已经完成,因此不支持动态链接(不过扩展的coff支持)。
elf文件格式不仅动态链接,而且有很好的扩展性。它可以描述可重定位文件、可执行文件和可共享文件(动态链接库)三类文件。
下面来看看ELF文件的结构图。
文件头部(ELF Header)
程序头部表(Program Header Table)
节区1(Section1)
节区2(Section2)
节区3(Section3)
...
节区头部(Section Header Table)
无论是文件头部、程序头部表、节区头部表还是各个节区,都是通过特定的结构体(struct)描述的,这些结构在elf.h文件中定义。文件头部用于描述
整个文件的类型、大小、运行平台、程序入口、程序头部表和节区头部表等信息。例如,我们可以通过文件头部查看该ELF文件的类型。
Quote: |
$ cat hello.c #典型的hello, world程序 #include <stdio.h>
int main(void) { printf("hello, world!\n"); return 0; } $ gcc -c hello.c #编译,产生可重定向的目标代码 $ readelf -h hello.o | grep Type #通过readelf查看文件头部找出该类型 Type: REL (Relocatable file) $ gcc -o hello hello.o #生成可执行文件 $ readelf -h hello | grep Type Type: EXEC (Executable file) $ gcc -fpic -shared -W1,-soname,libhello.so.0 -o libhello.so.0.0 hello.o #生成共享库 $ readelf -h libhello.so.0.0 | grep Type Type: DYN (Shared object file)
|
那节区头部表(将简称节区表)和程序头部表有什么用呢?实际上前者只对可重定向文件有用,而后者只对可执行文件和可共享文件有用。
节区表是用来描述各节区的,包括各节区的名字、大小、类型、虚拟内存中的位置、相对文件头的位置等,这样所有节区都通过节区表给描述了,这样连接器就可以
根据文件头部表和节区表的描述信息对各种输入的可重定位文件进行合适的链接,包括节区的合并与重组、符号的重定位(确认符号在虚拟内存中的地址)等,把各
个可重定向输入文件链接成一个可执行文件(或者是可共享文件)。如果可执行文件中使用了动态连接库,那么将包含一些用于动态符号链接的节区。我们可以通过
readelf -S(或objdump -h)查看节区表信息。
Quote: |
$ readelf -S hello #可执行文件、可共享库、可重定位文件默认都生成有节区表 ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048114 000114 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048128 000128 000020 00 A 0 0 4 [ 3] .hash HASH 08048148 000148 000028 04 A 5 0 4 ... [ 7] .gnu.version VERSYM 0804822a 00022a 00000a 02 A 5 0 2 ... [11] .init PROGBITS 08048274 000274 000030 00 AX 0 0 4 ... [13] .text PROGBITS 080482f0 0002f0 000148 00 AX 0 0 16 [14] .fini PROGBITS 08048438 000438 00001c 00 AX 0 0 4 ...
|
三种类型文件的节区(各个常见节区的作用请参考资料[11])可能不一样,但是有几个节区,例如.text, .data,
.bss是必须的,特别是.text,因为这个节区包含了代码。如果一个程序使用了动态链接库(引用了动态连接库中的某个函数),那么需要.interp
节区以便告知系统使用什么动态连接器程序来进行动态符号链接,进行某些符号地址的重定位。通常,.rel.text节区只有可重定向文件有,用于链接时对
代码区进行重定向,而.hash,.plt,.got等节区则只有可执行文件(或可共享库)有,这些节区对程序的运行特别重要。还有一些节区,可能仅仅是
用于注释,比如.comment,这些对程序的运行似乎没有影响,是可有可无的,不过有些节区虽然对程序的运行没有用处,但是却可以用来辅助对程序进行调
试或者对程序运行效率有影响。
虽然三类文件都必须包含某些节区,但是节区表对可重定位文件来说才是必须的,而程序的执行却不需要节区表,只需要程序头部表以便知道如何加载和执行文件。
不过如果需要对可执行文件或者动态连接库进行调试,那么节区表却是必要的,否则调试器将不知道如何工作。下面来介绍程序头部表,它可通过readelf
-l(或objdump -p)查看。
Quote: |
$ readelf -l hello.o #对于可重定向文件,gcc没有产生程序头部,因为它对可重定向文件没用
There are no program headers in this file.
$ readelf -l hello #而可执行文件和可共享文件都有程序头部
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00470 0x00470 R E 0x1000
LOAD 0x000470 0x08049470 0x08049470 0x0010c 0x00110 RW 0x1000
DYNAMIC 0x000484 0x08049484 0x08049484 0x000d0 0x000d0 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini
.rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
$ readelf -l libhello.so.0.0 #节区和上面类似,这里省略
|
从上面可看出程序头部表描述了一些段(Segment),这些段对应着一个或者多个节区,上面的readelf
-l很好地显示了各个段与节区的映射。这些段描述了段的名字、类型、大小、第一个字节在文件中的位置、将占用的虚拟内存大小、在虚拟内存中的位置等。这样
系统程序解释器将知道如何把可执行文件加载到内存中以及进行动态链接等动作。
该可执行文件包含7个段,PHDR指程序头部,INTERP正好对应.interp节区,两个LOAD段包含程序的代码和数据部分,分别包含有.text
和.data,.bss节区,DYNAMIC段包含.daynamic,这个节区可能包含动态连接库的搜索路径、可重定位表的地址等信息,它们用于动态连
接器。NOTE和GNU_STACK段貌似作用不大,只是保存了一些辅助信息。因此,对于一个不使用动态连接库的程序来说,可能只包含LOAD段,如果一
个程序没有数据,那么只有一个LOAD段就可以了。
总结一下,Linux虽然支持很多种可执行文件格式,但是目前ELF较通用,所以选择ELF作为我们的讨论对象。通过上面对ELF文件分析发现一个可执行
的文件可能包含一些对它的运行没用的信息,比如节区表、一些用于调试、注释的节区。如果能够删除这些信息就可以减少可执行文件的大小,而且不会影响可执行
文件的正常运行。
2、链接优化
从上面的讨论中已经接触了动态连接库。ELF中引入动态连接库后极大地方便了公共函数的共享,节约了磁盘和内存空间,因为不再需要把那些公共函数的代码链接到可执行文件,这将减少了可执行文件的大小。
与此同时,静态链接可能会引入一些对代码的运行可能并非必须的内容。你可以从
《GCC编译的背后(第二部分:汇编和链接)》 了
解到GCC链接的细节。从那篇Blog中似乎可以得出这样的结论:仅仅从是否影响一个C语言程序运行的角度上说,GCC默认链接到可执行文件的几个可重定
位文件(crt1.o,rti.o,crtbegin.o,crtend.o,crtn.o)并不是必须的,不过值得注意的是,如果没有链接那些文件但在
程序末尾使用了return语句,main函数将无法返回,因此需要替换为_exit调用;另外,既然程序在进入main之前有一个入口,那么main入
口就不是必须的。因此,如果不采用默认链接也可以减少可执行文件的大小。
3、可执行文件“减肥”实例
这里主要是根据上面两点来介绍如何减少一个可执行文件的大小。以"Hello World"为例。
首先来看看默认编译产生的Hello World的可执行文件大小。
系统默认编译
代码同上,下面是一组演示,
Quote: |
$ uname -r #先查看内核版本和gcc版本,以便和你的结果比较 2.6.22-14-generic $ gcc --version gcc (GCC) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2) ... $ gcc -o hello hello.c #默认编译 $ wc -c hello #产生一个大小为6442字节的可执行文件 6442 hello
|
不采用默认编译
可以考虑编辑时就把return 0替换成_exit(0)并包含定义该函数的unistd.h头文件。下面是从GCC编译的背后(第二部分:汇编和链接)》总结出的Makefile文件。
Code:
[Ctrl+A Select All]
把上面的代码复制到一个Makefile文件中,并利用它来编译hello.c。
Quote: |
$ make #编译 sed -i -e '/#include[ "<]*unistd.h[ ">]*/d;' -i -e '1i #include <unistd.h>' -i -e 's/return 0;/_exit(0);/' hello.c cc -S hello.c sed -i -e 's/main/_start/g' hello.s cc -c hello.s ld -dynamic-linker /lib/ld-linux.so.2 -L /usr/lib/ -lc -o hello hello.o $ ./hello #这个也是可以正常工作的 Hello World $ wc -c hello #但是大小减少了4382个字节,减少了将近70% 2060 hello $ echo "6442-2060" | bc
4382
$ echo "(6442-2060)/6442" | bc -l
.68022353306426575597
|
对于一个比较小的程序,能够减少将近70%“没用的”代码。至于一个大一点的程序(这个代码是[1]的作者写的一个小工具,我们后面会使用它)再看看效果。
Quote: |
$ gcc -o sstrip sstrip.c #默认编译的情况 $ wc -c sstrip 10912 sstrip $ sed -i -e "s/hello/sstrip/g" Makefile #把Makefile中的hello替换成sstrip $ make clean #清除默认编译的sstrip $ make #用我们的Makefile编译 $ wc -c sstrip 6589 sstrip $ echo "10912-6589" | bc -l #再比较大小,减少的代码还是4323个字节,减幅40% 4323 $ echo "(10912-6589)/10912" | bc -l .39616935483870967741
|
通过这两个简单的实验,我们发现,能够减少掉4000个字节左右,相当于4k左右。
删除对程序运行没有影响的节区
使用上述Makefile来编译程序,不链接那些对程序运行没有多大影响的文件,实际上也相当于删除了一些“没用”的节区,可以通过下列演示看出这个实质。
Quote: |
$ sed -i -e "s/sstrip/hello/g" Makefile #先看看用Makefile编译的结果,替换回hello $ make clean $ make $ readelf -l hello | grep "0[0-9]\ \ " 00 01 .interp 02 .interp .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.plt .plt .text .rodata 03 .dynamic .got.plt 04 .dynamic 05 $ make clean $ gcc -o hello hello.c $ readelf -l hello | grep "0[0-9]\ \ " 00 01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini
.rodata .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06
|
通过比较发现使用自定义的Makefile文件,少了这么多节区:.bss .ctors .data .dtors .eh_frame .fini .gnu.hash .got .init .jcr .note.ABI-tag .rel.dyn。
再看看还有哪些节区可以删除呢?通过之前的分析发现有些节区是必须的,那.hash?.gnu.version?呢,通过strip -R(或objcop -R)删除这些节区试试。
Quote: |
$ wc -c hello #查看大小,以便比较 2060 $ time ./hello #我们比较一下一些节区对执行时间可能存在的影响 Hello World
real 0m0.001s user 0m0.000s sys 0m0.000s $ strip -R .hash hello #删除.hash节区 $ wc -c hello 1448 hello $ echo "2060-1448" | bc #减少了612字节 612 $ time ./hello #发现执行时间长了一些(实际上也可能是进程调度的问题) Hello World
real 0m0.006s user 0m0.000s sys 0m0.000s $ strip -R .gnu.version hello #删除.gnu.version还是可以工作 $ wc -c hello 1396 hello $ echo "1448-1396" | bc #又减少了52字节 52 $ time ./hello Hello World
real 0m0.130s user 0m0.004s sys 0m0.000s $ strip -R .gnu.version_r hello #删除.gnu.version_r就不工作了 $ time ./hello ./hello: error while loading shared libraries: ./hello: unsupported version 0 of Verneed record
|
通过删除各个节区可以查看哪些节区对程序来说是必须的,不过有些节区虽然并不影响程序的运行却可能会影响程序的执行效率,这个可以上面的运行时间看出个大概。
通过删除两个“没用”的节区,我们又减少了52+612,即664字节。
删除可执行文件的节区表
用普通的工具没有办法删除节区表,但是参考资料[1]的作者已经写了这样一个工具。你可以从这里http://www.muppetlabs.com/~breadbox/software/elfkickers.html下载到那个工具,即我们上面作为一个演示例子的sstrip,它是该作者写的一序列工具ELFkickers中的一个。下载以后,编译,并复制到/usr/bin下,下面用它来删除节区表。
Quote: |
$ sstrip hello #删除ELF可执行文件的节区表 $ ./hello #还是可以正常运行,说明节区表对可执行文件的运行没有任何影响 Hello World $ wc -c hello #大小只剩下708个字节了 708 hello $ echo "1396-708" | bc #又减少了688个字节。 688
|
通过删除节区表又把可执行文件减少了688字节。现在回头看看相对于gcc默认产生的可执行文件,通过删除一些节区和节区表到底减少了多少字节?减幅达到了多少?
Quote: |
$ echo "6442-708" | bc # 5734 $ echo "(6442-708)/6442" | bc -l .89009624340266997826
|
减少了5734多字节,减幅将近90%,这说明:对于一个简短的hello.c程序而言,gcc引入了将近90%的对程序运行没有影响的数据。虽然通过删
除节区和节区表,使得最终的文件只有708字节,但是打印一个"Hello World"真的需要这么多字节么?
事实上未必,因为:
1、打印一段Hello World字符串,我们无须调用printf,也就无须包含动态连接库,因此.interp,.dynamic等节区又可以去掉。为什么?我们可以直接使用系统调用(sys_write)来打印字符串。
2、另外,我们无须把Hello World字符串存放到可执行文件中?而是让用户把它当作参数输入。
下面,继续进行可执行文件的“减肥”。
4、用汇编语言来重写"Hello World"
先来看看gcc默认产生的汇编代码情况。通过gcc的-S选项可得到汇编代码。
Quote: |
$ cat hello.c #这个是使用_exit和printf函数的版本 #include <stdio.h> /* printf */ #include <unistd.h> /* _exit */
int main() { printf("Hello World\n"); _exit(0); } $ gcc -S hello.c #生成汇编 $ cat hello.s #这里是汇编代码 .file "hello.c" .section .rodata .LC0: .string "Hello World" .text .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, (%esp) call _exit .size main, .-main .ident "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)" .section .note.GNU-stack,"",@progbits $ gcc -o hello hello.s #看看默认产生的代码大小 $ wc -c hello 6523 hello
|
现在对汇编代码(hello.s)进行简单的处理得到,
Code:
[Ctrl+A Select All]
再编译看看,
Quote: |
$ gcc -o hello.o hello.s $ wc -c hello
6443 hello $ echo "6523-6443" | bc #仅仅减少了80个字节 80
|
如果不采用默认编译呢并且删除掉对程序运行没有影响的节区和节区表呢?
Quote: |
$ sed -i -e "s/main/_start/g" hello.s #因为没有初始化,所以得直接进入代码,替换main为_start $ as -o hello.o hello.s $ ld -o hello hello.o --dynamic-linker /lib/ld-linux.so.2 -L /usr/lib -lc $ ./hello hello world! $ wc -c hello 1812 hello $ echo "6443-1812" | bc -l #和之前的实验类似,也减少了4k左右 4631 $ readelf -l hello | grep "\ [0-9][0-9]\ " 00 01 .interp 02 .interp .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.plt .plt .text 03 .dynamic .got.plt 04 .dynamic $ strip -R .hash hello $ strip -R .gnu.version hello $ wc -c hello 1200 hello $ sstrip hello $ wc -c hello #这个结果比之前的708(在删除所有垃圾信息以后)个字节少了708-676,即32个字节 676 hello $ ./hello Hello World
|
容易发现这32字节可能跟节区.rodata有关系,因为刚才在链接完以后查看节区信息时,并没有.rodata节区。
前面提到,实际上还可以不用动态连接库中的printf函数,也不用直接调用_exit,而是在汇编里头使用系统调用,这样就可以去掉和动态连接库关联的内容。(如果想了解如何在汇编中使用系统调用,请参考资料[9])。使用系统调用重写以后得到如下代码,
Code:
[Ctrl+A Select All]
现在编译就不再需要动态链接器ld-linux.so了,也不再需要链接任何库。
Quote: |
$ as -o hello.o hello.s $ ld -o hello hello.o $ readelf -l hello
Elf file type is EXEC (Executable file) Entry point 0x8048062 There are 1 program headers, starting at offset 52
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x0007b 0x0007b R E 0x1000
Section to Segment mapping: Segment Sections... 00 .text $ sstrip hello $ ./hello #完全可以正常工作 Hello World $ wc -c hello 123 hello $ echo "676-123" | bc #相对于之前,已经只需要123个字节了,又减少了553个字节 553
|
可以看到效果很明显,只剩下一个LOAD段,它对应.text节区。
不过是否还有办法呢?把Hello World作为参数输入,而不是硬编码在文件中。所以如果处理参数的代码少于Hello World字符串的长度,那么就可以达到减少目标文件大小的目的。
先来看一个能够打印程序参数的汇编语言程序,它来自参考资料[9]。
Code:
[Ctrl+A Select All]
编译看看效果,
Quote: |
$ as -o args.o args.s $ ld -o args args.o $ ./args "Hello World" #能够打印输入的字符串,不错 ./args Hello World $ sstrip args $ wc -c args #处理以后只剩下130字节 130 args
|
可以看到,这个程序可以接收用户输入的参数并打印出来,不过得到的可执行文件为130字节,比之前的123个字节还多了7个字节,看看还有改进么?分析上面的代码后,发现,原来的代码有些地方可能进行优化,优化后得到如下代码。
Code:
[Ctrl+A Select All]
再测试(记得先重新汇编、链接并删除没用的节区和节区表)。
Quote: |
$ wc -c hello 124 hello
|
现在只有124个字节,不过还是比123个字节多一个,还有什么优化的办法么?
先来看看目前hello的功能,感觉不太符合要求,因为只需要打印Hello
World,所以不必处理所有的参数,仅仅需要接收并打印一个参数就可以。这样的话,把jmp vnext(2字节)这个循环去掉,然后在第一个pop
%ecx语句之前加一个pop %ecx(1字节)语句就可以。
Quote: |
.global _start _start: popl %ecx popl %ecx #弹出argc[0]的地址 popl %ecx #弹出argv[1]的地址 test %ecx, %ecx jz exit movl %ecx, %ebx xorl %edx, %edx strlen: movb (%ebx), %al inc %edx inc %ebx test %al, %al jnz strlen movb $10, -1(%ebx) xorl %eax, %eax movb $4, %al xorl %ebx, %ebx incl %ebx int $0x80 exit: xorl %eax, %eax movl %eax, %ebx incl %eax int $0x80
|
现在刚好123字节,和原来那个代码大小一样,不过仔细分析,还是有减少代码的余地:因为在这个代码中,用了一段额外的代码计算字符串的长度,实际上如果
仅仅需要打印Hello
World,那么字符串的长度是固定的,即12。所以这段代码可去掉,与此同时测试字符串是否为空也就没有必要(不过可能影响代码健壮性!),当然,为了
能够在打印字符串后就换行,在串的末尾需要加一个回车($10)并且设置字符串的长度为12+1,即13,
Code:
[Ctrl+A Select All]
再看看效果,
Quote: |
$ wc -c hello 111 hello
|
现在只剩下111字节,比刚才少了12字节。貌似到了极限?还有措施么?
还有,仔细分析发现:系统调用sys_exit和sys_write都用到了eax和ebx寄存器,它们之间刚好有那么一点巧合:
1、sys_exit调用时,eax需要设置为1,ebx需要设置为0。
2、sys_write调用时,ebx刚好是1。
因此,如果在sys_exit调用之前,先把ebx复制到eax中,再对ebx减一,则可减少两个字节。
不过,因为标准输入、标准输出和标注错误都指向终端,如果往标准输入写入一些东西,它还是会输出到标准输出上,所以在上述代码中如果在sys_write
之前ebx设置为0,那么也可正常往屏幕上打印Hell
World,这样的话,sys_exit调用前就没必要修改ebx,而仅需把eax设置为1,这样就可减少3个字节。
Code:
[Ctrl+A Select All]
看看效果,
Quote: |
$ wc -c hello 108 hello
|
现在看一下纯粹的指令还有多少?
Quote: |
$ readelf -h hello | grep Size Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Size of section headers: 0 (bytes) $ echo "108-52-32" | bc 24
|
纯粹的指令只有24个字节了,还有办法再减少目标文件的大小么?如果看了参考资料[1],看样子你又要蠢蠢欲动了:这24个字节是否可以插入到文件头部或程序头部?如果可以那是否意味着还可减少可执行文件的大小呢?现在来比较一下这三部分的十六进制内容。
Quote: |
$ hexdump -C hello -n 52 #文件头(52bytes) 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00 |........T...4...| 00000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 00 00 |........4. .....| 00000030 00 00 00 00 |....| 00000034 $ hexdump -C hello -s 52 -n 32 #程序头(32bytes) 00000034 01 00 00 00 00 00 00 00 00 80 04 08 00 80 04 08 |................| 00000044 6c 00 00 00 6c 00 00 00 05 00 00 00 00 10 00 00 |l...l...........| 00000054 $ hexdump -C hello -s 84 #实际代码部分(24bytes) 00000054 59 59 59 c6 41 0c 0a 31 d2 b2 0d 31 c0 b0 04 31 |YYY.A..1...1...1| 00000064 db cd 80 31 c0 40 cd 80 |...1.@..| 0000006c
|
从上面结果发现ELF文件头部和程序头部还有好些空洞(0),是否可以通过引入跳转指令把24个字节分散放入到那些空洞里或者是直接覆盖掉那些系统并不关
心的内容?抑或是把代码压缩以后放入可执行文件中,并在其中实现一个解压缩算法?还可以是通过一些代码覆盖率测试工具(gcov,prof)对你的代码进
行优化?这个作为我们共同的练习吧!
由于时间关系,这里不再进一步讨论,如果想进一步研究,请阅读参考资料[1],它更深层次地讨论了ELF文件,特别是Linux系统对ELF文件头部和程序头部的解析。
到这里,关于可执行文件的讨论暂且结束,最后来一段小小的总结,那就是我们设法去减少可执行文件大小的意义?
实际上,通过这样一个讨论深入到了很多技术的细节,包括可执行文件的格式、目标代码链接的过程、Linux下汇编语言开发等。与此同时,可执行文件大小的
减少本身对嵌入式系统非常有用,如果删除那些对程序运行没有影响的节区和节区表将减少目标系统的大小,适应嵌入式系统资源受限的需求。除此之外,动态连接
库中的很多函数可能不会被使用到,因此也可以通过某种方式剔除[8][10]。
或许,你还会发现更多有趣的意义,欢迎给我发送邮件,一起讨论。
参考资料:
[1] A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
[2] UNIX/LINUX 平台可执行文件格式分析
http://blog.chinaunix.net/u/19881/showart_215242.html
[3] C/C++程序编译步骤详解
http://www.xxlinux.com/linux/article/development/soft/20070424/8267.html
[4] The Linux GCC HOW TO
http://www.faqs.org/docs/Linux-HOWTO/GCC-HOWTO.html
[5] ELF: From The Programmer's Perspective
http://linux.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html
[6] Understanding ELF using readelf and objdump
http://www.linuxforums.org/misc/understanding_elf_using_readelf_and_objdump.html
[7] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html
[8] 嵌入式Linux小型化技术
http://www.gexin.com.cn/UploadFile/document2008119102415.pdf
[9] Linux汇编语言开发指南
http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
[10] Library Optimizer
http://sourceforge.net/projects/libraryopt
[11] ELF file format and ABI
http://www.x86.org/ftp/manuals/tools/elf.pdf
http://www.muppetlabs.com/~breadbox/software/ELF.txt
(北大OS实验室)http://162.105.203.48/web/gaikuang/submission/TN05.ELF.Format.Summary.pdf
(alert7 大牛翻译)http://www.xfocus.net/articles/200105/174.html