シリーズ一覧
- 作って理解するコンテナ#1 - 導入編
- 作って理解するコンテナ#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言語においてもポイントになりますが初めて触れる場合は少々難しい場合があります。
そのため、作業用ディレクトリにおいては各テーマごとにプロジェクトを作成(=ディレクトリを作成)します。
モジュールの初期化ができたら、以下のコードを実装します。
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を確認します。
cat /etc/passwd | grep :1000:
== 実行結果 =================================================
user:x:1000:1000:user:/home/user:/bin/bash
cat /etc/group | grep :1000:
== 実行結果 =================================================
user:x:1000:
ログインしているユーザで実行したため、このプロセスのユーザID/グループIDはユーザ自身およびユーザが所属しているメイングループになっています。
ログインしているユーザの情報は以下で確認できます。
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となっており、一致していることが確認できます。
ここまではよくあるコマンドでの確認でしたが、今回はコンテナの仕組みを理解していくことが目的ですので、もう少しプロセスの本体(?)に迫ってみます。
※まだ実行したプログラムは終了しないでくださいね
深掘りしてみる
以下のコマンドを実行してみてください。
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
ファイルはシンボリックリンクであり、プロセスが実行しているプログラム自体を参照しています。
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
ファイルもまたシンボリックリンクであり、プロセスが現在アクセスしているディレクトリを参照しています。
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と現在のディレクトリ位置を確認します。
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
を確認します。
ls -l /proc/68150/cwd
== 実行結果 =================================================
lrwxrwxrwx 1 pyxgun pyxgun 0 Feb 16 09:58 /proc/68150/cwd -> /home/user
「ターミナルC」上のbashがアクセスしているディレクトリと、cwd
ファイルの参照先が同一になっています。
次に、「ターミナルC」上でほかのディレクトリに移動してみます。今回は/etcに移動します。
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
を確認してみます。
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」で以下のコマンドを実行してみてください。
cd fd
echo Hello World from Terminal B! > 1
== 実行結果 =================================================
なし
echoコマンドを実行しましたが、「ターミナルB」にはなにも表示されていないです。
その代わりに「ターミナルA」のほうで、Hello World from Terminal B!
が表示されていると思います。
Hello World from Terminal B!
これは、プログラムが起動しているプロセスが接続している端末に対して文字列を書き込んだため、この端末に接続しているプロセス側=「ターミナルA」で表示されています。
仮想端末も深掘りしていくといろいろと面白いので、興味が湧いた方はぜひ調べてみてください。
このように、/proc/{pid}
ディレクトリ内には、プログラムの実行環境に関する情報が詰まっています。
上記で説明したものはすべてコンテナを実装するうえで重要なものです。
本シリーズで今後コンテナランタイムを実装していく際は、表面的な実行結果だけでなく、 必ず/proc/{pid}
ディレクトリ内の情報を参照 し、コンテナとして動作しているプロセスが適切に設定されたのかを確認します。
そのほかにもns/
ディレクトリやmounts
ファイル、uid_map
やcgroup
など、コンテナを構成するうえで欠かせない重要なファイルが多く存在しています。
これらのファイルは今回は説明しませんが、該当する機能を実装する際に説明します。
今回のまとめ
今回はプロセスについて深掘りしてみました。
/proc/{pid}
ディレクトリには、そのプロセスの実行環境に関する情報がつまっていることが確認できましたね。
一部ファイルは特殊なファイルだったりするので確認が難しいですが、基本的には cat
で開けますので、今回紹介していないものも含めて一度漁ってみることをおすすめします。
次回予告
次回は「名前空間:PID編」を予定しています。
名前空間?PID?ということも含めて、実際にコンテナランタイムの実装を始めながら、重要な仕組みである「名前空間」を深堀りできればと思います。