第64章 伪终端

概述

伪终端(缩写通常是pty)是一个虚拟设备,提供一个IPC通道,一端是连接到终端设备的程序,一端也是一个程序,此程序通过IPC通过发送其输入并读取输出以此来驱动面向终端的程序

伪终端主从设备

伪终端提供了网络连接到面向终端程序之间缺失的一环,是一对互联的虚拟设备:主伪终端和从伪终端,伪终端对提供了一个IPC通道,一点类似于双向管道:两个进程分别打开主端和从端,并通过伪终端双向传输数据,伪终端的特点是从设备表现的跟标准终端一样

如何使用伪终端
  1. 驱动程序打开伪终端
  2. 驱动程序调用fork创建一个子进程并执行如下步骤:
  • 调用setsid启动一个新会话,使得该子进程成为会话的首进程,该操作使得子进程失去它的控制终端
  • 打开同伪终端主设备对应的从设备,由于子进程是会话的首进程且无控制终端,故而该从设备就成为子进程的控制终端了
  • 调用dup为从设备复制标准输入、标准输出、错误输出的文件描述符
  • 调用exec启动要连接到伪终端从设备的面向终端程序

此时驱动程序和面向终端的程序就可以通过伪终端进行通信

伪终端的应用
  • 网络服务
  • expect使用伪终端允许交互式面向终端程序可以从脚本文件中驱动
  • xterm类似的终端模拟器利用伪终端提供带有终端窗口的功能
  • screen利用伪终端在单个物理终端同多个进程(多个shell)实现多路复用
  • script利用伪终端记录在shell会话中的所有输入和输出
  • 向文件或管道写输出时,有时候利用伪终端绕过由stdio实现的默认块缓冲机制
    System V(UNIX98)和BSD伪终端
    System V的实现在某种程度比BSD接口易于使用,SUSv3的伪终端规范就是基于System V的接口,Linux上的伪终端通常指的是UNIX98伪终端

    UNIX98伪终端

    打开未使用的主设备:posix_openpt
    ```

    define _XOPEN_SOURCE 600

    include

    include

int posix_openpt(int oflag); // 返回值:若成功返回第一个可用的主设备的文件描述符,若出错返回-1 // oflag的取值: O_RDWR:同时可读可写 O_NOCTTY:使得该终端不要成为进程的控制终端,Linux上无论是否指定,都不会成为进程的控制终端 // 同open一样,打开的始终时最小的可用文件描述符 // 该调用会在/dev/pts文件夹中创建对应的伪终端从设备文件 // 每一对伪终端都会占据一小段不能被交换的内核内存空间 // Linux的/proc/sys/kernel/pty/max:伪终端的数量 // Linux的/proc/sys/kernel/pty/nr: 当前系统有多少伪终端正在使用中

  1. ##### 修改从设备属主和权限:grantpt

define _XOPEN_SOURCE 500

include

int grantpt(int fildes); // 返回值:若成功返回0,若出错返回-1 // 该函数会创建一个子进程pt_chown来执行设定用户ID为root的程序,在伪终端从设备做如下操作:

  1. 将从设备属主修改为与调用进程相同的有效用户ID
  2. 将从设备的组修改为tty
  3. 修改从设备的权限,使得拥有者由读写权限,组有写权限 // 在Linux上,会自动执行上述步骤,故而无需调用此函数,为了移植性,仍然应该调用
  1. ##### 解锁从设备:unlockpt

define _XOPEN_SOURCE 500

include

int unlockpt(int mfd); // 返回值:若成功返回0,若出错返回-1 // 该函数会移除从设备的内部锁,该从设备与文件描述符mfd所代表的伪终端主设备相关联,解锁的目的是为了允许调用进程在其他进程能够打开这个伪终端从设备之前执行必要的初始化工作,如调用grantpt // 该函数调用之前尝试打开伪终端从设备将导致失败,错误码EIO

  1. ##### 获取从设备名称:ptsname

define _XOPEN_SOURCE 500

include

char *ptsname(int mfd); // 返回值:若成功返回静态分配的字符串,若出错返回NULL // Linux上,返回形式:/dev/pts/nn的字符串,nn是该伪终端从设备专有的唯一标识符

GNU C提供的可重入版本: int ptsname_r(int fildes, char *buffer, size_t buflen);

  1. ### 打开主设备
  2. 自定义的pty_master_open隐藏了所有特定于UNIX98规范的细节,用于打开一个未使用的伪终端主设备
  3. ### 将进程连接到伪终端
  4. 自定义的pty_fork创建一个子进程,通过伪终端对,连接到父进程上<br />Linux上,glibc提供了两个相关的非标准函数<br />openpty:打开一个伪终端对,返回主设备和从设备的文件描述符,也可以设置终端属性和窗口大小<br />forkpty:除了没有提供类似sn_len的参数,与自定义实现pty_fork完全一样
  5. ### 伪终端IO
  6. 一对伪终端和一个双向管道很相似,任何写入伪终端主设备的数据都会在伪终端从设备作为输入出现,任何写入到从设备的数据也会在主设备作为输入出现,区别在于伪终端的从设备表现的就像是一个终端设备一样,其解释输入的方式和一个普通的控制终端键盘输入方式一样,如Ctrl+C产生SIGINT信号<br />如果关闭所有代表伪终端主设备的文件描述符,则:
  7. - 如果从设备有一个控制进程,会发送SIGHUP信号到那个进程
  8. - 从设备读取的read返回文件结尾EOF(值为0
  9. - 写入到从设备的write操作失败,错误码EIO
  10. 如果关闭所有代表伪终端从设备的文件描述符,则:
  11. - 主设备的read操作失败,错误码EIO
  12. - 写入到主设备的write操作会成功,除非从设备的输入队列已满,则阻塞,如果重新打开从设备,则因阻塞写入的字节都可以被读取
  13. ##### 信包模式
  14. 信包模式是当伪终端从设备上与软流控相关的事件发生时,自动通知给运行在伪终端主设备上进程的机制,这些事件包括:
  15. - 刷新输入或输出队列
  16. - 开启或开启终端输出(Ctrl+S/Ctrl+Q
  17. - 开启或关闭流控
  18. 可通过如下方式启动信包模式:

int arg;

// m_fd表示伪终端主设备文件描述符 if (-1 == ioctl(m_fd, TIOCPKT, &arg)) perror(“ioctl err”);

``` 启动信包模式后,从伪终端主设备读取,要么返回一个单字节非0控制符,表示从设备的状态十分改变,要么返回0字节,紧跟着写入到从设备的单个或多个字节数据
当工作于信包模式的伪终端状态发生变化,select会提示主设备发生异常(参数exceptfds),而poll在revents中返回POLLPRI

