硬件中断

  • 中断控制器
  • 启动中断
  • 处理定时器中断
  • 死锁
  • 竞争条件
  • HLT指令
  • 键盘输入

中断控制器

中断提供了一种从附加硬件设备通知CPU的方法。这个英特尔8259是1976年推出的可编程中断控制器(PIC)。8259具有8条中断线和几条用于与CPU通信的线路。一个主PIC和一个从PIC连接到主系统的一条中断线上,一共15个端口。

pics的默认配置不可用,因为它向CPU发送0-15范围内的中断向量号。这些数字已经被CPU异常占用。为了解决这个重叠问题,我们需要将PIC中断重新映射到不同的数字。只要不与异常重叠,实际范围就无关紧要,但通常选择的范围是32-47,因为这是32个例外插槽之后的第一个空闲数字。

配置是通过向pics的命令和数据端口写入特殊值来实现的。幸运的是,已经有一个箱子叫做pic8259_simple,所以我们不需要自己编写初始化序列。
我们将以下内容添加到我们的项目中:

# in Cargo.toml[dependencies]
pic8259_simple = "0.2.0"

然后设置主/副PIC布局的链式PICS结构,这里需要将interrupts文件加入如下代码:

// in src/interrupts.rsuse pic8259_simple::ChainedPics;
use spin;pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;pub static PICS: spin::Mutex<ChainedPics> =spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });

我们使用initialize函数来执行PIC初始化。

// in src/lib.rspub fn init() {gdt::init();interrupts::init_idt();unsafe { interrupts::PICS.lock().initialize() }; // new
}

cargo xrun 一切正常

启动中断

因为在CPU配置中禁用了中断,所以我们来开启中断

// in src/lib.rspub fn init() {gdt::init();interrupts::init_idt();unsafe { interrupts::PICS.lock().initialize() };x86_64::instructions::interrupts::enable();     // new
}

这个interrupts::enable的功能x86_64机箱执行特殊sti指令(“设置中断”)以启用外部中断。
当我们尝试cargo xrun现在,我们看到出现了双重故障:

造成这种双重故障的原因是硬件定时器在默认情况下是启用的,所以一旦启用中断,我们就开始接收定时器中断。因为我们还没有为它定义一个处理程序函数,所以会调用我们的双故障处理程序。

处理定时器中断

计时器使用主PIC的第0行,这意味着它作为中断32到达CPU(0+偏移量32)。而不是硬编码索引32,我们将其存储在InterruptIndex枚举:

现在,我们可以为计时器中断添加一个处理程序函数:

// in src/interrupts.rsuse crate::print;lazy_static! {static ref IDT: InterruptDescriptorTable = {let mut idt = InterruptDescriptorTable::new();idt.breakpoint.set_handler_fn(breakpoint_handler);unsafe {idt.double_fault.set_handler_fn(double_fault_handler).set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new}idt[InterruptIndex::Timer.as_usize()].set_handler_fn(timer_interrupt_handler); // newidt};
}extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{print!(".");
}

注意要将main.rs文件中的stack_overflow注释掉,如图所示

在定时器中断处理程序中,我们在屏幕上打印一个点。由于计时器中断是定期发生的,我们希望看到每个计时器滴答上出现一个点。然而,当我们运行它时,我们看到只有一个点被打印出来:


中断结束
上图所示只打印了一个点的原因是没有设置中断信号,PIC认为我们还在处理第一个定时器中断,一直等待中断结束的信号

为了发送EOI(中断结束信号),我们使用我们的静电PICS再次构造:

// in src/interrupts.rsextern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{print!(".");unsafe {PICS.lock().notify_end_of_interrupt(InterruptIndex::Timer.as_u8());}
}

这个notify_end_of_interrupt计算出主要的或次要的PIC是否发送了中断,然后使用command和data将EOI信号发送给相应控制器的端口。

当我们现在执行cargo xrun我们看到屏幕上周期性地出现点:

死锁

现在内核中有一种并发形式:计时器中断是异步发生的,因此它们可以中断我们的_start随时起作用。幸运的是,Rust的所有权系统防止了编译时与并发相关的许多类型的错误。
一个值得注意的例外是死锁。如果线程试图获取永远不会释放的锁,则会发生死锁。因此,线程无限期地挂起。

例如

