[PATCH 18/36] tuna: Add cpuset show and status commands

John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:56 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add two new cpuset subcommands to provide detailed information about
cpusets and system-wide overview:

1. 'tuna cpuset show <name>' - Display detailed information about a
   specific cpuset including CPUs, memory nodes, partition type, task
   count, and optional detailed task list with --show-tasks flag.

2. 'tuna cpuset status' - Display system-wide overview of all cpusets
   with summary statistics (total cpusets, CPUs assigned, tasks count,
   isolated cpusets) and a table view of all cpusets. Supports pattern
   filtering with --pattern and --skip-empty options.

Both commands share infrastructure through new helper functions:
- get_cpuset_info() - Gather detailed info about a single cpuset
- get_all_cpusets_info() - Gather info about multiple cpusets

These commands complement the existing create/list/destroy/move commands
and provide better visibility into cpuset configuration and usage.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 tuna-cmd.py | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 269 insertions(+)

diff --git a/tuna-cmd.py b/tuna-cmd.py
index 5c21d54bc2bf..b560ec805b7a 100755
--- a/tuna-cmd.py
+++ b/tuna-cmd.py
@@ -300,6 +300,17 @@ def gen_parser():
     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')
 
+    # cpuset show
+    cpuset_show = cpuset_subparser.add_parser('show', description='Show detailed information about a cpuset', help='Show cpuset details')
+    cpuset_show.add_argument('name', type=str, metavar='NAME', help='Cpuset name to show')
+    cpuset_show.add_argument('--show-tasks', action='store_true', help='Show detailed list of tasks/processes')
+
+    # cpuset status
+    cpuset_status = cpuset_subparser.add_parser('status', description='Show system-wide cpuset overview', help='Show cpuset status')
+    cpuset_status.add_argument('-p', '--pattern', type=str, metavar='PATTERN', help='Filter by glob pattern (e.g., tuna*)')
+    cpuset_status.add_argument('--skip-empty', action='store_true', help='Skip cpusets with no CPUs assigned')
+    cpuset_status.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only show top-level cpusets')
+
     return parser
 
 
@@ -831,6 +842,101 @@ def get_next_tuna_cpuset_name():
             return f'tuna{i}'
 
 
