一种安全且平台兼容的进程互斥锁,它是基于文件记录锁实现的。
1、文件记录锁
UNIX编程的“圣经”《Unix环境高级编程》中有对文件记录锁的详细描述。
下载链接:http://dl02.topsage.com/club/computer/Unix环境高级编程.rar
记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。
2、平台兼容性
各种UNIX系统支持的记录锁形式:
系统 |
建议性 |
强制性 |
fcntl |
lockf |
flock |
POSIX.1 |
* |
|
* |
|
|
XPG3 |
* |
|
* |
|
|
SVR2 |
* |
|
* |
* |
|
SVR3 SVR4 |
* |
* |
* |
* |
|
4.3BSD |
* |
|
* |
|
* |
4.3BSDReno |
* |
|
* |
|
* |
可以看成,记录锁在各个平台得到广泛支持。特别的,在接口上,可以统一于fcntl。
建议性锁和强制性锁之间的区别,是指其他文件操作函数(如open,read、write)是否受记录锁影响,如果是,那就是强制性的记录锁,大部分平台只是建议性的。不过,对实现进程互斥锁而言,这个影响不大。
3、接口描述
- #include <sys/types.h>
- #include <unistd.h>
- #include <fcnt1.h>
- int fcnt1(int filedes, int cmd, ...);
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。
- struct flock {
- short l_type;
- short l_whence;
- off_t l_start;
- off_t l_len;
- pid_t l_pid; /* PID of process blocking our lock
- };
以下说明fcntl函数的三种命令:
- F_GETLK决定由flockptr所描述的锁是否被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由flockptr所描述的锁,则这把现存的锁的信息写到flockptr指向的结构中。如果不存在这种情况,则除了将ltype设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
- F_SETLK设置由flockptr所描述的锁。如果试图建立一把按上述兼容性规则并不允许的锁,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
- F_SETLKW这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果由于存在其他锁,那么按兼容性规则由flockptr所要求的锁不能被创建,则调用进程睡眠。如果捕捉到信号则睡眠中断。
4、实现方案
如何 基于记录锁实现进程互斥锁?
1、需要一个定义全局文件名,这个文件名只能有相关进程使用。这需要在整个系统做一个规划。
2、规定同一个进程互斥锁对应着该文件的一个字节,字节位置称为锁的编号,这样可以用一个文件实现很多互斥锁。
3、编号要有分配逻辑,文件中要记录已经分配的编号,这个逻辑也要保护,所以分配0号锁为系统锁。
4、为了实现命名锁,文件中要记录名称与编号对应关系,这个对应关系的维护也需要系统锁保护。
这些逻辑都实现在一个FileLocks类中:
- class FileLocks
- {
- public:
- FileLocks();
- ~FileLocks();
- size_t alloc_lock();
- size_t alloc_lock(std::string const & keyname);
- void lock(size_t pos);
- bool try_lock(size_t pos);
- void unlock(size_t pos);
- void free_lock(size_t pos);
- void free_lock(std::string const & keyname);
- private:
- int m_fd_;
- };
-
- inline FileLocks & global_file_lock()
- {
- static FileLocks g_fileblocks( "process.filelock" );
- return g_fileblocks;
- }
这里用了一个FileLocks全局单例对象,它对应的文件名是“/tmp/filelock”,在FileLocks中,分别用alloc()和alloc(keyname)分配匿名锁和命名锁,用free_lock删除锁。free_lock(pos)删除匿名锁,free_lock(keyname)删除命名锁。
对锁的使用通过lock、try_lock、unlock实现,他们都带有一个pos参数,代表锁的编号。
有了FileLocks类作为基础,要实现匿名锁和命名锁就很简单了。
4.1、匿名锁
- class FileMutex
- {
- public:
- FileMutex()
- : m_lockbyte_(global_file_lock().alloc_lock())
- {
- }
- ~FileMutex()
- {
- global_file_lock().free_lock(m_lockbyte_);
- }
- void lock()
- {
- global_file_lock().lock(m_lockbyte_);
- }
- bool try_lock()
- {
- return global_file_lock().try_lock(m_lockbyte_);
- }
- void unlock()
- {
- global_file_lock().unlock(m_lockbyte_);
- }
- protected:
- size_t m_lockbyte_;
- };
需要注意的是,进程匿名互斥锁需要创建在共享内存上。只需要也只能某一个进程(比如创建共享内存的进程)调用构造函数,其他进程直接使用,同样析构函数也只能调用一次。
4.2、命名锁
命名锁只需要构造函数不同,可以直接继承匿名锁实现
- class NamedFileMutex
- : public FileMutex
- {
- public:
- NamedFileMutex(std::string const & key)
- : m_lockbyte_(global_file_lock().alloc_lock(key))
- {
- }
- ~NamedFileMutex()
- {
- m_lockbyte_ = 0;
- }
- };
需要注意,命名锁不住析构时删除,因为可能多个对象共享该锁。
5、线程安全性