2015-01-13 10 views
5

Das Aufrufen von tzset() nach Verzweigung scheint sehr langsam zu sein. Ich sehe nur die Langsamkeit, wenn ich zuerst im Elternprozess vor dem Verzweigen tzset() aufrufen. Meine TZ Umgebungsvariable ist nicht festgelegt. I dtruss 'd mein Testprogramm und es ergab, dass der Kindprozess liest /etc/localtime für jeden Aufruf tzset(), während der Elternprozess nur einmal liest. Dieser Dateizugriff scheint die Ursache für die Langsamkeit zu sein, aber ich konnte nicht feststellen, warum er jedes Mal im untergeordneten Prozess darauf zugreift.Warum ist tzset() nach dem Forken auf Mac OS X viel langsamer?

Hier ist mein Testprogramm foo.c:

#include <stdio.h> 
#include <stdlib.h> 
#include <sys/time.h> 
#include <unistd.h> 

void check(char *msg); 

int main(int argc, char **argv) { 
    check("before"); 

    pid_t c = fork(); 
    if (c == 0) { 
    check("fork"); 
    exit(0); 
    } 

    wait(NULL); 

    check("after"); 
} 

void check(char *msg) { 
    struct timeval tv; 

    gettimeofday(&tv, NULL); 
    time_t start = tv.tv_sec; 
    suseconds_t mstart = tv.tv_usec; 

    for (int i = 0; i < 10000; i++) { 
    tzset(); 
    } 

    gettimeofday(&tv, NULL); 
    double delta = (double)(tv.tv_sec - start); 
    delta += (double)(tv.tv_usec - mstart)/1000000.0; 

    printf("%s took: %fs\n", msg, delta); 
} 

ich zusammengestellt und foo.c wie folgt ausgeführt:

[[email protected] scratch]$ clang -o foo foo.c 
[[email protected] scratch]$ env -i ./foo 
before took: 0.002135s 
fork took: 1.122254s 
after took: 0.001120s 

ich Mac OS X 10.10.1 renne (auch reproduziert am 10.9.5).

