Операционные системы

Краткий очерк истории операционных систем. Классификация периферийных устройств и их архитектура. Характеристики файлов и архитектура файловых систем. Распределение памяти без использования виртуальных адресов. Драйверы устройств в Windows, UNIX.

Рубрика Программирование, компьютеры и кибернетика
Вид курс лекций
Язык русский
Дата добавления 01.02.2011
Размер файла 418,6 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

4.5.4. Процесс и нить как объекты

Подсистема управления процессами в Windows позволяет рассматривать процессы и нити как объекты, над которыми нити различных процессов могут выполнить целый ряд действий. Для этого требуется иметь хэндл процесса или нити. Как нам известно, такие хэндлы возвращаются как побочные результаты работы функций CreateProcess и CreateThread. Это позволяет процессу, запустившему другой процесс или нить, выполнять с ними необходимые действия. Однако в некоторых случаях желательно дать возможность управлять процессом не процессу-«родителю», а другому, совершенно постороннему процессу (например, программе администрирования системы). Проблема при этом в том, что, как уже отмечалось, хэндл в Windows имеет смысл только в пределах того процесса, который вызвал функцию Create и не может быть просто передан другому процессу.

Функция OpenProcess позволяет одному процессу получить хэндл любого другого процесса, указав для этого два параметра: идентификатор интересующего процесса и маску, задающую набор запрашиваемых прав по отношению к этому процессу. Если подсистема безопасности Windows, сверив маркер доступа запрашивающего процесса с дескриптором безопасности процесса-объекта (см. п. 3.8.4.1), сочтет запрос законным, то функция вернет требуемый хэндл.

А о каких, собственно, правах идет речь? Что именно один процесс может сделать с другим процессом? Основные права следующие:

запускать новую нить процесса;

запрашивать и изменять класс приоритета процесса;

читать и записывать данные в памяти процесса;

использовать процесс в качестве объекта синхронизации;

принудительно прекращать выполнение процесса;

запрашивать код завершения процесса.

Все перечисленные действия выполняются с помощью соответствующих API-функций (например, для прекращения процесса вызывается функция TerminateProcess), одним из параметров которых указывается хэндл открытого процесса-объекта.

4.5.5. Синхронизация нитей

4.5.5.1. Способы синхронизации

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

Традиционной для Windows формой синхронизации является обмен сообщениями (messages). При этом, механизм сообщений предназначен не только для синхронизации, но и для обмена данными. Далее работа с сообщениями будет рассмотрена подробно.

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

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

4.5.5.2. Объекты синхронизации и функции ожидания

Объекты синхронизации представляют собой частный случай объектов ядра Windows. Отличительной особенностью объектов синхронизации является возможность использовать в качестве аргументов функций ожидания.

Для создания объекта нужно вызвать функцию CreateXxx, где вместо Xxx указывается название конкретного типа объектов, например, CreateMutex или CreateEvent.

Общей чертой всех объектов синхронизации является наличие двух состояний, одно из которых называется сигнальным (signaled), а другое, соответственно, несигнальным. Смысл сигнального состояния различен для разных типов объектов, общим же является то, что состояние ожидания для нити заканчивается тогда, когда объект переходит в сигнальное состояние.

Для конкретных типов объектов вместо термина «сигнальное состояние» может быть удобно использовать как синонимы «свободно», «включено», «имеется», «произошло» и т.п., а для несигнального, соответственно, «занято», «выключено», «нет» и т.п.

Поскольку объекты существуют в системной памяти, процессы могут получить к ним доступ только с помощью хэндлов. Это требует затрат времени на переключение контекста памяти при работе с объектами, но зато дает возможность синхронизации нитей, принадлежащих разным процессам. Для этого при создании объектов ядра одним из параметров функции CreateXxx может указываться имя этого объекта. Если после этого другой процесс попытается создать объект того же типа и с тем же именем, то он получит хэндл уже существующего объекта. В результате оба процесса будут работать с одним и тем же объектом синхронизации.

К функциям ожидания относятся WaitForSingleObject (ожидание одного объекта) и WaitForMultipleObjects (ожидание нескольких объектов). Рассмотрим сначала первую из них.

Функция WaitForSingleObject имеет два аргумента:

хэндл какого-либо объекта синхронизации;

тайм-аут ожидания, т.е. максимальное время ожидания (в миллисекундах).

Эта функция - блокирующая: если в момент ее вызова указанный объект находится в несигнальном состоянии, то вызвавшая нить переводится в состояние ожидания (разумеется, пассивного!). Ожидание, как правило, завершается в одном из двух случаев: либо объект переходит в сигнальное состояние, либо истекает заданный тайм-аут. Третий, особый случай завершения связан с понятием «заброшенный мьютекс», которое будет пояснено ниже. Значение, возвращаемое функцией, позволяет узнать, какая из трех причин пробуждения нити имела место.

Если в момент вызова функции указанный объект уже находился в сигнальном состоянии, то ожидания не происходит, функция сразу же возвращает результат.

Если задана нулевая величина тайм-аута, то выполнение функции сводится к проверке, находится ли в данный момент объект в сигнальном состоянии.

Если значение тайм-аута равно константе INFINITE, то время ожидания не ограничено, т.е. нить может пробудиться только по сигнальному состоянию объекта.

Функция WaitForMultipleObjects отличается тем, что вместо единственного хэндла принимает в качестве аргументов адрес массива, содержащего несколько хэндлов, и количество этих хэндлов (размер массива). Как и в предыдущем случае, задается величина тайм-аута. Дополнительный, четвертый аргумент - булевский флаг режима ожидания. Значение FALSE означает ожидание любого объекта (достаточно, чтобы хоть один из них перешел в сигнальное состояние), TRUE - ожидание всех объектов (ожидание завершится, только когда все объекты окажутся в сигнальном состоянии).

Возвращаемое значение позволяет определить причину пробуждения:

переход одного из заданных объектов в сигнальное состояние, и какого именно объекта;

переход одного из объектов-мьютексов в «заброшенное» состояние (будет пояснено ниже), и какого именно объекта;

истечение тайм-аута ожидания.

Кроме двух названных, имеется еще ряд функций ожидания. Функции WaitForSingleObjectEx и WaitForMultipleObjectsEx позволяют также дожидаться завершения асинхронных операций ввода/вывода для указанного файла. Функция MsgWaitForMultipleObjects позволяет, наряду с объектами синхронизации, использовать для пробуждения появление сообщений, ожидающих обработки. Функция SignalObjectAndWait выполняет редкостный трюк: она переводит один объект в сигнальное состояние и тут же начинает дожидаться другого объекта.

4.5.5.3. Типы объектов синхронизации

Список объектов синхронизации включает следующие типы объектов:

процессы;

нити;

события (в узком смысле);

мьютексы (двоичные семафоры);

семафоры (недвоичные);

таймеры;

уведомления об изменениях в каталоге;

консольный ввод.

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

Простейшим из объектов, предназначенных только для синхронизации, является событие (event). Он создается при помощи функции CreateEvent, которая требует указать вид события (с автосбросом или с ручным сбросом), а также его начальное состояние - сигнальное или несигнальное. Переход в сигнальное состояние выполняется функцией SetEvent, а в несигнальное - ResetEvent. Функция PulseEvent как бы совмещает обе предыдущие: сигнальное состояние включается, функция ожидания, если она была вызвана ранее, завершается, но сигнальное состояние тут же сбрасывается.

Событие с автосбросом означает, что функция ожидания при завершении ожидания сбрасывает то событие (или события), которого (которых) она дождалась. Если несколько нитей ждут одного и того же события с автосбросом, то разблокирована будет только одна из них (Windows не гарантирует, какая именно), поскольку остальным «события не достанется». Напротив, событие с ручным сбросом остается в сигнальном состоянии и может быть сброшено только функцией ResetEvent, а до тех пор оно может «накормить» сколько угодно ожидающих нитей. Можно сказать, что событие с ручным сбросом больше похоже не на событие, а на состояние.

Следующий тип объектов - мьютекс (mutex). Это почти в чистом виде двоичный семафор в смысле Дейкстры. Мьютекс может быть либо свободен (это его сигнальное состояние), либо захвачен какой-либо нитью. При создании мьютекса в функции CreateMutex указывается его начальное состояние. Допускается многократный захват мьютекса одной и той же нитью, но после этого нить должна столько же раз освободить мьютекс, чтобы он вернулся в свободное состояние.

Для освобождения мьютекса нить, владеющая им, вызывает функцию ReleaseMutex, которая является аналогом дейкстровской V(S). Никакая другая нить, кроме владельца, не может освободить «чужой» мьютекс.

В специальной функции для захвата мьютекса нет необходимости, поскольку работу примитива P(S) с успехом выполняют функции ожидания.

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

Объект типа семафор (semaphore) представляет собой недвоичный семафор, используемый обычно для управления распределением ограниченного числа единиц ресурса. В функции CreateSemaphore указываются два числа: максимальное значение счетчика, связанного с семафором (сколько всего имеется единиц ресурса), и начальное значение этого счетчика (сколько единиц свободно). Сигнальным является состояние семафора со счетчиком больше нуля. Функция ожидания уменьшает счетчик на 1, а функция ReleaseSemaphore увеличивает его на заданное количество единиц, но так, чтобы результат не превышал максимального значения.

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

Таймер «для ожидания» (waitable timer) создается с помощью функции CreateWaitableTimer, после чего должен еще быть установлен функцией SetWaitableTimer. При создании таймера указывается его вид: с ручным сбросом или с автосбросом, аналогично объекту «событие». При вызове SetWaitableTimer указывается время до перехода в сигнальное состояние, в единицах по 100 нс. Время можно указать либо абсолютно (когда «зазвонить»), либо относительно текущего момента (как долго ждать). Можно также указать период (в миллисекундах), через который должен повторяться сигнал.

Таймер с ручным сбросом, перейдя в сигнальное состояние, остается в нем до вызова функции CancelWaitableTimer или повторного вызова SetWaitableTimer. Таймер с автосбросом может быть также сброшен функциями ожидания.

Полезно напомнить, что в Windows широко используется и другой тип таймеров «для сообщений», создаваемый функцией SetTimer, но эти таймеры не могут взаимодействовать с функциями ожидания и даже не являются объектами.

Объект типа уведомление об изменениях создается при помощи функции FindFirstChangeNotification. При этом указываются следующие аргументы:

каталог файловой системы, изменения в котором должны отслеживаться;

флаг, указывающий, должны ли отслеживаться изменения только в заданном каталоге или также во всех вложенных подкаталогах;

фильтр отслеживаемых изменений.

Фильтр изменений представляет собой маску, в которой отдельные биты задают такие события, как изменение списка имен файлов и каталогов (это включает их создание, удаление и переименование), изменение атрибутов файла, размера, даты изменения и атрибутов защиты.

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

Уведомления об изменениях позволяют избежать активного ожидания для программ, которые должны отслеживать и отображать состояние каталогов в динамике (как это делают, например, Проводник или Far Manager). При этом характер происшедшего изменения программа может выяснить, только перечитав каталог, но зато достоверно известно, что какое-то изменение было.

Хэндл объекта типа консольный ввод возвращается функцией CreateFile, у которой вместо имени открываемого файла указано устройство консольного ввода (CONIN$). Объект в сигнальном состоянии, когда в буфере ввода есть непрочитанные символы.

4.5.5.4. Критические секции

Еще одним средством синхронизации потоков служат переменные типа CRITICAL_SECTION. По назначению они совпадают с рассмотренными выше мьютексами, однако реализация двоичного семафора в этом случае совершенно иная. Если мьютексы - это один из типов объектов ядра, то критические секции - просто переменные. В чем здесь принципиальная разница? В том, что объекты существуют в памяти системы, поэтому прикладная программа не может напрямую обратиться к объекту, прочитать или записать его значение. Программа может только получить хэндл объекта, а затем она поручает системе выполнить то или иное действие с объектом, на который указывает данный хэндл. Такое решение позволяет использовать один объект для связи нескольких процессов, сохраняя при этом изоляцию памяти процесса от памяти системы и других процессов.

Недостаток использования объектов ядра в том, что обращение к ним всякий раз требует переключения контекста с пользовательского на системный и обратно, а это требует времени. Такова цена изоляции процессов.

Если требуется синхронизировать работу нитей одного и того же процесса, то более «дешевым» решением может быть использование переменных CRITICAL_SECTION. Программист должен описать переменную этого типа в программе процесса, при этом для всех нитей процесса будет доступен адрес этой переменной. Перед началом работы с критической секцией одна из нитей процесса должна вызвать функцию InitializeCriticalSection, передав ей адрес переменной. Затем он может использовать функцию EnterCriticalSection, чтобы занять двоичный семафор, и функцию LeaveCriticalSection, чтобы освободить его. Как и для мьютекса, один и тот же поток может несколько раз занять критическую секцию, но потом должен столько же раз освободить ее. Имеется еще удобная функция TryEnterCriticalSection, которая позволяет избежать блокировки и занимает критическую секцию, только если та была свободна в момент обращения. По истечении необходимости в синхронизации нитей следует вызвать функцию DeleteCriticalSection.

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

4.5.6. Сообщения

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

Сообщение представляет собой структуру данных, содержащую тип сообщения, адресат сообщения (конкретное окно, все окна или нить процесса), время посылки сообщения, координаты курсора (для сообщений от мыши), а также два параметра, смысл которых зависит от типа сообщения.

Система посылает сообщения приложениям по самым разным поводам: при нажатии клавиш и кнопок мыши, создании и закрытии окон, изменении размеров и положения окна, выборе объекта в каком-либо списке, выборе команды в меню и т.п. Часто одно и то же действие пользователя (например, щелчок левой кнопкой мыши на пункте меню) вызывает посылку нескольких разных сообщений.

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

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

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

Нить процесса получает сообщения, адресованные принадлежащим ей окнам или самой нити. Для выборки сообщений из очереди используется функция GetMessage. В случае отсутствия сообщений нить блокируется до их поступления. При желании программист может использовать функцию PeekMessage, которая проверяет наличие сообщения, не блокируя вызывающую нить.

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

В результате этого системе приходится действовать очень аккуратно. Нить, пославшая синхронное сообщение окну другой нити, блокируется. Сообщение помещается в отдельную очередь синхронных сообщений к нити-получателю. Эта нить продолжает свою нормальную работу до момента, когда она обратится за очередным сообщением, т.е. вызовет GetMessage или PeekMessage. Тогда система вызывает оконные функции нити для обработки каждого из ожидающих синхронных сообщений. Только когда все они обработаны, выполняется вызванная функция GetMessage или PeekMessage, проверяющая наличие асинхронных сообщений в очереди.

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

4.6. Управление процессами в UNIX

4.6.1. Жизненный цикл процесса

Понятие процесса в UNIX существенно отличается от аналогичного понятия в других популярных ОС. Если в MS-DOS, Windows и других системах новый процесс создается только при запуске программы, то в UNIX создание процесса и запуск программы - это два совершенно разных действия. При этом процесс в некотором роде «более первичен», чем программа. Если по стандартному определению (см. п. 4.2.1) процесс есть работа по выполнению программы, то в UNIX будет более уместно сказать, что программа - это один из ресурсов, используемых процессом.

Существует единственный способ создания процесса в UNIX, и этот способ заключается в вызове функции без параметров fork(). Эта функция создает новый процесс, который является точной копией процесса-родителя: выполняет ту же программу, наследует такие же хэндлы открытых файлов и т.д. При этом содержимое областей памяти процесса копируется. Единственным различием является идентификатор процесса (pid) - целое число, уникальное для каждого процесса в системе. После завершения создания оба процесса, и родитель, и потомок, будут выполнять одну и ту же команду, следующую в программе после вызова fork. Однако при этом функция fork возвращает процессу-родителю значение pid порожденного потомка, а потомку возвращает значение 0. Проверка возвращенного значения - простейший способ для процесса определить, «кто он такой» - родитель или потомок.

Типовой фрагмент программы на C может выглядеть примерно так:

pid = fork(); // Создание нового процесса

if (pid == -1) // Процесс не создан

{ обработка ошибки создания процесса}

else if (pid == 0) // Это порожденный процесс

{ операторы, выполняемые процессом-потомком }

else // Это процесс-родитель

{ операторы, выполняемые процессом-родителем }

В принципе, оба процесса могут в дальнейшем выполнять команды из одного и того же файла программы. Чаще, однако, процесс вскоре после создания начинает выполнять другую программу. Для этого используется одна из функций семейства exec. Несколько функций, имена которых начинаются с exec, различаются деталями - в каком формате передаются параметры командной строки, следует ли использовать поиск файла программы по переменной PATH и т.п. Каждая из этих функций запускает указанный файл программы, однако при этом не порождается новый процесс, просто текущий процесс меняет выполняемую программу. При этом полностью перестраивается заново контекст процесса.

Механизм создания новых процессов один и тот же как для процессов пользователя, так и для системных процессов. Единственным исключением является самый первый процесс, имеющий идентификатор 0. Он порождает второй системный процесс, который называется INIT и имеет идентификатор 1. Все остальные процессы в системе являются потомками INIT.

Для нормального завершения процесса используется функция exit(status). Целое число status означает код завершения, заданный программистом, при этом значение 0 означает успешное завершение, а ненулевые значения - разного рода ошибки и нестандартные случаи.

Процесс-потомок полностью независим от родителя, и завершаются эти процессы независимо друг от друга. Тем не менее, процесс-родитель имеет возможность синхронизироваться с моментом завершения потомка (проще говоря, подождать этого завершения). Для этого родитель выполняет вызов функции wait

pid = wait(&status);

Эта блокирующая функция переводит вызывающий процесс в ожидание до момента завершения любого из потомков, порожденных этим процессов. Так работает, например, интерпретатор команд UNIX, который запускает команду, введенную с консоли, и ожидает ее завершения. Функция wait возвращает pid завершившегося потомка, а в переменной status передает код завершения.

Если процесс-потомок завершает свое выполнение до того, как родитель вызвал функцию wait, то завершившийся процесс переходит в состояние, которое принято называть «зомби». Фактически от процесса остается лишь запись в таблице процессов, содержащая код завершения и информацию о затраченном процессорном времени. Все ресурсы, занимавшиеся процессом, освобождаются. Если в дальнейшем родитель все же вызовет wait, то после передачи кода завершения «зомби» будет вычеркнут из таблицы, на чем и закончится жизненный цикл процесса.

Возможна также ситуация, когда процесс-родитель завершается до того, как будет завершен его потомок. Поскольку процесс не должен оставаться без родителя, «сирота» будет «усыновлен» системным процессом INIT.

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

4.6.2. Группы процессов

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

Любой процесс может покинуть свою группу и объявить себя лидером новой группы, к которой будут относиться его потомки. Одна из групп является текущей (foreground), остальные группы - фоновыми (background). Процессы текущей группы могут получать ввод с управляющего терминала.

