введение_1 (1085732), страница 12
Текст из файла (страница 12)
Системный вызов (и библиотечная процедура) возвращает количество действительно прочитанных байтов в переменной count. Обычно эта величина совпадает с параметром nbytes, но может быть меньше, если, например, в процессе чтения процедуре встретился конец файла.
Если системный вызов не может быть выполнен или из-за неправильных параметров или из-за дисковой ошибки, значение счетчика count устанавливается равным -1, а номер ошибки помещается в глобальную переменную еrrпо. Программы всегда должны проверять результат системного вызова, чтобы отслеживать появление ошибки.
Системные вызовы выполняются за серию шагов. Вернемся к упоминавшемуся выше примеру вызова read для того, чтобы разъяснить этот момент. Сначала при подготовке к вызову библиотечной процедуры read, которая фактически осуществляет системный вызов read, вызывающая программа помещает параметры в стек, как показано в шагах 1-3 на рис. 1.17. Компиляторы С и C++ помещают параметры в стек в обратном порядке, так исторически сложилось (чтобы первым был параметр для printf, то есть строка формата оказалась на вершине стека). Первый и третий параметры передаются по значению, а второй параметр передается по ссылке, то есть передается адрес буфера (на то, что это ссылка, указывает символ &), а не его содержимое. Затем следует собственно вызов библиотечной процедуры (шаг 4). Эта команда процессора представляет собой обычную команду вызова процедуры и применяется для вызова любых процедур.
Библиотечная процедура, возможно, написанная на ассемблере, обычно помещает номер системного вызова туда, где его ожидает операционная система, например в регистр (шаг 5). Затем она выполняет команду TRAP (эмулированное прерывание) для переключения из пользовательского режима в режим ядра и начинает выполнение с фиксированного адреса внутри ядра (шаг 6). Запускаемая
программа ядра проверяет номер системного вызова и затем отправляет его нужному обработчику, как правило, используя таблицу указателей на обработчики системных вызовов, индексированную по номерам вызовов (шаг 7). В этом месте начинает функционировать обработчик системных вызовов (шаг 8). Как только он завершает свою работу, управление может возвращаться в пространство пользователя к библиотечной процедуре, к команде, следующей за командой TRAP (шаг 9). Эта процедура в свою очередь передает управление программе пользователя обычным способом, которым производится возврат из вызванной процедуры (шаг 10). Чтобы закончить работу, программа пользователя должна очистить стек, как это делается и после каждого вызова процедуры (шаг 11). Учитывая, что стек растет вниз, последняя команда увеличивает указатель стека ровно настолько, насколько нужно для удаления параметров, помещенных в стек перед запросом read. Теперь программа может продолжать свою работу.
А
Возврат к вызывающей процедуре
Переключение в режим ядрa
5 Передача кода для чтения в регистр
4 10 9
Увеличение показателя 11
Вызов read
-
Передача f d
2 Передача &buffer
1 Передача nbytes
6
7 8 8
8
дреса0xFFFFFFFF
Б
иблиотечная
процедура read
П
ространство
пользователя
Программа Программа
пользователя
вызывающая
read
Обработчик системного вызова
Я
Отправка

