-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Portability option to use vfork()? (or posix_spawn()) #11
Comments
You can't capture the output of the child process if you use vfork(). More info here: https://blog.famzah.net/2009/11/20/a-much-faster-popen-and-system-implementation-for-linux/ Am I missing something? |
(I should have encouraged to use Your post is wrong as to The child of
(The standard's With
There might be downsides to using |
Also, either way you must use only async-signal-safe library functions on the child side of |
Interesting... :) The first paragraph of the vfork() man page says the following:
Is the man page wrong about [1], [2], or [3]? The Open Group man page confirms the above statements:
Do you have sample code to demonstrate what you suggest? |
Yes, they're both wrong that you cannot call any other functions than If you look at glibc, it supports using Now, if you want to steer clear of Links: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/threads/spawn.c#304 |
I should add that Lots of libraries break if you use them, In general the only way it can be safe to do anything other than having the parent exit, or the child exec-or-exit soon after the fork, is to fork so early in the process' life that no libraries can have fork-unsafe behavior. Personally, I find process forking to be rather evil for this reason. I much prefer the spawn approach. Years ago, before I had a taste of fork-unsafety, I used to think that the Unix approach was so much better than the Windows approach. Now I'm in the fork-is-evil-use-spawn-instead camp. However, you're basically building a spawner, and so here (In my experience, writing fork-safe code can be much harder than writing thread-safe code. Particularly fixing fork-unsafe code to be fork-safe.) |
Also, long ago BSD removed
The history of |
I should also add that The only interesting wrinkle in the (non-)evolution of |
Oh, also from the Linux manpage:
which together with this from the
should clarify things further. Lastly, from the Linux manpage of
which means that you want to use |
I should also add that the *BSD manpages for vfork(2) are not nearly as scary as the ones from Linux or Open Group are. BSD, of course, is the source of vfork(2) in the first place. |
I'm researching this and need more time... I'm amazed by your expertise in the *fork() functions and implementation details. :) In the meantime, I have a few comments/questions. (1) It seems like "glibc" completely switched posix_spawn() to always use vfork() ?
(2) I don't use CLONE_VFORK but all other implementations seem to use it. Do you know why it's important to suspend the parent process until the child releases its virtual memory resources via a call to execve(2) or _exit(2) ? (in the context that I use it here as a spawner)
(3) How do you know that async-signal-safe functions are safe to be used within a vfork() child process?
|
(1) would not surprise me. Regarding (2)... In the beginning... there were no threads, only single-threaded processes. Fast-forward a bit and we have all sorts of new things, like COW and threads. COW made But what about threads? Well, I don't see any reason to stop any threads in the My take is that Regarding (3)... it's already the case that it's not safe to do certain things in the child-side of plain
If there are other threads in the parent writing to stdout via stdio, this is not safe! Calling POSIX talks about async-signal-safe functions: functions that are safe to call in handlers for asynchronous signals. These are typically just system calls.
Now, to answer the question in the SO you linked, in the case of The thing I typically do for daemonizing portably is this:
Now, let's talk about this. The bit about |
I should add that there's only two good things to do in async signal handlers (synchronous signal handlers are another story):
The above are safe even if you ALSO, calling
Just say no to Async signal handlers too are evil. About the only things worth doing with them are writing to But Thanks for the praise earlier. I was afraid I'd turned you off by appearing to be a know-it-all. |
Anybody calling |
If In the beginning it was less evil because there were no threads, signals were mostly fatal, and every program was simple. And in the beginning A trivial So even in the beginning Mind you, |
Even now you could use a helper program to avoid the signal handler races (one of the linked things talks about this). To use a helper, if you want to (I don't think it should be required, but it's kinda nifty!), just:
In order to properly pass |
Of course, it's easier to use your implementation if it doesn't need a helper :) So don't use a helper. Don't worry too much about the signals race. |
Nico, thanks for all your efforts to explain everything in details. I really enjoy this productive conversation. Duh, having the same stack requires the parent thread to be stopped, right :) An interesting fact is the current posix_spawn() implementation in "libc" -- they stop the parent, even though the clone()'d child has its own stack. That's because they don't use CLONE_SETTLS, in order for the child process to have its own TLS like "errno", etc. I'm starting to find too many flaws in my current implementation :) Am I doing anything right?
P.S This with the helper program is an interesting approach but probably has some performance penalty due to the double exec()? Additionally, how does it solve the signals race condition? It seems like the time window gets smaller because we do much of the work in the helper, but still the race condition exists between the vfork() and the exec() of the helper? |
(1) No idea. I'm not an expert on Linux's As to (1b), I'm not sure that You can always use (2) The signal handler race is about signal handlers running on the child-side of (3) That is just glibc being fucking broken as fuck. Excuse my French, but there's no polite way to put it. glibc's IMO glibc should just stop calling atfork handlers in (4) Multi-threaded applications should NEVER call There's nothing you could possibly do to protect against another thread doing a (5) I've not reviewed your code, honestly, not further than to notice that it's not portable :) Also, anything that uses the GPL is basically not interesting to me. LGPL is OK in some cases. I prefer BSD for just about everything, though various community licenses work for me too. So why am I helping you? I'm just providing friendly advice! There aren't many better-than-popen projects, so I think yours could use a hand, and here I am giving it. |
BTW, I tested My test has the main thread start two threads, one printing "worker here\n" every second, while the other calls
So, in fact, there's absolutely no reason that you should not use EDIT: And on non-Linux systems, whether Test program attached: vfork-test.c. |
I'm thinking I should take all my commentary and write up a blog post titled "fork() is evil, vfork() is not". Or something like that. |
Oh, and I misunderstood one of your questions.
The Open Group man page for |
Also, regarding the helper program idea... A very small helper program will be cached in memory. The exec system calls should be very fast by comparison to As to signals... yes, there remains a race condition if you have signal handlers that can affect the outcome of |
Actually, dealing with signals seems not so bad anyways. Block them in the parent, reset signals with handlers to |
I think we've cleared it up almost entirely. Only one thing still bugs me a little. Why would you state that "stopping just the thread calling your function is just fine: there's nothing for it to do anyways other than wait for the child to indicate readiness/outcome". Some applications do extensive process spawning, like Nagios for example, which runs many parallel checks via external binary programs. I have an instance which spawns thousands of processes per minute. Stopping the parent process for the short time between vfork() and the exec() introduces unneeded latency in the parent process. The best solution is that the parent initiates the spawn of a process, gets the file descriptor to communicate with the child process, and continues with its tasks. It then regularly polls via select() or similar calls whether this child has something to say, and/or if it died by receiving an EOF in the pipe. You can poll many such file descriptors in parallel from the parent process. P.S. I must admit that the vfork() approach is much more standard and well tested. |
Oh, I agree that the calling thread in the parent could just asynchronously handle the child's readiness/error message, and indeed, everything should be all async all the time. That's a bit of a mantra for me, and yet I missed the possibility that the parent thread might not have to wait synchronously :( Good on you to think of it! If you can get semantics like that with Of course, a synchronous spawn API that's very light-weight could easily be run from a thread anyways to turn it into async. So with |
Oh right! This is a |
OK, I have now looked at the code a bit, and here, then are the minimal changes you should make:
For portability, use |
I believe I found the reason why they didn't do it completely parallel. Here is a snippet from "sysdeps/unix/sysv/linux/spawni.c": /* The Linux implementation of posix_spawn{p} uses the clone syscall directly
with CLONE_VM and CLONE_VFORK flags and an allocated stack. The new stack
and start function solves most the vfork limitation (possible parent
clobber due stack spilling). The remaining issue are:
1. That no signal handlers must run in child context, to avoid corrupting
parent's state.
2. The parent must ensure child's stack freeing.
3. Child must synchronize with parent to enforce 2. and to possible
return execv issues.
The first issue is solved by blocking all signals in child, even
the NPTL-internal ones (SIGCANCEL and SIGSETXID). The second and
third issue is done by a stack allocation in parent, and by using a
field in struct spawn_args where the child can write an error
code. CLONE_VFORK ensures that the parent does not run until the
child has either exec'ed successfully or exited. */ The parent must free the allocated stack for the child. But the usage interface of That's why the developers of glibc chose to suspend the parent process until the child has exited. At least that's my version of "why they did it". Do you agree? |
@famzah (2) seems silly because it seems like the same problem must come up as to detached threads. Whatever works for detached threads, should work for a (3) is a bit more likely: how does the parent process know that the stack for that new child is no longer in use because the child exec'ed? This is very tricky! Normally I would use a pipe with ...But that's not appropriate for a library because it's racy: between the point where the
Still, those two methods should work asynchronously for a library like yours. But the first one won't do for a C library proper because it requires keeping more file descriptors open across library functions. The second one might not work at all. The third one only works on newer kernels. Maybe something with POSIX or SysV semaphores?? So I agree that (3) is probably the real reason You could still combine threads and |
Another method for solving (3) might be to check |
If the child gains or drops privs during or after the exec, using |
There is one more thing that could be done about (3) though:
You said using a statically-linked-with-musl wrapper executable meant an 8% slow-down. But if the parent doesn't have to block you might get higher throughput in spite of the higher latency. It's worth considering. |
A statically-linked-with-musl wrapper executable is only 8% slower than the assembler wrapper. But the assembler wrapper itself brings 50% slow-down compared to a plain |
The good thing about my library is that it re-implements If you don't have other comments, I'll ask on the glibc development mailing list if it's viable to run the child with its own TLS in parallel with the parent. And also how to set up a separate TLS structure from the allocated stack. |
Ah, right. I was focusing on how glibc might solve (3) in Yes, I think we're done here and this horse has been beaten to death. You should definitely send email to the glibc-dev list and/or open a ticket. Other libc's might also care. (I'm disappointed that |
When you post to glibc-dev, do, of course, send them a link to this issue. Also, you might want to cc' me -- I would like to follow the thread, and maybe participate. Hmm, which list exactly would it be? See https://www.gnu.org/software/libc/involved.html. I don't see a dev list as such. Would you send to lib-alpha, or glibc-bugs? I might have to subscribe first. |
@nicowilliams, to be honest I think we shouldn't get into the dark zone with the undocumented glibc features, at least for now. I have no good use-case and test-case, nor demand for this optimization. I've tried the idea with the separate helper thread which calls vfork(). And the results are pretty good. 26% slow-down compared to a pure vfork(); 13% comes just from the pthread_create() + pthread_join(). # direct call to "tiny2" by vfork()
famzah@vbox64:~/svn/github/popen-noshell/performance_tests/wrapper$ ./run-tests.sh
real 0m4.646s
real 0m4.619s
real 0m4.601s
famzah@vbox64:~/svn/github/popen-noshell/performance_tests/threads$ ./run-tests.sh
real 0m5.779s
real 0m5.835s
real 0m5.854s
# Only pthread() create + join without vfork():
real 0m0.764s
real 0m0.757s
real 0m0.763s |
@nicowilliams, to be honest I think we shouldn't get into the dark zone with the undocumented glibc features, at least for now. I have no good use-case and test-case, nor demand for this optimization.
Sure.
I've tried the idea with the separate helper thread which calls vfork(). And the results are pretty good. 26% slow-down compared to a pure vfork(); 13% comes just from the pthread_create() + pthread_join().
Don't pthread_join() though! Detach that thread!
|
To my surprise, creating the threads "detached" slows things down! Both using the famzah@vbox64:~/svn/github/popen-noshell/performance_tests/threads$ ./run-tests.sh
real 0m7.131s
real 0m7.190s
real 0m7.140s |
To my surprise, creating the threads "detached" slows things down! Both using the `pthread_attr_t *attr` feature to create the threads directly "detached", and using `pthread_detach()` give the same benchmark results, which are slower than the original code which uses `pthread_join()`:
I am as surprised as you! Perhaps cleaning up detached threads requires
a lot of overhead? OK, I give up :( Thanks for trying though!
I intend to find the time to finish an implementation of avfork() that
uses a task queue. Assuming that pthread_cond_wait() and
pthread_cond_signal() do not slow things down ridiculously, I expect
that to be reasonably fast. But who knows! With these numbers you're
showing, there may be no way to make it not suck other than to implement
avfork() directly in glibc (which I'm not willing to do).
|
The only "special" condition on my test machine is that it has one virtual CPU core, because this allows me to benchmark multi-threaded apps more easily. Maybe running those tests on a single CPU core could be the reason for this unexpected slow down. |
I am definitly not familiar with |
On reuse of stack, IMHO close-on-exec Create close-on-exec This can be potentially done on another thread or by the main program itself if it also utilizes polling interface.
|
Hi NobodyXu. I tried to grasp what you said but I'm too much out of context now. Furthermore, the standard posix_spawn() syscall should already be available on any up-to-date Linux distro, as it have been 4 years since it got merged into the glibc. My tests show that posix_spawn() is as fast as my library. Therefore, developers should switch to posix_spawn() which is maintained mainstream and should be more bug free. |
Sorry, I didn't know that you have given up on this topic.:D |
Hi famzah Sorry to bother you again, but I have successfully implemented @nicowilliams 's idea on $ ll -h bench_aspawn_responsiveness.out
-rwxrwxr-x 1 nobodyxu nobodyxu 254K Oct 2 15:02 bench_aspawn_responsiveness.out*
$ uname -a
Linux pop-os 5.4.0-7642-generic #46~1598628707~20.04~040157c-Ubuntu SMP Fri Aug 28 18:02:16 UTC x86_64 x86_64 x86_64 GNU/Linux
$ ./a.out
2020-10-02T15:02:45+10:00
Running ./bench_aspawn_responsiveness.out
Run on (12 X 4100 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x6)
L1 Instruction 32 KiB (x6)
L2 Unified 256 KiB (x6)
L3 Unified 9216 KiB (x1)
Load Average: 0.31, 0.36, 0.32
---------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------
BM_aspawn_no_reuse 18009 ns 17942 ns 38943
BM_aspawn/threads:1 14500 ns 14446 ns 48339
BM_vfork_with_shared_stack 46545 ns 16554 ns 44027
BM_fork 54583 ns 54527 ns 12810
BM_posix_spawn 125061 ns 29091 ns 24483 The column "Time" is measured in terms of system clock, while "CPU" is measured in terms of per-process CPU time. |
Intro on
|
Example code can be seen here |
I added a reference to your project in the README of "popen-noshell". Keep up the good work! |
@famzah Thank you and I will keep on improving it :D |
Hi all. I'm sorry if this is wrong thread for my question, but i'll give a try. What about if i have popen on old libc, and after that i must to wait input to pipe, catch it, and check if the child done - if not - wait for input again. Because of blocking of child when pipe fd buffer full. So, the only thing i can found to check if pipes fd buf have data already - using of ioctl(). How can i hack it? Thanks. |
If you mind blocking, you can simply If you do mind blocking, you can utilize |
No description provided.
The text was updated successfully, but these errors were encountered: