Перейти к содержимому

Процессы, потоки, сигналы и планирование в Linux

В Linux нет отдельных сущностей «процесс» и «поток» на уровне ядра — есть задача (task_struct), и всё различие в том, какие ресурсы задачи разделяют между собой. Обе создаются одним и тем же системным вызовом clone() (обёртки fork()/pthread_create() отличаются только набором флагов). Если передать CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD, получится поток: общее адресное пространство, общая таблица дескрипторов, общие обработчики сигналов и общий идентификатор группы потоков (TGID — то, что пользователь видит как PID). Без этих флагов получится процесс: своё адресное пространство (копируется по механизму copy-on-write), своя таблица файловых дескрипторов, свой PID. Планировщик при этом работает не с процессами, а именно с задачами-потоками: процессорное время выделяется потоку, а «однопоточный процесс» — это просто процесс с одним потоком.

Изоляция процессов держится на MMU и таблицах страниц: виртуальный адрес одного процесса физически не резолвится в память другого, попытка залезть за границы приводит к page fault и SIGSEGV. Именно поэтому обмен данными между процессами требует явного участия ядра — это и есть IPC: pipe/FIFO, доменные и сетевые сокеты, разделяемая память (System V shmget, POSIX shm_open + mmap), очереди сообщений, семафоры, сигналы, файлы с блокировками, eventfd/signalfd, передача файловых дескрипторов через SCM_RIGHTS. Потокам ничего этого не нужно — у них общая куча, но за это они платят необходимостью синхронизации и общей судьбой при падении.

Переключение контекста между потоками — работа ядра: сохранить регистры, указатель стека и счётчик команд в task_struct, выбрать следующую задачу, восстановить её состояние. Между потоками одного процесса это дешевле (адресное пространство то же, TLB не сбрасывается), между процессами — дороже (смена CR3, инвалидация TLB, если нет PCID; плюс холодные кеши). Порядок величины прямых затрат — единицы микросекунд, косвенных (промахи кеша после переключения) — часто больше прямых. Отсюда растёт вся мотивация горутин: рантайм Go мультиплексирует тысячи горутин на небольшое число потоков ОС (GOMAXPROCS штук активных), переключение делается в user space простой сменой SP/PC без входа в ядро, а стек горутины начинается с 2 КБ вместо 8 МБ виртуального стека потока.

Жизненный цикл процесса тоже удобно держать в голове целиком: fork() создаёт копию, execve() заменяет образ внутри того же PID, exit() переводит процесс в состояние зомби, и он остаётся в таблице процессов, пока родитель не вызовет wait()/waitpid() и не заберёт код возврата. Если родитель умер раньше — процесс становится сиротой и переусыновляется init (PID 1) или ближайшим subreaper, который зомби подберёт. Отсюда классическая проблема контейнеров: если PID 1 — это ваше приложение, которое не делает wait(), зомби будут копиться.

Сигналы — это механизм асинхронных уведомлений: у задачи есть битовая маска отложенных сигналов, ядро доставляет их при возврате в user space. Почти любой сигнал можно перехватить, заблокировать или проигнорировать — кроме SIGKILL и SIGSTOP, которые обрабатывает само ядро. Отсюда пара SIGTERM (вежливая просьба завершиться, приложение может закрыть соединения и сбросить буферы) и SIGKILL (принудительное снятие без шанса на уборку) — базис любого graceful shutdown.

В чем преимущества горутин по сравнению с потоками операционной системы?

Заголовок раздела «В чем преимущества горутин по сравнению с потоками операционной системы?»

Коротко. Горутина дешевле по памяти (стек стартует с 2 КБ и растёт по мере надобности против 8 МБ виртуального стека потока по умолчанию), дешевле по созданию и по переключению (переключение делает рантайм в user space, без системного вызова и без участия планировщика ядра), и её блокирующие операции ввода-вывода на самом деле не блокируют поток ОС — рантайм снимает горутину с потока и отдаёт поток другой работе. Поэтому миллион горутин — рабочая конфигурация, а миллион потоков ОС — нет.

Глубже. Технически горутины — это M:N-модель: N горутин мультиплексируются на M потоков ОС планировщиком GMP (G — горутина, M — поток ОС, P — логический процессор, их GOMAXPROCS штук). Стек горутины сегментированный только исторически; с Go 1.4 он непрерывный и растёт копированием: при входе в функцию проверяется stackguard, при нехватке вызывается morestack, стек копируется в новый вдвое больший кусок с корректировкой указателей — поэтому нельзя полагаться на адрес объекта на стеке в C-коде. Сетевые вызовы под капотом неблокирующие: файловый дескриптор регистрируется в netpoller (epoll на Linux), горутина паркуется через gopark, а поток идёт исполнять другие горутины. Настоящий блокирующий syscall (например, чтение с диска или cgo-вызов) отвязывает P от M через entersyscall, и sysmon через ~20 мкс отдаёт P другому потоку, при необходимости создав новый M. Что горутины не дают: настоящего параллелизма сверх GOMAXPROCS и приоритетов — если нужна жёсткая реалтайм-приоритизация или изоляция, это уровень потоков и sched_setscheduler.

В каких случаях происходит переключение контекста между горутинами?

Заголовок раздела «В каких случаях происходит переключение контекста между горутинами?»

Коротко. В точках, где горутина либо сама не может продолжать, либо рантайм её вытесняет: операции с каналами, select, блокировка на мьютексе/WaitGroup, сетевой и файловый ввод-вывод, системные вызовы, time.Sleep, runtime.Gosched(), выделение памяти, попадающее в медленный путь, фазы работы GC, а с Go 1.14 — ещё и асинхронное вытеснение по сигналу для горутин, которые крутятся дольше ~10 мс.

Глубже. Исторически планировщик Go был кооперативным: точки переключения вставлялись компилятором в пролог функций (проверка stackguard), поэтому цикл без вызовов функций — for {} или тесный счётный цикл — мог намертво занять поток и застопорить STW-фазу GC. В Go 1.14 появилось асинхронное вытеснение: sysmon замечает, что горутина держит P дольше 10 мс, и посылает потоку сигнал SIGURG; обработчик подменяет точку возврата на asyncPreempt, который сохраняет регистры и уводит горутину в очередь. Само переключение — это mcall/gogo: сохранение SP, PC, BP в поле g.sched и восстановление их у следующей горутины, десятки наносекунд, ни смены таблиц страниц, ни перехода в кольцо 0. Порядок выбора следующей горутины: runnext (только что разбуженная — оптимизация для ping-pong по каналу), локальная очередь P, каждый 61-й тик — глобальная очередь (чтобы она не голодала), затем воровство у других P и опрос netpoller.

Коротко. Приложение — это понятие для человека: программа как продукт, которая может состоять из одного или нескольких процессов (браузер: процесс на вкладку, отдельный GPU-процесс). Процесс — единица изоляции ресурсов в ОС: своё виртуальное адресное пространство, таблица дескрипторов, PID, права. Поток — единица планирования внутри процесса: свой стек, регистры и PC, но общая с остальными потоками процесса куча, код и дескрипторы.

Глубже. Иерархия «приложение → процессы → потоки» полезна как ответ на вопрос «где проходят границы отказа». Падение потока (например, из-за неперехваченной паники или SIGSEGV) убивает весь процесс, потому что сигнал доставляется группе потоков и обработчик по умолчанию терминирует всю группу. Падение одного процесса приложения оставляет остальные жить — на этом построена многопроцессная архитектура браузеров и nginx (master + worker). В Linux у процесса есть TGID (то, что показывают ps как PID) и у каждого потока — свой TID, видимый в /proc/<pid>/task/ и через ps -L.

Коротко. Процессы изолированы: у каждого своё адресное пространство, своя таблица файловых дескрипторов, свои обработчики сигналов; общаться могут только через IPC. Потоки одного процесса разделяют память, дескрипторы и обработчики сигналов, а личным имеют только стек, регистры и TLS. Как следствие: процессы дороже создавать и переключать, но надёжнее изолированы; потоки дешевле и быстрее обмениваются данными, но требуют синхронизации и падают вместе.

Глубже. В ядре Linux и то и другое — task_struct; разница только во флагах clone(). Поток получает CLONE_VM (общий mm_struct), CLONE_FILES (общая files_struct), CLONE_FS (общие cwd и umask), CLONE_SIGHAND и CLONE_THREAD (общая группа потоков и общий TGID). Практическое следствие CLONE_THREAD: сигнал, посланный по PID, доставляется любому потоку группы, у которого он не заблокирован, — поэтому в многопоточных программах сигналы обрабатывают через выделенный поток с sigwait или, как в Go, через signal.Notify, который прячет эту механику.

Коротко. kill <PID> — посылает SIGTERM (15), вежливое завершение. Если не помогло — kill -9 <PID> (SIGKILL). По имени: pkill -f <шаблон> или killall <имя>. Сервис под systemd правильнее гасить через systemctl stop <unit>, чтобы не подрался с рестарт-политикой.

Глубже. kill — не «убить», а «послать сигнал»: kill -l покажет список, kill -0 <PID> ничего не шлёт и служит проверкой существования процесса и прав. Отрицательный аргумент означает группу процессов: kill -TERM -<PGID> — всему дереву, что удобно для скриптов с потомками. Порядок действий на проде: сначала SIGTERM, дать таймаут на graceful shutdown (systemd по умолчанию 90 с, TimeoutStopSec), затем SIGKILL.

