第十章-输入输出系统

Ⅰ.锁

本质是互斥操作

原因:针对公共资源访问时,临界区若不加以互斥限制,可能导致执行过程中突然的中断导致出现异常。

1.互斥过程

设定互斥量M为二值信号量,0/1,P-,V+,现有两个进程A、B共同访问公共资源R,则有

1.线程A访问R,P-,M=0

2.线程B访问R,P-,M<0,阻塞线程B,P+

3.线程A访问R结束,V+,唤醒线程B

4.线程B开始执行

阻塞功能 :阻塞是线程主动的行为 。己阻塞的线程是由别人来唤醒的,通常是锁的持有者。不让线程在就绪队列中出现就行了,这样线程便没有机会运行。

线程自主阻塞之后,保存当前的寄存器,栈信息,当唤醒后,并不会从头开始执行,而是从线程阻塞的部分开始重新执行,并且时间片依旧是阻塞前的数值。

(1)实现线程阻塞和唤醒
c 复制代码
/*	阻塞过程:
* 1.关中断
* 2.更改当前线程状态为阻塞态
* 3.调用schedule(),从就绪队列中取出下一个就绪线程列执行
* 4.开中断
*/
void thread_blocked(enum task_status state){
    // 原子操作,必须关中断
    enum intr_status old_status = intr_distable();
    // 只有是传入下面三种状态才可以被阻塞
    ASSERT((state == TASK_BLOCKED) || (state == TASK_HANGING) || (state == TASK_WAITING));
    struct task_struct * cur_thread = running_thread();
    cur_thread->status = state;
    schedule();
    // list_pop(&thread_ready_list, &cur_thread->general_tag);
    intr_set_status(old_status);
}
/* 唤醒线程
* 1.关中断
* 2.判断当前线程是否在就绪队列中
* 3.不在,则添加;在,则PANIC
* 4.当前线程加入就绪队列
* 5.修改状态为TASK_READY
* 6.开中断
*/
// 被阻塞的进程由于没有执行,因此不可以调度schedule()函数
void thread_unblocked(struct task_struct* pthread){
    enum intr_status old_status = intr_disable();
    ASSERT((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_HANGING) || (pthread->status == TASK_WAITING));
    if(pthread->status != TASK_READY){
        ASSERT(!elem_find(&thread_ready_list, &pthread->general_tag));
        if(elem_find(&thread_ready_list, &pthread->general_tag)){
            PANIC("thread_unblock:blocked thread in ready_list");
        }
        list_push(&thread_ready_list, &pthread->general_tag);
        pthread->status = TASK_RUNNING;
    }
    intr_set_status(old_status);
}
(2)实现PV操作
c 复制代码
/* P操作,存在线程阻塞
* 1.关中断
* 2.判断当前semophore==0
* 3.为0,循环等待,并将当前线程加入信号量等待队列,并阻塞当前线程
* 4.不为0,semophore-1
* 5.开中断
*/
void sema_down(struct semaphore* psema) {
    emum intr_status old_status = intr_disable();
    while(psema->value == 0){
        ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
        if(elem_find(&psema->waiters, &running_thread()->general_tag)){
            PANIC("sema_down:P op failed");            
        }
        list_append(&psema->waiters, &running_thread()->general_tag);
        thread_blocked(TASK_BLOCKED);
    }
    psema->value--;
    ASSERT(psema->value == 0);
    intr_set_status(old_status);    
}
/* V操作,存在线程唤醒
* 1.关中断
* 2.判断当前信号量等待队列不为空,根据elem2entry找到队头线程的PCB地址,并唤醒
* 3.为空,semophore++
* 4.开中断
*/
void sema_up(struct semaphore* psema) {
    emum intr_status old_status = intr_disable();
    ASSERT(psema->value == 0);
    if(!list_empty(&psema->waiters)){
        struct task_struct* wait_thread = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
        thread_unblocked(wait_thread);
    }
    psema->value++;
    ASSERT(psema->value == 1);
    intr_set_status(old_status);    
}
(3)实现获得锁、释放锁操作
c 复制代码
/* 获得锁
* 1.先判断自己是不是锁的持有者
* 2.不是,P操作,持有锁,持有次数+1
* 3.反之,持有次数+1
*/
void lock_acquire (struct lock* plock) {
   /*	排除曾经自己已经持有锁但还未将其释放的情况	*/
    if(plock->holder != running_thread()){
        sema_down(&plock->semaphore);
        plock->holder = running_thread();
        ASSERT(plock->holder_repeat_nr == 0);
        plock->holder_repeat_nr = 1;
    }else{
        plock->holder_repeat_nr++;
    }
}
/* 释放锁
* 1.ASSERT()判断是否拥有锁
* 2.判断持有锁的次数,防止进程需要多次访问资源。
* 3.清空锁
* 3.V操作
*/
void lock_release (struct lock* plock) {
    ASSERT(plock->holder == running_thread());

    if(plock->holder_repeat_nr > 1){
        plock->holder_repeat_nr--;
        return;
    }
    ASSERT(plock->holder_repeat_nr == 1);
    plock->holder = NULL;
    plock->holder_repeat_nr = 0;
    sema_up(&plock->semaphore);
}

Ⅱ.从键盘获取输入

键的扫描码 :一个键的状态要么是按下,要么是弹起,因此一个键便有两个编码,按键被按下时的编码叫通码,也就是表示按键上的触点接通了内部电路,使硬件产生了一个码,故通码也称为 makecode。按键在被按住不松手时会持续产生相同的码,直到按键被松开时才终止,因此按键被松开弹起时产生的编码叫断码,也就是电路被断开了,不再持续产生码了,故断码也称为 breakcode。一个键的扫描码是由通码和断码组成的。

键盘编码器 :键盘是个独立的设备,在它内部有个叫作键盘编码器 的芯片,通常是 Intel 8048 或兼容芯片,它的作用是:每当键盘上发生按键操作,它就向键盘控制器报告哪个键被按下,以及键的扫描码。

键盘控制器 :在主机内部的主板上,通常是 Intel 8042 或兼容芯片,接收来自键盘编码器的按键信息,将其解码后保存,然后向中断代理发中断,之后处理器执行相应的中断处理程序读入 8042 处理保存过的按键信息。

1.从键盘读取输入的过程

• 扫描码有 3 套,现在一般键盘中的 8048 芯片支持的是第二套扫描码 。 因此每当有击键发生时, 8048发给 8042 的都是第二套键盘扫描码。

• 8042 为了兼容性,将接收到的第二套键盘扫描码转换成第一套扫描码。 8042 是按字节来处理的,每处理一个字节的扫描码后,将其存储到自己的输出缓冲区寄存器 。

• 然后向中断代理 8259A 发中断信号,这样我们的键盘中断处理程序通过读取 8042 的输出缓冲区寄存器,会获得第一套键盘扫描码。

2.键盘扫描码

根据键盘的更新迭代,出现了3种常用的键盘扫描码,为了兼容第一套键盘扫描码对应的中断处理程序,不管键盘用的是何种键盘扫描码,当键盘将扫描码发送到 8042 后,都由 8042 转换成第一套扫描码。

8042介于8048与处理器中间,担任中间人的职责,完成处理器对8048的设置,以及8048的扫描码输入给处理器。因此8042作为输入输出缓存区,包括输入、输出缓冲寄存器,以及状态、控制寄存器。

Ⅲ.键盘驱动程序

对于键盘驱动程序而言,每次都需要读取输出寄存器,取出键盘的扫描码并完成转为ASCII码的工作。

键盘驱动程序,初始化(注册键盘中断处理程序)

c 复制代码
#define KEY_BUF_PORT 0x60  // 键盘 buffer 寄存器端口号为 0x60
/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
/* 必须要读取输出缓冲区寄存器,否则8042不再继续响应键盘中断 */
   uint8_t scancode = inb(KBD_BUF_PORT);
   put_int(scancode);
   return;
}

/* 键盘初始化,注册键盘中断处理程序 */
void keyboard_init() {
   put_str("keyboard init start\n");
   register_handler(0x21, intr_keyboard_handler);
   put_str("keyboard init done\n");
}
1.C语言中有3中转义字符

(1) 一般转义字符,'\+单个字母' 的形式。

(2)八进制转义字符,'\0+三位八进制数字表示的 ASCII 码' 的形式 。

(3)十六进制转义字符,'\x+两位十六进制数字表示的 ASCII 码' 的形式 。

2.处理扫描码

键盘驱动程序需要完成 键盘扫描码->按键的ASCII码 的映射过程

(1)设计思路:
  1. 如果是一些用于操作方面的控制键,简称操作控制键,如<shift>、<ctrl>、<caps lock>,它通常是组合键,需要与其他键一起考虑,然后做出具体的行为展现,在键盘驱动中完成处理。
  2. 如果是一些用于字符方面的键,无论是可见字符,或是字符方面的控制键(简称字符控制键),如<backspace>,统统交给字符处理程序完成。

