作って理解するコンテナ#2 - プロセス編

## シリーズ一覧
1. [作って理解するコンテナ#1 - 導入編](https://qiita.com/pyxgun/items/f22832e7f412375ef8a5)
2. 作って理解するコンテナ#2 - プロセス編　← 今回の記事

## はじめに
今回から、コンテナの仕組みを深堀り/実装していきます。  
まずはコンテナを理解するのに欠かせない「プロセス」について深掘りしてみます。

## プロセス
「プロセス」とは、あるプログラムを起動する際に"そのプログラムを実行する環境やプログラムそのもの情報"などが含まれたカーネルオブジェクトです。  
プロセスに含まれる情報はカーネルの設計により異なりますが、以下のような情報が含まれています。

* プロセスID
* 親プロセスID
* 実行ファイルパス (起動するプログラムのパス)
* ユーザID/グループID
* ファイルディスクリプタ
* カレントディレクトリ
* ページテーブル (仮想メモリアドレス/物理メモリアドレスのマッピング)　　など...

カーネルは実行するプログラムを直接扱うのではなく「プロセス」という一つのかたまりを扱い、プロセスに含まれている資源情報や権限情報をもとに実際のプログラムを処理します。  
「カーネルはプロセスを扱う」という実装により、複数のプロセスを随時切り替え/プロセスに含まれる情報をもとにメモリへアクセスするといった、安全なプログラム実行および並列実行が可能になっています。  

簡単に表現するのであれば、カーネルにとってのプログラム実行指示書のようなものです。  

### 実装してみる - プロセス情報取得
せっかくなので、プロセス情報を取得するコードを書いてみます。  
作業用ディレクトリに `process_info` ディレクトリを作成、その中で `main.go` というファイルを作成します。
```ディレクトリ構成
.
├── container
└── workspace
    └── process_info
        └── main.go  <- 編集ファイル
```

以下のコマンドを実行し、モジュールを初期化します。
```
cd workspace/process
go mod init process_info
```
※補足：Goは一つのプロジェクトに対し、mainパッケージおよびmain関数は一つである必要があります。  
 このプロジェクトの考え方や上位階層で定義した関数へのアクセスなどは、Go言語においてもポイントになりますが初めて触れる場合は少々難しい場合があります。  
 そのため、作業用ディレクトリにおいては各テーマごとにプロジェクトを作成(=ディレクトリを作成)します。

モジュールの初期化ができたら、以下のコードを実装します。
```go:main.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	// プロセスID
	pid := os.Getpid()
	// 親プロセスID
	ppid := os.Getppid()
	// 実行ファイルパス
	exec_path, _ := os.Executable()
	// ユーザID
	uid := os.Getuid()
	// グループID
	gid := os.Getgid()

	fmt.Printf("プロセスID\t : %d\n", pid)
	fmt.Printf("親プロセスID\t : %d\n", ppid)
	fmt.Printf("実行ファイルパス : %s\n", exec_path)
	fmt.Printf("ユーザID\t : %d\n", uid)
	fmt.Printf("グループID\t : %d\n", gid)

	// Enterで終了
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
}
```

今回はビルドした後、実行します。
```
go build .
./process_info
```

実行すると、以下のようにプロセス情報が表示されると思います。
```
プロセスID       : 69900
親プロセスID     : 38668
実行ファイルパス  : /home/user/container-tsukuru/workspace/process/process_info
ユーザID         : 1000
グループID       : 1000
```
これらの情報を活用してちょっとだけプロセスを深堀りしてみます。
このプログラムはEnterキーを入力するまで終了せずに待機してくれるので、別のターミナルを起動しておいてください。

#### プロセスID/親プロセスID
起動した別のターミナルで、プログラムが表示しているプロセスID/親プロセスIDが一体何のプログラムを起動しているか確認してみましょう。
まずはプロセスIDからです。IDは各自の実行結果に併せて変更してください。
```
ps -P 89900

== 実行結果 =================================================
    PID PSR TTY      STAT   TIME COMMAND
  69900   0 pts/0    Sl+    0:00 ./process_info
```
プロセスIDは自分自身を表すIDですので、`process_info`プログラムになっています。  
これは表示されているプロセス情報の `実行ファイルパス` とも合致しています。 ※こちらは絶対パスですが  

次に親プロセスIDです。
```
ps -P 38668

== 実行結果 =================================================
    PID PSR TTY      STAT   TIME COMMAND
  68291   3 pts/2    Ss     0:00 -bash
```
親プロセスは、プロセスを起動したプロセスになります。今回はbashから `process_info` を起動したので、親プロセスのプログラムは `bash` になっています。

#### ユーザID/グループID
次にユーザIDとグループIDを確認します。
```sh:ユーザID
cat /etc/passwd | grep :1000:

== 実行結果 =================================================
user:x:1000:1000:user:/home/user:/bin/bash
```
```sh:グループID
cat /etc/group | grep :1000:

== 実行結果 =================================================
user:x:1000:
```
ログインしているユーザで実行したため、このプロセスのユーザID/グループIDはユーザ自身およびユーザが所属しているメイングループになっています。  
ログインしているユーザの情報は以下で確認できます。
```sh
id

== 実行結果 =================================================
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd)
```
ログインしているユーザ `user` は、ユーザID=1000、メイングループID=1000となっており、一致していることが確認できます。  

ここまではよくあるコマンドでの確認でしたが、今回はコンテナの仕組みを理解していくことが目的ですので、もう少しプロセスの本体(?)に迫ってみます。  
※まだ実行したプログラムは終了しないでくださいね

### 深掘りしてみる
以下のコマンドを実行してみてください。
```sh
cd /proc/69900
ls
```
実行すると、以下のようなディレクトリ/ファイルが確認できると思います。
```
arch_status  comm                fd                 limits     mountstats     pagemap      sessionid     status          wchan
attr         coredump_filter     fdinfo             loginuid   net            patch_state  setgroups     syscall
autogroup    cpu_resctrl_groups  gid_map            map_files  ns             personality  smaps         task
auxv         cpuset              io                 maps       numa_maps      projid_map   smaps_rollup  timens_offsets
cgroup       cwd                 ksm_merging_pages  mem        oom_adj        root         stack         timers
clear_refs   environ             ksm_stat           mountinfo  oom_score      sched        stat          timerslack_ns
cmdline      exe                 latency            mounts     oom_score_adj  schedstat    statm         uid_map
```
これらは、冒頭で説明した「そのプログラムを実行する環境やプログラムそのもの情報」になります。  
一部抜粋して詳細に見てみます。

#### `exe`ファイル
`exe`ファイルはシンボリックリンクであり、プロセスが実行しているプログラム自体を参照しています。  
```sh
ls -l exe

== 実行結果 =================================================
lrwxrwxrwx 1 user user 0 Feb 16 08:32 exe -> /home/user/container-tsukuru/workspace/process/process_info
```
これは先ほど実行したプログラムの「実行ファイルパス」と同じです。プログラム内で呼び出した `os.Executable()` はこの情報を参照/取得しているようです。

#### `cwd`ファイル
`cwd`ファイルもまたシンボリックリンクであり、プロセスが現在アクセスしているディレクトリを参照しています。
```sh
ls -l cwd

== 実行結果 =================================================
lrwxrwxrwx 1 pyxgun pyxgun 0 Feb 16 09:46 cwd -> /home/user/container-tsukuru/workspace/process/
```
アクセスしているディレクトリが変わるごとに、このシンボリックリンクも変わります。  

現在プログラムを動作させている「ターミナルA」と、今/procフォルダの中身をあさっている「ターミナルB」がありますが、もう一つ「ターミナルC」を立ち上げ、「ターミナルC」上で以下のコマンドを実行し、「ターミナルC」のbashのPIDと現在のディレクトリ位置を確認します。
```sh:ターミナルC
ps && pwd

== 実行結果 =================================================
    PID TTY          TIME CMD
  68150 pts/1    00:00:00 bash
  76805 pts/1    00:00:00 ps
/home/user
```
「ターミナルB」上で、以下のコマンドを実行し「ターミナルC」のbashプロセスの `cwd` を確認します。
```sh:ターミナルB
ls -l /proc/68150/cwd

== 実行結果 =================================================
lrwxrwxrwx 1 pyxgun pyxgun 0 Feb 16 09:58 /proc/68150/cwd -> /home/user
```
「ターミナルC」上のbashがアクセスしているディレクトリと、`cwd`ファイルの参照先が同一になっています。

次に、「ターミナルC」上でほかのディレクトリに移動してみます。今回は/etcに移動します。
```sh:ターミナルC
cd /etc && ps && pwd

== 実行結果 =================================================
    PID TTY          TIME CMD
  68150 pts/1    00:00:00 bash
  77186 pts/1    00:00:00 ps
/etc
```
bashは起動し続けているためプロセスIDは変わっていないです。
では、「ターミナルB」上で再度 `cwd` を確認してみます。
```sh:ターミナルB
ls -l /proc/68150/cwd

== 実行結果 =================================================
lrwxrwxrwx 1 pyxgun pyxgun 0 Feb 16 09:58 /proc/68150/cwd -> /etc
```
`cwd`ファイルの参照先も /etc になっていますね。
このように、プロセスがアクセスしているディレクトリは、常に `cwd`ファイルによって管理されています。

#### `net/`ディレクトリ
`net/`ディレクトリは、プロセスが利用するネットワークに関する情報が含まれています。  
この中にある `dev` ファイルを開いてみましょう。  
```
cd net
cat dev

== 実行結果(一部省略) ========================================
   Inter-|  Receive                                                 
    face | bytes     packets errs drop fifo frame compressed multicast
       lo: 122128895   851906    0    0    0     0          0         0
     eth0: 441740649  1116335    0    0    0     0          0     52850
karakuri0:         0        0    0    0    0     0          0         0  
karakuri1:     19799      274    0    0    0     0          0       162  
karakuri2:    310980     3294    0    0    0     0          0        67 
  docker0:         0        0    0    0    0     0          0         0
karakuri71418:  1324       18    0    0    0     0          0         0
karakuri71496   1076       14    0    0    0     0          0         0
```
ここで表示されているのは、プロセスが利用可能なネットワークインターフェイスの送受信に関する情報が確認できます。  
筆者の環境では、筆者が開発中のコンテナランタイム`karakuri`でコンテナを起動していますので、そのインターフェイスも確認できています。  
ここには記載しませんが、`ip a`コマンドで確認できるインターフェイスと一致していることも確認してみてください。  

#### `fd/`ディレクトリ
`fd/`ディレクトリは、プロセスが利用する標準入出力に関する情報が含まれています。  
以下のコマンドで確認してみましょう。
```
ls -l

== 実行結果 =================================================
total 0
dr-x------ 2 user user  3 Feb 16 08:52 ./
dr-xr-xr-x 9 user user  0 Feb 16 08:32 ../
lrwx------ 1 user user 64 Feb 16 08:58 0 -> /dev/pts/2
lrwx------ 1 user user 64 Feb 16 08:58 1 -> /dev/pts/2
lrwx------ 1 user user 64 Feb 16 08:58 2 -> /dev/pts/2
```
シンボリックリンクが3つありますが、それぞれ
* 0: 標準入力
* 1: 標準出力
* 2: 標準エラー

になり、今回の例ではすべて/dev/pts/2というファイルがリンクされています。  
詳細な説明をすると長くなるので簡単に、ptsは仮想端末と呼ばれるもので、bashなどを立ち上げたときに表示されている画面はこの仮想端末に接続し、文字出力や入力を待ち受ける動きになっています。  
モニターに実際の画面が表示されているのに「仮想」なの？というのをわかりやすくすると、もし仮にbashコマンドの接続先を物理端末にするのであれば、「bashが起動しているプロセスとモニターを直接ケーブルか何かで接続しようとする」ことになります。  
そんなことはできないので、プロセスは仮想的なモニター=仮想端末に接続している、とイメージしてもらえば良いです。  

蛇足にはなりますが、ちょっとだけ遊んでみます。
現在プログラムを動作させている「ターミナルA」と、今/procフォルダの中身をあさっている「ターミナルB」があり、今あなたは「ターミナルB」を操作していると思います。  
「ターミナルB」で以下のコマンドを実行してみてください。
```sh:ターミナルB
cd fd
echo Hello World from Terminal B! > 1
== 実行結果 =================================================
なし
```
echoコマンドを実行しましたが、「ターミナルB」にはなにも表示されていないです。  
その代わりに「ターミナルA」のほうで、`Hello World from Terminal B!` が表示されていると思います。  
```sh:ターミナルA
Hello World from Terminal B!
```

これは、プログラムが起動しているプロセスが接続している端末に対して文字列を書き込んだため、この端末に接続しているプロセス側＝「ターミナルA」で表示されています。  
仮想端末も深掘りしていくといろいろと面白いので、興味が湧いた方はぜひ調べてみてください。  

このように、`/proc/{pid}`ディレクトリ内には、プログラムの実行環境に関する情報が詰まっています。  
上記で説明したものはすべてコンテナを実装するうえで重要なものです。  
本シリーズで今後コンテナランタイムを実装していく際は、表面的な実行結果だけでなく、 **必ず`/proc/{pid}`ディレクトリ内の情報を参照** し、コンテナとして動作しているプロセスが適切に設定されたのかを確認します。

そのほかにも`ns/`ディレクトリや`mounts`ファイル、`uid_map`や`cgroup`など、コンテナを構成するうえで欠かせない重要なファイルが多く存在しています。  
これらのファイルは今回は説明しませんが、該当する機能を実装する際に説明します。  

## 今回のまとめ
今回はプロセスについて深掘りしてみました。  
`/proc/{pid}`ディレクトリには、そのプロセスの実行環境に関する情報がつまっていることが確認できましたね。  
一部ファイルは特殊なファイルだったりするので確認が難しいですが、基本的には `cat` で開けますので、今回紹介していないものも含めて一度漁ってみることをおすすめします。  

## 次回予告
次回は「名前空間：PID編」を予定しています。  
名前空間？PID？ということも含めて、実際にコンテナランタイムの実装を始めながら、重要な仕組みである「名前空間」を深堀りできればと思います。