Как стать автором
Обновить
766.2
OTUS
Цифровые навыки от ведущих экспертов

Syscall и cgo в Go

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.9K

Привет, Хабр!

Сегодня рассмотрим работу с системными утилитами в 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)

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

Как это сделать:

  1. Форкаем процесс (syscall.Fork()).

  2. Завершаем родительский, оставляя в живых только потомка.

  3. Создаём новую сессию, чтобы процесс не зависел от терминала (syscall.Setsid()).

  4. Закрываем 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-направлениям можно посмотреть в календаре.

Теги:
Хабы:
+7
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS