《操作系统真象还原》 第十章 输入输出系统

1.锁的实现

在上章实现的多线程打印字符的程序中,当一个线程在打印字符设置光标值的时候,可能会由于各种原因导致该线程下处理器,另外一个线程上处理器重新修改光标值,当该线程再次执行时,就会发生异常。因此,我们需要通过锁机制来实现线程的同步,也就是当多个线程会对同一段区域的内容修改时,只能由拥有锁的那一个线程进行操作。在上章的案例中,我们需要将打印字符put_str()函数修改成原子操作,要么一次性执行完,要么不执行。

在thread.c文件中添加以下两个函数

cpp 复制代码
//将当前正在运行的线程pcb中的状态字段设定为传入的status,一般用于线程主动设定阻塞
void thread_block(enum task_status stat) {
/* stat取值为TASK_BLOCKED,TASK_WAITING,TASK_HANGING,也就是只有这三种状态才不会被调度*/
   ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
   enum intr_status old_status = intr_disable();       //先关闭中断,因为涉及要修改阻塞队列,调度
   struct task_struct* cur_thread = running_thread();    //得到当前正在运行的进程的pcb地址
   cur_thread->status = stat; // 置其状态为stat 
   schedule();		      // 将当前线程换下处理器
/* 待当前线程被解除阻塞后才继续运行下面的intr_set_status */
   intr_set_status(old_status);
}
/* 将线程pthread解除阻塞 */
void thread_unblock(struct task_struct* pthread) {
   enum intr_status old_status = intr_disable();      //涉及队就绪队列的修改,此时绝对不能被切换走
   ASSERT(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
   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\n");
      }
      list_push(&thread_ready_list, &pthread->general_tag);    // 放到队列的最前面,使其尽快得到调度
      pthread->status = TASK_READY;
   } 
   intr_set_status(old_status);
}

thread_block()函数用于阻塞线程,当该线程未能申请到锁时就会调用该函数,将该线程的状态修改为阻塞状态,然后换下处理器,等待唤醒。thread_unblock用于唤醒线程,将传进的线程添加到就绪队列的队头,等待执行。

在thread目录下创建sync.c和sync.h文件

sync.h文件

bash 复制代码
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"

/* 信号量结构 */
struct semaphore {
   uint8_t  value;              //一个信号量肯定有值来表示这个量
   struct   list waiters;       //用一个双链表结点来管理所有阻塞在该信号量上的线程
};

/* 锁结构 */
struct lock {
   struct   task_struct* holder;	    //用于记录谁把二元信号量申请走了,而导致了该信号量的锁
   struct   semaphore semaphore;	    //一个锁肯定是来管理信号量的
   uint32_t holder_repeat_nr;		    //有时候线程拿到了信号量,但是线程内部不止一次使用该信号量对应公共资源,就会不止一次申请锁
                                        //内外层函数在释放锁时就会对一个锁释放多次,所以必须要记录重复申请的次数
};

void sema_init(struct semaphore* psema, uint8_t value); 
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);


#endif

在该文件中,定义了struct semaphore和struct lock两个结构体,第一个结构体表示信号量,信号量表示资源的个数,此处用于实现锁,只能取0或1两个值,被称为二元信号量,该结构体中维护了一个双向链表,记录阻塞在该信号量上的线程,当拥有该信号量的线程执行结束后在该队列中进行唤醒,第二个结构体表示锁,记录了拥有该锁的线程、该锁的信号量以及锁被同一个线程所拥有的次数。

sync.c文件

bash 复制代码
#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"

//用于初始化信号量,传入参数就是指向信号量的指针与初值
void sema_init(struct semaphore* psema, uint8_t value) {
   psema->value = value;       // 为信号量赋初值
   list_init(&psema->waiters); //初始化信号量的等待队列
}

//用于初始化锁,传入参数是指向该锁的指针
void lock_init(struct lock* plock) {
   plock->holder = NULL;
   plock->holder_repeat_nr = 0;
   sema_init(&plock->semaphore, 1); //将信号量初始化为1,因为此函数一般处理二元信号量
}

//信号量的down操作,也就是减1操作,传入参数是指向要操作的信号量指针。线程想要申请信号量的时候用此函数
void sema_down(struct semaphore* psema) {
   enum intr_status old_status = intr_disable();         //对于信号量的操作是必须关中断的

   //一个自旋锁,来不断判断是否信号量已经被分配出去了。为什么不用if,见书p450。
    while(psema->value == 0) {	// 若value为0,表示已经被别人持有
        ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
        /* 当前线程不应该已在信号量的waiters队列中 */
        if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
	        PANIC("sema_down: thread blocked has been in waiters_list\n");
        }
        //如果此时信号量为0,那么就将该线程加入阻塞队列,为什么不用判断是否在阻塞队列中呢?因为线程被阻塞后,会加入阻塞队列,除非被唤醒,否则不会
        //分配到处理器资源,自然也不会重复判断是否有信号量,也不会重复加入阻塞队列
        list_append(&psema->waiters, &running_thread()->general_tag); 
        thread_block(TASK_BLOCKED);    // 阻塞线程,直到被唤醒
    }
