понедельник, 20 сентября 2010 г.

Быстрый обзор того, как процессы завершают работу в Windows XP

Это перевод Quick overview of how processes exit on Windows XP. Автор: Реймонд Чен.

Выход из процесса является страшнейшим моментом в жизни процесса (примерно как приземление является страшнейшим моментом полёта).

Многие детали того, как процессы завершают работу, не документированы в Win32, так что различные реализации Win32 могут следовать различным механизмам. Например, Win32s, Windows 95 и Windows NT - все завершают процессы разным способом (я не удивлюсь, если окажется, что Windows CE использует ещё какой-то способ, отличный от указанных). Поэтому имейте в виду, что всё, что я напишу сейчас в этом посте, является деталями реализации и может быть изменено в любой момент без предупреждения. Я пишу это потому, что эти знания помогут вам подсветить баги, закопанные в вашем коде. В частности, я собираюсь сейчас обсудить, как процессы делают выход на Windows XP.

Сначала я должен чётко сказать, что я не согласен с многими шагами в механизме завершения работы процессов в Windows XP. Цель этой статьи - не в оправдании, а в заполнении пробелов в знаниях закулисных действий, так что вы будете вооружены, когда вам придётся отлаживать загадочный вылет или зависание при выходе (обратите внимание, что я просто ссылаюсь на него, как способ выхода процесса на Windows XP, а не говорю, как процесс выхода был спроектирован. Как говорит один из моих коллег: "Использование слова дизайн для описания этого - это как использование термина бассейн, чтобы сослаться на лужу в вашем саду").

Когда ваша программа вызывает ExitProcess, происходит множество вещей. Во-первых, все потоки в процессе (кроме того, кто вызвал ExitProcess) насильно завершаются. Это идёт из тех старых времён, когда теория предполагала, что когда процесс решает завершить свою работу, то он уже очистил все свои потоки. Завершение потоков, поэтому, это просто ремни безопасности, позволяющие поймать вещи, которые вы могли пропустить. Оно даже не ждёт двух секунд.

Итак, сейчас мы не говорим о счастливом завершении потока через ExitThread. Это невозможно, потому что поток может быть в середине работы. Внедрение вызова ExitThread в середину работы потока приведёт к рассылке уведомлений DLL_THREAD_DETACH в тот момент, когда поток к ним не готов (например, держит блокировку). Нет, эти потоки завершаются насильно в стиле TerminateThread: просто выдернете почву у него из под ног. Прощай. Теперь это бывший поток.

Ну, это весьма грубо, не так ли? И всё это после страшных предупреждений в MSDN, что TerminateThread - плохая функция и её надо избегать!

Подождите, это ещё не самое плохое.

Некоторые из этих потоков, которые были прерваны, могут владеть критическими секциями, мьютексами, самоделкиными объектами синхронизации (типа спин-блокировки) - всё это такие штуки, которые могут понадобится вызывающему потоку во время обработки DLL_PROCESS_DETACH. Ну, мьютексы вроде как в порядке: если вы попробуете войти в такой мьютекс, то получите загадочный код WAIT_ABANDONED, который говорит вам: "Ой-ой, наши данные испорчены".

Что насчёт критических секций? У них нет таких "ой-ой" значений - EnterCriticalSection не возвращает значение. Вместо этого ядро говорит "Открываем сезон на критические секции!". Я обычно представляю себе ворота на парковке, которые открываются и просто позволяют входить и выходить кому угодно.

С самоделкиными решениями - вы сами за себя.

Это означает, что если ваш код будет владеть критической секцией в момент, когда кто-то вызывает ExitProcess, то структура данных, которую защищает критическая секция, будет в каком-то нестабильном промежуточном состоянии (в конце концов, если бы она была в согласованном состоянии, вы бы, вероятно, отпустили бы критическую секцию! Ну, предполагая, что вы в неё зашли для изменения структуры данных). Запускается ваш код DLL_PROCESS_DETACH, входит в критическую секцию и это ему удаётся, потому что "все ворота открыты". Теперь ваш код DLL_PROCESS_DETACH начинает вести себя хаотично, поскольку значения в этой структуре данных несогласованы.

Ох, дорогой, теперь у тебя здорово испачканы руки.

И что, если ваш поток был прерван, пока он владел спин-блокировкой или чем-то ещё? Ваш код DLL_PROCESS_DETACH просто повиснет, ожидая события, которое никогда не произойдёт - освобождение блокировки убитым потоком.

Но подождите, это ещё не самое страшное.

Эта критическая секция могла быть той, что защищает кучу процесса! Если один из потоков был прерван в момент вызова функций типа HeapAlloc или LocalFree, то куча процесса может оказаться в несогласованном состоянии. Если ваша DLL_PROCESS_DETACH попытается выделить или освободить память, то она может вылететь из-за испорченной кучи.

Мораль истории: если вы получаете DLL_PROCESS_DETACH из-за завершения процесса(*), не пытайтесь сделать что-то умное. Просто верните управление, ничего не делая, позволив случиться обычной очистке процесса. Ядро закроет все ваши описатели объектов ядра. А любая память, что вы выделили, уйдёт автоматически, когда закроется адресное пространство вашего процесса. Просто дайте процессу тихо умереть.

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

Также заметьте, что если вы получаете DLL_PROCESS_DETACH из-за динамической выгрузки DLL, то вам нужно очистить ваши объекты и выделенную память, потому что процесс продолжит работу. Но с другой стороны, в случае динамической выгрузки в вашей DLL не должен работать никакой другой поток (потому что вы выгружаетесь), так что (предполагая, что вы правильно запрограммировали свою DLL) ни одна из ваших критических секций не будет занятой и все структуры данных будут согласованы.

Подожди-ка, катастрофа ещё не произошла.

Даже хотя ядро удалило все потоки, кроме одного, - это ещё не значит, что создание новых потоков заблокировано. Если кто-то вызовет CreateThread в их DLL_PROCESS_DETACH (как бы безумно это ни звучало) - поток будет добросовестно создан и начнёт работу! Но не забывайте про "все врата открыты", так что все ваши критические секции - просто занавески на окнах, которые внушают вам чувство комфорта.

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

В следующий раз, мы увидим, как способ завершения процессов в Windows XP вызвал не одну, а сразу две проблемы.

Примечания:
(*) Все, кто читает этот пост, уже должны знать, как определить, когда это так. Я предполагаю, что вы умны. Не разочаруйте меня.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.