22、文件锁机制:使用flock或lockf实现多进程文件访问控制

多进程同时读写同一个文件,这事儿我早年吃过不少亏。你想想看,两个进程同时往一个日志文件里写东西,结果互相覆盖,数据乱成一锅粥。嗯,这就是典型的“竞争条件”。

解决这个问题,说白了就是给文件上一把锁。C语言里常用的有两种:flocklockf。它们俩思路不太一样,但目标一致——让多进程有序访问文件。

为什么需要文件锁?

我举个例子你就明白了。假设你有一个计数器文件,里面存着当前用户数。进程A读出来是100,加1准备写回101。与此同时,进程B也读出来是100,加1也准备写回101。结果呢?本该是102,最后却成了101。数据丢了!

文件锁就是干这个的——保证同一时刻只有一个进程能操作关键数据。

核心要点:文件锁不是锁文件本身,而是锁住你对文件的“操作权”。其他进程想操作,就得等着。

flock:简单粗暴的劝退锁

flock 是BSD系统上就有的老牌锁。它操作的是文件描述符,而不是文件本身。什么意思呢?就是同一个文件,你用不同的文件描述符打开,flock 会认为它们是不同的锁。

先看个基本用法:

#include <stdio.h>
#include <sys/file.h>
#include <unistd.h>

int main() {
    FILE *fp = fopen("data.txt", "r+");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    int fd = fileno(fp);

    // 加锁:阻塞等待
    if (flock(fd, LOCK_EX) == -1) {
        perror("flock");
        fclose(fp);
        return 1;
    }

    printf("拿到锁了,开始干活\n");
    // 这里写你的文件操作...
    sleep(5);

    // 解锁
    flock(fd, LOCK_UN);
    fclose(fp);
    return 0;
}

flock 支持三种锁模式:

含义 说明
LOCK_SH 共享锁 多个进程可以同时加共享锁,适合读操作
LOCK_EX 排他锁 只有一个进程能加锁,适合写操作
LOCK_UN 解锁 释放锁

还有一个 LOCK_NB 标志,配合上面用。加上它,加锁时不会阻塞,直接返回错误。我经常用这个做“尝试加锁,不行就干别的”的逻辑。

我的习惯:读多写少的场景,用 LOCK_SH 共享锁。写操作时,先升级成 LOCK_EX。这样读操作不会被阻塞,效率高不少。

lockf:基于POSIX的精细锁

lockf 是POSIX标准的一部分。它跟 flock 最大的区别是——它可以锁文件的一部分!比如你只锁文件的前100字节,其他进程可以操作后面的内容。

来看个例子:

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

int main() {
    FILE *fp = fopen("data.txt", "r+");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    int fd = fileno(fp);

    // 锁住文件的前100字节
    if (lockf(fd, F_LOCK, 100) == -1) {
        perror("lockf");
        fclose(fp);
        return 1;
    }

    printf("锁住了前100字节\n");
    // 写数据...
    sleep(3);

    // 解锁
    lockf(fd, F_ULOCK, 100);
    fclose(fp);
    return 0;
}

lockf 的操作码:

操作码 含义 说明
F_LOCK 加锁(阻塞) 如果锁被占用,进程阻塞等待
F_TLOCK 尝试加锁(非阻塞) 加不上就返回错误,不等待
F_ULOCK 解锁 释放指定范围的锁
F_TEST 测试锁状态 检查指定范围是否被锁住
注意:lockf 的第三个参数是字节数。如果传0,表示锁住从当前位置到文件末尾的所有内容。我曾经因为这个参数传错,导致锁的范围不对,排查了半天。

flock vs lockf:怎么选?

说实话,这两个我都在项目里用过。给你个参考:

对比项 flock lockf
锁粒度 整个文件 文件的一部分
标准 BSD POSIX
跨平台 Linux/BSD/macOS Linux/Unix
与NFS兼容 部分支持 支持
使用场景 简单互斥 精细控制

我个人习惯是:如果只是简单的“别让两个进程同时写”,用 flock 就够了。代码少,逻辑清晰。但如果要处理大文件,多个进程各写各的区域,那就得上 lockf

避坑指南

我曾经在一个生产环境里遇到过这么个问题:进程A加了锁,然后崩溃了。结果锁没释放,其他进程全卡住了。嗯,这里要注意——flocklockf 在进程退出时都会自动释放锁。但如果是进程被 SIGKILL 杀掉呢?flock 会释放,lockf 也会释放。这是内核帮你做的,放心。

但有一个坑:如果你用 fork() 创建子进程,子进程会继承父进程的文件描述符和锁。这时候要小心,子进程解锁可能会影响到父进程的锁状态。我建议在 fork() 之后,子进程里主动关闭不需要的文件描述符。

小技巧:调试锁问题时,可以用 lslocks 命令查看系统里所有的文件锁。或者用 /proc/locks 文件,里面记录了内核维护的锁信息。我排查问题时经常看这个。

核心逻辑流程图

下面这张图展示了多进程使用文件锁的典型流程:

多进程文件锁工作流程 进程A 进程B 尝试加锁 (LOCK_EX) 尝试加锁 (LOCK_EX) 文件锁是否已被占用? (内核维护锁状态) 获得锁,执行文件操作 阻塞等待,直到锁释放

这张图里,进程A先尝试加锁,成功了就开始干活。进程B后到,发现锁被占着,就乖乖等着。等进程A干完活解锁,进程B才能拿到锁。这就是最典型的互斥访问模式。

实际项目中的建议

我在做数据库中间件的时候,用文件锁来保证配置文件的原子更新。具体做法是:

  1. 先写到一个临时文件
  2. 加锁
  3. rename() 替换原文件
  4. 解锁

这样即使写临时文件的过程中崩溃了,原文件也不会被破坏。嗯,这招叫“写时复制”,很多系统都在用。

记住:文件锁是协作锁。它只对主动加锁的进程有效。如果一个进程不守规矩,直接读写文件,锁就形同虚设。所以团队里要约定好——所有操作同一个文件的进程,都必须遵守加锁协议。

最后说一句:flocklockf 不能混用。它们在内核里维护的是两套独立的锁系统。你用一个加锁,另一个是感知不到的。我见过有人踩这个坑,排查了好久才发现是锁类型不匹配。


公众号:蓝海资料掘金营,微信deep3321