对于第一阶段,它与字符无直接的关系,因此咱们就在键盘驱动中处理。

对于第二阶段,咱们得知道用户按下的是什么宇符,不能把操作控制键当成字符传给字符处理程序,比如把 shift键的扫描码传给 put_char,这不就乱了吗?因此,咱们得把按键的扫描码转换成对应的字符,也就是将通码转换为字符的 ASCII 码,这就是前面所说的源到目标的映射关系。

**简言之:需要区分什么按键是需要显示的,什么按键是负责控制的。**这就需要建立扫描码和ASCII码的映射表,对键盘输入加以判定。

(2)建立键盘字符与通码映射关系

keymap[][0]表示未与shift组合的按键值,keymap[][1]表示与shift组合的按键值

c 复制代码
/* 以通码make_code为索引的二维数组 */
static char keymap[][2] = {
/* 扫描码   未与shift组合  与shift组合*/
/* ---------------------------------- */
/* 0x00 */	{0,	0},		
/* 0x01 */	{esc,	esc},		
/* 0x02 */	{'1',	'!'},		
/* 0x03 */	{'2',	'@'},		
/* 0x04 */	{'3',	'#'},		
/* 0x05 */	{'4',	'$'},		
/* 0x06 */	{'5',	'%'},		
/* 0x07 */	{'6',	'^'},		
/* 0x08 */	{'7',	'&'},		
/* 0x09 */	{'8',	'*'},		
/* 0x0A */	{'9',	'('},		
/* 0x0B */	{'0',	')'},		
/* 0x0C */	{'-',	'_'},		
/* 0x0D */	{'=',	'+'},		
/* 0x0E */	{backspace, backspace},	
/* 0x0F */	{tab,	tab},		
/* 0x10 */	{'q',	'Q'},		
/* 0x11 */	{'w',	'W'},		
/* 0x12 */	{'e',	'E'},		
/* 0x13 */	{'r',	'R'},		
/* 0x14 */	{'t',	'T'},		
/* 0x15 */	{'y',	'Y'},		
/* 0x16 */	{'u',	'U'},		
/* 0x17 */	{'i',	'I'},		
/* 0x18 */	{'o',	'O'},		
/* 0x19 */	{'p',	'P'},		
/* 0x1A */	{'[',	'{'},		
/* 0x1B */	{']',	'}'},		
/* 0x1C */	{enter,  enter},
/* 0x1D */	{ctrl_l_char, ctrl_l_char},
/* 0x1E */	{'a',	'A'},		
/* 0x1F */	{'s',	'S'},		
/* 0x20 */	{'d',	'D'},		
/* 0x21 */	{'f',	'F'},		
/* 0x22 */	{'g',	'G'},		
/* 0x23 */	{'h',	'H'},		
/* 0x24 */	{'j',	'J'},		
/* 0x25 */	{'k',	'K'},		
/* 0x26 */	{'l',	'L'},		
/* 0x27 */	{';',	':'},		
/* 0x28 */	{'\'',	'"'},		
/* 0x29 */	{'`',	'~'},		
/* 0x2A */	{shift_l_char, shift_l_char},	
/* 0x2B */	{'\\',	'|'},		
/* 0x2C */	{'z',	'Z'},		
/* 0x2D */	{'x',	'X'},		
/* 0x2E */	{'c',	'C'},		
/* 0x2F */	{'v',	'V'},		
/* 0x30 */	{'b',	'B'},		
/* 0x31 */	{'n',	'N'},		
/* 0x32 */	{'m',	'M'},		
/* 0x33 */	{',',	'<'},		
/* 0x34 */	{'.',	'>'},		
/* 0x35 */	{'/',	'?'},
/* 0x36	*/	{shift_r_char, shift_r_char},	
/* 0x37 */	{'*',	'*'},    	
/* 0x38 */	{alt_l_char, alt_l_char},
/* 0x39 */	{' ',	' '},		
/* 0x3A */	{caps_lock_char, caps_lock_char}
/*其它按键暂不处理*/
};
(3)更新键盘驱动程序
(3.1)读取扫描码
c 复制代码
static void intr_keyboard_handler(void) {
    ......
   bool break_code;
   uint16_t scancode = inb(KBD_BUF_PORT);

/* 若扫描码是e0开头的,表示此键的按下将产生多字节的扫描码,如shift、alt、ctrl、caps_lock等扫描码为2字节
 * 所以马上结束此次中断处理函数,等待下一个扫描码进来*/ 
   if (scancode == 0xe0) { 
      ext_scancode = true;    // 打开e0标记
      return;
   }

/* 如果上次是以0xe0开头,将扫描码合并 */
   if (ext_scancode) {
      scancode = ((0xe000) | scancode);
      ext_scancode = false;   // 关闭e0标记
   }   

   break_code = ((scancode & 0x0080) != 0);   // 获取break_code
    ......
}
(3.2)需要判断当前键盘是处于断码/通码

