mirror of
https://github.com/samba-team/samba.git
synced 2024-12-27 03:21:53 +03:00
f50bd0d050
This makes it easier to understand the process graph as there is not a duplicate (eg) ldap[master] process for each tfork waiter. when useing "ps -ef -o pid,comm" BUG: https://bugzilla.samba.org/show_bug.cgi?id=14287 Signed-off-by: Andrew Bartlett <abartlet@samba.org> Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
945 lines
18 KiB
C
945 lines
18 KiB
C
/*
|
|
fork on steroids to avoid SIGCHLD and waitpid
|
|
|
|
Copyright (C) Stefan Metzmacher 2010
|
|
Copyright (C) Ralph Boehme 2017
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "replace.h"
|
|
#include "system/wait.h"
|
|
#include "system/filesys.h"
|
|
#include "system/network.h"
|
|
#include "lib/util/samba_util.h"
|
|
#include "lib/util/sys_rw.h"
|
|
#include "lib/util/tfork.h"
|
|
#include "lib/util/debug.h"
|
|
#include "lib/util/util_process.h"
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
#include <pthread.h>
|
|
#endif
|
|
|
|
#ifdef NDEBUG
|
|
#undef NDEBUG
|
|
#endif
|
|
#include <assert.h>
|
|
|
|
/*
|
|
* This is how the process hierarchy looks like:
|
|
*
|
|
* +----------+
|
|
* | caller |
|
|
* +----------+
|
|
* |
|
|
* fork
|
|
* |
|
|
* v
|
|
* +----------+
|
|
* | waiter |
|
|
* +----------+
|
|
* |
|
|
* fork
|
|
* |
|
|
* v
|
|
* +----------+
|
|
* | worker |
|
|
* +----------+
|
|
*/
|
|
|
|
#ifdef HAVE_VALGRIND_HELGRIND_H
|
|
#include <valgrind/helgrind.h>
|
|
#endif
|
|
#ifndef ANNOTATE_BENIGN_RACE_SIZED
|
|
#define ANNOTATE_BENIGN_RACE_SIZED(obj, size, description)
|
|
#endif
|
|
|
|
#define TFORK_ANNOTATE_BENIGN_RACE(obj) \
|
|
ANNOTATE_BENIGN_RACE_SIZED( \
|
|
(obj), sizeof(*(obj)), \
|
|
"no race, serialized by tfork_[un]install_sigchld_handler");
|
|
|
|
/*
|
|
* The resulting (private) state per tfork_create() call, returned as a opaque
|
|
* handle to the caller.
|
|
*/
|
|
struct tfork {
|
|
/*
|
|
* This is returned to the caller with tfork_event_fd()
|
|
*/
|
|
int event_fd;
|
|
|
|
/*
|
|
* This is used in the caller by tfork_status() to read the worker exit
|
|
* status and to tell the waiter to exit by closing the fd.
|
|
*/
|
|
int status_fd;
|
|
|
|
pid_t waiter_pid;
|
|
pid_t worker_pid;
|
|
};
|
|
|
|
/*
|
|
* Internal per-thread state maintained while inside tfork.
|
|
*/
|
|
struct tfork_state {
|
|
pid_t waiter_pid;
|
|
int waiter_errno;
|
|
|
|
pid_t worker_pid;
|
|
};
|
|
|
|
/*
|
|
* A global state that synchronizes access to handling SIGCHLD and waiting for
|
|
* childs.
|
|
*/
|
|
struct tfork_signal_state {
|
|
bool available;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
pthread_cond_t cond;
|
|
pthread_mutex_t mutex;
|
|
#endif
|
|
|
|
/*
|
|
* pid of the waiter child. This points at waiter_pid in either struct
|
|
* tfork or struct tfork_state, depending on who called
|
|
* tfork_install_sigchld_handler().
|
|
*
|
|
* When tfork_install_sigchld_handler() is called the waiter_pid is
|
|
* still -1 and only set later after fork(), that's why this is must be
|
|
* a pointer. The signal handler checks this.
|
|
*/
|
|
pid_t *pid;
|
|
|
|
struct sigaction oldact;
|
|
sigset_t oldset;
|
|
};
|
|
|
|
static struct tfork_signal_state signal_state;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
static pthread_once_t tfork_global_is_initialized = PTHREAD_ONCE_INIT;
|
|
static pthread_key_t tfork_global_key;
|
|
#else
|
|
static struct tfork_state *global_state;
|
|
#endif
|
|
|
|
static void tfork_sigchld_handler(int signum, siginfo_t *si, void *p);
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
static void tfork_global_destructor(void *state)
|
|
{
|
|
anonymous_shared_free(state);
|
|
}
|
|
#endif
|
|
|
|
static int tfork_acquire_sighandling(void)
|
|
{
|
|
int ret = 0;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_mutex_lock(&signal_state.mutex);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
|
|
while (!signal_state.available) {
|
|
ret = pthread_cond_wait(&signal_state.cond,
|
|
&signal_state.mutex);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
signal_state.available = false;
|
|
|
|
ret = pthread_mutex_unlock(&signal_state.mutex);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tfork_release_sighandling(void)
|
|
{
|
|
int ret = 0;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_mutex_lock(&signal_state.mutex);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
|
|
signal_state.available = true;
|
|
|
|
ret = pthread_cond_signal(&signal_state.cond);
|
|
if (ret != 0) {
|
|
pthread_mutex_unlock(&signal_state.mutex);
|
|
return ret;
|
|
}
|
|
|
|
ret = pthread_mutex_unlock(&signal_state.mutex);
|
|
if (ret != 0) {
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
static void tfork_atfork_prepare(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = pthread_mutex_lock(&signal_state.mutex);
|
|
assert(ret == 0);
|
|
}
|
|
|
|
static void tfork_atfork_parent(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = pthread_mutex_unlock(&signal_state.mutex);
|
|
assert(ret == 0);
|
|
}
|
|
#endif
|
|
|
|
static void tfork_atfork_child(void)
|
|
{
|
|
int ret;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_mutex_unlock(&signal_state.mutex);
|
|
assert(ret == 0);
|
|
|
|
ret = pthread_key_delete(tfork_global_key);
|
|
assert(ret == 0);
|
|
|
|
ret = pthread_key_create(&tfork_global_key, tfork_global_destructor);
|
|
assert(ret == 0);
|
|
|
|
/*
|
|
* There's no data race on the cond variable from the signal state, we
|
|
* are writing here, but there are no readers yet. Some data race
|
|
* detection tools report a race, but the readers are in the parent
|
|
* process.
|
|
*/
|
|
TFORK_ANNOTATE_BENIGN_RACE(&signal_state.cond);
|
|
|
|
/*
|
|
* There's no way to destroy a condition variable if there are waiters,
|
|
* pthread_cond_destroy() will return EBUSY. Just zero out memory and
|
|
* then initialize again. This is not backed by POSIX but should be ok.
|
|
*/
|
|
ZERO_STRUCT(signal_state.cond);
|
|
ret = pthread_cond_init(&signal_state.cond, NULL);
|
|
assert(ret == 0);
|
|
#endif
|
|
|
|
if (signal_state.pid != NULL) {
|
|
|
|
ret = sigaction(SIGCHLD, &signal_state.oldact, NULL);
|
|
assert(ret == 0);
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_sigmask(SIG_SETMASK, &signal_state.oldset, NULL);
|
|
#else
|
|
ret = sigprocmask(SIG_SETMASK, &signal_state.oldset, NULL);
|
|
#endif
|
|
assert(ret == 0);
|
|
|
|
signal_state.pid = NULL;
|
|
}
|
|
|
|
signal_state.available = true;
|
|
}
|
|
|
|
static void tfork_global_initialize(void)
|
|
{
|
|
#ifdef HAVE_PTHREAD
|
|
int ret;
|
|
|
|
pthread_atfork(tfork_atfork_prepare,
|
|
tfork_atfork_parent,
|
|
tfork_atfork_child);
|
|
|
|
ret = pthread_key_create(&tfork_global_key, tfork_global_destructor);
|
|
assert(ret == 0);
|
|
|
|
ret = pthread_mutex_init(&signal_state.mutex, NULL);
|
|
assert(ret == 0);
|
|
|
|
ret = pthread_cond_init(&signal_state.cond, NULL);
|
|
assert(ret == 0);
|
|
|
|
/*
|
|
* In a threaded process there's no data race on t->waiter_pid as
|
|
* we're serializing globally via tfork_acquire_sighandling() and
|
|
* tfork_release_sighandling().
|
|
*/
|
|
TFORK_ANNOTATE_BENIGN_RACE(&signal_state.pid);
|
|
#endif
|
|
|
|
signal_state.available = true;
|
|
}
|
|
|
|
static struct tfork_state *tfork_global_get(void)
|
|
{
|
|
struct tfork_state *state = NULL;
|
|
#ifdef HAVE_PTHREAD
|
|
int ret;
|
|
#endif
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
state = (struct tfork_state *)pthread_getspecific(tfork_global_key);
|
|
#else
|
|
state = global_state;
|
|
#endif
|
|
if (state != NULL) {
|
|
return state;
|
|
}
|
|
|
|
state = (struct tfork_state *)anonymous_shared_allocate(
|
|
sizeof(struct tfork_state));
|
|
if (state == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_setspecific(tfork_global_key, state);
|
|
if (ret != 0) {
|
|
anonymous_shared_free(state);
|
|
return NULL;
|
|
}
|
|
#endif
|
|
return state;
|
|
}
|
|
|
|
static void tfork_global_free(void)
|
|
{
|
|
struct tfork_state *state = NULL;
|
|
#ifdef HAVE_PTHREAD
|
|
int ret;
|
|
#endif
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
state = (struct tfork_state *)pthread_getspecific(tfork_global_key);
|
|
#else
|
|
state = global_state;
|
|
#endif
|
|
if (state == NULL) {
|
|
return;
|
|
}
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_setspecific(tfork_global_key, NULL);
|
|
if (ret != 0) {
|
|
return;
|
|
}
|
|
#endif
|
|
anonymous_shared_free(state);
|
|
}
|
|
|
|
/**
|
|
* Only one thread at a time is allowed to handle SIGCHLD signals
|
|
**/
|
|
static int tfork_install_sigchld_handler(pid_t *pid)
|
|
{
|
|
int ret;
|
|
struct sigaction act;
|
|
sigset_t set;
|
|
|
|
ret = tfork_acquire_sighandling();
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
assert(signal_state.pid == NULL);
|
|
signal_state.pid = pid;
|
|
|
|
act = (struct sigaction) {
|
|
.sa_sigaction = tfork_sigchld_handler,
|
|
.sa_flags = SA_SIGINFO,
|
|
};
|
|
|
|
ret = sigaction(SIGCHLD, &act, &signal_state.oldact);
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
sigemptyset(&set);
|
|
sigaddset(&set, SIGCHLD);
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_sigmask(SIG_UNBLOCK, &set, &signal_state.oldset);
|
|
#else
|
|
ret = sigprocmask(SIG_UNBLOCK, &set, &signal_state.oldset);
|
|
#endif
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tfork_uninstall_sigchld_handler(void)
|
|
{
|
|
int ret;
|
|
|
|
signal_state.pid = NULL;
|
|
|
|
ret = sigaction(SIGCHLD, &signal_state.oldact, NULL);
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_sigmask(SIG_SETMASK, &signal_state.oldset, NULL);
|
|
#else
|
|
ret = sigprocmask(SIG_SETMASK, &signal_state.oldset, NULL);
|
|
#endif
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
ret = tfork_release_sighandling();
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void tfork_sigchld_handler(int signum, siginfo_t *si, void *p)
|
|
{
|
|
if ((signal_state.pid != NULL) &&
|
|
(*signal_state.pid != -1) &&
|
|
(si->si_pid == *signal_state.pid))
|
|
{
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Not our child, forward to old handler
|
|
*/
|
|
if (signal_state.oldact.sa_flags & SA_SIGINFO) {
|
|
signal_state.oldact.sa_sigaction(signum, si, p);
|
|
return;
|
|
}
|
|
|
|
if (signal_state.oldact.sa_handler == SIG_IGN) {
|
|
return;
|
|
}
|
|
if (signal_state.oldact.sa_handler == SIG_DFL) {
|
|
return;
|
|
}
|
|
signal_state.oldact.sa_handler(signum);
|
|
}
|
|
|
|
static pid_t tfork_start_waiter_and_worker(struct tfork_state *state,
|
|
int *_event_fd,
|
|
int *_status_fd)
|
|
{
|
|
int p[2];
|
|
int status_sp_caller_fd = -1;
|
|
int status_sp_waiter_fd = -1;
|
|
int event_pipe_caller_fd = -1;
|
|
int event_pipe_waiter_fd = -1;
|
|
int ready_pipe_caller_fd = -1;
|
|
int ready_pipe_worker_fd = -1;
|
|
ssize_t nwritten;
|
|
ssize_t nread;
|
|
pid_t pid;
|
|
int status;
|
|
int fd;
|
|
char c;
|
|
int ret;
|
|
|
|
*_event_fd = -1;
|
|
*_status_fd = -1;
|
|
|
|
if (state == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, p);
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
set_close_on_exec(p[0]);
|
|
set_close_on_exec(p[1]);
|
|
status_sp_caller_fd = p[0];
|
|
status_sp_waiter_fd = p[1];
|
|
|
|
ret = pipe(p);
|
|
if (ret != 0) {
|
|
close(status_sp_caller_fd);
|
|
close(status_sp_waiter_fd);
|
|
return -1;
|
|
}
|
|
set_close_on_exec(p[0]);
|
|
set_close_on_exec(p[1]);
|
|
event_pipe_caller_fd = p[0];
|
|
event_pipe_waiter_fd = p[1];
|
|
|
|
|
|
ret = pipe(p);
|
|
if (ret != 0) {
|
|
close(status_sp_caller_fd);
|
|
close(status_sp_waiter_fd);
|
|
close(event_pipe_caller_fd);
|
|
close(event_pipe_waiter_fd);
|
|
return -1;
|
|
}
|
|
set_close_on_exec(p[0]);
|
|
set_close_on_exec(p[1]);
|
|
ready_pipe_worker_fd = p[0];
|
|
ready_pipe_caller_fd = p[1];
|
|
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
close(status_sp_caller_fd);
|
|
close(status_sp_waiter_fd);
|
|
close(event_pipe_caller_fd);
|
|
close(event_pipe_waiter_fd);
|
|
close(ready_pipe_caller_fd);
|
|
close(ready_pipe_worker_fd);
|
|
return -1;
|
|
}
|
|
if (pid != 0) {
|
|
/* The caller */
|
|
|
|
/*
|
|
* In a threaded process there's no data race on
|
|
* state->waiter_pid as we're serializing globally via
|
|
* tfork_acquire_sighandling() and tfork_release_sighandling().
|
|
*/
|
|
TFORK_ANNOTATE_BENIGN_RACE(&state->waiter_pid);
|
|
|
|
state->waiter_pid = pid;
|
|
|
|
close(status_sp_waiter_fd);
|
|
close(event_pipe_waiter_fd);
|
|
close(ready_pipe_worker_fd);
|
|
|
|
set_blocking(event_pipe_caller_fd, false);
|
|
|
|
/*
|
|
* wait for the waiter to get ready.
|
|
*/
|
|
nread = sys_read(status_sp_caller_fd, &c, sizeof(char));
|
|
if (nread != sizeof(char)) {
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Notify the worker to start.
|
|
*/
|
|
nwritten = sys_write(ready_pipe_caller_fd,
|
|
&(char){0}, sizeof(char));
|
|
if (nwritten != sizeof(char)) {
|
|
close(ready_pipe_caller_fd);
|
|
return -1;
|
|
}
|
|
close(ready_pipe_caller_fd);
|
|
|
|
*_event_fd = event_pipe_caller_fd;
|
|
*_status_fd = status_sp_caller_fd;
|
|
|
|
return pid;
|
|
}
|
|
|
|
#ifndef HAVE_PTHREAD
|
|
/* cleanup sigchld_handler */
|
|
tfork_atfork_child();
|
|
#endif
|
|
|
|
/*
|
|
* The "waiter" child.
|
|
*/
|
|
setproctitle("tfork waiter process");
|
|
prctl_set_comment("tfork waiter");
|
|
CatchSignal(SIGCHLD, SIG_DFL);
|
|
|
|
close(status_sp_caller_fd);
|
|
close(event_pipe_caller_fd);
|
|
close(ready_pipe_caller_fd);
|
|
|
|
pid = fork();
|
|
if (pid == -1) {
|
|
state->waiter_errno = errno;
|
|
_exit(0);
|
|
}
|
|
if (pid == 0) {
|
|
/*
|
|
* The worker child.
|
|
*/
|
|
|
|
close(status_sp_waiter_fd);
|
|
close(event_pipe_waiter_fd);
|
|
|
|
/*
|
|
* Wait for the caller to give us a go!
|
|
*/
|
|
nread = sys_read(ready_pipe_worker_fd, &c, sizeof(char));
|
|
if (nread != sizeof(char)) {
|
|
_exit(1);
|
|
}
|
|
close(ready_pipe_worker_fd);
|
|
|
|
return 0;
|
|
}
|
|
state->worker_pid = pid;
|
|
setproctitle("tfork waiter process(%d)", pid);
|
|
prctl_set_comment("tfork(%d)", pid);
|
|
|
|
close(ready_pipe_worker_fd);
|
|
|
|
/*
|
|
* We're going to stay around until child2 exits, so lets close all fds
|
|
* other then the pipe fd we may have inherited from the caller.
|
|
*
|
|
* Dup event_sp_waiter_fd and status_sp_waiter_fd onto fds 0 and 1 so we
|
|
* can then call closefrom(2).
|
|
*/
|
|
if (event_pipe_waiter_fd > 0) {
|
|
int dup_fd = 0;
|
|
|
|
if (status_sp_waiter_fd == 0) {
|
|
dup_fd = 1;
|
|
}
|
|
|
|
do {
|
|
fd = dup2(event_pipe_waiter_fd, dup_fd);
|
|
} while ((fd == -1) && (errno == EINTR));
|
|
if (fd == -1) {
|
|
state->waiter_errno = errno;
|
|
kill(state->worker_pid, SIGKILL);
|
|
state->worker_pid = -1;
|
|
_exit(1);
|
|
}
|
|
event_pipe_waiter_fd = fd;
|
|
}
|
|
|
|
if (status_sp_waiter_fd > 1) {
|
|
do {
|
|
fd = dup2(status_sp_waiter_fd, 1);
|
|
} while ((fd == -1) && (errno == EINTR));
|
|
if (fd == -1) {
|
|
state->waiter_errno = errno;
|
|
kill(state->worker_pid, SIGKILL);
|
|
state->worker_pid = -1;
|
|
_exit(1);
|
|
}
|
|
status_sp_waiter_fd = fd;
|
|
}
|
|
|
|
closefrom(2);
|
|
|
|
/* Tell the caller we're ready */
|
|
nwritten = sys_write(status_sp_waiter_fd, &(char){0}, sizeof(char));
|
|
if (nwritten != sizeof(char)) {
|
|
_exit(1);
|
|
}
|
|
|
|
tfork_global_free();
|
|
state = NULL;
|
|
|
|
do {
|
|
ret = waitpid(pid, &status, 0);
|
|
} while ((ret == -1) && (errno == EINTR));
|
|
if (ret == -1) {
|
|
status = errno;
|
|
kill(pid, SIGKILL);
|
|
}
|
|
|
|
/*
|
|
* This writes the worker child exit status via our internal socketpair
|
|
* so the tfork_status() implementation can read it from its end.
|
|
*/
|
|
nwritten = sys_write(status_sp_waiter_fd, &status, sizeof(status));
|
|
if (nwritten == -1) {
|
|
if (errno != EPIPE && errno != ECONNRESET) {
|
|
_exit(errno);
|
|
}
|
|
/*
|
|
* The caller exitted and didn't call tfork_status().
|
|
*/
|
|
_exit(0);
|
|
}
|
|
if (nwritten != sizeof(status)) {
|
|
_exit(1);
|
|
}
|
|
|
|
/*
|
|
* This write to the event_fd returned by tfork_event_fd() and notifies
|
|
* the caller that the worker child is done and he may now call
|
|
* tfork_status().
|
|
*/
|
|
nwritten = sys_write(event_pipe_waiter_fd, &(char){0}, sizeof(char));
|
|
if (nwritten != sizeof(char)) {
|
|
_exit(1);
|
|
}
|
|
|
|
/*
|
|
* Wait for our parent (the process that called tfork_create()) to
|
|
* close() the socketpair fd in tfork_status().
|
|
*
|
|
* Again, the caller might have exitted without calling tfork_status().
|
|
*/
|
|
nread = sys_read(status_sp_waiter_fd, &c, 1);
|
|
if (nread == -1) {
|
|
if (errno == EPIPE || errno == ECONNRESET) {
|
|
_exit(0);
|
|
}
|
|
_exit(errno);
|
|
}
|
|
if (nread != 1) {
|
|
_exit(255);
|
|
}
|
|
|
|
_exit(0);
|
|
}
|
|
|
|
static int tfork_create_reap_waiter(pid_t waiter_pid)
|
|
{
|
|
pid_t pid;
|
|
int waiter_status;
|
|
|
|
if (waiter_pid == -1) {
|
|
return 0;
|
|
}
|
|
|
|
kill(waiter_pid, SIGKILL);
|
|
|
|
do {
|
|
pid = waitpid(waiter_pid, &waiter_status, 0);
|
|
} while ((pid == -1) && (errno == EINTR));
|
|
assert(pid == waiter_pid);
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct tfork *tfork_create(void)
|
|
{
|
|
struct tfork_state *state = NULL;
|
|
struct tfork *t = NULL;
|
|
pid_t pid;
|
|
int saved_errno;
|
|
int ret = 0;
|
|
|
|
#ifdef HAVE_PTHREAD
|
|
ret = pthread_once(&tfork_global_is_initialized,
|
|
tfork_global_initialize);
|
|
if (ret != 0) {
|
|
return NULL;
|
|
}
|
|
#else
|
|
tfork_global_initialize();
|
|
#endif
|
|
|
|
state = tfork_global_get();
|
|
if (state == NULL) {
|
|
return NULL;
|
|
}
|
|
*state = (struct tfork_state) {
|
|
.waiter_pid = -1,
|
|
.waiter_errno = ECANCELED,
|
|
.worker_pid = -1,
|
|
};
|
|
|
|
t = malloc(sizeof(struct tfork));
|
|
if (t == NULL) {
|
|
ret = -1;
|
|
goto cleanup;
|
|
}
|
|
|
|
*t = (struct tfork) {
|
|
.event_fd = -1,
|
|
.status_fd = -1,
|
|
.waiter_pid = -1,
|
|
.worker_pid = -1,
|
|
};
|
|
|
|
ret = tfork_install_sigchld_handler(&state->waiter_pid);
|
|
if (ret != 0) {
|
|
goto cleanup;
|
|
}
|
|
|
|
pid = tfork_start_waiter_and_worker(state,
|
|
&t->event_fd,
|
|
&t->status_fd);
|
|
if (pid == -1) {
|
|
ret = -1;
|
|
goto cleanup;
|
|
}
|
|
if (pid == 0) {
|
|
/* In the worker */
|
|
tfork_global_free();
|
|
t->worker_pid = 0;
|
|
return t;
|
|
}
|
|
|
|
/*
|
|
* In a threaded process there's no data race on t->waiter_pid as
|
|
* we're serializing globally via tfork_acquire_sighandling() and
|
|
* tfork_release_sighandling().
|
|
*/
|
|
TFORK_ANNOTATE_BENIGN_RACE(&t->waiter_pid);
|
|
|
|
t->waiter_pid = pid;
|
|
t->worker_pid = state->worker_pid;
|
|
|
|
cleanup:
|
|
if (ret == -1) {
|
|
saved_errno = errno;
|
|
|
|
if (t != NULL) {
|
|
if (t->status_fd != -1) {
|
|
close(t->status_fd);
|
|
}
|
|
if (t->event_fd != -1) {
|
|
close(t->event_fd);
|
|
}
|
|
|
|
ret = tfork_create_reap_waiter(state->waiter_pid);
|
|
assert(ret == 0);
|
|
|
|
free(t);
|
|
t = NULL;
|
|
}
|
|
}
|
|
|
|
ret = tfork_uninstall_sigchld_handler();
|
|
assert(ret == 0);
|
|
|
|
tfork_global_free();
|
|
|
|
if (ret == -1) {
|
|
errno = saved_errno;
|
|
}
|
|
return t;
|
|
}
|
|
|
|
pid_t tfork_child_pid(const struct tfork *t)
|
|
{
|
|
return t->worker_pid;
|
|
}
|
|
|
|
int tfork_event_fd(struct tfork *t)
|
|
{
|
|
int fd = t->event_fd;
|
|
|
|
assert(t->event_fd != -1);
|
|
t->event_fd = -1;
|
|
|
|
return fd;
|
|
}
|
|
|
|
int tfork_status(struct tfork **_t, bool wait)
|
|
{
|
|
struct tfork *t = *_t;
|
|
int status;
|
|
ssize_t nread;
|
|
int waiter_status;
|
|
pid_t pid;
|
|
int ret;
|
|
|
|
if (t == NULL) {
|
|
return -1;
|
|
}
|
|
|
|
if (wait) {
|
|
set_blocking(t->status_fd, true);
|
|
|
|
nread = sys_read(t->status_fd, &status, sizeof(int));
|
|
} else {
|
|
set_blocking(t->status_fd, false);
|
|
|
|
nread = read(t->status_fd, &status, sizeof(int));
|
|
if ((nread == -1) &&
|
|
((errno == EAGAIN) || (errno == EWOULDBLOCK) || errno == EINTR)) {
|
|
errno = EAGAIN;
|
|
return -1;
|
|
}
|
|
}
|
|
if (nread != sizeof(int)) {
|
|
return -1;
|
|
}
|
|
|
|
ret = tfork_install_sigchld_handler(&t->waiter_pid);
|
|
if (ret != 0) {
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* This triggers process exit in the waiter.
|
|
* We write to the fd as well as closing it, as any tforked sibling
|
|
* processes will also have the writable end of this socket open.
|
|
*
|
|
*/
|
|
{
|
|
size_t nwritten;
|
|
nwritten = sys_write(t->status_fd, &(char){0}, sizeof(char));
|
|
if (nwritten != sizeof(char)) {
|
|
close(t->status_fd);
|
|
return -1;
|
|
}
|
|
}
|
|
close(t->status_fd);
|
|
|
|
do {
|
|
pid = waitpid(t->waiter_pid, &waiter_status, 0);
|
|
} while ((pid == -1) && (errno == EINTR));
|
|
assert(pid == t->waiter_pid);
|
|
|
|
if (t->event_fd != -1) {
|
|
close(t->event_fd);
|
|
t->event_fd = -1;
|
|
}
|
|
|
|
free(t);
|
|
t = NULL;
|
|
*_t = NULL;
|
|
|
|
ret = tfork_uninstall_sigchld_handler();
|
|
assert(ret == 0);
|
|
|
|
return status;
|
|
}
|
|
|
|
int tfork_destroy(struct tfork **_t)
|
|
{
|
|
struct tfork *t = *_t;
|
|
int ret;
|
|
|
|
if (t == NULL) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
kill(t->worker_pid, SIGKILL);
|
|
|
|
ret = tfork_status(_t, true);
|
|
if (ret == -1) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|