😸

疑似端末のメモ

2024/11/01に公開

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.

https://man7.org/linux/man-pages/man7/pty.7.html

パイプとの違いは、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

clientsshdはネットワークを介して通信を行い、sshdclientからの入力を疑似端末の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
脚注
  1. sshdlistenするプロセス以外に、接続を受け付けたあとにforkして作られるプロセスや特権分離されたプロセスがある。 ↩︎

Discussion