Привет, Хабр!
Сегодня рассмотрим работу с системными утилитами в 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-направлениям можно посмотреть в календаре.