❝脚步不停,终达卓越!更多底层开发技巧,欢迎关注公众号《开源519》
在多进程开发中,共享资源(如配置文件、设备接口、共享内存块)的竞争访问是常见场景。若不加以控制,轻则导致数据错乱,重则引发进程崩溃。
传统解决方案中: 信号量(semaphore) 虽能实现跨进程同步,但接口设计繁琐(需手动管理计数);普通线程 mutex 仅能在单进程内生效,无法跨进程协同。更棘手的是,若持有锁的进程意外崩溃,未释放的锁可能导致其他进程永久阻塞——这类 “锁泄露” 问题往往难以排查。
秉持一贯的优雅编程理念,基于共享内存与 pthread 接口,设计并实现一个开箱即用、兼顾安全与易用的进程间互斥锁 ProcMutex 类,支持跨进程同步与异常安全,并通过 RAII 机制简化锁的生命周期管理。
一个可靠的进程间互斥锁需满足以下核心需求:
Lock()/Unlock() 接口,最好支持自动释放(避免手动解锁遗漏)。 基于上述需求,ProcMutex 类的设计围绕 “共享内存存状态,pthread 锁做同步,原子变量保安全” 展开,核心代码如下:
#ifndef __PROC_MUTEX_H__
#define __PROC_MUTEX_H__
#include <mutex>
#include <string>
#include <pthread.h>
#include <atomic>
// 共享内存中的数据结构,存储锁状态与计数
struct SharedData {
std::atomic<int> refCnt; // 引用计数(当前持有锁的次数)
std::atomic<int> waitCnt; // 等待锁的进程/线程数
pthread_mutex_t dataMutex; // 用于保护临界区的核心锁
pthread_mutex_t waitMutex; // 用于保护 waitCnt 的辅助锁
};
class ProcMutex {
public:
explicit ProcMutex(const std::string& mutexName); // 构造时初始化共享资源
~ProcMutex(); // 析构时清理资源
void Lock(); // 加锁
void Unlock(); // 解锁
private:
void Init(); // 初始化共享内存与锁
void DeInit(); // 销毁共享资源
void AddWait(); // 增加等待计数
void DelWait(); // 减少等待计数
private:
int mShmFd; // 共享内存文件描述符
std::string mMutexName; // 锁名称(用于标识共享内存)
SharedData* mSharedData; // 共享内存映射的指针
};
// RAII 封装,自动加解锁
class ProcLockGuard {
public:
explicit ProcLockGuard(ProcMutex& pMutex, std::mutex& tMutex);
~ProcLockGuard();
private:
std::mutex& mTMutex; // 线程内 mutex(防止同一进程内线程竞争)
ProcMutex& mPMutex; // 进程间 mutex
};
#endif // __PROC_MUTEX_H__
核心逻辑集中在共享内存初始化、锁状态管理与异常处理,关键代码解析如下:
pthread_mutexattr_setpshared 将互斥锁允许进程共享。
② 通过pthread_mutexattr_setrobust 开启健壮模式,持有者崩溃时,其他进程能检测到并恢复。void ProcMutex::Init() {
// 1. 创建/打开共享内存(以 mutex 名称为标识)
mShmFd = shm_open(mMutexName.c_str(), O_RDWR | O_CREAT, 0744);
if (mShmFd == -1) {
SPR_LOGE("shm_open failed! (%s)\n", strerror(errno));
return;
}
// 2. 设置共享内存大小为 SharedData 结构大小
if (ftruncate(mShmFd, sizeof(SharedData)) == -1) {
SPR_LOGE("ftruncate failed! (%s)\n", strerror(errno));
close(mShmFd);
return;
}
// 3. 映射共享内存到进程地址空间
mSharedData = reinterpret_cast<SharedData*>(mmap(NULL, sizeof(SharedData),
PROT_READ | PROT_WRITE, MAP_SHARED, mShmFd, 0));
if (mSharedData == MAP_FAILED) {
SPR_LOGE("mmap failed! (%s)\n", strerror(errno));
close(mShmFd);
return;
}
// 4. 初始化 mutex 属性(关键:设置为跨进程共享+健壮模式)
pthread_mutexattr_t mutexAttr;
pthread_mutexattr_init(&mutexAttr);
// 允许 mutex 在进程间共享
pthread_mutexattr_setpshared(&mutexAttr, PTHREAD_PROCESS_SHARED);
// 开启健壮模式:当持有者崩溃时,其他进程能检测到并恢复
pthread_mutexattr_setrobust(&mutexAttr, PTHREAD_MUTEX_ROBUST);
// 5. 首次初始化共享内存中的锁(refCnt 为 0 表示未初始化)
if (mSharedData->refCnt == 0) {
pthread_mutex_init(&mSharedData->dataMutex, &mutexAttr);
pthread_mutex_init(&mSharedData->waitMutex, &mutexAttr);
mSharedData->refCnt = 0;
mSharedData->waitCnt = 0;
}
pthread_mutexattr_destroy(&mutexAttr);
}
void ProcMutex::Lock() {
if (!mSharedData) return;
// 先尝试加锁,避免直接阻塞
int rc = pthread_mutex_trylock(&mSharedData->dataMutex);
if (rc != 0) {
AddWait(); // 加锁失败,增加等待计数
rc = pthread_mutex_lock(&mSharedData->dataMutex);
// 若持有者崩溃,尝试恢复锁状态(健壮模式的核心作用)
if (rc == EOWNERDEAD) {
SPR_LOGW("Mutex owner died, trying to recover\n");
if (pthread_mutex_consistent(&mSharedData->dataMutex) != 0) {
SPR_LOGE("Failed to make mutex consistent\n");
DelWait();
return;
}
rc = 0;
}
if (rc != 0) {
SPR_LOGE("pthread_mutex_lock failed: %s\n", strerror(rc));
DelWait();
return;
}
DelWait(); // 加锁成功,减少等待计数
}
// 原子增加引用计数(线程安全)
mSharedData->refCnt.fetch_add(1, std::memory_order_relaxed);
}
ProcLockGuardProcLockGuard::ProcLockGuard(ProcMutex& pMutex, std::mutex& tMutex)
: mTMutex(tMutex), mPMutex(pMutex) {
mTMutex.lock(); // 先锁线程内 mutex(防止同一进程内多线程竞争)
mPMutex.Lock(); // 再锁进程间 mutex
}
ProcLockGuard::~ProcLockGuard() {
mPMutex.Unlock(); // 先释放进程间锁
mTMutex.unlock(); // 再释放线程内锁
}
const std::string DEMO_SHARED_MUTEX = "demo_shared_mutex";
void childProcessWork(int processId, bool isCrashProcess)
{
std::mutex threadMutex;
ProcMutex procMutex(DEMO_SHARED_MUTEX);
SPR_LOG("进程 %d: 启动,准备竞争锁...\n", processId);
// 循环尝试访问共享资源
for (int i = 0; i < 3; ++i) {
ProcLockGuard lockGuard(procMutex, threadMutex);
SPR_LOG("进程 %d: 成功获取锁,正在访问资源(第%d次)\n", processId, i+1);
sleep(1);
// 特定进程在持有锁时异常退出(模拟崩溃)
if (isCrashProcess && i == 1) {
SPR_LOG("进程 %d: 即将异常退出(持有锁状态)!\n", processId);
std::abort();
}
SPR_LOG("进程 %d: 释放锁,等待下一次竞争...\n", processId);
}
SPR_LOG("进程 %d: 正常退出\n", processId);
}
int main()
{
constint NUM_PROCESSES = 3; // 总进程数
constint CRASH_PROCESS_ID = 1; // 指定崩溃的进程ID
SPR_LOG("=== 跨进程互斥锁演示程序启动 === \n"
"演示内容: \n"
"1. %d个进程竞争同一个共享资源 \n"
"2. 进程 %d: 在持有锁时异常退出 \n"
"3. 验证其他进程能否检测并恢复锁状态 \n\n", NUM_PROCESSES, CRASH_PROCESS_ID);
srand(time(nullptr));
std::vector<pid_t> childPids;
// 创建多个子进程
for (int i = 0; i < NUM_PROCESSES; ++i) {
pid_t pid = fork();
if (pid < 0) {
SPR_LOG("fork失败: %s\n", strerror(errno));
return EXIT_FAILURE;
} elseif (pid == 0) {
// 子进程逻辑:第 CRASH_PROCESS_ID 个进程会崩溃
childProcessWork(i + 1, (i + 1 == CRASH_PROCESS_ID));
return EXIT_SUCCESS;
} else {
childPids.push_back(pid);
usleep(50000); // 错开进程启动时间
}
}
// 父进程等待所有子进程结束
int status;
for (pid_t pid : childPids) {
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
SPR_LOG("父进程:子进程 %d: 正常退出,退出码 %d\n", pid, WEXITSTATUS(status));
} elseif (WIFSIGNALED(status)) {
SPR_LOG("父进程:子进程 %d: 异常退出,信号 %d\n", pid, WTERMSIG(status));
}
}
SPR_LOG("\n=== 演示结束 ===\n");
return EXIT_SUCCESS;
}
$ ./01_proc_guard
=== 跨进程互斥锁演示程序启动 ===
演示内容:
1. 3个进程竞争同一个共享资源
2. 进程 1: 在持有锁时异常退出
3. 验证其他进程能否检测并恢复锁状态
进程 1: 启动,准备竞争锁...
进程 1: 成功获取锁,正在访问资源(第1次)
进程 2: 启动,准备竞争锁...
进程 3: 启动,准备竞争锁...
进程 1: 释放锁,等待下一次竞争...
进程 1: 成功获取锁,正在访问资源(第2次)
进程 1: 即将异常退出(持有锁状态)!
174 ProcMutex W: Mutex owner died, trying to recover
进程 3: 成功获取锁,正在访问资源(第1次)
父进程:子进程 15076: 异常退出,信号 6
进程 3: 释放锁,等待下一次竞争...
进程 3: 成功获取锁,正在访问资源(第2次)
进程 3: 释放锁,等待下一次竞争...
进程 3: 成功获取锁,正在访问资源(第3次)
进程 3: 释放锁,等待下一次竞争...
进程 3: 正常退出
进程 2: 成功获取锁,正在访问资源(第1次)
进程 2: 释放锁,等待下一次竞争...
进程 2: 成功获取锁,正在访问资源(第2次)
进程 2: 释放锁,等待下一次竞争...
进程 2: 成功获取锁,正在访问资源(第3次)
进程 2: 释放锁,等待下一次竞争...
进程 2: 正常退出
父进程:子进程 15077: 正常退出,退出码 0
父进程:子进程 15078: 正常退出,退出码 0
=== 演示结束 ===
通过演示能够发现进程1在持锁状态崩溃时,进程2和进程3能够从阻塞状态恢复并正常获取锁。
主要验证多进程对临界区竞争的保护效果,测试也能够正常通过。由于篇幅过长,不再赘述,可在文末获取源码。
ProcMutex 通过共享内存+健壮 mutex 解决了跨进程同步问题,尤其对“进程崩溃导致锁泄露”的场景做了容错处理,提升了系统稳定性。mutex 一致,ProcLockGuard 实现 RAII 自动管理,降低了手动加解锁的出错风险。TimedLock)、锁状态查询等接口,后续再增加。用心感悟,认真记录,写好每一篇文章,分享每一框干货。
更多文章内容包括但不限于C/C++、Linux、开发常用神器等,可进入“开源519公众号”聊天界面输入“文章目录” 或者 菜单栏选择“文章目录”查看。公众号后台聊天框输入本文标题,在线查看源码。 在聊天框输入“开源519资料” 获取Linux C/C++ 学习资料书籍