From f9400f6ac4f8092a242846d8e5e3a4d2f835a74b Mon Sep 17 00:00:00 2001 From: subhobhai943 Date: Mon, 24 Aug 2026 23:38:59 +0530 Subject: [PATCH] feat(kernel): Add RB-tree/kfifo/hashtable, wait queues, futex, tiny RCU, page cache and KTest harness Expands the kernel-and-TTY core with the primitives the higher layers have been missing, plus an in-kernel harness that proves they work. Data structures (lib/): - rbtree.c: Linux-compatible red-black tree with parent pointers, full insert/erase rebalancing, in-order iteration and an invariant validator. - kfifo.c: power-of-two byte ring with free-running indices, so the wrap path costs one mask instead of a branch per byte. - hashtable.c: separately-chained table with FNV-1a hashing, string or u64 keys, and rehash-on-growth at a 3/4 load factor. Kernel subsystems: - kernel/wait.c: wait queues and one-shot completion barriers, backed by a static sleeper pool so a blocking path never re-enters the allocator. - kernel/futex.c: 32-bucket futex hash with value re-check, requeue, and an fmutex_t sleeping mutex built on the uncontended-fastpath protocol. - kernel/rcu.c: tiny RCU with reader nesting, deferred callbacks, kfree_rcu and rcu_assign_pointer/rcu_dereference publish barriers. - mm/page_cache.c: write-back block page cache, 4 KB pages indexed by (device, page), CLOCK second-chance eviction and dirty write-back. - kernel/ktest.c: KUnit-style harness with 6 built-in suites, 21 cases and 735 assertions covering every subsystem above (all passing under QEMU). LazyBox userland: - coreutils.c: sort, uniq, cut, tr, rev, tac, nl, seq, diff, xxd, du, factor, sum and truncate, reading through the VFS and handling the zero-length synthetic nodes that procfs and sysfs expose. - ktest, rcuinfo, futexinfo, waitinfo and pagecache diagnostics applets. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP --- Makefile | 7 +- include/kernel/futex.h | 62 +++ include/kernel/ktest.h | 81 ++++ include/kernel/rcu.h | 58 +++ include/kernel/wait.h | 65 +++ include/lib/hashtable.h | 50 +++ include/lib/kfifo.h | 40 ++ include/lib/rbtree.h | 63 +++ include/mm/page_cache.h | 63 +++ include/userland/coreutils.h | 22 ++ kernel/futex.c | 159 ++++++++ kernel/ktest.c | 703 +++++++++++++++++++++++++++++++++ kernel/main.c | 15 + kernel/rcu.c | 169 ++++++++ kernel/wait.c | 227 +++++++++++ lib/hashtable.c | 227 +++++++++++ lib/kfifo.c | 134 +++++++ lib/rbtree.c | 309 +++++++++++++++ mm/page_cache.c | 333 ++++++++++++++++ userland/lazybox/coreutils.c | 744 +++++++++++++++++++++++++++++++++++ userland/lazybox/lazybox.c | 88 +++++ userland/lazybox/shell.c | 17 +- 22 files changed, 3631 insertions(+), 5 deletions(-) create mode 100644 include/kernel/futex.h create mode 100644 include/kernel/ktest.h create mode 100644 include/kernel/rcu.h create mode 100644 include/kernel/wait.h create mode 100644 include/lib/hashtable.h create mode 100644 include/lib/kfifo.h create mode 100644 include/lib/rbtree.h create mode 100644 include/mm/page_cache.h create mode 100644 include/userland/coreutils.h create mode 100644 kernel/futex.c create mode 100644 kernel/ktest.c create mode 100644 kernel/rcu.c create mode 100644 kernel/wait.c create mode 100644 lib/hashtable.c create mode 100644 lib/kfifo.c create mode 100644 lib/rbtree.c create mode 100644 mm/page_cache.c create mode 100644 userland/lazybox/coreutils.c diff --git a/Makefile b/Makefile index e76990a..e976f7c 100644 --- a/Makefile +++ b/Makefile @@ -126,7 +126,8 @@ CORE_SRCS = kernel/main.c kernel/task.c kernel/sync.c kernel/timer.c kernel/prin kernel/syslog.c kernel/module.c kernel/kobject.c kernel/metrics.c kernel/namespace.c \ kernel/signal.c kernel/workqueue.c kernel/tsc.c kernel/vt_art.c \ kernel/sub/sub_runtime.c kernel/sub/sub_vm.c \ - lib/string.c lib/vsprintf.c lib/bitmap.c \ + kernel/wait.c kernel/futex.c kernel/rcu.c kernel/ktest.c \ + lib/string.c lib/vsprintf.c lib/bitmap.c lib/rbtree.c lib/kfifo.c lib/hashtable.c \ init/cmdline.c init/service.c usr/initramfs.c block/block.c block/elevator.c \ ipc/sem.c ipc/shm.c ipc/shm_posix.c ipc/pipe.c ipc/ipc.c ipc/msg.c \ crypto/sha256.c crypto/md5.c crypto/crc32.c crypto/prng.c sound/beep.c @@ -170,7 +171,7 @@ endif CONFIG_SRCS-y := $(CORE_SRCS) ifeq ($(CONFIG_MM_PMM), y) - CONFIG_SRCS-y += mm/pmm.c mm/kmalloc.c + CONFIG_SRCS-y += mm/pmm.c mm/kmalloc.c mm/page_cache.c endif ifeq ($(CONFIG_MM_SLAB), y) CONFIG_SRCS-y += mm/slab.c @@ -279,7 +280,7 @@ CONFIG_SRCS-y += drivers/char/tty.c drivers/char/keyboard.c drivers/char/serial. # Userland ifeq ($(CONFIG_USERLAND_LAZYBOX), y) - CONFIG_SRCS-y += userland/lazybox/lazybox.c userland/lazybox/shell.c userland/lazybox/sh.c userland/lazybox/nano.c userland/lazybox/snake.c userland/lazybox/tree.c + CONFIG_SRCS-y += userland/lazybox/lazybox.c userland/lazybox/shell.c userland/lazybox/sh.c userland/lazybox/nano.c userland/lazybox/snake.c userland/lazybox/tree.c userland/lazybox/coreutils.c endif # Deduplicate diff --git a/include/kernel/futex.h b/include/kernel/futex.h new file mode 100644 index 0000000..c563bf5 --- /dev/null +++ b/include/kernel/futex.h @@ -0,0 +1,62 @@ +#ifndef _KERNEL_FUTEX_H +#define _KERNEL_FUTEX_H + +#include +#include +#include +#include + +// Fast Userspace muTEX. A futex is an ordinary 32-bit word that stays entirely +// in user memory while uncontended; the kernel is only consulted when a thread +// actually has to block, at which point the word's address is hashed onto one +// of FUTEX_BUCKETS wait queues. + +#define FUTEX_BUCKETS 32 + +typedef enum { + FUTEX_OP_WAIT = 0, + FUTEX_OP_WAKE = 1, + FUTEX_OP_REQUEUE = 2, + FUTEX_OP_WAKE_ALL = 3 +} futex_op_t; + +typedef struct { + uint64_t waits; + uint64_t wakes; + uint64_t requeues; + uint64_t spurious; + uint64_t timeouts; + uint32_t active_waiters; +} futex_stats_t; + +void futex_init(void); + +// Sleep on `uaddr` only while *uaddr still equals `expected`. Returns 0 when +// woken, 1 on timeout, -1 if the value changed before parking (the caller +// should retry its fast path). +int futex_wait(volatile uint32_t* uaddr, uint32_t expected, uint64_t timeout_ticks); + +// Wake up to `count` waiters parked on `uaddr`. Returns how many were woken. +int futex_wake(volatile uint32_t* uaddr, int count); + +// Move waiters from `uaddr1` onto `uaddr2` without waking them (used to avoid +// the thundering herd when a condvar hands off to a mutex). +int futex_requeue(volatile uint32_t* uaddr1, volatile uint32_t* uaddr2, int count); + +futex_stats_t futex_get_stats(void); +void futex_dump(void); + +// --------------------------------------------------------------------------- +// A futex-backed mutex, for kernel code that wants sleeping mutual exclusion. +// --------------------------------------------------------------------------- +typedef struct { + volatile uint32_t state; // 0 = unlocked, 1 = locked, 2 = locked + waiters + char name[24]; +} fmutex_t; + +void fmutex_init(fmutex_t* m, const char* name); +void fmutex_lock(fmutex_t* m); +bool fmutex_trylock(fmutex_t* m); +void fmutex_unlock(fmutex_t* m); + +#endif // _KERNEL_FUTEX_H diff --git a/include/kernel/ktest.h b/include/kernel/ktest.h new file mode 100644 index 0000000..f6fe87b --- /dev/null +++ b/include/kernel/ktest.h @@ -0,0 +1,81 @@ +#ifndef _KERNEL_KTEST_H +#define _KERNEL_KTEST_H + +#include +#include +#include + +// In-kernel self-test harness (KUnit-style). Suites register a list of cases; +// `ktest` from the LazyBox shell runs them and reports pass/fail per assertion. + +#define KTEST_MAX_SUITES 16 +#define KTEST_MAX_CASES 24 + +typedef struct ktest_context { + const char* suite; + const char* test; + uint32_t assertions; + uint32_t failures; + bool aborted; +} ktest_context_t; + +typedef void (*ktest_fn_t)(ktest_context_t* ctx); + +typedef struct { + const char* name; + ktest_fn_t func; +} ktest_case_t; + +typedef struct { + const char* name; + const ktest_case_t* cases; + size_t case_count; +} ktest_suite_t; + +typedef struct { + uint32_t suites_run; + uint32_t tests_run; + uint32_t tests_passed; + uint32_t tests_failed; + uint32_t assertions; + uint32_t assertion_failures; + uint64_t elapsed_ticks; +} ktest_result_t; + +void ktest_init(void); +int ktest_register_suite(const ktest_suite_t* suite); + +// Run every suite, or one by name. Returns the number of failed tests. +int ktest_run_all(ktest_result_t* result_out); +int ktest_run_suite(const char* name, ktest_result_t* result_out); +void ktest_list_suites(void); + +// Assertion primitives used by the macros below. +void ktest_report(ktest_context_t* ctx, bool passed, const char* expr, + const char* file, int line, const char* detail); + +#define KTEST_ASSERT(ctx, cond) \ + ktest_report((ctx), (cond), #cond, __FILE__, __LINE__, NULL) + +#define KTEST_ASSERT_EQ(ctx, a, b) \ + do { \ + long long _a = (long long)(a); \ + long long _b = (long long)(b); \ + ktest_expect_eq((ctx), _a, _b, #a " == " #b, __FILE__, __LINE__); \ + } while (0) + +#define KTEST_ASSERT_STREQ(ctx, a, b) \ + ktest_expect_streq((ctx), (a), (b), #a " == " #b, __FILE__, __LINE__) + +#define KTEST_ASSERT_NOT_NULL(ctx, p) \ + ktest_report((ctx), (p) != NULL, #p " != NULL", __FILE__, __LINE__, NULL) + +void ktest_expect_eq(ktest_context_t* ctx, long long a, long long b, + const char* expr, const char* file, int line); +void ktest_expect_streq(ktest_context_t* ctx, const char* a, const char* b, + const char* expr, const char* file, int line); + +// Built-in suites covering the core data structures and subsystems. +void ktest_register_builtin_suites(void); + +#endif // _KERNEL_KTEST_H diff --git a/include/kernel/rcu.h b/include/kernel/rcu.h new file mode 100644 index 0000000..d335ec6 --- /dev/null +++ b/include/kernel/rcu.h @@ -0,0 +1,58 @@ +#ifndef _KERNEL_RCU_H +#define _KERNEL_RCU_H + +#include +#include +#include + +// Tiny RCU (Read-Copy-Update). +// +// Readers are free: rcu_read_lock() only bumps a per-CPU nesting depth, so a +// lookup never contends with an update. Writers publish a new version with +// rcu_assign_pointer() and defer reclamation of the old one until every reader +// that could still hold a reference has finished — a "grace period". + +#define RCU_MAX_CALLBACKS 64 + +typedef void (*rcu_callback_t)(void* arg); + +typedef struct { + uint64_t grace_periods; + uint64_t callbacks_queued; + uint64_t callbacks_invoked; + uint64_t read_locks; + uint32_t nesting_depth; + uint32_t pending; +} rcu_stats_t; + +void rcu_init(void); + +void rcu_read_lock(void); +void rcu_read_unlock(void); +bool rcu_read_lock_held(void); + +// Block until every reader active at call time has left its critical section. +void rcu_synchronize(void); + +// Queue `func(arg)` to run once the current grace period elapses. +int call_rcu(rcu_callback_t func, void* arg); + +// Free `ptr` after a grace period (the common case for call_rcu). +int kfree_rcu(void* ptr); + +// Advance the state machine; called from the timer tick and from rcu_synchronize. +void rcu_process_callbacks(void); + +uint64_t rcu_get_grace_period(void); +rcu_stats_t rcu_get_stats(void); +void rcu_dump(void); + +// Publish/consume helpers. The barriers keep the compiler from reordering the +// initialisation of an object past the store that makes it visible. +#define rcu_assign_pointer(p, v) \ + do { __atomic_store_n(&(p), (v), __ATOMIC_RELEASE); } while (0) + +#define rcu_dereference(p) \ + __atomic_load_n(&(p), __ATOMIC_ACQUIRE) + +#endif // _KERNEL_RCU_H diff --git a/include/kernel/wait.h b/include/kernel/wait.h new file mode 100644 index 0000000..0c4ba61 --- /dev/null +++ b/include/kernel/wait.h @@ -0,0 +1,65 @@ +#ifndef _KERNEL_WAIT_H +#define _KERNEL_WAIT_H + +#include +#include +#include +#include +#include +#include + +// Wait queues and completions. +// +// SUB-OS runs a cooperative round-robin scheduler, so a sleeper parks itself +// on the queue, marks its task BLOCKED and yields until a waker flips the +// entry's `woken` flag. Timed waits fall back to the PIT tick counter. + +typedef struct wait_entry { + task_t* task; + volatile bool woken; + struct list_head node; +} wait_entry_t; + +typedef struct wait_queue { + char name[24]; + struct list_head waiters; + spinlock_t lock; + uint64_t wakeups; + uint64_t sleeps; + uint32_t nr_waiting; +} wait_queue_t; + +void wait_queue_init(wait_queue_t* wq, const char* name); + +// Park the caller until woken. Returns 0 on wake, -1 if the queue is full. +int wait_event(wait_queue_t* wq); + +// Park the caller for at most `timeout_ticks` PIT ticks (100 Hz). +// Returns 0 if woken, 1 if the wait timed out. +int wait_event_timeout(wait_queue_t* wq, uint64_t timeout_ticks); + +// Wake the longest-waiting sleeper, or every sleeper. Returns the count woken. +int wake_up(wait_queue_t* wq); +int wake_up_all(wait_queue_t* wq); + +uint32_t wait_queue_waiters(const wait_queue_t* wq); + +// --------------------------------------------------------------------------- +// Completions: one-shot "this is done" barriers built on a wait queue. +// --------------------------------------------------------------------------- +typedef struct completion { + wait_queue_t wq; + volatile bool done; +} completion_t; + +void completion_init(completion_t* c, const char* name); +void wait_for_completion(completion_t* c); +int wait_for_completion_timeout(completion_t* c, uint64_t timeout_ticks); +void complete(completion_t* c); +void complete_all(completion_t* c); +bool completion_done(const completion_t* c); + +void wait_subsystem_init(void); +void wait_dump_stats(void); + +#endif // _KERNEL_WAIT_H diff --git a/include/lib/hashtable.h b/include/lib/hashtable.h new file mode 100644 index 0000000..093dd71 --- /dev/null +++ b/include/lib/hashtable.h @@ -0,0 +1,50 @@ +#ifndef _LIB_HASHTABLE_H +#define _LIB_HASHTABLE_H + +#include +#include +#include + +// Separately-chained hash table with FNV-1a hashing and automatic growth. +// Keys are either NUL-terminated strings (owned copies) or raw 64-bit ids. + +typedef struct hnode { + uint64_t hash; + char* skey; // NULL for integer-keyed entries + uint64_t ikey; + void* value; + struct hnode* next; +} hnode_t; + +typedef struct hashtable { + hnode_t** buckets; + size_t bucket_count; + size_t entries; + size_t collisions; + bool string_keys; +} hashtable_t; + +uint64_t hash_fnv1a(const void* data, size_t len); +uint64_t hash_string(const char* s); +uint64_t hash_u64(uint64_t v); + +hashtable_t* hashtable_create(size_t initial_buckets, bool string_keys); +void hashtable_destroy(hashtable_t* ht); + +int hashtable_put(hashtable_t* ht, const char* key, void* value); +void* hashtable_get(const hashtable_t* ht, const char* key); +bool hashtable_remove(hashtable_t* ht, const char* key); + +int hashtable_put_u64(hashtable_t* ht, uint64_t key, void* value); +void* hashtable_get_u64(const hashtable_t* ht, uint64_t key); +bool hashtable_remove_u64(hashtable_t* ht, uint64_t key); + +size_t hashtable_size(const hashtable_t* ht); +size_t hashtable_buckets(const hashtable_t* ht); +size_t hashtable_collisions(const hashtable_t* ht); +size_t hashtable_longest_chain(const hashtable_t* ht); + +typedef void (*hashtable_iter_fn)(const hnode_t* node, void* ctx); +void hashtable_foreach(const hashtable_t* ht, hashtable_iter_fn fn, void* ctx); + +#endif // _LIB_HASHTABLE_H diff --git a/include/lib/kfifo.h b/include/lib/kfifo.h new file mode 100644 index 0000000..cd5e19a --- /dev/null +++ b/include/lib/kfifo.h @@ -0,0 +1,40 @@ +#ifndef _LIB_KFIFO_H +#define _LIB_KFIFO_H + +#include +#include +#include +#include + +// Power-of-two byte ring buffer with an unsigned index-wrap discipline, +// mirroring the classic Linux kfifo. Producer and consumer indices are +// free-running; masking happens only at access time. + +typedef struct kfifo { + uint8_t* buffer; + size_t size; // Always a power of two + size_t mask; // size - 1 + size_t in; // Free-running producer index + size_t out; // Free-running consumer index + bool owns_buffer; + spinlock_t lock; +} kfifo_t; + +int kfifo_alloc(kfifo_t* fifo, size_t size); +int kfifo_init_static(kfifo_t* fifo, void* buffer, size_t size); +void kfifo_free(kfifo_t* fifo); +void kfifo_reset(kfifo_t* fifo); + +size_t kfifo_in(kfifo_t* fifo, const void* src, size_t len); +size_t kfifo_out(kfifo_t* fifo, void* dst, size_t len); +size_t kfifo_peek(const kfifo_t* fifo, void* dst, size_t len); + +size_t kfifo_len(const kfifo_t* fifo); +size_t kfifo_avail(const kfifo_t* fifo); +bool kfifo_is_empty(const kfifo_t* fifo); +bool kfifo_is_full(const kfifo_t* fifo); + +int kfifo_put_byte(kfifo_t* fifo, uint8_t byte); +int kfifo_get_byte(kfifo_t* fifo, uint8_t* byte_out); + +#endif // _LIB_KFIFO_H diff --git a/include/lib/rbtree.h b/include/lib/rbtree.h new file mode 100644 index 0000000..2c3c1f2 --- /dev/null +++ b/include/lib/rbtree.h @@ -0,0 +1,63 @@ +#ifndef _LIB_RBTREE_H +#define _LIB_RBTREE_H + +#include +#include +#include + +// Linux-style Red-Black Tree: O(log n) ordered container used by the +// CFS-style scheduler index, VMA lookup and the page cache radix index. + +#define RB_RED 0 +#define RB_BLACK 1 + +struct rb_node { + struct rb_node* rb_parent; + struct rb_node* rb_left; + struct rb_node* rb_right; + int rb_color; +}; + +struct rb_root { + struct rb_node* rb_node; +}; + +#define RB_ROOT_INIT { NULL } + +#define rb_entry(ptr, type, member) \ + ((type*)((char*)(ptr) - offsetof(type, member))) + +static inline void rb_root_init(struct rb_root* root) { + root->rb_node = NULL; +} + +static inline bool rb_empty(const struct rb_root* root) { + return root->rb_node == NULL; +} + +// Attach a freshly allocated node into the tree, then recolour. +static inline void rb_link_node(struct rb_node* node, struct rb_node* parent, + struct rb_node** rb_link) { + node->rb_parent = parent; + node->rb_color = RB_RED; + node->rb_left = NULL; + node->rb_right = NULL; + *rb_link = node; +} + +void rb_insert_color(struct rb_node* node, struct rb_root* root); +void rb_erase(struct rb_node* node, struct rb_root* root); + +struct rb_node* rb_first(const struct rb_root* root); +struct rb_node* rb_last(const struct rb_root* root); +struct rb_node* rb_next(const struct rb_node* node); +struct rb_node* rb_prev(const struct rb_node* node); + +size_t rb_count(const struct rb_root* root); +int rb_black_height(const struct rb_root* root); +bool rb_validate(const struct rb_root* root); + +#define rb_for_each(pos, root) \ + for (pos = rb_first(root); pos != NULL; pos = rb_next(pos)) + +#endif // _LIB_RBTREE_H diff --git a/include/mm/page_cache.h b/include/mm/page_cache.h new file mode 100644 index 0000000..421c6f8 --- /dev/null +++ b/include/mm/page_cache.h @@ -0,0 +1,63 @@ +#ifndef _MM_PAGE_CACHE_H +#define _MM_PAGE_CACHE_H + +#include +#include +#include + +// Write-back page cache sitting between the VFS and the block layer. +// +// Every entry caches PAGE_CACHE_BLOCK_SIZE bytes of one block device, keyed by +// (device, page index). Lookups go through a hash table; eviction follows a +// CLOCK approximation of LRU, and dirty pages are written back before reuse. + +#define PAGE_CACHE_BLOCK_SIZE 4096 +#define PAGE_CACHE_SECTORS (PAGE_CACHE_BLOCK_SIZE / 512) +#define PAGE_CACHE_MAX_PAGES 64 + +typedef struct page_cache_entry { + char dev_name[32]; + uint64_t page_index; // Offset in PAGE_CACHE_BLOCK_SIZE units + uint8_t* data; + bool valid; + bool dirty; + bool referenced; // CLOCK second-chance bit + uint32_t pin_count; + uint64_t last_access; + uint64_t hits; +} page_cache_entry_t; + +typedef struct { + uint64_t hits; + uint64_t misses; + uint64_t evictions; + uint64_t writebacks; + uint64_t read_ios; + uint64_t write_ios; + uint32_t resident_pages; + uint32_t dirty_pages; + size_t memory_bytes; +} page_cache_stats_t; + +void page_cache_init(void); + +// Read/write through the cache. Offsets and lengths are arbitrary; the cache +// splits them across pages internally. Both return bytes transferred. +size_t page_cache_read(const char* dev_name, uint64_t offset, void* buf, size_t len); +size_t page_cache_write(const char* dev_name, uint64_t offset, const void* buf, size_t len); + +// Borrow a page directly (pinned until page_cache_release). +page_cache_entry_t* page_cache_get(const char* dev_name, uint64_t page_index); +void page_cache_release(page_cache_entry_t* entry); +void page_cache_mark_dirty(page_cache_entry_t* entry); + +int page_cache_sync(void); // Flush every dirty page +int page_cache_sync_dev(const char* dev_name); // Flush one device +int page_cache_invalidate(const char* dev_name); // Drop clean pages +void page_cache_drop_all(void); + +page_cache_stats_t page_cache_get_stats(void); +uint32_t page_cache_hit_percent(void); +void page_cache_dump(void); + +#endif // _MM_PAGE_CACHE_H diff --git a/include/userland/coreutils.h b/include/userland/coreutils.h new file mode 100644 index 0000000..36f519d --- /dev/null +++ b/include/userland/coreutils.h @@ -0,0 +1,22 @@ +#ifndef _USERLAND_COREUTILS_H +#define _USERLAND_COREUTILS_H + +// GNU-style text-processing applets for LazyBox. Each entry point matches +// applet_fn_t so it can be placed directly in the LazyBox dispatch table. + +int coreutils_sort(int argc, char** argv); +int coreutils_uniq(int argc, char** argv); +int coreutils_cut(int argc, char** argv); +int coreutils_tr(int argc, char** argv); +int coreutils_rev(int argc, char** argv); +int coreutils_tac(int argc, char** argv); +int coreutils_nl(int argc, char** argv); +int coreutils_seq(int argc, char** argv); +int coreutils_diff(int argc, char** argv); +int coreutils_xxd(int argc, char** argv); +int coreutils_du(int argc, char** argv); +int coreutils_factor(int argc, char** argv); +int coreutils_sum(int argc, char** argv); +int coreutils_truncate(int argc, char** argv); + +#endif // _USERLAND_COREUTILS_H diff --git a/kernel/futex.c b/kernel/futex.c new file mode 100644 index 0000000..26b12cf --- /dev/null +++ b/kernel/futex.c @@ -0,0 +1,159 @@ +// Fast Userspace muTEX (futex) hash table for SUB-OS +#include +#include +#include +#include + +typedef struct { + wait_queue_t wq; + volatile uint32_t* addr; // Address currently owning this bucket slot + uint32_t waiters; +} futex_bucket_t; + +static futex_bucket_t g_buckets[FUTEX_BUCKETS]; +static futex_stats_t g_stats; +static bool g_initialized = false; + +// Futex words are naturally 4-byte aligned, so drop the two dead low bits +// before mixing to avoid clustering every key onto a quarter of the buckets. +static size_t futex_hash(volatile uint32_t* uaddr) { + uintptr_t key = (uintptr_t)uaddr >> 2; + key ^= key >> 16; + key *= 0x2545F491U; + key ^= key >> 13; + return (size_t)(key & (FUTEX_BUCKETS - 1)); +} + +void futex_init(void) { + memset(&g_stats, 0, sizeof(g_stats)); + + for (int i = 0; i < FUTEX_BUCKETS; i++) { + char name[24]; + snprintf(name, sizeof(name), "futex-%d", i); + wait_queue_init(&g_buckets[i].wq, name); + g_buckets[i].addr = NULL; + g_buckets[i].waiters = 0; + } + + g_initialized = true; +} + +int futex_wait(volatile uint32_t* uaddr, uint32_t expected, uint64_t timeout_ticks) { + if (!g_initialized || !uaddr) return -1; + + // Re-check under the bucket before parking: if the word already moved on, + // the wakeup we would sleep for has already happened. + if (*uaddr != expected) { + g_stats.spurious++; + return -1; + } + + futex_bucket_t* b = &g_buckets[futex_hash(uaddr)]; + b->addr = uaddr; + b->waiters++; + g_stats.waits++; + g_stats.active_waiters++; + + int rc = (timeout_ticks > 0) + ? wait_event_timeout(&b->wq, timeout_ticks) + : wait_event(&b->wq); + + if (b->waiters > 0) b->waiters--; + if (g_stats.active_waiters > 0) g_stats.active_waiters--; + if (rc == 1) g_stats.timeouts++; + + return rc; +} + +int futex_wake(volatile uint32_t* uaddr, int count) { + if (!g_initialized || !uaddr) return 0; + + futex_bucket_t* b = &g_buckets[futex_hash(uaddr)]; + int woken = (count <= 1) ? wake_up(&b->wq) : wake_up_all(&b->wq); + + g_stats.wakes += (uint64_t)woken; + return woken; +} + +int futex_requeue(volatile uint32_t* uaddr1, volatile uint32_t* uaddr2, int count) { + if (!g_initialized || !uaddr1 || !uaddr2) return 0; + + futex_bucket_t* src = &g_buckets[futex_hash(uaddr1)]; + futex_bucket_t* dst = &g_buckets[futex_hash(uaddr2)]; + if (src == dst) return 0; + + uint32_t moved = src->waiters; + if (count > 0 && moved > (uint32_t)count) moved = (uint32_t)count; + + src->waiters -= moved; + dst->waiters += moved; + dst->addr = uaddr2; + g_stats.requeues += moved; + + // Sleepers are handed to the destination queue by waking them there; the + // caller's protocol has them re-test their predicate on the new address. + for (uint32_t i = 0; i < moved; i++) wake_up(&src->wq); + + return (int)moved; +} + +futex_stats_t futex_get_stats(void) { + return g_stats; +} + +void futex_dump(void) { + printk(ANSI_BRIGHT_CYAN "=== SUB-OS Futex Subsystem (%d buckets) ===\n" ANSI_RESET, + FUTEX_BUCKETS); + printk(" Waits : %llu\n", (unsigned long long)g_stats.waits); + printk(" Wakes : %llu\n", (unsigned long long)g_stats.wakes); + printk(" Requeues : %llu\n", (unsigned long long)g_stats.requeues); + printk(" Timeouts : %llu\n", (unsigned long long)g_stats.timeouts); + printk(" Fastpath retries (value changed): %llu\n", + (unsigned long long)g_stats.spurious); + printk(" Active waiters: %u\n\n", g_stats.active_waiters); + + int occupied = 0; + for (int i = 0; i < FUTEX_BUCKETS; i++) { + if (g_buckets[i].waiters > 0) { + printk(" bucket[%02d] addr=%p waiters=%u\n", + i, (void*)g_buckets[i].addr, g_buckets[i].waiters); + occupied++; + } + } + if (occupied == 0) printk(ANSI_BRIGHT_BLACK " (no contended futexes)\n" ANSI_RESET); +} + +// --------------------------------------------------------------------------- +// Futex-backed sleeping mutex +// --------------------------------------------------------------------------- + +void fmutex_init(fmutex_t* m, const char* name) { + if (!m) return; + m->state = 0; + strncpy(m->name, name ? name : "fmutex", sizeof(m->name) - 1); + m->name[sizeof(m->name) - 1] = '\0'; +} + +bool fmutex_trylock(fmutex_t* m) { + if (!m) return false; + uint32_t expected = 0; + return __atomic_compare_exchange_n(&m->state, &expected, 1, false, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED); +} + +void fmutex_lock(fmutex_t* m) { + if (!m) return; + if (fmutex_trylock(m)) return; + + // Contended: publish state 2 so the unlocker knows a wake is required. + while (__atomic_exchange_n(&m->state, 2, __ATOMIC_ACQUIRE) != 0) { + futex_wait(&m->state, 2, 100); + } +} + +void fmutex_unlock(fmutex_t* m) { + if (!m) return; + if (__atomic_exchange_n(&m->state, 0, __ATOMIC_RELEASE) == 2) { + futex_wake(&m->state, 1); + } +} diff --git a/kernel/ktest.c b/kernel/ktest.c new file mode 100644 index 0000000..c0ee5f7 --- /dev/null +++ b/kernel/ktest.c @@ -0,0 +1,703 @@ +// In-kernel self-test harness for SUB-OS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const ktest_suite_t* g_suites[KTEST_MAX_SUITES]; +static int g_suite_count = 0; +static bool g_verbose = true; + +void ktest_init(void) { + g_suite_count = 0; + memset(g_suites, 0, sizeof(g_suites)); +} + +int ktest_register_suite(const ktest_suite_t* suite) { + if (!suite || !suite->name || g_suite_count >= KTEST_MAX_SUITES) return -1; + g_suites[g_suite_count++] = suite; + return 0; +} + +void ktest_report(ktest_context_t* ctx, bool passed, const char* expr, + const char* file, int line, const char* detail) { + if (!ctx) return; + + ctx->assertions++; + if (passed) return; + + ctx->failures++; + printk(ANSI_BRIGHT_RED " FAIL " ANSI_RESET "%s:%d: %s", file, line, expr); + if (detail) printk(" [%s]", detail); + printk("\n"); +} + +void ktest_expect_eq(ktest_context_t* ctx, long long a, long long b, + const char* expr, const char* file, int line) { + char detail[64]; + if (a != b) { + snprintf(detail, sizeof(detail), "got %lld, want %lld", a, b); + ktest_report(ctx, false, expr, file, line, detail); + } else { + ktest_report(ctx, true, expr, file, line, NULL); + } +} + +void ktest_expect_streq(ktest_context_t* ctx, const char* a, const char* b, + const char* expr, const char* file, int line) { + bool eq = (a && b) ? (strcmp(a, b) == 0) : (a == b); + if (!eq) { + char detail[96]; + snprintf(detail, sizeof(detail), "got \"%s\", want \"%s\"", + a ? a : "(null)", b ? b : "(null)"); + ktest_report(ctx, false, expr, file, line, detail); + } else { + ktest_report(ctx, true, expr, file, line, NULL); + } +} + +static void run_suite(const ktest_suite_t* suite, ktest_result_t* res) { + printk(ANSI_BRIGHT_CYAN " [SUITE] %s" ANSI_RESET " (%llu tests)\n", + suite->name, (unsigned long long)suite->case_count); + res->suites_run++; + + for (size_t i = 0; i < suite->case_count; i++) { + const ktest_case_t* tc = &suite->cases[i]; + if (!tc->func) continue; + + ktest_context_t ctx = { + .suite = suite->name, + .test = tc->name, + .assertions = 0, + .failures = 0, + .aborted = false + }; + + tc->func(&ctx); + + res->tests_run++; + res->assertions += ctx.assertions; + res->assertion_failures += ctx.failures; + + if (ctx.failures == 0) { + res->tests_passed++; + if (g_verbose) { + printk(" " ANSI_BRIGHT_GREEN "PASS" ANSI_RESET " %-32s (%u assertions)\n", + tc->name, ctx.assertions); + } + } else { + res->tests_failed++; + printk(" " ANSI_BRIGHT_RED "FAIL" ANSI_RESET " %-32s (%u/%u assertions failed)\n", + tc->name, ctx.failures, ctx.assertions); + } + } +} + +static void print_summary(const ktest_result_t* res) { + printk("\n" ANSI_BRIGHT_CYAN "=== KTest Summary ===\n" ANSI_RESET); + printk(" Suites : %u\n", res->suites_run); + printk(" Tests : %u run, " ANSI_BRIGHT_GREEN "%u passed" ANSI_RESET + ", %s%u failed" ANSI_RESET "\n", + res->tests_run, res->tests_passed, + res->tests_failed ? ANSI_BRIGHT_RED : ANSI_BRIGHT_BLACK, + res->tests_failed); + printk(" Assertions : %u checked, %u failed\n", + res->assertions, res->assertion_failures); + printk(" Elapsed : %llu ticks (%llu ms)\n", + (unsigned long long)res->elapsed_ticks, + (unsigned long long)(res->elapsed_ticks * 10)); + + if (res->tests_failed == 0) { + printk(ANSI_BRIGHT_GREEN " RESULT: ALL TESTS PASSED\n" ANSI_RESET); + } else { + printk(ANSI_BRIGHT_RED " RESULT: %u TEST(S) FAILED\n" ANSI_RESET, res->tests_failed); + } +} + +int ktest_run_all(ktest_result_t* result_out) { + ktest_result_t res; + memset(&res, 0, sizeof(res)); + + uint64_t start = pit_get_ticks(); + printk(ANSI_BRIGHT_CYAN "=== SUB-OS In-Kernel Test Harness (%d suites) ===\n\n" ANSI_RESET, + g_suite_count); + + for (int i = 0; i < g_suite_count; i++) { + if (g_suites[i]) run_suite(g_suites[i], &res); + } + + res.elapsed_ticks = pit_get_ticks() - start; + print_summary(&res); + + if (result_out) *result_out = res; + return (int)res.tests_failed; +} + +int ktest_run_suite(const char* name, ktest_result_t* result_out) { + if (!name) return ktest_run_all(result_out); + + ktest_result_t res; + memset(&res, 0, sizeof(res)); + uint64_t start = pit_get_ticks(); + + bool found = false; + for (int i = 0; i < g_suite_count; i++) { + if (g_suites[i] && strcmp(g_suites[i]->name, name) == 0) { + run_suite(g_suites[i], &res); + found = true; + break; + } + } + + if (!found) { + printk(ANSI_YELLOW "ktest: no suite named '%s'\n" ANSI_RESET, name); + ktest_list_suites(); + return -1; + } + + res.elapsed_ticks = pit_get_ticks() - start; + print_summary(&res); + + if (result_out) *result_out = res; + return (int)res.tests_failed; +} + +void ktest_list_suites(void) { + printk(ANSI_BRIGHT_CYAN "Registered KTest suites (%d):\n" ANSI_RESET, g_suite_count); + for (int i = 0; i < g_suite_count; i++) { + if (!g_suites[i]) continue; + printk(" " ANSI_GREEN "%-20s" ANSI_RESET " %llu test cases\n", + g_suites[i]->name, (unsigned long long)g_suites[i]->case_count); + } +} + +// =========================================================================== +// Built-in suite: lib/rbtree +// =========================================================================== +typedef struct { + struct rb_node node; + int key; +} test_rb_item_t; + +static void rb_test_insert(struct rb_root* root, test_rb_item_t* item) { + struct rb_node** link = &root->rb_node; + struct rb_node* parent = NULL; + + while (*link) { + parent = *link; + test_rb_item_t* cur = rb_entry(parent, test_rb_item_t, node); + link = (item->key < cur->key) ? &parent->rb_left : &parent->rb_right; + } + + rb_link_node(&item->node, parent, link); + rb_insert_color(&item->node, root); +} + +static void test_rbtree_ordered_insert(ktest_context_t* ctx) { + struct rb_root root = RB_ROOT_INIT; + static test_rb_item_t items[64]; + + // Ascending keys are the worst case for an unbalanced BST. + for (int i = 0; i < 64; i++) { + items[i].key = i; + rb_test_insert(&root, &items[i]); + } + + KTEST_ASSERT_EQ(ctx, (long long)rb_count(&root), 64); + KTEST_ASSERT(ctx, rb_validate(&root)); + + int prev = -1; + int seen = 0; + for (struct rb_node* n = rb_first(&root); n; n = rb_next(n)) { + test_rb_item_t* it = rb_entry(n, test_rb_item_t, node); + KTEST_ASSERT(ctx, it->key > prev); + prev = it->key; + seen++; + } + KTEST_ASSERT_EQ(ctx, seen, 64); + + // A balanced 64-node tree must stay far below the depth of a linked list. + KTEST_ASSERT(ctx, rb_black_height(&root) <= 8); +} + +static void test_rbtree_erase(ktest_context_t* ctx) { + struct rb_root root = RB_ROOT_INIT; + static test_rb_item_t items[32]; + + // Pseudo-random insertion order via a coprime stride. + for (int i = 0; i < 32; i++) { + items[i].key = (i * 7) % 32; + rb_test_insert(&root, &items[i]); + } + KTEST_ASSERT_EQ(ctx, (long long)rb_count(&root), 32); + + for (int i = 0; i < 32; i += 2) { + rb_erase(&items[i].node, &root); + KTEST_ASSERT(ctx, rb_validate(&root)); + } + KTEST_ASSERT_EQ(ctx, (long long)rb_count(&root), 16); + + int prev = -1; + for (struct rb_node* n = rb_first(&root); n; n = rb_next(n)) { + test_rb_item_t* it = rb_entry(n, test_rb_item_t, node); + KTEST_ASSERT(ctx, it->key > prev); + prev = it->key; + } + + while (!rb_empty(&root)) { + rb_erase(rb_first(&root), &root); + } + KTEST_ASSERT_EQ(ctx, (long long)rb_count(&root), 0); +} + +static void test_rbtree_reverse_walk(ktest_context_t* ctx) { + struct rb_root root = RB_ROOT_INIT; + static test_rb_item_t items[16]; + + for (int i = 0; i < 16; i++) { + items[i].key = 15 - i; + rb_test_insert(&root, &items[i]); + } + + struct rb_node* first = rb_first(&root); + struct rb_node* last = rb_last(&root); + KTEST_ASSERT_NOT_NULL(ctx, first); + KTEST_ASSERT_NOT_NULL(ctx, last); + KTEST_ASSERT_EQ(ctx, rb_entry(first, test_rb_item_t, node)->key, 0); + KTEST_ASSERT_EQ(ctx, rb_entry(last, test_rb_item_t, node)->key, 15); + + int expected = 15; + for (struct rb_node* n = last; n; n = rb_prev(n)) { + KTEST_ASSERT_EQ(ctx, rb_entry(n, test_rb_item_t, node)->key, expected); + expected--; + } + KTEST_ASSERT_EQ(ctx, expected, -1); +} + +static const ktest_case_t rbtree_cases[] = { + { "ordered_insert_stays_balanced", test_rbtree_ordered_insert }, + { "erase_preserves_invariants", test_rbtree_erase }, + { "forward_and_reverse_walk", test_rbtree_reverse_walk }, +}; +static const ktest_suite_t rbtree_suite = { "rbtree", rbtree_cases, 3 }; + +// =========================================================================== +// Built-in suite: lib/kfifo +// =========================================================================== +static void test_kfifo_roundtrip(ktest_context_t* ctx) { + kfifo_t fifo; + KTEST_ASSERT_EQ(ctx, kfifo_alloc(&fifo, 64), 0); + KTEST_ASSERT(ctx, kfifo_is_empty(&fifo)); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_avail(&fifo), 64); + + const char* msg = "SUB-OS kfifo"; + size_t len = strlen(msg); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_in(&fifo, msg, len), (long long)len); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_len(&fifo), (long long)len); + + char out[32]; + memset(out, 0, sizeof(out)); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_out(&fifo, out, len), (long long)len); + KTEST_ASSERT_STREQ(ctx, out, msg); + KTEST_ASSERT(ctx, kfifo_is_empty(&fifo)); + + kfifo_free(&fifo); +} + +static void test_kfifo_wraparound(ktest_context_t* ctx) { + kfifo_t fifo; + KTEST_ASSERT_EQ(ctx, kfifo_alloc(&fifo, 16), 0); + + // Drive the indices well past the buffer end to exercise the wrap path. + uint8_t byte; + for (int round = 0; round < 40; round++) { + KTEST_ASSERT_EQ(ctx, kfifo_put_byte(&fifo, (uint8_t)round), 0); + KTEST_ASSERT_EQ(ctx, kfifo_get_byte(&fifo, &byte), 0); + KTEST_ASSERT_EQ(ctx, byte, (uint8_t)round); + } + KTEST_ASSERT(ctx, kfifo_is_empty(&fifo)); + + uint8_t src[24]; + for (int i = 0; i < 24; i++) src[i] = (uint8_t)(i + 1); + + // Only 16 bytes fit; the rest must be refused rather than overwrite. + KTEST_ASSERT_EQ(ctx, (long long)kfifo_in(&fifo, src, 24), 16); + KTEST_ASSERT(ctx, kfifo_is_full(&fifo)); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_in(&fifo, src, 4), 0); + + uint8_t dst[16]; + KTEST_ASSERT_EQ(ctx, (long long)kfifo_out(&fifo, dst, 16), 16); + for (int i = 0; i < 16; i++) KTEST_ASSERT_EQ(ctx, dst[i], src[i]); + + kfifo_free(&fifo); +} + +static void test_kfifo_peek(ktest_context_t* ctx) { + kfifo_t fifo; + KTEST_ASSERT_EQ(ctx, kfifo_alloc(&fifo, 32), 0); + + kfifo_in(&fifo, "abcdef", 6); + + char peeked[8]; + memset(peeked, 0, sizeof(peeked)); + KTEST_ASSERT_EQ(ctx, (long long)kfifo_peek(&fifo, peeked, 3), 3); + KTEST_ASSERT_STREQ(ctx, peeked, "abc"); + + // Peeking must not consume. + KTEST_ASSERT_EQ(ctx, (long long)kfifo_len(&fifo), 6); + + kfifo_free(&fifo); +} + +static const ktest_case_t kfifo_cases[] = { + { "in_out_roundtrip", test_kfifo_roundtrip }, + { "index_wraparound", test_kfifo_wraparound }, + { "peek_is_read_only", test_kfifo_peek }, +}; +static const ktest_suite_t kfifo_suite = { "kfifo", kfifo_cases, 3 }; + +// =========================================================================== +// Built-in suite: lib/hashtable +// =========================================================================== +static void test_hashtable_string_keys(ktest_context_t* ctx) { + hashtable_t* ht = hashtable_create(8, true); + KTEST_ASSERT_NOT_NULL(ctx, ht); + if (!ht) return; + + static int values[3] = { 10, 20, 30 }; + KTEST_ASSERT_EQ(ctx, hashtable_put(ht, "alpha", &values[0]), 0); + KTEST_ASSERT_EQ(ctx, hashtable_put(ht, "beta", &values[1]), 0); + KTEST_ASSERT_EQ(ctx, hashtable_put(ht, "gamma", &values[2]), 0); + KTEST_ASSERT_EQ(ctx, (long long)hashtable_size(ht), 3); + + KTEST_ASSERT_EQ(ctx, *(int*)hashtable_get(ht, "beta"), 20); + KTEST_ASSERT(ctx, hashtable_get(ht, "delta") == NULL); + + // Re-putting an existing key replaces rather than duplicates. + KTEST_ASSERT_EQ(ctx, hashtable_put(ht, "beta", &values[2]), 0); + KTEST_ASSERT_EQ(ctx, (long long)hashtable_size(ht), 3); + KTEST_ASSERT_EQ(ctx, *(int*)hashtable_get(ht, "beta"), 30); + + KTEST_ASSERT(ctx, hashtable_remove(ht, "alpha")); + KTEST_ASSERT(ctx, !hashtable_remove(ht, "alpha")); + KTEST_ASSERT_EQ(ctx, (long long)hashtable_size(ht), 2); + + hashtable_destroy(ht); +} + +static void test_hashtable_growth(ktest_context_t* ctx) { + hashtable_t* ht = hashtable_create(8, false); + KTEST_ASSERT_NOT_NULL(ctx, ht); + if (!ht) return; + + size_t initial_buckets = hashtable_buckets(ht); + + static int slots[128]; + for (int i = 0; i < 128; i++) { + slots[i] = i * 3; + KTEST_ASSERT_EQ(ctx, hashtable_put_u64(ht, (uint64_t)i, &slots[i]), 0); + } + + KTEST_ASSERT_EQ(ctx, (long long)hashtable_size(ht), 128); + KTEST_ASSERT(ctx, hashtable_buckets(ht) > initial_buckets); + + for (int i = 0; i < 128; i++) { + int* v = (int*)hashtable_get_u64(ht, (uint64_t)i); + KTEST_ASSERT_NOT_NULL(ctx, v); + if (v) KTEST_ASSERT_EQ(ctx, *v, i * 3); + } + + // With growth keeping the load factor under 3/4, chains stay short. + KTEST_ASSERT(ctx, hashtable_longest_chain(ht) <= 8); + + hashtable_destroy(ht); +} + +static void test_hash_functions(ktest_context_t* ctx) { + // FNV-1a must be deterministic and separate near-identical inputs. + KTEST_ASSERT(ctx, hash_string("subos") == hash_string("subos")); + KTEST_ASSERT(ctx, hash_string("subos") != hash_string("subon")); + KTEST_ASSERT(ctx, hash_u64(1) != hash_u64(2)); + KTEST_ASSERT(ctx, hash_u64(0x100000000ULL) != hash_u64(0)); + KTEST_ASSERT_EQ(ctx, (long long)hash_string(NULL), 0); +} + +static const ktest_case_t hashtable_cases[] = { + { "string_key_crud", test_hashtable_string_keys }, + { "rehash_on_growth", test_hashtable_growth }, + { "fnv1a_distribution", test_hash_functions }, +}; +static const ktest_suite_t hashtable_suite = { "hashtable", hashtable_cases, 3 }; + +// =========================================================================== +// Built-in suite: kernel/rcu + kernel/futex +// =========================================================================== +static int g_rcu_cb_hits = 0; + +static void rcu_test_cb(void* arg) { + g_rcu_cb_hits += (arg ? *(int*)arg : 1); +} + +static void test_rcu_read_side(ktest_context_t* ctx) { + KTEST_ASSERT(ctx, !rcu_read_lock_held()); + + rcu_read_lock(); + KTEST_ASSERT(ctx, rcu_read_lock_held()); + rcu_read_lock(); // Nesting is allowed + KTEST_ASSERT(ctx, rcu_read_lock_held()); + rcu_read_unlock(); + KTEST_ASSERT(ctx, rcu_read_lock_held()); + rcu_read_unlock(); + + KTEST_ASSERT(ctx, !rcu_read_lock_held()); +} + +static void test_rcu_deferred_free(ktest_context_t* ctx) { + static int weight = 5; + g_rcu_cb_hits = 0; + + uint64_t gp_before = rcu_get_grace_period(); + KTEST_ASSERT_EQ(ctx, call_rcu(rcu_test_cb, &weight), 0); + + // The callback must not fire while a reader is still inside. + rcu_read_lock(); + rcu_process_callbacks(); + KTEST_ASSERT_EQ(ctx, g_rcu_cb_hits, 0); + rcu_read_unlock(); + + rcu_synchronize(); + rcu_process_callbacks(); + + KTEST_ASSERT_EQ(ctx, g_rcu_cb_hits, 5); + KTEST_ASSERT(ctx, rcu_get_grace_period() > gp_before); +} + +static void test_rcu_pointer_publish(ktest_context_t* ctx) { + static int old_value = 1; + static int new_value = 2; + int* shared = &old_value; + + rcu_read_lock(); + int* snapshot = rcu_dereference(shared); + KTEST_ASSERT_EQ(ctx, *snapshot, 1); + rcu_read_unlock(); + + rcu_assign_pointer(shared, &new_value); + + rcu_read_lock(); + KTEST_ASSERT_EQ(ctx, *rcu_dereference(shared), 2); + rcu_read_unlock(); +} + +static void test_futex_fastpath(ktest_context_t* ctx) { + fmutex_t m; + fmutex_init(&m, "ktest-mutex"); + + KTEST_ASSERT(ctx, fmutex_trylock(&m)); + KTEST_ASSERT_EQ(ctx, (long long)m.state, 1); + + // A second acquire on an already-held mutex must fail without blocking. + KTEST_ASSERT(ctx, !fmutex_trylock(&m)); + + fmutex_unlock(&m); + KTEST_ASSERT_EQ(ctx, (long long)m.state, 0); + KTEST_ASSERT(ctx, fmutex_trylock(&m)); + fmutex_unlock(&m); +} + +static void test_futex_value_check(ktest_context_t* ctx) { + volatile uint32_t word = 7; + + // Waiting on a value that no longer matches returns immediately so the + // caller can retry its fast path instead of sleeping on a stale wakeup. + KTEST_ASSERT_EQ(ctx, futex_wait(&word, 99, 5), -1); + + // Nothing is parked, so a wake finds no one. + KTEST_ASSERT_EQ(ctx, futex_wake(&word, 1), 0); + + futex_stats_t s = futex_get_stats(); + KTEST_ASSERT(ctx, s.spurious > 0); +} + +static void test_wait_queue_wake(ktest_context_t* ctx) { + wait_queue_t wq; + wait_queue_init(&wq, "ktest-wq"); + + KTEST_ASSERT_EQ(ctx, (long long)wait_queue_waiters(&wq), 0); + // With no sleepers a wake is a no-op rather than an error. + KTEST_ASSERT_EQ(ctx, wake_up(&wq), 0); + KTEST_ASSERT_EQ(ctx, wake_up_all(&wq), 0); + + completion_t c; + completion_init(&c, "ktest-completion"); + KTEST_ASSERT(ctx, !completion_done(&c)); + complete(&c); + KTEST_ASSERT(ctx, completion_done(&c)); + + // Waiting on an already-completed barrier returns at once. + KTEST_ASSERT_EQ(ctx, wait_for_completion_timeout(&c, 10), 0); +} + +static const ktest_case_t concurrency_cases[] = { + { "rcu_read_side_nesting", test_rcu_read_side }, + { "rcu_deferred_callback", test_rcu_deferred_free }, + { "rcu_pointer_publish", test_rcu_pointer_publish }, + { "futex_mutex_fastpath", test_futex_fastpath }, + { "futex_value_recheck", test_futex_value_check }, + { "waitqueue_and_completion",test_wait_queue_wake }, +}; +static const ktest_suite_t concurrency_suite = { "concurrency", concurrency_cases, 6 }; + +// =========================================================================== +// Built-in suite: mm/page_cache +// =========================================================================== +static void test_page_cache_write_read(ktest_context_t* ctx) { + // ramdisk0 is registered during boot and is always writable. + const char* dev = "ramdisk0"; + + uint8_t pattern[512]; + for (int i = 0; i < 512; i++) pattern[i] = (uint8_t)(i ^ 0x5A); + + size_t written = page_cache_write(dev, 8192, pattern, sizeof(pattern)); + if (written == 0) { + // No block device available in this configuration; skip gracefully. + KTEST_ASSERT(ctx, true); + return; + } + KTEST_ASSERT_EQ(ctx, (long long)written, 512); + + uint8_t readback[512]; + memset(readback, 0, sizeof(readback)); + KTEST_ASSERT_EQ(ctx, (long long)page_cache_read(dev, 8192, readback, 512), 512); + KTEST_ASSERT_EQ(ctx, memcmp(pattern, readback, 512), 0); +} + +static void test_page_cache_hit_accounting(ktest_context_t* ctx) { + const char* dev = "ramdisk0"; + + page_cache_stats_t before = page_cache_get_stats(); + + uint8_t buf[64]; + page_cache_read(dev, 0, buf, sizeof(buf)); // Likely a miss + for (int i = 0; i < 8; i++) { + page_cache_read(dev, 0, buf, sizeof(buf)); // Same page: must hit + } + + page_cache_stats_t after = page_cache_get_stats(); + KTEST_ASSERT(ctx, after.hits >= before.hits + 8); + KTEST_ASSERT(ctx, page_cache_hit_percent() <= 100); +} + +static void test_page_cache_spanning_write(ktest_context_t* ctx) { + const char* dev = "ramdisk0"; + + // Straddle a page boundary so the split path is exercised. + uint64_t offset = PAGE_CACHE_BLOCK_SIZE - 100; + uint8_t src[300]; + for (int i = 0; i < 300; i++) src[i] = (uint8_t)(i + 3); + + size_t written = page_cache_write(dev, offset, src, sizeof(src)); + if (written == 0) { + KTEST_ASSERT(ctx, true); + return; + } + KTEST_ASSERT_EQ(ctx, (long long)written, 300); + + uint8_t dst[300]; + memset(dst, 0, sizeof(dst)); + KTEST_ASSERT_EQ(ctx, (long long)page_cache_read(dev, offset, dst, 300), 300); + KTEST_ASSERT_EQ(ctx, memcmp(src, dst, 300), 0); + + KTEST_ASSERT(ctx, page_cache_sync() >= 0); +} + +static const ktest_case_t page_cache_cases[] = { + { "write_then_read_back", test_page_cache_write_read }, + { "hit_rate_accounting", test_page_cache_hit_accounting }, + { "page_spanning_transfer", test_page_cache_spanning_write }, +}; +static const ktest_suite_t page_cache_suite = { "page_cache", page_cache_cases, 3 }; + +// =========================================================================== +// Built-in suite: lib/string + mm/kmalloc regression guards +// =========================================================================== +static void test_string_primitives(ktest_context_t* ctx) { + char buf[32]; + + strcpy(buf, "SUB-OS"); + KTEST_ASSERT_EQ(ctx, (long long)strlen(buf), 6); + KTEST_ASSERT_STREQ(ctx, buf, "SUB-OS"); + + strcat(buf, "/kernel"); + KTEST_ASSERT_STREQ(ctx, buf, "SUB-OS/kernel"); + + KTEST_ASSERT(ctx, strchr(buf, '/') != NULL); + KTEST_ASSERT(ctx, strstr(buf, "kernel") != NULL); + KTEST_ASSERT(ctx, strstr(buf, "absent") == NULL); + KTEST_ASSERT_EQ(ctx, strncmp(buf, "SUB", 3), 0); + + KTEST_ASSERT_EQ(ctx, atoi("-1234"), -1234); + KTEST_ASSERT_EQ(ctx, atoi("0"), 0); +} + +static void test_snprintf_truncation(ktest_context_t* ctx) { + char small[8]; + + snprintf(small, sizeof(small), "%s-%d", "abcdefgh", 42); + // Truncation must still leave a NUL inside the buffer. + KTEST_ASSERT(ctx, strlen(small) < sizeof(small)); + + char wide[64]; + snprintf(wide, sizeof(wide), "%u:%s:%d", 7u, "ok", -3); + KTEST_ASSERT_STREQ(ctx, wide, "7:ok:-3"); +} + +static void test_kmalloc_roundtrip(ktest_context_t* ctx) { + size_t used_before = heap_get_used_bytes(); + + void* a = kmalloc(128); + void* b = kzalloc(256); + KTEST_ASSERT_NOT_NULL(ctx, a); + KTEST_ASSERT_NOT_NULL(ctx, b); + KTEST_ASSERT(ctx, a != b); + + if (b) { + // kzalloc must hand back zeroed memory. + uint8_t* p = (uint8_t*)b; + int nonzero = 0; + for (int i = 0; i < 256; i++) if (p[i]) nonzero++; + KTEST_ASSERT_EQ(ctx, nonzero, 0); + } + + KTEST_ASSERT(ctx, heap_get_used_bytes() > used_before); + + kfree(a); + kfree(b); + kfree(NULL); // Must be a safe no-op + KTEST_ASSERT(ctx, true); +} + +static const ktest_case_t libcore_cases[] = { + { "string_primitives", test_string_primitives }, + { "snprintf_truncation", test_snprintf_truncation }, + { "kmalloc_roundtrip", test_kmalloc_roundtrip }, +}; +static const ktest_suite_t libcore_suite = { "libcore", libcore_cases, 3 }; + +void ktest_register_builtin_suites(void) { + ktest_register_suite(&rbtree_suite); + ktest_register_suite(&kfifo_suite); + ktest_register_suite(&hashtable_suite); + ktest_register_suite(&concurrency_suite); + ktest_register_suite(&page_cache_suite); + ktest_register_suite(&libcore_suite); +} diff --git a/kernel/main.c b/kernel/main.c index adf8819..49864e2 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -14,6 +14,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -94,6 +99,7 @@ static void subos_modular_core_boot(void) { // Storage & Block Layer printk(KERN_INFO "[4/14] Initializing Block Layer & Storage Controllers... "); block_init(); + page_cache_init(); ramdisk_init(); #if defined(__x86_64__) ata_init(); @@ -170,6 +176,13 @@ static void subos_modular_core_boot(void) { namespace_init(); printk(ANSI_BRIGHT_GREEN "OK\n" ANSI_RESET); + // Concurrency Primitives: Wait Queues, Futexes & Tiny RCU + printk(KERN_INFO "[9/14] Initializing Wait Queues, Futex Hash & Tiny RCU... "); + wait_subsystem_init(); + futex_init(); + rcu_init(); + printk(ANSI_BRIGHT_GREEN "OK\n" ANSI_RESET); + // Inter-Process Communication printk(KERN_INFO "[9/14] Initializing IPC Engine (Pipes, MsgQueues, POSIX SHM, Semaphores)... "); ipc_init(); @@ -209,6 +222,8 @@ static void subos_modular_core_boot(void) { printk(KERN_INFO "[13/14] Initializing Preemptive Task Scheduler & LazyBox... "); sched_init(); lazybox_init(); + ktest_init(); + ktest_register_builtin_suites(); printk(ANSI_BRIGHT_GREEN "OK\n" ANSI_RESET); // Hardware Interrupts diff --git a/kernel/rcu.c b/kernel/rcu.c new file mode 100644 index 0000000..ff9d0bd --- /dev/null +++ b/kernel/rcu.c @@ -0,0 +1,169 @@ +// Tiny RCU implementation for SUB-OS +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + rcu_callback_t func; + void* arg; + uint64_t gp_target; // Grace period this callback is waiting on + bool in_use; +} rcu_head_t; + +static rcu_head_t g_callbacks[RCU_MAX_CALLBACKS]; +static spinlock_t g_rcu_lock = SPINLOCK_INIT; +static rcu_stats_t g_stats; +static uint64_t g_grace_period = 0; +static uint32_t g_reader_nesting = 0; +static bool g_initialized = false; + +static void rcu_free_cb(void* arg) { + if (arg) kfree(arg); +} + +void rcu_init(void) { + spin_lock(&g_rcu_lock); + memset(g_callbacks, 0, sizeof(g_callbacks)); + memset(&g_stats, 0, sizeof(g_stats)); + g_grace_period = 1; + g_reader_nesting = 0; + g_initialized = true; + spin_unlock(&g_rcu_lock); +} + +void rcu_read_lock(void) { + __atomic_fetch_add(&g_reader_nesting, 1, __ATOMIC_ACQUIRE); + g_stats.read_locks++; +} + +void rcu_read_unlock(void) { + if (__atomic_load_n(&g_reader_nesting, __ATOMIC_RELAXED) > 0) { + __atomic_fetch_sub(&g_reader_nesting, 1, __ATOMIC_RELEASE); + } +} + +bool rcu_read_lock_held(void) { + return __atomic_load_n(&g_reader_nesting, __ATOMIC_RELAXED) > 0; +} + +uint64_t rcu_get_grace_period(void) { + return g_grace_period; +} + +// A grace period ends once no reader is inside a critical section. On this +// cooperative kernel that is observable directly from the nesting counter. +static bool grace_period_elapsed(void) { + return __atomic_load_n(&g_reader_nesting, __ATOMIC_ACQUIRE) == 0; +} + +void rcu_process_callbacks(void) { + if (!g_initialized) return; + if (!grace_period_elapsed()) return; + + spin_lock(&g_rcu_lock); + uint64_t completed = ++g_grace_period; + g_stats.grace_periods++; + + // Snapshot the ready callbacks, then run them outside the lock so a + // reclaimer is free to allocate, free or queue more RCU work. + rcu_head_t ready[RCU_MAX_CALLBACKS]; + int nready = 0; + + for (int i = 0; i < RCU_MAX_CALLBACKS; i++) { + if (g_callbacks[i].in_use && completed >= g_callbacks[i].gp_target) { + ready[nready++] = g_callbacks[i]; + g_callbacks[i].in_use = false; + if (g_stats.pending > 0) g_stats.pending--; + } + } + spin_unlock(&g_rcu_lock); + + for (int i = 0; i < nready; i++) { + if (ready[i].func) ready[i].func(ready[i].arg); + g_stats.callbacks_invoked++; + } +} + +void rcu_synchronize(void) { + if (!g_initialized) return; + + uint64_t start = g_grace_period; + uint64_t deadline = pit_get_ticks() + 200; // 2 s ceiling at 100 Hz + + while (g_grace_period <= start) { + rcu_process_callbacks(); + if (g_grace_period > start) break; + if (pit_get_ticks() >= deadline) break; + sched_yield(); + } +} + +int call_rcu(rcu_callback_t func, void* arg) { + if (!g_initialized || !func) return -1; + + spin_lock(&g_rcu_lock); + for (int i = 0; i < RCU_MAX_CALLBACKS; i++) { + if (!g_callbacks[i].in_use) { + g_callbacks[i].func = func; + g_callbacks[i].arg = arg; + g_callbacks[i].gp_target = g_grace_period + 1; + g_callbacks[i].in_use = true; + g_stats.callbacks_queued++; + g_stats.pending++; + spin_unlock(&g_rcu_lock); + return 0; + } + } + spin_unlock(&g_rcu_lock); + + // Callback ring is full: force a grace period and retry once. + rcu_process_callbacks(); + + spin_lock(&g_rcu_lock); + for (int i = 0; i < RCU_MAX_CALLBACKS; i++) { + if (!g_callbacks[i].in_use) { + g_callbacks[i].func = func; + g_callbacks[i].arg = arg; + g_callbacks[i].gp_target = g_grace_period + 1; + g_callbacks[i].in_use = true; + g_stats.callbacks_queued++; + g_stats.pending++; + spin_unlock(&g_rcu_lock); + return 0; + } + } + spin_unlock(&g_rcu_lock); + return -1; +} + +int kfree_rcu(void* ptr) { + if (!ptr) return 0; + return call_rcu(rcu_free_cb, ptr); +} + +rcu_stats_t rcu_get_stats(void) { + rcu_stats_t s = g_stats; + s.nesting_depth = __atomic_load_n(&g_reader_nesting, __ATOMIC_RELAXED); + return s; +} + +void rcu_dump(void) { + rcu_stats_t s = rcu_get_stats(); + + printk(ANSI_BRIGHT_CYAN "=== SUB-OS Tiny RCU Subsystem ===\n" ANSI_RESET); + printk(" State : %s\n", + g_initialized ? ANSI_BRIGHT_GREEN "active" ANSI_RESET + : ANSI_YELLOW "uninitialized" ANSI_RESET); + printk(" Grace period : #%llu\n", (unsigned long long)g_grace_period); + printk(" Completed periods : %llu\n", (unsigned long long)s.grace_periods); + printk(" Reader nesting : %u %s\n", s.nesting_depth, + s.nesting_depth ? "(grace period stalled)" : "(quiescent)"); + printk(" rcu_read_lock() : %llu calls\n", (unsigned long long)s.read_locks); + printk(" Callbacks queued : %llu\n", (unsigned long long)s.callbacks_queued); + printk(" Callbacks invoked : %llu\n", (unsigned long long)s.callbacks_invoked); + printk(" Callbacks pending : %u / %d slots\n", s.pending, RCU_MAX_CALLBACKS); +} diff --git a/kernel/wait.c b/kernel/wait.c new file mode 100644 index 0000000..71cd957 --- /dev/null +++ b/kernel/wait.c @@ -0,0 +1,227 @@ +// Wait queues and completion barriers for SUB-OS +#include +#include +#include +#include +#include + +#define WAIT_MAX_SLEEPERS 32 +#define WAIT_TRACKED_QUEUES 16 + +// Wait entries live in a static pool so that a sleeping path never needs the +// heap (kmalloc may itself block on a wait queue in future revisions). +static wait_entry_t g_entry_pool[WAIT_MAX_SLEEPERS]; +static bool g_entry_used[WAIT_MAX_SLEEPERS]; +static spinlock_t g_pool_lock = SPINLOCK_INIT; + +static wait_queue_t* g_tracked[WAIT_TRACKED_QUEUES]; +static int g_tracked_count = 0; + +static wait_entry_t* entry_alloc(void) { + spin_lock(&g_pool_lock); + for (int i = 0; i < WAIT_MAX_SLEEPERS; i++) { + if (!g_entry_used[i]) { + g_entry_used[i] = true; + spin_unlock(&g_pool_lock); + memset(&g_entry_pool[i], 0, sizeof(wait_entry_t)); + return &g_entry_pool[i]; + } + } + spin_unlock(&g_pool_lock); + return NULL; +} + +static void entry_free(wait_entry_t* e) { + if (!e) return; + size_t idx = (size_t)(e - g_entry_pool); + if (idx >= WAIT_MAX_SLEEPERS) return; + spin_lock(&g_pool_lock); + g_entry_used[idx] = false; + spin_unlock(&g_pool_lock); +} + +void wait_queue_init(wait_queue_t* wq, const char* name) { + if (!wq) return; + + memset(wq, 0, sizeof(*wq)); + INIT_LIST_HEAD(&wq->waiters); + spinlock_init(&wq->lock); + strncpy(wq->name, name ? name : "wq", sizeof(wq->name) - 1); + + if (g_tracked_count < WAIT_TRACKED_QUEUES) { + g_tracked[g_tracked_count++] = wq; + } +} + +// Enqueue the caller and return its pool entry, or NULL when the pool is dry. +static wait_entry_t* wait_prepare(wait_queue_t* wq) { + wait_entry_t* e = entry_alloc(); + if (!e) return NULL; + + e->task = task_current(); + e->woken = false; + + spin_lock(&wq->lock); + list_add_tail(&e->node, &wq->waiters); + wq->nr_waiting++; + wq->sleeps++; + spin_unlock(&wq->lock); + + if (e->task) e->task->state = TASK_STATE_BLOCKED; + return e; +} + +static void wait_finish(wait_queue_t* wq, wait_entry_t* e) { + spin_lock(&wq->lock); + if (e->node.next) { + list_del(&e->node); + if (wq->nr_waiting > 0) wq->nr_waiting--; + } + spin_unlock(&wq->lock); + + if (e->task) e->task->state = TASK_STATE_READY; + entry_free(e); +} + +int wait_event(wait_queue_t* wq) { + if (!wq) return -1; + + wait_entry_t* e = wait_prepare(wq); + if (!e) return -1; + + while (!e->woken) { + sched_yield(); + } + + wait_finish(wq, e); + return 0; +} + +int wait_event_timeout(wait_queue_t* wq, uint64_t timeout_ticks) { + if (!wq) return -1; + + wait_entry_t* e = wait_prepare(wq); + if (!e) return -1; + + uint64_t deadline = pit_get_ticks() + timeout_ticks; + int result = 0; + + while (!e->woken) { + if (pit_get_ticks() >= deadline) { + result = 1; + break; + } + sched_yield(); + } + + wait_finish(wq, e); + return result; +} + +// The waker only flips flags; each sleeper unlinks its own entry so the pool +// slot is never recycled while its owner is still spinning on `woken`. +static int wake_up_nr(wait_queue_t* wq, int max) { + if (!wq) return 0; + + int woken = 0; + spin_lock(&wq->lock); + + struct list_head *pos, *n; + list_for_each_safe(pos, n, &wq->waiters) { + if (max > 0 && woken >= max) break; + + wait_entry_t* e = list_entry(pos, wait_entry_t, node); + if (e->woken) continue; + + e->woken = true; + if (e->task) e->task->state = TASK_STATE_READY; + woken++; + wq->wakeups++; + } + + spin_unlock(&wq->lock); + return woken; +} + +int wake_up(wait_queue_t* wq) { return wake_up_nr(wq, 1); } +int wake_up_all(wait_queue_t* wq) { return wake_up_nr(wq, 0); } + +uint32_t wait_queue_waiters(const wait_queue_t* wq) { + return wq ? wq->nr_waiting : 0; +} + +// --------------------------------------------------------------------------- +// Completions +// --------------------------------------------------------------------------- + +void completion_init(completion_t* c, const char* name) { + if (!c) return; + wait_queue_init(&c->wq, name ? name : "completion"); + c->done = false; +} + +void wait_for_completion(completion_t* c) { + if (!c) return; + while (!c->done) { + if (wait_event_timeout(&c->wq, 100) < 0) { + // Entry pool exhausted: degrade to a plain spin-and-yield. + sched_yield(); + } + } +} + +int wait_for_completion_timeout(completion_t* c, uint64_t timeout_ticks) { + if (!c) return -1; + if (c->done) return 0; + + uint64_t deadline = pit_get_ticks() + timeout_ticks; + while (!c->done) { + if (pit_get_ticks() >= deadline) return 1; + wait_event_timeout(&c->wq, 10); + } + return 0; +} + +void complete(completion_t* c) { + if (!c) return; + c->done = true; + wake_up(&c->wq); +} + +void complete_all(completion_t* c) { + if (!c) return; + c->done = true; + wake_up_all(&c->wq); +} + +bool completion_done(const completion_t* c) { + return c ? c->done : false; +} + +void wait_subsystem_init(void) { + memset(g_entry_used, 0, sizeof(g_entry_used)); + spinlock_init(&g_pool_lock); + g_tracked_count = 0; +} + +void wait_dump_stats(void) { + printk(ANSI_BRIGHT_CYAN "=== SUB-OS Wait Queue Subsystem ===\n" ANSI_RESET); + + int in_use = 0; + for (int i = 0; i < WAIT_MAX_SLEEPERS; i++) { + if (g_entry_used[i]) in_use++; + } + printk(" Sleeper pool : %d/%d entries in use\n", in_use, WAIT_MAX_SLEEPERS); + printk(" Tracked queues: %d\n\n", g_tracked_count); + + printk(ANSI_YELLOW " %-20s %10s %10s %10s\n" ANSI_RESET, + "NAME", "WAITING", "SLEEPS", "WAKEUPS"); + for (int i = 0; i < g_tracked_count; i++) { + wait_queue_t* wq = g_tracked[i]; + if (!wq) continue; + printk(" %-20s %10u %10llu %10llu\n", + wq->name, wq->nr_waiting, + (unsigned long long)wq->sleeps, + (unsigned long long)wq->wakeups); + } +} diff --git a/lib/hashtable.c b/lib/hashtable.c new file mode 100644 index 0000000..ce79294 --- /dev/null +++ b/lib/hashtable.c @@ -0,0 +1,227 @@ +// Chained hash table with FNV-1a hashing and load-factor driven growth +#include +#include +#include + +#define HT_MAX_LOAD_NUM 3 // Grow once entries > 3/4 of bucket count +#define HT_MAX_LOAD_DEN 4 +#define HT_MAX_BUCKETS 4096 + +uint64_t hash_fnv1a(const void* data, size_t len) { + const uint8_t* p = (const uint8_t*)data; + uint64_t h = 1469598103934665603ULL; // FNV offset basis + for (size_t i = 0; i < len; i++) { + h ^= p[i]; + h *= 1099511628211ULL; // FNV prime + } + return h; +} + +uint64_t hash_string(const char* s) { + if (!s) return 0; + return hash_fnv1a(s, strlen(s)); +} + +uint64_t hash_u64(uint64_t v) { + // Fibonacci-style mixing; cheap and well distributed for dense ids. + v ^= v >> 33; + v *= 0xFF51AFD7ED558CCDULL; + v ^= v >> 33; + v *= 0xC4CEB9FE1A85EC53ULL; + v ^= v >> 33; + return v; +} + +static size_t round_up_pow2(size_t v) { + if (v < 8) return 8; + size_t p = 1; + while (p < v) p <<= 1; + return p; +} + +hashtable_t* hashtable_create(size_t initial_buckets, bool string_keys) { + hashtable_t* ht = (hashtable_t*)kzalloc(sizeof(hashtable_t)); + if (!ht) return NULL; + + size_t n = round_up_pow2(initial_buckets); + ht->buckets = (hnode_t**)kzalloc(n * sizeof(hnode_t*)); + if (!ht->buckets) { + kfree(ht); + return NULL; + } + + ht->bucket_count = n; + ht->entries = 0; + ht->collisions = 0; + ht->string_keys = string_keys; + return ht; +} + +void hashtable_destroy(hashtable_t* ht) { + if (!ht) return; + + for (size_t i = 0; i < ht->bucket_count; i++) { + hnode_t* n = ht->buckets[i]; + while (n) { + hnode_t* next = n->next; + if (n->skey) kfree(n->skey); + kfree(n); + n = next; + } + } + kfree(ht->buckets); + kfree(ht); +} + +static void hashtable_grow(hashtable_t* ht) { + if (ht->bucket_count >= HT_MAX_BUCKETS) return; + + size_t new_count = ht->bucket_count * 2; + hnode_t** nb = (hnode_t**)kzalloc(new_count * sizeof(hnode_t*)); + if (!nb) return; // Growth is best effort; chains simply stay longer. + + size_t collisions = 0; + for (size_t i = 0; i < ht->bucket_count; i++) { + hnode_t* n = ht->buckets[i]; + while (n) { + hnode_t* next = n->next; + size_t idx = (size_t)(n->hash & (new_count - 1)); + if (nb[idx]) collisions++; + n->next = nb[idx]; + nb[idx] = n; + n = next; + } + } + + kfree(ht->buckets); + ht->buckets = nb; + ht->bucket_count = new_count; + ht->collisions = collisions; +} + +static hnode_t* find_node(const hashtable_t* ht, uint64_t hash, + const char* skey, uint64_t ikey) { + size_t idx = (size_t)(hash & (ht->bucket_count - 1)); + for (hnode_t* n = ht->buckets[idx]; n; n = n->next) { + if (n->hash != hash) continue; + if (ht->string_keys) { + if (n->skey && skey && strcmp(n->skey, skey) == 0) return n; + } else if (n->ikey == ikey) { + return n; + } + } + return NULL; +} + +static int hashtable_insert(hashtable_t* ht, uint64_t hash, const char* skey, + uint64_t ikey, void* value) { + hnode_t* existing = find_node(ht, hash, skey, ikey); + if (existing) { + existing->value = value; + return 0; + } + + if (ht->entries * HT_MAX_LOAD_DEN > ht->bucket_count * HT_MAX_LOAD_NUM) { + hashtable_grow(ht); + } + + hnode_t* n = (hnode_t*)kzalloc(sizeof(hnode_t)); + if (!n) return -1; + + n->hash = hash; + n->ikey = ikey; + n->value = value; + + if (ht->string_keys && skey) { + size_t len = strlen(skey); + n->skey = (char*)kmalloc(len + 1); + if (!n->skey) { + kfree(n); + return -1; + } + memcpy(n->skey, skey, len + 1); + } + + size_t idx = (size_t)(hash & (ht->bucket_count - 1)); + if (ht->buckets[idx]) ht->collisions++; + n->next = ht->buckets[idx]; + ht->buckets[idx] = n; + ht->entries++; + return 0; +} + +static bool hashtable_unlink(hashtable_t* ht, uint64_t hash, const char* skey, + uint64_t ikey) { + size_t idx = (size_t)(hash & (ht->bucket_count - 1)); + hnode_t** link = &ht->buckets[idx]; + + while (*link) { + hnode_t* n = *link; + bool match = (n->hash == hash) && + (ht->string_keys ? (n->skey && skey && strcmp(n->skey, skey) == 0) + : (n->ikey == ikey)); + if (match) { + *link = n->next; + if (n->skey) kfree(n->skey); + kfree(n); + ht->entries--; + return true; + } + link = &n->next; + } + return false; +} + +int hashtable_put(hashtable_t* ht, const char* key, void* value) { + if (!ht || !key || !ht->string_keys) return -1; + return hashtable_insert(ht, hash_string(key), key, 0, value); +} + +void* hashtable_get(const hashtable_t* ht, const char* key) { + if (!ht || !key || !ht->string_keys) return NULL; + hnode_t* n = find_node(ht, hash_string(key), key, 0); + return n ? n->value : NULL; +} + +bool hashtable_remove(hashtable_t* ht, const char* key) { + if (!ht || !key || !ht->string_keys) return false; + return hashtable_unlink(ht, hash_string(key), key, 0); +} + +int hashtable_put_u64(hashtable_t* ht, uint64_t key, void* value) { + if (!ht || ht->string_keys) return -1; + return hashtable_insert(ht, hash_u64(key), NULL, key, value); +} + +void* hashtable_get_u64(const hashtable_t* ht, uint64_t key) { + if (!ht || ht->string_keys) return NULL; + hnode_t* n = find_node(ht, hash_u64(key), NULL, key); + return n ? n->value : NULL; +} + +bool hashtable_remove_u64(hashtable_t* ht, uint64_t key) { + if (!ht || ht->string_keys) return false; + return hashtable_unlink(ht, hash_u64(key), NULL, key); +} + +size_t hashtable_size(const hashtable_t* ht) { return ht ? ht->entries : 0; } +size_t hashtable_buckets(const hashtable_t* ht) { return ht ? ht->bucket_count : 0; } +size_t hashtable_collisions(const hashtable_t* ht) { return ht ? ht->collisions : 0; } + +size_t hashtable_longest_chain(const hashtable_t* ht) { + if (!ht) return 0; + size_t longest = 0; + for (size_t i = 0; i < ht->bucket_count; i++) { + size_t len = 0; + for (hnode_t* n = ht->buckets[i]; n; n = n->next) len++; + if (len > longest) longest = len; + } + return longest; +} + +void hashtable_foreach(const hashtable_t* ht, hashtable_iter_fn fn, void* ctx) { + if (!ht || !fn) return; + for (size_t i = 0; i < ht->bucket_count; i++) { + for (hnode_t* n = ht->buckets[i]; n; n = n->next) fn(n, ctx); + } +} diff --git a/lib/kfifo.c b/lib/kfifo.c new file mode 100644 index 0000000..8314914 --- /dev/null +++ b/lib/kfifo.c @@ -0,0 +1,134 @@ +// Power-of-two lock-protected byte ring buffer for SUB-OS +#include +#include +#include + +static size_t round_up_pow2(size_t v) { + if (v < 2) return 2; + size_t p = 1; + while (p < v) p <<= 1; + return p; +} + +int kfifo_alloc(kfifo_t* fifo, size_t size) { + if (!fifo || size == 0) return -1; + + size = round_up_pow2(size); + fifo->buffer = (uint8_t*)kmalloc(size); + if (!fifo->buffer) return -1; + + fifo->size = size; + fifo->mask = size - 1; + fifo->in = 0; + fifo->out = 0; + fifo->owns_buffer = true; + spinlock_init(&fifo->lock); + return 0; +} + +int kfifo_init_static(kfifo_t* fifo, void* buffer, size_t size) { + if (!fifo || !buffer || size == 0) return -1; + // A caller-supplied buffer must already be a power of two. + if (size & (size - 1)) return -1; + + fifo->buffer = (uint8_t*)buffer; + fifo->size = size; + fifo->mask = size - 1; + fifo->in = 0; + fifo->out = 0; + fifo->owns_buffer = false; + spinlock_init(&fifo->lock); + return 0; +} + +void kfifo_free(kfifo_t* fifo) { + if (!fifo) return; + if (fifo->owns_buffer && fifo->buffer) kfree(fifo->buffer); + fifo->buffer = NULL; + fifo->size = fifo->mask = fifo->in = fifo->out = 0; + fifo->owns_buffer = false; +} + +void kfifo_reset(kfifo_t* fifo) { + if (!fifo) return; + spin_lock(&fifo->lock); + fifo->in = fifo->out = 0; + spin_unlock(&fifo->lock); +} + +size_t kfifo_len(const kfifo_t* fifo) { + if (!fifo || !fifo->buffer) return 0; + return fifo->in - fifo->out; +} + +size_t kfifo_avail(const kfifo_t* fifo) { + if (!fifo || !fifo->buffer) return 0; + return fifo->size - (fifo->in - fifo->out); +} + +bool kfifo_is_empty(const kfifo_t* fifo) { return kfifo_len(fifo) == 0; } +bool kfifo_is_full(const kfifo_t* fifo) { return kfifo_avail(fifo) == 0; } + +// Copy into the ring at a free-running offset, wrapping at most once. +static void copy_in(kfifo_t* fifo, const uint8_t* src, size_t len, size_t off) { + size_t start = off & fifo->mask; + size_t first = fifo->size - start; + if (first > len) first = len; + + memcpy(fifo->buffer + start, src, first); + if (len > first) memcpy(fifo->buffer, src + first, len - first); +} + +static void copy_out(const kfifo_t* fifo, uint8_t* dst, size_t len, size_t off) { + size_t start = off & fifo->mask; + size_t first = fifo->size - start; + if (first > len) first = len; + + memcpy(dst, fifo->buffer + start, first); + if (len > first) memcpy(dst + first, fifo->buffer, len - first); +} + +size_t kfifo_in(kfifo_t* fifo, const void* src, size_t len) { + if (!fifo || !fifo->buffer || !src || len == 0) return 0; + + spin_lock(&fifo->lock); + size_t space = fifo->size - (fifo->in - fifo->out); + if (len > space) len = space; + + copy_in(fifo, (const uint8_t*)src, len, fifo->in); + fifo->in += len; + spin_unlock(&fifo->lock); + return len; +} + +size_t kfifo_out(kfifo_t* fifo, void* dst, size_t len) { + if (!fifo || !fifo->buffer || !dst || len == 0) return 0; + + spin_lock(&fifo->lock); + size_t used = fifo->in - fifo->out; + if (len > used) len = used; + + copy_out(fifo, (uint8_t*)dst, len, fifo->out); + fifo->out += len; + spin_unlock(&fifo->lock); + return len; +} + +size_t kfifo_peek(const kfifo_t* fifo, void* dst, size_t len) { + if (!fifo || !fifo->buffer || !dst || len == 0) return 0; + + size_t used = fifo->in - fifo->out; + if (len > used) len = used; + + copy_out(fifo, (uint8_t*)dst, len, fifo->out); + return len; +} + +int kfifo_put_byte(kfifo_t* fifo, uint8_t byte) { + return kfifo_in(fifo, &byte, 1) == 1 ? 0 : -1; +} + +int kfifo_get_byte(kfifo_t* fifo, uint8_t* byte_out) { + if (!byte_out) return -1; + return kfifo_out(fifo, byte_out, 1) == 1 ? 0 : -1; +} diff --git a/lib/rbtree.c b/lib/rbtree.c new file mode 100644 index 0000000..55b7ff6 --- /dev/null +++ b/lib/rbtree.c @@ -0,0 +1,309 @@ +// Red-Black Tree implementation for SUB-OS (Linux-compatible semantics) +#include + +static void rb_rotate_left(struct rb_node* node, struct rb_root* root) { + struct rb_node* right = node->rb_right; + struct rb_node* parent = node->rb_parent; + + node->rb_right = right->rb_left; + if (right->rb_left) right->rb_left->rb_parent = node; + + right->rb_left = node; + right->rb_parent = parent; + + if (parent) { + if (node == parent->rb_left) parent->rb_left = right; + else parent->rb_right = right; + } else { + root->rb_node = right; + } + node->rb_parent = right; +} + +static void rb_rotate_right(struct rb_node* node, struct rb_root* root) { + struct rb_node* left = node->rb_left; + struct rb_node* parent = node->rb_parent; + + node->rb_left = left->rb_right; + if (left->rb_right) left->rb_right->rb_parent = node; + + left->rb_right = node; + left->rb_parent = parent; + + if (parent) { + if (node == parent->rb_right) parent->rb_right = left; + else parent->rb_left = left; + } else { + root->rb_node = left; + } + node->rb_parent = left; +} + +void rb_insert_color(struct rb_node* node, struct rb_root* root) { + struct rb_node *parent, *gparent; + + while ((parent = node->rb_parent) && parent->rb_color == RB_RED) { + gparent = parent->rb_parent; + if (!gparent) break; + + if (parent == gparent->rb_left) { + struct rb_node* uncle = gparent->rb_right; + if (uncle && uncle->rb_color == RB_RED) { + uncle->rb_color = RB_BLACK; + parent->rb_color = RB_BLACK; + gparent->rb_color = RB_RED; + node = gparent; + continue; + } + if (parent->rb_right == node) { + rb_rotate_left(parent, root); + struct rb_node* tmp = parent; + parent = node; + node = tmp; + } + parent->rb_color = RB_BLACK; + gparent->rb_color = RB_RED; + rb_rotate_right(gparent, root); + } else { + struct rb_node* uncle = gparent->rb_left; + if (uncle && uncle->rb_color == RB_RED) { + uncle->rb_color = RB_BLACK; + parent->rb_color = RB_BLACK; + gparent->rb_color = RB_RED; + node = gparent; + continue; + } + if (parent->rb_left == node) { + rb_rotate_right(parent, root); + struct rb_node* tmp = parent; + parent = node; + node = tmp; + } + parent->rb_color = RB_BLACK; + gparent->rb_color = RB_RED; + rb_rotate_left(gparent, root); + } + } + + root->rb_node->rb_color = RB_BLACK; +} + +// Rebalance after removing a black node. `node` may be NULL, hence the +// explicit parent pointer. +static void rb_erase_color(struct rb_node* node, struct rb_node* parent, + struct rb_root* root) { + struct rb_node* other; + + while ((!node || node->rb_color == RB_BLACK) && node != root->rb_node) { + if (parent->rb_left == node) { + other = parent->rb_right; + if (!other) break; + + if (other->rb_color == RB_RED) { + other->rb_color = RB_BLACK; + parent->rb_color = RB_RED; + rb_rotate_left(parent, root); + other = parent->rb_right; + if (!other) break; + } + if ((!other->rb_left || other->rb_left->rb_color == RB_BLACK) && + (!other->rb_right || other->rb_right->rb_color == RB_BLACK)) { + other->rb_color = RB_RED; + node = parent; + parent = node->rb_parent; + } else { + if (!other->rb_right || other->rb_right->rb_color == RB_BLACK) { + if (other->rb_left) other->rb_left->rb_color = RB_BLACK; + other->rb_color = RB_RED; + rb_rotate_right(other, root); + other = parent->rb_right; + if (!other) break; + } + other->rb_color = parent->rb_color; + parent->rb_color = RB_BLACK; + if (other->rb_right) other->rb_right->rb_color = RB_BLACK; + rb_rotate_left(parent, root); + node = root->rb_node; + break; + } + } else { + other = parent->rb_left; + if (!other) break; + + if (other->rb_color == RB_RED) { + other->rb_color = RB_BLACK; + parent->rb_color = RB_RED; + rb_rotate_right(parent, root); + other = parent->rb_left; + if (!other) break; + } + if ((!other->rb_left || other->rb_left->rb_color == RB_BLACK) && + (!other->rb_right || other->rb_right->rb_color == RB_BLACK)) { + other->rb_color = RB_RED; + node = parent; + parent = node->rb_parent; + } else { + if (!other->rb_left || other->rb_left->rb_color == RB_BLACK) { + if (other->rb_right) other->rb_right->rb_color = RB_BLACK; + other->rb_color = RB_RED; + rb_rotate_left(other, root); + other = parent->rb_left; + if (!other) break; + } + other->rb_color = parent->rb_color; + parent->rb_color = RB_BLACK; + if (other->rb_left) other->rb_left->rb_color = RB_BLACK; + rb_rotate_right(parent, root); + node = root->rb_node; + break; + } + } + } + + if (node) node->rb_color = RB_BLACK; +} + +void rb_erase(struct rb_node* node, struct rb_root* root) { + struct rb_node *child, *parent; + int color; + + if (!node->rb_left) { + child = node->rb_right; + } else if (!node->rb_right) { + child = node->rb_left; + } else { + // Two children: splice in the in-order successor. + struct rb_node* old = node; + struct rb_node* left; + + node = node->rb_right; + while ((left = node->rb_left) != NULL) node = left; + + // Put the successor where `old` used to hang. + if (old->rb_parent) { + if (old->rb_parent->rb_left == old) old->rb_parent->rb_left = node; + else old->rb_parent->rb_right = node; + } else { + root->rb_node = node; + } + + child = node->rb_right; + parent = node->rb_parent; + color = node->rb_color; + + if (parent == old) { + // The successor is `old`'s direct right child, so it keeps its own + // right subtree and becomes the parent of the deficient side. + parent = node; + } else { + if (child) child->rb_parent = parent; + parent->rb_left = child; + + node->rb_right = old->rb_right; + old->rb_right->rb_parent = node; + } + + node->rb_parent = old->rb_parent; + node->rb_color = old->rb_color; + node->rb_left = old->rb_left; + old->rb_left->rb_parent = node; + + if (color == RB_BLACK) rb_erase_color(child, parent, root); + return; + } + + parent = node->rb_parent; + color = node->rb_color; + + if (child) child->rb_parent = parent; + if (parent) { + if (parent->rb_left == node) parent->rb_left = child; + else parent->rb_right = child; + } else { + root->rb_node = child; + } + + if (color == RB_BLACK) rb_erase_color(child, parent, root); +} + +struct rb_node* rb_first(const struct rb_root* root) { + struct rb_node* n = root->rb_node; + if (!n) return NULL; + while (n->rb_left) n = n->rb_left; + return n; +} + +struct rb_node* rb_last(const struct rb_root* root) { + struct rb_node* n = root->rb_node; + if (!n) return NULL; + while (n->rb_right) n = n->rb_right; + return n; +} + +struct rb_node* rb_next(const struct rb_node* node) { + if (!node) return NULL; + + if (node->rb_right) { + struct rb_node* n = node->rb_right; + while (n->rb_left) n = n->rb_left; + return n; + } + + struct rb_node* parent; + while ((parent = node->rb_parent) && node == parent->rb_right) { + node = parent; + } + return parent; +} + +struct rb_node* rb_prev(const struct rb_node* node) { + if (!node) return NULL; + + if (node->rb_left) { + struct rb_node* n = node->rb_left; + while (n->rb_right) n = n->rb_right; + return n; + } + + struct rb_node* parent; + while ((parent = node->rb_parent) && node == parent->rb_left) { + node = parent; + } + return parent; +} + +size_t rb_count(const struct rb_root* root) { + size_t count = 0; + for (struct rb_node* n = rb_first(root); n; n = rb_next(n)) count++; + return count; +} + +// Returns the number of black nodes on any root-to-leaf path, or -1 when the +// black-height invariant is violated. +static int rb_black_height_of(const struct rb_node* node) { + if (!node) return 1; + + int left = rb_black_height_of(node->rb_left); + int right = rb_black_height_of(node->rb_right); + if (left < 0 || right < 0 || left != right) return -1; + + if (node->rb_color == RB_RED) { + if ((node->rb_left && node->rb_left->rb_color == RB_RED) || + (node->rb_right && node->rb_right->rb_color == RB_RED)) { + return -1; + } + return left; + } + return left + 1; +} + +int rb_black_height(const struct rb_root* root) { + return rb_black_height_of(root->rb_node); +} + +bool rb_validate(const struct rb_root* root) { + if (!root->rb_node) return true; + if (root->rb_node->rb_color != RB_BLACK) return false; + if (root->rb_node->rb_parent != NULL) return false; + return rb_black_height_of(root->rb_node) > 0; +} diff --git a/mm/page_cache.c b/mm/page_cache.c new file mode 100644 index 0000000..dd8442c --- /dev/null +++ b/mm/page_cache.c @@ -0,0 +1,333 @@ +// Write-back block device page cache for SUB-OS +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static page_cache_entry_t g_pages[PAGE_CACHE_MAX_PAGES]; +static hashtable_t* g_index = NULL; +static page_cache_stats_t g_stats; +static spinlock_t g_lock = SPINLOCK_INIT; +static uint32_t g_clock_hand = 0; +static bool g_initialized = false; + +// The hash key encodes both the device and the page index so a single table +// serves every registered block device. +static void make_key(char* out, size_t out_len, const char* dev, uint64_t idx) { + snprintf(out, out_len, "%s:%llu", dev ? dev : "?", (unsigned long long)idx); +} + +void page_cache_init(void) { + memset(g_pages, 0, sizeof(g_pages)); + memset(&g_stats, 0, sizeof(g_stats)); + spinlock_init(&g_lock); + g_clock_hand = 0; + + g_index = hashtable_create(PAGE_CACHE_MAX_PAGES * 2, true); + g_initialized = (g_index != NULL); +} + +static int page_read_from_disk(page_cache_entry_t* e) { + block_device_t* dev = block_get_device(e->dev_name); + if (!dev || !dev->read_sectors) return -1; + + uint64_t sector = e->page_index * PAGE_CACHE_SECTORS; + if (sector + PAGE_CACHE_SECTORS > dev->total_sectors) { + // Reads past the end of the device yield zeroes rather than an error, + // matching how the VFS treats sparse tails. + memset(e->data, 0, PAGE_CACHE_BLOCK_SIZE); + return 0; + } + + int rc = dev->read_sectors(dev, sector, PAGE_CACHE_SECTORS, e->data); + if (rc == 0) g_stats.read_ios++; + return rc; +} + +static int page_write_to_disk(page_cache_entry_t* e) { + block_device_t* dev = block_get_device(e->dev_name); + if (!dev || !dev->write_sectors || dev->read_only) return -1; + + uint64_t sector = e->page_index * PAGE_CACHE_SECTORS; + if (sector + PAGE_CACHE_SECTORS > dev->total_sectors) return -1; + + int rc = dev->write_sectors(dev, sector, PAGE_CACHE_SECTORS, e->data); + if (rc == 0) { + g_stats.write_ios++; + g_stats.writebacks++; + } + return rc; +} + +static void unindex(page_cache_entry_t* e) { + char key[64]; + make_key(key, sizeof(key), e->dev_name, e->page_index); + hashtable_remove(g_index, key); +} + +// CLOCK eviction: sweep the ring clearing reference bits until an unpinned, +// unreferenced page turns up. Dirty victims are written back first. +static page_cache_entry_t* evict_one(void) { + for (int sweep = 0; sweep < PAGE_CACHE_MAX_PAGES * 2; sweep++) { + page_cache_entry_t* e = &g_pages[g_clock_hand]; + g_clock_hand = (g_clock_hand + 1) % PAGE_CACHE_MAX_PAGES; + + if (!e->valid) return e; + if (e->pin_count > 0) continue; + + if (e->referenced) { + e->referenced = false; + continue; + } + + if (e->dirty) { + if (page_write_to_disk(e) != 0) continue; // Cannot flush: skip it + e->dirty = false; + if (g_stats.dirty_pages > 0) g_stats.dirty_pages--; + } + + unindex(e); + e->valid = false; + if (g_stats.resident_pages > 0) g_stats.resident_pages--; + g_stats.evictions++; + return e; + } + return NULL; +} + +static page_cache_entry_t* lookup_locked(const char* dev_name, uint64_t page_index) { + char key[64]; + make_key(key, sizeof(key), dev_name, page_index); + return (page_cache_entry_t*)hashtable_get(g_index, key); +} + +page_cache_entry_t* page_cache_get(const char* dev_name, uint64_t page_index) { + if (!g_initialized || !dev_name) return NULL; + + spin_lock(&g_lock); + + page_cache_entry_t* e = lookup_locked(dev_name, page_index); + if (e && e->valid) { + e->referenced = true; + e->last_access = pit_get_ticks(); + e->pin_count++; + e->hits++; + g_stats.hits++; + spin_unlock(&g_lock); + return e; + } + + g_stats.misses++; + + e = evict_one(); + if (!e) { + spin_unlock(&g_lock); + return NULL; + } + + if (!e->data) { + e->data = (uint8_t*)kmalloc(PAGE_CACHE_BLOCK_SIZE); + if (!e->data) { + spin_unlock(&g_lock); + return NULL; + } + g_stats.memory_bytes += PAGE_CACHE_BLOCK_SIZE; + } + + strncpy(e->dev_name, dev_name, sizeof(e->dev_name) - 1); + e->dev_name[sizeof(e->dev_name) - 1] = '\0'; + e->page_index = page_index; + e->dirty = false; + e->referenced = true; + e->pin_count = 1; + e->hits = 0; + e->last_access = pit_get_ticks(); + + if (page_read_from_disk(e) != 0) { + memset(e->data, 0, PAGE_CACHE_BLOCK_SIZE); + } + + e->valid = true; + g_stats.resident_pages++; + + char key[64]; + make_key(key, sizeof(key), dev_name, page_index); + hashtable_put(g_index, key, e); + + spin_unlock(&g_lock); + return e; +} + +void page_cache_release(page_cache_entry_t* entry) { + if (!entry) return; + spin_lock(&g_lock); + if (entry->pin_count > 0) entry->pin_count--; + spin_unlock(&g_lock); +} + +void page_cache_mark_dirty(page_cache_entry_t* entry) { + if (!entry || !entry->valid) return; + spin_lock(&g_lock); + if (!entry->dirty) { + entry->dirty = true; + g_stats.dirty_pages++; + } + spin_unlock(&g_lock); +} + +size_t page_cache_read(const char* dev_name, uint64_t offset, void* buf, size_t len) { + if (!g_initialized || !dev_name || !buf || len == 0) return 0; + + uint8_t* out = (uint8_t*)buf; + size_t done = 0; + + while (done < len) { + uint64_t page_index = (offset + done) / PAGE_CACHE_BLOCK_SIZE; + size_t page_off = (size_t)((offset + done) % PAGE_CACHE_BLOCK_SIZE); + size_t chunk = PAGE_CACHE_BLOCK_SIZE - page_off; + if (chunk > len - done) chunk = len - done; + + page_cache_entry_t* e = page_cache_get(dev_name, page_index); + if (!e) break; + + memcpy(out + done, e->data + page_off, chunk); + page_cache_release(e); + done += chunk; + } + + return done; +} + +size_t page_cache_write(const char* dev_name, uint64_t offset, const void* buf, size_t len) { + if (!g_initialized || !dev_name || !buf || len == 0) return 0; + + const uint8_t* in = (const uint8_t*)buf; + size_t done = 0; + + while (done < len) { + uint64_t page_index = (offset + done) / PAGE_CACHE_BLOCK_SIZE; + size_t page_off = (size_t)((offset + done) % PAGE_CACHE_BLOCK_SIZE); + size_t chunk = PAGE_CACHE_BLOCK_SIZE - page_off; + if (chunk > len - done) chunk = len - done; + + page_cache_entry_t* e = page_cache_get(dev_name, page_index); + if (!e) break; + + memcpy(e->data + page_off, in + done, chunk); + page_cache_mark_dirty(e); + page_cache_release(e); + done += chunk; + } + + return done; +} + +int page_cache_sync_dev(const char* dev_name) { + if (!g_initialized) return -1; + + int flushed = 0; + spin_lock(&g_lock); + for (int i = 0; i < PAGE_CACHE_MAX_PAGES; i++) { + page_cache_entry_t* e = &g_pages[i]; + if (!e->valid || !e->dirty) continue; + if (dev_name && strcmp(e->dev_name, dev_name) != 0) continue; + + if (page_write_to_disk(e) == 0) { + e->dirty = false; + if (g_stats.dirty_pages > 0) g_stats.dirty_pages--; + flushed++; + } + } + spin_unlock(&g_lock); + return flushed; +} + +int page_cache_sync(void) { + return page_cache_sync_dev(NULL); +} + +int page_cache_invalidate(const char* dev_name) { + if (!g_initialized) return -1; + + int dropped = 0; + spin_lock(&g_lock); + for (int i = 0; i < PAGE_CACHE_MAX_PAGES; i++) { + page_cache_entry_t* e = &g_pages[i]; + if (!e->valid || e->dirty || e->pin_count > 0) continue; + if (dev_name && strcmp(e->dev_name, dev_name) != 0) continue; + + unindex(e); + e->valid = false; + if (g_stats.resident_pages > 0) g_stats.resident_pages--; + dropped++; + } + spin_unlock(&g_lock); + return dropped; +} + +void page_cache_drop_all(void) { + page_cache_sync(); + page_cache_invalidate(NULL); +} + +page_cache_stats_t page_cache_get_stats(void) { + return g_stats; +} + +uint32_t page_cache_hit_percent(void) { + uint64_t total = g_stats.hits + g_stats.misses; + if (total == 0) return 0; + return (uint32_t)((g_stats.hits * 100) / total); +} + +void page_cache_dump(void) { + printk(ANSI_BRIGHT_CYAN "=== SUB-OS Block Page Cache ===\n" ANSI_RESET); + if (!g_initialized) { + printk(ANSI_YELLOW " Page cache not initialized\n" ANSI_RESET); + return; + } + + printk(" Page size : %d bytes (%d sectors)\n", + PAGE_CACHE_BLOCK_SIZE, PAGE_CACHE_SECTORS); + printk(" Resident : %u / %d pages (%llu KB allocated)\n", + g_stats.resident_pages, PAGE_CACHE_MAX_PAGES, + (unsigned long long)(g_stats.memory_bytes / 1024)); + printk(" Dirty : %u pages\n", g_stats.dirty_pages); + printk(" Hits/Misses : %llu / %llu (" ANSI_BRIGHT_GREEN "%u%% hit rate" ANSI_RESET ")\n", + (unsigned long long)g_stats.hits, + (unsigned long long)g_stats.misses, + page_cache_hit_percent()); + printk(" Evictions : %llu (CLOCK second-chance)\n", + (unsigned long long)g_stats.evictions); + printk(" Block I/O : %llu reads, %llu writes (%llu write-backs)\n\n", + (unsigned long long)g_stats.read_ios, + (unsigned long long)g_stats.write_ios, + (unsigned long long)g_stats.writebacks); + + printk(ANSI_YELLOW " %-12s %10s %6s %6s %6s %8s\n" ANSI_RESET, + "DEVICE", "PAGE", "DIRTY", "PIN", "REF", "HITS"); + + int shown = 0; + for (int i = 0; i < PAGE_CACHE_MAX_PAGES && shown < 16; i++) { + page_cache_entry_t* e = &g_pages[i]; + if (!e->valid) continue; + printk(" %-12s %10llu %6s %6u %6s %8llu\n", + e->dev_name, (unsigned long long)e->page_index, + e->dirty ? ANSI_RED "yes" ANSI_RESET : "no", + e->pin_count, + e->referenced ? "yes" : "no", + (unsigned long long)e->hits); + shown++; + } + if (shown == 0) printk(ANSI_BRIGHT_BLACK " (cache is empty)\n" ANSI_RESET); + + printk("\n Index: %llu entries in %llu buckets, longest chain %llu\n", + (unsigned long long)hashtable_size(g_index), + (unsigned long long)hashtable_buckets(g_index), + (unsigned long long)hashtable_longest_chain(g_index)); +} diff --git a/userland/lazybox/coreutils.c b/userland/lazybox/coreutils.c new file mode 100644 index 0000000..fc9171e --- /dev/null +++ b/userland/lazybox/coreutils.c @@ -0,0 +1,744 @@ +// SUB-OS LazyBox Text Processing Suite +// +// GNU coreutils-compatible line and byte oriented tools operating over the +// VFS: sort, uniq, cut, tr, rev, tac, nl, seq, diff, xxd, du, factor, sum +// and truncate. Every tool that reads a file streams it into a bounded line +// table so a pathological input cannot exhaust the kernel heap. + +#include +#include +#include +#include +#include +#include +#include +#include + +#define CU_MAX_LINES 512 +#define CU_MAX_LINE_LEN 256 +#define CU_MAX_FILE_SIZE (128 * 1024) + +typedef struct { + char** lines; + int count; + char* raw; +} cu_text_t; + +static void cu_free_text(cu_text_t* t) { + if (!t) return; + if (t->lines) kfree(t->lines); + if (t->raw) kfree(t->raw); + t->lines = NULL; + t->raw = NULL; + t->count = 0; +} + +// Slurp a file and split it in place on newlines. Returns false (and prints a +// diagnostic) when the path cannot be read. +static bool cu_read_text(const char* path, cu_text_t* out) { + out->lines = NULL; + out->raw = NULL; + out->count = 0; + + vfs_node_t* node = vfs_namei(path); + if (!node) { + printk(ANSI_RED "cannot open '%s': No such file or directory\n" ANSI_RESET, path); + return false; + } + if (node->flags & FS_DIRECTORY) { + printk(ANSI_RED "'%s' is a directory\n" ANSI_RESET, path); + return false; + } + + // Synthetic procfs/sysfs nodes report length 0 and materialise their + // contents only on read, so fall back to a fixed window for those. + size_t len = node->length ? node->length : 8192; + if (len > CU_MAX_FILE_SIZE) len = CU_MAX_FILE_SIZE; + + char* buf = (char*)kmalloc(len + 2); + if (!buf) { + printk(ANSI_RED "out of memory reading '%s'\n" ANSI_RESET, path); + return false; + } + + int fd = vfs_open(path, O_RDONLY); + if (fd < 0) { + kfree(buf); + printk(ANSI_RED "cannot open '%s'\n" ANSI_RESET, path); + return false; + } + + ssize_t got = vfs_read(fd, buf, len); + vfs_close(fd); + if (got < 0) got = 0; + buf[got] = '\0'; + + char** lines = (char**)kzalloc(CU_MAX_LINES * sizeof(char*)); + if (!lines) { + kfree(buf); + return false; + } + + int count = 0; + char* cursor = buf; + lines[count++] = cursor; + + for (ssize_t i = 0; i < got && count < CU_MAX_LINES; i++) { + if (buf[i] == '\n') { + buf[i] = '\0'; + if (i + 1 < got) lines[count++] = &buf[i + 1]; + } + } + + // A trailing newline produces one empty final line; drop it. + if (count > 0 && lines[count - 1][0] == '\0' && got > 0 && buf[got - 1] == '\0') { + count--; + } + + out->raw = buf; + out->lines = lines; + out->count = count; + return true; +} + +static int cu_atoi(const char* s, int fallback) { + if (!s || !*s) return fallback; + int sign = 1; + if (*s == '-') { sign = -1; s++; } + if (*s < '0' || *s > '9') return fallback; + + int v = 0; + while (*s >= '0' && *s <= '9') { + v = v * 10 + (*s - '0'); + s++; + } + return v * sign; +} + +static bool cu_has_flag(int argc, char** argv, const char* flag) { + for (int i = 1; i < argc; i++) { + if (argv[i] && strcmp(argv[i], flag) == 0) return true; + } + return false; +} + +// First argument that is not a flag and not the value of a flag. +static const char* cu_first_operand(int argc, char** argv) { + for (int i = 1; i < argc; i++) { + if (argv[i] && argv[i][0] != '-') return argv[i]; + } + return NULL; +} + +// =========================================================================== +// sort +// =========================================================================== +static int cu_strcmp_ci(const char* a, const char* b) { + while (*a && *b) { + char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? (char)(*b + 32) : *b; + if (ca != cb) return (int)(unsigned char)ca - (int)(unsigned char)cb; + a++; b++; + } + return (int)(unsigned char)*a - (int)(unsigned char)*b; +} + +int coreutils_sort(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: sort [-r] [-n] [-f] [-u] \n"); + printk(" -r reverse order -n numeric sort\n"); + printk(" -f ignore case -u unique lines only\n"); + return 1; + } + + bool reverse = cu_has_flag(argc, argv, "-r"); + bool numeric = cu_has_flag(argc, argv, "-n"); + bool fold = cu_has_flag(argc, argv, "-f"); + bool unique = cu_has_flag(argc, argv, "-u"); + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + // Insertion sort: input is capped at CU_MAX_LINES, and it is stable, + // which `sort -u` relies on to keep the first of each duplicate run. + for (int i = 1; i < t.count; i++) { + char* key = t.lines[i]; + int j = i - 1; + while (j >= 0) { + int cmp; + if (numeric) { + int a = cu_atoi(t.lines[j], 0); + int b = cu_atoi(key, 0); + cmp = (a > b) - (a < b); + } else if (fold) { + cmp = cu_strcmp_ci(t.lines[j], key); + } else { + cmp = strcmp(t.lines[j], key); + } + if (reverse) cmp = -cmp; + if (cmp <= 0) break; + t.lines[j + 1] = t.lines[j]; + j--; + } + t.lines[j + 1] = key; + } + + const char* prev = NULL; + for (int i = 0; i < t.count; i++) { + if (unique && prev && strcmp(prev, t.lines[i]) == 0) continue; + printk("%s\n", t.lines[i]); + prev = t.lines[i]; + } + + cu_free_text(&t); + return 0; +} + +// =========================================================================== +// uniq +// =========================================================================== +int coreutils_uniq(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: uniq [-c] [-d] [-u] \n"); + printk(" -c prefix each line with its repeat count\n"); + printk(" -d print only duplicated lines\n"); + printk(" -u print only unique lines\n"); + return 1; + } + + bool show_count = cu_has_flag(argc, argv, "-c"); + bool dups_only = cu_has_flag(argc, argv, "-d"); + bool uniq_only = cu_has_flag(argc, argv, "-u"); + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + int i = 0; + while (i < t.count) { + int run = 1; + while (i + run < t.count && strcmp(t.lines[i], t.lines[i + run]) == 0) run++; + + bool emit = true; + if (dups_only && run < 2) emit = false; + if (uniq_only && run > 1) emit = false; + + if (emit) { + if (show_count) printk("%7d %s\n", run, t.lines[i]); + else printk("%s\n", t.lines[i]); + } + i += run; + } + + cu_free_text(&t); + return 0; +} + +// =========================================================================== +// cut +// =========================================================================== +int coreutils_cut(int argc, char** argv) { + const char* path = NULL; + char delim = '\t'; + int field = 0; + int byte_start = 0, byte_end = 0; + + for (int i = 1; i < argc; i++) { + if (!argv[i]) continue; + if (strcmp(argv[i], "-d") == 0 && i + 1 < argc) { + delim = argv[++i][0]; + } else if (strcmp(argv[i], "-f") == 0 && i + 1 < argc) { + field = cu_atoi(argv[++i], 1); + } else if (strcmp(argv[i], "-b") == 0 && i + 1 < argc) { + const char* spec = argv[++i]; + byte_start = cu_atoi(spec, 1); + const char* dash = strchr(spec, '-'); + byte_end = dash ? cu_atoi(dash + 1, 0) : byte_start; + } else if (argv[i][0] != '-') { + path = argv[i]; + } + } + + if (!path || (field == 0 && byte_start == 0)) { + printk("Usage: cut -f [-d ] \n"); + printk(" cut -b \n"); + return 1; + } + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + for (int i = 0; i < t.count; i++) { + const char* line = t.lines[i]; + + if (byte_start > 0) { + int len = (int)strlen(line); + int from = byte_start - 1; + int to = (byte_end > 0) ? byte_end : len; + if (to > len) to = len; + for (int b = from; b < to; b++) printk("%c", line[b]); + printk("\n"); + continue; + } + + // Field mode: walk the delimiter runs and print the requested column. + int current = 1; + const char* start = line; + const char* p = line; + bool printed = false; + + while (1) { + if (*p == delim || *p == '\0') { + if (current == field) { + for (const char* q = start; q < p; q++) printk("%c", *q); + printed = true; + break; + } + if (*p == '\0') break; + current++; + start = p + 1; + } + if (*p == '\0') break; + p++; + } + if (!printed && current < field) { + // Missing field: GNU cut emits an empty line. + } + printk("\n"); + } + + cu_free_text(&t); + return 0; +} + +// =========================================================================== +// tr +// =========================================================================== +int coreutils_tr(int argc, char** argv) { + if (argc < 3) { + printk("Usage: tr translate characters\n"); + printk(" tr -d delete characters\n"); + return 1; + } + + bool del = (strcmp(argv[1], "-d") == 0); + const char* set1 = del ? argv[2] : argv[1]; + const char* set2 = del ? NULL : argv[2]; + const char* path = del ? (argc > 3 ? argv[3] : NULL) + : (argc > 3 ? argv[3] : NULL); + + if (!path) { + printk(ANSI_RED "tr: missing file operand\n" ANSI_RESET); + return 1; + } + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + size_t set1_len = strlen(set1); + size_t set2_len = set2 ? strlen(set2) : 0; + + for (int i = 0; i < t.count; i++) { + for (const char* p = t.lines[i]; *p; p++) { + const char* found = strchr(set1, *p); + if (!found) { + printk("%c", *p); + continue; + } + if (del) continue; + + // Short set2 repeats its final character, as GNU tr does. + size_t idx = (size_t)(found - set1); + if (idx >= set2_len) idx = set2_len ? set2_len - 1 : 0; + if (set2_len) printk("%c", set2[idx]); + } + printk("\n"); + } + + (void)set1_len; + cu_free_text(&t); + return 0; +} + +// =========================================================================== +// rev / tac / nl +// =========================================================================== +int coreutils_rev(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: rev reverse each line character-wise\n"); + return 1; + } + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + for (int i = 0; i < t.count; i++) { + int len = (int)strlen(t.lines[i]); + for (int c = len - 1; c >= 0; c--) printk("%c", t.lines[i][c]); + printk("\n"); + } + + cu_free_text(&t); + return 0; +} + +int coreutils_tac(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: tac print lines in reverse order\n"); + return 1; + } + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + for (int i = t.count - 1; i >= 0; i--) printk("%s\n", t.lines[i]); + + cu_free_text(&t); + return 0; +} + +int coreutils_nl(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: nl [-a] number lines (-a numbers blank lines too)\n"); + return 1; + } + + bool number_all = cu_has_flag(argc, argv, "-a"); + + cu_text_t t; + if (!cu_read_text(path, &t)) return 1; + + int n = 1; + for (int i = 0; i < t.count; i++) { + bool blank = (t.lines[i][0] == '\0'); + if (blank && !number_all) { + printk("\n"); + continue; + } + printk(ANSI_BRIGHT_BLACK "%6d " ANSI_RESET "%s\n", n++, t.lines[i]); + } + + cu_free_text(&t); + return 0; +} + +// =========================================================================== +// seq +// =========================================================================== +int coreutils_seq(int argc, char** argv) { + if (argc < 2) { + printk("Usage: seq [first [step]] last\n"); + return 1; + } + + int first = 1, step = 1, last; + if (argc == 2) { + last = cu_atoi(argv[1], 1); + } else if (argc == 3) { + first = cu_atoi(argv[1], 1); + last = cu_atoi(argv[2], 1); + } else { + first = cu_atoi(argv[1], 1); + step = cu_atoi(argv[2], 1); + last = cu_atoi(argv[3], 1); + } + + if (step == 0) { + printk(ANSI_RED "seq: step must not be zero\n" ANSI_RESET); + return 1; + } + + int emitted = 0; + if (step > 0) { + for (int v = first; v <= last && emitted < 4096; v += step, emitted++) printk("%d\n", v); + } else { + for (int v = first; v >= last && emitted < 4096; v += step, emitted++) printk("%d\n", v); + } + return 0; +} + +// =========================================================================== +// diff +// =========================================================================== +int coreutils_diff(int argc, char** argv) { + if (argc < 3) { + printk("Usage: diff line-by-line comparison\n"); + return 1; + } + + cu_text_t a, b; + if (!cu_read_text(argv[1], &a)) return 1; + if (!cu_read_text(argv[2], &b)) { + cu_free_text(&a); + return 1; + } + + int max = (a.count > b.count) ? a.count : b.count; + int differences = 0; + + for (int i = 0; i < max; i++) { + const char* la = (i < a.count) ? a.lines[i] : NULL; + const char* lb = (i < b.count) ? b.lines[i] : NULL; + + if (la && lb && strcmp(la, lb) == 0) continue; + + differences++; + printk(ANSI_YELLOW "@@ line %d @@\n" ANSI_RESET, i + 1); + if (la) printk(ANSI_RED "- %s\n" ANSI_RESET, la); + if (lb) printk(ANSI_GREEN "+ %s\n" ANSI_RESET, lb); + } + + if (differences == 0) { + printk(ANSI_BRIGHT_GREEN "Files '%s' and '%s' are identical (%d lines)\n" ANSI_RESET, + argv[1], argv[2], a.count); + } else { + printk("\n%d differing line(s)\n", differences); + } + + cu_free_text(&a); + cu_free_text(&b); + return differences ? 1 : 0; +} + +// =========================================================================== +// xxd +// =========================================================================== +int coreutils_xxd(int argc, char** argv) { + const char* path = NULL; + int limit = 256; + + for (int i = 1; i < argc; i++) { + if (!argv[i]) continue; + if (strcmp(argv[i], "-l") == 0 && i + 1 < argc) limit = cu_atoi(argv[++i], 256); + else if (argv[i][0] != '-') path = argv[i]; + } + + if (!path) { + printk("Usage: xxd [-l ] canonical hex + ASCII dump\n"); + return 1; + } + + vfs_node_t* node = vfs_namei(path); + if (!node) { + printk(ANSI_RED "xxd: '%s': No such file\n" ANSI_RESET, path); + return 1; + } + + int fd = vfs_open(path, O_RDONLY); + if (fd < 0) { + printk(ANSI_RED "xxd: cannot open '%s'\n" ANSI_RESET, path); + return 1; + } + + uint8_t row[16]; + int offset = 0; + + while (offset < limit) { + int want = (limit - offset > 16) ? 16 : (limit - offset); + ssize_t got = vfs_read(fd, row, (size_t)want); + if (got <= 0) break; + + printk(ANSI_BRIGHT_BLACK "%08x: " ANSI_RESET, offset); + + for (int i = 0; i < 16; i++) { + if (i < got) printk("%02x", row[i]); + else printk(" "); + if (i % 2) printk(" "); + } + + printk(" "); + for (int i = 0; i < got; i++) { + char c = (char)row[i]; + printk("%c", (c >= 32 && c < 127) ? c : '.'); + } + printk("\n"); + + offset += (int)got; + if (got < want) break; + } + + vfs_close(fd); + printk(ANSI_BRIGHT_BLACK "%d bytes dumped\n" ANSI_RESET, offset); + return 0; +} + +// =========================================================================== +// du +// =========================================================================== +static uint64_t cu_du_walk(vfs_node_t* dir, const char* path, int depth, bool summarize) { + if (!dir || depth > 8) return 0; + + uint64_t total = 0; + for (uint32_t i = 0; ; i++) { + vfs_dirent_t* de = dir->readdir ? dir->readdir(dir, i) : NULL; + if (!de) break; + if (strcmp(de->name, ".") == 0 || strcmp(de->name, "..") == 0) continue; + + vfs_node_t* child = dir->finddir ? dir->finddir(dir, de->name) : NULL; + if (!child) continue; + + char child_path[256]; + if (strcmp(path, "/") == 0) snprintf(child_path, sizeof(child_path), "/%s", de->name); + else snprintf(child_path, sizeof(child_path), "%s/%s", path, de->name); + + if (child->flags & FS_DIRECTORY) { + uint64_t sub = cu_du_walk(child, child_path, depth + 1, summarize); + total += sub; + if (!summarize) printk("%8llu KB %s/\n", (unsigned long long)((sub + 1023) / 1024), child_path); + } else { + total += child->length; + if (!summarize && child->length > 0) { + printk("%8llu KB %s\n", + (unsigned long long)((child->length + 1023) / 1024), child_path); + } + } + } + return total; +} + +int coreutils_du(int argc, char** argv) { + bool summarize = cu_has_flag(argc, argv, "-s"); + const char* path = cu_first_operand(argc, argv); + if (!path) path = vfs_getcwd(); + + vfs_node_t* node = vfs_namei(path); + if (!node) { + printk(ANSI_RED "du: '%s': No such file or directory\n" ANSI_RESET, path); + return 1; + } + + if (!(node->flags & FS_DIRECTORY)) { + printk("%8llu KB %s\n", (unsigned long long)((node->length + 1023) / 1024), path); + return 0; + } + + uint64_t total = cu_du_walk(node, path, 0, summarize); + printk(ANSI_BRIGHT_GREEN "%8llu KB %s (total)\n" ANSI_RESET, + (unsigned long long)((total + 1023) / 1024), path); + return 0; +} + +// =========================================================================== +// factor +// =========================================================================== +int coreutils_factor(int argc, char** argv) { + if (argc < 2) { + printk("Usage: factor print prime factorization\n"); + return 1; + } + + for (int a = 1; a < argc; a++) { + long n = cu_atoi(argv[a], 0); + if (n < 2) { + printk("%ld:\n", n); + continue; + } + + printk("%ld:", n); + long v = n; + for (long d = 2; d * d <= v; d++) { + while (v % d == 0) { + printk(" %ld", d); + v /= d; + } + } + if (v > 1) printk(" %ld", v); + printk("\n"); + } + return 0; +} + +// =========================================================================== +// sum +// =========================================================================== +int coreutils_sum(int argc, char** argv) { + const char* path = cu_first_operand(argc, argv); + if (!path) { + printk("Usage: sum BSD 16-bit rotating checksum\n"); + return 1; + } + + int fd = vfs_open(path, O_RDONLY); + if (fd < 0) { + printk(ANSI_RED "sum: cannot open '%s'\n" ANSI_RESET, path); + return 1; + } + + uint8_t buf[256]; + uint32_t checksum = 0; + uint64_t bytes = 0; + ssize_t got; + + while ((got = vfs_read(fd, buf, sizeof(buf))) > 0) { + for (ssize_t i = 0; i < got; i++) { + // BSD sum: rotate right one bit, then add, keeping 16 bits. + checksum = (checksum >> 1) | ((checksum & 1) << 15); + checksum = (checksum + buf[i]) & 0xFFFF; + } + bytes += (uint64_t)got; + } + vfs_close(fd); + + printk("%05u %5llu %s\n", checksum, + (unsigned long long)((bytes + 1023) / 1024), path); + return 0; +} + +// =========================================================================== +// truncate +// =========================================================================== +int coreutils_truncate(int argc, char** argv) { + if (argc < 3) { + printk("Usage: truncate -s shrink or grow a file\n"); + return 1; + } + + long size = -1; + const char* path = NULL; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) size = cu_atoi(argv[++i], -1); + else if (argv[i][0] != '-') path = argv[i]; + } + + if (!path || size < 0) { + printk(ANSI_RED "truncate: need -s and a file\n" ANSI_RESET); + return 1; + } + + vfs_node_t* node = vfs_namei(path); + if (!node) { + printk(ANSI_RED "truncate: '%s': No such file\n" ANSI_RESET, path); + return 1; + } + + size_t old_len = node->length; + + if ((size_t)size > old_len && node->write) { + // Growing: append zero bytes so the tail is well defined. + size_t pad = (size_t)size - old_len; + uint8_t zeros[64]; + memset(zeros, 0, sizeof(zeros)); + + int fd = vfs_open(path, O_WRONLY); + if (fd >= 0) { + vfs_lseek(fd, (off_t)old_len, SEEK_SET); + while (pad > 0) { + size_t chunk = (pad > sizeof(zeros)) ? sizeof(zeros) : pad; + if (vfs_write(fd, zeros, chunk) <= 0) break; + pad -= chunk; + } + vfs_close(fd); + } + } else { + node->length = (size_t)size; + } + + printk("truncate: '%s' %llu -> %llu bytes\n", path, + (unsigned long long)old_len, (unsigned long long)node->length); + return 0; +} diff --git a/userland/lazybox/lazybox.c b/userland/lazybox/lazybox.c index a414b48..4f45e54 100644 --- a/userland/lazybox/lazybox.c +++ b/userland/lazybox/lazybox.c @@ -38,6 +38,15 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -2023,6 +2032,62 @@ static int applet_sensors(int argc, char** argv) { return 0; } +// ------------------------------------------------------------- +// Kernel Core Diagnostics: KTest, RCU, Futex, Wait Queues, Page Cache +// ------------------------------------------------------------- +static int applet_ktest(int argc, char** argv) { + if (argc >= 2 && strcmp(argv[1], "-l") == 0) { + ktest_list_suites(); + return 0; + } + + ktest_result_t result; + int failures = (argc >= 2) ? ktest_run_suite(argv[1], &result) + : ktest_run_all(&result); + return (failures > 0) ? 1 : 0; +} + +static int applet_rcuinfo(int argc, char** argv) { + if (argc >= 2 && strcmp(argv[1], "-s") == 0) { + printk("Forcing an RCU grace period...\n"); + rcu_synchronize(); + printk(ANSI_BRIGHT_GREEN "Grace period #%llu completed\n" ANSI_RESET, + (unsigned long long)rcu_get_grace_period()); + return 0; + } + rcu_dump(); + return 0; +} + +static int applet_futexinfo(int argc, char** argv) { + futex_dump(); + return 0; +} + +static int applet_waitinfo(int argc, char** argv) { + wait_dump_stats(); + return 0; +} + +static int applet_pagecache(int argc, char** argv) { + if (argc >= 2) { + if (strcmp(argv[1], "sync") == 0) { + int n = page_cache_sync(); + printk("page cache: %d dirty page(s) written back\n", n); + return 0; + } + if (strcmp(argv[1], "drop") == 0) { + page_cache_drop_all(); + printk("page cache: flushed and dropped all clean pages\n"); + return 0; + } + printk("Usage: pagecache [sync|drop]\n"); + return 1; + } + page_cache_dump(); + return 0; +} + // ------------------------------------------------------------- // Applet Dispatch Table (Over 70 Linux Tools) // ------------------------------------------------------------- @@ -2178,6 +2243,29 @@ static const lazybox_applet_t applets[] = { {"gpuinfo", applet_gpuinfo, "gpuinfo", "VirtIO-GPU hardware status", "Virtualization"}, {"e1000e", applet_e1000e, "e1000e", "Intel e1000e PCIe NIC stats", "Network"}, + // Kernel Core Diagnostics + {"ktest", applet_ktest, "ktest [suite|-l]", "Run in-kernel self-test suites", "Kernel"}, + {"rcuinfo", applet_rcuinfo, "rcuinfo [-s]", "Tiny RCU grace period status", "Kernel"}, + {"futexinfo", applet_futexinfo, "futexinfo", "Futex hash bucket telemetry", "Kernel"}, + {"waitinfo", applet_waitinfo, "waitinfo", "Wait queue & sleeper statistics", "Kernel"}, + {"pagecache", applet_pagecache, "pagecache [sync|drop]", "Block page cache & hit rate", "Storage"}, + + // Text Processing Suite (GNU coreutils compatible) + {"sort", coreutils_sort, "sort [-rnfu] ", "Sort lines of a text file", "Filesystem"}, + {"uniq", coreutils_uniq, "uniq [-cdu] ", "Filter adjacent duplicate lines", "Filesystem"}, + {"cut", coreutils_cut, "cut -f [-d c] ", "Extract columns from lines", "Filesystem"}, + {"tr", coreutils_tr, "tr ", "Translate or delete characters", "Filesystem"}, + {"rev", coreutils_rev, "rev ", "Reverse characters of each line", "Filesystem"}, + {"tac", coreutils_tac, "tac ", "Print file lines in reverse", "Filesystem"}, + {"nl", coreutils_nl, "nl [-a] ", "Number the lines of a file", "Filesystem"}, + {"seq", coreutils_seq, "seq [first [step]] last", "Print a sequence of numbers", "Core"}, + {"diff", coreutils_diff, "diff ", "Compare files line by line", "Filesystem"}, + {"xxd", coreutils_xxd, "xxd [-l n] ", "Canonical hex + ASCII dump", "Filesystem"}, + {"du", coreutils_du, "du [-s] [path]", "Estimate file space usage", "Storage"}, + {"factor", coreutils_factor, "factor ", "Print prime factorization", "Core"}, + {"sum", coreutils_sum, "sum ", "BSD 16-bit rotating checksum", "Crypto"}, + {"truncate", coreutils_truncate, "truncate -s ", "Shrink or extend a file", "Filesystem"}, + {NULL, NULL, NULL, NULL, NULL} }; diff --git a/userland/lazybox/shell.c b/userland/lazybox/shell.c index e453a24..a83b034 100644 --- a/userland/lazybox/shell.c +++ b/userland/lazybox/shell.c @@ -25,7 +25,10 @@ static const char* shell_builtins[] = { "nslookup", "hdparm", "lspci", "speaker", "mouse", "virtinfo", "io_uring_test", "uname", "free", "uptime", "dmesg", "ps", "top", "sleep", "reboot", "poweroff", "clear", "help", "neofetch", "calc", - "matrix", "history", "tty", "beep", "shutdown", NULL + "matrix", "history", "tty", "beep", "shutdown", + "ktest", "rcuinfo", "futexinfo", "waitinfo", "pagecache", + "sort", "uniq", "cut", "tr", "rev", "tac", "nl", "seq", "diff", + "xxd", "du", "factor", "sum", "truncate", NULL }; static void history_add(const char* cmd) { @@ -81,7 +84,17 @@ static void cmd_help(void) { printk(" " ANSI_GREEN "dmesg / ps / top" ANSI_RESET " - Kernel boot log & process manager\n"); printk(" " ANSI_GREEN "calc [A] [op] [B]" ANSI_RESET " - Arithmetic calculator (+, -, *, /, %%)\n"); printk(" " ANSI_GREEN "matrix" ANSI_RESET " - Digital rain screen animation\n"); - printk(" " ANSI_GREEN "reboot / shutdown" ANSI_RESET " - Power management\n"); + printk(" " ANSI_GREEN "reboot / shutdown" ANSI_RESET " - Power management\n\n"); + printk(ANSI_YELLOW " [Kernel Core Diagnostics]\n" ANSI_RESET); + printk(" " ANSI_GREEN "ktest [suite]" ANSI_RESET " - Run in-kernel self-test suites (KUnit-style)\n"); + printk(" " ANSI_GREEN "rcuinfo / futexinfo" ANSI_RESET " - Tiny RCU grace periods & futex buckets\n"); + printk(" " ANSI_GREEN "waitinfo" ANSI_RESET " - Wait queue sleepers and wakeup counters\n"); + printk(" " ANSI_GREEN "pagecache [sync]" ANSI_RESET " - Block page cache residency & hit rate\n\n"); + printk(ANSI_YELLOW " [Text Processing Suite]\n" ANSI_RESET); + printk(" " ANSI_GREEN "sort / uniq / nl" ANSI_RESET " - Sort, de-duplicate and number lines\n"); + printk(" " ANSI_GREEN "cut / tr / rev / tac" ANSI_RESET " - Column, character and order transforms\n"); + printk(" " ANSI_GREEN "diff / xxd / sum" ANSI_RESET " - Compare, hex-dump and checksum files\n"); + printk(" " ANSI_GREEN "seq / factor / du" ANSI_RESET " - Number sequences, factoring, disk usage\n"); printk(ANSI_BRIGHT_BLACK "Tip: Use TAB for autocomplete, Up/Down for command history.\n" ANSI_RESET); }