RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 10:37:18 -0700
commita4c179499f861606b1c24cc62a5d62eaf3112e1b
parent02590b467a004b14ab80c26c0860b4a39fcfaee9
treebrowse files at this commit
WIP stage 15: libc plumbing (poll/epoll, nonblock, loopback, futex/threads, ptys)
MCargo.lock+1 -1
MCargo.toml+1 -1
Mbuild.rs+1 -0
Mdisk/scripts/selftest.sh+7 -0
Mscripts/run-headless.sh+13 -6
Msrc/dev.rs+59 -33
Msrc/errno.rs+5 -3
Msrc/fs/file.rs+49 -0
Msrc/fs/mod.rs+2 -2
Msrc/fs/pipe.rs+98 -15
Asrc/futex.rs+218 -0
Msrc/interrupts.rs+38 -4
Msrc/lib.rs+8 -6
Msrc/main.rs+13 -4
Msrc/mm.rs+42 -1
Asrc/net/device.rs+121 -0
Msrc/net/mod.rs+58 -21
Msrc/net/socket.rs+103 -7
Msrc/net/virtio_net.rs+4 -46
Asrc/poll.rs+420 -0
Msrc/process.rs+344 -108
Msrc/signal.rs+217 -88
Msrc/smp.rs+51 -0
Msrc/syscall.rs+387 -84
Msrc/thread.rs+35 -16
Msrc/time.rs+62 -3
Msrc/tty.rs+967 -179
Atests/libc.rs+111 -0
Mtests/net.rs+2 -1
Mtests/shell.rs+1 -1
Mtests/virtio.rs+1 -1
Auser/echoserver.rs+206 -0
Muser/getty.rs+41 -19
Auser/nbtest.rs+117 -0
Auser/ptytest.rs+106 -0
Muser/sh.rs+4 -1
Auser/threads.rs+135 -0
Muser/ulib.rs+387 -13

38 files changed, 4435 insertions, 664 deletions