Коротко. См. выше вопрос «Отличия потоков и процессов»: процесс — единица изоляции ресурсов (своё адресное пространство и дескрипторы), поток — единица планирования внутри процесса (свои стек и регистры при общей памяти). Добавлю практический критерий выбора: разные процессы берут, когда нужны изоляция сбоев и безопасность или разные права; потоки — когда нужен дешёвый доступ к общим данным.

Какие есть виды межпроцессного взаимодействия?

Заголовок раздела «Какие есть виды межпроцессного взаимодействия?»

Коротко. Каналы (анонимные pipe и именованные FIFO), сокеты (Unix domain и сетевые TCP/UDP), разделяемая память (System V shmget, POSIX shm_open + mmap, либо mmap файла с MAP_SHARED), очереди сообщений (System V и POSIX mq_*), семафоры, сигналы, файлы с блокировками (flock, fcntl), а также специфичные для Linux eventfd, signalfd, memfd и передача дескрипторов через SCM_RIGHTS.

Глубже. Выбирать удобно по двум осям — «сообщения или память» и «локально или по сети». Разделяемая память самая быстрая (после mmap ядро в обмене не участвует вообще), но требует внешней синхронизации: семафоры, futex в разделяемой памяти или атомарные операции. Unix domain socket — рабочая лошадка локального IPC: даёт границы сообщений в режиме SOCK_SEGPACKET/SOCK_DGRAM, потоковый режим SOCK_STREAM, права по правам на файл сокета, аутентификацию пира (SO_PEERCRED) и умеет передавать открытые дескрипторы. Pipe односторонний, с буфером 64 КБ по умолчанию, атомарен для записей до PIPE_BUF (4096 байт). Сигналы — это IPC на 1 бит информации, для данных не годятся. На уровне выше есть D-Bus, gRPC поверх UDS и просто общая БД или брокер — формально это уже не IPC ядра, но на собеседовании упомянуть уместно.

Коротко. Это сохранение состояния текущей задачи (регистры общего назначения, PC, SP, состояние FPU/SSE) в её структуру в ядре и восстановление состояния другой задачи, чтобы CPU продолжил выполнять её. Делается планировщиком при истечении кванта, блокировке на I/O, вытеснении более приоритетной задачей или добровольной уступке.

Глубже. Важно отличать три вещи. Переключение режима (user → kernel при системном вызове) — не переключение контекста: адресное пространство то же, задача та же, стоимость ~100 нс, выросшая после митигаций Spectre/Meltdown из-за KPTI. Переключение между потоками одного процесса — дешевле: mm_struct общий, таблицы страниц не меняются, TLB не сбрасывается. Переключение между процессами — дороже: меняется CR3, и без поддержки PCID/ASID это означает полную инвалидацию TLB. Прямые затраты — порядка 1–5 мкс, но основной ущерб косвенный: новая задача приходит на холодные L1/L2, и «догрев» может стоить больше самого переключения. Считать переключения удобно через vmstat 1 (колонка cs), pidstat -w, perf stat -e context-switches, а /proc/<pid>/status разделяет voluntary_ctxt_switches (сам заблокировался) и nonvoluntary_ctxt_switches (вытеснили).

Какой командой можно убить процесс, а если не умирает?

Заголовок раздела «Какой командой можно убить процесс, а если не умирает?»

Коротко. kill <PID>kill -9 <PID>. Если и SIGKILL не помогает, процесс почти наверняка либо в состоянии D (непрерываемый сон в ядре — ждёт диск, NFS, зависший драйвер), либо это зомби Z (он уже мёртв, надо забрать его через родителя), либо это поток ядра, либо процесс остановлен трассировщиком.

Глубже. Диагностика по шагам: ps -o pid,stat,wchan:32,cmd -p <PID> покажет состояние и точку ожидания в ядре, cat /proc/<PID>/stack (нужен root) — стек ядра, cat /proc/<PID>/wchan — функцию, в которой процесс спит. Для D вариантов немного: устранить причину (поднять NFS-сервер, дождаться I/O, dmesg на предмет ошибок устройства) — иначе только перезагрузка; SIGKILL останется отложенным и сработает, как только задача выйдет из непрерываемой секции. Для зомби: ps -o ppid= -p <PID>, дальше убивать или чинить родителя — сам зомби уже не процесс. Для остановленного (T): kill -CONT <PID>, потом SIGKILL. Ещё частый случай — PID 1 в контейнере: ядро не доставляет процессу с PID 1 сигналы, у которых нет обработчика и действие по умолчанию — терминировать, поэтому docker stop у образа без обработчика SIGTERM ждёт таймаут и добивает SIGKILL.

Какому объекту выделяется процессорное время в ос?

Заголовок раздела «Какому объекту выделяется процессорное время в ос?»

Коротко. Потоку. В Linux планировщик оперирует задачами (task_struct), и каждый поток — отдельная задача со своим TID; процесс как таковой процессорного времени не получает, он получает его суммарно по своим потокам.

Глубже. Отсюда и берутся значения %CPU больше 100% в top: если процесс имеет четыре активных потока, он может съесть до 400% в терминах одного ядра. Планирование в Linux разбито на классы (в порядке убывания приоритета): SCHED_DEADLINE, реалтаймовые SCHED_FIFO/SCHED_RR, обычный SCHED_OTHER (CFS, а начиная с ядра 6.6 — EEVDF), SCHED_BATCH и SCHED_IDLE. Внутри SCHED_OTHER доля времени задаётся nice (вес), а групповые ограничения — через cgroup v2 (cpu.weight, cpu.max). Планирование группами (CONFIG_FAIR_GROUP_SCHED) как раз позволяет делить время между контейнерами/пользователями, а не только между отдельными потоками.

Коротко. Они дорогие: 8 МБ виртуального стека по умолчанию (ulimit -s) и килобайты ядерных структур на каждый, создание — системный вызов в сотни микросекунд, переключение — микросекунды плюс промахи кеша. Плюс они шумят друг на друга: общая память требует синхронизации со всеми её гонками и дедлоками, а один SIGSEGV в любом потоке кладёт весь процесс.

Глубже. Модель «поток на запрос» плохо масштабируется именно из-за квадратичного роста накладных расходов: при 10 000 соединений вы получаете 10 000 потоков, планировщик тратит заметную долю CPU на переключения, а рабочие множества выбивают друг друга из кеша (thrashing). Отсюда исторический сдвиг к событийным моделям (epoll + пул потоков) и к зелёным потокам, где Go — самый массовый пример. Второй класс проблем — false sharing: два потока пишут в соседние переменные, попавшие в одну кеш-линию (64 байта), и линия постоянно мигрирует между ядрами; лечится выравниванием и паддингом. Третий — сложность корректности: гонки данных, приоритетная инверсия, дедлоки, которые не воспроизводятся под отладчиком.

Как вы реализуете процесс распаковки входящего Ethernet-фрейма: опишите последовательность шагов, в которой вы будете производить распаковку, чтобы добраться до нужных данных (например, до DNS протокола)?

Заголовок раздела «Как вы реализуете процесс распаковки входящего Ethernet-фрейма: опишите последовательность шагов, в которой вы будете производить распаковку, чтобы добраться до нужных данных (например, до DNS протокола)?»

Коротко. Слой за слоем: Ethernet-заголовок (по 6 байт MAC-адресов + 2 байта EtherType, при наличии — 4 байта VLAN-тега 802.1Q) → по EtherType 0x0800 идём в IPv4 (или 0x86DD в IPv6) → из IPv4-заголовка берём IHL, чтобы найти начало полезной нагрузки, и поле Protocol (17 — UDP, 6 — TCP) → UDP-заголовок 8 байт, смотрим порты, 53 → DNS → парсим DNS-сообщение: 12-байтовый заголовок, затем секции question/answer/authority/additional.

Глубже. Подводных камней тут больше, чем шагов. Все многобайтовые поля в сетевом порядке (big endian). IPv4-заголовок переменной длины: смещение payload — это IHL * 4, а не константа 20. Фрагментация: если MF установлен или Frag Offset != 0, DNS-сообщение может не поместиться в один фрейм, и нужна пересборка по (src, dst, id, proto). IPv6 вместо IHL имеет цепочку extension headers, которую надо пройти до Next Header. Для DNS поверх TCP есть двухбайтовый префикс длины перед сообщением, а само сообщение может прийти в нескольких сегментах — нужна пересборка TCP-потока. Внутри DNS имена кодируются как последовательность меток «длина + байты» с терминирующим нулём и поддерживают компрессию: байт с двумя старшими битами 11 означает 14-битный указатель на смещение в сообщении, поэтому наивный парсер зациклится на злонамеренном пакете — нужен лимит прыжков. На каждом шаге обязательна проверка границ буфера, иначе получаем классический out-of-bounds. Практически такое пишут не с нуля: AF_PACKET/libpcap для захвата и github.com/google/gopacket для разбора, а из типовой архитектуры стоит упомянуть zero-copy (PACKET_MMAP, AF_XDP) и то, что offload (LRO/GRO) может отдать вам «фрейм» больше MTU.

// Схема разбора без внешних зависимостей: только смещения и проверки границ.
// import "encoding/binary"
func dnsPayload(frame []byte) ([]byte, bool) {
if len(frame) < 14 {
return nil, false
}
off := 12
etherType := binary.BigEndian.Uint16(frame[off:])
for etherType == 0x8100 || etherType == 0x88a8 { // VLAN / QinQ
off += 4
if len(frame) < off+2 {
return nil, false
}
etherType = binary.BigEndian.Uint16(frame[off:])
}
off += 2
if etherType != 0x0800 || len(frame) < off+20 {
return nil, false
}
ip := frame[off:]
ihl := int(ip[0]&0x0f) * 4
if ihl < 20 || len(ip) < ihl+8 || ip[9] != 17 { // 17 = UDP
return nil, false
}
udp := ip[ihl:]
if binary.BigEndian.Uint16(udp[2:]) != 53 { // dst port
return nil, false
}
return udp[8:], true
}

Коротко. Это вопрос про личный опыт — фактов за вас тут никто не придумает. Отвечать надо конкретно и коротко: методология и её реальная (а не декларируемая) форма, длина итерации, состав команды, путь задачи от идеи до прода, кто принимает решения, как измеряете результат.

Глубже. Хороший каркас ответа: (1) состав команды и ваша роль в ней — «шесть человек: четыре бэкенда, QA, продакт, тимлид»; (2) методология и ритуалы, только те, что реально проводились — двухнедельные спринты, дейли 15 минут, груминг, ретро; (3) путь задачи — от требований продакта через декомпозицию и оценку к ветке, code review, CI, стенду и релизу; (4) как разбирались с непредвиденным — hotfix-процесс, дежурства, обработка инцидентов; (5) одна честная проблема и что вы с ней сделали. Интервьюер хочет понять, работали ли вы в инженерной культуре или в хаосе, и умеете ли вы её описать своими словами. Типичные ошибки: пересказ учебника по Scrum без единой детали своего проекта; жалобы на бывшего работодателя; обратная крайность — «у нас всё было идеально».

Какими способами в Linux можно посмотреть, какие файлы открыты процессом?

Заголовок раздела «Какими способами в Linux можно посмотреть, какие файлы открыты процессом?»

Коротко. ls -l /proc/<PID>/fd — самый прямой способ, символические ссылки указывают на файлы, сокеты, pipe, anon_inode. Ещё lsof -p <PID>, а обратный вопрос («кто держит файл») — fuser -v <file> или lsof <file>.

Глубже. Дополняющие источники: /proc/<PID>/fdinfo/<N> — текущее смещение, флаги, а для epoll/inotify ещё и содержимое; /proc/<PID>/maps — файлы, отображённые в память через mmap (их в fd может уже не быть, дескриптор после mmap закрывают); /proc/<PID>/cwd и /proc/<PID>/exe — рабочий каталог и сам бинарь; ss -tanp — сетевые сокеты с привязкой к процессу. Лимиты смотрятся в /proc/<PID>/limits или через prlimit --pid <PID>, а изменить на лету — prlimit --pid <PID> --nofile=65536:65536. Счётчик открытых дескрипторов удобно снимать как ls /proc/<PID>/fd | wc -l; рост этого числа — классический симптом утечки (в Go — незакрытые resp.Body, rows из database/sql, тикеры и файлы). Общесистемно: sysctl fs.file-nr и fs.file-max.

Потребляет ли память зомби-процесс? Почему они появляются и как с ними бороться?

Заголовок раздела «Потребляет ли память зомби-процесс? Почему они появляются и как с ними бороться?»

Коротко. Практически нет: адресное пространство, стек и дескрипторы освобождены при exit(), остаётся только task_struct с кодом возврата и статистикой — единицы килобайт ядерной памяти. Настоящий дефицитный ресурс, который зомби занимает, — это слот в таблице процессов (PID). Появляются они, когда родитель не вызвал wait()/waitpid(); лечатся тем, что родитель начинает забирать статусы, либо тем, что родителя убивают, и зомби переусыновляется init.

Глубже. Способы, которыми родитель может корректно «жать» потомков: синхронно вызывать waitpid(); ставить обработчик SIGCHLD и в нём крутить waitpid(-1, &st, WNOHANG) в цикле (сигналы стандартного диапазона не ставятся в очередь, поэтому за один вызов обработчика надо забрать всех); либо явно отказаться от статусов — signal(SIGCHLD, SIG_IGN) или sigaction с SA_NOCLDWAIT, тогда ядро реапит само. Классический трюк для демонов — двойной fork (см. отдельный вопрос ниже). В контейнерах проблема острее: PID 1 обязан реапить сирот, иначе зомби копятся до исчерпания pid_max/лимита cgroup pids.max; решается docker run --init, tini, dumb-init либо тем, что приложение само реапит. В Go всё это скрыто за os/exec: если вы сделали cmd.Start(), но не сделали cmd.Wait() — получите зомби; cmd.Run() ждёт сам. Найти зомби: ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/' или строка zombie в выводе top.

CPU Steal time = 10% - что это значит? В каких ситуациях возникает этот показатель?

Заголовок раздела «CPU Steal time = 10% - что это значит? В каких ситуациях возникает этот показатель?»

Коротко. Steal time — доля времени, в течение которой ваш виртуальный CPU был готов выполнять код, но гипервизор не давал ему физическое ядро, потому что отдавал его другой виртуалке. 10% значит, что примерно каждая десятая доля процессорного времени у вас отобрана: latency растёт, а изнутри гостя это выглядит как «CPU не загружен, а всё тормозит».

Глубже. Показатель виден только в виртуализованном окружении (%st в top, колонка st в vmstat 1, %steal в mpstat); на bare metal он всегда 0. Типичные причины: переподписка хоста по vCPU (гипервизор продал больше ядер, чем есть), шумные соседи, burstable-инстансы с исчерпанными кредитами (AWS t2/t3 при standard режиме, где после кредитов вас урезают), CPU-квота на уровне гипервизора. Важно не путать steal с throttling в cgroup: если контейнеру задан cpu.max и он его выбирает, время не показывается как steal — это видно в /sys/fs/cgroup/cpu.stat как nr_throttled и throttled_usec. Реакция на устойчивые 10%: посмотреть, воспроизводится ли на другом хосте (пересоздать инстанс — часто помогает, попадаете на менее нагруженный хост), перейти на dedicated/compute-optimized инстанс, снизить плотность, если хост свой. Короткие всплески steal нормальны; тревожно, когда это устойчивый уровень и коррелирует с ростом p99.

Что такое зомби-процесс? В чем его особенность?

Заголовок раздела «Что такое зомби-процесс? В чем его особенность?»

Коротко. Зомби (Z, EXIT_ZOMBIE) — процесс, который уже завершился, освободил всю память и дескрипторы, но его запись в таблице процессов ещё жива, потому что родитель не забрал код возврата через wait(). Особенность: его нельзя убить — он уже мёртв; kill -9 на него не действует, убирается он только wait() со стороны родителя или смертью родителя.

Глубже. Зомби — не баг, а часть контракта: ядро обязано сохранить код выхода и rusage до тех пор, пока родитель их не запросил, иначе родитель не смог бы узнать результат своего потомка. Проблемой это становится, когда зомби долгоживущие и массовые. Детали: у зомби в ps часто виден <defunct>, /proc/<PID> ещё существует, но /proc/<PID>/fd пуст; RSS равен 0. Смотрите ещё вопрос «Потребляет ли память зомби-процесс».

Коротко. R — выполняется или готов к выполнению; S — прерываемый сон (ждёт события, реагирует на сигналы); D — непрерываемый сон (обычно I/O в ядре, сигналы не доставляются); T — остановлен сигналом job control; t — остановлен трассировщиком; Z — зомби; X — мёртв (в ps практически не встречается); I — idle-поток ядра (с 4.14).

Глубже. R в Linux означает и «на CPU прямо сейчас», и «в runqueue» — отдельного ready нет. Разница S и D практическая: из S процесс можно выбить SIGKILL, из D — нет, пока он не выйдет из ядерной секции; массовое накопление D — верный признак проблем с диском или сетевой ФС. Есть промежуточный TASK_KILLABLE, который используется в NFS: непрерываемый сон, но SIGKILL его пробивает. Флаги рядом со статусом в ps aux: < — высокий приоритет, N — низкий (nice > 0), L — есть заблокированные страницы, s — лидер сессии, l — многопоточный, + — в foreground-группе терминала. Смотреть удобно ps -eo pid,stat,wchan:20,comm или top (строка Tasks: разбивает по состояниям).

Какие способы взаимодействия между процессами существуют?

Заголовок раздела «Какие способы взаимодействия между процессами существуют?»

Коротко. См. выше вопрос «Какие есть виды межпроцессного взаимодействия»: pipe/FIFO, сокеты (Unix domain и сетевые), разделяемая память, очереди сообщений, семафоры, сигналы, файлы с блокировками, eventfd/signalfd, передача дескрипторов через SCM_RIGHTS. Отличие акцента в том, что здесь уместно сразу назвать критерий выбора: нужен максимум скорости — разделяемая память, нужны структурированные сообщения и права — Unix domain socket, нужна работа по сети — TCP/gRPC.

Коротко. Сигнал — это механизм асинхронного уведомления процесса о событии: программное прерывание с номером и минимумом полезной нагрузки. У каждого сигнала есть действие по умолчанию (завершить, завершить с дампом, игнорировать, остановить, продолжить), которое процесс может переопределить обработчиком, кроме SIGKILL и SIGSTOP.

Глубже. У задачи в ядре есть маска заблокированных сигналов (sigprocmask) и множество отложенных; доставка происходит при возврате в user space. Сигналы 1–31 — стандартные и не ставятся в очередь: если SIGCHLD пришёл трижды до доставки, обработчик вызовется один раз (отсюда цикл waitpid(WNOHANG)). Сигналы 32–64 — реалтаймовые (SIGRTMIN..SIGRTMAX), они очередятся и несут значение. Внутри обработчика можно вызывать только async-signal-safe функции (список в man 7 signal-safety) — printf и malloc туда не входят. Современная практика — не писать обработчики, а превращать сигналы в события файлового дескриптора (signalfd) или, как в Go, читать их из канала: signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT) либо signal.NotifyContext. Полезно знать, что рантайм Go сам использует SIGURG для асинхронного вытеснения и SIGPROF для профилирования, поэтому под strace и gdb их видно постоянно, и это норма.

// import "context"; "errors"; "log"; "net/http"; "os/signal"; "syscall"; "time"
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)

Коротко. SIGTERM (15) — просьба завершиться: его можно перехватить, заблокировать или проигнорировать, и приложение успевает корректно закрыть соединения, дописать буферы, снять себя с балансировщика. SIGKILL (9) — принудительное снятие ядром: перехватить или заблокировать нельзя, процесс умирает немедленно и без всякой уборки.

Глубже. SIGKILL обрабатывается самим ядром, поэтому пользовательский код после него не выполняется вообще: буферы bufio, незакоммиченные транзакции, временные файлы, defer — всё теряется. Отсюда стандартный порядок и в systemd (KillSignal=SIGTERM, ожидание TimeoutStopSec, затем SIGKILL), и в Kubernetes (preStop hook → SIGTERMterminationGracePeriodSeconds по умолчанию 30 с → SIGKILL), и в Docker (docker stop = SIGTERM + 10 с + SIGKILL). Единственный случай, когда SIGKILL «не работает», — процесс в состоянии D или уже зомби. Практический вывод для сервиса на Go: обязательно обрабатывать SIGTERM и укладываться в grace-период, иначе платформа добьёт вас SIGKILL и вы получите оборванные запросы.

Как в Linux происходит fork процесса? Чем fork отличается от exec?

Заголовок раздела «Как в Linux происходит fork процесса? Чем fork отличается от exec?»

Коротко. fork() создаёт почти точную копию вызывающего процесса: новый PID, копия адресного пространства (реализована через copy-on-write — страницы помечаются read-only и физически дублируются только при записи), копия таблицы дескрипторов. В родителе fork возвращает PID ребёнка, в ребёнке — 0. execve() ничего не создаёт: он заменяет образ текущего процесса новой программой, сохраняя PID, родителя и открытые дескрипторы без O_CLOEXEC.

Глубже. fork — это обёртка над clone() без флагов разделения ресурсов. Что наследуется: дескрипторы (причём разделяется само file description, то есть общее смещение чтения), обработчики сигналов, cwd, umask, переменные окружения, приоритет, лимиты. Что не наследуется: PID, PPID, отложенные сигналы (очищаются), блокировки файлов через fcntl, таймеры, и главное — потоки: в ребёнке остаётся только тот поток, который вызвал fork. Отсюда правило, что между fork и exec в многопоточной программе можно вызывать только async-signal-safe функции (мьютекс, захваченный чужим потоком в момент fork, в ребёнке останется захваченным навсегда). Пара fork + exec — канонический способ запуска программы в UNIX; между ними ребёнок успевает перенаправить stdin/stdout, сменить uid, cwd, применить setsid. В Go напрямую fork не вызывают (рантайм многопоточный) — используют os/exec, который внутри делает forkExec под syscall.ForkLock и сразу execve; ещё есть posix_spawn и clone3/vfork как более дешёвые варианты, где полная копия таблиц не нужна.

Коротко. См. выше «Отличия потоков и процессов». Одной фразой: процесс — это адресное пространство плюс набор ресурсов, поток — это исполнение внутри него; у процесса память своя и защищённая, у потоков одного процесса — общая.

Что такое зомби процесс и как они появляются?

Заголовок раздела «Что такое зомби процесс и как они появляются?»

Коротко. См. выше «Что такое зомби-процесс». Появляются ровно одним способом: потомок завершился, а родитель не вызвал wait()/waitpid() — типично для приложения, которое делает cmd.Start() и забывает cmd.Wait(), или для PID 1 в контейнере, который не реапит сирот.

Коротко. См. выше «В чем преимущества горутин по сравнению с потоками ОС»: дешёвый стек с 2 КБ, переключение в user space без syscall, интеграция с netpoller, из-за которой блокирующий по виду сетевой вызов не занимает поток. Добавлю единственное реальное ограничение: параллелизм всё равно ограничен GOMAXPROCS, и горутины не спасают от CPU-bound нагрузки — они экономят на ожидании, а не на вычислениях.

Коротко. См. выше — вопрос уже разбирали. Отличие в изоляции ресурсов (адресное пространство, дескрипторы, сигналы) и в стоимости создания/переключения; планировщик ОС при этом видит и то и другое одинаково — как задачи.

Можно ли создать количество потоков больше, чем ядер процессора?

Заголовок раздела «Можно ли создать количество потоков больше, чем ядер процессора?»

Коротко. Да, и это нормальный режим работы: ОС мультиплексирует потоки на ядра по времени. Предел задаётся не числом ядер, а kernel.threads-max, kernel.pid_max, RLIMIT_NPROC, лимитом cgroup pids.max и памятью под стеки; практический потолок — десятки тысяч.

Глубже. Вопрос не «можно ли», а «сколько имеет смысл». Для чисто CPU-bound работы оптимум примерно равен числу доступных ядер (с учётом hyper-threading — числу логических процессоров, но выигрыш от SMT редко больше 20–30%): больше потоков не добавляют вычислительной мощности, зато добавляют переключений и промахов кеша. Для I/O-bound работы потоков нужно больше, потому что большую часть времени они спят; классическая оценка — N = ядра × (1 + время_ожидания / время_счёта). В Go этот баланс решается за вас: GOMAXPROCS (по умолчанию число логических CPU, а с Go 1.25 ещё и с учётом CPU-лимита cgroup) ограничивает число одновременно исполняющих горутины потоков, а на время блокирующих syscall рантайм создаёт дополнительные M; жёсткий потолок — runtime/debug.SetMaxThreads, по умолчанию 10 000.

Как происходил процесс создание новых фич? Насколько сильно декомпозировали задачу? Были ли фичи на несколько спринтов?

Заголовок раздела «Как происходил процесс создание новых фич? Насколько сильно декомпозировали задачу? Были ли фичи на несколько спринтов?»

Коротко. Вопрос про личный опыт. Интервьюер проверяет, умеете ли вы дробить задачу и договариваться о скоупе, а не только писать код. Отвечать надо на конкретном примере одной фичи из вашего проекта, проведя её от постановки до релиза.

Глубже. Каркас: (1) откуда приходила фича — продакт, аналитика, инцидент; (2) как формулировались требования и как вы участвовали в их уточнении (какие вопросы задавали, что резали); (3) техническое проектирование — обсуждали ли дизайн-док/RFC, кто ревьюил; (4) декомпозиция — на какие подзадачи резали (миграция схемы, API, обработчик, метрики, фича-флаг), какой был типичный размер задачи (хороший ответ — «1–3 дня, чтобы PR оставался ревьюабельным»); (5) большие фичи — да, бывали на несколько спринтов, и рассказать, как их вели: инкрементальная поставка за фича-флагом, поэтапный роллаут, отдельная ветка только когда без неё нельзя. Типичные ошибки: ответ «мне давали таску в Jira, я делал» (нулевое участие в проектировании); утверждение, что декомпозиция не нужна; невозможность назвать ни одного примера.

Коротко. Создаёт новый процесс — почти полную копию вызывающего: копирует task_struct, помечает страницы обоих процессов copy-on-write, дублирует таблицу дескрипторов. Возвращает 0 в дочернем процессе и PID ребёнка в родительском (или −1 при ошибке).

Глубже. См. развёрнуто выше в вопросе «Как в Linux происходит fork процесса». Стоит помнить, что благодаря COW сам fork дёшев по данным, но не бесплатен: копируются таблицы страниц, и для процесса с большим адресным пространством это заметно (классическая проблема Redis при BGSAVE). Ещё нюанс — fork может провалиться с ENOMEM при vm.overcommit_memory=2, потому что формально требует вдвое больше коммита.

А как процессы могут обмениваться информацией?

Заголовок раздела «А как процессы могут обмениваться информацией?»

Коротко. См. выше «Какие есть виды межпроцессного взаимодействия». Кратко ранжируя по типовому применению: локально и быстро — Unix domain socket или разделяемая память; между машинами — TCP/HTTP/gRPC или брокер; уведомить о событии — сигнал или eventfd; передать поток данных потомку — pipe.

В утилите top на Linux процесс отображается с нагрузкой 146% CPU. Возможно ли, чтобы один процесс использовал больше 100% процессора? Как интерпретировать это значение?

Заголовок раздела «В утилите top на Linux процесс отображается с нагрузкой 146% CPU. Возможно ли, чтобы один процесс использовал больше 100% процессора? Как интерпретировать это значение?»

Коротко. Да, это нормально. В top по умолчанию (режим Irix) 100% — это одно логическое ядро, поэтому 146% означает, что процесс суммарно занимает примерно полтора ядра своими потоками. Верхняя граница — 100% × число логических CPU.

