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

Куда идет поток после SysCall?

2.0 Middle🔥 191 комментариев
#Основы Go

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

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

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

Отличный вопрос, который касается самой сути взаимодействия пользовательского кода, ядра операционной системы и планировщика Go. Поток выполнения после системного вызова (SysCall) идет по сложному пути, который в Go имеет свою уникальную специфику из-за модели M:N (много потоков на много горутин) и собственного планировщика.

Общая концепция: переход между пространствами

Сначала вспомним общую теорию:

  1. Пользовательский режим (User Mode): Выполняется код приложения (в нашем случае — рантайм Go и программа). Доступ к ресурсам ограничен.
  2. Режим ядра (Kernel Mode): Выполняется код ОС. Полный доступ к оборудованию и ресурсам.
  3. SysCall — это контролируемый и синхронный ворота для перехода из пользовательского режима в режим ядра для выполнения привилегированной операции (чтение файла, работа с сетью, выделение памяти и т.д.).

После завершения операции ядром, поток должен вернуться в пользовательское пространство.

Стандартная ситуация в классических потоках (Pthreads)

В классической модели 1:1 (один поток ОС на один поток приложения) всё относительно просто:

  1. Поток (thread) делает syscall (например, read()).
  2. Происходит переключение контекста в ядро, поток блокируется на уровне ядра, пока операция не завершится (данные не поступят в сокет).
  3. Когда данные готовы, ядро помечает поток как готовый к выполнению.
  4. Планировщик ядра ОС в какой-то момент выделяет этому потоку CPU, и выполнение продолжается с точки сразу после syscall.

Здесь поток ОС и поток приложения — это одно целое, и им управляет планировщик ОС.

Специфика Go: кооперативный планировщик и неблокирующие вызовы

Модель Go M:N вводит промежуточные сущности:

  • G (Goroutine): Горутина, логическая единица выполнения.
  • M (Machine): Поток ОС (Machine), который непосредственно выполняет код.
  • P (Processor): Логический процессор, контекст планировщика. Хранит локальную очередь готовых к выполнению горутин.

Ключевая цель планировщика Go — не допустить блокировки M (потока ОС) на syscall, потому что это лишило бы возможности выполнять другие горутинки, привязанные к тому же P.

Детальный путь потока после Syscall в Go

Вот что происходит, когда горутина выполняет системный вызов (например, через сетевой запрос):

  1. Вход в Syscall. Горутина (G) на потоке M переходит в состояние Gsyscall. P, с которым был ассоциирован этот M, понимает, что его поток ушел в ядро.

  2. Отделение P от M. Планировщик Go отсоединяет (hand off) P от заблокированного в syscall M. Теперь P свободен и может быть использован для выполнения других горутин.

  3. Два возможных пути для M:

    *   **Блокирующий Syscall** (например, файловый ввод/вывод на обычном файле): Поток **M** блокируется на уровне ядра ОС. Он будет "спать", пока ядро не разбудит его готовыми данными.
    *   **Неблокирующий Syscall / Асинхронный ввод-вывод**: Go максимально использует неблокирующие интерфейсы (например, `epoll` в Linux). Здесь поток **M** формально делает syscall, но не блокируется навсегда. Он может быть использован для других задач.

  1. Что делает P? Отсоединенный P переходит в глобальную очередь или сразу ищет другой свободный поток M (или создает новый), чтобы продолжить выполнение горутин из своей локальной очереди. Это гарантирует, что блокировка одной горутины на I/O не останавливает работу всего приложения.

  2. Завершение Syscall и возвращение G. Когда системный вызов завершается (данные получены), происходит обратный переход из ядра.

    *   Заблокированный **M** просыпается.
    *   Он пытается вернуть свою горутину **G** обратно в выполнение. Для этого ему нужно получить контекст **P**.
    *   **M** пытается "занять" любой свободный **P** (не обязательно свой старый).
    *   Если свободного **P** нет, **G** помечается как готовная к выполнению, переходит в состояние `Grunnable` и помещается в глобальную очередь планировщика. Поток **M** при этом останавливается и ждет работы (или уходит в сон).

  1. Продолжение выполнения G. В конце концов, какой-то P на каком-то M достанет эту горутину G из очереди и начнет её выполнение с точки, следующей за syscall.

Ключевое отличие и преимущество

// Пример: сетевой HTTP-запрос в Go.
// Под капотом это syscall (например, через netpoll).
resp, err := http.Get("https://example.com")
// В момент ожидания ответа syscall, поток M, на котором выполнялась эта горутина,
// был освобожден для выполнения других горутин. P не простаивал.
// Когда ответ пришел, горутина была перепланирована на любой свободный M/P.
process(resp)

Главный вывод: В отличие от классических потоков, где после syscall поток ОС всегда продолжает выполнение того же потока приложения, в Go поток ОС (M) и горутина (G) на время syscall разделяются. Поток выполнения (G) после syscall попадает обратно в планировщик Go, который ставит его в очередь, и он будет выполнен на первом доступном потоке ОС (M), связанном со свободным контекстом (P). Это позволяет эффективно обрабатывать десятки тысяч одновременных операций ввода-вывода на небольшом числе реальных потоков ОС.

Таким образом, поток идет не "прямо назад" в код горутины, а сначала в логику планировщика Go, который решает, где и когда его возобновить, обеспечивая высокую конкурентность.

Куда идет поток после SysCall? | PrepBro