Глава 5 Системные вызовы

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

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

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

/proc
, в конечном счете сводятся к обращению через системные вызовы.

Интересно, что в ОС Linux реализовано значительно меньше системных вызовов, чем во многих других операционных системах[23].

В этой главе рассказывается о роли и реализации системных вызовов в операционной системе Linux.

API, POSIX и библиотека С

Обычно прикладные программы не разрабатываются с непосредственным использованием системных вызовов, при этом используются программные интерфейсы приложений (Application Programing Interface, API). Это является важным, так как в таком случае нет необходимости в корреляции между интерфейсами, которые используют приложения, и интерфейсами, которые предоставляет ядро. Различные API определяют набор программных интерфейсов, которые используются приложениями. Эти интерфейсы могут быть реализованы с помощью одного системного вызова, нескольких системных вызовов, а также вообще без использования системных вызовов. В действительности, может существовать один и тот же программный интерфейс приложений для различных операционных систем, в то время как реализация этих API может для разных ОС существенно отличаться.

Один из наиболее популярных программных интерфейсов приложений в мире Unix-подобных систем базируется на стандарте POSIX. Технически стандарт POSIX включает в себя набор стандартов IEEE[24], целью которого является обеспечение переносимого стандарта операционной системы, приблизительно базирующегося на ОС Unix. ОС Linux соответствует стандарту POSIX.

Стандарт POSIX является хорошим примером соотношения между интерфейсом API и системными вызовами. Для большинства Unix-подобных операционных систем вызовы интерфейса API, определенные в стандарте POSIX, сильно коррелируют с системными вызовами. Конечно, стандарт POSIX создавался для того, чтобы сделать те интерфейсы, которые предоставляли ранние версии ОС Unix, похожими между собой. С другой стороны, некоторые операционные системы, далекие от OS Unix, такие как Windows NT, предоставляют библиотеки, совместимые со стандартом POSIX.

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

Рис. 5.1. Взаимоотношения между приложением, библиотекой С и ядром на примере вызова функции

printf()

Дополнительно библиотека функций С также представляет большую часть API-стандарта POSIX.

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

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

Вызовы syscall

Системные вызовы (часто называемые syscall в ОС Linux) обычно реализуются в виде вызова функции. Для них могут быть определены один или более аргументов (inputs), которые могут приводить к тем или иным побочным эффектам[25], например к записи данных в файл или к копированию некоторых данных в область памяти, на которую указывает переданный указатель. Системные вызовы также имеют возвращаемое значение типа

long
[26], которое указывает на успешность выполнения операции или на возникшие ошибки. Обычно, но не всегда, возвращение отрицательного значения указывает на то, что произошла ошибка. Возвращение нулевого значения обычно (но не всегда) указывает на успешность выполнения операции. Системные вызовы ОС Unix в случае ошибки записывают специальный код ошибки в глобальную переменную
errno
. Значение этой переменной может быть переведено в удобочитаемую форму с помощью библиотечной функции
perror()
.

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

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

asmlinkage long sys_getpid(void) {

 return current->tgid;

)

Следует заметить, что в определении ничего не говорится о способе реализации. Ядро должно обеспечить необходимую функциональность системного вызова, но реализация может быть абсолютно свободной, главное, чтобы результат был правильный. Конечно, рассматриваемый системный вызов в действительности является таким же простым, как и показано, и существует не так уж много различных вариантов для его реализации (на самом деле более простого метода не существует)[27].

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

asmlinkage
в объявлении функции. Это волшебное слово дает компилятору информацию о том, что обращение к аргументам этой функции должно производиться только через стек. Для всех системных вызовов использование этого модификатора является обязательным. Во-вторых, следует обратить внимание, что системный вызов
getpid()
объявлен в ядре, как
sys_getpid()
. Это соглашение о присваивании имен используется для всех системных вызовов операционной системы Linux: системный вызов
bar()
должен быть реализован с помощью функции
sys_bar()
.

