Implement ionice - #637
Open
mmclinton wants to merge 3 commits into
Open
Conversation
added 3 commits
August 29, 2026 12:27
ionice reads and sets the I/O scheduling class and priority of a process through ioprio_get and ioprio_set. The option surface follows ionice(1): -c, -n, -p, -P, -u and -t, with trailing operands being further ids when an id option was given and the command to run otherwise. Behavior throughout is what util-linux 2.42.2 was observed doing; no C source was read. The parts a reader should not have to reverse engineer: - The idle class carries priority level 7 and none carries 0, whatever -n asks for. Every idle priority prints as the bare word "idle" regardless of the level packed with it, so stdout cannot show this - only the syscall arguments can. None is forced by the kernel, which refuses a none class carrying a nonzero level; idle's 7 is the reference's convention. - A lone id of 0 on the read path means the calling process rather than the group or user numbered 0, and only when there are no further ids and no priority is being set. Writing never falls back: -c 3 -u 0 really does target uid 0. - Options are diagnosed left to right, reconstructed from clap's value indices, so the leftmost bad one is reported. A second id option is rejected before its value is looked at. - -c outside 0-3 and -n outside 0-7 are rejected with exit 1. ionice(1) documents both ranges and util-linux 2.42.2 enforces neither: it hands the value to the kernel, which reinterprets it wherever the reinterpretation is legal, so -c 99 lands on idle and -n 8192 changes the class outright, both exiting 0, and an accepted -n 8 really means level 0 because the kernel reads the level modulo eight. Where the wrap lands on something the kernel will not take, as with -c 4 and -n -1, the set fails EINVAL and the reference exits 1 - which is what we exit as well. This is a deliberate divergence in which argv succeed, put to the maintainers as uutils#624 and answered with "you can enforce and document it". The disclosure belongs in the pull request body, so ionice.md and --help are left alone on purpose. One rider comes with it: the "unknown prio class N" warning no longer exists, because no class can be unknown any more, so -t has one fewer diagnostic to silence. - A value too wide for an i32 keeps its ERANGE wording, which is a failure the reference produces as well, and it is reported ahead of the documented range. - ioprio.rs owns the ioprio word - its bit layout, the class numbers and names, how it renders, and the two syscalls that carry it, which are the crate's only unsafe. ionice.rs owns what this tool decides to put in that word. The class stays an i32 rather than becoming an enum because the kernel treats it as an open value. Known divergences, each structural to clap rather than a defect in the port: the --help and --version layout, clap's diagnostics where getopt has its own, and clap stripping the = from an attached short-option value, which is tree-wide and which merged utilities already carry.
Fifty-five tests, plus one for the non-Linux arm: reading a priority by pid, pgid and uid, the lone-zero-id fallback, setting each class and level, the two classes that discard a given level, argument rejection on every numeric option, the command path with its 126 and 127 statuses, and the test_invalid_arg smoke test every other util carries. They defend the decisions a refactor would plausibly undo rather than restating what clap already guarantees. The idle level is read back with a direct syscall rather than through the tool's own encoder, because stdout prints the bare word "idle" whatever level is packed with it and an assertion sharing the encoder would agree with it wherever it was wrong. The lone-zero-id fallback, the left-to-right order of diagnosis, and the precedence of an i32 overflow over the documented range each have a test whose only job is to fail if someone tidies them away. Anything that needs a set to be refused is guarded on geteuid and skipped under root, which would let the set succeed - and in the pid 1 case would re-class init rather than earning the EPERM the test expects. CI is never root, so these run everywhere it matters. Diagnostics are asserted only as far as we write them; the errno text after our half belongs to libc. Two of them are about argument bytes rather than about ionice. The coreutils contributing guide asks that anything that could be an invalid-Unicode path stay OsStr, and here that is the command to exec: it is taken as an OsString and handed to process::Command untouched. Nothing proved it. A directory makes the discriminator cheap - exec refuses one with EACCES, so a 126 says the name reached the kernel intact where a mangled one would have named nothing and given 127. The other test holds the value options to the same rule from the other side: a -p value that is not valid Unicode has to reach our parser and be diagnosed as a bad PID, not be refused by clap first. Both were checked against a deliberately broken build before being kept: a String value parser on -p fails the second, a lossy Command::new fails the first, and neither test fails for the other's reason.
Both said the idle class has no priority level. It has one; ionice just does not print it. The kernel takes whatever level is packed with an idle set and hands it back unchanged - fixed_data packs 7 with every idle set, and the integration tests assert that ioprio_get returns (3 << 13) | 7 for a process one was applied to, which is only possible because the level survives. The wording mattered for the next person to touch either place: told the kernel ignores the level, they would read the 7 as arbitrary and the read-back assertion as pointless, and weakening that assertion would remove the only check on our idle encoding, since stdout prints the bare word "idle" whatever level it carries. The claim behind the choice of 7 is unchanged and still stands: nothing in the kernel forces it, so it is the reference's convention that we follow. Comment text only; no code line moved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ioniceshows or changes the I/O scheduling class and priority of a process. This is all of ionice(1):-c,-n,-p,-P,-u,-t,-h,-V, reading a priority with no arguments, setting one on a pid, pgid or uid, and running a command under a priority with the 126 and 127 statuses that go with a failed exec. No option is half implemented. It is Linux only; the other targets compile and exit 1 with a message.I built this against util-linux 2.42.2 and checked it against 2.37.4, 2.39.2 and 2.41.1 as well, comparing stdout, stderr, exit status and the arguments of the
ioprio_getandioprio_setsyscalls over 139 invocations. Those four references do not differ from each other on a single one of those cases, so what follows holds for all of them.The documented ranges are enforced
-coutside 0-3 and-noutside 0-7 are rejected with exit status 1 and no syscall. util-linux accepts most of them and exits 0 - it refuses only the ones whose wrap lands on a class the kernel will not take, such as-c 4and-n -1- so this is a deliberate divergence in which arguments succeed, not just in what gets printed. It is the question I raised in #624, and the answer there was to enforce and document.The reason it is worth a divergence is that the accepted values do not mean what they read as. The kernel takes the class from three bits and reads the level modulo eight, so
ionice -c 99runs idle,ionice -n 8192changes the class outright, and-n 8is accepted as a level of 0 - the highest best-effort priority, one step past the bottom of the documented range rather than one step below it.-c 10printsunknown prio class 10and then runs best-effort at exit 0 anyway; with-tit prints nothing at all.Three things follow that whoever reviews this may otherwise notice:
ionice: unknown prio class Nwarning does not exist here, because no class can be unknown any more.-ttherefore has one fewer thing to silence.-c 4and-n -1among them, we agree with it at 1.i32still reportsERANGE, unchanged, because the reference errors there too.ionice.mdand--helpare deliberately not amended. They already state both ranges, and per Sylvestre's advice this body is the disclosure.Two behaviors that are not in ionice(1)
Neither is visible in the output, and both are matched here:
idleclass always carries priority level 7 andnonealways carries 0, whatever-nasks for.0on the read path means the calling process, not the process group or the user numbered 0.The syscalls
There are no libc wrappers for
ioprio_getandioprio_set, soioprio.rscalls them throughsyscall(). Those two blocks are the onlyunsafein the crate.What the tests do not cover
55 integration tests, plus one for the non-Linux arm. Four items worth naming:
CAP_SYS_NICE, so only theEPERMrefusal is tested. A user namespace does not help: the kernel checks realtime ioprio against the initial user namespace, so uid 0 insideunshare -Uris refused exactly as before.-p.-Pand-uwrites are never verified against a live target.-cor-non a process the caller does not own beyond theEPERMcase, which earns its refusal from pid 1 belonging to root rather than from the class being lowered.Closes #624