mirror of
https://github.com/subhobhai943/SUB-OS.git
synced 2026-10-25 16:52:29 +00:00
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP
This commit is contained in:
1 parent
492045b24d
commit
f9400f6ac4
22 files changed
+3631
-5
No files matched your search
@@ -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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef _KERNEL_FUTEX_H
|
||||
#define _KERNEL_FUTEX_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <kernel/wait.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef _KERNEL_KTEST_H
|
||||
#define _KERNEL_KTEST_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef _KERNEL_RCU_H
|
||||
#define _KERNEL_RCU_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef _KERNEL_WAIT_H
|
||||
#define _KERNEL_WAIT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <lib/list.h>
|
||||
#include <kernel/sync.h>
|
||||
#include <kernel/task.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef _LIB_HASHTABLE_H
|
||||
#define _LIB_HASHTABLE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef _LIB_KFIFO_H
|
||||
#define _LIB_KFIFO_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <kernel/sync.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef _LIB_RBTREE_H
|
||||
#define _LIB_RBTREE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifndef _MM_PAGE_CACHE_H
|
||||
#define _MM_PAGE_CACHE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
// Fast Userspace muTEX (futex) hash table for SUB-OS
|
||||
#include <kernel/futex.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
// In-kernel self-test harness for SUB-OS
|
||||
#include <kernel/ktest.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <kernel/rcu.h>
|
||||
#include <kernel/futex.h>
|
||||
#include <kernel/wait.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <mm/page_cache.h>
|
||||
#include <lib/rbtree.h>
|
||||
#include <lib/kfifo.h>
|
||||
#include <lib/hashtable.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
#include <arch/arch.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
#include <kernel/metrics.h>
|
||||
#include <kernel/bpf.h>
|
||||
#include <kernel/kobject.h>
|
||||
#include <kernel/wait.h>
|
||||
#include <kernel/futex.h>
|
||||
#include <kernel/rcu.h>
|
||||
#include <kernel/ktest.h>
|
||||
#include <mm/page_cache.h>
|
||||
#include <mm/vma.h>
|
||||
#include <drivers/canvas.h>
|
||||
#include <drivers/pty.h>
|
||||
@@ -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
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
// Tiny RCU implementation for SUB-OS
|
||||
#include <kernel/rcu.h>
|
||||
#include <kernel/sched.h>
|
||||
#include <kernel/sync.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <arch/arch.h>
|
||||
#include <lib/string.h>
|
||||
|
||||
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);
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
// Wait queues and completion barriers for SUB-OS
|
||||
#include <kernel/wait.h>
|
||||
#include <kernel/sched.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <arch/arch.h>
|
||||
#include <lib/string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
// Chained hash table with FNV-1a hashing and load-factor driven growth
|
||||
#include <lib/hashtable.h>
|
||||
#include <lib/string.h>
|
||||
#include <mm/kmalloc.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// Power-of-two lock-protected byte ring buffer for SUB-OS
|
||||
#include <lib/kfifo.h>
|
||||
#include <lib/string.h>
|
||||
#include <mm/kmalloc.h>
|
||||
|
||||
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;
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
// Red-Black Tree implementation for SUB-OS (Linux-compatible semantics)
|
||||
#include <lib/rbtree.h>
|
||||
|
||||
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;
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
// Write-back block device page cache for SUB-OS
|
||||
#include <mm/page_cache.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <block/block.h>
|
||||
#include <lib/hashtable.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
#include <kernel/sync.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <arch/arch.h>
|
||||
|
||||
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));
|
||||
}
|
||||
@@ -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 <userland/coreutils.h>
|
||||
#include <fs/vfs.h>
|
||||
#include <kernel/printk.h>
|
||||
#include <mm/kmalloc.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/printf.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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] <file>\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] <file>\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 <n> [-d <char>] <file>\n");
|
||||
printk(" cut -b <start[-end]> <file>\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 <set1> <set2> <file> translate characters\n");
|
||||
printk(" tr -d <set1> <file> 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 <file> 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 <file> 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] <file> 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 <file1> <file2> 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 <bytes>] <file> 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 <number...> 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 <file> 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 <size> <file> 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 <size> 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;
|
||||
}
|
||||
@@ -38,6 +38,15 @@
|
||||
#include <kernel/tsc.h>
|
||||
#include <kernel/vt_art.h>
|
||||
#include <drivers/cpufreq.h>
|
||||
#include <userland/coreutils.h>
|
||||
#include <kernel/ktest.h>
|
||||
#include <kernel/rcu.h>
|
||||
#include <kernel/futex.h>
|
||||
#include <kernel/wait.h>
|
||||
#include <mm/page_cache.h>
|
||||
#include <lib/hashtable.h>
|
||||
#include <lib/kfifo.h>
|
||||
#include <lib/rbtree.h>
|
||||
#include <drivers/virtio_gpu.h>
|
||||
#include <drivers/e1000e.h>
|
||||
#include <drivers/virtio_input.h>
|
||||
@@ -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] <file>", "Sort lines of a text file", "Filesystem"},
|
||||
{"uniq", coreutils_uniq, "uniq [-cdu] <file>", "Filter adjacent duplicate lines", "Filesystem"},
|
||||
{"cut", coreutils_cut, "cut -f <n> [-d c] <file>", "Extract columns from lines", "Filesystem"},
|
||||
{"tr", coreutils_tr, "tr <set1> <set2> <file>", "Translate or delete characters", "Filesystem"},
|
||||
{"rev", coreutils_rev, "rev <file>", "Reverse characters of each line", "Filesystem"},
|
||||
{"tac", coreutils_tac, "tac <file>", "Print file lines in reverse", "Filesystem"},
|
||||
{"nl", coreutils_nl, "nl [-a] <file>", "Number the lines of a file", "Filesystem"},
|
||||
{"seq", coreutils_seq, "seq [first [step]] last", "Print a sequence of numbers", "Core"},
|
||||
{"diff", coreutils_diff, "diff <file1> <file2>", "Compare files line by line", "Filesystem"},
|
||||
{"xxd", coreutils_xxd, "xxd [-l n] <file>", "Canonical hex + ASCII dump", "Filesystem"},
|
||||
{"du", coreutils_du, "du [-s] [path]", "Estimate file space usage", "Storage"},
|
||||
{"factor", coreutils_factor, "factor <number...>", "Print prime factorization", "Core"},
|
||||
{"sum", coreutils_sum, "sum <file>", "BSD 16-bit rotating checksum", "Crypto"},
|
||||
{"truncate", coreutils_truncate, "truncate -s <n> <file>", "Shrink or extend a file", "Filesystem"},
|
||||
|
||||
{NULL, NULL, NULL, NULL, NULL}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user