第十九章 伪终端
时间:2005-08-31 来源:mantou
19.1 引言
在第九章中我们介绍了进行终端登录时,需要通过一个终端设备自动提供终端的语
义。在终端和运行程序之间有一个终端行规程(图11.2),通过这个规程我们能够
在终端上设置特殊字符(退格、行删除、中断等)。但是,当一个登录请求到达网
络连接时,终端行规程并不是自动被加载到网络连接和登录程序shell之间的。图
9.5显示了有一个伪终端设备驱动程序被用来提供终端语义。
除了用于网络登录,伪终端还被用在其他方面,我们将在本章中进行介绍。我们
将首先提供在SVR4和4.3+BSD系统下用于创建伪终端的函数,然后使用这些函数编
写一个程序用来调用pty。我们将看到这个程序的不同使用:在输入字符和终端显
示之间进行转换(BSD的码转换程序)和运行协同进程来避免我们在程序14.10中遇
到的缓冲区问题。
19.2 概述
伪终端这个名词暗示了与一个应用程序相比,它更加象一个终端。但事实上,伪终
端并不是一个真正的终端。图19.1显示了使用伪终端的进程的典型结构。其中关键
点如下:
图19.1 使用伪终端的典型进程结构
1 通常一个进程打开伪终端主设备然后调用fork。子进程建立了一个新的会话,打
开一个相应的伪终端从设备,将它复制成标准输入、标准输出和标准出错,然后调
用exec。伪终端从设备成为子进程的控制终端。
2 对于伪终端从设备之上的用户进程来说,其标准输入、标准输出和标准出错都能
当作终端设备使用。用户进程能够调用11章中讲到的所有输入/输出函数。但是因
为在伪终端从设备之下并没有真正的设备,无意义的函数调用(改变波特率、发送
中断符、设置奇偶校验等)将被忽略。
3 任何写到伪终端主设备的输入都会作为在从设备端的输入,反之亦然。事实上所
有从设备端的输入都来自于主设备上的用户进程。这看起来就象一个流管道(图1
5.3),但从设备上的终端行规程使我们拥有普通管道之外的其他处理能力。
图19.1显示了BSD系统中的伪终端结构。在19.3.2中我们将看到如何打开这些设备
。
在SVR4系统中伪终端是使用流系统来创建的(12.4节)。图19.2详细描述了SVR4系
统中各个伪终端模块之间的关系。在虚线框中的两个流模块是可选的。请注意在从
设备上的三个流模块同12.10网络登录程序的输出是一样的。在19.3.1小节中将介
绍如何组织这些流模块。
从现在开始将简化以上图示,首先我们不再画出图19.1的"读、写功能"或图19.2
的流首。我们还使用缩写"pty"表示伪终端,并将图19.2中所有伪终端从设备之上
的流模块集合表示为"终端行规程"模块。
图19.2 在SVR4下的伪终端组织结构
现在来看一下伪终端的几种典型用法。
网络登录服务器
伪终端用于构造网络登录服务器。典型的例子是telnetd和rlogind服务器。在St
evens[1990]的第15章中详细讨论了提供rlogin服务的步骤。一旦登录shell运行在
远端主机上,我们得到如图19.3的结构。同样的结构也用于telnetd服务器。
在rlogind服务器和登录shell之间有两个exec调用,这是因为login程序通常是在
两个exec之间检验用户是否合法的。
图19.3 rlogind服务器的进程组织结构
本图的一个关键点是驱动伪终端主设备的进程通常同时在读写另一个输入/输出流
。在本例中另一个输入/输出流是TCP/IP。这表示该进程必然使用了某种形式的如
select或poll那样的输入/输出多路转接(节12.5),或被分成两个进程。请回忆
我们在18.7节讨论过的一个进程和两个进程的的比较。
script程序
script程序是随SVR4和4.3+BSD提供的,该程序将终端会话期间所有的输入和输出
信息在一个文件中做一个拷贝。它通过将自己置于终端和登录shell的一个新的调
用之间来完成这个工作。图19.4详细描述了script程序相关的交互。这里我们特别
指出script程序通常是从登录shell启动的,该shell然后等待程序的结束。
当script程序在运行的时候,在伪终端从设备之上终端行规程的所有输出都被复制
到一个script文件中(通常叫做typescript)。因为我们的击键通常被行规程的模
块回显,该script文件也包括了输入的内容。但是,因为口令字不被回显,该scr
ipt文件不会包含口令字。
图19.4 script程序
本书中所有运行程序并显示其输出的实例都是由script程序实现的,这样避免了手
动拷贝程序输出可能带来的错误。
在19.5节开发一个通用的pty程序后,我们将看到一个巧妙的shell程序能够将它转
化成一个script程序。
expect程序
伪终端可以用来使交互式的程序运行在非交互的状态中。许多程序需要一个终端
来运行,18.7节中的call进程就是一个例子。它假定标准输入是一个终端并在启动
时将其设置为初始模式(18.20程序)。该程序不能从一个shell程序中被运行来自
动拨号到远程系统,登录,取出信息和登出。
同修改所有交互式程序来支持批处理模式的操作比较,一个更好的解决方法是提
供一种手段来通过一个script来驱动交互式程序。expect程序[Libes 1990;1991]
提供了这样的方法。类似于19.5节的pty程序,它使用伪终端来运行其他程序。但
是,expect还提供了一种编程语言用于检查程序的输出来确定以什么作为输入发送
给该程序。当一个交互式的程序开始从一个script运行时,我们不能仅仅是将scr
ipt中的所有内容输入到程序中去。相应的,我们要通过检查程序的输出来决定下
一步输入的内容。
运行协同进程
在14.10的程序例子中,我们不能调用使用标准输入/输出库进行输入、输出的协
同进程,这是因为当我们通过管道与协同进程进行通讯时,标准输入/输出库会将
标准输入、输出的内容放到缓冲区中,从而引起死锁。如果协同进程是一个已经编
译的程序而我们又没有源程序,我们就无法在源程序中加入fflush语句来解决这个
问题。图14.9显示了一个进程驱动协同进程的情况。我们需要做的是将一个伪终端
放到两个进程之间,如图19.5所示。
图19.5 用伪终端驱动一个协同进程
现在协同进程的标准输入和标准输出就象一个终端设备一样,所以标准输入/输出
库会将这两个流设置为行缓冲的。
父进程有两种不同的方法在自身和协同进程之间获得伪终端(这种情况下的父进程
可以象程序14.9,使用两个管道和协同进程进行通讯;或者象程序15.1那样,使用
一个流管道)。一个方法是父进程直接调用pty_fork函数(19.4节)而不是fork。
另一种方法是exec该pty程序,将协同进程作为参数(19.5节)。我们将在说明pt
y程序后介绍这两种方法。
观看长时间运行程序的输出
使用任一个标准shell,我们都可以将一个需要长时间运行的程序放到后台运行。
但是如果我们将该程序的标准输出重定向到一个文件,并且如果它产生的输出不多
,我们就不能方便地监控程序的进展,这是因为标准的输入/输出库会将标准输出
放在缓冲区中保存。我们看到的将只是成块的输出结果,有时甚至可能是8192字节
一块。
如果我们有源程序,我们可以加入fflush调用。另一种方法是,我们可以在pty程
序下运行该程序,让标准输入/输出库认为输出是终端。图19.6说明了这个结构,
我们将这个缓慢输出的程序称为slowout。从登录shell到pty进程的fort/exec箭头
用虚线表示,以强调pty进程是作为后台任务运行的。
19.3 打开伪终端设备
在SVR4和4.3+BSD系统中打开伪终端设备的方法有所不同。我们提供两个函数来处
理所有细节:ptym_open用来打开下一个有效的伪终端主设备,ptys_open用来打开
相应的从设备。
图19.6 使用伪终端运行一个缓慢输出的程序
#include "ourhdr.h"
int ptym_open(char *pts_name);
返回:如果操作成功,返回伪终端主设备文件
述符;否则返回-1
int ptys_open(int fdm, char *pts_name);
返回:如果操作成功,返回伪终端从设备文件
述符;否则返回-1
通常我们不直接调用这两个函数--函数pty_fork(19.4节)调用它们并fork出一个
子进程。
ptym_open决定下一个有效的伪终端主设备并打开该设备。这个调用必须分配一个
数组来存放主设备或从设备的名称,并且如果调用成功,相应的主设备或从设备的
名称会通过pts_name返回。这个名称和ptym_open返回的文件描述符将传给ptys_o
pen,该函数用来打开一个从设备。
在我们讲解pty_fork函数之后,使用两个函数来打开这两个设备的原因将会很明显
。通常,一个进程调用ptym_open来打开一个主设备并且得到从设备的名称。该进
程然后fork子进程,子进程在调用setid建立新的会话后调用ptys_open来打开从设
备。这就是从设备如何成为子进程的控制终端的过程。
19.1.1 系统V的版本4
所有在SVR4系统下的伪终端的流实现细节在AT&T[1990d]的第十二章中有所说明。
三个函数在下列手册页中描述:grantpt(3),unlockpt(3),和ptsname(3)。
伪终端主设备是/dev/ptmx。这是一个流的增殖设备。这意味着当我们打开该增殖
设备,其open例程自动决定第一个未被使用的伪终端主设备并打开这个设备。(在
下一节我们将看到在Berkeley系统中,我们必须自己找到第一个未被使用的伪终端
主设备。)
_______________________________________________________________________
________
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stropts.h>
#include "ourhdr.h"
extern char *ptsname(int); /* prototype not in any system header */
int
ptym_open(char *pts_name)
{
char *ptr;
int fdm;
strcpy(pts_name, "/dev/ptmx"); /* in case open fails */
if ( (fdm = open(pts_name, O_RDWR)) < 0)
return(-1);
if (grantpt(fdm) < 0) { /* grant access to slave */
close(fdm);
return(-2);
}
if (unlockpt(fdm) < 0) { /* clear slave's lock flag */
close(fdm);
return(-3);
}
if ( (ptr = ptsname(fdm)) == NULL) { /* get slave's name */
close(fdm);
return(-4);
}
strcpy(pts_name, ptr); /* return name of slave */
return(fdm); /* return fd of master */
}
int
ptys_open(int fdm, char *pts_name)
{
int fds;
/* following should allocate controlling terminal */
if ( (fds = open(pts_name, O_RDWR)) < 0) {
close(fdm);
return(-5);
}
if (ioctl(fds, I_PUSH, "ptem") < 0) {
close(fdm);
close(fds);
return(-6);
}
if (ioctl(fds, I_PUSH, "ldterm") < 0) {
close(fdm);
close(fds);
return(-7);
}
if (ioctl(fds, I_PUSH, "ttcompat") < 0) {
close(fdm);
close(fds);
return(-8);
}
return(fds);
}
_______________________________________________________________________
________
程序19.1 SVR4的伪终端打开函数
我们首先打开设备/dev/ptmx并得到伪终端主设备的文件描述符。打开这个主设备
自动锁定了对应的从设备。
我们然后调用grantpt来改变从设备的权限。执行如下操作:(a)将从设备的所
有权改为有效用户ID;(b)将组所有权改为组tty;(c)将权限改为只允许user
-read,user-write和group-write。将组所有权设置为tty并允许group-write权限
是因为程序wall(1)和write(1)的组标识符被设置为组tty。调用函数grantpt执行
/user/lib/pt_chmod。该程序的用户被设置为root因此它能够修改从设备的所有者
和权限。
函数unlockpt用来清除从设备的内部锁。在打开从设备前我们必须做这件事情。
并且我们必须调用ptsname来得到从设备的名称。这个名称的格式是/dev/pts/NNN
。
文件中接下来的函数是ptys_open,该函数真正被用来打开一个从设备。在SVR4系
统中,如果调用者是一个还没有控制终端的会话,open就会分配一个从设备作为控
制终端。如果我们不希望函数自动做这件事,可以在调用时指明O_NOCTTY标志。
在打开从设备后,我们将三个流模块放在从设备的流上。Ptem是"伪终端"虚拟模
块,ldterm是终端行规程模块。这两个模块合在一起象一个真正的终端模块一样工
作。ttcompat提供了向老系统如V7、4BSD和Xenix的ioctl调用的兼容性。这是一个
可选的模块,但是因为它自动尝试控制台登录和网络登录(见程序12.10的输出)
,我们将其加到从设备的流中。
调用这两个函数的结果是得到:伪终端主设备的文件描述符和从设备的文件描述
符。
19.3.2 4.3+BSD
在4.3+BSD系统中我们必须自己来确定第一个可用的伪终端主设备。为达到这个目
的,我们从/dev/ptyp0开始并不断尝试直到成功打开一个可用的伪终端主设备或试
完所有设备。在打开设备的时候,我们会看到两种可能的错误:EIO指设备已经被
使用;ENOENT表示设备不存在。在后一种情况,我们可以停止搜索,因为所有的伪
终端设备都在被使用中。一旦我们成功打开一个例如名为/dev/ptyMN的伪终端主设
备,那么对应的从设备的名称为/dev/ttyMN。
程序19.2中的函数ptys_open打开该从设备。我们在该函数中调用chown和chmod,
必须意识到调用这两个函数的进程必须有超级用户的权限。如果必须改变权限标志
,那么这两个函数必须放在一个set_user_ID的root用户的可执行程序中,这类似
于4.3+BSD系统下的grantpt函数。
在4.3+BSD系统之下打开pty从设备不具有象分配作为控制终端的设备那样的副作用
。我们将在下一节探讨如何在4.3+BSD系统下分配控制终端。
这个函数尝试16个不同的伪终端主设备:从/dev/ptyp0到/dev/ptyTf。具体有效的
pty设备号取决于两个因素:(a)在内核中配置的号码;(b)在/dev目录下的特
殊文件号。对于任何程序来说,有效的号码是(a)和(b)中较小的一个。并且,
即使(a)和(b)中小的值大于64,许多现有的BSD应用(telnetd,rlogind,等
等)会搜索程序19.2中第一个for循环中的"pqrs"。
_______________________________________________________________________
________
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include "ourhdr.h"
使用;ENOENT表示设备不存在。在后一种情况,我们可以停止搜索,因为所有的伪
终端设备都在被使用中。一旦我们成功打开一个例如名为/dev/ptyMN的伪终端主设
备,那么对应的从设备的名称为/dev/ttyMN。
程序19.2中的函数ptys_open打开该从设备。我们在该函数中调用chown和chmod,
必须意识到调用这两个函数的进程必须有超级用户的权限。如果必须改变权限标志
,那么这两个函数必须放在一个set_user_ID的root用户的可执行程序中,这类似
于4.3+BSD系统下的grantpt函数。
在4.3+BSD系统之下打开pty从设备不具有象分配作为控制终端的设备那样的副作用
。我们将在下一节探讨如何在4.3+BSD系统下分配控制终端。
这个函数尝试16个不同的伪终端主设备:从/dev/ptyp0到/dev/ptyTf。具体有效的
pty设备号取决于两个因素:(a)在内核中配置的号码;(b)在/dev目录下的特
殊文件号。对于任何程序来说,有效的号码是(a)和(b)中较小的一个。并且,
即使(a)和(b)中小的值大于64,许多现有的BSD应用(telnetd,rlogind,等
等)会搜索程序19.2中第一个for循环中的"pqrs"。
_______________________________________________________________________
________
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include "ourhdr.h"
int
ptym_open(char *pts_name)
{
int fdm;
char *ptr1, *ptr2;
strcpy(pts_name, "/dev/ptyXY");
/* array index: 0123456789 (for references in following code) */
for (ptr1 = "pqrstuvwxyzPQRST"; *ptr1 != 0; ptr1++) {
pts_name[8] = *ptr1;
for (ptr2 = "0123456789abcdef"; *ptr2 != 0; ptr2++) {
pts_name[9] = *ptr2;
/* try to open master */
if ( (fdm = open(pts_name, O_RDWR)) < 0) {
if (errno == ENOENT) /* different from EIO */
return(-1); /* out o
pty devices */
else
continue; /* try n
xt pty device */
}
pts_name[5] = 't'; /* change "pty" to "tty" */
return(fdm); /* got it, return fd of master *
}
}
return(-1); /* out of pty devices */
}
int
ptys_open(int fdm, char *pts_name)
{
struct group *grptr;
int gid, fds;
if ( (grptr = getgrnam("tty")) != NULL)
gid = grptr->gr_gid;
else
gid = -1; /* group tty is not in the group file */
/* following two functions don't work unless we're root
/
chown(pts_name, getuid(), gid);
chmod(pts_name, S_IRUSR | S_IWUSR | S_IWGRP);
if ( (fds = open(pts_name, O_RDWR)) < 0) {
close(fdm);
return(-1);
}
return(fds);
}
_______________________________________________________________________
________
程序19.2 4.3+BSD系统下的伪终端open函数
19.4 pty_fork函数
现在我们使用上一节中的两个函数:ptym_open和ptys_open,编写我们称之为pty
_fork的函数。这个新函数具有了如下功能:打开主设备和从设备,建立作为会话
管理者的子进程并使其具有控制终端。
#include <sys/types.h>
#include <termios.h>
#include <sys/ioctl.h> /* 4.3+BSD系统中,在这里定义了winsize结构 */
#include <ourhdr.h>
pid_t pty_fork(int *ptrfdm, char *slave_name,
const struct termios *slave_termios,
const struct winsize *slave_winsize);
返回:在子进程返回0;在父进程返回子进程的进程ID;遇到错误返回-1
Pty主设备的文件描述符通过ptrfdm指针返回。
如果slave_name不为空,从设备的名称被存放在该指针指向的存储区中。调用者必
须为该存储区分配空间。
如果指针slave_termios不为空,该指针所引用的结构将从设备的终端行规程初始
化。如果该指针为空,系统将从设备的termios结构初始化为一个由具体应用定义
的初始状态。类似的,如果slave_winsize指针不为空,该指针所引用的结构将从
设备的窗口大小初始化。如果该指针为空,winsize结构通常被初始化为0。
程序19.3显示了这个程序的代码。调用相应的ptym_open和ptys_open函数,这个函
数在SVR4和4.3+BSD系统下都可以使用。
在打开伪终端主设备后,fork将被调用。正如前面提到的,我们要等到调用setid
建立新的会话后才调用ptys_open。当调用setid时,子进程还不是一个进程组的l
eader(想一想为什么?)因此第9.5节列出的三个操作被使用:(a)子进程作为
对话的管理者创建一个新的对话;(b)子进程创建一个新的进程组;(c)子进程
没有控制终端。在SVR4系统中,当调用ptys_open时,从设备成为了控制终端。在
4.3+BSD系统中,我们必须调用ioctl并使用参数TIOCSCTTY来分配一个控制终端。
然后termios和winsize这两个结构在子进程中被初始化。最后从设备的文件描述符
被复制到子进程的标准输入、标准输出和标准出错中。这表示由子进程所exec的进
程都会将上述三个句柄同伪终端从设备联系起来。
在调用fork后,父进程返回伪终端主设备的描述符并返回。在下一节我们将在pty
程序中使用pty_fork。
_______________________________________________________________________
________
#include <sys/types.h>
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h> /* 44BSD requires this too */
#endif
#include "ourhdr.h"
pid_t
pty_fork(int *ptrfdm, char *slave_name,
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)) < 0)
err_sys("can't open master pty: %s", pts_name);
if (slave_name != NULL)
strcpy(slave_name, pts_name); /* return name of slave */
if ( (pid = fork()) < 0)
return(-1);
else if (pid == 0) { /* child */
if (setsid() < 0)
err_sys("setsid error");
/* SVR4 acquires controlling terminal on
open() */
if ( (fds = ptys_open(fdm, pts_name)) < 0)
err_sys("can't open slave pty");
err_sys("dup2 error to stderr");
if (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 */
}
}
_______________________________________________________________________
________
程序19.3 pty_fork函数
19.5 pty程序
写pty程序的目的是为了用键入
pty prog arg1 arg2
来代替
prog arg1 arg2
这样使我们可以用pty来执行另一个程序,该程序在一个自己的会话中执行,并和
一个伪终端连接。
让我们看以下pty程序的源代码。程序19.4包含main函数。它调用上一节的pty_f
ork函数。
_______________________________________________________________________
________
#include <sys/types.h>
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h> /* 44BSD requires this too */
#endif
#include "ourhdr.h"
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,
erbose;
pid_t pid;
char *driver, 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, "d:einv")) != EOF) {
switch (c) {
case 'd': /* driver for stdin/stdout */
driver = optarg;
break;
case 'e': /* noecho for slave pty's line disciplin
*/
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, &orig_termios, &size);
} else
pid = pty_fork(&fdm, 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) {
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, &orig_termios, &size);
} else
pid = pty_fork(&fdm, 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) {
stermios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
stermios.c_oflag &= ~(ONLCR);
/* also turn off NL to CR/NL mapping on output */
if (tcsetattr(fd, TCSANOW, &stermios) < 0)
err_sys("tcsetattr error");
}
_______________________________________________________________________
________
程序19.4 pty程序的main函数
在下一节我们检测pty程序的不同使用时,将会探讨不同的行命令选项。
在调用pty_fork前,我们取得了termios和winsize结构的值,将其传递给pty_fo
rk。通过这种方法,伪终端从设备具有和现在的终端相同的初始状态。
从pty_fork返回后,子进程关闭了伪终端从设备的回显,并调用execvp来执行命
令行指定的程序。所有的命令行选项将成为程序的选项。
父进程在调用exit时执行原先设置的exit处理程序,它复原终端状态,将用户终
端设置为初始模式(可选)。我们将在下一节讨论do_driver函数。
接下来父进程调用函数loop(程序19.5)。该函数仅仅是将所有标准输入拷贝到
伪终端主设备,并将伪终端主设备收到的所有内容拷贝到标准输出。同18.7节一样
,我们有两个选择--一个进程还是两个?为了有所区别,我们在这里使用两个进程
,尽管使用select或poll的单进程也是可行的。
_______________________________________________________________________
________
#include <sys/types.h>
#include <signal.h>
#include "ourhdr.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 buff[BUFFSIZE];
if ( (child = fork()) < 0) {
err_sys("fork error");
} else if (child == 0) { /* child copies stdin to ptym */
for ( ; ; ) {
if ( (nread = read(STDIN_FILENO, buff, BUFFSIZE)) < 0)
err_sys("read error from stdin");
else if (nread == 0)
break; /* EOF on stdin means we're done
*/
if (writen(ptym, buff, nread) != nread)
err_sys("writen error to master pty");
}
/* We always terminate when we encounter an EOF on stdin
but we only notify the parent if ignoreeof is 0. */
if (ignoreeof == 0)
kill(getppid(), 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, buff, BUFFSIZE)) <= 0)
break; /* signal caught, error, or EOF */
if (writen(STDOUT_FILENO, buff, 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);
return; /* parent returns to caller */
}
/* The child sends us a SIGTERM when it receives an EOF on
* the pty slave or encounters a read() error. */
static void
sig_term(int signo)
{
sigcaught = 1; /* just set flag and return */
return; /* probably interrupts read() of ptym */
}
_______________________________________________________________________
________
程序19.5 loop函数
注意,当使用两个进程时,如果一个终止,那么它必须通知另一个。我们用SIGT
ERM进行这种通知。
19.6 使用pty程序
接下来我们看一下pty程序的不同例子,了解一下使用不同命令行选项的必要性。
如果使用KornShell,我们执行:
pty ksh
得到一个运行在一个伪终端下的新的shell。
如果文件ttyname同我们在程序11.7看到的程序相同,我们可按如下方式执行pty
程序:
$who
stevens console Feb 6 10:43
stevens ttyp0 Feb 6 15:00
stevens ttyp1 Feb 6 15:00
stevens ttyp2 Feb 6 15:00
stevens ttyp3 Feb 6 &n