Linux 文件锁与记录锁

基本概念

记录锁

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

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

文件锁

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

粒度

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

记录锁与读写锁

记录锁(record lock)和读写锁(rw lock)的上锁方式都分为:读出锁、写入锁。读出锁可以同时有多个,是共享的;写入锁同时只能有一个,是互斥的。

读写锁与记录锁的区别:

  1. 多线程,多进程应用环境
    读写锁锁定的对象是内存中的锁变量,可用于多进程,也可以用于多线程;记录锁锁定的对象是记录(文件的某些字节范围),而文件描述符表是以进程为单位由操作系统给分配的,只能用于多进程。
    具体地,读写锁是pthread_rwlock_t类型的变量在内存中分配的。当读写锁是在单个进程内的各个线程间共享时(默认属性),分配的变量可以在单个进程内;当读写锁在共享内存区时,各进程共享读写锁变量。

  2. 优先级顺序
    读出锁和写入锁有一个优先级关系,在写入锁待处理期间,会优先选择优先级高的锁。
    读写锁有两种优先级策略:读出锁优先,写入锁优先。取决于具体实现。
    记录锁有多种种实现,Solaris 2.6, Digital Unix 4.0B以FIFO顺序处理;BSD/OS 3.1优先考虑读出锁。

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\n", id);
    writew_lock(file_fd);
    printf("thread %d get write flock\n", id);
    sleep(5);

    printf("thread %d work\n", id);
    un_lock(file_fd);
    printf("thread %d release write flock\n", id);
}

void *thread_test_flock2(void *arg)
{
    int id = (int)arg;

    printf("hello, thread %d\n", id);
    sleep(2);
    printf("thread %d get write flock\n", id);
    writew_lock(file_fd);

    printf("thread %d work\n", id);
    un_lock(file_fd);
    printf("thread %d release write flock\n", 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\n");

            writew_lock(fd);
            printf("pid2 get write lock\n");

            un_lock(fd);
            printf("pid2 unlock\n");
        }
        // pid1
        printf("hello, pid1\n");

        writew_lock(fd);
        printf("pid1 get write lock\n");
        sleep(5);

        un_lock(fd);
        printf("pid1 unlock\n");
    }

    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\n", id);
//    writew_lock(file_fd);
    flock(file_fd, LOCK_EX); // 取得独占锁
    printf("thread %d get write flock\n", id);
    sleep(5);

    printf("thread %d work\n", id);
//    un_lock(file_fd);
    flock(file_fd, LOCK_UN); // 释放锁
    printf("thread %d release write flock\n", id);
}

void *thread_test_flock2(void *arg)
{
    int id = (int)arg;

    printf("hello, thread %d\n", id);
    sleep(2);
    printf("thread %d get write flock\n", id);
//    writew_lock(file_fd);
    flock(file_fd, LOCK_EX); // 取得独占锁

    printf("thread %d work\n", id);
//    un_lock(file_fd);
    flock(file_fd, LOCK_UN); // 释放锁

    printf("thread %d release write flock\n", 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

posted @ 2021-09-02 18:57  明明1109  阅读(1795)  评论(2编辑  收藏  举报