Linux环境下的文件锁:flock与fcntl
2021-02-21 19:45:22 阿炯

Linux下的文件全局文件锁可分两类:BSD 锁(flock) 和 POSIX 记录锁(fcntl)。

在多个进程同时操作同一份文件的过程中,很容易导致文件中的数据混乱,需要锁操作来保证数据的完整性,这里介绍的针对文件的锁,称之为“文件锁”-flock(语意来自BSD,早期Linux不支持)。

FD vs OFD

FD (File Descriptor) :在进程内打开一个文件,返回一个FD,用户态的概念。

OFD(Open File Descriptor) :第一次打开文件时候,在内核创建一个OFD。如果不同进程打开同一个文件,不同的fd会对应同一个内核态的open fd。


flock是建议性锁,不具备强制性。一个进程使用flock将文件锁住,另一个进程可以直接操作正在被锁的文件,修改文件中的数据,原因在于flock只是用于检测文件是否被加锁,针对文件已经被加锁,另一个进程写入数据的情况,内核不会阻止这个进程的写入操作,也就是建议性锁的内核处理策略。flock主要三种操作类型:
LOCK_SH,共享锁,多个进程可以使用同一把锁,常被用作读共享锁;
LOCK_EX,排他锁,同时只允许一个进程使用,常被用作写锁;
LOCK_UN,释放锁;

进程使用flock尝试锁文件时,如果文件已经被其他进程锁住,进程会被阻塞直到锁被释放掉,或者在调用flock的时候,采用LOCK_NB参数,在尝试锁住该文件的时候,发现已经被其他服务锁住,会返回错误,errno错误码为EWOULDBLOCK。即提供两种工作模式:阻塞与非阻塞类型。

服务会阻塞等待直到锁被释放:
flock(lockfd,LOCK_EX)

服务会返回错误发现文件已经被锁住时:
ret = flock(lockfd,LOCK_EX|LOCK_NB)

同时ret = -1, errno = EWOULDBLOCK

flock锁的释放非常具有特色,即可调用LOCK_UN参数来释放文件锁,也可以通过关闭fd的方式来释放文件锁flock的第一个参数是fd,意味着flock会随着进程的关闭而被自动释放掉。

flock其中的一个使用场景为:检测进程是否已经存在

int checkexit(char* pfile){
    if (pfile == NULL){
        return -1;
    }
    int lockfd = open(pfile,O_RDWR);
    if (lockfd == -1){
        return -2;
    }
    int iret = flock(lockfd,LOCK_EX|LOCK_NB);
    if (iret == -1){
        return -3;
    }
    return 0;
}


1. 场景概述

在多线程开发中,互斥锁可以用于对临界资源的保护,防止数据的不一致,这是最为普遍的使用方法。那在多进程中如何处理文件之间的同步呢?我们看看下面的图:


图中所示的是两个进程在无同步的情况下同时更新同一个文件的过程,其主要的操作是:
1). 从文件中读取序号。
2). 使用这个序号完成应用程序定义的任务。
3). 递增这个序号并将其写回文件中。

从图中可得知两个进程读取分别增加了所读取到的序号,并写回到了文件中,但是如果有相互互斥的话,最后的值应该是1002,而不是所示的1001。为了防止出现这种情况,Linux提供了flock(对整个文件加锁)、fcntl(对整个文件区域加锁)两个函数来做进程间的文件同步。同时也可以使用信号量来完成所需的同步,但通常使用文件锁会更好一些,因为内核能够自动将锁与文件关联起来。

2. flock()

flock的声明如下:
#include <sys/file.h>
// Returns 0 on success, or -1 on error
int flock (intfd, int operation);

fcntl()函数提供了比该函数更为强大的功能,并且所拥有的功能也覆盖了flock()所拥有的功能,但是在某些应用中任然使用着flock()函数,并且在继承和锁释放方面的一些语义 中flock()与fcntl()还是有所不同的。

flock()系统调用是在整个文件中加锁,通过对传入的fd所指向的文件进行操作,然后在通过operation参数所设置的值来确定做什么样的操作。operation可以赋如下值:


在默认情况下,如果另一个进程已经持有了文件上的一个不兼容的锁,那么flock()会阻塞。如果需要防止这种情况的出现,可以在operation参数中对这些值取OR|。在这种情况下,如果一个进程已经持有了文件上的一个不兼容锁,那么flock()就会阻塞,相反,它会返回-1,并将errno设置成EWOULDBLOCK。

任意数量的进程可同时持有一个文件上的共享锁,但子任意时刻只能有一个进程能够持有一个文件上的互斥锁,这有点类似读写锁。下图是进程A先设置了锁,进程B后设置锁的支持情况:


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

3. 锁继承与释放的语义

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);

4. flock()的限制

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

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


flock提供了文件粒度的的lock,其owner是内核态open fd,而不是用户态fd,也不是inode。所以dup(),dup2(), or fcntl(), or folk()调用后,虽然返回不同的fd,但本质还是同一个内核open fd。假设进程1的fd3和进程2的fd4指向同一个内核open fd,对fd3加EX锁后,fd4依然可以获得EX锁。

fcntl的POSIX语意的lock,提供了字节范围粒度的lock,又称为record lock。使用fcntl需要提供fd,cmd和flock结构体。fcntl的owner是进程,不是fd也不是inode。

flockfcntl
锁力度整个文件记录
建议锁/强制锁建议锁建议锁/强制锁(不推荐)
锁的类型FL_FLOCK BSD版本FL_POSIX POSIX版本


flock和fcntl在内核中都用struct file_lock实现,其主要差别就在于owner的不同。如果lock的owner相同,conflict的检测就会跳过,即相同owner的lock可以递归申请。它们表现出了完全不同的行为,那么为什么会出现这种情况呢,下面要从进程级文件描述符表、系统级打开文件表、系统级inode表说起。

文件描述符表:该表记录进程打开的文件。它的表项里面有一个指针,指向存放在内核空间的文件表中的一个表项。它向用户提供一个简单的文件描述符,使得用户可以通过方便地访问一个文件。当进程使用open打开一个文件时,内核就会在这个表中添加一个表项。如果对同一个文件打开多次,那么将有多个表项。使用dup时,也会增加一个表项。

系统级文件打开表:文件表保存了进程对文件读写的偏移量以及进程对文件的存取权限。

系统级inode表:它记录了文件的类型、权限、UID、GID、link count、文件大小、创建时间已经文件存放位置等文件的基本属性。


进程打开文件时,这三个表的关系是:


实际上,fcntl和flock在对文件加锁的时候,虽然都是在文件的inode上进行上锁,但是flock认为,锁的持有者是打开文件表,而fcntl则认为锁的持有者是进程,这就造成了两者在实际使用中的不同!



从开发的角度看flock和lockf

从底层的实现来说,Linux的文件锁主要有两种:flock和lockf。需要额外对lockf说明的是,它只是fcntl系统调用的一个封装。从使用角度讲,lockf或fcntl实现了更细粒度文件锁,即:记录锁。我们可以使用lockf或fcntl对文件的部分字节上锁,而flock只能对整个文件加锁。这两种文件锁是从历史上不同的标准中起源的,flock来自BSD而lockf来自POSIX,所以lockf或fcntl实现的锁在类型上又叫做POSIX锁。

除了这个区别外,fcntl系统调用还可以支持强制锁Mandatory locking。强制锁的概念是传统UNIX为了强制应用程序遵守锁规则而引入的一个概念,与之对应的概念就是建议锁Advisory locking。我们日常使用的基本都是建议锁,它并不强制生效。这里的不强制生效的意思是,如果某一个进程对一个文件持有一把锁之后,其他进程仍然可以直接对文件进行各种操作的,比如open、read、write。只有当多个进程在操作文件前都去检查和对相关锁进行锁操作的时候,文件锁的规则才会生效,这就是一般建议锁的行为。而强制性锁试图实现一套内核级的锁操作。当有进程对某个文件上锁之后,其他进程即使不在操作文件之前检查锁,也会在open、read或write等文件操作时发生错误。内核将对有锁的文件在任何情况下的锁规则都生效,这就是强制锁的行为。由此可以理解,如果内核想要支持强制锁,将需要在内核实现open、read、write等系统调用内部进行支持。

从应用的角度来说,Linux内核虽然号称具备了强制锁的能力,但其对强制性锁的实现是不可靠的,建议大家还是不要在Linux下使用强制锁。事实上,在使用的Linux环境上,一个系统在mount -o mand分区的时候报错(archlinux kernel 4.5),而另一个系统虽然可以以强制锁方式mount上分区,但是功能实现却不完整,主要表现在只有在加锁后产生的子进程中open才会报错,如果直接write是没问题的,而且其他进程无论open还是read、write都没问题Centos 7 kernel 3.10。鉴于此,我们就不在此介绍如何在Linux环境中打开所谓的强制锁支持了。我们只需知道,在Linux环境下的应用程序,flock和lockf在是锁类型方面没有本质差别,他们都是建议锁,而非强制锁。

