Выбрать главу

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

Производительность системных вызовов

Системные вызовы в операционной системе Linux работают быстрее, чем во многих других операционных системах. Это отчасти связано с невероятно малым временем переключения контекста. Переход в режим ядра и выход из него являются хорошо отлаженным процессом и простым делом. Другой фактор — это простота как механизма обработки системных вызовов, так и самих системных вызовов.

Обработка системных вызовов

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

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

Таким механизмом, который может подать сигнал ядру, является программное прерывание: создается исключительная ситуация (exception) и система переключается в режим ядра для выполнения обработчика этой исключительной ситуации. Обработчик исключительной ситуации в данном случае и является обработчиком системного вызова (system call handler). Для аппаратной платформы x86 это программное прерывание определено как машинная инструкция int $0x80. Она приводит в действие механизм переключения в режим ядра и выполнение вектора исключительной ситуации с номером 128, который является обработчиком системных вызовов. Обработчик системных вызовов— это функция с очень подходящим именем system_call(). Данная функция зависима от аппаратной платформы и определена в файле entry.S[28]. В новых процессорах появилась такая новая функция, как sysenter. Эта функция обеспечивает более быстрый и специализированный способ входа в ядро для выполнения системного вызова, чем использование инструкции программного прерывания — int. Поддержка такой функции была быстро добавлена в ядро. Независимо от того, каким образом выполняется системный вызов, основным является то, что пространство пользователя вызывает исключительную ситуацию, или прерывание, чтобы вызвать переход в ядро.

Определение необходимого системного вызова

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

Для аппаратной платформы x86 номер системного вызова сохраняется в регистре процессора eax перед тем, как вызывается программное прерывание. Обработчик системных вызовов после этого считывает это значение из регистра eax. Для других аппаратных платформ выполняется нечто аналогичное.

Функция system_call() проверяет правильность переданного номера системного вызова путем сравнения его со значением постоянной NR_syscalls. Если значение номера больше или равно значению NR_syscalls, то функция возвращает значение -ENOSYS. В противном случае вызывается соответствующий системный вызов следующим образом:

call *sys_call_table(,%eax,4)

Так как каждый элемент таблицы системных вызовов имеет длину 32 бит (4 байт), то ядро умножает данный номер системного вызова на 4 для получения нужной позиции в таблице системных вызовов (рис. 5.2).

Рис. 5.2. Запуск обработчика системных вызовов и выполнение системного вызова

Передача параметров

В дополнение к номеру вызова, большинство системных вызовов требует передачи им одного или нескольких параметров. Во время перехвата исключительной ситуации пространство пользователя должно каким-либо образом передать ядру эти параметры. Самый простой способ осуществить такую передачу — это сделать по аналогии с передачей номера системной функции: параметры хранятся в регистрах процессора. Для аппаратной платформы x86 регистры ebx, ecx, edx, esi, edi содержат соответственно первые пять аргументов. В случае редких ситуаций с шестью или более аргументами, используется один регистр, который содержит указатель на память пространства пользователя, где хранятся все параметры.

Возвращаемое значение также передается в пространство пользователя через регистр. Для аппаратной платформа x86 оно хранится в регистре eax.

Реализация системных вызовов

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

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

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

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

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

вернуться

28

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