《LINUX教程:Linux 偽終端的基本原理 及其在遠程登錄(SSH,telnet等)中的應用》要點:
本文介紹了LINUX教程:Linux 偽終端的基本原理 及其在遠程登錄(SSH,telnet等)中的應用,希望對您有用。如果有疑問,可以聯系我們。
本文介紹了Linux中偽終端的創建,介紹了終端的回顯、行緩存、控制字符等特性,并在此基礎上解釋和模擬了telnet、SSH開啟長途會話的過程.
之前制作的一塊嵌入式板子,安裝了嵌入式Linux操作系統,可以通過串口(Console)登錄.為了方便使用,需要尋找通過網線遠程登錄的辦法.最初的想法是SSH,不過板子的ROM太小,存不了體積龐大龐大的OpenSSH套裝.后來換用了telnet,直接拿busybox的telnetd做服務器,效果很好.
后來有一天,發現了Linux中有一個直接建立TCP連接的工具:nc .在服務端使用nc -l 端口號
來進行監聽,在客戶端使用nc IP地址 端口號
來建立連接.建立連接后,nc會把從stdin讀入的字節流發送給另一方,把接收到的字節流寫入stdout中.配合便利的管道操作,不正可以將shell的輸入/輸出傳送到遠端機器上嗎?于是在Ubuntu中實驗操作如下(之后發現這種操作叫做“反彈shell”):
打開一個終端A,輸入敕令
mkfifo /tmp/p # 創建臨時管道
sh -i </tmp/p |& nc -l 2333 >/tmp/p
該命令將bash的標準輸入輸出與nc的標準輸出輸入連接起來,并由nc將其與socket連接起來.同時,nc監聽2333端口(如果使用小于1024的端口,必要root權限),等待遠程連接.現在打開另一個終端B,準備連接:
nc localhost 2333
這時,在終端B中出現了sh的提示符.輸入一般的shell命令后可以執行并得到結果.看來linux自帶的工具已經靈活、強大到足夠搭建一個小型的長途登錄系統.這個過程可以使用下面的圖來描述:
通過tty命令,我們看到,此時的shell并沒有一個tty終端.確實,它的標準輸入輸出都是管道.這會帶來一個問題,需要把持tty的一些命令,比如vi、less、sudo等都無法正常使用(可以動手試試效果怎么樣).更為要命的是,在終端B中按下Ctrl+C這樣的控制鍵,內核把結束信號發送給了客戶端nc,而不是遠程的程序!
Ctrl+C直接殺死nc,結束了會話.對比telnet,我們的登錄系統還缺少什么東西.這便是偽終端(pseudoterminal).
終端(terminal)是用戶拜訪計算機主機的設備,可以理解為一個顯示器和一個鍵盤的組合.Linux里面比較接近原始類型的一類終端設備是(一系列)控制臺.在Ubuntu等發行版本中按下Ctrl+Alt+F1(或F2, F3, ...)即可切換到相應控制臺下.程序通過拜訪/dev/tty1
等文件可以對這些控制臺讀寫.
除此以外,還有一種廣泛使用的虛擬設備——偽終端(pseudoterminal).每次在圖形界面使用“終端”應用,“終端”應用都會建立一個偽終端設備,名字類似/dev/pts/23
.終端中運行的程序,默認以此為尺度輸入輸出.
那終端有什么用呢?簡單地說,無論是使用Ctrl+C、Ctrl+Z來終止、暫停前臺任務,還是login、sudo的不顯示暗碼,都是終端的功勞.(事實上,終端和linux的進程管理密切相關.Shell的作業調度、前后臺進程組都是在終端的配合下完成的)
通過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端是模擬用戶一端的交互.之所以稱為虛擬化的終端,它除了轉發數據流外,還要有點終端的樣子.
最為一個虛擬的終端,每一個偽終端里面封裝了一個終端驅動,讓它能做到這些工作:
對,這些就是轉發數據之外的控制.
當用戶按下一個按鍵時,字符會呈現在屏幕上.這可不是CLI進程寫回來的.不信的話可以在終端里運行cat
,隨便輸入些什么按回車.第二行是cat
返回來的,第一行正是終端的特性.
終端驅動里存儲了一個狀態——回顯控制:是否將寫入master的字符再次送回master的讀端(顯示器).默認情況下這個是啟用的.在命令行里可以使用stty
來更改終端的狀態.好比在終端中運行
stty -echo
則會關掉當前終端的回顯.這時按下按鍵,已經沒有字符顯示出來了.輸入ls
等命令,能夠看到shell正常接收到我們的命令(此時回車并沒有顯示出來).這時cat
后,盲打一些筆墨,按下回車后看到只有一條筆墨了.
除了用戶通過命令行方式,CLI的程序還能通過系統調用來設置終端的回顯,比如login
,sudo
等程序就是通過暫時關閉回顯來暗藏密碼的.具體方式是在slave的文件描述符上調用ioctl
函數(參考man tty_ioctl
),不過推薦使用更友好的tcsetattr
函數.詳細設置可查閱man tcsetattr
.
另外,終端驅動還提供有行緩沖功能.還是以cat
為例:當我們輸入筆墨,在鍵入回車之前,cat
并不能讀取到我們輸入的字符.這里的cat
的行為可以理解為逐字符讀寫:
是誰阻止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
,并且被原樣返回.(可以試試上下左右、回車鍵的效果)
理解偽終端的基來源根基理后,我們就可以嘗試解釋telnet和SSH等遠程登錄的原理了.每次用戶通過客戶端連接服務端的時候,服務端創建一個偽終端master、slave字符設備對,在slave端運行login程序,將master端的輸入輸出通過網絡傳送至客戶端.至于客戶端,則將從網絡收到的信息直接關聯到鍵盤/顯示器上.我們將這個過程描述為下圖:
說了這么多,其實這個布局相比本文第一張圖而言,只多了一個偽終端.下面具體描述各部分的實現細節.
依照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號,然后把當前進程刷成nc
(man 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. */
externint grantpt (int __fd) __THROW;
/* Release an internal lock so the slave can be opened. Call after grantpt(). */
externint 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. */
externchar *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與偽終端的輸入輸出關聯起來.這一點通過輸入輸出重定向即可完成.不過,想要實現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 就像一個包羅萬象、又自成體統的小宇宙,它采用獨特的虛擬化技術,靈活的模塊化和重用機制,虛擬出各種設備,實現了驅動程序的隨意拼插.在這里,所有模塊都得到了充分的利用,并能夠像變形金剛那樣對各類需求提出面面俱到的辦理方案.
本文永遠更新鏈接地址:
歡迎參與《LINUX教程:Linux 偽終端的基本原理 及其在遠程登錄(SSH,telnet等)中的應用》討論,分享您的想法,維易PHP學院為您提供專業教程。
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/7022.html