#中断 #操作系统 #x86 #键盘 #定时器 #处理程序 #可插拔

nightly no-std bin+lib pluggable_interrupt_os

允许用户通过提供中断处理程序来创建简单的 x86 操作系统

7 个版本

0.4.3 2022 年 8 月 20 日
0.4.2 2022 年 8 月 8 日
0.2.0 2021 年 1 月 24 日
0.1.1 2021 年 1 月 7 日

#1028硬件支持

每月 33 次下载

MIT 许可证

41KB
524 代码行

概述

此包允许用户通过提供定时器和键盘的中断处理程序来创建简单的操作系统。如有时间,我可能会添加其他有用的中断处理程序。

我开发此包是为了支持我在 Hendrix College 的操作系统课程作业。它为裸机编程提供了很好的介绍。它尚未在生产环境中进行过“实战测试”。

代码大量借鉴了优秀的资源 在 Rust 中编写操作系统 的示例。我衷心感谢 Philipp Oppermann 创建此资源的努力。每个源文件中的注释指定了我从他那里采用哪些代码元素。

在尝试使用此包之前

在阅读并理解了上述教程中的思想之后,您可以使用此包来创建自己的可插拔中断操作系统 (PIOS)。

简单示例

以下是一个非常基础的示例(在本包的 main.rs 中找到)

#![no_std]
#![no_main]

use pc_keyboard::DecodedKey;
use pluggable_interrupt_os::HandlerTable;

fn tick() {
    print!(".");
}

fn key(key: DecodedKey) {
    match key {
        DecodedKey::Unicode(character) => print!("{}", character),
        DecodedKey::RawKey(key) => print!("{:?}", key),
    }
}

#[no_mangle]
pub extern "C" fn _start() -> ! {
    HandlerTable::new()
        .keyboard(key)
        .timer(tick)
        .start()
}

在这个例子中,我们从中断处理程序开始。每当定时器事件发生时,tick()处理程序会在屏幕上打印一个句点。每当按键被按下时,key()处理程序会显示按下的字符。通过将这两个函数的引用放入一个名为HandlerTable的对象中,_start()函数启动一切。在调用HandlerTable.start()后,开始执行。PIOS会退回到后台并无限循环,依靠事件处理程序执行任何感兴趣或重要的事件。

更复杂的例子

我创建了一个简单但更复杂的例子,你可以将其用作自己项目的模板。它包括教程中提到的文件:.cargo/configCargo.tomlx86_64-blog_os.json。安装其他组件后,它应该可以运行。

它演示了一个简单的交互式程序,该程序使用键盘和定时器中断。当用户按下一个可看到的键时,它将被添加到屏幕中间的字符串中。当用户按下一个箭头键时,字符串开始向指示的方向移动。这里是它的main.rs

#![no_std]
#![no_main]

use lazy_static::lazy_static;
use spin::Mutex;
use pc_keyboard::DecodedKey;
use pluggable_interrupt_template::LetterMover;
use pluggable_interrupt_os::HandlerTable;

#[no_mangle]
pub extern "C" fn _start() -> ! {
    HandlerTable::new()
        .keyboard(key)
        .timer(tick)
        .start()
}

lazy_static! {
    static ref LETTERS: Mutex<LetterMover> = Mutex::new(LetterMover::new());
}


fn tick() {
    LETTERS.lock().tick();
}

fn key(key: DecodedKey) {
    LETTERS.lock().key(key);
}

我创建了LetterMover结构体来表示应用程序状态。它被包装在一个Mutex中,并使用lazy_static!初始化,以确保安全访问。几乎任何非平凡程序都需要使用这种设计模式。

tick()函数在解锁对象后调用LetterMover::tick()方法。同样,key()函数在解锁对象后调用LetterMover::key()方法。

以下是其余代码,位于其lib.rs文件中

#![cfg_attr(not(test), no_std)]

use bare_metal_modulo::{ModNum, ModNumIterator};
use pluggable_interrupt_os::vga_buffer::{BUFFER_WIDTH, BUFFER_HEIGHT, plot, ColorCode, Color, is_drawable};
use pc_keyboard::{DecodedKey, KeyCode};
use num::traits::SaturatingAdd;

#[derive(Copy,Debug,Clone,Eq,PartialEq)]
pub struct LetterMover {
    letters: [char; BUFFER_WIDTH],
    num_letters: ModNum<usize>,
    next_letter: ModNum<usize>,
    col: ModNum<usize>,
    row: ModNum<usize>,
    dx: ModNum<usize>,
    dy: ModNum<usize>
}

impl LetterMover {
    pub fn new() -> Self {
        LetterMover {
            letters: ['A'; BUFFER_WIDTH],
            num_letters: ModNum::new(1, BUFFER_WIDTH),
            next_letter: ModNum::new(1, BUFFER_WIDTH),
            col: ModNum::new(BUFFER_WIDTH / 2, BUFFER_WIDTH),
            row: ModNum::new(BUFFER_HEIGHT / 2, BUFFER_HEIGHT),
            dx: ModNum::new(0, BUFFER_WIDTH),
            dy: ModNum::new(0, BUFFER_HEIGHT)
        }
    }

该数据结构表示用户已输入的字母、输入字母的总数、下一个要输入的字母的位置、字符串的位置以及其运动。最初,字符串由字母A组成,位于屏幕中央且不移动。

ModNum数据类型表示一个整数(模m)。它非常有用,可以将所有这些值保持在VGA缓冲区的约束范围内。

    fn letter_columns(&self) -> impl Iterator<Item=usize> {
        ModNumIterator::new(self.col)
            .take(self.num_letters.a())
            .map(|m| m.a())
    }

同样来自bare_metal_modulo包的ModNumIterator数据类型从指定的值开始,并在环中循环。在这种情况下,它只需要足够的值来表示绘制字符串时使用的所有列。使用ModNum确保所有列值都是合法的,并且适当循环。

    pub fn tick(&mut self) {
        self.clear_current();
        self.update_location();
        self.draw_current();
    }

    fn clear_current(&self) {
        for x in self.letter_columns() {
            plot(' ', x, self.row.a(), ColorCode::new(Color::Black, Color::Black));
        }
    }
    
    fn update_location(&mut self) {
        self.col += self.dx;
        self.row += self.dy;
    }
    
    fn draw_current(&self) {
        for (i, x) in self.letter_columns().enumerate() {
            plot(self.letters[i], x, self.row.a(), ColorCode::new(Color::Cyan, Color::Black));
        }
    }

在每次滴答声中

  • 清除当前字符串。
  • 更新其位置。
  • 在新的位置重新绘制字符串。
    pub fn key(&mut self, key: DecodedKey) {
        match key {
            DecodedKey::RawKey(code) => self.handle_raw(code),
            DecodedKey::Unicode(c) => self.handle_unicode(c)
        }
    }

    fn handle_raw(&mut self, key: KeyCode) {
        match key {
            KeyCode::ArrowLeft => {
                self.dx -= 1;
            }
            KeyCode::ArrowRight => {
                self.dx += 1;
            }
            KeyCode::ArrowUp => {
                self.dy -= 1;
            }
            KeyCode::ArrowDown => {
                self.dy += 1;
            }
            _ => {}
        }
    }

    fn handle_unicode(&mut self, key: char) {
        if is_drawable(key) {
            self.letters[self.next_letter.a()] = key;
            self.next_letter += 1;
            self.num_letters = self.num_letters.saturating_add(&ModNum::new(1, self.num_letters.m()));
        }
    }
}

键盘处理程序接收每个按键时输入的字符。可表示为char的键被添加到移动字符串中。箭头键改变字符串的移动方式。

后台代码运行 - 并发数据访问的替代解决方案

在赋予 .cpu_loop() 方法的函数中包含的代码将在没有触发中断时执行。这种选项导致了对中央数据结构并发访问的另一种实现。而不是使用自旋锁 Mutex,中央数据结构可以是一个在 cpu_loop 函数中的局部变量。可以使用来自 crossbeamAtomicCell 对象存储和访问有关中断的信息。

请注意,这种方法允许创建比先前方法更通用的程序,因为任意代码可以在等待中断的同时在 cpu_loop 中运行。

下面的代码是前一个示例中 main.rs 的更新版本,它采用了这种替代方法。上面的 lib.rs 代码与这个替代版本一起工作不受影响。

#![no_std]
#![no_main]

use pc_keyboard::DecodedKey;
use pluggable_interrupt_os::HandlerTable;
use pluggable_interrupt_os::vga_buffer::clear_screen;
use pluggable_interrupt_template::LetterMover;
use crossbeam::atomic::AtomicCell;

#[no_mangle]
pub extern "C" fn _start() -> ! {
    HandlerTable::new()
        .keyboard(key)
        .timer(tick)
        .startup(startup)
        .cpu_loop(cpu_loop)
        .start()
}

static LAST_KEY: AtomicCell<Option<DecodedKey>> = AtomicCell::new(None);
static TICKS: AtomicCell<usize> = AtomicCell::new(0);

fn cpu_loop() -> ! {
    let mut kernel = LetterMover::new();
    let mut last_tick = 0;
    loop {
        if let Some(key) = LAST_KEY.load() {
            LAST_KEY.store(None);
            kernel.key(key);
        }
        let current_tick = TICKS.load();
        if current_tick > last_tick {
            last_tick = current_tick;
            kernel.tick();
        }
    }
}

fn tick() {
    TICKS.fetch_add(1);
}

fn key(key: DecodedKey) {
    LAST_KEY.store(Some(key));
}

fn startup() {
    clear_screen();
}

结论性思考

从这些示例中我们可以看出,你的 PIOS 的能力将限于处理键盘和定时器事件以及在 VGA 缓冲区中显示文本。然而,在这个范围内,你可以取得很大的成就。我个人喜欢重新创建一个著名的 1980 年代 街机经典 版本。

这是一个教学实验。我很乐意听到任何认为这有用的或提出建议的人。

更新

  • 0.4
    • 更新到 pic8259 版本 0.10
    • 修复相应的维护错误。
  • 0.2
    • 添加了 is_drawable() 函数,以确定给定的 char 是否可以在 VGA 缓冲区中渲染。
    • 重写了 README.md,以描述 可插入中断模板

依赖项

~1MB
~19K SLoC