GoSuda

Go syscall is a brilliant replacement of low-level I/O

By Yunjin Lee
views ...

Summary

We will learn about direct system call on Go. Since Go is offering strict compiler errors and rigid GC, it is much better to replace low-level calls in Pure Go. Luckily, most of the C function calls are fully reimplemented in Go, in a good and contemporary manner. Let's take a look at it.

System Call

System call is a direct request to the operating system. Since system is usually written in rigid, old-fashioned style since it is running right on a hardware, we need to consider that its call must deliver strict, and correct form of a request. So, even if we don't need some variables, we still need to fill out the size regardless of use. Let's check with fully-working example.

Full Example

 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}

This example includes all variables, and print extensive information of current system information. We can compare this code, as a locker and a key.syscall.SYS_SYSINFO is a key that unlocks a locker that is inside of a kernel. Therefore, using correct key for a locker is important. What will happen when we use syscall.SYS_GETPID for this call? This is a key for a locker that contains Process ID. This will attempt to get a PID from a space for system information. As a result, none of the information can be read correctly; the call must be retured as failed state.

Now, we need to know which items are contained, and how items are ordered. In a first slot of a locker, we have Uptime, with a size of 2 64 2^64 : c90263519492728e7cc2d0ce840057b6 If we try to read this with 2 32 2^32 : c90263519492728e7cc2d0ce840057b6 We cannot use these kinds of partial binaries unless we are going to write low-level tricks.

After reading 64 bits of binary data, finally we are on a second slot. It can only be read accurately when we have read previous 64-bit sized integer.

With repeating those strict, and logical flows to obtain a proper information from a system, we can properly handle read data.

How to skip 'variable names'

Even though we cannot 'skip' variables themselves, it is important to distinguish used variables and discarded ones. If use of the program is clear enough, it is better to use nameless variables as placeholders than labeling each values even if they are not used forever. Let's check this with an example, "Free Memory Checker"

Example - Free Memory Checker

When checking Free Memory/Swaps, we don't need other information that is indicating different resources. To achieve a better visibility, you can make anonymous variables to hold specific spaces.

 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}

Consequently, variables are read without labels. Although anonymous values are actually stored into a structure, there is no labels/legible marks on a code.

Conclusion

  • Using Go's syscall and unsafe is still safer than C/CGo
  • If you are writing a huge project that can be expaneded easily:
    • Don't make anonymous variables; make each names for the members.
  • If you are writing a project that has limited use:
    • You can use anonymous variables to hold spaces those are actually unused.
  • Go's syscall is powerful and modern to handle low-level calls

Read Further

syscall unsafe x/sys/unix