flock和lockf另外一个差别是它们实现锁的方式不同。这在应用的时候表现在flock的语义是针对文件的锁,而lockf是针对文件描述符fd的锁。我们用一个例子来观察这个区别:
$ cat flock.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>

#define PATH "/tmp/lock"

int main(){
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    if (flock(fd, LOCK_EX) < 0) {
        perror("flock()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
            perror("open()");
            exit(1);
        }
*/
        if (flock(fd, LOCK_EX) < 0) {
            perror("flock()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);
    exit(0);
}

上面代码是一个flock的例子,其作用也很简单:

打开/tmp/lock文件。
使用flock对其加互斥锁。
打印“PID:locked!”表示加锁成功。
打开一个子进程,在子进程中使用flock对同一个文件加互斥锁。
子进程打印“PID:locked!”表示加锁成功。如果没加锁成功子进程会推出,不显示相关内容。
父进程回收子进程并推出。

这个程序直接编译执行的结果是:
$ ./flock
23279: locked!
23280: locked!

父子进程都加锁成功了。这个结果似乎并不符合我们对文件加锁的本意。按照我们对互斥锁的理解,子进程对父进程已经加锁过的文件应该加锁失败才对。我们可以稍微修改一下上面程序让它达到预期效果,将子进程代码段中的注释取消掉重新编译即可:
...
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
                perror("open()");
                exit(1);
        }
*/
...

将这段代码上下的/ /删除重新编译。之后执行的效果如下:
$ make flock
cc     flock.c   -o flock

$ ./flock
23437: locked!

此时子进程flock的时候会阻塞,让进程的执行一直停在这。这才是我们使用文件锁之后预期该有的效果。而相同的程序使用lockf却不会这样。这个原因在于flock和lockf的语义是不同的。使用lockf或fcntl的锁,在实现上关联到文件结构体,这样的实现导致锁不会在fork之后被子进程继承。而flock在实现上关联到的是文件描述符,这就意味着如果我们在进程中复制了一个文件描述符,那么使用flock对这个描述符加的锁也会在新复制出的描述符中继续引用。在进程fork的时候,新产生的子进程的描述符也是从父进程继承复制来的。在子进程刚开始执行的时候,父子进程的描述符关系实际上跟在一个进程中使用dup复制文件描述符的状态一样参见《UNIX环境高级编程》8.3节的文件共享部分。这就可能造成上述例子的情况,通过fork产生的多个进程,因为子进程的文件描述符是复制的父进程的文件描述符,所以导致父子进程同时持有对同一个文件的互斥锁,导致第一个例子中的子进程仍然可以加锁成功。这个文件共享的现象在子进程使用open重新打开文件之后就不再存在了,所以重新对同一文件open之后,子进程再使用flock进行加锁的时候会阻塞。另外要注意:除非文件描述符被标记了close-on-exec标记,flock锁和lockf锁都可以穿越exec,在当前进程变成另一个执行镜像之后仍然保留。

上面的例子中只演示了fork所产生的文件共享对flock互斥锁的影响,同样原因也会导致dup或dup2所产生的文件描述符对flock在一个进程内产生相同的影响。dup造成的锁问题一般只有在多线程情况下才会产生影响,所以应该避免在多线程场景下使用flock对文件加锁,而lockf/fcntl则没有这个问题。

为了对比flock的行为,我们在此列出使用lockf的相同例子,来演示一下它们的不同:
$ cat lockf.c
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <wait.h>

#define PATH "/tmp/lock"

int main(){
    int fd;
    pid_t pid;

    fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd < 0) {
        perror("open()");
        exit(1);
    }

    if (lockf(fd, F_LOCK, 0) < 0) {
        perror("lockf()");
        exit(1);
    }
    printf("%d: locked!\n", getpid());

    pid = fork();
    if (pid < 0) {
        perror("fork()");
        exit(1);
    }

    if (pid == 0) {
/*
        fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
        if (fd < 0) {
            perror("open()");
            exit(1);
        }
*/
        if (lockf(fd, F_LOCK, 0) < 0) {
            perror("lockf()");
            exit(1);
        }
        printf("%d: locked!\n", getpid());
        exit(0);
    }
    wait(NULL);
    unlink(PATH);    
    exit(0);
}

编译执行的结果是:
$ ./lockf

27262: locked!

在子进程不用open重新打开文件的情况下,进程执行仍然被阻塞在子进程lockf加锁的操作上。关于fcntl对文件实现记录锁的详细内容,大家可以参考《UNIX环境高级编程》中关于记录锁的14.3章节。

标准IO库文件锁

C语言的标准IO库中还提供了一套文件锁,它们的原型如下:
#include <stdio.h>

void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);

