Введение

Способ для программ доступа к службам ядра

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

Привилегии

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

Библиотека как посредник

Как правило, системы предоставляют библиотеку или API, который располагается между обычными программами и операционной системой. В системах, подобных Unix, этот API обычно является частью реализации библиотеки C (libc), такой как glibc, предоставляющей функции-оболочки для системных вызовов, часто названные так же, как и сами системные вызовы, которые они вызывают. В Windows NT этот API является частью Native API, находящегося в библиотеке; это недокументированный API, используемый реализациями обычного Windows API и непосредственно некоторыми системными программами в Windows. Функции-оболочки библиотеки предоставляют стандартную конвенцию вызова функций (вызов подпрограммы на уровне ассемблера) для использования системного вызова, а также делают системный вызов более модульным. Основная функция оболочки заключается в размещении всех аргументов, которые должны быть переданы системному вызову, в соответствующие регистры процессора (и, возможно, в стек вызовов), а также в установке уникального номера системного вызова для вызова ядра. Таким образом, библиотека, находящаяся между ОС и приложением, повышает переносимость. Сам вызов функции библиотеки не вызывает переключения в режим ядра и обычно является обычным вызовом подпрограммы (например, с использованием инструкции "CALL" в некоторых архитектурах наборов команд (ISA)). Фактический системный вызов передает управление ядру (и больше зависит от реализации и платформы, чем от абстрагирующего его библиотечного вызова). Например, в системах, подобных Unix, fork и execve — это функции библиотеки C, которые, в свою очередь, выполняют инструкции, вызывающие системные вызовы fork и exec. Непосредственное выполнение системного вызова в коде приложения более сложно и может потребовать использования встроенного ассемблерного кода (в C и C++), а также знания низкоуровневого бинарного интерфейса для операции системного вызова, который может изменяться со временем и, следовательно, не является частью бинарного интерфейса приложения; функции библиотеки предназначены для абстрагирования этого. В системах на основе экзоядра библиотека особенно важна как посредник. В экзоядрах библиотеки защищают пользовательские приложения от очень низкоуровневого API ядра и предоставляют абстракции и управление ресурсами. В IBM OS/360, DOS/360 и TSS/360 большинство системных вызовов реализуются через библиотеку макросов языка ассемблера, хотя есть несколько сервисов с связью вызовов. Это отражает их происхождение во времена, когда программирование на языке ассемблера было более распространено, чем использование языков высокого уровня. Поэтому системные вызовы IBM не могли быть непосредственно выполнены программами высокого уровня, но требовали вызываемой подпрограммы-оболочки на языке ассемблера. С тех пор IBM добавила множество сервисов, которые могут быть вызваны из языков высокого уровня, например, в z/OS и z/VSE. В более поздних версиях MVS/SP и во всех последующих версиях MVS некоторые макросы системных вызовов генерируют Program Call (PC).

Примеры и инструменты

На Unix, Unix-подобных и других операционных системах, соответствующих стандарту POSIX, популярными системными вызовами являются open, read, write, close, wait, exec, fork, exit и kill. Многие современные операционные системы имеют сотни системных вызовов. Например, в Linux и OpenBSD насчитывается более 300 различных вызовов, в NetBSD – около 500, в FreeBSD – более 500, в Windows – около 2000, разделенных между графическими (win32k) и основными (ntdll) системными вызовами, а в Plan 9 – 51. Инструменты, такие как strace, ftrace и truss, позволяют отслеживать выполнение процесса с самого начала и регистрировать все системные вызовы, которые он выполняет, или подключаться к уже запущенному процессу и перехватывать любые системные вызовы, сделанные этим процессом, если операция не нарушает прав доступа пользователя. Эта особенность программ обычно также реализуется с помощью системных вызовов, таких как ptrace, или системных вызовов к файлам в procfs.

Типичные варианты реализации