通码的扫描码scancode第8位为0,断码为1。

若为断码,需要将操作方面的控制键状态改为false,如shift、alt。将他们的断码的第8位改为0,作为通码访问keyb_map获取具体是哪个控制键,然后更改状态即可。

c 复制代码
/*---------------续上----------------*/

   if (break_code) {   // 若是断码break_code(按键弹起时产生的扫描码)
   /* 由于ctrl_r 和alt_r的make_code和break_code都是两字节,
   所以可用下面的方法取make_code,多字节的扫描码暂不处理 */
      uint16_t make_code = (scancode &= 0xff7f);   // 得到其make_code(按键按下时产生的扫描码)
   /* 若是任意以下三个键弹起了,将状态置为false */
      if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
	 ctrl_status = false;
      } else if (make_code == shift_l_make || make_code == shift_r_make) {
	 shift_status = false;
      } else if (make_code == alt_l_make || make_code == alt_r_make) {
	 alt_status = false;
      } /* 由于caps_lock不是弹起后关闭,所以需要单独处理 */
      return;   // 直接返回结束此次中断处理程序
   } 

若为通码,分为操作键和字符键两类,先要读取操作键,如shift、ctrl、alt、caps_lock,然后修改操作键的状态,根据操作键的状态确定映射的字符。

c 复制代码
/*---------------续上----------------*/

   /* 若为通码,只处理数组中定义的键以及alt_right和ctrl键,全是make_code */
   else if ((scancode > 0x00 && scancode < 0x3b) ||
            (scancode == alt_r_make) ||
            (scancode == ctrl_r_make))
   {
      bool shift = false; // 判断是否与shift组合,用来在一维数组中索引对应的字符
      if ((scancode < 0x0e) || (scancode == 0x29) ||
          (scancode == 0x1a) || (scancode == 0x1b) ||
          (scancode == 0x2b) || (scancode == 0x27) ||
          (scancode == 0x28) || (scancode == 0x33) ||
          (scancode == 0x34) || (scancode == 0x35))
      {
         /****** 代表两个字母的键 ********
             0x0e 数字'0'~'9',字符'-',字符'='
             0x29 字符'`'
             0x1a 字符'['
             0x1b 字符']'
             0x2b 字符'\\'
             0x27 字符';'
             0x28 字符'\''
             0x33 字符','
             0x34 字符'.'
             0x35 字符'/'
         *******************************/
         if (shift_down_last)
         { // 如果同时按下了shift键
            shift = true;
         }
      }
      else
      { // 默认为字母键
         if (shift_down_last && caps_lock_last)
         { // 如果shift和capslock同时按下
            shift = false;
         }
         else if (shift_down_last || caps_lock_last)
         { // 如果shift和capslock任意被按下
            shift = true;
         }
         else
         {
            shift = false;
         }
      }

      uint8_t index = (scancode &= 0x00ff); // 将扫描码的高字节置0,主要是针对高字节是e0的扫描码.
      char cur_char = keymap[index][shift]; // 在数组中找到对应的字符

      /* 只处理ascii码不为0的键 */
      if (cur_char)
      {
         put_char(cur_char);
         return;
      }

      /* 记录本次是否按下了下面几类控制键之一,供下次键入时判断组合键 */
      if (scancode == ctrl_l_make || scancode == ctrl_r_make)
      {
         ctrl_status = true;
      }
      else if (scancode == shift_l_make || scancode == shift_r_make)
      {
         shift_status = true;
      }
      else if (scancode == alt_l_make || scancode == alt_r_make)
      {
         alt_status = true;
      }
      else if (scancode == caps_lock_make)
      {
         /* 不管之前是否有按下caps_lock键,当再次按下时则状态取反,
          * 即:已经开启时,再按下同样的键是关闭。关闭时按下表示开启。*/
         caps_lock_status = !caps_lock_status;
      }
   }
   else
   {
      put_str("unknown key\n");
   }

Ⅳ.环形输入缓冲区

