文件锁是一种文件读写控制机制, 它可以控制多个并发运行的进程合理地读写文件, 在 Unix 中, 操作文件锁的 API 有多个, 常用的有 flock() / open() / fcntl() 等, flock() 锁定的粒度是文件级别的, fcntl() 可以有更细的锁定粒度, 可以锁定一个文件内的部分区域, 本文讨论 Unix 文件锁的 API 及其应用

22.1 flock() 系统调用

flock() 是比较简单的文件锁 API, 它最早出现于 4.2 BSD, 它的函数原型为

int flock(int fd, int operation);

其中第一个参数是文件描述符, 第二个参数为锁类型, 在 <sys/file.h> 中定义了 4 个宏, 分别是 LOCK_SH || LOCK_EX || LOCK_NB || LOCK_UN, 其中 LOCK_SH 表示共享锁, 一个文件可以同时被多个进程施加共享锁, 而 LOCK_EX 表示排他锁, 在任何时刻, 同一文件至多只有一个排他锁, 即排他锁与排他锁是互斥的, 排他锁与共享锁之间也是互斥的, 当 fd 对应的文件已经被其它进程加锁之后, 另一个进程调用 flock() 尝试锁定该文件, 若施加的锁与当前文件上的锁是互斥的, 则进程会被阻塞, 直到前一个进程将锁释放, 进程可以通过在加锁时设置 LOCK_NB 标志让之后尝试施加互斥锁的进程不陷入阻塞状态, 在设置了 LOCK_NB 标志后, 另一个进程想要添加针对同一文件的互斥锁时将不会阻塞, 而是直接返回, 并将 errno 设置为 EWOULDBLOCK, LOCK_UN 标志用于解除在 fd 上添加的文件锁

flock() 添加的锁是建议性锁, 也就是说使用 flock() 对文件加锁后, 其它进程仍可以获得访问权并修改文件, 其它进程只有通过 flock() 调用去检测才可以知晓该文件是否处于被锁定的状态, 因此 flock() 添加的文件锁只是起到通知的作用, 没有强制阻止其它进程修改文件的机制, 因此适合用于已知进程之间的互相协调, flock() 调用参数简单, 可以非常方便地实现多进程对同一文件的并发读写控制

关于 flock() 使用的注意事项

flock() 调用锁定的是文件描述符 fd 对应的文件, 而不是文件描述符本身, 这意味着如果通过 dup() 和 fork() 调用复制的文件描述符实际上共享同一个文件锁, 因此共享文件锁的进程中只要有一个通过设置 LOCK_UN 释放文件锁后, 其它进程也都会丢失文件锁, 以下是 man page 对于 flock() 的说明
Locks are on files, not file descriptors. That is, file descriptors duplicated through dup(2) or fork(2) do not result in multiple instances of a lock, but rather multiple references to a single lock. If a process holding a lock on a file forks and the child explicitly unlocks the file, the parent will lose its lock.

22.2 open() 系统调用

使用 open() 系统调用也可以实现与 flock() 类似的效果, open() 主要是用于在系统中打开一个文件, 它最早出现于 Version 6 AT&T UNIX 上, open() 函数的原型如下:

int open(const char *path, int oflag, ...);

path 参数为文件的路径, oflag 用来指示文件的操作方式, 如 O_RDONLY (只读), O_WRONLY (只写), O_RDWR (读写), open() 调用在使用时必须指定这 3 种方式的其中一种, oflag 除了指示文件读写方式之外还设有其它可与之组合的标志位 (即通过或运算组合), 如 O_NONBLOCK, O_EXLOCK, O_SHLOCK, O_CREAT, O_EXCL 等, 其中 O_EXLOCK 和 O_SHLOCK 与 flock() 调用的锁标志的语义完全相同, 分别用于在打开文件的同时对文件施加排他锁或共享锁, O_NONBLOCK 标志用于设置当文件已被某进程打开并锁定, 其它进程在此期间也要打开文件并施加与该文件已有的锁相互斥的锁时不会被阻塞, 而是直接返回相应的错误, O_CREAT 标志用于当 path 指定的文件不存在时自动创建文件, 当 O_EXCL 标志与 O_CREAT 共同使用时, 若文件已存在, 则调用将会失败, 因此使用 O_EXCL | O_CREAT 的组合可以实现与 flock() 相同的效果, 下面给出一段代码示例:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    int file_desc;
    int save_errno;
    file_desc = open("/tmp/test-open.lock", O_RDWR | O_CREAT | O_EXCL, 0444);
    if (file_desc == -1) {
        save_errno = errno;
        printf("Open failed with error %d\n", save_errno);
    } else {
        printf("Open succeeded\n");
    }
    exit(EXIT_SUCCESS);
}

