Linux伪终端

终端的历史

最初的计算机由于价格昂贵,因此,一台计算机一般是由多个人同时使用的。在这种情况下一台计算机需要连接上许多套键盘和显示器来供多个人使用。在以前专门有这种可以连上一台电脑的设备,只有显示器和键盘,还有简单的处理电路,本身不具有处理计算机信息的能力,他是负责连接到一台正常的计算 机上(通常是通过串口),然后登陆计算机,并对该计算机进行操作。当然,那时候的计算机操作系统都是多任务多用户的操作系统。这样一台只有显示器和键盘能够通过串口连接到计算机 的设备就叫做终端。

笔者刚毕业时(比较早),曾经做过银行系统,在银行的网点会配置一台中控机或者PC机,为每台主机配置多台终端,这样可以给多名柜员和出纳使用。

现在由于计算机硬件越来越便宜,通常一个人独占一台计算机,不再需要终端这段硬件设备,因此,现在终端的概念慢慢演化成了软件的概念。现在的终端种类有:

  • 伪终端
    伪终端又称为模拟终端,远程连接的终端或图形界面下打开的终端接口,设备文件为:/dev/pts/#
  • 虚拟终端
    Ctrl +Alt + F[1-6]
    图形终端Ctrl + Alt + F7
    设备文件为:/dev/tty#
  • 物理终端(控制台)
    与主机直接相连,设备文件为:/dev/console
  • 串行终端
    串口输出,设备文件为:/dev/ttys#

伪终端的基本原理

Linux支持两种pty:

  • UNIX98 pseudoterminal,使用的是devpts文件系统,挂载在/dev /pts目录
  • 在UNIX98 pseudoterminal之前,master pseudoterminal名字为/dev/ptyp0,…,slave pseudoterminal名字为/dev/ttyp0,…,这个方法需要预先分配好很多的设备节点。
    所以,这里我们主要是针对UNIX98伪终端,这种模式的伪终端,是由ptmx与pts配合实现。例如我们在telnet,ssh等远程终端工具中会使用到pty,通常的数据流是这样的:
              TCP/IP
telnet ====================> [ telnetd进程 ---> /dev/ptmx(master) ---> /dev/pts/?(slave) ---> getty ]

telnet通过网络协议与linux主机上的telnetd进程通讯,telnetd进程收到网络中的数据后,将数据写入/dev/ptmx,/dev/ptmx像管道一样将数据传递给/dev/pts/?,getty进程从pts/?读取数据传递给shell去执行。

伪终端设备

伪终端设备前面已经已经说过,它位于/dev/pts目录下,是一个数字代表的文件。
当我们在打开/dev/ptmx文件的时候,系统会自动在/dev/pts目录下创建一个新的设备文件,这时候,系统会自动给打开的/dev/ptmx文件描述符和/dev/pts/#设备文件之间形成m->s的关系。写入/dev/ptmx文件描述符的数据,会自动传到新创建的/dev/pts/#设备文件。
只要不关闭/dev/ptmx文件描述符,那么这个设备文件就会存在,一旦关闭,这个设备文件会自动消失

可以通过如下一个例子演示在”open /dev/ptmx”的时候在/dev/pts目录下生成的设备节点

$ 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

可见在重定向/dev/ptmx的时候在/dev/pts目录下多了个设备节点8,而当上面这个shell结束的时候再次ls /dev/pts目录,设备节点8又消失了。


用go语言实现

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中读取数据时,只有遇到换行符’\n’的时候才会返回,否则遇不到的话一直阻塞在那里。

  • 部分代码解读:

当进程open “/dev/ptmx”的时候,获得了一个新的pseudoterminal master(PTM)的文件描述符,同时会在/dev/pts目录下自动生成一个新的pseudoterminal slave(PTS)设备。每次open “/dev/ptmx”会得到一个不同的PTM文件描述符(多次open会得到多个文件描述符),并且有和这个PTM描述符关联的PTS。

grantpt, unlockpt: 在每次打开pseudoterminal slave的时候,必须传递对应的PTM的文件描述符。grantpt以获得权限,然后调用unlockpt解锁 。

向PTM写的数据可以从PTS读出来,向PTS写的数据可以从PTM读出来。注意,从例子中的输出可以看出,从PTM写的数据PTM自己也能读出来。

如何实现自己的终端程序

从上面的代码和原理,我们就很容易实现自己的伪终端,实现自己的伪终端程序,我们没必要像telnet和ssh一样,让getty程序去读取PTS的数据,再转发到对应的shell。
最简单的实现方式是,启动/bin/bash程序,把该程序的输入、输出与错误输出都绑定到PTS上,那么我们启动的/bin/bash程序就能够接受PTM的请求,并进行处理,把结果返回到PTM。
这块的代码,就不写出来了,大家有兴趣可以自己去实现。

在后续我会继续分享WebConsole实现篇就会用到这块原理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345