《unix环境高级编程》--- 伪终端

讲解:https://www.cnblogs.com/zzdyyy/p/7538077.html

伪终端对于一个应用程序而言,看上去像一个终端,但事实上伪终端并不是一个真正的终端。从内核角度看,伪终端看起来像一个双向管道,而事实上Solaris的伪终端就是用STREAMS构建的。伪终端总是成对地使用的,就好像是个管道的两端。一端的设备称为”主设备”(master),另一端的设备称为”从设备”(slave),每一对伪终端设备,例如/dev/ptys0和/dev/ttys0,就好像是通过一个管道连在一起,其”从设备”一端与普通的终端设备没有什么区别,而”主设备”一端则跟管道文件相似。

master端是更接近用户显示器、键盘的一端,slave端是在虚拟终端上运行的CLI(Command Line Interface,命令行接口)程序。Linux的伪终端驱动程序,会把“master端(如键盘)写入的数据”转发给slave端供程序输入,把“程序写入slave端的数据”转发给master端供(显示器驱动等)读取。

使用伪终端时的关键点:
1、通常一个进程打开伪终端主设备,然后调用fork。子进程建立了一个新的会话,打开一个相应的伪终端从设备,将其文件描述符复制到标准输入、标准输出和标准出错,然后调用exec。伪终端从设备成为子进程的控制终端。
2、对于伪终端从设备上的用户进程来说,其标准输入、标准输出和标准出错都是终端设备。对于这些文件描述符,用户进程可调用所有输入/输出函数,但无意义的函数调用(改变波特率、发送中断符、设置奇偶校验)将被忽略。
3、任何写到伪终端主设备的东西都会作为从设备的输入,反之亦然。所有设备端的输入都来自于伪终端主设备上的用户进程。类似于双向管道,但从设备上的终端行规程拥有普通管道没有的其他处理能力。

这里写图片描述

伪终端的用途:
(1)构造网络登录服务器,例如telnetd和rlogind服务器。
(2)script程序,将终端会话的所有输入和输出信息复制到一个文件中,自己置于终端和登录shell的一个新调用之间。
(3)expect程序,伪终端可以在非交互模式中驱动交互程序的运行
(4)运行协同进程
(5)观看长时间运行程序的输出

基于STREAMS的伪终端打开函数

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

/* 打开下一个可用的PTY主设备 */
int ptym_open(char *pts_name, int pts_namesz)
{
    char    *ptr;
    int fdm, err;

    /*
    int posix_openpt(int oflag);
    打开下一个可用的伪终端主设备。
    返回值:成功返回下一个可用的PTY主设备的文件描述符,出错返回-1
    oflag:位屏蔽字,指定如何打开主设备。可指定O_RDWR,要求打开主设备进行读、写;
               可指定O_NOCTTY以防止主设备成为调用者的控制终端。
    */
    if ((fdm = posix_openpt(O_RDWR)) < 0)
        return(-1);

    /*
    int grantpt(int filedes);
    在伪终端从设备可被使用前,必须设置它的权限,使得应用程序可以访问它。
    该函数把从设备节点的用户ID设置为调用者的实际用户ID,设置其组ID为一非指定值,
    通常是访问该终端设备的组。将权限设置为:对单个所有者是读/写,对组所有者是写(0620)
    */
    if (grantpt(fdm) < 0)       /* grant access to slave */
        goto errout;

    /*
    int unlockpt(int filedes);
    清除从设备的内部锁,从而允许应用程序打开该设备。
    进程打开从设备后,建立该设备的应用程序有机会在使用主、从设备之前正确地初始化这些从设备。
    */
    if (unlockpt(fdm) < 0)      /* clear slave's lock flag */
        goto errout;

    /*
    char *ptsname(int filedes);
    在给定主伪终端设备的文件描述符时,找到从伪终端设备的路径名。
    返回值:成功则返回指向PTY从设备名的指针,出错返回NULL
    */
    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] = '\0';
    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;

    /*
    打开从设备后,将三个STREAMS模块压入从设备流中。
    ptem是伪终端仿真模块,ldterm是终端行规程模块,这两个模块合在一起像一个真正的终端一样工作。
    ttcompat提供了向早期系统的ioctl调用的兼容性。这是一个可选模块,但因为对于控制台登录和网络登录,
    它是自动被压入的,所以我们将其压入从设备的流中。
    也可能不需要压入这三个模块,因为它们可能已经位于流中。使用I_FIND ioctl命令观察ldterm是否已在流中。
    */
    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函数
