[PATCH 12/36] tuna: Add cpuset move command for process migration
John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:50 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Implements 'tuna cpuset move' command to move processes into existing cpusets. Supports two input methods: - --threads: Thread list with pattern matching (e.g., kworker*) - --pids: Comma-separated list of PIDs Key features: - Validates cpuset exists before attempting moves - Gracefully handles invalid PIDs (returns False, continues) - Reports success/failure counts - Exits with error if all moves fail Also adds existing_ok parameter to Cpuset class to suppress warnings when accessing existing cpusets (follows os.makedirs convention). Changes: - tuna-cmd.py: Add cpuset move argument parser, handler, and dispatcher - tuna/cpuset.py: Add existing_ok parameter to Cpuset.__init__() and create_cpuset() to allow accessing existing cpusets without warnings Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- tuna-cmd.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ tuna/cpuset.py | 22 ++++++++---- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/tuna-cmd.py b/tuna-cmd.py index 2ff73426ba4f..0e70b9208665 100755 --- a/tuna-cmd.py +++ b/tuna-cmd.py @@ -292,6 +292,12 @@ def gen_parser(): cpuset_destroy.add_argument('--skip-empty', action='store_true', help='When using --pattern, skip cpusets with no CPUs assigned') cpuset_destroy.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only destroy top-level cpusets when using --pattern') + # cpuset move + cpuset_move = cpuset_subparser.add_parser('move', description='Move processes to a cpuset', help='Move processes to a cpuset') + cpuset_move.add_argument('-n', '--name', type=str, metavar='NAME', required=True, help='Cpuset name to move processes to') + cpuset_move.add_argument('-t', '--threads', **MODS['threads']) + cpuset_move.add_argument('-p', '--pids', dest='pid_list', type=str, metavar='PID-LIST', help='Comma-separated list of PIDs to move') + return parser @@ -1009,6 +1015,87 @@ def cpuset_destroy(name, pattern, force, skip_empty, recursive): sys.exit(1) +def cpuset_move(name, thread_list, pid_list): + """Handler for 'tuna cpuset move' command. + + Args: + name: Cpuset name to move processes to + thread_list: Thread list (already converted to list of PIDs by main()) + pid_list: PID list string (comma-separated PIDs only) + """ + # Check cgroup v2 support + ci = cpuset.CpusetsInit() + if not ci.supported: + print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr) + sys.exit(1) + + # Validate: must specify at least one of thread_list or pid_list + if not thread_list and not pid_list: + print("Error: must specify either --threads or --pids", file=sys.stderr) + sys.exit(2) + + # Check if cpuset exists + cpuset_path = os.path.join('/sys/fs/cgroup', name) + if not os.path.exists(cpuset_path): + print(f"Error: cpuset '{name}' does not exist", file=sys.stderr) + sys.exit(1) + + # Create cpuset object for the target cpuset + try: + cs = cpuset.Cpuset(name, existing_ok=True) + except (OSError, ValueError) as e: + print(f"Error accessing cpuset '{name}': {e}", file=sys.stderr) + sys.exit(1) + + # Build list of PIDs to move + pids = [] + + # Process thread_list if provided (already converted to list of PIDs by main()) + if thread_list: + # thread_list is already a list of integers from main()'s conversion + pids.extend(thread_list) + + # Process pid_list if provided (simple comma-separated PIDs) + if pid_list: + import re + pid_strings = [s.strip() for s in re.split(r'[,\s]+', pid_list) if s.strip()] + for pid_str in pid_strings: + if pid_str.isdigit(): + pids.append(int(pid_str)) + else: + print(f"Warning: ignoring invalid PID '{pid_str}'", file=sys.stderr) + + # Remove duplicates + pids = list(set(pids)) + + if not pids: + print("Error: no valid PIDs to move", file=sys.stderr) + sys.exit(2) + + # Move each PID to the cpuset + success_count = 0 + fail_count = 0 + + for pid in pids: + if cs.write_pid(pid): + success_count += 1 + else: + # write_pid returns False on failure + print(f"Warning: failed to move PID {pid} to cpuset '{name}'", file=sys.stderr) + fail_count += 1 + + # Print summary + print(f"Moved {success_count} process(es) to cpuset '{name}'", end='') + if fail_count > 0: + print(f" ({fail_count} failed)") + else: + print() + + # Exit with error code if all moves failed + if success_count == 0: + sys.exit(1) + + def main(): i18n_init() parser = gen_parser() @@ -1229,6 +1316,9 @@ def main(): elif args.cpuset_command == 'destroy': cpuset_destroy(args.name, args.pattern, args.force, args.skip_empty, args.recursive) + elif args.cpuset_command == 'move': + cpuset_move(args.name, args.thread_list, args.pid_list) + if __name__ == '__main__': main() diff --git a/tuna/cpuset.py b/tuna/cpuset.py index 4a3a71416c53..1039eb6c2c49 100644 --- a/tuna/cpuset.py +++ b/tuna/cpuset.py @@ -22,7 +22,7 @@ class Cpuset: mpath = '/sys/fs/cgroup' - def __init__(self, name=None, auto_destroy=False): + def __init__(self, name=None, auto_destroy=False, existing_ok=False): """ Initialize a cpuset @@ -32,11 +32,14 @@ class Cpuset: exiting a context manager (rteval style). If False (default), cpuset persists after program exits (tuna style). + existing_ok: If True, suppress warning when cpuset already exists. + Use this when accessing an existing cpuset rather than + creating a new one (default: False). """ self.cpuset_name = None self._cpuset_path = None self.auto_destroy = auto_destroy - self.create_cpuset(name) + self.create_cpuset(name, existing_ok=existing_ok) @property def cpuset_path(self): @@ -46,12 +49,16 @@ class Cpuset: def __str__(self): return f'cpuset_name={self.cpuset_name}, cpuset_path={self.cpuset_path}' - def create_cpuset(self, name=None): + def create_cpuset(self, name=None, existing_ok=False): """ Create a cpuset (cgroup) below /sys/fs/cgroup - If the cpuset already exists, prints a warning and returns without - creating a new one. + If the cpuset already exists, prints a warning (unless existing_ok=True) + and returns without creating a new one. + + Args: + name: Name of the cpuset to create + existing_ok: If True, don't warn when cpuset already exists """ if name is None: raise ValueError("cpuset name cannot be None") @@ -60,8 +67,9 @@ class Cpuset: # Check if cpuset already exists if os.path.exists(path): - logger.warning(f"Cpuset '{name}' already exists at {path}") - print(f"Warning: cpuset '{name}' already exists at {path}") + if not existing_ok: + logger.warning(f"Cpuset '{name}' already exists at {path}") + print(f"Warning: cpuset '{name}' already exists at {path}") self._cpuset_path = path return -- 2.54.0