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

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

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

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

6 ответов


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 функция также работала на глобальных данных. Любой выходной поток обычно использует глобальный буфер, прикрепленный к ресурсу, в который отправляются данные (буфер для терминала или для файла). Процесс печати обычно представляет собой последовательность копирования данных в буфер и последующей промывки буфера. Этот буфер должен быть защищен замки в порядке!--1--> делает. Следовательно,printf тоже неповторной.


давайте разберемся, что мы подразумеваем под повторный вход. Функция повторного входа может быть вызвана до завершения предыдущего вызова. Это может произойти, если

  • функция вызывается в обработчике сигнала (или более общем, чем Unix некоторый обработчик прерываний) для сигнала, который был поднят во время выполнения функции
  • функция вызывается рекурсивно

malloc не является повторным участником, потому что он управляет несколькими глобальными данными структуры, отслеживающие свободные блоки памяти.

printf не является повторным участником, потому что он изменяет глобальную переменную, т. е. содержимое файла* stout.


  • потокобезопасным
  • критическая секция
  • повторный вход

взять самый простой-первый: и malloc и printf are потокобезопасным. Они были гарантированно потокобезопасный в стандарте 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);
}

пример функции, которая составляет не потокобезопасна is strtok. Если вы позвоните strtok из двух разных потоков одновременно, результатом является неопределенное поведение - потому что strtok внутренне использует статический буфер для отслеживания его состояния. в glibc добавляет strtok_r чтобы исправить эту проблему, и C11 добавил то же самое (но необязательно и под другим именем, потому что не изобретено здесь), как strtok_s.

хорошо, но не printf использовать глобальные ресурсы для создания своих результатов? На самом деле, что бы это даже mean печатать в stdout из двух потоков одновременно? это подводит нас к следующей теме. Очевидно printf будет критическая секция в любом программа, которая использует его. только один поток выполнения может находиться внутри критической секции одновременно.

по крайней мере, в системах, совместимых с POSIX, это достигается путем printf начните с вызова flockfile(stdout) и заканчивается вызовом funlockfile(stdout), что в основном похоже на глобальный мьютекс, связанный с stdout.

однако, каждый отдельный FILE в программе разрешено иметь свой собственный мьютекс. Это означает, что один поток может вызвать fprintf(f1,...) в в то же время, когда второй поток находится в середине вызова fprintf(f2,...). Здесь нет расовых условий. (Если ваш libc фактически выполняет эти два вызова параллельно, это QoI вопрос. Я на самом деле не знаю, что делает glibc.)

аналогично, malloc вряд ли будет критическим разделом в любой современной системе, потому что современные системы достаточно умный, чтобы сохранить один пул памяти для каждого потока в системе, вместо того, чтобы все N потоков бороться за a единый пул. (The sbrk системный вызов по-прежнему, вероятно, будет критическим разделом, но malloc проводит очень мало времени в sbrk. Или mmap, или то, что крутые дети используют в эти дни.)

Итак,что значит входы на самом деле означает? в принципе, это означает, что функцию можно безопасно вызвать рекурсивно-текущий вызов "приостановлен" во время выполнения второго вызова, а затем первого вызова по-прежнему может "забрать, где он остановился.(Технически это может не из-за рекурсивного вызова: первый вызов может быть в потоке A, который прерывается посередине потоком B, который делает второй вызов. Но этот сценарий - всего лишь частный случай потокобезопасность, поэтому мы можем забыть об этом в этом абзаце.)

ни printf, ни malloc возможно be вызывается рекурсивно одним потоком, поскольку они являются листовыми функциями (они не вызывают себя и не вызывают управляемый пользователем код, который может сделать рекурсивный вызов). И, как мы видели выше, они были потокобезопасны против * многопоточных вызовов повторного входа с 2001 года (с помощью блокировок).

Итак, кто бы вам это ни сказал printf и malloc были нереентерабельные ошибся; то, что они хотели сказать, вероятно, что оба из них имеют потенциал, чтобы быть критические секции в вашей программе - узкие места, через которые может пройти только один поток.


педантичное Примечание: glibc предоставляет расширение, с помощью которого printf можно вызвать произвольный пользовательский код, включая повторный вызов. Это совершенно безопасно во всех своих перестановках - по крайней мере, в отношении безопасности потоков. (Очевидно, это открывает дверь абсолютно безумие уязвимости format-string.) Существует два варианта: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 все еще печатает его самостоятельно. То же самое касается выделения и освобождения памяти.


это потому, что оба работают с глобальными ресурсами: структурами памяти кучи и консолью.

EDIT: куча-это не что иное, как вид связанной структуры списка. Каждый malloc или free изменяет его, поэтому наличие нескольких потоков одновременно с доступом к нему для записи повредит его согласованность.

EDIT2: еще одна деталь: они могут быть повторно используемы по умолчанию с помощью мьютексов. Но этот подход является дорогостоящим, и нет гарантии, что они всегда будут использоваться в Среда МФ.

Итак, есть два решения: сделать 2 функции библиотеки, один возврат и один Не или оставить мьютекс часть для пользователя. Они выбрали второе.

кроме того, это может быть потому, что оригинальные версии этих функций были неповторной, поэтому были объявлены так, для совместимости.


Если вы пытаетесь вызвать malloc из двух отдельных потоков (если у вас нет потокобезопасной версии, не гарантированной стандартом C), происходят плохие вещи, потому что есть только одна куча для двух потоков. Же для printf - поведение неопределено. Это делает их в действительности неповторной.