C Academy · Урок

Блокирующий и неблокирующий ввод-вывод

Поймите, почему важны циклы обработки событий.

Урок 1 из 413 шагов

«Блокирующий и неблокирующий ввод-вывод» — бесплатный урок C Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.

Что означает блокирующий режим

Блокирующий системный вызов приостанавливает вызывающий поток, пока операция не сможет продолжиться. Если вызвать recv() для сокета без данных, ядро усыпит поток до поступления байтов.

Такую модель легко понимать: одно соединение, один поток, последовательный код. Проблемы начинаются, когда нужно одновременно обслуживать тысячи клиентов.

ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);

Проблема масштабирования

При блокирующем вводе-выводе один зависший клиент блокирует весь поток. Классическое решение — отдельный поток или процесс для каждого соединения.

Это работает до определённого предела, но 10 000 потоков означают 10 000 стеков, частое переключение контекста и большие накладные расходы планировщика. Это знаменитая проблема C10k, подтолкнувшая серверы к событийным архитектурам.

Неблокирующий режим

Неблокирующий сокет никогда не засыпает. Если вызов не может завершиться немедленно, он сразу возвращает -1 и устанавливает для errno значение EAGAIN или EWOULDBLOCK.

Теперь ваш код должен сам повторить попытку позже. Благодаря этому один поток может обслуживать множество сокетов, не застревая ни на одном из них.

ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    /* no data right now, try again later */
}

Установка O_NONBLOCK с помощью fcntl

Чтобы перевести дескриптор в неблокирующий режим, добавьте флаг O_NONBLOCK с помощью fcntl(). Сначала всегда считывайте текущие флаги, затем добавляйте к ним этот бит операцией OR, чтобы не затереть остальные настройки.

Та же вспомогательная функция используется для прослушивающих сокетов, принятых клиентских сокетов и каналов.

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

Обработка частичных чтений

В неблокирующем вводе-выводе частичные операции становятся обычным явлением. Вызов recv() может вернуть меньше запрошенных байтов, а send() — принять только часть вашего буфера.

Необходимо отслеживать, сколько данных уже отправлено или получено, и продолжать с этого места. Никогда не предполагайте, что один вызов передаст все байты.

size_t sent = 0;
while (sent < len) {
    ssize_t w = send(fd, buf + sent, len - sent, 0);
    if (w < 0) { if (errno == EAGAIN) break; else return -1; }
    sent += w;
}

Активное ожидание — плохой подход

Наивный способ использовать неблокирующие сокеты — постоянно обходить все сокеты и повторять попытки. Такое активное ожидание загружает CPU на 100 %, даже когда ничего не происходит.

На самом деле нужно попросить ядро: «сообщи, какие дескрипторы готовы, и позволь мне спать до этого момента». Именно это и обеспечивает уведомление о готовности.

Уведомление о готовности

Мультиплексирование ввода-вывода позволяет одному потоку одновременно ждать множество дескрипторов и просыпаться, только когда готов хотя бы один из них. Ядро выполняет наблюдение за вас.

Классические интерфейсы — select() и poll(). Они работают, но при каждом вызове повторно проверяют каждый дескриптор, что при большом масштабе становится дорого.

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);

Почему select и poll не масштабируются

И select(), и poll() имеют сложность O(n): каждый вызов передаёт ядру полный набор дескрипторов, ядро сканирует их все, а затем вы снова сканируете их все, чтобы найти готовые.

select() также ограничен примерно значением FD_SETSIZE — часто 1024. Для тысяч соединений эти накладные расходы становятся доминирующими.

Появление epoll

epoll — масштабируемое решение Linux. Вы один раз регистрируете интерес к дескриптору, а ядро поддерживает внутреннюю структуру данных, отслеживающую готовность.

Каждое ожидание возвращает только действительно готовые дескрипторы, поэтому затраты зависят от активных соединений, а не от их общего числа. Благодаря этому стоимость обработки примерно равна O(1) на готовое событие.

int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */

Неблокирующий режим вместе с epoll

epoll и неблокирующие сокеты работают в связке. epoll сообщает, что дескриптор готов, а неблокирующие вызовы позволяют полностью обработать его, не переходя в ожидание.

Всегда устанавливайте O_NONBLOCK на сокетах, передаваемых epoll. Иначе ложное пробуждение или частичное чтение может заблокировать единственный поток цикла обработки событий.

set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

Мысленная модель

Представьте сервер в виде цикла: заблокироваться в epoll_wait(), получить небольшой список готовых дескрипторов, выполнить неблокирующую работу с каждым и повторить.

В простое поток спит и просыпается только для настоящей работы. Теперь один поток может эффективно обслуживать десятки тысяч соединений.

Быстрая проверка

Проверьте, насколько хорошо вы поняли работу неблокирующих сокетов.

Итоги

Блокирующий ввод-вывод прост, но требует отдельного потока для каждого соединения, что не подходит при большом масштабе. Неблокирующий ввод-вывод немедленно возвращает результат с EAGAIN, не переходя в ожидание.

Опрос сокетов в тесном цикле расходует CPU, поэтому мы используем уведомление о готовности. select/poll имеют сложность O(n), а epoll масштабируется до многих тысяч соединений. Далее мы настроим epoll.

Можно начать бесплатно

Изучай C с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
39
Уроки
144

Часто задаваемые вопросы

Урок «Блокирующий и неблокирующий ввод-вывод» бесплатный?

Да — полный текст урока «Блокирующий и неблокирующий ввод-вывод» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.

Чему я научусь в уроке «Блокирующий и неблокирующий ввод-вывод»?

Поймите, почему важны циклы обработки событий. Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C Academy?

Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Блокирующий и неблокирующий ввод-вывод»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C Academy?

Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Блокирующий и неблокирующий ввод-вывод
  2. Настройка epoll
  3. Цикл обработки событий
  4. Простой эхо-сервер
← Назад к C Academy