linux 文件鎖

創建鎖文件對於資源的排他訪問,例如串口,是相當合適的,但是對於訪問大的共享文件就是太好了。假如我們擁有一個由一個程序寫入的大文件,但是是由許多不同的程序進行持續更新的。當一個程序正在記錄一些在較長的時間內所得到的數據,並且正在爲其他的一些程序進行處理時就會出現這樣的情況。這些正在處理的程序並不會等待日誌程序結束--他們是連續運行的--所以他們需要一些合作的方法從而可以提供對於同一個文件的同時訪問。

我們可以通過鎖住文件的一個區域來到達這種結果,這樣只是文件的某一個區域被鎖住,但是其他程序可以訪問程序的其他部分。這稱之爲文件段(file-segment),或是文件區域(file-region)。Linux有兩種方法可以做到這一點:使用fcntl系統調用與使用lockf調用。我們會主要了解fcntl接口,因爲這是最經常用到的接口。lockf是相對較爲簡單的,並且在Linux上只是fcntl的替換接口用法。然而,fcntl與lockf鎖機制不可以同時工作:他們使用不同的底層實現,所以我們不能混用這兩種調用;只使用這一種或是另一種。

我們在第3章介紹了fcntl調用。其定義如下:

#include <fcntl.h>
int fcntl(int fildes, int command, ...);

fcntl在文件描述符上進行操作,並且依據command參數可以執行不同的任務。而我們所感興趣的有關文件鎖的三個:

❑ F_GETLK
❑ F_SETLK
❑ F_SETLKW

當我們使用這些命令時,第三個參數必須是一個指向struct flock的指針,所以實際上的原型形式如下:

int fcntl(int fildes, int command, struct flock *flock_structure);

flock結構是依賴於實現的,但是他至少包含下面的成員:

❑ short l_type;
❑ short l_whence;
❑ off_t l_start;
❑ off_t l_len;
❑ pid_t l_pid;

l_type成員可以是幾個值中的一個,這些值通常定義在fcntl.h中。如下表所示:

值 描述
F_RDLCK 共享鎖(或讀鎖)。多個進程可以在文件的相同區域(或重疊)具有一個共享鎖。如果任何進程在文件的某一部分具有一個共享鎖,其他的進程就不可以在相同的區域獲得排他鎖。爲了獲得一個共享鎖,文件必須使用讀或是讀寫訪問模式打開。
F_UNLCK 解鎖;用於清除鎖。
F_WRLCK 排他鎖(或寫鎖)。在文件的某一個特定區域只可以有一個進程獲得排他鎖。一旦有一個進程具有一個這樣的鎖,其他的進程就不可以在此區域上獲得任何鎖類型。要獲得一個排他鎖,文件必須以寫或是讀寫模式打開。

l_whence成員定義了一個文件中的區域--一個連續的字節集合。l_whence的值必須是SEEK_SET,SEEK_CUR,SEEK_END中的一個(定義在unistd.h)中。他們分別對應於開始位置,當前位置以及文件結尾。l_whence定義了相對於l_start的偏移,l_start爲區域的第一個字節。通常,這個值爲SEEK_SET,所以l_start通常由文件的開始處算起。l_len參數定義了區域中的字節數。

l_pid參數用於報告存放鎖的進程。由後面的F_GETLK更詳細的描述了這一點。

文件中的第一個字節在任意時刻只能具有一種鎖類型,或者是共享鎖,或者是排他鎖,或者是解鎖。

fcntl調用的命令與選項有幾種組合,下面我們會依次進行討論。

F_GETLK命令

第一個命令是F_GETLK。他會獲得打開的文件fildes的鎖信息。他並不會嘗試爲文件加鎖。這個調用進程傳遞他希望創建的鎖的類型信息,並且使用F_GETLK命令的fcntl調用會返回阻止加鎖的信息。

flock結構所使用的值如下表所示:

值 描述
l_type 或者是共享鎖F_RDLCK,或者是排他鎖F_WRLCK
l_whence SEEK_SET,SEEK_CUR,SEEK_END其中的一個
l_start 感興趣文件區域的起始字節
l_len 感興趣的文件區域中的字節數
l_pid 帶有鎖的進程的標識符

一個進程可以使用F_GETLK來確定一個文件區域的加鎖狀態。他應該設置flock結構來標識他所需要的鎖類型並且定義所感興趣的文件區域。如果fcntl調用成功則會返回一個非-1的值。如果文件已經具有會阻止所請求執行鎖的鎖時,他就會使用相應的信息來覆蓋flock結構。如果請求鎖成功,flock結構則不會發生變化。如果F_GETLK調用不能獲取相應的信息,他就會返回-1來標識失敗。

如果F_GETLK調用成功(例如,他返回一個非-1的值),調用程序必須檢測flock結構的內容以確定他是否被修改。因爲l_pid的值會被設置爲鎖進程的值(如果查找成功),這是一個確定flock結構是否發生變化的合理區域。

F_SETLK命令

這個集合會嘗試加鎖或是解鎖fildes所指向的文件區域。flock結構中會用到的值(與F_GETLK所用到的值不同)如下表所示:

值 描述
l_type l_type可以只讀或是共享的F_RDLCK或是F_WRLCK。或者是隻讀或是共享的F_RDLCK;或者是排他或是寫入的F_WRLCK;或者是解鎖的F_UNLCK。
l_pid 不使用

如果加鎖成功,fcntl會返回一個非-1的值;如果失敗,則會返回-1。函數調用會立刻返回。

F_SETLKW命令

F_SETLKW命令與上面的F_SETLK命令相似,所不同的是如果他不能獲得鎖,他就會等待,直到可以獲得鎖爲止。一旦這個調用開始等待,他就只會在可以獲得鎖或是有信號發生時才返回。我們會在第11章討論信號

程序在一個文件上加的所有鎖都會在相應的文件描述符關閉時進行原子清除。當程序結束時也會自動執行這些動作。

使用鎖進行讀寫操作

當我們在一個文件區域上使用鎖時,使用底層的read與write調用來訪問文件中的數據是相當重要的,而不是高層的fread與fwrite。這是必須的,因爲fread與fwrite會在庫在執行數據的讀寫緩衝,所以執行一個fread調用來讀取一個文件的前100個字節也許(事實上,通常會這樣)會讀取多於100個字節,並且在庫中緩衝其餘的數據。如果程序然後使用fread來讀取接下來的100個字節,他實際上會讀取緩衝在庫中的數據,並且不允許底層的read調用由文件中讀取更多的數據。

要了解爲什麼這是一個問題,考慮兩個程序要更新同一個文件。假設這個文件由200個字節的全0數據組成。第一個程序首先運行,並且在文件的前100個字節上獲得了一個寫鎖。然後他使用fread來在這個100個字節中進行讀取。然而,正如我們在前面的章節所看到的,fread會一次讀取直到BUFSIZ字節的數據,所以實際上他會將所有文件讀取到內存中,但只會將前100個字節傳遞迴程序。

然後第二個程序啓動。他在程序的後100個字節上獲得一個寫鎖。這也會成功,因爲第一個程序只鎖住了前100個字節。第二個程序在100到199字節上寫入2,然後關閉文件,解鎖,退出。第一個程序然後鎖住文件的後100個字節,並且調用fread來讀取。因爲數據進行了緩衝,程序實際上看到的是100個0,而不是文件中實際存在的100個2。當我們使用read與write時就不會出這樣的問題。

上面所描述的加鎖也許會有些複雜,但是使用起來並沒有描述的那樣困難。

試驗--使用fcntl鎖住文件

下面讓我們來看一下文件鎖是如何工作的:lock3.c。要試驗文件鎖,我們需要兩個文件:一個用於鎖住文件,一個用於測試。第一個程序實現鎖功能。

