[Toybox] [PATCH v2] taskset: Add -c cpulist support

Jesse Rosenstock jmr at google.com
Thu Oct 2 02:06:05 PDT 2025


`taskset -c` is supported by util-linux as a user-friendly way of
specifying the affinity mask.

https://man7.org/linux/man-pages/man1/taskset.1.html#OPTIONS

v1 -> v2:
* Use sscanf() instead of estrtol() to parse CPU lists
* Revert lib/lib.c estrtol() change
---
 tests/taskset.test   |  31 ++++++++++++
 toys/other/taskset.c | 127
+++++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 135 insertions(+), 23 deletions(-)

diff --git a/tests/taskset.test b/tests/taskset.test
index 5732e23f0e..33e751b450 100755
--- a/tests/taskset.test
+++ b/tests/taskset.test
@@ -22,6 +22,37 @@
 testing 'set mask to last' \
   '(taskset $LAST taskset -p 0 | sed "s/.*: //")' "$LAST\n" '' ''

+# mask -> CPU list
+if [ $CPUS -gt 1 ]; then
+  testing 'print all as CPU list' \
+    '(taskset $MASK taskset -pc 0 | sed "s/.*: //")' "0-$((CPUS-1))\n" '' ''
+fi
+testing 'print first as CPU list' \
+  '(taskset 1 taskset -pc 0 | sed "s/.*: //")' '0\n' '' ''
+testing 'print last as CPU list' \
+  '(taskset $LAST taskset -pc 0 | sed "s/.*: //")' "$((CPUS-1))\n" '' ''
+
+# CPU list -> mask
+testing 'set CPU list to all' \
+  '(taskset -c 0-$((CPUS-1)) taskset -p 0 | sed "s/.*: //")' "$MASK\n" '' ''
+testing 'set CPU list to first' \
+  '(taskset -c 0 taskset -p 0 | sed "s/.*: //")' '1\n' '' ''
+testing 'set CPU list to last' \
+  '(taskset -c $((CPUS-1)) taskset -p 0 | sed "s/.*: //")' "$LAST\n" '' ''
+
+# CPU list -> CPU list
+if [ $CPUS -gt 2 ]; then
+  testing 'even CPU list' \
+    '(taskset -c $(seq -s, 0 2 $CPUS) taskset -pc 0 | sed "s/.*: //")' \
+    "0-$((($CPUS-1)&~1)):2\n" '' ''
+fi
+if [ $CPUS -gt 5 ]; then
+  # Prints as 0,1-5:2, not 0,1,3,5, since 1 not consumed as part of a
+  # range starting at 0.
+  testing 'CPU list printing not greedy' \
+    '(taskset -c 0,1-5:2 taskset -pc 0 | sed "s/.*: //")' '0,1-5:2\n' '' ''
+fi
+
 # alas procps-ng always says "-" for -o cpu so fetch the field from /proc
 testing 'run on first' \
   'X=$(taskset 1 cat /proc/self/stat); x() { echo ${39};}; x $X' \
diff --git a/toys/other/taskset.c b/toys/other/taskset.c
index 94044ebd07..1db2fbb429 100644
--- a/toys/other/taskset.c
+++ b/toys/other/taskset.c
@@ -2,7 +2,7 @@
  *
  * Copyright 2012 Elie De Brauwer <eliedebrauwer at gmail.com>

-USE_TASKSET(NEWTOY(taskset, "^pa", TOYFLAG_USR|TOYFLAG_BIN))
+USE_TASKSET(NEWTOY(taskset, "^pac", TOYFLAG_USR|TOYFLAG_BIN))
 USE_NPROC(NEWTOY(nproc, "a(all)", TOYFLAG_USR|TOYFLAG_BIN))

 config NPROC
@@ -19,17 +19,19 @@
   bool "taskset"
   default y
   help
-    usage: taskset [-ap] [mask] [PID | cmd [args...]]
+    usage: taskset [-apc] [mask] [PID | cmd [args...]]

     Launch a new task which may only run on certain processors, or change
     the processor affinity of an existing PID.

-    Mask is a hex string where each bit represents a processor the process
-    is allowed to run on. PID without a mask displays existing affinity.
-    A PID of zero means the taskset process.
+    The mask may be specified as  a hex string where each bit represents a
+    processor the process is allowed to run on, or as a CPU list with the
+    -c option. PID without a mask displays existing affinity. A PID of zero
+    means the taskset process.

     -p Set/get affinity of given PID instead of a new command
     -a Set/get affinity of all threads of the PID
+    -c Specify mask as a cpu list, for example 1,3,4-8:2
 */

 #define FOR_taskset
@@ -41,6 +43,16 @@
 #define sched_getaffinity(pid, size, cpuset) \
   syscall(__NR_sched_getaffinity, (pid_t)pid, (size_t)size, (void *)cpuset)

