在《进程互斥锁》中,我们看到了实现进程互斥锁的几个常见方案:Posix信号量、System V信号量以及线程锁共享,并且分析了他们的平台兼容性以及严重缺陷。这里要介绍
一种安全且平台兼容的进程互斥锁,它是基于文件记录锁实现的。
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, .../* struct flock *flockptr */);
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(称其为flockptr)是一个指向flock结构的指针。
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
};
以下说明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;
}
};
需要注意,命名锁不住析构时删除,因为可能多个对象共享该锁。