Linux终端

来源:互联网 发布:时间煮雨 歌词 知乎 编辑:程序博客网 时间:2024/04/19 17:08

终端驱动器与通用终端接口

有时程序需要更好的控制终端而不是使用简单的文件操作来达到。Linux提供了一个可以允许我们控制终端驱动器的接口集合,从而可以使得我们更好的控制终端的输入与输出处理过程。

概览

正如下图所示,我们可以通过一个与用来进行读写操作相分离的函数调用集合来控制终端。这使得数据接口更为清晰,同时又能更好的控制终端的行为。这并不说是说终端I/O接口是清晰的,而是可以处理各种不同的硬件。

在Linux术语中,控制接口设置一个"行规程"(line discipline),从而使得程序在指定终端驱动器的行为方面更为灵活。

我们可以控制的主要特征包括:

行编辑:决定是否允许编辑使用Backspace
缓冲:决定是否立即读取字符,或是在一个延时后读取
回显:允许我们控制回显,例如当我们正读取密码时
CR/LF:决定输入与输出映射,也就是当我们输入一个/n时会发生什么
行速度:很少用在PC控制台上,这些速度对于调制解调器和串口线上的终端非常重要

硬件模型

在我们详细的了解通用终端接口之前,了解一下他所驱动的硬件模型是非常重要的。

下图显示的是一台Unix机器通过一个串口连接到一个调制解调器,然而通过一条电话线和另一个调制解调器连接到远程端。事实上,这就是一些小的网络提供商所采用的配置类型。这就是相对遥远的客户/服务器模式,用于程序在主机上运行而用户在终端上工作的情况。

如果我们在一个运行Linux的PC上工作,这显得似乎是一个过于复杂的模型。然而,如果两个人都有调制解调器,如果我们愿意,我们可以使用一个终端模拟程序,例如minicom,来在彼此的机器上运行一个登陆会话,就如同使用一对调制解调器和一条电话线一样。

使用这样一个硬件模型的好处就是大多数真实世界的情况会形成这种最复杂情况的一个子集。支持他们比失去这个功能更为简单。

termios结构

termios是POSIX所指定的标准接口,与System V接口的termio类似。终端接口是通过在一个termios类型的结构中设置值以及使用一组函数调用来进行控制的。所有这些都定义在头文件termios.h中。

注 意:使用定义在termios.h中的函数的程序需要使用一个合适的函数库进行链接。这通常是curses库,所以当编译这一章的程序是,我们需要在编译 器命令行的最后加上-lcurses。在一些老的Linux系统上,curses库是由一个所谓的新curses或是ncurses来提供的。在这些情况 下,库名字与链接参数就分别变为-lncurses。

可以进行操作来影响终端的值可以分为几种模式:

输入(input)
输出(output)
控制(control)
本地(local)
特殊控制字符(Special control characters)

一个最小的termios结构通常声明如下(X/Open规范允许添加一些其他的域):

#include <termios.h>
struct termios {
    tcflag_t c_iflag;
    tcflag_t c_oflag;
    tcflag_t c_cflag;
    tcflag_t c_lflag;
    cc_t     c_cc[NCCS];
};

成员的名字对应上面列表中的五个参数。

我们可以通过调用tcgetattr函数来为终端初始化termios结构,其函数原型如下:

#include <termios.h>
int tcgetattr(int fd, struct termios *termios_p);

这个函数调用将终端接口变量的当前值写入由termios_p所指向的结构中。如果这些值被修改了,我们可以使用tcsetattr函数来重新配置终端接口:

#include <termios.h>
int tcsetattr(int fd, int actions, const struct termios *termios_p);

tcsetattr函数中的actions域控制如何应用这些修改。三个可能的值分别为:

TCSANOW:立即更改
TCSADRAIN:当前输出完成时更改
TCSAFLUSH:当前输出完成时更改,但是忽略当前可用的输入与read调用中未返回的输入

注意:在程序启动之前保存终端设置是非常重要的。通常程序负责初始保存并且在程序完成时恢复设置。

下面我们会详细的看一下这些模式以及相关的函数调用。某些详细的模式相当特殊并且很少用到,所以在这里我们只讨论主要特征。如果我们要了解更多的内容,我们可以查看我们本地的man手册页或是一份POSIX或X/Open规范的拷贝。