Глубже. Переключить представление можно клавишей I (Irix/Solaris mode): в Solaris-режиме значение делится на число CPU и никогда не превысит 100%. Клавиша 1 разворачивает загрузку по ядрам, H показывает отдельные потоки вместо процессов — именно так проверяют, действительно ли работа распараллелена или один поток упёрся в 100%, а остальные простаивают. Важно: %CPU в top — мгновенное значение за интервал обновления, а %CPU в ps aux — среднее за всё время жизни процесса, поэтому они расходятся. В контейнере с cpu.max процесс может показывать 146%, при этом упираясь в квоту и подвергаясь throttling — проверять в /sys/fs/cgroup/cpu.stat. Для Go-приложения 146% при GOMAXPROCS=8 — это просто несколько активных P плюс работа GC.

Коротко. См. выше «Команда чтобы убить процесс»: kill <PID> (SIGTERM), при отказе kill -9, по имени — pkill/killall, сервис — через systemctl stop. Найти PID: pgrep -af <шаблон>, ps aux | grep, ss -tlnp по порту.

Как построен процесс разработки? Какие артефакт из скрама используете?

Заголовок раздела «Как построен процесс разработки? Какие артефакт из скрама используете?»

Коротко. Вопрос про личный опыт. Здесь важно назвать артефакты правильно и честно сказать, какие из них у вас реально жили. Канонические артефакты Scrum ровно три: Product Backlog, Sprint Backlog и Increment; к ним привязаны обязательства — Product Goal, Sprint Goal и Definition of Done.

Глубже. События Scrum (их часто путают с артефактами): Sprint, Sprint Planning, Daily Scrum, Sprint Review, Sprint Retrospective. Роли: Product Owner, Scrum Master, Developers. Хороший ответ звучит так: «Формально Scrum с двухнедельными спринтами. Реально жили: бэклог в Jira, планирование с оценкой в story points, дейли, ретро раз в спринт. Sprint Goal формулировали не всегда — и это било по приоритизации. Definition of Done был записан: код-ревью двумя людьми, юнит- и интеграционные тесты, обновлённая документация API, метрики и алерты». Ошибки: перечислять burndown chart как артефакт (он вспомогательный инструмент, а не артефакт по Scrum Guide 2020); заявлять полный набор ритуалов, если половина из них не проводилась — уточняющий вопрос это вскроет.

Коротко. См. выше. Ключевое отличие в том, кто планирует: поток планирует ядро (вытесняюще, по таймеру, с переключением в кольцо 0), горутину — рантайм Go в user space (кооперативно в точках безопасности плюс асинхронное вытеснение с Go 1.14). Отсюда разница в цене: 2 КБ против мегабайтов стека и десятки наносекунд против микросекунд на переключение.

Что можете сказать об организации многозадачности на уровне ос?

Заголовок раздела «Что можете сказать об организации многозадачности на уровне ос?»

Коротко. Современные ОС используют вытесняющую многозадачность: аппаратный таймер прерывает выполнение, планировщик решает, кому отдать CPU дальше, и переключает контекст. В Linux планировщик модульный, с классами приоритета: SCHED_DEADLINE > SCHED_FIFO/SCHED_RR (реалтайм) > SCHED_OTHER (обычные задачи, CFS, с ядра 6.6 — EEVDF) > SCHED_BATCH > SCHED_IDLE.

Глубже. Кооперативная многозадачность (задача сама уступает CPU) осталась в старых ОС и в user-space рантаймах — Go до 1.14 работал именно так, и одна зациклившаяся горутина могла заморозить всё. CFS не оперировал квантами в привычном смысле: он вёл vruntime — виртуальное время, накрученное с поправкой на вес (nice), и всегда выбирал задачу с наименьшим vruntime, стремясь к «идеальной» справедливости. EEVDF (Earliest Eligible Virtual Deadline First), заменивший CFS, добавляет к этому явный учёт латентности: задача получает виртуальный дедлайн, что лучше обслуживает интерактивные короткие задачи. Дополнительные вещи, которые стоит назвать: тик планировщика и tickless-режим (NOHZ, NOHZ_FULL), балансировка задач между ядрами и NUMA-aware размещение, привязка через taskset/sched_setaffinity, групповое планирование через cgroup v2 (cpu.weight, cpu.max), приоритеты nice от −20 до +19 и опасность реалтайм-приоритетов (задача SCHED_FIFO в бесконечном цикле может подвесить ядро, отсюда sched_rt_runtime_us).

Как был организован рабочий процесс: использовали ли Scrum или Kanban, как проходила проработка требований?

Заголовок раздела «Как был организован рабочий процесс: использовали ли Scrum или Kanban, как проходила проработка требований?»

Коротко. Вопрос про личный опыт, близкий к предыдущим. Отвечайте честно, какая методология была и почему она подходила: Scrum — фиксированные итерации, планирование и обязательство на спринт; Kanban — непрерывный поток, лимиты WIP, метрики lead time и cycle time вместо velocity. Многие команды живут в «Scrumban» — доска с лимитами и дейли, но без жёстких спринтов.

Глубже. Про проработку требований интервьюер хочет услышать конкретику: кто пишет постановку (продакт, аналитик, вы сами), в каком виде (user story с критериями приёмки, дизайн-док, тикет из одной строки), проводится ли груминг/refinement, на каком этапе подключается разработчик, кто пишет тест-кейсы, как фиксируются нефункциональные требования (нагрузка, SLA, безопасность). Сильный ответ содержит пример, когда требования были плохими, вы это заметили до начала работы и что предприняли. Ошибка — рассказывать теорию Scrum вместо своего процесса, или отвечать «требования приходили сверху, я не участвовал».

Коротко. Поток — минимальная единица выполнения, которую планирует ядро: у него свой стек, набор регистров, счётчик команд, приоритет и TID, а адресное пространство, дескрипторы и обработчики сигналов он делит с остальными потоками процесса.

Глубже. В Linux поток — это task_struct, созданная clone() с CLONE_THREAD | CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_FS; все потоки процесса имеют общий TGID (это и есть «PID процесса»), а различаются TID. Модель Linux — 1:1 (NPTL): каждый pthread_create создаёт задачу ядра; исторически бывали M:N-реализации, но от них отказались из-за сложности. У каждого потока есть собственный ядерный стек (обычно 16 КБ на x86-64) и пользовательский стек, размер которого задаётся ulimit -s (8 МБ по умолчанию) или явно через pthread_attr_setstacksize. Виртуальная память под стек резервируется, а физическая выделяется по факту касания страниц, поэтому 8 МБ на поток — это в основном адресное пространство, а не RSS; но при тысячах потоков даже коммит и ядерные структуры становятся заметными.

Чем горутина отличается от потока операционной системы?

Заголовок раздела «Чем горутина отличается от потока операционной системы?»

Коротко. См. выше — вопрос уже разбирали. Одной фразой: горутина — сущность рантайма Go, невидимая ядру, которая мультиплексируется на потоки ОС; поток — сущность ядра, которую планирует ядро. Отсюда разница в стоимости на два порядка.

Как на вашем проекте происходил процесс тестирования перед деплоем?

Заголовок раздела «Как на вашем проекте происходил процесс тестирования перед деплоем?»

Коротко. Вопрос про личный опыт. Разложите его по уровням пирамиды и по стадиям пайплайна: что гоняется на каждом PR, что на merge в main, что перед релизом, и кто принимает решение выкатывать.

Глубже. Каркас хорошего ответа: (1) локально и в CI на PR — go vet, линтеры (golangci-lint), юнит-тесты с -race, проверка покрытия; (2) интеграционные тесты с реальными зависимостями в Docker (testcontainers-go, поднятые Postgres/Kafka), миграции прогоняются в тесте; (3) контрактные/e2e-тесты на стенде, smoke-набор после деплоя; (4) нагрузочное тестирование для критичных сервисов (k6, vegeta) и профилирование; (5) сама выкатка — стейджинг, затем canary или blue-green, наблюдение за метриками ошибок и латентности, автоматический откат по алерту, фича-флаги для рискованных изменений. Обязательно упомяните, что тестировали миграции БД на откат и что делали при обнаружении проблемы на проде. Ошибки: «тестировал QA, я не в курсе»; «покрытие 90%» без объяснения, что именно покрыто; отсутствие любого упоминания о том, что происходит после деплоя.

Что для вас самое сложное в процессе code review?

Заголовок раздела «Что для вас самое сложное в процессе code review?»

Коротко. Вопрос поведенческий: проверяют зрелость и коммуникацию. Хороший ответ называет конкретную трудность, показывает осознанную позицию и приём, которым вы её решаете, — без пафоса и без жалоб на коллег.

Глубже. Рабочие варианты честного ответа: (1) отделять «это неправильно» от «я бы сделал иначе» — помогает явно маркировать необязательные замечания как nit: и не блокировать PR из-за вкусовщины; (2) ревьюить большие PR — глаз замыливается после ~400 строк, поэтому договариваетесь о размере или просите автора провести по диффу; (3) понять контекст чужой доменной области — просите описание в PR: что за задача, почему такое решение, как проверялось; (4) формулировать критику так, чтобы она не читалась как наезд — писать вопросами и про код, а не про человека; (5) удерживать баланс скорости и качества, когда релиз горит. Ошибки: «мне ничего не сложно»; «сложно объяснять джунам, что они не правы» (звучит высокомерно); превращение ответа в жалобу на процесс.

Коротко. Это нагрузка, чья скорость упирается не в процессор, а в ввод-вывод: диск, сеть, ответ базы. Процессор при этом простаивает в ожидании, %CPU низкий, %wa (iowait) и число задач в состоянии D/S — высокие, а ускорить дело можно параллелизмом и уменьшением числа round-trip, но не более быстрым CPU.

