Общие вопросы по ОС и Linux
Кратко о теме
Заголовок раздела «Кратко о теме»Операционная система нужна ровно для двух вещей: спрятать разнообразие железа за переносимыми абстракциями (процесс вместо «процессор + память», файл вместо «блочное устройство», сокет вместо «сетевая карта») и мультиплексировать это железо между взаимно недоверяющими программами. Обе задачи требуют, чтобы у ядра была власть, которой нет у приложения, поэтому процессор физически разделён на уровни привилегий (на x86-64 это кольца, реально используются ring 0 для ядра и ring 3 для пользовательских программ). Отсюда вырастает вся остальная картина: kernel space и user space, MMU и раздельные адресные пространства, а также единственная легальная дверь между мирами — системный вызов.
Системный вызов на x86-64 — это инструкция syscall, которая переводит CPU в ring 0 и передаёт управление по адресу из MSR LSTAR в общую точку входа ядра. Номер вызова лежит в RAX, аргументы — в RDI, RSI, RDX, R10, R8, R9, результат возвращается в RAX (отрицательное значение в диапазоне -1..-4095 — это код ошибки, из которого libc делает errno). Переход недёшев: сохранение регистров, смена стека, а после митигаций Meltdown/Spectre (KPTI) ещё и смена таблиц страниц с промахами TLB. Именно поэтому вокруг syscall выросла целая индустрия оптимизаций — vDSO для «дешёвых» вызовов вроде clock_gettime, мультиплексоры готовности (epoll), батчинг (writev, sendmmsg), а в новых ядрах — io_uring с кольцевыми буферами без переходов в ядро на каждую операцию.
Второй крупный сюжет — изоляция. Linux не даёт одного большого рубильника «сделай контейнер»: он даёт независимые механизмы, из которых контейнер собирается. Namespaces режут видимость глобальных ресурсов (pid, mnt, net, ipc, uts, user, cgroup, time), cgroups ограничивают потребление (CPU, память, IO, pids), capabilities дробят всемогущество root на десятки отдельных прав, seccomp-bpf сужает набор доступных системных вызовов, LSM (SELinux/AppArmor) накладывает мандатные политики, а pivot_root/overlayfs дают контейнеру собственный корень. Docker и Kubernetes — это не отдельная технология изоляции, а сборка перечисленного.
Для Go-собеседования критично держать в голове стык рантайма и ядра. Ядро ничего не знает о горутинах — оно видит только потоки (в Linux — задачи, созданные clone). Планировщик Go реализует модель M:N: горутины (G) раскладываются рантаймом по логическим процессорам (P, их GOMAXPROCS штук), а те исполняются на потоках ОС (M). Переключение горутины — это смена нескольких регистров в user space, без входа в ядро, поэтому оно на порядки дешевле переключения потока. Блокирующий системный вызов ломает эту экономику: поток встаёт в ядре целиком. Поэтому весь сетевой и файловый ввод-вывод Go по возможности заворачивает в netpoller поверх epoll, а редкие честно блокирующие вызовы обрабатывает через entersyscall/exitsyscall, отдавая P другому потоку.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Linux access permissions. File system
Заголовок раздела «Linux access permissions. File system»Коротко. Права в Linux привязаны к inode и хранятся как три тройки rwx для владельца, группы и всех остальных плюс три спецбита (setuid, setgid, sticky) — итого четыре восьмеричные цифры, например 4755. Для файла r/w/x — это чтение, запись и исполнение; для каталога r — прочитать список имён, w — создавать и удалять записи в нём, x — «пройти сквозь» каталог при разрешении пути.
Глубже. Ключевая деталь, на которой заваливаются: право удалить файл определяется правами на каталог, а не на файл — rm вызывает unlink() над записью в каталоге. Чтобы в общих каталогах вроде /tmp пользователи не удаляли чужое, на них ставят sticky bit (chmod 1777), тогда удалять запись может только владелец файла, владелец каталога или root. Setuid на исполняемом файле заставляет процесс запуститься с эффективным UID владельца файла (/usr/bin/passwd), setgid на каталоге заставляет создаваемые внутри файлы наследовать группу каталога. На новых файлах права вычисляются как mode & ~umask. Сверх классической модели есть POSIX ACL (getfacl/setfacl) для точечных прав отдельным пользователям и capabilities (getcap/setcap), которые позволяют дать бинарнику одно конкретное право вроде CAP_NET_BIND_SERVICE вместо полного setuid-root.
Файловая система со стороны ядра — это VFS, общий слой с объектами superblock (смонтированная ФС), inode (метаданные файла: тип, права, uid/gid, времена, размер, указатели на блоки), dentry (кэш соответствия имя → inode) и file (открытый дескриптор с позицией и флагами). Имя файла не хранится в inode: оно живёт в каталоге, поэтому у одного inode может быть несколько имён (см. вопрос про жёсткие ссылки). Конкретные реализации — ext4 (журнал, экстенты), XFS (дефолт в RHEL, хорош на больших файлах и параллельной записи), Btrfs/ZFS (CoW, снапшоты, контрольные суммы), а также псевдо-ФС без диска: procfs, sysfs, tmpfs, cgroupfs, overlayfs. Всё это собирается в единое дерево через точки монтирования; в контейнерах дерево у каждого своё за счёт mount namespace.
Какой командой можно проверить потребляемые системой ресурсы на Unix?
Заголовок раздела «Какой командой можно проверить потребляемые системой ресурсы на Unix?»Коротко. Универсальный ответ — top (или htop): CPU, память, load average и топ процессов в одном экране. Дальше по подсистемам: free -h и /proc/meminfo — память, vmstat 1 и mpstat — CPU и подкачка, iostat -x 1 — диски, df -h/du -sh — место на ФС, ss -s/ss -tulpn — сокеты, pidstat/sar — статистика по процессам и история.
Глубже. Стоит проговорить, что цифры «сверху» бывают обманчивы. Load average в Linux считает не только процессы на CPU, но и находящиеся в uninterruptible sleep (состояние D, обычно ждущие диск), поэтому высокий LA при низком %us — почти всегда IO. В free важна колонка available, а не free: страничный кэш занимает всю свободную память и мгновенно отдаётся под нужды процессов. %iowait — это доля времени, когда CPU простаивал при наличии незавершённого IO, а не «загрузка диска»; для дисков смотрите %util и await в iostat -x. Внутри контейнера классические утилиты по умолчанию читают хостовые /proc/meminfo и /proc/cpuinfo, поэтому реальные лимиты надо смотреть в cgroup v2: /sys/fs/cgroup/memory.max, memory.current, cpu.max, cpu.stat (там же throttled_usec — главный индикатор CPU-троттлинга). Для Go-процесса дополнительно полезны runtime/metrics, GODEBUG=gctrace=1 и pprof — они покажут, что именно внутри процесса ест CPU и память.
Как глубоко работал с linux и знаешь ли внутреннее устройство?
Заголовок раздела «Как глубоко работал с linux и знаешь ли внутреннее устройство?»Коротко. Это вопрос про калибровку: интервьюер хочет понять, на каком уровне с вами дальше разговаривать, и проверить, что вы не преувеличиваете. Отвечайте честной шкалой «уверенно умею X — разбирался в Y — читал про Z, но руками не делал» и сразу дайте один конкретный пример отладки.
Глубже. Хороший каркас ответа: (1) уровень эксплуатации — сборка и деплой сервисов, systemd-юниты, права, сеть, лимиты, разбор инцидентов; (2) уровень диагностики — умею снять профиль (perf top, pprof), посмотреть системные вызовы (strace -f -c, bpftrace), понять, во что упёрлись (CPU, IO, лок, GC, лимит cgroup); (3) уровень внутреннего устройства — понимаю, что происходит при системном вызове, как устроены namespaces/cgroups, как работает page cache и OOM killer, как netpoller Go ложится на epoll. Дальше обязательно история: «был рост p99 у сервиса; iostat показал await под сотню миллисекунд, strace -c — что время уходит в fsync; оказалось, что логгер писал синхронно, вынесли в буферизованную запись». Типичные ошибки: заявить «знаю ядро», а потом не суметь объяснить разницу между процессом и потоком в Linux; или наоборот — из скромности сказать «я обычный прикладник» и лишить себя половины разговора. Не приписывайте себе чтение исходников ядра, если это неправда: следующий вопрос будет по ним.
Что такое namespace в linux и для чего нужно?
Заголовок раздела «Что такое namespace в linux и для чего нужно?»Коротко. Namespace — это механизм ядра, который делает глобальный ресурс ОС «локальным» для группы процессов: у них своя таблица PID, своё дерево монтирований, свой сетевой стек и так далее. Нужен, чтобы изолировать процессы друг от друга без запуска отдельной ОС; это фундамент контейнеров.
Глубже. Виды пространств имён: pid (свои номера процессов, PID 1 внутри), mnt (своё дерево монтирований), net (свои интерфейсы, таблицы маршрутизации, iptables, порты), ipc (System V IPC и POSIX-очереди), uts (hostname и domainname), user (своё отображение UID/GID — можно быть root внутри, будучи непривилегированным снаружи; именно это делает возможными rootless-контейнеры), cgroup (свой корень иерархии cgroup) и time (с ядра 5.6, сдвиг CLOCK_MONOTONIC/CLOCK_BOOTTIME). Управляются тремя системными вызовами: clone() с флагами CLONE_NEW* создаёт процесс уже в новых пространствах, unshare() переводит текущий процесс, setns() присоединяет процесс к существующему (так работает docker exec и nsenter). Namespace живёт, пока на него есть ссылка: процесс, bind-mount файла из /proc/<pid>/ns/ или открытый fd — поэтому «пустой» network namespace можно держать без процессов. Смотреть можно через lsns и ls -l /proc/<pid>/ns/. Важно проговорить границу: namespaces изолируют видимость, но не потребление — за квоты отвечают cgroups, а ядро всё равно одно на всех, поэтому это не барьер безопасности уровня виртуальной машины.
Допустим, программа запускает множество горутин. Операционная система ничего не знает о горутинах. Как в таком случае они должны выполняться?
Заголовок раздела «Допустим, программа запускает множество горутин. Операционная система ничего не знает о горутинах. Как в таком случае они должны выполняться?»Коротко. Через планировщик пользовательского уровня по модели M:N: рантайм Go мультиплексирует много горутин на небольшое число потоков ОС. Горутина (G) кладётся в очередь логического процессора (P, число задаётся GOMAXPROCS), а поток ОС (M) забирает P и последовательно исполняет его горутины, переключаясь между ними в user space.
Глубже. Переключение горутины — это сохранение нескольких регистров (SP, PC и пары служебных) в структуру g и загрузка их из другой g; в ядро при этом заходить не нужно, поэтому цена — сотни наносекунд против единиц микросекунд у переключения потоков. Начальный стек горутины — 2 КБ, он растёт копированием при переполнении, поэтому миллион горутин физически влезает в память, а миллион потоков — нет. Точки переключения: операции с каналами, мьютексы, сетевой ввод-вывод, time.Sleep, вызовы рантайма и вставленные компилятором проверки в прологах функций; с Go 1.14 добавлено асинхронное вытеснение — sysmon посылает потоку сигнал SIGURG, и обработчик паркует горутину, даже если та крутит цикл без вызовов функций. Когда горутина уходит в блокирующий системный вызов, поток блокируется вместе с ней, но рантайм отвязывает от него P (handoff) и отдаёт другому потоку, чтобы остальные горутины продолжали работать. Если работы у своего P нет, поток крадёт половину очереди у случайного другого P (work stealing) или берёт из глобальной очереди. Отдельно живёт netpoller: сетевые дескрипторы переводятся в неблокирующий режим и регистрируются в epoll, поэтому «блокирующий» с точки зрения кода conn.Read на самом деле паркует только горутину, а поток остаётся свободен.
В чем разница между символической ссылкой и жесткой ссылкой в Linux?
Заголовок раздела «В чем разница между символической ссылкой и жесткой ссылкой в Linux?»Коротко. Жёсткая ссылка (ln a b) — это просто ещё одно имя того же inode в каталоге; у файла увеличивается счётчик ссылок, и все имена равноправны. Символическая ссылка (ln -s a b) — отдельный файл особого типа, содержимое которого — строка с путём; она может указывать в никуда, на каталог и на другую файловую систему.
Глубже. Практические следствия. Жёсткие ссылки нельзя создать на каталог (кроме . и .., которые делает само ядро — иначе в дереве появились бы циклы) и нельзя перекинуть через границу файловой системы, потому что номер inode уникален только в пределах одной ФС. Данные освобождаются, когда счётчик ссылок (stat, поле Links) падает до нуля и файл никем не открыт — отсюда классический трюк «удалили лог, а место не вернулось»: процесс держит дескриптор. Симлинк ломается при переименовании цели (получается dangling link), может быть относительным или абсолютным, и вносит разницу между stat() (идёт по ссылке) и lstat() (описывает саму ссылку); прочитать цель — readlink. В Go это os.Link против os.Symlink, os.Stat против os.Lstat, os.Readlink, а filepath.EvalSymlinks разворачивает цепочку. Симлинки — источник уязвимостей вида symlink race (TOCTOU), поэтому в привилегированном коде открывают с O_NOFOLLOW или через openat2 с RESOLVE_NO_SYMLINKS.
Как в Linux происходит разрешение доменных имен?
Заголовок раздела «Как в Linux происходит разрешение доменных имен?»Коротко. Приложение вызывает getaddrinfo(), та идёт через подсистему NSS и порядок источников из /etc/nsswitch.conf — обычно сначала files (/etc/hosts), затем dns (рекурсивные серверы из /etc/resolv.conf), иногда myhostname/mdns. На современных дистрибутивах в resolv.conf часто стоит заглушка 127.0.0.53 — локальный кэширующий stub-резолвер systemd-resolved.
Глубже. В /etc/resolv.conf важны не только nameserver, но и search со списком доменов дописывания и опция ndots (по умолчанию 1): если в имени точек меньше ndots, резолвер сначала пробует имя с каждым суффиксом из search и только потом — как абсолютное. Именно это создаёт знаменитый эффект в Kubernetes, где ndots:5, и запрос к api.example.com порождает четыре лишних NXDOMAIN до попадания в цель; лечится точкой в конце имени (FQDN) или собственным dnsConfig. Go на Linux имеет два резолвера: чистый Go-резолвер, который сам читает /etc/hosts и /etc/resolv.conf и шлёт UDP/TCP-запросы (позволяет параллелить и не блокировать поток), и cgo-резолвер через getaddrinfo. Рантайм выбирает автоматически и уходит в cgo, когда конфигурация нестандартная (необычный nsswitch.conf, LDAP/NIS-источники, переменная LOCALDOMAIN и т. п.); принудительно переключается через GODEBUG=netdns=go или netdns=cgo, а GODEBUG=netdns=1 печатает решение. Отдельно помните, что Go-резолвер не кэширует ответы — TTL соблюдает нижестоящий кэш (systemd-resolved, dnsmasq, CoreDNS), а не процесс. Диагностика: dig/drill (обращается напрямую к DNS, минуя NSS), getent hosts foo (идёт полным путём NSS — разница в их ответах сразу показывает, что дело в /etc/hosts или nsswitch), resolvectl status.
Какие технологии изоляции предоставляет Linux?
Заголовок раздела «Какие технологии изоляции предоставляет Linux?»Коротко. Namespaces (изоляция видимости), cgroups v2 (лимиты по CPU, памяти, IO, числу процессов), chroot/pivot_root и overlayfs (свой корень ФС), capabilities (дробление прав root), seccomp-bpf (фильтр разрешённых системных вызовов), LSM — SELinux/AppArmor (мандатный контроль доступа). Контейнер — это комбинация всего перечисленного, а не отдельная сущность ядра.
Глубже. Полезно разложить по осям: что вижу — namespaces; сколько могу съесть — cgroups; что мне разрешено делать — capabilities, seccomp, LSM, no_new_privs. Стоит отдельно назвать user namespace как единственный механизм, дающий непривилегированному пользователю право создавать остальные пространства имён — на нём стоят rootless Podman и user-namespace-режим в Kubernetes. Важно честно сказать про предел: ядро общее, поэтому уязвимость в системном вызове или драйвере ломает изоляцию всех контейнеров на узле. Когда нужен более прочный барьер, добавляют второй уровень — виртуализацию (KVM, Firecracker, Kata Containers) или перехват системных вызовов в user space (gVisor). Из смежного стоит упомянуть nsjail/bubblewrap как готовые песочницы и landlock (с ядра 5.13) — LSM, который непривилегированный процесс может применить сам к себе.
Что содержит каталог /proc в Linux?
Заголовок раздела «Что содержит каталог /proc в Linux?»Коротко. /proc — виртуальная файловая система (procfs), которую ядро генерирует на лету: она не занимает места на диске, а её «файлы» — это интерфейс к структурам ядра. Там лежат каталоги по PID с информацией о каждом процессе и общесистемные файлы вроде meminfo, cpuinfo, loadavg, stat, mounts, net/, плюс /proc/sys — точки настройки sysctl.
Глубже. Внутри /proc/<pid>/ самое востребованное: status (человекочитаемое состояние, UID/GID, RSS, число потоков, capabilities), stat (то же в машинном виде — из него top считает CPU), cmdline и environ (аргументы и окружение, разделены \0), maps/smaps (карта виртуальной памяти с реальным потреблением по регионам), fd/ (симлинки на открытые дескрипторы — так находят «удалённый, но держащийся» файл), limits (ulimit), ns/ (ссылки на пространства имён), task/ (потоки), cgroup, oom_score. Общесистемное: /proc/meminfo, /proc/vmstat, /proc/interrupts, /proc/net/tcp (сырые данные для ss/netstat), /proc/self — ссылка на собственный процесс, /proc/sys/net/ipv4/... и прочие — запись в них равнозначна sysctl -w. Historically часть системной информации переехала в sysfs (/sys — устройства, драйверы, cgroup v2 в /sys/fs/cgroup), а /proc формально должен оставаться про процессы. В контейнере /proc монтируется заново в своём pid namespace, поэтому внутри виден только свой набор процессов; но глобальные файлы вроде meminfo без lxcfs всё равно показывают хостовые значения.
Кто управляет системными вызовами? Сколько времени работает горутина?
Заголовок раздела «Кто управляет системными вызовами? Сколько времени работает горутина?»Коротко. Системными вызовами управляет ядро: пользовательский код только выставляет номер вызова и аргументы в регистрах и исполняет инструкцию syscall; дальше всё делает ядро — точка входа, проверка прав, диспетчеризация по sys_call_table. Горутина не имеет фиксированного «времени жизни»: она выполняется, пока не заблокируется или пока планировщик её не вытеснит; ориентир кванта — около 10 мс, после чего sysmon помечает её как подлежащую вытеснению.
Глубже. Про первую половину: диспетчер вызова — общая точка входа (entry_SYSCALL_64), которая переключает стек, сохраняет регистры в pt_regs, прогоняет процесс через фильтры (seccomp, ptrace, audit) и вызывает функцию по номеру из RAX; сам номер зависит от архитектуры и ABI, поэтому таблицы у x86-64, arm64 и 32-битного режима разные. Про вторую: sysmon — служебный поток рантайма, который работает без P и раз в 20 мкс…10 мс просыпается; он забирает P у потока, застрявшего в системном вызове дольше ~20 мкс, вытесняет горутины, крутящиеся дольше 10 мс (forcePreemptNS), и запускает GC при простое. С Go 1.14 вытеснение асинхронное: раньше горутина без вызовов функций (например, for {}) не имела точек проверки и могла подвесить сборку мусора, теперь sysmon шлёт SIGURG потоку, и обработчик сигнала паркует горутину в безопасной точке. Важная оговорка: 10 мс — не гарантия и не квант в смысле ядра, а порог, после которого рантайм начинает пытаться вытеснить.
В чем минус Syscall?
Заголовок раздела «В чем минус Syscall?»Коротко. Он дорогой относительно обычного вызова функции: переход между кольцами, сохранение и восстановление контекста, вход в ядро, обратный переход — сотни наносекунд даже в лучшем случае, а после митигаций Spectre/Meltdown (KPTI, retpoline) заметно больше из-за смены таблиц страниц и промахов TLB и кэшей. Плюс он часто блокирующий, а значит выключает поток ОС целиком.
Глубже. Для Go к общей цене добавляется рантаймовая: обёртка вокруг syscall дёргает runtime.entersyscall/exitsyscall, чтобы планировщик знал о выходе потока «в ядро»; если вызов затянулся, sysmon отбирает у потока P и отдаёт другому M, а при возврате горутине может понадобиться ждать свободный P. Если блокирующих вызовов много (типично для файлового IO, который epoll на обычных файлах не покрывает), рантайм плодит потоки — вплоть до предела runtime/debug.SetMaxThreads (по умолчанию 10000), и вы получаете переключения контекста и рост потребления памяти под стеки. Отсюда стандартные способы уменьшать боль: батчинг (writev/readv, sendmmsg), буферизация в user space (bufio вместо посимвольной записи), мультиплексирование готовности через epoll вместо потока на соединение, vDSO для clock_gettime/gettimeofday (они выполняются вообще без входа в ядро — ядро отображает страничку с кодом и данными в адресное пространство процесса), mmap вместо read для больших файлов и io_uring для полностью асинхронного IO. Измерять просто: strace -c -f ./app покажет количество и суммарное время по каждому вызову.
В каком стандартном пакете Go находится функция для вызова системных вызовов?
Заголовок раздела «В каком стандартном пакете Go находится функция для вызова системных вызовов?»Коротко. В стандартном пакете syscall — функции syscall.Syscall, syscall.Syscall6, syscall.RawSyscall. Но пакет заморожен и в документации помечен как deprecated: для нового кода рекомендуется golang.org/x/sys/unix (или x/sys/windows), где набор вызовов и констант поддерживается в актуальном состоянии.
Глубже. Заморозка означает, что в syscall не добавляют новые вызовы и константы: он должен оставаться совместимым вечно, а меняющееся ядро живёт в x/sys. Прикладной код в норме вообще не обращается ни к тому, ни к другому: он использует os, net, os/exec, которые сами внизу вызывают нужное; спускаться на уровень syscall стоит только за вещами без обёртки — unix.Setns, unix.Prctl, unix.EpollCreate1, unix.Statfs, ioctl-ы. Пример корректного вызова через x/sys/unix:
package main
import ( "fmt"
"golang.org/x/sys/unix")
func main() { var st unix.Statfs_t if err := unix.Statfs("/", &st); err != nil { panic(err) } fmt.Println("free bytes:", st.Bavail*uint64(st.Bsize))}Если приходится звать вызов «руками», это unix.Syscall(trap, a1, a2, a3), где третье возвращаемое значение — unix.Errno (ноль означает успех); для вызовов, которые заведомо не блокируются, есть RawSyscall, не уведомляющий планировщик. Отдельно помните про runtime.KeepAlive и правила unsafe.Pointer/uintptr: указатель, преобразованный в uintptr, обязан превращаться обратно в аргументе того же выражения вызова, иначе GC вправе переместить или собрать объект.
os.syscall ;
Заголовок раздела «os.syscall ;»Коротко. Это обрывок списка вариантов ответа к предыдущему вопросу. Такого пакета в Go нет: правильный вариант — syscall (а в современном коде — golang.org/x/sys/unix).
Глубже. По вариантам, которые обычно предлагают в этом тесте: os.syscall — не существует, в пакете os нет ни такого подпакета, ни экспортируемой функции с таким именем (имя со строчной буквы к тому же было бы неэкспортируемым); go/syscall — тоже нет, префикс go/ в стандартной библиотеке занят инструментами анализа исходников (go/ast, go/parser, go/types); syscall — верный ответ. Полезно добавить, что пакет os действительно является потребителем syscall: os.File внутри держит дескриптор и работает через internal/poll, а сам рантайм пользуется внутренним internal/syscall/unix.
Что такое сисколл?
Заголовок раздела «Что такое сисколл?»Коротко. Системный вызов — это единственный легальный способ пользовательской программы попросить услугу у ядра: открыть файл, прочитать сокет, выделить память, создать процесс. Технически это контролируемый переход из ring 3 в ring 0 по заранее известной точке входа с номером операции и аргументами в регистрах.
Глубже. Ключевое слово — «контролируемый»: приложение не может прыгнуть в произвольный адрес ядра, оно попадает только в фиксированную точку входа, а ядро само решает, что и с какими правами делать. В Linux порядка трёхсот-четырёхсот вызовов; часть из них — базис, поверх которого построено всё остальное: read, write, openat, close, mmap, clone, execve, futex, epoll_wait, socket, ioctl. Важно не путать системный вызов с вызовом libc: printf — библиотечная функция, которая буферизует и в итоге дёргает write; malloc — аллокатор в user space, который лишь иногда просит у ядра память через mmap/brk. Стандарт POSIX описывает API, а номера вызовов — это ABI конкретной архитектуры, потому статически слинкованный бинарник x86-64 и arm64 несовместимы даже при одинаковом исходнике. Посмотреть, какие вызовы делает процесс, можно через strace, а на уровне ядра — через perf trace или bpftrace.
Что происходит во время вызова сискола?
Заголовок раздела «Что происходит во время вызова сискола?»Коротко. Программа кладёт номер вызова в RAX, аргументы в RDI/RSI/RDX/R10/R8/R9 и выполняет инструкцию syscall. Процессор переходит в ring 0, сохраняет адрес возврата в RCX и флаги в R11 и передаёт управление в точку входа ядра из MSR LSTAR. Ядро переключается на стек ядра, сохраняет регистры, диспетчеризует вызов по таблице, выполняет работу и возвращается инструкцией sysretq; результат приходит в RAX.
Глубже. По шагам детальнее: swapgs подменяет базу сегмента GS на per-CPU структуру ядра; при включённом KPTI переключаются таблицы страниц (отсюда сброс части TLB и заметный оверхед); регистры складываются в pt_regs на стеке ядра данного потока; далее прогоняются фильтры — seccomp-BPF может разрешить, отклонить с EPERM, убить процесс или отдать событие в user space, ptrace-трассировщик получает stop, audit пишет запись; затем вызывается функция из sys_call_table по номеру. Если операция не может завершиться сразу (нет данных в сокете, нужен блок с диска), ядро переводит задачу в состояние S/D и вызывает планировщик — CPU уходит другому потоку, а по готовности задача просыпается. На выходе проверяются отложенные дела: доставка сигналов, TIF_NEED_RESCHED, возврат из вложенных прерываний. Отрицательное значение -1..-4095 в RAX — код ошибки; libc превращает его в -1 и errno, Go — в syscall.Errno, реализующий error (сравнивать надо с errors.Is(err, syscall.EAGAIN), а не по строке). Часть «вызовов» вообще не входит в ядро: clock_gettime, gettimeofday, getcpu обслуживаются кодом vDSO, отображённым в адресное пространство процесса.
Что делает сисколл epoll?
Заголовок раздела «Что делает сисколл epoll?»Коротко. epoll — механизм Linux для масштабируемого ожидания готовности множества дескрипторов. Это три вызова: epoll_create1() создаёт объект-множество, epoll_ctl() добавляет/меняет/удаляет наблюдаемый fd с маской событий, epoll_wait() блокируется и возвращает список готовых. В отличие от select/poll стоимость ожидания не зависит от числа наблюдаемых дескрипторов — только от числа готовых.
Глубже. Разница в устройстве: select/poll каждый раз копируют весь набор дескрипторов в ядро и линейно его обходят — O(n) на каждый вызов; epoll хранит набор в ядре (красно-чёрное дерево) и поддерживает готовый список (ready list), в который дескриптор попадает по коллбэку от драйвера, поэтому epoll_wait — фактически O(готовых). Два режима: level-triggered (по умолчанию — событие сообщается, пока есть данные) и edge-triggered EPOLLET (только по факту изменения состояния; обязывает читать в цикле до EAGAIN и работать с неблокирующими fd). Ограничение, которое любят спрашивать: epoll не работает с обычными файлами на диске — они всегда «готовы», поэтому файловый IO остаётся блокирующим, и для него нужны потоки, io_uring или posix_fadvise-трюки. Go использует epoll в netpoller: internal/poll переводит сокеты в неблокирующий режим, регистрирует их через runtime.netpollopen в edge-triggered режиме с EPOLLET|EPOLLIN|EPOLLOUT|EPOLLRDHUP, а при EAGAIN паркует горутину (gopark); планировщик периодически (и в findrunnable) вызывает netpoll, получает готовые дескрипторы и делает их горутины runnable. Именно поэтому «по одной горутине на соединение» в Go — нормальный, а не наивный дизайн. Аналоги на других платформах: kqueue во FreeBSD/macOS, IOCP в Windows.
Какие знаешь примитивы синхронизации в ОС?
Заголовок раздела «Какие знаешь примитивы синхронизации в ОС?»Коротко. Базовые: мьютекс (взаимное исключение), семафор (счётчик разрешений), условная переменная (ожидание условия вместе с мьютексом), read-write lock, барьер, спинлок. В Linux всё пользовательское строится поверх futex — вызова, который позволяет ждать на адресе в памяти и будить ждущих. Плюс межпроцессные средства: сигналы, pipe/eventfd, POSIX-семафоры и очереди сообщений, разделяемая память, файловые блокировки flock/fcntl.
Глубже. Стоит подчеркнуть двухуровневую схему futex: быстрый путь — атомарная операция над словом в разделяемой памяти без входа в ядро; медленный — FUTEX_WAIT/FUTEX_WAKE, когда лок занят и надо усыпить или разбудить поток. На этом стоят и pthread-мьютексы glibc, и sync.Mutex в Go (через runtime_SemacquireMutex, внутри — futex на Linux). sync.Mutex в Go, кстати, имеет два режима: обычный (пришедшая горутина конкурирует с проснувшимися, лучше пропускная способность) и starvation mode при ожидании дольше 1 мс (лок передаётся в порядке очереди, лучше хвостовые задержки). Внутри ядра свой набор: spinlock (крутится, нельзя спать), mutex и semaphore (можно спать), seqlock (быстрое чтение с проверкой счётчика), RCU (читатели без блокировок, освобождение памяти отложено до grace period), completion. Разница спинлока и мьютекса — главный смысловой вопрос: спинлок жжёт CPU в ожидании и оправдан только на очень коротких критических секциях и на многоядерной системе, мьютекс отдаёт CPU, но платит за переключение контекста; гибрид — адаптивный мьютекс, который сначала немного крутится. Классические грабли, о которых полезно упомянуть: deadlock (нужен единый порядок захвата), livelock, priority inversion (лечится наследованием приоритета, PTHREAD_PRIO_INHERIT), thundering herd при пробуждении всех ждущих вместо одного.
Что такое user space и kernel space? Зачем нужно такое разделение?
Заголовок раздела «Что такое user space и kernel space? Зачем нужно такое разделение?»Коротко. Kernel space — привилегированный режим исполнения (ring 0) и адресное пространство ядра, где доступны все инструкции, вся физическая память и устройства. User space — непривилегированный режим (ring 3), где у каждого процесса своё виртуальное адресное пространство и нет прямого доступа к железу. Разделение нужно для защиты: сбой или злой умысел в приложении не должен ронять систему и не должен давать читать чужую память.
Глубже. Аппаратная основа — уровни привилегий CPU и MMU: страницы ядра помечены как доступные только супервизору, а SMEP/SMAP запрещают ядру исполнять код и (без явного разрешения) читать данные из пользовательских страниц. Поэтому ядро никогда не разыменовывает пользовательский указатель напрямую — оно копирует через copy_from_user/copy_to_user, которые проверяют адреса и корректно обрабатывают page fault. После Meltdown добавился KPTI: у пользовательского режима отдельный набор таблиц страниц, где ядро почти не отображено, — безопаснее, но дороже каждый переход. Практические следствия для инженера: любой обмен данными между мирами стоит денег и потому его хочется батчить; «zero-copy» техники (sendfile, splice, mmap, io_uring с зарегистрированными буферами) существуют именно ради экономии на копированиях и переходах; падение процесса — это SIGSEGV и корка, падение ядра — kernel panic на всю машину, из-за чего часть функциональности (FUSE, gVisor, драйверы в user space через DPDK/SPDK) намеренно выносят обратно в user space, размениваясь производительностью за надёжность.
Разворачивали ли вы сервисы напрямую на Linux или виртуальных машинах?
Заголовок раздела «Разворачивали ли вы сервисы напрямую на Linux или виртуальных машинах?»Коротко. Вопрос про личный опыт — интервьюер проверяет, понимаете ли вы жизненный цикл сервиса вне контейнерной обвязки. Отвечайте конкретно: что разворачивали, чем (systemd, Ansible, образ ВМ, k8s), и как решали типовые задачи — логи, ротация, автозапуск, обновление без даунтайма, лимиты.
Глубже. Каркас сильного ответа: (1) артефакт — как собирается (статический бинарник Go, CGO_ENABLED=0, версия в ldflags), как доставляется (пакет, tar, образ); (2) запуск — systemd-юнит с Restart=on-failure, отдельный непривилегированный пользователь, LimitNOFILE, sandbox-опции ProtectSystem, NoNewPrivileges, MemoryMax; (3) конфигурация и секреты — EnvironmentFile, Vault, права 0600; (4) наблюдаемость — логи в journald или stdout + сборщик, метрики Prometheus, healthcheck; (5) обновление — как выкатываете и как откатываете, что делаете с graceful shutdown (перехват SIGTERM, http.Server.Shutdown, дренаж из балансировщика). Если реального опыта на голых ВМ нет — скажите прямо и переведите разговор на то, что делали: «на проде был Kubernetes, но локально и на стенде поднимал через systemd, юнит писал сам — вот такой». Типичная ошибка: рассказывать про Kubernetes в ответ на вопрос про ВМ, не показав понимания, что делает kubelet руками systemd-юнита.
С какими операционными системами вы работали?
Заголовок раздела «С какими операционными системами вы работали?»Коротко. Простой калибровочный вопрос, часто в начале секции. Ответ — короткий и честный список с указанием роли: где разрабатываете, где эксплуатируете прод, где имели дело эпизодически.
Глубже. Хороший ответ звучит примерно так: «разработка на macOS/Linux, прод — Linux, дистрибутивы Debian/Ubuntu и RHEL-семейство, в контейнерах Alpine; с Windows работал в объёме кросс-компиляции и CI». Дальше стоит добавить деталь, которая показывает, что различия для вас не абстракция: разные системы вызовов и мультиплексоры (epoll в Linux, kqueue в BSD/macOS, IOCP в Windows), разница в путях и правах, отсутствие cgroup на macOS, musl против glibc в Alpine (и вытекающая история про DNS-резолвинг и статическую линковку в Go). Кросс-компиляция в Go делается через GOOS/GOARCH, но с CGO_ENABLED=1 уже нужен тулчейн целевой платформы — упоминание этого факта обычно закрывает вопрос. Не стоит перечислять всё, что когда-либо видели: следующий вопрос будет по самой экзотической позиции в списке.
Как запускать приложение при старте unix системы?
Заголовок раздела «Как запускать приложение при старте unix системы?»Коротко. На современных Linux — юнитом systemd: положить /etc/systemd/system/myapp.service, выполнить systemctl daemon-reload и systemctl enable --now myapp. Автозапуск обеспечивает секция [Install] с WantedBy=multi-user.target — enable просто создаёт симлинк в multi-user.target.wants/.
Глубже. Минимальный рабочий юнит:
[Unit]Description=My Go serviceAfter=network-online.targetWants=network-online.target
[Service]Type=notifyExecStart=/usr/local/bin/myapp --config /etc/myapp.yamlUser=myappRestart=on-failureRestartSec=2sLimitNOFILE=65535Environment=GOMAXPROCS=4
[Install]WantedBy=multi-user.targetType=simple подходит, если процесс просто работает на переднем плане; Type=notify — если приложение само сообщает о готовности через sd_notify (в Go — библиотека github.com/coreos/go-systemd/daemon), тогда зависимые юниты стартуют строго после реальной готовности; Type=forking нужен для демонов, уходящих в фон самостоятельно. Альтернативы и исторический контекст: SysV init со скриптами в /etc/init.d и уровнями запуска, Upstart, в BSD — rc.d и rc.conf, в macOS — launchd с plist, в контейнерах — вообще никакого init, процесс становится PID 1 через ENTRYPOINT (и обязан либо сам обрабатывать SIGTERM и жать зомби, либо запускаться под tini). Костыльные варианты (@reboot в crontab, rc.local) упоминать можно, но с оговоркой, что они не дают ни перезапуска, ни зависимостей, ни логов.
Что такое эмуляция?
Заголовок раздела «Что такое эмуляция?»Коротко. Эмуляция — программное воспроизведение поведения одной вычислительной платформы на другой: эмулятор интерпретирует или транслирует чужие инструкции и модели устройств. В отличие от виртуализации, где гостевой код в основном исполняется процессором нативно, при эмуляции каждая инструкция проходит через программную прослойку, поэтому она заметно медленнее, зато позволяет запускать код чужой архитектуры.
Глубже. Практический пример из ежедневной работы: сборка образа linux/arm64 на amd64-машине через docker buildx — это binfmt_misc (механизм ядра, который по «магическим байтам» ELF-файла подставляет интерпретатор) плюс qemu-user-static, транслирующий arm64-инструкции в x86-64. Тот же QEMU умеет два режима: полная системная эмуляция (qemu-system-*, с моделями устройств, годится для чужой архитектуры) и ускоренный режим с KVM, где эмуляции инструкций уже нет — это уже виртуализация. Внутри эмуляторов инструкции почти никогда не интерпретируются по одной: используется динамическая двоичная трансляция (в QEMU — TCG), которая транслирует блоки гостевого кода в код хоста и кэширует их; Rosetta 2 в macOS делает то же самое с упором на предварительную AOT-трансляцию. Полезно провести границу с соседними понятиями: контейнер — не эмуляция (то же ядро, та же архитектура), WINE — не эмулятор в строгом смысле (он реализует Windows API поверх Linux, инструкции исполняются нативно), а Docker Desktop на macOS запускает Linux в виртуальной машине, а не эмулирует его.
Для чего нужна операционная система?
Заголовок раздела «Для чего нужна операционная система?»Коротко. ОС решает три задачи: абстрагирует железо (процесс, файл, сокет вместо регистров контроллера), мультиплексирует его между программами (планировщик CPU, виртуальная память, очереди IO) и обеспечивает изоляцию и защиту, чтобы одна программа не мешала другой и не могла читать её данные. Плюс предоставляет стабильный API (системные вызовы), благодаря которому один бинарник работает на разном железе.
Глубже. Разложить по подсистемам полезно на собеседовании: управление процессами и планирование (в Linux — CFS, а с ядра 6.6 планировщик EEVDF), управление памятью (виртуальная адресация, страничная подкачка, page cache, OOM killer), файловые системы и VFS, сетевой стек, драйверы устройств и обработка прерываний, IPC и синхронизация, безопасность (права, capabilities, LSM). Стоит упомянуть спектр архитектур: монолитное ядро с загружаемыми модулями (Linux), микроядро с сервисами в user space (QNX, seL4, Minix), гибрид (XNU в macOS), unikernel — библиотека, слинкованная с одним приложением, где никакого разделения на кольца может не быть вовсе. Полезная мысль в финал: ОС — это не просто удобство, а условие мультизадачности с недоверием; на одноцелевом контроллере, где программа одна, ОС часто и не нужна (bare metal или тонкий RTOS-планировщик).
За счет чего стала возможна виртуализация?
Заголовок раздела «За счет чего стала возможна виртуализация?»Коротко. За счёт того, что чувствительные инструкции гостевой ОС можно перехватывать и отдавать гипервизору (trap-and-emulate по критериям Попека — Голдберга), а всё остальное исполнять нативно. На x86 это долго не работало из-за невиртуализуемых инструкций, поэтому сперва применяли двоичную трансляцию (VMware) и паравиртуализацию (Xen), а массовой виртуализация стала после аппаратных расширений Intel VT-x и AMD-V.
Глубже. VT-x/AMD-V вводят два новых режима — VMX root (гипервизор) и non-root (гость), у каждого свои кольца 0–3, так что гостевое ядро честно работает в своём ring 0, а при чувствительной операции происходит VM exit с сохранением состояния в структуру VMCS/VMCB. Второй ключевой кусок — память: аппаратная вложенная трансляция страниц (EPT у Intel, NPT/RVI у AMD) убрала дорогие shadow page tables, теперь MMU сам проходит две ступени — гостевую и хостовую. Третий — устройства: IOMMU (VT-d/AMD-Vi) позволяет безопасно пробрасывать устройство в гостя и делать DMA-remapping, SR-IOV нарезает одну сетевую карту на виртуальные функции, а для обычного случая используют паравиртуализованные драйверы virtio, которые вместо эмуляции реального железа общаются кольцевыми буферами и потому на порядок быстрее. В Linux всё это доступно через KVM — модуль ядра, превращающий ядро в гипервизор первого типа, поверх которого работают QEMU, Firecracker, Cloud Hypervisor. Дополнительно упомяните, что контейнеры выигрывают у ВМ не потому, что «виртуализация медленная», а потому, что не платят за второе ядро, второй планировщик и загрузку гостевой ОС; а микро-ВМ вроде Firecracker (стартует за десятки миллисекунд) специально сближают эти два мира.
Что такое initrd?
Заголовок раздела «Что такое initrd?»Коротко. initrd (initial ramdisk) — временная корневая файловая система, которую загрузчик кладёт в память вместе с ядром. Она нужна, чтобы ядро на раннем этапе смогло подгрузить модули и подготовить настоящий корень — драйверы контроллера диска, сборку RAID/LVM, расшифровку LUKS, монтирование сетевой ФС — и только потом переключиться на реальный root.
Глубже. Строго говоря, современные системы используют не initrd, а initramfs: initrd — это образ блочного устройства, который монтируется как ramdisk, а initramfs — cpio-архив (обычно сжатый), распаковываемый прямо в tmpfs/rootfs; имя initrd осталось по традиции, и его же употребляют в опциях загрузчика. Последовательность загрузки: загрузчик (GRUB/systemd-boot) читает ядро и образ initramfs в память → ядро распаковывает архив и запускает /init внутри него → скрипты (dracut, mkinitcpio, initramfs-tools) грузят модули, ищут корень по root=UUID=..., разворачивают LVM/LUKS/mdraid, при необходимости поднимают сеть → выполняется switch_root (или pivot_root), настоящий корень становится /, память под initramfs освобождается, стартует /sbin/init (systemd). Без initrd можно обойтись, если все нужные драйверы и файловые системы вкомпилированы в ядро статически — так делают на встраиваемых системах и в кастомных сборках. Полезные команды: lsinitrd/lsinitramfs — посмотреть содержимое, dracut -f / update-initramfs -u — пересобрать (обязательно после обновления драйверов или смены схемы дисков — иначе система не загрузится).
Как запустить сервис (при старте, периодически, вместе с другим сервисом)
Заголовок раздела «Как запустить сервис (при старте, периодически, вместе с другим сервисом)»Коротко. При старте — systemctl enable myapp (секция [Install] WantedBy=multi-user.target). Периодически — systemd timer с OnCalendar= или классический cron. Вместе с другим сервисом — зависимостями в [Unit]: Requires=/Wants= задают связь, After=/Before= — порядок; для жёсткой связки «живёт только вместе» — BindsTo= и PartOf=.
Глубже. Различия, которые обязательно уточняют. Wants= — мягкая зависимость: если зависимый юнит не стартовал, наш всё равно запустится; Requires= — жёсткая: падение или остановка зависимости останавливает и нас; BindsTo= строже Requires= — юнит останавливается даже если зависимость исчезла не по команде (например, пропало устройство); PartOf= работает в обратную сторону — stop/restart родителя транслируется на дочерние. Критично помнить, что Requires= сам по себе не задаёт порядок: без After= оба стартуют одновременно, и это классический источник плавающих багов «сервис не нашёл базу при старте». Для таймера пишут два файла — myapp.service (Type=oneshot) и myapp.timer:
[Unit]Description=Run myapp every 15 minutes
[Timer]OnCalendar=*:0/15Persistent=trueRandomizedDelaySec=30sAccuracySec=1s
[Install]WantedBy=timers.targetPersistent=true догоняет пропущенный запуск после простоя машины, RandomizedDelaySec размазывает пик, если таймеров много. Проверка: systemd-analyze calendar '*:0/15', systemctl list-timers. Преимущества таймеров над cron — общие логи в journald, зависимости, лимиты ресурсов и сэндбоксинг из юнита, systemctl start для ручного прогона. Ещё один способ «запустить вместе» — socket activation: myapp.socket держит слушающий сокет, systemd поднимает сервис при первом подключении и передаёт готовый дескриптор.
Как посмотреть что-то про сетку в Linux?
Заголовок раздела «Как посмотреть что-то про сетку в Linux?»Коротко. Базовый набор: ip a (адреса и интерфейсы), ip r (маршруты), ip neigh (ARP), ss -tulpn (слушающие сокеты с процессами), ss -tanp state established (соединения), ping/traceroute/mtr (доступность и путь), dig (DNS), tcpdump -ni eth0 port 443 (трафик), nft list ruleset или iptables -L -n -v (фильтрация).
Глубже. netstat и ifconfig считаются устаревшими (пакет net-tools) — на собеседовании лучше называть ss и ip, но знать, что старые команды делают то же. Полезные частности: ss -s — сводка по сокетам, быстро видно накопление TIME-WAIT или переполнение accept-очереди; ss -ltn показывает Send-Q для слушающего сокета — это размер backlog, а Recv-Q — сколько соединений в нём накопилось; nstat -az и /proc/net/snmp дают счётчики ретрансмиссий, TcpExtListenOverflows и TcpExtListenDrops — прямые улики переполнения. Дальше: ethtool -S eth0 (счётчики карты, дропы), ip -s link (ошибки на интерфейсе), conntrack -S (переполнение таблицы соединений — типичная причина странных таймаутов на NAT-узлах), sysctl net.ipv4.* (например, somaxconn, tcp_tw_reuse, размеры буферов). Для сетевых namespaces — ip netns list и ip netns exec <ns> ss -tulpn, а для контейнера — nsenter -t <pid> -n ss -tulpn (внутри контейнера обычно нет никаких утилит). Прикладной уровень: curl -v/curl -w '%{time_connect} %{time_starttransfer}\n', openssl s_client -connect host:443 для TLS, getent hosts против dig для разделения проблем NSS и DNS.
Как Go оборачивает системные вызовы?
Заголовок раздела «Как Go оборачивает системные вызовы?»Коротко. На Linux рантайм Go не использует libc: обёртки в syscall/x/sys/unix — это тонкие ассемблерные стабы, которые кладут номер и аргументы в регистры и исполняют syscall. Поверх этого syscall.Syscall вызывает runtime.entersyscall до и runtime.exitsyscall после, чтобы планировщик знал, что поток ушёл в ядро, и мог отдать его P другому потоку; syscall.RawSyscall этих уведомлений не делает и предназначен для быстрых невозможно-блокирующихся вызовов.
Глубже. Полная картина в три слоя. Внизу — ассемблерные точки входа рантайма (runtime/sys_linux_amd64.s и аналоги для других архитектур) и сгенерированные таблицы вызовов в x/sys/unix (zsyscall_linux_amd64.go генерируются mksyscall.pl из комментариев //sys). Посередине — политика планировщика: entersyscall переводит текущую g в состояние _Gsyscall и помечает P как _Psyscall; если вызов быстрый, exitsyscall возвращает тот же P почти бесплатно, а если затянулся дольше ~20 мкс, sysmon отбирает P (retake) и отдаёт другому M, и вернувшейся горутине придётся встать в очередь. Наверху — прикладные пакеты: net и os идут не напрямую, а через internal/poll.FD, который переводит дескриптор в неблокирующий режим, регистрирует его в netpoller (epoll) и при EAGAIN паркует горутину вместо блокировки потока — поэтому conn.Read вообще не выглядит как syscall с точки зрения планировщика. Отдельные особенности: на macOS и Windows идти напрямую в ядро нельзя (стабильный ABI — только libc/DLL), поэтому там Go вызывает системные библиотеки через libcCall/stdcall; часть времени берётся из vDSO без входа в ядро (runtime.nanotime); для cgo-вызовов работает похожий механизм entersyscall, из-за которого долгий C-код тоже не блокирует остальные горутины, но занимает поток. При ручной работе с unsafe.Pointer в аргументах помните правило конвертации unsafe.Pointer → uintptr внутри самого выражения вызова и runtime.KeepAlive для буферов, иначе GC вправе собрать объект прямо во время системного вызова.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Путают эмуляцию, виртуализацию и контейнеризацию: называют Docker «лёгкой виртуальной машиной», хотя ядро там общее и никакого гипервизора нет.
- Говорят, что namespaces ограничивают ресурсы. Namespaces ограничивают видимость; лимиты — это cgroups.
- Считают, что права на удаление файла определяются правами на сам файл, а не на каталог, и не могут объяснить sticky bit на
/tmp. - Уверенно заявляют, что жёсткая ссылка «копирует файл» или что симлинк можно сделать только на файл; и не помнят, что hardlink нельзя через границу ФС и на каталог.
- Отвечают, что горутина — это «лёгкий поток ОС». Ядро о горутинах не знает вовсе: это структуры рантайма, мультиплексируемые на потоки по схеме M:N.
- Называют пакет
os.syscallилиgo/syscall. Правильный ответ —syscall, а для нового кодаgolang.org/x/sys/unix. - Не могут объяснить, почему
epollлучшеselect, кроме «он быстрее»: ключевое — набор дескрипторов хранится в ядре, а стоимостьepoll_waitзависит от числа готовых, а не наблюдаемых fd. - Смотрят на
freeвместоavailableи на%iowaitкак на «загрузку диска»; внутри контейнера читают хостовый/proc/meminfoвместоmemory.maxв cgroup v2. - Считают, что
Requires=в systemd задаёт порядок запуска. Порядок задаёт толькоAfter=/Before=.
Что почитать
Заголовок раздела «Что почитать»- man7.org — разделы
namespaces(7),cgroups(7),capabilities(7),epoll(7),credentials(7),syscall(2),vdso(7). - Документация ядра: https://docs.kernel.org/ — особенно
admin-guide/cgroup-v2иfilesystems/proc. man systemd.unit,man systemd.service,man systemd.timer— источник истины по зависимостям и типам юнитов.- Исходники Go:
src/runtime/proc.go(планировщик, sysmon, entersyscall),src/runtime/netpoll_epoll.go,src/internal/poll/fd_unix.go. - Документация пакетов
syscall(pkg.go.dev/syscall) иgolang.org/x/sys/unix, а такжеunsafe.Pointer— правила конвертации вuintptr. - Brendan Gregg, «Systems Performance» и его страница про USE-метод — про то, какие команды и счётчики смотреть при разборе нагрузки.