UNIX环境高级编程(19-伪终端)

概述

伪终端是指,对于一个应用程序,它看上去是一个终端,而实际上却并不是一个真正的终端。

父进程首先打开一个伪终端主设备,随后fork,子进程打开相应的伪终端从设备,并将该文件描述符复制到stdin/out/err,最后调用exec。

对于伪终端从设备上的用户进程来说,其stdin/out/err都是终端设备,因此可以处理上一章介绍的各类终端I/O函数。并且,所有写到伪终端主设备的都会作为从设备的输入,反之亦然。

其典型结构如下图所示:

Typical arrangement of processes using a pseudo terminal

为说明方面,下面将伪终端简称为PTY。

典型用途

网络登录服务器

如telnetd和rlogind服务器。这方面不太熟悉,故不展开讲。

窗口系统终端模拟

窗口系统通常提供一个终端模拟器,这使得我们可以在命令行环境下通过shell运行程序。

终端模拟器是shell和窗口管理器之间的媒介。shell将自己的标准输入/输出/错误连接到PTY的从设备端,而终端模拟器则管理对应的主设备端。其大致框图如下:

Arrangement of processes for windowing system

当用户改变模拟器的窗口大小时,窗口管理器会通知模拟器,而模拟器则在主设备端利用TIOCSWINSZ命令设置从设备的窗口大小。前台PTY从设备的进程组会收到SIGWINCH信号,可以进行相应的处理。

script程序

script程序可以将终端的所有输入和输出信息复制到一个文件中(默认是typescript)。为了获取shell的输入输出信息,它需要调用一个shell并使其连接到PTY从设备上,而将自己连接到PTY主设备上,将得到的输入输出信息都复制到指定的文件中。

The script program

运行协同进程

当通过管道与协同进程通信时,标准I/O是全缓冲的,对于调用标准I/O的协同进程,除非手动调用fflush,否则会引起死锁。

而如果在两个进程之间放入伪终端,则协同进程会认为它是由终端驱动的,从而设置为行缓冲。通过调用pty_fork或者exec一个pty程序,并将协同进程作为参数即可实现。这两种方法用到的函数和程序会在下文详细说明。

观看长时间运行程序的输出

通常会将需要长时间运行的程序放到shell的后台运行(后面加上&)。如果需要观察其输出,一般将其重定向到一个文件,那么这时候输出就会变成全缓冲,需要积累到一定量的数据后才会真正输出。

与上面的协同进程类似,可以通过伪终端来解决,在pty程序下运行该程序。

Running a slow output program using a pseudo terminal

途中shell到pty的虚线表示pty进程是作为后台任务运行的。

打开伪终端设备

#include <stdlib.h>
#include <fcntl.h>
// Returns: file descriptor of next available PTY master if OK, −1 on error
int posix_openpt(int oflag);

posix_openpt用于打开下一个可用的PTY主设备,其oflag参数用于指定如何打开主设备,支持O_RDWR以读写方式打开,和O_NOCTTY防止主设备成为调用者的控制终端。

#include <stdlib.h>
// Both return: 0 on success, −1 on error
int grantpt(int fd);
int unlockpt(int fd);

这两个函数用于从设备的权限设置,在从设备可用之前,必须调用这两个函数。

grantpt函数把从设备节点的用户ID设置为调用者的实际用户ID,设置其组ID为一非指定值,通常是可以访问该终端设备的组。权限被设置为:对个体所有者是读/写,对组所有者是写(0620)。

unlockpt函数用于准予对PTY从设备的访问,从而允许应用程序打开该设备。

这两个函数使用的文件描述符都是与PTY主设备关联的文件描述符。

#include <stdlib.h>
// Returns: pointer to name of PTY slave if OK, NULL on error
char *ptsname(int fd);

ptsname可以利用主设备的描述符来找到对应从设备的路径名。该函数返回的名字可能存储于静态存储中,因此后续调用可能会覆盖它。、

XSI pseudo terminal functions

更便利的函数

本书作者提供了几个函数,帮助使用者处理了在调用上述函数时需要处理的细节。

打开主设备和从设备

#include "apue.h"
// Returns: file descriptor of PTY master if OK, −1 on error
int ptym_open(char *pts_name, int pts_namesz);
// Returns: file descriptor of PTY slave if OK, −1 on error
int ptys_open(char *pts_name);

ptym_open用于打开下一个可用的PTY主设备,调用者需要分配一个数组来存放pts_name返回的从设备的名字。pts_namesz用于指定数组长度,以避免该函数复制比数组空间更长的字符串。

ptys_open打开pts_name指定的从设备。