我们首先要了解的最重要的模式的是本地模式。正规与非正规模式是我们第一个程序中第二个问题的解决方法。我们可以指示我们的程序等一行输入或是在输入之后立即声明为输入。

输入模式

输入模式控制输入(终端驱动器在串口或是键盘上接收的字符)在传递给程序之前是如何处理的。我们通过在termios结构的c_iflag成员中设置相应的标记来进行设置。所有这些标记都定义为宏,而且可以使用位或进行组合。对于所有的终端模式都是如此。

可以用于c_iflag的宏为:

BRKINT:在一行中检测到中断(break)条件时产生一个中断
IGNBRK:在一行中忽略中断条件
INCRNL:将接收到的回车转换为换行
IGNCR:忽略接收到的因车
INLCR:将接收到的新行转换为回车
IGNPAR:忽略带有奇偶检验误差的字符
INPCK:在接收到的字符上执行奇偶校验
PARMRK:标记奇偶校验误差
ISTRIP:去除所有的输入字符
IXOFF:在输入上允许软件流控制
IXON:在输出上允许软件流控制

如果没有设置BRKINT与IGNBRK,一行中的break条件将会被读取为NULL(Ox00)字符。

我们并不需要经常的改变输入模式,因为默认值通常是最合适的,所以在这里我们并不会进行更为深入的讨论。

输出模式

这 些模式控制输出字符是如何进行处理的;也就是说,程序所发送的字符在传递到串口或是屏幕之前是如何被处理的。正如我们所期望的,许多输出模式都有相对应的 输入模式。同时也存在一些其他的标记,这些标记主要关注于允许需要时间处理字符的慢速终端。几乎所有的这些模式都可以使用终端功能的terminfo数据 进行很好的处理,这我们会在后面用到。

我们通过设置termios结构的c_flag成员标记来控制输出模式。我们可以在c_oflag使用的标记有:

OPOST:打开输出处理
ONLCR:将输出的新行转换为回车/换行对
OCRNL:将输出的回车转换为新行
ONOCR:在第0列不输出回车
ONLRET:新行也需要一个回车
OFILL:发送填充字符来提供延时
OFDEL:使用DEL作为填充字符,而不是NULL
NLDLY:新行延时选择
CRDLY:回车延时选择
TABDLY:Tab延时选择
BSDLY:Backspace延时选择
VTDLY:垂直Tab延时选择
FFDLY:换页延时选择

如果OPOST没有设置,所有其他的标记都会被忽略。

输出模式也并不经常使用,所以在这里我们也不会进行更深入的讨论。

控制模式

这些模式控制终端的硬件特点。我们可以通过设置termios结构中的c_cflag成员的值来指定控制模式,其可用的值为:

CLOCAL:忽略调制解调器状态行
CREAD:允许字符接收
CS5:在发送或是接收的字符中使用5位(5 bits)
CS6:在发送或是接收的字符中使用6位
CS7:在发送或是接收的字符中使用7位
CS8:在发送或是接收的字符中使用8位
CSTOPB:每个字符使用两个结束位,而不是一个
HUPCL:关闭时挂起调制解调器
PARENB:允许奇偶校验生成与检测
PARODD:使用介校验而不是奇校验

如果设置了HUPCL,当终端驱动器检测到指向终端的最后一个文件描述符已经关闭时,他就会将调制解调器控制行设置为挂起。

控制模式主要用于串口线连接到一个调制解调器上的情况,尽管他们与可以用于与终端交互。通常,使用termios的控制模式改变我们终端的配置要比改变默认行行为简单得多。

本地模式

这些模式控制终端的各种特性。我们可以通过设置termios结构中的c_lflag成员的值来指定本地模式,其可用的宏如下:

ECHO:允许输入字符的本地回显
ECHOE:在接收EPASE时执行Backspace,Space,Backspace组合
ECHOK:在KILL字符上执行清除行
ECHONL:回显新行字符
ICANON:允许正规输入处理
IEXTEN:允许实现特定函数
ISIG:允许信号
NOFLSH:禁止队列flush
TOSTOP:在写尝试上发送后台处理信号

最重要的两个标记为ECHO,这会允许我们抑制输入字符的回显,以及ICANON,他在两个不同的处理接收字符的模式中切换终端。如果设置了ICANON标记,这一行就处理正规模式;如果没有,这一行就处理非正规模式。

