Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613

This commit is contained in:
Kodlo-chan
2026-09-25 19:15:23 +07:00
parent 52d401f5e3
commit d313c28919
17 changed files with 5768 additions and 63 deletions
+648
View File
@@ -0,0 +1,648 @@
# 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