http://hi.baidu.com/leejun_2005/item/50a4c525a10c93cca4275a46

linux exec与重定向

exec和source都属于bash内部命令(builtins commands),在bash下输入man exec或man source可以查看所有的内部命令信息。

bash shell的命令分为两类:外部命令和内部命令。外部命令是通过系统调用或独立的程序实现的,如sed、awk等等。内部命令是由特殊的文件格式(.def)所实现,如cd、history、exec等等。

fork概念

在说明exe和source的区别之前,先说明一下fork的概念。

fork是linux的系统调用,用来创建子进程(child process)。子进程是父进程(parent process)的一个副本,从父进程那里获得一定的资源分配以及继承父进程的环境。子进程与父进程唯一不同的地方在于pid(process id)。

环境变量(传给子进程的变量,遗传性是本地变量和环境变量的根本区别)只能单向从父进程传给子进程。不管子进程的环境变量如何变化,都不会影响父进程的环境变量。

有两种方法执行shell scripts,一种是新产生一个shell,然后执行相应的shell scripts;一种是在当前shell下执行,不再启用其他shell。

新产生一个shell然后再执行scripts的方法是在scripts文件开头加入以下语句:#!/bin/sh

一般的script文件(.sh)即是这种用法。这种方法先启用新的sub-shell(新的子进程),然后在其下执行命令。

另外一种方法就是上面说过的source命令,不再产生新的shell,而在当前shell下执行一切命令。

source命令即点(.)命令。

在bash下输入man source,找到source命令解释处,可以看到解释”Read and execute commands from filename in the current shell environment and …”。从中可以知道,source命令是在当前进程中执行参数文件中的各个命令,而不是另起子进程(或sub-shell)。

在bash下输入man exec,找到exec命令解释处,可以看到有”No new process is created.”这样的解释,这就是说exec命令不产生新的子进程。那么exec与source的区别是什么呢?

exec命令在执行时会把当前的shell process关闭,然后换到后面的命令继续执行。

系统调用exec是以新的进程去代替原来的进程,但进程的PID保持不变。因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。

一个进程主要包括以下几个方面的内容:

  1. 一个可以执行的程序
  2. 与进程相关联的全部数据(包括变量,内存,缓冲区)
  3. 程序上下文(程序计数器PC,保存程序执行的位置)

exec是一个函数簇,由6个函数组成,分别是以excl和execv打头的。

执行exec系统调用,一般都是这样,用fork()函数新建立一个进程,然后让进程去执行exec调用。我们知道,在fork()建立新进程之后,父进各与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容copy到子进程中去,还有上下文也会copy到子进程中去。而为了提高效率,采用一种写时copy的策略,即创建子进程的时候,并不copy父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。从而父子进程拥有独立的地址空间。而对于fork()之后执行exec后,这种策略能够很好的提高效率,如果一开始就copy,那么exec之后,子进程的数据会被放弃,被新的进程所代替。

总之,如果你用exec调用,首先应该fork一个新的进程,然后exec. 而system不需要你fork新进程,已经封装好了。

详解及应用实例

基本概念

(这是理解后面的知识的前提,请务必理解)

FD 说明
0 stdin,标准输入
1 stdout,标准输出
2 stderr,标准错误输出
# 查看文件描述符
lsof -a -p $$ -d0,1,2
ll /proc/$$/fd

FD用来改变送出的数据信道(stdout, stderr),使之输出到指定的档案;

0 是 与 1> 是一样的;

在IO重定向中,stdout 与 stderr 的管道会先准备好,才会从 stdin 读进资料;

管道|(pipe line):上一个命令的 stdout 接到下一个命令的 stdin;

tee 命令是在不影响原本 I/O 的情况下,将 stdout 复制一份到档案去;

bash(ksh)执行命令的过程:分析命令-变量求值-命令替代(``和$( ))-重定向-通配符展开-确定路径-执行命令;

( ) 将 command group 置于 sub-shell 去执行,也称 nested sub-shell,它有一点非常重要的特性是:继承父shell的Standard input, output, and error plus any other open file descriptors。

exec 命令:常用来替代当前 shell 并重新启动一个 shell,换句话说,并没有启动子shell。使用这一命令时任何现有环境都将会被清除。 exec在对文件描述符进行操作的时候,也只有在这时,exec不会覆盖你当前的 shell 环境。

常用重定向

cmd &n 使用系统调用 dup (2) 复制文件描述符 n 并把结果用作标准输出

