Добавлен: 29.10.2018
Просмотров: 47988
Скачиваний: 190
76
Глава 1. Введение
чтение и удаление. А управление ресурсами компьютера проходит большей частью
незаметно для пользователей и осуществляется в автоматическом режиме. Так что
интерфейс между пользовательскими программами и операционной системой строится
в основном на абстракциях. Чтобы по-настоящему понять, что делает операционная
система, мы должны более подробно рассмотреть этот интерфейс. Имеющиеся в интер-
фейсе системные вызовы варьируются в зависимости от используемой операционной
системы (хотя основные понятия практически ничем не различаются).
Теперь нужно выбрать между неопределенной обобщенностью («у операционных
систем есть системные вызовы для чтения файлов») и какой-нибудь конкретной
системой («в UNIX есть системный вызов чтения, имеющий три параметра: в одном
из них определяется файл, в другом — куда поместить данные, а в третьем — сколько
байтов следует считать»).
Мы выбрали второй подход. Пусть он сложнее, зато позволяет лучше понять, как на
самом деле операционная система выполняет свою работу. Хотя все, что будет рассма-
триваться, имеет непосредственное отношение к стандарту POSIX (Международный
стандарт 9945-1), а следовательно к UNIX, System V, BSD, Linux, MINIX 3 и т. д.,
у большинства других современных операционных систем имеются системные вызовы,
выполняющие аналогичные функции, при некоторых отличиях в деталях. Посколь-
ку фактический механизм выполнения системного вызова существенно зависит от
конкретной машины и зачастую должен быть реализован на ассемблере, разработаны
библиотеки процедур, осуществляющие системные вызовы из программ, написанных,
например, на языке C.
Очень полезно всегда помнить следующее. Любой однопроцессорный компьютер одно-
моментно может выполнить только одну команду. Когда процесс выполняет пользова-
тельскую программу в режиме пользователя и нуждается в какой-нибудь услуге опера-
ционной системы, например в чтении данных из файла, он должен выполнить команду
системного прерывания, чтобы передать управление операционной системе. Затем опера-
ционная система по параметрам вызова определяет, что именно требуется вызывающему
процессу. После этого она обрабатывает системный вызов и возвращает управление той
команде, которая следует за системным вызовом. В некотором смысле выполнение си-
стемного вызова похоже на выполнение особой разновидности вызова процедуры, с той
лишь разницей, что системные вызовы входят в ядро, а процедурные — нет.
Для того чтобы прояснить механизм системных вызовов, рассмотрим системный
вызов чтения — read. Как уже упоминалось, он имеет три параметра: первый служит
для задания файла, второй указывает на буфер, а третий задает количество байтов,
которое нужно прочитать. Как практически все системные вызовы, он осуществляется
из программы на языке C с помощью вызова библиотечной процедуры, имя которой
совпадает с именем системного вызова: read. Вызов из программы на C может иметь
следующий вид:
count = read(fd, buffer, nbytes);
Системный вызов (и библиотечная процедура) возвращает количество фактически
считанных байтов, которое сохраняется в переменной count. Обычно это значение
совпадает со значением параметра nbytes, но может быть и меньше, если, например,
в процессе чтения будет достигнут конец файла.
Если системный вызов не может быть выполнен из-за неправильных параметров или
ошибки диска, значение переменной count устанавливается в −1, а номер ошибки по-
1.6. Системные вызовы
77
мещается в глобальную переменную errno. Программам обязательно нужно проверять
результаты системного вызова, чтобы отслеживать возникновение ошибки.
Выполнение системного вызова состоит из нескольких шагов. Для прояснения ситуа-
ции вернемся к упоминавшемуся ранее примеру вызова read. Сначала, при подготовке
вызова библиотечной процедуры read, которая фактически и осуществляет системный
вызов read, вызывающая программа помещает параметры в стек (рис. 1.17, шаги 1–3).
Рис. 1.17. 11 этапов выполнения системного вызова read(fd, buffer, nbytes)
Компиляторы C и C++ помещают параметры в стек в обратном порядке, следуя истори-
чески сложившейся традиции (чтобы на вершине стека оказался первый параметр функ-
ции printf — строка формата вывода данных). Первый и третий параметры передаются
по значению, а второй параметр передается по ссылке, поскольку это адрес буфера (о чем
свидетельствует знак &), а не его содержимое. Затем осуществляется фактический вызов
библиотечной процедуры (шаг 4). Эта команда представляет собой обычную команду
вызова процедуры и используется для вызова любых процедур.
Библиотечная процедура, возможно, написанная на ассемблере, обычно помещает но-
мер системного вызова туда, где его ожидает операционная система, например в регистр
(шаг 5). Затем она выполняет команду TRAP для переключения из пользовательского
режима в режим ядра, и выполнение продолжается с фиксированного адреса, находя-
щегося внутри ядра операционной системы (шаг 6). Фактически команда TRAP очень
похожа на команду вызова процедуры в том смысле, что следующая за ней команда
берется из удаленного места, а адрес возврата сохраняется в стеке для последующего
использования.
78
Глава 1. Введение
Тем не менее у команды TRAP и команды вызова процедуры есть два основных раз-
личия. Во-первых, побочный эффект, заключающийся в переключении в режим ядра.
Команда вызова процедуры не меняет используемый режим. А во-вторых, коман-
да TRAP не может получить относительный или абсолютный адрес местонахождения
процедуры, поскольку не может осуществить переход на произвольный адрес.
Начавшая работу после команды TRAP часть ядра (диспетчер на рис. 1. 17) проверяет
номер системного вызова, а затем передает управление нужному обработчику. Обычно
передача управления осуществляется посредством таблицы указателей на обработчики
системных вызовов, которая индексирована по номерам этих вызовов (шаг 7). После
этого вступает в действие обработчик конкретного системного вызова (шаг 8). Как
только обработчик закончит работу, управление может быть возвращено библиотеч-
ной процедуре, находящейся в пользовательской области памяти, той самой команде,
которая следует за командой TRAP (шаг 9). В свою очередь эта процедура вернет управ-
ление пользовательской программе по обычной схеме возврата из процедуры (шаг 10).
Чтобы завершить работу с процедурой read, пользовательская программа должна очи-
стить стек, точно так же, как она это делает после любого вызова процедуры (шаг 11).
Если в нашем примере стек растет вниз (как это чаще всего и бывает), пользовательская
программа в скомпилированном виде должна содержать команды увеличения указате-
ля стека ровно настолько, чтобы были удалены параметры, помещенные в стек перед
вызовом процедуры read. Теперь программа может продолжить свою работу.
При рассмотрении шага 9 было специально отмечено, что «управление может быть воз-
вращено библиотечной процедуре, находящейся в пользовательской области памяти».
Системный вызов может заблокировать вызывающую программу, препятствуя про-
должению ее работы. Например, вызывающая программа должна быть заблокирована
при попытке чтения с клавиатуры, когда на ней еще ничего не набрано. В этом случае
операционная система ищет другой процесс, который может быть запущен. Позже, ког-
да станут доступны требуемые входные данные, система вспомнит о заблокированном
процессе и будут выполнены шаги с 9-го по 11-й.
В следующих разделах мы рассмотрим некоторые из наиболее востребованных си-
стемных вызовов стандарта POSIX, или, точнее, библиотечных процедур, осущест-
вляющих эти системные вызовы. В стандарте POSIX определено более 100 процедур,
обеспечивающих обращение к системным вызовам. Некоторые наиболее важные из
них и сгруппированные для удобства по категориям перечислены в табл. 1.1. Далее мы
кратко опишем каждый вызов и его назначение.
Услуги, предоставляемые этими системными вызовами, в значительной степени опре-
деляют большую часть возможностей операционной системы, поскольку управление
ресурсами на персональных компьютерах осуществляется в минимальном объеме (во
всяком случае, по сравнению с большими машинами, обслуживающими множество
пользователей). Они включают в себя такие виды обслуживания, как создание и преры-
вание процессов, создание, удаление, чтение и запись файлов, управление каталогами,
ввод и вывод данных.
Отдельно стоит упомянуть, что отображение процедурных вызовов POSIX на систем-
ные вызовы не является взаимно однозначным. В стандарте POSIX определены про-
цедуры, которые должна предоставить совместимая с ним система, но он не указывает,
чем именно они должны быть реализованы: системными, библиотечными вызовами
или чем-нибудь еще. Если процедура может быть выполнена без системного вызова
1.6. Системные вызовы
79
(то есть без переключения в режим ядра), то она из соображений производительности
обычно выполняется в пользовательском пространстве. Однако большинство процедур
POSIX осуществляют системные вызовы, при этом обычно одна процедура непосред-
ственно отображается на один системный вызов. В некоторых случаях, особенно когда
несколько необходимых системе процедур мало чем отличаются друг от друга, один
системный вызов обрабатывает более одного вызова библиотечных процедур.
Таблица 1.1. Некоторые важнейшие системные вызовы POSIX
1
Вызов
Описание
Управление процессом
pid = fork()
Создает дочерний процесс, идентичный родительскому
pid = waitpid(pid, &statloc, options)
Ожидает завершения дочернего процесса
s = execve(name, argv, environp)
Заменяет образ памяти процесса
exit(status)
Завершает выполнение процесса и возвращает статус
Управление файлами
fd = open(file, how...)
Открывает файл для чтения, записи или для того и дру-
гого
s = close(fd)
Закрывает открытый файл
n = read(fd, buffer, nbytes)
Читает данные из файла в буфер
n = write(fd, buffer, nbytes)
Записывает данные из буфера в файл
position = lseek(fd, offset, whence)
Перемещает указатель файла
s = stat(name, &buf)
Получает информацию о состоянии файла
Управление каталогами и файловой системой
s = mkdir(name, mode)
Создает новый каталог
s = rmdir(name)
Удаляет пустой каталог
s = link(name1, name2)
Создает новый элемент с именем name2, указывающий
на name1
s = unlink(name)
Удаляет элемент каталога
s = mount(special, name, flag)
Подключает файловую систему
s = umount(special)
Отключает файловую систему
Разные
s = chdir(dirname)
Изменяет рабочий каталог
s = chmod(name, mode)
Изменяет биты защиты файла
s = kill(pid, signal)
Посылает сигнал процессу
seconds = time(&seconds)
Получает время, прошедшее с 1 января 1970 года
1
При возникновении ошибки возвращаемое значение кода завершения s равно –1. Исполь-
зуются следующие имена возвращаемых значений: pid — id процесса, fd — описатель файла,
n — количество байтов, position — смещение внутри файла и seconds — прошедшее время.
Описания параметров приведены в тексте.
80
Глава 1. Введение
1.6.1. Системные вызовы для управления процессами
Первая группа вызовов в табл. 1.1 предназначена для управления процессами. Начнем
рассмотрение системного вызова fork (разветвление). Вызов fork является единствен-
ным существующим в POSIX способом создания нового процесса. Он создает точную
копию исходного процесса, включая все дескрипторы файлов, регистры и т. п. После
выполнения вызова fork исходный процесс и его копия (родительский и дочерний
процессы) выполняются независимо друг от друга. На момент разветвления все их со-
ответствующие переменные имеют одинаковые значения, но поскольку родительские
данные копируются в дочерний процесс, последующие изменения в одном из них не
влияют на изменения в другом. (Текст программы, не подвергающийся изменениям,
является общим для родительского и дочернего процессов.) Системный вызов fork
возвращает нулевое значение для дочернего процесса и равное идентификатору до-
чернего процесса или PID — для родительского. Используя возвращенное значение
PID, два процесса могут определить, какой из них родительский, а какой — дочерний.
В большинстве случаев после вызова fork дочернему процессу необходимо выполнить
программный код, отличный от родительского. Рассмотрим пример работы системной
оболочки. Она считывает команду с терминала, создает дочерний процесс, ожидает,
пока дочерний процесс выполнит команду, а затем считывает другую команду, если
дочерний процесс завершается. Для ожидания завершения дочернего процесса роди-
тельский процесс выполняет системный вызов waitpid, который просто ждет, пока до-
черний процесс не закончит свою работу (причем здесь имеется в виду любой дочерний
процесс, если их несколько). Системный вызов waitpid может ожидать завершения
конкретного дочернего процесса или любого из запущенных дочерних процессов, если
первый параметр имеет значение −1. Когда работа waitpid завершается, по адресу, ука-
занному во втором параметре — statloc, заносится информация о статусе завершения
дочернего процесса (нормальное или аварийное завершение и выходное значение).
В третьем параметре определяются различные необязательные настройки.
Теперь рассмотрим, как вызов fork используется оболочкой. После набора команды
оболочка создает дочерний процесс, который должен выполнить команду пользова-
теля. Он делает это, используя системный вызов execve, который полностью заменяет
образ памяти процесса файлом, указанным в первом параметре. (Фактически сам
системный вызов называется exec, но некоторые библиотечные процедуры со слегка
отличающимися именами вызывают его с различными параметрами. Здесь мы будем
рассматривать все это как системные вызовы.) В листинге 1.1 показано использование
fork, waitpid и execve предельно упрощенной оболочкой.
Листинг 1.1. Оболочка, усеченная до минимума
#define TRUE 1
while (TRUE) { /* бесконечный цикл */
type_prompt( ); /* вывод приглашения на экран */
read_command(command, parameters); /* чтение ввода с терминала */
if (fork( ) != 0) { /* ответвление дочернего процесса */
/* код родительского процесса */
waitpid(-1, &status, 0); /* ожидание завершения дочернего процесса */
} else {
/* код дочернего процесса */