若兩個人同時編輯一個文件,其后果將如何呢?在很多UNIX系統中,該文件的最后狀態取決于寫該文件的最后一個進程。但是對于有些應用程序(例如數據庫),進程有時需要確保它正在單獨寫一個文件。為了向進程提供這種功能,商用UNIX系統提供了記錄鎖機制。
記錄鎖(record locking)的功能是:當一個進程正在讀或修改文件的某個部分時,它可以阻止其他進程修改同一文件區。對于UNIX系統而言,“記錄”這個詞是一種誤用,因為UNIX系統內核根本沒有使用文件記錄這種概念。更適合的術語可能是字節范圍鎖(byte-range locking),因為它鎖定的只是文件中的一個區域(也可能是整個文件)。
1、歷史
對早期UNIX系統的一種批評是它們不能用來運行數據庫系統,其原因是這些系統不支持部分地對文件加鎖。在UNIX系統開始進入商用計算領域時,很多系統開發小組以各種不同方式增加了對記錄鎖的支持。
早期的伯克利版本只支持flock函數。該函數鎖整個文件,不能鎖文件中的一部分。
SVR3通過fcntl函數增加了記錄鎖功能。在此基礎上構造了lockf函數,它提供了一個簡化的接口。這些函數允許調用者鎖一個文件中任意字節數的區域,長至整個文件,短至文件中的一個字節。
POSIX.1標準的基礎是fcntl。
2、fcntl記錄鎖
http://www.CUOXin.com/nufangrensheng/p/3500350.html中已經給出了fcntl函數的原型。
#include <fcntl.h>int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */);返回值:若成功則依賴于cmd,若出錯則返回-1
對于記錄鎖,cmd是F_GETLK、F_SETLK或F_SETLKW。第三個參數(稱其為flockptr)是一個指向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, or SEEK_END */ off_t l_len; /* length, in bytes; 0 means lock to EOF */ pid_t l_pid; /* returned with F_GETLK */};
對flock結構說明如下:
關于加鎖和解鎖區域的說明還要注意下列各點:
上面提到了兩種類型的鎖:共享讀鎖(l_type為F_RDLCK)和獨占寫鎖(F_WRLCK)?;疽巹t是:多個進程在一個給定的字節上可以有一把共享的讀鎖,但是在一個給定字節上只能有一個進程獨用的一把寫鎖。進一步而言,如果在一個給定字節上已經有一把或多把讀鎖,則不能在該字節上再加上寫鎖;如果在一個字節上已經有一把獨占性的寫鎖,則不能再對它加任何讀鎖。在表14-2示出了這些規則。
上面說明的兼容性規則適用于不同進程提出的鎖請求,并不適用于單個進程提出的多個鎖請求。如果一個進程對一個文件區間已經有了一把鎖,后來該進程又企圖在同一文件區間再加一把鎖,那么新鎖將替換老鎖。例如,若一進程在某文件的16-32字節區間有一把寫鎖,然后又試圖在16-32字節區間加一把讀鎖,那么該請求將成功執行(假定其他進程此時并不試圖向該文件的同一區間加鎖),原來的寫鎖被替換為讀鎖。
加讀鎖時,該描述符必須是讀打開;加寫鎖時,該描述符必須是寫打開。
以下說明fcntl函數的三種命令:
F_GETLK 判斷由flockptr所描述的鎖是否會被另外一把鎖所排斥(阻塞)。如果存在一把鎖,它阻止創建由flockptr所描述的鎖,則把該現存鎖的信息寫到flockptr指向的結構中。如果不存在這種情況,則除了將l_type設置為F_UNLCK之外,flockptr所指向結構中的其他信息保持不變。
F_SETLK 設置由flockptr所描述的鎖。如果試圖建立一把讀鎖(l_type設為F_RDLCK)或寫鎖(l_type設為F_WRLCK),而按上述兼容性規則不能允許,則fcntl立即出錯返回,此時errno設置為EACCES或EAGAIN。此命令也用來清除由flockptr說明的鎖(l_type為F_UNLCK)。
F_SETLKW 這是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果因為當前在所請求區間的某個部分另一進程已經有一把鎖,因而按兼容性規則由flockptr所請求的鎖不能被創建,則使調用進程休眠。如果請求創建的鎖已經可用,或者休眠由信號中斷,則該進程被喚醒。
應當了解,用F_GETLK測試能否建立一把鎖,然后用F_SETLK和F_SETLKW企圖建立一把鎖,這兩者不是一個原子操作。因此不能保證在這兩次fcntl調用之間不會有另一個進程插入并建立一把相關的鎖,從而使原來測試到的情況發生變化。如果不希望在建立鎖時可能產生的長期阻塞,則應使用F_SETLK,并對返回結果進行測試,以判別是否成功地建立了所要求的鎖。
在設置或釋放文件上的鎖時,系統按要求組合或裂開相鄰區。例如,若字節100-199是加鎖的區,需解鎖地150字節,則內核將維持兩把鎖,一把用于字節100-149,另一把用于字節151-199。圖14-1說明了這種情況。
圖14-1 文件字節范圍鎖
假定我們又對第150字節設置鎖,那么系統將會把三個相鄰的加鎖區合并成一個區(從字節100至199)。其結果如圖14-1中的第一圖所示,于是我們又回到了出發點。
實例:請求和釋放一把鎖
為了避免每次分配flock結構,然后又填入各項信息,可以用程序清單14-2中的函數lock_reg來處理所有這些細節。
程序清單14-2 加鎖和解鎖一個文件區域的函數
#include "apue.h"#include <fcntl.h>intlock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len){ struct flock lock; lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */ lock.l_start = offset; /* byte offset, relative to l_whence */ lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ lock.l_len = len; /* #bytes (0 means to EOF) */ return(fcntl(fd, cmd, &lock));}
因為大多數鎖調用是加鎖或解鎖一個文件區域(命令F_GETLK很少使用),故通常使用下列5個宏,它們都定義在apue.h中。
#define read_lock(fd, offset, whence, len) / lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))#define readw_lock(fd, offset, whence, len) / lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))#define write_lock(fd, offset, whence, len) / lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))#define writew_lock(fd, offset, whence, len) / lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))#define un_lock(fd, offset, whence, len) / lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
實例:測試一把鎖
程序清單14-3中定義了一個函數lock_test,可用其測試一把鎖。
程序清單14-3 測試一個鎖狀態的函數
#include "apue.h"#include <fcntl.h>pid_tlock_test(int fd, int type, off_t offset, int whence, off_t len){ struct flock lock; lock.l_type = type; /* F_RDLCK or F_WRLCK */ lock.l_start = offset; /* byte offset, relative to l_whence */ lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ lock.l_len = len; /* bytes (0 means to EOF) */ if(fcntl(fd, F_GETLK, &lock) < 0) err_sys("fcntl error"); if(lock.l_type == F_UNLCK) return(0); /* false, region isn't locked by another PRoc */ return(lock.l_pid); /* true, return pid of lock owner */}
如果存在一把鎖,它阻塞由參數說明的鎖請求,則此函數返回持有這把現存鎖的進程ID,否則此函數返回0。通常用下面兩個宏來調用此函數(它們也定義在apue.h中)。
#define is_read_lockable(fd, offset, whence, len) / (lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0)#define is_write_lockable(fd, offset, whence, len) / (lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0)
注意,進程不能使用lock_test函數測試它自己是否在文件的某一部分持有一把鎖。F_GETLK命令的定義說明,返回信息指示是否有現存的鎖阻止調用進程設置它自己的鎖。因為F_SETLK和F_SETLKW命令總是替換調用進程現存的鎖(若已存在),所以調用進程決不會阻塞在自己持有的鎖上;于是,F_GETLK命令決不會報告調用進程自己持有的鎖。
實例:死鎖
如果兩個進程相互等待對方持有并且鎖定的資源時,則這兩個進程就處于死鎖狀態。如果一個進程已經控制了文件中的一個加鎖區域,然后它又試圖對另一個進程控制的區域加鎖,則它就會休眠,在這種情況下,有發生死鎖的可能性。
程序清單14-4給出了一個死鎖的例子。子進程鎖字節0,父進程鎖字節1。然后,它們又都試圖鎖對方已經加鎖的字節。在該程序中使用了http://www.CUOXin.com/nufangrensheng/p/3510306.html中介紹的父、子進程同步例程(TELL_xxx和WAIT_xxx),使得每個進程能夠等待另一個進程獲得它設置的第一把鎖。運行程序清單14-4所示程序得到:
程序清單14-4 死鎖檢測實例
#include "apue.h"#include <fcntl.h>static voidlockabyte(const char *name, int fd, off_t offset){ if(writew_lock(fd, offset, SEEK_SET, 1) < 0) err_sys("%s: write_lock error", name); printf("%s: got the lock, byte %ld/n", name, offset);}int main(void){ int fd; pid_t pid; /* * Create a file and write two bytes to it. */ if((fd = creat("templock", FILE_MODE)) < 0) err_sys("creat error"); if(write(fd, "ab", 2) != 2) err_sys("write error"); TELL_WAIT(); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid == 0) /* child */ { lockabyte("child", fd, 0); TELL_PARENT(getppid()); WAIT_PARENT(); lockabyte("child", fd, 1); } else /* parent */ { lockabyte("parent", fd, 1); TELL_CHILD(pid); WAIT_CHILD(); lockabyte("parent", fd, 0); } exit(0);}
檢測到死鎖時,內核必須選擇一個進程接收出錯返回。在本實例中選擇了子進程,這是一個實現細節。在某些系統上,總是子進程接收到出錯信息;在另一些系統上,總是父進程接到出錯信息。在某些系統上,當試圖使用多把鎖時,有時是子進程接到出錯信息,有時則是父進程接到出錯信息。
3、鎖的隱含繼承和釋放
關于記錄鎖的自動繼承和釋放有三條規則:
(1)鎖與進程和文件兩方面有關。這有兩重含義:第一重很明顯,當一個進程終止時,它所建立的鎖全部釋放;第二重意思就不很明顯,任何時候關閉一個描述符時,則該進程通過這一描述符可以引用的文件上的任何一把鎖都被釋放(這些鎖都是該進程設置的)。這就意味著如果執行下列四步:
fd1 = open(pathname, ...);read_lock(fd1, ...);fd2 = dup(fd1);close(fd2);
則在close(fd2)后,在fd1上設置的鎖被釋放。如果將dup換為open,以打開另一描述符上的同一文件,其效果也一樣:
fd1 = open(pathname, ...);read_lock(fd1, ...);fd2 = open(pathname, ...);close(fd2);
(2)由fork產生的子進程不繼承父進程所設置的鎖。這意味著,若一個進程得到一把鎖,然后調用fork,那么對于父進程獲得的鎖而言,子進程被視為另一個進程,對于從父進程處繼承過來的任一描述符,子進程需要調用fcntl才能獲得它自己的鎖。這與鎖的作用是相一致的。鎖的作用是阻止多個進程同時寫同一文件(或同一文件區域)。如果子進程繼承父進程的鎖,則父、子進程就可以同時寫同一個文件。
(3)在執行exec后,新程序可以繼承原執行程序的鎖。但是注意,如果對一個文件描述符設置了close-on-exec標志,那么當作為exec的一部分關閉該文件描述符時,對相應文件的所有鎖都被釋放了。
4、FreeBSD的實現
先簡要地觀察FreeBSD實現中使用的數據結構。這會幫助我們進一步理解規則1:鎖是與進程、文件兩者相關聯的。
考慮一個進程,它執行下列語句(忽略出錯返回):
fd1 = open(pathname, ...);write_lock(fd1, 0, SEEK_SET, 1); /* parent write locks byte 0 */if((pid = fork()) > 0) /* parent */{ fd2 = dup(fd1); fd3 = open(pathname, ...); }else if(pid == 0){ read_lock(fd1, 1, SEEK_SET, 1); /* child read locks byte 1 */}pause();
圖14-2顯示了父、子進程暫停后的數據結構情況。
圖14-2 關于記錄鎖的FreeBSD數據結構
http://www.CUOXin.com/nufangrensheng/p/3498736.html中的圖3-3和http://www.CUOXin.com/nufangrensheng/p/3509492.html中的圖8-1已顯示了open、fork以及dup后的數據結構。有了記錄鎖后,在原來的這些圖上新加了lockf結構,它們由i節點結構開始相互鏈接起來。注意,每個lockf結構說明了一個給定進程的一個加鎖區域(由偏移量和長度定義)。圖中顯示了兩個lockf結構,一個是由父進程調用write_lock形成的,另一個則是由子進程調用read_lock形成的。每一個結構都包含了相應進程ID。
在父進程中,關閉fd1、fd2和fd3中的任意一個都將釋放由父進程設置的寫鎖。在關閉這三個描述符中的任意一個時,內核會從該描述符所關聯的i節點開始,逐個檢查lockf鏈接表中各項,并釋放由調用進程持有的各把鎖。內核并不清楚也不關心父進程是用哪一個描述符來設置這把鎖的。
實例
在程序清單13-2(http://www.CUOXin.com/nufangrensheng/p/3544370.html)中,我們了解到,守護進程可用一把文件鎖以保證只有該守護進程的唯一副本正在運行。程序清單14-5示出了lockfile函數的實現,守護進程可用該函數在文件上加鎖。
程序清單14-5 在文件整體上加鎖
#include <unistd.h>#include <fcntl.h>intlockfile(int fd){ struct flock fl; fl.l_type = F_WRLCK; fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; return(fcntl(fd, F_SETLK, &fl));}
另一種方法是,用write_lock函數定義lockfile函數:
#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)
5、在文件尾端加鎖(不是很明白)
在接近文件尾端加鎖或解鎖時需要特別小心。大多數實現按照l_whence的SEEK_CUR或SEEK_END值,用l_start以及文件當前位置或當前長度得到絕對文件偏移量。但是,常常需要相對于文件的當前位置或當前長度指定一把鎖。其原因是,我們在該文件上沒有鎖,所以不能調用lseek以正確無誤地獲得加鎖時的當前文件偏移量。(在lseek和加鎖調用之間,另一個進程可能改變該文件長度。)
考慮以下代碼序列:
writew_lock(fd, 0, SEEK_END, 0);write(fd, buf, 1);un_lock(fd, 0, SEEK_END);write(fd, buf, 1);
該代碼序列所作的可能并不是你所期望的。它得到一把寫鎖,該寫鎖從當前文件尾端起,包括以后可能添加到該文件的任何數據。假定在文件尾端時執行第一個write,它給文件添寫了1個字節,而該字節將被加鎖。跟隨其后的解鎖,其作用是對以后添寫到文件上的數據不再加鎖,但在它之前剛添寫的一個字節則保留加鎖。當執行第二個寫時,文件尾端又延伸了1個字節,但該字節并未加鎖。
當對文件的一部分加鎖時,內核將指定的偏移量變換成絕對文件偏移量。另外,除了指定一個絕對偏移量(SEEK_SET)之外,fcntl還允許我們相對于文件中的某個點(當前偏移量(SEEK_CUR)或文件尾端(SEEK_END))指定該偏移量。當前偏移量和文件尾端是可能不斷變化的,而這種變化又不應影響現存鎖的狀態,所以內核必須獨立于當前文件偏移量或文件尾端而記住鎖。
如果我們想要解除第一次write所寫1個字節上的鎖,那么應指定長度為-1。負的長度值表示在指定偏移量之前的字節數。
6、建議性鎖和強制性鎖(結合http://hi.baidu.com/24688395/item/93381c477d8e82ec1381da3d中的內容來理解)
考慮數據庫訪問例程庫。如果該庫中所有函數都以一致的方法處理記錄鎖,則稱使用這些函數訪問數據庫的任何進程集為合作進程(coOperating process)。如果這些函數是僅有的用來訪問數據庫的函數,那么它們使用建議性鎖是可行的。但是建議性鎖并不能阻止對數據庫文件有寫權限的任何其他進程對數據庫文件進行隨意的寫操作。沒有使用被認可的方法(數據庫函數庫)訪問數據庫的進程是一個非合作進程。
強制性鎖使內核對每一個open、read和write系統調用都進行檢查,檢查調用進程對正在訪問的文件是否違背了某一把鎖的作用。強制性鎖有時也被稱為強迫方式鎖(enforcement-mode locking)。
linux2.4.22和Solaris 9提供強制性記錄鎖,而FreeBSD 5.2.1和Mac OS X 10.3則不提供。強制性記錄鎖不是Single UNIX Specification的組成部分。在Linux中,如果用戶想要使用強制性鎖,則要在各個文件系統基礎上,對mount命令用 -o mand 選項打開該機制。
對一個特定文件打開其設置組ID位并關閉其組執行位,則對該文件開啟了強制性鎖機制(回憶http://www.CUOXin.com/nufangrensheng/p/3502457.html中的程序清單4-4)。因為當組執行位關閉時,設置組ID位不再有意義,所以SVR3的設計者借用兩者的這種組合來指定對一個文件的鎖是強制性的而非建議性的。
如果一個進程試圖讀、寫一個強制性鎖起作用的文件,而欲讀、寫的部分又由其他進程加上了讀或寫鎖,此時會發生什么呢?對這一問題的回答取決于三方面的因素:操作類型(read或write),其他進程保有的鎖的類型(讀鎖或寫鎖),以及有關描述符是阻塞還是非阻塞的。表14-3列出了8種可能性。
表14-3 強制性鎖對其他進程讀、寫的影響
其他進程在文件區段中持有的現存鎖的類型 | 阻塞描述符,試圖 read write | 非阻塞描述符,試圖 read write |
讀鎖 寫鎖 | 允許 阻塞 阻塞 阻塞 | 允許 EAGAIN EAGAIN EAGAIN |
除了表14-3中的read和write函數,其他進程持有的強制性鎖也會對open函數產生影響。通常,即使正在打開的文件具有強制性記錄鎖,該打開操作也會成功。后隨的read或write依從于表14-3所示的規則。但是,如果欲打開的文件具有強制性記錄鎖(讀鎖或寫鎖),而且open調用中的flag指定為O_TRUNC或O_CREAT,則不論是否指定O_NONBLOCK,open都立即出錯返回,errno設置為EAGAIN。
實例
程序清單14-6用于確定一個系統是否支持強制性鎖機制
程序清單14-6 確定是否支持強制性鎖
#include "apue.h"#include <errno.h>#include <fcntl.h>#include <sys/wait.h>intmain(int argc, char *argv[]){ int fd; pid_t pid; char buf[5]; struct stat statbuf; if(argc != 2) { fprintf(stderr, "usage: %s filename/n", argv[0]); exit(1); } if((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0) err_sys("open error"); if(write(fd, "abcdef", 6) != 6) err_sys("write error"); /* turn on set-group-ID and turn off group-execute */ if(fstat(fd, &statbuf) < 0) err_sys("fstat error"); if(fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0) err_sys("fchmod error"); TELL_WAIT(); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid > 0) /* parent */ { /* write lock entire file */ if(write_lock(fd, 0, SEEK_SET, 0) < 0) err_sys("write_lock error"); TELL_CHILD(pid); if(waitpid(pid, NULL, 0) < 0) err_sys("waitpid error"); } else /* child */ { WAIT_PARENT(); /* wait for parent to set lock */ set_fl(fd, O_NONBLOCK); /* set_fl()參見http://www.CUOXin.com/nufangrensheng/p/3500350.html */ /* first let's see what error we get if region is locked */ if(read_lock(fd, 0, SEEK_SET, 0) != -1) /* no wait */ err_sys("child: read_lock succeeded"); printf("read_lock of already-locked region returns %d/n", errno); /* now try to read the mandatory locked file */ if(lseek(fd, 0, SEEK_SET) == -1) err_sys("lseek error"); if(read(fd, buf, 2) < 0) err_ret("read failed (mandatory locking works)"); else printf("read OK (no mandatory locking), buf = %2.2s/n", buf); } exit(0);}
此程序首先創建一個文件,并使強制性鎖機制對其起作用。然后程序分裂為父進程和子進程。父進程對整個文件設置一把寫鎖,子進程則將該文件的描述符設置為非阻塞的,然后企圖對該文件設置一把讀鎖,我們期望這會出錯返回,并希望看到系統返回是EACCES或EAGAIN。接著,子進程將文件讀、寫位置調整到文件起點,并試圖讀(read)該文件。如果系統提供強制性鎖機制,則read應返回EACCES或EAGAIN(因為該描述符是非阻塞的),否則read返回所讀的數據。
在RedHat Linux 2.6.18上運行此程序(該系統不支持強制性鎖機制),得到:
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答