文件锁基础概念
当多个进程同时访问同一个文件时,如果不加控制,可能导致数据不一致或文件损坏。文件锁就是操作系统提供的用于协调并发文件访问的机制。Linux 系统提供了两种主要的文件锁接口:flock() 和 fcntl(),它们在锁粒度、语义和适用场景上各有不同。
文件锁的分类
从锁类型来看,文件锁分为两种:
- 共享锁(读锁):多个进程可以同时持有共享锁,用于读取文件,互不干扰
- 排他锁(写锁):同一时刻只能有一个进程持有排他锁,且此时不能有共享锁存在,用于写入文件
从锁粒度来看,又可分为整文件锁和记录锁(字节范围锁)。整文件锁对整个文件加锁,记录锁可以对文件的任意字节区间加锁。
flock 文件锁
flock() 是 BSD 系统引入的文件锁接口,语义简洁,对整个文件加锁。它只区分共享锁和排他锁,不支持记录级别的锁定。在 Linux 上,flock() 锁与文件描述符和文件表项关联,而非进程。
flock 基本用法
flock() 的操作类型包括:LOCK_SH(共享锁)、LOCK_EX(排他锁)、LOCK_UN(解锁),以及 LOCK_NB(非阻塞标志)。
#include <sys/file.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
int fd = open("data.txt", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
/* 获取排他锁 */
if (flock(fd, LOCK_EX) < 0) {
perror("flock LOCK_EX");
close(fd);
return 1;
}
/* 写入数据 */
const char *data = "Protected write operation\n";
write(fd, data, strlen(data));
printf("Write completed, releasing lock...\n");
/* 释放锁 */
flock(fd, LOCK_UN);
close(fd);
return 0;
}
非阻塞加锁模式
默认情况下 flock() 是阻塞的,如果锁已被其他进程持有,调用进程会一直等待。加上 LOCK_NB 标志后,如果无法立即获取锁,flock() 会立即返回 -1 并设置 errno 为 EWOULDBLOCK。
#include <sys/file.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main(void) {
int fd = open("data.txt", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
/* 尝试非阻塞获取排他锁 */
if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
if (errno == EWOULDBLOCK) {
printf("File is locked by another process\n");
} else {
perror("flock");
}
close(fd);
return 1;
}
printf("Lock acquired successfully\n");
/* ... 临界区操作 ... */
flock(fd, LOCK_UN);
close(fd);
return 0;
}
fcntl 记录锁
fcntl() 记录锁是 POSIX 标准定义的文件锁机制,可以对文件的任意字节范围加锁,因此称为记录锁或字节范围锁。与 flock() 不同,fcntl() 锁与 (进程ID, 文件inode) 关联,而非文件描述符。
fcntl 锁的结构体与操作
fcntl() 记录锁使用 struct flock 描述锁的属性,包括锁类型、起始偏移量、长度和锁的持有进程。
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/* 对文件的指定范围加排他锁 */
int lock_region(int fd, off_t offset, off_t len) {
struct flock fl;
memset(&fl, 0, sizeof(fl));
fl.l_type = F_WRLCK; /* 排他锁 */
fl.l_whence = SEEK_SET; /* 偏移基准 */
fl.l_start = offset; /* 起始偏移 */
fl.l_len = len; /* 锁定长度,0 表示到文件末尾 */
if (fcntl(fd, F_SETLKW, &fl) < 0) {
perror("fcntl F_SETLKW");
return -1;
}
return 0;
}
/* 解锁指定范围 */
int unlock_region(int fd, off_t offset, off_t len) {
struct flock fl;
memset(&fl, 0, sizeof(fl));
fl.l_type = F_UNLCK;
fl.l_whence = SEEK_SET;
fl.l_start = offset;
fl.l_len = len;
if (fcntl(fd, F_SETLKW, &fl) < 0) {
perror("fcntl unlock");
return -1;
}
return 0;
}
int main(void) {
int fd = open("records.dat", O_RDWR | O_CREAT, 0644);
if (fd < 0) { perror("open"); return 1; }
/* 锁定第 100~199 字节 */
if (lock_region(fd, 100, 100) == 0) {
printf("Locked region [100, 200)\n");
/* 在临界区内执行文件操作 */
lseek(fd, 100, SEEK_SET);
const char *data = "Updated record";
write(fd, data, strlen(data));
unlock_region(fd, 100, 100);
printf("Unlocked region\n");
}
close(fd);
return 0;
}
检测锁冲突
使用 F_GETLK 命令可以检测指定的锁是否会被其他进程的锁阻塞。如果存在冲突,fcntl() 会将冲突锁的信息写入 flock 结构体并返回 -1。
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
int fd = open("records.dat", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
struct flock fl;
memset(&fl, 0, sizeof(fl));
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0; /* 整个文件 */
/* 检测是否能获取写锁 */
if (fcntl(fd, F_GETLK, &fl) < 0) {
perror("fcntl F_GETLK");
close(fd);
return 1;
}
if (fl.l_type == F_UNLCK) {
printf("No conflicting lock, can proceed\n");
} else {
printf("Lock held by pid %d, type %s, "
"region [%ld, %ld)\n",
fl.l_pid,
fl.l_type == F_RDLCK ? "READ" : "WRITE",
(long)fl.l_start,
(long)(fl.l_start + fl.l_len));
}
close(fd);
return 0;
}
flock 与 fcntl 的区别
两种文件锁机制在实现细节和行为上存在显著差异,理解这些差异对于正确使用文件锁至关重要。
核心差异对比
- 锁粒度:
flock()只能对整个文件加锁;fcntl()可以对文件的任意字节区间加锁 - 锁关联对象:
flock()锁与文件表项关联,通过 dup 复制的文件描述符共享同一把锁;fcntl()锁与 (pid, inode) 关联,进程内所有文件描述符共享同一把锁 - 锁释放时机:
flock()在关闭所有引用同一文件表项的文件描述符时释放;fcntl()在进程终止或显式解锁时释放 - 子进程继承:
flock()锁不会被 fork 继承(但 dup 后共享);fcntl()锁属于进程,fork 后子进程不继承 - 标准兼容:
flock()来自 BSD,fcntl()属于 POSIX 标准,可移植性更好
选择建议
如果你的场景只需要对整个文件加锁,flock() 更简单直观;如果需要对文件的部分区域加锁,或者需要跨 NFS 使用,fcntl() 是更合适的选择。在 NFS 环境下,fcntl() 的记录锁通过 lock daemon 可以在网络文件系统上正常工作,而 flock() 通常是本地有效的。
实战:并发安全的日志写入
在实际项目中,多个进程向同一个日志文件写入数据是常见的并发场景。下面展示一个使用 flock() 实现的并发安全日志模块。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
typedef struct {
int fd;
char path[256];
} logger_t;
int logger_open(logger_t *log, const char *path) {
log->fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (log->fd < 0) {
perror("logger_open");
return -1;
}
strncpy(log->path, path, sizeof(log->path) - 1);
return 0;
}
void logger_write(logger_t *log, const char *level, const char *msg) {
/* 获取排他锁,保证写入的原子性 */
if (flock(log->fd, LOCK_EX) < 0) {
perror("flock");
return;
}
time_t now = time(NULL);
struct tm *t = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp),
"%Y-%m-%d %H:%M:%S", t);
char line[512];
int n = snprintf(line, sizeof(line),
"[%s] [%s] %s\n", timestamp, level, msg);
write(log->fd, line, n);
flock(log->fd, LOCK_UN);
}
void logger_close(logger_t *log) {
if (log->fd >= 0) {
close(log->fd);
log->fd = -1;
}
}
int main(void) {
logger_t log;
if (logger_open(&log, "app.log") < 0)
return 1;
logger_write(&log, "INFO", "Application started");
logger_write(&log, "WARN", "Low memory detected");
logger_write(&log, "ERROR", "Connection timeout");
logger_close(&log);
return 0;
}
总结
本文详细介绍了 C 语言中两种主要的文件锁机制及其实际应用:
- flock():接口简洁,适用于整文件加锁场景,锁与文件描述符关联,同一文件表项的描述符共享锁
- fcntl() 记录锁:支持字节范围锁定,POSIX 标准兼容,锁与进程关联,适合需要精细控制的场景
- 两者差异:锁粒度、关联对象、释放时机、NFS 支持等方面存在本质区别,需根据场景选择
- 实战应用:并发日志写入是文件锁的典型用例,结合 O_APPEND 标志和文件锁可保证写入原子性
文件锁虽然是一种相对简单的并发控制手段,但在多进程文件操作场景中不可或缺。正确理解 flock() 和 fcntl() 的语义差异,避免混用两种锁(它们互不感知,可能导致死锁),是编写健壮并发程序的关键。在实际开发中,建议根据项目需求选择其中一种并统一使用,确保并发控制逻辑的一致性和可维护性。