先前我們以read
終端設備為例介紹了非阻塞I/O,為什麼我們不直接對STDIN_FILENO
做非阻塞read
,而要重新open
一遍/dev/tty
呢?因為STDIN_FILENO
在程序啟動時已經被自動打開了,而我們需要在調用open
時指定O_NONBLOCK
標誌。這裡介紹另外一種辦法,可以用fcntl
函數改變一個已打開的檔案的屬性,可以重新設置讀、寫、追加、非阻塞等標誌(這些標誌稱為File Status Flag),而不必重新open
檔案。
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock *lock);
這個函數和open
一樣,也是用可變參數實現的,可變參數的類型和個數取決於前面的cmd
參數。下面的例子使用F_GETFL
和F_SETFL
這兩種fcntl
命令改變STDIN_FILENO
的屬性,加上O_NONBLOCK
選項,實現和例 28.3 “非阻塞讀終端”同樣的功能。
例 28.5. 用fcntl改變File Status Flag
#include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <stdlib.h> #define MSG_TRY "try again\n" int main(void) { char buf[10]; int n; int flags; flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK; if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) { perror("fcntl"); exit(1); } tryagain: n = read(STDIN_FILENO, buf, 10); if (n < 0) { if (errno == EAGAIN) { sleep(1); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); goto tryagain; } perror("read stdin"); exit(1); } write(STDOUT_FILENO, buf, n); return 0; }
以下程序通過命令行的第一個參數指定一個檔案描述符,同時利用Shell的重定向功能在該描述符上打開檔案,然後用fcntl
的F_GETFL
命令取出File Status Flag並打印。
#include <sys/types.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int val; if (argc != 2) { fputs("usage: a.out <descriptor#>\n", stderr); exit(1); } if ((val = fcntl(atoi(argv[1]), F_GETFL)) < 0) { printf("fcntl error for fd %d\n", atoi(argv[1])); exit(1); } switch(val & O_ACCMODE) { case O_RDONLY: printf("read only"); break; case O_WRONLY: printf("write only"); break; case O_RDWR: printf("read write"); break; default: fputs("invalid access mode\n", stderr); exit(1); } if (val & O_APPEND) printf(", append"); if (val & O_NONBLOCK) printf(", nonblocking"); putchar('\n'); return 0; }
運行該程序的幾種情況解釋如下。
$ ./a.out 0 < /dev/tty read only
Shell在執行a.out
時將它的標準輸入重定向到/dev/tty
,並且是隻讀的。argv[1]
是0,因此取出檔案描述符0(也就是標準輸入)的File Status Flag,用掩碼O_ACCMODE
取出它的讀寫位,結果是O_RDONLY
。注意,Shell的重定向語法不屬於程序的命令行參數,這個命行只有兩個參數,argv[0]
是"./a.out",argv[1]
是"0",重定向由Shell解釋,在啟動程序時已經生效,程序在運行時並不知道標準輸入被重定向了。
$ ./a.out 1 > temp.foo $ cat temp.foo write only
Shell在執行a.out
時將它的標準輸出重定向到檔案temp.foo
,並且是隻寫的。程序取出檔案描述符1的File Status Flag,發現是隻寫的,於是打印write only
,但是打印不到屏幕上而是打印到temp.foo
這個檔案中了。
$ ./a.out 2 2>>temp.foo write only, append
Shell在執行a.out
時將它的標准錯誤輸出重定向到檔案temp.foo
,並且是隻寫和追加方式。程序取出檔案描述符2的File Status Flag,發現是隻寫和追加方式的。
$ ./a.out 5 5<>temp.foo read write
Shell在執行a.out
時在它的檔案描述符5上打開檔案temp.foo
,並且是可讀可寫的。程序取出檔案描述符5的File Status Flag,發現是可讀可寫的。
我們看到一種新的Shell重定向語法,如果在<、>、>>、<>前面添一個數字,該數字就表示在哪個檔案描述符上打開檔案,例如2>>temp.foo表示將標准錯誤輸出重定向到檔案temp.foo並且以追加方式寫入檔案,注意2和>>之間不能有空格,否則2就被解釋成命令行參數了。檔案描述符數字還可以出現在重定向符號右邊,例如:
$ command > /dev/null 2>&1
首先將某個命令command的標準輸出重定向到/dev/null
,然後將該命令可能產生的錯誤信息(標准錯誤輸出)也重定向到和標準輸出(用&1標識)相同的檔案,即/dev/null
,如下圖所示。
/dev/null
設備檔案只有一個作用,往它裡面寫任何數據都被直接丟棄。因此保證了該命令執行時屏幕上沒有任何輸出,既不打印正常信息也不打印錯誤信息,讓命令安靜地執行,這種寫法在Shell腳本中很常見。注意,檔案描述符數字寫在重定向符號右邊需要加&號,否則就被解釋成檔案名了,2>&1其中的>左右兩邊都不能有空格。
除了F_GETFL
和F_SETFL
命令之外,fcntl
還有很多命令做其它操作,例如設置檔案記錄鎖等。可以通過fcntl
設置的都是當前進程如何訪問設備或檔案的訪問控制屬性,例如讀、寫、追加、非阻塞、加鎖等,但並不設置檔案或設備本身的屬性,例如檔案的讀寫權限、串口波特率等。下一節要介紹的ioctl
函數用於設置某些設備本身的屬性,例如串口波特率、終端窗口大小,注意區分這兩個函數的作用。