文件锁定
当多个进程需要同时操作一个文件的时候,为了互相协调,防止出现文件不一致的情况,就需要使用系统提供的文件锁定特性。
创建锁文件
创建锁文件的方式类似操作系统中的二进制信号量机制。这个锁文件是一个空文件,它的作用仅仅是作为一个标志,表示当前有一个进程正在使用文件。
注意这种机制只是一种建议锁,而不是强制锁。一个应用程序在明知道锁文件存在(有一个进程正在访问文件)的时候还是可以强行访问文件,这种行为不会引起程序异常,但是会使文件混乱。这种进程之间的协调需要程序员来完成。
因为这个锁文件是用来控制进程之间协调的,所以在创建这个文件的时候必须使用原子操作。这个行为可以通过open
系统调用接受O_CREAT | O_EXCL
参数实现。
下面是一个利用锁文件实现进程之间协调的例子:
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
const char * lock_file = "LOCK.test";
int main(void){
int file_desc; //表示锁文件是否创建成功
int tries = 10; //让进程尝试10次
while (tries--){
file_desc = open(lock_file, O_RDWR | O_CREAT | O_EXCL, 0444);
if(file_desc == -1){
printf("%d - Lock already exists\n", getpid());
sleep(3);
}else{
//临界区
printf("%d - I have exclusive access\n", getpid());
sleep(1);
//删除锁文件以退出临界区
(void)close(file_desc);
(void)unlink(lock_file);
sleep(2);
}
}
return 0;
}
使用Linux命令gcc test.c -o a
和./a & ./a
来执行该程序的两个副本,让两个程序同时运行。
区域锁定
用创建锁文件的方法可以让一个进程独占整个文件。但是有些时候不能这样实现,有些时候一个进程可能只需要对文件的一个部分进行访问,而其他没有访问的部分可以让其他的进程访问。这就是区域锁定实现的效果。
这种方式通过fcntl
系统调用来实现,它对一个打开的文件描述符进行操作,并能根据command
参数的设置完成不同的任务,其中command
可以是:
- F_GETLK: 查询当前文件的某一区域是否有锁,该区域由
flock
结构体指定,如果该区域有锁,函数就会修改flock
结构体中的相关信息,将它修改成当前区域的锁信息。并且该函数执行成功之后,会返回一个非-1
的值,而失败则返回-1
。如果函数执行成功,就可以通过检查函数执行前后flock
结构体是否被修改来判断当前区域是否有锁。通常比较方便的做法是检查flock
结构体中l_pid
成员的值。 - F_SETLK:向文件的某一区域加锁,锁的信息由
flock
结构体提。如果加锁成功,返回一个非-1
的值,加锁失败则返回-1
。 - F_SETLKW:该命令跟
F_SETLK
一样,不同的是如果它加锁失败,就会一直等待,直到加锁成功才会使函数返回。
当使用这些选项的时候,fcntl
的第三个参数必须是一个指向flock
结构的指针,所以实际的函数原型为:
int fcntl(int fildes, int command, struck flock * flock_structure);
其中flock
结构的成员依赖具体实现,但是至少包含以下成员:
- short l_type:锁的类型,可以是
F_RDLCK
共享锁(或读锁),F_UNLCK
解锁和F_WRLCK
独占锁(或写锁)。 - short l_whence:可以是
SEEK_SET
文件开头,SEEK_CUR
当前位置和SEEL_END
文件结尾。 - off_t l_start:锁定区域的第一个字节位置。
- off_t l_len:锁定区域的长度。
- pid_t l_pid:当前持有锁的进程。
读写加锁文件
当我们对加锁之后的文件进行读写的时候,必须要使用系统调用的read
和write
来进行读写,而不能使用C语言库中更高级的fread
和fwrite
。因为在C语言库中,为了提高读写效率,减少I/O操作,设置了缓存机制。当你读了文件中的100KB的内容时,函数库可能会利用这一次I/O的机会读取200KB的内容,以减少I/O操作,提高运行效率。同时,对于写操作,调用了fwrite
之后,函数库并不会立即使用I/O操作将数据写入文件,而是将其写入缓冲区,当缓冲区的内容达到一定数量之后,才会执行I/O操作,将多次调用fwrite
写入的文件一次性全写入文件。
这种机制确实可以减少I/O操作执行的次数,但是对于加锁的文件,这样就会产生问题,导致文件不一致。
下面是一个例子:
该程序为文件的两个区域加锁
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
const char * test_file = "test_lock";
int main(void){
int file_desc; // 用于存储打开的文件描述符
int byte_count; // 用来计算已经写入的字节数
char * byte_to_write = "A";
// 用于描述锁信息的结构体
struct flock region_1;
struct flock region_2;
int res; // 用来存储加锁函数执行的结果
// 打开一个文件描述符
file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if(!file_desc){
fprintf(stderr, "Can not open file %s for read / write\n", test_file);
return 1;
}
// 给文件添加一些数据
for(byte_count = 0; byte_count < 100; byte_count++){
write(file_desc, byte_to_write, 1);
}
// 把文件的10-30字节设为区域1,并在上面设置共享锁
region_1.l_type = F_RDLCK;
region_1.l_whence = SEEK_SET;
region_1.l_start = 10;
region_1.l_len = 20;
// 把文件的40-50字节设为区域2,并在上面设置独占锁
region_2.l_type = F_WRLCK;
region_2.l_whence = SEEK_SET;
region_2.l_start = 40;
region_2.l_len = 10;
// 锁定文件
printf("Process %d locking file\n", getpid());
res = fcntl(file_desc, F_SETLK, ®ion_1);
if(res == -1) fprintf(stderr, "Failed to lock region1\n");
res = fcntl(file_desc, F_SETLK, ®ion_2);
if(res == -1) fprintf(stderr, "Failed to lock region1\n");
// 加锁之后睡眠一会
sleep(60);
// 睡眠结束之后关闭文件
printf("Process %d closing file\n", getpid());
close(file_desc);
return 0;
}
在为文件加上锁之后,用下面的程序来对整个文件的各个区域进行锁查询:
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
const char * test_file = "test_lock";
#define SIZE_TO_TRY 5
void show_lock_info(struct flock * to_show);
int main(void){
int file_desc;
int res;
struct flock region_to_test;
int start_byte;
//打开一个文件描述符
file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if(!file_desc){
fprintf(stderr, "Unable to open file $s for read / write\n", test_file);
return 1;
}
//测试锁
for(start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY){
//设置希望测试的文件区域
region_to_test.l_type = F_WRLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
printf("Testing F_WRLCK on region from %d to %d\n",start_byte, start_byte + SIZE_TO_TRY);
//测试文件上的锁
res = fcntl(file_desc, F_GETLK, & region_to_test);
if(res == -1){
fprintf(stderr, "F_GETLK failed\n");
return 1;
}
if(region_to_test.l_pid != -1){
printf("Lock would fail. F_GETLK returned:\n");
show_lock_info(®ion_to_test);
}else{
printf("F_WRLCK - Lock would secceed\n");
}
//重新设置struct flock结构体的值,以便后续使用
printf("Now testing F_RDLCK on region from %d to %d\n", start_byte, start_byte + SIZE_TO_TRY);
region_to_test.l_type = F_WRLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
//这次用共享锁(读锁)测试文件
res = fcntl(file_desc, F_GETLK , ®ion_to_test);
if(res == -1){
fprintf(stderr, "F_GETLK failed\n");
return 1;
}
if(region_to_test.l_pid != -1){
printf("Lock would fail. F_GETLK returned:\n");
show_lock_info(®ion_to_test);
}else{
printf("F_RDLCK - Lock would secceed\n");
}
}
//测试完毕,关闭文件
close(file_desc);
return 0;
}
void show_lock_info(struct flock * to_show){
printf("\tl_type %d ", to_show->l_type);
printf("l_whence %d ", to_show->l_whence);
printf("l_start %d ", to_show->l_start);
printf("l_len %d ", to_show->l_len);
printf("l_pid %d ", to_show->l_pid);
}
使用Linux命令编译文件
gcc test_lock.c -o test
gcc set_lock.c -o set
先让set
程序后台运行:./set &
再让test
程序查询锁:./test