特殊控制字符

另外还有一些字符集合,例如Ctrl-C,当用户输入时会以特殊的方式运行。termios结构的c_cc数组成员包含映射到每一个支持函数的字符。每一个字符的位置(在数组中的索引)是由一个宏定义的,但是他们必须控制的字符并没有限制。

依据于终端是否设置为正规模式(例如,在termios的c_lfalg成员设置ICANON标记),c_cc数组以两种不同的方式来使用。

在这里我们要注意的是,两种不同模式的数组索引值所使用的方式有一某些重叠。正因为如此,我们绝不要混用这两种模式的值。

对于正规模式,数组索引为:

VEOF:EOF字符
VEOL:EOL字符
VERASE:ERASE字符
VINTR:INTR字符
VKILL:KILL字符
VQUIT:QUIT字符
VSUSP:SUSP字符
VSTART:START字符
VSTOP:STOP字符

对于非正规模式,数组索引为:

VINTR:INTR字符
VMIN:MIN值
VQUIT:QUIT字符
VSUSP:SUSP字符
VTIME:TIME值
VSTART:START字符
VSTOP:STOP字符

字符

因为特殊字符以及非正规字符MIN与TIME值对于高级输入字符的处理是如此重要,我们会在这里进行详细的解释。

字符        描述
INTR        使得终端驱动器向连接到终端的处理操作发送一个SIGINT信号。我们会在第11章更详细的讨论信息。
QUIT        使得终端驱动器向连接到终端的处理操作发送一个SIGQUIT信号。
ERASE        使得终端驱动器删除一行的最后一个字符。
KILL        使得终端驱动器删除整个行
EOF        使得终端驱动器将一行中的所有字符传递给正读取输入的程序。如果此行为空,read调用会返回零个字符,就如同read操作到达文件结尾时一样。
EOL        行结束符,与更为通常的新行字符相类似
SUSP        使得终端驱动器向连接到终端的操作发送一个SIGSUSP信号。如果我们的Unix系统支持工作控制,当前的程序就会被挂起。
STOP        阻止向终端发送更多的输出。他用于支持XON/XOFF流控制,通常设置为ASCII XOFF字符,Ctrl+S。
START        在STOP字符之后重启输出,通常为ASCII XON字符。

TIME与MIN值

TIME与MIN的值只用非正规模式,而且共同作用来控制输入的读取。同时,他们控制当程序试图读取与一个终端相关联的文件描述符时会发生什么。

有四种情况:

MIN=0同时TIME=0:在这种情况下,一个read调用会立即返回。如果某些字符可用,他们就会立即返回;如果没有可用字符,read会返回零并且不会读取任何字符。

MIN=0同时TIME>0:在这种情况下,当有任何可以读取的字符或是TIME的十分之一秒逝去时,read会返回。如果因为时间过期没有读取任何字符,read就会返回零。否则,他会返回读取的字符数。

MIN>0同时TIME=0:在这种情况下,read会等待直到有MIN个字符可以读取,然后返回读取的字符数。在文件结尾时会返回零。

MIN>0 同时TIME>0:这是最复杂的情况。当调用read时,他等待接收一个字符。当接收到第一个字符,以及在接下来的时间序列内接收到一个字符时,就 会启动一个中间字符(inter-character)计时器(如果他已经在运就重新启动)。当有MIN个字符可以读取或是中间字符计时器的TIME时间 值过去十分之一秒时,read会返回。这可以用于区分Escape按键的一下按下与一个函数键值转义序列的启动之间的区别。但要小心,网络通信或是高级处 理器会擦除时间信息。

通过设置非正规模式以及使用MIN与TIME值,程序可以执行一个字符一个字符的输入处理。

由Shell访问终端模式

如果我们要查看我们正使用的Shell所使用的termios设置,我们可以使用下面的命令来得到一个列表:

$ styy -a

在我们的Linux系统上,对标准的termios结构进行一些扩展,其输出如下:

speed 38400 baud; rows 44; columns 109; line = 0;
intr = ^C; quit = ^/; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 =
<undef>; start = ^Q; stop = ^S;
susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -
iuclc -ixany -imaxbel
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl
echoke

在这些输出之间,我们可以看到EOF字符为Ctrl+D并且允许回显。如果我们试验终端控制,我们可以很容易使得这个终端处理非标准状态,从而会使其使用非常困难。有几个方法可以做到这一点。

