1、所请求的字节数已读到时。此时无需读一个完整的行,如果读了部分行,也不会丢失任何信息,下一次读会从前一次读的停止处开始。
2、当读到一个行定界符时。在 终端特殊输入字符一节中已了解到,下列字符在规范模式中会被解释为“行结束”:NL、EOL、EOL2 和 EOF,还有如若已设置 ICRNL,但未设置 IGNCR,则 CR 字符的作用与 NL 字符一样,也会终止一行。
3、如果捕捉到信号,并且该函数不再自动重启,则读也会返回。
下面来看看 getpass 函数在 UNIX 系统中的一个典型实现。此函数由 login(1) 和 crypt(1) 程序调用。它读入用户在终端上键入的口令,它关闭了回显,但仍以规范模式工作,因为不管键入什么口令都能构成一个完整行。
#include#include #include #define MAX_PASS_LEN 8 // max #chars for user to enter char *getpass(const char *prompt){ static char buf[MAX_PASS_LEN+1]; // null byte at end sigset_t set, oset; sigemptyset(&set); sigaddset(&set, SIGINT); // block SIGSET sigaddset(&set, SIGTSTP); // block SIGTSTP sigprocmask(SIG_BLOCK, &set, &oset); // and save mask FILE *fp; if((fp=fopen(ctermid(NULL), "r+")) == NULL) return NULL; setbuf(fp, NULL); // close I/O buffer struct termios termst, otermst; tcgetattr(fileno(fp), &termst); // save tty state otermst = termst; // structure copy termst.c_lflag &= ~(ECHO |ECHOE |ECHOK | ECHONL); // disable echo tcsetattr(fileno(fp), TCSAFLUSH, &termst); fputs(prompt, fp); char *str = buf; int ch; while((ch=getc(fp))!=EOF && ch != '\n'){ if(str < &buf[MAX_PASS_LEN]) *str++ = ch; } *str = 0; // null terminate putc('\n', fp); // echo a newline tcsetattr(fileno(fp), TCSAFLUSH, &otermst); // restore tty state sigprocmask(SIG_SETMASK, &oset, NULL); // restore signal mask fclose(fp); return buf; } int main(void){ char *ptr; if((ptr=getpass("Enter password: ")) == NULL){ printf("getpass error\n"); return 1; } printf("password: %s\n", ptr); while(*ptr != 0) *ptr++ = 0; // zero it out when we're done with it return 0; }
关于本程序,需要注意以下几个方面。
1、阻塞了信号 SIGINT 和 SIGTSTP。否则在输入 INTR 字符 或 SUSP 字符时都会使程序终止,然后在禁止回显状态下返回到终端。但没有一个 getpass 版本捕捉、忽略或阻塞 SIGQUIT 信号,所以输入 QUIT 字符就会使程序异常终止,并且很可能使终端保持在禁止回显状态。
2、关闭了标准 I/O 流的缓冲,以免在读、写之间产生某些交叉(这样就需要多次调用 fflush)。也可使用不带缓冲的 I/O,但这样就需要使用 read 来模仿 getc 函数。
3、由于 getpass 中使用静态存储区来存储输入的明文口令,所以为了安全起见,在程序完成后应在内存中消除它。因为要是该程序会产生其他用户可能读取的 core 文件(core 的系统默认许可权使每个用户都能读它),或者如果某个其他进程能够设法读该进程的存储空间,则它们就可能会读到这个口令。当然也可以在 getpass 中将该口令加密存储。
可以通过关闭 termios 结构中 c_lflag 字段的 ICANON 标志来指定非规范模式。在非规范模式中,输入数据不装配成行,不处理下列特殊字符:ERASE、KILL、EOF、NL、EOL、EOL2、CR、REPRINT、STATUS 和 WERASE(见 终端特殊输入字符)。当读取了指定量的数据,或者已经超过了给定量的时间后,就会通知系统返回数据。这使用了 termios 结构中 c_cc 数组的两个下标为 VMIN 和 VTIME 的两个变量:MIN 和 TIME。MIN 指定一个 read 返回前的最小字节数,TIME 指定等待数据到达的分秒数(分秒为秒的 1/10)。有下列 4 种情形。
1、MIN > 0,TIME > 0:TIME 指定一个字节间定时器(interbyte timer),它只在第一个字节被接收时启动(这意味着在定时器超时时,read 至少会返回一个字节)。在定时器超时之前,若已接收到 MIN 个字节,则 read 返回 MIN 个字节。如果在接到 MIN 个字节之前,该定时器已超时,则 read 返回已接收到的字节。
2、MIN > 0,TIME = 0:read 在接收到 MIN 个字节之前不返回。这可能会使 read 长期阻塞。
3、MIN = 0,TIME > 0:这里的 TIME 指定的是一个在调用 read 时就会立即启动的读定时器。在接到一个字节或者该定时器超时时,read 即返回。如果是定时器超时,则 read 返回 0。
4、MIN = 0,TIME = 0:如果有数据可用,则 read 最多返回所要求的字节数。否则立即返回 0。
注意,为了兼容性,POSIX.1 允许下标 VMIN 和 VTIME 的值可分别与 VEOF 和 VEOL 的相同,但这同时也带来了可移植性问题。从非规范模式转换为规范模式时,必须恢复 VEOF 和 VEOL。例如,如果 VMIN 等于 VEOF,且不恢复它们的值,那么当把 VMIN 的典型值设置为 1 时,文件结束符就变成了 Ctrl+A。解决这一问题最简单的方法是:在要转入非规范模式时,将整个 termios 结构保存起来,以后再要转会规范模式时恢复它。
下面这个程序中的函数 tty_cbreak 和 tty_raw 分别将终端设置为 cbreak 模式(cbreak mode)和原始模式(raw mode)。在这两种模式之间转换时,需要先调用 tty_reset 函数,它将终端恢复到调用 tty_cbreak 或 tty_raw 之前的工作状态,以减少出错时终端处于不可用状态的机会。程序中还提供了另外两个函数 tty_atexit 和 tty_termios。tty_atexit 可被登记为退出处理程序,以保证 exit 时恢复终端工作模式。tty_termios 则返回一个指向原来规范模式下 termios 结构的指针。不过在查看代码之前,先看一下 cbreak 模式和 raw 模式要满足的要求,其中涉及到的终端标志介绍可见 终端 I/O 综述。
cbreak 模式要满足的要求如下。
1、非规范模式。如上文所述,这种模式关闭了对某些输入字符的处理,但没有关闭对信号的处理。调用者一般应当捕捉这些信号,以免使程序终止,导致终端保持在 cbreak 模式。
2、关闭回显。
3、每次输入一个字节。为此,可将 MIN 和 TIME 分别设置为 1 和 0。
raw 模式要满足的要求如下。
1、非规范模式,同时还关闭了对信号产生字符 ISIG 和扩充输入字符 IEXTEN 的处理,另外还禁用了 BRKINT 字符,使 BREAK 字符不再产生信号。
2、关闭回显。
3、禁止输入中的 CR 到 NL 映射 ICRNL、输入奇偶检测 INPCK、剥离输入字节的第 8 位 ISTRIP 以及输出流控制 IXON。
4、8 位字符 CS8,且禁用奇偶校验 PARENB。
5、禁止所有输出处理 OPOST。
6、每次输入一个字节(MIN=1, TIME=0)。
#include#include #include #include #include #include static struct termios save_termios; static int ttysavefd = -1; static enum{RESET, RAW, CBREAK} ttystate = RESET; int tty_cbreak(int fd){ // put terminal into a cbreak mode if(ttystate != RESET){ errno = EINVAL; return -1; } struct termios buf; if(tcgetattr(fd, &buf) < 0) return -1; save_termios = buf; // structure copy buf.c_lflag &= ~(ECHO | ICANON); // echo off, canonical mode off buf.c_cc[VMIN] = 1; // 1 byte at a time, buf.c_cc[VTIME] = 0; // and no timer if(tcsetattr(fd, TCSAFLUSH, &buf) < 0) return -1; //Varify that the changes stuck. tcsetattr can return 0 on partial success int err; if(tcgetattr(fd, &buf) < 0){ err = errno; tcsetattr(fd, TCSAFLUSH, &save_termios); errno = err; return -1; } if((buf.c_lflag & (ECHO |ICANON)) || buf.c_cc[VMIN]!=1 || buf.c_cc[VTIME ]!=0){ //only some of the changes were made, restore the original settings tcsetattr(fd, TCSAFLUSH, &save_termios); errno = EINVAL; return -1; } ttystate = CBREAK; ttysavefd = fd; return 0; } int tty_raw(int fd){ // put terminal into a raw mode if(ttystate != RESET){ errno = EINVAL; return -1; } struct termios buf; if(tcgetattr(fd, &buf) < 0) return -1; save_termios = buf; // structure copy buf.c_lflag &= ~(ECHO |ICANON |IEXTEN |ISIG); buf.c_iflag &= ~(BRKINT |ICRNL |INPCK |ISTRIP | IXON); buf.c_cflag &= ~(CSIZE |PARENB); buf.c_cflag |= CS8; // set 8 bits/char buf.c_oflag &= ~(OPOST); buf.c_cc[VMIN] = 1; buf.c_cc[VTIME] = 0; if(tcsetattr(fd, TCSAFLUSH, &buf) < 0) return -1; int err; if(tcgetattr(fd, &buf) < 0){ err = errno; tcsetattr(fd, TCSAFLUSH, &save_termios); errno = err; return -1; } if((buf.c_lflag & (ECHO |ICANON |IEXTEN |ISIG)) || (buf.c_iflag & (BRKINT |ICRNL |INPCK |ISTRIP |IXON)) || (buf.c_cflag & (CSIZE |PARENB |CS8)) != CS8 || (buf.c_oflag & OPOST) || buf.c_cc[VMIN]!=1 || buf.c_cc[VTIME]!=0 ) { tcsetattr(fd, TCSAFLUSH, &save_termios); errno = EINVAL; return -1; } ttystate = RAW; ttysavefd = fd; return 0; } int tty_reset(int fd){ // restore terminal's mode if(ttystate == RESET) return 0; if(tcsetattr(fd, TCSAFLUSH, &save_termios) < 0) return -1; ttystate = RESET; return 0; } void tty_atexit(void){ // can be set up by atexit(tty_atexit); if(ttysavefd >= 0) tty_reset(ttysavefd); } struct termios *tty_termios(void){ // let caller see original tty state return &save_termios; } /* ======================= test ============================ */ static void sig_catch(int signo){ printf("signal caught\n"); tty_reset(STDIN_FILENO); exit(0); } int main(void){ atexit(tty_atexit); signal(SIGINT, sig_catch); // catch SIGINT signal(SIGQUIT, sig_catch); // catch SIGQUIT signal(SIGTERM, sig_catch); // catch SIGTERM int i; char c; if(tty_raw(STDIN_FILENO) < 0){ printf("tty_raw error\n"); return 1; } printf("Enter raw mode characters, terminate with Backspace\n"); while((i=read(STDIN_FILENO, &c, 1)) == 1){ if((c &= 255) == 0177) // 0177 = ASCII Backspace break; printf("%#o\n", c); } if(i <= 0){ printf("read error\n"); return 1; } tty_reset(STDIN_FILENO); if(tty_cbreak(STDIN_FILENO) < 0){ printf("tty_cbreak error\n"); return 1; } printf("\nEnter cbreak mode characters, terminate with SIGINT\n"); while((i=read(STDIN_FILENO, &c, 1)) == 1) printf("%#o\n", c & 255); if(i <= 0){ printf("read error\n"); return 1; } tty_reset(STDIN_FILENO); return 0; }
编译后运行该程序,可以观察到两种终端模式的工作情况如下。
$ ./termmode.out Enter raw mode characters, terminate with Backspace 04 # Ctrl+D 03 # Ctrl+C 033 # F7 0133 061 070 0176 # Backspace Enter cbreak mode characters, terminate with SIGINT 01 # Ctrl+A 0177 # Backspace signal caught # Ctrl+C
这里,在原始模式中,输入的字符是文件结束符 Ctrl+D(04)、中断符 Ctrl+C(03)和特殊功能键 F7。该功能键在本人所用的终端上产生 5 个字符:ESC(033)、[(0133)、字符 1(061)、字符 8(070)和 ~(0176)。注意,因为在原始模式下关闭了输出处理(~OPOST),所以在每个字符后没有得到回车符。在 cbreak 模式下,不对特殊输入字符进行处理,但是仍对终端产生的信号进行处理。