Реализация системных вызовов требует передачи управления из пользовательского пространства в пространство ядра, что включает в себя специфическую особенность архитектуры. Типичный способ реализации – использование программного прерывания или ловушки. Прерывания передают управление ядру операционной системы, поэтому программному обеспечению достаточно установить в регистр необходимый номер системного вызова и выполнить программное прерывание. Это единственная техника, предоставляемая для многих RISC-процессоров, но архитектуры CISC, такие как x86, поддерживают дополнительные методы. Например, набор инструкций x86 содержит инструкции SYSCALL/SYSRET и SYSENTER/SYSEXIT (эти два механизма были независимо разработаны AMD и Intel соответственно, но по сути выполняют одно и то же). Это "быстрые" инструкции передачи управления, предназначенные для оперативной передачи управления ядру для системного вызова без накладных расходов, связанных с прерыванием. Linux 2.5 начал использовать их на x86, где это было возможно; ранее использовалась инструкция INT, при которой номер системного вызова помещался в регистр EAX перед выполнением прерывания 0x80. Более ранний механизм – шлюз вызова, изначально использовавшийся в Multics, а позже, например, в Intel x86. Он позволяет программе напрямую вызывать функцию ядра, используя безопасный механизм передачи управления, который операционная система настраивает заранее. Этот подход оказался непопулярным на x86, вероятно, из-за требования к дальнему вызову (вызову процедуры, расположенной в другом сегменте, чем текущий сегмент кода), который использует сегментацию памяти x86 и, как следствие, приводит к недостаточной переносимости, а также из-за существования более быстрых инструкций, упомянутых выше. Для архитектуры IA-64 используется инструкция EPC (Enter Privileged Code). Первые восемь аргументов системного вызова передаются в регистрах, а остальные – через стек. В семействе мэйнфреймов IBM System/360 и их преемниках инструкция Supervisor Call, содержащая номер в самой инструкции, а не в регистре, реализует системный вызов для устаревших функций в большинстве операционных систем IBM и для всех системных вызовов в Linux. В более поздних версиях MVS IBM использует инструкцию Program Call (PC) для многих новых функций. В частности, PC используется, когда вызывающая сторона может находиться в режиме "Блок запроса обслуживания" (Service Request Block, SRB). Миникомпьютер PDP-11 использовал инструкции , и , которые, подобно инструкциям IBM System/360 и x86, помещали код в саму инструкцию; они генерировали прерывания по конкретным адресам, передавая управление операционной системе. 32-битный преемник PDP-11, VAX, использовал инструкции , , и для выполнения системных вызовов к привилегированному коду на различных уровнях; код является аргументом инструкции.

Переключение режима процессора и контекста

Системные вызовы в большинстве Unix-подобных систем обрабатываются в режиме ядра, что достигается путем переключения процессора в более привилегированный режим исполнения, однако переключение контекста процесса не требуется, хотя и происходит переключение контекста привилегий. Аппаратное обеспечение воспринимает мир в терминах режима исполнения, определяемого регистром состояния процессора, а процессы являются абстракцией, предоставляемой операционной системой. Системный вызов, как правило, не требует переключения контекста на другой процесс; вместо этого он обрабатывается в контексте процесса, который его вызвал. В многопоточном процессе системные вызовы могут выполняться из нескольких потоков. Обработка таких вызовов зависит от архитектуры конкретного ядра операционной системы и среды выполнения приложения. Ниже приведен список типичных моделей, используемых операционными системами:

Модель "многие к одному": все системные вызовы от любого пользовательского потока в процессе обрабатываются одним потоком на уровне ядра. Эта модель имеет серьезный недостаток: любой блокирующий системный вызов (например, ожидание ввода от пользователя) может привести к зависанию всех остальных потоков. Кроме того, поскольку одновременно к ядру может получить доступ только один поток, эта модель не может использовать несколько ядер процессора. Модель "один к одному": каждый пользовательский поток привязывается к отдельному потоку ядра во время системного вызова. Эта модель решает проблему блокировки системных вызовов. Она используется во всех основных дистрибутивах Linux, macOS, iOS, последних версиях Windows и Solaris. Модель "многие ко многим": в этой модели пул пользовательских потоков отображается на пул потоков ядра. Все системные вызовы из пула пользовательских потоков обрабатываются потоками в соответствующем пуле потоков ядра. Гибридная модель: эта модель реализует как модель "многие ко многим", так и модель "один к одному" в зависимости от выбора, сделанного ядром. Она встречается в старых версиях IRIX, HP-UX и Solaris.