Итак «Погружение»! Кода по колено. Бр-р, холодно. Что может быть приятнее десятка-другого килобайт кода ядра на ночь. Не увлекает? Меня тоже. Как вы думаете, чем больше всего любит заниматься каждая нормальная операционная система? Вы, вероятно, полагаете, — выполнять ваше приложение. Заблуждаетесь, основное занятие всякой операционной системы - ожидание. Это может быть ожидание прерывания от операции ввода/вывода, ожидание запроса от вашего скучного приложения, или нервное поглядывание на системные часы, дабы не дать вашему приложению времени больше положенного. Ясно, что различные процессы могут одновременно попытаться выполнять системные вызовы, а эти вызовы могут застревать по самым разнообразным причинам — кому-то надо просмотреть каталог, которого нет в памяти, кому-то синхронно сбросить блок, кому-то сдвинуть руку какого-либо агрегата. Поэтому при переключении между процессами надо запоминать не только то, какую команду выполнял пользовательский процесс, но и то, в какой точке «завис» системный вызов, какой в этот момент был системный стек для данного вызова и т.п. Фактически правильнее говорить о том, что каждый процесс может выполняться в двух режимах: «режим пользователя» для действий типа дважды два, «режим ядра» - например, для вывода оного результата на экран. При работе в режиме ядра все процессы (впрочем, возможны фокусы диалектов) имеют доступ к любым данным системы. Думаю, каждому понятно, что одновременное обращение параллельных процессов к общим данным может привести к неприятным последствиям. Необходимо также учитывать процедуры обработки прерываний, которые могут прервать выполнение системного вызова и как-либо изменить структуры (типа «дай символ с терминала» и «приход символа с терминала») обрабатываемые в этом вызове. Естественно, что от этого нужно как-то защититься. Защитой от прерывания «критичного» кода системного вызова служит маскирование прерывания данного типа (иногда явное, но чаще изменением уровня разрешенного прерывания или общего флага разрешения прерываний без разбирательства с адресами и т.п.) перед началом критичного кода и восстановление состояния процессора после этого кода. В Linux это выполняется макросами (с ассемблерными вставками) наподобие save_flags/cli/sti/restore_flags, в других диалектах это может быть что-то начинающееся с spl... (spltty, spl6 и т.п., соответственно возврат состояния splx) и т.п. Как защищаются друг от друга процессы? Заметим, что речь можно вести в терминах ресурсов: доступен ли тот или иной ресурс (например, буфер ввода/вывода) процессу или занят другим процессом. Для работы с критичным кодом, в принципе, можно тоже полностью запрещать прерывания (как следствие, никакой параллельный процесс просто не будет перезапущен). Однако, что делать, если в этих скобках, окружающих критичный код, необходимо подождать результата? Например, если, выполняя операцию чтения, мы захватываем ресурс (буфер, устройство), обнаруживаем, что он не заполнен и ... что дальше? Вот мы и добрались до любимого занятия операционной системы - ожидания. Нам надо попытаться захватить ресурс и, если он не готов для использования, «заснуть» - дать работать другим. Для этого имеется специальный вызов ядра: __sleep_on в Linux и sleep в других диалектах. Для читающих «с погружением»: код можно найти в файле /usr/src/linux/kernel/sched.c и соответствующем включаемом файле. Первый параметр вызова задает ожидаемое событие. Во многих диалектах этот параметр представляет собой абстрактный адрес, то есть данные по этому адресу не используются - это лишь средство получить гарантированно разные идентификаторы/очереди ожидания; так, в Linux это явный адрес очереди. Второй параметр указывает на возможность выполнения прерывания данного вызова каким-либо сигналом. В Linux это делается явно (sleep_on или interruptable_sleep_on; оба имеют только один параметр и указывают тип ожидания). В других диалектах второй параметр sleep сравнивается с некоторой константой, обычно именуемой PZERO; если в запросе задан приоритет больше (соответственно задающее его число меньше), чем эта константа, sleep считается прерываемым. Непрерываемое ожидание служит для «короткоживущих» ожиданий, прерываемое для «долгоживущих», например, для ожидания символа с терминала. Что означает, что ожидание непрерываемое? Это значит, что даже команда «kill -9 pid» для процесса в таком режиме ожидания является не более, чем предложением «подохни, собака, будь так добра», которое он может полностью проигнорировать. Другими словами, во время ожидания такого типа процесс не обрабатывает никаких сигналов и продолжает ждать. Если для какой-либо операции с внешними устройствами используется такой вариант ожидания, то процесс может быть прерван (заметим, что реально он и так «прерван» и ничего не делает, лишь занимая память и отбирая другие ресурсы) только внешними причинами, сигналом сбоя/внимания/сброса по шине и т.п., которые отрабатываются соответствующими драйверами. Например, в системе, где все ожидания для SCSI-устройств были запрограммированы как «короткоживущие», мне приходилось нажимать на разного рода кнопки, приоткрывать лентопротяжку (вот грех-то) и предпринимать другие подобные действия для создания исключительной ситуации. Следующим интересным моментом является процесс пробуждения очереди. Все процессы, которые ждут требуемого события или один ресурс (семафор, блок и т.п.) просыпаются одновременно и попадают в очередь готовых на выполнение. Соответственно, если sleep был использован для разделения ресурсов, то проснувшиеся процессы должны вновь сделать попытку захвата ресурса. Пример подобного рода вы можете найти в /usr/src/linux/fs/fifo.c (первые же interruptible_sleep_on находятся внутри цикла запроса). Итак, Unix-процесс при помощи sleep всегда ждет только одного события. Вот напасть-то. А как же обрабатывать много событий от различных источников одновременно? Представляете радость любителей NT, прочитавших эти строки: у вас в Unix все криво, мы знаем!.. Смею вас уверить - не представляете. С другой стороны, существуют же вызовы наподобие select, которые предполагают ожидание событий ввода/вывода на некотором множестве каналов. Они-то как работают? Фокус выполняется достаточно просто. Никто не требует, что бы мы ожидали ровно в одной очереди. Да, sleep добавляет нас к очереди ожидания конкретного события. Однако либо это событие формулируется в виде «что-то произошло с имеющимися в списке файлами», либо (как это сделано, например, в Linux) можно добавить процесс явно в несколько очередей при помощи какого-нибудь специального вызова типа select_wait, который должен быть выдан для каждого файла или устройства с очередью событий соответствующего типа: чтение/запись/исключения (см. например usr/src/linux/drivers/char/busmouse.c). В результате, когда, например, на одном из требуемых для нас устройств происходит событие нужного типа, ожидающие его процессы пробуждаются командой типа wake_up_interruptible. Пробудившийся процесс заново проверяет все заявленные источники событий. Какие еще фокусы может породить такой механизм ожидания? Что произойдет, если какой-либо процесс попробует захватить какой-либо ресурс повторно (то есть захвативший его ранее как часть текущей операции)? Очевидно, что если никакой защиты от этого не предпринимается, то данный процесс и затем все процессы, которым необходим этот же ресурс, зависнут (до перезагрузки). Как это не странно, но стандартный пример зависания от Баха (Морис Бах - автор классической книги «Архитектура системы Unix» - прим. ред.) наподобие «link . ./a» срабатывает для некоторых диалектов Unix и файловых систем до сих пор (он основан именно на повторном захвате ресурсов, как вы полагаете, каких?). Заметим, что обычный пользователь не имеет возможности связывать каталоги и сотворить такое. Как мы видим, создать «бессмертный» процесс можно либо из-за ошибок драйверов и специфики поведения внешних устройств, либо из-за ошибок в программировании конкретных операций. Если ли еще другие формы «бессмертных»? Да, это «привидения» — зомби. Наиболее простой способ породить зомби следующий: (sleep 10 & exec sleep 30000) & В результате подобной операции мы сначала получим деревце процессов в виде sh -> sleep 30000 -> sleep 30, а через 30 секунд sh -> sleep 30000 -> зомби (проверьте, как ps отображает их в вашей системе). В Unix для процессов действует принцип «никто не забыт, ничто не забыто». Процесс не может исчезнуть до тех пор, пока его родитель не узнает код завершения процесса. Если же он погиб, заботу о сыне берет «сиротский дом» - процесс init. Для получения кода возврата родитель должен выдать специальный системный вызов. До тех пор, пока он этого не сделал, сын присутствует в системе. Сын не имеет никаких атрибутов процесса (кроме идентификатора) и не потребляет никаких ресурсов, кроме места в таблице процессов. Как вы, вероятно, догадываетесь, sleep даже в страшном сне не может представить, что у него есть сын. Соответственно, пока родитель не умрет (а если не вмешаться это произойдет часов через 8 с гаком), сын будет присутствовать в системе. Все это время сын попросту игнорирует любые операции наподобие «kill -9»: что с ним, с трупом сделается. Как в кино: чтобы стать бессмертным, надо один раз умереть. Продолжение темыНекоторые читатели заметки о возможности восстановления Linux при помощи загрузочного компакт-диска (см. «Мелочи жизни» в январском выпуске «Открытых систем» — Прим. ред.) обратили внимание, что автор не упомянул о владельцах дисков SCSI. Это, конечно, непростительное упущение, которое я попытаюсь исправить. 1 2
17.04.1999г Также в разделе:
|
СодержаниеОткрытые системы
Разное Эта рубрика в архиве
Список номеров за
|
||||||||||||||||||