1 程序代碼以必要的文件包含和變量聲明開始:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
const char *test_file = “/tmp/test_lock”;
int main()
{
int file_desc;
int byte_count;
char *byte_to_write = “A”;
struct flock region_1;
struct flock region_2;
int res;

2 打開一個文件描述符:

file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if (!file_desc) {
fprintf(stderr, “Unable to open %s for read/write/n”, test_file);
exit(EXIT_FAILURE);
}

3 在文件中寫入一些數據:

for(byte_count = 0; byte_count < 100; byte_count++) {
(void)write(file_desc, byte_to_write, 1);
}

4 使用共享鎖設置區域1,由10到30字節:

region_1.l_type = F_RDLCK;
region_1.l_whence = SEEK_SET;
region_1.l_start = 10;
region_1.l_len = 20;

5 使用排他鎖設置區域2,由40到50字節:

region_2.l_type = F_WRLCK;
region_2.l_whence = SEEK_SET;
region_2.l_start = 40;
region_2.l_len = 10;

6 現在鎖住文件:

printf(“Process %d locking file/n”, getpid());
res = fcntl(file_desc, F_SETLK, &region_1);
if (res == -1) fprintf(stderr, “Failed to lock region 1/n”);
res = fcntl(file_desc, F_SETLK, &region_2);
if (res == -1) fprintf(stderr, “Failed to lock region 2/n”);

7 然後等待一會:

sleep(60);
printf(“Process %d closing file/n”, getpid());
close(file_desc);
exit(EXIT_SUCCESS);
}

工作原理

這個程序首先創建了一個文件,以讀寫的方式打開,並向其中填充一些數據。然後他設置兩個區域:第一個由10到30字節,使用共享鎖,而第二個由40到50字節,使用排他鎖。然後程序調用fcntl來鎖住兩個區域,並且在程序關閉文件退出之前等待一會。

就程序本身而言,程序並沒有多大用處。我們需要另一個文件來測試文件鎖,lock4.c。

試驗--在文件上測試文件鎖

下面我們來編寫一個程序來測試文件上不同區域的鎖類型。

1 如平常一樣,我們的程序代碼以必要的文件包含和變量聲明開始:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
const char *test_file = “/tmp/test_lock”;
#define SIZE_TO_TRY 5
void show_lock_info(struct flock *to_show);
int main()
{
int file_desc;
int res;
struct flock region_to_test;
int start_byte;

2 打開一個文件描述符:

file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if (!file_desc) {
fprintf(stderr, “Unable to open %s for read/write”, test_file);
exit(EXIT_FAILURE);
}
for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {

3 設置我們希望測試的文件區域:

region_to_test.l_type = F_WRLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
printf(“Testing F_WRLCK on region from %d to %d/n”,
start_byte, start_byte + SIZE_TO_TRY);

4 測試文件鎖:

res = fcntl(file_desc, F_GETLK, &region_to_test);
if (res == -1) {
fprintf(stderr, “F_GETLK failed/n”);
exit(EXIT_FAILURE);
}
if (region_to_test.l_pid != -1) {
printf(“Lock would fail. F_GETLK returned:/n”);
show_lock_info(&region_to_test);
}
else {
printf(“F_WRLCK - Lock would succeed/n”);


} 5 使用共享鎖重複此操作。再次設置我們希望測試的文件區域:

region_to_test.l_type = F_RDLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
printf(“Testing F_RDLCK on region from %d to %d/n”,
start_byte, start_byte + SIZE_TO_TRY);

6 再次測試文件鎖:

res = fcntl(file_desc, F_GETLK, &region_to_test);
if (res == -1) {
fprintf(stderr, “F_GETLK failed/n”);
exit(EXIT_FAILURE);
}
if (region_to_test.l_pid != -1) {
printf(“Lock would fail. F_GETLK returned:/n”);
show_lock_info(&region_to_test);
}
else {
printf(“F_RDLCK - Lock would succeed/n”);
}
}
close(file_desc);
exit(EXIT_SUCCESS);
}
void show_lock_info(struct flock *to_show) {
printf(“/tl_type %d, “, to_show->l_type);
printf(“l_whence %d, “, to_show->l_whence);
printf(“l_start %d, “, (int)to_show->l_start);
printf(“l_len %d, “, (int)to_show->l_len);
printf(“l_pid %d/n”, to_show->l_pid);
}

要測試文件鎖,我們首先需要運行lock3程序;然後我們運行lock4程序來測試鎖文件。我們可以用下面的命令來使得lock3程序在後臺運行:

$ ./lock3 &
$ process 1534 locking file

返回命令提示符是因爲lock3在後臺運行,然後我們用下面的命令來運行lock4程序:

$ ./lock4

我們得到的程序輸出如下所示:

Testing F_WRLOCK on region from 0 to 5
F_WRLCK - Lock would succeed
Testing F_RDLOCK on region from 0 to 5
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 10 to 15
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 10 to 15
F_RDLCK - Lock would succeed
Testing F_WRLOCK on region from 15 to 20
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 15 to 20
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 25 to 30
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 25 to 30
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534
Testing F_RDLOCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534
...
Testing F_RDLOCK on region from 95 to 100
F_RDLCK - Lock would succeed

