介绍

  在linux下每打开一个终端,系统自动的就打开了三个文件,它们的文件描述符分别为0,1,2,功能分别是“标准输入”、“标准输出”和“标准错误输出”,同时对应了三个文件流指针,分别是stdin,stdout和stderr。三个文件描述符定义了对应的宏,分别为STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO下表为他们的对应关系:

标准输入 0 STDIN_FILENO stdin
标准输出 1 STDOUT_FILENO stdout
标准错误输出 2 STDERR_FILENO stderr

  在程序中通过read从STDIN_FILENO这个文件描述符中读取数据,实际上就是读取终端上键盘输入的数据,当然也可以通过fread从stdin这个文件流指针中读取数据。通过write往STDOUT_FILENO这个文件描述符中写数据,自然就是往终端输出数据了。标准错误输出也是往终端输出,它与标准输出的区别就是标准输出是有缓冲的,而标准错误输出是无缓冲的。

  在程序中可以通过各种办法从终端读取键盘输入的数据,有时候会希望调用的函数不阻塞,这里有三种方法可以实现。

1、通过ioctl清除非阻塞标志

  函数原型:

    int ioctl(int d, int request, ...);

  参数:

    d:文件描述符

    request:功能码。根据填写的功能码选择第三个参数。

  返回:

    成功返回0,失败返回-1。但是也有一部分功能返回的是一个数值。

  ioctl函数是一个万能函数,这里不详细解释。

  传入的第二个参数为FIONBIO表示“设置/清除非阻塞标志”,那么第三个参数要传入一个int类型的指针,指针指向的值为1表示设置非阻塞标志,那么对应的文件描述符为非阻塞,指针指向的值为0表示清除非阻塞标志,那么对应的文件描述符为阻塞,简单的说就是“0就阻塞,1就非阻塞

  因此调用如下代码即可实现非阻塞:

int attr = 1;
ioctl(STDIN_FILENO, FIONBIO, &attr);   /* 清除非阻塞标志 */

  通过该方法设置完成之后,在没有数据的情况下调用read函数将返回-1。

  完整测试代码如下:

 1 #include <stdio.h>
 2 #include <netinet/in.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 #include <termios.h>
 6 #include <string.h>
 7 #include <sys/ioctl.h>
 8 
 9 int main(int argc, const char *argv[])
10 {
11     char buf[128] = { 0 };
12     int len = 0;
13     int total = 0;
14     int attr = 1;
15     
16     ioctl(STDIN_FILENO, FIONBIO, &attr);   /* 清除非阻塞标志 */
17    
18     while (1) {
19         len = read(STDIN_FILENO, &buf[total], sizeof(buf) - total);
20         //printf("len = %d\n", len);  /* 如果不相信read变成了非阻塞可以去掉本行注释 */
21         if (len > 0) {
22             total += len;
23             if (buf[total - 1] == '\n') {
24                 printf("total = %d\n", total);
25                 printf("buf = %s\n", buf);     
26                 total = 0;
27                 memset(buf, 0, sizeof(buf));
28             }
29         }
30     }
31 
32     return 0;
33 }

2、通过fcntl设置非阻塞标志

  函数原型:

    int fcntl(int fd, int cmd, ... /* arg */ );

  参数:

    fd:文件描述符。

    cmd:功能码。根据填写的功能码选择第三个参数。

  返回:

    根据不同的功能决定。

  fcntl函数与ioctl函数看上去有很多相似性,实际上这两个函数确实有很多功能是重叠的。

  如果要通过fcntl让文件不阻塞,那么需要知道两个功能码:

  F_GETFL:取得fd的文件状态标志,如同下面的描述一样(arg被忽略)                    

       F_SETFL:设置给arg描述符状态标志,可以更改的几个标志是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC。

  很明显非阻塞标志是O_NONBLOCK,调用下列代码就可以实现非阻塞:

int attr = 0;
    
attr = fcntl(STDIN_FILENO, F_GETFL);
attr |= O_NONBLOCK;
fcntl(STDIN_FILENO, F_SETFL, attr);

  通过该方法设置完成之后,在没有数据的情况下调用read函数将返回-1。

  完整测试代码如下:

 1 #include <stdio.h>
 2 #include <netinet/in.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 #include <termios.h>
 6 #include <string.h>
 7 #include <sys/ioctl.h>
 8 
 9 int main(int argc, const char *argv[])
