应用程序的一个常见需求是从一个文件中读取一些数据,然后将这些数据写回文件。只要在一个时刻只有一个进程以这种方式使用文件就不会存在问题,但当多个进程同时更新一个文件时问题就出现了。本文将介绍两组不同的给文件加锁的API。
flock()函数对整个文件加锁
fcntl()对一个文件的部分区域加锁

使用flock()给文件加锁

函数原型:

#include <sys/file.h>
int flock(int fd,int operation);

fcntl()函数提供了比该函数更为强大的功能,并且所拥有的功能也覆盖了flock()所拥有的功能,但是在某些应用中任然使用着flock()函数,并且在继承和锁释放方面的一些语义中flock()与fcntl()还是有所不同的。
flock()系统调用是在整个文件中加锁,通过对传入的fd所指向的文件进行操作,然后在通过operation参数所设置的值来确定做什么样的操作。
LOCK_SH 在fd引用的文件上防止一把共享锁
LOCK_EX 在fd引用的文件上防止一把互斥锁
LOCK_UN 解锁fd引用的文件
LOCK_NB 发起一个非阻塞的锁请求
在默认情况下,如果另一个进程已经持有了文件上的一个不兼容的锁,那么flock()会阻塞。如果需要防止这种情况的出现,可以在operation参数中对这些值取OR(|)。在这种情况下,如果一个进程已经持有了文件上的一个不兼容锁,那么flock()就会阻塞,相反,它会返回-1,并将errno设置成EWOULDBLOCK。

任意数量的进程可同时持有一个文件上的共享锁,但子任意时刻只能有一个进程能够持有一个文件上的互斥锁。

无论程序以什么模式打开了文件(读、写或者读写),该文件上都可以放置一把共享锁或互斥锁。在实际操作过程中,参数operation可以指定对应的值将共享锁转换成互斥锁(反之亦然)。将一个共享锁转换成互斥锁,如果另一个进程要获取该文件的共享锁则会阻塞,除非operation参数指定了LOCK_NB标记,即:(LOCK_SH | LOCK_NB)。锁的转换过程不是一个原子操作,在转换的过程中首先会删除既有的锁,然后创建新锁。

源代码:

/*************************************************************************\
*                  Copyright (C) Michael Kerrisk, 2018.                   *
*                                                                         *
* This program is free software. You may use, modify, and redistribute it *
* under the terms of the GNU General Public License as published by the   *
* Free Software Foundation, either version 3 or (at your option) any      *
* later version. This program is distributed without any warranty.  See   *
* the file COPYING.gpl-v3 for details.                                    *
\*************************************************************************/

/* Listing 55-1 */

/* t_flock.c

   Demonstrate the use of flock() to place file locks.
*/
#include <sys/file.h>
#include <fcntl.h>
#include "curr_time.h"                  /* Declaration of currTime() */
#include "tlpi_hdr.h"

int
main(int argc, char *argv[])
{
    int fd, lock;
    const char *lname;

    if (argc < 3 || strcmp(argv[1], "--help") == 0 ||
            strchr("sx", argv[2][0]) == NULL)
        usageErr("%s file lock [sleep-time]\n"
                 "    'lock' is 's' (shared) or 'x' (exclusive)\n"
                 "        optionally followed by 'n' (nonblocking)\n"
                 "    'sleep-time' specifies time to hold lock\n", argv[0]);

    lock = (argv[2][0] == 's') ? LOCK_SH : LOCK_EX;
    if (argv[2][1] == 'n')
        lock |= LOCK_NB;

    fd = open(argv[1], O_RDONLY);               /* Open file to be locked */
    if (fd == -1)
        errExit("open");

    lname = (lock & LOCK_SH) ? "LOCK_SH" : "LOCK_EX";

    printf("PID %ld: requesting %s at %s\n", (long) getpid(), lname,
            currTime("%T"));

    if (flock(fd, lock) == -1) {
        if (errno == EWOULDBLOCK)
            fatal("PID %ld: already locked - bye!", (long) getpid());
        else
            errExit("flock (PID=%ld)", (long) getpid());
    }

    printf("PID %ld: granted    %s at %s\n", (long) getpid(), lname,
            currTime("%T"));

    sleep((argc > 3) ? getInt(argv[3], GN_NONNEG, "sleep-time") : 10);

    printf("PID %ld: releasing  %s at %s\n", (long) getpid(), lname,
            currTime("%T"));
    if (flock(fd, LOCK_UN) == -1)
        errExit("flock");

    exit(EXIT_SUCCESS);
}

