onemuri.space

golang でプロセスを扱ってみる

プロセスとは

OS が実行ファイルを読み込んで実行するためにメモリや CPU をまとめたプログラムの実行単位がプロセスです。

僕は普段 GCP の中でも GAE をよく使います。golang で API を開発するときに GAE は相性がいいです。個人的には golang+GAE の場合はデプロイの速度がかなり早いところが地味に気に入っていたりします。GAE にデプロイした golang で作成したサーバーは GAE 上ではプロセスを立ち上げる事でリクエストを受け付ける事ができる状態になります。スピンアップと言うのですが、これが爆速なのも良い点です。

余談はさておき、本記事ではプロセスの中身について覗いてみることにします。golang の視点や OS の視点からみたプロセスについてコードを示しながら考えていくことにしたいと思います。

golang からみたプロセス

プロセスには必ず プロセス ID という識別子が存在します。コメントアウト部分は例ですけど、このように一意に識別される値で表現されます。 go run main.go で実行したプロセスは今回の例の場合だと 50927 というプロセス ID で実行されていることになります。

fmt.Printf("プロセスID: %d\n", os.Getpid()) // プロセスID: 50927
fmt.Printf("親プロセスID: %d\n", os.Getppid()) // 親プロセスID: 50916

プロセスは実行するたびに識別子が変わります。上記のコードを何回か実行してみたら識別子が毎回変わる事がわかると思います。プロセスは起動した後、実行ファイルの EOF に到達したらプロセスを停止しているので、再度起動したときには空いているプロセス ID を割り当てるのでしょう。

次にプロセスを実行するユーザを表示してみましょう。

fmt.Printf("ユーザID: %d\n", os.Getuid()) // ユーザID: 500
fmt.Printf("グループID: %d\n", os.Getgid()) // グループID: 20

この場合であればユーザ ID が 500 のユーザーがファイルを実行してプロセス ID が 50927 としてプロセスを起動したことになります。また、グループ ID というのも一緒に出力していますが、わかりやすい例としては、パイプで繋げたコマンドなどです。

cat hoge.txt | echo "hoge"

catecho は同じプロセスグループになります。

プロセスの入出力

プロセスは 3 つの入出力データを持っています。

  • コマンドライン引数
  • 環境変数
  • 終了コード

コマンドライン引数

以下のようなコードを用意しておいて、

func main() {
  fmt.Printf("コマンドライン引数: %s\n", os.Args[1])
}

以下を実行すると、100os.Args[1] 渡されます。

go run main.go 100

環境変数

golang を扱う上で GOPATHを設定したかと思いますが、

func main() {
  fmt.Println("GOPATH: " + os.Getenv("GOPATH")) // GOPATH: / ~ some path/Go
}

環境変数を bash で表示させた時と同じ値が取得できていると思います。

echo $GOPATH

環境変数はとても頻繁に用いる概念?なので使えるようになっておいて無駄になることはないと思います。例えば、Git に含ませたくない秘匿情報は環境変数として設定しておいてアプリ上では、 os.Getenv() 関数で利用できるようにするというのは常套手段です。

終了コード

プロセスを終了させるときは終了コード(os.Exit()関数)を呼びます。

func main() {
  os.Exit(1) // exit status 1
}

慣習として、 0=正常終了1=エラー終了 という扱いになっています。

OS から見たプロセス

OS からプロセスを見てみると、数多くのプロセスを CPU 時間を使って効率よく動作させるタスクと考えられます。

golang のプログラムから他のプロセスを起動する

golang で実行しているプログラムから、他のプロセスを起動するためには exec.Command を使う事ができます

exec.Command

では早速、exec.Command で他のプロセスを扱ってみましょう。OS 標準で使える ls コマンドを実行してみましょう。

func main() {
  out, _ := exec.Command("ls").Output()
  fmt.Println(string(out))

  out, _ = exec.Command("sleep", "3").Output()
  fmt.Println(string(out))

  out, _ = exec.Command("ls", "-la").Output()
  fmt.Println(string(out))
}

せっかくなので、第二引数も設定したコードも実行できるようにしておきました。出力された文字列は、console 上で直接 lsls -la と実行した時と同じ結果だったのではないでしょうか。ついでに sleep 3 も実行してみました。 sleep コマンドは返り値があるわけではないので、画面上は何も表示されていないと思いますが、最初の lsから 3 秒待った後に ls -la が実行されているはずです。

まとめ

本記事ではシステムコールを呼び出しながらプロセスの ID やグループについて確認し、環境変数やプロセスを閉じるようなサンプルコードを載せてみました。golang の強みである goroutine(並列処理)でまた出てくると思いますので、その記事を書くときさらに詳細に考えてみようと思います。

参考

Go ならわかるシステムプログラミング

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細