Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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 просто вызывается из разных потоков. Действительно, концепция reentrancy выходит за пределы безопасности потоков, а также требует, чтобы функции работали нормально , даже если один из ее вызовов никогда не заканчивается. Это в основном аргументация, почему любая функция с блокировками не была бы повторной.

Функция printf также работает с глобальными данными. Любой выходной поток обычно использует глобальный буфер, прикрепленный к данным ресурса, отправляется (буфер для терминала или для файла). Процесс печати обычно представляет собой последовательность копирования данных для буферизации и очистки буфера после этого. Этот буфер должен быть защищен блокировками таким же образом malloc. Следовательно, printf также не является реентерабельным.

Ответ 2

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

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

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

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

Ответ 3

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

  • потокобезопасна
  • критический раздел
  • Реентрантная

Сначала взять самый простой: Оба malloc и printf потокобезопасные. С 2011 года они гарантируют безопасность потоков в стандарте 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 из двух разных потоков одновременно, результатом является поведение undefined, потому что strtok внутренне использует статический буфер, чтобы отслеживать его состояние. glibc добавляет strtok_r, чтобы исправить эту проблему, и C11 добавил то же самое (но необязательно и под другим именем, потому что Not Invented Here) как strtok_s.

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

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

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

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

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

Ни printf, ни malloc нельзя назвать рекурсивно одним потоком, поскольку они являются функциями листа (они не называют себя и не вызывают какой-либо управляемый пользователем код, который мог бы сделать рекурсивный вызов), И, как мы видели выше, с 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);
}

Ответ 4

Скорее всего потому, что вы не можете начать запись вывода, в то время как другой вызов printf все еще печатает его самостоятельно. То же самое касается распределения и освобождения памяти.

Ответ 5

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

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

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

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

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

Ответ 6

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