From nobody Tue Jul 28 00:00:29 2026 Received: from out-183.mta0.migadu.com (out-183.mta0.migadu.com [91.218.175.183]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 792743F12CA for ; Wed, 8 Jul 2026 15:39:13 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.183 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525155; cv=none; b=LGbU8YzKyaf+JlWQ8x3+qYGrmdfVHHW2T/tibyoF3EeqZgnXWnIwTeGDnl408FsHC/NrS0Ku/RBX7S9ac4dzddmsXPBh6oJ+Y8gzfiZjKrkgqnO4WkegyPWR65OW3YG3jP1/BzEun9QEKQ18nQ26xrkShR0ENcfLQjuSgvSaelA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525155; c=relaxed/simple; bh=vT28XGtOuxNLckjv4Iugxa7Q/eDOR6dpNoyDJ2d5mgM=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=K29B5jhlKomLNtdpkqk3fKoAIrpnbo/BA8YJLyGY7GXUlAqnc4Vfx42vCOF8WTNVdAkHe5T8+sp8olSTRMwtOX9aNobKqtCarV+EDIRtk6Gpz7YIeCOu+YMaVuQ35LNLgXaH+Tgq/xYmd7Ivexjla5JPzID4agSqdfp63Og9KJw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=PP4P1aVV; arc=none smtp.client-ip=91.218.175.183 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="PP4P1aVV" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525151; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=W23cILXEhbaTC3LZPNeIfbFSZCRVq791q8j4d9G1B+8=; b=PP4P1aVVb/GHZc4/FnsOSjRvuNISetbpQGfS9c0Vjj4w//lJMk0KJb11edXdjsrFHzffOv QG0uW06TcPuAaiusGjzRdJEA90gDtQbuNAClOZPlMSmaRKnOAA54+gT7CrhBfCwdJqLxvg 4i0TOCLA69tGoRgpElyO0boD0NYKz84= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 1/8] rv/da: introduce DA_MON_ALLOCATION_STRATEGY Date: Wed, 8 Jul 2026 23:38:27 +0800 Message-Id: <42cda27998fca0ca2573baac60a01ab9620b91f0.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Per-object DA storage allocation is currently limited to kmalloc on demand. Add a compile-time selector so monitors can choose among three strategies: DA_ALLOC_AUTO (default) - kmalloc per object on the monitor path DA_ALLOC_POOL - pre-allocated fixed-size llist pool; selected by defining DA_MON_POOL_SIZE DA_ALLOC_MANUAL - caller pre-inserts storage; framework only links the target field The pool strategy uses a lock-free llist (cmpxchg, no spinlock) so pool release is safe from RCU callback context without acquiring a lock. Moving allocation before the measurement window also prevents kmalloc latency. nomiss is updated to DA_ALLOC_MANUAL. Suggested-by: Gabriele Monaco Signed-off-by: Wen Yang --- include/rv/da_monitor.h | 247 +++++++++++++++++++---- include/rv/ha_monitor.h | 6 + kernel/trace/rv/monitors/nomiss/nomiss.c | 6 +- 3 files changed, 221 insertions(+), 38 deletions(-) diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h index 34b8fba9ecd4..9c9acc123e3b 100644 --- a/include/rv/da_monitor.h +++ b/include/rv/da_monitor.h @@ -14,7 +14,56 @@ #ifndef _RV_DA_MONITOR_H #define _RV_DA_MONITOR_H =20 +/* + * Allocation strategies for RV_MON_PER_OBJ monitors. + * + * Select the strategy with a single define before including this header: + * + * #define DA_MON_POOL_SIZE N - pool mode; N pre-allocated slot= s. + * Implies DA_ALLOC_POOL automatic= ally. + * #define DA_MON_ALLOCATION_STRATEGY \ + * DA_ALLOC_MANUAL - manual mode (see below). + * (neither) - auto mode (default). + * + * Do not define both DA_MON_POOL_SIZE and DA_MON_ALLOCATION_STRATEGY. + * + * DA_ALLOC_AUTO - lock-free kmalloc on the hot path; unbounded capacity. + * DA_ALLOC_POOL - pre-allocated fixed-size pool; set by defining DA_MON= _POOL_SIZE. + * DA_ALLOC_MANUAL - caller inserts storage before da_handle_start_event(); + * the framework only links the target field. + */ +#define DA_ALLOC_AUTO 0 +#define DA_ALLOC_POOL 1 +#define DA_ALLOC_MANUAL 2 + +#ifdef DA_MON_POOL_SIZE +#ifdef DA_MON_ALLOCATION_STRATEGY +#error "Define only one of DA_MON_POOL_SIZE or DA_MON_ALLOCATION_STRATEGY" +#endif +#if DA_MON_POOL_SIZE =3D=3D 0 +#error "DA_MON_POOL_SIZE must be non-zero" +#endif +#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_POOL +#endif + +#ifndef DA_MON_ALLOCATION_STRATEGY +#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_AUTO +#endif + +/* + * Provide a zero default so da_monitor_init() can reference + * DA_MON_POOL_SIZE in a plain C if() without an #if guard; the + * compiler eliminates the dead branch. + */ +#ifndef DA_MON_POOL_SIZE +#if DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL +#error "DA_ALLOC_POOL requires DA_MON_POOL_SIZE to be defined and non-zero" +#endif +#define DA_MON_POOL_SIZE 0 +#endif + #include +#include #include #include #include @@ -66,6 +115,16 @@ static struct rv_monitor rv_this; #define da_monitor_sync_hook() #endif =20 +/* + * Per-object teardown hook, called after da_monitor_reset_all() + + * da_monitor_sync_hook() and before hash_del_rcu() for each entry. + * All HA timer callbacks have completed at this point. + * Define before including this header. Default: no-op. + */ +#ifndef da_extra_cleanup +#define da_extra_cleanup(da_mon) +#endif + /* * Type for the target id, default to int but can be overridden. * A long type can work as hash table key (PER_OBJ) but will be downgraded= to @@ -404,6 +463,12 @@ struct da_monitor_storage { union rv_task_monitor rv; struct hlist_node node; struct rcu_head rcu; + /* + * Mutually exclusive with rcu: rcu is live during the RCU callback + * flight; free_node when the slot is in da_pool_free_list. + * Present in all monitors to avoid #if-gating the pool helpers. + */ + struct llist_node free_node; }; =20 #ifndef DA_MONITOR_HT_BITS @@ -495,18 +560,6 @@ static inline da_id_type da_get_id(struct da_monitor *= da_mon) return container_of(da_mon, struct da_monitor_storage, rv.da_mon)->id; } =20 -/* - * da_create_or_get - create the per-object storage if not already there - * - * This needs a lookup so should be guarded by RCU, the condition is check= ed - * directly in da_create_storage() - */ -static inline void da_create_or_get(da_id_type id, monitor_target target) -{ - guard(rcu)(); - da_create_storage(id, target, da_get_monitor(id, target)); -} - /* * da_fill_empty_storage - store the target in a pre-allocated storage * @@ -537,15 +590,79 @@ static inline monitor_target da_get_target_by_id(da_i= d_type id) return mon_storage->target; } =20 +/* + * Lock-free llist (cmpxchg) rather than kmem_cache/mempool: on + * PREEMPT_RT spinlock_t becomes a sleeping lock, which is forbidden + * in the rcuc kthread context where RCU callbacks run. + * + * Multiple producers (any context, any CPU) call llist_add; a single + * consumer (llist_del_first, serialised by the monitor's start lock) + * needs no additional synchronisation. + * + * Per-TU statics: each PER_OBJ monitor gets its own pool instance; + * da_pool_storage and da_pool_free_list are NULL/empty and the pool + * paths are dead code for non-pool monitors. + */ +static struct da_monitor_storage *da_pool_storage; +static LLIST_HEAD(da_pool_free_list); + +static void da_pool_return_cb(struct rcu_head *head) +{ + struct da_monitor_storage *ms =3D + container_of(head, struct da_monitor_storage, rcu); + + llist_add(&ms->free_node, &da_pool_free_list); +} + +/* + * da_create_pool_storage - pop a free pool slot and insert it into the ha= sh. + * + * Returns the new da_monitor, or NULL if the pool is exhausted. Finding + * an existing entry for the same id fires WARN_ON_ONCE (double-start bug). + * + * Caller must hold an RCU read-side CS and the monitor's serialisation lo= ck. + */ +static inline struct da_monitor * +da_create_pool_storage(da_id_type id, monitor_target target, + struct da_monitor *da_mon) +{ + struct da_monitor_storage *mon_storage, *existing; + struct llist_node *node; + + if (da_mon) + return da_mon; + + node =3D llist_del_first(&da_pool_free_list); + if (!node) + return NULL; + mon_storage =3D llist_entry(node, struct da_monitor_storage, free_node); + + mon_storage->id =3D id; + mon_storage->target =3D target; + + /* + * The caller's serialization lock ensures llist_del_first() is + * single-consumer, so no concurrent start for the same id is possible. + * Reaching here indicates a programming error (double-start for the + * same pid). + */ + existing =3D __da_get_mon_storage(id); + if (WARN_ON_ONCE(existing)) { + llist_add(&mon_storage->free_node, &da_pool_free_list); + return NULL; + } + hash_add_rcu(da_monitor_ht, &mon_storage->node, id); + return &mon_storage->rv.da_mon; +} + /* * da_destroy_storage - destroy the per-object storage * - * The caller is responsible to synchronise writers, either with locks or - * implicitly. For instance, if da_destroy_storage is called at sched_exit= and - * da_create_storage can never occur after that, it's safe to call this wi= thout - * locks. - * This function includes an RCU read-side critical section to synchronise - * against da_monitor_destroy(). + * Pool mode: removes from hash and returns the slot via call_rcu(). + * Kmalloc mode: removes from hash and frees via kfree_rcu(). + * + * Includes an RCU read-side critical section to synchronise against + * da_monitor_destroy(). */ static inline void da_destroy_storage(da_id_type id) { @@ -558,7 +675,10 @@ static inline void da_destroy_storage(da_id_type id) return; da_monitor_reset_hook(&mon_storage->rv.da_mon); hash_del_rcu(&mon_storage->node); - kfree_rcu(mon_storage, rcu); + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL) + call_rcu(&mon_storage->rcu, da_pool_return_cb); + else + kfree_rcu(mon_storage, rcu); } =20 static void __da_monitor_reset_all(void (*reset)(struct da_monitor *)) @@ -581,41 +701,98 @@ static inline void da_monitor_reset_state_all(void) __da_monitor_reset_all(da_monitor_reset_state); } =20 +/* Not part of the public API; called only by da_monitor_init(). */ +static inline int __da_monitor_init_pool(unsigned int prealloc_count) +{ + unsigned int i; + + da_pool_storage =3D kcalloc(prealloc_count, sizeof(*da_pool_storage), + GFP_KERNEL); + if (!da_pool_storage) + return -ENOMEM; + + for (i =3D 0; i < prealloc_count; i++) + llist_add(&da_pool_storage[i].free_node, &da_pool_free_list); + return 0; +} + +/* + * da_monitor_init - initialise the per-object monitor + */ static inline int da_monitor_init(void) { hash_init(da_monitor_ht); + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL) + return __da_monitor_init_pool(DA_MON_POOL_SIZE); return 0; } =20 +/* + * da_monitor_destroy - tear down the per-object monitor + * + * tracepoint_synchronize_unregister() flushes all in-flight tracepoint + * handlers and performs synchronize_rcu(), so no RCU reader holds a + * reference to any da_monitor_storage after it returns. + * da_monitor_reset_all() disables monitoring; combined with + * da_monitor_sync_hook() (synchronize_rcu() for HA), no timer callback + * can fire or be re-armed after this sequence. + * + * Pool mode: remaining hash entries are returned to the free list + * directly (no call_rcu needed -- see above). rcu_barrier() drains any + * da_pool_return_cb() callbacks queued by earlier da_destroy_storage() + * calls before the backing array is freed. + */ static inline void da_monitor_destroy(void) { - struct da_monitor_storage *mon_storage; + struct da_monitor_storage *ms; struct hlist_node *tmp; int bkt; =20 tracepoint_synchronize_unregister(); da_monitor_reset_all(); da_monitor_sync_hook(); - /* - * This function is called after all probes are disabled and no longer - * pending, we can safely assume no concurrent user. - */ - hash_for_each_safe(da_monitor_ht, bkt, tmp, mon_storage, node) { - hash_del_rcu(&mon_storage->node); - kfree(mon_storage); + + hash_for_each_safe(da_monitor_ht, bkt, tmp, ms, node) { + da_extra_cleanup(&ms->rv.da_mon); + hash_del_rcu(&ms->node); + /* No RCU readers remain; skip the grace period. */ + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL) { + llist_add(&ms->free_node, &da_pool_free_list); + } else { + kfree(ms); + } + } + + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL) { + /* + * Flush any da_pool_return_cb callbacks queued by + * da_destroy_storage() during normal monitor operation. + * After rcu_barrier(), no callback can reference pool slots; + * the backing array is safe to free. + */ + rcu_barrier(); + init_llist_head(&da_pool_free_list); + kfree(da_pool_storage); + da_pool_storage =3D NULL; } } =20 /* - * Allow the per-object monitors to run allocation manually, necessary if = the - * start condition is in a context problematic for allocation (e.g. schedu= ling). - * In such case, if the storage was pre-allocated without a target, set it= now. + * da_prepare_storage - allocate or retrieve storage for a monitoring sess= ion + * + * Dispatches to the strategy selected by DA_MON_ALLOCATION_STRATEGY. + * Caller must hold an RCU read-side CS. */ -#ifdef DA_SKIP_AUTO_ALLOC -#define da_prepare_storage da_fill_empty_storage -#else -#define da_prepare_storage da_create_storage -#endif /* DA_SKIP_AUTO_ALLOC */ +static inline struct da_monitor * +da_prepare_storage(da_id_type id, monitor_target target, + struct da_monitor *da_mon) +{ + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_POOL) + return da_create_pool_storage(id, target, da_mon); + if (DA_MON_ALLOCATION_STRATEGY =3D=3D DA_ALLOC_MANUAL) + return da_fill_empty_storage(id, target, da_mon); + return da_create_storage(id, target, da_mon); +} =20 #endif /* RV_MON_TYPE */ =20 diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h index 28d3c74cabfc..83199f90afe8 100644 --- a/include/rv/ha_monitor.h +++ b/include/rv/ha_monitor.h @@ -365,6 +365,12 @@ static inline bool ha_check_invariant_ns(struct ha_mon= itor *ha_mon, } /* * ha_invariant_passed_ns - prepare the invariant and return the time sinc= e reset + * + * If the env has not been initialised yet (first entry into a state with = an + * invariant), anchor the guard clock at the current time so that the full + * budget is available from this point. This preserves the documented + * guard->invariant ordering: ha_set_invariant_ns() is always preceded by a + * valid guard representation in env_store. */ static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum e= nvs env, u64 expire, u64 time_ns) diff --git a/kernel/trace/rv/monitors/nomiss/nomiss.c b/kernel/trace/rv/mon= itors/nomiss/nomiss.c index 8ead8783c29f..ac4d334e757f 100644 --- a/kernel/trace/rv/monitors/nomiss/nomiss.c +++ b/kernel/trace/rv/monitors/nomiss/nomiss.c @@ -17,8 +17,8 @@ =20 #define RV_MON_TYPE RV_MON_PER_OBJ #define HA_TIMER_TYPE HA_TIMER_WHEEL -/* The start condition is on sched_switch, it's dangerous to allocate ther= e */ -#define DA_SKIP_AUTO_ALLOC +/* Allocate storage in sched_setscheduler; sched_switch is too hot to allo= c. */ +#define DA_MON_ALLOCATION_STRATEGY DA_ALLOC_MANUAL typedef struct sched_dl_entity *monitor_target; #include "nomiss.h" #include @@ -214,7 +214,7 @@ static void handle_sys_enter(void *data, struct pt_regs= *regs, long id) if (p->policy =3D=3D SCHED_DEADLINE) da_reset(EXPAND_ID_TASK(p)); else if (new_policy =3D=3D SCHED_DEADLINE) - da_create_or_get(EXPAND_ID_TASK(p)); + da_create_empty_storage(get_entity_id(&p->dl, task_cpu(p), DL_TASK)); } =20 static void handle_sched_wakeup(void *data, struct task_struct *tsk) --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-186.mta0.migadu.com (out-186.mta0.migadu.com [91.218.175.186]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 831793F12C8 for ; Wed, 8 Jul 2026 15:39:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.186 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525158; cv=none; b=VzPspkYLCsgAxJF3Y9yHDlLNlhufBhB97F9EaiPd+5EKMDpwjvDdfj8Wn1iT1BXIuTWsgZ0Yoe+tkdJLFtbY9298d9Qu/QRi3U5lt7i7f+fZWscFtPycqKk51Hgjr5axWalvVkQWCLG6WqKElrPgi/fAd7gVvP1kmpIO9JD1qb8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525158; c=relaxed/simple; bh=g8ytAzCnw6Y+4iv02waxH+DYsyMf1uT7WrsdiDe9HOs=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=Zn1jxxjd7g8Ex8FepUvYHqzD0hQrFCqUFvi5SG4we3yOsHor8MA+tAIyIfxFbPW8YCmTeWuWQToz7ZPHKZUm4vGN2uXq/IiCqhqsm05zq2iRJZ8Mn4vi7ufFvaeNWjEWobKV/TODvwG5iYvTbr9kqDEKoUfItcK3cEeV7J3GtbM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=vG7prRM9; arc=none smtp.client-ip=91.218.175.186 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="vG7prRM9" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525154; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=c+eOfnJTdry0uWC1pEFvrTgvF3XCOc5syTxn+ho6Epk=; b=vG7prRM9xaheaGjf+eN0duBMAbwDe1TUzo/L8Xm9/5ro3t9wWXLLz7tjL+kNGeStQg6FoD QDTbGtf4uMPUjyddfG/4T3R5RCM5MwirtPKkABL3BjGd8Oa5S1hoCGhKre+CXxB8C9Yx8c xLTTawnPm08Ib5jit6ljU71UeCMw3xI= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 2/8] rv: add generic uprobe infrastructure for RV monitors Date: Wed, 8 Jul 2026 23:38:28 +0800 Message-Id: <31d0438f98e80503370a6fb3932d0ec7df0673c3.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Monitors that instrument user-space function boundaries need to resolve paths, register uprobes, and deregister them safely. Provide a thin wrapper so monitors share a single implementation of this boilerplate. struct rv_uprobe embeds struct uprobe_consumer directly, avoiding a separate heap allocation per probe. Embedding is safe after rv_uprobe_unregister(): rv_uprobe_sync() calls uprobe_unregister_sync() which performs synchronize_rcu_tasks_trace(), waiting for all rcu_read_lock_trace() readers (handler_chain()) to complete on all CPUs before returning; the caller may then free the containing struct. The API provides register, synchronous and nosync unregister, a global handler barrier (rv_uprobe_sync), and an active-state predicate. Handlers receive the uprobe_consumer pointer and recover per-probe state via container_of(uc, struct rv_uprobe, uc) or the containing struct. Suggested-by: Gabriele Monaco Signed-off-by: Wen Yang --- include/rv/rv_uprobe.h | 93 ++++++++++++++++++++++++++++++++ kernel/trace/rv/Kconfig | 7 +++ kernel/trace/rv/Makefile | 1 + kernel/trace/rv/rv_uprobe.c | 104 ++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 include/rv/rv_uprobe.h create mode 100644 kernel/trace/rv/rv_uprobe.c diff --git a/include/rv/rv_uprobe.h b/include/rv/rv_uprobe.h new file mode 100644 index 000000000000..2eab5d193e13 --- /dev/null +++ b/include/rv/rv_uprobe.h @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Generic uprobe infrastructure for RV monitors. + * + */ + +#ifndef _RV_UPROBE_H +#define _RV_UPROBE_H + +#include +#include + +struct pt_regs; +struct inode; + +/** + * struct rv_uprobe - embeddable uprobe handle for RV monitors + * + * Embed via DECLARE_RV_UPROBE() in the caller's struct and pass &name to + * rv_uprobe_register(). + * + * Lifetime: after rv_uprobe_unregister() (or rv_uprobe_unregister_nosync() + * followed by rv_uprobe_sync()) returns, synchronize_rcu_tasks_trace() has + * completed and no handler_chain() iteration can reference this struct. + * The caller may free the containing struct immediately after. + * + * @uc: embedded uprobe_consumer; set uc.handler / uc.ret_handler befo= re + * calling rv_uprobe_register(); use container_of(uc, rv_uprobe, = uc) + * inside handlers to reach this struct or its container + * @uprobe: registered uprobe pointer (NULL when not registered) + * @inode: inode of the probed binary (valid while registered) + */ +struct rv_uprobe { + struct uprobe_consumer uc; + struct uprobe *uprobe; + struct inode *inode; +}; + +/* Embed a named rv_uprobe inside a caller struct */ +#define DECLARE_RV_UPROBE(name) struct rv_uprobe name + +/** + * rv_uprobe_is_registered - test whether an uprobe is currently active + * @p: probe to test; may be NULL + */ +bool rv_uprobe_is_registered(const struct rv_uprobe *p); + +/** + * rv_uprobe_register - initialise and register an uprobe + * @binpath: absolute path to the target binary + * @offset: byte offset within the binary + * @p: caller-provided rv_uprobe (embedded via DECLARE_RV_UPROBE); + * p->uc.handler and/or p->uc.ret_handler must be set before thi= s call + * + * Resolves the path and registers p->uc with the uprobe subsystem. + * No heap allocation is performed. + * + * Returns 0 on success, negative errno on failure. + */ +int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprob= e *p); + +/** + * rv_uprobe_unregister - synchronously unregister a uprobe + * @p: probe to unregister; may be NULL (no-op) + * + * Removes the consumer from the uprobe subsystem and waits for all in-fli= ght + * handlers to complete (via synchronize_rcu_tasks_trace()). After this + * returns, the containing struct may be safely freed by the caller. + * Use rv_uprobe_unregister_nosync() + rv_uprobe_sync() to batch multiple + * deregistrations before a single synchronisation. + */ +void rv_uprobe_unregister(struct rv_uprobe *p); + +/** + * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting + * @p: probe to dequeue; may be NULL (no-op) + * + * Removes the consumer without waiting for in-flight handlers. The caller + * must call rv_uprobe_sync() before freeing the containing struct. + */ +void rv_uprobe_unregister_nosync(struct rv_uprobe *p); + +/** + * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete + * + * Global barrier: calls uprobe_unregister_sync() which performs + * synchronize_rcu_tasks_trace() + synchronize_srcu(&uretprobes_srcu). + * After this returns, no handler_chain() iteration referencing any + * previously deregistered consumer is still in progress. + */ +void rv_uprobe_sync(void); + +#endif /* _RV_UPROBE_H */ diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig index 3884b14df375..5bad1d63f411 100644 --- a/kernel/trace/rv/Kconfig +++ b/kernel/trace/rv/Kconfig @@ -59,6 +59,13 @@ config RV_PER_TASK_MONITORS This option configures the maximum number of per-task RV monitors that = can run simultaneously. =20 +config RV_UPROBE + bool + depends on RV && UPROBES + help + Generic uprobe infrastructure for RV monitors. Provides path + resolution, registration, and safe synchronous teardown. + source "kernel/trace/rv/monitors/wip/Kconfig" source "kernel/trace/rv/monitors/wwnr/Kconfig" =20 diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile index 94498da35b37..f139b904bea3 100644 --- a/kernel/trace/rv/Makefile +++ b/kernel/trace/rv/Makefile @@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) +=3D monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) +=3D monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) +=3D monitors/nomiss/nomiss.o # Add new monitors here +obj-$(CONFIG_RV_UPROBE) +=3D rv_uprobe.o obj-$(CONFIG_RV_REACTORS) +=3D rv_reactors.o obj-$(CONFIG_RV_REACT_PRINTK) +=3D reactor_printk.o obj-$(CONFIG_RV_REACT_PANIC) +=3D reactor_panic.o diff --git a/kernel/trace/rv/rv_uprobe.c b/kernel/trace/rv/rv_uprobe.c new file mode 100644 index 000000000000..a80bfa64578b --- /dev/null +++ b/kernel/trace/rv/rv_uprobe.c @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Generic uprobe infrastructure for RV monitors. + * + * struct rv_uprobe embeds struct uprobe_consumer directly. This is safe + * because rv_uprobe_sync() calls uprobe_unregister_sync(), which calls + * synchronize_rcu_tasks_trace(). handler_chain() runs under + * rcu_read_lock_trace(), so after synchronize_rcu_tasks_trace() returns, + * all in-flight handler_chain() iterations, including any pending + * uc->cons_node.next reads, have completed on all CPUs. The caller may + * then free the struct containing rv_uprobe immediately. + */ +#include +#include +#include +#include +#include + +/** + * rv_uprobe_register - initialise and register an uprobe + */ +int rv_uprobe_register(const char *binpath, loff_t offset, struct rv_uprob= e *p) +{ + struct inode *inode; + struct path path; + int ret; + + if (!p->uc.handler && !p->uc.ret_handler) + return -EINVAL; + + ret =3D kern_path(binpath, LOOKUP_FOLLOW, &path); + if (ret) + return ret; + + if (!d_is_reg(path.dentry)) { + path_put(&path); + return -EINVAL; + } + + inode =3D d_real_inode(path.dentry); + p->inode =3D inode; + + /* + * uprobe_register() requires the inode (and mount) to remain + * referenced across the call. Keep the path alive until after + * uprobe_register() has stored its own reference, then release it. + */ + p->uprobe =3D uprobe_register(inode, offset, 0, &p->uc); + path_put(&path); + if (IS_ERR(p->uprobe)) { + ret =3D PTR_ERR(p->uprobe); + p->uprobe =3D NULL; + p->inode =3D NULL; + return ret; + } + + return 0; +} +EXPORT_SYMBOL_GPL(rv_uprobe_register); + +/** + * rv_uprobe_is_registered - test whether an uprobe is currently active + */ +bool rv_uprobe_is_registered(const struct rv_uprobe *p) +{ + return p && p->uprobe; +} +EXPORT_SYMBOL_GPL(rv_uprobe_is_registered); + +/** + * rv_uprobe_unregister - synchronously unregister a uprobe + */ +void rv_uprobe_unregister(struct rv_uprobe *p) +{ + if (!p || !p->uprobe) + return; + + rv_uprobe_unregister_nosync(p); + rv_uprobe_sync(); +} +EXPORT_SYMBOL_GPL(rv_uprobe_unregister); + +/** + * rv_uprobe_unregister_nosync - dequeue an uprobe without waiting + */ +void rv_uprobe_unregister_nosync(struct rv_uprobe *p) +{ + if (!p || !p->uprobe) + return; + + uprobe_unregister_nosync(p->uprobe, &p->uc); + p->uprobe =3D NULL; + p->inode =3D NULL; +} +EXPORT_SYMBOL_GPL(rv_uprobe_unregister_nosync); + +/** + * rv_uprobe_sync - wait for all in-flight uprobe handlers to complete + */ +void rv_uprobe_sync(void) +{ + uprobe_unregister_sync(); +} +EXPORT_SYMBOL_GPL(rv_uprobe_sync); --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-177.mta0.migadu.com (out-177.mta0.migadu.com [91.218.175.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C570140929B for ; Wed, 8 Jul 2026 15:39:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.177 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525161; cv=none; b=VHKPtSov3GH+aWhfeCNyaOjdovUBk9XTvQyot+aoP0Q0WjjI55UkSmKgT4wGaiTqAQHd8gFWtHzNEZ9VutOxU3vGnzjwhmabxqyJZ9MwOUya2RbO/LjHQUgubarvQos+mYRm+aQ/SIpbiVB9ah1tnB6c/bLEo0XrjpIkTqf+P5s= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525161; c=relaxed/simple; bh=b8J4EWS0MTPJLZeOj6syvuNLuuEaTuiDf1EU1w94kcs=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=On0SSJdCQt02wogQNaojSWnIxwKANr40eaxmGfkroCUFtUAzXVQm5J5V/w4uvXpgPicNRUBYo0ITh6Ve4rlfPUtF8JxGpZZor31aGnqk9CG4TbKwZvq9ngCmmeYmSvNpzDRidmPRHA76p5/yUQj3sNdqCk/Thry9CoRwHcsEMhQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=ArQg6296; arc=none smtp.client-ip=91.218.175.177 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="ArQg6296" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525157; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=CA/mdAhuW+oHrSF/luPt/Kua0sHUMzh/xl4cARWqze4=; b=ArQg6296nLnMoh9a1BJHFCMTxmPQGvVWK03oJdHmfkAWc0ExovNyr/gAJ4wavhrK+rF/Oe kOseUOONmVC0Zwm9z0VqDdFFBLnQ2DCQEoadKAJpTNNKn4yYjPIYQJ3BQwie0dDU+1jbqd nN9X+4SXw7mjeEnYJQF0M78m/VQ0K08= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 3/8] rv/tlob: add tlob model DOT file Date: Wed, 8 Jul 2026 23:38:29 +0800 Message-Id: <9a76007a92dfbcb46ff15f0bfde50222ec3fe19c.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Add the Graphviz DOT specification of the tlob hybrid automaton to tools/verification/models/. The model has three states (running, waiting, sleeping), four transitions (switch_in, preempt, wakeup, sleep), and a single clock invariant clk_elapsed < BUDGET_NS() active in all states. Suggested-by: Gabriele Monaco Signed-off-by: Wen Yang --- tools/verification/models/tlob.dot | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tools/verification/models/tlob.dot diff --git a/tools/verification/models/tlob.dot b/tools/verification/models= /tlob.dot new file mode 100644 index 000000000000..a1834daff2ed --- /dev/null +++ b/tools/verification/models/tlob.dot @@ -0,0 +1,22 @@ +digraph state_automaton { + center =3D true; + size =3D "7,11"; + {node [shape =3D plaintext, style=3Dinvis, label=3D""] "__init_running"}; + {node [shape =3D ellipse] "running"}; + {node [shape =3D plaintext] "running"}; + {node [shape =3D plaintext] "waiting"}; + {node [shape =3D plaintext] "sleeping"}; + "__init_running" -> "running"; + "running" -> "running" [ label =3D "start;reset(clk_elapsed)" ]; + "running" [label =3D "running\nclk_elapsed < BUDGET_NS()", color =3D gr= een3]; + "waiting" [label =3D "waiting\nclk_elapsed < BUDGET_NS()"]; + "sleeping" [label =3D "sleeping\nclk_elapsed < BUDGET_NS()"]; + "running" -> "sleeping" [ label =3D "sleep" ]; + "running" -> "waiting" [ label =3D "preempt" ]; + "waiting" -> "running" [ label =3D "switch_in" ]; + "sleeping" -> "waiting" [ label =3D "wakeup" ]; + { rank =3D min ; + "__init_running"; + "running"; + } +} --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-184.mta0.migadu.com (out-184.mta0.migadu.com [91.218.175.184]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8088F42DA57 for ; Wed, 8 Jul 2026 15:39:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.184 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525164; cv=none; b=NfEcRmv5yrbRLM5XTPd1cFmgvCffzbe31J0ZKe9aDs8jBGi2PqWfr3lm/9irK89ZOJOjx9GjJT69UHUigRBtNjKWV2hjZHyWc+EzKIOQHKFjL+UpNfFSkYQaqO09PlM6v3RTSn0akIuDho0Pafzdz7ZGuhkkHtrkxwrut546qMg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525164; c=relaxed/simple; bh=nY4SWFLYIixvCSlJRfHv8cZQPX48Q8Y6mEE2vEUraJk=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=MzDb3VlxCm9z0P+/T0v+glmhi0PqGdlhrmI2e4lVYqwVzQv7/OyI/mMXfsvnUb6Gl1eEQjzKUNszTkKdCuA5bkJ7wKQgOm37HVTVaplci9108ICgCFpRm2Wik8NOSOz8Th97gwRku2yWmSZDoe9oubGuZcG31tAGcou29IilJ48= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=TxQ4j7E0; arc=none smtp.client-ip=91.218.175.184 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="TxQ4j7E0" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525160; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=CqHkdl+60f9/63R7GCGJP3HdRCb/L+LlnCxXcUtxWKI=; b=TxQ4j7E0px47pixPt1zlw3iYUqWA6Jfiv0GYUrI2jo1KYhOKQs6NRqG2YCfh5ttb0/dsSo t6/tjP7zthfbtNLfVNBsa3LLmvHrK149eEsuqGkJxLGt/eYI7xLXecMcfJMoLkvZ/uQxWX RsNNgNZTWIGsAyElraYScqQRoGF+3Ww= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 4/8] rv/ha: fix ha_invariant_passed_ns silent bypass of invariant check Date: Wed, 8 Jul 2026 23:38:30 +0800 Message-Id: In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang When env_store is U64_MAX (its initial sentinel value), ha_invariant_passed_ns() returns 0 immediately without initializing env_store to the current clock. Subsequent calls to ha_check_invariant_ns() then find env_store still at U64_MAX, causing the elapsed comparison to wrap and always report the invariant as satisfied, silently masking any violations. Fix by calling ha_reset_clk_ns() to establish the guard on the first invocation instead of returning early. Apply the same fix to ha_invariant_passed_jiffy(). Signed-off-by: Wen Yang --- include/rv/ha_monitor.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h index 83199f90afe8..dddf5694bcc8 100644 --- a/include/rv/ha_monitor.h +++ b/include/rv/ha_monitor.h @@ -375,12 +375,12 @@ static inline bool ha_check_invariant_ns(struct ha_mo= nitor *ha_mon, static inline u64 ha_invariant_passed_ns(struct ha_monitor *ha_mon, enum e= nvs env, u64 expire, u64 time_ns) { - u64 passed =3D 0; + u64 passed; =20 if (env < 0 || env >=3D ENV_MAX_STORED) return 0; if (ha_monitor_env_invalid(ha_mon, env)) - return 0; + ha_reset_clk_ns(ha_mon, env, time_ns); passed =3D ha_get_env(ha_mon, env, time_ns); ha_set_invariant_ns(ha_mon, env, expire - passed, time_ns); return passed; @@ -414,12 +414,12 @@ static inline bool ha_check_invariant_jiffy(struct ha= _monitor *ha_mon, static inline u64 ha_invariant_passed_jiffy(struct ha_monitor *ha_mon, enu= m envs env, u64 expire, u64 time_ns) { - u64 passed =3D 0; + u64 passed; =20 if (env < 0 || env >=3D ENV_MAX_STORED) return 0; if (ha_monitor_env_invalid(ha_mon, env)) - return 0; + ha_reset_clk_jiffy(ha_mon, env); passed =3D ha_get_env(ha_mon, env, time_ns); ha_set_invariant_jiffy(ha_mon, env, expire - passed); return passed; --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-189.mta0.migadu.com (out-189.mta0.migadu.com [91.218.175.189]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 2023643079F for ; Wed, 8 Jul 2026 15:39:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.189 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525166; cv=none; b=Ap3yuL5yx1q88XghfJXc8v0k9nZVgYzQyC4v+WdRlO20Qb29DjRq8eh0OeV5wbH+QEROOFeo36mf7dDWUP9s3NU8S5FpXSF8l2sV8JjTV24HEqDZKAlriFru8tSa6736JQIdgd7flw1eHc9gcQgaYi5Tsnd/7Iy2mRqPoKwaGgM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525166; c=relaxed/simple; bh=7+DF/aJEBcKDiuQzH1z8CrBJbpc3lfiLrZOiOABAHi0=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=Sa3nIjpjTNyNqiJICpitTlCxOh6keyPZReeag2Nq9tpUUlLJyalH7RyeqlKbF8gwssDXWjVx6cYVTJoRUwCo0sUMbD9WqXLfLk8jxl1jo1sYo4zirS9fo32Ay6vn2WUttirR8MmazuXPyFkgi+M9kWIk7rbPeaIHbYt74RKmNag= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=RNorSGBr; arc=none smtp.client-ip=91.218.175.189 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="RNorSGBr" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525163; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=11wI0rEoJdRnLQYyIux2fDqtgUfoApLYPxW50UVagiQ=; b=RNorSGBrrTOJfrJwOvVI3HSrLRr6ilvLh9EM/dUGO97F2G6RPbW2GqRmoQOi5/qgEfpfxI +BiACG84GPVbqK82kE8qTJRYX8S9bbKq44y/TOUZesatgShNB2myu5AYnkiEau6R/veGMO jB90a2SGJ8RE+iwL5PN9WU4v0WnARVA= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 5/8] rv/ha: make da_monitor_reset_hook and EVENT_NONE_LBL overridable Date: Wed, 8 Jul 2026 23:38:31 +0800 Message-Id: In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Wrap both definitions with #ifndef guards so HA-based monitors can substitute their own implementations before including this header. tlob uses this to define a reset hook that cancels per-task hrtimers on monitor teardown. Overrides must still call ha_monitor_reset_env() or cancel outstanding timers to avoid timer UAF. No behaviour change for monitors that do not override either macro. Reviewed-by: Gabriele Monaco Signed-off-by: Wen Yang --- include/rv/ha_monitor.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/rv/ha_monitor.h b/include/rv/ha_monitor.h index dddf5694bcc8..6d526eef8348 100644 --- a/include/rv/ha_monitor.h +++ b/include/rv/ha_monitor.h @@ -36,7 +36,15 @@ static bool ha_monitor_handle_constraint(struct da_monit= or *da_mon, da_id_type id); #define da_monitor_event_hook ha_monitor_handle_constraint #define da_monitor_init_hook ha_monitor_init_env + +/* + * Allow monitors to override da_monitor_reset_hook before including this + * header. The override must still call ha_monitor_reset_env() or cancel + * timers explicitly. + */ +#ifndef da_monitor_reset_hook #define da_monitor_reset_hook ha_monitor_reset_env +#endif #define da_monitor_sync_hook() synchronize_rcu() =20 #if !defined(HA_SKIP_AUTO_CLEANUP) && RV_MON_TYPE =3D=3D RV_MON_PER_TASK @@ -75,7 +83,9 @@ _Static_assert(offsetof(struct ha_monitor, da_mon) =3D=3D= 0, #define ENV_INVALID_VALUE U64_MAX /* Error with no event occurs only on timeouts */ #define EVENT_NONE EVENT_MAX +#ifndef EVENT_NONE_LBL #define EVENT_NONE_LBL "none" +#endif #define ENV_BUFFER_SIZE 64 =20 #ifdef CONFIG_RV_REACTORS --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-172.mta0.migadu.com (out-172.mta0.migadu.com [91.218.175.172]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 769994307BB for ; Wed, 8 Jul 2026 15:39:27 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.172 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525171; cv=none; b=QCnI36mgNN2XSLISV3VemLePMuv+BVIFPSIdBTr+rSRldHNc0vRlPLSFqdu7DY6OVLB29k0jF3/S8Xl3JFPwkwErdB1ttblESsvH7WZCSpfl9ciaFn3Fv0zuO3obu4Iwbq9VNWh7VrYDGXXRUX4RQmXQR2b/NUCfEHU7g7PiSUs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525171; c=relaxed/simple; bh=IduFOnSpLrmX00ZXna7Bgahm7/1OaRGxIxuPbfwvSvQ=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=VVF7ZAsPShUu7alJYO0GtKSC677pgHDu7xpRlZNIbJS8dPz7kuqweCM9nY6NPwhjaTX4z9A1+p8nxPu5EEJB4paL+zOYW1qmHo4uhMmBF/hPeb+11CB9skwoS6Z7RMe5mAFePnoC/OOtCYZ3PnlQ7DpybZuyQJruUtmMpW+KWeo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=EenfgAnt; arc=none smtp.client-ip=91.218.175.172 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="EenfgAnt" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525165; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=8xRcpJn0GSoaGlO07mbKiSOMW7jjXXCjV9jUXhHgVx4=; b=EenfgAntTSRoW9ms/oxvs8C/gz5YD5/sBBHagMshvIYqGVG/nFcJL9k9y7nkVsbNh58WBb GJgCkYWFQtuQBvKAKroZXiRSvx3xaCIQL4q8gE2eivJi4WihRZxcRL8K/21O+1dGVTtbKQ 0tlLhgp18vx/EHYu7rMPhMxTsc0EkI0= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 6/8] rv/tlob: add tlob hybrid automaton monitor Date: Wed, 8 Jul 2026 23:38:32 +0800 Message-Id: <09d656759685edcb0fbeead775300094e7ca5002.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang tlob (task latency over budget) is a per-task hybrid automaton RV monitor that tracks wall-clock time across a user-delimited code section and emits an error when elapsed time exceeds a configurable budget. The automaton has three states (running, waiting, sleeping) driven by sched_switch and sched_wakeup tracepoints, with a single clock invariant enforced by a per-task HRTIMER_MODE_REL_HARD timer. On budget expiry the monitor records a per-state time breakdown (running_ns, waiting_ns, sleeping_ns) before emitting error_env_tlob. Uprobe pairs are registered through a tracefs monitor file as "p PATH:OFFSET_START OFFSET_STOP threshold=3DNS" so arbitrary code sections can be delimited without modifying the target binary. Per-task state is allocated from a pre-allocated llist pool so the uprobe entry handler incurs no dynamic allocation inside the [T0, T1] measurement window. DA_ALLOC_POOL is used for the same reason on the da_monitor_storage side. tlob_start_lock is a spinlock_t because the uprobe handler runs under rcu_read_lock_trace() (Tasks Trace SRCU), which permits sleeping on PREEMPT_RT where spinlock_t is an rt_mutex. da_get_target_by_id() is wrapped in scoped_guard(rcu) because hash_for_each_possible_rcu() requires an RCU read-side critical section independent of the lock. Signed-off-by: Wen Yang --- Documentation/trace/rv/index.rst | 1 + Documentation/trace/rv/monitor_tlob.rst | 177 ++++ kernel/trace/rv/Kconfig | 1 + kernel/trace/rv/Makefile | 1 + kernel/trace/rv/monitors/tlob/Kconfig | 12 + kernel/trace/rv/monitors/tlob/tlob.c | 969 +++++++++++++++++++++ kernel/trace/rv/monitors/tlob/tlob.h | 151 ++++ kernel/trace/rv/monitors/tlob/tlob_trace.h | 52 ++ kernel/trace/rv/rv_trace.h | 1 + 9 files changed, 1365 insertions(+) create mode 100644 Documentation/trace/rv/monitor_tlob.rst create mode 100644 kernel/trace/rv/monitors/tlob/Kconfig create mode 100644 kernel/trace/rv/monitors/tlob/tlob.c create mode 100644 kernel/trace/rv/monitors/tlob/tlob.h create mode 100644 kernel/trace/rv/monitors/tlob/tlob_trace.h diff --git a/Documentation/trace/rv/index.rst b/Documentation/trace/rv/inde= x.rst index 29769f06bb0f..1501545b5f08 100644 --- a/Documentation/trace/rv/index.rst +++ b/Documentation/trace/rv/index.rst @@ -16,5 +16,6 @@ Runtime Verification monitor_wwnr.rst monitor_sched.rst monitor_rtapp.rst + monitor_tlob.rst monitor_stall.rst monitor_deadline.rst diff --git a/Documentation/trace/rv/monitor_tlob.rst b/Documentation/trace/= rv/monitor_tlob.rst new file mode 100644 index 000000000000..327c9832bf31 --- /dev/null +++ b/Documentation/trace/rv/monitor_tlob.rst @@ -0,0 +1,177 @@ +.. SPDX-License-Identifier: GPL-2.0 + +Monitor tlob +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +- Name: tlob - task latency over budget +- Type: per-object hybrid automaton (RV_MON_PER_OBJ) +- Author: Wen Yang + +Description +----------- + +The tlob monitor tracks per-task elapsed wall-clock time (CLOCK_MONOTONIC, +spanning running, waiting, and sleeping states) and reports a violation wh= en +the monitored task exceeds a configurable per-invocation budget threshold. + +The monitor implements a three-state hybrid automaton with a single clock +environment variable ``clk_elapsed``. The clock invariant +``clk_elapsed < BUDGET_NS()`` is active in all three states; when it is +violated the HA timer fires and the framework emits ``error_env_tlob`` +then calls ``da_monitor_reset()`` automatically:: + + | (initial, via task_start) + v + +--------------+ + | running | <-----------+ + +--------------+ | + | | | + sleep preempt switch_in + | | | + v v | + +---------+ +---------+ | + | sleeping| | waiting | -------+ + +---------+ +---------+ + | ^ + +---wakeup---+ + + Key transitions: + running --(sleep)------> sleeping (task blocks waiting for a resour= ce) + running --(preempt)----> waiting (task preempted, back in runqueue) + sleeping --(wakeup)-----> waiting (resource available, enters runqu= eue) + waiting --(switch_in)--> running (scheduler picks task, back on CP= U) + + ``tlob_start_task()`` calls ``da_handle_start_run_event(task->pid, ws, s= tart_tlob)``. + The ``start_tlob`` self-loop on the ``running`` state triggers + ``ha_setup_invariants()``, which resets ``clk_elapsed`` and arms the bud= get + timer automatically. ``tlob_stop_task()`` cancels the HA timer synchron= ously + via ``ha_cancel_timer_sync()``, then calls ``da_monitor_reset()``. + +The non-running condition (monitor not yet started or reset after a +stop/violation) is handled implicitly by the RV framework +(``da_mon->monitoring =3D=3D 0``) - it is not an explicit DA state. + +Per-task state lives in ``struct tlob_task_state`` which is stored as +``monitor_target`` in the framework's ``da_monitor_storage``, indexed by +pid. The per-invocation ``threshold_ns`` is read via +``ha_get_target(ha_mon)->threshold_ns`` inside the HA constraint functions, +following the same pattern as the ``nomiss`` monitor. + +Usage +----- + +tracefs interface (uprobe-based external monitoring) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``monitor`` tracefs file instruments an unmodified binary via uprobes. +The format follows the ftrace ``uprobe_events`` convention (``PATH:OFFSET`` +for the probe location, ``key=3Dvalue`` for configuration parameters):: + + p PATH:OFFSET_START OFFSET_STOP threshold=3DNS + +The uprobe at ``OFFSET_START`` fires ``tlob_start_task()``; the uprobe at +``OFFSET_STOP`` fires ``tlob_stop_task()``. Both offsets are ELF file +offsets of entry points in ``PATH``. ``PATH`` may contain ``:``; the last +``:`` in the ``PATH:OFFSET_START`` token is the separator. + +To remove a binding, use ``-PATH:OFFSET_START``:: + + echo 1 > /sys/kernel/tracing/rv/monitors/tlob/enable + + echo "p /usr/bin/myapp:0x12a0 0x12f0 threshold=3D5000000" \ + > /sys/kernel/tracing/rv/monitors/tlob/monitor + + # Remove a binding + echo "-/usr/bin/myapp:0x12a0" > /sys/kernel/tracing/rv/monitors/tlob/mon= itor + + # List registered bindings + cat /sys/kernel/tracing/rv/monitors/tlob/monitor + + # Read violations from the trace buffer + cat /sys/kernel/tracing/trace + +Violation tracepoints +~~~~~~~~~~~~~~~~~~~~~ + +Two tracepoints are emitted together on a budget violation: + +``error_env_tlob`` + Standard HA clock-invariant tracepoint (emitted by the RV framework). + Fields: ``id`` (task pid), ``state``, ``event`` (``"budget_exceeded"``), + ``env`` (``"clk_elapsed"``). + +``detail_env_tlob`` + Tlob-specific breakdown of elapsed time per DA state. + Fields: ``id`` (task pid), ``threshold_ns``, ``running_ns``, + ``waiting_ns``, ``sleeping_ns``. + + Use ``detail_env_tlob`` to diagnose *which phase* consumed the budget: + high ``sleeping_ns`` indicates I/O latency; high ``waiting_ns`` indicates + scheduler pressure; high ``running_ns`` indicates a compute overrun. + +Example: correlate the two tracepoints to see the breakdown:: + + trace-cmd record -e error_env_tlob -e detail_env_tlob & + # ... run workload ... + trace-cmd report + +tracefs files +~~~~~~~~~~~~~ + +The following files are specific to tlob under +``/sys/kernel/tracing/rv/monitors/tlob/``: + +``monitor`` (rw) + Write ``p PATH:OFFSET_START OFFSET_STOP threshold=3DNS`` + to bind two entry uprobes. Write ``-PATH:OFFSET_START`` to remove a + binding. Read to list registered bindings in the same format. + See the `tracefs interface (uprobe-based external monitoring)`_ section = above. + +Kernel API +---------- + +``tlob_start_task`` and ``tlob_stop_task`` are the implementation-level +functions called by the uprobe entry/exit handlers; the interface is +driven from userspace. + +.. kernel-doc:: kernel/trace/rv/monitors/tlob/tlob.c + :functions: tlob_start_task tlob_stop_task + +``tlob_start_task(task, threshold_ns)`` + Begin monitoring *task* with a total latency budget of *threshold_ns* + nanoseconds. Allocates per-task state, sets initial DA state to + ``running``, resets ``clk_elapsed``, and arms the HA budget timer. + Returns 0, -ENODEV (monitor disabled), -ERANGE (threshold out of range), + -EALREADY (already monitoring), -ENOSPC (at capacity), or -ENOMEM. + +``tlob_stop_task(task)`` + Stop monitoring *task*. Synchronously cancels the HA timer via + ``ha_cancel_timer_sync()``, checks ``da_monitoring()`` to determine outc= ome. + Returns 0 (clean stop, within budget), -EOVERFLOW (budget was exceeded), + -ESRCH (not monitored), or -EAGAIN (concurrent stop racing). + +Design notes +------------ + +Limitations: + +- The initial DA state is always ``running``, set by feeding the synthetic + event ``switch_in_tlob`` to ``da_handle_start_event()``. Monitoring a n= on-current + task that is already in waiting or sleeping state at call time misclassi= fies + the first interval as ``running_ns``. +- ``TASK_STOPPED`` and ``TASK_TRACED`` carry ``prev_state !=3D 0`` and are + therefore counted as ``sleeping_ns``, indistinguishable from + I/O-blocked time. +- ``sched_wakeup_new`` is not hooked. In practice this is not an issue + because ``tlob_start_task`` is always called from a running context. + +Specification +------------- + +Graphviz DOT file in tools/verification/models/tlob.dot. + +KUnit tests under ``kernel/trace/rv/monitors/tlob/tlob_kunit.c`` +(CONFIG_TLOB_KUNIT_TEST). + +User-space integration tests under ``tools/testing/selftests/verification/= `` +(requires CONFIG_RV_MON_TLOB=3Dy and root). diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig index 5bad1d63f411..ed0dc8241691 100644 --- a/kernel/trace/rv/Kconfig +++ b/kernel/trace/rv/Kconfig @@ -90,6 +90,7 @@ source "kernel/trace/rv/monitors/deadline/Kconfig" source "kernel/trace/rv/monitors/nomiss/Kconfig" # Add new deadline monitors here =20 +source "kernel/trace/rv/monitors/tlob/Kconfig" # Add new monitors here =20 config RV_REACTORS diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile index f139b904bea3..ae59e97f8682 100644 --- a/kernel/trace/rv/Makefile +++ b/kernel/trace/rv/Makefile @@ -20,6 +20,7 @@ obj-$(CONFIG_RV_MON_OPID) +=3D monitors/opid/opid.o obj-$(CONFIG_RV_MON_STALL) +=3D monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) +=3D monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) +=3D monitors/nomiss/nomiss.o +obj-$(CONFIG_RV_MON_TLOB) +=3D monitors/tlob/tlob.o # Add new monitors here obj-$(CONFIG_RV_UPROBE) +=3D rv_uprobe.o obj-$(CONFIG_RV_REACTORS) +=3D rv_reactors.o diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monito= rs/tlob/Kconfig new file mode 100644 index 000000000000..aa43382073d2 --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/Kconfig @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +config RV_MON_TLOB + bool "tlob monitor" + depends on RV && UPROBES && HIGH_RES_TIMERS + select HA_MON_EVENTS_ID + select RV_UPROBE + help + Enable the tlob (task latency over budget) hybrid-automaton RV + monitor. tlob tracks per-task elapsed wall-clock time across a + user-delimited code section and emits error_env_tlob when the + elapsed time exceeds a configurable per-invocation budget. diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitor= s/tlob/tlob.c new file mode 100644 index 000000000000..b45e84195131 --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/tlob.c @@ -0,0 +1,969 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * tlob: task latency over budget monitor + * + * Track the elapsed wall-clock time of a marked code path and detect when + * a monitored task exceeds its per-task latency budget. CLOCK_MONOTONIC + * is used so both on-CPU and off-CPU time count toward the budget. + * + * On a budget violation, two tracepoints are emitted from the hrtimer + * callback: error_env_tlob signals the violation, and detail_env_tlob + * provides a per-state time breakdown (running_ns, waiting_ns, sleeping_n= s) + * that pinpoints whether the overrun occurred in the running, waiting, + * or sleeping state. + * + * The monitor uses RV_MON_PER_OBJ: per-task state (struct tlob_task_state) + * is stored as monitor_target in the framework's hash table. + * + * One HA clock invariant is enforced: + * clk_elapsed < BUDGET_NS() (active in all states) + * + * Copyright (C) 2026 Wen Yang + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_NAME "tlob" + +#include +#include + +/* + * Per-task latency monitoring state. One instance per monitoring window. + * Stored as monitor_target in da_monitor_storage; freed via call_rcu. + */ +enum tlob_acc_idx { + TLOB_ACC_RUNNING, + TLOB_ACC_WAITING, + TLOB_ACC_SLEEPING, + TLOB_ACC_MAX, +}; + +struct tlob_task_state { + struct task_struct *task; /* via get_task_struct */ + u64 threshold_ns; /* budget in nanoseconds */ + + /* 1 =3D cleanup claimed; ha_setup_invariants won't restart the timer. */ + atomic_t stopping; + + /* Serialises accs_ns[]; held briefly (hardirq-safe). */ + raw_spinlock_t entry_lock; + u64 accs_ns[TLOB_ACC_MAX]; /* per-state elapsed ns */ + ktime_t last_ts; + + struct rcu_head rcu; + /* Free-list node; active only between call_rcu() return and next alloc. = */ + struct llist_node free_node; +}; + +#define RV_MON_TYPE RV_MON_PER_OBJ +#define HA_TIMER_TYPE HA_TIMER_HRTIMER + +typedef struct tlob_task_state *monitor_target; + +static inline void tlob_reset_notify(struct da_monitor *da_mon); +#define da_monitor_reset_hook tlob_reset_notify + +static inline void tlob_extra_cleanup(struct da_monitor *da_mon); +#define da_extra_cleanup tlob_extra_cleanup + +#define EVENT_NONE_LBL "budget_exceeded" + +#include "tlob.h" + +#define DA_MON_POOL_SIZE TLOB_MAX_MONITORED + +#include + +/* + * Called from da_monitor_reset() on both normal stop and hrtimer expiry. + * On violation (stopping=3D=3D0), emits detail_env_tlob. + */ +static inline void tlob_reset_notify(struct da_monitor *da_mon) +{ + struct ha_monitor *ha_mon =3D to_ha_monitor(da_mon); + struct tlob_task_state *ws; + + ha_monitor_reset_env(da_mon); + + if (!trace_detail_env_tlob_enabled()) + return; + + ws =3D ha_get_target(ha_mon); + if (!ws) + return; + + /* + * Emit per-state breakdown on budget violation only. + * stopping=3D=3D0: timer callback owns this path (genuine overrun). + * stopping=3D=3D1: normal stop claimed ownership first; skip. + */ + if (!atomic_read(&ws->stopping)) { + unsigned int curr_state =3D READ_ONCE(da_mon->curr_state); + u64 accs[TLOB_ACC_MAX], partial_ns; + unsigned long flags; + + /* + * Snapshot accumulators; partial_ns covers curr_state time + * not yet folded in (transition-out pending). + */ + raw_spin_lock_irqsave(&ws->entry_lock, flags); + partial_ns =3D ktime_get_ns() - ktime_to_ns(ws->last_ts); + accs[TLOB_ACC_RUNNING] =3D ws->accs_ns[TLOB_ACC_RUNNING] + + (curr_state =3D=3D running_tlob ? partial_ns : 0); + accs[TLOB_ACC_WAITING] =3D ws->accs_ns[TLOB_ACC_WAITING] + + (curr_state =3D=3D waiting_tlob ? partial_ns : 0); + accs[TLOB_ACC_SLEEPING] =3D ws->accs_ns[TLOB_ACC_SLEEPING] + + (curr_state =3D=3D sleeping_tlob ? partial_ns : 0); + raw_spin_unlock_irqrestore(&ws->entry_lock, flags); + + trace_detail_env_tlob(da_get_id(da_mon), ws->threshold_ns, + accs[TLOB_ACC_RUNNING], + accs[TLOB_ACC_WAITING], + accs[TLOB_ACC_SLEEPING]); + } +} + +#define BUDGET_NS(ha_mon) (ha_get_target(ha_mon)->threshold_ns) + +/* HA constraint functions (called by ha_monitor_handle_constraint) */ + +static u64 ha_get_env(struct ha_monitor *ha_mon, enum envs_tlob env, + u64 time_ns) +{ + if (env =3D=3D clk_elapsed_tlob) + return ha_get_clk_ns(ha_mon, env, time_ns); + return ENV_INVALID_VALUE; +} + +/* + * ha_verify_invariants - clk_elapsed < BUDGET_NS must hold in all states. + * + * The invariant is uniform across running/waiting/sleeping; check it + * unconditionally rather than enumerating each state. + */ +static inline bool ha_verify_invariants(struct ha_monitor *ha_mon, + enum states curr_state, enum events event, + enum states next_state, u64 time_ns) +{ + return ha_check_invariant_ns(ha_mon, clk_elapsed_tlob, time_ns); +} + +/* + * Convert invariant (deadline) to guard (reset anchor) on state transitio= ns. + * + * The conversion is identical for every departing state; skip only self-l= oops. + */ +static inline void ha_convert_inv_guard(struct ha_monitor *ha_mon, + enum states curr_state, enum events event, + enum states next_state, u64 time_ns) +{ + if (curr_state !=3D next_state) + ha_inv_to_guard(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon), time_ns); +} + +/* No per-event guard conditions for tlob; invariants suffice. */ +static inline bool ha_verify_guards(struct ha_monitor *ha_mon, + enum states curr_state, enum events event, + enum states next_state, u64 time_ns) +{ + return true; +} + +/* + * Guard on stopping: a sched_switch arriving after ha_cancel_timer_sync() + * would re-arm the timer and trigger an ODEBUG "activate active" splat. + * _acquire pairs with cmpxchg_release in tlob_stop_task. + */ +static inline void ha_setup_invariants(struct ha_monitor *ha_mon, + enum states curr_state, enum events event, + enum states next_state, u64 time_ns) +{ + if (atomic_read_acquire(&ha_get_target(ha_mon)->stopping)) + return; + if (next_state < state_max_tlob) + ha_start_timer_ns(ha_mon, clk_elapsed_tlob, BUDGET_NS(ha_mon), time_ns); + else + ha_cancel_timer(ha_mon); +} + +static bool ha_verify_constraint(struct ha_monitor *ha_mon, + enum states curr_state, enum events event, + enum states next_state, u64 time_ns) +{ + if (!ha_verify_invariants(ha_mon, curr_state, event, next_state, time_ns)) + return false; + + ha_convert_inv_guard(ha_mon, curr_state, event, next_state, time_ns); + + if (!ha_verify_guards(ha_mon, curr_state, event, next_state, time_ns)) + return false; + + ha_setup_invariants(ha_mon, curr_state, event, next_state, time_ns); + + return true; +} + +/* + * Pre-allocated pool for tlob_task_state slots. Lock-free llist so that + * tlob_ws_return_cb() (RCU callback) can return slots without acquiring a + * spinlock. Same concurrency model as da_pool_storage (see da_monitor.h). + */ +static struct tlob_task_state *tlob_ws_storage; +static LLIST_HEAD(tlob_ws_free_list); + +static void tlob_ws_return_cb(struct rcu_head *head) +{ + struct tlob_task_state *ws =3D + container_of(head, struct tlob_task_state, rcu); + + llist_add(&ws->free_node, &tlob_ws_free_list); +} + +/* Direct return to free list without RCU delay (ws was never published). = */ +static void tlob_ws_direct_return(struct tlob_task_state *ws) +{ + llist_add(&ws->free_node, &tlob_ws_free_list); +} + +static struct tlob_task_state *tlob_ws_alloc(void) +{ + struct llist_node *node =3D llist_del_first(&tlob_ws_free_list); + + if (!node) + return NULL; + + struct tlob_task_state *ws =3D + llist_entry(node, struct tlob_task_state, free_node); + + memset(ws, 0, sizeof(*ws)); + return ws; +} + +/* 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. + */ +static DEFINE_SPINLOCK(tlob_start_lock); + +/* Per-uprobe-binding state: a start + stop probe pair for one binary regi= on. */ +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. + * + * 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 =3D to_ha_monitor(da_mon); + struct tlob_task_state *ws =3D ha_get_target(ha_mon); + + if (!ws) + return; + + if (atomic_cmpxchg_release(&ws->stopping, 0, 1) !=3D 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 =3D da_get_target_by_id(task->pid); + if (!ws) + return false; + raw_spin_lock_irqsave(&ws->entry_lock, flags); + ws->accs_ns[idx] +=3D ktime_to_ns(ktime_sub(now, ws->last_ts)); + ws->last_ts =3D 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. */ +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 !=3D 0 -> sleep_tlob (running -> sleeping) + * prev, prev_state =3D=3D 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 th= at + * 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 =3D ktime_get(); + bool prev_preempted =3D (prev_state =3D=3D 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 =3D 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. + */ + 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 =3D tlob_ws_alloc(); + if (!ws) + return -ENOSPC; + + ws->task =3D task; + get_task_struct(task); + ws->threshold_ns =3D threshold_ns; + ws->last_ts =3D 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. + */ + 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 =3D da_get_target_by_id(task->pid); + if (!ws) + return -ESRCH; + + da_mon =3D da_get_monitor(task->pid, NULL); + if (unlikely(WARN_ON_ONCE(!da_mon))) + return -ESRCH; + + ha_mon =3D 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) !=3D 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 =3D !da_monitoring(da_mon); + if (!budget_exceeded) + da_monitor_reset(da_mon); + } + da_destroy_storage(task->pid); + + put_task_struct(ws->task); + call_rcu(&ws->rcu, tlob_ws_return_cb); + return budget_exceeded ? -EOVERFLOW : 0; +} +EXPORT_SYMBOL_GPL(tlob_stop_task); + +static int tlob_uprobe_entry_handler(struct uprobe_consumer *self, + struct pt_regs *regs, __u64 *data) +{ + struct tlob_uprobe_binding *b =3D + container_of(self, struct tlob_uprobe_binding, start_probe.uc); + + tlob_start_task(current, b->threshold_ns); + return 0; +} + +static int tlob_uprobe_stop_handler(struct uprobe_consumer *self, + struct pt_regs *regs, __u64 *data) +{ + tlob_stop_task(current); + return 0; +} + +/* + * Register start + stop entry uprobes for a binding. + * Called with tlob_uprobe_mutex held. + */ +static int tlob_add_uprobe(u64 threshold_ns, const char *binpath, + loff_t offset_start, loff_t offset_stop) +{ + struct tlob_uprobe_binding *tmp_b; + char pathbuf[TLOB_MAX_PATH]; + struct inode *inode; + struct path path __free(path_put) =3D {}; + char *canon; + int ret; + + if (binpath[0] !=3D '/') + return -EINVAL; + + struct tlob_uprobe_binding *b __free(kfree) =3D kzalloc_obj(*b, GFP_KERNE= L); + if (!b) + return -ENOMEM; + + b->threshold_ns =3D threshold_ns; + b->offset_start =3D offset_start; + b->offset_stop =3D offset_stop; + + ret =3D kern_path(binpath, LOOKUP_FOLLOW, &path); + if (ret) + return ret; + + if (!d_is_reg(path.dentry)) + return -EINVAL; + + inode =3D d_real_inode(path.dentry); + + /* Reject duplicate start offset for the same binary inode. */ + list_for_each_entry(tmp_b, &tlob_uprobe_list, list) { + if (tmp_b->offset_start =3D=3D offset_start && + rv_uprobe_is_registered(&tmp_b->start_probe) && + tmp_b->start_probe.inode =3D=3D inode) + return -EEXIST; + } + + canon =3D d_path(&path, pathbuf, sizeof(pathbuf)); + if (IS_ERR(canon)) + return PTR_ERR(canon); + strscpy(b->binpath, canon, sizeof(b->binpath)); + + b->start_probe.uc.handler =3D tlob_uprobe_entry_handler; + ret =3D rv_uprobe_register(b->binpath, offset_start, &b->start_probe); + if (ret) + return ret; + + b->stop_probe.uc.handler =3D tlob_uprobe_stop_handler; + ret =3D rv_uprobe_register(b->binpath, offset_stop, &b->stop_probe); + if (ret) { + rv_uprobe_unregister(&b->start_probe); + return ret; + } + + /* + * Do NOT write "b =3D no_free_ptr(b)": the re-assignment restores b, + * causing __free(kfree) to free a live list node on exit. + */ + list_add_tail(&no_free_ptr(b)->list, &tlob_uprobe_list); + return 0; +} + +static int tlob_remove_uprobe_by_key(loff_t offset_start, const char *binp= ath) +{ + struct tlob_uprobe_binding *b, *tmp; + struct path remove_path; + struct inode *inode; + int ret; + + ret =3D kern_path(binpath, LOOKUP_FOLLOW, &remove_path); + if (ret) + return ret; + + inode =3D d_real_inode(remove_path.dentry); + + ret =3D -ENOENT; + list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) { + if (b->offset_start !=3D offset_start) + continue; + if (b->start_probe.inode !=3D inode) + continue; + list_del(&b->list); + /* + * rv_uprobe_sync() may sleep, blocking tlob_monitor_read() on + * tlob_uprobe_mutex. Safe: list_del() above made the binding + * invisible to new readers before we drop the mutex. + */ + rv_uprobe_unregister_nosync(&b->start_probe); + rv_uprobe_unregister_nosync(&b->stop_probe); + rv_uprobe_sync(); + kfree(b); + ret =3D 0; + break; + } + + path_put(&remove_path); + return ret; +} + +static void tlob_remove_all_uprobes(void) +{ + struct tlob_uprobe_binding *b, *tmp; + LIST_HEAD(pending); + + mutex_lock(&tlob_uprobe_mutex); + list_for_each_entry_safe(b, tmp, &tlob_uprobe_list, list) { + list_move(&b->list, &pending); + rv_uprobe_unregister_nosync(&b->start_probe); + rv_uprobe_unregister_nosync(&b->stop_probe); + } + mutex_unlock(&tlob_uprobe_mutex); + + if (list_empty(&pending)) + return; + + /* + * One rv_uprobe_sync() covers all probes dequeued above. + * After this, no handler_chain() iteration can access any consumer. + * The embedded uprobe_consumers in each binding are safe to free. + */ + rv_uprobe_sync(); + + list_for_each_entry_safe(b, tmp, &pending, list) { + list_del(&b->list); + kfree(b); + } +} + +static ssize_t tlob_monitor_read(struct file *file, + char __user *ubuf, + size_t count, loff_t *ppos) +{ + const int line_sz =3D TLOB_MAX_PATH + 128; + struct tlob_uprobe_binding *b; + char *buf; + int n =3D 0, buf_sz, pos =3D 0; + ssize_t ret; + + mutex_lock(&tlob_uprobe_mutex); + list_for_each_entry(b, &tlob_uprobe_list, list) + n++; + + buf_sz =3D (n ? n : 1) * line_sz + 1; + buf =3D kmalloc(buf_sz, GFP_KERNEL); + if (!buf) { + mutex_unlock(&tlob_uprobe_mutex); + return -ENOMEM; + } + + list_for_each_entry(b, &tlob_uprobe_list, list) { + pos +=3D scnprintf(buf + pos, buf_sz - pos, + "p %s:0x%llx 0x%llx threshold=3D%llu\n", + b->binpath, + (unsigned long long)b->offset_start, + (unsigned long long)b->offset_stop, + b->threshold_ns); + } + mutex_unlock(&tlob_uprobe_mutex); + + ret =3D simple_read_from_buffer(ubuf, count, ppos, buf, pos); + kfree(buf); + return ret; +} + +/* + * Parse "p PATH:OFFSET_START OFFSET_STOP threshold=3DNS". + * PATH may contain ':'; the last ':' separates path from offset. + * Returns 0, -EINVAL, or -ERANGE. + */ +static int tlob_parse_uprobe_line(char *buf, u64 *thr_out, + char **path_out, + loff_t *start_out, loff_t *stop_out) +{ + unsigned long long thr =3D 0, stop_val =3D 0; + long long start_val; + char *p, *path_token, *token, *colon; + bool got_stop =3D false, got_thr =3D false; + int n; + + /* Must start with "p " */ + if (buf[0] !=3D 'p' || buf[1] !=3D ' ') + return -EINVAL; + + p =3D buf + 2; + while (*p =3D=3D ' ') + p++; + + /* First space-delimited token is PATH:OFFSET_START */ + path_token =3D strsep(&p, " \t"); + if (!path_token || !*path_token) + return -EINVAL; + + /* Split at last ':' to handle paths that contain ':'. */ + colon =3D strrchr(path_token, ':'); + if (!colon || colon - path_token < 2) + return -EINVAL; + *colon =3D '\0'; + + if (path_token[0] !=3D '/') + return -EINVAL; + + n =3D 0; + if (sscanf(colon + 1, "%lli%n", &start_val, &n) !=3D 1 || n =3D=3D 0) + return -EINVAL; + if (start_val < 0) + return -EINVAL; + + /* Remaining tokens: OFFSET_STOP threshold=3DNS */ + while (p && (token =3D strsep(&p, " \t")) !=3D NULL) { + if (!*token) + continue; + if (strncmp(token, "threshold=3D", 10) =3D=3D 0) { + if (kstrtoull(token + 10, 0, &thr)) + return -EINVAL; + if (thr < TLOB_MIN_THRESHOLD_NS || thr > TLOB_MAX_THRESHOLD_NS) + return -ERANGE; + got_thr =3D true; + } else if (!got_stop) { + long long sv; + + n =3D 0; + if (sscanf(token, "%lli%n", &sv, &n) !=3D 1 || n =3D=3D 0) + return -EINVAL; + if (sv < 0) + return -EINVAL; + stop_val =3D (unsigned long long)sv; + got_stop =3D true; + } else { + return -EINVAL; + } + } + + if (!got_stop || !got_thr) + return -EINVAL; + if (start_val =3D=3D (long long)stop_val) + return -EINVAL; + + *thr_out =3D thr; + *path_out =3D path_token; + *start_out =3D (loff_t)start_val; + *stop_out =3D (loff_t)stop_val; + return 0; +} + +/* + * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention). + */ +VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, char **path_out, + loff_t *start_out) +{ + char *binpath, *colon; + long long off; + int n =3D 0; + + if (buf[0] !=3D '-') + return -EINVAL; + binpath =3D buf + 1; + if (binpath[0] !=3D '/') + return -EINVAL; + colon =3D strrchr(binpath, ':'); + if (!colon || colon - binpath < 2) + return -EINVAL; + *colon =3D '\0'; + if (sscanf(colon + 1, "%lli%n", &off, &n) !=3D 1 || n =3D=3D 0) + return -EINVAL; + if (off < 0) + return -EINVAL; + *path_out =3D binpath; + *start_out =3D (loff_t)off; + return 0; +} + +VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf) +{ + loff_t offset_start, offset_stop; + u64 threshold_ns; + char *binpath; + int ret; + + if (buf[0] =3D=3D '-') { + ret =3D tlob_parse_remove_line(buf, &binpath, &offset_start); + if (ret) + return ret; + mutex_lock(&tlob_uprobe_mutex); + ret =3D tlob_remove_uprobe_by_key(offset_start, binpath); + mutex_unlock(&tlob_uprobe_mutex); + return ret; + } + ret =3D tlob_parse_uprobe_line(buf, &threshold_ns, &binpath, + &offset_start, &offset_stop); + if (ret) + return ret; + mutex_lock(&tlob_uprobe_mutex); + ret =3D tlob_add_uprobe(threshold_ns, binpath, offset_start, offset_stop); + mutex_unlock(&tlob_uprobe_mutex); + return ret; +} +EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe); + +static ssize_t tlob_monitor_write(struct file *file, + const char __user *ubuf, + size_t count, loff_t *ppos) +{ + char buf[TLOB_MAX_PATH + 128]; + + if (count >=3D sizeof(buf)) + return -EINVAL; + if (copy_from_user(buf, ubuf, count)) + return -EFAULT; + buf[count] =3D '\0'; + if (count > 0 && buf[count - 1] =3D=3D '\n') + buf[count - 1] =3D '\0'; + return tlob_create_or_delete_uprobe(buf) ?: (ssize_t)count; +} + +static const struct file_operations tlob_monitor_fops =3D { + .open =3D simple_open, + .read =3D tlob_monitor_read, + .write =3D tlob_monitor_write, + .llseek =3D noop_llseek, +}; + +static int __tlob_init_monitor(void) +{ + unsigned int i; + int retval; + + tlob_ws_storage =3D kcalloc(TLOB_MAX_MONITORED, sizeof(*tlob_ws_storage), + GFP_KERNEL); + if (!tlob_ws_storage) + return -ENOMEM; + + for (i =3D 0; i < TLOB_MAX_MONITORED; i++) + llist_add(&tlob_ws_storage[i].free_node, &tlob_ws_free_list); + + retval =3D ha_monitor_init(); + if (retval) { + kfree(tlob_ws_storage); + tlob_ws_storage =3D NULL; + init_llist_head(&tlob_ws_free_list); + return retval; + } + + rv_this.enabled =3D 1; + return 0; +} + +static void __tlob_destroy_monitor(void) +{ + rv_this.enabled =3D 0; + tlob_remove_all_uprobes(); + ha_monitor_destroy(); + init_llist_head(&tlob_ws_free_list); + kfree(tlob_ws_storage); + tlob_ws_storage =3D NULL; +} + +static int tlob_enable_hooks(void) +{ + rv_attach_trace_probe("tlob", sched_switch, handle_sched_switch); + rv_attach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup); + rv_attach_trace_probe("tlob", sched_process_exit, handle_sched_process_ex= it); + return 0; +} + +static void tlob_disable_hooks(void) +{ + rv_detach_trace_probe("tlob", sched_switch, handle_sched_switch); + rv_detach_trace_probe("tlob", sched_wakeup, handle_sched_wakeup); + rv_detach_trace_probe("tlob", sched_process_exit, handle_sched_process_ex= it); +} + +static int enable_tlob(void) +{ + int retval; + + retval =3D __tlob_init_monitor(); + if (retval) + return retval; + + return tlob_enable_hooks(); +} + +static void disable_tlob(void) +{ + tlob_disable_hooks(); + __tlob_destroy_monitor(); +} + +static struct rv_monitor rv_this =3D { + .name =3D "tlob", + .description =3D "Per-task latency-over-budget monitor.", + .enable =3D enable_tlob, + .disable =3D disable_tlob, + .reset =3D da_monitor_reset_all, + .enabled =3D 0, +}; + +static int __init register_tlob(void) +{ + int ret; + + ret =3D rv_register_monitor(&rv_this, NULL); + if (ret) + return ret; + + if (rv_this.root_d) { + if (!rv_create_file("monitor", RV_MODE_WRITE, rv_this.root_d, NULL, + &tlob_monitor_fops)) { + rv_unregister_monitor(&rv_this); + return -ENOMEM; + } + } + + return 0; +} + +static void __exit unregister_tlob(void) +{ + rv_unregister_monitor(&rv_this); +} + +module_init(register_tlob); +module_exit(unregister_tlob); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Wen Yang "); +MODULE_DESCRIPTION("tlob: task latency over budget per-task monitor."); diff --git a/kernel/trace/rv/monitors/tlob/tlob.h b/kernel/trace/rv/monitor= s/tlob/tlob.h new file mode 100644 index 000000000000..fceeba748c85 --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/tlob.h @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _RV_TLOB_H +#define _RV_TLOB_H + +/* + * C representation of the tlob hybrid automaton. + * + * Three-state HA following sched_stat / wwnr monitor naming conventions: + * + * running (initial) - task on CPU [sched_stat: runtime] + * waiting - task in runqueue [sched_stat: wait ] + * sleeping - task blocked [sched_stat: sleep ] + * + * Events (derived from sched_switch / sched_wakeup tracepoints): + * start - tlob_start_task() running -> running (resets clock) + * sleep - sched_switch, prev_state !=3D 0 running -> sleeping + * preempt - sched_switch, prev_state =3D=3D 0 running -> waiting + * wakeup - sched_wakeup sleeping -> waiting + * switch_in - sched_switch, next =3D=3D task waiting -> running + * + * One HA clock invariant: + * clk_elapsed < BUDGET_NS() active in all states (total latency budge= t) + * + * tlob_start_task() uses da_handle_start_run_event(start_tlob) to initial= ise + * the monitor: the DA framework sets the initial state and then processes= the + * start event, which resets clk_elapsed and arms the budget hrtimer via t= he + * generated ha_setup_invariants(). + * tlob_stop_task() calls ha_cancel_timer_sync() + da_monitor_reset() dire= ctly. + * + * For the format description see: + * Documentation/trace/rv/deterministic_automata.rst + */ + +#include +#include + +#define MONITOR_NAME tlob + +enum states_tlob { + running_tlob, + sleeping_tlob, + waiting_tlob, + state_max_tlob, +}; + +#define INVALID_STATE state_max_tlob + +enum events_tlob { + preempt_tlob, + sleep_tlob, + start_tlob, + switch_in_tlob, + wakeup_tlob, + event_max_tlob, +}; + +/* + * HA environment variable: clk_elapsed is the only clock. + * It measures wall-clock time since task_start and is active in all state= s. + */ +enum envs_tlob { + clk_elapsed_tlob, + env_max_tlob, + env_max_stored_tlob =3D env_max_tlob, +}; + +_Static_assert(env_max_stored_tlob <=3D MAX_HA_ENV_LEN, "Not enough slots"= ); +#define HA_CLK_NS + +struct automaton_tlob { + char *state_names[state_max_tlob]; + char *event_names[event_max_tlob]; + char *env_names[env_max_tlob]; + unsigned char function[state_max_tlob][event_max_tlob]; + unsigned char initial_state; + bool final_states[state_max_tlob]; +}; + +static const struct automaton_tlob automaton_tlob =3D { + .state_names =3D { + "running", + "sleeping", + "waiting", + }, + .event_names =3D { + "preempt", + "sleep", + "start", + "switch_in", + "wakeup", + }, + .env_names =3D { + "clk_elapsed", + }, + .function =3D { + /* running */ + { + waiting_tlob, /* preempt (sched_switch, prev_state =3D=3D 0) */ + sleeping_tlob, /* sleep (sched_switch, prev_state !=3D 0) */ + running_tlob, /* start (tlob_start_task, resets clock) */ + INVALID_STATE, /* switch_in (already on CPU) */ + INVALID_STATE, /* wakeup (TASK_RUNNING can't be woken) */ + }, + /* sleeping */ + { + INVALID_STATE, /* preempt (not on CPU) */ + INVALID_STATE, /* sleep (already sleeping) */ + INVALID_STATE, /* start (not in running state) */ + INVALID_STATE, /* switch_in (must go through waiting first) */ + waiting_tlob, /* wakeup */ + }, + /* waiting */ + { + INVALID_STATE, /* preempt (not on CPU) */ + INVALID_STATE, /* sleep (not on CPU) */ + INVALID_STATE, /* start (not in running state) */ + running_tlob, /* switch_in */ + INVALID_STATE, /* wakeup (already TASK_RUNNING) */ + }, + }, + .initial_state =3D running_tlob, + .final_states =3D { 1, 0, 0 }, +}; + +/* Maximum number of concurrently monitored tasks. */ +#define TLOB_MAX_MONITORED 64U + +/* Maximum binary path length for uprobe binding. */ +#define TLOB_MAX_PATH 256 + +/* Minimum monitoring budget (1 us). */ +#define TLOB_MIN_THRESHOLD_NS 1000ULL + +/* + * Upper bound on the monitoring budget (1 hour =3D 3 600 000 000 000 ns). + * The ns-resolution accumulators (running_ns, waiting_ns, sleeping_ns) + * are u64; keeping the window below this limit ensures they stay well + * clear of u64 overflow and covers every realistic latency-monitoring + * use case. + */ +#define TLOB_MAX_THRESHOLD_NS 3600000000000ULL + +/* Exported to uprobe layer and KUnit tests */ +int tlob_start_task(struct task_struct *task, u64 threshold_ns); +int tlob_stop_task(struct task_struct *task); + +#if IS_ENABLED(CONFIG_KUNIT) +int tlob_create_or_delete_uprobe(char *buf); +#endif /* CONFIG_KUNIT */ + +#endif /* _RV_TLOB_H */ diff --git a/kernel/trace/rv/monitors/tlob/tlob_trace.h b/kernel/trace/rv/m= onitors/tlob/tlob_trace.h new file mode 100644 index 000000000000..7ebdfbe41b86 --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/tlob_trace.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +/* + * Snippet to be included in rv_trace.h + */ + +#ifdef CONFIG_RV_MON_TLOB +DEFINE_EVENT(event_da_monitor_id, event_tlob, + TP_PROTO(int id, char *state, char *event, + char *next_state, bool final_state), + TP_ARGS(id, state, event, next_state, final_state)); + +DEFINE_EVENT(error_da_monitor_id, error_tlob, + TP_PROTO(int id, char *state, char *event), + TP_ARGS(id, state, event)); + +DEFINE_EVENT(error_env_da_monitor_id, error_env_tlob, + TP_PROTO(int id, char *state, char *event, char *env), + TP_ARGS(id, state, event, env)); + +/* + * detail_env_tlob - per-state latency breakdown emitted on budget violati= on. + * + * Fired immediately after error_env_tlob from the hrtimer callback. + * Fields show how much time was spent in each DA state since tlob_start_t= ask(). + * running_ns + waiting_ns + sleeping_ns approximately equals total + * elapsed time (threshold_ns exceeded). + */ +TRACE_EVENT(detail_env_tlob, + TP_PROTO(int id, u64 threshold_ns, + u64 running_ns, u64 waiting_ns, u64 sleeping_ns), + TP_ARGS(id, threshold_ns, running_ns, waiting_ns, sleeping_ns), + TP_STRUCT__entry( + __field(int, id) + __field(u64, threshold_ns) + __field(u64, running_ns) + __field(u64, waiting_ns) + __field(u64, sleeping_ns) + ), + TP_fast_assign( + __entry->id =3D id; + __entry->threshold_ns =3D threshold_ns; + __entry->running_ns =3D running_ns; + __entry->waiting_ns =3D waiting_ns; + __entry->sleeping_ns =3D sleeping_ns; + ), + TP_printk("pid=3D%d threshold_ns=3D%llu" + " running_ns=3D%llu waiting_ns=3D%llu sleeping_ns=3D%llu", + __entry->id, __entry->threshold_ns, + __entry->running_ns, __entry->waiting_ns, __entry->sleeping_ns) +); +#endif /* CONFIG_RV_MON_TLOB */ diff --git a/kernel/trace/rv/rv_trace.h b/kernel/trace/rv/rv_trace.h index 9622c269789c..a4bc215c1f15 100644 --- a/kernel/trace/rv/rv_trace.h +++ b/kernel/trace/rv/rv_trace.h @@ -189,6 +189,7 @@ DECLARE_EVENT_CLASS(error_env_da_monitor_id, =20 #include #include +#include // Add new monitors based on CONFIG_HA_MON_EVENTS_ID here =20 #endif --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-182.mta0.migadu.com (out-182.mta0.migadu.com [91.218.175.182]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3036C4343E1 for ; Wed, 8 Jul 2026 15:39:29 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.182 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525172; cv=none; b=sq0+z30nAOnEaHXdP8GnKy2vrnftQoxh6nhR1uSaPG6aJ/326+PO7cW449xNDMu+SgZz4ITCKiAE6qjGI8tX4R0VMZrgR8/KR6OHyWNAOjHhhlh7CAV3X6QTC0j91pHtDK53AJnPv04GPtG7uPXMQ2DtMEhgMn89934Vj3lL0ug= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525172; c=relaxed/simple; bh=2NizjHn1wd165dWwmEjKsUz+yEaZX2jeecpYo5qf1Kw=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=S4BcPINZ82uq98Zee2iLVix3A81C9OaUWkN3Huarzfo5vH6SUctOqhHBQbTVTZDO2GHSH4QOf1TnEo+Zk79w/y0/kMRpxON5YVYAr0nqy/zCm3JonOJ0Uc+slYjT6BMJHzISyecmLATdwQPObuUk0QQuFg4Lo9OcaI043eCoq0A= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=AORxvFBS; arc=none smtp.client-ip=91.218.175.182 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="AORxvFBS" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525168; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=kQ3hf7dGaovMdiVksikaMoIv3qF+j16rWQP4TKRwPUs=; b=AORxvFBSyxuyO7vRiBXVdZtqtZ/wa1R5akk7xVa4I12x/0in5QSpZ4nBo/3+GlRh/aKKl/ bhUc/iVimx4KOj8DtXLy044YqGV9HB4gZH4Mmo2nCO3LKn8lXQU0m7v/WZcuCc38WDOC02 +n7abgxhWuOgJ2cW3uZ8sZhve3gtNF4= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 7/8] rv/tlob: add KUnit tests for the tlob monitor Date: Wed, 8 Jul 2026 23:38:33 +0800 Message-Id: <18ab97e8a248e55dfeef5daa49daa407bf58dc56.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Add CONFIG_TLOB_KUNIT_TEST (tristate, depends on RV_MON_TLOB && KUNIT, default KUNIT_ALL_TESTS) with a test suite covering the uprobe-line parser. Tests call tlob_parse_uprobe_line() and tlob_parse_remove_line() directly rather than going through the top-level write handler, so they exercise parser logic only without touching the uprobe or filesystem subsystems. Cases cover valid inputs, malformed paths and offsets (including negative values), out-of-range thresholds, and valid and invalid remove lines. Signed-off-by: Wen Yang --- kernel/trace/rv/Makefile | 1 + kernel/trace/rv/monitors/tlob/.kunitconfig | 8 ++ kernel/trace/rv/monitors/tlob/Kconfig | 7 ++ kernel/trace/rv/monitors/tlob/tlob.c | 7 +- kernel/trace/rv/monitors/tlob/tlob.h | 4 +- kernel/trace/rv/monitors/tlob/tlob_kunit.c | 139 +++++++++++++++++++++ 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 kernel/trace/rv/monitors/tlob/.kunitconfig create mode 100644 kernel/trace/rv/monitors/tlob/tlob_kunit.c diff --git a/kernel/trace/rv/Makefile b/kernel/trace/rv/Makefile index ae59e97f8682..316d53398345 100644 --- a/kernel/trace/rv/Makefile +++ b/kernel/trace/rv/Makefile @@ -21,6 +21,7 @@ obj-$(CONFIG_RV_MON_STALL) +=3D monitors/stall/stall.o obj-$(CONFIG_RV_MON_DEADLINE) +=3D monitors/deadline/deadline.o obj-$(CONFIG_RV_MON_NOMISS) +=3D monitors/nomiss/nomiss.o obj-$(CONFIG_RV_MON_TLOB) +=3D monitors/tlob/tlob.o +obj-$(CONFIG_TLOB_KUNIT_TEST) +=3D monitors/tlob/tlob_kunit.o # Add new monitors here obj-$(CONFIG_RV_UPROBE) +=3D rv_uprobe.o obj-$(CONFIG_RV_REACTORS) +=3D rv_reactors.o diff --git a/kernel/trace/rv/monitors/tlob/.kunitconfig b/kernel/trace/rv/m= onitors/tlob/.kunitconfig new file mode 100644 index 000000000000..34ebf3b172ac --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/.kunitconfig @@ -0,0 +1,8 @@ +CONFIG_FTRACE=3Dy +CONFIG_HIGH_RES_TIMERS=3Dy +CONFIG_KUNIT=3Dy +CONFIG_MODULES=3Dy +CONFIG_RV=3Dy +CONFIG_RV_MON_TLOB=3Dy +CONFIG_TLOB_KUNIT_TEST=3Dy +CONFIG_UPROBES=3Dy diff --git a/kernel/trace/rv/monitors/tlob/Kconfig b/kernel/trace/rv/monito= rs/tlob/Kconfig index aa43382073d2..402ef2e5c076 100644 --- a/kernel/trace/rv/monitors/tlob/Kconfig +++ b/kernel/trace/rv/monitors/tlob/Kconfig @@ -10,3 +10,10 @@ config RV_MON_TLOB monitor. tlob tracks per-task elapsed wall-clock time across a user-delimited code section and emits error_env_tlob when the elapsed time exceeds a configurable per-invocation budget. + +config TLOB_KUNIT_TEST + tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS + depends on RV_MON_TLOB && KUNIT + default KUNIT_ALL_TESTS + help + Enable KUnit unit tests for the tlob RV monitor. diff --git a/kernel/trace/rv/monitors/tlob/tlob.c b/kernel/trace/rv/monitor= s/tlob/tlob.c index b45e84195131..a6f9c371646c 100644 --- a/kernel/trace/rv/monitors/tlob/tlob.c +++ b/kernel/trace/rv/monitors/tlob/tlob.c @@ -708,7 +708,7 @@ static ssize_t tlob_monitor_read(struct file *file, * PATH may contain ':'; the last ':' separates path from offset. * Returns 0, -EINVAL, or -ERANGE. */ -static int tlob_parse_uprobe_line(char *buf, u64 *thr_out, +VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out, loff_t *start_out, loff_t *stop_out) { @@ -782,6 +782,7 @@ static int tlob_parse_uprobe_line(char *buf, u64 *thr_o= ut, *stop_out =3D (loff_t)stop_val; return 0; } +EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line); =20 /* * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention). @@ -810,8 +811,9 @@ VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf, = char **path_out, *start_out =3D (loff_t)off; return 0; } +EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line); =20 -VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf) +static int tlob_create_or_delete_uprobe(char *buf) { loff_t offset_start, offset_stop; u64 threshold_ns; @@ -836,7 +838,6 @@ VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char = *buf) mutex_unlock(&tlob_uprobe_mutex); return ret; } -EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe); =20 static ssize_t tlob_monitor_write(struct file *file, const char __user *ubuf, diff --git a/kernel/trace/rv/monitors/tlob/tlob.h b/kernel/trace/rv/monitor= s/tlob/tlob.h index fceeba748c85..15bdf3b7fade 100644 --- a/kernel/trace/rv/monitors/tlob/tlob.h +++ b/kernel/trace/rv/monitors/tlob/tlob.h @@ -145,7 +145,9 @@ int tlob_start_task(struct task_struct *task, u64 thres= hold_ns); int tlob_stop_task(struct task_struct *task); =20 #if IS_ENABLED(CONFIG_KUNIT) -int tlob_create_or_delete_uprobe(char *buf); +int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out, + loff_t *start_out, loff_t *stop_out); +int tlob_parse_remove_line(char *buf, char **path_out, loff_t *start_out); #endif /* CONFIG_KUNIT */ =20 #endif /* _RV_TLOB_H */ diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.c b/kernel/trace/rv/m= onitors/tlob/tlob_kunit.c new file mode 100644 index 000000000000..7448f3fab959 --- /dev/null +++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.c @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KUnit tests for the tlob RV monitor. + * + */ +#include + +#include "tlob.h" + +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING"); + +/* Valid "p PATH:START STOP threshold=3DNS" lines. */ +static const char * const tlob_parse_valid[] =3D { + "p /usr/bin/myapp:4768 4848 threshold=3D5000000", + "p /usr/bin/myapp:0x12a0 0x12f0 threshold=3D10000000", + "p /opt/my:app/bin:0x100 0x200 threshold=3D1000000", +}; + +/* Malformed "p ..." lines that must be rejected with -EINVAL. */ +static const char * const tlob_parse_invalid[] =3D { + "p :0x100 0x200 threshold=3D5000", + "p /usr/bin/myapp:0x100 threshold=3D5000", + "p /usr/bin/myapp:-1 0x200 threshold=3D5000", + "p /usr/bin/myapp:0x100 -1 threshold=3D5000000", /* negative stop offset = */ + "p /usr/bin/myapp:0x100 0x200", + "p /usr/bin/myapp:0x100 0x100 threshold=3D5000", +}; + +/* threshold_ns out of valid range =3D> -ERANGE. */ +static const char * const tlob_parse_out_of_range[] =3D { + "p /usr/bin/myapp:0x100 0x200 threshold=3D0", + "p /usr/bin/myapp:0x100 0x200 threshold=3D999", + "p /usr/bin/myapp:0x100 0x200 threshold=3D3600000000001", +}; + +/* Valid "-PATH:OFFSET_START" remove lines. */ +static const char * const tlob_remove_valid[] =3D { + "-/usr/bin/myapp:0x100", + "-/opt/my:app/bin:0x200", +}; + +/* Malformed remove lines that must be rejected with -EINVAL. */ +static const char * const tlob_remove_invalid[] =3D { + "-usr/bin/myapp:0x100", + "-/usr/bin/myapp", + "-/:0x100", + "-/usr/bin/myapp:-1", /* negative offset */ + "-/usr/bin/myapp:abc", +}; + +static void tlob_parse_valid_accepted(struct kunit *test) +{ + u64 thr; + char *path; + loff_t start, stop; + char buf[128]; + int i; + + for (i =3D 0; i < ARRAY_SIZE(tlob_parse_valid); i++) { + strscpy(buf, tlob_parse_valid[i], sizeof(buf)); + KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path, + &start, &stop), 0); + } +} + +static void tlob_parse_invalid_rejected(struct kunit *test) +{ + u64 thr; + char *path; + loff_t start, stop; + char buf[128]; + int i; + + for (i =3D 0; i < ARRAY_SIZE(tlob_parse_invalid); i++) { + strscpy(buf, tlob_parse_invalid[i], sizeof(buf)); + KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path, + &start, &stop), -EINVAL); + } +} + +static void tlob_parse_out_of_range_rejected(struct kunit *test) +{ + u64 thr; + char *path; + loff_t start, stop; + char buf[128]; + int i; + + for (i =3D 0; i < ARRAY_SIZE(tlob_parse_out_of_range); i++) { + strscpy(buf, tlob_parse_out_of_range[i], sizeof(buf)); + KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr, &path, + &start, &stop), -ERANGE); + } +} + +static void tlob_remove_valid_accepted(struct kunit *test) +{ + char *path; + loff_t start; + char buf[128]; + int i; + + for (i =3D 0; i < ARRAY_SIZE(tlob_remove_valid); i++) { + strscpy(buf, tlob_remove_valid[i], sizeof(buf)); + KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path, &start), 0); + } +} + +static void tlob_remove_invalid_rejected(struct kunit *test) +{ + char *path; + loff_t start; + char buf[128]; + int i; + + for (i =3D 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) { + strscpy(buf, tlob_remove_invalid[i], sizeof(buf)); + KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path, &start), -EINVA= L); + } +} + +static struct kunit_case tlob_parse_cases[] =3D { + KUNIT_CASE(tlob_parse_valid_accepted), + KUNIT_CASE(tlob_parse_invalid_rejected), + KUNIT_CASE(tlob_parse_out_of_range_rejected), + KUNIT_CASE(tlob_remove_valid_accepted), + KUNIT_CASE(tlob_remove_invalid_rejected), + {} +}; + +static struct kunit_suite tlob_parse_suite =3D { + .name =3D "tlob_parse", + .test_cases =3D tlob_parse_cases, +}; + +kunit_test_suite(tlob_parse_suite); + +MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor"); +MODULE_LICENSE("GPL"); --=20 2.25.1 From nobody Tue Jul 28 00:00:29 2026 Received: from out-186.mta0.migadu.com (out-186.mta0.migadu.com [91.218.175.186]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C867A42DA3C for ; Wed, 8 Jul 2026 15:39:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.186 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525176; cv=none; b=GVANb3YCid6kIMxeKtCBBNUee69bI824VhQeyYZEBWtyT1smIwKZxsd9WSgoOnhGrjofKgFAGQcI0b5NJOxVWvHfFOMyAW6HSbsBI5ed+p4CjR4/7m+pfmYXtzq6/kcR0RqHnwc26xeDF66tRMjlhS4bhrXeTTXB6UqWTDI/Thw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783525176; c=relaxed/simple; bh=67I4upehHSmBR2ATzvirosBiYXNttTvdju/HzHURgd8=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=qekE4s9LHdP/svcV6QSQZSHqCo9ZI7r8r093LZ0wcDnK3U/3wUcKx0T9HGaib2019482Sv3xSOgJJT/T/kVmhP47YX8b8SVWQLWKMxPs9x6fDiPHiM1fNbBxR0TQb3L1KNb4h1L6vey5VQFoMn0pLhSkF5ot71BJTiNHd/+bC+g= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=iqA9BS2x; arc=none smtp.client-ip=91.218.175.186 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="iqA9BS2x" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783525171; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=3GwE92WeJSH8qKsz85avHeTfGL+kk5Nnqat6KWct5A0=; b=iqA9BS2xRGlCROOUSNhs2CVsPz7SrKYBP+iv8Cl4mZfQ+HoGGTqYeHS2tL7V5TeLRLk5PW ktZ5ihLGmGXZ/c7flAWjUClfxsLT5WdEujwnIX2x5AH3X1jFZVXQ1mu6VuhhIRfgoB3z3L 5WcW6RyWgfBNr4NpuAtKX801eSePAxg= From: wen.yang@linux.dev To: Gabriele Monaco Cc: Nam Cao , linux-trace-kernel@vger.kernel.org, linux-kernel@vger.kernel.org, Wen Yang Subject: [PATCH v4 8/8] selftests/verification: add tlob selftests Date: Wed, 8 Jul 2026 23:38:34 +0800 Message-Id: <4eb9a676efe90de8dfc1a9188d6ea81336e65e63.1783524627.git.wen.yang@linux.dev> In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Wen Yang Add seven ftrace-style test scripts for the tlob RV monitor under tools/testing/selftests/verification/test.d/tlob/. The tests cover uprobe binding management, budget violation detection, and per-state time accounting. Helper binaries tlob_target and tlob_sym are included in the same directory so the suite is self-contained. tlob_sym resolves ELF symbol offsets for uprobe registration; tlob_target provides busy-spin, sleep, and preempt workloads. ftracetest is updated to walk up the directory tree when searching for test.d/functions, so monitor subdirectories can be passed as the test directory without placing a dummy functions shim in each new directory. Signed-off-by: Wen Yang --- tools/testing/selftests/ftrace/ftracetest | 18 +- .../testing/selftests/verification/.gitignore | 2 + tools/testing/selftests/verification/Makefile | 19 +- .../verification/test.d/tlob/Makefile | 28 +++ .../test.d/tlob/run_tlob_tests.sh | 90 ++++++++ .../verification/test.d/tlob/tlob_sym.c | 209 ++++++++++++++++++ .../verification/test.d/tlob/tlob_target.c | 138 ++++++++++++ .../verification/test.d/tlob/uprobe_bind.tc | 37 ++++ .../test.d/tlob/uprobe_detail_running.tc | 51 +++++ .../test.d/tlob/uprobe_detail_sleeping.tc | 50 +++++ .../test.d/tlob/uprobe_detail_waiting.tc | 66 ++++++ .../verification/test.d/tlob/uprobe_multi.tc | 64 ++++++ .../test.d/tlob/uprobe_no_event.tc | 19 ++ .../test.d/tlob/uprobe_violation.tc | 67 ++++++ 14 files changed, 854 insertions(+), 4 deletions(-) create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefi= le create mode 100755 tools/testing/selftests/verification/test.d/tlob/run_tl= ob_tests.sh create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_s= ym.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/tlob_t= arget.c create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _bind.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _detail_running.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _detail_sleeping.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _detail_waiting.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _multi.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _no_event.tc create mode 100644 tools/testing/selftests/verification/test.d/tlob/uprobe= _violation.tc diff --git a/tools/testing/selftests/ftrace/ftracetest b/tools/testing/self= tests/ftrace/ftracetest index 0a56bf209f6c..91c007b0a74a 100755 --- a/tools/testing/selftests/ftrace/ftracetest +++ b/tools/testing/selftests/ftrace/ftracetest @@ -159,9 +159,21 @@ parse_opts() { # opts if [ -n "$OPT_TEST_CASES" ]; then TEST_CASES=3D$OPT_TEST_CASES fi - if [ -n "$OPT_TEST_DIR" -a -f "$OPT_TEST_DIR"/test.d/functions ]; then - TOP_DIR=3D$OPT_TEST_DIR - TEST_DIR=3D$TOP_DIR/test.d + if [ -n "$OPT_TEST_DIR" ]; then + # Walk up from OPT_TEST_DIR to find the nearest ancestor containing + # test.d/functions, allowing monitor subdirectories to be passed direc= tly. + dir=3D$OPT_TEST_DIR + while [ "$dir" !=3D "/" ]; do + if [ -f "$dir/test.d/functions" ]; then + TOP_DIR=3D$dir + TEST_DIR=3D$TOP_DIR/test.d + break + fi + dir=3D$(dirname "$dir") + done + if [ -z "$TOP_DIR" ]; then + errexit "no test.d/functions found above $OPT_TEST_DIR" + fi fi } =20 diff --git a/tools/testing/selftests/verification/.gitignore b/tools/testin= g/selftests/verification/.gitignore index 2659417cb2c7..cbbd03ee16c7 100644 --- a/tools/testing/selftests/verification/.gitignore +++ b/tools/testing/selftests/verification/.gitignore @@ -1,2 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only logs +test.d/tlob/tlob_sym +test.d/tlob/tlob_target diff --git a/tools/testing/selftests/verification/Makefile b/tools/testing/= selftests/verification/Makefile index aa8790c22a71..0b32bdfdb8db 100644 --- a/tools/testing/selftests/verification/Makefile +++ b/tools/testing/selftests/verification/Makefile @@ -1,8 +1,25 @@ # SPDX-License-Identifier: GPL-2.0 -all: =20 TEST_PROGS :=3D verificationtest-ktap TEST_FILES :=3D test.d settings EXTRA_CLEAN :=3D $(OUTPUT)/logs/* =20 +# Subdirectories that provide binaries used by the test runner. +# Each entry must contain a Makefile that accepts OUTDIR=3D and +# deposits its binaries there. +BUILD_SUBDIRS :=3D test.d/tlob + include ../lib.mk + +all: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) + +clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS)) + +.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \ + $(patsubst %,_clean_%,$(BUILD_SUBDIRS)) + +$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%: + $(MAKE) -C $* OUTDIR=3D"$(OUTPUT)" TOOLS_INCLUDES=3D"$(TOOLS_INCLUDES)" + +$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%: + $(MAKE) -C $* OUTDIR=3D"$(OUTPUT)" clean diff --git a/tools/testing/selftests/verification/test.d/tlob/Makefile b/to= ols/testing/selftests/verification/test.d/tlob/Makefile new file mode 100644 index 000000000000..05a2d2599c4e --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/Makefile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0 +# Builds tlob selftest helper binaries in the directory of this Makefile. +# +# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and +# tlob_target alongside the .tc scripts so they are self-contained. + +CFLAGS +=3D $(TOOLS_INCLUDES) + +# For standalone execution via vng +FTRACETEST :=3D ../../../ftrace/ftracetest +LOGDIR ?=3D ../../logs + +.PHONY: all +all: tlob_sym tlob_target + +tlob_sym: tlob_sym.c + $(CC) $(CFLAGS) -o $@ $< + +tlob_target: tlob_target.c + $(CC) $(CFLAGS) -o $@ $< + +.PHONY: run_tests +run_tests: all + @./run_tlob_tests.sh + +.PHONY: clean +clean: + $(RM) tlob_sym tlob_target diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_test= s.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh new file mode 100755 index 000000000000..cd949756e713 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Standalone runner for tlob selftests +# Usage: ./run_tlob_tests.sh [options] +# +# Options: +# -v, --verbose Verbose output +# -k, --keep Keep test logs +# -l, --logdir DIR Log directory (default: ../../logs) +# -h, --help Show this help + +set -e + +SCRIPT_DIR=3D"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FTRACETEST=3D"$SCRIPT_DIR/../../../ftrace/ftracetest" +LOGDIR=3D"$SCRIPT_DIR/../../logs" +VERBOSE=3D"" +KEEP=3D"" +EXTRA_ARGS=3D"" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + VERBOSE=3D"-v" + shift + ;; + -k|--keep) + KEEP=3D"-k" + shift + ;; + -l|--logdir) + LOGDIR=3D"$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -v, --verbose Verbose output" + echo " -k, --keep Keep test logs" + echo " -l, --logdir DIR Log directory (default: ../../logs)" + echo " -h, --help Show this help" + echo "" + echo "Examples:" + echo " $0 # Run all tlob tests" + echo " $0 -v # Run with verbose output" + echo " $0 -v -l /tmp/tlob-logs # Custom log directory" + echo "" + echo "With vng:" + echo " vng -v --rwdir $LOGDIR -- $0" + exit 0 + ;; + *) + EXTRA_ARGS=3D"$EXTRA_ARGS $1" + shift + ;; + esac +done + +# Build test helpers +echo "Building tlob test helpers..." +make -C "$SCRIPT_DIR" all + +# Check ftracetest exists +if [ ! -x "$FTRACETEST" ]; then + echo "Error: $FTRACETEST not found or not executable" + echo "Make sure you're running from the correct directory" + exit 1 +fi + +# Create log directory +mkdir -p "$LOGDIR" + +# Run tests +echo "Running tlob selftests..." +echo "Log directory: $LOGDIR" +echo "" + +# Export RV_BINDIR so test scripts can find tlob_target and tlob_sym +export RV_BINDIR=3D"$SCRIPT_DIR" + +# Pass the test directory, not individual .tc files +# ftracetest will discover all .tc files in the directory +"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \ + "$SCRIPT_DIR" $EXTRA_ARGS + +echo "" +echo "Tests completed. Logs saved to: $LOGDIR" diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c b/= tools/testing/selftests/verification/test.d/tlob/tlob_sym.c new file mode 100644 index 000000000000..a92fc49d1304 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * tlob_sym.c - ELF symbol-to-file-offset utility for tlob selftests + * + * Usage: tlob_sym sym_offset + * + * Prints the ELF file offset of in to stdout. + * + * Exit: 0 =3D found, 1 =3D error / not found. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int sym_offset(const char *binary, const char *symname) +{ + int fd; + struct stat st; + void *map; + Elf64_Ehdr *ehdr; + Elf32_Ehdr *ehdr32; + int is64; + uint64_t sym_vaddr =3D 0; + int found =3D 0; + uint64_t file_offset =3D 0; + + fd =3D open(binary, O_RDONLY); + if (fd < 0) { + fprintf(stderr, "open %s: %s\n", binary, strerror(errno)); + return 1; + } + if (fstat(fd, &st) < 0) { + close(fd); + return 1; + } + map =3D mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (map =3D=3D MAP_FAILED) { + fprintf(stderr, "mmap: %s\n", strerror(errno)); + return 1; + } + + ehdr =3D (Elf64_Ehdr *)map; + ehdr32 =3D (Elf32_Ehdr *)map; + if (st.st_size < 4 || + ehdr->e_ident[EI_MAG0] !=3D ELFMAG0 || + ehdr->e_ident[EI_MAG1] !=3D ELFMAG1 || + ehdr->e_ident[EI_MAG2] !=3D ELFMAG2 || + ehdr->e_ident[EI_MAG3] !=3D ELFMAG3) { + fprintf(stderr, "%s: not an ELF file\n", binary); + munmap(map, (size_t)st.st_size); + return 1; + } + is64 =3D (ehdr->e_ident[EI_CLASS] =3D=3D ELFCLASS64); + + if (is64) { + Elf64_Shdr *shdrs; + Elf64_Shdr *shstrtab_hdr; + + if (ehdr->e_shnum =3D=3D 0 || ehdr->e_shstrndx >=3D ehdr->e_shnum || + (uint64_t)ehdr->e_shoff + + (uint64_t)ehdr->e_shnum * sizeof(Elf64_Shdr) > (uint64_t)st.st_size)= { + fprintf(stderr, "%s: malformed ELF section table\n", binary); + munmap(map, (size_t)st.st_size); + return 1; + } + shdrs =3D (Elf64_Shdr *)((char *)map + ehdr->e_shoff); + shstrtab_hdr =3D &shdrs[ehdr->e_shstrndx]; + const char *shstrtab =3D (char *)map + shstrtab_hdr->sh_offset; + int si; + + for (int pass =3D 0; pass < 2 && !found; pass++) { + const char *target =3D pass ? ".dynsym" : ".symtab"; + + for (si =3D 0; si < ehdr->e_shnum && !found; si++) { + Elf64_Shdr *sh =3D &shdrs[si]; + const char *name =3D shstrtab + sh->sh_name; + + if (strcmp(name, target) !=3D 0) + continue; + + Elf64_Shdr *strtab_sh =3D &shdrs[sh->sh_link]; + const char *strtab =3D (char *)map + strtab_sh->sh_offset; + Elf64_Sym *syms =3D (Elf64_Sym *)((char *)map + sh->sh_offset); + uint64_t nsyms =3D sh->sh_size / sizeof(Elf64_Sym); + uint64_t j; + + for (j =3D 0; j < nsyms; j++) { + if (strcmp(strtab + syms[j].st_name, symname) =3D=3D 0) { + sym_vaddr =3D syms[j].st_value; + found =3D 1; + break; + } + } + } + } + + if (!found) { + fprintf(stderr, "symbol '%s' not found in %s\n", symname, binary); + munmap(map, (size_t)st.st_size); + return 1; + } + + Elf64_Phdr *phdrs =3D (Elf64_Phdr *)((char *)map + ehdr->e_phoff); + int pi; + + for (pi =3D 0; pi < ehdr->e_phnum; pi++) { + Elf64_Phdr *ph =3D &phdrs[pi]; + + if (ph->p_type !=3D PT_LOAD) + continue; + if (sym_vaddr >=3D ph->p_vaddr && + sym_vaddr < ph->p_vaddr + ph->p_filesz) { + file_offset =3D sym_vaddr - ph->p_vaddr + ph->p_offset; + break; + } + } + } else { + Elf32_Shdr *shdrs; + Elf32_Shdr *shstrtab_hdr; + + if (ehdr32->e_shnum =3D=3D 0 || ehdr32->e_shstrndx >=3D ehdr32->e_shnum = || + (uint64_t)ehdr32->e_shoff + + (uint64_t)ehdr32->e_shnum * sizeof(Elf32_Shdr) > (uint64_t)st.st_siz= e) { + fprintf(stderr, "%s: malformed ELF section table\n", binary); + munmap(map, (size_t)st.st_size); + return 1; + } + shdrs =3D (Elf32_Shdr *)((char *)map + ehdr32->e_shoff); + shstrtab_hdr =3D &shdrs[ehdr32->e_shstrndx]; + const char *shstrtab =3D (char *)map + shstrtab_hdr->sh_offset; + int si; + uint32_t sym_vaddr32 =3D 0; + + for (int pass =3D 0; pass < 2 && !found; pass++) { + const char *target =3D pass ? ".dynsym" : ".symtab"; + + for (si =3D 0; si < ehdr32->e_shnum && !found; si++) { + Elf32_Shdr *sh =3D &shdrs[si]; + const char *name =3D shstrtab + sh->sh_name; + + if (strcmp(name, target) !=3D 0) + continue; + + Elf32_Shdr *strtab_sh =3D &shdrs[sh->sh_link]; + const char *strtab =3D (char *)map + strtab_sh->sh_offset; + Elf32_Sym *syms =3D (Elf32_Sym *)((char *)map + sh->sh_offset); + uint32_t nsyms =3D sh->sh_size / sizeof(Elf32_Sym); + uint32_t j; + + for (j =3D 0; j < nsyms; j++) { + if (strcmp(strtab + syms[j].st_name, symname) =3D=3D 0) { + sym_vaddr32 =3D syms[j].st_value; + found =3D 1; + break; + } + } + } + } + + if (!found) { + fprintf(stderr, "symbol '%s' not found in %s\n", symname, binary); + munmap(map, (size_t)st.st_size); + return 1; + } + + Elf32_Phdr *phdrs =3D (Elf32_Phdr *)((char *)map + ehdr32->e_phoff); + int pi; + + for (pi =3D 0; pi < ehdr32->e_phnum; pi++) { + Elf32_Phdr *ph =3D &phdrs[pi]; + + if (ph->p_type !=3D PT_LOAD) + continue; + if (sym_vaddr32 >=3D ph->p_vaddr && + sym_vaddr32 < ph->p_vaddr + ph->p_filesz) { + file_offset =3D sym_vaddr32 - ph->p_vaddr + ph->p_offset; + break; + } + } + sym_vaddr =3D sym_vaddr32; + } + + munmap(map, (size_t)st.st_size); + + if (!file_offset && sym_vaddr) { + fprintf(stderr, "could not map vaddr 0x%lx to file offset\n", + (unsigned long)sym_vaddr); + return 1; + } + + printf("0x%lx\n", (unsigned long)file_offset); + return 0; +} + +int main(int argc, char *argv[]) +{ + if (argc !=3D 4 || strcmp(argv[1], "sym_offset") !=3D 0) { + fprintf(stderr, "Usage: %s sym_offset \n", argv[0]); + return 1; + } + return sym_offset(argv[2], argv[3]); +} diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_target.c= b/tools/testing/selftests/verification/test.d/tlob/tlob_target.c new file mode 100644 index 000000000000..adf4c2397fb3 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/tlob_target.c @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * tlob_target.c - uprobe target binary for tlob selftests. + * + * Provides three start/stop probe pairs, each designed to exercise a + * different dominant component of the detail_env_tlob ns breakdown: + * + * tlob_busy_work / tlob_busy_work_done - busy-spin: running_ns do= minates + * tlob_sleep_work / tlob_sleep_work_done - nanosleep: sleeping_ns d= ominates + * tlob_preempt_work / tlob_preempt_work_done - busy-spin + RT competito= r: + * waiting_ns dominates + * + * Usage: tlob_target [mode] + * + * mode is one of: busy (default), sleep, preempt. + * Loops in 200 ms iterations until has elapsed + * (0 =3D run for ~24 hours). + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#ifndef noinline +#define noinline __attribute__((noinline)) +#endif + +static inline int timespec_before(const struct timespec *a, + const struct timespec *b) +{ + return a->tv_sec < b->tv_sec || + (a->tv_sec =3D=3D b->tv_sec && a->tv_nsec < b->tv_nsec); +} + +static void timespec_add_ms(struct timespec *ts, unsigned long ms) +{ + ts->tv_sec +=3D ms / 1000; + ts->tv_nsec +=3D (long)(ms % 1000) * 1000000L; + if (ts->tv_nsec >=3D 1000000000L) { + ts->tv_sec++; + ts->tv_nsec -=3D 1000000000L; + } +} + +/* stop probe; noinline keeps the entry point visible to uprobes */ +noinline void tlob_busy_work_done(void) +{ + /* empty: uprobe fires on entry */ +} + +/* start probe; busy-spin so running_ns dominates */ +noinline void tlob_busy_work(unsigned long duration_ms) +{ + struct timespec start, now; + unsigned long elapsed; + + clock_gettime(CLOCK_MONOTONIC, &start); + do { + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed =3D (unsigned long)(now.tv_sec - start.tv_sec) + * 1000000000UL + + (unsigned long)(now.tv_nsec - start.tv_nsec); + } while (elapsed < duration_ms * 1000000UL); + + tlob_busy_work_done(); +} + +/* stop probe; noinline keeps the entry point visible to uprobes */ +noinline void tlob_sleep_work_done(void) +{ + /* empty: uprobe fires on entry */ +} + +/* start probe; nanosleep so sleeping_ns dominates */ +noinline void tlob_sleep_work(unsigned long duration_ms) +{ + struct timespec ts =3D { + .tv_sec =3D duration_ms / 1000, + .tv_nsec =3D (long)(duration_ms % 1000) * 1000000L, + }; + nanosleep(&ts, NULL); + tlob_sleep_work_done(); +} + +/* stop probe; noinline keeps the entry point visible to uprobes */ +noinline void tlob_preempt_work_done(void) +{ + /* empty: uprobe fires on entry */ +} + +/* + * start probe; busy-spin so an RT competitor on the same CPU drives + * waiting_ns (prev_state=3D=3D0 -> preempt event, task stays runnable off= -CPU). + */ +noinline void tlob_preempt_work(unsigned long duration_ms) +{ + struct timespec start, now; + unsigned long elapsed; + + clock_gettime(CLOCK_MONOTONIC, &start); + do { + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed =3D (unsigned long)(now.tv_sec - start.tv_sec) + * 1000000000UL + + (unsigned long)(now.tv_nsec - start.tv_nsec); + } while (elapsed < duration_ms * 1000000UL); + + tlob_preempt_work_done(); +} + +int main(int argc, char *argv[]) +{ + unsigned long duration_ms =3D 0; + const char *mode =3D "busy"; + struct timespec deadline, now; + + if (argc >=3D 2) + duration_ms =3D strtoul(argv[1], NULL, 10); + if (argc >=3D 3) + mode =3D argv[2]; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + timespec_add_ms(&deadline, duration_ms ? duration_ms : 86400000UL); + + do { + if (strcmp(mode, "sleep") =3D=3D 0) + tlob_sleep_work(200); + else if (strcmp(mode, "preempt") =3D=3D 0) + tlob_preempt_work(200); + else + tlob_busy_work(200); + clock_gettime(CLOCK_MONOTONIC, &now); + } while (timespec_before(&now, &deadline)); + + return 0; +} diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.t= c b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc new file mode 100644 index 000000000000..4a1c18c7485a --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc @@ -0,0 +1,37 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor uprobe binding (visible in monitor file, = removable, duplicate rejected) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +busy_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/= dev/null) +stop_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_don= e 2>/dev/null) +[ -n "$busy_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +"$UPROBE_TARGET" 30000 & +busy_pid=3D$! +sleep 0.05 + +echo 1 > monitors/tlob/enable +echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=3D5000000= 000" > "$TLOB_MONITOR" + +# Binding must appear in monitor file with canonical hex-offset format. +grep -qE "^p ${UPROBE_TARGET}:0x[0-9a-f]+ 0x[0-9a-f]+ threshold=3D[0-9]+$"= "$TLOB_MONITOR" +grep -q "threshold=3D5000000000" "$TLOB_MONITOR" + +# Duplicate offset_start must be rejected. +! echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=3D99990= 00" > "$TLOB_MONITOR" 2>/dev/null || false + +# Remove the binding; it must no longer appear. +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" +! grep -q "^p .*:0x${busy_offset#0x} " "$TLOB_MONITOR" || false + +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo 0 > monitors/tlob/enable diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail= _running.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detai= l_running.tc new file mode 100644 index 000000000000..afca157b5ea4 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_runnin= g.tc @@ -0,0 +1,51 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor detail running (running_ns dominates when= task busy-spins between probes) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +start_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>= /dev/null) +stop_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_don= e 2>/dev/null) +[ -n "$start_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +"$UPROBE_TARGET" 5000 & +busy_pid=3D$! +sleep 0.05 + +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# 10 us budget; task busy-spins 200 ms per iteration -> running_ns dominat= es. +echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=3D10000"= > "$TLOB_MONITOR" + +found=3D0; i=3D0 +while [ "$i" -lt 30 ]; do + sleep 0.1 + grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=3D1; break= ; } + i=3D$((i+1)) +done + +echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 0 > monitors/tlob/enable + +[ "$found" =3D "1" ] + +line=3D$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1) +running=3D$(echo "$line" | sed 's/.*running_ns=3D\([0-9]*\).*/\1/') +waiting=3D$(echo "$line" | sed 's/.*waiting_ns=3D\([0-9]*\).*/\1/') +sleeping=3D$(echo "$line" | sed 's/.*sleeping_ns=3D\([0-9]*\).*/\1/') +# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns. +[ "$running" -gt "$sleeping" ] + +echo > /sys/kernel/tracing/trace diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail= _sleeping.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_deta= il_sleeping.tc new file mode 100644 index 000000000000..0a6470b4cadb --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleepi= ng.tc @@ -0,0 +1,50 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor detail sleeping (sleeping_ns dominates wh= en task blocks between probes) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +start_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2= >/dev/null) +stop_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work_do= ne 2>/dev/null) +[ -n "$start_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +"$UPROBE_TARGET" 5000 sleep & +busy_pid=3D$! +sleep 0.05 + +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# 50 ms budget; task sleeps 200 ms per iteration -> sleeping_ns dominates. +echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=3D500000= 00" > "$TLOB_MONITOR" + +found=3D0; i=3D0 +while [ "$i" -lt 30 ]; do + sleep 0.1 + grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=3D1; break= ; } + i=3D$((i+1)) +done + +echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 0 > monitors/tlob/enable + +[ "$found" =3D "1" ] + +line=3D$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1) +running=3D$(echo "$line" | sed 's/.*running_ns=3D\([0-9]*\).*/\1/') +waiting=3D$(echo "$line" | sed 's/.*waiting_ns=3D\([0-9]*\).*/\1/') +sleeping=3D$(echo "$line" | sed 's/.*sleeping_ns=3D\([0-9]*\).*/\1/') +[ "$sleeping" -gt "$((running + waiting))" ] + +echo > /sys/kernel/tracing/trace diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail= _waiting.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detai= l_waiting.tc new file mode 100644 index 000000000000..ef22fce700fc --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waitin= g.tc @@ -0,0 +1,66 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor detail waiting (waiting_ns dominates when= task is preempted between probes) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +command -v chrt > /dev/null || exit_unsupported +command -v taskset > /dev/null || exit_unsupported + +start_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work= 2>/dev/null) +stop_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work_= done 2>/dev/null) +[ -n "$start_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +cpu=3D0 + +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# Register probe before the target starts so the start uprobe fires on the +# first entry to tlob_preempt_work. Budget: 500 ms. +echo "p ${UPROBE_TARGET}:${start_offset} ${stop_offset} threshold=3D500000= 000" > "$TLOB_MONITOR" + +# Target starts; start probe fires on tlob_preempt_work entry. +taskset -c "$cpu" "$UPROBE_TARGET" 5000 preempt & +busy_pid=3D$! +sleep 0.05 + +# RT hog on the same CPU preempts the target; target stays in waiting state +# (runnable, off-CPU) until the budget expires -> waiting_ns dominates. +chrt -f 99 taskset -c "$cpu" sh -c 'while true; do :; done' 2>/dev/null & +hog_pid=3D$! + +found=3D0; i=3D0 +while [ "$i" -lt 30 ]; do + sleep 0.1 + grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=3D1; break= ; } + i=3D$((i+1)) +done + +# Kill the RT hog first so tlob_target can release any in-flight SRCU read +# section from uprobe_notify_resume; otherwise probe removal blocks in +# synchronize_srcu with the hog monopolising the CPU at FIFO-99. +kill "$hog_pid" 2>/dev/null || true; wait "$hog_pid" 2>/dev/null || true +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo "-${UPROBE_TARGET}:${start_offset}" > "$TLOB_MONITOR" 2>/dev/null +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 0 > monitors/tlob/enable + +[ "$found" =3D "1" ] + +line=3D$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1) +running=3D$(echo "$line" | sed 's/.*running_ns=3D\([0-9]*\).*/\1/') +sleeping=3D$(echo "$line" | sed 's/.*sleeping_ns=3D\([0-9]*\).*/\1/') +waiting=3D$(echo "$line" | sed 's/.*waiting_ns=3D\([0-9]*\).*/\1/') +[ "$waiting" -gt "$((running + sleeping))" ] + +echo > /sys/kernel/tracing/trace diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.= tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc new file mode 100644 index 000000000000..a798f3e9b3fa --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc @@ -0,0 +1,64 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor multiple uprobe bindings (different offse= ts fire independently) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +busy_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/= dev/null) +busy_stop=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done = 2>/dev/null) +sleep_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2= >/dev/null) +sleep_stop=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work_don= e 2>/dev/null) +[ -n "$busy_offset" ] || exit_unsupported +[ -n "$busy_stop" ] || exit_unsupported +[ -n "$sleep_offset" ] || exit_unsupported +[ -n "$sleep_stop" ] || exit_unsupported + +"$UPROBE_TARGET" 30000 & # busy mode: tlob_busy_work fires every 200= ms +busy_pid=3D$! +"$UPROBE_TARGET" 30000 sleep & # sleep mode: tlob_sleep_work fires every 2= 00 ms +sleep_pid=3D$! +sleep 0.05 + +echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# Binding A: 5 s budget on the busy probe - must not fire in 200 ms loops. +echo "p ${UPROBE_TARGET}:${busy_offset} ${busy_stop} threshold=3D500000000= 0" > "$TLOB_MONITOR" +# Binding B: 10 us budget on the sleep probe - fires on first invocation. +echo "p ${UPROBE_TARGET}:${sleep_offset} ${sleep_stop} threshold=3D10000" = > "$TLOB_MONITOR" + +# Wait up to 2 s for error_env_tlob from binding B. +found=3D0; i=3D0 +while [ "$i" -lt 20 ]; do + sleep 0.1 + grep -q "error_env_tlob" /sys/kernel/tracing/trace && { found=3D1; break;= } + i=3D$((i+1)) +done + +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null +echo "-${UPROBE_TARGET}:${sleep_offset}" > "$TLOB_MONITOR" 2>/dev/null +kill "$sleep_pid" 2>/dev/null || true; wait "$sleep_pid" 2>/dev/null || tr= ue +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true + +echo 0 > monitors/tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable + +[ "$found" =3D "1" ] +# error_env_tlob payload: clock variable must be present. +# The event field can be "budget_exceeded" (hrtimer path) or the DA event +# name ("sleep", "preempt") depending on which fires first; don't constrai= n it. +grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk= _elapsed=3D" +# detail_env_tlob must appear alongside the error. +grep -q "detail_env_tlob" /sys/kernel/tracing/trace + +echo > /sys/kernel/tracing/trace diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_no_eve= nt.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc new file mode 100644 index 000000000000..bb2eeef17019 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc @@ -0,0 +1,19 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor no spurious events without active uprobe = binding +# requires: tlob:monitor + +TLOB_MONITOR=3Dmonitors/tlob/monitor + +echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +sleep 0.5 + +! grep -q "error_env_tlob" /sys/kernel/tracing/trace || false + +echo 0 > monitors/tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo > /sys/kernel/tracing/trace diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_violat= ion.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.= tc new file mode 100644 index 000000000000..8a94bd679b88 --- /dev/null +++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc @@ -0,0 +1,67 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later +# description: Test tlob monitor budget violation (error_env_tlob and deta= il_env_tlob fire with correct fields) +# requires: tlob:monitor + +RV_BINDIR=3D"${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}" +UPROBE_TARGET=3D"${RV_BINDIR}/tlob_target" +TLOB_SYM=3D"${RV_BINDIR}/tlob_sym" +[ -x "$UPROBE_TARGET" ] || exit_unsupported +[ -x "$TLOB_SYM" ] || exit_unsupported +TLOB_MONITOR=3Dmonitors/tlob/monitor + +busy_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/= dev/null) +stop_offset=3D$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_don= e 2>/dev/null) +[ -n "$busy_offset" ] || exit_unsupported +[ -n "$stop_offset" ] || exit_unsupported + +"$UPROBE_TARGET" 30000 & +busy_pid=3D$! +sleep 0.05 + +echo 1 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 1 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 1 > /sys/kernel/tracing/tracing_on +echo 1 > monitors/tlob/enable +echo > /sys/kernel/tracing/trace + +# 10 us budget - fires almost immediately; task is busy-spinning on-CPU. +echo "p ${UPROBE_TARGET}:${busy_offset} ${stop_offset} threshold=3D10000" = > "$TLOB_MONITOR" + +# wait up to 2 s for detail_env_tlob +found=3D0; i=3D0 +while [ "$i" -lt 20 ]; do + sleep 0.1 + grep -q "detail_env_tlob" /sys/kernel/tracing/trace && { found=3D1; break= ; } + i=3D$((i+1)) +done + +echo "-${UPROBE_TARGET}:${busy_offset}" > "$TLOB_MONITOR" 2>/dev/null +kill "$busy_pid" 2>/dev/null || true; wait "$busy_pid" 2>/dev/null || true +echo 0 > /sys/kernel/tracing/events/rv/error_env_tlob/enable +echo 0 > /sys/kernel/tracing/events/rv/detail_env_tlob/enable +echo 0 > monitors/tlob/enable + +[ "$found" =3D "1" ] + +# error_env_tlob must carry the clk_elapsed environment field. +# The event label is "budget_exceeded" when detected by the hrtimer callba= ck, +# or the triggering sched event name when detected by the constraint path = on a +# preemption that races with the timer (common on PREEMPT_RT / VM). Both = are +# valid detections; check the env field instead of the label. +grep "error_env_tlob" /sys/kernel/tracing/trace | head -n 1 | grep -q "clk= _elapsed=3D" + +# detail_env_tlob must have all five fields with the correct threshold +line=3D$(grep "detail_env_tlob" /sys/kernel/tracing/trace | head -n 1) +echo "$line" | grep -q "pid=3D" +echo "$line" | grep -q "threshold_ns=3D10000" +echo "$line" | grep -q "running_ns=3D" +echo "$line" | grep -q "waiting_ns=3D" +echo "$line" | grep -q "sleeping_ns=3D" + +# Busy-spin keeps the task on-CPU: running_ns must exceed sleeping_ns. +running=3D$(echo "$line" | sed 's/.*running_ns=3D\([0-9]*\).*/\1/') +sleeping=3D$(echo "$line" | sed 's/.*sleeping_ns=3D\([0-9]*\).*/\1/') +[ "$running" -gt "$sleeping" ] + +echo > /sys/kernel/tracing/trace --=20 2.25.1