mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-24 05:14:52 +02:00
8174231: Factor out and share PlatformEvent and Parker code for POSIX systems
Reviewed-by: stuefe, rehn, dcubed
This commit is contained in:
parent
84ba3ee3b8
commit
3a8c8edb86
10 changed files with 647 additions and 1369 deletions
|
@ -145,7 +145,6 @@ bool os::Linux::_supports_fast_thread_cpu_time = false;
|
|||
uint32_t os::Linux::_os_version = 0;
|
||||
const char * os::Linux::_glibc_version = NULL;
|
||||
const char * os::Linux::_libpthread_version = NULL;
|
||||
pthread_condattr_t os::Linux::_condattr[1];
|
||||
|
||||
static jlong initial_time_count=0;
|
||||
|
||||
|
@ -161,9 +160,6 @@ static bool check_signals = true;
|
|||
static int SR_signum = SIGUSR2;
|
||||
sigset_t SR_sigset;
|
||||
|
||||
// Declarations
|
||||
static void unpackTime(timespec* absTime, bool isAbsolute, jlong time);
|
||||
|
||||
// utility functions
|
||||
|
||||
static int SR_initialize();
|
||||
|
@ -386,7 +382,7 @@ extern "C" void breakpoint() {
|
|||
// signal support
|
||||
|
||||
debug_only(static bool signal_sets_initialized = false);
|
||||
static sigset_t unblocked_sigs, vm_sigs, allowdebug_blocked_sigs;
|
||||
static sigset_t unblocked_sigs, vm_sigs;
|
||||
|
||||
bool os::Linux::is_sig_ignored(int sig) {
|
||||
struct sigaction oact;
|
||||
|
@ -417,7 +413,6 @@ void os::Linux::signal_sets_init() {
|
|||
// In reality, though, unblocking these signals is really a nop, since
|
||||
// these signals are not blocked by default.
|
||||
sigemptyset(&unblocked_sigs);
|
||||
sigemptyset(&allowdebug_blocked_sigs);
|
||||
sigaddset(&unblocked_sigs, SIGILL);
|
||||
sigaddset(&unblocked_sigs, SIGSEGV);
|
||||
sigaddset(&unblocked_sigs, SIGBUS);
|
||||
|
@ -430,15 +425,12 @@ void os::Linux::signal_sets_init() {
|
|||
if (!ReduceSignalUsage) {
|
||||
if (!os::Linux::is_sig_ignored(SHUTDOWN1_SIGNAL)) {
|
||||
sigaddset(&unblocked_sigs, SHUTDOWN1_SIGNAL);
|
||||
sigaddset(&allowdebug_blocked_sigs, SHUTDOWN1_SIGNAL);
|
||||
}
|
||||
if (!os::Linux::is_sig_ignored(SHUTDOWN2_SIGNAL)) {
|
||||
sigaddset(&unblocked_sigs, SHUTDOWN2_SIGNAL);
|
||||
sigaddset(&allowdebug_blocked_sigs, SHUTDOWN2_SIGNAL);
|
||||
}
|
||||
if (!os::Linux::is_sig_ignored(SHUTDOWN3_SIGNAL)) {
|
||||
sigaddset(&unblocked_sigs, SHUTDOWN3_SIGNAL);
|
||||
sigaddset(&allowdebug_blocked_sigs, SHUTDOWN3_SIGNAL);
|
||||
}
|
||||
}
|
||||
// Fill in signals that are blocked by all but the VM thread.
|
||||
|
@ -464,12 +456,6 @@ sigset_t* os::Linux::vm_signals() {
|
|||
return &vm_sigs;
|
||||
}
|
||||
|
||||
// These are signals that are blocked during cond_wait to allow debugger in
|
||||
sigset_t* os::Linux::allowdebug_blocked_signals() {
|
||||
assert(signal_sets_initialized, "Not initialized");
|
||||
return &allowdebug_blocked_sigs;
|
||||
}
|
||||
|
||||
void os::Linux::hotspot_sigmask(Thread* thread) {
|
||||
|
||||
//Save caller's signal mask before setting VM signal mask
|
||||
|
@ -4828,29 +4814,11 @@ void os::init(void) {
|
|||
Linux::clock_init();
|
||||
initial_time_count = javaTimeNanos();
|
||||
|
||||
// pthread_condattr initialization for monotonic clock
|
||||
int status;
|
||||
pthread_condattr_t* _condattr = os::Linux::condAttr();
|
||||
if ((status = pthread_condattr_init(_condattr)) != 0) {
|
||||
fatal("pthread_condattr_init: %s", os::strerror(status));
|
||||
}
|
||||
// Only set the clock if CLOCK_MONOTONIC is available
|
||||
if (os::supports_monotonic_clock()) {
|
||||
if ((status = pthread_condattr_setclock(_condattr, CLOCK_MONOTONIC)) != 0) {
|
||||
if (status == EINVAL) {
|
||||
warning("Unable to use monotonic clock with relative timed-waits" \
|
||||
" - changes to the time-of-day clock may have adverse affects");
|
||||
} else {
|
||||
fatal("pthread_condattr_setclock: %s", os::strerror(status));
|
||||
}
|
||||
}
|
||||
}
|
||||
// else it defaults to CLOCK_REALTIME
|
||||
|
||||
// retrieve entry point for pthread_setname_np
|
||||
Linux::_pthread_setname_np =
|
||||
(int(*)(pthread_t, const char*))dlsym(RTLD_DEFAULT, "pthread_setname_np");
|
||||
|
||||
os::Posix::init();
|
||||
}
|
||||
|
||||
// To install functions for atexit system call
|
||||
|
@ -4862,6 +4830,9 @@ extern "C" {
|
|||
|
||||
// this is called _after_ the global arguments have been parsed
|
||||
jint os::init_2(void) {
|
||||
|
||||
os::Posix::init_2();
|
||||
|
||||
Linux::fast_thread_clock_init();
|
||||
|
||||
// Allocate a single page and mark it as readable for safepoint polling
|
||||
|
@ -5582,406 +5553,6 @@ void os::pause() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// Refer to the comments in os_solaris.cpp park-unpark. The next two
|
||||
// comment paragraphs are worth repeating here:
|
||||
//
|
||||
// Assumption:
|
||||
// Only one parker can exist on an event, which is why we allocate
|
||||
// them per-thread. Multiple unparkers can coexist.
|
||||
//
|
||||
// _Event serves as a restricted-range semaphore.
|
||||
// -1 : thread is blocked, i.e. there is a waiter
|
||||
// 0 : neutral: thread is running or ready,
|
||||
// could have been signaled after a wait started
|
||||
// 1 : signaled - thread is running or ready
|
||||
//
|
||||
|
||||
// utility to compute the abstime argument to timedwait:
|
||||
// millis is the relative timeout time
|
||||
// abstime will be the absolute timeout time
|
||||
// TODO: replace compute_abstime() with unpackTime()
|
||||
|
||||
static struct timespec* compute_abstime(timespec* abstime, jlong millis) {
|
||||
if (millis < 0) millis = 0;
|
||||
|
||||
jlong seconds = millis / 1000;
|
||||
millis %= 1000;
|
||||
if (seconds > 50000000) { // see man cond_timedwait(3T)
|
||||
seconds = 50000000;
|
||||
}
|
||||
|
||||
if (os::supports_monotonic_clock()) {
|
||||
struct timespec now;
|
||||
int status = os::Linux::clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
assert_status(status == 0, status, "clock_gettime");
|
||||
abstime->tv_sec = now.tv_sec + seconds;
|
||||
long nanos = now.tv_nsec + millis * NANOSECS_PER_MILLISEC;
|
||||
if (nanos >= NANOSECS_PER_SEC) {
|
||||
abstime->tv_sec += 1;
|
||||
nanos -= NANOSECS_PER_SEC;
|
||||
}
|
||||
abstime->tv_nsec = nanos;
|
||||
} else {
|
||||
struct timeval now;
|
||||
int status = gettimeofday(&now, NULL);
|
||||
assert(status == 0, "gettimeofday");
|
||||
abstime->tv_sec = now.tv_sec + seconds;
|
||||
long usec = now.tv_usec + millis * 1000;
|
||||
if (usec >= 1000000) {
|
||||
abstime->tv_sec += 1;
|
||||
usec -= 1000000;
|
||||
}
|
||||
abstime->tv_nsec = usec * 1000;
|
||||
}
|
||||
return abstime;
|
||||
}
|
||||
|
||||
void os::PlatformEvent::park() { // AKA "down()"
|
||||
// Transitions for _Event:
|
||||
// -1 => -1 : illegal
|
||||
// 1 => 0 : pass - return immediately
|
||||
// 0 => -1 : block; then set _Event to 0 before returning
|
||||
|
||||
// Invariant: Only the thread associated with the Event/PlatformEvent
|
||||
// may call park().
|
||||
// TODO: assert that _Assoc != NULL or _Assoc == Self
|
||||
assert(_nParked == 0, "invariant");
|
||||
|
||||
int v;
|
||||
for (;;) {
|
||||
v = _Event;
|
||||
if (Atomic::cmpxchg(v-1, &_Event, v) == v) break;
|
||||
}
|
||||
guarantee(v >= 0, "invariant");
|
||||
if (v == 0) {
|
||||
// Do this the hard way by blocking ...
|
||||
int status = pthread_mutex_lock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_lock");
|
||||
guarantee(_nParked == 0, "invariant");
|
||||
++_nParked;
|
||||
while (_Event < 0) {
|
||||
status = pthread_cond_wait(_cond, _mutex);
|
||||
// for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
|
||||
// Treat this the same as if the wait was interrupted
|
||||
if (status == ETIME) { status = EINTR; }
|
||||
assert_status(status == 0 || status == EINTR, status, "cond_wait");
|
||||
}
|
||||
--_nParked;
|
||||
|
||||
_Event = 0;
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_unlock");
|
||||
// Paranoia to ensure our locked and lock-free paths interact
|
||||
// correctly with each other.
|
||||
OrderAccess::fence();
|
||||
}
|
||||
guarantee(_Event >= 0, "invariant");
|
||||
}
|
||||
|
||||
int os::PlatformEvent::park(jlong millis) {
|
||||
// Transitions for _Event:
|
||||
// -1 => -1 : illegal
|
||||
// 1 => 0 : pass - return immediately
|
||||
// 0 => -1 : block; then set _Event to 0 before returning
|
||||
|
||||
guarantee(_nParked == 0, "invariant");
|
||||
|
||||
int v;
|
||||
for (;;) {
|
||||
v = _Event;
|
||||
if (Atomic::cmpxchg(v-1, &_Event, v) == v) break;
|
||||
}
|
||||
guarantee(v >= 0, "invariant");
|
||||
if (v != 0) return OS_OK;
|
||||
|
||||
// We do this the hard way, by blocking the thread.
|
||||
// Consider enforcing a minimum timeout value.
|
||||
struct timespec abst;
|
||||
compute_abstime(&abst, millis);
|
||||
|
||||
int ret = OS_TIMEOUT;
|
||||
int status = pthread_mutex_lock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_lock");
|
||||
guarantee(_nParked == 0, "invariant");
|
||||
++_nParked;
|
||||
|
||||
// Object.wait(timo) will return because of
|
||||
// (a) notification
|
||||
// (b) timeout
|
||||
// (c) thread.interrupt
|
||||
//
|
||||
// Thread.interrupt and object.notify{All} both call Event::set.
|
||||
// That is, we treat thread.interrupt as a special case of notification.
|
||||
// We ignore spurious OS wakeups unless FilterSpuriousWakeups is false.
|
||||
// We assume all ETIME returns are valid.
|
||||
//
|
||||
// TODO: properly differentiate simultaneous notify+interrupt.
|
||||
// In that case, we should propagate the notify to another waiter.
|
||||
|
||||
while (_Event < 0) {
|
||||
status = pthread_cond_timedwait(_cond, _mutex, &abst);
|
||||
assert_status(status == 0 || status == EINTR ||
|
||||
status == ETIME || status == ETIMEDOUT,
|
||||
status, "cond_timedwait");
|
||||
if (!FilterSpuriousWakeups) break; // previous semantics
|
||||
if (status == ETIME || status == ETIMEDOUT) break;
|
||||
// We consume and ignore EINTR and spurious wakeups.
|
||||
}
|
||||
--_nParked;
|
||||
if (_Event >= 0) {
|
||||
ret = OS_OK;
|
||||
}
|
||||
_Event = 0;
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_unlock");
|
||||
assert(_nParked == 0, "invariant");
|
||||
// Paranoia to ensure our locked and lock-free paths interact
|
||||
// correctly with each other.
|
||||
OrderAccess::fence();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void os::PlatformEvent::unpark() {
|
||||
// Transitions for _Event:
|
||||
// 0 => 1 : just return
|
||||
// 1 => 1 : just return
|
||||
// -1 => either 0 or 1; must signal target thread
|
||||
// That is, we can safely transition _Event from -1 to either
|
||||
// 0 or 1.
|
||||
// See also: "Semaphores in Plan 9" by Mullender & Cox
|
||||
//
|
||||
// Note: Forcing a transition from "-1" to "1" on an unpark() means
|
||||
// that it will take two back-to-back park() calls for the owning
|
||||
// thread to block. This has the benefit of forcing a spurious return
|
||||
// from the first park() call after an unpark() call which will help
|
||||
// shake out uses of park() and unpark() without condition variables.
|
||||
|
||||
if (Atomic::xchg(1, &_Event) >= 0) return;
|
||||
|
||||
// Wait for the thread associated with the event to vacate
|
||||
int status = pthread_mutex_lock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_lock");
|
||||
int AnyWaiters = _nParked;
|
||||
assert(AnyWaiters == 0 || AnyWaiters == 1, "invariant");
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "mutex_unlock");
|
||||
if (AnyWaiters != 0) {
|
||||
// Note that we signal() *after* dropping the lock for "immortal" Events.
|
||||
// This is safe and avoids a common class of futile wakeups. In rare
|
||||
// circumstances this can cause a thread to return prematurely from
|
||||
// cond_{timed}wait() but the spurious wakeup is benign and the victim
|
||||
// will simply re-test the condition and re-park itself.
|
||||
// This provides particular benefit if the underlying platform does not
|
||||
// provide wait morphing.
|
||||
status = pthread_cond_signal(_cond);
|
||||
assert_status(status == 0, status, "cond_signal");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// JSR166
|
||||
// -------------------------------------------------------
|
||||
|
||||
// The solaris and linux implementations of park/unpark are fairly
|
||||
// conservative for now, but can be improved. They currently use a
|
||||
// mutex/condvar pair, plus a a count.
|
||||
// Park decrements count if > 0, else does a condvar wait. Unpark
|
||||
// sets count to 1 and signals condvar. Only one thread ever waits
|
||||
// on the condvar. Contention seen when trying to park implies that someone
|
||||
// is unparking you, so don't wait. And spurious returns are fine, so there
|
||||
// is no need to track notifications.
|
||||
|
||||
// This code is common to linux and solaris and will be moved to a
|
||||
// common place in dolphin.
|
||||
//
|
||||
// The passed in time value is either a relative time in nanoseconds
|
||||
// or an absolute time in milliseconds. Either way it has to be unpacked
|
||||
// into suitable seconds and nanoseconds components and stored in the
|
||||
// given timespec structure.
|
||||
// Given time is a 64-bit value and the time_t used in the timespec is only
|
||||
// a signed-32-bit value (except on 64-bit Linux) we have to watch for
|
||||
// overflow if times way in the future are given. Further on Solaris versions
|
||||
// prior to 10 there is a restriction (see cond_timedwait) that the specified
|
||||
// number of seconds, in abstime, is less than current_time + 100,000,000.
|
||||
// As it will be 28 years before "now + 100000000" will overflow we can
|
||||
// ignore overflow and just impose a hard-limit on seconds using the value
|
||||
// of "now + 100,000,000". This places a limit on the timeout of about 3.17
|
||||
// years from "now".
|
||||
|
||||
static void unpackTime(timespec* absTime, bool isAbsolute, jlong time) {
|
||||
assert(time > 0, "convertTime");
|
||||
time_t max_secs = 0;
|
||||
|
||||
if (!os::supports_monotonic_clock() || isAbsolute) {
|
||||
struct timeval now;
|
||||
int status = gettimeofday(&now, NULL);
|
||||
assert(status == 0, "gettimeofday");
|
||||
|
||||
max_secs = now.tv_sec + MAX_SECS;
|
||||
|
||||
if (isAbsolute) {
|
||||
jlong secs = time / 1000;
|
||||
if (secs > max_secs) {
|
||||
absTime->tv_sec = max_secs;
|
||||
} else {
|
||||
absTime->tv_sec = secs;
|
||||
}
|
||||
absTime->tv_nsec = (time % 1000) * NANOSECS_PER_MILLISEC;
|
||||
} else {
|
||||
jlong secs = time / NANOSECS_PER_SEC;
|
||||
if (secs >= MAX_SECS) {
|
||||
absTime->tv_sec = max_secs;
|
||||
absTime->tv_nsec = 0;
|
||||
} else {
|
||||
absTime->tv_sec = now.tv_sec + secs;
|
||||
absTime->tv_nsec = (time % NANOSECS_PER_SEC) + now.tv_usec*1000;
|
||||
if (absTime->tv_nsec >= NANOSECS_PER_SEC) {
|
||||
absTime->tv_nsec -= NANOSECS_PER_SEC;
|
||||
++absTime->tv_sec; // note: this must be <= max_secs
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// must be relative using monotonic clock
|
||||
struct timespec now;
|
||||
int status = os::Linux::clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
assert_status(status == 0, status, "clock_gettime");
|
||||
max_secs = now.tv_sec + MAX_SECS;
|
||||
jlong secs = time / NANOSECS_PER_SEC;
|
||||
if (secs >= MAX_SECS) {
|
||||
absTime->tv_sec = max_secs;
|
||||
absTime->tv_nsec = 0;
|
||||
} else {
|
||||
absTime->tv_sec = now.tv_sec + secs;
|
||||
absTime->tv_nsec = (time % NANOSECS_PER_SEC) + now.tv_nsec;
|
||||
if (absTime->tv_nsec >= NANOSECS_PER_SEC) {
|
||||
absTime->tv_nsec -= NANOSECS_PER_SEC;
|
||||
++absTime->tv_sec; // note: this must be <= max_secs
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(absTime->tv_sec >= 0, "tv_sec < 0");
|
||||
assert(absTime->tv_sec <= max_secs, "tv_sec > max_secs");
|
||||
assert(absTime->tv_nsec >= 0, "tv_nsec < 0");
|
||||
assert(absTime->tv_nsec < NANOSECS_PER_SEC, "tv_nsec >= nanos_per_sec");
|
||||
}
|
||||
|
||||
void Parker::park(bool isAbsolute, jlong time) {
|
||||
// Ideally we'd do something useful while spinning, such
|
||||
// as calling unpackTime().
|
||||
|
||||
// Optional fast-path check:
|
||||
// Return immediately if a permit is available.
|
||||
// We depend on Atomic::xchg() having full barrier semantics
|
||||
// since we are doing a lock-free update to _counter.
|
||||
if (Atomic::xchg(0, &_counter) > 0) return;
|
||||
|
||||
Thread* thread = Thread::current();
|
||||
assert(thread->is_Java_thread(), "Must be JavaThread");
|
||||
JavaThread *jt = (JavaThread *)thread;
|
||||
|
||||
// Optional optimization -- avoid state transitions if there's an interrupt pending.
|
||||
// Check interrupt before trying to wait
|
||||
if (Thread::is_interrupted(thread, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Next, demultiplex/decode time arguments
|
||||
timespec absTime;
|
||||
if (time < 0 || (isAbsolute && time == 0)) { // don't wait at all
|
||||
return;
|
||||
}
|
||||
if (time > 0) {
|
||||
unpackTime(&absTime, isAbsolute, time);
|
||||
}
|
||||
|
||||
|
||||
// Enter safepoint region
|
||||
// Beware of deadlocks such as 6317397.
|
||||
// The per-thread Parker:: mutex is a classic leaf-lock.
|
||||
// In particular a thread must never block on the Threads_lock while
|
||||
// holding the Parker:: mutex. If safepoints are pending both the
|
||||
// the ThreadBlockInVM() CTOR and DTOR may grab Threads_lock.
|
||||
ThreadBlockInVM tbivm(jt);
|
||||
|
||||
// Don't wait if cannot get lock since interference arises from
|
||||
// unblocking. Also. check interrupt before trying wait
|
||||
if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int status;
|
||||
if (_counter > 0) { // no wait needed
|
||||
_counter = 0;
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "invariant");
|
||||
// Paranoia to ensure our locked and lock-free paths interact
|
||||
// correctly with each other and Java-level accesses.
|
||||
OrderAccess::fence();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ASSERT
|
||||
// Don't catch signals while blocked; let the running threads have the signals.
|
||||
// (This allows a debugger to break into the running thread.)
|
||||
sigset_t oldsigs;
|
||||
sigemptyset(&oldsigs);
|
||||
sigset_t* allowdebug_blocked = os::Linux::allowdebug_blocked_signals();
|
||||
pthread_sigmask(SIG_BLOCK, allowdebug_blocked, &oldsigs);
|
||||
#endif
|
||||
|
||||
OSThreadWaitState osts(thread->osthread(), false /* not Object.wait() */);
|
||||
jt->set_suspend_equivalent();
|
||||
// cleared by handle_special_suspend_equivalent_condition() or java_suspend_self()
|
||||
|
||||
assert(_cur_index == -1, "invariant");
|
||||
if (time == 0) {
|
||||
_cur_index = REL_INDEX; // arbitrary choice when not timed
|
||||
status = pthread_cond_wait(&_cond[_cur_index], _mutex);
|
||||
} else {
|
||||
_cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
|
||||
status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
|
||||
}
|
||||
_cur_index = -1;
|
||||
assert_status(status == 0 || status == EINTR ||
|
||||
status == ETIME || status == ETIMEDOUT,
|
||||
status, "cond_timedwait");
|
||||
|
||||
#ifdef ASSERT
|
||||
pthread_sigmask(SIG_SETMASK, &oldsigs, NULL);
|
||||
#endif
|
||||
|
||||
_counter = 0;
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "invariant");
|
||||
// Paranoia to ensure our locked and lock-free paths interact
|
||||
// correctly with each other and Java-level accesses.
|
||||
OrderAccess::fence();
|
||||
|
||||
// If externally suspended while waiting, re-suspend
|
||||
if (jt->handle_special_suspend_equivalent_condition()) {
|
||||
jt->java_suspend_self();
|
||||
}
|
||||
}
|
||||
|
||||
void Parker::unpark() {
|
||||
int status = pthread_mutex_lock(_mutex);
|
||||
assert_status(status == 0, status, "invariant");
|
||||
const int s = _counter;
|
||||
_counter = 1;
|
||||
// must capture correct index before unlocking
|
||||
int index = _cur_index;
|
||||
status = pthread_mutex_unlock(_mutex);
|
||||
assert_status(status == 0, status, "invariant");
|
||||
if (s < 1 && index != -1) {
|
||||
// thread is definitely parked
|
||||
status = pthread_cond_signal(&_cond[index]);
|
||||
assert_status(status == 0, status, "invariant");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extern char** environ;
|
||||
|
||||
// Run the specified command in a separate process. Return its exit value,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue