2015-01-13 12 views
5

Chiamare tzset() dopo la biforcazione sembra essere molto lento. Vedo la lentezza solo se prima chiamo il tzset() nel processo genitore. La mia variabile di ambiente TZ non è impostata. I dtruss del mio programma di test e ha rivelato che il processo figlio legge /etc/localtime per ogni chiamata tzset(), mentre il processo padre la legge solo una volta. Questo accesso ai file sembra essere la causa della lentezza, ma non sono stato in grado di determinare perché accedesse ad esso ogni volta nel processo figlio.Perché tzset() è molto più lento dopo la foratura su Mac OS X?

Ecco il mio programma di test 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); 
} 

ho compilato ed eseguito foo.c come questo:

[[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 

sto con Mac OS X 10.10.1 (riprodotto anche 10.9.5).

Originariamente ho notato la lentezza tramite ruby ​​(Time # localtime slow in child process).

+0

Minore: raccomandare 'difftime (tv.tv_sec, start)' invece di '(d ouble) (tv.tv_sec - start) '. '(double)' in 'delta + = (double) ...' non è necessario. – chux

+0

Penso che il motivo principale per le scarse prestazioni sia che in realtà controlla che il file del fuso orario non abbia cambiato ** ogni ** chiamata in locale - sta facendo il mac come comportarsi come l'impostazione del sistema può cambiare da sotto. Il cattivo comportamento della forcella potrebbe essere un effetto collaterale del meccanismo di notifica della modifica dei file che non viene utilizzato correttamente attraverso un 'fork()' - questa è solo una supposizione basata sull'esecuzione del codice tramite strumenti e alcuni googling. – Petesh

+0

Mi è piaciuta la teoria delle notifiche, quindi ho esaminato un po 'di più e ho postato una risposta qui sotto. – Muir

risposta

3

Sei fortunato che non hai esperienza nasal demons!

POSIX afferma che solo le funzioni di sicurezza del segnale asincrono sono legali per chiamare il processo figlio dopo lo fork() e prima di una chiamata a una funzione exec*(). Dal standard (enfasi aggiunta):

... il processo figlio può solo eseguire operazioni asincrone-signal-safe fino a quando una delle funzioni exec si chiama.

...

Ci sono due ragioni per cui i programmatori POSIX chiamano fork(). Uno dei motivi è per creare un nuovo thread di controllo all'interno dello stesso programma (che era originariamente possibile solo in POSIX creando un nuovo processo); l'altro è quello di creare un nuovo processo che esegue un programma diverso. Nell'ultimo caso , la chiamata a fork() è presto seguita da una chiamata a una delle funzioni exec.

Il problema generale con la creazione di fork() in un mondo a più thread è cosa fare con tutti i thread. Ci sono due alternative. Uno consiste nel copiare tutti i thread nel nuovo processo. Ciò fa sì che il programmatore o implementazione si occupi di thread sospesi sulle chiamate di sistema o che potrebbero essere in procinto di eseguire chiamate di sistema che non devono essere eseguite nel nuovo processo. L'altra alternativa è copia solo il thread che chiama fork(). Ciò crea la difficoltà che lo stato delle risorse locali del processo viene solitamente trattenuto nella memoria del processo . Se un thread che non sta chiamando fork() contiene una risorsa, quella risorsa non viene mai rilasciata nel processo secondario perché la thread il cui lavoro è quello di rilasciare la risorsa non esiste nel processo figlio .

Quando un programmatore sta scrivendo un programma multi-thread, il primo descritto l'uso di fork(), creando nuove discussioni nello stesso programma, è fornita dalla funzione pthread_create(). La funzione fork() è quindi utilizzato solo per eseguire nuovi programmi, e gli effetti delle funzioni che richiedono determinate risorse tra la chiamata al fork() e la chiamata a una funzione exec chiamando sono indefiniti.

ci sono gli elenchi delle funzioni async-signal-safe here e here. Per qualsiasi altra funzione, se non è specificamente documentato che le implementazioni sulle piattaforme a cui si sta implementando aggiungono una garanzia di sicurezza non standard, è necessario considerarle non sicure e il relativo comportamento sul lato figlio di un fork() non definito.

+0

"... il processo figlio può eseguire solo operazioni sicure con segnale asincrono fino a quando non viene chiamata una delle funzioni exec" sembra riferirsi a ciò che accade quando "il processo multi-thread chiama fork()". Quindi non penso che si applica alla domanda di cui sopra. – psanford

+0

Questa è la motivazione, ma non credo che la regola sia condizionata. Poiché qualsiasi programma può diventare multi-thread, le librerie di sistema devono tenerne conto.Possono registrare callback 'pthread_atfork()' la cui implementazione presuppone che il programma possa essere multi-thread al momento del fork. Possono quindi temporaneamente mettere lo stato condiviso in uno stato "sicuro da fork" nel callback di preparazione, ripristinarli nel callback genitore, ma lasciarli così nel callback secondario (perché non è necessario ripristinarli lì). –

+0

Penso che questa sia una lettura troppo ampia. Né APUE, Linux Programming Environment, né Butenhof parlano di questa restrizione al di fuori di un programma multithread, che sarebbe un'omissione evidente se fosse vera. Lo standard lo isola chiaramente in "Se un processo multi-thread chiama fork()" come sottolinea @psanford - Penso che una libreria di sistema thread-ready che non ha lasciato il fork fork safe in un processo a thread singolo nel il modo in cui descrivi verrebbe semplicemente infranto, dal momento che non c'è davvero spazio per un problema in questo caso, a meno che non ne venga creato uno. –

6

La risposta di Ken Thomases può essere corretta, ma ero curioso di una risposta più specifica perché trovo ancora la lentezza del comportamento inatteso per un programma a thread singolo che esegue un'operazione così semplice/comune dopo fork ing. Dopo aver esaminato http://opensource.apple.com/source/Libc/Libc-997.1.1/stdtime/FreeBSD/localtime.c (non sicuro al 100% questa è la fonte corretta), penso di avere una risposta.

Il codice utilizza notifiche passive per determinare se il fuso orario è cambiato (al contrario di stat ineo /etc/localtime ogni volta). Sembra che il token di notifica registrato non sia più valido nel processo figlio dopo lo fork ing. Inoltre, il codice tratta l'errore dall'utilizzo di un token non valido come notifica positiva che il fuso orario è cambiato e continua a leggere ogni volta /etc/localtime. Immagino che questo sia il tipo di comportamento indefinito che puoi ottenere dopo lo fork ing? Sarebbe bello se la libreria avesse notato l'errore e fosse nuovamente registrata per la notifica, comunque.

Ecco il frammento di codice da localtime.c che mescola il valore di errore con il valore di stato:

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

ho dimostrato che il token di registrazione non è più valido dopo forcella usando questo programma:

#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); 
} 

e corse in questo modo:

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