Привет, Хабр!
Сегодня рассмотрим работу с системными утилитами в Go. Будем напрямую общаться с ядром, дергать системные вызовы и писать код на C, чтобы Go не чувствовал себя один��ким.
Go славится своей безопасностью и простотой, но иногда нужно спуститься к системным вызовам, чтобы работать с процессами, файловыми дескрипторами и сетевыми сокетами на низком уровне.
Сделаем это через два инструмента:
syscall и golang.org/x/sys/unix — вызовы системных API, которые делают всякие вещи вроде
fork(),execve(),kill().cgo — мост между Go и C. Если нужно вызвать что‑то из libc или POSIX, это наш инструмент.
Работа с процессами через syscall
Начнем с простого: запустим новый процесс и узнаем его PID.
package main import ( "fmt" "syscall" "os" ) func main() { pid, _, errno := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0) if errno != 0 { fmt.Println("Ошибка:", errno) os.Exit(1) } if pid == 0 { fmt.Println("Дочерний процесс:", os.Getpid()) } else { fmt.Println("Родительский процесс:", os.Getpid(), "запустил процесс с PID", pid) } }
Здесь:
syscall.SYS_FORKделаетfork(), создавая новый процесс.Если
pid == 0, значит мы в дочернем процессе.Родитель просто узнаёт, кого он породил.
Можно делать что угодно: запускать фоновые процессы, форкать демонов или просто следить за pid ради забавы.
Запуск другой программы без exec.Command()
Все используют exec.Command(), но сделаем это вручную.
package main import ( "syscall" "os" ) func main() { err := syscall.Exec("/bin/ls", []string{"ls", "-l"}, os.Environ()) if err != nil { panic(err) } }
Этот код заменяет процесс на ls ‑l. Никаких оболочек, никакого Go‑кода после этого уже не будет выполняться.
Если нужно ��апустить процесс и не потерять управление, используем syscall.ForkExec().
package main import ( "fmt" "syscall" ) func main() { args := []string{"/bin/ls", "-l"} env := []string{"PATH=/bin"} pid, err := syscall.ForkExec(args[0], args, &syscall.ProcAttr{ Dir: "/", Env: env, Files: []uintptr{0, 1, 2}, // stdin, stdout, stderr }) if err != nil { panic(err) } fmt.Println("Процесс запущен, PID:", pid) }
Этот код:
Форкает процесс (создаёт новый)
Запускает в нём
ls ‑lПередаёт окружение и файловые дескрипторы
Теперь не теряем управление, и процесс не заменяет текущий.
Управление процессами
Теперь попробуем убить процесс программно.
package main import ( "fmt" "syscall" "os" "strconv" ) func main() { if len(os.Args) < 2 { fmt.Println("Использование: go run kill.go <pid>") os.Exit(1) } pid, _ := strconv.Atoi(os.Args[1]) err := syscall.Kill(pid, syscall.SIGKILL) if err != nil { fmt.Println("Ошибка:", err) } else { fmt.Println("Процесс", pid, "уничтожен") } }
Если запустить ps aux | grep target_process, а потом go run kill.go <PID>, то процесс исчезнет.
11 марта пройдёт открытый урок на тему «Дженерики в GO». Подробности
Использование cgo: работаем с C-функциями
Что, если нужно вызвать gettimeofday() из C?
package main /* #include <sys/time.h> */ import "C" import "fmt" func main() { var tv C.struct_timeval C.gettimeofday(&tv, nil) fmt.Println("Секунды:", tv.tv_sec, "Микросекунды:", tv.tv_usec) }
А если хочется прочитать права файла через stat()?
package main /* #include <sys/stat.h> */ import "C" import ( "fmt" "unsafe" ) func main() { var stat C.struct_stat path := C.CString("/etc/passwd") defer C.free(unsafe.Pointer(path)) if res := C.stat(path, &stat); res != 0 { fmt.Println("Ошибка получения информации о файле") return } fmt.Printf("Размер файла: %d байт\n", stat.st_size) }
Теперь можно вызывать функции из C так же просто, как из Go.
Примеры применения
Написание демона (fork + setsid)
Допустим, нужно создать фоновый процесс, который не зависел бы от терминала и мог работать, даже если мы закроем консоль.
Как это сделать:
Форкаем процесс (
syscall.Fork()).Завершаем родительский, оставляя в живых только потомка.
Создаём новую сессию, чтобы процесс не зависел от терминала (
syscall.Setsid()).Закрываем stdin, stdout, stderr, чтобы процесс не мешал системе.
package main import ( "fmt" "os" "syscall" "time" ) func main() { pid, err := syscall.ForkExec("/proc/self/exe", os.Args, &syscall.ProcAttr{ Files: []uintptr{0, 1, 2}, // stdin, stdout, stderr }) if err != nil { panic("Не удалось форкнуть процесс: " + err.Error()) } if pid > 0 { fmt.Println("Процесс запущен в фоне, PID:", pid) os.Exit(0) } syscall.Setsid() // Создаём новую сессию os.Stdout.Close() os.Stderr.Close() os.Stdin.Close() for { time.Sleep(10 * time.Second) fmt.Println("Демон работает...") } }
Запускаем копию самого себя (syscall.ForkExec()), после чего завершаем родителя (os.Exit(0)).
Потомок становится демоном, отвязываясь от терминала (syscall.Setsid()). Работает бесконечно, периодически что‑то делая.
Как запустить:
go build -o mydaemon ./mydaemon
Закроем терминал — процесс останется работать. Чтобы найти его:
ps aux | grep mydaemon
Если захотите прибить его:
kill -9 <PID>
Работа с сырыми сокетами в Go
Хотите написать свой ping, tcpdump или кастомный firewall? Для этого нужны сырые сокеты (raw sockets).
Обычно Go скрывает низкоуровневую работу с сетью, но можно залезть под капот через syscall.Socket().
Создание сырого ICMP‑сокета:
package main import ( "fmt" "golang.org/x/sys/unix" "net" "os" ) func main() { sock, err := unix.Socket(unix.AF_INET, unix.SOCK_RAW, unix.IPPROTO_ICMP) if err != nil { fmt.Println("Ошибка создания сырого сокета:", err) os.Exit(1) } defer unix.Close(sock) addr := &unix.SockaddrInet4{Port: 0} copy(addr.Addr[:], net.ParseIP("8.8.8.8").To4()) icmpPacket := []byte{8, 0, 0, 0, 0, 0, 0, 0} // Тип 8 (Echo Request), Код 0 if err := unix.Sendto(sock, icmpPacket, 0, addr); err != nil { fmt.Println("Ошибка отправки ICMP-запроса:", err) } else { fmt.Println("ICMP-запрос отправлен!") } }
Создаём сырой сокет unix.Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP), далее формируем ICMP Echo Request (типа ping). Отправляем пакет на 8.8.8.8 через unix.Sendto().
Запустить без sudo не получится, так как сырые сокеты требуют привилегий:
sudo go run raw_socket.go
Полный контроль над терминалом
Если вы писали CLI‑инструменты, то, возможно, сталкивались с задачей скрыть ввод пароля, захватить клавиши или управлять курсором.
Обычно это делается через stty, но сделаем это без оболочек, прямо через системные вызовы.
Скрываем ввод пароля в терминале (аналог sudo/ssh):
package main /* #include <termios.h> #include <unistd.h> */ import "C" import ( "fmt" "os" "unsafe" ) func disableEcho() *C.struct_termios { var oldTermios C.struct_termios fd := C.int(os.Stdin.Fd()) C.tcgetattr(fd, &oldTermios) // Получаем текущие настройки терминала newTermios := oldTermios newTermios.c_lflag &^= C.ECHO // Выключаем эхо-ввод C.tcsetattr(fd, C.TCSANOW, &newTermios) // Применяем изменения return &oldTermios } func restoreEcho(old *C.struct_termios) { fd := C.int(os.Stdin.Fd()) C.tcsetattr(fd, C.TCSANOW, old) // Восстанавливаем настройки терминала } func main() { fmt.Print("Введите пароль: ") oldTermios := disableEcho() defer restoreEcho(oldTermios) var password string fmt.Scanln(&password) fmt.Println("\nПароль сохранён (на самом деле нет)") }
Вызываем tcgetattr(), чтобы сохранить настройки терминала, далее отключаем флаг ECHO, чтобы не показывать вводимые символы. После ввода восстанавливаем настройки, иначе терминал зависнет.
Запускаем:
go run hide_input.go
Теперь вводимые символы не отображаются.
А ка��ие syscall и cgo используете вы? Пишите в комментарии, обсудим.
Все открытые уроки по Data Science и Machine Learning, а также по другим IT-направлениям можно посмотреть в календаре.