通常,不直接调用这两个函数,而是通过pty_fork函数(见下文)调用它们,并且会fork出一个子进程。

原始代码位于书本资料的/lib/ptyopen.c,可以参考本项目中的相关文件:https://gitee.com/maxiaowei/Linux/blob/master/apue/apue/ptyopen.c

摘录代码如下:

#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#if defined(SOLARIS)
#include <stropts.h>
#endif

int
ptym_open(char *pts_name, int pts_namesz)
{
    char	*ptr;
    int		fdm, err;

    if ((fdm = posix_openpt(O_RDWR)) < 0)
        return(-1);
    if (grantpt(fdm) < 0)		/* grant access to slave */
        goto errout;
    if (unlockpt(fdm) < 0)		/* clear slave's lock flag */
        goto errout;
    if ((ptr = ptsname(fdm)) == NULL)	/* get slave's name */
        goto errout;

    /*
     * Return name of slave.  Null terminate to handle
     * case where strlen(ptr) > pts_namesz.
     */
    strncpy(pts_name, ptr, pts_namesz);
    pts_name[pts_namesz - 1] = '';
    return(fdm);			/* return fd of master */
errout:
    err = errno;
    close(fdm);
    errno = err;
    return(-1);
}

int
ptys_open(char *pts_name)
{
    int fds;
#if defined(SOLARIS)
    int err, setup;
#endif

    if ((fds = open(pts_name, O_RDWR)) < 0)
        return(-1);

#if defined(SOLARIS)
    /*
     * Check if stream is already set up by autopush facility.
     */
    if ((setup = ioctl(fds, I_FIND, "ldterm")) < 0)
        goto errout;

    if (setup == 0) {
        if (ioctl(fds, I_PUSH, "ptem") < 0)
            goto errout;
        if (ioctl(fds, I_PUSH, "ldterm") < 0)
            goto errout;
        if (ioctl(fds, I_PUSH, "ttcompat") < 0) {
errout:
            err = errno;
            close(fds);
            errno = err;
            return(-1);
        }
    }
#endif
    return(fds);
}

pty_fork

#include "apue.h"
#include <termios.h>
// Returns: 0 in child, process ID of child in parent, −1 on error
pid_t pty_fork(int *ptrfdm, char *slave_name, int slave_namesz,
               const struct termios *slave_termios,
               const struct winsize *slave_winsize);

pty_fork会用fork调用打开主设备和从设备,创建作为会话首进程的子进程(利用setsid)并使其具有控制终端。

ptrfdm指针返回主设备的文件描述符;如果slave_name不为空,则从设备名被存储在该指针指向的内存空间;如果slave_termios不为空,则将从设备的终端行规程设定为指定的值(利用tcsetattr);slave_winsize同理(利用ioctl的TIOCSWINSZ命令)。

原始代码位于书本资料的/lib/ptyfork.c,也可以参考本项目中的相关文件:https://gitee.com/maxiaowei/Linux/blob/master/apue/apue/ptyfork.c

#include "apue.h"
#include <termios.h>

pid_t
pty_fork(int *ptrfdm, char *slave_name, int slave_namesz,
         const struct termios *slave_termios,
         const struct winsize *slave_winsize)
{
    int		fdm, fds;
    pid_t	pid;
    char	pts_name[20];

    if ((fdm = ptym_open(pts_name, sizeof(pts_name))) < 0)
        err_sys("can't open master pty: %s, error %d", pts_name, fdm);

    if (slave_name != NULL) {
        /*
         * Return name of slave.  Null terminate to handle case
         * where strlen(pts_name) > slave_namesz.
         */
        strncpy(slave_name, pts_name, slave_namesz);
        slave_name[slave_namesz - 1] = '';
    }

    if ((pid = fork()) < 0) {
        return(-1);
    } else if (pid == 0) {		/* child */
        if (setsid() < 0)
            err_sys("setsid error");

        /*
         * System V acquires controlling terminal on open().
         */
        if ((fds = ptys_open(pts_name)) < 0)
            err_sys("can't open slave pty");
        close(fdm);		/* all done with master in child */

#if	defined(BSD)
        /*
         * TIOCSCTTY is the BSD way to acquire a controlling terminal.
         */
        if (ioctl(fds, TIOCSCTTY, (char *)0) < 0)
            err_sys("TIOCSCTTY error");
#endif
        /*
         * Set slave's termios and window size.
         */
        if (slave_termios != NULL) {
            if (tcsetattr(fds, TCSANOW, slave_termios) < 0)
                err_sys("tcsetattr error on slave pty");
        }
        if (slave_winsize != NULL) {
            if (ioctl(fds, TIOCSWINSZ, slave_winsize) < 0)
                err_sys("TIOCSWINSZ error on slave pty");
        }

        /*
         * Slave becomes stdin/stdout/stderr of child.
         */
        if (dup2(fds, STDIN_FILENO) != STDIN_FILENO)
            err_sys("dup2 error to stdin");
        if (dup2(fds, STDOUT_FILENO) != STDOUT_FILENO)
            err_sys("dup2 error to stdout");
        if (dup2(fds, STDERR_FILENO) != STDERR_FILENO)
            err_sys("dup2 error to stderr");
        if (fds != STDIN_FILENO && fds != STDOUT_FILENO &&
          fds != STDERR_FILENO)
            close(fds);
        return(0);		/* child returns 0 just like fork() */
    } else {					/* parent */
        *ptrfdm = fdm;	/* return fd of master */
        return(pid);	/* parent returns pid of child */
    }
}