工作原理

對於文件中的每五個字節組,lock4設置一個區域結構來測試文件鎖,然後程序使用這個區域結構進行測試以確定其爲讀鎖還是寫鎖。返回的顯示了區域字節數,相對於零字節的偏移量,這會使得鎖請求失敗。因爲返回結構的l_pid部分包含當前使得文件鎖住的程序的進程標識,程序將其設置爲-1,然後當fcntl調用返回時,程序會檢測其是否發生了改變。如果所測試的區域並沒有被加鎖,l_pid就不會發生改變。

要理解程序的輸出,我們需要查看所包含的fcntl.h文件,由此我們可以瞭解l_type的值爲1是由F_WRLCK的值爲1時得來的,而l_type的值爲0是由F_RDLCK的值爲0得來的。所以,l_type的值爲1告訴我們由於排他的寫鎖而加鎖失敗,而l_type的值0是由已經存在讀鎖而引起的。在文件中這塊區域lock3程序並沒有加鎖,所以共享鎖與排他鎖都是成功的。

由10到30字節,我們可以看到他可能會獲得一個共享鎖,因爲lock3程序此處存在的是共享鎖,而不是排他鎖。在40到50字節區域,兩種類型的鎖都會失敗,因爲lock3程序在此處區域加了排他鎖。

 

有三種不同的文件鎖,這三種都是“諮詢性”的,也就是說它們依靠程序之間的
合作,所以一個項目中的所有程序封鎖政策的一致是非常重要的,當你的程序需
要和第三方軟件共享文件時應該格外地小心。

有些程序利用諸如 FIlENAME.lock 的文件鎖文件,然後簡單地測試此類文件是否存在。這種方法顯然不太好,因爲當產生文件的進程被殺後,鎖文件依然存在,這樣文件也許會被永久鎖住。UUCP 中把產生文件的進程號PID存入文件,但這樣做仍然不保險,因爲PID的利用是回收型
這裏是三個文件鎖函數:
flock();
lockf();
fcntl();

flock()是從BSD中衍生出來的,但目前在大多數UNIX系統上都能找到,在單個主
機上flock()簡單有效,但它不能在NFS上工作。Perl中也有一個有點讓人迷惑的
flock()函數,但卻是在perl內部實現的。

fcntl()是唯一的符合POSIX標準的文件鎖實現,所以也是唯一可移植的。它也同
時是最強大的文件鎖--也是最難用的。在NFS文件系統上,fcntl()請求會被遞
交給叫rpc.lockd的守護進程,然後由它負責和主機端的lockd對話,和flock()
不同,fcntl()可以實現記錄層上的封鎖。

lockf()只是一個簡化了的fcntl()文件鎖接口。

無論你使用哪一種文件鎖,請一定記住在鎖生效之前用sync來更新你所有的文件
輸入/輸出。

[code] lock(fd);
write_to(some_function_of(fd));
flush_output_to(fd); /* 在去鎖之前一定要衝洗輸出 */
unlock(fd);
do_something_else; /* 也許另外一個進程會更新它 */
lock(fd);
seek(fd, somewhere); /* 因爲原來的文件指針已不安全 */
do_something_with(fd);[/code] ...

一些有用的fcntl()封鎖方法(爲了簡潔略去錯誤處理):


[code] #include <fcntl.h>;
#include <unistd.h>;

read_lock(int fd) /* 整個文件上的一個共享的文件鎖 */
{
fcntl(fd, F_SETLKW, file_lock(F_RDLCK, SEEK_SET));
}

write_lock(int fd) /* 整個文件上的一個排外文件鎖 */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_SET));
}

append_lock(int fd) /* 一個封鎖文件結尾的鎖,
其他進程可以訪問現有內容 */
{
fcntl(fd, F_SETLKW, file_lock(F_WRLCK, SEEK_END));
}[/code]
前面所用的file_lock函數如下:

[code] struct flock* file_lock(short type, short whence)
{
static struct flock ret ;
ret.l_type = type ;
ret.l_start = 0 ;
ret.l_whence = whence ;
ret.l_len = 0 ;
ret.l_pid = getpid() ;
return &ret ;
}[/code]

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