Warum gibt es keinen offensichtlichen Klon oder keine Gabel in einem einfachen Bash-Befehl und wie wird das gemacht?

7

Betrachten Sie Folgendes (mit shSein /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 +++

Es gibt nichts Ungewöhnliches, grepersetzt einen gegabelten Prozess (hier über clone()) durch den Haupt-Shell-Prozess. So weit, ist es gut.

Jetzt mit 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 +++

Hier ist offensichtlich, dass grepPID des Shell-Prozesses und kein offensichtlicher fork()oder clone()Aufruf vorausgesetzt wird . Die Frage ist also, wie man basheine solche Akrobatik ohne einen der Aufrufe erreicht.

Beachten Sie jedoch, dass clone()syscalls angezeigt wird, wenn der Befehl eine Shell-Umleitung enthält, zdf > /dev/null

Sergiy Kolodyazhnyy
quelle
Fügen Sie nach dem einen weiteren Befehl hinzu grep. bashist möglicherweise intelligent genug, um nicht zu verzweigen, wenn nur ein einziger externer Befehl ausgeführt wird.
Kusalananda
@Kusalananda Ja. Ich habe die Quelle sehr schnell CMD_NO_FORK
durchforstet
Zumindest teilweise beantwortet am Ende von unix.stackexchange.com/a/123522/116858
Kusalananda
@Kusalananda Es antwortet ein bisschen, aber es fasst Dinge zusammen und die Frage fragt etwas ganz anderes (der verlinkte Beitrag fragt "Wie man Dinge verfolgt", während ich frage "Warum Bash Trace so aussieht"). Ich war gerade dabei, eine Antwort auf meine eigene Frage zu schreiben. Ich werde es vorerst hier lassen, da es für das verknüpfte Duplikat überhaupt nicht geeignet ist, aber tatsächlich meine beantwortet.
Sergiy Kolodyazhnyy
@Kusalananda Haukes Antwort scheint ungenau zu sein, da Bash nicht in erster Linie einen untergeordneten Prozess erzeugen und nicht anrufen sollte, execve da er sleepeingebaut ist, aber Homers Antwort beantwortet ihn angemessen, aber auch hier nur eine kurze Zusammenfassung. Ich hätte wohl erwähnen sollen, dass ich nach exakten Mechanismen auf Quellcode-Ebene gesucht habe. Würden Sie sagen, dass meine Antwort geeignet ist, Homers in diesem Beitrag zu unterstützen? Es macht mir nichts aus, wenn dieser Beitrag geschlossen wird. Ich habe die Antwort so oder so bekommen
Sergiy Kolodyazhnyy

Antworten:

9

Das sh -c 'command line'ist in der Regel durch die Dinge verwendet , wie system("command line"), ssh host 'command line', vi‚s !, cronund generell alles , was verwendet wird , eine Befehlszeile zu interpretieren, so ist es ziemlich wichtig ist es so effizient wie möglich zu machen.

Forking ist teuer, in Bezug auf CPU-Zeit, Speicher, zugewiesene Dateideskriptoren ... Es ist nur eine Verschwendung von Ressourcen, wenn ein Shell-Prozess vor dem Beenden nur auf einen anderen Prozess wartet. Außerdem ist es schwierig, den Exit-Status des separaten Prozesses, der den Befehl ausführen würde, korrekt zu melden (z. B. wenn der Prozess beendet wird).

Viele Schalen versuchen im Allgemeinen, die Anzahl der Gabeln als Optimierung zu minimieren. Sogar nicht optimierte Schalen mögen bashes in den sh -c cmdoder (cmd in subshell)Fällen. Im Gegensatz zu ksh oder zsh funktioniert dies nicht in bash -c 'cmd > redir'oder bash -c 'cmd1; cmd2'(dasselbe in Unterschalen). ksh93 ist der Prozess, bei dem Gabeln am weitesten vermieden werden.

Es gibt Fälle, in denen diese Optimierung nicht durchgeführt werden kann, z.

sh < file

Wo shkann die Verzweigung für den letzten Befehl nicht übersprungen werden, da möglicherweise mehr Text an das Skript angehängt wird, während dieser Befehl ausgeführt wird. Bei nicht durchsuchbaren Dateien kann das Dateiende nicht erkannt werden, da dies bedeuten kann, dass zu viel zu früh aus der Datei gelesen wird.

Oder:

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

Wo die Shell möglicherweise mehr Befehle ausführen muss, nachdem der "letzte" Befehl ausgeführt wurde.

Stéphane Chazelas
quelle
7

Durch Durchsuchen des Bash-Quellcodes konnte ich herausfinden, dass Bash das Gabeln tatsächlich ignoriert, wenn es keine Pipes oder Umleitungen gibt. Ab Zeile 1601 in 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;
    }

Später gehen diese Flags in die execute_disk_command()Funktion über, wodurch die Ganzzahlvariable nofork eingerichtet wird, die später überprüft wird, bevor versucht wird, sie zu forken . Der eigentliche Befehl selbst wird von der execve()Wrapper- Funktion shell_execve () entweder vom gegabelten oder vom übergeordneten Prozess ausgeführt, und in diesem Fall ist es der eigentliche übergeordnete Prozess.

Der Grund für eine solche Mechanik ist in Stephanes Antwort gut erklärt .


Randnotiz außerhalb des Rahmens dieser Frage: Es sollte beachtet werden, dass es anscheinend darauf ankommt, ob die Shell interaktiv ist oder über läuft -c. Vor dem Ausführen des Befehls befindet sich eine Gabelung. Dies ergibt sich aus der Ausführung straceauf der interaktiven Shell ( strace -e trace=process -f -o test.trace bash) und dem Überprüfen der Ausgabedatei:

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

Siehe auch Warum erzeugt bash keine Unterschale für einfache Befehle?

Sergiy Kolodyazhnyy
quelle