若两个人同时编辑一个文件,其后果将如何呢?在很多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结构的指针:
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 */ };
对flock结构说明如下:
关于加锁和解锁区域的说明还要注意下列各点:
上面提到了两种类型的锁:共享读锁(l_type为F_RDLCK)和独占写锁(F_WRLCK)。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独用的一把写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加上写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。在表14-2示出了这些规则。
上面说明的兼容性规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换老锁。例如,若一进程在某文件的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说明了这种情况。
图14-1 文件字节范围锁
假定我们又对第150字节设置锁,那么系统将会把三个相邻的加锁区合并成一个区(从字节100至199)。其结果如图14-1中的第一图所示,于是我们又回到了出发点。
实例:请求和释放一把锁
为了避免每次分配flock结构,然后又填入各项信息,可以用程序清单14-2中的函数lock_reg来处理所有这些细节。
程序清单14-2 加锁和解锁一个文件区域的函数
#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)); }
因为大多数锁调用是加锁或解锁一个文件区域(命令F_GETLK很少使用),故通常使用下列5个宏,它们都定义在apue.h中。
#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))
实例:测试一把锁
程序清单14-3中定义了一个函数lock_test,可用其测试一把锁。
程序清单14-3 测试一个锁状态的函数
#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 */ }
如果存在一把锁,它阻塞由参数说明的锁请求,则此函数返回持有这把现存锁的进程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所示程序得到:
程序清单14-4 死锁检测实例
#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); }
检测到死锁时,内核必须选择一个进程接收出错返回。在本实例中选择了子进程,这是一个实现细节。在某些系统上,总是子进程接收到出错信息;在另一些系统上,总是父进程接到出错信息。在某些系统上,当试图使用多把锁时,有时是子进程接到出错信息,有时则是父进程接到出错信息。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。
原文:http://www.cnblogs.com/nufangrensheng/p/3554168.html