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