diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile.am | 9 | ||||
-rw-r--r-- | src/shared/barrier.c | 440 | ||||
-rw-r--r-- | src/shared/barrier.h | 92 | ||||
-rw-r--r-- | src/test/test-barrier.c | 460 |
5 files changed, 1002 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore index bf53064105..48606313cc 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,7 @@ /tags /test-architecture /test-async +/test-barrier /test-boot-timestamp /test-bus-chat /test-bus-cleanup diff --git a/Makefile.am b/Makefile.am index 7025137683..f0d80ba748 100644 --- a/Makefile.am +++ b/Makefile.am @@ -841,6 +841,8 @@ libsystemd_shared_la_SOURCES = \ src/shared/login-shared.h \ src/shared/ring.c \ src/shared/ring.h \ + src/shared/barrier.c \ + src/shared/barrier.h \ src/shared/async.c \ src/shared/async.h \ src/shared/eventfd-util.c \ @@ -1251,6 +1253,7 @@ tests += \ test-ellipsize \ test-util \ test-ring \ + test-barrier \ test-tmpfiles \ test-namespace \ test-date \ @@ -1421,6 +1424,12 @@ test_ring_SOURCES = \ test_ring_LDADD = \ libsystemd-core.la +test_barrier_SOURCES = \ + src/test/test-barrier.c + +test_barrier_LDADD = \ + libsystemd-core.la + test_tmpfiles_SOURCES = \ src/test/test-tmpfiles.c diff --git a/src/shared/barrier.c b/src/shared/barrier.c new file mode 100644 index 0000000000..c198329cb0 --- /dev/null +++ b/src/shared/barrier.c @@ -0,0 +1,440 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann <dh.herrmann@gmail.com> + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <poll.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/eventfd.h> +#include <sys/types.h> +#include <unistd.h> + +#include "barrier.h" +#include "macro.h" +#include "util.h" + +/** + * Barriers + * This barrier implementation provides a simple synchronization method based + * on file-descriptors that can safely be used between threads and processes. A + * barrier object contains 2 shared counters based on eventfd. Both processes + * can now place barriers and wait for the other end to reach a random or + * specific barrier. + * Barriers are numbered, so you can either wait for the other end to reach any + * barrier or the last barrier that you placed. This way, you can use barriers + * for one-way *and* full synchronization. Note that even-though barriers are + * numbered, these numbers are internal and recycled once both sides reached the + * same barrier (implemented as a simple signed counter). It is thus not + * possible to address barriers by their ID. + * + * Barrier-API: Both ends can place as many barriers via barrier_place() as + * they want and each pair of barriers on both sides will be implicitly linked. + * Each side can use the barrier_wait/sync_*() family of calls to wait for the + * other side to place a specific barrier. barrier_wait_next() waits until the + * other side calls barrier_place(). No links between the barriers are + * considered and this simply serves as most basic asynchronous barrier. + * barrier_sync_next() is like barrier_wait_next() and waits for the other side + * to place their next barrier via barrier_place(). However, it only waits for + * barriers that are linked to a barrier we already placed. If the other side + * already placed more barriers than we did, barrier_sync_next() returns + * immediately. + * barrier_sync() extends barrier_sync_next() and waits until the other end + * placed as many barriers via barrier_place() as we did. If they already placed + * as many as we did (or more), it returns immediately. + * + * Additionally to basic barriers, an abortion event is available. + * barrier_abort() places an abortion event that cannot be undone. An abortion + * immediately cancels all placed barriers and replaces them. Any running and + * following wait/sync call besides barrier_wait_abortion() will immediately + * return false on both sides (otherwise, they always return true). + * barrier_abort() can be called multiple times on both ends and will be a + * no-op if already called on this side. + * barrier_wait_abortion() can be used to wait for the other side to call + * barrier_abort() and is the only wait/sync call that does not return + * immediately if we aborted outself. It only returns once the other side + * called barrier_abort(). + * + * Barriers can be used for in-process and inter-process synchronization. + * However, for in-process synchronization you could just use mutexes. + * Therefore, main target is IPC and we require both sides to *not* share the FD + * table. If that's given, barriers provide target tracking: If the remote side + * exit()s, an abortion event is implicitly queued on the other side. This way, + * a sync/wait call will be woken up if the remote side crashed or exited + * unexpectedly. However, note that these abortion events are only queued if the + * barrier-queue has been drained. Therefore, it is safe to place a barrier and + * exit. The other side can safely wait on the barrier even though the exit + * queued an abortion event. Usually, the abortion event would overwrite the + * barrier, however, that's not true for exit-abortion events. Those are only + * queued if the barrier-queue is drained (thus, the receiving side has placed + * more barriers than the remote side). + */ + +/** + * barrier_init() - Initialize a barrier object + * @obj: barrier to initialize + * + * This initializes a barrier object. The caller is responsible of allocating + * the memory and keeping it valid. The memory does not have to be zeroed + * beforehand. + * Two eventfd objects are allocated for each barrier. If allocation fails, an + * error is returned. + * + * If this function fails, the barrier is reset to an invalid state so it is + * safe to call barrier_destroy() on the object regardless whether the + * initialization succeeded or not. + * + * The caller is responsible to destroy the object via barrier_destroy() before + * releasing the underlying memory. + * + * Returns: 0 on success, negative error code on failure. + */ +int barrier_init(Barrier *obj) { + _cleanup_(barrier_destroy) Barrier b = { }; + int r; + + assert_return(obj, -EINVAL); + + b.me = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (b.me < 0) + return -errno; + + b.them = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (b.them < 0) + return -errno; + + r = pipe2(b.pipe, O_CLOEXEC | O_NONBLOCK); + if (r < 0) + return -errno; + + memcpy(obj, &b, sizeof(b)); + zero(b); + return 0; +} + +/** + * barrier_destroy() - Destroy a barrier object + * @b: barrier to destroy or NULL + * + * This destroys a barrier object that has previously been initialized via + * barrier_init(). The object is released and reset to invalid state. + * Therefore, it is safe to call barrier_destroy() multiple times or even if + * barrier_init() failed. However, you must not call barrier_destroy() if you + * never called barrier_init() on the object before. + * + * It is safe to initialize a barrier via zero() / memset(.., 0, ...). Even + * though it has embedded FDs, barrier_destroy() can deal with zeroed objects + * just fine. + * + * If @b is NULL, this is a no-op. + */ +void barrier_destroy(Barrier *b) { + if (!b) + return; + + /* @me and @them cannot be both FD 0. Lets be pedantic and check the + * pipes and barriers, too. If all are 0, the object was zero()ed and + * is invalid. This allows users to use zero(barrier) to reset the + * backing memory. */ + if (b->me == 0 && + b->them == 0 && + b->pipe[0] == 0 && + b->pipe[1] == 0 && + b->barriers == 0) + return; + + b->me = safe_close(b->me); + b->them = safe_close(b->them); + b->pipe[0] = safe_close(b->pipe[0]); + b->pipe[1] = safe_close(b->pipe[1]); + b->barriers = 0; +} + +/** + * barrier_set_role() - Set the local role of the barrier + * @b: barrier to operate on + * @role: role to set on the barrier + * + * This sets the roles on a barrier object. This is needed to know which + * side of the barrier you're on. Usually, the parent creates the barrier via + * barrier_init() and then calls fork() or clone(). Therefore, the FDs are + * duplicated and the child retains the same barrier object. + * + * Both sides need to call barrier_set_role() after fork() or clone() are done. + * If this is not done, barriers will not work correctly. + * + * Note that barriers could be supported without fork() or clone(). However, + * this is currently not needed so it hasn't been implemented. + */ +void barrier_set_role(Barrier *b, unsigned int role) { + int fd; + + assert(b); + assert(role == BARRIER_PARENT || role == BARRIER_CHILD); + /* make sure this is only called once */ + assert(b->pipe[1] >= 0 && b->pipe[1] >= 0); + + if (role == BARRIER_PARENT) { + b->pipe[1] = safe_close(b->pipe[1]); + } else { + b->pipe[0] = safe_close(b->pipe[0]); + + /* swap me/them for children */ + fd = b->me; + b->me = b->them; + b->them = fd; + } +} + +/* places barrier; returns false if we aborted, otherwise true */ +static bool barrier_write(Barrier *b, uint64_t buf) { + ssize_t len; + + /* prevent new sync-points if we already aborted */ + if (barrier_i_aborted(b)) + return false; + + do { + len = write(b->me, &buf, sizeof(buf)); + } while (len < 0 && (errno == EAGAIN || errno == EINTR)); + + if (len != sizeof(buf)) + goto error; + + /* lock if we aborted */ + if (buf >= (uint64_t)BARRIER_ABORTION) { + if (barrier_they_aborted(b)) + b->barriers = BARRIER_WE_ABORTED; + else + b->barriers = BARRIER_I_ABORTED; + } else if (!barrier_is_aborted(b)) { + b->barriers += buf; + } + + return !barrier_i_aborted(b); + +error: + /* If there is an unexpected error, we have to make this fatal. There + * is no way we can recover from sync-errors. Therefore, we close the + * pipe-ends and treat this as abortion. The other end will notice the + * pipe-close and treat it as abortion, too. */ + + b->pipe[0] = safe_close(b->pipe[0]); + b->pipe[1] = safe_close(b->pipe[1]); + b->barriers = BARRIER_WE_ABORTED; + return false; +} + +/* waits for barriers; returns false if they aborted, otherwise true */ +static bool barrier_read(Barrier *b, int64_t comp) { + uint64_t buf; + ssize_t len; + struct pollfd pfd[2] = { }; + int r; + + if (barrier_they_aborted(b)) + return false; + + while (b->barriers > comp) { + pfd[0].fd = (b->pipe[0] >= 0) ? b->pipe[0] : b->pipe[1]; + pfd[0].events = POLLHUP; + pfd[0].revents = 0; + pfd[1].fd = b->them; + pfd[1].events = POLLIN; + pfd[1].revents = 0; + + r = poll(pfd, 2, -1); + if (r < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + else if (r < 0) + goto error; + + if (pfd[1].revents) { + /* events on @them signal us new data */ + len = read(b->them, &buf, sizeof(buf)); + if (len < 0 && (errno == EAGAIN || errno == EINTR)) + continue; + + if (len != sizeof(buf)) + goto error; + } else if (pfd[0].revents & (POLLHUP | POLLERR | POLLNVAL)) { + /* POLLHUP on the pipe tells us the other side exited. + * We treat this as implicit abortion. But we only + * handle it if there's no event on the eventfd. This + * guarantees that exit-abortions do not overwrite real + * barriers. */ + buf = BARRIER_ABORTION; + } + + /* lock if they aborted */ + if (buf >= (uint64_t)BARRIER_ABORTION) { + if (barrier_i_aborted(b)) + b->barriers = BARRIER_WE_ABORTED; + else + b->barriers = BARRIER_THEY_ABORTED; + } else if (!barrier_is_aborted(b)) { + b->barriers -= buf; + } + } + + return !barrier_they_aborted(b); + +error: + /* If there is an unexpected error, we have to make this fatal. There + * is no way we can recover from sync-errors. Therefore, we close the + * pipe-ends and treat this as abortion. The other end will notice the + * pipe-close and treat it as abortion, too. */ + + b->pipe[0] = safe_close(b->pipe[0]); + b->pipe[1] = safe_close(b->pipe[1]); + b->barriers = BARRIER_WE_ABORTED; + return false; +} + +/** + * barrier_place() - Place a new barrier + * @b: barrier object + * + * This places a new barrier on the barrier object. If either side already + * aborted, this is a no-op and returns "false". Otherwise, the barrier is + * placed and this returns "true". + * + * Returns: true if barrier was placed, false if either side aborted. + */ +bool barrier_place(Barrier *b) { + assert(b); + + if (barrier_is_aborted(b)) + return false; + + barrier_write(b, BARRIER_SINGLE); + return true; +} + +/** + * barrier_abort() - Abort the synchronization + * @b: barrier object to abort + * + * This aborts the barrier-synchronization. If barrier_abort() was already + * called on this side, this is a no-op. Otherwise, the barrier is put into the + * ABORT-state and will stay there. The other side is notified about the + * abortion. Any following attempt to place normal barriers or to wait on normal + * barriers will return immediately as "false". + * + * You can wait for the other side to call barrier_abort(), too. Use + * barrier_wait_abortion() for that. + * + * Returns: false if the other side already aborted, true otherwise. + */ +bool barrier_abort(Barrier *b) { + assert(b); + + barrier_write(b, BARRIER_ABORTION); + return !barrier_they_aborted(b); +} + +/** + * barrier_wait_next() - Wait for the next barrier of the other side + * @b: barrier to operate on + * + * This waits until the other side places its next barrier. This is independent + * of any barrier-links and just waits for any next barrier of the other side. + * + * If either side aborted, this returns false. + * + * Returns: false if either side aborted, true otherwise. + */ +bool barrier_wait_next(Barrier *b) { + assert(b); + + if (barrier_is_aborted(b)) + return false; + + barrier_read(b, b->barriers - 1); + return !barrier_is_aborted(b); +} + +/** + * barrier_wait_abortion() - Wait for the other side to abort + * @b: barrier to operate on + * + * This waits until the other side called barrier_abort(). This can be called + * regardless whether the local side already called barrier_abort() or not. + * + * If the other side has already aborted, this returns immediately. + * + * Returns: false if the local side aborted, true otherwise. + */ +bool barrier_wait_abortion(Barrier *b) { + assert(b); + + barrier_read(b, BARRIER_THEY_ABORTED); + return !barrier_i_aborted(b); +} + +/** + * barrier_sync_next() - Wait for the other side to place a next linked barrier + * @b: barrier to operate on + * + * This is like barrier_wait_next() and waits for the other side to call + * barrier_place(). However, this only waits for linked barriers. That means, if + * the other side already placed more barriers than (or as much as) we did, this + * returns immediately instead of waiting. + * + * If either side aborted, this returns false. + * + * Returns: false if either side aborted, true otherwise. + */ +bool barrier_sync_next(Barrier *b) { + assert(b); + + if (barrier_is_aborted(b)) + return false; + + barrier_read(b, MAX((int64_t)0, b->barriers - 1)); + return !barrier_is_aborted(b); +} + +/** + * barrier_sync() - Wait for the other side to place as many barriers as we did + * @b: barrier to operate on + * + * This is like barrier_sync_next() but waits for the other side to call + * barrier_place() as often as we did (in total). If they already placed as much + * as we did (or more), this returns immediately instead of waiting. + * + * If either side aborted, this returns false. + * + * Returns: false if either side aborted, true otherwise. + */ +bool barrier_sync(Barrier *b) { + assert(b); + + if (barrier_is_aborted(b)) + return false; + + barrier_read(b, 0); + return !barrier_is_aborted(b); +} diff --git a/src/shared/barrier.h b/src/shared/barrier.h new file mode 100644 index 0000000000..7f76ec7910 --- /dev/null +++ b/src/shared/barrier.h @@ -0,0 +1,92 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#pragma once + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann <dh.herrmann@gmail.com> + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +#include <errno.h> +#include <inttypes.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> + +#include "macro.h" +#include "util.h" + +/* See source file for an API description. */ + +typedef struct Barrier Barrier; + +enum { + BARRIER_SINGLE = 1LL, + BARRIER_ABORTION = INT64_MAX, + + /* bias values to store state; keep @WE < @THEY < @I */ + BARRIER_BIAS = INT64_MIN, + BARRIER_WE_ABORTED = BARRIER_BIAS + 1LL, + BARRIER_THEY_ABORTED = BARRIER_BIAS + 2LL, + BARRIER_I_ABORTED = BARRIER_BIAS + 3LL, +}; + +enum { + BARRIER_PARENT, + BARRIER_CHILD, +}; + +struct Barrier { + int me; + int them; + int pipe[2]; + int64_t barriers; +}; + +int barrier_init(Barrier *obj); +void barrier_destroy(Barrier *b); + +void barrier_set_role(Barrier *b, unsigned int role); + +bool barrier_place(Barrier *b); +bool barrier_abort(Barrier *b); + +bool barrier_wait_next(Barrier *b); +bool barrier_wait_abortion(Barrier *b); +bool barrier_sync_next(Barrier *b); +bool barrier_sync(Barrier *b); + +static inline bool barrier_i_aborted(Barrier *b) { + return b->barriers == BARRIER_I_ABORTED || b->barriers == BARRIER_WE_ABORTED; +} + +static inline bool barrier_they_aborted(Barrier *b) { + return b->barriers == BARRIER_THEY_ABORTED || b->barriers == BARRIER_WE_ABORTED; +} + +static inline bool barrier_we_aborted(Barrier *b) { + return b->barriers == BARRIER_WE_ABORTED; +} + +static inline bool barrier_is_aborted(Barrier *b) { + return b->barriers == BARRIER_I_ABORTED || b->barriers == BARRIER_THEY_ABORTED || b->barriers == BARRIER_WE_ABORTED; +} + +static inline bool barrier_place_and_sync(Barrier *b) { + barrier_place(b); + return barrier_sync(b); +} diff --git a/src/test/test-barrier.c b/src/test/test-barrier.c new file mode 100644 index 0000000000..640e508679 --- /dev/null +++ b/src/test/test-barrier.c @@ -0,0 +1,460 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann <dh.herrmann@gmail.com> + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see <http://www.gnu.org/licenses/>. +***/ + +/* + * IPC barrier tests + * These tests verify the correct behavior of the IPC Barrier implementation. + * Note that the tests use alarm-timers to verify dead-locks and timeouts. These + * might not work on slow machines where 20ms are too short to perform specific + * operations (though, very unlikely). In case that turns out true, we have to + * increase it at the slightly cost of lengthen test-duration on other machines. + */ + +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <sys/time.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "barrier.h" +#include "def.h" +#include "util.h" + +/* 20ms to test deadlocks; All timings use multiples of this constant as + * alarm/sleep timers. If this timeout is too small for slow machines to perform + * the requested operations, we have to increase it. On an i7 this works fine + * with 1ms base-time, so 20ms should be just fine for everyone. */ +#define BASE_TIME 20 + +static void malarm(unsigned long msecs) { + struct itimerval v = { }; + + timeval_store(&v.it_value, msecs * USEC_PER_MSEC); + assert_se(setitimer(ITIMER_REAL, &v, NULL) >= 0); +} + +static void msleep(unsigned long msecs) { + assert_se(msecs < MSEC_PER_SEC); + usleep(msecs * USEC_PER_MSEC); +} + +#define TEST_BARRIER(_FUNCTION, _CHILD_CODE, _WAIT_CHILD, _PARENT_CODE, _WAIT_PARENT) \ + static void _FUNCTION(void) { \ + Barrier b; \ + pid_t pid1, pid2; \ + \ + assert_se(barrier_init(&b) >= 0); \ + \ + pid1 = fork(); \ + assert_se(pid1 >= 0); \ + if (pid1 == 0) { \ + barrier_set_role(&b, BARRIER_CHILD); \ + { _CHILD_CODE; } \ + exit(42); \ + } \ + \ + pid2 = fork(); \ + assert_se(pid2 >= 0); \ + if (pid2 == 0) { \ + barrier_set_role(&b, BARRIER_PARENT); \ + { _PARENT_CODE; } \ + exit(42); \ + } \ + \ + barrier_destroy(&b); \ + malarm(999); \ + { _WAIT_CHILD; } \ + { _WAIT_PARENT; } \ + malarm(0); \ + } + +#define TEST_BARRIER_WAIT_SUCCESS(_pid) \ + ({ \ + int pidr, status; \ + pidr = waitpid(_pid, &status, 0); \ + assert_se(pidr == _pid); \ + assert_se(WIFEXITED(status)); \ + assert_se(WEXITSTATUS(status) == 42); \ + }) + +#define TEST_BARRIER_WAIT_ALARM(_pid) \ + ({ \ + int pidr, status; \ + pidr = waitpid(_pid, &status, 0); \ + assert_se(pidr == _pid); \ + assert_se(WIFSIGNALED(status)); \ + assert_se(WTERMSIG(status) == SIGALRM); \ + }) + +/* + * Test basic sync points + * This places a barrier in both processes and waits synchronously for them. + * The timeout makes sure the sync works as expected. The msleep() on one side + * makes sure the exit of the parent does not overwrite previous barriers. Due + * to the msleep(), we know that the parent already exited, thus there's a + * pending HUP on the pipe. However, the barrier_sync() prefers reads on the + * eventfd, thus we can safely wait on the barrier. + */ +TEST_BARRIER(test_barrier_sync, + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + msleep(BASE_TIME * 2); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test wait_next() + * This places a barrier in the parent and syncs on it. The child sleeps while + * the parent places the barrier and then waits for a barrier. The wait will + * succeed as the child hasn't read the parent's barrier, yet. The following + * barrier and sync synchronize the exit. + */ +TEST_BARRIER(test_barrier_wait_next, + ({ + msleep(100); + malarm(BASE_TIME * 10); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(400); + assert_se(barrier_place(&b)); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test wait_next() multiple times + * This places two barriers in the parent and waits for the child to exit. The + * child sleeps 20ms so both barriers _should_ be in place. It then waits for + * the parent to place the next barrier twice. The first call will fetch both + * barriers and return. However, the second call will stall as the parent does + * not place a 3rd barrier (the sleep caught two barriers). wait_next() is does + * not look at barrier-links so this stall is expected. Thus this test times + * out. + */ +TEST_BARRIER(test_barrier_wait_next_twice, + ({ + msleep(BASE_TIME); + malarm(BASE_TIME); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_wait_next(&b)); + assert_se(0); + }), + TEST_BARRIER_WAIT_ALARM(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test wait_next() with local barriers + * This is the same as test_barrier_wait_next_twice, but places local barriers + * between both waits. This does not have any effect on the wait so it times out + * like the other test. + */ +TEST_BARRIER(test_barrier_wait_next_twice_local, + ({ + msleep(BASE_TIME); + malarm(BASE_TIME); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_wait_next(&b)); + assert_se(0); + }), + TEST_BARRIER_WAIT_ALARM(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test wait_next() with sync_next() + * This is again the same as test_barrier_wait_next_twice but uses a + * synced wait as the second wait. This works just fine because the local state + * has no barriers placed, therefore, the remote is always in sync. + */ +TEST_BARRIER(test_barrier_wait_next_twice_sync, + ({ + msleep(BASE_TIME); + malarm(BASE_TIME); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_sync_next(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test wait_next() with sync_next() and local barriers + * This is again the same as test_barrier_wait_next_twice_local but uses a + * synced wait as the second wait. This works just fine because the local state + * is in sync with the remote. + */ +TEST_BARRIER(test_barrier_wait_next_twice_local_sync, + ({ + msleep(BASE_TIME); + malarm(BASE_TIME); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_sync_next(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test sync_next() and sync() + * This tests sync_*() synchronizations and makes sure they work fine if the + * local state is behind the remote state. + */ +TEST_BARRIER(test_barrier_sync_next, + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_sync_next(&b)); + assert_se(barrier_sync(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_sync_next(&b)); + assert_se(barrier_sync_next(&b)); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME * 10); + msleep(BASE_TIME); + assert_se(barrier_place(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test sync_next() and sync() with local barriers + * This tests timeouts if sync_*() is used if local barriers are placed but the + * remote didn't place any. + */ +TEST_BARRIER(test_barrier_sync_next_local, + ({ + malarm(BASE_TIME); + assert_se(barrier_place(&b)); + assert_se(barrier_sync_next(&b)); + assert_se(0); + }), + TEST_BARRIER_WAIT_ALARM(pid1), + ({ + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test sync_next() and sync() with local barriers and abortion + * This is the same as test_barrier_sync_next_local but aborts the sync in the + * parent. Therefore, the sync_next() succeeds just fine due to the abortion. + */ +TEST_BARRIER(test_barrier_sync_next_local_abort, + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(!barrier_sync_next(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + assert_se(barrier_abort(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test matched wait_abortion() + * This runs wait_abortion() with remote abortion. + */ +TEST_BARRIER(test_barrier_wait_abortion, + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_wait_abortion(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + assert_se(barrier_abort(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test unmatched wait_abortion() + * This runs wait_abortion() without any remote abortion going on. It thus must + * timeout. + */ +TEST_BARRIER(test_barrier_wait_abortion_unmatched, + ({ + malarm(BASE_TIME); + assert_se(barrier_wait_abortion(&b)); + assert_se(0); + }), + TEST_BARRIER_WAIT_ALARM(pid1), + ({ + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test matched wait_abortion() with local abortion + * This runs wait_abortion() with local and remote abortion. + */ +TEST_BARRIER(test_barrier_wait_abortion_local, + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_abort(&b)); + assert_se(!barrier_wait_abortion(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + assert_se(barrier_abort(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test unmatched wait_abortion() with local abortion + * This runs wait_abortion() with only local abortion. This must time out. + */ +TEST_BARRIER(test_barrier_wait_abortion_local_unmatched, + ({ + malarm(BASE_TIME); + assert_se(barrier_abort(&b)); + assert_se(!barrier_wait_abortion(&b)); + assert_se(0); + }), + TEST_BARRIER_WAIT_ALARM(pid1), + ({ + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test child exit + * Place barrier and sync with the child. The child only exits()s, which should + * cause an implicit abortion and wake the parent. + */ +TEST_BARRIER(test_barrier_exit, + ({ + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME * 10); + assert_se(barrier_place(&b)); + assert_se(!barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +/* + * Test child exit with sleep + * Same as test_barrier_exit but verifies the test really works due to the + * child-exit. We add a usleep() which triggers the alarm in the parent and + * causes the test to time out. + */ +TEST_BARRIER(test_barrier_no_exit, + ({ + msleep(BASE_TIME * 2); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + malarm(BASE_TIME); + assert_se(barrier_place(&b)); + assert_se(!barrier_sync(&b)); + }), + TEST_BARRIER_WAIT_ALARM(pid2)); + +/* + * Test pending exit against sync + * The parent places a barrier *and* exits. The 20ms wait in the child + * guarantees both are pending. However, our logic prefers pending barriers over + * pending exit-abortions (unlike normal abortions), thus the wait_next() must + * succeed, same for the sync_next() as our local barrier-count is smaller than + * the remote. Once we place a barrier our count is equal, so the sync still + * succeeds. Only if we place one more barrier, we're ahead of the remote, thus + * we will fail due to HUP on the pipe. + */ +TEST_BARRIER(test_barrier_pending_exit, + ({ + malarm(BASE_TIME * 4); + msleep(BASE_TIME * 2); + assert_se(barrier_wait_next(&b)); + assert_se(barrier_sync_next(&b)); + assert_se(barrier_place(&b)); + assert_se(barrier_sync_next(&b)); + assert_se(barrier_place(&b)); + assert_se(!barrier_sync_next(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid1), + ({ + assert_se(barrier_place(&b)); + }), + TEST_BARRIER_WAIT_SUCCESS(pid2)); + +int main(int argc, char *argv[]) { + log_parse_environment(); + log_open(); + + test_barrier_sync(); + test_barrier_wait_next(); + test_barrier_wait_next_twice(); + test_barrier_wait_next_twice_sync(); + test_barrier_wait_next_twice_local(); + test_barrier_wait_next_twice_local_sync(); + test_barrier_sync_next(); + test_barrier_sync_next_local(); + test_barrier_sync_next_local_abort(); + test_barrier_wait_abortion(); + test_barrier_wait_abortion_unmatched(); + test_barrier_wait_abortion_local(); + test_barrier_wait_abortion_local_unmatched(); + test_barrier_exit(); + test_barrier_no_exit(); + test_barrier_pending_exit(); + + return 0; +} |