2007年5月30日星期三

微软最新推出一万美元咖啡桌式超酷电脑





see the video at http://www.youtube.com/v/Cog8b8ojji0
so cool,new technology new life

2007年5月16日星期三

C/C++ 编译器和调试器以及静态库、动态库使用汇总(zz)

http://www.chinaunix.net 作者:蓝色键盘 发表于:2003-05-09 14:01:19

经常的,有朋友问到有关unix下面调试的技术。我整理了大多数的unix系统下面的常用的调试工具的调试技术的文章。希望对大家有所帮助。

另外静态库、动态库也是问的频率比较高的问题。在这里也做了总结。

---------- 大多数unix系统下面的调试器的使用方法如下 ----------

gdb介绍
GNU 的调试器称为 gdb,该程序是一个交互式工具,工作在字符模式。在X-Window 系统中,有一个 gdb 的前端图形工具,称为 xxgdb。gdb 是功能强大的调试程序,可完成如下的调试任务:

* 设置断点
* 监视程序变量的值
* 程序的单步执行
* 修改变量的值

在可以使用 gdb 调试程序之前,必须使用 -g 选项编译源文件。可在makefile 中如下定义 CFLAGS 变量:

CFLAGS = -g

运行 gdb 调试程序时通常使用如下的命令:

gdb progname

在 gdb 提示符处键入help,将列出命令的分类,主要的分类有:
* aliases:命令别名
* breakpoints:断点定义;
* data:数据查看;
* files:指定并查看文件;
* internals:维护命令;
* running:程序执行;
* stack:调用栈查看;
* statu:状态查看;
* tracepoints:跟踪程序执行。
键入 help 后跟命令的分类名,可获得该类命令的详细清单。

gdb 的常用命令
命令 解释
break NUM 在指定的行上设置断点。
bt 显示所有的调用栈帧。该命令可用来显示函数的调用顺序。
clear 删除设置在特定源文件、特定行上的断点。其用法为:clear FILENAME:NUM。
continue 继续执行正在调试的程序。该命令用在程序由于处理信号或断点而
导致停止运行时。
display EXPR 每次程序停止后显示表达式的值。表达式由程序定义的变量组成。
file FILE 装载指定的可执行文件进行调试。
help NAME 显示指定命令的帮助信息。
info break 显示当前断点清单,包括到达断点处的次数等。
info files 显示被调试文件的详细信息。
info func 显示所有的函数名称。
info local 显示当函数中的局部变量信息。
info prog 显示被调试程序的执行状态。
info var 显示所有的全局和静态变量名称。
kill 终止正被调试的程序。
list 显示源代码段。
make 在不退出 gdb 的情况下运行 make 工具。
next 在不单步执行进入其他函数的情况下,向前执行一行源代码。
print EXPR 显示表达式 EXPR 的值。

******gdb 使用范例************************
-----------------
清单 一个有错误的 C 源程序 bugging.c
-----------------
#include
#include

static char buff [256];
static char* string;
int main ()
{

printf ("Please input a string: ");
gets (string);

printf ("\nYour string is: %s\n", string);
}
-----------------
上面这个程序非常简单,其目的是接受用户的输入,然后将用户的输入打印出来。该程序使用了一个未经过初
始化的字符串地址 string,因此,编译并运行之后,将出现 Segment Fault 错误:
$ gcc -o test -g test.c
$ ./test
Please input a string: asfd
Segmentation fault (core dumped)
为了查找该程序中出现的问题,我们利用 gdb,并按如下的步骤进行:
1.运行 gdb bugging 命令,装入 bugging 可执行文件;
2.执行装入的 bugging 命令;
3.使用 where 命令查看程序出错的地方;
4.利用 list 命令查看调用 gets 函数附近的代码;
5.唯一能够导致 gets 函数出错的因素就是变量 string。用 print 命令查看 string 的值;
6.在 gdb 中,我们可以直接修改变量的值,只要将 string 取一个合法的指针值就可以了,为此,我们在第
11 行处设置断点;
7.程序重新运行到第 11 行处停止,这时,我们可以用 set variable 命令修改 string 的取值;
8.然后继续运行,将看到正确的程序运行结果。

运行 gcc/egcs

**********运行 gcc/egcs***********************
GCC 是 GNU 的 C 和 C++ 编译器。实际上,GCC 能够编译三种语言:C、C++ 和 Object C(C 语言的一种面向对象扩展)。利用 gcc 命令可同时编译并连接 C 和 C++ 源程序。

如果你有两个或少数几个 C 源文件,也可以方便地利用 GCC 编译、连接并生成可执行文件。例如,假设你有
两个源文件 main.c 和 factorial.c 两个源文件,现在要编译生成一个计算阶乘的程序。
清单 factorial.c
-----------------------
#include
#include

int factorial (int n)
{
if (n <= 1)
return 1;

else
return factorial (n - 1) * n;
}
-----------------------

-----------------------
清单 main.c
-----------------------
#include
#include

int factorial (int n);

int main (int argc, char **argv)
{
int n;

if (argc < 2) {
printf ("Usage: %s n\n", argv [0]);
return -1;
}
else {
n = atoi (argv[1]);
printf ("Factorial of %d is %d.\n", n, factorial (n));
}

return 0;
}
-----------------------
利用如下的命令可编译生成可执行文件,并执行程序:
$ gcc -o factorial main.c factorial.c
$ ./factorial 5
Factorial of 5 is 120.

GCC 可同时用来编译 C 程序和 C++ 程序。一般来说,C 编译器通过源文件的后缀名来判断是 C 程序还是 C+
+ 程序。在 Linux 中,C 源文件的后缀名为 .c,而 C++ 源文件的后缀名为 .C 或 .cpp。
但是,gcc 命令只能编译 C++ 源文件,而不能自动和 C++ 程序使用的库连接。因此,通常使用 g++ 命令来完
完成 C++ 程序的编译和连接,该程序会自动调用 gcc 实现编译。
假设我们有一个如下的 C++ 源文件(hello.C):

#include

void main (void)
{
cout << "Hello, world!" << endl;
}

则可以如下调用 g++ 命令编译、连接并生成可执行文件:

$ g++ -o hello hello.C
$ ./hello
Hello, world!

**********************gcc/egcs 的主要选项*********
gcc 命令的常用选项
选项 解释
-ansi 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色,
例如 asm 或 typeof 关键词。
-c 只编译并生成目标文件。
-DMACRO 以字符串“1”定义 MACRO 宏。
-DMACRO=DEFN 以字符串“DEFN”定义 MACRO 宏。
-E 只运行 C 预编译器。
-g 生成调试信息。GNU 调试器可利用该信息。
-IDIRECTORY 指定额外的头文件搜索路径DIRECTORY。
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY。
-lLIBRARY 连接时搜索指定的函数库LIBRARY。
-m486 针对 486 进行代码优化。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-static 禁止使用共享连接。
-UMACRO 取消对 MACRO 宏的定义。
-w 不生成任何警告信息。
-Wall 生成所有警告信息。

#######SCO UNIX下面dbaxtra的调试技术#########
在sco unix下编程大多离不开C语言,即使是数据库应用也有很多是与c搭配使用的,例如informix esql/c 就可以在c语言中嵌入sql 语句。很多人认为在unix下写程序是件很痛苦的事情,其中一个很重要原因是不知道在unix下怎样调试程序。其实在sco unix源码调试器是dbxtra或dbXtra,linux下是gdb。它们类似turbo c的调试器,可以跟踪源码变量。在unix 下调试程序有如下传统方法

---- 一、在要调试语句之前,输出要调试的变量,利用printf()函数。

---- 二、写日志文件,把结果输出到文件中避免屏幕混乱,利用fprintf()函数。

---- 三、利用sco 内置调试器dbxtra或dbXtra。

---- dbxtra 适用字符界面,在sco unix的图形界面用dbXtra。(编按:请注意大小写)

以下是dbxtra基本命令:
c cont 在断点后继续执行
d delete 删除所设断点
h help 帮助
e edit 编辑源程序
n next 源程序区的内容向下翻一屏。
p print 显示变量
q quit 退出dbxtra
r run 运行程序,直到遇上设置的断点
rr rerun 再次运行
s step 单步运行
st stop 设置断点
j status 显示当前断点
t where 显示当前状态,列出所有设置的变量值
di display 开显示窗,用于查看变量
ud undisplay 删除显示窗的条目
f forward 源程序区的内容向上 翻一屏。
B backward 源程序区的内容向下 翻一屏。
Stopi stop inst 设置断点
tracei trace inst跟踪子程序

dbxtra [options] [objectfile ]

---- dbxtra 在启动时有个参数-Idir值得一提.我们在编写一个较大程序的时候,通常源程序和编译生成的可执行文件都放在不同的目录中,这样便于管理。默认 dbxtra将在可执行文件所在的目录下找匹配c的源程序。当我们启动时,指定-I参数,dbxtra就会到我们指定的目录下找匹配的c程序。 例如:
---- dbxtra -I"\work\c" program1

---- 源程序在用cc编译时要带上-g 参数,这样是加上符号表等调试信息。只有这样编译过的文件,dbxtra才可以调试。调试信息使源代码和机器码关联。

---- 下面这个C程序输出结果和我们的预想结果不一样,说明某些地方有错误。我们用调试器来调试它:

---- 程序一:

t.c
main()
{ int i=10 ,*p1;
float j=1.5,*p2;
p1=&
p2=&
p2=p1;
printf("%d,%d\n",*p1,*p2);
}

首先带上-g参数编译 cc -g -o t t.c
启动调试器 dbxtra t
屏幕显示:
1.main()
2.{ int i=10 ,*p1;
3. float j=1.5,*p2;
4. p1=&
5. p2=&
6. p2=p1;
7. printf("%d,%d\n",*p1,*p2);
8.}
C[browse] File:t.c Func.-
Readubg symbolic information
Type 'help' for help
(dbxtra)
(dbxtra)

设置断点:
(dbxtra)stop at 5
运行:
(dbxtra) run
程序自动在第5行停下。
这时我们可以看变量的值。
(dbxtra) print *p1

单步执行。
(dbxtra) step
程序将执行第5行源码,指针将移到第6行。
(dbxtra) print *p2

(dbxtra) step
程序执行了第6行源码后,将指针移到第7行。
(dbxtra) print *p1 , *p2

---- 我们发现 在执行了第6行源码后,*p1,*p2的值就不对了,所以问题就出在第6行上。仔细检查后发现指针p1指向整型,指针p2指向实型。它们之间的赋值要进行强制类型转换。这种错误在C程序中是很常见的。
---- 有时我们在调试一些程序时,要在整个程序运行中时刻监视莫些变量的值,例如程序一中我们要时刻了解*p1,*p2的值,除了在每一行程序执行完后,打print *p1,*p2外,还可以开一个显示窗口。