+#define TOYBUF_BITS (8*sizeof(toybuf))
+
+static int find_next_cpu(unsigned long *mask, int i)
+{
+  for (; i < TOYBUF_BITS; i++)
+    if (mask[i/(8*sizeof(long))] & (1UL << (i%(8*sizeof(long)))))
+      return i;
+  return i;
+}
+
 static void do_taskset(pid_t pid)
 {
   unsigned long *mask = (unsigned long *)toybuf;
@@ -56,11 +68,43 @@
       if (toys.optc)
         printf("pid %d's %s affinity mask: ", pid, i ? "new" : "current");

-      for (j = sizeof(toybuf)/sizeof(long), k = 0; --j>=0;) {
-        if (k) printf("%0*lx", (int)(2*sizeof(long)), mask[j]);
-        else if (mask[j]) {
-          k++;
-          printf("%lx", mask[j]);
+      if (FLAG(c)) {
+        // Print as cpu list
+        int next = 0;  // where to start looking for next cpu
+        int a;  // start of range
+
+        while ((a = find_next_cpu(mask, next)) < TOYBUF_BITS) {
+          // next is 0 only on first iteration.
+          if (next > 0) putchar(',');
+          next = find_next_cpu(mask, a + 1);
+          if (next == TOYBUF_BITS) {
+            printf("%d", a);
+          } else {
+            // Extend range as long as the step size is the same.
+            int b = next, step = b - a;
+            while ((next = find_next_cpu(mask, b + 1)) < TOYBUF_BITS
+                   && next - b == step) {
+              b = next;
+            }
+            if (b - a == step) {
+              // Only print one CPU; try to put the next CPU in the next range.
+              printf("%d", a);
+              next = b;
+            } else if (step == 1) {
+              printf("%d-%d", a, b);
+            } else {
+              printf("%d-%d:%d", a, b, step);
+            }
+          }
+        }
+      } else {
+        // Print as mask
+        for (j = sizeof(toybuf)/sizeof(long), k = 0; --j>=0;) {
+          if (k) printf("%0*lx", (int)(2*sizeof(long)), mask[j]);
+          else if (mask[j]) {
+            k++;
+            printf("%lx", mask[j]);
+          }
         }
       }
       putchar('\n');
@@ -68,20 +112,57 @@

     if (i || toys.optc < 2) return;

-    // Convert hex string to mask[] bits
     memset(toybuf, 0, sizeof(toybuf));
-    j = (k = strlen(s = *toys.optargs))-2*sizeof(toybuf);
-    if (j>0) {
-      s += j;
-      k -= j;
-    }
-    s += k;
-    for (j = 0; j<k; j++) {
-      unsigned long digit = *(--s) - '0';
-
-      if (digit > 9) digit = 10 + tolower(*s)-'a';
-      if (digit > 15) error_exit("bad mask '%s'", *toys.optargs);
-      mask[j/(2*sizeof(long))] |= digit << 4*(j&((2*sizeof(long))-1));
+    if (FLAG(c)) {
+      // Convert cpu list to mask[] bits
+      char* failed_cpu_list = "failed to parse CPU list: %s";
+      s = *toys.optargs;
+      do {
+        if (s != *toys.optargs) {
+          if (*s != ',') {
+            error_exit(failed_cpu_list, *toys.optargs);
+          }
+          s++;
+        }
+
+        // Parse a[-b[:step]].
+        int a, b, step = 1;
+        int nc = 0;
+        int n = sscanf(s, "%d%n-%d%n:%d%n", &a, &nc, &b, &nc, &step, &nc);
+
+        if (n == 1) {
+          b = a;
+        }
+
+        // Reject large steps to avoid overflow in loop below.
+        if (n<=0 || a<0 || b>=TOYBUF_BITS || b<a
+            || step<1 || step>=TOYBUF_BITS) {
+          error_exit(failed_cpu_list, *toys.optargs);
+        }
+
+        // Since `nc` is updated before [-:] is parsed, any other char will
+        // be left in `s` and cause a parse error in the next iteration.
+        s += nc;
+
+        for (j = a; j <= b; j += step) {
+          mask[j/(8*sizeof(long))] |= 1UL << (j%(8*sizeof(long)));
+        }
+      } while (*s);
+    } else {
+      // Convert hex string to mask[] bits.
+      j = (k = strlen(s = *toys.optargs))-2*sizeof(toybuf);
+      if (j>0) {
+        s += j;
+        k -= j;
+      }
+      s += k;
+      for (j = 0; j<k; j++) {
+        unsigned long digit = *(--s) - '0';
+
+        if (digit > 9) digit = 10 + tolower(*s)-'a';
+        if (digit > 15) error_exit("bad mask '%s'", *toys.optargs);
+        mask[j/(2*sizeof(long))] |= digit << 4*(j&((2*sizeof(long))-1));
+      }
     }

     if (-1 == sched_setaffinity(pid, sizeof(toybuf), (void *)mask))
-------------- next part --------------
A non-text attachment was scrubbed...
Name: taskset-cpu-list-v2.patch
Type: text/x-patch
Size: 7308 bytes
Desc: not available
URL: <http://lists.landley.net/pipermail/toybox-landley.net/attachments/20251002/7ff3ef93/attachment-0001.bin>


More information about the Toybox mailing list