Unix/Linux编程:终端编程

Unix/Linux编程:终端编程概述传统型终端和终端模拟器都需要同终端驱动程序相关联 由驱动程序负责处理设备上的输入和输出 如果是终端模拟器 这里的设备就是一个伪终端 当执行输入时 驱动程序可以工作在如下两种模式下规范模式 默认 在这种模式下 终

大家好,欢迎来到IT知识分享网。

概述

传统型终端和终端模拟器都需要同终端驱动程序相关联,由驱动程序负责处理设备上的输入和输出(如果是终端模拟器,这里的设备就是一个伪终端)

当执行输入时,驱动程序可以工作在如下两种模式下

  • 规范模式(默认):在这种模式下,终端的输入是按行来处理的,而且可以进行行编辑操作。每一行都由换行符来结束,当用户按下回车键可以产生换行符。在终端上执行的read()调用只会在一行输入完成只会才会返回,而且最多只会返回一行(如果read()请求的字节数少于当前行的可用字节,那么剩下的字节在下次read()调用时可用)。
  • 非规范模式:终端模式不会被装配成行。像 vi、more 和 less 这样的程序会将终端置于非规范模式,这样不需要用户按下回车键它们就能读取到单个的字符了。

终端驱动程序也能对一系列的特殊字符做解释,比如中断字符(通常为 Ctrl-C)以及文件结尾符(通常是 Ctrl-D)。将终端置于非规范模式下的程序会禁止处理某些或者所有这些特殊字符

终端驱动程序会对两个队列进行操作,如下图:一个用于从终端设备将输入字符传送到读取进程上,另一个用于将输出字符从进程传送到终端上。如果开启了终端回显功能,那么终端驱动程序会自动将任意的输入字符插入到输出队列的尾部,这样输入字符也会成为终端的输出。

在这里插入图片描述

  • SUSv3 规定了 MAX_INPUT 上限,在实现中可用来表示终端输入队列的最大长度。还有一个相关的上限 MAX_CANON,定义了处于规范模式下一行输入的最大字节数。在 Linux上sysconf(_SC_MAX_INPUT)和 sysconf(_SC_MAX_CANON)都会返回 255。但是,内核实际上并不会采用这些限制,而只是简单地在输入队列上加上了 4096 字节的限制。相对的,输出队列上也有一个这样的限制。然而应用程序不需要关心这些限制,这是因为如果一个进程产生输出的速度比终端驱动程序处理的速度还要快的话,内核会暂停执行写进程,直到输出队列的空间再次可用为止。
  • 在 Linux 上,我们通过调用 ioctl(fd, FIONREAD, &cnt)来获取终端输入队列中的未读取字节数,文件描述符 fd 指向的就是终端。这个特性在 SUSv3 中并没有规定。

获取和修改终端属性

函数 tcgetattr()和 tcsetattr()可以用来获取和修改终端的属性。

SYNOPSIS #include <termios.h> #include <unistd.h> int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int optional_actions, const struct termios *termios_p); 

参数fd是指向终端的文件描述符(如果fd不指向终端,调用这些函数就会失败,伴随错误码ENOTTY)

参数termios_p是一个指向结构体termios 的指针,用来记录终端的各项属性

#define NCCS 32 struct termios { 
    tcflag_t c_iflag; /* input mode flags */ tcflag_t c_oflag; /* output mode flags */ tcflag_t c_cflag; /* control mode flags */ tcflag_t c_lflag; /* local mode flags */ cc_t c_line; /* line discipline */ cc_t c_cc[NCCS]; /* control characters */ speed_t c_ispeed; /* input speed */ speed_t c_ospeed; /* output speed */ #define _HAVE_STRUCT_TERMIOS_C_ISPEED 1 #define _HAVE_STRUCT_TERMIOS_C_OSPEED 1 }; 

结构体 termios 中的前 4 个字段都是位掩码(数据类型 tcflag_t 是合适大小的整数类型),包含有可控制终端驱动程序各方面操作的标志。

  • c_iflag 包含控制终端输入的标志。
  • c_oflag 包含控制终端输出的标志。
  • c_cflag 包含与终端线速的硬件控制相关的标志。
  • c_lflag 包含控制终端输入的用户界面的标志。

c_line 字段指定了终端的行规程(line discipline)。为了达到对终端模拟器编程的目的,行规格将一直设为N_TTY,也就是所谓的新规程。这是内核中处理终端的代码中的一个组件,实现了规范模式下的IO处理。行规格的设定同串口编程有关

数组c_cc包含着终端的特殊字符,以及用来控制非规范模式下输入操作的相关字段。

c_ispeed 和 c_ospeed 字段在 Linux 上没有使用到(并且也没有在 SUSv3 中规定)。