---- (dbxtra)display *p1,*p2

---- 用undisplay 删掉不想要的变量。

---- 有些程序运行时要带参数,mycat /etc/passwd 在调试时候

---- (dbxtra) run '/etc/passwd'

---- 再运行时,无需再写一遍参数。

---- (dbxtra) rerun

---- 在涉及到curses库编程或屏幕有大量的人机界面时,为了调试方便,我们可以把程序输出结果重定向到个虚屏。

---- (dbxtra) run >/dev/tty03

---- 当然要先把tty03 disable 掉。(disable tty03)

#######创建和使用静态库#########

详细的使用情况,请大家man手册,这里只介绍一下。静态库相对的比较简单。

创建一个静态库是相当简单的。通常使用 ar 程序把一些目标文件(.o)组合在一起,
成为一个单独的库,然后运行 ranlib,以给库加入一些索引信息。

########创建和使用共享库#########
特殊的编译和连接选项
-D_REENTRANT 使得预处理器符号 _REENTRANT 被定义,这个符号激活一些宏特性。
-fPIC 选项产生位置独立的代码。由于库是在运行的时候被调入,因此这个
选项是必需的,因为在编译的时候,装入内存的地址还不知道。如果
不使用这个选项,库文件可能不会正确运行。
-shared 选项告诉编译器产生共享库代码。
-Wl,-soname -Wl 告诉编译器将后面的参数传递到连接器。而 -soname 指定了
共享库的 soname。
# 可以把库文件拷贝到 /etc/ld.so.conf 中列举出的任何目录中,并以
root 身份运行 ldconfig;或者
# 运行 export LD_LIBRARY_PATH='pwd',它把当前路径加到库搜索路径中去。

#######使用高级共享库特性#########

1. ldd 工具
ldd 用来显示执行文件需要哪些共享库, 共享库装载管理器在哪里找到了需要的共享库.

2. soname

共享库的一个非常重要的,也是非常难的概念是 soname——简写共享目标名(short for shared object name)。这是一个为共享库(.so)文件而内嵌在控制数据中的名字。如前面提到的,每一个程序都有一个需要使用的库的清单。这个清单的内容是一系列库的 soname,如同 ldd 显示的那样,共享库装载器必须找到这个清单。

soname 的关键功能是它提供了兼容性的标准。当要升级系统中的一个库时,并且新库的 soname 和老的库的 soname 一样,用旧库连接生成的程序,使用新的库依然能正常运行。这个特性使得在 Linux 下,升级使用共享库的程序和定位错误变得十分容易。

在 Linux 中,应用程序通过使用 soname,来指定所希望库的版本。库作者也可以通过保留或者改变 soname 来声明,哪些版本是相互兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。

查看/usr/local/lib 目录,分析 MiniGUI 的共享库文件之间的关系

3. 共享库装载器

当程序被调用的时候,Linux 共享库装载器(也被称为动态连接器)也自动被调用。它的作用是保证程序所需要的所有适当版本的库都被调入内存。共享库装载器名字是 ld.so 或者是 ld-linux.so,这取决于 Linux libc 的版本,它必须使用一点外部交互,才能完成自己的工作。然而它接受在环境变量和配置文件中的配置信息。

文件 /etc/ld.so.conf 定义了标准系统库的路径。共享库装载器把它作为搜索路径。为了改变这个设置,必须以 root 身份运行 ldconfig 工具。这将更新 /etc/ls.so.cache 文件,这个文件其实是装载器内部使用的文件之一。

可以使用许多环境变量控制共享库装载器的操作(表1-4+)。

表 1-4+ 共享库装载器环境变量
变量 含义
LD_AOUT_LIBRARY_PATH 除了不使用 a.out 二进制格式外,与 LD_LIBRARY_PATH 相同。
LD_AOUT_PRELOAD 除了不使用 a.out 二进制格式外,与 LD_PRELOAD 相同。
LD_KEEPDIR 只适用于 a.out 库;忽略由它们指定的目录。
LD_LIBRARY_PATH 将其他目录加入库搜索路径。它的内容应该是由冒号
分隔的目录列表,与可执行文件的 PATH 变量具有相同的格式。
如果调用设置用户 ID 或者进程 ID 的程序,该变量被忽略。
LD_NOWARN 只适用于 a.out 库;当改变版本号是,发出警告信息。
LD_PRELOAD 首先装入用户定义的库,使得它们有机会覆盖或者重新定义标准库。
使用空格分开多个入口。对于设置用户 ID 或者进程 ID 的程序,
只有被标记过的库才被首先装入。在 /etc/ld.so.perload 中指定
了全局版本号,该文件不遵守这个限制。

4. 使用 dlopen

另外一个强大的库函数是 dlopen()。该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。比如 Apache Web 服务器利用这个函数在运行过程中加载模块,这为它提供了额外的能力。一个配置文件控制了加载模块的过程。这种机制使得在系统中添加或者删除一个模块时,都不需要重新编译了。

可以在自己的程序中使用 dlopen()。dlopen() 在 dlfcn.h 中定义,并在 dl 库中实现。它需要两个参数:一个文件名和一个标志。文件名可以是我们学习过的库中的 soname。标志指明是否立刻计算库的依赖性。如果设置为 RTLD_NOW 的话,则立刻计算;如果设置的是 RTLD_LAZY,则在需要的时候才计算。另外,可以指定 RTLD_GLOBAL,它使得那些在以后才加载的库可以获得其中的符号。

当库被装入后,可以把 dlopen() 返回的句柄作为给 dlsym() 的第一个参数,以获得符号在库中的地址。使用这个地址,就可以获得库中特定函数的指针,并且调用装载库中的相应函数。

一、编写合格的动态链接库头文件

C语言的头文件,可供一个或多个程序引用,里面一般定义程序所需的常量,自定义类型及函数原型说明等.其中的函数原型说明,则供编译器检查语法,用于排除引用参数时类型不一致的错误.只有编写合格的动态链接库头文件,程序员才能正确使用动态链接库内的函数.

动态链接库头文件要采用C语言标准格式,其中的动态函数原型定义,不必象上文介绍的那样用(*动态函数名)的描述形式.请看下面的例子每行开始的数字为所在行行号,为笔者添加,供注解使用)

1 /* adatetime.h : 纵横软件制作中心雨亦奇(zhsoft@371.net)编写, 2002-03-06. */
2
3 #ifndef __DATETIME_H
4
5 #define __DATETIME_H
6
7 /* 日期结构 */
8 typedef struct
9 {
10 int year;
11 int mon;
12 int day;
13 }DATETYPE;
14
15 /* 时间结构 */
16 typedef struct
17 {
18 char hour;
19 char min;
20 char sec;
21 }TIMETYPE;
22
23 int getdate(DATETYPE *d); /* 取当前日期 */
24 int gettime(TIMETYPE *t); /* 取当前时间 */
25
26 #endif
27

注:与上文的datetime.h文件比较,从该头文件第23,24行可以看到,动态函数getdate,gettime的原型定义改变了,不再使用(*getdate),(*gettime)的格式了(这种格式使用较为罗嗦).

二、正确编译与命名动态链接库

为了让GCC编译器生成动态链接库,编译时须加选项-shared.(这点须牢记)

LINUX系统中,为了让动态链接库能被系统中其它程序共享,其名字应符合“lib*.so*”这种格式.如果某个动态链接库不符合此格式,则LINUX的动态链接库自动装入程序(ld.so)将搜索不到此链接库,其它程序也无法共享之.

格式中,第一个*通常表示为简写的库名,第二个*通常表示为该库的版本号.如:在我的系统中,基本C动态链接库的名字为libc.so.6,线程 pthread动态链接库的名字为libpthread.so.0等等.本文例子所生成的动态链接库的名字为libmy.so,虽没有版本号,但也符合所要求的格式.

生成该动态链接库的维护文件makefile-lib内容如下:

1 # makefile : 纵横软件制作中心雨亦奇编写, 2002-03-07.
2
3 all : libmy.so
4
5 SRC = getdate.c gettime.c
6
7 TGT = $(SRC:.c=.o)
8
9 $(SRC) : adatetime.h
10 @touch $@
11
12 %.o : %.c
13 cc -c $?
14
15 # 动态链接库(libmy.so)生成
16 libmy.so : $(TGT)
17 cc -s -shared -o $@ $(TGT)
18

运行命令:

$ make -f makefile-lib
$

即生成libmy.so库.

注: 维护文件中,第17行用-shared选项以生成动态链接库,用-s选项以去掉目标文件中的符号表,从而减小文件长度.

三、共享动态链接库

3.1 动态链接库配置文件

为了让动态链接库为系统所使用,需要维护动态链接库的配置文件--/etc/ld.so.conf.此文件内,存放着可被LINUX共享的动态链接库所在目录的名字(系统目录/lib,/usr/lib除外),各个目录名间以空白字符(空格,换行等)或冒号或逗号分隔.一般的LINUX发行版中,此文件均含一个共享目录/usr/X11R6/lib,为X window窗口系统的动态链接库所在的目录.

下面看看我的系统中此文件的内容如何:

# cat /etc/ld.so.conf
/usr/X11R6/lib
/usr/zzz/lib
#

由上可以看出,该动态库配置文件中,增加了一个/usr/zzz/lib目录.这是我自己新建的共享库目录,下面存放我新开发的可供系统共享的动态链接库.

3.2 动态链接库管理命令

为了让动态链接库为系统所共享,还需运行动态链接库的管理命令--ldconfig.此执行程序存放在/sbin目录下.

ldconfig 命令的用途,主要是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(格式如前介绍,lib*.so*),进而创建出动态装入程序(ld.so)所需的连接和缓存文件.缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表.

ldconfig通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令.

ldconfig命令行用法如下:

ldconfig [-v|--verbose] [-n] [-N] [-X] [-f CONF] [-C CACHE] [-r ROOT] [-l] [-p|--print-cache] [-c FORMAT] [--format=FORMAT] [-V] [-?|--help|--usage] path...

ldconfig可用的选项说明如下:

(1) -v或--verbose : 用此选项时,ldconfig将显示正在扫描的目录及搜索到的动态链接库,还有它所创建的连接的名字.

(2) -n : 用此选项时,ldconfig仅扫描命令行指定的目录,不扫描默认目录(/lib,/usr/lib),也不扫描配置文件/etc/ld.so.conf所列的目录.

(3) -N : 此选项指示ldconfig不重建缓存文件(/etc/ld.so.cache).若未用-X选项,ldconfig照常更新文件的连接.

