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?
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:
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 postkonditionmalloc
, więc ta implementacja nie zostanie ponownie wprowadzona.-
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.
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*.
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 printf
są thread-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
.
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);
}
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.
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.
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.
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