Номера системных вызовов

Каждому системному вызову операционной системы Linux присваивается номер системного вызова (syscall number). Этот уникальный номер используется для обращения к определенному системному вызову. Когда процесс выполняет системный вызов из пространства пользователя, процесс не обращается к системному вызову по имени.

Номер системного вызова является важным атрибутом. Однажды назначенный номер не должен меняться никогда, иначе это нарушит работу уже скомпилированных прикладных программ. Если системный вызов удаляется, то соответствующий номер не может использоваться повторно. В операционной системе Linux предусмотрен так называемый "не реализованный" ("not implemented") системный вызов — функция

sys_ni_syscall()
, которая не делает ничего, кроме того, что возвращает значение, равное
-ENOSYS
, — код ошибки, соответствующий неправильному системному вызову. Эта функция служит для "затыкания дыр" в случае такого редкого событии, как удаление системного вызова.

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

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 выдержали это испытание временем. Большинство из них такие же полезные и применимые сегодня, как и почти тридцать лет назад!

Проверка параметров

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

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

PID
допустимым. Каждый параметр должен проверяться не только на предмет допустимости и законности, но и на предмет правильности значения.

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

• Указатель указывает на область памяти в пространстве пользователя. Нельзя, чтобы процесс заставил ядро обратиться к памяти ядра от имени процесса.

• Указатель указывает на область памяти в адресном пространстве текущего процесса. Нельзя позволять, чтобы процесс заставил ядро читать данные других процессов.

• Для операций чтения есть права на чтение области памяти. Для операций записи есть права на запись области памяти. Нельзя, чтобы процессы смогли обойти ограничения на чтение и запись.

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

Для записи в пространство пользователя предоставляется функция

copy_to_user()
. Она принимает три параметра: адрес памяти назначения в пространстве пользователя; адрес памяти источника в пространстве ядра; и размер данных, которые необходимо скопировать, в байтах.

Для чтения из пространства пользователя используется функция

copy_from_user()
, которая аналогична функции
copy_to_user()
. Эта функция считывает данные, на которые указывает второй параметр, в область памяти, на которую указывает первый параметр, количество данных — третий параметр.

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

-EFAULT
.

Давайте рассмотрим пример системного вызова, который использует функции

copy_from_user()
и
copy_to_user()
. Системный вызов
silly_copy()
является до крайности бесполезным. Он просто копирует данные из своего первого параметра во второй. Это очень не эффективно, так как используется дополнительное промежуточное копирование в пространство ядра безо всякой причины. Но зато это позволяет проиллюстрировать суть дела.

/*

* Системный вызов silly copy — крайне бесполезная функция,

* которая копирует len байтов из области памяти,

* на которую указывает параметр src, в область памяти,

* на которую указывает параметр dst, с использованием ядра

* безо всякой на то причины. Но это хороший пример!

*/

asmlinkage long sys_silly_copy(unsigned long *src,

 unsigned long *dst, unsigned long len) {

 unsigned long buf;

 /* возвращаем ошибку, если размер машинного слова в ядре

   не совпадает с размером данных, переданных пользователем */

 if (len != sizeof(buf))

  return -EINVAL;

 /* копируем из src, который является адресом в пространстве

   пользователя, в buf */

 if (copy_from_user(&buf, src, len))

  return -EFAULT;

 /* копируем из buf в dst, который тоже является адресом

   в пространстве пользователя */

 if (copy_to_user(dst, &buf, len))

  return -EFAULT;

 /* возвращаем количество скопированных данных */

 return len;

}

Следует заметить, что обе функции,

copy_from_user()
и
copy_to_user()
, могут блокироваться. Это возникает, например, если страница памяти, содержащая данные пользователя, не находится в физической памяти, а в данный момент вытеснена на диск. В таком случае процесс будет находиться в приостановленном состоянии до тек пор, пока обработчик прерываний из-за отсутствия страниц (page fault handler) не возвратит страницу памяти в оперативную память из файла подкачки на диске.

