Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Entry point: yuneta_entry_point + ydaemon

Read this first. Everything else under Operating Yuneta assumes you already know what main() does, who is the parent of who, and why a yuno survives a kill -9 of its agent.

A yuno is an autonomous machine. It is engineered to keep running with or without an agent on the box. The agent is one more peer in the ecosystem — a manager, not a parent. The mechanism that makes that autonomy concrete lives in two files of kernel/c/root-linux/src/:

FileRole
entry_point.cThe int main() body every yuno calls. argp, gbmem, config, log, gobj_start_up, registration, then either daemon or foreground.
ydaemon.cThe supervision kernel under --start. Double fork, watcher process, auto-relaunch on abnormal exit.

Every standalone or citizen yuno’s main.c boils down to one call to yuneta_entry_point() (optionally preceded by yuneta_setup() if it wants to override defaults). See yunos/c/yuno_agent/src/main.c for the canonical example.


1. The picture

Process tree: init launches the grandparent yuneta_agent. The parent exits and leaves a watcher (session leader, waitpid loop) that forks the yuno child. On a crash the watcher re-execs the child. pgrep yuneta_agent returns two pids per yuno.

The same picture in text:

                 ┌─────────────────────────────────────────┐
init / sysv ──►  │ grandparent: yuneta_agent --start       │
                 │   fork()  ── parent exits ──►           │
                 │   ┌─────────────────────────────────────┤
                 │   │ watcher (session leader, watcher_pid)
                 │   │   waitpid() loop, ignores all signals except SIGCHLD
                 │   │   fork()  ──►                        │
                 │   │   ┌─────────────────────────────────┤
                 │   │   │ yuno child (pid)                 │
                 │   │   │   gobj tree, yev_loop_run        │
                 │   │   │   own signalfd handler (c_yuno.c)│
                 │   │   └─────────────────────────────────┘
                 └─────────────────────────────────────────┘

Two implications worth keeping in your head:


2. yuneta_setup() — pluggable defaults (optional)

Call before yuneta_entry_point() if you want to override any of:

KnobDefaultWhere the default is set
persistent_attrsdb_load/save/remove/list_persistent_attrs (dbsimple)entry_point.c
command_parserinternal command_parser()entry_point.c
stats_parserinternal stats_parser()entry_point.c
authz_checkerC_AUTHZ monoclass checkerentry_point.c
authentication_parserC_AUTHZ parserentry_point.c
MEM_MIN_BLOCK / MEM_MAX_BLOCK512 B / 16 MiBentry_point.c
MEM_SUPERBLOCK16 MiBentry_point.c
MEM_MAX_SYSTEM_MEMORY64 MiBentry_point.c
USE_OWN_SYSTEM_MEMORYFALSE (use libc malloc under gbmem)entry_point.c

The memory tunables are passed straight to gbmem_setup() later. Yunos that handle large messages (image proxies, mqtt brokers under load) bump MEM_MAX_BLOCK and MEM_MAX_SYSTEM_MEMORY. See yuno_agent/src/main.c for a representative call site.


3. yuneta_entry_point() — the choreography

entry_point.c. In execution order:

3.1 Identity sanity

3.2 argp

FlagEffect
-S, --startRun as daemon (double-fork + watcher). Else foreground.
-K, --stopCall daemon_shutdown(process_name) and exit (see §4.5).
-f, --config-file=FILEMerge external JSON on top of fixed/variable config.
-p, --print-configPrint final merged config and exit.
-P, --print-verbose-configPrint config with all defaults expanded and exit.
-r, --print-rolePrint {role,name,alias,version,date,description,tags,…} and exit.
-v, --versionPrint yuno version and exit.
-V, --yuneta-versionPrint yuneta runtime version and exit.
-l, --verbose-log=NOverride handler_options of the stdout log handler. Not a trace switch — it only selects which fields each line prints.
-g, --global-trace=LEVELEnable a global trace level before the first service starts. Repeatable and comma-separated. list prints the available levels. The only way to trace start up itself, since set-global-trace needs the agent channel that a failing yuno never opens. See DEBUGGING.md §4.1.

3.3 Close-and-rewire stdio in daemon mode

With --start, the framework closes every fd in [0, sysconf(_SC_OPEN_MAX)). Then it opens /dev/null to take fd 0, and calls dup2 for fd 1 and fd 2. After this, no inherited fd survives. check_open_fds() warns if anything stays open beyond 4.

