#HelixOS (ARM64 OS, not dead project!)
1 messages ยท Page 3 of 1
Well I now know what the bug is.... because of the face I've placed the heap at the end of the kernel, and map it to 0xFFFF800000000000, and the kernel is placed at -2GB... by HHDM offsetting no longer works in the traditional way. So I might want to tweak a few things or just place the heap elsewhere
Going to work on a much needed proper virt2phys funciton to handle the job. I didn't like the other hackish idea I had for this...
Good luck!
Thank you good sir! I've got that implemented, and now I get the proper physical address that I need instead of doing my semi-shitty HHDM virt2phys lol
I still don't, for me so far just slab allocator and a page frame allocator have been enough
chunk list allocator is best
a slab allocator is an implementation of a heap
I suppose it could be. I've never really thought of it as a heap but I guess you're right.
qemu: Guest says index 65535 is available
Go figure this starts to happen after I start deciding to use the heap....
of course XD
I swear QEMU don't like me or something ๐
i was talking about a stack of Chuck
I finally took your advice, and implemented that virt2phys function in my VMM as I've finally got a simple heap in place, and actually needed it
everybody end up needed it
I placed my heap just after the kernel, and mapped it to 0xFFFF800000000000 since the virtual base for me is 0xFFFF000000000000...
My previous virt2phys was a little shitty lol
okay
Have to fix the virtio driver to use this now too or really anything that makes used of kmalloc now that needs the physical base of something. Otherwise I get some ridiculous address value.
good
and then you work on the vfs ?
Absolutely!
Now that I've got a better allocator in place I feel better about starting the VFS
I got rid of the slab allocator too it served it's purpose
I don't know what or why now all of a sudden QEMU is saying:
qemu: Guest says index 65535 is available
I've been debugging what might be wrong, and it seems to happen either after I signify the queue that in ready, and update the ring index.
And what really irritates me is the fact it decides to manifest after I switch to a heap allocator. I mapped a 16MB range just after the kernel an mapped it to 0xFFFF800000000000 (yes it's contiguous). Apparently it doesn't like that.
So the rest of the virtio driver will use the kernel heap, but the queue itself for now is going to fall back to the PMM. At least till I can figure out why switching to the heap is causing this to fail
Annnnnnd new heap bugs have manifested..... apparently it thinks it's already out of memory....
Mmm, more bugs, everyone's favorite lol
It's good for a well balanced breakfast ๐
I think my issue though is my heap currnetly doesn't align anything... and one of the heap blocks that get allocated end up corrupted too
Oh God 
Probably overwriting the metadata causing this to happen in the first place... so I need to make sure I'm aligning things properly
I guess 16MB if heap clearly wasn't enough... so I increased it to 256MB
I need to work on making sure the returned address is correct for each block, and that I properly free, and coalesce the freed blocks to prevent fragmentation
a good heap can be increase at runtime if needed
I had plans to implement expansion when needed, but since this is the initial implementation I'm trying to get things in working order first...
Right now I'm trying to work on splitting available regions or just allocating whole blocks, and ensureing they're aligned properly
mmm... okay
I barely started workin on the new heap yesterday too. The virtio's discriptor stuff don't like the heap so for now I had to fall back to the PMM for now
I also will want to implement kmalloc_aligned().
I have an idea... allocate alighed heap blocks of 4K, and use a slab allcoation method to allocate smaller chunks?
why not just use a linked list?
i managed to make mine work, so now i can alloc any size i want
(does not need to be aligned)
That's kind of what I'm attempting to do right now, but I've been running into issues for larger allocations of 16K or so
I was thinking to combine the two ideas... allocate 4K alighed chunks for large allocations, and smaller allocations I can just do kind of a slab allocation method
i did something similar
i alloc the normal 4096 pages
then subdivide those into chunks of the size i request
but in a buddy like system, so they can easily be merged back
Ahhh I see then; I've got a region just after the kernel that I've decided to make use of for the kernel heap, and mapped it to my desired virtial memory region
Right now I've been testing a simple chunk allcoation method with allocations being 8-byte aligned and this is where I realize and see issues occur for large allocations....
I also implemented a proper virt2phys function in my VMM where I can finally properly get the physical address of page table entries, etc...
This was needed since the heap kind of branches away from the whole HHDM. And this a design decision on my part...
So now I don't need to use my hack-ish macro for the virt2phys calculation
I kind of have an idea in mind of how to combine the two. I'm hoping this plays nice with the virtio driver as it needs a contiguous 4K aligned region.
So far I'm really liking the idea. I'm in the process of getting things setup, but hope to get to a stage of being able to test allocations tonight. Otherwise I'll have to do that tomorrow
Feels nice to finally move onto a heap instead. The last allocators server their purpose to get the kernel going....
I think a better approach would be to have the heap allocate page-aligned allocations, and have a slab allocator like before just split these as needed. So instead of using the PMM I just use the heap
Just kind of playing around with different ideas...
So I thought of something different. Have kmalloc() allocate off the pre-reserved heap region, and have kmalloc() aligned allocate a page (or pages) when requested. I'm not sure if that's a very efficient approach, but was an idea I had.
Starting off simple...
Test #2 seems to have passed...
void *ptr = kmalloc(1024);
printf("ptr = 0x%lx\n", (uintptr_t)ptr);
void *ptr2 = kmalloc_aligned(16 * 1024, PAGE_SIZE);
printf("ptr2 = 0x%lx\n", (uintptr_t)ptr2);
void *ptr3 = kmalloc(40 * 1024);
printf("ptr3 = 0x%lx\n", (uintptr_t)ptr3);
kfree(ptr2);
kfree(ptr);
void *ptr4 = kmalloc_aligned(100 * 1024, PAGE_SIZE);
printf("ptr4 = 0x%lx\n", (uintptr_t)ptr4);
kfree(ptr3);
kfree(ptr4);
Currently no heap expansion at this time, but plan to implement this tomorrow
Just a simple heap for now....
I realize instead of placing the heap at the end of the kernel I should use some of the other usable/reclaimable regions to avoid issues.
Bits 55:58 are implementation-defined / free or at least from what I've read about. So I can use one of these to mark the heap when mapping, and just have my kernel handle this accordingly.
Seems I've got some currently unused usable regions that are around 50MB or so that I can use for the heap space.
This would probably be a better idea anyways... I just hope it plays nice with my virtio driver...
Looks like the heap will get around 101MB of usable space.
Now, if I decide to throw in the reclaimable memory this might be increased somewhat
I need to come up with how I'll fix this virtio driver since it required a physically contiguous memory region. Unless I take a chunk of the HHDM and reserve this for some kind of dma_alloc(), but right now that's not ideal as I was hoping to use the heap for these allocations.
๐
๐ญ
I'm not even sure what in that message caused automod to trip ๐
well walls of text/code are against rules in the server so maybe there's a max message length?
Ahhh that makes sense then
Lots of ffffff maybe. ๐
Most likely ๐
Your pmm doesn't support allocations above one page?
It does, but since I'm using the heap I have to work things a bit differently to make it work...
And the heap is using the rest of the usable/reclaim region after the first largest chunk, and mapped to the heaps virtual address region
Just kind of reworking things in the kernel currently for allocations..
probably the repetition, nice bypass though ๐
that's a common approach, slabs for sub-page sized allocations and then something for the larger allocations, which are usually rarer.
in the past I've implemented sub-page allocations as a page allocated from the pmm and accessed via the hhdm, and larger allocations falling back to the vmm.
you have to be careful with dependencies there though, as now anything that makes large allocations may depend on the vmm - and everything that depends on.
This is something I'm learning now since the virtio as well as other devices that required DMA need a 1:1 mapping. So trying to make the virtio queue use the heap region causes it to say this:
qemu: Guest says index 65535 is available
yeah for dma purposes you dont want to use the heap
I had a feeling... I'll probably want the queue to use the PMM or implement something to allocate and mark page frames as DMA use only
unless you're using the iommu devices will be using physical addresses, so you need a way to get those from whatever your code on the cpu is seeing.
so yeah, you can allocate pages from your pmm and access them via the hhdm on the cpu, makes it easy to work with them that way.
there's also MDLs, which are quite a handy primitive to have.
That's the case for me since the virtio device driver's only understands physcial addresses
This is the first I hear of MDL's
yeah, the page tables only apply to the code running on that cpu core
mdl = memory descriptor list. You can implement them in your vmm. The idea is you say "hey, I want a list of pages that back this region of virtual memory" and you get them. If you support swapping/paging-to-disk then this would also pin those pages in memory, the intent being that those pages are now stable and wont be moved. Meaning they're safe to hand out to devices.
Ahhh I see! I'm just barely getting a working read-only ext2 driver going so once I further get things working, and implement write support I can then implement paging to disk at some point... I already had this planned out since I'll really want to do this in general
it allows your driver code to allocate a chunk of virtual memory, and when the device needs to use it, you get the mdl and tell the device about the physical pages in the list. The device will need to support scatter-gather if you're going to pass it > page-sized regions.
This is quite interesting actually
its pretty handy
Scatter gather lists are everywhere in io
I think I'll want to implement this into my VMM in this case. That way stuff like the virtio driver's queue, etc, don't get paged/moved at all
iirc virtio still requires contiguous physical regions for the queues, but it plays nicely with SGL stuff elsewhere
but you can just use a single page for each queue ๐
Which will be nice... I just have to rework a few things ๐
Thank you for telling about this good sir! I'm going to go research more on this concept, and get started
This will be very handy for DMA-related stuff
no worries and good luck ๐
Thank you! ๐
Lol
my connection show nothing i clikced on then it clicked two times so i relicked
my connection is realllllly slow
Working on MDL's today
Hmm... I wonder if I can do a combination of using the kernel heap to store the MDL data itself, but have the physical pages backed by the PMM. Sounds like a good staring point.
typedef struct {
uintptr_t virt_base; // Virtual base of MDL
uintptr_t phys_base; // Physical base of backing pages
size_t length; // Length of MDL range
size_t page_count; // Number of physical pages backed by this MDL
bool is_pinned; // Prevent pages from being swapped
void *data; // Raw pointer to memory
} vmm_mdl_t;
Simple starting point...
why virt and phys base?
It's a Memory Descriptor List for DMA-related allocations
oh ok
This will be for my virtio driver, and other stuff that may need the HHDM. It also allows me to pin this memory so that it's not swapped or evicted when needing to free up pages
It's a new concept r4 told me about last night so I'm still learning about it
wdym by free up pages btw
If pages are needed or can be paged to disk then they can be. This MDL part allows me to protect the allocated region so that it's not paged out if pages are needed by other processes, etc...
I would recommend it... it's quite and interesting concept
virtio_queue_t *virtio_blk_queue_init(size_t queue_size) {
vmm_mdl_t *virtio_mdl = NULL;
virtio_queue_t *vq = NULL;
size_t desc_size = VIRTIO_DESC_ALIGN(sizeof(virtio_desc_t) * queue_size);
size_t avail_size = VIRTIO_AVAIL_ALIGN(sizeof(virtio_avail_t) + sizeof(uint16_t) * queue_size);
size_t used_size = VIRTIO_USED_ALIGN(sizeof(virtio_used_t) + sizeof(virtio_used_elem_t) * desc_size);
uintptr_t desc_offset = desc_size;
uintptr_t avail_offset = desc_offset + avail_size;
uintptr_t used_offset = avail_offset + used_size;
size_t total_size = VIRTIO_ALIGN(desc_size + avail_size + used_size, PAGE_SIZE);
// NOTE: Device drivers like virtio are DMA only so heap allocations don't work here
virtio_mdl = vmm_mdl_alloc(total_size);
vq = (virtio_queue_t *)((uintptr_t)virtio_mdl->data);
vq->desc = (virtio_desc_t *)((uintptr_t)vq + desc_offset);
vq->avail = (virtio_avail_t *)((uintptr_t)vq + avail_offset);
vq->used = (virtio_used_t *)((uintptr_t)vq + used_offset);
for (size_t i = 0; i < queue_size; i++) {
vq->desc[i].next = 1 + i;
}
vq->free_desc = 0;
vq->last_used = 0;
return vq;
}
Yeah but for stuff like this you don't want to just have randomly freed from memory... in this case my virtio queue descriptor stuff... that would cause all sorts of issues lol
Take your time! I wanted to get disk access going early on
That would be bad to if I was doing a read from the disk, and got the queue freed mid-read ๐
I think I may want to just do a direct mapping of the kernel heap instead of remapping it to a different region....
My vmm_mdl_alloc() calls to kmalloc() to store it's header accordingly, but of course the PMM is used to allocate the pages that are needed for the DMA stuff
Yeah I think I better just do that instead.... it'll me easier to manage the MDL allocations along with other things.
You can't use a phys base and page count
Because there's no guarantee the page frames are physically contiguous
You need an array of page frame numbers
Ahhh I see... I better adjust this then
The part I'm having a bit of a hard time with is thinking about how to provide the allocated frames for the MDL stuff. I plan to have it make use of the PMM of course since r4 did tell me this should be seperate from my kmalloc(), etc
At least I'm trying to think of a clean way to distribute allocated page frames.
will beat me to it
a list of page numbers/physical addresses will work, or an array.
as for how to get the pfns, you get the page mapped for each page-sized chunk of the virtual region. If you're doing demand paging stuff, then take care of that first ofc.
Right now I have all of the physical memory directly mapped into kernel space (like Limine does) so I know it'll be easy for me to just provide a set of page frames for this. But I'm wondering if I should keep a page count as well as the regions start and end address? Just for housekeeping, etc, for later of that DMA memory is no longer needed?
keep what you need to get the job done
I think I have something like: addr_space, virt_base, offset, length, pages[].
length + offset can be used to calculate the number of pages, you can make a nice macro for yourself too
I think that's what I'll end up going for in this case
Do you think I'd be able to reserve a part of a specific virtual memory region for this or does stuff like this require a 1:1 mapping? Even if I'm using direct mappings?
Alright I have an idea of how to get started with this.
typedef struct {
uintptr_t virt_base; // Virtual base of DMA region
size_t length; // Total requested length
size_t page_count; // Total number of backing pages
uint64_t offset; // Offset into page
bool is_pinned; // Whether pages are pinned or not
uint8_t **pages; // Array of backing page frames
} vmm_mdl_t;
Thinking something like this maybe?
#define VMM_MDL_PAGE_IDX(offset) ((offset) / PAGE_SIZE)
#define VMM_MDL_PAGE_OFFSET(offset) ((offset) % PAGE_SIZE)
Simple way to handle the page frames. Wtill a work-in-progress though...
Got a good and simple way to get pages, etc...
I'm starting to realize that maybe I should of kept my slab allocator still. I can always bring it back if I need it.
I just notice cases where I can allocate a slab for header information on DMA stuff and not waste a whole page on it
why did you get rid of your slab allocator
Because I wasn't thinking.... I'm kind of kicking myself in the ass for it now. I plan to bring it back tomorrow though
You just like randomly deleted it or what
No.. I had removed it when I was getting started on the heap thinking I wasn't going to need it. I was wrong, and learned that I should have different memory allocation API's for things...
I'm kind of realizing my mistake in removing the slab allocator
This is true... removing that slab allocator was a bit of a mistake. I plan to bring it back, but most likely have the pages that the heap uses be remapped elsewhere
yeah that could work, I do have to question representing the array of pages as uint8_t**, why not paddr_t*, or even paddr_t [].
and if I was being especially critical, do you really need that bool now? Save yourself the bytes until you get there, you might find other ways to do that ๐
qemu: virtio-blk missing headers
Apparently virtio doesn't like the fact I'm trying to do this..
I'll get it figured out
This is true.... I guess something like this instead:
typedef struct {
uintptr_t virt_base; // Virtual base of DMA region
size_t length; // Total requested length
size_t page_count; // Total number of backing pages
bool is_pinned; // Whether pages are pinned or not
uintptr_t pages[]; // Array of backing page frames
} vmm_mdl_t;
Okay, finally got my virtio driver working with this, but still need to adjust how I'm getting an allocated page frame from within the list.
Right now it works fine though
This was a bit of a pain in my ass to get working correctly....
I was thinking of haivng a part of memory used for DMA use
Nah the current method of allocation page frames is fine.
I guess since the reserved bitfields are implementation defined from what I understand I can also use that to mark pages as "pinned". And then just do a check for this when going to unmap I guess, but the VMM might need some slight work for this
Since I planned to reintroduce my slab allocator for kernel heap paging I was also considering remapping pages used by the heap into a different virtual address region.
I do use a bit to mark heap allocations.
I know that request descriptors also must be 1:1 mapped (HHDM works too of course). So I will need to implement slab allcoation as before for handling smaller allocations.
Might have to consider a few allocation API's for this too.....
I guess I could have a slab allocator that can be used for DMA-related stuff, etc. And combile this with the MDL stuff, and then have my kernel's kmalloc() use it's own slab allocator
From some information I've gathered I guess Linux does something similar to this as well. Kernel heap pages in my kernel can also be paged out when needed so this will be great. Motivates me to get that EXT2 driver going. I do need to work on the VFS design before I can continue though, but I'll do it in parts as I develop the filesystem.
For now since I got the beginnings of that MDL stuff going I'll work on the kernel heap slab allocator. After this I'll start working more on the DMA slab allocator, and have this and the MDL stuff kind of together for this.
Got a hell of a lot of work to do for sure.
I'll be doing a slab allcoator like before, but this will be for the kernel heap since it's a form of heap allcoator. I'll map the frames I get from the PMM to the heap virtual region, and any allocations larger than what the slab supports will just have full pages allocated to satisfy the allocation needs. After I'll work on the MDL DMA allocation API stuff.
Got a pretty good idea of what I want to do here
Kernel slab heap allocator is coming along. Just kind of taking time to decide on how I want to handle allocations, etc. Any length that's not supported by the slab will just be backed by pages to satisfy the requested size. After freeing I'll just unmap that entry or if it's a slab allocation just return the allocated memory to the freelist...
Well.... it wasn't until now I discovered a slab bug I had in my old code. The free list pointer I was setting ends up writing too far outside of it's page boundary, but I never noticed this due to the fact I was mapping all physical memory directly. Now that the heap handles setting it's own pages for isolation from other mapped pages the bug has manifested itself.... interesting
So basically an out-of-bounds write bug...
Just noticed my in my VMM for the vmm_unmap() if it returns it never releases the lock. Need to fix this
Otherwise I'll hit a deadlock
Well... apparently I spoke too soon because now when I acquire a lock in the VMM I deadlock ๐
I need to take a look and see where I'm forgetting to release the lock, etc...
Well, one issue I can see is that I try to take the lock twice when getting a physical address of the PTE. Not good...
@spring sonnet do you have any expรฉrience with ps2 keyboard i'm on my ps2 driver for the last 2 week a.d it still don't work ?
I don't sadly
Finally got my kmalloc() and MDL to play nice together. Far from perfect, but this is some really good progress.
The plan I have for larger page allocations is to just reuse the allocated pages and keep a free list of of them.
Lots of work being done on memory allocation for sure
Whats a kernel beyond a bloated memory allocator ๐
Because I'm autistic I can't tell if this was sarcasm or you were being serious.
Sarcastic but a little bit serious, good and versatile allocator is great to have
Oh, okay! I was a little worried there haha
It'll be much improved over time of course, but for now it'll work out...
Welp... the new heap allocator mostly works... need to take a look at the slab allocations ensure the freed slab objects are actually probaly being placed back into the freelist. Or if out of objects to create new one, and allocate from that.
Ahh... turns out I forgot to handle when new objects needed to be allocated if they run out....
I think I might be hitting a deadlock somehere in my heap allocator or VMM....
Yup it's a deadlock....
Fok
I can see too were I forget to release the lock before returning a pointer to allocated memory......
Heeey that's certainly improvment!!! I'll continue this tomorrow....
After fixing a few bugs with not releasing the lock a few times, and an out-of-bounds read bug I finally got things working ๐
My new heap allocator seems to be working fine too
Got one big ass commit to push tomorrow
soooo good bro!
I think for now I should get basic scheduling going...
yeah prob lol
round robin is pretty easy
but idk if its good lmaoo
That's actually what I was looking into doing haha
I'll take a look and read up on some stuff
Forgot to remove that comment before committing... I think I'll keep it there for a nice reminder...
it's so sad you can't edit commit (wihoot removing the commit of course)
I can always just leave it there for a reminder... since the repo right now only had the development branch
That'll definitely make sure you remember lol
amend before pushing
Seems from what I've seen about saving a task context on arm64 only x19-x30 (general purpose registers) need to be saved. Along with elr, and spsr according to the ABI
Kind of makes me wonder though about x0-x18 though.
I'll learn more as I research about this...
iโd guess theyโre the caller saved registers
That's what my thought was as well
x0-x8 are arguments and out regs, x17 and x18 might be clobbered by linker veneers before the callee is even entered, i assume x9-x16 are just marked as caller-saved
Idk why esr is part of the context, you'd often reorganize esr into something that makes sense for your kernel
That makes sense I had remembered about x0-8, but wasn't sure if they were really needed to be saved; I now know the answer to that question...
I realized pretty quickly that ESR is really only used for exception handling as far as I'm aware so I don't really need to fiddle with it during a context switch...
Yeah, the esr hold a lot of info but in exception handling you quickly get rid of the information
That's what I had realized haha; I only really need that information when an exception occurs, and after that it'll end up discarded
Was reading/learning about the round robin scheduling. Got a very very quick and dirty test going because I really wanted to test, debug, and expand my knowledge more.
After some bug fixing it seems to work...
I'm going to spawn more tasks just to test things, but this is pretty cool. I guess it makes use of the timer for this so I'm glad I had that already implemented before hand.
task scheduler amazing ! (will you do smp suppory at some point ?)
It's in it's very early stages as I design the round robin scheduler implementation.... but yes I did have plans for SMP at some point. I do need to adjust my GIC driver as well to handle multiple CPU's instead of just the one which won't really be diffuclt to handle
cool
Slowly making some progress....
I need to debug and see what caused the exception though. I'm pretty sure it's in my __context_switch() handler.
slowly !!! that actually pretty fast
I spent most of today reading about the round robin stuff, and got a start on something extremely simple to play around with... this will be changed up over time
My ass forgot to load x1's context, and ended up doing x0 again so that really caused some weird bugs...
Hell one of them was the fact when I returned from the exception I ended up in kslab_alloc() lmao
I'll be redoing that code anyways. I wanted to play around with an idea that I had to get a visual example of how it would work
This is going to be brutal to implement.
Trying to think of some design ideas for this round robin scheduler. I decided upon a free list queue, but something I was thinking about is since I planned to have the timer be used for this I don't like the idea of a schedule() function handling the timer reset since this is done in the timers IRQ handler. I know I'd need to keep track of the number of ticks per process so that if a process is finished before it's quantum is up then I can handle that accordingly or switch to the next task.
@unreal creek Didn't you implement a round robin scheduler? Or did you end up going with something else
yes
i have a round Robin
May I take a look at how you've implemented your scheduler to get some insparation? I want to better understand how I should handle the task setup, queue, and switching of tasks
okay so the basic stepcis that i have a function yeld() that switch ot the next task
the function yeld call the function schedule that tell to what process to switch to
and then it call context_switch (coded in assembly) to do the actual context switch
https://github.com/tayoky/stanix/blob/main/kernel/task/scheduler.c
https://github.com/tayoky/stanix/blob/main/kernel/arch/x86_64/pit.c
https://github.com/tayoky/stanix/blob/main/kernel/arch/x86_64/switch.s
if you want to take a look
An 64 bit OS made from scratch. Contribute to tayoky/stanix development by creating an account on GitHub.
An 64 bit OS made from scratch. Contribute to tayoky/stanix development by creating an account on GitHub.
Thank you good sir! I want to spend some time studying this before I get started
if you have any question you can ask me tomorow i'm gonna sleep
bye
Sounds good to me!
I'm just now learning that I can make this preemtive multitasking work through the IRQ context. Didn't even think about that.
I guess it's also "standard" to re-enable the timer in a schedule() function just before a context switch? It doesn't feel right to me, but I guess if that's what I have to do.
It's all starting to make sense to me now.
Now I'm understanding the flow of resetting the timer, and scheduling the next task.
#define TASK_STACK_SIZE (16 * 1024)
#define TASK_QUANTUM_MS 20
#define TASK_STATE_READY (1UL << 0UL)
#define TASK_STATE_RUNNING (1UL << 1UL)
#define TASK_STATE_BLOCKED (1UL << 2UL)
#define TASK_STATE_DEAD (1UL << 10UL)
typedef struct {
uint64_t x19; // 0x00
uint64_t x20; // 0x08
uint64_t x21; // 0x10
uint64_t x22; // 0x18
uint64_t x23; // 0x20
uint64_t x24; // 0x28
uint64_t x25; // 0x30
uint64_t x26; // 0x38
uint64_t x27; // 0x40
uint64_t x28; // 0x48
uint64_t x29; // 0x50
uint64_t x30; // 0x58
uint64_t sp; // 0x60
uint64_t elr_el1; // 0x68
uint64_t spsr_el1; // 0x70
uint8_t __pad[16]; // 0x80
uint128_t q[32]; // 0x80 - 0x280
uint64_t fpcr; // 0x280
uint64_t fpsr; // 0x288
} __attribute__((packed)) task_context_t;
typedef struct task {
task_context_t ctx; // Task context state
int task_id; // Unique task ID
int quantum; // Amount of time task has run for
uint64_t task_state; // Current state of task
size_t stack_size; // Task stack size (default 16KB)
void *stack; // Task stack pointer
struct task *next; // Next task in queue
} task_t;
Currently what I have in mind for the task information design, etc.
For some reason trying to debug context switches in vscode in QEMU causes my system to freeze. This is twice in the last two days I had to reboot.
It only happens in the while(1); loop in the next task I set, but I'm able to exit QEMU so it's no problem.
Well, I can start a new task, but as soon as I yield shit breaks. This is going to be difficult to debug.
ldr x19, [x1, #X19_OFFSET]
ldr x20, [x1, #X20_OFFSET]
ldr x21, [x1, #X21_OFFSET]
ldr x22, [x1, #X19_OFFSET]
ldr x23, [x1, #X20_OFFSET]
ldr x24, [x1, #X21_OFFSET]
ldr x25, [x1, #X19_OFFSET]
ldr x26, [x1, #X20_OFFSET]
ldr x27, [x1, #X21_OFFSET]
ldr x28, [x1, #X28_OFFSET]
ldr x29, [x1, #X29_OFFSET]
ldr x30, [x1, #X30_OFFSET]
Well...... I now see exactly why shit broke....
Whooooops
To make debugging a little easiest I suggest making snapshots and using them to rewind, look into qemu-img , and then in the monitor using savevm or loadvm
I assume the rewind feature in in QEMU's monitor, right?
But I will be making use of that feature...
Apparently using spinlocks in my schedule() function wasn't such a good idea. Even though I check the if the current task is the only one running, and return I make sure I'm releasing the lock, but the next time I take the lock I deadlock.
Removing them seems to fix that issue
Yeah, it lets you rewind to an earlier state (or fast forward, though it would overwrite memory) for quick iterations and still be in the same session
It became a must have tool for me to debug some problems, esp if you attach a debugger on qemu
That's going to be a really nifty tool to make use of for debugging then! I never knew QEMU even had such a feature haha
sched_create_task(&helix_init_stage2);
sched_start_task();
void helix_init_stage2(void) {
task_t *t = sched_get_current_task();
printf("Hello from task: %d!\n", t->task_id);
while(1) { sched_yield(); }
}
Doing a simple test....
Holy fuck I actually did multitasking!!! But I need to look into the crash
Duuuude that's pretty cool!!!
Nice!
Thank you good sir!! You should of seen my face when I seen the output hahaha
It's the first time working with multitasking so this is really exciting
So after a little testing you want to progress to userland?
That's the goal; I do need to get a VFS in place and cleanup my EXT2 driver a bit, but it'll be nice to test some userland stuff...
I think the crash might be related to a stack overflow issue. I originally was giving each task a 16KB stack, and increasing it to 64KB seems to allow for it to run a bit longer, but of course the crash happens again. I'm not too sure how large I should make the stack, but I also need to check and see if I'm doing anything to cause this to happen in the first place
Yep it's a stack overflow
Seems it's my first stack that's causing the overflow. Did a stack canary for testing, and printed the task ID.
Going to spend some time on fixing that, but I'm really excited to test context switching
I'm almost wondering it it's an issue in my allocator (kmalloc)
you could use the same struct for intterupt conext and task context (that what i'm doing)
That actually sounds like it would be a smart idea honestly...
I'm checking to see if my kmalloc() is doing any shenanigans that might be causing issues
But so far I have bare bones context switching and multitasking... Despite it being semi-broken lol
to avoid issue make sure yeld and schedule never call any memory management function
I don't think they are at all as far as I'm aware; The only time any memory allocation gets done is during the task creation, and after that nothing else touches it...
really good because for some People they use free in tje scheduler and then everything go wrong when you add mutex or things like that
I did learn not to use spinlocks in my schedule() function as well... I had ran into a deadlock issue
Looks like my kmalloc() has a few bugs I need to work out........
@unreal creek I can't help but feel like my context switching somehow runs out of memory and eventully overflows the stack.
context switching ? out of memory ? the conext switching should not allocate memory
hoooo stack overflow
how big is your stack ?
It doesn't though... I just notice when I run more than one task it preempts them for some time so that each task gets to run, but over time I somehow run into a translation fault
Each tasks stask is 64KB
what is a translation fault
It's a page fault
This is my current scheduler setup. It's quite bare minimual, but seems to work fine for some time when doing multitasking, but after some time I end up running into a page fault. I'm not too sure yet if it's when I'm loading/storing the contexts states
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
Yeah task #1 seems to run perfectly fine. It yields, and and because it's the only task running it continues. No crashes so far so I'm not doing something right here....
probably saving context
That's what I was thinking too....
This my current context switch/restore assembly code if you'd like to have a look
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
i think pushing things on stack is simpler also my level of aarch64 assembly suck and i can't really help on that
sorry
That's alright!! I'll keep debugging and see what I can find....
I wonder if I need to push anything onto the stack before the switch?
Is this something you handle on x86_64 at all?
If you exit from the middle of c function then make sure you return to the same spot, the compiler probably allocated stack and you never reach the point where it release it
So far the three test tasks don't exit, but yield instead.... the first task seems to run fine since it's the only one in the queue, but once I start adding 2 or more to the queue that's when things go south. I'm also checking my context switch code to make sure I'm not doing anything incorrectly during the load/store parts
Well that's one thing off the right off the bat I can tell is another issue
Duplicate saving
yes
I figured haha; I noticed some issues in my context switch code that I'll need to resolve...
One was the duplicate saving, and another was that I wasn't saving the fpcr and fpsr registers....
I'm also reloading them twice from the other context... wtf lol
mmmm.....
is it working now?
I'm still fixing a few things up, but I'll be testing soon
Does all of your tasks share the stack?
No, they all get their own stacks allocated. After making those adjustments it looks like my tasks all are running perfectly!!
Fuck... I spoke too soon... crashed lol
I suggest to log your stack after returning from yield, if I understand your test task correctly
Make sure its the same for each task, and not increasing
That's my next plan; I also noticed something a bit odd here:
Taking exception 4 [Data Abort] on CPU 0
...from EL1 to EL1
...with ESR 0x25/0x96000004
...with FAR 0x2a0
...with ELR 0xffffffff8001c09c
...to EL1 PC 0xffffffff80010200 PSTATE 0x3c5
The faulting address (FAR) shows something completely invalid too
A fucked up stack could cause this
You can look at your disassembly for the elr and see the address register is loaded from the stack
If not then the context for that register is broken
I actually just took a look at something too...
aarch64-linux-gnu-objdump -d kernel/kernel.elf | grep -A20 8001c09c
ffffffff8001c09c: f9415000 ldr x0, [x0, #672]
ffffffff8001c0a0: 92400000 and x0, x0, #0x1
ffffffff8001c0a4: f100001f cmp x0, #0x0
ffffffff8001c0a8: 540000e1 b.ne ffffffff8001c0c4 <schedule+0x6c> // b.any
ffffffff8001c0ac: f0000840 adrp x0, ffffffff80127000 <irq_handler_table+0x1dc8>
ffffffff8001c0b0: 91090000 add x0, x0, #0x240
ffffffff8001c0b4: f9400000 ldr x0, [x0]
ffffffff8001c0b8: f9400fe1 ldr x1, [sp, #24]
ffffffff8001c0bc: eb00003f cmp x1, x0
ffffffff8001c0c0: 54fffe61 b.ne ffffffff8001c08c <schedule+0x34> // b.any
ffffffff8001c0c4: f9400fe1 ldr x1, [sp, #24]
ffffffff8001c0c8: f9400be0 ldr x0, [sp, #16]
ffffffff8001c0cc: eb00003f cmp x1, x0
ffffffff8001c0d0: 540000a1 b.ne ffffffff8001c0e4 <schedule+0x8c> // b.any
ffffffff8001c0d4: d2800280 mov x0, #0x14 // #20
ffffffff8001c0d8: 9400043e bl ffffffff8001d1d0 <timer_set>
ffffffff8001c0dc: 97fffe8b bl ffffffff8001bb08 <__daif_clr>
ffffffff8001c0e0: 14000018 b ffffffff8001c140 <schedule+0xe8>
ffffffff8001c0e4: f9400be0 ldr x0, [sp, #16]
ffffffff8001c0e8: f9415000 ldr x0, [x0, #672]
ffffffff8001c0ec: 927ef801 and x1, x0, #0xfffffffffffffffd
The very first load there it might be that I'm probably accessing an invalid field most likely
x0 seems to be NULL in that case too...
That's what I've been thinking too, but it's weird that it runs fine for a good amount of time, but then ends up crashing
Not really, a fucked up stack can be fine
Yeah but look into how you got x0, since yield call
Something I realize I think I'm supposed to do is store x19-x30 onto the stack including the pointer to the task in x0. I noticed a bug when I don't do this that the first task gets ran twice instead of taking turns.
Going to spend today reworking some stuff..
@unreal creek
new_task->stack_size = TASK_STACK_SIZE;
new_task->stack = kmalloc(new_task->stack_size);
if (!new_task->stack) {
printf("SCHED: Failed to allocate new task stack! (out of memory)\n");
kfree(new_task, sizeof(task_t));
spinlock_release(&s);
return;
}
memset(new_task->stack, 0, new_task->stack_size);
// NOTE: Stack must be 16-byte aligned
uintptr_t sp = (uintptr_t)new_task->stack + new_task->stack_size;
sp = ALIGN_DOWN(sp, 16);
new_task->ctx.sp = sp;
new_task->ctx.elr_el1 = (uint64_t)(uintptr_t)__start;
new_task->ctx.spsr_el1 = __spsr_read();
Here's how I'm setting u pthe current stack. I do make sure it's aligned on 16-byte boundaries, but I'm not sure if my curent way of setting up the stack is causing issues. I don't see though how on even 64KB of stack space it stil eventually crashes unless my logic is messed up
yep seem good
does kmalloc take bytes as parameter and note a page count ?
It takes in a byte count; If the length is 2K or less it'll use the slab allocator otherwise anything else gets whole pages
Probably going to re-write all of the scheduler shit. I'm starting to get a bit irritated with the stack corruption and other tasks not properly running after the first two if they don't yield.
@spring sonnet i will switch to using sgdisk and qemu-img totally, mind telling me how to use sgdisk?
i saw i need to make a loopback file to the image file?
that seems unrelated to the thread ๐ญ
i talked with him on this thread about this earlier
i should have asked on misc tho
mb
There's a lot of stuff you can find on sgdisk usage
It's a pretty simple utility, and making use of qemu-img over dd is even better
I also understand how the stack overflow was occuring too during the context switch within the timer IRQ handler....
Right now I'm taking some time to learn how to handle such a thing...
Well, not just that but I'm also realizing other things about saving/loading the context, etc, and how changing the stack was kind of messing this up
At least for how I was handling things.
Yes, when an interrupt (IRQ) occurs on an ARM64 CPU, the CPU saves the current execution context before jumping to the interrupt handler. This saved context is later restored when the interrupt handler completes, allowing the CPU to resume its previous execution.
This is is helpful to know. I thought I had to handle saving the current context myself, and this is where I started to struggle to understand about scheduling tasks, etc.
Now it's making sense where the idle task comes into play.
Now I'm beginning to better understand with the more knowledge I gain.
void sched_create_task(void (*__start)()) {
task_t *new_task = NULL;
uint64_t *sp = NULL;
new_task = kmalloc(sizeof(task_t));
if (!new_task) {
printf("SCHED: Failed to initialize new task context!\n");
return;
}
// Initialize new task stack
new_task->stack_size = TASK_STACK_SIZE;
new_task->sp = kmalloc(new_task->stack_size);
if (!new_task->sp) {
printf("SCHED: Failed to initialize new task stack!\n");
kfree(new_task, sizeof(task_t));
return;
}
// Set temporary stack pointer to setup task context
sp = (uint64_t *)((uint8_t *)new_task->sp + new_task->stack_size);
// NOTE: Registers are stored/loaded in pairs!
for (uint64_t i = 0; i < 10; i++) {
*(--sp) = 0xDEADB33F; // x19-x28
}
// x30 -> return address of new task
*(--sp) = (uint64_t)__start;
// x29 -> frame pointer
*(--sp) = 0xDEADB33F;
new_task->sp = sp;
new_task->task_pid = task_pid++;
new_task->task_state |= TASK_STATE_READY;
if (!task_list) {
// First task in queue
task_list = new_task;
} else {
// Link task to end of queue
task_t *prev = task_list;
while(prev->next != NULL) {
prev = prev->next;
}
prev->next = new_task;
}
}
A very simple task setup function for now....
I also setup an idle task that calls another function to schedule the next task or just idle in a low-power state and wait for the next interrupt. This is a much clearner version of what I was doing too.
I can't ever seem to get context switching right. I somehow end up in a loop in the context switcher itself or I return back to the sceduler.
oof. good luck, scheduler bugs can be a hell to fix
what's your context switch looking like?
.global __context_switch
__context_switch:
// x0 -> pointer to current task_t struct
// x1 -> pointer to new task_t struct
// Store callee-saved registers
stp x19, x20, [sp, #-16]!
stp x21, x22, [sp, #-16]!
stp x23, x24, [sp, #-16]!
stp x25, x26, [sp, #-16]!
stp x27, x28, [sp, #-16]!
stp x29, x30, [sp, #-16]!
// Store SP in task context struct
mov x4, sp
str x4, [x0, #0x08]
// Load next task SP
ldr x4, [x1, #0x08]
mov sp, x4
// Restore callee-saved registers for next task
ldp x29, x30, [sp], #16
ldp x27, x28, [sp], #16
ldp x25, x26, [sp], #16
ldp x23, x24, [sp], #16
ldp x21, x22, [sp], #16
ldp x19, x20, [sp], #16
// Return to new task (x30 holds return address)
ret
It's rather simple where I pass in task structs to store/load the previous, and next tasks. It works with loading the idle task, then the next task (test task), but then when the test task yields it ends up in some kind of a loop in the context switcher or ends up returning to the scheduler. I've been having a helish time trying to figure out why this happens
I was thinking I might have to write the yield code in pure assembly
Yeah it's an ARM64 OS project
Are there any interrupts?
If you dont keep elr you'd gonna end up in a very nasty place
I do have the timer IRQ that increments a systick count, but the IRQ handler vector code saved the context (ELR, and SPSR included) and jumps to that code. Once it returns it gets restored but I feel like my logic for how I'm trying to handle this is a bit off. My context switcher doesn't currently handle saving ELR or SPSR as I thought this would cause issues when trying to return from the timer IRQ
I just notice that it works fine to idle till the systick time reaches the max quantum, then executes the next task in the queue, and when that task yields to give up it's CPU time that's when things seem to break
Yeah id wager its related to timer honestly, what do you do one getting a timer and decide its time to switch tasks?
So, here's how I handle incrementing the systicks witin the timer IRQ:
void timer_irq_handler(void) {
uint64_t curr_time = timer_virt_get_time();
uint64_t deadline = curr_time + saved_ticks;
timer_disable();
sys_ticks++;
__cntv_cval_write(deadline);
timer_enable();
}
And this is the task that checks for the max quantum
static void sched_idle_task(void) {
while(1) {
uint64_t curr_ticks = timer_get_sys_ticks();
if (curr_ticks >= TASK_QUANTUM_MS) {
// Reset sys_ticks ticks
timer_reset_sys_ticks();
// Run next task
sched_run_next_task();
}
printf("Idling...\n");
// Idle till we have another task to run
wfi();
}
}
This is the task that handles switching when the max quantum (10ms) has been reached and it's time to switch to the next task
__attribute__((noreturn)) void sched_run_next_task(void) {
task_t *prev = task_list, *next = task_list->next;
// Look for next task to run
while(!(next->task_state & TASK_STATE_READY) && next != task_list) {
next = next->next;
}
next->task_state &= ~TASK_STATE_READY;
next->task_state |= TASK_STATE_RUNNING;
task_list = next;
__context_switch(prev, next);
__builtin_unreachable();
}
Then my very simple logic for switching to the next task in the queue....
It works for the first two tasks, but when returning from the 2nd task breaks. I can't really make it sit and wait for an interrupt because then it ends up blocking the exection too...
Okay so your timer doesn't preempt the task, ig that's fine for now
I wasn't sure if the timer IRQ in particular needed to handle the task switch, but didn't like the idea of having any of that done in the timers IRQ handler
I mean you'd end up in the mercy of the task otherwise, if I make a task that wouldn't call yield you can't preempt it otherwise (no multi cores shenanigans)
But the less code in your timer handler the simpler it is, so for start thats a good idea not to preempt
That's what I thought as well. I wanted to keep the timer handler clean, and just increment a global variable that the scheduler code can get from there
I'll continue to debug and examine what's going on. I'll eventually figure out a solution to this
Im not sure I follow your code, how would you ever return to the idle task after the process finished his quota?
That's something I was thinking about too. I was thinking I might need some kind of "trampoline" that can handle something like this
Sounds like a horrible design decision though
That or I might need to rethink how I'm doing this
Yeah I think the idle and timer relations are both a little bit broken, if your idle is a task then it doesn't yield but wait until itself get an interrupt and then decide its been too long until the other task (which one?) Ran for enough and it transform itself and would never return, and if it would then that's a problem (unreachable + no return).
A small note if you use a lot of unreachables, I'd make a macro that assert you never got there, something like panic followed by builtin unreachable
For instance if you disable timer you'd stuck in an endless loop in idle
Yeah I think I better rethink how I'm doing this then...
I think I will implement a macro to asset and kernel panic for cases like this as well
Yeah I suggest taking a step back and thinking how do you want your timer to work, stuff like
What does it mean if a task finished its quota?
If a task kept yielding, does it need to be stopped through timer?
Is there one timer for the entire system or is it per task?
Yeah there's just the one timer for te whole system
I do plan to take a step back and think about this for a little bit...
Taking some time to study this topic a bit more, and understand how preemptive multitasking should work
I think it's the right thing to do in this case
Currently just researching about round robin context switching, etc, to learn more on this
I'm glad I decided to take a step back and retink this a bit.
This looks interesting.
This register will be extremely useful.
Im away from laptop at the moment, but iirc gcc would use it for tls, be wary of using it yourself
I dont recall if it uses el0 or el1 register for the kernel
I see this also see this is an attribute in GCC?
When you use thread local, I think gcc compiles into read from tpidr, idr if it use el0 or 1
Hmm.... seems it also provides some isolation, etc, for threads to store their own copy of local vairables, etc. Perhaps this may be useful for my use case in for the scheduler design?
I think it wouldn't really help you, and i advise against using arch specific mechanisms explicitly for generic stuff
I mean if gcc already use it then you'd get in trouble if you use it as well
This is true....
usually tpidr_el1 is used to store either a pointer to a Cpu structure for the current cpu or a pointer to the current thread's Thread structure
tpidr_el0 you shouldn't use in the kernel because its used by userspace for tls (and can be read/written by userspace directly)
I probably shouldn't mess with it then in this case...
I've also been learning about how Linux tackles the preemptive multitasking. It seems you can use the IRQ vector to restore to the next task context. I didn't know this could be done. Right now my IRQ vector code just saves the context, handles the IRQ, restores the current context, and returns from it
I was maybe considering adopting this approach
Looks like I'll be doing so.
Time to get working on this design a bit
I finally understand how context switching is actually supposed to work. It's not as difficult as I remember, and my last attempt was almost there. I'm glad I took a step back to relook over this.
Think today's task will be to design and implement task creation.
task_t *sched_create_task(const char *name, void (*__start)()) {
task_t *t = NULL;
uint64_t *sp = NULL;
if (!name || !__start) {
return NULL;
}
t = kmalloc(sizeof(task_t));
if (!t) {
return NULL;
}
strncpy((char *)t->task_name, name, sizeof(t->task_name));
t->task_id = next_task_id++;
t->task_resched = false;
t->status = TASK_READY;
t->ctx.elr_el1 = (uint64_t)(uintptr_t)__start;
t->ctx.spsr_el1 = __spsr_read();
t->ctx.fpcr = __fpcr_read();
t->ctx.fpsr = __fpsr_read();
t->stack_size = TASK_STACK_SIZE;
t->stack = kmalloc(t->stack_size);
if (!t->stack) {
kfree(t, sizeof(task_t));
return NULL;
}
// Temporary stack pointer
sp = (uint64_t *)(t->stack + t->stack_size);
// x19 -> Exception return address
*(--sp) = (uint64_t)t->ctx.elr_el1;
// x20 -> Saved program status register
*(--sp) = (uint64_t)t->ctx.spsr_el1;
// x19 -> FPU control register
*(--sp) = t->ctx.fpcr;
// x20 -> FPU status register
*(--sp) = t->ctx.fpsr;
// Set x19-x30 to clean state
for (uint64_t i = 0; i < 10; i++) {
*(--sp) = 0x00; // x19-x30
}
// Set current stack pointer
t->ctx.sp_el1 = (uint64_t)sp;
t->ctx.sp_el0 = 0x00;
return t;
}
For now, a really simple task creation function. This of course is just to get started.
I don't want to start complicating things on myself early on.
It does help to understand how the stack work as well so that I can properly setup the task stack. That way when I go to restore the task's context everything will get loaded properly.
__aarch64_irq_handler:
// Save callee-saved register contents
stp x19, x20, [sp, #-16]!
stp x21, x22, [sp, #-16]!
stp x23, x24, [sp, #-16]!
stp x25, x26, [sp, #-16]!
stp x27, x28, [sp, #-16]!
stp x29, x30, [sp, #-16]!
// Save ELR and SPSR
mrs x19, elr_el1
mrs x20, spsr_el1
stp x19, x20, [sp, #-16]!
// Save FPU control/status registers
mrs x19, fpcr
mrs x20, fpsr
stp x19, x20, [sp, #-16]!
// Handle IRQ
bl irq_handler
// Handle task scheduling, etc
b __ret_from_irq
__ret_from_irq:
// Check if task needs to be rescheduled
bl sched_maybe_reschedule
// If no task needs to be recheduled return to previous context
ldr w0, [x0, #TASK_CTX_RESCHED_OFFSET]
cbz w0, __ret_to_prev
// Perform context switch
__ret_to_prev:
// Exit back to previous context (no context switch)
// Restore FPU control/status register
ldp x19, x20, [sp], #16
msr fpcr, x19
msr fpsr, x20
// Restore ELR and SPSR
ldp x19, x20, [sp], #16
msr elr_el1, x19
msr spsr_el1, x20
// Restore callee-saved registers
ldp x29, x30, [sp], #16
ldp x27, x28, [sp], #16
ldp x25, x26, [sp], #16
ldp x23, x24, [sp], #16
ldp x21, x22, [sp], #16
ldp x19, x20, [sp], #16
// Return to previous task
eret
Still working on this, but kind of getting things ready for context switches in the IRQ handler.
Proceeding with the knowledge I have gained from looking over how such things are handled.
Well, looks like I might need to make a few changes already with how I'm braching in different conditions....
Somehow I broke the execution flow for after the IRQ handler returns. For some reason trying to branch to different lables is causing issues. This sucks
Oh God now I'm breaking things....
Apparently I do need to also save x0-x18....
Welp...
That means I better make sure that they get saved during a context switch.....
Remember not to save the irq handler registers but the before you enter irq
I mean unless you want to return to the irq handler, which is also an option
It seems I do need to save x0-x30 in the IRQ context so that other arguments, etc, are preserved before hand. Otherwise it seems only saving x19-x30 (including ELR and SPSR) seem to cause things to break
I don't recall on top of my head what's the abi, but if all of your asm follows abi the callee saved registers shouldn't change (irq_handler should save/restore them and so does sched_maybe_reschedule)
In the irq you must ensure all of the registers wouldn't change, if you decide to come back to the irq handler after your task been rescheduled (easier imo) then make sure all of your context switch follows the abi and irq handler ensures no register change
I know during IRQ entry, all general purpose registers must be saved, but then this kind of starts throwing stuff off for me on context switches.
I thought I had a grasp on how the flow is going to work...
You can try looking from a prospective of a callee saved and a caller saved from the moment a process calld yield until it returned and make sure the value are the same
(If its a callee saved, caller saved can be trashed before/after yield)
Because I've decided to go for preemptive multitasking instead of the former it seems that I will indeed need to save all of the general purpose registers (including special registers)
Mainly because when the task is interrupted I won't know what was being used so it's best to save things as it is...
I'll want to make sure my timer does atomic operations as well when handling the systick count or resetting it. Otherwise things might get a bit hairy.
__aarch64_irq_handler:
// Save general purpose registers
pusha 0
// Save ELR and SPSR
mrs x19, elr_el1
mrs x20, spsr_el1
stp x19, x20, [sp, #-16]!
// Save FPU control/status registers
mrs x19, fpcr
mrs x20, fpsr
stp x19, x20, [sp, #-16]!
// Handle IRQ
bl irq_handler
// Check if we need to rescheule
bl sched_maybe_reschedule
cbz x0, __ret_to_prev
// Switch context
__ret_to_prev:
// Return to current task
// Restore FPU control/status register
ldp x19, x20, [sp], #16
msr fpcr, x19
msr fpsr, x20
// Restore ELR and SPSR
ldp x19, x20, [sp], #16
msr elr_el1, x19
msr spsr_el1, x20
// Restore general purpose registers
popa 0
// Return to previous task
eret
A bit better improved version of what I was trying to do.. The macros for push and pop take a "null argument" so the compiler is happy (so I just pass zero in this case).
Still in the works of course, but so far it's looking good...
btw @spring sonnet have you ever tested on real hw?
I don't have any ARM hardware to test on unfortunately. QEMU's virt board is my only development platform
Going to get started on kernel threading.
Going to spend today studying and implementing mutex(s). I realize I'll need these as well.
This will be interesting learning about thread blocking, unblocking, etc.
So, from what I've seen/leanred I can make use of the TPIDR register in EL1 since my kernel will be the only one to use this, but I'll want to avoid using EL0's TPIDR register since GCC will be making use of this. I've been studying different thread implementations, and decided this is probably the best way to go on setting the current thread, etc.
Learning threading and mutex(s) has been a pain in the ass. It's a lot to take in.
Think of the bright side, as long as youre running qemu under x86 you wouldn't likely to face the very annoying memory coherency bugs and they would only pop up when trying to run in actual hw (ask me how i know ๐ )
๐
Are you saving a pointer or only the threadID btw?
I'm saving a pointer to the current thread struct
Great, otherwise you could've used TPIDRRO_EL0
I was thinking the EL0 version of this register was reserved for use in userspace only, so I opted to use the EL1 variant instead
Yeah, there's tpidr for el0 and el1 but also tpidrro for el0, a register that's read only from el0
I wonder if I can make use of both of them from EL1? I'm making use of TPIDR_EL1, but not TPIDR_EL0 yet
Im quite sure you need to set tls for the thread when doing user mode, you can search tls_thread_switch() in linux for instance
Sounds good to me! I'll get right on it
The part I'm trying to think about is how I'm going to cleanly implement keeping track of a threads quantum. My original approach was just preempt a task when the sys_tick count reached the set limit, and would switch to the next task.
I'd like to try and do better than this, and implement something better, so I need to spend a little bit thinking about this.
Well, I seem to have another allcoation bug manifesting.....
Need to debug why kmalloc() is returning NULL trying to allocate a thread struct
Oh no wait! It wasn't that! It's because I currently don't have a thread struct stored in TPIDR_EL1 right now, so the check for it fails. Whoopsie
I think I better just make a check or something for this so it doesn't assert
Now I somehow broke my IRQ vector code, and it crashes on IRQ return again.....
Ahh turns out that wasn't it. It's an issue in my thread_create() function.
I've got so much shit to work on......
I'll come back to the thread and sheduling stuff another time. I'm going to work on the VFS and cleanup my EXT2 driver..
There's always more stuff to work on, dont sweat it youre in a good pace
Thank you! ^-^; I figured I best work on some other parts of the kernel for now, and just come back to the threading, etc
You're the most productive when working on stuff that's interesting to you at the moment, make sure to keep everything you're working on on a separate branch so you wont lose track and wont have to complete an entire task just to change tasks
I absolutely agree! Right now the VFS and EXT2 driver stuff sounded a bit better to work on for now... I did make sure to setup my kernel in a dev branch rather than a release one, so that I can make changes when I need to. I still do try to make sure the kernel is buildable though, but changes are made that might break other things temporarily
Yeah i mean not a dev branch but rather branch for each task, threading and ext2 for instance
Yeah, i mean that's standard ๐
Well, I think I better do just that... ๐ ; It'll help me keep better track of things as well
Yeah and you can make small commits that will help you figure out what you were just doing earlier, and before merge you'll squash them all into one
I dont know why hobby os don't normally do prs honestly, at least until they are large enough that an alternative is non substantial
I really should start doing this in more projects as well...
Probably, I wish I've learnt how useful git is during uni
There's still things about git that I learn about that I never knew existed ๐
never merge again
use git pull --rebase 98% of the time it work
Got the ext2 branch made to work form there, and can just merge the changes to the main dev branch when it's ready
I like the idea of different branches better...
I didn't know git had a way to stash untracked files, and now I get to re-write my VFS code from last night ๐ญ
Got some VFS work to do today, and have to rework my EXT2 driver for this as well.
EXT2 branch has been made, and can commit ext2-specific stuff there...
God damn it I need to redo my run script.... Otherwise the EXT2 partition ends up invalid ๐
Got everything back in order, etc, and am getting the VFS going. Need to figure out how I'm going to rework the EXT2 driver for this...
You can pop the stash too, unless you delete .git or intentionally do some shenanigans, git wouldn't delete your changes beyond recovery
I had some untracked files that I didn't yet commit, and didn't realize I could do git stash --all, but had just ran git stash by itself. I didn't loose anything else other than the starting VFS code I was working on last night, so it's not a major loss
At least now I know this for in the future, but that really was not fun to deal with lol
I don't think I understand, did you delete untracked files accidentally because you thought they were stashed?
No, when I ran git stash, created the new branch, and switched to it I noticed that it had lost a few things. That's probably also due to the fact I ran git reset --hard HEAD~3 which was probably why that happened...
A hard reset can be undo by reflog to find the missing commits - hard reset only works on tracked files
A stash can be undo via git stash pop in your new branch
I did try to do that before, but told me the repo was up-to-date. I'm sure I might of did something else that I'm not remembering for that to not work at all, but I was able to get back to where I was previously
Now I can get started with populating the root node for the vfs...
Yeah you probably done something a little wrong, I suggest whenever youre in a weird git state to ask for help so you'd learn for next time
I certainly will! This was a lesson learned for sure...
Now I'm just getting irritated with this VFS design.
I can't think of any other way of implementing it either.
Mostly irritated because I got told my VFS design is "wrong".
Oof, sounds rough
I normally don't let it get to me, but tonight just wasn't a good night for that
I'm just doing some small rework to my vfs_mount() to better handle what I'd like for it to do.
I'm redoing the entire vfs.
I mean I don't think there is a way to do it "wrong" as vfs is not a single specific thing, your impl is just not what it usually looks like as it stores stuff like inode/atime/name in the generic struct
Well, that, and I'm realizing how crippling this design is, so I decided to make some changes to improve it...
Or at least that's what I'd like to do.
Going to re-work some of the VFS today
I'll be redoing my VFS. The current design is just crippling.
Good Luck!
Thank you! ๐
typedef struct vfs_superblock {
vfs_dirent_t *root;
vfs_fs_type_t *fs_type;
} vfs_superblock_t;
typedef struct vfs_fs_type {
char fs_name[VFS_MAX_NAME_LEN];
vfs_superblock_t *(*fs_mount)(const char *device, const char *path, uint64_t flags, vfs_fs_type_t *fs_type);
void (*fs_kill_sb)(vfs_superblock_t *sb);
} vfs_fs_type_t;
typedef struct vfs_node {
uint64_t inode;
uint64_t type, mode;
uint64_t atime, ctime, mtime;
size_t length;
char path[VFS_MAX_NAME_LEN];
void *fs_private;
} vfs_node_t;
typedef struct vfs_dirent {
vfs_node_t *node;
uint64_t type;
uint64_t mode;
uint64_t offset;
uint64_t length;
struct vfs_dirent *parent;
struct vfs_dirent *children;
char name[VFS_MAX_NAME_LEN];
} vfs_dirent_t;
typedef struct vfs {
vfs_fs_type_t *fs;
vfs_superblock_t *sb;
struct vfs *next;
} vfs_mount_t;
So, here's the new idea I have in mind. It's minimal for now, but considering my EXT2 driver currently only supports reads (no writes yet), I think this will be just fine.
Okay, the design is slightly being changed...
Okay this design isn't any better. I'm starting to piss myself off with this.
I'll get it figured out..
isn't vfs design fun ๐
I cam be fun... right now I'm just having a hard time thinking of a decent, minimal design to get started....
I'm looking at Klangs VFS design for some inspiration. Tomorrow I plan to rework everything once more....
in klangs vfs , finddir is just lookup
Taking a look at their design looked really nice, and since my old design was kind of similar to that I figured I can just rework some things..
And since I already have a good start on EXT2 I can just rework what I have there too to get me started
So, after studying Klangs VFS work for a little bit, I can see it's almsot similiar to how my original implementation was. This is really nice to see that my idea wasn't absolute shit.
I think I'll adjust the old design, and try to make things a bit more clean.
i mean it has directory cache
I don't think mine had any caching at all
well wihoot directory cache you can't really mount stuff
I don't know if it counts as caching, but I did keep a linked list of registered filesystems. But the way I was handling the mounting of EXT2 was really crippling, so I had to rethink the design
Thing is though I notice most of these design use tmpfs where I'm going to be mounting ext2 directly as root ("/")
that what i said you should have a tmpfs qs root then mount the ext2 and (eventually) chroot in to it
btw i won't really considear Toaruos as an exemple for fs stuff as it is mostly made to work with the iso filesystem
In my case I would like to mount the EXT2 partition as the root filesystem rather than having tmpfs right now and chrooting into it....
I can always use tmpfs on things like /dev when it comes time to implement tmpfs
I mean, that's good for stuff like userspace, but it's not an elegant solution for the kernel
Well, I realize why I was having a hard time with my old design... I should be storing the dirent struct within my vfs_root, which in turn holds pointers to nodes, etc.
The way I was attempting to do this before was just storing a pointer to a node, and this started to complicate things on myself.
@worthy lynx Hey, are you still working on your kernel?
I started a new one a few days ago but Iโm taking a break rn
I completely understand! It's always best to take a break, and come back to it when you feel up to it
typedef struct vfs_dirent {
vfs_node_t *node;
struct vfs_dirent *parent, *first_child, *next_sibling;
char name[VFS_MAX_NAME_LEN];
} vfs_dirent_t;
typedef struct vfs_opts {
vfs_dirent_t *(*fs_lookup)(const char *path);
vfs_dirent_t *(*fs_mount)(const char *path);
} vfs_opts_t;
typedef struct vfs_node {
uint64_t inode;
uint64_t uid, gid;
uint64_t refcount;
uint64_t type, mode;
uint64_t atime, ctime, mtime;
size_t length;
uint64_t num_links;
struct vfs_node *link;
void *fs_private;
char name[VFS_MAX_NAME_LEN];
} vfs_node_t;
typedef struct vfs_mount {
char fs_type[VFS_MAX_NAME_LEN];
uint64_t type;
vfs_dirent_t *root;
vfs_opts_t *fs_opts;
struct vfs_mount *next;
} vfs_mount_t;
So, this is still based off on the old design, but make some small changes. Right now, this should be fine for getting started, and I can always add/remove stuff later.
This feels just as crippled as the old design. I can't think of a better way to implement this.
Going to take a look at some vnode docs.
typedef struct vfs_dirent {
vfs_node_t *node;
struct vfs_dirent *parent, *first_child, *next_sibling;
char name[VFS_MAX_NAME_LEN];
} vfs_dirent_t;
typedef struct vfs_opts {
vfs_dirent_t *(*fs_lookup)(const char *path);
vfs_dirent_t *(*fs_mount)(const char *path);
} vfs_opts_t;
typedef struct vfs_node_opts {
// open(), create(), etc
} vfs_node_opts_t;
typedef struct vfs_node {
uint64_t inode;
uint64_t uid, gid;
uint64_t refcount;
uint64_t type, mode;
uint64_t atime, ctime, mtime;
size_t length;
uint64_t num_links;
struct vfs_node *link;
void *fs_private;
vfs_node_opts_t *fs_opts;
char name[VFS_MAX_NAME_LEN];
} vfs_node_t;
typedef struct vfs_mount {
uint64_t type;
vfs_dirent_t *root;
vfs_opts_t *opts;
struct vfs_mount *next;
} vfs_mount_t;
Think this is slightly better....
typedef struct vfs_dirent {
vfs_node_t *node;
vfs_mount_t *mounted;
struct vfs_dirent *parent, *first_child, *next_sibling;
char name[VFS_MAX_NAME_LEN];
} vfs_dirent_t;
typedef struct vfs_opts {
vfs_dirent_t *(*fs_lookup)(const char *path);
vfs_dirent_t *(*fs_mount)(const char *path);
} vfs_opts_t;
typedef struct vfs_node_opts {
// open(), create(), etc
} vfs_node_opts_t;
typedef struct vfs_node {
spinlock_t s;
uint64_t inode;
uint64_t uid, gid;
uint64_t refcount;
uint64_t type, mode;
uint64_t atime, ctime, mtime;
size_t length;
uint64_t num_links;
struct vfs_node *link;
void *fs_private;
vfs_node_opts_t *fs_opts;
char name[VFS_MAX_NAME_LEN];
} vfs_node_t;
typedef struct vfs_mount {
char fs_type[VFS_MAX_NAME_LEN];
vfs_dirent_t *root;
vfs_opts_t *opts;
struct vfs_mount *next;
} vfs_mount_t;
I think this should do just fine... I don't feel irritated with it either, so that's good...
@hallow comet Does this look sane enough for a VFS? ๐
Looks good to me (although tbh I don't know what a sane VFS is supposed to look like XD)
This is true haha; It feels like a better design than how I was originally doing things, so this should be fine...
vfs_dirent_t *ext2_mount(const char *path) {
uint32_t ret = 0;
vfs_dirent_t *v = NULL;
ext2_inode_t *root_inode = NULL;
uint32_t ext2_idx = gpt_partition_get_index("Rootfs");
uint64_t ext2_offset = gpt_partition_get_offset(ext2_idx);
if (!path) {
return NULL;
}
v = kmalloc(sizeof(vfs_dirent_t));
assert(v != NULL);
strncpy(v->name, path, sizeof(v->name));
sb = kmalloc(EXT2_SUPERBLOCK_SIZE);
assert(sb != NULL);
ret = virtio_blk_read((uint8_t *)sb, ext2_offset + 2, EXT2_SUPERBLOCK_SIZE);
assert(ret == 0);
assert(sb->ext2_signature == EXT2_SIGNATURE);
root_inode = ext2_read_inode(EXT2_ROOT_INODE);
assert(root_inode != NULL);
v->node = kmalloc(sizeof(vfs_node_t));
assert(v->node != NULL);
v->node->inode = EXT2_ROOT_INODE;
v->node->ctime = root_inode->ctime;
v->node->atime = root_inode->last_atime;
v->node->mtime = root_inode->last_mtime;
v->node->uid = root_inode->user_id;
v->node->gid = root_inode->group_id;
v->parent = NULL;
v->child = NULL;
v->sibling = NULL;
strncpy(v->name, path, sizeof(v->name));
v->node->fs_private = (ext2_inode_t *)root_inode;
return v;
}
A bit of a scary mount funciton.... lol
Not completed yet, but currently will work fine...
I think I only understood 10% of that function lol. But imma take your word on it
Lol shounds good to me
It's just basically reading out the ext2 root directory inode, and populating the root vfs node...
So basically "mounting" on "/"
ohhhh, okay that makes sense
The VFS & EXT2 are both in their early stages, so things may change over time design-wise....
For now, this will work just fine
....this vfs design feels kind of ridiculous....
make it better >:D
I'm going to re-write it.... It's not working out like I thought it would ๐
Got some progress made, but also just made some small changes to the VFS idea.
Needed some insparation, so I took a look at how Vinix was handling this, and gave me the idea to have a set of registered file systems. This will make things a bit more clean, and I can keep my vfs_mount() the way that I wanted it.
Finally understand how my VFS design is going to work, and will start getting things in place. For now, today's task will be to read the directory structure vfs the VFS.
Want to keep things simple, and steady.
So far, this is feeling like a better design than before. It's alightly modified, but it's kind of inspired by how Vinix handles mounts, and file system registering, etc.
Feels good to get some progress made finally.
Currently trying to decide on how to implement look-ups. I was thinking to check if an existing node was already populated for the requested entry, and if not, locate the entry within the EXT2 file system. Then, I can populate a new node(s) as needed.
that'd be a very good feeling, congrats
Thank you! ๐
it's pushed ?
Partially, but I'll be pushing the current changes right now. Look under the ext2-dev branch
okay i wiil take a look
Sounds good! Be warned, it's all still a work in progress....
Path creation if not already cached is working quite well
Thank you!! Yes, I'm going with EXT2 ^-^
do you have a devfs or equivalent?
Not currently....
I do plan to implement it eventually... I wanted to get my EXT2 filesystem mounted, then take it from there...
fair. You can always have a TempFS and have that support devices rather than having devfs+tempfs being separate (that's what I do)
imo tempfs is a better thing to build your vfs around initially but anyway
That's actually something I was wanting to do was implement tmpfs, and just use that to support devices, etc...
It'll make life much easier lol
yeah sounds good
cos then you can also use that same tempfs to unpack your initrd onto etc
(well not the same tempfs, on a different tempfs instance at a different mountpoint, but ykwim)
Oh absolutely; I did eventually plan to support ramdisk loading, and mounting the rootfs from there, but that's very much a work in progress..
fair enough. tmpfs dev can be a little tricky to get right honestly
Seeing others that have been working on tmpfs, I expect it to be a tough beast like EXT2...
But I'm willing to take on the task
Being able to get the VFS going finally is nice. I was fighting bad with the design decisions lol
a tempfs is simple enough, but making it genuinely good and clean is hard
For now, I think getting a functional implementation will be good enough, then I can worry about improving it later down the road
yeah
I'm already having to fix my ext2's internal lookup. I had some bugs I overlooked with it ending up in an infinite loop
Insane work on the vfs! Iโm on my first attempt right now. You wouldnโt happen to sit on any resources? Having a hard time finding any, Iโve already read Sunโs vnode paper a couple of times already lol
Thank you!! ๐ I've just read up on the Sun vnode paper, and started taking a look at other existing projects like Vinix to study their designs. It really helped to get some insparation
Right now, I've been working on doing path lookups, and can't decide if I should just not cache nodes in the vfs for now, and worry about that part later... lol
Feel free to use my design for insparation if needed as well! Be warned though that it's currently a work-in-progress, so things may change here and there
Well, I've moved my caching node logic into the VFS layer where it needs to be. Now, I can start calling my EXT2 lookup to locate the node if it's not already cached, and this will keep things fairly clean....
I still eventually need to handle indirect nodes for larger file sizes, but for now I won't worry about it too much
Well, so far root directory lookups are working great. I need to try and work on subdirectory lookups now.
Each nodes fs_private field conatins a pointer to it's ext2 inode in memory, so that it can access later. So far, I'm off to a good start....
Going to start implementing the ability to open/close/read files for now. Write support is planned later in the future...
Very much a work-in-progress, but I can actually open files now....
nice
Thank you!!! I do need to work on some stuff, and have it make calls to the filesystems actually open() function, but wanted to also test path look-ups...
So, today's task will be working on opening/closing/reading from a file, but doing so through the VFS
I also need to work on permission checks.... ๐
Doing some simple permission checks, and ensuring the operation isn't trying to perform writes (for now...). I plan to implement write support later down the road...
Pushes the new changes to the EXT2 development branch. Right now, it's quite simple, but I'm able to open files now, and do proper checks (somewhat) to ensure I'm not attempting to open a directory itself, and that if trying to create/write/append to files I return -EROFS. Very much a work-in-progress....
I think next up will be adding the ability to close a file, then work on reads...
I do also need stuff for doing stats on a file, etc... it'll all come as I go
Feels nice to get some progress made
Let's fucking go!!!
I've got simple reads working. Now, I do need to handle other cases for things like singly, doubly, and triply indirect blocks, but for now this works.
@unreal creek
I've also pushed the codem but of course this is all a work-in-progress. Let me know your thoughts!
I should test placing this under /home/helix/test.txt and really test my VFS capabilities
Yeah... I have bugs in the VFS to fix for those cases, but I'm glad I tested... otherise, it works!!
I'm at 12 starts on my repo too! It makes me happy to see folks taking interst in Helix
Had a few weird deadlock bugs to fix, and had to also make sure I was using spinlocks for reads, etc. Otherwise I'd run into race conditions it looks like.
Still have bug in the vfs_lookup() though. Appears to be undefined behavior, so I need to spend some time looking into that in the morning.
full of memory leak
also vfs_mount should have three argument
the type
the path to mount to
and the thing to mount
so you can so
vfs_mount("/mnt/t","ext2","/dev/vda")
Slowly working on that... that's why I was saying it's a work-in-progress...
That's planned as well, but not right at this moment...
I realize I wasn't freeing buffers that I allocated too, and that's not great
This is what happens when you code at 3am lol
Well, and incorrect lengths when trying to free because I overlook things...
Since my kfree() takes a length parameter
that a rule of osdev
don't use code you writing while falling asleep
I see I have a bug in my lookup with linking nodes. I can only open a single file at a time, etc, otherwise it shits itself.
I'm going to redo the lookup logic. I can't seem to track down the source of the bug because during debugging everything works, but when I run it without the debugger it crashed, so I think a redo is in order.
To be fair I did write that code at like 3am...
wait have i forgotten to helix
ok no good I've starred it already
I can't seem to get path lookups working correctly. I trying to debug it everything suddenly works fine, but when I don't debug it asserts. Irritating.
Rewrote this function three times.
I'm getting to the point now I just want to say fucking caching nodes for a while.
Had to adjust how I was setting up my stack apparently. It was getting corrupt
@unreal creek I see more of the memory leaks you were talking about. I had some in my ext2_lookup() as well..
Getting path lookups to work has been a pain in my ass
I'm going to attempt to re-think my node linking logic. My previous logic was leading to some undefined behavior, and was really difficult to debug.
Going to implement helpers to handle this task. Right now, I'll test my vfs_split_path() logic in userspace that bascially provides me an array of tokens of the split path, and can go from there.
I also need helpers to assist with linking/finding nodes, and then requesting from the EXT2 filesysstem for the uncached node.
No new progress updates for the VFS.
Had about enough of trying to design the shit because of undefined behavior, and when I go to debug shit just magically works.
Things working instruction by instru tion might mean you're lacking memory barriers somewhere (in 1 core env anyways)
I currently only use a single core, but I suspect I'm also most likely corrupting memory somewhere too. It's been a bit difficult to debug because things will end up working as expected, but when not debugging things break. It's weird sometimes it does, and sometimes it don't, so I feel like this counts as undefined behavior
Not necessarily, it might mean you don't put barriers when needed
If I were you Id look into your disassembly and see which instruction "breaks" and go from there
That's actually a really good idea. I'll spend some time to see what's up
I wasn't aware I needed memory barriers in my VFS at all...
Well, now I know...
Yeah, you're absolutely right. I do need to make sure I'm using memory barriers. I realize GCC might be reordering things during compilation. My spinlock also wasn't doing any kind of data memory syninc after taking/releasing the lock as well, so I imagine this can also end up being an issue.
Adding memory barriers around calls to filesystem operations from the VFS does in fact seem to fix a lot of the issues I ran into before. I may need to adjust my lookup logic though.
Well, at least for trying to read out more than a single file.
.....I realize I might have some issues in my EXT2 driver I need to look at.
I just feel relieved to finally get some of these issues fixed.
this shit give me a headache....
Going to make a small list node lib for my kernel to better manage this VFS stuff.
I think it'll make things a hell of a lot more clean with storing nodes, etc, than what I'm currently trying to do.
Probably going to say fuck the VFS for a while and go work on exception handling. I'm about sick of this bullshit. Apparently memory barriers did jack shit.
I'm just irrirated as fuck after spending multiple days debugging, and re-writing shit, and getting the exact same result. Debugging ends up somehow making it work, and I don't really get it,.
Have you pushed your code somewhere?
ub somewhere?
Just which ever code is currently pushed into my repo under the ext2-dev branch...
That's what I had came to the realization of is that I've got some undefined behavior somewhere. The issue is though is that it was really ridiculous to debug because as soon as I start stepping through code, things would appear to work fine, and on the next run I'd run into the same issue
have you implemented ubsan
I did find a tiny ubsan libaray I can maybe setup in the kernel...
Adding it into my kernel now. I have a feeling my kernel might shit itself after enabling it...
Suprisingly no
Well, time to add the old VFS code, etc, and see what happens.
https://github.com/rdmsr/tinyubsan/tree/master
This is what I had saved for use for now. It's made to work with hobby kernels, so I felt this would be a good starter for now
rdmsr is on this server, though I dont remember who it was lol
did you add -fsanitize=undefined too?
Yes
Well, so far I can see ubsan is working correctly
My current registers being printed though looks a bit uhh.... not right, but that's not a high priority at the moment
Need to now rework some things since I rolled back to an older commit where things worked. I have yet to figure out why I can't open and read more than a single file yet, but I'm sure it's just an issue with my logic or something...
@lost stream I'm glad you had be setup ubsan in my kernel. This is actually helping me catch some NULL pointer dereferences that I otherwise would have missed, and completely overlooked
I should add this to my kernel lol
It's caught a few NULL pointer dereferences so far that I ended up overlooking, so it does truly help. Only thing you need to implement is print(), but this can very well just be changed to use printf, etc, since it's a single file project
yup, its very nice to have
Decided to redo the VFS entirely.
I've been going back to reading the vnode paper once again, but also reading about the linux-style approach, and see about maybe combining the two like I wanted to do before. I never did do so like I wanted, and ended up with a pretty bad design, but would say it was a good first start.
again ...
Yeah, old design I kept running into undefined behavior. I couldn't track the bug easily, because in the debugger everything seems to work fine.
A rewrite was in order after spending a week and a half at it.
I'd rather just redesign it than spend another week on debugging, and pissing myself off.
UB are so annoying
This on in the VFS was an absolute pita, so I knew it was time to redesign the whole thing once again...
I've got tinyubsan in the project now as well, and that's actually helped a bit
Still working on it of course, but so far it seems like it's a good start.
Been finding some good documentations around that will assist with the design ideas.
I'm working on filesystem registering, ete, right now though...
I have to also modify my EXT2 driver as well. I ended up moving some things I was using to get stuff like the block address as a macro instead.
you need a bit more operation
btw you can now take a look at my vfs since it's now unix like : https://github.com/tayoky/stanix/blob/main/kernel/fs/vfs.c
other than that i don"/ think the vfs has to care about superblock
I was actually gonna ask about that, a VFS is just an abstraction for an actual FS no? So why would it need a superblock? (I could be completely wrong, I don't know much about VFS design, I was just curious)
exacly that !
a vfs don't need to care about superblock
in fact the vfs don't even have to know about any fs stuff like hardlink
It's a Linux-style approach.... Linux also does something similar to this, and has supereblock related opereations, etc. Yes, you're correct though that the VFS is just an abstraction layer
My previous approach was a bit bad, and I was struggling to fix some nasty bugs, so a redesign was in order. So far, I'm quite happy with how everything is turning out, but will take some time to get the bare-minimum in place...
Ahh, okay, that makes more sense
After studying the Linux VFS docs, etc, I became really interested in the design, so I wanted to implement my own version haha
So, I did a thing, and ended up making some of my EXT2 helpers as macros...
#define EXT2_GET_INODE_DATA_SIZE(inode) (((size_t)(inode)->hi_size_bytes << 32) | (inode)->lo_size_bytes)
#define EXT2_LBA_TO_OFFSET(lba, block_size) ((lba) * (block_size))
#define EXT2_OFFSET_TO_LBA(lba, block_size) ((lba) / (block_size))
#define EXT2_GET_INODE_SIZE(sb) ((sb)->inode_structure_size)
#define EXT2_GET_BLOCK_SIZE(sb) (1024 << (sb)->log2_block_size)
#define EXT2_GET_FRAG_SIZE(sb) (1024 << (sb)->log2_fragment_size)
#define EXT2_GET_BLOCK_GROUP_COUNT(sb) (((sb)->total_blocks + (sb)->blocks_per_group - 1) / (sb)->blocks_per_group)
#define EXT2_GET_INODE_BLOCK_GROUP(sb, ino) ((ino - 1) / (sb)->inodes_per_group)
#define EXT2_GET_INODE_INDEX(sb, ino) ((ino - 1) % (sb)->inodes_per_group)
#define EXT2_SECTORS_PER_BLOCK(block_size, sector_size) ((block_size) / (sector_size))
#define EXT2_INODES_PER_BLOCK(block_size, inode_size) ((block_size) / (inode_size))
I felt this would be a bit more clean instead of calling to different functions to perform such simple tasks
Ooh, I should do that too whenever I get to making my Ext2 server
It's all up to you honestly; However you design it is up to you ๐
VFS work is coming along well...
@hallow comet Well, a bug in my heap allocator finally manifested itself ๐
ono
while(slab != NULL) {
uintptr_t slab_start = (uintptr_t)slab + sizeof(*slab);
uintptr_t slab_end = slab_start + (slab->total_objects * slab->object_size);
if ((uintptr_t)ptr >= slab_start && (uintptr_t)ptr < slab_end) {
// Place freed object back into freelist
*(void **)ptr = slab->free_list;
slab->free_list = (void *)ptr;
slab->free_objects++;
}
// Move to next slab in list
slab = slab->next;
}
So, it looks like it's here... I think it also has to do with how I'm trying to allocate, then free the directory entry name....
Actually, I might need to check that...
๐
AY!!! ๐
Well, now to figure out why changing my shit to use vfs_inode_alloc() and vfs_dirent_alloc() now doesn't read the next three directories...
Lol
oh lord... XD
I swear it's always something ๐
Well, I done fucked up ๐
XD
Found it.....
vfs_dir->d_node->fs_private = (ext2_inode_t *)ext2_inode;
๐
Fok
Ohhh I see what I did there... whoopsie
I set it from within the directory entry, and not the inode itself
vfs_dir->d_node->fs_private = (ext2_inode_t *)ext2_inode;
vfs_dir->d_node = vfs_inode;
Welp..........
I overlooked the hell out of this...
vfs_inode->fs_private = (ext2_inode_t *)ext2_inode;
vfs_dir->d_node = vfs_inode;
This is how it should have been initialized....
Ahh, yeah that would be an issue lol
I'm trying to figure out why now I can't read the last two directories... I had reverted the change, but now apparently it still does the same thing.. figures ๐
VFS design seems like a pain XD
Oh God it's been painful ๐
Well, turns out I'm a little forgetful:
if (strcmp(tmp_name, path) == 0) {
ext2_dir = kmalloc(d->entry_size);
assert(ext2_dir != NULL);
memcpy(ext2_dir, d, d->entry_size);
// kfree(tmp_name, d->name_length + 1);
// break;
}
My code was doing what it was supposed to do, and break when the target directory gets found... all I had to do was comment out those two lines, and now it prints everything ๐
Figures ๐คฃ
I feel like an idiot for that ๐
Going to get the start of a tmpfs filesystem in place for /dev for now. At least now that my ext2 filesystem is mounted as "/" like I wanted
Now, I can begin the real fun stuff lol
Well.... my lookup semi-works, but I need to figure out what I'm doing incorrectly for setting up the /dev node information...
I also need to fix my shit, so that it properly used the tmpfs overlay
Well... that's better than it completely not working
The pain in the ass part is trying to implement a way to replace the original inode information for /dev with the tmpfs inode (to act as the new "root mount" on that directory....
I also have to make sure to preserve the original /dev inode data, so this ends up causing me to write some jank code to make it work lmao
That does sound annoying lol
It's a pain in my ass....
I have to take a lot of time to re-review my code, and logic to make sure what I want to do is correct...
Well, in my design too, I have a special entry in my directory entry struct that holds the "overlay" mount. So, when I go to do a lookup, if that exists, I just replace the current "root" with that overlay, and operate on that.... will this work as I expect it to? Probably not
It's worth a shot
The issue I'm having now, is when I call the new filesystem mount, and set it's overlay, I don't exactly store the requested entry from the original filesystems lookup. So, when I leave the context of my vfs_lookup I don't have any way of accessing that directory entry...
Apparently I have this really bad habbit of writing crippling code without realizing until shit breaks.
That would be an issue ๐คฃ
I'm pretty sure 50% of the code I write for my OS is poorly written, so don't feel too bad
I feel better knowing it's not just me...
It really sucks because now I've got a sligntly crippling design flaw in my VFS logic, and it's going to suuuuck trying to fix this ๐
void vfs_mount(const char *path, const char *fs_type, uint32_t flags) {
vfs_dirent_t *root = NULL;
vfs_filesystem_type_t *fs = NULL;
vfs_dirent_t *dir = NULL;
if (!path || !fs_type) {
return;
}
list_foreach(item, fs_types) {
vfs_filesystem_type_t *f = (vfs_filesystem_type_t *)item->data;
assert(f != NULL);
if (strcmp(f->fs_type, fs_type) == 0) {
fs = f;
break;
}
}
if (!fs) {
printf("VFS: Filesystem %s unregistered!\n", fs_type);
return;
}
if (!fs->fs_mount) {
printf("VFS: Filesystem %s has no fs_mount()!\n", fs_type);
return;
}
if (path[0] == '/' && path[1] == '\0') {
// Mount filesystem on rootfs
root = fs->fs_mount(fs, path, flags);
if (!root) {
printf("VFS: Filesystem fs_mount() failed!\n");
return;
}
if (!vfs_root) {
vfs_root = root;
printf("VFS: Mounted %s on %s\n", fs_type, path);
return;
} else {
// Don't overwrite current rootfs
printf("VFS: rootfs already mounted on %s\n", path);
return;
}
} else {
// Get original directory entry
dir = vfs_lookup(vfs_root, path);
assert(dir != NULL);
// Mount new filesystem
root = fs->fs_mount(fs, dir->d_name, flags);
assert(root != NULL);
// Setup new mount for overlay
dir->d_mnt_overlay = root;
printf("VFS: Mounted %s on %s\n", fs_type, path);
}
}
You'll see what I mean for the part that handles sub-directory mounts
list_insert(vfs_root->d_children, (vfs_dirent_t *)dir);
This should hopefully fix things.
My lookup is implemented to handle this, but this clearly isn't the corrent way of doing this.... oh well I just want my shit to work for now...
oof
I'll get it figured out...
Since right now I don't currently support unmounting of filesystems, I can can just directly overwrite the original inode with the new filesystem (tmpfs), and deal with the rest later
Not the most ideal solution, but it gets some progress made
Hey, that's something!
Decided to change gears for now.
Going to just focus on my EXT2 driver instead...
bit buggy, but so far it works. i can start working on openeing, closing, and reading files. need to eventually figure out the whole devices thing, but will be later down the road.
great after fixing an overflow bug now it only prints one directroy.
im so over this shit.
lord, you just can't catch a break
I swear it's so bad...
To make matters worse, I was stepping through my lookup code in the debugger, and of course it just works, so something's getting messed up or I'm running into undefined behavior once again...
I'm only irritated because I've already ran into this with the original VFS design
And now my EXT2 code is now doing the same ๐ญ
I mean unless you find the source of the bugs re-writing wouldn't be too effective. Perhaps you're not waiting on devices, if I were to debug it I'd look into qemu and put prints on start and completion. Perhaps there's a trace event that might be helpful but idk because I rarely use those
Yeah, I do agree that would just result in me facing the same issue again down the road most likely. I do believe QEMU has some virtio traces I can take a look at. I'll take a look and see what all traces it supports, and just enable everything that's relevant...
The problem with trace events I find is that you don't know what to expect unless you know qemu code, so idk how helpful it would be
Welp.... I guess for now it couldn't hurt just to see if I get any clues. Otherwise I'll have to try and see what else I can do
It seems I've fixed the issue. At least after ensuring I was using memory barriers where needed
Let's see if this still breaks trying to read other directories...
Yeah apparently trying to list more than two directories just breaks things.
I need to see why this is happening.....
Yeah I don't get what's happening or why it sometimes works, and then doesn't. I'll try and see if there's anything I can do to maybe track down if something is getting messed up. Apparently trying to use memory barriers still doesn't resolve anything.
ugh.
k... time to spend today reasearching and looking at event traces. Or anything else I can find.
Looking at the qemu code for virtio-blk there's a trace for virtio_blk_req_complete and inside virtio_blk_handle_request.
Idk enough about virtio blk to say if its enough, but you can put a semihosting call in your code where you expect the transaction to complete and check if the order is blk start => blk end => your semihosting or is it blk start => your semihosting => blk end
Looks like I'll need to build QEMU from source for some of this, since Debian/Ubuntu doesn't enable full debug tracing by default
Its quite easy, run configure --prefix=<your install dir> --target-list=aarch64-softmmu and then build
Sounds good to me!
Personally I add a couple of flags to add assertions but its irrelevant for you
I'll enable all debug features that I can as well
austin@ryzen-beast:/media/austin/Scratch/HelixOS/build$ ./sysroot/usr/bin/qemu-system-aarch64 --version
QEMU emulator version 10.0.0 (v10.0.0)
Copyright (c) 2003-2025 Fabrice Bellard and the QEMU Project developers
austin@ryzen-beast:/media/austin/Scratch/HelixOS/build$
Host version is like 8.x.x, so this should be a bit better. Time to check some of the debug stuff
virtio_blk_req_complete
virtio_blk_rw_complete
virtio_blk_zone_report_complete
virtio_blk_zone_mgmt_complete
virtio_blk_zone_append_complete
virtio_blk_handle_write
virtio_blk_handle_read
virtio_blk_submit_multireq
virtio_blk_handle_zone_report
virtio_blk_handle_zone_mgmt
virtio_blk_handle_zone_reset_all
virtio_blk_handle_zone_append
BEAUTIFUL
Now, I can start debugging...
virtio_blk_req_complete 3899.175 ... req=0x646cbf5d7fb0
virtio_blk_req_complete 2958.317 ... req=0x646cbf5d7fb0
virtio_blk_req_complete 663.827 ... req=0x646cbf5d7fb0
virtio_blk_req_complete 859.735 ... req=0x646cbf5d7fb0
virtio_blk_req_complete 1717.276 ... req=0x646cbf5d7fb0
Noticed a series of completions on the same address, but otherwise no duplictes.
I think my virtio driver might actually have a bug that I don't know about yet...
I would've put a semihosting call after you assume the transaction completed, and see which one happens first
I'm still trying to figure out how I setup the semi-hosting stuff
Its quite simple, you put the semihosting function (for example print string =4) into x0 and the arg in x1, then do instruction for semihosting. In aarch64 its hlt #0xf000
Ohhh now I understand.
Wait, beacuse my host OS is x86_64, doesn't that mean semi-hosting isn't going to work in this case?
It just now dawned on me.....
ffs..........
No, why?
Qemu emulate the semihosting
I thought that I had to have an ARM64 host for the semihosting to work properly
Well, in that case, I'll get started
No not at all, it depends on the arm machine and weather it supports semihosting, and qemu does
Thank God... I was worried I had to have an ARM64 host, but since that's not the case, I can get started...
Yeah, my virtio driver is a bit buggy it looks like, so it's time to fix things up.
I did need to fix it, but "forgot"....
I had to move some things out of the request submission into the init stage of the device setup.
Some of what of this also suffered from potential race conditions....
And now apparently headers are "missing" trying to fix these issues... Lovely.
fixed.
managed to fix those issues. had to fix an issue were I was incorrectly setting the status for writes, needed to move queue selection and queue size selection in init, needed to make sure that when waiting for a status change, I relax the CPU, now the driver works.
knew my driver was buggy, but kind of "forgot" about it.
@hallow comet Well, now I'm able to read more than two directories with these fixes, so this is nice...
Nice!
Turns out, one that bug I was especailly facing was a nasty race condition bug...
Glad you figured it out!
I do need to fix my lookup function in my EXT2 code though... I can list directories under "/", but listing things under "/home/helix" ends up not working ๐
yeah, that would be helpful lol
So far, it's working quite well!
@hallow comet My literal expression when I seen it working lmao
I would probably have that face too lol
Lol it's always nice when things finally start working after spanding weeks trying to debug...
niceee
Thank you good sir ๐
https://github.com/ProfessorLongBeard/HelixOS/tree/v0.0.1-dev
Also, repo was never deleted. Just made it private, and then made it public again.
Lol I was being complicated...
HelixOS (ARM64 OS, not dead project!)
Trying to fix the nested paths not being read/working has been completely irritating.
At least I fixed my virtio-blk bugs.
you losed all your star
It's alright
I don't think anyone really had interest in my project anyways
i restart anyway
honsetly well actually i was quite bored of seeing only vfs stuff here
I've gotten some bugs fixed that turned out to be my virtio driver, but I've also been trying to move forward from it.
I'm just trying to fix my path lookups, so that things work properly, and can start moving forward
okay
not really sure what else anyone would really be excited about here in that case. i was working on threading, was having major issues, then decided to shift my focus back to this, since i already had it semi-working.
Once I get this stuff finished, I can then work on file reads, etc...
I do need to work on exception handling
I understand if folks get sick of the vfs stuff here. It's something I really want to work on, so I'm going to till I'm satisfied with everything...
So, my virtio driver needed some locking around the submission of queues. I also had to ensure a memory barrier got used here to ensure changes were seen. So far, this seems to fix a few more issues I had, and should also hopefully fix another race condition I was facing as well.
I believe my code that waits for a device status to be reported after the request should also have some locking added to it. This should prevent any race conditions or concurrent access, etc.
Its always nice to keep an eye out, just most people aren't writing when looking
And + for doing whatever you feel necessary in your kernel until its at a point you want it to be
I do sometimes wonder who pops in to see status updates ever now and again. Right now, I've been working on fixing my virtio driver. Well, at least improving it... I've also been working on the VFS + EXT2 as it's what I've really enjoyed working on (minus the stressful debugging attempts)
I agree... Once I'm ready to try again with threads, and scheduling, it'll definitely be great to work on
apparently you can't store/load stomic variables with stdatomic because of some type of "trivially-copyable type". I don't understand how this even gets used at all.
never mind. after some rtfm-ing on: https://en.cppreference.com/w/c/atomic/atomic_load I managed to figure it out...
I mean when you think of the disassembly, there's a limit about how much you can atomically move in a single instruction (different per arch)
Nice, I recall saying you've had problems there a while back
Yes, I had figured out I had some nasty race condition bugs in my code, so this was causing some of my issues with reading from disk, and not getting correct results. Right now, I'm further improving this driver, by ensuring safe atomic operations during th status check after submitting a read/write request. I'm happy with how well it's performing now
This is very true...
Im not sure you'll need atmoic operation for status, considering you're not racing with other threads
With smp you'd probably just hold a lock that signify you're using the device at the moment or something similar
(Which you shouldn't bother until implementing smp, any smp implementation without actually testing it wouldn't work)
This is true... I thought it would be good to have a more safe status check operation either way to ensure I don't run into issues on concurrent accesses, etc...
You're going to have trouble with concurrent accesses if two threads would use the same status concurrently either way, you wouldn't be able to tell what status is for which core
Fixing smp problems is difficult when you're able to run smp, its impossible otherwise
I thought it might be a good idea anyways, because I seem to end up with duplicate completetions
I'm looking into alternate ways of handling this via ring descriptor index, etc...
Well, I did notice a few bugs in my EXT2 code. In the block group descriptor code I was allocating the temp descriptor the length of the block size (this is fine), but then later was trying to free it with the size of the struct... I"m really surprised that didn't manifest itself a lot sooner....
I'm also learning now, that I guess directories can span multiple blocks, so that might also be an issue of mine that I'm not currently handling.
At least I'm getting some bugs fixed in my virtio driver, and fixing my EXT2 code...
Reworking my EXT2 code a bit....
I realize some of what I was trying to do might not of been worked as I was expecting. Especially trying to break in a nested loop....
Well, I managed to fix my issue of reading nested paths. Now I can read out /home/helix in my EXT2 filesystem. I still need to figure out though why trying to print more than three directories causes things to break though...
solution : never have more than three directory inside a directory
this is not a bug
this is a feature
to prevent malicious program to make a bunch of dir
I just realized why my vfs_listdir() might be broken for nested path lookups.... I'm not splitting the path, and handling the lookup like I do in vfs_lookup(). Or even making the call for vfs_lookup()..... it took me this long to realize this.... wow
So, I decided to change pace, and go back to implementing kernel threads for a while.
Ayyy, threading ๐
I think this will be a nice change of pace honestly... I've been wanting to get kernel threads going for a while now...
I can always come back to the filesystem stuff later
runtime stuff is more important imo
filesystems can wait
or you can make the user implement them with a bad disk interface
That's what I had come to realize that I should probably get runtime stuff in place before I continue...
That way, when I go to start loading userspace programs, I'll have a proper way to run them
my eyes is burn when i see arm64 instead of aarch64
