diff --git a/nixos/modules/hardware/iosched.nix b/nixos/modules/hardware/iosched.nix new file mode 100644 index 000000000000..a7a4a2b41682 --- /dev/null +++ b/nixos/modules/hardware/iosched.nix @@ -0,0 +1,150 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.hardware.block; + escape = lib.strings.escape [ ''"'' ]; + udevValue = types.addCheck types.nonEmptyStr (x: builtins.match "[^\n\r]*" x != null) // { + name = "udevValue"; + description = "udev rule value"; + descriptionClass = "noun"; + }; + + inherit (lib) + mkIf + mkOption + types + + concatLines + concatStringsSep + mapAttrsToList + optional + ; +in +{ + options.hardware.block = { + defaultScheduler = mkOption { + type = types.nullOr udevValue; + default = null; + description = '' + Default block I/O scheduler. + + Unless `null`, the value is assigned through a udev rule matching all + block devices. + ''; + example = "kyber"; + }; + + defaultSchedulerRotational = mkOption { + type = types.nullOr udevValue; + default = null; + description = '' + Default block I/O scheduler for rotational drives (e.g. hard disks). + + Unless `null`, the value is assigned through a udev rule matching all + rotational block devices. + + This option takes precedence over + {option}`config.hardware.block.defaultScheduler`. + ''; + example = "bfq"; + }; + + scheduler = mkOption { + type = types.attrsOf udevValue; + default = { }; + description = '' + Assign block I/O scheduler by device name pattern. + + Names are matched using the {manpage}`udev(7)` pattern syntax: + + `*` + : Matches zero or more characters. + + `?` + : Matches any single character. + + `[]` + : Matches any single character specified in the brackets. Ranges are + supported via the `-` character. + + `|` + : Separates alternative patterns. + + + Please note that overlapping patterns may produce unexpected results. + More complex configurations requiring these should instead be specified + directly through custom udev rules, for example via + [{option}`config.services.udev.extraRules`](#opt-services.udev.extraRules), + to ensure correct ordering. + + Available schedulers depend on the kernel configuration but modern + Linux systems typically support: + + `none` + : No‐operation scheduler with no re‐ordering of requests. Suitable + for devices with fast random I/O such as NVMe SSDs. + + [`mq-deadline`](https://www.kernel.org/doc/html/latest/block/deadline-iosched.html) + : Simple latency‐oriented general‐purpose scheduler. + + [`kyber`](https://www.kernel.org/doc/html/latest/block/kyber-iosched.html) + : Simple latency‐oriented scheduler for fast multi‐queue devices + like NVMe SSDs. + + [`bfq`](https://www.kernel.org/doc/html/latest/block/bfq-iosched.html) + : Complex fairness‐oriented scheduler. Higher processing overhead, + but good interactive response, especially with slower devices. + + + Schedulers assigned through this option take precedence over + {option}`config.hardware.block.defaultScheduler` and + {option}`config.hardware.block.defaultSchedulerRotational` but may be + overridden by other udev rules. + ''; + example = { + "mmcblk[0-9]*" = "bfq"; + "nvme[0-9]*" = "kyber"; + }; + }; + }; + + config = + mkIf + (cfg.defaultScheduler != null || cfg.defaultSchedulerRotational != null || cfg.scheduler != { }) + { + services.udev.packages = [ + (pkgs.writeTextDir "etc/udev/rules.d/98-block-io-scheduler.rules" ( + concatLines ( + map (concatStringsSep ", ") ( + optional (cfg.defaultScheduler != null) [ + ''SUBSYSTEM=="block"'' + ''ACTION=="add|change"'' + ''TEST=="queue/scheduler"'' + ''ATTR{queue/scheduler}="${escape cfg.defaultScheduler}"'' + ] + ++ optional (cfg.defaultSchedulerRotational != null) [ + ''SUBSYSTEM=="block"'' + ''ACTION=="add|change"'' + ''ATTR{queue/rotational}=="1"'' + ''TEST=="queue/scheduler"'' + ''ATTR{queue/scheduler}="${escape cfg.defaultSchedulerRotational}"'' + ] + ++ mapAttrsToList (name: sched: [ + ''SUBSYSTEM=="block"'' + ''ACTION=="add|change"'' + ''KERNEL=="${escape name}"'' + ''ATTR{queue/scheduler}="${escape sched}"'' + ]) cfg.scheduler + ) + ) + )) + ]; + }; + + meta.maintainers = with lib.maintainers; [ mvs ]; +} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 9dcd3e88bfe8..44c0d499e275 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -72,6 +72,7 @@ ./hardware/i2c.nix ./hardware/infiniband.nix ./hardware/inputmodule.nix + ./hardware/iosched.nix ./hardware/keyboard/qmk.nix ./hardware/keyboard/teck.nix ./hardware/keyboard/uhk.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b1ca7bbcbcdf..1540ca8f10b0 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -488,6 +488,7 @@ in { honk = runTest ./honk.nix; installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {}); invidious = handleTest ./invidious.nix {}; + iosched = handleTest ./iosched.nix {}; isolate = handleTest ./isolate.nix {}; livebook-service = handleTest ./livebook-service.nix {}; pyload = handleTest ./pyload.nix {}; diff --git a/nixos/tests/iosched.nix b/nixos/tests/iosched.nix new file mode 100644 index 000000000000..5b9e8049ce1d --- /dev/null +++ b/nixos/tests/iosched.nix @@ -0,0 +1,68 @@ +import ./make-test-python.nix ( + { + pkgs, + ... + }: + let + qemu-img = pkgs.lib.getExe' pkgs.vmTools.qemu "qemu-img"; + empty = pkgs.runCommand "empty.qcow2" { } '' + ${qemu-img} create -f qcow2 "$out" 32M + ''; + in + { + name = "iosched"; + meta.maintainers = with pkgs.lib.maintainers; [ mvs ]; + + nodes.machine = { + virtualisation.qemu.options = [ + "-drive" + "id=sda,if=none,format=qcow2,readonly=on,file=${empty}" + "-drive" + "id=sdb,if=none,format=qcow2,readonly=on,file=${empty}" + "-drive" + "id=nvme0n1,if=none,format=qcow2,readonly=on,file=${empty}" + "-drive" + "id=mmcblk0,if=none,format=qcow2,file=./mmcblk0.qcow2" + "-device" + "virtio-scsi-pci,id=scsi0" + "-device" + "sdhci-pci" + "-device" + "scsi-hd,rotation_rate=1,bus=scsi0.0,drive=sda" + "-device" + "scsi-hd,rotation_rate=7200,bus=scsi0.0,drive=sdb" + "-device" + "sd-card,drive=mmcblk0" + "-device" + "nvme,serial=deadbeef,drive=nvme0n1" + ]; + + hardware.block = { + defaultScheduler = "none"; + defaultSchedulerRotational = "mq-deadline"; + scheduler = { + "nvme[0-9]*" = "kyber"; + "mmcblk[0-9]*" = "bfq"; + }; + }; + }; + + testScript = '' + import subprocess + + def check_scheduler(dev, scheduler): + machine.succeed("grep -F -q '[{}]' /sys/block/{}/queue/scheduler".format(scheduler, dev)) + + subprocess.check_call([ + "${qemu-img}", "create", "-f", "qcow2", "vm-state-machine/mmcblk0.qcow2", "32M" + ]) + + machine.start() + machine.succeed("udevadm verify --no-style") + check_scheduler("sda", "none") + check_scheduler("sdb", "mq-deadline") + check_scheduler("nvme0n1", "kyber") + check_scheduler("mmcblk0", "bfq") + ''; + } +)