UNIX環境高級編程(6):文件I/O(2)

文件共享:

UNIX系統支持在不同進程間共享打開的文件。內核使用三種數據結構表示打開的文件,他們之間的關係決定了在文件共享方面一個進程對另一個進程可能產生的影響:

(1)每個進程在進程表中都有一個記錄項,記錄項中包含有一張打開文件描述符表,可將其視爲一個矢量,每個描述符都佔用一項,與每個文件描述符相關聯的是:

  • 文件描述符標誌
  • 指向一個文件表項的指針

(2)內核爲所有打開文件維護一張文件表,每個文件表項包括:

  • 文件狀態標誌(讀,寫,添寫,同步和非阻塞等);
  • 當前文件偏移量;
  • 指向該文件v節點表項的指針;

(3)每個打開文件(或設備)都有一個v節點結構。v節點包含了文件類型和對此文件進行各種操作的函數的指針;對於大多數文件,v節點還包括了該文件的i節點(i-node,索引節點)。這些信息是打開文件時從磁盤上讀入內存的,所以所有關於文件的信息都是快速可供使用的。例如,i節點內包含了文件的所有者,文件長度,文件所在設備,指向文件實際數據在磁盤上所在位置的指針等等。

上面的討論是概念性的,與特定實現可能匹配,也可能不匹配(Linux沒有使用v節點,而是使用了通用i節點結構,但是在概念上,v節點與i節點是一樣的,兩者都指向文件系統特有的i節點結構,創建v節點結構的目的是對在一個計算機系統上的多文件系統類型提供支持,sun稱這種文件系統爲虛擬文件系統,稱與文件系統類型無關的i節點部分爲v節點)。

如果兩個獨立進程各自打開了同一個文件,打開該文件的每個進程都得到一個文件表項,但對一個給定的文件只有一個v節點表項。每個文件都有自己的文件表項的一個理由是:這種安排使每個進程都有它自己的對該文件的當前偏移量。

可能有多個文件描述符項指向同一個文件表項。例如dup函數,或者在fork後也會發生同樣的情況,此時父、子進程對於每一個打開文件文件描述符共享同一個文件表項。

因此多個進程讀同一文件都能正確工作,因爲每個進程都有它自己的文件表項,其中也有它自己的當前文件偏移量。但是多個進程寫同一個文件時,則可能產生預期不到的結果。

原子操作:

添寫至一個文件:

要在一個文件中進行添寫,第一種方法是“定位到文件尾端,然後寫”,它使用兩個分開的函數調用(lseek,write)。這種方法會出問題,任何一個需要多個函數調用的操作都不可能是原子操作,因爲在兩個函數之間,內核有可能會臨時掛起該進程。解決問題的方法是使這兩個操作對於其他進程而言成爲一個原子操作。

UNIX系統提供了一種方法使這種操作成爲原子操作,該方法是在打開文件時設置O_APPEND標誌。這就使得內核每次對該文件進行寫之前,都將進程的當前文件偏移量設置到該文件的尾端處,於是在每次寫之前就不再需要調用lseek。

pread和pwrite:

SUS包含了XSI擴展,該擴展允許原子性地定位搜索(seek)與執行I/O,pread和pwrite就是這種擴展:

#include <unistd.h>

ssize_t pread(int fieldes, void *buf, size_t nbytes, off_t offset);

返回值,讀到的字節數,若已到文件結尾則返回0,若出錯則返回-1;

ssize_t pwrite(int fieldes, const void *buf, size_t nbytes, off_t offset);

返回值,若成功則返回已寫的字節數,若出錯,則返回-1;

創建一個文件:

之前已經講過open函數的O_CREAT和O_EXCL選項,當同時指定這兩個選項,而該文件又已經存在時,open將失敗。這兩個選項使得檢查文件是否存在以及創建該文件這兩個操作是作爲一個原子操作執行的。

綜上所述,原子操作指的是由多步組成的操作,如果該操作原子地執行,則要麼執行完所有步驟,要麼一步也不執行,不可能只執行所有步驟的一個子集。

dup和dup2函數:

下面兩個函數都可用來複制一個現存的文件描述符:

#include <unistd.h>

int dup(int fieldes);

int dup2(int fieldes, int fieldes2);

兩函數的返回值:若成功則返回新的文件描述符,若出錯則返回-1。

由dup返回的新文件描述符一定是當前可用文件描述符中的最小數值,用dup2則可以用fieldes2參數指定新描述符的數值。如果fieldes2已經打開,則先將其關閉。如果fieldes等於fieldes2,則dup2返回filedes2,而不關閉它。

