当前位置: 首页 > news >正文

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

1.锁的实现

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

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

//将当前正在运行的线程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文件

#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文件

#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文件

#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文件

#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文件中添加代码

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文件

#ifndef __DEVICE_KEYBOARD_H
#define __DEVICE_KEYBOARD_H
void keyboard_init(void); 
#endif

keyboard.c文件

#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文件

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

http://www.dtcms.com/a/469607.html

相关文章:

  • 免费发布信息的网站网站建设规划文档
  • kali安装ARL-docker灯塔
  • Linux的Dynamic debug功能
  • 需要做网站建设的公司做流程图用什么网站
  • 常用的日期时间处理库Day.js和Moment.js
  • Verilog和FPGA的自学笔记5——三八译码器(case语句与锁存器)
  • Mpi多机通信环境搭建(2台机器)
  • 简述网站制作流程图如何免费注册淘宝店铺
  • 人工智能在数学教育中的应用 | 现状、探索与实践
  • VSCode括号高亮插件(vscode插件)bracket pair、活动括号对、括号线(未完全检查)
  • FPGA强化-串口rs232
  • 为何建设银行网站无法登陆公司官网开发
  • 2020年多媒体应用设计师考试上午真题
  • 构建算法远程开发调试环境
  • 南通网站建设系统方案龙岩网站设计 信任推商吧做词
  • Vivado进阶-Fpga中的mem的综合和应用
  • Jmeter设置负载阶梯式压测场景(详解教程)
  • 网站运营专员做六休一wordpress托管网站
  • WPF用户控件和依赖属性
  • 位运算 和 逻辑运算 以及 位运算指令
  • 工地招聘网站广告设计与制作视频
  • C++右值语义解析
  • java-高级技术(单元测试、反射)
  • 厦门做网站公司有哪些邯郸
  • Spring Boot 项目集成 Gradle:构建、测试、打包全流程教程
  • 电商主要是做什么工作东莞seo收费
  • SAP MM 通用物料移动过账冲销接口分享
  • 设计logo免费网站电商网站对比表格
  • SAP Vendor Invoice Management by OpenText (VIM)
  • 用 PyQt5 + PyPDF2 做一个“智能分页”的大 PDF 拆分器(含 GUI 与命令行双版本,附完整源码)