疑似端末のメモ
O'Reilly Japan - Linuxプログラミングインタフェースの疑似端末の章が詳しい。
疑似端末とはプロセス間通信(IPC)を実現するための方法のひとつ。パイプのように片方に書いたデータをもう片方から読み取ることができる。
A pseudoterminal (sometimes abbreviated "pty") is a pair of
virtual character devices that provide a bidirectional
communication channel. One end of the channel is called the
master; the other end is called the slave.
パイプとの違いは、slave側が端末として扱えるようになっていること。
The slave end of the pseudoterminal provides an interface that
behaves exactly like a classical terminal
https://man7.org/linux/man-pages/man7/pty.7.html
疑似端末が実際に使われている箇所
SSHなどのネットワークログインサービスに使われている。リモート側のホストで起動しているbash
などの端末用プログラムの標準出力、標準入力にソケットを直接接続できないので、代わりに疑似端末を使ってネットワークログインを実現している。
plantuml
@startuml
!theme blueprint
scale 1024 width
card "local host" as host1 {
rectangle "terminal" as terminal1
rectangle "client" as client
rectangle "TCP/IP" as tcpip1
}
card "remote host" as host2 {
rectangle "bash" as bash
rectangle "TCP/IP" as tcpip2
}
terminal1 <-u-> client
client --> tcpip1: "socket"
tcpip1 -l[hidden]-> terminal1
tcpip1 <-r-> tcpip2
tcpip2 -u[dotted]-> bash
@enduml
SSHログインした直後の各プロセスのイメージ
plantuml
@startuml
!theme blueprint
scale 1024 width
card "local host" as host1 {
rectangle "terminal" as terminal1
rectangle "client" as client
rectangle "TCP/IP" as tcpip1
}
card "remote host" as host2 {
rectangle "bash" as bash
rectangle "TCP/IP" as tcpip2
rectangle "sshd" as sshd
rectangle "pty master" as pty_master
rectangle "pty slave" as pty_slave
}
terminal1 <-u-> client
client <--> tcpip1: "socket"
tcpip1 -l[hidden]-> terminal1
tcpip1 <-r-> tcpip2
tcpip2 <-u-> sshd: "socket"
sshd <-d-> pty_master
pty_master <-r-> pty_slave
pty_slave <-u-> bash
tcpip2 -r[hidden]- pty_master
@enduml
client
とsshd
はネットワークを介して通信を行い、sshd
はclient
からの入力を疑似端末のmaster側に書き込み、bash
はその入力を疑似端末のslave側から受け取る。[1]
サンプルプログラム
script (1)の簡易バージョン。
script.rb
を実行すると、新しいセッションが作成され、入力とコマンドの実行結果がtypescript
ファイルに記録される。
require 'pty'
require 'io/console'
PTY.spawn("/bin/bash") do |r, w, child_pid|
finished = false
script = File.open("typescript", "w")
# slave側の端末で特殊文字の解釈がされる
STDIN.raw do
while !finished
rs, _ = IO.select([r, STDIN], [], [])
rs.each do |s|
if s == r
# pty -> stdout + file
begin
c = s.getc
print c
script.write(c)
rescue
finished = true
end
elsif s == STDIN
# stdin -> pty
begin
c = s.getc
w.write(c)
rescue
finished = true
end
end
end
end
end
ensure
script.close
end
pty.spawnは疑似端末のペアを作成しforkした子プロセスを疑似端末のslave側に接続して、疑似端末のmaster側を操作するためのIOオブジェクトと子プロセスのpidを返す。
端末から入力があったときはその入力をscript.rb
が受け取りそれを疑似端末のmaster側に書き込む。bash
は疑似端末のslave側から入力を読み込みコマンドを実行する。
plantuml
@startuml
!theme blueprint
scale 1024 width
card "process \ngroup" as pg1 {
rectangle script
}
card "process \ngroup" as session {
rectangle "bash" as bash
}
rectangle "カーネル" as kernel {
rectangle terminal
rectangle "pty master" as ptmx
rectangle "pty slave\n(制御端末)" as pts
}
rectangle "typescript(file)" as typescript
script -u- typescript
terminal -u-> script: "stdin"
script --> ptmx
bash <-- pts
ptmx <-l-> pts
terminal -r[hidden]- ptmx
pts -r[hidden]- terminal
script -r-> bash: "fork + exec"
@enduml
bash
の実行結果は疑似端末のslave側に書き込まれ、それをscript.py
が受け取り標準出力とファイル(typescript
)に書き込む。
plantuml
@startuml
!theme blueprint
scale 1024 width
card "process \ngroup" as pg1 {
rectangle script
}
card "process \ngroup" as session {
rectangle "bash" as bash
}
rectangle "カーネル" as kernel {
rectangle terminal
rectangle "pty master" as ptmx
rectangle "pty slave\n(制御端末)" as pts
}
rectangle "typescript(file)" as typescript
script -u-> typescript: "record"
terminal <-u- script: "stdout"
script <-- ptmx
bash --> pts
ptmx <-l-> pts
terminal -r[hidden]- ptmx
pts -r[hidden]- terminal
script -r-> bash: "fork + exec"
@enduml
実行例
bash-3.2$ echo $$
52139
bash-3.2$ ls
script.py typescript
bash-3.2$ exit
exit
typescript
の中身
[?1034hbash-3.2$ echo $$
52139
bash-3.2$ ls
script.py typescript
bash-3.2$ exit
exit
-
sshd
はlisten
するプロセス以外に、接続を受け付けたあとにfork
して作られるプロセスや特権分離されたプロセスがある。 ↩︎
Discussion