22、文件锁机制:文件锁的概念、flock函数、fcntl函数、多进程文件同步
文件锁这个东西,说白了就是给文件加把锁。
你想想看,多个进程同时读写一个文件,会出什么事?数据错乱、内容覆盖、甚至文件损坏。我在嵌入式项目里就吃过这个亏——两个进程同时写日志,结果日志文件里全是乱码,排查了半天才发现是同步问题。
嗯,今天我们就来聊聊文件锁。我会从概念讲起,然后带你看看两个最常用的锁函数:flock 和 fcntl,最后聊聊多进程怎么用锁做文件同步。
文件锁的概念
文件锁是一种同步机制。它让多个进程在访问同一个文件时,能按顺序来,而不是一窝蜂往上冲。
锁分两种:
- 共享锁(读锁):多个进程可以同时加读锁。大家都能读,但不能写。
- 独占锁(写锁):只有一个进程能加写锁。加了写锁,别人既不能读也不能写。
我习惯把共享锁叫「看门锁」——只让看,不让动。独占锁叫「施工锁」——锁上门,里面在施工,谁也别进来。
核心原则:读锁可共享,写锁必独占。
flock 函数
flock 是 BSD 系统提供的文件锁函数。它简单粗暴,用起来很直接。
#include <sys/file.h>
int flock(int fd, int operation);
参数 operation 有几种:
| 宏 | 含义 |
|---|---|
| LOCK_SH | 加共享锁 |
| LOCK_EX | 加独占锁 |
| LOCK_UN | 解锁 |
| LOCK_NB | 非阻塞模式(常与上面组合使用) |
举个例子:
int fd = open("data.txt", O_RDWR);
if (fd == -1) {
perror("open");
return -1;
}
// 加独占锁
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
close(fd);
return -1;
}
// 写文件...
write(fd, "hello", 5);
// 解锁
flock(fd, LOCK_UN);
close(fd);
我的经验:flock 有个坑——它锁的是文件描述符,不是文件本身。如果你用 dup 复制了 fd,两个 fd 共享同一个锁。我曾经因为这个踩过坑,调试了一下午才发现。
fcntl 函数
fcntl 是 POSIX 标准提供的文件锁函数。它比 flock 更强大,也更复杂。
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);
struct flock 结构体长这样:
struct flock {
short l_type; // F_RDLCK(读锁), F_WRLCK(写锁), F_UNLCK(解锁)
short l_whence; // SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start; // 起始偏移
off_t l_len; // 长度(0 表示到文件末尾)
pid_t l_pid; // 持有锁的进程 ID(仅用于 F_GETLK)
};
fcntl 支持三种命令:
F_SETLK:设置锁(非阻塞)F_SETLKW:设置锁(阻塞)F_GETLK:获取锁信息
看个例子:
struct flock lock;
lock.l_type = F_WRLCK; // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; // 锁整个文件
int fd = open("data.txt", O_RDWR);
if (fcntl(fd, F_SETLKW, &lock) == -1) {
perror("fcntl");
close(fd);
return -1;
}
// 写文件...
write(fd, "world", 5);
// 解锁
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
close(fd);
注意:fcntl 锁与进程关联。如果进程退出,锁会自动释放。但如果你 fork 了子进程,子进程不会继承父进程的锁——这一点和 flock 不同。
flock 与 fcntl 的区别
| 特性 | flock | fcntl |
|---|---|---|
| 标准 | BSD | POSIX |
| 锁粒度 | 整个文件 | 文件区域(可部分加锁) |
| 与 fd 关系 | 锁与 fd 关联 | 锁与进程关联 |
| fork 后行为 | 子进程继承锁 | 子进程不继承锁 |
| NFS 支持 | 部分支持 | 支持 |
我个人习惯:简单场景用 flock,复杂场景用 fcntl。比如只需要锁整个文件,flock 一行搞定。如果需要锁文件的一部分,那就得上 fcntl。
多进程文件同步
多进程同步,说白了就是让多个进程有序地访问文件。没有锁,就是一片混乱。
我画了一张图,帮你理解多进程文件锁的流程:
实际项目中,我常用这种模式:
// 进程 A
int fd = open("shared.dat", O_RDWR | O_CREAT, 0644);
flock(fd, LOCK_EX); // 等锁
// 读写文件...
flock(fd, LOCK_UN);
close(fd);
// 进程 B
int fd = open("shared.dat", O_RDWR);
flock(fd, LOCK_EX); // 等锁(如果 A 没释放,这里会阻塞)
// 读写文件...
flock(fd, LOCK_UN);
close(fd);
避坑指南:我曾经在项目中用 flock 做日志同步,结果发现进程意外崩溃时,锁没有释放。后来加了信号处理,在 SIGTERM 和 SIGINT 里主动解锁。嗯,这一点很重要——锁一定要在异常路径上也释放掉。
实际应用场景
- 日志文件:多个进程写同一个日志文件,用写锁保证日志不乱。
- 配置文件:读配置时加读锁,写配置时加写锁。
- 数据库文件:简单的键值存储,用文件锁做并发控制。
- 锁文件:用
/var/run/app.pid这种锁文件,防止程序重复启动。
最后说一句:文件锁不是万能的。如果你需要高性能的并发控制,还是得上数据库或者专门的同步机制。但对于嵌入式系统、简单工具脚本来说,文件锁够用了。
总结:flock 简单,fcntl 灵活。选哪个,看你的场景。记住一点——加锁要成对,解锁要及时。