diff --git a/kernel/src/driver/serial/mod.rs b/kernel/src/driver/serial/mod.rs index 971d01a87..a0551b879 100644 --- a/kernel/src/driver/serial/mod.rs +++ b/kernel/src/driver/serial/mod.rs @@ -8,7 +8,10 @@ use crate::mm::VirtAddr; use self::serial8250::serial8250_manager; use super::tty::{ - termios::{ControlMode, InputMode, LocalMode, OutputMode, Termios, INIT_CONTORL_CHARACTERS}, + termios::{ + ControlMode, InputMode, LocalMode, OutputMode, Termios, INIT_CONTORL_CHARACTERS, + INIT_C_LINE_ABI, + }, tty_ldisc::LineDisciplineType, }; @@ -67,6 +70,7 @@ lazy_static! { | LocalMode::IEXTEN, control_characters: INIT_CONTORL_CHARACTERS, line: LineDisciplineType::NTty, + c_line_abi: INIT_C_LINE_ABI, input_speed: SERIAL_BAUDRATE.data(), output_speed: SERIAL_BAUDRATE.data(), } diff --git a/kernel/src/driver/serial/serial8250/serial8250_pio.rs b/kernel/src/driver/serial/serial8250/serial8250_pio.rs index 8ac74fab5..85a6904dc 100644 --- a/kernel/src/driver/serial/serial8250/serial8250_pio.rs +++ b/kernel/src/driver/serial/serial8250/serial8250_pio.rs @@ -2,16 +2,17 @@ use core::{ hint::spin_loop, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering}, }; use alloc::{ + collections::VecDeque, string::ToString, sync::{Arc, Weak}, }; use crate::{ - arch::{driver::apic::ioapic::IoApic, io::PortIOArch, CurrentPortIOArch}, + arch::{driver::apic::ioapic::IoApic, io::PortIOArch, CurrentIrqArch, CurrentPortIOArch}, driver::{ base::device::{ device_number::{DeviceNumber, Major}, @@ -21,7 +22,7 @@ use crate::{ tty::{ console::ConsoleSwitch, kthread::{enqueue_tty_rx_byte_to_target_from_irq, TtyInputTarget}, - termios::WindowSize, + termios::{ControlMode, Termios, WindowSize}, tty_core::{TtyCore, TtyCoreData}, tty_driver::{TtyDriver, TtyDriverManager, TtyOperation}, tty_port::TtyInputByteResult, @@ -34,9 +35,13 @@ use crate::{ irqdesc::{IrqHandleFlags, IrqHandler, IrqReturn}, manage::irq_manager, tasklet::{tasklet_schedule, Tasklet, TaskletData}, - IrqNumber, + InterruptArch, IrqNumber, }, + filesystem::epoll::{event_poll::EventPoll, EPollEventType}, libs::{rwsem::RwSem, spinlock::SpinLock}, + process::ProcessManager, + sched::sched_yield, + time::{sleep::nanosleep, Duration, Instant, PosixTimeSpec}, }; use system_error::SystemError; @@ -45,9 +50,25 @@ use super::{Serial8250ISADevices, Serial8250ISADriver, Serial8250Manager, Serial static mut PIO_PORTS: [Option; 8] = [None, None, None, None, None, None, None, None]; -const SERIAL_8250_PIO_IRQ: IrqNumber = IrqNumber::new(IoApic::VECTOR_BASE as u32 + 4); +const SERIAL_8250_PIO_IRQS: [IrqNumber; 2] = [ + IrqNumber::new(IoApic::VECTOR_BASE as u32 + 3), + IrqNumber::new(IoApic::VECTOR_BASE as u32 + 4), +]; +const SERIAL_8250_IRQ_PASS_LIMIT: usize = 256; const SERIAL_8250_RX_IRQ_LIMIT: usize = 256; const SERIAL_8250_IER_RX_AVAILABLE: u8 = 0x01; +const SERIAL_8250_IER_TX_EMPTY: u8 = 0x02; +const SERIAL_8250_FCR_ENABLE_FIFO: u8 = 0x01; +const SERIAL_8250_FCR_CLEAR_XMIT: u8 = 0x04; +const SERIAL_8250_FCR_TRIGGER_14: u8 = 0xc0; +const SERIAL_8250_TX_QUEUE_SIZE: usize = 4096; +const SERIAL_8250_FIFO_SIZE: usize = 16; +const SERIAL_8250_TX_WAKEUP_CHARS: usize = 256; +const SERIAL_8250_EMERGENCY_TIMEOUT: Duration = Duration::from_millis(10); +const SERIAL_8250_CONSOLE_OWNER_NONE: usize = usize::MAX; +/// Linux tty_port_init() defaults closing_wait to 30 seconds. The software +/// queue and the physical transmitter share this single close-time budget. +const SERIAL_8250_CLOSE_WAIT: Duration = Duration::from_secs(30); lazy_static! { static ref SERIAL_8250_RX_RETRY_TASKLET: Arc = @@ -125,9 +146,15 @@ pub(super) fn serial8250_pio_port_early_init() -> Result<(), SystemError> { pub struct Serial8250PIOPort { iobase: Serial8250PortBase, baudrate: AtomicBaudRate, + frame_time_us: AtomicUsize, initialized: AtomicBool, + tx_fifo_size: AtomicUsize, rx_paused: AtomicBool, rx_state: SpinLock, + tx_state: SpinLock, + /// Serializes the DLAB register-alias window against runtime UART I/O. + hw_lock: SpinLock<()>, + console_owner: AtomicUsize, inner: RwSem, } @@ -136,6 +163,28 @@ struct Serial8250RxState { input_target: Option, ier: u8, irq_registered: bool, + receiver_enabled: bool, +} + +#[derive(Debug)] +struct Serial8250TxState { + queue: Option>, + tty: Weak, + console_active: bool, + flush_generation: usize, +} + +impl Serial8250TxState { + fn new() -> Self { + Self { + // The PIO ports are constructed before the heap is available. + // Allocate the process-context TX queue later during TTY install. + queue: None, + tty: Weak::new(), + console_active: false, + flush_generation: 0, + } + } } impl Serial8250RxState { @@ -144,6 +193,7 @@ impl Serial8250RxState { input_target: None, ier: 0, irq_registered: false, + receiver_enabled: true, } } } @@ -154,9 +204,17 @@ impl Serial8250PIOPort { let r = Self { iobase, baudrate: AtomicBaudRate::new(baudrate), + // The early console starts in the conventional 8N1 format. + frame_time_us: AtomicUsize::new( + 10_000_000usize.div_ceil(baudrate.data().max(1) as usize), + ), initialized: AtomicBool::new(false), + tx_fifo_size: AtomicUsize::new(1), rx_paused: AtomicBool::new(false), rx_state: SpinLock::new(Serial8250RxState::new()), + tx_state: SpinLock::new(Serial8250TxState::new()), + hw_lock: SpinLock::new(()), + console_owner: AtomicUsize::new(SERIAL_8250_CONSOLE_OWNER_NONE), inner: RwSem::new(Serial8250PIOPortInner::new()), }; @@ -181,6 +239,14 @@ impl Serial8250PIOPort { .unwrap(); // Set baud rate CurrentPortIOArch::out8(port + 2, 0xC7); // Enable FIFO, clear them, with 14-byte threshold + // IIR[7:6] == 11b is the 16550A FIFO-enabled indication. Older + // 8250/16450-compatible hardware only guarantees one THR byte. + let fifo_size = if CurrentPortIOArch::in8(port + 2) & 0xC0 == 0xC0 { + SERIAL_8250_FIFO_SIZE + } else { + 1 + }; + self.tx_fifo_size.store(fifo_size, Ordering::Release); CurrentPortIOArch::out8(port + 4, 0x08); // IRQs enabled, RTS/DSR clear (现代计算机上一般都不需要hardware flow control,因此不需要置位RTS/DSR) CurrentPortIOArch::out8(port + 4, 0x1E); // Set in loopback mode, test the serial chip CurrentPortIOArch::out8(port, 0xAE); // Test serial chip (send byte 0xAE and check if serial returns same byte) @@ -207,9 +273,10 @@ impl Serial8250PIOPort { } const fn check_baudrate(&self, baudrate: &BaudRate) -> Result<(), SystemError> { - // 错误的比特率 - if baudrate.data() > Self::SERIAL8250PIO_MAX_BAUD_RATE.data() - || Self::SERIAL8250PIO_MAX_BAUD_RATE.data() % baudrate.data() != 0 + let baud = baudrate.data(); + if baud == 0 + || baud > Self::SERIAL8250PIO_MAX_BAUD_RATE.data() + || Self::SERIAL8250PIO_MAX_BAUD_RATE.data().div_ceil(baud) > u16::MAX as u32 { return Err(SystemError::EINVAL); } @@ -226,27 +293,27 @@ impl Serial8250PIOPort { self.serial_in(5) & 0x20 != 0 } - /// 发送字节 - /// - /// ## 参数 - /// - /// - `s`:待发送的字节 - fn send_bytes(&self, s: &[u8]) { - while !self.is_transmit_empty() { - spin_loop(); - } + /// Both the transmitter FIFO and shift register are empty. + fn is_transmitter_idle(&self) -> bool { + self.serial_in(5) & 0x40 != 0 + } - for c in s { - self.serial_out(0, (*c).into()); - } + fn has_pending_interrupt(&self) -> bool { + let _guard = self.hw_lock.lock_irqsave(); + self.serial_in_raw(2) & 0x01 == 0 + } + + fn irq(&self) -> Option { + self.iobase.legacy_irq() } /// 读取一个字节,如果没有数据则返回None fn read_one_byte(&self) -> Option { - if !self.serial_received() { + let _guard = self.hw_lock.lock_irqsave(); + if self.serial_in_raw(5) & 1 == 0 { return None; } - return Some(self.serial_in(0) as u8); + Some(self.serial_in_raw(0) as u8) } fn set_input_target(&self, target: Option) { @@ -258,6 +325,390 @@ impl Serial8250PIOPort { self.set_rx_interrupt_enabled_locked(&mut rx_state, enabled); } + fn set_tx_interrupt_enabled(&self, enabled: bool) { + let mut rx_state = self.rx_state.lock_irqsave(); + if enabled { + rx_state.ier |= SERIAL_8250_IER_TX_EMPTY; + } else { + rx_state.ier &= !SERIAL_8250_IER_TX_EMPTY; + } + self.sync_ier_locked(&rx_state); + } + + fn enqueue_tx(&self, buf: &[u8]) -> usize { + let accepted = { + let mut tx_state = self.tx_state.lock_irqsave(); + let Some(queue) = tx_state.queue.as_mut() else { + return 0; + }; + let room = SERIAL_8250_TX_QUEUE_SIZE.saturating_sub(queue.len()); + let accepted = room.min(buf.len()); + queue.extend(&buf[..accepted]); + accepted + }; + if accepted != 0 { + self.pump_tx(); + } else if self.tx_room() == 0 { + self.set_tx_interrupt_enabled(true); + } + accepted + } + + fn tx_room(&self) -> usize { + self.tx_state + .lock_irqsave() + .queue + .as_ref() + .map(|queue| SERIAL_8250_TX_QUEUE_SIZE.saturating_sub(queue.len())) + .unwrap_or(0) + } + + fn tx_pending(&self) -> usize { + self.tx_state + .lock_irqsave() + .queue + .as_ref() + .map(VecDeque::len) + .unwrap_or(0) + } + + fn pump_tx(&self) { + let (sent, should_wake, tty) = { + let mut tx_state = self.tx_state.lock_irqsave(); + if tx_state.console_active { + return; + } + let tty = tx_state.tty.upgrade(); + let Some(queue) = tx_state.queue.as_mut() else { + return; + }; + let pending_before = queue.len(); + let mut sent = 0; + // THRE must be sampled after taking tx_state: both process and + // IRQ contexts can pump this port, and a pre-lock sample becomes + // stale as soon as another context fills the FIFO. + let _hw_guard = self.hw_lock.lock_irqsave(); + if self.serial_in_raw(5) & 0x20 != 0 { + while sent < self.tx_fifo_size.load(Ordering::Acquire) { + let Some(byte) = queue.pop_front() else { + break; + }; + self.serial_out_raw(0, byte.into()); + sent += 1; + } + } + drop(_hw_guard); + let pending = !queue.is_empty(); + let pending_after = queue.len(); + let should_wake = pending_before != 0 + && (pending_after == 0 + || (pending_before >= SERIAL_8250_TX_WAKEUP_CHARS + && pending_after < SERIAL_8250_TX_WAKEUP_CHARS)); + // Commit IER from the same queue snapshot before enqueue/clear can + // mutate it, otherwise a stale disable can strand new bytes. + self.set_tx_interrupt_enabled(pending); + (sent, should_wake, tty) + }; + + if sent != 0 && should_wake { + if let Some(tty) = tty { + tty.tty_wakeup(); + } + } + } + + fn clear_tx(&self) { + let mut tx_state = self.tx_state.lock_irqsave(); + if let Some(queue) = tx_state.queue.as_mut() { + queue.clear(); + } + tx_state.flush_generation = tx_state.flush_generation.wrapping_add(1); + self.set_tx_interrupt_enabled(false); + } + + fn abort_tx(&self) { + let mut tx_state = self.tx_state.lock_irqsave(); + if let Some(queue) = tx_state.queue.as_mut() { + queue.clear(); + } + tx_state.flush_generation = tx_state.flush_generation.wrapping_add(1); + self.set_tx_interrupt_enabled(false); + + // Linux's ordinary uart_flush_buffer() leaves bytes already accepted + // by a PIO 8250 alone. A close-time abort is different: Linux follows + // it with 8250 shutdown, which resets the hardware FIFOs before a + // later open. DragonOS has no separate shutdown callback yet, so do + // the transmit-only reset here without discarding pending RX data. + if self.tx_fifo_size.load(Ordering::Acquire) > 1 { + let _hw_guard = self.hw_lock.lock_irqsave(); + self.serial_out_raw( + 2, + (SERIAL_8250_FCR_ENABLE_FIFO + | SERIAL_8250_FCR_CLEAR_XMIT + | SERIAL_8250_FCR_TRIGGER_14) + .into(), + ); + } + } + + fn write_fifo_if_ready(&self, bytes: &[u8]) -> usize { + let _hw_guard = self.hw_lock.lock_irqsave(); + if self.serial_in_raw(5) & 0x20 == 0 { + return 0; + } + let count = bytes.len().min(self.tx_fifo_size.load(Ordering::Acquire)); + for byte in &bytes[..count] { + self.serial_out_raw(0, (*byte).into()); + } + count + } + + fn submit_runtime_output(&self, s: &[u8]) { + // This is the legacy, no-return-value console/debug interface: unlike + // the TTY write callback it cannot report a short write. Serialize it + // with the runtime queue, drain older TTY bytes first, then submit the + // whole message by polling. Console output is intentionally + // synchronous; ordinary userspace TTY output remains IRQ-driven. + let early_guard = self.tx_state.lock_irqsave(); + if early_guard.queue.is_none() { + // Timers and the interrupt pipeline are not available yet. + for byte in s { + while !self.is_transmit_empty() { + spin_loop(); + } + self.serial_out(0, (*byte).into()); + } + return; + } + drop(early_guard); + + // A PCB address is stable across migration and is also visible from a + // nested IRQ/exception on behalf of that task. Keep the Arc alive so + // the token cannot be reused while this submission owns the UART. + let current = ProcessManager::current_pcb(); + let owner_token = Arc::as_ptr(¤t) as usize; + if current.preempt_count() != 0 || !CurrentIrqArch::is_irq_enabled() { + match self.console_owner.compare_exchange( + SERIAL_8250_CONSOLE_OWNER_NONE, + owner_token, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + self.submit_emergency_output(s); + self.console_owner + .store(SERIAL_8250_CONSOLE_OWNER_NONE, Ordering::Release); + } + Err(owner) if owner == owner_token => self.submit_emergency_output(s), + Err(_) => {} + } + return; + } + loop { + match self.console_owner.compare_exchange( + SERIAL_8250_CONSOLE_OWNER_NONE, + owner_token, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => break, + Err(owner) if owner == owner_token => { + self.submit_emergency_output(s); + return; + } + Err(_) => { + // Process-context callers may wait without pinning a CPU. + if nanosleep(PosixTimeSpec::new(0, 1_000_000)).is_err() { + return; + } + } + } + } + let (mut older_remaining, flush_generation, tty) = { + let mut tx_state = self.tx_state.lock_irqsave(); + tx_state.console_active = true; + let pending = tx_state.queue.as_ref().map(VecDeque::len).unwrap_or(0); + let flush_generation = tx_state.flush_generation; + let tty = tx_state.tty.upgrade(); + self.set_tx_interrupt_enabled(false); + (pending, flush_generation, tty) + }; + let timeout = self.tx_batch_timeout(); + let mut healthy = true; + + while older_remaining != 0 { + if !self.wait_for_thre(timeout) { + healthy = false; + break; + } + let mut tx_state = self.tx_state.lock_irqsave(); + if tx_state.flush_generation != flush_generation { + older_remaining = 0; + break; + } + let Some(queue) = tx_state.queue.as_mut() else { + older_remaining = 0; + break; + }; + let count = older_remaining + .min(self.tx_fifo_size.load(Ordering::Acquire)) + .min(queue.len()); + if count == 0 { + older_remaining = 0; + break; + } + let _hw_guard = self.hw_lock.lock_irqsave(); + let sent = if self.serial_in_raw(5) & 0x20 == 0 { + 0 + } else { + for _ in 0..count { + self.serial_out_raw(0, queue.pop_front().unwrap().into()); + } + count + }; + drop(_hw_guard); + older_remaining -= sent; + if sent == 0 { + drop(tx_state); + continue; + } + } + + let mut offset = 0; + while healthy && offset < s.len() { + if !self.wait_for_thre(timeout) { + healthy = false; + break; + } + let sent = self.write_fifo_if_ready(&s[offset..]); + if sent == 0 { + continue; + } + offset += sent; + } + + { + let mut tx_state = self.tx_state.lock_irqsave(); + tx_state.console_active = false; + let pending = tx_state + .queue + .as_ref() + .map(|queue| !queue.is_empty()) + .unwrap_or(false); + self.set_tx_interrupt_enabled(pending); + } + if older_remaining == 0 { + if let Some(tty) = tty { + tty.tty_wakeup(); + } + } + if healthy { + self.pump_tx(); + } + self.console_owner + .store(SERIAL_8250_CONSOLE_OWNER_NONE, Ordering::Release); + drop(current); + } + + fn submit_emergency_output(&self, s: &[u8]) { + let Ok(mut tx_state) = self.tx_state.try_lock_irqsave() else { + return; + }; + let Ok(mut rx_state) = self.rx_state.try_lock_irqsave() else { + return; + }; + let Ok(_hw_guard) = self.hw_lock.try_lock_irqsave() else { + return; + }; + + let inherited_console_active = tx_state.console_active; + tx_state.console_active = true; + rx_state.ier &= !SERIAL_8250_IER_TX_EMPTY; + self.sync_ier_hw_locked(&rx_state); + + // Emergency output may run with interrupts or preemption disabled. + // Keep this independent of the configured baud rate: at B50 the + // ordinary FIFO-sized timeout would otherwise grow to several + // seconds. Linux's 8250 console likewise bounds an LSR wait to 10 ms. + let deadline = Instant::now() + SERIAL_8250_EMERGENCY_TIMEOUT; + let mut ready = true; + while self.serial_in_raw(5) & 0x20 == 0 { + if Instant::now() >= deadline { + ready = false; + break; + } + spin_loop(); + } + // Atomic diagnostics get one bounded FIFO batch. Longer output is + // intentionally truncated rather than extending IRQ/exception time. + if ready { + let count = s.len().min(self.tx_fifo_size.load(Ordering::Acquire)); + for byte in &s[..count] { + self.serial_out_raw(0, (*byte).into()); + } + } + + tx_state.console_active = inherited_console_active; + let pending = tx_state + .queue + .as_ref() + .map(|queue| !queue.is_empty()) + .unwrap_or(false); + if pending && !inherited_console_active { + rx_state.ier |= SERIAL_8250_IER_TX_EMPTY; + } else { + rx_state.ier &= !SERIAL_8250_IER_TX_EMPTY; + } + self.sync_ier_hw_locked(&rx_state); + } + + fn tx_batch_timeout(&self) -> Duration { + let frame_time_us = self.frame_time_us.load(Ordering::Acquire) as u64; + Duration::from_micros( + (frame_time_us * self.tx_fifo_size.load(Ordering::Acquire) as u64 * 2).max(2_000), + ) + } + + fn wait_for_thre(&self, timeout: Duration) -> bool { + let deadline = Instant::now() + timeout; + while !self.is_transmit_empty() { + if Instant::now() >= deadline { + return false; + } + spin_loop(); + } + true + } + + fn wait_for_tx_queue_empty_until( + &self, + tty: &TtyCoreData, + deadline: Instant, + ) -> Result { + let remaining = deadline.saturating_sub(Instant::now()); + if remaining == Duration::ZERO { + return Ok(self.tx_pending() == 0); + } + let events = (EPollEventType::EPOLLOUT | EPollEventType::EPOLLWRNORM).bits() as u64; + tty.write_wq() + .wait_event_interruptible_timeout(events, || self.tx_pending() == 0, remaining) + .map(|_| true) + } + + fn wait_for_transmitter_idle_until(&self, deadline: Instant) -> Result { + let poll_interval_us = + (self.frame_time_us.load(Ordering::Acquire) as u64).clamp(1_000, 100_000); + while !self.is_transmitter_idle() { + let remaining_us = deadline.saturating_sub(Instant::now()).total_micros(); + if remaining_us == 0 { + return Ok(false); + } + let sleep_us = poll_interval_us.min(remaining_us); + nanosleep(PosixTimeSpec::new(0, sleep_us as i64 * 1_000))?; + } + Ok(true) + } + fn set_rx_interrupt_enabled_locked(&self, rx_state: &mut Serial8250RxState, enabled: bool) { if enabled { rx_state.ier |= SERIAL_8250_IER_RX_AVAILABLE; @@ -268,12 +719,18 @@ impl Serial8250PIOPort { } fn sync_ier_locked(&self, rx_state: &Serial8250RxState) { + let _hw_guard = self.hw_lock.lock_irqsave(); + self.sync_ier_hw_locked(rx_state); + } + + /// Synchronize IER while the caller holds `hw_lock`. + fn sync_ier_hw_locked(&self, rx_state: &Serial8250RxState) { let ier = if rx_state.irq_registered { rx_state.ier } else { 0 }; - self.serial_out(1, ier.into()); + self.serial_out_raw(1, ier.into()); } fn mark_rx_irq_registered(&self) { @@ -294,18 +751,29 @@ impl Serial8250PIOPort { fn pump_rx(&self) -> Result<(), SystemError> { let mut rx_state = self.rx_state.lock_irqsave(); - let Some(target) = rx_state.input_target.as_ref() else { + let Some(target) = rx_state.input_target.clone() else { self.rx_paused.store(false, Ordering::Release); self.set_rx_interrupt_enabled_locked(&mut rx_state, false); return Ok(()); }; + if !rx_state.receiver_enabled { + for _ in 0..SERIAL_8250_RX_IRQ_LIMIT { + if self.read_one_byte().is_none() { + break; + } + } + self.rx_paused.store(false, Ordering::Release); + self.set_rx_interrupt_enabled_locked(&mut rx_state, true); + return Ok(()); + } + // Linux serial8250_rx_chars also keeps a 256-byte bound to avoid // unbounded RX IRQ/softirq CPU occupation. let mut received = 0; for _ in 0..SERIAL_8250_RX_IRQ_LIMIT { let mut producer = || self.read_one_byte(); - match enqueue_tty_rx_byte_to_target_from_irq(target, &mut producer) { + match enqueue_tty_rx_byte_to_target_from_irq(&target, &mut producer) { TtyInputByteResult::Enqueued => { received += 1; self.rx_paused.store(false, Ordering::Release); @@ -326,6 +794,182 @@ impl Serial8250PIOPort { } Ok(()) } + + fn set_tx_tty(&self, tty: Weak) { + let mut tx_state = self.tx_state.lock_irqsave(); + if tx_state.queue.is_none() { + tx_state.queue = Some(VecDeque::with_capacity(SERIAL_8250_TX_QUEUE_SIZE)); + } + tx_state.tty = tty; + } + + fn line_control(control_mode: ControlMode) -> u8 { + let mut lcr = match control_mode.intersection(ControlMode::CSIZE) { + ControlMode::CS5 => 0, + ControlMode::CS6 => 1, + ControlMode::CS7 => 2, + _ => 3, + }; + if control_mode.contains(ControlMode::CSTOPB) { + lcr |= 1 << 2; + } + if control_mode.contains(ControlMode::PARENB) { + lcr |= 1 << 3; + if !control_mode.contains(ControlMode::PARODD) { + lcr |= 1 << 4; + } + } + lcr + } + + fn frame_time_us(control_mode: ControlMode, baudrate: BaudRate) -> usize { + let data_bits = match control_mode.intersection(ControlMode::CSIZE) { + ControlMode::CS5 => 5, + ControlMode::CS6 => 6, + ControlMode::CS7 => 7, + _ => 8, + }; + let parity_bits = usize::from(control_mode.contains(ControlMode::PARENB)); + let stop_bits = if control_mode.contains(ControlMode::CSTOPB) { + 2 + } else { + 1 + }; + let frame_bits = 1 + data_bits + parity_bits + stop_bits; + (frame_bits * 1_000_000).div_ceil(baudrate.data().max(1) as usize) + } + + fn baud_control_mode(baud: u32) -> Option { + Some(match baud { + 0 => ControlMode::B0, + 50 => ControlMode::B50, + 75 => ControlMode::B75, + 110 => ControlMode::B110, + 134 => ControlMode::B134, + 150 => ControlMode::B150, + 200 => ControlMode::B200, + 300 => ControlMode::B300, + 600 => ControlMode::B600, + 1200 => ControlMode::B1200, + 1800 => ControlMode::B1800, + 2400 => ControlMode::B2400, + 4800 => ControlMode::B4800, + 9600 => ControlMode::B9600, + 19200 => ControlMode::B19200, + 38400 => ControlMode::B38400, + 57600 => ControlMode::B57600, + 115200 => ControlMode::B115200, + _ => return None, + }) + } + + fn encode_baud_rate(termios: &mut Termios, baud: u32) { + let Some(flag) = Self::baud_control_mode(baud) else { + return; + }; + let explicit_input_baud = termios.control_mode.intersects(ControlMode::CIBAUD); + termios + .control_mode + .remove(ControlMode::CBAUD | ControlMode::CIBAUD); + termios.control_mode.insert(flag); + if explicit_input_baud { + termios + .control_mode + .insert(ControlMode::from_bits_truncate(flag.bits() << 16)); + } + termios.input_speed = baud; + termios.output_speed = baud; + } + + /// Apply the 8250 line settings as one register transaction. + /// + /// DLAB aliases offsets 0/1 with THR/RBR/IER. The established lock order + /// is tx_state -> rx_state -> hw_lock, matching the TX and console paths. + fn apply_line_settings( + &self, + control_mode: ControlMode, + requested_baud: u32, + fallback_baud: u32, + ) -> Result { + let visible_baud = if requested_baud == 0 { + 0 + } else if requested_baud <= Self::SERIAL8250PIO_MAX_BAUD_RATE.data() { + requested_baud + } else { + if fallback_baud <= Self::SERIAL8250PIO_MAX_BAUD_RATE.data() { + fallback_baud + } else { + Self::SERIAL8250PIO_MAX_BAUD_RATE.data() + } + }; + // Linux programs a safe divisor while B0 controls hangup via MCR. + let hardware_baud = BaudRate::new(if visible_baud == 0 { + 9600 + } else { + visible_baud + }); + self.check_baudrate(&hardware_baud)?; + + // The runtime console releases tx_state while polling but keeps + // console_active set for the whole transaction. Emergency output + // holds a fully try-locked tx_state/rx_state/hw_lock tuple instead. + let _tx_state = loop { + let guard = self.tx_state.lock_irqsave(); + if !guard.console_active { + break guard; + } + drop(guard); + sched_yield(); + }; + let mut rx_state = self.rx_state.lock_irqsave(); + rx_state.receiver_enabled = control_mode.contains(ControlMode::CREAD); + let _hw_guard = self.hw_lock.lock_irqsave(); + let port = self.iobase as u16; + let lcr = Self::line_control(control_mode); + + unsafe { + // Stop UART interrupts before offset 1 becomes DLM. + CurrentPortIOArch::out8(port + 1, 0); + let divisor = self.divisor(hardware_baud).0; + CurrentPortIOArch::out8(port + 3, lcr | 0x80); + CurrentPortIOArch::out8(port, (divisor & 0xff) as u8); + CurrentPortIOArch::out8(port + 1, ((divisor >> 8) & 0xff) as u8); + CurrentPortIOArch::out8(port + 3, lcr); + + // Only B0 transitions change DTR/RTS; ordinary format changes + // preserve independently managed modem-control state. + let mcr = CurrentPortIOArch::in8(port + 4); + let mcr = if fallback_baud != 0 && visible_baud == 0 { + mcr & !0x03 + } else if fallback_baud == 0 && visible_baud != 0 { + mcr | 0x03 + } else { + mcr + }; + CurrentPortIOArch::out8(port + 4, mcr); + let ier = if rx_state.irq_registered { + rx_state.ier + } else { + 0 + }; + CurrentPortIOArch::out8(port + 1, ier); + } + + self.baudrate.store(hardware_baud, Ordering::Release); + self.frame_time_us.store( + Self::frame_time_us(control_mode, hardware_baud), + Ordering::Release, + ); + Ok(visible_baud) + } + + fn serial_in_raw(&self, offset: u32) -> u32 { + unsafe { CurrentPortIOArch::in8(self.iobase as u16 + offset as u16).into() } + } + + fn serial_out_raw(&self, offset: u32, value: u32) { + unsafe { CurrentPortIOArch::out8(self.iobase as u16 + offset as u16, value as u8) } + } } impl Serial8250Port for Serial8250PIOPort { @@ -340,16 +984,18 @@ impl Serial8250Port for Serial8250PIOPort { impl UartPort for Serial8250PIOPort { fn serial_in(&self, offset: u32) -> u32 { - unsafe { CurrentPortIOArch::in8(self.iobase as u16 + offset as u16).into() } + let _guard = self.hw_lock.lock_irqsave(); + self.serial_in_raw(offset) } fn serial_out(&self, offset: u32, value: u32) { // warning: pio的串口只能写入8位,因此这里丢弃高24位 - unsafe { CurrentPortIOArch::out8(self.iobase as u16 + offset as u16, value as u8) } + let _guard = self.hw_lock.lock_irqsave(); + self.serial_out_raw(offset, value) } fn divisor(&self, baud: BaudRate) -> (u32, DivisorFraction) { - let divisor = Self::SERIAL8250PIO_MAX_BAUD_RATE.data() / baud.data(); + let divisor = (Self::SERIAL8250PIO_MAX_BAUD_RATE.data() + baud.data() / 2) / baud.data(); return (divisor, DivisorFraction::new(0)); } @@ -385,7 +1031,9 @@ impl UartPort for Serial8250PIOPort { } fn handle_irq(&self) -> Result<(), SystemError> { - self.pump_rx() + self.pump_rx()?; + self.pump_tx(); + Ok(()) } fn iobase(&self) -> Option { @@ -433,10 +1081,23 @@ pub enum Serial8250PortBase { COM8 = 0x4e8, } +impl Serial8250PortBase { + /// Linux's fixed x86 legacy table defines IRQ resources only for the + /// first four ISA UARTs. Additional base addresses require platform + /// firmware or explicit configuration and must not guess an IRQ. + const fn legacy_irq(self) -> Option { + match self { + Self::COM1 | Self::COM3 => Some(IrqNumber::new(IoApic::VECTOR_BASE as u32 + 4)), + Self::COM2 | Self::COM4 => Some(IrqNumber::new(IoApic::VECTOR_BASE as u32 + 3)), + Self::COM5 | Self::COM6 | Self::COM7 | Self::COM8 => None, + } + } +} + /// 临时函数,用于向COM1发送数据 pub fn send_to_default_serial8250_pio_port(s: &[u8]) { if let Some(port) = unsafe { PIO_PORTS[0].as_ref() } { - port.send_bytes(s); + port.submit_runtime_output(s); } } @@ -475,13 +1136,65 @@ impl TtyOperation for Serial8250PIOTtyDriverInner { return Err(SystemError::ENODEV); } let pio_port = unsafe { PIO_PORTS[index].as_ref() }.ok_or(SystemError::ENODEV)?; - pio_port.send_bytes(&buf[..nr]); + Ok(pio_port.enqueue_tx(&buf[..nr])) + } + + fn write_room(&self, tty: &TtyCoreData) -> usize { + let index = tty.index(); + unsafe { PIO_PORTS.get(index).and_then(Option::as_ref) } + .map(Serial8250PIOPort::tx_room) + .unwrap_or(0) + } - Ok(nr) + fn chars_in_buffer(&self, tty: &TtyCoreData) -> usize { + let index = tty.index(); + unsafe { PIO_PORTS.get(index).and_then(Option::as_ref) } + .map(Serial8250PIOPort::tx_pending) + .unwrap_or(0) } fn flush_chars(&self, _tty: &TtyCoreData) {} + fn wait_until_sent(&self, tty: &TtyCoreData) -> Result<(), SystemError> { + let index = tty.index(); + if index >= unsafe { PIO_PORTS.len() } { + return Err(SystemError::ENODEV); + } + let port = unsafe { PIO_PORTS[index].as_ref() }.ok_or(SystemError::ENODEV)?; + // Linux bounds uart_wait_until_sent to twice the FIFO transmission + // time when hardware flow control is disabled. + let deadline = Instant::now() + port.tx_batch_timeout(); + port.wait_for_transmitter_idle_until(deadline)?; + Ok(()) + } + + fn set_termios(&self, tty: Arc, old_termios: Termios) -> Result<(), SystemError> { + let index = tty.core().index(); + let port = + unsafe { PIO_PORTS.get(index).and_then(Option::as_ref) }.ok_or(SystemError::ENODEV)?; + let termios = *tty.core().termios(); + let applied_baud = port.apply_line_settings( + termios.control_mode, + termios.output_speed, + old_termios.output_speed, + )?; + if !termios.control_mode.contains(ControlMode::CREAD) { + // A full TTY input queue may have paused RX and masked its IRQ. + // Drain already-received bytes now so CREAD-off data cannot be + // delivered after the receiver is enabled again. + port.pump_rx()?; + } + + // Linux falls back to the previous supported baud when a request is + // outside the hardware range. Keep the cached hardware fields aligned + // with the divisor selected above. B0 intentionally remains zero. + if applied_baud != termios.output_speed { + let mut current = tty.core().termios_write(); + Serial8250PIOPort::encode_baud_rate(&mut current, applied_baud); + } + Ok(()) + } + fn put_char(&self, tty: &TtyCoreData, ch: u8) -> Result<(), SystemError> { self.write(tty, &[ch], 1).map(|_| ()) } @@ -491,6 +1204,22 @@ impl TtyOperation for Serial8250PIOTtyDriverInner { } fn close(&self, tty: Arc) -> Result<(), SystemError> { + let index = tty.core().index(); + let port = + unsafe { PIO_PORTS.get(index).and_then(Option::as_ref) }.ok_or(SystemError::ENODEV)?; + let close_deadline = Instant::now() + SERIAL_8250_CLOSE_WAIT; + let queue_drained = port + .wait_for_tx_queue_empty_until(tty.core(), close_deadline) + .unwrap_or(false); + let physical_deadline = close_deadline.min(Instant::now() + port.tx_batch_timeout()); + let transmitter_idle = queue_drained + && port + .wait_for_transmitter_idle_until(physical_deadline) + .unwrap_or(false); + if !transmitter_idle { + // A failed/stalled UART must not leak bytes into the next open. + port.abort_tx(); + } tty.ldisc().flush_buffer(tty.clone())?; Ok(()) } @@ -519,12 +1248,14 @@ impl TtyOperation for Serial8250PIOTtyDriverInner { let vc = VirtConsole::new(Some(vc_data)); let port = unsafe { PIO_PORTS[tty_index].as_ref() }.ok_or(SystemError::ENODEV)?; let vc_index = vc_manager().alloc(vc.clone()).ok_or(SystemError::EBUSY)?; - self.do_install(driver, tty, vc.clone()).inspect_err(|_| { - vc_manager().free(vc_index); - port.set_input_target(None); - port.rx_paused.store(false, Ordering::Release); - port.set_rx_interrupt_enabled(false); - })?; + self.do_install(driver, tty.clone(), vc.clone()) + .inspect_err(|_| { + vc_manager().free(vc_index); + port.set_input_target(None); + port.rx_paused.store(false, Ordering::Release); + port.set_rx_interrupt_enabled(false); + })?; + port.set_tx_tty(Arc::downgrade(&tty)); let irq_registered = { let mut rx_state = port.rx_state.lock_irqsave(); rx_state.input_target = Some(TtyInputTarget::new(vc_index, vc.port())); @@ -537,6 +1268,21 @@ impl TtyOperation for Serial8250PIOTtyDriverInner { Ok(()) } + + fn flush_buffer(&self, tty: &TtyCoreData) -> Result<(), SystemError> { + let index = tty.index(); + let port = + unsafe { PIO_PORTS.get(index).and_then(Option::as_ref) }.ok_or(SystemError::ENODEV)?; + port.clear_tx(); + // Match Linux uart_flush_buffer(): releasing the software TX queue is + // a write-readiness transition. Do not call tty_wakeup() here because + // signal-character flush can enter with the N_TTY data lock held, and + // its synchronous write_wakeup callback would re-enter that lock. + tty.write_wq().wakeup_all(); + let events = EPollEventType::EPOLLOUT | EPollEventType::EPOLLWRNORM; + let _ = EventPoll::wakeup_epoll(tty.epitems(), events); + Ok(()) + } } pub(super) fn serial_8250_pio_register_tty_devices() -> Result<(), SystemError> { @@ -546,40 +1292,89 @@ pub(super) fn serial_8250_pio_register_tty_devices() -> Result<(), SystemError> )) .ok_or(SystemError::ENODEV)?; + let mut registered_irqs = [false; SERIAL_8250_PIO_IRQS.len()]; + for (slot, irq) in SERIAL_8250_PIO_IRQS.iter().copied().enumerate() { + let has_port = unsafe { PIO_PORTS.iter() } + .flatten() + .any(|port| port.irq() == Some(irq)); + if !has_port { + continue; + } + match irq_manager().request_irq( + irq, + "serial8250_pio".to_string(), + &Serial8250IrqHandler, + IrqHandleFlags::IRQF_SHARED | IrqHandleFlags::IRQF_TRIGGER_RISING, + DeviceId::new(Some("serial8250_pio"), None), + ) { + Ok(()) => registered_irqs[slot] = true, + Err(err) => { + log::error!("failed to request serial 8250 PIO irq {:?}: {:?}", irq, err); + } + } + } + + let irq_is_registered = |irq: IrqNumber| { + SERIAL_8250_PIO_IRQS + .iter() + .position(|candidate| *candidate == irq) + .is_some_and(|slot| registered_irqs[slot]) + }; + let mut registered_ports = 0; for (i, port) in unsafe { PIO_PORTS.iter() }.enumerate() { if let Some(port) = port { - let core = driver.init_tty_device(Some(i)).inspect_err(|_| { - log::error!( - "failed to init tty device for serial 8250 pio port {}, port iobase: {:?}", - i, + let Some(irq) = port.irq() else { + log::warn!( + "serial 8250 PIO port {:?} has no enumerated IRQ resource; skipping tty registration", port.iobase ); - })?; - core.resize( core.clone(), WindowSize::DEFAULT) - .inspect_err(|_| { + continue; + }; + if !irq_is_registered(irq) { + log::error!( + "serial 8250 PIO port {:?} cannot be registered without IRQ {:?}", + port.iobase, + irq + ); + continue; + } + let core = match driver.init_tty_device(Some(i)) { + Ok(core) => core, + Err(err) => { log::error!( - "failed to resize tty device for serial 8250 pio port {}, port iobase: {:?}", + "failed to init tty device for serial 8250 pio port {}, port iobase: {:?}: {:?}", i, - port.iobase + port.iobase, + err ); - })?; + continue; + } + }; + if let Err(err) = core.resize(core.clone(), WindowSize::DEFAULT) { + // The TTY already exists and cannot currently be rolled back. + // Keep its IRQ route usable and retain the driver's default + // size instead of abandoning this and every later port. + log::error!( + "failed to resize tty device for serial 8250 pio port {}, port iobase: {:?}: {:?}", + i, + port.iobase, + err + ); + } + port.mark_rx_irq_registered(); + registered_ports += 1; } } - irq_manager() - .request_irq( - SERIAL_8250_PIO_IRQ, - "serial8250_pio".to_string(), - &Serial8250IrqHandler, - IrqHandleFlags::IRQF_SHARED | IrqHandleFlags::IRQF_TRIGGER_RISING, - Some(DeviceId::new(Some("serial8250_pio"), None).unwrap()), - ) - .inspect_err(|e| { - log::error!("failed to request irq for serial 8250 pio: {:?}", e); - })?; - - for port in unsafe { PIO_PORTS.iter() }.flatten() { - port.mark_rx_irq_registered(); + if registered_ports == 0 && unsafe { PIO_PORTS.iter().any(Option::is_some) } { + // free_irq() is not implemented yet. Once any request succeeded, + // report this one-shot boot registration as complete instead of + // inviting a retry that would duplicate the installed IRQ action. + if registered_irqs.iter().any(|registered| *registered) { + log::error!("no serial 8250 PIO TTY could be initialized"); + return Ok(()); + } + return Err(SystemError::ENODEV); } retry_serial8250_pio_input(); @@ -592,14 +1387,40 @@ struct Serial8250IrqHandler; impl IrqHandler for Serial8250IrqHandler { fn handle( &self, - _irq: IrqNumber, + irq: IrqNumber, _static_data: Option<&dyn IrqHandlerData>, _dynamic_data: Option>, ) -> Result { - for port in unsafe { PIO_PORTS.iter() }.flatten() { - port.handle_irq()?; + let mut handled = false; + for _ in 0..SERIAL_8250_IRQ_PASS_LIMIT { + let mut pending_in_pass = false; + for port in unsafe { PIO_PORTS.iter() } + .flatten() + .filter(|port| port.irq() == Some(irq)) + { + if !port.has_pending_interrupt() { + continue; + } + pending_in_pass = true; + handled = true; + if let Err(err) = port.handle_irq() { + log::error!( + "failed to service serial 8250 PIO port {:?} on irq {:?}: {:?}", + port.iobase, + irq, + err + ); + } + } + if !pending_in_pass { + break; + } } - Ok(IrqReturn::Handled) + Ok(if handled { + IrqReturn::Handled + } else { + IrqReturn::NotHandled + }) } } diff --git a/kernel/src/driver/tty/pty/mod.rs b/kernel/src/driver/tty/pty/mod.rs index 6ad113e5b..07b48fb33 100644 --- a/kernel/src/driver/tty/pty/mod.rs +++ b/kernel/src/driver/tty/pty/mod.rs @@ -10,7 +10,7 @@ use crate::{ syscall::user_access::{UserBufferReader, UserBufferWriter}, }; -use self::unix98pty::{Unix98PtyDriverInner, NR_UNIX98_PTY_MAX}; +use self::unix98pty::{pty_drain_workqueue_init, Unix98PtyDriverInner, NR_UNIX98_PTY_MAX}; use super::{ termios::{ControlMode, InputMode, LocalMode, OutputMode, TTY_STD_TERMIOS}, @@ -177,6 +177,7 @@ impl PtyCommon { #[unified_init(INITCALL_DEVICE)] #[inline(never)] pub fn pty_init() -> Result<(), SystemError> { + pty_drain_workqueue_init(); let mut ptm_driver = TtyDriver::new( NR_UNIX98_PTY_MAX, "ptm", diff --git a/kernel/src/driver/tty/pty/unix98pty.rs b/kernel/src/driver/tty/pty/unix98pty.rs index 39ea96a50..d930eaaf9 100644 --- a/kernel/src/driver/tty/pty/unix98pty.rs +++ b/kernel/src/driver/tty/pty/unix98pty.rs @@ -15,8 +15,11 @@ use crate::{ termios::{ControlCharIndex, ControlMode, InputMode, LocalMode, Termios}, tty_core::{TtyCore, TtyCoreData, TtyFlag, TtyIoctlCmd, TtyPacketStatus}, tty_device::{TtyDevice, TtyFilePrivateData}, - tty_driver::{TtyDriver, TtyDriverPrivateData, TtyDriverSubType, TtyOperation}, + tty_driver::{ + TtyCorePrivateField, TtyDriver, TtyDriverPrivateData, TtyDriverSubType, TtyOperation, + }, }, + exception::workqueue::{Work, WorkQueue}, filesystem::{ devpts::DevPtsFs, epoll::{event_poll::EventPoll, EPollEventType}, @@ -34,6 +37,14 @@ use crate::{ syscall::user_access::UserBufferWriter, }; +lazy_static! { + static ref PTY_DRAIN_WQ: Arc = WorkQueue::new("pty-drain"); +} + +pub(super) fn pty_drain_workqueue_init() { + lazy_static::initialize(&PTY_DRAIN_WQ); +} + use super::{ptm_driver, pts_driver, PtyCommon}; pub const NR_UNIX98_PTY_MAX: u32 = 128; @@ -73,6 +84,8 @@ struct PtyDevPtsLink { slave_to_master_draining: AtomicBool, master_to_slave_drain_requested: AtomicBool, slave_to_master_drain_requested: AtomicBool, + master_to_slave_drain_scheduled: AtomicBool, + slave_to_master_drain_scheduled: AtomicBool, master_to_slave_discarding: AtomicBool, slave_to_master_discarding: AtomicBool, master_to_slave_flushing: AtomicBool, @@ -98,6 +111,7 @@ struct DrainResult { delivered: usize, freed_backlog: usize, still_pending: bool, + yielded: bool, } #[derive(Debug)] @@ -213,6 +227,8 @@ impl PtyDevPtsLink { slave_to_master_draining: AtomicBool::new(false), master_to_slave_drain_requested: AtomicBool::new(false), slave_to_master_drain_requested: AtomicBool::new(false), + master_to_slave_drain_scheduled: AtomicBool::new(false), + slave_to_master_drain_scheduled: AtomicBool::new(false), master_to_slave_discarding: AtomicBool::new(false), slave_to_master_discarding: AtomicBool::new(false), master_to_slave_flushing: AtomicBool::new(false), @@ -228,6 +244,14 @@ impl PtyDevPtsLink { } } + fn drain_scheduled_for_source(&self, subtype: TtyDriverSubType) -> Option<&AtomicBool> { + match subtype { + TtyDriverSubType::PtyMaster => Some(&self.master_to_slave_drain_scheduled), + TtyDriverSubType::PtySlave => Some(&self.slave_to_master_drain_scheduled), + _ => None, + } + } + fn state_flags_for_source( &self, subtype: TtyDriverSubType, @@ -372,6 +396,7 @@ impl PtyDevPtsLink { fn write_to_peer( &self, + owner: Arc, subtype: TtyDriverSubType, to: Arc, buf: &[u8], @@ -398,10 +423,18 @@ impl PtyDevPtsLink { break queue_guard.push_slice(&buf[..nr]); }; + // Preserve the existing in-order fast path when the peer termios state + // is stable. If a peer termios writer is active, never block while the + // source endpoint may hold its own termios/output locks: defer delivery + // to the workqueue and break the cross-endpoint ABBA dependency. + let Some(_peer_termios_guard) = to.core().termios_try_read_lock() else { + Self::schedule_peer_drain(owner, subtype, to); + return Ok(accepted); + }; + if accepted == 0 { - let result = self.drain_to_peer(subtype, to.clone())?; + let result = self.drain_to_peer_termios_locked(subtype, to.clone(), usize::MAX)?; if result.freed_backlog != 0 { - // Space became available in this PTY direction. Self::wake_source_writer(&to); } accepted = loop { @@ -420,7 +453,7 @@ impl PtyDevPtsLink { } if accepted != 0 { - let result = self.drain_to_peer(subtype, to.clone())?; + let result = self.drain_to_peer_termios_locked(subtype, to.clone(), usize::MAX)?; if result.freed_backlog != 0 { Self::wake_source_writer(&to); } @@ -428,10 +461,78 @@ impl PtyDevPtsLink { Ok(accepted) } + fn schedule_peer_drain( + owner: Arc, + subtype: TtyDriverSubType, + to: Arc, + ) { + let Some(hook) = owner.as_any().downcast_ref::() else { + return; + }; + let Some(scheduled) = hook.drain_scheduled_for_source(subtype) else { + return; + }; + if scheduled + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return; + } + + PTY_DRAIN_WQ.enqueue(Work::new(move || { + let Some(hook) = owner.as_any().downcast_ref::() else { + return; + }; + let Some(scheduled) = hook.drain_scheduled_for_source(subtype) else { + return; + }; + + let result = hook.drain_to_peer_budgeted(subtype, to.clone(), 16); + if let Ok(result) = result.as_ref() { + if result.freed_backlog != 0 { + Self::wake_source_writer(&to); + } + } + + scheduled.store(false, Ordering::Release); + let should_retry = result + .as_ref() + .map(|result| result.yielded || !result.still_pending) + .unwrap_or(false) + && hook + .queue_for_source(subtype) + .map(|queue| !queue.lock_irqsave().is_empty()) + .unwrap_or(false); + if should_retry { + Self::schedule_peer_drain(owner.clone(), subtype, to.clone()); + } + })); + } + fn drain_to_peer( &self, subtype: TtyDriverSubType, to: Arc, + ) -> Result { + let _termios_guard = to.core().termios_read_lock(); + self.drain_to_peer_termios_locked(subtype, to.clone(), usize::MAX) + } + + fn drain_to_peer_budgeted( + &self, + subtype: TtyDriverSubType, + to: Arc, + max_chunks: usize, + ) -> Result { + let _termios_guard = to.core().termios_read_lock(); + self.drain_to_peer_termios_locked(subtype, to.clone(), max_chunks) + } + + fn drain_to_peer_termios_locked( + &self, + subtype: TtyDriverSubType, + to: Arc, + max_chunks: usize, ) -> Result { let Some(queue) = self.queue_for_source(subtype) else { return Err(SystemError::ENODEV); @@ -456,6 +557,7 @@ impl PtyDevPtsLink { return Ok(result); } + let mut chunks = 0; loop { if flushing.load(Ordering::Acquire) { let _ = self.discard_requested_prefix(queue); @@ -512,20 +614,18 @@ impl PtyDevPtsLink { continue; } - let delivered = - match to - .core() - .port() - .unwrap() - .receive_buf(&chunk[..chunk_len], &[], chunk_len) - { - Ok(delivered) => delivered, - Err(err) => { - let _ = self.discard_requested_prefix(queue); - draining.store(false, Ordering::Release); - return Err(err); - } - }; + let delivered = match to.core().port().unwrap().receive_buf_termios_locked( + &chunk[..chunk_len], + &[], + chunk_len, + ) { + Ok(delivered) => delivered, + Err(err) => { + let _ = self.discard_requested_prefix(queue); + draining.store(false, Ordering::Release); + return Err(err); + } + }; queue.lock_irqsave().advance_front(delivered); result.delivered += delivered; result.freed_backlog += delivered; @@ -550,6 +650,13 @@ impl PtyDevPtsLink { draining.store(false, Ordering::Release); break; } + chunks += 1; + if chunks >= max_chunks && !queue.lock_irqsave().is_empty() { + result.still_pending = true; + result.yielded = true; + draining.store(false, Ordering::Release); + break; + } } if !result.still_pending { @@ -736,7 +843,13 @@ impl TtyOperation for Unix98PtyDriverInner { if let Some(hook_arc) = tty.private_fields() { if let Some(hook) = hook_arc.as_any().downcast_ref::() { - return hook.write_to_peer(tty.driver().tty_driver_sub_type(), to, buf, nr); + return hook.write_to_peer( + hook_arc.clone(), + tty.driver().tty_driver_sub_type(), + to, + buf, + nr, + ); } } @@ -758,16 +871,26 @@ impl TtyOperation for Unix98PtyDriverInner { } fn flush_buffer(&self, tty: &TtyCoreData) -> Result<(), SystemError> { - let Some(to) = tty.link() else { - return Ok(()); - }; + let to = tty.link(); if let Some(hook_arc) = tty.private_fields() { if let Some(hook) = hook_arc.as_any().downcast_ref::() { hook.clear_pending_from(tty.driver().tty_driver_sub_type())?; + // Releasing bridge capacity can satisfy blocked writers and + // POLLOUT waiters. Wake the source after making the new room + // visible. + if let Some(to) = to.as_ref() { + PtyDevPtsLink::wake_source_writer(to); + } else { + tty.write_wq().wakeup_all(); + } } } + let Some(to) = to else { + return Ok(()); + }; + if to.core().contorl_info_irqsave().packet { tty.contorl_info_irqsave() .pktstatus @@ -1041,6 +1164,7 @@ pub fn ptmx_open( *data = FilePrivateData::Tty(TtyFilePrivateData { tty: tty.clone(), flags: *flags, + hangup_generation: tty.core().file_open_hangup_generation(), }); let core = tty.core(); diff --git a/kernel/src/driver/tty/termios.rs b/kernel/src/driver/tty/termios.rs index 4d9a66242..ce5a56255 100644 --- a/kernel/src/driver/tty/termios.rs +++ b/kernel/src/driver/tty/termios.rs @@ -33,7 +33,12 @@ pub struct Termios { pub control_mode: ControlMode, pub local_mode: LocalMode, pub control_characters: [u8; CONTORL_CHARACTER_NUM], + /// Active line discipline (derived from c_line_abi). pub line: LineDisciplineType, + /// Raw ABI c_line value preserved across TCGETS/TCSETA round-trips. + /// Linux stores the c_line byte as-is; line discipline switching is + /// done via TIOCSETD, not by writing c_line in termios. + pub c_line_abi: u8, pub input_speed: u32, pub output_speed: u32, } @@ -50,14 +55,14 @@ pub struct PosixTermios { } impl PosixTermios { - pub fn from_kernel_termios(termios: Termios) -> Self { + pub fn from_kernel_termios(termios: &Termios) -> Self { Self { c_iflag: termios.input_mode.bits, c_oflag: termios.output_mode.bits, c_cflag: termios.control_mode.bits, c_lflag: termios.local_mode.bits, c_cc: termios.control_characters, - c_line: termios.line as u8, + c_line: termios.c_line_abi, } } @@ -70,23 +75,20 @@ impl PosixTermios { local_mode: LocalMode::from_bits_truncate(self.c_lflag), control_characters: self.c_cc, line: LineDisciplineType::from_line(self.c_line), + c_line_abi: self.c_line, input_speed: self.input_speed().unwrap_or(38400), output_speed: self.output_speed().unwrap_or(38400), } } fn output_speed(&self) -> Option { - let flag = ControlMode::from_bits_truncate(self.c_cflag & ControlMode::CBAUD.bits()); - flag.baud_rate() + let cflag = ControlMode::from_bits_truncate(self.c_cflag & ControlMode::CBAUD.bits()); + cflag.baud_rate() } fn input_speed(&self) -> Option { - let ibaud = (self.c_cflag & ControlMode::CIBAUD.bits()) >> 16; - if ibaud == 0 { - self.output_speed() - } else { - ControlMode::from_bits_truncate(ibaud).baud_rate() - } + let cflag = ControlMode::from_bits_truncate(self.c_cflag); + cflag.input_baud_rate() } } @@ -146,6 +148,7 @@ lazy_static! { | LocalMode::IEXTEN, control_characters: INIT_CONTORL_CHARACTERS, line: LineDisciplineType::NTty, + c_line_abi: INIT_C_LINE_ABI, input_speed: 38400, output_speed: 38400, } @@ -356,7 +359,7 @@ bitflags! { const TERMIOS_WAIT =2; const TERMIOS_TERMIO =4; const TERMIOS_OLD =8; - } +} } impl ControlMode { @@ -398,6 +401,109 @@ impl ControlMode { _ => None, } } + + /// Get input baud rate from CIBAUD bits, falling back to output baud rate. + pub fn input_baud_rate(&self) -> Option { + let ibaud = (self.bits() & Self::CIBAUD.bits()) >> 16; + if ibaud == 0 { + self.baud_rate() + } else { + ControlMode::from_bits_truncate(ibaud).baud_rate() + } + } +} + +pub const NCC: usize = 8; + +/// Default c_line ABI value (N_TTY = 0). +pub const INIT_C_LINE_ABI: u8 = 0; + +/// Legacy SVR4 `struct termio` — the smaller predecessor of `struct termios`. +/// Used by ioctl commands TCGETA / TCSETA / TCSETAW / TCSETAF. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct PosixTermio { + pub c_iflag: u16, + pub c_oflag: u16, + pub c_cflag: u16, + pub c_lflag: u16, + pub c_line: u8, + pub c_cc: [u8; NCC], + /// Explicit padding in place of repr(C) implicit tail padding. + /// Essential: `Default::default()` zeros this field, preventing kernel + /// stack data leak to userspace via TCGETA. + pub(crate) _pad: u8, +} + +// Compile-time guard: sizeof(PosixTermio) must be 18 to match C struct +// termio on all supported ABIs. If this fails on a new architecture, +// the _pad field and/or repr alignment need adjustment. +const _: () = assert!(core::mem::size_of::() == 18); + +impl PosixTermio { + /// Convert kernel Termios → user-space termio. + /// Fields wider than u16 are truncated; excess c_cc slots are dropped. + pub fn from_kernel_termios(termios: &Termios) -> Self { + let mut cc = [0u8; NCC]; + let copy_len = NCC.min(termios.control_characters.len()); + cc[..copy_len].copy_from_slice(&termios.control_characters[..copy_len]); + Self { + c_iflag: termios.input_mode.bits as u16, + c_oflag: termios.output_mode.bits as u16, + c_cflag: termios.control_mode.bits as u16, + c_lflag: termios.local_mode.bits as u16, + c_line: termios.c_line_abi, + c_cc: cc, + _pad: 0, + } + } + + /// Convert user-space termio → kernel Termios, merging with `old`. + /// + /// Matches Linux 6.6 generic `user_termio_to_kernel_termios()`: + /// the low 16 bits of each flag word come from the termio, the high + /// 16 bits are preserved from `old`. Only the first NCC control + /// characters are overwritten; the rest are preserved from `old`. + /// + /// `self._pad` is intentionally ignored — it exists solely to prevent + /// kernel stack data leaks to userspace via TCGETA reads. + /// User-supplied `_pad` values from TCGETA ioctl are harmless. + pub fn to_kernel_termios(self, old: &Termios) -> Termios { + let mut cc = old.control_characters; + cc[..NCC].copy_from_slice(&self.c_cc); + + let control_mode = ControlMode::from_bits_truncate( + (old.control_mode.bits & 0xffff_0000) | self.c_cflag as u32, + ); + let (input_speed, output_speed) = Self::speeds_from_cflag(control_mode); + + Termios { + input_mode: InputMode::from_bits_truncate( + (old.input_mode.bits & 0xffff_0000) | self.c_iflag as u32, + ), + output_mode: OutputMode::from_bits_truncate( + (old.output_mode.bits & 0xffff_0000) | self.c_oflag as u32, + ), + control_mode, + local_mode: LocalMode::from_bits_truncate( + (old.local_mode.bits & 0xffff_0000) | self.c_lflag as u32, + ), + control_characters: cc, + line: LineDisciplineType::from_line(self.c_line), + c_line_abi: self.c_line, + input_speed, + output_speed, + } + } + + /// Extract input/output speeds from a merged kernel Termios c_cflag, + /// mirroring how Linux `set_termios()` calls `tty_termios_baud_rate()` + /// after `user_termio_to_kernel_termios()`. + pub fn speeds_from_cflag(cflag: ControlMode) -> (u32, u32) { + let ospeed = cflag.baud_rate().unwrap_or(38400); + let ispeed = cflag.input_baud_rate().unwrap_or(ospeed); + (ispeed, ospeed) + } } /// 对应termios中控制字符的索引 diff --git a/kernel/src/driver/tty/tty_core.rs b/kernel/src/driver/tty/tty_core.rs index 4193342d9..b37357cc9 100644 --- a/kernel/src/driver/tty/tty_core.rs +++ b/kernel/src/driver/tty/tty_core.rs @@ -16,23 +16,25 @@ use crate::{ event_poll::{EventPoll, LockedEPItemLinkedList}, EPollEventType, EPollItem, }, + filesystem::vfs::fasync::{FAsyncItem, FAsyncItems}, libs::{ rwlock::{RwLock, RwLockReadGuard, RwLockUpgradableGuard, RwLockWriteGuard}, + rwsem::{RwSem, RwSemReadGuard, RwSemWriteGuard}, spinlock::{SpinLock, SpinLockGuard}, wait_queue::{EventWaitQueue, WaitQueue}, }, mm::VirtAddr, - process::{pid::Pid, ProcessControlBlock}, + process::{pid::Pid, ProcessControlBlock, ProcessManager}, syscall::user_access::{UserBufferReader, UserBufferWriter}, }; use super::{ - termios::{ControlMode, PosixTermios, Termios, TtySetTermiosOpt, WindowSize}, + termios::{ControlMode, PosixTermio, PosixTermios, Termios, TtySetTermiosOpt, WindowSize}, tty_driver::{TtyCorePrivateField, TtyDriver, TtyDriverSubType, TtyDriverType, TtyOperation}, tty_job_control::TtyJobCtrlManager, tty_ldisc::{ ntty::{NTtyData, NTtyLinediscipline}, - TtyLineDiscipline, + TtyLdiscDrainResult, TtyLineDiscipline, }, tty_port::TtyPort, virtual_terminal::{vc_manager, virtual_console::VirtualConsoleData, DrawRegion}, @@ -131,6 +133,7 @@ impl TtyCore { let core = TtyCoreData { tty_driver: driver, termios: RwLock::new(termios), + termios_rwsem: RwSem::new(()), name, flags: RwLock::new(TtyFlag::empty()), count: AtomicUsize::new(0), @@ -143,9 +146,11 @@ impl TtyCore { vc_index: AtomicUsize::new(usize::MAX), ctrl: SpinLock::new(TtyControlInfo::default()), closing: AtomicBool::new(false), + hangup_generation: AtomicUsize::new(0), flow: SpinLock::new(TtyFlowState::default()), link: RwLock::default(), epitems: LockedEPItemLinkedList::default(), + fasync_items: FAsyncItems::new(), device_number, privete_fields: SpinLock::new(None), }; @@ -243,11 +248,16 @@ impl TtyCore { pub fn tty_vhangup(tty: Arc) { { let mut flags = tty.core().flags_write(); - if flags.contains(TtyFlag::HUPPED) { + if flags.intersects(TtyFlag::HUPPED | TtyFlag::HUPPING) { return; } flags.insert(TtyFlag::HUPPING); + tty.core().hangup_generation.fetch_add(1, Ordering::AcqRel); } + // Linux removes fasync registrations before replacing existing tty + // file operations with hung_up_tty_fops. This also clears O_ASYNC on + // every pre-hangup open file description. + tty.core().fasync_items.clear(); let (sid, pgid) = { let ctrl = tty.core().contorl_info_irqsave(); @@ -285,16 +295,17 @@ impl TtyCore { pub fn tty_mode_ioctl(tty: Arc, cmd: u32, arg: usize) -> Result { let core = tty.core(); - let real_tty = if core.driver().tty_driver_type() == TtyDriverType::Pty - && core.driver().tty_driver_sub_type() == TtyDriverSubType::PtyMaster - { + let redirected_from_pty_master = core.driver().tty_driver_type() == TtyDriverType::Pty + && core.driver().tty_driver_sub_type() == TtyDriverSubType::PtyMaster; + let real_tty = if redirected_from_pty_master { core.link().unwrap() } else { tty }; match cmd { TtyIoctlCmd::TCGETS => { - let termios = PosixTermios::from_kernel_termios(*real_tty.core.termios()); + let snapshot = real_tty.core.committed_termios_snapshot(); + let termios = PosixTermios::from_kernel_termios(&snapshot); let mut user_writer = UserBufferWriter::new( VirtAddr::new(arg).as_ptr::(), core::mem::size_of::(), @@ -304,11 +315,23 @@ impl TtyCore { user_writer.copy_one_to_user(&termios, 0)?; return Ok(0); } + TtyIoctlCmd::TCGETA => { + let snapshot = real_tty.core.committed_termios_snapshot(); + let termio = PosixTermio::from_kernel_termios(&snapshot); + let mut user_writer = UserBufferWriter::new( + VirtAddr::new(arg).as_ptr::(), + core::mem::size_of::(), + true, + )?; + user_writer.copy_one_to_user(&termio, 0)?; + return Ok(0); + } TtyIoctlCmd::TCSETS => { return TtyCore::core_set_termios( real_tty, VirtAddr::new(arg), TtySetTermiosOpt::TERMIOS_OLD, + redirected_from_pty_master, ); } TtyIoctlCmd::TCSETSW => { @@ -316,6 +339,7 @@ impl TtyCore { real_tty, VirtAddr::new(arg), TtySetTermiosOpt::TERMIOS_WAIT | TtySetTermiosOpt::TERMIOS_OLD, + redirected_from_pty_master, ); } TtyIoctlCmd::TCSETSF => { @@ -325,6 +349,33 @@ impl TtyCore { TtySetTermiosOpt::TERMIOS_FLUSH | TtySetTermiosOpt::TERMIOS_WAIT | TtySetTermiosOpt::TERMIOS_OLD, + redirected_from_pty_master, + ); + } + TtyIoctlCmd::TCSETA => { + return TtyCore::core_set_termios( + real_tty, + VirtAddr::new(arg), + TtySetTermiosOpt::TERMIOS_TERMIO, + redirected_from_pty_master, + ); + } + TtyIoctlCmd::TCSETAW => { + return TtyCore::core_set_termios( + real_tty, + VirtAddr::new(arg), + TtySetTermiosOpt::TERMIOS_WAIT | TtySetTermiosOpt::TERMIOS_TERMIO, + redirected_from_pty_master, + ); + } + TtyIoctlCmd::TCSETAF => { + return TtyCore::core_set_termios( + real_tty, + VirtAddr::new(arg), + TtySetTermiosOpt::TERMIOS_FLUSH + | TtySetTermiosOpt::TERMIOS_WAIT + | TtySetTermiosOpt::TERMIOS_TERMIO, + redirected_from_pty_master, ); } _ => { @@ -334,6 +385,9 @@ impl TtyCore { } pub fn tty_perform_flush(tty: Arc, arg: usize) -> Result { + // Job control check for TCFLSH ioctl. This is a separate path + // from core_set_termios (termios ioctls); the two are never + // invoked concurrently for the same operation. TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTOU)?; match arg { @@ -359,13 +413,28 @@ impl TtyCore { tty: Arc, arg: VirtAddr, opt: TtySetTermiosOpt, + redirected_from_pty_master: bool, ) -> Result { + // Check job control: prevent background process groups from changing + // terminal attributes without permission. Matches Linux 6.6 + // set_termios() which calls tty_check_change() before any work. + // This path is separate from tty_perform_flush (TCFLSH ioctl); + // the two never overlap in a single operation. + TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTOU)?; + #[allow(unused_assignments)] // TERMIOS_TERMIO下会用到 - let mut tmp_termios = *tty.core().termios(); + let mut tmp_termios = tty.core().committed_termios_snapshot(); if opt.contains(TtySetTermiosOpt::TERMIOS_TERMIO) { - todo!() + let user_reader = UserBufferReader::new( + arg.as_ptr::(), + core::mem::size_of::(), + true, + )?; + let mut termio = PosixTermio::default(); + user_reader.copy_one_from_user(&mut termio, 0)?; + tmp_termios = termio.to_kernel_termios(&tmp_termios); } else { let user_reader = UserBufferReader::new( arg.as_ptr::(), @@ -379,20 +448,101 @@ impl TtyCore { tmp_termios = term.to_kernel_termios(); } - if opt.contains(TtySetTermiosOpt::TERMIOS_FLUSH) { - let ld = tty.ldisc(); - let _ = ld.flush_buffer(tty.clone()); - } + if opt.intersects(TtySetTermiosOpt::TERMIOS_WAIT | TtySetTermiosOpt::TERMIOS_FLUSH) { + let events = (EPollEventType::EPOLLOUT | EPollEventType::EPOLLWRNORM).bits() as u64; + + loop { + let write_guard = tty.core().write_lock().lock_interruptible(false)?; + let termios_guard = tty.core().termios_write_lock_interruptible()?; + + let output_closed = Self::output_terminal_closed(tty.core()); + if output_closed && !redirected_from_pty_master { + return Err(SystemError::EIO); + } + + if !output_closed { + match tty.ldisc().drain_output(tty.clone())? { + TtyLdiscDrainResult::Drained => {} + TtyLdiscDrainResult::NeedWriteRoom(required) => { + let required = required.max(1); + drop(termios_guard); + drop(write_guard); + tty.core().write_wq().wait_event_interruptible(events, || { + Self::output_terminal_closed(tty.core()) + || tty.write_room(tty.core()) >= required + || !tty.ldisc().output_pending() + })?; + if Self::output_terminal_closed(tty.core()) + && !redirected_from_pty_master + { + return Err(SystemError::EIO); + } + continue; + } + } + + if tty.chars_in_buffer(tty.core()) != 0 { + drop(termios_guard); + drop(write_guard); + tty.core().write_wq().wait_event_interruptible(events, || { + Self::output_terminal_closed(tty.core()) + || tty.chars_in_buffer(tty.core()) == 0 + })?; + if Self::output_terminal_closed(tty.core()) && !redirected_from_pty_master { + return Err(SystemError::EIO); + } + continue; + } + } + + // Linux flushes input and waits for the physical transmitter + // before tty_set_termios() takes termios_rwsem for the short + // attribute transaction. Keeping the write side of the + // semaphore across either operation can deadlock synchronous + // input producers and unnecessarily stalls RX at low baud. + drop(termios_guard); - if opt.contains(TtySetTermiosOpt::TERMIOS_WAIT) { - // TODO + // Linux performs input flush after the output-drain recheck. + if opt.contains(TtySetTermiosOpt::TERMIOS_FLUSH) { + tty.ldisc().flush_buffer(tty.clone())?; + } + + if opt.contains(TtySetTermiosOpt::TERMIOS_WAIT) && !output_closed { + tty.wait_until_sent(tty.core())?; + let current = ProcessManager::current_pcb(); + if current.has_pending_signal_fast() && current.has_pending_not_masked_signal() + { + return Err(SystemError::ERESTARTSYS); + } + if Self::output_terminal_closed(tty.core()) && !redirected_from_pty_master { + return Err(SystemError::EIO); + } + } + + let termios_guard = tty.core().termios_write_lock_interruptible()?; + let result = Self::set_termios_locked(tty.clone(), tmp_termios); + drop(termios_guard); + drop(write_guard); + Self::retry_deferred_output(&tty); + result?; + return Ok(0); + } } - TtyCore::set_termios_next(tty, tmp_termios)?; + Self::set_termios_next(tty, tmp_termios)?; Ok(0) } fn set_termios_next(tty: Arc, new_termios: Termios) -> Result<(), SystemError> { + let termios_guard = tty.core().termios_write_lock_interruptible()?; + let result = Self::set_termios_locked(tty.clone(), new_termios); + drop(termios_guard); + Self::retry_deferred_output(&tty); + result + } + + /// Apply termios while the caller holds `termios_write_lock_interruptible()`. + fn set_termios_locked(tty: Arc, new_termios: Termios) -> Result<(), SystemError> { let mut termios = tty.core().termios_write(); let old_termios = *termios; @@ -418,6 +568,19 @@ impl TtyCore { Ok(()) } + fn output_terminal_closed(core: &TtyCoreData) -> bool { + let flags = core.flags(); + flags.intersects(TtyFlag::IO_ERROR | TtyFlag::HUPPED | TtyFlag::HUPPING) + || (flags.contains(TtyFlag::OTHER_CLOSED) + && core.driver().tty_driver_sub_type() != TtyDriverSubType::PtyMaster) + } + + fn retry_deferred_output(tty: &Arc) { + if tty.core().flags().contains(TtyFlag::DO_WRITE_WAKEUP) { + tty.tty_wakeup(); + } + } + pub fn tty_do_resize(&self, windowsize: WindowSize) -> Result<(), SystemError> { // TODO: 向前台进程发送信号 *self.core.window_size_write() = windowsize; @@ -470,6 +633,8 @@ pub struct TtyFlowState { pub struct TtyCoreData { tty_driver: Arc, termios: RwLock, + /// Serializes termios users with the update + driver/ldisc notification transaction. + termios_rwsem: RwSem<()>, name: String, flags: RwLock, /// 在初始化时即确定不会更改,所以这里不用加锁 @@ -490,12 +655,18 @@ pub struct TtyCoreData { ctrl: SpinLock, /// 是否正在关闭 closing: AtomicBool, + /// Incremented when a hangup starts so file instances opened before that + /// point retain Linux's hung_up_tty_fops semantics independently of later + /// reopens. + hangup_generation: AtomicUsize, /// 流控状态 flow: SpinLock, /// 链接tty link: RwLock>, /// epitems epitems: LockedEPItemLinkedList, + /// Open file descriptions registered for asynchronous TTY notification. + fasync_items: FAsyncItems, /// 设备号 device_number: DeviceNumber, @@ -542,6 +713,64 @@ impl TtyCoreData { self.flags.write_irqsave() } + #[inline] + pub fn hangup_generation(&self) -> usize { + self.hangup_generation.load(Ordering::Acquire) + } + + #[inline] + pub fn file_hung_up(&self, file_hangup_generation: usize) -> bool { + self.hangup_generation() != file_hangup_generation + } + + /// Snapshot the generation assigned to a newly opening file. + /// + /// A file which races with an in-progress hangup belongs to the generation + /// being hung up. Holding the flags read lock pairs this decision with the + /// generation increment performed under the flags write lock. + pub fn file_open_hangup_generation(&self) -> usize { + let flags = self.flags.read_irqsave(); + let generation = self.hangup_generation.load(Ordering::Acquire); + if flags.contains(TtyFlag::HUPPING) { + generation.wrapping_sub(1) + } else { + generation + } + } + + /// Clear a completed pre-open hangup only if no newer hangup raced with + /// this open. The generation comparison and flag update share the same + /// critical section as tty_vhangup's generation increment. + pub fn finish_file_open(&self, file_hangup_generation: usize) { + let mut flags = self.flags.write_irqsave(); + if self.hangup_generation.load(Ordering::Acquire) == file_hangup_generation { + flags.remove(TtyFlag::HUPPED); + } + } + + pub fn add_fasync( + &self, + file_hangup_generation: usize, + item: FAsyncItem, + ) -> Result<(), SystemError> { + let flags = self.flags.read_irqsave(); + if flags.contains(TtyFlag::HUPPING) + || self.hangup_generation.load(Ordering::Acquire) != file_hangup_generation + { + return Err(SystemError::ENOTTY); + } + self.fasync_items.add(item); + Ok(()) + } + + pub fn remove_fasync(&self, file: &Weak) { + self.fasync_items.remove(file); + } + + pub fn remove_fasync_file(&self, file: &crate::filesystem::vfs::file::File) { + self.fasync_items.remove_file(file); + } + pub fn private_fields(&self) -> Option> { self.privete_fields.lock().clone() } @@ -556,6 +785,37 @@ impl TtyCoreData { self.termios.write_irqsave() } + /// Snapshot only a fully committed termios transaction. + pub fn committed_termios_snapshot(&self) -> Termios { + let _guard = self.termios_read_lock(); + *self.termios() + } + + #[inline] + pub fn termios_read_lock(&self) -> RwSemReadGuard<'_, ()> { + self.termios_rwsem.read() + } + + #[inline] + pub fn termios_read_lock_interruptible(&self) -> Result, SystemError> { + self.termios_rwsem.read_interruptible() + } + + #[inline] + pub fn termios_try_read_lock(&self) -> Option> { + self.termios_rwsem.try_read() + } + + #[inline] + pub fn termios_write_lock_interruptible(&self) -> Result, SystemError> { + self.termios_rwsem.write_interruptible() + } + + #[inline] + pub fn termios_write_lock(&self) -> RwSemWriteGuard<'_, ()> { + self.termios_rwsem.write() + } + #[inline] pub fn set_termios(&self, termios: Termios) { let mut termios_guard = self.termios_write(); @@ -796,6 +1056,11 @@ impl TtyOperation for TtyCore { return tty.tty_driver.driver_funcs().chars_in_buffer(tty); } + #[inline] + fn wait_until_sent(&self, tty: &TtyCoreData) -> Result<(), SystemError> { + tty.tty_driver.driver_funcs().wait_until_sent(tty) + } + #[inline] fn set_termios(&self, tty: Arc, old_termios: Termios) -> Result<(), SystemError> { return self diff --git a/kernel/src/driver/tty/tty_device.rs b/kernel/src/driver/tty/tty_device.rs index fefab48d3..b7e30aa5b 100644 --- a/kernel/src/driver/tty/tty_device.rs +++ b/kernel/src/driver/tty/tty_device.rs @@ -31,6 +31,7 @@ use crate::{ epoll::{EPollEventType, EPollItem}, kernfs::KernFSInode, vfs::{ + fasync::FAsyncItem, file::{File, FileFlags}, utils::DName, FilePrivateData, FileType, IndexNode, InodeMode, Metadata, PollableInode, @@ -51,6 +52,7 @@ use super::{ tty_core::{TtyCore, TtyFlag, TtyIoctlCmd}, tty_driver::{TtyDriverManager, TtyDriverSubType, TtyDriverType, TtyOperation}, tty_job_control::TtyJobCtrlManager, + tty_ldisc::TtyLdiscFileContext, virtual_terminal::vty_init, }; @@ -150,13 +152,12 @@ impl TtyDevice { &self.name } - fn tty_core(private_data: &FilePrivateData) -> Result, SystemError> { - let (tty, _) = if let FilePrivateData::Tty(tty_priv) = private_data { - (tty_priv.tty.clone(), tty_priv.flags) + fn tty_file(private_data: &FilePrivateData) -> Result<&TtyFilePrivateData, SystemError> { + if let FilePrivateData::Tty(tty_priv) = private_data { + Ok(tty_priv) } else { - return Err(SystemError::EIO); - }; - Ok(tty) + Err(SystemError::EIO) + } } /// tty_open_current_tty - get locked tty of current task @@ -193,7 +194,18 @@ impl Debug for TtyDevice { impl PollableInode for TtyDevice { fn poll(&self, private_data: &FilePrivateData) -> Result { - let tty = TtyDevice::tty_core(private_data)?; + let tty_file = TtyDevice::tty_file(private_data)?; + if tty_file.is_hung_up() { + return Ok((EPollEventType::EPOLLIN + | EPollEventType::EPOLLOUT + | EPollEventType::EPOLLERR + | EPollEventType::EPOLLHUP + | EPollEventType::EPOLLRDNORM + | EPollEventType::EPOLLWRNORM) + .bits() as usize); + } + + let tty = tty_file.tty(); tty.ldisc().poll(tty) } @@ -202,7 +214,15 @@ impl PollableInode for TtyDevice { epitem: Arc, private_data: &FilePrivateData, ) -> Result<(), SystemError> { - let tty = TtyDevice::tty_core(private_data)?; + let tty_file = TtyDevice::tty_file(private_data)?; + if tty_file.is_hung_up() { + // hung_up_tty_poll() is permanently ready and does not register + // a wait queue. epoll observes the fixed mask immediately after + // this callback. + return Ok(()); + } + + let tty = tty_file.tty(); let core = tty.core(); core.add_epitem(epitem); Ok(()) @@ -213,9 +233,50 @@ impl PollableInode for TtyDevice { epitem: &Arc, private_data: &FilePrivateData, ) -> Result<(), SystemError> { - let tty = TtyDevice::tty_core(private_data)?; + let tty_file = TtyDevice::tty_file(private_data)?; + let hung_up = tty_file.is_hung_up(); + let tty = tty_file.tty(); let core = tty.core(); - core.remove_epitem(epitem) + match core.remove_epitem(epitem) { + Err(SystemError::ENOENT) if hung_up => Ok(()), + result => result, + } + } + + fn add_fasync( + &self, + fasync_item: FAsyncItem, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + let tty_file = Self::tty_file(private_data)?; + tty_file + .tty() + .core() + .add_fasync(tty_file.hangup_generation, fasync_item) + } + + fn remove_fasync( + &self, + file: &Weak, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + Self::tty_file(private_data)? + .tty() + .core() + .remove_fasync(file); + Ok(()) + } + + fn release_fasync( + &self, + file: &File, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + Self::tty_file(private_data)? + .tty() + .core() + .remove_fasync_file(file); + Ok(()) } } @@ -256,11 +317,13 @@ impl IndexNode for TtyDevice { } let tty = tty.unwrap(); + let hangup_generation = tty.core().file_open_hangup_generation(); // 设置privdata *data = FilePrivateData::Tty(TtyFilePrivateData { tty: tty.clone(), flags: *mode, + hangup_generation, }); tty.core().contorl_info_irqsave().clear_dead_session(); @@ -275,6 +338,11 @@ impl IndexNode for TtyDevice { return Err(err); } + // A successful open after a completed hangup is a normal tty file in + // Linux. Do not clear a hangup which raced with this open: its + // generation change marks this file as one of the hung-up instances. + tty.core().finish_file_open(hangup_generation); + let driver = tty.core().driver(); // 考虑 O_NOCTTY:显式指定则不设置控制终端;pty master 也不会成为控制终端。 if !(mode.contains(FileFlags::O_NOCTTY) @@ -306,10 +374,14 @@ impl IndexNode for TtyDevice { buf: &mut [u8], data: MutexGuard, ) -> Result { - let (tty, flags) = if let FilePrivateData::Tty(tty_priv) = &*data { - (tty_priv.tty(), tty_priv.flags) - } else { - return Err(SystemError::EIO); + let tty_file = Self::tty_file(&data)?; + if tty_file.is_hung_up() { + return Ok(0); + } + let tty = tty_file.tty(); + let file_context = TtyLdiscFileContext { + flags: tty_file.flags, + hangup_generation: tty_file.hangup_generation, }; drop(data); @@ -325,7 +397,14 @@ impl IndexNode for TtyDevice { loop { let mut size = (len - offset).min(buf.len() - offset); - size = ld.read(tty.clone(), &mut buf[offset..], size, &mut cookie, 0, flags)?; + size = ld.read( + tty.clone(), + &mut buf[offset..], + size, + &mut cookie, + 0, + file_context, + )?; // 本次迭代未读取到数据,可能是EOF或暂时无数据可读 if size == 0 { break; @@ -355,17 +434,21 @@ impl IndexNode for TtyDevice { data: MutexGuard, ) -> Result { let mut count = len; - let (tty, flags) = if let FilePrivateData::Tty(tty_priv) = &*data { - (tty_priv.tty(), tty_priv.flags) - } else { + let tty_file = Self::tty_file(&data)?; + if tty_file.is_hung_up() { return Err(SystemError::EIO); + } + let tty = tty_file.tty(); + let file_context = TtyLdiscFileContext { + flags: tty_file.flags, + hangup_generation: tty_file.hangup_generation, }; drop(data); let ld = tty.ldisc(); let core = tty.core(); let write_guard = core .write_lock() - .lock_interruptible(flags.contains(FileFlags::O_NONBLOCK))?; + .lock_interruptible(file_context.flags.contains(FileFlags::O_NONBLOCK))?; let mut chunk = 2048; if core.flags().contains(TtyFlag::NO_WRITE_SPLIT) { chunk = 65536; @@ -380,7 +463,7 @@ impl IndexNode for TtyDevice { // 将数据从buf拷贝到writebuf - let ret = match ld.write(tty.clone(), &buf[written..], size, flags) { + let ret = match ld.write(tty.clone(), &buf[written..], size, file_context) { Ok(ret) => ret, Err(err) => { if written != 0 { @@ -485,15 +568,25 @@ impl IndexNode for TtyDevice { arg: usize, data: MutexGuard, ) -> Result { - let (tty, _) = if let FilePrivateData::Tty(tty_priv) = &*data { - (tty_priv.tty(), tty_priv.flags) - } else { - return Err(SystemError::EIO); - }; + let tty_file = Self::tty_file(&data)?; + let hung_up = tty_file.is_hung_up(); + let tty = tty_file.tty(); // Drop the lock early: tty ioctl paths may block. drop(data); + // Linux replaces only the file instances which existed at hangup + // with hung_up_tty_fops. A later successful reopen must remain usable. + // Check the original file endpoint before PTY master mode ioctls + // redirect to the slave. + if hung_up { + return if cmd == TtyIoctlCmd::TIOCSPGRP { + Err(SystemError::ENOTTY) + } else { + Err(SystemError::EIO) + }; + } + match cmd { TtyIoctlCmd::TIOCSETD | TtyIoctlCmd::TIOCSBRK @@ -766,12 +859,17 @@ impl CharDevice for TtyDevice { pub struct TtyFilePrivateData { pub tty: Arc, pub flags: FileFlags, + pub hangup_generation: usize, } impl TtyFilePrivateData { pub fn tty(&self) -> Arc { self.tty.clone() } + + pub fn is_hung_up(&self) -> bool { + self.hangup_generation != self.tty.core().hangup_generation() + } } /// 初始化tty设备和console子设备 diff --git a/kernel/src/driver/tty/tty_driver.rs b/kernel/src/driver/tty/tty_driver.rs index c70ade7b9..79639616f 100644 --- a/kernel/src/driver/tty/tty_driver.rs +++ b/kernel/src/driver/tty/tty_driver.rs @@ -529,6 +529,11 @@ pub trait TtyOperation: Sync + Send + Debug { 0 } + /// Wait until bytes accepted by the driver have physically left the device. + fn wait_until_sent(&self, _tty: &TtyCoreData) -> Result<(), SystemError> { + Ok(()) + } + fn set_termios(&self, _tty: Arc, _old_termios: Termios) -> Result<(), SystemError> { Err(SystemError::ENOSYS) } diff --git a/kernel/src/driver/tty/tty_ldisc/mod.rs b/kernel/src/driver/tty/tty_ldisc/mod.rs index f4e9083a6..d7982e04c 100644 --- a/kernel/src/driver/tty/tty_ldisc/mod.rs +++ b/kernel/src/driver/tty/tty_ldisc/mod.rs @@ -12,6 +12,18 @@ use super::{ pub mod ntty; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TtyLdiscDrainResult { + Drained, + NeedWriteRoom(usize), +} + +#[derive(Debug, Clone, Copy)] +pub struct TtyLdiscFileContext { + pub flags: FileFlags, + pub hangup_generation: usize, +} + pub trait TtyLineDiscipline: Sync + Send + Debug { fn open(&self, tty: Arc) -> Result<(), SystemError>; fn close(&self, tty: Arc) -> Result<(), SystemError>; @@ -25,6 +37,31 @@ pub trait TtyLineDiscipline: Sync + Send + Debug { Ok(()) } + /// Drain pending ldisc output (opost + echo), blocking until complete. + /// + /// Returns the exact minimum driver write room needed to make progress. + /// + /// Callers MUST loop on `NeedWriteRoom`: wait for the requested room, + /// then call `drain_output` again until it returns `Drained`. See + /// `core_set_termios` for the canonical retry pattern. + /// + /// # Default implementation + /// + /// Returns `Drained`. **Line disciplines that have their own output + /// queues (opost / echo) MUST override this method.** The default + /// causes TCSADRAIN to silently skip draining for undiscovered ldiscs. + fn drain_output(&self, _tty: Arc) -> Result { + Ok(TtyLdiscDrainResult::Drained) + } + + /// Whether this line discipline still owns output that has not yet been + /// accepted by the driver. This must not sleep: wait-queue predicates use + /// it to notice progress made by `write_wakeup` even when that callback + /// consumes all newly available driver room. + fn output_pending(&self) -> bool { + false + } + /// ## tty行规程循环读取函数 /// /// ### 参数 @@ -40,14 +77,14 @@ pub trait TtyLineDiscipline: Sync + Send + Debug { len: usize, cookie: &mut bool, offset: usize, - flags: FileFlags, + file_context: TtyLdiscFileContext, ) -> Result; fn write( &self, tty: Arc, buf: &[u8], len: usize, - flags: FileFlags, + file_context: TtyLdiscFileContext, ) -> Result; fn ioctl(&self, tty: Arc, cmd: u32, arg: usize) -> Result; @@ -81,6 +118,19 @@ pub trait TtyLineDiscipline: Sync + Send + Debug { count: usize, ) -> Result; + /// Receive input while the caller already holds this TTY's termios read + /// semaphore. PTY uses this to lock the peer before marking a direction + /// as actively draining, avoiding nested reader acquisition. + fn receive_buf2_termios_locked( + &self, + tty: Arc, + buf: &[u8], + flags: Option<&[u8]>, + count: usize, + ) -> Result { + self.receive_buf2(tty, buf, flags, count) + } + /// ## 唤醒线路写者 fn write_wakeup(&self, _tty: &TtyCore) -> Result<(), SystemError> { Err(SystemError::ENOSYS) @@ -93,12 +143,18 @@ pub enum LineDisciplineType { } impl LineDisciplineType { + /// Convert a raw c_line ABI byte to a LineDisciplineType. + /// + /// NOTE: this accepts u8 rather than LineDisciplineType, so adding + /// new enum variants (e.g. `Ppp = 1`) will NOT trigger a compiler + /// error here. When extending the enum, update this match arm + /// to map the new variant(s). pub fn from_line(line: u8) -> Self { match line { 0 => Self::NTty, - _ => { - todo!() - } + // Unknown / unsupported line disciplines fall back to NTty, + // matching Linux behaviour (N_TTY is the default). + _ => Self::NTty, } } } diff --git a/kernel/src/driver/tty/tty_ldisc/ntty.rs b/kernel/src/driver/tty/tty_ldisc/ntty.rs index ec3347aad..ebd49d69e 100644 --- a/kernel/src/driver/tty/tty_ldisc/ntty.rs +++ b/kernel/src/driver/tty/tty_ldisc/ntty.rs @@ -35,7 +35,7 @@ use crate::{ time::Duration, }; -use super::TtyLineDiscipline; +use super::{TtyLdiscDrainResult, TtyLineDiscipline}; pub const NTTY_BUFSIZE: usize = 4096; pub const ECHO_COMMIT_WATERMARK: usize = 256; pub const ECHO_BLOCK: usize = 256; @@ -117,18 +117,20 @@ impl NTtyLinediscipline { } } - fn drain_opost_pending(&self, tty: &TtyCore) -> Result { + fn drain_opost_pending(&self, tty: &TtyCore) -> Result { let core = tty.core(); loop { let pending = self.disc_data().opost_pending_bytes().to_vec(); if pending.is_empty() { - return Ok(true); + return Ok(TtyLdiscDrainResult::Drained); } - let written = tty.write(core, &pending, pending.len())?; + let written = tty.write(core, &pending, pending.len()).inspect_err(|_| { + core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); + })?; if written == 0 { core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); - return Ok(false); + return Ok(TtyLdiscDrainResult::NeedWriteRoom(1)); } self.disc_data().advance_opost_pending(written); @@ -136,14 +138,16 @@ impl NTtyLinediscipline { } } - fn drain_echoes(&self, tty: &TtyCore) -> Result<(), SystemError> { + fn drain_echoes(&self, tty: &TtyCore) -> Result { let core = tty.core(); loop { while let Some(bytes) = { self.disc_data().echo_pending_bytes() } { - let written = tty.write(core, &bytes, bytes.len())?; + let written = tty.write(core, &bytes, bytes.len()).inspect_err(|_| { + core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); + })?; if written == 0 { core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); - return Ok(()); + return Ok(TtyLdiscDrainResult::NeedWriteRoom(1)); } { let mut guard = self.disc_data(); @@ -160,20 +164,39 @@ impl NTtyLinediscipline { }; let Some(step) = step else { - break; + let required = { + let guard = self.disc_data(); + if !guard.has_echo_output_pending() { + 0 + } else { + guard + .next_echo_step(&termios, usize::MAX) + .map(|step| step.bytes.len().max(1)) + .unwrap_or(1) + } + }; + if required == 0 { + break; + } + core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); + return Ok(TtyLdiscDrainResult::NeedWriteRoom(required)); }; if !step.bytes.is_empty() { let mut sent = 0; while sent < step.bytes.len() { - let written = tty.write(core, &step.bytes[sent..], step.bytes.len() - sent)?; + let written = tty + .write(core, &step.bytes[sent..], step.bytes.len() - sent) + .inspect_err(|_| { + core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); + })?; if written == 0 { if sent != 0 { let mut guard = self.disc_data(); guard.set_echo_pending_step(step, sent); } core.flags_write().insert(TtyFlag::DO_WRITE_WAKEUP); - return Ok(()); + return Ok(TtyLdiscDrainResult::NeedWriteRoom(1)); } sent += written; } @@ -189,7 +212,7 @@ impl NTtyLinediscipline { } else { core.flags_write().remove(TtyFlag::DO_WRITE_WAKEUP); } - Ok(()) + Ok(TtyLdiscDrainResult::Drained) } fn packet_status_pending(core: &TtyCoreData, packet: bool) -> bool { @@ -1771,13 +1794,22 @@ impl TtyLineDiscipline for NTtyLinediscipline { /// ## 重置缓冲区的基本信息 fn flush_buffer(&self, tty: Arc) -> Result<(), system_error::SystemError> { let core = tty.core(); - if let Some(port) = core.port() { - if port.clear_input() != 0 { - retry_tty_input_producers(); - } - } - pty_flush_input_buffer(tty.clone(), || { - let _ = core.termios(); + // Stop queue-to-ldisc delivery before taking termios_rwsem. A worker + // may already have marked a chunk as draining and then block on the + // read side, so waiting for it while holding the write side deadlocks. + let port = core.port(); + if let Some(port) = port.as_ref() { + port.begin_input_flush(); + } + + // Match n_tty_flush_buffer(): resetting the ring indices is a write + // transaction against concurrent read and receive paths. + let _termios_guard = core.termios_write_lock(); + let cleared_port_input = port + .as_ref() + .map(|port| port.clear_input_during_flush()) + .unwrap_or(0); + let result = pty_flush_input_buffer(tty.clone(), || { let mut ldata = self.disc_data(); ldata.read_head = 0; ldata.canon_head = 0; @@ -1793,7 +1825,19 @@ impl TtyLineDiscipline for NTtyLinediscipline { if core.link().is_some() { ldata.packet_mode_flush(core); } - })?; + }); + drop(_termios_guard); + + if let Some(port) = port.as_ref() { + port.finish_input_flush(); + if port.has_input() { + tty_kick_input_worker(tty.clone()); + } + } + if cleared_port_input != 0 { + retry_tty_input_producers(); + } + result?; core.read_wq().wakeup_all(); core.write_wq().wakeup_all(); @@ -1820,6 +1864,23 @@ impl TtyLineDiscipline for NTtyLinediscipline { Ok(()) } + fn drain_output(&self, tty: Arc) -> Result { + // Block on output_lock — a concurrent writer will release it once + // its output is submitted to the hardware, at which point we can + // drain the remaining opost/echo backlog. Without blocking, + // TCSADRAIN can silently switch termios while a writer is still + // mid-flight (see tty-termios-drain-bugs.md B1). + let _output_guard = self.output_lock.lock(); + match self.drain_opost_pending(&tty)? { + TtyLdiscDrainResult::Drained => self.drain_echoes(&tty), + blocked => Ok(blocked), + } + } + + fn output_pending(&self) -> bool { + self.disc_data().has_output_wakeup_pending() + } + #[inline(never)] fn read( &self, @@ -1828,10 +1889,18 @@ impl TtyLineDiscipline for NTtyLinediscipline { len: usize, cookie: &mut bool, _offset: usize, - flags: FileFlags, + file_context: super::TtyLdiscFileContext, ) -> Result { + let core = tty.core(); + if core.file_hung_up(file_context.hangup_generation) { + return Ok(0); + } + if !*cookie { + TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTIN)?; + } + let mut termios_guard = Some(core.termios_read_lock()); let mut ldata; - if flags.contains(FileFlags::O_NONBLOCK) { + if file_context.flags.contains(FileFlags::O_NONBLOCK) { let ret = self.disc_data_try_lock(); if ret.is_err() { return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); @@ -1840,7 +1909,6 @@ impl TtyLineDiscipline for NTtyLinediscipline { } else { ldata = self.disc_data(); } - let core = tty.core(); let termios = core.termios(); let mut nr = len; @@ -1866,6 +1934,7 @@ impl TtyLineDiscipline for NTtyLinediscipline { *cookie = false; let read_tail_moved = tail != ldata.read_tail; drop(ldata); + drop(termios_guard.take()); if read_tail_moved { tty_kick_input_worker(tty.clone()); Self::check_pty_unthrottle_after_read(&tty); @@ -1875,8 +1944,6 @@ impl TtyLineDiscipline for NTtyLinediscipline { drop(termios); - TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTIN)?; - let mut minimum: usize = 0; let mut current_wait = NTtyReadWait::Forever; let mut inter_byte_timeout = None; @@ -1904,6 +1971,10 @@ impl TtyLineDiscipline for NTtyLinediscipline { let tail = ldata.read_tail; drop(ldata); while nr != 0 { + if core.file_hung_up(file_context.hangup_generation) { + break; + } + // todo: 处理packet模式 if packet { let link = core.link().unwrap(); @@ -1928,7 +1999,9 @@ impl TtyLineDiscipline for NTtyLinediscipline { let core = tty.core(); if !ldata.input_available(core.termios(), false) { drop(ldata); + drop(termios_guard.take()); let _ = pty_drain_pending_to(tty.clone()); + termios_guard = Some(core.termios_read_lock()); ldata = self.disc_data(); } @@ -1941,14 +2014,20 @@ impl TtyLineDiscipline for NTtyLinediscipline { { let flags = core.flags(); if flags.contains(TtyFlag::OTHER_CLOSED) { - if flags.contains(TtyFlag::HUPPED) || flags.contains(TtyFlag::HUPPING) { + if core.file_hung_up(file_context.hangup_generation) + || flags.contains(TtyFlag::HUPPED) + || flags.contains(TtyFlag::HUPPING) + { break; } ret = Err(SystemError::EIO); break; } - if flags.contains(TtyFlag::HUPPED) || flags.contains(TtyFlag::HUPPING) { + if core.file_hung_up(file_context.hangup_generation) + || flags.contains(TtyFlag::HUPPED) + || flags.contains(TtyFlag::HUPPING) + { break; } } @@ -1957,7 +2036,7 @@ impl TtyLineDiscipline for NTtyLinediscipline { break; } - if flags.contains(FileFlags::O_NONBLOCK) + if file_context.flags.contains(FileFlags::O_NONBLOCK) || core.flags().contains(TtyFlag::LDISC_CHANGING) { ret = Err(SystemError::EAGAIN_OR_EWOULDBLOCK); @@ -1970,12 +2049,14 @@ impl TtyLineDiscipline for NTtyLinediscipline { } drop(ldata); + drop(termios_guard.take()); let events = (EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM).bits() as u64; let readiness = || { let ldata = self.disc_data(); ldata.input_available(core.termios(), false) || Self::packet_status_pending(core, packet) || core.flags().contains(TtyFlag::OTHER_CLOSED) + || core.file_hung_up(file_context.hangup_generation) || core.flags().contains(TtyFlag::HUPPED) || core.flags().contains(TtyFlag::HUPPING) || core.flags().contains(TtyFlag::LDISC_CHANGING) @@ -1996,14 +2077,18 @@ impl TtyLineDiscipline for NTtyLinediscipline { } break; } + termios_guard = Some(core.termios_read_lock()); continue; } if ldata.icanon && !core.termios().local_mode.contains(LocalMode::EXTPROC) { let more = ldata.canon_copy_from_read_buf(buf, &mut nr, &mut offset)?; if more { - *cookie = true; - break; + // The current canonical record wrapped around the ring. + // Finish it in this invocation while the termios read side + // is still held; returning a cookie would let tcsetattr() + // change ICANON/EXTPROC in the middle of one read syscall. + continue; } } else { // 非标准模式 @@ -2017,8 +2102,10 @@ impl TtyLineDiscipline for NTtyLinediscipline { if ldata.copy_from_read_buf(core.termios(), buf, &mut nr, &mut offset)? && offset >= minimum { - *cookie = true; - break; + // A ring wrap is an implementation detail. Continue the + // same read under the same termios snapshot instead of + // exposing a lock gap through tty_device's cookie loop. + continue; } } @@ -2034,6 +2121,7 @@ impl TtyLineDiscipline for NTtyLinediscipline { let ldata = self.disc_data(); let read_tail_moved = tail != ldata.read_tail; drop(ldata); + drop(termios_guard); if read_tail_moved { tty_kick_input_worker(tty.clone()); Self::check_pty_unthrottle_after_read(&tty); @@ -2052,22 +2140,35 @@ impl TtyLineDiscipline for NTtyLinediscipline { tty: Arc, buf: &[u8], len: usize, - _flags: FileFlags, + file_context: super::TtyLdiscFileContext, ) -> Result { let mut nr = len; let mut out_buf = Vec::with_capacity(NTTY_BUFSIZE); let pcb = ProcessManager::current_pcb(); let binding = tty.clone(); let core = binding.core(); - let mut termios = *core.termios(); - if termios.local_mode.contains(LocalMode::TOSTOP) { + if core.termios().local_mode.contains(LocalMode::TOSTOP) { TtyJobCtrlManager::tty_check_change(tty.clone(), Signal::SIGTTOU)?; } + let mut termios_guard = Some(core.termios_read_lock()); + let mut termios = *core.termios(); let mut output_guard = Some(self.output_lock.lock()); + if core.file_hung_up(file_context.hangup_generation) { + return Err(SystemError::EIO); + } + self.disc_data().process_echoes(tty.clone()); - self.drain_echoes(&tty)?; + // Echo drain is best-effort in the write path; Ok(false) means + // the hardware FIFO was full and draining will be retried by + // write_wakeup once the FIFO has room (DO_WRITE_WAKEUP is set). + // Known limitation: this can add latency to echo output during + // heavy writes, but the alternative (retry loop here) risks + // starving the write path. + // Echo output is best-effort. Pending state and DO_WRITE_WAKEUP are + // retained by drain_echoes so a later driver wakeup can retry it. + let _ = self.drain_echoes(&tty); let mut offset = 0; loop { @@ -2077,7 +2178,8 @@ impl TtyLineDiscipline for NTtyLinediscipline { } return Err(SystemError::ERESTARTSYS); } - if core.flags().contains(TtyFlag::HUPPED) + if core.file_hung_up(file_context.hangup_generation) + || core.flags().contains(TtyFlag::HUPPED) || (core.flags().contains(TtyFlag::OTHER_CLOSED) && core.driver().tty_driver_sub_type() != TtyDriverSubType::PtyMaster) || core.flags().contains(TtyFlag::HUPPING) @@ -2198,7 +2300,7 @@ impl TtyLineDiscipline for NTtyLinediscipline { break; } - if _flags.contains(FileFlags::O_NONBLOCK) + if file_context.flags.contains(FileFlags::O_NONBLOCK) || core.flags().contains(TtyFlag::LDISC_CHANGING) { if offset != 0 { @@ -2214,10 +2316,12 @@ impl TtyLineDiscipline for NTtyLinediscipline { // 休眠一段时间 // 获取到termios读锁,避免termios被更改导致行为异常 drop(output_guard.take()); + drop(termios_guard.take()); let wait_result = core.write_wq().wait_event_interruptible( EPollEventType::EPOLLOUT.bits() as u64, || { - if core.flags().contains(TtyFlag::HUPPED) + if core.file_hung_up(file_context.hangup_generation) + || core.flags().contains(TtyFlag::HUPPED) || (core.flags().contains(TtyFlag::OTHER_CLOSED) && core.driver().tty_driver_sub_type() != TtyDriverSubType::PtyMaster) || core.flags().contains(TtyFlag::HUPPING) @@ -2237,12 +2341,14 @@ impl TtyLineDiscipline for NTtyLinediscipline { }, ); if let Err(err) = wait_result { + termios_guard = Some(core.termios_read_lock()); output_guard = Some(self.output_lock.lock()); if offset != 0 { break; } return Err(err); } + termios_guard = Some(core.termios_read_lock()); output_guard = Some(self.output_lock.lock()); termios = *core.termios(); } @@ -2252,6 +2358,7 @@ impl TtyLineDiscipline for NTtyLinediscipline { } drop(output_guard); + drop(termios_guard); Ok(offset) } @@ -2526,10 +2633,18 @@ impl TtyLineDiscipline for NTtyLinediscipline { Ok(event.bits() as usize) } + /// Write wakeup: drain pending opost/echo output when hardware FIFO has room. + /// + /// Echo drain errors are intentionally logged rather than propagated — + /// this is a best-effort echo path; the caller at `tty_core.rs` already + /// discards the `write_wakeup` return value with `let _ = …`. fn write_wakeup(&self, tty: &TtyCore) -> Result<(), SystemError> { + let Some(_termios_guard) = tty.core().termios_try_read_lock() else { + return Ok(()); + }; if let Some(_output_guard) = self.output_lock.try_lock() { - if self.drain_opost_pending(tty)? { - self.drain_echoes(tty)?; + if self.drain_opost_pending(tty)? == TtyLdiscDrainResult::Drained { + let _ = self.drain_echoes(tty)?; } } Ok(()) @@ -2573,11 +2688,12 @@ impl TtyLineDiscipline for NTtyLinediscipline { flags: Option<&[u8]>, count: usize, ) -> Result { + let _termios_guard = tty.core().termios_read_lock(); let mut ldata = self.disc_data(); let ret = ldata.receive_buf_common(tty.clone(), buf, flags, count, false); drop(ldata); if let Some(_output_guard) = self.output_lock.try_lock() { - self.drain_echoes(&tty)?; + let _ = self.drain_echoes(&tty); } ret } @@ -2588,12 +2704,29 @@ impl TtyLineDiscipline for NTtyLinediscipline { buf: &[u8], flags: Option<&[u8]>, count: usize, + ) -> Result { + let _termios_guard = tty.core().termios_read_lock(); + let mut ldata = self.disc_data(); + let ret = ldata.receive_buf_common(tty.clone(), buf, flags, count, true); + drop(ldata); + if let Some(_output_guard) = self.output_lock.try_lock() { + let _ = self.drain_echoes(&tty); + } + ret + } + + fn receive_buf2_termios_locked( + &self, + tty: Arc, + buf: &[u8], + flags: Option<&[u8]>, + count: usize, ) -> Result { let mut ldata = self.disc_data(); let ret = ldata.receive_buf_common(tty.clone(), buf, flags, count, true); drop(ldata); if let Some(_output_guard) = self.output_lock.try_lock() { - self.drain_echoes(&tty)?; + let _ = self.drain_echoes(&tty); } ret } diff --git a/kernel/src/driver/tty/tty_port.rs b/kernel/src/driver/tty/tty_port.rs index 4593fc5b7..6016e881b 100644 --- a/kernel/src/driver/tty/tty_port.rs +++ b/kernel/src/driver/tty/tty_port.rs @@ -42,6 +42,7 @@ struct TtyInputQueue { len: usize, generation: usize, draining: bool, + flushing: bool, } impl TtyInputQueue { @@ -52,6 +53,7 @@ impl TtyInputQueue { len: 0, generation: 0, draining: false, + flushing: false, } } @@ -183,6 +185,20 @@ pub trait TtyPort: Sync + Send + Debug { ret } + fn receive_buf_termios_locked( + &self, + buf: &[u8], + _flags: &[u8], + count: usize, + ) -> Result { + let tty = self.port_data().internal_tty().ok_or(SystemError::ENODEV)?; + let ld = tty.ldisc(); + // N_TTY wakes read waiters and epoll when it commits input. Avoid a + // nested poll here: poll may try to drain PTY input and reacquire the + // termios semaphore already held by the caller. + ld.receive_buf2_termios_locked(tty, buf, None, count) + } + fn internal_tty(&self) -> Option> { self.port_data().internal_tty() } @@ -203,6 +219,15 @@ pub trait TtyPort: Sync + Send + Debug { fn clear_input(&self) -> usize; + /// Stop new queue-to-ldisc drains and wait for an in-flight drain to + /// finish. The caller must pair this with `finish_input_flush`. + fn begin_input_flush(&self); + + /// Clear the queue while an input flush gate is held. + fn clear_input_during_flush(&self) -> usize; + + fn finish_input_flush(&self); + fn clear_input_from_receive(&self) -> usize; fn drain_input_to_ldisc(&self, max_count: usize) -> Result; @@ -300,6 +325,35 @@ impl TtyPort for DefaultTtyPort { }) } + fn begin_input_flush(&self) { + self.input_drain_wq.wait_until(|| { + let mut queue = self.input_queue.lock_irqsave(); + let Some(queue) = queue.as_mut() else { + return Some(()); + }; + if queue.draining || queue.flushing { + return None; + } + queue.flushing = true; + Some(()) + }); + } + + fn clear_input_during_flush(&self) -> usize { + self.input_queue + .lock_irqsave() + .as_mut() + .map(TtyInputQueue::clear_buffer) + .unwrap_or(0) + } + + fn finish_input_flush(&self) { + if let Some(queue) = self.input_queue.lock_irqsave().as_mut() { + queue.flushing = false; + } + self.input_drain_wq.wakeup_all(None); + } + fn clear_input_from_receive(&self) -> usize { self.input_queue .lock_irqsave() @@ -316,6 +370,13 @@ impl TtyPort for DefaultTtyPort { let Some(queue) = queue.as_mut() else { return Ok(TtyInputDrain::default()); }; + if queue.flushing { + return Ok(TtyInputDrain { + still_pending: !queue.is_empty(), + blocked: true, + ..TtyInputDrain::default() + }); + } let (copied, generation) = queue.copy_front(&mut chunk[..max_count]); if copied != 0 { queue.draining = true; diff --git a/kernel/src/driver/tty/virtual_terminal/mod.rs b/kernel/src/driver/tty/virtual_terminal/mod.rs index f048d3ece..d7f3ff405 100644 --- a/kernel/src/driver/tty/virtual_terminal/mod.rs +++ b/kernel/src/driver/tty/virtual_terminal/mod.rs @@ -10,12 +10,9 @@ use system_error::SystemError; use unified_init::macros::unified_init; use crate::{ - driver::{ - base::device::{ - device_number::{DeviceNumber, Major}, - device_register, IdTable, - }, - serial::serial8250::send_to_default_serial8250_port, + driver::base::device::{ + device_number::{DeviceNumber, Major}, + device_register, IdTable, }, filesystem::devfs::{devfs_register, devfs_unregister}, init::initcall::INITCALL_LATE, @@ -436,7 +433,6 @@ impl TtyOperation for TtyConsoleDriverInner { // if String::from_utf8_lossy(buf) == "Hello world!\n" { // loop {} // } - send_to_default_serial8250_port(buf); let ret = tty.do_write(buf, nr); self.flush_chars(tty); ret diff --git a/kernel/src/filesystem/vfs/fasync.rs b/kernel/src/filesystem/vfs/fasync.rs index 9862b20ed..ff05a4164 100644 --- a/kernel/src/filesystem/vfs/fasync.rs +++ b/kernel/src/filesystem/vfs/fasync.rs @@ -19,7 +19,7 @@ use crate::{ use alloc::sync::Arc; use system_error::SystemError; -use super::file::{File, FileFlags}; +use super::file::File; pub const FASYNC_POLL_IN: i64 = 0x00000001 | 0x00000040; pub const FASYNC_POLL_OUT: i64 = 0x00000004 | 0x00000100 | 0x00000200; @@ -77,8 +77,11 @@ pub struct FAsyncItem { } impl FAsyncItem { - pub fn new(file: Weak, fd: i32) -> Self { - Self { file, fd } + pub fn new(file: &Arc, fd: i32) -> Self { + Self { + file: Arc::downgrade(file), + fd, + } } /// Get the file reference @@ -132,6 +135,7 @@ impl FAsyncItems { /// Add a FAsyncItem pub fn add(&self, item: FAsyncItem) { let mut guard = self.items.lock(); + guard.retain(FAsyncItem::is_alive); for old_item in guard.iter_mut() { if Weak::ptr_eq(old_item.file_weak(), item.file_weak()) { old_item.set_fd(item.fd()); @@ -147,10 +151,28 @@ impl FAsyncItems { guard.retain(|item| !Weak::ptr_eq(item.file_weak(), file)); } - /// Clear all items - #[allow(dead_code)] + /// Remove a registration while the last strong File reference is being + /// dropped and a new Weak cannot be constructed. + pub fn remove_file(&self, file: &File) { + let file_ptr = file as *const File; + self.items + .lock() + .retain(|item| item.file_weak().as_ptr() != file_ptr); + } + + /// Remove every registration and clear FASYNC on each live open file + /// description. Drain the list before taking per-file fasync locks to + /// preserve the update path's fasync-lock -> item-list lock order. pub fn clear(&self) { - self.items.lock().clear(); + let items = { + let mut guard = self.items.lock(); + core::mem::take(&mut *guard) + }; + for item in items { + if let Some(file) = item.file() { + file.clear_fasync_flag(); + } + } } /// Send SIGIO to all registered file owners @@ -218,25 +240,42 @@ impl FAsyncItems { } } -pub fn set_file_fasync(file: &Arc, fd: i32, enabled: bool) -> Result<(), SystemError> { - let mut flags = file.flags(); - if enabled { - flags.insert(FileFlags::FASYNC); - } else { - flags.remove(FileFlags::FASYNC); - } - - file.set_flags(flags)?; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FAsyncHandlerPolicy { + Optional, + Required, +} - if let Ok(pollable) = file.inode().as_pollable_inode() { - let private_data = file.private_data.lock(); - if enabled { - let item = FAsyncItem::new(Arc::downgrade(file), fd); - let _ = pollable.add_fasync(item, &private_data); +pub fn set_file_fasync( + file: &Arc, + fd: i32, + enabled: bool, + handler_policy: FAsyncHandlerPolicy, +) -> Result<(), SystemError> { + file.update_fasync_flag(enabled, || { + if let Ok(pollable) = file.inode().as_pollable_inode() { + let private_data = file.private_data.lock(); + let result = if enabled { + let item = FAsyncItem::new(file, fd); + pollable.add_fasync(item, &private_data) + } else { + pollable.remove_fasync(&Arc::downgrade(file), &private_data) + }; + if let Err(err) = result { + if err != SystemError::ENOSYS { + return Err(err); + } + if handler_policy == FAsyncHandlerPolicy::Required { + return Err(SystemError::ENOTTY); + } + Ok(false) + } else { + Ok(true) + } + } else if handler_policy == FAsyncHandlerPolicy::Required { + Err(SystemError::ENOTTY) } else { - let _ = pollable.remove_fasync(&Arc::downgrade(file), &private_data); + Ok(false) } - } - - Ok(()) + }) } diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index d4e6fd010..be8b08daa 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -584,6 +584,8 @@ pub struct File { offset: AtomicUsize, /// 文件的打开模式 flags: RwSem, + /// Serializes fasync registration with lifecycle-driven flag removal. + fasync_lock: Mutex<()>, /// 文件的访问模式 mode: RwSem, /// 文件类型 @@ -1123,6 +1125,7 @@ impl File { io_fs, offset: AtomicUsize::new(0), flags: RwSem::new(flags), + fasync_lock: Mutex::new(()), mode: RwSem::new(mode), file_type, readdir_state: Mutex::new(ReaddirState::default()), @@ -1849,6 +1852,7 @@ impl File { io_fs: self.io_fs.clone(), offset: AtomicUsize::new(self.offset.load(Ordering::SeqCst)), flags: RwSem::new(flags), + fasync_lock: Mutex::new(()), mode: RwSem::new(mode), file_type: self.file_type, readdir_state: Mutex::new(self.readdir_state.lock().clone()), @@ -1935,6 +1939,11 @@ impl File { // todo: 是否需要调用inode的open方法,以更新private data(假如它与flags有关的话)? // 也许需要加个更好的设计,让inode知晓文件的打开模式发生了变化,让它自己决定是否需要更新private data + // Preserve lifecycle-driven FASYNC changes while merging the other + // status flags. This lock is also held by fasync registration and TTY + // hangup cleanup. + let _fasync_guard = self.fasync_lock.lock(); + // 访问模式不可修改 let old_flags = self.flags(); let old_accflags = old_flags.access_flags(); @@ -1948,8 +1957,7 @@ impl File { const SETFL_MASK: u32 = FileFlags::O_APPEND.bits() | FileFlags::O_NONBLOCK.bits() | FileFlags::O_DIRECT.bits() - | FileFlags::O_NOATIME.bits() - | FileFlags::FASYNC.bits(); + | FileFlags::O_NOATIME.bits(); let new_bits = (new_flags.bits() & SETFL_MASK) | (old_flags.bits() & !SETFL_MASK); new_flags = FileFlags::from_bits_truncate(new_bits); @@ -1960,6 +1968,39 @@ impl File { return Ok(()); } + /// Serialize a fasync registration update with the corresponding status + /// bit commit. + /// + /// Keeping the dedicated fasync lock across the inode callback lets inode + /// lifecycle paths (notably TTY hangup) clear a registered item and its + /// FASYNC bit without racing a delayed post-callback commit. + pub(crate) fn update_fasync_flag(&self, enabled: bool, update: F) -> Result<(), SystemError> + where + F: FnOnce() -> Result, + { + let _guard = self.fasync_lock.lock(); + if self.flags().contains(FileFlags::FASYNC) == enabled { + return Ok(()); + } + + if update()? { + let mut flags = self.flags.write(); + if enabled { + flags.insert(FileFlags::FASYNC); + } else { + flags.remove(FileFlags::FASYNC); + } + } + Ok(()) + } + + /// Clear the handler-owned FASYNC status bit during inode teardown. + pub(crate) fn clear_fasync_flag(&self) { + let _guard = self.fasync_lock.lock(); + let mut flags = self.flags.write(); + flags.remove(FileFlags::FASYNC); + } + /// @brief 重新设置文件的大小 /// /// 如果文件大小增加,则文件内容不变,但是文件的空洞部分会被填充为0 @@ -2116,6 +2157,13 @@ impl Drop for File { let _ = self.remove_epitem(&epitem); } + if self.flags().contains(FileFlags::FASYNC) { + if let Ok(pollable) = self.inode.as_pollable_inode() { + let private_data = self.private_data.lock(); + let _ = pollable.release_fasync(self, &private_data); + } + } + super::flock::release_all_for_file(self); if self.mode.read().contains(FileMode::FMODE_WRITER) { if let Some(mnt_inode) = self.inode.clone().downcast_arc::() { diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 6d6bfc456..50306a793 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -382,6 +382,15 @@ pub trait PollableInode: Any + Sync + Send + Debug + CastFromSync { // Default implementation: not supported Err(SystemError::ENOSYS) } + + /// Remove fasync state during final open-file-description release. + fn release_fasync( + &self, + _file: &file::File, + _private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + Err(SystemError::ENOSYS) + } } pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { @@ -990,8 +999,10 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { _data: usize, _private_data: MutexGuard, ) -> Result { - // 若文件系统没有实现此方法,则返回"不支持" - return Err(SystemError::ENOSYS); + // Match Linux vfs_ioctl(): a file without an ioctl handler does not + // support the command. sys_ioctl translates this internal error to + // the userspace-visible ENOTTY. + Err(SystemError::ENOIOCTLCMD) } /// @brief 获取inode所在的文件系统的指针 diff --git a/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs b/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs index 352c44619..bfdbfa5ba 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs @@ -1,6 +1,6 @@ use crate::arch::ipc::signal::Signal; use crate::arch::syscall::nr::SYS_FCNTL; -use crate::filesystem::vfs::fasync::set_file_fasync; +use crate::filesystem::vfs::fasync::{set_file_fasync, FAsyncHandlerPolicy}; use crate::filesystem::vfs::FileType; use crate::filesystem::vfs::InodeFlags; use crate::ipc::pipe::LockedPipeInode; @@ -189,9 +189,27 @@ impl SysFcntlHandle { } let old_fasync = current_flags.contains(FileFlags::FASYNC); let new_fasync = new_flags.contains(FileFlags::FASYNC); - file.set_flags(new_flags)?; + + // Apply every potentially failing non-FASYNC flag update + // before invoking the fasync handler. Linux likewise + // validates flags first and lets the handler own FASYNC. + let mut non_fasync_flags = new_flags; + if old_fasync { + non_fasync_flags.insert(FileFlags::FASYNC); + } else { + non_fasync_flags.remove(FileFlags::FASYNC); + } + file.set_flags(non_fasync_flags)?; + if old_fasync != new_fasync { - set_file_fasync(&file, fd, new_fasync)?; + if let Err(err) = + set_file_fasync(&file, fd, new_fasync, FAsyncHandlerPolicy::Optional) + { + // A failed fasync transition must not commit the + // other status flags from this F_SETFL request. + file.set_flags(current_flags)?; + return Err(err); + } } // Keep socket object nonblocking state in sync with file flags. diff --git a/kernel/src/filesystem/vfs/syscall/sys_ioctl.rs b/kernel/src/filesystem/vfs/syscall/sys_ioctl.rs index 1e489f610..b3796ffa8 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_ioctl.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_ioctl.rs @@ -2,7 +2,7 @@ use crate::arch::interrupt::TrapFrame; use crate::arch::syscall::nr::SYS_IOCTL; -use crate::filesystem::vfs::fasync::set_file_fasync; +use crate::filesystem::vfs::fasync::{set_file_fasync, FAsyncHandlerPolicy}; use crate::filesystem::vfs::file::File; use crate::filesystem::vfs::file::FileFlags; use crate::syscall::table::FormattedSyscallParam; @@ -168,7 +168,7 @@ impl SysIoctlHandle { UserBufferReader::new(data as *const i32, core::mem::size_of::(), true)?; let value = user_reader.buffer_protected(0)?.read_one::(0)?; - set_file_fasync(file, fd, value != 0)?; + set_file_fasync(file, fd, value != 0, FAsyncHandlerPolicy::Required)?; Ok(0) } diff --git a/kernel/src/ipc/pipe.rs b/kernel/src/ipc/pipe.rs index 202eca4f9..0d24f6371 100644 --- a/kernel/src/ipc/pipe.rs +++ b/kernel/src/ipc/pipe.rs @@ -1174,6 +1174,26 @@ impl PollableInode for LockedPipeInode { } Ok(()) } + + fn release_fasync( + &self, + file: &crate::filesystem::vfs::file::File, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + if let FilePrivateData::Pipefs(pipe_data) = private_data { + let flags = pipe_data.flags; + if !flags.is_write_only() { + self.read_fasync_items.remove_file(file); + } + if !flags.is_read_only() { + self.write_fasync_items.remove_file(file); + } + } else { + self.read_fasync_items.remove_file(file); + self.write_fasync_items.remove_file(file); + } + Ok(()) + } } impl IndexNode for LockedPipeInode { diff --git a/kernel/src/net/socket/inode.rs b/kernel/src/net/socket/inode.rs index 880d039d9..4d60447b9 100644 --- a/kernel/src/net/socket/inode.rs +++ b/kernel/src/net/socket/inode.rs @@ -462,4 +462,9 @@ impl PollableInode for T { self.fasync_items().remove(file); Ok(()) } + + fn release_fasync(&self, file: &File, _: &FilePrivateData) -> Result<(), SystemError> { + self.fasync_items().remove_file(file); + Ok(()) + } } diff --git a/user/apps/c_unitest/test_tty_termios.c b/user/apps/c_unitest/test_tty_termios.c new file mode 100644 index 000000000..3f62b0fae --- /dev/null +++ b/user/apps/c_unitest/test_tty_termios.c @@ -0,0 +1,199 @@ +// test_tty_termios.c — verify TCSAFLUSH / TCSADRAIN and legacy termio ioctls +// on a valid TTY fd (PTY slave). +// +// Regression coverage for: "tcsetattr(0, TCSAFLUSH, &t) fails with ENOTTY" +// and TCSETA/TCSETAW/TCSETAF/TCGETA returning ENOIOCTLCMD. +// +// This file intentionally overlaps with dunitest/suites/normal/tty_termios.cc. +// The C version is a fast, self-contained static binary for QEMU smoke testing. +// The C++ gtest version is the full CI regression suite. Keep both: the C test +// catches regressions in environments where dunitest cannot run. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Legacy SVR4 struct termio — not exposed by glibc's . */ +#define NCC 8 +struct termio_compat { + unsigned short c_iflag; + unsigned short c_oflag; + unsigned short c_cflag; + unsigned short c_lflag; + unsigned char c_line; + unsigned char c_cc[NCC]; + unsigned char _pad; /* match kernel PosixTermio layout */ +}; + +#ifndef TCGETA +#define TCGETA 0x5405 +#endif +#ifndef TCSETA +#define TCSETA 0x5406 +#endif +#ifndef TCSETAW +#define TCSETAW 0x5407 +#endif +#ifndef TCSETAF +#define TCSETAF 0x5408 +#endif + +static int failures = 0; + +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (errno=%d: %s)\n", msg, errno, \ + strerror(errno)); \ + failures++; \ + } else { \ + printf("ok: %s\n", msg); \ + } \ + } while (0) + +/* Like CHECK, but for assertions that don't follow a syscall — + * errno is irrelevant and printing it is misleading. */ +#define CHECK_NOERR(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", msg); \ + failures++; \ + } else { \ + printf("ok: %s\n", msg); \ + } \ + } while (0) + +int main(void) { + int ptm = -1, pts = -1; + + if (openpty(&ptm, &pts, NULL, NULL, NULL) == -1) { + perror("openpty"); + return EXIT_FAILURE; + } + + struct termios t = {}; + + /* 1. tcgetattr + tcsetattr(TCSANOW) round-trip. */ + CHECK(tcgetattr(pts, &t) == 0, "tcgetattr on PTY slave"); + t.c_lflag &= ~(ICANON | ECHO); + CHECK(tcsetattr(pts, TCSANOW, &t) == 0, "tcsetattr TCSANOW"); + struct termios back; + CHECK(tcgetattr(pts, &back) == 0, "tcgetattr after TCSANOW"); + CHECK_NOERR((back.c_lflag & (ICANON | ECHO)) == 0, "TCSANOW settings survive"); + + /* 2. tcsetattr TCSADRAIN — must not fail with ENOTTY. + * TODO: add a stress test where master writes data → slave TCSADRAIN + * → verify drain actually waited for output to complete. */ + CHECK(tcsetattr(pts, TCSADRAIN, &t) == 0, "tcsetattr TCSADRAIN succeeds"); + + /* 3. tcsetattr TCSAFLUSH — the dpkg/apt regression. */ + CHECK(tcsetattr(pts, TCSAFLUSH, &t) == 0, "tcsetattr TCSAFLUSH succeeds"); + CHECK(tcgetattr(pts, &back) == 0, "tcgetattr after TCSAFLUSH"); + CHECK_NOERR((back.c_lflag & (ICANON | ECHO)) == 0, "TCSAFLUSH settings survive"); + + /* 4. Legacy termio ioctls — must not return ENOTTY. */ + struct termio_compat tio; + memset(&tio, 0, sizeof(tio)); + + errno = 0; + CHECK(ioctl(pts, TCGETA, &tio) == 0, "ioctl TCGETA succeeds"); + + tio.c_lflag |= ISIG; + errno = 0; + CHECK(ioctl(pts, TCSETA, &tio) == 0, "ioctl TCSETA succeeds"); + /* Verify TCSETA was applied by reading back via TCGETA. */ + { + struct termio_compat rdback; + memset(&rdback, 0, sizeof(rdback)); + CHECK(ioctl(pts, TCGETA, &rdback) == 0, "TCGETA after TCSETA"); + CHECK_NOERR((rdback.c_lflag & ISIG) != 0, "TCSETA ISIG flag applied"); + } + + tio.c_lflag &= ~(unsigned short)ISIG; + errno = 0; + CHECK(ioctl(pts, TCSETAW, &tio) == 0, "ioctl TCSETAW succeeds"); + /* Verify TCSETAW was applied. */ + { + struct termio_compat rdback; + memset(&rdback, 0, sizeof(rdback)); + CHECK(ioctl(pts, TCGETA, &rdback) == 0, "TCGETA after TCSETAW"); + CHECK_NOERR((rdback.c_lflag & ISIG) == 0, "TCSETAW cleared ISIG"); + } + + tio.c_lflag |= ISIG; + errno = 0; + CHECK(ioctl(pts, TCSETAF, &tio) == 0, "ioctl TCSETAF succeeds"); + /* Verify TCSETAF was applied. */ + { + struct termio_compat rdback; + memset(&rdback, 0, sizeof(rdback)); + CHECK(ioctl(pts, TCGETA, &rdback) == 0, "TCGETA after TCSETAF"); + CHECK_NOERR((rdback.c_lflag & ISIG) != 0, "TCSETAF ISIG flag applied"); + } + /* 5. Cross-check: TCGETA reports low 16 bits of what TCGETS reports. */ + struct termios full = {}; + memset(&tio, 0, sizeof(tio)); + CHECK(tcgetattr(pts, &full) == 0, "tcgetattr for cross-check"); + CHECK(ioctl(pts, TCGETA, &tio) == 0, "TCGETA for cross-check"); + CHECK_NOERR((full.c_lflag & 0xffff) == tio.c_lflag, "termio c_lflag is low 16 bits"); + CHECK_NOERR((full.c_iflag & 0xffff) == tio.c_iflag, "termio c_iflag is low 16 bits"); + CHECK_NOERR((full.c_cflag & 0xffff) == tio.c_cflag, "termio c_cflag is low 16 bits"); + CHECK_NOERR((full.c_oflag & 0xffff) == tio.c_oflag, "termio c_oflag is low 16 bits"); + + /* c_line round-trip: verify that a non-zero c_line value set + * through TCSETA is preserved when read back via TCGETA. The + * kernel's c_line_abi field retains the raw ABI byte even when + * LineDisciplineType::from_line falls back to N_TTY. */ + memset(&tio, 0, sizeof(tio)); + tio.c_line = 42; + CHECK(ioctl(pts, TCSETA, &tio) == 0, "TCSETA with c_line=42"); + struct termio_compat tio2; + memset(&tio2, 0, sizeof(tio2)); + CHECK(ioctl(pts, TCGETA, &tio2) == 0, "TCGETA after c_line=42"); + CHECK_NOERR(tio2.c_line == 42, "c_line preserved through termio round-trip"); + + /* 6. termio round-trip preserves merge semantics: high bits of c_cflag + * (e.g. CIBAUD/ADDRB region) set via termios survive a TCSETA call. */ + CHECK(tcgetattr(pts, &full) == 0, "tcgetattr before TCSETA merge check"); + tcflag_t orig_cflag = full.c_cflag; + memset(&tio, 0, sizeof(tio)); + CHECK(ioctl(pts, TCGETA, &tio) == 0, "TCGETA before TCSETA merge check"); + /* Set a flag bit we know is currently clear so the subsequent + * clear of that bit is a real operation, not a no-op. */ + tio.c_lflag |= ECHO; + CHECK(ioctl(pts, TCSETA, &tio) == 0, "TCSETA set ECHO before merge check"); + tio.c_lflag &= ~(unsigned short)ECHO; /* flip a low-16-bit flag */ + CHECK(ioctl(pts, TCSETA, &tio) == 0, "TCSETA merge apply"); + CHECK(tcgetattr(pts, &full) == 0, "tcgetattr after TCSETA merge check"); + CHECK_NOERR((full.c_cflag & 0xffff0000u) == (orig_cflag & 0xffff0000u), + "TCSETA preserves high 16 bits of c_cflag"); + CHECK_NOERR((full.c_lflag & ECHO) == 0, "TCSETA applied low-16-bit change"); + + /* 7. Linux reports ENOTTY for a termios ioctl on a non-TTY fd. */ + int nullfd = open("/dev/null", O_RDWR); + if (nullfd >= 0) { + errno = 0; + int rc = tcsetattr(nullfd, TCSANOW, &t); + CHECK(rc == -1 && errno == ENOTTY, + "tcsetattr on /dev/null fails with ENOTTY"); + close(nullfd); + } else { + printf("skip: cannot open /dev/null (errno=%d: %s)\n", errno, strerror(errno)); + } + + close(ptm); + close(pts); + + if (failures) { + fprintf(stderr, "%d test(s) FAILED\n", failures); + return EXIT_FAILURE; + } + printf("all tty termios tests passed\n"); + return EXIT_SUCCESS; +} diff --git a/user/apps/tests/dunitest/suites/normal/fcntl_signal.cc b/user/apps/tests/dunitest/suites/normal/fcntl_signal.cc index afdffc4a1..03d27e11b 100644 --- a/user/apps/tests/dunitest/suites/normal/fcntl_signal.cc +++ b/user/apps/tests/dunitest/suites/normal/fcntl_signal.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -87,6 +88,26 @@ TEST(FcntlSignal, GetSigDefaultsToZero) { close(fds[1]); } +TEST(FcntlSignal, UnsupportedFasyncDoesNotCommitFlag) { + int fd = open("/dev/null", O_RDONLY); + ASSERT_GE(fd, 0); + + int before = fcntl(fd, F_GETFL); + ASSERT_GE(before, 0); + ASSERT_EQ(0, fcntl(fd, F_SETFL, before | O_ASYNC)); + + int after = fcntl(fd, F_GETFL); + ASSERT_GE(after, 0); + EXPECT_EQ(0, after & O_ASYNC); + + int enabled = 1; + errno = 0; + EXPECT_EQ(-1, ioctl(fd, FIOASYNC, &enabled)); + EXPECT_EQ(ENOTTY, errno); + + close(fd); +} + TEST(FcntlSignal, SetSigRoundTripAndValidation) { int fds[2] = {-1, -1}; ASSERT_EQ(0, pipe(fds)); diff --git a/user/apps/tests/dunitest/suites/normal/tty_pty_hangup.cc b/user/apps/tests/dunitest/suites/normal/tty_pty_hangup.cc index cdaf0eaf0..7be8d371b 100644 --- a/user/apps/tests/dunitest/suites/normal/tty_pty_hangup.cc +++ b/user/apps/tests/dunitest/suites/normal/tty_pty_hangup.cc @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -1114,7 +1115,8 @@ TEST(TtyPtyHangup, MasterCloseMakesSlaveObserveHangup) { pair.master.reset(); short revents = PollEvents(pair.slave.get()); - EXPECT_NE(0, revents & POLLHUP); + constexpr short kHungUpEvents = POLLIN | POLLOUT | POLLERR | POLLHUP; + EXPECT_EQ(kHungUpEvents, revents); char ch = 0; errno = 0; @@ -1124,6 +1126,90 @@ TEST(TtyPtyHangup, MasterCloseMakesSlaveObserveHangup) { EXPECT_EQ(-1, write(pair.slave.get(), "x", 1)); EXPECT_EQ(EIO, errno) << "slave write after master close errno=" << errno << " (" << strerror(errno) << ")"; + + int async = 1; + errno = 0; + EXPECT_EQ(-1, ioctl(pair.slave.get(), FIOASYNC, &async)); + EXPECT_EQ(ENOTTY, errno) << "slave FIOASYNC after master close errno=" << errno << " (" + << strerror(errno) << ")"; + + int flags = fcntl(pair.slave.get(), F_GETFL); + ASSERT_GE(flags, 0); + errno = 0; + EXPECT_EQ(-1, fcntl(pair.slave.get(), F_SETFL, flags | O_ASYNC)); + EXPECT_EQ(ENOTTY, errno) << "slave F_SETFL(O_ASYNC) after master close errno=" << errno << " (" + << strerror(errno) << ")"; +} + +TEST(TtyPtyHangup, HangupClearsAsyncState) { + PtyPair pair = OpenRawPty(); + ASSERT_GE(pair.master.get(), 0); + ASSERT_GE(pair.slave.get(), 0); + + int flags = fcntl(pair.slave.get(), F_GETFL); + ASSERT_GE(flags, 0); + ASSERT_EQ(0, fcntl(pair.slave.get(), F_SETFL, flags | O_ASYNC)); + ASSERT_NE(0, fcntl(pair.slave.get(), F_GETFL) & O_ASYNC); + + pair.master.reset(); + + flags = fcntl(pair.slave.get(), F_GETFL); + ASSERT_GE(flags, 0); + EXPECT_EQ(0, flags & O_ASYNC); + + int async = 0; + EXPECT_EQ(0, ioctl(pair.slave.get(), FIOASYNC, &async)); + EXPECT_EQ(0, fcntl(pair.slave.get(), F_SETFL, flags & ~O_ASYNC)); +} + +struct AsyncFlagRaceArgs { + int fd; + int async_flags; + std::atomic stop{false}; + std::atomic unexpected_errno{0}; +}; + +void* ToggleNonblockAcrossHangup(void* opaque) { + auto* args = static_cast(opaque); + bool nonblock = false; + while (!args->stop.load(std::memory_order_acquire)) { + nonblock = !nonblock; + int flags = args->async_flags; + if (nonblock) { + flags |= O_NONBLOCK; + } else { + flags &= ~O_NONBLOCK; + } + if (fcntl(args->fd, F_SETFL, flags) < 0 && errno != ENOTTY) { + args->unexpected_errno.store(errno, std::memory_order_release); + break; + } + } + return nullptr; +} + +TEST(TtyPtyHangup, ConcurrentStatusFlagUpdateCannotRestoreAsync) { + PtyPair pair = OpenRawPty(); + ASSERT_GE(pair.master.get(), 0); + ASSERT_GE(pair.slave.get(), 0); + + int flags = fcntl(pair.slave.get(), F_GETFL); + ASSERT_GE(flags, 0); + ASSERT_EQ(0, fcntl(pair.slave.get(), F_SETFL, flags | O_ASYNC)); + + AsyncFlagRaceArgs args{pair.slave.get(), flags | O_ASYNC}; + pthread_t updater; + ASSERT_EQ(0, pthread_create(&updater, nullptr, ToggleNonblockAcrossHangup, &args)); + usleep(1000); + pair.master.reset(); + usleep(1000); + args.stop.store(true, std::memory_order_release); + ASSERT_EQ(0, pthread_join(updater, nullptr)); + + EXPECT_EQ(0, args.unexpected_errno.load(std::memory_order_acquire)); + flags = fcntl(pair.slave.get(), F_GETFL); + ASSERT_GE(flags, 0); + EXPECT_EQ(0, flags & O_ASYNC); } TEST(TtyPtyHangup, ChildExitDrainsSlaveOutputBeforeMasterEio) { diff --git a/user/apps/tests/dunitest/suites/normal/tty_termios.cc b/user/apps/tests/dunitest/suites/normal/tty_termios.cc new file mode 100644 index 000000000..1fb6d2d4b --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/tty_termios.cc @@ -0,0 +1,656 @@ +// tty_termios.cc — verify TCSAFLUSH / TCSADRAIN and legacy termio ioctls +// on a valid TTY fd (PTY slave). +// +// Regression coverage for: "tcsetattr(0, TCSAFLUSH, &t) fails with ENOTTY" +// and TCSETA/TCSETAW/TCSETAF/TCGETA returning ENOIOCTLCMD. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +/* Legacy SVR4 struct termio — not exposed by glibc's . */ +constexpr int kNcc = 8; +struct TermioCompat { + unsigned short c_iflag; + unsigned short c_oflag; + unsigned short c_cflag; + unsigned short c_lflag; + unsigned char c_line; + unsigned char c_cc[kNcc]; + unsigned char _pad = 0; /* match kernel PosixTermio layout */ +}; + +#ifndef TCGETA +#define TCGETA 0x5405 +#endif +#ifndef TCSETA +#define TCSETA 0x5406 +#endif +#ifndef TCSETAW +#define TCSETAW 0x5407 +#endif +#ifndef TCSETAF +#define TCSETAF 0x5408 +#endif + +class UniqueFd { +public: + UniqueFd() = default; + explicit UniqueFd(int fd) : fd_(fd) {} + UniqueFd(const UniqueFd&) = delete; + UniqueFd& operator=(const UniqueFd&) = delete; + + UniqueFd(UniqueFd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + + UniqueFd& operator=(UniqueFd&& other) noexcept { + if (this != &other) { + reset(); + fd_ = other.fd_; + other.fd_ = -1; + } + return *this; + } + + ~UniqueFd() { reset(); } + + int get() const { return fd_; } + + void reset(int fd = -1) { + if (fd_ >= 0) { + close(fd_); + } + fd_ = fd; + } + +private: + int fd_ = -1; +}; + +class TermiosRestorer { +public: + TermiosRestorer(int fd, const struct termios& term) : fd_(fd), term_(term) {} + TermiosRestorer(const TermiosRestorer&) = delete; + TermiosRestorer& operator=(const TermiosRestorer&) = delete; + ~TermiosRestorer() { tcsetattr(fd_, TCSANOW, &term_); } + +private: + int fd_; + struct termios term_; +}; + +struct PtyPair { + UniqueFd master; + UniqueFd slave; +}; + +PtyPair OpenRawPty() { + int master = -1; + int slave = -1; + if (openpty(&master, &slave, nullptr, nullptr, nullptr) < 0) { + ADD_FAILURE() << "openpty failed: errno=" << errno << " (" << strerror(errno) << ")"; + return {}; + } + + PtyPair pair{UniqueFd(master), UniqueFd(slave)}; + + struct termios term = {}; + if (tcgetattr(pair.slave.get(), &term) < 0) { + ADD_FAILURE() << "tcgetattr failed: errno=" << errno << " (" << strerror(errno) << ")"; + return {}; + } + + term.c_iflag = 0; + term.c_oflag = 0; + term.c_lflag = 0; + term.c_cflag |= CS8; + term.c_cc[VMIN] = 1; + term.c_cc[VTIME] = 0; + + if (tcsetattr(pair.slave.get(), TCSANOW, &term) < 0) { + ADD_FAILURE() << "tcsetattr failed: errno=" << errno << " (" << strerror(errno) << ")"; + } + + return pair; +} + +struct TcsetattrThreadArgs { + int fd = -1; + int action = TCSANOW; + struct termios term = {}; + std::atomic started{false}; + std::atomic done{false}; + int rc = -1; + int saved_errno = 0; +}; + +void* TcsetattrThread(void* opaque) { + auto* args = static_cast(opaque); + args->started.store(true, std::memory_order_release); + args->rc = tcsetattr(args->fd, args->action, &args->term); + args->saved_errno = errno; + args->done.store(true, std::memory_order_release); + return nullptr; +} + +bool WaitForFlag(const std::atomic& flag, int timeout_ms) { + for (int elapsed = 0; elapsed < timeout_ms; ++elapsed) { + if (flag.load(std::memory_order_acquire)) { + return true; + } + usleep(1000); + } + return flag.load(std::memory_order_acquire); +} + +/* -------------------------------------------------------------------------- + * tcgetattr + tcsetattr(TCSANOW) round-trip + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, TcSanowRoundTrip) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &t), 0) << strerror(errno); + + tcflag_t orig = t.c_lflag; + t.c_lflag &= ~(ICANON | ECHO); + ASSERT_EQ(tcsetattr(pty.slave.get(), TCSANOW, &t), 0) << strerror(errno); + + struct termios back = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &back), 0) << strerror(errno); + EXPECT_EQ(back.c_lflag & (ICANON | ECHO), 0u) + << "TCSANOW settings should survive"; +} + +/* -------------------------------------------------------------------------- + * tcsetattr TCSADRAIN — must not fail with ENOTTY. + * TODO: add a stress test where master writes data → slave TCSADRAIN + * → verify drain actually waited for output to complete. + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, TcsadrainSucceeds) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &t), 0) << strerror(errno); + EXPECT_EQ(tcsetattr(pty.slave.get(), TCSADRAIN, &t), 0) << strerror(errno); +} + +/* + * DragonOS keeps bytes that the peer N_TTY buffer cannot yet accept in a + * driver-owned PTY queue analogous to Linux's peer flip-buffer. TCSADRAIN must + * not wait for either layer of the peer input path to be consumed. + */ +TEST(TtyTermios, TcsadrainDoesNotWaitForPtyInputBacklog) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &t), 0) << strerror(errno); + + int slave_flags = fcntl(pty.slave.get(), F_GETFL, 0); + ASSERT_GE(slave_flags, 0); + ASSERT_EQ(fcntl(pty.slave.get(), F_SETFL, slave_flags | O_NONBLOCK), 0); + + char fill[1024]; + memset(fill, 'q', sizeof(fill)); + size_t accepted = 0; + for (;;) { + ssize_t n = write(pty.slave.get(), fill, sizeof(fill)); + if (n > 0) { + accepted += static_cast(n); + continue; + } + ASSERT_EQ(n, -1); + ASSERT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK) << strerror(errno); + break; + } + ASSERT_GT(accepted, 0u); + + TcsetattrThreadArgs args; + args.fd = pty.slave.get(); + args.action = TCSADRAIN; + args.term = t; + pthread_t waiter; + ASSERT_EQ(pthread_create(&waiter, nullptr, TcsetattrThread, &args), 0); + if (!WaitForFlag(args.started, 1000)) { + ADD_FAILURE() << "tcsetattr thread did not start"; + pty.master.reset(); + pthread_join(waiter, nullptr); + return; + } + + if (!WaitForFlag(args.done, 1000)) { + ADD_FAILURE() << "TCSADRAIN waited for unread PTY input"; + pty.master.reset(); + } + ASSERT_EQ(pthread_join(waiter, nullptr), 0); + ASSERT_TRUE(args.done.load(std::memory_order_acquire)); + EXPECT_EQ(args.rc, 0) << "errno=" << args.saved_errno << " (" + << strerror(args.saved_errno) << ")"; +} + +TEST(TtyTermios, HungUpSlaveModeIoctlsReturnLinuxErrors) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.master.get(), 0); + ASSERT_GE(pty.slave.get(), 0); + + struct termios term = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &term), 0) << strerror(errno); + TermioCompat legacy = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &legacy), 0) << strerror(errno); + + pty.master.reset(); + + errno = 0; + EXPECT_EQ(tcgetattr(pty.slave.get(), &term), -1); + EXPECT_EQ(errno, EIO); + + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TCGETA, &legacy), -1); + EXPECT_EQ(errno, EIO); + + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TCSETA, &legacy), -1); + EXPECT_EQ(errno, EIO); + + pid_t pgrp = getpgrp(); + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TIOCSPGRP, &pgrp), -1); + EXPECT_EQ(errno, ENOTTY); +} + +TEST(TtyTermios, PtyMasterDrainActionsSucceedAfterSlaveClose) { + for (int action : {TCSADRAIN, TCSAFLUSH}) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.master.get(), 0); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.master.get(), &t), 0) << strerror(errno); + + int slave_flags = fcntl(pty.slave.get(), F_GETFL, 0); + ASSERT_GE(slave_flags, 0); + ASSERT_EQ(fcntl(pty.slave.get(), F_SETFL, slave_flags | O_NONBLOCK), 0); + + char fill[1024]; + memset(fill, 'z', sizeof(fill)); + bool saturated = false; + for (int i = 0; i < 1024; ++i) { + ssize_t n = write(pty.slave.get(), fill, sizeof(fill)); + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + saturated = true; + break; + } + ASSERT_GT(n, 0) << strerror(errno); + } + ASSERT_TRUE(saturated); + pty.slave.reset(); + + TcsetattrThreadArgs args; + args.fd = pty.master.get(); + args.action = action; + args.term = t; + pthread_t waiter; + ASSERT_EQ(pthread_create(&waiter, nullptr, TcsetattrThread, &args), 0); + if (!WaitForFlag(args.started, 1000)) { + ADD_FAILURE() << "tcsetattr thread did not start"; + pty.master.reset(); + pthread_join(waiter, nullptr); + return; + } + if (!WaitForFlag(args.done, 1000)) { + ADD_FAILURE() << "PTY master drain action hung after slave close"; + pty.master.reset(); + } + ASSERT_EQ(pthread_join(waiter, nullptr), 0); + EXPECT_EQ(args.rc, 0) << "action=" << action << " errno=" << args.saved_errno + << " (" << strerror(args.saved_errno) << ")"; + } +} + +/* + * DragonOS N_TTY can retain an echo step when the PTY bridge has no room. + * TCSADRAIN must wait for that ldisc-owned output to be submitted, but it + * must not wait for the master application to consume the entire PTY input. + */ +TEST(TtyTermios, TcsadrainWaitsForRetainedEchoOnly) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &t), 0) << strerror(errno); + t.c_iflag = 0; + t.c_oflag = 0; + t.c_lflag = ECHO | ECHOCTL; + ASSERT_EQ(tcsetattr(pty.slave.get(), TCSANOW, &t), 0) << strerror(errno); + + int slave_flags = fcntl(pty.slave.get(), F_GETFL, 0); + ASSERT_GE(slave_flags, 0); + ASSERT_EQ(fcntl(pty.slave.get(), F_SETFL, slave_flags | O_NONBLOCK), 0); + + // Fill both the peer N_TTY buffer and the PTY bridge so the two-byte + // ECHOCTL rendering remains owned by this line discipline. The bridge + // backlog itself must not keep TCSADRAIN waiting after that echo is + // submitted. + char fill[1024]; + memset(fill, 'x', sizeof(fill)); + size_t accepted = 0; + for (;;) { + ssize_t n = write(pty.slave.get(), fill, sizeof(fill)); + if (n > 0) { + accepted += static_cast(n); + continue; + } + ASSERT_EQ(n, -1); + ASSERT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK) << strerror(errno); + break; + } + ASSERT_GT(accepted, 0u); + + const char control = 0x01; // ECHOCTL renders this as "^A" (two bytes). + ASSERT_EQ(write(pty.master.get(), &control, 1), 1) << strerror(errno); + + // RX delivery and echo generation run asynchronously. Observe the byte on + // the slave before starting tcsetattr so the test cannot pass merely + // because the drain raced ahead of the retained echo step. + char received = 0; + bool input_delivered = false; + for (int i = 0; i < 1000; ++i) { + ssize_t n = read(pty.slave.get(), &received, 1); + if (n == 1) { + input_delivered = true; + break; + } + ASSERT_EQ(n, -1); + ASSERT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK) << strerror(errno); + usleep(1000); + } + ASSERT_TRUE(input_delivered) << "PTY input delivery timed out"; + ASSERT_EQ(received, control); + + int master_flags = fcntl(pty.master.get(), F_GETFL, 0); + ASSERT_GE(master_flags, 0); + ASSERT_EQ(fcntl(pty.master.get(), F_SETFL, master_flags | O_NONBLOCK), 0); + + TcsetattrThreadArgs args; + args.fd = pty.slave.get(); + args.action = TCSADRAIN; + args.term = t; + pthread_t waiter; + ASSERT_EQ(pthread_create(&waiter, nullptr, TcsetattrThread, &args), 0); + if (!WaitForFlag(args.started, 1000)) { + ADD_FAILURE() << "tcsetattr thread did not start"; + pty.master.reset(); + pthread_join(waiter, nullptr); + return; + } + + // With no room for the retained two-byte echo step, the ioctl must not + // report completion. This catches the old bool drain / always-true wait. + EXPECT_FALSE(WaitForFlag(args.done, 20)); + + char echo_room[2]; + for (size_t freed = 0; freed < sizeof(echo_room); ++freed) { + bool got_byte = false; + for (int i = 0; i < 1000; ++i) { + ssize_t n = read(pty.master.get(), &echo_room[freed], 1); + if (n == 1) { + got_byte = true; + break; + } + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { + ADD_FAILURE() << "master read failed: " << strerror(errno); + break; + } + usleep(1000); + } + if (!got_byte) { + ADD_FAILURE() << "failed to free echo byte " << freed; + pty.master.reset(); + pthread_join(waiter, nullptr); + return; + } + if (freed == 0) { + EXPECT_FALSE(WaitForFlag(args.done, 20)); + } + } + EXPECT_TRUE(WaitForFlag(args.done, 1000)); + + if (!args.done.load(std::memory_order_acquire)) { + // Closing the peer guarantees a bounded cleanup path even on failure. + pty.master.reset(); + } + ASSERT_EQ(pthread_join(waiter, nullptr), 0); + ASSERT_TRUE(args.done.load(std::memory_order_acquire)); + EXPECT_EQ(args.rc, 0) << "errno=" << args.saved_errno << " (" + << strerror(args.saved_errno) << ")"; + + // TCSADRAIN waits for the retained echo to be accepted by the PTY driver, + // not for the peer to consume the already accepted backlog. + char backlog = 0; + EXPECT_EQ(read(pty.master.get(), &backlog, 1), 1); +} + +/* -------------------------------------------------------------------------- + * tcsetattr TCSAFLUSH — the dpkg/apt regression + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, TcsaflushSucceeds) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios t = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &t), 0) << strerror(errno); + t.c_lflag &= ~(ICANON | ECHO); + ASSERT_EQ(tcsetattr(pty.slave.get(), TCSAFLUSH, &t), 0) << strerror(errno); + + struct termios back = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &back), 0) << strerror(errno); + EXPECT_EQ(back.c_lflag & (ICANON | ECHO), 0u) + << "TCSAFLUSH settings should survive"; +} + +/* -------------------------------------------------------------------------- + * Legacy termio TCGETA — must not return ENOTTY + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, LegacyTcgeta) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + TermioCompat tio = {}; + errno = 0; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &tio), 0) + << "TCGETA should succeed: errno=" << errno << " (" << strerror(errno) << ")"; +} + +/* -------------------------------------------------------------------------- + * Legacy termio TCSETA / TCSETAW / TCSETAF + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, LegacyTcsetaFamily) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + TermioCompat tio = {}; + tio.c_lflag |= ISIG; + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TCSETA, &tio), 0) + << "TCSETA: errno=" << errno << " (" << strerror(errno) << ")"; + /* Verify TCSETA was applied. */ + { + TermioCompat rdback = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &rdback), 0); + EXPECT_NE(rdback.c_lflag & ISIG, 0u) << "TCSETA ISIG flag applied"; + } + + tio.c_lflag &= ~static_cast(ISIG); + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TCSETAW, &tio), 0) + << "TCSETAW: errno=" << errno << " (" << strerror(errno) << ")"; + /* Verify TCSETAW was applied. */ + { + TermioCompat rdback = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &rdback), 0); + EXPECT_EQ(rdback.c_lflag & ISIG, 0u) << "TCSETAW cleared ISIG"; + } + + tio.c_lflag |= ISIG; + errno = 0; + EXPECT_EQ(ioctl(pty.slave.get(), TCSETAF, &tio), 0) + << "TCSETAF: errno=" << errno << " (" << strerror(errno) << ")"; + /* Verify TCSETAF was applied. */ + { + TermioCompat rdback = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &rdback), 0); + EXPECT_NE(rdback.c_lflag & ISIG, 0u) << "TCSETAF ISIG flag applied"; + } +} + +/* -------------------------------------------------------------------------- + * Cross-check: TCGETA low 16 bits match TCGETS + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, TermioLow16Bits) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios full = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &full), 0); + + TermioCompat tio = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &tio), 0); + + EXPECT_EQ(full.c_lflag & 0xffff, tio.c_lflag) + << "termio c_lflag should be low 16 bits of termios c_lflag"; + EXPECT_EQ(full.c_iflag & 0xffff, tio.c_iflag) + << "termio c_iflag should be low 16 bits of termios c_iflag"; + EXPECT_EQ(full.c_cflag & 0xffff, tio.c_cflag) + << "termio c_cflag should be low 16 bits of termios c_cflag"; + EXPECT_EQ(full.c_oflag & 0xffff, tio.c_oflag) + << "termio c_oflag should be low 16 bits of termios c_oflag"; +} + +/* -------------------------------------------------------------------------- + * c_line round-trip: non-zero c_line survives TCSETA → TCGETA + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, CLineRoundtrip) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + TermioCompat tio = {}; + tio.c_line = 42; + ASSERT_EQ(ioctl(pty.slave.get(), TCSETA, &tio), 0) + << "TCSETA with c_line=42"; + + TermioCompat tio2 = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &tio2), 0); + EXPECT_EQ(tio2.c_line, 42u) + << "c_line should survive termio round-trip"; +} + +/* -------------------------------------------------------------------------- + * Merge semantics: high 16 bits of c_cflag survive a TCSETA call + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, TermioMergeHighBits) { + auto pty = OpenRawPty(); + ASSERT_GE(pty.slave.get(), 0); + + struct termios full = {}; + ASSERT_EQ(tcgetattr(pty.slave.get(), &full), 0); + tcflag_t orig_cflag = full.c_cflag; + + TermioCompat tio = {}; + ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, &tio), 0); + /* Set a flag bit we know is currently clear so the subsequent + * clear of that bit is a real operation, not a no-op. */ + tio.c_lflag |= ECHO; + ASSERT_EQ(ioctl(pty.slave.get(), TCSETA, &tio), 0) + << "TCSETA set ECHO before merge check"; + tio.c_lflag &= ~static_cast(ECHO); /* flip a low-16-bit flag */ + + ASSERT_EQ(ioctl(pty.slave.get(), TCSETA, &tio), 0) + << "TCSETA merge apply: errno=" << errno << " (" << strerror(errno) << ")"; + + ASSERT_EQ(tcgetattr(pty.slave.get(), &full), 0); + EXPECT_EQ(full.c_cflag & 0xffff0000u, orig_cflag & 0xffff0000u) + << "TCSETA should preserve high 16 bits of c_cflag"; + EXPECT_EQ(full.c_lflag & ECHO, 0u) + << "TCSETA should apply low-16-bit change"; +} + +/* -------------------------------------------------------------------------- + * Serial8250 termios must reach the UART hardware callback. Before the + * regression fix the default ENOSYS callback made the TTY core restore all + * hardware-related c_cflag bits even though tcsetattr/ioctl returned success. + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, Serial8250AppliesModernAndLegacySettings) { + UniqueFd serial(open("/dev/ttyS0", O_RDWR | O_NOCTTY)); + if (serial.get() < 0) { + GTEST_SKIP() << "cannot open /dev/ttyS0: " << strerror(errno); + return; + } + + struct termios original = {}; + ASSERT_EQ(tcgetattr(serial.get(), &original), 0) << strerror(errno); + TermiosRestorer restore(serial.get(), original); + + struct termios modern = original; + modern.c_cflag &= ~(CSIZE | CSTOPB | PARODD); + modern.c_cflag |= CS7 | PARENB; + ASSERT_EQ(cfsetispeed(&modern, B9600), 0); + ASSERT_EQ(cfsetospeed(&modern, B9600), 0); + ASSERT_EQ(tcsetattr(serial.get(), TCSANOW, &modern), 0) << strerror(errno); + + struct termios modern_back = {}; + ASSERT_EQ(tcgetattr(serial.get(), &modern_back), 0) << strerror(errno); + EXPECT_EQ(cfgetospeed(&modern_back), static_cast(B9600)); + EXPECT_EQ(modern_back.c_cflag & CSIZE, static_cast(CS7)); + EXPECT_NE(modern_back.c_cflag & PARENB, 0u); + EXPECT_EQ(modern_back.c_cflag & PARODD, 0u); + + TermioCompat legacy = {}; + ASSERT_EQ(ioctl(serial.get(), TCGETA, &legacy), 0) << strerror(errno); + legacy.c_cflag &= ~static_cast(CBAUD | CSIZE | PARENB | PARODD); + legacy.c_cflag |= static_cast(B19200 | CS8); + ASSERT_EQ(ioctl(serial.get(), TCSETA, &legacy), 0) << strerror(errno); + + struct termios legacy_back = {}; + ASSERT_EQ(tcgetattr(serial.get(), &legacy_back), 0) << strerror(errno); + EXPECT_EQ(cfgetospeed(&legacy_back), static_cast(B19200)); + EXPECT_EQ(legacy_back.c_cflag & CSIZE, static_cast(CS8)); + EXPECT_EQ(legacy_back.c_cflag & PARENB, 0u); +} + +/* -------------------------------------------------------------------------- + * tcsetattr on a non-TTY fd must fail with Linux's ENOTTY. + * -------------------------------------------------------------------------- */ +TEST(TtyTermios, NonTtyFails) { + struct termios t = {}; + int fd = open("/dev/null", O_RDWR); + if (fd < 0) { + GTEST_SKIP() << "cannot open /dev/null: " << strerror(errno); + return; + } + errno = 0; + int rc = tcsetattr(fd, TCSANOW, &t); + int saved_errno = errno; + close(fd); + EXPECT_EQ(rc, -1) << "tcsetattr on non-TTY fd should fail"; + EXPECT_EQ(saved_errno, ENOTTY) + << "tcsetattr on non-TTY fd should report ENOTTY"; +} + +} // namespace + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index f5846bf60..c68ec4e8a 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -82,3 +82,4 @@ normal/virtiofs_dax normal/af_packet_sockopt normal/af_packet_e2e normal/af_packet_mcast +normal/tty_termios