О
перации-
Оная
система
Рис. 1.17. 11 этапов выполнения системного вызова read(fd, buffer, nbytes)
На шаге 9 мы использовали выражение «может возвращаться в пространство пользователя к библиотечной процедуре...» не просто так. Системный вызов может блокировать вызвавшую его процедуру, препятствуя продолжению ее работы. Например, если она пытается прочесть что-то с клавиатуры, а там еще ничего не набрано, процедура должна быть блокирована. В этом случае операционная система ищет процесс, который может быть запущен следующим. Позже, когда нужное устройство станет доступно, система вспомнит о блокированном процессе и шаги 9-11 будут выполнены.
В следующих разделах мы рассмотрим некоторые из наиболее часто применяющихся системных вызовов стандарта POSIX или, точнее, библиотечных процедур, которые выполняют эти вызовы. В POSIX существует более 100 процедурных вызовов. Часть наиболее важных процедурных вызовов представлена в табл. 1.1, где они для удобства распределены в четыре группы. Далее мы кратко опишем каждый вызов и его действие. Службы, предоставляемые этими вызовами, в значительной степени определяют действия операционной системы, так как управление ресурсами на персональном компьютере минимально (по крайней мере, по сравнению с большими машинами, на которых работают несколько пользователей). К этим службам относятся такие функции, как создание и завершение процессов, создание, удаление, чтение и запись файлов, управление каталогами, выполнение ввода и вывода.
Таблица 1.1. Некоторые из основных системных вызовов POSIX
В ызов Описание
У правление процессами
P id=fork()1 Создает дочерний процесс, идентичный родительскому
Pid=waitpid(pid, &statloc, options) Ожидает завершения дочернего процесса
s=execve(name, argv, environp) Перемещает образ памяти процесса
Exit(status) Завершает выполнение процесса и возвращает статус
У правление файлами
f d=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, указывающий
на namel
s=unlink(name) Удаляет элемент каталога
s=mount(special, name, flag) Монтирует файловую систему
s=umount(special) Демонтирует файловую систему
Р азные
s =chdir(dimame) Изменяет рабочий каталог
s=chmod(name, mode) Изменяет биты защиты файла
s=kill(pid, signal) Посылает сигнал процессу
Seconds=time(&seconds) Получает время, прошедшее с 1 января 1970 года
1
Возвращаемая величина s равна -1, если произошла ошибка. Возвращаемые коды выглядят так: pid выдает идентификатор процесса, fd — описатель файла, п — количество байтов, position — смещение в файле и seconds — прошедшее время. Параметры описываются дальше в тексте.
Особое внимание следует обратить на то, что преобразование вызовов процедур POSIX в системные вызовы не является взаимно однозначным. Стандарт POSIX определяет ряд процедур, которые должны поддерживать совместимые системы, но он не указывает, являются ли они системными вызовами, библиотечными вызовами или чем-нибудь еще. Если процедуру можно выполнить без системного вызова (то есть без переключения в режим работы ядра), то обычно она работает в пространстве пользователя, потому что так быстрее. Однако большинство процедур POSIX выполняет системные вызовы, обычно с одной процедурой, преобразующейся напрямую в системный вызов. В некоторых случаях, особенно когда требуемые процедуры являются всего лишь разновидностями друг друга, один системный вызов обрабатывает сразу несколько библиотечных вызовов.
Системные вызовы для управления процессами
Первая группа в табл. 1.1 управляет процессами. Начнем рассмотрение с вызова fork. Системный вызов fork (разветвление) является единственным способом создания нового процесса в UNIX. Он создает точную копию исходного процесса, включая дескрипторы файла, регистры и т. п. После вызова fork исходный процесс и его копия (родительский и дочерний) развиваются по отдельности друг от друга. Все переменные имеют одинаковые величины во время вызова fork, но как только родительские данные скопированы для создания дочернего процесса, последующие изменения в одном из них уже не влияют на другой. (Текст программы, который не изменяется, распределяется между родительским и дочерним процессами). Вызов fork возвращает величину, равную нулю в дочернем процессе и равную идентификатору дочернего процесса или PID в родительском. Используя возвращенный PID, два процесса могут различить, какой из них родительский, а какой — дочерний.
В большинстве случаев после вызова fork дочернему процессу необходимо выполнить программный код, отличный от предназначенного для родительского процесса. Рассмотрим пример оболочки. Она читает команды с терминала, запускает дочерний процесс, ждет, пока дочерний процесс выполнит команду, и читает следующую команду после завершения работы дочернего процесса. Ожидая, пока дочерний процесс закончит работу, родительский процесс выполняет системный вызов waitpid, который ожидает завершения дочернего процесса (или всех дочерних процессов, если их на данный момент несколько). Waitpid может ждать окончания какого-либо определенного дочернего процесса или любого дочернего процесса, для этого нужно задать первый параметр вызова равным -1. Когда waitpid выполнен, указатель, задаваемый вторым параметром statlос, будет установлен на статус завершения дочернего процесса (нормальное или аварийное завершение и выходное значение). Третий параметр определяет различные необязательные настройки.
Теперь рассмотрим, как вызов fork используется оболочкой. Когда печатается команда, оболочка создает дочерний процесс, который должен выполнить команду пользователя. Он делает это с помощью системного вызова execve, заменяющего весь его образ памяти файлом, названным в первом параметре. (Фактически самим системным вызовом является exec, но несколько различных библиотечных
процедур вызывают его с разными параметрами и незначительно отличающимися именами. Мы здесь воспользуемся ими как системными вызовами.) Весьма упрощенная оболочка, иллюстрирующая использование команд fork, waitpid и execve, показана в листинге 1.1.
Листинг 1.1. Усеченная оболочка1
#define TRUE I
while (TRUE) { /* вечный цикл */