42-使用flock文件锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82932036

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函数可能出现的问题:

  1. 在加锁之前,可能用户空间缓存已经被填满
  2. 锁被删除之后,可能会刷新户空间缓存

解决的办法就是:

  1. 使用read或write系统调用取代C标准库I/O调用
  2. 在对文件加锁后立即刷新缓存,释放锁之前再次刷新缓存

7. flock的限制

 

1. flock是一种“建议性”锁,可能会导致其他进程不加锁访问文件。

2. 很多NFS(Network File System)即网络文件系统并不支持flock文件加锁

3. flock只能对整个文件加锁,加锁的粒度较大,不利于协作进程之间的并发。例如多个进程访问文件的各个不同部分,这种不必要的加锁会阻碍进程的并发操作。

由于历史问题,linux NFS并不支持flock锁,所以综合以上这些原因,在linux中并不推荐使用flock对文件加锁。而是使用fcntl对文件加锁。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82932036
今日推荐