たとえばRuby のちょっとしたスクリプト を試したいときにdocker run -it ruby
等でirb
を立ち上げて試したりするわけですが、よく使うdocker run
やdocker exec
の -i
, -t
ってそもそも何なんだろうとふとした時に思いました。
そして、それぞれ何のためのオプションなのか、--help
を見ても「Keep STDIN open...? pseudo-TTY...?」ぱっと理解できなくて辛くなり。
-i, --interactive Keep STDIN open even if not attached
-t, --tty Allocate a pseudo-TTY
docs.docker.com
『なるほどUNIXプロセス 』を読んでこのあたりの基礎知識が身についた気がしているので、「とりあえず付けておけばOKでしょ」ぐらいのふわっとした理解から「なぜ無いといけないのか」がわかるところまで掘り下げてみたメモです。
途中で標準入出力とかファイルディスクリプタ 等の知識が必要なシーンがあり、個人的に同書の内容の一部について良い復習になりました。
用語
本記事中に出てくる言葉などをサクッと見つめてみます。
/dev
Unix においてデバイス ファイルをマウントするためのディレクト リ。
Unix の世界では「すべてがファイル」であり、デバイス もソケットやパイプも全てファイルとして扱われる。
tty
ttyとは、標準入出力となっている端末デバイス (制御端末、controlling terminal)の名前を表示するUnix 系のコマンドである。元来ttyとはteletypewriter(テレタイプライター)のことを指す。
https://ja.wikipedia.org/wiki/Tty
概念を理解するには ttyとptsとは – PAYFORWARD の図と、以下の説明がわかりやすいです。
今では考えられないが,一昔前は1つのホストに色々な人が端末をつないで作業をしていたとのこと.ここでいう端末はWindows やMac などのクライアント端末ではなく,キーボードのような役割を果たしていたと考えていいかな.
pts
pts - スペシャルファイル (デバイス)の説明 - Linux コマンド集 一覧表 によると、ptsは擬似端末スレーブ (pseudo-terminal slave; PTS)。
ファイルシステム 上では/dev/pts
に作成される、仮想端末を示す。
標準入出力
割愛しますが標準ストリーム - Wikipedia 参照。
こちらも割愛します。
ファイル記述子 - Wikipedia 参照。
macOS 10.14.2でいくつかのコマンドを試してみます。
まずterminal (私の場合はiTerm2)を起動した時点で最後にログインしたterminalのttyの情報が出力されている。(今までぜんぜん気にしていなかった)*1
Last login: Sat May 4 21:31:25 on ttys001
複数端末から接続するような状態に似せるため、terminalを2つ起動しておきます。
以降それぞれをterminal 1
, terminal 2
と呼び、実行したコマンドの先頭行に記載します。特に記載のないものはどちらのterminalで実行しても結果が同じものです(正確には一致しないけれども本筋じゃないので無視できるもの)。
tty
デバイス ファイル名を取得するという、tty
コマンドを実行します。
❯ tty
/dev/ttys01
各terminalごとに異なるデバイス ファイル名が割り当てられていることがわかります。
❯ tty
/dev/ttys02
ps
ps
コマンドで起動しているzsh プロセスと、紐づくTTYを確認できる。
❯ ps
PID TTY TIME CMD
17083 ttys001 0 :00.04 /Users/ohbarye/Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp ohbarye
17085 ttys001 0 :01.26 -zsh
17126 ttys002 0 :00.02 -zsh
17250 ttys003 0 :00.08 /Users/ohbarye/Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp ohbarye
17252 ttys003 0 :01.08 -zsh
17293 ttys004 0 :00.01 -zsh
w
w
コマンドでもできる。
❯ w
21:39 up 119 days, 20:21, 3 users, load averages: 2.86 2.72 2.73
USER TTY FROM LOGIN@ IDLE WHAT
ohbarye console - 01 319 63days -
ohbarye s003 - 21:38 1 -zsh
ohbarye s001 - 21:38 - w
リダイレクト
tty
コマンドでそれぞれのterminalのデバイス ファイル名がわかったところで、標準出力の内容を渡してみます。
前述の通り「Unix はすべてをファイルとして扱う」ため、デバイス ファイル /dev/ttys001
もファイルのように振る舞えます。他のデバイス (terminal) にメッセージを送るときもファイルにリダイレクトするかのように echo "hello from $(tty)" > /dev/ttys001
ができます。
❯ echo "hello from $( tty ) " > /dev/ttys001
デバイス ファイルにリダイレクトする様子
リダイレクトは「標準出力の出力先を別ファイルにすること」なので送信元のターミナル(デバイス )には結果が出力されず、送信先 のターミナル(デバイス )に出力されるというわけです。
リダイレクト先を自身のデバイス ファイルにすると、自身のterminalに返ってきます。
docker container上でttyとあそぶ
ようやくdockerコマンド打っていくところです。
まず、1つめのterminalからdocker containerをdocker run --rm -it --name tty-test ruby bash
で起動します。ruby のdocker imageをたまたま使っていますが何でもよいです。
tty
コマンドを実行すると現在利用している端末がpts(擬似端末)として表現されていることがわかります。
# terminal 1
$ docker run --rm -it --name tty-test ruby bash
root@f8e2c6358cea:/# tty
/dev/pts/0
次に、2つめのterminalからdocker exec -it tty-test bash
を実行し、同じdocker container上にもう一つbash プロセスを立ち上げてみます。
今度はデバイス ファイル名がincrementされた別物が出現しました。接続を増やすたびに増えていくようです。
# terminal 2
$ docker exec -it tty-test bash
root@f8e2c6358cea:/# tty
/dev/pts/1
docker container上にbash プロセスを2つ立ち上げてtty確認
/dev/pts
を見ることで現在接続されている端末の数がわかります。
root@221079d1d77c:/# ls -la /dev/pts
total 0
drwxr-xr-x 2 root root 0 May 4 09:21 .
drwxr-xr-x 5 root root 360 May 4 09:21 ..
crw--w---- 1 root tty 136, 0 May 4 09:22 0
crw--w---- 1 root tty 136, 1 May 4 09:24 1
crw-rw-rw- 1 root root 5, 2 May 4 09:24 ptmx
ptmx
についてはpts - スペシャルファイル (デバイス)の説明 - Linux コマンド集 一覧表 を参照(あまりよくわかっていない)
ps a
すると2つのbash プロセス(とps a
プロセス)が見えます。
root@221079d1d77c:/# ps a
PID TTY STAT TIME COMMAND
1 pts/0 Ss 0:00 bash
7 pts/1 Ss+ 0:00 bash
25 pts/0 R+ 0:00 ps a
docker run
(terminal 1側) で先に起動したのはpid=1の方です。dockerはcontainerを立ち上げるときに指定されたコマンドをpid=1で実行するためです。
消去法でdocker exec
で後に起動したのはpid=7の方だとわかりますが、このpidは実行のたびに変わるので同じ番号であることは保証されません。
pid=1に関する余談
ふつうUnix ではpid=1はinitプロセスで、initプロセスに対する操作は特殊な扱いになります。(man 2 kill
等に記載あり)
また、node.jsをdocker container上で動かす場合にこのpid=1が問題になることがあるようです。
Docker で node.js を動かすときは PID 1 にしてはいけない - ngzmのブログ
docker container上でも同じくリダイレクトによるメッセージの送り合いが echo "hello from $(tty)" > /dev/pts/0
等で出来ます。
`echo "hello from $(tty)" > /dev/pts/0`
/proc/<pid>/fd
を確認することで各bash プロセスが利用するファイルディスクリプタ を確認することもできます。
docker run
で起動したpid=1のプロセス(bash )では0:標準入力(stdin)、1:標準出力(stdout)、2:標準エラー出力 (stderr)の3つが/dev/pts/0
に割り当てられています。(255はbash 特有のもの*2 )
# terminal 1
root@8ee8c01fb676:/# ls -la /proc/1/fd
total 0
dr-x------ 2 root root 0 May 4 09:40 .
dr-xr-xr-x 9 root root 0 May 4 09:40 ..
lrwx------ 1 root root 64 May 4 09:40 0 -> /dev/pts/0
lrwx------ 1 root root 64 May 4 09:40 1 -> /dev/pts/0
lrwx------ 1 root root 64 May 4 09:40 2 -> /dev/pts/0
lrwx------ 1 root root 64 May 4 09:40 255 -> /dev/pts/0
docker exec
で起動したプロセス側も同様に/dev/pts/1
が使われています。
# terminal 2
root@8ee8c01fb676:/# ls -la /proc/7/fd
total 0
dr-x------ 2 root root 0 May 4 09:40 .
dr-xr-xr-x 9 root root 0 May 4 09:40 ..
lrwx------ 1 root root 64 May 4 09:40 0 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 09:40 1 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 09:40 2 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 09:40 255 -> /dev/pts/1
-t
, -i
の役割を確認する
長らく本題から逸れました。
ここからようやく、それぞれのオプションが無い場合にどんな挙動をするかを見ることで、-t
, -i
オプションの役割を確認する作業に入っていきます。
-t
オプションを無くしてみる
docker exec --help
を見ると-t
はpseudo-TTY (pts) を割り当てるオプションとあります。
-t, --tty Allocate a pseudo-TTY
ptsが無い場合にどのような挙動をするのか見てみます。
docker run --rm -it --name tty-test ruby bash
で立ち上げたterminal 1側のbash プロセスはそのままに、docker exec -i tty-test bash
(-t
がないことに注目) を実行します。
ps ax
を実行するとdocker exec
側のbash プロセスのTTYが ? になっています。
これは制御するターミナル(デバイス )が割り当てられていないことを示しています。
# terminal 1
$ docker run --rm -it --name tty-test ruby bash
# terminal 2
$ docker exec -i tty-test bash
# terminal 1
root@a7144f8d8ef0:/# ps ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss 0:00 bash
17 ? Ss 0:00 bash
41 pts/0 R+ 0:00 ps ax
ちなみにps a
ではterminal 2側のbash プロセスは出てきません。x
オプションが必要です。このオプションをつけることで制御端末がないプロセスも含むようになります。
# terminal 1
root@a7144f8d8ef0:/# ps a
PID TTY STAT TIME COMMAND
1 pts/0 Ss 0:00 bash
40 pts/0 R+ 0:00 ps a
include processes which do not have a controlling terminal
man ps
より
/dev/pts
を見てみても、たしかに0
(terminal 1, docker run
側)しかいません。
root@a7144f8d8ef0:/# ls -la /dev/pts
total 0
drwxr-xr-x 2 root root 0 May 4 09:28 .
drwxr-xr-x 5 root root 360 May 4 09:28 ..
crw--w---- 1 root tty 136, 0 May 4 09:38 0
crw-rw-rw- 1 root root 5, 2 May 4 09:38 ptmx
(terminal 2, docker exec
側で)立ち上げたbash プロセスのファイルディスクリプタ はどうなっているのでしょうか?
すべてpipe:[2700x]
のような表記になっています。これが何なのかよくわかっていないが少なくともデバイス ファイルでなくpipeであることはわかりました。
root@203d1f3d7ddb:/# ls -la /proc/6/fd
total 0
dr-x------ 2 root root 0 May 4 09:41 .
dr-xr-xr-x 9 root root 0 May 4 09:41 ..
lr-x------ 1 root root 64 May 4 09:41 0 -> pipe:[27004]
l-wx------ 1 root root 64 May 4 09:41 1 -> pipe:[27005]
l-wx------ 1 root root 64 May 4 09:41 2 -> pipe:[27006]
また、docker exec
したほうのterminal 2でtty
を打ってみると、not a tty
という結果が返ってきます。
# terminal 2
❯ docker exec -i tty-test bash
tty
not a tty
つまり「-t
オプションを付けないと、ptsが割り当てられず=標準入出力がterminalに繋がらず、インタラクティブ な操作ができない」ことがわかりました。
-i
オプションをなくしてみる
次は-i
オプションを見てみます。こちらもdocker exec --help
による説明を貼っておきます。
-i, --interactive Keep STDIN open even if not attached
今度は-i
抜きのコマンドdocker exec -t tty-test bash
を試してみます。
docker exec
側のterminal 2では標準入力を受け付けなくなります。echo '1'
などを実行しても結果が返ってきません。
ただし-t
によりptsが割り当てられているのでデバイス ファイル名を特定すればメッセージを送りつけることはできます。
`-i`オプションがないと標準入力を受け付けなくなる
# terminal 1
root@fe2c5869d96c:/# ps au
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 18184 3240 pts/0 Ss 14:05 0:00 bash
root 6 0.0 0.0 18180 3160 pts/1 Ss+ 14:05 0:00 bash
root 13 0.0 0.0 36632 2764 pts/0 R+ 14:07 0:00 ps au
root@fe2c5869d96c:/# ls -la /proc/6/fd
total 0
dr-x------ 2 root root 0 May 4 14:05 .
dr-xr-xr-x 9 root root 0 May 4 14:05 ..
lrwx------ 1 root root 64 May 4 14:05 0 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 14:05 1 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 14:05 2 -> /dev/pts/1
lrwx------ 1 root root 64 May 4 14:07 255 -> /dev/pts/1
root@fb2a467d7ee8:/# echo "hello from $(tty)" > /dev/pts/1
# terminal 2 側に表示される
-i
オプションはhelpの説明そのままですが「-i
オプションを付けないと、terminalからの入力を受け付けなくなる」ことが実際に試してわかりました。
-it
オプションをなくしてみる
-i
も-t
も省略したdocker exec tty-test bash
を実行すると一瞬で終了します。
# terminal 2
$ docker exec tty-test bash
うーん、何も受け付けないし出力もしないbash プロセスが立ち上がるのではと思ったのですが、どういう仕組みでこのプロセスを終了させているのかよくわかっていません。
docker attach
docker exec
と似ているけどぜんぜん違うコマンドdocker attach
についてもついでに見ておきます。
docker attach
は指定した起動中のcontainerのルートプロセス(pid=1のプロセス)の標準出力・標準入力につなぐ
docker exec
は指定した起動中のcontainerで新しいプロセスを立ち上げる
# terminal 1 で起動したあとに
$ docker run --rm -it --name tty-test ruby bash
# terminal 2 で attach する
$ docker attach tty-test
docker attachでpid=1と同化したようす
片方のterminalで行った入出力がまったく同じようにもう一方のterminalに再現するのがわかります。macOS 上のterminalは2つでも、docker container上ではまったく同一のプロセスであり、標準入出力が同一のpts (ここでは/dev/pts/0
) に割り当てられているためです。
また、docker attach
に--no-stdin
オプションをつけるとSTDOUT、STDERRは受け付けるが入力は受け付けられないことも同様のやり方で確認できます。
`docker attach --no-stdin`でSTDINを受け付けないようす
ちなみにプロセスをexitせずにattachを抜けるには Ctrl+P -> Ctrl+Q です。このショートカットが他のソフトウェアと衝突する場合には --detach-keys
ショートカットで別のkeyを割り当てることもできます。*3
感想
docker (run|exec)
の-i
, t
オプションからttyとは何かを理解しようと手を動かす中で、dockerを理解するにはUnix システムの初歩の初歩を押さえることが本当に大事 だと痛感しました。これまでもこの辺(プロセス・ファイルディスクリプタ ・標準入出力 etc.)の理解が足りなかったんだろうな〜と反省。
ふだん何気なく使っているdockerのコマンド群もUnix システムの理解がないといつまでも「俺たちは雰囲気でdockerを使っている」状態から抜け出せないなと思うわけです。
これからもやっていくぞ!!
参考
技術評論社 (2018-02-23)
売り上げランキング: 25,094
Eric S.Raymond
KADOKAWA (2019-03-08)
売り上げランキング: 193,583