Bash的function中exit不退出程序

Bash的function中exit不退出程序

现象

最近脚本写的多,注意到一个以前也遇到过但是从没仔细思考的问题:

为什么某些情况下function中exit,脚本依旧继续执行?

举个例子,exit_test.sh:

function exit_test()
{
    echo in
    exit
}

echo $$

exit_test | tee
echo out

echo 'why get here'

执行这个脚本会输出why get here。一般情况下,我们期望在脚本中任何地方exit都要结束进程,那这里发生了什么呢?

原因

分部分屏蔽可能导致这现象的代码,很容易发现,如果把

exit_test | tee

改为

exit_test

就会正常退出。
废话不多说了,直接上gdb调试bash的过程:

  • exit_test的结果:
Breakpoint 1, execute_function (var=0x739bc8, words=0x739828, flags=0, fds_to_close=0x739508, async=0, subshell=0) at execute_cmd.c:4378
4378    {
(gdb) bt
#0  execute_function (var=0x739bc8, words=0x739828, flags=0, fds_to_close=0x739508, async=0, subshell=0) at execute_cmd.c:4378
#1  0x000000000043d995 in execute_builtin_or_function (words=0x739828, builtin=0x0, var=0x739bc8, redirects=0x0, fds_to_close=0x739508, flags=0)
    at execute_cmd.c:4769
#2  0x000000000043c7c0 in execute_simple_command (simple_command=0x739608, pipe_in=-1, pipe_out=-1, async=0, fds_to_close=0x739508)
    at execute_cmd.c:4170
#3  0x0000000000436388 in execute_command_internal (command=0x739788, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x739508)
    at execute_cmd.c:787
#4  0x00000000004359dd in execute_command (command=0x739788) at execute_cmd.c:390
#5  0x0000000000420dea in reader_loop () at eval.c:160
#6  0x000000000041eb30 in main (argc=2, argv=0x7fffffffdd88, env=0x7fffffffdda0) at shell.c:755
  • exit_test | tee的结果:
(gdb) set args './exit_test.sh'
(gdb) b make_child
Breakpoint 1 at 0x44de4b: file jobs.c, line 1717.
(gdb) r
Starting program: /home/guangmu/Downloads/bash-4.3.30/bash './exit_test.sh'
26827

Breakpoint 1, make_child (command=0x7367a8 "exit_test", async_p=0) at jobs.c:1717
1717    {
(gdb) bt
#0  make_child (command=0x7367a8 "exit_test", async_p=0) at jobs.c:1717
#1  0x000000000043c080 in execute_simple_command (simple_command=0x739608, pipe_in=-1, pipe_out=4, async=0, fds_to_close=0x72aee8)
    at execute_cmd.c:3944
#2  0x0000000000436388 in execute_command_internal (command=0x739788, asynchronous=0, pipe_in=-1, pipe_out=4, fds_to_close=0x72aee8)
    at execute_cmd.c:787
#3  0x0000000000438dc1 in execute_pipeline (command=0x739848, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x7394c8) at execute_cmd.c:2344
#4  0x00000000004394cb in execute_connection (command=0x739848, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x7394c8) at execute_cmd.c:2508
#5  0x00000000004366de in execute_command_internal (command=0x739848, asynchronous=0, pipe_in=-1, pipe_out=-1, fds_to_close=0x7394c8)
    at execute_cmd.c:945
#6  0x00000000004359dd in execute_command (command=0x739848) at execute_cmd.c:390
#7  0x0000000000420dea in reader_loop () at eval.c:160
#8  0x000000000041eb30 in main (argc=2, argv=0x7fffffffdd88, env=0x7fffffffdda0) at shell.c:755
(gdb) f 1
#1  0x000000000043c080 in execute_simple_command (simple_command=0x739608, pipe_in=-1, pipe_out=4, async=0, fds_to_close=0x72aee8)
    at execute_cmd.c:3944
3944          if (make_child (savestring (the_printed_command_except_trap), async) == 0)
(gdb) l
3939         vast majority of cases. */
3940          maybe_make_export_env ();
3941    
3942          /* Don't let a DEBUG trap overwrite the command string to be saved with
3943         the process/job associated with this child. */
3944          if (make_child (savestring (the_printed_command_except_trap), async) == 0)
3945        {
3946          already_forked = 1;
3947          simple_command->flags |= CMD_NO_FORK;
3948    
(gdb) p dofork
$1 = 1
(gdb) l 3926
3921    
3922      /* If we're in a pipeline or run in the background, set DOFORK so we
3923         make the child early, before word expansion.  This keeps assignment
3924         statements from affecting the parent shell's environment when they
3925         should not. */
3926      dofork = pipe_in != NO_PIPE || pipe_out != NO_PIPE || async;
3927    
3928      /* Something like `%2 &' should restart job 2 in the background, not cause
3929         the shell to fork here. */
3930      if (dofork && pipe_in == NO_PIPE && pipe_out == NO_PIPE &&

最后再看看bash的源码里面怎么写的:

static int
execute_simple_command (simple_command, pipe_in, pipe_out, async, fds_to_close)
     SIMPLE_COM *simple_command;
     int pipe_in, pipe_out, async;
     struct fd_bitmap *fds_to_close;
{
  WORD_LIST *words, *lastword;
  char *command_line, *lastarg, *temp;
  int first_word_quoted, result, builtin_is_special, already_forked, dofork;
  pid_t old_last_async_pid;
  sh_builtin_func_t *builtin;
  SHELL_VAR *func;
  volatile int old_builtin, old_command_builtin;

  result = EXECUTION_SUCCESS;
  special_builtin_failed = builtin_is_special = 0;
  command_line = (char *)0;

  QUIT;

  /* If we're in a function, update the line number information. */
  if (variable_context && interactive_shell && sourcelevel == 0)
    line_number -= function_line_number;

  /* Remember what this command line looks like at invocation. */
  command_string_index = 0;
  print_simple_command (simple_command);

#if 0
  if (signal_in_progress (DEBUG_TRAP) == 0 && (this_command_name == 0 || (STREQ (this_command_name, "trap") == 0)))
#else
  if (signal_in_progress (DEBUG_TRAP) == 0 && running_trap == 0)
#endif
    {
      FREE (the_printed_command_except_trap);
      the_printed_command_except_trap = the_printed_command ? savestring (the_printed_command) : (char *)0;
    }

  /* Run the debug trap before each simple command, but do it after we
     update the line number information. */
  result = run_debug_trap ();
#if defined (DEBUGGER)
  /* In debugging mode, if the DEBUG trap returns a non-zero status, we
     skip the command. */
  if (debugging_mode && result != EXECUTION_SUCCESS)
    return (EXECUTION_SUCCESS);
#endif

  first_word_quoted =
    simple_command->words ? (simple_command->words->word->flags & W_QUOTED) : 0;

  last_command_subst_pid = NO_PID;
  old_last_async_pid = last_asynchronous_pid;

  already_forked = dofork = 0;

  /* If we're in a pipeline or run in the background, set DOFORK so we
     make the child early, before word expansion.  This keeps assignment
     statements from affecting the parent shell's environment when they
     should not. */
  dofork = pipe_in != NO_PIPE || pipe_out != NO_PIPE || async;

  /* Something like `%2 &' should restart job 2 in the background, not cause
     the shell to fork here. */
  if (dofork && pipe_in == NO_PIPE && pipe_out == NO_PIPE &&
    simple_command->words && simple_command->words->word &&
    simple_command->words->word->word &&
    (simple_command->words->word->word[0] == '%'))
    dofork = 0;

  if (dofork)
    {
      /* Do this now, because execute_disk_command will do it anyway in the
     vast majority of cases. */
      maybe_make_export_env ();

      /* Don't let a DEBUG trap overwrite the command string to be saved with
     the process/job associated with this child. */
      if (make_child (savestring (the_printed_command_except_trap), async) == 0)
    {
      already_forked = 1;
      simple_command->flags |= CMD_NO_FORK;

      subshell_environment = SUBSHELL_FORK;
      if (pipe_in != NO_PIPE || pipe_out != NO_PIPE)
        subshell_environment |= SUBSHELL_PIPE;
      if (async)
        subshell_environment |= SUBSHELL_ASYNC;

      /* We need to do this before piping to handle some really
         pathological cases where one of the pipe file descriptors
         is < 2. */
      if (fds_to_close)
        close_fd_bitmap (fds_to_close);

      do_piping (pipe_in, pipe_out);
      pipe_in = pipe_out = NO_PIPE;
#if defined (COPROCESS_SUPPORT)
      coproc_closeall ();
#endif

      last_asynchronous_pid = old_last_async_pid;

      CHECK_SIGTERM;
    }
      else
    {
      /* Don't let simple commands that aren't the last command in a
         pipeline change $? for the rest of the pipeline (or at all). */
      if (pipe_out != NO_PIPE)
        result = last_command_exit_value;
      close_pipes (pipe_in, pipe_out);
#if defined (PROCESS_SUBSTITUTION) && defined (HAVE_DEV_FD)
      /* Close /dev/fd file descriptors in the parent after forking the
         last child in a (possibly one-element) pipeline.  Defer this
         until any running shell function completes. */
      if (pipe_out == NO_PIPE && variable_context == 0) /* XXX */
        unlink_fifo_list ();        /* XXX */
#endif
      command_line = (char *)NULL;      /* don't free this. */
      bind_lastarg ((char *)NULL);
      return (result);
    }
    }

  /* If we are re-running this as the result of executing the `command'
     builtin, do not expand the command words a second time. */
  if ((simple_command->flags & CMD_INHIBIT_EXPANSION) == 0)
    {
      current_fds_to_close = fds_to_close;
      fix_assignment_words (simple_command->words);
      /* Pass the ignore return flag down to command substitutions */
      if (simple_command->flags & CMD_IGNORE_RETURN)    /* XXX */
    comsub_ignore_return++;
      words = expand_words (simple_command->words);
      if (simple_command->flags & CMD_IGNORE_RETURN)
    comsub_ignore_return--;
      current_fds_to_close = (struct fd_bitmap *)NULL;
    }
  else
    words = copy_word_list (simple_command->words);

  /* It is possible for WORDS not to have anything left in it.
     Perhaps all the words consisted of `$foo', and there was
     no variable `$foo'. */
  if (words == 0)
    {
      this_command_name = 0;
      result = execute_null_command (simple_command->redirects,
                     pipe_in, pipe_out,
                     already_forked ? 0 : async);
      if (already_forked)
    exit (result);
      else
    {
      bind_lastarg ((char *)NULL);
      set_pipestatus_from_exit (result);
      return (result);
    }
    }

  lastarg = (char *)NULL;

  begin_unwind_frame ("simple-command");

  if (echo_command_at_execute)
    xtrace_print_word_list (words, 1);

  builtin = (sh_builtin_func_t *)NULL;
  func = (SHELL_VAR *)NULL;
  if ((simple_command->flags & CMD_NO_FUNCTIONS) == 0)
    {
      /* Posix.2 says special builtins are found before functions.  We
     don't set builtin_is_special anywhere other than here, because
     this path is followed only when the `command' builtin is *not*
     being used, and we don't want to exit the shell if a special
     builtin executed with `command builtin' fails.  `command' is not
     a special builtin. */
      if (posixly_correct)
    {
      builtin = find_special_builtin (words->word->word);
      if (builtin)
        builtin_is_special = 1;
    }
      if (builtin == 0)
    func = find_function (words->word->word);
    }

  /* In POSIX mode, assignment errors in the temporary environment cause a
     non-interactive shell to exit. */
  if (posixly_correct && builtin_is_special && interactive_shell == 0 && tempenv_assign_error)
    {
      last_command_exit_value = EXECUTION_FAILURE;
      jump_to_top_level (ERREXIT);
    }
  tempenv_assign_error = 0; /* don't care about this any more */

  add_unwind_protect (dispose_words, words);
  QUIT;

  /* Bind the last word in this command to "$_" after execution. */
  for (lastword = words; lastword->next; lastword = lastword->next)
    ;
  lastarg = lastword->word->word;

#if defined (JOB_CONTROL)
  /* Is this command a job control related thing? */
  if (words->word->word[0] == '%' && already_forked == 0)
    {
      this_command_name = async ? "bg" : "fg";
      last_shell_builtin = this_shell_builtin;
      this_shell_builtin = builtin_address (this_command_name);
      result = (*this_shell_builtin) (words);
      goto return_result;
    }

  /* One other possibililty.  The user may want to resume an existing job.
     If they do, find out whether this word is a candidate for a running
     job. */
  if (job_control && already_forked == 0 && async == 0 &&
    !first_word_quoted &&
    !words->next &&
    words->word->word[0] &&
    !simple_command->redirects &&
    pipe_in == NO_PIPE &&
    pipe_out == NO_PIPE &&
    (temp = get_string_value ("auto_resume")))
    {
      int job, jflags, started_status;

      jflags = JM_STOPPED|JM_FIRSTMATCH;
      if (STREQ (temp, "exact"))
    jflags |= JM_EXACT;
      else if (STREQ (temp, "substring"))
    jflags |= JM_SUBSTRING;
      else
    jflags |= JM_PREFIX;
      job = get_job_by_name (words->word->word, jflags);
      if (job != NO_JOB)
    {
      run_unwind_frame ("simple-command");
      this_command_name = "fg";
      last_shell_builtin = this_shell_builtin;
      this_shell_builtin = builtin_address ("fg");

      started_status = start_job (job, 1);
      return ((started_status < 0) ? EXECUTION_FAILURE : started_status);
    }
    }
#endif /* JOB_CONTROL */

run_builtin:
  /* Remember the name of this command globally. */
  this_command_name = words->word->word;

  QUIT;

  /* This command could be a shell builtin or a user-defined function.
     We have already found special builtins by this time, so we do not
     set builtin_is_special.  If this is a function or builtin, and we
     have pipes, then fork a subshell in here.  Otherwise, just execute
     the command directly. */
  if (func == 0 && builtin == 0)
    builtin = find_shell_builtin (this_command_name);

  last_shell_builtin = this_shell_builtin;
  this_shell_builtin = builtin;

  if (builtin || func)
    {
      if (builtin)
        {
      old_builtin = executing_builtin;
      old_command_builtin = executing_command_builtin;
      unwind_protect_int (executing_builtin);   /* modified in execute_builtin */
      unwind_protect_int (executing_command_builtin);   /* ditto */
        }
      if (already_forked)
    {
      /* reset_terminating_signals (); */   /* XXX */
      /* Reset the signal handlers in the child, but don't free the
         trap strings.  Set a flag noting that we have to free the
         trap strings if we run trap to change a signal disposition. */
      reset_signal_handlers ();
      subshell_environment |= SUBSHELL_RESETTRAP;

      if (async)
        {
          if ((simple_command->flags & CMD_STDIN_REDIR) &&
            pipe_in == NO_PIPE &&
            (stdin_redirects (simple_command->redirects) == 0))
        async_redirect_stdin ();
          setup_async_signals ();
        }

      subshell_level++;
      execute_subshell_builtin_or_function
        (words, simple_command->redirects, builtin, func,
         pipe_in, pipe_out, async, fds_to_close,
         simple_command->flags);
      subshell_level--;
    }
      else
    {
      result = execute_builtin_or_function
        (words, builtin, func, simple_command->redirects, fds_to_close,
         simple_command->flags);
      if (builtin)
        {
          if (result > EX_SHERRBASE)
        {
          switch (result)
            {
            case EX_REDIRFAIL:
            case EX_BADASSIGN:
            case EX_EXPFAIL:
              /* These errors cause non-interactive posix mode shells to exit */
              if (posixly_correct && builtin_is_special && interactive_shell == 0)
            {
              last_command_exit_value = EXECUTION_FAILURE;
              jump_to_top_level (ERREXIT);
            }
            }
          result = builtin_status (result);
          if (builtin_is_special)
            special_builtin_failed = 1;
        }
          /* In POSIX mode, if there are assignment statements preceding
         a special builtin, they persist after the builtin
         completes. */
          if (posixly_correct && builtin_is_special && temporary_env)
        merge_temporary_env ();
        }
      else      /* function */
        {
          if (result == EX_USAGE)
        result = EX_BADUSAGE;
          else if (result > EX_SHERRBASE)
        result = EXECUTION_FAILURE;
        }

      set_pipestatus_from_exit (result);

      goto return_result;
    }
    }

  if (autocd && interactive && words->word && is_dirname (words->word->word))
    {
      words = make_word_list (make_word ("cd"), words);
      xtrace_print_word_list (words, 0);
      goto run_builtin;
    }

  if (command_line == 0)
    command_line = savestring (the_printed_command_except_trap ? the_printed_command_except_trap : "");

#if defined (PROCESS_SUBSTITUTION)
  if ((subshell_environment & SUBSHELL_COMSUB) && (simple_command->flags & CMD_NO_FORK) && fifos_pending() > 0)
    simple_command->flags &= ~CMD_NO_FORK;
#endif

  result = execute_disk_command (words, simple_command->redirects, command_line,
            pipe_in, pipe_out, async, fds_to_close,
            simple_command->flags);

 return_result:
  bind_lastarg (lastarg);
  FREE (command_line);
  dispose_words (words);
  if (builtin)
    {
      executing_builtin = old_builtin;
      executing_command_builtin = old_command_builtin;
    }
  discard_unwind_frame ("simple-command");
  this_command_name = (char *)NULL; /* points to freed memory now */
  return (result);
}

其中,下面的代码处理了在管道或后台进程(jobs)的情况下,是否fork的问题:

  /* If we're in a pipeline or run in the background, set DOFORK so we
     make the child early, before word expansion.  This keeps assignment
     statements from affecting the parent shell's environment when they
     should not. */
  dofork = pipe_in != NO_PIPE || pipe_out != NO_PIPE || async;

  /* Something like `%2 &' should restart job 2 in the background, not cause
     the shell to fork here. */
  if (dofork && pipe_in == NO_PIPE && pipe_out == NO_PIPE &&
    simple_command->words && simple_command->words->word &&
    simple_command->words->word->word &&
    (simple_command->words->word->word[0] == '%'))
    dofork = 0;

结论

bash对于带有管道的那一行命令和没有管道的情况是不同的,带有管道的命令将先fork后执行。此时在function中的exit事实上是结束了子进程。

解决办法

result=$(exit_test)? No. 依旧是fork。
要获得function输出的同时又能在函数内部exit,我现在找到的办法只有重定向,下面是示例代码:

function exit_test()
{
    echo in
    exit
}

echo $$

exit_test | tee
echo out

echo 'why get here'

result=`exit_test`
echo -n 'result: '
echo "$result"
echo out

echo 'why get here'

exit_test > ./exit_test.log
# Script exited.
echo -n 'log: '
cat ./exit_test.log
echo out

echo 'why get here'

一些疑惑

昨晚写到2点,知道了bash对pipeline的特殊处理。实在太困去睡了,但留下了几个疑惑尚待解开:

  • 在上文的函数exit_test中,echo $$的结果和函数外面的父进程是一样的,为什么?
  • pipeline先fork后execute,这是设计上可选的,还是实现上必须的,又或者这就是不应该的呢?

在上文的函数exit_test中,echo $$的结果和函数外面的父进程是一样的,为什么?

这里有一个例子:

function exit_test()
{
        echo $$
        echo 'before exit'
        exit
        echo 'after exit'
        echo $$
}

echo $$
exit_test | tee

echo 'why get here'

运行输出:

6017
6017
before exit
why get here

由上文我们知道,exit_test | tee这行中进入exit_test函数后已经在子进程了,gdb显示的进程号也是新的,但是输出的结果依旧是父进程的进程号。
首先我们看看manual怎么写的:

$ man bash
...
   Pipelines
       A pipeline is a sequence of one or more commands separated by one of the control operators | or |&.  The format for a pipeline is:

              [time [-p]] [ ! ] command [ [|│|&] command2 ... ]

       The standard output of command is connected via a pipe to the standard input of command2.  This connection is  performed  before  any  redirections
       specified  by  the command (see REDIRECTION below).  If |& is used, the standard error of command is connected to command2’s standard input through
       the pipe; it is shorthand for 2>&1 |.  This implicit redirection of the standard error is performed after any redirections specified  by  the  com-
       mand.

       The  return  status  of  a  pipeline  is  the  exit status of the last command, unless the pipefail option is enabled.  If pipefail is enabled, the
       pipeline’s return status is the value of the last (rightmost) command to exit with a non-zero status, or zero if all  commands  exit  successfully.
       If  the  reserved word !  precedes a pipeline, the exit status of that pipeline is the logical negation of the exit status as described above.  The
       shell waits for all commands in the pipeline to terminate before returning a value.

       If the time reserved word precedes a pipeline, the elapsed as well as user and system time consumed by its execution are reported when the pipeline
       terminates.  The -p option changes the output format to that specified by POSIX.  The TIMEFORMAT variable may be set to a format string that speci-
       fies how the timing information should be displayed; see the description of TIMEFORMAT under Shell Variables below.

       Each command in a pipeline is executed as a separate process (i.e., in a subshell).
...
   Special Parameters
       $      Expands to the process ID of the shell.  In a () subshell, it expands to the process ID of the current shell, not the subshell.
...
   Shell Variables
       BASHPID
              Expands to the process id of the current bash process.  This differs from $$ under certain circumstances, such  as  subshells  that  do  not
              require bash to be re-initialized.
...
       PPID   The process ID of the shell’s parent.  This variable is readonly.
...

到这步我觉得我这么多废话还不如直接上文档……
改改测试脚本:

function exit_test()
{
        echo $BASHPID
        echo $PPID
        echo 'before exit'
        exit
        echo 'after exit'
        echo $BASHPID
        echo $PPID
}

echo $BASHPID
exit_test | tee

echo 'why get here'

运行输出:

$ bash ./exit_test.sh
6918
6919
29974
before exit
why get here
$ echo $$
29974

$$变量的实现

直接上代码。

variables.c:
...
/* The value of $$. */
pid_t dollar_dollar_pid;
...

dollar_dollar_pid是一个全局变量,make_child函数并不会在生成子进程后就置这个值:

jobs.c:
/* Fork, handling errors.  Returns the pid of the newly made child, or 0.
   COMMAND is just for remembering the name of the command; we don't do
   anything else with it.  ASYNC_P says what to do with the tty.  If
   non-zero, then don't give it away. */
pid_t
make_child (command, async_p)
     char *command;
     int async_p;
{
...
  /* Create the child, handle severe errors.  Retry on EAGAIN. */
  while ((pid = fork ()) < 0 && errno == EAGAIN && forksleep < FORKSLEEP_MAX)
    {
      /* bash-4.2 */
      /* If we can't create any children, try to reap some dead ones. */
      waitchld (-1, 0);

      sys_error ("fork: retry");
      RESET_SIGTERM;

      if (sleep (forksleep) != 0)
    break;
      forksleep <<= 1;
    }
...
  if (pid == 0)
    {
      /* In the child.  Give this child the right process group, set the
     signals to the default state for a new process. */
      pid_t mypid;

      mypid = getpid ();
#if defined (BUFFERED_INPUT)
      /* Close default_buffered_input if it's > 0.  We don't close it if it's
     0 because that's the file descriptor used when redirecting input,
     and it's wrong to close the file in that case. */
      unset_bash_input (0);
#endif /* BUFFERED_INPUT */

      /* Restore top-level signal mask. */
      sigprocmask (SIG_SETMASK, &top_level_mask, (sigset_t *)NULL);

      if (job_control)
    {
      /* All processes in this pipeline belong in the same
         process group. */

      if (pipeline_pgrp == 0)   /* This is the first child. */
        pipeline_pgrp = mypid;

      /* Check for running command in backquotes. */
      if (pipeline_pgrp == shell_pgrp)
        ignore_tty_job_signals ();
      else
        default_tty_job_signals ();

      /* Set the process group before trying to mess with the terminal's
         process group.  This is mandated by POSIX. */
      /* This is in accordance with the Posix 1003.1 standard,
         section B.7.2.4, which says that trying to set the terminal
         process group with tcsetpgrp() to an unused pgrp value (like
         this would have for the first child) is an error.  Section
         B.4.3.3, p. 237 also covers this, in the context of job control
         shells. */
      if (setpgid (mypid, pipeline_pgrp) < 0)
        sys_error (_("child setpgid (%ld to %ld)"), (long)mypid, (long)pipeline_pgrp);

      /* By convention (and assumption above), if
         pipeline_pgrp == shell_pgrp, we are making a child for
         command substitution.
         In this case, we don't want to give the terminal to the
         shell's process group (we could be in the middle of a
         pipeline, for example). */
      if (async_p == 0 && pipeline_pgrp != shell_pgrp && ((subshell_environment&SUBSHELL_ASYNC) == 0))
        give_terminal_to (pipeline_pgrp, 0);

#if defined (PGRP_PIPE)
      if (pipeline_pgrp == mypid)
        pipe_read (pgrp_pipe);
#endif
    }
      else          /* Without job control... */
    {
      if (pipeline_pgrp == 0)
        pipeline_pgrp = shell_pgrp;

      /* If these signals are set to SIG_DFL, we encounter the curious
         situation of an interactive ^Z to a running process *working*
         and stopping the process, but being unable to do anything with
         that process to change its state.  On the other hand, if they
         are set to SIG_IGN, jobs started from scripts do not stop when
         the shell running the script gets a SIGTSTP and stops. */

      default_tty_job_signals ();
    }

#if defined (PGRP_PIPE)
      /* Release the process group pipe, since our call to setpgid ()
     is done.  The last call to sh_closepipe is done in stop_pipeline. */
      sh_closepipe (pgrp_pipe);
#endif /* PGRP_PIPE */

#if 0
      /* Don't set last_asynchronous_pid in the child */
      if (async_p)
    last_asynchronous_pid = mypid;      /* XXX */
      else
#endif
#if defined (RECYCLES_PIDS)
      if (last_asynchronous_pid == mypid)
        /* Avoid pid aliasing.  1 seems like a safe, unusual pid value. */
    last_asynchronous_pid = 1;
#endif
    }
...
}

而置$$的值也像文档所述,在shell初始化时:

variables.c:
/* Initialize the shell variables from the current environment.
   If PRIVMODE is nonzero, don't import functions from ENV or
   parse $SHELLOPTS. */
void
initialize_shell_variables (env, privmode)
     char **env;
     int privmode;
{
...
  /* Remember this pid. */
  dollar_dollar_pid = getpid ();
...
}
shell.c:
/* Do whatever is necessary to initialize the shell.
   Put new initializations in here. */
static void
shell_initialize ()
{
...
  /* Initialize internal and environment variables.  Don't import shell
     functions from the environment if we are running in privileged or
     restricted mode or if the shell is running setuid. */
#if defined (RESTRICTED_SHELL)
  initialize_shell_variables (shell_environment, privileged_mode||restricted||running_setuid);
#else
  initialize_shell_variables (shell_environment, privileged_mode||running_setuid);
#endif
...
}

pipeline先fork后execute,这是设计上可选的,还是实现上必须的,又或者这就是不应该的呢?

  1. 是,设计上这肯定是可选的,但在我看来这最充分照顾语法的灵活性和实现的便捷性的方案;
  2. 是,从现在的代码上看,这基本是实现上必须的。如果要改动这部分,一点点小小的改动要带来很多非常复杂的函数的大量改动。特别的,这些函数又充斥着如此之多的全局变量和跳转;
  3. 至少,我认为不应该。pipeline中的第一个command无条件fork,在我看来,完全是为了更加容易地实现do_piping。这是一个用心处理能更好解决的问题,而对一个function只fork不exec不shell_initialize,会让bash使用者无意间写出一个不是期望执行结果的脚本。

以上部分可以参考
execute_simple_commandexecute_subshell_builtin_or_functionexecute_builtin_or_function
函数。


如果有时间,我会改一个pipeline第一个command不强制fork的bash。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页