禁用中断字符和更改文件结束符为Ctrl+B
禁止使用终端驱动程序产生SIGINT信号的特殊字符。但仍可使用kill函数向进程发送该信号。
#include "apue.h"
#include <termios.h>
int main(void)
{
struct termios term;
long vdisable;
/* 仅当标准输入是终端设备时才修改终端特殊字符 */
if(isatty(STDIN_FILENO) == 0)
err_quit("standard input is not a terminal device");
/* 获取_POSIX_VDISABLE值 */
if((vdisable = fpathconf(STDIN_FILENO, _PC_VDISABLE)) < 0)
err_quit("fpathconf error or _POSIX_VDISABLE not in effect");
/*
int tcgetattr(int filedes, struct termios *termptr);
从内核获取termios结构。
struct termios
{
tcflag_t c_iflag; input flags
tcflag_t c_oflag; output flags
tcflag_t c_cflag; control flags
tcflag_t c_lflag; local flags
cc_t c_cc[NCCS]; control characters
}; */
if(tcgetattr(STDIN_FILENO, &term) < 0) /* fetch tty state */
err_sys("tcgetattr error");
term.c_cc[VINTR] = vdisable; /* disable INTR character */
term.c_cc[VEOF] = 2; /* EOF is Control-B*/
/*
int tcsetattr(int filedes, int opt, const struct termios *termptr);
TCSAFLUSH: 发送了所有输出后更改才发生。在更改发生时未读的所有输入数据都被删除(刷清)
修改了termios结构后,设置属性 */
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &term) < 0)
err_sys("tcsetattr error");
exit(0);
}
tcgetattr和tcsetattr实例
#include "apue.h"
#include <termios.h>
int main(void)
{
struct termios term;
if(tcgetattr(STDIN_FILENO, &term) < 0)
err_sys("tcgetattr error");
switch(term.c_cflag & CSIZE)
{
case CS5:
printf("5 bits/byte\n");
break;
case CS6:
printf("6 bits/byte\n");
break;
case CS7:
printf("7 bits/byte\n");
break;
case CS8:
printf("8 bits/byte\n");
break;
default:
printf("unkown bits/byte\n");
}
term.c_cflag &= ~CSIZE; /* zero out the bits */
term.c_cflag |= CS8; /* set 8 bits/byte */
if(tcsetattr(STDIN_FILENO, TCSANOW, &term) < 0)
err_sys("tcsetattr error");
exit(0);
}
stty命令
ctermid函数
#include <stdio.h>
char *ctermid(char *ptr);
运行时确定控制终端的名字。因大多数UNIX系统都使用/dev/tty作为控制终端名,所以该函数主要为其他系统
提供可移植性。
返回值:成功则返回指向控制终端名的指针,出错返回指向空字符串的指针。
如果ptr非null,指向长度至少为L_ctermid字节的数组,进程的控制终端名存放在该数组。
L_ctermid定义在<stdio.h>中。若ptr是一个空指针,则为其分配空间(通常为静态变量),
然后进程的控制终端名存放在该数组中。
实现
#include <stdio.h>
#include <string.h>
static char ctermid_name[L_ctermid];
char *ctermid(char *str)
{
if(str == NULL)
str = ctermid_name;
return (strcpy(str, "dev/tty")); /* strcpy() returns str */
}
isatty函数
#include <unistd.h>
int isatty(int filedes);
当描述符filedes引用一个终端设备时返回真。
实现
#include <termios.h>
#include "apue.h"
int isatty(int fd)
{
struct termios ts;
return (tcgetattr(fd, &ts) != 1); /* true if no error (is a tty) */
}
int main(void)
{
printf("fd 0: %s\n", isatty(0) ? "tty" : " not a tty");
printf("fd 1: %s\n", isatty(1) ? "tty" : " not a tty");
printf("fd 2: %s\n", isatty(2) ? "tty" : " not a tty");
exit(0);
}
ttyname函数
#include <unistd.h>
char *ttyname(int fieldes);
返回在该文件描述符上打开的终端设备的路径名。
返回值:指向终端路径名的指针,出错返回NULL。
实现
读/dev目录,寻找具有相同设备号和i节点编号的表项。
终端名可能在/dev的子目录中,所以需搜索在/dev之下的整个文件子系统。会跳过很多产生不正确或
奇怪结果的目录,它们是/dev/.、/dev/..和/dev/fd。也跳过了一些别名,即/dev/stdin、/dev/stdout及
/dev/stderr,它们是对在/dev/fd目录文件的符号链接。
#include <sys/stat.h>
#include <dirent.h>
#include <limits.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include "apue.h"
struct devdir
{
struct devdir *d_next;
char *d_name;
};
static struct devdir *head;
static struct devdir *tail;
static char pathname[_POSIX_PATH_MAX + 1];
static void add(char *dirname)
{
struct devdir *ddp;
int len;
len = strlen(dirname);
/* Skip ., .., and /dev/fd. */
if((dirname[len-1] == '.') && (dirname[len-2] == '/' ||
(dirname[len-2] == '.' && dirname[len-3] == '/')))
return;
if(strcmp(dirname, "/dev/fd") == 0)
return;
ddp = malloc(sizeof(struct devdir));
if(ddp == NULL)
return;
ddp->d_name = strdup(dirname);
if(ddp->d_name == NULL)
{
free(ddp);
return;
}
ddp->d_next = NULL;
if(tail == NULL)
{
head = ddp;
tail = ddp;
}
else
{
tail->d_next = ddp;
tail = ddp;
}
}
static void cleanup(void)
{
struct devdir *ddp, *nddp;
ddp = head;
while(ddp != NULL)
{
nddp = ddp->d_next;
free(ddp->d_name);
free(ddp);
ddp = nddp;
}
head = NULL;
tail = NULL;
}
static char *searchdir(char *dirname, struct stat *fdstatp)
{
struct stat devstat;
DIR *dp;
int devlen;
struct dirent *dirp;
strcpy(pathname, dirname);
if((dp = opendir(dirname)) == NULL)
return (NULL);
strcat(pathname, "/");
devlen = strlen(pathname);
while((dirp = readdir(dp)) != NULL)
{
strncpy(pathname + devlen, dirp->d_name, _POSIX_PATH_MAX - devlen);
/* Skip aliases */
if(strcmp(pathname, "/dev/stdin") == 0 || strcmp(pathname, "/dev/stdout") == 0 ||
strcmp(pathname, "/dev/stderr") == 0)
continue;
if(stat(pathname, &devstat) < 0)
continue;
if(S_ISDIR(devstat.st_mode))
{
add(pathname);
continue;
}
/* found a match */
if(devstat.st_ino == fdstatp->st_ino && devstat.st_dev == fdstatp->st_dev)
{
closedir(dp);
return (pathname);
}
}
closedir(dp);
return (NULL);
}
char *ttyname(int fd)
{
struct stat fdstat;
struct devdir *ddp;
char *rval;
if(isatty(fd) == 0)
return (NULL);
if(fstat(fd, &fdstat) < 0)
return (NULL);
if(S_ISCHR(fdstat.st_mode) == 0)
return (NULL);
rval = searchdir("/dev", &fdstat);
if(rval == NULL)
{
for(ddp = head; ddp != NULL; ddp = ddp->d_next)
if((rval = searchdir(ddp->d_name, &fdstat)) != NULL)
break;
}
cleanup();
return (rval);
}
int main(void)
{
char *name;
if(isatty(0))
{
name = ttyname(0);
if(name == NULL)
name = "undefined";
}
else
name = "not a tty";
printf("fd 0: %s\n", name);
if(isatty(1))
{
name = ttyname(1);
if(name == NULL)
name = "undefined";
}
else
name = "not a tty";
printf("fd 1: %s\n", name);
if(isatty(2))
{
name = ttyname(2);
if(name == NULL)
name = "undefined";
}
else
name = "not a tty";
printf("fd 2: %s\n", name);
exit(0);
}
getpass函数
读入用户在终端上键入的口令。由login(1)和crypt(1)调用。为了读口令,必须禁止回显,但仍可使终端以规范模式工作,因为用户键入口令后,一定要键入回车,从而构成完整行。
#include <signal.h>
#include <stdio.h>
#include <termios.h>
#include "apue.h"
#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 */
char *ptr;
sigset_t sig, osig;
struct termios ts, ots;
FILE *fp;
int c;
/* 调用ctermid打开控制终端,而不是直接将/dev/tty写在程序中。
以读、写模式打开控制终端 */
if((fp = fopen(ctermid(NULL), "r+")) == NULL)
return (NULL);
/* 不带缓冲,否则需要fflush */
setbuf(fp, NULL);
/* 阻塞信号SIGINT和SIGTSTP,否则,在输入INTR或SUSP字符时会使程序终止,并使
终端仍处于禁止回显状态。直到getpass返回前才解除对它们的阻塞。*/
sigemptyset(&sig);
sigaddset(&sig, SIGINT);
sigaddset(&sig, SIGTSTP);
sigprocmask(SIG_BLOCK, &sig, &osig);
tcgetattr(fileno(fp), &ts);
ots = ts;
ts.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
tcsetattr(fileno(fp), TCSAFLUSH, &ts);
fputs(prompt, fp);
ptr = buf;
while((c = getc(fp)) != EOF && c != '\n')
if(ptr < &buf[MAX_PASS_LEN])
*ptr++ = c;
*ptr = 0;
putc('\n', fp);
tcsetattr(fileno(fp), TCSAFLUSH, &ots);
sigprocmask(SIG_SETMASK, &osig, NULL);
fclose(fp);
return (buf);
}
int main(void)
{
char *ptr;
if((ptr = getpass("Enter password:")) == NULL)
err_sys("getpass error");
printf("password: %s\n", ptr);
/* getpass函数完成后,为安全起见,清除放过用户键入的未经加密的明文口令的存储区 */
while(*ptr != 0)
*ptr++ = 0;
exit(0);
}
非规范模式 noncanonical mode
规范模式 canonical mode:
系统每次返回一行。
非规范模式 noncanonical mode:
在noncanonical模式,输入的数据不会被收集成行,并且特殊字符不会被处理:ERASE, KILL, EOF, NL, EOL, EOL2, CR, REPRINT, STATUS, WERASE。
当已经读取了指定数目的数据或者过了一个指定的时间之后,告诉系统返回。这个技术使用两个变量,它们存放在termios结构中的c_cc数组中:MIN和TIME。这两个数组的元素通过名称VMIN和VTIME被索引到。通过关闭termios结构中的c_lflag域中的ICANON标记来指定。
原始端模式和cbreak终端模式
#include "apue.h"
#include <termios.h>
#include <errno.h>
static struct termios save_termios;
static int ttysavefd = -1;
static enum { RESET, RAW, CBREAK } ttystate = RESET;
/*
cbreak模式:
1、非规范模式。不对某些特殊字符处理。
2、关闭回显标志。
3、每次输入一个一字节。为此将MIN设置为1,将TIME设置为0.
*/
int tty_cbreak(int fd)
{
int err;
struct termios buf;
if(ttystate != RESET)
{
errno = EINVAL;
return (-1);
}
if(tcgetattr(fd, &buf) < 0)
return (-1);
save_termios = buf; /* structure copy */
/* Echo off, canonical mode off */
buf.c_lflag &= ~(ECHO | ICANON);
/* 1 byte at a time, no timer */
buf.c_cc[VMIN] = 1;
buf.c_cc[VTIME] = 0;
if(tcsetattr(fd, TCSAFLUSH, &buf) < 0)
return (-1);
/* Verify that the changes stuck. tcsetattr can return 0 on partial success */
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)
{
tcsetattr(fd, TCSAFLUSH, &save_termios);
errno = EINVAL;
return (-1);
}
ttystate = CBREAK;
ttysavefd = fd;
return (0);
}
/*
原始模式:
1、非规范模式。也关闭对产生信号字符(ISIG)和扩充输入字符(IEXTEN)的处理。
另外,禁用BRKINT,使BREAK不再产生信号。
2、关闭回显(ECHO)标志。
3、禁用ICRNL、INPCK、ISTRIP和IXON标志。从而不再将输入的CR字符转换为NL(ICRNL),
使奇偶校验不起作用(INPCK),不再剥离输入字节的第8位(ISTRIP),
不再进行输出流控制(IXON)。
4、8位字符(CS8),且禁用奇偶性检测(PARENB)。
5、禁用所有输出处理(OPOST)
6、每次输入一个字节(MIN=1, TIME=0)
*/
int tty_raw(int fd)
{
int err;
struct termios buf;
if(ttystate != RESET)
{
errno = EINVAL;
return (-1);
}
if(tcgetattr(fd, &buf) < 0)
return (-1);
save_termios = buf; /* structrue copy */
/* Echo off, canonical mode off, extended input processing off,
signal chars off */
buf.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* No SIGINT on BREAK, CR-to-NL off, input parity check off,
don't strip 8th bit on input, output flow control off */
buf.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* Clear size bits, parity checking off */
buf.c_cflag &= ~(CSIZE | PARENB);
/* Set 8 bits/char */
buf.c_cflag |= CS8;
/* Output processing off */
buf.c_oflag &= ~(OPOST);
/* 1 byte at a time, no timer */
buf.c_cc[VMIN] = 1;
buf.c_cc[VTIME] = 0;
if(tcsetattr(fd, TCSAFLUSH, &buf) < 0)
return (-1);
/* Verify that the changes stuck. tcsetarrt can return 0 on partial success */
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)
{
/* Only some of the change were made. Restore the original settings. */
tcsetattr(fd, TCSAFLUSH, &save_termios);
errno = EINVAL;
return (-1);
}
ttystate = RAW;
ttysavefd = fd;
return (0);
}
/* 将终端恢复位调用tty_cbreak和tty_raw之前的工作模式 */
int tty_reset(int fd)
{
if(ttystate == RESET)
return (0);
if(tcsetattr(fd, TCSAFLUSH, &save_termios) < 0)
return (-1);
ttystate = RESET;
return (0);
}
/* 可被登记位终止处理程序, 以保证exit恢复终端工作模式*/
void tty_atexit(void)
{
if(ttysavefd >= 0)
tty_reset(ttysavefd);
}
/* 返回一个指向原先的规范模式termios结构的指针 */
struct termios *tty_termios(void)
{
return (&save_termios);
}
static void sig_catch(int signo)
{
printf("signal caught\n");
tty_reset(STDIN_FILENO);
exit(0);
}
int main(void)
{
int i;
char c;
/* 在编写更改终端模式的程序时,应捕捉大多数信号,以便在程序终止前恢复终端模式 */
if(signal(SIGINT, sig_catch) == SIG_ERR)
err_sys("signal(SIGINT) error");
if(signal(SIGQUIT, sig_catch) == SIG_ERR)
err_sys("signal(SIGQUIT) error");
if(signal(SIGTERM, sig_catch) == SIG_ERR)
err_sys("signal(SIGTERM) error");
if(tty_raw(STDIN_FILENO) < 0)
err_sys("tty_raw error");
printf("Enter raw mode characters, terminate with 'a'\n");
while((i = read(STDIN_FILENO, &c, 1)) == 1)
{
if((c &= 255) == 97) /* 0177 = ASCII 'a' */
break;
printf("%o\n", c);
}
if(tty_reset(STDIN_FILENO) < 0)
err_sys("tty_reset error");
if(i <= 0)
err_sys("read error");
if(tty_cbreak(STDIN_FILENO) < 0)
err_sys("tty_cbreak error");
printf("\nEnter cbreak mode characters, terminate with SIGINT\n");
while((i = read(STDIN_FILENO, &c, 1)) == 1)
{
c &= 255;
printf("%o\n", c);
}
if(tty_reset(STDIN_FILENO) < 0)
err_sys("tty_reset error");
if(i <= 0)
err_sys("read error");
exit(0);
}
结果
yjp@yjp-VirtualBox:~/apue/18termios$ ./tty
Enter raw mode characters, terminate with 'a'
4 输入 Ctrl + D
33 输入 F7
133
61
70
176
输入 a
Enter cbreak mode characters, terminate with SIGINT
1 输入 Ctrl + A
40 输入 空格
signal caught 输入 Ctrl + C
终端窗口大小
打印当前窗口大小,然后休眠。每次窗口大小改变时,就捕捉到SIGWINCH信号,然后打印新的窗口大小。
#include "apue.h"
#include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif
static void pr_winsize(int fd)
{
/*
struct winsize
{
unsigned short ws_row; rows, in characters
unsigned short ws_col; columns, in characters
unsigned short ws_xpixel; horizontal size, pixels (unused)
unsigned short ws_ypixel; vertical size, piexls (unused)
}
*/
struct winsize size;
/* 将winsize结构的新值存放在内核中。如果此新值与内核中的当前值不同,
则向前台进程组发送SIGWINCH信号 */
if(ioctl(fd, TIOCGWINSZ, (char *)&size) < 0)
err_sys("TIOCGWINSZ error");
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
}
static void sig_winch(int signo)
{
printf("SIGWINCH received\n");
pr_winsize(STDIN_FILENO);
}
int main(void)
{
if(isatty(STDIN_FILENO) == 0)
exit(1);
if(signal(SIGWINCH, sig_winch) == SIG_ERR)
err_sys("signal error");
pr_winsize(STDIN_FILENO); /* print initial size */
for(;;)
pause(); /* and sleep forever */
}
结果
yjp@yjp-VirtualBox:~/apue/18termios$ ./winsize
24 rows, 80 columns 初始窗口大小
SIGWINCH received 更改窗口大小
25 rows, 84 columns
SIGWINCH received 更改窗口大小
25 rows, 63 columns
^C 输入Ctrl + C