#shkwve: an incomplete kernel/maybe bootloader/UEFI thing/maybe something else if i get bored again
1 messages · Page 7 of 1
My port is a queue
Idk, it's funny to me now that we've basically came up with the similar design independently
yeah
I see
but you have distinct send rights
Yes
so its the exact same thing
But they go to the same queue
I guess that can work but you probably need to be able to revoke ports if you design it like that
but with different names
why?
I do have that ability
i mean first of all you only need to be told when the last user of a port exits
You can either delete the queue all together, or rewoke rights (from both sides) individually
Then also both sides get notified of that
but yeah also making ports revokable isnt like crazy hard
well yeah but you probably want the server to be able to force termination of a connection if a client misbehaves
how would it misbehave
And all rights get deleted when the port gets deleted

Try to spam garbage, for example
Yeah
im planning to have limits on size anyway
I haven't though of that
(so that you can presize buffers properly)
I mean there is an easy solution to that
so like the worst you can do by spamming garbage is dos
which you can do in so many other ways
its a fifo queue too
The issue with my design is that I just clone the rights, so if one client misbehaves, I'd revoke the right for everyone
yeah i have that kind of thing too
E.g. in pmbus
the solution is to just not have to ban individual clients
But it can be easily solved by explicitly asking the clients to create new rights for each client
Then it can be more granular
just dont ban 
But this is also useful for when either the client or the server crashes
if the client crashes no
So you get notified in both of the cases, or like don't have to wait
if the server crashes that just means the server part got abandoned which is different
As a server, you have to know you have no clients
So that you can free the internal state and stuff
i basically do this: ```rs
let mut ifc = ql.inflight_calls.cursor_mut();
ifc.move_next();
while let Some(node) = ifc.remove() {
let op = unsafe { OpRef::from_raw(node) };
let op = unsafe { op.get_unchecked::<CallTreeq>() };
op.complete_err(uapi::Status::Abandoned);
}
you statically patch a nop into a branch if you decide at runtime whatever condition is met
if the condition changes very rarely this makes the common case faster
Why does that need asm goto tho
how would you do it otherwise
No idea I just learned about its existence
yeah makes sense
this is the other side tho
Alternatives work by patching raw assembly
well yes thats the same idea
this is the receive side (queue) dying
i dont notify on a port dying but i can fix that probably
asm goto just means this assembly can jump somewhere else?
yes
Ah
in a static branch case you do something like ```rs
fn static_bool() -> bool {
asm!("/* patch it into a branch to {a} if it should be taken */ nop", a = label { return true });
return false;
}
The sender side is line 672
Is this better in if statements because the CPU can see that its a literal and so it doesn't need to predict stuff?
its better in if statements because compilers arent dumb
they can see that if you have code like if if something { true } else { false } { a } else { b } thats the same as if something { a } else { b }
and the return thing is enough for gcc/clang to turn it into a direct branch
and yes its better because it avoids a conditional branch
and its only a nop if the branch is not taken which is almost always way cheaper
e.g. on m1, a nop is 1 uop and you can retire 8 every cycle
And what if the arch jump helpers are out of line
a cond branch is also one uop, but its between 0.5 and 1 cycle
in reality its a macro
its not actually a function
and yes making it an out of line function would make it awful and dumb
and its on units 1 or 2 and a nop is on any i think (or none)
On linux it is a function, although they always_inline it
oh wtf
but close enough
anyway yeah
Anyway... microkernel right 
btw are you going to use the new acpi crate
yeah
im either gonna use uacpi or make my own
own would be interesting to see
guess
generic spinlocks are uhhh certainly a design choice
well you have no idea what the OS is doing
anyway they dont handle allocation failures so its as good as useless for me
they dont??
uh is uacpi robust to malloc saying "no u"
ofc
every single allocation failure is handled gracefully in uacpi
damn
will have to talk to the author about it
they are fine if you dont care about alloc failures
and there isnt like a great alternative other than "lol write your own collections"
...which u kinda do for a kernel crate lol
which understandably people dont want to do
no its not that bad
what about linked list or intrusive containers
ah
because their linked lists arent fine 
well the linked list iirc is fine but the singly linked list can only push at the front not at the back
even tho its trivial to make tailqueues work properly
huh lol
they just dont have one so like
but anyway usually linked lists arent great
so if u wanna push something u have to walk the entire list?
for the singly linked list yeah
because they only have a stack
and not a tailqueue
broadly speaking, what is the architecture of shkwve going to look like?
idk it keeps changing every 5 seconds lol
yeah its a microkernel thing
unless i get bored of that in which case it wont be a microkernel
and yeah i have some ipc rn and thats it
the userspace is extremely barebones
is it going to be more advanced than managarm
advanced in what way
microkernel wise
like the primitives etc
although maybe
async model
its a completely different design so its not really comparable
can u explain the difference in designs?
i mean not rly? its just all different i think
the only similarity is that we both my kernel and managarm has refcounted handles
lol
i think
no
lol
well first of all lets start with the fact that getting the reply could be an intrinsic part of doing ipc or not
if i did a microkernel i would certainly use these for ipc
they're fast
sync ipc in general isnt that fast afaik
or well it has good latency but not throughput
doors is
its sync right?
fully
yeah
it's thread-migrating IPC
in place of the thread you have a stack of activations in different contexts
the principal problem, i spoke of it before, is how it scales to complex, mutually-reentrant activation stacks
like with my design its not conceptually very hard to allow you to queue up a bunch of operations in one syscall
stack management becomes difficult
and you can receive a bunch per syscall trivially
and with a doors-style system that cant really work
you have at least one syscall per ipc
and also your throughput is exactly the reciprocal of the latency (i.e. you couldnt possibly make it any worse except with a worse constant overhead
)
anyway back to lock ordering issues
i cant copy while holding the queue lock
but i also need to hold the queue lock to prevent cancellation
Do you have your own UEFI boot proto?
no
well you can cancel async operations at any(ish) time
just copy, then check if it's canceled..?
you cant copy after its cancelled
(except the smp problems)
because then its a uaf
just IPI the other CPU and disable syscalls while copying so it can't be canceled
well no you cant do that
the copying on its own requires taking locks and possibly blocking
because you can copy from/to a pager
weren't you copying to kernel's stack first?
i mean i copy through kernel memory sure
i have a percpu for it but yeah
but the problem is still there
i dont want to reacquire the lock every time i do a little bit of a copy lol
I mean I don't have this problem because my messages hold the buffers
while they are in the queue
yeah but i dont want to do that because thats cancer
lol
anyway the solution is to put a second lock inside of the queue
which is what allows you to do cancellation on it
yeah
don't you have a lock for rights already?
like this is the same issue as if the other side also cancels
uh that doesnt help
the queue has a lock yes
but you cant take that lock while doing a usercopy
How do you become "more advanced" than Managarm regarding its microkernel design? 
^^^^
how would you make a more advanced monolithic kernel design anyway
by i getting more performance in every possible scenario due to my incredible design which is totally possible and not at all infeasible
well i could add an event that you have to wait on before you cancel
i think ill do that
hmm except that makes cancelling way more complicated lmao
actually better idea: i'll make an event you have to wait on before its actually complete
but what if you rugpull and unmap memory, etc.?
wdym unmap memory
then you get a fault
either an error code or just a full exception
wait, do you get async pagefaults in the receiver?
wdym
I'm confused now to how your ipc works
Do both sides have to be calling the kernel at the same time for it to go though?
so your recieve is async
yes
my operations are all fully async except for reply-send which is sync
but reply-send is guaranteed to complete without waiting on anything except pagers
but then, who gets the pagefault/error when the async recieve can't happen?
noone
or well probably the caller idk i havent decided yet
i could asynchronously deliver an exception
my receive is synchronous...
its not?
you can have many clients connected to a single recv
and you could have outgoing requests also use that afaiu
which is like the definition of async
Then I'm confused
The things get placed to my queue asynchronously
But I do pop front synchronously, and it blocks if there are no messages
Doesn't it make it synchronous?
I mean I've been thinking about it, and I can make it fully async as an optimization, or whatever
Without changing the send API
and then what would you do with it lol
(I mean I was also planning to make blocking send)
You can significally reduce the number of syscalls?
Yeah
so batching?
you can make a multi-recv op just fine
Or like while doing other work
thats what i have
I mean sure, but do I need it?
multirecv is most of the gain imo
Like with rust futures for example, I can just make it set a flag in memory on completion, then in busy servers you'll just check the memory first, before calling into the kernel
Like full ring buffer kinda thing
This would remove memory allocations
Since it would go into the buffer directly
Like what you're doing right now
Thinking of which, you can also make the cancelation async 
sure like completion buffers
not if you want to do rust you cant 
okay now my kernel is getting very confused
its saying 0 bytes received
and it received data properly
i think my syscall code is fucked lmao
yeah
much better
userspace works!
oh oop its printing a random debug print lol
and it works with kasan too albeit more slowly
im gonna try make gdb work better
who needs gdb
well i want to make gdb work at least kinda okay
instead of it working terribly
my plan is to write my own gdb client and server so that i can make a shim that sits between gdb and qemu and lies to gdb about my threads etc
Somehow, I think I never really had problems with gdb
gdb is the most robust program of all time
"progress"
hardcoded that one
hmm
pwndbg doesnt really know what is going on it gets the perms wrong too
i imagine if i updated to latest it would help
lol im making so many errors that are like Data format error: invalid hex string: &{%!m(string=encoding/hex: odd length hex string)}].
ok i wrote a hack that makes it work properly
time to make it work but using the llvm debugserver's documented api, so that i can actually upstream it lmao
because i have a feeling that if i write a PR which calls an api thats like "qsk.plzz_give_memory_map_kthx" they wont really accept that
pwndbg-lldb is even better
doesn't lldb support the gdb protocol as well?
thats the only one they support
except slightly different because of course it is
ah i thought you meant something else due to:
llvm debugserver's documented api
ah
also gdb is so absurdly picky when it comes to the xml you send ngl
if you have a union, then supply a size and also start/end values for the fields, you get an error that bitfields must have a specified size
which is lol
python
import pwndbg.gdblib.vmmap
def get_known_maps_v2():
if not pwndbg.aglib.proc.alive():
return ()
pages = list(pwndbg.gdblib.vmmap.info_proc_maps())
pages.sort(key=lambda page: page.start)
return tuple(pages)
pwndbg.gdblib.vmmap.get_known_maps = get_known_maps_v2
end
``` okay i have this awful hack
which makes pwndbg know how to do proc pid always
and now i even made it so that it can pass through singlestepping
gdb is so efficient
thats why every time you singlestep, it sends 120 packets
im not sure how much of it is pwndbg but like cmon
i might actually write a memory read cache atp
ig they never optimized single step perf
i imagine its pwndbg not being efficient
and gdb not doing caching at all
lldb has multimemread which makes it less bad at least
caching would be quite hard as you'd need to be able to understand all possible side effects of an instruction
and there could be insane cases
such as memory access trapping to a higher EL with visible side effects 
Instead of GDB, you could support XNU's kernel debugger protocol which lldb supports
idr the name
ah KDP
that means i can only use gdb
why? i dont mean caching between instructions btw
i mean caching within one instruction
it might be pwndbg not gdb idk, but gdb should have caching for this imo. they send many overlapping reads, then send more, and they are very short (even though latency is a bigger cost than throughput here most likely)
ah i see
im gonna try adding some caching to see how much can be gained but i suspect the answer is "a lot"
Why does it matter though
its noticably slow (idk if thats the cause but like)
because each request is an ipc
so if every step you send like 120 rpcs, your perf isnt going to be great
libstdc++'s unordered_map manages to actually lose against the extremely low effort frg::hash_map when testing random keys 
BM_HashMap_Find_frg/1024 6361 ns 6360 ns 110181 items_per_second=161.006M/s
BM_HashMap_Find_frg/16384 212194 ns 212178 ns 3303 items_per_second=77.2181M/s
BM_HashMap_Find_frg/262144 6429989 ns 6428793 ns 110 items_per_second=40.7766M/s
BM_HashMap_Find_std/1024 8952 ns 8950 ns 77770 items_per_second=114.41M/s
BM_HashMap_Find_std/16384 271224 ns 271203 ns 2585 items_per_second=60.4122M/s
BM_HashMap_Find_std/262144 9164705 ns 9162696 ns 75 items_per_second=28.6099M/s
Does it have the same iterator stability properties?
yes
but it doesn't have the bucket API
i didn't look into what it takes to impl that because it's stupid anyway
the open addressing with linear probing and overflow counters is substantially better in this benchmark (2-3x for small table sizes)
how can u ensure iterator stability with open adressing
BM_HashMap_Find_rcu/1024 2427 ns 2427 ns 284987 items_per_second=421.954M/s
BM_HashMap_Find_rcu/16384 129493 ns 129479 ns 5392 items_per_second=126.538M/s
BM_HashMap_Find_rcu/262144 4943796 ns 4943186 ns 138 items_per_second=53.0314M/s
you allocate once per element
this is for a new rcu enabled table that i'm adding which uses the overflow counters
inserts are slightly slower (~15%) though because they need to maintain the counters
no
ah wait yes
but it doesnt iterate in the correct order
libstdc++ is like awful though
are you doing vectorized scanning?
no
at least use the SWAR trick
15% is a lot more than slightly ngl
you can use an indirection vector as well
which isnt rcu compatible but is a lot faster
Then you couldn't use it in a kernel
Unless you put it behind openmp or something
theres a trick which lets you compare 8 7 bit values to a single 8 bit value in a few instructions without simd instructions
and get a bitmask
What is it
uhh idr youd have to check the code of any good hashtable
folly does this
i assume it rehashes when it's exceeded?
uh
and you just hope its rare

i said this before but i don't think you can assume that
or rather, i don't think a good hash function saves you
because no matter how good the hash function is, its range may be quite small
compared to the number of insertions/deletions
for tables that have small sizes but many updates
How do you do fine-grained locking in a hashmap?
Is it worth it to do something like sharding or should you do lockfree
wdym
like it only produces values in [0, 2^32) or something?
the definition of a good hash function is that it produces values which are uniformly distributed over the whole interval required 
no, it only produces values in [0, 512] because the table is <= 512 entries but receives 1M updates per sec
this works way better for folly i think because its effectively a 254*14 entry overflow requirement
i think max(true value for overflow refc) = max(probe length), right?
yeah
so youd need to have a probe length of 254
say you insert into slots 0, 1, 2, 3, 4 w/o collisions and then get a collision on insert into slot 0
oh right
overflow rc = 1 but proble length = 5
so its ≤ max probe length
since you have overflow if an entry before has a continuous probing run from there to here
and you need at least {overflow refc} entries to get it that high
so if you have a count of 254 you have an entry for which the probe length is ≥ 254
which is already a problem
and the solution is to rehash at a load factor that prevents this from happening
especially for folly when you have 14-way probing this would be an absurdly bad probe length
yeah true
but yeah maybe you should rehash once this happens too much
maybe rehash every max(4*capacity, 16) insertions or something
or every capacity^2 insertions or whatever
you want to set the rehash interval to be a multiple of the capacity so that it still runs in amortized linear time
@untold basin do you know how folly hashtables compare to abseil?
I think abseil has the swiss table design which might only be good with SIMD
A comparative, extendible benchmarking suite for C and C++ hash-table libraries.
this might be relevant but they don't benchmark folly
verstable seems to be quite good
uhhm using a max load factor of 0.875 with std::unordered_map is certainly one of the ideas of all time ngl
also those results uhhhh do not look great
like you shouldnt be getting that much spikyness
and the string keyed benchmarks are entirely useless because the hash function used is fnv (which is really not great; in particular, for short strings, you kill the performance of all of the open addressed simd tables in many cases)
0.875 is not that insane
That's the same that i used above for the frigg rcu table
(because 7/8 can be computed w/o division)
yeah thats not the issue
the issue is changing the load factor from the default for whatever table you are using is uhhh
especially for tables which are using chaining like std thats really bad afaik
well, at some point it probably becomes quite bad but at 0.875 the average chain length is probably still quite low
yeah i imagine its probably not like a huge diff
the bigger issue is the string hash is really bad + the results do not look good
like i know the stl is bad but its not quite that bad in terms of noise
unless you did something really dumb
e.g. if you only did one run per sample, or if you use the same input so every sample is the same
What do you think about this
it probably doesnt matter
unless you do something really dumb like one alloc per object
yeah that variance looks sus indeed
hmm i found out there are tools similar to gnatprove but for rust
and now i wonder if unis have a monopoly on formal verification tools
okay so i have an idea
what if i remove refcounting entirely
because that would kinda solve a lot of the reference cycle issues i think
also i could have userspace explicitly allocate objects if i was careful in the api design
you can use capability delegation trees as in seL4
which is nice for accounting
but the cost of that is extra book keeping
is that just a weak pointer or how would it work?
something like that yeah
except without preserving the presence of the alloc
so a handle is the 3tuple {object ptr, object serial, object rights} (in the kernel ofc)
you allocate objects in a virtually mapped region
and if the page is mapped there it always contains objects
or i might use a global handle indirection table idk yet
the handle table approach has some advantages in terms of simplicity