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手册):
- 使用posix_openpt打开master;
- 使用grantpt设置调用进程为slave的属主并允许其对slave进行读写操作;
- 使用unlockpt对slave解锁;
- 使用ptsname返回slave的设备名;
- 使用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------