📄 19.htm
字号:
<p>符: </p>
<p>pty -i slowout < /dev/null > file.out & </p>
<p>这个标志导致在遇到文件结束符时,程序19.5的子进程终止,但子进程不会</p>
<p>使父进 </p>
<p>程也终止。相反的,父进程一直将伪终端从设备的输出拷贝到标准输出(本例</p>
<p>中的 </p>
<p>file.out)。 </p>
<p>script程序 </p>
<p>使用pty程序,我们可以用下面的方式实现BSD系统中的script(1)程序。
</p>
<p>#!/bin/sh </p>
<p>pty "${SHELL:-/bin/sh}" | tee typescript </p>
<p>一旦执行这个script程序,我们可以运行ps来观察进程之间的关系。图19.8</p>
<p>显示了 </p>
<p>这些关系。 </p>
<p>图19.8 用shell程序实现的script的有关进程 </p>
<p>在这个例子中,我们假设SHELL变量是KornShell(可能是/dev/ksh)。正</p>
<p>如我们前 </p>
<p>面提到的,script仅仅是将新的shell(和所有的子进程)的输出拷贝出</p>
<p>来,但是 </p>
<p>因为伪终端从设备上的行规程模块通常允许回显,绝大多数我们的键入都被写</p>
<p>到t </p>
<p>ypescript文件中去。 </p>
<p>运行协同进程 </p>
<p>在程序14.9中,我们不能让协同进程使用标准输入/输出函数,其原因是标准</p>
<p>输入 </p>
<p>和输出不是终端,他们的输入和输出将被放到缓冲区中。如果我们用
</p>
<p>if (execl("./pty", "pty", "-e", "add2", (char
*) 0) < 0) </p>
<p>替代 </p>
<p>if (execl("./add2", "add2", (char *) 0) < 0) </p>
<p>在pty下运行协同进程,该程序即使使用了标准输入/输出仍然可以正确运</p>
<p>行。 </p>
<p>图19.9显示了在使用伪终端作为协同进程的输入/输出的情况下,进程运行的</p>
<p>相互 </p>
<p>结构。框中的"driving program"是前面提到过的改变了execl的程序</p>
<p>14.9。这是图 </p>
<p>19.5的一个扩充,它显示了所有的进程间联系和数据流。 </p>
<p>图19.9 运行一协同进程,以pty作为其输入和输出 </p>
<p>这个例子显示了pty程序的-e(不回显)开关的重要性。pty不是以交互方式</p>
<p>运行 </p>
<p>的,这是因为它的标准输入不是一个终端。在程序19.4中interactive标志</p>
<p>缺省是 </p>
<p>false,这是因为对isatty调用的返回结果是false。这意味着在真正的终端</p>
<p>之上的 </p>
<p>行规程保持在典型模式下并允许回显。指定-e选项后,我们关掉了在伪终端</p>
<p>从设备 </p>
<p>的上的行规程模块的回显。如果我们不这样做,我们键入的每一个字符都将被</p>
<p>两个 </p>
<p>行规程模块显示两次。 </p>
<p>我们还要用-e选项关闭termios结构的ONLCR标志,防止所有的协同进程的输</p>
<p>出被回 </p>
<p>车和换行符终止。 </p>
<p>在不同的系统上测试这个例子会遇到我们在12.8节中描述readn和writen函</p>
<p>数时提 </p>
<p>到的问题。当描述符不是引用普通的磁盘文件时,从read返回的读取数据量</p>
<p>可能因 </p>
<p>实现不同而有所区别。协同进程使用pty时,如果调用通过管道的read而返回</p>
<p>结果 </p>
<p>不到一行,将输出不可预测的结果。解决的方法不是使用程序14.9而是修改</p>
<p>过的使 </p>
<p>用标准输入/输出库的练习14.5的程序,将两个管道都设置为行缓冲的。这样</p>
<p>fget </p>
<p>s函数将会读完一个整行为止。程序14.9的while循环假设送到协同进程的每</p>
<p>一行都 </p>
<p>会带来一行的返回结果。 </p>
<p>用非交互模式驱动交互式的程序 </p>
<p>虽然让pty运行所有的协同进程是非常诱人的想法,但如果协同进程是交互式</p>
<p>的, </p>
<p>就不能正常工作。问题在于pty只是将其标准输入复制到pty,并将来自pty</p>
<p>的复制 </p>
<p>到其标准输出。而并不关心具体得到什么数据。 </p>
<p>举个例子,我们可以在pty运行18.7节的call客户端直接控制modem: </p>
<p>pty call t2500 </p>
<p>这样做不比直接键入call t2500有什么优点,但我们可能希望从一个script</p>
<p>运行c </p>
<p>all程序来取得modem的一些内部寄存器的内容。如果t2500.cmd包括两行:</p>
<p>aatn? </p>
<p>~. </p>
<p>第一行打印出modem的寄存器值,第二行终止call程序。但是如果我们运</p>
<p>行: </p>
<p>pty -I < t2500.cmd call t2500 </p>
<p>结果就不是我们希望得到的。事实上文件t2500.cmd的内容首先被送到了</p>
<p>modem。当 </p>
<p>我们交互运行call程序时我们等待modem的"connected",但是pty程序不知</p>
<p>道这样 </p>
<p>做。这就是为什么需要比pty更巧妙的程序,如expect,来从script文件运</p>
<p>行交互 </p>
<p>式程序。 </p>
<p>在pty上即使运行程序14.9也不能正常工作,这是因为程序14.9认为它在一</p>
<p>个管道 </p>
<p>写入的每一行都会在另一个管道产生一行。而且,14.9程序总是先发送一行</p>
<p>到系统 </p>
<p>进程,然后再读取一行。在上面的例子中,我们需要先收到行"已连接",然</p>
<p>后再发 </p>
<p>送数据。 </p>
<p>这里有一些通过script驱动交互式程序的方法。我们可以在pty上增加一种</p>
<p>命令语 </p>
<p>言和一个解释器。但是一个适当的命令语言可能十倍于pty程序的大小。另一</p>
<p>种方 </p>
<p>法是使用命令语言并用pty_fork函数来调用交互式程序,这正是expect程序</p>
<p>所做的 </p>
<p>。 </p>
<p>我们将采用一种不同的方法,使用选项-d让pty程序同一个管理输入和输出的</p>
<p>驱动 </p>
<p>进程连接起来。该驱动进程的标准输出是pty的标准输入,反之亦然。这有点</p>
<p>象协 </p>
<p>同进程,只是在pty的"另一边"。此种进程结构与图19.9中所示的几乎相</p>
<p>同,但是 </p>
<p>在这种情况下由pty来完成驱动进程的fork和exec。而且我们将在pty和驱动</p>
<p>进程之 </p>
<p>间使用一个单独的管道,而不是两个半双工管道。 </p>
<p>程序19.6是do_driver函数的源代码,该函数被19.4节pty程序的main函数</p>
<p>在使用 </p>
<p>-d选项时调用。 </p>
<p>_______________________________________________________________________</p>
<p>________ </p>
<p>#include <sys/types.h> </p>
<p>#include <signal.h> </p>
<p>#include "ourhdr.h" </p>
<p>void </p>
<p>do_driver(char *driver) </p>
<p>{ </p>
<p>pid_t child; </p>
<p>int pipe[2]; </p>
<p>/* create a stream pipe to communicate with the driver */ </p>
<p>if (s_pipe(pipe) < 0) </p>
<p>err_sys("can't create stream pipe"); </p>
<p>if ( (child = fork()) < 0) </p>
<p>err_sys("fork error"); </p>
<p>else if (child == 0) { /* child */ </p>
<p>close(pipe[1]); </p>
<p>/* stdin for driver */ </p>
<p>if (dup2(pipe[0], STDIN_FILENO) != STDIN_FILENO) </p>
<p>err_sys("dup2 error to stdin"); </p>
<p>/* stdout for driver */ </p>
<p>if (dup2(pipe[0], STDOUT_FILENO) != STDOUT_FILENO) </p>
<p>err_sys("dup2 error to stdout"); </p>
<p>close(pipe[0]); </p>
<p>/* leave stderr for driver alone */ </p>
<p>execlp(driver, driver, (char *) 0); </p>
<p>err_sys("execlp error for: %s", driver); </p>
<p>} </p>
<p>close(pipe[0]); /* parent */ </p>
<p>if (dup2(pipe[1], STDIN_FILENO) != STDIN_FILENO) </p>
<p>err_sys("dup2 error to stdin"); </p>
<p>if (dup2(pipe[1], STDOUT_FILENO) != STDOUT_FILENO) </p>
<p>err_sys("dup2 error to stdout"); </p>
<p>close(pipe[1]); </p>
<p>/* Parent returns, but with stdin and stdout connected </p>
<p>to the driver. */ </p>
<p>} </p>
<p>_______________________________________________________________________</p>
<p>________ </p>
<p>程序19.6 pty程序的do_driver函数 </p>
<p>通过写自己的驱动程序的方法,我们可以随意地驱动交互式程序。即使驱动程</p>
<p>序有 </p>
<p>和pty连接在一起的标准输入和标准输出,它仍然可以通过/dev/tty同用户</p>
<p>交互。 </p>
<p>这个解决方法仍不如expect程序通用,但是它提供了一种不到50行代码的选</p>
<p>择方案 </p>
<p>。 </p>
<p>19.7 其他特性 </p>
<p>伪终端还有其他特性,我们在这里简略提一下。AT&T[1990d]和4.3+BSD系</p>
<p>统的操作 </p>
<p>手册有更详细的内容。 </p>
<p>打包模式 </p>
<p>打包模式能够使伪终端主设备了解到伪终端从设备的状态变化。在SVR4系统</p>
<p>中可以 </p>
<p>将流模块pckt放到主设备端来设置这种模式。图19.2显示了这种可选模式。</p>
<p>在4.3 </p>
<p>+BSD系统中可以通过TIOCPKT的ioctl来设置这种模式。 </p>
<p>SVR4和4.3+BSD系统中具体的打包模式有所不同。在SVR4系统中,读取伪终</p>
<p>端主设 </p>
<p>备的进程必须调用getmsg从流中取得数据,这是因为pckt模块将一些事件转</p>
<p>化为无 </p>
<p>数据的流消息。在4.3+BSD系统中每一次从伪终端主设备的读操作都会在可选</p>
<p>数据 </p>
<p>之后返回状态字节。 </p>
<p>无论实现的方法是什么样的,打包模式的目的是,当伪终端从设备之上的行规</p>
<p>程模 </p>
<p>块出现以下事件时,通知进程从伪终端主设备读取数据:当读入队列被刷新;</p>
<p>当写 </p>
<p>出队列被刷新;当输出被停止(如:Control-S);当输出重新开始;当</p>
<p>XON/XOFF </p>
<p>流开关被关闭后重新打开;当XON/XOFF流开关被打开后重新关闭。这些事件</p>
<p>被rlo </p>
<p>gin server和rlogin client等使用。 </p>
<p>远程模式 </p>
<p>伪终端主设备可以用TIOCREMOTE的ioctl将伪终端从设备设置成远程模式。</p>
<p>虽然S </p>
<p>VR4和4.3+BSD系统使用同样的命令来打开或关闭这个特性,但是在SVR4系统</p>
<p>中ioc </p>
<p>tl的第三个参数是整数而4.3+BSD中是整数的指针。 </p>
<p>当伪终端主设备将伪终端从设备设置成这种模式时,它是通知伪终端从设备之</p>
<p>上的 </p>
<p>行规程模块对从主设备收到的任何数据都不要进行处理,无论它是不是从设备</p>
<p>的t </p>
<p>ermios结构的规范或非规范标志。远程模式适用于窗口管理器这种进行自己</p>
<p>的行编 </p>
<p>辑的应用模式。 </p>
<p>窗口大小变化 </p>
<p>伪终端主设备上的进程可以用TIOCSWINSZ的ioctl来设置从设备的窗口的大</p>
<p>小。如 </p>
<p>果新的大小和老的不同,一个SIGWINCH信号将被发送到伪终端从设备的前台</p>
<p>进程组 </p>
<p>。 </p>
<p>信号发生 </p>
<p>读写伪终端主设备的进程可以向伪终端从设备的进程组发送信号。在SVR4系</p>
<p>统, </p>
<p>可以通过TIOCSIGNAL的ioctl完成这个功能,第三个参数就是信号的数值。</p>
<p>在4.3+ </p>
<p>BSD中通过TIOCSIG的ioctl来完成,第三个参数就是信号编号值的指针。
</p>
<p>19.8 摘要 </p>
<p>本章首先说明了在SVR4和4.3+BSD系统中打开伪终端的代码。然后用此代码</p>
<p>提供了 </p>
<p>用于多种不同应用的通用的pty_fork函数。这个函数是小程序(pty)的基</p>
<p>础。并 </p>
<p>且讨论了许多伪终端的属性。 </p>
<p>伪终端在大多数UNIX系统中每天都被用来进行网络登录。我们检查了伪终端</p>
<p>的许 </p>
<p>多其他用途,从script程序到用批处理script来驱动交互式程序。 </p>
<p>习题 </p>
<p>19.1 当我们用telnet或rlogin远程登录到一个BSD系统上时,象我们在</p>
<p>19.3.2小结 </p>
<p>讨论过的那样,伪终端从设备的所有者和权限被设置。该过程是如何发生的?</p>
<p>19.2 修改4.3+BSD系统中的ptys_open函数,使之调用一个set-user-ID程</p>
<p>序来改变 </p>
<p>伪终端从设备的所有者和权限(象SVR4系统中的grantpt函数所做的)。
</p>
<p>19.3 使用pty程序来决定你的系统初始化termios结构和winsize结构的</p>
<p>值。 </p>
<p>19.4 重写loop函数(程序19.5),使之成为一个使用select或poll的单个</p>
<p>进程。 </p>
<p>19.5 在子进程中,pty_fork返回后,标准输入、标准输出和标准出错都以</p>
<p>读写方 </p>
<p>式打开。你能够将标准输入变成只读的,另两个变成只写的吗? </p>
<p>19.6 在图19.7中,指出哪个进程组是前台的,哪个进程组是后台的,并指</p>
<p>出会话 </p>
<p>管理者。 </p>
<p>19.7 在图19.7中,当我们键入文件终结符时,进程终止的顺序是什么?如</p>
<p>果可能 </p>
<p>的话,用进程计数来修改之。 </p>
<p>19.8 script(1)程序通常在开始时在输出文件头增加一行,在结束时在输</p>
<p>出文件 </p>
<p>末尾增加一行。将这个特性加到我们展示的简单shell脚本中。 </p>
<p>19.9 解释为什么在下面的例子中,文件data的内容被输出到终端上,而程</p>
<p>序ttyn </p>
<p>ame只产生输出而从不读取输入。 </p>
<p>$ cat data 一个有两行的文件 </p>
<p>hello, </p>
<p>world </p>
<p>$ pty -I < data ttyname -i 指忽略stdin的文件结束标志 </p>
<p>hello, </p>
<p>world </p>
<p>fd 0: /dev/ttyp5 我们期望ttyname输出这三行 </p>
<p>fd 1: /dev/ttyp5 </p>
<p>fd 2: /dev/ttyp5 </p>
<p>19.10 写一个调用pty_fork的程序,该程序有一个子进程,该子进程exec</p>
<p>另一个你 </p>
<p>写的程 序。子进程调用的新的程序能够捕获SIGTERM和SIGWINCH。当捕获到</p>
<p>消息 </p>
<p>时,该程序要打印出来,并且对于后一种消息,还要打印终端窗口大小。然后</p>
<p>让父 </p>
<p>进程向我们在19.7节描述过的,有ioctl的伪终端从设备的进程组发送</p>
<p>SIGTERM消息 </p>
<p>。从伪终端从设备读回消息验证我们捕获的消息。接下来用父进程设置伪终端</p>
<p>从设 </p>
<p>备窗口的大小,并读回伪终端从设备的输出。让父进程退出并确定是否要伪终</p>
<p>端从 </p>
<p>设备进程也终止,如果要终止,应如何终止? </p>
<p>-</font></p>
</body>
</html>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -