1. 核心使用三種資料結構表示開啟的檔案,它們之間的關係決定了在檔案分享權限設定方面一個進程對另一個進程的影響。
(1). 每個進程在進程表中都一個記錄項(task_struct),包含一個開啟檔案描述符表(存放在使用者空間)。每個檔案描述符為表中一項,包括檔案描述符標識和指向一個檔案表項的指標。
(2). 核心為所有開啟檔案維持一張檔案表,每個檔案表項包括:
a). 檔案狀態標識(RD, WR, APPEND, 同步和非阻塞等)。注意,檔案描述符範圍是一個進程,而檔案狀態標識則適用於指向檔案表中該表項的所有進程的描述符。使用fcntl函數來修改這兩個結構。下面第3節將會解釋。
b). 當前檔案位移量
c). 指向該檔案v節點表項的指標。
(3). 每個開啟檔案或裝置都有一個v-node結構,包含檔案類型和對此檔案進行各種操作的函數的指標。對於大多數檔案,v-node還包含了該檔案的i-node節點(i-node包含檔案所有者、長度、所在裝置、指向檔案資料庫在磁碟上位置的指標等)。這些資訊是開啟檔案時從磁碟讀入記憶體的,所以所有關於檔案的資訊都是快速可以使用的。
Linux沒有使用v-node,而是使用了通用的i-node節點結構。雖然兩者實現有所不同,但在概念上,v-node和i-node是一樣的,都指向檔案系統相關的i-node結構。
三張表的關係,進程表項有一個進程開啟了標準輸出(fd=0)和標準輸入(fd=1)兩個檔案。所以task_struct結構的開啟檔案描述符表中有這兩項(每項包含fd標誌和檔案指標),其中檔案指標指向檔案表(每項包含檔案狀態標誌,當前檔案位移量和v-node指標),其中v-node指標指向v-node節點表(每項包含v-node info, i-node info&當前檔案長度等)。Linux沒有將相關資料結構分為i-node和v-node,而是使用一個獨立於檔案系統的i-node和一個依賴檔案系統的i-node。
如果兩個進程開啟了同一個檔案,則該檔案在檔案表中有兩項,這樣可以記錄每個進程對該檔案的當前位移量。但是v-node項中只有一項。在完成每個write操作後,在檔案表項中當前檔案位移量即增加所寫的位元組數。如果檔案位移量超過了檔案長度,則檔案長度更新為當前位移量。所以在使用O_APPEND標識開啟一個檔案,則相應標識也被設定到檔案表項中。每次寫檔案操作,首先會設定i-node表項的檔案長度,再將寫的資料添加到檔案末尾。無論lseek定位到哪裡,都只會修改當前檔案表項的位移量,而沒有進程任何I/0操作。
fork—父子進程會對每一個開啟的檔案描述符共用一個檔案表表項。
dup會使多個檔案描述符指向同一表項。
2. 由於檔案表中記錄了每個進程的當前位移量,所以多個進程同時讀一個檔案可以正確工作。但是多個進程寫同一個檔案就會產生的預料不到的結果,需要原子操作。
例如多個進程使用O_APPEND選項來open一個檔案,實際上這個寫的操作在原來版本的實現中相當於如下形式 :
if (lseek(fd, 0L, 2) < 0)
err_sys(“can not seek”);
if(lseek(fd, buf, 100) != 100)
err_sys(“can not write”)
如果不是原子操作,多個進程同時使用這種方法添加資料就會產生問題。(比如應用伺服器中多線程執行的多個組件寫日誌操作。)
在UNIX中擴充了lseek+read/write的原子性操作函數,原型為
#include <unistd.h>
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int filedes, void *buf, size_t nbytes, off_t offset);
從filedes的offset處(從檔案開始位置的位移量)讀取/寫入nbytes個字元。檔案當前位移量不改變。
調用pread相當於順序調用lseek 和read,但是pread 又與這種順序調用有下列重要區別:
1. 調用pread時,無法中斷其定位和讀操作。
2. 不更新檔案指標。
調用pwrite相當於順序調用lseek和write,同樣是原子操作且無法中斷。
3. 另一種需要原子操作的情形:在使open函數
自動建立檔案時,即不帶選項O_CREAT,O_EXCL(如果檔案已經存在,則出錯。如果不存在則建立該檔案),以避免抹掉原有檔案的記錄。
if ((fd=open(pathname, O_WRONLY)) < 0) {
if(errno == ENOENT) { //no such file or directory
if ((fd = creat(pathname, mode)) < 0)
err_sys(“creat error”);
} else {
err_sys(“open error”);
}
}
同樣在open和creat之間也可能導致問題。例如進程A執行open沒有檢測到原有檔案,然後調度到進程B建立檔案並寫入了一些東西。這是進程A繼續執行creat部分,新建立的檔案會清除進程B建立的檔案。需要引入記錄鎖機制(APUE Chapter 14.3)。
記錄鎖record locking的功能是:當一個進程讀或者修改檔案的某個部分時,阻止其他進程修改同一檔案。由於UNIX系統核心沒有使用檔案記錄的概念,所以更適合的而屬於應該是位元組範圍鎖,因為它鎖定的只是檔案的一個地區。
Linux2.4 以上的核心支援flock函數(鎖住整個檔案), fcntl記錄鎖和lockf。
#include <fcntl.h>
int fcntl(int filedes, int cmd, …/*struct flock */);
struct flock{
short l_type; /*F_RDLCK, F_WRLCK or F_UNLCK*/
/*共用讀鎖,獨佔性寫鎖和解鎖一個地區*/
off_t l_start; /*offset in bytes, relative to l_whence*/
/*要加鎖的地區起始位元組位移量*/
short l_whence;/*SEEK_SET, SEEK_CUR, SEEK_END*/
off_t l_len; /*length, in bytes; 0 means lock to EOF*/
/*所以為了鎖住整個檔案需要將whence,start設為SEEK_SET 0, l_len為0*/
pid_t l_pid; /*returned with F_GETLK*/
};
鎖的類型分為兩種F_RDLCK共用讀鎖和獨佔寫鎖F_WRLCK。基本規則是:多個進程在一個給定的位元組上可以有一把共用的讀鎖,但是在一個給定位元組上只能有一個進程獨用一把寫鎖。即如果在某個位元組上已經有一把獨佔性寫鎖,則不能再對它加任何讀鎖。
不同進程之間鎖請求:
讀鎖 寫鎖
無鎖 允許 允許
一個或多個讀鎖 允許 拒絕
一個寫鎖 拒絕 拒絕
相同進程之間鎖請求:
如果一個進程對一個檔案區間已經有一把鎖,該進程後面又企圖在同一檔案區間再加一個鎖,那麼新鎖將替換老鎖。
加讀鎖時,該描述副必須是讀開啟的。加寫鎖鎖必須好是寫開啟的。
F_GETLK判斷由flockptr所描述的鎖是否會被另一把鎖所排斥。如果存在一把鎖,它阻止建立由flockptr鎖描述鎖。如果不存在,則除了將l_type設定為F_UNLCK;
F_SETLK
F_SETLKW F_SETLK的阻塞版本。如果請求建立的鎖已經可用,可進程被喚醒。
鎖的隱含整合和釋放:
(1).進程中止時,所有建立的鎖都釋放。
(2).關閉一個描述符,則該進程通過這一描述符可以引用的任何一把鎖(前提是這些鎖由該進程設定的)都將釋放。例如
fd1=open(pathname, …);
read_lock(fd1, …);
fd2 = dup(fd1);
close(fd2);
由於釋放了fd2,這時fd1設定的鎖也會被釋放。
fork產生的子進程不整合父進程鎖設定的鎖。子進程需要調用fcntl才能獲得自己的鎖。這樣防止父進程和子進程同時寫一個檔案。
在執行exec後,新程式可以繼承原執行程式的鎖。但是注意,如果對一個檔案描述符設定了close-on-exec那麼exec時關閉檔案描述符,相應的檔案所有鎖都被釋放了。
建議鎖又稱協同鎖。對於這種類型的鎖,核心只是提供加減鎖以及檢測是否加鎖的操作,但是不提供鎖的控制與協調工作。也就是說,如果應用程式對某個檔案進行操作時,沒有檢測是否加鎖或者無視加鎖而直接向檔案寫入資料,核心是不會加以阻攔控制的。因此,建議鎖,不能阻止進程對檔案的操作,而只能依賴於大家自覺的去檢測是否加鎖然後約束自己的行為;
強制鎖,是OS核心的檔案鎖。每個對檔案操作時,例如執行open、read、write等操作時,OS內部檢測該檔案是否被加了強制鎖,如果加鎖導致這些檔案操作失敗。也就是核心強制應用程式來遵守遊戲規則;
例如vim使用建議鎖來編輯一個檔案,開啟多個檔案時會提示這個檔案已加鎖/處於編輯中。但是對於沒有使用建立鎖的編輯器依然可以開啟並編輯這個檔案,檔案的最後狀態取決於最後一個操作檔案的進程。
4. dup和dup2函數:用於複製一個現存的檔案描述符。
#include <unistd.h>
int dup(int filedes); //返回分配的是當前可用檔案描述符的最小值
//它的操作等效於fcntl(filedes, F_DUPFD, 0);
int dup2(int filedes, int filedes2); //複製結果為返回filedes2指定的描述符。如果filedes2指定的描述符已經開啟,則先關閉它。如果filedes和filedes2相同則不關閉。它的操作等效於close(filedes2);fcntl(filedes, F_DUPFD, filedes2);但是dup2是一個原子操作,不用擔心同步問題。
這兩個函數返回的檔案描述符與參數filedes共用一個檔案表項。
5. sync, fsync和fdatasync函數
當將資料寫入檔案時,核心通常先將該資料複製到其中一個緩衝區中,如果該緩衝區尚未寫滿。則並不將其排入輸出隊列。而是等待其寫滿或者當核心需要重用該緩衝區以便存放其他磁碟塊資料時,再將該緩衝區排入輸出隊列。然後待其到達隊首時,才進行實際的I/O才做,這種輸出方式成為延時寫delayed write。可以減少磁碟I/O次數,缺點是降低了檔案系統的更新次數。系統故障時可能造成內容丟失。UNIX系統提供了sync、fsync和fdatasync三個函數來保證磁碟上實際檔案系統與緩衝區高速緩衝中內容的一致性。
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
void sync(void);
sync只將所有修改過的塊緩衝區排入寫隊列,然後就返回。不等待實際的磁碟操作結束。通常成為update的系統守護進程會周期性地(30s)調用sync函數。這就保證定期沖洗核心的塊緩衝區。
fsync只對檔案描述符filedes指定的單一檔案起作用,並等待寫磁碟操作然後返回。fsync對資料庫這樣的應用程式確保了修改過的塊會寫回到磁碟上。(比如Oracle的redo log需要這種操作!)
fdatesync 函數類似fsync,但它隻影響檔案的資料部分。而除資料之外,fsync還包括檔案屬性的更新。
6. fcntl函數:改變已開啟檔案的性質。
#include <fcntl.h>
int fcntl(int filedes, int cmd, …/*一個整數或者記錄指標*/);
實現功能:
(1). 複製一個現有檔案描述符cmd = F_DUPFD;新檔案描述符作為函數值返回
(2). 獲得/設定檔案描述符標記cmd = F_GETFD或F_SETFD;注意:使用F_SETFD和SETFL時必須要謹慎。需要先獲得當前值根據需要來修改它。
(3). 獲得/設定檔案狀態標識cmd = F_GETFL或F_SETFL; F_GETFL對應的檔案標識作為函數值返回。O_開頭的(open函數部分有說明)。三個訪問標識位O_RDWR,O_WRONLY,O_RDONLY並不各佔一位,他們互斥。需要用屏蔽字O_ACCMODE取得訪問模式位再與其中一種作比較。F_GETFL將檔案部分設定為第三個參數。設定檔案狀態標識時常用或|運算,~取補運算。例如當前若干設定為flags|=O_APPEND;等。首先獲得當前值,int val; (val=fcntl(fd, F_GETFL, 0) ) >= 0, 設定flags相關選項為val|=flags; 關閉flags相關選項為val&=~flags;
如果設定了O_SYNC那麼每次write都要等待直到資料寫到硬碟再返回。資料庫系統需要這種機制以確認write返回時資料確定寫到磁碟上,以免系統崩潰時資料丟失。但是會大大增加系統時鐘時間。
(4). 獲得/設定非同步I/O所有權cmd = GETOWN後SETOWN;擷取/設定當前SIGIO和SIGURG訊號的進程ID和進程組ID。
(5). 獲得/設定記錄所cmd = F_GETLK, F_SETLK或者F_SETLKW;
#include "apue.h"
-
- #include <fcntl.h>
-
-
-
- int
-
- main(int argc, char *argv[])
-
- {
-
- int val;
-
-
-
- if (argc != 2)
-
- err_quit("usage: a.out <descriptor#>");
-
-
-
- if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
-
- err_sys("fcntl error for fd %d", atoi(argv[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: err_dump("unknown access mode");
-
- }
-
-
-
- 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);
-
- }