pty程序

用pty来执行另一个程序时,那个程序在它自己的会话中执行,并和一个伪终端连接。

当使用pty来运行一个程序的时候,以运行cat为例,其运行框图如下:

Process groups and sessions for pty cat

pty程序调用上一节所讲的pty_fork函数后,在它的子进程中调用exec执行命令行指定的程序,而父进程则调用loop函数,将标准输入接收到的内容复制到PTY主设备,将PTY主设备接收到的内容复制到标准输出。

pty程序包含main.c、loop.c和driver.c3个文件,可在书本资料的pty文件夹下找到。或者在本项目的ch19文件夹下寻找(pty.c对应于main.c),https://gitee.com/maxiaowei/Linux/tree/master/apue/ch19。对于loop函数,除了作者给出的通过父子进程实现,也可以使用select或poll实现(利用select实现可参考项目同目录下的19.3.c,poll由于不太熟悉未能成功)。

现将pty程序的main函数部分摘录如下,方便了解其具体实现:

int
main(int argc, char *argv[])
{
    int             fdm, c, ignoreeof, interactive, noecho, verbose;
    pid_t			pid;
    char			*driver;
    char			slave_name[20];
    struct termios	orig_termios;
    struct winsize	size;

    interactive = isatty(STDIN_FILENO);
    ignoreeof = 0;
    noecho = 0;
    verbose = 0;
    driver = NULL;

    opterr = 0;		/* don't want getopt() writing to stderr */
    while ((c = getopt(argc, argv, OPTSTR)) != EOF) {
        switch (c) {
        case 'd':		/* driver for stdin/stdout */
            driver = optarg;
            break;

        case 'e':		/* noecho for slave pty's line discipline */
            noecho = 1;
            break;

        case 'i':		/* ignore EOF on standard input */
            ignoreeof = 1;
            break;

        case 'n':		/* not interactive */
            interactive = 0;
            break;

        case 'v':		/* verbose */
            verbose = 1;
            break;

        case '?':
            err_quit("unrecognized option: -%c", optopt);
        }
    }
    if (optind >= argc)
        err_quit("usage: pty [ -d driver -einv ] program [ arg ... ]");

    if (interactive) {	/* fetch current termios and window size */
        if (tcgetattr(STDIN_FILENO, &orig_termios) < 0)
            err_sys("tcgetattr error on stdin");
        if (ioctl(STDIN_FILENO, TIOCGWINSZ, (char *) &size) < 0)
            err_sys("TIOCGWINSZ error");
        pid = pty_fork(&fdm, slave_name, sizeof(slave_name),
          &orig_termios, &size);
    } else {
        pid = pty_fork(&fdm, slave_name, sizeof(slave_name),
          NULL, NULL);
    }

    if (pid < 0) {
        err_sys("fork error");
    } else if (pid == 0) {		/* child */
        if (noecho)
            set_noecho(STDIN_FILENO);	/* stdin is slave pty */

        if (execvp(argv[optind], &argv[optind]) < 0)
            err_sys("can't execute: %s", argv[optind]);
    }

    if (verbose) {
        fprintf(stderr, "slave name = %s
", slave_name);
        if (driver != NULL)
            fprintf(stderr, "driver = %s
", driver);
    }

    if (interactive && driver == NULL) {
        if (tty_raw(STDIN_FILENO) < 0)	/* user's tty to raw mode */
            err_sys("tty_raw error");
        if (atexit(tty_atexit) < 0)		/* reset user's tty on exit */
            err_sys("atexit error");
    }

    if (driver)
        do_driver(driver);	/* changes our stdin/stdout */

    loop(fdm, ignoreeof);	/* copies stdin -> ptym, ptym -> stdout */

    exit(0);
}
原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250356.html