从实现角度来说,stdio库中实现的文件锁与flock或lockf有本质区别。作为一种标准库,其实现的锁必然要考虑跨平台的特性,所以其结构都是在用户态的FILE结构体中实现的,而非内核中的数据结构来实现。这直接导致的结果就是,标准IO的锁在多进程环境中使用是有问题的。进程在fork的时候会复制一整套父进程的地址空间,这将导致子进程中的FILE结构与父进程完全一致。就是说,父进程如果加锁了,子进程也将持有这把锁,父进程没加锁,子进程由于地址空间跟父进程是独立的,所以也无法通过FILE结构体检查别的进程的用户态空间是否家了标准IO库提供的文件锁。这种限制导致这套文件锁只能处理一个进程中的多个线程之间共享的FILE 的进行文件操作。就是说,多个线程必须同时操作一个用fopen打开的FILE 变量,如果内部自己使用fopen重新打开文件,那么返回的FILE *地址不同,也起不到线程的互斥作用。

我们分别将两种使用线程的状态的例子分别列出来,第一种是线程之间共享同一个FILE *的情况,这种情况互斥是没问题的:
$ cat racing_pthread_sharefp.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>
#include <pthread.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"
static FILE *filep;

void *do_child(void *p){
    int fd;
    int ret, count;
    char buf[NUM];

    flockfile(filep);

    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fread(buf, NUM, 1, filep);

    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fwrite(buf, strlen(buf), 1, filep);

    funlockfile(filep);

    return NULL;
}

int main(){
    pthread_t tid[COUNT];
    int count;

    filep = fopen(FILEPATH, "r+");
    if (filep == NULL) {
        perror("fopen()");
        exit(1);
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
            perror("pthread_create()");
            exit(1);
        }
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_join(tid[count], NULL) != 0) {
            perror("pthread_join()");
            exit(1);
        }
    }

    fclose(filep);

    exit(0);
}

另一种情况是每个线程都fopen重新打开一个描述符,此时线程是不能互斥的:
$ cat racing_pthread_threadfp.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/file.h>
#include <wait.h>
#include <pthread.h>

#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"

void *do_child(void *p){
    int fd;
    int ret, count;
    char buf[NUM];
    FILE *filep;

    filep = fopen(FILEPATH, "r+");
    if (filep == NULL) {
        perror("fopen()");
        exit(1);
    }

    flockfile(filep);

    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fread(buf, NUM, 1, filep);

    count = atoi(buf);
    ++count;
    sprintf(buf, "%d", count);
    if (fseek(filep, 0L, SEEK_SET) == -1) {
        perror("fseek()");
    }
    ret = fwrite(buf, strlen(buf), 1, filep);

    funlockfile(filep);

    fclose(filep);
    return NULL;
}

int main(){
    pthread_t tid[COUNT];
    int count;

    for (count=0;count<COUNT;count++) {
        if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
            perror("pthread_create()");
            exit(1);
        }
    }

    for (count=0;count<COUNT;count++) {
        if (pthread_join(tid[count], NULL) != 0) {
            perror("pthread_join()");
            exit(1);
        }
    }

    exit(0);
}

以上程序大家可以自行编译执行看看效果。

文件锁相关命令

系统为我们提供了flock命令,可以方便我们在命令行和shell脚本中使用文件锁。需要注意的是,flock命令是使用flock系统调用实现的,所以在使用这个命令的时候请注意进程关系对文件锁的影响。flock命令的使用方法和在脚本编程中的使用可以参见我的另一篇文章《shell编程之常用技巧》中的bash并发编程和flock这部分内容,在此不在赘述。还可以使用lslocks命令来查看当前系统中的文件锁使用情况,一个常见的现实如下:
# lslocks
COMMAND           PID   TYPE  SIZE MODE  M      START        END PATH
firefox         16280  POSIX    0B WRITE 0          0          0 /home/freeoa/.mozilla/firefox/bk2bfsto.default/.parentlock
dmeventd          344  POSIX    4B WRITE 0          0          0 /run/dmeventd.pid
gnome-shell       472  FLOCK    0B WRITE 0          0          0 /run/user/120/wayland-0.lock
flock           27452  FLOCK    0B WRITE 0          0          0 /tmp/lock
lvmetad           248  POSIX    4B WRITE 0          0          0 /run/lvmetad.pid

