我们在 Linux 环境中用 C 编程时,如果对文件读写,Linux 会自动给文件加锁嘛?以及怎么加文件锁?
task1: 验证Linux不会自动给文件加锁
先说结论,结论是不会
我写了一个这样的程序
#include <stdio.h>
#include <unistd.h>
int main() {
const char* pathname = "your_file_pathname.txt";
FILE* file = NULL;
int count = 100;
if(access(pathname, F_OK) == 0) {
file = fopen(pathname, "r+");
printf("open in r+ mode\n");
}
else {
file = fopen(pathname, "w+");
printf("open in w+ mode\n");
}
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
fseek(file, 0, SEEK_END); // 将文件指针移动到文件末尾
long file_size = ftell(file); // 获取文件大小
if (file_size == 0) {
fprintf(file, "0\n"); // 文件为空,写入0
fflush(file); // 刷新文件缓冲区,确保写入文件
}
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
int num;
fscanf(file, "%d", &num); // 读取文件中的整数
printf("num = %d\n", num);
sleep(2);
while(count--) {
num++; // 将整数加1
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
fprintf(file, "%d\n", num); // 将更新后的整数写回文件
fflush(file); // 刷新文件缓冲区,确保写入文件
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
fscanf(file, "%d", &num); // 读取文件中的整数
printf("num = %d\n", num);
sleep(2);
}
fclose(file); // 关闭文件
return 0;
}
上面这个程序会读取文件中的数字,然后给数字+1,再写回文件
这个程序里没有给文件加锁,我同时运行了 8 个这样的程序,最后的 result file 里的数字是 127,而非 800,说明 Linux 本身并不会给文件加锁
task2: 如何手动给文件加锁?
首先根据 ChatGPT,我们可以获得如下代码(经过部分注释和修改,可以根据注释理解源码):
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
// 打开文件
fd = open("file.txt", O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}
// 设置文件锁
// 我们定义了一个struct flock结构体来描述文件锁的属性,包括锁的类型、起始位置和长度。
struct flock lock;
lock.l_type = F_WRLCK; // 写锁
// short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
// __off_t l_start; /* Offset where the lock begins. */
// l_whence 和 l_start 是共同指定锁的起始位置的
lock.l_whence = SEEK_SET;
lock.l_start = 0;
// __off_t l_len; /* Size of the locked area; zero means until EOF. */
lock.l_len = 0; // 锁定整个文件
// 接下来,我们使用fcntl函数来获取文件锁,使用F_SETLKW标志表示在获取锁时阻塞进程,直到锁可用。
// # define F_SETLKW 7 /* Set record locking info (blocking). */
if (fcntl(fd, F_SETLKW, &lock) == -1) {
perror("fcntl");
exit(1);
}
// 在文件中写入数据 NOTE: 关键区域
// ...
// 在写入数据完成后,我们再次使用fcntl函数来释放文件锁,使用F_SETLK标志表示释放锁。
// # define F_RDLCK 0 /* Read lock. */
// # define F_WRLCK 1 /* Write lock. */
// # define F_UNLCK 2 /* Remove lock. */
// 根据手册来看,F_SETLKW 是阻塞式获取/释放锁,F_SETLK是非阻塞获取/释放锁
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("fcntl");
exit(1);
}
// 关闭文件
close(fd);
return 0;
}
根据手册阅读,如下:
只需要修改 lock.l_type = F_UNLCK; 就可以决定是上锁还是释放锁。F_SETLK 和 F_SETLKW 只是决定 阻塞/非阻塞 获取/释放 锁
我们做个实验看看,“写另外一个文件2,获取锁后不释放,文件1分别使用 阻塞/非阻塞 方式获取锁,看是否如手册所描述一般行为”
经过测试,当 Holding lock 的程序被强制退出时,它所持有的锁也会被强制释放
测试1:blocking 阻塞式获取锁
首先我们测试,先使用 norelease.c 文件获取锁,随后不退出
接着再使用 blocking.c 获取锁,可以发现会阻塞再这个地方
以下是 blocking.c 源码:
可以看到第36行的 printf 并没有被执行,blocking.c 验证完毕
测试2:nonblocking 非阻塞式获取锁
可以看到,非阻塞式获取锁确实是非阻塞的,它会让 fcntl() 调用返回 -1,从我们的代码来看,最终是执行了 31 行的 perror() 之后异常退出
task3: 再做一遍 task1 的实验,加上锁
首先把 task1 的 sleep 参数设置为 1,count 设置为 50,运行两个
最终文本文件中的整数是 56,确实出现了 race condition
现在加上锁,此时代码如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
struct flock lock;
int fd;
void init_lock() {
lock.l_whence = SEEK_SET; // 锁的起始位置是 SEEK_SET + 0
lock.l_start = 0;
lock.l_len = 0; // 锁定整个文件
}
void acquire_lock() {
lock.l_type = F_WRLCK; // 写锁
if (fcntl(fd, F_SETLKW, &lock) == -1) {
perror("fcntl");
exit(1);
}
}
// NOTE: 释放锁不需要阻塞,因为释放锁的必须拥有锁
// NOTE: 如果释放锁的进程没有拥有锁,那说明并发写错了
void release_lock() {
lock.l_type = F_UNLCK; // 释放锁
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("fcntl");
exit(1);
}
}
int main() {
const char* pathname = "your_file_pathname.txt";
FILE* file = NULL;
int count = 50;
if(access(pathname, F_OK) == 0) {
file = fopen(pathname, "r+");
printf("open in r+ mode\n");
}
else {
file = fopen(pathname, "w+");
printf("open in w+ mode\n");
}
if (file == NULL) {
printf("无法打开文件\n");
return 1;
}
// 获取文件描述符
fd = fileno(file);
// 初始化全局锁
init_lock();
// 关键区域 ------------ start
// 上锁
acquire_lock();
fseek(file, 0, SEEK_END); // 将文件指针移动到文件末尾
long file_size = ftell(file); // 获取文件大小
if (file_size == 0) {
fprintf(file, "0\n"); // 文件为空,写入0
fflush(file); // 刷新文件缓冲区,确保写入文件
}
// 开锁
release_lock();
// 关键区域 ------------ end
// 关键区域 ------------ start
// 上锁
acquire_lock();
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
int num;
fscanf(file, "%d", &num); // 读取文件中的整数
printf("num = %d\n", num);
sleep(1);
while(count--) {
num++; // 将整数加1
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
fprintf(file, "%d\n", num); // 将更新后的整数写回文件
fflush(file); // 刷新文件缓冲区,确保写入文件
// 开锁
release_lock();
// 上锁
acquire_lock();
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
fscanf(file, "%d", &num); // 读取文件中的整数
printf("num = %d\n", num);
sleep(1);
}
// 开锁
release_lock();
// 关键区域 ------------ end
fclose(file); // 关闭文件
return 0;
}
再次编译执行,发现最终文本文件的整数是 100,说明加锁确实有效防止了 race condition