Learning Platform
Глоссарий Troubleshooting
Урок 03.03 · 18 мин
Начальный
SyscallKernelstraceAPI

Что такое syscall — граница процесс/ядро

В прошлом уроке мы поняли: процесс в user mode не может делать привилегированных вещей напрямую. Но ему нужно открывать файлы, читать сеть, выделять память — это всё привилегированные операции. Как же оно работает? Через системные вызовы (syscalls).

Syscall — единственный легальный способ для user-программы попросить ядро что-то сделать. Это «контролируемая дверь» между user и kernel. В этом уроке: что такое syscall, как программа их делает, какие категории syscalls бывают, и главное — как читать strace, чтобы понимать, что физически делает ваша программа.


Аналогия: syscall как заказ в ресторане

Представьте ресторан. Вы (user process) сидите за столиком. Кухня (kernel) — закрытая зона: туда нельзя заходить, нельзя самому брать продукты, нельзя пользоваться плитой. Между вами и кухней — окно выдачи и официант (syscall interface).

Если хотите еды:

  1. Подзываете официанта (syscall instruction).
  2. Делаете заказ: «номер 42, omakase, без васаби» (номер syscall + аргументы).
  3. Официант передаёт заказ на кухню (CPU переключается в ring 0).
  4. Кухня готовит (kernel выполняет операцию).
  5. Официант приносит результат (возврат в ring 3 с результатом).

Вы не можете сами зайти на кухню. Вы не можете заказать «достань из холодильника соседа продукты» — кухня проверит и откажет. Это и есть syscall: вы делаете запрос через формальный интерфейс, ядро решает, что и как выполнить.


Анатомия syscall: что внутри

Конкретно syscall работает так на x86_64 Linux:

Анатомия syscall: как процесс просит ядро открыть файл
1. Подготовка argsUser-программа кладёт аргументы syscall в регистры. Соглашение для x86_64 Linux: rax=номер syscall, rdi=arg1, rsi=arg2, rdx=arg3, r10=arg4, r8=arg5, r9=arg6
2. Инструкция syscallПрограмма выполняет инструкцию syscall (на x86_64). CPU читает rax -- это номер запрошенного syscall. Переключение в ring 0, переход в kernel entry point
3. Validate argsЯдро проверяет аргументы: указатели в user range, права есть, fd валидный. Если что-то не так -- возврат с errno (EBADF, EFAULT, EACCES, etc)
4. ВыполнениеЯдро выполняет работу: ходит в page cache, читает диск, парсит данные. Может заблокироваться -- тогда процесс уходит в sleep, scheduler берёт другой
5. ВозвратЯдро кладёт результат в rax (положительное число при успехе, отрицательное -errno при ошибке) и выполняет sysretq -- возврат в ring 3
6. User codeПрограмма продолжает с инструкции после syscall. Проверяет rax: если меньше 0 -- ошибка, иначе результат

Обратите внимание: программа никогда не «прыгает» в произвольное место ядра. Инструкция syscall всегда передаёт управление в одну фиксированную точку, заданную в MSR-регистре LSTAR. Ядро смотрит на rax и решает, что делать. Это даёт безопасность: нельзя обмануть и попасть в произвольный kernel-код.


Какие syscalls бывают

В Linux x86_64 сейчас около 350+ syscalls. Их можно разделить на категории:

Категории системных вызовов Linux
ProcessРабота с процессами: fork(), execve(), exit(), waitpid(), kill(), getpid(), getppid(), clone(). Создание, завершение, info
File I/OРабота с файлами: open(), close(), read(), write(), lseek(), stat(), fstat(), mmap(), fsync(). Самая частая категория
DirectoryДиректории: mkdir(), rmdir(), rename(), unlink(), getdents(), chdir(), getcwd()
MemoryПамять: brk() (heap), mmap()/munmap() (mapping), mprotect() (права), madvise() (hints). И всё management виртуальной памяти
NetworkСеть: socket(), bind(), listen(), accept(), connect(), send(), recv(), sendto(), recvfrom(). TCP/UDP/Unix
IPCInter-process: pipe(), shmget(), msgget(), semop(), futex(). Способы общения процессов
SignalsСигналы: kill(), sigaction(), sigprocmask(), pause(), nanosleep(). Asynchronous notifications
TimeВремя: clock_gettime(), gettimeofday(), nanosleep(), timer_create(), settimeofday()
SecurityБезопасность: setuid(), setgid(), chmod(), chown(), capset(), prctl(), seccomp()

Не нужно знать все 350. На практике вы постоянно встречаете 20-30: open, read, write, close, stat, mmap, munmap, brk, fork, execve, exit, wait, kill, signal, socket, connect, send, recv, accept, listen, getpid, getuid, clock_gettime, futex, ioctl. Всё остальное — по необходимости.


Syscalls vs функции libc

Тонкий момент: программы редко вызывают syscalls напрямую. Они пользуются библиотеками. На Linux это libc (обычно glibc на серверах или musl в Alpine/контейнерах).

Что делает libc:

  1. Предоставляет удобный C API: open(), read(), printf().
  2. Под капотом эти функции вызывают syscalls.
  3. Иногда обёртка тонкая (getpid() -> просто syscall), иногда толстая (printf() — много форматирования + один write()).
Путь от вашего кода до syscall
PythonВаш Python код: open('/path/file', 'r').read(). Python -- интерпретируемый язык, который сам написан на C и компилируется в нативный код
CPythonCPython интерпретатор: вызывает функции C-runtime для I/O. Внутри -- libc вызовы
libc (glibc)Системная библиотека: fopen(), fread() и т.д. -- обёртки над syscalls. Добавляют буферизацию, error handling
syscallФинальный шаг: инструкция syscall с rax=2 (sys_open). Переход в kernel mode
KernelЯдро: парсит path, проверяет permissions, ищет inode, выделяет file descriptor, возвращает

Зачем это знать: когда вы дебажите, важно понять, где замедление. Если syscall — значит, ядро/диск/сеть. Если в libc до syscall — значит, форматирование или буферизация в user space.

# Если у вас C компилятор, посмотрите разницу между write через libc и raw syscall:
cat > /tmp/test.c << 'EOF'
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>

int main() {
    const char *msg = "hello via libc\n";
    write(1, msg, strlen(msg));  // libc wrapper

    const char *msg2 = "hello via syscall\n";
    syscall(SYS_write, 1, msg2, strlen(msg2));  // raw syscall

    return 0;
}
EOF
gcc /tmp/test.c -o /tmp/test
/tmp/test
# Покажет оба сообщения. strace -c покажет, что оба сделали один write() syscall
strace -c /tmp/test

strace — ваш главный инструмент

Что такое процесс

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

Пример:

strace ls /tmp 2>&1 | head -20

Вывод:

