The time has come to improve the text output, so that we can do more
complicated things with text, and have multiple processes writing text
output without interfering with each other like rtl8139
and gopher
in the last section.
The terminal consists of both input and output: when a user is interacting with a program the keyboard & mouse input should go to that program, and anything the program outputs should be shown on screen. We therefore need some kind of multiplexer which will take one console (combination of input & output) and split it into multiple virtual consoles.
A sketch of how this might work is shown below:
<vga> <keyboard/mouse> | | < vconsole > | ------------------------ | | | debug login1 login2 ... | shell
The vga
program should provide an interface for writing text to
screen. The vconsole
multiplexer will catch keys, perhaps Ctrl +
F1
, Ctrl + F2
etc., and switch the input & output between multiple
sets of input/output pairs: One could be a debug/status screen, and
others connected to instances of a login
program. That program will
be responsible for restricting user capabilities, mainly by
customising the Virtual File System (VFS) that the user sees. When a
user logs in a shell would be connected, which might have further
multiplexing for Ctrl + Tab
switching between processes, or for
arranging multiple programs on screen at once.
To enable all this to happen we need a way to specify where on the screen text should be drawn, what color it should be, whether it blinks etc. Fortunately there is an ANSI standard for doing this.
We probably need a very basic VGA driver in the kernel, at least for now, as a fallback and for debugging any startup issues. The choice then is between making that kernel driver more complete, or creating a new VGA driver which runs in user space. In the long run something like UEFI GOP is probably the way to go for high-resolution graphics, so extended VGA support will become optional rather than baked into the kernel. I’d like to keep the kernel as minimal as possible, so this will be a user-space driver.
There is a Rust vga crate which is hosted on github and includes both text (up to 80x25) and graphics modes (up to 640x480x16). It depends on some other crates, including font8x8 which provides 8x8 pixel unicode characters.
VGA (and all other graphics drivers that I know of) depend heavily on Direct Memory Access (DMA): the CPU writes to memory addresses, which are mapped into registers on the graphics card.
To access VGA memory from user programs we need to map the physical
0xA0000 .. 0xBFFFF
(inclusive). One way to do this would be to
identity map this range into user programs’ address space. The kernel
would need to know which programs should have this access, or all
programs would be able to write directly to VGA memory. Perhaps this
could be a capability like I/O privileges, that processes could choose
whether to share with their process children:
- Quite simple to implement: If a video memory flag is set in process
Params
then identity map the VGA memory range. - Multiple processes will have access to the VGA memory, unless there is a mechanism to unmap the memory range. This might be useful if a video driver crashes and another driver is needed
- Not clear if it generalises to different framebuffers e.g. UEFI GOP or other hardware.
Instead in EuraliOS we’ll reuse the memory chunk mechanism which allows regions of memory to be allocated, passed between processes and free’d. This:
- Enables this memory access to be passed between processed, but not shared.
- Needs some special handling in the allocation and freeing.
The code needed is quite short, however: We create a “special” memory chunk mapping a specified physical address:
pub fn special_memory_chunk(
thread: &Thread,
num_pages: u64,
start_physaddr: u64
) -> Result<(VirtAddr, PhysAddr), usize> {
// Virtual address of the available page chunk
let start_virtaddr = match memory::find_available_page_chunk(
thread.page_table_physaddr) {
Some(value) => value,
None => return Err(syscalls::SYSCALL_ERROR_MEMORY)
};
match memory::create_physical_range_pages(
thread.page_table_physaddr,
start_virtaddr,
num_pages,
PhysAddr::new(start_physaddr)) {
Ok(physaddr) => Ok((start_virtaddr, physaddr)),
Err(_) => Err(syscalls::SYSCALL_ERROR_MEMORY)
}
}
which we first allocate in a thread’s address space, remove it, and send it in a message:
let thread = process::new_user_thread(...).unwrap();
// Allocate a memory chunk mapping video memory
let (virtaddr, _) = process::special_memory_chunk(
&init_thread,
32, // Pages, 128k. 0xC0000 - 0xA0000
0xA0000).unwrap();
// Remove chunk from table so it can be sent
let (physaddr, _) = init_thread.take_memory_chunk(virtaddr).unwrap();
// Send a message to process containing the chunk.
// When received the chunk will be mapped into address space
thread_rendezvous.write().send(None, Message::Long(
message::VIDEO_MEMORY,
(0xC0000 - 0xA0000).into(),
physaddr.into()
));
The process can receive this message when it starts. Processes are usually given
two Rendezvous handles, STDIN
and STDOUT
. Receiving the video memory message
could be through either, but here it’s through STDOUT to avoid collisions with
input from other sources e.g. keyboard:
// Expect a video memory buffer from the kernel
// Note: Sent to STDOUT channel to avoid conflict with keyboard
let (vmem_length, vmem_handle) = match syscalls::receive(&STDOUT) {
Ok(Message::Long(
message::VIDEO_MEMORY,
MessageData::Value(length),
MessageData::MemoryHandle(handle))) => {
(length, handle)
},
m => {
panic!("Expected video memory message. Received {:?}", m);
}
};
Having mapped video memory into a user program’s address space, the
next task is to figure out how to write to it. The vga
library
expects to find the VGA buffer at the virtual address identity mapped
to the physical address i.e. the range 0xA0000 .. 0xBFFFF
(inclusive), but our video memory chunk might be mapped to different
virtual memory addresses. We therefore need to modify the vga
library
to use arbitrary video addresses.
I’ve added a video_memory_start
field to the Vga
type, defaulting
to the 0xa0000
physical memory address, and a set_memory_start
method to change the default to an arbitrary virtual address. The
FrameBuffer
enum type is extended to hold an arbitrary usize
address, rather than constant physical addresses.
We can now move the VGA driver execution out of the kernel and into the
init
program, passing on the I/O privilege:
// Start the VGA driver
syscalls::exec(
include_bytes!("../../user/vga_driver"),
syscalls::EXEC_PERM_IO, // I/O permissions
vga_com2.clone(),
vga_com2);
The kernel now just starts the init
user program, and that user
program then starts everything else. Unfortunately loading
vga_driver
results in a kernel panic, shown in figure
fig-kernel-panic:
This occurs when the kernel tries to allocate memory for the
vga_driver
ELF binary the allocation fails on this line:
// Assemble a slice then copy to a Vec in the kernel heap
let bin_vec = unsafe{slice::from_raw_parts(bin, bin_length as usize)}.to_vec();
This is because the kernel heap is only 100kb, so part of the solution is to just increase the kernel heap size. It would be good to avoid a kernel panic by first checking if memory can be allocated:
// Assemble a slice pointing to user data
let bin_slice = unsafe{slice::from_raw_parts(bin, bin_length as usize)};
let mut bin_vec : Vec<u8> = Vec::new();
// Reserve space
if bin_vec.try_reserve_exact(bin_slice.len()).is_err() {
// Could not allocate memory
println!("[kernel] Couldn't allocate {} bytes for Exec from thread {}", bin_slice.len(), thread.tid());
thread.return_error(SYSCALL_ERROR_MEMORY);
process::set_current_thread(thread);
return;
}
// Copy data into vector, which is now large enough
bin_vec.extend(bin_slice.iter());
Now we get the slightly more useful result in figure fig-user-panic:
The kernel can’t allocate enough memory, so returns with an error
message; the user program (init
) panics but other programs (pci
)
continue.
When running multiple programs simultaneously it would be good to be able to switch between them. While a program is not writing to the physical screen the changes should be made to a memory buffer, and then when the screen is switched the buffer should be copied into VGA memory. If we want to completely separate the VGA driver from the virtual console code then the rendering code (interpreting ANSI etc.) needs to be duplicated: Reading from VGA memory is (apparently) very slow so even when a process is writing to the physical screen we would still need to render to a separate buffer.
In a concession to efficiency the VGA driver in EuraliOS will provide multiple screen “writers”, and a mechanism to switch between them. A separate process can then decide the policy of when to switch, based on e.g keyboard inputs.
We can now switch between screens with this function, which sends a message to the VGA driver with the screen ID to switch to:
fn activate_writer(vga_com: &CommHandle, writer_id: u64) {
syscalls::send(
&vga_com,
Message::Short(message::WRITE, writer_id, 0));
}
The special keys like `F1` to `F12` are not currently forwarded by the keyboard handler, as only Unicode characters are sent. For now we can just intercept TAB and ESC characters to switch between two screens, one for system programs (`pci`, `rtl8139` and `tcp`) and one for the user program (`gopher`):
loop {
// Wait for keyboard input
match syscalls::receive(&STDIN) {
Ok(syscalls::Message::Short(
message::CHAR, ch, _)) => {
// Received a character
if ch == 9 { // TAB
activate_writer(&vga_com, writer_user_id);
} else if ch == 27 { // ESC
activate_writer(&vga_com, writer_sys_id);
} else {
syscalls::send(&input_user,
syscalls::Message::Short(
message::CHAR, ch, 0));
}
}
_ => {
// Ignore
}
}
}
Now the TAB and ESC keys switch between the system screen with messages from `tcp` and `rtl8139` networking processes on one screen (fig fig-writer-sys), and the output of the `gopher` program on the other (fig fig-gopher).
ANSI escape codes are used to change the position and color of the cursor on text-based terminals.
https://github.com/rust-osdev/ansi_rgb
In the next section we’ll start making the operating system more useful by adding a simple file system stored in memory (a ramdisk).