当咱们在键盘上敲下一个字母的时候,究竟是怎么发送到相应的进程的呢?咱们经过ps、who等命令看到的相似tty一、pts/0这样的输出,它们的做用和区别是什么呢?php
在计算机出来之前,人们就已经在使用一种叫teletype的设备,用来相互之间传递信息,看起来像下面这样:前端
+----------+ Physical Line +----------+ | teletype |<--------------------->| teletype | +----------+ +----------+
两个teletype之间用线链接起来,线两端可能也有相似于调制解调器之类的设备(这里将它们忽略),在一端的teletype上敲键盘时,相应的数据会发送到另外一端的teletype,具体功能是干什么的,我也不太了解。(我脑壳里面想到画面是在一端敲字,另外一端打印出来)shell
这些都是老古董了,彻底没接触过,因此只能简单的推测。vim
等到计算机支持多任务后,人们想到把这些teletype连到计算机上,做为计算机的终端,从而能够操做计算机。windows
使用teletype的主要缘由有两个(我的看法):后端
现实中已经存在了大量不一样厂商的teletype,能够充分利用现有资源缓存
teletype的相关网络已经比较成熟,连起来方便bash
因而链接就发展成这样:服务器
+----------+ +----------+ +-------+ Physical Line +-------+ +------+ | | | Terminal |<->| Modem |<--------------------->| Modem |<->| UART |<->| Computer | +----------+ +-------+ +-------+ +------+ | | +----------+
左边的Terminal就是各类各样的teletype网络
物理线路两边用上了Modem,就是咱们常说的“猫”,那是由于后来网络已经慢慢的变发达了,你们能够共享链接了。(大概推测,可能不对)
UART能够理解为将teletype的信号转换成计算机能识别的信号的设备
计算机为了支持这些teletype,因而设计了名字叫作TTY的子系统,内部结构以下:
+-----------------------------------------------+ | Kernel | | +--------+ | | +--------+ +------------+ | | | +----------------+ | | UART | | Line | | TTY |<---------->| User process A | <------>| |<->| |<->| | | +----------------+ | | driver | | discipline | | driver |<---------->| User process B | | +--------+ +------------+ | | | +----------------+ | +--------+ | | | +-----------------------------------------------+
UART driver对接外面的UART设备
Line discipline主要是对输入和输出作一些处理,能够理解它是TTY driver的一部分
TTY driver用来处理各类终端设备
用户空间的进程经过TTY driver来和终端打交道
为了简单起见,后面的介绍中再也不单独列出UART driver和Line discipline,能够认为它们是TTY driver的一部分
对于每个终端,TTY driver都会建立一个TTY设备与它对应,若是有多个终端链接过来,那么看起来就是这个样子的:
+----------------+ | TTY Driver | | | | +-------+ | +----------------+ +------------+ | | |<---------->| User process A | | Terminal A |<--------->| ttyS0 | | +----------------+ +------------+ | | |<---------->| User process B | | +-------+ | +----------------+ | | | +-------+ | +----------------+ +------------+ | | |<---------->| User process C | | Terminal B |<--------->| ttyS1 | | +----------------+ +------------+ | | |<---------->| User process D | | +-------+ | +----------------+ | | +----------------+
当驱动收到一个终端的链接时,就会根据终端的型号和参数建立相应的tty设备(上图中设备名称叫ttyS0是由于大部分终端的链接都是串行链接),因为每一个终端可能都不同,有本身的特殊命令和使用习惯,因而每一个tty设备的配置可能都不同。好比按delete键的时候,有些多是要删前面的字符,而有些多是删后面的,若是没配置对,就会致使某些按键不是本身想要的行为,这也是咱们在使用模拟终端时,若是默认的配置跟咱们的习惯不符,须要作一些个性化配置的缘由。
后来随着计算机的不断发展,teletype这些设备逐渐消失,咱们再也不须要专门的终端设备了,每一个机器都有本身的键盘和显示器,每台机器均可以是其它机器的终端,远程的操做经过ssh来实现,可是内核TTY驱动这一架构没有发生变化,咱们想要和系统中的进程进行I/O交互,仍是须要经过TTY设备,因而出现了各类终端模拟软件,而且模拟的也是常见的几种终端,如VT100、VT220、XTerm等。
能够经过命令
toe -a
列出系统支持的全部终端类型能够经过命令infocmp来比较两个终端的区别,好比
infocmp vt100 vt220
将会输出vt100和vt220的区别。
在讨论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
pts也是tty设备,它们的关系后面会介绍到
经过上面的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理解成一个管道(pipe),在一端写的内容能够从另外一端读取出来,反之亦然。
这里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设备是如何建立的,以及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的时候,只能看到最新的输出,之前的输出已经不在了。
不肯定这里的终端模拟器对应内核中具体的哪一个模块,但确定有这么个东西存在
+----------+ +------------+ | 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通讯相关的除外)。
经常使用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服务器时,看到的仍是上次的内容。
从上面的流程中应该能够看出来了,对用户空间的程序来讲,他们没有区别,都是同样的;从内核里面来看,pts的另外一端链接的是ptmx,而tty的另外一端链接的是内核的终端模拟器,ptmx和终端模拟器都只是负责维护会话和转发数据包;再看看ptmx和内核终端模拟器的另外一端,ptmx的另外一端链接的是用户空间的应用程序,如sshd、tmux等,而内核终端模拟器的另外一端链接的是具体的硬件,如键盘和显示器。
先先来看看当前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的配置,下面介绍一些常见的的配置项。
这个配置通常由终端控制,当终端的窗口大小发生变化时,须要经过必定的手段修改该配置,好比ssh协议里面就有修改窗口大小的参数,sshd收到客户端的请求后,会经过API修改tty的这个参数,而后由tty经过信号SIGWINCH通知前端程序(好比shell或者vim),前端程序收到信号后,再去读tty的这个参数,而后就知道如何调整本身的输出排版了。
tty除了在终端和前端进程之间转发数据以外,还支持不少控制命令,好比终端输入了CTRL+C,那么tty不会将该输入串转发给前端进程,而是将它转换成信号SIGINT发送给前端进程。这个就是用来配置控制命令对应的输入组合的,好比咱们能够配置“intr = ^E”表示用CTRL+E代替CTRL+C。
这是两个特殊的控制命令,估计常常有人会碰到,在键盘上不当心输入CTRL+S后,终端没反应了,即没输出,也不响应任何输入。这是由于这个命令会告诉TTY暂停,阻塞全部读写操做,即不转发任何数据,只有按了CTRL+Q后,才会继续。这个功能应该是历史遗留,之前终端和服务器之间没有流量控制功能,因此有可能服务器发送数据过快,致使终端处理不过来,因而须要这样一个命令告诉服务器不要再发了,等终端处理完了后在通知服务器继续。
该命令如今比较经常使用的一个场景就是用tail -f
命令监控日志文件的内容时,能够随时按CTRL+S让屏幕中止刷新,看完后再按CTRL+Q让它继续刷,若是不这样的话,须要先CTRL+C退出,看完后在从新运行tail -f
命令。
在终端输入字符的时候,之因此咱们能及时看到咱们输入的字符,那是由于TTY在收到终端发过去的字符后,会先将字符原路返回一份,而后才交给前端进程处理,这样终端就能及时的显示输入的字符。echo就是用来控制该功能的配置项,若是是-echo的话表示disable echo功能。
若是你在shell中运行程序的时候,后面添加了&,好比./myapp &
,这样myapp这个进程就会在后台运行,但若是这个进程继续往tty上写数据呢?这个参数就用来控制是否将输出转发给终端,也即结果会不会在终端显示,这里“-tostop”表示会输出到终端,若是配置为“tostop”的话,将不输出到终端,而且tty会发送信号SIGTTOU给myapp,该信号的默认行为是将暂停myapp的执行。
除了上面介绍配置时提到的SIGINT,SIGTTOU,SIGWINCHU外,还有这么几个跟TTY相关的信号
当后台进程读tty时,tty将发送该信号给相应的进程组,默认行为是暂停进程组中进程的执行。暂停的进程如何继续执行呢?请参考下一篇文章中的SIGCONT。
当tty的另外一端挂掉的时候,好比ssh的session断开了,因而sshd关闭了和ptmx关联的fd,内核将会给和该tty相关的全部进程发送SIGHUP信号,进程收到该信号后的默认行为是退出进程。
终端输入CTRL+Z时,tty收到后就会发送SIGTSTP给前端进程组,其默认行为是将前端进程组放到后端,而且暂停进程组里全部进程的执行。
跟tty相关的信号都是能够捕获的,能够修改它的默认行为
本文介绍了常见的tty功能和特色,下一篇中将详细介绍和tty密切相关的进程session id,进程组,job,后台程序等,敬请期待。