Files

649 lines
64 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<std::string>`.
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<T&&>(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 | Как правильно называть пробел в алгоритмах на собеседовании — скрывать или называть прямо? — называть прямо: конкретная тема добирается сейчас, с указанием, что уже понятно (сложность, инварианты)
## Проверь себя
<details>
<summary>1. Почему для top-K наибольших элементов в потоковом режиме нужен именно min-heap размера k, а не max-heap?</summary>
Min-heap хранит k текущих кандидатов, и на вершине (O(1) доступ) всегда самый маленький из
них — именно с ним сравнивают каждый новый элемент: если новый больше минимума кучи, минимум
вытесняется, новый занимает его место. Max-heap размера k показывал бы на вершине самый
большой из текущих k кандидатов, что не даёт дешёвого способа проверить "стоит ли вытеснять
кого-то" — пришлось бы искать минимум отдельно, теряя весь выигрыш от кучи.
</details>
<details>
<summary>2. Процесс завершился с кодом 139. Что произошло и какой командой в коде это разбирают?</summary>
139 = 128 + 11, то есть процесс убит сигналом SIGSEGV — падение по памяти (например,
разыменование невалидного указателя). В коде это разбирается макросами WIFSIGNALED(status) и
WTERMSIG(status) над status, полученным из wait/waitpid, а не вычитанием 128 вручную.
</details>
<details>
<summary>3. Почему epoll_wait даёт O(1) на готовое событие, а select — O(n) на весь набор при каждом вызове?</summary>
select при каждом вызове проходит весь переданный набор дескрипторов целиком, чтобы понять,
какие из них готовы — работа пропорциональна общему числу отслеживаемых дескрипторов
независимо от того, сколько реально готовы. epoll разносит регистрацию (epoll_ctl, список
интереса на красно-чёрном дереве в ядре) и ожидание (epoll_wait): ядро само добавляет
дескриптор в список готовых асинхронно, когда его состояние меняется, и epoll_wait просто
отдаёт содержимое этого списка — работа пропорциональна числу реально готовых дескрипторов.
</details>
<details>
<summary>4. Для 10.0.1.130/26 назовите адрес сети и диапазон доступных хостов, объяснив расчёт.</summary>
/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.
</details>
<details>
<summary>5. Почему TCP-рукопожатие требует три сегмента, а не два?</summary>
Соединение TCP полнодуплексное — у каждого направления свой независимый ISN (начальный
порядковый номер), и обе стороны должны не только сообщить свой ISN, но и получить
подтверждение, что собеседник его получил. После SYN и SYN-ACK сервер ещё не знает, дошёл ли
его SYN-ACK до клиента — только финальный ACK клиента даёт это подтверждение и завершает
синхронизацию обоих направлений.
</details>
<details>
<summary>6. Чем зомби-процесс отличается от процесса-сироты и что физически занимает зомби?</summary>
Зомби — завершившийся процесс, чья запись (код возврата) ещё не забрана родителем через
wait/waitpid; он не занимает память, но занимает слот в таблице процессов. Сирота — процесс,
чей родитель умер раньше него; его усыновляет init/systemd (PID 1), и это состояние не
связано с зомби напрямую — сирота продолжает работать, зомби уже завершился и ждёт только
уборки записи.
</details>
## Материалы
- 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