Системный вызов — это запрос к ОС, чтобы она выполнила некоторые операции для пользовательского процесса. В современных операционных системах некоторые операции ни в коем случае не могут выполняться непосредственно пользовательским процессом без помощи ОС. Примерами таких операций являются чтение с диска, запись на диск, разветвление процесса и т. д.
Зачем нужны системные вызовы
Есть две основные причины, по которым такие инструкции недоступны непосредственно для процесса:
- Первый — защита: ОС должна сделать множество проверок, чтобы оценить право процесса запрашивать такую инструкцию.
- Второй заключается в том, что ОС должна обновлять свои структуры данных при выполнении инструкции. Если мы воспользуемся системным вызовом fork, ОС потребуется обновить множество структур данных, связанных с процессами, чтобы учесть новый процесс, добавить его в очередь выполнения и т. д.
Функция-оболочка системного вызова
Следует сделать очень важное различие. В C, когда мы открываем файл с помощью fopen("filename.txt","r")
, мы не делаем системный вызов. Вместо этого мы вызываем функцию fopen
библиотеки C, которая сделает за нас системный вызов open
. Код функции fopen определен в реализации библиотеки C, такой как Glibc, а код вызова open
system определен в ядре. ОС.
Переключение из пользовательского режима в режим ядра
Когда процесс выполняется, он может работать в двух режимах: пользовательском режиме или режиме ядра. Он запускается в пользовательском режиме, когда выполняет обычные инструкции ЦП, не требующие привилегий, таких как переход по адресу, загрузка из памяти, запись в память и т. д.Однако, когда процесс должен выполняться с привилегиями инструкции, которые он должен передать операционной системе для выполнения от ее имени, это то, что мы называем режимом ядра.
При выполнении системного вызова процесс должен переключиться из пользовательского режима в режим ядра, поскольку он собирается выполнять привилегированные инструкции. Переключение из пользовательского режима в режим ядра включает в себя множество изменений в состоянии и привилегиях текущего процесса:
- Понятие текущей привилегии хранится в процессоре. Например, в процессорах x86 эта информация доступна в регистре CS под двухбитным флагом CPL (текущий уровень привилегий). Его значение равно 0 в режиме ядра и 3 в пользовательском режиме. Таким образом, первым шагом системного вызова является изменение значения CPL на 0.
- Сегмент кода больше не является сегментом кода процесса (который включает инструкции выполняемой программы). Теперь сегмент кода является сегментом кода ядра (это потому, что инструкции
open
доступны не в исполняемой программе, а в коде ядра). - Сегмент стека больше не указывает на пользовательский стек, а указывает на стек ядра. Каждый процесс (в более общем случае каждый поток) имеет 2 стека: стек пользовательского режима, в котором мы выполняем инструкции в программе, и стек режима ядра, в котором мы выполняем инструкции ядра, такие как обработка системных вызовов. Есть много причин, по которым у нас два стека вместо одного. По сути, это вопрос защиты, чтобы никакая информация, которую ядро использовало при выполнении, не была доступна процессу при переключении обратно в пользовательский режим, а во-вторых, чтобы код ядра не отвечал за переполнение стека пользовательской программы…
Для переключения программы из пользовательского режима в режим ядра нет другого способа, кроме прерываний.
В общем, прерывание может быть аппаратным прерыванием, таким как прерывание таймера, которое заставляет ЦП переключаться в режим ядра и выполнять, например, переключение процесса, или это может быть программное прерывание, вызванное самой программой, такое как деление нулем, ошибкой страницы или .. системным вызовом.
Прерывания обрабатываются операционной системой с помощью таблицы, называемой таблицей дескрипторов прерываний (IDT), которая сопоставляет каждый тип прерывания с функцией, которую ОС будет выполнять при возникновении прерывания (мы называем эту функцию обработчиком прерывания). Например, первая запись этой таблицы может содержать код, который будет выполняться при делении на ноль.
Инструкция «int $0x80»
Чтобы сделать системный вызов в процессоре x86, мы выполняем инструкцию int $0x80
. int означает прерывание, а 0x80
(шестнадцатеричное число, соответствующее 128) — это позиция в таблице IDT обработчика системных вызовов. Перед вызовом этой инструкции мы сохраняем в четко определенных регистрах номер системного вызова функции, которую мы хотим выполнить (открыть, прочитать, разветвить) и ее аргументы. Например, номер системного вызова должен храниться в регистре EAX.
Выполнение этой инструкции произведет переключение из режима пользователя в режим ядра. Он извлекает сегмент стека ядра из сегмента TSS, а затем помещает регистры режима пользователя в этот стек ядра (регистр сегмента пользовательского стека SS, указатель стека ESP, регистр сегмента кода CS, EIP указателя инструкции и eflags). Затем сегмент кода ядра и указатель инструкции извлекаются из таблицы IDT, на которую указывает регистр idtr .
По завершении выполнения int $0x80
указатель инструкций указывает на первую инструкцию в обработчике системных вызовов, которая выполнит некоторые проверки и некоторое сохранение состояния перед выполнением инструкций read
, open
, .. или любого другого системного вызова, который мы сделали.
Стоит подчеркнуть разницу между обычным вызовом функции и системным вызовом. Когда мы выполняем вызов функции, мы помещаем аргументы в стек и переходим к функции. Системный вызов не может сделать то же самое, потому что мы не выполняем в одном и том же стеке, поэтому аргументы скорее хранятся в регистрах, и поскольку код ядра не находится в том же сегменте кода, поэтому простая инструкция перехода не будет работать. .
Возврат из системного вызова: инструкция «iret»
Когда ядро завершило выполнение системного вызова, оно должно вернуть руку пользовательскому процессу. Точно так же, как существует специальная инструкция для переключения из пользовательского режима в режим ядра, существует еще одна специальная инструкция для переключения обратно в пользовательский режим. Речь идет о iret
инструкции. Он извлекает пользовательские регистры, которые были отправлены int(
CS, SS, ESP, EIP и eflags, и сохраняет их обратно в соответствующих регистрах. Сделав это, мы вернулись в пользовательский режим и готовы продолжить выполнение нашей программы.