(4) -X : 此选项指示ldconfig不更新文件的连接.若未用-N选项,则缓存文件正常更新.

(5) -f CONF : 此选项指定动态链接库的配置文件为CONF,系统默认为/etc/ld.so.conf.

(6) -C CACHE : 此选项指定生成的缓存文件为CACHE,系统默认的是/etc/ld.so.cache,此文件存放已排好序的可共享的动态链接库的列表.

(7) -r ROOT : 此选项改变应用程序的根目录为ROOT(是调用chroot函数实现的).选择此项时,系统默认的配置文件/etc/ld.so.conf,实际对应的为 ROOT/etc/ld.so.conf.如用-r /usr/zzz时,打开配置文件/etc/ld.so.conf时,实际打开的是/usr/zzz/etc/ld.so.conf文件.用此选项,可以大大增加动态链接库管理的灵活性.

( -l : 通常情况下,ldconfig搜索动态链接库时将自动建立动态链接库的连接.选择此项时,将进入专家模式,需要手工设置连接.一般用户不用此项.

(9) -p或--print-cache : 此选项指示ldconfig打印出当前缓存文件所保存的所有共享库的名字.

(10) -c FORMAT 或 --format=FORMAT : 此选项用于指定缓存文件所使用的格式,共有三种ld(老格式),new(新格式)和compat(兼容格式,此为默认格式).

(11) -V : 此选项打印出ldconfig的版本信息,而后退出.

(12) -? 或 --help 或 --usage : 这三个选项作用相同,都是让ldconfig打印出其帮助信息,而后退出.

举三个例子:

例1:

# ldconfig -p
793 libs found in cache `/etc/ld.so.cache'
libzvt.so.2 (libc6) => /usr/lib/libzvt.so.2
libzvt.so (libc6) => /usr/lib/libzvt.so
libz.so.1.1.3 (libc6) => /usr/lib/libz.so.1.1.3
libz.so.1 (libc6) => /lib/libz.so.1
......
#

注: 有时候用户想知道系统中有哪些动态链接库,或者想知道系统中有没有某个动态链接库,这时,可用-p选项让ldconfig输出缓存文件中的动态链接库列表,从而查询得到.例子中,ldconfig命令的输出结果第1行表明在缓存文件/etc/ld.so.cache中找到793个共享库,第2行开始便是一系列共享库的名字及其全名(绝对路径).因为实际输出结果太多,为节省篇幅,以......表示省略的部分.

例2:

# ldconfig -v
/lib:
liby.so.1 -> liby.so.1
libnss_wins.so -> libnss_wins.so
......
/usr/lib:
libjscript.so.2 -> libjscript.so.2.0.0
libkspell.so.2 -> libkspell.so.2.0.0
......
/usr/X11R6/lib:
libmej-0.8.10.so -> libmej-0.8.10.so
libXaw3d.so.7 -> libXaw3d.so.7.0
......
#

注: ldconfig命令在运行正常的情况下,默认不输出什么东西.本例中用了-v选项,以使ldconfig在运行时输出正在扫描的目录及搜索到的共享库, 用户可以清楚地看到运行的结果.执行结束后,ldconfig将刷新缓存文件/etc/ld.so.cache.

例3:

# ldconfig /usr/zhsoft/lib
#

注: 当用户在某个目录下面创建或拷贝了一个动态链接库,若想使其被系统共享,可以执行一下"ldconfig 目录名"这个命令.此命令的功能在于让ldconfig将指定目录下的动态链接库被系统共享起来,意即:在缓存文件/etc/ld.so.cache中追加进指定目录下的共享库.本例让系统共享了/usr/zhsoft/lib目录下的动态链接库.需要说明的是,如果此目录不在/lib,/usr/lib 及/etc/ld.so.conf文件所列的目录里面,则再度运行ldconfig时,此目录下的动态链接库可能不被系统共享了.

3.3 动态链接库如何共享

了解了以上知识,我们可以采用以下三种方法来共享动态链接库注:均须在超级用户状态下操作,以我的动态链接库libmy.so共享过程为例)

(1) 拷贝动态链接库到系统共享目录下,或在系统共享目录下为该动态链接库建立个连接(硬连接或符号连接均可,常用符号连接).这里说的系统共享目录,指的是LINUX动态链接库存放的目录,它包含/lib,/usr/lib以及/etc/ld.so.conf文件内所列的一系列目录.

# cp libmy.so /lib
# ldconfig
#

或:

# ln -s `pwd`/libmy.so /lib
# ldconfig
#

(2)将动态链接库所在目录名追加到动态链接库配置文件/etc/ld.so.conf中.

# pwd >> /etc/ld.so.conf
# ldconfig
#

(3)利用动态链接库管理命令ldconfig,强制其搜索指定目录,并更新缓存文件,便于动态装入.

# ldconfig `pwd`
#

需要说明的是,这种操作方法虽然有效,但效果是暂时的,供程序测试还可以,一旦再度运行ldconfig,则缓存文件内容可能改变,所需的动态链接库可能不被系统共享了.与之相比较,前两种方法是可靠的方法,值得业已定型的动态链接库共享时采用.前两种方法还有一个特点,即最后一条命令都是 ldconfig,也即均需要更新一下缓存文件,以确保动态链接库的共享生效.

四、含有动态函数的程序的编译

4.1 防止编译因未指定动态链接库而出错

当一个程序使用动态函数时,编译该程序时就必须指定含所用动态函数的动态链接库,否则编译将会出错退出.如本文示例程序ady.c的编译(未明确引用动态链接库libmy.so):

# cc -o ady ady.c
/tmp/ccL4FsJp.o: In function `main':
/tmp/ccL4FsJp.o(.text+0x43): undefined reference to `gettime'
collect2: ld returned 1 exit status
#

注: 因为ady.c所含的动态函数getdate,gettime不在系统函数库中,所以连接时出错.

4.2 编译时引用动态链接库的几种方式

(1)当所用的动态链接库在系统目录(/lib,/usr/lib)下时,可用编译选项-l来引用.即:

# cc -lmy -o ady ady.c
#

注:编译时用-l选项引用动态链接库时,库名须使用其缩写形式.本例的my,表示引用libmy.so库.若引用光标库libncurses.so,须用-lncurses.注意,-l选项与参数之间不能有空格,否则会出错.

(2)当所用的动态链接库在系统目录(/lib,/usr/lib)以外的目录时,须用编译选项-L来指定动态链接库所在的目录(供编译器查找用),同时用-l选项指定缩写的动态链接库名.即:

# cc -L/usr/zzz/lib -lmy -o ady ady.c
#

(3)直接引用所需的动态链接库.即:

# cc -o ady ady.c libmy.so
#



# cc -o ady ady.c /lib/libmy.so
#

等等.其中,动态链接库的库名可以采用相对路径形式(文件名不以/开头),也可采用绝对路径形式(文件名以/开头).

五、动态链接程序的运行与检查

5.1 运行

编译连接好含动态函数的程序后,就可以运行它了.动态链接程序因为共享了系统中的动态链接库,所以其空间占用很小.但这并不意味功能的减少,它的执行与静态连接的程序执行,效果完全相同.在命令提示符下键入程序名及相关参数后回车即可,如下例:

$ ady
动态链接库高级应用示范
当前日期: 2002-03-11
当前时间: 19:39:06
$

5.2 检查

检查什么?检查动态链接程序究竟需要哪些共享库,系统中是否已有这些库,没有的话,用户好想办法把这些库装上.

怎么检查呢?这里,告诉你一个实用程序--ldd,这个程序就是专门用来检查动态链接程序依赖哪些共享库的.

ldd命令行用法如下:

ldd [--version] [-v|--verbose] [-d|--data-relocs] [-r|--function-relocs] [--help] FILE...

各选项说明如下:

(1) --version : 此选项用于打印出ldd的版本号.

(2) -v 或 --verbose : 此选项指示ldd输出关于所依赖的动态链接库的尽可能详细的信息.

(3) -d 或 --data-relocs : 此选项执行重定位,并且显示不存在的函数.

(4) -r 或 --function-relocs : 此选项执行数据对象与函数的重定位,同时报告不存在的对象.

(5) --help : 此选项用于打印出ldd的帮助信息.

注: 上述选项中,常用-v(或--verbose)选项.

ldd的命令行参数为FILE...,即一个或多个文件名(动态链接程序或动态链接库).

例1:

$ ldd ady
libmy.so => ./libmy.so (0x40026000)
libc.so.6 => /lib/libc.so.6 (0x40028000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
$

注: 每行=>前面的,为动态链接程序所需的动态链接库的名字,而=>后面的,则是运行时系统实际调用的动态链接库的名字,所需的动态链接库在系统中不存在时,=>后面将显示"not found",括号所括的数字为虚拟的执行地址.本例列出ady所需的三个动态链接库,其中libmy.so为自己新建的动态链接库,而 libc.so.6与/lib/ld-linux.so.2均为系统的动态链接库,前一个为基本C库,后一个动态装入库(用于动态链接库的装入及运行).

例2:

$ ldd -v ady
libmy.so => ./libmy.so (0x40026000)
libc.so.6 => /lib/libc.so.6 (0x40028000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

Version information:
./ady:
libc.so.6 (GLIBC_2.1.3) => /lib/libc.so.6
libc.so.6 (GLIBC_2.0) => /lib/libc.so.6
./libmy.so:
libc.so.6 (GLIBC_2.1.3) => /lib/libc.so.6
libc.so.6 (GLIBC_2.0) => /lib/libc.so.6
/lib/libc.so.6:
ld-linux.so.2 (GLIBC_2.1.1) => /lib/ld-linux.so.2
ld-linux.so.2 (GLIBC_2.2.3) => /lib/ld-linux.so.2
ld-linux.so.2 (GLIBC_2.1) => /lib/ld-linux.so.2
ld-linux.so.2 (GLIBC_2.2) => /lib/ld-linux.so.2
ld-linux.so.2 (GLIBC_2.0) => /lib/ld-linux.so.2
$

注:本例用-v选项以显示尽可能多的信息,所以例中除列出ady所需要的动态链接库外,还列出了程序所需动态链接库版本方面的信息.

小结: 在LINUX动态链接库的高级应用中,关键有两点,一是如何让动态链接库为LINUX系统所共享,二是编译连接程序时如何做.让动态链接库为系统所共享, 主要是用ldconfig管理命令,维护好系统共享库的缓存文件/etc/ld.so.cache.编译连接时如何做?注意连接上所用的动态链接库就可以了.LINUX动态链接库的高级应用,用一用就明白:其实,就是这么简单!

[1 楼] | Posted: 2005-08-24 10:35

ppking

级别: 论坛版主
精华: 1
发帖: 28
威望: 65 点
金钱: 136 RMB
贡献值: 0 点
注册时间:2005-08-16
最后登录:2005-08-26

********几种不同UNIX系统常用的动态连接库建立的参数说明*****

创建共享库和链接可执行文件类似:首先把源代码编译成目标文件, 然后把目标文件链接起来.目标文件需要创建成位置无关码(position-independent code) (PIC),概念上就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方, (用于可执行文件的目标文件通常不是用这个方式编译的.)链接动态库的命令包含特殊标志,与链接可执行文件的命令是有区别的. --- 至少理论上如此.在一些系统里的现实更恶心.

在下面的例子里,我们假设你的源程序代码在 foo.c 文件里并且将创建成名字叫 foo.so的共享库.中介的对象文件将叫做 foo.o,除非我们另外注明.一个共享库可以 包含多个对象文件,不过我们在这里只用一个.

BSD/OS
创建 PIC 的编译器标志是 -fpic.创建共享库的链接器标志是 -shared.

gcc -fpic -c foo.c
ld -shared -o foo.so foo.o
上面方法适用于版本 4.0 的 BSD/OS.

FreeBSD
创建 PIC 的编译器标志是 -fpic.创建共享库的链接器标志是 -shared.

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o
上面方法适用于版本 3.0 的 FreeBSD.

HP-UX
创建 PIC 的系统编译器标志是 +z.如果使用 GCC 则是 -fpic. 创建共享库的链接器标志是 -b.因此

cc +z -c foo.c


gcc -fpic -c foo.c
然后

ld -b -o foo.sl foo.o
HP-UX 使用 .sl 做共享库扩展,和其它大部分系统不同.

IRIX
PIC 是缺省,不需要使用特殊的编译器选项. 生成共享库的链接器选项是 -shared.

cc -c foo.c
ld -shared -o foo.so foo.o

Linux
创建 PIC 的编译器标志是 -fpic.在一些平台上的一些环境下, 如果 -fpic 不能用那么必须使用-fPIC. 参考 GCC 的手册获取更多信息. 创建共享库的编译器标志是 -shared.一个完整的例子看起来象:

cc -fpic -c foo.c
cc -shared -o foo.so foo.o

NetBSD
创建 PIC 的编译器标志是 -fpic.对于 ELF 系统, 带 -shared 标志的编译命令用于链接共享库. 在老的非 ELF 系统里,使用ld -Bshareable.

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

OpenBSD
创建 PIC 的编译器标志是 -fpic. ld -Bshareable 用于链接共享库.

gcc -fpic -c foo.c
ld -Bshareable -o foo.so foo.o

Solaris
创建 PIC 的编译器命令是用 Sun 编译器时为 -KPIC 而用 GCC 时为 -fpic.链接共享库时两个编译器都可以用 -G 或者用 GCC 时还可以是 -shared.

cc -KPIC -c foo.c
cc -G -o foo.so foo.o


gcc -fpic -c foo.c
gcc -G -o foo.so foo.o

Tru64 UNIX
PIC 是缺省,因此编译命令就是平常的那个. 带特殊选项的 ld 用于链接:

cc -c foo.c
ld -shared -expect_unresolved '*' -o foo.so foo.o
用 GCC 代替系统编译器时的过程是一样的;不需要特殊的选项.

UnixWare
SCO 编译器创建 PIC 的标志是-KPIC GCC 是 -fpic. 链接共享库时 SCO 编译器用 -G 而 GCC 用-shared.

cc -K PIC -c foo.c
cc -G -o foo.so foo.o
or

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

技巧: 如果你想把你的扩展模块打包,用在更广的发布中,那么你应该考虑使用 GNU Libtool 制作共享库.它把平台之间的区别封装成 了一个通用的并且非常强大的接口.严肃的包还要求考虑有关库版本, 符号解析方法和一些其他的问题.

生成的共享库文件然后就可以装载到 PostgreSQL里面去了.在给 CREATE FUNCTION 命令声明文件名的时候,我们必须声明共享库文件的名字而不是中间目标文件的名字.请注意你可以在 CREATE FUNCTION 命令上忽略 系统标准的共享库扩展 (通常是.so或.sl), 并且出于最佳的兼容性考虑也应该忽略.

[2 楼] | Posted: 2005-08-24 10:36

ppking

级别: 论坛版主
精华: 1
发帖: 28
威望: 65 点
金钱: 136 RMB
贡献值: 0 点
注册时间:2005-08-16
最后登录:2005-08-26

源程序的显示和搜索

程序出错一般来说不只是出错的那条语句本身造成的。事实上出现错误经常是前面或相关的代码执行了不正确的操作或少了某
些必要的处理。因此调试过程中经常要观察一下源程序中的语句,或者在程序中搜索某个符号出现在什么地方。其中字符串的
搜索功能同vi基本上是相同的,而文件的显示则同另外一个我们没有具体讨论的编辑器ed类似。下面我们将具体介绍这些命令。

1.源程序的显示

在用core进入sdb之后,在*提示符后输入w命令,该命令指示sdb显示源程序中的当前行为中心的前后10行的内容并保持当前行
不变:

* w

7:int

8: TestInput(char * ValueInput)

9: {while ( * ValueInput)

10: if (! isdigit( * ValueInput)) return (! TESTOK);

11: else ValueInput++;

12: return ((100/atoi(ValueInput))? TESTOK:! TESTOK);

13: }

*

我们看到,在进入sdb时,当前行是第12行,以该行为中心的10行内容正好就是上面所显示出来的。其他可以显示源程序语
句的sdb命令如下:

P 显示当前行
l 显示对应于当前指令的那条语句
Z 显示当前行开始的下面10条语句
Ctrl+D 显示当前行之后(不包括当前行)的第10条语句
n 显示第n条语句,这里n是一个数
注意这些命令显示出的是源程序语句还是汇编语句(后面我们将要介绍)取决于最近一次显示出的是什么。

2.改变当前行

在用户显示语句时,当前行也会相应地发生变化。例如,Z命令将使当前行向程序尾移动9行,而Ctrl+D则使当前行向后移
动10行。

在使用数字来显示某行语句时将使该行语句成为当前行。而在*提示符之后按一下回车,当前行将下移一行。例如,接着上面
的例子,输入:

* 8p

8: TEstInput(char * ValueInput)

* 回车

9: { while ( * ValueInput)}

*

这里8p实际上是两条命令的组合。它使当前行移至源文件的第八行,然后再显示出新的当前行。按回车键将使当前行后移一行。

3.改变当前源文件

在vi中我们可以用e命令对另外某个文件进行编辑。sdb也提供了e命令,可以用此命令来改变当前文件,如:

* e myprog.c

current file is now myprog.c

* 8p

8: main(int argc,char * argv[])

*

我们看到,当前文件改变之后,sdb将第一行设为是当前行。如果此文件的第一行是个函数,那么该函数便成为当前函数。
否则将临时出现没有当前函数的情况。

在上一节中,我们介绍过在命令行中可以指定源文件搜索目录名列表(缺省情况为当前目录)。如果某个文件不在此搜索
目录中,则可以用e命令将其加入:

* e Another SourceDir

这里Another SourceDir是一个目录名。如果要显示该目录下的某个文件,只需要输入:

* e FileName.c

当然直接使用:

* e Another SourceDir/FileName.c

也能达到同样的效果。

使用:

* e FunctionName

将使包含函数FunctionName的文件名成为当前文件,而当前函数不言而喻将成为FunctionName。当前行则理所当然的是该
函数的第一行。同一程序中函数名在各模块中的唯一性保证了这一点是能够成功的,但如果包含指定函数的文件不在当前
搜索目录列表中,则必须用e命令将其加入。

4.字符串的搜索

在vi中,我们可以在命令方式下使用“/“或者“?”命令,从当前位置向后或者向前搜索某个字符串,在sdb中也同样可
以完成这一点。使用这两个命令我们可以查找源程序中某个或某类符号的出现。之所以说某类,是因为我们可以用正规表
达式来指定待搜索的串(也即在搜索串中可以使用*,?,[,],-,^这类特殊字符)。

例如,为了查找myprog.c中argv出现在那些行上,可输入:

* /argv/

8: main(ini argc,char * argv[])

sdb将从当前行开始向文件尾搜索,到达文件尾之后又从文件头开始直至搜索到某个匹配的串或到达当前行为止。

与/相反,?命令将从当前行向文件头方向搜索,因此如果我们将上述/argv/换成:

* ? argv?

14: printf("The %dth value' %s'\tis BAD! \n",i,argv);

*

所得的结果一般是不同的。

/或?命令之后的/或?并不是必须的。另外如果要在同一方向上继续搜索上次搜索过的串,只需要直接输入/或者?即可。

[3 楼] | Posted: 2005-08-24 10:38

ppking

级别: 论坛版主
精华: 1
发帖: 28
威望: 65 点
金钱: 136 RMB
贡献值: 0 点
注册时间:2005-08-16
最后登录:2005-08-26

***************ld是怎么连接的**********************

由於静态与共享程式库两者间不相容的格式的差异性与动词*link*过量使用於指称*编译完成後的事情*与*当编译好的程式使用时所发生的事情*这两件事上头,使得这一章节变得复杂了许多。( and, actually, the overloading of the word `load' in a comparable but opposite sense)不过,再复杂也就是这样了,所以阁下不必过於担心。

为了稍微减轻读者的困惑,我们称执行期间所发生的事为*动态载入*,这一主题会在下一章节中谈到。你也会在别的地方看到我把动态载入描述成*动态连结*,不过不会是在这一章节中。换句话说,这一章节所谈的,全部是指发生在编译结束後的连结。

6.1 共享程式库 vs静态程式库

建立程式的最後一个步骤便是连结;也就是将所有分散的小程式组合起来,看看是否遗漏了些什麽。显然,有一些事情是很多程式都会想做的---例如,开启档案,接著所有与开档有关的小程式就会将储存程式库的相关档案提供给你的程式使用。在一般的Linux系统上,这些小程式可以在/lib与 /usr/lib/目录底下找到。

当你用的是静态的程式库时,连结器会找出程式所需的模组,然後实际将它们拷贝到执行档内。然而,对共享程式库而言,就不是这样了。共享程式库会在执行档内留下一个记号,指明*当程式执行时,首先必须载入这个程式库*。显然,共享程式库是试图使执行档变得更小,等同於使用更少的记忆体与磁碟空间。 Linux内定的行为是连结共享程式库,只要Linux能找到这些共享程式库的话,就没什麽问题;不然,Linux就会连结静态的了。如果你想要共享程式库的话,检查这些程式库(*.sa for a.out, *.so for ELF)是否住在它们该在的地方,而且是可读取的。

在Linux 上,静态程式库会有类似libname.a这样的名称;而共享程式库则称为libname.so.x.y.z,此处的x.y.z是指版本序号的样式。共享程式库通常都会有连结符号指向静态程式库(很重要的)与相关联的.sa档案。标准的程式库会包含共享与静态程式库两种格式。

你可以用ldd(List Dynamic Dependencies)来查出某支程式需要哪些共享程式库。 $ ldd /usr/bin/lynx libncurses.so.1 => /usr/lib/libncurses.so.1.9.6 libc.so.5 => /lib/libc.so.5.2.18

这是说在我的系统上,WWW浏览器*lynx*会依赖libc.so.5 (the C library)与libncurses.so.1(终端机萤幕的控制)的存在。若某支程式缺乏独立性, ldd就会说‘statically linked’或是‘statically linked (ELF)’。

6.2 终极审判(‘sin() 在哪个程式库里?’)

nm 程式库名称应该会列出此程式库名称所参考到的所有符号。这个指令可以应用在静态与共享程式库上。假设你想知道tcgetattr()是在哪儿定义的:你可以如此做,

$ nm libncurses.so.1 |grep tcget U tcgetattr

*U*指出*未定义*---也就是说ncurses程式库有用到tegetattr(),但是并没有定义它。你也可以这样做,

$ nm libc.so.5 | grep tcget 00010fe8 T __tcgetattr 00010fe8 W tcgetattr 00068718 T tcgetpgrp

*W*说明了*弱态(weak)*,意指符号虽已定义,但可由不同程式库中的另一定义所替代。最简单的*正常*定义(像是tcgetpgrp)是由*T*所标示:

标题所谈的问题,最简明的答案便是libm.(so|a)了。所有定义在的函数都保留在maths程式库内;因此,当你用到其中任何一个函数时,都需要以-lm的参数连结此程式库。

6.3 X档案?

ld: Output file requires shared library `libfoo.so.1`

ld与其相类似的命令在搜寻档案的策略上,会依据版本的差异而有所不同,但是唯一一个你可以合理假设的内定目录便是/usr/lib了。如果你希望身处它处的程式库也列入搜寻的行列中,那麽你就必须以-L选项告知gcc或是ld。

要是你发现一点效果也没有,就赶紧察看看那档案是不是还乖乖的躺在原地。就a.out而言,以-lfoo参数来连结,会驱使ld去寻找 libfoo.sa(shared stubs);如果没有成功,就会换成寻找libfoo.a(static)。就ELF而言, ld会先找libfoo.so,然後是libfoo.a。libfoo.so通常是一个连结符号,连结至libfoo.so.x。

6.4 建立你自己的程式库 控制版本

与其它任何的程式一样,程式库也有修正不完的bugs的问题存在。它们也可能产生出一些新的特点,更改目前存在的模组的功效,或是将旧的移除掉。这对正在使用它们的程式而言,可能会是一个大问题。如果有一支程式是根据那些旧的特点来执行的话,那怎麽办?

所以,我们引进了程式库版本编号的观念。我们将程式库*次要*与*主要*的变更分门别类,同时规定*次要*的变更是不允许用到这程式库的旧程式发生中断的现象。你可以从程式库的档名分辨出它的版本(实际上,严格来讲,对ELF而言仅仅是一场天大的谎言;继续读将下去,便可明白为什麽了): libfoo.so.1.2的主要版本是1,次要版本是2。次要版本的编号可能真有其事,也可能什麽都没有---libc在这一点上用了*修正程度*的观念,而订出了像libc.so.5.2.18这样的程式库名称。次要版本的编号内若是放一些字母、底线、或是任何可以列印的ASCII字元,也是很合理的。

ELF与a.out格式最主要的差别之一就是在设置共享程式库这件事上;我们先看ELF,因为它比较简单一些。

ELF?它到底是什麽东东ㄋㄟ?

ELF (Executable and Linking Format)最初是由USL(UNIX System Laboratories)发展而成的二进位格式,目前正应用於Solaris与System V Release 4上。由於ELF所增涨的弹性远远超过Linux过去所用的a.out格式,因此GCC与C程式库的发展人士於1995年决定改用ELF为Linux标准的二进位格式。

怎麽又来了?

这一节是来自於‘/news-archives/comp.sys.sun.misc’的文件。

ELF (“Executable Linking Format”)是於SVR4所引进的新式改良目的档格式。ELF比起COFF可是多出了不少的功能。以ELF而言,它*是*可由使用者自行延伸的。 ELF视一目的档为节区(sections),如串列般的组合;而且此串列可为任意的长度(而不是一固定大小的阵列)。这些节区与COFF的不一样,并不需要固定在某个地方,也不需要以某种顺序排列。如果使用者希望补捉到新的资料,便可以加入新的节区到目的档内。ELF也有一个更强而有力的除错法式,称为 DWARF(Debugging With Attribute Record Format)□目前Linux并不完全支援。DWARF DIEs(Debugging Information Entries)的连结串列会在ELF内形成 .debug的节区。DWARF DIEs的每一个 .debug节区并非一些少量且固定大小的资讯记录的集合,而是一任意长度的串列,拥有复杂的属性,而且程式的资料会以有□围限制的树状资料结构写出来。 DIEs所能补捉到的大量资讯是COFF的 .debug节区无法望其项背的。(像是C++的继承图。)

ELF 档案是从SVR4(Solaris 2.0 ?)ELF存取程式库(ELF access library)内存取的。此程式库可提供一简便快速的介面予ELF。使用ELF存取程式库最主要的恩惠之一便是,你不再需要去察看一个ELF档的qua 了。就UNIX的档案而言,它是以Elf*的型式来存取;呼叫elf_open()之後,从此时开始,你只需呼叫elf_foobar()来处理档案的某一部份即可,并不需要把档案实际在磁碟上的image搞得一团乱。

ELF的优缺点与升级至ELF等级所需经历的种种痛苦,已在ELF-HOWTO内论及;我并不打算在这儿涂浆糊。ELF HOWTO应该与这份文件有同样的主题才是。

ELF共享程式库

若想让libfoo.so成为共享程式库,基本的步骤会像下面这样:

$ gcc -fPIC -c *.c $ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o $ ln -s libfoo.so.1.0 libfoo.so.1 $ ln -s libfoo.so.1 libfoo.so $ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH

这会产生一个名为libfoo.so.1.0的共享程式库,以及给予ld适当的连结(libfoo.so)还有使得动态载入程式(dynamic loader)能找到它(libfoo.so.1)。为了进行测试,我们将目前的目录加到LD_LIBRARY_PATH里。

当你津津乐道於程式库制做成功之时,别忘了把它移到如/usr/local/lib的目录底下,并且重新设定正确的连结路径。 libfoo.so.1与libfoo.so.1.0的连结会由ldconfig依日期不断的更新,就大部份的系统来说,ldconfig会在开机过程中执行。libfoo.so的连结必须由手动方式更新。如果你对程式库所有组成份子(如标头档等)的升级,总是抱持著一丝不□的态度,那麽最简单的方法就是让libfoo.so -> libfoo.so.1;如此一来,ldconfig便会替你同时保留最新的连结。要是你没有这麽做,你自行设定的东东就会在数日後造成千奇百怪的问题出现。到时候,可别说我没提醒你啊!

$ su # cp libfoo.so.1.0 /usr/local/lib # /sbin/ldconfig # ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so

版本编号、soname与符号连结

每一个程式库都有一个soname。当连结器发现它正在搜寻的程式库中有这样的一个名称,连结器便会将soname箝入连结中的二进位档内,而不是它正在运作的实际的档名。在程式执行期间,动态载入程式会搜寻拥有soname这样的档名的档案,而不是程式库的档名。因此,一个名为libfoo.so 的程式库,就可以有一个libbar.so的soname了。而且所有连结到libbar.so的程式,当程式开始执行时,会寻找的便是 libbar.so了。

这听起来好像一点意义也没有,但是这一点,对於了解数个不同版本的同一个程式库是如何在单一系统上共存的原因,却是关键之钥。Linux程式库标准的命名方式,比如说是libfoo.so.1.2,而且给这个程式库一个libfoo.so.1的soname。如果此程式库是加到标准程式库的目录底下(e.g. /usr/lib),ldconfig会建立符号连结libfoo.so.1 -> libfoo.so.1.2,使其正确的image能於执行期间找到。你也需要连结libfoo.so -> libfoo.so.1,使ld能於连结期间找到正确的soname。

所以罗,当你修正程式库内的bugs,或是添加了新的函数进去(任何不会对现存的程式造成不利的影响的改变),你会重建此程式库,保留原本已有的 soname,然後更改程式库档名。当你对程式库的变更会使得现有的程式中断,那麽你只需增加soname中的编号---此例中,称新版本为 libfoo.so.2.0,而soname变成libfoo.so.2。紧接著,再将libfoo.so的连结转向新的版本;至此,世界又再度恢复了和平!

其实你不须要以此种方式来替程式库命名,不过这的确是个好的传统。ELF赋予你在程式库命名上的弹性,会使得人气喘呼呼的搞不清楚状况;有这样的弹性在,也并不表示你就得去用它。

ELF总结:假设经由你睿智的观察发现有个惯例说:程式库主要的升级会破坏相容性;而次要的升级则可能不会;那麽以下面的方式来连结,所有的一切就都会相安无事了。

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

a.out---旧旧的格式□

建立共享程式库的便利性是升级至ELF的主要原因之一。那也是说,a.out可能还是有用处在的。上ftp站去抓 ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz;解压缩後你会发现有20页的文件可以慢慢的读哩。我很不喜欢自己党派的偏见表现得那麽的淋璃尽致,可是从上下文间,应该也可以很清楚的嗅出我从来不拿石头砸自己的脚的脾气吧!

ZMAGIC vs QMAGIC

QMAGIC 是一种类似旧格式的a.out(亦称为ZMAGIC)的可执行档格式,这种格式会使得第一个分页无法map。当0-4096的□围内没有mapping存在时,则可允许NULL dereference trapping更加的容易。所产生的边界效应是你的执行档会比较小(大约少1K左右)。

只有即将作废的连结器有支援ZMAGIC,一半已埋入棺材的连结器有支援这两种格式;而目前的版本仅支援QMAGIC而已。事实上,这并没有多大的影响,那是因为目前的核心两种格式都能执行。

*file*命令应该可以确认程式是不是QMAGIC的格式的。

档案配置

一a.out (DLL)的共享程式库包含两个真实的档案与一个连结符号。就*foo*这个用於整份文件做为□例的程式库而言,这些档案会是 libfoo.sa与libfoo.so.1.2;连结符号会是libfoo.so.1,而且会指向libfoo.so.1.2。这些是做什麽用的?

在编译时,ld会寻找libfoo.sa。这是程式库的*stub*档案。而且含有所有执行期间连结所需的exported的资料与指向函数的指标。

执行期间,动态载入程式会寻找libfoo.so.1。这仅仅是一个符号连结,而不是真实的档案。故程式库可更新成较新的且已修正错误的版本,而不会损毁任何此时正在使用此程式库的应用程式。在新版---比如说libfoo.so.1.3---已完整呈现时,ldconfig会以一极微小的操作,将连结指向新的版本,使得任何原本使用旧版的程式不会感到丝毫的不悦。

DLL 程式库(我知道这是无谓的反覆---所以对我提出诉讼吧!)通常会比它们的静态副本要来得大多。它们是以*洞(holes)*的形式来保留空间以便日後的扩充。这种*洞*可以不占用任何的磁碟空间。一个简单的cp呼叫,或是使用makehole程式,就可以达到这样效果。因为它们的位址是固定在同一位置上,所以在建立程式库後,你可以把它们拿掉。不过,千万不要试著拿掉ELF的程式库。

**********************动态载入过程***************

Linux有共享程式库,如果之前你已坐著读完上一章节,想必现在一听到像这样的说词,便会立刻感到头昏。有一些照惯例而言是在连结时期便该完成的工作,必须延迟到载入时期才能完成。 7.2 错误讯息

把你连结的错误寄给我!我不会做任何的事,不过我可以把它们写起来**

can't load library: /lib/libxxx.so, Incompatible version

(a. out only) 这是指你没有xxx程式库的正确的主要版本。可别以为随随 便便弄个连结到你目前拥有的版本就可以了,如果幸运的话,就只会造成你的程式分页错误而已。去抓新的版本.ELF类似的情况会造成像下面这样的讯息:

ftp: can't load library 'libreadline.so.2'

warning using incompatible library version xxx

(a. out only)你的程式库的次要版本比起这支程式用来编译的还要旧。程式依然可以执行。只是可能啦!我想,升个级应该没什麽伤害吧!

7.3 控制动态载入器的运作

有一组环境变数会让动态载入器有所反应。大部份的环境变数对ldd的用途要比起对一般users的还要来得更多。而且可以很方便的设定成由ldd配合各种参数来执行。这些变数包括,

LD_BIND_NOW --- 正常来讲,函数在呼叫之前是不会让程式寻找的。设定这个旗号会使得程式库一载入,所有的寻找便会发生,同时也造成起始的时间较慢。当你想测试程式,确定所有的连结都没有问题时,这项旗号就变得很有用。 LD_PRELOAD可以设定一个档案,使其具有*覆盖*函数定义的能力。例如,如果你要测试记忆体分配的方略,而且还想置换*malloc*,那麽你可以写好准备替换的副程式,并把它编译成mallolc.,然後: $ LD_PRELOAD=malloc.o; export LD_PRELOAD $ some_test_program LD_ELF_PRELOAD 与 LD_AOUT_PRELOAD 很类似,但是仅适用於正确的二进位型态。如果设定了 LD_something_PRELOAD 与 LD_PRELOAD ,比较明确的那一个会被用到。 LD_LIBRARY_PATH是一连串以分号隔离的目录名称,用来搜寻共享程式库。对ld而言,并没有任何的影响;这项只有在执行期间才有影响。另外,对执行setuid与setgid的程式而言,这一项是无效的。而LD_ELF_LIBRARY_PATH与LD_AOUT_LIBRARY_PATH这两种旗号可根据各别的二进位型式分别导向不同的搜寻路径。一般正常的运作下,不应该会用到LD_LIBRARY_PATH;把需要搜寻的目录加到 /etc/ld.so.conf/里;然後重新执行ldconfig。 LD_NOWARN 仅适用於a.out。一旦设定了这一项(LD_NOWARN=true; export LD_NOWARN),它会告诉载入器必须处理fatal-warnings(像是次要版本不相容等)的警告讯息。 LD_WARN仅适用於ELF。设定这一项时,它会将通常是致命讯息的“Can*t find library”转换成警告讯息。对正常的操作而言,这并没有多大的用处,可是对ldd就很重要了。 LD_TRACE_LOADED_OBJECTS仅适用於ELF。而且会使得程式以为它们是由ldd所执行的: $ LD_TRACE_LOADED_OBJECTS=true /usr/bin/lynx libncurses.so.1 => /usr/lib/libncurses.so.1.9.6 libc.so.5 => /lib/libc.so.5.2.18

7.4 以动态载入撰写程式

如果你很熟悉Solaris 2.x所支援的动态载入的工作的话,你会发现Linux在这点上与其非常的相近。这一部份在H.J.Lu的ELF程式设计文件内与dlopen(3)的manual page(可以在ld.so的套件上找到)上有广泛的讨论。

linux下的静态、动态库zz

Linux下的静态库以.a结尾(Winodws下为.lib)
Linux下的动态库以.so 或 .so.y结尾,其中y代表版本号(Windows下为.dll)
而且,Linux下的库必须以lib开头,用于系统识别(如:libjpeg.a libsdl.so)

静态库必要的目标代码的是在对程序编译的时候被加入到程序中,而运行时不再需要.a的库了
而动态库,则是在运行时转载
所以,动态链接的可执行代码比静态链接的可执行代码小的多

把一个源代码编译成.so:
gcc -shared -o libtry.so try.c

而要生成静态库:
gcc -c try1.c
gcc -c try2.c
ar cqs libtry.a try1.o try2.o(或 ar r libtry.a try1.o try2.o)

静态库的使用
# gcc -c main.c -o main.o
# gcc main.o -o name -L. -ltry (-l后面的名字就是我们上面生成的try库)


动态库的使用
动态库分为显式调用和隐式调用
1.显示调用
需要了解以下几个函数:
const char *dlerror(void);
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,为NULL时表示操作函数执行成功。
void *dlopen(const char *filename, int flag);
成功则返回为void*的句柄。flag可以为RTLD_LAZY(表明在动态链接库的函数代码执行时解决);RTLD_NOW(表明在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误)。filename为动态库路径。
void *dlsym(void *handle, char *symbol);
dlsym 根据动态链接库操作句柄(handle)与符号(实际上就是欲调用的函数名),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。 int dlclose (void *handle); 参数为动态链接库的句柄。 显式调用动态链接库必须包含动态链接库功能接口dlfcn.h(包含dl系列函数的声明)和被调函数的声明。
看起来还比较简单,但是当你对动态链接库函数使用比较频繁的时候,就知道他的麻烦了,所以,不推崇。
2.隐式调用
所谓隐式调用,就是调用的时候直接使用动态库中的函数,并不区别对待。
但是在隐式调用的时候必须要让程序能找到你所调用的函数的所属动态库。
我们先来建立一个感性认识:
# more /etc/ld.so.conf 你会看到以下内容:
/usr/kerberos/lib
/usr/X11R6/lib
/usr/lib/sane
/usr/lib/qt-3.1/lib
/usr/lib/mysql
/usr/lib/qt2/lib
/usr/local/lib
/usr/local/BerkeleyDB.4.3/lib
ld.so.conf是系统对动态链接库进行查找的路径配置文件,也就是说该文件是系统链接工具/usr/bin/ld查找动态链接库的地图,所以,要达到我们的目的有以下几种方法:
a.将自己的动态链接库文件拷到以上路径的目录下
# cp libwx.so.1 /usr/local/lib
b.将自己动态链接库文件的路径加入到该文件中
# vi /etc/ld.so.conf, 然后加入自己的路径(或pwd >>/etc/ld.so.conf)
(千万不要将>>写成>,前者是添加,后者是覆盖)
c.把当前路径加入环境变量LD_LIBRARY_PATH,其实就是/usr/bin/ld的环境变量
# export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

编译的时候:
# gcc -o try main.c libtry.so.1
如果没有让/usr/bin/ld知道你的动态链接库在哪,编译的时候就要告诉它:
# gcc -o try main.c /root/libtry.so.1 ( 或:# gcc -L/root/wx -o qqq main.c libmy.so.1)
-L指定动态链接库所在的目录,有时候用gcc还会碰到-I,-l,他们分别指定头文件的目录和所链接的动态链接库


# ldd try 用来查看可执行文件qqq的动态链接库的依赖关系

libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x00867000)
libSDL-1.2.so.0 => /usr/lib/libSDL-1.2.so.0 (0x0638a000)
libc.so.6 => /lib/tls/libc.so.6 (0x0046d000)
libm.so.6 => /lib/tls/libm.so.6 (0x00598000)
libdl.so.2 => /lib/libdl.so.2 (0x005bd000)
libasound.so.2 => /lib/libasound.so.2 (0x062e1000)
libX11.so.6 => /usr/X11R6/lib/libX11.so.6 (0x005d5000)
libXext.so.6 => /usr/X11R6/lib/libXext.so.6 (0x0069e000)
libpthread.so.0 => /lib/tls/libpthread.so.0 (0x006ae000)
/lib/ld-linux.so.2 (0x00450000)

