• Linux 文件锁与记录锁


    基本概念

    记录锁

    记录上锁(record locking)是读写锁的一种扩展类型,可用于亲缘进程或无亲缘进程之间共享某个文件的读和写,常简称为记录锁。读写锁可参见这篇文章:Linux 自旋锁,互斥量(互斥锁),读写锁

    记录锁锁定的文件通过文件描述符访问,调用fcntl执行上锁和解锁操作。记录锁的维护通常在内核中,属主是由属主进程ID标识。也就是说,记录锁用于不同进程间的上锁,而不是用于同一进程内不同线程间的上锁。

    文件锁

    记录锁可以指定文件中待上锁或解锁部分的字节范围(byte range)。当记录锁锁定的范围是整个文件时,就称为文件上锁(file locking,简称文件锁)。可以说,文件锁是一种特殊的记录锁。

    粒度

    粒度(granularity)用于标记能被锁住的对象的大小。对于Posix记录锁,粒度是单个字节。对于文件锁,粒度是整个文件。

    记录锁与读写锁

    记录锁和读写锁的上锁方式都分为:读出锁、写入锁。读出锁可以同时有多个,写入锁同时只能有一个。

    不过,读写锁是pthread_rwlock_t类型的变量在内存中分配的。当读写锁是在单个进程内的各个线程间共享时(默认属性),分配的变量可以在单个进程内;当读写锁在共享内存区时,各进程共享读写锁变量。
    也就是说,读写锁锁定的对象是内存中的锁变量,可用于多进程,也可以用于多线程。

    记录锁锁定的对象是记录(文件的某些字节范围),用于多进程。

    Posix记录上锁

    函数接口fcntl

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

    参数

    • fd 已打开文件的文件描述符
    • cmd 有3个取值,代表3个命令:F_SETLK, F_SETLKW, F_GETLK。
    • arg flock结构对象

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

    cmd3个命令:

    取值 描述
    F_SETLK 获取(l_type = F_RDLCK或F_WRLCK)或释放(l_type=F_UNLCK)由arg指向的flock结构所描述的锁。
    如果无法将该锁授予调用进程,该函数就立即返回一个EACCESS或EAGAIN错误而不阻塞。
    F_SETLKW 与F_SETLK类似,区别在于调用线程如果无法取得锁,调用线程将阻塞到该锁能取得为止。名字最后的W是“wait”的意思。
    F_GETLK 检查是否有线程/进程已获得锁。如果当前没有上锁,由arg指向的flock.l_type会被置为F_UNLCK;否则,由arg指向的flock对象就会包含持有该锁的进程ID。

    返回值
    成功取决于cmd;出错,返回-1。

    既然返回值已经能表示获得锁的结果,为何还需要cmd=F_GETLK命令?
    因为当执行fcntl + F_SETLK/F_SETLKW命令返回一个错误时,导致该错误的某个锁的信息可以由F_GETLK命令返回,从而允许确定是哪个进程锁住了所请求的文件区,及上锁方式(读出锁或写入锁)。当然,当文件区解锁时,F_GETLK命令也可能返回文件区解锁的信息。

    flock结构锁住文件区

    上锁类型
    flock对文件区的上锁类型由l_type成员决定:

    • F_RDLCK 读出方式上锁
    • F_WRLCK 写入方式上锁
    • F_UNLCK 解锁(不论之前以读出,还是写入方式上锁)

    锁定范围
    flock对文件区的锁定范围取决于3个成员值l_whence + l_start + l_len

    l_whence用来解释l_start起始位置,粗略定位起始位置;l_start(相对偏移)是在l_whence基础上的偏移;l_len指定从l_whence和l_start决定的偏移开始的连续字节数。

    下面根据l_whence 取值来解释文件区锁定范围:

    • SEEK_SET: l_start相对于文件的开头解释,锁定范围为[l_start, l_len);
    • SEEK_CUR: l_start相对于文件的当前字节偏移(即文件当前读写指针位置)解释,锁定范围为[cur_pos + l_start, cur_pos + l_len)(假设当前文件读写指针位于cur_pos(相对于文件起始位置而言的偏移));
    • SEEK_END: l_start相对于文件的末尾解释,锁定范围为[file_size+ l_start, file_size+ l_len)(假设当前文件字节数为file_size);

    锁住整个文件
    锁住整个文件的方式有2种:
    1)设置flock结构成员l_whence = SEEK_SET,l_start = 0, l_len = 0(0表示不限制字节数);
    2)调用lseek将读写指针定位到文件开头,然后设置flock结构成员l_whence = SEEK_CUR,,l_start = 0, l_len = 0(0表示不限制字节数);

    最后,调用fcntl F_SETLK/F_SETLKW获得锁;

    常用第1)种方式。

    记录锁的释放
    1)通过设置flock.l_type = F_UNLCK,调用fcntl F_SETLK/F_SETLKW来解锁;
    2)通过关闭与文件关联的进程,会关闭所有文件描述符,删除锁;

    问题:记录锁能应用于多线程吗?

    先不着急下结论,而是做个实验:创建2个线程tid1和tid2,观察tid1获得记录锁期间,tid2是否也能同时获得锁。
    如果tid1获得记录锁的时候,tid2也能获得记录锁,说明记录锁不能用于多线程;否则,说明记录锁可以用于多线程。

    void *thread_test_flock1(void *arg)
    {
        int id = (int)arg;
    
        printf("hello, thread %d
    ", id);
        writew_lock(file_fd);
        printf("thread %d get write flock
    ", id);
        sleep(5);
    
        printf("thread %d work
    ", id);
        un_lock(file_fd);
        printf("thread %d release write flock
    ", id);
    }
    
    void *thread_test_flock2(void *arg)
    {
        int id = (int)arg;
    
        printf("hello, thread %d
    ", id);
        sleep(2);
        printf("thread %d get write flock
    ", id);
        writew_lock(file_fd);
    
        printf("thread %d work
    ", id);
        un_lock(file_fd);
        printf("thread %d release write flock
    ", id);
    }
    

    这是获得写入锁和解锁的操作:

    int writew_lock(int fd)
    {
        struct flock lock;
        lock.l_type = F_WRLCK;
        lock.l_whence = SEEK_SET;
        lock.l_start = 0;
        lock.l_len = 0;
        lock.l_pid = getpid();
    
        if (fcntl(fd, F_SETLKW, &lock) < 0) {
            perror("fcntl error");
            exit(1);
        }
        return 0;
    }
    
    int un_lock(int fd)
    {
        struct flock lock;
        lock.l_type = F_UNLCK;
        lock.l_whence = SEEK_SET;
        lock.l_start = 0;
        lock.l_len = 0;
        lock.l_pid = getpid();
    
        if (fcntl(fd, F_SETLKW, &lock) < 0) {
            perror("fcntl error");
            exit(1);
        }
        return 0;
    }
    

    file_fd定义及创建线程的main:

    int file_fd = -1;
    int main()
    {
        pthread_t tid1, tid2;
    
        if ((file_fd = open("test_flock.txt", O_CREAT | O_WRONLY)) < 0) {
            perror("open error");
            exit(1);
        }
    
        pthread_create(&tid1, NULL, thread_test_flock1, (void *)1);
        pthread_create(&tid2, NULL, thread_test_flock2, (void *)2);
    
        pthread_join(tid1, NULL);
        pthread_join(tid2, NULL);
    
        return 0;
    }
    

    运行结果:
    表明在线程tid1获得写入锁时,tid2页获得了写入锁。说明记录锁不能应用于多线程环境。

    hello, thread 1
    thread 1 get write flock
    hello, thread 2
    thread 2 get write flock
    thread 2 work
    thread 2 release write flock
    thread 1 work
    thread 1 release write flock
    

    问题:记录锁能用于多进程吗?

    虽然我们已经知道是可以,不过,这里还是实验验证下,跟前面多线程环境做对比。

    获得写入锁、解锁操作,跟前面一样,保持不变。main函数见下:

    int main()
    {
        pid_t pid1, pid2;
        int fd = open("test_flock.txt", O_CREAT | O_WRONLY, 0774);
        if (fd < 0) {
            perror("open error");
            exit(1);
        }
    
        if ((pid1 = fork()) < 0) { // error
            perror("fork1 error");
            exit(1);
        }
        else if (pid1 == 0) { // child - pid1
            if ((pid2 = fork()) < 0) {// error
                perror("fork2 error");
                exit(1);
            }
            else if (pid2 == 0){// child - pid2
                sleep(1);
                printf("hello, pid2
    ");
    
                writew_lock(fd);
                printf("pid2 get write lock
    ");
    
                un_lock(fd);
                printf("pid2 unlock
    ");
            }
            // pid1
            printf("hello, pid1
    ");
    
            writew_lock(fd);
            printf("pid1 get write lock
    ");
            sleep(5);
    
            un_lock(fd);
            printf("pid1 unlock
    ");
        }
    
        int status;
        while (waitpid(-1, &status, 0) > 0) {
            if (errno == ECHILD) {
                break;
            }
        }
        return 0;
    }
    

    运行结果:
    可以看出,在进程pid1释放记录锁前,pid2无法获得锁。

    hello, pid1
    pid1 get write lock
    hello, pid2
    pid1 unlock
    pid2 get write lock
    pid2 unlock
    hello, pid1
    pid1 get write lock
    pid1 unlock
    

    文件上锁

    文件上锁通常使用flock()函数进行,相比较记录上锁的调用fcntl,传入flock结构更简便。

    flock函数接口

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

    参数

    • fd 已打开文件描述符。
    • operation 上锁操作可以是下面几个取值:
    operation值 描述
    LOCK_SH Place a shared lock. More than one process may hold a shared lock for a given file at a given time.
    LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
    LOCK_UN Remove an existing lock held by this process.

    可以看出,LOCK_SH用于共享锁,对应记录上锁的读锁;LOCK_EX用于独占锁,对应记录上锁的写入锁;LOCK_UN用于解锁。

    返回值
    成功,返回0;出错,返回-1.

    问题:文件上锁flock能应用于多线程环境?

    答案是不行的。验证方式类似前面的,将获得写入锁writew_lock,和解锁un_lock,都换成flock函数。

    #include <sys/file.h>
    
    void *thread_test_flock1(void *arg)
    {
        int id = (int)arg;
    
        printf("hello, thread %d
    ", id);
    //    writew_lock(file_fd);
        flock(file_fd, LOCK_EX); // 取得独占锁
        printf("thread %d get write flock
    ", id);
        sleep(5);
    
        printf("thread %d work
    ", id);
    //    un_lock(file_fd);
        flock(file_fd, LOCK_UN); // 释放锁
        printf("thread %d release write flock
    ", id);
    }
    
    void *thread_test_flock2(void *arg)
    {
        int id = (int)arg;
    
        printf("hello, thread %d
    ", id);
        sleep(2);
        printf("thread %d get write flock
    ", id);
    //    writew_lock(file_fd);
        flock(file_fd, LOCK_EX); // 取得独占锁
    
        printf("thread %d work
    ", id);
    //    un_lock(file_fd);
        flock(file_fd, LOCK_UN); // 释放锁
    
        printf("thread %d release write flock
    ", id);
    }
    

    运行结果:
    可以看到,在线程1释放独占锁之前,线程2也获得了独占锁。

    hello, thread 1
    thread 1 get write flock
    hello, thread 2
    thread 2 get write flock
    thread 2 work
    thread 2 release write flock
    thread 1 work
    thread 1 release write flock
    

    参考

    UNP 卷2

  • 相关阅读:
    HDU_5688
    微服务的一致性问题
    我来博客园啦
    异常:This application has no explicit mapping for /error, so you are seeing this as a fallback.
    异常:Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException
    异常:Unknown lifecycle phase "mvn". You must specify a valid lifecycle
    SpringMVC中遇到页面跳转出现404错误的问题
    Could not resolve view with name '***' in servlet with name 'dispatcher'
    关于Could not resolve dependencies for project
    让我们相互交流
  • 原文地址:https://www.cnblogs.com/fortunely/p/15219611.html
Copyright © 2020-2023  润新知