1. flock函数
flock函数是专门用于实现对文件加锁的,与fcntl不同的是,flock系统调用最初是源自BSD的,而fcntl则是源自System V,flock是对整个文件进行加锁,而fcntl不仅可以对整个文件加锁,还可以对文件部分区域加锁,更加具有灵活性。
flock - apply or remove an advisory lock on an open file
从flock函数的语义上来看,flock提供的文件锁是一种“建议性”锁,并非强制使用。也就是说,一个进程对文件进行读写操作时可以忽略文件锁的限制,但是如果要保证文件中的数据不会出现问题,所有访问文件的进行都必须加锁。
函数原型:
#include <sys/file.h>
int flock(int fd , int operation);
返回值说明:成功返回0,失败返回-1并设置errno错误
参数fd:指定要加锁的文件描述符
参数operation:指定对文件锁的操作,即加锁或解锁。例如LOCK_SH(共享锁),LOCK_EX(互斥锁),LOCK_UN(解锁),LOCK_NB(非阻塞加锁)等。
LOCK_SH(共享锁):如果operation指定了LOCK_SH,当有多个进程对文件进行加锁都会成功,并实现对文件的读操作,相当于多线程里的读写锁。
LOCK_EX(互斥锁):同一时刻只能有一个线程能加锁成功,并对文件进行读写操作,相当于多线程里互斥量。
LOCK_NB(非阻塞加锁):LOCK_NB是以非阻塞方式进行加锁,可以和LOCK_EX或LOCK_SH配合使用,如果加锁失败不会阻塞,而是出错返回。
2. flock加锁类型的兼容性
当多个进程对文件进行加锁时,无论是以读、写、或读写哪种方式,都可以在对文件设置共享锁或互斥锁,使用不同类型的锁对文件加锁可能产生不同的情况。

