第55章 文件加锁

        前面章节介绍了进程能用来同步的各项技术,包括信号(20章--22章)和信号量(47章和53章)。本章将介绍专门为文件设计的同步技术。

55.1 概述

        应用程序的一个常见需求是从一个文件中读取一些数据,修改这些数据,然后将这些数据协会文件。只要在一个时刻只有一个几次呢很难过以这种方式使用文件就不会存在问题,但当多个进程同时更新一个文件时的问题就出现了。假设各个进程按照下面的顺序来更新一个包含了一个序号的文件。

  1. 从文件中读取序号
  2. 使用这个序号完成应用程序定义的任务。
  3. 递增这个序号并将其写回文件。

        这里存在地问题是两个进程在没有采用任何同步技术地情况下可能会同时执行上面地步骤从而导致(举例)出现图55-1中给出地结果(这里假设序号地初始值为1000)。

        问题很明显:在执行完上述步骤之后,文件中包含地值为1001,但其所包含地值应该是1002.(这是一种竞争条件。)为防止出现这种情况就需要采用某种形式地进程间同步。

        尽管可以使用(比如说)信号量来完成所需地同步,但通常文件锁更好一些,因为内核能够自动与文件关联起来。

        本章将介绍两组不同地给文件加锁地API.

  • flock()对整个文件加锁
  • fcntl()对一个文件区域加锁。

        flock()系统调用源自BSD,而fcntl()则源自 System V.

        使用flock()和fcntl(0地常规方法如下。

  1. 给文件加锁。
  2. 执行文件I/O
  3. 几所文件使得其他进程能够给文件加锁。

        尽管文件夹所通常会与文件I/O一起使用,但也可以作为一项更通用地同步技术来使用。写作进程可以约定一个进程对整个文件或一个文件区域进行加锁表示对一些共享资源(如一个共享内存区域)而非文件本身地访问。

混合使用加锁和stdio函数

        由于stdio库会在用户空间进行缓冲,因此在混合使用stdio函数与本章介绍地加锁技术时需要特别小心。这里地问题是一个输入缓冲器在被加锁之前可能会被填满或者一个输出缓冲器在锁被删除之后可能会被刷新。要避免这些问题则可以采用下面地方法。

  • 使用read()和write()(以及相关调用)取代stdio库来执行文件I/O。
  • 在对文件加锁之后立即刷新stdio流,并且在释放锁之前立即再次刷新这个流
  • 使用setbuf()(或类似地函数)来禁用stdioo缓冲,当然这可能会牺牲一些效率。

劝告式和强制式加锁

        在本章剩余部分会将锁分成劝告式和强制式两种。在默认情况下,文件锁是劝告式地,这表示一个进程可以简单地忽略另一个进程在文件上放置地锁。要使得劝告式枷锁模型能够正常工作,所有访问文件地进程都必须要配合,即在执行文件I/O之前首先在文件上放置一把锁。与之对应地是,强制式加锁会强制一个进程在执行I/O时需要最从其他进程持有地锁。

55.2 使用flock()给文件加锁

        尽管fcntl()提供地功能涵盖了flock()提供地功能,但这里仍然需要对齐进行介绍,因为在一些应用程序中仍然使用者flock()并且其在继承和锁释放方面地一些语义与fcntl()是不同地。

#include <sys/file.h>
int flock(int fd,int operation);
        Return 0 on success, or -1 on error

        flock()系统调用在整个文件上放置一个锁。待加锁的文件是通过传入fd地一个打开着地文件描述符来指定地,operation参数指定了表55-1中描述地LOCK_SH、LOCK_EX以及LOCK_UN值中地一个。

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

         任意数量的进程可同时持有一个文件上的共享锁,但在同一个时刻只有一个进程能够持有一个文件上的互斥锁。(换句话说,互斥锁会拒绝其他进程的互斥和共享锁请求。)表55-2对flock(0锁的兼容规则尽心了总结。这里假设进程A首先放置了锁,表中给出了进程B是否能够放置一把锁。

         不管一个进程在文件上的访问模式是什么(读、写或读写),他都可以在文件上放置一把共享锁或互斥锁。

        如果再次调用flock()并在opreation参数中指定恰当的值可以将一个既有共享锁转换成一个互斥锁(反之亦然)。将一个共享锁转换成一个互斥锁,在另一个进程中持有了文件上的共享锁时会阻塞,除非指定了LOCK_NB标记。

        锁转换的过程不一定是原子的。在咋混换过程中首先会删除既有的锁,然后创建哪一个新锁。在这两步之间另一个进程对一个不兼容锁的未决请求可能会得到满足。如果发生了这种情况,那么转换过程会被阻塞,或者在制定了LOCK_NB的情况下转换过程会失败并且进程会对是原先持有的锁。

        程序清单55-1演示了如何使用flock().这个程序对一个文件夹所,睡眠指定的秒数,然后对文件解锁。程序接收三个命令行参数,其中第一个参数是待加锁的文件,第二个参数指定了锁的类型(共享或互斥)以及是否包含LOCK_NB(非阻塞)标记,第三个参数指定了在获取和释放锁之间睡眠的秒数,并且这个参数是可选的,其默认值是10秒。

程序清单55-1:使用flock()---t_flock.c

#include <fcntl.h>
#include <sys/file.h>
#include <malloc.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>

#define BUF_SIZE 1000
char * currTime(const char *format)
{
    static char buf[BUF_SIZE];  /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL)
        return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}

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)
    {
        printf("%s file lock [sleep-time]\n"
                "   'lock' is 's' (shared) or 'x' (exclusive)\n"
                "       optionally follwed by 'n'(nonblocking)\n"
                "   'secs' specifies time to hold lock\n",argv[0]);
        return -1;
    }
    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)
    {
        perror("open :");
        return -1;
    }

    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)
        {
            printf("PID %ld:already locked-bye!\n",(long)getpid());
            return -1;
        }else
        {
            printf("flock (PID=%ld)\n", (long) getpid());
            return -1;
        }
    }
    printf("PID %ld:granted %s at%s\n",(long)getpid(),lname,currTime("%T"));
    sleep((argc >3)?atoi(argv[3]):10);
    printf("PID %ld:releasing %s at%s\n",(long)getpid(),lname,currTime("%T"));

    if(flock(fd,LOCK_UN) == -1)
    {
        perror("flock22:");
        return -1;
    }

    exit(EXIT_SUCCESS);
}

        使用程序清单55-1 中的程序可以展开一些实验来研究flock()的行为。下面给给出了其中一些例子。下面首先创建一个文件,然后在后台启动一个实例并持有共享锁60秒。

 接着启动另一个能够请求一个共享锁的程序实例,然后释放这个共享锁。

 但当启动另一个程序实例来非阻塞地请求一个互斥锁时程序就会立即失败

 当启动另一个程序实例来阻塞地请求一个互斥锁时程序就会阻塞。当原来持有共享锁地后台进程在60S之后释放这个锁,被阻塞地请求就会得到满足。

 55.2.1 锁继承与释放地语义

        根据表55-1,通过flock()调用并将opreation参数指定为LOCK_UN可以释放一个文件锁。此外,索贿在相应地文件描述符被关闭之后自动被释放。但问题其实要更加复杂,通过flock(0获取地文件锁是与打开地文件描述符(5.4)而不是文件描述符或文件(i-node)本身相关联地。这意味着当一个文件描述符被复制时(通过dup()、dup2()或一个fcntl()F_DUPFD操作),新文件描述符会引用同一个文件锁。例如如果获取了fd所引用地文件上地一个锁,那么下面地代码(忽略了错误检查)会释放这个锁。

flock(fd,LOCK_EX);  //Gain lock via 'fd'
newfd = dup(fd);   //'newfd' refers to same lock as 'fd'
flock(newfd,LOCK_UN);   //Frees lock acquired via 'fd'

         如果已经通过了一个特定地文件描述符获取了一个锁并创建了该文件描述符地一个或多个副本,那么---如果不显式地执行一个解锁操作---只有当所有地描述符副本都被关闭之后锁才会被释放。

        如果使用open()获取第二个引用同一个文件地文件描述符(以及关联地打开地文件描述符),那么flock()会将第二个描述符当成是一个不同地描述符。例如执行下面这些代码地进程会在第二个flock()调用上阻塞。

fd1 = open("a.txt",O_RDWR);
fd2 = open("a.txt",O_RDWR);
flock(fd1,LOCK_EX);
flock(fd2,LOCK_EX);

        这样一个进程就能够使用flock()来京自己锁在一个文件之外。使用fcntl()返回地记录锁是无法取得这种效果地。

        当使用fork()创建一个子进程时。这个子进程会复制其父进程地文件描述符,并且与使用dup(0调用之类地函数赋值的描述符一样,这些描述符会引用同一个打开为文件描述,进而会引用同一个锁。例如下面的代码会导致一个子进程删除一个父进程的锁。

flock(fd,LOCK_EX)  //Parent obtains lock
if(fork()==0)       //If child,..
    flock(fd,LOCK_UN)   //Release lock shared with parent

        有时候可以利用这些语义来将一个文件锁从父进程(原子的)传输到子进程:在fork()之后,父进程关闭其文件描述符,然后所就只在子进程的控制之下了。读者稍后就会看到用fcntl()返回的记录锁时复发取得这种效果的。

        通过flock()创建的锁在exec()中会得到保留(除非在文件描述符上设置了close-on-exec标记并且该文件描述符是最后一个引用底层的打开的文件描述的描述符)。

        上面秒数的flock()在Linux上的语义与其在经典的BSD实现上的语义是一致的。在一些UNIX实现上,flock()时使用fcntl()实现的,fcntl()锁的继承和释放语义与flock()锁的继承与释放语义是不同的。由于flock()创建的锁与fcntl()创建的锁之间的交互是未定义的,因此应用程序应该只使用其中一种文件加锁方法。

55.2.2 flock()的限制

        通过flock()放置的锁存在几个限制。

  • 只能对整个文件加锁。这种粗粒度的加锁会限制写作技能成之间的并发性。例如,假设存在多个进程,其中各个进程都想要同时访问同一个文件的不同部分,那么通过flock()加锁会不必要地阻止这些进程并完成这些操作。
  • 通过flock()只能放置劝告式锁
  • 很多NFS实现不识别flock(0放置地锁。

        因为历史原因,Linux NFS服务器不支持flock()锁。从内核2.6.12起,Linux NFS服务器通过将flock()锁实现成整个文件上的一个fcntl()锁来支持flock()锁。这种做法在混合服务器上的BSD锁和客户端上的BSD锁时会导致一些奇怪的结果:客户端通常无法看到服务器的锁,反之亦然。

55.3 使用fcntl()给记录加锁

        使用fcntl()(5.2节)能够在一个文件的人一部分上放置一把锁,这个文件部分既可以是一个字节,也可以是整个文件。这种形式的文件加锁通常被称为记录枷锁,但这种称为是不恰当的,因为UNIX 系统上是一个字节续流,并不存在记录边界的概念,文件记录的概念只存在于引用程序中。

        一般来讲,fcntl()会被用来锁住文件中与应用程序定义的记录边界对应的字节范围,这也是术语记录枷锁的由来。术语字节范围、文件区域以及文件段很少被用到,但他们更加精确地描述了这把锁。

        SUSv3 要求普通文件支持记录加锁,同时也允许其他文件类型也支持文件加锁。尽管记录锁通常只应用于普通文件时才有意义(因为对于大多数其他文件类型,讨论文件中所包含地数据地自己范围是毫无意义地),但是在Linux可以将一个记录锁应用在任意类型地文件描述符上。

        图55-2显示了如何使用记录锁来同步两个进程对文件中地同一块区域地访问。(在这幅图中假设所有地锁请求都会阻塞,这样他们在锁被另一个进程持有时就会等待。)

         用来创建或删除一个文件锁地fcntl()的调用的常规形式如下。

struct flock flockstr;
/*Set filds of 'flockstr' to describe lock to be placed or removed*/
fcntl(fd,cmd,&flockstr);  /*Place lock defined by f1*/

        fd参数是一个打开着的文件描述符,他引用了待加锁的文件。

        在讨论cmd参数之前首先描述一下flock结构。

flock结构

        flock结构定义了带获取或删除的锁,其定义如下所示。

struct flock{
    short l_type;  /*Lock type: F_RDLCK,F_WRLCK,F_UNLCK*/
    short l_whence;  /*How to interpret 'l_start':SEEK_SET,SEEK_CUR,SEEK_END*/

    short l_start;  /*Offset where the lock begins*/
    short l_len;  /*Number of bytes to lock; 0 means "until EOF"*/
    short l_pid;  /*Process prevent our lock (F_GETLK only)*/
}

        l_type字段表示需要放置的锁的类型,其取值为表55-3中列出的的值的一个。

        从语义上来讲,读(F_RDLCK)和写(F_WRLCK)锁对应于flock()施加的gongxiangsuo-和互斥锁,并且他们遵循着同样的兼容性规则(表55-2):任何数量的进程能够持有一块文件区域上的读锁,但只有一个进程能够持有一把写锁,并且这把锁会将其他进程的读锁和写锁排除在外。将l_type指定为F_UNLCK类似于flock()LOCK_UN操作。

 为了在一个文件上放置一把读锁就必须要打开文件以允许读取。类似地,要防止一把写锁就必须打开文件以允许写入。要防止两种锁就必须要打开文件以允许读写(O_RDWR)。试图在文件上放置一把与文件访问模式不兼容地锁将会导致一个IEBADF错误。

        l_whence、l_start以及l_en字段一起指定了待加锁的字节范围。前两个字段类似于传入lseek()的whence和offset参数(4.7节)。l_start字段指定了文件中的一个偏移量,其具体含义需根据下列规则来解释。

  • 当l_whence为SEEK_SET时,为文件的起始位置。
  • 当l_whence为SEEK_CUR时,为当前的文件偏移量
  • 当l_whence为SEEK_END时,为文件的结尾位置。

        在后两种情况中,l_start可以是一个负数,只要最终得到的文件位置不会小于文件的起始位置(字节0)即可。

        l_len字段包含一个指定待加锁的字节数的整数,其起始位置由l_whence和l_start定义。对文件结尾之后并不存在的字节进行加锁是可以的,但无法对文件起始之前的字节进行加锁。

        从内核2.4.21开始,Linux 允许在l_en中指定一个负值。这是请求对在l_whence和l_start指定的位置之前的l_len字节(即范围在(l_start-abs(l_en))到(l_start-1)之间的字节)进行加锁。SUSv3允许但并没有要求这种特性,其他几个UNIX实现也提供了这个特性。

        一般来讲,应用程序应该支队所需的最小自字节范围进行加锁,这样其他进程就能够同时对同一个文件的不同同区域进行加锁,进而取得更大的并发性。

在某些情况下需要对属于的最小范围进行限定。在诸如NFS和CIFS之类的网络文件系统上回合使用记录锁和mmap()调用会导致不期望的结果。之所以会发生这种问题是因为mmap()映射文件的单位是系统分页大小奥。如果一个文件锁是分页对齐的,那么所有一切都会正常工作,因为锁会覆盖与一个脏分页对应的整个区域。但如果锁没有分页对齐,那么就会存在一种竞争条件---当映射分页的人一部分发生变更之后内核可能就会写入未被锁覆盖的区域

        将l_len指定为0具有特殊含义,即“对范围从l_start和l_whence确定的起始位置到文件结尾位置之内的所有字节加锁,不管文件增长到多大”。这种处理方式在无法提前直到型一个文件中加入多少字节的情况下比较方便的。要锁住整个文件则可以将l_whence指定为SEEK_SET,并将l_start和l_en都指定为0.

cmd参数

        fcntl()在操作文件锁时其cmd参数的取值有以下三个,其中前两个值可以用来获取和释放锁。

F_SETLK

        获取(l_type是F_RDLCK或F_WRLCK)获释放(l_type是F_UNLCK)由flockstr指定的字节上的字节上的锁。如果另一个进程持有了一把待加锁的区域中任意部分上的不兼容的锁时,fcntl()就会失败并返回EAGAN错误。在一些UNIX实现上fcntl()在碰到这种情况时会失败并返回WACCES错误。SUSv3允许实现采用其中任意一种处理方式,因此可移植的应用应该对这两个值都测进行试。

F_SETLKW

        这个值与F_SETLK是一样的,除了在有另一个进程持有一把待加锁的区域中任意部分上的不兼容的锁时,调用就会阻塞直到锁的请求得到满足。如果正在处理一个信号并且没有指定SA_RESATRT(21.5),那么F_SETLKW操作就可能会被中断(即失败并返回EINTR错误)。开发人员可以利用这种行为来使用alarm()或setitimer()为一个加锁请求设置一个超时时间。

        注意,fnctl()要么会锁柱整个区域,要么就不会对任何字节加锁们这里并不存在只锁住请求区域中那些当前未被锁住的字节的概念。

        剩下的一个fcntl()操作可以用来确定是否可以在一个给定的区域上放置一把锁。

F_GETLK

        检测是否能够获取flockstr指定的区域上的锁《但实际上不获取这把锁。l_type字段的值必须为F_RDLCK或F_WRLCK。flockstr结构是一个值-结果参数,在返回时它包含了有关是否能够放置指定的锁的信息。如果允许加锁(即在指定的文件区域上不存在不兼容的锁),那么在l_type字段中会返回F_UNLCK,并且剩余的字段会保持不变。如果在区域上存在一个或多个不兼容的锁,那么flockstr会返回与那些所中其中一把锁(无法确定是哪把锁)相关的信息,包括其类型(l_type)、字节范围(l_start和len;l_whence总是返回为SEEK_SEK)以及持有这把锁的进程的进程ID(l_pid).

        注意在使用F_GETLK之后接着使用F_SETLK或F_SETLKW的话可能会出现竞争条件,因为在执行后面一个操作时,F_GETTLK返回的信息可能已经过时了,因此F_GETLK的实际作用比其一开始看起来的作用要小很多。即使F_GETLK表示可以放置一把锁,任然需要为F_SETTLK返回一个错误或F_SETLKW阻塞做好准备。

        GNU C库还实现了函数lockf(),它仅仅是一个fcntl()的简化接口。(SUSv3规定了lockf(),但并没有规定lockf()与fcntl()之间的关系。在大多数UNIX系统上,lockf()的实现都是基于fcntl()的。)形如lockf(fd,operation,size)的调用等价于在调用fcntl()时将l_whence设置为SEEK_CUR,l_start设置为0,以及将l_len设置为size,即lockf()将会锁住从当前文件偏移量开始到文件结束的字节序列。lock()的operation参数类似于fcntl()的cmd参数,但是用于获取、释放以及测试锁的存在性的常量值是不同的。lockf()只放置互斥锁。

锁获取和释放的细节

        有关获取和释放由fcntl()创建的锁方面需要注意以下几点。

  • 解锁一块文件区域总是立即成功。即当前并不持有一块区域上的锁,随着快区域加锁也不是一个错误。
  • 在任何一个时刻,一个进程只能持有一个文件的某个特定区域上的狱中锁。在之前已经锁住的区域上放置一把会导致不发生任何事情(心锁的类型与既有锁的类型是一样的)或原子地将既有锁转换成新模式。在后一种情况中,当将一个读锁转换成写锁时需要为调用返回一个错误(F_SETLK)或阻塞(F_SETLKW)做好准备。(这与flock()是不同地,他的锁转换不是原子地。)
  • 一个进程永远都无法将自己锁在一个文件区域之外,即使通过多个引用同一文件地文件描述符放置锁也是如此。(这与flock()是不同地)
  • 在已经持有地锁中间放置一把模式不同地锁会产生三把锁:在新锁两端会创建两个模式为之前模式更小一点地锁(图55-3).与此相反地是,获取与模式相同地一把既有锁相邻或重叠地第二把锁会产生单个覆盖两把锁地合并区域地聚合锁。除此之外,还存在其他的组合情况。如对一个大型既有锁地中间地一个区域进行解锁会在已解锁地区域两端产生两个更小一点地与所著区域。如果一个新锁与一个模式不同地既有锁重叠了,那么既有锁会收缩,因为重叠地字节会合并进新锁中。

  •  在文件区域锁方面,关闭一个文件描述符具备一些不寻常地语义,在55.3.5节会介绍

55.3.1 死锁

        在使用F_SETLKW时需要弄清楚图55-4中阐述的场景类别。在这种场景中,每个进程的第二个锁请求会被另一个进程持有的锁阻塞。这种场景被称为死锁。如果内核不对这种情况进行抑制,那么将会导致两个进程永远阻塞。为避免这种情况,内核会对F_SETLKW发起的每个新锁请求进行检查以判断是否会导致死锁。如果会导致死锁,那么内核会选中其中一个被阻塞的进程使其fcntl()调用接触阻塞并返回EDEADLK。(在Linux 上,尽成灰选中最近的fcntl()调用,但SUSv3并没有要求这种行为,并且这种行为在后续的Linux版本或其他Linux实现上可能不成立。使用F_SETLKW的所有进程都必须要为处理EDEADLK做准备。)

           即使在多个不同的文件上放置锁时也能检测书死锁情形,即涉及多个进程的循环思索。(举个例子,对于循环思索,意味着进程A等待获取北津城B锁住的区域上的锁,进程B等待进程C持有的锁,进程C等待进程A持有的锁。)

55.3.2 示例:一个交互式加锁程序

        程序清单55-2中的程序允许交互的试验记录枷锁。这个程序接收一个命令行参数:待加锁的文件名称。使用这个程序能够阿姨内政很多之前介绍的有关记录枷锁的论断。这个成被设计成了一个交互式程序并接受形如下面的命令。

cmd lock start length [whence]

        在cmd参数中可以指定g来执行一个F_GETLK,指定s来执行一个F_SETLK,或指定w来执行一个F_SETLKW.剩下的参数用来初始化传入fcntl()的flock结构。lock参数指定了l_type字段的取值,其中r表示F_RDLCK,W表示F_WRLCK, u表示F_UNLCK。start和length参数是整数,他们指定了l_start和len字段的取值。最后一个是可选的wnence参数,它指定了l_whence字段的取值,其中s表示SEEK_SET(默认值),c表示SEEK_CUR,e表示SEEK_END。

程序清单55-2 实验记录枷锁------i_fcntl_locking.c

        

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define MAX_LINE 100
static void displayCmdFmt(void)
{
    printf("\n    Format: cmd lock start length [whence]\n\n");
    printf("    'cmd' is 'g' (GETLK), 's' (SETLK), or 'w' (SETLKW)\n");
    printf("    'lock' is 'r' (READ), 'w' (WRITE), or 'u' (UNLOCK)\n");
    printf("    'start' and 'length' specify byte range to lock\n");
    printf("    'whence' is 's' (SEEK_SET, default), 'c' (SEEK_CUR), "
           "or 'e' (SEEK_END)\n\n");
}

int main(int argc, char *argv[])
{
    int fd,numRead,cmd,status;
    char lock,cmdCh,whence,line[MAX_LINE];
    struct flock fl;
    long long len,st;

    if(argc !=2 || strcmp(argv[1],"--help") == 0)
    {
        printf("%s file\n",argv[0]);
        return -1;
    }

    fd = open(argv[1],O_RDWR);
    if(fd == -1)
    {
        perror("open:");
        return -1;
    }
    printf("Enter ? for help\n");
    for(;;){ /*Prompt for locking command and carry it out*/
        printf("PID=%ld> ",(long)getpid());
        fflush(stdout);

        if (fgets(line, MAX_LINE, stdin) == NULL)       /* EOF */
            exit(EXIT_SUCCESS);
        line[strlen(line) - 1] = '\0';          /* Remove trailing '\n' */

        if (*line == '\0')
            continue;                           /* Skip blank lines */

        if (line[0] == '?') {
            displayCmdFmt();
            continue;
        }

        whence = 's';                   /* In case not otherwise filled in */

        numRead = sscanf(line, "%c %c %lld %lld %c", &cmdCh, &lock,
                        &st, &len, &whence);
        fl.l_start = st;
        fl.l_len = len;

        if (numRead < 4 || strchr("gsw", cmdCh) == NULL ||
                strchr("rwu", lock) == NULL || strchr("sce", whence) == NULL) {
            printf("Invalid command!\n");
            continue;
        }

        cmd = (cmdCh == 'g') ? F_GETLK : (cmdCh == 's') ? F_SETLK : F_SETLKW;
        fl.l_type = (lock == 'r')?F_RDLCK:(lock == 'w')?F_WRLCK:F_UNLCK;
        fl.l_whence = (whence == 'c')?SEEK_CUR:
                      (whence == 'e')?SEEK_END:SEEK_SET;
        status = fcntl(fd,cmd,&fl);  /*Perform request.....*/
        if(cmd == F_GETLK){
            if(status == -1){
                printf("fcntl - F_GETLK");
            }else{
                if(fl.l_type == F_UNLCK)
                    printf("[PID=%ld] Lock can be placed\n",(long)getpid());
                else
                    printf("[PID=%ld] Denied by %s lock on %lld:%lld "
                            "(held by PID %ld)\n", (long) getpid(),
                            (fl.l_type == F_RDLCK) ? "READ" : "WRITE",
                            (long long) fl.l_start,
                            (long long) fl.l_len, (long) fl.l_pid);
            }
        } else {                /* F_SETLK, F_SETLKW */
            if (status == 0)
                printf("[PID=%ld] %s\n", (long) getpid(),
                        (lock == 'u') ? "unlocked" : "got lock");
            else if (errno == EAGAIN || errno == EACCES)        /* F_SETLK */
                printf("[PID=%ld] failed (incompatible lock)\n",
                        (long) getpid());
            else if (errno == EDEADLK)                          /* F_SETLKW */
                printf("[PID=%ld] failed (deadlock)\n", (long) getpid());
            else
                printf("fcntl - F_SETLK(W)");
        }
    }
}

        在下面的shell 会话日志中演示了如何使用程序清单55-2中的程序,其中运行了两个实例来在同一个大小为100字节的文件上放置锁。图55-5给出了shell会话日志中各个点上准予和排队的加锁请求的咋黄台并在下面的注释中进行标注。

        首先启动程序清单55-2中的程序的第一个实例(进程A)并在文件中0~39字节区域上放置一把锁。

         接着启动程序的第二个实例(进程B)并在文件中第70个自己到文件结尾的区域上放置一把读锁。

         刺客出现了图55-5中a部分的情形,其中进程A(进程ID为790)和进程B(进程ID为800)持有了文件的不同部分上的锁。

现在回到进程A让其尝试在整个文件上放置一把写锁。首先通过F_GETLK检测是否可以加锁并得到存在一个冲突的锁的信息。接着尝试通过F_SETLK放置一把锁,但这个操作也会失败。最后尝试通过F_SETLKW放置一把锁,这次将会阻塞。

         此刻出现了图55-5中b部分的情形,其中进程A和进程B分别持有了文件的不同部分上的锁,并且进程A还有一个排着队的对整个文件的加锁请求。

        接着继续在进程B中尝试在整个文件上放置一把写锁。首先使用F_GETLK检测一下是否可以加锁并得到存在一个冲突的锁的信息。接着尝试使用F_SETLKW加锁。

        图55-5中的c部分给出了当进程B发起一个在整个文件上放置一把锁的阻塞请求发生的情形:死锁,此刻内核会选择其中一个加锁请求失败----在本例中进程B的请求将会被选中并从其fcntl()调用接收到EDEADLK错误。

        接着持续在进程B中删除其在文件上的所有锁。 

 

        从上面输出最后一行中可以看出进程A的被阻塞的加锁请求被准予了。

        重要的一定是,需要意识到即使进程B的思索请求被取消之后他仍然持有了其它的锁,因此进程A的排着队的加锁请求仍然会被阻塞。进程A加锁请求只有在进程B删除了其持有的锁之后才会被准予,这就出现了图55-5中d部分的情形。

55.3.3 示例:一个加锁函数库 

        程序清单55-3给出了一组在其他程序中可以使用的加锁函数,如下所示。

  • lockRegion()函数使用F_SETLK在文件描述符fd引用的打开着的文件位置上放置一把锁。type参数指定了锁的而类型(F_RDLCK或F_WRLCK)。whence、start以及len参数指定type参数指定了需加锁的字节范围。这些参数为用爱枷锁的flockstr结构中名称类似的字段提供了值。
  • lockRegionWait()函数与lockRegion()类似,但他发起的是一个阻塞式的加锁请求,即它使用了F_SETLKW而不是F_SETTLK.

  •  regionIsLocked()函数检测是否可以在一个文件上放置一把锁。这个函数的参数与lockRegion函数接收的参数是一样的。这个函数在没有进程持有与调用中指定的锁冲突时将返回0.如果存在一个进程持有了冲突的锁,那么这个函数就会返回一个非0值(即true)---持有冲突锁的进程ID。

程序清单55-3:文件区域加锁函数----region_locking.c

/* Listing 55-3 */

#include <fcntl.h>
#include <sys/types.h>
/* Lock a file region (private; public interfaces below) */

static int
lockReg(int fd, int cmd, int type, int whence, int start, off_t len)
{
    struct flock fl;

    fl.l_type = type;
    fl.l_whence = whence;
    fl.l_start = start;
    fl.l_len = len;

    return fcntl(fd, cmd, &fl);
}

int                     /* Lock a file region using nonblocking F_SETLK */
lockRegion(int fd, int type, int whence, int start, int len)
{
    return lockReg(fd, F_SETLK, type, whence, start, len);
}

int                     /* Lock a file region using blocking F_SETLKW */
lockRegionWait(int fd, int type, int whence, int start, int len)
{
    return lockReg(fd, F_SETLKW, type, whence, start, len);
}

/* Test if a file region is lockable. Return 0 if lockable, or
   PID of process holding incompatible lock, or -1 on error. */

pid_t
regionIsLocked(int fd, int type, int whence, int start, int len)
{
    struct flock fl;

    fl.l_type = type;
    fl.l_whence = whence;
    fl.l_start = start;
    fl.l_len = len;

    if (fcntl(fd, F_GETLK, &fl) == -1)
        return -1;

    return (fl.l_type == F_UNLCK) ? 0 : fl.l_pid;
}

55.3.4 锁的限制和性能

        SUSv3允许一个实现为所能获取记录锁的数量设置一个固定的、系统级别的上限。当达到这个限制时,fcntl()就会失败并返回ENOLCK错误。Linux并没有为所能获取的记录锁的数量设置一个固定的上限,至于具体数量则受限于可用的内存数量。(很多其他UNIX实现也采用了类似的做法。)

        获取和释放记录锁的速度有多快呢?这个问题没有固定的答案,因为这些操作的速度取决于用来维护记录锁的内核数据结构和具体的某一把锁在这个数据结构中所处的位置。本章稍后就会介绍这个数据结构,在此之前首先来考虑急待你能够影响其设计的需求。

  • 内核需要能够将一个新锁和任意位于新锁任意一端的模式相同的既有锁(由同一个进程持有)合并起来。
  • 新锁可能会完全取代调用进程持有的一把或多把持有锁,内核需要容易地定位出所有这些锁。
  • 当在一把既有锁地中间创建一个模式不同的新锁时,分割既有锁地工作(图55-3)应该是比较简单地。

        用来维护锁相关信息地内核数据结构需要被设计成满足这些要求。每个大开着地文件都有一个关联链表,链表中保存着文件上地锁。列表中地锁会先按照进程ID再按照起始偏移量来排序。图55-6给出了这样地列表。

        内核在与一个打开着地文件相关联地锁链表中维护着flock()锁与文件租用。(在55.5节中讨论/proc/locks文件时将会对文件租用进行简要介绍。)但这种类型地锁地数量通常要小很多很多。因此不大可能会对性能产生影响。

        每次需要在这个数据结构中添加一把新锁时,内核都必须要检查是否与文件上地既有锁有冲突。这个搜索过程是从列表头开始顺序开展地。

        假设有大量地锁随即地分布在很多进程中,那么就可以说,添加或删除一个锁所需地时间与文件上已有锁地数量之间大概是一个线性关系。

55.3.5 锁继承和释放地语义 

        fcntl()记录锁继承和释放地语义与使用flock()创建地锁地继承和释放的语义是不同的,以下几点需要注意。

  • 由fork()创建的子进程不会继承记录锁。这与flock()是不同的,在使用flock()创建的锁时,子进程会继承一个引用同一把锁的引用并且能够释放这把锁,同而导致父进程也会失去这把锁。
  • 记录锁在exec()中会得到保留。(但需要注意下面描述的close-on-exec标记的作用。)
  • 一个进程中的所有线程会共享同一组记录锁。
  • 记录锁同时与一个进程和一个i-node(5.4节)关联,从这种关联关系中可以得出一个毫不意外的结果就是当一个进程终止之后,其所有记录锁会被释放。另一个稍微有点出乎意料的结果是当一个进程关闭了一个文件描述符之后,进程持有的对应文件上的所有锁都会被释放,不管这些锁是通过那个文件描述符获得的。例如下面的代码中,close(fd2)调用会释放调用进程持有的testfile文件之上的锁,尽管这些锁是通过文件描述符fd1获得的。
struct flock fl;
fl._ltype = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;

fd1 = open("testfile",O_RDWR);
fd2 = open("testfile",O_RDWR);

if(fcntl(fd1,cmd,&fl) == -1)
{
    perror("fcntl:");
    return -1;
}

close(fd2);

        不管引用同一个文件的各个描述符是如何获得的以及不管描述符是如何被关闭的,上面最后一点中描述的语义都是适用的。例如dup()、dup2()以及fcntl()都可以用来获取一个打开着的文件描述符的副本。除了执行一个显式的close()之外,一个描述符在设置了close-on-exec标记时会被一个exec()调用关闭,或者也可以通过一个dup2()调用来关闭其第二个文件描述符参数,当然前提是该描述符已经被打开了。

        fcntl()锁的继承和释放语义是一个架构上的缺陷。例如他们使得使用库包中的记录锁容易发生问题,因为一个库函数无法阻止调用者关闭一个引用了一个被锁住的文件的文件描述符,从而会导致删除一个通过库代码获得的锁。另一种可选的实现方案是将锁与文件描述符关联起来,而不是与i-node关联起来。但之所以采用当前这种语义是存在历史原因的,并且这种语义现在已经变成了记录锁的标准行为。遗憾的是,这些语义会极大地限制fcntl()加锁工具的实用性。

        在使用flock()时,一把锁只会与一个打开的文件描述关联,并且会持续发挥作用直到持有这把锁的引用任意进程显式地释放这把锁或所有引用这个打开着地文件描述地文件描述符被关闭之后为止。

55.3.6 锁定饿死和排队地加锁请求地优先级

        当多个进程必须要等待以便能够在当前被锁住地区域上放置一把锁时,一些列地问题出现了。

        一个进程是否能够等待以便在由一系列进程放置读锁地同一块区域上放置一把写锁并因此可能会导致饿死?在Linux上(以及很多其他UNIX实现上),一些列地读锁确实能够导致一个被阻塞地写锁饿死,甚至会无限地饿死。

        当两个或多个进程等待放置一把锁时,是否存在一些规则来确定在锁可用时哪个进程会获取锁?例如,锁清秋是否满足FIFO顺序?规则跟每个进程请求地锁地类型是否有关系(即一个请求读锁地进程是否会优先于请求一个写锁地进程,或反之亦然,或都不是)?在Linux上地规则如下描述。

  • 排队地锁请求被准予地顺序是不确定地。如果多个进程正在等待加锁,那么他们被满足地顺序取决于进程的调度。
  • 写者并不比读者拥有更高的优先权,反之亦然。 

        在其他系统桑这些论断可能就是不正确的了。在一些UNIX实现上,锁请求的服务是按照FIFO的顺序来完成的,并且读者比写者拥有更高的优先权。

55.4 强制加锁 

        到目前为止介绍的锁都是劝告式锁。这意味着一个进程可以自由地忽略fcntl()(或flock())地使用或简单地在文件上执行I/O.内核不会组织进程地这种行为。在使用劝告式锁时,应用程序地设计者需要:

  • 为文件设置合适地所有权(或组所有权)以及权限以防止非协作进程执行I/O;
  • 通过在执行I/O之前获取恰当地锁来确保构成应用程序地进程相互协作。

        与其他UNIX实现一样,Linux 也允许fcntl()记录锁是强制式地。这表示需对每个文件I/O操作进行检查以判断其他进程在执行I/O所在地文件区域上是否有任何不兼容地锁。

        为了在Linux 上使用强制式加锁就必须要在包含待加锁地文件地文件系统以及每个待加锁地文件上启用这一功能。通过在挂在文件系统时使用(Linux 特有地)-omand 选项能够在该文件系统上启用强制式加锁。

# mount -o mand /dev/sda10 /testfs

        在程序中可以通过在调用mount(2)(14.8.1节)时指定MS_MANDLOCK标记来取得同样的而结果。

        通过查看不带任何选项的mount(8)命令的输出就能看出一个挂载文件系统是否启用了强制式加锁。

#mount | grep sda10
/dev/sda10 on /testfs type ext3 (rw,mand)

        文件上那个强制加锁的启用是通过开启set-group-ID权限位和关闭group-execute权限来完成的。这种权限位组合在其他场景中是毫无意义的,并且在之前的UNIX实现中并没有用到这种权限位组合。正因为如此,后面的UNIX系统再新增强制式加锁时就无需修改既有程序或添加新的系统调用了。在shell中可以按照下面的方法在一个文件上启用强制式加锁。

$ chmod g+s,g-x /testfs/file

        在一个程序中可以通过使用chmod()或fchmod()(15.4.7节)恰当地设置文件上的权限来启用该文件上的强制式加锁。

        当显式一个启用了强制式加锁权限位的文件的权限时,ls(1)会在froup-execute权限列中会显示一个S。

         所有原生Linux和UNIX文件系统都支持强制式加锁,但一些网络文件系统和非UNIX文件系统可能就不支持强制式枷锁了。例如微软的VFAT文件系统没有set-group-ID权限位,因此在VFAT文件系统上就无法启用强制式加锁了。

强制式加锁对文件I/O操作的影响

        如果在一个文件上启用强制式加锁时,那么执行数据传输的系统调用(如read()或write())在碰到锁冲突(即在当前被读或写操作锁住的区域上执行一个写入操作或在当前被写锁住的区域上执行一个读操作)时会发生什么呢?这个问题的答案取决于是以阻塞模式还是非阻塞模式打开了文件。如果以阻塞模式打开了文件,那么系统调用就会阻塞,如果在打开文件时使用了O_NONBLOCK标记,那么系统调用就会立即失败并返回EAGAIN错误。类似的规则同样适用于truncate()和ftruncate(),前提是他们尝试从中增加或删除字节的文件当前被另一个进程锁住(为了读或者写)了。

        如果以非阻塞模式打开了一个文件(即在open调用中没有指定O_NONBLOCK),那么I/O系统调用可能会导致死锁情形的出现。考虑图55-7中给出的例子,其中两个进程都打开了同一个文文件以执行阻塞式I/O,他们先获取了文件中不同部分上的写锁,然后分别尝试写入被对方锁住的区域。内核在解决这个问题时采用的方式与解决由两个fcntl()调用引起的死锁问题时所用的方式是一样的(55.3.1节):他选择死锁锁涉及到的其中一个进程并使其write()系统调用失败(返回EDAGAIN错误 )。

        如果存在进程持有了 一个文件人一部分上的强制式读锁和谐所,那么就无法在该文件上创建一个共享内存映射(即在调用mmap()时指定了MAP_SHARED标记)。同样,如果一个文件参与了一个共享内存映射,那么就无法在该文件的人一部分上放置一把强制式锁。在这两种情况中,相关的系统调用会立即返回失败并返回EAGAIN错误。之所以存在这些限制的原因在考虑内存映射的实现之后就变得清晰起来了。在49.4.2节中曾经介绍过一个即从文件中读取又向文件写入的共享内存映射(特别时后一个操作会与文件上任意类型的锁产生冲突)。此外,这种文件I/O是通过内存管理子系统完成的,而这个子系统是不清楚系统中任意一个文件锁所处的位置的。因此为防止一个映射更新一个被放置了强制式锁的文件,内核需要执行一个简单的检查---在执行mmap()调用时检查待映射的文件中所有位置上是否存在锁(对于fcntl()调用也是如此)。

强制式加锁警告

        强制式锁所起的作用其实没有一开始看起来那么大,它存在一些潜在的缺陷和问题。

  • 在一个文件上持有一把强制式锁并不能组织其他进程删除这个文件,因为只要在父母路上拥有合适的权限就能够与一个文件断开连接。
  • 在一个可公开访问的文件上启用强制式锁之前需要经过深思熟虑,因为即使特权进程也无法覆盖一个强制式锁。恶意用户可能会持续地持有该文件上地锁以制造拒绝服务地攻击。(在大多数情况下可以通过关闭set-group-ID位来使得该文件可再次访问,但强制式文件锁造成系统挂起时就无法这样做了。)
  • 使用强制式加锁存在性能开销。在启用了强制式加锁地文件上执行地每个I/O系统调用中,内核都必须检查在文件上是否存在冲突地锁。如果文件上存在大量地锁,那么这种检查工作会极大地降低I/O系统调用地效率。
  • 强制式加锁还会在应用程序涉及阶段造成额外地开销,因为需要处理每个I/O系统调用返回EAGAIN(非阻塞I/O)或EDEADLK(阻塞I/O)错误地情况。
  • 因为在当前地Linux实现中存在一些内核竞争条件,因此在有些情况下执行I/O操作地系统调用在文件上存在本应该拒绝这些操作地强制式锁也能成功。 

        总地来说,应该比卖你使用强制式加锁。 

55.5 /proc/lock文件 

        通过检查Linux特有地/proc/locks文件中地内容能够查看系统中当前存在地锁,下面给出一个示例文件所包含地信息。

        /proc/locks文件显示了使用flock()和fcntl()创建地锁地相关信息。每把锁地8个字段地含义如下(从左至右)。

  1. 锁在该文件上锁有锁中地序号(55.3.4节)
  2. 锁地类型。其中FLOCK表示flock创建地锁,POSIX表示fcntl()创建地锁。
  3. 锁地模式,其值是 ADVISORY 或MANDATORY。
  4. 锁地类型,其值是READ或WRITE(对应于fcntl()的共享锁和互斥锁)。
  5. 持有锁的进程的进程ID
  6. 三个用冒号分割的数字,他们标识出了锁所属的文件。这些数字是文件所处的文件系统的主要和次要设备号,后面跟着文件的i-node号。
  7. 锁的起始字节。对flock()锁来讲其值永远是0.
  8. 锁的末尾字节,七张EOF表示锁眼神到文件的结尾(即对于fcntl()创建的锁来讲是将l_len指定为0).对于flock()锁来讲,这一列的值永远是EOF

        使用/proc/locks中的信息能够找出哪个进程持有了哪个文件上的锁。下面的shell会话显式了如何找出上面列表中的序号为3的的锁的此类信息。这个锁由进程ID为967所有,其中所属的文件在主要ID为8、次要ID为1的设备上的第789801个i-node上下面首先使用ps(1)列出进程ID为967的进程的相关信息.

         从上面的输出可以看出持有锁的程序是snapd.

        为找出被锁住的文件,下面首先在/dev目录中搜索文件并确定ID为8:1的设备是/dev/sda7

接着确定设备/dev/sda7的挂载点并在该部分文件系统中搜索i-node号为789801的文件。

 

find -mount 选项放置find进入/下的子目录(表示其他文件系统的挂载点)进行搜索。

55.6 仅运行一个程序的单个实例

         一些程序---特别是很多daemon--需要确保同一时刻只有一个程序实例在系统中运行。完成这项任务的一个常见方法是让daemon在一个目标目录中创建一个文件并在该文件上放置一把锁。daemon在其执行期间一直持有这个文件锁并在即将终止之前删除这个文件。如果启动了damon的另一个实例,那么他在获取该文件上的写锁时就会失败,其结果是他会意识到daemon的另一个实例肯定正在运行,然后终止。

很多网络服务器采用了另一种常规做法,即当服务器绑定的众所周知的socket端口号已经被使用时就认为该服务器实例已经处于运行状态了。

/var/run目录通常是存放此类锁文件的位置。或者也可以在daemon的配置文件中加一行来指定文件的位置。

        通常,daemon会将其进程ID写入所文件,因此这个文件在命名时通常将.pid作为扩展名(如syslogd会创建文件/var/run/syslogd.pid)。这些对于那些需要找出daemon的进程ID的应用程序来讲是比较有用的。它还允许执行额外的健全检查---可以向20.5节描述的那样使用kill(pid,0)来检查进程ID是否存在。

        用来创建和锁住一个进程ID锁文件的代码存在很多微笑的差异。程序清单55-4提供了一个函数createPidFile(),它封装了上面描述的步骤。调用这个函数通常会使用下面这样的代码。

if(createPidFile("mydaemon","/var/run/mydaemon.pid",0) == -1)
{
    printf("createPidFile:");
    retutn -1;
}

         createPidFile()函数中的一个精妙之处是使用ftruncate()来清除锁文件中之前存在的所有字符串。之所以要这样做是因为daemon的上一个实例在删除文件时可能因系统崩溃而失败。在这种情况下,如果新daemon实例的进程ID较小,那么可能就无法完全覆盖之前文件中的内容。例如,如果进程ID是789,那么就只会像文件写入789\n,但之前的daemon实例可能已经向文件写入了12345\n,这时如果不截断文件的话得到的内容就会使789\n5\n.从严格意义上来讲,清楚所有既有字符串并不是必需的,但这样做显得更加简洁并且能够排除产生混淆的可能。

        在flags参数中可以指定常量CPF_CLOEXEC将会导致createPidFile()为文件描述符设置close-on-exec标记(27.4节)。这对于通过调用exec()重启字节的服务器来讲是比较有用的。如在exec()时文件描述符没有被关闭,那么重新启动的服务器会认为服务器的另一个实例正处于运行状态。

程序清单55-4:创建一个PID锁文件以确保只有一个程序实例被启动了----create_pid_file.c

/* Listing 55-4 */

#include <sys/stat.h>
#include <fcntl.h>
#include "region_locking.h"             /* For lockRegion() */
#include "create_pid_file.h"            /* Declares createPidFile() and
                                           defines CPF_CLOEXEC */
#include "tlpi_hdr.h"

#define BUF_SIZE 100            /* Large enough to hold maximum PID as string */

/* Open/create the file named in 'pidFile', lock it, optionally set the
   close-on-exec flag for the file descriptor, write our PID into the file,
   and (in case the caller is interested) return the file descriptor
   referring to the locked file. The caller is responsible for deleting
   'pidFile' file (just) before process termination. 'progName' should be the
   name of the calling program (i.e., argv[0] or similar), and is used only for
   diagnostic messages. If we can't open 'pidFile', or we encounter some other
   error, then we print an appropriate diagnostic and terminate. */

int
createPidFile(const char *progName, const char *pidFile, int flags)
{
    int fd;
    char buf[BUF_SIZE];

    fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1)
        errExit("Could not open PID file %s", pidFile);

    if (flags & CPF_CLOEXEC) {

        /* Set the close-on-exec file descriptor flag */

        flags = fcntl(fd, F_GETFD);                     /* Fetch flags */
        if (flags == -1)
            errExit("Could not get flags for PID file %s", pidFile);

        flags |= FD_CLOEXEC;                            /* Turn on FD_CLOEXEC */

        if (fcntl(fd, F_SETFD, flags) == -1)            /* Update flags */
            errExit("Could not set flags for PID file %s", pidFile);
    }

    if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
        if (errno  == EAGAIN || errno == EACCES)
            fatal("PID file '%s' is locked; probably "
                     "'%s' is already running", pidFile, progName);
        else
            errExit("Unable to lock PID file '%s'", pidFile);
    }

    if (ftruncate(fd, 0) == -1)
        errExit("Could not truncate PID file '%s'", pidFile);

    snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
    if (write(fd, buf, strlen(buf)) != strlen(buf))
        fatal("Writing to PID file '%s'", pidFile);

    return fd;
}

55.7 老式加锁技术

        在较早的不支持文件加锁的UNIX实现上可以使用一些特别的加锁技术。尽管所有这些技术都已经被fcntl()记录加锁所取代。因为在一些较早的应用程序中仍然存在他们 的身影。所有这些技术在性质上都是劝告式的。

open(file,O_CREAT|O_EXCL,......)加上 unlink(file)

        SUSv3要求使用了O_CREAT和O_EXCL标记的open调用有原子地执行检查文件地存在性以及创建文件地两个步骤(5.1节)。这意味着如果两个进程尝试在创建一个文件时指定这些标记,那么就保证只有其中一个进程能够成功。(另一个进程会从open()中收到EEXIST错误。)这种调用与unlink()系统调用组合起来就构成了一种加锁机制地基础。获取锁可通过成功地使用O_CREAT和O_EXCL标记打开文件后,立即跟着一个close()来完成。释放锁则可以通过使用unlnk()来完成。尽管这项技术能够正常工作,但它存在一些局限。

  • 如果open()失败了,即表示其他进程拥有了锁,那么就必须要在某种循环中重试open()操作,这种操作既可以时持续不同地(这样将会浪费CPU时间,),也可以在相邻两次尝试之间加上一定地延迟(这意味着在锁可用地时刻和实际获取锁地时刻之间可能存在一定地延迟)。有了fcntl()之后则可以使用F_SETLKW来阻塞直到锁可用为止。
  • 使用open()和unlink()获取和释放锁涉及到文件系统地操作,这比记录锁要慢很多。
  • 如果一个进程意外终止并且没有删除所文件,那么锁就不会被释放。处理这个问题存在特别的技术,包括检查文件的上次修改时间和让所的持有者将其进程ID写入文件,这样能够检查进程是否存在,但这些技术没有一项是安全可靠的。与之相反的是,在一个进程终止时,记录锁的释放操作的是原子的。
  • 如果放置多把锁(即使用多个锁文件),那么就无法检测出死锁。如果发生了死锁,那么造成isuo的进程就会永远保持阻塞。(每个进程都会定在那里检查是否能够获取请求的锁。)与之形成鲜明对比的是,内核会对fcntl()记录锁进行死锁检查。
  • 第二版的NFS不支持O_EXCL语义。Linux 2.4NFS客户端也没有正确的实现O_EXCL,即使是第三版的NFS以及之后的版本也没能完成这个任务。

link(file,lockfile)加上unlink(lockfile)

        link系统调用在新链接已经存在时会失败的事实可作为一种加锁机制,而解锁功能则还是使用unlink()来完成。常规的做法是让需要获取锁的进程创建一个唯一的临时文件名,一般来讲需要包含进程ID(如果锁文件被创建于一个网络文件系统上,那么可能的话再加上主机名)。要获取锁则需要将这个临时文件链接到某个约定的标准路径名上。(硬链接在语义上需要两个路径名位于同一个文件系统上。)如果link()调用成功,那么就是获取了锁,如果失败(EEXIST),那么就是另一个进程持有了锁,因此必须要在稍后某个时刻重新尝试获取锁。这项技术与上面介绍的open(file,O_CREAT|O_EXCL,......)技术存在同样的局限

open(file,O_CREAT | O_TRUNC|O_WRONLY,0) plus unlink(file)

         当指定O_TRUNC并且写权限被拒绝时在既有文件上调用open()会失败的事实可作为一项枷锁技术的基础。要获取一把锁可以使用下面的代码(省略了错误检查)来创建一个新文件。

fd = open(file,O_CREAT | O_TRUNC|O_WRONLY,(mode_t)0);
close(fd);

        如果open()调用成功(即文件之前不存在),那么就是获取了锁。如果因EACCES而失败(即问价存在但没有人拥有权限),那么其他进程持有了锁,还需要后面某个时刻尝试重新获取锁。这项技术与前面介绍的技术存在相同的权限,还需要注意的是,不能再具备超级用户特权的程序中使用这项技术,因为open总会成功,不管文件上设置的权限是什么

55.8总结

        文件锁使得进程能够同步对一个文件的访问。Linux提供了另种文件加锁的系统调用:从BSD衍生出来的flock()和从System V衍生出来的fcntl()。尽管这两组系统调用再大多数UNIX实现上都是可用的,但只有fcntl()加锁在SUsv3中进行了标准化。

        flock()系统调用将一个文件的任意区域上放置锁("记录锁")这个区域可以是单字节的也可以是整个文件。可防止的锁有两种:读锁和写锁,他们之间的兼容性语义与flock()放置的共享锁和互斥锁之间的兼容想语义类似。如果一个阻塞式(F_SETLKW)锁请求将会导致死锁,那么内核会让其中一个受影响的进程的fcntl()失败(返回EDEADLK错误)。

        使用flock()和fcntl()放置的锁之间式互相不可见的(除了在使用fcntl()实现folck()的系统)。通过flock()和fcntl()放置的锁在flock()放置的锁在fork()中的继承语义和在文件描述符被关闭时的释放语义是不同的。

        Linux特有的/proc/locks 文件给出了系统中所有进程当前持有的文件锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值