用fork调用打开主设备和从设备,创建作为会话首进程的子进程并使其具有控制终端。
返回值:子进程中返回0,父进程中返回子进程的进程ID,出错返回-1。

打开PTY主设备后,调用fork。子进程先调用setsid建立新的会话,然后调用ptys_open。
当调用setsid时,子进程还不是一个进程组的首进程,因此执行以下3个步骤:
1、为子进程创建一个新的会话,它是该会话首进程
2、为子进程创建一个新的进程组
3、子进程断开与以前可能有的控制终端的关联,于是不再有控制终端
Linux和Solaris中,调用pyts_open时,从设备成为新会话的控制终端。在FreeBSD和Mac OS X中,必须调用ioctl并使用参数TIOCSCTTY分配一个控制终端。
然后在termios和winsize这两个结构在子进程中被初始化。
最后从设备的文件描述符被复制到子进程的标准输入、标准输出和标准出错中。意味着不管子进程以后调用
exec执行何种进程,它都具有同PTY从设备(其控制终端)联系起来的上述三个描述符。
调用fork后,父进程返回PTY主设备的描述符及子进程ID。

#include "apue.h"
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif

/*
PTY主设备的文件描述符通过ptrfdm指针返回。
如果slave_name不为空,从设备名就被存放在该指针指向的存储区中。调用者必须为该存储区分配空间。
如果slave_termios不为空,则用其初始化从设备的终端行规程。如果为空,把从设备的termios结构设置
为实现定义的初始状态。
如果slave_winsize不为空,则用其初始化从设备的窗口大小。如果为空,winsize被初始化为0。

*/
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 slave. Null terminate to handle case where
           strlen(pts_name) > slave_namesz */
        strncpy(slave_name, pts_name, slave_namesz);
        slave_name[slave_namesz - 1] = '\0';
    }

    if((pid = fork()) < 0)
        return (-1);
    else if(pid == 0)
    {
        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(TIOCSCTTY)
        /* 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 erro to stderr");
        if(fds != STDIN_FILENO && fds != STDOUT_FILENO && fds != STDERR_FILENO)
            close(fds);
        return (0);
    }
    else  /* parent */
    {
        *ptrfdm = fdm;  /* return fd of master */
        return (pid);   /* parent returns pid of child */
    }
}

pty程序的main函数
getopt分析命令行参数。
调用pty_fork前,取termios和winsize结构的当前值,将其作为参数传递给pty_fork,使得PTY从设备具有和当前终端相同的初始状态。
从pty_fork返回后,子进程可选择地关闭PTY从设备的回送,并调用execvp执行命令行指定的程序。所有余下的命令行参数成为该程序的参数。
父进程可选地将用户端设置为原始模式。这种情况下,父进程也设置退出处理程序,使得在调用exit时复原终端状态。
接下来,父进程调用loop。该函数仅仅将从标准输入接收到的所有内容复制到PTY主设备,并将PTY主设备接收到的所有内容复制到标准输出。

#include "apue.h"
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif

#ifdef LINUX
#define OPTSTR "+d:einv"
#else
#define OPTSTR "d:einv"
#endif

static void set_noecho(int);  /* at the end of this file */
void do_driver(char *);       /* in the file driver.c */
void loop(int, int);          /* in the file loop.c */

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\n", slave_name);
        if(driver != NULL)
            fprintf(stderr, "driver = %s\n", 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);
}

/* turn off echo (for slave pty) */
static void set_noecho(int fd)
{
    struct termios stermios;

    if(tcgetattr(fd, &stermios) < 0)
        err_sys("tcgetattr error");

    stermios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);

    /* also turn off NL to CR/NL mapping on output */
    stermios.c_lflag &= ~(ONLCR);

    if(tcsetattr(fd, TCSANOW, &stermios) < 0)
        err_sys("tcsetattr error");
}

loop函数
子进程将从标准输入接收到的所有内容复制到PTY主设备,父进程将PTY主设备接收到的所有内容复制到标准输出。
当使用两个进程时,如果一个终止,必须通知另一个。本例用SIGTERM信号通知。

#include "apue.h"

#define BUFFSIZE 512

static void sig_term(int);
static volatile sig_atomic_t sigcaught;  /* set by signal handler */

