# D7 (30.09). Повтор и мок-интервью Последний день — не новая тема, а тренировка в формате реального собеседования: 3 часа алгоритмов вслух с таймером и 2 часа системных вопросов "объясни механизм за 30 секунд". Ниже — карта, куда смотреть по слабым местам, и два мок-сценария с эталонными ответами. ## 1. Карта слабых мест | Домен | Что спрашивают | Куда смотреть | |---|---|---| | O-нотация, хеш-таблица | сложности операций, устройство хеш-таблицы, коллизии, rehash | `D1_algo` §1–5 | | fork/exec/wait/сигналы/errno | зомби, коды 137/139, `sigaction`, `EINTR`/`EAGAIN` | `D1_linux` | | Деревья, BST, куча, top-K | обходы, инвариант BST, `sift-up`/`sift-down`, `nth_element` | `D2_algo` §1–6 | | open/mmap/epoll/select/TIME_WAIT | буферизация, page fault, `FD_SETSIZE`, ET vs LT | `D2_linux` §1–7 | | padding, правило 0/3/5, virtual-деструктор, move, UB | `sizeof`/`offsetof`, double-free, vtable, ASAN/UBSAN | `D2_cpp` §1–5 | | Списки, стек/очередь, кольцевой буфер | разворот, цикл Флойда, dummy-узел, монотонный стек | `D3_algo` | | Потоки, mutex, CV, deadlock, atomic | data race, 4 условия deadlock, `memory_order`, TSan | `D3_threads` | | gdb, core dump, valgrind | точки останова, `bt`, ASAN vs gdb, `ulimit -c` | `D3_gdb` | | Графы: BFS/DFS, топосортировка, Дейкстра, DSU | сложности `O(V+E)`, `O((V+E) log V)`, ранг+сжатие пути | `D4_algo` | | Ethernet/ARP/VLAN/MTU/IPv4/TTL/ICMP/CIDR | заголовки по байтам, checksum, /24 vs /26 | `D4_net` | | ДП: рюкзак, монеты, LCS, Edit Distance, LIS | таблица `(n+1)×(m+1)`, обратный проход по весу, `O(n log n)` LIS | `D5_algo` | | TCP: заголовок, handshake, состояния, TIME_WAIT, UDP, NAT, DNS, DHCP | 20/8 байт заголовков, `2×MSL`, `tcpdump` | `D5_net` | | bash: pipefail, xargs, awk/sed, `/proc`, ulimit, strace | коды возврата, `set -euo pipefail`, дескрипторы | `D5_bash` | | Ядро обзорно: syscall, модуль, `/proc`/`/sys`, chardev, ioctl, device tree | user space vs kernel space, `copy_to_user` | `D6_kernel` | Механизмы уровня "откуда это следует" (ООП, STL, Git/Docker/GDB детальнее) — в `HR_BASE_deep.md`, если конкретный урок дня даёт ответ слишком коротко. **Факты для карточек** - base | Сколько уроков покрывает план перед D7? — 13 файлов (`D1_algo`…`D6_docker`) по алгоритмам, Linux, C++, сетям, потокам, отладке, bash и ядру - core | Где искать формулировки механизмов, если в уроке дня их не хватает? — `HR_BASE_deep.md` - base | Сколько часов длится каждый мок сегодня? — мок №1 (алгоритмы) 3 часа, мок №2 (системное) 2 часа Почему дальше: карта показывает, где искать теорию; дальше — тренировка в формате реального таймера, начиная с алгоритмов. ## 2. Мок №1. Алгоритмы (3 часа, 6 задач × 25 минут) Формат: 25 минут на задачу — сначала вслух проговорить план и уточняющие вопросы (первые 3–5 минут), потом решение. На реальном скрининге читаемый план ценится не меньше кода: если план верный, а код не дописан за 25 минут, это всё ещё сильный ответ. Ниже — не готовый код, а эталонная последовательность шагов для каждой задачи; сам код — в соответствующем уроке дня. ### Задача 1. Хеш-таблица с нуля (`tasks/10_hash`, теория — `D1_algo` §3, §5) **Что проверяет интервьюер:** понимание механизма, а не вызов готового `std::unordered_map` — обоснование выбора разрешения коллизий и сложности, а не память формул. **Критерии сильного ответа:** называет компоненты (массив корзин, хеш-функция, коллизии, load factor) без подсказки; сразу разделяет среднюю сложность `O(1)` и худший случай `O(n)`; осознанно выбирает chaining или open addressing и объясняет цену выбора; упоминает rehash при превышении порога загрузки. **Ловушки** - Путают среднюю и худшую сложность операций → на вопрос "а что если все ключи попали в одну корзину" отвечают "всё равно O(1)" → сразу видно, что структура не понята, а заучена. - При open addressing не знают про удаление через tombstone → предлагают просто очищать ячейку → следующий поиск обрывается раньше времени, не находя элемент дальше по цепочке зондирования. - Не проговаривают степень двойки для capacity → используют деление по модулю вместо `hash & (cap - 1)`, не объясняя, зачем вообще нужна степень двойки. **Эталонный план по шагам:** 1. Уточнить тип ключей (строки/числа), нужен ли `erase`, ожидаемый порядок `N`. 2. Взять `capacity` степенью двойки, задать порог load factor (например, 0.7). 3. Выбрать хеш-функцию: для строк — FNV-1a или `std::hash`. 4. Выбрать разрешение коллизий: chaining — проще и безопаснее уложить в 25 минут. 5. `insert`: посчитать `idx = hash & (cap - 1)`, добавить в корзину; если load factor превышен — rehash в массив вдвое больше, перенести все элементы. 6. `get`/`erase`: пройти цепочку по индексу; для open addressing `erase` — пометить tombstone, не очищать ячейку. 7. Проговорить итоговые сложности и триггер rehash. **Сложность:** амортизированно `O(1)` на вставку/поиск/удаление; худший случай (плохой хеш, все ключи в одной корзине) — `O(n)`; сам rehash — `O(n)`, но происходит редко, поэтому не портит амортизированную оценку. **Уточняющие вопросы кандидата:** нужна ли потокобезопасность? Ключи известны заранее (тогда возможен perfect hashing)? Важен ли порядок вставки при обходе? **Факты для карточек** - base | Средняя сложность операций хеш-таблицы? — амортизированное O(1) - core | При каком load factor обычно триггерят rehash при open addressing? — около 0.7 - core | Зачем capacity берут степенью двойки? — `idx = hash & (cap - 1)` вместо дорогого деления по модулю - deep | Каков стандартный max_load_factor по умолчанию у `std::unordered_map` в большинстве реализаций (libstdc++, MSVC)? — 1.0 Почему дальше: хеш-таблица даёт O(1) доступ по ключу без порядка; следующая задача — структура с O(1) доступом к экстремуму и осмысленным порядком по величине. ### Задача 2. Куча / top-K (`tasks/11_heap`, теория — `D2_algo` §5–6) **Что проверяет интервьюер:** различие между построением кучи и потоковым top-K; выбор между полной кучей, min-heap размера k и `nth_element` по контексту задачи. **Критерии сильного ответа:** первым делом спрашивает про размер данных (влезают ли в память); знает сложности всех трёх подходов; не путает направление кучи для потокового top-K (для top-K наибольших нужен именно min-heap размера k). **Ловушки** - Строят кучу через `push` в цикле вместо bottom-up `heapify` → результат корректный, но `O(n log n)` вместо `O(n)` → на миллионах элементов заметно медленнее, видно по времени выполнения. - В потоковом top-K сравнивают новый элемент с максимумом кучи, а не с минимумом → кандидаты вытесняются в обратном порядке → итоговый top-K оказывается неверным набором. - Используют `nth_element` там, где нужен отсортированный результат, и забывают досортировать k-элементный префикс → набор элементов верный, порядок — нет. **Эталонный план по шагам:** 1. Уточнить `k`, `N`, влезают ли данные в память целиком, нужен ли отсортированный результат. 2. Если данные помещаются в память — `heapify` за `O(n)`, затем `k` раз `pop` по `O(log n)`. 3. Если поток / не влезает — держать min-heap размера `k`: сравнивать новый элемент с `top()` кучи (O(1)); если новый больше минимума — вытеснить минимум, добавить новый. 4. Альтернатива без построения кучи — `nth_element` (quickselect), в среднем `O(n)`, но без готового порядка внутри половин. 5. Если нужен отсортированный top-K через `nth_element` — досортировать k-элементный префикс отдельно. 6. Проговорить, почему выбран именно этот вариант. **Сложность:** полная куча — `O(n + k log n)` время, `O(n)` память; потоковый min-heap — `O(n log k)` время, `O(k)` память; `nth_element` — среднее `O(n)`, худшее `O(n²)`. **Уточняющие вопросы кандидата:** данные статичны или стримятся? Нужен ли устойчивый порядок при равных ключах? Есть ли жёсткое ограничение по памяти? **Факты для карточек** - base | Сложность top-K через полную кучу? — O(n + k log n) - core | Какую кучу держат для потокового top-K наибольших элементов размера k? — min-heap размера k - core | Средняя и худшая сложность nth_element? — среднее O(n), худшее O(n²) - deep | Как получить top-K частых элементов за O(n) без log-множителя? — подсчёт хеш-таблицей O(n) + bucket sort по частоте (массив корзин размера n+1) Почему дальше: куча и top-K работают со случайным доступом по значению; следующая задача — структура, где доступ строго последовательный через указатели, и там есть собственный класс ошибок — циклы. ### Задача 3. Связный список с циклом (`tasks/02_list`, теория — `D3_algo` §5) **Что проверяет интервьюер:** понимание, почему указатели вообще встречаются (а не просто память алгоритма Флойда), корректная обработка граничных случаев. **Критерии сильного ответа:** объясняет механизм через относительную скорость (`fast` догоняет `slow` на 1 узел за шаг внутри цикла), а не просто произносит "медленный и быстрый указатель"; корректно достраивает поиск входа в цикл вторым проходом. **Ловушки** - Сравнивают значения узлов вместо указателей → на списке с повторяющимися значениями без реального цикла даёт ложное срабатывание. - Забывают проверку `fast && fast->next` перед `fast->next->next` → падение на `nullptr` для списка без цикла чётной/нечётной длины на границе. - Не могут объяснить вторую часть (поиск входа в цикл) без домашней заготовки → интервьюер спрашивает "почему это работает", а не "какой код писать". **Эталонный план по шагам:** 1. Уточнить: список одно- или двусвязный, возможны ли пустой список и self-loop (узел ссылается сам на себя). 2. `slow = head`, `fast = head`. 3. Пока `fast && fast->next`: `slow` на 1 шаг, `fast` на 2 шага; если `slow == fast` — цикл найден, выйти из цикла проверки. 4. Если `fast` дошёл до `nullptr` — цикла нет. 5. Для входа в цикл: сбросить один указатель на `head`, второй оставить в точке встречи; двигать оба по 1 шагу — встретятся ровно на входе в цикл. 6. Кратко обосновать почему (из `2(a+b) = a+b+n·c` следует `a = (n-1)c + (c-b)`). **Сложность:** время `O(n)`, память `O(1)` — без хеш-множества посещённых узлов. **Уточняющие вопросы кандидата:** нужна ли длина цикла отдельно от факта его наличия? Список гарантированно не пуст? **Факты для карточек** - base | Сложность обнаружения цикла алгоритмом Флойда по времени и памяти? — O(n) время, O(1) память - core | С какой относительной скоростью fast догоняет slow внутри цикла? — 1 узел за шаг - core | Как найти вход в цикл после первой встречи указателей? — сбросить один указатель на head, оба двигать по 1 шагу — встретятся на входе Почему дальше: список — структура с одним "соседом" на узел; граф — структура с произвольным числом соседей, и обход там даёт другие гарантии в зависимости от порядка обхода. ### Задача 4. BFS по матрице/графу (теория — `D4_algo` §2) **Что проверяет интервьюер:** умение свести сетку к неявному графу соседей, правильный момент пометки `visited`, вывод сложности через размеры входа. **Критерии сильного ответа:** явно проговаривает, что кратчайший путь по рёбрам гарантирован именно порядком FIFO очереди, а не самим фактом использования BFS "потому что так учили"; помечает узел посещённым в момент постановки в очередь, а не при извлечении. **Ловушки** - Помечают `visited` при извлечении из очереди, а не при добавлении → один и тот же узел кладётся в очередь несколько раз → деградация по времени, иногда неверный `dist`. - Не проверяют границы сетки перед обращением к соседней клетке → выход за границы массива. - Считают диагональных соседей, хотя задача просила 4-связность (или наоборот) → неверный граф с самого начала, весь дальнейший разбор бессмыслен. **Эталонный план по шагам:** 1. Уточнить: сетка или произвольный граф; 4 или 8 соседей; есть ли препятствия или веса рёбер. 2. Представление: список смежности для явного графа; прямой обход соседних клеток для сетки. 3. `dist[] = -1` (или `visited[][] = false`), очередь, `dist[src] = 0`, `push(src)`. 4. Пока очередь не пуста: `pop u`; для каждого непосещённого соседа `v` — `dist[v] = dist[u] + 1`, `push(v)`, пометить как посещённый сразу здесь. 5. Ответ — `dist[target]` (или число посещённых клеток для задач на компоненты/острова). **Сложность:** `O(V+E)` для графа; для сетки `R×C` — `O(R·C)`, так как `V = R·C`, а `E` пропорционально `V` при фиксированном числе соседей на клетку (4 или 8). **Уточняющие вопросы кандидата:** веса рёбер одинаковы? Граф ориентированный? Нужен сам путь или только его длина? **Факты для карточек** - base | Сложность BFS на сетке R×C? — O(R·C) - core | Почему BFS гарантирует кратчайший путь по числу рёбер? — FIFO-очередь раскрывает граф строго по слоям расстояния - core | В какой момент нужно помечать узел посещённым, чтобы избежать повторных вставок в очередь? — в момент постановки в очередь, а не при извлечении - deep | Как обойти граф со весами рёбер только 0 и 1 за O(V+E) без полноценной Дейкстры? — 0-1 BFS: deque, вес 0 — push_front, вес 1 — push_back Почему дальше: BFS решает граф с одинаковой "ценой" каждого ребра; следующая задача — с одномерным массивом, где решение каждого следующего элемента зависит от предыдущих подрешений, а не от соседей в структуре — это динамическое программирование. ### Задача 5. ДП на строки: LCS / Edit Distance (теория — `D5_algo` §6) **Что проверяет интервьюер:** умение построить таблицу `(n+1)×(m+1)`, объяснить переходы словами, а не просто написать формулу по памяти. **Критерии сильного ответа:** явно объясняет базовый случай (пустая строка/префикс) и почему размер таблицы `(n+1)×(m+1)`, а не `n×m`; для Edit Distance проговаривает все три операции (замена/удаление/вставка) и почему берётся минимум трёх соседних ячеек. **Ловушки** - Заводят таблицу `n×m` вместо `(n+1)×(m+1)` → нет места под базовый случай "один из префиксов пуст" → неверные значения на границе или выход за границы массива. - В Edit Distance забывают инициализировать нулевую строку/столбец → сравнение с мусорными значениями → неверный ответ без падения программы (тихая ошибка). - Путают направление переходов LCS (берут `min` вместо `max` при несовпадении символов) → результат заведомо меньше правильного. **Эталонный план по шагам:** 1. Уточнить: нужна длина LCS, сама подпоследовательность (восстановление пути) или Edit Distance. 2. Завести таблицу `dp[(n+1)][(m+1)]`; строка/столбец 0 — базовый случай "один из префиксов пуст". 3. LCS: при совпадении символов — `dp[i-1][j-1] + 1`; иначе — `max(dp[i-1][j], dp[i][j-1])`. 4. Edit Distance: `dp[i][0] = i`, `dp[0][j] = j`; при совпадении — `dp[i-1][j-1]`; иначе — `1 + min(замена, удаление, вставка)`. 5. Ответ — `dp[n][m]`. 6. Если нужна экономия памяти и не нужно восстановление пути — держать только 2 строки таблицы. **Сложность:** время и память `O(n·m)`; при экономии памяти (без восстановления пути) — `O(min(n,m))` память. **Уточняющие вопросы кандидата:** нужна ли сама подпоследовательность/путь операций, или только число? Укладывается ли `n·m` в лимит по времени при заданных ограничениях на длину строк? **Факты для карточек** - base | Размер таблицы LCS/Edit Distance для строк длины n и m? — (n+1)×(m+1) - base | Временная и пространственная сложность LCS/Edit Distance? — O(n·m) и по времени, и по памяти - core | Три операции, между которыми выбирают минимум в Edit Distance? — замена, удаление, вставка Почему дальше: LCS и Edit Distance решают задачу за O(n·m) явной таблицей; следующая классическая задача решается той же схемой за O(n²), но улучшается до O(n log n) совсем другим приёмом — это повод спросить про компромисс между простотой и асимптотикой. ### Задача 6. ДП/НВП — LIS (`tasks/05_lis`, теория — `D5_algo` §7) **Что проверяет интервьюер:** знание наивного `O(n²)` и продвинутого `O(n log n)` подходов, умение оценить ограничения по `N` и сразу выбрать нужный алгоритм. **Критерии сильного ответа:** сразу считает, влезает ли наивный `O(n²)` в лимит времени при данном `N` (например, на `N = 100 000` это `10¹⁰` операций — не укладывается в 2 секунды); понимает, что массив `tails[]` — не сама LIS, а вспомогательная структура минимальных хвостов. **Ловушки** - Путают длину массива `tails[]` (результат) с самой LIS-последовательностью → на просьбу "выведите саму подпоследовательность" отвечают неверно, потому что `tails[]` не хранит реальный путь. - Делают линейный поиск позиции вместо `lower_bound` в `tails[]` → теряют весь выигрыш от `O(n log n)`, получают снова `O(n²)`. - Не уточняют строгое или нестрогое возрастание → готовое решение может не соответствовать формулировке задачи. **Эталонный план по шагам:** 1. Уточнить `N` и лимит по времени/памяти, строгое или нестрогое возрастание. 2. Если `N` мало — наивный `O(n²)`: `dp[i]` — длина LIS, заканчивающейся на `a[i]`, перебор `j < i` с `a[j] < a[i]`. 3. Если `N` большое — массив `tails[]`: для каждого нового элемента бинарным поиском (`lower_bound`) найти позицию замены или расширения `tails`. 4. Ответ — текущая длина `tails[]`. 5. Проговорить: для восстановления самой подпоследовательности нужен отдельный массив индексов-предков, `tails[]` для этого не подходит. **Сложность:** наивный подход — `O(n²)` время, `O(n)` память; продвинутый — `O(n log n)` время, `O(n)` память. **Уточняющие вопросы кандидата:** нужна ли сама подпоследовательность или только длина? Строгое возрастание или нестрогое (допускаются равные соседние значения)? **Факты для карточек** - base | Сложность наивного LIS и продвинутого через tails[]? — O(n²) и O(n log n) соответственно - core | Почему наивный O(n²) не проходит на N=100 000 за 2 секунды? — 10¹⁰ операций, не укладывается в типичный лимит времени внутреннего теста - core | Что хранит tails[k]? — минимальный возможный последний элемент возрастающей подпоследовательности длины k+1 - deep | Как называется классический приём построения tails[] с заменой через бинарный поиск? — patience sorting (раскладка карт по кучкам) Почему дальше: алгоритмическая часть проверяет чистые структуры данных с однозначным эталонным ответом; системная часть мока проверяет то же самое качество понимания, но в формате устного объяснения без кода — и там интервьюер чаще всего задаёт один и тот же вопрос дважды, разными словами, чтобы проверить глубину. ## 3. Мок №2. Системное (2 часа, 12 вопросов) Формат: интервьюер задаёт короткий вопрос, ждёт ответ на 20–40 секунд, затем "углубляет" — задаёт уточняющий вопрос, который проверяет, понята ли причина, а не просто выучен факт. Ниже — по 4 вопроса на C/C++, Linux и сети. ### C/C++ **Вопрос 1. Посчитайте `sizeof(struct { char a; int b; char c; })` на x86-64 и объясните откуда взялось число.** Чего ждёт интервьюер: конкретное число (12) и раскладку по смещениям, а не "наверное, 6". Заготовка ответа (20–40 сек): "Поля дают 6 байт, но `sizeof` будет 12. `a` на смещении 0; дальше 3 байта паддинга, потому что `int b` требует выравнивания на 4 байта; `b` занимает смещения 4–7; `c` на смещении 8. Полезные данные кончаются на 9 байте, но размер всей структуры округляется вверх до кратного выравниванию самого строгого поля — здесь `int`, выравнивание 4 — отсюда ещё 3 байта хвостового паддинга и итог 12." Углубление: "А если переставить поля — `char a; char c; int b`?" → 8 байт: два `char` подряд без паддинга между ними, потом `int` с выравниванием 4, конец уже кратен 4 — экономия 4 байта на объект. "Что делает `#pragma pack(1)`?" → убирает паддинг (`sizeof` станет 6), но доступ к невыровненным полям дороже на x86 и может аппаратно упасть на платформах со строгим выравниванием. **Факты для карточек** - base | sizeof(struct { char a; int b; char c; }) на x86-64? — 12 байт - core | sizeof той же структуры с полями в порядке char a; char c; int b? — 8 байт **Вопрос 2. Что такое правило 0/3/5 и когда его нужно применять?** Чего ждёт интервьюер: связь между "класс владеет ресурсом" и необходимостью явно писать копирование/перемещение. Заготовка ответа: "Если класс сам управляет ресурсом (владеющий указатель, файловый дескриптор) и поэтому определяет деструктор, он почти наверняка должен явно определить и конструктор/оператор копирования (правило трёх), а с C++11 — ещё и перемещения (правило пяти). Если ни одну из пяти функций не объявить, компилятор сгенерирует все сам, и сгенерированное копирование — побитовое: для владеющего указателя это значит, что обе копии получат один и тот же адрес, и оба деструктора вызовут `delete` на нём — double-free." Углубление: "А правило нуля?" → не писать ни одну из пяти вручную, доверив владение готовой RAII-обёртке (`unique_ptr`, `vector`, `string`) — её сгенерированные копирование/перемещение уже корректны. **Факты для карточек** - base | Какие 5 функций входят в правило пяти? — деструктор, конструктор копирования, оператор присваивания копированием, конструктор перемещения, оператор присваивания перемещением - core | Что делает сгенерированный компилятором конструктор копирования по умолчанию? — побитовое (memberwise) копирование каждого поля **Вопрос 3. Что физически делает `std::move`?** Чего ждёт интервьюер: развенчание мифа "move перемещает данные во время выполнения". Заготовка ответа: "Ничего во время выполнения — это `static_cast(x)`, явный каст к rvalue-ссылке. Единственный эффект — при выборе перегрузки компилятор теперь предпочитает move-конструктор, а не копирующий. Реальную работу делает move-конструктор конкретного типа: для `vector` это перенос трёх указателей (начало, конец данных, конец ёмкости) и обнуление их у источника — O(1), без копирования элементов." Углубление: "В каком состоянии объект после `std::move`?" → в валидном, но неопределённом состоянии по стандарту; использование старых данных — не UB, но логическая ошибка, не ловится санитайзерами. **Факты для карточек** - base | Что физически делает std::move во время выполнения? — ничего: это static_cast к rvalue-ссылке - core | За какое время перемещается std::vector и что именно переносится? — O(1): три внутренних указателя (начало, конец данных, конец ёмкости) **Вопрос 4. Назовите 2–3 примера UB в C++ и объясните разницу ASAN/UBSAN.** Чего ждёт интервьюер: конкретные примеры (не общее "неопределённое поведение — это плохо") и чёткое разделение зон ответственности двух санитайзеров. Заготовка ответа: "Знаковое переполнение `int` (`INT_MAX + 1`), сдвиг на число бит больше или равное разрядности типа (`1 << 32` для 32-битного `int`), разыменование `nullptr` — всё UB. ASAN проверяет ошибки работы с памятью: границы, use-after-free, double-free. UBSAN проверяет сами операции языка, являющиеся UB по стандарту: переполнение, сдвиг, выравнивание. Они не заменяют друг друга, включают вместе: `-fsanitize=address,undefined`." Углубление: "Почему код может работать в `-O0` и падать в `-O2`?" → оптимизатор релизной сборки строит код в предположении, что UB не происходит, и может убрать проверки, на которые программист рассчитывал. **Факты для карточек** - base | Значение INT_MAX для 32-битного int? — 2147483647 - core | Флаг компилятора, включающий сразу ASAN и UBSAN? — -fsanitize=address,undefined Почему дальше: C++ отвечает за корректность в пределах одного процесса; следующий блок вопросов — про то, что происходит на границе процесса и ядра. ### Linux **Вопрос 5. Процесс упал, echo $? показывает 139. Что произошло?** Чего ждёт интервьюер: связь оболочечного кода возврата с сигналом, а не просто "процесс упал". Заготовка ответа: "Оболочка кодирует код возврата как 128 + номер сигнала. 139 = 128 + 11 — `SIGSEGV`, падение по памяти. 137 = 128 + 9 — `SIGKILL`, часто это OOM-killer. 143 = 128 + 15 — `SIGTERM`, корректный запрос на завершение. В коде это разбирается макросами `WIFSIGNALED`/`WTERMSIG` над `status` из `waitpid`, а не вручную арифметикой." Углубление: "Можно ли перехватить SIGKILL обработчиком?" → нет, `SIGKILL` (9) и `SIGSTOP` (19) нельзя ни перехватить, ни заблокировать — ядро обрабатывает их безусловно, это единственный гарантированный способ остановить процесс, который сам игнорирует остальные сигналы. **Факты для карточек** - base | Откуда взялись коды возврата 137 и 139? — 128 + номер сигнала: 137 = SIGKILL(9), 139 = SIGSEGV(11) - core | Какие два сигнала нельзя перехватить или заблокировать? — SIGKILL (9) и SIGSTOP (19) **Вопрос 6. Что такое зомби-процесс?** Чего ждёт интервьюер: точное понимание, что зомби не расходует память, а занимает запись в таблице процессов. Заготовка ответа: "Завершившийся ребёнок не исчезает полностью — ядро держит его запись (код возврата) в таблице процессов, пока родитель не заберёт её через `wait`/`waitpid`. Такой процесс — зомби, состояние `Z` в `ps`. Памяти он не занимает, но занимает слот в таблице процессов; накопление зомби — это утечка слотов, а не утечка памяти." Углубление: "Как избавиться от зомби, если родитель не вызывает wait?" → либо родитель обязан вызывать `waitpid` (часто в обработчике `SIGCHLD`), либо если родитель сам умирает раньше ребёнка — ребёнка усыновляет init/systemd (PID 1), который делает `wait` за него, и ребёнок зомби не остаётся. **Факты для карточек** - base | Состояние зомби-процесса в выводе ps? — Z - core | Что именно занимает зомби-процесс — память или что-то другое? — слот в таблице процессов, не память **Вопрос 7. Чем epoll лучше select для сервера с тысячами соединений?** Чего ждёт интервьюер: количественное сравнение сложности и лимитов, а не общую фразу "epoll быстрее". Заготовка ответа: "select ограничен `FD_SETSIZE` = 1024 дескриптора и на каждый вызов проходит весь набор целиком — `O(n)` независимо от того, сколько реально готово, плюс `fd_set` надо пересобирать перед каждым вызовом. epoll разносит регистрацию (`epoll_ctl`, список интереса на красно-чёрном дереве в ядре) и ожидание (`epoll_wait`, который просто возвращает уже готовые дескрипторы) — `O(1)` на каждое готовое событие, без лимита 1024." Углубление: "Level-triggered или edge-triggered по умолчанию?" → level-triggered — у epoll по умолчанию и всегда у select/poll: событие сообщается, пока условие истинно. Edge-triggered — явный флаг `EPOLLET`, требует вычитывать данные до `EAGAIN` на неблокирующем дескрипторе, иначе остаток данных не будет сигнализирован повторно. **Факты для карточек** - base | Жёсткий лимит select и его значение? — FD_SETSIZE, 1024 - core | Сложность epoll_wait на одно готовое событие против select/poll на весь набор? — epoll_wait O(1) на событие; select/poll O(n) на весь набор при каждом вызове **Вопрос 8. Что делает mmap и чем MAP_SHARED отличается от MAP_PRIVATE?** Чего ждёт интервьюер: механизм ленивого отображения через page fault, а не "mmap читает файл в память". Заготовка ответа: "mmap резервирует диапазон виртуальных адресов и связывает его со страницами файла в страничном кэше ядра; физического чтения при самом вызове нет. При первом обращении к странице — page fault: minor, если страница уже в кэше (дёшево), major, если её нужно реально прочитать с диска (дорого). `MAP_SHARED` — изменения видны всем процессам и попадают в файл на диске; `MAP_PRIVATE` — copy-on-write, как при `fork`: страницы общие, пока не начинается запись, тогда процесс получает свою копию, а файл на диске не меняется." Углубление: "Когда read быстрее mmap?" → при однократном последовательном чтении небольшого файла — накладные расходы на настройку отображения и page fault на каждую новую страницу не амортизируются, обычный `read` одним вызовом дешевле. **Факты для карточек** - base | Разница MAP_SHARED и MAP_PRIVATE? — MAP_SHARED: изменения видны другим и попадают в файл; MAP_PRIVATE: copy-on-write, изменения приватны - core | Чем minor page fault отличается от major? — minor: страница уже в кэше (дёшево); major: страница читается с диска (дорого) Почему дальше: Linux управляет одним узлом; следующий блок — про то, как узлы вообще находят друг друга и обмениваются байтами по проводу. ### Сети **Вопрос 9. Из чего состоит Ethernet-заголовок и что меняет VLAN?** Чего ждёт интервьюер: точные числа байт заголовка и понимание, зачем и куда вставляется тег. Заготовка ответа: "Ethernet-заголовок — 14 байт: 6 байт MAC получателя, 6 байт MAC отправителя, 2 байта EtherType. После полезной нагрузки — 4-байтовый трейлер FCS, в заголовок не входит. VLAN-тег 802.1Q по стандарту вставляет 4 дополнительных байта между MAC отправителя и EtherType: TPID (2 байта, фиксировано 0x8100) и TCI (2 байта: PCP 3 бита, DEI 1 бит, VID 12 бит, диапазон реально используемых значений 1–4094). Заголовок с тегом растёт с 14 до 18 байт." Углубление: "Что будет, если не учесть эти 4 байта при расчёте MTU?" → ошибка ровно на 4 байта — типично ловится сравнением дампа `tcpdump` с ожидаемой длиной кадра. **Факты для карточек** - base | Длина Ethernet-заголовка и длина заголовка с VLAN-тегом? — 14 байт без тега, 18 байт с тегом 802.1Q - core | Значение TPID и число бит поля VID? — TPID = 0x8100, VID = 12 бит (диапазон 1–4094) **Вопрос 10. Дана подсеть 10.0.1.130/26. Назовите сеть, broadcast и диапазон хостов.** Чего ждёт интервьюер: расчёт на лету, а не заученный пример из урока. Заготовка ответа: "/26 — 6 бит под хосты, 64 адреса всего, 62 доступно хостам (минус адрес сети и broadcast). Маска в десятичном виде — 255.255.255.192. Для 10.0.1.130/26: сеть — 10.0.1.128, broadcast — 10.0.1.191, диапазон хостов — 129..190." Углубление: "Как быстро найти самую узкую подсеть под 100 хостов?" → формула `32 - ceil(log2(h+2))`: для `h=100` это `32 - ceil(log2(102)) = 32 - 7 = /25` (126 хостов — ближайшая подходящая степень двойки сверху, `/26` с 62 хостами уже не хватит). **Факты для карточек** - base | Сколько хостов доступно в подсети /26 и как выглядит маска в десятичном виде? — 62 хоста, 255.255.255.192 - core | Сеть, broadcast и диапазон хостов для 10.0.1.130/26? — сеть 10.0.1.128, broadcast 10.0.1.191, хосты 129–190 **Вопрос 11. Как работает traceroute и при чём тут TTL?** Чего ждёт интервьюер: понимание, что traceroute не использует отдельный протокол обнаружения маршрута, а эксплуатирует побочный эффект TTL. Заготовка ответа: "TTL уменьшается на 1 на каждом маршрутизаторе на пути пакета; при обнулении пакет отбрасывается, а отправителю уходит ICMP Time Exceeded (тип 11). traceroute последовательно шлёт пакеты с TTL 1, 2, 3... — пакет с TTL 1 умирает на первом маршрутизаторе, тот присылает Time Exceeded со своим адресом; пакет с TTL 2 умирает на втором, и так далее, пока пакет не дойдёт до получателя. Список хопов восстанавливается по цепочке ICMP-ответов без специального протокола обнаружения маршрута." Углубление: "Почему ping иногда не проходит, а обычное TCP-соединение на тот же хост работает?" → ICMP может быть заблокирован файрволом отдельно от TCP-порта — это два разных уровня фильтрации, `ping host` и `curl host` в таком случае дают разный результат. **Факты для карточек** - base | На сколько уменьшается TTL на каждом маршрутизаторе и какой ICMP-тип шлётся при обнулении? — на 1; ICMP Time Exceeded, тип 11 - core | С какого TTL traceroute начинает зондирование и почему именно так восстанавливает маршрут? — с TTL=1, наращивая на 1 — каждый пакет умирает на очередном хопе и раскрывает его адрес **Вопрос 12. Три сегмента TCP-рукопожатия и зачем нужен TIME_WAIT?** Чего ждёт интервьюер: точная последовательность флагов и механическая причина TIME_WAIT, а не "так положено по стандарту". Заготовка ответа: "SYN от клиента со своим ISN → SYN+ACK от сервера (Ack = ISN клиента + 1, и собственный ISN сервера) → ACK от клиента, подтверждающий ISN сервера. Двух шагов недостаточно, потому что соединение полнодуплексное: сервер не может быть уверен, что его SYN-ACK дошёл, пока не получит финальный ACK. Закрытие — уже 4 сегмента (FIN, ACK, FIN, ACK) из-за полузакрытия каждого направления по отдельности. Сторона, отправившая финальный ACK, уходит в TIME_WAIT на 2×MSL — она должна поймать задержавшиеся в сети дубликаты старых сегментов и быть готова повторно отправить последний ACK, если партнёр повторит FIN." Углубление: "Сколько реально длится TIME_WAIT в Linux?" → 60 секунд, фиксированная константа ядра `TCP_TIMEWAIT_LEN`, а не номинальные 4 минуты (2×2 мин) по RFC 793. **Факты для карточек** - base | Сколько сегментов в three-way handshake и в полном закрытии соединения? — 3 (SYN, SYN+ACK, ACK) и 4 (FIN, ACK, FIN, ACK) - deep | Сколько секунд реально длится TIME_WAIT в Linux? — 60 секунд (константа TCP_TIMEWAIT_LEN), не совпадает с номинальными 4 минутами по RFC 793 Почему дальше: 12 вопросов закрывают C/C++, Linux и сети по отдельности; дальше — не техника, а формат самого разговора: что спросить у работодателя и как рассказать про себя, чтобы не потерять баллы на ровном месте. ## 4. Вопросы работодателю Задавать под конец интервью, когда спросят "есть ли у вас вопросы" — не сам факт вопроса важен, а конкретность. 1. На каком стандарте C++ пишут в проекте (C++11/14/17/20), какой компилятор и целевая архитектура (x86, ARM, embedded-таргет)? 2. Как устроено код-ревью: обязательно ли оно для джуна перед мерджем, кто ревьюит, сколько итераций в среднем проходит один PR? 3. Что гоняет CI на каждый коммит — юнит-тесты, статический анализ, санитайзеры? Обязательно ли покрытие тестами для мерджа? 4. Какие задачи обычно дают джуну в первый месяц — багфиксы, мелкие фичи, документация, разбор legacy-кода? 5. Есть ли доступ к реальному стенду/железу для отладки, или разработка сначала идёт на эмуляторе/симуляторе? 6. Кто ментор на испытательный срок и как часто идёт синхронизация — ежедневный созвон, раз в неделю, по запросу? 7. Что для команды считается успехом джуна через год — конкретные вехи или метрики, а не общие слова? 8. Какая доля времени уходит на поддержку и доработку существующего кода против новых задач с нуля? **Факты для карточек** - base | Сколько вопросов работодателю стоит подготовить заранее на такое интервью? — 8, каждый — конкретный, не риторический - core | Когда обычно задают вопросы работодателю на техническом скрининге? — в конце интервью, по приглашению интервьюера Почему дальше: вопросы работодателю закрывают техническую часть разговора; последний пункт — не техника вообще, а то, как честно и без потери баллов рассказать про собственный опыт. ## 5. Легенда по опыту Задача — не приукрашивать, а структурировать то, что уже сделано руками, так, чтобы интервьюер за 60–90 секунд понял уровень, не тратя время на наводящие вопросы. Рабочий каркас для таких ответов — STAR (Situation, Task, Action, Result): сначала контекст в одной фразе, потом конкретное действие, потом измеримый результат. **Что говорить как есть, потому что это правда и это ценно:** - Практика с C/C++ и Linux не в вакууме, а руками: написанный код, работа в терминале, скрипты. - Опыт сборки и CI на собственном проекте (например, сборка APK и настройка пайплайна) — конкретный пример того, что такое "довести до работающего результата", даже если стек не совпадает с C++/embedded напрямую: понимание пайплайна сборки, зависимостей, автоматизации переносится. - Текущая подготовка по плану — не "готовился неделю перед собеседованием для галочки", а системный разбор конкретных тем с самопроверкой (карточки, мок-интервью) — это тоже сигнал об умении учиться быстро и целенаправленно. **Как объяснять пробелы, не оправдываясь:** - "Алгоритмы — тема, которую я целенаправленно добираю последние недели: понимаю формальные свойства структур (сложность, инварианты, границы применимости), но production-опыта оптимизации под high-load нет — это зона роста, а не то, что я скрываю." - Если спросят про конкретный API, которого не касался — честно "не работал с этим напрямую, но по механизму ожидаю Y, потому что Z" — интервьюер оценивает способность рассуждать, а не только базу знаний. **Чего не говорить:** - Не заявлять претензию на роль лида или архитектурные решения, если такого опыта не было — на джуновском скрининге это резко расходится с реальным уровнём ответов на технические вопросы и подрывает доверие ко всему остальному сказанному. - Не преувеличивать глубину embedded/kernel-опыта: тема ядра в этом плане закрыта на уровне "отвечать словами" (`D6_kernel`), а не "писал драйверы в проде" — если спросят "а сами писали?", честный ответ "нет, разбирал теоретически и на учебных примерах" безопаснее любой неправды, которая всплывёт на первом же уточняющем вопросе. **Факты для карточек** - base | Из каких 4 частей состоит структура STAR для ответа про опыт? — Situation, Task, Action, Result - core | Как правильно называть пробел в алгоритмах на собеседовании — скрывать или называть прямо? — называть прямо: конкретная тема добирается сейчас, с указанием, что уже понятно (сложность, инварианты) ## Проверь себя
1. Почему для top-K наибольших элементов в потоковом режиме нужен именно min-heap размера k, а не max-heap? Min-heap хранит k текущих кандидатов, и на вершине (O(1) доступ) всегда самый маленький из них — именно с ним сравнивают каждый новый элемент: если новый больше минимума кучи, минимум вытесняется, новый занимает его место. Max-heap размера k показывал бы на вершине самый большой из текущих k кандидатов, что не даёт дешёвого способа проверить "стоит ли вытеснять кого-то" — пришлось бы искать минимум отдельно, теряя весь выигрыш от кучи.
2. Процесс завершился с кодом 139. Что произошло и какой командой в коде это разбирают? 139 = 128 + 11, то есть процесс убит сигналом SIGSEGV — падение по памяти (например, разыменование невалидного указателя). В коде это разбирается макросами WIFSIGNALED(status) и WTERMSIG(status) над status, полученным из wait/waitpid, а не вычитанием 128 вручную.
3. Почему epoll_wait даёт O(1) на готовое событие, а select — O(n) на весь набор при каждом вызове? select при каждом вызове проходит весь переданный набор дескрипторов целиком, чтобы понять, какие из них готовы — работа пропорциональна общему числу отслеживаемых дескрипторов независимо от того, сколько реально готовы. epoll разносит регистрацию (epoll_ctl, список интереса на красно-чёрном дереве в ядре) и ожидание (epoll_wait): ядро само добавляет дескриптор в список готовых асинхронно, когда его состояние меняется, и epoll_wait просто отдаёт содержимое этого списка — работа пропорциональна числу реально готовых дескрипторов.
4. Для 10.0.1.130/26 назовите адрес сети и диапазон доступных хостов, объяснив расчёт. /26 оставляет 6 бит под хосты (32-26=6), это 64 адреса всего, из них 62 доступны хостам (минус адрес сети и broadcast). Маска — 255.255.255.192, последний октет сети получается округлением 130 вниз до кратного 64: сеть 10.0.1.128, broadcast 10.0.1.191 (128+63), диапазон хостов — 129..190.
5. Почему TCP-рукопожатие требует три сегмента, а не два? Соединение TCP полнодуплексное — у каждого направления свой независимый ISN (начальный порядковый номер), и обе стороны должны не только сообщить свой ISN, но и получить подтверждение, что собеседник его получил. После SYN и SYN-ACK сервер ещё не знает, дошёл ли его SYN-ACK до клиента — только финальный ACK клиента даёт это подтверждение и завершает синхронизацию обоих направлений.
6. Чем зомби-процесс отличается от процесса-сироты и что физически занимает зомби? Зомби — завершившийся процесс, чья запись (код возврата) ещё не забрана родителем через wait/waitpid; он не занимает память, но занимает слот в таблице процессов. Сирота — процесс, чей родитель умер раньше него; его усыновляет init/systemd (PID 1), и это состояние не связано с зомби напрямую — сирота продолжает работать, зомби уже завершился и ждёт только уборки записи.
## Материалы - cppreference, `std::unordered_map` — https://en.cppreference.com/w/cpp/container/unordered_map - cppreference, `std::move` — https://en.cppreference.com/w/cpp/utility/move - man7.org, `epoll(7)` — https://man7.org/linux/man-pages/man7/epoll.7.html - man7.org, `mmap(2)` — https://man7.org/linux/man-pages/man2/mmap.2.html - man7.org, `wait(2)` — https://man7.org/linux/man-pages/man2/wait.2.html - RFC 793, Transmission Control Protocol — https://datatracker.ietf.org/doc/html/rfc793