Прежде всего нужно разобраться в том, как правильно писать реентерабельный код. Даже если вы не намереваетесь запускать программу в многопоточной среде, могут найтись люди, которые захотят перенести ваше приложение на другую платформу и повысить его производительность за счет организации нескольких потоков. Они оценят ваше стремление избегать побочных эффектов. Говоря о переносимости, следует отметить, что в Windows не реализован системный вызов fork() и создание нового процесса обходится очень дорого, зато создание потока не влечет почти никаких накладных расходов.

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

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

□ если программа возбуждает исключение, не сняв замка, возможна тупиковая ситуация в другой части программы, которая ждет освобождения этого замка. Один из способов решения этой проблемы – инкапсулировать захват и освобождение замка в объект С++, тогда в ходе раскрутки стека будет вызван деструктор объекта, который и освободит замок. Отметим, что при этом захваченный ресурс может остаться в неопределенном состоянии; в некоторых случаях лучше допустить тупиковую ситуацию, чем продолжать работу с таким ресурсом;

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

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

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

В обработчике сигнала или исключения единственно безопасная вещь – это вызов exit(). Наилучшие рекомендации по этому вопросу мы встречали в статье Михала Залевски «Delivering Signals for Fun and Profit: Understanding, Exploiting and Preventing Signal Handling Related Vulnerabilities*-:

□ используйте в обработчиках сигналов только реентерабельные библиотечные функции. Для этого многие известные программы придется значительно переработать. Есть, правда, половинчатое решение – написать для каждой небезопасной библиотечной функции обертывающий код, который будет проверять некоторый глобальный флаг во избежание повторного входа;

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

□ блокируйте доставку сигналов на время нахождения в обработчике сигнала.

Для решения проблемы «момент проверки / момент использования» (TOCTOU) лучше всего создавать файлы в таком месте, куда обычные пользователи не имеют права ничего записывать. В случае каталогов такое не всегда возможно. При программировании на платформе Windows не забывайте, что с файлом (как и с любым другим объектом) можно связать дескриптор безопасности в момент создания. Задание прав доступа в момент создания объекта устраняет возможность гонки между моментами создания и определения прав доступа. Чтобы избегнуть гонки между моментом проверки существования и объекта и моментом создания нового объекта, у вас есть несколько вариантов, зависящих от типа объекта. В случае файлов самое правильное – задать флаг CREATE_NEW при вызове функции CreateFile. Тогда если файл существует, то функция завершится с ошибкой. Создание каталогов еще проще: любое обращение к функции Сгеа–teDirectory завершается с ошибкой, если каталог с указанным именем существует. Но проблема все равно может возникнуть. Предположим, что вы хотите поместить свое приложение в каталог Files\MyApp, но противник уже создал такой каталог заранее. Теперь у него есть полный доступ к этому каталогу, в том числе и право удалять из него файлы, даже если для самого файла разрешение на удаление отсутствует. Вызовы API, предназначенные для создания объектов некоторых типов, не предусматривают различий между операциями «создавать новый» и «открывать всегда». Такой вызов завершится успешно, но GetLastError вернет код ERROR_ALREADY_EXISTS. Корректный способ обработки ситуации, когда вы не хотите открывать существующий объект, таков: