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