← Назад к вопросам

Как реализуются блокирующие системные вызовы?

2.7 Senior🔥 91 комментариев
#Конкурентность и горутины#Операционные системы и Linux

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализация блокирующих системных вызовов в Go

Блокирующие системные вызовы в Go реализуются через взаимодействие рантайма Go с операционной системой, при этом они интегрированы в модель конкурентности на основе горутин (goroutines), а не потоков ОС. Вот детальный разбор процесса.

Ключевые компоненты рантайма Go

  • G (Goroutine): Легковесная сущность, планируемая рантаймом Go.
  • M (Machine): Поток ОС (kernel thread), на котором исполняется код.
  • P (Processor): Логический процессор, связывающий M и ожидающие G.
  • Сетевой поллер (netpoller): Внутренний механизм Go для асинхронного ввода -вывода.

Типичный жизненный цикл блокирующего вызова

Рассмотрим пример чтения из файла или сетевого сокета:

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, _ := net.Dial("tcp", "example.com:80")
    buf := make([]byte, 1024)

    // Этот вызов Read является блокирующим на уровне горутины
    n, err := conn.Read(buf)
    fmt.Printf("Read %d bytes\n", n)
}

Детальный механизм работы

  1. Инициирование вызова:
    Когда горутина выполняет блокирующую операцию (например, `conn.Read()`), рантайм Go сначала проверяет, доступны ли данные немедленно.

  1. Перевод в режим ожидания:
    Если данные недоступны (например, в сокете нет данных для чтения), горутина **не блокирует поток ОС (M)**. Вместо этого:
    *   Горутина (G) переводится в состояние `waiting`.
    *   Дескриптор файла или сокета регистрируется в **сетевом поллере** рантайма.
    *   Текущий поток ОС (M) освобождается и может быть использован для выполнения других готовых горутин.

  1. Использование неблокирующего ввода-вывода ОС:
    На уровне ОС рантайм Go использует **асинхронные механизмы**:
    *   **epoll** (Linux)
    *   **kqueue** (BSD/macOS)
    *   **IOCP** (Windows)
    Эти механизмы позволяют рантайму получить уведомление от ядра ОС, когда файловый дескриптор готов к операции, без постоянного активного ожидания.

  1. Пробуждение горутины:
    Когда данные становятся доступны (например, пришли по сети), сетевой поллер получает уведомление от ОС. Затем:
    *   Рантайм находит горутину, ожидающую этот дескриптор.
    *   Переводит её в состояние `runnable`.
    *   Помещает в **очередь исполнения** одного из процессоров (P).

  1. Возобновление выполнения:
    Когда освобождается логический процессор (P) и поток ОС (M), горутина возобновляет выполнение с точки блокирующего вызова, получая данные или ошибку.

Пример с файловой системой

Для блокирующих операций с файлами (через os.File) механизм похож, но с важной оговоркой:

package main

import (
    "os"
)

func main() {
    f, _ := os.Open("data.txt")
    buf := make([]byte,)`Обратите внимание, что в исходном сообщении пользователя код был обрезан. Я завершу его правильно для целостности примера.`

    buf := make([]byte, 1024)

    // Для файловых операций Go может использовать отдельные потоки ОС
    n, err := f.Read(buf)
}

Поскольку не все файловые системы поддерживают асинхронный ввод-вывод, рантайм Go может:

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

Преимущества подхода Go

  • Высокая масштабируемость: Один поток ОС может обслуживать тысячи блокирующих горутин.
  • Эффективное использование ресурсов: Не создаются тысячи потоков ОС для тысячи одновременных операций ввода-вывода.
  • Естественная модель программирования: Код выглядит как синхронный, но работает асинхронно "под капотом".

Сравнение с традиционными потоками

// В традиционных языках (Java/C++) блокирующий read() блокирует поток ОС:
// thread.performBlockingRead() -> поток ОС переходит в состояние ожидания

// В Go блокирующий Read() блокирует только горутину:
// goroutine.performBlockingRead() -> горутина ждет, поток ОС свободен

Таким образом, блокирующие системные вызовы в Go реализованы через гибридную модель: для разработчика они выглядят как синхронные блокирующие операции, но рантайм преобразует их в асинхронные на уровне ОС, интегрируя в собственную эффективную модель планирования горутин. Это позволяет сочетать простоту синхронного кода с производительностью асинхронных систем.

Как реализуются блокирующие системные вызовы? | PrepBro