讲解: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
#include
#if defined(SOLARIS)
#include
#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
#ifndef TIOCGWINSZ
#include
#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
#ifndef TIOCGWINSZ
#include
#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)
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,仍可正确执行。