《LINUX教程:Linux 偽終端的基本原理 及其在遠(yuǎn)程登錄(SSH,telnet等)中的應(yīng)用》要點(diǎn):
本文介紹了LINUX教程:Linux 偽終端的基本原理 及其在遠(yuǎn)程登錄(SSH,telnet等)中的應(yīng)用,希望對(duì)您有用。如果有疑問,可以聯(lián)系我們。
本文介紹了Linux中偽終端的創(chuàng)建,介紹了終端的回顯、行緩存、控制字符等特性,并在此基礎(chǔ)上解釋和模擬了telnet、SSH開啟長途會(huì)話的過程.
之前制作的一塊嵌入式板子,安裝了嵌入式Linux操作系統(tǒng),可以通過串口(Console)登錄.為了方便使用,需要尋找通過網(wǎng)線遠(yuǎn)程登錄的辦法.最初的想法是SSH,不過板子的ROM太小,存不了體積龐大龐大的OpenSSH套裝.后來換用了telnet,直接拿busybox的telnetd做服務(wù)器,效果很好.
后來有一天,發(fā)現(xiàn)了Linux中有一個(gè)直接建立TCP連接的工具:nc .在服務(wù)端使用nc -l 端口號(hào)
來進(jìn)行監(jiān)聽,在客戶端使用nc IP地址 端口號(hào)
來建立連接.建立連接后,nc會(huì)把從stdin讀入的字節(jié)流發(fā)送給另一方,把接收到的字節(jié)流寫入stdout中.配合便利的管道操作,不正可以將shell的輸入/輸出傳送到遠(yuǎn)端機(jī)器上嗎?于是在Ubuntu中實(shí)驗(yàn)操作如下(之后發(fā)現(xiàn)這種操作叫做“反彈shell”):
打開一個(gè)終端A,輸入敕令
mkfifo /tmp/p # 創(chuàng)建臨時(shí)管道
sh -i </tmp/p |& nc -l 2333 >/tmp/p
該命令將bash的標(biāo)準(zhǔn)輸入輸出與nc的標(biāo)準(zhǔn)輸出輸入連接起來,并由nc將其與socket連接起來.同時(shí),nc監(jiān)聽2333端口(如果使用小于1024的端口,必要root權(quán)限),等待遠(yuǎn)程連接.現(xiàn)在打開另一個(gè)終端B,準(zhǔn)備連接:
nc localhost 2333
這時(shí),在終端B中出現(xiàn)了sh的提示符.輸入一般的shell命令后可以執(zhí)行并得到結(jié)果.看來linux自帶的工具已經(jīng)靈活、強(qiáng)大到足夠搭建一個(gè)小型的長途登錄系統(tǒng).這個(gè)過程可以使用下面的圖來描述:
通過tty命令,我們看到,此時(shí)的shell并沒有一個(gè)tty終端.確實(shí),它的標(biāo)準(zhǔn)輸入輸出都是管道.這會(huì)帶來一個(gè)問題,需要把持tty的一些命令,比如vi、less、sudo等都無法正常使用(可以動(dòng)手試試效果怎么樣).更為要命的是,在終端B中按下Ctrl+C這樣的控制鍵,內(nèi)核把結(jié)束信號(hào)發(fā)送給了客戶端nc,而不是遠(yuǎn)程的程序!
Ctrl+C直接殺死nc,結(jié)束了會(huì)話.對(duì)比telnet,我們的登錄系統(tǒng)還缺少什么東西.這便是偽終端(pseudoterminal).
終端(terminal)是用戶拜訪計(jì)算機(jī)主機(jī)的設(shè)備,可以理解為一個(gè)顯示器和一個(gè)鍵盤的組合.Linux里面比較接近原始類型的一類終端設(shè)備是(一系列)控制臺(tái).在Ubuntu等發(fā)行版本中按下Ctrl+Alt+F1(或F2, F3, ...)即可切換到相應(yīng)控制臺(tái)下.程序通過拜訪/dev/tty1
等文件可以對(duì)這些控制臺(tái)讀寫.
除此以外,還有一種廣泛使用的虛擬設(shè)備——偽終端(pseudoterminal).每次在圖形界面使用“終端”應(yīng)用,“終端”應(yīng)用都會(huì)建立一個(gè)偽終端設(shè)備,名字類似/dev/pts/23
.終端中運(yùn)行的程序,默認(rèn)以此為尺度輸入輸出.
那終端有什么用呢?簡單地說,無論是使用Ctrl+C、Ctrl+Z來終止、暫停前臺(tái)任務(wù),還是login、sudo的不顯示暗碼,都是終端的功勞.(事實(shí)上,終端和linux的進(jìn)程管理密切相關(guān).Shell的作業(yè)調(diào)度、前后臺(tái)進(jìn)程組都是在終端的配合下完成的)
通過man pts
可以查閱linux對(duì)偽終端的介紹.偽終端是偽終端master和偽終端slave這一對(duì)字符設(shè)備./dev/ptmx
是用于創(chuàng)立一對(duì)master、slave的文件.當(dāng)一個(gè)進(jìn)程打開它時(shí),獲得了一個(gè)master的文件描述符(file descriptor),同時(shí)在/dev/pts
下創(chuàng)立了一個(gè)slave設(shè)備文件.
master端是更接近用戶顯示器、鍵盤的一端,slave端是在虛擬終端上運(yùn)行的CLI(Command Line Interface,命令行接口)法式.Linux的偽終端驅(qū)動(dòng)法式,會(huì)把“master端(如鍵盤)寫入的數(shù)據(jù)”轉(zhuǎn)發(fā)給slave端供法式輸入,把“法式寫入slave端的數(shù)據(jù)”轉(zhuǎn)發(fā)給master端供(顯示器驅(qū)動(dòng)等)讀取.
我們打開的“終端”桌面程序,其實(shí)是一種終端模擬器.當(dāng)終端模擬器運(yùn)行時(shí),它通過/dev/ptmx
打開master端,創(chuàng)建了一個(gè)偽終端對(duì),并讓shell運(yùn)行在slave端.當(dāng)用戶在終端模擬器中按下鍵盤按鍵時(shí),它發(fā)生字節(jié)流并寫入master中,shell便可從slave中讀取輸入;shell和它的子程序,將輸出內(nèi)容寫入slave中,由終端模擬器負(fù)責(zé)將字符打印到窗口中.
(終端模擬器的顯示原理就不在這里展開了,這里認(rèn)為鍵盤按鍵形成一列字撙節(jié)、向顯示器輸出字撙節(jié)后便打印到屏幕上)
linux中為什么要提出偽終端這個(gè)概念呢?shell等命令行程序不可以直接從顯示器和鍵盤讀取數(shù)據(jù)嗎?為了同屏運(yùn)行多個(gè)終端模擬器、并實(shí)現(xiàn)遠(yuǎn)程登錄,還真不能讓bash直接跨過偽終端這一層.在操作系統(tǒng)的一大思想——虛擬化的指導(dǎo)下,為多個(gè)終端模擬器、遠(yuǎn)程用戶分配多個(gè)虛擬的終端是有需要的.上圖中的shell使用的slave端就是一個(gè)虛擬化的終端.master端是模擬用戶一端的交互.之所以稱為虛擬化的終端,它除了轉(zhuǎn)發(fā)數(shù)據(jù)流外,還要有點(diǎn)終端的樣子.
最為一個(gè)虛擬的終端,每一個(gè)偽終端里面封裝了一個(gè)終端驅(qū)動(dòng),讓它能做到這些工作:
對(duì),這些就是轉(zhuǎn)發(fā)數(shù)據(jù)之外的控制.
當(dāng)用戶按下一個(gè)按鍵時(shí),字符會(huì)呈現(xiàn)在屏幕上.這可不是CLI進(jìn)程寫回來的.不信的話可以在終端里運(yùn)行cat
,隨便輸入些什么按回車.第二行是cat
返回來的,第一行正是終端的特性.
終端驅(qū)動(dòng)里存儲(chǔ)了一個(gè)狀態(tài)——回顯控制:是否將寫入master的字符再次送回master的讀端(顯示器).默認(rèn)情況下這個(gè)是啟用的.在命令行里可以使用stty
來更改終端的狀態(tài).好比在終端中運(yùn)行
stty -echo
則會(huì)關(guān)掉當(dāng)前終端的回顯.這時(shí)按下按鍵,已經(jīng)沒有字符顯示出來了.輸入ls
等命令,能夠看到shell正常接收到我們的命令(此時(shí)回車并沒有顯示出來).這時(shí)cat
后,盲打一些筆墨,按下回車后看到只有一條筆墨了.
除了用戶通過命令行方式,CLI的程序還能通過系統(tǒng)調(diào)用來設(shè)置終端的回顯,比如login
,sudo
等程序就是通過暫時(shí)關(guān)閉回顯來暗藏密碼的.具體方式是在slave的文件描述符上調(diào)用ioctl
函數(shù)(參考man tty_ioctl
),不過推薦使用更友好的tcsetattr
函數(shù).詳細(xì)設(shè)置可查閱man tcsetattr
.
另外,終端驅(qū)動(dòng)還提供有行緩沖功能.還是以cat
為例:當(dāng)我們輸入筆墨,在鍵入回車之前,cat
并不能讀取到我們輸入的字符.這里的cat
的行為可以理解為逐字符讀寫:
是誰阻止cat
及時(shí)讀入字符了呢?其實(shí)是終端驅(qū)動(dòng).它默認(rèn)開啟了一個(gè)行緩沖區(qū),這樣等法式要調(diào)用read
系統(tǒng)調(diào)用時(shí),先讓法式阻塞著(blocked),等用戶輸入一整行后,才解除阻塞.我們可以使用下列命令將行緩存大小設(shè)置為1:
stty min 1 -icanon
這時(shí),運(yùn)行cat,嘗試輸入筆墨.每輸入一個(gè)字符,能夠立即返回一個(gè)字符.(把min改為time,還能設(shè)置輸入字符最長1秒后阻塞)
這些終端的狀態(tài)屬性信息還有很多,好比設(shè)置終端的寬度、高度等.具體可以參考man stty
.
特殊控制字符,是指Ctrl和其他鍵的組合.如Ctrl+C、Ctrl+Z等等.用戶按下這些按鍵,終端模擬器(鍵盤)會(huì)在master端寫入一個(gè)字節(jié).規(guī)則是:Ctrl+字母得到的字節(jié)是(大寫)字母的ascii碼減去0x40.好比Ctrl+C是0x03,Ctrl+Z是0x1A.參見下表:
驅(qū)動(dòng)收到這些特殊字符,并不會(huì)像收到正常字節(jié)那樣處理.在echo的時(shí)候,它返回兩個(gè)可見字符.好比鍵入Ctrl+C(0x03),就會(huì)回顯^和C(0x5E 0x03)兩個(gè)字符.更重要的是,驅(qū)動(dòng)將會(huì)攔截某些控制字符,他們不會(huì)被轉(zhuǎn)發(fā)給slave端,而是觸發(fā)作業(yè)控制(job control)的規(guī)則:向前臺(tái)進(jìn)程組發(fā)送SIGINT信號(hào).
要想繞過這一機(jī)制,我們可以使用stty的一些設(shè)置.下面的命令能夠同時(shí)關(guān)閉控制字符的特殊語義、設(shè)置行緩沖年夜小為1:
stty raw
然后,運(yùn)行cat
命令,我們鍵入的所有字符,包含控制字符Ctrl+C(0x03),都會(huì)成功傳遞給cat
,并且被原樣返回.(可以試試上下左右、回車鍵的效果)
理解偽終端的基來源根基理后,我們就可以嘗試解釋telnet和SSH等遠(yuǎn)程登錄的原理了.每次用戶通過客戶端連接服務(wù)端的時(shí)候,服務(wù)端創(chuàng)建一個(gè)偽終端master、slave字符設(shè)備對(duì),在slave端運(yùn)行l(wèi)ogin程序,將master端的輸入輸出通過網(wǎng)絡(luò)傳送至客戶端.至于客戶端,則將從網(wǎng)絡(luò)收到的信息直接關(guān)聯(lián)到鍵盤/顯示器上.我們將這個(gè)過程描述為下圖:
說了這么多,其實(shí)這個(gè)布局相比本文第一張圖而言,只多了一個(gè)偽終端.下面具體描述各部分的實(shí)現(xiàn)細(xì)節(jié).
依照man pts
中的介紹,要?jiǎng)?chuàng)建master、slave對(duì),只需要用open
系統(tǒng)調(diào)用打開/dev/ptmx
文件,即可得到master的文件描述符.同時(shí),在/dev/pts
中已經(jīng)創(chuàng)建了一個(gè)設(shè)備文件,表示slave端.但是,為了能讓其他進(jìn)程(login,shell)打開slave端,需要依照手冊介紹來調(diào)用兩個(gè)函數(shù):
Before opening the pseudoterminal slave, you must pass the master's file descriptor to grantpt(3) and unlockpt(3).
詳細(xì)信息可以查閱man 3 grantpt
,man 3 unlockpt
文檔.
我們可以直接關(guān)閉(man 2 close
)終端創(chuàng)建進(jìn)程的0和1號(hào)文件描述符,把master端的文件描述符拷貝(man 2 dup
)到0和1號(hào),然后把當(dāng)前進(jìn)程刷成nc
(man 3 exec
).這雖然是比較優(yōu)雅的做法,但比較復(fù)雜.而且當(dāng)沒有進(jìn)程打開slave的時(shí)候,nc從master處讀不到數(shù)據(jù)(read返回0),會(huì)認(rèn)為是EOF而結(jié)束連接.所以這里用一個(gè)笨方法:將所有從master讀到的數(shù)據(jù)通過管道送給nc,將所有從nc得到的數(shù)據(jù)寫入master.我們需要兩個(gè)線程完成這件事.
此末節(jié)代碼總結(jié)如下:
//ptmxtest.c
//先是一些頭文件和函數(shù)聲明
#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'}; //創(chuàng)建緩沖區(qū),這里只需要大小為1字節(jié)
int main()
{
//創(chuàng)建master、slave對(duì)并解鎖slave字符設(shè)備文件
int mfd = open("/dev/ptmx", O_RDWR);
grantpt(mfd);
unlockpt(mfd);
//查詢并在控制臺(tái)打印slave文件位置
fprintf(stderr,"%s\n",ptsname(mfd));
int pid=fork();//分為兩個(gè)進(jìn)程
if(pid)//父進(jìn)程從master讀字節(jié),并寫入標(biāo)準(zhǔn)輸出中
{
while(1)
{
if(read(mfd,buf,1)>0)
write(1,buf,1);
else
sleep(1);
}
}
else//子進(jìn)程從標(biāo)準(zhǔn)輸入讀字節(jié),并寫入master中
{
while(1)
{
if(read(0,buf,1)>0)
write(mfd,buf,1);
else
sleep(1);
}
}
return 0;
}
將文件保留后,打開一個(gè)終端(稱為終端A),運(yùn)行下列命令,在命令行中建立此程序與nc
的通道:
gcc -o ptmxtest ptmxtest.c
mkfifo /tmp/p
nc -l 2333 </tmp/p | ./ptmxtest >/tmp/p
至此,圖中的②構(gòu)建完畢,已經(jīng)有一個(gè)nc在監(jiān)聽2333端口,它的輸入輸出通過管道送到ptmxtest法式中,ptmxtest又將這些信息搬運(yùn)給master端.
在我的Ubuntu中運(yùn)行命令后顯示,創(chuàng)立的slave設(shè)備文件是/dev/pts/20.
在圖中①處的地方,必要將login與偽終端的輸入輸出關(guān)聯(lián)起來.這一點(diǎn)通過輸入輸出重定向即可完成.不過,想要實(shí)現(xiàn)Ctrl+C等作業(yè)控制,還必要更多的設(shè)置.這涉及到一些Linux的進(jìn)程管理的知識(shí)(感興趣的可以去搜索“進(jìn)程、進(jìn)程組、會(huì)話、控制終端”等關(guān)鍵字).
一個(gè)進(jìn)程與終端的聯(lián)系,不僅取決于它的輸入輸出,還有它的控制終端(Controlling terminal,可通過tty
命令查詢,通過/dev/tty
打開).簡單地說,進(jìn)程控制終端是誰,誰能力向進(jìn)程發(fā)送控制信號(hào).這里要將login的控制終端設(shè)為偽終端,具體說是slave設(shè)備文件才行.
設(shè)置控制終端必要使用終端設(shè)備的ioctl
來實(shí)現(xiàn).查看man tty_ioctl
,可以找到相關(guān)信息:
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參數(shù)來設(shè)置控制終端,但它要求調(diào)用者是沒有控制終端的會(huì)話組長(Session leader).所以要先指定TIOCNOTTY參數(shù)來放棄當(dāng)前控制終端,并用setsid
函數(shù)(man 2 setsid
)創(chuàng)建新的會(huì)話并設(shè)置本身為組長.
我們將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); //打開當(dāng)前控制終端
ioctl(old, TIOCNOTTY); //放棄當(dāng)前控制終端
//根據(jù)"man 2 setsid"的說明,調(diào)用setsid的進(jìn)程不能是進(jìn)程組組長(從bash中運(yùn)行的命令是組長),故fork出一個(gè)子進(jìn)程,讓組長結(jié)束,子進(jìn)程脫離進(jìn)程組成為新的會(huì)話組長
int pid=fork();
if(pid==0){
setsid(); //子進(jìn)程成為會(huì)話組長
perror("setsid"); //顯示setsid是否成功
ioctl(0, TIOCSCTTY, 0); //這時(shí)可以設(shè)置新的控制終端了,設(shè)置控制終端為stdin
execv("/bin/login", argv); //把當(dāng)前進(jìn)程刷成login
}
return 0;
}
保留文件后,打開一個(gè)終端(稱為終端B),編譯運(yùn)行:
gcc -o mylogin mylogin.c
#假設(shè)這里的slave設(shè)備是/dev/pts/20
#因?yàn)閘ogin要讀取暗碼文件,需要用root權(quán)限執(zhí)行
sudo ./mylogin </dev/pts/20 >/dev/pts/20 2>&1
該命令將實(shí)驗(yàn)圖中①處的slave設(shè)備,重定向至mylogin的stdin、stdout和stderr.在程序執(zhí)行時(shí),會(huì)將控制終端設(shè)置為偽終端,然后執(zhí)行l(wèi)ogin.至此,服務(wù)端全部建立完畢.
客戶端處于實(shí)驗(yàn)圖的③處.打開新的終端(終端C),這里簡單地使用nc連接遠(yuǎn)程socket,并且nc的輸入輸出重定向至鍵盤、顯示器即可.但是要注意,nc是運(yùn)行在終端C上的,而終端C的默認(rèn)屬性會(huì)攔截字符Ctrl+C、使用行緩沖區(qū)域.這樣nc的輸入輸出其實(shí)并不直接是鍵盤、顯示器.為此,我們先設(shè)置終端C的屬性,再運(yùn)行nc:
stty raw -echo
nc localhost 2333 #改行沒有回顯,要摸黑輸入
然后,在終端C中出現(xiàn)了我們打印的setsid的信息,和login的提示符.在終端C中,使用鍵盤可以正常登錄,得到shell的提示符.使用tty
命令能夠看到當(dāng)前shell使用的控制終端是/dev/pts/20,也便是我們創(chuàng)建的偽終端.輸入w
命令可以看到系統(tǒng)中登錄的用戶和登錄終端.
至此為止,我們實(shí)現(xiàn)了類似telnet的長途登錄.
linux中終端驅(qū)動(dòng)自己有回顯、行緩存、作業(yè)控制等豐富的屬性,在此基礎(chǔ)上實(shí)現(xiàn)的偽終端在終端模擬器、遠(yuǎn)程登錄等場合下能夠得到多種應(yīng)用.
在實(shí)驗(yàn)過程中也牽扯到進(jìn)程控制、輸入輸出重定向、網(wǎng)絡(luò)通信這么多的知識(shí),更體現(xiàn)出linux的復(fù)雜精致的結(jié)構(gòu).我感覺,linux 就像一個(gè)包羅萬象、又自成體統(tǒng)的小宇宙,它采用獨(dú)特的虛擬化技術(shù),靈活的模塊化和重用機(jī)制,虛擬出各種設(shè)備,實(shí)現(xiàn)了驅(qū)動(dòng)程序的隨意拼插.在這里,所有模塊都得到了充分的利用,并能夠像變形金剛那樣對(duì)各類需求提出面面俱到的辦理方案.
本文永遠(yuǎn)更新鏈接地址:
歡迎參與《LINUX教程:Linux 偽終端的基本原理 及其在遠(yuǎn)程登錄(SSH,telnet等)中的應(yīng)用》討論,分享您的想法,維易PHP學(xué)院為您提供專業(yè)教程。
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.fzlkiss.com/jiaocheng/7022.html