分类: C/C++
2008-03-10 07:23:41
19.6 使用p t y程序
接下来看一下p t y程序的不同例子,了解一下使用不同命令行选择项的必要性。
如果使用K o r n S h e l l,我们执行:
pty ksh
得到一个运行在一个伪终端下的新的s h e l l。
如果文件t t y n a m e同程序11 - 7相同,可按如下方式执行p t y程序:
$ w h o
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 15:48
stevens ttyp4 Feb 7 14:28
t t y p4是 正 在 使 用 的 最 高 终 端 设 备
$ pty ttyname
在 p t y上 运 行 程 序 11 - 7
fd 0: /dev/ttyp5 t t y p
5是 下 一 个 有 效 的 p t y设 备 号
fd 1: /dev/ttyp5
fd 2: /dev/ttyp5
6 . 7节讨论了记录当前U N I X系统登录用户的u t m p文件。那么在伪终端上运行程序的用户是否被认为登录了呢?如果是远程登录, t e l n e t d和r l o g i n d,显然伪终端上的用户应该在u t m p中拥有相应条目。但是,从窗口系统或运行s c r i p t程序,在伪终端上运行s h e l l的用户是否应该在u t m p中拥有相应条目呢?这个问题一直没有一个统一的认识。有的系统有记录,有的没有。如果没有记录的话, w h o ( 1 )程序一般不会显示正在被使用的伪终端。
除非u t m p允许其他用户的写许可权,否则一般的程序将不能对其进行写操作。某些系统提
供这个写许可权。
当在p t y上运行作业控制s h e l l时,它能够正常地运行。例如,
pty ksh
在p t y上运行K o r n S h e l l。我们能够在这个新s h e l l下运行程序和使用作业控制,如同在登录s h e l l中一样。但如果在p t y下运行一个交互式程序而不是作业控制s h e l l,比如:
pty cat
一切正常直到键入作业控制的暂停字符。在S V R 4和4 . 3 + B S D系统中作业控制暂停字符将会被显示为ˆ Z而被忽略。在S u n O S
为了明白其中的原因,我们需要检查所有相关的进程、这些进程所属的进程组和对话期。图1 9 - 7显示了pty cat运行的结构图。
当键入暂停字符( C t r l - Z),它将被c a t进程下的行规程模块所识别,这是因为p t y将终端(在p t y父进程之下)设置为初始模式。但内核不会终止c a t进程,这是因为它属于一个孤儿进程组(见9 . 1 0节)。c a t的父进程是p t y的父进程,属于另一个对话期。
不同的系统处理这种情况的方法也不同。在P O S I X . 1中这个S I G T S T P信号不被发送给进程。早期的伯克利系统递送一个进程甚至不能捕获的S I G K I L L。这就是我们在S u n O S
在S V R 4和4 . 3 + B S D系统中,我们修改了程序1 0 - 2 2来观察结果。修改后的程序能够在捕获S I G T S T P后进行打印,并在捕获S I G C O N T信号后再打印一次并继续执行。这说明S I G T S T P被进程捕获,但是当进程试图发送信号给自己来暂停本进程的时候,内核立即发送S I G C O N T信号使之继续执行。内核将不会让进程被作业控制停止。S V R 4和4 . 3 + B S D系统的这种处理方法比起发送一个S I G K I L L的方法来显得不那么激烈。
当使用p t y来运行作业控制s h e l l时,被这个新s h e l l调用的作业将不是任何孤儿进程组的成员,这是因为作业控制s h e l l总是属于同一个对话期。在这种情况下, C t r l - Z被发送到被s h e l l调用的进程,而不是s h e l l本身。
让被p t y调用的进程能够处理作业控制信号的唯一的方法是:另外增加一个命令行标志让
p t y子进程能够自己认识作业暂停字符,而不是让该字符通过其他行规程模块。
另一个使用p t y进行作业控制交互的例子见图1 9 - 6。如果运行一个程序:
pty slowout > file.out &
当子进程试图从标准输入(终端)读入数据时, p t y进程立刻停止运行。这是因为该作业是一个后台作业并且当它试图访问终端时会使作业控制停止。如果将标准输入重定向使得p t y不从终端读取数据,如:
pty slowout < /dev/null > file.out &
那么p t y程序立即终止,这是因为它从标准输入读取到一个文件结束符。解决这个问题的方法
是使用- i选择项。这个选择项的含义是忽略来自标准输入的文件结束符:
pty -i slowout < /dev/null > file.out &
这个标志导致在遇到文件结束符时,程序1 9 - 5的子进程终止,但子进程不会使父进程也终止。相反的,父进程一直将伪终端从设备的输出拷贝到标准输出(本例中的f i l e . o u t)。
使用p t y程序,可以用下面的方式实现B S D系统中的s c r i p t(1)程序。
#!/bin/sh
pty "${SHELL:-/bin/sh}" | tee typescript
一旦执行这个s c r i p t程序,即可以运行p s来观察进程之间的关系。图1 9 - 8显示了这些关系。
在这个例子中,假设S H E L L变量是K o r n S h e l l(可能是/ b i n / k s h)。如前面所述,s c r i p t仅仅是将新的s h e l l(和它调用的所有的子进程)的输出拷贝出来,但是因为伪终端从设备上的行规程模块通常允许回显,故绝大多数键入都被写到t y p e s c r i p t文件中去。
在程序1 4 - 9中,我们不能让协同进程使用标准I / O函数,其原因是标准输入和输出不是终
端,其输入和输出将被放到缓存中。如果用
if (execl("./pty", "pty", "-e", "add2", (char *) 0) < 0)
替代:
if (execl("./add2", "add2", (char *) 0) < 0)
在p t y下运行协同进程,该程序即使使用了标准I / O仍然可以正确运行。图1 9 - 9显示了在使用伪终端作为协同进程的输入和输出的情况。框中的“驱动程序”是前面提到过的改变了e x e c l的程序1 4 - 9。这是图1 9 - 5的一个扩充,它显示了所有的进程间联系和数据流。
这个例子显示了对于p t y程序- e(不回显)选择项的重要性。p t y不以交互方式运行,这是
因为它的标准输入不是一个终端。在程序1 9 - 4中i n t e r a c t i v e标志默认为f a l s e,这是因为对i s a t t y调用的返回结果是f a l s e。这意味着在真正的终端之上的行规程保持在典型模式下并允许回显。
指定- e选择项后,关掉了伪终端从设备上的行规程模块的回显。如果不这样做,则键入的每一个字符都将被两个行规程模块显示两次。我们还要用- e选择项关闭t e r m i o s结构的O N L C R标志,防止所有的协同进程的输出被回车和换行符终止。 在不同的系统上测试这个例子会遇到1 2 . 8节描述r e a d n和w r i t e n函数时提到的问题。当描述符不是引用普通的磁盘文件时,从r e a d返回的读取数据量可能因实现不同而有所区别。协同进程使用p t y时,如果调用通过管道的r e a d而返回结果不到一行,将输出不可预测的结果。解决的方法不是使用程序1 4 - 9而是使用修改过的使用标准I / O库的习题1 4 . 5的程序,将两个管道都设置为行缓存。这样f g e t s函数将会读完一个整行。程序1 4 - 9的w h i l e循环假设送到协同进程的每一行都会带来一行的返回结果。
虽然让p t y运行所有的协同进程是非常诱人的想法,但如果协同进程是交互式的,就不能正常工作。问题在于p t y只是将其标准输入复制到p t y,并将来自p t y的复制到其标准输出。而并不关心具体得到什么数据。
举个例子,我们可以在p t y直接与调制解调器对话之下运行1 8 . 7节的c a l l客户机:
pty call t2500
这样做不比直接键入call t2500有什么优点,但我们可能希望用一个s h e l l命令运行c a l l程序来取得调制解调器的一些内部寄存器的内容。如果t 2 5 0 0 . c m d包括两行:
a a t n ?
˜ .
第一行打印出调制解调器的寄存器值,第二行终止c a l l程序。但是如果运行:
pty -1 < t2500.cmd call t2500
结果就不是我们希望得到的。事实上文件t 2 5 0 0 . c m d的内容首先被送到了调制解调器。当交互运行c a l l程序时我们等待调制解调器显示“ C o n n e c t e d”,但是p t y程序不知道这样做。这就是为什么需要比p t y更巧妙的程序,如e x p e c t,自脚本文件运行交互式程序。
在p t y上即使运行程序1 4 - 9也不能正常工作,这是因为程序1 4 - 9认为它在一个管道写入的每一行都会在另一个管道产生一行。而且, 1 4 - 9程序总是先发送一行到系统进程,然后再读取一行。在上面的例子中,我们需要先收到行“ C o n n e c t e d”,然后再发送数据。这里有一些使用s h e l l命令驱动交互式程序的方法。可以在p t y上增加一种命令语言和一个解释器。但是一个适当的命令语言可能十倍于p t y程序的大小。另一种方法是使用命令语言并用p t y _ f o r k函数来调用交互式程序,这正是e x p e c t程序所做的。
我们将采用一种不同的方法,使用选择项- d让p t y程序同一个管理输入和输出的驱动进程连接起来。该驱动进程的标准输出是p t y的标准输入,反之亦然。这有点像协同进程,只是在p t y的“另一边“。此种进程结构与图1 9 - 9中所示的几乎相同,但是在这种情况下由p t y来完成驱动进程的f o r k和e x e c。而且我们将在p t y和驱动进程之间使用一个单独的流管道,而不是两个半双工管道。
程序1 9 - 6是d o _ d r i v e r函数的源码,该函数被p t y(见程序1 9 - 4)的m a i n函数在使用- d选项时调用。
程序19-6 pty程序的d o _ d r i v e r函数
#include
#include
#include "ourhdr.h"
void do_driver( char *driver)
{
pid_t child;
int pipe(2);
if (s_pipe(pipe) < 0)
err_sys("can't create strea pipe");
if ((child = fork ()) < 0)
err_sys("fork error");
else if (child == 0){
close (pipe[1]);
if (dup2(pipe[0],STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
if (dup2(pipe[0], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close (pipe[0]);
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");
close (pipe[1]);
}
通过编写自己的驱动程序的方法,可以随意地驱动交互式程序。即使驱动程序有和p t y连接在一起的标准输入和标准输出,它仍然可以通过/ d e v / t t y同用户交互。这个解决方法仍不如e x p e c t程序通用,但是它提供了一种不到5 0行代码的选择方案。
19.7 其他特性
伪终端还有其他特性,我们在这里简略提一下。AT & T [ 1 9 9 0 d ]和4 . 3 + B S D系统的操作手册有更详细的内容。
打包模式能够使伪终端主设备了解到伪终端从设备的状态变化。在S V R 4系统中可以将流
模块p c k t压入主设备端来设置这种模式。图1 9 - 2显示了这种可选模式。在4 . 3 + B S D系统中可以通过T I O C P K T的i o c t l来设置这种模式。
S V R 4和4 . 3 + B S D系统中具体的打包模式有所不同。在S V R 4系统中,读取伪终端主设备的进程必须调用g e t m s g从流中取得数据,这是因为p c k t模块将一些事件转化为无数据的流消息。
在4 . 3 + B S D系统中每一次从伪终端主设备的读操作都会在可选数据之后返回状态字节。
无论实现的方法是什么样的,打包模式的目的是,当伪终端从设备之上的行规程模块出现以下事件时,通知进程从伪终端主设备读取数据:读入队列被刷新;写出队列被刷新;输出被停止(如:C t r l - S);输出重新开始; X O N / X O F F流开关被关闭后重新打开; X O N / X O F F流开
关被打开后重新关闭。这些事件被r l o g i n客户机和r l o g i d服务器等使用。
伪终端主设备可以用T I O C R E M O T E的i o c t l将伪终端从设备设置成远程模式。虽然S V R 4和4 . 3 + B S D系统使用同样的命令来打开或关闭这个特性,但是在S V R 4系统中i o c t l的第三个参数是一个整型数,而4 . 3 + B S D中是一个指向整型数的指针。
当伪终端主设备将伪终端从设备设置成这种模式时,它通知伪终端从设备之上的行规程模
块对从主设备收到的任何数据都不要进行处理,无论它是不是从设备的t e r m i o s结构的规范或非规范标志。远程模式适用于窗口管理器这种进行自己的行编辑的应用模式。
伪终端主设备上的进程可以用T I O C S W I N S Z的i o c t l来设置从设备的窗口大小。如果新的大小和老的不同,一个S I G W I N C H信号将被发送到伪终端从设备的前台进程组。
读写伪终端主设备的进程可以向伪终端从设备的进程组发送信号。在S V R 4系统中,可以
通过T I O C S I G N A L的i o c t l完成这个功能,第三个参数就是信号的数值。在4 . 3 + B S D中通过T I O C S I G的i o c t l来完成,第三个参数就是指向信号编号值的指针。
19.8 小结
本章首先说明了在S V R 4和4 . 3 + B S D系统中打开伪终端的代码。然后用此代码提供了用于多种不同应用的通用的pty_fork函数。这个函数是小程序(pty)的基础。并且讨论了许多伪终
端的属性。
伪终端在大多数U N I X系统中每天都被用来进行网络登录。我们检查了伪终端的许多其他用途,从s c r i p t程序到使用批处理脚本来驱动交互式程序。