GoSuda

syscall.Syscall 및 unsafe.Pointer를 활용한 순수 Go 기반의 직접적인 Linux 시스템 호출

By Yunjin Lee views ...

요약

Go에서 직접 시스템 호출(system call)을 사용하는 방법을 배워 보겠습니다. Go는 엄격한 컴파일러 오류 검사와 빡빡한 GC를 제공하므로, 저수준 호출을 순수 Go로 대체하는 것이 훨씬 낫습니다. 다행히 대부분의 C 함수 호출은 좋고 현대적인 방식으로 Go에서 완전히 재구현되어 있습니다. 그럼 살펴보겠습니다.

시스템 호출

시스템 호출(system call)은 운영체제에 대한 직접적인 요청입니다. 시스템은 보통 하드웨어 위에서 직접 실행되므로 경직되고 구식의 방식으로 작성되어 있으며, 따라서 호출은 엄격하고 정확한 형식의 요청을 전달해야 한다는 점을 고려해야 합니다. 그러므로 일부 변수를 사용하지 않더라도 사용 여부와 관계없이 크기를 반드시 채워야 합니다. 완전히 동작하는 예제로 확인해 보겠습니다.

전체 예제

 1package main
 2import (
 3	"fmt"
 4	"syscall"
 5	"unsafe"
 6)
 7
 8type sysinfo_t struct {
 9	Uptime    int64
10	Loads     [3]uint64
11	Totalram  uint64
12	Freeram   uint64
13	Sharedram uint64
14	Bufferram uint64
15	Totalswap uint64
16	Freeswap  uint64
17	Procs     uint16
18	Pad       uint16
19	_         [4]byte
20	Totalhigh uint64
21	Freehigh  uint64
22	MemUnit   uint32
23	_         [4]byte
24}
25
26func main() {
27	var info sysinfo_t
28	_, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
29	if errno != 0 {
30		fmt.Println("sysinfo syscall failed:", errno)
31		return
32	}
33
34	scale := float64(1 << 16)
35	fmt.Printf("Uptime: %d seconds\n", info.Uptime)
36	fmt.Printf("Load Average: %.2f %.2f %.2f\n",
37		float64(info.Loads[0])/scale,
38		float64(info.Loads[1])/scale,
39		float64(info.Loads[2])/scale)
40	fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
41		info.Totalram*uint64(info.MemUnit)/1024/1024,
42		info.Freeram*uint64(info.MemUnit)/1024/1024,
43		info.Bufferram*uint64(info.MemUnit)/1024/1024)
44	fmt.Printf("Swap: total=%d MB free=%d MB\n",
45		info.Totalswap*uint64(info.MemUnit)/1024/1024,
46		info.Freeswap*uint64(info.MemUnit)/1024/1024)
47	fmt.Printf("Processes: %d\n", info.Procs)
48}

이 예제는 모든 변수를 포함하며, 현재 시스템 정보를 자세히 출력합니다. 이 코드를 자물쇠와 열쇠에 비유할 수 있습니다.syscall.SYS_SYSINFO는 커널 내부에 있는 자물쇠를 여는 열쇠입니다. 따라서 자물쇠에 맞는 올바른 열쇠를 사용하는 것이 중요합니다. 이 호출에 syscall.SYS_GETPID를 사용하면 어떻게 될까요? 이것은 프로세스 ID를 담고 있는 자물쇠의 열쇠입니다. 이는 시스템 정보를 위한 공간에서 PID를 가져오려고 시도하게 됩니다. 결과적으로 어떤 정보도 올바르게 읽을 수 없으며, 이 호출은 실패 상태로 반환되어야 합니다.

이제 어떤 항목들이 포함되어 있는지, 그리고 항목들이 어떤 순서로 배열되어 있는지 알아야 합니다. 자물쇠의 첫 번째 칸에는 크기가 2^64인 Uptime이 있습니다. 이를 2^32 단위로 읽으려 하면 비트 시퀀스가 완전히 읽히지 않습니다. 저수준 기법을 사용하려는 것이 아니라면, 이러한 부분적인 이진 데이터는 사용할 수 없습니다.

64비트의 이진 데이터를 읽은 후에야 비로소 두 번째 칸에 도달합니다. 이전의 64비트 정수를 먼저 읽어야만 정확하게 읽을 수 있습니다. 이러한 엄격하고 논리적인 흐름을 반복함으로써 시스템에서 올바른 정보를 얻고, 읽은 데이터를 적절히 처리할 수 있습니다.

'variable names'를 생략하는 방법

변수 자체를 '생략'할 수는 없지만, 사용하는 변수와 버리는 변수를 구분하는 것이 중요합니다. 프로그램의 용도가 충분히 명확하다면, 값을 사용하지 않을 수도 있으므로 각 값에 이름을 붙이기보다 이름 없는 변수를 자리표시자로 사용하는 것이 낫습니다. "Free Memory Checker"라는 예제로 이를 확인해 보겠습니다.

예제 - Free Memory Checker

여유 메모리/스왑을 확인할 때는 다른 자원을 나타내는 정보가 필요하지 않습니다. 더 나은 가시성을 얻기 위해, 특정 공간을 보관하는 익명 변수를 만들 수 있습니다.

 1package main
 2
 3import (
 4	"fmt"
 5	"syscall"
 6	"unsafe"
 7)
 8
 9type sysinfo_t struct {
10	_          int64
11	_         [3]uint64
12	Totalram  uint64
13	Freeram   uint64
14	Sharedram uint64
15	Bufferram uint64
16	Totalswap uint64
17	Freeswap  uint64
18	_         uint16  // anonymous, and unused ones are marked as _ (익명이며, 사용되지 않는 항목은 _로 표시)
19	_         uint16
20	_         [4]byte
21	_         uint64
22	_         uint64
23	MemUnit   uint32
24	_         [4]byte
25}
26
27func main() {
28	var info sysinfo_t
29	_, _, errno := syscall.Syscall(syscall.SYS_SYSINFO, uintptr(unsafe.Pointer(&info)), 0, 0)
30	if errno != 0 {
31		fmt.Println("sysinfo syscall failed:", errno)
32		return
33	}
34
35	fmt.Printf("Memory: total=%d MB free=%d MB buffer=%d MB\n",
36		info.Totalram*uint64(info.MemUnit)/1024/1024,
37		info.Freeram*uint64(info.MemUnit)/1024/1024,
38		info.Bufferram*uint64(info.MemUnit)/1024/1024)
39	fmt.Printf("Swap: total=%d MB free=%d MB\n",
40		info.Totalswap*uint64(info.MemUnit)/1024/1024,
41		info.Freeswap*uint64(info.MemUnit)/1024/1024)
42}

결과적으로 변수는 레이블 없이 읽힙니다. 익명 값들도 실제로는 구조체에 저장되지만, 코드에는 레이블이나 읽기 쉬운 표시가 없습니다.

결론

  • Go의 syscall과 unsafe를 사용하는 것은 C/CGo를 사용하는 것보다 여전히 안전합니다.
  • 쉽게 확장될 수 있는 대규모 프로젝트를 작성하는 경우:
    • 익명 변수를 사용하지 말고, 각 멤버에 이름을 붙이십시오.
  • 사용 범위가 제한된 프로젝트를 작성하는 경우:
    • 실제로 사용되지 않는 공간을 보관하기 위해 익명 변수를 사용할 수 있습니다.
  • Go의 syscall은 저수준 호출을 처리하는 데 강력하고 현대적입니다.

더 읽어보기

syscall unsafe x/sys/unix