Понятие группы процессов играет важную роль в ряде ситуаций при работе системы. Например, если пользователь нажимает Ctrl+C, то всем процессам текущей группы посылается сигнал о необходимости завершения. При разрыве соединения с терминалом подобный сигнал посылается всем процессам, для которых этот терминал являлся управляющим.

Процесс может, создав собственную группу, затем «открепиться» от управляющего терминала. Такой процесс, называемый в UNIX «демоном», теряет возможность вести диалог с пользователем, но зато он не будет завершаться, когда пользователь закончит сеанс работы с системой. Демоны в UNIX выполняют обычно общесистемные задачи, такие, как управление печатью, получение и отправка почты, автоматический запуск процессов в заранее заданные моменты времени и т.п.

4.6.3. Программные каналы

Одним из «фирменных» изобретений UNIX, впоследствии позаимствованных другими ОС, является понятие программного канала или «трубопровода» (pipe), позволяющего выполнять обмен данными между процессами с помощью тех же системных вызовов, которые используются для чтения/записи данных при работе с файлами и с периферийными устройствами.

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

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

Если все процессы закрыли хэндлы записи в канал (то есть, нет шансов, что в канал будут помещены еще какие-нибудь данные), то процесс-читатель, выбрав все данные, которые еще оставались в канале, прочтет затем признак конца файла.

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

Использование безымянных каналов имеет одно ограничение: все процессы, работающие с каналом, должны быть потомками процесса, создавшего канал. Для передачи данных между неродственными процессами можно использовать именованные каналы, называемые также каналами FIFO. Такой канал создается системным вызовом mknod, при этом указывается путь и имя канала, как при создании файла. Имена каналов хранятся в каталогах файловой системы UNIX наравне с именами обычных и специальных файлов. Чтобы открыть канал для чтения или для записи, используется обычный системный вызов open с указанием требуемого режима доступа, как при открытии файла.

4.6.4. Сигналы

Сигнал представляет собой уведомление процесса о некотором событии, посылаемое системой или другим процессом.

Основными характеристиками сигнала являются его номер и адресат сигнала. В разных версиях UNIX используется от 16 до 32 сигналов. Номер сигнала означает причину его посылки. В программах номер сигнала обычно задается одной из стандартных констант. Отметим следующие сигналы:

SIGKILL - процесс должен быть немедленно прекращен;

SIGTERM - более «вежливая» форма: система предлагает процессу прекратиться, но он может и не послушаться;

SIGILL - процесс выполнил недопустимую команду;

SIGSEGV - процесс обратился к неверному адресу памяти;

SIGHUP - разорвана связь процесса с управляющим терминалом (например, модем «повесил трубку»);

SIGPIPE - процесс попытался записать данные в канал, к другому концу которого не присоединен ни один процесс;

SIGSTOP - процесс должен быть немедленно приостановлен;

SIGCONT - приостановленный процесс возобновляет работу;

SIGINT - пользователь нажал Ctrl+C, чтобы прервать процесс;

SIGALRM - поступил сигнал от ранее запущенного таймера;

SIGCHLD - завершился один из потомков процесса;

SIGPWR - возникла угроза потери электропитания, компьютер переключился на автономный источник (т.е. пора срочно спасать данные);

SIGUSR1, SIGUSR2 - номера сигналов, предоставленные в распоряжение прикладных программ для использования в произвольных целях.

При возникновении соответствующего события система посылает сигнал процессу или группе процессов (например, SIGHUP посылается всем процессам, связанным с данным терминалом).

Любой процесс также может послать любой сигнал другому процессу или группе процессов. Для этого используется системный вызов со страшным именем kill. Параметрами функции служат номер посылаемого сигнала и получатель сигнала. Сигнал может быть послан конкретному процессу (указывается pid получателя), а также всем процессам группы отправителя или другой указанной группы, или всем процессам, запущенным данным пользователем. Привилегированный пользователь может даже послать сигнал всем процессам в системе, за исключением корневых системных процессов 0 и 1.

Для каждого процесса система хранит маску, задающую его реакцию на каждый из сигналов. Всего возможно три вида реакции.

По умолчанию - выполняются действия, предусмотренные системой для данного номера сигнала. Для большинства сигналов действия по умолчанию предусматривают завершение процесса.

Игнорировать - процесс никак не реагирует на получение сигнала.

Обработать - в этом случае процесс должен задать адрес функции, которая будет вызвана при получении сигнала.

Для сигналов SIGKILL и SIGSTOP всегда используется реакция по умолчанию. Для остальных сигналов процесс может установить требуемую реакцию. Традиционным средством для этого является системный вызов signal. Одним из параметров этой функции указывается номер сигнала, другим - либо адрес функции, обрабатывающей сигнал, либо одно из специальных значений, означающих «По умолчанию» и «Игнорировать».

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

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

