1. stdin,stdout,stderr
对于新生成的任何进程来讲,都可以使用stdin,stdout,stderr这些文件指针来访问标准输入,标准输出,错误文件。他们的类型都是FILE *,属于c运行库的类型。而内核则使用文件描述符来代表文件。STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO分别被定义为0,1,2。
Dup2( srcfd , destfd )的作用为将srcfd文件描述符复制一份,并且让destfd代表复制后的文件描述符。这样srcfd,destfd指向共同的file table entry,并拥有共同的inode。调用该函数时,destfd除了可以是普通的文件描述符之外,还可以是STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO的任意一个。这就位管道命令提供了很好的支持。
注:调用
dup2
时,如果
destfd
已经是一个有效的文件描述符,那么将首先关闭
destfd
(调用
close
函数)
2. fork
fork函数可以生成子进程,并且子进程共享父进程的所有的文件描述符,数据,heap,stack。Child其实是parent的一份clone。事实上linux就是利用clone实现的fork。
把fork和dup2,pipe结合起来,就可以实现shell中支持的管道|了。
[1] 父进程首先调用pipe,返回fd0(用来从管道读数据),fd1(用来象管道写数据)
[2] 父进程调用fork生成子进程,此时子进程也共享到了父进程的fd0,fd1
[3] 子进程调用dup2( fd0 , STDIN_FILENO),这样就导致了子进程从stdin读取数据时,实际是从管道读取数据
[4] 子进程调用exec系列函数,执行相应的程序
[5] 父进程通过fd1向管道写数据
[6] 子进程通过stdin读取父进程向管道写入的数据。
其实,这就是shell执行管道的基本流程。
3. Unix哲学
一般的控制台程序都是从stdin读取参数或需要处理的数据,并把结果输出到stdout中。而unix的哲学是:
[1] Rule of Modularity: Write simple parts connected by clean interfaces
[2] Rule of Composition: Design programs to be connected to other programs
[3] Rule of Clarity: Clarity is better than cleverness
[4] Rule of Simplicity: Design for simplicity; add complexity only where you must
[5] Rule of Transparency: Design for visibility to make inspection and debugging easier
[6] Rule of Robustness: Robustness is the child of transparency and simplicity
[7] Rule of Least Surprise: In interface design, always do the least surprising thing
[8] Rule of Repair: When you must fail, fail noisily and as soon as possible
[9] Rule of Economy: Programmer time is expensive; conserve it in preference to machine time
[10] Rule of Generation: Avoid hand-hacking; write programs to write programs when you can
[11] Rule of Representation: Use smart data so program logic can be stupid and robust
[12] Rule of Separation: Separate policy from mechanism; separate interfaces from engines
[13] Rule of Optimization: Prototype before polishing. Get it working before you optimize it
[14] Rule of Diversity: Distrust all claims for “one true way”
[15] Rule of Extensibility: Design for the future, because it will be here sooner than you think
基于这些哲学,就导致了unix系统中有很多小的工具,每个工具的功能都很单一,并且这些工具可以任意组合,完成复杂的功能。比如find,grep,awk,xargs等等的组合。
这些功能组合是通过管道|来完成的,即前一个程序的输出,作为下一个程序的输入。
我们看看grep的命令行参数:
Usage: grep [OPTION]... PATTERN
[FILE] ...
Search for PATTERN in
each FILE or standard input.
Example: grep -i 'hello world'
menu.h main.c
我们可以看到,grep可以从多个文件中搜索字符串,并且也可以从stdin读取字符串并搜索。上例中,就是从menu.h main.c中搜索hello world。如果输入grep –i ‘hello’,并回车,然后grep将等待用户输入一行数据,并在该行数据中搜索hello,如果找到的话,就会输出该字符串。
$grep –i ‘hello’
Abcdefg
Hi hello
Hi hello
Stop
^D
$
其中蓝色的字符为用户输入的字符,红色的为grep找到匹配的字符串后,输出的字符。
我们可以猜到grep的实现,如果命令行的最后有一个文件名,或多个文件名,或路径的话,将从这些文件中读取内容,并匹配regex。否则的话,将从stdin读取数据,并匹配regex。
联系到上面所讲的pipe,fork,dup2函数,我们可以看到,这就是shell的基本执行过程。需要注意的是,
grep
必须支持从
stdin
读取数据,否则管道就不能够实现了。当然,想支持管道操作的程序必须遵守这个规则。
4. Xargs
假设有这样一个需求,我们需要从整个文件系统中搜索字符串hello,我们写了如下的shell:
$grep –i ‘hello’ /*/*
这将导致命令行参数太多。而每个系统对于参数列表的大小都有限制。比如ARG_MAX一般至少定义为4096 bytes。如果超过了ARG_MAX,将产生shell错误:
Argument list too lang
为了避免这个问题可以使用xargs命令。他的格式为:
xargs [opt] [
command [initial-arguments] ]
其中opt是xargs本身的命令行参数。
他的作用为,build and execute command lines from standard input。他从stdin读取由空格分割的字符串(假设为arg0,arg1,… argN),并执行
command [initial-arguments] arg0 arg1 …argN
,如果参数太多的话,
xargs
保证参数大小在不超过系统限制的
ARG_MAX bytes
大小的前提下,一次或多次执行
command [initial-arguments]
命令。比如执行了如下命令:
$find / -name ‘*.h’ | xargs grep –i ‘stdin’ | less
假设执行两次,第一次为
grep –i ‘stdin’ a1.h a2.h … a3000.h | less
第二次为
grep –i ‘stdin’ a3001.h a3002.h … a4000.h | less
该命令的实际执行情况为:(推测)
Shell
执行
find
,
xargs
和
less
程序。
xargs
顺序执行了两次
grep
程序。
xargs从find的结果读取数据是很普通的,不需要额外的解释。
xargs同less的数据传递看起来有些麻烦,其实也挺简单的。
xargs
从
stdin
读取管道的数据,并按照
ARG_MAX
为界限进行分割,执行
fork
和
execv(“grep”)
一次或多次就可以了。因为
grep
使用普通的
printf
来输出结果,而这样的结果正好作为
less
的输入。因为
xargs
和
grep
虽然存在父子关系,但是他们的
stdout
是同一个
stdout
。对于
less
程序来讲,
grep
的输出和
xargs
的输出是没有区别的。
注意:
[1] XXX | grep –i ‘hello’
[2] XXX | xargs grep –i ‘hello’
[1]
的情况下,
grep
将通过管道读取
XXXX
的输出结果,并在该结果中搜索
hello
。
[2]
的情况下,
xargs
将通过管道读取
XXXX
的输出结果,并将该结果作为
grep
的最后的
FILE
参数,和
grep –i ‘hello’
组合成完整的命令(如
grep –i ‘hello’ stdio.h stdlib.h
)后,执行该命令。
grep
从
stdio.h stdlib.h
文件中搜索
hello
。
两者对于管道的输出作不同的处理,前者是
grep
直接从
stdin
中读取管道数据,并搜索。后者是
xargs
(他的命令行选项为
grep –i ‘hello’
)
直接从
stdin
中读取管道数据,并和
grep –i ‘hello’
组合,然后调用
exec
执行该命令。这种不同是体现在
grep
和
xargs
对读取管道数据后采取的不同处理,和管道本身的机制没有关系。