Systèmes › Déploiement

Systèmes

Cette page n’est pas encore traduite : voici la version anglaise.

Deployment: hardware, execution context, communication

Where the parts run, and how archec follows them while they run: the deployment, the living model (archec live) and containers.

The logical model (a system) says what the parts are. Three more questions are kept apart from it, so the same system can be deployed more than one way:

  • Hardware — where things run. A processor is a component with cores (cores 8, core safety { type "Cortex-R52" }), an os, ram; a bus is a component with kind bus and a bitrate. Put them in a platform system.
  • Execution context — what runs. A deployment places binarys on processors; a binary has processes (address spaces; none on an RTOS), and they have threads and isrs with priority, period, deadline, core, stack. A thread runs parts: their functions, handlers and state machine execute on it, one at a time.
  • Communication — how two parts talk, on ports and links: pattern (fire-and-forget by default, request-reply, pub-sub, broadcast: who waits and how many receive) and buffer (queue + depth/overflow, sample, stream: what the receiver keeps), plus timeout, deadline, stale-after, reliability, topic.
deployment Vehicle "L4 stack on the vehicle" {
  system Car                        // what runs
  platform CarHw                    // where

  binary adas "adas-stack" on adcu_soc {
    process driving { core cpu2; thread drive { runs adcu; priority 80 } }
  }
  binary brake_fw "brake.elf" on brake_mcu {      // FreeRTOS: tasks, no process
    thread ctl { runs brakes; period 1ms; step regulate; priority 10 }
  }
  route adcu.can_brake -> brakes.can over chassis_can { priority 3 }
  chain obstacle { through adcu, brakes; budget 100ms }
}

The transport of each link follows from where its ends run (same thread, same process, same processor through ipc-latency, or another processor through its route) — a link between two processors with no route is an error. The linter also reports parts with behaviour that run nowhere, a core a processor does not have, a bus that does not reach both processors, mismatched patterns, a request with no handler for it, unbounded queues on an RTOS, and mixed safety levels in one address space. archec analyze (and the editor) computes per-core utilisation and fixed-priority response times from wcet, period and priority, bus load from message sizes and periods, and the worst-case latency of each chain against its budget. See examples/autonomous-car/deployment.arche.

What the system then does is measured against it: archec gen --target c --trace (or embassy --trace) puts trace points in the generated code — each thread's activation and completion, messages, bus frames, chains — and archec timing main.arche <trace…> reads the traces (or any Chrome trace JSON) and sets each thread's period, jitter and response time, each core's load, each bus's load and each chain's latency beside the prediction: worse than predicted means the analysis is optimistic (a wcet too small), over a deadline or a budget is a miss. See examples/timing. Built live (make LIVE=1, or -DARCHE_TRACE_LIVE), the C writes each trace line on stdout as it happens — the state each part's machine enters among them (arche-trace <ns> state counter.Seen 0) — for a running system to be followed.

Execution and memory#

A thread is an active object: it runs its parts one message at a time, from its queue. queue 32 sizes that queue (on an RTOS a thread woken by messages should say it), and affinity cpu1, cpu2 gives the cores it may run on when one core does not say it. A thread may also run one port of a part: runs sink.metrics handles that port's messages on it, not on the part's thread.

A part no thread runs can still run: place it in a process. It is then passive (it runs in its callers' threads) or protected (one caller at a time). execution active says it needs a thread of its own.

Memory comes from allocators. A processor says its memories (memory ddr { size 1GiB; shared }: a memory with no type and no address); an allocator in the deployment, a binary or a process takes its blocks from one of them, or from a parent allocator. A place then says where a part's memory comes from, per use: hrt and lrt (its real-time data and the rest), saved and tmp (an algorithm's working memory), comm (what its ports marshal). A dataflow port's buffering says how big its buffers are, whether consumers copy or share them, when they are freed and where they come from.

deployment Target {
  allocator heap { kind heap; block-size 1MiB; memory cpu.ddr; shared }
  binary chain on cpu {
    options mygen { orb "rteorb" }
    process main {
      allocator frames { kind pool; block-size 4KiB; blocks 32; parent heap; alignment 64 }
      thread work { runs chain; priority 50; queue 32; affinity cpu1, cpu2 }
      thread watch { runs sink.metrics; queue 8 }
      place log { execution protected; memory hrt frames }
      place chain.f1 { memory saved frames; memory tmp frames }
      place chain.split.input {
        memory comm frames
        buffering { max-size 4KiB; transfer copy; destroy after-processing; allocator frames }
      }
    }
  }
}

The linter reports an active part no thread runs, a passive part a thread runs, a thread that runs an out port, an allocator of another process, an allocator in a local memory that is mapped, and overflows: a child allocator larger than one block of its parent, more uses than blocks, a buffer larger than a block, allocators larger than their memory. A buffer that is shared and freed once sent is an error: its consumers would read freed memory.

options <generator> { … } on a binary (or a process) sets that generator's options for it only, over the project's; options { … } sets them for any generator. The generator model gives each binary and each process its options resolved. See examples/signal-chain.

Live#

archec live main.arche runs a deployment's containers under podman (the generated C traced live, --target containerd), follows their logs and podman stats, and keeps the model's status as it runs: each requirement, chain, thread and container pass, fail or pending. A chain over its budget fails the requirement it satisfies; a thread late or a container over its memory-limit fails what the parts it runs satisfy; a runtime monitor's arche-trace <ns> monitor REQ-1 1 fails REQ-1. --json streams the state (one JSON per line, each item with its file and line) for the editor; --once --duration 30s is a CI gate; --replay trace.log evaluates traces already written.

Containers#

On a Linux processor whose compute gives a runtime containerd, a container holds binaries instead of putting them straight on the processor. It names its image after its id, and its cgroup and sandbox: the cores it may use (core, its cpuset), memory-limit, network host|bridge|none, the host devices it sees (/dev/ttyS1), Linux capabilities (sys_nice for policy fifo threads, all for a privileged one) and environment variables (prop NAME = "value"). Its binaries take its on:

container driving "registry.local/l4/driving:0.3" on adcu_soc {
  core cpu2, cpu3, cpu4, cpu5; memory-limit 4GiB; network host
  capabilities sys_nice; prop RUST_LOG = "info"
  binary adas { language rust; thread drive { runs adcu; core cpu2; priority 80; policy fifo } }
}

A container with a memory-limit counts once against its processor's ram, and its processes are checked against that limit. A link between two containers costs the processor's ipc-latency, or, when one of them is on a bridge network, its latency. The linter reports containers on a non-Linux processor or one with no runtime, a thread outside its container's cores, a real-time thread without sys_nice, a link leaving a network none container, a container on a CAN bus without network host (SocketCAN can0 is a network interface, not a device), one on another bus (a serial line) that sees no device, and a binary inside a container that also says on. archec gen --target containerd writes the images and the compose files, and --target k8s the Kubernetes (k3s) manifests that run them, each pinned to its processor's node (see Code generation).

To put every process in a container of its own without writing each one, give the deployment packaging container: each process of a binary on a Linux processor gets a container named after it, on that processor, with its core(s) as the cpuset, its memory-limit, the host's network and sys_nice when one of its threads is real-time (a binary's threads with no process share one named after the binary). Containers written out stay as written; write one out to give it an image, a device or another network.

deployment Vehicle {
  packaging container
  binary adas on adcu_soc {
    process driving { core cpu2; memory-limit 4GiB; thread drive { runs adcu; policy fifo } }
    process island { core safety; thread guard { runs adcu.brakes_path } }
  }
}

In the Arche project, the containers of the simulated deployment go to containers.yaml (processor, cpuset cores, memoryLimit, network, latency, image, devices, capabilities, env); a process in one, or a thread with no process, says container: <id> and keeps its own target and core. Arche's Hardware page frames the threads of each core by their container; the simulation runs them as before.