From: Wen Yang <wen.yang@linux.dev>
To: Gabriele Monaco <gmonaco@redhat.com>
Cc: Nam Cao <namcao@linutronix.de>,
linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor
Date: Thu, 20 Aug 2026 02:34:42 +0800 [thread overview]
Message-ID: <d0ef8e7e-13a6-49d9-b553-aa940f6cc3a1@linux.dev> (raw)
In-Reply-To: <9fab4d73b21d588d216d57079d34455deb211a88.camel@redhat.com>
On 7/20/26 22:49, Gabriele Monaco wrote:
> On Wed, 2026-07-08 at 23:38 +0800, wen.yang@linux.dev wrote:
>> From: Wen Yang <wen.yang@linux.dev>
>>
>> +/* Uprobe binding list; protected by tlob_uprobe_mutex. */
>> +static LIST_HEAD(tlob_uprobe_list);
>> +static DEFINE_MUTEX(tlob_uprobe_mutex);
>> +
>> +/*
>> + * Serialises duplicate-check + da_handle_start_run_event() per pid.
>> + * spinlock_t not raw_spinlock_t: uprobe handlers run under Tasks Trace
>> + * SRCU (rcu_read_lock_trace()), which permits sleeping on PREEMPT_RT.
>> + */
>
> I don't think these comments add much value. spinlock_t is allowed in
> RCU critical sections anyway (it's some sort of preemption and RCU is
> preemptible under PREEMPT_RT).
>
> Of course if it isn't really required we don't use a raw spinlock, you
> don't need a justification here.
>
>> +static DEFINE_SPINLOCK(tlob_start_lock);
>> +
>> +/* Per-uprobe-binding state: a start + stop probe pair for one binary region.
>> */
>> +struct tlob_uprobe_binding {
>> + struct list_head list;
>> + u64 threshold_ns;
>> + char binpath[TLOB_MAX_PATH];
>> + loff_t offset_start;
>> + loff_t offset_stop;
>> + DECLARE_RV_UPROBE(start_probe);
>> + DECLARE_RV_UPROBE(stop_probe);
>> +};
>> +
>> +/*
>> + * Per-task teardown invoked by da_monitor_destroy() for each hash entry.
>> + * CAS on stopping (0->1) claims exclusive cleanup ownership.
>
> I find reading acronyms extremely annoying, since non-locking algorithms
> are already complex on their own, why don't you just say cmpxchg (which
> is searchable) instead of CAS. Sure CAS isn't an obscure acronym but I
> could find at least another 4 different definitions in the kernel tree.
>
>> + *
>> + * No per-entry ha_cancel_timer_sync(): da_monitor_destroy() calls
>> + * da_monitor_reset_all() + synchronize_rcu() before this hook, and
>> + * ha_mon_destroying prevents new timer callbacks from running.
>> + */
>> +static inline void tlob_extra_cleanup(struct da_monitor *da_mon)
>> +{
>> + struct ha_monitor *ha_mon = to_ha_monitor(da_mon);
>> + struct tlob_task_state *ws = ha_get_target(ha_mon);
>> +
>> + if (!ws)
>> + return;
>> +
>> + if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
>> + return;
>> +
>> + put_task_struct(ws->task);
>> + /*
>> + * da_monitor_destroy() has already called synchronize_rcu(); no
>> + * reader holds ws. Return the slot directly without call_rcu.
>> + */
>> + llist_add(&ws->free_node, &tlob_ws_free_list);
>> +}
>> +
>> +static inline bool __tlob_acc(struct task_struct *task, ktime_t now,
>> + enum tlob_acc_idx idx)
>> +{
>> + struct tlob_task_state *ws;
>> + unsigned long flags;
>> +
>> + guard(rcu)();
>> + ws = da_get_target_by_id(task->pid);
>> + if (!ws)
>> + return false;
>> + raw_spin_lock_irqsave(&ws->entry_lock, flags);
>> + ws->accs_ns[idx] += ktime_to_ns(ktime_sub(now, ws->last_ts));
>> + ws->last_ts = now;
>> + raw_spin_unlock_irqrestore(&ws->entry_lock, flags);
>> + return true;
>> +}
>> +
>> +/* Accumulate running_ns for prev; returns true if prev is monitored. */
>> +static inline bool tlob_acc_running(struct task_struct *task, ktime_t now)
>> +{
>> + return __tlob_acc(task, now, TLOB_ACC_RUNNING);
>> +}
>> +
>> +/* Accumulate waiting_ns for next; returns true if next is monitored. */
>
> There's no next and prev here, plus you're describing __tlob_acc()'s
> behaviour 3 times, I'd say just document that (even if it isn't the
> primary facing function) and that's all.
>
>> +static inline bool tlob_acc_waiting(struct task_struct *task, ktime_t now)
>> +{
>> + return __tlob_acc(task, now, TLOB_ACC_WAITING);
>> +}
>> +
>> +/*
>> + * handle_sched_switch - advance the DA on every context switch.
>> + *
>> + * Generates three DA events:
>> + * prev, prev_state != 0 -> sleep_tlob (running -> sleeping)
>> + * prev, prev_state == 0 -> preempt_tlob (running -> waiting)
>> + * next -> switch_in_tlob (waiting -> running)
>> + *
>> + * A single ktime_get() at handler entry is shared by both acc calls so that
>> + * prev's running_ns and next's waiting_ns share the same context-switch
>> + * timestamp; neither absorbs handler overhead into its accumulator.
>> + *
>> + * No waiting->sleeping edge exists: a task can only block voluntarily
>> + * (call schedule()) while it is executing on CPU, which corresponds to
>> + * the running DA state. A task in the waiting state is TASK_RUNNING in
>> + * kernel terms (on the runqueue) and cannot block itself.
>> + *
>> + * da_handle_event() is called unconditionally: it skips tasks that have no
>> + * monitor entry in the hash table.
>> + */
>> +static void handle_sched_switch(void *data, bool preempt_unused,
>> + struct task_struct *prev,
>> + struct task_struct *next,
>> + unsigned int prev_state)
>> +{
>> + ktime_t now = ktime_get();
>> + bool prev_preempted = (prev_state == 0);
>> +
>> + if (tlob_acc_running(prev, now))
>> + da_handle_event(prev->pid, NULL,
>> + prev_preempted ? preempt_tlob : sleep_tlob);
>> + if (tlob_acc_waiting(next, now))
>> + da_handle_event(next->pid, NULL, switch_in_tlob);
>> +}
>> +
>> +/* Accumulate sleeping_ns on wakeup; returns true if task is monitored. */
>> +static inline bool tlob_acc_sleeping(struct task_struct *task, ktime_t now)
>> +{
>> + return __tlob_acc(task, now, TLOB_ACC_SLEEPING);
>> +}
>> +
>> +/*
>> + * handle_sched_wakeup - sleeping -> waiting transition.
>> + *
>> + * try_to_wake_up() skips TASK_RUNNING tasks, so this never fires for a
>> + * task already in running or waiting state.
>> + */
>> +static void handle_sched_wakeup(void *data, struct task_struct *p)
>> +{
>> + ktime_t now = ktime_get();
>> +
>> + if (tlob_acc_sleeping(p, now))
>> + da_handle_event(p->pid, NULL, wakeup_tlob);
>> +}
>> +
>> +/*
>> + * handle_sched_process_exit - clean up if a task exits without TRACE_STOP.
>> + *
>> + * Called in do_exit() context; the task still has a valid pid here.
>> + * tlob_stop_task() returns -ESRCH if the task is not monitored, which is
>> fine.
>> + */
>> +static void handle_sched_process_exit(void *data, struct task_struct *p,
>> + bool group_dead)
>> +{
>> + tlob_stop_task(p);
>> +}
>> +
>> +/**
>> + * tlob_start_task - begin monitoring @task with budget @threshold_ns ns.
>> + * @task: Task to monitor; may be current or another task.
>> + * @threshold_ns: Latency budget in nanoseconds (wall-clock; running +
>> + * waiting + sleeping).
>> + * Must be in [1000, TLOB_MAX_THRESHOLD_NS].
>> + *
>> + * Returns 0, -ENODEV, -ERANGE, -EALREADY, or -ENOSPC (pool at capacity).
>> + */
>> +int tlob_start_task(struct task_struct *task, u64 threshold_ns)
>> +{
>> + struct tlob_task_state *ws;
>> +
>> + if (!da_monitor_enabled())
>> + return -ENODEV;
>> +
>> + if (threshold_ns < TLOB_MIN_THRESHOLD_NS ||
>> + threshold_ns > TLOB_MAX_THRESHOLD_NS)
>> + return -ERANGE;
>> +
>> + /* Serialise duplicate-check + pool-slot claim; see tlob_start_lock.
>> */
>> + guard(spinlock)(&tlob_start_lock);
>> +
>> + /*
>> + * __da_get_mon_storage() uses hash_for_each_possible_rcu(), which
>> + * requires an RCU read-side critical section. On PREEMPT_RT,
>> + * spinlock_t is an rt_mutex and does not satisfy this requirement.
>> + */
>
> Also this is probably misleading, getting the monitor requires RCU, then
> the fact some in some configuration something other than read_lock_rcu()
> would work too (e.g. disabling preemption) is irrelevant.
>
>> + scoped_guard(rcu) {
>> + if (da_get_target_by_id(task->pid))
>> + return -EALREADY;
>> + }
>> +
>> + /*
>> + * Both tlob_ws_alloc() and da_handle_start_run_event() pop from
>> + * pre-allocated pools of size TLOB_MAX_MONITORED; NULL return means
>> + * the pool is at capacity.
>> + */
>> + ws = tlob_ws_alloc();
>> + if (!ws)
>> + return -ENOSPC;
>> +
>> + ws->task = task;
>> + get_task_struct(task);
>> + ws->threshold_ns = threshold_ns;
>> + ws->last_ts = ktime_get();
>> + raw_spin_lock_init(&ws->entry_lock);
>> +
>> + /*
>> + * da_handle_start_run_event() claims a pool slot via
>> da_prepare_storage(),
>> + * initialises the monitor, and delivers start_tlob in one step: the
>> + * generated ha_setup_invariants() resets clk_elapsed and arms the
>> timer.
>> + * Returns 0 if the da_monitor_storage pool is exhausted.
>> + */
>
> And try not to be too specific about the internal implementation of the
> library, that can change without notice, we don't want to have to update
> all comments.
>
> Here it is indeed non-trivial to assume no space if
> da_handle_start_run_event() fails, but that's only consequence of the
> fact that this ws is certainly new (by construction) and the monitor is
> enabled: da_handle_start_run_event() can only fail if allocation failed.
>
> da_handle_start_* functions return 1 if an event was handled, this is
> by the way not documented..
>
> You could simply say something like:
>
> da_handle_start_run_event() returns false if no event was handled, in
> this case it can happen only if memory allocation failed
>
>> + if (!da_handle_start_run_event(task->pid, ws, start_tlob)) {
>> + put_task_struct(task);
>> + tlob_ws_direct_return(ws);
>> + return -ENOSPC;
>> + }
>> +
>> + return 0;
>> +}
>> +EXPORT_SYMBOL_GPL(tlob_start_task);
>> +
>> +/**
>> + * tlob_stop_task - stop monitoring @task.
>> + * @task: Task to stop.
>> + *
>> + * CAS on ws->stopping (0->1) under RCU claims cleanup ownership;
>> + * the winner cancels the timer synchronously and frees all resources.
>> + *
>> + * Returns 0, -EOVERFLOW (budget exceeded), -ESRCH (not monitored),
>> + * or -EAGAIN (concurrent caller claimed cleanup).
>> + */
>> +int tlob_stop_task(struct task_struct *task)
>> +{
>> + struct da_monitor *da_mon;
>> + struct ha_monitor *ha_mon;
>> + struct tlob_task_state *ws;
>> + bool budget_exceeded;
>> +
>> + scoped_guard(rcu) {
>> + ws = da_get_target_by_id(task->pid);
>> + if (!ws)
>> + return -ESRCH;
>> +
>> + da_mon = da_get_monitor(task->pid, NULL);
>> + if (unlikely(WARN_ON_ONCE(!da_mon)))
>> + return -ESRCH;
>> +
>> + ha_mon = to_ha_monitor(da_mon);
>> +
>> + /*
>> + * CAS (0->1) claims cleanup ownership under RCU (ws
>> guaranteed valid).
>> + * _release pairs with atomic_read_acquire in
>> ha_setup_invariants.
>> + */
>> + if (atomic_cmpxchg_release(&ws->stopping, 0, 1) != 0)
>> + return -EAGAIN;
>> + }
>> + /*
>> + * ws and ha_mon are used below outside the RCU guard. This is safe:
>> + * the winning CAS (stopping: 0->1) is the only path that frees ws,
>> + * and da_destroy_storage() below is the only call that returns the
>> + * pool slot. No concurrent path can free either object.
>> + */
>> +
>> + /* Wait for in-flight timer callback before reading da_monitoring. */
>> + ha_cancel_timer_sync(ha_mon);
>> +
>> + /* Timer fired first -> budget exceeded; otherwise reset normally. */
>> + scoped_guard(rcu) {
>> + budget_exceeded = !da_monitoring(da_mon);
>> + if (!budget_exceeded)
>> + da_monitor_reset(da_mon);
>> + }
>> + da_destroy_storage(task->pid);
>
> Here you're playing with the state machine, that looks fragile from a
> monitor code.
> You could probably have budget_exceeded as part of your target and set
> it from tlob_reset_notify() when you know there was an expiration.
>
> Perhaps "stop" could be yet another event in the state machine, bringing
> to the "stopped" state, which would automatically call reset(), you only
> need to grab ws->stopping before handling the event for
> tlob_reset_notify() to work as expected.
> Though you'd probably still need ha_cancel_timer_sync() for the rare but
> not impossible in-flight timer not yet in RCU critical section..
>
> I'd rather avoid as much as possible bringing in ha_mon/da_mon, but you
> probably cannot do better here, just you could avoid relying on
> da_monitoring().
>
> What do you think?
>
Thanks.
I took this wholesale, it is exactly how v5 is structured now:
- tlob.dot gains a "stopped" state and a "stop" event (running ->
stopped, only from running; stopped -> running on "start").
- tlob_stop_task() no longer resets or frees anything. It claims the
window with a cmpxchg on ws->stopping, waits for the in-flight timer
with ha_cancel_timer_sync() (as you predicted, still needed), then
dispatches da_handle_event(pid, NULL, stop_tlob). The entry stays in
the hash table, parked.
- budget_exceeded moved into the target: tlob_reset_notify() sets
ws->budget_exceeded on genuine timer expiry, and tlob_stop_task()
reads it back. No reliance on da_monitoring() anywhere on the stop
path.
- A later "start" for the same task restarts the parked window in
place: same pool slot, hash entry and task_struct reference, no
allocation. ha_setup_invariants() clears ws->stopping on the start
transition, so __tlob_acc() keeps scheduler events off the parked
entry (the model has no stopped self-loops).
- ws->stopping is per-window (cleared on restart); final teardown is
claimed per-task through ws->destroying (tlob_extra_cleanup() on
monitor disable switches to destroying too). This lets
handle_sched_process_exit() call stop then destroy, and lets unbind
reap tasks parked under a removed binding (tlob_unbind_reap(), closing
the leak where parked tasks lingered until exit).
The stop path still touches ha_mon (to read the target and cancel the
timer) but no longer touches da_mon or its monitoring flag.
--
Best wishes,
Wen
next prev parent reply other threads:[~2026-08-19 18:34 UTC|newest]
Thread overview: 22+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-08 15:38 [PATCH v4 0/8] rv/tlob: Add task latency over budget RV monitor wen.yang
2026-07-08 15:38 ` [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY wen.yang
2026-07-17 13:46 ` Gabriele Monaco
2026-08-19 18:25 ` Wen Yang
2026-07-20 10:23 ` Gabriele Monaco
2026-07-08 15:38 ` [PATCH v4 2/8] rv: add generic uprobe infrastructure for RV monitors wen.yang
2026-07-20 15:22 ` Gabriele Monaco
2026-08-19 18:29 ` Wen Yang
2026-07-08 15:38 ` [PATCH v4 3/8] rv/tlob: add tlob model DOT file wen.yang
2026-07-08 15:38 ` [PATCH v4 4/8] rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check wen.yang
2026-07-20 11:31 ` Gabriele Monaco
2026-07-08 15:38 ` [PATCH v4 5/8] rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable wen.yang
2026-07-08 15:38 ` [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor wen.yang
2026-07-20 14:49 ` Gabriele Monaco
2026-08-19 18:34 ` Wen Yang [this message]
2026-07-08 15:38 ` [PATCH v4 7/8] rv/tlob: add KUnit tests for the tlob monitor wen.yang
2026-07-22 14:42 ` Gabriele Monaco
2026-07-08 15:38 ` [PATCH v4 8/8] selftests/verification: add tlob selftests wen.yang
2026-07-22 13:34 ` Gabriele Monaco
2026-07-22 13:59 ` Gabriele Monaco
2026-07-23 13:29 ` Gabriele Monaco
2026-07-23 11:53 ` [PATCH v4 0/8] rv/tlob: Add task latency over budget RV monitor Gabriele Monaco
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=d0ef8e7e-13a6-49d9-b553-aa940f6cc3a1@linux.dev \
--to=wen.yang@linux.dev \
--cc=gmonaco@redhat.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=namcao@linutronix.de \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).