//! virtio-console: a second terminal, `hvc0` ("hypervisor console", the //! name Linux gives it). QEMU connects it to anything a *chardev* can be: //! //! -device virtio-serial-pci -device virtconsole,chardev=hvc //! -chardev socket,id=hvc,path=target/hvc0.sock,server=on,wait=off //! (then on the host: socat - UNIX-CONNECT:target/hvc0.sock) //! -chardev file,id=hvc,path=out.txt,input-path=in.txt (tests) //! //! Two queues: 0 = receive (host -> us), 1 = transmit (us -> host). (With //! the MULTIPORT feature there'd be more ports and a control queue; we //! don't ask for it, so it's exactly one port, which QEMU opens for us.) //! //! * **Receiving**: we keep 8 empty 512-byte buffers in the receive queue. //! When the host sends bytes the device fills one, puts it on the used //! ring and raises an interrupt; we copy the bytes into INPUT and put the //! buffer straight back. Readers of /dev/hvc0 wait on INPUT. //! * **Sending**: 8 buffers too. A write copies into a free one and kicks //! the device. If all 8 are still in flight (host not reading), we wait a //! few milliseconds and then *drop* the output: kernel logging must never //! hang because nobody is listening. //! //! Every kernel log line (`serial_print!`) also goes here -- except while //! a program has /dev/hvc0 open (e.g. a shell runs on it via `getty`), so //! that session isn't interleaved with log noise. use super::{Transport, Virtqueue}; use crate::memory; use crate::pci::PciDevice; use crate::thread::WaitQueue; use alloc::{format, string::String, vec::Vec}; use conquer_once::spin::OnceCell; use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crossbeam_queue::ArrayQueue; use spin::Mutex; use x86_64::{PhysAddr, instructions::interrupts::without_interrupts}; const SLOTS: u16 = 8; // buffers per direction const SLOT_SIZE: usize = 512; // 8 x 512 = one page per direction struct Hvc { transport: Transport, rx: Virtqueue, tx: Virtqueue, rx_phys: u64, tx_phys: u64, } static HVC: Mutex> = Mutex::new(None); static PRESENT: AtomicBool = AtomicBool::new(false); /// Bytes received from the host, not read yet. static INPUT: OnceCell> = OnceCell::uninit(); static INPUT_READY: WaitQueue = WaitQueue::new(); /// How many open /dev/hvc0 files there are (kernel log mirroring pauses /// while there are any). pub static OPEN_COUNT: AtomicUsize = AtomicUsize::new(0); /// Bytes we had to drop because the host wasn't reading. pub static DROPPED: AtomicUsize = AtomicUsize::new(0); fn input() -> &'static ArrayQueue { INPUT.get_or_init(|| ArrayQueue::new(4096)) } /// virtio-serial: transitional id 0x1003, modern 0x1043. pub fn is_virtio_console(d: &PciDevice) -> bool { d.vendor_id == super::VENDOR && matches!(d.device_id, 0x1003 | 0x1043) } pub fn present() -> bool { PRESENT.load(Ordering::Relaxed) } fn slot_ptr(base_phys: u64, slot: u16) -> *mut u8 { memory::phys_to_virt(PhysAddr::new(base_phys + slot as u64 * SLOT_SIZE as u64)).as_mut_ptr() } pub fn init(devices: &[PciDevice]) -> Option { let pci = devices.iter().find(|d| is_virtio_console(d))?; let setup = || -> Option { let transport = Transport::new(pci)?; transport.begin_init(0)?; // Receive interrupts on; transmit is polled (we reap finished // buffers whenever we write). let mut rx = transport.setup_queue(0, SLOTS, true)?; let tx = transport.setup_queue(1, SLOTS, false)?; let rx_phys = memory::alloc_contiguous_frames(1)?.as_u64(); let tx_phys = memory::alloc_contiguous_frames(1)?.as_u64(); while let Some(slot) = rx.next_free() { rx.push(&[(rx_phys + slot as u64 * SLOT_SIZE as u64, SLOT_SIZE as u32, true)])?; } transport.driver_ok(); rx.notify(); Some(Hvc { transport, rx, tx, rx_phys, tx_phys }) }; let Some(hvc) = setup() else { return Some(String::from("hvc0: virtio-console found but setup failed")); }; input(); without_interrupts(|| *HVC.lock() = Some(hvc)); PRESENT.store(true, Ordering::Relaxed); let irq = pci.interrupt_line(); crate::interrupts::enable_pci_irq(irq); Some(format!("hvc0: virtio-console at {:02x}:{:02x}.{}, IRQ {}", pci.bus, pci.device, pci.function, irq)) } /// Move received bytes from the used ring into INPUT and recycle the /// buffers. Called from the interrupt handler, and by readers (in case the /// interrupt line isn't one we handle). fn poll_rx(hvc: &mut Hvc) -> bool { let mut got = false; while let Some((slot, len)) = hvc.rx.pop_used() { let data = unsafe { core::slice::from_raw_parts(slot_ptr(hvc.rx_phys, slot), (len as usize).min(SLOT_SIZE)) }; for &b in data { let _ = input().push(b); // full: drop (4 KiB nobody reads) } got |= len > 0; let _ = hvc.rx.push(&[(hvc.rx_phys + slot as u64 * SLOT_SIZE as u64, SLOT_SIZE as u32, true)]); } if got { hvc.rx.notify(); } got } pub fn handle_irq() { let mut got = false; if let Some(mut guard) = HVC.try_lock() { if let Some(hvc) = guard.as_mut() { hvc.transport.read_isr(); got = poll_rx(hvc); } } if got { INPUT_READY.wake_all(); } } /// Send bytes to the host. Never blocks for long: drops what doesn't fit /// within ~10 ms. Returns how many bytes were sent. pub fn write(bytes: &[u8]) -> usize { if !present() { return 0; } without_interrupts(|| { let mut guard = HVC.lock(); let Some(hvc) = guard.as_mut() else { return 0 }; let mut sent = 0; for chunk in bytes.chunks(SLOT_SIZE) { // Reap finished sends; wait a little if none is free. let deadline = crate::time::now_us() + 10_000; let slot = loop { while hvc.tx.pop_used().is_some() {} if let Some(slot) = hvc.tx.next_free() { break Some(slot); } if crate::time::now_us() > deadline { break None; } core::hint::spin_loop(); }; let Some(slot) = slot else { DROPPED.fetch_add(bytes.len() - sent, Ordering::Relaxed); break; }; unsafe { core::ptr::copy_nonoverlapping(chunk.as_ptr(), slot_ptr(hvc.tx_phys, slot), chunk.len()) }; hvc.tx.push(&[(hvc.tx_phys + slot as u64 * SLOT_SIZE as u64, chunk.len() as u32, false)]); hvc.tx.notify(); sent += chunk.len(); } sent }) } /// Kernel log mirror (called by serial.rs for every `serial_print!`). pub fn log(args: core::fmt::Arguments) { if !present() || OPEN_COUNT.load(Ordering::Relaxed) > 0 { return; } /// Collect the formatted text in a small stack buffer (no heap needed). struct Buf { data: [u8; 256], len: usize, } impl core::fmt::Write for Buf { fn write_str(&mut self, s: &str) -> core::fmt::Result { for &b in s.as_bytes() { if self.len == self.data.len() { write(&self.data); self.len = 0; } self.data[self.len] = b; self.len += 1; } Ok(()) } } let mut buf = Buf { data: [0; 256], len: 0 }; let _ = core::fmt::Write::write_fmt(&mut buf, args); write(&buf.data[..buf.len]); } /// Read received bytes into `buf`; blocks until at least one is there /// (or a signal arrives: ERESTARTSYS). pub fn read(buf: &mut [u8]) -> Result { if buf.is_empty() { return Ok(0); } loop { let take = |buf: &mut [u8]| { let mut n = 0; while n < buf.len() { match input().pop() { Some(b) => { buf[n] = b; n += 1; } None => break, } } n }; without_interrupts(|| { if let Some(hvc) = HVC.lock().as_mut() { poll_rx(hvc); } }); let n = take(buf); if n > 0 { return Ok(n); } // Sleep until the interrupt handler has data (or 1 tick, then poll). let deadline = crate::interrupts::ticks() + 1; INPUT_READY.wait_until_deadline_intr(deadline, || (!input().is_empty()).then_some(()))?; } } /// Bytes waiting in the input buffer (tests). pub fn pending_input() -> usize { input().len() } /// Read all buffered input without blocking (tests). pub fn drain_input() -> Vec { without_interrupts(|| { if let Some(hvc) = HVC.lock().as_mut() { poll_rx(hvc); } }); core::iter::from_fn(|| input().pop()).collect() }