编译 / 运行如上的程序, 第一次会成功创建文件, 当第二次运行时, 程序的 errno 为 17 (EEXIST), 因为文件已存在了, 此时再次使用 O_CREAT | O_EXCL 执行 open() 调用将会失败, 这里的 open() 调用是原子性的, 即 检查文件不存在 与 创建文件 是一个原子操作, 因此可以用来作为进程锁使用

22.3 fcntl() 系统调用

上面我们讨论的 flock() 调用以及 open() 调用, 它们对文件的锁定粒度都是整个文件级别的, fcntl() 是一种锁定粒度更细的文件锁 API, 它最早出现于 4.2 BSD 上, 它的函数原型如下:

int fcntl(int fildes, int cmd, ...);

fcntl() 调用的参数比较多, 用法相比 flock() 和 open() 要复杂一些, 其中第一个参数为文件描述符, 第二个参数为操作指令, 与锁相关的操作指令有 F_SETLK, F_GETLK 以及 F_SETLKW, 其中 F_SETLK 用于对文件加锁, F_GETLK 用于测试并获取文件锁信息 (如果存在), F_SETLKW 用于加锁, 如果当前无法加锁则会阻塞, 当使用这 3 个操作指令时, 其第三个参数是 *struct flock 类型, struct flock 的结构如下

struct flock {
    off_t   l_start;        /* starting offset */
    off_t   l_len;          /* len = 0 means until end of file */
    pid_t   l_pid;          /* lock owner */
    short   l_type;         /* lock type: read/write, etc. */
    short   l_whence;       /* type of l_start */
};

该参数描述了锁类型, 锁定的文件区域等, 来看如下的代码示例:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

int main() {
    int file_desc;
    int byte_count;
    char *byte_to_write = "A";
    struct flock region_1;
    struct flock region_2;
    int res;
    // 创建测试文件
    file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
    if (!file_desc) {
        fprintf(stderr, "Unable to open %s for read/write\n", test_file);
        exit(EXIT_FAILURE);
    }
    // 向测试文件中写入 100 个字符 A
    for (byte_count = 0; byte_count < 100; byte_count++) {
        (void) write(file_desc, byte_to_write, 1);
    }
    // 对前 30 个字节添加读锁
    region_1.l_type = F_RDLCK;
    region_1.l_whence = SEEK_SET;
    region_1.l_start = 10;
    region_1.l_len = 20;
    // 对 40 ~ 50 字节添加写锁
    region_2.l_type = F_WRLCK;
    region_2.l_whence = SEEK_SET;
    region_2.l_start = 40;
    region_2.l_len = 10;
    printf("Process %d locking file\n", getpid());

    // 锁定文件
    res = fcntl(file_desc, F_SETLK, &region_1);
    if (res == -1) fprintf(stderr, "Failed to lock region 1\n");
    res = fcntl(file_desc, F_SETLK, &region_2);
    if (res == -1) fprintf(stderr, "Failed to lock region 2\n");
    // 等待 60s, 便于使用另一个程序进行锁测试
    sleep(60);
    printf("Process %d closing file\n", getpid());
    close(file_desc);
    exit(EXIT_SUCCESS);
}

在上面的代码示例中, 首先创建一个测试文件, 然后对文件的前 30 个字节区域添加读锁, 对文件的第 40 ~ 50 字节区域添加写锁, 我们使用另外一个程序对上面创建的文件进行测试, 代码如下:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

#define SIZE_TO_TRY 5

void show_lock_info(struct flock *to_show);

int main() {
    int file_desc;
    int res;
    struct flock region_to_test;
    int start_byte;
    file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
    if (!file_desc) {
        fprintf(stderr, "Unable to open %s for read/write", test_file);
        exit(EXIT_FAILURE);
    }
    for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {
        // 尝试添加写锁
        region_to_test.l_type = F_WRLCK;
        region_to_test.l_whence = SEEK_SET;
        region_to_test.l_start = start_byte;
        region_to_test.l_len = SIZE_TO_TRY;
        region_to_test.l_pid = -1;
        printf("Testing F_WRLCK on region from %d to %d\n",
        start_byte, start_byte + SIZE_TO_TRY);

        // 获取锁信息
        // 如果文件已被锁定, 并且 region_to_test 结构声明的锁信息与文件锁互斥, 则相关的锁信息会写到 region_to_test 结构体中
        res = fcntl(file_desc, F_GETLK, &region_to_test);
        if (res == -1) {
            fprintf(stderr, "F_GETLK failed\n");
            exit(EXIT_FAILURE);
        }
        if (region_to_test.l_pid != -1) {
            printf("Lock would fail. F_GETLK returned:\n");
            show_lock_info(&region_to_test);
        } else {
            printf("F_WRLCK - Lock would succeed\n");
        }


        region_to_test.l_type = F_RDLCK;
        region_to_test.l_whence = SEEK_SET;
        region_to_test.l_start = start_byte;
        region_to_test.l_len = SIZE_TO_TRY;
        region_to_test.l_pid = -1;
        printf("Testing F_RDLCK on region from %d to %d\n",
               start_byte, start_byte + SIZE_TO_TRY);
        res = fcntl(file_desc, F_GETLK, &region_to_test);
        if (res == -1) {
            fprintf(stderr, "F_GETLK failed\n");
            exit(EXIT_FAILURE);
        }
        if (region_to_test.l_pid != -1) {
            printf("Lock would fail. F_GETLK returned:\n");
            show_lock_info(&region_to_test);
        } else {
            printf("F_RDLCK - Lock would succeed\n");
        }
    }
    close(file_desc);
    exit(EXIT_SUCCESS);
}

void show_lock_info(struct flock *to_show) {
    printf("\tl_type %d, ", to_show->l_type);
    printf("l_whence %d, ", to_show->l_whence);
    printf("l_start %d, ", (int) to_show->l_start);
    printf("l_len %d, ", (int) to_show->l_len);
    printf("l_pid %d\n", to_show->l_pid);
}

编译 / 运行测试代码, 可以看到如下输出:

Testing F_WRLCK on region from 0 to 5
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 0 to 5
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 5 to 10
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 5 to 10
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 10 to 15
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 10, l_len 20, l_pid 37848
Testing F_RDLCK on region from 10 to 15
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 15 to 20
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 10, l_len 20, l_pid 37848
Testing F_RDLCK on region from 15 to 20
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 20 to 25
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 10, l_len 20, l_pid 37848
Testing F_RDLCK on region from 20 to 25
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 25 to 30
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 10, l_len 20, l_pid 37848
Testing F_RDLCK on region from 25 to 30
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 30 to 35
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 30 to 35
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 35 to 40
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 35 to 40
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 3, l_whence 0, l_start 40, l_len 10, l_pid 37848
Testing F_RDLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 3, l_whence 0, l_start 40, l_len 10, l_pid 37848
Testing F_WRLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
l_type 3, l_whence 0, l_start 40, l_len 10, l_pid 37848
Testing F_RDLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
l_type 3, l_whence 0, l_start 40, l_len 10, l_pid 37848
Testing F_WRLCK on region from 50 to 55
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 50 to 55
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 55 to 60
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 55 to 60
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 60 to 65
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 60 to 65
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 65 to 70
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 65 to 70
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 70 to 75
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 70 to 75
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 75 to 80
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 75 to 80
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 80 to 85
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 80 to 85
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 85 to 90
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 85 to 90
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 90 to 95
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 90 to 95
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 95 to 100
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 95 to 100
F_RDLCK - Lock would succeed

可以看到 fcntl() 调用只锁定了文件的指定区域, 当一个进程对文件的部分区域添加了排他锁之后, 其他进程可以对该文件的其他区域进行加锁, 只要二者没有重叠便不会加锁失败, 我们可以利用 fcntl() 来实现一个简化版的针对关系型数据库的行级锁, 使用一个文件来存放数据表的数据, 数据表每一行记录的长度是等长的, 因此可以根据区域选择性的对数据文件加锁, 实现多个进程同时读写同一个文件而又不会破坏一致性

以上是对 Unix 文件锁的一个概述, 读者可查阅 man page 获取更详细的用法


如果文章对您有帮助, 不妨请作者喝杯咖啡