/* 若value为1或被唤醒后,会执行下面的代码,也就是获得了锁。*/
    psema->value--;
    ASSERT(psema->value == 0);	    
/* 恢复之前的中断状态 */
    intr_set_status(old_status);
}

//信号量的up操作,也就是+1操作,传入参数是指向要操作的信号量的指针。且释放信号量时,应唤醒阻塞在该信号量阻塞队列上的一个进程
void sema_up(struct semaphore* psema) {
/* 关中断,保证原子操作 */
   enum intr_status old_status = intr_disable();
   ASSERT(psema->value == 0);	    
   if (!list_empty(&psema->waiters)) {   //判断信号量阻塞队列应为非空,这样才能执行唤醒操作
      struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
      thread_unblock(thread_blocked);
   }
   psema->value++;
   ASSERT(psema->value == 1);	    
/* 恢复之前的中断状态 */
   intr_set_status(old_status);
}

//获取锁的函数,传入参数是指向锁的指针
void lock_acquire(struct lock* plock) {
//这是为了排除掉线程自己已经拿到了锁,但是还没有释放就重新申请的情况
   if (plock->holder != running_thread()) { 
        sema_down(&plock->semaphore);    //对信号量进行down操作
        plock->holder = running_thread();
        ASSERT(plock->holder_repeat_nr == 0);
        plock->holder_repeat_nr = 1;    //申请了一次锁
   } else {
        plock->holder_repeat_nr++;
   }
}

//释放锁的函数,参数是指向锁的指针
void lock_release(struct lock* plock) {
   ASSERT(plock->holder == running_thread());
   //如果>1,说明自己多次申请了该锁,现在还不能立即释放锁
   if (plock->holder_repeat_nr > 1) {   
      plock->holder_repeat_nr--;
      return;
   }
   ASSERT(plock->holder_repeat_nr == 1);    //判断现在lock的重复持有数是不是1只有为1,才能释放

   plock->holder = NULL;	   //这句必须放在up操作前,因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,
                                //就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了
   plock->holder_repeat_nr = 0;
   sema_up(&plock->semaphore);	   // 信号量的V操作,也是原子操作
}

此文件中主要定义了关于信号量和锁的函数,sema_init和lock_init用于初始化,sema_down()函数用于申请信号量,即信号量减1,对信号量的操作必须是原子的,因此在执行之前和之后分别调用了关开中断的函数,通过自旋锁的形式与竞争信号量,因为该线程第一次没能竞争到信号量时,进入阻塞状态,当被唤醒时,信号量可能会被其它线程抢走,因此要通过while循环不停地去竞争信号量,当抢到信号量后,将信号量减1,sema_up()函数为释放信号量,从当前信号量的阻塞队列中选择一个线程,调用thread_unblock()去唤醒。lock_acquire()函数就是获取锁的函数,会先判断当前线程和该锁的拥有者是否是同一个线程,若是,只需在锁的拥有次数+1,否则调用sema_down()去申请信号量,将锁的拥有者设为该线程。lock_release()函数是释放锁,先判断锁的拥有次数是否为1,若不为1,减1后直接返回,还不能释放,若为1,先将锁的拥有者设为NULL,然后调用sema_up()函数释放锁的信号量。

在device目录下创建console.c文件和console.h

console.h文件

cpp 复制代码
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
void console_init(void);
void console_acquire(void);
void console_release(void);
void console_put_str(char* str);
void console_put_char(uint8_t char_asci);
void console_put_int(uint32_t num);
#endif

console.c文件

cpp 复制代码
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "../thread/sync.h"
#include "../thread/thread.h"
static struct lock console_lock;    // 控制台锁

/* 初始化终端 */
void console_init() {
  lock_init(&console_lock); 
}

/* 获取终端 */
void console_acquire() {
   lock_acquire(&console_lock);
}

/* 释放终端 */
void console_release() {
   lock_release(&console_lock);
}

/* 终端中输出字符串 */
void console_put_str(char* str) {
   console_acquire(); 
   put_str(str); 
   console_release();
}

/* 终端中输出字符 */
void console_put_char(uint8_t char_asci) {
   console_acquire(); 
   put_char(char_asci); 
   console_release();
}

/* 终端中输出16进制整数 */
void console_put_int(uint32_t num) {
   console_acquire(); 
   put_int(num); 
   console_release();
}

该函数主要功能就是定义一个锁,对之前的各种打印函数进行修改,使其具有原子性,也就是在调用各种打印函数之前必须先获取锁,执行结束后释放锁。

在init.c文件中的init_all函数中添加console_init()并且将main.c中的打印函数替换为console_put_str()函数,再次编译,启动虚拟机,会发现打印频率变快,之前我们在打印函数前后调用了关开中断函数,导致打印频率较低。

2.获取键盘的输入输出

键盘是一个独立的设备,在键盘里存在着键盘编码器Intel8048芯片,用于监控键盘的操作,当键盘的某个按键被按下时,就会向键盘控制器发送信号,键盘控制器位于主机的主板上,通常为Intel8042芯片,它的作用是接收键盘编码器发来的信号,解码后发送给之前提到的中断代理8059A,因此,键盘的输入实际上就是发生了一次中断,我们需要去编写对应的中断处理函数,就可以获取到键盘的输入。

在kernel.s文件中添加代码

bash 复制代码
VECTOR 0x20,ZERO	;时钟中断对应的入口
VECTOR 0x21,ZERO	;键盘中断对应的入口
VECTOR 0x22,ZERO	;级联用的
VECTOR 0x23,ZERO	;串口2对应的入口
VECTOR 0x24,ZERO	;串口1对应的入口
VECTOR 0x25,ZERO	;并口2对应的入口
VECTOR 0x26,ZERO	;软盘对应的入口
VECTOR 0x27,ZERO	;并口1对应的入口
VECTOR 0x28,ZERO	;实时时钟对应的入口
VECTOR 0x29,ZERO	;重定向
VECTOR 0x2a,ZERO	;保留
VECTOR 0x2b,ZERO	;保留
VECTOR 0x2c,ZERO	;ps/2鼠标
VECTOR 0x2d,ZERO	;fpu浮点单元异常
VECTOR 0x2e,ZERO	;硬盘
VECTOR 0x2f,ZERO	;保留

在device目录中创建keyboard.c文件和keyboard.h文件

keyboard.h文件

cpp 复制代码
#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void); 
#endif

keyboard.c文件

cpp 复制代码
#include "keyboard.h"
#include "print.h"
#include "interrupt.h"
#include "io.h"
#include "global.h"

#define KBD_BUF_PORT 0x60	   // 键盘buffer寄存器端口号为0x60

/* 键盘中断处理程序 */
static void intr_keyboard_handler(void) {
   put_char('k');
//每次必须要从8042读走键盘8048传递过来的数据,否则8042不会接收后续8048传递过来的数据
   inb(KBD_BUF_PORT);
   return;
}

/* 键盘初始化 */
void keyboard_init() {
   put_str("keyboard init start\n");
   register_handler(0x21, intr_keyboard_handler);       //注册键盘中断处理函数
   put_str("keyboard init done\n");
}

此文件就是定义了一个简单的键盘中断处理程序,即不管输入什么,都打印k字符,然后通过inb()从端口号读取数据,方便接收下一个数据,最后将该中断处理函数注册到中断处理函数数组中。

在init.c文件中的init_all()函数中添加keyboard_init()函数。

修改main.c函数为死循环,拥有接收键盘的操作。

main.c文件

cpp 复制代码
#include "print.h"
#include "init.h"
#include "../thread/thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
   put_str("I am kernel\n");
   init_all();

   intr_enable();
   while(1);
   return 0;
}

重新编译,启动虚拟机,按下键盘中的任意按键都会打印k字符。按下和弹起都会打印。

相关推荐
麦麦鸡腿堡3 小时前
Java的封装
java·开发语言
沢田纲吉4 小时前
《LLVM IR 学习手记(五):关系运算与循环语句的实现与解析》
前端·c++·llvm
沢田纲吉4 小时前
《LLVM IR 学习手记(六):break 语句与 continue 语句的实现与解析》
前端·c++·llvm
爱和冰阔落4 小时前
【C++进阶】继承上 概念及其定义 赋值兼容转换 子类默认成员函数的详解分析
c++
APItesterCris4 小时前
Node.js/Python 实战:编写一个淘宝商品数据采集器
大数据·开发语言·数据库·node.js
余辉zmh5 小时前
【C++篇】:LogStorm——基于多设计模式下的同步&异步高性能日志库项目
开发语言·c++·设计模式
艾莉丝努力练剑5 小时前
【C++STL :list类 (二) 】list vs vector:终极对决与迭代器深度解析 && 揭秘list迭代器的陷阱与精髓
linux·开发语言·数据结构·c++·list
寒冬没有雪5 小时前
矩阵的翻转与旋转
c++·算法·矩阵
努力也学不会java5 小时前
【Java并发】深入理解synchronized
java·开发语言·人工智能·juc