[PATCH 34/36] tuna: Add --cpuset option to run command

John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:12 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add support for running commands directly in cpusets using the
--cpuset option. This allows launching processes with both CPU
isolation (via cpusets) and scheduling priority settings.

The implementation:
- Adds --cpuset CPUSET-NAME option to run command parser
- Makes --cpuset mutually exclusive with CPU affinity options (-c, -S, -N)
- Validates cpuset exists before launching the process
- Extends run_command() to accept cpuset_name parameter
- Implements parent-child coordination: child sets priority/affinity
  and execs command, parent migrates child to cpuset
- Handles race condition where fast commands finish before migration
  by checking process existence with signal 0
- Adds signal import for SIGTERM handling

Example usage:
  tuna run --cpuset my_rt 'taskset -pc $$ && sleep 5'
  tuna run --cpuset my_rt -p FIFO:50 'stress-ng --cpu 1'

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 tuna-cmd.py  | 14 ++++++++++++--
 tuna/tuna.py | 28 +++++++++++++++++++++++++++-
 2 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/tuna-cmd.py b/tuna-cmd.py
index 4703ea41f8d4..25257c465532 100755
--- a/tuna-cmd.py
+++ b/tuna-cmd.py
@@ -221,6 +221,7 @@ def gen_parser():
     run_group.add_argument('-c', '--cpus', **MODS['cpus'])
     run_group.add_argument('-S', '--sockets', **MODS['sockets'])
     run_group.add_argument('-N', '--nohz_full', **MODS['nohz_full'])
+    run_group.add_argument('--cpuset', type=str, metavar='CPUSET-NAME', help='Run process in specified cpuset (cgroup v2) instead of setting CPU affinity')
     run.add_argument('-p', '--priority', **MODS['priority'])
     run.add_argument('-b', '--background', **MODS['background'])
 
@@ -1659,7 +1660,7 @@ def main():
                         print(f"[ERROR] Invalid RT priority: {rtprio}. RT priorities must be 1-99", file=sys.stderr)
                         sys.exit(2)
                 # If only priority specified (no policy), assume RT and validate
-                elif policy is None and rtprio != 0:
+                elif policy is None and rtprio is not None and rtprio != 0:
                     if rtprio < 1 or rtprio > 99:
                         print(f"[ERROR] Invalid RT priority: {rtprio}. RT priorities must be 1-99", file=sys.stderr)
                         sys.exit(2)
@@ -1738,8 +1739,17 @@ def main():
                 sys.exit(1)
 
     elif args.command in ['run', 'r']:
+        # Check if using cpuset mode
+        cpuset_name = None
+        if hasattr(args, 'cpuset') and args.cpuset:
+            # Verify cpuset exists
+            cpuset_path = os.path.join('/sys/fs/cgroup', args.cpuset)
+            if not os.path.exists(cpuset_path):
+                print(f"Error: cpuset '{args.cpuset}' does not exist", file=sys.stderr)
+                sys.exit(1)
+            cpuset_name = args.cpuset
 
-        tuna.run_command(args.run_command, args.priority[0], args.priority[1], args.cpu_list, args.background)
+        tuna.run_command(args.run_command, args.priority[0], args.priority[1], args.cpu_list, args.background, cpuset_name)
 
     elif args.command in ['priority', 'p']:
 
diff --git a/tuna/tuna.py b/tuna/tuna.py
index 5e5f5578fbe4..fd08b5efe1ae 100755
--- a/tuna/tuna.py
+++ b/tuna/tuna.py
@@ -6,6 +6,7 @@ import copy
 import errno
 import os
 import re
+import signal
 import sys
 import shlex
 import fnmatch
@@ -708,7 +709,7 @@ def get_kthread_sched_tunings(proc=None):
 
     return kthreads
 
-def run_command(cmd, policy, rtprio, cpu_list, background):
+def run_command(cmd, policy, rtprio, cpu_list, background, cpuset_name=None):
     newpid = os.fork()
     if newpid == 0:
         cmd_list = shlex.split(cmd)
@@ -732,6 +733,31 @@ def run_command(cmd, policy, rtprio, cpu_list, background):
             print(f"tuna: {err}")
             sys.exit(2)
     else:
+        # If cpuset mode, move the child process to the cpuset
+        if cpuset_name:
+            from tuna import cpuset
+            try:
+                cs = cpuset.Cpuset(cpuset_name, existing_ok=True)
+                if not cs.write_pid(newpid):
+                    # Check if process still exists - if it exited quickly, that's okay
+                    try:
+                        os.kill(newpid, 0)  # Signal 0 just checks if process exists
+                        # Process still exists but couldn't be moved to cpuset - this is an error
+                        print(f"tuna: failed to move process {newpid} to cpuset '{cpuset_name}'", file=sys.stderr)
+                        os.kill(newpid, signal.SIGTERM)
+                        sys.exit(1)
+                    except OSError:
+                        # Process already exited - that's okay, it just ran quickly
+                        pass
+            except (OSError, ValueError) as e:
+                print(f"tuna: error accessing cpuset '{cpuset_name}': {e}", file=sys.stderr)
+                # Try to kill the child process if it still exists
+                try:
+                    os.kill(newpid, signal.SIGTERM)
+                except OSError:
+                    pass
+                sys.exit(1)
+
         if not background:
             os.waitpid(newpid, 0)
 
-- 
2.54.0