void loop(int ptym, int ignoreeof)
{
    pid_t child;
    int nread;
    char buf[BUFFSIZE];

    if((child = fork()) < 0)
    {
        err_sys("fork error");
    }

    /* child copies stdin to ptym */
    else if(child == 0)
    {
        for(;;)
        {
            if((nread = read(STDIN_FILENO, buf, BUFFSIZE)) < 0)
                err_sys("read error from stdin");
            else if(nread == 0)
                break;
            if(writen(ptym, buf, nread) != nread)
                err_sys("writen error to master pty");
        }

        /* We always terminate when we encounter an EOF on stdin
           but we notify the parent only if ignoreeof is 0 */       
        if(ignoreeof == 0)
            kill(getpid(), SIGTERM);  /* notify parent */
        exit(0);  /* and terminate; child can't return */
    }

    /* Parent copies ptym to stdout */
    if(signal_intr(SIGTERM, sig_term) == SIG_ERR)
        err_sys("signal_intr error for SIGTERM");
    for(;;)
    {
        if((nread = read(ptym, buf, BUFFSIZE)) <= 0)
            break;
        if(writen(STDOUT_FILENO, buf, nread) != nread)
            err_sys("writen error to stdout");
    }

    /* There are three ways to get here: sig_term() below caught the SIGTERM
       from the child, we read an EOF on the pty master (which means we have
       to signal the child to stop), or an error */
    if(sigcaught == 0)  /* tell child if it didn't send us the signal */
        kill(child, SIGTERM);

    /* Parent returns to caller */
}

/* The child sends us SIGTERM when it gets EOF on the pty slave or when read() fails.
   We probably interrupted the read() of ptym. */
static void sig_term(int signo)
{
    sigcaught = 1;  /* jusg set flag and return */
}

pty程序的do_driver函数
从shell脚本驱动交互式程序。使用选项-d使pty程序的输入和输出与驱动进程连接起来。该驱动进程的标准输出是pty的标准输入,反之亦然。有点像协同进程,只是在pty的“另一边”。pty完成驱动进程的fork和exec。在pty和驱动进程之间使用一个双向的流管道,而不是两个半双工管道。
在使用-d选项时,以下函数被pty的main调用。

#include "apue.h"

void do_driver(char *driver)
{
    pid_t child;
    int pipe[2];

    /* Create a stream pipe to communicate with the driver */
    if(fd_pipe(pipe) < 0)
        err_sys("can't create stream pipe");

    if((child = fork()) < 0)
        err_sys("can't create stream pipe");
    else if(child == 0)  /* child */
    {
        close(pipe[1]);

        /* stdin for driver */
        if(dup2(pipe[0], STDIN_FILENO) != STDIN_FILENO)
            err_sys("dup2 error to stdin");

        /* stdout for driver */
        if(dup2(pipe[0], STDOUT_FILENO) != STDOUT_FILENO)
            err_sys("dup2 error to stdout");
        if(pipe[0] != STDIN_FILENO && pipe[0] != STDOUT_FILENO)
            close(pipe[0]);

        /* leave stderr for driver alone */
        execlp(driver, driver, (char *)0);
        err_sys("execlp error for: %s", driver);
    }

    close(pipe[0]);
    if(dup2(pipe[1], STDIN_FILENO) != STDIN_FILENO)
        err_sys("dup2 error to stdin");
    if(dup2(pipe[1], STDOUT_FILENO) != STDOUT_FILENO)
        err_sys("dup2 error to stdout");
    if(pipe[1] != STDIN_FILENO && pipe[1] != STDOUT_FILENO)
        close(pipe[1]);

    /* Parent returns, but with stdin and stdout connected to the driver */
}

以上函数之间的关系:

PROGS = pty
all:    $(PROGS)
pty:    main.o loop.o driver.o $(LIBAPUE)
    $(CC) $(CFLAGS) -o pty main.o loop.o driver.o $(LDFLAGS) $(LDLIBS)

使用
1、作业控制交互
pyt cat
这里写图片描述

2、检查长时间运行程序的输出
忽略来自标准输入的文件结束符。遇到文件结束符时,子进程终止,但子进程不会告诉父进程也终止。相反,父进程一直在将PTY从设备的输出复制到标准输出(本例的file.out)

./pty -i slowout < /dev/null > file.out &

3、script程序
执行以下脚本:

#!bin/sh
./pty "${SHELL:-bin/sh}" | tee typescript

script仅仅是将新的shell(和它调用的所有的子进程)的输出复制处理,但因为PTY从设备上的行规程
模块通常允许回送,所以绝大多数键入都被写入typescript文件中。

4、运行协同进程
if(execl("./add2", "add2", (char *)0) < 0)
替换为if(execl("./pty", "pty", "-e", "add2", (char *)0) < 0)
在pty下运行协同进程,即使协同进程使用了标准I/O,仍可正确执行。

猜你喜欢

转载自blog.csdn.net/u012319493/article/details/80650714