Considere o seguinte (com sh
ser /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 (aqui feito via clone()
) do processo principal do shell. Até agora 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 fica aparente é que grep
assume pid do processo shell e não aparente fork()
ou clone()
call. A questão é, então, como bash
conseguir tais acrobacias sem nenhuma das chamadas?
Observe, no entanto, que clone()
syscalls aparece se o comando contiver redirecionamento de shell, comodf > /dev/null
O
sh -c 'command line'
são normalmente usados por coisas comosystem("command line")
,ssh host 'command line'
,vi
's!
,cron
e mais geralmente qualquer coisa que seja usada para interpretar uma linha de comando, então é muito importante torná-la o mais eficiente possível.Bifurcar é caro, em tempo de CPU, memória, descritores de arquivos alocados... Ter um processo de 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 é encerrado).
Muitos shells geralmente tentam minimizar o número de forks como uma otimização. Mesmo shells não otimizados como
bash
fazê-lo nos casossh -c cmd
ou(cmd in subshell)
. Ao contrário do ksh ou zsh, ele não faz isso embash -c 'cmd > redir'
orbash -c 'cmd1; cmd2'
(o mesmo em subshells). ksh93 é o processo que vai mais longe em evitar bifurcações.Há casos em que essa otimização não pode ser feita, como ao fazer:
Onde
sh
não pode pular 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, ele não pode detectar o final do arquivo, pois isso pode significar ler muito cedo do arquivo.Ou:
Onde o shell pode ter que executar mais comandos após o "último" comando ter sido executado.
Ao cavar o código-fonte do bash, consegui descobrir que o bash de fato ignorará a bifurcação se não houver pipes ou redirecionamentos. Da linha 1601 em execute_cmd.c :
Mais tarde, esses sinalizadores vão para a
execute_disk_command()
função, que configura a variável inteira nofork , que depois é verificada antes de tentar bifurcar . O próprio comando real seria executado pela funçãoexecve()
wrapper shell_execve() do processo bifurcado ou pai e, neste caso, é o pai real.A razão para tal mecânica está bem explicada na resposta de Stephane .
Nota lateral fora do escopo desta questão: deve-se notar que aparentemente importa se o shell é interativo ou executado via
-c
. Antes de executar o comando, haverá uma bifurcação. Isso fica evidente ao executarstrace
no shell interativo (strace -e trace=process -f -o test.trace bash
) e verificar o arquivo de saída:Veja também Por que o bash não gera um subshell para comandos simples?