1、管道
管道是Linux中的一个重要概念,你们常常会使用管道来进行一些操做,好比最为常见的是一些命令输出的分屏显示使用 more来管道。可是在日常交互式操做的时候,不多人会关心一个管道命令是否执行成功,由于成功错误一眼就看到了,若是程序出错,一般的程序都会很是友好的提示错在哪里了。可是对于一些脚本中,这个命令的返回值就比较重要了,由于脚本要自生自灭,没有人会在运行时真正关注它。固然这些也不是引出这个问题的缘由,由于我平时也不多写bash的脚本,并且写的makefile的数量要比bash脚本多,可是Makefile中命令的不少语法就是直接的bash脚本,因此有所接触也须要有些了解。
对于早期的makefile模式,好像GNU make的官方文档中就有一个关于典型编译命令的说明,其中对于生成依赖文件的命令大体是如此的:
gcc -M -MM xxx.c | sed -e 'pattern' > xxx.d
你们注意,这个命令看起来 是很是和谐的,可是一些都是在正常输入的前提下。如今假设gcc命令在预处理的时候就出错了,此时你们猜想一下会有什么问题。为了给你们一点思考时间,我扯一下关于正常路径和异常路径的区别。其实对于一个功能,正常路径是一望便知的,可是软件的质量倒是在异常处理机制中见分晓的。反过来讲,对于开发人员来讲,难得的不是解释一个软件结果为何是正确的,而是解释为何软件会出这种异常行为。
好了,假设gcc预处理出错(预处理是能够出错的,好比#include的一个头文件不存在),此时gcc通常会立刻退出运行,此时它向管道的写入端没有任何输出(即便有输出也是不完整的),可是对于以后的sed来讲,它跟着遭殃,它以可写方式打开了依赖文件,因此即便sed自己不会向依赖中写入任何内容,这个xxx.d文件内容也会被清空。这个xx.o文件将会只依赖xxx.c文件,而全部的xxx.c包含(所以会依赖)的文件的更新都不会引发xxx.o从新编译,这就是依赖丢失,更为严重的是,可能使用未更新的.o文件连接出可执行文件而没有错误。
2、管道进程组回收
一、从执行到waitpid的调用链
这里就不废话了,直接显示一下调用链(基于bash4.1版本)
(gdb) bt
#0 0x00426e20 in waitpid () from /lib/libc.so.6
#1 0x08087c3c in waitchld (wpid=2122, block=1) at jobs.c:3063
#2 0x08086878 in wait_for (pid=2122) at jobs.c:2422
#3 0x08072b48 in execute_command_internal (command=0x8124fc8, asynchronous=0,
pipe_in=8, pipe_out=-1, fds_to_close=0x81519e8) at execute_cmd.c:769
#4 0x08074cff in execute_pipeline (command=0x8151088, asynchronous=0,
pipe_in=-1, pipe_out=-1, fds_to_close=0x81519e8) at execute_cmd.c:2150
#5 0x0807507b in execute_connection (command=0x8151088, asynchronous=0,
pipe_in=-1, pipe_out=-1, fds_to_close=0x81519e8) at execute_cmd.c:2243
#6 0x08072e1e in execute_command_internal (command=0x8151088, asynchronous=0,
pipe_in=-1, pipe_out=-1, fds_to_close=0x81519e8) at execute_cmd.c:885
#7 0x080722fa in execute_command (command=0x8151088) at execute_cmd.c:375
#8 0x08060a4a in reader_loop () at eval.c:152
#9 0x0805eae9 in main (argc=1, argv=0xbffff3d4, env=0xbffff3dc) at shell.c:749
(gdb)
二、管道组什么时候从waitchld 返回
在waitchld 函数中,它是经过waitpid(-1,……)来进行子进程的回收,也就是当管道中的全部子进程都被wait到以后bash才返回;或者更通俗的说,就是bash把管道组中的全部命令都当作一个总体来等待,以后其中全部的进程都退出,这个管道才算彻底退出。这样想一想也有道理,否则会有不一致问题,你们本是经过管道链接符手拉手链接一块儿,结束一块儿结束,You jump,I jump。
\bash-4.1\jobs.c
waitchld (wpid, block)
do
{
……
pid = WAITPID (
-1, &status, waitpid_flags);
注意的是这里waitpid的第一个参数是-1,因此能够等待全部子进程,而管道组中的全部进程都是bash的子进程,因此它们都会被这个函数等待到。
……
/* Remember status, and whether or not the process is running. */
child->
status = status;
每一个子进程的退出码都保存在各自进程结构中,不会覆盖和干扰,也就是这里并无决定管道组的返回值。
child->running = WIFCONTINUED(status) ? PS_RUNNING : PS_DONE;
……
}
while ((sigchld || block == 0) && pid > (pid_t)0);
三、管道组返回值肯定
wait_for (pid)
/* The exit state of the command is either the termination state of the
child, or the termination state of the job. If a job, the status
of the last child in the pipeline is the significant one. If the command
or job was terminated by a signal, note that value also. */
termination_state = (job != NO_JOB) ?
job_exit_status (job)
通常经过这个流程来肯定返回值。
:
process_exit_status (child->status);
job_exit_status--->>>raw_job_exit_status (job)
static WAIT
raw_job_exit_status (job)
int job;
{
register PROCESS *p;
int fail;
WAIT ret;
if (
pipefail_opt)
该选项经过 set -o pipefail 命令使能,默认没有打开,若是使能,将管道中最后一个非零返回值将做为整个管道的返回值。
{
fail = 0;
p = jobs[job]->pipe;
do
{
if (WSTATUS (p->status) != EXECUTION_SUCCESS)
fail = WSTATUS(p->status);
p = p->next;
}
while (p != jobs[job]->pipe);
WSTATUS (ret) = fail;
return ret;
}
for (p = jobs[job]->pipe; p->next != jobs[job]->pipe; p = p->next)
不然,管道最后一个进程返回值做为管道命令返回值。
;
return (p->status);
}
四、和$?汇合
在shell中经过$?来显示前一个命令的执行结果,因此咱们看一下这个结果是如何和$?结合在一块儿的。在execute_command_internal函数的最后,会将waitfor的返回值赋值给全局变量 last_command_exit_value :
last_command_exit_value = exec_result;
static WORD_DESC *
param_expand (string, sindex, quoted, expanded_something,
contains_dollar_at, quoted_dollar_at_p, had_quoted_null_p,
pflags)
/* $? -- return value of the last synchronous command. */
case '?':
temp = itos
(last_command_exit_value);
break;
3、bash对于set选项处理位置
对应的,在内核构建的时候,能够看到每次执行命令前都会执行
set -e
,bash手册对于该命令的说明为:
-e Exit immediately if a simple command (see Section 3.2.1 [Simple
Commands], page 8) exits with a non-zero status, unless the command
that fails is part of the command list immediately following
a while or until keyword, part of the test in an if statement,
part of a && or || list, or if the command’s return status is being
inverted using !. A trap on ERR, if set, is executed before the shell
exits.
也就是在执行bash命令时,若是任何一个命令出现错误,那么马上终止执行。也就是说,其中的任何一个命令都不能返回错误。
这个功能主要是在
bash-4.1\flags.c和
bash-4.1\builtins\set.def
两个文件中实现的。其实set.def文件是很容易找到的,可是对于flags的查找并非那么直观,因此这里记录一下。
4、set -e 实现流程
在flags.c中能够看到,对于该选项对应的内容为全局变量exit_immediately_on_error
{ 'e', &
exit_immediately_on_error },
bash-4.1\execute_cmd.c
execute_command_internal (command, asynchronous, pipe_in, pipe_out,
fds_to_close)
if (ignore_return == 0 && invert == 0 &&
((posixly_correct && interactive == 0 && special_builtin_failed) ||
(
exit_immediately_on_error && pipe_in == NO_PIPE && pipe_out == NO_PIPE && exec_result != EXECUTION_SUCCESS)))
{
last_command_exit_value = exec_result;
run_pending_traps ();
jump_to_top_level (ERREXIT);
}
break;
该跳转将会跳转到
int
reader_loop ()
while
(EOF_Reached == 0)
{
int code;
code = setjmp (top_level);
……
if (code != NOT_JUMPED)
{
indirection_level = our_indirection_level;
switch (code)
{
/* Some kind of throw to top_level has occured. */
case FORCE_EOF:
case ERREXIT:
case EXITPROG:
current_command = (COMMAND *)NULL;
if (exit_immediately_on_error)
variable_context = 0; /* not in a function */
EOF_Reached = EOF;
该赋值将会致使函数主体循环while (EOF_Reached == 0)退出,进而readerloop退出。
goto exec_done;
……
exec_done:
QUIT;
}
indirection_level--;
return (last_command_exit_value);
从reader_loop退出以后进入main函数最后
/* Read commands until exit condition. */
reader_loop ();
exit_shell (last_command_exit_value);
此处整个shell退出,exit_shell--->>>sh_exit--->>>exit (s)。
5、测试代码
一、管道组退出码验证
[tsecer@Harry root]$ sleep 1234 | sleep 30
在另外一个窗口经过kill -9 杀死sleep 1234,管道返回值为0.
You have new mail in /var/spool/mail/root
[tsecer@Harry root]$ echo $?
0
[tsecer@Harry root]$ set -o pipefail
使能pipefail选项。
[tsecer@Harry root]$ sleep 1234 | sleep 30
从另一个窗口中使用kill -9 杀死sleep 1234,此处判断管道执行失败。
Killed
[tsecer@Harry root]$ echo $?
137
[tsecer@Harry root]$
二、set -e 验证 这个比较简单,你们能够试一下 set -e ls /dev/nonexistdir 以后shell退出,因为shell退出,因此我这里就没有办法给你们拷贝内容看了,因此你们将就一下就行了。