yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

「Goならわかるシステムプログラミング」の11章を読んだ(プロセスの役割と Go言語による操作)

前章に続いて写経しつつ「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やbashPowerShellなどが動いている黒い画面(白いこともありますが)のことを、擬似端末(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といった仕組みで起動することによりデーモン化する方法が一般的でしょう。この方法であれば、管理方法も他の常駐プログラムと同じように扱えるというメリットもあります。

子プロセスの内部実装

Unix系のOSには次のようなシステムコールがあります。

  • fork()
  • vfork()
  • rfork() (BSD
  • clone() (Linux
  • fork()は親を複製して子プロセスを作る
  • vfork()はメモリブロックのコピーを行わない
  • rfork()は呼び出す側で各資源をコピーするかどうかを細かく条件設定できる
  • Linuxではこのようなメモリの共有もフラグで制御できる、より柔軟なclone()システムコールを内部で使っている