.dll
dll---com组件dll
| |
| |__常规dll--win32 dll
| |___mfc dll
| |___extended dll (所有dll不参与编译)
lib
|_____与obj文件类似的未编译过符号文件,与obj文件区别是声明了转入转出函数

DLL与LIB的区别:
1.DLL是一个完整程序,其已经经过链接,即不存在同名引用,且有导出表,与导入表
lib是一个代码集(也叫函数集)他没有链接,所以lib有冗余,当两个lib相链接时地址会重新建立,当然还有其它相关的不同,用lib.exe就知道了
2.在生成dll时,经常会生成一个.lib(导入与导出),这个lib实际上不是真正的函数集,其每一个导出导入函数都是跳转指令,直接跳转到DLL中的位置,这个目的是外面的程序调用dll时自动跳转
3.实际上最常用的lib是由lib.exe把*.obj生成的lib,这才是真正的库,他是代码集,可完全代替目标代码

jrtplib-3.7.0在arm-linux上交叉编译

折腾了一下午,终于编译通过了。
export CXX=arm-linux-g++
./configure --host=arm-linux
make
三步就OK了。

2007年5月13日星期日

Linux环境进程间通信:管道及有名管道

管道及有名管道

在本系列序中作者概述了 linux 进程间通信的几种主要手段。其中管道和有名管道是最早的进程间通信机制之一,管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。 认清管道和有名管道的读写规则是在程序中应用它们的关键,本文在详细讨论了管道和有名管道的通信机制的基础上,用实例对其读写规则进行了程序验证,这样做有利于增强读者对读写规则的感性认识,同时也提供了应用范例。

1、 管道概述及相关API应用

1.1 管道相关的关键概念

管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

* 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
* 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
* 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
* 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

1.2管道的创建:

#include
int pipe(int fd[2])

该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。

1.3管道的读写规则:

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。

从管道中读取数据:

* 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;
* 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。

关于管道的读规则验证:


/**************
* readtest.c *
**************/
#include
#include
#include
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[100];
char w_buf[4];
char* p_wbuf;
int r_num;
int cmd;

memset(r_buf,0,sizeof(r_buf));
memset(w_buf,0,sizeof(r_buf));
p_wbuf=w_buf;
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
printf("\n");
close(pipe_fd[1]);
sleep(3);//确保父进程关闭写端
r_num=read(pipe_fd[0],r_buf,100);
printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf));

close(pipe_fd[0]);
exit();
}
else if(pid>0)
{
close(pipe_fd[0]);//read
strcpy(w_buf,"111");
if(write(pipe_fd[1],w_buf,4)!=-1)
printf("parent write over\n");
close(pipe_fd[1]);//write
printf("parent close fd[1] over\n");
sleep(10);
}
}
/**************************************************
* 程序输出结果:
* parent write over
* parent close fd[1] over
* read num is 4 the data read from the pipe is 111
* 附加结论:
* 管道写端关闭后,写入的数据将一直存在,直到读出为止.
****************************************************/

