Мутексы
Приложение. Инверсия приоритета и борьба с ней
Гэндальф: Это Балрог! Бежим. Гимли: Но мы все равно не можем бежать быстрее демона! Леголас: Нам достаточно бежать быстрее тебя!
Анонимная пародия
В системах с приоритетным планированием при взаимодействии процессов и нитей с разными приоритетами возникает ряд специфических проблем, объединяемых названием инверсия приоритета (priority inversion).
Один из наглядных примеров этой проблемы можно наблюдать, если редактировать большой документ в текстовом процессоре Microsoft Word 2000 с включенным автосохранением. Проблема воспроизводится только если вставлять текст в начало или в середину документа, вынуждая Word переразбивать текст на страницы. При сохранении документа Word должен завершить разбиение на страницы, а для этого он вынужден остановить редактирование, иначе есть риск, что переразбиение на страницы никогда не завершится. Таким образом, при редактировании большого документа с включенным автосохранением пользователь большую часть времени взаимодействует с высокоприоритетным и действительно быстро реагирующим потоком, отвечающим за редактирование - но иногда оказывается вынужден ждать завершения работы низкоприоритетного фонового потока. Это выглядит как внезапное "зависание" Word. Если в интерактивных приложениях инверсия приоритета только раздражает, в системах жесткого реального времени она может приводить к действительно серьезным проблемам.
При расчете времени реакции на событие, разработчик системы реального времени должен принимать во внимание не только время исполнения кода, непосредственно обрабатывающего это событие, но и времена работы всех критических секций во всех нитях, которые могут удерживать мутексы, необходимые обработчику - ведь обработчик не сможет продолжить исполнение, пока не захватит эти мутексы, а произвольно снимать их нельзя, потому что они сигнализируют, что защищаемый ими разделяемый ресурс находится в несогласованном состоянии.
Если высокоприоритетная нить пытается захватить мутекс, занятый низкоприоритетной нитью, то в определенном смысле получится, что эта нить будет работать со скоростью низкоприоритетного процесса.
В условиях, когда планировщик не обеспечивает справедливого распределения времени центрального процессора, а в системе наравне с высокоприоритетной нитью (работа которой нас интересует) и низкоприоритетной (которая держит мутекс) существуют еще среднеприоритетные нити, может оказаться, что низкоприоритетная нить не будет получать управления в течение значительного времени. На это же время будет заблокирована и высокоприоритетная нить.
Особенно серьезна эта проблема, когда высоко- и низкоприоритетная нити относятся к разным классам планирования - а в системах реального времени так оно и есть.
Инверсия приоритета в Mars Pathfinder
При разработке ПО для бортового компьютера космического аппарата Mars Pathfinder, была принята архитектура, которую сами разработчики называли "общей шиной" - глобальная разделяемая область памяти, которую все процессы в системе использовали для коммуникации. Доступ к этой области защищался мутексом.
В системе существовала высокоприоритетная нить, занимавшаяся управлением шиной, которая периодически запускалась по сигналам таймера и собирала из "шины" некоторую важную для нее информацию. Эта же нить должна была сбрасывать сторожевой таймер, подтверждая, что система функционирует нормально.
Кроме того, в системе существовало еще несколько нитей, осуществлявших доступ к шине, в частности нить сбора метеорологических данных. Эта нить также запускалась по расписанию и копировала данные в буфер "шины", разумеется, захватывая при этом мутекс.
Таким образом, если нить управления "шиной" запускалась во время работы "метеорологической" нити, то она должна была ждать некоторое дополнительное время. В моменты, когда в системе не было других активных нитей, это не приводило к проблемам.
Однако когда накладывалось исполнение трех нитей, интервал ожидания сильно увеличивался, и это приводило к срабатыванию сторожевого таймера и сбросу бортового компьютера - в данном случае, ложному, потому что система все-таки оставалась работоспособной.
При тестировании на Земле сбросы такого рода несколько раз происходили, но разработчики не смогли воспроизвести условия их возникновения (срабатывание ошибки требует наложения трех нитей, каждая из которых сама по себе большую часть времени заблокирована) и отправили на Марс аппарат с недиагностированной проблемой. При эксплуатации непредсказуемые и происходящие, в среднем, раз в несколько дней сбросы бортового компьютера доставляли много неудобств центру управления полетом; к тому же, специалисты центра вынуждены были объяснять происходящее журналистам. На копии системы на Земле сбросы долгое время не удавалось воспроизвести; наконец, под утро, когда в лаборатории оставался только один разработчик, сброс все-таки произошел и по анализу отладочного дампа системы проблему удалось решить.
Основным средством борьбы с инверсией приоритета является наследование приоритета (priority inheritance). Обычно наследование контролируется флагом в параметрах мутекса или в параметрах системного вызова, захватывающего мутекс. Если высокоприоритетная нить пытается захватить мутекс с таким флагом, то приоритет нити, удерживающей этот мутекс, приравнивается приоритету нашей нити. Таким образом, в каждый момент времени, реальный приоритет нити, удерживающей мутекс, равен наивысшему из приоритетов нитей, ожидающих этого мутекса.
Более радикальное решение называется потолком приоритета (priority ceiling). Оно состоит в том, что приоритет нити, удерживающей мутекс, приравнивается наивысшему из приоритетов нитей, которые могут захватить этот мутекс. Разумеется, во время исполнения определить потолок приоритета невозможно, он должен устанавливаться программистом как параметр мутекса.
Легко понять, что во время работы в критических секциях, защищенных такими мутексами, задачи - даже низкоприоритетные - должны подчиняться всем ограничениям, которые может накладывать исполнение в режиме реального времени. Если мы в каком-то смысле не доверяем низкоприоритетной нити (в частности, не можем оценить время, в течение которого она будет удерживать мутекс), нам следует отказаться от использования разделяемой памяти для взаимодействия с этой нитью и перейти к каким-либо буферизованным средствам обмена данными. При этом достаточно обеспечить гарантированное время передачи данных в буфер; для того, чтобы избежать переполнения буфера, достаточно того, чтобы средний темп генерации данных высокоприоритетной нитью был ниже среднего же возможного темпа их обработки низкоприоритетным потребителем. Впрочем, если темп такой обработки подвержен большим флуктуациям (например, из-за наличия в системе других процессов), может оказаться необходимо предусмотреть возможность сброса части данных для борьбы с переполнением буфера - именно так борются с перегрузками сетевые маршрутизаторы и коммутаторы.
Необходимо отметить, что и наследование, и потолок приоритета применимы только к мутексам и другим примитивам синхронизации, для которых можно указать, какая именно нить их удерживает. Точнее говоря, для борьбы с инверсией приоритета необходимо, чтобы приоритет повышался для нити, которая будет освобождать семафор. Для мутексов это всегда та же нить, которая его захватывала, но для семафоров-счетчиков это, вообще говоря, неверно.
Поэтому даже те системы, которые предоставляют наследование или потолки приоритетов для мутексов, не предоставляют аналогичных средств для семафоров-счетчиков. В книге [QNX 2004] приводятся результаты экспериментов над системой реального времени QNX, из которых видно, что эта ОС реализует наследование приоритетов на мутексах, но при использовании семафоров-счетчиков можно получить классический случай инверсии приоритета.