Почему malloc() и printf() называются нереентерабельными?
-
30-09-2019 - |
Вопрос
В системах UNIX мы знаем malloc()
является нереентерабельной функцией (системный вызов).Почему это так?
Аналогично, printf()
также считается, что он не является реентерабельным;почему?
Я знаю определение повторного входа, но я хотел бы знать, почему оно применимо к этим функциям.Что мешает им быть гарантированно реентерабельными?
Решение
malloc
а также printf
Обычно используют глобальные структуры и используют синхронизацию на основе блокировки внутри. Вот почему они не возвращаются.
То malloc
Функция может быть безрезультатно, либо потоком небезопасно. Оба не возвращаются:
Malloc работает на глобальной куче, и возможно, что две разные вызовы
malloc
Это происходит одновременно, вернуть тот же блок памяти. (2-й вызов malloc должен произойти до того, как адрес чанка будет получен, но чан не помечен как недоступный). Это нарушает постконmalloc
, Таким образом, эта реализация не будет возврата.Чтобы предотвратить этот эффект, безопасно для потоков реализации
malloc
будет использовать синхронизацию на основе блокировки. Однако, если malloc вызывается из обработчика сигнала, может произойти следующая ситуация: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
Эта ситуация не произойдет, когда
malloc
просто вызывается из разных потоков. Действительно, концепция повторной связи выходит за рамки безопасности потоков, а также требует функций для правильной работы Даже если один из его вызовов никогда не прекращается. Отказ Это в основном рассуждение, почему любая функция с замками не будет возврата.
То printf
Функция также эксплуатируется по глобальным данным. Любой выходной поток обычно использует глобальный буфер, прикрепленный к данным ресурса, отправляются (буфер для терминала или для файла). Процесс печати обычно является последовательностью копирования данных для буфера и промывки буфера после этого. Этот буфер должен быть защищен замками таким же образом malloc
делает. Следовательно, printf
также не Reentrant.
Другие советы
Давайте понять, что мы подразумеваем под повторный участник. Отказ Функция повторной участники может быть вызвана до завершения предыдущего вызова. Это может произойти, если
- Функция вызывается в обработчике сигнала (или в более общем случае, чем Unix некоторые обработчика прерывания) для сигнала, который был поднят во время выполнения функции
- Функция называется рекурсивно
Malloc не является возвратом, потому что он управляет несколькими глобальными структурами данных, которые отслеживают бесплатные блоки памяти.
Printf не является возвратом, потому что он изменяет глобальную переменную, т. Е. Содержание файла * Stout.
Здесь есть как минимум три концепция, все из которых сливаются на разговорном языке, что может быть, почему вы были смущены.
- Безопасен в потоке
- Критический раздел
- повторный участник
Чтобы сделать самый простой первый: Обе malloc
а также printf
являются Безопасен в потоке. Отказ Они были гарантированы безопасными потоками в стандарте C с 2011 года, в Posix с 2001 года, а на практике на практике задолго до этого. Что это значит, что следующая программа гарантирована не схватить или проявлять плохое поведение:
#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);
}
Пример функции, которая не безрезультатно является strtok
. Отказ Если вы звоните strtok
Из двух разных нитей одновременно результат не определено поведение - потому что strtok
Внутренне использует статический буфер для отслеживания его состояния. Glibc добавляет strtok_r
исправить эту проблему, а C11 добавил одно и то же (но необязательно и под другим именем, потому что не изобретелен здесь) как strtok_s
.
Хорошо, но не printf
Используйте глобальные ресурсы для создания его вывода тоже? На самом деле, что бы это даже иметь в виду печатать на stdout из двух потоков одновременно? Это приводит нас к следующей теме. Очевидно printf
собирается быть Критический раздел В любой программе, которая использует это. Только одна нить выполнения разрешено быть в критическом разделе одновременно.
По крайней мере, в позывных системах, это достигается, имея printf
начать с звонка к flockfile(stdout)
и заканчиваться звонком funlockfile(stdout)
, который в основном нравится принимать глобальную Mutex, связанную со Stdout.
Однако каждый отчетливый FILE
В программе разрешено иметь свой собственный мьютекс. Это означает, что один поток может позвонить fprintf(f1,...)
в то же время, когда второй поток находится в середине вызова fprintf(f2,...)
. Отказ Здесь нет никакого состояния расы. (Будет ли ваша libc фактически проходит эти два звонка параллельно Qoi. проблема. Я на самом деле не знаю, что делает Glibc.)
По аналогии, malloc
вряд ли будет критический раздел в любой современной системе, потому что современные системы достаточно умный, чтобы держать один пул памяти для каждого потока в системе, вместо того, чтобы иметь все n потоков бороться над одним бассейном. (То sbrk
Системный вызов все равно, вероятно, будет критическим секцией, но malloc
тратит очень мало своего времени в sbrk
. Отказ Или mmap
, или любые крутые дети используют в эти дни.)
Хорошо, итак что значит повторное приема на самом деле значит? В принципе, это означает, что функция может быть смело называться рекурсивно - текущий вызов «введен в удержание», в то время как второй вызов работает, а затем первый вызов все еще может «поднять, где он остановился». (Технически это мощь Не быть связано с рекурсивным вызовом: первый вызов может быть в потоке A, который прерывается в середине потока B, что делает второй вызов. Но этот сценарий - это просто особый случай Безопасность потоков, Итак, мы можем забыть об этом в этом пункте.)
Ни один printf
ни malloc
может возможно быть Позвонил рекурсивно одним потоком, потому что они являются функциями листьев (они не называют себя и не вызывают любого кода, управляемого пользователем, который мог бы сделать рекурсивный вызов). И, как мы увидели выше, они были безопасны к потоку против * Multi- * резьбовых вызовов повторных приглашений с 2001 года (с помощью блокировки).
Итак, кто бы тебе сказал, что printf
а также malloc
были нерентриты были неверными; То, что они должны сказать, это, вероятно, они оба имеют потенциал быть критические разделы В вашей программе - узкие места, где только один поток может пройти через некоторое время.
ПЕДАНТАНСТВО ПРИМЕЧАНИЕ: GLIBC обеспечивает расширение, через который printf
Можно сделать, чтобы позвонить произвольному пользовательскому коду, в том числе повторно называю себя. Это идеально в безопасности во всех его перестановках - по крайней мере, насколько связано с потоком безопасности. (Очевидно, это открывает дверь в абсолютно безумный Уязвимости форматирования-строки.) Есть два варианта: register_printf_function
(который задокументирован и разумно вменяемый, но официально «устаревший») и register_printf_specifier
(который почти идентичны, за исключением одного дополнительного документации параметра и Общая недостаток документации по пользователем). Я бы не порекомендовал ни одного из них, и не упомянул их здесь просто как интересную сторону.
#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);
}
Скорее всего, поскольку вы не можете начать писать вывод, пока другой вызов на printf по-прежнему печатает себя. То же самое касается распределения памяти и освобождение.
Это потому, что и то, и другое работает с глобальными ресурсами:куча структур памяти и консоль.
Редактировать:куча - это не что иное, как своего рода структура связанного списка.Каждый malloc
или free
изменяет его, поэтому одновременное использование нескольких потоков с доступом на запись к нему приведет к нарушению его согласованности.
РЕДАКТИРОВАТЬ 2:еще одна деталь:их можно было бы сделать реентерабельными по умолчанию, используя мьютексы.Но такой подход является дорогостоящим, и нет гарантии, что они всегда будут использоваться в среде MT.
Итак, есть два решения:сделать 2 библиотечные функции, одну реентерабельную и одну нет, или оставить часть мьютекса пользователю.Они выбрали второе.
Кроме того, это может быть связано с тем, что исходные версии этих функций не были реентерабельными, поэтому они были объявлены таковыми для совместимости.
Если вы попробуйте позвонить в MALLOC из двух отдельных потоков (если у вас нет версии без поток, не гарантированной стандарты C), случаются плохие вещи, потому что есть только одна куча для двух потоков. То же самое для printf- поведение неопределено. Это то, что делает их в реальности нерентринта.