Linux 的僞終端的基本原理 及其在遠程登陸(SSH,telnet等)中的應用

本文介紹了linux中僞終端的建立,介紹了終端的回顯、行緩存、控制字符等特性,並在此基礎上解釋和模擬了telnet、SSH開啓遠程會話的過程。linux

1、輕量級遠程登陸

以前製做的一塊嵌入式板子,安裝了嵌入式linux操做系統,能夠經過串口(Console)登陸。爲了方便使用,須要尋找經過網線遠程登陸的方法。最初的想法是SSH,不過板子的ROM過小,存不了體積龐大龐大的OpenSSH套裝。後來換用了telnet,直接拿busybox的telnetd作服務器,效果很好。ios

後來有一天,發現了Linux中有一個直接創建TCP鏈接的工具:nc 。在服務端使用nc -l 端口號 來進行監聽,在客戶端使用nc IP地址 端口號來創建鏈接。創建鏈接後,nc會把從stdin讀入的字節流發送給另外一方,把接收到的字節流寫入stdout中。配合方便的管道操做,不正能夠將shell的輸入/輸出傳送到遠端機器上嗎?因而在Ubuntu中實驗操做以下(以後發現這種操做叫作「反彈shell」):shell

打開一個終端A,輸入命令緩存

mkfifo /tmp/p  # 建立臨時管道
sh -i </tmp/p |& nc -l 2333 >/tmp/p

該命令將bash的標準輸入輸出與nc的標準輸出輸入鏈接起來,並由nc將其與socket鏈接起來。同時,nc監聽2333端口(若是使用小於1024的端口,須要root權限),等待遠程鏈接。如今打開另外一個終端B,準備鏈接:bash

nc localhost 2333

這時,在終端B中出現了sh的提示符。輸入通常的shell命令後能夠執行並獲得結果。看來linux自帶的工具已經靈活、強大到足夠搭建一個小型的遠程登陸系統。這個過程可使用下面的圖來描述:服務器

經過tty命令,咱們看到,此時的shell並無一個tty終端。確實,它的標準輸入輸出都是管道。這會帶來一個問題,須要操縱tty的一些命令,好比vi、less、sudo等都沒法正常使用(能夠動手試試效果怎麼樣)。更爲要命的是,在終端B中按下Ctrl+C這樣的控制鍵,內核把結束信號發送給了客戶端nc,而不是遠程的程序!網絡

Ctrl+C直接殺死nc,結束了會話。對比telnet,咱們的登陸系統還缺乏什麼東西。這就是僞終端(pseudoterminal)。session

2、瞭解僞終端

1. 終端和它的做用

終端(terminal)這個詞擁有不少含義,這裏儘可能將其分開說明。less

歷史上,終端(有時被成爲tty,tele typewriter)是用戶訪問計算機主機的硬件設備,能夠理解爲一個顯示器和一個鍵盤的組合。socket

  • 現代Linux裏面比較接近此概念的是(一系列)虛擬控制檯(virtual console)。在Ubuntu等發行版本中按下Ctrl+Alt+F1(或F2, F3, ...)便可切換到相應控制檯下。/dev/tty1等文件是這些硬件在linux下的設備文件。程序經過這些文件的讀寫實現對控制檯的讀寫,經過ioctl實現對硬件參數的設置。

終端還能夠指代設備文件,實現軟件接口。好比常見的/dev/tty1文件,還有/dev/pts目錄下的全部文件。

  • 對終端設備文件進行讀寫,可以從鍵盤讀取輸入,從顯示器進行輸出,實現交互式的輸入輸出
  • linux中的每一個進程有一個「控制終端(control terminal)」的屬性(取值爲設備文件),用於實現做業控制。在終端上輸入Ctrl+C、Ctrl+Z,則以該終端爲控制終端的前臺進程組會收到終止、暫停的信號。
  • 對終端設備進行ioctl操做,能夠實現終端相關的硬件參數設置。login、sudo的不顯示密碼,都離不開對終端設備的操做。

終端還能夠指代「終端模擬器」。終端模擬器是應用程序,用於模擬一個終端。它通常是GUI程序,帶有窗口。從窗口輸入的字符做爲模擬鍵盤的輸入,在窗口上打印的字符做爲模擬顯示器的輸出。終端模擬器還須要建立模擬的終端設備(如/dev/pts/1),用於當作命令行進程(CLI進程)的輸入輸出、控制終端。當鍵盤鍵入一個字符,它要讓CLI進程從終端設備中讀到這個字符,當CLI進程寫入終端設備時,終端模擬器要讀到並顯示出來。

終端模擬器的這個需求,偏偏和telnet這種遠程登陸服務器的需求類似。telnet服務器也要建立模擬的終端設備,用於當作命令行進程(CLI進程)的輸入輸出、控制終端。當從網絡收到一個字符,它要讓CLI進程從終端設備中讀到這個字符,當CLI進程寫入終端設備時,telnet要把輸出發送到網絡。