// in src/vga_buffer.rs[…]#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {use core::fmt::Write;WRITER.lock().write_fmt(args).unwrap();
}

这个代码不用添加,已经存在
它锁定WRITER,电话write_fmt并在函数结束时隐式地解锁。现在,假设一个中断发生在WRITER被锁定,中断处理程序也尝试打印一些内容。
这个WRITER被锁定,因此中断处理程序等待直到空闲。但这从未发生因为_start函数只在中断处理程序返回后继续运行(打印内容)。因此,整个系统挂起。
触发死锁
我们可以很容易地在内核中触发这样的死锁,方法是在循环的末尾打印一些东西。

// in src/main.rs#[no_mangle]
pub extern "C" fn _start() -> ! {println!("Hello World{}", "!");lyh_os::init();println!("It did not crash!");loop {use lyh_os::print;print!("-");        // new}
}

当我们在QEMU中运行它时

我们看到只有有限数量的连字符被打印出来,直到第一个定时器中断发生。然后系统挂起,因为计时器中断处理程序在试图打印点时会死锁。这就是我们在上面的输出中没有看到点的原因。
避免死锁
为了避免这种死锁,我们可以禁用中断,只要Mutex被锁定:

// in src/vga_buffer.rs/// Prints the given formatted string to the VGA text buffer
/// through the global `WRITER` instance.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {use core::fmt::Write;use x86_64::instructions::interrupts;   // newinterrupts::without_interrupts(|| {     // newWRITER.lock().write_fmt(args).unwrap();});
}

这个without_interrupts函数采用封闭并在没有中断的环境中执行。我们使用它来确保只要Mutex被锁上了。当我们现在运行我们的内核时,我们看到它在不挂起的情况下继续运行。总的来说就是禁用中断。

我们可以将相同的更改应用于我们的串行打印功能,以确保它不会出现死锁:

// in src/serial.rs#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {use core::fmt::Write;use x86_64::instructions::interrupts;       // newinterrupts::without_interrupts(|| {         // newSERIAL1.lock().write_fmt(args).expect("Printing to serial failed");});
}

禁用中断不应该是一个通用的解决方案,问题是它增加了最坏的中断延迟

竞争条件

如果你跑cargo xtest你可能会看到test_println_output测试失败:

原因是测试和计时器处理程序之间的竞争条件。
测试看起来是这样的:

// in src/vga_buffer.rs#[test_case]
fn test_println_output() {let s = "Some test string that fits on a single line";println!("{}", s);for (i, c) in s.chars().enumerate() {let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();assert_eq!(char::from(screen_char.ascii_character), c);}
}

发生竞争条件是因为计时器中断处理程序可能在println以及屏幕字符的读取之间运行。

要解决这个问题,我们执行了以下更改。
1、将lock()写入完整的测试中,用允许打印到已锁定的写入器的writeln代替println
2、避免死锁,测试期间禁用中断
3、定时器中断可能在测试之前运行,所以在打印字符串s前打印一额外行

// in src/vga_buffer.rs#[test_case]
fn test_println_output() {use core::fmt::Write;use x86_64::instructions::interrupts;let s = "Some test string that fits on a single line";interrupts::without_interrupts(|| {let mut writer = WRITER.lock();writeln!(writer, "\n{}", s).expect("writeln failed");for (i, c) in s.chars().enumerate() {let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();assert_eq!(char::from(screen_char.ascii_character), c);}});
}

HLT指令

因为我们使用了简单的空循环语句,使得CPU不停的工作,非常的低效率
所以我们让CPU进入休息的状态,知道下一个中断到达

// in src/lib.rspub fn hlt_loop() -> ! {loop {x86_64::instructions::hlt();}
}

我们现在可以用这个hlt_loop而不是无尽的循环_start和panic职能:

// in src/main.rs#[no_mangle]
pub extern "C" fn _start() -> ! {println!("Hello World{}", "!");lyh_os::init();println!("It did not crash!");lyh_os::hlt_loop();            // new
}#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);lyh_os::hlt_loop();            // new
}

让我们更新我们的lib.rs

// in src/lib.rs/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {init();test_main();hlt_loop();         // new
}pub fn test_panic_handler(info: &PanicInfo) -> ! {serial_println!("[failed]\n");serial_println!("Error: {}\n", info);exit_qemu(QemuExitCode::Failed);hlt_loop();         // new
}