10 {
11     char buf[128] = { 0 };
12     int len = 0;
13     int total = 0;
14     int attr = 0;
15     
16     /* 设置 O_NONBLOCK属性 */
17     attr = fcntl(STDIN_FILENO, F_GETFL);
18     attr |= O_NONBLOCK;
19     fcntl(STDIN_FILENO, F_SETFL, attr);
20    
21     while (1) {
22         len = read(STDIN_FILENO, &buf[total], sizeof(buf) - total);
23         //printf("len = %d\n", len);  /* 如果不相信read变成了非阻塞可以去掉本行注释 */
24         if (len > 0) {
25             total += len;
26             if (buf[total - 1] == '\n') {
27                 printf("total = %d\n", total);
28                 printf("buf = %s\n", buf);     
29                 total = 0;
30                 memset(buf, 0, sizeof(buf));
31             }
32         }
33     }
34 
35     return 0;
36 }

3、通过设置termios实现

  termios是一个结构体,详细介绍可以参见这篇博客:https://www.cnblogs.com/dartagnan/archive/2013/04/25/3042417.html

  需要用到的函数有tcgetattr和tcsetattr,这两个函数的用法在这篇博客中有介绍:https://www.cnblogs.com/Suzkfly/p/11055532.html

  操作流程就是先用tcgetattr获取文件属性,修改属性之后再用tcsetattr设置进去,关键代码如下:

    tcgetattr(STDIN_FILENO, &attr);
    attr.c_cc[VTIME] = 0;
    attr.c_cc[VMIN] = 0;
    attr.c_lflag &= ~ICANON;        /* 禁用规范输入模式 */
    tcsetattr(STDIN_FILENO, TCSANOW, &attr);

  经过测试,attr.c_lflag &= ~ICANON;这一句一定要写,原因我也不知道,但是写了这句话会带来某些问题,在最后进行分析。

  将attr.c_cc[VTIME]和attr.c_cc[VMIN]的值都设置为0。attr.c_cc[VTIME]和attr.c_cc[VMIN]共同决定了read函数的返回时机。表示读阻塞时间,单位是1/10秒。

  如果经过了c_cc[VTIME]这么长时间,缓冲区内有数据,但是还没达到c_cc[VMIN]个数据,read也会返回。而如果当缓冲区内有了c_cc[VMIN]个数据时,无论等待时间是否到了c_cc[VTIME],read都会返回,但返回值可能比c_cc[VMIN]还大,根据实际数据量而定。如果将c_cc[VMIN]的值设置为0,那么当经过c_cc[VTIME]时间后read也会返回,返回值为0。如果将c_cc[VTIME]和c_cc[VMIN]都设置为0,那么程序运行的效果与设置O_NONBLOCK类似,不同的是如果设置了O_NONBLOCK,那么在没有数据时read返回-1,而如果没有设置O_NONBLOCK,那么在没有数据时read返回的是0。

  完整测试代码如下:

 1 #include <stdio.h>
 2 #include <netinet/in.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 #include <termios.h>
 6 #include <string.h>
 7 #include <sys/ioctl.h> 
 8 
 9 int main(int argc, const char *argv[])
10 {
11     int port = 0;
12     char buf[128] = { 0 };
13     int len = 0;
14     int total = 0;
15     struct termios attr;
16       
17     tcgetattr(STDIN_FILENO, &attr);
18     attr.c_cc[VTIME] = 0;
19     attr.c_cc[VMIN] = 0;
20     attr.c_lflag &= ~ICANON;        /* 禁用规范输入模式 */
21     tcsetattr(STDIN_FILENO, TCSANOW, &attr);
22 
23     while (1) {
24         len = read(STDIN_FILENO, &buf[total], sizeof(buf) - total);
25         //printf("len = %d\n", len);
26         if (len > 0) {
27             total += len;
28             if (buf[total - 1] == '\n') {
29                 printf("total = %d\n", total);
30                 printf("buf = %s\n", buf);     
31                 total = 0;
32                 memset(buf, 0, sizeof(buf));
33             }
34         }
35     }
36 
37     return 0;
38 }

4、分析总结

  特别要注意的一点是,在测试的时候,如果你不知道你的操作会产生什么结果,在下次测试之前一定要重新开一个终端,因为如果终端没有结束的话,就表示文件没有关闭,即使程序退出了,之前的设置也是会保存的。

  通过这三种方法都实现了设置属性为非阻塞,其实第一和第二种方法是一样的,第三种方法由于禁用了规范输入模式,会带来某些问题。所谓规范输入模式就是对某些特殊的键会产生特殊的效果,比如退格键应该回删一个字符,但通过第三种方法会将退格键也当成一个字符读取进去了。比如,在终端输入“abc”再输入一个退格,然后按回车,通过第一和第二种方法会接收到“ab\n”三个字符,而通过第三种方法则会收到“abc”+“退格”+“\n”5个字符。

  第一和第二种方法虽然read不会阻塞,但是并不是说只要通过键盘输入了数据,它就能读到数据,即使通过键盘输入了一些数据,read函数返回的还是-1,直到按下回车键为止。