mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-20 19:06:37 +03:00
vmsig: a neutral signaling layer between sensors/input and controls
An epoll-driven, neutral transfer-event bus that connects sensors and input actuators to one or more controls, bidirectionally. It owns the transfer context and events — delivery order, priority, protocol-level timing, and an interrupt-driven event model over fd sources (eventfd/timerfd/sockets) — and stays agnostic to both the sensor/input drivers and the control. What lives here: - memctx: a coherent address-space context per endpoint — the guest address-space root paired with a pre-opened read-only RAM-region fd, with per-endpoint epoch invalidation and retained replay to late subscribers. Perception lives in out-of-tree sensor libraries that consume this datum read-only. - exclusive-ownership leases for destructive resource classes (input, power, memory-write). - write-signaled memory writes (MEMWRITE): an atomic write to guest memory routed through the seam under an exclusive lease, never a writable mapping. - a host-management seam for VM lifecycle/status and a neutral input-injection command path. - multi-VM endpoints; capability-gated, audited control authorization over an in-process or unix-socket transport. Builds against headers only by default (a stub mode that exercises the seam without a VM); armed builds link the real sensor/input libraries behind flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
/* ctx.c — transfer context: priority, ordering, protocol timing.
|
||||
* This is the SISC-critical seam. No behavioral timing here: commands arrive
|
||||
* already decided by control; the context only orders and paces them. */
|
||||
#include "ctx_internal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Default depth ceiling for a single band (per source,dir) when no policy is set. */
|
||||
#define VMSIG_CTX_DEFAULT_INFLIGHT 4096
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
/* ---- node recycling (free-list under the shared mutex) ------------------- */
|
||||
static ev_node* node_get(vmsig_ctx* c) {
|
||||
ev_node* n = c->freelist;
|
||||
if (n) { c->freelist = n->next; return n; }
|
||||
return malloc(sizeof *n);
|
||||
}
|
||||
static void node_put(vmsig_ctx* c, ev_node* n) {
|
||||
n->next = c->freelist;
|
||||
c->freelist = n;
|
||||
}
|
||||
|
||||
vmsig_ctx* vmsig_ctx_new(void) {
|
||||
vmsig_ctx* c = calloc(1, sizeof *c);
|
||||
if (!c) return NULL;
|
||||
if (pthread_mutex_init(&c->lock, NULL) != 0) { free(c); return NULL; }
|
||||
for (int d = 0; d < 2; d++) {
|
||||
c->dir[d].timing_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
|
||||
if (c->dir[d].timing_fd < 0) {
|
||||
for (int k = 0; k < d; k++) close(c->dir[k].timing_fd);
|
||||
pthread_mutex_destroy(&c->lock);
|
||||
free(c);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
void vmsig_ctx_free(vmsig_ctx* c) {
|
||||
if (!c) return;
|
||||
for (int d = 0; d < 2; d++) {
|
||||
for (int p = 0; p < VMSIG_PRIO_MAX; p++) {
|
||||
ev_node* n = c->dir[d].band[p].head;
|
||||
while (n) { ev_node* nx = n->next; vmsig_payload_release(&n->ev); free(n); n = nx; }
|
||||
}
|
||||
if (c->dir[d].timing_fd >= 0) close(c->dir[d].timing_fd);
|
||||
}
|
||||
/* actually free the recycled nodes (no payload attached) */
|
||||
ev_node* f = c->freelist;
|
||||
while (f) { ev_node* nx = f->next; free(f); f = nx; }
|
||||
pthread_mutex_destroy(&c->lock);
|
||||
free(c);
|
||||
}
|
||||
|
||||
int vmsig_ctx_set_policy(vmsig_ctx* c, vmsig_source src, vmsig_dir dir,
|
||||
vmsig_prio default_prio, const vmsig_timing* t) {
|
||||
if (!c || src >= VMSIG_SRC_MAX || dir > VMSIG_DIR_DOWN) return -1;
|
||||
pthread_mutex_lock(&c->lock);
|
||||
ctx_policy* pol = &c->policy[src][dir];
|
||||
pol->default_prio = default_prio;
|
||||
if (t) pol->timing = *t; else memset(&pol->timing, 0, sizeof pol->timing);
|
||||
pol->policy_set = 1;
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void band_push_tail(ev_band* b, ev_node* n) {
|
||||
n->next = NULL;
|
||||
if (b->tail) b->tail->next = n; else b->head = n;
|
||||
b->tail = n;
|
||||
b->count++;
|
||||
}
|
||||
|
||||
int vmsig_ctx_submit(vmsig_ctx* c, vmsig_dir dir, vmsig_event* ev) {
|
||||
if (!c || !ev || dir > VMSIG_DIR_DOWN) return -1;
|
||||
vmsig_source src = ev->source < VMSIG_SRC_MAX ? ev->source : VMSIG_SRC_NONE;
|
||||
|
||||
pthread_mutex_lock(&c->lock);
|
||||
ctx_policy* pol = &c->policy[src][dir];
|
||||
|
||||
/* effective priority = max(policy default, emitter request) */
|
||||
vmsig_prio eff = ev->prio > pol->default_prio ? ev->prio : pol->default_prio;
|
||||
if (eff >= VMSIG_PRIO_MAX) eff = VMSIG_PRIO_MAX - 1;
|
||||
|
||||
ev->seq = ++c->seq;
|
||||
if (ev->ts_ns == 0) ev->ts_ns = now_ns();
|
||||
ev->prio = eff;
|
||||
|
||||
ev_band* band = &c->dir[dir].band[eff];
|
||||
|
||||
/* coalescing: a burst of the same kind+endpoint is collapsed (newest wins) */
|
||||
if (pol->timing.coalesce_ns) {
|
||||
for (ev_node* n = band->head; n; n = n->next) {
|
||||
if (n->ev.kind == ev->kind && n->ev.endpoint == ev->endpoint) {
|
||||
vmsig_payload_release(&n->ev);
|
||||
uint32_t keep_seq = n->ev.seq; /* keep position in the order */
|
||||
n->ev = *ev;
|
||||
n->ev.seq = keep_seq;
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* backpressure: channel depth is bounded. When no policy is set
|
||||
* (max_inflight==0), a BUILT-IN default ceiling applies (drop newest),
|
||||
* so the queue does not grow without bound under a command flood. */
|
||||
uint32_t cap = pol->timing.max_inflight ? pol->timing.max_inflight
|
||||
: VMSIG_CTX_DEFAULT_INFLIGHT;
|
||||
uint8_t dp = pol->timing.max_inflight ? pol->timing.drop_policy
|
||||
: VMSIG_DROP_NEWEST;
|
||||
if (band->count >= (int)cap) {
|
||||
if (dp == VMSIG_DROP_OLDEST) {
|
||||
ev_node* old = band->head; /* drop the oldest */
|
||||
if (old) {
|
||||
band->head = old->next;
|
||||
if (!band->head) band->tail = NULL;
|
||||
band->count--;
|
||||
vmsig_payload_release(&old->ev);
|
||||
node_put(c, old);
|
||||
}
|
||||
} else {
|
||||
/* NEWEST / BLOCK (the loop must not block) — drop the incoming event */
|
||||
vmsig_payload_release(ev);
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
ev_node* node = node_get(c);
|
||||
if (!node) { pthread_mutex_unlock(&c->lock); return -1; }
|
||||
node->ev = *ev; /* take ownership of the payload */
|
||||
band_push_tail(band, node);
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vmsig_ctx_next(vmsig_ctx* c, vmsig_dir dir, vmsig_event* out) {
|
||||
if (!c || !out || dir > VMSIG_DIR_DOWN) return -1;
|
||||
pthread_mutex_lock(&c->lock);
|
||||
ctx_dir* d = &c->dir[dir];
|
||||
uint64_t now = now_ns();
|
||||
uint64_t min_rem = 0;
|
||||
int have_rem = 0;
|
||||
|
||||
/* Walk bands from highest priority to lowest, and within a band from head
|
||||
* to tail, returning the FIRST event "matured" against its protocol min_gap.
|
||||
* A paced source thus waits without blocking ready events of other sources.
|
||||
* Within one source the order is preserved (its earlier events come first). */
|
||||
for (int p = VMSIG_PRIO_MAX - 1; p >= 0; p--) {
|
||||
ev_band* b = &d->band[p];
|
||||
ev_node* prev = NULL;
|
||||
ev_node* n = b->head;
|
||||
while (n) {
|
||||
vmsig_source src = n->ev.source < VMSIG_SRC_MAX ? n->ev.source : VMSIG_SRC_NONE;
|
||||
ctx_policy* pol = &c->policy[src][dir];
|
||||
int due = 1;
|
||||
uint64_t rem = 0;
|
||||
if (pol->timing.min_gap_ns) {
|
||||
uint64_t due_at = pol->last_emit_ns + pol->timing.min_gap_ns;
|
||||
if (now < due_at) { due = 0; rem = due_at - now; }
|
||||
}
|
||||
if (due) {
|
||||
if (prev) prev->next = n->next; else b->head = n->next;
|
||||
if (b->tail == n) b->tail = prev;
|
||||
b->count--;
|
||||
pol->last_emit_ns = now;
|
||||
*out = n->ev; /* payload ownership -> caller */
|
||||
node_put(c, n);
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 1;
|
||||
}
|
||||
if (!have_rem || rem < min_rem) { min_rem = rem; have_rem = 1; }
|
||||
prev = n;
|
||||
n = n->next;
|
||||
}
|
||||
}
|
||||
|
||||
/* nothing matured: arm the timing-fd for the nearest due time (if any waiting) */
|
||||
if (have_rem) {
|
||||
struct itimerspec its;
|
||||
memset(&its, 0, sizeof its);
|
||||
its.it_value.tv_sec = (time_t)(min_rem / 1000000000ull);
|
||||
its.it_value.tv_nsec = (long)(min_rem % 1000000000ull);
|
||||
if (its.it_value.tv_sec == 0 && its.it_value.tv_nsec == 0) its.it_value.tv_nsec = 1;
|
||||
timerfd_settime(d->timing_fd, 0, &its, NULL);
|
||||
}
|
||||
pthread_mutex_unlock(&c->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vmsig_ctx_timing_fd(vmsig_ctx* c, vmsig_dir dir) {
|
||||
if (!c || dir > VMSIG_DIR_DOWN) return -1;
|
||||
return c->dir[dir].timing_fd;
|
||||
}
|
||||
Reference in New Issue
Block a user