+def get_cpuset_info(cpuset_name):
+    """Get detailed information about a cpuset.
+
+    Args:
+        cpuset_name: Name of the cpuset (e.g., 'tuna0')
+
+    Returns:
+        dict: Cpuset information with keys:
+            - name: cpuset name
+            - path: full path to cpuset
+            - cpus: CPU list string (e.g., "0-3")
+            - mems: Memory nodes string (e.g., "0")
+            - tasks: List of PIDs
+            - task_count: Number of tasks
+            - partition: Partition type (isolated/member/root/n/a)
+            - blocklisted_pids: List of (pid, comm) tuples for blocklisted processes
+            - error: Error message if failed to read info
+
+    """
+    cs_path = os.path.join('/sys/fs/cgroup', cpuset_name)
+    info = {
+        'name': cpuset_name,
+        'path': cs_path,
+        'cpus': '',
+        'mems': '',
+        'tasks': [],
+        'task_count': 0,
+        'partition': 'n/a',
+        'blocklisted_pids': [],
+        'error': None
+    }
+
+    try:
+        # Read CPUs
+        with open(os.path.join(cs_path, 'cpuset.cpus'), 'r', encoding='utf-8') as f:
+            info['cpus'] = f.read().strip()
+
+        # Read memory nodes
+        with open(os.path.join(cs_path, 'cpuset.mems'), 'r', encoding='utf-8') as f:
+            info['mems'] = f.read().strip()
+
+        # Read tasks
+        with open(os.path.join(cs_path, 'cgroup.procs'), 'r', encoding='utf-8') as f:
+            info['tasks'] = [line.strip() for line in f if line.strip()]
+            info['task_count'] = len(info['tasks'])
+
+        # Read partition type (if available)
+        partition_file = os.path.join(cs_path, 'cpuset.cpus.partition')
+        if os.path.exists(partition_file):
+            with open(partition_file, 'r', encoding='utf-8') as f:
+                info['partition'] = f.read().strip()
+
+        # Check for blocklisted processes (skip system cgroups)
+        if not cpuset_name.endswith(('.mount', '.scope', '.slice')):
+            for pid_str in info['tasks']:
+                try:
+                    pid = int(pid_str)
+                    if cpuset.Cpuset._is_process_blocklisted(pid):
+                        comm = cpuset.Cpuset._get_process_name(pid)
+                        info['blocklisted_pids'].append((pid, comm))
+                except ValueError:
+                    pass
+
+    except (OSError, IOError) as e:
+        info['error'] = str(e)
+
+    return info
+
+
+def get_all_cpusets_info(pattern=None, recursive=True, skip_empty=False):
+    """Get information about all cpusets matching criteria.
+
+    Args:
+        pattern: Glob pattern to filter cpusets (None for all)
+        recursive: If True, search nested cpusets
+        skip_empty: If True, skip cpusets with no CPUs assigned
+
+    Returns:
+        list: List of cpuset info dictionaries (from get_cpuset_info)
+    """
+    cpusets = cpuset.list_cpusets(pattern=pattern, recursive=recursive)
+    infos = []
+
+    for cs_name in cpusets:
+        info = get_cpuset_info(cs_name)
+
+        # Skip empty cpusets if requested
+        if skip_empty and not info['cpus']:
+            continue
+
+        infos.append(info)
+
+    return infos
+
+
 def cpuset_create(cpu_list, name, isolated, memory_nodes=None):
     """Handler for 'tuna cpuset create' command.
 
@@ -1147,6 +1253,163 @@ def cpuset_move(name, thread_list, pid_list):
         sys.exit(1)
 
 
+def cpuset_show(name, show_tasks):
+    """Handler for 'tuna cpuset show' command.
+
+    Args:
+        name: Cpuset name to show details for
+        show_tasks: If True, show detailed task list
+    """
+    # 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)
+
+    # 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)
+
+    # Get cpuset info
+    info = get_cpuset_info(name)
+
+    if info['error']:
+        print(f"Error reading cpuset info: {info['error']}", file=sys.stderr)
+        sys.exit(1)
+
+    # Print detailed information
+    print(f"Cpuset: {name}")
+    print(f"Path: {info['path']}")
+    print(f"CPUs: {info['cpus'] or '(none)'}")
+    print(f"Memory Nodes: {info['mems'] or '(none)'}")
+    print(f"Partition: {info['partition']}")
+    print(f"Task Count: {info['task_count']}")
+
+    # Show task details if requested
+    if show_tasks and info['tasks']:
+        print(f"\nTasks ({len(info['tasks'])} total):")
+        print(f"{'PID':<10} {'COMMAND':<30} {'STATUS':<10}")
+        print("-" * 52)
+
+        for pid_str in info['tasks']:
+            try:
+                pid = int(pid_str)
+                comm = cpuset.Cpuset._get_process_name(pid)
+                is_blocked = cpuset.Cpuset._is_process_blocklisted(pid)
+                status = "BLOCKED" if is_blocked else "ok"
+                print(f"{pid:<10} {comm:<30} {status:<10}")
+            except (ValueError, OSError):
+                print(f"{pid_str:<10} {'(error)':<30} {'error':<10}")
+
+    # Show blocklist warnings
+    if info['blocklisted_pids']:
+        print()
+        print("⚠️  WARNINGS:")
+        for pid, comm in info['blocklisted_pids']:
+            print(f"  - Blocklisted process PID {pid} ({comm})", file=sys.stderr)
+        print("  These critical system processes should not be in custom cpusets!", file=sys.stderr)
+
+
+def cpuset_status(pattern, skip_empty, recursive):
+    """Handler for 'tuna cpuset status' command.
+
+    Args:
+        pattern: Glob pattern to filter cpusets (None for all)
+        skip_empty: If True, skip cpusets with no CPUs assigned
+        recursive: If True, search nested cpusets
+    """
+    # 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)
+
+    # Get all cpusets info
+    infos = get_all_cpusets_info(pattern=pattern, recursive=recursive, skip_empty=skip_empty)
+
+    if not infos:
+        if pattern:
+            print(f"No cpusets found matching pattern '{pattern}'")
+        else:
+            print("No cpusets found")
+        return
+
+    # Calculate system-wide statistics
+    total_cpus_assigned = set()
+    total_tasks = 0
+    isolated_cpusets = []
+    cpusets_with_tasks = []
+
+    for info in infos:
+        if info['cpus']:
+            # Parse CPU list to get individual CPUs
+            try:
+                cpu_list = tuna.expand_cpulist(info['cpus'])
+                total_cpus_assigned.update(cpu_list)
+            except (ValueError, IndexError):
+                pass
+
+        total_tasks += info['task_count']
+
+        if info['partition'] == 'isolated':
+            isolated_cpusets.append(info['name'])
+
+        if info['task_count'] > 0:
+            cpusets_with_tasks.append(info['name'])
+
+    # Print system overview
+    print("=" * 70)
+    print("SYSTEM CPUSET OVERVIEW")
+    print("=" * 70)
+    print(f"Total cpusets: {len(infos)}")
+    print(f"CPUs assigned: {len(total_cpus_assigned)}")
+    print(f"Total tasks: {total_tasks}")
+    print(f"Isolated cpusets: {len(isolated_cpusets)}")
+    print()
+
+    # Print cpuset summary table
+    print(f"{'NAME':<25} {'CPUS':<20} {'MEMS':<8} {'TASKS':<7} {'PARTITION':<10}")
+    print("-" * 75)
+
+    for info in infos:
+        cpus_display = info['cpus'] or "(none)"
+        mems_display = info['mems'] or "(none)"
+
+        # Truncate long names with ellipsis
+        name_display = info['name']
+        if len(name_display) > 24:
+            name_display = name_display[:21] + "..."
+
+        # Truncate long CPU lists
+        if len(cpus_display) > 19:
+            cpus_display = cpus_display[:16] + "..."
+
+        print(f"{name_display:<25} {cpus_display:<20} {mems_display:<8} {info['task_count']:<7} {info['partition']:<10}")
+
+    # Show isolated cpusets if any
+    if isolated_cpusets:
+        print()
+        print(f"Isolated cpusets ({len(isolated_cpusets)}):")
+        for name in isolated_cpusets:
+            print(f"  - {name}")
+
+    # Check for blocklisted processes across all cpusets
+    warnings = []
+    for info in infos:
+        if info['blocklisted_pids']:
+            for pid, comm in info['blocklisted_pids']:
+                warnings.append((info['name'], pid, comm))
+
+    if warnings:
+        print()
+        print("⚠️  WARNINGS - Blocklisted processes found:")
+        for cpuset_name, pid, comm in warnings:
+            print(f"  - {cpuset_name}: PID {pid} ({comm})", file=sys.stderr)
+        print("  These critical system processes should not be in custom cpusets!", file=sys.stderr)
+
+
 def main():
     i18n_init()
     parser = gen_parser()
@@ -1370,6 +1633,12 @@ def main():
         elif args.cpuset_command == 'move':
             cpuset_move(args.name, args.thread_list, args.pid_list)
 
+        elif args.cpuset_command == 'show':
+            cpuset_show(args.name, args.show_tasks)
+
+        elif args.cpuset_command == 'status':
+            cpuset_status(args.pattern, args.skip_empty, args.recursive)
+
 
 if __name__ == '__main__':
     main()
-- 
2.54.0