В прошлой лекции была рассмотрена архитектура операционной системы UNIX и её ядра. Было сказано о том, что эта операционная систета базируется на двух «китах»: файлах и процессах. В этой лекции речь пойдет о процессах: что это такое и как они взаимодействуют в рамках UNIX-системы.
С процессами связано несколько важных компонент ядра UNIX, это — управление памятью, планировщик процессов, обеспечение межпроцессного взаимодействия.
Презентация 3-02: управление памятью
Как уже было сказано выше, в UNIX реализована виртуальная память процессов. Каждый процесс исполняется в собственном адресном пространстве и не может непосредственно обращаться к памяти других процессоров. Такая изоляция процессов друг от друга обеспечивается механизмами операционной системы и поддерживается на аппаратном уровне в современных процессорах.
Разделение виртуальной памяти отдельных процессов необходимо в целях безопасности — процессы не должны иметь возможность по собственной инициативе менять данные других процессов — не только из-за потенциальных возможностей злоумышленников, но и потому, что программы могут совершать ошибки, и аварийное завершение одного процесса не должно отражаться на ходе работы других процессов и операционной системы в целом.
В современных реализациях UNIX для большинства аппаратных архитектур используется страничная организация виртуальной памяти. В этом случае всё адресное пространство процесса разбивается на участки одинакового размера (страницы), аналогичным образом разбивается вся физическая память. Каждая станица адресного пространства процесса может отображаться на какую-то страницу физической памяти. Это обеспечивается специальной таблицей преобразования адресов, которую операционная система поддерживает в адекватном состоянии. Таким образом, физическая память разделяется между множеством процессов, причём каждый из процессов имеет доступ только к «своим» страницам, что гарантируется операционной системой и аппаратными возможностями процессора.
В 32-разрядной архитектуре объём адресного пространства процесса равен 4 гигабайтам. Так как число процессов в системе потенциально не ограничено, очевидно, что суммарный размер адресного пространства, необходимого всем процессам, вполне может превысить размеры физической памяти. Конечно, большая часть программ оперирует довольно небольшим объёмом памяти. Однако страничное преобразование позволяет решить проблему недостатка физической памяти: неиспользуемые страницы могут копироваться из физической памяти на диск в специальную область подкачки и храниться там до момента, когда они вновь потребуются для исполнения процесса.
Ещё один мощный механизм, возможный благодаря страничному преобразованию, — это файлы, проецируемые в память. Процесс может «присоединить» файл к своему виртуальному адресному пространству. Это означает, что при обращении к файлу или изменении данных в нем операционная система будет автоматически выполнять отображение содержимого файла в страницы памяти процесса и обратно. Фактически операционная система здесь использует тот же механизм, что и при работе с областью подкачки, когда данные перемещаются с внешнего носителя в память при обращении к соответствующему адресу виртуальной памяти. Отображаемые в память файлы используется в тех случаях, когда необходимо с минимальными временными затратами многократно модифицировать содержимое файла, например, в базах данных или редакторах.
На рисунке Рисунок 1.38, «Размещение страниц виртуальной памяти» показаны возможные случаи размещения страницы виртуальной памяти — в физической памяти, в области подкачки, в файле.
Презентация 3-03: контекст процесса
В операционной системе UNIX традиционно поддерживается классическая схема мультипрограммирования. Система предоставляет возможность параллельного (или псевдопараллельного в случае наличия только одного аппаратного процессора) выполнения нескольких пользовательских программ. Каждому такому выполнению соответствует процесс операционной системы.
Каждому процессу соответствует контекст, в котором он выполняется. Этот контекст включает пользовательский контекст (т. е. содержимое виртуального адресного пространства, сегментов программного кода, данных, стека, разделяемых сегментов и сегментов файлов, отображаемых в виртуальную память), содержимое аппаратных регистров — регистровый контекст (регистр счетчика команд, регистр состояния процессора, регистр указателя стека и регистры общего назначения), а также структуры данных ядра (контекст системного уровня), связанные с этим процессом. Контекст процесса системного уровня в ОС UNIX состоит из «статической» и «динамических» частей. Для каждого процесса имеется одна статическая часть контекста системного уровня и переменное число динамических частей.
Статическая часть контекста процесса системного уровня включает следующее:
Уникальный номер, идентифицирующий процесс. По сути, это номер строки в таблице процессов — специальной внутренней структуре ядра операционной системы, хранящей информацию о процессах.
В любой момент времени ни у каких двух процессов номера не могут совпадать, однако после завершения процесса его номер освобождается и может быть в дальнейшем использован для идентификации любого вновь запущенного процесса.
В операционнной системе UNIX процессы выстраиваются в иерархию — новый процесс может быть создан только одним из уже существующих процессов, который выступает для него родительским.
Очевидно, что в такой схеме должен присутствовать один процесс с особым статусом: он должен быть порождем ядром операционной системы и будет являться родительским для всех остальных процессов в системе. В UNIX такой процесс имеет собственное имя — init. Подробнее об этом процессе сказано в разделе «Процесс init».
Каждый процесс в любой момент времени находится в одном из нескольких определенных состояний: инициализация, исполнение, приостановка, ожидание ввода-вывода, завершение и т. п. (см. Рисунок 1.39, «Состояния процесса в UNIX»).
Большинство этих состояний совпадает с классическим набором состояний процессов в многозадачных операционных системах. Для операционной системы UNIX характерно особое состояние процесса — зомби. Процесс получает это состояние, если он завершился раньше, чем этого ожидал его родительский процесс. В UNIX перевод процессов в состояние зомби служит для корректного завершения группы процессов, освобождения ресурсов и т. п.
Идентификатор пользователя и группы, от имени которых исполняется процесс. Используются операционной системой для определения границ доступа для процесса. Подробнее о правах доступа будет сказано в лекции «Введение в безопасность UNIX».
Число, используемое при планировании
(см. «Планирование процессов») исполнения процесса в
операционной системе. Традиционное решение операционной системы UNIX
состоит в использовании динамически изменяющихся
приоритетов. При образовании каждого процесса ему приписывается
некоторый устанавливаемый
системой статический приоритет, который в дальнейшем может быть изменен
с помощью системного вызова
nice
. Реальным критерием
планирования выступает динамический приоритет, статический
приоритет составляет основу начального значения динамического приоритета
процесса. Все процессы с
динамическим приоритетом не ниже порогового участвуют в конкуренции за
процессор.
Список структур ядра, описывающий все файлы, открытые этим процессом для ввода-вывода.
Динамическая часть контекста процесса — это один или несколько стеков, которые используются процессом при выполнении в режиме пользователя и в режиме ядра (в процессе прерываний и системных вызовов).
Презентация 3-04: планирование процессов
Основной проблемой организации многопользовательского (правильнее сказать, мультипрограммного) режима в любой операционной системе является организация планирования «параллельного» выполнения нескольких процессов. Операционная система должна обладать четкими критериями для определения того, какому готовому к выполнению процессу и когда предоставить ресурс процессора.
Наиболее распространенным алгоритмом планирования в системах разделения времени является кольцевой режим (Round Robin). Основной смысл алгоритма состоит в том, что время процессора делится на кванты фиксированного размера, а процессы, готовые к выполнению, выстраиваются в кольцевую очередь (см. Рисунок 1.40, «Схема планирования с кольцевой очередью»). У этой очереди имеются два указателя — начала и конца. Когда процесс, выполняющийся на процессоре, исчерпывает свой квант процессорного времени, он снимается с процессора, ставится в конец очереди, а ресурсы процессора отдаются процессу, находящемуся в начале очереди. Если выполняющийся на процессоре процесс откладывается (например, по причине обмена с некоторым внешним устройством) до того, как он исчерпает свой квант, то после повторной активизации он становится в конец очереди (не смог доработать — не вина системы). Это прекрасная схема разделения времени в случае, когда все процессы одновременно помещаются в оперативной памяти.
Однако операционная система UNIX всегда была рассчитана на то, чтобы обслуживать больше процессов, чем можно одновременно разместить в основной памяти. Другими словами, часть процессов, потенциально готовых выполняться, размещалась во внешней памяти (куда образ памяти процесса попадал в результате откачки). Для оптимизации работы в этом случае требуется несколько более гибкая схема планирования при разделении ресурсов процессора. В результате было введено понятие приоритета (см. Рисунок 1.41, «Схема планирования с кольцевой очередью и приоритетами»). В операционной системе UNIX на основании значения приоритета процесса определяется, во-первых, возможность процесса пребывать в основной памяти и на равных конкурировать за процессор. Во-вторых, от значения приоритета процесса зависит размер временного кванта, который предоставляется процессу для работы на процессоре при достижении своей очереди. В-третьих, значение приоритета влияет на место процесса в общей очереди процессов.
Презентация 3-05: создание новых процессов
Механизм порождения новых процессов довольно существенно различается в разных операционных системах. Во всех операционных системах семейства UNIX новые процессы в системе не появляются «из ниоткуда», а ответвляются от уже существующих в системе процессов.
Когда возникает необходимость создания нового процесса (C), текущий процесс (P)
выполняет системный вызов fork
(см. рисунок Рисунок 1.42, «Создание нового процесса»). При этом создаётся новый процесс,
представляющий собой копию исходного процесса и его
контекста. Новый процесс отличается тем, что у него другой
PID, а родителем для него является запустивший процесс (P). Далее
дочерний процесс (C) с помощью системного вызова exec
запускает вместо себя другую программу, заново проинициализировав свои код и данные.
Существует также и обратная связь между дочерним процессом и
родительским. Родительский процесс (P)
может синхронизовать своё исполнение с завершением процесса
(C) (ожидать завершения дочернего процесса) с помощью
специального системного вызова wait
.
В современных версиях операционной системы UNIX помимо процессов существует понятие потока (или нити) исполнения. В рамках процесса может существовать несколько потоков, каждый из которых исполняется независимо, но все они объединены общим виртуальным адресным пространством. Можно сказать, что все процессы исполняются с единственным потоком по умолчанию, но при необходимости могут быть созданы новые потоки.
Потоки отсутствовали в оригинальной архитектуре UNIX и были добавлены под влиянием современных архитектур персональных компьютеров, в которых переключение контекста исполнения между процессами занимает значительно большее время, чем переключение контекста исполнения между потоками. Однако ценой эффективного переключения между потоками является сильное влияние потоков в рамках одного процесса друг на друга (критическая ошибка в одном потоке приводит к завершению всего процесса), поэтому в UNIX-системах потоки традиционно используются редко.
Презентация 3-06: межпроцессное взаимодействие
Полная изоляция процессов в операционной системе бессмысленна, так как им часто необходимо обмениваться данными в процессе работы. Поэтому одна из важнейших задач операционной системы — обеспечивать контролируемые взаимодействия процессов, в том числе за счет возможности разделения одного сегмента памяти между виртуальной памятью нескольких процессов. Для решения задачи межпроцессного взаимодействия в операционной системе UNIX существует набор специальных средств: потоки ввода-вывода, перменные окружения, каналы и сокеты, разделяемая память и сигналы. Рассмотрим подробнее все эти механизмы межпроцессного взаимодействия.
Презентация 3-07: разделяемая память
Самый простой способ «обойти» разделение виртуальных пространств процессов — использование разделяемой памяти. Это специальный механизм, с помощью которого средствами операционной системы два процесса могут обращаться к общему участку физической памяти — каждый через свое адресное пространство.
Для операционной системы этот способ является наиболее простым — ведь все страница виртуальной памяти процессов в любом случае проецируются на какую-то область физической памяти — так почему бы на ту же область не проецировать часть адресного пространства другого процесса? Самое важное, что такое взаимодействие не требует каких-либо накладных расходов, процессы обмениваются информацией со скоростью обращения к памяти.
Однако для пользователя такой способ межпроцессного взаимодействия является труднодоступным. Во-первых, программы, взаимодействующие таким образом, должны изначально содержать соответствующий код — с помощью специальных системных вызовов обе программы должны обозначить участки своих адресных пространств, предназначенные для обмена информацией. Другая сложность состоит в том, что разделяемая память сама по себе не содержит средств синхронизации, программы должны согласованно изменять общий участок памяти, чтобы не испортить данные; обычно для этих целей применяются семафоры и аналогичные механизмы синхронизации.
Таким образом, разделяемая память — наиболее быстрый способ обмена, но при этом малопргодный для широкого использования. Обычная сфера применения разделяемой памяти — специализированные высокопроизводительные программы. Стоит также обратить внимание на явную аналогию разделяемой памяти и исполнения множества потоков в рамках одного процесса — в UNIX эти инструменты построения программ используются редко и только в связи с высокопроизводительными вычислениями и вводом-выводом.
Презентация 3-08: переменные окружения
Каждый запускаемый процесс система снабжает неким информационным пространством, которое этот процесс может изменять. Правила пользования этим пространством просты: в нем можно задавать именованные хранилища данных (переменные окружения), в которые записывать любую текстовую информацию (присваивать значение переменной окружения), а впоследствии эту информацию считывать (подставлять значение переменной).
В UNIX дочерний процесс создаётся как точная копия родительского, поэтому его окружение — также точная копия родительского. Если про дочерний процесс известно, что он использует значения некоторых переменных из числа передаваемых ему с окружением, родительский может заранее указать, каким из копируемых в окружении переменных нужно изменить значение (см. рисунок Рисунок 1.44, «Наследование переменных окружения»). Одна и та же программа при одинаковом использовании, но в измененном окружении — может выдавать различные результаты. Например, от переменной окружения может зависить язык интерфейса программы или формат используемых данных.
Конечно, переменные окружения — очень ограниченное средство межпроцессного взаимодействия. Во-первых, действие их односторонне, так как дочерний процесс не может изменить окружение родительского процесса. Более того, родительский процесс никак не может воспользоваться информацией из окружения дочернего. Во-вторых, окружение уже запущенного процесса изменить извне нельзя. В-третьих, через переменные окружения можно передавать только текстовые данные, обычно небольшого объёма. Поэтому переменные окружения используются в основном для задания условий запуска программы: положение конфигурационных файлов, требуемые параметры интефейса и аналогичные простые параметры.
Сигналы — одно из традиционных средств межпроцессного взаимодействия в UNIX. Сигнал может быть отправлен процессу операционной системой или другим процессом. Операционная система использует сигналы для доставки процессу уведомлений об ошибках и неправильном поведении.
При получении сигнала исполнение процесса приостанавливается и запускается специальная подпрограмма — обработчик сигнала. Обработчики сигналов могут быть явно определены в исходном тексте исполняемой программы, если же они отсутствуют, а также в некоторых специальных случаях используется стандартный обработчик, определённый операционной системой.
У сигнала есть только одна характеристика, несущая
информацию — его номер (целое число). Иначе
говоря, сигналы — это заранее определённый и
пронумерованный список сообщений. Для удобства использования
каждый сигнал имеет сокращённое буквенное имя. Список
сигналов и их имён стандартизован и практически не
отличается в различных версиях UNIX. Для
отправки сигналов процессам используется специальный системный вызов
kill
и одноимённая ему пользовательская
утилита. К основным сигналам относятся:
Процесс может с
помощью специального системного вызова abort
задать
время, через которое ему необходимо отправить
сигнал. Через указанный промежуток времени операционная
система доставит процессу сигнал SIGALARM. Обычно этот
прием применяется для задания таймаутов. Если процесс не
разегистрировал обработчик этого сигнала, то обработчик
по умолчанию завершает процесс.
Сигнал отправляется родительскому процессу в случае завершения его дочернего процесса. По умолчанию сигнал игнорируется.
Сигнал продолжения исполнения программы после остановки. Обработчика по умолчанию нет.
Сигнал ошибки в вычислениях с плавующей точкой, отправляется операционной системой при некорректном исполении программы. Обработчик по умолчанию завершает процесс.
Сигнал закрытия терминала, к которому привязан данный процесс. Обычно отправляется операционной системой всем процессам, запущенным из командной строки при завершении сеанса пользователя. Обработчик по умолчанию завершает процесс.
Сигнал некорректной инструкции. Отправляется операционной системой процессу в случае, если в исполнении программы встретилась некорректная инструкция процессора. Обработчик по умолчанию завершает процесс.
Сигнал аварийного завершения процесса. По этому сигналу процесс завершается немедленно — без освобождения ресурсов. Этот сигнал не может быть перехвачен, заблокирован или переопределён самим процессом, всегда используется стандартный обработчик опрационной системы. Этот сигнал используется для гарантированного завершения процесса.
Сигнал отправляется процессу, который пытается отправить данные в канал, закрытый с противоположной стороны. Такая ситуация может возникнуть в случае, если один из взаимодействующих процессов был аварийно завершён. Обработчик по умолчанию завершает процесс.
Сигнал отправляется процессу операционной системой, если была произведена неверная операция с памятью (обращение по несуществующему или защищённому адресу). Обработчик по умолчанию завершает процесс.
Сигнал приостановки работы процесса. Этот сигнал не может быть перехвачен, заблокирован или переопределён. Используется для гарантированной приостановки работы процесса с полным сохранением его состояния и возможностью возобновления.
Сигнал завершения процесса, как правило используется для корректного завершения его работы. Пример использования сигнала показан на рисунке Рисунок 1.45, «Пример использования сигнала SIGTERM»
«Пользовательские» сигналы — могут использоваться процессами для всевозможных уведомлений. Обработчик по умолчанию завершает процесс.
Сигналы являются ограниченным средством межпроцесснного обмена. Они прекрасно подходят для уведомлений, но не могут использоваться для передачи информации между процессами. Сигналы передаются без каких-либо сопутствующих данных, поэтому они обычно комбинируются с другими способами обмена (например, как показано на рисунке Рисунок 1.46, «Использование сигналов при межпроцессном обмене» — здесь сообщения между процессами сохраняются в общем файле, а сигнал служит для уведомления о приходе нового сообщения). Ещё одна интересная особенность сигналов — в случае поступления нескольких сигналов одного вида в течение короткого интервала времени, принимающий процесс рассматривает их как один поступивший сигнал, и вызывает обработчик только один раз, т. е. сигналы не накапливаются.
Канал — поток данных между двумя или несколькими процессами, имеющий интерфейс, аналогичный чтению или записи в файл. Каналы бывают одно- и двунаправленными. В UNIX каналы, как и многие другие системные объекты, представлены в виде файлов, вся работа с ними производится через базовый файловый интерфейс — открытие и закрытие файла, чтение и запись данных и т. п. В этом смысле каналы можно представлять в виде специализированных файлов, которые не хранят информацию, а лишь накапливают её до следующей операции чтения из канала другим процессом, образуя очередь.
По умолчанию в UNIX каждому процессу при запуске ставится в соответствие три открытых файла: стандартного ввода, стандартного вывода и стандартного вывода для ошибок. С помощью средств командной строки (см. подробнее в «Терминал и командная строка») такие потоки для разных процессов могут быть объединены так, что, к примеру, вывод одного процесса будет подаваться на ввод другого (см. рисунок Рисунок 1.47, «Неименованый канал между двумя процессами». То есть процесс работает с тремя потоками данных одинаково вне зависимости от того, обычные это файлы или же кананлы. В более общем смысле такие потоки называют неименованными каналами. Канал создаётся по запросу и существует только в ходе работы двух процессов, другие процессы в системе не могут обратиться к этому каналу. Если процесс на одной из сторон канала завершается и закрывает канал, другому процессу посылается специальный сигнал — SIGPIPE.
Другой вид каналов в UNIX — именованные каналы — представляют собой особый тип файлов. Эти файлы располагаются в файловой системе и могут быть открыты любым процессом (если это позволяется правами доступа, см. раздел «Введение в безопасность UNIX»). Одни процессы записывают данные в канал, другие — читают из него, данные продвигаются по каналу в порядке очереди (FIFO).
Каналы широко используются в UNIX, как при запуске программ в командной строке, так и при взаимодействии системных процессов. Главное достоинство каналов — простота и удобство использования привычного файлового интерфейса. С другой стороны, данные в каналах передаются в одном направлении и последовательно, что ограничивает сферу применения каналов.
Сокеты предоставляют альтернативный интерфейс обмена данными как в рамках одной системы, так и между процессами, запущенными на разных машинах в сети.
Интерфейс сокетов позволяет явно разделить во взаимодействии двух процессов серверную и клиентскую часть. Серверный процесс инициализирует сокет и ждёт входящих соединений от других процессов. Клиентский процесс устанавливает соединение, и с этого момента по образовавшемуся каналу можно передавать поток данных в обе стороны. Такие сокеты называются потоковыми. Другой тип сокетов, датаграмный, позволяет отправлять сообщения (длиной не более 64 кбайт) между процессами, привязанными к данному сокету.
Интерфейс сокетов вперывые появился в операционной системе BSD и использовался для связи компьютеров через сеть Internet по протоколам TCP (потоковые сокеты) и UDP (датаграмные сокеты), о чём подробнее рассказывается в разделе «Сеть в UNIX». Это основное применение сокетов, и до настоящего времени они являются стандартным средством взаимодействия процессов в сети. Кроме того, существует локальный вариант взаимодействия через сокеты, в котором обмен данными происходит через специальные файлы, расположенные в файловой системе (фактически, это аналог именованых каналов, но с интерфейсом сокетов).
Управление процессами — второй важный аспект работы операционной системы UNIX. В операционной системе реализован сложный механизм управления памятью, позволяющий организовать собственное виртуальное адресное пространство для каждого процессса в системе.
Каждый процесс в системе имеет уникальный идентификатор, состояние и контекст исполнения. В UNIX реализован механизм псевдопараллельного исполнения множества процессов. Планирование процессов производится на основе динамических приоритетов.
Новые процессы создаются путем разветвления процесса на родительский и дочерний. Таким образом, все процессы в системе выстраиваются в строгую иерархию.
Для межпроцессного взаимодействия используются специальные средства, основными среди которых являются разделяемая память, каналы, сигналы и сокеты. Каждый из этих способов взаимодействия обладает своими достоинствами и характерной областью применения.
Ключевые термины: страница, область подкачки, файлы, проецируемые в память, контекст процесса, идентификатор процесса, родительский процесс, init, состояние процесса, зомби, приоритет процесса, планирование, Round Robin, поток, межпроцессное взаимодействие, разделяемая память, окружение, переменная окружения, сигнал, канал, неименованный канал, сокет