Пишем код на GO под Linux

Язык ядра Linux, его модулей и утилит написаны на языке C. Хоть он и является старым языком и прародителем многих других, но его до сих пор используют. В экосистему линукса постепенно проникают и более молодые языки — например, Rust. Но сегодня мы поговорим об детище Google — GoLang.

Я много пишу про этот замечательный язык и в этой статье предлагаю изучить основы системного программирования на Go, мы изучим как работать с ядром, юзерспейсом линукса. Расскажу об стандарте POSIX, а также узнаем, как сочетать C и Go-код.

Пишем код на GO под Linux

Go, или Golang — это компилируемый многопоточный язык программирования с открытым исходным кодом. Довольно часто его применяют в веб-сервисах, клиент-серверных приложений и других вещей, связанных с сетью. Например, на нем можно писать микросервисы. Также его используют для создания небольших CLI-утилит.

Go — это детище Google, они пытались создать язык программирования, в котором была и легкость изучения Python, и скорость работы C/C++. Они сделали его компилируемым, но у него есть редко используемый интерпретатор. Также разработчики избегали сделать Go тяжеловесным как C++. Также одна из целей была — отобразить в языке возможность параллельных вычислений в многопроцессорных (SMP, многоядерных) системах.

Язык специально сделан так, чтобы сфокусировать внимание разработчика на архитектуре приложений, а не на, например, отслеживание устаревшего синтаксиса.

При упоминании Go, чаще всего всплывают следующие преимущества:

  • Автоматическое управление памятью и сборщик мусора. Go может выдерживать большие нагрузки и имеет высокую производительность как C++, но моменты управления памятью в нем опущены на компилятор.
  • Syntax Sugar (синтаксический сахар) — небольшие послабления, которые позволяет писать код быстрее и короче. Не обязательно везде ставить точку с запятой, немного можно упростить операции.
  • Автоформатирование кода. Компилятор сам расставляет отступы с помощью gofmt. Но важно использовать табуляцию.
  • Автоматическое создание мануалов. Если вы не хотите заморачиваться над созданием документации — можно использовать godoc. Он найден все комментарии к коду и сделает из них мануал.
  • Отслеживание устаревших конструкций. Инструмент gofix сканирует код на устаревшие стандарты и предлагает их исправить.
  • Инструменты тестирования — в Go включено множество инструментов тестирования — от банальных проверок соответствия типов до рекомендаций и исправлений на основе официальной документации
  • Отслеживание состояния гонки — когда язык многопоточный, требуется следить за потоками, чтобы одна функция не смогла выполниться впереди другой. Есть дополнительные инструменты для включения детектора гонки.
  • Профилирование — в Go есть пакет pprof и утилита go tool pprof
  • Низкоуровневое программирование — да-да, Go все таки может работать непосредственно с памятью, и существует пакет unsafe .
  • Кроссплатформеность — поддержка Go осуществляется для Linux, Windows, MacOS и даже Free и Open BSD систем, а также для разных процессорных архитектур.
  • Горутины — это функции, которые способны работать параллельно, асинхронно.

Хоть и Go относительно молодой язык программирования, он уже успел стать популярным инструментом для создания ПО. Например, тот же Docker, которым вы с 99% шансом пользовались, написан на Go. В 2009 стал языком года по версии TIOBE. В каждом дистрибутиве линукса есть огромное количество библиотек для Go.

Еще одной отличительной способностью Go является быстрота исполнения программы — чаще всего даже быстрее языка C.

Связь с C-кодом (CGO)

Программы на Go могут непосредственно использовать C-код. Для этого существует CGO. Для его использования пишется обычный Go-код, но тот, который импортирует псевдо пакет "C". Go-код после этого может ссылаться к типам из языка C — C.size_t, переменным ( C.stdout ), или к функциям ( C.putchar() ). Это полезно для Linux, таким образом обеспечивается низкоуровневый API ко всем системным вызовам и библиотекам .so .

Пример кода:

package main // #include // #include import "C" import "unsafe" func main() { str := "Hello, World\n" cs := C.CString(str) C.fputs(cs, (*C.FILE) (C.stdout)) C.free(unsafe.Pointer(cs)) }

Комментарии в синтаксисе Go является кодом на языке C, при подключении даного псевдопакета.

Флаги компилятора GCC могут быть переданы также через комментарии:

package main // #cgo CFLAGS: -O3 // #include // #include import "C" import "unsafe" func main() { str := "Hello, World\n" cs := C.CString(str) C.fputs(cs, (*C.FILE) (C.stdout)) C.free(unsafe.Pointer(cs)) }

Также существуют несколько специальных функций для конвертации типов из C в Go и обратно:

Таким вот образом, код на Go может использовать богатство наработанных библиотек C/C++ и фрагментов кода на этих двух языках, и, самое главное, использовать все API библиотек POSIX и Linux!

Многопроцессорность

Для начала, небольшое введение в сами процессы и процессор в Linux.

Основная информация о процессоре формируется ядром Linux в псевдофайловой системе procfs.

Вот пример файла cpuinfo (информация о процессоре) на процессоре AMD C50:

Процессоры, ядра

$ cat /proc/cpuinfo processor : 0 vendor_id : AuthenticAMD cpu family : 20 model : 1 model name : AMD C-50 Processor stepping : 0 microcode : 0x5000029 cpu MHz : 997.782 cache size : 512 KB physical id : 0 siblings : 2 core id : 0 cpu cores : 2 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 6 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf pni monitor ssse3 cx16 popcnt lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch ibs skinit wdt hw_pstate vmmcall arat npt lbrv svm_lock nrip_save pausefilter bugs : fxsave_leak sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass bogomips : 1996.85 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual power management: ts ttp tm stc 100mhzsteps hwpstate processor : 1 vendor_id : AuthenticAMD cpu family : 20 model : 1 model name : AMD C-50 Processor stepping : 0 microcode : 0x5000029 cpu MHz : 998.619 cache size : 512 KB physical id : 0 siblings : 2 core id : 1 cpu cores : 2 apicid : 1 initial apicid : 1 fpu : yes fpu_exception : yes cpuid level : 6 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf pni monitor ssse3 cx16 popcnt lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch ibs skinit wdt hw_pstate vmmcall arat npt lbrv svm_lock nrip_save pausefilter bugs : fxsave_leak sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass bogomips : 1996.85 TLB size : 1024 4K pages clflush size : 64 cache_alignment : 64 address sizes : 36 bits physical, 48 bits virtual power management: ts ttp tm stc 100mhzsteps hwpstate

Каждое физическое ядро является относительно автономным независимым процессором.

Но иногда можно встретить, что вроде бы в 4-ядерном процессоре только два ядра на сокет. Но при внимательном изучении, можно понять что каждый из этих процессоров имеет два потока выполнение (hyperthreading, HT, гипертрединг). Это второе, логическое ядро, при некоторых условиях может выполнять поток команд параллельно основному физическому ядру. Главная особенность HT — более-менее повышение суммарной производительности пары физическое+логическое ядро наблюдается только при соблюдении условий — ядра в паре должны выполнять как можно более разнородные операции. Но даже в этом случае максимальный выигрыш производительности двух ядер оценивается максимум в 10-30%, и то в серьезных задачах.

На некоторых задачах распределение работы на все ядра может не только не увеличить производительность, но даже и уменьшить ее, причем до 70%. Например, это можно заметить при майнинге криптовалют.

Параллельные процессоры и fork

Первые модели параллелизма были созданы на уровне процесса многозадачной ОС как единица параллельного выполнения.

Но настоящая популяризация параллельности в UNIX пришла с добавления системного вызова fork(). Концепция ветвления, то есть форк, была включена даже в стандарты POSIX.

Вызов fork() разветвляет текущий процесс на родительский (текущий) и дочерний (parent и child процесс). В системах с виртуальной памятью за счет механизма copy-on-write создание полной копии родительского процесса без копирования. И поэтому создание дочернего процесса очень быстрое.

Модель ветвления процессов дала жизнь парадигме построению параллельных клиент-серверных программ.

Единственная разница между родительским и дочерним процессом — то что вызов fork() возращает 0 в дочернем процессе и значение PID этого процесса в родительском, и N<0 — если при выполнении вызова произошла ошибка

Потоки

Я надеюсь, вы знаете что такое закон Мура, и почему он перестал действовать примерно в 2000х годах. Согласно этому закону, кол-во транзисторов на кристалле интегральной схемы удваивается каждые 24 месяца (2 года). Также есть немного другая версия — каждые 18 месяцев (полтора года) производительность процессоров должна удваиваться из-за сочетания количества транзисторов и увеличения тактовых частот процессоров.

