前回までのあらすじ
- GitHubを利用して、おじさんの成果物を公開する準備が整いつつあるよ
- 目標を立てたよ、LINE風アプリが作れるようになることを目指すよ
- 開発環境の差異を軽減することが出来ないか考えてみるよ
- Webサーバを構築してみるよ
今回やること
- Webサーバの一つであるApache HTTP Serverの起動にチャレンジしてみる。
Dockerのおさらい(おじさんの理解)
- Dockerfileを作成する。
- アプリを動作させる環境がどのようなものか、どのようなアプリを起動するのかについて書く。
- Linuxディストリビューション(Dockerの場合、Linuxカーネル以外のOSを構成するプログラムの集合を指す)を含むベースイメージを元に作成する。
- おじさんの場合はベースイメージにAmazon Linux 2を選択したよ。
- Dockerfileからイメージを作成する。
- イメージからコンテナを作成&起動する。
Apache HTTPD Serverのインストール
1. 公式サイトの手順を調べる(http://httpd.apache.org/)
- 公式サイトのインストール手順
- スマホのアプリのように、ダウンロードしてインストール完了といった単純な方法が書かれていないよ。
- インストーラの代わりに、Apache HTTPD Serverのプログラムコードが公開されているよ。
- プログラムコードのことをソースコードとも呼ぶらしいよ。以後、おじさんはソースコードと呼ぶことにするよ。
- 何やらコンパイルと呼ぶ作業?が必要みたい。
1-1. コンパイルって何だろう・・・
-
よくわからないからおじさん頑張って調べてみたよ。
-
Apache HTTPD Serverの場合、ソースコード(プログラムコード)は、C言語と呼ばれる種類の表現方法で書かれていることがわかったよ。
-
macOS、Windows、LinuxなどのOSは、ソースコードのままではアプリとして実行することが出来ないみたいだよ・・・。
-
ソースコード(プログラムコード)をコンピュータに与えれば、コンピュータは指示通りに動くものだと思っていたよ・・・。
-
ソースコードを元に、アプリとして実行するには次の手順が必要みたいだよ。
- (1) ソースコードをダウンロードする。(Apache HTTPD Serverのソースコードはここにあるよ)
- (2) ソースコードをOSが求める仕様通りの形式に変換・翻訳・整形する。
- (3) オブジェクトファイルや、その他、誰かが作成した別のオブジェクトファイル(再利用可能なプログラムの部品をライブラリと呼ぶらしいよ)を、合体させたり、OSが求める仕様通りの形式に整形する。
- (4) リンカによって作成されたアプリは、その内容をメインメモリに読み込むことで、アプリとして起動することが可能になるみたいだよ。
- アプリをメインメモリに展開するソフトウェアを、ローダと呼ぶみたいだよ。
-
ここまでの調査で、コンパイルとは、ソースコードをOSが理解出来る形式に変換・翻訳する行為だと理解したよ。
- Apache HTTP Serverの公式サイトに書かれている「コンパイル」とは、コンパイルとリンクの2つの作業を総称していることがわかったよ。
-
要するに、ソースコードから、OS上で実行可能なアプリに変換・翻訳・整形する手順が書かれていたことがわかったよ。
-
おじさんはプログラムのこと、まだよくわからないんだけど、ネットに落ちている情報をみると、おじさんのmacOSでもC言語をコンパイルすることができるみたいなんだ。
-
前記の内容がいまいち実感できないから、簡単なC言語のプログラムを書いて、前記の図の流れを追体験してみたよ。
- 注意: 事前にApple Storeから、Xcodeというアプリをインストールしておかないと出来ない作業みたいだよ。
-
C言語のプログラムをVS Codeなどで、「main.c」というファイル名で下記の通り書いてみたよ。(おじさんも書いている内容の意味はわからないけど、Hello って文字を表示するだけのプログラムみたいだよ。)
# include <stdio.h>
int main(int argc, char *argv[]) {
printf("Hello\n");
return 0;
}
- 続いて、ターミナルを開いて、main.cの保存先(おじさんの場合は/Users/oyaji以下に保存したよ)に作業するディレクトリを移動するよ。
cd /Users/oyaji
- 続いて、プリプロセッサを試してみるよ。 macOSの環境では「gcc」と呼ばれるコマンドを利用することで試すことが出来るよ。
gcc -E main.c
- すると、ぐちゃぐちゃ〜っと、プログラムコードのようなものが表示されるよ。長いから省略するけど、これが前処理(プリプロセス)したC言語のソースコードだよ。
- #include(何かはおじさんもまだわからないけど)という記述が消えて、代わりにプログラムっぽい何かに置き換えられているよ・・・。
- どうも、人間のために便利な表現がC言語は可能のようだけど、それを、コンピュータにとって都合の良い形に展開?整形?した結果のようだよ。
// 〜 中略 〜
int printf(const char * restrict, ...) __attribute__((__format__ (__printf__, 1, 2)));
int putc(int, FILE *);
int putchar(int);
// 〜 中略 〜
int main(int argc, char *argv[]) {
printf("Hello\n");
return 0;
}
- プリプロセッサの仕事が何かは、実際にC言語をやってみないとよくわからなさそうだね。
- 続いて、コンパイルを試してみるよ。 ターミナル上で次のコマンドを入力するとC言語のソースコードが、アセンブリ言語のソースコードに変換されるよ。
gcc -S main.c
- 実行すると、main.cと同じディレクトリ内にmain.sというファイルが作られているよ。これがアセンブリ言語に変換した結果だよ。
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 14
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -20(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello\n"
.subsections_via_symbols
- 何書いているか、だんだんよくわからなくなってきたね。でも、アセンブリ言語も人間のためのプログラミング言語らしいよ。
- アセンブリ言語の状態ではコンピュータは理解することが出来ないらしいよ。続いて、アセンブリ言語(main.s)から、コンピュータが理解可能なマシン語に翻訳してみるよ。
- ターミナルで引き続き次のコマンドを実行すると、アセンブルが実行されるよ。
as -o main.o main.s
- 成功すると、main.sと同じディレクトリに、main.o(オブジェクトファイル)が出来上がってるよ。
- この状態だと、アプリを実行するために必要なパーツ(ライブラリ)や、OSが期待する形になってないみたいだよ。
- 続いて、次のコマンドでリンクしてみるよ。(本当はldというリンカが使われるのだけど、指定するパラメータが複雑なのでgccを使うよ)
gcc -v -o main main.o
- 実行結果が表示されるよ。(-vをつけているので) 「ld」というリンカが使われていることがわかるよ。
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -dynamic -arch x86_64 -macosx_version_min 10.14.0 -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk -o main main.o -L/usr/local/lib -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/10.0.1/lib/darwin/libclang_rt.osx.a
- リンク後のファイルは、main.oと同じディレクトリ内に「main」というファイルで作成されているよ。
- 次のコマンドで、試しにターミナル上で「main」を実行してみるよ。
./main
- 実行結果だよ。ターミナル上に「Hello」と表示されたよ。
- なんだか、文字を表示するだけでも、大変なことが起きてるんだね。
- C言語が何かよくわからないけど、C言語で書かれたソースコードから、実行可能なアプリになるまでを試すことが出来たよ・・・。
1-2. コンパイルの調査で、おじさんにとって衝撃的だったこと
- ソースコード(プログラムコード)は、機械のために存在すると、おじさんは考えていたけど、むしろ人間のために存在するという事実に驚いたよ。
- ABIに準拠するファイル形式は、2進数(0と1の2値)の羅列で構成されているようなので、人間が手作業で作成するのは非効率みたいなんだ。
- Apache HTTPD Serverの場合は、C言語?と呼ばれる種類のソースコードで開発されているみたいだけど、それは開発効率を改善するためだって理解したよ。
1-3. 実行可能なアプリの状態で配布しないのはなぜだろう・・・
- おじさんは未確認だけど、Windows用のインストーラは用意してくれているみたいだよ。
- 前記の通り、OSが求めるアプリの形式は、OSの種類やCPUによって異なるみたいなんだ。Linuxだけをみても、複数の種類が存在するので、全ての環境に最適なアプリの形で配布するというのは無理がありそうだよ・・・。
- ソースコードだけを提供して、実行する環境毎に最適なアプリを作成出来るようにした方が効率は良さそうだよね。(本当の理由はおじさんにはわからないけどね)
1-4. もう少し簡単そうな方法を調べてみるよ・・・
- 実際にコンパイルとやらを試してみるべきかもだけど、それは別の機会に試すことにするよ。(むしろ試した方が楽しそうだけど・・・)
- ちょっと切り口を変えて、Amazonの公式サイトにApache HTTPD Serverのインストール手順が存在するか確認してみるよ。(ベースイメージにAmazon Linux 2を選択していたことを思い出したよ)
2. AWS(Amazon Web Services)の手順を調べる
- Dockerの例ではなかったけれど、AWSが提供するクラウドコンピュータ上にApache HTTPD Serverをインストールする手順があったよ。
- Amazon Linux 2には、「yum」と呼ばれるインストーラが含まれるみたい!! うれしい。
- yumは、あらかじめソースコードからアプリの形式に変換・翻訳済みのファイルを、インストールしてくれるソフトウェアのようだよ。
- インストールだけではなく、インストール済みのソフトウェアのアップデートにも対応してくれるものみたい。
- macOSのApp Storeや、Windows Updateのようなものなのかな。
- ということで、おじさん、AWS上の手順を一度試してみることにするよ。
2-1. Dockerfileを書く前に・・・
- 調べて→Dockerfileに書いて→イメージファイルを作成して→起動して・・・を繰り返せばいつか成功すると思うけど、なんだか効率が悪そうだよ。一発で成功するとは思えないし・・・。
- コンテナ内で直接インストール作業を試すことも出来るみたいだから、次の作業順でチャレンジしてみるよ。
- はじめにコンテナ内で直接インストール作業を試す。 試行錯誤する。
- インストール作業に成功すれば、その作業をDockerfileに書く。
- Dockerfileからイメージファイルを作成する。
- イメージファイルから好きなときにWebサーバを起動することが出来るようになる。ハッピー。
2-2. Amazon Linux 2のベースイメージからコンテナを作成&起動&試行錯誤する準備
docker imagesで、作成済み or ダウンロード済みのイメージ一覧を取得する。
docker images
一覧のIMAGE IDをみると、「b94321659aca」に該当するイメージが、ベースイメージのAmazon Linux 2であることがわかるよ。
REPOSITORY TAG IMAGE ID CREATED SIZE
ojisanimg1 1.0 826623e781ef 19 hours ago 375MB
amazonlinux 2.0.20190508 b94321659aca 3 weeks ago 162MB
もしも、docker imagesで、amazonlinuxが見当たらない場合は、(No.10) おじさんが、LINE風アプリを開発する - Dockerを使用してコンテナを作成&実行してみるを実施するか、次のコマンドでベースイメージをダウンロードすることが出来るよ。
docker pull amazonlinux:2.0.20190508
引き続き、次のコマンドで、ベースイメージを元にコンテナを作成&起動して、起動したコンテナ内をmacOSのターミナルから操作することが出来るようになるよ。
「b94321659aca」は、前記の「docker images」で確認したイメージIDだよ。
docker run -i -t --rm b94321659aca /bin/bash
すると、次のように表示されるよ。一見するとよくわからないけど、コンテナ内でコマンドを実行することが出来る状態なんだよ。
bash-4.2#
試しに、OSの名前を表示するコマンド uname を実行してみるよ。
bash-4.2# uname -a
Linux aeaab6158a92 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
わおっ。Linuxと表示されているよ!! おじさんはmacOS上で実行しているから、明らかにmacOSではなくて、別のどこか(コンテナだよ)で実行されたことがわかる結果だね。
コンテナ内の作業をやめるときは、exitコマンドを実行すれば良いみたいだよ。
bash-4.2# exit
「docker run」コマンドで、何やらコンテナ内でコマンドを実行出来るようになったみたいだけど、どういうことかイマイチおじさんわからないよ。
おじさんは子供のときから「説明の出来ないことはやるな」って親に言われて育ってきたから、よくわからないことは、これからひとつずつ解決していくよ。
(でもいま思ったんだけど、誰でもはじめは説明出来ないよね。実際に調べたり手を動かして試してみないとさ。)
うーん。おじさんにはわけのわからない言葉が並んでるよ。つらいね。
docker runのパラメータ | 解説 |
---|---|
-i | 標準入力を開いたままにする。 |
-t | pseudo-tty(疑似端末)を割り当てる。 |
--rm | --rmを指定すると、コンテナの停止後、停止したコンテナが自動的に削除されるようになる。(通常は、docker runを実行する都度、コンテナは新規に作成され、コンテナの停止後も、コンテナ自体は残り続ける。実験中は--rmをつけておくと便利。) |
b94321659aca | 「docker images」で確認したイメージIDだよ。(イメージIDは開発環境によって異なるよ) |
/bin/bash | コンテナ内のAmazon Linux 2に付属する、Bashと呼ばれるシェルを実行する。 |
2-2-1. 標準入力、擬似端末、シェルって何?!
- docker runのパラメータの意味を理解するには、「プロセス(Process)」「標準入出力(Standard streams)」「制御端末(tty)」「擬似端末(pseudo-tty/pty)」「シェル(shell)」の存在を理解する必要があるみたいだよ。
- なんだか本格的になってきたね・・・。おじさん不安でたまらないよ。
2-2-2. docker runと、Docker daemonと、コンテナの関係
- 概要はここに書いてあったよ。
- ここでおじさんが言いたいことは、macOSから直接、コンテナの中身をいじくり回すことなんて出来ないよってことが言いたいんだよ。(でも実際はコンテナ内にアクセスすることが出来たっぽいよね)
- docker runのやっていることは、指定するイメージファイルからコンテナを作成&起動するように、Docker daemonに対して、指示を送ることであって、docker run自体がコンテナを作成&起動することはないみたいだよ。
- イメージやコンテナの管理はDocker daemonの仕事なので、おじさんのmacOSから、コンテナの中へアクセスするには、「docker run」↔「Docker daemon」↔「コンテナ」と、中継する必要があるみたいなの。
- Docker daemonと、docker runを実行するコンピュータは、同じコンピュータとは限らない。
- そもそも、Docker Desktop for Macのように、Docker daemonは仮想マシン(の上で動作するLinux)上で動作していたり、または、どこか別の場所に設定されたコンピュータ上で動作しているかもしれないんだ。この点からも、docker runのようにdockerコマンドを介して、間接的にコンテナとやりとりする必要があると言えるよ。
2-2-3. ターミナル起動〜docker runで、コンテナ内でコマンドを実行するまでの出来事
「docker run」コマンドはターミナルアプリ経由で実行しているよね。
ところがおじさんは、ターミナルがそもそも何者なのかよくわかってないんだよね。
docker runが実行されるまでの流れを、おじさんなりに調べてみたよ。(おじさんは勉強中だから、嘘が書いているかもしれないよ)
ターミナルアプリが何者かわからないけど、macOSで動作するアプリ。だから、前記のように、コンパイル?されるなどして、作られたものであることは間違いないと思うよ。
と、それはさておき、ターミナルアプリのアイコンをクリックすると、黒い画面が表示されるよ。
こんな感じでターミナルアプリが起動する。
-
macOSのターミナルアプリをwikipediaで調べてみると、ターミナルアプリは「端末エミュレータ(terminal emulator)」であると書かれているよ。
-
続いて、端末エミュレータをwikipediaで調べてみると、端末エミュレータはコンピュータプログラムであり、端末(terminal)を再現(エミュレート)したものと書いてあるよ。
-
そもそも端末(terminal)がよくわからないので、さらにwikipediaで調べてみたよ。ここから先はおじさんの理解を書いていくよ。
macOSやLinux、UnixといったOSは、1台のコンピュータ(下図、右側のサーバと書かれている箱)を複数人に共有することが出来るみたいなんだけど、
複数人に1台のコンピュータを共有するシチュエーションで、端末エミュレータなるものが必要になるっぽいんだよ。(おじさん、絵心がなくて泣けて来るよ・・・)
AさんとBさんは、インターネットを経由して、遠隔地にあるサーバに接続して、遠隔操作することを考えてみるよ。
この場合、AさんとBさんの手元にはそれぞれ、ディスプレイやキーボードがあるけれど、遠隔地のサーバ側には、接続する人の数分のキーボードもディスプレイも接続されているわけじゃないんだ。
それでも、AさんやBさんのディスプレイやキーボードが、あたかもサーバ側に物理的に接続されているかのように、振る舞う仕組みを端末エミュレータを介して実現しているみたいだよ。(詳細は徐々に明らかにしていくよ)
このような仕組みがあるおかげで、遠隔で操作されるプログラムからすれば、物理的なキーボードで操作されているのか、それとも遠隔接続で操作されているのかを意識する必要がなくなるみたいだよ。なんだか賢いね。
続いて疑問になるのが、おじさんのmacOS自体は、おじさんの手に届くところにあるのに、なぜ端末エミュレータなんか使わなきゃならないのかってことだよ。
遠隔操作じゃないのね。
でも、ターミナルを複数起動すると、なんだか納得出来るものがあるよ。
ターミナルのウィンドウそれぞれが、独立して動作して、それぞれ異なるプログラムを動作させることが出来るようになっているよ。
つまり、1台のMacの中に、1つの接続というわけではなくて、Aさん・Bさんの例のように、ウィンドウの数だけ独立した接続が必要になるってことなんだ。
だから自分自身のコンピュータであったとしても、端末エミュレータが必要になるみたいだよ。
実際にはここまで単純ではないけれど、端末エミュレータの必要性を説明するための抽象画だと思ってほしいよ。
もう少し深掘りしていくよ。
結論を書くと端末エミュレータ自体は、シェル(Shell)と呼ばれるプログラムを(間接的に)起動して、疑似端末と呼ばれる特殊なファイルの内容を読み書きしているだけのようなんだ。 何を言っているのか、おじさんにもよくわからなくなってきたから、少しずつ整理していくよ。
と、深掘りするまえに、プロセスとは何か、標準入出力とは何かを知っておく必要があるみたいだよ。
まずはプロセスから。
アプリは起動すると、「プロセス」と呼ばれるアプリが起動した状態になるらしいよ。
例えば、WindowsのExcelやWordみたいなアプリって、複数立ち上げると、それぞれのワークブックごとに内容が異なるよね。 この例のように、同じアプリであっても起動後のアプリの状態(Excelのワークブック)は異なるように、実行中のアプリの状態を表現する単位みたいだよ。
下図はWindowsでExcelのプロセスが5つ動作しているイメージだよ。(Windowsを例にしているのは、macOSの場合だと、一つのアプリは一つのプロセスでしか動作しないからだよ。macOS版のExcelを例にすると、ワークブックをいくら開こうが、Excelのプロセスは一つだけだよ。)
ここまでで、アプリ(プログラム)は、実行中になると「プロセス」と呼ばれる状態になるってことがわかったよ。
プロセスは、プロセス同士で通信する手段・方法が様々用意されているみたいだよ。
プロセス同士の通信を、プロセス間通信(IPC)と呼ぶらしいよ。
プロセス間通信の内容によっては、アプリ側であらかじめ考慮しておく必要があるけれど、標準で用意されているデータの伝送手段があって、この標準的な伝送手段を標準入出力と呼ぶみたいだよ。
標準入出力は、起動したプロセスに標準的に与えられるデータの伝送手段で、入力側も出力側も共に何かしらのファイルに関連付けられているみたいだよ。
実験のため、次のコマンドを試してみるよ。
echo oyaji
こんな感じで、ターミナルに「oyaji」と表示されるだけだよ。
echoコマンドのマニュアルを表示して意味を確認してみるよ。マニュアルを表示するには「man」コマンドを使えば良いよ。
man echo
すると、次のようにマニュアルが表示されるよ。(終了するには、「q」アルファベットのキューを入力)
マニュアルには「標準出力(standard output)に引数を書き込む」旨が書かれているよ。
この場合の引数とは、「oyaji」にあたる部分だよ、つまり、echoコマンドのパラメータのことだよ。
echoコマンドも例外ではなく、実行中はプロセスとして動作するよ。
再び、echoコマンドのマニュアルに書いてあった「標準出力(standard output)に引数を書き込む」が重要になってくるよ。
「echo oyaji」を実行すると、「oyaji」の文字が標準出力に書き込まれるみたい。
でも実際はターミナルのウィンドウに「oyaji」と出力(表示)されたよ。これは、ターミナルのプロセスと、echoコマンドの標準出力との間に何かしらの関係があることを示唆するものだよ。
この謎を解き明かすためには、macOSやLinux、UnixといったOSの基本原理である「デバイスファイル(スペシャルファイル」についても知っておく必要があるよ。
話がややこしくなってきたので、箸休めに、おじさんの書いた猫の絵で癒やされてほしいよ。
どうだろう。癒やされたかな。
次はデバイスファイル(スペシャルファイル)についてだよ。
Unixと呼ばれるOS(や、Unix系OSと呼ばれるLinuxなど)では、Everything is a fileといった設計思想があるらしくて、ありとあらゆるものをファイル(ただしくは、ファイルディスクリプタ)で表現しようという考えみたいだよ。
この設計思想のおかげで、入出力といったデータの流れのあるものをファイルという統一された方法で操作出来るようになるみたい。
例えば、CDやDVDのような機器の操作から、インターネットなどのネットワーク処理(socket)すらも、Unix系のOSでは、ファイル(ファイルディスクリプタ)として扱うようなんだ。
このような特殊なファイルをデバイスファイル(またはスペシャルファイル)と呼ぶみたいだよ。
ここで言いたいのは、macOSやLinux、Unix上での「ファイル」とは、おじさんのポエムのようなテキストファイルだけではなくて、ハードウェア(CDやDVD、キーボードやディスプレイなど)やネットワークなどの、入出力といったデータの流れのあるものを指すこともあるってことなんだよ。
おじさんが使っているmacOSでも同じことが言えるんだよ。
ここまでを整理すると次の情報が手に入ったよ。
- アプリは起動するとプロセスと呼ばれる状態となってプログラムが実行されるよ。
- プロセスには、アプリが意識していなくても、プロセスへのデータの通信経路が作成され、これを標準入出力と呼ぶ。
- 標準入出力は、データの流れを表すもので、データの入力元に何かしらのファイル、データの出力先にも何かしらのファイルが関連付けられているよ。
- アプリ(プロセス)からすれば、標準入出力の元・先が何であるのかを知る必要はないみたいだよ。
- Unix系OSでは、データの入出力といったデータの流れをファイル(ファイルディスクリプタ)で表現しているよ。macOSも同様だよ。
- おじさんのポエムのようなファイルとは別に、例えば、ディスプレイやキーボード、ネットワークといった機器もファイル(ファイルディスクリプタ)で表現されている。このような特殊なファイルをデバイスファイル(またはスペシャルファイル)と呼ぶみたいだよ。
これらの情報を元に、ターミナルが起動して、echoコマンドを実行するまでの出来事を詳細に解説してみるよ。
まずターミナルアプリがユーザの操作をきっかけに実行されるよ。当然、ターミナルアプリもプロセスとして実行するよ。
ターミナルプロセスがopenシステムコールなどを使用して、「/dev/ptmx」(擬似端末)のデバイスファイル(スペシャルファイル)にアクセス。
すると、擬似端末のマスタとスレーブのペアが作成される。(このファイルが何者かは後述)
以降、擬似端末のことをpty(マスタ)、pty(スレーブ)と書く。
続いて、ターミナルプロセスがforkシステムコールを実行する。forkシステムコールを実行すると、実行元プロセスの写しが作成される。(プログラムの実行状態なども含め複製される)
ターミナルプロセス(写し)が、dup2システムコールを実行し、ターミナルプロセス(写し)の標準入出力先をpty(スレーブ)に変更する。
ターミナルプロセス(写し)が、execシステムコールを実行し、loginコマンドのプロセスにターミナルプロセス(写し)を置き換える。
loginプロセスに置き換え後も、標準入出力先はpty(スレーブ)に維持される。(ファイルディスクリプタの状態はfork/exec後も維持されるため)
loginプロセスはログインユーザ固有の設定(例えば、作業ディレクトリをホームディレクトリに変更する)などを行い、forkシステムコールを実行する。
loginプロセスの複製が作成される。
loginプロセス(写し)は、execシステムコールを実行して、loginプロセス(写し)をbashプロセスに置き換える。
loginプロセス(写しではない)は、closeシステムコールを実行して標準入出力を閉じる。(この時点でloginプロセスの仕事は何もないため)
bashプロセスが起動する。なお、bashプロセスはシェルと呼ばれるプログラム。
ターミナルプロセスに対して、「echo oyaji」と入力すると、ターミナルプロセスはpty(マスタ)に「echo oyaji」と書き込みする。
pty(マスタ)に書き込みした内容は、pty(スレーブ)の先の標準入力に流れ込むため、bashプロセス側で「echo oyaji」の入力を取り出すことが出来る。
ターミナルからの入力を間接的に受け取った後、bashプロセスはforkシステムコールを実行する。
- pty(マスタ)とpty(スレーブ)はつながっている。
- pty(マスタ)に書き込むデータは、pty(スレーブ)から読み取ることが出来る。
- pty(スレーブ)に書き込むデータは、pty(マスタ)から読み取ることが出来る。
- pty(擬似端末)は、プロセス間通信の手段の一つと言える。
bashのプロセスが複製される。
bashプロセス(写し)が、execシステムコールを実行して、bashプロセス(写し)をechoプロセスに置き換える。
echoプロセスも複製元のbashプロセスと同様に、標準入出力先はpty(スレーブ)に設定されている。
echoプロセスは、コマンドの引数(パラメータ)に与えられた内容を標準出力に書き込む処理を実行する。
引数の値は「oyaji」なので、標準出力に「oyaji」を書き込む。
標準出力先はpty(スレーブ)のため、結果的にpty(マスタ)の入力に「oyaji」のデータが流れる。
ターミナルプロセスはpty(マスタ)から、pty(スレーブ)の書き込みを取り出して、ターミナルウィンドウに出力する。
echoプロセスは仕事を終えたため、プロセスを終了する。
その結果こうなる。
ここまでの情報を整理をしてみるよ。
- pty(擬似端末)を使用して、プロセス間(ターミナル〜bashやbash以降のプロセス)のデータの受け渡しを実現している。
- ターミナルはpty(疑似端末)のマスタに対して流れてくるデータを表示、キーボードからの入力を、pty(疑似端末)のマスタに書き込む。
- ターミナルはforkシステムコールを使用して、自身のプロセスを複製する。
- 複製したプロセスの標準入出力先をpty(スレーブ)に設定する。なお、pty(スレーブ)の名前を取得するには、ptsnameシステムコールを使用する。
- 複製したプロセスから、execシステムコールを実行して、複製したプロセスをloginプロセスに置き換える。
- なぜforkとexecが別れているのか考えてみると面白いと思うよ。
- なぜわざわざ、プロセスを複製してから、目的のプログラムのプロセスに置き換えるように設計されているのか。
- こんな言葉があるよ。
- loginプロセスでは、ログインユーザ固有の設定(例えば、作業ディレクトリをユーザのホームディレクトリに設定する)など実行した後に、forkシステムコールを実行する。
- loginプロセス(写し)では、execシステムコールを実行し、loginプロセス(写し)をbashプロセスに置き換える。
- bashプロセスでは、fork&execで、プロセス複製後にechoプロセスに置き換える。
- echoプロセスは標準出力に書き込みする。標準出力先はpty(スレーブ)のため、ターミナルが監視するpty(マスタ)に伝わり、ターミナルの画面上にechoプロセスが書き込む内容が出力される。
いまいち、擬似端末(pty)の実感が沸かないと思うんだけど、簡単に試すことが出来るよ。
下図はターミナルを2つ起動してみた例だよ。
ttyコマンドを実行することで、標準入力に設定されているpty(スレーブ)を確認することが出来るよ。
tty
すると、それぞれのターミナルに異なる内容の文字が表示されるはずだよ。
おじさんの場合は、「/dev/ttys000」と「/dev/ttys001」とそれぞれ表示されたよ。
「/dev/ttys000」と「/dev/ttys001」それぞれが、pty(スレーブ)のファイルを表しているよ。このような特殊なファイルをデバイスファイル(スペシャルファイル)と呼ぶらしいよ。
ターミナルのウィンドウそれぞれに、pty(マスタ)・pty(スレーブ)のペアが設定されていることがわかるよ。
試しに、ターミナルのウィンドウそれぞれに表示されているpty(スレーブ)に対して書き込みしてみるよ。
次のコマンドを実行すると、/dev/ttys001に対して、helloという文字を書き込むことになるよ。
echoコマンド自体は引数(hello)の内容を標準出力に書き込むというものだったけど、「>」(リダイレクトと呼ぶ)を書くことで、
標準出力を「/dev/ttys001」に変更することになるよ。(まるでdup2システムコールそのものだね)
なお、「>」リダイレクトや、echoプロセスの起動は、bashプロセス(シェル)が実行していることだよ。
echo hello > /dev/ttys001
うーん。どんどん理解してきた気がするね。
bashはシェルと呼ばれるプログラムだけど、macOSやLinuxカーネルといったOSと対話するための手段の一つだよ。
シェルはOSが提供する機能(システムコール)を実行して、他のアプリやコマンドを起動したり、簡単なプログラムも実行することが出来るみたいだよ。
ついでに、実行中のプロセスを確認する方法もここで書き残しておこうと思うんだ。
次の「ps」コマンドで実行中のプロセスを確認することが出来るよ。
ps -eaf
わわっと、動いているプロセスが多すぎて、何がなんだか・・・。
「Terminal」の文言が含まれる行に限定して表示されるように改良してみるよ。
ps -eaf | grep Terminal
「|」は、パイプといって、リダイレクトのように標準入出力を変更するものだよ。
psコマンドと、grepコマンドの意味はそれぞれ下図の通りだよ。
この2つのコマンドを、パイプと呼ばれるデバイスファイル(スペシャルファイル)を使って、標準入出力をそれぞれ繋ぎ合わせることで
psコマンドの結果をgrepコマンドでフィルタリングすることが出来るんだよ。
実行結果だよ。(注:UID、PIDなどのヘッダは、わかりやすくするために手で追加したものだよ)
UID PID PPID C STIME TTY TIME CMD
501 84232 1 0 5:41PM ?? 0:12.44 /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
501 93153 84234 0 7:27PM ttys000 0:00.00 grep Terminal
PID(プロセスID)列は、プロセスを識別するための固有の番号を表すよ。
この例の場合、PIDが「84232」のプロセスがターミナルアプリのプロセスだよ。
なお、PPID(親プロセスID)列は、fork元の親プロセスのプロセスIDを意味するよ。 PIDが1のプロセスからTerminalが作成されていることがわかるけど、PIDが1のプロセスは特別なプロセスで、initプロセス等と呼ぶことがあるみたいだよ。(はじめに作成されるプロセスだよ)
引き続き、ターミナルのプロセスID(84232)で検索してみるよ。
ps -eaf | grep 84232
実行結果だよ。 loginプロセスが2つあることに注目だよ。
これはターミナルウィンドウを2つ開いているからだよ。
UID PID PPID C STIME TTY TIME CMD
501 84232 1 0 9:48PM ?? 0:30.23 /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal
0 84233 84232 0 9:48PM ttys000 0:00.01 login -pf oyaji
501 93246 84234 0 4:09AM ttys000 0:00.00 grep 84232
0 93137 84232 0 4:05AM ttys001 0:00.02 login -pf oyaji
さらに、1つ目のloginプロセスのプロセスIDで検索してみるよ。
ps -eaf | grep 84233
結果だよ。 bashプロセスがあるね。
UID PID PPID C STIME TTY TIME CMD
0 84233 84232 0 9:48PM ttys000 0:00.01 login -pf oyaji
501 84234 84233 0 9:48PM ttys000 0:00.10 -bash
501 93302 84234 0 4:11AM ttys000 0:00.00 grep 84233
もう片側のloginプロセスも。
ps -eaf | grep 93137
やっぱりbashプロセスが存在するね。前記の説明通りプロセスは作成されていることがわかるよ。
プロセスは親子関係(forkするプロセス、forkされたプロセス)があることもわかったよ。
UID PID PPID C STIME TTY TIME CMD
501 93351 84234 0 4:13AM ttys000 0:00.00 grep 93137
0 93137 84232 0 4:05AM ttys001 0:00.02 login -pf oyaji
501 93138 93137 0 4:05AM ttys001 0:00.01 -bash
長かったけど、docker runのパラメータの意味を理解することが本来の課題だったんだよ。長くて忘れかけていたよ。
こんなコマンドだったよ。
docker run -i -t --rm b94321659aca /bin/bash
下図がdocker runの全貌だよ。ここまでの情報を集結させて挑むことにするよ。
- docker runの右端にある「/bin/bash」とは、図の中にあるコンテナプロセス内で、どのようなプログラムを実行するのかを指定しているんだよ。この例では、「/bin/bash」なので、「コンテナ内の/binディレクトリのなかにある、bashコマンドを実行する」という意味になるんだよ。
- この指定を別のコマンドに変えるだけで、コンテナ内で実行するコマンドを変更することが出来るよ。
- 「-t」は、docker runの実行元で、コンテナプロセス内の標準入出力を中継出来るように、擬似端末(pty)を作成することを指示するものなんだよ。(Docker daemonが動作するLinux上で擬似端末を作るように指示)
- 「-i」は、ターミナルからの入力をコンテナ側に伝搬することが出来るように、Linux側の標準入力を有効にする設定のようだよ。
- 「-rm」は、コンテナ終了後に、コンテナを削除するように指示するものだよ。(ゴミコンテナが残らないように)
- docker(run)と、Docker daemonは、それぞれ異なるコンピュータ上で動作する可能性があるので、ネットワークやインターネットを経由して送受信する必要があるんだ。おじさんも今はよくわからないけど、ソケットと呼ばれる通信方法で実現しているみたいだよ。
あとがき
うへーーー。おじさんちょっと疲れたよ。前準備だけでこんなに大変だと思わなかったよ〜。
でも何でも積み重ねだから、徐々に楽になっていくことを期待するよ。
次回は、コンテナ内をこねくりまわして、Apache HTTP Serverを何がなんでもインストールしてみるよ。
そうそう忘れてた。
macOSのloginコマンドなどのプログラムは、ソースコード(Libsystem-1252.200.5)にて公開されているよ。(C言語で書かれているみたいだよ)
コンパイルする方法もどこかに書かれていることだろうから、コマンドを改造してみるのも面白いかもしれないね。
ターミナルアプリのソースコードが見当たらなかったので、ターミナル周辺に関しては状況証拠や他のターミナルのプログラムを参考に勉強してみたよ。