当通过tcsetattr来修改终端属性时,参数optional_actions用来确定何时这些修改将生效,该参数可以被指定为如下值的一种:

  • TCSANOW :修改立刻得到生效
  • TCSADRAIN :当所有当前处于排队中的输出已经传送到终端之后,修改得到生效。通常,该标志应该在修改影响终端的输出时才会指定,这样我们就不会影响到已经处于排队中、但还没有显示出来的输出数据。
  • TCSAFLUSH :该标志的产生的效果同 TCSADRAIN,但是除此之外,当标志生效时那些仍然等待处理的输入数据都会被丢弃。这个特性很有用,比如,当读取一个密码时,此时我们希望关闭终端回显功能,并防止用户提前输入

通常(也是推荐做法)修改终端属性的方法是调用 tcgetattr()来获取一个包含有当前设定的 termios 结构体,然后调用 tcsetattr()将更新后的结构体传回给驱动程序。(这种方法可确保我们传递给 tcsetattr()的是一个完全初始化过的结构体。)例如,我们可以采用下列代码将终端的回显功能关闭

struct termios tp; if(tcpgetattr(STDIN_FILENO, &tp) == -1){ 
    exit(1); } tp.c_lflag &= ~ECHO; if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1){ 
    exit(1); } 

如果任何一个对终端属性的修改请求可以执行的话,函数tcsetattr()将返回成功;它只会在没有任何修改请求能执行时才会返回失败。这意味着当我们修改多个属性时,有时可能有必要再调用一次 tcgetattr()来获取新的终端属性,并同之前的修改请求做对比

stty命令

stty命令是以命令行的形式来模拟tcgetaddr和tcsetaddr的功能,允许我们在shell上检视和修改其属性。当我们监视、调试或者取消程序修改的终端属性时,这个工具非常有用。

我们可以采用如下的命令检视所有终端的当前属性(这里是在一个虚拟控制台上执行的)。
在这里插入图片描述

  • 上述输出的第一行显示出了终端的线速(比特每秒)、终端的窗口大小以及以数值形式给出的行规程(0 代表 N_TTY,即新行规程)。
  • 接下来的 3 行显示出了有关各种终端特殊字符的设定。^C 表示 Ctrl-C,以此类推。字符
    串< undef>表示相应的终端特殊字符目前没有定义。min和time的值与非规范模式下的输入有关
  • 剩下的几行显示出了 termios 结构体中 c_cflag、c_iflag、c_oflag 以及 c_lflag 字段中各个
    标志的设定(按顺序显示)。这里的标志名前带有一个连字符(-)的表示目前被禁用,否则表
    示当前已设定。

如果输入命令时不加任何命令行参数,那么 stty 只会显示出线速、行规程以及任何其他偏离了正常值的设定。

终端特殊字符

下表列出了 Linux 终端驱动程序所能识别的特殊字符。

  • 前两列显示了字符的名称以及对应在 c_cc 数组中用作下标的常量值。(可以看到,这些常量只是简单地在字符名前加上了 V作为前缀。)CR 和 NL 字符没有对应的 c_cc 下标,因为这些字符的值不能改变。
  • 默认设定这一列显示了特殊字符通常的默认值。除了能够将终端特殊字符设定为指定值之外,还可以通过将该值设定为fpathconf(fd, _PC_VDISABLE)的返回值来关闭该字符。
  • 每个特殊字符的操作受 termios 结构体位掩码字段中的各种标志设定的影响,请参见表格中倒数第 2 列。
字 符 c_cc 下标 描 述 默 认 设 定 相关的位掩码标志 SUSv3
CR (无) 回车 ^M ICANON、IGNCR、ICRNL、OPOST、OCRNL、ONOCR
DISCARD VDISCARD 丢弃输出 ^O (未实现
EOF VEOF 文件结尾 ^D ICANON
EOL VEOL 行结尾 ICANON
EOL2 VEOL2 另一种行结尾 ICANON,IEXTEN
ERASE VERASE 擦除字符 ^? ICANON
INTR VINTR 中断(SIGINT) ^C ISIG
KILL VKILL 擦除一行 ^U ICANON
NL (无) 换行 ^J ICANON、INLCR、ECHONL、OPOST、ONLCR、ONLRET
QUIT VQUIT 退出(SIGQUIT) ^\ ISIG
REPRINT VREPRINT 重新打印输入行 ^R ICANON、IEXTEN、ECHO
START VSTART 开始输出 ^Q IXON、IXOFF
STOP VSTOP 停止输出 ^S IXON、IXOFF
SUSP VSUSP 暂停(SIGTSTP) ^Z ISIG
WERASE VWERASE 擦除一个字 ^W ICANON、IEXTEN

CR

CR是回车符。这个字符会传递给正在读取输入的进程。在默认设定了ICRNL标志(在输入中)将 CR 映射为 NL)的规范模式下(设定 ICANON 标志),这个字符首先被转换为一个换行符,然后再传递给读取输入的进程。如果设定了IGNCR(忽略CR标志),那么就在输入上忽略这个字符(此时必须用真正的换行符来作为一行的结束)。输出一个CR字符将导致终端将光标移到一行的开始处

