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

enh enh at google.com
Tue Sep 30 13:22:41 PDT 2025


On Sun, Sep 28, 2025 at 2:10 PM Jesse Rosenstock <jmr at google.com> wrote:
>
> `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
> ---
>  lib/lib.c            |   7 +-
>  tests/taskset.test   |  31 ++++++++++++
>  toys/other/taskset.c | 130
> +++++++++++++++++++++++++++++++++++++++++++----------
>  3 files changed, 144 insertions(+), 24 deletions(-)
>
> diff --git a/lib/lib.c b/lib/lib.c
> index 7ae3ff66f0..b9d1f88b4e 100644
> --- a/lib/lib.c
> +++ b/lib/lib.c
> @@ -293,7 +293,12 @@
>  {
>    errno = 0;
>
> -  return strtoll(str, end, base);
> +  long long l = strtoll(str, end, base);
> +  // POSIX says strtoll *may* set errno to EINVAL when no input is consumed.
> +  // https://pubs.opengroup.org/onlinepubs/9799919799/functions/strtol.html
> +  // Set it consistently.

which libc doesn't set it? that would be a more useful comment...

> +  if (str == *end) errno = EINVAL;
> +  return l;
>  }
>
>  long long xstrtol(char *str, char **end, int base)
> 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..d4b1f1bb87 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,60 @@
>
>      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]].

wouldn't this be simpler with sscanf() (and accepting that you might
have 1, 2, or 3 matches)?

> +        long long a = estrtol(s, &s, 10);
> +        if (errno || a < 0) error_exit(failed_cpu_list, *toys.optargs);
> +
> +        long long b = a, step = 1;
> +        if (*s == '-') {
> +          s++;
> +          b = estrtol(s, &s, 10);
> +          if (errno || b >= TOYBUF_BITS || b < a) {
> +            error_exit(failed_cpu_list, *toys.optargs);
> +          }
> +
> +          if (*s == ':') {
> +            s++;
> +            step = estrtol(s, &s, 10);
> +            // Reject large steps to avoid overflow in loop below.
> +            if (errno || step < 1 || step >= TOYBUF_BITS) {
> +              error_exit(failed_cpu_list, *toys.optargs);
> +            }
> +          }
> +        }
> +
> +        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))
> _______________________________________________
> Toybox mailing list
> Toybox at lists.landley.net
> http://lists.landley.net/listinfo.cgi/toybox-landley.net


More information about the Toybox mailing list