I ran into some ownership issues working with Unix fork (through the nix crate). I wanted to move a value into the child but keep it borrowed in the parent.
I wrote this abstraction which I think is safe. I'd appreciate if someone could confirm the safety of it, or help me understand why it migh not be.
fn fork<T, C: FnOnce(T)/* -> ! */>(data_ref: &T, child: C) -> Pid {
match nix::unistd::fork().unwrap() {
ForkResult::Child => {
let data = unsafe { (data_ref as *const T).read() };
child(data);
unreachable!();
},
ForkResult::Parent { child } => child,
}
}
Note that the child closure shouldn't return, but -> ! isn't available yet so I just threw in unreachable!.
One point which I'm not sure about is read vs read_unaligned. I assumed that Rust references are always aligned.
It would look like
fork(&data, |data| {
// do something with data
// do something that doesn't return control flow
// e.g. cmd.exec(), or a loop, or a panic
})
enum Diverging {}
fn fork<T, F> (data_ref: &T, child: F) -> Pid
where
F : FnOnce(&T) -> Diverging,
{
match ::nix::unistd::fork().unwrap() {
ForkResult::Child => {
match child(&data) {} // : !
},
ForkResult::Parent { child } => child,
}
}
But generally, using unix's fork with shared pointers does not lead to the intuitive behavior you'd expect, and I am pretty sure it is not compatible with Rust guarantees: shared global memory (shm_* family, mmap) becomes inherently aliased and thus unsafe to mutate; the other memory is, AFAIK, entirely copied (copy-on-write for perfomance), so there is no real "sharing", here. Finally, the child process can survive its parent's death and thus outlive it, so careful with that too.
Yep, hence mutation with those APIs is inherently racy, leading to potential UB unless it is synchronized.
The following C program, for instance, attempts to increment many times and in parallel from both the parent and the forked child a shared (through mmap) uint64_t, and leads to the given shared integer having around 52% of the total expected value: