先前我們以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函數用於設置某些設備本身的屬性,例如串口波特率、終端窗口大小,注意區分這兩個函數的作用。