Читая документацию на system(3), обратил внимание, что функция указана как MT-Safe.
В тоже время, функция манипулирует signal disposition, а обработчики сигналов — общие для всего процесса. Так что её потокобезопасность под вопросом.
Здравый смысл подсказывает, что если параллельно system()
в другом потоке работает другой вызов system()
или любой пользовательский код, изменяющий или читающий обработчики сигналов SIGINT
и SIGQUIT
, то будет состояние гонки и спецэффекты.
Как написано в SUS, функция не обязана быть потокбезоасной, и с реализацией потокобезопасности в ней всё сложно — не только по причинам, которые я описал.
В этом месте всё окончательно запуталось, и я пошел смотреть, что пишет документация других ОС. Во FreeBSD ман-страница очень краткая и просто отсылает к POSIX. То же самое в остальных BSD.
В Solaris 10 находим больше информации. Функция помечена как MT-Unsafe. И добавлено следующее прмиечание:
The system() function manipulates the signal handlers for SIGINT,
SIGQUIT, and SIGCHLD. It is therefore not safe to call system() in a
multithreaded process, since some other thread that manipulates these
signal handlers and a thread that concurrently calls system() can in-
terfere with each other in a destructive manner. If, however, no such
other thread is active, system() can safely be called concurrently from
multiple threads. See popen(3C) for an alternative to system() that is
thread-safe.
Итак, в случае, если другой поток манипулирует обработчиками сигналов, то будет состояние гонки. Но если несколько потоков выполняют вызов system()
конкурентно, этот случай является безопасным.
Если подумать как это может быть реализовано, приходит на ум способ с использованием атомарного счётчика, считающего конкурентные вызовы system()
. При изменении счётчика с 0 на 1 (нет других конкурентных вызовов, мы первые) сохраняем старое состояние сигналов в глобальной переменной и устанавливаем новое. При изменении счётчика с 1 на 0 (больше не осталось других конкурентных вызовов, мы последние) восстанавливаем сохранённое состояние сигналов. При этом поток, который сохранил состояние, не обязан быть тем же самым, что будет состояние восстанавливать.
А теперь посмотрим исходники:
- FreeBSD — функция потоконебезопасная.
- NetBSD — функция потоконебезопасная.
- OpenBSD — функция потоконебезопасная.
- Solaris — функция потокобезопасная относительно восстановления обработчиков сизналов. Интересный код и примечания в нём. Первый раз читаю кусочек исходников Solaris, и если Sun Microsystems так же ответственно подходила ко всему остальному, мне это нравится.
- glibc — здесь используется похожий подход. Но некоторые пояснения в документации бы не помешали. Стоило бы дополнить ман-страницу таким же примечанием, как в Солярисе.
Ну и самое интересное… musl!
Функция в musl является потоконебезопасной. Реализована практически так же, как в BSD. Это не было бы большим грехом, но в glibc функция заявлена как безопасная, а на странице Functional differences from glibc упоминания о system()
отсутствуют. Таким образом при отсутствии формального бага в коде имеется баг в документации.
Так что я зарепортил баг относительно лакуны в документации musl:
Относительно потокобезопасности и проблем с документацией вокруг неё вроде бы всё, но история была бы неполной, если бы я не упомянул об еще одном различии.
Вызов system(NULL)
может использоваться для того, чтобы определить, доступен ли интерпретатор команд в системе вообще. И здесь спецификация SUS делает интересный финт. Следите за руками:
- If command is a null pointer, system() shall return non-zero to indicate that a command processor is available, or zero if none is available.
- The system() function shall always return non-zero when command is NULL.
Что за противоречивые параграфы?
В данном случае дело в том, что первое предложение цитирует стандарт Си, который охватывает весь спектр платформ, где может быть запущен код на Си, и не требует от системы быть POSIX-compliant.
А второе предложение — уже собственное требование SUS о том, что POSIX-совместимая реализация должна возвращать ненулевое значение. Поскольку система, в которой нет sh-совместимого интерпретатора команд, не является POSIX-совместимой.
Таким образом POSIX-совместимая реализация libc не обязана вообще проверять наличие интерпретатора, а может всегда возвращать 1.
Следующие реализации так и сделали:
- FreeBSD
- OpenBSD
- musl
Но здесь следует еще один финт. Вопрос такой. Любая ли ран-тайм конфигурация Unix-подобной системы является POSIX-совместимой? И если в системе есть поддержка chroot()
или любого типа контейнеризации, то ответ — нет.
Если мы делаем chroot в отдельный каталог, чтобы ограничить потенциальное воздействие процесса на систему, то там может не быть sh
. Там может вообще ничего не быть. И процесс в данном случае работает в окружении, которое не является POSIX-compliant. Поэтому более корректно — всё же проверять наличие интерпретатора.
Следующие реализации действительно проверяют, что sh
имеется:
- NetBSD — проверяет, что файл есть, и у процесса есть права на его исполнение.
- Solaris — аналогично.
- Glibc — примененён очень изобретательный подход. При вызове
system(NULL)
выполняется вызовsystem("exit 0") == 0
. Таким образом любая проблема с интерпретатором (отсутствует, нет прав на исполнение, битый файл и т.п.) будет отловлена автоматически.
В том, как именно вызывается sh
, у разных реализаций также есть различия. Но здесь я остановлюсь, чтобы не превращать эту заметку в трактат.