&- 关闭标准输出

n&- 表示将 n 号输出关闭

上述所有形式都可以前导一个数字,此时建立的文件描述符由这个数字指定而不是缺省的 0 或 1。如:

... 2>file 运行一个命令并把错误输出(文件描述符 2)定向到 file。

... 2>&1 运行一个命令并把它的标准输出和输出合并。(严格的说是通过复制文件描述符 1 来建立文件描述符 2 ,但效果通常是合并了两个流。)

我们对 2>&1详细说明一下 :

2>&1 也就是 FD2=FD1 ,这里并不是说FD2 的值等于FD1的值,因为 > 是改变送出的数据信道,也就是说把 FD2 的 “数据输出通道” 改为 FD1 的 “数据输出通道”。

如果仅仅这样,这个改变好像没有什么作用,因为 FD2 的默认输出和 FD1 的默认输出本来都是 monitor,一样的!但是,当 FD1 是其他文件,甚至是其他 FD 时,这个就具有特殊的用途了。请大家务必理解这一点。

恢复

如果 stdin, stdout, stderr 进行了重定向或关闭, 但没有保存原来的 FD, 可以将其恢复到 default 状态吗?

如果关闭了stdin,因为会导致退出,那肯定不能恢复。

如果重定向或关闭 stdout和stderr其中之一,可以恢复,因为他们默认均是送往monitor(但不知会否有其他影响)。

如恢复重定向或关闭的 stdout: exec 1>&2 ,恢复重定向或关闭的stderr:exec 2>&1

如果stdout和stderr全部都关闭了,又没有保存原来的FD,可以用:exec 1>/dev/tty 恢复。

cmd >a 2>a :stdout和stderr都直接送往文件 a ,a文件会被打开两遍,由此导致stdout和stderr互相覆盖。

cmd >a 2>&1 :stdout直接送往文件a ,stderr是继承了FD1的管道之后,再被送往文件a 。a文件只被打开一遍,就是FD1将其打开。

我想:他们的不同点在于:

cmd >a 2>a 相当于使用了两个互相竞争使用文件a的管道;

而cmd >a 2>&1 只使用了一个管道,但在其源头已经包括了stdout和stderr。

从IO效率上来讲,cmd >a 2>&1的效率应该更高!

I/O和I/O重定向的详细介绍

取至《高级Bash脚本编程指南》中内容。

http://www.tsnc.edu.cn/default/tsnc_wgrj/doc/abs-3.9.1_cn/html/ioredirintro.html

I/O重定向

默认情况下始终有3个"文件"处于打开状态, stdin(键盘), stdout(屏幕), 和stderr(错误消息输出到屏幕上). 这3个文件和其他打开的文件都可以被重定向. 对于重定向简单的解释就是捕捉一个文件, 命令, 程序, 脚本, 或者是脚本中的代码块(请参考例子 3-1和例子 3-2)的输出, 然后将这些输出作为输入发送到另一个文件, 命令, 程序, 或脚本中.

每个打开的文件都会被分配一个文件描述符. [1] stdin, stdout, 和stderr的文件描述符分别是0, 1, 和 2. 除了这3个文件, 对于其他那些需要打开的文件, 保留了文件描述符3到9. 在某些情况下, 将这些额外的文件描述符分配给stdin, stdout, 或stderr作为临时的副本链接是非常有用的. [2] 在经过复杂的重定向和刷新之后需要把它们恢复成正常状态(请参考例子 16-1).

# --------------------------------------------------------------------
COMMAND_OUTPUT >
   # 将stdout重定向到一个文件.
   # 如果这个文件不存在, 那就创建, 否则就覆盖.

ls -lR > dir-tree.list
   # 创建一个包含目录树列表的文件.

: > filename
   # >操作, 将会把文件"filename"变为一个空文件(就是size为0).
   # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
   # :是一个占位符, 不产生任何输出.

> filename
   # >操作, 将会把文件"filename"变为一个空文件(就是size为0).
   # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
   # (与上边的": >"效果相同, 但是某些shell可能不支持这种形式.)

COMMAND_OUTPUT >>
   # 将stdout重定向到一个文件.
   # 如果文件不存在, 那么就创建它, 如果存在, 那么就追加到文件后边.


   # 单行重定向命令(只会影响它们所在的行):
# --------------------------------------------------------------------
1>filename
   # 重定向stdout到文件"filename".
1>>filename
   # 重定向并追加stdout到文件"filename".
2>filename
   # 重定向stderr到文件"filename".
2>>filename
   # 重定向并追加stderr到文件"filename".
&>filename
   # 将stdout和stderr都重定向到文件"filename".

M>N
  # "M"是一个文件描述符, 如果没有明确指定的话默认为1.
  # "N"是一个文件名.
  # 文件描述符"M"被重定向到文件"N".
M>&N
  # "M"是一个文件描述符, 如果没有明确指定的话默认为1.
  # "N"是另一个文件描述符.

# --------------------------------------------------------------------
# 重定向stdout, 一次一行.
LOGFILE=script.log

echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
# 每行过后, 这些重定向命令会自动"reset".


# --------------------------------------------------------------------
# 重定向stderr, 一次一行.
ERRORFILE=script.errors

bad_command1 2>$ERRORFILE       #  Error message sent to $ERRORFILE.
bad_command2 2>>$ERRORFILE      #  Error message appended to $ERRORFILE.
bad_command3                    #  Error message echoed to stderr,
                                #+ and does not appear in $ERRORFILE.
# 每行过后, 这些重定向命令也会自动"reset".

# --------------------------------------------------------------------
2>&1
   # 重定向stderr到stdout.
   # 将错误消息的输出, 发送到与标准输出所指向的地方.

i>&j
   # 重定向文件描述符i到j.
   # 指向i文件的所有输出都发送到j.

>&j
   # 默认的, 重定向文件描述符1(stdout)到j.
   # 所有传递到stdout的输出都送到j中去.

0< FILENAME
 < FILENAME
   # 从文件中接受输入.
   # 与">"是成对命令, 并且通常都是结合使用.
   #
   # grep search-word <filename

[j]<>filename
   # 为了读写"filename", 把文件"filename"打开, 并且将文件描述符"j"分配给它.
   # 如果文件"filename"不存在, 那么就创建它.
   # 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
   #
   # 这种应用通常是为了写到一个文件中指定的地方.
   echo 1234567890 > File    # 写字符串到"File".
   exec 3<> File             # 打开"File"并且将fd 3分配给它.
   read -n 4 <&3             # 只读取4个字符.
   echo -n . >&3             # 写一个小数点.
   exec 3>&-                 # 关闭fd 3.
   cat File                  # ==> 1234.67890
   # 随机访问.

|
   # 管道.
   # 通用目的处理和命令链工具.
   # 与">", 很相似, 但是实际上更通用.
   # 对于想将命令, 脚本, 文件和程序串连起来的时候很有用.
   cat *.txt | sort | uniq > result-file
   # 对所有.txt文件的输出进行排序, 并且删除重复行.
   # 最后将结果保存到"result-file"中.

请参考例子 12-28和例子 A-15.

子进程继承了打开的文件描述符. 这就是为什么管道可以工作. 如果想阻止fd被继承, 那么可以关掉它.

# 只将stderr重定到一个管道.

exec 3>&1                              # 保存当前stdout的"值"(将fd3指向fd0相同目标)
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # 对'grep'关闭fd 3(但不关闭'ls',正常输出内容不受grep影响)
#              ^^^^   ^^^^
ls -l 2>&1 >&3 | grep bad              # 这样输出内容被转到了fd3,也不会受grep影响
ls badabc -l 2>&1 >&3 |grep bad        # stderr通过fd1输出,会受grep影响

exec 3>&-                              # 对于剩余的脚本来说, 关闭它.

# 感谢, S.C.

如果想了解关于I/O重定向更多的细节, 请参考Appendix E. 注意事项

使用exec

exec <filename命令会将stdin重定向到文件中. 从这句开始, 所有的stdin就都来自于这个文件了, 而不是标准输入(通常都是键盘输入). 这样就提供了一种按行读取文件的方法, 并且可以使用sed和/或awk来对每一行进行分析.

同样的, exec >filename命令将会把stdout重定向到一个指定的文件中. 这样所有命令的输出就都会发送到那个指定的文件, 而不是stdout.

exec N > filename会影响整个脚本或当前shell. 对于这个指定PID的脚本或shell来说, 从这句命令执行之后, 就会重定向到这个文件中, 然而 . . .

N > filename只会影响新fork出来的进程, 而不会影响整个脚本或shell. not the entire script or shell.

感谢你, Ahmed Darwish, 指出这个问题.

I/O重定向是一种避免可怕的子shell中不可访问变量问题的方法.

代码块重定向

象while, until, 和for循环代码块, 甚至if/then测试结构的代码块, 都可以对stdin进行重定向. 即使函数也可以使用这种重定向方式(请参考例子 23-11). 要想做到这些, 都要依靠代码块结尾的<操作符.

我们也可以修改前面的例子使其能重定向循环的标准输出.

重定向代码块的stdout, 与"将代码块的输出保存到文件中"具有相同的效果. 请参考例子 3-2.

重定向的应用

巧妙地运用I/O重定向, 能够解析和粘合命令输出的各个片断(请参考例子 11-7). 这样就可以产生报告与日志文件.

Appendix E. I/O和I/O重定向的详细介绍

由Stephane Chazelas编写, 本书作者修订

一个命令期望前3个文件描述符是可用的. 第一个, fd 0(标准输入, stdin), 用作读取. 另外两个, (fd 1, stdout和fd 2, stderr), 用来写入.

每个命令都会关联到stdin, stdout, 和stderr. ls 2>&1意味着临时的将ls命令的stderr连接到shell的stdout.

按惯例, 命令一般都是从fd 0(stdin)上读取输入, 打印输出到fd 1(stdout)上, 错误输出一般都输出到fd 2(stderr)上. 如果这3个文件描述中的某一个没打开, 你可能就会遇到麻烦了:

bash$ cat /etc/passwd >&-
cat: standard output: Bad file descriptor

比如说, 当xterm运行的时候, 它首先会初始化自身. 在运行用户shell之前, xterm会打开终端设备(/dev/pts/<n> 或者类似的东西)三次.

这里, Bash继承了这三个文件描述符, 而且每个运行在Bash上的命令(子进程)也都依次继承了它们, 除非你重定向了这些命令. 重定向意味着将这些文件描述符中的某一个, 重新分配到其他文件中(或者分配到一个管道中, 或者是其他任何可能的东西). 文件描述符既可以被局部重分配(对于一个命令, 命令组, 一个子shell, 一个while循环, if或case结构...), 也可以全局重分配, 对于余下的shell(使用exec).

ls > /dev/null 表示将运行的ls命令的fd 1连接到/dev/null上.

bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    363 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        2u   CHR  136,1         3 /dev/pts/1

bash$ exec 2> /dev/null
bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    371 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        2w   CHR    1,3       120 /dev/null

bash$ bash -c 'lsof -a -p $$ -d0,1,2' | cat
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    379 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    379 root    1w  FIFO    0,0      7118 pipe
 lsof    379 root    2u   CHR  136,1         3 /dev/pts/1

bash$ echo "$(bash -c 'lsof -a -p $$ -d0,1,2' 2>&1)"
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    426 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    426 root    1w  FIFO    0,0      7520 pipe
 lsof    426 root    2w  FIFO    0,0      7520 pipe

这是用来展示不同类型的重定向.

练习: 分析下面的脚本.

#!/usr/bin/env bash

mkfifo /tmp/fifo1 /tmp/fifo2

while read a; do echo "FIFO1: $a"; done < /tmp/fifo1 &
exec 7> /tmp/fifo1
exec 8> >(while read a; do echo "FD8: $a, to fd7"; done >&7)

exec 3>&1
(
 (
  (
   while read a; do echo "FIFO2: $a"; done < /tmp/fifo2 | tee /dev/stderr | tee /dev/fd/4 | tee /dev/fd/5 | tee /dev/fd/6 >&7 &
   exec 3> /tmp/fifo2

   echo 1st, to stdout
   sleep 1
   echo 2nd, to stderr >&2
   sleep 1
   echo 3rd, to fd 3 >&3
   sleep 1
   echo 4th, to fd 4 >&4
   sleep 1
   echo 5th, to fd 5 >&5
   sleep 1
   echo 6th, through a pipe | sed 's/.*/PIPE: &, to fd 5/' >&5
   sleep 1
   echo 7th, to fd 6 >&6
   sleep 1
   echo 8th, to fd 7 >&7
   sleep 1
   echo 9th, to fd 8 >&8

  ) 4>&1 >&3 3>&- | while read a; do echo "FD4: $a"; done 1>&3 5>&- 6>&-
 ) 5>&1 >&3 | while read a; do echo "FD5: $a"; done 1>&3 6>&-
) 6>&1 >&3 | while read a; do echo "FD6: $a"; done 3>&-

rm -f /tmp/fifo1 /tmp/fifo2

# 对于每个命令和子shell, 分别指出每个fd的指向.

exit 0