操作系统实现:调度与多任务(二)信号量与同步、线程安全的 I/O

输入输出系统#

同步与锁#

代码

在多线程环境下,对于显存和端口的竞态访问将导致显存写入偏移位置计算错误导致的 GP 异常。为此引入信号量与同步机制

线程在等待信号量时将主动阻塞 (Block),直到被其它线程取消阻塞 (Unblock),只有线程自身可以将自己阻塞,线程无法强制使得其它线程阻塞。

void KrBlockThread(enum KR_THREAD_STATE state) {
    uint8_t              intr;
    enum KR_TASK_STRUCT *pt;

    KASSERT(state == KR_THREAD_STATE_BLOCKED || state == KR_THREAD_STATE_WAITING || state == KR_THREAD_STATE_HANGING);
    intr      = DisableIntr();
    pt        = KrGetRunningThreadPcb();
    pt->State = state;
    KrDefaultSchedule();
    SetIntrStatus(intr);
}

void KrUnblockThread(struct KR_INTR_STACK *thread) {
    uint8_t intr;

    KASSERT(thread->State == KR_THREAD_STATE_BLOCKED || thread->State == KR_THREAD_STATE_HANGING ||
            thread->State == KR_THREAD_STATE_WAITING);
    
    intr = DisableIntr();
    if (thread->State != KR_THREAD_STATE_READY) {
        if (KrListHasElement(&gReadyThreadList, &thread->SchedTag))
            KPANIC("KrUnblockThread: blocked thread in Ready List");
        KrInsertListHeader(&gReadyThreadList, &thread->SchedTag);
        thread->State = KR_THREAD_STATE_READY;
    }
    SetIntrStatus(intr);
}

要实现互斥锁,只需要实现二元信号量即可。

struct KR_BINARY_SEMAPHONE {
    BOOL           Value;
    struct KR_LIST Waiters;
};

void KrBinSemaInit(struct KR_BINARY_SEMAPHONE *semaphone, BOOL initVal);

void KrBinSemaAcquire(struct KR_BINARY_SEMAPHONE *semaphone);

void KrBinSemaRelease(struct KR_BINARY_SEMAPHONE *semaphone);
  • KR_BINARY_SEMAPHONE 结构体:表示二元信号量
    • Value: 布尔值(0或1),表示信号量是否可用(1可用,0不可用)。
    • Waiters: 等待队列,存储因信号量不可用而阻塞的线程。
    • 初始化(KrBinSemaInit): 所有二元信号量必须使用此函数初始化
    • 获取信号量(KrBinSemaAcquire):将当前线程加入 Waiters 队列, 阻塞当前线程,触发调度切换到其他线程。
    • 释放信号量(KrBinSemaRelease): 从队列头部取出一个等待线程,唤醒该线程,使其尝试重新获取信号量
    • 严格互斥Value 只能在 0 和 1 间切换,确保二进制语义

KR_LOCK 是二元信号量的一简单封装,表示互斥递归锁。

struct KR_LOCK {
    struct KR_TASK_STRUCT     *Holder;
    struct KR_BINARY_SEMAPHONE Semaphone;
    uint32_t                   HolderRepeatedCnt;
};

void KrLockInit(struct KR_LOCK *lock);

void KrLockAcquire(struct KR_LOCK *lock);

void KrLockRelease(struct KR_LOCK *lock);

HolderRepeatedCnt 确保了锁的持有者可以递归获取锁,也即允许同一个线程多次获取锁而不会造成死锁。同时其他线程必须等待锁被完全释放后才能获取。

console.c 实现了简单的控制台设备,使用锁确保了多线程情况下不会出现 GP 异常。

#ifndef __HIMUOS_LIB_DEVICE_CONSOLE_H
#define __HIMUOS_LIB_DEVICE_CONSOLE_H 1

void InitConsole(void);

void ConsoleAcquire(void);

void ConsoleRelease(void);

void ConsoleWriteStr(const char *str);

void ConsoleWriteLine(const char *str);

void ConsoleWriteChar(char ch);

void ConsoleWriteAddress(void *addr);

void ConsoleWriteInt(int val, int base);

#endif

键盘获取输入#

以下讨论均基于老式 PS/2 键盘。大多数现代键盘通过 USB 接口连接到 USB 控制器,遵循 HID 协议。USB 键盘通过 USB 主控制器(EHCI/XHCI)与系统通信,而非以下 PS/2 协议。现代计算机中,PS/2 控制器功能通常被集成到芯片组,通过模拟的 I/O 端口(0x60 和 0x64)与系统交互。无论如何,如果计算机支持 PS/2 兼容端口,那么以下代码都是可用的。

在历史发展过程中出现过 3 套键盘扫描码,无论用户使用的是那一套,都能被 i8042 兼容芯片转化为第一套扫描码并被操作系统的键盘中断处理程序接受。

一旦用户按下任意一个键,都会发送一个通码 (makecode), (如果)用户一直按着这个键,那就一直发送该通码,直到一松开该键就会发送该键对应的 断码(breakcode)。 对于第一套键盘码而言,

  1. 大部分键的扫描码都是一字节,而且总是有 +(80)16=
  2. 部分扩展键,可能由两字节组成, 它的通码是 0xe0, 0x?? 断码则是 0xe0, 0x?? + 0x80。
  3. 还有特殊键可能由四字节组成,不过我们对此不予处理。
  4. 通码的最高位(第八位)总是为 0, 断码总是为 1。
  5. 大部分操作系统不会对断码进行处理。

对于旧式 8086 兼容机,键盘中断信号来自于 8259A 上的主片 IR1 接口(也即中断号 0x21):

; 8259A 
VECTOR 0x20,ZERO
VECTOR 0x21,ZERO
VECTOR 0x22,ZERO
VECTOR 0x23,ZERO
VECTOR 0x24,ZERO
VECTOR 0x25,ZERO
VECTOR 0x26,ZERO
VECTOR 0x27,ZERO
VECTOR 0x28,ZERO
VECTOR 0x29,ZERO
VECTOR 0x2a,ZERO
VECTOR 0x2b,ZERO
VECTOR 0x2c,ZERO
VECTOR 0x2d,ZERO
VECTOR 0x2e,ZERO
VECTOR 0x2f,ZERO

在键盘中断处理程序通过读 0x60 来获取扫描码:

int scancode = inb(KBD_BUFFER_PORT);

端口一次只能传递一个字节,因此按一次部分特殊键(扩展键)可能引发两次中断(松开也会引发两次,共 4 次,四字节按键同理)

我们对四字节的按键不予处理,断码也不予处理。具体实现参见 keyboard.c

  • 通过 inb 读取键盘缓冲区端口(0x60)获取原始扫描码。
  • 若扫描码为 0xe0,标记下一次扫描码为扩展键(如方向键),并暂存扩展标识 kExtScancode
  • 若存在扩展标识,将扫描码高字节设为 0xe000 以标记扩展键,并重置标识
  • 通过扫描码最高位(0x80)判断是按下(Make Code)还是释放(Break Code), 若为释放事件,更新对应修饰键(Shift/Ctrl/Alt)的全局状态为 FALSE
  • 结合 Shift 和 CapsLock 状态决定符号键(如数字、标点),字母键切换

环形缓冲区#

iocbuf.c 实现了固定大小(128 字节)的基础的线程安全循环缓冲区实现,并提供支持生产者和消费者模型。

所谓环形缓冲区,就是固定大小的连续内存区域以循环队列方式访问的区域。当写入数据时,Head前移;读取数据时,Tail前移。

环形缓冲区确保了线程安全:

  • Lock:确保对缓冲区状态的检查和修改是原子操作,防止多个线程同时访问导致的数据不一致。
  • 阻塞与唤醒机制:
    • 当生产者发现缓冲区已满,调用IcbWait阻塞自己,等待消费者消费数据。
    • 当消费者读取数据后,检查是否有生产者等待,若有则调用IcbWakeup唤醒。
    • 反之,当消费者发现缓冲区为空,也会阻塞自己,等待生产者生产数据。

对于键盘设备而言,键盘中断处理程序为生产者,而未来的实现的 Shell、用户程序等则为消费者。

// keyboard.h
extern struct IO_CIR_BUFFER gKeyboardBuffer;

// keyboard.c
	ch = kKeymap[scancode][shift];
        if (ch) {
            if (!IcbFull(&gKeyboardBuffer)) {
                // PrintChar(ch);
                // 键盘中断处理程序作为生产者将获取到的字符放入缓冲区
                // 之后其它线程可以消费。
                IcbPut(&gKeyboardBuffer, ch);
            }
            return;
        }

作者:himu-qaq

出处:https://www.cnblogs.com/himu-qaq/p/18703017

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Himu  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示