Dlaczego malloc() i printf () są określane jako non-reentrant?

W systemach uniksowych wiemy, że malloc() Jest funkcją Nie-reentrantową (wywołaniem systemowym). Dlaczego?

Podobnie, printf() również mówi się, że nie jest reentrant; dlaczego?

Znam definicję re-entrancy, ale chciałem wiedzieć, dlaczego odnosi się ona do tych funkcji. Co uniemożliwia im reentrant?

Author: Jonathan Leffler, 2010-10-15

6 answers

malloc i printf zwykle używają struktur globalnych i wykorzystują synchronizację opartą na blokadach wewnętrznie. Dlatego nie są reentrantowe.

Funkcja malloc może być bezpieczna dla wątku lub niebezpieczna dla wątku. Oba nie są reentrantowe:

  1. Malloc działa na globalnej stercie i możliwe jest, że dwa różne wywołania malloc, które występują w tym samym czasie, zwracają ten sam blok pamięci. (Drugie wywołanie malloc powinno nastąpić przed pobraniem adresu fragmentu, ale fragment nie jest oznaczony jako niedostępny). To narusza postkondition malloc, więc ta implementacja nie zostanie ponownie wprowadzona.

  2. Aby zapobiec temu efektowi, implementacja malloc bezpieczna dla wątku używałaby synchronizacji opartej na blokadach. Jeśli jednak malloc zostanie wywołany z funkcji obsługi sygnału, może wystąpić następująca sytuacja:

    malloc();            //initial call
      lock(memory_lock); //acquire lock inside malloc implementation
    signal_handler();    //interrupt and process signal
    malloc();            //call malloc() inside signal handler
      lock(memory_lock); //try to acquire lock in malloc implementation
      // DEADLOCK!  We wait for release of memory_lock, but 
      // it won't be released because the original malloc call is interrupted
    

    Ta sytuacja nie nastąpi, gdy {[1] } jest po prostu wywoływana z różnych wątków. W istocie koncepcja reentrancji wykracza poza thread-safety, a także wymaga poprawnego działania funkcji , nawet jeśli jedno z jego wywołań nigdy się nie skończy . To jest w zasadzie rozumowanie, dlaczego jakakolwiek funkcja z blokadami nie byłaby ponownie włączana.

Funkcja printf operowała również na danych globalnych. Każdy strumień wyjściowy zwykle wykorzystuje globalny bufor dołączony do danych zasobu, do którego są wysyłane (bufor terminala lub pliku). Proces drukowania jest zwykle sekwencją kopiowania danych do bufora i bufor później. Bufor ten powinien być chroniony blokadami w taki sam sposób jak malloc. W związku z tym printf jest również niecentralny.

 52
Author: P Shved,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-18 23:02:41

Zrozummy, co rozumiemy przez re-entrant . Funkcja ponownego włączenia może zostać wywołana przed zakończeniem poprzedniego wywołania. Może się to zdarzyć, jeśli

  • funkcja jest wywoływana w funkcji obsługi sygnału (lub bardziej ogólnie niż w systemie Unix, w funkcji obsługi przerwań) dla sygnału, który został wywołany podczas wykonywania funkcji
  • funkcję nazywa się rekurencyjnie

Malloc nie jest re-entrancem, ponieważ zarządza kilkoma globalnymi strukturami danych, które śledzą wolną pamięć bloki.

Printf nie jest ponownie uruchamiany, ponieważ modyfikuje zmienną globalną tj. zawartość pliku*.

 10
Author: JeremyP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-15 10:46:35

Są tu co najmniej trzy pojęcia, z których wszystkie są ze sobą powiązane w języku potocznym, co może być powodem, dla którego byłeś zdezorientowany.

  • thread-safe
  • sekcja krytyczna
  • re-entrant

Najpierw najprostszy: zarówno malloc jak i printfthread-safe. Są one gwarantowane w standardzie c od 2011 roku, w POSIX od 2001 roku, a w praktyce od dawna wcześniej. Oznacza to, że następujący program ma gwarancję, że nie ulegnie awarii ani nie będzie wykazywał złego zachowania: {]}

#include <pthread.h>
#include <stdio.h>

void *printme(void *msg) {
  while (1)
    printf("%s\r", (char*)msg);
}

int main() {
  pthread_t thr;
  pthread_create(&thr, NULL, printme, "hello");        
  pthread_create(&thr, NULL, printme, "goodbye");        
  pthread_join(thr, NULL);
}

Przykładem funkcji, która nie jest bezpieczna dla wątku jest strtok. Jeśli wywołasz strtok z dwóch różnych wątków jednocześnie, wynikiem będzie nieokreślone zachowanie - ponieważ strtok wewnętrznie używa bufora statycznego do śledzenia jego stanu. glibc dodaje strtok_r, aby rozwiązać ten problem, a C11 dodał to samo (ale opcjonalnie i pod inną nazwą, bo nie Wynalezione tutaj) jako strtok_s.

Ok, ale czy nie używa globalnych zasobów do budowania swoich wyników? W rzeczywistości, co w ogóle oznacza drukować na stdout z dwóch wątków jednocześnie? to prowadzi nas do następnego tematu. Oczywiście printf będzie sekcja krytyczna w każdym programie, który go używa. tylko jeden wątek wykonania może znajdować się w sekcji krytycznej jednocześnie.

Przynajmniej w standardzie POSIX Systemy, jest to osiągane przez printf zaczyna się od wywołania do flockfile(stdout) i kończy się wywołaniem do funlockfile(stdout), co jest w zasadzie jak wzięcie globalnego mutex związanego z stdout.

