終端的歷史
最初的計(jì)算機(jī)由于價(jià)格昂貴俄精,因此,一臺(tái)計(jì)算機(jī)一般是由多個(gè)人同時(shí)使用的。在這種情況下一臺(tái)計(jì)算機(jī)需要連接上許多套鍵盤和顯示器來(lái)供多個(gè)人使用剂府。在以前專門有這種可以連上一臺(tái)電腦的設(shè)備,只有顯示器和鍵盤剃盾,還有簡(jiǎn)單的處理電路腺占,本身不具有處理計(jì)算機(jī)信息的能力,他是負(fù)責(zé)連接到一臺(tái)正常的計(jì)算 機(jī)上(通常是通過(guò)串口)痒谴,然后登陸計(jì)算機(jī)衰伯,并對(duì)該計(jì)算機(jī)進(jìn)行操作。當(dāng)然积蔚,那時(shí)候的計(jì)算機(jī)操作系統(tǒng)都是多任務(wù)多用戶的操作系統(tǒng)意鲸。這樣一臺(tái)只有顯示器和鍵盤能夠通過(guò)串口連接到計(jì)算機(jī) 的設(shè)備就叫做終端。
筆者剛畢業(yè)時(shí)(比較早)尽爆,曾經(jīng)做過(guò)銀行系統(tǒng)怎顾,在銀行的網(wǎng)點(diǎn)會(huì)配置一臺(tái)中控機(jī)或者PC機(jī),為每臺(tái)主機(jī)配置多臺(tái)終端漱贱,這樣可以給多名柜員和出納使用槐雾。
現(xiàn)在由于計(jì)算機(jī)硬件越來(lái)越便宜,通常一個(gè)人獨(dú)占一臺(tái)計(jì)算機(jī)幅狮,不再需要終端這段硬件設(shè)備募强,因此,現(xiàn)在終端的概念慢慢演化成了軟件的概念”肓現(xiàn)在的終端種類有:
- 偽終端
偽終端又稱為模擬終端钻注,遠(yuǎn)程連接的終端或圖形界面下打開(kāi)的終端接口,設(shè)備文件為:/dev/pts/# - 虛擬終端
Ctrl +Alt + F[1-6]
圖形終端Ctrl + Alt + F7
設(shè)備文件為:/dev/tty# - 物理終端(控制臺(tái))
與主機(jī)直接相連配猫,設(shè)備文件為:/dev/console - 串行終端
串口輸出幅恋,設(shè)備文件為:/dev/ttys#
偽終端的基本原理
Linux支持兩種pty:
- UNIX98 pseudoterminal,使用的是devpts文件系統(tǒng)泵肄,掛載在/dev /pts目錄
- 在UNIX98 pseudoterminal之前捆交,master pseudoterminal名字為/dev/ptyp0,…,slave pseudoterminal名字為/dev/ttyp0,…腐巢,這個(gè)方法需要預(yù)先分配好很多的設(shè)備節(jié)點(diǎn)品追。
所以,這里我們主要是針對(duì)UNIX98偽終端冯丙,這種模式的偽終端肉瓦,是由ptmx與pts配合實(shí)現(xiàn)。例如我們?cè)趖elnet,ssh等遠(yuǎn)程終端工具中會(huì)使用到pty泞莉,通常的數(shù)據(jù)流是這樣的:
TCP/IP
telnet ====================> [ telnetd進(jìn)程 ---> /dev/ptmx(master) ---> /dev/pts/?(slave) ---> getty ]
telnet通過(guò)網(wǎng)絡(luò)協(xié)議與linux主機(jī)上的telnetd進(jìn)程通訊哪雕,telnetd進(jìn)程收到網(wǎng)絡(luò)中的數(shù)據(jù)后,將數(shù)據(jù)寫(xiě)入/dev/ptmx鲫趁,/dev/ptmx像管道一樣將數(shù)據(jù)傳遞給/dev/pts/?斯嚎,getty進(jìn)程從pts/?讀取數(shù)據(jù)傳遞給shell去執(zhí)行。
偽終端設(shè)備
偽終端設(shè)備前面已經(jīng)已經(jīng)說(shuō)過(guò)挨厚,它位于/dev/pts目錄下堡僻,是一個(gè)數(shù)字代表的文件。
當(dāng)我們?cè)诖蜷_(kāi)/dev/ptmx文件的時(shí)候疫剃,系統(tǒng)會(huì)自動(dòng)在/dev/pts目錄下創(chuàng)建一個(gè)新的設(shè)備文件钉疫,這時(shí)候,系統(tǒng)會(huì)自動(dòng)給打開(kāi)的/dev/ptmx文件描述符和/dev/pts/#設(shè)備文件之間形成m->s的關(guān)系慌申。寫(xiě)入/dev/ptmx文件描述符的數(shù)據(jù)陌选,會(huì)自動(dòng)傳到新創(chuàng)建的/dev/pts/#設(shè)備文件。
只要不關(guān)閉/dev/ptmx文件描述符蹄溉,那么這個(gè)設(shè)備文件就會(huì)存在咨油,一旦關(guān)閉,這個(gè)設(shè)備文件會(huì)自動(dòng)消失
可以通過(guò)如下一個(gè)例子演示在”open /dev/ptmx”的時(shí)候在/dev/pts目錄下生成的設(shè)備節(jié)點(diǎn)
$ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 3 4 5 6 7 ptmx
0 1 2 3 4 5 6 7 8 ptmx
$ ls /dev/pts
0 1 2 3 4 5 6 7 ptmx
可見(jiàn)在重定向/dev/ptmx的時(shí)候在/dev/pts目錄下多了個(gè)設(shè)備節(jié)點(diǎn)8柒爵,而當(dāng)上面這個(gè)shell結(jié)束的時(shí)候再次ls /dev/pts目錄役电,設(shè)備節(jié)點(diǎn)8又消失了。
用go語(yǔ)言實(shí)現(xiàn)
package main
import (
"fmt"
"os"
"strconv"
"syscall"
"unsafe"
)
func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
return e
}
return nil
}
func ptsname(f *os.File) (string, error) {
var n uint32
err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n)))
if err != nil {
return "", err
}
return "/dev/pts/" + strconv.Itoa(int(n)), nil
}
func unlockpt(f *os.File) error {
var u int32
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}
func StartPty() (pty, tty *os.File, err error) {
p, err := os.OpenFile("/dev/ptmx", os.O_RDWR | syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
err = unlockpt(p)
if err != nil {
return nil, nil, err
}
fmt.Println("sname is :", sname)
t, err := os.OpenFile(sname, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
return p, t, nil
}
func main() {
m, s, err := StartPty()
if err != nil {
fmt.Printf("start pty: " , err)
os.Exit(-1)
}
defer m.Close()
defer s.Close()
n, err := m.Write([]byte("hello world!\n")) ;
fmt.Printf("write master, %d:%v\n", n, err)
buf := make([]byte, 256)
n, err = s.Read(buf)
fmt.Println("read from slave:", string(buf[0:n]))
n, err = s.Write([]byte("slave!\n"))
fmt.Printf("write slave, %d:%v\n", n, err)
n, err = m.Read(buf[:])
fmt.Println("read from master:", string(buf[0:n]))
}
上面這段程序的輸出如下:
sname is : /dev/pts/9
write master, 13:<nil>
read from slave: hello world!
write slave, 7:<nil>
read from master: hello world!
slave!
注意:從slave中讀取數(shù)據(jù)時(shí)棉胀,只有遇到換行符’\n’的時(shí)候才會(huì)返回法瑟,否則遇不到的話一直阻塞在那里。
- 部分代碼解讀:
當(dāng)進(jìn)程open “/dev/ptmx”的時(shí)候唁奢,獲得了一個(gè)新的pseudoterminal master(PTM)的文件描述符霎挟,同時(shí)會(huì)在/dev/pts目錄下自動(dòng)生成一個(gè)新的pseudoterminal slave(PTS)設(shè)備。每次open “/dev/ptmx”會(huì)得到一個(gè)不同的PTM文件描述符(多次open會(huì)得到多個(gè)文件描述符)麻掸,并且有和這個(gè)PTM描述符關(guān)聯(lián)的PTS酥夭。
grantpt, unlockpt: 在每次打開(kāi)pseudoterminal slave的時(shí)候,必須傳遞對(duì)應(yīng)的PTM的文件描述符脊奋。grantpt以獲得權(quán)限熬北,然后調(diào)用unlockpt解鎖 。
向PTM寫(xiě)的數(shù)據(jù)可以從PTS讀出來(lái)诚隙,向PTS寫(xiě)的數(shù)據(jù)可以從PTM讀出來(lái)讶隐。注意,從例子中的輸出可以看出久又,從PTM寫(xiě)的數(shù)據(jù)PTM自己也能讀出來(lái)巫延。
如何實(shí)現(xiàn)自己的終端程序
從上面的代碼和原理效五,我們就很容易實(shí)現(xiàn)自己的偽終端,實(shí)現(xiàn)自己的偽終端程序烈评,我們沒(méi)必要像telnet和ssh一樣火俄,讓getty程序去讀取PTS的數(shù)據(jù),再轉(zhuǎn)發(fā)到對(duì)應(yīng)的shell讲冠。
最簡(jiǎn)單的實(shí)現(xiàn)方式是,啟動(dòng)/bin/bash程序适瓦,把該程序的輸入竿开、輸出與錯(cuò)誤輸出都綁定到PTS上,那么我們啟動(dòng)的/bin/bash程序就能夠接受PTM的請(qǐng)求玻熙,并進(jìn)行處理否彩,把結(jié)果返回到PTM。
這塊的代碼嗦随,就不寫(xiě)出來(lái)了列荔,大家有興趣可以自己去實(shí)現(xiàn)。
在后續(xù)我會(huì)繼續(xù)分享WebConsole實(shí)現(xiàn)篇就會(huì)用到這塊原理枚尼。