Глубже. Практическое различие с CPU-bound определяет и архитектуру, и настройки. Для CPU-bound: число воркеров ≈ числу ядер, профилирование pprof по CPU, оптимизация алгоритмов и аллокаций. Для I/O-bound: высокая конкурентность (в Go — просто много горутин, netpoller это вытянет), батчинг и pipelining, кеширование, пулы соединений (SetMaxOpenConns), устранение проблемы N+1 запросов, таймауты и контексты на каждый внешний вызов. Диагностика: top (%wa), iostat -x 1 (await, %util), pidstat -d, для Go — pprof покажет, что горутины стоят в runtime.gopark, а trace — сколько времени ушло в сетевые ожидания. Отдельно отметьте, что iowait — это доля времени, когда CPU был idle и при этом были незавершённые I/O-запросы; на многоядерных машинах он легко вводит в заблуждение, надёжнее смотреть PSI (/proc/pressure/io).

В чем разница между процессом и потоком? А в чем разница?

Заголовок раздела «В чем разница между процессом и потоком? А в чем разница?»

Коротко. См. выше — это тот же вопрос. Процесс изолирован адресным пространством и таблицей дескрипторов, поток живёт внутри процесса и делит их с соседями, имея личными только стек, регистры и TLS. Если интервьюер переспрашивает «а в чём ещё?», добавляйте измерения: стоимость создания и переключения, гранулярность планирования (планируется поток), поведение при падении (падает весь процесс), способ обмена данными (IPC против общей памяти), сигналы (доставляются группе потоков).

Коротко. См. выше «Что делает сисколл fork»: системный вызов, создающий копию процесса с copy-on-write адресным пространством, возвращающий 0 ребёнку и PID ребёнка родителю.

Коротко. Это экспоненциально сглаженное среднее число задач, находящихся в состояниях «выполняется/готово к выполнению» (R) и «непрерываемый сон» (D), за 1, 5 и 15 минут. В Linux, в отличие от классического UNIX, туда входит и ожидание дискового I/O, поэтому LA — это не «загрузка CPU», а «длина очереди на ресурсы».

Глубже. Интерпретация: сравнивать надо с числом логических CPU. LA = 4 на четырёхъядерной машине — примерно полная утилизация; на одноядерной — трёхкратная перегрузка. Значение читается из /proc/loadavg (там же четвёртое поле — «выполняющихся/всего задач» и пятое — последний созданный PID). Ключевая ловушка: высокий LA при почти нулевом %CPU означает, что задачи стоят в D — умирает диск или сетевая ФС, а вовсе не процессор. Обратная ситуация тоже бывает: LA низкий, а сервис тормозит из-за throttling в cgroup или steal time — в LA это не отражается. Из-за экспоненциального сглаживания LA инертен: короткий всплеск размажется, а после реального пика значение будет спадать минутами. Более точный инструмент — PSI (/proc/pressure/cpu, io, memory), где напрямую видно, сколько времени задачи простояли из-за нехватки конкретного ресурса.

А в чем разница между рутиной и потоком операционной системы?

Заголовок раздела «А в чем разница между рутиной и потоком операционной системы?»

Коротко. Речь о горутине — см. выше. Горутина планируется рантаймом Go в пользовательском пространстве и стоит 2 КБ стека, поток планируется ядром и стоит на порядки дороже; N горутин мультиплексируются на M потоков.

Используете ли вы Helm или Argo CD для автоматизации и ускорения процесса релиза?

Заголовок раздела «Используете ли вы Helm или Argo CD для автоматизации и ускорения процесса релиза?»

Коротко. Вопрос про личный опыт с явным техническим подтекстом. Отвечать надо фактически (что реально стояло) и показать понимание, что это инструменты из разных слоёв: Helm — пакетирование и шаблонизация манифестов Kubernetes, Argo CD — GitOps-контроллер непрерывной доставки, который следит, чтобы состояние кластера соответствовало Git. Они не альтернативы: Argo CD прекрасно раскатывает Helm-чарты.

Глубже. Что уместно добавить, если опыт есть: как выглядел репозиторий (чарт приложения плюс values на окружение, либо Kustomize-оверлеи), как решались секреты (External Secrets, Sealed Secrets, Vault — не в Git открытым текстом), как выкатывались миграции (init-контейнер или отдельный Job), как делался прогрессивный роллаут (Argo Rollouts, canary, maxSurge/maxUnavailable) и как быстро откатывались (helm rollback против git revert в GitOps-модели). Полезно упомянуть принцип GitOps: единственный источник правды — Git, состояние кластера декларативно, дрейф детектируется и чинится контроллером. Если опыта нет — так и сказать, обозначив, что использовали (raw-манифесты, Kustomize, деплой из GitLab CI через kubectl apply) и что понимаете, какую проблему Helm и Argo CD решают.

Что такое процесс и поток, и в чем разница?

Заголовок раздела «Что такое процесс и поток, и в чем разница?»

Коротко. См. выше — вопрос повторяет уже разобранное. Процесс — исполняемая программа вместе с выделенными ей ресурсами и изолированным адресным пространством; поток — единица выполнения внутри процесса, планируемая ядром. Разница: изоляция и стоимость.

Коротко. В первую очередь изоляция памяти: у каждого процесса свои таблицы страниц, MMU не даёт обратиться к чужой физической памяти, попытка выхода за границы даёт page fault и SIGSEGV. Плюс отдельная таблица файловых дескрипторов, свои credentials (uid/gid, capabilities), свои обработчики сигналов и, при использовании namespaces, изолированные представления PID, сети, точек монтирования и IPC.

Глубже. Уровни изоляции стоит перечислить по нарастанию: аппаратная (кольца защиты, MMU, бит NX), ядерная (namespaces — pid, net, mnt, uts, ipc, user, cgroup, time — плюс cgroup для ресурсов), политики безопасности (capabilities вместо всемогущего root, seccomp-BPF для ограничения набора syscall, LSM — SELinux/AppArmor), и наконец виртуализация или микроVM (Firecracker, gVisor, Kata) там, где ядро в общем доступе недопустимо. Чего изоляция процессов не даёт: общего ядра избежать нельзя, поэтому уязвимость в syscall — это побег; кеши и предсказатель переходов физически общие, отсюда Spectre/Meltdown и side-channel-атаки; общие ресурсы вроде диска, сети и файловой системы разделяются, если их явно не ограничить cgroup и namespace. Контейнер — это ровно набор namespaces + cgroups + seccomp/LSM поверх обычного процесса, а не отдельная сущность ядра.

Нужно обмениваться информацией между процессами. Какие инструменты для этого может предложить ОС?

Заголовок раздела «Нужно обмениваться информацией между процессами. Какие инструменты для этого может предложить ОС?»

Коротко. См. выше «Какие есть виды межпроцессного взаимодействия». Если формулировать как выбор инструмента под задачу: односторонний поток данных потомку — pipe; двусторонний локальный обмен с правами и передачей дескрипторов — Unix domain socket; максимальная пропускная способность на большом объёме — разделяемая память (shm_open + mmap) с семафором или futex для синхронизации; редкие уведомления — сигнал или eventfd; работа между хостами — сетевой сокет.

Глубже. Практический ориентир по стоимости: разделяемая память после настройки не требует переходов в ядро вообще и упирается только в пропускную способность памяти; UDS — один syscall и копирование на сообщение, десятки микросекунд на round-trip; локальный TCP медленнее UDS из-за прохода через стек, хотя и работает через loopback. Не забудьте про синхронизацию: разделяемая память без барьеров и блокировок — это гонки; для процессов подходят POSIX-семафоры в разделяемой памяти (sem_init с pshared=1), pthread_mutex с атрибутом PTHREAD_PROCESS_SHARED или прямой futex. В Go шаред-мемори через syscall.Mmap возможен, но идиоматичнее взять UDS с gRPC или обычный TCP — это почти всегда достаточно быстро и намного менее хрупко.

За счет чего обеспечивается потокобезопасность каналов?

Заголовок раздела «За счет чего обеспечивается потокобезопасность каналов?»

Коротко. Канал в Go — это структура hchan с обычным мьютексом рантайма внутри: каждая операция отправки, получения и close захватывает этот мьютекс, поэтому конкурентный доступ безопасен. Канал не lock-free; безопасность обеспечивается блокировкой плюс механикой парковки/пробуждения горутин через gopark/goready.

Глубже. Внутри hchan лежат кольцевой буфер buf с индексами sendx/recvx, счётчик qcount, флаг closed, очереди ожидающих sendq и recvq (списки sudog) и сам lock. Оптимизация, которую стоит знать: если в момент отправки в recvq уже стоит получатель, значение копируется напрямую из стека отправителя в стек получателя (sendsendDirect), минуя буфер — это и быстрее, и объясняет, почему небуферизованный канал даёт синхронную передачу. Отправка в полный канал или чтение из пустого паркует горутину, а не крутит спин. Гарантии модели памяти Go, на которые опираются программы: отправка в канал происходит-до завершения соответствующего получения; закрытие канала происходит-до получения нулевого значения из-за закрытия; для небуферизованного канала получение происходит-до завершения отправки. Отдельно: close на закрытом канале и отправка в закрытый — паника, чтение из закрытого возвращает нулевое значение и ok == false, а nil-канал блокируется навсегда (что используют, чтобы отключить ветку в select).

