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



  • 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.



  • xor schrieb:

    Offene Datei-Deskriptoren werden an einen Fremdprozess übertragen!

    Dafür gibts glücklicherweise das Close-on-Exec-Flag (O_CLOEXEC) für open(2), mittlerweile auch standardisiert.

    tntnet schrieb:

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

    Ja, ist Fedora 18. Offiziell released ist es noch nicht, aber das hat mich noch nie abgehalten. ;).

    Die Streuungen bei deinen Messungen verwundern mich auch etwas. Hatte das bei mir vor dem letzten Posting extra noch überprüft, aber keine nennenswerten Schwankungen festgestellt. Mit -O2 habe ich auch mal übersetzt, ebenfalls ohne Unterschied.



  • nman schrieb:

    Kann ich immer noch nicht nachvollziehen:

    Bei mir (OS X 10.6.8) gibt es minimale Unterschiede und vfork ist einen tick schneller.

    $ gcc -O2 fork_bench.c -o fork_bench
    $ time ./fork_bench 1000 1000 v
    
    real	0m0.551s
    user	0m0.164s
    sys 	0m0.377s
    $ time ./fork_bench 1000 1000 f
    
    real	0m0.808s
    user	0m0.205s
    sys 	0m0.537s
    $ time ./fork_bench 1000 1000 f 1
    
    real	0m0.808s
    user	0m0.204s
    sys 	0m0.544s
    $ time ./fork_bench 1000 1000 v 1
    
    real	0m0.550s
    user	0m0.164s
    sys 	0m0.377s
    

    $ uname -a
    Darwin TS-iMac.local 10.8.0 Darwin Kernel Version 10.8.0: Tue Jun 7 16:33:36 PDT 2011; root:xnu-1504.15.3~1/RELEASE_I386 i386



  • Und unter AIX 6.1 gibt es keinen Unterschied zwischen fork und vfork . Aber sicher gibt es nicht so viele, die so etwas zu Hause haben. Nur der Vollständigkeit halber wollte ich das mal erwähnen, da ich zufällig gerade Zugriff auf so eine Kiste habe.


Anmelden zum Antworten