第一个方法是,如果我们的stty版本支持,我们可以使用下面的命令:

$ stty sane

如果我们失去了回车键到新行字符的映射,我们需要输入stty sane,但不是按回车,而是按下Ctrl+J(新行字符)。

第二个方法是使用stty -g命令将当前的stty设置存为可以重新读取的格式。在命令行中,我们可以使用下面的命令:

$ stty -g > save_stty
..
<experiment with settings>
..
$ stty $(cat save_stty)

对于最后一个stty命令我们仍然需要使用Ctrl+J而不是回车。我们要以在shell脚本中使用同样的技术:

save_stty=”$(stty -g)”
<alter stty settings>
stty $save_stty

第三个方法就是使用另一个不同的终端,使用ps命令来查看我们要使其成为不可用的shell,然后使用kill HUP <process id>来强制结束这个shell。因为stty参数总是在一个登陆提示出现之前进行设置的,所以我们可以正常登陆。

由命令行设置终端模式

我们也可以使用stty命令直接由命令行来设置终端模式。

要设置一个我们的shell脚本可以执行单个字符读取的模式,我们需要关闭正规模式,同时将MIN设置为1,而TIME设置为0。命令如下:

$ stty -icanon min 1 time 0

现在终端被设置为可以立即读取字符,我们可以试着运行我们的第一个程序。我们就会发现其运行情况正如我们所希望的那样。

我们也可以在提示输入密码之前关闭回显来输入密码。其命令如下:

$ stty -echo

一定要记住在我们试验之后一定要用命令stty echo来打开回显。

终端速率

termios结构所提供的最后一个函数可以操作线速率。并没有为终端速率定义成员;相反,他是由函数调用来设置的。输入与输出速率是分别处理的。

四个调用原型为:

#include <termios.h>
speed_t cfgetispeed(const struct termios *);
speed_t cfgetospeed(const struct termios *);
int cfsetispeed(struct termios *, speed_t speed);
int cfsetospeed(struct termios *, speed_t speed);

注 意,这些函数是作用在termios结构上的,而不是直接作用在端口上。这就意味着要设置一个新的速率,我们必须使用tcgetattr读取当前的设置, 使用上面函数调用中的一个来设置速率,然后使用tcsetattr写回termios结构。只有tcsetattr调用之后,线速率才会改变。

在上面的函数调用中允许各种速率值,其中最重要的为如下几个:

B0:挂起终端
B1200:1200波特
B2400:2400波特
B9600:9600波特
B19200:19200波特
B38400:38400波特

标准并没有定义大于38400的速率,对于大于这个速率的串口也没有相应的支持函数。

一 些系统,包括Linux,为选择更快的速率定义了B57600,B115200和B230400几个速率。如果我们正使用Linux的某个早期版本,而并 不可以使用这些常量,我们可以使用setserial命令来获得57600和115200等非标准速率。在这种情况下,当选择B38400时会使用这些速 率。这两个方法都是不可以移植的,所以我们使用时要小心。

其余的函数

对于终端控制还有一些其他的函数。这些函数直接作用在文件描述符上,而不需要读取与设置termios结构。他们的定义如下:

#include <termios.h>
int tcdrain(int fd);
int tcflow(int fd, int flowtype);
int tcflush(int fd, int in_out_selector);

这些函数的目的如下:

tcdrain会使得调用函数在所有的输出队列发送之前等待。
tcflow用于中止或是重启输出。
tcflush可以用于冲刷输入,输出或是两者。

现在我们已经讨论了关于termios结构相当多的主题内容,下面让我们来看一些实际的例子。也许最简单的就是在读取密码时禁止回显了。我们可以通过关闭ECHO标记来做到。

试验--使用termios读取密码

1 我们的密码程序,password.c,以下面的定义开始:

