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退出,因此我這裏就沒有辦法給你們拷貝內容看了,因此你們將就一下就行了。