当我们现在在QEMU中运行内核时,我们看到CPU的使用率要低得多。

键盘输入

让我们为键盘中断添加一个处理程序函数。它非常类似于我们如何定义定时器中断的处理程序,它只是使用了一个不同的中断号:

// in src/interrupts.rs#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {Timer = PIC_1_OFFSET,Keyboard, // new
}lazy_static! {static ref IDT: InterruptDescriptorTable = {let mut idt = InterruptDescriptorTable::new();idt.breakpoint.set_handler_fn(breakpoint_handler);unsafe {idt.double_fault.set_handler_fn(double_fault_handler).set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new}idt[InterruptIndex::Timer.as_usize()].set_handler_fn(timer_interrupt_handler);// newidt[InterruptIndex::Keyboard.as_usize()].set_handler_fn(keyboard_interrupt_handler);idt};
}extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{print!("k");unsafe {PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());}
}

在中断处理程序中,我们打印一个k并将中断信号的结束发送给中断控制器。
读取扫描代码
找出哪一个按下键后,需要查询键盘控制器。我们通过从ps/2控制器的数据端口读取数据来做到这一点,这是I/O端口带号0x60:

// in src/interrupts.rsextern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{use x86_64::instructions::port::Port;let mut port = Port::new(0x60);let scancode: u8 = unsafe { port.read() };print!("{}", scancode);unsafe {PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());}
}

我们使用Port类型x86_64从键盘的数据端口读取字节(扫描码)。
当我们按下键盘

解读扫描码
要将扫描代码转换为键,可以使用Match语句:

// in src/interrupts.rsextern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{use x86_64::instructions::port::Port;let mut port = Port::new(0x60);let scancode: u8 = unsafe { port.read() };// newlet key = match scancode {0x02 => Some('1'),0x03 => Some('2'),0x04 => Some('3'),0x05 => Some('4'),0x06 => Some('5'),0x07 => Some('6'),0x08 => Some('7'),0x09 => Some('8'),0x0a => Some('9'),0x0b => Some('0'),_ => None,};if let Some(key) = key {print!("{}", key);}unsafe {PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());}
}

上面的代码翻译数字键0-9的按键,忽略所有其他键。
当我们按下0-9键,就会显示相应的数字

翻译其他键的方式也是一样的。幸运的是有一个名为pc-keyboard库用于翻译扫描代码集1和2的扫描代码,因此我们不需要自己实现这一点。要使用这个库,我们将它添加到Cargo.toml并将其导入我们的lib.rs:

# in Cargo.toml[dependencies]
pc-keyboard = "0.5.0"

现在我们可以用这个库重写我们的keyboard_interrupt_handler:

// in/src/interrupts.rsextern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: &mut InterruptStackFrame)
{use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};use spin::Mutex;use x86_64::instructions::port::Port;lazy_static! {static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1,HandleControl::Ignore));}let mut keyboard = KEYBOARD.lock();let mut port = Port::new(0x60);let scancode: u8 = unsafe { port.read() };if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {if let Some(key) = keyboard.process_keyevent(key_event) {match key {DecodedKey::Unicode(character) => print!("{}", character),DecodedKey::RawKey(key) => print!("{:?}", key),}}}unsafe {PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());}
}

在每次中断时,我们锁定互斥锁,从键盘控制器读取扫描代码,并将其传递给add_byte方法,该方法将扫描代码转换为Option。这个KeyEvent包含导致事件的键以及它是新闻事件还是发布事件。

要解释这个关键事件,我们将它传递给process_keyevent方法,如果可能,将键事件转换为字符。例如,翻译A小写的键a字符或大写A字符,取决于是否按下Shift键。

使用这个修改后的中断处理程序,我们现在可以编写文本:

大写也是可以的。
操作系统概念1~5次实验报告:https://download.csdn.net/download/weixin_43979304/15321050?spm=1001.2014.3001.5503

操作系统原理实验(四)深渊:竞争条件与死锁(硬件中断)相关推荐

  1. rust键位失灵_用Rust写操作系统(四)——竞争条件与死锁

    一.概要说明 当多个任务访问同一个资源(数据)是就会引发竞争条件问题,这不仅在进程间会出现,在操作系统和进程间也会出现.由竞争条件引发的问题很难复现和调试,这也是其最困难的地方.本实验的目的在于了解竞 ...

  2. ZUCC_操作系统原理实验_Lab9进程的通信消息队列

    lab9进程的通信–消息队列 一.两个进程并发执行,通过消息队列,分别进行消息的发送和接收 1.代码: //接受消息 #include<stdio.h> #include<stdli ...

  3. ZUCC_操作系统原理实验_实验九 消息队列

    操作系统原理实验报告 课程名称 操作系统原理实验 实验项目名称 实验九 消息队列 实验目的 了解 Linux 系统的进程间通信机构 (IPC): 理解Linux 关于消息队列的概念: 掌握 Linux ...

  4. 操作系统原理实验-进程同步

    操作系统原理实验报告 实验题目 实验二进程同步 实验二.进程同步 1.1 实验目的 现代操作系统的核心是多道程序设计.多处理器和分布式处理器,这些方案和操作系统设计技术的基础都是并发.当多个进程并发执 ...

  5. 计算机操作系统原理第四章习题

    计算机操作系统原理第四章习题 1.什么是静态链接.装入时动态链接和运行时的动态链接? 2.简述分页系统和分段系统的异同点 3.什么情况下需要重定位?为什么要引入重定位? 4.在具有快表的段页式存储管理 ...

  6. 计算机网络云南大学实验四,云南大学软件学计算机网络原理实验四.doc

    云南大学软件学计算机网络原理实验四 实验四.web服务器套接字编程实验指导 1.实验目的: 编写一个WEB服务器程序,可以接受来自浏览器的访问,并传输页面(包含多个对象)到浏览器.掌握Socket编程 ...

  7. 操作系统原理 实验1、2

    操作系统原理 实验1.2 1.高响应比作业调度 代码示例 #include<malloc.h> #include<stdio.h> #include<string.h&g ...

  8. 8255交通灯实验的微型计算机,微机原理实验四实验报告8255控制交通灯实验

    <微机原理实验四实验报告8255控制交通灯实验>由会员分享,可在线阅读,更多相关<微机原理实验四实验报告8255控制交通灯实验(4页珍藏版)>请在人人文库网上搜索. 1.实验四 ...

  9. 计算机网络云南大学实验四,云南大学软件学院计算机网络原理实验四.doc

    云南大学软件学院计算机网络原理实验四 实验四.web服务器套接字编程实验指导 1.实验目的: 编写一个WEB服务器程序,可以接受来自浏览器的访问,并传输页面(包含多个对象)到浏览器.掌握Socket编 ...

最新文章

  1. 一种注册表沙箱的思路、实现——研究Reactos中注册表函数的实现2
  2. 2.SDL游戏开发:把代码写长一点(一)
  3. Javascript数组操作方法
  4. STL 之remove,remove_if,remove_copy,remove_copy_if
  5. Android--List与ArrayList区别(转)
  6. axios框架里面如何使用get,post,通用ajax方法请求。
  7. Handler学习总结
  8. windows form参数传递过程
  9. 智能优化算法:水基湍流优化算法-附代码
  10. android ndk下载安装教程,NDK安装教程20180605
  11. ENVI监督分类错误:分离度为0.00000解决办法
  12. 第一次出书的经验分享
  13. 致信oa系统服务器ip,OA系统登陆考勤IP控制
  14. 李开复创办创新工场的发言稿及访谈
  15. Linux 网桥功能使用
  16. windows server 2008 64 位 上安装 postgreSQL 、 openbravo 报错解决
  17. 还敢搞黄色?4 个色情网站被一锅端,9 名福利姬被刑拘!
  18. 纯代码告诉你:我的原弈非常(Yanj Future)是怎么下棋的
  19. 猫哥教你写爬虫 027--模块介绍
  20. libgdx中文社区网正式上线了-libgdx.net

热门文章

  1. 操作系统——文件系统
  2. 深入探讨JDBC往MySQL中插入Timestamp类型字段报错问题
  3. 软件的国际化、Jstl国际化标签
  4. H3C option43配置
  5. DevExpress TreeList的三角形改成加号
  6. linux操作系统崩溃,Linux操作系统死机处理方法总结
  7. python实现月食效果实例代码
  8. 西门子PLC之读与写
  9. littlevgl 6.0 外部spiflash 显示中文
  10. java 处理pdb文件格式_PDB(Protein Data Bank)数据格式详解