主要參考了http://blog.csdn.net/anonymalias/article/details/9197641(anonymalias的專欄)
記錄鎖相當於線程同步中讀寫鎖的一種擴充類型,可以用來對有親緣或無親緣關係的進程進行檔案讀與寫的同步,通過 fcntl 函數來執行上鎖操作。儘管讀寫鎖也可以通過在共用記憶體區來進行進程的同步,但是fcntl記錄上鎖往往更容易使用,且效率更高。
記錄鎖和讀寫鎖一樣也有兩種鎖:共用讀鎖(F_RDLCK)和獨佔寫鎖(F_WRLCK),使用規則也基本一致: 檔案給定位元組區間,多個進程可以有一把共用讀鎖,即允許多個進程以讀模式訪問該位元組區; 檔案給定位元組區間,只能有一個進程有一把獨佔寫鎖,即只允許有一個進程已寫入模式訪問該位元組區; 檔案給定位元組區間,如果有一把或多把讀鎖,不能在該位元組區再加寫鎖,同樣,如果有一把寫鎖,不能在該位元組區再加任何讀寫鎖。
fcntl 函數有多種用途,我們這裡只討論關於記錄鎖的部分,具體如下:
int fcntl(int fd, int cmd, struct flock *lock);//需 #include <fcntl.h>/*cmd = F_GETLK,測試能否建立一把鎖cmd = F_SETLK,設定鎖cmd = F_SETLKW,阻塞設定一把鎖*///POSIX只定義fock結構中必須有以下的資料成員,具體實現可以增加struct flock { short l_type; /* 鎖的類型: F_RDLCK, F_WRLCK, F_UNLCK */ short l_whence; /* 加鎖的起始位置:SEEK_SET, SEEK_CUR, SEEK_END 分別表示檔案頭,當前位置和檔案尾*/ off_t l_start; /* 加鎖的起始位移,相對於l_whence */ off_t l_len; /* 上鎖的位元組數,如果為0,表示從位移處一直到檔案的末尾*/ pid_t l_pid; /* 已經佔用鎖的PID(只對F_GETLK 命令有效) */ /*...*/};//Return value: 0表示成功,-1表示失敗
F_SETLK (struct flock *) Acquire a lock (when l_type is F_RDLCK or F_WRLCK) or release a lock (when l_type is F_UNLCK) on the bytes specified by the l_whence, l_start, and l_len fields of lock. If a conflicting lock is held by another process, this call returns -1 and sets errno to EACCES or EAGAIN.F_SETLKW (struct flock *) As for F_SETLK, but if a conflicting lock is held on the file, then wait for that lock to be released. If a signal is caught while waiting, then the call is interrupted and (after the sig‐ nal handler has returned) returns immediately (with return value -1 and errno set to EINTR; see signal(7)).F_GETLK (struct flock *) On input to this call, lock describes a lock we would like to place on the file. If the lock could be placed, fcntl() does not actually place it, but returns F_UNLCK in the l_type field of lock and leaves the other fields of the structure unchanged. If one or more incompatible locks would prevent this lock being placed, then fcntl() returns details about one of these locks in the l_type, l_whence, l_start, and l_len fields of lock and sets l_pid to be the PID of the process holding that lock.
注意仔細閱讀上面解釋。 這裡需要注意的是,用F_GETLK測試能否建立一把鎖,然後接著用F_SETLK或F_SETLKW企圖建立一把鎖,由於這兩者不是一個原子操作,所以不能保證兩次fcntl之間不會有另外一個進程插入並建立一把相關的鎖,從而使一開始的測試情況無效。所以一般不希望上鎖時阻塞,會直接通過調用F_SETLK,並對返回結果進行測試,以判斷是否成功建立所要求的鎖。
1. 記錄鎖只能用於進程間同步
上面所闡述的規則只適用於不同進程提出的鎖請求,並不適用於單個進程提出的多個鎖請求。即如果一個進程對一個檔案區間已經有了一把鎖,後來該進程又試圖在同一檔案區間再加一把鎖,那麼新鎖將會覆蓋老鎖。
//在同一進程加鎖的情況下,可以繼續加鎖(調用程式在文章後面給出)void *thread_test(void* fd_ptr){int fd = * (int *)fd_ptr;flock lock;lock_init(&lock, F_WRLCK, SEEK_SET, 0, 0);if(writew_lock(fd) == 0)cout << "Got it!" << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;}int main(){ char *FILE_PATH = "a.txt";int FILE_MODE = 0664;pthread_t pid;void *retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd); cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;pthread_create(&pid, NULL, thread_test, (void *)&fd);pthread_join(pid, &retVal); unlock(fd); return 0;}
輸出為:
00Got it。00
說明如果一個進程對一個檔案區間已經有了一把鎖,後來該進程(哪怕是該進程下另外一個線程)又試圖在同一檔案區間再加一把鎖,那麼新鎖將會覆蓋老鎖。所以記錄鎖不能用於同一進程的多線程同步。
下面測試不同進程下情況:
//在父同進程裡加讀鎖,看子進程能否繼續加鎖(調用程式在文章後面給出)int main(){ char *FILE_PATH = "a.txt";int FILE_MODE = 0664;int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd);if(fork() == 0){sleep(3); cout << "I'm child." << endl;cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;exit(0);}else{cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;}wait(&retVal); unlock(fd); return 0;}
結果如下:
I'm father.00I'm child.113310
注意父進程裡加的是共用讀鎖,此時父進程是可以加任何鎖的,而子進程只能加讀鎖。
將父進程裡的共用讀鎖換成獨佔寫鎖:
//在父同進程裡加寫鎖,看子進程能否繼續加鎖(調用程式在文章後面給出)int main(){ char *FILE_PATH = "a.txt";int FILE_MODE = 0664;int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd);if(fork() == 0){sleep(3); cout << "I'm child." << endl;cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;exit(0);}else{cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;}wait(&retVal); unlock(fd); return 0;}
參考上面的測試,結果可想而知:
I'm father.00I'm child.1139311393
此時的父進程加的是寫鎖,所以子進程什麼鎖都不能加。
2. 鎖的粒度
這裡要提到兩個概念:記錄上鎖和檔案上鎖。
記錄上鎖:對於UNIX系統而言,“記錄”這一詞是一種誤用,因為UNIX系統核心根本沒有使用檔案記錄這種概念,更適合的術語應該是位元組範圍鎖,因為它鎖住的只是檔案的一個地區。用粒度來表示被鎖住檔案的位元組數目。對於記錄上鎖,粒度最大是整個檔案。
檔案上鎖:是記錄上鎖的一種特殊情況,即記錄上鎖的粒度是整個檔案的大小。
之所以有檔案上鎖的概念是因為有些UNIX系統支援對整個檔案上鎖,但沒有給檔案內的位元組範圍上鎖的能力。
3. 記錄鎖的隱含繼承與釋放
關於記錄鎖的繼承和釋放有三條規則,如下:
(1)鎖與進程和檔案兩方面有關,體現在: 當一個進程終止時,它所建立的記錄鎖將全部釋放; 當關閉一個檔案描述符時,則進程通過該檔案描述符引用的該檔案上的任何一把鎖都將被釋放。
//測試進程的結束是否影響該進程加的鎖(調用程式在文章後面給出)int main(){ char *FILE_PATH = "a.txt";int FILE_MODE = 0664;int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);if(fork() == 0){ writew_lock(fd); cout << "I'm child and I have a writew_lock." << endl;sleep(3);exit(0);}else{sleep(1);cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;wait(&retVal);cout << "My child is over." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;} unlock(fd); return 0;}
結果如下:
I'm child and I have a writew_lock.I'm father.1214912149My child is over.00
說明在子進程結束後,它所建立的鎖失效。
//測試fd對鎖的影響#include <iostream>#include <fcntl.h>#include <pthread.h>#include <unistd.h>#include <stdlib.h>#include <sys/types.h>#include <sys/wait.h>#include <sys/shm.h>using namespace std;struct share_data{pthread_cond_t cond;pthread_mutex_t mutex;pthread_mutexattr_t mutexAttr;pthread_condattr_t condAttr;};int main(){ char *FILE_PATH = "a.txt";int FILE_MODE = 0664;int retVal;int shmid;struct share_data *shm;shmid = shmget(IPC_PRIVATE, sizeof(struct share_data), 0644 | IPC_CREAT);int fd_1 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); int fd_2 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);if(fork() == 0){ shm = (struct share_data *) shmat(shmid, 0, 0);pthread_mutexattr_init(&(shm->mutexAttr)); pthread_mutexattr_setpshared(&(shm->mutexAttr), PTHREAD_PROCESS_SHARED);pthread_mutex_init(&(shm->mutex), &(shm->mutexAttr));pthread_condattr_init(&(shm->condAttr));pthread_condattr_setpshared(&(shm->condAttr), PTHREAD_PROCESS_SHARED);pthread_cond_init(&(shm->cond), &(shm->condAttr));writew_lock(fd_1); cout << "I'm child and I have a writew_lock." << endl;sleep(5);close(fd_1);pthread_cond_signal(&(shm->cond));sleep(3);exit(0);}else{ shm = (struct share_data *) shmat(shmid, 0, 0);pthread_mutex_lock(&(shm->mutex));sleep(1);cout << "I'm father." << endl; cout<<lock_test(fd_2, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd_2, F_RDLCK, SEEK_SET, 0, 0)<<endl;cout << "I'm waiting." << endl;pthread_cond_wait(&(shm->cond), &(shm->mutex));cout << "My son's fd_1 is over." << endl; cout<<lock_test(fd_2, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd_2, F_RDLCK, SEEK_SET, 0, 0)<<endl;}wait(&retVal); return 0;}
這個例子較複雜一些,除了記錄鎖,還有互斥鎖和條件變數。互斥鎖和條件變數的目的是為了在子進程close(fd_1)之後再喚醒主進程,看看當子進程用 fd_1 加過鎖再釋放掉 fd_1 後對父進程的影響。
結果如下:
I'm child and I have a writew_lock.I'm father.1746017460I'm waiting.My son's fd_1 is over.00
證明了當關閉一個檔案描述符時,則進程通過該檔案描述符引用的該檔案上的任何一把鎖都將被釋放。
(2)由fork產生的子進程不繼承父進程所設定的鎖。即對於父進程建立的鎖而言,子進程被視為另一個進程。記錄鎖本身就是用來同步不同進程對同一檔案區進行操作,如果子進程繼承了父進程的鎖,那麼父子進程就可以同時對同一檔案區進行操作,這有違記錄鎖的規則,所以存在這麼一條規則。之前的代碼已經證明了這點。
3)執行exec後,新程式可以繼承原執行程式的鎖。但是,如果一個檔案描述符設定了close-on-exec標誌,在執行exec時,會關閉該檔案描述符,所以對應的鎖也就被釋放了,也就無所謂繼承了。
4. 記錄鎖的讀和寫的優先順序
我們知道,讀寫鎖函數是優先考慮等待讀模式佔用鎖的線程,這種實現的一個很大缺陷就是出現寫入線程餓死的情況。那麼對於記錄鎖呢,具體進行以下2個方面測試: 進程擁有讀出鎖,然後寫入鎖等待期間額外的讀出鎖處理; 進程擁有寫入鎖,那麼等待的寫入鎖和等待的讀出鎖的優先順序;
int main(){ int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); readw_lock(fd); //child 1 if (fork() == 0) { cout<<"child 1 try to get write lock..."<<endl; writew_lock(fd); cout<<"child 1 get write lock..."<<endl; unlock(fd); cout<<"child 1 release write lock..."<<endl; exit(0); } //child 2 if (fork() == 0) { sleep(3); cout<<"child 2 try to get read lock..."<<endl; readw_lock(fd); cout<<"child 2 get read lock..."<<endl; unlock(fd); cout<<"child 2 release read lock..."<<endl; exit(0); } sleep(10); unlock(fd); return 0;}
結果如下:
child 1 try to get write lock...child 2 try to get read lock...child 2 get read lock...child 2 release read lock...child 1 get write lock...child 1 release write lock...
此處是利用到了writew_lock() ,readw_lock()以及unlock()函數裡面 F_SETLCKW 參數,該參數使得當前進程不能獲得鎖時會陷入阻塞,等待直到可以獲得鎖(返回0)或者收到某種訊號(返回-1)。 值得注意的是子進程1獲得寫鎖是在子進程2釋放鎖後還過了較長時間,明顯是在父進程unlock(fd)之後。 可知在有寫入進程等待的情況下,對於讀出進程的請求,系統會先滿足讀進程(即是)。那麼這也就可能導致寫入進程餓死的局面。
int main(){ int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); writew_lock(fd);int retVal; //child 1 if (fork() == 0) { cout<<"child 1 try to get write lock..."<<endl; writew_lock(fd); cout<<"child 1 get write lock..."<<endl; unlock(fd); cout<<"child 1 release write lock..."<<endl; exit(0); } //child 2 if (fork() == 0) { sleep(3); cout<<"child 2 try to get read lock..."<<endl; readw_lock(fd); cout<<"child 2 get read lock..."<<endl; unlock(fd); cout<<"child 2 release read lock..."<<endl; exit(0); } sleep(10); unlock(fd); return 0;}
我的測試結果如下:
child 1 try to get write lock...child 2 try to get read lock...child 2 get read lock...child 2 release read lock...child 1 get write lock...child 1 release write lock...
結果表明還是讀鎖的優先順序高啊。 與參考博文的結果FIFO不同。。。先這麼著吧
最後要說的是檔案描述符fd在fork()之後的子進程裡是與父進程裡的fd雖然指向同一個檔案,但已經是兩個不同的fd了。見下面的例子:
int main(){ int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); writew_lock(fd); //child if (fork() == 0) { cout<<"child try to get write lock..."<<endl; writew_lock(fd); cout<<"child get write lock..."<<endl; unlock(fd); cout<<"child release write lock..."<<endl; exit(0); }sleep(5);close(fd);sleep(5); return 0;}
結果:
child try to get write lock...child get write lock...child release write lock...
其中1,2兩句間,3句與結束之間有較長時間等待。表明就是sleep()函數的原因。當父進程裡的fd回收後,子進程才拿到寫鎖。