Каждый такой вызов добавляет prepare_func в список функций, которые будут автоматически выполнены (в порядке, обратном их регистрации) перед созданием нового процесса с помощью fork(). Аналогично parent_func и child_func добавляются в список функций, которые будут автоматически вызваны (в порядке их регистрации) в родителе и потомке соответственно прямо перед возвращением fork().
Иногда обработчики создания нового процесса могут пригодиться в библиотечном коде, в котором используются потоки. Без них библиотеки не могли бы работать с приложениями, которые вызывают fork() и не учитывают того, что библиотечные функции могут создавать новые потоки.
Потомок, созданный вызовом fork(), наследует обработчики создания нового процесса от вызывающего потока. Во время выполнения exec() данные обработчики не сохраняются (это невозможно сделать, поскольку exec() перезаписывает их код).
Дальнейшие подробности об обработчиках создания нового процесса и примеры их использования можно найти в книге [Butenhof, 1996].
В Linux обработчики создания нового процесса не вызываются, если программа вызывает vfork() из библиотеки NPTL. Это не относится к программам, которые используют реализацию LinuxThreads.
Если какой-либо поток вызовет exit() или главная программа выполнит инструкцию return (что то же самое), все потоки будут сразу же уничтожены; при этом не будут выполнены деструкторы данных уровня потока и обработчики для освобождения ресурсов.
В этом разделе мы обратимся к теории и бегло рассмотрим три разные модели реализации программного интерфейса потоков. Это станет хорошим подспорьем для раздела 33.5, в котором описываются реализации многопоточности в Linux. Разница между упомянутыми выше моделями заключается в способе привязки потоков к
В реализации многопоточности вида M:1 все процедуры, связанные с созданием потока, планированием и синхронизацией (закрытие мьютексов, ожидание условных переменных и т. д.), полностью выполняются внутри процесса пользовательской библиотекой. Ядру ничего не известно о существовании в процессе разных потоков.
Реализации типа M:1 имеют несколько преимуществ. Главное из них заключается в том, что многие операции с потоками, такие как создание, завершение, переключение контекста и работа с мьютексами и условными переменными, имеют высокую производительность, так как не требуют переключения в режим ядра. Кроме того, реализацию типа M:1 можно относительно легко переносить из системы в систему, поскольку библиотека, реализующая потоки, не требует поддержки ядра.
Тем не менее данная модель обладает некоторыми серьезными недостатками.
• Когда поток делает системный вызов, такой как read(), поток выполнения переходит от библиотеки уровня пользователя к ядру. Это означает, что в случае блокирования вызова read() заблокированными окажутся все потоки в процессе.
• Поскольку ядру ничего не известно об отдельных потоках внутри процесса, оно не может планировать их выполнение, распределяя их между разными процессорами. Также невозможно назначить потоку повышенный приоритет по сравнению с потоками в других процессах, потому что отдельный процесс полностью берет на себя все планирование.
В реализации многопоточности вида 1:1 каждый поток привязывается к отдельному экземпляру KSE. Ядро планирует выполнение каждого потока отдельно. Процедура синхронизации потоков реализована внутри ядра в виде системных вызовов.
Модель 1:1 исключает недостатки, характерные для реализаций типа M:1. Блокирующий системный вызов не приводит к блокированию всех потоков в процессе, а ядро способно планировать выполнение потоков, распределяя их между разными процессорами.
Однако такие операции, как создание потоков, переключение контекста и синхронизация, работают медленней, поскольку им требуется переключение в режим ядра. Более того, дополнительные ресурсы, необходимые на выделение отдельных экземпляров KSE для каждого отдельного потока в приложении, могут существенно загрузить планировщик ядра, что негативно скажется на общей производительности системы.
Но, несмотря на эти недостатки, модель типа 1:1 обычно является более предпочтительной по сравнению с M:1. Именно она применяется в обеих реализациях многопоточности в Linux — LinuxThreads и NPTL