[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