exec和source都属于bash内部命令(builtins commands),在bash下输入man exec或man source可以查看所有的内部命令信息。
bash shell的命令分为两类:外部命令和内部命令。外部命令是通过系统调用或独立的程序实现的,如sed、awk等等。内部命令是由特殊的文件格式(.def)所实现,如cd、history、exec等等。
在说明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系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。原进程的代码段,数据段,堆栈段被新的进程所代替。
一个进程主要包括以下几个方面的内容:
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,标准错误输出 |
1
2
3
|
# 查看文件描述符
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的效率应该更高!
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
31
|
exec
0
exec
1>outfilename
# 打开文件outfilename作为stdout
exec
2>errfilename
# 打开文件errfilename作为 stderr
exec
1&-
# 关闭 FD1
exec
5>&-
# 关闭 FD5
exec
4<&1
# 备份当前stdout至FD4
exec
1>1.txt
# stdout重定向至1.txt
exec
1<&4
# 恢复stdout
exec
4>&-
# 关闭 FD4
# 重定向操作范例
cat
> 1 < 11 22 33 44 55
66 22 33 11 33
324 25 63 634 745
EOF
cat
> 2 < > 1.txt
EOF
exec
4<&1
# 备份当前stdout
exec
1>1.txt
while
read
line;
do
echo
$line;
done
< 1
exec
1<&4
# 恢复stdout
exec
4>&-
sh .
/2
cat
1.txt
|
取至《高级Bash脚本编程指南》中内容。
http://www.tsnc.edu.cn/default/tsnc_wgrj/doc/abs-3.9.1_cn/html/ioredirintro.html
默认情况下始终有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).
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
# --------------------------------------------------------------------
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 [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"中.
|
command < input-file > output-file command1 | command2 | command3 > output-file
请参考例子 12-28和例子 A-15.
1
2
3
4
5
6
7
8
9
10
11
|
ls
-yz >>
command
.log 2>&1
# 将错误选项"yz"的结果放到文件"command.log"中.
# 因为stderr被重定向到这个文件中,
#+ 所有的错误消息也就都指向那里了.
# 注意, 下边这个例子就不会给出相同的结果.
ls
-yz 2>&1 >>
command
.log
# 输出一个错误消息, 但是并不写到文件中.
# 如果将stdout和stderr都重定向,
#+ 命令的顺序会有些不同.
|
1
2
3
4
5
6
7
8
9
|
n<&-
#关闭输入文件描述符n.
0<&-, <&-
#关闭stdin.
n>&-
#关闭输出文件描述符n.
1>&-, >&-
#关闭stdout.
|
子进程继承了打开的文件描述符. 这就是为什么管道可以工作. 如果想阻止fd被继承, 那么可以关掉它.
1
2
3
4
5
6
7
8
9
10
11
|
# 只将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命令将会把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). 要想做到这些, 都要依靠代码块结尾的<操作符.
#!/bin/bash
# 使用'exec'重定向stdin.
exec
6<&0
# 将文件描述符#6与stdin链接起来.
# 保存stdin.
exec
< data-
file
# stdin被文件"data-file"所代替.
read
a1
# 读取文件"data-file"的第一行.
read
a2
# 读取文件"data-file"的第二行.
echo
echo
"Following lines read from file."
echo
"-------------------------------"
echo
$a1
echo
$a2
echo
;
echo
;
echo
exec
0<&6 6<&-
# 现在将stdin从fd #6中恢复, 因为刚才我们把stdin重定向到#6了,
#+ 然后关闭fd #6 ( 6<&- ), 好让这个描述符继续被其他进程所使用.
#
# <&6 6<&- 这么做也可以.
echo
-n
"Enter data "
read
b1
# 现在"read"已经恢复正常了, 就是能够正常的从stdin中读取.
echo
"Input read from stdin."
echo
"----------------------"
echo
"b1 = $b1"
echo
exit
0
#!/bin/bash
# reassign-stdout.sh
LOGFILE=logfile.txt
exec
6>&1
# 将fd #6与stdout链接起来.
# 保存stdout.
exec
> $LOGFILE
# stdout就被文件"logfile.txt"所代替了.
# ----------------------------------------------------------- #
# 在这块中所有命令的输出都会发送到文件$LOGFILE中.
echo
-n
"Logfile: "
date
echo
"-------------------------------------"
echo
echo
"Output of \"ls -al\" command"
echo
ls
-al
echo
;
echo
echo
"Output of \"df\" command"
echo
df
# ----------------------------------------------------------- #
exec
1>&6 6>&-
# 恢复stdout, 然后关闭文件描述符#6.
echo
echo
"== stdout now restored to default == "
echo
ls
-al
echo
exit
0
#!/bin/bash
# upperconv.sh
# 将一个指定的输入文件转换为大写.
E_FILE_ACCESS=70
E_WRONG_ARGS=71
if
[ ! -r
"$1"
]
# 判断指定的输入文件是否可读?
then
echo
"Can't read from input file!"
echo
"Usage: $0 input-file output-file"
exit
$E_FILE_ACCESS
fi
# 即使输入文件($1)没被指定
#+ 也还是会以相同的错误退出(为什么?).
if
[ -z
"$2"
]
then
echo
"Need to specify output file."
echo
"Usage: $0 input-file output-file"
exit
$E_WRONG_ARGS
fi
exec
4<&0
exec
< $1
# 将会从输入文件中读取.
exec
7>&1
exec
> $2
# 将写到输出文件中.
# 假设输出文件是可写的(添加检查?).
# -----------------------------------------------
cat
- |
tr
a-z A-Z
# 转换为大写.
# ^^^^^ # 从stdin中读取.
# ^^^^^^^^^^ # 写到stdout上.
# 然而, stdin和stdout都被重定向了.
# -----------------------------------------------
exec
1>&7 7>&-
# 恢复stout.
exec
0<&4 4<&-
# 恢复stdin.
# 恢复之后, 下边这行代码将会如预期的一样打印到stdout上.
echo
"File \"$1\" written to \"$2\" as uppercase conversion."
exit
0
#!/bin/bash
# avoid-subshell.sh
# 由Matthew Walker所提出的建议.
Lines=0
echo
cat
myfile.txt |
while
read
line;
# (译者注: 管道会产生子shell)
do
{
echo
$line
(( Lines++ ));
# 增加这个变量的值
#+ 但是外部循环却不能访问.
# 子shell问题.
}
done
echo
"Number of lines read = $Lines"
# 0
# 错误!
echo
"------------------------"
exec
3<> myfile.txt
while
read
line <&3
do
{
echo
"$line"
(( Lines++ ));
# 增加这个变量的值
#+ 现在外部循环就可以访问了.
# 没有子shell, 现在就没问题了.
}
done
exec
3>&-
echo
"Number of lines read = $Lines"
# 8
echo
exit
0
# 下边这些行是这个脚本的结果, 脚本是不会走到这里的.
$
cat
myfile.txt
Line 1.
Line 2.
Line 3.
Line 4.
Line 5.
Line 6.
Line 7.
Line 8.
代码块重定向
#!/bin/bash
# redir2.sh
if
[ -z
"$1"
]
then
Filename=names.data
# 如果没有指定文件名, 则使用这个默认值.
else
Filename=$1
fi
#+ Filename=${1:-names.data}
# 这句可代替上面的测试(参数替换).
count=0
echo
while
[
"$name"
!= Smith ]
# 为什么变量$name要用引号?
do
read
name
# 从$Filename文件中读取输入, 而不是在stdin中读取输入.
echo
$name
let
"count += 1"
done
<
"$Filename"
# 重定向stdin到文件$Filename.
# ^^^^^^^^^^^^
echo
;
echo
"$count names read"
;
echo
exit
0
# 注意在一些比较老的shell脚本编程语言中,
#+ 重定向的循环是放在子shell里运行的.
# 因此, $count 值返回后会是 0, 此值是在循环开始前的初始值.
# *如果可能的话*, 尽量避免在Bash或ksh中使用子shell,
#+ 所以这个脚本能够正确的运行.
# (多谢Heiner Steven指出这个问题.)
# 然而 . . .
# Bash有时还是*会*在一个使用管道的"while-read"循环中启动一个子shell,
#+ 与重定向的"while"循环还是有区别的.
abc=hi
echo
-e
"1\n2\n3"
|
while
read
l
do
abc=
"$l"
echo
$abc
done
echo
$abc
# 感谢, Bruno de Oliveira Schneider
#+ 给出上面的代码片段来演示此问题.
# 同时, 感谢, Brian Onn, 修正了一个注释错误.
#!/bin/bash
# 这是上个脚本的另一个版本.
# Heiner Steven建议,