mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Hardware Interrupts: Fix a race condition in the test_println_output test
This commit is contained in:
@@ -355,6 +355,86 @@ pub fn _print(args: ::core::fmt::Arguments) {
|
|||||||
|
|
||||||
Note that disabling interrupts shouldn't be a general solution. The problem is that it increases the worst case interrupt latency, i.e. the time until the system reacts to an interrupt. Therefore interrupts should be only disabled for a very short time.
|
Note that disabling interrupts shouldn't be a general solution. The problem is that it increases the worst case interrupt latency, i.e. the time until the system reacts to an interrupt. Therefore interrupts should be only disabled for a very short time.
|
||||||
|
|
||||||
|
## Fixing a Race Condition
|
||||||
|
|
||||||
|
If you run `cargo xtest` you might see the `test_println_output` test failing:
|
||||||
|
|
||||||
|
```
|
||||||
|
> cargo xtest --lib
|
||||||
|
[…]
|
||||||
|
Running 4 tests
|
||||||
|
test_breakpoint_exception...[ok]
|
||||||
|
test_println... [ok]
|
||||||
|
test_println_many... [ok]
|
||||||
|
test_println_output... [failed]
|
||||||
|
|
||||||
|
Error: panicked at 'assertion failed: `(left == right)`
|
||||||
|
left: `'.'`,
|
||||||
|
right: `'S'`', src/vga_buffer.rs:205:9
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason is a _race condition_ between the test and our timer handler. Remember, the test looks like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// in src/vga_buffer.rs
|
||||||
|
|
||||||
|
#[test_case]
|
||||||
|
fn test_println_output() {
|
||||||
|
serial_print!("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);
|
||||||
|
}
|
||||||
|
|
||||||
|
serial_println!("[ok]");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The test prints a string to the VGA buffer and then checks the output by manually iterating over the `buffer_chars` array. The race condition occurs because the timer interrupt handler might run between the `println` and the reading of the screen characters. Note that this isn't a dangerous _data race_, which Rust completely prevents at compile time. See the [_Rustonomicon_][nomicon-races] for details.
|
||||||
|
|
||||||
|
[nomicon-races]: https://doc.rust-lang.org/nomicon/races.html
|
||||||
|
|
||||||
|
To fix this, we need to keep the `WRITER` locked for the complete duration of the test, so that the timer handler can't write a `.` to the screen in between. The fixed test looks like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// in src/vga_buffer.rs
|
||||||
|
|
||||||
|
#[test_case]
|
||||||
|
fn test_println_output() {
|
||||||
|
use core::fmt::Write;
|
||||||
|
use x86_64::instructions::interrupts;
|
||||||
|
|
||||||
|
serial_print!("test_println_output... ");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
serial_println!("[ok]");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We performed the following changes:
|
||||||
|
|
||||||
|
- We keep the writer locked for the complete test by using the `lock()` method explicitly. Instead of `println`, we use the [`writeln`] marco that allows printing to an already locked writer.
|
||||||
|
- To avoid another deadlock, we disable interrupts for the tests duration. Otherwise the test might get interrupted while the writer is still locked.
|
||||||
|
- Since the timer interrupt handler can still run before the test, we print an additional newline `\n` before printing the string `s`. This way, we avoid test failure when the timer handler already printed some `.` characters to the current line.
|
||||||
|
|
||||||
|
[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html
|
||||||
|
|
||||||
|
With the above changes, `cargo xtest` now deterministically succeeds again.
|
||||||
|
|
||||||
|
This was a very harmless race condition that only caused a test failure. As you can imagine, other race conditions can be much more difficult to debug due to their non-deterministic nature. Luckily, Rust prevents us from data races, which are the most serious class of race conditions since they can cause all kinds of undefined behavior, including system crashes and silent memory corruptions.
|
||||||
|
|
||||||
## The `hlt` Instruction
|
## The `hlt` Instruction
|
||||||
|
|
||||||
Until now we used a simple empty loop statement at the end of our `_start` and `panic` functions. This causes the CPU to spin endlessly and thus works as expected. But it is also very inefficient, because the CPU continues to run at full speed even though there's no work to do. You can see this problem in your task manager when you run your kernel: The QEMU process needs close to 100% CPU the whole time.
|
Until now we used a simple empty loop statement at the end of our `_start` and `panic` functions. This causes the CPU to spin endlessly and thus works as expected. But it is also very inefficient, because the CPU continues to run at full speed even though there's no work to do. You can see this problem in your task manager when you run your kernel: The QEMU process needs close to 100% CPU the whole time.
|
||||||
|
|||||||
Reference in New Issue
Block a user