前章に続いて写経しつつ「Goならわかるシステムプログラミング」の11章を読む。
現在起動中のプログラムの絶対パスを表示
package main import ( "fmt" "os" ) func main() { path, _ := os.Executable() fmt.Printf(" 実行ファイル名 : %s\n", os.Args[0]) fmt.Printf(" 実行ファイルパス: %s\n", path) }
go run main.go 実行ファイル名 : /var/folders/9d/kwmd2hfx44q7_f3s_t8l5vtr0000gn/T/go-build955877971/b001/exe/main 実行ファイルパス: /var/folders/9d/kwmd2hfx44q7_f3s_t8l5vtr0000gn/T/go-build955877971/b001/exe/main
プロセスID、親プロセスID取得
func main() { fmt.Printf(" プロセス ID: %d\n", os.Getpid()) fmt.Printf(" 親プロセス ID: %d\n", os.Getppid()) }
プロセスグループ
プロセスを束ねたグループというものがあり、プロセスはそのグループを示すID情報を持っています。次のようにパイプ(|)でつなげて実行された仲間が、1つのプロセスグループ(別名ジョブ)になります。
セッショングループ
プロセスグループと似た概念として、セッショングループがあります。同じターミナルから起動したアプリケーションであれば、同じセッショングループになります。 同じキーボードにつながって同じ端末に出力するプロセスも同じセッショングループとなります。
プロセスグループとセッショングループの表示
package main import ( "fmt" "os" "syscall" ) func main() { sid, _ := syscall.Getsid(os.Getpid()) fmt.Fprintf(os.Stderr, "グループID: %d セッションID: %d\n", syscall.Getpgrp(), sid) }
ユーザーIDとグループID、サブグループを表示
package main import ( "fmt" "os" ) func main() { fmt.Printf("ユーザID: %d\n", os.Getuid()) fmt.Printf("グループID: %d\n", os.Getgid()) groups, _ := os.Getgroups() fmt.Printf("サブグループ: %v\n", groups) }
終了コード
func main() { os.Exit(1) }
/gopsutilでプロセス確認
package main import ( "fmt" "github.com/shirou/gopsutil/process" "os" ) func main() { p, _ := process.NewProcess(int32(os.Getppid())) name, _ := p.Name() cmd, _ := p.Cmdline() fmt.Printf("parent pid: %d name: '%s' cmd: '%s'\n", p.Pid, name, cmd) }
プロセスの実行で使われた実行ファイル名と、実行時のプロセスの引数情報を表示しています。これ以外にも、ホストのOS情報、CPU情報、プロセス情報、ストレージ情報など、数多くの情報が取得できます。
引数として外部プログラムを指定すると、その外部プログラムの実行にかかった時間を表示するプログラム
package main import ( "fmt" "os" "os/exec" ) func main() { if len(os.Args) == 1 { return } cmd := exec.Command(os.Args[1], os.Args[2:]...) err := cmd.Run() if err != nil { panic(err) } state := cmd.ProcessState fmt.Printf("%s\n", state.String()) fmt.Printf(" Pid: %d\n", state.Pid()) fmt.Printf(" System: %v\n", state.SystemTime()) fmt.Printf(" User: %v\n", state.UserTime()) }
package main import ( "fmt" "time" ) func main() { for i := 0; i < 10; i++ { fmt.Println(i) time.Sleep(time.Second) } }
上記ファイルをbuildする
go build -o count count.go
このcountプログラムを起動し、標準出力に(stdout)というプリフィックスを付けつつリアルタイムでリダイレクトするサンプルを下記に示します。
package main import ( "bufio" "fmt" "os/exec" ) func main() { count := exec.Command("./count") stdout, _ := count.StdoutPipe() go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { fmt.Printf("(stdout) %s\n", scanner.Text()) } }() err := count.Run() if err != nil { panic(err) } }
疑似端末
OSに備わっている、cmd.exeやbashやPowerShellなどが動いている黒い画面(白いこともありますが)のことを、擬似端末(Pseudo Terminal)と呼びます。
自分が擬似端末であると詐称するには、POSIX系OSではgithub.com/kr/ptyパッケージ、Windowsではgithub.com/iamacarpet/go-winptyパッケージを使います。
以下のコードをcheckと言う名前でbuildする
go build -o check ./main.go
package main import ( "fmt" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" "io" "os" ) func main() { var out io.Writer if isatty.IsTerminal(os.Stdout.Fd()) { out = colorable.NewColorableStdout() } else { out = colorable.NewNonColorable(os.Stdout) } if isatty.IsTerminal(os.Stdin.Fd()) { fmt.Fprintln(out, "stdin: terminal") } else { fmt.Println("stdin: pipe") } if isatty.IsTerminal(os.Stdout.Fd()) { fmt.Fprintln(out, "stdout: terminal") } else { fmt.Println("stdout: pipe") } if isatty.IsTerminal(os.Stderr.Fd()) { fmt.Fprintln(out, "stderr: terminal") } else { fmt.Println("stderr: pipe") } }
その後以下実行
package main import ( "github.com/kr/pty" "io" "os" "os/exec" ) func main() { cmd := exec.Command("./check") stdpty, stdtty, _ := pty.Open() defer stdtty.Close() cmd.Stdin = stdpty cmd.Stdout = stdpty errpty, errtty, _ := pty.Open() defer errtty.Close() cmd.Stderr = errtty go func() { io.Copy(os.Stdout, stdpty) }() go func() { io.Copy(os.Stderr, errpty) }() err := cmd.Run() if err != nil { panic(err) } }
デーモン化
そのような場合でも終了しないように、下記のような特別な細工が施された プロセスがデーモンです。
- セッションID、グループIDを新しいものにして既存のセッションとグループか ら独立
- カレントのディレクトリはルートに移動
- フォークしてからブートプロセスのinitを親に設定し、実際の親はすぐに終了
- 標準入出力も起動時のものから切り離される(通常は/dev/nullに設定される)
しかし、フォークが必要なところからもわかるとおり、Go言語自身ではデーモン化が積極的にサポートされていません。とはいえ、syscall以下の機能を駆使することでデーモン化は可能です。そのようなパッケージも探せばいくつも出てきます。現在では、通常のプログラムとして作ったうえで、launchctlやsystemd、daemontoolsといった仕組みで起動することによりデーモン化する方法が一般的でしょう。この方法であれば、管理方法も他の常駐プログラムと同じように扱えるというメリットもあります。