#charmOS
1 messages ยท Page 3 of 1
fancy state machine:
enum test_result {
TEST_RESULT_SUCCESS = 0,
TEST_RESULT_SKIPPED = 1,
TEST_RESULT_FAILED = 2,
};
extreme fancy
too complicated for me
๐จ
whats the advantage over just capturing printf?
probably nothing, but this lets me change how i decide to log individual test messages if I want to put them in a file on the filesystem or something in the future
not yet but it would be trivial to setup because iirc QEMU has some way of letting you use an outb instruction to return a code for the QEMU process itself so maybe I could add that to capture the test result statuses and run it in CI
for my bootloader i would just emit a special sequence of bytes via 0xE9 lol
and then shutdown qemu from inside
morse code error message reporting better tbh
use a local runner and record the audio fr 
least insane method
nvme async bio support
next up is IDE ( ๐คฎ ) and also ATAPI
and i will rember to introduce atapi support for ahci
๐ฅ
uniata
Michael Soft's Window
ugh... async IO or any remotely performant IO on IDE is gonna be impossible
since it's got no queues, no slots, no anything
every thread just has to wait for the previous thread to finish the entire command because i cant track waiters in a way that lets me know which thread to wake up
๐ ts pmo
why
I am pretty sure (and I can go rtfm again if I'm wrong about this) that IDE doesnt allow you to submit another command if the previous one isnt finished
so I can only have one thread block on the device at a time because it absolutely has to finish the command to unlock the device and let another thread read or write on it
it is kind of unfortunate
uhhhh the former thing does not imply the latter
at all
!
idk why youre saying that like it obviously follows
you can just queue up the requests in a linked list in memory
start the next one in the ISR that finished the last one
no threads involved
yea this is the async IO model i have that already have implemented for nvme and ahci
but what about synchronous IO
wouldnt you still need to wait for the requests to one by one complete
just wait on your own request and youll get woken up whenever its gotten to and completed
oh wait this is identical to what i do in existing drivers where blocking IO is just async io with a block and yield
ok nvm thanks mister sky
who up creating they IO queues rn
apparently the interrupt vectors have to be different across queues
was having a bug where core 3 would catch core 0's queue interrupt
funky
first iteration of async prefetch logic
i hopes it works
hmmmm i dont think the prefetcher should have start_lba and end_lba probably
That's also known as readahead btw
ohhh ok cool thanks ๐
readahead
I'll call the "asynchronously loading of blocks of an inode during a read" 'readahead' and I'll just keep the name of "prefetch" for the block cache since it'll probably be used elsewhere
readahead works fine ๐ฅ
nice
no noticable speedup in tests since I don't do any aggressive reading or writing
I think I will implement an IO scheduler for these asynchronous requests over time, idk
I will definitely do delayed batch cache writeback though because that is kind of important ๐ฅ
yeah I'll perhaps need a way of queueing, coalescing/merging, and scheduling the block device asynchronous IO requests, but I dunno if that will have a big overhead or not
maybe I will implement reordering and reprioritization of asynchronous IO requests by introducing priority levels
reordering will probably make HDDs happier too since there's less seeking to do
i used the IWYU program and now my main file doesn't have FIFTY LINES OF INCLUDES ANYMORE ๐ ๐ ๐ ๐ ๐
huge win fr
my main worry was just that changing headers would greatly increase compile times if I used headers i didn't need in some files
hopefully this will be good in the long term ๐
around 500 lines away from the next power of two in LOC ๐ฅ
i love powers of two
main.c
#include <acpi/lapic.h>
sane
it's mostly for the init functions and stuff
just make a generic init like c menix had
all init functions are in their own section and then you just call them as pointers
:3
brother reinvents [[gnu::constructor]]
๐ค that's a gnu extension
so is [[gnu::section(...)]]
__attribute__((section)) type stuff
๐ฅ
there is also .preinit_array but i don't know if there's an attribute like early_constructor or whatever
destructor 
could also use init priority to sort allocator init before everything else 
[[gnu::{con,de}structor]] doesn't have any semantics other than being placed in init/fini right?
elf constructors can have an associated priority iirc
but i think it applies to the whole section per object file
i mean the linker sorts them
since after linking you don't have the individual object files
speaking of init i almost forgot the most important thing
why is this an ifdef
so you can explicitly include GDT init... ok
very important features
it's what the magic incantation ```
KEEP ((SORT_BY_INIT_PRIORITY(.init_array.) SORT_BY_INIT_PRIORITY(.ctors.*)))
once I make an ncurses config menu like linux I'll have GDT_INIT as a config option
very very important
is that encoded in the elf?
oh it's literally just .init_array.<priority>
qookie@selenium /tmp ฮป gcc -c -x c - -o test.o <<< "
__attribute__((constructor(200))) void foo() {}
__attribute__((constructor(201))) void bar() {}
"
qookie@selenium /tmp ฮป readelf -SW test.o | grep " .init_array"
[ 4] .init_array.00200 INIT_ARRAY 0000000000000000 000058 000008 08 WA 0 0 8
[ 6] .init_array.00201 INIT_ARRAY 0000000000000000 000060 000008 08 WA 0 0 8
qookie@selenium /tmp ฮป
i've written a shitty elf linker for my 65c816 project
I just remembered that block devices have flush commands due to write caches ๐ฅ
I need a flush function pointer in my block device abstraction now, oopsies
oh yeah I'll also do that rcu thing
seems pretty simple and I have everything necessary to do it
I'll probably implement a simple defer function to run something after some time has passed with one shot hpet interrupts
yippee I get to make a min-heap, again, using macros, for event scheduling, for IO and timer-deferred functions
oh yeah I could probably do that thanks qookie ๐ช
I'll probably wrap over my current send_bio_async (which is instant) for each device with a generic block device IO scheduler
this will allow me to queue up requests and then execute a per-device dispatch()
that will then automatically pick the highest priority commands first to send, and send as many commands in parallel as the underlying device can handle
dispatch() could probably also coalesce requests, but I don't think that'll happen often since my block cache automatically makes sure it isn't sending requests that are at adjacent LBAs
I'll probably also make a per device dispatch_threshold so if it sees enough things have been enqueued it'll automatically start sending some requests (not all of them tho), so that poor devices like IDE drives don't immediately get inundated with dozens of requests whereas NVMe devices would probably be fine
this is kind of an outline of the block device IO scheduler
-
the reordering is fairly generic, but with device specific policies, it can be more optimized, so it'll be implemented per-device. the general rule of thumb is high priority before low priority, with priority being more important than transfer direction
-
the request coalescing is a per-device policy, and each bio has its own driver_data private pointer, so there is no need to make a new aggregated bio structure as I can simply use that. then, each bio has a skip field and an is_aggregate field, allowing me to identify which ones to submit (and how to submit them), as well as which bios I can continue coalescing.
-
coalescing works by attempting a try_coalesce() which will return true if a coalesced block was added to the list/heap, and it will continue until a certain limit is reached, or if coalescing cannot continue
the general coalescing policy that the block device scheduler can use for a quick, "rule of thumb" heuristic before passing the task off to the underlying driver is
- bios must be contiguous
- data transfer must be the same direction
- priority levels must match
I can probably also add custom block device flags such as NO_SCHEDULE for ramdisks and such, which will turn the entire scheduler into a no-op and such
oh yeah this is only for asynchronous lol, synchronous events are instant, always-go-first, do-not-pass-go, do-not-collect-two-hundred-dollars, and come before all asynchronous requests
this is mostly because I only really use sync IO in filesystem detection and forcing a read into the cache, whereas stuff like prefetches and (eventually) writeback are all asynchronous (and will become the more dominant form of blkdev IO activity)
I think I'll have 5 or 7 priority levels, and allow priority boosts if two requests have a potential optimization (such as a potential coalesce opportunity), so that the less urgent request is boosted to the priority level of the higher priority request (to prevent priority inversion) and allow the optimization to happen
I can probably also integrate that hpet timed function deferring thing to allow the scheduler to automatically start submitting requests if enough time elapses between request enqueues even if the dispatch threshold isn't reached
as for dispatching, I think it would be beneficial to have partial dispatch and full dispatch functions, where the partial dispatch would only execute the higher priority commands (up to a certain specified priority level), and the full dispatch would execute all commands
this way I can attend to high priority requests first under heavy load, and low priority requests once there is less work
maybe I could also integrate priority boosts here, where if a request has been in the queue after X dispatch events have passed, it will get boosted to the highest priority for immediate submission on the next dispatch or the next time dispatch_threshold is reached
5 priority levels seems good, I'll have URGENT, HIGH, MEDIUM, LOW, and BACKGROUND
๐ฅ
I have just realized that there is no need for a separate block cache because block devices are files here in unix land so I can just register as VFS nodes and give them normal page caches like everybody else
I will do that later probably
beware.
do I need to take great care here
yes
basically the same sectors in your disk page cache could be in file page caches
causing major confusion
indeed I have thought about that
you dont have to worry about this if you do a buffer cache for metadata
does unix allow arbitrary r/w of mounted block devices
doing that alongside a page cache for file data is a valid strategy
i believe @solid crescent plans on doing this in keyronex
yes
at your own risk
theres no attempt to keep coherency between what you wrote there and whats in page caches
and vice versa
so I should separate my block device block cache and make it for just metadata and pull from the page cache to get file data? or what
you can do a page cache for both youll just need to make sure you can take the extra steps required to "mask off" the page-out IO coming from the disk's page cache, on a per-sector basis
im leaning currently more toward doing a buffer cache for metadata
i think its simpler and more flexible
i also think it could probably be implemented atop the page cache for the disk
and just exempt those pages from the normal write-out
and have specialized machinery for doing write-out from the buffer cache
which does this
and also supports ordering of sector write-outs
wdym?
so that if you alias any file data sectors within the disk page cache, you dont overwrite them inappropriately
which could lead to incoherency between the disk and the file's page cache
also this is only an issue for filesystems which freely intermix sectors of data and metadata, FAT does not but some others do
how does managarm solve this issue
this doesnt solve the problem
this actually is the original thing that INTRODUCES the problem
yes the problem is clear now that you say this
but it doesn't affect ext2 either
so it works fine :^)
NT has to solve this problem for NTFS and their solution was to make all of the NTFS metadata be part of "virtual files" and to access it through page caches for those
for example theres a "bitmap file", the inode table is a file, etc
and that successfully evades the issue
HPFS (the OS/2 filesystem, which NT supported originally) is a more interesting case because theres no real way to do that trick and it freely intermixes sectors of data and metadata
will doing this on a unix system even work? like what if I take this approach, will software still work as it should?
or no
and for that they maintain structures in-memory that it basically uses to mask off the page-out IO going to the disk from the disk's page cache; whenever they access metadata, they add those sectors to this mask, and then during write-out they look up the mask (which i think is kept as an AVL tree of disk extents) and use it to "mask off" any sectors that should not be written (basically by splitting up the IOs)
it accesses the disk through a virtual file it creates so that it receives the IO and has a chance to do this before passing it on otherwise unmolested to the disk driver
thats not even related to this issue
im leaning more toward just using a buffer cache for metadata like i said
it doesnt have to be a pure buffer cache
you can get the benefits of a page cache by using the page cache machinery for reading and for tracking the pages and stuff, but then use special buffer cache machinery for write-outs
so the page cache becomes read only here?
and do other special stuff like when you read a page of disk metadata through this "buffer cache", you allocate enough buffer structures to describe each sector within the page individually
basically yes
the buffer cache does the write-outs itself using direct noncached IO
and the page cache never writes these special pages itself
so you basically have a way of tracking pages in the page cache and saying "don't write these out" and those never get written out?
i mean you can put a flag in their per page frame struct thing
that says "dont write this out"
doesnt have to be that complicated
i havent actually implemented this scheme this is just my current idea
youd use buffer cache like interfaces to access the data
ok thanks, then all I'll have to do is I can keep my block/buffer cache, use it for metadata only, then have a page cache with "no write out" pages that hold metadata, and fs drivers will only check the block/buffer cache to guarantee they get untouched versions of metadata?
or what
and when you mark a sector modified, its buffer structure gets insertion sorted (by sector number) into a list of dirty sectors, which is kept in order of the ideal write-out order
the write-out thread walks this list and tries to do the IO as contiguously on disk as possible, in as large chunks as possible
but when you mark a sector modified you can also supply a flag saying "it must be written out in the order i marked it modified" and then itll be forced to the end of the list
and you can use this for stuff like journaling filesystems where theres all kinds of special strict orders stuff has to be written
to ensure integrity
thanks mister sky this is really really helpful ๐๐๐
I'll probably do my async request scheduling first and then this right afterwards since I can have a proper writeback system
you could keep a list of dirty extents and update that and that would make it a lot faster to insertion sort sectors in (because thered be way fewer items on the list cuz dirty sectors tend to be clustered together and can be represented as a single item with a range) but that would require another allocation dependency, at metadata modification time, so it depends on whether you can tolerate that
i can tolerate that
so thats prob how ill do it
actually no you could probably use the buffer at the start of the extent as the extent
and thats safe because it wont go away until the write-out is complete anyway
so theres no allocation dependency after all
there is one here
at the time you look up a sector (cuz if that results in a read of a new page into the page cache then youll need to allocate an array of buffer structures for each sector within the page)
but theres an allocation dependency there anyway because of the need to allocate memory to put the sector data in
so thats fine
theres also an optimization possible which is that if you know youll only be reading the metadata, you can ask for a read-only view of the sector data
and what this does is just give you a direct pointer to the page cache data for the sector
without potentially causing a buffer structure to be allocated
so that could be faster
alrighty thanks ๐ I'll probably optimize last but I'll keep these in mind
@solid crescent second ping but see above for my current ideas on a modernized buffer cache built atop a page cache and reusing all the read-in machinery including clustering and readahead and the viewcache and so on, so that only the write-out needs to be implemented specially
probably FAAAAAR from the first to come up with this, this might be what UBC already does i dunno i need to read more about it
I forgor I needed to prevent preemption when modifying current thread state ๐ I had a bit of code where I would mark the current thread as blocked and purely by chance I would get preempted and my thread would just disappear ๐ฅ
it fixed tho
decided to implement a bit of logic to track the amount of times a request was boosted in priority due to starvation
this way, if I have a request at the lowest priority, it won't just slowly climb up prioirities at a constant rate, and instead scale fast, where on the first boost it goes up one priority, then on the second it goes up by 2 or 3, and then the 3rd boost forces it to get submitted
similarly I made the amount of time an already boosted request has to have been waiting in queue for to have the next boost happen lower
this way, if a request has already been boosted, it doesnt need to wait that same amount of time to get boosted again and waits a bit less
idk hopefully this works out
I think I could probably steal ideas from this tomfoolery for my thread scheduler lol
I might have a priority inversion inversion problem where old starved requests are unfairly boosted ahead of higher priority requests
oh brother
priority inversion inversion inversion when
I'm kinda missing rust with their impl blocks where you can just say self so I dont have to pass a pointer around everywhere
eh whatever
javascript reference
ok i was dillydallying but defer works fine now
somehow my entire bio scheduler which I wrote in one go without tests for the most part worked first try when I wrote tests for it lol
time to stress test
bugs are hiding from me
I have found one single bug in the bio scheduler so far.... very suspicious
Port fio
If you have userspace
That thing can destroy disks
I will do that yes thanks for the suggestion
Yeah np
asynchronous IO is so ๐
It can do pretty much anything, multiple threads pinned to CPUs, any types of requests, random, any block size, any backend etc
my plan is to eventually have the filesystem drivers use asynchronous operations internally but still have a fully synchronous userspace exposed API
today's brainrot has been making small adjustments to these constants, recompiling, checking tests, changing again, recompiling once more, and doing it until things look good
config brainrot
What did you read for designing the block scheduler?
Why not automate it
linux source code ๐
linux allows you to select many different schedulers for IO
my thread scheduler took 2 hours of fine tuning and that's because I stopped to play balatro in the middle so it won't be that bad
I decided to go with a centralized generic scheduler with per device policy for things like reordering and coalescing
this way I don't have to build multiple schedulers
Fair enough
Personally ill never give up on an opportunity to waste 3 hours to save 30 mins
I'm currently fine tuning it because my generic boosting policy was leading to priority inversion inversion which I can't say I'm a fan of
basically, an old request that got boosted would get placed in a high priority queue that has new requests in it, and that old initially lower priority request would go before all the high priority ones
priority inversion inversion
the bandaid fix is just to say if (queue_has_too_many_requests(q)) do_not_boost();
I might make the scheduler try to estimate a perceived_priority at a finer grained level in the future tho
this is truly scheduler brainrot part 2: the block device strikes back
Yes
In general
just an array of addresses
I'll definitely go back and add more to it
since scatter gather is cool
for now I just did that since the bio sched was a bit more important
wdym?
the request submitters are expected to provide a page aligned virtual address and a given amount of sectors to read
Yes but each virtual page is backed by a random phys page, so u have to walk the ptes and accumulate a list of phys pages right
yes, but rn i dont have to worry about scatter gather since the allocator that I use to create the virtual address will always allocate continguous phyiscal pages
good point tho
i'll probably make proper sglists
yea I'll definitely do that now that u bring it up
๐ ๐
Damn
> git commit -a
[main e3b9bc1] [nvme]: create prp list no matter what
1 file changed, 70 insertions(+), 19 deletions(-)```
thanks ๐ I just construct an array of prps lol
this is technically not needed but might as well
once again infy with the goated recommendations, although constructing PRPs isn't necessary rn since my allocator works the way it does, it will deffo help into the future
it just array
back to bio sched
I might add fastpaths that skip any optimization like coalescing and just put a thing in the queue
the funny thing about this is that the primary reason I have for making this bio_scheduler at all is because my thinkpad t400 (the machine I want to run this on) is an HDD machine, and the optimizations that scheduling asynchronous events allows for can reduce the wear and tear on the device
this is probably so incredibly extra for that ๐
What's the job of a bio scheduler?
it schedules asynchronous Block IO (BIO) requests to manage priority levels, perform boosts on starved requests, allow the block device to optimize requests via reordering and coalescing (reordering helps on HDDs to reduce seek times, and coalescing merges adjacent requests into one), and other stuff ๐
my current bio scheduler uses a 5 level MLFQ, which is mostly an optimization so that dispatching requests doesn't have to do any list traversal and it just unlinks the list head of a given queue and sends it off
it also has per-device function pointers so that each device can implement its own policy on how it wants to perform things like coalescing and reordering, and it also allows some devices like NVMes to entirely skip, or barely perform any coalescing and reordering if it doesn't need it
I didn't wanna do anything fancy so the queues are just intrusive doubly linked circular lists
in the future I may add grouped requests and the scheduler will try to make requests in the same group fire off at the same time or similar times, and that would probably be just by inserting the requests right next to each other in the list
I think for groups, I will give each bio a group_id, which is -1 if there is no group
then, when I enqueue requests and detect there is a group, I will create and track the group as its own queue
after I send all the enqueues for a certain group, the sender can execute a group_seal(disk, group_id) which will tell the scheduler to start sending requests in the group, and the whole group can have a priority level just like with the regular priority levels
group IDs can be trivially created with 64 bit monotonically increasing counters once again
holy baloney my scheduler queueing is slow... 15 microseconds for enqueue ๐ฌ but I'm on an ARM M4 Mac RN so maybe once I get back on my x86 host it won't be that bad
it should be like 3-5 microseconds ideally
(this is counting coalesce, reorder, and other optimizations)
just port to arm 
true...
I can't wait to get home to my x86 machine tomorrow to properly benchmark my asynchronous request scheduler ๐ฅ
15 us per enqueue is like a maximum of 60,000 request enqueues per second ๐ฅ and since the algorithm is fully locking I can't make it faster with SMP ๐จ
hopefully this is just an emulation thing
I think my async bio scheduler will make block write ordering easier since I can just change the priority of the write request depending on what is in the contents of the block I'm writing (metadata, file data, etc)
isn't the preferred policy to write data blocks before metadata blocks
probably
no journaling filesystem moment ๐ข
I should probably do devtmpfs ๐ completely forgot about that
then I make the FAT driver use the asynchronous IO stack
I am actually surprised that the boosts naturally submitting requests over time is actually not horrendously inverting priorities
a few of them are slightly skewed due to coalescing and timing but for the most part it's good
it is actually funny how these are consistent even after running 8,000 requests of varying randomized priorities, lbas, and everything
I think i can just get away with a FIFO queue for IDE async waiter/request tracking
time to get to that today
getting kinda yappy
I decided to steal maravillosa's idea of having linker segments for init functions for PCI stuff
it is very nice
> git commit -a
[main 71640f6] [ide ata]: asynchronous IO beginnings (first we block and interrupt)
5 files changed, 115 insertions(+), 38 deletions(-)
``` ๐ฅ
it going good
I gotta do atapi cdrom after this yay
x git commit -a
[main 90a69bf] [ahci]: bio interfaces for bio sched implemented
9 files changed, 59 insertions(+), 44 deletions(-)``` took a detour and implemented scheduler API for ahci
back to IDE
porting quicksort gameplay
can you port tiktok?
yes I'll make the OS play subway surfers in the terminal background
yk, while I'm still dillydallying in driver land I might start doing some PM (Power Management) stuff
seems cool
"the code is the documentation" moment
๐ฅ IDE bein funky with my async io stack, been having this issue for a bit now
idk no one even uses IDE so I might just not have async with it
funky wacko boom
ATAPI could probably share the request management system with IDE since they're both manually tracking requests with lists
on ahci i could just do one request per slot and nvme could just do one request per queue idx ๐ฅ
ok the whole bug was that i was using an 8 bit integer instead of a 16 bit integer
kind of crazy
bug fixed
lol i find it a bit funny that just tweaking a few small numbers in each block device's config can make the requests go from having horrendous timing and insane priority inversion to being perfectly sane and normal
i guess this will be very customizable
maybe i can figure out a way to get userspace to customize this, idk
ok cool beans the IDE/PATA async IO interface is fully implemented and works ๐ I will probably make this code generic and make it work for the other things like ATAPI
sysctls?
probably yea, but I will likely only let userspace customize the configurations for each block device to a certain degree so that userspace can't just horrendously break block device IO scheduling
static struct bio_scheduler_ops ide_bio_ops = {
.should_coalesce = noop_should_coalesce,
.reorder = ide_reorder,
.do_coalesce = noop_do_coalesce,
.max_wait_time =
{
[BIO_RQ_BACKGROUND] = 35,
[BIO_RQ_LOW] = 25,
[BIO_RQ_MEDIUM] = 10,
[BIO_RQ_HIGH] = 4,
[BIO_RQ_URGENT] = 0,
},
.dispatch_threshold = 96,
.boost_occupance_limit =
{
[BIO_RQ_BACKGROUND] = 64,
[BIO_RQ_LOW] = 56,
[BIO_RQ_MEDIUM] = 48,
[BIO_RQ_HIGH] = 40,
[BIO_RQ_URGENT] = 32,
},
.min_wait_ms = 1,
.tick_ms = 25,
};``` so these are the configs for each block device
and i guess i can figure out a way to get userspace to toggle the numbers around for each block device
how do you decide the urgency?
the caller specifies this lol
requests are boosted too
https://github.com/BlueGummi/charmos/blob/main/include%2Fblock%2Fsched.h check the really big comment in this
for example
#define EXT2_PRIO_DIRENT BIO_RQ_MEDIUM
#define EXT2_PRIO_INODE BIO_RQ_MEDIUM
#define EXT2_PRIO_DATA BIO_RQ_HIGH
#define EXT2_PRIO_BITMAPS BIO_RQ_BACKGROUND
#define EXT2_PRIO_SBLOCK BIO_RQ_LOW```
urgency typically just signals to the scheduler that during boosts, the less urgent requests are boosted less often and take longer until dispatch
and also during manual dispatch it goes from high urgency queues to low urgency ones
nothing beats a jet 2 holiday
I will add request grouping soon so you can guarantee that a group of requests is submitted at the same time but I don't have anything that needs this rn so it's not a priority
did u figure all of this out by just reading linux sources?
I had to make some stuff up along the way but a good portion, yes
by "make stuff up", it's just my own optimizations and design decisions that differ from linux
for example, I'm pretty sure linux doesn't do a generic scheduler with per device policy, and instead has many different schedulers that you assign to each device
so I just made a generic scheduler and allowed each device to implement its own ways of doing certain optimizations, and device flags to skip certain optimizations (like NVMe is so heckin fast that I skip all optimizations)
unit and integration tests are my lord and savior ๐ they have allowed me to find so many edge case bugs because I wrote them so aggressively
and I can very confidently change code and instantly get verification as to whether or not it worked
truly amazing
devtmpfs will be pretty fun now since I have all of these policies for each block device that the user gets to toggle
cool
I lowk feel like writing a floppy driver for no reason
bah idk if I ever feel funny enough I'll do it
the PATA/IDE generic serial FIFO request queue async IO implementation is pretty portable to other drivers so I'll make it generic and then use it for ATAPI and floppy
ohh i see
yea looks cool
struct generic_disk {
enum disk_flags flags;
enum generic_disk_type type;
enum fs_type fs_type;
void *fs_data;
char name[16];
uint64_t total_sectors;
bool is_removable;
void *driver_data;
uint32_t sector_size;
/* both of these take full priority over the async operations.
* do not pass go, do not collect two hundred dollars, submit instantly.
*
* these are sync and blocking
*
* these are not used in many areas though, and such, we can get away
* with instant submission for the most part*/
bool (*read_sector)(struct generic_disk *disk, uint64_t lba,
uint8_t *buffer, uint64_t sector_count);
bool (*write_sector)(struct generic_disk *disk, uint64_t lba,
const uint8_t *buffer, uint64_t sector_count);
/* immediate asynchronous submission */
bool (*submit_bio_async)(struct generic_disk *disk,
struct bio_request *bio);
void (*print)(struct generic_disk *disk); // this one for physical disk
struct bio_scheduler_ops *ops;
struct bio_scheduler *scheduler;
struct bcache *cache;
uint64_t partition_count;
struct generic_partition *partitions;
};``` I now get the great joy of figuring out how to expose this to userspace via devtmpfs
TRY part 2 is here
I didn't like the look of
enum errno e = operation();
TEST_ASSERT(!ERR_IS_FATAL(e));``` so I just wrapped it over `FAIL_IF_FATAL` and now we have `TRY` part 2

I wanna see if I can potentially get test timeouts working but idk, I don't need that rn
I mean it should be trivial, I can just defer_enqueue(kill_if_timeout, test_data, timeout);
๐ฅ
ykw just for old times sake I'm gonna rename my bcache_ent_release to brelse()
nostalgia
and then make a bread over bcache_ent_get and bcache_ent_acquire
bread ๐ค
static inline void bcache_ent_release(struct bcache_entry *ent) {
refcount_dec(&ent->refcount);
bcache_ent_unlock(ent);
}``` we have `brelse` at home ๐
your wish is granted
this is inconsistent with my naming scheme tho so I'll just call em ext2_block_read and bcache_ent_release
oh yea here is brelse
been thinking about the split cache thing where filesystems maintain internal private block caches for metadata and filesystem block device partitions have page caches that have certain pages marked as no_writeback
I think the FS would just keep an internal structure to keep track of data vs metadata addresses
probably an augmented AVL tree would be fine
hmmm.... should do multicore tests ๐ฅ
Holiday homework ๐ญ
ong I have to do physics homework and cOlLegE pRepAraTiOn like BLUD I am FIFTEEN that's QUITE FAR AWAY
๐ฅ
oh well
You're 15??
yes ๐ข
I am a zygote in internet years
is this shocking to you
what ๐ how does my behavior resemble a twenty year old in any way
how have I achieved unc status ๐๐ฅ
man this happens shockingly often
Anyways study hard get into good college ๐
when I email professors they keep thinking I'm lying about my age and then I meet them in person and they go "damn. bro really is a child."
hilarious stuff ngl
peak advice
Ikr
no work today because I need to learn gnu octave to rewrite something into python
scientific computing gameplay
i should probably list out the things imma do/wanna do soon
- bio scheduler request groups
- devtmpfs
- complete FAT driver refactor with all the new stuff
- xhci finish
- xhci devices and stuff
- cleanup async functionality in AHCI because it really sucks rn
- start work on ehci because that is what the thinkpad t400 that i will run this on has
- e1000 work
- UDP stack with maybe a networking scheduler?
i dont think i can make a generic scheduler that can be used in a bunch of places, who knows - TCP too (later on)
and in order, it will go
devtmpfs, AHCI driver cleanup + AHCI ATAPI support, bio scheduler groups, FAT refactor, XHCI, e1000, UDP, TCP, EHCI
rn i need to learn gnu octave for that thingy mabob that is completely unrelated to this
I think I have joined the matlab haters
this language was not fun
it's like someone tried to merge C and python
very strange language
we will return to our scheduled programming tomorrow then
i rembered to add block cache status checks in the tests but these are a bit unnecessary since i need to wait for the async writeback to finish
๐ฅ

nice
ah I think I am getting how to do my scheduler correctly
struct thread should only contain the 6 callee saved registers
if interrupt preemption happens, there is no need to save the rest of the registers because they will simply be on the stack of the thread and will just get loaded back in when context returns to this thread
manual yield will just call some inline asm function to switch out the register state and ret
so if thread A is preempted via interrupt, it saves its registers on the stack, and then passes in the callee saved registers into the schedule function
schedule function loads the new thread, but does NOT return to the first preempted thread and iretq until it is scheduled again
after some work, the scheduler comes back around to the previously preempted thread, and restores the callee saved registers, and hits a ret, transferring control back to the interrupt handler, where it will pop all registers and iretq and go back to what it was doing
๐ฅ
something like that
ok now that my prefrontal cortex has decided to be functional I think I will rewrite the scheduler's save and restore logic properly
yippee
I'm feeling funny and I might port my feed forward neural network I wrote a while back to C and then used fixed point arithmetic and then port it to my kernel
I might use it in thread scheduling if I get bored
just for stuff like reordering and balancing and stuff
I dunno how I will train the goober though
probably hardcode the weights with a script during compile time
certainly not something i was expecting to read
this is so meta, I get to use a neural network to train my neural network
why not
I'm just putting my neural network code to good use
I was using it as a calculator before this
definitely a
:3
moment
https://github.com/BlueGummi/vibe_based_math/blob/main/src/main.rs I know the code is god awful, I wrote this for an intro to CS class and I thought the people grading it wouldn't read it if it was too long
I had a much more sane and regular version of this in C
blud why are you working on scheduling so much
I just gotta dig it up
fun ๐
I did async block IO scheduling before this
I just thought it would be interesting to make my thread scheduler neural network based
because it's 2025 and who doesn't have machine learning in their kernel
I was told that if i don't have a neural network in my kernel I won't be a cool kid and I'll lose aura
just kidding
I just think this will be funny is all
everyone knows what only cool sigma people have gdt init... ok in their kernel
I know I know... but I'm not like other osdevs
idk what I'm saying
I just need my neural network code to be used for something useful besides a vibes based calculator
what better use than thread scheduling
see my neural network is literally only being used as a calculator rn
so I just need a good use for it
this is a maximally unserious project
I will probably stick to just my feed forward neural network because it is simple and fast and anything slower incurs too much of an overhead for it to be good in kernel
honestly I might not even rewrite it to C because I support rust in the kernel
so just fixed point arithmetic and some clean up
ok I think the way I will integrate this is by having the scheduler ask the neural network "which of these threads is best to run right now?" and the neural network returns the best thread to run
so for example it might input something like
[
recent_cpu_time,
wait_time,
priority_nice,
io_wait_ratio, // io-bound?
last_cpu_id,
num_wakeups_recently, // interactivity
core_current_load,
migration_distance,
...
]``` for each thread and then make decisions
actually I can assign a score to each thread
and the thread that scores the highest gets run
idk how to make this happen super duper fast tho
hmmm I'll need to benchmark the floating point implementation
this is going to be a very long term project in the kernel so I'll need to sketch a very solid blueprint before I go and do anything stupid
for now I'll focus on the elephants in the room and touch up on the neural network scheduler design plan here and there
Simple ema can be used h
i really hope I can get the neural network scheduler done by september ngl
That's crazy
I just need to port code and train it
ema?
Estimated Moving Averages
have you looked into any neural network based scheduling
it's very cool
Not really
u don't think it cool? ๐จ
I remember coming across this but never read it
No I mean
I've not looked into it
ok that's it then I'm porting chatgpt to my kernel and asking it to make scheduling decisions
oh ok
hello chat we are back to our scheduled programming
i fixed the scheduler to not be jank and all tests pass
now it properly switches my context ๐
no more of the yield == int 0x20 nonsense
hooray
this was an extremely fast fix wow
took under 45 minutes
mister sky was once again correct that this was significantly easier
oh yeah the manual yield is faster so my things now run faster
new schedule routine ๐ค no more interrupt BS
void schedule(void) {
uint64_t core_id = get_sch_core_id();
struct scheduler *sched = local_schs[core_id];
bool interrupts = spin_lock(&sched->lock);
struct thread *curr = sched->current;
struct thread *next = NULL;
struct scheduler *victim = NULL;
struct thread *prev = sched->current;
/* skip */
if (!sched->active)
goto end;
/* core 0 will recompute the steal threshold */
maybe_recompute_threshold(core_id);
/* re-insert the running thread to its new level */
scheduler_save_thread(sched, curr);
/* check if we can steal */
if (!scheduler_can_steal_work(sched))
goto regular_schedule;
if (!try_begin_steal())
goto regular_schedule;
/* attempt a steal */
begin_steal(sched);
victim = scheduler_pick_victim(sched);
if (!victim) {
/* victim cannot be stolen from - early abort */
stop_steal(sched, victim);
goto regular_schedule;
}
struct thread *stolen = scheduler_steal_work(victim);
/* done stealing work now. another core can steal from us */
stop_steal(sched, victim);
/* work was successfully stolen */
if (stolen) {
next = stolen;
goto load_new_thread;
}
regular_schedule:
next = scheduler_pick_regular_thread(sched);
load_new_thread:
load_thread(sched, next);
update_core_current_thread(next);
if (!next) {
disable_timeslice();
goto end;
}
if (all_threads_unrunnable(sched) && next->state == IDLE_THREAD)
disable_timeslice();
end:
/* do not change interrupt status */
spin_unlock(&sched->lock, interrupts);
if (prev)
switch_context(&prev->regs, &next->regs);
else
load_context(&next->regs);
}```
Do you do lazy fpu save/restore?
Oh what
that's for later
isnt userspace will cry if you save only 6 regs ?
the interrupt handler saves all the registers
when i return control flow to the thread it goes to that interrupt handler
which restores all registers
๐
i feel like making a worker thread event pool rather than spawning a bunch of threads I'm ngl
feels simpler
I already have event pools/queues from my function deferring after some milliseconds so now i just need to port that over to a worker thread event pool
garbage collection
garbage collection gameplay
longest symbol names I've written in a bit
event pool gameplay
static bool event_pool_ran = false;
static void event_pool_fn(void *arg) {
(void) arg;
event_pool_ran = true;
ADD_MESSAGE("event pool ran");
}
REGISTER_TEST(event_pool_test, SHOULD_NOT_FAIL, IS_UNIT_TEST) {
event_pool_add(event_pool_fn, NULL);
sleep_ms(50);
TEST_ASSERT(event_pool_ran);
SET_SUCCESS;
}```
I should setup testing in CI/CD ngl
I did not realize this was just a Deferred Procedure Call and just called it defer_func_t ๐
whatever it's just a name
I will probably add a central thread registry to keep track of all threads regardless of their state so stuff like htop works
๐ฅ
i am mildly concerned about the neural network scheduler idea performance wise
my current scheduling algorithm with rebalancing takes 5,000 clock cycles
I don't have too much leeway, any more than 20,000 clock cycles and ๐ฅ
I'll need to benchmark the NN
whew ok nevermind I am NOT cooked
the neural network runs in 5,000 clock cycles we're chilling (floating point)
fire
ya ok it's consistently 4-5,000 clock cycles this is baller
ok i made the NN code suck less
time to port to fixed point
I am starting to understand why science code is often so bad looking
great ok I'll train it on XOR and stuff
chat, I have ported the neural network to fixed point
time to benchmark
woah hoa HOAH
Took 124 clock cycles
Input: [0, 0], Output (fixed): 3515, Predicted: 0, Expected: 0
Took 318 clock cycles
Input: [0, 65536], Output (fixed): 61385, Predicted: 1, Expected: 1
Took 110 clock cycles
Input: [65536, 0], Output (fixed): 61446, Predicted: 1, Expected: 1
Took 290 clock cycles
Input: [65536, 65536], Output (fixed): 4780, Predicted: 0, Expected: 0```
under 400 clock cycles!!!
ok great the neural network is now pretty trivial to port, but I'll hold off on this until I get to userspace and have heavy workloads to test
oh yeah turns out the reason my kernel wouldnt boot with less than 5MB of RAM is because I forgot to -s the binary and it was building in all the debug symbols and mister bootloader didnt like that
it's 300kb without the debug symbols
time to boot on 3MB RAM
darn that doesnt work
#!/bin/bash
grep -rnE '\b(k[zm]alloc(_aligned)?)\s*\(' . | grep -v '/tests/' | while read -r line; do
file=$(echo "$line" | cut -d: -f1)
lineno=$(echo "$line" | cut -d: -f2)
next5=$(sed -n "$((lineno+1)),$((lineno+5))p" "$file")
if_line=$(echo "$next5" | grep -m 1 -E '^\s*if\s*\(.*\)')
if ! echo "$if_line" | grep -Eq '!\s*\w+|==\s*NULL'; then
echo "Possibly unchecked alloc at $file:$lineno"
fi
done``` ah yes
delightful
"Boy do I sure love adding allocation failure handling paths"
hooray it isnt exploding
all tests pass with 20 meggies of ram
all tests pass with 16 meggies of ram
I think this is good enough for now
I'll probably regularly test the kernel with low RAM usage from now on because I might end up porting this to older 32 bit systems and those aren't exactly famous for having bountiful memory
what do you use the neural network for? or rather, how do you want to use it for scheduling?
you give the neural network data about a task and it outputs a score and the scheduler runs the task with the highest score
so every time you save a thread after it finishes running, it also saves a score
and the scores are per-queue level
since it's an MLFQ
the performance overhead is shockingly small and has only decreased after I ported it to fixed point arithmetic
what kind of data?
it pretty much always takes less than 1,500 clock cycles to run no matter how complex (within reason ofc) the network is
#1378106389347700946 message
that's just a sketch
I was originally going to use manual calculation heuristics using this data and then realized "wait a neural network would be 10 times better here"
and also much easier to scale and configure and debug and stuff (and also likely be faster)
ill take a look once you implement it, seems interesting
it's mostly just a different approach to what existing schedulers already do, and I'm primarily doing this out of laziness
for example, let's say right now I'm only tracking 3 thread local variables
if I wanna do manual calculation heuristics, sure that will work fine
but later on, when I wanna track 5, 7, 13, or even more, manual calculation heuristics become very very annoying to add/remove and still remain performant
so the neural network is used here due to laziness since I don't want to constantly be rewriting a bunch of heuristic rules and the NN will likely do similar work
obviously this is going to be held off until userspace when I can benchmark real software and get real results, and it will likely take a solid few days to integrate and train the neural network
I've only been working on this project for two months lol so it might be a bit, likely until september
but I've already ported the neural network to fixed point instead of floating point and I already have the GlobalAllocator for rust implemented in the kernel so it should perfectly drop in (I wrote the NN in rust)
the rest of the kernel is in C but this will be rust
pretty fun stuff ๐
(i dont know the scheduling algorithm but) I dont really think a NN is faster than if you just did something like heuristic1*weight1+heuristic2*weight2+โฆ as that sounds like what you kind of do then
the neural network runs in under 1,500 clock cycles, with 100+ neurons (so speed isn't a huge concern) and the trouble with heuristics is that, sure they might be faster, but it becomes difficult to add/remove heuristics and test all of them, whereas with a neural network you can just say
"I want the input values of x, and y to produce an output of around Z" and you can train it offline to get neuron weights that produce exactly that result and just copy the neuron weights over
the current scheduler without the neural network runs in 6-7,000 clock cycles and as long as it stays under 10,000 to save the thread and pick a new thread I will be happy with it
also the neural network could learn and "nudge" the weights as the system runs, so if I can make training more efficient I can do that as well
and it's very easy to do this with a neural network since you have a learning rate which you can use to adjust how much you wanna "nudge" it
wouldnโt you need to run the nn on every single thread, if you want to select based on highest score? thatโd probably not scale well
no, you run it when you save the thread so the score is always there
oh right oops
so when you add a thread back into the run queues after it ran, it would compute it, and if you wake a blocked thread, it would also compute it when you add it to the run queues
the only real overhead is the linear search but that shouldn't be too bad
๐
How would you train it though, it sounds more like a RL problem then a traditional NN, and it has its overheaad and its much harder to train then traditional nn
I guess I would probably need a form of a "starvation bonus" but that would probably not be too bad, I can just use a base score and a quick heuristic to find that out
you could just have like a heap then
ya probably
wdym? I can run workloads with a set of weights, see the performance, and just go "I want the weights to go more this way" and train the neural network so the weights go more the way I want them to
fn main() {
let inputs = [
[FIXED_ZERO, FIXED_ZERO],
[FIXED_ZERO, FIXED_ONE],
[FIXED_ONE, FIXED_ZERO],
[FIXED_ONE, FIXED_ONE],
];
let expected = [FIXED_ZERO, FIXED_ONE, FIXED_ONE, FIXED_ZERO];
let mut net = NeuralNetwork::new(2, 1, 4, 1).unwrap(); // small network for XOR
let learning_rate = FIXED_ONE / 4;
for _ in 0..10_000 {
for i in 0..inputs.len() {
net.train(&inputs[i], &[expected[i]], learning_rate);
}
}
for i in 0..inputs.len() {
let tsc = unsafe { std::arch::x86_64::_rdtsc() };
let output = net.run(&inputs[i])[0];
println!("Took {} clock cycles", unsafe {
std::arch::x86_64::_rdtsc() - tsc
});
let predicted = if output > FIXED_ONE / 2 { 1 } else { 0 };
println!(
"Input: {:?}, Output (fixed): {}, Predicted: {}, Expected: {}",
&inputs[i],
output,
predicted,
expected[i] / FIXED_ONE
);
}
}``` I mean this is how training works rn
you just give it inputs and expected outputs
and I can trivially dump the weights after it runs
You don't have the expected output, that's what you're trying to figure out
I guess so, I could probably then have a set of tests I can run in the kernel where I adjust the neural network until it performs as ideally as possible
so yeah reinforcement learning like you said would probably be better
rust
This is really not a good idea to manulaly touch (or honestly even see..) the nn weights.. Looking a little into RL might be a good start but I warn you that in real life RL is very hard to train properly and usually doesn't converge
well I didn't mean "adjust" as in manually alter weights, but just training the neural network until it gets a better result
so maybe I would have some way to compute a "reward", and "nudge" the NN towards producing better "rewards"
I'll definitely look into RL then
But you wouldn't see a better result without knowing what it did wrong, you can't really backprop "how long this task done" into your net
the result would be computed with a bunch of things, not just a few fields
so I would know if it did something wrong if it, say gave a really low score to an "interactive" thread
I would define fixed cases that I know good scores for, and train the neural network to produce those, and I can just use traditional heuristics to get these good scores
and then afterwards I can have the neural network take over
But then the NN would converge to your reward function, which you can compute without NN
I dunno this is a long term thing
the reward isn't the decision tho
it's saying "how well you did"
NN is supposed to make the decision
I wont pretand to know too much about ML, and hoping this idea would work, but yeah its not as simple as plugging the nn
like if you were building a neural network to train to estimate things, the reward would be how close the estimated number is to the actual value and the neural network just trains itself until it can accurately predict the actual values in cases both covered and not covered by test cases
It would converge to maximize the reword you give it is a more accurate form, true
for example I previously used this NN to compute squares of numbers, and during training I would train it on only integers, but during runtime I could use it on floating point numbers and it would do just fine (or fairly close, within a percent or two)
Im looking forward to see how well this works in any case
I should probably just use my neural network on something normal like image recognition lmao
this is indeed silly
I will probably turn my DPC pool/worker thread event pool into a two-level MLFQ
so just high and normal
โ
defer_free gameplay
ok so my stinky and lame ringbufffer workqueue might suck when it comes to lock contention
but I also will have the problem that some defers become inefficient
like if I wanna defer a free during an ISR and I use per-core workqueues, then that defer will just run after the ISR finishes (bad) instead of putting it onto another core which can run in parallel (good)
ok, so I think I can go with per-worker-thread event pools, and have some way of communicating between them. I can use very primitive stealing mechanisms
if other.event_count > self.event_count {
if try_lock_worker_thread(other) {
self.steal_work(other);
}
}```
without the need for complex heuristics like my scheduler
then, there can be three enqueues for work
a) `event_pool_add`
b) `event_pool_add_local`
c) `event_pool_add_remote`
`event_pool_add` will choose the least loaded worker thread to add work to and wake
`event_pool_add_local` adds to the caller's core's worker thread
and `event_pool_add_remote` will add to the least loaded core that is NOT the caller's core
I can simply force the thread spawning to guarantee it spawns one worker thread per core (trivial task) to verify that all this works accordingly, and for now, one worker thread per core will suffice
this way, for things like `defer_free`, I can call out to `event_pool_add_remote`, so that another core can potentially perform the free in parallel rather than `event_pool_add` which may add to the core itself
looooooooooong
I wonder if there's a better way to signal to the reader that "hey, this technically does nothing but it implicitly means that X will happen"
whatever
ok i am gonna change my worker thread system to not be super duper dumb
each event pool (per-core) gets one worker thread that is permanent (this way if I have an event pool add from an ISR and the threads for a pool are all non-existent I do not go spawning threads in an ISR)
then worker threads (except the permanent ones) are checked and reaped if they are inactive every 5 minutes
and for all the non-permanent threads I'll just spawn them as needed
๐ง
once I go back and reimplement allocators (this WILL eventually happen) I will make them NUMA aware because I think it would be really really funny and I have everything to support that 
oh yeah, forgot to mention that "We have CONFIG_NO_HZ at home
the cores disable their timeslice if no work is to be done
so here only core 0 is being scheduled since everyone else goes "I ain't doin allat" since they're only running one thing
imma also make it disable if there is only one thing running
lol you can see the reaper thread waking in the logs when the idle thread runs and then the timeslice is disabled so it goes away
holy smokes SOMEHOW all tests pass when I run with 1 core, I had the foresight in the back of my mind to write everything so it works fine on UP systems
the zero intel atom users will be so pleased with this
oh yeah I should do the C-state thing
and the primary reason for doing this, once again, is because I am the only person who plans to run this code on a known device (thinkpad t400) and I want the battery to not immediately ๐ฅ while the code is running
bazinga
the hpet keeps bothering me and i cant figure out why 
ok it was NOT the HPET, it was the PIT
I should uh probably make a generic timer abstraction lmao
great ok that means we now have CONFIG_NO_HZ at home
I am bored, let's make our allocator lockless tonight
I'm thinking I'll have a current slab for each core, a global atomic way of allocating new slabs, and cross-core frees will add the object to be freed to a ringbuffer on the target core, and I can simply check this ringbuffer and flush it every time I want to free something before freeing said something
i think i still need locks for my EMPTY, PARTIAL, FULL lists ๐
ts so tragic
I dont wanna make a lockless SLL rn
I have an end goal for this OS I think
running fnaf

