bash中管道命令返回值如何确定(上)

一、管道
管道是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, 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) 
2、管道组何时从waitchld 返回
在waitchld 函数中,它是通过waitpid(-1,……)来进行子进程的回收,也就是当管道中的所有子进程都被wait到之后bash才返回;或者更通俗的说,就是bash把管道组中的所有命令都当做一个整体来等待,之后其中所有的进程都退出,这个管道才算完全退出。这样想想也有道理,不然会有不一致问题,大家本是通过管道连接符手拉手连接一起,结束一起结束,You jump,I jump。
ash-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.cbash-4.1uiltinsset.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、管道组退出码验证
[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]$ 
2、set -e 验证
这个比较简单,大家可以试一下
set -e
ls /dev/nonexistdir
之后shell退出,由于shell退出,所以我这里就没有办法给大家拷贝内容看了,所以大家将就一下就好了。
 
 
 
 
 
原文地址:https://www.cnblogs.com/tsecer/p/10486328.html