The memory remains: Permanent memory with systemd and a Rust allocator
darkcoding.net
darkcoding.net
[0] https://doc.rust-lang.org/reference/type-layout.html#represe...
But then you're going to hit other problems. This isn't a new idea. Presumably one of the reasons you're restarting your server is to upgrade it ...
I feel like it is the missing piece here because you could do hash table lookups in shared memory, too!
Cool article, I learnt about a new systemd and custom allocators in Rust !
It needs to _seriously_ improve discoverability and documentation.
https://en.wikipedia.org/wiki/Launchd#Socket_activation_prot...
I do believe that launchd came first: https://news.ycombinator.com/item?id=2565780
Perhaps I misunderstood your comment in interpreting it to say socket activation's an original systemd concept.
I was trying to respond to "Why does systemd have to store file descriptors?" with "That's actually the entire point of its existence"
Of course tmpfs storage does get written to disk via swap.
The biggest reason to use memfd_create is that you can seal the memfd so it cannot be resized, and then it's safe to mmap on another side of a trust boundary; if you mmap an arbitrary attacker-controlled FD, the attacker(/poorly written client) can make you segfault. Wayland uses sealed memfds to pass framebuffers.
This seems like way less work, seems way less convoluted than writing a whole intermediary layer to serialize objects out into some whatever format on disk, & read them back. The objects are just there, primed, ready to go. It harkens back to single level store days, when disk-backing memory was done by the OS (multics). https://gunkies.org/wiki/Single-level_store
Another way of looking at this is as a return to the old days before we made everything convoluted.
Like needing to allocate a fixed amount of memory for your whole program upfront or juggling multiple fds.
Or that there is some state you can not carry over even with that and if you try you can easily run into soundness issues.
Or that the state saved might be "in between" operations in a very unexpected way and in turn unsound.
etc.
Can I not do the normal virtual memory thing and allocate far more memory than I actually have hardware for?
> Or that there is some state you can not carry over even with that and if you try you can easily run into soundness issues.
I mean, fair... you're going to have to be careful what you save either way but making saving things the default does seem like more of a footgun.
> Or that the state saved might be "in between" operations in a very unexpected way and in turn unsound.
This applies even if you only use it for some structs? This whole mechanism seems like it would only be sound if it was only used after clean exits.
yes, except maybe if a Drop constructor is run unexpectedly on the exit or similar
generally I think using it with anything which has pointers and/or runs Drop in it is brittle and prone to bugs
in turn most things which do not have pointers should be fine with a clean exit
and anything which only consists of memory where any bit combination is always valid should always be sound even on a abort (e.g. a `[u8]` allocated directly in that memory region or a `[T]` where `T` only has primitive non-allocating types)
I do think though that these patterns of having file descriptors of stuff that can be passed around, and having semi-seamless "just map in the object whole" have a ton of super powered ultra-high performance wins to it that deserve a lot of exploration & effort that justify their pursuit.
But Checkpoint Restore in Userspace/criu just works & is great for fast loading a thing. Still, we need at least some fd passing for apps like no-downtime-restart (or smart draining + SO_REUSEPORT); that's a real need. Going further and passing around memfd's is cool.
It's gotten dropped, but for example Apache Arrow had a Plasma object store for this stuff for a while, that I wish was still on the scene (or something like it). Arrow is a particular data-format with it's own libraries, versus what we have here which is coding-as-usual(-ish); 1/2 patterns. The second might be harder but I still think it has potential.
Sure there's problems. Unix won and multics lost, and we have half a century of accrued complexity around that, and a couple weekends of hacking isn't going to get us all the way back to a perfect Single Level Store.
Is this something we can and should all switch to? No. The costs are high, it's not practical. But I think over time, especially with the rise of CXL and rdma resurgent, we'll see zero-cooy zero-serialize techniques like this grow back & be huge performance wins for us, be much simpler to handle, even with the adaption we'll have to do.
I'm working on a project now where I'm maintaining half a dozen forks just to add a single line.
</sarc>
As far as I can tell you should only use it for "splatted" data, maybe allowing gaps.
E.g. using it to not needing to load again some kind of multiple 1GiB bit mask which are just a 1GiB blob of bits each would probably work grate. Using it on structs which do not contain any pointer (i.e. no Strings, no Vecs, no &str etc.) would work okay. But the moment any pointers are stored in data carried over that way things become a huge mess and are basically fundamentally unsafe as they can easily become unsound.
I dislike it because it violates the "rule of least power". Basically, if you never have to validate your internal data, you can never tell it's corrupt.
It also introduces some neat little known tech.
Even if "serializing" state for carrying it over with a controlled restart the File Descriptor Store might still be an interesting case use (in some cases, in others it might a problem). Also you probably would dump bincode or similar for such a "carry over state" serialization, there is very little reason to go with strings.
Through there are some major differences:
- PRO: This approach works with types which do not implement any serialization mechanism or e.g. cyclic data structure. At least theoretically you could carry in-progress futures over a restart with that (practically it's a horror story to try to do so which likely won't work most times).
- CON: This approach has issues with types containing any form of pointers, references and similar. They all also need to use the special Allocator and then there are issues if after the restart the mem file isn't mapped to the exact same address range. This makes it way harder to use, bugs can be subtle introduced and lead errors you normally can't have in rust without unsafe code.
- PRO: You don't have to move/copy anything around, so if you e.g. have some huge splatted (i.e. flat in memory) structures this can safe quite a bit on upstart time. (idk. lets say some 1GiB bit mask for some kind of caching)
- CON: Debugging the carried over state, or e.g. copying it between systems is not possible or at least much more complex. Reboots might also be an issue (I have to check).
In generally for many use-cases this is pointles, and for the times where it's not pointless it's often either brittle or too limited and in turn better avoided. Through for a very few use-cases it can be a huge boon.
- PRO (& CON): You don't need to take an explicit action to "save" the state, you do the action once when setting it up at the begin of the program, it's also a CON as depending on usage in case of a hard stop you can end up with a state with partial writes to it (can't happen if only used on clean shutdown)
PPS:
- Using mmap with MAP_FIXED to get a fixed address every execution to make pointers work _is a potential security vulnerability_ (it should have been called MAP_REPLACE IMHO). On some platforms you can use MAP_FIXED_NOREPLACE to safely do so, but I have no idea how to choose an address as there is always a risk of address layout randomization getting in your way.
- on platforms where MAP_PRIVATE is implemented using a copy-on-write mechanic you could have a pattern where on a start without a fd you create the fd and load some huge data into it and then restart, when started with fd you mmap it with MAP_PRIVATE allowing you to change the data but also to always go back to the clean state by restarting. No idea for what use-case that makes sense but I found it interesting (also haven't tried it out).
Where are you using mmap(MAP_FIXED) without first calling mmap(PROT_NONE) to get the base address?
Edit: systemd is a religion now, and it's blasphemy to speak against it?
No, yours is just a low-effort comment that doesn't meaningfully add to the discussion.
I strongly disliked systemd (and its proponents) for a long time. Until I realized it just doesn't matter, and all the hate it gets is just boring and takes time away from useful things.
build all the functionality into systemd!