@@ -261,7 +261,7 @@ dependencies = [  [[package]] name = "rustbelt"-version = "0.14.0"+version = "0.15.0" dependencies = [  "bootloader",  "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rustbelt"-version = "0.14.0"+version = "0.15.0" edition = "2024"  [dependencies]
@@ -76,6 +76,7 @@ const PROGRAMS: &[&str] = &[     "ifconfig", "ping", "host", "fetch", "httpd", // stage 12: networking     "random", "getty", "tile", // stage 13: virtual drivers     "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes+    "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing ];  const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -100,6 +100,13 @@ start spin 1000 kill %1 wait kill -l+# Stage 15: libc plumbing. Threads summing on all 4 CPUs (futex mutex,+# TLS), an epoll + poll echo server over 127.0.0.1, ptys and the line+# discipline, nonblocking I/O; each prints "all checks passed".+threads+echoserver+ptytest+nbtest nosuchprogram cd Q: cd A:/tmp
@@ -68,7 +68,7 @@ VIRTIO_DEVICES=(-vga none -device virtio-vga -device virtio-rng-pci  LOG=target/headless-serial.log set +e-timeout 90 qemu-system-x86_64 -smp 4 \+timeout 120 qemu-system-x86_64 -smp 4 \     -drive format=raw,file="$IMG" \     -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \     -drive "$EXT2_DRIVE" \@@ -85,7 +85,7 @@ set -e # Lines the shell self-test must have printed (only for a plain boot). if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     expect=(-        "Rustbelt 0.14.0 (stage 14: processes) x86_64" # ver+        "Rustbelt 0.15.0 (stage 15: libc plumbing) x86_64" # ver         "[smp] 4 CPUs online"                          # smp.rs started 3 more cores         "B:  /mnt/disk0   fat32"                       # drives         "C:  /mnt/disk1   ext2"@@ -149,6 +149,10 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         "[sigtest] all checks passed"         "(start spin 1000), exit code 143"             # kill %1 (SIGTERM)         "15) SIGTERM"                                  # kill -l+        "[threads] all checks passed"                  # stage 15: libc plumbing+        "[echoserver] all checks passed"+        "[ptytest] all checks passed"+        "[nbtest] all checks passed"         "sh: nosuchprogram: command not found"         "cd: there is no drive Q:"         "A:/tmp $ pwd"@@ -157,11 +161,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     for line in "${expect[@]}"; do         grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }     done-    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 29,+    # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 33,     # `cat C:/chunks.txt | wc -l` (copied from E:) -> 3,     # `type D:/COW.TXT | wc -l` (copied from E:) -> 4, and     # `cat D:/seq.txt | wc -l` (downloaded by wget) -> 20000.-    for n in 1 29 3 4 20000; do+    for n in 1 33 3 4 20000; do         grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }     done     # No memory leaks: once the shell and all ~60 programs it ran have@@ -172,7 +176,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then         echo "[run-headless] MISSING: frames back at the baseline after the shell"; missing=1     fi     # The other direction: the host fetched a page from httpd in the guest.-    if grep -qF "Hello from Rustbelt! Rustbelt 0.14.0" "$CURL_OUT" 2>/dev/null; then+    if grep -qF "Hello from Rustbelt! Rustbelt 0.15.0" "$CURL_OUT" 2>/dev/null; then         echo "[run-headless] host curl got httpd's page: $(head -1 "$CURL_OUT")"     else         echo "[run-headless] MISSING: host curl through hostfwd to httpd"; missing=1@@ -180,8 +184,11 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then     # random -n 8: 16 hex digits; random -d 6 5: five numbers 1-6.     grep -qxE -- "[0-9a-f]{16}" "$LOG" || { echo "[run-headless] MISSING: random -n 8 output"; missing=1; }     grep -qxE -- "[1-6]( [1-6]){4}" "$LOG" || { echo "[run-headless] MISSING: random -d 6 5 output"; missing=1; }+    # hvc0 is a tty now: its output turns "\n" into "\r\n" (ONLCR), so+    # drop the '\r's before comparing whole lines.+    tr -d '\r' < "$HVC_OUT" > "$HVC_OUT.txt" 2>/dev/null && mv "$HVC_OUT.txt" "$HVC_OUT"     # The shell on hvc0 ran the host's commands and answered over hvc0.-    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.14.0 (stage 14" \+    for line in "Rustbelt getty: shell on this terminal" "Rustbelt 0.15.0 (stage 15" \                 "$ echo hi over hvc0" "hi over hvc0" "fb0" "hvc0"; do         grep -qF -- "$line" "$HVC_OUT" 2>/dev/null || { echo "[run-headless] MISSING on hvc0: $line"; missing=1; }     done
@@ -4,7 +4,11 @@ //!   /dev/null      reads as empty, swallows writes //!   /dev/random    random bytes (virtio-rng; /dev/urandom is the same -- //!   /dev/urandom   on modern Linux they only differ before boot-time seeding)+//!   /dev/console   the console terminal (keyboard + screen) //!   /dev/hvc0      the virtio console: a second terminal+//!   /dev/tty       your controlling terminal, whichever it is (tty.rs)+//!   /dev/ptmx      opening it creates a pseudo-terminal: you get the+//!   /dev/pts/N     master end, and its slave end appears as /dev/pts/N //!   /dev/fb0       the virtio-gpu framebuffer: pixels at byte offsets, //!                  plus ioctls (see syscall.rs) for its size, flushing, //!                  filling rectangles and moving the text console@@ -17,9 +21,9 @@ use crate::errno::Errno; use crate::fs::file::{File, KIND_DEVICE, SEEK_CUR, SEEK_END, SEEK_SET, Stat}; use crate::fs::{DirEntry, FileSystem, Inode, InodeKind};+use crate::tty; use crate::virtio::{console, gpu, rng}; use alloc::{string::String, sync::Arc, vec::Vec};-use core::sync::atomic::Ordering; use spin::Mutex;  pub struct DevFs;@@ -35,7 +39,7 @@ impl FileSystem for DevFs {  /// The devices that exist on this machine. fn names() -> Vec<&'static str> {-    let mut names = alloc::vec!["null", "random", "urandom"];+    let mut names = alloc::vec!["console", "null", "ptmx", "pts", "random", "tty", "urandom"];     if console::present() {         names.push("hvc0");     }@@ -57,10 +61,53 @@ impl Inode for DevDir {     }     fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {         let name = names().into_iter().find(|n| *n == name).ok_or(Errno::ENOENT)?;+        if name == "pts" {+            return Ok(Arc::new(PtsDir));+        }         Ok(Arc::new(DevNode(name)))     }     fn list(&self) -> Result<Vec<DirEntry>, Errno> {-        Ok(names().into_iter().map(|n| DirEntry { name: String::from(n), kind: InodeKind::Device }).collect())+        let kind = |n| if n == "pts" { InodeKind::Directory } else { InodeKind::Device };+        Ok(names().into_iter().map(|n| DirEntry { name: String::from(n), kind: kind(n) }).collect())+    }+}++/// /dev/pts: one entry per pseudo-terminal (named by number).+struct PtsDir;++impl Inode for PtsDir {+    fn kind(&self) -> InodeKind {+        InodeKind::Directory+    }+    fn size(&self) -> usize {+        tty::pty_count()+    }+    fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+        let n: usize = name.parse().map_err(|_| Errno::ENOENT)?;+        if !tty::pty_numbers().contains(&n) {+            return Err(Errno::ENOENT);+        }+        Ok(Arc::new(PtsNode(n)))+    }+    fn list(&self) -> Result<Vec<DirEntry>, Errno> {+        Ok(tty::pty_numbers().into_iter().map(|n| DirEntry { name: alloc::format!("{}", n), kind: InodeKind::Device }).collect())+    }+}++struct PtsNode(usize);++impl Inode for PtsNode {+    fn kind(&self) -> InodeKind {+        InodeKind::Device+    }+    fn size(&self) -> usize {+        0+    }+    fn mode(&self) -> u32 {+        0o620+    }+    fn open_device(&self) -> Result<Option<Arc<dyn File>>, Errno> {+        tty::open_pts(self.0).map(Some)     } } @@ -80,17 +127,17 @@ impl Inode for DevNode {     fn mode(&self) -> u32 {         0o666     }-    fn open_device(&self) -> Option<Arc<dyn File>> {-        Some(match self.0 {+    fn open_device(&self) -> Result<Option<Arc<dyn File>>, Errno> {+        Ok(Some(match self.0 {             "null" => Arc::new(Null),             "random" | "urandom" => Arc::new(Random),-            "hvc0" => {-                console::OPEN_COUNT.fetch_add(1, Ordering::Relaxed);-                Arc::new(Hvc)-            }-            "fb0" => Arc::new(Framebuffer::new(gpu::framebuffer()?)),-            _ => return None,-        })+            "console" => tty::console_file(),+            "hvc0" => tty::open_hvc(),+            "tty" => tty::open_ctty()?,+            "ptmx" => tty::open_ptmx()?,+            "fb0" => Arc::new(Framebuffer::new(gpu::framebuffer().ok_or(Errno::ENODEV)?)),+            _ => return Ok(None),+        }))     } } @@ -130,27 +177,6 @@ impl File for Random {     } } -struct Hvc;--impl File for Hvc {-    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {-        console::read(buf)-    }-    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {-        console::write(buf);-        Ok(buf.len()) // what didn't fit is dropped, like a serial line-    }-    fn stat(&self) -> Result<Stat, Errno> {-        device_stat(0)-    }-}--impl Drop for Hvc {-    fn drop(&mut self) {-        console::OPEN_COUNT.fetch_sub(1, Ordering::Relaxed);-    }-}- /// /dev/fb0: the framebuffer as a file of `stride * height * 4` bytes. /// Writing changes pixels in memory; they reach the screen on the FLUSH /// ioctl or when the file is closed (we remember which rows changed).
@@ -8,7 +8,8 @@ pub enum Errno {     ENOENT = 2,        // no such file or directory     ESRCH = 3,         // no such process (kill)     EINTR = 4,         // interrupted by a signal-    EIO = 5,           // input/output error (disk trouble)+    EIO = 5,           // input/output error (disk trouble; a pty whose other end closed)+    ENXIO = 6,         // no such device (/dev/tty without a controlling terminal)     E2BIG = 7,         // argument list too long     ENOEXEC = 8,       // not a valid executable     EBADF = 9,         // bad file descriptor@@ -49,6 +50,7 @@ pub enum Errno {     ENOTCONN = 107,       // not connected     ETIMEDOUT = 110,      // connect/DNS timed out     ECONNREFUSED = 111,   // nobody listens on that port (got a RST)+    EINPROGRESS = 115,    // nonblocking connect: started, not done yet     // Kernel-internal (never seen by programs): "a signal interrupted this     // call". On the way back to user space signal.rs turns them into     // EINTR or restarts the call (Linux uses the same numbers).@@ -60,12 +62,12 @@ impl Errno {     /// The errno for a negative syscall result like -2, if we know it.     pub fn from_ret(ret: i64) -> Option<Errno> {         use Errno::*;-        const ALL: [Errno; 46] = [+        const ALL: [Errno; 48] = [             EPERM, ESRCH, EINTR, ERESTARTSYS, ERESTARTNOHAND, ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENODEV,             ENOTDIR, EISDIR, EINVAL, EMFILE, ENOTTY, EFBIG, ENOSPC, ESPIPE, EROFS, EPIPE, ERANGE,             ENAMETOOLONG, ENOSYS, ENOTEMPTY, ELOOP, EOPNOTSUPP, ENOTSOCK, EDESTADDRREQ, EMSGSIZE,             EPROTONOSUPPORT, EAFNOSUPPORT, EADDRINUSE, ENETDOWN, ENETUNREACH, ECONNRESET, EISCONN,-            ENOTCONN, ETIMEDOUT, ECONNREFUSED,+            ENOTCONN, ETIMEDOUT, ECONNREFUSED, ENXIO, EINPROGRESS,         ];         ALL.into_iter().find(|&e| -(e as i64) == ret)     }
@@ -75,6 +75,41 @@ pub trait File: Send + Sync {     fn inode(&self) -> Option<Arc<dyn Inode>> {         None     }++    // --- stage 15: what event loops and a C library need -----------------++    /// Readiness right now, as poll(2) bits (crate::poll::POLLIN...): can a+    /// read / a write go ahead without blocking? Files that never block+    /// (regular files, /dev/null) are always ready.+    fn poll(&self) -> u16 {+        crate::poll::POLLIN | crate::poll::POLLOUT+    }+    /// O_NONBLOCK (fcntl F_SETFL, ioctl FIONBIO): instead of blocking,+    /// fail with EAGAIN. It belongs to the open file, so dup'ed and+    /// inherited fds share it. Only files that can block keep it.+    fn set_nonblocking(&self, _on: bool) {}+    fn nonblocking(&self) -> bool {+        false+    }+    /// Device-specific ioctls (ttys: termios, window size...).+    fn ioctl(&self, _cmd: u64, _arg: u64) -> Result<u64, Errno> {+        Err(Errno::ENOTTY)+    }+    /// pread/pwrite: at an explicit offset, leaving the file offset alone.+    fn read_at(&self, _buf: &mut [u8], _offset: u64) -> Result<usize, Errno> {+        Err(Errno::ESPIPE)+    }+    fn write_at(&self, _buf: &[u8], _offset: u64) -> Result<usize, Errno> {+        Err(Errno::ESPIPE)+    }+    /// epoll_ctl / epoll_wait need the epoll instance behind the fd.+    fn as_epoll(&self) -> Option<&crate::poll::Epoll> {+        None+    }+    /// The terminal behind the fd, if it is one (tty.rs).+    fn as_tty(&self) -> Option<&crate::tty::TtyFile> {+        None+    } }  /// A file or directory opened through the VFS: inode + current position.@@ -134,6 +169,20 @@ impl File for InodeFile {         Ok(Stat::of(&*self.inode))     } +    fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<usize, Errno> {+        match self.inode.kind() {+            InodeKind::Directory => Err(Errno::EISDIR),+            _ => self.inode.read_at(offset as usize, buf),+        }+    }++    fn write_at(&self, buf: &[u8], offset: u64) -> Result<usize, Errno> {+        match self.inode.kind() {+            InodeKind::Directory => Err(Errno::EISDIR),+            _ => self.inode.write_at(offset as usize, buf),+        }+    }+     fn inode(&self) -> Option<Arc<dyn Inode>> {         (self.inode.kind() == InodeKind::File).then(|| self.inode.clone())     }
@@ -106,8 +106,8 @@ pub trait Inode: Send + Sync {     }     /// Device files: the `File` that `open` should return (None = an     /// ordinary inode, opened as an `InodeFile`).-    fn open_device(&self) -> Option<Arc<dyn file::File>> {-        None+    fn open_device(&self) -> Result<Option<Arc<dyn file::File>>, Errno> {+        Ok(None)     } } 
@@ -6,20 +6,31 @@ //! every write end is closed (then read returns 0 = EOF); writing when every //! read end is closed fails with EPIPE. Closing = dropping the last `Arc` //! of an end (fd closed, process exited), which our `Drop` impls notice.-//! Simplification: the buffer is unbounded, so writers never block.+//!+//! The buffer holds at most PIPE_CAPACITY bytes (64 KiB, like Linux): a+//! writer that runs ahead of its reader blocks until there's room. With+//! O_NONBLOCK, instead of blocking, a read of an empty pipe or a write to+//! a full one fails with EAGAIN ("try again later"); event loops use+//! poll/epoll (poll.rs) to learn when that is.  use super::file::{File, KIND_PIPE, Stat}; use crate::errno::Errno;+use crate::poll::{self, POLLERR, POLLHUP, POLLIN, POLLOUT}; use crate::thread::WaitQueue; use alloc::{collections::VecDeque, sync::Arc};+use core::sync::atomic::{AtomicBool, Ordering}; use spin::Mutex; use x86_64::instructions::interrupts::without_interrupts; +/// How many bytes a pipe buffers before writers have to wait.+pub const PIPE_CAPACITY: usize = 64 * 1024;+ struct Pipe {     // Locked with interrupts off only (it's touched in `wait_until`'s     // condition, see the locking rule in thread.rs).     state: Mutex<PipeState>,     readers_wait: WaitQueue,+    writers_wait: WaitQueue, }  struct PipeState {@@ -28,16 +39,17 @@ struct PipeState {     read_closed: bool, } -pub struct PipeReader(Arc<Pipe>);-pub struct PipeWriter(Arc<Pipe>);+pub struct PipeReader(Arc<Pipe>, AtomicBool);+pub struct PipeWriter(Arc<Pipe>, AtomicBool);  /// A new pipe: (read end, write end). pub fn new() -> (PipeReader, PipeWriter) {     let pipe = Arc::new(Pipe {         state: Mutex::new(PipeState { buffer: VecDeque::new(), write_closed: false, read_closed: false }),         readers_wait: WaitQueue::new(),+        writers_wait: WaitQueue::new(),     });-    (PipeReader(pipe.clone()), PipeWriter(pipe))+    (PipeReader(pipe.clone(), AtomicBool::new(false)), PipeWriter(pipe, AtomicBool::new(false))) }  impl File for PipeReader {@@ -45,18 +57,28 @@ impl File for PipeReader {         if buf.is_empty() {             return Ok(0);         }+        let nonblocking = self.nonblocking();         // Block until there's data or no writer is left (or a signal).-        self.0.readers_wait.wait_until_intr(|| {+        let n = self.0.readers_wait.wait_until_intr(|| {             let mut state = self.0.state.lock();             if state.buffer.is_empty() {-                return if state.write_closed { Some(0) } else { None };+                return match () {+                    _ if state.write_closed => Some(Ok(0)),+                    _ if nonblocking => Some(Err(Errno::EAGAIN)),+                    _ => None,+                };             }             let n = buf.len().min(state.buffer.len());             for (dst, src) in buf.iter_mut().zip(state.buffer.drain(..n)) {                 *dst = src;             }-            Some(n)-        })+            Some(Ok(n))+        })??;+        if n > 0 {+            self.0.writers_wait.wake_all(); // there's room now+            poll::notify();+        }+        Ok(n)     }     fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {         Err(Errno::EBADF) // wrong end@@ -65,38 +87,99 @@ impl File for PipeReader {         let size = without_interrupts(|| self.0.state.lock().buffer.len());         Ok(Stat { kind: KIND_PIPE, size: size as u64, mode: 0 })     }+    fn poll(&self) -> u16 {+        without_interrupts(|| {+            let state = self.0.state.lock();+            let mut events = 0;+            if !state.buffer.is_empty() {+                events |= POLLIN;+            }+            if state.write_closed {+                events |= POLLHUP; // (a read returns 0 = EOF)+            }+            events+        })+    }+    fn set_nonblocking(&self, on: bool) {+        self.1.store(on, Ordering::Relaxed);+    }+    fn nonblocking(&self) -> bool {+        self.1.load(Ordering::Relaxed)+    } }  impl File for PipeWriter {     fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {         Err(Errno::EBADF)     }+    /// Write all of `buf`, waiting for room as needed. Interrupted by a+    /// signal (or non-blocking) after some bytes: return that count.     fn write(&self, buf: &[u8]) -> Result<usize, Errno> {-        without_interrupts(|| {+        let nonblocking = self.nonblocking();+        let mut written = 0;+        let result = self.0.writers_wait.wait_until_intr(|| {             let mut state = self.0.state.lock();             if state.read_closed {-                return Err(Errno::EPIPE);+                return Some(Err(Errno::EPIPE));+            }+            let room = PIPE_CAPACITY - state.buffer.len();+            let n = room.min(buf.len() - written);+            state.buffer.extend(&buf[written..written + n]);+            written += n;+            if n > 0 {+                // Wake readers now, not after the loop: if the pipe is full+                // we're about to sleep until one of them makes room.+                self.0.readers_wait.wake_all();+                poll::notify();+            }+            match () {+                _ if written == buf.len() => Some(Ok(written)),+                _ if nonblocking && written > 0 => Some(Ok(written)),+                _ if nonblocking => Some(Err(Errno::EAGAIN)),+                _ => None, // full: wait for a reader to make room             }-            state.buffer.extend(buf);-            Ok(buf.len())-        })?;-        self.0.readers_wait.wake_all();-        Ok(buf.len())+        });+        match result {+            Ok(r) => r,+            Err(_) if written > 0 => Ok(written), // a signal, after some bytes+            Err(e) => Err(e),+        }     }     fn stat(&self) -> Result<Stat, Errno> {         Ok(Stat { kind: KIND_PIPE, size: 0, mode: 0 })     }+    fn poll(&self) -> u16 {+        without_interrupts(|| {+            let state = self.0.state.lock();+            if state.read_closed {+                POLLERR // (a write fails with EPIPE)+            } else if state.buffer.len() < PIPE_CAPACITY {+                POLLOUT+            } else {+                0+            }+        })+    }+    fn set_nonblocking(&self, on: bool) {+        self.1.store(on, Ordering::Relaxed);+    }+    fn nonblocking(&self) -> bool {+        self.1.load(Ordering::Relaxed)+    } }  impl Drop for PipeWriter {     fn drop(&mut self) {         without_interrupts(|| self.0.state.lock().write_closed = true);         self.0.readers_wait.wake_all(); // readers see EOF+        poll::notify();     } }  impl Drop for PipeReader {     fn drop(&mut self) {         without_interrupts(|| self.0.state.lock().read_closed = true);+        self.0.writers_wait.wake_all(); // writers get EPIPE+        poll::notify();     } }
@@ -0,0 +1,218 @@+//! Futexes ("fast userspace mutexes"): the one syscall threads need to+//! sleep and wake each other.+//!+//! A lock in a C library is just an integer in memory. Taking a free lock+//! is one atomic instruction in user space -- no syscall at all. Only when+//! a thread has to *wait* does it ask the kernel:+//!+//!   futex(&word, FUTEX_WAIT, expected): "put me to sleep, but only if+//!                                        word still == expected"+//!   futex(&word, FUTEX_WAKE, n):        "wake up to n sleepers on word"+//!+//! The "only if still equal" check is the whole trick: the kernel does it+//! while holding its own lock, the same lock FUTEX_WAKE takes. So if the+//! lock holder releases the lock (changes the word) and calls WAKE between+//! our user-space check and our WAIT, either WAIT sees the changed word+//! and returns EAGAIN at once, or we're already queued and WAKE finds us.+//! No wake-up can get lost.+//!+//! The kernel keeps no state per futex word: just a list of sleepers,+//! each labelled with the word it sleeps on (its *key*). For memory private+//! to one process the key is (address space, virtual address); for+//! MAP_SHARED memory, which different processes may map at different+//! addresses, it's the physical address.+//!+//! pthread_join uses it too: the kernel writes 0 to the exiting thread's+//! tid word and does a FUTEX_WAKE on it (CLONE_CHILD_CLEARTID).++use crate::errno::Errno;+use crate::memory;+use crate::process::Process;+use crate::thread::{self, Tid, WaitQueue};+use alloc::vec::Vec;+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts;++pub const FUTEX_WAIT: u64 = 0;+pub const FUTEX_WAKE: u64 = 1;+pub const FUTEX_REQUEUE: u64 = 3;+pub const FUTEX_CMP_REQUEUE: u64 = 4;+pub const FUTEX_WAIT_BITSET: u64 = 9;+pub const FUTEX_WAKE_BITSET: u64 = 10;+pub const FUTEX_PRIVATE_FLAG: u64 = 128;+pub const FUTEX_CLOCK_REALTIME: u64 = 256;+pub const FUTEX_BITSET_MATCH_ANY: u32 = u32::MAX;++/// Which word a sleeper waits on.+#[derive(Clone, Copy, PartialEq, Eq, Debug)]+enum Key {+    /// (level-4 page table = the address space, virtual address)+    Private(u64, u64),+    /// Physical address (MAP_SHARED memory).+    Shared(u64),+}++struct Waiter {+    id: u64,+    key: Key,+    bitset: u32,+    kthread: Tid,+    woken: bool,+}++/// All sleepers. Locked with interrupts off (read in wait conditions).+static WAITERS: Mutex<Vec<Waiter>> = Mutex::new(Vec::new());+/// Only used for its sleeping logic: wakers wake threads one by one.+static QUEUE: WaitQueue = WaitQueue::new();+static NEXT_ID: AtomicU64 = AtomicU64::new(1);++/// Look up the key of `uaddr` in `p` and read the word there (through the+/// physical mapping, with the address space locked so it can't change+/// under us). The page must have been faulted in.+fn key_and_value(p: &Process, uaddr: u64, private: bool) -> Result<(Key, u32), Errno> {+    let mm = p.mm.lock();+    let (frame, _) = memory::user_pte(mm.l4, uaddr).ok_or(Errno::EFAULT)?;+    let phys = frame.start_address().as_u64() + (uaddr & 0xfff);+    let value = unsafe { core::ptr::read_volatile(memory::phys_to_virt(x86_64::PhysAddr::new(phys)).as_ptr::<u32>()) };+    let shared = !private && mm.vmas().iter().any(|v| v.shared && (v.start..v.end).contains(&uaddr));+    let key = if shared { Key::Shared(phys) } else { Key::Private(mm.l4.start_address().as_u64(), uaddr) };+    Ok((key, value))+}++/// Check a futex address and fault its page in.+fn prepare(uaddr: u64) -> Result<(), Errno> {+    if uaddr % 4 != 0 {+        return Err(Errno::EINVAL);+    }+    if !crate::mm::user_access_ok(uaddr, 4, false) {+        return Err(Errno::EFAULT);+    }+    Ok(())+}++/// FUTEX_WAIT: sleep while *uaddr == val, until woken, `deadline` (PIT+/// tick) or a signal.+pub fn wait(p: &Process, uaddr: u64, val: u32, deadline: Option<u64>, bitset: u32, private: bool) -> Result<u64, Errno> {+    if bitset == 0 {+        return Err(Errno::EINVAL);+    }+    prepare(uaddr)?;+    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);+    // Compare and enqueue in one step under WAITERS (see the top).+    without_interrupts(|| {+        let (key, value) = key_and_value(p, uaddr, private)?;+        let mut waiters = WAITERS.lock();+        if value != val {+            return Err(Errno::EAGAIN);+        }+        waiters.push(Waiter { id, key, bitset, kthread: thread::current_id(), woken: false });+        Ok(())+    })?;+    let woken = || WAITERS.lock().iter().any(|w| w.id == id && w.woken).then_some(());+    let result = match deadline {+        Some(deadline) => QUEUE.wait_until_deadline_intr(deadline, woken),+        None => QUEUE.wait_until_intr(woken).map(Some),+    };+    // Leave the list; if a wake-up raced with the timeout/signal, it counts.+    let was_woken = without_interrupts(|| {+        let mut waiters = WAITERS.lock();+        let i = waiters.iter().position(|w| w.id == id).expect("futex waiter vanished");+        waiters.swap_remove(i).woken+    });+    match result {+        _ if was_woken => Ok(0),+        Ok(_) => Err(Errno::ETIMEDOUT),+        Err(_) => Err(Errno::EINTR),+    }+}++/// Wake up to `n` sleepers on `key` (matching `bitset`); move up to+/// `requeue` more to `to` instead. Returns how many were woken (+ moved).+fn wake_key(key: Key, n: u64, bitset: u32, requeue: Option<(Key, u64)>) -> u64 {+    let mut kthreads = Vec::new();+    let mut moved = 0;+    without_interrupts(|| {+        let mut waiters = WAITERS.lock();+        for w in waiters.iter_mut().filter(|w| w.key == key && !w.woken && w.bitset & bitset != 0) {+            if (kthreads.len() as u64) < n {+                w.woken = true;+                kthreads.push(w.kthread);+            } else if let Some((to, max)) = requeue {+                if moved < max {+                    w.key = to;+                    moved += 1;+                }+            }+        }+    });+    for &k in &kthreads {+        thread::wake(k);+    }+    kthreads.len() as u64 + moved+}++/// FUTEX_WAKE from the kernel (thread exit, CLONE_CHILD_CLEARTID).+pub fn wake(p: &Process, uaddr: u64, n: u64) -> u64 {+    match prepare(uaddr).and_then(|_| without_interrupts(|| key_and_value(p, uaddr, false))) {+        Ok((key, _)) => wake_key(key, n, FUTEX_BITSET_MATCH_ANY, None),+        Err(_) => 0,+    }+}++/// How many sleepers there are (tests: nothing left behind).+pub fn waiters() -> usize {+    without_interrupts(|| WAITERS.lock().len())+}++/// The futex syscall (202): futex(uaddr, op, val, timeout/val2, uaddr2, val3).+pub fn sys_futex(p: &Process, uaddr: u64, op: u64, val: u64, timeout: u64, uaddr2: u64, val3: u64) -> Result<u64, Errno> {+    let private = op & FUTEX_PRIVATE_FLAG != 0;+    let realtime = op & FUTEX_CLOCK_REALTIME != 0;+    let cmd = op & !(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME);+    match cmd {+        FUTEX_WAIT | FUTEX_WAIT_BITSET => {+            let bitset = if cmd == FUTEX_WAIT { FUTEX_BITSET_MATCH_ANY } else { val3 as u32 };+            let deadline = match timeout {+                0 => None,+                ptr => {+                    let us = crate::syscall::read_timespec_us(ptr)?;+                    // FUTEX_WAIT: relative. FUTEX_WAIT_BITSET: an absolute+                    // time on CLOCK_MONOTONIC (or REALTIME with the flag).+                    let us = if cmd == FUTEX_WAIT {+                        us+                    } else {+                        let now = if realtime { crate::time::realtime_us() } else { crate::time::now_us() };+                        us.saturating_sub(now)+                    };+                    if us == 0 {+                        return Err(Errno::ETIMEDOUT);+                    }+                    Some(crate::time::deadline_after_us(us))+                }+            };+            wait(p, uaddr, val as u32, deadline, bitset, private)+        }+        FUTEX_WAKE | FUTEX_WAKE_BITSET => {+            let bitset = if cmd == FUTEX_WAKE { FUTEX_BITSET_MATCH_ANY } else { val3 as u32 };+            if bitset == 0 {+                return Err(Errno::EINVAL);+            }+            prepare(uaddr)?;+            let (key, _) = without_interrupts(|| key_and_value(p, uaddr, private))?;+            Ok(wake_key(key, val, bitset, None))+        }+        FUTEX_REQUEUE | FUTEX_CMP_REQUEUE => {+            prepare(uaddr)?;+            prepare(uaddr2)?;+            let (key, value) = without_interrupts(|| key_and_value(p, uaddr, private))?;+            if cmd == FUTEX_CMP_REQUEUE && value != val3 as u32 {+                return Err(Errno::EAGAIN);+            }+            let (to, _) = without_interrupts(|| key_and_value(p, uaddr2, private))?;+            // (val2 travels in the timeout argument's register.)+            Ok(wake_key(key, val, FUTEX_BITSET_MATCH_ANY, Some((to, timeout))))+        }+        _ => Err(Errno::ENOSYS),+    }+}
@@ -4,8 +4,9 @@ //! a handler function: //!   * vectors 0..=31  are CPU exceptions (divide error, page fault, ...) //!   * vectors 32..=47 are where we remap the 8259 PIC's 16 hardware IRQs-//!   * 0x40, 0x41, 0xFF come from the local APIC (see apic.rs): its timer,-//!     the "reschedule" IPI one CPU sends another, and spurious interrupts+//!   * 0x40, 0x41, 0x42, 0xFF come from the local APIC (see apic.rs): its+//!     timer, the "reschedule" and "TLB shootdown" IPIs one CPU sends+//!     another, and spurious interrupts //! //! All CPUs share this one IDT. Device IRQs (PIT, keyboard) only ever //! arrive on CPU 0; APIC timer ticks and IPIs arrive on every CPU.@@ -44,6 +45,7 @@ pub enum InterruptIndex {     Keyboard,             // IRQ 1: PS/2 keyboard     ApicTimer = 0x40,     // each CPU's local APIC timer: time slices (~100 Hz)     Reschedule = 0x41,    // IPI: "wake up, there's work for you"+    TlbShootdown = 0x42,  // IPI: "flush your TLB" (smp::tlb_shootdown)     ApicSpurious = 0xFF,  // the APIC's "never mind" interrupt } @@ -89,6 +91,7 @@ lazy_static! {         idt[PIC_1_OFFSET + 10].set_handler_fn(pci_irq10_handler);         idt[PIC_1_OFFSET + 11].set_handler_fn(pci_irq11_handler);         idt[InterruptIndex::Reschedule.as_u8()].set_handler_fn(reschedule_handler);+        idt[InterruptIndex::TlbShootdown.as_u8()].set_handler_fn(tlb_shootdown_handler);         idt[InterruptIndex::ApicSpurious.as_u8()].set_handler_fn(spurious_handler);         idt     };@@ -145,8 +148,9 @@ fn exception(frame: &TrapFrame, sig: u32, code: i32, addr: u64, what: &'static s /// usually *not* an error: demand paging and copy-on-write (mm.rs) fix the /// page up and the instruction runs again. Only if the address isn't in /// any region (or the region forbids the access) does the program get-/// SIGSEGV. The kernel itself never faults on user memory (syscalls check-/// buffers first), so a kernel-mode page fault is a bug.+/// SIGSEGV. The kernel itself faults on user memory only when another+/// thread changed it during a syscall (below); any other kernel-mode page+/// fault is a bug. fn page_fault(frame: &mut TrapFrame) {     use crate::mm::Fault;     use x86_64::registers::control::Cr2;@@ -178,6 +182,30 @@ fn page_fault(frame: &mut TrapFrame) {         signal::force(sig, SigInfo::fault(sig, code, addr), why);         return;     }+    // The kernel touched user memory a syscall had checked (user_slice), but+    // another thread of the process changed the mapping since: munmap'd it,+    // or fork made it copy-on-write. Fix it up like a user fault; if the+    // memory is really gone, map a blank page so the kernel can finish, and+    // kill the process (Linux would return EFAULT; we have no way back to+    // the syscall from here).+    if (crate::memory::USER_START..crate::memory::USER_END).contains(&addr) {+        if let Some(p) = crate::thread::current_process() {+            if frame.rflags & (1 << 9) != 0 {+                x86_64::instructions::interrupts::enable(); // as it was+            }+            let write = error.contains(PageFaultErrorCode::CAUSED_BY_WRITE);+            let mut mm = p.mm.lock();+            if mm.handle_fault(addr, write, false).is_ok() {+                return;+            }+            serial_println!("[kernel] syscall touched unmapped user memory {:#x} (another thread unmapped it?)", addr);+            if mm.map_scratch(addr).is_some() {+                drop(mm);+                signal::force(signal::SIGSEGV, SigInfo::fault(signal::SIGSEGV, signal::SEGV_MAPERR, addr), "bad user pointer in a syscall");+                return;+            }+        }+    }     // The kernel touched a kernel stack's guard page: that thread overflowed.     if crate::stack::is_guard_page(addr) {         crate::thread::die_of_stack_overflow(addr);@@ -277,6 +305,12 @@ extern "x86-interrupt" fn reschedule_handler(_stack_frame: InterruptStackFrame)      crate::thread::schedule(); } +/// Another CPU changed page tables we may have cached (threads, smp.rs).+extern "x86-interrupt" fn tlb_shootdown_handler(_stack_frame: InterruptStackFrame) {+    crate::smp::handle_tlb_shootdown();+    crate::apic::eoi();+}+ /// Spurious APIC interrupt: ignore it (and no EOI, by the spec). extern "x86-interrupt" fn spurious_handler(_stack_frame: InterruptStackFrame) {} 
@@ -25,11 +25,12 @@ pub mod acpi; // ACPI tables: just enough to list the CPUs (MADT) pub mod allocator; // heap: maps heap pages + the #[global_allocator] pub mod apic; // local APIC: per-CPU timer, inter-processor interrupts pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions-pub mod dev; // /dev: null, random, hvc0, fb0+pub mod dev; // /dev: null, random, hvc0, fb0, tty, ptmx, pts/N pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers pub mod fbcon; // text console drawn on the framebuffer pub mod font; // 8x16 bitmap font (Spleen) for fbcon+pub mod futex; // futexes: user-space locks' sleep/wake (threads) pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard@@ -37,6 +38,7 @@ pub mod net; // networking: virtio-net driver + smoltcp TCP/IP, sockets pub mod memory; // paging, frame allocator, page tables, shared-frame counts pub mod mm; // a process's memory map: regions, demand paging, copy-on-write pub mod pci; // PCI bus enumeration (config space via ports 0xCF8/0xCFC)+pub mod poll; // poll, select, epoll: waiting for many files at once pub mod process; // processes: fork, execve, exit, wait4, process groups pub mod serial; // COM1 serial port pub mod signal; // signals: kill, handlers, signal frames, default actions@@ -47,7 +49,7 @@ pub mod task; // cooperative multitasking: async tasks + executor pub mod trap; // TrapFrame: saved user registers, the common return to ring 3 pub mod time; // microsecond clock (TSC calibrated against the PIT) pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues)-pub mod tty; // console: keyboard line discipline + fd 0/1/2+pub mod tty; // terminals: line discipline, termios, console/hvc0/ptys pub mod vga_buffer; // the console: VGA text mode, or fbcon on a GPU pub mod virtio; // modern virtio transport + virtio-rng, -console, -gpu @@ -102,10 +104,10 @@ pub fn init_drivers() -> alloc::vec::Vec<alloc::string::String> {     log } -/// Networking: find the virtio-net card, start smoltcp, DHCP and the netd-/// thread. Needs the scheduler (call after `init_smp`). Returns the boot-/// log line, or None without a network card.-pub fn init_net() -> Option<alloc::string::String> {+/// Networking: loopback, plus the virtio-net card if there is one (smoltcp,+/// DHCP and the netd thread). Needs the scheduler (call after `init_smp`).+/// Returns the boot log lines.+pub fn init_net() -> alloc::vec::Vec<alloc::string::String> {     net::init(&pci::scan()) } 
@@ -98,10 +98,14 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {         kprintln!("[virtio] {}", line);     } -    // --- Network (net/): virtio-net + smoltcp; DHCP runs in the background ---    match rustbelt::init_net() {-        Some(line) => kprintln!("[net] {}", line),-        None => kprintln!("[net] no virtio-net card (start QEMU with -device virtio-net-pci)"),+    // --- Network (net/): loopback + virtio-net + smoltcp; DHCP runs in the+    // background ----------------------------------------------------------+    let net = rustbelt::init_net();+    for line in &net {+        kprintln!("[net] {}", line);+    }+    if !rustbelt::net::present() {+        kprintln!("[net] no virtio-net card (start QEMU with -device virtio-net-pci)");     }      // --- The shell (user/sh.rs), run by its own kernel thread ---------------@@ -135,6 +139,11 @@ fn shell_supervisor() {         rustbelt::exit_qemu(rustbelt::QemuExitCode::Failed);         return;     }+    // Start hvc0's tty (and its input thread `hvcd`) now rather than on+    // the first open, so its kernel stack isn't counted as a leak below.+    if rustbelt::virtio::console::present() {+        rustbelt::tty::hvc();+    }     loop {         #[cfg(feature = "headless")]         let frames_before = rustbelt::memory::frames_in_use();
@@ -214,12 +214,48 @@ impl Mm {     pub fn unmap(&mut self, start: u64, end: u64) {         self.split_at(start, end);         self.vmas.retain(|v| v.end <= start || v.start >= end);+        let mut freed = Vec::new();         for addr in (start..end).step_by(PAGE_SIZE as usize) {             if let Some(frame) = memory::unmap_user_page(self.l4, addr) {-                memory::release_frame(frame);                 memory::flush_user_page(addr);+                freed.push(frame);             }         }+        // Other threads' CPUs must forget the pages *before* the frames+        // can be reused for something else.+        if !freed.is_empty() {+            self.shootdown();+        }+        for frame in freed {+            memory::release_frame(frame);+        }+    }++    /// Map a blank, writable page at `addr` that belongs to no region (see+    /// the kernel-mode page fault in interrupts.rs). Freed with the address+    /// space.+    pub fn map_scratch(&mut self, addr: u64) -> Option<()> {+        let page = addr & !(PAGE_SIZE - 1);+        let frame = memory::alloc_zeroed_frame()?;+        let mut flags = Flags::PRESENT | Flags::WRITABLE | Flags::USER_ACCESSIBLE;+        if memory::nx_enabled() {+            flags |= Flags::NO_EXECUTE;+        }+        match memory::user_pte(self.l4, page) {+            Some(_) => {+                let old = memory::replace_user_frame(self.l4, page, frame, flags)?;+                memory::flush_user_page(page);+                memory::release_frame(old);+            }+            None => memory::map_user_frame(self.l4, page, frame, flags)?,+        }+        Some(())+    }++    /// Make every other CPU running this address space flush its TLB+    /// (threads: see smp::tlb_shootdown).+    pub fn shootdown(&self) {+        crate::smp::tlb_shootdown(self.l4.start_address().as_u64());     }      /// mprotect: new permissions for `start..end`, which must be fully@@ -248,6 +284,7 @@ impl Mm {                 }             }         }+        self.shootdown(); // permissions may have shrunk         Ok(())     } @@ -345,6 +382,9 @@ impl Mm {                     }                     let flags = vma.pte_flags(true);                     memory::replace_user_frame(self.l4, page, copy, flags).ok_or(Fault::OutOfMemory)?;+                    // Other threads of ours may still see the old frame.+                    memory::flush_user_page(page);+                    self.shootdown();                     memory::release_frame(frame); // our share of the original                 }             }@@ -412,6 +452,7 @@ impl Mm {             if shared { flags } else { flags - Flags::WRITABLE }         });         flush_all();+        self.shootdown(); // our other threads' CPUs: no more writable entries         ok.map(|_| child) // on failure, dropping `child` frees what it got     } 
@@ -0,0 +1,121 @@+//! The one "device" smoltcp talks to: the virtio-net card (if there is+//! one) *plus* a loopback queue.+//!+//! **Loopback.** Programs talk to themselves over the network all the+//! time: a database on 127.0.0.1, a test client and server, a compositor+//! and its clients. Linux has a separate `lo` interface for that. We keep+//! smoltcp's single Ethernet interface and give it a second address,+//! 127.0.0.1/8, next to DHCP's. Then this device does the trick: a frame+//! meant for *ourselves* never goes to the card, it's put on the `lo`+//! queue, and `receive` hands queued frames back to smoltcp first.+//! "Meant for ourselves" is:+//!   * an ARP request asking who has 127.x.x.x or our own DHCP address+//!     (smoltcp then answers itself: "that's me", with its own MAC), and+//!   * any frame sent to our own MAC address.+//! So 127.0.0.1 works with no network card at all (MAC 02:00:00:00:00:01,+//! a "locally administered" one), and connecting to our own 10.0.2.15+//! doesn't leave the machine either, just like Linux.++use super::virtio_net::{MTU, VirtioNet};+use alloc::collections::VecDeque;+use alloc::vec;+use alloc::vec::Vec;+use smoltcp::phy::{self, DeviceCapabilities, Medium};+use smoltcp::time::Instant;++/// Our MAC when there's no card.+pub const LOOPBACK_MAC: [u8; 6] = [0x02, 0, 0, 0, 0, 1];+/// Frames waiting on lo at most (more are dropped, like a full queue).+const LO_QUEUE: usize = 256;++pub struct NetDevice {+    pub nic: Option<VirtioNet>,+    pub mac: [u8; 6],+    /// Our DHCP address (frames ARPing for it are ours).+    pub own_ip: Option<[u8; 4]>,+    lo: VecDeque<Vec<u8>>,+    pub lo_packets: u64,+}++impl NetDevice {+    pub fn new(nic: Option<VirtioNet>) -> NetDevice {+        let mac = nic.as_ref().map_or(LOOPBACK_MAC, |n| n.mac);+        NetDevice { nic, mac, own_ip: None, lo: VecDeque::new(), lo_packets: 0 }+    }++    /// Frames are waiting on lo (net::poll then polls smoltcp again).+    pub fn lo_pending(&self) -> bool {+        !self.lo.is_empty()+    }++    /// Is this outgoing Ethernet frame addressed to us? (See the top.)+    fn is_for_us(&self, frame: &[u8]) -> bool {+        if frame.len() < 14 {+            return false;+        }+        if frame[0..6] == self.mac {+            return true;+        }+        // ARP (ethertype 0x0806): the asked-for IP is at bytes 38..42.+        if frame[12..14] == [0x08, 0x06] && frame.len() >= 42 {+            let target = [frame[38], frame[39], frame[40], frame[41]];+            return target[0] == 127 || Some(target) == self.own_ip;+        }+        false+    }+}++// smoltcp asks a `Device` for *tokens*: an RxToken is "a received frame you+// may look at", a TxToken is "permission to build one frame to send".++pub struct RxToken(Vec<u8>);+pub struct TxToken<'a>(&'a mut NetDevice);++impl phy::Device for NetDevice {+    type RxToken<'a> = RxToken;+    type TxToken<'a> = TxToken<'a>;++    fn receive(&mut self, _now: Instant) -> Option<(RxToken, TxToken<'_>)> {+        // Loopback first, then the card.+        let frame = match self.lo.pop_front() {+            Some(f) => f,+            None => self.nic.as_mut()?.receive_frame()?,+        };+        Some((RxToken(frame), TxToken(self)))+    }++    fn transmit(&mut self, _now: Instant) -> Option<TxToken<'_>> {+        Some(TxToken(self))+    }++    fn capabilities(&self) -> DeviceCapabilities {+        let mut caps = DeviceCapabilities::default();+        caps.medium = Medium::Ethernet;+        caps.max_transmission_unit = MTU;+        caps+    }+}++impl phy::RxToken for RxToken {+    fn consume<R, F: FnOnce(&[u8]) -> R>(self, f: F) -> R {+        f(&self.0)+    }+}++impl phy::TxToken for TxToken<'_> {+    /// smoltcp builds the frame in `f`; we then decide where it goes.+    fn consume<R, F: FnOnce(&mut [u8]) -> R>(self, len: usize, f: F) -> R {+        let dev = self.0;+        let mut frame = vec![0; len.min(MTU)];+        let result = f(&mut frame);+        if dev.is_for_us(&frame) {+            if dev.lo.len() < LO_QUEUE {+                dev.lo_packets += 1;+                dev.lo.push_back(frame);+            }+        } else if let Some(nic) = dev.nic.as_mut() {+            nic.send_frame(frame.len(), |buf| buf.copy_from_slice(&frame[..buf.len()]));+        } // no card: nowhere to send it+        result+    }+}
@@ -2,6 +2,8 @@ //! //! The layers, bottom to top (see the README's networking chapter): //!   * virtio_net.rs  moves raw Ethernet frames in and out of the card.+//!   * device.rs      what smoltcp sees: the card plus a loopback queue,+//!                    so 127.0.0.1 works (even with no card at all). //!   * smoltcp        does everything protocol-shaped: Ethernet addressing, //!                    ARP (who has 10.0.2.2?), IPv4, ICMP (ping), UDP, TCP //!                    (connections, retransmits, windows), DHCP and DNS.@@ -22,6 +24,7 @@ //! Locking: `NET` is taken with interrupts off (wait-queue conditions look //! at sockets with interrupts off; see the locking rule in thread.rs). +pub mod device; pub mod socket; pub mod virtio_net; @@ -35,6 +38,7 @@ use smoltcp::socket::{dhcpv4, dns, tcp}; use smoltcp::time::Instant; use smoltcp::wire::{DnsQueryType, EthernetAddress, HardwareAddress, IpAddress, IpCidr, Ipv4Address}; use spin::Mutex;+use device::NetDevice; use virtio_net::VirtioNet; use x86_64::instructions::interrupts::without_interrupts; use x86_64::instructions::port::Port;@@ -50,7 +54,7 @@ pub struct IpConfig {  pub struct Net {     pub iface: Interface,-    pub device: VirtioNet,+    pub device: NetDevice,     pub sockets: SocketSet<'static>,     dhcp: SocketHandle,     dns: SocketHandle,@@ -71,7 +75,7 @@ static IRQ_PENDING: AtomicBool = AtomicBool::new(false); /// without taking the NET lock. static IO_BASE: AtomicU16 = AtomicU16::new(0); -/// Run `f` on the network state (None if there's no network card).+/// Run `f` on the network state (None before `init`). pub fn with_net<R>(f: impl FnOnce(&mut Net) -> R) -> Option<R> {     without_interrupts(|| NET.lock().as_mut().map(f)) }@@ -81,35 +85,53 @@ pub fn now() -> Instant {     Instant::from_micros(crate::time::now_us() as i64) } -/// Find the first virtio-net card, set up smoltcp on it, start DHCP and-/// the netd thread. Returns a line for the boot log.-pub fn init(devices: &[crate::pci::PciDevice]) -> Option<String> {-    let pci = devices.iter().find(|d| virtio_net::is_virtio_net(d))?;-    let mut device = VirtioNet::new(pci)?;+/// The loopback address, 127.0.0.1/8.+fn loopback_cidr() -> IpCidr {+    IpCidr::new(IpAddress::Ipv4(Ipv4Address::new(127, 0, 0, 1)), 8)+}++/// Is this address on our loopback network (127.x.x.x)?+pub fn is_loopback(ip: [u8; 4]) -> bool {+    ip[0] == 127+}++/// Set up smoltcp on the first virtio-net card (if any) plus loopback,+/// start DHCP and the netd thread. Returns lines for the boot log.+pub fn init(devices: &[crate::pci::PciDevice]) -> Vec<String> {+    let nic = devices.iter().find(|d| virtio_net::is_virtio_net(d)).and_then(VirtioNet::new);+    let irq = nic.as_ref().map(|n| n.irq);+    let io_base = nic.as_ref().map_or(0, |n| n.io_base);+    let mut device = NetDevice::new(nic);     let mac = device.mac;     let mut config = Config::new(HardwareAddress::Ethernet(EthernetAddress(mac)));     // Random seed: TCP's initial sequence numbers and DHCP transaction ids.     // From virtio-rng (rng.rs) -- guessable ones would let an attacker     // inject packets into our connections.     config.random_seed = crate::virtio::rng::u64();-    let iface = Interface::new(config, &mut device, now());+    let mut iface = Interface::new(config, &mut device, now());+    iface.update_ip_addrs(|addrs| {+        addrs.push(loopback_cidr()).ok();+    });     let mut sockets = SocketSet::new(Vec::new());     let dhcp = sockets.add(dhcpv4::Socket::new());     let dns = sockets.add(dns::Socket::new(&[], vec![None, None, None, None]));-    let irq = device.irq;     // Ephemeral (client) ports start at a random spot in 49152..65000, so     // they're not predictable either.     let first_port = 49152 + (crate::virtio::rng::u64() % (65000 - 49152)) as u16;-    IO_BASE.store(device.io_base, Ordering::Relaxed);+    IO_BASE.store(io_base, Ordering::Relaxed);     without_interrupts(|| {         *NET.lock() = Some(Net { iface, device, sockets, dhcp, dns, config: None, closing: Vec::new(), next_port: first_port });     });-    crate::interrupts::enable_pci_irq(irq);     crate::thread::spawn("netd", netd);-    Some(alloc::format!(-        "eth0: virtio-net, MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}, IRQ {}",-        mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], irq-    ))+    let mut log = alloc::vec![String::from("lo: loopback, 127.0.0.1/8")];+    if let Some(irq) = irq {+        crate::interrupts::enable_pci_irq(irq);+        log.push(alloc::format!(+            "eth0: virtio-net, MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}, IRQ {}",+            mac[0], mac[1], mac[2], mac[3], mac[4], mac[5], irq+        ));+    }+    log }  /// Interrupt handler part (called by interrupts.rs for PCI IRQ lines).@@ -135,14 +157,24 @@ pub fn kick() { }  /// Let smoltcp process everything that's pending: received frames,-/// queued data to send, timers. Then wake blocked socket calls.+/// queued data to send, timers. Then wake blocked socket calls (and+/// poll/epoll waiters). pub fn poll() {     with_net(|n| {-        n.iface.poll(now(), &mut n.device, &mut n.sockets);+        // Frames smoltcp sent to itself (loopback) are only received on+        // the *next* poll, so go round until lo is empty. Bounded, so+        // two sockets chattering away can't keep us here forever.+        for _ in 0..64 {+            n.iface.poll(now(), &mut n.device, &mut n.sockets);+            if !n.device.lo_pending() {+                break;+            }+        }         n.check_dhcp();         n.reap_closing();     });     CHANGED.wake_all();+    crate::poll::notify(); }  /// The network thread: poll, sleep until a frame arrives or a timer is@@ -174,13 +206,17 @@ impl Net {                 dns: c.dns_servers.iter().map(|d| d.octets()).collect(),             }),         };+        // DHCP's address first: smoltcp uses the first address as the+        // source for destinations outside both networks.         self.iface.update_ip_addrs(|addrs| {             addrs.clear();             if let Some(c) = &new_config {                 let ip = Ipv4Address::from(c.address);                 addrs.push(IpCidr::new(IpAddress::Ipv4(ip), c.prefix_len)).ok();             }+            addrs.push(loopback_cidr()).ok();         });+        self.device.own_ip = new_config.as_ref().map(|c| c.address);         self.iface.routes_mut().remove_default_ipv4_route();         let mut servers = Vec::new();         if let Some(c) = &new_config {@@ -229,7 +265,7 @@ impl Net {  /// Is there a network card at all? pub fn present() -> bool {-    with_net(|_| ()).is_some()+    with_net(|n| n.device.nic.is_some()).unwrap_or(false) }  /// Wait (up to `timeout_us`) until DHCP has given us an address.@@ -306,11 +342,12 @@ pub struct NetInfo {  pub fn info() -> NetInfo {     with_net(|n| {-        let s = n.device.stats;+        let Some(nic) = &n.device.nic else { return NetInfo::default() };+        let s = nic.stats;         let mut info = NetInfo {             present: 1,-            mac: n.device.mac,-            irq: n.device.irq,+            mac: nic.mac,+            irq: nic.irq,             rx_packets: s.rx_packets,             rx_bytes: s.rx_bytes,             tx_packets: s.tx_packets,
@@ -12,7 +12,10 @@ //! Blocking: smoltcp never blocks, it just says "no data yet". So e.g. //! `recv` sleeps on `net::CHANGED` (woken after every `net::poll`) until //! its socket has data, the peer closed, or the timeout (SO_RCVTIMEO) ran-//! out. No busy-waiting: a blocked program costs no CPU.+//! out. No busy-waiting: a blocked program costs no CPU. A *nonblocking*+//! socket (O_NONBLOCK) never sleeps: "would block" becomes EAGAIN, and a+//! TCP connect returns EINPROGRESS while the handshake goes on (poll for+//! POLLOUT to learn when it's done). `File::poll` below says what's ready. //! //! A listening TCP socket is a small *backlog* of smoltcp sockets all //! listening on the port (a smoltcp socket handles exactly one@@ -22,6 +25,8 @@ use super::{Net, poll, with_net}; use crate::errno::Errno; use crate::fs::file::{File, KIND_SOCKET, Stat};+use crate::poll::{POLLERR, POLLHUP, POLLIN, POLLOUT};+use core::sync::atomic::{AtomicBool, Ordering}; use alloc::vec; use alloc::vec::Vec; use smoltcp::iface::SocketHandle;@@ -58,6 +63,8 @@ struct State { pub struct Socket {     pub kind: Kind,     state: Mutex<State>,+    /// O_NONBLOCK: never sleep, say EAGAIN instead.+    nonblock: AtomicBool, }  /// An IPv4 address + port, as plain numbers (the syscall layer converts@@ -74,6 +81,12 @@ fn addr(ep: IpEndpoint) -> Addr {     (ip.octets(), ep.port) } +/// Wait for a usable route to `to`: loopback always has one, anything+/// else needs DHCP to have configured us.+fn route_ready(to: Addr) -> Result<(), Errno> {+    if super::is_loopback(to.0) { Ok(()) } else { super::wait_configured(5_000_000).map(|_| ()) }+}+ fn new_tcp() -> tcp::Socket<'static> {     tcp::Socket::new(tcp::SocketBuffer::new(vec![0; TCP_BUFFER]), tcp::SocketBuffer::new(vec![0; TCP_BUFFER])) }@@ -81,8 +94,8 @@ fn new_tcp() -> tcp::Socket<'static> { impl Socket {     /// socket(): a new, unconnected socket.     pub fn new(kind: Kind) -> Result<Socket, Errno> {-        if !super::present() {-            return Err(Errno::ENETDOWN);+        if with_net(|_| ()).is_none() {+            return Err(Errno::ENETDOWN); // net::init not run         }         let mut state = State { handle: None, listeners: Vec::new(), local_port: 0, remote: None, timeout_us: None };         match kind {@@ -102,7 +115,11 @@ impl Socket {                 });             }         }-        Ok(Socket { kind, state: Mutex::new(state) })+        Ok(Socket::with_state(kind, state))+    }++    fn with_state(kind: Kind, state: State) -> Socket {+        Socket { kind, state: Mutex::new(state), nonblock: AtomicBool::new(false) }     }      /// Lock our state and the network (always in that order, interrupts@@ -121,6 +138,10 @@ impl Socket {         timeout_us: Option<u64>,         mut f: impl FnMut(&mut State, &mut Net) -> Option<Result<R, Errno>>,     ) -> Result<R, Errno> {+        if self.nonblock.load(Ordering::Relaxed) {+            // One look, no sleeping.+            return self.locked(&mut f)?.unwrap_or(Err(Errno::EAGAIN));+        }         let deadline = timeout_us.map_or(u64::MAX, crate::time::deadline_after_us);         // Interruptible: Ctrl+C must be able to stop a program waiting         // for the network.@@ -161,7 +182,7 @@ impl Socket {     /// connect(): TCP does the three-way handshake (SYN, SYN-ACK, ACK) and     /// waits for it; UDP/ICMP just remember the default destination.     pub fn connect(&self, to: Addr) -> Result<(), Errno> {-        super::wait_configured(5_000_000)?;+        route_ready(to)?;         if self.kind != Kind::Tcp {             if self.kind == Kind::Udp && without_interrupts(|| self.state.lock().local_port) == 0 {                 self.bind(0)?;@@ -180,6 +201,9 @@ impl Socket {             Ok(())         })??;         poll(); // send the SYN now+        if self.nonblock.load(Ordering::Relaxed) {+            return Err(Errno::EINPROGRESS); // poll for POLLOUT to see it finish+        }         let result = self.wait(Some(CONNECT_TIMEOUT_US), |st, n| {             match n.sockets.get::<tcp::Socket>(st.handle?).state() {                 tcp::State::SynSent | tcp::State::SynReceived => None,@@ -240,14 +264,16 @@ impl Socket {             let peer = n.sockets.get::<tcp::Socket>(handle).remote_endpoint().map(addr).unwrap_or(([0; 4], 0));             let state =                 State { handle: Some(handle), listeners: Vec::new(), local_port: st.local_port, remote: None, timeout_us: None };-            Some(Ok((Socket { kind: Kind::Tcp, state: Mutex::new(state) }, peer)))+            Some(Ok((Socket::with_state(Kind::Tcp, state), peer)))         })     }      /// send()/sendto()/write(). Blocks while the send buffer is full.     pub fn send(&self, data: &[u8], to: Option<Addr>) -> Result<usize, Errno> {         if self.kind == Kind::Udp && without_interrupts(|| self.state.lock().local_port) == 0 {-            super::wait_configured(5_000_000)?;+            if let Some(to) = to {+                route_ready(to)?;+            }             self.bind(0)?; // first send picks a local port, like Linux         }         let sent = self.wait(None, |st, n| {@@ -379,4 +405,74 @@ impl File for Socket {     fn as_socket(&self) -> Option<&Socket> {         Some(self)     }+    fn set_nonblocking(&self, on: bool) {+        self.nonblock.store(on, Ordering::Relaxed);+    }+    fn nonblocking(&self) -> bool {+        self.nonblock.load(Ordering::Relaxed)+    }+    /// What's ready right now (for poll/select/epoll).+    fn poll(&self) -> u16 {+        let kind = self.kind;+        self.locked(|st, n| {+            let mut ready = 0;+            match kind {+                Kind::Tcp if !st.listeners.is_empty() => {+                    // Listening: "readable" = a connection to accept.+                    let waiting = st.listeners.iter().any(|&h| {+                        !matches!(n.sockets.get::<tcp::Socket>(h).state(), tcp::State::Listen | tcp::State::SynReceived)+                    });+                    if waiting {+                        ready |= POLLIN;+                    }+                }+                Kind::Tcp => match st.handle {+                    // Never connected: nothing to read or write.+                    None => ready |= POLLOUT | POLLHUP,+                    Some(h) => {+                        let sock = n.sockets.get::<tcp::Socket>(h);+                        match sock.state() {+                            tcp::State::SynSent | tcp::State::SynReceived => {} // handshake going on+                            tcp::State::Closed => ready |= POLLIN | POLLERR | POLLHUP, // refused or reset+                            _ => {+                                if sock.can_recv() || !sock.may_recv() {+                                    ready |= POLLIN; // data, or EOF+                                }+                                if sock.can_send() || !sock.may_send() {+                                    ready |= POLLOUT; // room (or EPIPE waiting)+                                }+                                if !sock.may_recv() && !sock.may_send() {+                                    ready |= POLLHUP;+                                }+                            }+                        }+                    }+                },+                Kind::Udp => {+                    if let Some(h) = st.handle {+                        let sock = n.sockets.get::<udp::Socket>(h);+                        if sock.can_recv() {+                            ready |= POLLIN;+                        }+                        if sock.can_send() {+                            ready |= POLLOUT;+                        }+                    }+                }+                Kind::Icmp => {+                    if let Some(h) = st.handle {+                        let sock = n.sockets.get::<icmp::Socket>(h);+                        if sock.can_recv() {+                            ready |= POLLIN;+                        }+                        if sock.can_send() {+                            ready |= POLLOUT;+                        }+                    }+                }+            }+            ready+        })+        .unwrap_or(POLLERR)+    } }
@@ -25,8 +25,6 @@ use crate::pci::PciDevice; use alloc::vec::Vec; use core::ptr::{read_volatile, write_volatile}; use core::sync::atomic::{Ordering, fence};-use smoltcp::phy::{self, DeviceCapabilities, Medium};-use smoltcp::time::Instant; use x86_64::PhysAddr; use x86_64::instructions::port::Port; @@ -216,7 +214,7 @@ impl VirtioNet {      /// Take one received frame (copied out; its buffer goes straight back     /// to the device).-    fn receive_frame(&mut self) -> Option<Vec<u8>> {+    pub fn receive_frame(&mut self) -> Option<Vec<u8>> {         let (i, len) = self.rx.pop_used()?;         let len = len.clamp(HEADER_LEN, BUF_SIZE);         let frame = self.rx.buffer(i)[HEADER_LEN..len].to_vec();@@ -235,7 +233,7 @@ impl VirtioNet {     }      /// Send one frame: `fill` writes its `len` bytes.-    fn send_frame<R>(&mut self, len: usize, fill: impl FnOnce(&mut [u8]) -> R) -> R {+    pub fn send_frame<R>(&mut self, len: usize, fill: impl FnOnce(&mut [u8]) -> R) -> R {         let len = len.min(MTU);         self.reclaim_tx();         // QEMU sends synchronously when notified, so a buffer is always@@ -262,45 +260,5 @@ fn align_up(x: usize, align: usize) -> usize {     x.div_ceil(align) * align } -// --- smoltcp glue --------------------------------------------------------------//-// smoltcp asks a `Device` for *tokens*: an RxToken is "a received frame you-// may look at", a TxToken is "permission to build one frame to send". This-// lets smoltcp work without copying in zero-copy drivers; ours copies the-// received frame out (simple, and frames are small).--pub struct RxToken(Vec<u8>);-pub struct TxToken<'a>(&'a mut VirtioNet);--impl phy::Device for VirtioNet {-    type RxToken<'a> = RxToken;-    type TxToken<'a> = TxToken<'a>;--    fn receive(&mut self, _now: Instant) -> Option<(RxToken, TxToken<'_>)> {-        let frame = self.receive_frame()?;-        Some((RxToken(frame), TxToken(self)))-    }--    fn transmit(&mut self, _now: Instant) -> Option<TxToken<'_>> {-        Some(TxToken(self))-    }--    fn capabilities(&self) -> DeviceCapabilities {-        let mut caps = DeviceCapabilities::default();-        caps.medium = Medium::Ethernet;-        caps.max_transmission_unit = MTU;-        caps-    }-}--impl phy::RxToken for RxToken {-    fn consume<R, F: FnOnce(&[u8]) -> R>(self, f: F) -> R {-        f(&self.0)-    }-}--impl phy::TxToken for TxToken<'_> {-    fn consume<R, F: FnOnce(&mut [u8]) -> R>(self, len: usize, f: F) -> R {-        self.0.send_frame(len, f)-    }-}+// The smoltcp glue (Device, RxToken, TxToken) is in device.rs, which+// also adds the loopback queue.
@@ -0,0 +1,420 @@+//! Waiting for several files at once: poll, select and epoll.+//!+//! A server talking to many clients can't call `read` on one of them and+//! block -- the others would wait. Instead it asks the kernel "tell me+//! which of these fds are ready" and then only reads/writes those, which+//! never blocks. That loop ("wait for events, handle each, repeat") is an+//! **event loop**; every GUI toolkit, web server and terminal emulator is+//! built around one. Three interfaces for the same idea:+//!+//!   * `poll(fds, n, timeout)`: an array of {fd, events wanted, events+//!     returned}. Simple; the kernel looks at every fd on every call.+//!   * `select(n, readfds, writefds, exceptfds, timeout)`: the 1983 BSD+//!     original, with bitmaps of fds instead of an array.+//!   * `epoll`: register the fds *once* in an epoll instance (itself an+//!     fd) with epoll_ctl, then epoll_wait returns just the ready ones.+//!     Linux's answer to servers with 10 000 connections.+//!+//! **How our kernel does it.** Every file can say what's ready right now+//! (`File::poll`). Anything that might make a file ready -- data into a+//! pipe, a key typed, a packet for a socket -- calls `notify()`, which+//! bumps a global counter and wakes every poller. A poller checks all its+//! fds; if none is ready it sleeps until the counter changes (or its+//! timeout), then checks again. Simple, and never misses an event; a big+//! kernel instead keeps per-file wait lists so one packet only wakes the+//! pollers of that socket.+//!+//! Simplification: epoll is *level-triggered* only (EPOLLET is accepted+//! but means the same thing: "ready" is reported as long as it lasts).+//! Regular files are always ready (Linux too), and epoll refuses them+//! with EPERM like Linux does.++use crate::errno::Errno;+use crate::fs::file::{File, KIND_DIR, KIND_FILE, Stat};+use crate::syscall::{read_timespec_us, user_slice, user_slice_mut};+use crate::thread::WaitQueue;+use alloc::sync::{Arc, Weak};+use alloc::vec::Vec;+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex;++// poll(2) event bits (Linux's values). epoll uses the same numbers.+pub const POLLIN: u16 = 0x1; // data to read (or EOF / a connection to accept)+pub const POLLPRI: u16 = 0x2; // urgent data+pub const POLLOUT: u16 = 0x4; // room to write+pub const POLLERR: u16 = 0x8; // error (e.g. pipe without readers)+pub const POLLHUP: u16 = 0x10; // other side hung up+pub const POLLNVAL: u16 = 0x20; // not an open fd+pub const POLLRDNORM: u16 = 0x40;+pub const POLLWRNORM: u16 = 0x100;+pub const POLLRDHUP: u16 = 0x2000;++/// Bumped on every possible readiness change (see the top).+static GENERATION: AtomicU64 = AtomicU64::new(0);+/// Pollers sleep here.+static POLLERS: WaitQueue = WaitQueue::new();++/// Something may have become ready: wake all pollers to look again.+pub fn notify() {+    GENERATION.fetch_add(1, Ordering::SeqCst);+    POLLERS.wake_all();+}++/// Add the "normal data" aliases a program may ask for instead.+fn with_aliases(events: u16) -> u16 {+    let mut e = events;+    if events & POLLIN != 0 {+        e |= POLLRDNORM;+    }+    if events & POLLOUT != 0 {+        e |= POLLWRNORM;+    }+    e+}++/// The poll loop: call `check` (with interrupts on) until it returns+/// Some, the deadline (PIT tick; Some(0) = don't wait) passes -> Ok(None),+/// or a signal arrives -> Err.+pub fn wait_ready<T>(deadline: Option<u64>, mut check: impl FnMut() -> Option<T>) -> Result<Option<T>, Errno> {+    loop {+        let generation = GENERATION.load(Ordering::SeqCst);+        if let Some(result) = check() {+            return Ok(Some(result));+        }+        if deadline.is_some_and(|d| crate::interrupts::ticks() >= d) {+            return Ok(None);+        }+        let changed = || (GENERATION.load(Ordering::SeqCst) != generation).then_some(());+        match deadline {+            None => POLLERS.wait_until_intr(changed)?,+            Some(d) => {+                POLLERS.wait_until_deadline_intr(d, changed)?;+            }+        }+    }+}++/// A timeout in milliseconds (negative = forever) as a deadline.+fn ms_deadline(ms: i64) -> Option<u64> {+    match ms {+        ms if ms < 0 => None,+        0 => Some(0),+        ms => Some(crate::time::deadline_after_us(ms as u64 * 1000)),+    }+}++/// A `struct timespec *` timeout (NULL = forever) as a deadline.+fn timespec_deadline(ptr: u64) -> Result<Option<u64>, Errno> {+    if ptr == 0 {+        return Ok(None);+    }+    Ok(Some(match read_timespec_us(ptr)? {+        0 => 0,+        us => crate::time::deadline_after_us(us),+    }))+}++/// ppoll / pselect6 / epoll_pwait: run with signal mask `mask_ptr` (if+/// given) while waiting. If a signal interrupts, the old mask comes back+/// only after its handler ran (signal.rs), like Linux -- otherwise the+/// signal we waited for could end up blocked again before delivery.+fn with_sigmask<T>(mask_ptr: u64, f: impl FnOnce() -> Result<T, Errno>) -> Result<T, Errno> {+    if mask_ptr == 0 {+        return f();+    }+    let mask = u64::from_le_bytes(user_slice(mask_ptr, 8)?.try_into().unwrap());+    crate::signal::set_temporary_mask(mask);+    let result = f();+    if !matches!(result, Err(Errno::EINTR | Errno::ERESTARTNOHAND)) {+        crate::signal::restore_temporary_mask();+    }+    result+}++// ---------------------------------------------------------------------------+// poll / ppoll+// ---------------------------------------------------------------------------++/// poll(fds, nfds, timeout_ms) (7) and ppoll(fds, nfds, *timespec, *sigmask,+/// 8) (271). `struct pollfd {int fd; short events; short revents;}`.+pub fn sys_poll(fds_ptr: u64, nfds: u64, deadline: Option<u64>) -> Result<u64, Errno> {+    if nfds > 1024 {+        return Err(Errno::EINVAL);+    }+    let p = crate::thread::current_process().ok_or(Errno::EINVAL)?;+    let raw = user_slice(fds_ptr, nfds * 8)?;+    // (fd, events wanted, the file: None = not open)+    let entries: Vec<(i32, u16, Option<Arc<dyn File>>)> = raw+        .chunks(8)+        .map(|c| {+            let fd = i32::from_le_bytes(c[0..4].try_into().unwrap());+            let events = u16::from_le_bytes(c[4..6].try_into().unwrap());+            (fd, events, if fd < 0 { None } else { p.file(fd as u64).ok() })+        })+        .collect();+    drop(p);+    let mut revents = alloc::vec![0u16; entries.len()];+    let ready = wait_ready(deadline, || {+        let mut count = 0;+        for (i, (fd, events, file)) in entries.iter().enumerate() {+            revents[i] = match file {+                _ if *fd < 0 => 0, // ignored entry+                None => POLLNVAL,+                Some(f) => f.poll() & (with_aliases(*events) | POLLERR | POLLHUP),+            };+            count += (revents[i] != 0) as u64;+        }+        (count > 0).then_some(count)+    })+    .map_err(|_| Errno::ERESTARTNOHAND)?+    .unwrap_or(0);+    let out = user_slice_mut(fds_ptr, nfds * 8)?;+    for (i, r) in revents.iter().enumerate() {+        out[8 * i + 6..8 * i + 8].copy_from_slice(&r.to_le_bytes());+    }+    Ok(ready)+}++pub fn sys_poll_ms(fds_ptr: u64, nfds: u64, timeout_ms: i64) -> Result<u64, Errno> {+    sys_poll(fds_ptr, nfds, ms_deadline(timeout_ms))+}++pub fn sys_ppoll(fds_ptr: u64, nfds: u64, ts_ptr: u64, mask_ptr: u64) -> Result<u64, Errno> {+    let deadline = timespec_deadline(ts_ptr)?;+    with_sigmask(mask_ptr, || sys_poll(fds_ptr, nfds, deadline))+}++// ---------------------------------------------------------------------------+// select / pselect6+// ---------------------------------------------------------------------------++/// select(nfds, *readfds, *writefds, *exceptfds, *timeval) (23). Each set+/// is a bitmap: bit n = fd n. On return the sets hold only the ready fds.+pub fn sys_select(nfds: u64, sets: [u64; 3], deadline: Option<u64>) -> Result<u64, Errno> {+    if nfds > 1024 {+        return Err(Errno::EINVAL);+    }+    let words = nfds.div_ceil(64) as usize;+    // Read the three bitmaps (a null pointer = empty set).+    let mut wanted = [[0u64; 16]; 3];+    for (k, &ptr) in sets.iter().enumerate() {+        if ptr != 0 {+            let bytes = user_slice(ptr, words as u64 * 8)?;+            for w in 0..words {+                wanted[k][w] = u64::from_le_bytes(bytes[8 * w..8 * w + 8].try_into().unwrap());+            }+        }+    }+    let p = crate::thread::current_process().ok_or(Errno::EINVAL)?;+    let mut files: Vec<(usize, Arc<dyn File>)> = Vec::new();+    for fd in 0..nfds as usize {+        if (0..3).any(|k| wanted[k][fd / 64] & (1 << (fd % 64)) != 0) {+            files.push((fd, p.file(fd as u64)?)); // EBADF for a closed fd+        }+    }+    drop(p);+    let mut result = [[0u64; 16]; 3];+    let ready = wait_ready(deadline, || {+        result = [[0u64; 16]; 3];+        let mut count = 0;+        for (fd, file) in &files {+            let events = file.poll();+            let bit = 1u64 << (fd % 64);+            // readable: data, EOF/hangup or an error (the read won't block)+            let tests = [POLLIN | POLLHUP | POLLERR, POLLOUT | POLLERR, POLLPRI];+            for k in 0..3 {+                if wanted[k][fd / 64] & bit != 0 && events & tests[k] != 0 {+                    result[k][fd / 64] |= bit;+                    count += 1;+                }+            }+        }+        (count > 0).then_some(count)+    })+    .map_err(|_| Errno::ERESTARTNOHAND)?+    .unwrap_or(0);+    for (k, &ptr) in sets.iter().enumerate() {+        if ptr != 0 {+            let out = user_slice_mut(ptr, words as u64 * 8)?;+            for w in 0..words {+                out[8 * w..8 * w + 8].copy_from_slice(&result[k][w].to_le_bytes());+            }+        }+    }+    Ok(ready)+}++/// select's `struct timeval {seconds, microseconds}` (NULL = forever).+pub fn sys_select_tv(nfds: u64, r: u64, w: u64, e: u64, tv_ptr: u64) -> Result<u64, Errno> {+    let deadline = if tv_ptr == 0 {+        None+    } else {+        let tv = user_slice(tv_ptr, 16)?;+        let secs = i64::from_le_bytes(tv[..8].try_into().unwrap());+        let usecs = i64::from_le_bytes(tv[8..].try_into().unwrap());+        if secs < 0 || !(0..1_000_000).contains(&usecs) {+            return Err(Errno::EINVAL);+        }+        let us = secs as u64 * 1_000_000 + usecs as u64;+        Some(if us == 0 { 0 } else { crate::time::deadline_after_us(us) })+    };+    sys_select(nfds, [r, w, e], deadline)+}++/// pselect6(nfds, r, w, e, *timespec, *{sigmask ptr, size}) (270).+pub fn sys_pselect6(nfds: u64, r: u64, w: u64, e: u64, ts_ptr: u64, mask_arg: u64) -> Result<u64, Errno> {+    let deadline = timespec_deadline(ts_ptr)?;+    let mask_ptr = if mask_arg == 0 { 0 } else { u64::from_le_bytes(user_slice(mask_arg, 8)?.try_into().unwrap()) };+    with_sigmask(mask_ptr, || sys_select(nfds, [r, w, e], deadline))+}++// ---------------------------------------------------------------------------+// epoll+// ---------------------------------------------------------------------------++pub const EPOLL_CTL_ADD: u64 = 1;+pub const EPOLL_CTL_DEL: u64 = 2;+pub const EPOLL_CTL_MOD: u64 = 3;+pub const EPOLLET: u32 = 1 << 31;+pub const EPOLLONESHOT: u32 = 1 << 30;+pub const EPOLL_CLOEXEC: u64 = 0o2000000;++/// One watched fd.+struct Interest {+    fd: u64,+    /// Weak: closing the file (its last fd) makes the entry go away, like+    /// Linux; the epoll instance doesn't keep files open.+    file: Weak<dyn File>,+    events: u32,+    data: u64,+    /// EPOLLONESHOT: reported once, then off until EPOLL_CTL_MOD.+    disabled: bool,+}++/// An epoll instance: the "interest list".+pub struct Epoll {+    interest: Mutex<Vec<Interest>>,+}++impl Epoll {+    /// The ready entries as (events, data), at most `max`.+    fn ready(&self, max: usize) -> Vec<(u32, u64)> {+        let mut out = Vec::new();+        let mut list = self.interest.lock();+        list.retain(|i| i.file.strong_count() > 0); // closed files drop out+        for i in list.iter_mut().filter(|i| !i.disabled) {+            let Some(file) = i.file.upgrade() else { continue };+            let events = file.poll() as u32 & (i.events | (POLLERR | POLLHUP) as u32);+            if events != 0 {+                out.push((events, i.data));+                if i.events & EPOLLONESHOT != 0 {+                    i.disabled = true;+                }+                if out.len() == max {+                    break;+                }+            }+        }+        out+    }+}++impl File for Epoll {+    fn read(&self, _buf: &mut [u8]) -> Result<usize, Errno> {+        Err(Errno::EINVAL)+    }+    fn write(&self, _buf: &[u8]) -> Result<usize, Errno> {+        Err(Errno::EINVAL)+    }+    fn stat(&self) -> Result<Stat, Errno> {+        Ok(Stat { kind: crate::fs::file::KIND_DEVICE, size: 0, mode: 0o600 })+    }+    /// An epoll fd is readable when one of its files is ready (so epoll+    /// instances can be polled themselves). Doesn't consume ONESHOTs.+    fn poll(&self) -> u16 {+        let list = self.interest.lock();+        let ready = list.iter().any(|i| {+            !i.disabled && i.file.upgrade().is_some_and(|f| f.poll() as u32 & (i.events | (POLLERR | POLLHUP) as u32) != 0)+        });+        if ready { POLLIN } else { 0 }+    }+    fn as_epoll(&self) -> Option<&Epoll> {+        Some(self)+    }+}++/// epoll_create1(flags) (291); epoll_create(size) (213) is the same+/// without flags.+pub fn sys_epoll_create1(flags: u64) -> Result<u64, Errno> {+    if flags & !EPOLL_CLOEXEC != 0 {+        return Err(Errno::EINVAL);+    }+    let p = crate::thread::current_process().ok_or(Errno::EINVAL)?;+    p.add_file_with(Arc::new(Epoll { interest: Mutex::new(Vec::new()) }), 0, flags & EPOLL_CLOEXEC != 0)+}++/// `struct epoll_event {u32 events; u64 data;}` -- packed: 12 bytes.+fn read_event(ptr: u64) -> Result<(u32, u64), Errno> {+    let b = user_slice(ptr, 12)?;+    Ok((u32::from_le_bytes(b[0..4].try_into().unwrap()), u64::from_le_bytes(b[4..12].try_into().unwrap())))+}++/// epoll_ctl(epfd, op, fd, *event) (233).+pub fn sys_epoll_ctl(epfd: u64, op: u64, fd: u64, event_ptr: u64) -> Result<u64, Errno> {+    let p = crate::thread::current_process().ok_or(Errno::EINVAL)?;+    let ep_file = p.file(epfd)?;+    let file = p.file(fd)?;+    let ep = ep_file.as_epoll().ok_or(Errno::EINVAL)?;+    if Arc::ptr_eq(&ep_file, &file) {+        return Err(Errno::EINVAL); // can't watch itself+    }+    if matches!(file.stat()?.kind, KIND_FILE | KIND_DIR) {+        return Err(Errno::EPERM); // always ready: pointless (Linux says EPERM too)+    }+    let mut list = ep.interest.lock();+    list.retain(|i| i.file.strong_count() > 0);+    let pos = list.iter().position(|i| i.fd == fd && i.file.upgrade().is_some_and(|f| Arc::ptr_eq(&f, &file)));+    match op {+        EPOLL_CTL_ADD | EPOLL_CTL_MOD => {+            let (events, data) = read_event(event_ptr)?;+            match (op, pos) {+                (EPOLL_CTL_ADD, Some(_)) => return Err(Errno::EEXIST),+                (EPOLL_CTL_ADD, None) => list.push(Interest { fd, file: Arc::downgrade(&file), events, data, disabled: false }),+                (_, None) => return Err(Errno::ENOENT),+                (_, Some(i)) => list[i] = Interest { fd, file: Arc::downgrade(&file), events, data, disabled: false },+            }+        }+        EPOLL_CTL_DEL => {+            list.remove(pos.ok_or(Errno::ENOENT)?);+        }+        _ => return Err(Errno::EINVAL),+    }+    drop(list);+    notify(); // a waiter may care about the new entry+    Ok(0)+}++/// epoll_wait(epfd, *events, max, timeout_ms) (232) and epoll_pwait(...,+/// *sigmask, 8) (281).+pub fn sys_epoll_pwait(epfd: u64, events_ptr: u64, max: i64, timeout_ms: i64, mask_ptr: u64) -> Result<u64, Errno> {+    if max <= 0 || max > 1024 {+        return Err(Errno::EINVAL);+    }+    let file = crate::thread::current_process().ok_or(Errno::EINVAL)?.file(epfd)?;+    let ep = file.as_epoll().ok_or(Errno::EINVAL)?;+    user_slice_mut(events_ptr, max as u64 * 12)?; // check first+    let ready = with_sigmask(mask_ptr, || {+        wait_ready(ms_deadline(timeout_ms), || {+            let r = ep.ready(max as usize);+            (!r.is_empty()).then_some(r)+        })+        .map_err(|_| Errno::EINTR) // epoll_wait is never restarted (Linux)+    })?+    .unwrap_or_default();+    let out = user_slice_mut(events_ptr, ready.len() as u64 * 12)?;+    for (i, (events, data)) in ready.iter().enumerate() {+        out[12 * i..12 * i + 4].copy_from_slice(&events.to_le_bytes());+        out[12 * i + 4..12 * i + 12].copy_from_slice(&data.to_le_bytes());+    }+    Ok(ready.len() as u64)+}
@@ -36,20 +36,29 @@ //! sends SIGINT to that whole group -- every program of the pipeline, but //! not the shell (tty.rs). //!+//! **Threads** (stage 15). A process can run several threads: each is a+//! `Task` with its own registers, kernel stack, thread id, TLS pointer and+//! signal mask, but they all share the process's memory, files and signal+//! handlers. `clone(CLONE_VM | CLONE_THREAD | ...)` makes one (that's what+//! pthread_create does). The first thread's id equals the pid. `exit` ends+//! one thread; `exit_group` (what C's exit() calls) ends them all. The+//! process itself ends when its last thread does.+//! //! **Memory lifetime.** A process's frames and page tables are freed when //! the last `Arc<Process>` goes away (dropping its `Mm`). That's always-//! after its thread is gone: the thread holds an Arc until the scheduler-//! frees it, which happens only once the CPU has switched to another-//! thread and address space. So no CPU can still be using the page tables.+//! after its threads are gone: each holds an Arc (through its Task) until+//! the scheduler frees it, which happens only once the CPU has switched to+//! another thread and address space. So no CPU can still be using the page+//! tables.  use crate::elf::{self, ElfError}; use crate::errno::Errno; use crate::fs::{self, file::File}; use crate::mm::Mm;-use crate::signal::{self, SigInfo, SignalState};+use crate::signal::{self, SigInfo, SignalState, ThreadSig}; use crate::thread::{self, WaitQueue}; use crate::trap::TrapFrame;-use crate::{serial_println, tty::Console};+use crate::serial_println; use alloc::{collections::BTreeMap, string::String, sync::Arc, vec, vec::Vec}; use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; use spin::Mutex;@@ -66,6 +75,8 @@ pub const MAX_ARGS_SIZE: usize = 64 * 1024; /// Max processes at once: fork() fails with EAGAIN beyond that, which is /// what stops a fork bomb from eating all memory. pub const MAX_PROCESSES: usize = 64;+/// Max threads in one process (clone fails with EAGAIN beyond that).+pub const MAX_THREADS: usize = 64; /// Environment of processes the kernel starts. pub const DEFAULT_ENV: [&str; 3] = ["PATH=/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"]; @@ -152,12 +163,56 @@ impl FdTable {     } } +/// One thread of a process (what Linux calls a "task"). The scheduler's+/// kernel thread that runs it holds an `Arc<Task>` (thread.rs).+pub struct Task {+    /// Thread id (gettid). The first thread's is the pid.+    tid: AtomicU64,+    pub process: Arc<Process>,+    /// The scheduler thread running it (set when it starts).+    kthread: AtomicUsize,+    /// Its FS segment base: the thread-local-storage pointer (arch_prctl,+    /// clone's CLONE_SETTLS). Loaded into the CPU on every switch.+    pub fs_base: AtomicU64,+    /// set_tid_address / CLONE_CHILD_CLEARTID: when the thread exits, write+    /// 0 there and futex-wake it (that's how pthread_join waits).+    pub clear_child_tid: AtomicU64,+    /// Registers it starts user mode with (taken when it starts).+    start: Mutex<Option<TrapFrame>>,+    /// Told to exit (another thread called exit_group or execve).+    pub killed: AtomicBool,+    /// Its own signal mask and pending signals. Locked with interrupts off,+    /// after the process's `signals` if both are needed.+    pub sig: Mutex<ThreadSig>,+}++impl Task {+    pub fn tid(&self) -> u64 {+        self.tid.load(Ordering::Relaxed)+    }++    /// Wake it if it's blocked (it then checks for signals / being killed).+    pub fn wake(&self) {+        let kthread = self.kthread.load(Ordering::Acquire);+        if kthread != usize::MAX {+            thread::wake(kthread);+        }+    }+}+ pub struct Process {     pub pid: u64,     /// Parent pid; 0 = started by the kernel itself, ORPHAN = parent exited.     pub parent: AtomicU64,     /// Process group (job control; see the top of this file).     pgid: AtomicU64,+    /// Session (setsid; a login session = the processes of one terminal).+    sid: AtomicU64,+    /// Controlling terminal (tty.rs): what /dev/tty opens, whose Ctrl+C+    /// reaches this process's group.+    pub ctty: Mutex<Option<Arc<crate::tty::Tty>>>,+    /// Path of the running program (/proc/self/exe).+    pub exe: Mutex<String>,     /// Program name (changes with execve).     name: Mutex<String>,     /// Print every syscall this process makes (`run --trace`; survives exec).@@ -166,12 +221,18 @@ pub struct Process {     pub cwd: Mutex<String>,     /// The address space.     pub mm: Mutex<Mm>,-    /// Its thread's id (set right after the thread is created).-    tid: AtomicUsize,-    /// CPUs its thread has run on (bit per CPU; kept up by the scheduler).+    /// Its live threads. Locked with interrupts off.+    tasks: Mutex<Vec<Arc<Task>>>,+    /// Scheduler threads that ran its threads and may not be freed yet+    /// (reaping waits for them). Locked with interrupts off.+    kthreads: Mutex<Vec<thread::Tid>>,+    /// exit_group was called (or a fatal signal arrived): every thread+    /// exits, and the process ends with this status. Interrupts off.+    exiting: Mutex<Option<ExitStatus>>,+    /// A thread of it exited (execve waits for the others to go).+    thread_exit: WaitQueue,+    /// CPUs its threads have run on (bit per CPU; kept up by the scheduler).     pub ran_on: AtomicU32,-    /// Registers its thread starts user mode with (taken by the thread).-    start: Mutex<Option<TrapFrame>>,     /// The environment it was started with (`spawn` passes it on).     env: Mutex<Vec<String>>,     fds: Mutex<FdTable>,@@ -180,7 +241,8 @@ pub struct Process {     exited: WaitQueue,     /// Its own wait4 sleeps here; children wake it when they change state.     pub child_wait: WaitQueue,-    /// Signal handlers, mask, pending signals. Locked with interrupts off.+    /// Signal handlers, process-wide pending signals, stop state. Locked+    /// with interrupts off. (Masks are per thread: `Task::sig`.)     pub signals: Mutex<SignalState>,     /// vfork: the parent waits until this child execs or exits.     vfork_done: AtomicBool,@@ -203,13 +265,17 @@ pub struct SpawnArgs {     pub parent: u64,     /// Process group: 0 = a new group of its own.     pub pgid: u64,+    /// Session: 0 = a new session of its own.+    pub sid: u64,+    /// Controlling terminal.+    pub ctty: Option<Arc<crate::tty::Tty>>,     pub trace: bool, }  impl SpawnArgs {     /// Defaults for a process started by the kernel: console stdio, cwd "/".     pub fn kernel(name: &str) -> SpawnArgs {-        let console: Arc<dyn File> = Arc::new(Console);+        let console: Arc<dyn File> = crate::tty::console_file();         SpawnArgs {             args: vec![String::from(name)],             env: DEFAULT_ENV.iter().map(|s| String::from(*s)).collect(),@@ -217,6 +283,8 @@ impl SpawnArgs {             cwd: String::from("/"),             parent: 0,             pgid: 0,+            sid: 0,+            ctty: Some(crate::tty::console()),             trace: false,         }     }@@ -258,13 +326,18 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc         pid,         parent: AtomicU64::new(spawn.parent),         pgid: AtomicU64::new(if spawn.pgid == 0 { pid } else { spawn.pgid }),+        sid: AtomicU64::new(if spawn.sid == 0 { pid } else { spawn.sid }),+        ctty: Mutex::new(spawn.ctty),+        exe: Mutex::new(String::from(path)),         name: Mutex::new(String::from(name)),         trace: AtomicBool::new(spawn.trace),         cwd: Mutex::new(spawn.cwd),         mm: Mutex::new(mm),-        tid: AtomicUsize::new(usize::MAX),+        tasks: Mutex::new(Vec::new()),+        kthreads: Mutex::new(Vec::new()),+        exiting: Mutex::new(None),+        thread_exit: WaitQueue::new(),         ran_on: AtomicU32::new(0),-        start: Mutex::new(Some(start)),         env: Mutex::new(spawn.env),         fds: Mutex::new(FdTable::new([stdin, stdout, stderr])),         status: Mutex::new(None),@@ -274,27 +347,49 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc         vfork_done: AtomicBool::new(false),     };     serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);-    Ok(start_thread(process))+    Ok(start_process(process, start, 0, 0)) } -/// Put the process in the table and give it a thread.-fn start_thread(process: Process) -> Arc<Process> {+/// Put the process in the table and start its first thread (tid = pid).+fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Process> {     let process = Arc::new(process);-    let l4 = process.mm.lock().l4;     without_interrupts(|| PROCESSES.lock().insert(process.pid, process.clone()));-    let name = process.name();-    let tid = thread::spawn_with(&name, user_thread_entry, l4, Some(process.clone()));-    process.tid.store(tid, Ordering::Release);+    start_task(&process, process.pid, start, fs_base, blocked);     process } -/// First code of every process thread (still ring 0, but already running+/// Start a thread `tid` of `process` that enters user mode with `start`.+fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Task> {+    let task = Arc::new(Task {+        tid: AtomicU64::new(tid),+        process: process.clone(),+        kthread: AtomicUsize::new(usize::MAX),+        fs_base: AtomicU64::new(fs_base),+        clear_child_tid: AtomicU64::new(0),+        start: Mutex::new(Some(start)),+        killed: AtomicBool::new(false),+        sig: Mutex::new(ThreadSig::new(blocked)),+    });+    without_interrupts(|| process.tasks.lock().push(task.clone()));+    let l4 = process.mm.lock().l4;+    thread::spawn_with(&process.name(), user_thread_entry, l4, Some(task.clone()));+    task+}++/// First code of every user thread (still ring 0, but already running /// with the process's CR3 and kernel stack): enter user mode with the /// prepared registers. fn user_thread_entry() {-    let process = thread::current_process().expect("process thread without process");-    let frame = process.start.lock().take().expect("no start registers");-    drop(process); // return_to_user never returns: don't leak the Arc+    let task = thread::current_task().expect("user thread without task");+    let me = thread::current_id();+    task.kthread.store(me, Ordering::Release);+    without_interrupts(|| task.process.kthreads.lock().push(me)); // reap() joins it+    let frame = task.start.lock().take().expect("no start registers");+    let doomed = task.killed.load(Ordering::Acquire) || task.process.exiting().is_some();+    drop(task); // return_to_user never returns: don't leak the Arc+    if doomed {+        exit_thread(ExitStatus::Exited(0)); // the group is going away already+    }     crate::trap::return_to_user(frame) } @@ -482,13 +577,31 @@ impl Process {         self.env.lock().clone()     } -    /// Wake its thread if it's blocked (it then checks for signals).+    pub fn sid(&self) -> u64 {+        self.sid.load(Ordering::Relaxed)+    }++    /// Wake its threads if they're blocked (they then check for signals).     pub fn wake(&self) {-        let tid = self.tid.load(Ordering::Acquire);-        if tid != usize::MAX {-            thread::wake(tid);+        for task in self.tasks() {+            task.wake();         }     }++    /// Its live threads.+    pub fn tasks(&self) -> Vec<Arc<Task>> {+        without_interrupts(|| self.tasks.lock().clone())+    }++    /// The thread with id `tid`, if it's one of ours and alive.+    pub fn task(&self, tid: u64) -> Option<Arc<Task>> {+        without_interrupts(|| self.tasks.lock().iter().find(|t| t.tid() == tid).cloned())+    }++    /// Set once exit_group has been called (all threads are on their way out).+    pub fn exiting(&self) -> Option<ExitStatus> {+        without_interrupts(|| *self.exiting.lock())+    } }  /// Look up a live (or not yet reaped) process.@@ -513,76 +626,123 @@ pub fn group_members(pgid: u64) -> Vec<Arc<Process>> {     }) } -/// End the current process (exit syscall, or killed by a signal).+/// End the whole current process: every thread of it (exit_group, a+/// fatal signal). The first status given wins.+pub fn exit_group(status: ExitStatus) -> ! {+    if let Some(p) = thread::current_process() {+        without_interrupts(|| {+            p.exiting.lock().get_or_insert(status);+        });+        // The other threads notice at their next return to user mode, or+        // right away if they're sleeping (signal::interrupted).+        p.wake();+        signal::wake_stopped();+    }+    exit_thread(status)+}++/// The old name: end the current process. pub fn exit_current(status: ExitStatus) -> ! {-    if let Some(process) = thread::current_process() {-        // Close all files (a pipe's reader sees EOF once its writers are gone).-        let files = core::mem::take(&mut process.fds.lock().files);-        drop(files);-        crate::tty::process_exited(process.pid);-        let name = process.name();-        match status {-            // Serial only: on screen the shell reports exit codes itself.-            ExitStatus::Exited(code) => {-                serial_println!("[process] pid {} ({}) exited with code {}", process.pid, name, code)-            }-            // A crash (SIGSEGV...) is worth a line on screen; a signal-            // somebody sent on purpose (Ctrl+C, kill) isn't.-            ExitStatus::Killed(sig, why) if why != signal::name(sig) => {-                crate::kprintln!("[process] pid {} ({}) killed: {} ({})", process.pid, name, why, signal::name(sig))-            }-            ExitStatus::Killed(sig, _) => {-                serial_println!("[process] pid {} ({}) killed by {}", process.pid, name, signal::name(sig))-            }+    exit_group(status)+}++/// End the calling thread (the exit syscall). If it's the last one, the+/// process ends too -- with `status`, unless exit_group already set one.+pub fn exit_thread(status: ExitStatus) -> ! {+    if let Some(task) = thread::current_task() {+        let p = task.process.clone();+        // pthread_join sleeps on this word with a futex until it's 0.+        let ctid = task.clear_child_tid.swap(0, Ordering::Relaxed);+        if ctid != 0 && ctid % 4 == 0 && crate::mm::user_access_ok(ctid, 4, true) {+            unsafe { core::ptr::write_volatile(ctid as *mut u32, 0) };+            crate::futex::wake(&p, ctid, 1);+        }+        let last = without_interrupts(|| {+            let mut tasks = p.tasks.lock();+            tasks.retain(|t| !Arc::ptr_eq(t, &task));+            tasks.is_empty()+        });+        if !last {+            serial_println!("[process] pid {} thread {} exited", p.pid, task.tid());+        }+        drop(task);+        p.thread_exit.wake_all();+        if last {+            let status = p.exiting().unwrap_or(status);+            process_exit(p, status);         }-        // No preemption from here on: we're about to disappear anyway.-        x86_64::instructions::interrupts::disable();-        let removed = {-            // The table lock makes "set my status" and "look at my parent"-            // one step, so a parent exiting at the same time on another CPU-            // either sees our status (and reaps us) or orphans us first (and-            // we reap ourselves). Never both, never neither.-            let mut table = PROCESSES.lock();-            *process.status.lock() = Some(status);-            let mut removed = Vec::new();-            let children: Vec<Arc<Process>> = table.values().filter(|c| c.parent() == process.pid).cloned().collect();-            for child in children {-                if child.status().is_some() {-                    removed.extend(table.remove(&child.pid)); // exited: reap it-                } else {-                    child.parent.store(ORPHAN, Ordering::Relaxed); // still running-                }-            }-            // Nobody will wait for us if we're an orphan, or if our parent-            // said so by ignoring SIGCHLD explicitly (POSIX): reap ourselves.-            let parent_ignores = table.get(&process.parent()).is_some_and(|p| {-                let action = p.signals.lock().action(signal::SIGCHLD);-                action.handler == signal::SIG_IGN || action.flags & signal::SA_NOCLDWAIT != 0-            });-            if process.parent() == ORPHAN || parent_ignores {-                removed.extend(table.remove(&process.pid));-            }-            removed-        };-        drop(removed); // (frees their memory if they're completely gone)-        process.exited.wake_all(); // the kernel may be blocked in wait()-        process.vfork_done.store(true, Ordering::Release);-        VFORK_WAIT.wake_all();-        let code = match status {-            ExitStatus::Exited(_) => signal::CLD_EXITED,-            ExitStatus::Killed(sig, _) if signal::default_action(sig) == signal::DefaultAction::Core => signal::CLD_DUMPED,-            ExitStatus::Killed(..) => signal::CLD_KILLED,-        };-        let si_status = match status {-            ExitStatus::Exited(c) => c as i32,-            ExitStatus::Killed(sig, _) => sig as i32,-        };-        signal::notify_parent(&process, code, si_status);-        drop(process);     }     thread::exit_current() } +/// The last thread of `process` is exiting: close everything, become a+/// zombie, tell the parent.+fn process_exit(process: Arc<Process>, status: ExitStatus) {+    // Close all files (a pipe's reader sees EOF once its writers are gone).+    let files = core::mem::take(&mut process.fds.lock().files);+    drop(files);+    crate::tty::process_exited(&process); // (raw mode, controlling terminal)+    let name = process.name();+    match status {+        // Serial only: on screen the shell reports exit codes itself.+        ExitStatus::Exited(code) => {+            serial_println!("[process] pid {} ({}) exited with code {}", process.pid, name, code)+        }+        // A crash (SIGSEGV...) is worth a line on screen; a signal+        // somebody sent on purpose (Ctrl+C, kill) isn't.+        ExitStatus::Killed(sig, why) if why != signal::name(sig) => {+            crate::kprintln!("[process] pid {} ({}) killed: {} ({})", process.pid, name, why, signal::name(sig))+        }+        ExitStatus::Killed(sig, _) => {+            serial_println!("[process] pid {} ({}) killed by {}", process.pid, name, signal::name(sig))+        }+    }+    // No preemption from here on: we're about to disappear anyway.+    x86_64::instructions::interrupts::disable();+    let removed = {+        // The table lock makes "set my status" and "look at my parent"+        // one step, so a parent exiting at the same time on another CPU+        // either sees our status (and reaps us) or orphans us first (and+        // we reap ourselves). Never both, never neither.+        let mut table = PROCESSES.lock();+        *process.status.lock() = Some(status);+        let mut removed = Vec::new();+        let children: Vec<Arc<Process>> = table.values().filter(|c| c.parent() == process.pid).cloned().collect();+        for child in children {+            if child.status().is_some() {+                removed.extend(table.remove(&child.pid)); // exited: reap it+            } else {+                child.parent.store(ORPHAN, Ordering::Relaxed); // still running+            }+        }+        // Nobody will wait for us if we're an orphan, or if our parent+        // said so by ignoring SIGCHLD explicitly (POSIX): reap ourselves.+        let parent_ignores = table.get(&process.parent()).is_some_and(|p| {+            let action = p.signals.lock().action(signal::SIGCHLD);+            action.handler == signal::SIG_IGN || action.flags & signal::SA_NOCLDWAIT != 0+        });+        if process.parent() == ORPHAN || parent_ignores {+            removed.extend(table.remove(&process.pid));+        }+        removed+    };+    drop(removed); // (frees their memory if they're completely gone)+    process.exited.wake_all(); // the kernel may be blocked in wait()+    process.vfork_done.store(true, Ordering::Release);+    VFORK_WAIT.wake_all();+    let code = match status {+        ExitStatus::Exited(_) => signal::CLD_EXITED,+        ExitStatus::Killed(sig, _) if signal::default_action(sig) == signal::DefaultAction::Core => signal::CLD_DUMPED,+        ExitStatus::Killed(..) => signal::CLD_KILLED,+    };+    let si_status = match status {+        ExitStatus::Exited(c) => c as i32,+        ExitStatus::Killed(sig, _) => sig as i32,+    };+    signal::notify_parent(&process, code, si_status);+    drop(process);+}+ /// Block (the calling thread) until `process` has ended and its thread is /// gone, then remove it from the process table. Used by the kernel (tests, /// the shell supervisor). Once the caller drops its own `Arc`, all of the@@ -593,18 +753,15 @@ pub fn wait(process: &Process) -> ExitStatus {     status } -/// Wait for the (exited) process's thread to be freed, then remove the+/// Wait for the (exited) process's threads to be freed, then remove the /// process from the table. fn reap(process: &Process) {-    // (The tid is stored right after the thread is created; on another CPU-    // the thread may have run and exited before that.)-    let tid = loop {-        match process.tid.load(Ordering::Acquire) {-            usize::MAX => thread::yield_now(),-            tid => break tid,-        }-    };-    thread::join(tid);+    // Every thread registers itself before it can exit, and the status is+    // only set when the last one exits: so the list is complete here.+    let kthreads = without_interrupts(|| core::mem::take(&mut *process.kthreads.lock()));+    for tid in kthreads {+        thread::join(tid);+    }     let removed = without_interrupts(|| PROCESSES.lock().remove(&process.pid));     drop(removed); }@@ -613,6 +770,8 @@ fn reap(process: &Process) { pub const WNOHANG: u64 = 1; pub const WUNTRACED: u64 = 2; pub const WCONTINUED: u64 = 8;+/// Linux's "wait for threads too" flags: accepted, nothing to do here.+const WALL_FLAGS: u64 = 0x2000_0000 | 0x4000_0000 | 0x8000_0000;  /// Kernel-side wait for one child (the tests use it): EAGAIN with WNOHANG /// while it runs.@@ -637,7 +796,7 @@ enum Found { /// -1: any child; 0: any child in my process group; < -1: any child in /// group -pid. Returns (pid, status), or pid 0 with WNOHANG and no news. pub fn wait4(me: &Process, pid: i64, options: u64) -> Result<(u64, i32), Errno> {-    if options & !(WNOHANG | WUNTRACED | WCONTINUED) != 0 {+    if options & !(WNOHANG | WUNTRACED | WCONTINUED | WALL_FLAGS) != 0 {         return Err(Errno::EINVAL);     }     let matches = |c: &Process| {@@ -706,17 +865,26 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -     }     let pid = NEXT_PID.fetch_add(1, Ordering::Relaxed);     let signals = without_interrupts(|| parent.signals.lock().fork());+    // Only the calling thread is copied, with its TLS pointer and mask.+    let me = thread::current_task();+    let fs_base = me.as_ref().map_or(0, |t| t.fs_base.load(Ordering::Relaxed));+    let blocked = me.as_ref().map_or(0, |t| without_interrupts(|| t.sig.lock().blocked));     let child = Process {         pid,         parent: AtomicU64::new(parent.pid),         pgid: AtomicU64::new(parent.pgid()),+        sid: AtomicU64::new(parent.sid()),+        ctty: Mutex::new(parent.ctty.lock().clone()),+        exe: Mutex::new(parent.exe.lock().clone()),         name: Mutex::new(parent.name()),         trace: AtomicBool::new(parent.tracing()),         cwd: Mutex::new(parent.cwd.lock().clone()),         mm: Mutex::new(mm),-        tid: AtomicUsize::new(usize::MAX),+        tasks: Mutex::new(Vec::new()),+        kthreads: Mutex::new(Vec::new()),+        exiting: Mutex::new(None),+        thread_exit: WaitQueue::new(),         ran_on: AtomicU32::new(0),-        start: Mutex::new(Some(start)),         env: Mutex::new(parent.env()),         fds: Mutex::new(parent.fds.lock().clone()), // same open files (Arc clones)         status: Mutex::new(None),@@ -726,7 +894,7 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -         vfork_done: AtomicBool::new(false),     };     serial_println!("[process] pid {} forked pid {}", parent.pid, pid);-    let child = start_thread(child);+    let child = start_process(child, start, fs_base, blocked);     if vfork {         // The parent sleeps until the child has exec'd or exited (real         // vfork shares the memory meanwhile; we just use copy-on-write).@@ -735,9 +903,57 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -     Ok(pid) } +/// clone(CLONE_THREAD ...): a new thread in process `p`, starting like the+/// caller returning from clone with rax = 0, on stack `stack`, with TLS+/// pointer `tls` (if Some). Returns its tid, which is also written to the+/// user addresses in `set_tid` (CLONE_PARENT_SETTID / CHILD_SETTID) before+/// the thread can run.+pub fn clone_thread(p: &Arc<Process>, frame: &TrapFrame, stack: u64, tls: Option<u64>, clear_tid: u64, set_tid: &[u64]) -> Result<u64, Errno> {+    let me = thread::current_task().ok_or(Errno::EINVAL)?;+    if p.tasks().len() >= MAX_THREADS || p.exiting().is_some() {+        return Err(Errno::EAGAIN);+    }+    let mut start = *frame;+    start.rax = 0; // "you're the new thread"+    if stack != 0 {+        start.rsp = stack;+    }+    for &ptr in set_tid {+        crate::syscall::user_slice_mut(ptr, 4)?; // check first+    }+    let tid = NEXT_PID.fetch_add(1, Ordering::Relaxed);+    for &ptr in set_tid {+        crate::syscall::user_slice_mut(ptr, 4)?.copy_from_slice(&(tid as u32).to_le_bytes());+    }+    let fs_base = tls.unwrap_or_else(|| me.fs_base.load(Ordering::Relaxed));+    let blocked = without_interrupts(|| me.sig.lock().blocked);+    // Forget scheduler threads of threads that are completely gone.+    without_interrupts(|| p.kthreads.lock().retain(|&k| thread::exists(k)));+    let task = start_task(p, tid, start, fs_base, blocked);+    task.clear_child_tid.store(clear_tid, Ordering::Relaxed);+    serial_println!("[process] pid {} new thread {}", p.pid, tid);+    Ok(tid)+}++/// execve in a process with several threads: the others must go first+/// (the new program starts with one thread). Linux calls this de_thread.+fn kill_other_threads(p: &Arc<Process>) {+    let Some(me) = thread::current_task() else { return };+    for t in p.tasks() {+        if !Arc::ptr_eq(&t, &me) {+            t.killed.store(true, Ordering::Release);+            t.wake();+        }+    }+    signal::wake_stopped();+    p.thread_exit.wait_until(|| (p.tasks.lock().len() == 1).then_some(()));+    // The survivor takes over the main thread id, like Linux.+    me.tid.store(p.pid, Ordering::Relaxed);+}+ /// execve, after the arguments have been copied in: replace the current /// program. `frame` gets the new program's registers.-pub fn execve(p: &Process, frame: &mut TrapFrame, path: &str, argv: &[Vec<u8>], envp: &[Vec<u8>]) -> Result<(), Errno> {+pub fn execve(p: &Arc<Process>, frame: &mut TrapFrame, path: &str, argv: &[Vec<u8>], envp: &[Vec<u8>]) -> Result<(), Errno> {     let size: usize = argv.iter().chain(envp).map(|s| s.len() + 1 + 8).sum();     if size > MAX_ARGS_SIZE {         return Err(Errno::E2BIG);@@ -750,6 +966,7 @@ pub fn execve(p: &Process, frame: &mut TrapFrame, path: &str, argv: &[Vec<u8>],      let args: Vec<&[u8]> = argv.iter().map(|a| a.as_slice()).collect();     let env: Vec<&[u8]> = envp.iter().map(|a| a.as_slice()).collect();     // --- point of no return ---+    kill_other_threads(p);     let loaded = {         let mut mm = p.mm.lock();         mm.clear();@@ -760,6 +977,11 @@ pub fn execve(p: &Process, frame: &mut TrapFrame, path: &str, argv: &[Vec<u8>],      };     *frame = start;     *p.name.lock() = String::from(name);+    *p.exe.lock() = String::from(path);+    if let Some(me) = thread::current_task() {+        me.fs_base.store(0, Ordering::Relaxed); // no TLS until the new libc sets one+        me.clear_child_tid.store(0, Ordering::Relaxed);+    }     *p.env.lock() = envp.iter().map(|e| String::from_utf8_lossy(e).into_owned()).collect();     without_interrupts(|| p.signals.lock().exec());     let closed = p.fds.lock().exec();@@ -789,6 +1011,20 @@ pub fn setpgid(me: &Process, pid: u64, pgid: u64) -> Result<(), Errno> {     Ok(()) } +/// setsid(): start a new session (and process group) led by `p`, with no+/// controlling terminal yet (the next terminal it opens becomes one, see+/// tty.rs). A process-group leader can't: its group would straddle two+/// sessions -- which is why getty forks first.+pub fn setsid(p: &Process) -> Result<u64, Errno> {+    if without_interrupts(|| PROCESSES.lock().values().any(|q| q.pgid() == p.pid)) {+        return Err(Errno::EPERM);+    }+    p.sid.store(p.pid, Ordering::Relaxed);+    p.pgid.store(p.pid, Ordering::Relaxed);+    *p.ctty.lock() = None;+    Ok(p.pid)+}+ /// Send `sig` from the kernel to group `pgid` (Ctrl+C, Ctrl+Z). pub fn signal_group(pgid: u64, sig: u32) {     let _ = signal::send_group(pgid, sig, SigInfo::from_pid(sig, 0));
@@ -36,11 +36,17 @@ //! `struct rt_sigframe`, so real libcs (musl) will work unchanged. //! //! **Stop and continue** (job control): SIGSTOP / SIGTSTP (Ctrl+Z) stop a-//! process -- its thread just sleeps -- until SIGCONT. The parent learns of+//! process -- its threads just sleep -- until SIGCONT. The parent learns of //! it from `wait4(WUNTRACED)` and SIGCHLD.+//!+//! **Threads.** Handlers (and so what a signal *does*) belong to the+//! process, but each thread has its own *mask* and its own pending set.+//! `kill` and Ctrl+C send to the process: whichever thread doesn't block+//! it takes it. A fault (SIGSEGV) or `tgkill` targets one thread. A fatal+//! signal kills all threads; a stop stops them all.  use crate::errno::Errno;-use crate::process::{self, ExitStatus, Process};+use crate::process::{self, ExitStatus, Process, Task}; use crate::thread::{self, WaitQueue}; use crate::trap::TrapFrame; use alloc::sync::Arc;@@ -199,16 +205,70 @@ pub enum Report {     Continued, } +/// Pending signals with their siginfo (standard signals don't queue: one+/// pending instance each, like Linux).+#[derive(Default)]+pub struct Pending {+    pub set: u64,+    info: Vec<(u32, SigInfo)>,+}++impl Pending {+    /// Mark `sig` pending with this siginfo.+    fn raise(&mut self, sig: u32, info: SigInfo) {+        self.set |= bit(sig);+        self.info.retain(|(s, _)| *s != sig);+        self.info.push((sig, info));+    }++    /// Take the lowest signal of `allowed` (SIGKILL first).+    fn take(&mut self, allowed: u64) -> Option<(u32, SigInfo)> {+        let set = self.set & allowed;+        if set == 0 {+            return None;+        }+        let sig = if set & bit(SIGKILL) != 0 { SIGKILL } else { set.trailing_zeros() + 1 };+        self.set &= !bit(sig);+        let info = match self.info.iter().position(|(s, _)| *s == sig) {+            Some(i) => self.info.swap_remove(i).1,+            None => SigInfo::new(sig, SI_KERNEL),+        };+        Some((sig, info))+    }++    fn discard(&mut self, mask: u64) {+        self.set &= !mask;+    }+}++/// One thread's signal state (`Task::sig`, locked with interrupts off).+pub struct ThreadSig {+    pub blocked: u64,+    /// Signals sent to this thread only (faults, tgkill).+    pub pending: Pending,+    /// Why the kernel forced a signal (a fault), for the kill message.+    reason: Option<&'static str>,+    /// The real mask while ppoll/pselect6/epoll_pwait run with a temporary+    /// one (see `set_temporary_mask`).+    saved_mask: Option<u64>,+}++impl ThreadSig {+    pub fn new(blocked: u64) -> ThreadSig {+        ThreadSig { blocked, pending: Pending::default(), reason: None, saved_mask: None }+    }++    /// The signals this thread may take now.+    fn allowed(&self) -> u64 {+        !(self.blocked & !UNBLOCKABLE)+    }+}+ /// A process's signal state. Locked with interrupts off (`Process::signals`). pub struct SignalState {-    pub pending: u64,-    pub blocked: u64,+    /// Signals sent to the whole process (kill, Ctrl+C, SIGCHLD).+    pub pending: Pending,     actions: [SigAction; NSIG as usize],-    /// siginfo of each pending signal (standard signals don't queue: one-    /// pending instance each, like Linux).-    info: Vec<(u32, SigInfo)>,-    /// Why the kernel forced this signal (a fault), for the kill message.-    reason: Option<&'static str>,     pub stopped: bool,     /// A stop/continue the parent hasn't collected with wait4 yet.     pub report: Option<Report>,@@ -216,21 +276,13 @@ pub struct SignalState {  impl SignalState {     pub fn new() -> SignalState {-        SignalState {-            pending: 0,-            blocked: 0,-            actions: [SigAction::default(); NSIG as usize],-            info: Vec::new(),-            reason: None,-            stopped: false,-            report: None,-        }+        SignalState { pending: Pending::default(), actions: [SigAction::default(); NSIG as usize], stopped: false, report: None }     } -    /// fork: the child inherits the handlers and the blocked mask, but no-    /// pending signals.+    /// fork: the child inherits the handlers (and, see process.rs, the+    /// calling thread's mask), but no pending signals.     pub fn fork(&self) -> SignalState {-        SignalState { actions: self.actions, blocked: self.blocked, ..SignalState::new() }+        SignalState { actions: self.actions, ..SignalState::new() }     }      /// execve: the program with the handlers is gone, so caught signals go@@ -255,37 +307,19 @@ impl SignalState {             || (handler == SIG_DFL && matches!(default_action(sig), DefaultAction::Ignore | DefaultAction::Continue))     } -    /// Pending and not blocked: should interrupt a wait.-    fn deliverable(&self) -> u64 {-        self.pending & !(self.blocked & !UNBLOCKABLE)-    }+} -    /// Take the lowest deliverable signal.-    fn dequeue(&mut self) -> Option<(u32, SigInfo)> {-        let set = self.deliverable();-        if set == 0 {-            return None;-        }-        // SIGKILL first, otherwise the lowest number.-        let sig = if set & bit(SIGKILL) != 0 { SIGKILL } else { set.trailing_zeros() + 1 };-        self.pending &= !bit(sig);-        let info = match self.info.iter().position(|(s, _)| *s == sig) {-            Some(i) => self.info.swap_remove(i).1,-            None => SigInfo::new(sig, SI_KERNEL),-        };-        Some((sig, info))-    }+/// Threads of a stopped process wait here for SIGCONT / SIGKILL.+static STOPPED: WaitQueue = WaitQueue::new(); -    /// Mark `sig` pending with this siginfo.-    fn raise(&mut self, sig: u32, info: SigInfo) {-        self.pending |= bit(sig);-        self.info.retain(|(s, _)| *s != sig);-        self.info.push((sig, info));-    }+/// Make stopped threads look at their state again (process.rs: a thread+/// group is exiting).+pub fn wake_stopped() {+    STOPPED.wake_all(); } -/// Thread of a stopped process waits here for SIGCONT / SIGKILL.-static STOPPED: WaitQueue = WaitQueue::new();+/// Signals that make a stopped process continue / that stop it.+const STOP_SIGNALS: u64 = bit(SIGSTOP) | bit(SIGTSTP) | bit(SIGTTIN) | bit(SIGTTOU);  /// Send signal `sig` to `p`. Returns at once; `p` acts on it on its way /// back to user mode.@@ -293,13 +327,17 @@ pub fn send(p: &Arc<Process>, sig: u32, info: SigInfo) {     if p.status().is_some() || !(1..=NSIG).contains(&sig) {         return; // already exited (a zombie): nothing to do     }+    let tasks = p.tasks();     let continued = without_interrupts(|| {         let mut s = p.signals.lock();         let mut continued = false;         if sig == SIGCONT || sig == SIGKILL {             // Continue (even if SIGCONT itself is ignored or blocked), and             // forget pending stops.-            s.pending &= !(bit(SIGSTOP) | bit(SIGTSTP) | bit(SIGTTIN) | bit(SIGTTOU));+            s.pending.discard(STOP_SIGNALS);+            for t in &tasks {+                t.sig.lock().pending.discard(STOP_SIGNALS);+            }             if s.stopped {                 s.stopped = false;                 s.report = Some(Report::Continued);@@ -307,12 +345,13 @@ pub fn send(p: &Arc<Process>, sig: u32, info: SigInfo) {             }         }         if default_action(sig) == DefaultAction::Stop {-            s.pending &= !bit(SIGCONT); // a stop cancels a pending continue+            s.pending.discard(bit(SIGCONT)); // a stop cancels a pending continue         }         // Ignored signals are dropped right away -- unless blocked: the         // program may unblock it after installing a handler.-        if sig == SIGKILL || !s.ignored(sig) || s.blocked & bit(sig) != 0 {-            s.raise(sig, info);+        let blocked = tasks.iter().any(|t| t.sig.lock().blocked & bit(sig) != 0);+        if sig == SIGKILL || !s.ignored(sig) || blocked {+            s.pending.raise(sig, info);         }         continued     });@@ -327,6 +366,28 @@ pub fn send(p: &Arc<Process>, sig: u32, info: SigInfo) {     STOPPED.wake_all(); // a stopped one needs to see SIGKILL } +/// Send `sig` to one thread (tgkill). Its effect (handler, death, stop)+/// is still the process's; only who takes it differs.+pub fn send_thread(t: &Task, sig: u32, info: SigInfo) {+    if t.process.status().is_some() || !(1..=NSIG).contains(&sig) {+        return;+    }+    if sig == SIGKILL || sig == SIGCONT || default_action(sig) == DefaultAction::Stop {+        // These act on the whole process anyway.+        if let Some(p) = process::get(t.process.pid) {+            return send(&p, sig, info);+        }+    }+    without_interrupts(|| {+        let s = t.process.signals.lock();+        let mut ts = t.sig.lock();+        if !s.ignored(sig) || ts.blocked & bit(sig) != 0 {+            ts.pending.raise(sig, info);+        }+    });+    t.wake();+}+ /// Send to every process in process group `pgid`. ESRCH if it's empty. pub fn send_group(pgid: u64, sig: u32, info: SigInfo) -> Result<(), Errno> {     let members = process::group_members(pgid);@@ -344,25 +405,32 @@ pub fn send_group(pgid: u64, sig: u32, info: SigInfo) -> Result<(), Errno> { /// the faulting instruction -- so if it is, the default action (death) /// applies, like Linux's force_sig. pub fn force(sig: u32, info: SigInfo, reason: &'static str) {-    let Some(p) = thread::current_process() else { return };+    let Some(t) = thread::current_task() else { return };     without_interrupts(|| {-        let mut s = p.signals.lock();-        if s.action(sig).handler == SIG_IGN || s.blocked & bit(sig) != 0 {+        let mut s = t.process.signals.lock();+        let mut ts = t.sig.lock();+        if s.action(sig).handler == SIG_IGN || ts.blocked & bit(sig) != 0 {             s.actions[sig as usize - 1] = SigAction::default();-            s.blocked &= !bit(sig);+            ts.blocked &= !bit(sig);         }-        s.raise(sig, info);-        s.reason = Some(reason);+        ts.pending.raise(sig, info);+        ts.reason = Some(reason);     }); } -/// Does the current process have a signal that should interrupt a-/// blocking syscall? (Called from thread.rs with interrupts off.)+/// Should the current thread stop sleeping in a syscall: a signal it can+/// take, its process stopping, or being told to exit? (Called from+/// thread.rs with interrupts off.) pub fn interrupted() -> bool {-    match thread::current_process() {-        Some(p) => without_interrupts(|| p.signals.lock().deliverable() != 0),-        None => false, // kernel threads don't get signals+    let Some(t) = thread::current_task() else { return false }; // kernel threads don't get signals+    if t.killed.load(core::sync::atomic::Ordering::Relaxed) || t.process.exiting().is_some() {+        return true;     }+    without_interrupts(|| {+        let s = t.process.signals.lock();+        let ts = t.sig.lock();+        s.stopped || (s.pending.set | ts.pending.set) & ts.allowed() != 0+    }) }  /// Tell `child`'s parent that it stopped/continued/exited: wake its wait4@@ -399,14 +467,27 @@ pub fn deliver(frame: &mut TrapFrame) {     if !frame.from_user() {         return;     }-    let Some(p) = thread::current_process() else { return };+    let Some(t) = thread::current_task() else { return };+    let p = t.process.clone();     // Writing the signal frame may fault user pages in: interrupts on.     x86_64::instructions::interrupts::enable();     loop {+        // Another thread called exit_group / execve, or died of a signal.+        if t.killed.load(core::sync::atomic::Ordering::Acquire) || p.exiting().is_some() {+            drop((t, p));+            process::exit_thread(ExitStatus::Exited(0));+        }+        // Another thread took a stop signal: we stop too.+        if without_interrupts(|| p.signals.lock().stopped) {+            wait_while_stopped(&t);+            continue;+        }         let next = without_interrupts(|| {             let mut s = p.signals.lock();-            let (sig, info) = s.dequeue()?;-            Some((sig, info, s.action(sig), s.reason.take()))+            let mut ts = t.sig.lock();+            let allowed = ts.allowed();+            let (sig, info) = ts.pending.take(allowed).or_else(|| s.pending.take(allowed))?;+            Some((sig, info, s.action(sig), ts.reason.take()))         });         let Some((sig, info, action, reason)) = next else { break };         match action.handler {@@ -414,11 +495,11 @@ pub fn deliver(frame: &mut TrapFrame) {             SIG_IGN | SIG_DFL => match default_action(sig) {                 DefaultAction::Ignore | DefaultAction::Continue => continue,                 DefaultAction::Stop if sig == SIGKILL => unreachable!(),-                DefaultAction::Stop => stop(&p, sig),+                DefaultAction::Stop => stop(&t, sig),                 DefaultAction::Terminate | DefaultAction::Core => {                     let why = reason.unwrap_or(name(sig));-                    drop(p);-                    process::exit_current(ExitStatus::Killed(sig, why));+                    drop((t, p));+                    process::exit_group(ExitStatus::Killed(sig, why));                 }             },             _ => {@@ -432,17 +513,19 @@ pub fn deliver(frame: &mut TrapFrame) {                     }                     frame.vector = 0; // dealt with: not a syscall return any more                 }-                if setup_frame(&p, frame, sig, &info, &action).is_err() {+                if setup_frame(&t, frame, sig, &info, &action).is_err() {                     // Couldn't write the frame (stack overflowed or bad                     // restorer): like Linux, the process dies of SIGSEGV.-                    drop(p);-                    process::exit_current(ExitStatus::Killed(SIGSEGV, "bad signal frame"));+                    drop((t, p));+                    process::exit_group(ExitStatus::Killed(SIGSEGV, "bad signal frame"));                 }                 without_interrupts(|| {                     let mut s = p.signals.lock();-                    s.blocked |= action.mask & !UNBLOCKABLE;+                    let mut ts = t.sig.lock();+                    ts.saved_mask = None; // (it's in the frame: sigreturn restores it)+                    ts.blocked |= action.mask & !UNBLOCKABLE;                     if action.flags & SA_NODEFER == 0 {-                        s.blocked |= bit(sig); // no nesting of the same signal+                        ts.blocked |= bit(sig); // no nesting of the same signal                     }                     if action.flags & SA_RESETHAND != 0 {                         s.actions[sig as usize - 1] = SigAction::default();@@ -452,24 +535,64 @@ pub fn deliver(frame: &mut TrapFrame) {             }         }     }-    // No handler ran: an interrupted syscall simply runs again.+    // No handler ran: the temporary mask of a ppoll & co. can go, and an+    // interrupted syscall simply runs again.+    restore_temporary_mask();     if frame.is_syscall() && restart_code(frame.rax).is_some() {         restart(frame);     } } -/// Stop the current process until SIGCONT (or SIGKILL).-fn stop(p: &Arc<Process>, sig: u32) {+/// ppoll / pselect6 / epoll_pwait / (sigsuspend-like) waits: block exactly+/// `mask` until `restore_temporary_mask`. If a signal interrupts the wait,+/// the syscall leaves the temporary mask in place and `deliver` restores+/// the real one -- after the handler, through the signal frame -- so the+/// signal that ended the wait can't be blocked again before it's handled.+pub fn set_temporary_mask(mask: u64) {+    if let Some(t) = thread::current_task() {+        without_interrupts(|| {+            let mut ts = t.sig.lock();+            let old = ts.blocked;+            ts.saved_mask.get_or_insert(old);+            ts.blocked = mask & !UNBLOCKABLE;+        });+    }+}++/// Put the real mask back (no-op without a temporary one).+pub fn restore_temporary_mask() {+    if let Some(t) = thread::current_task() {+        without_interrupts(|| {+            let mut ts = t.sig.lock();+            if let Some(mask) = ts.saved_mask.take() {+                ts.blocked = mask;+            }+        });+    }+}++/// Stop the current process (all its threads) until SIGCONT (or SIGKILL).+fn stop(t: &Arc<Task>, sig: u32) {+    let p = &t.process;     without_interrupts(|| {         let mut s = p.signals.lock();         s.stopped = true;         s.report = Some(Report::Stopped(sig));     });     crate::serial_println!("[signal] pid {} stopped by {}", p.pid, name(sig));+    p.wake(); // the other threads stop in deliver     notify_parent(p, CLD_STOPPED, sig as i32);+    wait_while_stopped(t);+}++/// Sleep until the process is continued, killed, or told to exit.+fn wait_while_stopped(t: &Task) {+    let p = &t.process;     STOPPED.wait_until(|| {         let s = p.signals.lock();-        (!s.stopped || s.pending & bit(SIGKILL) != 0).then_some(())+        let killed = (s.pending.set | t.sig.lock().pending.set) & bit(SIGKILL) != 0;+        let exiting = t.killed.load(core::sync::atomic::Ordering::Relaxed) || p.exiting().is_some();+        (!s.stopped || killed || exiting).then_some(())     }); } @@ -537,7 +660,7 @@ const USER_RFLAGS: u64 = 0x0004_0DD5 | (1 << 16); /// Note: we don't save the FPU/SSE registers (`fpstate` = 0). Our kernel /// doesn't use them and our programs' handlers don't touch floats, so they /// survive anyway; a full libc needs XSAVE state here (roadmap).-fn setup_frame(p: &Process, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action: &SigAction) -> Result<(), ()> {+fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action: &SigAction) -> Result<(), ()> {     if action.flags & SA_RESTORER == 0 || action.restorer == 0 {         return Err(()); // nowhere to return to (x86_64 requires a restorer)     }@@ -546,7 +669,12 @@ fn setup_frame(p: &Process, frame: &mut TrapFrame, sig: u32, info: &SigInfo, act     // moving rsp), aligned so the handler starts with rsp % 16 == 8, as     // if it had been called.     let sp = ((frame.rsp.wrapping_sub(128).wrapping_sub(size)) & !0xf).wrapping_sub(8);-    let blocked = without_interrupts(|| p.signals.lock().blocked);+    // The mask sigreturn restores: the real one, if a ppoll & co. had+    // swapped in a temporary mask.+    let blocked = without_interrupts(|| {+        let ts = t.sig.lock();+        ts.saved_mask.unwrap_or(ts.blocked)+    });     let s = crate::gdt::selectors();     let mcontext = SigContext {         r8: frame.r8,@@ -602,13 +730,13 @@ pub fn sys_rt_sigreturn(frame: &mut TrapFrame) {     let sp = frame.rsp.wrapping_sub(8);     let size = core::mem::size_of::<RtSigFrame>() as u64;     if !crate::mm::user_access_ok(sp, size, false) {-        process::exit_current(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));+        process::exit_group(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));     }     let rt: RtSigFrame = unsafe { core::ptr::read_unaligned(sp as *const RtSigFrame) };     let m = rt.uc.mcontext;     let user = crate::memory::USER_START..crate::memory::USER_END;     if !user.contains(&m.rip) || !(user.start..=user.end).contains(&m.rsp) {-        process::exit_current(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));+        process::exit_group(ExitStatus::Killed(SIGSEGV, "bad sigreturn frame"));     }     let s = crate::gdt::selectors();     *frame = TrapFrame {@@ -635,8 +763,8 @@ pub fn sys_rt_sigreturn(frame: &mut TrapFrame) {         vector: 0, // not a syscall return: rax is the restored value         error: 0,     };-    if let Some(p) = thread::current_process() {-        without_interrupts(|| p.signals.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE);+    if let Some(t) = thread::current_task() {+        without_interrupts(|| t.sig.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE);     } } @@ -662,20 +790,21 @@ pub fn sys_rt_sigaction(p: &Process, sig: u64, new: Option<SigAction>, set_size:             s.actions[sig as usize - 1] = new;             // Setting a signal to "ignored" discards it if pending (POSIX).             if s.ignored(sig) {-                s.pending &= !bit(sig);+                s.pending.discard(bit(sig));             }         }         old     })) } -/// rt_sigprocmask(how, *set, *old, sigsetsize = 8). Returns the old mask.-pub fn sys_rt_sigprocmask(p: &Process, how: u64, set: Option<u64>, set_size: u64) -> Result<u64, Errno> {+/// rt_sigprocmask(how, *set, *old, sigsetsize = 8): the calling thread's+/// mask. Returns the old mask.+pub fn sys_rt_sigprocmask(t: &Task, how: u64, set: Option<u64>, set_size: u64) -> Result<u64, Errno> {     if set_size != 8 {         return Err(Errno::EINVAL);     }     without_interrupts(|| {-        let mut s = p.signals.lock();+        let mut s = t.sig.lock();         let old = s.blocked;         if let Some(set) = set {             s.blocked = match how {
@@ -60,6 +60,57 @@ pub fn cpu_id() -> usize {     APIC_TO_CPU[apic::id() as usize & 0xFF].load(Ordering::Relaxed) as usize } +/// The address space (CR3 value) each CPU is running right now.+static ACTIVE_CR3: [AtomicU64; MAX_CPUS] = [const { AtomicU64::new(0) }; MAX_CPUS];+/// Set by a CPU asking CPU n to flush its TLB; CPU n clears it when done.+static FLUSH_PENDING: [AtomicBool; MAX_CPUS] = [const { AtomicBool::new(false) }; MAX_CPUS];++/// The scheduler tells us which address space `cpu` switched to.+pub fn set_active_cr3(cpu: usize, cr3: u64) {+    ACTIVE_CR3[cpu].store(cr3, Ordering::SeqCst);+}++/// **TLB shootdown.** Each CPU caches page-table entries in its TLB. When+/// a page is unmapped, made read-only or replaced (copy-on-write) in an+/// address space that threads are using on *other* CPUs right now, their+/// TLBs may still hold the old entry -- a thread could keep writing to a+/// page that was just freed. So: interrupt every other CPU running the+/// address space `cr3` and wait until each has flushed its TLB. (One+/// thread per process never needs this: no other CPU runs its address+/// space at the same time.) Call with interrupts *on*, after changing the+/// page tables and before freeing any frame they pointed to.+pub fn tlb_shootdown(cr3: u64) {+    let me = without_interrupts(cpu_id);+    let mut targets = 0u32;+    for cpu in 0..cpu_count() {+        if cpu != me && ACTIVE_CR3[cpu].load(Ordering::SeqCst) == cr3 {+            FLUSH_PENDING[cpu].store(true, Ordering::SeqCst);+            let target = CPU_TO_APIC[cpu].load(Ordering::Relaxed);+            without_interrupts(|| apic::send_ipi(target, interrupts::InterruptIndex::TlbShootdown as u8));+            targets |= 1 << cpu;+        }+    }+    // Wait until they're done. Another CPU may be shooting at *us* at the+    // same time while we have interrupts off: answer its request by hand,+    // or we'd wait for each other forever.+    while (0..MAX_CPUS).any(|cpu| targets & (1 << cpu) != 0 && FLUSH_PENDING[cpu].load(Ordering::SeqCst)) {+        without_interrupts(|| {+            if FLUSH_PENDING[cpu_id()].load(Ordering::SeqCst) {+                handle_tlb_shootdown();+            }+        });+        core::hint::spin_loop();+    }+}++/// The shootdown IPI arrived (interrupts.rs): forget all user translations.+pub fn handle_tlb_shootdown() {+    use x86_64::registers::control::Cr3;+    let (frame, flags) = Cr3::read();+    unsafe { Cr3::write(frame, flags) };+    FLUSH_PENDING[cpu_id()].store(false, Ordering::SeqCst);+}+ /// How many CPUs are running. pub fn cpu_count() -> usize {     CPU_COUNT.load(Ordering::Acquire)
@@ -9,17 +9,20 @@ //! man pages mostly apply. The process calls added in stage 14 (fork, //! execve, wait4, mmap, signals, dup, pipe2, fcntl, process groups) follow //! Linux exactly -- C strings, structs, flags -- so a C library can use-//! them as they are. The older file calls still take paths as pointer +-//! length (Linux: NUL-terminated); relative paths start at the cwd.+//! them as they are, and so do stage 15's (poll/epoll, futex, threads,+//! ttys, nanosleep, readv/writev...). The older file calls still take+//! paths as pointer + length (Linux: NUL-terminated); relative paths start+//! at the cwd. Every blocking call honours O_NONBLOCK (-> EAGAIN). //! //! | nr  | name        | arguments                           | note | //! |-----|-------------|-------------------------------------|------|-//! |   0 | read        | fd, buf, len                        | blocks: console until Enter, pipe until data/EOF |-//! |   1 | write       | fd, buf, len                        | to a pipe without readers: EPIPE + SIGPIPE |-//! |   2 | open        | path, path_len, flags               | O_CREAT 0o100, O_TRUNC 0o1000, O_APPEND, O_CLOEXEC 0o2000000 |+//! |   0 | read        | fd, buf, len                        | blocks: tty until a line (canonical) or a byte (raw), pipe until data/EOF |+//! |   1 | write       | fd, buf, len                        | to a pipe without readers: EPIPE + SIGPIPE; a full pipe blocks |+//! |   2 | open        | path, path_len, flags               | O_CREAT 0o100, O_NOCTTY 0o400, O_TRUNC 0o1000, O_APPEND, O_NONBLOCK 0o4000, O_CLOEXEC 0o2000000 | //! |   3 | close       | fd                                  | | //! |   4 | stat        | path, path_len, *Stat               | our own small `Stat {kind, size, mode}`; follows symlinks | //! |   5 | fstat       | fd, *Stat                           | |+//! |   7 | poll        | *pollfd, n, timeout_ms              | -1 = forever; pipes, sockets, ttys, files, epoll fds | //! |   8 | lseek       | fd, offset, whence                  | SEEK_SET/CUR/END = 0/1/2 | //! |   9 | mmap        | addr, len, prot, flags, fd, offset  | MAP_PRIVATE/SHARED, MAP_ANONYMOUS, MAP_FIXED; files: private only | //! |  10 | mprotect    | addr, len, prot                     | PROT_READ 1, PROT_WRITE 2, PROT_EXEC 4 |@@ -28,44 +31,65 @@ //! |  13 | rt_sigaction | sig, *new, *old, 8                 | kernel `struct sigaction`; handlers need SA_RESTORER | //! |  14 | rt_sigprocmask | how, *set, *old, 8               | SIG_BLOCK 0 / SIG_UNBLOCK 1 / SIG_SETMASK 2 | //! |  15 | rt_sigreturn |                                    | only from a signal handler's restorer |-//! |  16 | ioctl       | fd, request, arg                    | console: TTY_SET_RAW (ours), TIOCGPGRP, TIOCSPGRP, TIOCGWINSZ; /dev/fb0: FB_* (below) |+//! |  16 | ioctl       | fd, request, arg                    | any fd: FIONBIO; ttys: TCGETS/TCSETS(W/F), TIOCGPGRP/SPGRP, TIOCSCTTY, TIOCNOTTY, TIOCGWINSZ/SWINSZ, TIOCGPTN, TIOCSPTLCK, TTY_SET_RAW 1 (ours); /dev/fb0: FB_* (below) |+//! |  17 | pread64     | fd, buf, len, offset                | regular files; leaves the file position alone |+//! |  18 | pwrite64    | fd, buf, len, offset                | |+//! |  19 | readv       | fd, *iovec, n                       | `struct iovec {base, len}`, up to 1024 |+//! |  20 | writev      | fd, *iovec, n                       | | //! |  22 | pipe        | *[u32; 2]                           | [read end, write end] |+//! |  23 | select      | n, *rd, *wr, *ex, *timeval          | fd bitmaps (n <= 1024) | //! |  24 | sched_yield |                                     | | //! |  32 | dup         | fd                                  | lowest free fd | //! |  33 | dup2        | old, new                            | closes `new` first if open | //! |  34 | pause       |                                     | sleeps until a signal; returns EINTR |-//! |  35 | sleep_ms    | milliseconds                        | Linux's nr 35 is nanosleep(timespec); ours is simpler. EINTR on a signal |+//! |  35 | nanosleep   | *timespec, *remaining               | EINTR on a signal (remaining time written back) | //! |  39 | getpid      |                                     | |-//! |  41 | socket      | domain, type, protocol              | AF_INET (2) only; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket |-//! |  42 | connect     | fd, *sockaddr_in, len               | TCP: handshake, blocks (10 s timeout); UDP/ICMP: set default destination |+//! |  41 | socket      | domain, type, protocol              | AF_INET (2) only; type may add SOCK_NONBLOCK/SOCK_CLOEXEC; SOCK_STREAM (1) = TCP, SOCK_DGRAM (2) = UDP, SOCK_DGRAM + IPPROTO_ICMP (1) = ping socket |+//! |  42 | connect     | fd, *sockaddr_in, len               | TCP: handshake, blocks (10 s timeout; nonblocking: EINPROGRESS); UDP/ICMP: set default destination. 127.x = loopback | //! |  43 | accept      | fd, *sockaddr_in or 0, *u32 len or 0 | blocks for a connection (SO_RCVTIMEO applies); returns the new fd | //! |  44 | sendto      | fd, buf, len, flags, *sockaddr_in or 0 | Linux has a 6th arg (addrlen); we only take 5, the address is always 16 bytes | //! |  45 | recvfrom    | fd, buf, len, flags, *sockaddr_in or 0 | blocks (SO_RCVTIMEO -> EAGAIN); 0 = TCP peer closed. No addrlen pointer | //! |  49 | bind        | fd, *sockaddr_in, len               | sets the local port (address ignored: we have one) | //! |  50 | listen      | fd, backlog                         | TCP; backlog 1..4 | //! |  54 | setsockopt  | fd, level, name, *value, len        | only SOL_SOCKET (1) / SO_RCVTIMEO (20) with a `struct timeval` |-//! |  56 | clone       | flags, stack, ...                   | only fork-like flags: SIGCHLD + optional CLONE_VFORK (else ENOSYS) |+//! |  56 | clone       | flags, stack, *ptid, *ctid, tls     | fork-like (SIGCHLD [+ CLONE_VFORK]) or a thread: CLONE_VM|FS|FILES|SIGHAND|THREAD|SYSVSEM [+ SETTLS, PARENT_SETTID, CHILD_CLEARTID] | //! |  57 | fork        |                                     | copy-on-write; parent gets the pid, child 0 | //! |  58 | vfork       |                                     | fork + the parent waits until the child execs or exits | //! |  59 | execve      | path, argv, envp (C strings)        | Linux's stack layout: argc, argv, envp, auxv |-//! |  60 | exit        | code                                | |+//! |  60 | exit        | code                                | ends this *thread* only (exit_group ends the process) | //! |  61 | wait4       | pid, *status, options, *rusage      | WNOHANG 1, WUNTRACED 2, WCONTINUED 8; rusage must be 0 | //! |  62 | kill        | pid, sig                            | pid 0 = my group, -1 = all, < -1 = group -pid | //! |  63 | uname       | *utsname                            | Linux's `struct utsname` (6 x 65-byte strings) |-//! |  72 | fcntl       | fd, cmd, arg                        | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3, F_SETFL 4, F_DUPFD_CLOEXEC 1030 |-//! |  79 | getcwd      | buf, len                            | returns the length (no NUL) |+//! |  72 | fcntl       | fd, cmd, arg                        | F_DUPFD 0, F_GETFD 1, F_SETFD 2, F_GETFL 3, F_SETFL 4 (O_NONBLOCK), F_DUPFD_CLOEXEC 1030 |+//! |  79 | getcwd      | buf, len                            | like Linux: writes a NUL, returns the length including it | //! |  80 | chdir       | path, path_len                      | | //! |  83 | mkdir       | path, path_len                      | | //! |  84 | rmdir       | path, path_len                      | only empty directories | //! |  87 | unlink      | path, path_len                      | files and symlinks (not what a symlink points to) |-//! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL) |+//! |  89 | readlink    | path, path_len, buf, len            | a symlink's target; returns its length (cut to `len`, no NUL); /proc/self/exe = my program | //! | 109 | setpgid     | pid, pgid                           | 0 = myself / a new group | //! | 110 | getppid     |                                     | 0 if started by the kernel or orphaned | //! | 111 | getpgrp     |                                     | |+//! | 112 | setsid      |                                     | new session + group, no controlling tty | //! | 121 | getpgid     | pid                                 | |+//! | 124 | getsid      | pid                                 | |+//! | 158 | arch_prctl  | code, addr                          | ARCH_SET_FS 0x1002 / ARCH_GET_FS 0x1003: the TLS pointer |+//! | 186 | gettid      |                                     | thread id (= pid for the first thread) |+//! | 200 | tkill       | tid, sig                            | |+//! | 202 | futex       | *u32, op, val, *timespec, *u32, val3 | FUTEX_WAIT 0, WAKE 1, WAIT_BITSET 9, WAKE_BITSET 10 (+ PRIVATE 128) |+//! | 213 | epoll_create | size                               | = epoll_create1(0) | //! | 217 | getdents64  | fd, buf, len                        | Linux `struct linux_dirent64` records |-//! | 228 | clock_gettime | clock, *timespec                  | CLOCK_REALTIME/MONOTONIC both = time since boot (we have no RTC driver) |-//! | 231 | exit_group  | code                                | = exit (one thread per process) |+//! | 218 | set_tid_address | *u32                            | cleared + futex-woken when this thread exits; returns the tid |+//! | 228 | clock_gettime | clock, *timespec                  | REALTIME 0 (CMOS clock at boot + uptime), MONOTONIC 1 (since boot) |+//! | 231 | exit_group  | code                                | ends every thread of the process |+//! | 232 | epoll_wait  | epfd, *epoll_event, max, timeout_ms | level-triggered; EPOLLONESHOT supported |+//! | 233 | epoll_ctl   | epfd, op, fd, *epoll_event          | ADD 1, DEL 2, MOD 3 |+//! | 234 | tgkill      | tgid, tid, sig                      | a signal for one thread |+//! | 270 | pselect6    | n, *rd, *wr, *ex, *timespec, *{*mask, 8} | |+//! | 271 | ppoll       | *pollfd, n, *timespec, *mask, 8     | sets the mask while waiting |+//! | 281 | epoll_pwait | epfd, *events, max, timeout, *mask, 8 | |+//! | 288 | accept4     | fd, *addr, *len, flags              | flags: SOCK_NONBLOCK, SOCK_CLOEXEC |+//! | 291 | epoll_create1 | flags                             | EPOLL_CLOEXEC | //! | 292 | dup3        | old, new, flags                     | flags: O_CLOEXEC | //! | 293 | pipe2       | *[u32; 2], flags                    | flags: O_CLOEXEC | //! | 318 | getrandom   | buf, len, flags                     | random bytes (virtio-rng); up to 64 KiB per call; flags ignored |@@ -117,7 +141,7 @@ //! OS use, and it's faster.  use crate::errno::Errno;-use crate::fs::file::{InodeFile, Stat};+use crate::fs::file::{File, InodeFile, Stat}; use crate::fs::{self, InodeKind}; use crate::trap::TrapFrame; use crate::{memory, process, signal, thread};@@ -135,7 +159,7 @@ pub const SYS_LSEEK: u64 = 8; pub const SYS_IOCTL: u64 = 16; pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24;-pub const SYS_SLEEP_MS: u64 = 35;+pub const SYS_NANOSLEEP: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60; pub const SYS_UNAME: u64 = 63;@@ -148,6 +172,7 @@ pub const SYS_READLINK: u64 = 89; pub const SYS_SOCKET: u64 = 41; pub const SYS_CONNECT: u64 = 42; pub const SYS_ACCEPT: u64 = 43;+pub const SYS_ACCEPT4: u64 = 288; pub const SYS_SENDTO: u64 = 44; pub const SYS_RECVFROM: u64 = 45; pub const SYS_BIND: u64 = 49;@@ -186,6 +211,28 @@ pub const SYS_EXIT_GROUP: u64 = 231; pub const SYS_DUP3: u64 = 292; pub const SYS_PIPE2: u64 = 293; pub const SYS_TRACE_ME: u64 = 505;+// stage 15+pub const SYS_POLL: u64 = 7;+pub const SYS_PREAD64: u64 = 17;+pub const SYS_PWRITE64: u64 = 18;+pub const SYS_READV: u64 = 19;+pub const SYS_WRITEV: u64 = 20;+pub const SYS_SELECT: u64 = 23;+pub const SYS_SETSID: u64 = 112;+pub const SYS_GETSID: u64 = 124;+pub const SYS_ARCH_PRCTL: u64 = 158;+pub const SYS_GETTID: u64 = 186;+pub const SYS_TKILL: u64 = 200;+pub const SYS_FUTEX: u64 = 202;+pub const SYS_EPOLL_CREATE: u64 = 213;+pub const SYS_SET_TID_ADDRESS: u64 = 218;+pub const SYS_EPOLL_WAIT: u64 = 232;+pub const SYS_EPOLL_CTL: u64 = 233;+pub const SYS_TGKILL: u64 = 234;+pub const SYS_PSELECT6: u64 = 270;+pub const SYS_PPOLL: u64 = 271;+pub const SYS_EPOLL_PWAIT: u64 = 281;+pub const SYS_EPOLL_CREATE1: u64 = 291;  // Socket constants (Linux's values). pub const AF_INET: u64 = 2;@@ -196,6 +243,7 @@ pub const SOL_SOCKET: u64 = 1; pub const SO_RCVTIMEO: u64 = 20;  pub const O_CREAT: u64 = 0o100;+pub const O_NOCTTY: u64 = 0o400; pub const O_TRUNC: u64 = 0o1000; pub const O_APPEND: u64 = 0o2000; pub const O_NONBLOCK: u64 = 0o4000;@@ -208,19 +256,29 @@ pub const F_GETFL: u64 = 3; pub const F_SETFL: u64 = 4; pub const F_DUPFD_CLOEXEC: u64 = 1030; pub const FD_CLOEXEC: u64 = 1;-/// Terminal ioctls (Linux's numbers).-pub const TIOCGPGRP: u64 = 0x540F;-pub const TIOCSPGRP: u64 = 0x5410;-pub const TIOCGWINSZ: u64 = 0x5413;+/// ioctl(FIONBIO, *int): O_NONBLOCK on/off, for any file.+pub const FIONBIO: u64 = 0x5421; /// mmap flags. pub const MAP_SHARED: u64 = 0x01; pub const MAP_PRIVATE: u64 = 0x02; pub const MAP_FIXED: u64 = 0x10; pub const MAP_ANONYMOUS: u64 = 0x20;-/// clone flags we understand.-pub const CLONE_VFORK: u64 = 0x4000;-/// ioctl request (ours): arg 1 = raw mode, 0 = cooked; returns the old mode.-pub const TTY_SET_RAW: u64 = 1;+/// clone flags (Linux's values).+pub const CLONE_VM: u64 = 0x100; // share the address space+pub const CLONE_FS: u64 = 0x200; // share cwd/umask+pub const CLONE_FILES: u64 = 0x400; // share the fd table+pub const CLONE_SIGHAND: u64 = 0x800; // share signal handlers+pub const CLONE_VFORK: u64 = 0x4000; // parent waits until the child execs/exits+pub const CLONE_THREAD: u64 = 0x10000; // same process: a thread+pub const CLONE_SYSVSEM: u64 = 0x40000;+pub const CLONE_SETTLS: u64 = 0x80000; // new thread's FS base = tls argument+pub const CLONE_PARENT_SETTID: u64 = 0x100000; // write the tid to *ptid+pub const CLONE_CHILD_CLEARTID: u64 = 0x200000; // at exit: *ctid = 0 + futex wake+pub const CLONE_DETACHED: u64 = 0x400000;+pub const CLONE_CHILD_SETTID: u64 = 0x1000000; // write the tid to *ctid+/// arch_prctl codes.+pub const ARCH_SET_FS: u64 = 0x1002;+pub const ARCH_GET_FS: u64 = 0x1003; pub const SPAWN_TRACE: u64 = 1; /// Framebuffer ioctls (see the table at the top). pub const FB_GET_INFO: u64 = 0x4600;@@ -241,7 +299,28 @@ pub fn name(nr: u64) -> &'static str {         SYS_IOCTL => "ioctl",         SYS_PIPE => "pipe",         SYS_SCHED_YIELD => "sched_yield",-        SYS_SLEEP_MS => "sleep_ms",+        SYS_NANOSLEEP => "nanosleep",+        SYS_POLL => "poll",+        SYS_PREAD64 => "pread64",+        SYS_PWRITE64 => "pwrite64",+        SYS_READV => "readv",+        SYS_WRITEV => "writev",+        SYS_SELECT => "select",+        SYS_SETSID => "setsid",+        SYS_GETSID => "getsid",+        SYS_ARCH_PRCTL => "arch_prctl",+        SYS_GETTID => "gettid",+        SYS_TKILL => "tkill",+        SYS_FUTEX => "futex",+        SYS_EPOLL_CREATE => "epoll_create",+        SYS_SET_TID_ADDRESS => "set_tid_address",+        SYS_EPOLL_WAIT => "epoll_wait",+        SYS_EPOLL_CTL => "epoll_ctl",+        SYS_TGKILL => "tgkill",+        SYS_PSELECT6 => "pselect6",+        SYS_PPOLL => "ppoll",+        SYS_EPOLL_PWAIT => "epoll_pwait",+        SYS_EPOLL_CREATE1 => "epoll_create1",         SYS_GETPID => "getpid",         SYS_EXIT => "exit",         SYS_WAIT4 => "wait4",@@ -284,6 +363,7 @@ pub fn name(nr: u64) -> &'static str {         SYS_SOCKET => "socket",         SYS_CONNECT => "connect",         SYS_ACCEPT => "accept",+        SYS_ACCEPT4 => "accept4",         SYS_SENDTO => "sendto",         SYS_RECVFROM => "recvfrom",         SYS_BIND => "bind",@@ -434,16 +514,16 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_DUP2 => current().and_then(|p| if a1 == a2 { p.file(a1).map(|_| a1) } else { p.dup_to(a1, a2, false) }),         SYS_DUP3 => sys_dup3(a1, a2, a3),         SYS_PAUSE => Err(thread::pause()),-        SYS_SLEEP_MS => {-            // The PIT fires ~18.2 times a second: 1 tick = ~55 ms. Round up.-            thread::sleep_ticks_intr(a1.div_ceil(55)).map(|_| 0)-        }+        SYS_NANOSLEEP => sys_nanosleep(a1, a2),         SYS_GETPID => current().map(|p| p.pid),-        SYS_CLONE => sys_clone(frame, a1, a2),+        SYS_CLONE => sys_clone(frame, a1, a2, a3, a4, a5),         SYS_FORK => current().and_then(|p| process::fork(&p, frame, 0, false)),         SYS_VFORK => current().and_then(|p| process::fork(&p, frame, 0, true)),         SYS_EXECVE => sys_execve(frame, a1, a2, a3),-        SYS_EXIT | SYS_EXIT_GROUP => process::exit_current(process::ExitStatus::Exited(a1 as i32 as i64)),+        // exit ends only the calling thread (the process ends with its+        // last thread); exit_group ends them all. libc's exit() is the latter.+        SYS_EXIT => process::exit_thread(process::ExitStatus::Exited(a1 as i32 as i64)),+        SYS_EXIT_GROUP => process::exit_group(process::ExitStatus::Exited(a1 as i32 as i64)),         SYS_WAIT4 => sys_wait4(a1 as i32 as i64, a2, a3),         SYS_KILL => current().and_then(|p| signal::sys_kill(&p, a1 as i32 as i64, a2)),         SYS_UNAME => sys_uname(a1),@@ -464,17 +544,43 @@ extern "C" fn syscall_dispatch(frame: &mut TrapFrame) {         SYS_SYSINFO => sys_sysinfo(a1),         SYS_SOCKET => sys_socket(a1, a2, a3),         SYS_CONNECT => with_socket(a1, |s| s.connect(read_sockaddr(a2)?)).map(|_| 0),-        SYS_ACCEPT => sys_accept(a1, a2, a3),+        SYS_ACCEPT => sys_accept(a1, a2, a3, 0),+        SYS_ACCEPT4 => sys_accept(a1, a2, a3, a4),         SYS_SENDTO => sys_sendto(a1, a2, a3, a5),         SYS_RECVFROM => sys_recvfrom(a1, a2, a3, a5),         SYS_BIND => with_socket(a1, |s| s.bind(read_sockaddr(a2)?.1)).map(|_| 0),         SYS_LISTEN => with_socket(a1, |s| s.listen(a2 as usize)).map(|_| 0),         SYS_SETSOCKOPT => sys_setsockopt(a1, a2, a3, a4, a5),-        SYS_CLOCK_GETTIME => sys_clock_gettime(a2),+        SYS_CLOCK_GETTIME => sys_clock_gettime(a1, a2),         SYS_RESOLVE => sys_resolve(a1, a2, a3, a4),         SYS_NETINFO => copy_to_user(a1, &crate::net::info()).map(|_| 0),         SYS_GETRANDOM => sys_getrandom(a1, a2),         SYS_TRACE_ME => current().map(|p| p.trace.store(a1 != 0, Ordering::Relaxed)).map(|_| 0),+        // --- stage 15 ---+        SYS_PREAD64 => sys_pread(a1, a2, a3, a4),+        SYS_PWRITE64 => sys_pwrite(a1, a2, a3, a4),+        SYS_READV => sys_readv(a1, a2, a3),+        SYS_WRITEV => sys_writev(a1, a2, a3),+        SYS_POLL => crate::poll::sys_poll_ms(a1, a2, a3 as i32 as i64),+        SYS_PPOLL => crate::poll::sys_ppoll(a1, a2, a3, a4),+        SYS_SELECT => crate::poll::sys_select_tv(a1, a2, a3, a4, a5),+        SYS_PSELECT6 => crate::poll::sys_pselect6(a1, a2, a3, a4, a5, frame.r9),+        SYS_EPOLL_CREATE => if (a1 as i32) <= 0 { Err(Errno::EINVAL) } else { crate::poll::sys_epoll_create1(0) },+        SYS_EPOLL_CREATE1 => crate::poll::sys_epoll_create1(a1),+        SYS_EPOLL_CTL => crate::poll::sys_epoll_ctl(a1, a2, a3, a4),+        SYS_EPOLL_WAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, 0),+        SYS_EPOLL_PWAIT => crate::poll::sys_epoll_pwait(a1, a2, a3 as i32 as i64, a4 as i32 as i64, a5),+        SYS_FUTEX => current().and_then(|p| crate::futex::sys_futex(&p, a1, a2, a3, a4, a5, frame.r9)),+        SYS_GETTID => task().map(|t| t.tid()),+        SYS_SET_TID_ADDRESS => task().map(|t| {+            t.clear_child_tid.store(a1, Ordering::Relaxed);+            t.tid()+        }),+        SYS_ARCH_PRCTL => sys_arch_prctl(a1, a2),+        SYS_TGKILL => sys_tgkill(Some(a1), a2, a3),+        SYS_TKILL => sys_tgkill(None, a1, a2),+        SYS_SETSID => current().and_then(|p| process::setsid(&p)),+        SYS_GETSID => current().and_then(|p| if a1 == 0 { Ok(p.sid()) } else { process::get(a1).map(|t| t.sid()).ok_or(Errno::ESRCH) }),         _ => Err(Errno::ENOSYS),     };     let ret = match result {@@ -535,7 +641,7 @@ fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {         SYS_CHDIR | SYS_MKDIR | SYS_RMDIR | SYS_UNLINK => text(a[0], a[1]),         SYS_SPAWN => format!("{}, {}", text(a[0], a[1]), text(a[2], a[3])),         SYS_SCHED_YIELD | SYS_GETPID => String::new(),-        SYS_CLOSE | SYS_SLEEP_MS | SYS_EXIT | SYS_EXIT_GROUP | SYS_DUP => format!("{}", a[0] as i64),+        SYS_CLOSE | SYS_EXIT | SYS_EXIT_GROUP | SYS_DUP => format!("{}", a[0] as i64),         SYS_PIPE | SYS_UNAME => format!("{:#x}", a[0]),         SYS_FSTAT | SYS_GETCWD | SYS_MOUNTS => format!("{}, {:#x}", a[0], a[1]),         SYS_WAIT4 => format!("{}, {:#x}, {}", a[0] as i32, a[1], a[2]),@@ -555,6 +661,23 @@ fn current() -> Result<Arc<process::Process>, Errno> {     thread::current_process().ok_or(Errno::EINVAL) } +/// The calling thread.+fn task() -> Result<Arc<process::Task>, Errno> {+    thread::current_task().ok_or(Errno::EINVAL)+}++/// Read a `struct timespec {i64 seconds, i64 nanoseconds}` as microseconds+/// (rounded up, so a timeout never ends early).+pub fn read_timespec_us(ptr: u64) -> Result<u64, Errno> {+    let b = user_slice(ptr, 16)?;+    let secs = i64::from_le_bytes(b[..8].try_into().unwrap());+    let nsecs = i64::from_le_bytes(b[8..].try_into().unwrap());+    if secs < 0 || !(0..1_000_000_000).contains(&nsecs) {+        return Err(Errno::EINVAL);+    }+    Ok((secs as u64).saturating_mul(1_000_000).saturating_add((nsecs as u64).div_ceil(1000)))+}+ /// Turn a user pointer + length into a slice, after checking that the user /// may really access that memory. Never trust pointers from user space! pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> {@@ -607,7 +730,7 @@ fn user_path(ptr: u64, len: u64) -> Result<String, Errno> { }  /// Copy a plain-data struct out to user memory.-fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {+pub fn copy_to_user<T: Copy>(ptr: u64, value: &T) -> Result<(), Errno> {     let size = core::mem::size_of::<T>();     let dst = user_slice_mut(ptr, size as u64)?;     let src = unsafe { core::slice::from_raw_parts(value as *const T as *const u8, size) };@@ -633,8 +756,15 @@ fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {     };     // Device files (/dev/...) come with their own File.     let cloexec = flags & O_CLOEXEC != 0;-    if let Some(device) = inode.open_device() {-        return current()?.add_file_with(device, 0, cloexec);+    if let Some(device) = inode.open_device()? {+        let p = current()?;+        if flags & O_NONBLOCK != 0 {+            device.set_nonblocking(true);+        }+        if flags & O_NOCTTY == 0 {+            crate::tty::opened(&p, &*device); // maybe our controlling terminal now+        }+        return p.add_file_with(device, 0, cloexec);     }     if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File {         inode.truncate()?;@@ -656,13 +786,17 @@ fn sys_fstat(fd: u64, stat_ptr: u64) -> Result<u64, Errno> { }  fn sys_pipe2(fds_ptr: u64, flags: u64) -> Result<u64, Errno> {-    if flags & !O_CLOEXEC != 0 {-        return Err(Errno::EINVAL); // O_NONBLOCK, O_DIRECT: not supported+    if flags & !(O_CLOEXEC | O_NONBLOCK) != 0 {+        return Err(Errno::EINVAL); // O_DIRECT (packet mode): not supported     }     let cloexec = flags & O_CLOEXEC != 0;     user_slice_mut(fds_ptr, 8)?; // check before creating anything     let process = current()?;     let (reader, writer) = fs::pipe::new();+    if flags & O_NONBLOCK != 0 {+        reader.set_nonblocking(true);+        writer.set_nonblocking(true);+    }     let read_fd = process.add_file_with(Arc::new(reader), 0, cloexec)?;     let write_fd = match process.add_file_with(Arc::new(writer), 0, cloexec) {         Ok(fd) => fd,@@ -682,8 +816,9 @@ fn sys_dup3(old: u64, new: u64, flags: u64) -> Result<u64, Errno> {     current()?.dup_to(old, new, flags & O_CLOEXEC != 0) } -/// fcntl: just the fd flags (close-on-exec) and duplication. Status flags:-/// everything is blocking, read/write (O_NONBLOCK isn't supported yet).+/// fcntl: the fd flags (close-on-exec), duplication, and the one status+/// flag we have: O_NONBLOCK (it belongs to the open file, so it's shared+/// with dup'ed fds). Everything is opened read/write. fn sys_fcntl(fd: u64, cmd: u64, arg: u64) -> Result<u64, Errno> {     let p = current()?;     let file = p.file(fd)?;@@ -693,15 +828,19 @@ fn sys_fcntl(fd: u64, cmd: u64, arg: u64) -> Result<u64, Errno> {         F_DUPFD_CLOEXEC => p.add_file_with(file, arg as usize, true),         F_GETFD => p.cloexec(fd).map(|on| if on { FD_CLOEXEC } else { 0 }),         F_SETFD => p.set_cloexec(fd, arg & FD_CLOEXEC != 0).map(|_| 0),-        F_GETFL => Ok(2), // O_RDWR-        F_SETFL if arg & O_NONBLOCK != 0 => Err(Errno::EINVAL),-        F_SETFL => Ok(0),+        F_GETFL => Ok(2 | if file.nonblocking() { O_NONBLOCK } else { 0 }), // O_RDWR+        F_SETFL => {+            file.set_nonblocking(arg & O_NONBLOCK != 0);+            Ok(0)+        }         _ => Err(Errno::EINVAL),     } } +/// getcwd: like Linux, the path with a NUL; returns the length + 1. fn sys_getcwd(buf: u64, len: u64) -> Result<u64, Errno> {-    let cwd = current()?.cwd.lock().clone();+    let mut cwd = current()?.cwd.lock().clone();+    cwd.push('\0');     if (len as usize) < cwd.len() {         return Err(Errno::ERANGE);     }@@ -719,7 +858,10 @@ fn sys_chdir(path_ptr: u64, path_len: u64) -> Result<u64, Errno> { }  fn sys_readlink(path_ptr: u64, path_len: u64, buf: u64, len: u64) -> Result<u64, Errno> {-    let target = fs::readlink(&user_path(path_ptr, path_len)?)?;+    let path = user_path(path_ptr, path_len)?;+    // There's no /proc yet, but libcs and programs ask "where am I?"+    // with readlink("/proc/self/exe"): answer just that one.+    let target = if path == "/proc/self/exe" { current()?.exe.lock().clone() } else { fs::readlink(&path)? };     let n = target.len().min(len as usize);     user_slice_mut(buf, n as u64)?.copy_from_slice(&target.as_bytes()[..n]);     Ok(n as u64)@@ -757,39 +899,26 @@ fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_pt         cwd: parent.cwd.lock().clone(),         parent: parent.pid,         pgid: parent.pgid(), // same process group, like fork+        sid: parent.sid(),   // ...and session+        ctty: parent.ctty.lock().clone(),         trace: opts.flags & SPAWN_TRACE != 0,     };     drop(parent);     process::spawn_path(&path, spawn).map(|child| child.pid) } +/// ioctl: FIONBIO for any file; the rest is up to the file (terminals:+/// tty.rs; the framebuffer: below). fn sys_ioctl(fd: u64, request: u64, arg: u64) -> Result<u64, Errno> {-    let process = current()?;-    let file = process.file(fd)?;+    let file = current()?.file(fd)?;     if let Some(fb) = file.as_framebuffer() {         return fb_ioctl(fb, request, arg);     }-    if file.stat()?.kind != crate::fs::file::KIND_CONSOLE {-        return Err(Errno::ENOTTY);-    }-    match request {-        TTY_SET_RAW => Ok(crate::tty::set_raw(arg != 0, process.pid) as u64),-        // Which process group is in the foreground (gets Ctrl+C)?-        TIOCGPGRP => copy_to_user(arg, &(crate::tty::foreground() as u32)).map(|_| 0),-        TIOCSPGRP => {-            let pgid = read_user_u32s::<1>(arg)?[0] as u64;-            if process::group_members(pgid).is_empty() {-                return Err(Errno::ESRCH);-            }-            crate::tty::set_foreground(pgid);-            Ok(0)-        }-        TIOCGWINSZ => {-            let (cols, rows) = crate::vga_buffer::size();-            copy_to_user(arg, &[rows as u16, cols as u16, 0, 0]).map(|_| 0)-        }-        _ => Err(Errno::EINVAL),+    if request == FIONBIO {+        file.set_nonblocking(read_user_u32s::<1>(arg)?[0] != 0);+        return Ok(0);     }+    file.ioctl(request, arg) }  /// The /dev/fb0 ioctls (table at the top of this file).@@ -833,7 +962,7 @@ fn sys_uname(buf: u64) -> Result<u64, Errno> {         "Rustbelt",                     // sysname (`uname -s`)         "qemu",                         // nodename (hostname)         env!("CARGO_PKG_VERSION"),      // release (`uname -r`)-        "stage 14: processes",          // version+        "stage 15: libc plumbing",      // version         "x86_64",                       // machine         "(none)",                       // domainname     ];@@ -945,12 +1074,17 @@ fn sys_socket(domain: u64, ty: u64, protocol: u64) -> Result<u64, Errno> {         (SOCK_DGRAM, IPPROTO_ICMP) => Kind::Icmp,         _ => return Err(Errno::EPROTONOSUPPORT),     };-    current()?.add_file(Arc::new(Socket::new(kind)?))+    // SOCK_NONBLOCK and SOCK_CLOEXEC have O_NONBLOCK's and O_CLOEXEC's values.+    let socket = Socket::new(kind)?;+    socket.set_nonblocking(ty & O_NONBLOCK != 0);+    current()?.add_file_with(Arc::new(socket), 0, ty & O_CLOEXEC != 0) } -fn sys_accept(fd: u64, addr_ptr: u64, len_ptr: u64) -> Result<u64, Errno> {+/// accept(fd, *addr, *len) and accept4(..., flags: SOCK_NONBLOCK|SOCK_CLOEXEC).+fn sys_accept(fd: u64, addr_ptr: u64, len_ptr: u64, flags: u64) -> Result<u64, Errno> {     let (socket, peer) = with_socket(fd, |s| s.accept())?;-    let new_fd = current()?.add_file(Arc::new(socket))?;+    socket.set_nonblocking(flags & O_NONBLOCK != 0);+    let new_fd = current()?.add_file_with(Arc::new(socket), 0, flags & O_CLOEXEC != 0)?;     write_sockaddr(addr_ptr, peer)?;     if len_ptr != 0 {         copy_to_user(len_ptr, &16u32)?;@@ -985,9 +1119,15 @@ fn sys_setsockopt(fd: u64, level: u64, name: u64, value: u64, len: u64) -> Resul     Ok(0) } -/// `struct timespec {i64 seconds, i64 nanoseconds}` since boot.-fn sys_clock_gettime(ts_ptr: u64) -> Result<u64, Errno> {-    let us = crate::time::now_us();+/// clock_gettime(clock, *timespec): CLOCK_REALTIME (0, wall-clock time+/// since 1970, from the CMOS clock at boot) or the MONOTONIC family (1,+/// 4 RAW, 6 COARSE, 7 BOOTTIME: time since boot). CPU-time clocks: EINVAL.+fn sys_clock_gettime(clock: u64, ts_ptr: u64) -> Result<u64, Errno> {+    let us = match clock {+        0 | 5 => crate::time::realtime_us(), // REALTIME, REALTIME_COARSE+        1 | 4 | 6 | 7 => crate::time::now_us(),+        _ => return Err(Errno::EINVAL),+    };     copy_to_user(ts_ptr, &[us / 1_000_000, (us % 1_000_000) * 1000])?;     Ok(0) }@@ -1060,14 +1200,177 @@ fn sys_execve(frame: &mut TrapFrame, path: u64, argv: u64, envp: u64) -> Result<     Ok(0) } -/// clone: only the fork-like uses (no threads yet): exit signal SIGCHLD-/// (or 0) in the low byte, optionally CLONE_VFORK, optionally a new stack.-fn sys_clone(frame: &TrapFrame, flags: u64, stack: u64) -> Result<u64, Errno> {+/// clone(flags, stack, *ptid, *ctid, tls): a new thread (CLONE_THREAD)+/// or a new process (fork-like flags).+///+/// A thread shares everything with its process -- memory, fds, signal+/// handlers -- so CLONE_THREAD needs CLONE_VM and CLONE_SIGHAND, like+/// Linux. (CLONE_FILES / CLONE_FS are accepted and implied: our threads+/// always share the fd table and cwd.) pthread_create passes them all plus+/// SETTLS (its thread block), PARENT_SETTID and CHILD_CLEARTID (so+/// pthread_join can sleep on a futex until the thread is gone).+fn sys_clone(frame: &TrapFrame, flags: u64, stack: u64, ptid: u64, ctid: u64, tls: u64) -> Result<u64, Errno> {+    let p = current()?;+    if flags & CLONE_THREAD != 0 {+        const ALLOWED: u64 = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM+            | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID | CLONE_DETACHED;+        if flags & (CLONE_VM | CLONE_SIGHAND) != CLONE_VM | CLONE_SIGHAND || flags & !(ALLOWED | 0xff) != 0 {+            return Err(Errno::EINVAL);+        }+        let mut set_tid = Vec::new();+        if flags & CLONE_PARENT_SETTID != 0 {+            set_tid.push(ptid);+        }+        if flags & CLONE_CHILD_SETTID != 0 {+            set_tid.push(ctid);+        }+        let clear = if flags & CLONE_CHILD_CLEARTID != 0 { ctid } else { 0 };+        let tls = (flags & CLONE_SETTLS != 0).then_some(tls);+        return process::clone_thread(&p, frame, stack, tls, clear, &set_tid);+    }+    // A process: exit signal SIGCHLD (or 0) in the low byte, optionally+    // CLONE_VFORK (+ CLONE_VM: posix_spawn's "vfork"; we copy-on-write+    // instead of sharing, which a vfork child can't tell apart).     let exit_signal = flags & 0xff;-    if flags & !(0xff | CLONE_VFORK) != 0 || (exit_signal != 0 && exit_signal != signal::SIGCHLD as u64) {-        return Err(Errno::ENOSYS); // CLONE_VM, CLONE_THREAD...: threads are future work+    let allowed = 0xff | CLONE_VFORK | CLONE_PARENT_SETTID | if flags & CLONE_VFORK != 0 { CLONE_VM } else { 0 };+    if flags & !allowed != 0 || (exit_signal != 0 && exit_signal != signal::SIGCHLD as u64) {+        return Err(Errno::EINVAL); // shared memory/files between processes: not supported     }-    process::fork(&current()?, frame, stack, flags & CLONE_VFORK != 0)+    let pid = process::fork(&p, frame, stack, flags & CLONE_VFORK != 0)?;+    if flags & CLONE_PARENT_SETTID != 0 {+        copy_to_user(ptid, &(pid as u32))?;+    }+    Ok(pid)+}++/// arch_prctl(ARCH_SET_FS, addr) / (ARCH_GET_FS, *addr): the FS segment+/// base, which libc points at the thread's own data (thread-local storage:+/// `fs:[0]` is "this thread's block" on x86_64). Saved per thread and+/// reloaded by the scheduler on every switch.+fn sys_arch_prctl(code: u64, addr: u64) -> Result<u64, Errno> {+    let t = task()?;+    match code {+        ARCH_SET_FS => {+            if addr >= crate::memory::USER_END {+                return Err(Errno::EPERM); // must be a (canonical) user address+            }+            t.fs_base.store(addr, Ordering::Relaxed);+            x86_64::registers::model_specific::FsBase::write(x86_64::VirtAddr::new(addr));+            Ok(0)+        }+        ARCH_GET_FS => copy_to_user(addr, &t.fs_base.load(Ordering::Relaxed)).map(|_| 0),+        _ => Err(Errno::EINVAL),+    }+}++/// tgkill(tgid, tid, sig) / tkill(tid, sig): a signal for one thread+/// (pthread_kill, raise). sig 0 = just check it exists.+fn sys_tgkill(tgid: Option<u64>, tid: u64, sig: u64) -> Result<u64, Errno> {+    if sig >= signal::NSIG as u64 {+        return Err(Errno::EINVAL);+    }+    let me = current()?;+    let procs = match tgid {+        Some(pid) => alloc::vec![process::get(pid).ok_or(Errno::ESRCH)?],+        None => process::all(),+    };+    let t = procs.iter().find_map(|p| p.task(tid)).ok_or(Errno::ESRCH)?;+    if sig != 0 {+        signal::send_thread(&t, sig as u32, signal::SigInfo::from_pid(sig as u32, me.pid));+    }+    Ok(0)+}++/// nanosleep(*req, *rem). Our timer ticks every ~55 ms, so sleeps are+/// rounded up to that. Cut short by a signal: the time left goes to *rem.+fn sys_nanosleep(req: u64, rem: u64) -> Result<u64, Errno> {+    let us = read_timespec_us(req)?;+    if us == 0 {+        thread::yield_now();+        return Ok(0);+    }+    // The PIT ticks only every ~55 ms, so sleep whole ticks first and then+    // yield until the (TSC-precise) clock says the time is really up.+    let target = crate::time::now_us() + us;+    let deadline = crate::time::deadline_after_us(us).saturating_sub(1);+    let mut result = match deadline.checked_sub(crate::interrupts::ticks()) {+        Some(t) if t > 0 => thread::sleep_ticks_intr(t),+        _ => Ok(()),+    };+    while result.is_ok() && crate::time::now_us() < target {+        if crate::signal::interrupted() {+            result = Err(Errno::ERESTARTNOHAND);+        } else {+            thread::yield_now();+        }+    }+    if result.is_err() && rem != 0 {+        let left = target.saturating_sub(crate::time::now_us());+        copy_to_user(rem, &[left / 1_000_000, left % 1_000_000 * 1000])?;+    }+    result.map(|_| 0)+}++/// pread64 / pwrite64: at `offset`, without moving the file offset.+fn sys_pread(fd: u64, buf: u64, len: u64, offset: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    if (offset as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    file.read_at(user_slice_mut(buf, len)?, offset).map(|n| n as u64)+}++fn sys_pwrite(fd: u64, buf: u64, len: u64, offset: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    if (offset as i64) < 0 {+        return Err(Errno::EINVAL);+    }+    file.write_at(user_slice(buf, len)?, offset).map(|n| n as u64)+}++/// The `struct iovec {void *base; size_t len;}` array of readv/writev.+fn read_iovecs(ptr: u64, count: u64) -> Result<Vec<(u64, u64)>, Errno> {+    if count > 1024 {+        return Err(Errno::EINVAL);+    }+    let raw = user_slice(ptr, count * 16)?;+    Ok(raw.chunks(16).map(|c| (u64::from_le_bytes(c[..8].try_into().unwrap()), u64::from_le_bytes(c[8..].try_into().unwrap()))).collect())+}++/// readv: one read into a kernel buffer, then spread over the buffers+/// (so a pipe or terminal sees a single read, like Linux).+fn sys_readv(fd: u64, iov: u64, count: u64) -> Result<u64, Errno> {+    let file = current()?.file(fd)?;+    let iovs = read_iovecs(iov, count)?;+    let total: u64 = iovs.iter().map(|v| v.1).sum::<u64>().min(1 << 20);+    for &(base, len) in &iovs {+        user_slice_mut(base, len)?; // check first+    }+    let mut buf = alloc::vec![0u8; total as usize];+    let n = file.read(&mut buf)?;+    let mut done = 0;+    for &(base, len) in &iovs {+        let k = (len as usize).min(n - done);+        user_slice_mut(base, k as u64)?.copy_from_slice(&buf[done..done + k]);+        done += k;+    }+    Ok(n as u64)+}++/// writev: gather the buffers and write them in one go (so the pieces of+/// one printf can't be interleaved with another thread's output).+fn sys_writev(fd: u64, iov: u64, count: u64) -> Result<u64, Errno> {+    let process = current()?;+    let file = process.file(fd)?;+    let mut buf = Vec::new();+    for (base, len) in read_iovecs(iov, count)? {+        buf.extend_from_slice(user_slice(base, len)?);+    }+    let result = file.write(&buf).map(|n| n as u64);+    if result == Err(Errno::EPIPE) {+        signal::send(&process, signal::SIGPIPE, signal::SigInfo::from_pid(signal::SIGPIPE, 0));+    }+    result }  fn sys_wait4(pid: i64, status_ptr: u64, options: u64) -> Result<u64, Errno> {@@ -1100,7 +1403,7 @@ fn sys_rt_sigaction(sig: u64, new_ptr: u64, old_ptr: u64, size: u64) -> Result<u  fn sys_rt_sigprocmask(how: u64, set_ptr: u64, old_ptr: u64, size: u64) -> Result<u64, Errno> {     let set = if set_ptr == 0 { None } else { Some(u64::from_le_bytes(user_slice(set_ptr, 8)?.try_into().unwrap())) };-    let old = signal::sys_rt_sigprocmask(&*current()?, how, set, size)?;+    let old = signal::sys_rt_sigprocmask(&*task()?, how, set, size)?;     if old_ptr != 0 {         copy_to_user(old_ptr, &old)?;     }
@@ -61,7 +61,7 @@ //! address" pointing at `thread_trampoline`, which calls the thread's function.  use crate::errno::Errno;-use crate::process::Process;+use crate::process::{Process, Task}; use crate::smp::{self, MAX_CPUS}; use crate::stack::KernelStack; use alloc::{@@ -100,8 +100,9 @@ struct Thread {     stack: Option<KernelStack>,     /// Level-4 page table to run with (the kernel's, or a process's own).     cr3: PhysFrame,-    /// The process this thread belongs to (None = pure kernel thread).-    process: Option<Arc<Process>>,+    /// The user thread (and through it the process) this kernel thread+    /// runs (None = pure kernel thread).+    task: Option<Arc<Task>>,     state: State,     /// Some CPU is (still) running on this thread's stack.     on_cpu: bool,@@ -215,13 +216,13 @@ impl Scheduler {     } } -fn new_thread(name: &str, stack: Option<KernelStack>, cr3: PhysFrame, process: Option<Arc<Process>>) -> Thread {+fn new_thread(name: &str, stack: Option<KernelStack>, cr3: PhysFrame, task: Option<Arc<Task>>) -> Thread {     Thread {         name: String::from(name),         rsp: 0,         stack,         cr3,-        process,+        task,         state: State::Ready,         on_cpu: false,         woken: false,@@ -290,13 +291,14 @@ pub fn spawn(name: &str, entry: fn()) -> Tid {     spawn_with(name, entry, crate::memory::kernel_l4_frame(), None) } -/// Start a thread for `process`: runs `entry` (in the kernel) with the-/// process's address space `cr3` active; `entry` then drops to ring 3.-pub fn spawn_with(name: &str, entry: fn(), cr3: PhysFrame, process: Option<Arc<Process>>) -> Tid {-    new_kernel_thread(name, entry, cr3, process, true)+/// Start a kernel thread for user thread `task`: runs `entry` (in the+/// kernel) with the process's address space `cr3` active; `entry` then+/// drops to ring 3.+pub fn spawn_with(name: &str, entry: fn(), cr3: PhysFrame, task: Option<Arc<Task>>) -> Tid {+    new_kernel_thread(name, entry, cr3, task, true) } -fn new_kernel_thread(name: &str, entry: fn(), cr3: PhysFrame, process: Option<Arc<Process>>, queue: bool) -> Tid {+fn new_kernel_thread(name: &str, entry: fn(), cr3: PhysFrame, task: Option<Arc<Task>>, queue: bool) -> Tid {     let stack = KernelStack::new().expect("out of memory for a kernel stack");      // Build the initial stack, top down (stacks grow towards lower addresses):@@ -316,7 +318,7 @@ fn new_kernel_thread(name: &str, entry: fn(), cr3: PhysFrame, process: Option<Ar     let rsp = top - 8 * initial.len() as u64;     unsafe { core::ptr::copy_nonoverlapping(initial.as_ptr(), rsp as *mut u64, initial.len()) }; -    let mut thread = new_thread(name, Some(stack), cr3, process);+    let mut thread = new_thread(name, Some(stack), cr3, task);     thread.rsp = rsp;     let id = interrupts::without_interrupts(|| {         let mut s = lock();@@ -369,8 +371,14 @@ pub fn schedule() {         t.state = State::Running;         t.on_cpu = true;         t.ran_on |= bit;-        if let Some(process) = &t.process {-            process.ran_on.fetch_or(bit, core::sync::atomic::Ordering::Relaxed);+        if let Some(task) = &t.task {+            task.process.ran_on.fetch_or(bit, core::sync::atomic::Ordering::Relaxed);+            // Thread-local storage: each user thread has its own FS base+            // (set with arch_prctl). The CPU has one FS_BASE register, so+            // it's reloaded on every switch to a user thread.+            x86_64::registers::model_specific::FsBase::write(x86_64::VirtAddr::new_truncate(+                task.fs_base.load(core::sync::atomic::Ordering::Relaxed),+            ));         }         if let Some(stack) = &t.stack {             if t.tlb_clean_on & bit == 0 {@@ -387,6 +395,8 @@ pub fn schedule() {             // every address space. Writing CR3 also flushes the TLB.             unsafe { Cr3::write(t.cr3, Cr3::read().1) };         }+        // Which address space this CPU runs now (TLB shootdowns, memory.rs).+        smp::set_active_cr3(cpu, t.cr3.start_address().as_u64());         CPU_IDLING[cpu].store(t.idle, Ordering::Relaxed);         let new_rsp = t.rsp;         s.current[cpu] = Some(next);@@ -414,7 +424,7 @@ fn finish_switch() {         let state = t.state;         // A finished thread: take out what it owns (its stack, and its         // share of the process) to free below, outside the lock.-        let leftovers = (state == State::Finished).then(|| (prev, t.stack.take(), t.process.take()));+        let leftovers = (state == State::Finished).then(|| (prev, t.stack.take(), t.task.take()));         (leftovers, state == State::Ready)     };     if requeued {@@ -456,7 +466,16 @@ pub fn current_process() -> Option<Arc<Process>> {     interrupts::without_interrupts(|| {         let mut s = lock();         let cur = s.current(smp::cpu_id());-        s.thread(cur).process.clone()+        s.thread(cur).task.as_ref().map(|t| t.process.clone())+    })+}++/// The user thread the running kernel thread carries, if any.+pub fn current_task() -> Option<Arc<Task>> {+    interrupts::without_interrupts(|| {+        let mut s = lock();+        let cur = s.current(smp::cpu_id());+        s.thread(cur).task.clone()     }) } @@ -666,7 +685,7 @@ pub fn die_of_stack_overflow(addr: u64) -> ! {         addr & !0xfff     );     if current_process().is_some() {-        crate::process::exit_current(crate::process::ExitStatus::Killed(crate::signal::SIGSEGV, "kernel stack overflow"));+        crate::process::exit_group(crate::process::ExitStatus::Killed(crate::signal::SIGSEGV, "kernel stack overflow"));     }     exit_current() }
@@ -42,8 +42,67 @@ pub fn now_us() -> u64 {     (rdtsc() - TSC_START.load(Ordering::Relaxed)) * 1000 / per_ms } -/// The PIT tick at which `us` microseconds from now will have passed-/// (rounded up, at least the next tick). For sleeping/timeouts.+/// The PIT tick by which `us` microseconds from now will surely have+/// passed. For sleeping/timeouts, which may run late but never early: we+/// may be anywhere inside the current ~55 ms tick, so one extra tick. pub fn deadline_after_us(us: u64) -> u64 {-    crate::interrupts::ticks() + us.div_ceil(PIT_TICK_US).max(1)+    crate::interrupts::ticks() + us.div_ceil(PIT_TICK_US) + 1+}++// ---------------------------------------------------------------------------+// Wall-clock time (CLOCK_REALTIME)+// ---------------------------------------------------------------------------++/// Unix time (microseconds since 1970-01-01 UTC) at boot (now_us = 0),+/// read from the CMOS clock on first use. 0 = not read yet.+static BOOT_REALTIME_US: AtomicU64 = AtomicU64::new(0);++/// Read a CMOS register (the PC's battery-backed real-time clock).+fn cmos(reg: u8) -> u8 {+    use x86_64::instructions::port::Port;+    unsafe {+        Port::<u8>::new(0x70).write(reg);+        Port::<u8>::new(0x71).read()+    }+}++/// The RTC's date and time as Unix seconds. QEMU's RTC runs on UTC.+fn rtc_unix_seconds() -> u64 {+    x86_64::instructions::interrupts::without_interrupts(|| {+        // Don't read in the middle of an update (bit 7 of status A).+        while cmos(0x0A) & 0x80 != 0 {+            core::hint::spin_loop();+        }+        let [sec, min, hour, day, month, year] = [0x00, 0x02, 0x04, 0x07, 0x08, 0x09].map(cmos);+        let status_b = cmos(0x0B);+        // Usually the values are BCD (0x59 = 59) unless bit 2 says binary.+        let bin = |v: u8| if status_b & 0x04 != 0 { v as u64 } else { ((v >> 4) * 10 + (v & 0xf)) as u64 };+        let mut h = bin(hour & 0x7f);+        if status_b & 0x02 == 0 && hour & 0x80 != 0 {+            h = (h + 12) % 24; // 12-hour clock, PM+        }+        let (y, m, d) = (2000 + bin(year), bin(month), bin(day));+        days_from_civil(y, m, d) * 86400 + h * 3600 + bin(min) * 60 + bin(sec)+    })+}++/// Days since 1970-01-01 for a date (Howard Hinnant's algorithm).+fn days_from_civil(y: u64, m: u64, d: u64) -> u64 {+    let y = if m <= 2 { y - 1 } else { y };+    let era = y / 400;+    let yoe = y - era * 400;+    let mp = (m + 9) % 12;+    let doy = (153 * mp + 2) / 5 + d - 1;+    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;+    era * 146097 + doe - 719468+}++/// Microseconds since 1970-01-01 UTC (clock_gettime(CLOCK_REALTIME)).+pub fn realtime_us() -> u64 {+    let mut boot = BOOT_REALTIME_US.load(Ordering::Relaxed);+    if boot == 0 {+        boot = (rtc_unix_seconds() * 1_000_000).saturating_sub(now_us()).max(1);+        BOOT_REALTIME_US.store(boot, Ordering::Relaxed);+    }+    boot + now_us() }
@@ -1,231 +1,1019 @@-//! The console "tty": keyboard input, and the `File` behind file-//! descriptors 0, 1 and 2.+//! Terminals ("ttys"): the console, /dev/hvc0, and pseudo-terminals. //!-//! Two input modes, like a Linux terminal:+//! **Why terminals are weird.** A 1970s terminal was a keyboard and a+//! screen on a serial line, and the kernel sat between that line and the+//! programs. Typing went through a "line discipline" that let you fix+//! typos *before* a program saw the line, echoed what you typed, and+//! turned a few keys into signals (Ctrl+C = stop that program). All of+//! that survives: every terminal -- the console, the virtio console, a+//! terminal window -- is a `Tty` running the same line discipline,+//! configured with a `termios` struct (TCGETS/TCSETS ioctls; libc's+//! tcgetattr/tcsetattr): //!-//! * **cooked** (canonical, the default): typed characters are echoed and-//!   collected in a line buffer (Backspace edits it); only when Enter is-//!   pressed does the whole line become readable from fd 0. So `read(0)`-//!   gets a complete, already-edited line. Programs like `cat` use this.-//! * **raw**: every key goes to the reader immediately and nothing is echoed;-//!   arrow keys arrive as ANSI escape sequences (`ESC [ A` = up). The shell-//!   switches to raw while it edits its command line, so it can do its own-//!   echo, Backspace and up-arrow history. (Linux: `tcsetattr` / `ioctl`.)+//! * **canonical** mode (ICANON, the default): typed bytes collect in a+//!   line buffer you can edit (Erase, ^U kills the line, ^D = end of+//!   file); `read` only sees whole lines. **Raw**/non-canonical mode:+//!   every byte is readable at once (editors, our shell's line editor).+//! * ECHO: the tty prints what you type (passwords turn it off).+//! * ISIG: ^C, ^\ and ^Z send SIGINT, SIGQUIT and SIGTSTP to the+//!   terminal's *foreground process group* -- the job the shell put in+//!   front (TIOCSPGRP). That's why ^C kills `cat` but not the shell.+//! * Output: ONLCR turns "\n" into "\r\n" (a real terminal needs the+//!   carriage return to go back to column 0). //!-//! In cooked mode Ctrl+C and Ctrl+Z don't become input: they send SIGINT /-//! SIGTSTP to the *foreground process group* (set by the shell with-//! ioctl TIOCSPGRP) -- that's how Ctrl+C stops the running program but not-//! the shell. (The virtio console, hvc0, has no job control.)+//! **Sessions.** `setsid()` starts a session (a login, roughly); its first+//! terminal opened becomes the session's *controlling terminal*, which+//! `/dev/tty` always refers to, and whose ^C goes to the session's+//! foreground group. When the session leader exits, the terminal is let+//! go and the foreground job gets SIGHUP ("hang up", from modems). //!-//! Output (`Console::write`) understands two control characters besides-//! '\n': '\x08' erases the character before the cursor, and '\x0c' (form-//! feed) clears the screen.+//! **Pseudo-terminals** (ptys) are terminals with a program instead of a+//! keyboard and screen on the other side: opening /dev/ptmx gives the+//! *master* end and creates /dev/pts/N, the *slave* end. Bytes written to+//! the master are "typed" into the slave (through the line discipline);+//! what programs write to the slave comes out of the master. Terminal+//! emulators, ssh and tmux are all programs holding a pty master.  use crate::errno::Errno;-use crate::fs::file::{File, KIND_CONSOLE, Stat};+use crate::fs::file::{File, KIND_CONSOLE, KIND_DEVICE, Stat};+use crate::poll::{self, POLLHUP, POLLIN, POLLOUT};+use crate::process::Process; use crate::signal; use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer};-use alloc::string::String;+use alloc::{collections::VecDeque, format, string::String, sync::Arc, vec::Vec}; use conquer_once::spin::OnceCell;-use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};-use crossbeam_queue::ArrayQueue;+use core::sync::atomic::{AtomicBool, Ordering}; use pc_keyboard::KeyCode; use spin::Mutex;+use x86_64::instructions::interrupts::without_interrupts; -/// The line being typed in cooked mode (not yet readable).-static LINE: Mutex<String> = Mutex::new(String::new());-/// Bytes waiting for `read(0, ...)`: finished lines (cooked) or keys (raw).-static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();-/// Raw mode on?-static RAW: AtomicBool = AtomicBool::new(false);-/// pid of the process that switched raw mode on (0 = none). If it exits-/// without switching back, the console goes back to cooked mode.-static RAW_OWNER: AtomicU64 = AtomicU64::new(0);+// --- termios (Linux's x86_64 layout and values) ----------------------------- -/// The foreground process group: Ctrl+C / Ctrl+Z signal it (0 = none).-/// The shell sets it to each job it runs, and back to itself after.-static FOREGROUND: AtomicU64 = AtomicU64::new(0);--pub fn foreground() -> u64 {-    FOREGROUND.load(Ordering::SeqCst)+/// `struct termios`: 36 bytes.+#[repr(C)]+#[derive(Clone, Copy)]+pub struct Termios {+    pub iflag: u32,+    pub oflag: u32,+    pub cflag: u32,+    pub lflag: u32,+    pub line: u8,+    pub cc: [u8; 19], } -pub fn set_foreground(pgid: u64) {-    FOREGROUND.store(pgid, Ordering::SeqCst);+// iflag+const INLCR: u32 = 0x40; // input '\n' -> '\r'+const IGNCR: u32 = 0x80; // drop input '\r'+const ICRNL: u32 = 0x100; // input '\r' -> '\n' (Enter sends '\r')+const IXON: u32 = 0x400;+// oflag+const OPOST: u32 = 0x1; // process output at all+const ONLCR: u32 = 0x4; // output '\n' -> "\r\n"+// lflag+const ISIG: u32 = 0x1;+const ICANON: u32 = 0x2;+const ECHO: u32 = 0x8;+const ECHOE: u32 = 0x10;+const ECHOK: u32 = 0x20;+const NOFLSH: u32 = 0x80;+const ECHOCTL: u32 = 0x200;+const ECHOKE: u32 = 0x800;+const IEXTEN: u32 = 0x8000;+// c_cc indexes+const VINTR: usize = 0;+const VQUIT: usize = 1;+const VERASE: usize = 2;+const VKILL: usize = 3;+const VEOF: usize = 4;+const VTIME: usize = 5;+const VMIN: usize = 6;+const VSUSP: usize = 10;+const VEOL: usize = 11;++impl Termios {+    /// What a fresh Linux terminal starts with (`stty sane`).+    pub const fn sane() -> Termios {+        let mut cc = [0u8; 19];+        cc[VINTR] = 3; // ^C+        cc[VQUIT] = 0x1c; // ^\+        cc[VERASE] = 0x7f; // DEL (we also take ^H, what our keyboard sends)+        cc[VKILL] = 0x15; // ^U+        cc[VEOF] = 4; // ^D+        cc[VMIN] = 1;+        cc[8] = 0x11; // VSTART ^Q+        cc[9] = 0x13; // VSTOP ^S+        cc[VSUSP] = 0x1a; // ^Z+        cc[12] = 0x12; // VREPRINT ^R+        cc[14] = 0x17; // VWERASE ^W+        cc[15] = 0x16; // VLNEXT ^V+        Termios {+            iflag: ICRNL | IXON,+            oflag: OPOST | ONLCR,+            cflag: 0xbf, // B38400 | CS8 | CREAD | HUPCL+            lflag: ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN,+            line: 0,+            cc,+        }+    }+    fn bytes(&self) -> [u8; 36] {+        unsafe { core::mem::transmute(*self) }+    }+    fn from_bytes(b: &[u8]) -> Termios {+        unsafe { core::ptr::read_unaligned(b.as_ptr() as *const Termios) }+    } } -/// Threads blocked in `Console::read`, waiting for input.-static INPUT_WAIT: WaitQueue = WaitQueue::new();+// ioctl numbers+pub const TCGETS: u64 = 0x5401;+pub const TCSETS: u64 = 0x5402; // now+pub const TCSETSW: u64 = 0x5403; // after output drains (ours never queues)+pub const TCSETSF: u64 = 0x5404; // ...and throw away unread input+pub const TIOCSCTTY: u64 = 0x540E;+pub const TIOCGPGRP: u64 = 0x540F;+pub const TIOCSPGRP: u64 = 0x5410;+pub const TIOCGWINSZ: u64 = 0x5413;+pub const TIOCSWINSZ: u64 = 0x5414;+pub const FIONREAD: u64 = 0x541B;+pub const FIONBIO: u64 = 0x5421;+pub const TIOCNOTTY: u64 = 0x5422;+pub const TIOCGSID: u64 = 0x5429;+pub const TIOCGPTN: u64 = 0x8004_5430;+pub const TIOCSPTLCK: u64 = 0x4004_5431;+/// Ours (from stage 6): raw mode on/off in one call. Undone automatically+/// if the process that switched it on exits. Returns the old setting.+pub const TTY_SET_RAW: u64 = 1;++/// How many input bytes a tty buffers (like Linux's N_TTY: 4 KiB).+const INPUT_MAX: usize = 4096;+/// How much pty output can wait for the master to read it.+const OUTPUT_MAX: usize = 64 * 1024; -fn input() -> &'static ArrayQueue<u8> {-    INPUT.get_or_init(|| ArrayQueue::new(4096))+// --- the tty ----------------------------------------------------------------++/// Where a tty's output goes / its input comes from.+#[derive(Clone, Copy, PartialEq)]+enum Driver {+    Console, // keyboard in; VGA/framebuffer + serial out+    Hvc,     // virtio console (both ways)+    Pty,     // the master end, through `output` } -/// Switch between raw (`true`) and cooked mode for process `pid`. Returns-/// the old mode.-pub fn set_raw(raw: bool, pid: u64) -> bool {-    LINE.lock().clear(); // a half-typed cooked line doesn't survive a switch-    RAW_OWNER.store(if raw { pid } else { 0 }, Ordering::SeqCst);-    RAW.swap(raw, Ordering::SeqCst)+pub struct Tty {+    pub name: String,+    driver: Driver,+    /// Pty number (the N of /dev/pts/N).+    index: usize,+    /// Locked with interrupts off (touched in wait conditions).+    state: Mutex<TtyState>,+    readers: WaitQueue,+    writers: WaitQueue, } -/// A process ended: if it left the console in raw mode, undo that (so the-/// next program, or a respawned shell, gets normal line input).-pub fn process_exited(pid: u64) {-    if RAW_OWNER.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_ok() {-        RAW.store(false, Ordering::SeqCst);-    }+struct TtyState {+    termios: Termios,+    /// The canonical-mode line being edited (not yet readable).+    line: Vec<u8>,+    /// Readable input: finished lines (canonical) or bytes. An empty chunk+    /// is an end of file (^D on an empty line): `read` returns 0 once.+    input: VecDeque<Vec<u8>>,+    winsize: [u16; 4], // rows, cols, x pixels, y pixels+    /// Session it's the controlling terminal of (0 = none).+    session: u64,+    /// Foreground process group (gets ^C).+    pgrp: u64,+    /// TTY_SET_RAW: who switched raw on, and the settings before.+    raw_owner: Option<(u64, Termios)>,+    // --- ptys only ---+    output: VecDeque<u8>, // slave output, for the master to read+    master_open: bool,+    slaves_open: usize,+    slave_opened: bool, // (master reads EIO only once a slave came and went)+    locked: bool,       // TIOCSPTLCK: slave can't be opened yet } -pub fn is_raw() -> bool {-    RAW.load(Ordering::SeqCst)+/// What the line discipline decided while holding the lock; done after.+#[derive(Default)]+struct Effects {+    echo: Vec<u8>,+    signal: Option<u32>, } -/// Make `bytes` readable and wake readers. (Queue full: drop -- nobody reads.)-fn deliver(bytes: &[u8]) {-    for &b in bytes {-        let _ = input().push(b);+impl Tty {+    fn new(name: String, driver: Driver, index: usize) -> Arc<Tty> {+        let (cols, rows) = vga_buffer::size();+        Arc::new(Tty {+            name,+            driver,+            index,+            state: Mutex::new(TtyState {+                termios: Termios::sane(),+                line: Vec::new(),+                input: VecDeque::new(),+                winsize: if driver == Driver::Console { [rows as u16, cols as u16, 0, 0] } else { [24, 80, 0, 0] },+                session: 0,+                pgrp: 0,+                raw_owner: None,+                output: VecDeque::new(),+                master_open: true,+                slaves_open: 0,+                slave_opened: false,+                locked: driver == Driver::Pty,+            }),+            readers: WaitQueue::new(),+            writers: WaitQueue::new(),+        })     }-    INPUT_WAIT.wake_all();-} -/// Called by the keyboard task for every typed character.-pub fn input_char(c: char) {-    if is_raw() {-        let mut utf8 = [0; 4];-        deliver(c.encode_utf8(&mut utf8).as_bytes());-        return;-    }-    let mut line = LINE.lock();-    match c {-        '\u{8}' => {-            // Backspace: only erase what's in the current line.-            if line.pop().is_some() {-                vga_buffer::backspace();-                serial_print!("\x08"); // uart_16550 turns this into "\x08 \x08"-            }-        }-        '\n' => {-            print!("\n");-            serial_print!("\n");-            line.push('\n');-            deliver(line.as_bytes());-            line.clear();-        }-        // Ctrl+C / Ctrl+Z: the "line discipline" turns them into signals-        // for the foreground job (Linux: the ISIG flag of termios). Raw mode-        // passes them through as bytes 3 / 26 instead (above).-        '\u{3}' | '\u{1a}' => {-            let (echo, sig) = if c == '\u{3}' { ("^C\n", signal::SIGINT) } else { ("^Z\n", signal::SIGTSTP) };-            print!("{}", echo);-            serial_print!("{}", echo);-            line.clear(); // the half-typed line is thrown away-            drop(line);-            let fg = foreground();-            if fg != 0 {-                crate::process::signal_group(fg, sig);-            }-        }-        c if (c as u32) < 0x20 && c != '\t' => {} // other control keys: ignore-        c => {-            print!("{}", c);-            serial_print!("{}", c);-            line.push(c);-        }-    }-}--/// Called by the keyboard task for keys that aren't characters (arrows...).-pub fn input_key(key: KeyCode) {-    if is_raw() {-        // The ANSI/VT100 sequences a Linux terminal would send.-        let seq: &[u8] = match key {-            KeyCode::ArrowUp => b"\x1b[A",-            KeyCode::ArrowDown => b"\x1b[B",-            KeyCode::ArrowRight => b"\x1b[C",-            KeyCode::ArrowLeft => b"\x1b[D",-            KeyCode::Home => b"\x1b[H",-            KeyCode::End => b"\x1b[F",-            KeyCode::Delete => b"\x1b[3~",-            _ => return,-        };-        deliver(seq);-    } else {-        print!("<{:?}>", key);-        serial_print!("<{:?}>", key);+    fn lock(&self) -> spin::MutexGuard<'_, TtyState> {+        self.state.lock()     }-} -/// The console as a file: reading gets keyboard input, writing prints to the-/// screen and serial port.-pub struct Console;+    /// Foreground process group.+    pub fn foreground(&self) -> u64 {+        without_interrupts(|| self.lock().pgrp)+    } -/// Held for the whole of one console `write` (see `Console::write`).-static CONSOLE_WRITE: spin::Mutex<()> = spin::Mutex::new(());+    pub fn set_foreground(&self, pgid: u64) {+        without_interrupts(|| self.lock().pgrp = pgid);+    } -impl File for Console {-    /// Blocks until input is available. Cooked: returns up to `buf.len()`-    /// bytes of a line, stopping after its '\n'. Raw: whatever keys are there.-    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {-        if buf.is_empty() {-            return Ok(0);+    /// Session it's the controlling terminal of (0 = none).+    pub fn session(&self) -> u64 {+        without_interrupts(|| self.lock().session)+    }++    // --- input: the line discipline -----------------------------------------++    /// Bytes arriving from the keyboard / the host / the pty master.+    pub fn input(&self, bytes: &[u8]) {+        let (fx, pgrp) = without_interrupts(|| {+            let mut s = self.lock();+            let mut fx = Effects::default();+            for &b in bytes {+                s.input_byte(b, &mut fx, self.driver);+            }+            (fx, s.pgrp)+        });+        if !fx.echo.is_empty() {+            let _ = self.output(&fx.echo, false); // (echo into a full pty: dropped)+        }+        if let (Some(sig), true) = (fx.signal, pgrp != 0) {+            crate::process::signal_group(pgrp, sig);         }-        // Block until input arrives: the thread is parked on INPUT_WAIT-        // (using no CPU) and `deliver` wakes it. The closure runs with-        // interrupts off; the ArrayQueue is lock-free, so that's fine.-        INPUT_WAIT.wait_until_intr(|| {-            let mut n = 0;-            while n < buf.len() {-                let Some(b) = input().pop() else { break };-                buf[n] = b;-                n += 1;+        self.readers.wake_all();+        poll::notify();+    }++    // --- output -------------------------------------------------------------++    /// Send bytes towards the screen/host/master, with output processing+    /// (ONLCR). `block`: a program's write waits for room in a pty;+    /// echo doesn't (it's dropped instead).+    fn output(&self, bytes: &[u8], block: bool) -> Result<usize, Errno> {+        let termios = without_interrupts(|| self.lock().termios);+        let asked = bytes.len(); // what we report: the caller's count, not the cooked one+        let cooked: Vec<u8>;+        let onlcr = self.driver != Driver::Console && termios.oflag & (OPOST | ONLCR) == OPOST | ONLCR && bytes.contains(&b'\n');+        let bytes = if onlcr {+            let mut v = Vec::with_capacity(bytes.len() + 16);+            for &b in bytes {                 if b == b'\n' {-                    break;+                    v.push(b'\r');                 }+                v.push(b);             }-            if n > 0 { Some(n) } else { None }-        })+            cooked = v;+            &cooked[..]+        } else {+            bytes+        };+        match self.driver {+            Driver::Console => console_output(bytes),+            Driver::Hvc => {+                crate::virtio::console::write(bytes);+            }+            Driver::Pty => {+                let mut done = 0;+                let r = self.writers.wait_until_intr(|| {+                    let mut s = self.lock();+                    if !s.master_open {+                        return Some(Err(Errno::EIO)); // hung up+                    }+                    let n = (OUTPUT_MAX - s.output.len()).min(bytes.len() - done);+                    s.output.extend(&bytes[done..done + n]);+                    done += n;+                    if n > 0 {+                        self.readers.wake_all(); // (the master's readers)+                        poll::notify();+                    }+                    (done == bytes.len() || !block).then_some(Ok(()))+                });+                match r {+                    Ok(Err(e)) => return Err(e),+                    Err(e) if done == 0 => return Err(e),+                    _ => {}+                }+                if done < bytes.len() {+                    // Only part of it fit (nonblocking, or a signal). Report+                    // how many of the *caller's* bytes went out: every '\n'+                    // sent cost two cooked bytes ("\r\n"), and a lone '\r'+                    // added in front of an unsent '\n' doesn't count.+                    if done == 0 {+                        return Err(Errno::EAGAIN);+                    }+                    let mut sent = done;+                    if onlcr {+                        sent -= bytes[..done].iter().filter(|&&b| b == b'\n').count();+                        if bytes[done] == b'\n' {+                            sent -= 1;+                        }+                    }+                    return Ok(sent);+                }+            }+        }+        Ok(asked)     } -    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {-        // One write = one unbroken piece of output, even when programs on-        // other CPUs write at the same time. Taken in thread context only-        // (never in an interrupt handler), so while waiting we just yield.-        let _turn = loop {-            match CONSOLE_WRITE.try_lock() {-                Some(guard) => break guard,-                None => crate::thread::yield_now(),+    // --- the slave/terminal side, as a file ---------------------------------++    fn read(&self, buf: &mut [u8], nonblocking: bool) -> Result<usize, Errno> {+        if buf.is_empty() {+            return Ok(0);+        }+        let (vmin, vtime, canon) = without_interrupts(|| {+            let t = self.lock().termios;+            (t.cc[VMIN], t.cc[VTIME], t.lflag & ICANON != 0)+        });+        // Non-canonical with VMIN = 0: don't wait (VTIME = 0) or wait at+        // most VTIME tenths of a second for something.+        let deadline = match (canon, vmin, vtime) {+            (false, 0, 0) => Some(0),+            (false, 0, t) => Some(crate::time::deadline_after_us(t as u64 * 100_000)),+            _ => None,+        };+        let mut take = || {+            let mut s = self.lock();+            if self.driver == Driver::Pty && !s.master_open && s.input.is_empty() {+                return Some(Ok(0)); // hung up: end of file+            }+            match s.take_input(buf, canon) {+                Some(n) => Some(Ok(n)),+                None if nonblocking => Some(Err(Errno::EAGAIN)),+                None => None,             }         };-        // Print valid UTF-8 as is, anything else as '?'. Control characters-        // are handled one by one.-        for chunk in buf.utf8_chunks() {-            for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) {-                let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) {-                    Some(text) => (text, piece.chars().last()),-                    None => (piece, None),+        let r = match deadline {+            None => self.readers.wait_until_intr(take)?,+            Some(0) => without_interrupts(|| take()).unwrap_or(Ok(0)),+            Some(d) => self.readers.wait_until_deadline_intr(d, take)?.unwrap_or(Ok(0)),+        };+        poll::notify(); // (room in the input buffer)+        r+    }++    fn poll_slave(&self) -> u16 {+        without_interrupts(|| {+            let s = self.lock();+            let mut events = if s.input.is_empty() { 0 } else { POLLIN };+            if self.driver == Driver::Pty {+                if !s.master_open {+                    events |= POLLHUP | POLLIN;+                } else if s.output.len() < OUTPUT_MAX {+                    events |= POLLOUT;+                }+            } else {+                events |= POLLOUT;+            }+            events+        })+    }++    // --- the pty master side ------------------------------------------------++    fn master_read(&self, buf: &mut [u8], nonblocking: bool) -> Result<usize, Errno> {+        if buf.is_empty() {+            return Ok(0);+        }+        let r = self.readers.wait_until_intr(|| {+            let mut s = self.lock();+            if s.output.is_empty() {+                return match () {+                    _ if s.slave_opened && s.slaves_open == 0 => Some(Err(Errno::EIO)), // Linux: EIO+                    _ if nonblocking => Some(Err(Errno::EAGAIN)),+                    _ => None,                 };-                print!("{}", text);-                serial_print!("{}", text);-                match control {-                    Some('\x08') => {-                        vga_buffer::backspace();-                        serial_print!("\x08");+            }+            let n = buf.len().min(s.output.len());+            for (dst, src) in buf.iter_mut().zip(s.output.drain(..n)) {+                *dst = src;+            }+            Some(Ok(n))+        })?;+        self.writers.wake_all();+        poll::notify();+        r+    }++    fn poll_master(&self) -> u16 {+        without_interrupts(|| {+            let s = self.lock();+            let mut events = POLLOUT;+            if !s.output.is_empty() {+                events |= POLLIN;+            }+            if s.slave_opened && s.slaves_open == 0 {+                events |= POLLHUP;+            }+            events+        })+    }++    // --- ioctls ---------------------------------------------------------------++    fn ioctl(&self, file: &TtyFile, cmd: u64, arg: u64) -> Result<u64, Errno> {+        use crate::syscall::{copy_to_user as copy_out, user_slice};+        let p = crate::thread::current_process().ok_or(Errno::EINVAL)?;+        let read_u32 = || -> Result<u32, Errno> { Ok(u32::from_le_bytes(user_slice(arg, 4)?.try_into().unwrap())) };+        match cmd {+            TCGETS => copy_out(arg, &without_interrupts(|| self.lock().termios).bytes()).map(|_| 0),+            TCSETS | TCSETSW | TCSETSF => {+                let t = Termios::from_bytes(user_slice(arg, 36)?);+                without_interrupts(|| self.lock().set_termios(t, cmd == TCSETSF));+                self.readers.wake_all();+                Ok(0)+            }+            TTY_SET_RAW => {+                let was_raw = without_interrupts(|| {+                    let mut s = self.lock();+                    let was_raw = s.termios.lflag & ICANON == 0;+                    if arg != 0 && !was_raw {+                        let old = s.termios;+                        s.raw_owner = Some((p.pid, old));+                        let mut raw = old;+                        raw.lflag &= !(ICANON | ECHO | ISIG | IEXTEN);+                        raw.iflag &= !(ICRNL | INLCR | IGNCR | IXON);+                        raw.cc[VMIN] = 1;+                        raw.cc[VTIME] = 0;+                        s.set_termios(raw, false);+                    } else if arg == 0 && was_raw {+                        let old = s.raw_owner.take().map_or(Termios::sane(), |(_, t)| t);+                        s.set_termios(old, false);+                    }+                    was_raw+                });+                self.readers.wake_all();+                Ok(was_raw as u64)+            }+            TIOCGWINSZ => copy_out(arg, &without_interrupts(|| self.lock().winsize)).map(|_| 0),+            TIOCSWINSZ => {+                let b = user_slice(arg, 8)?;+                let ws = [0, 2, 4, 6].map(|i| u16::from_le_bytes([b[i], b[i + 1]]));+                let pgrp = without_interrupts(|| {+                    let mut s = self.lock();+                    s.winsize = ws;+                    s.pgrp+                });+                if pgrp != 0 {+                    crate::process::signal_group(pgrp, signal::SIGWINCH);+                }+                Ok(0)+            }+            FIONREAD => {+                let n: usize = without_interrupts(|| {+                    let s = self.lock();+                    if file.master { s.output.len() } else { s.input.iter().map(|c| c.len()).sum() }+                });+                copy_out(arg, &(n as u32)).map(|_| 0)+            }+            FIONBIO => {+                file.set_nonblocking(read_u32()? != 0);+                Ok(0)+            }+            TIOCGPTN if file.master => copy_out(arg, &(self.index as u32)).map(|_| 0),+            TIOCSPTLCK if file.master => {+                let lock = read_u32()? != 0;+                without_interrupts(|| self.lock().locked = lock);+                Ok(0)+            }+            TIOCGPGRP => copy_out(arg, &(self.foreground() as u32)).map(|_| 0),+            TIOCSPGRP => {+                let pgid = read_u32()? as u64;+                let session = self.session();+                if session != 0 && session != p.sid() {+                    return Err(Errno::ENOTTY); // not our terminal+                }+                if crate::process::group_members(pgid).is_empty() {+                    return Err(Errno::ESRCH);+                }+                self.set_foreground(pgid);+                Ok(0)+            }+            TIOCGSID => match self.session() {+                0 => Err(Errno::ENOTTY),+                sid => copy_out(arg, &(sid as u32)).map(|_| 0),+            },+            TIOCSCTTY => attach(&p, self.self_arc()?, arg == 1).map(|_| 0),+            TIOCNOTTY => {+                let mine = p.ctty.lock().as_ref().is_some_and(|t| core::ptr::eq(&**t, self));+                if !mine {+                    return Err(Errno::ENOTTY);+                }+                if p.sid() == p.pid {+                    hang_up(self);+                }+                *p.ctty.lock() = None;+                Ok(0)+            }+            _ => Err(Errno::ENOTTY),+        }+    }++    /// The Arc for `self` (from the registry).+    fn self_arc(&self) -> Result<Arc<Tty>, Errno> {+        all_ttys().into_iter().find(|t| core::ptr::eq(&**t, self)).ok_or(Errno::ENOTTY)+    }+}++impl TtyState {+    fn set_termios(&mut self, t: Termios, flush: bool) {+        let was_canon = self.termios.lflag & ICANON != 0;+        self.termios = t;+        if flush {+            self.input.clear();+            self.line.clear();+        }+        // Leaving canonical mode: a half-edited line becomes readable.+        if was_canon && t.lflag & ICANON == 0 && !self.line.is_empty() {+            let line = core::mem::take(&mut self.line);+            self.input.push_back(line);+        }+    }++    fn buffered(&self) -> usize {+        self.line.len() + self.input.iter().map(|c| c.len()).sum::<usize>()+    }++    /// One input byte through the line discipline.+    fn input_byte(&mut self, mut b: u8, fx: &mut Effects, driver: Driver) {+        let t = self.termios;+        // Input translations: Enter sends '\r'; programs want '\n'.+        if b == b'\r' {+            if t.iflag & IGNCR != 0 {+                return;+            }+            if t.iflag & ICRNL != 0 {+                b = b'\n';+            }+        } else if b == b'\n' && t.iflag & INLCR != 0 {+            b = b'\r';+        }+        // ^C ^\ ^Z: signals for the foreground job.+        if t.lflag & ISIG != 0 {+            let sig = match b {+                _ if b == t.cc[VINTR] && b != 0 => Some(signal::SIGINT),+                _ if b == t.cc[VQUIT] && b != 0 => Some(signal::SIGQUIT),+                _ if b == t.cc[VSUSP] && b != 0 => Some(signal::SIGTSTP),+                _ => None,+            };+            if let Some(sig) = sig {+                if t.lflag & NOFLSH == 0 {+                    self.line.clear(); // the half-typed line is thrown away+                    self.input.clear();+                }+                if t.lflag & ECHO != 0 {+                    // "^C" -- plus a newline, so the next prompt starts+                    // on a fresh line (Linux leaves that to the shell).+                    fx.echo.extend_from_slice(&[b'^', b + 0x40, b'\n']);+                }+                fx.signal = Some(sig);+                return;+            }+        }+        if t.lflag & ICANON == 0 {+            // Raw: readable at once.+            if self.buffered() < INPUT_MAX {+                match self.input.back_mut() {+                    Some(chunk) if !chunk.is_empty() => chunk.push(b),+                    _ => self.input.push_back(alloc::vec![b]),+                }+                if t.lflag & ECHO != 0 {+                    fx.echo.push(b);+                }+            }+            return;+        }+        // Canonical: edit the line.+        let echo = t.lflag & ECHO != 0;+        let erase: &[u8] = if driver == Driver::Console { b"\x08" } else { b"\x08 \x08" };+        match b {+            _ if b == t.cc[VERASE] || b == 0x08 => {+                if self.line.pop().is_some() && echo && t.lflag & ECHOE != 0 {+                    fx.echo.extend_from_slice(erase);+                }+            }+            _ if b == t.cc[VKILL] => {+                for _ in 0..self.line.len() {+                    if echo && t.lflag & ECHOK != 0 {+                        fx.echo.extend_from_slice(erase);+                    }+                }+                self.line.clear();+            }+            _ if b == t.cc[VEOF] => {+                // ^D: hand over the line so far (without the ^D); on an+                // empty line that's a 0-byte read = end of file.+                let line = core::mem::take(&mut self.line);+                self.input.push_back(line);+            }+            b'\n' => {+                if echo {+                    fx.echo.push(b'\n');+                }+                self.line.push(b'\n');+                let line = core::mem::take(&mut self.line);+                self.input.push_back(line);+            }+            _ if b == t.cc[VEOL] && b != 0 => {+                self.line.push(b);+                let line = core::mem::take(&mut self.line);+                self.input.push_back(line);+            }+            _ if self.buffered() >= INPUT_MAX - 1 => {} // full (room kept for '\n')+            _ => {+                self.line.push(b);+                if echo {+                    if b < 0x20 && b != b'\t' && t.lflag & ECHOCTL != 0 {+                        fx.echo.extend_from_slice(&[b'^', b + 0x40]); // ^X+                    } else {+                        fx.echo.push(b);                     }-                    Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear-                    None => {}                 }             }-            if !chunk.invalid().is_empty() {-                print!("?");-                serial_print!("?");+        }+    }++    /// Copy readable input to `buf`: canonical = at most one line (or an+    /// EOF = 0), raw = whatever is there. None = nothing to read.+    fn take_input(&mut self, buf: &mut [u8], canon: bool) -> Option<usize> {+        let mut n = 0;+        while n < buf.len() {+            let Some(chunk) = self.input.front_mut() else { break };+            if chunk.is_empty() {+                // An end of file: returned on its own (as 0) if first.+                if n == 0 {+                    self.input.pop_front();+                    return Some(0);+                }+                break;+            }+            let k = chunk.len().min(buf.len() - n);+            buf[n..n + k].copy_from_slice(&chunk[..k]);+            chunk.drain(..k);+            n += k;+            if chunk.is_empty() {+                self.input.pop_front();+                if canon {+                    break; // one line per read+                }+            }+        }+        (n > 0).then_some(n)+    }+}++/// Print to the screen and serial port (the console's output). '\x08'+/// erases the character before the cursor and '\x0c' clears the screen.+fn console_output(buf: &[u8]) {+    // One write = one unbroken piece of output, even when programs on+    // other CPUs write at the same time. Taken in thread context only, so+    // while waiting we just yield.+    let _turn = loop {+        match CONSOLE_WRITE.try_lock() {+            Some(guard) => break guard,+            None => crate::thread::yield_now(),+        }+    };+    for chunk in buf.utf8_chunks() {+        for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) {+            let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) {+                Some(text) => (text, piece.chars().last()),+                None => (piece, None),+            };+            print!("{}", text);+            serial_print!("{}", text);+            match control {+                Some('\x08') => {+                    vga_buffer::backspace();+                    serial_print!("\x08");+                }+                Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear+                None => {}             }         }-        Ok(buf.len())+        if !chunk.invalid().is_empty() {+            print!("?");+            serial_print!("?");+        }+    }+}++static CONSOLE_WRITE: Mutex<()> = Mutex::new(());++// --- the registry: console, hvc0, ptys --------------------------------------++static CONSOLE: OnceCell<Arc<Tty>> = OnceCell::uninit();+static HVC: OnceCell<Arc<Tty>> = OnceCell::uninit();+/// Ptys by number; None = free.+static PTYS: Mutex<Vec<Option<Arc<Tty>>>> = Mutex::new(Vec::new());+/// Most ptys at once.+pub const MAX_PTYS: usize = 64;++/// The console tty (keyboard + screen).+pub fn console() -> Arc<Tty> {+    CONSOLE.get_or_init(|| Tty::new(String::from("console"), Driver::Console, 0)).clone()+}++/// The console as a file (fds 0/1/2 of programs started by the kernel).+pub fn console_file() -> Arc<dyn File> {+    Arc::new(TtyFile::new(console(), false))+}++/// /dev/hvc0's tty. Its input is fed by the `hvcd` kernel thread, started+/// the first time hvc0 is opened (until then, tests can read the raw+/// virtio console directly).+pub fn hvc() -> Arc<Tty> {+    HVC.get_or_init(|| {+        crate::thread::spawn("hvcd", hvc_input_thread);+        Tty::new(String::from("hvc0"), Driver::Hvc, 0)+    })+    .clone()+}++fn hvc_input_thread() {+    let mut buf = [0u8; 256];+    loop {+        if let Ok(n) = crate::virtio::console::read(&mut buf) {+            hvc().input(&buf[..n]);+        }+    }+}++fn all_ttys() -> Vec<Arc<Tty>> {+    let mut v: Vec<Arc<Tty>> = without_interrupts(|| PTYS.lock().iter().flatten().cloned().collect());+    v.extend(CONSOLE.get().cloned());+    v.extend(HVC.get().cloned());+    v+}++/// Open /dev/ptmx: a new pty; returns the master end.+pub fn open_ptmx() -> Result<Arc<dyn File>, Errno> {+    let tty = without_interrupts(|| {+        let mut ptys = PTYS.lock();+        let index = match ptys.iter().position(|p| p.is_none()) {+            Some(i) => i,+            None if ptys.len() < MAX_PTYS => {+                ptys.push(None);+                ptys.len() - 1+            }+            None => return Err(Errno::ENOSPC),+        };+        let tty = Tty::new(format!("pts/{}", index), Driver::Pty, index);+        ptys[index] = Some(tty.clone());+        Ok(tty)+    })?;+    Ok(Arc::new(TtyFile::new(tty, true)))+}++/// Pty numbers in use (the /dev/pts listing).+pub fn pty_numbers() -> Vec<usize> {+    without_interrupts(|| PTYS.lock().iter().enumerate().filter(|(_, p)| p.is_some()).map(|(i, _)| i).collect())+}++/// Open /dev/pts/N.+pub fn open_pts(n: usize) -> Result<Arc<dyn File>, Errno> {+    let tty = without_interrupts(|| PTYS.lock().get(n).cloned().flatten()).ok_or(Errno::ENOENT)?;+    let ok = without_interrupts(|| {+        let s = tty.lock();+        s.master_open && !s.locked+    });+    if !ok {+        return Err(Errno::EIO); // locked (not unlockpt'ed yet) or hung up+    }+    Ok(Arc::new(TtyFile::new(tty, false)))+}++/// Open /dev/hvc0 as a terminal.+pub fn open_hvc() -> Arc<dyn File> {+    Arc::new(TtyFile::new(hvc(), false))+}++/// Open /dev/tty: the caller's controlling terminal.+pub fn open_ctty() -> Result<Arc<dyn File>, Errno> {+    let p = crate::thread::current_process().ok_or(Errno::ENXIO)?;+    let tty = p.ctty.lock().clone().ok_or(Errno::ENXIO)?;+    Ok(Arc::new(TtyFile::new(tty, false)))+}++/// How many ptys exist (leak checks).+pub fn pty_count() -> usize {+    pty_numbers().len()+}++// --- sessions and controlling terminals ---------------------------------------++/// Make `tty` the controlling terminal of `p`'s session. Only a session+/// leader without one may, and only if the tty is free (or `steal`).+pub fn attach(p: &Process, tty: Arc<Tty>, steal: bool) -> Result<(), Errno> {+    if p.sid() != p.pid || p.ctty.lock().is_some() {+        return Err(Errno::EPERM);     }+    without_interrupts(|| {+        let mut s = tty.lock();+        if s.session != 0 && s.session != p.sid() && !steal {+            return Err(Errno::EPERM);+        }+        s.session = p.sid();+        s.pgrp = p.pgid();+        Ok(())+    })?;+    *p.ctty.lock() = Some(tty);+    Ok(())+} +/// open() of a terminal without O_NOCTTY: a session leader without a+/// controlling terminal gets this one (System V style, what Linux does).+pub fn opened(p: &Process, file: &dyn File) {+    if let Some(tf) = file.as_tty() {+        if !tf.master && p.sid() == p.pid && p.ctty.lock().is_none() && tf.tty.session() == 0 {+            let _ = attach(p, tf.tty.clone(), false);+        }+    }+}++/// The session leader let go of `tty`: no session owns it any more, and+/// its foreground job is told with SIGHUP (+ SIGCONT in case it's stopped).+fn hang_up(tty: &Tty) {+    let pgrp = without_interrupts(|| {+        let mut s = tty.lock();+        s.session = 0;+        core::mem::replace(&mut s.pgrp, 0)+    });+    if pgrp != 0 {+        crate::process::signal_group(pgrp, signal::SIGHUP);+        crate::process::signal_group(pgrp, signal::SIGCONT);+    }+}++/// A process ended: undo its TTY_SET_RAW, and if it led the session of+/// its controlling terminal, hang that up.+pub fn process_exited(p: &Process) {+    for tty in all_ttys() {+        let restored = without_interrupts(|| {+            let mut s = tty.lock();+            match s.raw_owner {+                Some((pid, old)) if pid == p.pid => {+                    s.raw_owner = None;+                    s.set_termios(old, false);+                    true+                }+                _ => false,+            }+        });+        if restored {+            tty.readers.wake_all();+        }+    }+    if let Some(tty) = p.ctty.lock().take() {+        if tty.session() == p.pid {+            hang_up(&tty);+        }+    }+}++// --- the old console API (keyboard driver, tests) ---------------------------++pub fn foreground() -> u64 {+    console().foreground()+}++pub fn set_foreground(pgid: u64) {+    console().set_foreground(pgid);+}++/// Called by the keyboard task for every typed character.+pub fn input_char(c: char) {+    let mut utf8 = [0; 4];+    console().input(c.encode_utf8(&mut utf8).as_bytes());+}++/// Keys that aren't characters: the ANSI/VT100 sequences a Linux terminal+/// sends for them.+pub fn input_key(key: KeyCode) {+    let seq: &[u8] = match key {+        KeyCode::ArrowUp => b"\x1b[A",+        KeyCode::ArrowDown => b"\x1b[B",+        KeyCode::ArrowRight => b"\x1b[C",+        KeyCode::ArrowLeft => b"\x1b[D",+        KeyCode::Home => b"\x1b[H",+        KeyCode::End => b"\x1b[F",+        KeyCode::Delete => b"\x1b[3~",+        _ => return,+    };+    console().input(seq);+}++// --- a terminal as a file -----------------------------------------------------++/// An open terminal: the console, hvc0, a pty slave -- or a pty master.+pub struct TtyFile {+    pub tty: Arc<Tty>,+    pub master: bool,+    nonblocking: AtomicBool,+}++impl TtyFile {+    fn new(tty: Arc<Tty>, master: bool) -> TtyFile {+        without_interrupts(|| {+            let mut s = tty.lock();+            if master {+                s.master_open = true;+            } else if tty.driver == Driver::Pty {+                s.slaves_open += 1;+                s.slave_opened = true;+            }+        });+        if tty.driver == Driver::Hvc && !master {+            crate::virtio::console::OPEN_COUNT.fetch_add(1, Ordering::Relaxed);+        }+        TtyFile { tty, master, nonblocking: AtomicBool::new(false) }+    }+}++impl File for TtyFile {+    fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+        if self.master {+            self.tty.master_read(buf, self.nonblocking())+        } else {+            self.tty.read(buf, self.nonblocking())+        }+    }+    fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+        if self.master {+            self.tty.input(buf); // "typed" into the slave+            Ok(buf.len())+        } else {+            self.tty.output(buf, !self.nonblocking())+        }+    }     fn stat(&self) -> Result<Stat, Errno> {-        Ok(Stat { kind: KIND_CONSOLE, size: 0, mode: 0 })+        // Terminals look like the console (that's what isatty checks).+        Ok(Stat { kind: if self.master { KIND_DEVICE } else { KIND_CONSOLE }, size: 0, mode: 0o620 })+    }+    fn poll(&self) -> u16 {+        if self.master { self.tty.poll_master() } else { self.tty.poll_slave() }+    }+    fn set_nonblocking(&self, on: bool) {+        self.nonblocking.store(on, Ordering::Relaxed);+    }+    fn nonblocking(&self) -> bool {+        self.nonblocking.load(Ordering::Relaxed)+    }+    fn ioctl(&self, cmd: u64, arg: u64) -> Result<u64, Errno> {+        self.tty.ioctl(self, cmd, arg)+    }+    fn as_tty(&self) -> Option<&TtyFile> {+        Some(self)+    }+}++impl Drop for TtyFile {+    fn drop(&mut self) {+        let tty = &self.tty;+        if tty.driver == Driver::Hvc && !self.master {+            crate::virtio::console::OPEN_COUNT.fetch_sub(1, Ordering::Relaxed);+        }+        if tty.driver != Driver::Pty {+            return;+        }+        let master_gone = without_interrupts(|| {+            let mut s = tty.lock();+            if self.master {+                s.master_open = false;+            } else {+                s.slaves_open -= 1;+            }+            self.master+        });+        if master_gone {+            // The terminal "hung up": its session hears SIGHUP, readers+            // get EOF, writers EIO; the number is free again.+            hang_up(tty);+            without_interrupts(|| PTYS.lock()[tty.index] = None);+        }+        tty.readers.wake_all();+        tty.writers.wake_all();+        poll::notify();     } }
@@ -0,0 +1,111 @@+//! Stage 15, the plumbing a C library needs: threads and futexes, poll and+//! epoll, nonblocking I/O, loopback networking, ttys and ptys. As in+//! stage 14 the detailed checks live in user programs (threads,+//! echoserver, ptytest, nbtest: exit code = failed checks); this file runs+//! them and adds what only the kernel can see: nothing is left behind+//! (threads, processes, futex waiters, ptys, sockets, memory frames).+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, futex, memory, net, thread, tty};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rustbelt::init();+    rustbelt::init_memory(boot_info);+    rustbelt::init_smp(); // all 4 CPUs run threads+    fs::init();+    rustbelt::init_storage();+    rustbelt::init_net(); // brings up 127.0.0.1 (and eth0 if QEMU has one)+    test_main();+    rustbelt::hlt_loop();+}++fn run(name: &str, args: &[&str]) -> ExitStatus {+    let mut spawn = SpawnArgs::kernel(name);+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+    process::wait(&p)+}++/// Snapshot of everything a buggy program could leak.+#[derive(Debug, PartialEq)]+struct Counts {+    threads: usize,+    processes: usize,+    futex_waiters: usize,+    ptys: usize,+    sockets: usize,+    frames: usize,+}++fn counts() -> Counts {+    thread::sleep_ticks(3); // let exited threads' stacks be freed+    Counts {+        threads: thread::thread_count(),+        processes: process::count(),+        futex_waiters: futex::waiters(),+        ptys: tty::pty_count(),+        sockets: net::socket_count(),+        frames: memory::frames_in_use(),+    }+}++/// Run a test program twice: once to warm caches, then again to check that+/// it passes and that every count comes back to where it was.+fn run_without_leaks(name: &str) {+    assert_eq!(run(name, &[]), ExitStatus::Exited(0), "{} failed (warm-up)", name);+    let before = counts();+    assert_eq!(run(name, &[]), ExitStatus::Exited(0), "{} failed", name);+    let after = counts();+    assert_eq!(after, before, "{} leaked something", name);+    assert_eq!(after.futex_waiters, 0, "futex waiters left behind");+    assert_eq!(after.ptys, 0, "ptys left behind");+}++/// 4 threads sum 1..=40,000,000 under a futex mutex, on all 4 CPUs.+#[test_case]+fn threads_and_futexes() {+    run_without_leaks("threads");+}++/// One epoll and one poll server over 127.0.0.1, three forked clients each.+#[test_case]+fn poll_epoll_echo_over_loopback() {+    run_without_leaks("echoserver");+}++/// pty round trips, canonical vs raw, ^C -> SIGINT, hangup on close.+#[test_case]+fn ptys_and_line_discipline() {+    run_without_leaks("ptytest");+}++/// EAGAIN everywhere, fcntl/FIONBIO, writev/pread/pwrite, clocks.+#[test_case]+fn nonblocking_io_and_libc_extras() {+    run_without_leaks("nbtest");+}++/// Loopback works without DHCP and without the NIC: 127.0.0.1 is local+/// whatever eth0 is doing.+#[test_case]+fn loopback_is_always_there() {+    assert!(net::is_loopback([127, 0, 0, 1]));+    assert!(!net::is_loopback([10, 0, 2, 15]));+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -35,7 +35,8 @@ fn main(boot_info: &'static BootInfo) -> ! {     rustbelt::init_smp();     fs::init();     rustbelt::init_storage();-    rustbelt::init_net().expect("no virtio-net card: test-args need -device virtio-net-pci");+    rustbelt::init_net();+    assert!(net::present(), "no virtio-net card: test-args need -device virtio-net-pci");     test_main();     rustbelt::hlt_loop(); }
@@ -176,7 +176,7 @@ fn scripted_shell_session() {         "piped text\n",         "wc -l\n2\n",         "saved\nmore\n",-        "Rustbelt 0.14.0",+        "Rustbelt 0.15.0",         "B:/ $ exit 3\n",     ] {         assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);
@@ -150,7 +150,7 @@ fn dev_lists_the_devices() {     let dev = fs::lookup("/dev").expect("no /dev");     let mut names: Vec<String> = dev.list().expect("list /dev").into_iter().map(|e| e.name).collect();     names.sort();-    assert_eq!(names, ["fb0", "hvc0", "null", "random", "urandom"]);+    assert_eq!(names, ["console", "fb0", "hvc0", "null", "ptmx", "pts", "random", "tty", "urandom"]);     assert_eq!(fs::lookup("/dev/fb0").unwrap().size(), 1024 * 768 * 4); } 
@@ -0,0 +1,206 @@+//! echoserver -- an event-loop echo server over loopback (127.0.0.1).+//!+//! One process serves several clients at once without threads: it never+//! blocks on any single socket. Every socket is nonblocking, and the loop+//! asks the kernel "which of my sockets are ready?" (epoll_wait, or plain+//! poll), then only accepts/reads/writes those:+//!+//!     loop {+//!         ready = epoll_wait(...)        // sleep until something happens+//!         for each ready fd:+//!             listener -> accept new clients, watch them too+//!             client   -> read; echo it back; 0 bytes = client left+//!     }+//!+//! The self-test forks 3 client processes that talk to it over loopback,+//! once with epoll and once with poll, then checks UDP over loopback.+//! With an argument (`echoserver PORT`) it just serves forever.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use alloc::vec::Vec;+use ulib::*;++const LOOPBACK: Ip = Ip([127, 0, 0, 1]);+const CLIENTS: usize = 3;+const ROUNDS: usize = 3;++/// A client process: connect, send ROUNDS messages, check each echo.+fn client(port: u16, id: usize) -> i64 {+    let fd = socket(SOCK_STREAM, 0);+    if connect(fd, LOOPBACK, port) < 0 {+        return 10;+    }+    for round in 0..ROUNDS {+        let msg = format!("hello {} from client {}", round, id);+        write_all(fd, msg.as_bytes());+        let mut buf = [0u8; 64];+        let mut got = 0;+        while got < msg.len() {+            let n = read(fd, &mut buf[got..]);+            if n <= 0 {+                return 11;+            }+            got += n as usize;+        }+        if &buf[..got] != msg.as_bytes() {+            return 12;+        }+    }+    close(fd); // the server sees end-of-file+    0+}++fn listener(port: u16) -> i64 {+    let fd = socket(SOCK_STREAM | O_NONBLOCK, 0); // SOCK_NONBLOCK+    if bind(fd, port) < 0 || listen(fd, 4) < 0 {+        return -1;+    }+    fd+}++/// Accept everyone waiting (nonblocking: stop at EAGAIN).+fn accept_all(lfd: i64, mut add: impl FnMut(i64)) {+    loop {+        let (fd, _) = accept(lfd);+        if fd < 0 {+            break; // -EAGAIN: nobody else waiting+        }+        set_nonblocking(fd, true);+        add(fd);+    }+}++/// Read what's there and echo it back. Returns false when the client left.+fn echo(fd: i64, served: &mut usize) -> bool {+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n == -EAGAIN {+            return true; // drained for now+        }+        if n <= 0 {+            return false; // 0 = end of file (or an error)+        }+        write_all(fd, &buf[..n as usize]);+        *served += 1;+    }+}++/// The epoll flavour: register fds once, then wait for just the ready ones.+/// Returns the number of clients served to the end (or -1 on a stall).+fn serve_epoll(lfd: i64, want: usize) -> i64 {+    let ep = epoll_create1(0);+    epoll_ctl(ep, EPOLL_CTL_ADD, lfd, EPOLLIN, lfd as u64);+    let (mut finished, mut served) = (0, 0);+    let mut events = [EpollEvent::default(); 8];+    while finished < want {+        let n = epoll_wait(ep, &mut events, 5000);+        if n <= 0 {+            close(ep);+            return -1; // 5 s with nothing happening: something's wrong+        }+        for ev in &events[..n as usize] {+            let fd = ev.data as i64;+            if fd == lfd {+                accept_all(lfd, |c| {+                    epoll_ctl(ep, EPOLL_CTL_ADD, c, EPOLLIN, c as u64);+                });+            } else if !echo(fd, &mut served) {+                epoll_ctl(ep, EPOLL_CTL_DEL, fd, 0, 0);+                close(fd);+                finished += 1;+            }+        }+    }+    close(ep);+    println!("echoserver: epoll: {} clients, {} echoes", finished, served);+    finished as i64+}++/// The poll flavour: pass the whole fd list on every call.+fn serve_poll(lfd: i64, want: usize) -> i64 {+    let mut fds = alloc::vec![PollFd { fd: lfd as i32, events: POLLIN, revents: 0 }];+    let (mut finished, mut served) = (0, 0);+    while finished < want {+        if poll(&mut fds, 5000) <= 0 {+            return -1;+        }+        let mut new = Vec::new();+        let mut gone = Vec::new();+        for p in fds.iter() {+            if p.revents == 0 {+                continue;+            }+            if p.fd as i64 == lfd {+                accept_all(lfd, |c| new.push(c));+            } else if !echo(p.fd as i64, &mut served) {+                close(p.fd as i64);+                gone.push(p.fd);+                finished += 1;+            }+        }+        fds.retain(|p| !gone.contains(&p.fd));+        fds.extend(new.into_iter().map(|c| PollFd { fd: c as i32, events: POLLIN, revents: 0 }));+    }+    println!("echoserver: poll: {} clients, {} echoes", finished, served);+    finished as i64+}++/// Start the clients, run the server, collect the clients' results.+fn round(c: &mut Checks, port: u16, use_epoll: bool) {+    let lfd = listener(port);+    c.check(lfd >= 0, "listen on a loopback port");+    let mut pids = Vec::new();+    for id in 0..CLIENTS {+        let pid = fork();+        if pid == 0 {+            close(lfd);+            exit(client(port, id));+        }+        pids.push(pid);+    }+    let finished = if use_epoll { serve_epoll(lfd, CLIENTS) } else { serve_poll(lfd, CLIENTS) };+    close(lfd);+    let ok = pids.iter().all(|&pid| wait(pid) == 0);+    let what = if use_epoll { "epoll" } else { "poll" };+    c.check(finished == CLIENTS as i64, &format!("{}: served {} clients to the end", what, CLIENTS));+    c.check(ok, &format!("{}: every client got its echoes back", what));+}++fn main() -> i64 {+    if let Some(port) = arg(1).and_then(|a| a.parse().ok()) {+        let lfd = listener(port);+        if lfd < 0 {+            eprintln!("echoserver: can't listen on port {}", port);+            return 1;+        }+        println!("echoserver: listening on port {} (epoll)", port);+        loop {+            serve_epoll(lfd, usize::MAX);+        }+    }+    let mut c = Checks::new("echoserver");+    round(&mut c, 7070, true);+    round(&mut c, 7071, false);++    // UDP over loopback: a datagram to ourselves.+    let a = socket(SOCK_DGRAM, 0);+    let b = socket(SOCK_DGRAM, 0);+    bind(b, 7072);+    c.check(sendto(a, b"ping", LOOPBACK, 7072) == 4, "UDP sendto 127.0.0.1");+    set_recv_timeout(b, 2000);+    let mut buf = [0u8; 16];+    let (n, from) = recvfrom(b, &mut buf);+    c.check(n == 4 && &buf[..4] == b"ping" && from.ip() == LOOPBACK, "UDP datagram arrives, from 127.0.0.1");+    close(a);+    close(b);+    c.done()+}
@@ -1,14 +1,22 @@ //! getty [-1] DEVICE -- run a shell on another terminal, e.g. /dev/hvc0. //! //! Real Unix systems run one "getty" per terminal line: it opens the line-//! and starts a login shell on it. Ours skips the login: it opens DEVICE-//! and spawns `sh` with stdin, stdout and stderr all on it. When the shell-//! exits (`exit`) a new one starts, unless `-1` was given.+//! and starts a login shell on it. Ours skips the login. Since stage 15 it+//! does it the classic Unix way, with fork + exec:+//!+//!   1. fork: the child will become the shell.+//!   2. setsid (in the child): a new *session* with no terminal yet.+//!   3. open DEVICE: a session leader without a terminal that opens one+//!      gets it as its *controlling terminal*. That's what lets ^C on+//!      that terminal send SIGINT to the right programs.+//!   4. dup2 it onto fds 0, 1 and 2 (stdin, stdout, stderr).+//!   5. execve the shell: it keeps the fds and the session.+//!+//! getty waits for the shell; when it exits (`exit`) a new one starts,+//! unless `-1` was given. //! //! Try it: `getty /dev/hvc0 &` here, then on the host-//! `socat - UNIX-CONNECT:target/hvc0.sock` and type commands. That shell-//! is not "interactive" (no line editing): it reads one line at a time and-//! prints each line after the prompt, like a script.+//! `socat - UNIX-CONNECT:target/hvc0.sock` and type commands. #![no_std] #![no_main] @@ -20,6 +28,27 @@ use ulib::*;  const SHELL: &str = "/mnt/disk0/bin/sh"; +/// The child's side (steps 2-5). Only returns on failure.+fn become_shell(dev: &str) -> i64 {+    setsid();+    let fd = open(dev, O_RDWR); // becomes our controlling terminal+    if fd < 0 {+        eprintln!("getty: {}: {}", dev, strerror(fd));+        return 126;+    }+    for target in 0..3 {+        dup2(fd, target);+    }+    if fd > 2 {+        close(fd);+    }+    let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";+    write(1, greeting.as_bytes());+    let err = execve(SHELL, &["sh"], &environ());+    eprintln!("getty: {}: {}", SHELL, strerror(err));+    127+}+ fn main() -> i64 {     let (once, dev) = match (arg(1), arg(2)) {         (Some("-1"), Some(dev)) => (true, dev),@@ -30,24 +59,17 @@ fn main() -> i64 {         }     };     loop {-        let fd = open(dev, 0);-        if fd < 0 {-            eprintln!("getty: {}: {}", dev, strerror(fd));-            return 1;+        let pid = fork();+        if pid == 0 {+            exit(become_shell(dev));         }-        let greeting = "\nRustbelt getty: shell on this terminal (type `exit` to end it)\n";-        write(fd, greeting.as_bytes());-        let opts = SpawnOptions { stdin: fd, stdout: fd, stderr: fd, flags: 0 };-        let pid = spawn(SHELL, "sh", &opts);         if pid < 0 {-            eprintln!("getty: {}: {}", SHELL, strerror(pid));-            close(fd);+            eprintln!("getty: fork: {}", strerror(pid));             return 1;         }         let status = wait(pid);-        close(fd);-        if once {-            return status;+        if once || status == 126 || status == 127 {+            return status; // done, or the terminal/shell can't be opened         }         sleep_ms(500); // don't spin if the shell keeps dying at once     }
@@ -0,0 +1,117 @@+//! nbtest -- checks O_NONBLOCK: "would block" becomes EAGAIN instead.+//!+//! A blocking read on an empty pipe sleeps until data comes. With+//! O_NONBLOCK (set with fcntl(F_SETFL), ioctl(FIONBIO), pipe2 or+//! SOCK_NONBLOCK) it returns -EAGAIN at once ("try again later"), which+//! is what an event loop wants: it only reads when poll said "ready", and+//! must never get stuck on one fd.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use ulib::*;++const FIONBIO: u64 = 0x5421;++fn main() -> i64 {+    let mut c = Checks::new("nbtest");+    let mut buf = [0u8; 4096];++    // Pipes.+    let (r, w) = pipe2(O_NONBLOCK).unwrap();+    c.check(read(r, &mut buf) == -EAGAIN, "pipe: read with nothing there: EAGAIN");+    let mut p = [PollFd { fd: r as i32, events: POLLIN, revents: 0 }];+    c.check(poll(&mut p, 0) == 0, "pipe: poll says not readable");+    let mut total = 0;+    loop {+        let n = write(w, &buf);+        if n < 0 {+            c.check(n == -EAGAIN, "pipe: write to a full pipe: EAGAIN");+            break;+        }+        total += n;+    }+    println!("nbtest: the pipe took {} bytes before it was full", total);+    c.check(total == 65536, "pipe: holds 64 KiB, like Linux");+    let mut p = [PollFd { fd: w as i32, events: POLLOUT, revents: 0 }];+    c.check(poll(&mut p, 0) == 0, "pipe: poll says a full pipe isn't writable");+    c.check(read(r, &mut buf) == 4096, "pipe: then reading works");+    c.check(poll(&mut p, 0) == 1 && p[0].revents & POLLOUT != 0, "pipe: room again -> POLLOUT");+    close(w);+    while read(r, &mut buf) > 0 {}+    c.check(read(r, &mut buf) == 0, "pipe: writer gone -> end of file, not EAGAIN");+    close(r);++    // fcntl and FIONBIO switch it on and off.+    let (r, w) = pipe().unwrap();+    c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK == 0, "fcntl F_GETFL: blocking by default");+    set_nonblocking(r, true);+    c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK != 0, "fcntl F_SETFL O_NONBLOCK sticks");+    c.check(read(r, &mut buf) == -EAGAIN, "fcntl-made nonblocking pipe: EAGAIN");+    let off = 0i32;+    ioctl(r, FIONBIO, &off as *const i32 as u64);+    c.check(fcntl(r, F_GETFL, 0) as u64 & O_NONBLOCK == 0, "ioctl FIONBIO 0 turns it off");+    close(r);+    close(w);++    // Sockets (over loopback).+    let l = socket(SOCK_STREAM, 0);+    bind(l, 7080);+    listen(l, 2);+    set_nonblocking(l, true);+    c.check(accept(l).0 == -EAGAIN, "socket: accept with nobody calling: EAGAIN");+    let s = socket(SOCK_STREAM | O_NONBLOCK, 0);+    let r = connect(s, Ip([127, 0, 0, 1]), 7080);+    c.check(r == 0 || r == -EINPROGRESS, "socket: nonblocking connect: EINPROGRESS (or done at once)");+    let mut p = [PollFd { fd: s as i32, events: POLLOUT, revents: 0 }];+    c.check(poll(&mut p, 2000) == 1 && p[0].revents & POLLOUT != 0, "socket: poll POLLOUT = connected");+    let mut p = [PollFd { fd: l as i32, events: POLLIN, revents: 0 }];+    c.check(poll(&mut p, 2000) == 1, "socket: listener POLLIN = a connection to accept");+    let (a, _) = accept(l);+    c.check(a >= 0, "socket: accept it");+    c.check(read(s, &mut buf) == -EAGAIN, "socket: recv with no data: EAGAIN");+    write(a, b"hi");+    let mut p = [PollFd { fd: s as i32, events: POLLIN, revents: 0 }];+    poll(&mut p, 2000);+    c.check(read(s, &mut buf) == 2, "socket: data after POLLIN");+    close(a);+    close(s);+    close(l);++    // A terminal: the slave side of a pty.+    if let Ok((m, n)) = openpty() {+        let slave = open(&alloc::format!("/dev/pts/{}", n), O_RDWR | O_NOCTTY | O_NONBLOCK);+        c.check(read(slave, &mut buf) == -EAGAIN, "tty: read with no line typed: EAGAIN");+        close(slave);+        close(m);+    } else {+        c.check(false, "openpty");+    }++    // Small extras libc needs: pread/pwrite, writev, clocks, nanosleep.+    let fd = open("/tmp/nbtest.txt", O_CREAT | O_TRUNC | O_RDWR);+    writev(fd, &[b"hello ", b"vectored ", b"world"]);+    pwrite(fd, b"HELLO", 0);+    let n = pread(fd, &mut buf, 6);+    c.check(&buf[..n.max(0) as usize] == b"vectored world", "writev + pread at an offset");+    c.check(lseek(fd, 0, SEEK_CUR) == 20, "pread/pwrite leave the file position alone");+    lseek(fd, 0, SEEK_SET);+    let n = read(fd, &mut buf);+    c.check(&buf[..n.max(0) as usize] == b"HELLO vectored world", "pwrite overwrote the start");+    close(fd);+    unlink("/tmp/nbtest.txt");+    let (secs, _) = clock_gettime(0);+    c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");+    let t0 = now_us();+    nanosleep_us(30_000);+    c.check(now_us() - t0 >= 25_000, "nanosleep 30 ms");+    let mut link = [0u8; 64];+    let n = readlink("/proc/self/exe", &mut link);+    c.check(n > 0 && link[..n as usize].ends_with(b"nbtest"), "readlink /proc/self/exe = this program");+    c.done()+}
@@ -0,0 +1,106 @@+//! ptytest -- checks pseudo-terminals (ptys) and the line discipline.+//!+//! A pty is a pair: the *master* side is held by a terminal emulator (or+//! ssh, tmux, a compositor's terminal window), the *slave* side+//! (/dev/pts/N) looks exactly like a real terminal to the program running+//! on it. Between them sits the line discipline: in *canonical* mode it+//! collects a whole line (with backspace editing) before the program can+//! read it, echoes what's typed, turns ^C into SIGINT; in *raw* mode bytes+//! pass straight through (what editors and games use).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::format;+use ulib::*;++/// Read whatever the other side has (waits up to 1 s for the first byte).+fn read_some(fd: i64, buf: &mut [u8]) -> usize {+    let mut p = [PollFd { fd: fd as i32, events: POLLIN, revents: 0 }];+    if poll(&mut p, 1000) <= 0 {+        return 0;+    }+    read(fd, buf).max(0) as usize+}++fn main() -> i64 {+    let mut c = Checks::new("ptytest");+    let (master, n) = match openpty() {+        Ok(x) => x,+        Err(e) => {+            println!("ptytest: openpty: {}", strerror(e));+            return 1;+        }+    };+    let path = format!("/dev/pts/{}", n);+    let slave = open(&path, O_RDWR | O_NOCTTY);+    c.check(slave >= 0, &format!("open {} (the slave side)", path));+    let mut buf = [0u8; 64];++    // Canonical mode: a line is readable only once it's complete.+    write(master, b"hel");+    set_nonblocking(slave, true);+    c.check(read(slave, &mut buf) == -EAGAIN, "canonical: half a line can't be read yet (EAGAIN)");+    write(master, b"lo\x7f\x7fp me\r"); // two backspaces, then Enter ('\r')+    let got = read_some(slave, &mut buf);+    c.check(&buf[..got] == b"help me\n", "canonical: backspace edits the line, Enter ('\\r') ends it as '\\n'");+    let got = read_some(master, &mut buf);+    c.check(got > 0 && buf[..got].ends_with(b"\r\n"), "echo: what's typed comes back to the master");++    // Output processing: the program's '\n' reaches the terminal as "\r\n".+    write(slave, b"out\n");+    let got = read_some(master, &mut buf);+    c.check(&buf[..got] == b"out\r\n", "output: '\\n' becomes \"\\r\\n\" (ONLCR)");++    // Raw mode: every byte at once, no echo, no conversions.+    let saved = tcgetattr(slave).unwrap_or_default();+    let mut raw = saved;+    cfmakeraw(&mut raw);+    c.check(tcsetattr(slave, &raw) == 0, "TCSETS: switch to raw mode");+    write(master, b"x\r\x03");+    let got = read_some(slave, &mut buf);+    c.check(&buf[..got] == b"x\r\x03", "raw: bytes arrive at once, unchanged (even ^C)");+    set_nonblocking(master, true);+    c.check(read(master, &mut buf) == -EAGAIN, "raw: no echo");+    set_nonblocking(master, false);+    tcsetattr(slave, &saved);+    c.check(tcgetattr(slave).map(|t| t.lflag & ICANON != 0).unwrap_or(false), "TCGETS: canonical mode back on");+    // (We keep `slave` open: with no slave open at all, the master reports+    // a hang-up (POLLHUP) instead of waiting for the child to open it.)++    // ^C: a child makes the pty its controlling terminal (setsid + open)+    // and waits; typing ^C on the master must kill it with SIGINT.+    let pid = fork();+    if pid == 0 {+        setsid();+        let fd = open(&path, O_RDWR); // session leader: becomes our ctty+        if fd < 0 {+            exit(3);+        }+        write(fd, b"ready\n");+        loop {+            pause();+        }+    }+    let got = read_some(master, &mut buf); // "ready\r\n"+    c.check(&buf[..got] == b"ready\r\n", "child opened the pty as its controlling terminal");+    write(master, b"\x03");+    let status = match wait4(pid, 0) {+        Ok((_, s)) => s,+        Err(_) => 0,+    };+    c.check(wifsignaled(status) && wtermsig(status) == SIGINT, "^C on the master: SIGINT for the foreground job");+    let got = read_some(master, &mut buf);+    c.check(got >= 2 && &buf[..2] == b"^C", "^C is echoed as \"^C\"");++    // Hang-up: closing the master gives the slave end-of-file.+    close(master);+    c.check(read(slave, &mut buf) == 0, "master closed: the slave reads end-of-file");+    close(slave);+    c.done()+}
@@ -212,7 +212,10 @@ impl Shell {         let mut line: Vec<u8> = Vec::new();         let mut hist = self.history.len(); // position while browsing history         let mut esc = 0; // 0: normal, 1: after ESC, 2: after ESC [-        let mut buf = [0u8; 16];+        // One byte per read: if several lines were typed (or pasted) ahead,+        // the rest stays in the tty for the next prompt instead of being+        // lost when we stop at the first Enter.+        let mut buf = [0u8; 1];         'read: loop {             let n = read(0, &mut buf);             if n <= 0 {
@@ -0,0 +1,135 @@+//! threads [N] -- several threads in one process, pthread-style.+//!+//! Threads share *everything* a process owns: memory, open files, signal+//! handlers. Each has only its own registers, stack and TLS block (and a+//! thread id). ulib's `spawn_thread` = pthread_create: clone with+//! CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | ..., a fresh+//! stack, CLONE_SETTLS for the TLS block, and CLONE_CHILD_CLEARTID so+//! `join` can sleep on a futex until the kernel says "thread gone".+//!+//! The demo: a parallel sum. 1..=COUNT is split into N slices (default 4,+//! one per CPU); each thread adds up its slice, then adds its result to a+//! shared total under a futex mutex. Many small increments of a shared+//! counter (also under the mutex) check the mutex really excludes; a+//! counter bumped *without* it shows why that matters.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::sync::Arc;+use alloc::vec::Vec;+use core::cell::UnsafeCell;+use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};+use ulib::*;++const COUNT: u64 = 40_000_000;+const BUMPS: u64 = 20_000;++/// Data guarded by a FutexMutex (a tiny `Mutex<T>`).+struct Shared {+    lock: FutexMutex,+    total: UnsafeCell<u64>,+    bumps: UnsafeCell<u64>,+    /// Bumped with a plain read + write, no lock: races lose updates.+    racy: UnsafeCell<u64>,+}+unsafe impl Sync for Shared {}+unsafe impl Send for Shared {}++fn main() -> i64 {+    let n: u64 = arg(1).and_then(|a| a.parse().ok()).unwrap_or(4).clamp(1, 16);+    let mut c = Checks::new("threads");+    let shared = Arc::new(Shared {+        lock: FutexMutex::new(),+        total: UnsafeCell::new(0),+        bumps: UnsafeCell::new(0),+        racy: UnsafeCell::new(0),+    });+    let cpus_seen = Arc::new(AtomicU64::new(0)); // bit i = some thread ran on CPU i+    let tids = Arc::new(AtomicU64::new(0)); // sum of the threads' tids+    let tls_ok = Arc::new(AtomicU32::new(0));+    let start = now_us();+    let mut threads = Vec::new();+    for i in 0..n {+        let (shared, cpus_seen, tids, tls_ok) = (shared.clone(), cpus_seen.clone(), tids.clone(), tls_ok.clone());+        let from = COUNT * i / n + 1;+        let to = COUNT * (i + 1) / n;+        // Each thread gets `i` as its TLS value.+        let t = spawn_thread(i, move || {+            let mut sum = 0u64;+            for x in from..=to {+                sum = sum.wrapping_add(core::hint::black_box(x));+            }+            cpus_seen.fetch_or(1 << sysinfo().this_cpu, Ordering::Relaxed);+            tids.fetch_add(gettid() as u64, Ordering::Relaxed);+            if tls_value() == i {+                tls_ok.fetch_add(1, Ordering::Relaxed);+            }+            for _ in 0..BUMPS {+                shared.lock.lock();+                unsafe { *shared.bumps.get() += 1 };+                shared.lock.unlock();+                unsafe {+                    let v = core::ptr::read_volatile(shared.racy.get());+                    core::ptr::write_volatile(shared.racy.get(), v + 1);+                }+            }+            shared.lock.lock();+            unsafe { *shared.total.get() += sum };+            shared.lock.unlock();+        });+        match t {+            Ok(t) => threads.push(t),+            Err(e) => {+                println!("threads: spawn_thread: {}", strerror(e));+                return 1;+            }+        }+    }+    let spawned_tids: u64 = threads.iter().map(|t| t.tid as u64).sum();+    for t in threads {+        t.join();+    }+    let ms = (now_us() - start) / 1000;+    let total = unsafe { *shared.total.get() };+    let bumps = unsafe { *shared.bumps.get() };+    let racy = unsafe { *shared.racy.get() };+    let cpus = cpus_seen.load(Ordering::Relaxed).count_ones();+    println!("threads: {} threads summed 1..={} = {} in {} ms on {} CPU(s)", n, COUNT, total, ms, cpus);+    println!("threads: locked counter {} (want {}), unlocked counter {} (races lose updates)", bumps, n * BUMPS, racy);+    c.check(total == COUNT * (COUNT + 1) / 2, "parallel sum is right");+    c.check(bumps == n * BUMPS, "the futex mutex never let two threads in at once");+    c.check(tls_ok.load(Ordering::Relaxed) == n as u32, "every thread saw its own TLS value (fs:[8])");+    c.check(tids.load(Ordering::Relaxed) == spawned_tids, "gettid in each thread = the tid clone returned");+    c.check(gettid() == getpid(), "the main thread's tid is the pid");++    // A futex wait whose value doesn't match returns at once (EAGAIN); one+    // that matches sleeps until its timeout.+    let word = AtomicU32::new(7);+    c.check(futex_wait(&word, 8, None) == -EAGAIN, "futex_wait with a stale value: EAGAIN");+    let t0 = now_us();+    let r = futex_wait(&word, 7, Some(50_000));+    c.check(r == -ETIMEDOUT && now_us() - t0 >= 40_000, "futex_wait times out after 50 ms");+    c.check(futex_wake(&word, 1) == 0, "futex_wake with nobody waiting wakes 0");++    // exit vs exit_group: a thread that calls exit (60) ends only itself.+    let alive = Arc::new(AtomicU32::new(0));+    let a2 = alive.clone();+    let t = spawn_thread(0, move || {+        a2.store(1, Ordering::SeqCst);+        // falls off the end: ulib calls exit(0) = just this thread+    })+    .unwrap();+    t.join();+    c.check(alive.load(Ordering::SeqCst) == 1, "a thread's exit ends only that thread");++    // tgkill: a signal for one particular thread (here: ourselves).+    c.check(tgkill(getpid(), gettid(), 0) == 0, "tgkill(pid, tid, 0) finds the thread");+    c.check(tgkill(getpid(), 99999, 0) == -ESRCH, "tgkill to a missing thread: ESRCH");+    c.done()+}
@@ -29,7 +29,7 @@ pub const SYS_LSEEK: u64 = 8; pub const SYS_IOCTL: u64 = 16; pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24;-pub const SYS_SLEEP_MS: u64 = 35;+pub const SYS_NANOSLEEP: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60; pub const SYS_WAIT4: u64 = 61;@@ -227,7 +227,12 @@ pub fn sched_yield() {     syscall3(SYS_SCHED_YIELD, 0, 0, 0); } pub fn sleep_ms(ms: u64) {-    syscall3(SYS_SLEEP_MS, ms, 0, 0);+    nanosleep_us(ms * 1000);+}+/// nanosleep(*timespec, *remaining): 0, or -EINTR if a signal woke us.+pub fn nanosleep_us(us: u64) -> i64 {+    let ts = [us / 1_000_000, (us % 1_000_000) * 1000];+    syscall3(SYS_NANOSLEEP, ts.as_ptr() as u64, 0, 0) } pub fn getpid() -> i64 {     syscall3(SYS_GETPID, 0, 0, 0)@@ -590,9 +595,11 @@ pub fn sysinfo() -> SysInfo {     syscall3(SYS_SYSINFO, &mut info as *mut SysInfo as u64, 0, 0);     info }-/// Current directory into `buf`; returns its length (or -errno).+/// Current directory into `buf`; returns its length (or -errno). The+/// kernel, like Linux, also writes a NUL and counts it; we don't. pub fn getcwd(buf: &mut [u8]) -> i64 {-    syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+    let n = syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0);+    if n > 0 { n - 1 } else { n } } pub fn chdir(path: &str) -> i64 {     syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)@@ -1068,8 +1075,9 @@ fn panic(info: &core::panic::PanicInfo) -> ! { // up gives us more (zeroed, demand-paged) memory. // Same idea as the kernel's fixed-size-block allocator: sizes are rounded up // to a power of two (16 B .. 1 MiB) and freed blocks go on one free list-// per size, to be reused by the next allocation of that size. Programs are-// single-threaded, so no lock.+// per size, to be reused by the next allocation of that size. Programs may+// have several threads (stage 15), so a tiny spinlock guards the heap; a+// thread that finds it taken yields its CPU instead of spinning long.  const CLASSES: usize = 17; // 16 << 16 = 1 MiB @@ -1078,9 +1086,21 @@ struct Heap {     used: UnsafeCell<u64>,  // bump pointer: end of what's handed out     end: UnsafeCell<u64>,   // the current break     free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty)+    lock: AtomicBool, } -unsafe impl Sync for Heap {} // single-threaded programs+unsafe impl Sync for Heap {} // the fields are only touched under `lock`++impl Heap {+    fn lock(&self) {+        while self.lock.swap(true, Ordering::Acquire) {+            sched_yield();+        }+    }+    fn unlock(&self) {+        self.lock.store(false, Ordering::Release);+    }+}  #[global_allocator] static HEAP: Heap = Heap {@@ -1088,6 +1108,7 @@ static HEAP: Heap = Heap {     used: UnsafeCell::new(0),     end: UnsafeCell::new(0),     free: UnsafeCell::new([0; CLASSES]),+    lock: AtomicBool::new(false), };  /// Size class for a layout: 0 = 16 bytes, 1 = 32, ...@@ -1098,6 +1119,26 @@ fn class_of(layout: Layout) -> usize {  unsafe impl GlobalAlloc for Heap {     unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+        self.lock();+        let p = unsafe { self.alloc_locked(layout) };+        self.unlock();+        p+    }++    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+        let class = class_of(layout);+        self.lock();+        unsafe {+            let free = &mut *self.free.get();+            *(ptr as *mut usize) = free[class];+            free[class] = ptr as usize;+        }+        self.unlock();+    }+}++impl Heap {+    unsafe fn alloc_locked(&self, layout: Layout) -> *mut u8 {         let class = class_of(layout);         if class >= CLASSES {             return core::ptr::null_mut(); // too big: "out of memory"@@ -1133,14 +1174,347 @@ unsafe impl GlobalAlloc for Heap {             start as *mut u8         }     }+} -    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {-        let class = class_of(layout);-        unsafe {-            let free = &mut *self.free.get();-            *(ptr as *mut usize) = free[class];-            free[class] = ptr as usize;+// --- Stage 15: what a libc's plumbing needs ----------------------------------++pub const SYS_POLL: u64 = 7;+pub const SYS_PREAD64: u64 = 17;+pub const SYS_PWRITE64: u64 = 18;+pub const SYS_READV: u64 = 19;+pub const SYS_WRITEV: u64 = 20;+pub const SYS_CLONE: u64 = 56;+pub const SYS_EXIT_THREAD: u64 = 60; // Linux's "exit": just this thread+pub const SYS_SETSID: u64 = 112;+pub const SYS_ARCH_PRCTL: u64 = 158;+pub const SYS_GETTID: u64 = 186;+pub const SYS_FUTEX: u64 = 202;+pub const SYS_EPOLL_WAIT: u64 = 232;+pub const SYS_EPOLL_CTL: u64 = 233;+pub const SYS_TGKILL: u64 = 234;+pub const SYS_EPOLL_CREATE1: u64 = 291;++pub const O_NOCTTY: u64 = 0o400;+pub const O_NONBLOCK: u64 = 0o4000;+pub const O_RDWR: u64 = 2;+pub const F_GETFL: u64 = 3;+pub const F_SETFL: u64 = 4;+pub const EINPROGRESS: i64 = 115;++/// Turn O_NONBLOCK on or off for `fd` (fcntl F_GETFL + F_SETFL).+pub fn set_nonblocking(fd: i64, on: bool) -> i64 {+    let flags = fcntl(fd, F_GETFL, 0);+    if flags < 0 {+        return flags;+    }+    let flags = if on { flags as u64 | O_NONBLOCK } else { flags as u64 & !O_NONBLOCK };+    fcntl(fd, F_SETFL, flags)+}++pub fn pread(fd: i64, buf: &mut [u8], offset: u64) -> i64 {+    syscall6(SYS_PREAD64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64, offset, 0, 0)+}+pub fn pwrite(fd: i64, buf: &[u8], offset: u64) -> i64 {+    syscall6(SYS_PWRITE64, fd as u64, buf.as_ptr() as u64, buf.len() as u64, offset, 0, 0)+}+/// writev: several buffers in one call (`struct iovec {base, len}`).+pub fn writev(fd: i64, bufs: &[&[u8]]) -> i64 {+    let iov: alloc::vec::Vec<[u64; 2]> = bufs.iter().map(|b| [b.as_ptr() as u64, b.len() as u64]).collect();+    syscall3(SYS_WRITEV, fd as u64, iov.as_ptr() as u64, iov.len() as u64)+}++/// Linux 228: 0 = CLOCK_REALTIME (wall clock), 1 = CLOCK_MONOTONIC.+pub fn clock_gettime(clock: u64) -> (u64, u64) {+    let mut ts = [0u64; 2];+    syscall3(SYS_CLOCK_GETTIME, clock, ts.as_mut_ptr() as u64, 0);+    (ts[0], ts[1])+}++// poll / epoll (see the kernel's poll.rs).+pub const POLLIN: u16 = 0x1;+pub const POLLOUT: u16 = 0x4;+pub const POLLERR: u16 = 0x8;+pub const POLLHUP: u16 = 0x10;+pub const POLLNVAL: u16 = 0x20;++/// `struct pollfd`.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct PollFd {+    pub fd: i32,+    pub events: u16,+    pub revents: u16,+}+/// Wait until one of `fds` is ready (timeout_ms -1 = forever); returns+/// how many are, 0 on timeout.+pub fn poll(fds: &mut [PollFd], timeout_ms: i64) -> i64 {+    syscall3(SYS_POLL, fds.as_mut_ptr() as u64, fds.len() as u64, timeout_ms as u64)+}++pub const EPOLL_CTL_ADD: u64 = 1;+pub const EPOLL_CTL_DEL: u64 = 2;+pub const EPOLL_CTL_MOD: u64 = 3;+pub const EPOLLIN: u32 = 0x1;+pub const EPOLLOUT: u32 = 0x4;++/// `struct epoll_event` is *packed* on x86_64: 4 + 8 = 12 bytes.+#[repr(C, packed)]+#[derive(Clone, Copy, Default)]+pub struct EpollEvent {+    pub events: u32,+    pub data: u64,+}+pub fn epoll_create1(flags: u64) -> i64 {+    syscall3(SYS_EPOLL_CREATE1, flags, 0, 0)+}+pub fn epoll_ctl(epfd: i64, op: u64, fd: i64, events: u32, data: u64) -> i64 {+    let ev = EpollEvent { events, data };+    syscall5(SYS_EPOLL_CTL, epfd as u64, op, fd as u64, &ev as *const EpollEvent as u64, 0)+}+pub fn epoll_wait(epfd: i64, out: &mut [EpollEvent], timeout_ms: i64) -> i64 {+    syscall5(SYS_EPOLL_WAIT, epfd as u64, out.as_mut_ptr() as u64, out.len() as u64, timeout_ms as u64, 0)+}++// Sessions and terminals.+pub fn setsid() -> i64 {+    syscall3(SYS_SETSID, 0, 0, 0)+}+pub const TCGETS: u64 = 0x5401;+pub const TCSETS: u64 = 0x5402;+pub const TIOCSCTTY: u64 = 0x540E;+pub const TIOCGPTN: u64 = 0x8004_5430;+pub const TIOCSPTLCK: u64 = 0x4004_5431;+pub const ICANON: u32 = 0x2;+pub const ECHO: u32 = 0x8;+pub const ISIG: u32 = 0x1;+pub const VMIN: usize = 6;+pub const VTIME: usize = 5;++/// Linux's `struct termios` (36 bytes): the terminal's settings.+#[repr(C)]+#[derive(Clone, Copy, Default)]+pub struct Termios {+    pub iflag: u32,+    pub oflag: u32,+    pub cflag: u32,+    pub lflag: u32,+    pub line: u8,+    pub cc: [u8; 19],+}+pub fn tcgetattr(fd: i64) -> Result<Termios, i64> {+    let mut t = Termios::default();+    match ioctl(fd, TCGETS, &mut t as *mut Termios as u64) {+        0 => Ok(t),+        e => Err(e),+    }+}+pub fn tcsetattr(fd: i64, t: &Termios) -> i64 {+    ioctl(fd, TCSETS, t as *const Termios as u64)+}+/// Raw mode, like libc's cfmakeraw: no line editing, echo or signals,+/// no character conversions; read returns as soon as 1 byte is there.+pub fn cfmakeraw(t: &mut Termios) {+    t.iflag &= !(0x1 | 0x2 | 0x8 | 0x20 | 0x40 | 0x80 | 0x100 | 0x400); // IGNBRK BRKINT PARMRK ISTRIP INLCR IGNCR ICRNL IXON+    t.oflag &= !0x1; // OPOST+    t.lflag &= !(ECHO | 0x40 | ICANON | ISIG | 0x8000); // ECHO ECHONL ICANON ISIG IEXTEN+    t.cflag = (t.cflag & !0x130) | 0x30; // CS8, no parity+    t.cc[VMIN] = 1;+    t.cc[VTIME] = 0;+}++/// Open a new pseudo-terminal: returns (master fd, slave number N); the+/// slave is /dev/pts/N. Like posix_openpt + grantpt + unlockpt.+pub fn openpty() -> Result<(i64, u32), i64> {+    let master = open("/dev/ptmx", O_RDWR | O_NOCTTY);+    if master < 0 {+        return Err(master);+    }+    let mut n = 0u32;+    let unlock = 0u32;+    let r = ioctl(master, TIOCGPTN, &mut n as *mut u32 as u64);+    let r = if r == 0 { ioctl(master, TIOCSPTLCK, &unlock as *const u32 as u64) } else { r };+    if r != 0 {+        close(master);+        return Err(r);+    }+    Ok((master, n))+}++// Threads: clone + futex + TLS.+pub const FUTEX_WAIT: u64 = 0;+pub const FUTEX_WAKE: u64 = 1;+pub const FUTEX_PRIVATE_FLAG: u64 = 128;+pub const CLONE_VM: u64 = 0x100;+pub const CLONE_FS: u64 = 0x200;+pub const CLONE_FILES: u64 = 0x400;+pub const CLONE_SIGHAND: u64 = 0x800;+pub const CLONE_THREAD: u64 = 0x10000;+pub const CLONE_SYSVSEM: u64 = 0x40000;+pub const CLONE_SETTLS: u64 = 0x80000;+pub const CLONE_PARENT_SETTID: u64 = 0x100000;+pub const CLONE_CHILD_CLEARTID: u64 = 0x200000;+pub const ARCH_SET_FS: u64 = 0x1002;+pub const ARCH_GET_FS: u64 = 0x1003;++use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};++pub fn gettid() -> i64 {+    syscall3(SYS_GETTID, 0, 0, 0)+}+pub fn tgkill(tgid: i64, tid: i64, sig: i32) -> i64 {+    syscall3(SYS_TGKILL, tgid as u64, tid as u64, sig as u64)+}+/// Sleep while `*word == expected` (until futex_wake, or `timeout_us`).+pub fn futex_wait(word: &AtomicU32, expected: u32, timeout_us: Option<u64>) -> i64 {+    let ts = timeout_us.map(|us| [us / 1_000_000, (us % 1_000_000) * 1000]);+    let ts_ptr = ts.as_ref().map_or(0, |t| t.as_ptr() as u64);+    syscall6(SYS_FUTEX, word.as_ptr() as u64, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, expected as u64, ts_ptr, 0, 0)+}+/// Wake up to `n` threads sleeping on `word`; returns how many woke.+pub fn futex_wake(word: &AtomicU32, n: u32) -> i64 {+    syscall6(SYS_FUTEX, word.as_ptr() as u64, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, n as u64, 0, 0, 0)+}++/// A mutex built on a futex (Ulrich Drepper's "Futexes Are Tricky",+/// mutex #2). The word is 0 = unlocked, 1 = locked, 2 = locked and+/// someone may be sleeping. Taking a free lock is one atomic instruction,+/// no syscall; only contention goes to the kernel.+pub struct FutexMutex {+    state: AtomicU32,+}+impl FutexMutex {+    pub const fn new() -> FutexMutex {+        FutexMutex { state: AtomicU32::new(0) }+    }+    pub fn lock(&self) {+        if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok() {+            return; // fast path: it was free+        }+        // Slow path: mark "contended" (2) and sleep until it's free. The+        // kernel only puts us to sleep if the word is *still* 2, so an+        // unlock between our swap and the futex call can't be missed.+        while self.state.swap(2, Ordering::Acquire) != 0 {+            futex_wait(&self.state, 2, None);+        }+    }+    pub fn unlock(&self) {+        // 1 -> 0: nobody waits. 2 -> 0: wake one sleeper.+        if self.state.swap(0, Ordering::Release) == 2 {+            futex_wake(&self.state, 1);+        }+    }+}++/// Thread-local storage, the x86_64 way: each thread's FS register base+/// points at its own block, whose first word points at itself (so code can+/// find the block with `mov rax, fs:[0]`). We keep one u64 per thread in+/// the second word.+#[repr(C)]+pub struct TlsBlock {+    pub self_ptr: u64,+    pub value: u64,+}+pub fn arch_prctl(code: u64, addr: u64) -> i64 {+    syscall3(SYS_ARCH_PRCTL, code, addr, 0)+}+/// This thread's TLS value (fs:[8]).+pub fn tls_value() -> u64 {+    let v: u64;+    unsafe { asm!("mov {}, fs:[8]", out(reg) v, options(nostack, readonly)) };+    v+}++/// A running thread (see `spawn_thread`).+pub struct Thread {+    pub tid: i64,+    /// The kernel writes our tid here (CLONE_PARENT_SETTID) and zeroes it ++    /// futex-wakes it when the thread exits (CLONE_CHILD_CLEARTID): that's+    /// how `join` knows it's done, exactly like pthread_join.+    tid_word: alloc::boxed::Box<AtomicU32>,+    stack: u64,+    tls: alloc::boxed::Box<TlsBlock>,+}++const THREAD_STACK: u64 = 64 * 1024;+type ThreadFn = alloc::boxed::Box<dyn FnOnce()>;++/// The new thread starts here, on its own stack, with the boxed closure.+extern "C" fn thread_entry(arg: u64) -> ! {+    let f = unsafe { alloc::boxed::Box::from_raw(arg as *mut ThreadFn) };+    f();+    // exit (60), not exit_group: only this thread ends.+    syscall3(SYS_EXIT_THREAD, 0, 0, 0);+    loop {}+}++/// Start `f` in a new thread of this process (same memory, same fds),+/// with `tls_value` in its TLS block. Like pthread_create.+pub fn spawn_thread(tls_value: u64, f: impl FnOnce() + Send + 'static) -> Result<Thread, i64> {+    let stack = mmap(0, THREAD_STACK, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+    if stack < 0 {+        return Err(stack);+    }+    let stack = stack as u64;+    let boxed: alloc::boxed::Box<ThreadFn> = alloc::boxed::Box::new(alloc::boxed::Box::new(f));+    let arg = alloc::boxed::Box::into_raw(boxed) as u64;+    let mut tls = alloc::boxed::Box::new(TlsBlock { self_ptr: 0, value: tls_value });+    tls.self_ptr = &*tls as *const TlsBlock as u64;+    let tid_word = alloc::boxed::Box::new(AtomicU32::new(0));+    // The child's stack starts with [entry, arg] for the asm below.+    let sp = stack + THREAD_STACK - 16;+    unsafe {+        *(sp as *mut u64) = thread_entry as usize as u64;+        *((sp + 8) as *mut u64) = arg;+    }+    let flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM+        | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID;+    let tid_ptr = tid_word.as_ptr() as u64;+    let ret: i64;+    // clone returns twice: the parent gets the tid, the child 0 -- on its+    // new stack, so it must not touch the parent's stack frame. It pops+    // [entry, arg] and calls entry(arg) straight from assembly (after the+    // two pops rsp is 16-byte aligned, as `call` wants).+    unsafe {+        asm!(+            "syscall",+            "test rax, rax",+            "jnz 2f",+            "pop rax",+            "pop rdi",+            "call rax",+            "ud2",+            "2:",+            inlateout("rax") SYS_CLONE as i64 => ret,+            in("rdi") flags,+            in("rsi") sp,+            in("rdx") tid_ptr,+            in("r10") tid_ptr,+            in("r8") tls.self_ptr,+            lateout("rcx") _,+            lateout("r11") _,+            options(nostack)+        );+    }+    if ret < 0 {+        unsafe { drop(alloc::boxed::Box::from_raw(arg as *mut ThreadFn)) };+        munmap(stack, THREAD_STACK);+        return Err(ret);+    }+    Ok(Thread { tid: ret, tid_word, stack, tls })+}++impl Thread {+    /// Wait for the thread to finish, then free its stack.+    pub fn join(self) {+        loop {+            let tid = self.tid_word.load(Ordering::Acquire);+            if tid == 0 {+                break; // the kernel cleared it: the thread is gone+            }+            futex_wait(&self.tid_word, tid, None);         }+        munmap(self.stack, THREAD_STACK);+        drop(self.tls);     } }