fork & exec vs. posix_spawn (Fork ;-) aus Programm unter Linux und Windows)



  • ich empfehle für Windows CreateProcess und für Linux posix_spawn

    fork() stammt aus PDP-11 Zeiten, als Programme max. 64 Kb groß waren. Heutzutage kommt es regelmäßig zu einem Speicherüberlauf, wenn ein Hostprozeß, der rel. viel Speicher verbraucht, mit fork() ein kleines ext. Programm starten möchte.



  • Skym0sh0 schrieb:

    Prozesserzeugung = exec-Familie (da gibts ja drölfzig-Brillionen von exec's)

    exec* erzeugt keinen Prozess.



  • Skym0sh0 schrieb:

    nein nein. wofür gibts denn die exec-Familie?!

    in den Übungsaufgaben (wahrscheinlich sehr praxisnah!!!!!!) wurde fork immer nur verwendet/gefordert um quasi Nebenläufigkeit zu erhalten.

    Ich finde es halt fraglich den Programmcode für 2 verschiedene Programmabläufe in das gleiche Programm nur durch ein if getrennt zu implementieren.

    Verschiedene Funktionalitäten = verschiedene Funktionen
    Nebenläufigkeit = Threads
    Prozesserzeugung = exec-Familie (da gibts ja drölfzig-Brillionen von exec's)

    Ist wie gesagt meine halbgare persönliche Meinung dazu...

    Wie kannst Du es fraglich finden, wenn Du nicht weißt, wofür das gut ist? Gehe doch einfach mal davon aus, dass Du den Sinn und Konzept von fork nicht verstanden hast und daher einfach nicht das Wissen hast, um Dir ein Urteil zu bilden.

    Fork wird praktisch immer in Verbindung mit exec verwendet und damit wird das ganze sehr flexibel.

    dd++ schrieb:

    fork() stammt aus PDP-11 Zeiten, als Programme max. 64 Kb groß waren. Heutzutage kommt es regelmäßig zu einem Speicherüberlauf, wenn ein Hostprozeß, der rel. viel Speicher verbraucht, mit fork() ein kleines ext. Programm starten möchte.

    Der ist richtig gut 🙄 . Wenn man keine Ahnung hat, sollte man sich ein wenig zurück halten. Mit fork werden unter Unix neue Prozesse erzeugt. Also ich habe nicht regelmäßig Speicherüberläufe, obwohl ich ständig Prozesse starte (nur als Stichwort: copy-on-write 😉 ).



  • wenn man von einem kleinen Programm ein kleines anderes Programm starten möchte (so wie auf der PDP-11), kann man das auch mit fork+exec machen. wenn man von einem "großen" Programm (mit viel belegtem Speicher) ein kleines Programm starten möchte, ist posix_spawn in den meisten Fällen die bessere Alternative.

    wenn man z.B. von Java oder Ruby mit fork+exec einen neuen Prozeß startet, wird die komplette VM dupliziert. deshalb gibt es spzezielle posix_spawn Bindungen, gucke https://github.com/rtomayko/posix-spawn#readme und http://bryanmarty.com/blog/2012/01/14/forking-jvm/



  • Es wird überhaupt nichts dupliziert. Der Kibnbdprozess bekommt seinen eigenen Kontext + Stack, die Filedeskriptortabelle und Speicher ist mit dem Elternprozess geteilt und wird erst bei Änderungen kopiert ... was bei einem sofortigem exec* nie auftritt. Von daher ist fork ähnlich schlank wie das Erzeugen eines Threads, solange der neue Prozess nur seine eigenen Resourcen anfässt.



  • wenn ich direkt nach fork() exec*() aufrufe, wozu brauche ich dann fork()? kann man das nicht auch in einem einzigen Funktionsaufruf zusammenfassen? ja, man kann, dieser Aufruf heißt posix_spawn()


  • Mod

    Die Diskussion ist mittlerweile so offtopic, dass ich sie abgetrennt und in ein passenderes Forum verschoben habe.



  • Dieser Thread wurde von Moderator/in SeppJ aus dem Forum C++ (auch C++0x und C++11) in das Forum Linux/Unix verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.



  • dd++ schrieb:

    wenn ich direkt nach fork() exec*() aufrufe, wozu brauche ich dann fork()? kann man das nicht auch in einem einzigen Funktionsaufruf zusammenfassen? ja, man kann, dieser Aufruf heißt posix_spawn()

    So einfach ist es nicht. In der Regel wird eben nicht direkt nach fork gleich exec aufgerufen, sondern erst mal die Umgebung für den neuen Prozess angepasst. Zwischen fork und exec kann ich beispielsweise die Standardeingabe und -ausgabe umbiegen oder den Benutzer wechseln, wenn das Programm unter root-Rechten gestartet wurde oder Umgebungsvariablen setzen. Einiges geht mit posix_spawn aber eben nicht alles. fork und exec sind halt noch eine Etage tiefer angesiedelt.



  • dd++ schrieb:

    wenn man von einem kleinen Programm ein kleines anderes Programm starten möchte (so wie auf der PDP-11), kann man das auch mit fork+exec machen. wenn man von einem "großen" Programm (mit viel belegtem Speicher) ein kleines Programm starten möchte, ist posix_spawn in den meisten Fällen die bessere Alternative.

    wenn man z.B. von Java oder Ruby mit fork+exec einen neuen Prozeß startet, wird die komplette VM dupliziert. deshalb gibt es spzezielle posix_spawn Bindungen, gucke https://github.com/rtomayko/posix-spawn#readme und http://bryanmarty.com/blog/2012/01/14/forking-jvm/

    Danke übrigens für den Hinweis. Das erinnert mich mal wieder daran, wie krank Java ist. Der 2. Artikel von Bryan Marty erwähnt ein erwähnt ein embedded System mit extrem wenig Arbeitsspeicher. Nämlich 512M 😮 . Das ist schon ein krankes System, welches mit so einem gigantischen Speicher noch Probleme bekommt.



  • dd++ schrieb:

    wenn man z.B. von Java oder Ruby mit fork+exec einen neuen Prozeß startet, wird die komplette VM dupliziert.

    Liest du eigentlich auch, was du verlinkst? Das steht da nämlich so nirgends. Wäre auch bei jeder halbwegs vernünftigen Fork-Implementierung riesiger Unfug.

    Jedes Unix, das etwas auf sich hält, implementiert den Fork-Systemcall so, dass nur solche Speicherseiten kopiert werden, auf die nach dem fork(2)-Aufruf schreibend zugegriffen wird. Copy-on-Write eben. Erreichen lässt sich das ohne großen Mehraufwand: Seiten als PROT_READ markieren und ein Schreibzugriff führt ganz natürlich (unterstützt durch die Hardware) dazu, dass du in den Kernel trapst, die Seite kopieren und anschließend die Berechtigung wieder auf PROT_READ|PROT_WRITE setzen kannst.



  • in den beiden Artikeln steht recht deutlich, daß die Leute Probleme mit fork() haben. bei dem Ruby Artikel sind auch noch Performance Vergleiche angeführt. wenn man eine Weile sucht, findet man noch mehr solche Beispiele

    posix_spawn ist zunächst mal eine POSIX Definition. die Implementierung ist von System zu System unterschiedlich. auf Solaris ist die Implementierung lightweight und threadsafe. ich hoffe mal, die Linux Kernel Developer das irgendwann einmal auch noch auf die Reihe bekommen



  • Ich bin auf http://lwn.net/Articles/360720/ gestoßen. Und das verlinkte fork_bench.c habe ich mir geholt und ein wenig weiter gebaut. Mich hat interessiert, wie fork sich zu vfork verhält und wie relevant das in Zusammenhang mit exec ist. Hier ist das Programm:

    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/wait.h>
    
    extern char ** environ;
    
    int
    main(int argc, char *argv[])
    {
        long c = atoi(argv[1]) * 1024 * 1024;
        int n = atoi(argv[2]);
        int i = 0;
        int e_argc = 2;
        char* e_argv[] = { "/bin/true", NULL };
    
        if (n) {
            char *ptr = malloc(c);
            memset(ptr, 0, c);
        }
    
        for (; i < n; i++) {
            pid_t childpid = argv[3] && argv[3][0] == 'v' ? vfork() : fork();
            if (childpid) {
                waitpid(childpid, NULL, 0);
            } else {
                if (argv[3] && argv[4])
                  execve(e_argv[0], e_argv, environ);
                exit(0);
            }
        }
    }
    

    Hier die Ausführung:
    `

    $ time ./fork_bench 1000 100 v

    real 0m0.539s

    user 0m0.140s

    sys 0m0.350s

    $ time ./fork_bench 1000 100 f

    real 0m3.480s

    user 0m0.132s

    sys 0m3.090s

    $ time ./fork_bench 1000 100 f 1

    real 0m3.602s

    user 0m0.136s

    sys 0m3.135s

    $ time ./fork_bench 1000 100 v 1

    real 0m0.572s

    user 0m0.117s

    sys 0m0.353s`

    Die Tests laufen also mit einem malloc von 1GB jeweils 100 mal. Die Tests mit dem 'v' als 3. Parameter verwenden vfork und die anderen fork. Mit der zusätzlichen 1 wird noch ein exec auf /bin/true ausgeführt. Bemerkenswert ist, dass das exec kaum einen Unterschied macht. vfork ist allerdings signifikant schneller. Und zwar hier um den Faktor 10, wie man an der Zeile mit "sys" erkennen kann.

    Die Tests liefen auf einem Fedora 17, 64 Bit, Intel-i5 mit 2.7GHz, 6G RAM.

    Bitte schlagt mich nicht wegen der fehlenden Parameterprüfung. Ich habe das einfach so stupide weiter gebaut. Dennoch würde ich mich dafür feuern, wenn ich bei mir angestellt wäre 🤡 .



  • tntnet schrieb:

    Bemerkenswert ist, dass das exec kaum einen Unterschied macht. vfork ist allerdings signifikant schneller. Und zwar hier um den Faktor 10, wie man an der Zeile mit "sys" erkennen kann.

    Interessant. Ganz nachvollziehen kann ich deine Ergebnisse bei mir aber nicht.

    $ make fork_bench
    cc     fork_bench.c   -o fork_bench
    
    $ time ./fork_bench 1000 100 v
    
    real    0m0.217s
    user    0m0.098s
    sys     0m0.113s
    
    $ time ./fork_bench 1000 100 f
    
    real    0m0.306s
    user    0m0.105s
    sys     0m0.157s
    
    $ time ./fork_bench 1000 100 v 1
    
    real    0m0.285s
    user    0m0.104s
    sys     0m0.125s
    
    $ time ./fork_bench 1000 100 f 1
    
    real    0m0.358s
    user    0m0.098s
    sys     0m0.169s
    

    Fork ist bei mir nur etwas langsamer als vfork, nicht wie bei dir gleich um eine Größenordnung. Der Code wurde 1:1 aus deinem Posting in eine neue Datei fork_bench.c kopiert. System ist Fedora 18 (d.h. glibc 2.16, gcc 4.7.2) mit 3.7.0-Kernel auf Core i7 920 (2,67 GHz) und 12 GiB RAM. Alles x86_64 natürlich.

    Mal gucken, ob sich ausgraben lässt, woran genau das liegt.



  • der Unterschied zwischen fork() und vfork() zeigt, daß bei fork() eben doch so einiges kopiert wird, auch wenn einige Leute behaupten, fork() kopiere nichts. die Ergebnisse sind auch vergleichbar mit dem Test in dem oben verlinkten Ruby Artikel

    damals auf der PDP-11 war die Welt noch in Ordnung. einige Leute verwenden fork() seit 1980 und hatten noch nie Probleme damit ... herzlichen Glückwunsch

    heutzutage hat man im Programm offene Databaseconnections, mehrere Threads, Mutexobjekte usw., und da kommt es mit fork/vfork regelmäßig zu Deadlocks, Memory Leaks und anderen Problemen. deshalb gibt es z.B. solche Funktionen wie pthread_atfork()

    http://linux.die.net/man/3/pthread_atfork

    und sqldetach()

    http://publib.boulder.ibm.com/infocenter/idshelp/v115/index.jsp?topic=%2Fcom.ibm.esqlc.doc%2Fsii-xbsql-14203.htm

    optimal ist eine posix_spawn Implementierung, die threadsafe ist und vfork oder einen nativen Systemcall verwendet, wie z.B. unter Solaris

    https://bugzilla.redhat.com/show_bug.cgi?id=131938
    https://bugzilla.redhat.com/show_bug.cgi?id=193631

    eine relativ einfache und sichere Alternative ist auch, daß man am Anfang vom Hauptprogramm, bevor man den ersten Thread startet und bevor man die erste Databaseconnection öffnet, einen Hilfsprozeß startet. dieser Hilfsprozeß hat nur einen Thread und macht nichst weiter, als über IPC auf Kommandos vom Hauptprogramm zu warten. immer wenn das Hauptprogramm einen ext. Prozeß starten möchte, schickt es ein Kommando zum Hilfsprozeß und der startet das dann



  • dd++ schrieb:

    der Unterschied zwischen fork() und vfork() zeigt, daß bei fork() eben doch so einiges kopiert wird, auch wenn einige Leute behaupten, fork() kopiere nichts.

    Schau dir doch einfach mal die fork-Implementierung deiner libc an und fertig.

    Ich kann die Benchmarks auch nicht nachvollziehen.

    Um Horst Hannelores Benchmarks zu ergänzen hier noch ein 32-bittiges Squeeze auf x86-Hardware (interessant weil 32 bit und uralte Paketversionen):

    ~/fork_bench % time ./fork_bench 1000 100 v
    ./fork_bench 1000 100 v  0.13s user 0.73s system 99% cpu 0.866 total
    
    ~/fork_bench % time ./fork_bench 1000 100 f
    ./fork_bench 1000 100 f  0.12s user 0.74s system 100% cpu 0.858 total
    
    ~/fork_bench % time ./fork_bench 1000 100 f 1
    ./fork_bench 1000 100 f 1  0.15s user 0.70s system 100% cpu 0.847 total
    
    ~/fork_bench % time ./fork_bench 1000 100 v 1
    ./fork_bench 1000 100 v 1  0.13s user 0.72s system 94% cpu 0.893 total
    
    ~ % uname -r
    2.6.32-5-686-bigmem
    ~ % apt-cache show libc6-i686 | grep Version
    Version: 2.11.3-4
    ~ % apt-cache show gcc | grep Version
    Version: 4:4.4.5-1
    


  • Ich finde, dass die Performance eines Systemaufrufes generell nicht vorhersehbar ist und glaube daher nicht, dass man sein Programm-Design deshalb an die Effizienz eines Betriebssystems anpassen sollte.

    Will man fork()/vfork() und exec() einsetzen, möchte man oft ein Fremdprogram starten bzw steuern und da finde ich ein paar andere Dinge viel wichtiger, auf die oft vergessen wird:
    Offene Datei-Deskriptoren werden an einen Fremdprozess übertragen!
    Das kann eine Bibliothek sein, die im Stammprozess einen Thread erzeugt hat und dort eine Sperr-Datei für einen exklusiven Zugriff auf eine Datenbank geöffnet hat und so entsteht dann in dem neuen Prozess eine nie wieder freigegebene Sperre. Ein close() auf alle Deskriptoren nach dem fork() im Kind ist wirklich sehr unschön und vfork() könnt soetwas gar nicht realisieren. Dann noch die Frage ob der neue Prozess eine eigene Session (setsid()) bekommen soll oder nicht und dann auch mit dem Stammprozess gekillt wird oder eben nicht.

    Dass andere Threads aus dem Stammprozess im Kind einfach terminiert werden und nur pthread_atfork() aufräumen könnte, hilft auch selten. Wie erkläre ich einem std::vector in einem anderen Thread aus dem Stammprozess, dass er bei einem fork() im Kind seine Daten freigeben soll?

    Dann gibt es auch noch das Problem, wie ein Kind-Prozess den Erfolg eines exec() seinem Parent mitteilt, damit dieser das Kind fernsteuern kann ... OK, dafür gibt es zwar Lösungen mit zusätzlichen Pipes und Auto-close aber alles in allem kann ich da ein CreateProcess() aus der WinAPI nur vorziehen.

    Ich habe mich inzwischen zwar an dieses Coding-Schema gewöhnt aber stelle trotzdem fest, dass man die posix-Funktionen sehr schwer in RAII-konforme C++ Klassen kapseln kann. Die WinAPI ist da kompakter und das Design lässt sich da mit weit weniger Seiteneffekten kapseln. Signale sind dann auch noch so ein Sonderfall ... aber naja ... nobody is perfect ... dafür versagt Windows wieder an ganz ander Stellen jämmerlich 😃

    PS: Ich kenn da so ein Embedded System mit 2 GB RAM und Java schafft es mit 3 Prozessen auch dort regelmäßig den Start neuer Prozesse zu versauen 😡
    ... und immer genau dann leuchten die Dollar- bzw Euro-Symbole in meinen C++-Augäpfeln auf 🕶



  • Horst Hannelore schrieb:

    Mal gucken, ob sich ausgraben lässt, woran genau das liegt.

    Auch interessant. Zumal Deine Konfiguration meiner recht ähnlich ist. Einen Unterschied gibt es allerdings in der Ausführung: Ich habe fork_bench mit -O2 übersetzt. Das scheint aber nicht wirklich einen großen Unterschied zu machen.

    Bei weiteren Untersuchungen habe ich festgestellt, dass die Streuung der Ergebnisse groß ist. Abhilfe schafft die Erhöhung der Wiederholungen:

    $ time ./fork_bench 1000 1000 f
    
    real    0m20.121s
    user    0m0.122s
    sys     0m17.393s
    $ time ./fork_bench 1000 1000 v
    
    real    0m0.417s
    user    0m0.105s
    sys     0m0.274s
    

    vfork bleibt deutlich schneller.

    Hast Du eigentlich wirklich Fedora 18? Das ist noch nicht released.



  • Under Linux, fork(2) is implemented using copy-on-write pages, so the only
    penalty incurred by fork(2) is the time and memory required to duplicate the
    parent's page tables, and to create a unique task structure for the child.
    However, in the bad old days a fork(2) would require making a complete copy of
    the caller's data space, often needlessly, since usually immediately afterward
    an exec(3) is done.



  • tntnet schrieb:

    Abhilfe schafft die Erhöhung der Wiederholungen:
    […]
    vfork bleibt deutlich schneller.

    Kann ich immer noch nicht nachvollziehen:

    ~/fork_bench % time ./fork_bench 1000 1000 f
    ./fork_bench 1000 1000 f  0.12s user 0.75s system 99% cpu 0.868 total
    
    ~/fork_bench % time ./fork_bench 1000 1000 v
    ./fork_bench 1000 1000 v  0.08s user 0.80s system 96% cpu 0.908 total
    

    Hier wird sogar mit steigender Anzahl von Wiederholungen der Unterschied zwischen vfork und fork zugunsten von fork größer:

    ~/fork_bench % time ./fork_bench 1000 100000 f
    ./fork_bench 1000 100000 f  0.20s user 1.92s system 98% cpu 2.161 total
    ~/fork_bench % time ./fork_bench 1000 100000 v
    ./fork_bench 1000 100000 v  0.14s user 3.13s system 48% cpu 6.688 total
    

    Hast Du eigentlich wirklich Fedora 18? Das ist noch nicht released.

    Aber RC1 gibts schon, habe ich gestern auch testweise schon auf einer VM installiert.


Anmelden zum Antworten