Чтобы разобраться, что такое реентерабельные функции, сначала рассмотрим различие между однопоточными и многопоточными программами. Классические программы UNIX имеют только
В главе 29 мы рассмотрим, каким образом можно явно создавать программы, содержащие несколько потоков выполнения. Однако понятие многопоточного выполнения также имеет отношение к программам, в которых используются обработчики сигналов. Поскольку обработчик сигнала может асинхронно прервать выполнение программы в любой момент времени, основная программа и обработчик сигнала, по сути, формируют два независимых (хотя и не параллельных) потока в рамках одного процесса.
Функция называется
В стандарте SUSv3 реентерабельные функции определены как функции, «успешное выполнение которых гарантируется при вызове двумя или более потоками так, как если бы эти потоки вызывали данную функцию поочередно в неопределенном порядке, даже если в действительности выполнение функций накладывается».
Функция может быть нереентерабельной, если она обновляет глобальные или статические структуры данных. (Функция, которая использует только локальные переменные, гарантированно является реентерабельной.) Если две инициации (то есть два потока) функции одновременно пытаются обновить одну и ту же глобальную переменную или структуру данных, то очень вероятно, что эти обновления вступят в конфликт и приведут к выдаче неверных результатов. Для примера представим ситуацию, когда один поток выполнения находится в процессе обновления связанного списка с целью добавить в него новый элемент, но одновременно другой поток пытается обновить тот же список. Так как добавление нового элемента списка требует обновления нескольких указателей, если другой поток прерывает эти шаги и обновляет те же указатели, в результате получается хаос.
Такие случаи на самом деле далеко не редкость при работе со стандартной библиотекой С. Например, в подразделе 7.1.3 мы уже отмечали, что функции malloc() и free() работают со связанными списками освобожденной памяти, доступной для перераспределения из динамической области. Если вызов функции malloc(), осуществленный основной программой, прерывается обработчиком сигнала, который также вызывает функцию malloc(), то такой связанный список может быть поврежден. По этой причине семейство функций malloc() и другие библиотечные функции, которые применяют их, являются нереентерабельными.
Другие библиотечные функции являются нереентерабельными, потому что они возвращают информацию, используя статически выделенную память. Примерами таких функций (описываются по тексту книги) могут быть crypt(), getpwnam(), gethostbyname() и getservbyname(). Если обработчик сигнала вызывает одну из этих функций, то он перезапишет информацию, возвращенную любым предыдущим вызовом той же функции из основной программы (или наоборот).
Функции могут быть нереентерабельными, если они используют для внутренних операций статические структуры данных. Самыми очевидными примерами могут быть члены библиотеки stdio: printf(), scanf() и т. д, обновляющие внутренние структуры данных для буферизированного ввода-вывода. Таким образом, задействуя в обработчике сигнала функцию printf(), мы иногда можем увидеть странный вывод или даже аварийное завершение программы, или повреждение данных, если обработчик события прерывает программу во время выполнения функции printf() или другой функции из библиотеки stdio.
Даже если мы не используем нереентерабельные библиотечные функции, мы все равно можем столкнуться с проблемами реентерабельности. Если обработчик событий обновляет глобальные структуры данных, определенные программистом, обновляемые также из основной программы, то мы можем сказать, что обработчик событий является нереентерабельным по отношению к основной программе.
Если функция является нереентерабельной, то страница справочника будет, как правило, содержать явное или неявное указание на это. В частности, обращайте внимание на утверждения, что функция задействует или возвращает данные в статически выделенных переменных.
Пример программы
В листинге 21.1 продемонстрирована нереентерабельная природа функции crypt() (см. раздел 8.5). В качестве аргументов командной строки данная программа принимает две строки текста. Программа выполняет следующие шаги.
1. Вызов функции crypt() для зашифровки первой строки текста — аргумента и копирование этой строки текста в отдельный буфер с помощью функции strdup().