Системный вызов
Системный вызов (англ. system call, сокр. syscall) — это программный интерфейс между прикладной программой и ядром операционной системы. Он представляет собой механизм, с помощью которого пользовательский процесс запрашивает выполнение привилегированных операций, недоступных в обычном режиме выполнения (пользовательском режиме). Системные вызовы являются фундаментальным элементом архитектуры современных операционных систем, обеспечивая изоляцию, безопасность и контролируемый доступ к аппаратным ресурсам компьютера.
История
Концепция системных вызовов возникла на ранних этапах развития операционных систем. В первых мэйнфреймах, таких как IBM System/360 (1964 год), прикладные программы могли напрямую обращаться к аппаратному обеспечению, что приводило к нестабильности системы. Переход к многозадачности и разделению времени потребовал формализации доступа.
В 1960-х годах в операционной системе MULTICS (MIT, Bell Labs, General Electric) была впервые реализована идея «кольца защиты» (ring protection). MULTICS использовала 8 уровней привилегий, и переход с одного уровня на другой осуществлялся через специальные инструкции — прообраз современных системных вызовов.
Операционная система UNIX, разработанная в 1970 году в Bell Labs, упростила эту модель. В UNIX системные вызовы были реализованы как библиотечные функции (например, read, write, open), которые внутри себя выполняли инструкцию программного прерывания. Этот подход стал стандартом де-факто для большинства последующих ОС.
В 1980-х годах с появлением персональных компьютеров и операционных систем MS-DOS и Windows механизмы системных вызовов были адаптированы для x86-архитектуры. В современных системах (Linux, Windows, macOS) системные вызовы реализованы через специальные инструкции процессора, такие как syscall (x86-64), sysenter (x86) или svc (ARM).
Архитектура и механизм работы
Режимы выполнения процессора
Современные процессоры поддерживают как минимум два режима выполнения:
- Пользовательский режим (user mode) — ограниченный набор инструкций, запрет на прямой доступ к аппаратному обеспечению и системным таблицам.
- Режим ядра (kernel mode) — полный доступ ко всем ресурсам, возможность выполнения привилегированных инструкций.
Прикладные программы работают в пользовательском режиме. Для выполнения системных вызовов необходимо переключиться в режим ядра.
Процесс выполнения системного вызова
- Инициализация: прикладная программа помещает номер системного вызова и аргументы в регистры процессора (или в стек) в соответствии с соглашением о вызовах, принятым в данной ОС.
- Переключение контекста: программа выполняет специальную инструкцию (например,
syscall), которая вызывает аппаратное переключение в режим ядра. - Обработка: ядро, используя таблицу системных вызовов, находит соответствующую функцию-обработчик. Проверяет аргументы на корректность и безопасность (например, адреса памяти должны принадлежать вызывающему процессу).
- Выполнение: обработчик выполняет запрошенную операцию (чтение файла, выделение памяти, создание процесса и т.д.).
- Возврат: результат выполнения (или код ошибки) помещается в регистр, и ядро возвращает управление прикладной программе, переключая процессор обратно в пользовательский режим.
Таблица системных вызовов
Каждая операционная система имеет фиксированный набор системных вызовов, пронумерованных от 0 до N. Например, в Linux (версия 5.x) существует около 450 системных вызовов. Таблица системных вызовов — это массив указателей на функции ядра, хранящийся в памяти. Номер вызова служит индексом в этой таблице.
Классификация системных вызовов
Системные вызовы можно классифицировать по функциональному назначению:
Управление процессами
fork()— создание нового процесса (копия текущего).execve()— замена образа текущего процесса новым.exit()— завершение процесса.waitpid()— ожидание завершения дочернего процесса.getpid()— получение идентификатора процесса.
Управление файловой системой
open()— открытие файла или создание нового.read()— чтение данных из файла.write()— запись данных в файл.close()— закрытие файлового дескриптора.stat()— получение информации о файле (размер, права доступа, время модификации).
Управление памятью
brk()/sbrk()— изменение размера кучи (heap) процесса.mmap()— отображение файла или устройства в память.munmap()— удаление отображения.mprotect()— изменение прав доступа к странице памяти.
Межпроцессное взаимодействие (IPC)
pipe()— создание канала (pipe).socket()— создание сокета для сетевого взаимодействия.shmget()— получение идентификатора разделяемой памяти (System V).semop()— операции с семафорами.
Управление временем и сигналами
alarm()— установка таймера.signal()— установка обработчика сигнала.gettimeofday()— получение текущего времени.
Различия между операционными системами
Linux
Linux использует системные вызовы в стиле UNIX. На архитектуре x86-64 вызовы выполняются через инструкцию syscall. Номер вызова передаётся в регистре rax, аргументы — в регистрах rdi, rsi, rdx, r10, r8, r9. Возвращаемое значение помещается в rax. Код ошибки (отрицательное значение) также возвращается в rax. Стандартная библиотека C (glibc) предоставляет обёртки для всех системных вызовов.
Windows (NT)
В Windows системные вызовы называются Native API (ntdll.dll). Они реализованы через инструкцию sysenter (x86) или syscall (x86-64). Номер вызова передаётся в регистре eax, аргументы — в стеке или регистрах. В отличие от Linux, Windows использует объектно-ориентированную модель: системные вызовы оперируют объектами (файлы, процессы, семафоры), которые идентифицируются дескрипторами. Пользовательские приложения обычно работают через Win32 API (kernel32.dll, user32.dll), который внутри вызывает Native API.
macOS (XNU)
macOS, основанная на ядре XNU (гибрид микроядра Mach и монолитного ядра BSD), использует два набора системных вызовов: вызовы Mach (для управления задачами и памятью) и вызовы BSD (для файловых операций, процессов, сокетов). Вызовы выполняются через инструкцию syscall (x86-64) или svc (ARM). Номер вызова передаётся в регистре rax, аргументы — в rdi, rsi, rdx, r10, r8, r9.
Безопасность и системные вызовы
Системные вызовы являются критической точкой с точки зрения безопасности. Неправильная обработка аргументов или отсутствие проверок может привести к уязвимостям:
- Переполнение буфера: если ядро не проверяет размер передаваемых данных, злоумышленник может записать данные за пределами выделенного буфера.
- Race condition: одновременный доступ нескольких процессов к одному ресурсу может привести к состоянию гонки.
- Privilege escalation: ошибка в обработчике системного вызова может позволить пользовательскому процессу получить доступ к данным ядра или выполнить привилегированные операции.
Современные операционные системы применяют механизмы защиты:
- ASLR (Address Space Layout Randomization) — случайное размещение таблицы системных вызовов в памяти.
- SMAP/SMEP (Supervisor Mode Access Prevention/Execution Prevention) — аппаратная защита, предотвращающая доступ ядра к пользовательской памяти без явного разрешения.
- Seccomp (Secure Computing Mode) — механизм в Linux, позволяющий процессу ограничить набор доступных системных вызовов.
Производительность
Системные вызовы являются относительно дорогими операциями по сравнению с обычными вызовами функций. Затраты включают:
- Переключение контекста (сохранение/восстановление регистров).
- Переключение режима процессора (user mode → kernel mode → user mode).
- Очистку конвейера команд (pipeline flush) при изменении режима.
- Проверку аргументов и выполнение кода ядра.
Для повышения производительности используются следующие подходы:
- Пакетная обработка (batching): объединение нескольких системных вызовов в один (например,
readv/writevдля чтения/записи нескольких буферов). - Асинхронные системные вызовы: возврат управления до завершения операции (io_uring в Linux).
- VDSO (Virtual Dynamic Shared Object): отображение некоторых системных вызовов (например,
gettimeofday) в пользовательское пространство, что позволяет избежать переключения в ядро.
Примеры
Пример 1: Чтение файла в Linux (C)
```c
include <unistd.h>
include <fcntl.h>
int main() { char buffer[100]; int fd = open("file.txt", O_RDONLY); // системный вызов open() if (fd == -1) return 1; ssize_t bytes = read(fd, buffer, sizeof(buffer)); // системный вызов read() close(fd); // системный вызов close() return 0; } ```
Пример 2: Создание процесса в Windows (C, Win32 API)
```c
include <windows.h>
int main() { STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; // CreateProcess вызывает системные вызовы Native API CreateProcess("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); // системный вызов WaitForSingleObject CloseHandle(pi.hProcess); // системный вызов CloseHandle CloseHandle(pi.hThread); return 0; } ```
Интересные факты
- В ранних версиях UNIX (1970-е годы) системные вызовы выполнялись через инструкцию
trap(программное прерывание) с номером 0. В современных системах для этого используются специализированные инструкции. - В ядре Linux существует механизм ftrace, позволяющий трассировать все системные вызовы, выполняемые процессом, без изменения кода программы.
- В операционной системе Plan 9 (Bell Labs, 1990-е) системные вызовы были заменены на единый интерфейс — «файловую систему»: все ресурсы (процессы, сетевые соединения, устройства) представлялись как файлы, а операции с ними — как чтение/запись.
- Некоторые встраиваемые системы и микроядра (например, L4) реализуют системные вызовы как обычные вызовы функций, передавая управление ядру через IPC (межпроцессное взаимодействие).
Источники
- Таненбаум Э., Бос Х. «Современные операционные системы» (4-е издание). — СПб.: Питер, 2015.
- Бовет Д., Чезати М. «Ядро Linux. Сборка и отладка». — М.: ДМК Пресс, 2019.
- Документация Linux Kernel: «Системные вызовы» (Linux man pages, раздел 2).
- Документация Microsoft: «Windows Native API» (MSDN).
- Документация Apple: «XNU Kernel Programming Guide» (Apple Developer).
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →