Что такое syscall — граница процесс/ядро
В прошлом уроке мы поняли: процесс в user mode не может делать привилегированных вещей напрямую. Но ему нужно открывать файлы, читать сеть, выделять память — это всё привилегированные операции. Как же оно работает? Через системные вызовы (syscalls).
Syscall — единственный легальный способ для user-программы попросить ядро что-то сделать. Это «контролируемая дверь» между user и kernel. В этом уроке: что такое syscall, как программа их делает, какие категории syscalls бывают, и главное — как читать strace, чтобы понимать, что физически делает ваша программа.
Аналогия: syscall как заказ в ресторане
Представьте ресторан. Вы (user process) сидите за столиком. Кухня (kernel) — закрытая зона: туда нельзя заходить, нельзя самому брать продукты, нельзя пользоваться плитой. Между вами и кухней — окно выдачи и официант (syscall interface).
Если хотите еды:
- Подзываете официанта (
syscallinstruction). - Делаете заказ: «номер 42, omakase, без васаби» (номер syscall + аргументы).
- Официант передаёт заказ на кухню (CPU переключается в ring 0).
- Кухня готовит (kernel выполняет операцию).
- Официант приносит результат (возврат в ring 3 с результатом).
Вы не можете сами зайти на кухню. Вы не можете заказать «достань из холодильника соседа продукты» — кухня проверит и откажет. Это и есть syscall: вы делаете запрос через формальный интерфейс, ядро решает, что и как выполнить.
Анатомия syscall: что внутри
Конкретно syscall работает так на x86_64 Linux:
Обратите внимание: программа никогда не «прыгает» в произвольное место ядра. Инструкция syscall всегда передаёт управление в одну фиксированную точку, заданную в MSR-регистре LSTAR. Ядро смотрит на rax и решает, что делать. Это даёт безопасность: нельзя обмануть и попасть в произвольный kernel-код.
Какие syscalls бывают
В Linux x86_64 сейчас около 350+ syscalls. Их можно разделить на категории:
Не нужно знать все 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:
- Предоставляет удобный C API:
open(),read(),printf(). - Под капотом эти функции вызывают syscalls.
- Иногда обёртка тонкая (
getpid()-> просто syscall), иногда толстая (printf()— много форматирования + один write()).
Зачем это знать: когда вы дебажите, важно понять, где замедление. Если 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) = ?
Что происходит:
- execve — запуск программы ls (мы видим начало работы).
- brk, mmap, arch_prctl — настройка памяти и TLS.
- openat ld.so.cache, libselinux.so.1… — загрузка shared libraries.
- openat /tmp — открытие нашей директории.
- getdents64 — чтение содержимого директории (имена файлов).
- write(1, …) — вывод на stdout (fd=1).
- 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:
В 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)