概述
伪终端是指,对于一个应用程序,它看上去是一个终端,而实际上却并不是一个真正的终端。
父进程首先打开一个伪终端主设备,随后fork,子进程打开相应的伪终端从设备,并将该文件描述符复制到stdin/out/err,最后调用exec。
对于伪终端从设备上的用户进程来说,其stdin/out/err都是终端设备,因此可以处理上一章介绍的各类终端I/O函数。并且,所有写到伪终端主设备的都会作为从设备的输入,反之亦然。
其典型结构如下图所示:
为说明方面,下面将伪终端简称为PTY。
典型用途
网络登录服务器
如telnetd和rlogind服务器。这方面不太熟悉,故不展开讲。
窗口系统终端模拟
窗口系统通常提供一个终端模拟器,这使得我们可以在命令行环境下通过shell运行程序。
终端模拟器是shell和窗口管理器之间的媒介。shell将自己的标准输入/输出/错误连接到PTY的从设备端,而终端模拟器则管理对应的主设备端。其大致框图如下:
当用户改变模拟器的窗口大小时,窗口管理器会通知模拟器,而模拟器则在主设备端利用TIOCSWINSZ
命令设置从设备的窗口大小。前台PTY从设备的进程组会收到SIGWINCH信号,可以进行相应的处理。
script程序
script程序可以将终端的所有输入和输出信息复制到一个文件中(默认是typescript)。为了获取shell的输入输出信息,它需要调用一个shell并使其连接到PTY从设备上,而将自己连接到PTY主设备上,将得到的输入输出信息都复制到指定的文件中。
运行协同进程
当通过管道与协同进程通信时,标准I/O是全缓冲的,对于调用标准I/O的协同进程,除非手动调用fflush,否则会引起死锁。
而如果在两个进程之间放入伪终端,则协同进程会认为它是由终端驱动的,从而设置为行缓冲。通过调用pty_fork或者exec一个pty程序,并将协同进程作为参数即可实现。这两种方法用到的函数和程序会在下文详细说明。
观看长时间运行程序的输出
通常会将需要长时间运行的程序放到shell的后台运行(后面加上&)。如果需要观察其输出,一般将其重定向到一个文件,那么这时候输出就会变成全缓冲,需要积累到一定量的数据后才会真正输出。
与上面的协同进程类似,可以通过伪终端来解决,在pty程序下运行该程序。
途中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
可以利用主设备的描述符来找到对应从设备的路径名。该函数返回的名字可能存储于静态存储中,因此后续调用可能会覆盖它。、
更便利的函数
本书作者提供了几个函数,帮助使用者处理了在调用上述函数时需要处理的细节。
打开主设备和从设备
#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为例,其运行框图如下:
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);
}