42-使用flock檔案鎖
1. flock函式
flock函式是專門用於實現對檔案加鎖的,與fcntl不同的是,flock系統呼叫最初是源自BSD的,而fcntl則是源自System V,flock是對整個檔案進行加鎖,而fcntl不僅可以對整個檔案加鎖,還可以對檔案部分割槽域加鎖,更加具有靈活性。
flock - apply or remove an advisory lock on an open file
從flock函式的語義上來看,flock提供的檔案鎖是一種“建議性”鎖,並非強制使用。也就是說,一個程序對檔案進行讀寫操作時可以忽略檔案鎖的限制,但是如果要保證檔案中的資料不會出現問題,所有訪問檔案的進行都必須加鎖。
函式原型:
#include <sys/file.h>
int flock(int fd , int operation);
返回值說明:成功返回0,失敗返回-1並設定errno錯誤
引數fd:指定要加鎖的檔案描述符
引數operation:指定對檔案鎖的操作,即加鎖或解鎖。例如LOCK_SH(共享鎖),LOCK_EX(互斥鎖),LOCK_UN(解鎖),LOCK_NB(非阻塞加鎖)等。
LOCK_SH(共享鎖):如果operation指定了LOCK_SH,當有多個程序對檔案進行加鎖都會成功,並實現對檔案的讀操作,相當於多執行緒裡的讀寫鎖。
LOCK_EX(互斥鎖):同一時刻只能有一個執行緒能加鎖成功,並對檔案進行讀寫操作,相當於多執行緒裡互斥量。
LOCK_NB(非阻塞加鎖):LOCK_NB是以非阻塞方式進行加鎖,可以和LOCK_EX或LOCK_SH配合使用,如果加鎖失敗不會阻塞,而是出錯返回。
2. flock加鎖型別的相容性
當多個程序對檔案進行加鎖時,無論是以讀、寫、或讀寫哪種方式,都可以在對檔案設定共享鎖或互斥鎖,使用不同型別的鎖對檔案加鎖可能產生不同的情況。
以LOCK_SH方式加鎖時,程序A和程序B都能加鎖成功,都能成功操作檔案,因為鎖型別相容。其他情況加鎖都是不相容的,只有第一個程序能加鎖成功,也只有第一個程序能操作檔案,後面其他加鎖的程序將會阻塞,直到第一個程序釋放鎖,後面的程序才有可能會加鎖成功。
3. flock加鎖型別相容實驗
程序A
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(int argc , char *args[])
{
int lock_fd;
int ret;
int i = 0;
if(argc < 2){
puts("argc < 2");
exit(1);
}
//1 :共享鎖
//2 : 互斥鎖
int n = atoi(args[1]);
//開啟檔案
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加鎖
if(n == 1){
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
perror("flock error :");
puts("A is lock LOCK_SH fail");
exit(-1);
}
puts("A is lock LOCK_SH succesful");
}else{
//以LOCK_EX方式加鎖
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
perror("flock error :");
puts("A is lock LOCK_EX fail");
exit(-1);
}
puts("A is lock LOCK_EX succesful");
}
//加鎖成功後,列印hello
for(i = 0; i < 5; i++){
puts("hello");
sleep(2);
}
//解鎖
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
if(n == 1){
puts("A is unlock LOCK_SH");
}else{
puts("A is unlock LOCK_EX");
}
return 0;
}
程序B
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(int argc , char *args[])
{
int lock_fd;
int ret;
int i = 0;
if(argc < 2){
puts("argc < 2");
exit(1);
}
//1 :共享鎖
//2 : 互斥鎖
int n = atoi(args[1]);
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加鎖
if(n == 1){
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
perror("flock error :");
puts("B is lock LOCK_SH fail");
exit(-1);
}
puts("B is lock LOCK_SH succesful");
}else{
//以LOCK_EX方式加鎖
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
perror("flock error :");
puts("B is lock LOCK_EX fail");
exit(-1);
}
puts("B is lock LOCK_EX succesful");
}
//加鎖成功後列印world
for(i = 0; i < 5; i++){
puts("world");
sleep(2);
}
//解鎖
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
if(n == 1){
puts("B is unlock LOCK_SH");
}else{
puts("B is unlock LOCK_EX");
}
return 0;
}
程序A以LOCK_SH(共享鎖)方式加鎖,程序B以LOCK_SH(共享鎖)方式加鎖:
多個程序以LOCK_SH(共享鎖)方式對檔案加鎖都能成功
程序A以LOCK_SH(共享鎖)方式加鎖,程序B以LOCK_EX(互斥鎖)方式加鎖:
程序A和程序B兩個程序加鎖不相容,只有第一個加鎖的程序才會成功,程序B加鎖失敗會阻塞,直到程序A釋放鎖為止。
4. 指定LOCK_NB非阻塞加鎖
在上一小節的示例程式中,多個程序對檔案進行加鎖時,如果鎖的型別不相容,只有第一個程序會加鎖成功,其他程序可能會因此阻塞。
例如程序A以LOCK_SH方式加鎖成功後,程序B呼叫flock以LOCK_EX方式加鎖時會阻塞,然後等待程序A釋放鎖,如果指定了LOCK_NB選項,那麼程序B不會阻塞,而是出錯返回並設定errno為EWOULDBLOCK錯誤。
現在對程序B的程式碼做以下修改:
//以LOCK_EX方式加鎖,並指定LOCK_NB非阻塞
ret = flock(lock_fd , LOCK_EX | LOCK_NB);
if(ret < 0){
//進一步判斷是否為EWOULDBLOCK錯誤
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
perror("flock error :");
puts("B is lock LOCK_EX fail");
exit(-1);
}
程序A以LOCK_SH(共享鎖)方式加鎖,程序B以LOCK_EX(互斥鎖)方式加鎖,程式執行結果如下:
當程序B在指定了LOCK_NB選項,以非阻塞方式加鎖失敗後不會阻塞,而是立馬出錯返回。
5. flock加鎖可能出現的問題
如果一個程序呼叫flock對檔案以LOCK_SH方式加鎖時,接著再次呼叫flock以LOCK_EX方式加鎖會將原來的共享鎖轉換為一個互斥鎖,但是這個轉換過程並不是原子操作。因為在轉換過程中會刪除先前持有的共享鎖,然後再建立一個新的互斥鎖,但是在這個轉換過程中可能會讓另一個阻塞的程序加鎖成功。
舉個栗子:
A和B兩個程序對同一檔案進行加鎖,B程序以LOCK_SH方式加鎖成功,A程序以LOCK_EX方式加鎖預設情況下會阻塞,此時B程序再次呼叫flock以LOCK_EX方式加鎖就會將原來的共享鎖轉換為一個互斥鎖,但在這個轉換過程中可能會讓A程序加鎖成功(可能會發生)。
如果發生了這種情況,而且B程序沒有指定LOCK_NB的話,轉換過程將會阻塞,如果指定了LOCK_NB,那麼轉換過程將會失敗並丟失原來已持有的鎖(在最初的BSD flock實現和大多數unix實現會出現這樣的情況)。
示例程式如下:
A程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(void)
{
int lock_fd;
int ret;
int i = 0;
//開啟檔案
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_EX,以阻塞方式加鎖
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
puts("A is lock LOCK_EX fail");
exit(-1);
}
puts("A is lock LOCK_EX succesful");
for(i = 0; i < 5; i++){
puts("AAAAAAAAA");
sleep(2);
}
//解鎖
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
puts("A is unlock LOCK_UN succesful");
return 0;
}
B程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(void)
{
int lock_fd;
int ret;
int i = 0;
//開啟檔案
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加鎖
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
puts("B is lock LOCK_SH fail");
exit(-1);
}
puts("B is lock LOCK_SH succesful");
//等待程序A加鎖,然後阻塞
sleep(3);
//在轉換過程中,可能會讓阻塞的程序A加鎖成功
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
puts("B is lock LOCK_EX fail");
exit(-1);
}
puts("------lock is change LOCK_EX--------");
puts("B is lock LOCK_EX succesful");
for(i = 0; i < 5; i++){
puts("BBBBBBBBB");
sleep(2);
}
//解鎖
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
puts("B is unlock LOCK_EX succesful");
return 0;
}
程式執行結果:
從程式的執行結果來看,B程序在轉換過程中並沒有發生之前所說的這種情況。
6. flock加鎖和C標準庫I/O
由於C標準庫I/O呼叫會在使用者空間快取,因此在使用flock加鎖中呼叫C標準庫I/O函式可能出現的問題:
- 在加鎖之前,可能使用者空間快取已經被填滿
- 鎖被刪除之後,可能會重新整理戶空間快取
解決的辦法就是:
- 使用read或write系統呼叫取代C標準庫I/O呼叫
- 在對檔案加鎖後立即重新整理快取,釋放鎖之前再次重新整理快取