這種共同的需求在linux中有一個統一實現——僞終端(pseudoterminal)。沒錯,上面的/dev/pts/文件夾裏的以數字命名的文件就是僞終端的設備文件。

2. 僞終端的介紹

經過man pts能夠查閱linux對僞終端的介紹。僞終端是僞終端master和僞終端slave(終端設備文件)這一對字符設備。/dev/ptmx是用於建立一對master、slave的文件。當一個進程打開它時,得到了一個master的文件描述符(file descriptor),同時在/dev/pts下建立了一個slave設備文件。

master端是更接近用戶顯示器、鍵盤的一端,slave端是在虛擬終端上運行的CLI(Command Line Interface,命令行接口)程序。Linux的僞終端驅動程序,會把「master端(如鍵盤)寫入的數據」轉發給slave端供程序輸入,把「程序寫入slave端的數據」轉發給master端供(顯示器驅動等)讀取。

咱們打開的「終端」桌面程序,實際上是一種終端模擬器。當終端模擬器運行時,它經過/dev/ptmx打開master端,建立了一個僞終端對,並讓shell運行在slave端。當用戶在終端模擬器中按下鍵盤按鍵時,它產生字節流並寫入master中,shell即可從slave中讀取輸入;shell和它的子程序,將輸出內容寫入slave中,由終端模擬器負責將字符打印到窗口中。

(終端模擬器的顯示原理就不在這裏展開了,這裏認爲鍵盤按鍵造成一列字節流、向顯示器輸出字節流後便打印到屏幕上)

linux中爲何要提出僞終端這個概念呢?shell等命令行程序不能夠直接從顯示器和鍵盤讀取數據嗎?爲了同屏運行多個終端模擬器、並實現遠程登陸,還真不能讓bash直接跨過僞終端這一層。在操做系統的一大思想——虛擬化的指導下,爲多個終端模擬器、遠程用戶分配多個虛擬的終端是有必要的。上圖中的shell使用的slave端就是一個虛擬化的終端。master端是模擬用戶一端的交互。之因此稱爲虛擬化的終端,它除了轉發數據流外,還要有點終端的樣子。

3. 做爲終端的僞終端

最爲一個虛擬的終端,每個僞終端裏面封裝了一個終端驅動,讓它能作到這些事情:

  1. 爲程序提供一些輸入輸出模式的幫助,好比輸入密碼時隱藏字符
  2. 爲用戶提供對進程的控制,好比按下Ctrl+C結束前臺進程

對,這些就是轉發數據以外的控制。

終端的屬性:回顯控制和行控制

當用戶按下一個按鍵時,字符會出如今屏幕上。這可不是CLI進程寫回來的。不信的話能夠在終端裏運行cat,隨便輸入些什麼按回車。第二行是cat返回來的,第一行正是終端的特性。

終端驅動裏存儲了一個狀態——回顯控制:是否將寫入master的字符再次送回master的讀端(顯示器)。默認狀況下這個是啓用的。在命令行裏可使用stty來更改終端的狀態。好比在終端中運行

stty -echo

則會關掉當前終端的回顯。這時按下按鍵,已經沒有字符顯示出來了。輸入ls等命令,可以看到shell正常接收到咱們的命令(此時回車並無顯示出來)。這時cat後,盲打一些文字,按下回車後看到只有一條文字了。

除了用戶經過命令行方式,CLI的程序還能經過系統調用來設置終端的回顯,好比loginsudo等程序就是經過暫時關閉回顯來隱藏密碼的。具體方式是在slave的文件描述符上調用ioctl函數(參考man tty_ioctl),不過推薦使用更友好的tcsetattr函數。詳細設置可查閱man tcsetattr

另外,終端驅動還提供有行緩衝功能。仍是以cat爲例:當咱們輸入文字,在鍵入回車以前,cat並不能讀取到咱們輸入的字符。這裏的cat的行爲能夠理解爲逐字符讀寫:

while(read(0, &c, 1) > 0) //read from stdin, while not EOF
    write(1, &c, 1);  //write to stdout

是誰阻止cat及時讀入字符了呢?實際上是終端驅動。它默認開啓了一個行緩衝區,這樣等程序要調用read系統調用時,先讓程序阻塞着(blocked),等用戶輸入一整行後,才解除阻塞。咱們可使用下列命令將行緩存大小設置爲1:

stty min 1 -icanon

這時,運行cat,嘗試輸入文字。每輸入一個字符,可以當即返回一個字符。(把min改成time,還能設置輸入字符最長被阻塞1秒)

這些終端的狀態屬性信息還有不少,好比設置終端的寬度、高度等。具體能夠參考man stty

特殊控制字符

特殊控制字符,是指Ctrl和其餘鍵的組合。如Ctrl+C、Ctrl+Z等等。用戶按下這些按鍵,終端模擬器(鍵盤)會在master端寫入一個字節。規則是:Ctrl+字母獲得的字節是(大寫)字母的ascii碼減去0x40。好比Ctrl+C是0x03,Ctrl+Z是0x1A。參見下表:

驅動收到這些特殊字符,並不會像收到正常字節那樣處理。在echo的時候,它返回兩個可見字符。好比鍵入Ctrl+C(0x03),就會回顯^和C(0x5E 0x03)兩個字符。更重要的是,驅動將會攔截某些控制字符,他們不會被轉發給slave端,而是觸發做業控制(job control)的規則:向前臺進程組發送SIGINT信號。

要想繞過這一機制,咱們可使用stty的一些設置。下面的命令可以同時關閉控制字符的特殊語義、設置行緩衝大小爲1:

stty raw

而後,運行cat命令,咱們鍵入的全部字符,包括控制字符Ctrl+C(0x03),都會成功傳遞給cat,而且被原樣返回。(能夠試試上下左右、回車鍵的效果)

3、實驗:利用僞終端實現遠程登陸

理解僞終端的基本原理後,咱們就能夠嘗試解釋telnet和SSH等遠程登陸的原理了。每次用戶經過客戶端鏈接服務端的時候,服務端建立一個僞終端master、slave字符設備對,在slave端運行login程序,將master端的輸入輸出經過網絡傳送至客戶端。至於客戶端,則將從網絡收到的信息直接關聯到鍵盤/顯示器上。咱們將這個過程描述爲下圖:

說了這麼多,其實這個結構相比本文第一張圖而言,只多了一個僞終端。下面具體描述各部分的實現細節。

服務端②:建立僞終端,並將master重定向至nc

按照man pts中的介紹,要建立master、slave對,只須要用open系統調用打開/dev/ptmx文件,便可獲得master的文件描述符。同時,在/dev/pts中已經建立了一個設備文件,表示slave端。可是,爲了能讓其餘進程(login,shell)打開slave端,須要按照手冊介紹來調用兩個函數:

Before opening the pseudoterminal slave, you must pass the master's file descriptor to grantpt(3) and unlockpt(3).

具體信息能夠查閱man 3 grantpt,man 3 unlockpt文檔。

咱們能夠直接關閉(man 2 close)終端建立進程的0和1號文件描述符,把master端的文件描述符拷貝(man 2 dup)到0和1號,而後把當前進程刷成ncman 3 exec)。這雖然是比較優雅的作法,但比較複雜。並且當沒有進程打開slave的時候,nc從master處讀不到數據(read返回0),會認爲是EOF而結束鏈接。因此這裏用一個笨辦法:將全部從master讀到的數據經過管道送給nc,將全部從nc獲得的數據寫入master。咱們須要兩個線程完成這件事。

此小節代碼總結以下:

//ptmxtest.c

//先是一些頭文件和函數聲明
#include<stdio.h>
#define _XOPEN_SOURCE
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/ioctl.h>

/* Chown the slave to the calling user.  */
extern int grantpt (int __fd) __THROW;

/* Release an internal lock so the slave can be opened.
   Call after grantpt().  */
extern int unlockpt (int __fd) __THROW;

/* Return the pathname of the pseudo terminal slave associated with
   the master FD is open on, or NULL on errors.
   The returned storage is good until the next call to this function.  */
extern char *ptsname (int __fd) __THROW __wur;

char buf[1]={'\0'};  //建立緩衝區,這裏只須要大小爲1字節
int main()
{
    //建立master、slave對並解鎖slave字符設備文件
    int mfd = open("/dev/ptmx", O_RDWR);
    grantpt(mfd);
    unlockpt(mfd);
    //查詢並在控制檯打印slave文件位置
    fprintf(stderr,"%s\n",ptsname(mfd));

    int pid=fork();//分爲兩個進程
    if(pid)//父進程從master讀字節,並寫入標準輸出中
    {
        while(1)
        {
            if(read(mfd,buf,1)>0)
                write(1,buf,1);
            else
                sleep(1);
        }
    }
    else//子進程從標準輸入讀字節,並寫入master中
    {
        while(1)
        {
            if(read(0,buf,1)>0)
                write(mfd,buf,1);
            else
                sleep(1);
        }
    }

    return 0;
}

將文件保存後,打開一個終端(稱爲終端A),運行下列命令,在命令行中創建此程序與nc的通道:

gcc -o ptmxtest ptmxtest.c
mkfifo /tmp/p
nc -l 2333 </tmp/p | ./ptmxtest >/tmp/p

至此,圖中的②構建完畢,已經有一個nc在監聽2333端口,它的輸入輸出經過管道送到ptmxtest程序中,ptmxtest又將這些信息搬運給master端。

