Linux中讓終端輸入變為非阻塞的三種方法


介紹

  在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,直到按下回車鍵為止。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM