Linux終端簡介與pty編程


一、Linux終端簡介  

  終端是一種特殊的字符設備,用來向計算機輸入數據和顯示計算機的輸出,最早的終端是由teletype公司生產的一種電傳打字機,它將從穿孔紙帶讀取的程序代碼傳送給計算機,將計算機的輸出以紙質形式打印出來。tty是teletype的縮寫,后來便成了終端設備的代名詞,下圖為型號為model 33 ASR的teletype終端。

 

  隨后出現了基於顯示器和鍵盤的終端,極大提高了打印輸出的速度和交互性,這些早期的終端都不具備計算能力,只能完成最基本的輸入輸出功能,稱為啞終端。啞終端不能完成文本編輯操作,一旦出現漏掉或錯誤的字符,則必須從頭開始輸入,而且啞終端的硬件電路復雜,需要的電子器件數目多,基於啞終端的諸多缺點人們開發出了智能終端,智能終端具有簡易處理器,可以提供友好的編輯環境(如圖形化界面),允許用戶在本地完成文本編輯后以數據塊的形式輸入到計算機。

  早期計算機體積龐大,價格高昂,一個工作機構里面往往通過多台外置終端共享同一台主機,隨着計算機技術的發展和PC的普及,終端逐漸與計算機集成在一起,即鍵盤與顯示器,單純用於輸入輸出的終端設備慢慢失去其原先的作用。現代操作系統大都提供方便操作的圖形化用戶界面,但對系統管理員、軟件開發人員來說,使用命令行、腳本等能更有效地完成工作,終端模擬器是一種模擬文本終端的計算機軟件,用於為用戶提供訪問本地或遠程主機的文本界面(如命令行),下圖為Ubuntu系統下的模擬終端窗口。

  Linux系統中一切都是文件,每個終端設備都有一個對應的設備文件,有些終端設備文件可能並不對應物理存在的終端設備,也可與其他文件對應同一個終端設備,從設備文件的角度,Linux終端可划分為以下幾種。

  • 串行端口終端

  與計算機串行端口(RS-232)連接的終端設備,對應的設備文件名稱為/dev/tty(或/dev/cu)+類型+設備編號,如/dev/ttyS0,S表示設備類型,0為指定類型下的設備編號。這里的串行端口可以是通過硬件或軟件模擬的,如USB轉串口,虛擬串口。

 

  • 偽終端

  成對存在的邏輯設備,包括主、從設備,可以為主、從設備上的應用程序提供一種雙向通信管道,從設備上的應用進程可以像使用真實終端一樣從偽終端讀入數據(或輸出信息)。Linux支持BSD和system V兩種風格的偽終端設備,BSD風格下偽終端是系統預創建的,/dev/ptyXY表示主設備,/dev/ttyXY表示從設備,其中,X、Y分別屬於字符集{{p-z},{a-e}}和{{0-9},{a-f}}。system V風格(又稱為UNIX 98)下偽終端是動態創建的,所有的主設備對應的設備文件都是/dev/ptmx(主設備號為5,次設備號為2),而從設備對應的設備文件都位於/dev/pts/目錄下,以設備的數字編號命名(如/dev/pts/0)。

 

 

 

  • 控制台終端

  提供系統管理接口的終端設備,讀取管理員的操作指令,輸出系統運行信息(應用程序、系統程序、內核等)。控制台對應的設備文件名稱為/dev/console,Linux系統中可以在內核啟動時指定控制台對應的終端設備(比如ttyS0),對console文件的操作會轉義為對實際終端設備的操作。控制台是可選的Linux內核配置項,大多數嵌入式系統並不支持控制台(比如手機),在系統啟動后直接提供一個用戶操作界面。

  控制台只允許單用戶登錄,一些類UNIX系統(如BSD、Linux、UnixWare等)中引入了虛擬控制台,也稱為虛擬終端(VT),允許多個用戶同時從不同的VT登錄,創建相互隔離的系統會話。現行的Linux發行版本中一般會創建7個虛擬控制台,對應的設備文件依次為/dev/tty1~tty7,另外/dev/tty0指示當前虛擬控制台,用戶可以通過按鍵Alt+F1~7進行控制台切換。虛擬控制台以顯示器與鍵盤作為IO設備,linux系統缺省控制台為tty0,因此默認情況下,系統啟動信息會在顯示器上輸出,用戶從鍵盤輸入登錄信息。Linux系統允許將多個設備指定為控制台,此時系統信息會在指定的多個設備上同時輸出,但輸入只能從最后指定的終端設備上讀取。

 

  • 控制終端

  任何系統會話都基於一個特定終端,即為會話發起進程的控制終端。發起會話的進程為會話的頭進程,頭進程的進程組ID與會話ID都等於頭進程的PID,通過fork調用生成的子進程會繼承父進程的會話ID、進程組ID和控制終端屬性。/dev/tty表示當前進程的控制終端,主設備號為5,次設備號0。

  tty命令可以查看當前會話所使用的實際終端設備,ps ax命令可以查看系統中所有進程的控制終端(如果程序沒有控制終端,如內核線程、守護進程,TTY一欄顯示為“?”)。

 