В современных версиях UNIX, наряду с традиционными средствами обработки сигналов, может также использоваться аппарат «надежных сигналов», позволяющий преодолеть названные трудности.

4.6.5. Средства взаимодействия процессов в стандарте POSIX

Десятилетия успешного использования UNIX выявили, тем не менее, ряд недостатков в исходной архитектуре этой системы. Одним из самых заметных пробелов была явная слабость механизма синхронизации процессов, основанного фактически лишь на сигналах и на функции wait. На практике в большинстве реализаций UNIX вводились дополнительные, более удобные средства межпроцессного взаимодействия, однако возникала проблема несовместимости таких средств для разных версий UNIX. Разнобой был пресечен в начале 90-х годов с выработкой стандарта POSIX, объединившего все лучшее, что к тому времени было предложено в разных версиях UNIX.

К средствам, которые, согласно POSIX, должна поддерживать любая современная реализация UNIX, относятся, в частности:

сигналы;

безымянные и именованные каналы;

очереди сообщений;

семафоры;

совместно используемые (разделяемые) области памяти.

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

4.6.6. Планирование процессов

4.6.6.1. Состояния процессов в UNIX

UNIX является многозадачной системой с вытесняющей приоритетной диспетчеризацией.

Диаграмма основных состояний процесса, показанная на рис. 4 _ 1, в случае UNIX может быть уточнена так, как показано на рис. 4 _ 2.

Рис. 4 _ 2

Поставим следующий вопрос: в каком состоянии находится процесс, когда он вызвал системную функцию и ядро системы выполняет эту функцию? При описании работы большинства ОС этот вопрос обходят молчанием. В UNIX дается четкий ответ: процесс продолжает работать, но он переходит в режим ядра и выполняет системный код. Системные подпрограммы, работающие в режиме ядра, могут использовать все ресурсы системы. При этом контекст процесса остается доступен, что позволяет при выполнении системной функции использовать память и другие ресурсы процесса.

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

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

Из сказанного можно сделать два важных вывода.

Процесс, работающий в режиме ядра, не может быть вытеснен ни по истечению кванта времени, ни при активизации более приоритетного процесса. Вытеснение процесса возможно только в момент возврата из режима ядра в режим задачи.

Переход в состояние сна всегда происходит из выполнения в режиме ядра. После пробуждения процесс возвращается в режим ядра через состояние готовности.

В диаграмме на рис. 4_2 не учтены еще некоторые состояния, в которых может находиться процесс в UNIX. К их числу относятся состояние старта (процесс только что создан, но еще не готов к выполнению), состояние зомби (см. п. 4.6.1) и состояние приостановки, в которое переходит процесс, получивший сигнал SIGSTOP (см. п. 4.6.4).

4.6.6.2. Приоритеты процессов

В большинстве версий UNIX используются уровни приоритета от 0 до 127. Будем для определенности считать, что 0 соответствует высшему приоритету, хотя в некоторых версиях дело обстоит наоборот.

Весь диапазон приоритетов разделяется на верхнюю часть (приоритеты режима ядра) и нижнюю часть (приоритеты режима задачи). Это деление показано на рис. 4 _ 3.

Рис. 4 _ 3

Текущий (динамический) приоритет процесса, работающего в режиме задачи, определяется суммой трех слагаемых: базового значения, относительного приоритета данного процесса и «штрафа» за интенсивное использование процессорного времени.

Базовое значение приоритета - это то значение, которое система по умолчанию присваивает новому процессу при его создании. Во многих версиях UNIX базовое значение равно высшему приоритету задачи + 20.

Относительный приоритет, который почему-то называется в UNIX «nice number» Использование слова «nice» - «приятный, любезный, благовоспитанный, изящный» в данном контексте обычно объясняют так: этот процесс настолько любезен, что уступает всем дорогу., присваивается процессу при его создании. По умолчанию система устанавливает для процесса нулевое значение относительного приоритета. Обычный пользователь может только увеличить это значение (т.е. понизить приоритет процесса), а привилегированный пользователь может и уменьшить вплоть до высшего приоритета задачи (для самых приоритетных процессов). При создании нового процесса он наследует относительный приоритет родителя.

Штраф за использование процессорного времени увеличивается для работающего процесса с каждым прерыванием от таймера. Вследствие этого, высокоприоритетный процесс не сможет монополизировать использование процессора и время от времени должен будет уступать квант времени низкоприоритетным процессам. Однако система «не злопамятна»: через каждую секунду происходит уменьшение накопленных процессами штрафов наполовину. Таким образом, процесс, отлученный от процессора, через некоторое время восстановит свой исходный приоритет.

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

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

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

4.6.7. Интерпретатор команд shell

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

Однако при изучении UNIX нельзя обойти стороной такую интересную и развитую часть системы, как интерпретатор команд, чаще называемый просто шелл (shell).

Шелл не является частью ядра UNIX, по своему статусу это обычная прикладная программа, выделяющаяся только своим назначением, которое заключается в выполнении команд пользователя, задаваемых либо в интерактивном (диалоговом) режиме, либо в виде командных файлов, называемых также шелл-скриптами. Существуют различные варианты шелла, которые, совпадая в основном, предлагают несколько разные дополнительные возможности.

Набор возможностей, предоставляемых любым интерпретатором команд UNIX, настолько широк, что может быть предметом изучения в отдельном курсе. Здесь будет дано только минимальное представление о принципах работы шелла. Более подходящей формой изучения шелла являются лабораторные работы.

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

Базовой конструкцией языка команд UNIX (языка shell) является простая команда. Она состоит из имени команды и, возможно, параметров, разделенных пробелами. Имя команды - это обычно имя исполняемого файла (либо двоичной программы, либо шелл-скрипта).

Большинство команд UNIX выводят результат своей работы в текстовом виде на стандартный вывод. Как и в MS-DOS, это фактически означает вывод в файл или устройство, чей хэндл равен 1. По умолчанию это означает, что результаты выводятся на управляющий терминал пользователя. Однако стандартный вывод легко может быть перенаправлен в файл или на устройство. Для этого в команде используются символы `>' и `>>'.

