一、文件锁:多进程协作的“交通规则”
大家好,我是你们的嵌入式老兵。今天咱们聊聊文件锁。
你想想看,多个进程同时读写同一个文件,会发生什么?数据错乱、内容覆盖、甚至文件损坏。我早年做嵌入式数据采集系统时,就吃过这个亏——两个进程同时写日志文件,结果日志内容像被猫踩过键盘一样,乱七八糟。
文件锁,说白了就是给文件加把锁。谁拿到锁,谁才能操作。没拿到的,要么等着,要么绕道走。这就是进程间协作的“交通规则”。
核心概念:文件锁是一种进程间同步机制,用于协调多个进程对同一文件的并发访问,防止数据竞争和不一致。
1.1 文件锁的分类:读锁 vs 写锁
文件锁分两种,理解起来很简单:
- 读锁(共享锁):多个进程可以同时加读锁。大家只读不写,互不干扰。
- 写锁(排他锁):只有一个进程能加写锁。写的时候,别人不能读也不能写。
我习惯这么记:读锁是“共享单车”,大家都能骑;写锁是“私家车”,只有车主能用。
| 锁类型 | 别称 | 兼容性 | 典型场景 |
|---|---|---|---|
| 读锁 | 共享锁 | 可被多个读锁兼容 | 配置文件读取、日志查看 |
| 写锁 | 排他锁 | 与任何锁都不兼容 | 数据更新、文件写入 |
1.2 flock 函数:简单粗暴的文件锁
flock 是 Linux 下最直接的文件锁接口。它的用法很简单:
#include <sys/file.h>
int fd = open("data.txt", O_RDWR);
// 加写锁(阻塞模式)
flock(fd, LOCK_EX);
// ... 读写操作 ...
// 解锁
flock(fd, LOCK_UN);
常用的锁操作宏:
LOCK_SH:加读锁LOCK_EX:加写锁LOCK_UN:解锁LOCK_NB:非阻塞模式(与上面三个按位或)
我的经验:flock 是基于整个文件的,不能锁文件中的某一段。如果你只需要锁文件的一部分,得用 fcntl。
1.3 fcntl 函数:更精细的“手术刀”
fcntl 比 flock 强大得多。它可以锁定文件的任意区域,粒度更细。
#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 = getpid();
// 加锁(阻塞模式)
fcntl(fd, F_SETLKW, &lock);
// ... 操作 ...
// 解锁
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
这里要注意 l_type 的取值:
F_RDLCK:读锁F_WRLCK:写锁F_UNLCK:解锁
命令参数也有讲究:
F_SETLK:非阻塞加锁F_SETLKW:阻塞加锁(W 表示 Wait)F_GETLK:测试锁状态
避坑指南:我曾经在项目中混用 flock 和 fcntl,结果发现它们互不感知!flock 和 fcntl 是两套独立的锁机制,不要混用。选一套用到底。
1.4 阻塞与非阻塞:等还是不等?
加锁时有两种模式:
- 阻塞模式:拿不到锁就挂起等待,直到锁可用。适合“我必须拿到锁才能干活”的场景。
- 非阻塞模式:拿不到锁立即返回错误。适合“有锁就干,没锁就做别的”的场景。
flock 的非阻塞用法:
if (flock(fd, LOCK_EX | LOCK_NB) == -1) {
if (errno == EWOULDBLOCK) {
printf("锁被占用了,我先干点别的\n");
}
}
fcntl 的非阻塞用法:
if (fcntl(fd, F_SETLK, &lock) == -1) {
if (errno == EACCES || errno == EAGAIN) {
printf("锁冲突,跳过\n");
}
}
1.5 文件锁的继承与释放
这里有几个关键点,我当年踩过坑:
- 继承问题:
fork()创建的子进程会继承父进程的文件锁。但exec()系列函数会关闭锁(除非设置了 close-on-exec)。 - 释放时机:进程退出时,所有文件锁自动释放。但别依赖这个,显式解锁才是好习惯。
- dup 与锁:通过
dup()复制的文件描述符共享同一个锁。关闭其中一个 fd 不会释放锁,必须关闭所有引用。
重要规则:文件锁与进程关联,而非与文件描述符关联。同一个进程多次打开同一文件,每个 fd 可以独立加锁,但彼此会冲突。
1.6 应用场景:什么时候用文件锁?
我总结了几种典型场景:
- 日志文件写入:多个进程写同一个日志文件,用写锁保证日志不交错。
- 配置文件更新:读配置时加读锁,写配置时加写锁,避免读到半成品。
- 数据库文件操作:简单的键值对数据库,用文件锁保证事务一致性。
- 单实例守护进程:用文件锁防止同一个程序启动多次。
1.7 综合案例:多进程日志写入器
来看一个完整的例子。假设我们有三个进程同时写日志,用文件锁保证不乱:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <time.h>
void write_log(const char *msg) {
int fd = open("app.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
perror("open");
return;
}
// 加写锁,阻塞等待
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
close(fd);
return;
}
// 写入日志
time_t now = time(NULL);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&now));
dprintf(fd, "[%s] [PID=%d] %s\n", time_str, getpid(), msg);
// 模拟耗时操作
usleep(100000); // 100ms
// 解锁
flock(fd, LOCK_UN);
close(fd);
}
int main() {
for (int i = 0; i < 5; i++) {
write_log("这是一条日志记录");
sleep(1);
}
return 0;
}
运行三个实例:
./log_writer &
./log_writer &
./log_writer &
你会发现日志文件内容整齐有序,每条日志完整连续,不会出现交叉。
我的建议:实际项目中,日志库通常自带锁机制。但理解底层原理,能帮你排查那些“日志丢了”或“日志乱了”的诡异问题。
知识体系总览
下面这张图帮你理清文件锁的核心脉络:
文件锁是进程间通信的基石之一。它不像管道或消息队列那样传输数据,但它保证了数据访问的安全性。嗯,说白了,它就是那个在后台默默维护秩序的“保安”。
下回当你遇到多进程写文件的问题时,别急着上数据库,先想想文件锁能不能搞定。很多时候,一把简单的锁就能解决大问题。