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


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


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


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


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


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

Хоть и 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-строку
func C.CString(string) *C.char
// C-строка в Go-строку
func C.GoString(*C.char) string
// C-строка, длина в Go-строку
func C.GoStringN(*C.char, C.int) string
// C-указатель, длина в Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Таким вот образом, код на 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-разработки, программирования и кодинга! Вы получите пользу, а я буду делать для вас качественный контент, а кто хочет больше тут я собрал другие качественные каналы, которые помогут вам в обучении.


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



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


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


  1. DrArgentum
    01.05.2024 12:26

    Прикольная статья, мне понравилась. Особенно про то как работать с системными вызовами и С из голанга


  1. Maxim_Evstigneev
    01.05.2024 12:26
    +2

    В самом начале:

    1. Компилятор сам расставляет отступы с помощью gofmt. Но важно использовать табуляцию.

    И далее примеры по тексту:

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

    Коллеги, мы не компиляторы golang`а - нам бы в примерах сразу нормальное форматирование. :) Поправьте, пожалуйста.


    1. DrArgentum
      01.05.2024 12:26

      При переносе статьи на Хабр сломался)


      1. DrArgentum
        01.05.2024 12:26

        Я это знаю, ибо сам помогал писать эту статью


    1. Golangcoder Автор
      01.05.2024 12:26

      Исправил


  1. Imaginarium
    01.05.2024 12:26
    +2

    Язык ядра Linux, его модулей и утилит написаны на языке C

    Простите, но глаза ломаются и дёргаются, когда с первой строки язык ядра написан на языке С )

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

    Вы меня извините опять, но я не смог представить параллельные процессоры. Попытался представить ортогональные и положить угол равным нулю, но не помогло)


  1. stvoid
    01.05.2024 12:26
    +2

    Ожидал увидеть что-то более реальное, типа "для примера возьмем исходники программы $NAME, для наглядности напишем свою обертку, где выставим нужные нам параметры" и т.п.

    Я конечно могу найти что-то вроде https://github.com/google/brotli , поковырять, попытаться понять как это устроено и работает, но всё же... Как то не хватает в статье более-менее реальных примеров, а то на самом интересном всё и закончилось. Я даже думал где-то в конце должна быть рекламная ссылка на курсы.


  1. Octabun
    01.05.2024 12:26
    +16

    Я от природы очень глупый, но понял, что

    • Go - замечательный язык программирования

    • В Телеграм ему посвящены один замечательный и ещё 17 просто очень хороших каналов

    • Маскотом Go (до этой статьи никогда не задумывался) является суслик (Gopher) что весьма изящно перекликается с Gofer или даже дальше, с Golfer

    А теперь чего я по глупости не понял, занятым умникам рекомендую не читать - не будет повода гадить в карму
    • При чём тут Пингвин, не верю что Go не под Линукс работает иначе

    • При чём тут системное программирование, не верю что системное программирование тождественно возможности взаимодействовать с С и ассемблером

    • Не верю что в системном программировании допустима нестабильность по времени вызванная сборщиком мусора, да и двоечка в минус по производительности показанная моим примитивным нерепрезентативным тестом - тоже не к месту

    • Не верю что в системном программировании допустима способность языка из-под тишка плодить артефакты уровня ОС как это делают горутины с ветками

    • При чём тут fork если тут же рядом сказано - все горутины работают в одном адресном пространстве

    • Статья не упоминающая недостатки вовсе заставляет полагать что любой недостаток имеет уровень deal breaker, например, навязанная многословная и разрушающая связность кода обработка ошибок провоцирующая желание на неё забить, то есть дающая обратныйй задуманному эффект, это по слухам

    • Так и не понял как работают горутины, посмотрел доки, об этом ниже

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

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

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

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

    Первый же пример горутины в доках https://go.dev/tour/concurrency/1 показался странноватым, как потыкал в него, так он принял вид

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func say(s string, delay time.Duration, c chan int) {
    	for i := 0; i < 5; i++ {
    		time.Sleep(delay)
    		fmt.Println(s)
    	}
    	//close(c) //Deadlock if omitted, requires a channel per call
    	c <- 1
    }
    
    func main() {
    	c := make(chan int,3) // without this just exits cleanly(!!!) saying nothing
    	//d := make(chan int,3)
    	go say("world", 150 * time.Millisecond, c)
    	go say("hello", 100 * time.Millisecond, c)  //d)
    	u := 0
    	for i := range c {
    		u = u+1  // := instead of = is stealthy and deadly
    		if u == 2 {  // requires global knowledge about all the goroutines run
    			close(c)
    		}
    	}
    	//for i := range d {
    	//	fmt.Println(i)
    	//}
    }
    

    от которого просто разит Гуглом.


  1. pvzh
    01.05.2024 12:26
    +4

    Если у вас остались вопросы

    Не вопросы, а нестыковки. Их так много, что разбор потянет на целую статью.

    Автоформатирование кода. Компилятор сам расставляет отступы с помощью gofmt. Но важно использовать табуляцию.

    Автоформатирование есть для любого уважающего себя языка. Просто в Go оно «в комплекте». Компилятор не расставляет отступы. Табуляция будет независимо от вашей воли.

    Если вы не хотите заморачиваться над созданием документации — можно использовать godoc.

    Не, тут другой посыл - механизм документации уже «в комплекте», заморачиваться и не придётся.

    Низкоуровневое программирование — да-да, Go все таки может работать непосредственно с памятью, и существует пакет unsafe .

    Пакет существует, но это не повод его сразу применять. Его использование снижает безопасность, надёжность, переносимость и поддерживаемость программ.

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

    Реально запутался;) Зачем по-разному называть, если уже есть один устоявшийся термин?

    В каждом дистрибутиве линукса есть огромное количество библиотек для Go.

    Но зачем? О чём вообще речь? Зачем брать библиотеки из дистрибутива, если в Go свой прекрасный менеджер пакетов? И чаще всего Go-код компилируется в самодостаточный бинарь. Даже сам Go легко ставится и обновляется путём распаковки архива. Не в каждом дистрибутиве запакечен свежий Go.

    Кроссплатформеность — поддержка Go осуществляется для Linux, Windows, MacOS и даже Free и Open BSD систем

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

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

    Точно в яблочко! Но это плохо согласуется с приёмами из статьи, вся эта возня с Cgo и ассемблерными вставками. К примеру, работать с SQLite можно и через либу с C-биндингами и через нативный драйвер. Так вот, я предпочту драйвер без Cgo, потому что мне важна кросс-компиляция и не хочется зависеть от C-компилятора и его тулчейна. Всё же Go это больше про прикладное программирование.


  1. Sly_tom_cat
    01.05.2024 12:26

    Я бы только не забывал добавлять, что с подключением кусков на С можно забыть про фантастически быструю компиляцию Golang.


  1. kale
    01.05.2024 12:26
    +1

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

    Тот случай, когда одним голословным и очень сомнительным утверждением портят впечатление обо всей статье. Подтверждающие ссылки привести сможете?


  1. mobile_Tukay
    01.05.2024 12:26

    Интересно, Горутины в Голанг называются "параллельными" сопрограммами. В Питоне программирование Корутин называется "асинхронным". Часто на собеседованиях по Python задается вопрос - отличия threading / multiprocessing / asyncio. Да, до последней версии Python выполняется на одном ядре.

    Нельзя ли назвать Горутины в Golang "параллельно-асинхронными"? Поскольку 1) в Golang есть возможность выполнять на нескольких ядрах 2) как в питоне, во время простаивания (например, во время ожидания ответов сетевых запросов) Горутины отдают выполнение в Loop.


  1. maksimuimin
    01.05.2024 12:26

    Спасибо за статью! Я периодически пишу на Go, но чаще всё-таки на C. Была как-то идея гошный код в .so собрать и к сишке подключить, но побоялся, не хотелось гошную асинхронщину с сишной смешивать. Был ли у кого-то опыт CGO "в обратную сторону", можете поделиться?)