#Zinnia
1 messages · Page 12 of 1
I mean relocatable in the ELF sense, as in not linked yet.
and meson already gives me this integration
instead of having to deal with two build systems
not to mention, again, there's no real way to encode this info in cargo.toml
You mean you can either encode static or dynamic but not something that depends on config?
yes
Ohh yeah cargo basically explicitly said they don't want to replace make
i don't like cargo at all
maybe meson has better rustc invocations 
@vast lotus what does struct vmm stand for?
what's the reference to inode/dentry for?
That's the file that was exec'd to create the address space
If you pass HYDROGEN_INVALID_HANDLE to hydrogen_fs_fopen you get a file descriptor to it
It's intended to be used in the same situations as /proc/self/exe on linux
I could've put it in the process as well ig since a successful exec always makes the process single-threaded (kills other threads if current process, otherwise fails if process already has threads), that'd probably be a more logical place to put it but this works as well
so if i understand it correctly all this does is page table + list of mappings?
does it just keep track of every call to mmap or something?
It keeps track of allocated virtual memory regions
It merges them if possible but if not then it effectively tracks mmap calls yes
so also the mapped fds
The mapped memory objects, but that's effectively the same thing
in your kernel or generally?
Oh and it keeps the regions in an AVL tree as well
In my kernel because I don't have any memory objects that aren't associated with files yet
Aside from the vDSO I guess
okay I've got the bare bones vfs functions inplemented
i could technically already read executables now, but I'm going for a page cache now so i can mmap files
fr.mw laptop
ignore that it took 20 seconds to start aps for some reason
hm, very odd
the fuck?
Is the CPU freq low? Last time I tried an OS of mine on real HW, it was super slow because the CPU freq is relatively slow by default. At least on the platform I used.
based
bru
hm
anyone know what could be causing this
it's just refusing to start ap 4
after a reboot it does it but now it takes ages again
nice reflection
Hair reveal
hm maybe I am missing a memory barrier
okay maybe i'll have to reconsider using meson with rust
at least the initial setup is not as easy as i had assumed
yea
technically there's meson_cargo
to connect cargo to meson
but atp i can just use makefiles
rust with makefiles goes crazy
well what do you think i should do
just stick with cargo i think
it does what it's supposed to do well
(build the kernel)
it's also supposed to build drivers :(
doesn't it?
it does, but idk if i can do stuff like built-in drivers
or like, either modular or built-in drivers
ofc i'd like to stick to cargo but i haven't really found a way to do this
like an nvme-builtin feature
and make kernel depend on it
if that feature is enabled
hm
idk i dont think you can do that with cargo and nothing else
xmake does rust 
you might want to do something like
generate cargo lmao
just write your own little tool
to like generate Cargo.toml files
well yes
i didn't know
there's no build type
build type is determined by crate-type
you can have two kinds of library types
in cargo.toml
like
you don't want a dylib for built in modules
i know
you can do this
then the build.rs could determine the crate type to use based on the configuration
i didn't know you could do that
that's nice
but then still
there's the linking it to the kernel
that's easy-ish
like linux, you can have a drivers crate
and the build.rs generates the import for it
kernel->drivers->nvme->kernel
does that work?
i guess with extern it does
okay you go cook man
i'm not sure, but i think cargo can break cycles
i believe in you

i might have to do a page cache to do mmap on files
real
god why
idk how my shit is gonna work yet
zig?
oh
lmao
no not really
what do you do
i have a WaitObject which has a list of waiters, when you try to .lock a mutex which is already held it creates a WaitEntry which is (thread, blockToken) and adds it to the waiter list then yields
and mutex on release pops one entry from the list and wakes it up
nice
yeah it's pretty simple
and pretty buggy probably
won't find out until later i guess
i think my percpu code is completely fucked :(((
i thought it worked
yes but that's just an illusion
because
i realized i can't do per cpu vars like in c++
since in c++ iirc those are initialized via a constructor
i.e. you can run the same code x amount of times, right?
because if i memset the percpu block to 0, that means all the statics are broken now
i.e. static function pointers
looking at the hex of the percpu section shows that there are multiple non-zero bytes
then don't do that lol
you use whatever is in the section as a template
when initializing a new percpu you copy the template into the new percpu memory region
yea but i don't think i can use it as a template if the bsp writes to it 😭
maybe i'm overthinking
well the bsp shouldn't use the template as it's percpu region
hm
you can do smth like
you can provide a special bsp percpu with a linker script
just do what you already do for percpu but copy it into a percpu_bsp section or smth
and provide symbols for that
because if i can do it at link time i could write to per-cpu data before mman is even set up
i'd have like ".percpu.template" and ".percpu"
well my solution also does it at link time 
yes
exactly
/* Per-CPU data block. MUST always come last. */
.percpu ALIGN(CONSTANT(MAXPAGESIZE)) : {
LD_PERCPU_START = .;
*(.percpu.init)
*(.percpu)
. = ALIGN(CONSTANT(MAXPAGESIZE));
LD_PERCPU_END = .;
LD_DATA_END = .;
} :percpu```
you can just copy this before the bss section, make it part of data and get symbols to it
idk what .percpu.init is but yeah
maybe what im saying doesnt make much sense but you get the poiunt
interesting
idk, you cook
you are on the right path
does ld really allow input sections to occur multiple times?
what
that's mine
how is mine different
.
I see more lines 
in this
yes because if you use that storage for the BSP then you can't use it anymore
so that's what we were talking about
you allocate storage at init
what if i want to use it before allocations are available
why would you
threading
before allocation?
yeah
that doesn't make much sense but you can just have another section just for bsp
guess wtf we're doing rn
what I said

hm okay linker script isn't helping here
okay what if i just store the percpu data, and then reserve enough for the bsp
then copy it at runtime
yes
I don't have a .percpu section, I just dump a struct pointer in a CPU register 
in menix you can define per cpu variables at any point in the code
i dump it into the GSBASE msr
for example:
which register btw
how do you make it accessible then?
RISC-V -> sscratch, x86 -> GSBASE
by loading the start of the section into gs_base
or rather, start + (cpu_id * size)
thats cool but I feel like you can easily lose track of what's per-cpu and what's not
so what
i disagree
per cpu variables should only be used where they're defined most of the time
like, a lapic percpu variable only makes sense to use in the apic code
and the rest (like scheduling structures, cpu id, etc) are part of a globally accessible struct

one large struct is just messy
especially when you need to have arch specific members
Not realy IMO
I just name the arch-specific struct the same thing on all architectures.
arch_cpulocal_t in my case
but why
yeah same
I have struct arch_cpu_cb
arch specific fields shouldn't be visible outside of the arch code imo
to ensure it's portable???
wat
no like, why do that if you're only ever going to access its members in the arch impl
how else would you have architectural per-cpu fields in the struct
because whatever is in struct cpu_local is stored in GS
and struct arch_cpu_cb is in that struct
i get that
C has plenty of places where I'm forced to tell code that shouldn't touch variables, what those variables are.
my main point is that it's very easy to see which variables are per-cpu and which aren't
because they're essentially wrapped in a PerCpu block
... but not necessarily in the place you're using it.
Because global variables just have names.
but to use it you have to call a function on that type
Whereas, of course, in the cpulocal_t struct, you must always obtain a handle to it first.
if you try to access them directly you'll get compiler errors
why meson rust
but doesnt cargo do its job fine?
what else do u need it to do
im not that big into rust but i believe cargo should be okay?
As far as we know, Cargo doesn't support the way he wants to do kernel modules that are either statically or dynamically linked via a config option.
ohhhh
that makes sense
it's missing post build actions, advanced configuration (à la kconfig)
This is unironically why I use Makefiles in addition to CMake
cmake can do post build?
Yes but it can't do everything I need it to
Specifically the way I go about some things like launching QEMU are just way easier to do via Makefiles
i do it via terminal
Well yes but make qemu is a whole lot more convenient doing cmake -B build and whatever else to run QEMU, and is especially a lot easier than doing the whole thing by hand.
i prefer to do it by hand so i can stop and launch what i need in that moment
i do that already
but in my distro
not the kernel
I don't make such a distinction just yet
I have a severe case of skill no time issue
@near tartan do you use your arm64 thinkpad for testing? can you run one for me if so? It's the thing, I asked you before for orange pi, but this time, it moved further and now concern is to investigate on how RAM is placed in the system address space, is it fragmented etc. Building PFN from UEFI memory map turned to be quite a challenge. Knowing memory layout on real hw platforms would be insightful. If you can, then when you have time and will, here's the link to zip archive. Unzip it the way so that those 2 directories go into the root directory of a USB thumbdrive (or SD card if applicable) and from UEFI Boot Manager launch efi\Upptech\antload.efi via the "boot from file" option (or using UEFI shell if available) and show its output.
https://ant-upptech.sourceforge.io/uploads/lybid-arm64-3.zip
it's 100% fragmented
I see.
yeah would be interesting to know what the memory map looks like
/* We expect the bootloader to fill in the size */
looks like its exactly one region tho
@80000000
so /usr/dev work ...
Menix running Minecraft when
Here's how RAM is assigned for Pinetab 2, rk3566 SoC, uboot's UEFI. In the output, 1st number is system address, 2nd is number of pages. Gaps are present. Worse, both edk2 and uboot for some reason use EfiReservedMemoryType, that by the specification shouldn't have been used. And what to do with it, it's neither a real gap, nor should be preserved by the OS in working and sleep states, not bad memory nor even could be read/written. It just screws up the map. But well, it is as it is.
well firmware engineers wipe their ass with the specification
That’s minesweeper?
Link?
it works with *BSD
so hopes are high
it uses CMake to build
honestly id write better AML then a firmware engineer
does it make sense to abstract scheduling algorithms
aka
pub trait Alg {
fn next(&self) -> Task;
fn insert(&self, t: Arc<Task>);
}
sounds cool and I don't see any problem with making traits for these
that's good design, cyclic deps are broken anyway
probably
Rust already notorious for outrageously large compilation units, if crates could circular then would become one single big unit that has to compile at once
well my issue atm is that i want to have built-in drivers
if i split them into their own crate that will introduce a cyclic dependency
because the drivers need the kernel and vice versa
the kernel doesn't really depend on it, but i somehow need to link it into the kernel binary
or i just say fuck it and leave drivers to be modular
sometimes i really do miss the flexibility of c/c++ codebases
why?
i like the way linux separates drivers into their own dir but makes them non-optional
e.g. irqchip
tbh i hate everything about cargo
it's definitely one of the weaker parts of the rust ecosystem
ive said this a billion times but i'd like to use meson with rust, but it's really hard to setup with external crates and such
and i need to have a custom build of core and alloc
it does what they want you to do
there's no way to do anything advanced
even build scripts are pretty limited
Can't you just make a kernel-core crate with all the core infra, then driver crates that depend on that and finally a whole-kernel crate that includes everything?
i basically suggested that, i guess
at some point at least
the only problem is deciding which drivers are builtin and which are not
i don't think there's an easy way to do that with cargo
but i think doing something like a script which generates toml files with the correct values + a file that can be included that contains a list of builtin modules so kernel can initialize them
we'll see
i don't think there's a way with build scripts to specify the build type
in general cargo isn't that flexible and you'll need a custom build system for that
it's probably easiest if the build system just falls back to cargo in the end
for crate support mainly, i guess
one thing about them i like, you can really separate the interface and the implementation
oh yea
i "solved" that by having a generic wrapper function that just calls an implementation with a certain api
meson could do crates but you have to write a wrap file for each dep (not that bad i guess)
and you can have circular dependency in a sensible way in that A and B of course don't require to see each other's .c file, B is still effectively "depending" on A, i.e. B.h includes A.h, but both A.c and B.c include A.h and B.h
oh yeah
another nicety
modula has an interface/implementation distinction but the whole implementation must be one file
carbon 
not c++
c++ but red
with proper planning it's possible to write e.g. foo.h and then have foo_impl1.c, foo_impl2.c etc. and compile them conditionally, as in
obj-$(CONFIG_FOO_IMPL1) += foo_impl1.o
obj-$(CONFIG_FOO_IMPL2) += foo_impl2.o
# ...
isn't carbon just what google excreted after the C++ committee laughed off their attempt to carry out a hostile takeover?
Is Carbon even used for anything?
doubt it
it kinda looks like "We have rust at home"
well the current obstacle is a page cache
i looked at maestro but it's not really compatible with my design
but idk how to even design it properly
the curse of being self taught
huh? just read existing OSs implementations
like linux's, NT's, etc
idk what that has to do with self taught
every time i look at the code of another project like linux i feel overwhelmed because i don't even know what i'm looking for
i don't know any theory behind anything
i just go by what works
learn the theory then read the code then you'll understand much better
🤯
I personally dont read linux code because I feel like its super messy
duh
BSD code I understand much more
so yea just pickup a textbook on whatever you want to copy
id do linux if I were you, it seems like that's what you're aiming for
dont even need to look at the code if u know the theory well
i do have the tanenbaum book, but it's like 2k pages
why not a generic book on that topic?
read it again until u understand it, write a python script to simulate it in usermode or something then implement it
because you'll have much more details, you can read about how other OSes do it too and mix&match
and the generic stuff is good as well for a general overview
you'd start with a generic book to learn why we need a page cache and how they work generally then you read about how a bunch of OSes actually do it
and now you'll have a very good understanding of page caches, why we need them (what problem do they solve) and what typical implementations look like
and you do that for every component you work on 
💀 see you in 2035
if you already know why we need page caches id just skip the generic book and go straight to an implementation
ight
this
do this with what is closest to your kernel atm, I'd do linux if I were you since you seem to have a linux-style vfs iirc
yes that's very likely
there's also a linux memory management book I think
any linux book will likely go into details
oo
What's not compatible?
is the strictness of rust keeping you from doing something youd want
You can make the default implementation return EINVAL like I did
no no, i mean like
you cannot call the function on some inodes
because i have different ops per inode type
with an enum
https://blog.lenot.re/a/page-cache here, I found a good resource

🤯
Page cache my beloved
I think I fucked up the fileops and nodeops interface btw. There should be an implementation for each filesystem type and file type but I only have one per filesystem type. So I end up having to recheck the type of the file when doing an op on it
do explain#
you have one file ops per fs?
Actually I don't even remember
xd
Maybe I should have one fileops per file type, yeah. Not one per fs
well in my design, files are created/opened by the fs
Which is probably the good way to do it I think
hey mister maestro, I also have a block cache/page cache thing for my block devices (each block device has a cache of buffered blocks on itself), how do you decide when to evict things from the cache?
are these the same thing
I might be talking about something totally different
from what i can tell it's just a timeout
I evict stuff when the file is deleted or if the system runs out of memory
The time thing I have is just a task that writes dirty page to disk
But the page stays in cache after being written
my cache is write-through for now :p I will probably implement writeback functionality tho, seems cool
thanks mister maestro 👍👍😁
You're welcome, mister Gummi
I used to think the sentence "Unused RAM is wasted RAM" was just an excuse to write inefficient software but then I started to understand it when I wrote the page cache.
You want to use all the available memory on the system to avoid accessing the disk as much as possible
It also applies to other cases. If a value is expensive to compute, you may want to use memory to store it instead of recomputing it each time
I'll adopt the linux page cache strategy
at least a basic variation of it
looking at the docs it seems sane enough
man i miss headers
I don't even know if I did the Linux thing
mfw apps show up as having 7TB of virtual memory but almost none of it is used because they're only assigned to the process and the process hasn't accessed the addresses yet 💥
I once wrote a program for a competition that strictly allocated memory in a fifo way
So to get a few % in extra allocation performance i made it alloc a couple tib in virtual space
and used a bump allocator until it hit the end of the range
while madvise(free)ing the head of the range
alloca

yeah it was right after they tried to get the abi break paper through and everyone else said no
google is very unaware that like 99% of companies cannot just rebuild the entire universe whenever they feel like
imagine if microsoft had to explain to boomers that can hardly use a computer that they need mvcrt90-abi2.dll rather than mvcrt90-abi1.dll
nightmare territory
i mean
on windows some programs just literally ship their own vc++ runtime
those are at least backwards compatible
you can take msvcrt140.dll and run a program that expects 130
that's a... stretch
you really shouldn't be doing that
but you could say the same for msvcrt140-abi1.dll and msvcrt140-abi2.dll :^)
it works until it doesn't :^)
if it was a thing of course
idk this just sounds like making an already pretty painful situation 10x worse because google is difficult
tbh not being able to rebuild the world sounds like a huge mess
true
also, realistically the only thing that'd be impossible is linking c++ 23 code into pre c++ 23 code etc
why?
they could just keep the old code in the .so and add the new code into a new ns
like what happened in c++11 with std::string
don't think closed source stuff that maintains abi compat does it much different
well yeah but thats like 95% of corporate software
closed source dlls from a company you've never heard of
i think i'm cooking
i made a virtual object struct which keeps track of the mapped physical pages, the backing memory, etc
MenixExecutiveMenixObjectAllocator
MEMOA
yes
should a PFNDB have a global mutex or a per-page mutex? 🤔
you're going to need a global one anyway for the allocator
or ar least a regional one
true
okay this freebsd book has a bunch of nice implementation explanations
i should read more about os internals instead of going in head first
There are many types of locking and waiting that you might do, some of them global, some of them per-page. For example, waiting for IO, which might just be a global waitlist-backed condwait or similar.
so both?
neither
well
youll probably find situations where a per-page mutex is good
but a "global mutex" for the pfndb is a bad way to think of it
instead you should just add mutexes and change them around as your needs evolve
for example youll probably have a mutex around the page lists
no need to conflate that with some other mutex
page lists? like the freelist pointers?
that and whatever other page lists you may have
depending on your vmm design and page replacement strategy and so on
i believe i dont have another
hm
then i need to use atomic pointers
or wrap the next pointer in a mutex
because rust ™
what about what @modern hamlet said
I mentioned "a global one [mutex] for the allocator"
my message is kinda similar to this one
yes that's probably a good idea
unless you can make a lockless freelist allocator
well it's not really an idea, i'm kind of forced to do that :p
otherwise i can't modify the head
my kernel uses a global spinlock for PFNs but that's because I suck
either that or my MM depends on PFN allocation being fast and also not blocking because of the former reason
damn
well okay I was exaggerating a little bit, I don't suck, but I think it might be possible to use a blocking lock instead of a spinlock for my PFN allocation
I just don't feel like doing it
i should look into rcu
looks inside rcu
rcu
oh yeah a dumber preemption-masking rcu could be useful in implementing a smarter blocking rcu probably huh
eh
does linux do this
if i knew how to impl it properly it should not be hard
linux uses sleeping rcus or whatever i think(?)
yeah but do they use a dumber rcu to imlpement the slepeing rcu
not sure
thatd be funny
as far as i understand it you lock an rcu and write to another memory location, then swap pointers atomically to update
rcu is just, i defer deletion of something until im sure no one is using it anymore
thats too broad of a definition
on the read side u just copy the pointer after locking it
that could be refcounting too
i mean refcounting sort of solves this problem as well
it does im just saying that "i defer deletion until im sure no one is using it" is like
every garbage collection scheme ever
yeah
but you also went on to say this so i probably just typed too fast
isnt that just rc
yes but in rc u bump some refcount
in rcu u dont bump anything
so there's less overhead
hm
and the person that wants to delete the thing does the work
i thought it was a synchronization primitive
in rc you do the work if you're the last to reference the object
well sort of lol
it's a lifetime prolongation primitive

bruh
basic principle is that with rcu you dont delete the thing until youre sure that everybody who could possibly have touched the thing has passed through a "quiescent point"
which is some point where they definitely are not touching the thing anymore
so with preemption-blocking rcu for example you can wait for every unblocked thread that existed at the time you touched the thing to pass through the scheduler at least once and then you know nobody has a pointer to the thing
void rcu_read_lock(void) { }
void rcu_read_unlock(void) { }
void synchronize_rcu(void)
{
int cpu;
for_each_possible_cpu(cpu)
run_on(cpu);
}
and you know that by blocking entry to the scheduler while using the thing
well no this is wrong
if you block preemption you only need to make sure every CPU
has passed through the scheduler
and then you know nobody has a pointer to the thing
the stronger thing of "every thread" would only be needed if you didnt block preemption (its also broken because the scheduler could run while he still has a pointer to the thing)
I don't know exactly how valid this is but my approach is just going to be to disable interrupts entirely while reading what's behind an RCU pointer, and adding a grace period (currently 50us) before calling free.
there's an example with a mutex
static DEFINE_RWLOCK(rcu_gp_mutex);
void rcu_read_lock(void)
{
read_lock(&rcu_gp_mutex);
}
void rcu_read_unlock(void)
{
read_unlock(&rcu_gp_mutex);
}
void synchronize_rcu(void)
{
write_lock(&rcu_gp_mutex);
smp_mb__after_spinlock();
write_unlock(&rcu_gp_mutex);
}
i assume u want this
How come?
so u spin for 50 us then unconditionally free it?
thats uhh
not an rcu
the grace period extends indefinitely for as long as someone is working with a copy
Oh.
because 50us isnt a guarantee that the pointer has passed out of knowledge of every other core
on a real system that might work like 100% of the time but under virtualization itll quickly break
I can add an atomic counter to the data (radix tree's rtree_node_t)
thats called reference counting (a different technique)
Well no, because the thing that frees would wait on it.
maybe just do this?
thatll be slower than if you just had the person who dropped the count to 0 free it
because youre still doing the refcounting work plus involving this extra asynchronous context of the free-er for some reason
kind of defeats a lot of the point
i mean you might want to remove the deletion from the path of the guy who dropped it to zero
no big deal, he can just enqueue it to an atomic singly linked list
Doesn't this either fatten all my RCU pointers or make it so that only one thread can read at a time?
yeah if its that expensive to delete u can just have the free callback put it on some list and wake up a work queue
rcu supports infinite concurrent readers
What does rcu_read_lock's mutex actually do to make that possible then?
it's a rw lock
meaning readers can acquire it any number of times
and the write is exclusive
...
So I make the lock in my nodes an rwlock instead of exclusive, and lock it once for each thread that reads the node's content?
Well I'm trying to make my radix tree somewhat inspired by Linux' xarray, which does fine-grained locking with spinlocks.
Maybe we should move the discussion of how I implement this over to #1153085124959809616 ?
yeah
RCU is not really an rwlock
unlike an rwlock, RCU has no concept of an exclusive locking mode
oh true i also need an rwlock impl
RCU is better seen as a barrier
void rcu_lock(rcu* rcu) { asm volatile("" ::: "memory"); }
got it thank you for this correct information

think of it like this: there are 3 primitives: lock(), unlock() and sync() and their relation is:
for each lock() that happens on thread T before a sync() in thread S starts, it is guaranteed that there is a corresponding unlock() by thread T that happens before the sync() in thread S completes
yeah but a fully compliant implementation can be created using a global rw lock
and it probably wouldnt even suck that bad
why would you do that instead of using per data structure rwlocks?
that doesn't make any sense
u wouldnt do that and no one argued that it's the thing u do when u rcus
its one compliant way to do it tho
its a superset so its fully complaint tho
that's not very interesting though
that's like saying: you can implement mutexes in a compliant way by not doing SMP
it's true
u still need mutexes in a uni processor system
depends on the specifics
you can just disable interrupts instead
in any case, "i can implement a weak synchronization mechanism using a strong one" is not really an interesting statement
youll probably still need a mutex if blocking in the kernel exists
well thats just an extreme example, but the thing about RCUs is their idea can be showcased using a very trivial implementation
even unix v6 had a handful of blocking mutexes (explicitly implemented in ugly ways everywhere they were used) around like blocking io and stuff
also, if i add "and lock() and unlock() are wait free" to my definition, the rwlock impl is not possible anymore ;D
the write lock needs to wait for all pending readers to complete
in your rwlock implementation
lock and unlock are wait free
unless you make the rwlock non-fair but then you cannot guarantee forward progress anymore
there is no way to make lock() and unlock() wait free with an rwlock implementation
depends on your definition of wait free
no, sync() is of course not wait free, there is also no rcu implementation where sync() is wait free
that's just impossible
exactly
wait free has a precise definition
namely, the number of instructions is bounded by a constant, no matter what other CPUs do
since my sync() releases the write lock right after acquiring it
isnt that a constant number of instructions
no? because if that CPU goes up in magic smoke while the write lock is taken, the readers will be blocked?
what magic smoke
wait free is a worst case guarantee
like literally the cpu stops working?
yes
uhh lol
or if the interconnect that connects the cpu to the rest of the system fails
or whatever
then ill switch to this impl
void rcu_read_lock(void) { }
void rcu_read_unlock(void) { }
void synchronize_rcu(void)
{
int cpu;
for_each_possible_cpu(cpu)
run_on(cpu);
}
wait free has a precise meaning and one CPU waiting for anther CPU is not wait free
that's just incorrect
lock() needs to disable preemption for that to work
well it assumes non preemptible kernel
what does run_on do
sched_setaffinity ig?
I'd advise everybody to think about synchronization mechanism in terms of what they guarantee and not in terms of how they're implemented
and this #1287456798407790684 message is the proper description of what it guarantees
if you implement code based on the inner workings of synchronization mechanisms, it's easy to make use of some extra properties that an alternative implementation will not provide
btw even if you disregard wait freedom that implementation has problems
if your rwlock is not fair (= it prefers readers), you get fast lock() and unlock() times but you cannot guarantee forward progress of sync() anymore, even if all threads behave correctly
if your rwlock is fair, then lock() and unlock() are not fast anymore because a lock() may need to write for a sync() to finish and the sync() may need to wait for another reader to finish
fair
the run_on() toy implementation and proper implementations can avoid both issues at the same time
what about rcu_read_lock in an interrupt handler?
Depends how your rw lock is implemented
well, if someone write locks then is interrupted, the interrupting context cannot do a read lock.
lock free pmm and vmm ftw
i once analogised rcu to words to the effect of "like a global spldpc()" which i think was an interesting insight but now i've completely forgotten what the thrust of the analogy was
The rust experience
very much so
idk implementing data structures was 10x faster in C but you get untriggered UB left n right
I've already gotten this far I'm not giving up yet
sunk cost fallacy
With rust u get the same ub but because it optimized half of your kernel out 
or worse because you have code in unsafe
well rust keeps me away from a lot of synchronization bullshit
honestly im glad my current project is C even
I was focusing too much on C++ stuff with C++ (like making a huge templated library)
i miss headers
doubt it honestly
What are you gonna be using it for
wdym
Wdym wdym
what am I gonna use what for
The library
it forces you to use encapsulation where variables can have multiple accesses
basically I had started a rewrite of my kernel and was meant to use the library with it (I wrote it in preparation while studying other OSes)
but now I joined ethan's project so the library stays unused
but I learned a bunch of stuff so it doesnt really bother me and it was fun
I'm still stealing stuff from it
and converting to C
Yeah
i dont get why so many people say this
that's one of the main niceties in rust
send + sync
because rust forces a model on you it thinks it's right
but that doesnt mean it is for your use case
sometimes you do want to race on purpose
really?
yea
one place where I race on purpose to avoid lock contention is load balancing
(shameless steal from mintia)
idk how I'm going to do that in rust
i might have to make a special sync primitive for that
oh also I use it in random programs too because it is very nice to use
what prevents you from doing a FakeLock type
deriving Sync
that's what i mean
crazy
that's why you use a good language
I swear one day our bullying will prove itself worth it and you'll switch to C 
rust comes in extremely handy with refcounted objects and error handling with locks
it's basically idiot proof
if i didn't have that I'd deadass just use C
does it take into account lock ordering
locks get destroyed in reverse locking order
thats not what I meant
does it allow you to declare a lock ordering and ensures your locks are taken in that order
i don't understand what order you mean
to avoid deadlocks, it is very useful to have a defined lock ordering where for example you lock A then B and release B and A and you must ALWAYS do that
if you lock B before A you break the ordering and you're prone to deadlock
you could do that with nested locks
that's then enforced at the type level
you can lock A then B and unlock A, but then everything that was part of A becomes invalid
so if B is in the struct of A, B becomes invalid
if you first unlock A, then it won't compile
yea i dont think thats what I meant
but anyway this is tricky to analyze statically
you just gotta be not an idiot when writing your code 
that's what the link you sent is talking about tho
you cannot
theres probably a way you can do this tho I do not know of it
A is a lock, B is another
yes
B is not inside of A
oh
yea that's different
i don't know how you can ensure that tbh
do other languages do that?
no
I was wondering if rust could
but it's probably a tricky problem to statically analyze like I said
yep
actually
not that bad
assign a rank to each lock statically
I'm sure rustc could do this easily
oh wait no you can totally do that in rust
you can make a lock depend on another
during construction
e.g. with a weak ref
the dev does that, and the compiler enforces that ranks are taken in ascending order or something
Lock ordering enforcement at compile time
that's neat
Now that I've read about rcu and implemented I get it I think. DPCs are functions deferred until a certain CPU state is reached (IPL lowered below dispatch), rcu callbacks are functions deferred until all cpus went through a quiescent state, so I can see where you were coming from
Isn't DPC queue being drained at DPC_LEVEL (DISPATCH_LEVEL)? not below.
the DPC queue is drained during the lowering operation
like, when someone calls Lower IRQL, that's when DPCs get dispatched
I meant to say, that they are executed at DPC_LEVEL, not below.
DPCs for IRQL x are executed as soon as IRQL < x, but during the execution of the DPCs IRQL is x
they are executed at dispatch level yes, but they are executed only at the moment when someone lowers the irql to below dispatch level
this is tricky, because sometimes you might take two locks of the same "rank", for example during a VFS rename operation. and sometimes you take two locks which normally mustn't be waited on when holding the other, but it is actually ok because you were holding a third lock which protects this.
the rank only enforces the ordering
sometimes if you have to lock two objects whose locks are the same "rank" you can still have a consistent ordering by taking them in the order of the objects' addresses
yes, but consider e.g. two dentries. it is ok to lock the parent before the child but not vice versa. this is annoying to represent directly as lock ranks.
(in-reply-to: this message)
yes, this doesn't work then
but I think they could still be helpful for simpler lock relationships
I guess you could impl LockedBefore<DEntryLock> for DEntryLock {}, but then you would lose some of the nice properties of ranks.
or if you know the number of maximum children beforehand you could do it at runtime (assign a decreasing rank everytime depth increases) but that's not really good
i'm currently coming up with a way on how to represent mmap'd objects
should i store a reference to a struct object in my struct file?
That's a design decision.
My plan is to have INode be the mmapable object type of thing. And INode is referenced by File anyways.
how do you represent anon memory?
anon inode
oh
INode is just a struct
with great care
bruh
I have some very rough ideas but it is a problem for future me.
start by reference counting page frames
then your page cache will actually hold a weak reference to each of these pages so that it can be immediately removed when the page is to be reclaimed (the page frame isn't mapped anywhere)
reading from and writing to the cache is a filesystem specific operation, right?
as in, implementing a Pager trait on the VFS INode level makes no sense
also does it make sense to have a cache for any inode type or just for regular files?
(@distant cypress )
me personally I think an agnostic cache does no harm (this is what I will implement), and you can have stuff like negative entries so if you fail to lookup any entry, you can insert that in the cache and say it's a negative entry
i am not talking about dentry cache
this way the next lookup can just check that and go "hey we know this doesn't exist"
oh oopsies
i already cache negative entries
i'm talking about whether i should cache an inode's pages:
- regardless of the file type (anything that isn't a regular file)
- on the vfs inode level or on the fs impl level
id say you do not want to duplicate the caching code for each file system type. Just have a generic file cache with like a radix tree of pages. Then each fs provides "cache ops" like read_page(file, page, offset), write_page(file, page, offset). For other file types/uncacheable types you just ignore the page cache
page cache absolutely needs to be a unified thing
thats essential for the vmm to know how to do stuff well
i was recommending the vnode cache be per-filesystem
or rather, i was recommending the vnode cache be eliminated and replaced with the natural caching you get from above by the reference held by the dirent cache
and the lookup structures for vnodes that are currently in memory are given to the filesystem driver
no, because you can map from the page cache directly to user mode
regular files, directories, symbolic links, and block devices
basically anything that has a set amount of content and has a degree of "random access"
exceptions include network adapters, pseudoterminals and character devices
so inodes and block devices
yeah thats a good thing to point out is that directories should be cached like normal files
theyre just files with structured data
you can still cat them on some unixes and get the raw data
for some reason you can do this specifically with the /dev directory on mac os x but not with any other directory
i discovered this when i was like 8 and ive never seen anybody else mention it
its bizarre
i wonder what use this has
actually i think i learned why this is
i don't recommend you allow that
internally yes
but not to user applications
its because the devfs implementation in macos dates back so old that some ls implementations would still read() and parse the directory structure (which is how it always worked in OG unix)
so the devfs authors made sure to fake the old bsd ufs directory structure upon read()s
so those ls would still work
later the ability to read() directories in normal filesystems disappeared
but the devfs one remained as a vestige of like 1985
oh also, how do i use cached entities if they are smaller than the page size? do i need to keep track of the object size in bytes?
yeah
the general question you really want to ask is what to do if its not a multiple of the page size
the answer is the remainder of the page should just be filled with zeroes
and you dont read from there
you truncate the read to the byte size
if you mmap a file youll see zeroes there
unavoidable
also unavoidable that people can write stuff to the page there if they mmaped the end of the file writable
yea i'll just not cache it
it just has no effect
it's like writing out of bounds of a malloc region
well not exactly because that breaks shit meanwhile this is harmless
if you grow the file you just make sure to zero out the new space within the final cached page so that anything previously written there through an mmap'd view doesnt unexpectedly appear in the grown area
all my advice on this comes from weird bugs i encountered after i implemented it so u know its legit
how do i best keep track of objects in an address space? just a linked list or a btree indexed by pn?
you mean mapped regions?
i was planning on using an AVL tree for lookup and a linked list for scanning for a good sized spot to put a mapping
so like if you mmap with a preferred base and say "but it can go somewhere else, preferably after this" you can use the AVL tree to look up the region previous to the preferred base (or containing it if its already mapped) and then the linked list to scan for a large enough empty area
theres also maple trees from linux
which are evil rcu radix trees i think
that they use for this purpose
i can see how radix trees make sense for this
i mean it might literally not be a radix tree i think i read about them exactly one time like 2 years ago
might be a b tree variant or something
so id google it before taking me at my word
all i know is its designed for rcu and only works with rcu (well you can probably do some contrived locking to make it work without rcu but some other data structure probably becomes preferable once you remove the "good with rcu" constraint)
the struct object abstraction is really nice for me, because i can get rust to automatically handle the writeback and pmm when the object has no more references left
careful about where you lower the refcount then
if you dont want to unexpectedly become responsible for shit tons of IO at that moment
or potentially deadlock even
as long as it's mapped somewhere it'll not get dropped
i don't manually refcount them
so this is what i have right now (please correct me if i'm missing anything):
- a
Pagertrait withget_pages,put_pages,get_byte_sizeandhas_pagecallbacks to read from cache, write back to cache, get the size of the object and check if a page can be cached INodeimplements thePagertraitINodecontains anObject, with a weak ref to aPagerimpl (the inode pager in this case)- you can
read,writeto anObject - a mapped object in a process' address space is an entry in a
BTreeMapwith a refcounted reference to anObject(and prot), and the start of the mapping as the key - during a page fault, the current process' address space is scanned for
Objects, if there is one registered at the faulted address, the missing physical page is mapped for the MMU (with potential read-ahead at the pager's discretion)
It's also possible to have an interface where the filesystem provides an i_read_page and i_writeback_page, and then this in combination with i_truncate or i_fallocate is used on write/read.
ah, I should've read further down before I answered. 
What does your get_pages and put_pages do?
get_pages fetches the data from the backing storage, e.g. disk for a given set of offsets and writes them into phys memory (or allocates a new page if the pfn hasn't been cached before)
put_pages writes them back to the disk
@distant cypress i'm stealing your idea
if i use INodes for anon mappings, i can refcount and free physical memory automatically.
lol
if i center everything around inodes, then i can move the caching to the fs layer like will said
yeah...
the thing with the page cache (and honestly, most of the kernel except drivers) is that it is a large part of both the filesystem layer and the memory manager
@distant cypress @autumn jasper for writing into the cached pages, should i allocate a contiguous phys region and just hhdm it in the kernel, or should i map it once into the kernel and keep a list of pages that are in that mapping?
i've been going back and forth for like a solid hour
i can't decide because both have their drawbacks
properly mapping it in the kernel has the benefit of being able to swap out the backing physical page without updaing the virtual addresses
true
but if u dont care just hhdm it
thinking about if i should make a collection type for this
eh fuck it for now i'll just do the hhdm thing
that's a fs impl detail anyways
so NT has a whole component for this called the Cache Manager
in the system there are these things called "system space views" which are fixed size mappings of a file in kernel VA space
and they work exactly the same as user VA space mappings would, if you fault on them, the data is loaded from the page cache, etc.
now you can achieve similar behavior using the HHDM, but you will have to trigger the loading of the page yourself manually if it doesn't exist, among other things
We came up with a hobby OS ism for this which is "file view cache" or "viewcache" which might be a more descriptive term
Cache manager implies it deals with data caching, which it doesn't rly
so if it's per file, does that mean i should have a viewcache object on the struct inode, or is that going to be an fs impl detail again
View cache should definitely be generic
The only thing I was suggesting being an fs driver responsibility is lookup of vnodes
oh
The vnode structure itself should be generic
yea i already did that from the beginning
The vfs just doesn't care where the filesystem got it from
It just says
Here's a vnode for a directory and a name. Give me a ref'd vnode for da child
Fs driver looks up whatever structure it maintains
Importantly this means the fs driver is responsible for its own locking for said structure
Removing the vnode from this structure requires a fs callback later
But you should have some way to extend it on the VFS side
like for example allocating vnodes with an additional bump of X bytes to fit your FSD specific extension
Yes
🤔
oh yea how do i deal with a resizing cache if i don't use the hhdm? do i just have to remap it every time its size changes?
Map it in fixed size chunks
It can have multiple mapping windows at once
a file
And you cache these chunks and replace them when needed and stuff
i mean for the kernel, maybe i'm misunderstanding
i need a way to give me a contiguous buffer for file read and write
right?
i could also just not map it in the kernel and write the buffer by hand by traversing the page list :P
Okay so the file view mapping I was talking about is legit like mmap() like demand paged and all that
How I do it is I have a region of kernel address space set aside that I call cache space
The page tables for it are pre initialized and I keep a free list of 32kb chunks within it, and I thread this free list thru the PTEs themselves
When I need to access a file I check if that 32k aligned region of the file already has a mapping
If so I use that, otherwise I grab one off the free list and map it there
If none are on the free list I grab one from the list of cached but unreferenced view chunks
(which disassociates it from its file and stuff)
And replace it
In the page fault handler I have special handling for page faults in this region, I actually index an array by the # of chunk within the space and this yields a pointer to a structure representing the chunk and I can super quickly get to the file that way and then fault the page in from its page cache