【Linux高级 I/O(6)】初识文件锁—— flock()方法(附代码示例)

        想象一下,当两个人同时编辑磁盘中同一份文件时,其后果将会如何呢?在 Linux 系统中,该文件的最后状态通常取决于写该文件的最后一个进程。多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O 操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!

        对于有些应用程序,进程有时需要确保只有自己能够对某一文件进行 I/O 操作,在这段时间不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。  

        前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制,锁机制实现用于对共享资源的访问进行保护;只不过互斥锁、自旋锁、读写锁与文件锁的应用场景不一样,互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问保护,做到线程同步。

        而文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性,linux 通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。 譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程) 可以对其进行读写操作。

        一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底, 文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态。

        文件锁的分类

        文件锁可以分为建议性锁和强制性锁两种:

        ⚫ 建议性锁

        建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件,这是建议性锁的一种用法;但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以访问的;如果是这样,那么建议性锁就没有起到任何作用,如果要使得建议性锁起作用, 那么大家就要遵守协议,访问文件之前先对文件上锁。这就好比交通信号灯,规定红灯不能通行,绿灯才可以通行,但如果你非要在红灯的时候通行,谁也拦不住你,那么后果将会导致发生交通事故;所以必须要大家共同遵守交通规则,交通信号灯才能起到作用。

        ⚫ 强制性锁:

        强制性锁比较好理解,它是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取到文件锁的情况下是无法对文件进行访问的。其本质原因在于,强制性锁会让内核检查每一个 I/O 操作(譬如 read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。当一个文件被上锁进行写入操作的时候,内核将阻止其它进程对其进行读写操作。采取强制性锁对性能的影响很大,每次进行读写操作都必须检查文件锁。

        在 Linux 系统中,可以调用 flock()、fcntl()以及 lockf()这三个函数对文件上锁,接下来将向大家介绍每个函数的使用方法。

flock()函数加锁

       先来学习系统调用 flock(),使用该函数可以对文件加锁或者解锁,但是 flock()函数只能产生建议性锁, 其函数原型如下所示:

#include <sys/file.h>

int flock(int fd, int operation);

        使用该函数需要包含头文件<sys/file.h>。

        函数参数和返回值含义如下:

        fd:参数 fd 为文件描述符,指定需要加锁的文件。

        operation:参数 operation 指定了操作方式,可以设置为以下值的其中一个:

  •  LOCK_SH:在 fd 引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一 个文件的共享锁,该共享锁可被多个进程同时拥有。
  • LOCK_EX:在 fd 引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只能同时被一个进程所拥有。
  • LOCK_UN:解除文件锁定状态,解锁、释放锁。 除了以上三个标志外,还有一个标志:
  • LOCK_NB:表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、直到其它进程释放锁为止,如果不想让程序被阻塞,可以指定 LOCK_NB 标志,如果无法获取到锁应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK),通常与 LOCK_SH 或 LOCK_EX 一起使用,通过位或运算符组合在一起。

        返回值:成功将返回 0;失败返回-1、并会设置 errno, 对于 flock(),需要注意的是,同一个文件不会同时具有共享锁和互斥锁。

        使用示例

        下面代码演示了使用 flock()函数对一个文件加锁和解锁(建议性锁)。程序首先调用 open()函数将文件打开,文件路径通过传参的方式传递进来;文件打开成功之后,调用 flock()函数对文件加锁(非阻塞方式、排它锁),并打印出“文件加锁成功”信息,如果加锁失败便会打印出“文件加锁失败”信息。然后调用 signal 函数为 SIGINT 信号注册了一个信号处理函数,当进程接收到 SIGINT 信号后会执行 sigint_handler()函数,在信号处理函数中对文件进行解锁,然后终止进程。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>

static int fd = -1; //文件描述符

/* 信号处理函数 */
static void sigint_handler(int sig){
    if (SIGINT != sig)
    return;
    /* 解锁 */
    flock(fd, LOCK_UN);
    close(fd);
    printf("进程 1: 文件已解锁!\n");
}    

