close

Вход

Забыли?

вход по аккаунту

?

KaluzniiZayc

код для вставкиСкачать
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное автономное образовательное учреждение
высшего профессионального образования
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
АЭРОКОСМИЧЕСКОГО ПРИБОРОСТРОЕНИЯ
В. П. Калюжный, К. В. Зац
ОПЕРАЦИОННЫЕ СИСТЕМЫ
Учебное пособие
Санкт-Петербург
2012
УДК 004.4(075.8)
ББК 32.973
К17
Рецензенты:
кандидат технических наук кафедры моделирования
вычислительных и электронных систем ГУАП В. С. Павлов;
кандидат технических наук кафедры прикладной математики
АНО ВПО Международный банковский институт Л. А. Прусова
Утверждено
редакционно-издательским советом университета
в качестве учебного пособия
Калюжный, В. П.
К17 Операционные системы: учеб. пособие / В. П. Калюжный,
К. В. Зац. – СПб.: ГУАП, 2012. – 146 с.: ил.
ISBN 978-5-8088-0754-9
В пособии изложены основы организации универсальных ОС,
в том числе UNIX-подобных. Особое внимание уделено примерам,
заданиям и вопросам, что делает возможным его использование
в качестве методической основы для лабораторного практикума
по дисциплине. Предназначено для студентов направления 230200
(Информационные системы) по специальностям: 230201 (Информационные системы в технологии и бизнесе); 230203 (Информационные системы в дизайне); 230204 (Информационные системы в медиаиндустрии).
УДК 004.4(075.8)
ББК 32.973
ISBN 978-5-8088-0754-9
© Санкт-Петербургский государственный
университет аэрокосмического
приборостроения (ГУАП), 2012
© В. П. Калюжный, К. В. Зац, 2012
СПИСОК СОКРАЩЕНИЙ
ОЗУ – оперативное запоминающее устройство
ОС – операционная система
ПК – персональный компьютер
ЦП – центральный процессор
API – application programming interface
CPU – central processing unit
DMA – direct memory access
DNS – domain name system
FTP – file transfer protocol
GNU – библиотека языка С
HDD – hard (magnetic) disk drive
NFS – network file system
PID – process identifier
POSIX – portable operating system interface for unix
RAM – random access memory
rlogin – remote LOGIN
SNMP – simple network management protocol
TCP/IP – transmission control protocol/internet protocol
Telnet – протокол удаленной эмуляции терминала
TSL – test and set lock
3
1. ЯДРО И МОДУЛИ ОПЕРАЦИОННОЙ СИСТЕМЫ
Некоторые операционные системы ( ОС ) выполнены так, что
ядро содержит лишь один модуль, в этом случае добавление или
изменение его свойств осуществить не просто. Другие ОС могут содержать несколько модулей, и их называют микроядерными (например, Windows 2000).
1.1. Функции ядра
В состав ядра входят функции, выполняющие внутрисистемные
задачи организации вычислительных процессов: переключение
контекстов, загрузка-выгрузка страниц, обработка прерываний.
Эти функции недопустимы для приложений. Другой класс функций ядра служит для поддержки приложений, создавая для них
прикладную программную среду. Приложения могут обращаться
к ядру с запросами, т.е. системными вызовами для выполнения тех
или иных задач, например чтение – открытие файлов, вывод графической информации на дисплей и тому подобное. Функции ядра,
которые могут вызываться приложениями, образуют интерфейс
прикладного программирования. Функции, выполняемые модулями ядра, являются наиболее часто используемыми функциями ОС,
поэтому скорость их выполнения определяет производительность
всей системы в целом. Для обеспечения высокой скорости работы
ОС все модули ядра (или их большая часть) постоянно находятся в
оперативной памяти, т.е. являются резидентными. Ядро является
движущей силой всего вычислительного процесса, поэтому особенно важно обеспечить его надежность (защищенность). Часто ядро
оформлено в виде модуля специального формата, отличающегося
от формата пользовательских приложений. Современные ОС состоят не только из программ, а используют также аппаратные средства самого процессора.
1.2. Вспомогательные модули
Утилиты – это программы, решающие отдельные задачи управления и сопровождения компьютерной системы, такие как программы сжатия дисков, архивации данных и тому подобное. Они
бывают следующих видов:
4
системные обрабатывающие программы: текстовые и графические редакторы, компиляторы, компоновщики;
программы, предоставляющие пользователю дополнительные услуги: специальные программы пользовательского интерфейса, игры;
библиотеки процедур различного назначения, упрощающие
разработку приложений.
Как и обычные приложения, для выполнения своих задач утилиты и обрабатывающие программы, а также библиотеки ОС обращаются к функциям ядра посредством системных вызовов. Разделение
ОС на ядро и вспомогательные модули обеспечивает легкую расширяемость ОС. Например, чтобы добавить новую высокоуровневую
функцию, достаточно разработать новый модуль, при этом не требуется модифицировать ответственные функции, образующие ядро системы. Внесение изменений в функции ядра может оказаться гораздо сложнее. В некоторых случаях такое изменение может потребовать его полной перекомпиляции. Модули ОС, которые оформлены
в виде утилит, системных обработывающих программ и библиотек,
обычно загружаются в оперативную память только для выполнения
своих функций, иначе говоря, являются транзитными. Постоянно в
оперативной памяти находятся только самые необходимые коды ОС,
относящиеся к ее ядру. Такая организация экономит оперативную
память компьютера. Важным свойством архитектуры ОС, построенной на ядре, является возможность защиты кодов и данных ОС за
счет использования ядра в привилегированном режиме.
1.3. Ядро в привилегированном режиме
Для надежного выполнения кода вычислительного процесса ОС
должна иметь по отношению к приложениям привилегии, иначе некорректно работающее приложение может разрушить часть ее кода.
Поэтому ядро ОС должно обладать особыми полномочиями по отношению к приложениям, это нужно так же для того, чтобы играть
роль "арбитра" в споре приложений за ресурсы системы. Ни одно из
приложений не должно иметь возможность без ведома ОС получить
дополнительную область памяти, занимать процессор дольше разрешенного времени, управлять совместно используемыми внешними
устройствами. Такие привилегии невозможно обеспечить без специальных средств аппаратной поддержки. Аппаратура должна поддерживать как минимум два режима работы: пользовательский режим
и привилегированный режим (режим ядра). Приложение ставится
5
в подчиненное положение за счет запрета выполнения в привилегированном режиме некоторых критических команд, связанных с
переключением процессора с задачи на задачу, управлением устройствами ввода-вывода, доступа к механизмам распределения и защиты памяти. Выполнение некоторых команд в пользовательском
режиме запрещается безусловно (например, команда перехода из
пользовательского режима в привилегированный и обратно). Тогда как другие команды могут быть запрещены для выполнения в
определенных условиях. Например, команды ввода-вывода могут
быть запрещены при доступе к контроллеру жесткого диска, который хранит данные, общие для ОС и приложений Условие выполнения критической команды находится под полным контролем ОС.
Контроль обеспечивается за счет запрета выполнения определенных
команд в пользовательском режиме. Аналогичным способом обеспечиваются привелегии ОС при доступе к памяти. Например, выполнение команды доступа к памяти для приложения разрешается,
если команда обращается к области, отведенной для данного приложения; и запрещается, если такое обращение происходит в области,
где расположены коды самой ОС. Полный контроль ОС над доступом к памяти достигается за счет того, что команды конфигурирования механизма защиты памяти (например, команды, управляющие
указателем таблицы дескрипторов памяти) разрешается выполнять
только в привилегированном режиме. Важно, что ОС ограничивает
обращение не только к своим областям, но и к областям, используемым другими приложениями. В этом случае говорят, что каждая
программа выполняется в своем адресном пространстве. Это свойство позволяет локализовать некорректно работающее приложение
и обеспечить таким образом стабильность работы всей ОС.
Обычно ядро состоит из следующих слоев (рис. 1).
Средства аппаратной поддержки ОС. К операционной системе
относят не все аппаратные средства компьютера, а только средства
ее аппаратной поддержки, которые прямо участвуют в организации вычислительного процесса:
средства поддержки привилегированного режима;
система прерываний;
средства переключения контекстов процессов;
средства защиты областей памяти.
Машинно-зависимые компоненты ОС. Это микрокод, способный взаимодействовать с аппаратурой процессора и со слоем базовых механизмов ядра. Этот слой экранирует ядро от особенностей
аппаратуры. Слоистая структура ядра делает всю ОС расширяемой
6
Интерфейс системных вызовов
Менеджеры ресурсов
Базовые механизмы
Машинно-зависимые
компоненты
Аппаратная поддержка
Аппаратура
Рис. 1
(открытой), следовательно, изменение какого-либо из слоев ядра
не затрагивает другие его части.
Слой базовых механизмов ядра выполняет основные функции
ядра. К ним относятся:
переключение контекстов процессора;
загрузка-выгрузка страниц памяти;
низкоуровневые операции ввода-вывода.
Этот слой также ведет основные информационные структуры
ядра. В нем не принимаются никакие решения относительно действий ядра в какой-либо ситуации, а выполняются лишь те директивы, которые приняты на более высоких уровнях.
Менеджеры ресурсов (диспетчеры) состоят из модулей, решающих задачи по управлению основными ресурсами ОС.
Основные ресурсы:
процессор, а точнее время, которое выделяется на определенные
задачи;
память;
файловая система;
устройства ввода-вывода.
Обычно в данном слое работают диспетчеры процессов, основной
задачей которых является учет имеющихся ресурсов, освобождение
7
и их распределение между потребителями (процессами). Каждый из
менеджеров ведет учет ресурсов строго определенного типа и планирует их распределение в соответствии с запросами приложений. Например, менеджер виртуальной памяти управляет перемещениями
страниц из оперативной памяти на диск и обратно. Этот менеджер
должен отслеживать интенсивность обращения к страницам, время
пребывания в памяти, состояния процессов, использующих данные
и многие другие параметры, на основании которых он время от времени принимает решения о том, какие страницы выгрузить, а какие
загрузить. Все менеджеры используют табличные структуры. Для
исполнения принятых решений менеджер обращается к нижестоящему слою базовых механизмов с запросами о загрузке-выгрузке
конкретной страницы. Внутри слоя существуют тесные взаимные
связи, отображающие тот факт, что для выполнения процессу нужен
доступ единовременно к нескольким ресурсам, а именно: процессору, области памяти, к определенному файлу или устройству вводавывода. Например, при создании процесса менеджер процессов обращается к менеджеру памяти, который должен выделить процессу
определенную область памяти для его кодов и данных.
Интерфейс системных вызовов является самым верхним слоем ядра, он взаимодействует непосредственно с приложениями,
системными утилитами и образует прикладной программный интерфейс (API). Функции API (обслуживание системных вызовов)
предоставляют доступ к ресурсам системы в удобной и компактной форме. Для осуществления таких действий системные вызовы
обычно обращаются за помощью к функциям менеджеров ресурсов, причем для выполнения одного системного вызова может потребоваться несколько таких обращений.
1.4. Процесс и модель процесса
Модель процесса базируется на двух независимых концепциях:
группирование ресурсов и выполнение программы. Процесс – это
абстрактное понятие, описывающее работу программы (это не просто программа). В этой модели все функционирующее на компьютере программное обеспечение (иногда включая ОС) организовано в
виде набора последовательных процессов. Процесс – выполняемая
программа, включающая текущее значение счетчика команд, регистров и переменных (состояние). С позиции данной абстрактной
модели у каждого процесса есть свой собственный центральный
8
процессор. Процесс – это активность определенного рода. У процесса есть программа, входные, выходные данные, а также состояние
(стек, аккумулятор, состояние регистров). Один процессор может
переключаться между различными процессами, используя алгоритм планирования для определения моментов переключения от
одного процесса к другому.
1.5. Создание процессов
В универсальных ОС создание и прерывание процессов происходит по мере необходимости. Есть четыре основных события, приводящие к созданию процессов:
инициализация системы;
выполнение изданного работающим процессом системного вызова на создание нового процесса;
запрос пользователя на создание процесса;
инициирование пакетного задания.
Обычно при загрузке ОС создаются множество процессов. Некоторые из них являются высокоприоритетными, т.е. обеспечивающими
взаимодействие с пользователем и выполняющие заданную работу.
Остальные процессы являются фоновыми, они не связаны с конкретными пользователями, но выполняют особые функции. Например,
фоновый процесс может быть предназначен для обработки приходящей почты, активизируясь только по мере появления писем. Такие
фоновые процессы, связанные с почтой, новостями, web-страницами,
выводом на печать, называются демонами. Процессы могут создаваться не только в процессе загрузки системы, но и позже. Например, новый процесс может быть создан по просьбе выполняющегося в
данный момет процесса. Во всех перечисленных случаях, новый процесс формируется одинаково, а именно: текущий процесс выполняет
системный запрос на создание нового процесса. В роли текущего процесса может выступать процесс, запущенный пользователем, системный процесс, инициируемый клавиатурой, мышью и тому подобное.
В любом случае этот процесс всего лишь выполняет системный запрос, а также прямо или косвенно содержит информацию о программе, которую нужно запустить в этом процессе. В Unix-подобных ОС
существует только один системный запрос, направленный на создание нового процесса – это fork. Этот запрос создает дубликат выполняемого процесса. После выполнения fork двум процессам (родительскому и дочернему) соответствуют одинаковые образы памяти,
9
строки окружения и одни и те же открытые файлы. Далее, обычно
дочерний процесс выполняет системный вызов для изменения своего
образа памяти и запуска новой программы.
1.6. Завершение процесса
Рано или поздно процесс завершает свое существование. Чаще
всего это происходит благодаря одному из следующих событий:
обычный выход;
выход по ошибке;
выход по неисправимой ошибке;
уничтожение другим процессом.
В первом случае процесс выполняет системный запрос, чтобы
сообщить ОС о том, что он выполнил свою работу.
Второй причиной может стать неустранимая ошибка. Например, пользователь вводит команду компиляции файла, которого не
существует. В этом случае компилятор просто завершает свою работу. Интерактивные процессы, рассчитанные на работу с экраном
(графический режим) обычно не завершают работу при получении
неправильных параметров, а просят пользователя ввести правильные параметры, происходит диалог.
В третьем случае ошибка вызвана самим процессом. Чаще всего
она связана с ошибкой в программе. Например, выполнение недопустимой команды, обращение к несуществующей области памяти
и тому подобное. В некоторых ОС, например Linux, процесс может
информировать ОС о том, что он сам обрабатывает некоторые ошибки. В этом случае процессу посылается сигнал и он сам должен обработать ошибку.
В четвертом случае выполняется системный запрос на уничтожение определенного процесса (kill – Unix, Terminate Process –
Windows). В обоих случаях процесс, который уничтожает другой
процесс, должен обладать соответствующими полномочиями. В некоторых ОС при завершении процесса преднамеренно или нет – все
процессы, созданные этим процессом завершаются.
1.7. Иерархия процессов
В некоторых ОС, в частности Linux, родительские и дочерние
процессы остаются связанными. Дочерний процесс также может в
10
свою очередь создавать процессы, формируя иерархию процессов.
В Unix все эти дальнейшие потомки образуют группу процессов.
Сигнал, посылаемый пользователем с клавиатуры, доставляется
всем членам группы, взаимодействующим с клавиатурой в данный
момент. Обычно это все активные процессы, созданные в текущем
окне. Каждый из процессов может перехватить сигнал, игнорировать его или выполнить другое действие, предусмотренное по
умолчанию. В образе загрузки Unix – подобных ОС присутствует
специальный процесс – init. При запуске он считывает файл, в котором имеется информация о количестве терминалов. Терминал
(консоль) – это устройство ввода и вывода. Затем процесс развивается таким образом, чтобы каждому терминалу соответствовал
один процесс. Если пароль правильный – процесс входа в систему запускает оболочку для обработки команд пользователя, которые в свою очередь могут запускать процессы. Таким образом, все
процессы в системе принадлежат одному дереву, начинающемуся
с процесса init. В Windows не существует понятия иерархии процессов, все они равноправны. Единственным инструментом, напоминающим подобие иерархии в Windows, является возможность
создания процесса со специальным маркером, который называется
дискриптером. Дискриптер (маркер) позволяет контролировать дочерний процесс. Но маркер можно передать другому процессу, нарушая иерархию. В Unix это невозможно.
1.8. Состояния процессов
Несмотря на то, что процесс является самостоятельной сущностью со своим счетчиком команд и внутренним состоянием, существует необходимость взаимодействия с другими процессами. Например, выходные данные одного процесса могут быть входными
данными для другого. В зависимости от относительных скоростей
процессов, может получиться так, что процесс уже готов к запуску,
но входных данных для него еще нет. В этом случае процесс блокируется до поступления входных данных. Также возможна ситуация, когда процесс, готовый и способный работать, останавливается, поскольку ОС решила предоставить процессор другому процессу. Эти две ситуации являются принципиально разными: в первом
случае приостановка выполнения является внутренней проблемой
(например, невозможно обработать командную строку, пока она
не введена); во втором случае проблема является технической (не11
хватка процессорного времени). Диаграмма состояний процессов
показана на рис. 2.
Действие – это работающий процесс, в этот конкретный момент
использующий процессор. Готовый к работе – процесс временно
приостановлен, чтобы позволить выполниться другому процессу.
Блокировка или заблокирование процесса – процесс не может быть
запущен прежде, чем произойдет некоторое внешнее событие.
1 переход – процесс блокируется, ожидая входных данных.
2 переход – планировщик выбирает другой процесс.
3 переход – планировщик выбирает этот процесс.
4 переход – доступны входные данные.
Первые два состояния похожи. В обоих случаях процесс может
быть запущен. Только во втором случае процессор недоступен. Третье состояние отличается тем, что запустить процесс невозможно,
независимо от того загружен процессор или нет.
Переход 1 происходит тогда, когда процессор обнаруживает, что
продолжение работы невозможно. В некоторых ОС процесс должен
выполнить системный запрос, например, block или pause, чтобы
оказаться в заблокированном состоянии. В других системах (Unix)
процесс автоматически блокируется, если он не в состоянии считать
из специального файла или канала данные. Переход 2 и 3 вызываются частью ОС, называемой планировщиком процессов. Переход
2 происходит, если планировщик решил предоставить процессор
другому процессу. Переход 3 происходит, когда все остальные процессы уже исчерпали свое процессорное время и процессор снова
передается первому процессу. Переход 4 происходит с появлением
внешнего события, ожидавшегося процессом, например, прибытие
Действие
2
1
3
Блокировка
Готовность
4
Рис. 2
12
входных данных. Если в этот момент не запущен никакой другой
процесс, то срабатывает переход 3 и процесс запускается. В противном случае процессу придется некоторое время находиться в состоянии готовности, пока не освободится процессор.
1.9. Переключение процессов
Для реализации процессов ОС ведет информационную структуру, которая называется таблицей процессов. В ней столько строк,
сколько процессов, и столько столбцов, сколько имеется параметров, описывающих данный процесс. Эти параметры можно разделить на три группы.
1 группа параметров относится к управлению процессами.
2 группа описывает используемую процессом память.
3 группа – параметры, которые описывают используемые файлы (ресурсы файловой системы).
Во время работы в оперативном запоминающем устройстве
(ОЗУ) содержатся коды ядра и коды процесса, который выполняется в данный момент. Если необходимо перейти к другому процессу – приходится загружать эти коды в оперативную память, но
это можно сделать только используя диск (обращение к диску). Но
диск для ОС является внешним устройством, а это значит, что без
прерывания здесь не обойтись. Однако прерывания могут потребоваться не обязательно тогда, когда необходимо переходить к другому процессу. Например, выполняющемуся процессу может понадобиться внешнее устройство, и он может попросить ОС предоставить
его ему и поэтому он посылает запрос. Другой случай: работающий
процесс может быть на время приостановлен, если при определенных условиях какое-либо внешнее устройство попросит ОС сделать
это. Если ОС требуется запустить новый процесс, то обращение к
диску как к внешнему устройству неизбежно, поэтому начинается
прерывание.
На начальной стадии этого процесса работают аппаратные средства процессора.
Аппаратно запоминается счетчик команд.
Запоминается слово состояния программы, а также содержимое
одного или нескольких базовых регистров.
Завершают свою работу аппаратные средства тем, что считывают вектор прерывания диска (это начальный адрес процедуры прерываний) и запускают эту программу.
13
С этого момента и почти до конца всего прерывания используются
программные средства. Сначала вызывается процедура на ассемблере, которая устанавливает временный стек и считывает основные параметры, которые являются столбцами в таблице процессов, а также
заполняет соответствующую строку для данного процесса, который
был остановлен. Такие действия, как установка стека, модификация
регистров, и их считывание невозможно выразить на языке высокого уровня, поэтому все это делается на ассемблере. После того, как
строка в таблице модифицирована – программа на ассемблере передает управление программе прерываний, написанной на языке высокого уровня. Далее процедура на языке высокого уровня главным
образом буферизует данные и подготавливает их для обмена с внешним устройством. После завершения этой части программа на языке
высокого уровня вызывает программу на языке ассемблера, но прежде планировщик, используя данные из таблицы процессов, может
выбрать и подготовить для запуска другой процесс.
Содержимое таблицы процессов (ее столбцы) можно сгруппировать в трех частях.
1 часть – управление процессом.
Регистры (содержимое).
Счетчик команд.
Слово состояния программы.
Указатель стека.
Состояние процесса.
Приоритет.
Параметр планирования.
Идентификатор процесса (его PID).
Родительский процесс (PPID).
Принадлежность к группе процессов (если она есть).
Сигналы.
Время начала процесса.
Используемое процессорное время.
Процессорное время дочернего процесса.
Время следующего аварийного сигнала.
2 часть – управление памятью.
Указатель на текстовый сегмент.
Указатель на сегмент данных.
Указатель на сегмент стека.
3 часть – управление файлами.
Корневой каталог.
Рабочий каталог.
14
Дескрипторы файла (описатели).
Идентификатор файла (номер).
Идентификатор группы.
После того как планировщик (менеджер процессов) закончил
свою работу, управление передается программе на языке ассемблера, которая загружает регистры и карту памяти для вновь запускаемого процесса. Если ОС хотела запустить новый процесс, загружается карта памяти и регистры для нового процесса (если ОС имела
в виду новый процесс), или старого процесса (если ОС имела в виду
старый процесс).
Динамика переключения между процессами опирается на алгоритм прерываний и включает в себя следующие действия:
аппаратное обеспечение сохраняет в стеке счетчик команд и т.п.;
аппаратное обеспечение загружает новый счетчик команд из таблицы векторов прерываний;
процедура на ассемблере сохраняет регистры;
процедура на ассемблере устанавливает новый стек;
запускается программа обработки прерываний на языке высокого уровня (она считывает и буферизует входные и выходные данные);
планировщик (программа на языке высокого уровня) выбирает
следующий процесс (когда происходит переключение между процессами);
программа на языке высокого уровня передает управление процедуре на ассемблере;
процедура на ассемблере запускает новый процесс.
1.10. Потоки и модель потока
В универсальных ОС каждому процессу соответствует адресное
пространство и одиночный управляющий поток. Фактически это
и определяет процесс. Тем не менее, часто встречаются ситуации,
в которых предпочтительно иметь несколько квазипараллельных
управляющих потоков в одном адресном пространстве, как если
бы они были различными процессами (но разделяющими одно
адресное пространство). Модель процесса базируется на двух независимых концепциях: группирование ресурсов и выполнение
программы. Иногда их необходимо разделять и здесь появляется
понятие потока. У потока общее адресное пространство. У потока
есть счетчик команд, отслеживающий выполнение действий; есть
регистры, в которых хранятся текущие переменные; стек, содер15
жащий протокол выполнения команд, где на каждую процедуру,
вызванную, но еще не вернувшуюся, отведена часть стека. Хотя поток должен выполняться внутри процесса, следует разделять эти
понятия. Процессы используются для группирования ресурсов, а
потоки являются объектами, поочередно выполняемыми на центральном процессоре. Концепция потоков добавляет к модели процесса возможность одновременного выполнения в одном и том же
процессе нескольких независимых программ. Несколько потоков,
работающих параллельно в одном процессе, аналогичны нескольким процессам, идущим квазипараллельно на одном компьютере.
В первом случае потоки разделяют адресное пространство, открытые файлы и другие ресурсы. Во втором случае процессы совместно
пользуются физической памятью, дисками, принтерами и другими
ресурсами.
1.11. Межпроцессорное взаимодействие.
Состояние состязания
Межпроцессорное взаимодействие может приводить к состоянию
состязания. Процессам часто бывает необходимо взаимодействовать
между собой. Например, выходные данные одного процесса могут
потребоваться другому. Поэтому необходимо правильно организовать взаимодействие между ними, не прибегая к прерываниям.
Проблему можно разделить на три части:
передача данных от одного процесса к другому;
два процесса могут столкнуться между собой в критических ситуациях (например, каждому из них в определенный момент может потребоваться последний мегабайт оперативной памяти);
согласование действий между процессами: если например, процесс А должен поставить данные, а процесс В выводить их на печать, то В должен ждать и не начинать печатать, пока не поступят
данные от процесса А.
Два из перечисленных пунктов относятся и к потокам. Первая
проблема (передача данных) в случае потоков проблемой не является, поскольку у потоков общее адресное пространство. Процессы,
работающие совместно, могут сообща использовать общее хранилище данных. Каждый из них может считывать оттуда данные и
записывать туда информацию. Это так же может быть участок в основной памяти или файл общего доступа.
Рассмотрим пример сетевого принтера (рис. 3).
16
Директорий спулера
Процесс А
4
Progr a
5
Progr b
6
Progr c
Out = 4
7
…
Процесс В
In = 7
Рис. 3
Если процессу требуется вывести на печать файл, он перемещает
имя файла в специальный каталог – директорий спулера. Другой
процесс – демон печати периодически проверяет наличие файлов,
которые необходимо напечатать, затем печатает файл и удаляет его
имя из каталога. Каталог спулера состоит из сегментов, в каждом
из которых может храниться имя файла. Также есть две совместно
используемые переменные (out – которая указывает на следующий
файл для печати; in – обозначает следующий свободный сегмент).
Две переменные можно хранить в одном файле (состоящем из двух
слов), который доступен всем процессам. Пусть, например, в данный момент сегменты 0–3 пусты (эти файлы уже напечатаны), а
сегменты 4–6 заняты (эти файлы ждут своей очереди на печать).
Более или менее одновременно процессы А и В решают поставить
свои файлы для печати в очередь. Процесс А считывает значение
переменной in и сохраняет ее в собственную локальную переменную free slot. После этого происходит прерывание по таймеру и
процессор переключается на процесс В. Этот процесс в свое время тоже считывает значение переменной in и сохраняет ее в собственную локальную переменную в следующий свободный слот.
В данный момент оба процесса считают, что следующий свободный
сегмент – 7. Процесс В сохраняет в каталоге спулера имя файла и
заменяет переменную in на 8. Затем начинает заниматься своими
задачами. Наконец, управление опять переходит к процессу А и он
продолжает с того момента, на котором остановился. Он обращается к переменной free slot и считывает ее значение и записывает
в сегмент 7 имя файла (удаляя имя файла, которое туда занес процесс В). Затем он изменяет значение переменной на 8. После всех
этих действий структура каталога не нарушена, но файл процесса
B не будет напечатан. Cитуация, в которой два или более процесса считывают или записывают данные одновременно, и конечный
17
результат зависит от того, какой из них был первым, называется
состоянием состязания.
1.12. Критические области
Основным решением проблем, связанных с совместным использованием памяти, файлов и тому подобного, является запрет одновременной записи и чтения разделяемых (общих) данных более чем
одним процессом. Иными словами, необходимо взаимное исключение. Это означает, что в момент, когда один процесс использует разделяемые данные, другому процессу это делать запрещено. В рассмотренном примере эта ситуация возникла из-за того, что процесс
В начал работу с одной из совместно используемых переменных до
того, как процесс А ее закончил. Часть программы, в которой есть
обращение к совместно используемым данным, называется критической областью. Если удастся избежать одновременного нахождения процессов в критической области, можно избежать состязаний. Несмотря на то, что это требование исключает состязания, оно
не достаточно для правильной совместной работы квазипараллельных процессов и эффективного использования данных. Для этого
необходимо выполнение четырех условий:
два процесса не должны одновременно находиться в критических областях;
в программе не должно быть предложений о скорости или количестве процессов;
процесс, находящийся в критической области, не может или не
должен блокировать другой процесс;
недопустима ситуация, в которой процесс неопределенно долго
ждет попадания в критическую область.
1.13. Запрещение прерываний
и переменные блокировки
Рассмотрим возможность аппаратного решения этой проблемы.
Самое простое решение состоит в запрещении всех прерываний при
входе процесса в критическую область и разрешение прерываний при
выходе из критической области. Если прерывания запрещены, то запрещены и прерывания по таймеру. Поскольку процессор переключается с одного процесса на другой только по прерыванию, отключе18
ние прерываний исключает передачу процессора другому процессу.
Таким образом, запретив прерывания, процесс может сохранять совместно используемые данные, не опасаясь вмешательства другого
процесса, но все же неправильно давать пользовательскому процессу
возможность запрета прерываний. Если, например, процесс отключил все прерывания, и в результате какого-либо сбоя не включил их
обратно, ОС прекратит свое существование. Для ядра характерно запрещение прерываний для некоторых команд при работе с переменными и списками. Возникновение прерывания в момент, когда список готовых процессов находится в неопределенном состоянии, могло
бы привести ядро к краху. Вывод: инструмент запрещения прерывания необходим для ОС, но не для пользовательских процессов.
Рассмотрим возможность программного решения. Пусть имеется одна совместно используемая переменная блокировки, изначально равная нулю. Если процесс хочет попасть в критическую
область, он предварительно считывает значение переменной блокировки; если переменная равна нулю, то процесс изменяет ее на 1 и
входит в критическую область; если переменная равна единице, то
процесс ждет, пока ее значение не изменится на ноль. Таким образом, ноль означает, что ни одного процесса в критической области
нет, а единица наоборот, что есть процессы в критической области.
У этого метода те же проблемы, что и в примере с каталогом спулера, а именно: первый процесс считывает переменную блокировки,
обнаруживает, что она равна нулю, но прежде, чем он успевает изменить ее на единицу, управление может получить другой процесс,
изменяющий ее на единицу. Когда первый процесс снова получает
управление, он тоже заменяет переменную блокировки на единицу,
и два процесса одновременно оказываются в критической области.
Проблема не решается повторной проверкой значения переменной.
Второй процесс может получить управление как раз после того, как
первый процесс закончил вторую проверку, но еще не заменил значение переменной блокировки.
1.14. Алгоритм Петерсона и команда TSL
Датский математик Деккер был первым, кто разработал программное решение проблемы взаимного исключения. В 1981 г.
Петерсон разработал алгоритм, состоящий из двух процедур, написанных на языке С. Прежде чем обратиться к совместно используемым переменным, процесс вызывает процедуру enter region со
19
своим номером 0 или 1 в качестве параметра, поэтому процессу при
необходимости придется подождать, прежде чем войти в критическую область. После выхода из критической области процесс вызывает процедуру leave region, чтобы обозначить свой выход и тем
самым разрешить другому процессу вход в критическую область.
1.15. Примитивы
межпроцессорного взаимодействия
Оба решения корректны, но они обладают одним и тем же недостатком: использование активного ожидания. Они реализуют следующий алгоритм: перед входом в критическую область процесс
проверяет, можно ли это сделать. Если нельзя, то процесс входит
в цикл, ожидая возможности войти в критическую область. Этот
алгоритм не только бесцельно тратит процессорное время, но, кроме того, может возникнуть ситуация, которую называют проблемой инверсий приоритета. Возможность вхождения в критическую
область должен предвидеть программист. Примитивы блокируют
процессы в случае запрета на вход в критическую область. Одной из
простейших является пара примитивов sleep и wake up. Примитив
sleep – это системный запрос, в результате которого вызывающий
процесс блокируется, пока его не разбудит другой процесс. У системного запроса wake up один параметр – это процесс, который
следует запустить. Также возможно наличие одного параметра у
обоих запросов: адрес ячейки памяти, используемой для согласования запросов ожидания и запуска.
1.16. Проблема производителя и потребителя
Эта проблема называется проблемой ограниченного буфера. Если
один процесс (производитель) помещает данные в буфер, а другой
процесс (потребитель), забирает их оттуда, может возникнуть проблема. Трудности могут возникнуть в момент, когда производитель
хочет поместить в буфер очередную порцию данных, но обнаруживает, что буфер полон. Для производителя решением является
ожидание, пока потребитель полностью или частично не освободит
буфер. Аналогично, если потребитель хочет забрать данные из буфера, а он пуст, потребитель уходит в состояние ожидания до тех
пор, пока производитель что-нибудь не положит туда и не разбу20
дит потребителя. Это решение является простым, однако оно приводит к состоянию состязания, когда оба процесса могут оказаться
в состоянии ожидания. Решением может быть использование бита
ожидания активации. Если сигнал активации послан процессу, не
находящемуся в состоянии ожидания, этот бит устанавливается.
Позднее, когда процесс пытается уйти в состояние ожидания, бит
активации сбрасывается, но процесс остается активным. Этот бит
играет роль "копилки" сигналов активации.
1.17. Семафоры и решение проблемы
производителя и потребителя
В 1965 г. Дейкстра предложил использовать целую переменную
для подсчета сигналов запуска, сохраненных на будущее. Им был
предложен новый тип переменных – семафоры, значение которых
может быть нулем (в случае отсутствия сохраненных сигналов активации) или некоторым положительным числом, соответствующим количеству отложенных активирующих сигналов. Он предложил две операции: down и up. Операция down сравнивает значение
семафора с нулем. Если значение семафора >0, то операция down
уменьшает его, т.е. расходует один из отложенных сигналов активации, и возвращает управление. Если значение семафора равно
нулю, то процедура не возвращает управление процессу, а он сам
переводится в состояние ожидания. Все операции проверки значения семафора, его изменение или переводы процессов в состоянии
ожидания, выполняются как единое, неделимое, элементарное
действие. Тем самым гарантируется, что после начала операции
ни один процесс не получит доступа к семафору до окончания либо
блокировки операции. Операция up увеличивает значение семафора. Если с ним связаны один или несколько ожидающих процессов,
которые не могут завершить более раннюю операцию down, один
из них выбирается системой, например, случайным образом, и ему
разрешается завершить свою операцию down. Таким образом, после операции up, примененной к семафору, связанному с несколькими ожидающими процессами, значение семафора так и остается
равным нулю. Но за то число ожидающих процессов уменьшается
на единицу. Операции увеличения значения семафора и активации процесса также неделимы, т.е. ни один процесс не может быть
блокирован во время выполнения операции up также, как ни один
процесс не может быть блокирован во время выполнения операции.
21
Такую возможность дает аппаратно реализованная на кристалле
процессора команда TSL ( test and set lock ).
Проблему потерянных сигналов запуска можно решить с помощью семафоров. Стандартным решением является реализация
операций down и up в виде системных запросов с запретом ОС всех
прерываний на время проверки семафора, изменение его значения
и возможного перевода процесса в состояние ожидания. Поскольку для выполнения всех этих действий требуется лишь несколько
команд процессора, запрет прерываний на столь короткий промежуток времени не приносит вреда. Если используется несколько
процессов, каждый семафор необходимо защитить переменной
блокировки с использованием команды ТSL, чтобы гарантировать
обращение к семафору лишь одного процесса. Использование команды ТSL принципиально отличается от активного ожидания,
при котором производитель и потребитель ждут опорожнения или
пополнения буфера. Операции с семафором могут занимать лишь
несколько долей микросекунды, тогда как активное ожидание может затянуться на время, большее на несколько порядков.
1.18. Мьютексы
Иногда используется упрощенная версия семафора, называемая
мьютексом. Мьютекс не способен считать, он может лишь управлять
взаимным исключением доступа к совместно используемым ресурсам или кодам. Реализация мьютекса проста и эффективна, что делает их использование особенно удобным в случае потоков, действующих только в пространстве пользователя. Мьютекс – это переменная, которая может находиться в одном из двух состояний: блокированном и неблокированном. Поэтому для описания мьютекса нужен
всего один бит, хотя чаще используется целочисленная переменная,
у которой: 0 – неблокируемое состояние, а любое другое положительное целое число соответствует блокируемому состоянию. Значение
мьютекса устанавливается двумя процедурами. Если поток собирается войти в критическую область, он вызывает процедуру мьютекс
– lock. Если мьютекс не заблокирован (вход в критическую область
разрешен), запрос выполняется и вызывающий поток может попасть
в критическую область. Напротив, если мьютекс заблокирован, вызывающий поток блокируется до тех пор, пока другой поток, находящийся в критической области не выйдет из нее, вызвав процедуру
мьютекс – anlock. Если мьютекс блокирует несколько потоков, то из
22
них случайным образом выбирается один. Мьютексы легко реализуются в пространстве пользователя, если доступна команда TSL.
1.19. Функции ОС по управлению памятью
Под памятью будем понимать оперативную память компьютера (RAM или ОЗУ), в отличие от жесткого диска (storage). Оперативной памяти для сохранения информации требуется постоянное
электропитание.
Функции ОС по управлению памятью в мультипрограммной системе сводятся к выполнению следущих действий:
отслеживание свободной и занятой памяти;
выделение памяти процессам и освобождение памяти по мере их
завершения;
вытеснение кодов и данных процессов из оперативной памяти
на диск (полное или частичное), когда размера основной памяти
недостаточно для размещения в ней всех процессов, и возвращение
их в оперативную память, когда в ней освобождается место;
настройка адресов программы на конкретную область физической памяти.
Во время работы ОС ей часто приходится создавать служебные
информационные структуры, такие как описатели процессов и потоков; различные таблицы распределения ресурсов; буферы, используемые процессами для обмена данными; синхронизирующие объекты
и тому подобное. Все эти системные объекты требуют памяти. В некоторых ОС заранее (во время установки) резервируется некоторый
фиксированный объем памяти для системных нужд. Существует и
другой подход, при котором память для системных целей выделяется динамически. В таком случае различные подсистемы ОС при создании своих таблиц, объектов и структур, обращаются к подсистеме
памяти с запросами. Помимо первоначального выделения памяти
процессам при их создании ОС должна заниматься также динамическим распределением памяти, т.е. выполнять запросы приложений
на выделение им дополнительной памяти во время их выполнения.
1.20. Типы адресов
Для идентификации переменных и команд на разных этапах
жизненного цикла программы используются символьные имена
23
(метки), виртуальные адреса и физические адреса. Символьные
имена присваивает программист при написании программы на алгоритмическом языке или ассемблере. Виртуальные адреса (математические или логические) выбирает транслятор, переводящий
программу на машинный язык. Поскольку во время трансляции в
общем случае неизвестно в какое место оперативной памяти будет
загружена программа, то транслятор присваивает переменным или
командам виртуальные (условные) адреса, обычно считая по умолчанию, что начальным адресом программы будет нулевой адрес.
Физические адреса соответствуют номерам ячеек памяти, где в действительности расположены или будут расположены переменные и
команды. Совокупность виртуальных адресов процесса называется
его виртуальным адресным пространством. Диапазон возможных
адресов виртуального пространства у всех процессов является одним и тем же. Например, при использовании 32-разрядной шины
адреса диапазон виртуальных адресов определяется границами:
0000000016 ÷ FFFFFFFF16 , 232 примерно равно 4 Гб.
Тем не менее каждый процесс имеет собственное виртуальное
адресное пространство, т.е. транслятор присваивает виртуальные
адреса переменным и кодам каждой программы независимо. Совпадение адресов переменных и команд различных процессов не
приводят к конфликтам, так как в том случае, когда эти переменные одновременно присутствуют в памяти, ОС отображает их на
разные физические адреса.
Следует различать величину адресного пространства (физического и виртуального) и объем памяти, который в нем помещается.
Количество возможных сочетаний дает максимальное количество
ячеек адресуемой физической памяти (232). Между тем в ячейке находится не один байт информации, а слово, т.е. число кратное байту.
1.21. Образ процесса
и виртуальное адресное пространство
Содержимое назначенного процессу виртуального адресного
пространства, т.е. коды команд, исходные и промежуточные данные, а также результаты вычислений, представляют собой образ
процесса. Во время работы процесса постоянно выполняются переходы от прикладных кодов к кодам ОС, которые либо явно вызываются из прикладных процессов как системные функции; либо
вызываются как реакция на внешние события или исключитель24
Индивидуальные части виртуального адресного пространства
Paged
non
Paged
Общая часть виртуального адресного пространства
Рис. 4
ные ситуации, возникающие при некорректном поведении прикладных кодов. Для того чтобы упростить передачу управления от
прикладного кода к коду ОС, а также для легкого доступа модулей
ОС к прикладным данным (например, для вывода их на внешние
устройства) в большинстве ОС ее сегменты разделяют виртуальное
адресное пространство с прикладными сегментами активного процесса, т.е. сегменты ОС и сегменты активного процесса образуют
единое виртуальное адресное пространство. Обычно виртуальное
адресное пространство процесса делится на две непрерывные части:
системную и пользовательскую (рис. 4). В некоторых ОС, например
Windows NT, ОS/2, эти части делятся поровну и имеют одинаковый
размер по 2 Gb. Хотя соотношение может быть и другим, например
1 Гб для ОС, а остальное – пользователю. Часть виртуального адресного пространства каждого процесса, отводимая для сегментов
ОС, является идентичной для всех процессов. Поэтому при смене
активного процесса заменяется только вторая часть виртуального
адресного пространства, содержащая его индивидуальные сегменты. Как правило, это коды и данные прикладной программы.
Системная часть виртуальной памяти ОС любого типа включает область, подвергаемую страничному вытеснению, – Paged; и
область, на которую страничное вытеснение не распространяется, – Nonpaged. В невытесняемой области размещаются модули
ОС, требующие быстрой реакции и/или постоянного присутствия в
памяти. Например, диспетчер потоков или код, который управляет заменой страниц памяти (загрузка-выгрузка страниц). Остальные модули ОС могут подвергаться страничному вытеснению как и
пользовательские сегменты.
1.22. Методы распределения памяти
Все алгоритмы распределения (управления) памятью (рис. 5)
можно разделить на 2 класса:
25
алгоритмы, в которых используется перемещение сегментов
процессов между оперативной памятью и диском;
алгоритмы, в которых внешняя память не используется.
Простейший способ управления оперативной памятью состоит в
том, что память разбивается на несколько областей фиксированной
величины, называемых разделами. Такое разбиение может быть
выполнено вручную оператором во время старта системы или во
время ее установки. После этого границы разделов не изменяются.
Очередной процесс, поступивший на выполнение, помещается либо
в общую очередь, либо в очередь к некоторому разделу (рис. 6).
Методы распределения памяти
Без использования
внешней памяти
С использованием
внешней памяти
Фиксированные
разделы
Страничное
распределение
Динамические
разделы
Сегментное
распределение
Перемещаемые
разделы
Сегментностраничное
Рис. 5
1 раздел
Очередная задача
2 раздел
очередь
3 раздел
Рис. 6
26
Подсистема управления памятью в этом случае выполняет следующие задачи:
сравнивает объем памяти, требуемой для вновь поступившей
задачи, с размерами свободных разделов и выбирает подходящий
раздел;
осуществляет загрузку программы в один из разделов и настройку адресов.
Уже на этапе трансляции разработчик программы может создать раздел, в котором ее следует выполнять. Это может позволить
без использования перемещаемого загрузчика получить машинный код, настроенный на конкретную область памяти. При простоте реализации данный метод имеет существенные недостатки,
так как в каждом разделе может выполняться только один процесс – число задач ограничено числом разделов. Кроме того, независимо от размера программы, она будет занимать весь раздел.
С другой стороны, разбиение памяти на разделы не позволяет выполнять процессы, программы которых не помещаются ни в один
из разделов, и для которых было бы достаточно памяти нескольких
разделов. Рассмотрим распределение памяти динамическими разделами. Каждому поступившему вновь на выполнение приложению на этапе создания процесса выделяется вся необходимая ему
память (если достаточный объем памяти отсутствует, процесс не
создается). После завершения процесса память освобождается и на
это место может быть загружен другой процесс. Таким образом, в
производный момент времени ОЗУ представляет собой случайную
последовательность занятых и свободных участков (разделов произвольного размера). Функции ОС, предназначенные для реализации данного метода, следующие:
ведение таблиц свободных и занятых областей, в которых указываются начальные адреса и размеры участков;
при создании нового процесса анализ требуемой памяти, просмотр таблицы свободных областей и выбор раздела, размер которого достаточен для размещения кодов и данных нового процесса.
Такой выбор может осуществляться по разным правилам, например: "Первый попавшийся раздел достаточного размера"; "Раздел,
имеющий наименьший достаточный размер";
загрузка программы в выделенный раздел и корректировка таблиц свободных и занятых областей.
Данный способ предполагает, что программный код не перемещается во время выполнения, а значит, настройка адресов может
быть проведена единовременно во время загрузки;
27
после завершения процесса корректировка таблиц свободных и
занятых областей.
По сравнению с разбиением на фиксированные разделы, динамический метод более гибок. Но у него есть существенный недостаток –
фрагментация памяти, т.е. образование большого числа несмежных
участков в свободной памяти небольшого размера. При использовании перемещаемых разделов удается решить проблему фрагментации памяти. Один из методов борьбы с фрагментацией – это перемещение всех занятых участков в сторону старших или младших
адресов так, чтобы вся свободная память образовала одну незанятую область. В случае такого решения на ОС накладываются дополнительные функции: ОС время от времени вынуждена копировать
содержание разделов из одного участка памяти в другой, при этом
корректируя таблицы свободных и занятых областей. Эта процедура
называется сжатием. Так как программы (процессы) перемещаются
в в ходе своего выполнения, то в данном случае не удается выполнить настройку адресов с помощью перемещаемого загрузчика. Приходится использовать динамическое преобразование адресов. Хотя
процедура сжатия приводит к более экономичному использованию
памяти, она может потребовать значительных ресурсов от системы,
что часто перевешивает преимущество данного метода.
1.23. Свопинг и виртуальная память
Свопинг – это подкачка. В мультипрограммном режиме помимо
активного процесса, который в настоящий момент выполняется процессором, имеются приостановленные процессы, находящиеся в состоянии ожидания ввода-вывода или освобождения ресурсов. Образы таких неактивных процессов могут быть временно выгружены на
диск до следующего цикла активности. К моменту, когда приходит
очередь выполнения такого выгруженного процесса, его образ возвращается с диска в оперативную память (целиком). Если при этом
выясняется, что свободного места в ОЗУ не хватает, то на диск загружается другой процесс. Такая подмена ОЗУ дисковой памятью позволяет повысить уровень мультипрограммирования. Транслятор,
используя виртуальные адреса, переводит программу в машинный
код, как будто в ее распоряжении имеется однородная оперативная
память большого объема. Виртуализация оперативной памяти осуществляется совокупностью программных кодов ОС и аппаратных
средств процессора и включает решение следующих задач:
28
размещение данных в запоминающих устройствах разного типа,
например, часть кода программы в ОЗУ, а часть – на диске;
перемещение по мере необходимости данных между памятью и
диском;
выбор образов процессов и их частей для перемещения из ОЗУ
на диск и обратно;
преобразование виртуальных адресов в физические.
Виртуальная оперативная память может быть выполнена на основе двух разных подходов:
swаpping – образы процессов при этом выгружаются на диск и
возвращаются в ОЗУ целиком (в чистом виде сейчас не используется);
виртуальная память – между ОЗУ и диском перемещаются части (сегменты, страницы и т.п.) образов процессов.
Свопинг является частным случаем виртуальной памяти. Свопингу в чистом виде свойственна избыточность: когда ОС решает
активизировать процесс, для его выполнения, как правило, не
требуется загружать весь процесс, а достаточно загрузить небольшую часть кодового сегмента, подлежащих выполнению команд и
частей сегментов данных, с которыми в данный момент работает
программа, а также отвести место под стек. Аналогично при освобождении памяти для загрузки нового процесса часто не требуется
выгружать другой процесс целиком. Достаточно вытеснить на диск
только часть его образца. Кроме того, своппинг в чистом виде имеет
еще один недостаток: при его использовании часто не удается загрузить для выполнения процесс, виртуальное адресное пространство
которого превышает имеющуюся в наличии свободную память.
В настоящее время все множество реализаций виртуальной памяти может быть представлено тремя классами:
страничная виртуальная память. Организует перемещение частей процессов между памятью и диском страницами (частями виртуального адресного пространства фиксированного и относительно
небольшого размера);
сегментная виртуальная память. Предполагает перемещение
кодов сегментами, т.е. частями виртуального адресного пространства произвольного размера, полученного с учетом смыслового значения перемещаемых кодов;
сегментно-страничная виртуальная память. Использует двухуровневое деление: виртуальное адресное пространство делится на
сегменты, затем сегменты делятся на страницы. Единицей перемещения кодов является страница. Этот способ управления памятью
объединяет в себе элементы обоих предыдущих подходов.
29
1.24. Страничное распределение памяти
Виртуальное адресное пространство каждого процесса делится
на части одинакового фиксированного размера, называемые виртуальными страницами. В общем случае размер виртуального адресного пространства процесса не кратен размеру страницы, поэтому
последняя страница каждого процесса самодополняется фиктивной областью. Вся оперативная память компьютера также делится
на части такого же размера, называемые физическими страницами. Размер страницы выбирается равным степени двойки. В большинстве ОС размер страниц равен 4096, 512, 1024 байт. Это позволяет упростить механизм преобразования адресов. При создании
процесса ОС загружает в оперативную память несколько его виртуальных страниц (начальные страницы каждого сегмента и сегмента данных). Копия всего виртуального адресного пространства процесса находится на диске. Смежные виртуальные страницы необязательно располагаются в смежных физических страницах. Для
каждого процесса ОС создает таблицу страниц – информационную
структуру, содержащую записи обо всех виртуальных страницах
процесса. Запись из таблицы называется дескриптором страницы
и включает следующую информацию:
номер физической страницы, в которую загружена данная виртуальная страница;
признак присутствия (флаг), устанавливаемый в единицу, если
виртуальная страница находится в ОЗУ;
признак модификации страницы (флаг), который устанавливается в единицу всякий раз когда происходит запись по адресу, относящемуся к данной странице;
признак обращения к странице (флаг), называемый битом доступа. Устанавливается в единицу при каждом обращении по адресу, относящемуся к данной странице.
Признаки присутствия модификации и обращения в большинстве современных процессоров устанавливаются аппаратно схемами процессора при выполнении операции с памятью.
Сами же таблицы страниц, также как и описываемые ими страницы, размещаются в ОЗУ. Адрес таблицы страниц включается в
контекст соответствующего процесса. При активации очередного
процесса ОС загружает адрес его таблицы страниц в специальный
регистр процессора, при этом при каждом обращении к памяти выполняется поиск номера виртуальной страницы, содержащей требуемый адрес. Затем по этому номеру определяется нужный эле30
мент таблицы страниц и из него извлекается описывающая страницу информация. Далее анализируется признак присутствия,
и если данная страница находится в ОЗУ, то выполняются преобразования виртуального адреса в физический, т.е. виртуальный
адрес заменяется указанным в таблице физическим адресом. Если
же нужная виртуальная страница в данный момент выгружена на
диск, происходит так называемое страничное прерывание. При
этом выполняющийся процесс переводится в состояние ожидания
и активизируется другой процесс из очереди процессов, находящейся в состоянии готовности. Программа обработки страничного
прерывания находит на диске требуемую виртуальную страницу
(для этого ОС должна помнить положение вытесненной страницы в
страничном файле диска файловой системы) и пытается загрузить
ее в ОЗУ. Если в памяти имеется свободная физическая страница,
то загрузка выполняется немедленно, если свободного места нет, то
на основании принятой в данной ОС стратегии замещения страниц
решается вопрос о том, какую страницу следует выгрузить из оперативной памяти. После того как выбрана страница, которая должна покинуть ОЗУ, обнуляется ее бит присутствия и анализируется
признак модификации. Если выталкиваемая страница за время последнего пребывания в ОЗУ была модифицирована, то ее новая версия должна быть перезаписана на диск; если же нет, то принимается во внимание, что на диске уже имеется предыдущая копия этой
виртуальной страницы и никакой записи на диск не производится.
1.25. Преобразование виртуальной страницы
в физическую
Задача подсистемы виртуальной памяти состоит в отображении
виртуальных страниц в физические. Объем страницы выбирается
кратным степени двойки, из этого следует, что смещение S может
быть получено отделением k младших разрядов в двоичной записи
адреса, а оставшиеся старшие разряды адреса представляют собой
двоичную запись номера страницы (при этом не важно, является
страница виртуальной или физической). Например, если размер
страницы равен 210, то в этом примере – 1000111001, номер страницы будет равен числу 2, т.е. 10, а смещение – 00111001.
Номер страницы и ее начальный адрес могут быть получены
один из другого путем отбрасывания k нулей, соответствующих
смещению. Часто говорят, что таблица страниц содержит началь31
ный физический адрес страницы памяти, хотя на самом деле в таблице указаны только старшие разряды адреса. Начальный адрес
страницы также называется базовым. В пределах страницы непрерывная последовательность виртуальных адресов отражается в непрерывную последовательность физических адресов, это значит,
что смещения виртуального и физического адреса равны между собой. Из этого следует простая схема преобразования виртуального
адреса в физический (рис. 7).
Пусть произошло обращение к памяти по некоторому виртуальному адресу. Аппаратными средствами центрального процессора
(ЦП) выполняются следующие действия:
из специального регистра ЦП извлекается адрес Т таблицы страниц (для каждого процесса этот адрес свой) активного процесса. На
основании начального адреса Т и номера виртуальной страницы Р
(старшие разряды виртуального адреса), а также длины отдельной
записи в таблице страниц (системная константа L), определяется
адрес нужного дискриптера таблицы страниц, т.е. число (Т + РL);
из этого дескриптора извлекается номер соответствующей физической страницы n;
к номеру физической страницы присоединяется смещение S
(младшие разряды виртуального адреса).
K – младших разрядов
P
T+PL
S смещение виртуальной
страницы
номер виртуальной
страницы
№ физических страниц
Регистр T ЦП
n
n № физического
сегмента
S смецение физического
сегмента
L – системная константа
Рис. 7
32
Для уменьшения времени преобразования адресов во всех процессорах предусмотрен аппаратный механизм получения физического адреса по виртуальному. С этой же целью номер страницы выбирается равным степени 2, благодаря чему двоичная запись адреса
легко разделяется на номер страницы и смещение. В результате такой процедуры преобразования адресов более длительная операция
сложения заменяется операцией присоединения (конкантенации).
1.26. Сегментное распределение памяти
При таком методе виртуальное адресное пространство процесса
делится на части – сегменты, размер которых определяется с учетом смыслового значения содержащейся в них информации. Отдельный сегмент может представлять собой подпрограмму, массив
данных и т.п. Деление виртуального адресного пространства на
сегменты осуществляется компилятором на основе указаний программиста или в соответствии с принятыми в системе соглашениями. Максимальный размер сегмента определяется разрядностью
виртуального адреса, например 32-разрядная организация шины
адреса позволяет адресовать 232 ячеек ОЗУ. При этом максимально
возможное виртуальное адресное пространство процесса представляет собой набор из N виртуальных сегментов, каждый из которых
равен 4 Гб. В каждом сегменте виртуальные адреса находятся в
диапазоне (00000000 – FFFFFFFF). Сегменты не упорядочены друг
относительно друга, так что общего линейного виртуального адреса
для сегментов не существует.
Виртуальный адрес задается парой чисел: номером сегмента и
линейным виртуальным адресом внутри сегмента. При загрузке
процесса в ОЗУ помещается только часть его сегмента. Полная копия виртуального адресного пространства находится на диске. Для
каждого загруженного сегмента ОС подыскивает непрерывный
участок свободной памяти достаточного размера. Смежные сегменты виртуальной памяти одного процесса могут занять в физической
памяти несмежные участки. Если во время выполнения процесса
происходит обращение по виртуальному адресу, относящемуся к
сегменту, который в данный момент не присутствует в ОЗУ, происходит прерывание. ОС приостанавливает активный процесс, запускает на выполнение следующий, а параллельно организует загрузку нужного сегмента с диска. При отсутствии в памяти места
для загрузки сегмента ОС выбирает сегмент на выгрузку. На этапе
33
создания процесса во время загрузки его образа в ОЗУ система создает таблицу сегментов процесса, аналогичную таблицу страниц, в
которой для каждого сегмента указывается:
1) базовый физический адрес в ОЗУ;
2) размер сегмента;
3) правила доступа к сегменту;
4) признаки модификации, присутствия и обращения к данному
сегменту.
Если виртуальное адресное пространство нескольких процессов
включает один и тот же сегмент, то в таблице сегментов этих процессов делаются ссылки на один и тот же участок ОЗУ, в котором
данный сегмент загружается в единственном экземпляре. Таким
образом, сегментное распределение памяти имеет много общего со
страничным распределением.
Механизмы преобразования адресов этих двух способов управления памятью тоже похожи, но имеются и существенные отличия, которые являются следствием того, что сегменты, в отличие
от страниц, имеют произвольный размер. Виртуальный адрес при
сегментной организации может быть представлен парой g, S (номер
сегмента + смещение).
Физический адрес получается путем сложения адреса базового
сегмента, который определяется по номеру сегмента из таблицы
сегментов, и смещения S (рис. 8).
В данном случае невозможно обойтись объединением, поскольку размер страницы равен степени 2, следовательно, в двоичном
виде он выражается числом с несколькими нулями в младших
g – номер сегмента
S смещение
Таблица сегментов
Физический адрес
Рис. 8
34
разрядах, страницы же имеют один размер, и, следовательно, их
начальные адреса кратны размеру страниц, и выражаются также
числами с нулями в младших разрядах. Именно поэтому ОС заносит в таблицу страниц не полные адреса, а номера физических страниц, которые совпадают со старшими разрядами базовых адресов.
Сегменты же могут располагаться в физической памяти с любого
адреса, следовательно, для определения местоположения в памяти
необходимо задавать их полный начальный физический адрес. Использование операции сложения вместо конкантенации замедляет
процедуру преобразования виртуального адреса в физический по
сравнению со страничной организацией.
Еще одним недостатком сегментного распределения памяти является фрагментация памяти, это означает, что после освобождения памяти от сегмента свободным остается произвольный участок
памяти, в который может быть записан либо сегмент такого же,
либо меньшего размера. При продолжительной работе компьютера
память может оказаться сильно фрагментированной. Для устранения этого используется дефрагментация памяти, в ходе которой
часть сегментов выгружается в область своппинга и затем записывается обратно в память без промежутков.
При страничной организации памяти такая проблема тоже есть,
но она меньше проявляется.
1.27. Сегментно-страничное распределение памяти
Перемещение данных между памятью и диском осуществляется
не сегментами, а страницами, для этого каждый виртуальный сегмент и физическая память делится на страницы равного размера,
что позволяет более эффективно использовать память, сократив до
минимума фрагментацию. В отличие от набора виртуальных адресов при сегментной организации памяти все виртуальные сегменты
в сегментно-страничном методе образуют одно непрерывное линейное виртуальное адресное пространство. Координаты байтов в виртуальном адресном пространстве при сегментно-страничной организации можно задать двумя способами:
линейным виртуальным адресом, который равен сдвигу одного
байта относительно границы общего линейного виртуального пространства;
парой чисел, одно из которых является номером сегмента, а другое – смещением относительно начала сегмента.
35
При этом в отличие от сегментной модели при такой организации необходимо каким-то образом указать начальный виртуальный адрес сегмента с данным номером.
Система виртуальной памяти сегментно-страничной организации использует второй способ, так как он позволяет определить
принадлежность адреса заданному сегменту и проверить права доступа процесса к нему. Для каждого процесса ОС создает отдельную таблицу сегментов, в которой содержатся описатели (дескрипторы) всех сегментов процесса. Описание сегмента включает назначение ему права доступа и другие характеристики, подобные тем,
которые содержатся в дискриптерах сегмента при сегментной организации памяти (флаги, например). Однако имеется принципиальное отличие. В поле базового адреса указывается не начальный
физический адрес сегмента, отведенный ему в результате загрузки
в оперативную память, а начальный линейный виртуальный адрес
сегмента в пространстве виртуальных адресов. Наличие базового
виртуального адреса сегмента в дескипторе позволяет однозначно
преобразовать адрес, заданный в виде пары (№ сегмента, смещение в сегменте) в линейный виртуальный адрес байта, который затем преобразуется в физический адрес страничным механизмом.
Деление общего линейного виртуального адресного пространства
процесса и физической памяти на страницы осуществляется так
же, как это делается при страничной организации памяти. Размер
страницы выбирается равным степени двойки, что упрощает механизм преобразования виртуальных адресов в физические. Виртуальные страницы нумеруются в пределах виртуального адресного
пространства каждого процесса, а физические – в пределах ОЗУ.
При создании процессов в память загружается только часть страниц, остальные загружаются только по необходимости. Время от
времени система выгружает уже ненужные страницы, освобождая место для новых. Система ведет для каждого процесса таблицу
страниц, в которой указывается соответствие виртуальных страниц физическим. Базовые адреса таблицы сегментов и таблицы
страниц процесса являются частью его контекста. При активации
процесса эти адреса загружаются в специальные регистры ЦП и используются механизмом преобразования адресов. Преобразование
виртуального адреса в физический происходит в два этапа (рис. 9).
На первом этапе работает механизм сегментации. Исходный
виртуальный адрес, заданный в виде пары (номер сегмента, смещение) преобразуется в линейный виртуальный адрес. Для этого на
основании базового адреса таблицы сегментов и номера сегмента,
36
Регистр ЦП
Начальный адрес
таблицы
Номер сегмента
Линейный
виртуальный адрес
Таблица сегментов
Дескриптор сегмента
Смещение сегмента
I ЭТАП
Базовый виртуальный
адрес сегмента
Номер
страницы
Таблица страниц
II ЭТАП
Дескриптор страниц
Смещение
страницы
базовый
физический
адрес
исходный
физический
адрес
Рис. 9
вычисляется адрес дискриптера сегмента, анализируются его поля,
и выполняется проверка возможности выполнения заданной операции. Если доступ к сегменту разрешен, то вычисляется линейный виртуальный адрес путем сложения базового адреса сегмента,
извлеченного из дескриптора, и смещения, заданного в исходном
виртуальном адресе.
На втором этапе работает страничный механизм. Полученный
линейный виртуальный адрес преобразуется в искомый физический адрес. В результате преобразования линейный виртуальный
адрес представляется в том виде, в котором он используется при
страничной организации, в виде пары (номер страницы, смещение). Благодаря тому, что размер страницы выбран равным степени
двойки, эта задача решается простым отделением некоторого количества младших двоичных разрядов, при этом в старших содержится номер виртуальной страницы, а в младших смещение искомого
элемента относительно начала страницы. Если размер страницы
равен 2k, то смещением является содержимое k младших разрядов,
а остальные старшие разряды содержат номер виртуальной страницы, которой принадлежит искомый адрес. Далее преобразование
адреса происходит так же, как при страничной организации: старшие разряды линейного виртуального адреса, содержащие номер
37
виртуальной страницы заменяются на номер физической страницы, взятый из таблицы страниц, а младшие разряды виртуального
адреса, содержащие смещение остаются без изменения.
1.28. КЭШ-память
КЭШ – это способ совместного функционирования двух типов
памяти, отличающихся временем доступа и способом хранения
данных, который за счет динамического копирования в быстрое
запоминающее устройство наиболее часто используемой информации из более медленного запоминающего устройства, позволяет
ускорить доступ к данным, хранящимся на диске.
КЭШ-память прозрачна для программ и пользователей. Система не требует никакой внешней информации. Ни пользователь ни
программа не принимают участия в перемещении данных из ОЗУ в
КЭШ и обратно. Все это делается автоматически системными средствами. КЭШ-памятью также называют способы организации работы запоминающих устройств двух типов (быстрого и медленного).
КЭШ быстрая память, ОЗУ – медленная. Если кэширование применяется для уменьшения среднего времени доступа к ОЗУ, то в качестве КЭШ используют быстродействующую статическую память
(среднее время обращения к ОЗУ – 6 нс, а к статическому КЭШ –
1 нс). Если кэширование используется системой ввода-вывода для
ускорения доступа к данным, хранящимся на диске, то в этом случае роль КЭШ-памяти выполняют буферы ОЗУ, в которых оседают
наиболее активно используемые данные. Виртуальную память так
же можно считать одним из вариантов реализации принципов кэширования, в котором ОЗУ выступает в роли КЭШ по отношению к
внешней дисковой памяти.
Содержимое КЭШ-памяти – это совокупность записей обо всех
элементах, загруженных в нее из основной памяти. Каждая запись
об элементе данных включает в себя:
значение элемента данных (сама информация);
адрес, который этот элемент данных имеет в ОЗУ (в основной памяти);
дополнительная информация, которая используется для реализации алгоритма смещения данных в КЭШе и обычно включает
признак модификации и признак действительности данных.
При каждом обращении к основной памяти по физическому адресу просматривается содержимое КЭШ-памяти с целью определения,
38
не находятся ли там нужные данные. КЭШ-память не является
адресуемой, поэтому поиск нужных данных осуществляется по содержимому, т.е. по взятому из запроса значению поля адреса в ОЗУ.
Если данные обнаруживаются в КЭШе (быстрой памяти), т.е.
произошло КЭШ-попадание – данные считываются из КЭШ и результат передается источнику запроса.
Если нужные данные отсутствуют в КЭШе, т.е. произошел
КЭШ-промах – они считываются из основной памяти и передаются
источнику запроса и одновременно с этим копируются в КЭШ. Схема работы КЭШ приведена на рис. 10.
Среднее время доступа в системе с КЭШ линейно зависит от вероятности попадания в КЭШ и изменяется от среднего времени доступа к ОЗУ до среднего времени доступа к КЭШ. Таким образом,
использование КЭШ имеет смысл, когда вероятность попадания в
него высока. Эта вероятность зависит от нескольких факторов, таких как объем КЭШ; алгоритм замещения данных в КЭШ; особенности выполняемой программы; времени ее работы; уровня мультипрограммирования и других особенностей вычислительного процесса. В большинстве реализаций КЭШ процент попадания оказывается высоким (свыше 90%). Такое высокое значение вероятности
нахождения данных в КЭШ объясняется наличием двух объективных свойств. Они называются свойствами пространственной и временной локальности.
Временная локальность означает, что если произошло обращение по некоторому адресу, то следующее обращение по тому же
адресу с большой вероятностью произойдет в ближайшее время.
Пространственная локальность подразумевает, что если произошло обращение по некоторому адресу, то с высокой степенью
вероятности в ближайшее время произойдет обращение к соседИсточник запроса
к основной памяти
медленный запрос
медленный ответ
быстрый ответ
Основная
память
(ОЗУ)
КЭШ
Рис. 10
39
ним адресам. Основываясь на свойстве временной локальности,
данные, только что считанные из основной памяти, размещают в
ЗУ быстрого доступа (КЭШ), предполагая при этом, что скоро они
опять понадобятся. В начале работы системы, когда КЭШ еще пуст,
почти каждый запрос к основной памяти выполняется по полной
программе: просмотр КЭШа, констатация промаха, чтение данных из основной памяти, передача результата источнику запроса
и копирование данных в КЭШ. Затем, по мере заполнения КЭШ, в
соответствии со свойством временной локальности возрастает вероятность обращения к данным, которые уже были использованы на
предыдущих этапах работы, т.е. к данным, которые содержатся в
КЭШе и могут быть оттуда быстро считаны.
Свойства пространственной локальности также используются для увеличения вероятности КЭШ-попаданий: как правило, с
КЭШ-памяти считывается не один информационный элемент, к
которому произошло обращение, а целый блок данных, расположенный в основной памяти в непосредственной близости с данным
элементом. Поскольку при выполнении программы очень высока
вероятность, что команды выбираются из памяти последовательно одна за другой из соседних ячеек, то имеет смысл загружать в
КЭШ-память целый фрагмент программы. Например, если программа ведет обработку массива, то ее работу можно ускорить, загрузив в КЭШ часть или даже весь массив.
1.29. Устройства ввода-вывода
Устройства ввода-вывода можно разделить на две категории:
блочные устройства и символьные.
Блочным устройством называется устройство, хранящее данные
фиксированного размера, причем у каждого блока имеется адрес.
Обычно размеры блоков независимо от типов устройства колеблются в пределах (512 ÷ 32765) байт. Важное свойство состоит в том,
что каждый блок может быть прочитан независимо от остальных
блоков. Наиболее распространенными блочными устройствами являются диски.
Символьное устройство принимает или предоставляет поток
символов из какой-либо блочной структуры. Оно не является адресуемым и не выполняет операции поиска.
Примерами символьных устройств являются принтеры, интерфейсные адаптеры, мыши и т.п. Устройства ввода-вывода по40
крывают огромный диапазон скоростей, что создает определенные
трудности для программного обеспечения, поскольку приходится
обеспечивать работоспособность и производительность на скоростях передачи данных, различающихся на несколько порядков.
В табл. 1 приведены значения скоростей передачи данных для разных устройств ввода-вывода.
Таблица 1
Значения скоростей передачи данных
для разных устройств ввода-вывода
Устройство
Клавиатура
Мышь
Модем аналоговый
ISDN
Лазерный принтер
Сканер
10 Мб Ethernet
USB 2.0
CD-ROM 40 скоростей
100 Мб Ethernet
IDE (ATA-2)
WiFi 1394
ScSi-disk ultra-2
Гигабитная сеть Ethernet
Шина PCI
Скорости данных
10 байт/сек
100 байт/сек
7 кб/сек
16 кб/сек
100 кб/сек
400 кб/сек
1,5 мб/сек
1,5 мб/сек
6 мб/сек
12 мб/сек
16,5 мб/сек
50 мб/сек
80 мб/сек
125 мб/сек
528 мб/сек
Контроллер – это электронный компонент любого устройства
ввода-вывода. Интерфейс между устройством и контроллером часто является интерфейсом очень низкого уровня. Работа контроллера заключается в преобразовании последовательного потока битов в блок байтов и в выполнении коррекции ошибок, если это необходимо. Битовый поток обычно собирается бит за битом в буфере
контроллера, затем проверяется контрольная сумма блока и, если
она совпадает с объявленной (например в случае HDD в заголовке
сектора), блок объявляется считанным без ошибок, после чего он
копируется, например, в буфер, расположенный в ОЗУ. Контроллер монитора работает как бит-последовательное устройство на
таком же низком уровне. Он считывает из памяти байты, которые
следует отобразить, и формирует сигналы для вывода изображения
на экран.
41
1.30. Способы организации ввода-вывода
На рис. 11 изображена схема контроллера устройства ввода-вывода.
Современные контроллеры внешних устройств имеют все признаки вычислительной системы, т.е. в их состав входит процессор,
ОЗУ и внутренние шины.
Рассмотрим различные способы отображения регистров внешних устройств на память вычислительной системы. У процессора есть две команды для ввода и вывода. Это команды <IN REG.
PORT> и <OUT PORT. REG>. С помощью команды <IN> процессор
может записать из регистра устройства ввода-вывода порт в свой
регистр. Команда <OUT> выполняет противоположное действие –
записывает из своего регистра в регистр внешнего устройства.
Команды <IN> и <OUT> есть в ассемблере, но не могут использоваться в языках высокого уровня.
С другой стороны на языках высокого уровня можно применять
команды <write> и <read>, с помощью которых можно писать и
считывать из ОЗУ. Например, команда <In R0 3> и <MOV R0 3>
выполняет принципиально разные действия. Первая команда считает содержимое порта 3 в регистр R0 процессора, а вторая команда считает в этот же регистр содержимое слова памяти по адресу 3.
Таким образом, 3 в этих командах означают различные адреса из
непересекающихся адресных пространств. Это происходит за счет
того, что в случае, например, записи в регистр R0 процессора из
порта 3, возбуждается линия управления, т.е. аппаратно эти адресные пространства разносятся. Имеются три способа для организации ввода-вывода.
Электромеханическая часть устройства
Буферная
память
Управляющие
регистры
Контроллер
устройства
линия управления
Шина
Рис. 11
42
1 способ. Раздельные адресные пространства
Разделение происходит за счет линии управления.
FFFFFFFF
Память
ОЗУ
00000000
00000000
Порт
ввода-вывода
2 способ. Одно адресное пространство
При отображении регистров ввода-вывода на память каждая
команда процессора, обращающаяся к памяти, может с таким же
успехом обращаться к управляющим регистрам устройств.
Адресное
пространство
ввода-вывода
Память
ОЗУ
При этом каждому управляющему регистру назначается уникальный адрес в памяти. Иногда такую схему называют отображаемым на адресное пространство памяти вводом-выводом. Обычно
для регистров устройств отводятся адреса на вершине адресного
пространства.
Существуют разные гибридные схемы отображения ввода-вывода на память.
3 способ. Гибридный
Один из возможных вариантов третьего, гибридного подхода.
ОЗУ
Буферы
Порты
ввода-вывода
ОЗУ
Эта схема широко используется в платформах на базе Intel, в которых помимо портов ввода-вывода с адресами от 0 до 64 Кб используется адресное пространство ОЗУ от 640 Кб до 1 Мб для буферов
устройств ввода-вывода. Во всех случаях, когда ЦП хочет прочитать слово данных либо из памяти, либо из порта ввода-вывода, он
сначала выставляет нужный адрес на адресную шину, после чего
выставляет <read> (считать) на управляющую шину. Сигнальная
43
линия при этом позволяет отличить обращение к памяти от обращения к порту. В зависимости от состояния этой линии на запрос
процессора реагирует либо устройство ввода-вывода (контроллер),
либо память. Если пространство адресов общее (вариант 2), то каждый модуль памяти и каждое устройство ввода-вывода сравнивает выставленный на шину адрес с обслуживаемым им диапазоном
адресов. Если адрес попадает в этот диапазон, то соответствующее
устройство реагирует на запрос процессора. Поскольку выделенные внешним устройством адреса удаляются из памяти, внешние
устройства не реагируют на них и конфликта не происходит. Схемы 1 и 2 имеют свои достоинства и недостатки.
Достоинствами является следующее.
При отображении на адресное пространство памяти ввода-вывода не требуются специальные команда процессора <in> и <out>.
В результате программу можно написать целиком на языке С, без
вставок на Ассемблере и обращений к подпрограммам.
При отображении регистров ввода-вывода на память не требуется специального механизма защиты от пользовательских процессов, пытающихся обращаться к внешним устройствам. Все, что
нужно сделать – это исключить ту часть адресного пространства,
на которую отображаются управляющие регистры ввода-вывода
из адресного пространства пользователя. В результате такая схема
позволяет разместить драйверы различных устройств в различных
адресных пространствах, тем самым не только уменьшив размер
ядра, но и исключив вмешательство драйверов в дела друг друга.
К недостаткам этих схем относится следующее.
В большинстве современных ПК применяется кэширование памяти. кэширование управляющих регистров привело бы к краху
ядра. Чтобы не допустить такой ситуации, необходима специальная аппаратура, способная выборочно запрещать кэширование.
Например, в зависимости от номера страницы памяти, к которой
CPU
процессора
Ввод-вывод
шина
Рис. 12
44
Память
ОЗУ
обращается процессор. Таким образом, отображение регистров ввода-вывода на память увеличивает сложность аппаратуры и самой
ОС, которой приходится управлять избирательным кэшированием.
При едином адресном пространстве все модули памяти и устройства ввода-вывода должны изучать все обращения процессора к
памяти, чтобы определить, на какие следует реагировать. Если у
компьютера одна общая шина, реализовать подобный просмотр обращений не сложно (рис. 12).
1.31. Использование нескольких шин
для ввода-вывода
Соединение элементов в компьютере по принципу "каждый с
каждым" практически неосуществимо из-за его сложности, поэтому уже на первых компьютерах использовались шины. Шина – не
только набор проводников, по которым могут передаваться уровни
напряжений, соответствующие нулям и единицам, но и устройства, которые ею управляют в соответствии с протоколом шины.
Главная характеристика шины это ее разрядность.
Как правило, шина состоит из трех частей:
одна ее часть предназначена для передачи данных – это шина
данных;
другая часть – для передачи адресов (адреса имеют не только
ячейки ОЗУ, но и практически все активные компоненты компьютера);
шина управления, выполняющая сервисные функции, помогающие управлять элементами компьютера.
Следующая характеристика шины – скорость передачи данных
(килобайт/мегабайт в секунду), чем выше разрядность – тем больше скорость. Помимо самих проводников в шину входят обслуживающие ее микросхемы, называемые мостами. Основная функция
мостов – управление шиной по ее протоколу. Любая шина, которая
управляется мостом, пробегает по времени определенное количество состояний, которые повторяются в цикле. В каждый данный
момент шина может находиться только в определенном состоянии:
принимать данные, ждать, передавать данные и тому подобное.
В современных ПК имеются несколько шин:
системная шина: чаще всего самая быстрая; имеет наибольшее
число разрядов; соединяет две главные части ПК – процессор и память. Таким образом, в ядро системы входит три компонента, без
45
которых она не может существовать: процессор, шина (системная)
и память;
одна или несколько локальных шин, выполняющих вспомогательные функции. К таким шинам могут присоединяться внешние
устройства. В задачу мостов входят также задачи передачи данных
с одной шины на другую. Мосты необходимы потому, что шины могут работать на разных частотах и по разным протоколам;
сервисная шина, которая соединяет некоторые элементы или компоненты компьютера, такие как системные часы и тому подобное.
Идея использования шины состоит в том, что все, кому надо общаться между собой в системе, подключаются к шине (они – абоненты). В каждый данный момент шина может соединять только 2
устройства, остальные устройства в такие моменты должны от нее
отключаться.
В конструкции современных ПК используется системная, т.е.
быстрая шина, напрямую соединяющая процессор и память, показанная на рис. 13.
Быстрая шина предназначена для увеличения скорости обмена данными между процессором и памятью. Она является системной, чему в архитектуре общей шины сильно мешали медленные
устройства ввода-вывода.
Сложность применения системной шины на компьютерах с отображением регистров ввода-вывода на память состоит в том, что у
устройств ввода-вывода нет способа определить адреса памяти, выставляемые процессором на эту шину, следовательно, они не могут
реагировать на такие адреса, поэтому чтобы отображение регистров
ввода-вывода могло работать по этой схеме, необходимы специальные меры.
Для решения этой проблемы можно сначала все обращения к
памяти посылать по системной (быстрой) шине (чтобы не снижать
быстрая шина
CPU
процессора
Память ОЗУ
универсальная шина
Рис. 13
46
Вводвывод
производительности). Если память не отвечает, процессор пытается сделать это еще раз, но по медленной (универсальной) шине, к
которой подключены устройства ввода-вывода. Такое решение работает, но требует увеличения сложности аппаратуры.
1.32. Прямой доступ к памяти (DMA)
Центральный процессор может запрашивать данные от контроллера ввода-вывода по одному байту, но подобная система обмена
данными крайне нежелательна, так как расходует огромное количество процессорного времени, поэтому на практике уже давно используется другая схема – DМА. ОС может использовать этот метод
лишь при наличии соответствующего оборудования – контроллера
DМА.
DМА-контроллер может получать доступ к системной шине независимо от процессора. Он содержит несколько регистров, доступных процессору для чтения и записи. К ним относятся: регистр
адреса памяти, счетчик байтов и один или несколько управляющих
регистров. Эти регистры определяют какой порт ввода-вывода должен быть использован, направление переноса (чтение из устройств
ввода-вывода или запись), единицу переноса (осуществлять перенос
побайтно или пословно), а также число байтов, которые следует перенести за одну операцию. Таким образом, процессор может запрограммировать контроллер на перенос данных из устройствава ввода-вывода в память и обратно так, как это удобно для ОС (рис. 14).
Рассмотрим, как происходит перенос данных с диска в оперативную память.
Диск
Контроллер
ДМА
Контроллер диска
Память
1
4
Адрес
БУФЕР
Счетчик
CPU
ОЗУ
Управление
5
2
3
Шина
Рис. 14
47
Сначала контроллер считывает с диска блок (один или несколько секторов) последовательно, байт за байтом, пока весь блок не
окажется во внутреннем буфере контроллера. Этот контроллер
проверяет контрольную сумму, чтобы убедиться, что при чтении
не произошло ошибки. После этого контроллер диска инициирует
прерывание. Когда ОС начинает работу, она может прочитать блок
диска побайтно или пословно, в цикле сохраняя считанное слово
или байт в оперативной памяти.
При использовании DМА происходит следующее. Сначала процессор программирует DМА-контроллер (этап 1), устанавливая
его регистры и указывая таким образом, какие данные и куда следует переместить. Затем процессор дает команду дисковому контроллеру прочитать данные во внутренний буфер и проверить контрольную сумму. Когда данные проверены и получены контроллером диска, DМА-контроллер начинает перенос данных, посылая
по шине на контроллер диска запрос чтения (шаг 2). Этот запрос
выглядит как обычный запрос чтения, так что контроллер диска
даже не знает, пришел он от процессора или DМА-контроллера.
Адрес памяти уже находится на адресной шине, так что контроллер диска знает, куда следует переслать слово из своего внутреннего буфера.
Запись в память (шаг 3) является еще одним стандартным циклом шины.
Когда запись закончена, контроллер диска также по шине посылает сигнал подтверждения контроллеру DМА (шаг 4). Затем контроллер DМА увеличивает используемый адрес памяти и уменьшает значение счетчика байт. После этого шаги 2–4 повторяются,
пока значение счетчика не станет равно нулю.
По завершению цикла копирования DМА-контроллер инициирует прерывание процессора (шаг 5).
Отметим, что все это время, до 5-го шага, процессор и контроллерDМА занимались своим делом (процессор выполнял программу, а
DМА-контроллер читал данные).
Самые простые DМА-контроллеры за один раз выполняют одну
операцию переноса данных, как это было описано выше. Более
сложные контроллеры могут выполнять за один раз несколько подобных операций. У них несколько каналов, каждый из которых
управляется своим набором внутренних регистров. Такой контроллер может осуществлять перенос данных "одновременно" и обслуживать несколько устройств ввода-вывода. Многие шины могут работать в двух режимах: пословном и поблочном.
48
Пословный режим. В таком режиме процедура выглядит так,
как описано выше, но контроллер выставляет запрос на перенос
первого слова и получает его, так что если процессору нужна шина,
ему придется подождать. Этот механизм называется захватом цикла, потому что контроллер периодически забирает случайный цикл
у процессора, слегка его притормаживая.
Поблочный режим. В этом режиме контроллер DМА велит
устройству занять шину, сделать серию пересылок и отпустить ее.
Такой способ называется пакетным режимом, он более эффективен, чем захват цикла, поскольку занятие шины требует времени, а
в пакетном режиме эта процедура выполняется всего один раз для
передачи блока данных.
1.33. Процедура прерываний.
Контроллер прерываний
Аппаратное прерывание – это сигнализация от устройства (его
контроллера) центральному процессору о некоторых событиях,
требующих программных действий. Прерывания требуют приостановки выполнения текущего потока команд (с сохранением
состояния) и запуска процедуры обработки прерывания. Эта процедура первым делом должна идентифицировать источник прерывания (их может быть несколько), а затем выполнить действия,
связанные с реакцией на событие. Если события должны вызывать
некоторые действие прикладной программы, то обработчику прерывания следует только подать через ОС сигнал, который запустит
поток команд, выполняющий эти действия.
Контроллер прерываний является периферийным устройством,
которое связано с процессором через ту или иную шину расширения ввода-вывода. По этой шине процессор может обращаться к регистрам контроллера, программируя его режимы и управляя им, а
также получать от контроллера 8-битный вектор прерывания, для
чего в интерфейсе системной шины процессора и шины расширения
имеется специальная команда подтверждения прерывания. Контроллер имеет входы запросов от источников и один выход общего
запроса. Каждому из входов соответствует свой вектор. Программированием регистров контроллера задается номер вектора для входа
0, остальным входам соответствуют последующие номера. Каждый
вход может быть программно замаскирован – тогда он не вызывает сигнал общего запроса. Контроллер занимает два адреса в про49
странстве ввода-вывода, программное обращение позволяет управлять режимами работы контроллера, а также приоритетами.
1.34. Принципы прогрманого обеспечения
ввода-вывода
При разработке той части ОС, которая отвечает за ввод-вывод,
следует придерживаться ряда принципов, главными из которых
являются следующие.
Ключевая концепция разработки ПО ввода-вывода известна как
независимость от устройств. Эта идея означает возможность написания программ, способных получать доступ к любому устройству
ввода/вывода без предварительного указания конкретного устройства. Например, программа, читающая данные из входного файла, должна с одинаковым успехом читать данные, записанные на
дискете, жестком диске или компакт-диске. При этом не должны
требоваться какие-либо изменения в программе. В качестве выходного устройства также с равным успехом может быть указан экран,
файл на любом диске или принтер. Таким образом, все проблемы,
связанные с отличиями этих устройств, должна решать ОС.
Имя файла или устройства должно быть просто текстовой строкой или целым числом и никаким образом не зависеть от физического устройства. Например, в UNIX-подобных ОС все диски могут
быть произвольным образом интегрированы в иерархию файловой
системы так, что пользователю необязательно знать, какое имя
каждому устройству соответствует. Таким образом, все файлы и
устройства адресуются одним и тем же способом по имени.
Ошибки должны обрабатываться как можно ближе к аппаратуре. Если контроллер обнаруживает например, ошибку чтения,
он должен по возможности попытаться исправить ее сам. Если он
не может этого сделать, то эту ошибку должен обработать драйвер
устройства, возможно, попытавшись считать блок данных еще раз.
Иногда ошибки бывают временными. Как, например, ошибки чтения, вызванные пылинками на читающих головках. Такие ошибки
часто исчезают при повторном чтении. Только если нижний уровень не может сам справиться с проблемой, ему следует информировать об этом верхний уровень.
Например, когда сетевой пакет приходит по сети, ОС не знает,
куда его поместить до тех пор, пока не будет прочитан и проанализирован заголовок пакета. Кроме того, для многих устройств ре50
ального времени крайне важными оказываются параметры сроков
поступления даных, поэтому поступающие данные должны быть
помещены в буфер заранее, чтобы скорость, с которой эти данные
получаются из буфера или передаются в него вспомогательными
программами, не зависела бы от скорости заполнения буфера.
1.35. Програмный ввод-вывод
Главным аспектом программного ввода-вывода на примере печати (сетевой принтер) состоит в том, что после печати каждого
символа процессор в цикле опрашивает готовность устройства. Такое поведение процессора называется опросом или ожиданием готовности (активным ожиданием). Программный ввод-вывод легко
реализуется, но его существенный недостаток состоит в том, что
процессор занят все время, пока он осуществляется.
1.36. Управляемый прерываниями ввод-вывод.
Использование DМА
Предоставить процессору возможность делать что-нибудь в то
время, когда принтер находится в состоянии готовности, можно с
помощью прерывания. Процессор вызывает планировщик, который запускает какой-либо другой процесс, а процесс, попросивший
распечатать строку, оказывается заблокирован на все время печати
строки. Когда принтер напечатал символ и готов принять следующий, он инициализирует прерывание. Это прерывание вызывает
остановку текущего процесса и сохранение его состояния. Затем
запускается процедура обработки прерывания от принтера. Если
напечатаны все символы, то обработчик принимает меры для разблокировки процессов пользователя. В противном случае он печатает следующий символ, подтверждая прерывание, и возвращается
к процессу, выполнение которого было приостановлено. Недостаток метода в том, что при печати каждого символа сама обработка
занимает некоторое время, это неэффективный метод.
Решение этой проблемы заключается в использовании DМА.
Идея состоит в том, чтобы позволить котроллеру DМА поставлять
принтеру символы по одному, не беспокоя при этом процессор. По
существу этот метод почти не отличается от программного вводавывода, с той лишь разницей, что всю работу вместо процессора
51
выполняет контроллер DМА. Наибольший выигрыш от использования DМА состоит в уменьшении количества прерываний с одного
на один печатный символ до одного на один буфер.
Если символов много, а прерывание обрабатывается медленно,
то этот выигрыш весьма существенен.
1.37. Программные уровни ввода-вывода
Программное обеспечение ввода-вывода обычно организуется
в виде четырех уровней. У каждого уровня есть четко очерченная
функция, которую он должен выполнять, и строго определенный
интерфейс с соседним уровнями. Функции и интерфейсы этих
уровней меняются от одной ОС к другой.
1.38. Обработка прерываний и драйверы
Хотя программный ввод-вывод бывает полезен, для большинства операций ввода-вывода прерывания являются неприятным,
но необходимым средством.
Лучший способ сделать прерывания незаметными заключается
в блокировке драйвера, начавшего операцию ввода-вывода, вплоть
до окончания этой операции и получения прерывания. Драйвер
может заблокировать себя сам, выполнив на семафоре процедуру
<down> на переменной состояния. Когда прерывание начинается, начинает работать обработчик прерываний, а после окончания
прерывания он может заблокировать драйвер, допустивший прерывание.
В любом случае драйвер разблокируется обработчиком прерывания. Такая схема лучше всего работает в драйверах, являющихся процедурами ядра со своим собственным состоянием, стеком и
счетчиком команд.
Рассмотрим программные уровни ввода-вывода.
1. Программное обеспечение ввода-вывода уровня пользователя.
2. Устройство-независимое программное обеспечение ОС.
3. Драйверы устройств.
4. Обработчики прерываний.
5. Аппаратура.
В большинстве операционных систем определен стандартный
интерфейс, который должен поддерживать все блочные драйверы
52
и второй стандартный интерфейс, который должен поддерживать
все символьные драйверы.
Эти интерфейсы включают наборы процедур, которые могут вызываться остальной частью ОС для обращения к драйверу.
К этим процедурам относятся чтение блока, запись символьной
строки и тому подобное.
Некоторые ОС являются монолитными, т.е. представляют собой единую двоичную программу, содержащую в себе в откомпилированном виде все необходимые ей драйверы. Хотя уже в MS-DOS
перешли к динамической перегрузке драйверов.
Рассмотрим структуру и функции драйверов.
Важными компонентами программного обеспечения являются
драйверы – программные модули, содержащие процедуры работы с устройствами. Необходимость выделения драйверов в отдельные модули заключается в следующем. Устройство определенного
назначения может иметь самые разные реализации. Разработчик
драйвера хорошо знает программную модель и особенности работы
со своим устройством. У драйверов несколько функций. Наиболее
очевидная функция любого драйвера состоит в обработке абстрактных запросов чтения и записи от независимого от устройств программного обеспечения, которое расположено над ним. Кроме этого
драйверы должны выполнять некоторые другие функции, например
драйвер при необходимости должен инициализировать устройство.
Ему также может понадобиться управлять регистрацией событий и
управлять энергосбережением. Типичный драйвер начинает работу
с проверки входных параметров. Если они не удовлетворяют определенным требованиям, драйвер возвращает ошибку. В противном
случае драйвер преобразует абстрактные команды в конкретные.
Например, дисковый драйвер может преобразовать линейный номер
блока в физические координаты (головка – дорожка – сектор). Затем
драйвер может проверить, не используется ли данное устройство в
данное время. Если оно занято, запрос может быть поставлен в очередь. Если свободно, проверяется аппаратный статус устройства,
чтобы определить может ли запрос быть обслужен немедленно.
Также может оказаться необходимым включить устройство
или запустить двигатель прежде чем начать процедуру переноса
данных. Как только устройство включено и готово, можно начать
процесс управления им. Управление подразумевает выдачу серии
команд. Именно в драйвере определяется последовательность команд, после чего драйвер начинает записывать команды в регистры
контроллера устройства.
53
После записи каждой команды в контроллер необходимо проверить, принял ли ее контроллер и готов ли принять следующую.
Некоторые контроллеры способны принимать связанные списки
команд, находящиеся в памяти. Они способны сами считывать эти
списки при помощи ОС.
После того, как драйвер передал все команды контроллеру, считывание может развиваться двумя способами. Часто драйвер должен ждать, пока контроллер не выполнит для него работу, поэтому
он блокируется до тех пор, пока прерывание от устройства его не
разблокирует. В другом случае операция завершается без задержек, и драйверу не нужно блокироваться.
Драйвер возвращает информацию о своем состоянии и для вызывающей программы. Если в очереди находятся другие запросы,
один из них может быть выбран и запущен. В противном случае
драйвер блокируется в ожидании следующего запроса. Драйверам не разрешается обращаться к системным вызовам, но им часто
бывает необходимо взаимодействовать с ядром. Обычно разрешается обращение к некоторым системным процедурам. Например,
драйверы обращаются к системным процедурам для выделения
им аппаратно-фиксированных страниц памяти в качестве буфера. А также затем, чтобы вернуть эти станицы обратно ядру. Кроме того, драйверы пользуются вызовами, управляющими таймерами, контроллерами прерываний, контроллерами DMA и тому
подобное.
1.39. Независимое от устройств
программное обеспечение ввода-вывода
Единообразный интерфейс для устройств
Размер блока не зависит от устройства.
Основная задача независимого от устройств программного обеспечения состоит в выполнении функций ввода-вывода, общих для
всех устройств, и предоставление единообразного интерфейса для
программ уровня пользователя.
Единообразный интерфейс драйверов устройств
Главной задачей ОС является обеспечение того, чтобы все
устройства ввода-вывода и их драйверы выглядели бы более или
менее одинаково. Эта задача связана с интерфейсом между драйверами устройств и остальной частью ОС.
54
Сложность в том, что функции драйверов, доступные системе,
отличаются. На практике это означает, что функции ядра, необходимые для драйверов, тоже отличаются.
Принципиально другой подход состоит в том, что у всех драйверов может быть сделан одинаковый или похожий интерфейс. При
этом значительно легче установить новый драйвер при условии, что
он соответствует стандартному интерфейсу. Это также означает,
что программист, пишущий драйвер, знает, что от него требуется,
т.е. какие функции он должен реализовывать и к каким функциям
ядра можно обращаться.
Другой аспект единообразия интерфейса состоит в именовании
устройств ввода-вывода. Независимое от устройств ввода-вывода
программное обеспечение занимается отображением символьных
имен устройств на соответствующие драйверы.
1.40. Буферизация ввода-вывода
Одна из возможных стратегий обработки поступающих символов
от входных устройств состоит в обращении процессов пользователя
к системному вызову <read> и блокировки в ожидании отдельного
символа. Каждый прибывающий символ выглядит как прерывание.
Процедура обработки прерываний передает символ пользовательскому процессу и разблокирует его. Поместив куда-нибудь полученный символ, процесс читает следующий и опять блокируется. Недостаток такого подхода заключается в том, что процесс пользователя
должен быть активизирован при прибытии каждого символа, что
неэффективно. Более эфективно решение, когда пользовательский
процесс предоставляет буфер размером в n символов в пространстве
пользователя, после чего выполняет чтение n символов.
Процедура обработки прерываний помещает приходящие символы в буфер, пока он не заполнится. Затем активизируется процесс пользователя. Но у такого подхода есть и недостаток: если
слишком много процессов начнут фиксировать свои страницы в памяти, то <pull> (общее количество) доступных страниц уменьшится, в результате чего уменьшится производительность. Третий подход состоит в создании буфера, в который обработчик прерываний
будет помещать поступающие символы в ядре. Когда этот буфер
заполнится, извлекается страница с буфером пользователя и содержание буфера помещается туда за одну операцию. Такая схема намного эффективнее.
55
Вопросы к первому разделу
1. Перечислите функциями ОС по управлению памятью в мультипрограммной системе.
2. Что используется для идентификации переменных и команд
на разных этапах жизненного цикла программы?
3. Является ли диапазон возможных адресов виртуального пространства у всех процессов одираковым?
4. Почему совпадение виртуальных адресных пространств разных процессов не приводит к конфликтам?
5. Чем определяется максимальное значение виртуального
адресного пространства?
6. Что такое образ процесса?
7. В каких случаях выполняются переходы от прикладного кода
к кодам ОС при выполнении процесса?
8. Какая часть виртуального адресного пространства заменяется
при смене активного процесса?
9. Содержит ли виртуальное адресное пространство части?
10. Какие модули ОС размещаются в невытесняемой области системной части виртуальной памяти?
11. Перечислите функции, выполняемые ОС при распределении
памяти динамическими разделами.
12. Какие задачи решаются при виртуализации оперативной памяти?
13. Какие задачи выполняет подсистема управления памятью
при распределении фиксированными разделами?
14. На чем основан механизм виртуальной памяти?
15. Образуют ли сегменты ОС активного процесса единое виртуальное адресрое пространство?
16. Кто назначает виртуальные и физические адреса?
17. Является ли часть виртуального адресного пространства
процесса, отводимая под сегменты ОС, единой для всех процессов?
18. Какая информация из таблицы страниц устанавливается аппаратными схемами процессора?
19. Куда загружается адрес таблици страниц процесса при его
активизациии?
20. Перечислите этапы страничного прерывания.
21. Что происходит после того как выбрана страница, которая
должна покинуть ОЗУ?
22. В каком случае происходит страничное прерывание?
23. Какая информация содержится в дескрипторе страниц?
56
24. Что такое базовый адрес?
25. Какие сведения содержатся в таблице сегментов?
26. Какие недостатки у сегментного распределения памяти?
27. У каждого процесса своя таблица страниц?
28. Что содержится в таблице сегментов при сегментно-страничном распределении памяти?
29. Что позволяет наличие базового виртуального адреса сегмента?
30. Являются ли библиотеки процедур частью системы вводавывода?
31. Какие средства обеспечения ввода-вывода есть в пространстве пользователя?
32. Могут ли сегменты ОС разделять виртуальное адресное пространство с прикладными сегментами активного процесса?
33. Чем определяется максимальный размер сегмента?
34. На что указывает вектор прерываний?
35. Совпадают ли виртуальные и физические адреса процесса по
объему и содержанию?
36. Подвержены ли коды ядра ОС страничному вытеснению?
37. От чего зависит вероятность обнаружения данных в КЭШе?
38. При каких условиях происходит страничное прерывание?
39. Как соотносятся размеры виртуального и физического адресного пространства?
40. Назовите функции ОС по управлению памятью?
41. Чем определяется максимальный размер виртуального
адресного пространства?
42. Чем определяется минимальный размер виртуального адресного пространства?
43. В чем разница между виртуальной памятью и свопингом?
44. Перечислите известные вам типы адресов
57
2. UNIX-ПОДОБНЫЕ ОС
Взаимодействие операционной системы UNIX с аппаратурой и
вспомогательными средствами пользователя удобно рассматривать как пирамиду (рис. 15) [3]. На нижнем уровне располагается
аппаратное обеспечение, состоящее из цен­трального процессора,
памяти, дисков и других устройств. Операционная система UNIX
управляет аппаратным обеспечением и предоставляет программам
интерфейс системных вызовов. Эти системные вызовы позволяют
программам создавать про­цессы, файлы и прочие ресурсы, а также
управлять ими.
Программы обращаются к системным вызовам, помещая аргументы в регист­ры центрального процессора (или иногда в стек) и
выполняя команду эмулиро­ванного прерывания для переключения
из пользовательского режима в режим ядра и передачи управления
операционной системе UNIX. Поскольку на языке высокого уровня невозможно написать программу эмулированного прерывания,
этим занимаются библиотечные функции, по одной на системный
вызов. Эти процедуры написаны на ассемблере, но они могут вызываться из программ, написанных на С. Каждая такая процедура помещает аргументы в нужное место и выполняет команду эмулированного прерывания TRAP. Таким образом, чтобы обратиться к системному вызову read, программа, написанная например, на языке
С++, должна вызвать библиотечную процедуру read. Важную роль
в UNIX-подобных ОС играет стандарт P1003 (Portable Operating
System-4), который часто называют POSIX. Этот стандарт по существу определяет интерфейс переносимой операционной системы. Другими словами, POSIX объединяет библиотечные процедуры, соответствующие системным вызовам, их параметры, а также
Прикладные
программы
пользователей
Обслуживающие программы
Стандартная оболочка
ОС UNIX
Аппаратное обеспечение
Рис. 15
58
указывает, что они должны делать и какой результат возвращать.
В этом стандарте не упоминаются сами системные вызовы, но даются правила их описания и использования. Помимо операционной системы и библиотеки системных вызовов, все вер­сии UNIX
содержат большое количество стандартных программ, некоторые
из них описываются стандартом POSIX 1003.2, тогда как другие
могут различаться в разных версиях системы UNIX. К таким программам относятся командный процессор (оболочка), компиляторы, редакторы, программы обработки текста и утилиты для работы
с файлами. Именно эти программы и запускаются пользователем с
терминала. Таким образом, в UNIX-подобной операционной системе имеется три интерфейса: интерфейс системных вызовов, интерфейс библиотечных функций и интерфейс, образованный набором
стандартных обслуживающих программ.
2.1. Структура ядра операционной системы UNIX
Аппаратные и эмулированные
прерывания
Именование Отобра­жение Страничные
файлов
процессов прерывания
Дисциплины
Обработанканалов связи ный телетайп
Телетайп
Упраление
терминалом
Сокеты
Модули
сетевых
протоколов
Файловая
система
Виртуальная
память
Маршрутизация
Буферный
КЭШ
Страничный
КЭШ
Символь- Драйверы
ные устрой- сетевых
ства
устройств
Драйверы
дисковых устройств
Обра­
ботчик
сигналов
Создание
и завершение
процессов
Системные вызовы
Планировщик
процессов
Диспетчериза­
ция процессов
Аппаратура
Рис. 16
На рис. 16 приведена упрощенная структура, дающая представление о составе ядра UNIX-подобной ОС.
Нижний уровень ядра состоит из драйверов устройств и процедуры диспетче­ризации процессов. Все драйверы системы UNIX де59
лятся на два класса: драйверы символьных устройств и драйверы
блочных устройств. Основное различие между этими двумя классами устройств заключается в том, что на блочных устройствах разрешается операция поиска, а на символьных нет. Технически сетевые устрой­ства представляют собой символьные устройства, но они
обрабатываются иначе, поэтому на схеме они выделены отдельно.
Диспетчеризация процессов производится при возникновении прерывания. При этом низкоуровневая программа останавливает выполнение ра­ботающего процесса, сохраняет его состояние в таблице процессов ядра и запус­кает соответствующий драйвер. Кроме
того, диспетчеризация процессов произво­дится также, когда ядро
завершает свою работу и пора снова запустить процесс пользователя. Программа диспетчеризации процессов написана на ассемблере
и представляет собой отдельную от процедуры планирования программу. Уровень выше сетевых драйверов вы­полняет в том чиcле и
функции поддержки стека TCP/IP. Боль­шинство систем UNIX содержат в своем ядре модули этого стека, что дает возможность организовать программную поддержку статической маршрутизации.
Над уровнем маршрутизации расположены программы, реализующие прикладные сетевые протоколы и системы, такие как FTP,
Telnet, SNMP, DNS, NFS, rlogin и т.п. Над сетевыми прикладными протоколами располагается интерфейс сокетов, позволяющий
про­граммам создавать "почтовые ящики" для сетей и протоколов
по определенным правилам. Для использования сокетов пользовательские программы должны установить и описать параметры
которые их "интересуют" на низлежащих уровнях. Над дисковыми драйверами располагаются буферный кэш и страничный кэш
файловой системы. Над буферным кэшэм располагаются файловые
системы. Большинством сис­тем UNIX поддерживаются несколько
файловых систем, включая быструю фай­ловую систему Беркли,
журнальную файловую систему, а также различные виды файловых систем System V. Все эти файловые системы совместно используют общий буферный кэш. Выше файловых систем помещается
именование файлов, управление каталогами, управление жесткими и символьными связями, а также другие средства файловой системы, одинаковые для всех файловых систем. Над страничным кэшэм располагается система виртуальной памяти. В ней содержится
вся логика работы со страницами, например алгоритм замещения
страниц. Поверх него находится программа отображения файлов
на виртуальную память и высо­коуровневая программа управления
страничными прерываниями. Эта программа решает, что нужно
60
делать при возникновении страничного прерывания. Сначала она
проверяет допустимость обращения к памяти и, если все в порядке,
определя­ет местонахождение требуемой страницы и то, как она может быть получена. Последний столбец имеет отношение к управлению процессами. Над диспетчером располагается планировщик
процессов, выбирающий процесс, который должен быть запущен
следующим. Если потоками управляет ядро, то управление потоками также помещается здесь, хотя в некоторых системах UNIX
управление потоками вынесено в пространство пользователя. Над
планировщиком располо­жена программа для обработки сигналов и
отправки их в требуемом направлении, а также программа, занимающаяся созданием и завершением процессов. Верхний уровень
представляет собой интерфейс системы. Слева располагается интерфейс системных вызовов. Все системные вызовы поступают сюда и
направ­ляются одному из модулей низших уровней в зависимости
от природы системно­го вызова. Правая часть верхнего уровня представляет собой вход для аппаратных и эмулированных прерываний, включая сигналы [1], страничные прерывания, разно­образные
исключительные ситуации процессора и прерывания ввода-вывода.
2.2. Загрузка UNIX
Детали процесса загрузки UNIX-подобной операционной системы варьируются от системы к системе. Но в общих чертах этот процесс выглядит так. Когда компьютер включается, в память считывается и исполняется первый сектор (главная загру­зочная запись)
загружаемого диска. Этот сектор содержит небольшую (512-бай­
товую) программу, загружающую автономную программу под названием boot с за­грузочного устройства, как правило, с жесткого
диска. Программа boot сначала копирует саму себя в фиксированный адрес памяти в старших адресах, чтобы осво­бодить нижнюю
память для операционной системы. Загрузившись, программа boot
считывает корневой каталог с загрузочного устройства. Чтобы сделать это, она должна понимать формат файловой систе­мы и каталога. Затем она считывает ядро операционной системы и передает
ему управление. На этом программа boot завершает свою работу,
после чего уже рабо­тает ядро системы. Начальная программа ядра
написана на ассемблере и является в значительной мере машиннозависимой. Как правило, эта программа устанавливает указатель
стека, определяет тип центрального процессора, вычисляет коли61
чество имеюще­гося в наличии ОЗУ, устанавливает запрет прерываний, разрешение работы диспетчера памяти, наконец, вызывает
процедуру main, написанную на языке С, чтобы запустить основ­
ную часть операционной системы.
Программа на языке С также должна проделать значительную
работу по ини­циализации, но эта инициализация скорее логическая, нежели физическая. Она начинается с того, что выделяет память под буфер сообщений, что должно помочь решению проблем с
загрузкой системы. По мере выполнения инициализа­ции в этот буфер записываются сообщения, информирующие о том, что происхо­
дит в системе. В случае неудачной загрузки их можно прочитать оттуда с помощью специальной программы. Затем выделяется память
для структур данных ядра. Большинство этих структур имеют фиксированный размер, но размер некоторых из них, например размер
буферного кэша и некоторых структур таблиц управления страницами памяти, зависит от доступного объема оперативной памяти.
Затем операционная система начинает определение конфигурации
компьюте­ра. Операционная система считывает файлы конфигурации, в которых сообщает­ся, какие типы устройств ввода-вывода могут присутствовать, и проверяет, какие из устройств действительно
присутствуют. Если проверяемое устройство отвеча­ет, оно добавляется к таблице подключенных устройств. Если устройство не отвечает, оно считается отсутствующим и в дальнейшем игнорируется. Как только список устройств определен, операционная система
должна найти драйверы устройств. На этом этапе различные версии
UNIX могут вести себя по разному. В частности, система 4.4BSD не
может динамически загружать драйверы устройств, поэтому любое
устройство ввода-вывода, чей драйвер не был стати­чески скомпонован с ядром, не может использоваться. Некоторые другие версии
UNIX, как, например, Linux, напротив, могут динамически загружать драйверы (как это могут делать все версии MS-DOS и Windows).
Главный аргумент в пользу динамичес­кой загрузки заключается в том, что клиентам с различными конфигурациями может быть
поставлен один и тот же двоичный файл, который автоматически
за­грузит необходимые ему драйверы. Если драйверы не могут загружаться динамически, такой сценарий предотвращает установку в
ядро неотлаженной или вредоносной программы. Более того, в больших системах конфигурация аппаратуры точно известна уже во время компиляции и компоновки операционной системы. Изменения
производятся довольно редко, поэтому перекомпоновка системы при
добавлении нового устройства не представ­ляет проблемы.
62
Когда загружающаяся операционная система определила конфигурацию аппаратного обеспечения, она должна загрузить процесс 0, установить его стек и запустить этот процесс. Процесс 0 продолжает инициализацию, выполняя такие задачи, как программирование таймера реального времени, монтирова­ние корневой файловой системы и создание процесса 1 (init) и страничного демона
(процесс 2). Процесс init проверяет свои флаги, в зависимости от
которых он запускает операционную систему либо в однопользовательском, либо в многопользовательском режиме. В первом случае
он создает процесс, выполняющий оболочку, и ждет, когда тот завершит свою работу. Во втором случае процесс init создает процесс,
испол­няющий сценарий оболочки инициализации системы /etc/
rc, который может вы­полнять проверку непротиворечивости файловой системы, монтировать допол­нительные файловые системы,
запускать демонов и т.д. Затем он считывает специальный файл, в
котором перечисляются терминалы и некоторые их свойства. Для
каж­дого разрешенного терминала он создает копию самого себя,
которая затем испол­няет программу getty. Программа getty устанавливает для каждой линии (некоторые из них могут быть, например, модемами) скорость линии, после чего выводит на терминале
при­глашение к входу в систему:
login:
После этого программа getty пытается прочитать имя пользователя, введенное с клавиатуры. Когда пользователь садится за
терминал и вводит свое имя, програм­ма getty завершает свою работу выполнением программы регистрации /bin/login. После этого
программа login запрашивает у пользователя его пароль, зашифро­
вывает его и сравнивает с зашифрованным паролем, хранящимся в
файле паро­лей /etc/passwd. Если пароль введен верно, программа
login вместо себя запускает оболочку пользователя, которая ждет
первой команды. Если пароль введен невер­но, программа login просто еще раз спрашивает имя пользователя.
2.3. Оболочка UNIX
У многих версий системы UNIX имеется графический интерфейс
пользователя, схожий с популярными интерфейсами, примененными на компьютере Macintosh и впоследствии в системе Windows.
Однако многие программисты до сих пор предпочитают интерфейс
63
командной строки, называемый оболочкой (shell). По­добный интерфейс значительно быстрее в использовании, существенно мощнее,
проще расширяется. Хотя система UNIX полностью поддерживает
графическое окружение (X Windows), часто про­граммисты создают
несколько консольных окон и действуют так, как если бы у них был
десяток алфавитно-цифровых терминалов, на каждом из которых
запущена оболочка. Когда оболочка запускается, она инициализируется, а затем печатает на экране символ приглашения к вводу
(обычно это знак доллара или процента) и ждет, ког­да пользователь
введет командную строку. После этого, оболочка извлекает из нее команду и анализирует ее. Если ошибки нет, то команда выполняется,
(например, выполняется программа) а затем, опять следует приглашение к работе. Здесь важно под­черкнуть, что оболочка представляет собой обычную пользовательскую програм­му. Все, что ей нужно,
– это способность ввода с терминала и вывода на терминал, а также
возможность запускать другие программы. У команд оболочки могут быть аргументы, которые передаются запускаемой программе в
виде текстовых строк. Например, командная строка ср sor dst запускает программу ср с двумя аргументами sor и dst. Эта программа
интерпре­тирует первый аргумент как имя существующего файла.
Она копирует этот файл и называет его копию dst.
Программа вроде оболочки не должна открывать терминал, чтобы прочитать с него или вывести на него строку. Вместо этого запускаемые программы автома­тически получают доступ к файлу,
называемому стандартным устройством вво­да (standard input), и
к файлу, называемому стандартным устройством вывода (standard
output), а также к файлу, называемому standard error (стандартное уст­ройство для вывода сообщений об ошибках). По умолчанию
всем трем устрой­ствам соответствует терминал, т.е. клавиатура
для ввода и экран для вывода. Многие программы в системе UNIX
читают данные со стандартного устройства ввода и пишут на стандартное устройство вывода.
Стандартные ввод и вывод также можно перенаправить, что является очень полезным свойством. Для этого используются символы "<" и ">" соответственно.
Разрешается их одновременное использование в одной командной строке. Например, команда srt <in >out заставляет программу srt взять в качестве входного файл in и направить вывод в файл
out. Поскольку стандартный вывод сообщений об ошибках не был
перена­правлен, все сообщения об ошибках будут печататься на
экране.
64
2.4. Процессы в системе UNIX
Каждый процесс в UNIX запускает одну программу и изначально получает один поток управления. Другими словами, у процесса
есть счетчик команд, указывающий на следующую исполняемую
команду процессора. Боль­шинство версий UNIX позволяют процессу после того, как он запущен, создавать дополнительные потоки.
UNIX представляет собой многозадачную систему, так что несколько незави­симых процессов могут работать квази одновременно.
У каждого пользователя может быть одновременно несколько активных процессов, так что в большой системе могут одновременно
работать сотни и более процессов. Действительно, на большинстве
однопользовательских рабочих станций, даже когда пользователь
куда-либо отлучается, работают десятки фоновых процессов, называемых демо­нами. Они запускаются автоматически при загрузке
системы. Типичным демоном является cron daemon. Он просыпается раз в минуту, про­веряя, не нужно ли чего сделать. Если у него
есть работа, он ее выполняет и от­правляется спать дальше. Этот
демон позволяет планировать в системе UNIX активность на минуты, часы, дни и даже месяцы вперед. Другие демоны управляют
входящей и исходящей элек­тронной почтой, очередями на принтер, проверяют, достаточно ли еще осталось свободных страниц
памяти и т.д. Демоны реализуются в системе UNIX довольно просто, так как каждый из них представляет собой отдельный процесс,
независи­мый от всех остальных процессов. Процессы создаются в
операционной системе UNIX чрезвычайно несложно. Системный
вызов fork создает точную копию исходного процесса, называемого родительским процессом. Новый процесс называется дочерним
процессом. У ро­дительского и у дочернего процессов есть свои собственные образы памяти. Если родительский процесс впоследствии
изменяет какие-либо свои переменные, из­менения остаются невидимыми для дочернего процесса, и наоборот. Открытые файлы
совместно используются родительским и дочерним процес­сами.
Это значит, что если какой-либо файл был открыт до выполнения
системно­го вызова fork, он останется открытым в обоих процессах
и в дальнейшем. Изме­нения, произведенные с этим файлом, будут
видимы каждому процессу. Такое поведение является единственно разумным, так как эти изменения будут также видны любому
другому процессу, который тоже откроет этот файл. Тот факт, что
образы памяти, переменные, регистры и все остальное у родитель­
ского процесса и у дочернего идентично, приводит к затруднению.
65
Как процессам узнать, который из них должен исполнять родительскую програм­му, а который дочернюю? Эта проблема решается
просто: системный вызов fork возвращает дочернему процессу число 0, а родительскому – отличный от нуля PID (Process IDentifier –
идентификатор процесса) дочернего процесса. Таким образом, оба
процесса могут проверить возвращаемое значение и действовать,
например, так, как это реализовано в нижеприведенном листинге
программы создания процесса.
рid = fork( ); /* если fork завершился успешно, pid > 0 в родительском процессе*/
if pid < 0) {
handle_егrог(); /* fork потерпел неудачу (например, память или
какая-либо таблица переполнена) */
} е1se if (рid > 0) {
/* здесь располагается родительская программа. */
} е1se {
/* здесь располагается дочерняя программа. */
}
Процессы распознаются по своим PID-идентификаторам. При
создании процесса его РID выдается родителю нового процесса.
Если дочерний процесс желает узнать свой РID, он может воспользоваться системным вызовом getpid. Идентификаторы процессов
используются различным образом. Например, когда дочерний
процесс завершается, его РID также выдается его родителю. Это
может быть важно, так как у родительского процесса может быть
много дочерних процессов. Поскольку у дочерних процессов также могут быть дочерние процессы, исходный процесс может создать целое дерево родственников. В системе UNIХ процессы могут
общаться друг с другом с помощью разновидности обмена сообщениями. Можно создать канал между двумя процессами, в который
один процесс может писать поток байтов, а другой процесс может
его читать. Эти каналы иногда называют трубами. Синхронизация
процессов достигается путем блокирования процесса при попытке
прочитать данные из пустого канала. Когда данные появляются в
канале, процесс разблокируется. Когда оболочка читает, например, строку Srt < f | head. Она создает два процесса, srt и head, а
также устанавливает между ними канал таким образом, что стандартный поток вывода программы srt соединялся со стандартным
потоком ввода программы head. При этом все данные, формируемые программой srt, попадают напрямую программе head, для чего
66
не требуется временного файла. Если канал переполняется, система приостанавливает работу программы srt, пока программа head
не удалит из него хоть сколько-нибудь данных. Процессы также
могут общаться другим способом: при помощи программных прерываний. Один процесс может послать другому так называемый
сигнал. Процессы могут сообщить системе, какие действия следует предпринимать, когда придет сигнал. У процесса есть выбор:
проигнорировать сигнал, перехватить его или позволить сигналу
уничтожить процесс (действие по умолчанию для большинства сигналов). Если процесс выбрал перехват посылаемых ему сигналов,
он должен указать процедуры обработки сигналов. Когда сигнал
прибывает, управление передается обработчику. Когда процедура
обработки сигнала завершает свою работу, управление снова возвращается в то место процесса, в котором она находилась, когда
пришел сигнал. Обработка сигналов аналогична обработке аппаратных прерываний ввода-вывода. Процесс может посылать сигналы только членам его группы процессов. Процесс может послать
сигнал сразу всей группе за один системный вызов.
2.5. Управление процессами в UNIХ
У каждого процесса есть пользовательская часть, в которой работает программа пользователя. Однако когда один из потоков обращается к системному вызову, происходит эмулированное прерывание с переключением в режим ядра. После этого поток начинает
работу в контексте ядра, с отличной картой памяти и полным доступом к ресурсам компьютера. С этого момента у него есть свой стек
и счетчик команд. Это важно, так как системный вызов может блокироваться на полпути, например, ожидая завершения дисковой
операции. При этом счетчик команд и регистры сохраняются таким
образом, чтобы поток можно было восстановить в режиме ядра.
Ядро поддерживает две структуры данных, обеспечивающих
их выполнение: это таблица процессов и структура пользователя.
Таблица процессов распологается в ОЗУ резидентно и обеспечивает переключение с процесса на процесс. В этой таблице содержится
информация обо всех процессах, в том числе и тех, которых в данный момент нет в ОЗУ. Структура пользователя загружается в ОЗУ,
лишь в том случае, если процесс выполняется.
Информация в таблице процессов может быть разделена на следующие категории:
67
Параметры планирования. Они нужны для выбора процесса которому будет передано управление и содержат информацию о использованном процессорном времени и времени, проведенном в
ожидании;
образ памяти. Это указатели на сегменты программы, данных и
стека и соответствующие таблицы. Если процесса в данный момент
нет в памяти, то здесь содержится информация о том, как его найти
на диске;
сигналы. Это указатели на то, какие сигналы заблокированы,
перехватываются и игнорируются а также находятся в процессе
доставки;
дополнительная информация. Это PID процесса и его родителя, идентификаторы пользователя и группы, текущее состояние и
ожидаемые процессом события.
Хотя процессу, выгруженному на диск, можно послать сигнал,
такой процесс не может читать файл, поэтому информация о сигналах содержится в таблице процессов резидентно, даже если процесс не выполняется. В тоже время описатели файлов находятся в
структуре пользователя и загружаются в ОЗУ вместе с процессом.
Данные, хранящиеся в структуре пользователя, содержат следующую информацию:
регистры процессора. Cодержимое этих регистров сохраняется
здесь, если происходит прерывание с переходом в режим ядра;
состояние системного вызова. Это информация о текущем системном вызове, включая параметры и результат;
таблица дескрипторов файлов. Эта таблица используется в качестве индекса для определения структуры данных, соответствующей файлу, с которым работает системный вызов;
учетная информация. Указатель на таблицу, учитывающую
процессорное время, использованное процессом в пользовательском и системном режимах. В некоторых системах здесь также
ограничивается процессорное время, которое может использовать
процесс, максимальный размер стека, количество страниц памяти
и т. д.;
стек ядра. Фиксированный стек для использования процессом в
режиме ядра.
Когда выполняется системный вызов fork, вызывающий процесс обращается в ядро и ищет свободную ячейку в таблице процессов, в которую можно записать данные о дочернем процессе. Если
свободная ячейка находится, системный вызов копирует туда информацию из ячейки родительского процесса. Затем он выделяет
68
память для сегментов данных и для стека дочернего процесса, куда
копируются соответствующие сегменты родительского процесса.
Структура пользователя (которая часто хранится вместе с сегментом стека) копируется вместе со стеком. Программный сегмент может либо копироваться, либо использоваться совместно, если он доступен только для чтения. Начиная с этого момента дочерний процесс может быть запущен. Когда пользователь вводит с терминала
команду, например ls, оболочка создает новый процесс, клонируя
свой собственный процесс с помощью системного вызова fork. Новый процесс оболочки затем вызывает системный вызов ехес, чтобы считать в свою область памяти содержимое исполняемого файла
ls. Механизм создания нового процесса довольно прост. Для дочернего процесса создается новая ячейка в таблице процессов, которая
заполняется из соответствующей ячейки родительского процесса.
Дочерний процесс получает PID, затем настраивается его карта
памяти. Кроме того, дочернему процессу предоставляется совместный доступ к файлам родительского процесса. Затем настраиваются регистры дочернего процесса, после чего он готов к запуску.
Семантика системного вызова fork требует, чтобы никакая область
памяти не использовалась совместно родительскими и дочерними процессами. Дочернему процессу выделяются новые таблицы
страниц, но эти таблицы указывают на страницы родительского
процесса, помеченные как доступные только для чтения. Когда
дочерний процесс пытается писать в такую страницу, происходит
прерывание. При этом ядро выделяет дочернему процессу новую
копию этой страницы, к которой этот процесс получает также и доступ записи. Таким образом, копируются только те страницы, в которые дочерний процесс пишет новые данные. Такой механизм называется копированием при записи. При этом экономится память,
так как страницы с программой не копируются. После того как дочерний процесс начинает работу, его программа (копия оболочки)
выполняет системный вызов ехес, задавая имя команды в качестве
параметра. При этом ядро находит и проверяет исполняемый файл,
копирует в ядро аргументы и строки окружения, а также освобождает старое адресное пространство и его таблицы страниц.
2.6. Управление процессами в UNIХ
Рассмотрим теперь системные вызовы UNIX, предназначенные
для управления процессами [4]. Обсуждение системных вызовов
69
проще всего начать с системного вызова fork. Этот системный вызов представляет собой единственный способ создания новых процессов в системах UNIX. Он создает точную копию оригинального
процесса, включая все описатели файлов, регистры и все остальное. После выполнения системного вызова fork исходный процесс
и его копия (родительский процесс и дочерний) идут каждый своим
путем. Сразу после выполнения системного вызова fork: значение
всех соответствующих переменных в обоих процессах одинаково,
но затем изменения переменных в одном процессе не влияет на
переменные другого процесса. Системный вызов fоrk возвращает
значение, равное нулю для дочернего процесса и идентификатор
(PID) дочернего процесса для родительского. Таким образом, два
процесса могут определить, кто из них родитель, а кто дочерний
процесс.
Системные вызовы, относящиеся к процессам
рid=fork() Создать дочерний процесс, идентичный родительскому
рid=waitpid(рid, &statloc, орts) Ждать завершения дочернего
процесса
s=ехесve(name, argv, envp) Заменить образ памяти процесса
ехit(status) Завершить выполнение процесса и вернуть статус
s=sigastion(sig, &set, &old) Определить действие, выполняемое
при приходе сигнала
s=sigreturn(&context) Вернуть управление после обработки сигнала
s=sigprocmask(how, &set, &old) Исследовать или изменить маску сигнала
s=sigpending(set) Получить или установить блокированные сигналы
s=kill(pid, sig) Послать сигнал процессу
s=sigsuspend(sigmask) Заменить маску сигнала и приостановить
процесс
residual=alarm(seconds) Установить будильник
s=pause() Приостановить выполнение процесса до следующего
сигнала
В большинстве случаев после системного вызова fork дочернему процессу потребуется выполнить программу, отличающуюся от
программы, выполняемой родительским процессом. Рассмотрим
работу оболочки. Она считывает команды с терминала, выполняет
введенную команду, затем ждет окончания работы дочернего про70
цесса, после чего считывает следующую команду. Для ожидания
завершения дочернего процесса родительский процесс обращается
к системному вызову waitpid. У этого системного вызова три параметра. В первом параметре указывается PID процесса, завершение
которого ожидается. Если вместо идентификатора процесса указать число -1, то в этом случае системный вызов ожидает завершения любого дочернего процесса. Второй параметр представляет собой адрес переменной, в которую записывается статус завершения
дочернего процесса (нормальное или ненормальное завершение, а
также возвращаемое на выходе значение). Третий параметр определяет, будет ли обращающийся к системному вызову waitpid процесс блокирован до завершения дочернего процесса или сразу получит управление после обращения к системному вызову. В случае
оболочки дочерний процесс должен выполнить команду, введенную пользователем. Он выполняет это при помощи системного вызова ехес, который заменяет весь образ памяти содержимым файла,
указанным в первом параметре системного вызова. Упрощенный
вариант оболочки, иллюстрирующей использование системных
вызовов fork, waitpid и ехес, приведен ниже.
while (TRUE) { /* вечный цикл */
type_promt( ); /* вывести приглашение ко вводу */
read command(command, params); /* прочитать с клавиатуры
строку */
рid = fork( ); /* ответвить дочерний процесс */
if (pid<0) {
рrint("Создать процесс невозможно"); /* ошибка */
continue; /* следующий цикл */
}
If (pid != 0) {
Waitpid (-1, &status, 0); /* родительский процесс ждет завершения дочернего процесса */
} else {
execve(command, params, 0); /* дочерний процесс выполняет работу */
}
}
В общем случае у системного вызова три параметра: имя исполняемого файла, указатель на массив аргументов и указатель на
массив строк окружения. Различные варианты системного вызова
exec, включая процедуры execl, execv, execle и execve, позволяют
71
пропускать некоторые параметры или задавать их другими способами. Все эти процедуры обращаются к вызову exec, процедуры с
таким именем нет.
Рассмотрим простой системный вызов ехit, который процессы
должны использовать, заканчивая исполнение. У него есть один
параметр, статус выхода (от 0 до 255), возвращаемый родительскому процессу в переменной status системного вызова waitpid. Младший байт переменной status содержит статус завершения, равный
0 при нормальном завершении или код ошибки при аварийном завершении. Например, если родительский процесс выполняет оператор n = waitpid(-1, &status. 0); он будет приостановлен до тех пор,
пока не завершится какой-либо дочерний процесс. Если дочерний
процесс завершится со, скажем, значением статуса, равным 4, в качестве параметра библиотечной процедуры ехit, то родительский
процесс получит РID дочернего процесса и значение статуса, равное
0х0400 (0х означает в программах на языке С шестнадцатеричное
число). Младший байт переменной status относится к сигналам,
старший байт представляет собой значение, задаваемое дочерним
процессом в виде параметра при обращении к системному вызову
ехit. Если процесс уже завершил свою работу, а родительский процесс не ожидает этого события, то дочерний процесс переводится
в так называемое состояние зомби, т.е. приостанавливается. Когда
родительский процесс наконец обращается к библиотечной процедуре waitpid, дочерний процесс завершается. Несколько системных
вызовов относятся к сигналам, используемым различными способами. Например, если пользователь по ошибке велит текстовому
редактору отобразить содержание очень длинного файла, а затем
осознает свою ошибку, то потребуется некоторый способ прервать
работу редактора. Обычно для этого пользователь нажимает специальную клавишу (например, DEL или CTRL+С), в результате чего
редактору посылается сигнал. Редактор перехватывает сигнал и
останавливает вывод. Чтобы заявить о своем желании перехватить
тот или иной сигнал, процесс может воспользоваться системным
вызовом sigation. Первый параметр этого системного вызова – сигнал, который требуется перехватить. Второй параметр представляет собой указатель на структуру, в которой хранится указатель на
процедуру обработки сигнала вместе с различными битами и флагами. Третий параметр указывает на структуру, в которой система
возвращает информацию о текущем обрабатываемом сигнале, на
случай, если позднее его нужно будет восстановить. Обработчик
сигнала может выполняться сколь угодно долго. Однако на прак72
тике обработка сигналов не занимает много времени. Когда процедура обработки сигнала завершает свою работу, она возвращается к
той точке, в которой ее прервали.
Системный вызов sigaction может также использоваться для
игнорирования сигнала или чтобы восстановить действие по умолчанию, заключающееся в уничтожении процесса. Нажатие на клавишу DEL не является единственным способом послать сигнал.
Системный вызов kill позволяет процессу послать сигнал любому
родственному процессу. Выбор названия для данного системного
вызова (kill – убить, уничтожить) не особенно удачен, так как по
большей части он используется процессами не для уничтожения
других процессов, а, наоборот, в надежде, что этот сигнал будет
перехвачен и обработан соответствующим образом. Во многих приложениях реального времени бывает необходимо прервать процесс
через определенный интервал времени, чтобы заставить его сделать
что-либо, например переслать повторно возможно потерянный пакет по ненадежной линии связи. Для обработки данной ситуации
существует системный вызов а1аrm (будильник). Параметр этого
системного вызова задает временной интервал, по истечении которого процессу посылается сигнал SIGALARM.
2.7. Системные вызовы управления потоками
В первых версиях ОС UNIX потоков не было. Поэтому применяли различные пакеты поддержки потоков, однако написать переносимую программу при этом стало почти невозможно. В конце концов, системные вызовы, используемые для управления потоками,
были стандартизированы в виде части стандарта POSIX (Р1003.1с).
В стандарте POSIX не указывается, должны ли потоки реализовываться в пространстве ядра или в пространстве пользователя. Преимущество потоков в пользовательском пространстве состоит в том,
что они легко реализуются без необходимости изменения ядра, в то
же время переключение потоков осуществляется эффективно. Недостаток потоков в пространстве пользователя заключается в том,
что если один из потоков заблокируется (например, на операции
ввода-вывода, семафоре или страничном прерывании), все потоки
процесса блокируются. Ядро полагает, что существует только один
поток, и не передает управление процессу потока, пока блокировка
не снимется. Таким образом, системные вызовы, определенные в
стандарте Р1003.1с, были тщательно отобраны так, чтобы потоки
73
могли быть реализованы любым способом. До тех пор пока пользовательские программы четко придерживаются семантики стандарта Р1003.1с, оба способа реализации должны работать корректно.
Когда используется системная реализация потоков, они являются
настоящими системными вызовами. При использовании потоков
на уровне пользователя они полностью реализуются в динамической библиотеке в пространстве пользователя. Например, системный вызов pthread_create создает новый поток. Обращение к этому системному вызову производится следующим образом: err =
pthread_create(&tid, attr, function, arg).
Этот вызов создает в текущем процессе новый поток, в котором
работает программа function, а arg передается этой программе в качестве параметра. Идентификатор нового потока хранится в памяти по адресу, на который указывает первый параметр. С помощью
параметра attr можно задавать для нового потока определенные
атрибуты, такие как приоритет планирования. После успешного
выполнения данного системного вызова в адресном пространстве
пользователя появляется на один поток больше. Поток, выполнивший свою работу и желающий прекратить свое существование,
обращается к системному вызову рthread_exit. Поток может подождать, пока не завершится процесс, обратившись к системному
вызову pthread_join. Если ожидаемый поток уже завершил свою
работу, системный вызов pthread_join выполняется мгновенно.
В противном случае обратившийся к нему поток блокируется. Синхронизация потоков может осуществляться при помощи мьютексов. Как правило, мьютекс охраняет какой-либо ресурс, например
буфер, совместно используемый двумя потоками, чтобы гарантировать, что только один поток в каждый момент времени имеет доступ к общему ресурсу, предполагается, что потоки блокируют (захватывают) мьютекс перед обращением к ресурсу и разблокируют
(отпускают) его, когда ресурс им более не нужен. До тех пор пока
потоки соблюдают данный протокол, состояния состязания можно
избежать. Мьютексы подобны двоичным семафорам, т.е. семафорам, способным принимать только значения 0 и 1. Мьютексы могут
создаваться вызовом pthread_mutex_init и уничтожаться при помощи вызова рthread_mutex_destгоу. Мьютекс может находиться
в одном из двух состояний: блокированный и разблокированный.
Поток может заблокировать мьютекс с помощью вызова рthread_
mutex_lосk. Если мьютекс уже заблокирован, то поток, обратившийся к этому вызову, блокируется. Когда поток, захвативший
мьютекс, выполнил свою работу в критической области, он должен
74
освободить мьютекс, обратившись к вызову рthread_mutex_unlock.
Мьютексы предназначены для кратковременной блокировки, например для защиты совместно используемой переменной. Они не
предназначаются для долговременной синхронизации, например
для ожидания, когда освободится накопитель на ленте или диск.
Для долговременной синхронизации применяются переменные состояния. Эти переменные создаются с помощью вызова pthread_
count_init и уничтожаются вызовом pthread_count_destroy. Переменные состояния используются следующим образом: один поток
ждет, когда переменная примет определенное значение, а другой
поток сигнализирует ему изменением этой переменной. Например,
обнаружив, что нужный ему накопитель занят, поток может обратиться к вызову pthread_count_wait, задав в качестве параметра
адрес переменной, которую все потоки согласились связать с накопителем или диском. Когда поток, наконец, освободит внешнее
устройство, он обращается к вызову pthread_count_signal, чтобы
изменить переменную состояния и тем самым сообщить ожидающим потокам, что внешнее устройство свободно. Если ни один поток
в этот момент не ждет, когда освободится это внешнее устройство,
сигнал просто теряется. Другими словами, переменные состояния
не считаются семафорами. С потоками, мьютексами и переменными состояния также определены несколько других операций.
2.8. Сигналы
Сигналы, требуемые стандартом POSIX, приведены ниже [5].
В большинстве систем UNIX также имеются дополнительные сигналы, но программы, использующие их, могут оказаться непереносимыми на другие версии UNIX.
Сигналы, требуемые стандартом POSIX:
SIGАВRТ Посылается, чтобы прервать процесс и создать дамп
памяти
SIGАLRM Истекло время будильника
SIGEРЕ Произошла ошибка при выполнении операции с плавающей точкой (например, деление на 0)
SIGHUР Модем повесил трубку на телефонной линии, использовавшейся процессом
SIGILL Пользователь нажал клавишу DEL, чтобы прервать процесс
75
SIGQUIТ Пользователь нажал клавишу, требуя прекращения
работы процесса с созданием дампа памяти
SIGKILL Посылается, чтобы уничтожить процесс (не может игнорироваться или перехватываться)
SIGРIРЕ Процесс пишет в канал, из которого никто не читает
SIGSEGV Процесс обратился к неверному адресу памяти
SIGTERM Вежливая просьба процессу завершить свою работу
SIGUSR1 Может быть определено приложением
SIGUSR2 Может быть определено приложением
Иногда бывает необходимо прервать процесс на определенное
время, а затем заставить его сделать что-либо, например повторно
отослать в сеть пакет, возможно, не дошедший до адресата. Для обработки подобных ситуаций имеется системный вызов alarm. Параметр этого системного вызова задает временной интервал, по истечении которого процессу посылается сигнал SIGALRM. У процесса в каждый момент времени может быть только один будильник
(alarm) c. Например, если процесс обращается к системному вызову аlarm с параметром 10 с, а 5 с спустя снова обращается к нему с
параметром 20 с, то он получит только один сигнал через 20 с после второго системного вызова. Первый сигнал будет отменен вторым обращением к системному вызову а1аrm. Если параметр системного вызова а1агm равен нулю, то такое обращение отменяет
любой сигнал будильника. Если сигнал будильника не перехватывается, то действие по умолчанию заключается в уничтожении
процесса.
Вопросы ко второму разделу
1. C какой концепцией тесно связано понятие сигналов? Приведите примеры использования сигналов.
2. Целесообразно ли забирать у зомби процесса память?
3. Почему процесс может послать сигнал только членам своей
группы?
4. Какие процессы обладают более высоким приоритетом – демоны или интерактивные процессы?
5. Для чего в таблице процессов для каждого процесса хранится
PID родительского процесса?
6. Почему начальный загрузчик, находящийся в нулевом секторе диска, не используется напрямую для загрузки ОС?
7. Может ли страничное прерывание привести к завершению
процесса, его вызвавшего?
76
8. Как после выполнения системного вызова fork определить,
какой процесс является дочерним, а какой родительским?
9. Перечислите параметры системного вызова waitpid.
10. Перечислите преимущества и недостатки использования потоков.
11. Для чего предназначены мьютексы?
77
3. ЗАДАНИЯ И ПРИМЕРЫ
3.1. Описание некоторых команд
и системных вызовов linux
Команда top
Команда top показывает список работающих в данный момент
процессов и важную информацию о них, включая использование
ими памяти и процессора. Этот список интерактивно формируется
в реальном времени.
Основные поля вывода команды top:
Поле
Описание
PID
PPID
USER
GROUP
S
PR
N1
Идентификатор процесса
Идентификатор родительского процесса
Идентификатор пользователя, запустившего процесс
Идентификатор группы, которой принадлежит процесс
Состояние процесса
Приоритет процесса
Относительный приоритет процесса
Количество процессорного времени, которое использует
TIME+, TIME
процесс с момента своего запуска
Полный объем виртуальной памяти, которую занимает
VIRT
процесс
Объем резидентной виртуальной памяти, которую занимаRES
ет процесс
Объем общей виртуальной памяти, которую использует
SHR
процесс
SWAP
Объем виртуальной памяти процесса, выгруженной на диск
%CPU
Процент использования общего процессорного времени
%MEM
Процент использования доступной физической памяти
CMD
Команда, использованная для создания процесса
Интерактивные команды, которые можно использовать в top
Команда
Пробел
h
k
n
u
M
78
Описание
Немедленно обновить содержимое экрана
Вывести справку о программе
Уничтожить процесс. Программа запрашивает у вас код
процесса и сигнал, который будет ему послан
Изменить число отображаемых процессов. Вам предлагается ввести число
Сортировать по имени пользователя
Сортировать по объёму используемой памяти
P
H
A
Сортировать по загрузке процессора
Переключить отображения потоков
Включить режим отображения альтернативной панели top
Изменить nice-фактор процесса. Программа запрашивает
у вас код процесса и nice-фактор, который будет ему присвоен
R
Утилита Watch
Утилита запускает и следит за программой через фиксированные интервалы времени. Если интервал не задан с помощью опции
-n <seconds>, то команда будет запускаться каждые 2 секунды. Завершить программу можно с помощью нажатия клавиш CTRL-C.
Опция -d подсветит изменения в выводе.
Пример: watch -d ls -l – наблюдение за изменениями в директории.
Команда ps
Выдает информацию об активных процессах.
Командой ps обрабатываются следующие опции. Команда
-e
-d
-N
-d
-l
-f
Описание
Вывести информацию обо всех процессах
Вывести информацию обо всех процессах, кроме лидеров
групп
Отрицание выбора
Все процессы, кроме главных системных процессов сеанса
Генерировать листинг в длинном формате
Генерировать полный листинг
Пример: ps – e – вывести информацию о всех процессах
3.2. Системные вызовы управления
процессами
int execv (const char *FILENAME, char *const ARGV[])
Функция исполняет файл, заданный строкой FILENAME как
новое изображение процесса. Аргумент ARGV представляет собой
массив строк, заканчивающихся нулем, которые используются как
аргумент argv функции main. Последним элементом этого массива
должен быть нулевой указатель. Первый элемент массива – имя
файла программы. Среда нового изображения процесса берется из
переменной environ текущего изображения процесса.
79
exit(int status);
Функция exit() приводит к обычному завершению программы, и
величина status возвращается процессу-родителю.
pid_t fork (void)
Функция создает новый процесс. Прототип функции определен
в заголовочном файле <unistd.h>. Если операция выполнилась
успешно, то выполнение и родительского, и дочернего процессов
продолжается начиная с команды, следующей за fork. В случае дочернего процесса fork возвращает значение 0, а в случае родительского процесса – идентификатор порожденного процесса.
pid_t waitpid (pid_t PID, int *STATUS-PTR, int OPTIONS)
Используется для запроса информации о состоянии дочернего
процесса, идентификатор которого равен PID. Обычно вызывающий процесс приостанавливает выполнение до тех пор, пока дочерний процесс не завершит выполнение. Аргумент PID может принимать и другие значения. Значение –1 означает ожидание любого
процесса, 0 – запрашивает информацию о процессах, находящихся
в той же группе процессов. Если информация о состоянии процесса
доступна немедленно, функция не ожидает окончания выполнения
процесса. Если запрошен статус нескольких процессов, то возвращается статус случайного процесса. Аргумент OPTIONS является
битовой маской, значение которой может формироваться из следующих макросов:
WNOHANG Родительский процесс не должен ожидать завершения дочернего;
WUNTRACED Запрашивается информация об остановленных и
завершенных процессах.
Информация о состоянии сохраняется в объекте, на который
указывает STATUS-PTR, если он не является нулевым указателем.
В случае нормального завершения функция возвращает идентификатор процесса, для которого запрашивается состояние, иначе
возвращается –1. Переменная errno может принимать следующие
значения:
EINTR Выполнение функции прервалось поступившим сигналом;
ECHILD Отсутствуют дочерние процессы или процесс, идентификатор которого передан в функции не является дочерним;
EINVAL Неверное значение аргумента OPTIONS.
Функция sigaction определяет обработчик сигнала. Функция
определена в заголовочном файле <signal.h>.
80
int sigaction(int SIGNUM, const struct sigaction *ACTION, struct
sigaction *OLD_ACTION)
Структура sigaction определяется следующим образом:
struct sigaction {
int sa_flags;
sighandler_t sa_handler;
sigset_t sa_mask;
};
sa_handler используется таким же образом, как и аргумент
ACTION в функции signal; sa_mask задает список сигналов, которые должны быть заблокированы при выполнении обработчика;
sa_flags задает значения флагов, которые влияют на поведение
сигнала.
Аргумент ACTION задает новый обработчик для сигнала, OLD_
ACTION используется для того, чтобы возвратить информацию
об обработчике, ассоциированном с сигналом. Если OLD_ACTION
является нулевым указателям, информация о предыдущем обработчике не возвращается. Если ACTION является нулевым указателем, обработчик сигнала не изменяется. Функция возвращает
следующие значения:
0 в случае успеха;
-1 в случае ошибки;
EINVAL в случае неправильно заданного идентификатора сигнала.
Набор сигналов, который заблокирован в данный момент, называется "маской сигналов". Каждый процесс имеет свою собственную маску сигналов. При создании нового процесса, он наследует
маску сигналов родительского процесса. Для модификации маски
сигналов используется следующая функция:
int sigprocmask(int HOW, const sigset_t *SET, sigset_t *OLDSET)
Аргумент HOW определяет, каким образом изменяется маска
сигналов и может принимать следующие значения:
SIG_BLOCK – сигналы, задаваемые в наборе, блокируются – добавляются к уже существующей маске сигналов;
SIG_UNBLOCK – сигналы, задаваемые в наборе, разблокируются – удаляются из уже существующей маски сигналов процесса;
SIG_SETMASK устанавливает набор сигналов для процесса, старое содержимое маски игнорируется.
Аргумент OLDSET используется для возврата старого содержимого маски сигналов процесса. Функция возвращает 0 в случае
81
успеха и –1 в противном случае. При возникновении ошибки переменная errno может принимать следующее значение:
EINVAL SIGNUM содержит неправильный номер сигнала.
Для проверки того, обработчики каких сигналов активны в настоящий момент используется следующая функция:
int sigpending(sigset_t *SET)
Функция возвращает информацию об активных в текущий момент сигналах. Если имеется заблокированный сигнал, поступивший процессу, то он также включается в маску сигналов. Возвращает 0 в случае успешного выполнения и –1 в случае ошибки.
Для посылки сигналов другим процессам может использоваться
функция kill. Она может использоваться для выполнения множества различных действий. Необходимость посылать сигналы другим процессам возникает, например, в следующих случаях:
родительский процесс запускает порожденный для выполнения
некоторой задачи, причем выполняющий неопределенный цикл, и
завершает его, когда в дальнейшем выполнении задачи нет необходимости;
процесс выполняется как один из процессов в группе и нуждается в прерывании или информировании других процессов при возникновении ошибки или какого-либо критического события;
два процесса необходимо синхронизировать при одновременном
выполнении.
int kill(pid_t PID, int SIGNUM)
Функция kill посылает сигнал SIGNUM процессу или группе
процессов, задаваемому PID. PID может принимать следующие
значения:
> 0 сигнал посылается процессу, идентификатор которого равен
PID;
== 0 сигнал посылается всем процессам, которые находятся в
той же группе процессов, что и процесс-отправитель;
< -1 сигнал посылается группе процессов, идентификатор которой равен PID;
== -1 если процесс является привилегированным, то сигнал посылается всем процессам, кроме некоторых системных процессов.
Иначе, сигнал посылается всем процессам, владельцем которых
является текущий пользователь.
Если сигнал успешно послан, то kill возвращает 0, иначе сигнал
не посылается и функция возвращает –1. В случае неудачи в переменной errno возвращаются следующие коды:
82
EINVAL аргумент SIGNUM содержит неправильный номер сигнала;
EPERM отсутствуют привилегии на посылку сигнала процессу
или группе процессов;
ESCRH аргумент PID не соответствует ни одному из существующих процессов или групп процессов.
Процесс может послать сигнал самому себе с помощью вызова
kill(getpid(), SIG), что равнозначно raise(SIG).
Наиболее безопасным способом ожидания сигнала является использование функции sigsuspend:
int sigsuspend(sigset_t *SET)
Функция заменяет маску сигналов процесса и приостанавливает
его работу до поступления одного из сигналов, который не заблокирован маской. Маска, задаваемая SET, действует только на время
ожидания, вызываемого функцией, после возвращения управления процессу восстанавливается старая маска сигналов.
3.3. Компилятор GCC
Средствами, традиционно используемыми для создания программ для открытых операционных систем, являются инструменты
разработчика GNU [2]. Одним из этих инструментов является компилятор GCC, эта аббревиатура означает – GNU Compiler Collection.
Файлы с исходными кодами программ это обычные текстовые файлы, и создавать их можно с помощью любого текстового редактора.
Создадим отдельный каталог hello. Это будет каталог проекта.
В нём создадим текстовый файл hello.c со следующим текстом:
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return(0);
}
Затем в консоли перейдем в каталог проекта. Наберите команду
gcc hello.c
Теперь посмотрим, что произошло. В каталоге появился новый
файл a.out. Это и есть исполняемый файл. Запустим его. Наберите
в консоли:
./a.out
83
Программа должна запуститься, т.е. должен появиться текст:
Hello world!
Компилятор gcc по умолчанию присваивает всем созданным
исполняемым файлам имя a.out. Если необходимо назвать его подругому, следует к команде на компиляцию добавить флаг -o и имя,
которым вы хотите его назвать. Давайте наберём такую команду:
gcc hello.c -o hello
Мы видим, что в каталоге появился исполняемый файл с названием hello. Запустим его.
./hello
3.4. Задания
Задание 1. Процессы и потоки. Часть 1
Цель работы: практическое изучение процессов и потоков с помощью дискриптора процессов top в UNIX-подобной ОС.
1. Загрузить ОС UBUNTU, выбрав соответствующий вариант загрузки в меню загрузчика.
2. Ввести имя пользователя studentN, где N – номер рабочего места в лаборатории, если считать по часовой стрелке.
3. На приглашение ввести пароль, введите 12345678.
После загрузки ОС в графическом режиме, выберите в меню Applications Accessoires, а затем Terminal.
4. Ввести команду вызова справочной системы man с указанием
объекта справки, т.е. man top.
5. Ознакомиться с описанием дискриптора top.
6. Ввести команду top для ее запуска в режиме отображения основной панели.
7. Используя top, определить:
общее количество процессов;
активных;
в состоянии ожидания;
остановленных;
zombie.
8. С помощью субкоманды А включить режим отображения альтернативной панели top.
9. Ввести субкоманду u и имя пользователя, процессы которого
следует отображать:
u studentN.
84
10. Используя данные, отображаемые дискриптором, определить
для процессов пользователя top, bash и gnome-terminal их идентификаторы (PID) и идентификаторы процессов родителей (PPID).
11. Используя эти данные, определить:
какие из этих процессов были созданы ОС, а какие – пользователем;
какие из этих трех процессов являются родительскими, а какие – дочерними по отношению к друг к другу;
cобытия (действия), которые приводили к созданию каждого из
этих процессов.
12.  Включить полученные данные в отчет.
13.  В письменном виде ответить на следующие вопросы:
может ли выполняться программа на компъютере без операционной системы;
может ли в мультипрограммной среде в каждый данный момент
времени выполняться более одного процесса.
Задание 2. Процессы и потоки. Часть 2
Цель работы: практическое изучение процессов и потоков с помощью дискриптора процессов top в UNIX-подобной ОС.
1. Для процессов gnom-terminal, bash и top определить следующие параметры:
приоритет;
статус;
размер резидентной памяти;
процент использования процессора;
размер разделяемой памяти процесса;
размер свопируемой памями;
размер физической памяти, используемый исполняемым кодом;
размер памяти, выделенной для данных и стека;
количество неудачных обращений процесса к страницам вне его
адресного пространства;
количество страниц, которые были модифицированы с тех пор,
когда они были последний раз записаны на диск.
2. Используя субкоманду H – переключение отображения потоков, определить потоки, их PID, PPID, и название, а также
родственные связи и их возможную принадлежность к процессам
пользователя.
3. Для выявленных потоков определить значения параметров,
приведенных в п. 1.
85
4. С помощью top определить процесс с PID, равным 1, и его
свойства.
5. Включить в отчет ответы на следующие вопросы:
что такое образ процесса;
название, назначение и свойства процесса с идентификатором 1.
Задание 3. Управление процессами
Цель работы: практическое изучение способов управления процессами в UNIX-подобной ОС.
1. Используя субкоманду Renice ( R ), изменить статус ( NI ) процесса init со значения 0 на значение -5.
2. Используя субкоманду Renice ( R ), изменить статус ( NI ) процесса khelper со значения -5 на значение -1.
3. Убедиться, что статусы процессов init и gnom-terminal приобрели необходимые значения.
Уничтожение процессов
4. С помощью субкоманды k уничтожить процессы, созданные ОС:
dbus-launch;
mapping-daemon;
ssh-agent.
5. Сделать выводы по результатам выполнения п. 4 работы в
письменном виде. В частности, определить, возможен ли случай,
когда при уничтожении процесса уничтожаются и другие процессы. Объяснить это явление.
6. Определить в каких случаях уничтожение процесса может
приводить к зависанию ОС.
7. В письменном виде ответить на вопрос – в чем главное отличие между процессами x-session-manage и gnome-screensav?
Задание 4. Системные вызовы
Цель работы: получение практических навыков использования
системных вызовов при написании программ.
1. Ознакомиться с листингом программы, приведенной в примерах 1 и 2, используемыми там командами fork, execv, waitpid, kill,
fopen, fclose.
2. Скомпилировать программы и запустить программу StartProcess. Ознакомиться с результатами выполнения вычислений числа pi.
3. С помощью команд watch и ps наблюдать динамику процессов,
порождаемых программой StartProcess. Пример возможного синтаксиса команды: watch -n 0,01 -d ps -C StartProcess,DummyProcess -F -l f
86
4. Получить файл, содержащий все изменения информации о состоянии процессов, запущенных программой StartProcess. Пример
возможного синтаксиса команды: while(true) do ps -C StartProcess,
DummyProcess -F -l >> log.txt; done
5. С помощью команды ps определить и проанализировать изменения следующих параметров процессов, запускаемых программой StartProcess:
pid и ppid;
статусы процессов;
выделяемое планировщиком время ЦП для процессов;
приоритеты процессов и поправки к ним (nice-фактор);
размер образов процессов в памяти;
адрес события, которого ожидает процесс. У активного процесса
эта колонка пуста;
истраченное процессами время ЦП;
объем физической оперативной памяти выделенной процессу;
6. Отметить полученные результаты по всем параметрам в отчете.
Задание 5. Сигналы
Цель работы: ознакомление с сигналами, используемыми в ОС
Linux, а также получение навыков их применения при написании
программ.
1. Ознакомиться с листингом программы, приведенной в примере 3, используемыми там функциями kill, getpid, getppid, sigaction, sigfillset.
2. Скомпилировать и запустить программу signal.c. Ознакомиться с результатами ее выполнения и включить их в отчет.
3. Написать программу(ы), иллюстрирующую получение следующих типов сигналов процессом:
SIGKILL Этот сигнал приводит к немедленному завершению
процесса. Этот сигнал процесс не может игнорировать или установить для него новый обработчик;
SIGTERM Этот сигнал является запросом на завершение процесса;
SIGCHLD Система посылает этот сигнал процессу при завершении одного из его дочерних процессов;
SIGHUP Система посылает этот сигнал, когда происходит отключение от терминала;
SIGINT Система посылает этот сигнал, когда пользователь нажимает комбинацию клавиш Ctrl+C;
87
SIGILL Система посылает этот сигнал при попытке выполнить
недопустимую операцию;
SIGFPE Система посылает этот сигнал при попытке выполнить
недопустимую операцию с плавающей точкой;
SIGSEGV Система посылает этот сигнал при выполнении программой недопустимого обращения к памяти;
SIGPIPE Система посылает этот сигнал при обращении программы к разрушенному потоку данных.
При написании программы можно воспользоваться функцией
raise(sig).
4. Пояснить действия, происходящие при приеме этих сигналов
процессом.
5. Включить полученные результаты в отчет.
Пример 1
Эта программа предназначена для практического изучения системных вызовов и некоторых возможностей файловой системы в
UNIX-подобных ОС.
При запуске она создает определенное количество дочерних процессов, каждый из которых вычисляет число p с заданной точностью.
При создании дочернего процесса также создается файл, в который дочерний процесс возможно запишет результат своей работы.
После этого программа ожидает 2 секунды, и затем происходит
уничтожение первой половины дочерних процессов.
Далее происходит ожидание завершения работы оставшейся половины дочерних процессов.
Когда все процессы уничтожены или завершили свою работу
успешно, программа завершает работу.
Рекомендуется ВНИМАТЕЛЬНО ознакомится со страницами
man для следующих функций:
fork – создание дочернего процесса;
execv – выполнение внешнего процесса внутри текущего;
waitpid – ожидание завершения дочернего процесса;
kill – запрос на завершение дочернего процесса;
fopen – открывает файл;
fclose – закрывает файл.
Операции с файлами и процессами используют низкоуровневые
функции, в то время как вывод на консоль осуществляется средствами стандартной библиотеки.
88
#include <sys/wait.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <vector>
#include <iostream>
// Данным макросом определяется количество создаваемых дочерних
// процессов.
#define NUM_PROCESSES 10
int main (int argc, char* argv[])
{
// В данной переменной будет хранится идентификатор только что
// созданного дочернего процесса.
// pid_t cpid;
// В данном массиве переменных будут сохранены все идетнификаторы для созданных дочерних процессов.
std::vector<pid_t> startedProcesses;
for (int i = 0; i < NUM_PROCESSES; ++i)
{
// После выполнения вызова fork() процесс будет раздвоен
// начиная со следующего вызова выполнения в обоих (старом и новом)
// процессах.
// Начнется следующей строкой, при этом разница будет только
// в возвращенном fork() значении.
cpid = fork();
// Если результат выполнения fork равен -1 то это значит, что
// мы находимся в родительском процессе и создать дочерний процесс
// неудалось.
if (cpid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (cpid == 0) {
// Если результат выполнения fork равен 0, то это значит что мы
// находимся в только что созданном ДОЧЕРНЕМ процессе.
// Далее запустим выполнение внешнего процесса для вычисления
// числа p.
// Формируем командную строку, с которой будет запущен
// внешний процесс DummyProcess внутри нового дочернего
// процесса.
89
char szIterations[10];
char * newargv[] =
{
"./DummyProcess",
szIterations,
NULL
};
// Формируем количество итераций при вычислении p. У всех
// процессов результат будет несколько отличаться.
sprintf(szIterations, "%d", 100000000 + i*1000000);
std::cout << "Starting process with "<< szIterations << "iterations! "<< std::endl;
strcpy(newargv[1], szIterations);
// Выполняем внешний процесс внутри себя. Обратите внимание,
// в текущей директории должен быть
// исполняемый файл DummyProcess умеющий вычислять p!
execv("./DummyProcess",newargv);
}
else
{
// если результат выполнения fork больше 0, то это значит, что
// мы находимся в родительском процессе и знаем
// идентификатор только что созданного дочернего процесса.
// сохраним идентификатор только что созданного дочернего
// процесса для последующей работы с ним.
startedProcesses.push_back(cpid);
// Создадим файл, в который новый дочерний процесс запишет
// результат своей работы в случае удачного завершения.
// Сформируем имя файла из идентификатора дочернего
// процесса и расширения ".pi.text".
char fileName[128];
sprintf(fileName, "%d.pi.text", cpid);
FILE* file = fopen(fileName, "w+");
// Закроем только что созданный файл.
fclose(file);
}
}
// Ожидаем 2 секунды.
std::cout << "Created all processes! Sleeping 2 seconds before the
massacre" << std::endl;
90
sleep(2);
// Принудительно уничтожаем первую половину только что
// созданных процессов, используя их идентификаторы.
for (int i = 0; i < NUM_PROCESSES / 2; ++i)
{
int killresult;
std::cout << "About to kill the child process!" << std::endl;
killresult = kill(startedProcesses[i], SIGKILL);
if (killresult)
std::cout << "Failed to kill the process!" << std::endl;
else
std::cout << "Killed the process!" << std::endl;
}
// По очереди ожидаем заврешения работы оставшейся половины
// дочерних процессов, вычисляющих p.
for (int i = NUM_PROCESSES / 2; i < NUM_PROCESSES ; ++i)
{
int status;
waitpid(startedProcesses[i], &status, 0 );
}
// Выход. После выхода в половине файлов, созданных родительским
// процессом, будут содержатся результаты вычисления p
// дочерними процессами, для тех процессов, которым удалось "
// дожить" до этого.
exit(EXIT_SUCCESS);
}
Пример 2
Эта программа выполняет следующие действия.
Во-первых, она вычисляет число p методом Лейбница с количеством итераций, заданным как параметр командной строки.
Во-вторых, по окончании вычислений, она открывает файл с
именем, выведенным из идентификатора процесса и записывает в
него результат.
Операции с файлами используют низкоуровневые функции, в
то время как ввод и вывод на коносль осуществляется средствами
стандартной библиотеки.
#include <iostream>
#include <iomanip>
#include <stdio.h>
91
#include <stdlib.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[])
{
std::cout <<"Dummy Process Starting!" << std::endl;
int iterations;
if (argc == 1)
{
// В случае если количество итераций не указано, запрашиваем его
// у пользователя.
std::cout << "Computing PI with Leibnitz series. Please enter number
of iterations:";
std::cin >> iterations;
}
else if (argc == 2)
{
// Количество итерацияй для вычисления указано первым
// параметром командной строки.
iterations = atoi(argv[1]);
}
else
{
// Печать помощи и выход, если параметров командной строки
// больше одного.
std::cout << "Usage: DummyProcess [numberOfIterations]" << std::endl;
exit(0);
}
long double ourPi=4.0;
bool addFlop=true;
unsigned int denom=3;
// Вычисление p методом Лейбница. Занимает процессорное время!
for (unsigned int i=1;i<=iterations;++i)
{
if (addFlop) {
ourPi-=(4.0/denom);
addFlop=false;
denom+=2;
92
} else {
ourPi+=(4.0/denom);
addFlop=true;
denom+=2;
}
}
// Вывод результата на консоль.
std::cout << std::setiosflags(std::ios::showpoint) << std::setiosflags
(std::ios::fixed) << std::setprecision(20);
std::cout << "Pi calculated with" << iterations << "iterations is:
"<< ourPi << std::endl;
// Пытаемся открыть файл, состоящий из идетнификатора текущего
// (этого) процесса и расширения ".pi.text", для записи реультата.
// Данный файл должен быть создан родительских процессом!
// Формируем имя файла
pid_t myPid = getpid();
char fileName[128];
sprintf(fileName, "%d.pi.text", myPid);
// Открываем файл.
FILE* file = fopen(fileName, "r+");
if (file != NULL)
{
// Файл удачно открыт. Осуществляем печать результата в файл.
fprintf(file, "Pi calculated with %d iterations is:
%.20Lf",iterations, ourPi);
// Закрываем файл.
fclose(file);
}
return 0;
}
Пример 3
В этом примере представлена программа, функционирующая
следующим образом. Родительский процесс порождает дочерний
процесс и ждет пока дочерний завершит инициализацию. Дочерний процесс сообщает родителю, когда он готов, посылая ему сигнал SIGUSR1, используя вызов kill.
#include <signal.h>
#include <stdio.h>
93
#include <sys/types.h>
#include <unistd.h>
// В этом примере, родительский процесс порождает дочерний
// процесс и ждет пока дочерний завершит инициализацию.
// Дочерний процесс сообщает родителю, когда он готов, посылая
// ему сигнал SIGUSR1, используя функцию kill.
volatile sig_atomic_t usr_interrupt = 0;
void
synch_signal (int sig)
{
usr_interrupt = 1;
}
// Дочерний процесс выполняет эту функцию.
void
child_function (void)
{
printf ("дочерний процесс запущен, его pid= %d.\n",
(int) getpid ());
kill (getppid (), SIGUSR1);
puts ("дочерний процесс завершен");
}
int
main (void)
{
struct sigaction usr_action;
sigset_t block_mask;
pid_t child_id;
sigfillset (&block_mask);
usr_action.sa_handler = synch_signal;
usr_action.sa_mask = block_mask;
usr_action.sa_flags = 0;
sigaction (SIGUSR1, &usr_action, NULL);
// Создание дочернего процесса.
child_id = fork ();
if (child_id == 0)
child_function ();
while (!usr_interrupt);
puts ("конец");
return 0;
}
94
СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ
1. Литвинов Д. Г. Операционная система UNIX. Улан-Удэ,
2000.
2. Пантелеичев Д. Разработка программного обеспечения для
Linux. Инструментарий URL: http://www.linuxcenter.ru/lib/
books/linuxdev/.
3. Робачевский А. Операционная система Unix. СПб., 2005.
4. Ричард Стивенс У., Стивен А. Раго. UNIX. Профессиональное программирование. СПБ-М., 2007.
5. Стивенс У. UNIX: взаимодействие процессов. СПб., 2002.
95
ПРИЛОЖЕНИЕ
БИБЛИОТЕКА ЯЗЫКА
C GNU GLIBC С КОММЕНТАРИЯМИ
1. Сигналы
Сигнал – программное прерывание процесса. Операционная система использует сигналы, чтобы сообщить об исключительных ситуациях выполняемой программе. Некоторые сигналы сообщают
об ошибках типа ссылок к недопустимым адресам памяти; другие
сообщают асинхронные события, типа разъединения телефонной
линии.
Библиотека GNU C определяет типы сигналов, каждый для конкретного вида события. Некоторые события делают нецелесообразным или невозможным обычное продолжение программы, и соответствующие сигналы обычно прерывают программу. Другие виды
сигналов сообщают о малозначимых событиях, и игнорируются по
умолчанию.
Если ожидается событие, которое вызывает сигналы, можно
определить функцию-обработчик и сообщить операционной системе, чтобы она выполнила ее, когда придет заданный тип сигнала.
Один процесс может посылать сигнал другому процессу; это позволяет родительскому процессу прерывать дочерние, или сообщаться и синхронизироваться двум связанным процессам.
2. Типы сигналов
Сигнал сообщает об исключительном событии. Ниже приведены
некоторые из событий, которые могут вызывать (или генерировать)
сигнал:
− Ошибка в программе типа деления на нуль или выдачой адреса
вне допустимого промежутка.
− Запрос пользователя, чтобы прерывать или завершить программу.
(Большинство ОС позволяют пользователю приостанавливать
программу, печатая C-z, или завершать с C-c).
− Окончание дочернего процесса.
− Окончание ожидания таймера или будильника.
− Обращение "уничтожить процесс" из другого (или из этого)
процесса. Сигналы – ограниченная но полезная форма межпроцессорной связи.
96
Каждый из этих видов событий (за исключением явных обращений, чтобы уничтожать и вызывать) генерирует собственный вид
сигнала.
3. Порождение сигналов
События, которые генерируют сигналы, относятся к трем главным категориям: ошибки, внешние события, и явные запросы.
Ошибки означают, что программа сделала кое-что недопустимое
и не может продолжать выполнение. Не все виды ошибок генерируют сигналы. Например, открытие несуществующего файла –
ошибка, но она не вызывает сигнал; вместо этого, open возвращает
-1. Ошибки, которые обязательно связаны с некоторыми библиотечными функциями, сообщаются возвратом значения, которое
указывает ошибку. Ошибки, которые вызывают сигналы могут
случаться и в программе, а не только в вызовах из библиотек. Они
включают деление на нуль и недопустимые адреса памяти.
Явный запрос означает использование библиотечной функции
типа kill, чья цель – специально генерировать сигнал.
Сигналы могут быть сгенерированы синхронно или асинхронно. Синхронный сигнал относится к специфическому действию в
программе, и вызывается (если не блокирован) в течение этого действия.
Асинхронные сигналы сгенерированы снаружи при контроле
над процессом, который получает их. Эти сигналы занимают непредсказуемое время в течение выполнения. Внешние события генерируют сигналы асинхронно, так делают и явные запросы, которые обращаются к некоторому другому процессу.
Данный тип сигнала является обычно синхронным или асинхронным. Например, сигналы для ошибок обычно синхронны. Но
любой тип сигнала может быть сгенерирован синхронно или асинхронно с явным запросом.
4. Передача сигналов
Когда сигнал сгенерирован, он откладывается. Обычно он задерживается на короткий период времени и потом передаеся процессу.
Однако, если этот вид сигнала в данный момент заблокирован, он
может оставаться отложенным, пока сигнал этого вида не откроют. Если только он откроется, он будет передан немедленно. Когда
сигнал передан, или сразу же или после задержки, выполняется заданное действие для этого сигнала. Для некоторых сигналов, типа
97
SIGKILL и SIGSTOP, действие фиксировано, но для большинства
сигналов, программа имеет выбор: игнорировать сигнал, определить функцию обработчика, или принять заданное по умолчанию
действие для этого вида сигнала. Программа определяет свой выбор
используя функции типа signal или sigaction. В этом случае говорят, что обработчик захватывает сигнал. В то время как обработчик
выполняется, этот специфический сигнал обычно блокируется.
Если заданное действие для вида сигнала – игнорировать его, то
любой такой сигнал, будет игнорироваться. Это случается, даже если
сигнал блокирован в это же время. Сигнал, отброшенный таким образом, передан не будет никогда, даже если программа впоследствии
определяет другое действие для этого вида сигнала и откроет его.
Если прибывает сигнал, который программа не обрабатывает, и
не игнорирует, происходит заданное по умолчанию действие. Каждый вид сигнала имеет собственное заданное по умолчанию действие. Для большинства типов сигналов, заданное по умолчанию
действие должно завершить процесс. Для некоторых сигналов, которые представляют незначительные события, заданное по умолчанию действие не должно выполняться.
Когда сигнал завершает процесс, родительский процесс может
определять причину окончания, исследуя код состояния окончания, сообщенный wait или waitpid функциями. Если программа,
запущена из оболочки и завершена сигналом, оболочка обычно печатает сообщение об ошибке.
Сигналы, которые сообщают об ошибках в программе, имеют
особенность: когда один из этих сигналов завершает процесс, он
также формирует дамп core-файла, в который записывает состояние процесса во время окончания. Поэтому можно исследовать core­
файл с помощью отладчика, чтобы определить причину ошибки.
5. Стандартные сигналы
В этом разделе приводятся имена различных стандартных сигналов и описаны события, которорые их вызывают. Каждое имя
сигнала это макрокоманда, которая замещает положительное целое число указывающее на этот тип сигнала. Эти числа могут быть
разными у разных ОС, но значения имен стандартизированы.
Имена сигналов определены в заглавном файле "signal.h".
int NSIG (макрос)
Значение этой символической константы – общее число определенных сигналов. Так как номера сигналов размещены последова98
тельно, NSIG на один больше чем самое большое определенное число сигнала.
6. Сигналы ошибок
Некоторые программы обрабатывают сигналы, возникающие
при ошибке в программе, чтобы закончиться перед завершением. Обработчик должен завершить работу, определяя заданное по
умолчанию действие для данного сигнала, если он был. Как правило это приводит к завершению программы.
Заданное по умолчанию действие должно заставить процесс завершиться. Если такой сигнал блокируется или игнорируется программой, она может пострадать если сигналы не сгенерированы
raise или kill вместо реальной ошибки.
Когда один из этих сигналов об ошибке в программе завершает
процесс, он также формирует core-файл, который записывает состояние процесса во время окончания. Файл называется "core"и записан
в текущий каталог процесса. Цель core-файлов – обеспечить возможность их анализа с помощью отладчика, чтобы определить ошибку.
int SIGFPE (макрос)
Сигнал SIGFPE сообщает фатальную арифметическую ошибку.
Хотя имя происходит от "исключение с плавающей запятой", этот
сигнал фактически обозначает все арифметические ошибки, включая деление на нуль и переполнение. Если программа сохраняет
целочисленные данные в стандарте, который используется операциями с плавающей запятой, это часто, вызывает исключение "недопустимая операция", потому что процессор не может распознать
данные как число с плавающей запятой.
FPE_INTOVF_TRAP
Целочисленное переполнение (невозможно в программе на С).
FPE_INTDIV_TRAP
Целочисленное деление на нуль.
FPE_SUBRNG_TRAP
Переход нижнего индекса (программы C этого не проверяют).
FPE_FLTOVF_TRAP
Плавающее переполнения.
FPE_FLTDIV_TRAP
Плавающее/десятичное деление на нуль.
FPE_FLTUND_TRAP
Плавающее антипереполнение (переполнение снизу – слишком
маленькое число).
99
FPE_DECOVF_TRAP
Десятичное переполнения. (Не многие компьютеры имеют десятичную арифметику, и C никогда не использует ее.)
int SIGILL (макрос)
Так как компилятор C генерирует только допустимые команды,
SIGILL обычно указывает, что исполняемый файл разрушен. Как
правило это передача недопустимого объекта там, где ожидался указатель на функцию, или запись после конца автоматического массива
int SIGSEGV (макрос)
Этот сигнал сгенерируется, когда программа пробует читать или
писать вне памяти, которая размещена для этого. ( Сигнал возникает только, когда программа зашла слишком далеко, чтобы быть
обнаруженной механизмом защиты памяти системы.) имя сокращение "нарушение сегментации".
int SIGBUS (макрос)
Этот сигнал возникает, когда применяется недопустимый указатель. Подобно SIGSEGV, этот сигнал – обычно результат применения неинициализированного указателя. Различие между ними в
том, что SIGSEGV указывает на недопустимый доступ к допустимой памяти, в то время как SIGBUS указывает на доступ к недопустимому адресу. В частности SIGBUS сигналы часто возникают
из- за применения расположенного с нарушением границ указателя, типа целого числа.
int SIGABRT (макрос)
Этот сигнал указывает ошибку, обнаруженную программой непосредственно и сообщается, вызовом abort.
7. Сигналы завершения
Эти сигналы используются, чтобы сообщить, что процесс завершился. Причина обработки этих сигналов обычно то, что ваша программа должна выполнить некоторые действия перед фактическим
завершением. Например, Вы может потребоваться сохранять информацию о состоянии, удалить временные файлы, или восстанавливать
предыдущие режимы терминала. Такой обработчик должен закончиться определением заданного по умолчанию действия для сигнала.
int SIGHUP (макрос)
SIGHUP ("зависание") сигнал используется, чтобы сообщить,
что терминал пользователя разъединен, возможно, потому что сетевое или телефонное соединение было прервано Этот сигнал также
используется, чтобы сообщить окончание процесса управления на
100
терминале; это окончание действительно разъединяет все процессы
в сеансе с терминалом управления
int SIGINT (макрос)
SIGINT ("прерывание программы") сигнал посылается, когда
пользователь печатает INTR символ (обычно C-c).
int SIGQUIT (макрос)
Сигнал SIGQUIT подобен SIGINT, за исключением того, что он
управляется другой клавишей (символом QUIT), обычно C-\ и производит core-файл, когда он завершает процесс, точно так же как
сигнал ошибки в программе. Это указывает на условие ошибки в
программе"обнаруженной" пользователем.
int SIGTERM (макрос)
Сигнал SIGTERM – обобщенный сигнал, используемый, чтобы
вызвать окончание программы. В отличие от SIGKILL, этот сигнал
может быть блокирован, обрабатываться, и игнорироваться.
Команда оболочки kill генерирует SIGTERM по умолчанию.
int SIGKILL (макрос)
Сигнал SIGKILL используется, чтобы вызвать непосредственное
окончание программы. Он не может быть обработан или игнорироваться, и следовательно всегда фатален. Также не возможно блокировать этот сигнал.
Этот сигнал может быть сгенерирован только явным запросом.
Так как он не может быть обработан, его следует генерировать только после попытки применения менее сильнодействующего средства
типа C-c или SIGTERM.
Фактически, если SIGKILL будет не в состоянии завершать процесс, то это ошибка операционной системы.
8. Сигнализация
Эти сигналы используются, чтобы указать окончание времени
таймеров. Заданное по умолчанию поведение для этих сигналов
должно вызвать окончание программы.
int SIGALRM (макрос)
Этот сигнал обычно указывает окончание таймера, который измеряет реальное время или время часов.
Он используется функцией alarm, например.
int SIGVTALRM (макрос)
Этот сигнал обычно указывает окончание таймера, который измеряет CPU время, используемое текущим процессом. Его имя –
сокращение "виртуальный таймер".
101
int SIGPROF (макрос)
Этот сигнал обычно указывает окончание таймера, который измеряет и CPU время, используемое текущим процессом, и CPU время, израсходованное от имени процесса системой.
Асинхронные Сигналы ввода-вывода
Сигналы, перечисленные здесь используются вместе с асинхронными средствами ввода-вывода. Необходимо явно вызвать
fcntl, чтобы дать возможность специфическому описателю файла
генерировать эти сигналы.
int SIGIO (макрос)
Сигнал посылается, когда дескриптор файла готов выполнить
ввод или вывод.
В большинстве операционных систем, мониторы и сокеты единственые виды файлов, которые могут генерировать SIGIO; другие
виды, включая обычные файлы, никогда не генерируют SIGIO,
даже если Вы спрашиваете их.
int SIGURG (макрос)
Этот сигнал послан, когда "срочные" данные или данные вне потока прибывают в этот сокет.
9. Сигналы управления заданиями
Эти сигналы используются, чтобы поддерживать управление заданиями. Если система не поддерживает управление заданиями,
то эти макрокоманды, определены, но сигналы непосредственно не
могут быть вызваны или обрабатываться.
int SIGCHLD (макрос)
Этот сигнал послан родительскому процессу всякий раз, когда
один из дочерних процессов завершается или останавливается.
Заданное по умолчанию действие для этого сигнала – игнорировать это. Если установлен обработчик для этого сигнала, в то время
как имеются дочерние процессы, которые завершились, но не сообщили об их состоянии через wait или waitpid, то применяется ли
ваш обработчик к этим процессам или нет зависит от специфики
операционной системы.
int SIGCONT (макрос)
Вы можете посылать сигнал SIGCONT процессу, чтобы заставить его продолжиться. Заданное по умолчанию поведение для этого сигнала должно заставить процесс продолжиться, если он остановлен, и игнорировать его иначе.
Большинство программ не имеет никакой причины обработать
SIGCONT; они просто продолжают выполнение без информации, что
102
они когда-либо останавливались. Вы можете использовать обработчик для SIGCONT, чтобы заставить программу сделать нечто специальное, когда она остановлена и продолжиться; например, перепечатывать подсказку, когда она приостановлена при ожидании ввода.
int SIGSTOP (макрос)
Сигнал SIGSTOP останавливает процесс. Он не может быть обработан, игнорироваться, или блокироваться.
int SIGTSTP (макрос)
Сигнал SIGTSTP – интерактивный сигнал останова. В отличие
от SIGSTOP, этот сигнал может быть обработан и игнорироваться.
Программа должна обработать этот сигнал, если необходимо
оставить файлы или таблицы системы в безопасном состоянии, при
остановке процесса. Например, программы, которые выключают
отображение на экране, должны обработать SIGTSTP, так чтобы они
могли направлять отображение обратно на экран перед остановкой.
Этот сигнал сгенерирован, когда пользователь печатает символ
SUSP (обычно C-z).
int SIGTTIN (макрос)
Процесс не может читать с терминала пользователя, если он выполняется как фоновый. Когда любой фоновый процесс пробует
читать с терминала, всем процессам посылется сигнал SIGTTIN.
Заданное по умолчанию действие для этого сигнала должно остановить процесс.
int SIGTTOU (макрос)
Подобен SIGTTIN, но сгенерирован, когда фоновый процесс делает
попытку записи на терминал или устанавливать режимы. Повторно
заданное по умолчанию действие должно остановить процесс.
В то время когда процесс приостановлен, сигналы не могут быть
переданы ему, за исключением сигналов SIGKILL и (очевидно)
SIGCONT. Сигнал SIGKILL всегда вызывает окончание процесса
и не может быть блокирован или игнорироваться. Вы можете блокировать или игнорировать SIGCONT, но он всегда заставляет процесс быть продолженным во всяком случае, если он остановлен.
Посылка сигнала SIGCONT заставляет игнорировать любые ранее
отложенные процессы.
10. Дополнительные сигналы
Эти сигналы используются, чтобы сообщить о некоторых дополнительных условиях. Заданное по умолчанию действие для них
должно заставить процесс завершиться.
103
int SIGPIPE (макрос)
Если используются трубы или FIFO, необходимо обеспечить,
чтобы один процесс открыл трубу для чтения перед тем как другой начал запись. Если процесс считывания не начинается, или завершается неожиданно, запись в трубу или FIFO вызывает сигнал
SIGPIPE. Если SIGPIPE блокирован, обрабатывается или игнорируется, дргие обращения вместо этого вызывают EPIPE.
int SIGUSR1 (макрос)
int SIGUSR2 (макрос)
SIGUSR1 и SIGUSR2 полезны для межпроцессорной связи. Так
как эти сигналы обычно фатальны, необходимо написать обработчик сигнала для них в программе, которая получает сигнал.
11. Нестандартные сигналы
Специфические операционные системы поддерживают дополнительные сигналы, не перечисленные выше. Стандарт ANSI C
резервирует все идентификаторы, начинающиеся с "SIG" сопровождаемые символом верхнего регистра для имен сигналов. Например, некоторые системы особо поддерживают сигналы, которые
соответствуют аппаратным средствам. Некоторые другие виды сигналов используются, чтобы выполнить ограничения времени CPU
или использования файловой системы, для асинхронных изменений конфигурации терминала, и т.п.. Системы могут также определять имена сигналов, которые являются побочными результатами исследований для стандартных имен сигналов.
Заданное по умолчанию действие (или действие, установленное
оболочкой) для определенных реализацией сигналов обычно приемлемо. Обычно не стоит игнорировать или блокировать сигналы,
о которых нет информации, или пробовать устанавливать обработчик для сигналов, чьи значения не ясны.
Ниже перечислены некоторые сигналы, которые обычно используются в большинстве операционных систем:
SIGCLD Устаревшее имя для SIGCHLD.
SIGTRAP Сгенерированный командой breakpoint машины. Используется отладчиками. Заданное по умолчанию действие должно
формировать core-файл.
SIGIOT Сгенерированный PDP-11 "iot" командой; эквивалент
SIGABRT.
Заданное по умолчанию действие должно формировать core­
файл.
104
SIGEMT Ловушка эмулятора; следует из некоторых невыполненных команд. Это – сигнал ошибки в программе.
SIGSYS Ошибочный системный вызов; т.е. команда
системного вызова была выполнена, но код системного вызова
недопустим. Это – сигнал ошибки в программе.
SIGPOLL Это – имя сигнала System V, более или менее подобное
SIGIO.
SIGXCPU Превышение ограничения времени CPU. Заданное по
умолчанию действие – окончание программы.
SIGXFSZ Файлом превышено ограничение размера. Заданное по
умолчанию действие – окончание программы.
SIGWINCH Изменение размера окна. Он генерируется на некоторых системах, когда размер текущегокна на экране изменен.
Заданное по умолчанию действие – игнорировать это.
12. Сообщения сигнала
Оболочка печатает сообщение, описывающее сигнал, который
завершил дочерний процесс. Простой способ печатать сообщение,
описывающее сигнал состоит в том, чтобы использовать функции
strsignal и psignal. Эти функции используют номер сигнала, чтобы
определить, какой вид сигнала описывать. Номер сигнала может
исходить из состояния окончания дочернего процесса.
char * strsignal (int signum) (функция)
Эта функция возвращает указатель на статически размещенную
строку, содержащую сообщение, описывающее сигнал. Не следует
изменять содержимое этой строки, так как она может быть перезаписана при последующих обращениях. Можно сохранить его копию, если потребуется сослаться на него позже.
Эта функция – расширение GNU, объявленное в заглавном файле "string.h".
void psignal (int signum, const char *message)
Эта функция печатает сообщение, описывающее сигнал, в
стандартный поток ошибок – stderr.
Если вызвана psignal с сообщением, которое является или пустым указателем или пустой строкой, psignal печатает сообщение,
соответствующее сигналу, добавляя конечный символ перевода
строки.
Если обеспечивается непустой аргумент сообщения, то psignal
предворяет вывод этой строкой. Она добавляет двоеточие и пробел,
чтобы отделить сообщение от строки, соответствующей сигналу.
105
Эта функция – возможность BSD, объявленная в заглавном файле "stdio.h".
Имеется также массив sys_siglist, который содержит сообщения
для различных кодов сигнала. Этот массив существует в системах
BSD, в отличие от strsignal.
13. Определение действий сигнала
Самый простой способ изменить действие для сигнала состоит в
том, чтобы использовать функцию signal. Можно определять встроенное действие (типа игнорировать сигнал), или установлен обработчик.
Основная Обработка сигналов
Функция signal обеспечивает простой интерфейс для установки
действия для специфического сигнала.
Функция и связанные макрокоманды объявлены в заголовочном файле "signal.h".
sighandler_t (тип данных)
Это тип функций обработчика сигнала. Обработчики сигнала
воспринимают один целочисленный аргумент, определяющий номер сигнала, и возвращают тип void. Можно определять функции
обработчика примерно так:
void handler (int signum) { . . . }
Имя sighandler_t для этого типа данных – расширение GNU.
sighandler_t signal (int signum, sighandler_t action) (функция)
Функция signal устанавливает action как действие для сигнала
signum.
Первый аргумент, signum, идентифицирует сигнал, чьим поведением требуется управлять, и должен быть номером сигнала. Соответствующий способ определять номер сигнала – одним из символических имен сигналов, не использующих явное число, потому
что числовой код для данного вида сигнала может измениться от
одной операционной системы к другой.
Второй аргумент, action, определяет действие используемое для
сигнала signum. Оно может быть одно из следующих: SIG_DFL
определяет заданное по умолчанию действие для специфического
сигнала. SIG_IGN определяет, что сигнал должен игнорироваться.
Программы не должны игнорировать сигналы, которые представляют серьезные события, или обычно используется, чтобы запросить
окончание. Нельзя игнорировать SIGKILL или SIGSTOP. Можно игнорировать сигналы ошибки в программе подобно SIGSEGV, но игно106
рирование ошибки не будет давать возможность программе продолжить осмысленное выполнение. Игнорирование запросов пользователя типа SIGINT, SIGQUIT, и SIGTSTP недопустимо.
Если не желательно, чтобы сигналы были переданы в некоторую
часть программы, нужно блокировать их, а не игнорировать. handler
обеспечивает адрес функции обработчика в программе, выполнение
этого обработчика определяется как способ передать сигнал.
Функция signal возвращает action, который был задан для указанного signum. Можно сохранять это значение и восстанавливать
его позже, вызывая signal снова.
Если signal не может выполнить запрос, она возвращает SIG_
ERR. Следующие errno условия ошибки определены для этой
функции:
EINVAL
Определен недопустимый signum; или была попытка игнорировать или обеспечивать обработчик для SIGKILL или SIGSTOP.
Вот простой пример установки обработчика, чтобы удалить временные файлы, при получении некоторых фатальных сигналов:
#include <signal.h>
void
termination_handler (int signum)
{
struct temp_file *p;
for (p = temp_file_list; p; p = p->next)
unlink (p->name);
}
int
main (void)
{
...
if (signal (SIGINT, termination_handler)
== SIG_IGN)
signal (SIGINT, SIG_IGN);
if (signal (SIGHUP, termination_handler)
== SIG_IGN)
signal (SIGHUP, SIG_IGN);
if (signal (SIGTERM, termination_handler)
== SIG_IGN)
signal (SIGTERM, SIG_IGN);
...
}
107
Если данный сигнал был предварительно установлен, чтобы игнорироваться, то код избегает изменять эту установку. Потому что
оболочки часто игнорируют некоторые сигналы, при старте дочерних процессов, и эти процессы не долхны изменять это.
sighandler_t ssignal (int signum, sighandler_t action) (функция)
Функция ssignal выполняет то же самое действие что и signal;
она предоставляется только для совместимости с SVID.
sighandler_t SIG_ERR (макрос)
Значение этой макрокоманды используется как возвращаемое
значение из signal, чтобы указать ошибку.
14. Сложная обработка сигнала
Функция sigaction имеет тот же самый основной эффект как
signal: она определяет, как сигнал должен быть обработан процессом. Однако, sigaction предлагает большое количество управления,
за счет большей сложности. В частности sigaction позволяет определять дополнительные флаги, чтобы управлять тем, когда генерируется сигнал и как вызывается обработчик.
Функция sigaction объявлена в "signal.h".
struct sigaction (тип данных)
Структуры типа struct sigaction используются в функции
sigaction, чтобы определить всю информацию относительно того,
как обработать специфический сигнал. Эта структура содержит по
крайней мере следующие элементы:
sighandler_t sa_handler
Это используется таким же образом, как аргумент action функции signal. Значение может быть SIG_DFL, SIG_IGN, или указатель на функцию.
sigset_t sa_mask
Это определяет набор сигналов, которые будут блокированы, в
то время как обработчик выполняется. Сигнал, который был передан, автоматически блокируется по умолчанию прежде, чем обработчик начат его обрабатывать. Если необходимо, чтобы этот сигнал не был блокирован внутри обработчика, нужно записать в обработчике код чтобы разблокировать его.
int sa_flags
Определяет различные флаги, которые могут воздействовать на
поведение сигнала.
int sigaction (int signum, const struct sigaction *action, struct
sigaction *old_action)
108
Аргумент action используется, чтобы установить новое действие
для указанного сигнала, в то время как old_action аргумент
используется, чтобы возвратить информацию относительно
действия, предварительно связанного с этим сигналом. (Другими
словами, можно выяснить, что старое действие в действительности делало для сигнала, и восстановить его позже, если это
необходимо.)
Возвращаемое значение от sigaction – нуль, если она преуспевает, и -1 при отказе. Следующие errno условия ошибки определены
для функции:
EINVAL
аргумент signum не допустим, или необходимо обрабатывать
или игнорировать SIGKILL или SIGSTOP.
Взаимодействие signal и sigaction
Функция sigaction обладает большими возможностями, чем
signal, так что возвращаемое значение из signal не может выражать
все действия sigaction. Следовательно, если используется signal,
чтобы сохранить и позже восстанавливать действие, она может
быть не способна восстановить правильно обработчик, который
был установлен с sigaction.
Чтобы избежать проблем всегда используйте sigaction, чтобы
сохранить и восстановить обработчик, если программа использует sigaction вообще. Так как sigaction более общая, она может
правильно сохранять и восстанавливать любое действие, независимо от того, было ли оно установлено первоначально с signal или
sigaction.
Если используется signal, а потом sigaction, адрес обработчика,
который будет получен, может быть не таким как тот, что был определен с signal. Он так же не может использоваться как аргумент
action в signal. Так что лучше отказаться от использования того и
другого механизма последовательно внутри однй программы.
Общая функция signal – возможность ANSI C, в то время как
sigaction – часть POSIX.1 стандарта. Если есть сомнения относительно переносимости на не-posix системы, то следует использовать функцию signal.
15. Пример функции sigaction
Выше был приведен пример установки простого обработчика для сигналов окончания, используя signal. Далее следует
эквивалентный пример, использующий sigaction:
109
#include <signal.h>
void
termination_handler (int signum)
{
struct temp_file *p;
for (p = temp_file_list; p; p = p->next)
unlink (p->name);
}
int
main (void)
{
...
struct sigaction new_action, old_action;
new_action.sa_handler = termination_handler;
sigemptyset (&new_action.sa_mask);
new_action.sa_flags = 0;
sigaction (SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGINT, &new_action, NULL);
sigaction (SIGHUP, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGHUP, &new_action, NULL);
sigaction (SIGTERM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGTERM, &new_action, NULL);
...
}
Программа загружает структуру new_action с желательными
параметрами и передает ее в sigaction. При использовании signal,
следует избегать обрабатывать предварительно установленные сигналы. Здесь же sigaction позволяет исследовать текущее действие
без определения нового.
Другой пример. Он отыскивает информацию относительно текущего действия для SIGINT без замены этого действия.
struct sigaction query_action;
if (sigaction (SIGINT, NULL, &query_action) < 0)
/* sigaction возвращает -1 в случае ошибки. */
else if (query_action.sa_handler == SIG_DFL)
/* SIGINT обработан заданным по умолчанию,
фатальным способом. */
else if (query_action.sa_handler == SIG_IGN)
110
/ * SIGINT игнорируется. * /
else
/* Определенный программистом
обработчик сигнала. */
16. Флаги для sigaction
Элемент структуры sa_flags – sigaction определяет специальные
возможности и большинстве случаев, SA_RESTART – хорошее значение, чтобы использовать для этого поля.
Значение sa_flags интерпретируется как битовая маска. Таким
образом, необходио выбрать флаги, которые требуется установить,
сделать операцию OR для этих флагов, и сохранить результат sa_
flags в элементе структуры sigaction.
Каждый номер сигнала имеет собственный набор флагов. Каждое обращение к sigaction воздействует на один номер сигнала, и
флаги, которые определены, применяются только к этому специфическому сигналу.
В библиотеке GNU C, установка обработчика сигнала обнуляет
все флаги, кроме SA_RESTART, чье значение зависит от установок, которые сделаны в siginterrupt. Эти макрокоманды определены в заголовочном файле "signal.h".
int SA_NOCLDSTOP (макрос)
Этот флаг имеет смысл только для сигнала SIGCHLD. Когда
флаг установлен, система передает сигнал для завершенного дочернего процесса, но не для того, который остановлен. По умолчанию,
SIGCHLD передан для завершенных дочерних процессов и для
остановленных дочерних процессов.
int SA_ONSTACK (макрос)
Если этот флаг установлен для конкретного сигнала, система использует стек сигнала при сообщении этого вида сигнала.
int SA_RESTART (макрос)
Этот флаг может потребоваться если сигнал передан в течение выполнения некоторых примитивов (типа open, read или
write). Имеются два варианта: библиотечная функция может
продолжиться, или она может возвратить отказ с кодом ошибки
EINTR.
Выбор управляется SA_RESTART флагом для конкретного вида
сигнала, который был передан. Если флаг установлен, по возвращении из обработчика продолжается библиотечная функция. Если
флаг не установлен, происходит сбой функции.
111
17. Начальные действия сигнала
После создания, новый процесс наследует обработку сигналов из
родительского процесса. Однако, когда загружается новый образ
процесса, используя функцию exec обработчики любых сигналов
возвращаются к SIG_DFL. Конечно, новая программа может устанавливать собственные обработчики.
Когда программа выполняется оболочкой, оболочка обычно
устанавливает начальные действия для дочернего процесса как
SIG_DFL или SIG_IGN, соответственно.
Вот пример того, как устанавливать обработчик для SIGHUP, но
если SIGHUP в настоящее время не игнорируется:
...
struct sigaction temp;
sigaction (SIGHUP, NULL, &temp);
if (temp.sa_handler != SIG_IGN)
{
temp.sa_handler = handle_sighup;
sigemptyset (&temp.sa_mask);
sigaction (SIGHUP, &temp, NULL);
}
18. Определение обработчиков сигнала
Здесь показано, как написать функцию обработчика сигнала,
которая может быть установлена функциями sigaction или signal.
Обработчик сигнала это функция, которая компилируется вместе с остальной частью программы. Вместо непосредственно вызова функции, можно использовать signal или sigaction, чтобы сообщить, чтобы операционная система вызвала ее, когда приходит
сигнал. Это называется установкой обработчика.
Имеются две cтратегии, которые можно использовать в функциях обработчика сигнала:
Вы можете иметь функцию обработчика, которая отмечает, что
сигнал пришел, в некоторых глобальных структурах данных, и
нормально возвращается.
Вы можете иметь функцию обработчика, которая завершает
программу или передает управление к отметке, где программа может избавиться от ситуации, которая вызвала сигнал.
Нужно быть осторожным при написании функций обработчика, потому что они могут вызываться асинхронно. То есть обработчик может вызываться в любом месте программы, непредсказуе112
мо. Если два сигнала прибывают в течение короткого интервала,
один обработчик может выполняться внутри другого. Следующий раздел показывает, что необходимо делать, чтобы избежать
этого.
19. Обработчики возвращающихся сигналов
Небезопасно возвращаться из обработчика при сигнале ошибки
в программе, потому что поведение программы, когда функция обработчика возвращается, не определено. Возвращающиеся бработчики должны изменять некоторую глобальную переменную, чтобы
избежать этой проблемы.
Обычно, эта переменная исследуется периодически программой. Тип данных должен быть sig_atomic. Вот простой пример такой программы. Она выполняет тело цикла, пока она не отметит,
что сигнал SIGALRM прибыл.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
volatile sig_atomic_t keep_going = 1;
void
catch_alarm (int sig)
{
keep_going = 0;
signal (sig, catch_alarm);
}
void
do_stuff (void)
{
puts ("Doing stuff while waiting
for alarm....");
}
int
main (void)
{
signal (SIGALRM, catch_alarm);
alarm (2);
while (keep_going)
do_stuff ();
return EXIT_SUCCESS;
}
113
20. Обработчики, завершающие процесс
Функции-Обработчики, которые завершают программу, обычно
используются, чтобы вызвать организованную очистку от сигналов
ошибки в программе и интерактивных прерываний.
Самый простой способ для обработчика завершить процесс состоит в том, чтобы вызвать тот же самый сигнал, который активизировал обработчик. Вот, как сделать это:
volatile sig_atomic_t fatal_error_in_progress = 0;
void
fatal_error_signal (int sig)
{
if (fatal_error_in_progress)
raise (sig);
fatal_error_in_progress = 1;
/ * Теперь делаем очищающие действия:
– установка режимов терминала
– уничтожение дочерних процессов
– удаление временных файлов * /
...
raise (sig);
}
Нелокальная передача управления в обработчиках
Можно делать нелокальную передачу управления вне обработчика сигнала, используя setjmp и longjmp средства
Когда обработчик делает нелокальную передачу управления,
часть программы, которая выполнялась, не будет продолжаться. Если эта часть программы была в процессе изменения важной
структуры данных, структура данных останется несогласованной.
Так как программа не завершается, несогласованность должна
быть отмечена позже.
Имеются два способа избежать этой проблемы. Первый – блокировать сигнал для частей программы, которые изменяют важные структуры данных. Блокирование сигнала задерживает выдачу, пока он не открыт, как только критическое изменение
закончено.
Иначе, нужно повторно инициализировать определяющие
структуры данных в обработчике сигнала, или сделать их значения
непротиворечивыми.
Вот довольно схематический пример, показывающий переинициализацию одной глобальной переменной.
114
#include <signal.h>
#include <setjmp.h>
jmp_buf return_to_top_level;
volatile sig_atomic_t waiting_for_input;
void
handle_sigint (int signum)
{
waiting_for_input = 0;
longjmp (return_to_top_level, 1);
}
int
main (void)
{
...
signal (SIGINT, sigint_handler);
...
while (1) {
prepare_for_command ();
if (setjmp (return_to_top_level) == 0)
read_and_execute_command ();
}
}
char *
read_data ()
{
if (input_from_terminal) {
waiting_for_input = 1;
...
waiting_for_input = 0;
}
else {
...
}
}
21. Прибытие сигналов
во время выполнения обработчика
Что случается, если другой сигнал приходит, когда выполняется ваша функция обработчика сигнала?
Когда вызывается обработчик для конкретного сигнала, этот
сигнал обычно блокируется, пока обработчик не возвращается. Это
115
означает что, если два сигнала того же самого вида приходят через
очень короткий интервал времени, второй будет приостановлен,
пока первый не будет обработан. (Обработчик может явно открыть
сигнал, используя sigprocmask, если нужно позволить прибывать
большему количеству сигналов этого типа).
Однако, обработчик может все еще прерываться получением
другого вида сигнала. Чтобы избежать этого, можно использовать
sa_mask – элемент структуры action, передаваемой sigaction, чтобы явно определить, какие сигналы должны быть блокированы в
то время как обработчик сигнала выполняется. Следует всегда использовать sigaction, чтобы установить обработчик для сигнала,
который может прийти асинхронно.
22. Случай обьединения сигналов
Если несколько сигналов того же самого типа переданы процессу
прежде, чем обработчик сигнала может быть вызван, то обработчик
может вызываться только один раз. В результате сигналы обьединяются в один. Эта ситуация может возникать, когда сигнал блокирован, или в многопроцессорной среде, где система занята выполнением некоторых других процессов, в то время как сигнал передан. Это
означает, что нельзя надежно использовать обработчик сигнала, чтобы считать сигналы. Единственное, что можно сделать, это узнать,
прибыл ли по крайней мере один сигнал начиная с данного времени.
Вот пример обработчика для SIGCHLD, который компенсирует
предположение, что число полученных сигналов не может равняться числу дочерних процессов, генерируя их. Он подразумевает, что
программа следит за всеми дочерними процессами цепочкой структур следующим образом:
struct process
{
struct process *next;
int pid;
int input_descriptor;
int status;
};
struct process *process_list;
Этот пример также использует флаг, чтобы указать, прибыли
ли сигналы начиная с некоторого времени в прошлом, когда
программа в последний раз обнулила его.
int process_status_change;
116
Вот обработчик непосредственно:
void
sigchld_handler (int signo)
{
int old_errno = errno;
while (1) {
register int pid;
int w;
struct process *p;
do
{
errno = 0;
pid = waitpid (WAIT_ANY,
&w,
WNOHANG | WUNTRACED);
}
while (pid <= 0 && errno == EINTR);
if (pid <= 0) {
errno = old_errno;
return;
}
for (p = process_list; p; p = p->next)
if (p->pid == pid) {
p->status = w;
p->have_status = 1;
if (WIFSIGNALED (w) ||
WIFEXITED (w))
if (p->input_descriptor)
FD_CLR (p->input_descriptor, &input_wait_mask);
++process_status_change;
}
}
}
Вот соответствующий способ проверять флаг process_status_
change:
if (process_status_change) {
struct process *p;
process_status_change = 0;
for (p = process_list; p; p = p->next)
if (p->have_status) {
... Исследуют p->status ...
117
}
}
Важно очистить флаг перед исследованием списка; иначе, если
сигнал был передан только перед очисткой флага, и после того, как
соответствующий элемент списка процесса был проверен, изменение
состояния будет неотмеченным, пока следующий сигнал не прибыл,
чтобы установить флаг снова. Можно, конечно, избежать этой проблемы, блокировав сигнал при просмотре списка, но более важно гарантировать правильность, делая все в надлежащем порядке.
Цикл, который проверяет состояние процесса избегает исследовать p->status, пока он не видит, что состояние было законно сохранено. Он должен удостовериться, что status не может изменяться
в середине доступа к нему. Как только p->have_status установлен,
это означает что дочерний процесс остановлен или завершен, и в
любом случае он не может останавливаться или завершаться снова,
для получения более подробной информации относительно копирования с прерываниями во время доступа к переменной.
Ниже показано, rаким образом можyj проверять, выполнился
ли обработчик начиная с последней проверки.
Эта методика использует счетчик, который никогда не изменяется снаружи обработчика. Вместо того, чтобы очищать счетчик,
программа помнит предыдущее значение, и видит, изменилось
ли оно начиная с предыдущей проверки. Преимущество этого метода – в том, что различные части программы могут проверяться
независимо, каждая часть проверяет имелся ли сигнал начиная с
последней проверки в этой части.
sig_atomic_t process_status_change;
sig_atomic_t last_process_status_change;
...
{
sig_atomic_t prev=last_process_status_change;
last_process_status_change =
process_status_change;
if (last_process_status_change != prev) {
struct process *p;
for (p = process_list; p; p = p->next)
if (p->have_status) {
... Проверка p->status ...
}
}
}
118
23. Обработка сигнала и неповторно
используемые функции
Функции-Обработчики обычно делают не очень много. Самый
лучший обработчик – который не делает ничего, но устанавливает
внешнюю переменную, которую программа проверяет регулярно, и
оставляет всю серьезную работу программе. Это самое лучшее, потому что обработчик может вызываться асинхронно, в непредсказуемое время, возможно в середине системного вызова, или даже
между началом и концом оператора Cи, который требует выполнения многократных команд. Изменяемые структуры данных могут
быть в несогласованном состоянии, когда вызывается функция обработчика. Даже копирование одной int переменной в другую занимает две команды на большинстве машин.
Это означает, что нужно быть очень осторожным используя обработчик сигнала.
Если обработчик должен обратиться к некоторым глобальным
переменным вашей программы, объявляйте эти переменные независимо. Это сообщает компилятору, что значение переменной может изменяться асинхронно, и запрещает некоторые оптимизации.
Если вызывается функция в обработчике, удостоверьтесь, что
она повторно используется относительно сигналов, и что сигнал не
может прервать обращение к зависимой функции. Функция не может повторно использоваться, если она использует память, которая
не на стеке.
Если функция использует статическую переменную или глобальную переменную, или динамически размещенный объект, который она устанавливает для себя, то она – неповторно используемая и любые два обращения к функции могут смешиваться.
Например, предположите, что обработчик сигнала использует
gethostbyname. Эта функция возвращает значение в статическом
объекте, многократно используя тот же самый объект каждый раз.
Если сигнал прибывает в течение обращения в gethostbyname, или
даже после него (в то время как программа все еще использует значение), то он затрет значение еще не прочитанное программой. Однако, если программа не использует gethostbyname или любую другую
функцию, которая возвращает информацию в том же самом объекте,
или если она всегда блокирует сигналы вокруг каждого использования, то опасности нет. Имеется большое количество библиотечных
функций возвращающих значения в фиксированном объекте, всегда
многократно используя тот же самый объект в этом режиме, и все из
них вызывают ту же самую проблему. Если функция использует и
119
изменяет объект, который Вы обеспечиваете, то она неповторно используема; два обращения могут смешиваться, если они используют
тот же самый объект. Этот случай возникает, когда осуществляется ввод – вывод, используя потоки. Предположите, что обработчик
сигнала печатает сообщение с fprintf. Предположим что программа
была в середине обращения к fprintf, используя некоторый поток,
когда был получен сигнал. И сообщение обработчика сигнала и данные программы могут быть разрушены, потому что оба обращения
функционируют на том же самом потоке непосредственно. Однако,
если извесно, что поток используемый обработчиком не может использоваться программой одновременно, когда прибывают сигналы,
то все нормально. На большинстве систем, malloc и free неповторно
используемы, потому что они используют статическую структуру
данных, которая хранит сведения о свободных блоках. В результате,
все библиотечные функции, которые резервируют или освобождают
память, не повторно используемы, включая функции, которые резервируют место, чтобы сохранить результат. Самый лучший способ
избегать потребности зарезервировать память – зарезервировать заранее пространство для обработчиков сигнала. Самый лучший способ избегать освобождения памяти в обработчике отмечать или запоминать объекты, которые будут освобождены, и иметь возможность
проверять в программе время от времени, ждет ли кто-нибудь, чтобы его освобождили. Но это должно быть выполнено аккуратно, так
как размещение объекта в цепочке – не быстрая операция, и если
оно прервано другим обработчиком сигнала, который делает то же
самое, можно "потерять" один из объектов. В системе GNU, malloc и
free безопасны для использования в обработчиках сигнала, потому
что они блокируют сигналы. В результате, библиотечные функции,
которые резервируют пространство для результата также безопасны
в обработчиках сигналов. Obstack функции резервирования безопасны, если не используется тот же самый obstack, и внутри и вне обработчика сигнала.
Любая функция, которая изменяет errno, неповторно используема, но можно исправить это: в обработчике, можно сохранить первоначальное значение errno, и восстановить его перед возвращением. Это предотвращает смешивание ошибок, которые происходят
внутри обработчика сигнала, с ошибками из системных вызовов
в точке прерывания программы. Эта методика оправдывает себя,
например, если Вы хотите вызвать обработчик, который изменяет
специфический объект в памяти, можно сделать его безопасным,
сохраняя и восстанавливая этот объект.
120
Просто чтение из объекта памяти безопасно, если иметь дело с
любым из значений, которые могли бы появляться в объекте одновременно, когда сигнал получен. Доступ к некоторым типам данных требует больше чем одну команду, это означает что обработчик
может выполняться "в середине" доступа к переменной.
Просто запиись в объект памяти безопасна, если только внезапное изменение значения, в любое время, когда обработчик мог бы
выполняться, не будет нарушать чего-нибудь.
24. Быстрый доступ и обработка сигнала
Для любого вида данных в программе, нужно быть внимательным к тому, что доступ к одиночному элементу данных не обязательно быстрый. Это означает, что это может занимать больше чем
одну команду. В таких случаях, обработчик сигнала может выполняться в середине чтения или запииси объекта.
Имеются три способа, при помощи которых можно справляться
с этой проблемой. Можно использовать типы данных, обращение к
которым всегда происходит быстро; Можно также блокировать все
сигналы вокруг любого доступа, который лучше не прерывать.
25. Проблемы с немгновенным доступом
Вот пример, который показывает то, что может случиться, если
обработчик сигнала выполняется в середине изменения переменной. (Прерывание чтения переменной может также привести к парадоксальным результатам, но здесь мы показываем только запись.)
#include <signal.h>
#include <stdio.h>
struct two_words { int a, b; } memory;
void
handler(int signum)
{
printf ("%d,%d\n", memory.a, memory.b);
alarm (1);
}
int
main (void)
{
static struct two_words zeros = {0,0},
ones = {1,1};
121
signal (SIGALRM, handler);
memory = zeros;
alarm (1);
while (1)
{
memory = zeros;
memory = ones;
}
}
Эта программа заполняет память с нулями и еденицами, чередуя все время, тем временем, раз в секунду, обработчик сигнала
таймера печатает текущее содержимое. (Вызов printf в обработчике безопасен в этой программе, потому что она конечно не вызывается снаружи обработчика, когда случается сигнал.)
Ясно, эта программа может печатать пару нулей или пару едениц. Но это не все что она может делать! На большинстве машин сохранение нового значения в памяти занимает несколько команд, и
значение сохраняется по одному слову. Если сигнал передан между
этими командами, обработчик, может находить, что memory.a –
нуль, а memory.b – один (или наоборот).
На некоторых машинах может быть возможно сохранить новое
значение в памяти только одной командой, которая не может быть
прервана. На этих машинах, обработчик будет всегда печатать два
нуля или две еденицы.
26. Типы данных c быстрым доступом
Чтобы избежать неопределенности относительно прерывания доступа к переменной, можно использовать специфический тип данных, для которого доступ является всегда быстрым: sig_atomic_t.
При чтении и запииси этого типа данных, когда выполняется одиночная команда, у обработчика нет возможности выполниться "в
середине" доступа.
Тип sig_atomic_t – всегда целочисленный тип данных.
sig_atomic_t
К объектам этого типа обращение происходит быстро.
Можно считать, что int и других целочисленных типов не больших, чем int, еще быстрые.
К типам pointer реализуется быстрый доступ; это очень удобно.
Это верно для всех машин, которые поддерживает библиотека GNU
C, и для всех систем POSIX.
122
27. Быстрое использование шаблонов
Некоторые шаблоны доступа избегают любой проблемы, даже
если доступ прерван. Например, флаг, который установлен обработчиком, и проверяется и очищается программой main время от
времени, всегда безопасен, даже если доступ к нему фактически
требует двух команд. Чтобы показать, что это так, мы должны рассмотреть каждый доступ, который мог быть прерван, и показывать,
что не имеется никакой проблемы, если он прерван.
Прерывание в середине тестирования флага безопасно, потому
что либо он распознан отличным от нуля, тогда точное значение не
важно, либо он будет отличным от нуля, в следующий раз.
Прерывание в середине очистки флага не проблема, потому что
либо программа записывает нуль, если сигнал входит прежде, чем
флаг очищен, либо обрабтчик заканчивается, и последующие события происходят при флаге отличном от нуля.
Если код обрабатывает оба этих случая правильно, то он может
также обрабатывать сигнал в середине очистки флага.
28. Примитивы, прерванные сигналами
Сигнал может прибывать и обрабатываться, в то время как примитив ввода-вывода типа open или read ждет устройство ввода-вывода. Если обработчик сигнала возвращается, система задает вопрос: что должно случиться затем?
POSIX определяет один подход: делайте примитивный сбой сразу же. Код ошибки для этого вида отказа – EINTR. Это гибко, но
обычно неудобно. Обычно, приложения POSIX, которые используют обработчики сигнала, должны проверить EINTR после каждой
библиотечной функции, которая может возвращать его, чтобы пытаться обратиться снова. Часто программисты забывают это проверять, что является общим источником ошибок.
Библиотека GNU обеспечивает удобный способ повторить обращение после временного отказа макрокомандой TEMP_FAILURE_
RETRY:
TEMP_FAILURE_RETRY (expression) (макрос)
Эта макрокоманда оценивает выражение один раз. Если оно
терпит неудачу и код ошибки EINTR, TEMP_FAILURE_RETRY
оценивает это снова, и много раз, пока результат не временный
отказ.
Значение, возвращенное TEMP_FAILURE_RETRY – любое
произведенное выражением значение.
123
BSD избегает EINTR полностью и обеспечивает более удобный
подход: перезапускать прерванный примитив. Если выбиран этот
подход, EINTR можно не использовать.
Однако можно выбирать любой подход в библиотеке GNU. Если
используется sigaction, чтобы установить обработчик сигнала,
можно определять, как этот обработчик должен вести себя. Если
определен SA_RESTART флаг, после возврата из этого обработчика
продолжится примитив; иначе, возврат из этого обработчика вызовет EINTR. Если используется специфический обработчик и Вы
не определяете сsigaction или siginterrupt, он использует заданный
по умолчанию выбор. Заданный по умолчанию выбор в библиотеке
GNU зависит от возможностей макрокоманд, которые определены.
Если это _BSD_SOURCE или _GNU_SOURCE перед вызовом сигнала, значение по умолчанию – продолжить примитивы; иначе, значение по умолчанию должно делать сбой с EINTR.
Примитивы, на которые воздействует это: close, fcntl (операция
F_SETLK), open, read, recv, recvfrom, select, send, sendto, tcdrain,
waitpid, wait, и write.
Имеется одна ситуация, где возобновление никогда не случается, независимо от того, какой выбор сделан: когда функция преобразования типа например read или write прервана сигналом после
пересылки части данных. В этом случае, функция возвращает число байтов, уже перемещенных, указывая частичный успех.
29. Сигналы производства
Кроме сигналов, которые сгенерированы в результате аппаратной проверки или прерывания, программа может явно посылать
сигналы себе или другому процессу.
Передача Сигналов Самому себе
Процесс может посылать себе сигнал функцией raise. Эта функция объявлена в "signal.h".
int raise (int signum)
Функция raise посылает сигнал процессу вызова. Она возвращает нуль, если она успешна и значение отличное от нуля, если она
терпит неудачу. Единственная причина для отказа – если значение
signum недопустимо.
int gsignal (int signum)
Функция gsignal делает то же самое, что и raise; она нужна только
для совместимости с SVID. Удобное использование raise – воспроизвести заданное по умолчанию поведение сигнала, который уже обра124
ботан. Например, предположите, что пользователь вашей программы
печатает символ SUSP (обычно C-z) чтобы послать интерактивный
сигнал останова (SIGTSTP), и Вы хотите очистить некоторые внутренние буферы данных перед остановкой. Можете сделать это так:
#include <signal.h>
void
tstp_handler (int sig)
{
signal (SIGTSTP, SIG_DFL);
...
raise (SIGTSTP);
}
void
cont_handler (int sig)
{
signal (SIGCONT, cont_handler);
signal (SIGTSTP, tstp_handler);
}
int
main (void)
{
signal (SIGCONT, cont_handler);
signal (SIGTSTP, tstp_handler);
...
}
30. Передача сигналов другому процессу
Функция kill может использоваться, чтобы послать сигнал
другому процессу. Несмотря на имя, она может использоваться
для множества вещей отличных от завершения процесса. Вот
некоторые примеры ситуаций, когда можно посылать сигналы
между процессами:
Родительский процесс назначает дочернему выполнять задачу, возможно имеющую бесконечный цикл, и завершает дочерний
процесс, когда задача больше ненужна.
Процесс выполняется как часть группы, и должен завершать
или информировать другие процессы в группе, когда происходитошибка или другое событие.
Два процесса должны синхронизироваться при работе вместе.
Функция kill объявлена в "signal.h".
int kill (pid_t pid, int signum) (функция)
125
Функция kill посылает сигнал signum процессу или группе с ,
заданным pid. signum может также иметь нулевое значение, чтобы проверить правильность pid. Pid определяет процесс или группу
процессов, как получателя сигнала:
Pid > 0
Процесс, чей идентификатор – pid.
Pid == 0
Все процессы в той же самой группе что и отправитель. Отправитель непосредственно не получает сигнал.
Pid < -1
Группа процесса, чей идентификатор есть -pid.
Pid == -1
Если процесс привилегирован, посылает сигнал всем процессам,
кроме некоторых специальных процессов системы. Иначе, посылает сигнал всем процессам с тем же самым эффективным ID пользователя.
Процесс может посылать сигнала себе обращением
kill (getpid (), signum).
Если kill используется процессом, чтобы послать сигнал себе, и
сигнал не блокирован, то kill позволяет этому процессу принять по
крайней мере один сигнал прежде чем он возвращается.
Возвращаемое значение из kill – нуль, если сигнал может быть
послан успешно. Иначе, никакой сигнал не послан, и возвращается
значение -1. Если pid определяет посылку сигнала отдельным процессам, kill успешно завершается, если он может посылать сигнал
по крайней мере одному из них. Нет никакого способа узнать, какой из процессов получил сигнал.
Следующие errno условия ошибки определены для этой функции:
EINVAL
аргумент signum – недопустимое или неподдерживаемое число.
EPERM
Вы не имеете привилегий, чтобы послать сигнал процессу или
любому из процессов в группе процесса, именованной pid.
ESCRH
id аргумент не относится к существующему процессу или группе.
int killpg (int pgid, int signum) (функция)
Подобна kill, но посылает сигнала группе процесса pgid. Эта
функция предусмотрена для совместимости с BSD.
Обращение kill (getpid (), sig) имеет тот же самый эффект как
raise (sig).
126
31. Права для использования kill
Имеются ограничения, которые запрещают использование kill,
чтобы послать сигнал любому процессу.
Они предназначены, чтобы предотвратить антиобщественное
поведение типа произвольного уничтожения процессов, принадлежащих другому пользователю. Обычно kill используется, чтобы
передать сигналы между родителем и дочерним процессами, или
процессами братьями, и в этих ситуациях Вы обычно имеете право
послать сигнал. Единственное общее исключение это когда выполняется программа setuid на дочернем процессе; если программа изменяет реальный UID также как эффективный UID, Вы не имете
права послать сигнал. Программа su делает это.
Имеет ли процесс право, чтобы послать сигнал другому процессу, определяется пользовательскими ID этих двух процессов. Вообще, чтобы можно было посылать сигнал другому процессу, посылающий процесс должен принадлежать привилегированному
пользователю (подобно "root"), или реальный, или эффективный
пользовательский ID процесса посылки должен соответствовать реальному, или эффективному пользовательскому ID процесса получения. В некоторых реализациях, родительский процесс способен
послать сигнал дочернему процессу, даже если пользовательские
ID не соответствуют. Другие реализации, могут предписывать другие ограничения.
Сигнал SIGCONT – частный случай. Он может быть послан, если
отправитель – часть того же самого сеанса что и получатель, независимо от пользовательских ID.
32. Использование kill для связи
Здесь приводится пример, показывающий, как сигналы могут
использоваться для межпроцессорной связи. Для этого предусмотрены сигналы SIGUSR1 и SIGUSR2. Так как эти сигналы фатальны по умолчанию, процесс, который, как предполагается, получает их, должен обрабатывать их через signal или sigaction.
В этом примере, родительского процесс порождает дочерний
процесс и ждет пока дочерний завершит инициализацию. Дочерний процесс сообщает родителю, когда он готов, посылая ему сигнал SIGUSR1, используя функцию kill.
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
127
#include <unistd.h>
volatile sig_atomic_t usr_interrupt = 0;
void
synch_signal (int sig)
{
usr_interrupt = 1;
}
/* Дочерний процесс выполняет эту функцию. */
void
child_function (void)
{
printf ("I’m here!!! My pid is %d.\n",
(int) getpid ());
kill (getppid (), SIGUSR1);
puts ("Bye, now....");
exit (0);
}
int
main (void)
{
struct sigaction usr_action;
sigset_t block_mask;
pid_t child_id;
sigfillset (&block_mask);
usr_action.sa_handler = synch_signal;
usr_action.sa_mask = block_mask;
usr_action.sa_flags = 0;
sigaction (SIGUSR1, &usr_action, NULL);
/* Создание дочернего процесса. */
child_id = fork ();
if (child_id == 0)
child_function ();
while (!usr_interrupt)
;
puts ("That’s all, folks!");
return 0;
}
Этот пример использует активное ожидание, что нежелательно,
потому что это потеря времени CPU, которое другие программы
могли бы использовать.
Блокированные Сигналы
128
Блокирование сигнала сообщает операционной системе, чтобы
он был задержан и передан позже. Вообще, программа просто так
не блокирует сигналы, она может также игнорировать их, устанавливая их действия как SIG_IGN. Но полезно блокировать сигналы
ненадолго, чтобы предотвратить прерывание чувствительных операций. Например:
Можно использовать функцию sigprocmask, чтобы блокировать
сигналы, в то время как Вы изменяете глобальные переменные, которые также изменяются обработчиками для этих сигналов.
Можно устанавливать sa_mask в вашем обращении к sigaction,
чтобы блокировать некоторые сигналы, в то время как выполняется специфический обработчик сигнала. Этим способом, обработчик сигнала может выполняться без того, чтобы прерываться
сигналами.
33. Польза блокированиия сигналов
Временное блокирование сигналов с sigprocmask дает возможность предотвратить прерывания при выполнении критических частей вашего кода. Если сигналы прибывают в эту часть программы,
они будут переданы позже, после того, как будут открыты.
Например, это полезно для совместного использования данных
обработчиком сигнала и остальной частью программы. Если тип
данных – не sig_atomic_t, то обработчик сигнала может выполняться, когда остальная часть программы закончила только половину чтения или запииси данных.
Чтобы сделать программу надежной, можно предотвращать выполнение обработчика сигнала, в то время как остальная часть программы исследует или изменяет эти данные, блокировав соответствующий сигнал в частях программы, которые касаются данных.
Блокирование сигналов также необходимо, когда необходимо
выполнить некоторое действие только, если сигнал не прибыл.
Предположм, что обработчик для сигнала устанавливает флаг типа
sig_atomic_t; Вы хотели бы проверить флаг и выполнить действие,
если флаг не установлен. Это ненадежно. Предположите, что сигнал передан немедленно после того, как Вы проверяете флаг, но
перед последовательным действием: тогда программа выполнит
действие, даже если сигнал прибыл.
Единственый способ проверять, надежно ли прибыл ли сигнал,
состоит в том, чтобы проверять это в то время, когда сигнал блокирован.
129
34. Наборы сигналов
Все функции блокирования сигнала используют структуру данных называемую набором сигналов, чтобы определить на какие
сигналы воздействовать. Таким образом, каждое действие включает две стадии: создание набора сигналов, и передача его как аргумента библиотечной функции.
Эти средства объявлены в заглавном файле "signal.h".
sigset_t
Тип данных sigset_t используется, чтобы представить набор
сигналов. Внутренне, он может быть выполнен как целый или как
структурный тип.
Для переносимости, чтобы инициализировать, изменять и читать информацию из объектов sigset_t, используйте только функции, описанные в этом разделе, не пробуйте манипулировать ими
непосредственно.
Имеются два способа инициализировать набор сигналов. Можно
первоначально определить его пустыми через sigemptyset и затем
добавлять заданные сигналы индивидуально. Можно определить
их полными через sigfillset и тогда ножно удалять заданные сигналы индивидуально.
Необходимо всегда инициализировать набор сигналов одной из
этих двух функций перед использованием. Не стоит устанавливать
все сигналы явно, потому что объект sigset_t может включать некоторую другую информацию (подобно полю версии) которое также
должно быть инициализировано.
int sigemptyset (sigset_t *set)
Эта функция инициализирует набор наборов сигналов, чтобы
исключить все определенные сигналы. Она всегда возвращает 0.
int sigfillset (sigset_t *set)
Эта функция инициализирует набор наборов сигналов, чтобы
включить все определенные сигналы. Возвращаемое значение – 0.
int sigaddset (sigset_t *set, int signum)
Эта функция добавляет сигнал signum к набору наборов сигналов. Все что она делает – изменяет набор; она не блокирует или не
открывает никаких сигналов.
Возвращаемое значение – 0 при успехе и -1 при отказе. Следующее errno – условие ошибки определено для этой функции:
EINVAL
аргумент знака не определяет допустимый сигнал.
int sigdelset (sigset_t *set, int signum)
130
Эта функция удаляет сигнал signum из набора сигналов.
Возвращаемое значение и условия ошибки – такие же как для
sigaddset.
Есть функция, которая проверяет, что сигналы находятся в наборе сигналов:
int sigismember (const sigset_t *set, int signum)
Функция sigismember проверяет, является ли сигнал signum элементом набора сигналов. Она возвращает 1, если сигнал
находится в наборе, 0 если нет, и -1, если имеется ошибка.
Следующее errno условие ошибки определено для функции:
EINVAL
аргумент signum не определяет допустимый сигнал.
Маска Сигналов Процесса
Набор сигналов, которые в настоящее время блокированы, называется маской сигналов. Каждый процесс имеет собственную маску сигналов. Когда создается новый процесс, он наследует маску
родителя. Вы можете блокировать или открывать сигналы с большей гибкостью, изменяя маску сигналов.
Прототип для sigprocmask функции находится в "signal.h".
int sigprocmask (int how, const sigset_t *set, sigset_t *oldset)
Функция Sigprocmask используется, чтобы исследовать или
изменять маску сигналов процесса, аргумент how определяет, как
изменяется маска сигналов, и должен быть одним из следующих
значений:
SIG_BLOCK
Блокирует сигналы в set, и добавляет их к существующей маске. Другими словами, новая маска – объединение существующей
маски и set.
SIG_UNBLOCK
Открывает сигналы в set и удаляет их из существующей маски.
SIG_SETMASK
Использует set для маски; игнорируя предыдущее значение
маски.
Последний аргумент, oldset, используется, чтобы возвратить
информацию относительно старой маски сигналов процесса. Если
необходимо только изменять маску без того, чтобы рассмотривать
ее, передавайте пустой указатель как oldset аргумент. Если Вы
хотите знать, что находится в маске, без того, чтобы заменить ее,
передайте пустой указатель для set. Oldset аргумент часто используется, чтобы запомнить предыдущую маску сигналов, для восстановления.
131
Если вызов sigprocmask открывает любые отложенные сигналы, то по крайней мере один из этих сигналов будет передан процессу прежде, чем sigprocmask возвртится. Порядок, в котором
передаются отложенные сигналы является не определенным, но
можно управлять порядком явно, делая несколько обращений
к sigprocmask, чтобы открывать различные сигналы по одному.
Sigprocmask функция возвращает 0, если она успешна, и -1, в противном случае. Следующие errno условия ошибки определены для
этой функции:
EINVAL
Аргумент how недопустим.
Блокировать SIGKILL и SIGSTOP нельзя, но если набор сигналов включает их, то sigprocmask только игнорируют их вместо
того, чтобы возвратить состояние ошибки.
35. Блокирование для проверки наличия сигнала
Предположим, что установлен обработчик для сигналов
SIGALRM, который устанавливает флаг всякий раз, когда прибывает сигнал, и ваша программа main проверяет этот флаг время от
времени и сбрасывает его. Можно предотвращать прибытие дополнительных сигналов SIGALRM в неподходящее время, ограничивая критическую часть кода обращениями к sigprocmask, примерно так:
sig_atomic_t flag = 0;
int
main (void)
{
sigset_t block_alarm;
...
sigemptyset (&block_alarm);
sigaddset (&block_alarm, SIGALRM);
while (1)
{
sigprocmask (SIG_BLOCK, &block_alarm,
NULL);
if (flag)
{
actions-if-not-arrived
flag = 0;
}
132
sigprocmask (SIG_UNBLOCK, &block_alarm,
NULL);
...
}
}
Блокирование Сигналов для Обработчика
Когда обработчик сигнала вызывается, как правило, нужно,
чтобы он закончился без прерываний другим сигналом. С момента старта обработчика до момента его окончания, необходимо блокировать сигналы, которые могли бы запутать его или разрушить
данные.
Когда функция обработчика вызывается для сигнала, этот сигнал
автоматически блокируется (в дополнение к любым другим сигналам, которые являются уже в маске сигналов процесса) пока обработчик выполняется. Если Вы устанавливаете обработчик для SIGTSTP,
например, то поступление этого сигнала, вынуждает дальнейшие
SIGTSTP сигналы ждать в течение выполнения обработчика.
Однако, по умолчанию, другие виды сигналов не блокированы;
они могут прибывать в течение выполнения обработчика.
Надежный способ блокировать другие виды сигналов в течение
выполнения обработчика состоит в том, чтобы использовать sa_
mask элемент структуры sigaction.
Вот пример:
#include <signal.h>
#include <stddef.h>
void catch_stop ();
void
install_handler (void)
{
struct sigaction setup_action;
sigset_t block_mask;
sigemptyset (&block_mask);
sigaddset (&block_mask, SIGINT);
sigaddset (&block_mask, SIGQUIT);
setup_action.sa_handler = catch_stop;
setup_action.sa_mask = block_mask;
setup_action.sa_flags = 0;
sigaction (SIGTSTP, &setup_action, NULL);
}
<tscreen><verb> Это более надежно чем блокирование друих
синалов явно в коде обработчика. Если Вы блокируете сигналы в
133
обработчике, Вы не можете избежать по крайней мере короткого
интервала в начале обработчика, где они еще не блокированы.
Вы не можете удалять сигналы из текущей маски процесса,
испольуя этот механим. Однако Вы можете делать обращения к
sigprocmask внутри вашего обработчика, чтобы блокировать или
открыть синалы, по Вашему желанию.
В любом случае, кода обработчик вовращается, система восстанавливает маску, которая была до обработчика.
<sect2>Прверка Отложенных Синалов
<p>
Вы можете выяснять, какие синалы отложены в любое время,
выывая
sigpending. Эта функция объявлена в "signal.h".
<tscreen><verb> int sigpending (sigset_t *set) (функция)
Sigpending функция сохраняет информацию относительно отложенных сигналов в set. Если там отложенный сигнал, который
блокирован, то этот сигнал – элемент возвращенного set. (Вы можете проверить, является ли специфический сигнал элемент этого
set, использующего sigismember. Возвращаемое значение – 0 при
успехе, и -1 при отказе.
Тестирование отложен ли сигнал полезно не часто. Тестирование сигнала который не блокирован, почти всегда бессмысленно.
Вот пример.
#include <signal.h>
#include <stddef.h>
sigset_t base_mask, waiting_mask;
sigemptyset (&base_mask);
sigaddset (&base_mask, SIGINT);
sigaddset (&base_mask, SIGTSTP);
sigprocmask (SIG_SETMASK, &base_mask, NULL);
...
sigpending (&waiting_mask);
if (sigismember (&waiting_mask, SIGINT)) {
/* Пользователь пробовал уничтожать процесс. */
}
else if (sigismember (&waiting_mask, SIGTSTP)) {
/*Пользователь пробовал остановить процесс.*/
}
Не забудьте, что, если имеется задержка некоторого сигнала для
вашего процесса, дополнительные сигналы этого же самого типа
могут быть отброшены. Например, если сигнал SIGINT отложен,
134
когда прибывает другой сигнал SIGINT, ваша программа будет возможно видеть только один из них.
Примечание Переносимости: функция sigpending новая в
POSIX.1. Более старые системы не имеют никакого эквивалентного средства.
Запоминание Сигнала, для отложенного вызова
Вместо того, чтобы блокировать сигнал используя библиотечные
средства, можно получить почти те же самые результаты, делая так
чтобы обработчик устанавливал флаг, который будет проверен позже, когда Вы "откроете". Вот пример:
volatile sig_atomic_t signal_pending;
volatile sig_atomic_t defer_signal;
void
handler (int signum)
{
if (defer_signal)
signal_pending = signum;
else
... /*"Действительно" обрабатываем сигнал.*/
}
...
void
update_mumble (int frob)
{
defer_signal++;
mumble.a = 1;
mumble.b = hack ();
mumble.c = frob;
defer_signal--;
if (defer_signal == 0 && signal_pending != 0)
raise (signal_pending);
}
Обратите внимание, как специфический сигнал сохранен в
signal_pending. Этим способом, можно обрабатывать несколько типов неудобных сигналов.
Можно увеличивать или уменьшать defer_signal так, чтобы вложенные критические разделы работали правильно; таким образом,
если update_mumble вызывалась с signal_pending, уже отличным
от нуля, сигналы будут отсрочены не только внутри update_mumble,
но также внутри вызывающего оператора. Вот почему signal_
pending,не проверяется если defer_signal все еще отличен от нуля.
135
Приращение и уменьшение defer_signal требует больше чем
одну команду; и возможно сигнал случиться в середине. Но это не
вызывает никакой проблемы. Если сигнал случается достаточно
рано чтобы увидеть значение до приращения или уменьшения, то
это эквивалентно сигналу который, пришел перед началом приращения или уменьшения, что является случаем который работает
правильно.
Абсолютно необходимо увеличить defer_signal перед тестированием signal_pending, потому что это позволяет избежать тонкой
ошибки. Если бы мы делали это в другом порядке, примерно так,
if (defer_signal == 1 && signal_pending != 0)
raise (signal_pending);
defer_signal--;
то сигнал, прибывающий между условным оператором и
оператором уменьшения был бы эффективно "потерян" на
неопределенное количество времени. Обработчик просто установил
бы defer_signal, но программа, уже проверявшая эту переменную,
не будет проверять переменную снова.
Ошибки подобно этим, называются ошибками синхронизации.
Они особенно опасны, потому что они случаются редко и их почти
невозможны воспроизвести. Вы не сможете найти их отладчиком,
как Вы нашли бы воспроизводимую ошибку. Так что надо быть особенно осторожным, чтобы избежать их.
36. Ожидание сигнала
Если ваша программа управляется внешними событиями, или
использует сигналы для синхронизации, то она должена возможно
ждать, пока сигнал не прибудет.
Использование pause
Простой способ ждать прибытия сигнала – вызвать pause.
int pause () (функция)
Функция pause приостанавливает выполнение программы, пока
не прибывает сигнал, чье действие должно также выполнить функцию обработчика, или завершить процесс.
Если сигнал выполняет функцию обработчика, то pause возвращается. Это рассматривается как неудача (так как "успешное" поведение должно было бы приостановить программу навсегда), так
что возвращаемое значение -1. Даже если Вы определяете, что другие примитивы должны продолжиться, когда обработчик системы
возвращается это не имеет никакого эффекта на pause; она всегда
терпит неудачу, когда сигнал обработан.
136
Следующие errno условия ошибки определены для этой функции:
EINTR
функция была прервана сигналом.
Если сигнал вызывает окончание программы, pause не возвращается.
Функция pause объявлена в "unistd.h".
Проблемы с pause
Простота pause может скрывать серьезные ошибки синхронизации, которые могут привести программу к зависанию.
Безопасно использовать pause, если реальная работа вашей программы выполняется обработчиками сигнала непосредственно,
а программа не делает ничего кроме обращения к pause. Каждый
сигнал будет заставлять обработчик делать следующий пакет работы, которая должна быть выполнена, и возвращаться, так чтобы
цикл программы мог вызывать pause снова.
Вы не можете безопасно использовать pause, чтобы ждать, пока
не прибудет еще один сигнал, и тогда продолжить реальную работу.
Даже если приняты меры, чтобы обработчик сигнала сотрудничал, устанавливая флаг, все еще невозможно использовать pause
надежно. Вот пример такой проблемы:
if (!usr_interrupt)
pause ();
/* работа, после прибытия сигнала. * /
...
Она имеет ошибку: сигнал может прибывать после того, как переменная usr_interrupt проверена, но перед обращением к pause.
Если никакие дальнейшие сигналы не прибывают, процесс никогда не выполнится снова.
Можно изменять верхнее ограничение ожидания, используя
sleep в цикле, вместо того чтобы использовать pause. Вот, на что это
походит:
while (!usr_interrupt)
sleep (1);
/* работа, после прибытия синала. */
...
Можно ждать, пока специфический обработчик сигнала не выполнен, используя sigsuspend.
Использование sigsuspend
Чистый и надежный способ ждать сигнал состоит в том, чтобы
блокировать его и тогда использовать sigsuspend.
137
Используя sigsuspend в цикле, можно ждать некоторые виды
сигналов, разрешая другим видам сигналов обрабатываться их обработчиками.
int sigsuspend (const sigset_t *set) (функция)
Эта функция заменяет маску сигналов процесса на set и тогда
приостанавливает процесс, пока не передан сигнал, чье действие
должно завершать процесс или вызывать функцию обработки
сигнала. Другими словами, программа действительно будет
приостановлена, пока один из сигналов, который – не элемент set,
не прибудет.
Если процесс пробужден сигналом, который вызывает функцию
обработчика, и функция обработчика возвращается, то sigsuspend
также возвращается.
Маска остается set только, пока sigsuspend ждет. Функция
sigsuspend всегда восстанавливает предыдущую маску сигналов,
когда она возвращается.
Возвращаемое значение и условия ошибки – такие же как для
pause.
Вот надежный вариант:
sigset_t mask, oldmask;
...
sigemptyset (&mask);
sigaddset (&mask, SIGUSR1);
...
/ * Ждем получения сигнала. * /
sigprocmask (SIG_BLOCK, &mask, &oldmask);
while (!usr_interrupt)
sigsuspend (&oldmask);
sigprocmask (SIG_UNBLOCK, &mask, NULL);
Когда sigsuspend возвращается, она сбрасывает маску сигналов процесса к первоначальному значению, в этом случае сигнал
SIGUSR1 еще раз блокирован. Второе обращение к sigprocmask необходимо чтобы явно открыть этот сигнал.
37. BSD обработка сигнала
Этот раздел описывает альтернативные функции обработки сигнала, происходящие от UNIX BSD. Эти средства поддерживаются в
основном для совместимости с UNIX BSD.
Они обеспечивают одну возможность, которая не доступна через
функции POSIX: Можно определять отдельный стек для исполь138
зования в некоторых обработчиках сигнала. Использование стека
сигнала единственый способ, которым можно обрабатывать сигнал, вызванный переполнением стека.
38. POSIX и BSD средства обработки сигналов
Имеются много подобий между средствами BSD и POSIX для обработки сигналов, потому что средства POSIX были вдохновлены
средствами BSD. Кроме наличия различных имен для функций,
чтобы избежать конфликтов, следует отметить эти различия:
UNIX BSD представляет маски сигналов как int битовая маска,
а не как объект sigset_t.
Средства BSD используют отличное значение по умолчанию для
про­верки, должен ли прерванный примитив терпеть неудачу или
нет. Средства POSIX делают сбой системных вызовов, если Вы не
определяете, что они должны продолжиться. Со средством BSD,
значение по умолчанию не должно делать сбой системных вызовов,
если не оговорено, что они должны терпеть неудачу.
UNIX BSD имеет понятие стека сигналов. Это альтернативный
стек, который используется в течение выполнения функций обработчика сигнала, вместо стека нормального выполнения.
Средства BSD объявлены в "signal.h".
39. Функция BSD, для установки обработчика
struct sigvec (тип данных)
Этот тип данных – эквивалент BSD struct sigaction он используется, чтобы опреде­лить действия сигнала для sigvec функции. Он
содержит следующие эле­менты:
sighandler_t sv_handler
Это – функция обработчика.
int sv_mask
Это – маска дополнительных сигналов, которые будут блокированы, в то время как функция обработчика вызывается.
int sv_flags
Это – битовая маска, используемая, чтобы определить различные флаги, которые воздействуют на поведение сигнала. Вы можете также обратиться к этому полю как sv_onstack. Эти символические константы могут использоваться, чтобы обеспе­чить значения
для sv_flags поля структуры sigvec. Это поле – значение битовой
маски, следовательно необходимо слить флаги, представляющие
интерес для Вас вместе через OR.
139
int SV_ONSTACK
Если этот бит установлен в sv_flags поле структуры sigvec, это
означает – использовать стек сигнала при получении сигнала.
int SV_INTERRUPT (макрос)
Если этот бит установлен в sv_flags поле структуры sigvec, это
означает что, системные вызовы, прерванные этим видом сигнала
не долж­ ны быть перезапущены, если обработчик возвращается;
взамен, системные вызовы должны возвратиться с EINTR состоянием ошибки.
int SV_RESETHAND (макрос)
Если этот бит усткновлен в sv_flags поле структуры sigvec, это
означает – сбросить действие для сигнала обратно к SIG_DFL, когда сиг­нал получен.
int sigvec (int signum, const struct sigvec *action,struct sigvec
*old_action)
Эта функция – эквивалент sigaction; она устанавливает действие
для сигнала signum, возвращая информацию относительно
предыдущего действия для этого сигнала в old_action.
int siginterrupt (int signum, int failflag) (функция)
Эта функция определяет, что использовать, когда некоторые
прими­тивы прерваны обрабаткой сигнала signum. Если failflag –
ложь, то при­митивы рестартуют после сигнала. Если failflag – истина, обработка signum заставляет эти примитивы терпеть неудачу
с кодом ошибки EINTR.
40. Функции BSD для блокирования сигналов
int sigmask (int signum) (макрос)
Эта макрокоманда возвращает маску сигналов, которая имеет
бит для установки сигнала signum. Можете слить через OR результаты отдель­ных обращений к sigmask вместе, чтобы определять
больше чем один сиг­нал. Например,
(sigmask (SIGTSTP) | sigmask (SIGSTOP)
| sigmask (SIGTTIN) | sigmask (SIGTTOU))
определяет маску, которая включает все сигналы останова
управления заданиями.
int sigblock (int mask) (функция)
Эта функция эквивалентна sigprocmask с аргументом how –
SIG_BLOCK: она добавляет сигна­ лы, заданные маской к набору
блокированных сигналов процесса вызова. Возвращаемое значение – предыдущий набор блокированных сигналов.
int sigsetmask (int mask) (функция)
140
Это эквивалент функции sigprocmask с аргументом how – SIG_
SETMASK: она устанавливает маску сигналов вызывающего процесса как mask. Возвращаемое значение предыдущий набор блокированных сигналов.
int sigpause (int mask) (функция)
Эта функция – эквивалент sigsuspend: она устанавливает маску
сигналов вызывающего процесса как mask, и ждет прибытия сигнала. Она при возвращаении восстанавливает предыдущий набор
блокированных сигналов.
41. Использование отдельного стека сигнала
Стек сигнала – специальная область памяти, которую нужно
исполь­зовать как стек в течение выполнения обработчиков сигнала. Он должен быть довольно большим, чтобы избежать переполнения; макрокоманда SIGS­ TKSZ определяет канонический размер для стеков сигналов. Вы можете ис­ пользовать malloc, чтобы
зарезервировать пространство для стека. Вызо­вите sigaltstack или
sigstack, чтобы система использовала это прост­ранство для стека
сигнала.
При этом не нужно писать обработчик сигнала по-другому чтобы использо­вать стек сигнала. Переключение одного стека на другой происходит ав­томатически.
Имеются два интерфейса для сообщения системе использовать
отдель­ный стек сигнала. Sigstack – более старый интерфейс, который исходит из 4.2 BSD. Sigaltstack – более новый интерфейс, и исходит из 4.4 BSD. Интерфейс sigaltstack имеет преимущество – не
требуется, чтобы ваша программа знала в каком направлении растет стек, что зависит от специ­ фической машины и операционной
системы.
struct sigaltstack (тип данных)
Эта структура описывает стек сигнала. Она содержит следующие эле­менты:
void *ss_sp
Этим указываем на основание стека сигнала.
size_t ss_size
размер (в байтах) стека сигнала, на который указывает "ss_sp".
Вы должны установить здесь, сколько места Вы зарезервирова­ли
для стека. Есть две макрокоманды, определенные в "signal.h" которые необходимо использовать в вычислении этого размера:
SIGSTKSZ
141
каноническиий размер для стека сигнала. Он должен быть дос­
таточным для использования.
MINSIGSTKSZ
количество пространства стека сигнала, нужное операционной
системе только, чтобы выполнить сигнал. Размер стека сигнала
должен быть больший чем этот.
Для большинства случаев SIGSTKSZ для ss_size достаточен. Можете использовать различный размер. В этом случае,необходимо
зарезервировать MINSIGSTKSZ дополнительных байт для стека
сигнала и увеличивать ss_size.
int ss_flags
Это поле содержит поразрядное OR этих флагов:
SA_DISABLE
Сообщает системе, что она не должна использовать стек сигнала.
SA_ONSTACK
Устанавливается системой, и указывает, что стек сигнала использован в настоящее время.
int sigaltstack (const struct sigaltstack *stack, struct sigaltstack
*oldstack) (функция)
Sigaltstack функция определяет альтернативный стек для
использо­вания в течение обработки сигнала.
Если oldstack – не пустой указатель, информация относительно
в настоящее время установленного стека сигнала будет возвращена
в распо­ложение, на которое он указывает. Если stack – не пустой
указатель, то он будет установлен как новый стек для использования обработчиками сигнала.
Возвращаемое значение – 0 при успехе и -1 при отказе. Если si­
galtstack сбоит, она устанавливает errno как одно из этих значений:
EINVAL
Вы пробовали отключать стек, который был фактически использован в настоящее время.
ENOMEM
Размер альтернативного стека был слишком мал. Он должен
быть большее чем MINSIGSTKSZ.
Вот более старый интерфейс sigstack.
struct sigstack (тип данных)
Эта структура описывает стек сигнала. Она содержит следующие эле­менты:
void *ss_sp
указатель вершины стека. Если стек растет вниз на вашей машине, он должен указывать на начало области, которую Вы заре142
зервировали. Если стек растет вверх, он должен указывать на нижнюю часть.
int ss_onstack
Это поле истинно, если процесс в настоящее время использует
этот стек.
int sigstack (const struct sigstack *stack, struct sigstack *oldstack) (функция)
Sigstack функция определяет альтернативный стек для использования в течение обработки сигнала.
Когда сигнал получен процессом, и стек сигнала используется,
система переключается на в настоящее время установленный стек
сигнала, в то время как выполняется обработчик для этого сигнала.
Если oldstack – не пустой указатель, информация относительно
в настоящее время установленного стека сигнала будет возвращена
в расположение, на которое он указывает. Если stack – не пустой
указатель, то он будет установлен как новый стек для использования обработчиками сигнала.
Возвращаемое значение – 0 при успехе и –1 при отказе.
143
СОДЕРЖАНИЕ
Список сокращений............................................................
1. Ядро и модули операционной системы...............................
1.1. Функции ядра.........................................................
1.2. Вспомогательные модули..........................................
1.3. Ядро в привилегированном режиме............................
1.4. Процесс и модель процесса........................................
1.5. Создание процессов..................................................
1.6. Завершение процесса................................................
1.7. Иерархия процессов.................................................
1.8. Состояния процессов................................................
1.9. Переключение процессов..........................................
1.10. Потоки и модель потока..........................................
1.11. Межпроцессорное взаимодействие. Состояние состязания...........................................................................
1.12. Критические области..............................................
1.13. Запрещение прерываний и переменные блокировки....
1.14. Алгоритм Петерсона и команда TSL..........................
1.15. Примитивы межпроцессорного взаимодействия.........
1.16. Проблема производителя и потребителя....................
1.17. Семафоры и решение проблемы производителя
и потребителя................................................................
1.18. Мьютексы.............................................................
1.19. Функции ОС по управлению памятью.......................
1.20. Типы адресов.........................................................
1.21. Образ процесса и виртуальное адресное пространство..
1.22. Методы распределения памяти.................................
1.23. Свопинг и виртуальная память.................................
1.24. Страничное распределение памяти...........................
1.25. Преобразование виртуальной страницы в физическую
1.26. Сегментное распределение памяти............................
1.27. Сегментно-страничное распределение памяти............
1.28. КЭШ-память.........................................................
1.29. Устройства ввода-вывода.........................................
1.30. Способы организации ввода-вывода .........................
1.31. Использование нескольких шин для ввода-вывода......
1.32. Прямой доступ к памяти (DMA)...............................
1.33. Процедура прерываний. Контроллер прерываний.......
1.34. Принципы прогрманого обеспечения ввода-вывода.....
1.35. Програмный ввод-вывод..........................................
144
3
4
4
4
5
8
9
10
10
11
13
15
16
18
18
19
20
20
21
22
23
23
24
25
28
30
31
33
35
38
40
42
45
47
49
50
51
1.36. Управляемый прерываниями ввод-вывод. Использование DМА....................................................................
1.37. Программные уровни ввода-вывода..........................
1.38. Обработка прерываний и драйверы...........................
1.39. Независимое от устройств программное обеспечение
ввода-вывода.................................................................
1.40. Буферизация ввода-вывода......................................
Вопросы к первому разделу.................................................
2. UNIX-подобные ОС.........................................................
2.1. Структура ядра операционной системы UNIX..............
2.2. Загрузка UNIX.......................................................
2.3. Оболочка UNIX.......................................................
2.4. Процессы в системе UNIX.........................................
2.5. Управление процессами в UNIХ.................................
2.6. Управление процессами в UNIХ.................................
2.7. Системные вызовы управления потоками....................
2.8. Сигналы.................................................................
Вопросы ко второму разделу................................................
3. Задания и примеры.........................................................
3.1. Описание некоторых команд и системных вызовов
linux............................................................................
3.2. Системные вызовы управления процессами.................
3.3. Компилятор GCC.....................................................
3.4. Задания..................................................................
Задание 1. Процессы и потоки. Часть 1.................................
Задание 2. Процессы и потоки. Часть 2.................................
Задание 3. Управление процессами.......................................
Задание 4. Системные вызовы..............................................
Задание 5. Сигналы............................................................
Пример 1..........................................................................
Пример 2..........................................................................
Пример 3..........................................................................
Список использованных источников.....................................
Приложение. Библиотека языка C GNU glibc с комментариями..................................................................................
51
52
52
54
55
56
58
59
61
63
65
67
69
73
75
76
78
78
79
83
84
84
85
86
86
87
88
91
93
95
96
145
Учебное издание
Калюжный Виталий Павлович
Зац Кирилл Валерьевич
ОПЕРАЦИОННЫЕ СИСТЕМЫ
Учебное пособие
Редактор А. В. Подчепаева
Верстальщик С. Б. Мацапура
Сдано в набор 22.05.12. Подписано к печати 27.09.12.
Формат 60×84 1/16. Бумага офсетная. Усл. печ. л. 8,5.
Уч.-изд. л. 9,1. Тираж 100 экз. Заказ № 495.
Редакционно-издательский центр ГУАП
190000, Санкт-Петербург, Б. Морская ул., 67
Документ
Категория
Без категории
Просмотров
8
Размер файла
1 950 Кб
Теги
kaluzniizayc
1/--страниц
Пожаловаться на содержимое документа