Многие команды используют также стандартный ввод (хэндл 0), который по умолчанию означает данные, вводимые с клавиатуры терминала. Признаком конца ввода служит комбинация Ctrl+D. Стандартный ввод также может быть перенаправлен для чтения данных из файла или с устройства (с помощью символа `<'), или даже непосредственно из текста команды.

Как правило, стандартный вывод команд UNIX имеет как можно более регулярную структуру. Например, команда просмотра каталога ls -l выдает в каждой строке информацию об одном файле, без общего заголовка и без итоговых данных. Очень часто вывод команды выглядит как таблица, столбцы которой разделены знаками табуляции. Это облегчает последующую обработку выведенных данных следующими командами. Из тех же соображений команды не выдают лишних сообщений типа «Команда успешно выполнена», хотя могут выдавать сообщения об ошибках.

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

Составная команда состоит из простых команд, соединенных в виде конвейера или списка.

Конвейер означает параллельное выполнение нескольких команд с передачей данных по мере их обработки от одной команды к следующей, как на заводском конвейере. Запись конвейера состоит из нескольких команд, разделенных знаками `|'. Для выполнения конвейера шелл запускает одновременно работающие процессы для каждой команды, при этом стандартный вывод каждой команды перенаправляется на стандартный ввод следующей. Фактически для такого перенаправления используется механизм программных каналов, описанный в п. 4.6.3. Перед запуском конвейера шелл создает необходимое количество каналов, а при запуске каждого процесса связывает его стандартные хэндлы 0 и 1 с соответствующими каналами, как показано на рис. 4 _ 4.

Рис. 4 _ 4

Список означает последовательное выполнение команд. Он состоит из нескольких команд, разделенных знаками `;', `&&' или `||'. Если две команды разделены знаком `;', то следующая команда запускается после завершения предыдущей. Если команды разделены знаком `&&', то следующая будет выполняться только в том случае, если код завершения предыдущей равен 0 (нормальное завершение). Напротив, знак `||' означает, что следующая команда будет выполняться только в том случае, если код завершения предыдущей не равен 0 (завершение с ошибкой).

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

Как при интерактивной работе, так и при выполнении скриптов могут определяться и использоваться переменные, имеющие строковые значения. Ряд переменных определяется системой, например, PATH содержит список каталогов, в которых шелл ищет команды, а HOME - «домашний» каталог текущего пользователя. Для получения значения переменной перед ее именем записывается символ `$'. В скриптах можно также использовать значения параметров, с которыми был вызван скрипт, от $1 до $9.

Шелл, как и любой язык программирования, содержит набор операторов управления порядком выполнения команд, таких как if, case, while, until, for, break и некоторые другие. Логические выражения, используемые в операторах управления, строятся на основе кодов завершения команд, при этом специальная команда test позволяет проверить разнообразные условия, такие, как существование и тип указанного файла, равенство или неравенство строковых и числовых выражений и т.п.

Скорость выполнения шелл-скриптов во много раз меньше, чем скорость компилированной программы на C или на другом языке, однако шелл позволяет резко упростить решение многих практических задач, связанных с управлением операционной системой, обработкой текстовых файлов и т.п. Это достигается за счет того, что шелл-скрипты позволяют использовать «крупные блоки»: составлять новые программы путем изощренного комбинирования уже имеющихся программ-утилит, набор которых в любой современной версии UNIX весьма обширен.

5. Управление памятью

5.1. Основные задачи управления памятью

Основная память (она же ОЗУ) является важнейшим ресурсом, эффективное использование которого решающим образом влияет на общую производительность системы.

Для однозадачных ОС управление памятью не является серьезной проблемой, поскольку вся память, не занятая системой под собственные нужды, может быть отдана в распоряжение единственного пользовательского процесса. Процедуры управления памятью решают следующие задачи:

выделение памяти для процесса пользователя при его запуске и освобождение этой памяти при завершении процесса;

обеспечение настройки запускаемой программы на выделенные адреса памяти;

управление выделенными областями памяти по запросам программы пользователя (например, освобождение части памяти перед запуском порожденного процесса).

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

К основным задачам, которые должна решать подсистема управления памятью многозадачной ОС, добавляются следующие.

предоставление процессам возможностей получения и освобождения дополнительных областей памяти в ходе работы;

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

изоляция памяти процессов, исключающая случайное или намеренное несанкционированное обращение одного процесса к областям памяти, занимаемым другим процессом;

предоставление процессам возможности обмена данными через общие области памяти.

5.2. Виртуальные и физические адреса

Понятие «адрес памяти» может рассматриваться с двух точек зрения. С одной стороны, при написании любой программы ее автор либо явно указывает, по каким адресам должны размещаться переменные и команды (так бывает при программировании на языке ассемблера), либо присвоение конкретных адресов доверяется системе программирования. Те адреса памяти, которые записаны в программе, принято называть виртуальными адресами.

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

В ЭВМ первого поколения не делалось различия между виртуальными и физическими адресами: в программе требовалось указывать физические адреса. Это означало, что такая программа могла правильно работать, только если сама программа и все ее данные при каждом запуске (и на любом компьютере) должны были размещаться по одним и тем же физическим адресам. Такой подход стал крайне неудобным, как только была поставлена задача передать распределение памяти под управление ОС.

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


Подобные документы

  • Проектирование ОС Windows 2000, ее архитектура. Процессы и потоки. Уровни запросов на прерывания. Менеджер объектов. Распределение виртуальной памяти. Трансляция виртуальных адресов в физические. Локальный вызов процедуры. Структура сообщения LPC.

    презентация [1,5 M], добавлен 24.01.2014

  • Сущность и принцип работы операционной системы, правила и преимущества ее использования. Возможности различных операционных систем, их сильные и слабые стороны. Сравнительная характеристика систем Unix и Windows NT, их потенциал и выполняемые задачи.

    реферат [10,5 K], добавлен 09.10.2009

  • Три группы компонентов в составе современной операционной системы: ядро (планировщик и драйверы устройств), системные библиотеки, оболочка с утилитами. Типы архитектур ядер операционных систем: монолитное, модульное, гибридное, микро-, экзо-, наноядро.

    курсовая работа [22,1 K], добавлен 27.05.2014

  • Важность операционной системы для мобильных устройств. Популярность операционных систем. Доля LINUX на рынке операционных систем. История OS Symbian, BlackBerry OS, Palm OS. Отличия смартфона от обычного мобильного телефона. Учет ограничений по памяти.

    презентация [477,3 K], добавлен 01.12.2015

  • Назначение и функции операционных систем компьютера. Аппаратные и программные ресурсы ЭВМ. Пакетные ОС. Системы с разделением времени: Multics, Unix. Многозадачные ОС для ПК с графическим интерфейсом: Windows, Linux, Macintosh. ОС для мобильных устройств.

    курсовая работа [53,4 K], добавлен 05.12.2014

  • Характеристика, функции, типы, виды и состав операционных систем. Первая коммерческая система unix system. Операционные системы, основанные на графическом интерфейсе, пи–система, семейство unix. История и основные предпосылки появления ОС Windows.

    курсовая работа [66,9 K], добавлен 18.01.2011

  • Основные классификации операционных систем. Операционные системы семейства OS/2, UNIX, Linux и Windows. Разграничение прав доступа и многопользовательский режим работы. Пользовательский интерфейс и сетевые операции. Управление оперативной памятью.

    реферат [22,8 K], добавлен 11.05.2011

  • Разграничение прав пользователя в операционной системе. Предварительная настройка операционной системы с последующей установкой драйверов для периферийных устройств и системных комплектующих. Классификация операционных систем и периферийных устройств.

    реферат [2,1 M], добавлен 26.10.2022

  • Основные понятия об операционных системах. Виды современных операционных систем. История развития операционных систем семейства Windows. Характеристики операционных систем семейства Windows. Новые функциональные возможности операционной системы Windows 7.

    курсовая работа [60,1 K], добавлен 18.02.2012

  • Эволюция и классификация ОС. Сетевые операционные системы. Управление памятью. Современные концепции и технологии проектирования операционных систем. Семейство операционных систем UNIX. Сетевые продукты фирмы Novell. Сетевые ОС компании Microsoft.

    творческая работа [286,2 K], добавлен 07.11.2007

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.