3. 计数信号量:创建与配置、资源计数管理、典型应用场景(资源池管理)
好,咱们接着聊信号量。前面讲了二值信号量,说白了就是个开关——要么开、要么关。但实际项目中,资源往往不止一个。比如一个串口缓冲池有10个缓冲区,或者一个内存池有20个块。这时候二值信号量就不够用了。
计数信号量就是干这个的。它维护一个计数器,记录当前可用资源的数量。我刚开始用VxWorks时,总觉得计数信号量就是二值信号量的升级版,后来踩了坑才发现——没那么简单。
3.1 计数信号量的创建与配置
创建计数信号量,核心函数就一个:semCCreate()。它的原型长这样:
SEM_ID semCCreate(
int options, // 选项:优先级队列或FIFO队列
int initialCount // 初始计数值
);
嗯,这里要注意两个参数。
第一个参数 options:
SEM_Q_PRIORITY:等待任务按优先级排队。高优先级的先拿到信号量。SEM_Q_FIFO:先来后到,谁先等谁先拿。
我个人习惯用 SEM_Q_PRIORITY。为什么?因为实时系统里,高优先级任务往往更紧急。你想想看,一个中断处理任务在等资源,结果被一个后台打印任务插队了,这合理吗?
第二个参数 initialCount:
这个值代表初始有多少资源可用。比如你有一个10个槽位的缓冲池,初始全部空闲,那就传10。如果一开始有3个被占用了,那就传7。
3.2 资源计数管理
计数信号量的核心操作就两个:semTake() 和 semGive()。
- semTake():计数器减1。如果计数器为0,任务阻塞等待。
- semGive():计数器加1。如果有任务在等,唤醒一个。
说白了,这就是一个「借」和「还」的过程。你借一个资源,计数器减1;你还回来,计数器加1。简单吧?
但坑往往藏在细节里。我曾经在一个项目中,用计数信号量管理DMA描述符池。每个描述符对应一个DMA传输任务。代码大概长这样:
/* 创建计数信号量,初始10个描述符可用 */
SEM_ID semDmaPool = semCCreate(SEM_Q_PRIORITY, 10);
/* 任务A:申请描述符 */
void taskA(void) {
semTake(semDmaPool, WAIT_FOREVER);
/* 拿到描述符,开始DMA传输 */
...
/* 传输完成,释放描述符 */
semGive(semDmaPool);
}
/* 任务B:同样申请描述符 */
void taskB(void) {
semTake(semDmaPool, WAIT_FOREVER);
/* 拿到描述符,开始DMA传输 */
...
semGive(semDmaPool);
}
看起来没问题对吧?但实际跑起来,系统偶尔会死锁。排查了很久才发现——有个任务在拿到描述符后,因为DMA传输超时,直接返回了,忘了调用 semGive()。结果计数器少了一个,资源池慢慢枯竭。
semTake() 和 semGive() 必须成对出现,而且尽量放在同一个函数内。如果跨函数传递,一定要在注释里标明。
3.3 典型应用场景:资源池管理
计数信号量最经典的应用,就是管理资源池。比如网络协议栈里的mbuf池、文件系统里的缓存块池、驱动里的DMA描述符池。
我给大家画个图(嗯,用文字描述):
资源池 = 一组相同类型的资源 + 一个计数信号量
每个资源有一个状态标志:空闲或占用。计数信号量的值,就是空闲资源的数量。
来看一个完整的例子:管理一个10个缓冲区的串口发送池。
/* 缓冲区结构 */
typedef struct {
BOOL inUse;
UINT8 data[256];
} BUF_NODE;
BUF_NODE bufPool[10];
SEM_ID semBufPool;
/* 初始化 */
void bufPoolInit(void) {
int i;
for (i = 0; i < 10; i++) {
bufPool[i].inUse = FALSE;
}
semBufPool = semCCreate(SEM_Q_PRIORITY, 10);
}
/* 申请缓冲区 */
BUF_NODE* bufAlloc(void) {
BUF_NODE* pBuf = NULL;
int i;
/* 等待可用资源 */
semTake(semBufPool, WAIT_FOREVER);
/* 查找空闲缓冲区 */
for (i = 0; i < 10; i++) {
if (!bufPool[i].inUse) {
bufPool[i].inUse = TRUE;
pBuf = &bufPool[i];
break;
}
}
return pBuf;
}
/* 释放缓冲区 */
void bufFree(BUF_NODE* pBuf) {
pBuf->inUse = FALSE;
semGive(semBufPool);
}
这个模式很常见。你想想看,如果没有计数信号量,多个任务同时申请缓冲区,就会出现竞争——两个任务可能拿到同一个缓冲区。有了计数信号量,semTake() 保证了「只有空闲资源时才放行」。
3.4 几个实用技巧
最后分享几个我实际项目中用到的技巧:
- 超时机制:
semTake()不要总用WAIT_FOREVER。我习惯给个超时时间,比如100ms。如果超时了,说明资源紧张,可以打印日志或者尝试降级处理。 - 资源泄漏检测:在调试阶段,可以用
semShow()查看当前信号量的计数值。如果发现计数值一直在减少,说明有任务拿了没还。 - 优先级反转:当使用
SEM_Q_PRIORITY时,要注意优先级反转问题。低优先级任务拿着资源,高优先级任务在等,中间优先级的任务插队运行——高优先级任务就被「反转」了。解决办法是使用优先级继承协议,但VxWorks的计数信号量默认不支持。需要自己实现或者用其他机制。
嗯,计数信号量就讲这么多。说白了,它就是管理「多个同类资源」的利器。你只要记住:创建时设好初始值,使用时成对Take/ Give,异常分支别忘了释放。做到这三点,基本不会出大问题。
下一节我们讲互斥信号量,那个更复杂一些,涉及到优先级继承和死锁预防。到时候我会分享一个我当年在航天项目中踩过的坑——嗯,那个故事挺有意思的。