在個人Ubuntu中運行命令後顯示,建立的slave設備文件是/dev/pts/20。

服務端①:將login程序與終端關聯起來

在圖中①處的地方,須要將login與僞終端的輸入輸出關聯起來。這一點經過輸入輸出重定向便可完成。不過,想要實現Ctrl+C等做業控制,還須要更多的設置。這涉及到一些Linux的進程管理的知識(感興趣的能夠去搜索「進程、進程組、會話、控制終端」等關鍵字)。

一個進程與終端的聯繫,不只取決於它的輸入輸出,還有它的控制終端(Controlling terminal,可經過tty命令查詢,經過/dev/tty打開)。簡單地說,進程控制終端是誰,誰才能向進程發送控制信號。這裏要將login的控制終端設爲僞終端,具體說是slave設備文件才行。

設置控制終端須要使用終端設備的ioctl來實現。查看man tty_ioctl,能夠找到相關信息:

Controlling terminal

TIOCSCTTY int arg
Make the given terminal the controlling terminal of the calling process. The calling process must be a session leader and not have a controlling terminal already. For this case, arg should be specified as zero.

...

TIOCNOTTY void
If the given terminal was the controlling terminal of the calling process, give up this controlling terminal. ...

比較重要的信息是,咱們能夠指定TIOCSCTTY參數來設置控制終端,但它要求調用者是沒有控制終端的會話組長(Session leader)。因此要先指定TIOCNOTTY參數來放棄當前控制終端,並用setsid函數(man 2 setsid)建立新的會話並設置本身爲組長。

咱們將login包裝一層,完成上面的操做,獲得新的程序mylogin:

//mylogin.c

#include<stdio.h>
#define _XOPEN_SOURCE
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<termios.h>
#include<sys/ioctl.h>

int main(int argc, char *argv[])
{
    int old=open("/dev/tty",O_RDWR);  //打開當前控制終端
    ioctl(old, TIOCNOTTY);  //放棄當前控制終端
  
    //根據"man 2 setsid"的說明,調用setsid的進程不能是進程組組長(從bash中運行的命令是組長),故fork出一個子進程,讓組長結束,子進程脫離進程組成爲新的會話組長
    int pid=fork();
    if(pid==0){
        setsid();  //子進程成爲會話組長
        perror("setsid");  //顯示setsid是否成功
        ioctl(0, TIOCSCTTY, 0);  //這時能夠設置新的控制終端了,設置控制終端爲stdin
        execv("/bin/login", argv);  //把當前進程刷成login
    }
    return 0;
}

保存文件後,打開一個終端(稱爲終端B),編譯運行:

gcc -o mylogin mylogin.c
#假設這裏的slave設備是/dev/pts/20
#由於login要讀取密碼文件,須要用root權限執行
sudo ./mylogin </dev/pts/20 >/dev/pts/20 2>&1

該命令將實驗圖中①處的slave設備,重定向至mylogin的stdin、stdout和stderr。在程序執行時,會將控制終端設置爲僞終端,而後執行login。至此,服務端所有創建完畢。

客戶端:鏈接遠程機器,配置本地終端

客戶端處於實驗圖的③處。打開新的終端(終端C),這裏簡單地使用nc鏈接遠程socket,而且nc的輸入輸出重定向至鍵盤、顯示器便可。可是要注意,nc是運行在終端C上的,而終端C的默認屬性會攔截字符Ctrl+C、使用行緩衝區域。這樣nc的輸入輸出其實並不直接是鍵盤、顯示器。爲此,咱們先設置終端C的屬性,再運行nc:

stty raw -echo
nc localhost 2333  #該行沒有回顯,要摸黑輸入

而後,在終端C中出現了咱們打印的setsid的信息,和login的提示符。在終端C中,使用鍵盤能夠正常登陸,獲得shell的提示符。使用tty命令可以看到當前shell使用的控制終端是/dev/pts/20,也就是咱們建立的僞終端。輸入w命令能夠看到系統中登陸的用戶和登陸終端。

至此爲止,咱們實現了相似telnet的遠程登陸。

結語

linux中終端驅動自己有回顯、行緩存、做業控制等豐富的屬性,在此基礎上實現的僞終端在終端模擬器、遠程登陸等場合下可以獲得多種應用。

在實驗過程當中也牽扯到進程控制、輸入輸出重定向、網絡通訊這麼多的知識,更體現出linux的複雜精緻的結構。我感受,linux 就像一個一應俱全、又自成體統的小宇宙,它採用獨特的虛擬化技術,靈活的模塊化和重用機制,虛擬出各類設備,實現了驅動程序的隨意拼插。在這裏,全部模塊都獲得了充分的利用,並可以像變形金剛那樣對各種需求提出面面俱到的解決方案。

相關文章
相關標籤/搜索