1. 程式人生 > >42-使用flock檔案鎖

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函式可能出現的問題:

  1. 在加鎖之前,可能使用者空間快取已經被填滿
  2. 鎖被刪除之後,可能會重新整理戶空間快取

解決的辦法就是:

  1. 使用read或write系統呼叫取代C標準庫I/O呼叫
  2. 在對檔案加鎖後立即重新整理快取,釋放鎖之前再次重新整理快取