Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,18 @@ Userspace is a buildroot-produced rootfs cpio, loaded at boot via QEMU
`-initrd` (path from DTB `/chosen/linux,initrd-{start,end}`) and
extracted into a tmpfs-backed `/` in `initramfs::extract()`:

- **dash** + **GNU coreutils** come from the buildroot package set
(`cmake/buildroot.cmake`, `configs/solaya_riscv64_buildroot_defconfig.in`).
- Solaya's Rust binaries (init, dhcpd, tcp_echo, webserver, test
- **busybox** (init + sh + applets) and **dash** + **GNU coreutils** come
from the buildroot package set (`cmake/buildroot.cmake`,
`configs/solaya_riscv64_buildroot_defconfig.in`).
- Solaya's Rust userspace binaries (dhcpd, tcp_echo, webserver, test
fixtures like `prog1`/`*-test`) are built by `userspace-rust` and
layered on top via `BR2_ROOTFS_OVERLAY` — they end up at `/bin/<name>`.
- PID 1 is Solaya's Rust `init` (read from `/bin/init` by
`process_table::load_init_bytes`), which execs `/bin/dhcpd` once then
spawns `/bin/dash`.
- PID 1 is busybox (read from `/sbin/init` by
`process_table::load_init_bytes`, which resolves the buildroot symlink
to `/bin/busybox`). Busybox reads `/etc/inittab`
(`configs/overlay/etc/inittab`): runs `/etc/init.d/rcS`, waits on
`/bin/dhcpd` to configure the network, then respawns
`/bin/dash -i` on the serial console.

### Adding a userspace program

Expand Down
13 changes: 5 additions & 8 deletions configs/solaya_riscv64_buildroot_defconfig.in
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,11 @@ BR2_TOOLCHAIN_EXTERNAL_BOOTLIN_RISCV64_LP64D_MUSL_STABLE=y
# are separate follow-up features.
BR2_STATIC_LIBS=y

# --- Init system: Solaya's Rust init stays PID 1 -------------------------
# Buildroot provides no init (we don't use busybox init yet — needs kernel
# AF_UNIX socketpair + shebang execve support). Our Rust init.rs runs
# as PID 1 from /bin/init and execs /bin/dash directly.
BR2_INIT_NONE=y
# BR2_PACKAGE_BUSYBOX is not set — dropping busybox entirely also
# auto-enables BR2_PACKAGE_BUSYBOX_SHOW_OTHERS, which is required by the
# coreutils package selection below.
# --- Init system: busybox init as PID 1 ----------------------------------
# Installs /sbin/init as a symlink to /bin/busybox; inittab comes from
# configs/overlay/etc/inittab.
BR2_INIT_BUSYBOX=y
BR2_PACKAGE_BUSYBOX=y

# --- Shell + coreutils ---------------------------------------------------
BR2_PACKAGE_DASH=y
Expand Down
6 changes: 6 additions & 0 deletions crates/driver-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ pub trait CharDevice: Send + Sync {
/// Write `data`. Returns the number of bytes written (typically
/// `data.len()` for synchronous console-style devices).
fn write(&self, data: &[u8]) -> Result<usize, IoError>;

/// True if this device is a terminal. `openat` uses this to wrap the
/// fd in `FileDescriptor::Tty` instead of the default VfsFile.
fn is_tty(&self) -> bool {
false
}
}

/// Static framebuffer description, returned by `DisplayDevice::framebuffer`.
Expand Down
4 changes: 4 additions & 0 deletions crates/kernel/src/fs/devfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ impl VfsNode for CharNode {
fn truncate(&self, _length: usize) -> Result<(), Errno> {
Ok(())
}

fn char_device(&self) -> Option<Arc<dyn CharDevice>> {
Some(self.device.clone())
}
}

struct DisplayNode {
Expand Down
10 changes: 9 additions & 1 deletion crates/kernel/src/fs/vfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use alloc::{
vec::Vec,
};
use core::sync::atomic::{AtomicU64, Ordering};
use driver_api::BlockDevice;
use driver_api::{BlockDevice, CharDevice};
use headers::errno::Errno;

use hal::spinlock::Spinlock;
Expand Down Expand Up @@ -179,6 +179,14 @@ pub trait VfsNode: Send + Sync {
None
}

/// If this node is backed by a character device, return an
/// `Arc<dyn CharDevice>`. Used by `openat` to recognise `/dev/console`
/// and produce a blocking `FileDescriptor::Tty` instead of the default
/// non-blocking `VfsFile`.
fn char_device(&self) -> Option<Arc<dyn CharDevice>> {
None
}

fn atime(&self) -> (i64, u32) {
(0, 0)
}
Expand Down
17 changes: 7 additions & 10 deletions crates/kernel/src/io/uart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,9 @@ use headers::errno::Errno;

pub use console::uart::CONSOLE_UART;

/// `CharDevice` adapter for the console UART.
///
/// Carries the TTY line discipline internally: `write` goes through the TTY
/// `process_output` path (handles ONLCR, echo, etc.) before hitting the
/// UART; `read` drains cooked bytes from the TTY input buffer.
///
/// The TTY itself still lives in `io/tty_device` and is wired up at init.
/// Fully decoupling TTY from UART stays deferred (#250 item #5).
/// `CharDevice` adapter for the console UART. Write goes through the TTY
/// line discipline (ONLCR, echo, ...) before the UART; read drains cooked
/// input bytes.
pub struct ConsoleCharDevice;

impl CharDevice for ConsoleCharDevice {
Expand All @@ -47,10 +42,12 @@ impl CharDevice for ConsoleCharDevice {
}
Ok(data.len())
}

fn is_tty(&self) -> bool {
true
}
}