实现script(1)程序

简化版的标准script(1)程序:开启一个新的shell会话,从该会话记录所有的输入和输出到文件,其实大部分shell会话都是使用script程序记录的
普通的登录会话中,shell直接连接到用户的终端,运行script程序时,将自己置于用户的终端和shell之间,然后使用一对伪终端在自己和shell之间创建通信通道
shell:连接到伪终端从设备上
script:连接到伪终端主设备端,如同代理,接收用户键入到终端的输入然后写入伪终端主设备,从伪终端从设备读取再写入到用户的终端,会生成一个输出文件(默认名为typescript),该文件包含所有输出到伪终端主设备的字节,这就达到了不仅记录shell会话产生的输出,还包含了用户提供的输入的效果
WechatIMG55.jpeg

终端属性和窗口大小

伪终端主从设备共享终端属性和窗口大小,这意味着运行在伪终端主设备的程序可以通过主设备文件描述符调用tcsetattr和ioctl来修改从设备的属性和窗口大小
伪终端从设备上面向屏幕的程序如vi输出如果因为主设备修改窗口大小而出现错乱,可按如下步骤修正:

  1. script父进程安装SIGWINCH信号处理函数
  2. 当script父进程收到SIGWINCH信号,使用TIOCGWINSZ ioctl操作伪终端窗口关联的标准输入获取一个winsize结构体,利用它再使用TIOCSWINSZ ioctl操作修改伪终端主设备的窗口大小
  3. 如果新的伪终端窗口大小与旧的不同,内核会产生SIGWINCH信号给伪终端从设备的前台进程组,vi这样的屏幕处理程序可捕获此信号并执行TIOCSWINSZ ioctl操作更新它们的终端窗口大小

    BSD风格的伪终端

    可选的组件配置选项:CONFIG_LEGACY_PTYS
    BSD伪终端同UNIX98伪终端的区别仅仅只是在如何找到并打开伪终端主从设备的细节上
  • UNIX98伪终端:通过调用posix_openpt(打开/dev/ptmx伪终端主设备的克隆)获取未使用的伪终端主设备,通过ptsname获取对应的从设备名称
  • BSD伪终端:主从设备已经在/dev下预先创建好了,每个主设备的名称按照/dev/ptyxy的形式呈现,x由[p-za-e]范围的16个字符替换,y由[0-9a-f]范围内的16个字符替换,从设备名称为/dev/ttyxy,如/dev/ptyp0和/dev/ttyp0组成了BSD伪终端对

要找出未使用的伪终端对,通过循环尝试打开每个主设备,直到能成功打开为止,调用open可能遇到两个错误:

  1. 如果主设备不存在,错误码为ENOENT,通常表示已经遍历整个主设备的组合,未找到空闲设备
  2. 如果主设备正在使用,错误码EIO,一般忽略直接打开下一个设备即可

一旦找到可用的主设备,获取从设备名称只需要替换pty为tty即可,对于BSD伪终端,没有等价的grantpt函数修改从设备的属主和权限,如果需要修改,必须显示的调用chown和chmod