Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv
This commit is contained in:
+106
-4
@@ -4,14 +4,116 @@
|
||||
принятых байт обратно в то же соединение, обслуживает несколько клиентов одновременно,
|
||||
завершается по SIGTERM/SIGINT аккуратно (exit 0).
|
||||
|
||||
Требования:
|
||||
## Требования
|
||||
|
||||
- мультиплексирование через `epoll` (не блокирующий `accept` в цикле, не поток на клиента);
|
||||
- сокеты неблокирующие, обработка `EAGAIN`/`EINTR`, частичные чтения и записи;
|
||||
- `SO_REUSEADDR`, bind на 127.0.0.1, прослушивание на нужном порту;
|
||||
- до 64 одновременных соединений, закрытие по EOF со стороны клиента;
|
||||
- никаких утечек дескрипторов (тест проверяет закрытие).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | На каком адресе и с каким флагом сокета должен слушать сервер? — 127.0.0.1, `SO_REUSEADDR`
|
||||
- base | Сколько одновременных соединений сервер должен держать минимум? — 64
|
||||
- core | По какому событию сервер закрывает соединение с клиентом? — по EOF со стороны клиента
|
||||
|
||||
Почему дальше: раз сервер должен обслуживать до 64 соединений одним потоком без блокировки
|
||||
на каждом, нужен механизм, который скажет, какие именно дескрипторы готовы — `epoll`.
|
||||
|
||||
## Механизм: epoll, уровень vs фронт
|
||||
|
||||
`epoll` — мультиплексор ввода-вывода ядра Linux: `epoll_ctl` регистрирует дескрипторы в
|
||||
«списке интереса», который ядро хранит между вызовами; когда состояние дескриптора меняется
|
||||
(пришли данные, освободился буфер записи), ядро само кладёт его в «список готовых»;
|
||||
`epoll_wait` просто возвращает этот список. Отсюда сложность на каждый вызов пропорциональна
|
||||
числу реально готовых дескрипторов, а не общему их числу (в отличие от `select`/`poll`,
|
||||
которые каждый раз заново проходят весь набор).
|
||||
|
||||
У `epoll` два режима уведомления:
|
||||
- **LT (level-triggered, по умолчанию)** — `epoll_wait` возвращает дескриптор снова и снова,
|
||||
пока в его буфере остаются непрочитанные данные (условие «уровня» истинно). Прочитал не
|
||||
всё за один раз — на следующем `epoll_wait` дескриптор опять придёт в списке готовых.
|
||||
- **ET (edge-triggered, флаг `EPOLLET`)** — `epoll_wait` сообщает о готовности только один раз,
|
||||
в момент перехода состояния «не готов → готов» (фронт). Если не вычитать буфер целиком за
|
||||
этот раз, повторного уведомления не будет, пока не придут новые данные (новый фронт).
|
||||
|
||||
Из этого прямо следует требование к сокетам: **ET обязывает читать/писать в цикле до
|
||||
`EAGAIN`**, потому что единственный сигнал готовности уже был использован и другого не будет,
|
||||
пока состояние снова не изменится. А раз цикл идёт до `EAGAIN`, дескриптор обязан быть
|
||||
**неблокирующим** (`O_NONBLOCK`) — на блокирующем сокете последний вызов `read`/`write` в этом
|
||||
цикле, когда данных больше нет, завис бы навсегда вместо того, чтобы вернуть `-1`/`EAGAIN`.
|
||||
При LT неблокирующий режим тоже нужен по той же причине разделения потока между многими
|
||||
клиентами, но не читать «всё до EAGAIN» за раз не так опасно — событие придёт снова.
|
||||
|
||||
`EAGAIN` (синоним `EWOULDBLOCK`) — не ошибка сбоя, а сигнал «сейчас нечего делать, попробуй
|
||||
позже»; `EINTR` — вызов прерван доставкой сигнала (например, при обработке SIGTERM/SIGINT) и
|
||||
его нужно просто повторить, а не трактовать как реальную ошибку. Частичное чтение/запись
|
||||
(`read`/`write` вернул меньше байт, чем просили) — нормальное поведение сокета, а не признак
|
||||
проблемы: буфер ядра ограничен, и остаток нужно дочитывать/дописывать следующим вызовом.
|
||||
|
||||
**Ловушки**
|
||||
- Трактовать `EAGAIN` как ошибку и закрывать соединение → сервер рвёт рабочие соединения
|
||||
просто потому, что в момент проверки данные ещё не пришли → видно как случайные обрывы
|
||||
под нагрузкой, ловится логированием `errno` перед реакцией на ошибку.
|
||||
- Забыть вычитывать буфер в цикле до `EAGAIN` при `EPOLLET` → повторного уведомления не будет,
|
||||
часть данных «зависает» в буфере ядра → клиент не получает остаток эха, тест на эхо большого
|
||||
объёма данных повисает или обрезает ответ.
|
||||
- Не закрывать `fd` при ошибке/отключении клиента → утечка дескрипторов → процесс упирается
|
||||
в лимит `ulimit -n` и получает `EMFILE` на новые `accept`, видно через `lsof -p PID` или
|
||||
`/proc/PID/fd`.
|
||||
- Игнорировать частичную запись (`write` вернул меньше, чем передали) → часть эха теряется
|
||||
без ошибки → видно как обрезанный ответ клиенту при большом объёме данных за одну посылку.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что означает `EAGAIN` на неблокирующем сокете? — не ошибка, сигнал «данных пока нет, попробуй позже»
|
||||
- core | В чём разница между LT и ET режимами epoll? — LT сообщает о готовности, пока данные остаются в буфере; ET — один раз, при переходе «не готов → готов»
|
||||
- core | Почему ET требует неблокирующих дескрипторов? — цикл чтения до EAGAIN на блокирующем сокете завис бы на последнем вызове, когда данных больше нет
|
||||
- deep | Какая асимптотика у `epoll_wait` относительно select/poll? — O(1) на готовое событие против O(n) на весь набор у select/poll
|
||||
|
||||
Почему дальше: раз сервер должен «аккуратно завершаться по SIGTERM/SIGINT», нужно понимать,
|
||||
как доставка сигнала прерывает системные вызовы и как это связать с циклом `epoll_wait`.
|
||||
|
||||
## Завершение по сигналу
|
||||
|
||||
Аккуратное завершение (`exit 0`) означает: сервер должен закрыть все сокеты и выйти из цикла
|
||||
`epoll_wait`, а не просто дать процессу упасть от сигнала по умолчанию. Практический механизм —
|
||||
установить обработчик на SIGTERM/SIGINT, который выставляет флаг (`volatile sig_atomic_t`), и
|
||||
проверять этот флаг в цикле после каждого возврата из `epoll_wait` (который сам может вернуться
|
||||
с `-1`/`EINTR` из-за прихода сигнала — это нужно отличать от реальной ошибки и не падать на ней).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Каким кодом возврата должен завершаться сервер по SIGTERM/SIGINT? — 0
|
||||
- core | Что возвращает `epoll_wait`, если его прервал сигнал? — -1 с `errno == EINTR`, не ошибка выполнения
|
||||
|
||||
## Проверка
|
||||
|
||||
Запускается как самостоятельный бинарник: `./solution <порт>`.
|
||||
Проверка: `python3 grade.py 07` — тест сам поднимает сервер, открывает 3 соединения,
|
||||
льёт данные, проверяет эхо и корректное завершение по SIGTERM.
|
||||
Критерий: все `ok` + ревью кода (использование epoll, обработка ошибок).
|
||||
Проверка: `python3 grade.py 07` — тест сам поднимает сервер, открывает 3 соединения, льёт
|
||||
данные, проверяет эхо и корректное завершение по SIGTERM. Критерий: все `ok` + ревью кода
|
||||
(использование epoll, обработка ошибок).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сколько соединений открывает тест `grade.py 07`? — 3
|
||||
- base | Каким сигналом тест проверяет корректное завершение сервера? — SIGTERM
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. Почему при `EPOLLET` недостаточно один раз прочитать данные из сокета, даже если это
|
||||
покрыло все данные, которые были на момент события?
|
||||
<details><summary>Ответ</summary>Потому что ET сообщает о фронте один раз; если между этим
|
||||
чтением и следующим `epoll_wait` в сокет придут новые данные до того, как буфер опустел,
|
||||
можно потерять уведомление — правильная практика: читать в цикле, пока `read` не вернёт
|
||||
`EAGAIN`, тогда гарантированно вычитан весь буфер на момент вызова.</details>
|
||||
|
||||
2. Почему в этой задаче нельзя просто заблокироваться в `accept()` в цикле по одному клиенту?
|
||||
<details><summary>Ответ</summary>Требование — до 64 одновременных соединений одним потоком;
|
||||
блокирующий `accept`/`read` на одном соединении не даёт параллельно обслуживать остальные —
|
||||
отсюда обязательность `epoll` и неблокирующих сокетов.</details>
|
||||
|
||||
3. Чем частичная запись (`write` вернул меньше байт, чем передано) отличается от ошибки записи?
|
||||
<details><summary>Ответ</summary>Это не ошибка: буфер сокета ограничен по размеру, ядро
|
||||
приняло часть данных и вернуло, сколько именно — оставшуюся часть нужно дописать следующим
|
||||
вызовом `write`, когда `EPOLLOUT` снова покажет готовность.</details>
|
||||
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user