2015-01-13 46 views
5

Wywołanie tzset() po rozwidleniu wydaje się być bardzo wolne. Widzę powolność, jeśli najpierw wywołuję tzset() w procesie nadrzędnym przed rozwidleniem. Moja zmienna środowiskowa TZ nie jest ustawiona. I dtruss "Mój program testowy i ujawnił, że proces potomny czyta /etc/localtime dla każdego wywołania , podczas gdy proces nadrzędny odczytuje go tylko raz. Ten dostęp do pliku wydaje się być źródłem spowolnienia, ale nie byłem w stanie określić, dlaczego uzyskuje dostęp do niego za każdym razem w procesie potomnym.Dlaczego funkcja tzset() jest znacznie wolniejsza po rozwidleniu w systemie Mac OS X?

Oto mój program testowy 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); 
} 

skompilowany i wykonany foo.c tak:

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

Używam Mac OS X 10.10.1 (również powielana w 10.9.5).

Początkowo zauważyłem powolność za pomocą ruby ​​(Time # localtime slow in child process).

+0

Mniejszej: polecam 'difftime (przedawnienia tv.tv_sec, start)' zamiast '(d ouble) (tv.tv_sec - start) '. '(double)' in 'delta + = (double) ...' nie jest potrzebne. – chux

+0

Myślę, że główną przyczyną słabej wydajności jest to, że faktycznie sprawdza, czy plik strefy czasowej nie zmienił ** każdego ** połączenia lokalnego - robi to z mac zachowaniem, jak ustawienie systemu może się zmienić spod niego. Słabe zachowanie się widelca może być skutkiem ubocznym stosowanego mechanizmu notyfikacji zmiany pliku, który nie działa poprawnie w trybie 'fork()' - jest to tylko domniemanie oparte na uruchomieniu kodu za pomocą instrumentów i niektórych metod google. – Petesh

+0

Podobała mi się teoria powiadomień, więc zbadałem trochę więcej i wysłałem odpowiedź poniżej. – Muir

Odpowiedz

3

Masz szczęście, że nie doświadczyłeś nasal demons!

POSIX stwierdza, że ​​tylko funkcje bezpiecznego sygnału asynchronicznego mogą wywoływać procesy potomne po fork() i przed wywołaniem funkcji exec*(). Z standard (podkreślenie dodane):

... proces dziecko może wykonać tylko asynchroniczny sygnał-bezpiecznych operacji do czasu, gdy jedna z funkcji exec nazywa.

...

Istnieją dwa powody, dla których programiści nazywają fork() POSIX. Jednym z powodów jest , aby utworzyć nowy wątek kontroli w tym samym programie (który był pierwotnie możliwy tylko w POSIXie, tworząc nowy proces); innym jest utworzenie nowego procesu uruchamiającego inny program. W ostatnim przypadku wywołanie fork() wkrótce wywołuje jedno z następujących funkcji: dla exec.

Ogólny problem z wykonywaniem fork() działa w wielowątkowym świecie jest co zrobić ze wszystkimi wątkami. Istnieją dwie alternatywy. Jeden polega na skopiowaniu wszystkich wątków do nowego procesu. To powoduje, że programator lub implementacja radzi sobie z wątkami, które są zawieszone pod numerem podczas wywołań systemowych lub które mogą być w trakcie wykonywania wywołań systemowych, które nie powinny być wykonywane w nowym procesie, . Inną alternatywą jest skopiuj tylko wątek, który wywołuje fork(). Stwarza to trudności , że stan zasobów procesowych jest zwykle przechowywany w pamięci procesowej . Jeśli wątek, który nie wywołuje fork() przechowuje zasób, zasób nigdy nie zostaje zwolniony w procesie potomnym, ponieważ wątek , którego zadaniem jest zwolnienie zasobu, nie istnieje w procesie podrzędnym .

Gdy programator pisania wielowątkowy programu pierwszego sposobie użycia fork(), tworzenie nowych nici w tym samym programie jest zapewnia funkcję pthread_create(). Funkcja fork() zatem wykorzystywane tylko do uruchomienia nowych programów, a efekty wywoływania funkcji które wymagają pewnych zasobów między wywołaniu fork() i wezwanie do funkcji exec są niezdefiniowane.

Istnieją listy funkcji bezpiecznych sygnałów asynchronicznych here i here. W przypadku każdej innej funkcji, jeśli nie jest ona specjalnie udokumentowana, że ​​implementacje na platformach, na które użytkownik wdraża, dodają niestandardowe gwarancje bezpieczeństwa, należy uznać to za niebezpieczne, a jego zachowanie po stronie podrzędnej obiektu fork() jest niezdefiniowane.

+0

"... proces potomny może wykonywać tylko operacje asynchroniczne z sygnałem, dopóki nie zostanie wywołana jedna z funkcji exec" zdaje się odnosić do tego, co dzieje się, gdy "wielowątkowy proces wywołuje fork()". Nie sądzę, aby miało to zastosowanie do powyższego pytania. – psanford

+0

To jest uzasadnienie, ale nie sądzę, że reguła jest warunkowa. Ponieważ każdy program może stać się wielowątkowy, biblioteki systemowe muszą to uwzględnić.Mogą one rejestrować wywołania zwrotne 'pthread_atfork()', których implementacja zakłada, że ​​program może być wielowątkowy w momencie rozwidlenia. Mogą więc tymczasowo ustawić stan współdzielony w stan "fork-safe" w przygotowywanym callbacku, przywrócić je w macierzystym wywołaniu zwrotnym, ale pozostawić je w ten sposób w dziecięcym wywołaniu zwrotnym (ponieważ nie jest wymagane, aby je przywrócić). –

+0

Myślę, że to zbyt szerokie czytanie. Ani APUE, środowisko programistyczne Linuksa, ani Butenhof nie wspominają nic o tym ograniczeniu poza programem wielowątkowym, co byłoby prawdziwym pominięciem, gdyby było prawdą. Standard wyraźnie izoluje to na "Jeśli proces wielowątkowy nazywa fork()", jak wskazuje @psanford - myślę, że biblioteka systemu gotowa do gwintów, która nie pozostawiła widełek podrzędnych w procesie z pojedynczym gwintem w sposób, w jaki opisujesz, byłby po prostu zepsuty, ponieważ w takim przypadku nie ma miejsca na problem, chyba że zostanie utworzony. –

6

Odpowiedź Ken Thomases może być poprawna, ale byłem ciekawy dokładniejszej odpowiedzi, ponieważ wciąż znajduję powolne nieoczekiwane zachowanie dla programu jednowątkowego wykonującego tak prostą/wspólną operację po fork. Po zbadaniu http://opensource.apple.com/source/Libc/Libc-997.1.1/stdtime/FreeBSD/localtime.c (nie 100% pewności, że to jest poprawne źródło), myślę, że mam odpowiedź.

Kod wykorzystuje powiadomienia pasywne w celu określenia, czy strefa czasowa się zmieniła (w przeciwieństwie do stat ing /etc/localtime za każdym razem). Wygląda na to, że zarejestrowany token powiadomienia staje się nieważny w procesie potomnym po fork. Co więcej, kod ten traktuje błąd związany z używaniem nieważnego tokena jako pozytywnego powiadomienia o zmianie strefy czasowej i za każdym razem odczytuje /etc/localtime. Przypuszczam, że jest to rodzaj niezdefiniowanego zachowania, które można uzyskać po fork? Byłoby miło, gdyby biblioteka zauważyła błąd i ponownie zarejestrowała się w zgłoszeniu.

Oto fragment kodu z localtime.c która łączy wartość błędu o wartości stanu:

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

I wykazały, że token rejestracji traci ważność po widelca za pomocą tego programu:

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

i prowadził go tak:

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