DISCARD

DISCARD是丢弃输出字符。尽管这个字符定义在了数组c_cc中,但实际上在Linux上没有任何效果。在一些其他的Unix实现中,一旦输入这个字符将导致程序输出被丢弃。这个字符就像一个开关—-再输入一次将重新打开输出显示。当程序产生大量输出而我们希望略过其中一些输出时这个功能就非常有用。(在传统的终端上这个功能更加有用,因为此时线速会更加缓慢,而且也不存在什么其他的“终端窗口”。)这个字符不会发送给读取进程。

EOF

EOF 是传统模式下的文件结尾字符(通常是 Ctrl-D)。在一行的开始处输入这个字符会导致在终端上读取输入的进程检测到文件结尾的情况(即,read()返回 0)。如果不在一行的开始处,而在其他地方输入这个字符,那么该字符会立刻导致 read()完成调用,返回这一行中目前为止读取到的字符数。在这两种情况下,EOF 字符本身都不会传递给读取的进程

EOL 以及 EOL2

EOL 和 EOL2 是附加的行分隔字符,对于规范模式下的输入,其表现就如同换行(NL)符一样,用来终止一行输入并使该行对读取进程可见。默认情况下,这些字符是未定义的。如果定义了它们,它们是会被发送给读取进程的。EOL2 字符只有当设置了 IEXTEN(扩展输入处理)标志时(默认会设置)才能工作

用到这些字符的机会很少。一种应用是在 telnet 中。通过将 EOL 或 EOL2 设定为 telnet 的换码符(通常是 Ctrl-],或者如果工作在 rlogin 模式下时为~),telnet 能立刻捕获到字符,就算是正在规范模式下读取输入时也是如此。

ERASE

在规范模式下,输入 ERASE 字符会擦除当前行中前一个输入的字符。被擦除的字符以及ERASE 字符本身都不会传递给读取输入的进程

INTR

INTR 是中断字符。如果设置了 ISIG (开启信号)标志(默认会设置),输入这个字符会产生一个中断信号(SIGINT),并发送给终端的前台进程组。INTR 字符本身是不会发送给读取输入的进程的

KILL

KILL 是擦除行(也称为 kill line)字符。在规范模式下,输入这个字符使得当前这行输入被丢弃(即,到目前为止输入的字符连同 KILL 字符本身,都不会传递给读取输入的进程了)

LNEXT

LNEXT是下一个字符的字面化表示(literal next)。在某些情况下,我们可能希望将终端特殊字符的其他一个看作一个普通字符,将其作为输入传递给读取进程。输入 LNEXT 字符后(通常是 Ctrl-V)使得下一个字符将以字面形式来处理,避免终端驱动程序执行任何针对特殊字符的解释处理。因而,我们可以输入 Ctrl-V Ctrl-C 这样的 2 字符序列,提供一个真正的Ctrl-C 字符(ASCII 码为 3)作为输入传递给读取进程。LNEXT 字符本身并不会传递给读取进程。这个字符只有在设定了 IEXTEN 标志(默认会设置)的规范模式下才会被解释

NL