向管道中写入数据:

* 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。
注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。

对管道的写规则的验证1:写端对读端存在的依赖性


#include
#include
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[4];
char* w_buf;
int writenum;
int cmd;

memset(r_buf,0,sizeof(r_buf));
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
close(pipe_fd[0]);
close(pipe_fd[1]);
sleep(10);
exit();
}
else if(pid>0)
{
sleep(1); //等待子进程完成关闭读端的操作
close(pipe_fd[0]);//write
w_buf="111";
if((writenum=write(pipe_fd[1],w_buf,4))==-1)
printf("write to pipe error\n");
else
printf("the bytes write to pipe is %d \n", writenum);

close(pipe_fd[1]);
}
}

则输出结果为: Broken pipe,原因就是该管道以及它的所有fork()产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe,读者可自己验证一下该结论。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)

对管道的写规则的验证2:linux不保证写管道的原子性验证


#include
#include
#include
main(int argc,char**argv)
{
int pipe_fd[2];
pid_t pid;
char r_buf[4096];
char w_buf[4096*2];
int writenum;
int rnum;
memset(r_buf,0,sizeof(r_buf));
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

if((pid=fork())==0)
{
close(pipe_fd[1]);
while(1)
{
sleep(1);
rnum=read(pipe_fd[0],r_buf,1000);
printf("child: readnum is %d\n",rnum);
}
close(pipe_fd[0]);

exit();
}
else if(pid>0)
{
close(pipe_fd[0]);//write
memset(r_buf,0,sizeof(r_buf));
if((writenum=write(pipe_fd[1],w_buf,1024))==-1)
printf("write to pipe error\n");
else
printf("the bytes write to pipe is %d \n", writenum);
writenum=write(pipe_fd[1],w_buf,4096);
close(pipe_fd[1]);
}
}

输出结果:
the bytes write to pipe 1000
the bytes write to pipe 1000 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 1000
the bytes write to pipe 120 //注意,此行输出说明了写入的非原子性
the bytes write to pipe 0
the bytes write to pipe 0
......

结论:

写入数目小于4096时写入是非原子的!
如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论:
写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。

1.4管道应用实例:

实例一:用于shell

管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个shell程序(Bourne shell或C shell等)键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行:

$kill -l 运行结果见附一。

$kill -l | grep SIGRTMIN 运行结果如下:


30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1
34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14

实例二:用于具有亲缘关系的进程间通信

下面例子给出了管道的具体应用,父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理。


#include
#include
main()
{
int pipe_fd[2];
pid_t pid;
char r_buf[4];
char** w_buf[256];
int childexit=0;
int i;
int cmd;

memset(r_buf,0,sizeof(r_buf));

if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
if((pid=fork())==0)
//子进程:解析从管道中获取的命令,并作相应的处理
{
printf("\n");
close(pipe_fd[1]);
sleep(2);

while(!childexit)
{
read(pipe_fd[0],r_buf,4);
cmd=atoi(r_buf);
if(cmd==0)
{
printf("child: receive command from parent over\n now child process exit\n");
childexit=1;
}

else if(handle_cmd(cmd)!=0)
return;
sleep(1);
}
close(pipe_fd[0]);
exit();
}
else if(pid>0)
//parent: send commands to child
{
close(pipe_fd[0]);

w_buf[0]="003";
w_buf[1]="005";
w_buf[2]="777";
w_buf[3]="000";
for(i=0;i<4;i++)
write(pipe_fd[1],w_buf[i],4);
close(pipe_fd[1]);
}
}
//下面是子进程的命令处理函数(特定于应用):
int handle_cmd(int cmd)
{
if((cmd<0)||(cmd>256))
//suppose child only support 256 commands
{
printf("child: invalid command \n");
return -1;
}
printf("child: the cmd from parent is %d\n", cmd);
return 0;
}

1.5管道的局限性

管道的主要局限性正体现在它的特点上:

* 只支持单向数据流;
* 只能用于具有亲缘关系的进程之间;
* 没有名字;
* 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
* 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

2、 有名管道概述及相关API应用

2.1 有名管道相关的关键概念

管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是, FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

2.2有名管道的创建


#include
#include
int mkfifo(const char * pathname, mode_t mode)

该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

2.3有名管道的打开规则

有名管道比管道多了一个打开操作:open。

FIFO的打开规则:

如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。

如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

对打开规则的验证参见附2。

2.4有名管道的读写规则

从FIFO中读取数据:

约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。

* 如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
* 对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
* 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
* 如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。

注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

向FIFO中写入数据:

约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

对于设置了阻塞标志的写操作:

* 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
* 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:

* 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
* 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

对FIFO读写规则的验证:

下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。

程序1:写FIFO的程序


#include
#include
#include
#include
#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)
//参数为即将写入的字节数
{
int fd;
char w_buf[4096*2];
int real_wnum;
memset(w_buf,0,4096*2);
if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");

if(fd==-1)
if(errno==ENXIO)
printf("open error; no reading process\n");

fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
//设置非阻塞标志
//fd=open(FIFO_SERVER,O_WRONLY,0);
//设置阻塞标志
real_wnum=write(fd,w_buf,2048);
if(real_wnum==-1)
{
if(errno==EAGAIN)
printf("write to fifo error; try later\n");
}
else
printf("real write num is %d\n",real_wnum);
real_wnum=write(fd,w_buf,5000);
//5000用于测试写入字节大于4096时的非原子性
//real_wnum=write(fd,w_buf,4096);
//4096用于测试写入字节不大于4096时的原子性

if(real_wnum==-1)
if(errno==EAGAIN)
printf("try later\n");
}
程序2:与程序1一起测试写FIFO的规则,第一个命令行参数是请求从FIFO读出的字节数
#include
#include
#include
#include
#define FIFO_SERVER "/tmp/fifoserver"

main(int argc,char** argv)
{
char r_buf[4096*2];
int fd;
int r_size;
int ret_size;
r_size=atoi(argv[1]);
printf("requred real read bytes %d\n",r_size);
memset(r_buf,0,sizeof(r_buf));
fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);
//fd=open(FIFO_SERVER,O_RDONLY,0);
//在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本
if(fd==-1)
{
printf("open %s for read error\n");
exit();
}
while(1)
{

memset(r_buf,0,sizeof(r_buf));
ret_size=read(fd,r_buf,r_size);
if(ret_size==-1)
if(errno==EAGAIN)
printf("no data avlaible\n");
printf("real read bytes %d\n",ret_size);
sleep(1);
}
pause();
unlink(FIFO_SERVER);
}

程序应用说明:

把读程序编译成两个不同版本:

* 阻塞读版本:br
* 以及非阻塞读版本nbr

把写程序编译成两个四个版本:

* 非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg
* 非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw
* 阻塞且请求写的字节数大于PIPE_BUF版本:bwg
* 阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw

下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读

验证阻塞写操作:

1. 当请求写入的数据量大于PIPE_BUF时的非原子性:
* nbr 1000
* bwg
2. 当请求写入的数据量不大于PIPE_BUF时的原子性:
* nbr 1000
* bw

验证非阻塞写操作:

1. 当请求写入的数据量大于PIPE_BUF时的非原子性:
* nbr 1000
* nbwg
2. 请求写入的数据量不大于PIPE_BUF时的原子性:
* nbr 1000
* nbw

不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:

对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;

而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。

对于读操作的验证则比较简单,不再讨论。

2.5有名管道应用实例

在验证了相应的读写规则后,应用实例似乎就没有必要了。

小结:

管道常用于两个方面:(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。

管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输"协议",采用传播具有特定意义的消息。

要灵活应用管道及FIFO,理解它们的读写规则是关键。

附1: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 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 32) SIGRTMIN 33) SIGRTMIN+1
34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14
50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10
54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6
58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2
62) SIGRTMAX-1 63) SIGRTMAX

除了在此处用来说明管道应用外,接下来的专题还要对这些信号分类讨论。

附2:对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)


#include
#include
#include
#include
#define FIFO_SERVER "/tmp/fifoserver"

int handle_client(char*);
main(int argc,char** argv)
{
int r_rd;
int w_fd;
pid_t pid;

if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
printf("cannot create fifoserver\n");
handle_client(FIFO_SERVER);

}

int handle_client(char* arg)
{
int ret;
ret=w_open(arg);
switch(ret)
{
case 0:
{
printf("open %s error\n",arg);
printf("no process has the fifo open for reading\n");
return -1;
}
case -1:
{
printf("something wrong with open the fifo except for ENXIO");
return -1;
}
case 1:
{
printf("open server ok\n");
return 1;
}
default:
{
printf("w_no_r return ????\n");
return 0;
}
}
unlink(FIFO_SERVER);
}

int w_open(char*arg)
//0 open error for no reading
//-1 open error for other reasons
//1 open ok
{
if(open(arg,O_WRONLY|O_NONBLOCK,0)==-1)
{ if(errno==ENXIO)
{
return 0;
}
else
return -1;
}
return 1;

}

参考文献:

1. UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。丰富的UNIX进程间通信实例及分析,对Linux环境下的程序开发有极大的启发意义。
2. linux内核源代码情景分析(上、下),毛德操、胡希明著,浙江大学出版社,当要验证某个结论、想法时,最好的参考资料;
3. UNIX环境高级编程,作者:W.Richard Stevens,译者:尤晋元等,机械工业出版社。具有丰富的编程实例,以及关键函数伴随Unix的发展历程。
4. http://www.linux.org.tw/CLDP/gb/Secure-Programs-HOWTO/x346.html 点明linux下sigaction的实现基础,linux源码../kernel/signal.c更说明了问题;
5. pipe手册,最直接而可靠的参考资料
6. fifo手册,最直接而可靠的参考资料