Последняя проверка — это проверка на соответствие правам доступа. В старых версиях ядра Linux стандартом было использование функции

suser()
для системных вызовов, которые требуют прав пользователя root. Эта функция просто проверяла, запущен ли процесс от пользователя root. Сейчас эту функцию убрали и заменили более мелко структурированным набором системных "возможностей использования" (capabilities). В новых системах предоставляется возможность проверять специфические права доступа к специфическим ресурсам. Функция
capable()
с допустимым значением флага, определяющего тип прав, возвращает ненулевое значение, если пользователь обладает указанным правом, и нуль— в противном случае. Например, вызов
capable
(
CAP_SYS_NICE
) проверяет, имеет ли вызывающий процесс возможность модифицировать значение параметра nice других процессов. По умолчанию суперпользователь владеет всеми правами, а пользователь, не являющийся пользователем root, не имеет никаких дополнительных прав. Следующий пример системного вызова, который демонстрирует использование возможностей использования, тоже является практически бесполезным.

asmlinkage long sys_am_i_popular(void) {

 /* Проверить, имеет пи право процесс использовать

   возможность CAP_SYS_NICE */

 if (!capable(CAP_SYS_NICE))

  return -EPERM;

 /* Возвратить нуль, чтобы обозначить успешное завершение */

 return 0;

}

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

.

Контекст системного вызова

Как уже обсуждалось в главе 3, "Управление процессами", при выполнении системного вызова ядро работает в контексте процесса. Указатель

current
указывает на текущее задание, которое и есть процессом, выполняющим системный вызов.

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

schedule()
), а также является полностью вытесняемым. Эти два момента важны. Возможность переходить в приостановленное состояние означает, что системный вызов может использовать большую часть функциональных возможностей ядра. Как будет видно из главы 6, "Прерывания и обработка прерываний", наличие возможности переходить в приостановленное состояние значительно упрощает программирование ядра[29]. Тот факт, что контекст процесса является вытесняемым, подразумевает, что, как и в пространстве пользователя, текущее задание может быть вытеснено другим заданием. Так как новое задание может выполнить тот же системный вызов, необходимо убедиться, что системные вызовы являются реентерабельными. Это очень похоже на требования, выдвигаемые для симметричной мультипроцессорной обработки. Способы защиты, которые обеспечивают реентерабельность, описаны в главе 8, "Введение в синхронизацию выполнения кода ядра", и в главе 9, "Средства синхронизации в ядре".

После завершение системного вызова управление передается обратно в функцию

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

Окончательные шаги регистрации системного вызова

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

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

• Для всех поддерживаемых аппаратных платформ номер системной функции должен быть определен в файле

include/linux/unistd.h
.

• Системный вызов должен быть вкомпилирован в образ ядра (в противоположность компиляции в качестве загружаемого модуля[30]). Это просто соответствует размещению кода в каком-нибудь важном файле каталога

kernel/
.

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

foo()
. Вначале функция
sys_fоо()
должна быть добавлена в таблицу системных вызовов. Для большинства аппаратных платформ таблица системных вызовов размещается в файле
entry.S
и выглядит примерно следующим образом.

ENTRY (sys_call_table)

 .long sys_restart_syscall / * 0 * /

 .long sys_exit

 .long sys_fork

 .long sys_read

 .long sys_write

 .long sys_open /* 5 */

...

 .long sys_timer_delete

 .long sys_clock_settime

 .long sys_clock_gettime /* 280 */

 .long sys_clock_getres

 .long sys_clock_nanosleep

Необходимо добавить новый системный вызов в конец этого списка:

.long sys_foo

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

Далее необходимо добавить номер системного вызова в заголовочный файл

include/asm/unistd.h
, который сейчас выглядит примерно так.

/*

* This file contains the system call numbers.

*/

#define __NR_restart_syscall 0

#define __NR_exit 1

#define __NR_fork 2

