Por que não há clone ou fork aparente no comando bash simples e como é feito?

6

Considere o seguinte (com sh sendo /bin/dash ):

$ strace -e trace=process sh -c 'grep "^Pid:" /proc/self/status /proc/$$/status'
execve("/bin/sh", ["sh", "-c", "grep \"^Pid:\" /proc/self/status /"...], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7fcc8b661540) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fcc8b661810) = 24865
wait4(-1, /proc/self/status:Pid:    24865
/proc/24864/status:Pid: 24864
[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 24865
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=24865, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
exit_group(0)                           = ?
+++ exited with 0 +++

Não há nada incomum, grep substituiu um processo bifurcado (feito aqui por clone() ) do processo shell principal. Até aí tudo bem.

Agora com o bash 4.4:

$ strace -e trace=process bash -c 'grep "^Pid:" /proc/self/status /proc/$$/status'
execve("/bin/bash", ["bash", "-c", "grep \"^Pid:\" /proc/self/status /"...], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7f8416b88740) = 0
execve("/bin/grep", ["grep", "^Pid:", "/proc/self/status", "/proc/25798/status"], [/* 47 vars */]) = 0
arch_prctl(ARCH_SET_FS, 0x7f8113358b80) = 0
/proc/self/status:Pid:  25798
/proc/25798/status:Pid: 25798
exit_group(0)                           = ?
+++ exited with 0 +++

Aqui, o que é aparente é que grep assume pid do processo shell e nenhuma chamada fork() ou clone() aparente. A pergunta é, então, como bash consegue essas acrobacias sem nenhuma das chamadas?

Observe, no entanto, que clone() syscalls aparece se o comando contiver redirecionamento de shell, como df > /dev/null

    
por Sergiy Kolodyazhnyy 03.09.2018 / 08:00

2 respostas

9

Os sh -c 'command line' são normalmente usados por coisas como system("command line") , ssh host 'command line' , vi ! , cron e, mais geralmente, tudo o que é usado para interpretar uma linha de comando, por isso é bastante importante torná-lo o mais eficiente possível.

O bifurcação é caro, em tempo de CPU, memória, descritores de arquivos alocados ... Ter um processo shell esperando por outro processo antes de sair é apenas um desperdício de recursos. Além disso, torna difícil relatar corretamente o status de saída do processo separado que executaria o comando (por exemplo, quando o processo é eliminado).

Muitas camadas geralmente tentam minimizar o número de garfos como uma otimização. Mesmo shells não otimizados como bash fazem isso nos casos sh -c cmd ou (cmd in subshell) . Ao contrário do ksh ou zsh, ele não faz isso em bash -c 'cmd > redir' ou bash -c 'cmd1; cmd2' (mesmo em subshells). ksh93 é o processo que vai mais longe em evitar garfos.

Existem casos em que essa otimização não pode ser feita, como quando se faz:

sh < file

Onde sh não pode ignorar a bifurcação para o último comando, porque mais texto pode ser anexado ao script enquanto esse comando estiver em execução. E para arquivos não pesquisáveis, não é possível detectar o fim do arquivo, pois isso pode significar ler muito cedo demais no arquivo.

Ou:

sh -c 'trap "echo Ouch" INT; cmd'

Onde o shell pode precisar executar mais comandos depois que o comando "last" for executado.

    
por 03.09.2018 / 10:40
8

Ao pesquisar o código-fonte do bash, consegui descobrir que o bash de fato ignorará o processo de bifurcação se não houver canais ou redirecionamentos. De linha 1601 em execute_cmd.c :

  /* If this is a simple command, tell execute_disk_command that it
     might be able to get away without forking and simply exec.
     This means things like ( sleep 10 ) will only cause one fork.
     If we're timing the command or inverting its return value, however,
     we cannot do this optimization. */
  if ((user_subshell || user_coproc) && (tcom->type == cm_simple || tcom->type == cm_subshell) &&
      ((tcom->flags & CMD_TIME_PIPELINE) == 0) &&
      ((tcom->flags & CMD_INVERT_RETURN) == 0))
    {
      tcom->flags |= CMD_NO_FORK;
      if (tcom->type == cm_simple)
    tcom->value.Simple->flags |= CMD_NO_FORK;
    }

Mais tarde, esses sinalizadores vão para a função execute_disk_command() , que configura nofork integer variable, que depois é verificada antes de tentar forking . O próprio comando em si seria executado pelo execve() wrapper function shell_execve () do processo bifurcado ou pai e, neste caso, é o pai real.

A razão para tal mecânica é bem explicada na resposta de Stephane .

Nota lateral fora do escopo desta pergunta: deve-se notar que, aparentemente, é importante se o shell é interativo ou executado via -c . Antes de executar o comando, haverá um fork. Isso fica evidente na execução de strace no shell interativo ( strace -e trace=process -f -o test.trace bash ) e na verificação do arquivo de saída:

19607 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_t
idptr=0x7f2d35e93a10) = 19628
19607 wait4(-1,  <unfinished ...>
19628 execve("/bin/true", ["/bin/true"], [/* 47 vars */]) = 0

Veja também Por que o bash não gera um subshell para comandos simples?

    
por 03.09.2018 / 09:02