NetBSD-Bugs archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
Re: kern/49017: vfork does not suspend all threads
The following reply was made to PR kern/49017; it has been noted by GNATS.
From: Nico Williams <Nico.Williams%twosigma.com@localhost>
To: <gnats-bugs%netbsd.org@localhost>
Cc:
Subject: Re: kern/49017: vfork does not suspend all threads
Date: Wed, 5 Apr 2017 18:42:25 +0000
Please do NOT stop any threads in the vfork() parent other than the one that
called vfork(). Also, allow me to make an argument for this.
First, I suppose I should look at rationales for stopping all threads in a
vfork() parent. I can think of two (but if I'm missing some, let me know!): a)
that the man page always said that the parent process is stopped, ergo it must
now mean "all threads in the parent process", and b) that the set of safe
functions to call in the vfork() child is made unacceptably smaller by not
stopping all threads in the parent.
Before refuting my strawman rationales for stopping all threads, I'll explain
why stopping all threads is highly undesirable: it kills performance, the very
reason for vfork()'s existence.
There are several ways to use vfork() to spawn children in a high-performance
way:
- First, obviously, in posix_spawn().
It would be terrible to have to stop all of a JVM's many threads just to
spawn a child, and would negate some of vfork()'s massive performance
advantage over fork().
Why should unrelated threads in the parent suffer? (This gets to the safety
issues which I posit might motivate stopping all parent threads, and which I
address below.) Even if there were a strong safety argument for this, we
should aim to make it go away as the performance rationale for using vfork()
is extremely important in real life cases.
(I should point out that, for example, Linux's vfork() does not stop all
other threads in the parent. I can provide a test program that demonstrates
this.)
- Second, one can implement a very fast popen()-like API that uses a threaded
taskq where threads pre-vfork(), enabling a program to spawn processes
faster than with posix_spawn(): without blocking for the child to spin up
then execve()-or-_exit() -- the threads that pre-call vfork() block that
way, but the threads that dispatch the requests to the pre-vfork()ed
children do not block at all, they only call write(2) to write the job to a
pipe to the child.
See my gist about this where I describe this in detail and propose a new
function with this signature:
pid_t avfork(int (*start_routine)(void *), void *arg);
and provide a partial implementation based on a pre-vforking threaded taskq:
https://gist.github.com/nicowilliams/a8a07b0fc75df05f684c23c18d7db234
There is such a very fast popen()-like implementation here:
https://github.com/famzah/popen-noshell (warning GPLv3)
that uses clone() on Linux to get something very much like the avfork() that
I argue for. Its author needs to be able to spawn thousands of processes
very quickly sometimes (see
https://github.com/famzah/popen-noshell/issues/11#issuecomment-287235234).
Now, to knock down my strawman rationales for stopping all threads in the
vfork() parent:
- Regarding (a), pre-threads vfork() man page text saying "stops the parent
process" should not be interpreted as meaning "all threads" now that we have
a threaded world. Clearly the original authors could not have meant that,
nor for that matter would they have meant that only the thread that called
vfork() in the parent must be stopped. We must decide this matter de novo.
Clearly the thread that called vfork() must be stopped until the child
execve()s or _exit()s. That much is utterly clear: because two schedulable
threads/entities simply cannot share a stack concurrently. So we only need
to decide whether other threads in the parent must also be stopped, and the
original man page text simply can't guide us as to that as it predates
threads.
- Regarding (b), it may already the case that the set of functions that may
safely be called in the vfork() child is somewhat smaller than the set of
functions that may be called in a fork() child. Since POSIX has deprecated
vfork(), we don't know what that set is (though we can inspect earlier POSIX
standards) and may now define it to our liking.
In any case, the set of async-signal-safe functions defined by POSIX looks
like it should be safe to call in a vfork() child on any reasonable OS since
all of them should be system calls that do not affect the shared address
space (or anything else that might still be shared between the child and the
parent): http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_04.html
As an aside, obviously the child might also probably not want to change FD
or FL flags with fcntl() for file descriptors shared with the parent. And
it should also not use the horrible POSIX file locking, though that's mostly
because nothing should use the horrible POSIX file locking! This aside
brought to you by intense feelings of disgust elicited by POSIX file locking.
Note that there are a number of functions NOT INCLUDED in the standard list
of async-signal-safe functions:
- pthread_*()
- brk(), sbrk(), mmap(), munmap(), mprot()
- the heap allocator (quite naturally, since it might need to call
brk()/sbrk() and/or mmap()/munmap(), or pthread_*() functions, none of
which are async-signal-safe)
which means that the scariest functions one might call on the child-side of
vfork() are by definition (e.g., the old POSIX vfork() specification)
already not safe to call on the child-side of vfork().
In any case, again, NetBSD is free to further narrow the set of functions
that are safe to call in the child-side of vfork() should that be necessary.
The biggest problem with vfork(), really, is that unsafe signal handlers in the
might run in the child before the child can block them. This could be bad even
if all threads in the parent are stopped.
Indeed, I would argue that the set of functions that are safe to call in an
asynchronous signal handler (as opposed to the child-side of fork() or vfork())
is smaller than that which POSIX says. The only things I ever do in the signal
handlers I write are:
- write to sig_atomic_t variables
- call write(2) to write a single byte into a pipe that is used in the
application's event loop
If the application does not have an event loop I do sometimes ensure that
there's a thread blocking on read(2) on the other side of that pipe.
- call write(2) to write to stderr
- call _exit(2)
If I had my way those would be actions things I'd allow in signal handlers in
POSIX! (And then we'd have to give a new name to the async-signal-safe
function set that we reuse to define the functions that are safe to call in
various other contexts such as the child-side of fork()!)
Thanks for taking the time to read this -- it's probably too long, and I
apologize about that. If I'm wrong about something here, please let me know!
Nico
--
Home |
Main Index |
Thread Index |
Old Index