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

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字符。按下和弹起都会打印。

相关推荐
Tingjct几秒前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
猷咪27 分钟前
C++基础
开发语言·c++
IT·小灰灰28 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧30 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q31 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳031 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾31 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB34 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683635 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计1 小时前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识