NL是换行符。在规范模式下,该字符终结一行输入。NL字符本身是会包含在行中返回给读取进程的(规范模式下,CR 字符通常会转换为 NL。)输出一个 NL 字符导致终端将光标移动到下一行。如果设置了 OPOST 和 ONLCR(将 NL 映射为 CR-NL)标志(默认会设置),那么在输出中,一个换行符就会映射为一个 2 字符序列—CR 加上 NL。(同时设定 ICRNL 和ONLCR 标志意味着一个输入的 CR 字符会转换为 NL,然后回显为 CR 加上 NL。

QUIT

如果设置了 ISIG 标志(默认会设置),输入 QUIT 字符会产生一个退出信(SIGQUIT),并发送到终端的前台进程组中。QUIT 字符本身并不会传递给读取进程

REPRINT

REPRINT字符代表重新打印输入。在规范模式下,如果设置了IEXTEN标志(默认会设置),输入该字符会使得当前的输入行(还没有输入完全)重新显示在终端上。如果某个其他的程序(例如 wall(1)或者 write(1))输出已经使终端的显示变得混乱不堪,那么此时这个功能就特别有用了。REPRINT 字符本身是不会传递给读取进程的

START和STOP

START 和 STOP 分别代表开始输出和停止输出字符。当设定了 IXON (启动开始/停止输出控制)标志时(默认会设定),这两个字符才能工作。(START 和 STOP 字符在一些终端模拟器中不会生效。

输入STOP字符会暂停终端输出。STOP字符本身不会传递给读取进程。如果设定了IXOFF标志,而且终端的输入队列已满,那么终端驱动程序会自动发送一个STOP字符来对输入进行字节流控制

输入START字符会使得之前由STOP暂停的终端输出得到恢复。START 字符本身不会传递给读取进程。如果设定了 IXOFF(启动开始/停止输入控制)标志(默认是不会设定的),且终端驱动程序之前由于输入队列已满已经发送过了一个 STOP 字符,那么一旦当输入队列中又有了空间,此时终端驱动程序会自动发送一个 START 字符以恢复输出

如果设定了 IXANY 标志,那么任何字符,不仅仅只是 START,都可以按顺序输入以重启输出(同样,这个字符也不会传递给读取进程)

SUSP

SUSP 代表暂停字符。如果设定了 ISIG 标志(默认会设定),输入这个字符会产生终端暂停信号(SIGTSTP),并发送给终端的前台进程组。SUSP 字符本身不会发送给读取进程

WERASE

WERASE 是擦除单词字符。在规范模式下,设定了 IEXTEN 标志(默认会设定)后输入这个字符会擦除前一个单词的所有字符。一个单词被看做是一串字符序列,可包含数字和下划线。(在某些 UNIX 实现中,单词被看做是由空格分隔的字符序列。

看个例子

#include <termio.h> #include <ctype.h> //修改终端的中断字符 int main(int argc, char *argv[]) { 
    struct termios tp; int intrChar; if (argc > 1 && strcmp(argv[1], "--help") == 0){ 
    printf("%s [intr-char]\n", argv[0]); exit(EXIT_FAILURE); } /* Determine new INTR setting from command line */ if (argc == 1) { 
    /* Disable */ intrChar = fpathconf(STDIN_FILENO, _PC_VDISABLE); if (intrChar == -1){ 
    printf("Couldn't determine VDISABLE"); exit(EXIT_FAILURE); } } else if (isdigit((unsigned char) argv[1][0])) { 
    intrChar = strtoul(argv[1], NULL, 0); /* Allows hex, octal */ } else { 
    /* Literal character */ intrChar = argv[1][0]; } /*获取当前终端设置,修改 INTR 字符,并将更改推送回终端驱动程序 */ if (tcgetattr(STDIN_FILENO, &tp) == -1){ 
    printf("tcgetattr"); exit(EXIT_FAILURE); } tp.c_cc[VINTR] = intrChar; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1){ 
    printf("tcsetattr"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } 

将中断字符设为 Ctrl-L(ASCII 码为 12),然后通过 stty 命令对修改做验证
在这里插入图片描述
之后我们启动一个进程,运行 sleep(1)。我们发现输入 Ctrl-C(ASCII 码为 3) 已经不会产生终止进程的效果了,而输入 Ctrl-L 才会终止进程
在这里插入图片描述
现在我们显示出 shell 变量$?的值,该值会给出上一条命令的终止状态
在这里插入图片描述
我们看到进程终止的状态为 130。这表示该进程由信号 130 – 128 = 2 来杀死,而信号 2正是 SIGINT。

接下来我们通过该程序来关闭中断字符
在这里插入图片描述
现在我们发现无论是 Ctrl-C 还是 Ctrl-L 都不会产生 SIGINT 信号了,我们必须使用 Ctrl-\来终止这个进程
在这里插入图片描述

终端标志

下表列出了 termios 结构体中 4 个标志字段所控制的设置。

字段/标志 描 述 默 认
c_iflag
BRKINT 在 BREAK 状态下发出信号中断(SIGINT) 打开
ICRNL 在输入中将 CR 映射为 NL 打开
IGNBRK 忽略 BREAK 状态 关闭
IGNCR 在输入中忽略 CR 关闭
IGNPAR 忽略有奇偶校验错误的字符 关闭
IMAXBEL 终端输入队列满时发出铃响(未使用) (打开)
INLCR 在输入中将 NL 映射为 CR 关闭
INPCK 开启输入奇偶校验检查 关闭
ISTRIP 从输入字符中去掉最高位(bit 8) 关闭
IUTF8 输入为 UTF-8 编码(从 Linux 2.6.4 开始) 关闭
IUCLC 在输入中将大写字符映射为小写字符(如果 IEXTEN 也同时设置的话) 关闭
IXANY 允许用任意字符来重启已停止的输出 关闭
IXOFF 启动开始/停止输入流控 关闭
IXON 启动开始/停止输出流控 打开
PARMRK 标记奇偶校验错误(带有两个前缀字节:0377 + 0) 关闭

在这里插入图片描述
在这里插入图片描述

BRKINT

  • 如果设定了 BRKINT,且没有设定 IGNBRK 标志,那么当出现 BREAK 状态时会发送SIGINT 信号到前台进程组
  • 在许多 UNIX系统中,BREAK 状态就表现为一个发送给远端主机的信号,用来将线速(波特率)调整为适合于终端的数值。
  • 在虚拟控制台上,我们可以通过按下 Ctrl-Break 来产生一个 BREAK 状态。

ECHO

  • 设置了 ECHO 标志将开启回显输入字符的功能。ECHO 标记在规范和非规范模式下都是有效的。
  • 当读取密码时,禁止回显是很有用的
  • vi 的命令模式下回显也是被禁止的,此时由键盘产生的字符被解释为编辑命令而不是文本输入。

ECHOCTL

  • 如果设置了 ECHO 标志,那么开启 ECHOCTL 标志会导致除了制表符、换行符、START 和STOP 之外的控制字符都将以类似^A(Ctrl-A)的形式回显出来。
  • 如果关闭 ECHOCTL 标志,控制字符将不再回显

ECHOE

  • 在规范模式下,设定ECHOE标识使得ERASE能以可视化的可视化执行,将退格-空格-退格格这样的序列输出到终端上。
  • 如果关闭了 ECHOE 标志,那么 ERASE 字符本身就会回显出来(例如以^?的形式),但仍然会完成删除一个字符的功能

ECHOK和ECHOKE

  • ECHOK 和 ECHOKE 标志控制着在规范模式下使用 KILL(擦除行)字符时的可视化显示。
  • 在默认情况下(同时设置两个标志),一行文本以可视化的方式擦除(参见ECHOE)。
  • 如果其中任一标志被关闭,那么就不会执行可视化的擦除(但输入行仍然会被丢弃),而 KILL 字符本身会被回显出来(例如以^U 的形式)。
  • 如果设定了 ECHOK 而关闭了 ECHOKE,那么也会输出一个换行符。

ICANON

  • 设定了 ICANON 标志将启动规范模式输入。
  • 输入会集中成行,并且会打开对特殊字符EOF、EOL、EOL2、ERASE、LNEXT、KILL、REPRINT 以及 WERASE 的解释处理(但需要注意下面描述到的 IEXTEN 标志所产生的效果)。

IEXTEN

  • 设定 IEXTEN 标志将打开对输入字符的扩展处理功能。
  • 必须设定这个标志(同 ICANON 一样),才能正确解释 EOL2、LNEXT、REPRINT 以及 WERASE 这样的特殊字符。
  • 要使 IUCLC标志生效,也必须要设定 IEXTEN 标志才行

IMAXBEL

  • Linux 上忽略了 IMAXBEL 标志的设定。
  • 在登录控制台上,当输入队列已满时总是会响起响铃声。

IUTF8

设定 IUTF8 标志将打开加工模式(cooked mode),以此当执行行编辑时能够正确地处理 UTF-8 输入。

NOFLSH

  • 默认情况下,当输入 INTR、QUIT 或 SUSP 字符而产生信号时,任何在终端输入和输出队列中未处理完的数据都会被刷新(丢弃)。
  • 设定 NOFLSH 标志后将关闭这种刷新行为。

OPOST

  • 设定 OPOST 标志后将打开输出的后续处理功能。
  • 必须设定该标志才能使 termios 结构体中c_oflag 字段中的标志生效。(相反,关闭 OPOST 标志将禁止对所有的输出做后续处理。)

PARENB、IGNPAR、INPCK、PARMRK 以及 PARODD

PARENB、IGNPAR、INPCK、PARMRK 以及 PARODD 标志同奇偶校验生成和检查有关。

  • PARENB 标志可为输出字符打开奇偶校验位,并为输入字符做奇偶校验检查。
  • 如果我们只希望生成输出的奇偶校验,那么我们可以通过关闭 INPCK 标志来禁止对输入做奇偶校验检查。
  • 如果设定了 PARODD 标志,那么在输入和输出上都会采用奇数奇偶校验,否则就会采用偶数奇偶校验。

剩下的标志规定了当输入字符出现奇偶校验错误时应该如何处理。

  • 如果设定了 IGNPAR标志,那么字符将被丢弃(不会传递给读取进程)。
  • 否则,如果设定了 PARMRK 标志,那么该字符会传递给读取进程,但会在前面加上 2 字节的序列 0377 + 0。(如果设定了 PARMRK标志,但关闭了 ISTRIP 标志,那么字符 0377 会加倍成 0377 + 0377。)
  • 如果关闭 PARMRK 标志,但设定了 INPCK 标志,那么字符被丢弃,且不会传递给读取进程任何字节。
  • 如果 IGNPAR、PARMRK 或 INPCK 都没有设定,那么该字符会传递给读取进程。

例子:关闭终端回显功能

 #include <termio.h> #include <ctype.h> //修改终端的中断字符 #define BUF_SIZE 100 int main(int argc, char *argv[]) { 
    struct termios tp, save; char buf[BUF_SIZE]; /* Retrieve current terminal settings, turn echoing off */ if (tcgetattr(STDIN_FILENO, &tp) == -1){ 
    printf("tcgetattr"); exit(EXIT_FAILURE); } save = tp; /* So we can restore settings later */ tp.c_lflag &= ~ECHO; /* ECHO off, other bits unchanged */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tp) == -1){ 
    printf("tcsetattr"); exit(EXIT_FAILURE); } /* Read some input and then display it back to the user */ printf("Enter text: "); fflush(stdout); if (fgets(buf, BUF_SIZE, stdin) == NULL) printf("Got end-of-file/error on fgets()\n"); else printf("\nRead: %s", buf); /* Restore original terminal settings */ if (tcsetattr(STDIN_FILENO, TCSANOW, &save) == -1){ 
    printf("tcsetattr"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } 

在这里插入图片描述
在这里插入图片描述

终端的IO模式

规范模式

我们可以通过设定ICANON标志来打开规范模式输入。可以通过如下几点开区分是否为规范模式下的输入

  • 输入被装配成行,通过如下几种行结束符来终结:NL、EOL、EOL2(如果设定了IEXTEN 标志)、EOF(除了一行中的初始位置)或者 CR(如果打开了 ICRNL 标志)除了 EOF 之外,其他的行结束符都会传递给读取的进程(作为一行中的最后一个字符)。
  • 打开了行编辑功能,这样可以修改当前行中的输入。因此,下列字符是可用的:ERASE、KILL。如果设定了 IEXTEN 标志的话,WERASE 也是可用的。
  • 如果设定了 IEXTEN 标志,则 REPRINT 和 LNEXT 字符也都是可用的。

在规范模式下,当存在有一行完整的输入时,终端上的read()调用才会返回。(如果请求的字节数比一行中所包含的字节小,那么 read()只会获取到该行的一部分。剩余的字节只有在后序的 read()调用中取得。)如果 read()调用被信号处理例程中断,且该信号没有系统调用重启,此时 read()也会终止执行

非规范模式

一样应用程序比如vi、less在用户没有提供行终止符时也需要从终端中读取字符。非规范模式正式用于这个目的。在非规范模式下(关闭 ICANON 标志)不会处理特殊的输入。特别的一点是:输入不再装配成行,相反会立即对程序可见。

在什么情况下一个非规范模式下的read()调用会完成?我们可以指定非规范模式下的read()调用在经历了一段特定的时间后,或者在读取了特定数量的字节后,又或者是两者兼有的情况下终止执行。termios结构体中的c_cc数组里有两个元素可用于决定这种轻微:TIME和MIN。元素 TIME(通过常量 VTIME 来索引)以十分之一秒为单位来指定超时时间。元素MIN(通过 VMIN 来索引)指定了被读取字节数的最小值。(MIN 和 TIME 的设置对规范模式下的终端 I/O 不产生任何影响。

加工模式、cbreak模式以及原始模式

在这里插入图片描述
加工模式本质上就是带有处理默认特殊字符功能的规范模式(可以对 CR、NL 和 EOF进行解释;打开行编辑功能;处理可产生信号的字符;设定 ICRNL、OCRNL 标志等)。

原始模式则恰好相反,它属于非规范模式,所有的输入和输出都不能做任何处理,而且不能回显。(如果应用程序需要确保终端驱动程序绝对不会对传输的数据做任何修改,那就应该使用这种模式。

cbreak 模式处于加工模式和原始模式之间。输入是按照非规范的方式来处理的,但产生信号的字符会被解释,且仍然会出现各种输入和输出的转换(取决于个别标志的设定)。cbreak模式并不会禁止回显,但采用这种模式的应用程序通常都会禁止回显功能。cbreak 模式在与屏幕处理相关的应用程序中很有用(比如 less),这类程序允许逐个字符的输入,但仍然需要对 INTR、QUIT 以及 SUSP 这样的字符做解释。

例子:将终端切换到cbreak和原始模式中

#include <termio.h> #include <ctype.h> /* Place terminal referred to by 'fd' in cbreak mode (noncanonical mode with echoing turned off). This function assumes that the terminal is currently in cooked mode (i.e., we shouldn't call it if the terminal is currently in raw mode, since it does not undo all of the changes made by the ttySetRaw() function below). Return 0 on success, or -1 on error. If 'prevTermios' is non-NULL, then use the buffer to which it points to return the previous terminal settings. */ int ttySetCbreak(int fd, struct termios *prevTermios) { 
    struct termios t; if (tcgetattr(fd, &t) == -1) return -1; if (prevTermios != NULL) *prevTermios = t; t.c_lflag &= ~(ICANON | ECHO); t.c_lflag |= ISIG; t.c_iflag &= ~ICRNL; t.c_cc[VMIN] = 1; /* Character-at-a-time input */ t.c_cc[VTIME] = 0; /* with blocking */ if (tcsetattr(fd, TCSAFLUSH, &t) == -1) return -1; return 0; } /* Place terminal referred to by 'fd' in raw mode (noncanonical mode with all input and output processing disabled). Return 0 on success, or -1 on error. If 'prevTermios' is non-NULL, then use the buffer to which it points to return the previous terminal settings. */ int ttySetRaw(int fd, struct termios *prevTermios) { 
    struct termios t; if (tcgetattr(fd, &t) == -1) return -1; if (prevTermios != NULL) *prevTermios = t; t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO); /* Noncanonical mode, disable signals, extended input processing, and echoing */ t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR | INPCK | ISTRIP | IXON | PARMRK); /* Disable special handling of CR, NL, and BREAK. No 8th-bit stripping or parity error handling. Disable START/STOP output flow control. */ t.c_oflag &= ~OPOST; /* Disable all output processing */ t.c_cc[VMIN] = 1; /* Character-at-a-time input */ t.c_cc[VTIME] = 0; /* with blocking */ if (tcsetattr(fd, TCSAFLUSH, &t) == -1) return -1; return 0; } 
#include <signal.h> #include <ctype.h> #include <fcntl.h> #include <termios.h> static volatile sig_atomic_t gotSigio = 0; static void sigioHandler(int sig){ 
    gotSigio = 1; } int ttySetCbreak(int fd, struct termios *prevTermios) { 
    struct termios t; if (tcgetattr(fd, &t) == -1) return -1; if (prevTermios != NULL) *prevTermios = t; t.c_lflag &= ~(ICANON | ECHO); t.c_lflag |= ISIG; t.c_iflag &= ~ICRNL; t.c_cc[VMIN] = 1; /* Character-at-a-time input */ t.c_cc[VTIME] = 0; /* with blocking */ if (tcsetattr(fd, TCSAFLUSH, &t) == -1) return -1; return 0; } int main(int argc, char *argv[]){ 
    int flags, j, cnt; struct termios origTermios; char ch; struct sigaction sa; bool done; /* 为 "I/O possible" 信号建立处理程序 */ sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = sigioHandler; if (sigaction(SIGIO, &sa, NULL) == -1){ 
    perror("sigaction"); exit(EXIT_FAILURE); } /* 设置接收"I/O possible" 信号的所有者进程 */ if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1){ 
    perror("fcntl F_SETOWN"); exit(EXIT_FAILURE); } /* 启用“I/Opossible”信号并使文件描述符的I/O非阻塞 */ flags = fcntl(STDIN_FILENO, F_GETFL); if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) { 
    perror("fcntl F_SETFL"); exit(EXIT_FAILURE); } if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1) { 
    perror("ttySetCbreak"); exit(EXIT_FAILURE); } for (done = false, cnt = 0; !done ; cnt++) { 
    for (j = 0; j < ; j++) continue; /* Slow main loop down a little */ if (gotSigio) { 
    /* Is input available? */ gotSigio = 0; /* Read all available input until error (probably EAGAIN) or EOF (not actually possible in cbreak mode) or a hash (#) character is read */ while (read(STDIN_FILENO, &ch, 1) > 0 && !done) { 
    printf("cnt=%d; read %c\n", cnt, ch); done = ch == '#'; } } } /* Restore original terminal settings */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1) { 
    perror("tcsetattr TCSAFLUSH"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } 
static struct termios userTermios; //用户定义的终端设置 static void handler(int sig){ 
    // 一般处理程序:恢复 tty 设置并退出 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1) { 
    perror("tcsetattr"); exit(EXIT_FAILURE); } _exit(EXIT_SUCCESS); } /* SIGTSTP与SIGSTOP都是使进程暂停(都使用SIGCONT让进程重新激活)。唯一的区别是SIGSTOP不可以捕获。 捕捉SIGTSTP后一般处理如下: 1)处理完额外的事 2)恢复默认处理 3)发送SIGTSTP信号给自己。(使进程进入suspend状态。) * */ static void tstpHandler(int sig) /* Handler for SIGTSTP */ { 
    struct termios ourTermios; /* To save our tty settings */ sigset_t tstpMask, prevMask; struct sigaction sa; int savedErrno; savedErrno = errno; /* We might change 'errno' here */ /* Save current terminal settings, restore terminal to state at time of program startup */ if (tcgetattr(STDIN_FILENO, &ourTermios) == -1){ 
    perror("tcgetattr"); exit(EXIT_FAILURE); } if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1){ 
    perror("tcsetattr"); exit(EXIT_FAILURE); } /* Set the disposition of SIGTSTP to the default, raise the signal once more, and then unblock it so that we actually stop */ if (signal(SIGTSTP, SIG_DFL) == SIG_ERR) { 
    perror("signal"); exit(EXIT_FAILURE); } raise(SIGTSTP); sigemptyset(&tstpMask); sigaddset(&tstpMask, SIGTSTP); if (sigprocmask(SIG_UNBLOCK, &tstpMask, &prevMask) == -1) { 
    perror("sigprocmask"); exit(EXIT_FAILURE); } /* Execution resumes here after SIGCONT */ if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1){ 
    perror("sigprocmask"); exit(EXIT_FAILURE); } sigemptyset(&sa.sa_mask); /* Reestablish handler */ sa.sa_flags = SA_RESTART; sa.sa_handler = tstpHandler; if (sigaction(SIGTSTP, &sa, NULL) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } /* The user may have changed the terminal settings while we were stopped; save the settings so we can restore them later */ if (tcgetattr(STDIN_FILENO, &userTermios) == -1) { 
    perror("tcsetattr"); exit(EXIT_FAILURE); } /* Restore our terminal settings */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &ourTermios) == -1){ 
    perror("tcsetattr"); exit(EXIT_FAILURE); } errno = savedErrno; } int main(int argc, char *argv[]) { 
    char ch; struct sigaction sa, prev; ssize_t n; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (argc > 1) { 
    /* Use cbreak mode */ // 将终端设为 cbreak 模式。以前的终端设置都保存在全局变量 userTermios 中 if (ttySetCbreak(STDIN_FILENO, &userTermios) == -1) { 
    perror("ttySetCbreak"); exit(EXIT_FAILURE); } /* 终端特殊字符可以在 cbreak 模式下生成信号。 抓住它们,以便我们可以调整终端模式。 * 只有当信号没有被忽略时,我们才建立处理程序。 */ sa.sa_handler = handler; if (sigaction(SIGQUIT, NULL, &prev) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } if (prev.sa_handler != SIG_IGN) if (sigaction(SIGQUIT, &sa, NULL) == -1){ 
    perror("sigaction"); exit(EXIT_FAILURE); } if (sigaction(SIGINT, NULL, &prev) == -1){ 
    perror("sigaction"); exit(EXIT_FAILURE); } if (prev.sa_handler != SIG_IGN) if (sigaction(SIGINT, &sa, NULL) == -1){ 
    perror("sigaction"); exit(EXIT_FAILURE); } sa.sa_handler = tstpHandler; if (sigaction(SIGTSTP, NULL, &prev) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } if (prev.sa_handler != SIG_IGN) if (sigaction(SIGTSTP, &sa, NULL) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } } else { 
    /* Use raw mode */ if (ttySetRaw(STDIN_FILENO, &userTermios) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } } //为信号 SIGTERM 安装处理例程,这是为了捕获由 kill 命令默认发送的信号。 sa.sa_handler = handler; if (sigaction(SIGTERM, &sa, NULL) == -1) { 
    perror("sigaction"); exit(EXIT_FAILURE); } setbuf(stdout, NULL); /* Disable stdout buffering */ //执行一个循环,从标准输入(stdin)上一次读取一个字符,并在标准输出上回显。 for (;;) { 
    /* Read and echo stdin */ n = read(STDIN_FILENO, &ch, 1); if (n == -1) { 
    perror("read"); break; } if (n == 0) /* Can occur after terminal disconnect */ break; if (isalpha((unsigned char) ch)) /* 在输出之前将所有的字符转换为小写形式。 */ putchar(tolower((unsigned char) ch)); else if (ch == '\n' || ch == '\r') putchar(ch); //换行符(\n)和回车符(\r)不做任何修改就直接回显 else if (iscntrl((unsigned char) ch)) printf("^%c", ch ^ 64); /* Echo Control-A as ^A, etc. */ else putchar('*'); /* All other chars as '*' */ if (ch == 'q') /* Quit loop */ break; } if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &userTermios) == -1) { 
    perror("tcsetattr"); exit(EXIT_FAILURE); } exit(EXIT_SUCCESS); } 

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/150976.html

(0)
上一篇 2025-03-16 12:26
下一篇 2025-03-16 12:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信