Открыть сервис

Блокирующий системный вызов

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

Принцип работы

Когда поток (thread) или процесс выполняет блокирующий системный вызов, ядро операционной системы проверяет, может ли запрошенная операция быть выполнена немедленно. Если нет (например, данные ещё не поступили в сокет, или файл заблокирован другим процессом), ядро переводит вызывающий поток в состояние ожидания (например, TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE в Linux). Поток удаляется из очереди готовых к выполнению процессов и помещается в очередь ожидания, связанную с конкретным ресурсом (файловым дескриптором, семафором, каналом и т.д.). Процессор переключается на выполнение другого готового потока.

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

Примеры блокирующих системных вызовов

Ввод-вывод на файлах и сокетах

  • read() и write() — при работе с файловыми дескрипторами, открытыми в блокирующем режиме (по умолчанию), вызов read() блокируется, пока данные не будут доступны (например, при чтении из пустого канала или сокета). write() может блокироваться, если буфер отправки заполнен.
  • accept() — в сетевом программировании блокируется, пока не поступит новое входящее соединение.
  • connect() — блокируется, пока не будет установлено TCP-соединение (или не произойдёт ошибка).

Ожидание процессов и сигналов

  • wait() и waitpid() — приостанавливают выполнение родительского процесса до завершения дочернего процесса.
  • pause() — блокирует процесс до получения сигнала.
  • sleep() и nanosleep() — блокируют поток на заданный интервал времени.

Синхронизация

  • sem_wait() — блокируется, пока семафор не станет положительным.
  • pthread_mutex_lock() — блокируется, пока мьютекс не будет захвачен (если не используется PTHREAD_MUTEX_ERRORCHECK или PTHREAD_MUTEX_RECURSIVE с особыми условиями).
  • futex() (fast userspace mutex) — в Linux используется для реализации блокировок в пользовательском пространстве; при конкуренции поток блокируется в ядре.

Ввод-вывод с терминала

  • Чтение из стандартного ввода (stdin) в блокирующем режиме — программа ждёт, пока пользователь не введёт строку и не нажмёт Enter.

Блокирующие и неблокирующие вызовы

Блокирующие вызовы контрастируют с неблокирующими, которые немедленно возвращают управление, даже если операция не может быть выполнена (например, read() с флагом O_NONBLOCK возвращает -1 с кодом ошибки EAGAIN или EWOULDBLOCK). Неблокирующие вызовы требуют циклического опроса (polling) или использования механизмов мультиплексирования (например, select(), poll(), epoll в Linux, kqueue во FreeBSD, IOCP в Windows).

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

Поведение в многопоточных и многопроцессных системах

В многопоточных приложениях блокирующий вызов блокирует только тот поток, который его вызвал, а не весь процесс. Другие потоки того же процесса продолжают выполняться. Это позволяет эффективно обрабатывать несколько операций ввода-вывода одновременно, не создавая отдельных процессов. Однако если заблокированный поток удерживает блокировку (например, мьютекс), другие потоки, ожидающие эту блокировку, также могут быть заблокированы, что может привести к взаимоблокировке (deadlock).

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

Прерываемые и непрерываемые блокировки

В ядре Linux блокирующие вызовы могут быть двух типов:

  • Прерываемые (TASK_INTERRUPTIBLE) — поток может быть выведен из ожидания сигналом (например, SIGINT). В этом случае системный вызов возвращает -EINTR. Это позволяет пользователю прервать длительную операцию (например, нажатием Ctrl+C).
  • Непрерываемые (TASK_UNINTERRUPTIBLE) — поток не реагирует на сигналы. Используется для критических операций, которые не должны быть прерваны (например, ожидание завершения записи на диск). Длительное нахождение процесса в таком состоянии может указывать на проблемы с драйвером или оборудованием.

Существует также состояние TASK_KILLABLE (в Linux), которое является гибридом: поток может быть прерван только сигналом SIGKILL.

Влияние на производительность

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

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

Примеры в разных операционных системах

  • Linux/Unix: системные вызовы read(), write(), open(), connect(), accept(), waitpid(), nanosleep(), futex(). Флаг O_NONBLOCK при открытии файла или сокета делает их неблокирующими.
  • Windows: функции ReadFile(), WriteFile(), WaitForSingleObject(), Sleep(), WSAAccept(). В Windows для асинхронного ввода-вывода используются перекрывающиеся операции (overlapped I/O) и порты завершения ввода-вывода (IOCP).
  • FreeBSD: аналогично Linux, но с дополнительными механизмами, такими как kqueue для мультиплексирования.

Историческая справка

Блокирующие системные вызовы появились в ранних версиях UNIX (1970-е годы) как естественный способ синхронизации с медленными устройствами ввода-вывода (терминалы, принтеры). В то время многозадачность была кооперативной, и блокировка процесса была единственным способом не тратить процессорное время впустую. С развитием вытесняющей многозадачности и многопоточности блокирующие вызовы остались основным механизмом для большинства приложений, хотя в высоконагруженных системах их всё чаще заменяют асинхронными и event-driven подходами (например, Node.js, libuv, io_uring в Linux).

Критика и ограничения

Основной недостаток блокирующих вызовов — потенциальная неэффективность при большом количестве одновременных операций ввода-вывода. В классической модели «один поток на одно соединение» (например, в ранних веб-серверах Apache) каждый блокирующий вызов создаёт поток, что приводит к большим накладным расходам на переключение контекста и потребление памяти. Это стало причиной появления неблокирующих и асинхронных моделей (nginx, Node.js, epoll).

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

Источники

  • Таненбаум Э., Бос Х. «Современные операционные системы» (4-е издание). — СПб.: Питер, 2015.
  • Стивенс У. Р. «UNIX. Профессиональное программирование» (3-е издание). — СПб.: Питер, 2018.
  • Документация ядра Linux: man-страницы read(2), write(2), futex(2), epoll(7).
  • Документация Microsoft Windows: «Synchronous and Asynchronous I/O» (MSDN).
  • Corbet J., Rubini A., Kroah-Hartman G. «Linux Device Drivers» (3rd edition). — O'Reilly Media, 2005.

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →