信号量核心机制说明及实际应用(结合ArduPilot代码)
一、信号量的基本概念
信号量本质是一个整数计数器,用于跟踪共享资源的可用数量,或标记某个事件的发生。它通过定义两种原子操作(不可中断)来控制线程对资源的访问:
- P操作(等待/获取,Acquire):尝试获取资源使用权。若信号量值>0,将其减1并继续执行;若值=0,线程阻塞等待,直到信号量被释放。
- V操作(释放,Release):释放资源使用权。将信号量值加1,若有线程因等待该信号量而阻塞,唤醒其中一个线程。
信号量的核心作用是:
- 互斥(Mutex):确保同一时间只有一个线程访问共享资源(如共享数据、硬件外设)。
- 同步:协调多个线程的执行顺序(如线程A必须在事件X发生后才能执行,线程B负责触发事件X)。
二、信号量的两种常见类型
1. 二进制信号量(Binary Semaphore)
- 值只能为0或1,用于实现互斥(类似“锁”)。
- 初始值为1:表示资源可用。线程获取时(P操作)值变为0(资源占用),释放时(V操作)值变回1(资源释放)。
- 应用场景:保护共享数据(如传感器数据、配置参数),避免多线程并发修改导致的数据错乱。
2. 计数信号量(Counting Semaphore)
- 值可以是任意非负整数,用于控制对有限数量资源的并发访问(如3个缓冲区,最多允许3个线程同时使用)。
- 初始值为资源总数N:每个线程获取资源时值减1,释放时值加1。当值=0时,后续线程阻塞等待。
- 应用场景:限制同时访问某资源的线程数量(如网络连接池、IO设备队列)。
三、信号量的核心操作(原子性保证)
信号量的P/V操作必须是原子操作(不可被中断),否则会导致竞态条件(Race Condition)。例如,若两个线程同时执行P操作,可能都判断信号量值>0,导致超过1个线程获取资源。
以二进制信号量为例,操作逻辑如下(伪代码):
// P操作(获取资源)
void sem_wait(Semaphore *sem) {disable_interrupts(); // 关闭中断,确保操作原子性if (sem->value > 0) {sem->value--; // 资源可用,占用它} else {add_thread_to_wait_queue(sem); // 资源被占用,线程阻塞等待block_current_thread(); // 切换到其他线程}enable_interrupts(); // 恢复中断
}// V操作(释放资源)
void sem_post(Semaphore *sem) {disable_interrupts(); // 关闭中断,确保操作原子性if (wait_queue_not_empty(sem)) {wake_one_thread_from_queue(sem); // 唤醒一个等待线程} else {sem->value++; // 无等待线程,释放资源}enable_interrupts(); // 恢复中断
}
四、在ArduPilot中的实际应用(结合代码)
ArduPilot的ChibiOS调度器中大量使用二进制信号量实现线程同步,以下是典型场景:
1. 保护共享资源(如回调函数列表)
// 信号量定义(用于保护定时器回调列表)
chBSemObjectInit(&_timer_semaphore, false); // 初始值为0(ChibiOS中false表示初始值0,需先post)// 注册定时器回调(多线程可能并发调用)
void Scheduler::register_timer_process(AP_HAL::MemberProc proc) {chBSemWait(&_timer_semaphore); // P操作:获取信号量(若被占用则阻塞)// 临界区:修改共享的回调函数列表if (_num_timer_procs < MAX_PROCS) {_timer_proc[_num_timer_procs++] = proc;}chBSemSignal(&_timer_semaphore); // V操作:释放信号量
}
- 作用:
_timer_semaphore
确保_timer_proc
数组(定时器回调列表)的修改是原子操作,避免多线程同时添加回调导致的数组越界或数据错乱。 - ChibiOS接口:
chBSemWait
(P操作)、chBSemSignal
(V操作)用于二进制信号量。
2. 线程同步(如延迟预期管理)
// 信号量用于保护延迟预期的共享变量
WITH_SEMAPHORE(expect_delay_sem); // RAII模式:自动获取和释放信号量// 临界区:修改延迟预期的嵌套计数、开始时间等变量
if (ms == 0) {if (expect_delay_nesting > 0) {expect_delay_nesting--;}
} else {expect_delay_start = AP_HAL::millis();expect_delay_nesting++;
}
WITH_SEMAPHORE
宏:是ArduPilot封装的RAII(资源获取即初始化)工具,展开后等价于:{Semaphore::ScopedLock lock(expect_delay_sem); // 构造时P操作// 临界区代码 } // 析构时自动V操作,确保信号量释放(即使发生异常)
- 作用:保护
expect_delay_nesting
(延迟嵌套计数)等变量,避免主线程在修改时被其他线程打断,确保延迟预期逻辑的正确性。
3. 防止递归调用(如定时器回调)
void Scheduler::_run_timers() {if (_in_timer_proc) {return; // 若已在执行回调,直接返回(防止递归)}_in_timer_proc = true; // 标记正在执行// 临界区:执行所有注册的定时器回调for (int i = 0; i < _num_timer_procs; i++) {_timer_proc[i]();}_in_timer_proc = false; // 标记执行结束
}
- 结合信号量:虽然这里用标志位
_in_timer_proc
防止递归,但本质上与信号量的互斥逻辑一致——确保同一时间只有一个线程执行回调列表,避免回调函数内部再次触发_run_timers
导致的栈溢出或数据冲突。
五、信号量的常见问题与解决方案
1. 死锁(Deadlock)
- 现象:线程A持有信号量S1并等待S2,线程B持有S2并等待S1,双方永久阻塞。
- 举例:
// 线程A chBSemWait(S1); chBSemWait(S2); // 等待S2(被线程B持有)// 线程B chBSemWait(S2); chBSemWait(S1); // 等待S1(被线程A持有)
- 解决方案:
- 按固定顺序获取信号量(如先S1后S2)。
- 限制信号量持有时间(超时后释放并重试)。
- 使用ChibiOS的
chSemWaitTimeout
(带超时的P操作)。
2. 优先级反转(Priority Inversion)
- 现象:低优先级线程持有信号量,高优先级线程因等待该信号量而被阻塞,导致高优先级任务延迟。
- 举例:
- 低优先级线程L持有信号量S,正在执行。
- 中优先级线程M抢占L的CPU(因优先级更高),导致L无法释放S。
- 高优先级线程H等待S,却因M的抢占而迟迟无法执行(H优先级 > M > L,但H被L间接阻塞)。
- 解决方案:
- 优先级继承:ChibiOS支持“优先级继承协议”,当低优先级线程持有高优先级线程等待的信号量时,临时将低优先级线程提升到高优先级,确保其尽快释放资源。
- 缩短临界区:减少信号量的持有时间(如临界区仅包含必要操作)。
3. 性能开销
- 信号量操作涉及原子指令和可能的线程切换,频繁调用会增加CPU负担。
- 优化:
- 减少临界区大小(仅保护必要代码)。
- 对只读共享数据,可使用“无锁读取”(如复制到线程本地缓存)。
- 在实时性要求极高的场景(如传感器采样),使用中断屏蔽替代信号量(短期禁用中断,避免线程切换)。
信号量是多线程编程中解决同步与互斥问题的核心工具,其核心价值在于:
- 互斥保护:确保共享资源(如数据、硬件)同一时间仅被一个线程访问,避免数据竞争。
- 时序协调:控制线程执行顺序,满足实时系统中“事件驱动”的需求(如传感器数据就绪后再执行姿态解算)。