关于作者

郑彦兴,男,现攻读国防科大计算机学院网络方向博士学位。您可以通过电子邮件 mlinux@163.com和他联系。

Linux下的多进程编程

  (一) 理解Linux下进程的结构

   Linux下一个进程在内存里有三部份的数据,就是“数据段”,“堆栈段”和“代码段”,其实学过汇编语言的人一定知道,一般的CPU象 I386,都有上述三种段寄存器,以方便操作系统的运行。“代码段”,顾名思义,就是存放了程序代码的数据,假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一

  个代码段。

   堆栈段存放的就是子程序的返回地址、子程序的参数以及程序的局部变量。而数据段则存放程序的全局变量,常数以及动态数据分配的数据空间(比如用malloc之类的函数取得的空间)。这其中有许多细节问题,这里限于篇幅就不多介绍了。系统如果同时运行数个相同的程序,它们之间就不能使用同一个堆栈段和数据 段。

  (二) 如何使用fork

   在Linux下产生新的进程的系统调用就是fork函数,这个函数名是英文中“分叉”的意思。为什么取这个名字呢?因为一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就“分叉”了,所以这个名字取得很形象。下面就看看如何具体使用fork,这段程序演示了使用fork的基本框架:

  void main(){

  int i;

  if ( fork() == 0 ) {

  /* 子进程程序 */

  for ( i = 1; i < 1000; i )

  printf("This is child process\n");

  }

  else {

  /* 父进程程序*/

  for ( i = 1; i < 1000; i )

  printf("This is process process\n");

  }

  }

   程序运行后,你就能看到屏幕上交替出现子进程与父进程各打印出的一千条信息了。如果程序还在运行中 ,你用ps命令就能看到系统中有两个它在运行了。

   那么调用这个fork函数时发生了什么呢?一个程序一调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。

   读者也许会问,如果一个大程序在运行中,它的数据段和堆栈都很大,一次fork就要复制一次,那么fork 的系统开销不是很大吗?其实UNIX自有其解决的办法,大家知道,一般CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在通常情况下是 4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的, fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。

  一个小幽默:下面演示一个足以"搞死"Linux的小程序,其源代码非常简单:

  void main()

  {

  for(;;) fork();

  }

   这个程序什么也不做,就是死循环地fork,其结果是程序不断产生进程,而这些进程又不断产生新的进程,很快,系统的进程就满了,系统就被这么多不断产生的进程"撑死了"。用不着是root,任何人运行上述程序都足以让系统死掉。哈哈,但这不是Linux不安全的理由,因为只要系统管理员足够聪明,他(或她)就可以预先给每个用户设置可运行的最大进程数,这样,只要不是root,任何能运行的进程数也许不足系统总的能运行和进程数的十分之一,这样,系统管理员就能对付上述恶意的程序了。

  (三) 如何启动另一程序的执行

   下面我们来看看一个进程如何来启动另一个程序的执行。在Linux中要使用exec类的函数,exec类的函数不止一个,但大致相同,在 Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp,下面我只以execlp为例,其它函数究竟与execlp有何区别,请通过manexec命令来了解它们的具体情况。

   一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过 exec类函数中有的还允许继承环境变量之类的信息。)

   那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合fork与exec的 使用。下面一段代码显示如何启动运行其它程序:

  char command[256];

  void main()

  {

  int rtn; /*子进程的返回数值*/

  while(1) {

  /* 从终端读取要执行的命令 */

  printf( ">" );

  fgets( command, 256, stdin );

  command[strlen(command)-1] = 0;

  if ( fork() == 0 ) {

  /* 子进程执行此命令 */

  execlp( command, command );

  /* 如果exec函数返回,表明没有正常执行命令,打印错误信息*/

  perror( command );

  exit( errorno );

  }

  else {

  /* 父进程, 等待子进程结束,并打印子进程的返回值 */

  wait ( &rtn );

  printf( " child process return %d\n",. rtn );

  }

  }

  }

   此程序从终端读入命令并执行之,执行完成后,父进程继续等待从终端读入命令。熟悉DOS和WINDOWS系统调用的朋友一定知道 DOS/WINDOWS也有exec类函数,其使用方法是类似的,但DOS/WINDOWS还有spawn类函数,因为DOS是单任务的系统,它只能将 “父进程”驻留在机器内再执行“子进程”,这就是spawn类的函数。 WIN32已经是多任务的系统了,但还保留了spawn类函数,WIN32中实现spawn函数的方法同前述UNIX中的方法差不多,开设子进程后父进程等待子进程结束后才继续运行。UNIX在其一开始就是多任务的系统,所以从核 心角度上讲不需要spawn类函数。

   另外,有一个更简单的执行其它程序的函数system,它是一个较高层的函数,实际上相当于在SHELL环境 下执行一条命令,而exec类函数则是低层的系统调用。

  (四) Linux的进程与Win32的进程/线程有何区别

   熟悉WIN32编程的人一定知道,WIN32的进程管理方式与UNIX上有着很大区别,在UNIX里,只有进程的概念 ,但在WIN32里却还有一个“线程”的概念,那么UNIX和WIN32在这里究竟有着什么区别呢?

   UNIX里的fork是七十年代UNIX早期的开发者经过长期在理论和实践上的艰苦探索后取得的成果,一方面, 它使操作系统在进程管理上付出了最小的代价,另一方面,又为程序员提供了一个简洁明了的多进程方法。

   WIN32里的进程/线程是继承自OS/2的。在WIN32里,“进程”是指一个程序,而“线程”是一个“进程” 里的一个执行“线索”。从核心上讲,WIN32的多进程与UNIX并无多大的区别,在WIN32里的线程才相当于UNIX 的进程,是一个实际正在执行的代码。但是,WIN32里同一个进程里各个线程之间是共享数据段的。这才是与 UNIX的进程最大的不同。

   下面这段程序显示了WIN32下一个进程如何启动一个线程:(请注意,这是个终端方式程序,没有图形界面 )

  int g;

  DWORD WINAPI ChildProcess( LPVOID lpParameter ){

  int i;

  for ( i = 1; i < 1000; i ) {

  g ;

  printf( "This is Child Thread: %d\n", g );

  }

  ExitThread( 0 );

  };

  void main()

  {

  int threadID;

  int i;

  g = 0;

  CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID );

  for ( i = 1; i < 1000; i ) {

  g ;

  printf( "This is Parent Thread: %d\n", g );

  }

  }

   在WIN32下,使用CreateThread函数创建线程,与UNIX不同,线程不是从创建处开始运行的,而是由 CreateThread指定一个函数,线程就从那个函数处开始运行。此程序同前面的UNIX程序一样,由两个线程各打印1000条信息。 threadID是子线程的线程号,另外,全局变量g是子线程与父线程共享的,这就是与UNIX最大的不同之处。大家可以看出,WIN32的进程/线程要比UNIX复杂,在UNIX里要实现类似WIN32的线程并不难,只要fork以后,让子进程调用ThreadProc函数,并且为全局变量开设共享数据区就行了,但在WIN32下就无法实现类似fork的功能了。所以现在WIN32下的C语言编译器所提供的库函数虽然已经能兼容大多数UNIX的库函数,但却仍无法实现fork。

   对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在WIN32下,一个程序员很容易忘记线程之间的数据是共享的这一情况,一个线程修改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。但在UNIX下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变得 更清晰与安全。

   Linux还有自己的一个函数叫clone,这个函数是其它UNIX所没有的,而且通常的Linux也并不提供此函数(要使用此函数需自己重新编译内核,并设置CLONE_ACTUALLY_WORKS_OK选项),clone函数提供了更多的创建新进程的功能,包括象完全共享数据段这样的功能。

   至于WIN32的“进程”概念,其含义则是“应用程序”,也就是相当于UNIX下的exec了。

(http://www.fanqiang.com)

linux grep

用‘grep’搜索文本文件

如果您要在几个文本文件中查找一字符串,可以使用‘grep’命令。‘grep’在文本中搜索指定的字符串。
假设您正在‘/usr/src/linux/Documentation’目录下搜索带字符串‘magic’的文件:

$ grep magic /usr/src/linux/Documentation/*
sysrq.txt:* How do I enable the magic SysRQ key?
sysrq.txt:* How do I use the magic SysRQ key?

其中文件‘sysrp.txt’包含该字符串,讨论的是 SysRQ 的功能。

默认情况下,‘grep’只搜索当前目录。如果此目录下有许多子目录,‘grep’会以如下形式列出:

grep: sound: Is a directory

这可能会使‘grep’的输出难于阅读。这里有两种解决的办法:

* 明确要求搜索子目录:grep -r
* 或忽略子目录:grep -d skip

当然,如果预料到有许多输出,您可以通过 管道 将其转到‘less’上阅读:

$ grep magic /usr/src/linux/Documentation/* | less

这样,您就可以更方便地阅读。

有一点要注意,您必需提供一个文件过滤方式(搜索全部文件的话用 *)。如果您忘了,‘grep’会一直等着,直到该程序被中断。如果您遇到了这样的情况,按 ,然后再试。

下面是一些有意思的命令行参数:

* grep -i pattern files :不区分大小写地搜索。默认情况区分大小写,
* grep -l pattern files :只列出匹配的文件名,
* grep -L pattern files :列出不匹配的文件名,
* grep -w pattern files :只匹配整个单词,而不是字符串的一部分(如匹配‘magic’,而不是‘magical’),
* grep -C number pattern files :匹配的上下文分别显示[number]行,
* grep pattern1 | pattern2 files :显示匹配 pattern1 或 pattern2 的行,
* grep pattern1 files | grep pattern2 :显示既匹配 pattern1 又匹配 pattern2 的行。

这里还有些用于搜索的特殊符号:

* \< 和 \> 分别标注单词的开始与结尾。
例如:
o grep man * 会匹配 ‘Batman’、‘manic’、‘man’等,
o grep '\ o grep '\' 只匹配‘man’,而不是‘Batman’或‘manic’等其他的字符串。
* '^':指匹配的字符串在行首,
* '$':指匹配的字符串在行尾,
* 如果您不习惯命令行参数,可以试试图形界面的‘grep’,如 reXgrep 。这个软件提供 AND、OR、NOT 等语法,还有漂亮的按钮 :-) 。如果您只是需要更清楚的输出,不妨试试 fungrep 。



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1526741