Коротко. ps aux (BSD-синтаксис) или ps -ef (UNIX-синтаксис) — полный список; ps -eLf или ps -eo pid,tid,comm -L — с потоками; pstree -p — дерево; интерактивно — top или htop. Поиск по имени — pgrep -af <шаблон>.

Глубже. Самое полезное на практике — ps -eo с явным набором колонок: ps -eo pid,ppid,stat,pcpu,pmem,rss,etime,wchan:20,args --sort=-pcpu | head -20. Топ по памяти: --sort=-rss. Первоисточник для всех этих утилит — /proc: каталог на каждый PID, /proc/<PID>/status, /proc/<PID>/stat, /proc/<PID>/cmdline (аргументы разделены нулевыми байтами), /proc/<PID>/environ, /proc/<PID>/task/ со списком потоков. Полезные соседи: pidof, fuser (кто держит файл), ss -tlnp (кто слушает порт), systemctl status (процессы юнита), ps -o pid,cgroup для привязки к контейнеру.

Как посмотреть потребление каких-нибудь ресуров?

Заголовок раздела «Как посмотреть потребление каких-нибудь ресуров?»

Коротко. CPU и память — top/htop, ps aux --sort=-pcpu, vmstat 1, mpstat -P ALL 1; память подробно — free -h, /proc/meminfo, smem; диск — iostat -x 1, iotop, df -h, du -sh; сеть — ss -s, iftop, nload, sar -n DEV 1; всё сразу по процессу — pidstat -p <PID> 1.

Глубже. Для одного процесса самое информативное — /proc/<PID>/status (VmRSS, VmSize, Threads, счётчики переключений контекста) и /proc/<PID>/io (read_bytes, write_bytes — реальный I/O до/после page cache). В контейнерах смотреть надо не на общесистемные цифры, а на cgroup: /sys/fs/cgroup/memory.current, memory.max, memory.events (там oom_kill), cpu.stat (throttled_usec), io.stat. Для профилирования под нагрузкой — perf top -p <PID>, perf record -g, flame graph. Для Go-сервиса первым делом включают net/http/pprof и снимают /debug/pprof/heap, /debug/pprof/profile?seconds=30, /debug/pprof/goroutine?debug=2, а метрики рантайма (runtime/metrics) отдают в Prometheus. Для сводной картины давления на ресурсы — PSI в /proc/pressure/{cpu,io,memory}.

Коротко. Сервис под systemd — systemctl stop <unit> (или systemctl kill -s SIGKILL <unit>, если завис): systemd сам пошлёт SIGTERM главному процессу, подождёт TimeoutStopSec и добьёт SIGKILL всей cgroup юнита. Сама команда kill не «убивает», а посылает процессу сигнал — по умолчанию SIGTERM (15).

Глубже. kill — это тонкая обёртка над системным вызовом kill(pid, sig). Формы аргумента: положительный PID — конкретному процессу; 0 — всей своей группе процессов; отрицательное число — группе с этим PGID; -1 — всем процессам, которым вы имеете право слать сигналы (крайне опасно от root). Права: слать сигнал можно, если ваш реальный или эффективный UID совпадает с UID цели, либо у вас CAP_KILL. Важное отличие systemctl stop от ручного kill: если у юнита Restart=always, убитый вручную процесс тут же поднимется заново, а ещё systemd убивает всю cgroup, включая потомков, которые при ручном kill осиротеют и продолжат работать. Более современный интерфейс без гонок по переиспользованию PID — pidfd_open + pidfd_send_signal.

Чем отличаются сигналы, какие можно перехватить?

Заголовок раздела «Чем отличаются сигналы, какие можно перехватить?»

Коротко. Перехватить (установить обработчик), заблокировать или проигнорировать можно любой сигнал, кроме двух: SIGKILL (9) и SIGSTOP (19) — их обрабатывает ядро, и обойти это нельзя. Остальные различаются номером, действием по умолчанию (terminate, core dump, ignore, stop, continue) и семантикой.

Глубже. Рабочий минимум наизусть: SIGHUP (1) — обрыв терминала, а по традиции — «перечитай конфиг»; SIGINT (2) — Ctrl+C; SIGQUIT (3) — Ctrl+, по умолчанию core dump, а в Go-программе печатает стеки всех горутин и завершается; SIGKILL (9) — не перехватывается; SIGSEGV (11) — обращение к неотображённой памяти; SIGPIPE (13) — запись в закрытый канал/сокет; SIGTERM (15) — вежливое завершение; SIGSTOP (19)/SIGCONT (18) — приостановка и продолжение; SIGTSTP (20) — Ctrl+Z, в отличие от SIGSTOP перехватывается; SIGCHLD (17) — завершился потомок; SIGUSR1/SIGUSR2 (10, 12) — свободны для приложения. Сигналы 1–31 не очередятся (повторные до доставки схлопываются), реалтаймовые 32–64 очередятся и сохраняют порядок. В Go signal.Notify умеет ловить всё, кроме SIGKILL и SIGSTOP; синхронные сигналы от ошибок программы (SIGSEGV, SIGBUS, SIGFPE) рантайм обрабатывает сам и превращает в panic/фатальную ошибку — перехватывать их через Notify бессмысленно. Помните также, что SIGURG в Go занят под вытеснение, а SIGPIPE рантайм гасит для сетевых дескрипторов (ошибка приходит как EPIPE из Write).

Когда процесс может не реагировать на SIGKILL?

Заголовок раздела «Когда процесс может не реагировать на SIGKILL?»

Коротко. Когда он находится в состоянии D (TASK_UNINTERRUPTIBLE) — сидит в ядре в ожидании завершения I/O и просто не может быть прерван; сигнал останется отложенным и сработает, когда операция завершится. Ещё два формальных случая: процесс уже зомби (Z) — убивать нечего, и это поток ядра, который сигналами не управляется.

Глубже. Практические источники D: зависший NFS-сервер (классика), сбойный диск с бесконечными ретраями, драйвер устройства, залипший в ядре, fsync на переполненном или тормозящем сторадже, ожидание страницы под mmap с отвалившимся бэкендом, зависший FUSE-демон. Диагностика: cat /proc/<PID>/stack и /proc/<PID>/wchan покажут, где именно застряли, dmesg часто содержит INFO: task ... blocked for more than 120 seconds (сработал hung_task_timeout_secs). Средство лечения — устранить причину: поднять NFS/FUSE, размонтировать с umount -f/-l, восстановить устройство; если причина не устраняется — перезагрузка. Полумера для NFS — монтировать с intr/soft или пользоваться тем, что часть ожиданий сделана TASK_KILLABLE и всё-таки пробивается SIGKILL. Отдельный случай, который часто путают с «не реагирует на SIGKILL»: PID 1 в контейнере игнорирует SIGTERM (нет обработчика — ядро не доставляет сигнал с действием по умолчанию), но SIGKILL его убивает нормально.

Коротко. Зомби — процесс, который завершился, но его статус ещё не забран родителем через wait(); он существует только как запись в таблице процессов. Сирота (orphan) — противоположная ситуация: процесс жив, а его родитель умер; ядро немедленно переусыновляет такого потомка init (PID 1) или ближайшему предку с флагом subreaper.

Глубже. Механика переусыновления: при выходе процесса ядро проходит по его детям и меняет им PPID — увидеть это можно, запустив потомка, убив родителя и посмотрев ps -o ppid= -p <child> (станет 1 или PID subreaper). Флаг subreaper ставится через prctl(PR_SET_CHILD_SUBREAPER, 1) и используется systemd (systemd --user), контейнерными рантаймами и супервизорами, чтобы сироты доставались им, а не хостовому init. Практическая связка двух понятий: сирота — это способ избавиться от будущих зомби, потому что init всегда честно реапит; и наоборот, зомби в контейнере накапливаются именно потому, что роль init там играет приложение, которое wait() не делает. Ещё один родственный термин — daemon: намеренно осиротевший процесс, отвязанный от терминала (см. вопрос про двойной fork).

Зачем может понадобиться вызвать fork два раза подряд?

Заголовок раздела «Зачем может понадобиться вызвать fork два раза подряд?»

Коротко. Классическая демонизация. Первый fork позволяет родителю сразу выйти, чтобы процесс стал сиротой и был переусыновлён init; затем setsid() создаёт новую сессию и отвязывает от управляющего терминала; второй fork нужен, чтобы процесс перестал быть лидером сессии и уже никогда не смог случайно захватить управляющий терминал. Побочный, но полезный эффект: внук переусыновляется init, поэтому зомби не остаётся.

Глубже. Полная последовательность демонизации в UNIX: fork → родитель _exitsetsid → второй fork → промежуточный _exitchdir("/") (чтобы не держать точку монтирования) → umask(0) → закрыть/перенаправить stdin/stdout/stderr в /dev/null → при необходимости записать pid-файл. Второй сценарий двойного fork, не связанный с демонами: вы хотите запустить долгоживущий процесс и не хотите вызывать wait() — делаете fork, в ребёнке ещё раз fork и сразу выходите из промежуточного, а родитель делает waitpid на промежуточном (он завершается мгновенно); внук достаётся init, который его и реапнет. Практическое замечание: сегодня демонизировать вручную почти никогда не нужно — systemd запускает сервисы в foreground (Type=simple/notify), сам управляет cgroup, логами и перезапуском, и «правильный» демон 12-факторного приложения просто пишет в stdout и не форкается. В Go написать классический двойной fork напрямую нельзя из-за многопоточного рантайма — используют перезапуск себя через os/exec с Setsid: true в syscall.SysProcAttr.

Как дебажить зависшую программу (в топе висит, на вызовы не реагирует, CPU потребляет)?

Заголовок раздела «Как дебажить зависшую программу (в топе висит, на вызовы не реагирует, CPU потребляет)?»

Коротко. Сначала классифицировать зависание: жрёт CPU — значит крутится в цикле или в GC/спинлоках; не жрёт — значит дедлок или ожидание I/O. Инструменты по порядку: top -H -p <PID> (какой поток горит), strace -p <PID> (какие syscall, если тишина — не в ядре), perf top -p <PID> (какая функция жжёт CPU), а для Go-программы — kill -QUIT <PID> для дампа всех горутин или pprof по живому процессу.

Глубже. Развёрнутый порядок действий для Go-сервиса. (1) curl localhost:6060/debug/pprof/goroutine?debug=2 — полные стеки всех горутин: сразу видно, стоят ли тысячи на одном мьютексе или канале. Если HTTP-эндпоинт не отвечает (весь рантайм застрял), берём kill -QUIT — рантайм печатает то же самое в stderr и падает; GOTRACEBACK=all или GOTRACEBACK=crash расширяют вывод и дают core dump. (2) CPU-профиль: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30, смотрим top и flame graph — тесный цикл виден мгновенно. (3) perf top -p <PID> работает даже без pprof и покажет, если время уходит в GC (runtime.scanobject), в спинлоки (runtime.lock2, sync.(*Mutex).lockSlow) или в чужую библиотеку/cgo. (4) strace -f -p <PID> -c покажет, залипли ли в syscall (например, бесконечный futex или read); пустой вывод при 100% CPU — верный признак пользовательского цикла. (5) Отладчик: dlv attach <PID>, команда goroutines -t и stack — можно посмотреть переменные. (6) Проверить состояние потоков ps -L -o pid,tid,stat,pcpu -p <PID> и переключения контекста в /proc/<PID>/status. Типичные находки: дедлок на двух мьютексах, забытый sync.WaitGroup.Wait, чтение из канала, в который никто не пишет, бесконечный for без точки вытеснения в cgo-коде, деградация GC при массовых аллокациях, regexp с катастрофическим бэктрекингом (в Go RE2 этого не делает, но в cgo-библиотеках бывает).

Вам приходит задача. Что бы вы сделали перед ее выполнением и в процессе?

Заголовок раздела «Вам приходит задача. Что бы вы сделали перед ее выполнением и в процессе?»

Коротко. Поведенческий вопрос про инженерную дисциплину. Ответ строится по трём фазам: понять задачу до начала кода, спланировать и договориться, и держать обратную связь в процессе.

Глубже. Каркас. Перед началом: прочитать постановку и переформулировать её своими словами; уточнить у автора цель («какую проблему пользователя решаем») и критерии приёмки; проверить, нет ли уже готового решения в коде; оценить влияние на смежные сервисы и на данные (миграции, обратная совместимость API, размер таблиц); прикинуть нефункциональные требования — нагрузка, таймауты, идемпотентность, что делать при ошибках; оценить трудоёмкость и, если она заметно больше ожиданий заказчика, сказать об этом сразу; при необходимости декомпозировать на подзадачи и договориться, что попадёт в первый инкремент. В процессе: делать небольшими шагами с ранним PR или черновиком дизайна; писать тесты вместе с кодом; сообщать о найденных проблемах и изменении оценки как можно раньше, а не в день дедлайна; не расширять скоуп молча — обнаруженный технический долг оформлять отдельной задачей; добавить наблюдаемость (логи, метрики, алерты) как часть работы; согласовать способ выката (фича-флаг, поэтапный роллаут) и план отката. Ошибки в ответах: «сразу начинаю писать код»; отсутствие любого упоминания коммуникации; обещание «уточнить всё до последней детали», что читается как паралич.

Как была устроена команда и процесс разработки?

Заголовок раздела «Как была устроена команда и процесс разработки?»

Коротко. Вопрос про личный опыт, близкий к предыдущим о рабочем процессе. Отвечать двумя блоками: состав и роли (сколько людей, кто чем занимается, кому вы подчинялись, кто принимает продуктовые и технические решения) и процесс (методология, длина итерации, путь задачи, релизный цикл, дежурства).

Глубже. Хорошая структура: (1) продукт и его масштаб в цифрах — «сервис на 3 тысячи RPS, 15 микросервисов, 5 команд»; (2) ваша команда — «6 человек: 3 бэкенда на Go, фронтендер, QA, тимлид; продакт и дизайнер шарились между двумя командами»; (3) зона ответственности команды — какие сервисы вы владели, был ли on-call; (4) процесс — от постановки до прода, с ревью, CI, стейджингом и релизом; (5) как принимались технические решения — RFC/дизайн-доки, архитектурный комитет или «кто делает, тот и решает». Хорошо добавить, что менялось за время вашей работы и что вы сами инициировали (ввели шаблон PR, наладили алерты, ускорили CI). Ошибки: ответ в две фразы; невозможность назвать размер команды и свою роль; критика коллег.

Как числа с плавающей запятой представлены в процессоре?

Заголовок раздела «Как числа с плавающей запятой представлены в процессоре?»

Коротко. По стандарту IEEE 754: число хранится как знак, смещённый порядок и мантисса. float32 (binary32) — 1 + 8 + 23 бита, смещение порядка 127; float64 (binary64) — 1 + 11 + 52 бита, смещение 1023. Значение нормализованного числа равно (-1)^s × 1.mantissa × 2^(exp - bias): у мантиссы есть подразумеваемая ведущая единица, которая не хранится.

Глубже. Особые значения кодируются крайними значениями порядка: все нули в порядке — это ноль (со знаком: +0 и -0) и денормализованные числа (без ведущей единицы, дают плавное снижение точности у нуля, но считаются медленно); все единицы — бесконечности (нулевая мантисса) и NaN (ненулевая, есть quiet и signaling). Отсюда практические следствия: NaN != NaN — единственное значение, не равное самому себе (в Go это ломает использование NaN как ключа map и делает math.IsNaN обязательным); 0.1 + 0.2 != 0.3, потому что 0.1 в двоичной системе — бесконечная периодическая дробь; сравнивать float надо с допуском, а деньги хранить в целых копейках или в decimal. Точность: float64 даёт около 15–17 значащих десятичных цифр и представляет все целые до 2^53 точно — поэтому JSON-числа больше 2^53 теряют точность. Округление по умолчанию — к ближайшему с округлением к чётному. Аппаратно на x86-64 всё это исполняется на SSE2 (addsd, mulsd), а не на легаси-стеке x87 с его 80-битными регистрами, что делает результаты воспроизводимыми между платформами. В Go нетипизированные константы вычисляются с произвольной точностью на этапе компиляции, а спецификация разрешает объединять умножение и сложение в FMA — если нужно принудительное округление промежуточного результата, применяют явное преобразование float64(x*y) + z.

Коротко. См. выше — вопрос повторяется в задании многократно. Процесс владеет изолированным адресным пространством и набором ресурсов; поток — исполняющаяся сущность внутри процесса, разделяющая с соседями память и дескрипторы и имеющая свои стек, регистры и TID. Планировщик выделяет время потокам, обмен между процессами требует IPC, а падение любого потока валит весь процесс.

  • Говорят «поток — это лёгкий процесс», но не могут назвать, что именно разделяется: адресное пространство, дескрипторы, обработчики сигналов; и не знают, что в Linux и то и другое создаётся clone() с разными флагами.
  • Утверждают, что процессорное время выделяется процессу. Планировщик оперирует потоками (задачами), поэтому один процесс легко показывает 146% CPU.
  • Считают зомби-процесс «утечкой памяти». Память у него уже освобождена; занимает он слот PID и несколько килобайт ядерных структур — и это всё равно проблема, но по другой причине.
  • Пытаются убить зомби через kill -9 и удивляются, что не помогает. Зомби убирается только wait() родителя или смертью родителя.
  • Путают steal time с throttling в cgroup: первый показывает, что время отобрал гипервизор, второй — что вы упёрлись в собственный лимит cpu.max, и виден он в cpu.stat, а не в %st.
  • Читают load average как «загрузку CPU в условных единицах» и не знают, что в Linux туда входят задачи в состоянии D, то есть высокий LA может означать проблему с диском при простаивающем процессоре.
  • Говорят, что SIGKILL убивает всегда и мгновенно, не зная про состояние D и про особый статус PID 1 при доставке сигналов.
  • Пишут «graceful shutdown», но забывают, что в Kubernetes grace period по умолчанию 30 секунд, а после него прилетит SIGKILL — и долгие фоновые задачи всё равно оборвутся.
  • Объясняют горутины как «те же потоки, только в Go»: теряется главное — планирование в user space, стек с 2 КБ и netpoller, из-за которого блокирующий по виду вызов не занимает поток ОС.
  • Считают каналы lock-free. Внутри hchan обычный мьютекс рантайма; ценность каналов не в скорости, а в семантике и гарантиях модели памяти.
  • Не различают переключение режима (syscall) и переключение контекста (смену задачи), из-за чего называют стоимость системного вызова в микросекундах, а переключения контекста — в наносекундах.
  • Считают fork дорогим из-за копирования памяти. Память копируется лениво (COW), дорого копируются таблицы страниц — что и делает fork заметным для процессов с большим RSS.

Список исходных вопросов с привязкой к компаниям: ../questions/processes.md