キーボードで文字を入力すると、その文字は対応するプロセスにどのように送信されるのでしょうか? ps、who、その他のコマンドを通じて tty1 や pts/0 などの出力が表示されますが、それらの機能と違いは何ですか?
TTY の歴史
マルチタスクをサポートするコンピューターが登場する前に
コンピューターが登場する前、人々はすでにテレタイプと呼ばれるデバイスを使用して相互に通信していました。以下:
+----------+ Physical Line +----------+ | teletype |<--------------------->| teletype | +----------+ +----------+
2 つのテレタイプは回線で接続されています。回線の両端にモデムに似たデバイスがある場合もあります (ここでは無視されます)。一方の端でテレタイプを入力します。キーボードを押すと、対応するデータが相手側のテレタイプに送信されますが、具体的な機能についてはよくわかりません。 (私の頭の中では、画像は一方の端でタイプされ、もう一方の端で印刷されていると考えています)
これらは古い骨董品であり、私はそれらに触れたことがないため、単純な推測しかできません。
マルチタスクをサポートするコンピュータの出現後、
コンピュータがマルチタスクをサポートした後、人々はこれらのテレタイプをコンピュータの端末としてコンピュータに接続し、コンピュータが操作された。
テレタイプを使用する主な理由は 2 つあります (個人的な意見):
実際には、さまざまなメーカーからすでに多数のテレタイプが存在しており、既存のリソースを最大限に活用
teletype の関連ネットワークは比較的成熟しており、接続が簡単です
したがって、接続は次のように発展しました:
+----------+ +----------+ +-------+ Physical Line +-------+ +------+ | | | Terminal |<->| Modem |<--------------------->| Modem |<->| UART |<->| Computer | +----------+ +-------+ +-------+ +------+ | | +----------+
-
左側の端末はさまざまなテレタイプです。
モデムは物理回線の両側で使用されます。これは、私たちがよく「猫」と呼ぶものです。それは、ネットワークが徐々に進化し、より発達し、誰もが接続を共有できるようになったからです。 (大まかな推測です。間違っている可能性があります)
- #UART は、テレタイプ信号をコンピュータが認識できる信号に変換するデバイスとして理解できます
+-----------------------------------------------+ | Kernel | | +--------+ | | +--------+ +------------+ | | | +----------------+ | | UART | | Line | | TTY |<---------->| User process A | <------>| |<->| |<->| | | +----------------+ | | driver | | discipline | | driver |<---------->| User process B | | +--------+ +------------+ | | | +----------------+ | +--------+ | | | +-----------------------------------------------+
- UART ドライバーは外部に接続しますUART デバイス
- ライン制御は主に入力と出力の処理を行います。TTY ドライバーの一部であると理解できます ##TTYドライバーは、さまざまな端末デバイスの処理に使用されます。
- ユーザー空間プロセスは、TTY ドライバーを使用して端末を処理します。
TTY デバイス
For各端末の場合、TTY ドライバーはそれに対応する TTY デバイスを作成します。複数の端末が接続されている場合は、次のようになります:
+----------------+ | TTY Driver | | | | +-------+ | +----------------+ +------------+ | | |<---------->| User process A | | Terminal A |<--------->| ttyS0 | | +----------------+ +------------+ | | |<---------->| User process B | | +-------+ | +----------------+ | | | +-------+ | +----------------+ +------------+ | | |<---------->| User process C | | Terminal B |<--------->| ttyS1 | | +----------------+ +------------+ | | |<---------->| User process D | | +-------+ | +----------------+ | | +----------------+
ドライバーが端末から接続を受信すると、対応する TTY デバイスを作成します端末のモデルとパラメータに応じた tty デバイス (上の図のデバイス。ほとんどの端末接続はシリアル接続であるため、名前は ttyS0 です) 各端末は異なる場合があり、独自の特別なコマンドと使用習慣があるため、各 tty デバイスは異なる場合があります。たとえば、delete キーを押すと、前の文字を削除するものと、次の文字を削除するものがあります。設定が正しくないと、一部のキーが意図したとおりに動作しないことがあります。これは、シミュレートされた端末を使用する場合にも当てはまります。 . デフォルトの設定が習慣に合わない場合は、個人用の設定を行う必要があります。
その後、コンピュータの継続的な発展に伴い、テレタイプ装置は徐々に姿を消し、特別な端末装置は必要なくなりました。各マシンには独自のキーボードとモニタがあり、各マシンは他のマシンから使用できるようになりました。操作は ssh を介して実装されますが、カーネル TTY ドライバーのアーキテクチャは変更されていません。I/O のためにシステム内のプロセスと対話したい場合は、依然として TTY デバイスを使用する必要があるため、さまざまな端末シミュレーション ソフトウェアが登場しています。また、VT100、VT220、XTerm などのいくつかの一般的な端末もシミュレートします。
pts も tty デバイスです。それらの関係は後ほど紹介しますコマンド
- toe -a
を使用すると、システムでサポートされているすべての端末タイプをリストできます。コマンド infocmp を使用できます。2 つの端末間の違いを比較するには、たとえば
infocmp vt100 vt220
と入力すると、vt100 と vt220 の違いが出力されます。TTY デバイスがどのように作成および設定されるかを説明する前に、まずプロセスによって TTY がどのように使用されるかを見てみましょう。
プログラムによる TTY の処理方法
#先用tty命令看看当前bash关联到了哪个tty dev@debian:~$ tty /dev/pts/1 #看tty都被哪些进程打开了 dev@debian:~$ lsof /dev/pts/1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME bash 907 dev 0u CHR 136,1 0t0 4 /dev/pts/1 bash 907 dev 1u CHR 136,1 0t0 4 /dev/pts/1 bash 907 dev 2u CHR 136,1 0t0 4 /dev/pts/1 bash 907 dev 255u CHR 136,1 0t0 4 /dev/pts/1 lsof 1118 dev 0u CHR 136,1 0t0 4 /dev/pts/1 lsof 1118 dev 1u CHR 136,1 0t0 4 /dev/pts/1 lsof 1118 dev 2u CHR 136,1 0t0 4 /dev/pts/1 #往tty里面直接写数据跟写标准输出是一样的效果 dev@dev:~$ echo aaa > /dev/pts/2 aaa
上記の lsof からわかるように、現在実行中の bash と lsof の stdin(プロセス 0u)、stdout(1u)、stderr(2u) はすべてこの TTY にバインドされます。
以下は、tty とプロセスおよび I/O デバイス間の相互作用の構造図です。Input +--------------------------+ R/W +------+ ----------->| |<---------->| bash | | pts/1 | +------+ <-----------| |<---------->| lsof | Output | Foreground process group | R/W +------+ +--------------------------+
tty はパイプ (パイプ) として理解できます。一方の端で書かれた内容をもう一方の端から読み取ることができ、その逆も同様です。
这里input和output可以简单的理解为键盘和显示器,后面会介绍在各种情况下input/ouput都连接的什么东西。
tty里面有一个很重要的属性,叫Foreground process group,记录了当前前端的进程组是哪一个。process group的概念会在下一篇文章中介绍,这里可以简单的认为process group里面只有一个进程。
当pts/1收到input的输入后,会检查当前前端进程组是哪一个,然后将输入放到进程组的leader的输入缓存中,这样相应的leader进程就可以通过read函数得到用户的输入
当前端进程组里面的进程往tty设备上写数据时,tty就会将数据输出到output设备上
当在shell中执行不同的命令时,前端进程组在不断的变化,而这种变化会由shell负责更新到tty设备中
从上面可以看出,进程和tty打交道很简单,只要保证后台进程不要读写tty就可以了,即写后台程序时,要将stdin/stdout/stderr重定向到其它地方(当然deamon程序还需要做很多其它处理)。
先抛出两个问题(后面有答案):
当非前端进程组里面的进程(后台进程)往tty设备上写数据时,会发生什么?会输出到outpu上吗?
当非前端进程组里面的进程(后台进程)从tty设备上读数据时,会发生什么?进程会阻塞吗?
TTY是如何被创建的
下面介绍几种常见的情况下tty设备是如何创建的,以及input和output设备都是啥。
键盘显示器直连(终端)
先看图再说话:
+-----------------------------------------+ | Kernel | | +--------+ | +----------------+ +----------+ | +-------------------+ | tty1 |<---------->| User processes | | Keyboard |--------->| | +--------+ | +----------------+ +----------+ | | Terminal Emulator |<->| tty2 |<---------->| User processes | | Monitor |<---------| | +--------+ | +----------------+ +----------+ | +-------------------+ | tty3 |<---------->| User processes | | +--------+ | +----------------+ | | +-----------------------------------------+
键盘、显示器都和内核中的终端模拟器相连,由模拟器决定创建多少tty,比如你在键盘上输入ctrl+alt+F1时,模拟器首先捕获到该输入,然后激活tty1,这样键盘的输入会转发到tty1,而tty1的输出会转发到显示器,同理用输入ctrl+alt+F2,就会切换到tty2。
当模拟器激活tty时如果发现没有进程与之关联,意味着这是第一次打开该tty,于是会启动配置好的进程并和该tty绑定,一般该进程就是负责login的进程。
当切换到tty2后,tty1里面的输出会输出到哪里呢?tty1的输出还是会输出给模拟器,模拟器里会有每个tty的缓存,不过由于模拟器的缓存空间有限,所以下次切回tty1的时候,只能看到最新的输出,以前的输出已经不在了。
不确定这里的终端模拟器对应内核中具体的哪个模块,但肯定有这么个东西存在
SSH远程访问
+----------+ +------------+ | Keyboard |------>| | +----------+ | Terminal | | Monitor |<------| | +----------+ +------------+ | | ssh protocol | ↓ +------------+ | | | ssh server |--------------------------+ | | fork | +------------+ | | ↑ | | | | write | | read | | | | +-----|---|-------------------+ | | | | | ↓ | ↓ | +-------+ | +-------+ | +--------+ | pts/0 |<---------->| shell | | | | +-------+ | +-------+ | | ptmx |<->| pts/1 |<---------->| shell | | | | +-------+ | +-------+ | +--------+ | pts/2 |<---------->| shell | | +-------+ | +-------+ | Kernel | +-----------------------------+
这里的Terminal可能是任何地方的程序,比如windows上的putty,所以不讨论客户端的Terminal程序是怎么和键盘、显示器交互的。由于Terminal要和ssh服务器打交道,所以肯定要实现ssh的客户端功能。
这里将建立连接和收发数据分两条线路解释,为了描述简洁,这里以sshd代替ssh服务器程序:
建立连接
1.Terminal请求和sshd建立连接
2.如果验证通过,sshd将创建一个新的session
3.调用API(posix_openpt())请求ptmx创建一个pts,创建成功后,sshd将得到和ptmx关联的fd,并将该fd和session关联起来。
#pty(pseudo terminal device)由两部分构成,ptmx是master端,pts是slave端, #进程可以通过调用API请求ptmx创建一个pts,然后将会得到连接到ptmx的读写fd和一个新创建的pts, #ptmx在内部会维护该fd和pts的对应关系,随后往这个fd的读写会被ptmx转发到对应的pts。 #这里可以看到sshd已经打开了/dev/ptmx dev@debian:~$ sudo lsof /dev/ptmx COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME sshd 1191 dev 8u CHR 5,2 0t0 6531 /dev/ptmx sshd 1191 dev 10u CHR 5,2 0t0 6531 /dev/ptmx sshd 1191 dev 11u CHR 5,2 0t0 6531 /dev/ptmx
4.同时sshd创建shell进程,将新创建的pts和shell绑定
收发消息
1.Terminal收到键盘的输入,Terminal通过ssh协议将数据发往sshd
2.sshd收到客户端的数据后,根据它自己管理的session,找到该客户端对应的关联到ptmx上的fd
3.往找到的fd上写入客户端发过来的数据
4.ptmx收到数据后,根据fd找到对应的pts(该对应关系由ptmx自动维护),将数据包转发给对应的pts
5.pts收到数据包后,检查绑定到自己上面的当前前端进程组,将数据包发给该进程组的leader
6.由于pts上只有shell,所以shell的read函数就收到了该数据包
7.shell对收到的数据包进行处理,然后输出处理结果(也可能没有输出)
8.shell通过write函数将结果写入pts
9.pts将结果转发给ptmx
10.ptmx根据pts找到对应的fd,往该fd写入结果
11.sshd收到该fd的结果后,找到对应的session,然后将结果发给对应的客户端
键盘显示器直连(图形界面)
+----------+ +------------+ | Keyboard |------>| | +----------+ | Terminal |--------------------------+ | Monitor |<------| | fork | +----------+ +------------+ | | ↑ | | | | write | | read | | | | +-----|---|-------------------+ | | | | | ↓ | ↓ | +-------+ | +-------+ | +--------+ | pts/0 |<---------->| shell | | | | +-------+ | +-------+ | | ptmx |<->| pts/1 |<---------->| shell | | | | +-------+ | +-------+ | +--------+ | pts/2 |<---------->| shell | | +-------+ | +-------+ | Kernel | +-----------------------------+
为了简化起见,本篇不讨论Linux下图形界面里Terminal程序是怎么和键盘、显示器交互的。
这里和上面的不同点就是,这里的Terminal不需要实现ssh客户端,但需要把ssh服务器要干的活也干了(当然ssh通信相关的除外)。
SSH + Screen/Tmux
常用Linux的同学应该对screen和tmux不陌生,通过它们启动的进程,就算网络断开了,也不会受到影响继续执行,下次连上去时还能看到进程的所有输出,还能继续接着干活。
这里以tmux为例介绍其原理:
+----------+ +------------+ | Keyboard |------>| | +----------+ | Terminal | | Monitor |<------| | +----------+ +------------+ | | ssh protocol | ↓ +------------+ | | | ssh server |--------------------------+ | | fork | +------------+ | | ↑ | | | | write | | read | | | | +-----|---|-------------------+ | | ↓ | | ↓ | +--------+ +-------+ | +-------+ fork +-------------+ | | ptmx |<->| pts/0 |<---------->| shell |-------->| tmux client | | +--------+ +-------+ | +-------+ +-------------+ | | | | ↑ | +--------+ +-------+ | +-------+ | | | ptmx |<->| pts/2 |<---------->| shell | | | +--------+ +-------+ | +-------+ | | ↑ | Kernel | ↑ | +-----|---|-------------------+ | | | | | | |w/r| +---------------------------+ | | | | fork | | ↓ | | +-------------+ | | | | | tmux server |<--------------------------------------------+ | | +-------------+
系统中的ptmx只有一个,上图中画出来了两个,目的是为了表明tmux服务器和sshd都用ptmx,但它们之间又互不干涉。
这种情况要稍微复杂一点,不过原理都是一样的,前半部分和普通ssh的方式是一样的,只是pts/0关联的前端进程不是shell了,而是变成了tmux客户端,所以ssh客户端发过来的数据包都会被tmux客户端收到,然后由tmux客户端转发给tmux服务器,而tmux服务器干的活和ssh的类似,也是维护一堆的session,为每个session创建一个pts,然后将tmux客户端发过来的数据转发给相应的pts。
由于tmux服务器只和tmux客户端打交道,和sshd没有关系,当终端和sshd的连接断开时,虽然pts/0会被关闭,和它相关的shell和tmux客户端也将被kill掉,但不会影响tmux服务器,当下次再用tmux客户端连上tmux服务器时,看到的还是上次的内容。
TTY和PTS的区别
从上面的流程中应该可以看出来了,对用户空间的程序来说,他们没有区别,都是一样的;从内核里面来看,pts的另一端连接的是ptmx,而tty的另一端连接的是内核的终端模拟器,ptmx和终端模拟器都只是负责维护会话和转发数据包;再看看ptmx和内核终端模拟器的另一端,ptmx的另一端连接的是用户空间的应用程序,如sshd、tmux等,而内核终端模拟器的另一端连接的是具体的硬件,如键盘和显示器。
常见的TTY配置
先先来看看当前tty的所有配置:
dev@dev:~$ stty -a speed 38400 baud; rows 51; columns 204; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0; -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc ixany imaxbel -iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc
stty还可以用来修改tty的参数,用法请参考
man stty
只要是有权限的程序,都可以通过Linux提供的API来修改TTY的配置,下面介绍一些常见的的配置项。
rows 51; columns 204;
这个配置一般由终端控制,当终端的窗口大小发生变化时,需要通过一定的手段修改该配置,比如ssh协议里面就有修改窗口大小的参数,sshd收到客户端的请求后,会通过API修改tty的这个参数,然后由tty通过信号SIGWINCH通知前端程序(比如shell或者vim),前端程序收到信号后,再去读tty的这个参数,然后就知道如何调整自己的输出排版了。
intr = ^C
tty除了在终端和前端进程之间转发数据之外,还支持很多控制命令,比如终端输入了CTRL+C,那么tty不会将该输入串转发给前端进程,而是将它转换成信号SIGINT发送给前端进程。这个就是用来配置控制命令对应的输入组合的,比如我们可以配置“intr = ^E”表示用CTRL+E代替CTRL+C。
start = ^Q; stop = ^S;
这是两个特殊的控制命令,估计经常有人会碰到,在键盘上不小心输入CTRL+S后,终端没反应了,即没输出,也不响应任何输入。这是因为这个命令会告诉TTY暂停,阻塞所有读写操作,即不转发任何数据,只有按了CTRL+Q后,才会继续。这个功能应该是历史遗留,以前终端和服务器之间没有流量控制功能,所以有可能服务器发送数据过快,导致终端处理不过来,于是需要这样一个命令告诉服务器不要再发了,等终端处理完了后在通知服务器继续。
该命令现在比较常用的一个场景就是用tail -f
命令监控日志文件的内容时,可以随时按CTRL+S让屏幕停止刷新,看完后再按CTRL+Q让它继续刷,如果不这样的话,需要先CTRL+C退出,看完后在重新运行tail -f
命令。
echo
在终端输入字符的时候,之所以我们能及时看到我们输入的字符,那是因为TTY在收到终端发过去的字符后,会先将字符原路返回一份,然后才交给前端进程处理,这样终端就能及时的显示输入的字符。echo就是用来控制该功能的配置项,如果是-echo的话表示disable echo功能。
-tostop
如果你在shell中运行程序的时候,后面添加了&,比如./myapp &
,这样myapp这个进程就会在后台运行,但如果这个进程继续往tty上写数据呢?这个参数就用来控制是否将输出转发给终端,也即结果会不会在终端显示,这里“-tostop”表示会输出到终端,如果配置为“tostop”的话,将不输出到终端,并且tty会发送信号SIGTTOU给myapp,该信号的默认行为是将暂停myapp的执行。
TTY 関連の信号
上記の構成の紹介で説明した SIGINT、SIGTTOU、および SIGWINCHU に加えて、いくつかの TTY 関連の信号があります
SIGTTIN
バックグラウンド プロセスが tty を読み取ると、tty は対応するプロセス グループにシグナルを送信します。デフォルトの動作では、プロセス グループ内のプロセスの実行が一時停止されます。中断されたプロセスの実行を続行するにはどうすればよいですか? SIGCONT については次の記事を参照してください。
SIGHUP
たとえば、tty のもう一方の端がハングすると、ssh セッションが切断されるため、sshd は ptmx に関連付けられた fd を閉じ、カーネルは tty 関連のすべてのプロセスを実行します。 SIGHUP シグナルを送信し、シグナルを受信した後のプロセスのデフォルトの動作はプロセスを終了します。
SIGTSTP
端末が CTRL Z を入力すると、tty は SIGTSTP を受信した後、フロントエンド プロセス グループに送信します。デフォルトの動作では、フロントエンド プロセス グループをバックエンドを実行し、プロセス グループを一時停止し、すべてのプロセスの実行を停止します。
tty に関連する信号をキャプチャでき、そのデフォルトの動作を変更できます
結論
この記事では、一般的な tty の機能と機能を紹介します。次回の記事では、ttyと密接に関係するプロセスセッションID、プロセスグループ、ジョブ、バックグラウンドプログラムなどを詳しく紹介しますので、ご期待ください。
関連する推奨事項: 「Linux ビデオ チュートリアル 」