3.4 Allocator switch — this is load-bearing

gbmem_get_allocators(&malloc, &realloc, &calloc, &free);
json_set_alloc_funcs(malloc_func, free_func);     // jansson now uses gbmem
gbmem_setup(MEM_MAX_BLOCK, MEM_MAX_SYSTEM_MEMORY, USE_OWN_SYSTEM_MEMORY,
            MEM_MIN_BLOCK, MEM_SUPERBLOCK);

After this line, every json_* allocation goes through gbmem_* and is tracked under CONFIG_DEBUG_TRACK_MEMORY.

Test-author trap: any json_pack() / set_expected_results() called before yuneta_entry_point returns gets libc-tracked memory that gbmem cannot free later → false leaks. Put that setup inside register_yuno_and_more() (which runs at §3.10 below), never in main(). See memory feedback_test_json_allocator_timing.

3.5 Logging boot

glog_init() registers the available log handler types (stdout, file, udp). rotatory_start_up() arms the rotation timer. No handlers are attached yet.

3.6 Merge the config

json_config() merges, in order:

fixed_config (compiled into the yuno) +
variable_config (compiled, intended to be overridden) +
--config-file=… contents +
[json blob passed as argp positional argument]

view-config on a running yuno (ycommand command-yuno id=<id> service=__yuno__ command=view-config) returns this merged result, not the on-disk file. See memory feedback_yuno_runtime_config.

3.7 Environment registration

Reads environment.{work_dir, domain_dir, xpermission, rpermission} from the merged config and calls register_yuneta_environment(). This is what later powers yuneta_realm_file(), yuneta_log_file() and the rest of the path helpers.

3.8 gobj_start_up()

Initializes the gobj registry, persistent-attrs subsystem, the four parsers (command/stats/authz/authentication) and the trace plumbing. From here on, gobj_create*/gobj_log_*/gobj_subscribe_event are usable.

3.9 Log handlers — files and UDP

Reads environment.{daemon|console}_log_handlers depending on __as_daemon__. For each entry:

3.10 Register gclasses

result += yunetas_register_c_core();   // runtime gclasses (c_tcp, c_timer, …)
result += register_yuno_and_more();    // the yuno's own gclasses + setup

register_yuno_and_more() runs after environment + gobj_start_up are ready, but before any service or yuno gobj is created. This is the canonical place to:

3.11 Branch on daemon mode

if(__as_daemon__) {
    daemon_run(process, process_name, work_dir, domain_dir, cleaning_fn);
    return gobj_get_exit_code();
} else {
    process(process_name, work_dir, domain_dir, cleaning_fn);
    return result;
}

The foreground path runs process() directly. The daemon path goes through ydaemon.c. Both paths reach the same process() function.


4. ydaemon.c — the supervision kernel

ydaemon.c. The reason a yuno survives kill -9 yuneta_agent.

4.1 Double fork

continue_as_daemon(): fork() once. The parent calls _exit(EXIT_SUCCESS). The child becomes session leader via setsid() and records watcher_pid = getpid(). This is the watcher process.

relauncher(): fork() again. The watcher waitpid()s on the grandchild, and the grandchild is the actual yuno. The grandchild inherits umask 0, chdirs to work_dir, and calls process().

4.2 Watcher signal posture

signal(SIGPIPE, SIG_IGN);
signal(SIGTERM, SIG_IGN);
signal(SIGALRM, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGINT,  SIG_IGN);     // ctrl+c
signal(SIGUSR1, SIG_IGN);
signal(SIGUSR2, SIG_IGN);

The watcher is deliberately deaf to everything except SIGCHLD (delivered implicitly via waitpid) and SIGKILL (uncatchable, terminates it).

4.3 waitpid() decision matrix

Event reported by waitpid()Watcher action
WIFEXITED && exit_code == 0return 1 → exit (no relaunch)
WIFEXITED && exit_code != 0return -1 → sleep(2) + relaunch
WIFSIGNALED && signal == SIGKILLreturn 1 → exit (no relaunch)
WIFSIGNALED && signal != SIGKILL (SEGV…)return -1 → sleep(2) + relaunch
anything elsereturn -1 → relaunch

relaunch_times is bumped on every restart and logged with:

