62. Как Go runtime взаимодействует с ОС
Почему Go нужна своя среда выполнения
В отличие от скриптовых языков (PHP, Python, Ruby), работающих поверх тяжелых внешних интерпретаторов, или платформ JVM/.NET, требующих массивных виртуальных машин, язык Go компилируется непосредственно в нативный машинный код целевого процессора. Однако скомпилированный файл на Go — это не «голый» двоичный код, общающийся напрямую с кремнием. В каждый исполняемый бинарник компилятор встраивает развитую, высокоэффективную среду выполнения — Go Runtime (runtime).
Зачем современному языку программирования нужна собственная встроенная среда выполнения, если операционная система уже предоставляет богатый набор примитивов: системные потоки (pthread), страничную виртуальную память, файловые дескрипторы и сокеты?
Дело в том, что системные примитивы ядра Linux проектировались как универсальные, но относительно «тяжелые» строительные блоки. В сценариях экстремальных нагрузок (сотни тысяч одновременных WebSocket-соединений, микросервисные RPC-вызовы, обработка миллионов сетевых запросов в секунду) прямое использование механизмов ОС наталкивается на жесткие физические барьеры:
- Дороговизна переключения контекста (Context Switch): переключение физических потоков ядра требует перехода процессора в привилегированный Ring 0, сброса конвейера инструкций, сохранения десятков регистров и инвалидации строк кэшей процессора и таблиц TLB (тысячи тактов CPU на каждое переключение).
- Фрагментация памяти (Memory Fragmentation): частые вызовы классических функций
malloc/freeиз стандартной библиотеки C (glibc) создают блокировки на уровне аллокатора и фрагментируют кучу. - Исчерпание лимитов потоков ядра (Thread pool exhaustion): создание 100 000 системных потоков ОС парализует ядро Linux и мгновенно упрется в лимиты памяти, ведь стек каждого системного треда по умолчанию занимает от 2 до 8 МБ.
Go Runtime решает эту фундаментальную задачу, выступая интеллектуальным посредником между абстракцией вашего кода и операционной системой. Он создает легковесную управляемую среду прямо поверх ядра Linux, даруя разработчику элегантную иллюзию миллионов одновременно работающих микропотоков — горутин.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph UserCode ["Пространство кода разработчика"]
Goroutines["Сотни тысяч горутин: go worker()"]
Channels["Каналы и мьютексы: chan, sync.Mutex"]:::accent
NetConn["Сетевые соединения: net.Conn, http.Server"]:::network
end
subgraph GoRuntime ["Встроенная среда выполнения (Go Runtime)"]
GMP["Планировщик G-M-P (User-space context switch)"]
MemAlloc["Аллокатор памяти (TCMalloc-style, mheap, mcache)"]
NetPoller["Сетевой поллер netpoller (epoll / kqueue / IOCP)"]
SignalHandler["Диспетчер сигналов (SIGURG, SIGSEGV)"]
end
subgraph LinuxKernel ["Ядро Linux (Kernel Space)"]
Pthreads["Потоки ядра: pthread (task_struct)"]
VMSubsystem["Виртуальная память: mmap, mprotect, madvise"]:::system
EpollCore["Подсистема epoll_wait и сокеты"]:::process
HardwareCPU["Физические ядра процессора (CPU Cores)"]
end
Goroutines --> GMP
Channels --> GMP
NetConn --> NetPoller
GMP --> Pthreads
MemAlloc --> VMSubsystem
NetPoller --> EpollCore
Pthreads --> HardwareCPU
Модель планирования G-M-P: Мост между горутинами и потоками ОС
Ядро Linux ничего не знает о существовании горутин. Для планировщика ядра Linux (CFS или EEVDF) единицей планирования является исключительно поток операционной системы (pthread, представленный структурой ядра task_struct). Чтобы эффективно связать легковесный мир Go с тяжеловесным миром ОС, рантайм использует элегантную модель G-M-P:
| Сущность | Аналог в ядре ОС | Назначение и физический смысл |
|---|---|---|
g (goroutine) |
Поток в стеке ядра / легковесный поток | Контекст выполнения горутины: динамический стек (начиная всего с ~2 КБ), указатели инструкций (PC), указатели стека (SP), статус (Gwaiting, Grunnable, Grunning). |
m (machine) |
pthread / sched_getaffinity |
Реальный системный поток ОС, созданный ядром через clone(2). Исполняет физический машинный код на процессорном ядре. |
p (logical processor) |
Виртуальное ядро / SMP |
Логический процессор (контекст выполнения), предоставляющий ресурсы для исполнения кода Go: локальная очередь готовых горутин (runq), кэш аллокатора памяти, счетчик тиков. |
Планировщик Go (runtime.schedule) работает исключительно в User Space (пространстве пользователя). Когда одна горутина уступает место другой (например, при чтении из канала или системном вызове), Go не обращается к ядру через системные вызовы syscall.
Вместо этого рантайм вызывает собственные низкоуровневые процедуры: gogo написана на ассемблере (src/runtime/asm_*.s), а gopark и goready — обычные функции на Go (src/runtime/proc.go). Эти процедуры напрямую перезаписывают регистры процессора: меняют указатель вершины стека RSP и адрес следующей инструкции RIP. Это обеспечивает zero-cost context switch: переключение между горутинами занимает порядка сотни наносекунд (в замерах около 140 нс на переключение через канал) против примерно микросекунды и выше при переключении потоков ядра!
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph LogicalP ["Логический процессор (P)"]
RunqHead["Локальная очередь: runq (до 256 горутин)"]
SchedTick["Счетчик тиков: schedtick"]:::process
MCache["Локальный кэш аллокатора: mcache"]:::data
end
subgraph OSThread ["Системный поток (M)"]
M_Thread["Поток ОС (pthread / task_struct)"]
CurrentG["Текущая исполняемая горутина (g)"]
end
subgraph KernelScheduler ["Ядро Linux"]
CPU_Core["Физическое ядро CPU (SMP / Affinity)"]
end
RunqHead -->|"Следующая g"| CurrentG
LogicalP <-->|"Связан с"| M_Thread
CurrentG -->|"Машинные инструкции"| M_Thread
M_Thread -->|"Выполняется на"| CPU_Core
Управление памятью: mmap, mprotect и madvise под капотом
В отличие от программ на языке C, где аллокатор glibc malloc исторически двигал границу сегмента данных процесса через системные вызовы brk/sbrk, рантайм Go полностью отказывается от устаревших интерфейсов и управляет адресным пространством исключительно через современный системный вызов mmap:
- Резервирование виртуального пространства (Virtual Reservation):
При старте процесса функция аллокатора GosysAllocвызывает системный вызовmmapс флагамиMAP_ANONYMOUS | MAP_PRIVATEи правамиPROT_NONE. Рантайм мгновенно резервирует колоссальный диапазон виртуальных адресов (гигабайты адресного пространства), но не запрашивает физическую память RAM. Это действие бесплатно для физической оперативной памяти — ядро лишь регистрирует диапазон в структурахvm_area_struct. - Ленивый коммит физических страниц (Page Commit):
По мере того как сервису требуются реальные блоки памяти для новых объектов, рантайм делает вызовmprotect(addr, size, PROT_READ | PROT_WRITE). Только при первом физическом обращении процессора к этим адресам аппаратный блок MMU генерирует мягкое исключение Minor Page Fault, и ядро выделяет реальные физические страницы ОЗУ, настраивая записи вPage Table. - Возврат физической памяти операционной системе (Memory Reclamation):
Когда сборщик мусора Go (GC) освобождает неиспользуемые объекты, рантайм не выполняет системный вызовmunmap(который привел бы к разрушению виртуальных диапазонов). Фоновый сборщик мусора (scavenger) возвращает физические страницы ядру через вызовmadvise(addr, size, MADV_DONTNEED). В Linux этот вызов немедленно освобождает физические фреймы RAM и уменьшает RSS процесса (начиная с Go 1.16 рантайм вернулся кMADV_DONTNEEDпо умолчанию вместо отложенногоMADV_FREE, чтобы RSS в мониторинге отражал реальное потребление). При следующем обращении ядро прозрачно предоставит чистую страницу.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph GoAlloc ["Аллокатор Go Runtime"]
SysAlloc["sysAlloc(): резервирование виртуальных адресов"]
PageCommit["Коммит под объекты: mprotect()"]
Scavenger["Фоновый Scavenger (GC): освобождение страниц"]
end
subgraph KernelVM ["Подсистема виртуальной памяти Linux"]
MmapReserve["mmap(MAP_ANONYMOUS, PROT_NONE): запись в VMA"]
PhysicalRAM["Minor Page Fault: реальная память в Page Table"]:::data
MadviseReclaim["madvise(MADV_DONTNEED / MADV_FREE): ленивый сброс RAM"]
end
SysAlloc --> MmapReserve
PageCommit --> PhysicalRAM
Scavenger --> MadviseReclaim
Обработка блокирующих вызовов и парковка горутин
Что происходит, когда горутина инициирует длительную блокирующую операцию на уровне операционной системы — например, чтение большого файла с диска (os.File.Read) или синхронное подключение к сокету базы данных (net.Dial)?
Поскольку ядро Linux работает с потоками ОС, системный тред m, на котором выполнялась данная горутина, физически засыпает внутри ядра (блокируется в системном вызове). Если бы планировщик Go ничего не предпринимал, то логический процессор p остался бы парализованным вместе с этим тредом, а сотни остальных готовых к исполнению горутин замерли бы в очереди!
Чтобы предотвратить блокировку всей системы, Go Runtime применяет отточенный механизм Hand-off (передача процессора) и Parker (парковка горутин):
- Вход в системный вызов: Перед выполнением потенциально блокирующего вызова рантайм проверяет флаги горутины (
g.lockedm != 0) и вызывает служебную процедуруentersyscall. - Отсоединение логического процессора (Hand-off): Планировщик временно отвязывает (
revoke) логический процессорpот заблокированного потокаm. Тредmуходит в глубокий сон внутри ядра, удерживая только заблокированную горутинуg. - Спасение очереди горутин: Освободившийся процессор
p(его отбирает фоновыйsysmonчерезretakeлибо сам поток передаёт при заведомо блокирующем вызове) подхватывается другим свободным потокомmиз пула спящих тредов (либо рантайм порождает новый поток: через системный вызовclone, а в бинарнике с cgo — черезpthread_create), и выполнение оставшихся горутин из очередиrunqмгновенно продолжается на другом ядре CPU! - Пробуждение и возвращение в строй: Когда системный вызов на диске завершается, поток
mпросыпается и вызывает процедуруexitsyscall. Рантайм пытается вернуть потокmк его родному процессоруp. Если процессор занят, горутинаgпереводится в статусGrunnableи отправляется в глобальную очередь задач, а освободившийся потокmпаркуется в пул ожидания.
sequenceDiagram
autonumber
participant G1 as Горутина 1 (g1)
participant M1 as Поток ОС (m1)
participant P as Логический процессор (p)
participant Kernel as Ядро Linux (VFS / Disk)
participant M2 as Новый/Спящий поток (m2)
G1->>M1: os.File.Read() (Блокирующий ввод-вывод)
M1->>Kernel: sys_read(fd, buf, size)
Note over M1,Kernel: Поток m1 блокируется в пространстве ядра
M1->>P: entersyscall: отсоединение (revoke) процессора p
P->>M2: Hand-off: процессор p пересаживается на поток m2
M2->>P: Запуск горутины g2 из локальной очереди runq!
Note over Kernel: Диск прочитал данные, sys_read завершен
Kernel-->>M1: Возврат данных в m1
M1->>G1: exitsyscall: горутина g1 готова (Grunnable)
M1->>P: Попытка вернуть процессор p (или сброс g1 в глобальную очередь)
Сетевой поллер (netpoll) и мультиплексирование IO
Сетевой ввод-вывод в Go радикально отличается от дискового: Go никогда не блокирует поток операционной системы m на сетевых сокетах!
Вместо создания отдельного системного потока на каждое сетевое соединение, рантайм Go реализует реактивную событийно-ориентированную модель (Event-Driven I/O) на базе встроенного сетевого поллера — netpoll:
- В Linux: нативный интерфейс
epoll(epoll_create,epoll_ctl,epoll_wait). - В FreeBSD и macOS: подсистема
kqueue(kqueue,kevent). - В Windows: порты завершения ввода-вывода
IOCP(I/O Completion Ports).
При создании слушателя net.Listener или установке исходящего соединения (net.Dial, net.Dialer) рантайм Go переводит сетевой файловый дескриптор в неблокирующий режим с помощью системного вызова syscall.SetNonblock(fd, true) и регистрирует этот fd в дескрипторе epoll.
Когда горутина вызывает метод чтения netFD.Read/Write, а в сетевом буфере сокета ядра еще нет данных:
- Системный вызов возвращает ошибку
EAGAINилиEWOULDBLOCK. - Go Runtime переводит текущую горутину в состояние ожидания
Gwaitingи паркует ее с помощью функции рантаймаgopark. - Поток ОС
mостается абсолютно свободным и немедленно берет на исполнение следующую готовую горутину из очереди! - Специальный системный поток или фоновый цикл планировщика вызывает системный вызов
epoll_wait. Как только сетевая карта принимает сетевые пакеты TCP, ядро пробуждает поллер. - Поллер находит спящую горутину в структуре кэша дескрипторов
pollCacheи с помощью вызоваgoreadyпереводит ее в состояниеGrunnable, возвращая в очередь выполнения. Для разработчика код выглядит кристально чистым и последовательным:data, err := conn.Read().
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Client["Внешний TCP клиент"]:::entry -->|"TCP Data Packet"| SocketFD["Сетевой сокет (fd non-blocking)"]
SocketFD -->|"epoll_ctl (EPOLLIN)"| EpollEngine["Сетевой поллер netpoll (epoll)"]
EpollEngine -->|"epoll_wait возвращает готовый fd"| M_Worker["Поток ОС (m)"]
M_Worker -->|"goready(g)"| RunnableQueue["Горутина g переведена в Grunnable"]
RunnableQueue -->|"conn.Read() успешно завершен"| UserLogic["Бизнес-логика Go"]
Обработка сигналов и пробуждение планировщика
Go Runtime регистрирует собственные обработчики системных сигналов операционной системы прямо при старте процесса, используя низкоуровневый вызов rt_sigaction:
| Сигнал ядра | Роль и назначение в архитектуре Go Runtime |
|---|---|
SIGURG |
Асинхронная вытесняющая многозадачность (Non-cooperative preemption). Начиная с версии Go 1.14, планировщик Go не ждет, пока горутина добровольно вызовет функцию или выделит память в бесконечном цикле. Фоновый тред sysmon детектирует зависшую горутину и посылает потоку m сигнал SIGURG. Обработчик сигнала ядра прерывает поток и принудительно переключает контекст регистров, спасая приложение от зависания! |
SIGSEGV / SIGBUS |
Аппаратные сбои адресации памяти. Перехватываются рантаймом и транслируются в контролируемую Go-панику (panic: runtime error) через функцию sigpanic. |
SIGUSR1 / SIGUSR2 |
Сигналы межпоточного взаимодействия и системной отладки, а также обработка специфических состояний вроде переполнения стека горутины SIGSTKFLT. |
SIGPOLL / SIGIO |
netpoll на современных платформах не является сигнальным. Пробуждение происходит через возврат из epoll_wait / kevent / IOCP. Сигналы (SIGIO/SIGPOLL) используются Go крайне редко (в основном для legacy-совместимости или специфичных BSD-путей) и не являются механизмом event-loop и не требуют постоянных опросов (polling). |
SIGWINCH |
Сигнал изменения геометрии окна терминала, перехватываемый стандартной библиотекой для корректного обновления размера буфера консольного вывода os.Stdout. |
Mechanical Sympathy: Влияние на производительность и кэш CPU
Глубокое понимание физического взаимодействия среды выполнения Go с операционной системой открывает путь к максимальной производительности:
- Стоимость переключения контекста:
Переключение легковесных горутинg -> gв пространстве пользователя требует всего ~100–300 наносекунд. Переключение системных тредовm -> mчерез ядро Linux обходится в 1000–5000 наносекунд, сопровождаясь сбросом конвейера и инвалидацией записей TLB. Именно поэтому рантайм Go стремится минимизировать число системных потоковm, жестко балансируя их черезGOMAXPROCS. - Локальность процессорных кэшей (Cache Locality):
Горутины, привязанные к локальной очереди одного логического процессораp, исполняются на одном и том же физическом потокеmи делят между собой сверхбыстрый кэш L1/L2 одного процессорного ядра. Это радикально сокращает число промахов кэша (cache miss) по сравнению с архитектурами, где задачи хаотично разбрасываются по разным ядрам процессора. - Архитектура NUMA и привязка к сокетам:
На многосокетных серверах с архитектурой неравномерного доступа к памяти (NUMA) рантайм Go не распределяет память по узлам автоматически. Однако грамотный SRE-инженер может использовать системные утилиты Linuxtasksetилиnumactl, привязывая группы потоков Go-процесса к конкретным NUMA-нодам и исключая медленные межсокетные транзакции по шине QPI/UPI. - Сборка мусора и системные аллокации:
Во время работы фазы маркировки GC рантайм сканирует указатели памяти. Если процессу не хватает адресного пространства или ядро блокирует выделение виртуальных страницmmap, рантайм генерирует неустранимую фатальную ошибкуfatal error: cannot allocate memory, даже если на сервере остается свободная физическая RAM.
Типичные вопросы на собеседованиях (Middle+/Senior)
- Как Go обрабатывает блокирующие системные вызовы, не блокируя весь процесс?
Ответ: Планировщик использует механизм Syscall Hand-off. Перед вызовом ядровогоsys_readрантайм помечает, что потокmушёл в системный вызов (entersyscall). Если вызов затягивается (больше одного тикаsysmon, и при этом есть готовые горутины или нет свободныхP, либо прошло ~10 мс),sysmonотбираетpу потокаm; для заведомо блокирующих вызововpотдаётся сразу. Тредmзасыпает в ядре, а освободившийсяpподхватывается другим потоком из пула, продолжая исполнять горутины. При возврате из syscall горутина возвращается в очередь планирования. Если свободных потоковmнет, рантайм создаёт новый (черезclone, а в бинарнике с cgo —pthread_create). - Почему RSS Go-процесса не падает сразу после вызова
runtime.GC()?
Ответ: Вызовruntime.GC()освобождает объекты внутри Go heap, но не возвращает страницы ядру ОС автоматически в ту же миллисекунду. Возвратом физических страниц ОС занимается фоновый процесс очистки (scavenger), периодически вызывающийmadvise(addr, size, MADV_DONTNEED). Кроме того, Go может повторно использовать свободные spans для новых аллокаций. Принудительно вернуть освобожденные физические страницы ядру можно вызовом функцииdebug.FreeOSMemory(). - Чем концептуально
GOMAXPROCSотличается от реального количества ядер процессора?
Ответ: ПеременнаяGOMAXPROCSзадает количество логических процессоровp(контекстов выполнения Go), способных одновременно исполнять пользовательский Go-код. Количество системных потоков ОСmпри этом может быть значительно большеGOMAXPROCS(до 10 000) из-за потоков, заблокированных в дисковом вводе-выводе или вызовах CGo. - Как сетевой поллер
netpollзащищается от проблемы Thundering Herd (эффекта громогласного стада) при работе сepoll?
Ответ: Go регистрирует сетевые дескрипторы один раз в режиме триггера по фронту (EPOLLET). Сетевой поллер централизованно опрашивает сокеты черезepoll_wait(в планировщике или потокеsysmon) и извлекает готовые события. Пробуждение и распределение горутин происходит в userspace через структуруpollDescс атомарными флагами и вызовомgoready(). Благодаря edge-triggered режиму исключаются лишние пробуждения тредов и постоянные вызовыepoll_ctl(EPOLL_CTL_MOD).
Итог
- Go Runtime как архитектурный мост: Рантайм Go — это высокопроизводительная виртуальная машина пространства пользователя, превращающая грубые системные интерфейсы Linux в сверхлегковесные и безопасные абстракции.
- Планировщик G-M-P: Осуществляет переключение контекста между горутинами (
g) на уровне регистров CPU за порядка сотни наносекунд без системных вызовов в ядро Linux. - Виртуальная память нового поколения: Go отказывается от
brkиmalloc, резервируя гигабайты адресного пространства черезmmapи лениво возвращая страницы операционной системе через системный вызовmadvise(MADV_DONTNEED). - Сетевой поллер
netpoll: Интегрируется с низкоуровневыми селекторами ядра (epoll,kqueue,IOCP), исключая блокировку потоков ОС на сетевых сокетах. - Асинхронная вытесняющая многозадачность: Сигнал
SIGURGгарантирует, что даже непрерывные циклы без вызовов функций не смогут заблокировать планировщик Go.
Мы изучили архитектурный союз операционной системы и рантайма Go. Теперь настало время подняться на самую вершину и взглянуть на весь пройденный путь целиком. В финальной лекции второго модуля мы сведем все воедино: кремний процессора, виртуальную память, системные вызовы, планировщик и сеть — в цельную инженерную картину мира Go-разработчика.
Переходим к грандиозному финалу: 63. Итоги раздела. Картина ОС целиком для Go-разработчика.