安全且平台兼容的进程互斥锁

《进程互斥锁》中,我们看到了实现进程互斥锁的几个常见方案:Posix信号量、System V信号量以及线程锁共享,并且分析了他们的平台兼容性以及严重缺陷。这里要介绍

一种安全且平台兼容的进程互斥锁,它是基于文件记录锁实现的。

1、文件记录锁

UNIX编程的“圣经”《Unix环境高级编程》中有对文件记录锁的详细描述。

下载链接:http://dl02.topsage.com/club/computer/Unix环境高级编程.rar

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

2、平台兼容性

各种UNIX系统支持的记录锁形式: 

系统建议性强制性fcntllockfflock
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;
    }
};

需要注意,命名锁不住析构时删除,因为可能多个对象共享该锁。

5、线程安全性

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值