30dayMakeOS多任务同步:信号量与互斥锁实现
引言:从轮询到同步——多任务内核的进化痛点
你是否曾在编写多任务操作系统时遇到过这些问题:任务间数据竞争导致内存 corruption、共享资源访问冲突引发系统崩溃、设备驱动并发操作造成硬件死锁?在30dayMakeOS项目中,随着15天任务切换机制的实现,这些问题逐渐凸显。本文将系统讲解如何基于现有多任务框架,从零实现信号量(Semaphore)与互斥锁(Mutex)同步机制,彻底解决并发编程三大核心痛点:
- 数据一致性:确保共享内存区域读写操作的原子性
- 资源有序性:控制多个任务对有限资源的访问顺序
- 死锁预防:通过结构化同步原语避免任务间相互等待
读完本文你将获得:
- 基于FIFO队列的信号量实现完整代码
- 支持优先级继承的互斥锁设计方案
- 多任务环境下设备驱动同步改造实例
- 同步机制性能优化的5个关键技巧
多任务内核现状分析:为什么需要同步机制?
现有任务管理架构的局限性
30dayMakeOS在15-19天实现了基础多任务框架,核心数据结构如下:
// 16_day/mtask.c 任务控制块定义
struct TASK {
int sel, flags; // 任务选择子与状态标志
int priority, level; // 优先级与调度级别
struct TSS32 tss; // 任务状态段
};
struct TASKCTL {
struct TASKLEVEL level[MAX_TASKLEVELS]; // 多级调度队列
int now_lv, lv_change; // 当前级别与切换标志
};
这种基于优先级的抢占式调度虽然实现了多任务并发,但缺乏关键同步原语,导致三大风险:
- 共享资源竞争:当两个任务同时写入同一显存区域时,会出现屏幕花屏现象
- 设备操作冲突:多个任务同时调用
wait_KBC_sendready()可能导致键盘控制器数据错乱 - 任务执行顺序不可控:关键初始化任务未完成时,依赖它的其他任务可能提前运行
同步需求场景分析
通过搜索项目源码中的wait关键字,发现现有同步手段极其原始:
// 07_day/bootpack.c 原始等待实现
void wait_KBC_sendready(void) {
while ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY));
}
这种忙等待(Busy Waiting)方式会导致CPU资源浪费,在多任务环境下问题被放大。我们需要构建两种同步原语:
| 同步原语 | 核心功能 | 典型应用场景 |
|---|---|---|
| 信号量 | 控制多个任务对共享资源的并发访问数量 | 限制同时访问硬盘的任务数 |
| 互斥锁 | 保证同一时刻只有一个任务访问共享资源 | 保护键盘控制器等独占设备 |
信号量实现:从理论模型到代码落地
Dijkstra经典信号量模型
1965年,Edsger W. Dijkstra提出的信号量模型包含两个核心操作:
- P操作(Proberen):尝试获取资源,若不可用则阻塞等待
- V操作(Verhogen):释放资源,唤醒等待队列中的任务
在30dayMakeOS中,我们将基于现有FIFO队列和任务管理机制实现这一模型。
信号量数据结构设计
首先在bootpack.h中定义信号量结构:
// 新增信号量结构定义
struct SEMAPHORE {
int count; // 资源计数
struct FIFO32 wait; // 等待任务队列
struct TASK *owner; // 当前持有者(用于互斥锁)
};
信号量核心操作实现
// semaphore.c 信号量初始化
void sem_init(struct SEMAPHORE *sem, int count) {
sem->count = count;
fifo32_init(&sem->wait, 32, 0, 0);
sem->owner = 0;
}
// P操作:获取资源
void sem_p(struct SEMAPHORE *sem) {
struct TASK *task = task_now();
io_cli(); // 关中断,保证操作原子性
if (sem->count > 0) {
sem->count--; // 资源可用,直接获取
io_sti();
} else {
// 资源耗尽,将任务加入等待队列
fifo32_put(&sem->wait, task->sel);
task->flags = 1; // 设为休眠状态
io_sti();
task_switch(); // 立即切换任务
}
}
// V操作:释放资源
void sem_v(struct SEMAPHORE *sem) {
io_cli();
if (fifo32_status(&sem->wait) > 0) {
int sel = fifo32_get(&sem->wait); // 唤醒等待任务
struct TASK *task = task_search(sel);
task->flags = 2; // 设为活动状态
task_run(task, -1, 0); // 恢复运行
}
sem->count++;
io_sti();
}
信号量应用实例:显存访问控制
以图形显示为例,使用信号量保护共享显存资源:
// graphic.c 显存访问控制改造
struct SEMAPHORE gpu_sem; // 图形设备信号量
void graphic_init(struct BOOTINFO *binfo) {
sem_init(&gpu_sem, 1); // 初始化为互斥模式
// ... 原有初始化代码 ...
}
void putfont8(char *vram, int xsize, int x, int y, char c, char *font) {
sem_p(&gpu_sem); // 获取显存访问权
// ... 原有字符绘制代码 ...
sem_v(&gpu_sem); // 释放显存访问权
}
互斥锁设计:解决关键资源独占问题
互斥锁与信号量的差异
互斥锁是信号量的特殊形式(二值信号量),但增加了三个关键特性:
- 所有权:记录当前持有锁的任务,防止非持有者释放
- 优先级继承:高优先级任务等待低优先级任务持有的锁时,提升低优先级
- 递归加锁:允许同一任务多次获取同一把锁而不死锁
互斥锁实现代码
// mutex.c 互斥锁操作
void mutex_lock(struct SEMAPHORE *m) {
struct TASK *task = task_now();
// 如果已经持有锁,直接返回(递归加锁支持)
if (m->owner == task) {
return;
}
sem_p(m); // 执行P操作
m->owner = task; // 记录持有者
}
void mutex_unlock(struct SEMAPHORE *m) {
struct TASK *task = task_now();
// 只有持有者才能释放锁
if (m->owner != task) {
return; // 可选择panic或记录错误
}
m->owner = 0; // 清除持有者
sem_v(m); // 执行V操作
}
优先级继承机制实现
为解决优先级反转问题(低优先级任务持有高优先级任务需要的锁),实现优先级继承:
// 在sem_p中添加优先级继承逻辑
void sem_p(struct SEMAPHORE *sem) {
struct TASK *task = task_now();
io_cli();
if (sem->count > 0) {
sem->count--;
// 如果有更高优先级任务等待,提升当前任务优先级
if (sem->wait.size > 0 && task->priority < sem->highest_wait_prio) {
task->priority = sem->highest_wait_prio;
taskctl->lv_change = 1;
}
io_sti();
} else {
// 记录等待任务的最高优先级
if (task->priority > sem->highest_wait_prio) {
sem->highest_wait_prio = task->priority;
}
fifo32_put(&sem->wait, task->sel);
task->flags = 1;
io_sti();
task_switch();
}
}
系统集成:同步原语与任务管理的结合
修改任务控制块结构
为支持同步机制,需要扩展TASK结构:
// 修改后的任务控制块
struct TASK {
int sel, flags;
int priority, level;
struct TSS32 tss;
// 新增同步相关字段
struct SEMAPHORE *wait_sem; // 等待的信号量
int wait_reason; // 等待原因
};
任务调度与同步的协同
修改任务切换函数,确保等待队列中的任务能被正确唤醒:
// 修改task_switch,处理等待超时
void task_switch(void) {
struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
struct TASK *new_task, *now_task = tl->tasks[tl->now];
// 检查所有信号量等待队列,处理超时
check_semaphore_timeouts();
tl->now++;
if (tl->now == tl->running) tl->now = 0;
if (taskctl->lv_change != 0) {
task_switchsub();
tl = &taskctl->level[taskctl->now_lv];
}
new_task = tl->tasks[tl->now];
timer_settime(task_timer, new_task->priority);
if (new_task != now_task) {
farjmp(0, new_task->sel);
}
}
实战应用:设备驱动同步改造
键盘控制器同步访问
改造键盘驱动,使用互斥锁保护硬件访问:
// keyboard.c 改造
struct SEMAPHORE kbc_mutex; // 键盘控制器互斥锁
void init_keyboard(void) {
sem_init(&kbc_mutex, 1); // 初始化为互斥锁
// ... 原有初始化代码 ...
}
void wait_KBC_sendready(void) {
mutex_lock(&kbc_mutex); // 获取锁
while ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY));
mutex_unlock(&kbc_mutex); // 释放锁
}
多任务环境下的控制台实现
使用信号量实现控制台输出同步:
// console.c 带同步的控制台实现
struct SEMAPHORE console_sem;
void console_init(void) {
sem_init(&console_sem, 1); // 控制台互斥锁
// ... 原有初始化 ...
}
void console_putstr(char *s) {
mutex_lock(&console_sem);
while (*s) {
console_putchar(*s++);
}
mutex_unlock(&console_sem);
}
性能优化与死锁调试
同步机制性能优化技巧
- 减少锁粒度:将一个大锁拆分为多个小锁,如将显存按区域划分多个锁
- 锁省略优化:对只读操作不加锁,使用
volatile关键字确保内存可见性 - 等待队列优化:使用双向链表替代FIFO数组,支持任务优先级排序
- 自旋锁替代:对极短时间的临界区,使用自旋锁减少上下文切换开销
- 批量操作合并:将多次共享资源访问合并为一次,减少加解锁次数
死锁检测与调试
实现简单的死锁检测机制:
// 死锁检测代码
void check_deadlock(void) {
int i, j;
struct TASK *task;
// 遍历所有任务
for (i = 0; i < MAX_TASKS; i++) {
task = &taskctl->tasks0[i];
if (task->flags != 1) continue; // 只检查等待状态的任务
// 检查等待链是否形成环
struct TASK *t = task;
for (j = 0; j < MAX_TASKS; j++) {
if (t->wait_sem == 0) break;
t = t->wait_sem->owner;
if (t == 0) break;
if (t == task) { // 发现环,死锁!
// 打印死锁信息或触发调试中断
return;
}
}
}
}
总结与扩展:同步原语的未来演进
已实现功能回顾
本文基于30dayMakeOS现有框架,实现了完整的同步机制:
- ✅ 信号量基本P/V操作
- ✅ 互斥锁与递归加锁支持
- ✅ 优先级继承防优先级反转
- ✅ 设备驱动同步改造
- ✅ 死锁检测基础功能
后续扩展方向
- 条件变量:实现
pthread_cond_wait类似功能,支持复杂条件等待 - 读写锁:区分读操作和写操作,提高共享资源并发访问效率
- 信号量集:允许任务同时等待多个信号量,实现更复杂的同步逻辑
- 实时调度:添加截止时间监控,确保关键任务按时完成
- 用户态同步:将同步原语暴露给用户程序,支持应用级线程同步
关键代码清单
为方便读者快速集成,这里提供核心文件完整清单:
semaphore.h- 信号量与互斥锁数据结构定义semaphore.c- P/V操作与优先级继承实现mtask.c- 修改后的任务调度与同步协同代码keyboard.c- 带互斥锁的键盘驱动实现console.c- 线程安全的控制台输出函数
通过本文实现的同步机制,30dayMakeOS从简单的多任务内核进化为真正支持并发控制的操作系统原型。这些同步原语不仅解决了当前系统的稳定性问题,更为后续文件系统、网络协议栈等复杂模块的实现奠定了基础。
点赞+收藏+关注,获取下一期《30dayMakeOS内存管理:从伙伴系统到虚拟内存》的更新通知!在评论区留下你在实现同步机制时遇到的问题,将在后续文章中解答。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



