文件锁:flock、fcntl、lockf。多进程/多线程文件访问冲突解决
文件锁这个话题,说实话,我早年做嵌入式开发时吃过不少亏。那时候多个进程同时写一个日志文件,结果日志内容互相穿插,读出来根本没法看。后来才意识到——文件锁不是可选项,而是多进程环境下的必需品。
今天咱们就把 flock、fcntl、lockf 这三兄弟掰扯清楚。它们都能解决文件访问冲突,但脾气秉性完全不同。
为什么需要文件锁?
你想想看,两个进程同时往一个文件里写数据,会发生什么?
- 数据交错: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 就对了。
多线程场景下的文件锁
多线程和多进程不太一样。同一进程内的多个线程共享文件描述符,所以用 flock 或 fcntl 加锁时,一个线程加锁,其他线程也能看到这把锁。
但有个问题:如果线程A加了锁,线程B在同一个文件描述符上尝试加锁,会怎么样?
flock:线程B会成功!因为flock不区分同一进程内的不同线程。fcntl:同样会成功,因为fcntl的锁是基于进程的。
所以,多线程场景下,文件锁并不能完全替代线程间的互斥锁。我一般会配合 pthread_mutex 一起用——先拿线程锁,再拿文件锁。
最佳实践:多线程 + 多进程混合场景,用 pthread_mutex 保护线程内共享资源,用 fcntl 保护进程间文件访问。两层锁,各司其职。
知识体系图
实际项目中的选择策略
我做了这么多年C开发,总结了一套选择策略:
- 单机、简单互斥:用
flock。代码少,不容易出错。 - 需要精细控制(比如只锁文件的一部分):用
fcntl。虽然代码多,但灵活。 - 跨网络文件系统(NFS):用
fcntl或lockf。flock在NFS上表现不稳定。 - 只需要排他锁,不想写复杂结构体:用
lockf。它就是个简化版。
一个小技巧:调试文件锁问题时,用 lsof 命令可以查看当前哪些进程持有文件锁。我排查死锁问题时,这招屡试不爽。
最后说一句:文件锁不是银弹。它解决的是「进程间」的文件访问冲突。如果你要解决「线程间」的共享数据冲突,还是老老实实用 pthread_mutex 吧。两种锁各管各的,别混为一谈。
公众号:蓝海资料掘金营,微信deep3321