Почему malloc() и printf() называются нереентерабельными?

StackOverflow https://stackoverflow.com/questions/3941271

Вопрос

В системах UNIX мы знаем malloc() является нереентерабельной функцией (системный вызов).Почему это так?

Аналогично, printf() также считается, что он не является реентерабельным;почему?

Я знаю определение повторного входа, но я хотел бы знать, почему оно применимо к этим функциям.Что мешает им быть гарантированно реентерабельными?

Это было полезно?

Решение

malloc а также printf Обычно используют глобальные структуры и используют синхронизацию на основе блокировки внутри. Вот почему они не возвращаются.

То malloc Функция может быть безрезультатно, либо потоком небезопасно. Оба не возвращаются:

  1. Malloc работает на глобальной куче, и возможно, что две разные вызовы malloc Это происходит одновременно, вернуть тот же блок памяти. (2-й вызов malloc должен произойти до того, как адрес чанка будет получен, но чан не помечен как недоступный). Это нарушает посткон malloc, Таким образом, эта реализация не будет возврата.

  2. Чтобы предотвратить этот эффект, безопасно для потоков реализации 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- поведение неопределено. Это то, что делает их в реальности нерентринта.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top