跨进程同步难题?一个通用进程互斥锁的封装与实现

  • Home
  • 卡牌图鉴
  • 跨进程同步难题?一个通用进程互斥锁的封装与实现

❝脚步不停,终达卓越!更多底层开发技巧,欢迎关注公众号《开源519》

引言 在多进程开发中,共享资源(如配置文件、设备接口、共享内存块)的竞争访问是常见场景。若不加以控制,轻则导致数据错乱,重则引发进程崩溃。

传统解决方案中: 信号量(semaphore) 虽能实现跨进程同步,但接口设计繁琐(需手动管理计数);普通线程 mutex 仅能在单进程内生效,无法跨进程协同。更棘手的是,若持有锁的进程意外崩溃,未释放的锁可能导致其他进程永久阻塞——这类 “锁泄露” 问题往往难以排查。

秉持一贯的优雅编程理念,基于共享内存与 pthread 接口,设计并实现一个开箱即用、兼顾安全与易用的进程间互斥锁 ProcMutex 类,支持跨进程同步与异常安全,并通过 RAII 机制简化锁的生命周期管理。

需求分析 一个可靠的进程间互斥锁需满足以下核心需求:

跨进程有效性:能在多个独立进程间同步,而非局限于单进程内的线程。异常容错:当持有锁的进程崩溃时,其他进程能检测并恢复锁状态,避免永久阻塞。线程安全:同一进程内的多个线程调用时,不会出现状态错乱。易用性:提供类似普通 mutex 的 Lock()/Unlock() 接口,最好支持自动释放(避免手动解锁遗漏)。资源自动管理:进程退出时,自动清理共享资源(如共享内存),避免残留。详细设计 基于上述需求,ProcMutex 类的设计围绕 “共享内存存状态,pthread 锁做同步,原子变量保安全” 展开,核心代码如下:

核心结构与类定义(ProcMutex.h)代码语言:javascript复制#ifndef __PROC_MUTEX_H__

#define __PROC_MUTEX_H__

#include

#include

#include

#include

// 共享内存中的数据结构,存储锁状态与计数

struct SharedData {

std::atomic refCnt; // 引用计数(当前持有锁的次数)

std::atomic 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__

实现细节(ProcMutex.cpp) 核心逻辑集中在共享内存初始化、锁状态管理与异常处理,关键代码解析如下:

初始化:共享内存与跨进程锁

① 通过共享内存和pthread_mutexattr_setpshared 将互斥锁允许进程共享。

② 通过pthread_mutexattr_setrobust 开启健壮模式,持有者崩溃时,其他进程能检测到并恢复。代码语言:javascript复制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(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);

}

加锁与异常处理代码语言:javascript复制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);

}

RAII 自动管理:ProcLockGuard代码语言:javascript复制ProcLockGuard::ProcLockGuard(ProcMutex& pMutex, std::mutex& tMutex)

: mTMutex(tMutex), mPMutex(pMutex) {

mTMutex.lock(); // 先锁线程内 mutex(防止同一进程内多线程竞争)

mPMutex.Lock(); // 再锁进程间 mutex

}

ProcLockGuard::~ProcLockGuard() {

mPMutex.Unlock(); // 先释放进程间锁

mTMutex.unlock(); // 再释放线程内锁

}

实例使用持锁进程崩溃演示示例代码

验证当持锁进程崩溃时,其他进程能否从阻塞状态恢复并成功获取锁。代码语言:javascript复制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 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;

}

效果代码语言:javascript复制$ ./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++ 学习资料书籍