XXX@ubuntu:~$ ps -ax
   PID TTY      STAT   TIME COMMAND
     1 ?        Ss     0:02 /sbin/init auto noprompt
     2 ?        S      0:00 [kthreadd]
     3 ?        S      0:00 [ksoftirqd/0]
     5 ?        S<     0:00 [kworker/0:0H]
     7 ?        S      0:04 [rcu_sched]
     8 ?        S      0:00 [rcu_bh]
     9 ?        S      0:00 [migration/0]
    10 ?        S      0:00 [watchdog/0]
    11 ?        S      0:00 [kdevtmpfs]
    12 ?        S<     0:00 [netns]
    13 ?        S<     0:00 [perf]
  14 ?        S      0:00 [khungtaskd]
  …
1108 tty1     Ss+    0:00 /sbin/agetty --noclear tty1 linux
6977 tty3     Ss+    0:00 /sbin/agetty --noclear tty3 linux
6982 tty2     Ss+    0:00 /sbin/agetty --noclear tty2 linux
3329 tty5     Ss     0:00 /bin/login --
3384 tty5     S+     0:00 -bash

 

  通過ioctl調用對/dev/tty設置TIOCNOTTY標記將使得調用進程與其控制終端脫離,如果調用進程為會話頭進程,當前會話的所有進程都會丟失控制終端,在后台運行的守護進程需要使用這種調用。setsid調用可以使調用進程(不能是頭進程)在新會話中運行,與當前會話控制終端脫離,並成為新會話的頭進程,此時調用進程將不具有控制終端。當系統會話因外部原因異常終止時(如網絡故障導致telnet連接斷開),會話關聯的所有進程將失去控制終端。

二、偽終端原理

  偽終端的出現是因為一些面向終端應用的輸入(或輸出)不再直接來自(或去往)實際終端,比如在網絡登錄應用中,getty進程需要從終端獲取用戶登錄信息,而用戶輸入是通過socket傳遞到網絡服務進程,此時如果getty進程能通過一種虛擬的終端從網絡服務進程讀取用戶輸入,就能成功實現基於網絡的登錄。

  偽終端是一對虛擬的字符設備,linux內核使用一種符合tty線規程(line discipline)的雙向管道連接偽終端的主從設備。主設備上的任何寫入操作都會反映到從設備上,反之亦然,從設備上的應用進程可以像使用傳統終端一樣讀取來自主設備上應用程序的輸入,以及向主設備應用輸出信息。偽終端從設備應用通常是主設備應用的子進程,主應用打開一對偽終端並fork一個子進程,然后子進程打開並使用從設備,偽終端應用的實現模型可以用下圖表示。

 

三、偽終端編程接口

  從2.6.4版本內核開始,BSD風格偽終端被認為過時,不再被新的應用使用,可以在配置內核時去除對BSD偽終端的支持。

老式的BSD偽終端的使用是先依次對每個master文件執行open操作直到成功,然后從應用程序打開相應的slave並執行讀寫操作。