MSGSET_SYSTEM "Daemon relaunched"  process=… pid=… relaunch_times=… signal_code=… exit_code=…

That log line is the only indication a watcher resurrection happened. If you see relaunch_times > 0 after a quiet day, something crashed.

4.4 What the agent does and does not do

4.5 --stop / daemon_shutdown()

daemon_shutdown() scans /proc/*/comm for entries matching process_name and calls kill_proc() for each:

kill(pid, SIGQUIT);   // soft exit — let it delete pid file, flush logs
sleep(1);
kill(pid, SIGKILL);   // hard — guarantee it goes

The scan matches both processes of the pair, because the watcher and the child share the process name. Each one answers differently:

The second kill() is therefore not what breaks a relaunch loop: it is the guarantee for the case where one second was not enough — a child stuck in its shutdown, and the watcher still sitting in waitpid(). SIGKILL is uncatchable and takes both.

That exit code is the whole protocol. The restart path uses the other value: timeout_restart sets gobj_set_exit_code(-1), and the non-zero exit is precisely what asks the watcher to launch the yuno again.

4.6 get_watcher_pid()

Exported so c_yuno.c can include both pid and watcher_pid in the yuno’s identity card. That is how the agent gets the yuno_pid and the watcher_pid rows in its treedb (used by kill-yuno, see §7).


5. process() — the inner loop

entry_point.c. What every yuno runs after the daemon ceremony is complete.

  1. Emit the startup banner (MSGSET_STARTUP "Starting yuno") with the full realm + yuno identity. This is the first line in any logs/<N>.log.

  2. gobj_create_yuno(__yuno_name__, C_YUNO, kw_yuno) → the grandmother gobj of the runtime tree.

  3. For every entry in config.services[]: gobj_service_factory(name, tree) instantiates the service subtree.

  4. run_services() → start every service in declared order.

  5. yev_loop_run(yuno_event_loop(), -1) → block here for the rest of the process’s life. Returns only when set_yuno_must_die() flips the stop flag (§6).

  6. stop_services() → graceful shutdown in reverse order.

  7. gobj_end() → destroy yuno, free baseline allocations.

  8. yev_loop_stop() + yuno_event_destroy().

  9. rotatory_end(), json_decref(__jn_config__), optional cleaning_fn().

  10. print_track_mem() — under CONFIG_DEBUG_TRACK_MEMORY, dumps any surviving blocks. gobj_end() must run before any get_cur_system_memory() check, per the test rule in CLAUDE.md.

When process() returns, the daemon child reaches the bottom of relauncher() and exits with gobj_get_exit_code() — which determines whether the watcher exits cleanly (§4.3).


6. Signals inside the yuno child

c_yuno.c installs a signalfd() that watches SIGTERM, SIGQUIT, SIGINT, SIGALRM, SIGUSR1, SIGUSR2, SIGPIPE. yev_loop pumps it. The handler (c_yuno.c):

SignalFirst timeSecond time
SIGQUIT / SIGINT / SIGALRMset_yuno_must_die() → exit code 0 → yev_loop_run returns → clean shutdown → watcher does not relaunch_exit(0) immediately (still exit code 0, still no relaunch)
SIGUSR1cycle global trace level (off → L0 → L1 → L2 → off)same
SIGUSR2toggle deep tracingsame
SIGTERMdaemon (--start): ignored — foreground: same as SIGQUITdaemon: ignored — foreground: _exit(0)
SIGPIPEignoredignored

Three consequences:


7. How the agent kills a yuno (the watcher’s view)

c_agent.c::kill_yuno(). For each yuno node in the treedb:

int signal2kill = gobj_read_integer_attr(gobj, "signal2kill");   // default SIGQUIT
kill(yuno_pid, signal2kill);
if(signal2kill == SIGKILL) {
    kill(watcher_pid, signal2kill);    // only on --force / set-quick-kill
}

Two modes, toggled by the agent’s signal2kill attribute (SDATA default 3 = SIGQUIT, c_agent.c):

That is why kill-yuno with default options can fail to make a wedged yuno go away: the child has to cooperate with SIGQUIT. If a yuno hangs its signalfd handler, set-quick-kill is the escape hatch.


8. Crash forensics (/var/crash/core.%e)

The .deb (see packages/deb/make-yuneta-agent-deb.sh) wires this up end-to-end. Everything below is on every machine where the package is installed. On a developer machine without the package, apply it by hand.

8.1 sysctl

/etc/sysctl.d/99-yuneta-core.conf:

net.core.somaxconn = 65535
kernel.core_uses_pid = 0
kernel.core_pattern = /var/crash/core.%e
fs.file-max = 4000000
fs.nr_open  = 4000000

8.2 PAM limits

/etc/security/limits.d/99-yuneta-core.conf:

yuneta soft core unlimited
yuneta hard core unlimited
yuneta soft nofile unlimited
yuneta hard nofile unlimited

The postinst appends session required pam_limits.so to /etc/pam.d/common-session and common-session-noninteractive so these limits apply to interactive shells and to su - invocations from the init script.

8.3 SysV init limits

/etc/init.d/yuneta_agent calls _set_limits before su - yuneta -c '/yuneta/agent/yuneta_agent --start …':

ulimit -c unlimited
ulimit -l unlimited                    # memlock, for the io_uring rings
ulimit -Hn 200000 ; ulimit -n 200000   # fallback 65535

ulimit -l is here as well as in /etc/security/limits.d/, because pam_limits does not reach every boot path that runs this script. An io_uring ring is pinned memory, counted per user, so a node that starts many yunos at once fails in yev_loop_create() with ENOMEM when the memlock ceiling is the default one.

8.3.1 The init script does not start a web server itself

The script starts and stops the node’s web server through systemd:

WEBUNIT="yuneta-webserver.service"
systemctl start "$WEBUNIT"      # start_web
systemctl stop  "$WEBUNIT"      # stop_web

systemctl start on a unit that already runs does nothing, so it does not matter whether systemd or this script gets there first at boot. The unit also serves the web server the NODE chose, nginx or openresty.

An older version of the script ran /yuneta/bin/nginx/sbin/nginx directly. That is a second web server, always plain nginx, that races the unit for ports 80 and 443. On a node that serves with openresty it lost the race and wrote [emerg] bind() … Address already in use at every boot; had it won, it would have served the stock server_name localhost configuration for every vhost of the node.

The script is generated by the packagers (packages/{deb,rpm}/make-yuneta-agent-*.sh). The copy in yunos/c/yuno_agent/service/ must stay identical to the .deb one: the CMake install rule puts it in /yuneta/agent, which is where the postinst reads it from, so a build on a source node replaces the packaged script with it.

The owner of the cores is yuneta:yuneta. /var/crash itself is 0775 root:yuneta.

8.4 The post-mortem workflow

ls -lt /var/crash/                       # find the freshest core.<role>
gdb /yuneta/agent/<role> /var/crash/core.<role>
(gdb) bt full
(gdb) info threads

Cross-reference with the yuno’s logs:

ls -lt /yuneta/realms/<realm>/<yuno>/logs/
grep -a 'Daemon relaunched' /yuneta/realms/<realm>/<yuno>/logs/*.log

The Daemon relaunched line is what the watcher emits after each restart. The signal_code/exit_code fields tell you whether it was a crash (signal_code != 0) or a bad exit (exit_code != 0). Either way, the core in /var/crash/ is from the previous incarnation.


9. Pitfalls (concrete)

  1. Anything json_* before gbmem_setup leaks. Do not json_pack in main() before yuneta_entry_point(). Use register_yuno_and_more. 2. Executable basename must equal yuno_role. The framework enforces this. Do not mv a yuno binary to rename it. Use update-binary, so that the agent rewrites the launcher script too.

  2. test_ prefix skips the 15-char APP_NAME limit. This is convenient for test binaries, and the source marks it as a BUG. Do not depend on it for production yunos.

  3. Two pids per yuno. ps -ef | grep <role> returns the watcher and the child. The child is the one with the open log fds. The watcher shows in ppid and has no open files of its own (ls -l /proc/<pid>/fd). 5. relaunch_times > 0 is a crash signal. No alarm starts. Add Daemon relaunched to the checklist of your log greps.

  4. The framework overwrites the cores. The pattern has no PID. If a yuno crashes in a loop, only the last core survives. If you want the history, run cp /var/crash/core.<role> /var/crash/core.<role>.$(date +%s).

  5. gobj_end belongs to process(). A custom cleaning_fn runs after gobj_end, after rotatory_end, and after json_decref of the config. At that time the gobj system is gone. Use cleaning_fn only to free things that do not depend on it.


See also