Linux TTY/PTS概述

當咱們在鍵盤上敲下一個字母的時候,究竟是怎麼發送到相應的進程的呢?咱們經過ps、who等命令看到的相似tty一、pts/0這樣的輸出,它們的做用和區別是什麼呢?php

TTY歷史

支持多任務的計算機出現以前

在計算機出來之前,人們就已經在使用一種叫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的信號轉換成計算機能識別的信號的設備

內核TTY子系統

計算機爲了支持這些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設備

對於每個終端,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等。

  1. 能夠經過命令toe -a列出系統支持的全部終端類型

  2. 能夠經過命令infocmp來比較兩個終端的區別,好比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

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是如何被建立的

下面介紹幾種常見的狀況下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的session斷開了,因而sshd關閉了和ptmx關聯的fd,內核將會給和該tty相關的全部進程發送SIGHUP信號,進程收到該信號後的默認行爲是退出進程。

SIGTSTP

終端輸入CTRL+Z時,tty收到後就會發送SIGTSTP給前端進程組,其默認行爲是將前端進程組放到後端,而且暫停進程組裏全部進程的執行。

跟tty相關的信號都是能夠捕獲的,能夠修改它的默認行爲

結束語

本文介紹了常見的tty功能和特色,下一篇中將詳細介紹和tty密切相關的進程session id,進程組,job,後臺程序等,敬請期待。

參考

相關文章
相關標籤/搜索