这其中,TYPE主要表示锁类型,就是上文我们描述的flock和lockf。lockf和fcntl实现的锁事POSIX类型。M表示是否事强制锁,0表示不是。如果是记录锁的话,START和END表示锁住文件的记录位置,0表示目前锁住的是整个文件。MODE主要用来表示锁的权限,实际上这也说明了锁的共享属性。在系统底层,互斥锁表示为WRITE,而共享锁表示为READ,如果这段出现*则表示有其他进程正在等待这个锁。其余参数可以参考man lslocks。



linux中fcntl()、lockf、flock的区别

这三个函数的作用都是给文件加锁,那它们有什么区别呢?首先flock和fcntl是系统调用,而lockf是库函数。lockf实际上是fcntl的封装,所以lockf和fcntl的底层实现是一样的,对文件加锁的效果也是一样的。后面分析不同点时大多数情况是将fcntl和lockf放在一起的。下面首先看每个函数的使用,从使用的方式和效果来看各个函数的区别。

1. flock

函数原型

#include <sys/file.h>
int flock(int fd, int operation);  // Apply or remove an advisory lock on the open file specified by fd,只是建议性锁

其中fd是系统调用open返回的文件描述符,operation的选项有:
LOCK_SH :共享锁
LOCK_EX :排他锁或者独占锁
LOCK_UN : 解锁。
LOCK_NB:非阻塞与以上三种操作一起使用

关于flock函数,首先要知道flock函数只能对整个文件上锁,而不能对文件的某一部分上锁,这是于fcntl/lockf的第一个重要区别,后者可以对文件的某个区域上锁。其次,flock只能产生劝告性锁。我们知道,linux存在强制锁mandatory lock和劝告锁advisory lock。所谓强制锁,比较好理解,就是你家大门上的那把锁,最要命的是只有一把钥匙,只有一个进程可以操作。所谓劝告锁,本质是一种协议,你访问文件前,先检查锁,这时候锁才其作用,如果不那么介意,不管三七二十一,就要读写,那么劝告锁没有任何的作用。而遵守协议,读写前先检查锁的那些进程,叫做合作进程。再次,flock和fcntl/lockf的区别主要在fork和dup。

(1) flock创建的锁是和文件打开表项struct file相关联的,而不是fd。这就意味着复制文件fd通过fork或者dup后,那么通过这两个fd都可以操作这把锁例如通过一个fd加锁,通过另一个fd可以释放锁,也就是说子进程继承父进程的锁。但是上锁过程中关闭其中一个fd,锁并不会释放因为file结构并没有释放,只有关闭所有复制出的fd,锁才会释放。测试程序入程序一。

程序一
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
int main (int argc, char ** argv){
    int ret;
    int fd1 = open("./tmp.txt",O_RDWR);
    int fd2 = dup(fd1);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);
    ret = flock(fd1,LOCK_EX);
    printf("get lock1, ret: %d\n", ret);
    ret = flock(fd2,LOCK_EX);
    printf("get lock2, ret: %d\n", ret);
    return 0;
}

运行结果如图,对fd1上锁,并不影响程序通过fd2上锁。对于父子进程,参考程序二。


程序二
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
int main (int argc, char ** argv){
    int ret;
    int pid;
    int fd = open("./tmp.txt",O_RDWR);
    if ((pid = fork()) == 0){
        ret = flock(fd,LOCK_EX);
        printf("chile get lock, fd: %d, ret: %d\n",fd, ret);
        sleep(10);
        printf("chile exit\n");
        exit(0);
    }
ret = flock(fd,LOCK_EX);
printf("parent get lock, fd: %d, ret: %d\n", fd, ret);
printf("parent exit\n");
return 0;
}

运行结果如图,子进程持有锁,并不影响父进程通过相同的fd获取锁,反之亦然。


(2)使用open两次打开同一个文件,得到的两个fd是独立的因为底层对应两个file对象,通过其中一个加锁,通过另一个无法解锁,并且在前一个解锁前也无法上锁。测试程序如程序三:

程序三
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
int main (int argc, char ** argv){
    int ret;
    int fd1 = open("./tmp.txt",O_RDWR);
    int fd2 = open("./tmp.txt",O_RDWR);
    printf("fd1: %d, fd2: %d\n", fd1, fd2);
    ret = flock(fd1,LOCK_EX);
    printf("get lock1, ret: %d\n", ret);
    ret = flock(fd2,LOCK_EX);
    printf("get lock2, ret: %d\n", ret);
    return 0;
}

结果如图,通过fd1获取锁后,无法再通过fd2获取锁。


(3)使用exec后,文件锁的状态不变。

(4)flock不能再NFS文件系统上使用,如果要在NFS使用文件锁,请使用fcntl。

(5)flock锁可递归,即通过dup或者或者fork产生的两个fd,都可以加锁而不会产生死锁。

2. lockf与fcntl

函数原型

#include <unistd.h>
int lockf(int fd, int cmd, off_t len);

fd为通过open返回的打开文件描述符。cmd的取值为:
   F_LOCK:给文件互斥加锁,若文件以被加锁,则会一直阻塞到锁被释放。
   F_TLOCK:同F_LOCK,但若文件已被加锁,不会阻塞,而回返回错误。
   F_ULOCK:解锁。
   F_TEST:测试文件是否被上锁,若文件没被上锁则返回0,否则返回-1。
len:为从文件当前位置的起始要锁住的长度。

通过函数参数的功能,可以看出lockf只支持排他锁,不支持共享锁。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

struct flock {
...
short l_type;/* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start;   /* Starting offset for lock */
off_t l_len;     /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
...        

};

文件记录加锁相关的cmd 分三种:
F_SETLK:申请锁读锁F_RDLCK,写锁F_WRLCK或者释放所F_UNLCK,但是如果kernel无法将锁授予本进程被其他进程抢了先,占了锁,不傻等,返回error。

F_SETLKW:和F_SETLK几乎一样,唯一的区别,这厮是个死心眼的主儿,申请不到,就傻等。

F_GETLK:这个接口是获取锁的相关信息: 这个接口会修改我们传入的struct flock。

通过函数参数功能可以看出fcntl是功能最强大的,它既支持共享锁又支持排他锁,即可以锁住整个文件,又能只锁文件的某一部分。

下面看fcntl/lockf的特性:
(1)上锁可递归,如果一个进程对一个文件区间已经有一把锁,后来进程又企图在同一区间再加一把锁,则新锁将替换老锁。

(2)加读锁共享锁文件必须是读打开的,加写锁排他锁文件必须是写打开。

(3)进程不能使用F_GETLK命令来测试它自己是否再文件的某一部分持有一把锁。F_GETLK命令定义说明,返回信息指示是否现存的锁阻止调用进程设置它自己的锁。因为,F_SETLK和F_SETLKW命令总是替换进程的现有锁,所以调用进程绝不会阻塞再自己持有的锁上,于是F_GETLK命令绝不会报告调用进程自己持有的锁。

(4)进程终止时,他所建立的所有文件锁都会被释放,队医flock也是一样的。

(5)任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一把锁都被释放这些锁都是该进程设置的,这一点与flock不同。如:
fd1 = open(pathname, …);
lockf(fd1, F_LOCK, 0);
fd2 = dup(fd1);
close(fd2);

则在close(fd2)后,再fd1上设置的锁会被释放,如果将dup换为open,以打开另一描述符上的同一文件,则效果也一样。

fd1 = open(pathname, …);
lockf(fd1, F_LOCK, 0);
fd2 = open(pathname, …);
close(fd2);

(6)由fork产生的子进程不继承父进程所设置的锁,这点与flock也不同。

(7)在执行exec后,新程序可以继承原程序的锁,这点和flock是相同的如果对fd设置了close-on-exec,则exec前会关闭fd,相应文件的锁也会被释放。

(8)支持强制性锁:对一个特定文件打开其设置组ID位(S_ISGID),并关闭其组执行位(S_IXGRP),则对该文件开启了强制性锁机制。再Linux中如果要使用强制性锁,则要在文件系统mount时,使用_omand打开该机制。

3. 两种锁的关系

那么flock和lockf/fcntl所上的锁有什么关系呢?答案时互不影响。测试程序如下:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
int main(int argc, char **argv){
    int fd, ret;
    int pid;
    fd = open("./tmp.txt", O_RDWR);
    ret = flock(fd, LOCK_EX);
    printf("flock return ret : %d\n", ret);
    ret = lockf(fd, F_LOCK, 0);
    printf("lockf return ret: %d\n", ret);
    sleep(100);
    return 0;
}

测试结果如下:
$./a.out

flock return ret : 0
lockf return ret: 0

可见flock的加锁,并不影响lockf的加锁。另外我们可以通过/proc/locks查看进程获取锁的状态。