执行过程:

[root@centos-7 filelock]# touch tfile
# 在后台启动一个程序实例并持有一个共享锁60s
[root@centos-7 filelock]# ./t_flock tfile s 60 &
[2] 12819
[root@centos-7 filelock]# PID 12819: requesting LOCK_SH at 17:00:22
PID 12819: granted    LOCK_SH at 17:00:22

#启动一个程序实例并持有共享锁2s后释放
[root@centos-7 filelock]# ./t_flock tfile s 2
PID 12863: requesting LOCK_SH at 17:00:31
PID 12863: granted    LOCK_SH at 17:00:31
PID 12863: releasing  LOCK_SH at 17:00:33

#当启动一个程序实例来非阻塞地请求互斥锁时会立刻失败
[root@centos-7 filelock]# ./t_flock tfile xn
PID 12904: requesting LOCK_EX at 17:00:41
ERROR: PID 12904: already locked - bye!

当启动一个程序实例来阻塞地请求互斥锁时,等12819释放锁后,获取互斥锁成功
[root@centos-7 filelock]# ./t_flock tfile x
PID 12927: requesting LOCK_EX at 17:00:47
PID 12819: releasing  LOCK_SH at 17:01:22
PID 12927: granted    LOCK_EX at 17:01:22
PID 12927: releasing  LOCK_EX at 17:01:32
[2]-  完成                  ./t_flock tfile s 60
[root@centos-7 filelock]# 

flock锁继承与释放

flock()根据调用时operation参数传入LOCK_UN的值来释放一个文件锁。此外,锁会在相应的文件描述符被关闭之后自动释放。同时,当一个文件描述符被复制时(dup()、dup2()、或一个fcntl() F_DUPFD操作),新的文件描述符会引用同一个文件锁。

flock(fd, LOCK_EX);
new_fd = dup(fd);
flock(new_fd, LOCK_UN);

这段代码先在fd上设置一个互斥锁,然后通过fd创建一个指向相同文件的新文件描述符new_fd,最后通过new_fd来解锁。从而我们可以得知新的文件描述符指向了同一个锁。所以,如果通过一个特定的文件描述符获取了一个锁并且创建了该描述符的一个或多个副本,那么,如果不显示的调用一个解锁操作,只有当文件描述符副本都被关闭了之后锁才会被释放。

由上我们可以推出,如果使用fork()创建一个子进程,子进程会复制父进程中的所有描述符,从而使得它们也会指向同一个文件锁。例如下面的代码会导致一个子进程删除一个父进程的锁:

flock (fd, LOCK_EX);
if (0 == fork ()) {
    flock (fd, LOCK_UN);
}

所以,有时候可以利用这些语义来将一个文件锁从父进程传输到子进程:在fork()之后,父进程关闭其文件描述符,然后锁就只在子进程的控制之下了。通过fork()创建的锁在exec()中会得以保留(除非在文件描述符上设置了close-on-exec标记并且该文件描述符是最后一个引用底层的打开文件描述的描述符)。

如果程序中使用open()来获取第二个引用同一个文件的描述符,那么,flock()会将其视为不同的文件描述符。如下代码会在第二个flock()上阻塞。

fd1 = open ("test.txt", O_RDWD);
fd2 = open ("test.txt", O_RDWD);

flock (fd1, LOCK_EX);
flock (fd2, LOCK_EX);

flock锁的限制

只能对整个文件进行加锁。这种粗粒度的加锁会限制协作进程间的并发。假如存在多个进程,其中各个进程都想同时访问同一个文件的不同部分。
通过flock()只能放置劝告式锁。
很多NFS实现不识别flock()放置的锁。

在默认情况下,文件锁是劝告式的,这表示一个进程可以简单地忽略另一个进程在文件上放置的锁。要使得劝告式加锁模型能够正常工作,所有访问文件的进程都必须要配合,即在执行文件IO之前先放置一把锁。