execve("/usr/bin/ls", ["ls", "/tmp"], 0x7ffe...) = 0
brk(NULL) = 0x55c1...
arch_prctl(ARCH_SET_FS, 0x7fa1...) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa1...
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=149564, ...}) = 0
mmap(NULL, 149564, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa1...
close(3) = 0
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\\177ELF\\2\\1\\1\\0\\0..."..., 832) = 832
...
openat(AT_FDCWD, "/tmp", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
getdents64(3, 0x55c1..., 32768) = 184
write(1, "files\\nhere\\n", 12) = 12
close(3) = 0
exit_group(0) = ?

Что происходит:

  1. execve — запуск программы ls (мы видим начало работы).
  2. brk, mmap, arch_prctl — настройка памяти и TLS.
  3. openat ld.so.cache, libselinux.so.1… — загрузка shared libraries.
  4. openat /tmp — открытие нашей директории.
  5. getdents64 — чтение содержимого директории (имена файлов).
  6. write(1, …) — вывод на stdout (fd=1).
  7. exit_group — завершение.

Вы видите ВСЁ, что делает программа на границе с ядром. Это супер-полезно для диагностики.

Полезные опции strace

# Подсчёт времени и количества syscalls (без листинга):
strace -c command

# Только определённые syscalls:
strace -e openat,read,write command       # конкретные
strace -e trace=file command               # категория file

# С временными метками (когда был вызван):
strace -tt command

# С длительностью каждого вызова:
strace -T command

# Включить вывод дочерних процессов:
strace -f command

# Прицепиться к запущенному процессу:
strace -p <PID>

# В файл:
strace -o trace.log command

# Длиннее строки (по умолчанию обрезает):
strace -s 256 command

strace в реальных сценариях

Сценарий 1: программа зависла — что она делает?

# 1. Найти PID процесса:
pgrep -f "myscript.py"
# 12345

# 2. Прицепиться strace:
sudo strace -p 12345

# Если видите:
# read(3, ...
# (висит на read)
# Значит, программа ждёт ввода с fd=3. ls -la /proc/12345/fd/3 -- что это

Сценарий 2: программа падает — что последнее делала?

strace -f -o trace.log buggy_program
# (программа упала)
tail -20 trace.log
# Последние syscalls перед падением видны

Сценарий 3: что грузит CPU?

strace -c -p <PID>
# Ctrl-C через минуту -- показывает:
# % time   seconds  usecs/call   calls   errors syscall
# 65.4%   0.0234    23           1000           epoll_wait
# 22.1%   0.0079    7            1200           read
# ...
# Если 80% времени в read -- значит, программа в основном ждёт I/O

Сценарий 4: где программа потеряла файл?

strace -e openat program 2>&1 | grep -E 'ENOENT|EACCES'
# Покажет все попытки открыть несуществующие или запрещённые файлы
# Часто видно "ищет /etc/config", когда конфиг переехал
find, locate, which: поиск файлов

errno: коды ошибок syscalls

Когда syscall возвращает отрицательное значение — это код ошибки. Их имена начинаются с E:

Самые частые errno коды
EACCESPermission denied. Файл существует, но у процесса нет прав. Проверяйте chmod, ACL, SELinux
ENOENTNo such file or directory. Файл не существует. Часто typo в пути или файл удалён
EEXISTFile exists. Когда пытаетесь создать что-то с O_EXCL флагом, а оно уже есть
EBADFBad file descriptor. Вы передали fd, который не открыт. Часто после close + использование того же fd
EAGAINResource temporarily unavailable / Would block. На non-blocking сокете нет данных. Норма для async кода, надо обрабатывать
EINTRInterrupted system call. Syscall был прерван сигналом (SIGINT). По старому стандарту надо retry, по новому ядро часто перезапускает само
ENOMEMOut of memory. Не удалось выделить память. Может быть RLIMIT, может быть реально нет
EPERMOperation not permitted. Похоже на EACCES, но обычно про привилегированные операции (например, kill чужого процесса)
EFAULTBad address. Вы передали указатель на невалидную память. Часто баг в коде

В C-коде после неудачного syscall errno содержит код. В Python библиотеки бросают исключения (FileNotFoundError, PermissionError). Но базовый код errno одинаковый везде.

# Посмотреть все errno:
errno -l | head -30

# Узнать описание конкретного:
errno EACCES
# 13 EACCES Permission denied

Стоимость syscall

Syscall не бесплатный. Минимум — порядка 100ns на современном CPU (за вход в kernel mode и выход). Если syscall что-то делает — может быть микросекунды, миллисекунды, секунды.

Примеры:

  • getpid() — ~50-100ns (только переход).
  • gettimeofday() — ~50ns (через vDSO, без перехода! см. M11).
  • read() из page cache — ~1-10 микросекунд.
  • read() с диска — 100 микросекунд - 10 миллисекунд (SSD vs HDD).
  • fsync() — от 100us до 100ms (зависит от диска).

Для производительности важно минимизировать syscalls. Это значит:

  • Буферизовать вывод (не вызывать write на каждый байт).
  • Использовать mmap для больших файлов вместо read/write.
  • Использовать epoll/io_uring вместо чтения по одному.

Попробуй сам

Несколько практических упражнений:

# 1. Сколько syscalls делает простая команда?
strace -c ls 2>&1 | tail -10
# Запоминает таблицу: какие syscalls, сколько раз

# 2. Сколько syscalls делает Python "hello world"?
echo 'print("hello")' > /tmp/h.py
strace -c python3 /tmp/h.py 2>&1 | tail -20
# Python -- тяжёлый: сотни syscalls для запуска интерпретатора, загрузки модулей

# 3. Что делает ваш shell, когда вы запускаете команду:
strace -e execve,fork,clone,wait4 -f -- bash -c 'echo hello'
# Покажет fork + execve cycle

# 4. Посмотреть только ошибки:
strace -e openat ls /nonexistent 2>&1 | grep -v "= 3"
# Покажет ENOENT попытки

# 5. Скорость разных syscalls:
strace -c bash -c 'for i in {1..1000}; do echo $$ > /dev/null; done'
# Тысяча write() в /dev/null -- посмотреть % time

# 6. Что делает curl при простом запросе:
strace -e socket,connect,sendto,recvfrom -- curl -s example.org > /dev/null
# Видно: socket + connect + send (HTTP request) + recv (response)

Проверка знанийKnowledge check
Junior спрашивает: 'Если syscalls -- единственный способ ядру что-то сделать, а они стоят микросекунды, как современные серверы обрабатывают миллионы запросов в секунду? Это же миллионы syscalls = миллионы микросекунд = больше секунды на CPU?'
ОтветAnswer
Отличный вопрос про производительность. Несколько ответов в зависимости от уровня оптимизации: (1) Batching. Вместо тысячи syscalls по 100ns -- один syscall, который обрабатывает тысячу событий. Так работает epoll() в Linux: один вызов возвращает информацию о тысяче готовых сокетов. Один syscall, миллион событий. Современные web-серверы (nginx, envoy) работают на epoll и обрабатывают десятки тысяч соединений в одном потоке. (2) io_uring -- новая submission-completion модель в Linux 5.1+. Вы кладёте запросы в submission queue (shared memory между user и kernel), ядро выполняет асинхронно, кладёт результаты в completion queue. Минимум syscalls на много операций. Используют ScyllaDB, Postgres 16+, и т.д. (3) DPDK/kernel bypass. Для самых критичных задач (telco, HFT trading) делают полный bypass ядра -- драйверы в user space, прямой доступ к сетевой карте, нет syscalls вообще. Получают 100M+ packets/sec на одном core. Но это специализированные решения, не для типичного приложения. (4) Числа. На современном Intel/AMD CPU один syscall занимает ~100ns. 1 миллион syscalls = 100ms = 10% CPU на одном core. Это допустимо для большинства приложений. Беспокоиться нужно когда счёт идёт на десятки миллионов syscalls/sec на core, что бывает на специализированных задачах. (5) vDSO для частых лёгких syscalls. clock_gettime() -- читается из shared page без перехода в kernel вообще. Один из самых частых вызовов в любой программе становится в 10-20 раз быстрее. Итог: 'миллион rps' -- это не миллион syscalls на запрос. Это десятки тысяч соединений, multiplexed через epoll, каждое соединение -- несколько syscalls на event. И ВСЁ ЕЩЁ многие сервисы оптимизируют syscalls агрессивно -- через batching, async, kernel bypass.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что такое system call (syscall) и зачем он нужен?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4