30dayMakeOS多任务同步:信号量与互斥锁实现

30dayMakeOS多任务同步:信号量与互斥锁实现

【免费下载链接】30dayMakeOS 《30天自制操作系统》源码中文版。自己制作一个操作系统(OSASK)的过程 【免费下载链接】30dayMakeOS 项目地址: https://gitcode.com/gh_mirrors/30/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;                   // 当前级别与切换标志
};

这种基于优先级的抢占式调度虽然实现了多任务并发,但缺乏关键同步原语,导致三大风险:

  1. 共享资源竞争:当两个任务同时写入同一显存区域时,会出现屏幕花屏现象
  2. 设备操作冲突:多个任务同时调用wait_KBC_sendready()可能导致键盘控制器数据错乱
  3. 任务执行顺序不可控:关键初始化任务未完成时,依赖它的其他任务可能提前运行

同步需求场景分析

通过搜索项目源码中的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);  // 释放显存访问权
}

互斥锁设计:解决关键资源独占问题

互斥锁与信号量的差异

互斥锁是信号量的特殊形式(二值信号量),但增加了三个关键特性:

  1. 所有权:记录当前持有锁的任务,防止非持有者释放
  2. 优先级继承:高优先级任务等待低优先级任务持有的锁时,提升低优先级
  3. 递归加锁:允许同一任务多次获取同一把锁而不死锁

互斥锁实现代码

// 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);
}

性能优化与死锁调试

同步机制性能优化技巧

  1. 减少锁粒度:将一个大锁拆分为多个小锁,如将显存按区域划分多个锁
  2. 锁省略优化:对只读操作不加锁,使用volatile关键字确保内存可见性
  3. 等待队列优化:使用双向链表替代FIFO数组,支持任务优先级排序
  4. 自旋锁替代:对极短时间的临界区,使用自旋锁减少上下文切换开销
  5. 批量操作合并:将多次共享资源访问合并为一次,减少加解锁次数

死锁检测与调试

实现简单的死锁检测机制:

// 死锁检测代码
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操作
  • ✅ 互斥锁与递归加锁支持
  • ✅ 优先级继承防优先级反转
  • ✅ 设备驱动同步改造
  • ✅ 死锁检测基础功能

后续扩展方向

  1. 条件变量:实现pthread_cond_wait类似功能,支持复杂条件等待
  2. 读写锁:区分读操作和写操作,提高共享资源并发访问效率
  3. 信号量集:允许任务同时等待多个信号量,实现更复杂的同步逻辑
  4. 实时调度:添加截止时间监控,确保关键任务按时完成
  5. 用户态同步:将同步原语暴露给用户程序,支持应用级线程同步

关键代码清单

为方便读者快速集成,这里提供核心文件完整清单:

  1. semaphore.h - 信号量与互斥锁数据结构定义
  2. semaphore.c - P/V操作与优先级继承实现
  3. mtask.c - 修改后的任务调度与同步协同代码
  4. keyboard.c - 带互斥锁的键盘驱动实现
  5. console.c - 线程安全的控制台输出函数

通过本文实现的同步机制,30dayMakeOS从简单的多任务内核进化为真正支持并发控制的操作系统原型。这些同步原语不仅解决了当前系统的稳定性问题,更为后续文件系统、网络协议栈等复杂模块的实现奠定了基础。

点赞+收藏+关注,获取下一期《30dayMakeOS内存管理:从伙伴系统到虚拟内存》的更新通知!在评论区留下你在实现同步机制时遇到的问题,将在后续文章中解答。

【免费下载链接】30dayMakeOS 《30天自制操作系统》源码中文版。自己制作一个操作系统(OSASK)的过程 【免费下载链接】30dayMakeOS 项目地址: https://gitcode.com/gh_mirrors/30/30dayMakeOS

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值