#include <termios.h>
#include <stdio.h>
#define PASSWORD_LEN 8
int main()
{
    struct termios initialrsettings, newrsettings;
    char password[PASSWORD_LEN + 1];

2 接下来,添加一行来由当前的标准输入读取当前的设置,并且将其拷贝到我们前面创建的termios结构中:

tcgetattr(fileno(stdin), &initialrsettings);

3 制作一份原始设置的拷贝来替换他们。在newrsettings中关闭ECHO标记,并且询问用户密码:

newrsettings = initialrsettings;
newrsettings.c_lflag &= ~ECHO;
printf(“Enter password: “);

4 然后将终端属性设置为newrsettings,并且读取密码。最后,将终端属性设置为其原始属性,并且打印密码来验证前面的效果。

  if(tcsetattr(fileno(stdin), TCSAFLUSH, &newrsettings) != 0) {
      fprintf(stderr,”Could not set attributes/n”);
  }
  else {
      fgets(password, PASSWORD_LEN, stdin);
      tcsetattr(fileno(stdin), TCSANOW, &initialrsettings);
      fprintf(stdout, “/nYou entered %s/n”, password);
  }
  exit(0);
}

工作原理

$ ./password
Enter password:
You entered hello
$

在这个例子中,在Enter password:提示之后输入hello,但是输入的字符并没有回显。直到用户按下Enter时才产生输出。

我们很小心的使用语句 X &= ~FLAG(清除相应的FLAG位)来改变我们需要改变的标记位。如果需要,我们可以使用X |= FLAG来设置由FLAG定义的位,虽然在我们上面的这个例子中并不需要这样。

当我们设置属性性时,我们使用TCSAFLUSH来忽略程序准备读取之前用户所输入的字符。这是使得用户在回显关闭之前不要输入密码的一个好办法。同时我们在程序结束之前恢复了先前的设置。

termios结构的另一个通常用法就是可以设置终端为一种我们可以立即读取用户输入字符的状态。我们可以通过关闭正规模式并且设置MIN与TIME的值来做到。

试验--读取每个字符

1 使用我们的新程序,我们可以对我们的菜单程序做出修改。下面的代码与pasword.c相类似,但是需要插入到menu3.c中来生成我们的新程序menu4.c。在开始这前,我们需要在程序顶部包含一个新的头文件:

#include <stdio.h>
#include <unistd.h>
#include <termios.h>

2 然后我们需要在main函数中定义一些新的变量:

int choice = 0;
FILE *input;
FILE *output;
struct termios initial_settings, new_settings;

3 我们需要在调用getchoice函数之前修改终端特点,这就是我们需要插入代码的地方:

    fprintf(stderr, “Unable to open /dev/tty/n”);
    exit(1);
}
tcgetattr(fileno(input),&initial_settings);
new_settings = initial_settings;
new_settings.c_lflag &= ~ICANON;
new_settings.c_lflag &= ~ECHO;
new_settings.c_cc[VMIN] = 1;
new_settings.c_cc[VTIME] = 0;
if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
    fprintf(stderr,”could not set attributes/n”);
}
    fprintf(stderr, “Unable to open /dev/tty/n”);
    exit(1);
}
tcgetattr(fileno(input),&initial_settings);
new_settings = initial_settings;
new_settings.c_lflag &= ~ICANON;
new_settings.c_lflag &= ~ECHO;
new_settings.c_cc[VMIN] = 1;
new_settings.c_cc[VTIME] = 0;
if(tcsetattr(fileno(input), TCSANOW, &new_settings) != 0) {
    fprintf(stderr,”could not set attributes/n”);
}

4 同时我们需要在程序结束之前恢复原始设置:

do {
    choice = getchoice(“Please select an action”, menu, input, output);
      printf(“You have chosen: %c/n”, choice);
  } while (choice != ‘q’);
  tcsetattr(fileno(input),TCSANOW,&initial_settings);
  exit(0);
}

5 现在我们需要检测回车/r以确保我们在非正规模式,因为不会再执行默认的CR到LF的映射:

do {
    selected = fgetc(in);
} while (selected == ‘/n’ || selected == ‘/r’);

6 不幸的是,如果此时用户在我们程序运行时按下Ctrl+C,程序就会终止。我们可以通过在本地模式中清除ISIG标记来禁止特殊字符的处理。在主函数中添加下面的代码行:

new_settings.c_lflag &= ~ISIG;

如果我们将这些修改加入到我们的程序中,我们现在就会得到一个立即响应并且输入不会回显朱程序:

$ ./menu4
Choice: Please select an action
a - add new record
d - delete record
q - quit
You have chosen: a
Choice: Please select an action
a - add new record
d - delete record
q - quit
You have chosen: q
$

如果我们按下Ctrl+C,他就会被直接传递给程序,并且将其看作是不正确的选择。

原创粉丝点击