$ps aux | grep a.out | grep -v grep
123751   18849  0.0  0.0  11904   440 pts/5    S+   01:09   0:00 ./a.out

$sudo cat /proc/locks | grep 18849
1: POSIX  ADVISORY  WRITE 18849 08:02:852674 0 EOF
2: FLOCK  ADVISORY  WRITE 18849 08:02:852674 0 EOF

我们可以看到/proc/locks下面有锁的信息:我现在分别叙述下含义:
1)POSIX FLOCK 这个比较明确,就是哪个类型的锁。flock系统调用产生的是FLOCK,fcntl调用F_SETLK,F_SETLKW或者lockf产生的是POSIX类型,有次可见两种调用产生的锁的类型是不同的;

2)ADVISORY表明是劝告锁;

3)WRITE顾名思义,是写锁,还有读锁;

4)18849是持有锁的进程ID。当然对于flock这种类型的锁,会出现进程已经退出的状况。

5)08:02:852674表示的对应磁盘文件的所在设备的主设备好,次设备号,还有文件对应的inode number。

6)0表示的是所的其实位置

7)EOF表示的是结束位置。 这两个字段对fcntl类型比较有用,对flock来是总是0 和EOF。



linux使用flock文件锁解决脚本重复执行问题

在使用crontab管理定时脚本时,脚本执行时间超过n分钟,这时就会造成多个脚本同时执行,严重时甚至拖垮服务器,影响服务器上的其它服务。这里希望其在执行完上一任务,再执行下一任务,如果上一任务未执行完成,则这次的任务不执行,直到下一周期再判断,如果上一任务执行完成,则可以执行下一任务。

改进方:可以使用一个锁文件,来记录任务是否执行中。首先判断/tmp/mytest.lock是否存在,如果不存在,则创建,然后执行任务,任务执行完后删除锁文件。如果锁文件已经存在,则退出这次的任务。

<?php
$lockfile = '/tmp/mytest.lock';
if(file_exists($lockfile)){
    exit();
}else{
    file_put_contents($lockfile, 1, true);
}
for($i=0; $i<300; $i++){
    echo date('Y-m-d H:i:s')."\n";
    sleep(1);
}
unlink($lockfile);
?>

这样的确可以保证任务执行其间不会有新任务执行,但这样需要在任务文件中写代码做判断,不方便。能不能把任务锁定的判断放在任务以外呢?

可使用linux flock文件锁机制实现任务锁定,解决冲突。

格式:
flock [-sxun][-w #] fd#
flock [-sxon][-w #] file [-c] command

Usage:
flock [options] <file|directory> <command> [command args]
flock [options] <file|directory> -c <command>
flock [options] <file descriptor number>

选项:
-s, --shared:获得一个共享锁
-e, -x, --exclusive: 获得一个独占锁,有时称为写锁。这是默认值
-u, --unlock:移除一个锁,通常是不需要的,脚本执行完会自动丢弃锁
-n, --nb, --nonblock:如果没有立即获得锁,直接失败而不是等待;有关使用的退出代码,请参见-E选项
-w, --timeout:如果没有立即获得锁,等待指定时间
-o, --close:在运行命令前关闭文件的描述符号。用于如果命令产生子进程时会不受锁的管控
-c, --command:在shell中运行一个单独的命令
-h, --help 显示帮助
-V, --version:显示版本


继续用回第一个test.pl,文件锁使用独占锁,如果锁定则失败不等待,参数为-xn
* * * * * flock -xn /tmp/mytest.lock -c 'perl /home/freeoa/bin/test.pl >> /home/freeoa/log/test.log'
*/1 * * * * flock -xn /tmp/mytest.lock -c '/usr/local/freeoa/start.sh > /dev/null 2>&1 &'

这样当任务未执行完成,下一任务判断到/tmp/mytest.lock被锁定,则结束当前的任务,下一周期再判断。

flock这个系统调用是对于整个文件的建议性锁,也就是说,如果一个进程在一个文件inode上放了锁,那么其它进程是可以知道的建议性锁不强求进程遵守。最棒的一点是,它的第一个参数是文件描述符,在此文件描述符关闭时,锁会自动释放。而当进程终止时,所有的文件描述符均会被关闭。于是,很多时候就不用考虑解锁的事情了。

flock 有个对应的 shell 命令也叫 flock,当服务器上经常看到一堆未退出的 cron 任务进程,可将所有这样的任务包一层 flock 就不会导致启动 N 个进程做同一件事了。

flock -n /tmp/.my.lock -c 'command to run'

flock 命令除了接收文件名参数外,还可以接收文件描述符参数,这种方法在 shell 脚本里特别有用。比如如下代码:
lockit () {
  exec 7<>.lock
  flock -n 7 || {
    echo "Waiting for lock to release..."
    flock 7
  }
}

exec行打开.lock文件为 7 号文件描述符,然后拿 flock 尝试锁它。如果失败了,就输出一条消息,并且等待锁被释放。那个 7 号文件描述符就让它一直开着了,反正脚本执行完毕内核会释放,也不用去调用trap内建命令了。

上边有一点很有意思的是,flock 是一个子进程,但是因为文件描述符在 fork 和 execve 中会共享,而 flock 锁在 fork 和 execve 时也不会改变,所以子进程在那个文件描述符上加锁后,即使它退出了,因为那个文件描述符父进程还有一份,所以不会被关闭,锁也就得以保留。所以,如果余下的脚本里要是有进程带着那个文件描述符 fork 到后台锁就不会在脚本执行完后自动解除啦……

经测试,其它一些类 Unix 系统上或者没有 flock 这个系统调用,只有 fcntl 那个,或者行为和 Linux 的不一样。


文件锁flock、lockf和fcntl区别总结:
1) flock是系统调用,为System V锁
2) fcntl是系统调用,lockf是基于fcntl实现的libc库函数,为posix锁
3) flock可以同时用于多线程和多进程互斥x86 Linux验证
4) 而lockf和fcntl只能用于多进程
5) 对于NFS,只能使用fcntl,而flock只能用于本地文件系统
6) flock只是建议性锁
7) fcntl可以实现强制性锁
8) flock只能对整个文件加锁
9) fcntl和lockf可以只加锁文件的指定部分
10) flock锁不会被fork出的子进程继承,对于dup得到的fd是递归的,对于open得到的fd是非递归的
11) fcntl锁会被fork出的子进程继承,对于open得到的fd是递归的
12) flock和file table entry相关,而不是fd
13) flock和fcntl锁互不影响,可同时时对同一个文件上锁,而不会死锁
查看锁信息: cat /proc/locks


flock命令参数:
-s为共享锁,在定向为某文件的FD上设置共享锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置独占锁的请求失败,而其他进程试图在定向为此文件的FD上设置共享锁的请求会成功。

-e为独占或排他锁,在定向为某文件的FD上设置独占锁而未释放锁的时间内,其他进程试图在定向为此文件的FD上设置共享锁或独占锁都会失败。只要未设置-s参数,此参数默认被设置。

-u手动解锁,一般情况不必须,当FD关闭时,系统会自动解锁,此参数用于脚本命令一部分需要异步执行,一部分可以同步执行的情况。

-n为非阻塞模式,当试图设置锁失败,采用非阻塞模式,直接返回1,并继续执行下面语句。

-w设置阻塞超时,当超过设置的秒数,就跳出阻塞,返回值设置为1,并继续执行下面语句。

-o必须是使用第一种格式时才可用,表示当执行command前关闭设置锁的FD,以使command的子进程不保持锁。

-c执行其后的comand。

例子:
$ cat mylockfile.sh
#!/bin/sh
set -x
#exec 6<>"mylockfile.sh"
{
    flock -n 6
    #[ "$?" -eq "1" ] && {echo "fail";exit;}
    if [ "$?" -eq "1" ] ; then
        echo "fail";
        exit;
    fi
    echo $$
    sleep 10
} 6<>"mylockfile.sh"

$ chmod a+x mylockfile.sh

第一个终端:
$ sh mylockfile.sh
+ flock -n 6
+ '[' 0 -eq 1 ']'
+ echo 27292
27292
+ sleep 10

睡眠10秒钟,同时我在另一个终端上运行这个脚本会出现如下情形:

第二个终端:
$ sh mylockfile.sh
+ flock -n 6
+ '[' 1 -eq 1 ']'
+ echo fail
fail
+ exit

所以理解-n选项后就会明白了。


本文总结自互联网,感谢各位。


参考来源:
初识fcntl文件操作
Linux编程中的文件锁之flock
文件锁:解密 flock 和 fcntl 的区别
Linux的进程间通信:文件和文件锁
Linux中fcntl()、lockf、flock的区别
The Linux Programming Interface中的Chapter 4和Chapter 55
Advanced Programming in the UNIX® Environment, Third Edition中的Chapter 14.3