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
183 changes: 181 additions & 2 deletions kernel/src/arch_impl/aarch64/context_switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub extern "C" fn check_need_resched_and_switch_arm64(
// No reschedule needed
if from_el0 {
// Check for pending signals before returning to userspace
// TODO: Implement signal delivery for ARM64
check_and_deliver_signals_for_current_thread_arm64(frame);
}
return;
}
Expand All @@ -110,12 +110,20 @@ pub extern "C" fn check_need_resched_and_switch_arm64(

// Handle "no switch needed" case
if schedule_result.is_none() {
// Even though no context switch happens, check for signals
// when returning to userspace
if from_el0 {
check_and_deliver_signals_for_current_thread_arm64(frame);
}
return;
}

if let Some((old_thread_id, new_thread_id)) = schedule_result {
if old_thread_id == new_thread_id {
// Same thread continues running
// Same thread continues running, but check for pending signals
if from_el0 {
check_and_deliver_signals_for_current_thread_arm64(frame);
}
return;
}

Expand Down Expand Up @@ -764,3 +772,174 @@ fn emit_el0_entry_marker() {
raw_uart_str("[ OK ] EL0_SMOKE: userspace executed + syscall path verified\n");
}
}

// =============================================================================
// ARM64 Signal Delivery
// =============================================================================

/// Check and deliver pending signals for the current thread (ARM64)
///
/// Called when returning to userspace (EL0) without a context switch.
/// This ensures signals are delivered promptly even when the same thread keeps running.
///
/// Key differences from x86_64:
/// - User stack pointer is in SP_EL0, not in the exception frame
/// - Uses TTBR0_EL1 instead of CR3 for page table switching
/// - SPSR contains processor state instead of RFLAGS
fn check_and_deliver_signals_for_current_thread_arm64(frame: &mut Aarch64ExceptionFrame) {
// Get current thread ID
let current_thread_id = match crate::task::scheduler::current_thread_id() {
Some(id) => id,
None => return,
};

// Thread 0 is the idle thread - it doesn't have a process with signals
if current_thread_id == 0 {
return;
}

// Try to acquire process manager lock
let mut manager_guard = match crate::process::try_manager() {
Some(guard) => guard,
None => return, // Lock held, skip signal check this time
};

// Track if signal termination happened (for parent notification after borrow ends)
let mut signal_termination_info: Option<crate::signal::delivery::ParentNotification> = None;

if let Some(ref mut manager) = *manager_guard {
// Find the process for this thread
if let Some((_pid, process)) = manager.find_process_by_thread_mut(current_thread_id) {
// Check for expired timers
crate::signal::delivery::check_and_fire_alarm(process);
crate::signal::delivery::check_and_fire_itimer_real(process, 5000);

if crate::signal::delivery::has_deliverable_signals(process) {
// Read current SP_EL0 (user stack pointer)
let sp_el0: u64;
unsafe {
core::arch::asm!("mrs {}, sp_el0", out(reg) sp_el0, options(nomem, nostack));
}

// Switch to process's page table for signal delivery
// On ARM64, this is TTBR0_EL1
if let Some(ref page_table) = process.page_table {
let ttbr0_value = page_table.level_4_frame().start_address().as_u64();
unsafe {
// Write new TTBR0
core::arch::asm!(
"msr ttbr0_el1, {}",
"dsb ish",
"isb",
in(reg) ttbr0_value,
options(nomem, nostack)
);
}
}

// Create SavedRegisters from exception frame for signal delivery
let mut saved_regs = create_saved_regs_from_frame(frame, sp_el0);

// Deliver signals
let signal_result = crate::signal::delivery::deliver_pending_signals(
process,
frame,
&mut saved_regs,
);

// If signals were delivered, update SP_EL0 with new stack pointer
// The signal frame was pushed onto the user stack
if !matches!(signal_result, crate::signal::delivery::SignalDeliveryResult::NoAction) {
unsafe {
core::arch::asm!(
"msr sp_el0, {}",
in(reg) saved_regs.sp,
options(nomem, nostack)
);
}
}

match signal_result {
crate::signal::delivery::SignalDeliveryResult::Terminated(notification) => {
// Signal terminated the process
crate::task::scheduler::set_need_resched();
signal_termination_info = Some(notification);
setup_idle_return_arm64(frame);
crate::task::scheduler::switch_to_idle();
// Don't return here - fall through to handle notification
}
crate::signal::delivery::SignalDeliveryResult::Delivered => {
if process.is_terminated() {
crate::task::scheduler::set_need_resched();
setup_idle_return_arm64(frame);
crate::task::scheduler::switch_to_idle();
}
}
crate::signal::delivery::SignalDeliveryResult::NoAction => {}
}
}
}
// process borrow has ended here

// Drop manager guard first to avoid deadlock when notifying parent
drop(manager_guard);

// Notify parent if signal terminated a child
if let Some(notification) = signal_termination_info {
crate::signal::delivery::notify_parent_of_termination_deferred(&notification);
}
}
}

/// Create SavedRegisters from an Aarch64ExceptionFrame and SP_EL0
///
/// This is needed because the signal delivery code operates on SavedRegisters,
/// which includes the stack pointer that isn't in the exception frame on ARM64.
///
/// This function is the core of ARM64 signal delivery - it converts the
/// exception frame (used by hardware) to SavedRegisters (used by the signal
/// infrastructure). This conversion is ARM64-specific because:
/// - ARM64 exception frames don't include SP_EL0 (user stack pointer)
/// - The register mapping is completely different from x86_64
/// - SPSR/ELR have ARM64-specific semantics
pub fn create_saved_regs_from_frame(
frame: &Aarch64ExceptionFrame,
sp_el0: u64,
) -> crate::task::process_context::SavedRegisters {
crate::task::process_context::SavedRegisters {
x0: frame.x0,
x1: frame.x1,
x2: frame.x2,
x3: frame.x3,
x4: frame.x4,
x5: frame.x5,
x6: frame.x6,
x7: frame.x7,
x8: frame.x8,
x9: frame.x9,
x10: frame.x10,
x11: frame.x11,
x12: frame.x12,
x13: frame.x13,
x14: frame.x14,
x15: frame.x15,
x16: frame.x16,
x17: frame.x17,
x18: frame.x18,
x19: frame.x19,
x20: frame.x20,
x21: frame.x21,
x22: frame.x22,
x23: frame.x23,
x24: frame.x24,
x25: frame.x25,
x26: frame.x26,
x27: frame.x27,
x28: frame.x28,
x29: frame.x29,
x30: frame.x30,
sp: sp_el0,
elr: frame.elr,
spsr: frame.spsr,
}
}
1 change: 1 addition & 0 deletions kernel/src/arch_impl/aarch64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub use syscall_entry::{is_el0_confirmed, syscall_return_to_userspace_aarch64};
#[allow(unused_imports)]
pub use context_switch::{
check_need_resched_and_switch_arm64,
create_saved_regs_from_frame,
idle_loop_arm64,
perform_context_switch,
switch_to_new_thread,
Expand Down
3 changes: 0 additions & 3 deletions kernel/src/ipc/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,11 @@ impl PipeBuffer {
/// Wake all threads waiting to read from this pipe
fn wake_read_waiters(&mut self) {
let waiters: Vec<u64> = self.read_waiters.drain(..).collect();
#[cfg(target_arch = "x86_64")]
for tid in waiters {
crate::task::scheduler::with_scheduler(|sched| {
sched.unblock(tid);
});
}
#[cfg(target_arch = "aarch64")]
let _ = waiters; // On ARM64 we don't have a scheduler yet
}

/// Check if pipe is readable (has data or EOF)
Expand Down
10 changes: 9 additions & 1 deletion kernel/src/process/creation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,15 @@ pub fn create_user_process(name: String, elf_data: &[u8]) -> Result<ProcessId, &
crate::task::scheduler::spawn(Box::new(main_thread.clone()));
crate::serial_println!("create_user_process: scheduler::spawn completed");

// Note: ARM64 doesn't have TTY support yet, so no foreground pgrp setting
// Set this process as the foreground process group for the console TTY
// This ensures Ctrl+C (SIGINT) and other TTY signals go to this process
if let Some(tty) = crate::tty::console() {
tty.set_foreground_pgrp(pid.as_u64());
log::debug!(
"create_user_process: Set PID {} as foreground pgrp for TTY (ARM64)",
pid.as_u64()
);
}

log::info!(
"create_user_process: User thread {} enqueued for scheduling",
Expand Down
20 changes: 20 additions & 0 deletions kernel/src/task/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ static SCHEDULER: Mutex<Option<Scheduler>> = Mutex::new(None);
/// Global need_resched flag for timer interrupt
static NEED_RESCHED: AtomicBool = AtomicBool::new(false);

/// Counter for unblock() calls - used for testing pipe wake mechanism
/// This is a global atomic because:
/// 1. unblock() is called via with_scheduler() which already holds the scheduler lock
/// 2. Tests need to read this outside the scheduler lock
/// 3. AtomicU64 ensures visibility across threads without additional locking
static UNBLOCK_CALL_COUNT: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);

/// Get the current unblock() call count (for testing)
///
/// This function is used by the test framework to verify that pipe wake
/// mechanisms actually call scheduler.unblock(). It's only called when
/// the boot_tests feature is enabled.
#[allow(dead_code)] // Used by test_framework when boot_tests feature is enabled
pub fn unblock_call_count() -> u64 {
UNBLOCK_CALL_COUNT.load(Ordering::SeqCst)
}

/// The kernel scheduler
pub struct Scheduler {
/// All threads in the system
Expand Down Expand Up @@ -293,6 +310,9 @@ impl Scheduler {

/// Unblock a thread by ID
pub fn unblock(&mut self, thread_id: u64) {
// Increment the call counter for testing (tracks that unblock was called)
UNBLOCK_CALL_COUNT.fetch_add(1, Ordering::SeqCst);

if let Some(thread) = self.get_thread_mut(thread_id) {
if thread.state == ThreadState::Blocked || thread.state == ThreadState::BlockedOnSignal {
thread.set_ready();
Expand Down
Loading
Loading