?? 19.htm
字號:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<meta name="GENERATOR" content="Microsoft FrontPage 3.0">
<title>C:\WINDOWS\Desktop\UnixProg\7.htm</title>
</head>
<body>
<font SIZE="2">
<h1 align="center">第十九章 偽終端 </h1>
<p>19.1 引言 </p>
<p>在第九章中我們介紹了進行終端登錄時,需要通過一個終端設備自動提供終端</p>
<p>的語 </p>
<p>義。在終端和運行程序之間有一個終端行規程(圖11.2),通過這個規程我</p>
<p>們能夠 </p>
<p>在終端上設置特殊字符(退格、行刪除、中斷等)。但是,當一個登錄請求到</p>
<p>達網 </p>
<p>絡連接時,終端行規程并不是自動被加載到網絡連接和登錄程序shell之間</p>
<p>的。圖 </p>
<p>9.5顯示了有一個偽終端設備驅動程序被用來提供終端語義。 </p>
<p>除了用于網絡登錄,偽終端還被用在其他方面,我們將在本章中進行介紹。我</p>
<p>們 </p>
<p>將首先提供在SVR4和4.3+BSD系統下用于創建偽終端的函數,然后使用這些</p>
<p>函數編 </p>
<p>寫一個程序用來調用pty。我們將看到這個程序的不同使用:在輸入字符和終</p>
<p>端顯 </p>
<p>示之間進行轉換(BSD的碼轉換程序)和運行協同進程來避免我們在程序</p>
<p>14.10中遇 </p>
<p>到的緩沖區問題。 </p>
<p>19.2 概述 </p>
<p>偽終端這個名詞暗示了與一個應用程序相比,它更加象一個終端。但事實上,</p>
<p>偽終 </p>
<p>端并不是一個真正的終端。圖19.1顯示了使用偽終端的進程的典型結構。其</p>
<p>中關鍵 </p>
<p>點如下: </p>
<p>圖19.1 使用偽終端的典型進程結構 </p>
<p>1 通常一個進程打開偽終端主設備然后調用fork。子進程建立了一個新的會</p>
<p>話,打 </p>
<p>開一個相應的偽終端從設備,將它復制成標準輸入、標準輸出和標準出錯,然</p>
<p>后調 </p>
<p>用exec。偽終端從設備成為子進程的控制終端。 </p>
<p>2
對于偽終端從設備之上的用戶進程來說,其標準輸入、標準輸出和標準出</p>
<p>錯都能 </p>
<p>當作終端設備使用。用戶進程能夠調用11章中講到的所有輸入/輸出函數。但</p>
<p>是因 </p>
<p>為在偽終端從設備之下并沒有真正的設備,無意義的函數調用(改變波特率、</p>
<p>發送 </p>
<p>中斷符、設置奇偶校驗等)將被忽略。 </p>
<p>3
任何寫到偽終端主設備的輸入都會作為在從設備端的輸入,反之亦然。事</p>
<p>實上所 </p>
<p>有從設備端的輸入都來自于主設備上的用戶進程。這看起來就象一個流管道</p>
<p>(圖1 </p>
<p>5.3),但從設備上的終端行規程使我們擁有普通管道之外的其他處理能力。</p>
<p>圖19.1顯示了BSD系統中的偽終端結構。在19.3.2中我們將看到如何打開這</p>
<p>些設備 </p>
<p>。 </p>
<p>在SVR4系統中偽終端是使用流系統來創建的(12.4節)。圖19.2詳細描述了</p>
<p>SVR4系 </p>
<p>統中各個偽終端模塊之間的關系。在虛線框中的兩個流模塊是可選的。請注意</p>
<p>在從 </p>
<p>設備上的三個流模塊同12.10網絡登錄程序的輸出是一樣的。在19.3.1小節</p>
<p>中將介 </p>
<p>紹如何組織這些流模塊。 </p>
<p>從現在開始將簡化以上圖示,首先我們不再畫出圖19.1的"讀、寫功能"或圖</p>
<p>19.2 </p>
<p>的流首。我們還使用縮寫"pty"表示偽終端,并將圖19.2中所有偽終端從設</p>
<p>備之上 </p>
<p>的流模塊集合表示為"終端行規程"模塊。 </p>
<p>圖19.2 在SVR4下的偽終端組織結構 </p>
<p>現在來看一下偽終端的幾種典型用法。 </p>
<p>網絡登錄服務器 </p>
<p>偽終端用于構造網絡登錄服務器。典型的例子是telnetd和rlogind服務器。</p>
<p>在St </p>
<p>evens[1990]的第15章中詳細討論了提供rlogin服務的步驟。一旦登錄</p>
<p>shell運行在 </p>
<p>遠端主機上,我們得到如圖19.3的結構。同樣的結構也用于telnetd服務</p>
<p>器。 </p>
<p>在rlogind服務器和登錄shell之間有兩個exec調用,這是因為login程序通</p>
<p>常是在 </p>
<p>兩個exec之間檢驗用戶是否合法的。 </p>
<p>圖19.3 rlogind服務器的進程組織結構 </p>
<p>本圖的一個關鍵點是驅動偽終端主設備的進程通常同時在讀寫另一個輸入/輸</p>
<p>出流 </p>
<p>。在本例中另一個輸入/輸出流是TCP/IP。這表示該進程必然使用了某種形式</p>
<p>的如 </p>
<p>select或poll那樣的輸入/輸出多路轉接(節12.5),或被分成兩個進程。</p>
<p>請回憶 </p>
<p>我們在18.7節討論過的一個進程和兩個進程的的比較。 </p>
<p>script程序 </p>
<p>script程序是隨SVR4和4.3+BSD提供的,該程序將終端會話期間所有的輸入</p>
<p>和輸出 </p>
<p>信息在一個文件中做一個拷貝。它通過將自己置于終端和登錄shell的一個新</p>
<p>的調 </p>
<p>用之間來完成這個工作。圖19.4詳細描述了script程序相關的交互。這里我</p>
<p>們特別 </p>
<p>指出script程序通常是從登錄shell啟動的,該shell然后等待程序的結束。</p>
<p>當script程序在運行的時候,在偽終端從設備之上終端行規程的所有輸出都</p>
<p>被復制 </p>
<p>到一個script文件中(通常叫做typescript)。因為我們的擊鍵通常被行規</p>
<p>程的模 </p>
<p>塊回顯,該script文件也包括了輸入的內容。但是,因為口令字不被回顯,</p>
<p>該scr </p>
<p>ipt文件不會包含口令字。 </p>
<p>圖19.4 script程序 </p>
<p>本書中所有運行程序并顯示其輸出的實例都是由script程序實現的,這樣避</p>
<p>免了手 </p>
<p>動拷貝程序輸出可能帶來的錯誤。 </p>
<p>在19.5節開發一個通用的pty程序后,我們將看到一個巧妙的shell程序能夠</p>
<p>將它轉 </p>
<p>化成一個script程序。 </p>
<p>expect程序 </p>
<p>偽終端可以用來使交互式的程序運行在非交互的狀態中。許多程序需要一個終</p>
<p>端 </p>
<p>來運行,18.7節中的call進程就是一個例子。它假定標準輸入是一個終端并</p>
<p>在啟動 </p>
<p>時將其設置為初始模式(18.20程序)。該程序不能從一個shell程序中被運</p>
<p>行來自 </p>
<p>動撥號到遠程系統,登錄,取出信息和登出。 </p>
<p>同修改所有交互式程序來支持批處理模式的操作比較,一個更好的解決方法是</p>
<p>提 </p>
<p>供一種手段來通過一個script來驅動交互式程序。expect程序[Libes</p>
<p>1990;1991] </p>
<p>提供了這樣的方法。類似于19.5節的pty程序,它使用偽終端來運行其他程</p>
<p>序。但 </p>
<p>是,expect還提供了一種編程語言用于檢查程序的輸出來確定以什么作為輸</p>
<p>入發送 </p>
<p>給該程序。當一個交互式的程序開始從一個script運行時,我們不能僅僅是</p>
<p>將scr </p>
<p>ipt中的所有內容輸入到程序中去。相應的,我們要通過檢查程序的輸出來決</p>
<p>定下 </p>
<p>一步輸入的內容。 </p>
<p>運行協同進程 </p>
<p>在14.10的程序例子中,我們不能調用使用標準輸入/輸出庫進行輸入、輸出</p>
<p>的協 </p>
<p>同進程,這是因為當我們通過管道與協同進程進行通訊時,標準輸入/輸出庫</p>
<p>會將 </p>
<p>標準輸入、輸出的內容放到緩沖區中,從而引起死鎖。如果協同進程是一個已</p>
<p>經編 </p>
<p>譯的程序而我們又沒有源程序,我們就無法在源程序中加入fflush語句來解</p>
<p>決這個 </p>
<p>問題。圖14.9顯示了一個進程驅動協同進程的情況。我們需要做的是將一個</p>
<p>偽終端 </p>
<p>放到兩個進程之間,如圖19.5所示。 </p>
<p>圖19.5 用偽終端驅動一個協同進程 </p>
<p>現在協同進程的標準輸入和標準輸出就象一個終端設備一樣,所以標準輸入/</p>
<p>輸出 </p>
<p>庫會將這兩個流設置為行緩沖的。 </p>
<p>父進程有兩種不同的方法在自身和協同進程之間獲得偽終端(這種情況下的父</p>
<p>進程 </p>
<p>可以象程序14.9,使用兩個管道和協同進程進行通訊;或者象程序15.1那</p>
<p>樣,使用 </p>
<p>一個流管道)。一個方法是父進程直接調用pty_fork函數(19.4節)而不是</p>
<p>fork。 </p>
<p>另一種方法是exec該pty程序,將協同進程作為參數(19.5節)。我們將在</p>
<p>說明pt </p>
<p>y程序后介紹這兩種方法。 </p>
<p>觀看長時間運行程序的輸出 </p>
<p>使用任一個標準shell,我們都可以將一個需要長時間運行的程序放到后臺運</p>
<p>行。 </p>
<p>但是如果我們將該程序的標準輸出重定向到一個文件,并且如果它產生的輸出</p>
<p>不多 </p>
<p>,我們就不能方便地監控程序的進展,這是因為標準的輸入/輸出庫會將標準</p>
<p>輸出 </p>
<p>放在緩沖區中保存。我們看到的將只是成塊的輸出結果,有時甚至可能是</p>
<p>8192字節 </p>
<p>一塊。 </p>
<p>如果我們有源程序,我們可以加入fflush調用。另一種方法是,我們可以在</p>
<p>pty程 </p>
<p>序下運行該程序,讓標準輸入/輸出庫認為輸出是終端。圖19.6說明了這個結</p>
<p>構, </p>
<p>我們將這個緩慢輸出的程序稱為slowout。從登錄shell到pty進程的</p>
<p>fort/exec箭頭 </p>
<p>用虛線表示,以強調pty進程是作為后臺任務運行的。 </p>
<p>19.3 打開偽終端設備 </p>
<p>在SVR4和4.3+BSD系統中打開偽終端設備的方法有所不同。我們提供兩個函</p>
<p>數來處 </p>
<p>理所有細節:ptym_open用來打開下一個有效的偽終端主設備,ptys_open</p>
<p>用來打開 </p>
<p>相應的從設備。 </p>
<p>圖19.6 使用偽終端運行一個緩慢輸出的程序 </p>
<p>#include "ourhdr.h" </p>
<p>int ptym_open(char *pts_name); </p>
<p>返回:如果操作成功,返回偽終端主設備文件 </p>
<p>述符;否則返回-1 </p>
<p>int ptys_open(int fdm, char *pts_name); </p>
<p>返回:如果操作成功,返回偽終端從設備文件 </p>
<p>述符;否則返回-1 </p>
<p>通常我們不直接調用這兩個函數--函數pty_fork(19.4節)調用它們并</p>
<p>fork出一個 </p>
<p>子進程。 </p>
<p>ptym_open決定下一個有效的偽終端主設備并打開該設備。這個調用必須分</p>
<p>配一個 </p>
<p>數組來存放主設備或從設備的名稱,并且如果調用成功,相應的主設備或從設</p>
<p>備的 </p>
<p>名稱會通過pts_name返回。這個名稱和ptym_open返回的文件描述符將傳給</p>
<p>ptys_o </p>
<p>pen,該函數用來打開一個從設備。 </p>
<p>在我們講解pty_fork函數之后,使用兩個函數來打開這兩個設備的原因將會</p>
<p>很明顯 </p>
<p>。通常,一個進程調用ptym_open來打開一個主設備并且得到從設備的名</p>
<p>稱。該進 </p>
<p>程然后fork子進程,子進程在調用setid建立新的會話后調用ptys_open來</p>
<p>打開從設 </p>
<p>備。這就是從設備如何成為子進程的控制終端的過程。 </p>
<p>19.1.1 系統V的版本4 </p>
<p>所有在SVR4系統下的偽終端的流實現細節在AT&T[1990d]的第十二章中有所</p>
<p>說明。 </p>
<p>三個函數在下列手冊頁中描述:grantpt(3),unlockpt(3),和</p>
<p>ptsname(3)。 </p>
<p>偽終端主設備是/dev/ptmx。這是一個流的增殖設備。這意味著當我們打開</p>
<p>該增殖 </p>
<p>設備,其open例程自動決定第一個未被使用的偽終端主設備并打開這個設</p>
<p>備。(在 </p>
<p>下一節我們將看到在Berkeley系統中,我們必須自己找到第一個未被使用的</p>
<p>偽終端 </p>
<p>主設備。) </p>
<p>_______________________________________________________________________</p>
<p>________ </p>
<p>#include <sys/types.h> </p>
<p>#include <sys/stat.h> </p>
<p>#include <errno.h> </p>
<p>#include <fcntl.h> </p>
<p>#include <stropts.h> </p>
<p>#include "ourhdr.h" </p>
<p>extern char *ptsname(int); /* prototype not in any system</p>
<p>header */ </p>
<p>int </p>
<p>ptym_open(char *pts_name) </p>
<p>{ </p>
<p>char *ptr; </p>
<p>int fdm; </p>
<p>strcpy(pts_name, "/dev/ptmx"); /* in case open fails */ </p>
<p>if ( (fdm = open(pts_name, O_RDWR)) < 0) </p>
<p>return(-1); </p>
<p>if (grantpt(fdm) < 0) { /* grant access to slave */ </p>
<p>close(fdm); </p>
<p>return(-2); </p>
<p>} </p>
<p>if (unlockpt(fdm) < 0) { /* clear slave's lock flag */ </p>
<p>close(fdm); </p>
<p>return(-3); </p>
<p>} </p>
<p>if ( (ptr = ptsname(fdm)) == NULL) { /* get slave's name */ </p>
<p>close(fdm); </p>
<p>return(-4); </p>
<p>} </p>
<p>strcpy(pts_name, ptr); /* return name of slave */ </p>
<p>return(fdm); /* return fd of master */ </p>
<p>} </p>
<p>int </p>
<p>ptys_open(int fdm, char *pts_name) </p>
<p>{ </p>
<p>int fds; </p>
<p>/* following should allocate controlling terminal */ </p>
<p>if ( (fds = open(pts_name, O_RDWR)) < 0) { </p>
<p>close(fdm); </p>
<p>return(-5); </p>
<p>} </p>
<p>if (ioctl(fds, I_PUSH, "ptem") < 0) { </p>
<p>close(fdm); </p>
<p>close(fds); </p>
<p>return(-6); </p>
<p>} </p>
<p>if (ioctl(fds, I_PUSH, "ldterm") < 0) { </p>
<p>close(fdm); </p>
<p>close(fds); </p>
<p>return(-7); </p>
<p>} </p>
<p>if (ioctl(fds, I_PUSH, "ttcompat") < 0) { </p>
<p>close(fdm); </p>
<p>close(fds); </p>
<p>return(-8); </p>
<p>} </p>
<p>return(fds); </p>
<p>} </p>
<p>_______________________________________________________________________</p>
<p>________ </p>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -