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

4. В функции-оболочке выполняется машинный код системного прерывания (int 0x80), заставляющий процессор переключиться из пользовательского режима в режим ядра и выполнить код, указатель на который расположен в векторе прерывания системы 0x80 (в десятичной системе счисления — 128).

В более современных архитектурах x86-32 реализуется инструкция sysenter, предоставляющая более быстрый способ входа в режим ядра, по сравнению с обычной инструкцией системного прерывания int 0x80. Использование sysenter поддерживается в версии ядра 2.6 и в glibc, начиная с версии 2.3.2.

5. В ответ на системное прерывание 0x80 ядро для его обработки инициирует свою подпрограмму system_call() (которая находится в ассемблерном файле arch/x86/kernel/entry.S). Обработчик прерывания делает следующее;

1) сохраняет значения регистров в стеке ядра (см. раздел 6.5);

2) проверяет допустимость номера системного вызова;

3) вызывает соответствующую подпрограмму обслуживания системного вызова. Ее поиск ведется по номеру системного вызова: в таблице всех подпрограмм обслуживания системных вызовов в качестве индекса используется номер системного вызова (переменная ядра sys_call_table). Если у подпрограммы обслуживания системного вызова имеются аргументы, то она сначала проверяет их допустимость. Например, она проверяет, что адреса указывают на места, допустимые в пользовательской памяти. Затем подпрограмма обслуживания системного вызова выполняет требуемую задачу, которая может предполагать изменение значений адресов, указанных в переданных аргументах, и перемещение данных между пользовательской памятью и памятью ядра (например, в операциях ввода-вывода). И наконец, подпрограмма обслуживания системного вызова возвращает подпрограмме system_call() код возврата;

4) восстанавливает значения регистров из стека ядра и помещает в стек возвращаемое значение системного вызова;

5) возвращает управление функции-оболочке, одновременно переводя процессор в пользовательский режим.

6. Если возвращаемое значение подпрограммы обслуживания системного вызова свидетельствует о возникновении ошибки, то функция-оболочка присваивает это значение глобальной переменной errno (см. раздел 3.4). Затем функция-оболочка возвращает управление вызывавшему ее коду, предоставляя ему целочисленное значение, указывающее на успех или неудачу системного вызова.

В Linux подпрограммы обслуживания системных вызовов следуют соглашению о том, что для указания успеха возвращается неотрицательное значение. При ошибке подпрограмма возвращает отрицательное число, являющееся значением одной из errno-констант с противоположным знаком. Когда возвращается отрицательное значение, функция-оболочка библиотеки C меняет его знак на противоположный (делая его положительным), копирует результат в errno и возвращает значение –1, чтобы указать вызывающей программе на возникновение ошибки.

Это соглашение основано на предположении, что подпрограммы обслуживания системных вызовов в случае успеха не возвращают отрицательных значений. Но для некоторых таких подпрограмм это предположение неверно. Обычно это не вызывает никаких проблем, поскольку диапазон превращенных в отрицательные числа значений errno не пересекается с допустимыми отрицательными возвращаемыми значениями. Тем не менее в одном случае все же появляется проблема — когда дело касается операции F_GETOWN системного вызова fcntl(), рассматриваемого в разделе 59.3.

На рис. 3.1 на примере системного вызова execve() показана описанная выше последовательность. В Linux/x86-32 execve() является системным вызовом под номером 11 (__NR_execve). Следовательно, 11-я запись в векторе sys_call_table содержит адрес sys_execve(), подпрограммы, обслуживающей этот системный вызов. (В Linux подпрограммы обслуживания системных вызовов обычно имеют имена в формате sys_xyz(), где xyz() является соответствующим системным вызовом.)

Рис. 3.1. Этапы выполнения системного вызова

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

В качестве примера издержек на осуществление системного вызова рассмотрим системный вызов getppid(). Он просто возвращает идентификатор родительского процесса, которому принадлежит вызывающий процесс. В одной из принадлежащих автору книги x86-32-систем с запущенной Linux 2.6.25 на совершение 10 миллионов вызовов getppid() ушло приблизительно 2,2 секунды. То есть на каждый вызов ушло около 0,3 микросекунды. Для сравнения, на той же системе на 10 миллионов вызовов функции языка C, которая просто возвращает целое число, ушло 0,11 секунды, или около 1/12 времени, затраченного на вызовы getppid(). Разумеется, большинство системных вызовов имеет более существенные издержки, чем getppid().