构建缓冲区保存键盘扫描码转换的字符,每次从缓冲区取出字符,完成打印。

利用生产者消费者模式构建环形队列,实现字符的取出和保存。主要完成缓冲区存储状态、添加1字节、删除1字节等操作。

1.环形队列结构体
c 复制代码
/*  环形队列    */
struct ioqueue {
    struct lock lock;
    struct task_struct *producer;
    struct task_struct *consumer;
    // 缓冲区大小
    char buf[bufsize];
    // 队首,数据往队首处写入
    int32_t head;
    // 队尾,数据从队尾处读出
    int32_t tail;
};
2.缓冲区满
c 复制代码
/*  返回 pos 在缓冲区中的下一个位置值  */
static int32_t next_pos (int32_t pos) {
    return (pos+1)%bufsize;
}
/*  采用头插法加入元素,判断队列是否已满  */
bool ioq_full(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);
    return next_pos(ioq->head) == ioq->tail;
}
3.缓冲区为空
c 复制代码
/*  采用头插法加入元素,判断队列是否为空  */
bool ioq_empty(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);
    return ioq->head == ioq->tail;
}
4.缓冲区为空时,等待
c 复制代码
/*      当前生产者或消费者在此缓冲区上等待      */
static void ioq_wait(struct task_struct ** waiter) {
    ASSERT((waiter != NULL) && (&waiter == NULL));
    *waiter = running_thread();
    thread_blocked(TASK_BLOCKED);
}
5.缓冲区不为空,唤醒waiterr
c 复制代码
/*  唤醒 waiter    */
static void wakeup(struct task_struct** waiter) {
    ASSERT(&waiter != NULL);
    thread_unblocked(*waiterr);
    *waiterr = NULL;
}
6.缓冲区添加一字节
c 复制代码
/*  生产者往 ioq 队列中写入一个字符 byte    */
void ioq_putchar(struct ioqueue* ioq, char byte) {
    ASSERT(intr_get_intr() == INTR_OFF);
    while(ioq_full(ioq)){
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq);
        lock_release(&ioq->lock);
    }
    ioq->buf[ioq->head] = byte;
    ioq->head = next_pos(ioq->head);

    if(ioq->consumer != NULL){
        wakeup(&ioq->consumer);
    }
}
  1. 判断缓冲区是否满了
  2. 满了则请求锁,并wait,阻塞当前线程,释放锁
  3. 反之,头插法,在队列头插入新元素
  4. 唤醒消费者
7.缓冲区取出一字节
c 复制代码
/*  消费者从 ioq 队列中获取一个字符   */
char ioq_getchar(struct ioqueue* ioq) {
    ASSERT(intr_get_status() == INTR_OFF);
    while(ioq_empty(ioq)){
        lock_acquire(&ioq->lock);
        ioq_wait(&ioq);
        lock_release(&ioq->lock);
    }

    char byte = ioq->buf[ioq->tail];
    ioq->tail = next_pos(ioq->tail);
    if(ioq->producer != NULL){
        wakeup(&ioq->consumer);
    }
    return byte;
}
  1. 判断缓冲区是否为空
  2. 为空则请求锁,并wait,阻塞当前线程,释放锁
  3. 反之,从队尾取出新元素
  4. 唤醒生产者

over

相关推荐
别说我什么都不会1 小时前
当OpenHarmony遇上OpenEuler
操作系统·嵌入式·harmonyos
一念杂记4 小时前
U盘DIY,三步打造你的"行走操作系统" 即插即用秒变专属电脑
操作系统
刘大猫2617 小时前
二、搭建MyBatis采用xml方式,验证CRUD(增删改查操作)
操作系统·自动化运维·设计
别说我什么都不会17 小时前
使用Multipass编译OpenHarmony工程
操作系统·嵌入式·harmonyos
别说我什么都不会20 小时前
鸿蒙轻内核M核源码分析系列二一 05 文件系统FatFS
操作系统·嵌入式·harmonyos
蓝天下小溪旁戴着耳机去放羊21 小时前
详解数据传输——零拷贝、direct IO
性能优化·操作系统
银色火焰战车1 天前
基于编译器特性浅析C++程序性能优化
开发语言·c++·重构·系统架构·操作系统
别说我什么都不会2 天前
鸿蒙轻内核M核源码分析系列二一 03 文件系统LittleFS
操作系统·嵌入式·harmonyos
charlie1145141912 天前
从0开始的操作系统手搓教程24——完成我们的键盘驱动子系统
驱动开发·操作系统·键盘·手搓教程