178 lines
6.1 KiB
C
178 lines
6.1 KiB
C
/* hak_raisetick() and hak_rcvtick() - the per-instance tick counter pair.
|
|
*
|
|
* The tick is published as a counter rather than a flag so that the raiser and
|
|
* the scheduler never write the same field: hak_raisetick() only increments
|
|
* hak->tick, and the scheduler only writes hak->last_tick, copying the value
|
|
* it observed. A raise landing between the scheduler's test and its update is
|
|
* therefore still pending afterwards rather than being lost, and no atomic
|
|
* operation is needed to get that.
|
|
*
|
|
* This checks that contract directly on the fields, which is white-box but is
|
|
* the only way to reach hak_raisetick() - it has no script-level binding, and
|
|
* driving it through a running VM cannot isolate it from the global tick.
|
|
*
|
|
* The second half then drives it end to end: a script forks a process and
|
|
* spins without ever yielding, and hak_raisetick() is called from the
|
|
* vm_checkbc callback - ordinary context, where an embedder would call it.
|
|
* The OS ticker is deliberately never started, so gtick never moves and the
|
|
* global half plays no part; only the per-instance tick can break the spin.
|
|
* t/tick-01.hak covers the global half. */
|
|
|
|
#include <hak.h>
|
|
#include "tap.h"
|
|
#include <string.h>
|
|
|
|
static void state_contract (void)
|
|
{
|
|
hak_t* hak;
|
|
|
|
hak = hak_openstd(0, HAK_NULL);
|
|
OK (hak != HAK_NULL, "instantiation");
|
|
if (!hak) return;
|
|
|
|
/* enabling reception seeds the watermark, so an instance does not act on
|
|
* ticks raised before it was listening */
|
|
hak_raisetick(hak);
|
|
hak_raisetick(hak);
|
|
hak_rcvtick(hak, 1);
|
|
OK (hak->last_tick == hak->tick, "enabling reception discards earlier ticks");
|
|
|
|
hak_raisetick(hak);
|
|
OK (hak->last_tick != hak->tick, "hak_raisetick leaves a tick pending");
|
|
|
|
/* a second raise while one is already pending must not cancel it - the
|
|
* flag version could lose one here, the counter cannot */
|
|
hak_raisetick(hak);
|
|
OK (hak->last_tick != hak->tick, "a second raise keeps the tick pending");
|
|
|
|
/* the scheduler consumes a tick by copying, never by clearing */
|
|
hak->last_tick = hak->tick;
|
|
OK (hak->last_tick == hak->tick, "copying the observed value consumes it");
|
|
|
|
hak_raisetick(hak);
|
|
OK (hak->last_tick != hak->tick, "and a later raise is seen again");
|
|
|
|
/* disabling reception must not lose a pending tick either - rcv_tick
|
|
* gates whether ticks are acted on, not whether they are recorded */
|
|
hak_rcvtick(hak, 0);
|
|
OK (hak->last_tick != hak->tick, "disabling reception leaves the tick recorded");
|
|
OK (hak->rcv_tick == 0, "reception is off");
|
|
|
|
hak_rcvtick(hak, 1);
|
|
OK (hak->last_tick == hak->tick, "re-enabling reception reseeds the watermark");
|
|
OK (hak->rcv_tick == 1, "reception is on");
|
|
|
|
hak_close(hak);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------ */
|
|
|
|
/* The spinner breaks out as soon as the forked process sets the flag, so a
|
|
* count below the bound means the tick was delivered and acted on.
|
|
*
|
|
* hak_execute() answers the value of whichever process finishes LAST, so the
|
|
* sem-wait after the loop is load-bearing: it makes the main process finish
|
|
* last in both outcomes, and the returned value is therefore always its own
|
|
* counter rather than the forked process's. Without it, a run in which nothing
|
|
* preempted the spinner would answer the setter's value and read as a pass. */
|
|
#define SPIN_BOUND 300000
|
|
#define BC_BEFORE_RAISE 20000
|
|
|
|
static const char SRC[] =
|
|
"flag := 0\n"
|
|
"s := (core.sem-new)\n"
|
|
"fun setter() { flag := 1 ; core.sem-signal s }\n"
|
|
"p := (core.fork setter)\n"
|
|
"i := 0\n"
|
|
"while (< i 300000) { if (== flag 1) { break } ; i := (+ i 1) }\n"
|
|
"core.sem-wait s\n"
|
|
"r := i\n";
|
|
|
|
static hak_oow_t bc_seen = 0;
|
|
static int raised = 0;
|
|
|
|
static void cb_checkbc (hak_t* hak, hak_oob_t bcode)
|
|
{
|
|
if (!raised && ++bc_seen >= BC_BEFORE_RAISE)
|
|
{
|
|
raised = 1;
|
|
hak_raisetick(hak);
|
|
}
|
|
}
|
|
|
|
static int on_cnode (hak_t* hak, hak_cnode_t* obj)
|
|
{
|
|
return hak_compile(hak, obj, 0);
|
|
}
|
|
|
|
/* The script below reaches the process and semaphore primitives through the
|
|
* core module, which is where they are registered. A build configured with
|
|
* --enable-static-module links that module in and resolves it unaided, but one
|
|
* without it has to load the module from the build tree, and a bare
|
|
* hak_openstd() has nowhere to look. HAK_TEST_MODLIBDIRS comes from
|
|
* t/Makefile.am and names the same directories run.sh passes to the script
|
|
* tests via --modlibdirs. */
|
|
static int set_modlibdirs (hak_t* hak)
|
|
{
|
|
#if defined(HAK_TEST_MODLIBDIRS)
|
|
return hak_setoption(hak, HAK_OPT_MODLIBDIRS_BCSTR, HAK_TEST_MODLIBDIRS);
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static void preempts_a_spinner (void)
|
|
{
|
|
hak_t* hak;
|
|
hak_cb_t cb;
|
|
hak_bitmask_t trait;
|
|
hak_oop_t retv;
|
|
hak_ooi_t iters = -1;
|
|
|
|
hak = hak_openstd(0, HAK_NULL);
|
|
OK (hak != HAK_NULL, "instantiation");
|
|
if (!hak) return;
|
|
|
|
hak_getoption(hak, HAK_OPT_TRAIT, &trait);
|
|
trait |= HAK_TRAIT_AWAIT_PROCS | HAK_TRAIT_LANG_ENABLE_EOL;
|
|
hak_setoption(hak, HAK_OPT_TRAIT, &trait);
|
|
|
|
OK (set_modlibdirs(hak) == 0, "module search path");
|
|
|
|
memset (&cb, 0, sizeof(cb));
|
|
cb.vm_checkbc = cb_checkbc;
|
|
OK (hak_regcb(hak, &cb) != HAK_NULL, "callback registration");
|
|
|
|
OK (hak_ignite(hak, 0) == 0, "ignition");
|
|
OK (hak_addbuiltinprims(hak) == 0, "builtin primitives");
|
|
OK (hak_attachcciostdwithbcstr(hak, HAK_NULL) == 0, "source input stream");
|
|
OK (hak_attachudiostdwithbcstr(hak, "", "") == 0, "user data streams");
|
|
OK (hak_beginfeed(hak, on_cnode) == 0, "begin feed");
|
|
OK (hak_feedbchars(hak, SRC, strlen(SRC)) == 0, "feed the script");
|
|
OK (hak_endfeed(hak) == 0, "end feed");
|
|
|
|
hak_rcvtick(hak, 1); /* without this every tick is ignored */
|
|
|
|
retv = hak_execute(hak);
|
|
if (!retv) printf("# execute failed: [%d] %s\n", (int)hak_geterrnum(hak), hak_geterrbmsg(hak));
|
|
OK (retv != HAK_NULL, "execution");
|
|
if (retv && HAK_OOP_IS_SMOOI(retv)) iters = HAK_OOP_TO_SMOOI(retv);
|
|
|
|
printf("# raised=%d iterations=%ld (bound %d)\n", raised, (long)iters, SPIN_BOUND);
|
|
|
|
OK (raised == 1, "the callback reached the raise");
|
|
OK (iters >= 0 && iters < SPIN_BOUND,
|
|
"hak_raisetick preempts a process that never yields");
|
|
OK (iters > 100, "the switch came from the tick, not from an eager fork");
|
|
|
|
hak_close(hak);
|
|
}
|
|
|
|
int main (int argc, char* argv[])
|
|
{
|
|
no_plan();
|
|
state_contract();
|
|
preempts_a_spinner();
|
|
return exit_status();
|
|
}
|