int main(int argc, char *argv[])    {
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    /* 打开文件 */
    fd = open(argv[1], O_WRONLY);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }
    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
        perror("进程 1: 文件加锁失败");
        exit(-1);
    }
    printf("进程 1: 文件加锁成功!\n");

    /* 为 SIGINT 信号注册处理函数 */
    signal(SIGINT, sigint_handler);
    for ( ; ; )
        sleep(1);
}

        加锁成功之后,程序进入了 for 死循环,一直持有锁;此时我们可以执行另一个程序,如下所示,该程序首先也会打开文件,文件路径通过传参的方式传递进来,同样在程序中也会调用 flock()函数对文件加锁(排它锁、非阻塞方式),不管加锁成功与否都会执行下面的 I/O 操作,将数据写入文件、在读取出来并打印。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[100] = "Hello World!";
    int fd;
    int len;
    if (2 != argc) {
        fprintf(stderr, "usage: %s <file>\n", argv[0]);
        exit(-1);
    }
    
    /* 打开文件 */
    fd = open(argv[1], O_RDWR);
    if (-1 == fd) {
        perror("open error");
        exit(-1);
    }

    /* 以非阻塞方式对文件加锁(排它锁) */
    if (-1 == flock(fd, LOCK_EX | LOCK_NB))
        perror("进程 2: 文件加锁失败");
    else
        printf("进程 2: 文件加锁成功!\n");

    /* 写文件 */
    len = strlen(buf);
    if (0 > write(fd, buf, len)) {
        perror("write error");
        exit(-1);
    }
    printf("进程 2: 写入到文件的字符串<%s>\n", buf);

    /* 将文件读写位置移动到文件头 */
    if (0 > lseek(fd, 0x0, SEEK_SET)) {
        perror("lseek error");
        exit(-1);
    }
    /* 读文件 */
    memset(buf, 0x0, sizeof(buf)); //清理 buf
    if (0 > read(fd, buf, len)) {
        perror("read error");
        exit(-1);
    }
    printf("进程 2: 从文件读取的字符串<%s>\n", buf);

    /* 解锁、退出 */
    flock(fd, LOCK_UN);
    close(fd);
    exit(0);
}

        把上文代码分别作为应用程序 1和应用程序 2,将它们分别编译成不同的可执行文件 testApp1 和 testApp2,如下所示:

         在进行测试之前,创建一个测试用的文件 infile,直接使用 touch 命令创建即可,首先执行 testApp1 应用程序,将 infile 文件作为输入文件,并将其放置在后台运行:

         testApp1 会在后台运行,由 ps 命令可查看到其 pid 为 20710。接着执行 testApp2 应用程序,传入相同的文件 infile,如下所示:

         从打印信息可知,testApp2 进程对 infile 文件加锁失败,原因在于锁已经被 testApp1 进程所持有,所以 testApp2 加锁自然会失败;但是可以发现虽然加锁失败,但是 testApp2 对文件的读写操作是没有问题的,是成功的,这就是建议性锁(非强制)的特点;正确的使用方式是,在加锁失败之后不要再对文件进行 I/O 操作了,遵循这个协议。

        接着我们向 testApp1 进程发送一个 SIGIO 信号,让其对文件 infile 解锁,接着再执行一次 testApp2,如下所示:

         使用 kill 命令向 testApp1 进程发送编号为 2 的信号,也就是 SIGIO 信号,testApp1 接收到信号之后, 对 infile 文件进行解锁、然后退出;接着再次执行 testApp2 程序,从打印信息可知,这次能够成功对 infile 文件加锁了,读写也是没有问题的。

        关于 flock()的几条规则

  • 同一进程对文件多次加锁不会导致死锁。当进程调用 flock()对文件加锁成功,再次调用 flock()对文件(同一文件描述符)加锁,这样不会导致死锁,新加的锁会替换旧的锁。譬如调用 flock()对文件加共享锁,再次调用 flock()对文件加排它锁,最终文件锁会由共享锁替换为排它锁。
  • 文件关闭的时候,会自动解锁。进程调用 flock()对文件加锁,如果在未解锁之前将文件关闭,则会导致文件锁自动解锁,也就是说,文件锁会在相应的文件描述符被关闭之后自动释放。同理,当一个进程终止时,它所建立的锁将全部释放。
  • 一个进程不可以对另一个进程持有的文件锁进行解锁。
  • 由 fork()创建的子进程不会继承父进程所创建的锁。这意味着,若一个进程对文件加锁成功,然后该进程调用 fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子进程从父进程继承了其文件描述符,但不能继承文件锁。这个约束是有道理的,因为锁的作用就是阻止多个进程同时写同一个文件,如果子进程通过 fork()继承了父进程的锁,则父进程和子进程就可以同时写同一个文件了。

        除此之外,当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以,如下所示:

flock(fd, LOCK_EX); //加锁
new_fd = dup(fd);
flock(new_fd, LOCK_UN); //解锁

        这段代码先在 fd 上设置一个排它锁,然后使用 dup()对 fd 进行复制得到新文件描述符 new_fd,最后通过 new_fd 来解锁,这样可以解锁成功。但是,如果不显示的调用一个解锁操作,只有当所有文件描述符都被关闭之后锁才会被释放。譬如上面的例子中,如果不调用 flock(new_fd, LOCK_UN)进行解锁,只有当 fd 和 new_fd 都被关闭之后锁才会自动释放。

        关于flock()内容就暂时到这里为止,后面我们将学习使用 fcntl()对文件上锁。

猜你喜欢

转载自blog.csdn.net/cj_lsk/article/details/130873466