#define __NR_read 3

#define __NR_write 4

#define __NR_open 5

...

#define __NR_mq_unlink    278

#define __NR_mq_timedsend   279

#define __NR_mq_timedreceive 280

#define __NR_mq_notify    281

#define __NR_mq_getsetattr  282

В конец файла добавляется следующая строка.

#define __NR_foo 283

В конце концов необходимо реализовать сам системный вызов

foo()
. Так как системный вызов должен быть вкомпилорован в образ ядра во всех конфигурациях, мы его поместим в файл
kernel/sys.c
. Код необходимо размещать в наиболее подходящем файле. Например, если функция относится к планированию выполнения процессов, то ее необходимо помещать в файл
sched.c
.

/*

* sys_foo - всеми любимый системный вызов.

*

* Возвращает размер стека ядра процесса

*/

asmlinkage long sys_foo(void) {

 return THREAD_SIZE;

}

Это все! Загрузите новое ядро. Теперь из пространства пользователя можно вызвать системную функцию

foo()
.

Доступ к системным вызовам из пространства пользователя

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

glibc
уже его поддерживает!

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

int $0x80
. Эти макросы имеют имя
syscalln()
, где
n
— число от нуля до шести. Это число соответствует числу параметров, которые должны передаваться в системный вызов, так как макросу необходима информация о том, сколько ожидается параметров, и соответственно, нужно записать эти параметры в регистры процессора. Например, рассмотрим системный вызов
open()
, который определен следующим образом.

long open(const char *filename, int flags, int mode)

Макрос для вызова этой системной функции будет выглядеть так.

#define NR_open 5

_syscall3(long, NR_open, const char*, filename, int, flags, int, mode);

После этого приложение может просто вызывать функцию

open()
.

Каждый макрос принимает

2 + 2*n
параметров. Первый параметр соответствует типу возвращаемого значения системного вызова. Второй параметр — имя системного вызова. После этого следуют тип и имя каждого параметра в том же порядке, что и у системного вызова. Постоянная
NR_open
, которая определена в файле
, — это номер системного вызова. В функцию на языке программирования С такой вызов превращается с помощью вставок на языке ассемблера, которые выполняют рассмотренные в предыдущем разделе шаги. Значения аргументов помещаются в соответствующие регистры, и выполняется программное прерывание, которое перехватывается в режиме ядра. Вставка данного макроса в приложение — это все, что необходимо для выполнения системного вызова
open()
.

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

#define __NR_foo 283

__syscall0()(long, foo)


int main() {

 long stack_size;


stack_size = foo();

 printf("Размер стека ядра равен %ld\n" , stack_size);

 return 0;

}

Почему не нужно создавать системные вызовы

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

Для создания нового интерфейса в виде системного вызова могут быть следующие "за".

• Системные вызовы просто реализовать и легко использовать.

• Производительность системных вызовов в операционной системе Linux очень высока.

Возможные "против".

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

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

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

• Для простого обмена информацией системный вызов — это "стрельба из пушки по воробьям".

Возможные варианты.

• Реализовать файл устройства и использовать функции

read()
и
write()
для этого устройства, а также использовать функцию
ioctl()
для манипуляции специфическими параметрами или для получения специфической информации.

• Некоторые интерфейсы, например семафоры, могут быть представлены через дескрипторы файлов. Управлять этими устройствами также можно по аналогии с файлами.

• Добавить информационный файл в соответствующем месте файловой системы

sysfs
.

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

Малая частота добавления новых системных вызовов свидетельствует о том, что Linux — это стабильная операционная система с полным набором функций. Очень немного системных вызовов было добавлено во время разработки серий ядер 2.3 и 2.5. Большая часть из новых системных вызовов предназначена для улучшения производительности.

В заключение о системных вызовах

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

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

В конце главы были рассмотрены "за" и "против" относительно реализации системных вызовов и представлен краткий список возможных вариантов добавления новых системных вызовов.

Загрузка...