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

文件锁简介

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

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

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

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

文件锁的基本规则

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

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

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

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

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

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

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

flock介绍

函数原型

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

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 <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的取值如下:

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

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

  • F_ULOCK :解锁。

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

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

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

fcntl介绍

函数原型

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

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) */ 
...        
}; 

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. 由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;
}

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

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

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 嵌入式Linux操作系统原理应用(第三版)是一本全面介绍嵌入式Linux操作系统的参考书籍。本书主要包括嵌入式系统概述、Linux内核概述、Linux系统启动、系统调试、应用程序开发等方面的内容。 本书首先介绍了嵌入式系统的概念,包括嵌入式系统的分类、结构和特点。然后就Linux内核进行了全面的讲解,包括内核编译、模块的使用、驱动程序的编写等方面。接着讲解了Linux系统启动的流程,包括BIOS、U-boot、内核启动过程等,深入探讨了系统中各个组成部分的作用和相互之间的关系。 在系统调试方面,书中涵盖了日志系统的使用、内存管理、调试工具的使用等方面的内容,帮助开发者提高代码质量。最后,本书详细介绍了嵌入式Linux应用程序的开发,包括Qt应用程序开发、网络编程、多线程、定时器和中断等方面的内容。 总体来说,《嵌入式Linux操作系统原理应用(第三版)》是一本非常全面、系统的介绍嵌入式Linux操作系统的参考书籍,对开发者来说有较高的参考价值。 ### 回答2: 嵌入式linux操作系统是一种经过裁剪和优化的操作系统,它具有稳定、可靠、安全、高效、易于移植等特点。嵌入式linux操作系统主要应用于各种嵌入式系统中,例如手机、路由器、工控设备等。该操作系统可以根据不同的应用场景进行定制,满足不同的需求。 嵌入式linux操作系统的核心是内核,它是操作系统中最基本、最核心的部分。内核负责管理系统资源、调配各种任务、驱动硬件设备等。嵌入式linux操作系统还包括启动引导程序、文件系统、各种库和应用程序等。启动引导程序是系统启动的第一步,它负责加载内核和其他文件文件系统负责管理和组织存储在磁盘或其他存储设备上的文件。库和应用程序是系统提供的各种工具和接口,用于开发和运行应用程序。 嵌入式linux操作系统的应用非常广泛。在工业自动化领域,该操作系统被广泛应用于各种自动化设备中,例如工业机器人、数控机床等。在智能家居领域,嵌入式linux操作系统被应用于智能家居设备中,例如智能门、智能灯泡等。在移动互联网领域,嵌入式linux操作系统被应用于各种智能手机、平板电脑等移动设备中。 总之,嵌入式linux操作系统是一种非常重要的操作系统,它具有广泛的应用前景和市场需求。对于开发人员而言,掌握嵌入式linux操作系统的原理应用技术,将带来更多的机会和挑战。 ### 回答3: 嵌入式linux操作系统原理应用(第三版)是一本介绍嵌入式linux操作系统的重要理论和实践方法的书籍。嵌入式linux操作系统是运行在各类嵌入式设备(如智能手机、电视机顶盒、路由器等)中的小型化的操作系统,其实现原理和一般的linux操作系统有所不同。 该书第一部分介绍了嵌入式linux操作系统的基础知识,包括嵌入式系统的体系结构、linux命令行的使用、嵌入式开发工具的选取与使用等。第二部分则介绍了嵌入式linux操作系统的内核原理、进程管理、内存管理等方面的内容。其中,最重要的是对内核源代码的进行深入分析,通过讲解内核源代码的各个部分来帮助读者建立更全面的嵌入式linux操作系统的工作机制的认识。 第三部分则介绍了嵌入式linux操作系统在实际应用中的开发过程,包括在嵌入式系统中的裸机开发和在操作系统上进行应用开发等方面的知识。书中提供了大量的示例代码和实验项目,以实际的案例来帮助读者理解嵌入式linux操作系统的实现方法和技术。 总之,嵌入式linux操作系统原理应用(第三版)是一本结合理论和实践的丰富介绍嵌入式linux操作系统的书籍,不仅适合嵌入式开发工程师和研究人员使用,也适合希望学习嵌入式linux操作系统原理应用的计算机科学和工程领域的学生和爱好者学习。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值