Что нужно для работы с файловым дескриптором (сокетом) в неблокирующем режиме в Linux?

Ответ

Чтобы работать с файловым дескриптором (чаще всего сокетом) в неблокирующем режиме в Linux, необходимо выполнить два ключевых шага:

  1. Установить флаг O_NONBLOCK для дескриптора.
  2. Обрабатывать специальные коды ошибок EAGAIN или EWOULDBLOCK при операциях ввода-вывода, которые в данном контексте не являются ошибками, а сигнализируют о "попробуй позже".

Установка неблокирующего режима:

#include <fcntl.h>
#include <unistd.h>

// Способ 1: При создании сокета
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

// Способ 2: Для уже существующего дескриптора с помощью fcntl
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);
}

Обработка операций:

#include <sys/socket.h>
#include <errno.h>

char buffer[1024];
ssize_t bytes_read = recv(sockfd, buffer, sizeof(buffer), 0);

if (bytes_read == -1) {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // Данных для чтения нет. Дескриптор не готов.
        // Не ошибка, нужно вернуться к циклу поллинга.
    } else {
        // Реальная ошибка (например, соединение разорвано)
        perror("recv failed");
    }
} else if (bytes_read == 0) {
    // Конец файла (EOF) / соединение закрыто корректно
} else {
    // Успешно прочитано `bytes_read` байт
    process_data(buffer, bytes_read);
}

Важное замечание: Работа в неблокирующем режиме сама по себе неэффективна, если постоянно опрашивать дескрипторы в цикле (busy-waiting). Для эффективного ожидания готовности множества дескрипторов необходимо использовать механизмы мультиплексирования ввода-вывода, такие как select, poll или, что предпочтительнее в Linux, epoll. Они сообщают, какие дескрипторы готовы для операций без блокировки.

Ответ 18+ 🔞

Ага, слушай сюда, про неблокирующие сокеты. Ну это ж классика, ёпта, как бутерброд с колбасой. Всё просто, как два пальца обоссать, но если не понять суть — будет тебе хитрая жопа на ровном месте.

Вот представь: твоя программа, как официант в забитом кабаке. Если он блокирующий, то подойдёт к одному столику, стоит и ждёт, пока мужик там десять минут решает, брать ему сало или нет. А остальные в это время орут «Эй, пива давай!» и дохуя злые. Так работать нельзя, терпения ноль ебать.

Задача — сделать так, чтобы официант носился как угорелый, ко всем подбегал, и если кто-то не готов (типа «я ещё думаю»), то он сразу летит к следующему. Вот это и есть неблокирующий режим.

Как это делается, блядь?

Первое — надо сказать системе: «Слушай, чувак, я не буду тут стоять и ждать, если ты мне ничего не дашь». Делается это флагом O_NONBLOCK.

// Вариант раз — сразу при рождении сокета, чтоб с пелёнок неблокирующий
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

// Вариант два — если сокет уже живёт своей жизнью, а ты вдруг решил
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(), а данных нет. Раньше бы программа встала колом и ждала, пока кто-то что-то пришлёт. А теперь что? Теперь он сразу тебе вернёт управление и скажет: «Ни хуя себе, братан, данных нет».

Но как он это скажет? А вот через магические коды ошибок EAGAIN или EWOULDBLOCK. Это не ошибки на самом деле, это такие вежливые отмазки: «Извини, чувак, я бы рад, но пока не готов, зайди позже».

char buffer[1024];
ssize_t bytes_read = recv(sockfd, buffer, sizeof(buffer), 0);

if (bytes_read == -1) {
    // Вот тут главная проверка, вся соль!
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // Всё ок, просто данных нет. Не дергайся.
        // Иди проверяй другие сокеты или поспи немного.
    } else {
        // А вот это уже пиздец, реальная ошибка.
        // Соединение отвалилось, или ещё какая хрень.
        perror("recv failed");
    }
} else if (bytes_read == 0) {
    // Клиент вежливо отсоединился. Ну и хуй с ним.
} else {
    // Ура, прочитали! Работай с данными.
    process_data(buffer, bytes_read);
}

И вот тут главный подвох, на котором все спотыкаются, я тебя предупреждаю. Если ты просто в цикле будешь дергать recv() на сотне сокетов и каждый раз получать EAGAIN — твоя программа сожрёт овердохуища процессорного времени, просто тупо проверяя, не появилось ли чего. Это как официант, который каждую секунду подбегает к каждому столику и спрашивает: «Ну чё, решил? Нет? А сейчас?»

Так делать — пиздопроебибна идея. Для этого умные люди придумали системы оповещения: select, poll и король всех — epoll. Они сидят, следят за всеми твоими сокетами, и как только какой-то из них готов к чтению или записи — они тебе кричат: «Эй, иди к третьему столику, там мужик уже заказ сделал!». И ты идешь и работаешь только с теми, кто готов. Эффективность — ебать копать.

Короче, алгоритм такой:

  1. Включи неблокирующий режим.
  2. Смирись с тем, что EAGAIN — это норма жизни, а не ошибка.
  3. НИКОГДА не работай без epoll/poll/select, иначе твой ЦПУ сгорит, а программа будет тормозить как хуй в пальто.
  4. Профит.

Всё, понял? Работай.