3-1. os/exec.Cmdとexec (syscall)
ここからはNamespaceの機能を実装していきます。
Namespaceでは特にプロセスの概念が大事になってくるので、プロセスの動きを体感してみましょう!
【前提】この節で扱うsyscall
fork
自らをコピーしてプロセスを生成します。
以下はCの関数定義です。
pid_t fork(void);forkにおいては処理が終わった後、親プロセスと子プロセスが同じ地点から処理を始めます。すなわち、子プロセスは親プロセスにあった変数を全てコピーして引き継ぎます。
自分が親か子かは、返り値で判断します。
int main() {
int a = 0;
pid_t pid = fork();
if (pid == 0)
printf("子プロセスです!PID: %d、a: %d\n", getpid(), a); // 子プロセス
else if (pid > 0)
printf("親プロセスです!子のPID: %d、a: %d\n", pid, a); // 親プロセス
else
printf("fork()に失敗しました\n"); // エラー
}Go言語は、その特性上fork()を直接呼ぶことを推奨せず、パッケージに関数を用意していません。
Goで新しいプロセスを生やしたい場合、基本的にos/execパッケージを使う以外の方法はありません。
詳しくは6-2で触れます。
exec
プロセス本体/PIDを変えないまま、プロセスで実行するプログラムをまるっと入れ替えます。 まるっと入れ替える対象は今いるプロセスなので、これが呼ばれた後の処理は基本的に実行されません。
func Exec(argv0 string, argv []string, envv []string) errorforkとexecの合わせ技
1-2で見たように、forkとexecの合わせ技を使用することで、別プログラムを子プロセスで実行することが可能になります。

今のコードの挙動を見る
現在のmain.goは、os/exec.Cmdを使って指定されたエントリーポイントを実行しています。
// 作成した簡易コンテナ内でエントリーポイントを実行
cmd := exec.Command(c.EntryPoint[0], c.EntryPoint[1:]...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
return errors.WithStack(err)
}
return nilこのプログラムを実行した時、どのようにプロセスが生成されるのか確かめてみましょう。
シェルを2つ立ち上げて下さい。片方はプログラム実行用、もう片方は挙動確認用です。
まずはプログラム実行用のシェルのPIDを調べます。以下のコマンドで表示できます。
echo $$この出力結果を使って、挙動確認用シェルで以下のコマンドを実行します。
watch -n 1 pstree -p {プログラム実行用シェルのPID}このコマンドは、指定したPIDをルートとするプロセスツリーを、一定時間ごとに更新しながら表示してくれます。
このような表示になればOKです (PIDは任意)。
Every 1.0s: pstree -p 35107
bash(35107)bashの横に出ている数字はPIDです。
この状態で、プログラム実行用シェルでプログラムを実行します。
ビルド・実行が走ると、bashが子プロセスとして実行されるはずです (見た目ではわかりませんが)。
$ make run
go build -o main *.go
./main run bash
$この時点で挙動確認用シェルを見てみましょう。
Every 1.0s: pstree -p 35107
bash(35107)---make(131848)---main(132749)-+-bash(132757)
|-{main}(132750)
|-{main}(132751)
|-{main}(132752)
|-{main}(132753)
|-{main}(132754)
`-{main}(132755)bashの子プロセスとしてmake upが実行され、make upの子プロセスとしてmain (今回作ったプログラム) が実行され、その子プロセスとして再びbashが実行されているという構図になっていますね。
実はos/exec.Cmdが外部コマンドを実行する際、内部的にfork + execを実行しているため、mainの子プロセスとしてbashが呼び出されているわけです。
syscallのexecを単体で使ってみる
さあ、いよいよ実装課題に入ります!unix.Exec()のsyscallを使って、os/exec.Cmdを置き換えてみましょう!
func Exec(argv0 string, argv []string, envv []string) errorヒント1
基本的には単純に置き換えちゃいましょう!
stdin、stdout、stderrは、プロセスがまるっと置き換わるので考慮しなくても大丈夫です。
ヒント2
unix.Execのargv0は絶対パスである必要があります。
コマンド名から絶対パスを見つける関数、どうやらos/execにありそうですよ...?
想定解答
想定解答
// runサブコマンド
func runCommand(c Config) error {
// cgroupの設定
if err := SetupCgroup(c.Name, os.Getpid(), c.Cgroup); err != nil {
return errors.WithStack(err)
}
// rootfsの設定
_ = unix.Unshare(unix.CLONE_NEWNS) // rootfsで使うので、Namespace系の処理だが仮置き
if err := SetupRootfs(c.Rootfs); err != nil {
return errors.WithStack(err)
}
// 作成した簡易コンテナ内でエントリーポイントを実行
cmd := exec.Command(c.EntryPoint[0], c.EntryPoint[1:]...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
return errors.WithStack(err)
}
path, err := exec.LookPath(c.EntryPoint[0])
if err != nil {
return errors.WithStack(err)
}
if err := unix.Exec(path, c.EntryPoint, os.Environ()); err != nil {
return errors.WithStack(err)
}
return nil
}挙動が変わったことを確かめる
先程コードの挙動を見たときと同じように実行すると、挙動確認用シェルの画面は以下のような表示になるはずです。
Every 1.0s: pstree -p 35107
bash(35107)---make(145940)---bash(145989)無事mainのプログラムがbashに置き換わる形で実行されていますね!
forkとexecを分けて考える感覚、わかってきましたか?