it's all a bunch of 2d sprites so the sad little igpu on my thinkpad won't have to do any heavy 3d work
CI CD is currently being very very weird
I think the mkfs version over there is doing something strange idk
yeah their version of mkfs is 1.47.0 and mine is 1.47.2 I think that's doing a weird
๐ฅ
Doubt
What's the issue
in CI/CD the superblock is read with an inodes per group of zero for some reason, I think my struct might be wrong idk
struct isn't wrong
odd
the problem is probably somewhere else
like 2 patch versions are unlikely to change anything
commit 713ecc74191d106a0c769b338644a7eb04203f5f (HEAD -> main, origin/main, origin/HEAD)
Author: gummi <[email protected]>
Date: Sun Jul 20 19:48:03 2025 -0500
[ext2]: reduce pointer dereferences``` truly incredible optimizations
u were right again
was not mkfs's problem
all tests pass now and the code is faster by a wee bit
right ok now that I have once again confirmed that no one should ever let me do DevOps, it's back to on-demand worker thread spawn and reaping
thought this looked cool so I'll save it here
ok I think I will do thread priorities properly
basically, the scheduler should go down the queues and for real time threads, it will disable the timeslice (if one is scheduled), and for timesharing ones, it will enable the timeslice, and RT threads will never decay their queue level, and timesharing ones will
lastly I will have a few background threads (I will call them background to differentiate them from the idle thread), that will only run once everything else is done
the highest queue level is THREAD_PRIO_RT, and then the next three are HIGH, MID, LOW (for timesharing) and the last one is BACKGROUND
this actually matches my bio scheduler funnily enough, which has URGENT, HIGH, MEDIUM, LOW, and background, how curious!
bing bop boom boom boom bop bam
rn my thread priorities barely exist
What was the issue?
i dont actually know, I think I was re-reading the superblock improperly (dunno why i was rereading it)
few fields became wrong
zoopisie
Might be worth looking into that further
Sounds a bit sus that a second read is different
i think I just forgot to add a byte offset
it worked fine locally and maybe mkfs just did a very slight different thing
probably just forgot to add the byte offset ya I just double checked
๐คช
no reason to fix it since the first read (that actually properly calls the function to read the superblock) is sufficient, idk why I was doing it again
lol just had a small data race due to tickless, fixed that pretty ez tho
flashback to whatever the hell this was
I still have yet to see make/qemu/whatever that was segfault itself again
there's probably a data race in qemu's emulated device
ok ok now that all that dillydallying is done and my scheduler has RT threads (done properly) I can finally go back to doing the event pool on demand thread spawning and destruction
struct worker_thread {
struct thread *thread;
time_t last_active;
bool should_exit;
bool is_permanent;
struct worker_thread *next;
};
struct event_pool {
struct spinlock lock;
struct condvar queue_cv;
struct worker_task tasks[EVENT_POOL_CAPACITY];
struct worker_thread threads[MAX_WORKERS]; // maybe don't do a static limit (?)
uint64_t head; // producer index
uint64_t tail; // consumer index
uint64_t num_tasks;
uint64_t num_workers;
uint64_t idle_workers;
uint64_t total_spawned;
time_t last_spawn_attempt;
};``` ok so I have this thing going here, each core has its own pool, and its own set of threads.
so basically
- one worker thread per event pool at the start
- when enqueuing work, check "Hey, do we have a worker that can run this task?", if not, and if the time since the last worker spawned for a pool is long enough, and if there are not too many worker threads on the pool, spawn another
- every worker will set up a timer event so that after some time it automatically checks itself to see if any work was done during that time period, and exit if not
- this time duration for the timer event is dynamic. the first few workers will have longer time durations (30s to 1m), whereas the last few workers will have much shorter ones (3s to 10s), because the cases where I spawn a bunch of workers will hopefully not be super long lived, and saving memory is nice
- eventually, all workers but the permanent one will be gone
the trouble is now figuring out the max workers per pool. the static cap here is just a placeholder, but I might use it if i can't figure something out
I think a static one will be ok
ok so I guess the plan to start incorporating this will be
- start using this system to replace the current system (just one thread for now)
- start spawning new worker threads based off of the policy
- give each worker thread an
inactivity_check_periodwhere it'll check for activity after some time and cleanup if there is nothing to do, with this value being determined based off of the value ofnum_workersat thread spawn time
๐ฅ great ok now I hopefully won't do a massive stupid whilst implementing this
i would do it as a periodic thing instead
I suppose so yea, I'll test this approach and then test that approach and just see which one does better
I just wanted the inactivity checks to be more dynamic since the first few worker threads spawned for an event pool will likely be useful for longer than the last few
oh yeah and if by periodic you meant it will repeatedly check once every inactivity_check_period, that's what I'm gonna do lol
alright chat
the job is done
on demand worker thread spawning and reaping ๐
code isn't super clean so I'll take a break and then go clean stuff up a bit
void worker_main(void) {
struct worker_thread *worker = get_this_worker_thread();
struct event_pool *pool = get_event_pool_local();
bool interrupts = are_interrupts_enabled();
while (1) {
bool idle = false;
time_t start_idle = 0;
interrupts = spin_lock(&pool->lock);
if (idle_loop_should_exit(pool, worker, &idle, &start_idle)) {
spin_unlock(&pool->lock, interrupts);
worker_exit(pool, worker);
}
do_work_from_pool(pool, worker, interrupts, &idle);
}
}``` ok this is probably readable enough for me
neat stuff
I guess I can make the worker thread stacks smaller
DPCs are pretty light
non page aligned 64 byte stack 
got bored and made panic/oops prettier
I just noticed that my tests have been consistently increasing in speed for some reason
are my features somehow making them faster
lol that's funny
like my filesystem integration test went from 12ms to 10ms to 8ms and now it is 6ms
recursive priority inheriting mutexes are a doozy
if thread RT blocks on a mutex held by thread TS, and thread TS blocks on a mutex held by thread BG, then BG must somehow realize that RT, which is on an entirely separate mutex, is in the priority inheritance chain, and BG must figure out that it must boost to RT
oh brother
hmmmmmmmmm
you got it backwards
uh oh
RT needs to be able to boost TS, and walk the chain to BG and boost that too
before RT blocks himself
ohh yeah it goes the other way
alrighty great stuff yes
I think I'll need something like a blocker chain on threads to walk... hmmmmm
ah this looks like the mintia KiWaitBlock I think
no thats diff
the thread blocking chain for locks is chained through KeThread and KiTurnstile structures
ohh ok ok ๐
ok so what I'm understanding is that each thread should keep track of the thread it is blocked on, so something like
struct thread {
...
struct thread *blocker;
...
};```
So, priority propagation chains work something like this
**BG** acquires lock A, and lock B, releasing lock B.
**TS** acquires lock B, but blocks on lock A.
**TS** marks its `blocker` as **BG**, and boosts **BG**'s priority to *TS*, and stops since **BG** has no `blocker`.
**RT** blocks on lock B.
**RT** marks its `blocker` as **TS**, and boosts **TS**'s priority to *RT*, and then sees that **TS**'s `blocker` is **BG** (now perceived as __TS__), so it boosts **BG** to *RT* as well, and then stops since **BG** has no `blocker`.
i go more like thread^.BlockedOnTurnstile^.Owner^.BlockedOnTurnstile^.Owner^.BlockedOnTurnstile...
alternating between threads and turnstiles
ohh ok yea, this was more primitive, I'm still tryna get the "Foggy Overview" in my head before I start refining things
it might be possible for me to link threads directly but theres no reason to since i have to have the turnstile anyway for other reasons
so i may as well save space in the thread by putting that link in the turnstile
oh wait funnily enough
struct mutex {
struct thread *owner;
struct thread_queue waiters;
struct spinlock lock;
bool initialized;
};```
So I kind of sort of already have a "turnstile" in my mutex (did not realize it was called that), so all this means is that I have to make my mutex have a separate turnstile object that a thread takes a pointer to when it acquires the mutex, and rather than needing the `blocker`, I can just walk `thread->turnstile->owner->turnstile...`
literallly copied you smh
you dont get the pointer to the turnstile that way
u get it by looking it up in a hash table keyed by the address of the mutex
with per bucket locking
there may or may not already be a turnstile there, theres only one if the mutex is currently being contended on
if theres not then you need to initialize one
why would this be necessary rather than embedding the turnstile in the mutex tho
two reasons
one is that turnstiles are big
if you do this, your mutex can be the size of 1 pointer
very small
true
secondly turnstiles cant go in paged pool
but mutexes can
if this is done
paged pool being swappable kernel heap
it turns out you only really need one pointer worth of overhead for each mutex
which is good if you like embedding them in structures all the time
and all the extra info only needs to scale with number of threads
rather than with number of mutexes
so it's literally just struct mutex{}; then
because its only needed when theres contention
neat!
and the max number of mutexes that can be contended on at a time is like threads / 2 (because you need two threads to contend on one mutex)
you dont strictly speaking need any more instances of contention info (such as a queue of waiting threads or priority inheritance info) than that
in practice turnstile memory usage scales with # of threads rather than # of threads / 2
because the latter would require a global free list of turnstiles which would be inefficient for locking
its easier to give each thread a turnstile when its created
and let it just use that when it contends on a held mutex and needs to initialize a turnstile due to being the first thread to do so
important to note that turnstiles are not embedded in the thread though
you might wonder why if theres a one to one number of turnstiles and threads
basically its because a thread's turnstile can outlive it
because
when a turnstile is unblocked (all waiting threads are awoken)
you cant control the order in which the threads are scheduled and cease usage of the turnstile
so if the thread that initialized the turnstile left first, and took the turnstile with it, the others would come in and try to access the turnstile and die
refcounting?
so instead each thread that blocks on a turnstile will put its turnstile on a "free list" of turnstiles which is anchored on the turnstile theyre blocking on
when each thread leaves, it takes whatever turnstile is on the head of the list with it
ohhh ok
when the last thread leaves, and sees this list empty, it takes the main turnstile itself
so theres a case where the thread whose turnstile it was is first to be scheduled in after being unblocked, and takes another turnstile and leaves
and thats basically why you cant put the turnstile in the thread
because that thread might then immediately terminate or something and then its original turnstile has outlived it and become someone else's
the turnstiles get juggled around; their lifetime is not equivalent to that of the thread they were created for
so for a pointer sized mutex, would each mutex just have a singular pointer to an owner thread, so on the fast path, I just set the current thread as the owner, never touching turnstiles, and on the slow path, I go "Hey, there is an owner here, I will find the turnstile (potentially allocating it), register this owner as the owner on the turnstile, then block on this"?
sounds good
you got one at thread creation time
ahhh right yea
if you allocated the turnstile there thered be deadlock
potentially
its very bad to put an allocation dependency in your blocking mutex's impl
wait but then what about hashing the mutex address to get the turnstile, wdym by the thread creation time? how do I know where to put the turnstile
mildly confused
- you are born with a turnstile, you are allocated one as part of thread creation
- at some point you go to grab a mutex and it is already held
- you look up the mutex in the hash table and theres no turnstile, so you must be the first guy to contend on it
- you link your turnstile into the hash chain and initialize it to represent that mutex, and then block on it
ohhhhhhh thanks ๐
I'll need to refcount turnstiles to avoid deallocating them while theyre in use probably
and have some way of deferring a free of a turnstile once everyone is done with it and the thread it was borrowed from is freed๐ฅ
hmmmmm
no you dont
the "free list" of turnstiles that is inside the turnstile thats being used for the lock is your refcount
if its empty then the refcount is 1
otherwise its >1
thats all the info u need
so by free list do you mean each waiter's turnstile, or what
i am starting to think you didnt read what i said above
i shall read again
starting here and onward
explains what i mean by free list
ohhh i see
yes
wait but if a thread blocks on a mutex how does it know where to get its own turnstile from to use for that mutex
or do I just have that as a field in the thread
this
my code for this really isnt THAT hard to read probably
birth_turnstile or something ok
ok yes first it is time to figure out how to bump a priority on a thread running on a different core and adjust things properly
so to go from TS->RT
first, adjust priority field in the struct
if thread is not running,
remove and reinsert to appropriate runqueue
no IPI
if thread is running (on another core)
send the IPI
disable the timeslice on that core, and do any other work needed
and there is no case where the thread could possibly be running on the current core because there's no way for the thread that's boosting the other thread to be running simultaneously
And for TS->Higher TS or from BG->TS there is no IPI since timeslices are not changed
Year of the charmOS desktop (I followed the post)
I've only been working on this for about two months and the codebase is only like 20 thousand lines so it will be a bit
hopefully we get userspace by september
filesystem stuff is mostly done I just need a few caches to make stuff faster and then implement my dual cache system for filesystem integrity guarantees
today is mutex improving tho
oh boy
first fixing up my RBT impl
then making it into a macro
and then using it for turnstile queues since it's O(log n) to wake the highest priority waiter
i found where freebsd puts their turnstile stuff so I'll read that too
grep -rine "target" has to be one of my most used commands probably
yippee
sys sys
forgot to say i did indeed finish the RBT only to decide that a min heap is better here
so this is useless unless i want CFS (I dont)
this is a reflection of the influence the solaris approach to SMP had on other kernels
I didn't see linux say anything about turnstiles which is interesting
solaris had the grand ambition that there would be basically no spinlocks at all outwith the scheduler itself (where what would be called 'dispatcher locks' are directly spinlocks)
ya I saw that in a post about their turnstiles, everything uses sleeping mutexes except for the scheduler
neat plan
when freebsd, also netbsd, copied a lot of the ideas from solaris, they also copied an artefact of the incomplete realisation of that plan in solaris - in solaris they wanted to thread all drivers so that spinlocks become unnecessary, but they didn't complete this
so solaris mutexes are associated with an IPL and if it's low (vast majority of mutexes) then it's a sleeping (and only adaptively spinning) mutex, if it's higher it's a spinlock
so their sleeping mutex and spinlocks are the same struct mtx and it checks the IPL to figure out what to do?
fascinating
yes, an (i think) quite inelegant thing
though the dispatcher locks, used by the scheduler itself, are true spinlocks and only spinlocks
why don't they just separate out the structs everywhere if they do have true exclusively spinning spinlocks? was it because of their idea with threading drivers?
i think to ease old drivers porting
solaris has a stable DDI
so you get an opaque "pil" ("ipl" with the "i" moved along) from certain APIs in the DDI and you initialise your mutex with this
why don't other kernels like Linux take after Solaris as well? I read Linux's mutexes and saw nothing about a turnstile
linux [$]
> grep -rine turnstile
include/drm/task_barrier.h:42: struct semaphore enter_turnstile;
include/drm/task_barrier.h:43: struct semaphore exit_turnstile;
include/drm/task_barrier.h:46:static inline void task_barrier_signal_turnstile(struct semaphore *turnstile,
include/drm/task_barrier.h:52: up(turnstile);
include/drm/task_barrier.h:59: sema_init(&tb->enter_turnstile, 0);
include/drm/task_barrier.h:60: sema_init(&tb->exit_turnstile, 0);
include/drm/task_barrier.h:81: task_barrier_signal_turnstile(&tb->enter_turnstile, tb->n);
include/drm/task_barrier.h:83: down(&tb->enter_turnstile);
include/drm/task_barrier.h:95: task_barrier_signal_turnstile(&tb->exit_turnstile, tb->n);
include/drm/task_barrier.h:97: down(&tb->exit_turnstile);
``` how curious
linux was very siloed
they needed matt dillon (then of freebsd, later dragonfly founder) to help them whip their vmm into shape
this worked out to their advantage when they got some dynix people sent to work on them who introduced dynix's RCU mechanism
as to turnstiles, torvalds despises priority inheritance
"Friends don't let friends use priority inheritance".
Just don't do it. If you really need it, your system is broken anyway.
Linus
has linux had any problems related to their lack of priority inheritance in mutexes?
https://lwn.net/Articles/178253/ this article I'm looking through is quite old
Imagine a system with two processes running, one at high priority and the other at a much lower [...]
Correct me if I'm mistaken, but does Linux actively (more actively than others?) employ lock-free techniques like "RCU", and might this assist with that issue?
yes, they've always used spinlocks a great deal in linux, which mostly obviates priority inversion in itself, and then RCU techniques recover a great deal of scalability
I suppose I'll just stick with implementing priority inheritance in this hobby kernel's blocking mutexes, whilst trying to make most uses of mutexes only result in waiting threads taking the fast path of spinning for a little bit
I avoided RCU since it has that overhead of making a duplicate copy of the data, which I was not a fan of for things like the filesystem drivers
thanks ๐
I relied on information from a wikipedia page about the prioritisation inversion problem, which states " Because priority inversion involves a low-priority task blocking a high-priority task, one way to avoid priority inversion is to avoid blocking, for example by using non-blocking algorithms such as read-copy-update."
when solaris people invented turnstiles the main motive was to make them as small as possible ?
as i understand you build pointer sized locks with them
in fact this was already possible in ancient UNIX
you could even have a zero-sized lock
since you could wait on any address or indeed any integer whatsoever with sleep()
turnstiles are distinctive in that they combine this mechanism for storing the waiters' queue out-of-line from the thing waited on, with a priority inheritance mechanism
by what way do you achieve them?
how are waiters represented? are they perhaps represented by structs allocated on the stack of the waiting thread, and ultimately linked onto a list headed by the lock-word itself?
i thought it might be, i implemented push locks once, i never did figure out how to do do priority inheritance with them though. windows did, some way or another
in windows the convoy avoidance solution was to move from waking only the next waiter to waking the whole herd of waiters at once
can you link your kernel
the hope is that if you introduce a short period of spinning on the lock before committing to sleep on it, you avoid the cost of the whole sleeping & waking step, which would dominate if the actual time spent under the lock were short
also, u do jira issues for your own kernel as a solo dev?
why did u decide to make it private?
ah
this is a different thing altogether but that reminded me of it in some way: http://www.popcornlinux.org/index.php/replicated-kernel-linux
I'm probably gonna fix up my idle thread so it's not just a halt loop
I'll probably make it more dynamic
so first it halt loops, and sets a timer interrupt for a short bit of time, like 100ms
if any work was done during that time, it resets back to the halt loop (I can use a simple state machine here), otherwise, it goes and tries to do scheduler thread work stealing
then, again if it succeeds, it resets back to going into the halt loop, but if it fails and no work is stolen, it searches for work elsewhere to migrate, such as the event pools, since these are more expensive and less impactful to go search and migrate work from
if it succeeds in a steal here, it resets to going back to the halt loop, but if it fails once again (no work migrated), it sets the processor up for a deeper sleep state (like a C level) and enters that interruptable sleep state
should be ok
ok I dillydallyed tonight and did scheduler optimization instead of that idle thread thing
the slowness was partially from an O(n) operation which could've been O(1) and mainly from me constantly doing MMIO r/w to the LAPIC to enable/disable the timeslice
got rid of most of that
task pick and switch takes 500-600 clock cycles instead of 5,000-6,000
and also made the hpet use all the cores
because it's 2025
what does that mean
It means they are using the hpet in 2025
no like, what does he mean by "all the cores"
I imagine they're broadcasting interupts via the ioapic or making use of as many hpet comparators as they can.
I was memeing earlier
autoboost ?
number 2
linuxhw is my lord and savior ๐ I can finally emulate C-states to test the idle thread
oh yeah i was testing on BIOS this whole time. I tried uefi and everything worked perfectly fine so that's neat
i am le guessing that I need to import custom ACPI tables to emulate C states
this no finds anythings
do u expect VMs to expose c state configuration?
probably not
also u need to let the firmware know u are able to control it
then it loads extra tables with c states
its not that simple
o ok
qemus blob doesnt have it ofc
you can dump your pcs tables to check what those look like
u might have to call _OSC a few times to negotiate that
I'll see if I can get tables from linuxhw to work in QEMU
nah that wont work
oh boy i get to learn ASL hooray
the c state stuff is likely in a separate SSDT so that might work
if u grab linuxhw stuff
if u grab an existing one u just feed it directly
u can use acpixtract <thing>.bin
and it will give u all tables
> acpixtract 1C0315195E84.bin
Intel ACPI Component Architecture
ACPI Binary Table Extraction Utility version 20241212
Copyright (c) 2000 - 2023 Intel Corporation
DSDT - 62200 bytes written (0x0000F2F8) - dsdt.dat
SSDT - 601 bytes written (0x00000259) - ssdt1.dat
SSDT - 2123 bytes written (0x0000084B) - ssdt2.dat
SSDT - 1183 bytes written (0x0000049F) - ssdt3.dat
SSDT - 2545 bytes written (0x000009F1) - ssdt4.dat
SSDT - 541 bytes written (0x0000021D) - ssdt5.dat
SSDT - 281 bytes written (0x00000119) - ssdt6.dat
SSDT - 1714 bytes written (0x000006B2) - ssdt7.dat
SSDT - 771 bytes written (0x00000303) - ssdt8.dat
SSDT - 1135 bytes written (0x0000046F) - ssdt9.dat``` very fancy
thanks
grep: ssdt6.dat: binary file matches
grep: ssdt7.dat: binary file matches``` in here it seems
great
then u just need to find a table which usually has CPU somewhere in its name
yeah
iasl thing.dat to disassemble
looks great
so would I just plug this thing into qemu via -acpitable
or wait no
hmmmm
I guess I'll try it just to see if anything pops up in my search for the _CST
btw the first line here references a dead rvalue
u can find the object its trying to reference and paste it in the file and then recompile it
the uacpi_namespace_node_name?
or what
so there isn't really a way to make qemu emulate the ACPI tables of a whole machine? interesting
sad
well yes u dont get to control cpu power states as a vm
No _CST on _PR_ -> not foundhmmm that _PR_ wasn't there before so something happened
pr is a predefined object that always exists
ohh ok
so I suppose I'll decomp the /* External reference */s and copy them into this file and recompile it again
u can just do
Device (\_PR.CPU0) { }
the main thing u have to find is the CPU0._CST table its trying to return
ssdt 7 has a few things (I don't copy the text to discord since the indentation is super big and it would look bad)
yeah u might be fucked here
uh oh
since its not a hardcoded table
i guess it constructs it by reading some proprietary stuff
from registers etc
cant u just hardcode one yourself?
do you have a better one i could use or nah
for testing
i guess but im not very familiar with asl and that would take a bit