這些函數返回的新文件描述符與參數filedes共享同一個文件表項。因爲兩個描述符指向同一文件表項,所以它們共享同一文件狀態標誌(讀、寫、添寫等)以及同一當前文件偏移量。 但是每個文件描述符都有它自己的一套文件描述符標誌。

複製文件描述符的另一種方法是使用fcntl函數:

  • 調用dup(filedes) 等效於fcntl(filedes, F_DUPFD, 0);
  • 而調用dup2(filedes, filedes2)等效於close(filedes2); fcntl(filedes, F_DUPFD, filedes2);

但是後一種情況,dup2並不完全等同於close加上fcntl。它們之間的區別在於:
  • dup2是一個原子操作,而close及fcntl則包含兩個函數調用;
  • dup2和fcntl有某些不同的errno;

sync、fsync、fdatasync函數:

傳統的UNIX實現在內核中設有緩衝區高速緩存或頁面高速緩存,大多數磁盤I/O都通過緩衝進行。當將數據寫入文件時,內核通常先將該數據複製到其中一個緩衝區中,如果該緩衝區尚未寫滿,則並不將其排入輸出隊列,而是等待其寫滿或者內核需要重用該緩衝區以存放其它磁盤塊數據時,再將該緩衝排入到輸出隊列,然後待其到達隊首時,才進行實際的I/O操作。這種輸出方式被稱爲“延遲寫”。

延遲寫減少了磁盤讀寫次數,但是卻降低了文件內容的更新速度,使得欲寫到文件中的數據在一段時間內並沒有寫到磁盤上。當系統發生故障時,這種延遲可能造成文件更新內容的丟失。

爲了保證磁盤上實際文件系統與緩衝區高速緩存中內容的一致性,UNIX系統提供了sync,fsync和fdatasync三個函數。

#include <unistd.h>

int fsync(int filedes);

int fdatasync(int filedes);

返回值:若成功則返回0,若出錯則返回-1;

void sync(void);

sync函數只是將所有修改過的塊緩衝區排入寫隊列,然後就返回,它並不等待實際寫磁盤操作結束。通常稱爲update的系統守護進程會週期性地調用sync函數,命令sync(1)也調用sync函數。fsync函數只對由文件描述符filedes指定的單一文件起作用,並且等待寫磁盤操作結束,然後返回。fdatasync函數類似於fsync,但它隻影響文件的數據部分,而fsync還會同步更新文件的屬性。

fcntl函數:

fcntl函數可以改變已打開文件的性質:

#include <fcntl.h>

int fcntl(int filedes, int cmd, ... /* arg */);

返回值:若成功則依賴於cmd,若出錯則返回-1。

fcntl函數有5種功能:

  • 複製一個現有的描述符(cmd = F_DUPFD);
  • 獲得/設置文件描述符標記(cmd = F_GETFD或F_SETFD);
  • 獲得/設置文件狀態標誌(cmd = F_GETFL 或 F_SETFL);
  • 獲得/設置異步I/O所有權(cmd = F_GETOWN 或 F_SETOWN)
  • 獲得/設置記錄鎖(cmd = F_GETLK F_SETLK 或 F_SETLKW);

F_DUPFD:在上面已經講過了,複製文件描述符filedes,新文件描述符作爲函數值返回,它是尚未打開的各描述符中大於或等於第三個參數值中各值的最小值。

F_GETFD:對應於filedes的文件描述符標誌作爲函數值返回,當前只定義了一個文件描述符標誌FD_CLOEXEC;

F_SETFD:對於filedes設置文件描述符標誌,新標誌值按第三個參數設置。

F_GETFL:對應於filedes的文件狀態標誌作爲函數值返回,在說明open函數時,已經說明了文件狀態標誌(注意,O_RDONLY、O_WRONLY、O_RDWR三個訪問方式標誌並不各佔一位,因此首先要用屏蔽字O_ACCMODE取得訪問模式位,然後將結果與這三種值中的任何一種作比較)。

F_SETFL:將文件狀態標誌設置爲第三個參數的值,可以更改的幾個標誌是:O_APPEND,O_NONBLOCK,O_SYNC,O_DSYNC,O_RSYNC,O_FSYNC、O_ASYNC。

F_GETOWN:取當前接收SIGIO和SIGURG信號的進程ID或進程組ID;

F_SETOWN:設置接收SIGIO和SIGURG信號的進程ID和進程組ID,正的arg指定一個進程ID,負的arg表示等於arg絕對值的一個進程組ID;

下列程序接受一個文件描述符作爲參數,打印該文件描述符所指向的文件表項中的文件狀態標誌:

/*
 * Copyright (C) [email protected]
 */


#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>


int
main(int argc, char *argv[])
{
	int val;

	if (argc != 2) {
		printf("usage: ./fcntl fd\n");
		exit(1);
	}


	if ( (val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) {
		printf("fcntl error\n");
		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:
			printf("unknown access mode");
			break;
	}

	if (val & O_APPEND) {
		printf(", append");
	}
	if (val & O_NONBLOCK) {
		printf(", nonblocking");
	}
#if defined(O_SYNC)
	if (val & O_SYNC) {
		printf(", synchronous writes");
	}
#endif
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)
	if (val & O_FSYNC) {
		printf(", synchronous writes");
	}
#endif
	putchar('\n');
	
	exit(0);
}

上述使用了功能測試宏_POSIX_C_SOURCE,並且條件編譯了POSIX.1中沒有定義的文件訪問標誌。下面是該程序在bash中執行結果其中5<>temp.foo表示在文件描述符5上打開文件temp.foo以供讀寫:


在修改文件描述符標誌或文件狀態標誌時必須謹慎,先要取得現有的標誌值,然後根據需要修改它,最後設置新標誌值,不能只是執行F_SETFD或F_SETFL,這樣會關閉以前設置的標誌位:

下列程序顯示了對一個文件描述符設置一個或多個文件狀態標誌的範例:

/*
 * Copyright (C) [email protected]
 */


#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>


void
set_fl(int fd, int flags)
{
	int val;
	
	if ( (val = fcntl(fd, F_GETFL, 0)) < 0) {
		printf("get file flag error\n");
		return ;
	}

	val |= flags;

	if (fcntl(fd, F_SETFL, val) < 0) {
		printf("set file flag error\n");
	}
}

如果將 var |= flags 改爲 val &= ~flags則用來關閉某個標誌位。

程序運行時,設置O_SYNC標誌會增加時鐘時間。因爲每次write操作都要等待,直至數據已寫到磁盤上再返回。所以當支持同步寫時,系統時間和時鐘時間應當會顯著增加。

雖然可以在調用open函數時就設置文件狀態標誌,但是fcntl函數仍然非常有必要。fcntl函數允許在僅知道文件描述符的情況下修改其性質。例如,標準輸出是由shell打開的,因此我們無法通過open函數來設置其文件狀態標誌,但是通過fcntl函數可以做到。

ioctl函數:

ioctl函數是I/O操作的雜物箱,不能用其它函數表示的I/O操作通常都能用ioctl表示,終端I/O是iotcl的最大使用方面。

#include <unistd.h> /* System V */

#include <sys/ioctl.h> /* BSD and Linux */

#include <stropts> /* XSI STREAMS */

int ioctl(int filedes,int request,...);

iotcl函數只是SUS標準的一個擴展,以便處理STREAMS設備,但是UNIX系統實現用它進行很多雜項設備操作,有些實現甚至將它擴展到用於普通文件。

在此函數原型中,我們表示的只是iotcl函數本身所要求的頭文件。通常,還要求另外的設備專用頭文件。每個設備驅動程序都可以定義它自己專用的一組ioctl命令,系統則爲不同種類的設備提供通用的ioctl命令。

/dev/fd

較新的系統都提供名爲/dev/fd的目錄,其目錄項是名爲0,1,2等的文件,打開文件/dev/fd/n等效於複製描述符n(假定描述符n是打開的)。

在下列函數調用中:

fd = open("/dev/fd/0", mode);

大多數系統會忽略它所指定的mode,而另外一些則要求mode必須是所涉及的文件原先打開時所使用mode的子集。上面的函數調用等效於fd = dup(0)。所以描述符0和fd共享同一文件表項。

某些系統提供路徑名/dev/stdin、/dev/stdout/和/dev/stderr,這些等效於/dev/fd/0、/dev/fd/1和/dev/fd/2。

/dev/fd文件主要由shell使用,它允許那些使用路徑名作爲調用參數的程序,能用處理其它路徑名的相同方式處理標準輸入和輸出。雖然很多程序都支持在命令行中使用"-"作爲一個參數,特指標準輸入或輸出。但是/dev/fd則提高了文件名參數的一致性,也更加清晰。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章