《unix环境高级编程》--- 终端I/O

禁用中断字符和更改文件结束符为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

猜你喜欢

转载自blog.csdn.net/u012319493/article/details/80639566