Linux 信号量
-
- 一、信号量基础概念
-
- [1.1 同步机制的核心需求](#1.1 同步机制的核心需求)
- [1.2 信号量的核心原理](#1.2 信号量的核心原理)
- [1.3 信号量类型对比](#1.3 信号量类型对比)
- 二、实战代码解析
-
- [2.1 共享内存与信号量结合示例](#2.1 共享内存与信号量结合示例)
- [2.2 信号量类实现要点](#2.2 信号量类实现要点)
- 三、关键实现细节分析
-
- [3.1 初始化三步骤](#3.1 初始化三步骤)
- [3.2 SEM_UNDO机制](#3.2 SEM_UNDO机制)
- [3.3 原子操作保证](#3.3 原子操作保证)
- 四、进阶应用场景
-
- [4.1 生产者-消费者模型](#4.1 生产者-消费者模型)
- [4.2 读写锁实现](#4.2 读写锁实现)
- 五、最佳实践建议
- 六、常见问题排查
- 七、现代替代方案
一、信号量基础概念
1.1 同步机制的核心需求
在多进程/多线程编程中,当多个执行单元需要访问共享资源时,必须引入同步机制来保证数据一致性。信号量(Semaphore)正是解决这一问题的经典方案。
1.2 信号量的核心原理
信号量本质上是一个计数器,通过两个原子操作实现进程同步:
- P操作(wait):申请资源,计数器减1
- V操作(post):释放资源,计数器加1
1.3 信号量类型对比
类型 | System V信号量 | POSIX信号量 |
---|---|---|
初始化方式 | 需要显式初始化 | 可静态初始化 |
作用域 | 系统级 | 进程级 |
性能 | 较高开销 | 较低开销 |
功能复杂度 | 支持信号量集合 | 仅支持单个信号量 |
二、实战代码解析
2.1 共享内存与信号量结合示例
cpp
// 演示使用信号量给共享内存加锁
#include "_public.h"
struct stgirl {
int no;
char name[51];
};
int main(int argc, char* argv[]) {
if (argc != 3) {
cout << "Usage: ./test no name\n";
return -1;
}
// 创建/获取共享内存
int shmid = shmget(0x5005, sizeof(stgirl), 0640|IPC_CREAT);
stgirl* ptr = (stgirl*)shmat(shmid, 0, 0);
// 初始化信号量
csemp mutex;
mutex.init(0x5005);
// 临界区保护
cout << "申请加锁...\n";
mutex.wait();
// 操作共享数据
cout << "原值: no=" << ptr->no << ", name=" << ptr->name << endl;
ptr->no = atoi(argv[1]);
strcpy(ptr->name, argv[2]);
sleep(10); // 模拟耗时操作
mutex.post();
shmdt(ptr);
return 0;
}
2.2 信号量类实现要点
cpp
class csemp {
public:
bool init(key_t key, unsigned short value=1, short sem_flg=SEM_UNDO);
bool wait(short sem_op=-1);
bool post(short sem_op=1);
// ...其他成员函数
private:
int m_semid;
short m_sem_flg;
};
// 初始化流程图
graph TD
A[开始] --> B{信号量是否存在?}
B -- 存在 --> C[直接获取]
B -- 不存在 --> D[创建新信号量]
D --> E[设置初始值]
E --> F[初始化完成]
三、关键实现细节分析
3.1 初始化三步骤
- 尝试获取现有信号量
- 创建新信号量(IPC_EXCL保证原子性)
- 设置初始值(仅创建者需要)
3.2 SEM_UNDO机制
- 作用:防止进程异常终止导致的死锁
- 实现方式:内核维护调整记录
- 适用场景:建议用于互斥锁场景
3.3 原子操作保证
cpp
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; // P操作
sem_b.sem_flg = SEM_UNDO;
semop(m_semid, &sem_b, 1);
四、进阶应用场景
4.1 生产者-消费者模型
cpp
// 初始化两个信号量
csemp empty(10); // 缓冲区空位
csemp full(0); // 已填充数量
// 生产者
empty.wait();
// 生产数据...
full.post();
// 消费者
full.wait();
// 消费数据...
empty.post();
4.2 读写锁实现
cpp
csemp mutex(1); // 互斥锁
csemp writeLock(1);// 写锁
int readers = 0;
// 读锁定
mutex.wait();
readers++;
if (readers == 1) writeLock.wait();
mutex.post();
// 读解锁
mutex.wait();
readers--;
if (readers == 0) writeLock.post();
mutex.post();
// 写锁定
writeLock.wait();
// 写解锁
writeLock.post();
五、最佳实践建议
-
命名规范
- 使用ftok生成唯一key
- 示例:
key_t key = ftok("/tmp", 'A');
-
错误处理
cppif (semop(...) == -1) { if (errno == EINTR) { // 处理信号中断 } // 其他错误处理 }
-
性能优化
- 优先考虑POSIX信号量
- 避免过度使用信号量集合
- 设置合理的超时机制
-
调试技巧
- 使用
ipcs -s
查看信号量状态 - 通过
semctl
获取当前值
- 使用
六、常见问题排查
-
ENOSPC错误
- 原因:系统信号量数量达到上限
- 解决:
sysctl -w kernel.sem="250 32000 100 128"
-
EIDRM错误
- 现象:信号量被意外删除
- 预防:增加引用计数机制
-
死锁检测
- 使用
pstack
分析进程堆栈 - 借助valgrind工具链检测
- 使用
七、现代替代方案
-
原子变量
cppstd::atomic<int> counter(0); counter.fetch_add(1, std::memory_order_relaxed);
-
文件锁
cppint fd = open("lockfile", O_CREAT|O_RDWR, 0644); flock(fd, LOCK_EX);
-
RCU机制
- 适用读多写少场景
- 无锁读取设计
信号量作为经典的进程同步工具,在系统级编程中仍具有重要地位。理解其底层机制并结合现代编程范式,能够帮助开发者构建更健壮的并发系统。在实际应用中,需要根据具体场景选择最合适的同步策略,平衡性能与安全性。