C语言文件锁 flock
应用场景:
在多进程环境中,多个进程可能会同时访问共享资源,这时就需要考虑如何确保数据的一致性,避免出现竞态条件和数据损坏的情况。例如,在一个业务中多个进程自检需要用到共享内存。而在使用共享内存时,需要考虑多进程同时写入时不发生冲突的问题,基于使用的系统是Linux发行版,解决方案有以下两个:
- 使用信号量实现互斥。
- 使用C语言的文件锁 flock 函数,利用其独享的性质,来实现进程间的互斥访问。而且,相比较于信号量,使用flock函数构成的互斥锁,在进程以外退出时,进程所持有的文件锁会自动解锁。
flock
PS: flock函数是进程安全的,但不是线程安全的,故使用时可能需要自行加锁。
man手册:
man 2 flock
头文件及声明
#include <sys/file.h>
int flock(int fd, int operation);
参数
参数这建议自己查看man手册的内容。
- fd: 一个已打开的文件描述符。
- operation: 文件锁选项, 包括 LOCK_SH(共享锁), LOCK_EX(独占锁), LOCK_UN(解锁),在文件加锁时,若文件已加锁,则会处于程序会处于阻塞,如果希望程序不阻塞,则operation选项需或上LOCK_NB,例如,operation传入 LOCK_EX|LOCK_NB。
返回值:
失败时返回-1
简单的文件锁示例代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#define LOCKFILE "/tmp/lockfile/.lockfile"
int main()
{
int fd = 0;
int ret = 0;
do {
fd = open(LOCKFILE, O_RDONLY);
if (-1 == fd)
{
fd = open(LOCKFILE, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
if (-1 == fd)
{
printf ("open() for [%s] failed.\n", LOCKFILE);
break;
}
}
// file lock
ret = flock(fd, LOCK_EX);
if (-1 == ret)
{
printf ("flock() failed.\n");
break;
}
// do something
// ...
flock(fd, LOCK_UN);
}
while (0);
if (fd != -1)
{
close(fd);
}
return 0;
}
进程读写锁代码
代码如下:
测试环境:C++20, 由于使用了共享锁,所以建议c++标准起码>=C++17
Result.hpp
#ifndef RESULT_H
#define RESULT_H 1
// 这个枚举是我习惯用于作为返回值, 此处模拟的是rust的Result
// 加上class是为了用c++的强类型系统
enum class Result
{
OK,
ERROR,
PARA_ERROR,
RANGE_ILLEGAL,
FILE_LOCK_FAIL,
MKDIR_FAIL
};
#endif
ProcLock.hpp
#ifndef _PROC_LOCK_HPP_
#define _PROC_LOCK_HPP_ 1
#include "Result.hpp"
// 这里为进程锁的接口头文件
namespace ProcLock
{
// 进程锁的类型,即用于锁哪个共享资源,可以自己添加
// 例如我想锁一个共享空间是用于存放证书的,那么可以添加 CERT_TYPE
enum class Type
{
TEST_TYPE,
CERT_TYPE,
MAX_NUMS// 不使用, 仅用于计数
};
// 进程锁接口函数,包括独占锁和读写锁
Result Lock(const Type type);
Result UnLock(const Type type);
Result SharedLock(const Type type);
Result SharedUnLock(const Type type);
// 用于进程锁的RAII类,实现构造函数加锁,析构函数解锁,防止遗忘
class RAII_Lock
{
private:
ProcLock::Type m_type;
public:
explicit RAII_Lock(const ProcLock::Type type)
: m_type(type)
{
Lock(m_type);
}
~RAII_Lock()
{
UnLock(m_type);
}
};
class RAII_ShardLock
{
private:
ProcLock::Type m_type;
public:
explicit RAII_ShardLock(const ProcLock::Type type)
: m_type(type)
{
SharedLock(m_type);
}
~RAII_ShardLock()
{
SharedUnLock(m_type);
}
};
}
#endif
ProcLock.cpp
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <mutex>
#include <filesystem>
#include <shared_mutex>
#include "ProcLock.hpp"
// 文件锁的文件模式,用于创建一个新文件
#define LOCK_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
// 文件锁文件的存放路径
constexpr char PROC_LOCK_FILE_PATH[] = "/tmp/.lock/";
// 总共有多少个文件锁类型
constexpr unsigned PROC_LOCK_MAX_NUM = static_cast<unsigned>(ProcLock::Type::MAX_NUMS);
// 进程锁类,使用类是为了其数据成员为私有,其实使用static定义全局变量也行
class CProcLock
{
private:
// 进程锁初始化标志
bool m_is_init;
// flock对应的fd数组
int m_fds[PROC_LOCK_MAX_NUM];
// 线程读写锁,为了实现进程锁线程安全
std::shared_mutex m_rw_mtxs[PROC_LOCK_MAX_NUM];
// 互斥锁,为了锁住进程读锁的数量
std::mutex m_shard_nums_mtxs[PROC_LOCK_MAX_NUM];
// 统计当前有多少个线程持有进程读锁,进程读锁计数器
unsigned m_shared_lock_nums[PROC_LOCK_MAX_NUM];
public:
CProcLock() noexcept
: m_is_init(false) // 把进程锁初始状态设定为false
{
// 使用c++17引入的文件系统
// ec是为了避免抛出异常,这个在boost库中很常见,相当于C语言的errno
std::error_code ec;
char file_name[256] = {0};
// c++17文件系统引入的类
// 参考链接: https://zh.cppreference.com/w/cpp/filesystem/directory_entry
std::filesystem::directory_entry dir(PROC_LOCK_FILE_PATH);
do
{
// 判断存放进程锁文件的目录是否存在以及是否为目录
if (dir.exists(ec) != true || dir.is_directory(ec) != true)
{
// 若该路径代表的不是目录,则删除该文件
if (dir.is_directory(ec) != true)
{
// 参考链接: https://zh.cppreference.com/w/cpp/filesystem/remove
std::filesystem::remove(PROC_LOCK_FILE_PATH, ec);
}
// 创造该目录
// 参考链接: https://zh.cppreference.com/w/cpp/filesystem/create_directory
if (std::filesystem::create_directory(PROC_LOCK_FILE_PATH, ec) != true)
{
std::printf("in CProcLock:: create_directory() failed.\n");
break;
}
}
for (unsigned i = 0U; i < PROC_LOCK_MAX_NUM; i++)
{
::memset(file_name, 0, sizeof(file_name));
::snprintf(file_name, sizeof(file_name), "%s%04u.lockfile", PROC_LOCK_FILE_PATH, i);
// 初始化flock所需要的文件描述符数组
m_fds[i] = ::open(file_name, O_RDWR|O_CREAT, LOCK_MODE);
// 将读锁计数器的数目初始化为0
m_shared_lock_nums[i] = 0U;
// 文件描述符打开失败
if (-1 == m_fds[i]) // open fail, break
{
break;
}
// 文件描述符数组初始化完毕,将进程锁初始化状态设为true
if (PROC_LOCK_MAX_NUM-1 == i) // end, set init flag true
{
m_is_init = true;
}
}
}
while (0);
}
~CProcLock()
{
// 在析构函数中,对文件锁进行尝试解锁,并关闭已打开的文件描述符,以及设初始化状态为false
for (unsigned i = 0U; i < PROC_LOCK_MAX_NUM; i++)
{
::flock(m_fds[i], LOCK_UN);
if (m_fds[i] >= 0)
{
::close(m_fds[i]);
m_fds[i] = -1;
}
}
m_is_init = false;
}
Result lock(const ProcLock::Type type) // 写锁
{
if (ProcLock::Type::MAX_NUMS == type) // 类型错误
{
return Result::PARA_ERROR;
}
if (m_is_init != true) // 进程锁为初始化
{
return Result::FILE_LCOK_UN_INIT;
}
// 将枚举转换为对应的数组下标,以获得对应的文件描述符和mutex
const unsigned type_num = static_cast<unsigned>(type);
// 先保证线程安全,对应的线程读写锁加写锁
m_rw_mtxs[type_num].lock();
// 对文件进行加锁
const int ret = ::flock(m_fds[type_num], LOCK_EX);
return (ret != 0 ? Result::FILE_LOCK_FAIL : Result::OK);
}
Result shared_lock(const ProcLock::Type type)
{
if (ProcLock::Type::MAX_NUMS == type)
{
return Result::PARA_ERROR;
}
if (m_is_init != true)
{
return Result::FILE_LCOK_UN_INIT;
}
int ret = 0;
const unsigned type_num = static_cast<unsigned>(type);
// 先保证线程安全,对应的线程读写锁加读锁
m_rw_mtxs[type_num].lock_shared();
// 对应的文件锁的互斥锁加锁,目的是保证持有读锁的计数器线程安全
m_shard_nums_mtxs[type_num].lock();
// 当进程读锁计数器数目为0时,对文件加读锁
if (0 == m_shared_lock_nums[type_num])
{
ret = ::flock(m_fds[type_num], LOCK_SH);
}
// 进程读锁计数器数目++
m_shared_lock_nums[type_num]++;
// 解开互斥锁
m_shard_nums_mtxs[type_num].unlock();
return (ret != 0 ? Result::FILE_LOCK_FAIL : Result::OK);
}
Result unlock(const ProcLock::Type type)
{
if (ProcLock::Type::MAX_NUMS == type)
{
return Result::PARA_ERROR;
}
if (m_is_init != true)
{
return Result::FILE_LCOK_UN_INIT;
}
const unsigned type_num = static_cast<unsigned>(type);
// 解开文件锁
const int ret = ::flock(m_fds[type_num], LOCK_UN);
// 解开文件锁后再解开对应的线程读写锁的写锁
m_rw_mtxs[type_num].unlock();
return (ret != 0 ? Result::FILE_LOCK_FAIL : Result::OK);
}
Result shared_unlock(const ProcLock::Type type)
{
if (ProcLock::Type::MAX_NUMS == type)
{
return Result::PARA_ERROR;
}
if (m_is_init != true)
{
return Result::FILE_LCOK_UN_INIT;
}
int ret = 0;
const unsigned type_num = static_cast<unsigned>(type);
// 对应的文件锁的互斥锁加锁,目的是保证持有读锁的计数器线程安全
m_shard_nums_mtxs[type_num].lock();
// 进程读锁计数器--
if (m_shared_lock_nums[type_num] > 0)
{
m_shared_lock_nums[type_num]--;
}
// 当进程读锁计数器为0时,对文件进行解锁
if (0 == m_shared_lock_nums[type_num])
{
ret = ::flock(m_fds[type_num], LOCK_UN);
}
// 解开互斥锁
m_shard_nums_mtxs[type_num].unlock();
// 解开文件锁后再解开对应的线程读写锁的读锁
m_rw_mtxs[type_num].unlock_shared();
return (ret != 0 ? Result::FILE_LOCK_FAIL : Result::OK);
}
};
// 这里是为了实现为单例模式的
// 这也是我为什么把类声明定义在源文件中而非头文件中
static CProcLock g_PROC_LOCK;
Result ProcLock::Lock(const ProcLock::Type type)
{
return g_PROC_LOCK.lock(type);
}
Result ProcLock::UnLock(const ProcLock::Type type)
{
return g_PROC_LOCK.unlock(type);
}
Result ProcLock::SharedLock(const ProcLock::Type type)
{
return g_PROC_LOCK.shared_lock(type);
}
Result ProcLock::SharedUnLock(const ProcLock::Type type)
{
return g_PROC_LOCK.shared_unlock(type);
}