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实现篇就会用到这块原理。