首页 > 其他 > 详细

高级I/O之记录锁

时间:2014-02-19 00:40:13      阅读:422      评论:0      收藏:0      [点我收藏+]

若两个人同时编辑一个文件,其后果将如何呢?在很多UNIX系统中,该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序(例如数据库),进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。

记录锁(record locking)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。对于UNIX系统而言,“记录”这个词是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念。更适合的术语可能是字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。

1、历史

对早期UNIX系统的一种批评是它们不能用来运行数据库系统,其原因是这些系统不支持部分地对文件加锁。在UNIX系统开始进入商用计算领域时,很多系统开发小组以各种不同方式增加了对记录锁的支持。

早期的伯克利版本只支持flock函数。该函数锁整个文件,不能锁文件中的一部分。

SVR3通过fcntl函数增加了记录锁功能。在此基础上构造了lockf函数,它提供了一个简化的接口。这些函数允许调用者锁一个文件中任意字节数的区域,长至整个文件,短至文件中的一个字节。

POSIX.1标准的基础是fcntl。

2、fcntl记录锁

http://www.cnblogs.com/nufangrensheng/p/3500350.html中已经给出了fcntl函数的原型。

#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */);
返回值:若成功则依赖于cmd,若出错则返回-1

对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针:

bubuko.com,布布扣
struct flock{
    short    l_type;       /* F_RDLCK, F_WRLCK, or F_UNLCK */
    off_t    l_start;      /* offset in bytes, relative to l_whence */
    short    l_whence;     /* SEEK_SET, SEEK_CUR, or SEEK_END */
    off_t    l_len;        /* length, in bytes; 0 means lock to EOF */
    pid_t    l_pid;        /* returned with F_GETLK */
};
bubuko.com,布布扣

对flock结构说明如下:

  • 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)。
  • 要加锁或解锁区域的起始字节偏移量,这由l_start和l_whence两者决定。
  • 区域的字节长度,由l_len表示。
  • 具有能阻塞当前进程的锁,其持有进程的ID存放在l_pid中(仅由F_GETLK返回)。

关于加锁和解锁区域的说明还要注意下列各点:

  • l_start是相对偏移量(字节),l_whence则决定了相对偏移量的起点。这与lseek函数(http://www.cnblogs.com/nufangrensheng/p/3498080.html)中最后两个参数类似。确实,l_whence可选用的值是SEEK_SET、SEEK_CUR或SEEK_END。
  • 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始。
  • 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能偏移量为止,也就是不管添写到该文件中多少数据,它们都处于锁的范围内(不必猜测会有多少字节被追加到文件之后)。
  • 为了锁整个文件,我们设置l_start和l_whence,使锁的起点在文件起始处,并且说明长度(l_len)为0。(有多种方法可以指定文件起始处,但常用的方法是将l_start指定为0,l_whence指定为SEEK_SET。)

上面提到了两种类型的锁:共享读锁(l_type为F_RDLCK)和独占写锁(F_WRLCK)。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独用的一把写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加上写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。在表14-2示出了这些规则。

bubuko.com,布布扣

上面说明的兼容性规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换老锁。例如,若一进程在某文件的16-32字节区间有一把写锁,然后又试图在16-32字节区间加一把读锁,那么该请求将成功执行(假定其他进程此时并不试图向该文件的同一区间加锁),原来的写锁被替换为读锁。

加读锁时,该描述符必须是读打开;加写锁时,该描述符必须是写打开。

以下说明fcntl函数的三种命令:

F_GETLK          判断由flockptr所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则把该现存锁的信息写到flockptr指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。

F_SETLK          设置由flockptr所描述的锁。如果试图建立一把读锁(l_type设为F_RDLCK)或写锁(l_type设为F_WRLCK),而按上述兼容性规则不能允许,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。此命令也用来清除由flockptr说明的锁(l_type为F_UNLCK)。

F_SETLKW       这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果因为当前在所请求区间的某个部分另一进程已经有一把锁,因而按兼容性规则由flockptr所请求的锁不能被创建,则使调用进程休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。

应当了解,用F_GETLK测试能否建立一把锁,然后用F_SETLK和F_SETLKW企图建立一把锁,这两者不是一个原子操作。因此不能保证在这两次fcntl调用之间不会有另一个进程插入并建立一把相关的锁,从而使原来测试到的情况发生变化。如果不希望在建立锁时可能产生的长期阻塞,则应使用F_SETLK,并对返回结果进行测试,以判别是否成功地建立了所要求的锁。

在设置或释放文件上的锁时,系统按要求组合或裂开相邻区。例如,若字节100-199是加锁的区,需解锁地150字节,则内核将维持两把锁,一把用于字节100-149,另一把用于字节151-199。图14-1说明了这种情况。

bubuko.com,布布扣

                                    图14-1 文件字节范围锁

假定我们又对第150字节设置锁,那么系统将会把三个相邻的加锁区合并成一个区(从字节100至199)。其结果如图14-1中的第一图所示,于是我们又回到了出发点。

实例:请求和释放一把锁

为了避免每次分配flock结构,然后又填入各项信息,可以用程序清单14-2中的函数lock_reg来处理所有这些细节。

程序清单14-2 加锁和解锁一个文件区域的函数

bubuko.com,布布扣
#include "apue.h"
#include <fcntl.h>

int
lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
    struct flock    lock;

    lock.l_type = type;        /* F_RDLCK, F_WRLCK, F_UNLCK */
    lock.l_start = offset;        /* byte offset, relative to l_whence */
    lock.l_whence = whence;        /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;        /* #bytes (0 means to EOF) */

    return(fcntl(fd, cmd, &lock));
}
bubuko.com,布布扣

   因为大多数锁调用是加锁或解锁一个文件区域(命令F_GETLK很少使用),故通常使用下列5个宏,它们都定义在apue.h中。 

bubuko.com,布布扣
#define    read_lock(fd, offset, whence, len) \
                lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define    readw_lock(fd, offset, whence, len) \
                lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define    write_lock(fd, offset, whence, len) \
                lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define    writew_lock(fd, offset, whence, len) \
                lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define    un_lock(fd, offset, whence, len) \
                lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
bubuko.com,布布扣

实例:测试一把锁

程序清单14-3中定义了一个函数lock_test,可用其测试一把锁。

程序清单14-3 测试一个锁状态的函数

bubuko.com,布布扣
#include "apue.h"
#include <fcntl.h>

pid_t
lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock    lock;
    lock.l_type = type;        /* F_RDLCK or F_WRLCK */
    lock.l_start = offset;     /* byte offset,  relative to l_whence */
    lock.l_whence = whence;    /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;          /* bytes (0 means to EOF) */

    if(fcntl(fd, F_GETLK, &lock) < 0)
        err_sys("fcntl error");

    if(lock.l_type == F_UNLCK)
        return(0);         /* false, region isn‘t locked by another proc */
    return(lock.l_pid);    /* true, return pid of lock owner */
}
bubuko.com,布布扣

如果存在一把锁,它阻塞由参数说明的锁请求,则此函数返回持有这把现存锁的进程ID,否则此函数返回0。通常用下面两个宏来调用此函数(它们也定义在apue.h中)。

#define    is_read_lockable(fd, offset, whence, len) \
              (lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0)
#define    is_write_lockable(fd, offset, whence, len) \
              (lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0)

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

实例:死锁

如果两个进程相互等待对方持有并且锁定的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,则它就会休眠,在这种情况下,有发生死锁的可能性。

程序清单14-4给出了一个死锁的例子。子进程锁字节0,父进程锁字节1。然后,它们又都试图锁对方已经加锁的字节。在该程序中使用了http://www.cnblogs.com/nufangrensheng/p/3510306.html中介绍的父、子进程同步例程(TELL_xxx和WAIT_xxx),使得每个进程能够等待另一个进程获得它设置的第一把锁。运行程序清单14-4所示程序得到:

bubuko.com,布布扣

程序清单14-4 死锁检测实例

bubuko.com,布布扣
#include "apue.h"
#include <fcntl.h>

static void
lockabyte(const char *name, int fd, off_t offset)
{
    if(writew_lock(fd, offset, SEEK_SET, 1) < 0)
        err_sys("%s: write_lock error", name);
    printf("%s: got the lock, byte %ld\n", name, offset);
}

int 
main(void)
{
    int    fd;
    pid_t    pid;

    /*
    * Create a file and write two bytes to it.
    */
    if((fd = creat("templock", FILE_MODE)) < 0)
        err_sys("creat error");
    if(write(fd, "ab", 2) != 2)
        err_sys("write error");

    TELL_WAIT();
    if((pid = fork()) < 0)
    {
        err_sys("fork error");
    }
    else if(pid == 0)            /* child */
    {
        lockabyte("child", fd, 0);
        TELL_PARENT(getppid());
        WAIT_PARENT();
        lockabyte("child", fd, 1);
    }
    else                    /* parent */
    {
        lockabyte("parent", fd, 1);
        TELL_CHILD(pid);
        WAIT_CHILD();
        lockabyte("parent", fd, 0);
    }
    exit(0);
}
bubuko.com,布布扣

检测到死锁时,内核必须选择一个进程接收出错返回。在本实例中选择了子进程,这是一个实现细节。在某些系统上,总是子进程接收到出错信息;在另一些系统上,总是父进程接到出错信息。在某些系统上,当试图使用多把锁时,有时是子进程接到出错信息,有时则是父进程接到出错信息。

本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

高级I/O之记录锁

原文:http://www.cnblogs.com/nufangrensheng/p/3554168.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!