Jednakże, każdy odrębny FILE w programie może mieć swój własny mutex. Oznacza to, że jeden wątek może wywołać fprintf(f1,...) w tym samym czasie, gdy drugi wątek jest w środku wywołania fprintf(f2,...). Nie ma tu żadnych warunków rasowych. (Czy Twoja libc faktycznie uruchamia te dwa wywołania równolegle, jest QoI problem. Nie wiem, co robi glibc.)

Podobnie, malloc jest mało prawdopodobne, aby być sekcją krytyczną w każdym nowoczesnym systemie, ponieważ nowoczesne systemy są wystarczająco inteligentne, aby utrzymać jedną pulę pamięci dla każdego wątku w systemie, zamiast mieć wszystkie N wątków walczących o jedną pulę. (Wywołanie systemowe sbrk nadal prawdopodobnie będzie sekcją krytyczną, ale malloc spędza bardzo mało czasu w sbrk. Lub mmap, czy cokolwiek fajnego dzieciaki używają tych dni.)

Ok, więc co robi ponowne wejście wredny? zasadniczo oznacza to, że funkcja może być bezpiecznie wywoływana rekurencyjnie - bieżące wywołanie jest "wstrzymane", podczas gdy drugie wywołanie jest uruchomione, a następnie pierwsze wywołanie jest nadal w stanie " podnieść się tam, gdzie zostało przerwane."(Technicznie rzecz biorąc, to może nie być spowodowane wywołaniem rekurencyjnym: pierwsze wywołanie może być w wątku A, który jest przerywany w środku przez wątek B, co sprawia, że druga inwokacja. Ale ten scenariusz jest tylko specjalnym przypadkiem thread-safety , więc możemy o tym zapomnieć w tym akapicie.)

Ani printf Ani malloc nie mogą być wywoływane rekurencyjnie przez pojedynczy wątek, ponieważ są to funkcje liścia (nie wywołują siebie ani nie wywołują żadnego kodu kontrolowanego przez użytkownika, który mógłby wywołać rekurencyjnie). I, jak widzieliśmy powyżej, zostały one zabezpieczone przed * multi - * thread re-entrant połączeń od 2001 (za pomocą zamki).

Ktokolwiek ci powiedział, że printf i malloc nie są reentrantowe, był w błędzie; chodziło im prawdopodobnie o to, że obie mają potencjał, aby być {33]}sekcjami krytycznymi {34]} w twoim programie - wąskimi gardłami, w których tylko jeden wątek może przejść na raz.


Uwaga pedantyczna: glibc zapewnia rozszerzenie, za pomocą którego printf można wywołać dowolny kod użytkownika, włączając w to ponowne wywołanie samego siebie. Jest to całkowicie bezpieczne we wszystkich swoich permutacjach - przynajmniej tak dalece w trosce o bezpieczeństwo wątku. (Oczywiście otwiera drzwi do absolutnie szalonych luk w formacie-string.) Istnieją dwa warianty: register_printf_function (który jest udokumentowany i rozsądnie zdrowy, ale oficjalnie "przestarzały") i register_printf_specifier (który jest prawie identyczny z wyjątkiem jednego dodatkowego nieudokumentowanego parametru i całkowitego braku dokumentacji użytkownika). Nie polecam żadnego z nich, a wspominam je tutaj tylko jako interesujący bok.

#include <stdio.h>
#include <printf.h>  // glibc extension

int widget(FILE *fp, const struct printf_info *info, const void *const *args) {
  static int count = 5;
  int w = *((const int *) args[0]);
  printf("boo!");  // direct recursive call
  return fprintf(fp, --count ? "<%W>" : "<%d>", w);  // indirect recursive call
}
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) {
  argtypes[0] = PA_INT;
  return 1;
}
int main() {
  register_printf_function('W', widget, widget_arginfo);
  printf("|%W|\n", 42);
}
 3
Author: Quuxplusone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-11 20:10:18

Najprawdopodobniej dlatego, że nie możesz zacząć pisać wyjścia, podczas gdy inne wywołanie printf nadal drukuje to samo. To samo dotyczy alokacji pamięci i dealokacji.

 1
Author: stdan28,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-15 10:16:05

To dlatego, że oba działają z globalnymi zasobami: strukturami pamięci heap i konsolą.

EDIT: sterta jest niczym innym jak rodzajem połączonej struktury listy. Każdy malloc LUB free modyfikuje go, więc posiadanie kilku wątków w tym samym czasie z dostępem do zapisu spowoduje uszkodzenie jego spójności.

EDIT2: kolejny szczegół: mogą być domyślnie ustawione ponownie za pomocą muteksów. Ale takie podejście jest kosztowne i nie ma gwarancji, że będą one zawsze używane w środowisku MT.

Są więc dwa rozwiązania: utworzenie 2 funkcji bibliotecznych, jednej reentrantowej, a drugiej nie lub pozostawienie części mutex użytkownikowi. Wybrali drugą.

Może to być również spowodowane tym, że oryginalne wersje tych funkcji nie były recentrujące, więc zostały zadeklarowane tak dla zgodności.

 -2
Author: ruslik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-15 10:41:56

Jeśli spróbujesz wywołać malloc z dwóch oddzielnych wątków (chyba że masz wersję bezpieczną dla wątków, nie gwarantowaną przez standard C), złe rzeczy się zdarzają, ponieważ jest tylko jedna sterta dla dwóch wątków. To samo dotyczy printf - zachowanie jest nieokreślone. To sprawia, że w rzeczywistości nie są reentrantowe.

 -4
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-10-15 10:20:11