6. fcntl

先前我們以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_GETFLF_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的重定向功能在該描述符上打開檔案,然後用fcntlF_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,如下圖所示。

圖 28.3. 重定向之後的檔案描述符表

重定向之後的檔案描述符表

/dev/null設備檔案只有一個作用,往它裡面寫任何數據都被直接丟棄。因此保證了該命令執行時屏幕上沒有任何輸出,既不打印正常信息也不打印錯誤信息,讓命令安靜地執行,這種寫法在Shell腳本中很常見。注意,檔案描述符數字寫在重定向符號右邊需要加&號,否則就被解釋成檔案名了,2>&1其中的>左右兩邊都不能有空格。

除了F_GETFLF_SETFL命令之外,fcntl還有很多命令做其它操作,例如設置檔案記錄鎖等。可以通過fcntl設置的都是當前進程如何訪問設備或檔案的訪問控制屬性,例如讀、寫、追加、非阻塞、加鎖等,但並不設置檔案或設備本身的屬性,例如檔案的讀寫權限、串口波特率等。下一節要介紹的ioctl函數用於設置某些設備本身的屬性,例如串口波特率、終端窗口大小,注意區分這兩個函數的作用。