文件鎖定是多用戶、多任務操作系統中一個非常重要的組成部分。程序經常需要共享數據,而這通常是通過文件來實現的。因此,對於這些程序來說,建立某種控制文件的方式就非常重要了。只有這樣,文件才可以通過一種安全的方式更新,或者說,當一個程序正在對文件進行寫操作時,文件就會進入一個暫時狀態在這個狀態下,如果另外一個程序嘗試讀這個文件,它就會自動停下來等待這個狀態的結束。
Linux提供了多種特性來實現文件鎖。其中最簡單的方法就是以原子操作方式創建鎖文件,所謂"原子操作"就是在創建鎖文件時,系統將不允許其他的事情發生。這就給程序提供了一種方式來確保它所創建是文件是唯一的,而且這個文件不可能被其它程序在同一時刻創建。第二種方法更高級一些,它允許程序鎖定文件的一部分,從而可以獨享對這一部分內容的訪問。有這兩種不同的方式可以實現第二種形式的文件鎖定。我們將只對其中的一種做詳細介紹,因為兩種方式非常相似——第二種方式不過是程序接口稍微不同而已。
一、這些鎖文件通常都被放置在一個特定位置,並帶有一個與控制資源相關的文件名。例如,當一個調制解調器正在
被使用時,Linux通常會在/var/spool目錄下創建一個鎖文件。
注意:鎖文件僅僅只是充當一個指示器的角色,程序間需要通過相互協作來使用它們。
二、區域鎖定
用創建鎖文件的方法來控制對諸如串行口或者不經常訪問的文件之類的資源的獨占式訪問,是一個不錯的選擇,但它並不適用於訪問大型的共享文件。假設你有一個大文件,它有一個程序寫入數據,但卻有許多不同的程序同時對這個文件進行更新。當一個程序負責記錄長期以來連續收集到的數據,而其他一些程序將一直不停地運行,所以它們需要一些協調方法來提供對一個文件的並發訪問。可以通過鎖定文件區域的方法來解決這個問題,文件中的某個特定部分被鎖定了,但是其他程序可以訪問這個文件中的其它部分。這被稱為文件段鎖定或文件區域鎖定。Linux提供了至少兩種方式來實現這一功能:它們使用不同的底層實現,因此決不要混合使用這兩種類型的調用,而應堅持使用其中的一種。
三、文件鎖功能函數
int fcntl(int fileds, int command, struct flock *flock_structure);
fcntl對一個打開的文件描述符進行操作,並根據command參數的設置完成不同的任務。
command參數:F_GETLK、F_SETLK、F_SETLKW;
flock結構體:short l_type、short l_whence、off_t l_shart、off_t l_len、pid_t l_pid。
1、F_GETLK:用於獲取files(第一個參數)打開的文件的鎖信息。它不會嘗試去鎖定文件。調用進程把自己想創建的鎖類型信息傳遞給fcntl,使用F_GETLK命令的fcntl就會返回將會阻止獲取鎖的任何信息。
short l_type:如果是共享(只讀)鎖則取值為F_RDLCK,如果是獨占(寫)鎖則取值為F_WRLCK
short l_whence:SEEK_SET、SEEK_CUR、SEEK_END中的一個
off_t l_shart:感興趣的文件區域的第一個字節的相對位置
off_t l_len:感興趣的文件區域的字節數
pid_t l_pid:持有鎖的進程的標識符
進程可能使用F_GETLK調用來查看文件中某個區域的當前鎖狀態。它應該設置flock結構體來表明它需要的鎖類型,並定義它感興趣的文件區域。fcntl調用如果成功就返回非-1的值。如果文件已被鎖定從而阻止鎖請求成功執行,fcntl會用相關信息覆蓋flock結構。如果鎖請求可以成功執行,flock結構將保持不變。如果F_GETLK調用無法獲得信息,它將返回-1表明失敗。如果F_GETLK調用成功,調用程序就必須檢查flock結構的內容來判斷其是否被修改過。因為l_pid的值被設置成持有鎖的進程(若有的話)的標識符,多以通過檢查這個字段就可以很方便地判斷出flock結構是否被修改過。
2、F_SETLK:對files指向的文件的某個區域加鎖或解鎖。flock結構中使用的值(與F_GETLK命令中用到的不同之
處)如下:
l_type:如果是只讀或共享鎖則取值為F_RDLCK,如果是獨占或寫鎖則取值為F_WRLCK,如果是解鎖則取值為F_UNLK
l_pid:不使用與F_GETLK一樣,要加鎖的區域由flock結構中的l_start、l_whence和l_en的值定義。如果加鎖成功,fcntl將返回一個非-1的值;如果失敗,則返回-1.這個函數總是立刻返回。
3、F_SETLKW:與介紹的F_SETLK作用相同,但在無法獲取鎖時,這個調用將等待直到可以為止。一旦這個調用開始等待,只有在可以獲取鎖或收到一個信號時它才會返回。程序對某個文件擁有的所有鎖都將在響應的文件描述符被關閉時自動清除。在程序結束時也會自動清除各種鎖。
四、鎖定狀態下的讀寫操作
當對文件區域加鎖之後,必須使用底層的read和write調用來訪問文件中的數據,而不要使用更高級的fread和fwrite調用,這是因為fread和fwrite會對讀寫的數據進行緩存,所以執行一次fread調用來讀取文件中的頭100個字節可能(事實上,是幾乎肯定如此)會讀取超過100個字節的數據,並將多余的數據在函數庫中進行緩存。如果程序再次使用fread來讀取下100個字節的數據,它實際上將讀取已緩存在函數庫中的數據,而不會引發一個底層的read調用來從文件中讀取更多的數據。
例1:測試文件上的鎖
lock.c-->lock
#include#include #include #include const char *test_file="test_lock"; int main(int argc, char *argv[]) { int file_desc; int byte_count; char *byte_to_write="A"; struct flock region_1; struct flock region_2; int res; 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); } for(byte_count=0;byte_count<100;byte_count++) { write(file_desc,byte_to_write,1); } //把文件的10~30字節設為區域1,並在其上設置共享鎖 region_1.l_type=F_RDLCK; region_1.l_whence=SEEK_SET; region_1.l_start=10; region_1.l_len=20; //把文件的40~50字節設為區域2,並在其上設置獨占鎖 region_2.l_type=F_WRLCK; region_2.l_whence=SEEK_SET; region_2.l_start=40; region_2.l_len=10; //鎖定文件 printf("Process %d locking file\n",getpid()); res=fcntl(file_desc,F_SETLK,®ion_1); if(res==-1)fprintf(stderr,"Failed to lock region_1\n"); res=fcntl(file_desc,F_SETLK,®ion_2); if(res==-1)fprintf(stderr,"Failed to lock region_2\n"); sleep(60); printf("Process %d closing file\n",getpid()); close(file_desc); exit(EXIT_SUCCESS); return 0; }
lock2.c -->lock2
#include#include #include #include const char *test_file="test_lock"; #define SIZE_TO_TRY 5 void show_lock_info(struct flock *to_show); int main(int argc, char *argv[]) { int file_desc; int res; struct flock region_to_test; int start_byte; 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) { //設置希望測試的文件區域 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); //測試文件上的鎖 res=fcntl(file_desc,F_GETLK,®ion_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(®ion_to_test); } else { printf("F_WRLCK - Lock would succed\n"); } //用共享(讀)鎖重復測試一次,再次設置希望測試的文件區域 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); //再次測試文件上的鎖 res=fcntl(file_desc,F_GETLK,®ion_to_test); if(res==-1) { fprintf(stderr,"F_GETLK faild\n"); exit(EXIT_FAILURE); } if(region_to_test.l_pid!=-1) { printf("Lock would fail. F_GETLK returned:\n"); show_lock_info(®ion_to_test); } else { printf("F_RDLCK - Lock would succeed\n"); } } close(file_desc); return 0; } void show_lock_info(struct flock *to_show) { printf("\t l_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); }
為了測試首先運行./lock &(後台運行),然後運行./lock2
下面為輸出內容:
Testing F_WRLCK on region from 0 to 5 F_WRLCK - Lock would succed Testing F_RDLCK on region from 0 to 5 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 5 to 10 F_WRLCK - Lock would succed Testing F_RDLCK on region from 5 to 10 F_RDLCK - Lock would succeed Testing F_WRLCK 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 6448 Testing F_RDLCK on region from 10 to 15 F_RDLCK - Lock would succeed Testing F_WRLCK 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 6448 Testing F_RDLCK on region from 15 to 20 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 20 to 25 Lock would fail. F_GETLK returned: l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 6448 Testing F_RDLCK on region from 20 to 25 F_RDLCK - Lock would succeed Testing F_WRLCK 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 6448 Testing F_RDLCK on region from 25 to 30 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 30 to 35 F_WRLCK - Lock would succed Testing F_RDLCK on region from 30 to 35 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 35 to 40 F_WRLCK - Lock would succed Testing F_RDLCK on region from 35 to 40 F_RDLCK - Lock would succeed Testing F_WRLCK 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 6448 Testing F_RDLCK 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 6448 Testing F_WRLCK on region from 45 to 50 Lock would fail. F_GETLK returned: l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 6448 Testing F_RDLCK on region from 45 to 50 Lock would fail. F_GETLK returned: l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 6448 Testing F_WRLCK on region from 50 to 55 F_WRLCK - Lock would succed Testing F_RDLCK on region from 50 to 55 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 55 to 60 F_WRLCK - Lock would succed Testing F_RDLCK on region from 55 to 60 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 60 to 65 F_WRLCK - Lock would succed Testing F_RDLCK on region from 60 to 65 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 65 to 70 F_WRLCK - Lock would succed Testing F_RDLCK on region from 65 to 70 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 70 to 75 F_WRLCK - Lock would succed Testing F_RDLCK on region from 70 to 75 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 75 to 80 F_WRLCK - Lock would succed Testing F_RDLCK on region from 75 to 80 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 80 to 85 F_WRLCK - Lock would succed Testing F_RDLCK on region from 80 to 85 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 85 to 90 F_WRLCK - Lock would succed Testing F_RDLCK on region from 85 to 90 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 90 to 95 F_WRLCK - Lock would succed Testing F_RDLCK on region from 90 to 95 F_RDLCK - Lock would succeed Testing F_WRLCK on region from 95 to 100 F_WRLCK - Lock would succed Testing F_RDLCK on region from 95 to 100 F_RDLCK - Lock would succeed
解析:
lock2程序把文件中的每5個字節分成一組,為每個組設置一個區域結構來測試鎖,然後通過使用這些結構來判斷對應區域是否可以被加寫鎖或讀鎖。返回信息將顯示造成鎖請求失敗的區域字節數和從字節0開始的偏移量。因為返回結構中的l_pid元素包含當前擁有文件鎖的程序的進程標識符,所以程序先把它設置為-1(一個無效值),然後在fcntl調用返回後檢測其值是否被修改過。如果該區域當前未被鎖定,l_pid的值就不會被改變。
為了理解程序的輸出含義,需要查看程序中包含的頭文件fcntl.h(通常是/usr/include/fcntl.h),l_type的值為1對應的定義為F_WRLCK:表明鎖失敗的原因是已經存在一個寫鎖了,l_type的值為0對應的定義為F_RDLCK:已經存在一個讀鎖了。在文件中未被lock2程序鎖定的區域上,無論是共享鎖還是獨占鎖都將會成功。可以看到10~30字節上可以設置一個共享鎖,因為程序lock2在該區域上設置的是共享鎖而不是獨占鎖。而在40~50字節的區域上,兩種鎖都將失敗,因為lock2已經在該區域上設置了一個獨占鎖(F_WRLCK)。
例2:文件鎖的競爭
lock.c -->lock 與上訴代碼相同
lock3.c -->lock3
#include#include #include #include const char *test_file="test_lock"; int main(int argc, char *argv[]) { int file_desc; int res; struct flock region_to_lock; 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); } //程序的其余部分指定文件的不同區域,並嘗試在它們之上執行不同的鎖操作 region_to_lock.l_type=F_RDLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=10; region_to_lock.l_len=5; printf("Process %d,trying F_RDLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLK,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - obtained to lock region\n",getpid()); } region_to_lock.l_type=F_UNLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=10; region_to_lock.l_len=5; printf("Process %d,trying F_UNLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLK,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - unlocked region\n",getpid()); } region_to_lock.l_type=F_UNLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=0; region_to_lock.l_len=50; printf("Process %d,trying F_UNLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLK,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - unlocked region\n",getpid()); } region_to_lock.l_type=F_WRLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=16; region_to_lock.l_len=5; printf("Process %d,trying F_WRLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLK,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - obtained lock on region\n",getpid()); } region_to_lock.l_type=F_RDLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=40; region_to_lock.l_len=10; printf("Process %d,trying F_RDLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLK,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - obtained lock on region\n",getpid()); } region_to_lock.l_type=F_WRLCK; region_to_lock.l_whence=SEEK_SET; region_to_lock.l_start=16; region_to_lock.l_len=5; printf("Process %d,trying F_WRLCK, region %d to %d\n",getpid(),(int)region_to_lock.l_start,(int)(region_to_lock.l_start+region_to_lock.l_len)); res=fcntl(file_desc,F_SETLKW,®ion_to_lock); if(res==-1) { printf("Process %d - faild to lock region\n",getpid()); } else { printf("Process %d - obtained lock on region\n",getpid()); } printf("Process %d ending\n",getpid()); close(file_desc); exit(EXIT_SUCCESS); return 0; }
為了測試首先運行./lock &(後台運行),然後運行./lock3
下面為輸出內容:
Process 26333,trying F_RDLCK, region 10 to 15 Process 26333 - obtained to lock region Process 26333,trying F_UNLCK, region 10 to 15 Process 26333 - unlocked region Process 26333,trying F_UNLCK, region 0 to 50 Process 26333 - unlocked region Process 26333,trying F_WRLCK, region 16 to 21 Process 26333 - faild to lock region Process 26333,trying F_RDLCK, region 40 to 50 Process 26333 - faild to lock region Process 26333,trying F_WRLCK, region 16 to 21 Process 26323 closing file Process 26333 - obtained lock on region Process 26333 ending
解析:
首先,這個程序嘗試試用共享鎖來鎖定文件中10~15字節的區域。這塊區域已被一個共享鎖鎖定,但共享鎖允許同時使用,因此加鎖成功。然後解除它自己對這塊區域的共享鎖,這也成功了。接下來,這個程序視圖解除這個文件前50字節上的鎖,雖然它實際上未對這塊區域進行鎖定,但也成功了,因為雖然這個程序並未對這個區域加鎖,但解鎖請求最終的結果取決於這個程序在文件的頭50個字節上並沒有設置任何鎖。
這個程序接下來視圖用一把獨占鎖來鎖定文件中16~21字節的區域。由於這個區域上已有了一把共享鎖,獨占鎖無法創建,因此這個鎖定操作失敗了。然後,程序又嘗試試用一把共享鎖來鎖定文件中40~59字節的區域。由於這個區域上已經有了一把獨占鎖,因此這個鎖定操作也失敗了。最後,程序再次嘗試在文件中16~21字節的區域上獲得一把獨占鎖,但這次它用F_SETLKW命令來等待直到它可以獲得一把鎖為止。於是程序的輸出就會遇到一個很長的停頓,直到已鎖住這塊區域的lock程序因為完成sleep調用、關閉文件而釋放了它先前獲得的所有鎖為止。lock3程序繼續執行,成功鎖定了這塊區域,最後它也退出了運行。