純粋な Go による syscall.Syscall と unsafe.Pointer を用いた Linux システムコールの直接呼び出し

概要
Go における直接的なシステムコールについて学びます。 Go は厳格なコンパイラエラーと堅牢な GC を提供しているため、低レベルの呼び出しを純粋な Go で置き換えることがはるかに望ましいです。幸いなことに、ほとんどの C の関数呼び出しは、優れた現代的な方法で Go に完全に再実装されています。それでは見ていきましょう。
システムコール
システムコールとは、オペレーティングシステムへの直接的な要求です。システムはハードウェア上で直接動作するため、通常は堅苦しく古風な形式で書かれており、その呼び出しは厳密かつ正確な形式の要求でなければならないことを考慮する必要があります。したがって、変数を使用しない場合でも、使用の有無にかかわらずサイズを埋める必要があります。完全に動作する例で確認してみましょう。
完全な例
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 を取得しようとする試みになります。その結果、いかなる情報も正しく読み取ることができず、呼び出しは失敗の状態として返されなければなりません。
次に、どの項目が含まれているか、そして項目がどのように並んでいるかを知る必要があります。錠前の最初のスロットには Uptime があり、そのサイズは 2^64 です。これを 2^32 で読み取ろうとすると、ビット列は完全には読み取られません。低レベルの工夫を行うのでない限り、このような部分的なバイナリを使用することはできません。
64 ビットのバイナリデータを読み取った後、ようやく二番目のスロットに到達します。それは、直前の 64 ビット整数を読み取った後にのみ正確に読み取ることができます。このような厳密で論理的な流れを繰り返すことで、システムから適切な情報を得て、読み取ったデータを適切に扱うことができます。
「変数名」を省略する方法
変数そのものを「省略」することはできませんが、使用される変数と破棄される変数を区別することが重要です。プログラムの用途が十分に明確である場合、たとえ永久に使用されない値であっても、各値にラベルを付けるよりも、名前のない変数をプレースホルダーとして使用するほうが望ましいです。「空きメモリチェッカー」という例で確認してみましょう。
例 - 空きメモリチェッカー
空きメモリやスワップを確認する際には、異なるリソースを示すその他の情報は必要ありません。より見やすくするために、特定の領域を保持する匿名変数を作成できます。
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 // 匿名であり、未使用のものは _ で示す
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は、低レベルの呼び出しを扱うための強力で現代的な手段です