• Linux 文件锁的原理、实现和应用


    文件锁简介

    在多数unix系统中,当多个进程/线程同时编辑一个文件时,该文件的最后状态取决于最后一个写该文件的进程。但对于有些应用程序,如数据库,各个进程需要保证它正在单独地写一个文件,这时就要用到文件锁。

    文件锁(也叫记录锁)的作用是,当一个进程读写文件的某部分时,其他进程就无法修改同一文件区域。更合适的术语可能是字节范围锁,应为它锁定的是一个文件中的一个区域(也可以是整个文件。)

    文件锁还分为建议性锁和强制性锁,这里主要介绍建议性锁。

    能够实现文件锁的函数有flockfcntllockf,主要是用前两个。flockfcntl是系统调用,而lockf是库函数,实际上是fcntl的封装。flockfcntl加文件锁,两者是不冲突,对应内核类型分别为FLOCKPOSIX

    文件锁的基本规则

    1. 文件锁是进程级别的锁,一个进程中的所有线程共享此进程的身份。

    2. 任意多个进程在一个给定的字节范围上,每个进程都可以持有一个共享性的读锁,但只能有一个进程持有一个独占性的写锁

    3. 如果在一个给定的字节范围上,已经有一个或多个读锁,则不能在此范围上再加写锁。如果在一个给定的字节范围上已经有一个写锁,则不能在此范围上再加任何读锁或写锁。

    4. 对于一个进程而言,如果进程对某个文件区域已经有了一个锁,然后又试图在相同区域再加一个锁,在没有冲突的前提下,则新锁会替换旧锁。

    5. 加读锁时,该描述符必须是读打开,加写锁时,该描述符必须是写打开。

    规则如表2-1所示:
    请添加图片描述

    表2-1 不同进程文件锁加锁规则

    flock介绍

    函数原型

    #include 
    int flock(int fd, int operation);
    
    • 1
    • 2

    fd是系统调用open返回的文件描述符

    operation的选项如下:

    • LOCK_SH :表示要创建一个共享锁,在任意时间内,一个文件的共享锁可以被多个进程拥有

    • LOCK_EX :表示要创建一个排他锁,在任意时间内,一个文件的排他锁,只能被一个进程拥有

    • LOCK_UN : 表示删除该进程创建的锁即解锁

    • LOCK_NB : 非阻塞(与以上三种操作一起使用)

    主要特性

    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 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    本地文件系统:

    请添加图片描述

    nfs导出:

    请添加图片描述

    dup测试

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    本地文件系统:
    在这里插入图片描述

    nfs导出:

    在这里插入图片描述

    fork测试

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    本地文件系统:

    在这里插入图片描述

    nfs导出:
    在这里插入图片描述

    死锁检查测试

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    测试结果如下:
    在这里插入图片描述

    父子进程互相等待死锁了,栈的信息如下
    在这里插入图片描述

    lockf介绍

    #include 
    int lockf(int fd, int cmd, off_t len);
    
    • 1
    • 2

    fd为通过open返回的打开文件描述符。

    cmd的取值如下:

    • F_LOCK :给文件加排他锁,若文件已被加锁,则会一直阻塞到锁被释放。

    • F_TLOCK :同F_LOCK,但若文件已被加锁,不会阻塞,并返回错误。

    • F_ULOCK :解锁。

    • F_TEST :测试文件是否被加锁,若文件没被加锁则返回0,否则返回-1。

    len 为从文件当前位置的起始要锁住的长度。

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

    fcntl介绍

    函数原型

    #include 
    int fcntl(int fd, int cmd, struct flock *lock);
    
    • 1
    • 2

    fd为通过open返回的打开文件描述符。

    cmd的取值如下:

    • F_SETLK:申请锁(读锁F_RDLCK,写锁F_WRLCK)或者释放所(F_UNLCK),但是如果kernel无法将锁授予本进程(被其他进程持有),立即返回error,不会阻塞,并将冲突锁的信息,保存存在 struct flock中。

    • F_SETLKW:和F_SETLK几乎一样,唯一的区别是申请不到锁,就会阻塞。

    • F_GETLK:这个操作是获取锁的相关信息,并会修改我们传入的lock。进程可以通过此操作,来获取fd指向的那个文件的加锁信息。执行该操作时,lock中就保存了希望对文件的加锁信息(或者是测试是否可以加锁)。如果确实存和lock冲突的锁,内核会把冲突的锁的信息写到lock中,并将该锁拥有者的PID写入 l_pid字段中,然后返回;否则,就将lock中的l_type设置为 F_UNLCK,并保持 lock中其他信息不变返回,而不是对该文件真正加锁。

    需要注意的是,F_GETLK 用于测试是否可以加锁,在 F_GETLK 测试可以加锁之后,F_SETLKF_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) */ 
    ...        
    }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    flock结构说明:

    • 锁类型:共享读锁F_RDLCK,独占写锁F_WRLCK,解锁F_UNLCK

    • 加锁或解锁区域的起始字节偏移量,由l_start和l_whence决定。

    • l_start是相对偏移量,l_whence决定了l_start的起点,l_whence可选用的值为SEEK_SET, SEEK_CUR, SEEK_END。

    • 区域字节长度(l_len)

    • F_GETLK获取已存在的冲突锁的进程PID(l_pid)

    • 锁可以在文件尾处开始或者越过尾端开始,但是不能在文件起始位置之前开始

    • 若l_len=0, 表示锁的范围可以扩大到最大可能偏移量,这意味着,不论往文件中追加多少数据,它们都处于锁的范围内

    • 设置l_start和l_whence指向文件的起始位置,并且指定l_len=0,以实现对整个文件加锁(一般l_start=0, l_whence=SEEK_SET)

    主要特性

    1. 加锁可递归,如果一个进程对一个文件区间已经有一个锁,后来又在同一区间再加一个锁,在没有冲突的前提下,则新锁将替换老锁。

    2. 加读锁(共享锁)文件必须是读打开,加写锁(排他锁)文件必须是写打开。

    3. 进程不能使用F_GETLK命令来测试它自己是否在文件的某一部分持有一个锁。F_GETLK命令定义说明,返回信息指示是否现存的锁阻止调用进程设置它自己的锁。因为,F_SETLKF_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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    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 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    本地文件系统:
    在这里插入图片描述

    nfs导出:
    在这里插入图片描述

    dup测试

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    本地文件系统:

    在这里插入图片描述

    nfs导出:
    在这里插入图片描述

    fork测试

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    本地文件系统:
    在这里插入图片描述

    nfs导出:
    在这里插入图片描述

    死锁检查测试

    测试代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    测试结果如下:
    请添加图片描述

    自己进程检查到了死锁,直接返回,不再阻塞,最后父子都退出了。

  • 相关阅读:
    adrv9025 dpd错误标识
    C Primer Plus(6) 中文版 第8章 字符输入/输出和输入验证 8.1 单字符I/O:getchar()和putchar()
    Java面试题-Java核心基础-第十三天(序列化)
    突破视觉边界:深入探索AI图像识别的现状与挑战
    Web前端:JS社区推荐的最佳JavaScript框架
    基于transformer一步一步训练一个多标签文本分类的BERT模型
    架设一台NFS服务器,并按照以下要求配置
    使用 Go HTTP 框架 Hertz 进行 JWT 认证
    Docker——本文来自AI创作助手
    JavaScript——作用域和预解析,深度理解代码执行程序
  • 原文地址:https://blog.csdn.net/SaberJYang/article/details/126217112