文件锁:flock、fcntl、lockf。多进程/多线程文件访问冲突解决

文件锁这个话题,说实话,我早年做嵌入式开发时吃过不少亏。那时候多个进程同时写一个日志文件,结果日志内容互相穿插,读出来根本没法看。后来才意识到——文件锁不是可选项,而是多进程环境下的必需品。

今天咱们就把 flockfcntllockf 这三兄弟掰扯清楚。它们都能解决文件访问冲突,但脾气秉性完全不同。

为什么需要文件锁?

你想想看,两个进程同时往一个文件里写数据,会发生什么?

  • 数据交错:A进程写了一半,B进程插进来写,最后文件内容乱七八糟
  • 读取脏数据:读的时候别人正在写,读到一半的数据
  • 文件损坏:极端情况下,文件系统元数据都可能出问题

文件锁就是给文件加一把「门锁」——谁拿到钥匙谁进门,其他人门外等着。

核心原则:文件锁只对「主动检查锁」的进程有效。不检查锁的进程,照样可以无视锁直接读写。所以文件锁是「君子协定」,不是强制机制。

三种文件锁的对比

特性 flock fcntl lockf
标准来源 BSD POSIX POSIX(基于fcntl)
锁粒度 整个文件 文件区域(字节范围) 文件区域(字节范围)
与文件描述符关系 关联文件描述符 关联进程+inode 关联进程+inode
NFS支持 部分支持 支持 支持
子进程继承 不继承 不继承 不继承
使用复杂度 简单 较复杂 中等

flock:简单粗暴,全文件锁

flock 是我个人用得最多的。为什么?因为它简单。一个函数调用,搞定整个文件的锁定。

#include <sys/file.h>

int fd = open("data.txt", O_RDWR);
if (fd == -1) { /* 处理错误 */ }

// 加共享锁(读锁)
flock(fd, LOCK_SH);

// 加排他锁(写锁)
flock(fd, LOCK_EX);

// 解锁
flock(fd, LOCK_UN);

// 非阻塞尝试
flock(fd, LOCK_EX | LOCK_NB);

我在项目中遇到过一个问题:两个进程都用 flock 加锁,但其中一个进程 fork 了子进程。子进程继承了文件描述符,结果父子进程互相干扰。嗯,这里要注意——flock 的锁是和文件描述符绑定的,不是和进程绑定的。如果你 dup 了文件描述符,两个描述符共享同一把锁。

我的建议:如果只是简单的「整个文件互斥访问」,用 flock 就够了。别把事情搞复杂。

fcntl:精细控制,区域锁

fcntl 就复杂多了。它能锁定文件的某一段,而不是整个文件。比如你只想锁文件的前100个字节,后面让别人随便读写。

#include <fcntl.h>

struct flock lock;
lock.l_type = F_WRLCK;    // 排他锁
lock.l_whence = SEEK_SET; // 从文件头开始
lock.l_start = 0;         // 偏移0
lock.l_len = 100;         // 锁定100字节
lock.l_pid = 0;           // 由内核填充

int fd = open("data.txt", O_RDWR);
if (fcntl(fd, F_SETLKW, &lock) == -1) {
    // 阻塞等待锁
}

// 非阻塞版本用 F_SETLK
if (fcntl(fd, F_SETLK, &lock) == -1) {
    if (errno == EACCES || errno == EAGAIN) {
        // 锁被占用
    }
}

// 解锁:设置 l_type 为 F_UNLCK
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);

这里有个坑,我曾经踩过:fcntl 的锁是和进程 + inode 绑定的。如果你 fork 了子进程,子进程不会继承锁。但如果你用 dup 复制了文件描述符,它们共享同一个文件描述,锁仍然有效。

注意:fcntl 的锁在进程退出时会自动释放。但如果你不显式解锁,其他进程可能一直等下去。我曾经见过一个服务挂了,结果所有客户端都卡在等待锁上——因为没人释放锁。

lockf:fcntl的简化版

lockf 其实是 fcntl 的封装,接口更友好一些。它只支持排他锁,不支持共享锁。

#include <unistd.h>

int fd = open("data.txt", O_RDWR);

// 锁定从当前位置开始的整个文件
lockf(fd, F_LOCK, 0);    // 阻塞
lockf(fd, F_TLOCK, 0);   // 非阻塞

// 锁定指定长度
lockf(fd, F_LOCK, 100);  // 锁定100字节

// 解锁
lockf(fd, F_ULOCK, 0);

说白了,lockf 就是 fcntl 的「傻瓜版」。如果你只需要排他锁,又不想记 struct flock 那一堆字段,用 lockf 就对了。

多线程场景下的文件锁

多线程和多进程不太一样。同一进程内的多个线程共享文件描述符,所以用 flockfcntl 加锁时,一个线程加锁,其他线程也能看到这把锁。

但有个问题:如果线程A加了锁,线程B在同一个文件描述符上尝试加锁,会怎么样?

  • flock:线程B会成功!因为 flock 不区分同一进程内的不同线程。
  • fcntl:同样会成功,因为 fcntl 的锁是基于进程的。

所以,多线程场景下,文件锁并不能完全替代线程间的互斥锁。我一般会配合 pthread_mutex 一起用——先拿线程锁,再拿文件锁。

最佳实践:多线程 + 多进程混合场景,用 pthread_mutex 保护线程内共享资源,用 fcntl 保护进程间文件访问。两层锁,各司其职。

知识体系图

文件锁知识体系 文件锁 flock fcntl lockf BSD标准 整个文件锁定 与fd绑定 使用简单 POSIX标准 区域锁定 与进程+inode绑定 支持NFS 基于fcntl 区域锁定 仅排他锁 接口友好 选择建议:简单场景用flock,精细控制用fcntl 多进程/多线程文件访问冲突解决

实际项目中的选择策略

我做了这么多年C开发,总结了一套选择策略:

  1. 单机、简单互斥:用 flock。代码少,不容易出错。
  2. 需要精细控制(比如只锁文件的一部分):用 fcntl。虽然代码多,但灵活。
  3. 跨网络文件系统(NFS):用 fcntllockfflock 在NFS上表现不稳定。
  4. 只需要排他锁,不想写复杂结构体:用 lockf。它就是个简化版。

一个小技巧:调试文件锁问题时,用 lsof 命令可以查看当前哪些进程持有文件锁。我排查死锁问题时,这招屡试不爽。

最后说一句:文件锁不是银弹。它解决的是「进程间」的文件访问冲突。如果你要解决「线程间」的共享数据冲突,还是老老实实用 pthread_mutex 吧。两种锁各管各的,别混为一谈。


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