Skip to content

3-1. os/exec.Cmdとexec (syscall)

ここからはNamespaceの機能を実装していきます。
Namespaceでは特にプロセスの概念が大事になってくるので、プロセスの動きを体感してみましょう!

【前提】この節で扱うsyscall

fork

自らをコピーしてプロセスを生成します。
以下はCの関数定義です。

c
pid_t fork(void);

forkにおいては処理が終わった後、親プロセスと子プロセスが同じ地点から処理を始めます。すなわち、子プロセスは親プロセスにあった変数を全てコピーして引き継ぎます。
自分が親か子かは、返り値で判断します。

c
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を変えないまま、プロセスで実行するプログラムをまるっと入れ替えます。 まるっと入れ替える対象は今いるプロセスなので、これが呼ばれた後の処理は基本的に実行されません

go
func Exec(argv0 string, argv []string, envv []string) error

forkとexecの合わせ技

1-2で見たように、forkとexecの合わせ技を使用することで、別プログラムを子プロセスで実行することが可能になります。

forkとexec

今のコードの挙動を見る

現在のmain.goは、os/exec.Cmdを使って指定されたエントリーポイントを実行しています。

go
  // 作成した簡易コンテナ内でエントリーポイントを実行
  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を調べます。以下のコマンドで表示できます。

bash
echo $$

この出力結果を使って、挙動確認用シェルで以下のコマンドを実行します。

bash
watch -n 1 pstree -p {プログラム実行用シェルのPID}

このコマンドは、指定したPIDをルートとするプロセスツリーを、一定時間ごとに更新しながら表示してくれます。
このような表示になればOKです (PIDは任意)。

plaintext
Every 1.0s: pstree -p 35107

bash(35107)

bash横に出ている数字はPIDです。

この状態で、プログラム実行用シェルでプログラムを実行します。
ビルド・実行が走ると、bash子プロセスとして実行されるはずです (見た目ではわかりませんが)。

console
$ make run
go build -o main *.go
./main run bash
$

この時点で挙動確認用シェルを見てみましょう。

plaintext
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置き換えてみましょう!

go
func Exec(argv0 string, argv []string, envv []string) error
ヒント1

基本的には単純に置き換えちゃいましょう!
stdin、stdout、stderrは、プロセスがまるっと置き換わるので考慮しなくても大丈夫です。

ヒント2

unix.Execargv0絶対パスである必要があります。
コマンド名から絶対パスを見つける関数、どうやらos/execにありそうですよ...?

想定解答

想定解答
go
// 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
}

挙動が変わったことを確かめる

先程コードの挙動を見たときと同じように実行すると、挙動確認用シェルの画面は以下のような表示になるはずです。

plaintext
Every 1.0s: pstree -p 35107

bash(35107)---make(145940)---bash(145989)

無事mainのプログラムがbashに置き換わる形で実行されていますね!

forkexecを分けて考える感覚、わかってきましたか?