UNIX 98偽終端的一般使用流程如下(具體參考linux man手冊):

  1. 使用posix_openpt打開master;
  2. 使用grantpt設置調用進程為slave的屬主並允許其對slave進行讀寫操作;
  3. 使用unlockpt對slave解鎖;
  4. 使用ptsname返回slave的設備名;
  5. 使用open打開slave設備並進行讀寫操作。

  上述步驟中的函數都來自glibc庫,函數原型分別說明如下:

函數名

posix_openpt

功能

打開一個未被使用的偽終端設備

參數

flags

int

設備操作標記,可以是0或以下兩項的之一(或組合)

O_RDWR —— 允許對設備同時進行讀寫操作,此標記通常需要指定;

O_NOCTTY —— 不將設備作為進程的控制終端。

返回值

int,如果成功,返回master設備的文件描述符,否則-1

說明

需要包含stdlib.h頭文件,Glibc2.2.1及其后續版本支持,一些UNIX系統沒有該函數,可以自行實現如下。

int posix_openpt(int flags)

{

    return open("/dev/ptmx", flags);

}

 

函數名

grantpt

功能

改變指定master對應從設備的屬主與訪問權限

參數

fd

int

主設備文件描述符

返回值

int,如果成功,返回0,否則-1

說明

需要包含stdlib.h頭文件,Glibc2.1及其后續版本支持。

 

函數名

unlockpt

功能

解鎖指定master對應的從設備

參數

fd

int

主設備文件描述符

返回值

int,如果成功,返回0,否則-1

說明

需要包含stdlib.h頭文件,Glibc2.1及其后續版本支持。

 

函數名

ptsname

功能

獲取指定master對應的從設備的名稱

參數

fd

int

主設備文件描述符

返回值

char *,如果成功,在一個靜態存儲區存放設備名並返回其地址,否則NULL。

說明

需要包含stdlib.h頭文件,Glibc2.1及其后續版本支持。

返回指針不能被調用進程釋放。

此函數的可重入版本如下:

int ptsname_r(int fd, char *buf, size_t buflen)

該函數將返回的從設備名存放在buf指向的緩沖區,buflen為緩沖區大小。如果成功,ptsname_r返回0,否則返回非0值。

 

  偽終端編程更常使用的API是openpty,其直接實現了上述流程的所有步驟,函數說明如下:

函數名

openpty

功能

獲取一對可用的偽終端

參數

amaster

int *

指向主設備文件描述符

aslave

int *

指向從設備文件描述符

name

char *

如果輸入參數不為空,存放返回的從設備名稱

termp

struct

termios *termp

傳入的從設備終端參數,通常設置為NULL

winp

struct

winsize *winp

傳入的從設備窗口大小,通常設置為NULL

返回值

int,如果成功,返回0,否則-1。

說明

需要包含pty.h頭文件,glibc2與libc5及后續版本支持,不遵循posix標准。

從glibc2.8開始對入參termp和winp增加const修飾符。

glibc2.0.92之前版本返回的是BSD偽終端對,2.0.92及后續版本返回UNIX 98偽終端對。

由於從設備名長度不可預知,如果通過傳入非空的name參數來獲取slave設備名,會有緩沖區溢出的危險。

 

  login_tty函數用於實現在指定的終端上啟動登錄會話,函數說明如下:

函數名

login_tty

功能

為指定終端上的登錄會話作准備

參數

fd

int *

指定終端設備的文件描述符

返回值

int,如果成功,返回0,否則-1。

說明

需要包含utmp.h頭文件,glibc2與libc5及后續版本支持,不遵循posix標准。

 

  forkpty函數整合了openpty,fork和 login_tty,在網絡服務程序可用於為新登錄用戶打開一對偽終端,並創建相應的會話子進程。

函數名

forkpty

功能

為新會話打開一對偽終端並創建處理進程

參數

amaster

int *

指向主設備文件描述符

name

char *

如果輸入參數不為空,存放返回的從設備名稱

termp

struct

termios *termp

傳入的從設備終端參數,通常設置為NULL

winp

struct

winsize *winp

傳入的從設備窗口大小,通常設置為NULL

返回值

int,如果失敗,返回-1,否則,子進程返回0,父進程返回子進程的PID。

說明

需要包含pty.h頭文件,glibc2與libc5及后續版本支持,不遵循posix標准。

從glibc2.8開始對入參termp和winp增加const修飾符。

由於從設備名長度不可預知,如果通過傳入非空的name參數來獲取slave設備名,會有緩沖區溢出的危險。

 

  注意:使用opentty,login_pty和forkpty需要鏈接util庫。

 

四、偽終端編程示例

  以下程序啟動后打開一對偽終端,不斷打印從主設備上讀取的數據。

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <string.h>
  5 #include <sys/types.h>
  6 #include <linux/limits.h>
  7 #include <pty.h> /* for openpty and forkpty */
  8 #include <utmp.h> /* for login_tty */
  9 #include <time.h>
 10 
 11 #define SLAVE_DEV_NAME_MAX_LEN       100
 12 #define PTY_BUFF_MAX_LEN             100
 13 
 14 
 15 /* 
 16  * call opentty
 17  * print any data read from ptmx
 18  */
 19 int main(int argc, char *argv[])
 20 {
 21     int mpty = 0;
 22     int spty = 0;
 23     char spty_name[SLAVE_DEV_NAME_MAX_LEN] = {'/0'};
 24     char *pname = NULL;
 25     
 26     int rv = 0;
 27     int namelen = 0;
 28 
 29     int n = 0;
 30     char buf[PTY_BUFF_MAX_LEN] = {'/0'};
 31 
 32     fd_set rdfdset;
 33 
 34     rv = openpty(&mpty, &spty, spty_name, NULL, NULL);
 35 
 36     if (-1 == rv)
 37     {
 38         perror("Failed to get a pty");
 39         goto ERROR;
 40     }
 41 
 42     printf("Get a pty pair, FD -- master[%d] slave[%d]\n", mpty, spty);
 43     printf("Slave name is:%s\n", spty_name);
 44 
 45     /* Monitoring the pty master for reading */
 46     FD_ZERO(&rdfdset);
 47     FD_SET(mpty, &rdfdset);
 48 
 49     while (1)
 50     {
 51         rv = select(mpty + 1, &rdfdset, NULL, NULL, NULL);
 52         
 53         if (0 > rv)
 54         {
 55             perror("Failed to select");
 56             goto ERROR;
 57         }
 58 
 59         if (FD_ISSET(mpty, &rdfdset))
 60         {
 61             /* Now data can be read from the pty master */
 62             n = read(mpty, buf, PTY_BUFF_MAX_LEN);
 63             if (0 < n)
 64             {
 65                 int ii = 0;
 66                 
 67                 memset(buf + n, 0, PTY_BUFF_MAX_LEN - n);
 68 
 69                 printf("-----------------------------------\n");
 70                 printf("Message from slave:\n");
 71                 printf("%s\n", buf);
 72                 printf("------%d bytes------\n\n", n);
 73 
 74             }
 75             else if (0 == n)
 76             {
 77                 printf("No byte is read from the master\n");
 78             }
 79             else
 80             {
 81                 perror("Failed to read the master");
 82                 goto ERROR;
 83             }
 84         }
 85         else
 86         {
 87             printf("The master isn't readable!\n");
 88             goto ERROR;
 89         }
 90     }
 91 
 92 
 93 ERROR:
 94 
 95     if (0 < mpty)
 96     {
 97         close(mpty);
 98     }
 99 
100     if (0 < spty)
101     {
102         close(spty);
103     }
104 
105     return -1;
106         
107 }

   

  啟動一個終端窗口進入ptytest.c存放目錄,輸入如下命令編譯代碼。

XXX@ubuntu:~$ gcc -o ptytest -g ptytest.c –lutil  

  輸入./ptytest指令運行程序,終端窗口運行結果如下:

Get a pty pair, FD -- master[3] slave[4]

Slave name is:/dev/pts/23

  打開另一個終端窗口,鍵入指令“echo hello, world  > /dev/pts/23”,ptytest程序運行窗口輸出結果如下:

Get a pty pair, FD -- master[3] slave[4]
Slave name is:/dev/pts/23
-----------------------------------
Message from slave:
hello, world

------14 bytes------

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM