1. 終端

1.1. 終端的基本概念

在UNIX系統中,用戶通過終端登錄系統後得到一個Shell進程,這個終端成為Shell進程的控制終端(Controlling Terminal),在第 1 節 “引言”講過,控制終端是保存在PCB中的信息,而我們知道fork會複製PCB中的信息,因此由Shell進程啟動的其它進程的控制終端也是這個終端。預設情況下(沒有重定向),每個進程的標準輸入、標準輸出和標准錯誤輸出都指向控制終端,進程從標準輸入讀也就是讀用戶的鍵盤輸入,進程往標準輸出或標准錯誤輸出寫也就是輸出到顯示器上。此外在第 33 章 信號還講過,在控制終端輸入一些特殊的修飾鍵可以給前台進程發信號,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT

第 28 章 檔案與I/O中講過,每個進程都可以通過一個特殊的設備檔案/dev/tty訪問它的控制終端。事實上每個終端設備都對應一個不同的設備檔案,/dev/tty提供了一個通用的介面,一個進程要訪問它的控制終端既可以通過/dev/tty也可以通過該終端設備所對應的設備檔案來訪問。ttyname函數可以由檔案描述符查出對應的檔案名,該檔案描述符必須指向一個終端設備而不能是任意檔案。下面我們通過實驗看一下各種不同的終端所對應的設備檔案名。

例 34.1. 查看終端對應的設備檔案名

#include <unistd.h>
#include <stdio.h>

int main()
{
    printf("fd 0: %s\n", ttyname(0));
    printf("fd 1: %s\n", ttyname(1));
    printf("fd 2: %s\n", ttyname(2));
    return 0;
}

在圖形終端窗口下運行這個程序,可能會得到

$ ./a.out
fd 0: /dev/pts/0
fd 1: /dev/pts/0
fd 2: /dev/pts/0

再開一個終端窗口運行這個程序,可能又會得到

$ ./a.out
fd 0: /dev/pts/1
fd 1: /dev/pts/1
fd 2: /dev/pts/1

用Ctrl-Alt-F1切換到字元終端運行這個程序,結果是

$ ./a.out
fd 0: /dev/tty1
fd 1: /dev/tty1
fd 2: /dev/tty1

讀者可以再試試在Ctrl-Alt-F2的字元終端下或者在telnetssh登陸的網絡終端下運行這個程序,看看結果是什麼。

1.2. 終端登錄過程

一台PC通常只有一套鍵盤和顯示器,也就是只有一套終端設備,但是可以通過Ctrl-Alt-F1~Ctrl-Alt-F6切換到6個字元終端,相當於有6套虛擬的終端設備,它們共用同一套物理終端設備,對應的設備檔案分別是/dev/tty1~/dev/tty6,所以稱為虛擬終端(Virtual Terminal)。設備檔案/dev/tty0表示當前虛擬終端,比如切換到Ctrl-Alt-F1的字元終端時/dev/tty0就表示/dev/tty1,切換到Ctrl-Alt-F2的字元終端時/dev/tty0就表示/dev/tty2,就像/dev/tty一樣也是一個通用的介面,但它不能表示圖形終端窗口所對應的終端。

再舉個例子,做嵌入式開發時經常會用到串口終端,目標板的每個串口對應一個終端設備,比如/dev/ttyS0/dev/ttyS1等,將主機和目標板用串口綫連起來,就可以在主機上通過Linux的minicom或Windows的超級終端工具登錄到目標板的系統。

內核中處理終端設備的模組包括硬件驅動程式和線路規程(Line Discipline)

圖 34.1. 終端設備模組

終端設備模組

硬件驅動程式負責讀寫實際的硬件設備,比如從鍵盤讀入字元和把字元輸出到顯示器,線路規程像一個過濾器,對於某些特殊字元並不是讓它直接通過,而是做特殊處理,比如在鍵盤上按下Ctrl-Z,對應的字元並不會被用戶程序的read讀到,而是被線路規程截獲,解釋成SIGTSTP信號發給前台進程,通常會使該進程停止。線路規程應該過濾哪些字元和做哪些特殊處理是可以配置的。

終端設備有輸入和輸出隊列緩衝區,如下圖所示。

圖 34.2. 終端緩衝

終端緩衝

以輸入隊列為例,從鍵盤輸入的字元經線路規程過濾後進入輸入隊列,用戶程序以先進先出的順序從隊列中讀取字元,一般情況下,當輸入隊列滿的時候再輸入字元會丟失,同時系統會響鈴警報。終端可以配置成回顯(Echo)模式,在這種模式下,輸入隊列中的每個字元既送給用戶程序也送給輸出隊列,因此我們在命令行鍵入字元時,該字元不僅可以被程序讀取,我們也可以同時在屏幕上看到該字元的回顯。

現在我們來看終端登錄的過程:

1、系統啟動時,init進程根據配置檔案/etc/inittab確定需要打開哪些終端。例如配置檔案中有這樣一行:

1:2345:respawn:/sbin/getty 9600 tty1

/etc/passwd類似,每個欄位用:號隔開。開頭的1是這一行配置的id,通常要和tty的尾碼一致,配置tty2的那一行id就應該是2。第二個欄位2345表示運行級別2~5都執行這個配置。最後一個欄位/sbin/getty 9600 tty1init進程要fork/exec的命令,打開終端/dev/tty1,波特率是9600(波特率只對串口和Modem終端有意義),然後提示用戶輸入帳號。中間的respawn欄位表示init進程會監視getty進程的運行狀態,一旦該進程終止,init會再次fork/exec這個命令,所以我們從終端退出登錄後會再次提示輸入帳號。

有些新的Linux發行版已經不用/etc/inittab這個配置檔案了,例如Ubuntu用/etc/event.d目錄下的配置檔案來配置init

2、getty根據命令行參數打開終端設備作為它的控制終端,把檔案描述符0、1、2都指向控制終端,然後提示用戶輸入帳號。用戶輸入帳號之後,getty的任務就完成了,它再執行login程序:

execle("/bin/login", "login", "-p", username, NULL, envp);

3、login程序提示用戶輸入密碼(輸入密碼期間關閉終端的回顯),然後驗證帳號密碼的正確性。如果密碼不正確,login進程終止,init會重新fork/exec一個getty進程。如果密碼正確,login程序設置一些環境變數,設置當前工作目錄為該用戶的主目錄,然後執行Shell:

execl("/bin/bash", "-bash", NULL);

注意argv[0]參數的程序名前面加了一個-,這樣bash就知道自己是作為登錄Shell啟動的,執行登錄Shell的啟動腳本。從getty開始execlogin,再execbash,其實都是同一個進程,因此控制終端沒變,檔案描述符0、1、2也仍然指向控制終端。由於fork會複製PCB信息,所以由Shell啟動的其它進程也都是如此。

1.3. 網絡登錄過程

虛擬終端或串口終端的數目是有限的,虛擬終端一般就是/dev/tty1~/dev/tty6六個,串口終端的數目也不超過串口的數目。然而網絡終端或圖形終端窗口的數目卻是不受限制的,這是通過偽終端(Pseudo TTY)實現的。一套偽終端由一個主設備(PTY Master)和一個從設備(PTY Slave)組成。主設備在概念上相當於鍵盤和顯示器,只不過它不是真正的硬件而是一個內核模組,操作它的也不是用戶而是另外一個進程。從設備和上面介紹的/dev/tty1這樣的終端設備模組類似,只不過它的底層驅動程式不是訪問硬件而是訪問主設備。通過例 34.1 “查看終端對應的設備檔案名”的實驗結果可以看到,網絡終端或圖形終端窗口的Shell進程以及它啟動的其它進程都會認為自己的控制終端是偽終端從設備,例如/dev/pts/0/dev/pts/1等。下面以telnet為例說明網絡登錄和使用偽終端的過程。

圖 34.3. 偽終端

偽終端

  1. 用戶通過telnet客戶端連接伺服器。如果伺服器配置為獨立(Standalone)模式,則在伺服器監聽連接請求是一個telnetd進程,它fork出一個telnetd子進程來服務客戶端,父進程仍監聽其它連接請求。

    另外一種可能是伺服器端由系統服務程序inetdxinetd監聽連接請求,inetd稱為Internet Super-Server,它監聽系統中的多個網絡服務連接埠,如果連接請求的連接埠號和telnet服務連接埠號一致,則fork/exec一個telnetd子進程來服務客戶端。xinetdinetd的升級版本,配置更為靈活。

  2. telnetd子進程打開一個偽終端設備,然後再經過fork一分為二:父進程操作偽終端主設備,子進程將偽終端從設備作為它的控制終端,並且將檔案描述符0、1、2指向控制終端,二者通過偽終端通信,父進程還負責和telnet客戶端通信,而子進程負責用戶的登錄過程,提示輸入帳號,然後調用exec變成login進程,提示輸入密碼,然後調用exec變成Shell進程。這個Shell進程認為自己的控制終端是偽終端從設備,偽終端主設備可以看作鍵盤顯示器等硬件,而操作這個偽終端的“用戶”就是父進程telnetd

  3. 當用戶輸入命令時,telnet客戶端將用戶輸入的字元通過網絡發給telnetd伺服器,由telnetd伺服器代表用戶將這些字元輸入偽終端。Shell進程並不知道自己連接的是偽終端而不是真正的鍵盤顯示器,也不知道操作終端的“用戶”其實是telnetd伺服器而不是真正的用戶。Shell仍然解釋執行命令,將標準輸出和標准錯誤輸出寫到終端設備,這些數據最終由telnetd伺服器發回給telnet客戶端,然後顯示給用戶看。

如果telnet客戶端和伺服器之間的網絡延遲較大,我們會觀察到按下一個鍵之後要過幾秒鐘才能回顯到屏幕上。這說明我們每按一個鍵telnet客戶端都會立刻把該字元發送給伺服器,然後這個字元經過偽終端主設備和從設備之後被Shell進程讀取,同時回顯到偽終端從設備,回顯的字元再經過偽終端主設備、telnetd伺服器和網絡發回給telnet客戶端,顯示給用戶看。也許你會覺得吃驚,但真的是這樣:每按一個鍵都要在網絡上走個來回!

BSD系列的UNIX在/dev目錄下創建很多ptyXXttyXX設備檔案,XX由字母和數字組成,ptyXX是主設備,相對應的ttyXX是從設備,偽終端的數目取決於內核配置。而在SYS V系列的UNIX上,偽終端主設備是/dev/ptmx,“mx”表示Multiplex,意思是多個主設備復用同一個設備檔案,每打開一次/dev/ptmx,內核就分配一個主設備,同時在/dev/pts目錄下創建一個從設備檔案,當終端關閉時就從/dev/pts目錄下刪除相應的從設備檔案。Linux同時支持上述兩種偽終端,目前的標準傾向于SYS V的偽終端。