zoukankan      html  css  js  c++  java
  • 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);
    }
    
  • 相关阅读:
    jsp自定义标签
    spring JDBC 返回list<map>集合
    AWS 身份及验证服务(四)
    AWS 存储服务(三)
    AWS 核心服务概述(二)
    云计算和 AWS 概述(一)
    AWS 解决方案架构师考点(Storage)
    利用 AWS 无服务架构之语音合成
    AWS 云上安全最佳实践
    持续集成 Jenkins +Gitlab + SSH 自动发布 HTML 代码
  • 原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250356.html
Copyright © 2011-2022 走看看