以LOCK_SH方式加锁时,进程A和进程B都能加锁成功,都能成功操作文件,因为锁类型兼容。其他情况加锁都是不兼容的,只有第一个进程能加锁成功,也只有第一个进程能操作文件,后面其他加锁的进程将会阻塞,直到第一个进程释放锁,后面的进程才有可能会加锁成功。
3. flock加锁类型兼容实验
进程A
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(int argc , char *args[])
{
int lock_fd;
int ret;
int i = 0;
if(argc < 2){
puts("argc < 2");
exit(1);
}
//1 :共享锁
//2 : 互斥锁
int n = atoi(args[1]);
//打开文件
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加锁
if(n == 1){
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
perror("flock error :");
puts("A is lock LOCK_SH fail");
exit(-1);
}
puts("A is lock LOCK_SH succesful");
}else{
//以LOCK_EX方式加锁
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
perror("flock error :");
puts("A is lock LOCK_EX fail");
exit(-1);
}
puts("A is lock LOCK_EX succesful");
}
//加锁成功后,打印hello
for(i = 0; i < 5; i++){
puts("hello");
sleep(2);
}
//解锁
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
if(n == 1){
puts("A is unlock LOCK_SH");
}else{
puts("A is unlock LOCK_EX");
}
return 0;
}
进程B
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(int argc , char *args[])
{
int lock_fd;
int ret;
int i = 0;
if(argc < 2){
puts("argc < 2");
exit(1);
}
//1 :共享锁
//2 : 互斥锁
int n = atoi(args[1]);
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加锁
if(n == 1){
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
perror("flock error :");
puts("B is lock LOCK_SH fail");
exit(-1);
}
puts("B is lock LOCK_SH succesful");
}else{
//以LOCK_EX方式加锁
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
perror("flock error :");
puts("B is lock LOCK_EX fail");
exit(-1);
}
puts("B is lock LOCK_EX succesful");
}
//加锁成功后打印world
for(i = 0; i < 5; i++){
puts("world");
sleep(2);
}
//解锁
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
if(n == 1){
puts("B is unlock LOCK_SH");
}else{
puts("B is unlock LOCK_EX");
}
return 0;
}
进程A以LOCK_SH(共享锁)方式加锁,进程B以LOCK_SH(共享锁)方式加锁:
多个进程以LOCK_SH(共享锁)方式对文件加锁都能成功
进程A以LOCK_SH(共享锁)方式加锁,进程B以LOCK_EX(互斥锁)方式加锁:
进程A和进程B两个进程加锁不兼容,只有第一个加锁的进程才会成功,进程B加锁失败会阻塞,直到进程A释放锁为止。
4. 指定LOCK_NB非阻塞加锁
在3小节的示例程序中,多个进程对文件进行加锁时,如果锁的类型不兼容,只有第一个进程会加锁成功,其他进程可能会因此阻塞。
例如进程A以LOCK_SH方式加锁成功后,进程B调用flock以LOCK_EX方式加锁时会阻塞,然后等待进程A释放锁,如果指定了LOCK_NB选项,那么进程B不会阻塞,而是出错返回并设置errno为EWOULDBLOCK错误。
现在对进程B的代码做以下修改:
//以LOCK_EX方式加锁,并指定LOCK_NB非阻塞
ret = flock(lock_fd , LOCK_EX | LOCK_NB);
if(ret < 0){
//进一步判断是否为EWOULDBLOCK错误
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
perror("flock error :");
puts("B is lock LOCK_EX fail");
exit(-1);
}
进程A以LOCK_SH(共享锁)方式加锁,进程B以LOCK_EX(互斥锁)方式加锁,程序执行结果如下:
当进程B在指定了LOCK_NB选项,以非阻塞方式加锁失败后不会阻塞,而是立马出错返回。
5. flock加锁可能出现的问题
如果一个进程调用flock对文件以LOCK_SH方式加锁时,接着再次调用flock以LOCK_EX方式加锁会将原来的共享锁转换为一个互斥锁,但是这个转换过程并不是原子操作。因为在转换过程中会删除先前持有的共享锁,然后再创建一个新的互斥锁,但是在这个转换过程中可能会让另一个阻塞的进程加锁成功。
举个栗子:
A和B两个进程对同一文件进行加锁,B进程以LOCK_SH方式加锁成功,A进程以LOCK_EX方式加锁默认情况下会阻塞,此时B进程再次调用flock以LOCK_EX方式加锁就会将原来的共享锁转换为一个互斥锁,但在这个转换过程中可能会让A进程加锁成功(可能会发生)。
如果发生了这种情况,而且B进程没有指定LOCK_NB的话,转换过程将会阻塞,如果指定了LOCK_NB,那么转换过程将会失败并丢失原来已持有的锁(在最初的BSD flock实现和大多数unix实现会出现这样的情况)。
示例程序如下:
A进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(void)
{
int lock_fd;
int ret;
int i = 0;
//打开文件
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_EX,以阻塞方式加锁
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
puts("A is lock LOCK_EX fail");
exit(-1);
}
puts("A is lock LOCK_EX succesful");
for(i = 0; i < 5; i++){
puts("AAAAAAAAA");
sleep(2);
}
//解锁
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
puts("A is unlock LOCK_UN succesful");
return 0;
}
B进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
int main(void)
{
int lock_fd;
int ret;
int i = 0;
//打开文件
lock_fd = open("test.txt" , O_RDONLY | O_CREAT , 0664);
if(lock_fd < 0){
perror("open error: ");
exit(1);
}
//以LOCK_SH方式加锁
ret = flock(lock_fd , LOCK_SH);
if(ret < 0){
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
puts("B is lock LOCK_SH fail");
exit(-1);
}
puts("B is lock LOCK_SH succesful");
//等待进程A加锁,然后阻塞
sleep(3);
//在转换过程中,可能会让阻塞的进程A加锁成功
ret = flock(lock_fd , LOCK_EX);
if(ret < 0){
if(errno == EWOULDBLOCK){
puts("errno is return EWOULDBLOCK");
exit(1);
}
puts("B is lock LOCK_EX fail");
exit(-1);
}
puts("------lock is change LOCK_EX--------");
puts("B is lock LOCK_EX succesful");
for(i = 0; i < 5; i++){
puts("BBBBBBBBB");
sleep(2);
}
//解锁
ret = flock(lock_fd , LOCK_UN);
if(ret < 0){
perror("flock error:" );
}
puts("B is unlock LOCK_EX succesful");
return 0;
}
程序执行结果:
从程序的执行结果来看,B进程在转换过程中并没有发生之前所说的这种情况。
6. flock加锁和C标准库I/O
由于C标准库I/O调用会在用户空间缓存,因此在使用flock加锁中调用C标准库I/O函数可能出现的问题:
- 在加锁之前,可能用户空间缓存已经被填满
- 锁被删除之后,可能会刷新户空间缓存
解决的办法就是:
- 使用read或write系统调用取代C标准库I/O调用
- 在对文件加锁后立即刷新缓存,释放锁之前再次刷新缓存
7. flock的限制
1. flock是一种“建议性”锁,可能会导致其他进程不加锁访问文件。
2. 很多NFS(Network File System)即网络文件系统并不支持flock文件加锁
3. flock只能对整个文件加锁,加锁的粒度较大,不利于协作进程之间的并发。例如多个进程访问文件的各个不同部分,这种不必要的加锁会阻碍进程的并发操作。
由于历史问题,linux NFS并不支持flock锁,所以综合以上这些原因,在linux中并不推荐使用flock对文件加锁。而是使用fcntl对文件加锁。