bash中管道命令返回值如何确定(上)
发布时间:2020-12-15 18:19:31 所属栏目:安全 来源:网络整理
导读:一、管道 管道是Linux中的一个重要概念,大家经常会使用管道来进行一些操作,比如最为常见的是一些命令输出的分屏显示使用 more来管道。但是在平常交互式操作的时候,很少人会关心一个管道命令是否执行成功,因为成功错误一眼就看到了,如果程序出错,通常的
一、管道
管道是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文件链接出可执行文件而没有错误。 二、管道进程组回收 1、从执行到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,? ??? pipe_in=-1,fds_to_close=0x81519e8) at execute_cmd.c:2150 #5? 0x0807507b in execute_connection (command=0x8151088,fds_to_close=0x81519e8) at execute_cmd.c:2243 #6? 0x08072e1e in execute_command_internal (command=0x8151088,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)? 2、管道组何时从waitchld 返回 在waitchld 函数中,它是通过waitpid(-1,……)来进行子进程的回收,也就是当管道中的所有子进程都被wait到之后bash才返回;或者更通俗的说,就是bash把管道组中的所有命令都当做一个整体来等待,之后其中所有的进程都退出,这个管道才算完全退出。这样想想也有道理,不然会有不一致问题,大家本是通过管道连接符手拉手连接一起,结束一起结束,You jump,I jump。 bash-4.1jobs.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); 3、管道组返回值确定 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); } 4、和$?汇合 在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; 三、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.1flags.c和 bash-4.1builtinsset.def 两个文件中实现的。其实set.def文件是很容易找到的,但是对于flags的查找并不是那么直观,所以这里记录一下。 四、set -e 实现流程 在flags.c中可以看到,对于该选项对应的内容为全局变量exit_immediately_on_error? ? { ‘e‘,& exit_immediately_on_error?}, bash-4.1execute_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)。 五、测试代码 1、管道组退出码验证 [ [email?protected] root]$ sleep 1234 | sleep 30?? 在另一个窗口通过kill -9 杀死sleep 1234,管道返回值为0. You have new mail in /var/spool/mail/root [ [email?protected] root]$ echo $? 0 [ [email?protected] root]$ set -o pipefail??????????? 使能pipefail选项。 [ [email?protected] root]$ sleep 1234 | sleep 30?? 从另外一个窗口中使用kill -9 杀死sleep 1234,此处判断管道执行失败。 Killed [ [email?protected] root]$ echo $? 137 [ [email?protected] root]$? 2、set -e 验证 这个比较简单,大家可以试一下 set -e ls /dev/nonexistdir 之后shell退出,由于shell退出,所以我这里就没有办法给大家拷贝内容看了,所以大家将就一下就好了。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |