Linux 文件鎖的原理、實現和應用

文件鎖簡介


在多數 unix 系統中,當多個進程 / 線程同時編輯一個文件時,該文件的最後狀態取決於最後一個寫該文件的進程。但對於有些應用程序,如數據庫,各個進程需要保證它正在單獨地寫一個文件,這時就要用到文件鎖。

文件鎖(也叫記錄鎖)的作用是,當一個進程讀寫文件的某部分時,其他進程就無法修改同一文件區域。更合適的術語可能是字節範圍鎖,應爲它鎖定的是一個文件中的一個區域(也可以是整個文件。)

文件鎖還分爲建議性鎖和強制性鎖,這裏主要介紹建議性鎖。

能夠實現文件鎖的函數有 flock、fcntl 和 lockf,主要是用前兩個。flock 和 fcntl 是系統調用,而 lockf 是庫函數,實際上是 fcntl 的封裝。flock 和 fcntl 加文件鎖,兩者是不衝突,對應內核類型分別爲 FLOCK 和 POSIX。

文件鎖的基本規則


  1. 文件鎖是進程級別的鎖,一個進程中的所有線程共享此進程的身份。

  2. 任意多個進程在一個給定的字節範圍上,每個進程都可以持有一個共享性的讀鎖,但只能有一個進程持有一個獨佔性的寫鎖。

  3. 如果在一個給定的字節範圍上,已經有一個或多個讀鎖,則不能在此範圍上再加寫鎖。如果在一個給定的字節範圍上已經有一個寫鎖,則不能在此範圍上再加任何讀鎖或寫鎖。

  4. 對於一個進程而言,如果進程對某個文件區域已經有了一個鎖,然後又試圖在相同區域再加一個鎖,在沒有衝突的前提下,則新鎖會替換舊鎖。

  5. 加讀鎖時,該描述符必須是讀打開,加寫鎖時,該描述符必須是寫打開。

規則如表 2-1 所示:

表 2-1 不同進程文件鎖加鎖規則

flock 介紹


函數原型

#include <sys/file.h>
int flock(int fd, int operation);

fd 是系統調用 open 返回的文件描述符

operation 的選項如下:

主要特性

  1. 只能加建議性鎖。

  2. 只能對整個文件加鎖,而不能對文件的某一區域加鎖。

  3. 使用 exec 後,文件鎖的狀態不變。

  4. flock 鎖是可以遞歸,即通過 dup 或者 fork 產生的兩個 fd,都可以加鎖而不會產生死鎖。因爲其創建的鎖是和文件打開表項(struct file)相關聯的,而不是 fd。這就意味着複製文件 fd(通過 fork 或者 dup)後,這兩個 fd 都可以操作這把鎖(例如通過一個 fd 加鎖,通過另一個 fd 可以釋放鎖),也就是說子進程繼承父進程的鎖。但是加鎖過程中,關閉其中一個 fd,鎖是不會被釋放的(因爲 struct file 並沒有釋放),只有關閉所有複製出的 fd,鎖纔會被釋放。

  5. 使用 open 兩次打開同一個文件,得到的兩個 fd 是獨立的(因爲底層對應兩個 struct file 對象),通過其中一個 fd 加鎖,通過另一個 fd 無法解鎖,並且在前一個解鎖前也無法加有衝突的鎖。

  6. flock 在 NFS 文件系統上使用時,服務端 NFSD 將文件鎖的類型由 FLOCK 改爲 POSIX。

  7. 不會進行死鎖檢查。

特性測試

open 測試

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>

int main (int argc, char *argv[])
{
    int ret;
    int fd1 = open(argv[1],O_RDWR);
    int fd2 = open(argv[1],O_RDWR);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);
    ret = flock(fd1, LOCK_EX|LOCK_NB)
    printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    ret = flock(fd2, LOCK_EX|LOCK_NB);
    printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    return 0;
}

本地文件系統:

nfs 導出:

dup 測試

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>

int main (int argc, char *argv[])
{
    int ret;
    int fd1 = open(argv[1],O_RDWR);
    int fd2 = dup(fd1);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);
    ret = flock(fd1, LOCK_EX|LOCK_NB)
    printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    ret = flock(fd2, LOCK_EX|LOCK_NB);
    printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    return 0;
}

本地文件系統:

nfs 導出:

fork 測試

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>

int main (int argc, char ** argv)
{
    int ret;
    int pid;
    int fd = open(argv[1],O_RDWR);
    if ((pid = fork()) == 0){
        ret = flock(fd,LOCK_EX|LOCK_NB);
        printf("child get lock, fd: %d, ret: %d",fd, ret);
        if (ret == -1)
            printf(" error(%d:%s).", errno, strerror(errno));
        printf("\n");
        sleep(10);
        printf("child exit\n");
        exit(0);
    }
    ret = flock(fd,LOCK_EX|LOCK_NB);
    printf("parent get lock, fd: %d, ret: %d", fd, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    waitpid(pid);
    printf("parent exit\n");
    return 0;
}

本地文件系統:

nfs 導出:

死鎖檢查測試

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

void flock_set(int fd, char *file_name, char *process_type)
{
    int ret;
    printf("process %s pid %d start set flock for %s by fd %d.\n",
            process_type, getpid(), file_name, fd);
    ret = flock(fd, LOCK_EX);
    printf("process %s pid %d set flock for %s by fd end, ret %d",
            process_type, getpid(), file_name, fd, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
}

int main (int argc, char *argv[])
{
    int pid;
    int fd1, fd2;

    printf("====test FL_FLOCK dead lock ====\n", argv[1]);

    if ((pid = fork()) == 0){
        fd1 = open(argv[1], O_WRONLY|O_CREAT);
        fd2 = open(argv[2], O_WRONLY|O_CREAT);

        flock_set(fd2, argv[2], "child");
        sleep(1);
        flock_set(fd1, argv[1], "child");

        sleep(2);
        printf("process child exit\n");
        exit(0);
    }

    fd1 = open(argv[1], O_WRONLY|O_CREAT);
    fd2 = open(argv[2], O_WRONLY|O_CREAT);

    flock_set(fd1, argv[1], "parent");
    sleep(1);
    flock_set(fd2, argv[2], "parent");

    waitpid(pid);
    printf("process parent exit\n");
    return 0;
}

測試結果如下:

父子進程互相等待死鎖了,棧的信息如下

lockf 介紹


#include <unistd.h>
int lockf(int fd, int cmd, off_t len);

fd 爲通過 open 返回的打開文件描述符。

cmd 的取值如下:

len 爲從文件當前位置的起始要鎖住的長度。

lockf 只支持排他鎖,不支持共享鎖。

fcntl 介紹


函數原型

#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);

fd 爲通過 open 返回的打開文件描述符。

cmd 的取值如下:

需要注意的是,F_GETLK 用於測試是否可以加鎖,在 F_GETLK 測試可以加鎖之後,F_SETLK 和 F_SETLKW 就會企圖申請一個鎖,但是這兩者之間並不是一個原子操作,也就是說,在 F_SETLK 或者 F_SETLKW 還沒有成功加鎖之前,另外一個進程就有可能已經加上了一個鎖。而且 F_SETLKW 有可能導致程序長時間睡眠。還有,進程對某個文件擁有的各種類型的鎖,會在相應的文件描述符被關閉時自動清除,進程運行結束後,其所加的各種鎖也會自動清除。

flock 結構如下:

struct flock {
... 
    short l_type;   /* Type of lock: F_RDLCK,F_WRLCK,F_UNLCK */
    short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */ 
    off_t l_start;  /* Starting offset for lock */ 
    off_t l_len;    /* Number of bytes to lock */ 
    pid_t l_pid;    /* PID of process blocking our lock (F_GETLK only) */ 
...        
};

flock 結構說明:

主要特性

  1. 加鎖可遞歸,如果一個進程對一個文件區間已經有一個鎖,後來又在同一區間再加一個鎖,在沒有衝突的前提下,則新鎖將替換老鎖。

  2. 加讀鎖(共享鎖)文件必須是讀打開,加寫鎖(排他鎖)文件必須是寫打開。

  3. 進程不能使用 F_GETLK 命令來測試它自己是否在文件的某一部分持有一個鎖。F_GETLK 命令定義說明,返回信息指示是否現存的鎖阻止調用進程設置它自己的鎖。因爲,F_SETLK 和 F_SETLKW 命令總是替換進程的現有鎖,所以調用進程絕不會阻塞再自己持有的鎖上,於是 F_GETLK 命令絕不會報告調用進程自己持有的鎖。

  4. 進程終止時,他所建立的所有文件鎖都會被釋放,同 flock。

  5. 任何時候關閉一個描述符時,則該進程通過這一描述符可以引用的文件上的任何一個鎖都被釋放(這些鎖都是該進程設置的),與 flock 不同。例如:

    fd1 = open(pathname, …);
    fcntl(fd1, F_SETLK, …);
    fd2 = dup(fd1);
    close(fd2);
    // 在close(fd2)後,在fd1上加的鎖,會被釋放。
	// 如果將dup換爲open,以打開同一文件的另一描述符,則效果也一樣。
    fd1 = open(pathname, …);
    fcntl(fd1, F_SETLK, …);
    fd2 = open(pathname, …);
    close(fd2);
  1. 由 fork 產生的子進程不繼承父進程所設置的鎖,與 flock 不同。

  2. 在執行 exec 後,新程序可以繼承原程序的鎖,這點和 flock 是相同的。(如果對 fd 設置了 close-on-exec,則 exec 前會關閉 fd,相應文件的鎖也會被釋放)。

  3. 支持強制性鎖:對一個特定文件打開其設置組的 ID 位 (S_ISGID),並關閉其組執行位 (S_IXGRP),則對該文件開啓了強制性鎖機制。再 Linux 中如果要使用強制性鎖,則要在文件系統 mount 時,使用_omand 打開該機制。

  4. 阻塞方式加鎖時,會進行死鎖檢查。死鎖鏈搜索深度爲 10 步,超過該深度的不再進行死鎖檢查。

特性測試

open 測試

#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main (int argc, char *argv[])
{
    int ret;
    int fd1, fd2;
    struct flock lock;

    fd1 = open(argv[1], O_RDWR);
    fd2 = open(argv[1], O_RDWR);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);

    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_type = F_WRLCK;

    ret = fcntl(fd1, F_SETLK, &lock);
    printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");

    ret = fcntl(fd2, F_SETLK, &lock);
    printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    return 0;
}

本地文件系統:

nfs 導出:

dup 測試

#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main (int argc, char *argv[])
{
    int ret;
    int fd1, fd2;
    struct flock lock;

    fd1 = open(argv[1], O_RDWR);
    fd2 = dup(fd1);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);

    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_type = F_WRLCK;

    ret = fcntl(fd1, F_SETLK, &lock);
    printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");

    ret = fcntl(fd2, F_SETLK, &lock);
    printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    return 0;
}

本地文件系統:

nfs 導出:

fork 測試

#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main (int argc, char ** argv)
{
    int ret;
    int pid;
    int fd;
    struct flock lock;

    fd = open(argv[1],O_RDWR);
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    lock.l_type = F_WRLCK;

    if ((pid = fork()) == 0){
        ret = fcntl(fd, F_SETLK, &lock);
        printf("child set lock, fd: %d, ret: %d",fd, ret);
        if (ret == -1)
            printf(" error(%d:%s).", errno, strerror(errno));
        printf("\n");
        sleep(2);
        printf("child exit\n");
        exit(0);
    }
    ret = fcntl(fd, F_SETLK, &lock);
    printf("parent set lock, fd: %d, ret: %d", fd, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
    waitpid(pid);
    printf("parent exit\n");
    return 0;
}

本地文件系統:

nfs 導出:

死鎖檢查測試

測試代碼如下:

#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void lock_set(int fd, struct flock *lock, char *process_type)
{
    int ret;
    int lock_end = lock->l_start + lock->l_len;
    printf("process %s pid %d start set lock[%d %d], by fd %d.\n",
            process_type, lock->l_pid, lock->l_start, lock_end, fd);
    ret = fcntl(fd, F_SETLKW, lock);
    printf("process %s pid %d set lock[%d %d] by fd %d end, ret %d",
            process_type, lock->l_pid, lock->l_start, lock_end, fd, ret);
    if (ret == -1)
        printf(" error(%d:%s).", errno, strerror(errno));
    printf("\n");
}

int main (int argc, char *argv[])
{
    int pid;
    int fd;
    struct flock lock;

    fd = open(argv[1],O_RDWR);
    lock.l_whence = SEEK_SET;
    lock.l_type = F_WRLCK;

    printf("====test FL_POSIX dead lock for %s====\n", argv[1]);

    if ((pid = fork()) == 0){

        lock.l_pid = getpid();
        lock.l_start = 20;
        lock.l_len = 10;
        lock_set(fd, &lock, "child");

        sleep(1);
        lock.l_start = 1;
        lock.l_len = 10;
        lock_set(fd, &lock, "child");

        sleep(2);
        printf("process child exit\n");
        exit(0);
    }

    lock.l_pid = getpid();
    lock.l_start = 1;
    lock.l_len = 10;
    lock_set(fd, &lock, "parent");

    sleep(1);
    lock.l_start = 20;
    lock.l_len = 10;
    lock_set(fd, &lock, "parent");

    waitpid(pid);
    printf("process parent exit\n");
    return 0;
}

測試結果如下:

自己進程檢查到了死鎖,直接返回,不再阻塞,最後父子都退出了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1nwZm1oSAbNHeyOL11C8XQ