Но с 2003 года стало ясно — дальнейший рост производительности будет расти не за счёт увеличения частоты процессора, а за счет количества ядер процессора.

С 1995 года весь API pthread_t вошел в стандарты POSIX.

Поток — это "легкая" единица планирования ядра. Переключение потоков осуществляется относительно легко, оно требует переключения контекста.

Как минимум один поток существует в каждом процессе. Если внутри процесса создается несколько потоков ядра, то они все совместно используют выделенную процессору память.

Корутины, горутины или сопрограммы в Go

Язык Go, как я уже сказал ранее, сочетает в себе лаконизм и ясность C с такими высокоуровневыми вещами, как множественные возвраты из функций, простота работ со строками и т.д.

Но одной из главной фишек Go считается не это. Язык предназначен для поддержания параллельного выполнения на нескольких процессоров, ядрах. Для этого любую функцию Go можно запустить в отдельной ветке.

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

Корутины, горутины, сопрограммы — все это синонимы ветвей в Go. Так что не путайтесь, если я буду их использовать.

Так как же работает параллелизм в Go?

Одна из целей создания Go, как я уже не раз повторял — это эффективное выполнение многих корутин. В Go встроен механизм планировщика параллельных горутин, который первоначально реализовал Дмитрий Вьюков. Этот планировщик по схеме M:N, где M — потоки ядра, а N — число сопрограмм.

Число M чаще всего равно числу процессоров.

A N параллельных корутин (N>M или N>>M) реализуются как легкие сопрограммы пространства пользователя, реализующих модель кооперативной многозадачности. Горутины прикреплены к потокам, но время от времени, при возникновении разбалансировки, могут перераспределяться между потоками ядра.

Go дает возможность создать новую ветвь (поток) выполнения программы (goroutine, go-процедуру) с помощью выражения go. Это выражение запускает функцию в другой заново созданной go-процедуре (сопрограмме). Все go-процедуры в одной программе используют одно и то же адресное пространство.

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

Go позволяет использовать ассемблерные вставки в коде. Написание функций на ассемблере прямо в Go не так уж сложно, как кажется. В качестве примера, рассмотрим функцию sum, которая складывает два int64:

func sum(a int64, b int64) int64

Хотя это стандартная функция, в ней отсутствует тело. Поэтому компилятор выдаст ошибку при попытке сборки программы.

Для реализации функции на ассемблере добавим файл с расширением .s:

text sum(sb),$0-24 movq a+0(fp), ax addq b+8(fp), ax movq ax, ret+16(fp) ret

Теперь мы можем собрать, протестировать и использовать функцию sum как обычную. Этот подход широко применяется в различных пакетах, таких как runtime, math, bytealg, syscall, reflect, crypto, позволяя использовать аппаратные оптимизации процессора и команды, отсутствующие в самом языке. Во многом благодаря этому можно создать полноценное ядро операционной системы.

Однако следует учитывать, что функции на ассемблере не могут быть оптимизированы и встроены компилятором. Для обхода этого ограничения разработчики создали встраиваемые функции.

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

Встраиваемые функции представляют собой элегантное решение, предоставляющее доступ к низкоуровневым операциям без необходимости расширения спецификации языка. В случае отсутствия специфических примитивов sync/atomic (например, в некоторых вариантах arm), или операций из math/bits, компилятор будет вставлять полифил на обычном Go.

Скорость имеет значение© Павел Дуров

Заключение

Go не был разработан для low-level кодинга, но при наличии желания вы можете даже разработать на нем ОС.

Я надеюсь, вам понравилась статья. Если у вас остались вопросы или есть комментарии — прошу оставить их, я обязательно отвечу и прочитаю. И не забывайте ставить плюсы!

А также советую почитать мой канал, посвященный Go, если вы хотите больше погрузиться в прекрасный мир go-разработки, программирования и кодинга! Вы получите пользу, а я буду делать для вас качественный контент, а кто хочет больше тут я собрал другие качественные каналы, которые помогут вам в обучении.

Источники информации

Читайте также:

44
4 комментария

От форматирования кровьглазаболь

слетело, все поправил