Ich bemerkte ursprünglich die Langsamkeit über Ruby (Zeit # localtime langsam im Kind-Prozess).

+0

Minor: empfehlen 'difftime (tv.tv_sec, Start)' 'statt (d double) (tv.tv_sec - start) '. '(double)' in 'delta + = (double) ...' wird nicht benötigt. – chux

+0

Ich denke, der Hauptgrund für die schlechte Leistung ist, dass es tatsächlich überprüft, dass die Zeitzone Datei hat sich nicht ** jeden ** Ortszeit Anruf geändert - es tut das Mac-Verhalten wie die Systemeinstellung von darunter ändern kann. Das schlechte Fork-Verhalten kann ein Nebeneffekt des Mechanismus zur Benachrichtigung über Dateiänderungen sein, der über 'fork()' nicht ordnungsgemäß funktioniert - dies ist nur eine Annahme, die darauf beruht, dass der Code durch Instrumente und etwas Googlen ausgeführt wird. – Petesh

+0

Ich mochte die Benachrichtigungstheorie, also untersuchte ich etwas mehr und schrieb eine Antwort unten. – Muir

Antwort

3

Sie haben Glück, dass Sie nasal demons nicht erlebt haben!

POSIX gibt an, dass nur async-signalsichere Funktionen im Child-Prozess nach dem fork() und vor einem Aufruf einer exec*()-Funktion zulässig sind. Vom standard (Hervorhebung hinzugefügt):

... kann das Kind Prozess nur Asynchron-Signal-sicheren Betrieb bis zu dem Zeitpunkt als eine der exec Funktionen aufgerufen auszuführen.

...

Es gibt zwei Gründe, warum POSIX Programmierer fork() nennen. Ein Grund ist , um einen neuen Thread der Kontrolle innerhalb des gleichen Programms zu erstellen (das war ursprünglich nur möglich in POSIX durch Erstellen eines neuen Prozesses); Die andere ist, einen neuen Prozess zu erstellen, der ein anderes Programm ausführt. In dem letztgenannten Fall folgt auf den Anruf an fork() bald ein Anruf an eine der exec Funktionen.

Das allgemeine Problem mit der fork() Arbeit in einer Multi-Thread-Welt ist, was mit allen Threads zu tun. Es gibt zwei Alternativen. Eine ist, alle Threads in den neuen Prozess zu kopieren. Dies bewirkt, dass der Programmierer oder die Implementierung Threads behandelt, die bei Systemaufrufen ausgesetzt sind oder Systemaufrufe ausführen können, die in dem neuen Prozess nicht ausgeführt werden sollten. Die andere Alternative ist Kopieren Sie nur den Thread, der fork() aufruft. Dies erzeugt die Schwierigkeit , dass der Zustand prozeßlokaler Ressourcen normalerweise im Prozeß Speicher gehalten wird. Wenn ein Thread, der fork() nicht aufruft, eine Ressource enthält, wird die Ressource niemals im untergeordneten Prozess freigegeben, da der Thread , dessen Aufgabe es ist, die Ressource freizugeben, nicht im untergeordneten Prozess vorhanden ist.

Wenn ein Programmierer ein Multi-Threaded-Programm zu schreiben, beschrieben die ersten Verwendung von fork(), neue Threads im gleichen Programm zu schaffen, ist von der pthread_create() Funktion versehen. Die fork() Funktion ist somit verwendet nur neue Programme und die Auswirkungen der Aufruf von Funktionen , die erfordern bestimmte Ressourcen zwischen dem Aufruf von fork() und dem Aufruf zu einer exec Funktion sind nicht definiert laufen.

Es gibt Listen von Asynchron-Signal sichere Funktionen here und here. Für andere Funktionen ist es nicht speziell dokumentiert, dass die Implementierungen auf den Plattformen, auf denen Sie bereitstellen, eine nicht standardmäßige Sicherheitsgarantie hinzufügen. Sie müssen diese daher als unsicher betrachten und ihr Verhalten auf der untergeordneten Seite von fork() nicht definiert sein.

+0

"... der untergeordnete Prozess darf nur async-signalsichere Operationen ausführen, bis eine der exec-Funktionen aufgerufen wird" scheint sich darauf zu beziehen, was passiert, wenn "multi-threaded process fork()" aufruft. Ich denke also nicht, dass es auf die obige Frage zutrifft. – psanford

+0

Das ist die Begründung, aber ich glaube nicht, dass die Regel bedingt ist. Da jedes Programm multi-threaded werden kann, müssen die Systembibliotheken dies berücksichtigen.Sie können 'pthread_atfork()' Callbacks registrieren, deren Implementierung davon ausgeht, dass das Programm zum Zeitpunkt des Forks Multi-Threading sein könnte. Sie können daher den freigegebenen Zustand vorübergehend in einen "fork-safe" -Zustand im Prepare-Callback versetzen, sie im Parent- Callback wiederherstellen, aber im Child-Callback so belassen (weil sie dort nicht wiederhergestellt werden müssen). –

+0

Ich denke, das ist eine zu breite Lektüre. Weder APUE noch die Linux Programming Environment oder Butenhof erwähnen etwas über diese Einschränkung außerhalb eines Multithread-Programms, was eine ziemlich eklatante Auslassung wäre, wenn sie wahr wäre. Der Standard isoliert dies eindeutig auf "Wenn ein Multithread-Prozess fork()" aufruft, wie @psanford darauf hinweist - ich denke, eine Thread-fähige Systembibliothek, die das Kind in einem Single-Thread-Prozess in der Was du beschreibst, wäre einfach kaputt, denn in einem solchen Fall gibt es wirklich keinen Spielraum für ein Problem, es sei denn, man erstellt es. –

6

Ken Thomases Antwort mag korrekt sein, aber ich war neugierig auf eine spezifischere Antwort, weil ich immer noch das unerwartete Verhalten der Langsamkeit für ein single-threaded Programm, das eine so einfache/übliche Operation nach fork ing. Nach Prüfung http://opensource.apple.com/source/Libc/Libc-997.1.1/stdtime/FreeBSD/localtime.c (nicht 100% sicher, das ist die richtige Quelle), denke ich, dass ich eine Antwort habe.

Der Code verwendet passive Benachrichtigungen, um festzustellen, ob sich die Zeitzone geändert hat (im Gegensatz zu ing /etc/localtime jedes Mal). Es scheint, dass das registrierte Benachrichtigungstoken im untergeordneten Prozess nach fork ungültig wird. Darüber hinaus behandelt der Code den Fehler bei der Verwendung eines ungültigen Tokens als positive Benachrichtigung, dass sich die Zeitzone geändert hat, und liest jedes Mal /etc/localtime. Ich denke, das ist die Art von undefiniertem Verhalten, die Sie nach fork ing bekommen können? Es wäre schön, wenn die Bibliothek den Fehler bemerkt und sich erneut für die Benachrichtigung registriert hätte. Hier

ist der Code-Snippet von localtime.c, die den Fehlerwert mit dem Statuswert mischt:

nstat = notify_check(p->token, &ncheck); 
if (nstat || ncheck) { 

ich gezeigt, dass die Registrierungstoken nach Gabel mit diesem Programm ungültig werden:

#include <notify.h> 
#include <stdio.h> 
#include <stdlib.h> 

void bail(char *msg) { 
    printf("Error: %s\n", msg); 
    exit(1); 
} 

int main(int argc, char **argv) { 
    int token, something_changed, ret; 

    notify_register_check("com.apple.system.timezone", &token); 

    ret = notify_check(token, &something_changed); 
    if (ret) 
    bail("notify_check #1 failed"); 
    if (!something_changed) 
    bail("expected change on first call"); 

    ret = notify_check(token, &something_changed); 
    if (ret) 
    bail("notify_check #2 failed"); 
    if (something_changed) 
    bail("expected no change"); 

    pid_t c = fork(); 
    if (c == 0) { 
    ret = notify_check(token, &something_changed); 
    if (ret) { 
     if (ret == NOTIFY_STATUS_INVALID_TOKEN) 
     printf("ret is invalid token\n"); 

     if (!notify_is_valid_token(token)) 
     printf("token is not valid\n"); 

     bail("notify_check in fork failed"); 
    } 

    if (something_changed) 
     bail("expected not changed"); 

    exit(0); 
    } 

    wait(NULL); 
} 

Und lief es so:

muir-mb:projects muir$ clang -o notify_test notify_test.c 
muir-mb:projects muir$ ./notify_test 
ret is invalid token 
token is not valid 
Error: notify_check in fork failed