Что происходит между вашим print() и железом: системные вызовы, fork и exec, сигналы, планировщик, потоки и GIL, load average и контейнеры.
Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки.
Процессор умеет работать в двух режимах. Ваш код выполняется в пользовательском (на x86 — кольцо 3): привилегированные инструкции запрещены, доступна только своя память. Ядро работает в кольце 0 и может всё.
Выберите действие — увидите, где оно выполняется.
Системный вызов — единственная дверь из программы в ядро. Номер вызова и аргументы кладутся в регистры, инструкция syscall переключает процессор в режим ядра. В Linux около 400 вызовов, и любой ввод-вывод в любом языке сводится к ним.
strace показывает эти вызовы у работающей программы. Выберите строку на Python и пройдите по шагам.
Вход в ядро и выход — это сотни наносекунд: сохранить регистры, проверить аргументы, а с защитой от Meltdown и Spectre ещё и сбросить часть состояния процессора. Обычный вызов функции — около наносекунды.
open() и Java BufferedOutputStream копят по 8 КБ, Go bufio.Writer — 4 КБ.os.File.Write в Go, FileOutputStream в Java и fs.writeSync в Node не буферизуют: каждый вызов — системный.Программа пишет в файл 100 МБ. Двигайте размер буфера.
Процесс — это запущенная программа вместе со всем, что ядро ей выделило: адресное пространство, открытые файлы, права, окружение. Внутри — один или несколько потоков, которые выполняют код.
Почти всё это видно в /proc/<PID>/. Нажмите на часть процесса.
В Unix новый процесс появляется в два шага. fork() делает копию текущего процесса, execve() заменяет в копии программу. Между ними ребёнок настраивает себя: перенаправляет вывод, меняет каталог. Так shell выполняет ls > out.txt.
Копия дешёвая: страницы памяти общие и копируются только при записи (copy-on-write). Но таблицы страниц копируются сразу — у процесса на 10 ГБ fork занимает порядка 100 мс. Пройдите по шагам и попробуйте другие сценарии.
Сигнал — асинхронное уведомление процессу от ядра или другого процесса. На каждый сигнал есть действие по умолчанию, и почти любой можно перехватить своим обработчиком.
SIGKILL и SIGSTOP перехватить нельзя: процесс не узнает, что его убили, и не успеет ничего закрыть.SIGKILL (часто это OOM killer), 143 — SIGTERM.process.on('SIGTERM').kill, docker stop, Kubernetes. По умолчанию — смерть. Ловите и завершайтесь аккуратно.KeyboardInterrupt.wait.BrokenPipeError.docker stop шлёт SIGTERM процессу с PID 1 в контейнере, ждёт 10 секунд (Kubernetes — 30) и добивает SIGKILL. Есть две ловушки.
exec: PID 1 — это shell, и сигнал до приложения не доходит.SIGTERM в роли PID 1 его просто не заметит. А ещё PID 1 обязан забирать осиротевших зомби.Решение: exec в скрипте, обработчик SIGTERM и маленький init (docker run --init, tini, dumb-init). Выберите вариант и нажмите docker stop.
Готовых к работе задач обычно больше, чем ядер. Планировщик Linux (CFS, с версии 6.6 — EEVDF) делит время честно: у каждой задачи счётчик потраченного времени с учётом веса, и на ядро идёт та, у кого он меньше.
nice меняет вес: каждая ступень — примерно ±10% процессора.A и B только считают, C — веб-сервер: 1 мс работы, потом 8 мс ждёт сеть. Добавьте задач, уберите ядро, поменяйте nice или переключитесь на простую очередь.
Чтобы сменить задачу на ядре, ядро сохраняет её регистры, указатель стека и счётчик команд, выбирает следующую и загружает её состояние. При переходе в другой процесс оно ещё и меняет таблицу страниц.
vmstat 1 (столбец cs) и pidstat -w.Двигайте частоту переключений и размер рабочих данных задачи.
Поток — это ещё один исполнитель внутри процесса: свои регистры и стек, но общие память, файлы и обработчики сигналов. В Linux поток — та же задача ядра, что и процесс, просто созданная clone() с флагами «делить память и файлы».
Общая память — это и плюс (обмен данными без копирования), и минус: гонки данных и общая судьба. Синхронизация — в презентации про конкурентность, лёгкие потоки и async — про асинхронный ввод-вывод.
Стек потока — 8 МБ виртуальной памяти в Linux и 1 МБ в Java, но реально занимаются только тронутые страницы: обычно десятки килобайт. Упираются раньше в лимиты ядра и в переключения.
pthread_create: Resource temporarily unavailable, в Java — OutOfMemoryError: unable to create native thread.pids.max из cgroup, у Go — свой лимит в 10 000 потоков ОС.Двигайте число потоков: какой лимит сработает первым на сервере с 16 ГБ?
В CPython байткод выполняет только поток, держащий глобальную блокировку интерпретатора (GIL). Потоки отдают её каждые 5 мс и на время ввода-вывода. Поэтому потоки ускоряют ожидание сети, но не вычисления на чистом Python.
multiprocessing или ProcessPoolExecutor: отдельные процессы со своим GIL, но данные передаются через pickle.python3.14t): потоки работают параллельно, однопоточный код медленнее на 5–10%.Выберите задачу и способ, нажмите «Запустить». Задача — 8 одинаковых кусков работы на 4 ядрах.
Потоки ОС 1:1, но байткод выполняет один поток за раз. Вычисления — в процессы. На Linux с 3.14 процессы по умолчанию запускаются через forkserver, а не fork.
Ваш JS — один поток с циклом событий. Файлы, DNS, crypto и zlib уходят в пул libuv из 4 потоков (UV_THREADPOOL_SIZE). Вычисления — в worker_threads: отдельные изоляты V8 в том же процессе, обмен через postMessage или SharedArrayBuffer.
Обычный поток — поток ОС с 1 МБ стека. Поэтому их держат в пулах (ExecutorService) фиксированного размера. С Java 21 есть виртуальные потоки: миллионы задач поверх нескольких потоков ОС.
Горутины M:N: тысячи горутин на GOMAXPROCS потоках ОС, стек от 2 КБ. Блокирующий системный вызов занимает поток целиком, и рантайм запускает ещё один на замену.
Общий принцип: потоков ОС, которые одновременно считают, должно быть примерно столько же, сколько ядер. Остальное — ожидание, его дешевле обслуживать циклом событий или лёгкими потоками.
Три числа в uptime и top — сглаженное среднее числа задач, которые работают или ждут ядро, за 1, 5 и 15 минут. Сравнивайте с числом ядер: load 8 на 8 ядрах — полная загрузка, на 2 ядрах — очередь вчетверо длиннее.
В Linux в load входят и задачи в состоянии D — непрерываемое ожидание диска или NFS. Поэтому высокий load при свободном процессоре — повод смотреть на диск. Двигайте ползунки и следите, как догоняют средние.
Никакой виртуальной машины: процессы контейнера видны в ps на хосте. Изоляцию дают namespaces — свои PID, сеть, файловая система, hostname. Ресурсы ограничивают cgroups.
cpu.max = 200000 100000 значит 200 мс процессорного времени на каждые 100 мс. Квоту тратят все потоки вместе.cpu.stat: nr_throttled.os.cpu_count() в Python видит все ядра, GOMAXPROCS учитывает лимит только с Go 1.25.Двигайте лимит и число занятых потоков. Запросу нужно 50 мс процессора.
В программе 8 потоков. Один из них вызывает fork() без exec. Что окажется в процессе-ребёнке?
multiprocessing по умолчанию использует forkserver. Безопасно сразу после fork только вызвать exec.На сервере с 8 ядрами load average 40, а top показывает 92% idle. Что происходит?
ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /D/', а vmstat 1 покажет их число в столбце b. Частые причины — перегруженный диск или отвалившийся NFS.8 потоков Python с GIL считают hashlib.sha256() от блоков по 1 МБ на 8 ядрах. Какое будет ускорение по сравнению с одним потоком?
hashlib отпускает его на данных больше 2 КБ, так же поступают zlib, операции numpy над большими массивами, чтение файлов и сокетов. Пока поток внутри такого кода, другие выполняют Python. Медленно в потоках только то, что крутится в байткоде.ps, top -H, htop — процессы, их потоки и состояния: R работает, S спит, D ждёт диск, Z зомби.strace -f -c — какие системные вызовы и сколько времени на них уходит. Сильно замедляет программу: на проде осторожно./proc/<PID>/ — всё о процессе: память, файлы, лимиты, переключения контекста.py-spy dump --pid для Python, jstack для Java, kill -QUIT для Go.perf top и perf sched — где тратится процессор и сколько задачи ждут в очереди.
ps -eo pid,ppid,stat,nlwp,rss,cmd --forest
pstree -p 2210
strace -f -c python app.py # сводка
strace -f -e trace=process,signal -p 2210
grep -E 'State|Threads|ctxt' /proc/2210/status
ls -l /proc/2210/fd # файлы и сокеты
cat /proc/2210/limits
tr '\0' '\n' < /proc/2210/environ
vmstat 1 # r — очередь, b — D, cs
pidstat -w 1 # переключения контекста
cat /sys/fs/cgroup/cpu.max
cat /sys/fs/cgroup/cpu.stat # nr_throttled
ls -l /proc/2210/ns # namespaces
exec в скриптах, обработчик SIGTERM и --init.Ваша программа не владеет процессором — она его арендует у ядра.