/// Register the console UART as a `CharDevice` in both the registry and
/// devfs. Called once during kernel init.
pub fn register_console_char_device() {
let device: Arc<dyn CharDevice> = Arc::new(ConsoleCharDevice);
crate::drivers::registry::<dyn CharDevice>().register(device.clone());
Expand Down
17 changes: 9 additions & 8 deletions crates/kernel/src/processes/process_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,11 @@ pub fn init() {

/// Source the PID-1 ELF image from the initramfs-populated rootfs.
///
/// /bin/init is Solaya's Rust init (delivered via the buildroot overlay).
/// /sbin/init would be busybox if we ever flipped back to it — keeping
/// both paths means swapping busybox in is a one-line reorder plus the
/// AF_UNIX socketpair + shebang execve kernel work that's currently on
/// the follow-up list.
/// `/sbin/init` is buildroot's busybox (symlink to `/bin/busybox`).
/// `/init` stays in the search list as the conventional initramfs
/// fallback.
fn load_init_bytes() -> Arc<[u8]> {
const INIT_PATHS: &[&str] = &["/bin/init", "/sbin/init", "/init"];
const INIT_PATHS: &[&str] = &["/sbin/init", "/init"];
for path in INIT_PATHS {
let Ok(node) = fs::resolve_path(path) else {
continue;
Expand All @@ -75,7 +73,7 @@ fn load_init_bytes() -> Arc<[u8]> {
crate::info!("init: loaded PID 1 from {path} ({n} bytes)");
return Arc::<[u8]>::from(buf);
}
panic!("init: no /sbin/init, /bin/init, or /init in the root filesystem");
panic!("init: no /sbin/init or /init in the root filesystem");
}

pub struct ProcessTable {
Expand Down Expand Up @@ -247,13 +245,16 @@ impl ProcessTable {
return false;
}
t.raise_signal(sig);
let sigtimedwait_hit = t.sigtimedwait_matches(sig);
// SIGCONT resumes stopped threads
if sig == headers::syscall_types::SIGCONT && t.get_state() == ThreadState::Stopped {
t.clear_pending_stop_signals();
t.set_state(ThreadState::Runnable);
return true;
}
if t.has_pending_unblocked_signal() && t.get_state() == ThreadState::Waiting {
if (sigtimedwait_hit || t.has_pending_unblocked_signal())
&& t.get_state() == ThreadState::Waiting
{
t.set_state(ThreadState::Runnable);
return true;
}
Expand Down
11 changes: 7 additions & 4 deletions crates/kernel/src/processes/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ impl PendingSignals {
self.0 &= !(1u64 << sig);
}

pub fn first_unblocked(&self, mask: u64) -> Option<u32> {
let deliverable = self.0 & !mask;
if deliverable == 0 {
/// Lowest-numbered pending signal that intersects `allowed`.
/// Callers pass `!sigmask` for delivery or the sigtimedwait set
/// directly to wait for blocked signals.
pub fn first_matching(&self, allowed: u64) -> Option<u32> {
let matched = self.0 & allowed;
if matched == 0 {
return None;
}
Some(deliverable.trailing_zeros())
Some(matched.trailing_zeros())
}
}

Expand Down
52 changes: 41 additions & 11 deletions crates/kernel/src/processes/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ pub struct Thread {
pub stopped_notified: bool,
pub stop_signal: u32,
thread_name: Option<String>,
/// Active `rt_sigtimedwait` set, if the thread is currently suspended
/// in that syscall. `send_signal` consults this to wake the thread
/// even for signals that are blocked in `sigmask`.
sigtimedwait_mask: Option<u64>,
}

impl core::fmt::Display for Thread {
Expand Down Expand Up @@ -258,6 +262,7 @@ impl Thread {
stopped_notified: false,
stop_signal: 0,
thread_name: None,
sigtimedwait_mask: None,
}))
}

Expand Down Expand Up @@ -299,11 +304,12 @@ impl Thread {
self.state = ThreadState::Runnable;
return true;
}
if self.has_pending_unblocked_signal() {
if self.has_pending_unblocked_signal() || self.sigtimedwait_pending() {
// A signal arrived while the thread was Running (before the
// syscall yielded). Same reasoning as the wakeup_pending branch:
// drop to Runnable so the scheduler can re-pick us and deliver
// the signal via the normal path.
// the signal via the normal path. sigtimedwait_pending covers
// blocked signals the thread is explicitly waiting for.
self.state = ThreadState::Runnable;
return true;
}
Expand Down Expand Up @@ -502,27 +508,51 @@ impl Thread {
}

pub fn has_pending_unblocked_signal(&self) -> bool {
self.signal_state
.pending
.first_unblocked(self.signal_state.sigmask.sig[0])
.is_some()
self.peek_first_unblocked_signal().is_some()
}

pub fn peek_first_unblocked_signal(&self) -> Option<u32> {
self.signal_state
.pending
.first_unblocked(self.signal_state.sigmask.sig[0])
.first_matching(!self.signal_state.sigmask.sig[0])
}

pub fn take_next_pending_signal(&mut self) -> Option<u32> {
let sig = self
.signal_state
.pending
.first_unblocked(self.signal_state.sigmask.sig[0])?;
let sig = self.peek_first_unblocked_signal()?;
self.signal_state.pending.clear(sig);
Some(sig)
}

/// Lowest-numbered pending signal that is in `set`, regardless of sigmask.
pub fn first_pending_in_set(&self, set: u64) -> Option<u32> {
self.signal_state.pending.first_matching(set)
}

pub fn clear_pending(&mut self, sig: u32) {
self.signal_state.pending.clear(sig);
}

pub fn set_sigtimedwait_mask(&mut self, mask: u64) {
self.sigtimedwait_mask = Some(mask);
}

pub fn clear_sigtimedwait_mask(&mut self) {
self.sigtimedwait_mask = None;
}

/// True when a raised signal should wake a thread parked in
/// `rt_sigtimedwait`, regardless of the thread's sigmask.
pub fn sigtimedwait_matches(&self, sig: u32) -> bool {
self.sigtimedwait_mask
.is_some_and(|m| m & (1u64 << sig) != 0)
}

/// True if any pending signal matches the active `rt_sigtimedwait` set.
pub fn sigtimedwait_pending(&self) -> bool {
self.sigtimedwait_mask
.is_some_and(|m| self.signal_state.pending.first_matching(m).is_some())
}

pub fn get_sigaction_raw(&self, sig: u32) -> &sigaction {
&self.signal_state.sigaction[sig as usize]
}
Expand Down
Loading
Loading