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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章