From 863ca343830f5e0d0cf2976bddd0a328e3cd6822 Mon Sep 17 00:00:00 2001 From: eleksir Date: Mon, 29 Apr 2024 20:42:23 +0300 Subject: [PATCH] Implemented linux ionice funtionality --- .reniced.yaml.example | 8 +++++ Makefile | 4 +-- README.md | 21 ++++++++++++ globals.go | 54 +++++++++++++++++++++++++++++ ionice.go | 17 +++++++++ ionice_linux.go | 80 +++++++++++++++++++++++++++++++++++++++++++ ionice_other.go | 16 +++++++++ main.go | 25 +++++++------- types.go | 8 +++++ util.go | 27 +++++++++++++++ 10 files changed, 244 insertions(+), 16 deletions(-) create mode 100644 globals.go create mode 100644 ionice.go create mode 100644 ionice_linux.go create mode 100644 ionice_other.go diff --git a/.reniced.yaml.example b/.reniced.yaml.example index 378275e..906a03d 100644 --- a/.reniced.yaml.example +++ b/.reniced.yaml.example @@ -161,4 +161,12 @@ prio: - siriknowledged - triald +ioprio: + - name: + - htop + - top + - atop + class: 2 + prio: 1 + # vim: set ft=yaml et ai ts=2 sw=2 sts=2: diff --git a/Makefile b/Makefile index e34bd27..bb386a9 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,13 @@ #!/usr/bin/env gmake -f BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -FILELIST=main.go types.go util.go BINARY=reniced all: clean build build: - go build ${BUILDOPTS} -o ${BINARY} ${FILELIST} - + go build ${BUILDOPTS} -o ${BINARY} clean: go clean diff --git a/README.md b/README.md index 33eb51b..8dac00c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,27 @@ Unlike supplpied systemd unit, supplied System V Init script assumes reniced run setting must be set to true). Script also expects to find pid-file at **/var/run/reniced.pid** and executable itself at **/usr/sbin/reniced**. +## Note on ionice operation + +Currently implemented only linux io prio. On all other reniced-supported systems it is noop. Please note that io +priority can be set only by digits. Refer to *man ionice* for specific io classes and priorities inplemented there. + +Here is table with numbers for defined class and priorities. + +| class name | class number | list of possible priorities | +|:-----------|-------------:|----------------------------:| +| NONE |0 |0 | +| RealTime |1 |0, 1, 2, 3, 4, 5, 6, 7 | +| BestEffort |2 |0, 1, 2, 3, 4, 5, 6, 7 | +| Idle |3 |0 | + +None is default io prio if process is not ioreniced. And it is the same value and behaviour as BestEffort with priority +of 4. + +The lower priority value means higher priority and process will eat more io. + +RealTime prio is not available for regular users but for root only. + ## Special "thanks" To security assholes that disrupt workflows and make this world worse. diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..9a85ae1 --- /dev/null +++ b/globals.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + "syscall" + + "github.com/alitto/pond" +) + +// Пул воркеров. +var pool *pond.WorkerPool + +// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. +var sigChan = make(chan os.Signal, 1) + +var kill = make(map[string]syscall.Signal) +var renice = make(map[string]int) +var ioreniceClass = make(map[string]uint32) +var ioreniceClassdata = make(map[string]uint32) + +var NotImplementedError = fmt.Errorf("SYSCALL Not Implemented on current platform") //nolint:revive,stylecheck + +const IOPRIO_CLASS_NONE = uint32(0) //nolint:revive,stylecheck +const IOPRIO_CLASS_RT = uint32(1) //nolint:revive,stylecheck +const IOPRIO_CLASS_BE = uint32(2) //nolint:revive,stylecheck +const IOPRIO_CLASS_IDLE = uint32(3) //nolint:revive,stylecheck + +const IOPRIO_WHO_PROCESS = 1 //nolint:revive,stylecheck +const IOPRIO_WHO_PGRP = 2 //nolint:revive,stylecheck +const IOPRIO_WHO_USER = 3 //nolint:revive,stylecheck + +const IOPRIO_CLASS_SHIFT = uint32(13) //nolint:revive,stylecheck +const IOPRIO_PRIO_MASK = ((uint32(1) << IOPRIO_CLASS_SHIFT) - 1) //nolint:revive,stylecheck + +var ClassToString = map[uint32]string{ + IOPRIO_CLASS_NONE: "none", + IOPRIO_CLASS_RT: "realtime", + IOPRIO_CLASS_BE: "best-effort", + IOPRIO_CLASS_IDLE: "idle", +} + +var StringToClass = map[string]uint32{ + "none": IOPRIO_CLASS_NONE, + "0": IOPRIO_CLASS_NONE, + "realtime": IOPRIO_CLASS_RT, + "1": IOPRIO_CLASS_RT, + "best-effort": IOPRIO_CLASS_BE, + "2": IOPRIO_CLASS_BE, + "idle": IOPRIO_CLASS_IDLE, + "3": IOPRIO_CLASS_IDLE, +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/ionice.go b/ionice.go new file mode 100644 index 0000000..e3c071b --- /dev/null +++ b/ionice.go @@ -0,0 +1,17 @@ +package main + +func PrioToClass(prio uint32) uint32 { + return prio >> IOPRIO_CLASS_SHIFT +} + +func PrioToClassdata(prio uint32) uint32 { + return prio & IOPRIO_PRIO_MASK +} + +func ClassAndClassdataToPrio(class uint32, classdata uint32) uint32 { + return ((class) << IOPRIO_CLASS_SHIFT) | classdata +} + +func PrioToClassAndClassdata(prio uint32) (uint32, uint32) { + return PrioToClass(prio), PrioToClassdata(prio) +} diff --git a/ionice_linux.go b/ionice_linux.go new file mode 100644 index 0000000..f8d76d7 --- /dev/null +++ b/ionice_linux.go @@ -0,0 +1,80 @@ +//go:build linux +// +build linux + +package main + +import ( + "fmt" + "log" + "syscall" + + proc "github.com/shirou/gopsutil/v3/process" +) + +// GetIOPriority достаёт приоритет вводв-вывода для укаанного процесса. +func GetIOPriority(which int, who int) (uint32, error) { + var ( + err error + r0, _, e1 = syscall.Syscall(syscall.SYS_IOPRIO_GET, uintptr(which), uintptr(who), 0) + prio = uint32(r0) + ) + + if e1 != 0 { + err = fmt.Errorf(fmt.Sprintf("Received error number %v", e1)) + } + + return prio, err +} + +// GetIOPriority ставит приоритет вводв-вывода для укаанного процесса. +func SetIOPriority(which int, who int, prio uint32) error { + var ( + err error + _, _, e1 = syscall.Syscall(syscall.SYS_IOPRIO_SET, uintptr(which), uintptr(who), uintptr(prio)) + ) + + if e1 != 0 { + err = fmt.Errorf(fmt.Sprintf("Received error number %v", e1)) + } + + return err +} + +// IORenice выставляет процессу приоритет для операций ввода-вывода. +func IORenice(cnf Config, p *proc.Process, processName string) { + if currentIOPrioLevel, err := GetIOPriority(IOPRIO_WHO_PROCESS, int(p.Pid)); err == nil { + currentClass, currentClassdata := PrioToClassAndClassdata(currentIOPrioLevel) + class := ioreniceClass[processName] + classdata := ioreniceClassdata[processName] + + if class == currentClass && classdata == currentClassdata { + if cnf.Debug && currentIOPrioLevel != 0 { // 0 - это дефолтный уровень io niceness. + log.Printf( + "IONiceness for %s(%d) already set to %d %d", + processName, + p.Pid, + currentClass, + currentClassdata, + ) + } + } else { + iopriority := ClassAndClassdataToPrio(class, classdata) + + _ = SetIOPriority(IOPRIO_WHO_PROCESS, int(p.Pid), iopriority) + + if cnf.Debug { + log.Printf( + "Set ioniceness for %s(%d) to %d %d (was %d %d)", + processName, + p.Pid, + class, + classdata, + currentClass, + currentClassdata, + ) + } + } + } else { + log.Printf("Unable to get ioprio for %s(%d)", processName, p.Pid) + } +} diff --git a/ionice_other.go b/ionice_other.go new file mode 100644 index 0000000..e932cf2 --- /dev/null +++ b/ionice_other.go @@ -0,0 +1,16 @@ +//go:build !linux +// +build !linux + +package main + +import proc "github.com/shirou/gopsutil/v3/process" + +func GetIOPriority(which int, who int) (prio uint32, err error) { + return 0, NotImplementedError +} + +func SetIOPriority(which int, who int, prio uint32) (err error) { + return NotImplementedError +} + +func IORenice(cnf Config, p *proc.Process, processName string) {} diff --git a/main.go b/main.go index dc13bf1..8c93a94 100644 --- a/main.go +++ b/main.go @@ -15,15 +15,6 @@ import ( proc "github.com/shirou/gopsutil/v3/process" ) -// Пул воркеров. -var pool *pond.WorkerPool - -// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. -var sigChan = make(chan os.Signal, 1) - -var kill = make(map[string]syscall.Signal) -var renice = make(map[string]int) - // main основная функция программы. func main() { // Самое время поставить траппер сигналов. @@ -86,7 +77,6 @@ func reniced(cnf Config) { for _, p := range processList { ok := pool.TrySubmit(func() { if processName, err := p.Name(); err == nil { - // Для каждого процесса извлекаем его текущий priority. if currentPrioLevel, err := syscall.Getpriority(syscall.PRIO_PROCESS, int(p.Pid)); err == nil { if niceLevel := renice[processName]; niceLevel != 0 { @@ -121,11 +111,18 @@ func reniced(cnf Config) { ) } } else if cnf.Debug { - log.Printf("Niceness for %d already set to %d", p.Pid, niceLevel) + log.Printf( + "Niceness for %s(%d) already set to %d", + processName, + p.Pid, + niceLevel, + ) } } } + IORenice(cnf, p, processName) + // Посылаем процессу сигналы, если таковые есть в конфиге. if killSignal := kill[processName]; killSignal != 0 { switch killSignal { //nolint:exhaustive @@ -134,8 +131,9 @@ func reniced(cnf Config) { if cnf.Debug { log.Printf( - "Matching processName with killProcessname (%s), SIG%s sent", + "Matching processName with killProcessname %s(%d), SIG%s sent", processName, + p.Pid, strings.ToUpper(killSignal.String()), ) } @@ -145,8 +143,9 @@ func reniced(cnf Config) { if cnf.Debug { log.Printf( - "Matching processName with killProcessname (%s), SIG%s sent", + "Matching processName with killProcessname %s(%d), SIG%s sent", processName, + p.Pid, strings.ToUpper(killSignal.String()), ) } diff --git a/types.go b/types.go index cfd8d80..50ebd95 100644 --- a/types.go +++ b/types.go @@ -29,6 +29,8 @@ type Config struct { Kill Kill `yaml:"kill,omitempty"` Prio Prio `yaml:"prio,omitempty"` + + IOPrio IOPrio `yaml:"ioprio,omitempty"` } type Prio []struct { @@ -41,4 +43,10 @@ type Kill []struct { Name []string `yaml:"name,omitempty"` } +type IOPrio []struct { + Class uint32 `yaml:"class,omitempty"` + Prio uint32 `yaml:"prio,omitempty"` + Name []string `yaml:"name,omitempty"` +} + /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/util.go b/util.go index a6b719d..9a63e2e 100644 --- a/util.go +++ b/util.go @@ -122,6 +122,33 @@ func readConfFile(path string) (Config, error) { } } + for _, v := range cfg.IOPrio { + switch { + case len(v.Name) == 0: + log.Println("Skipping empty name entry in ioprio config block") + case v.Class > 3: + log.Printf("Skipping entry %s with class > 3 in ioprio config block", v.Name) + case v.Class == 0 && v.Prio != 0: + log.Printf("Skipping entry %s with class 0 and prio not 0 in ioprio config block", v.Name) + case v.Class == 3 && v.Prio != 0: + log.Printf("Skipping entry %s with class 3 and prio not 0 in ioprio config block", v.Name) + case v.Class == 1 && v.Prio > 7: + log.Printf("Skipping entry %s with class 1 and prio > 7 in ioprio config block", v.Name) + case v.Class == 2 && v.Prio > 7: + log.Printf("Skipping entry %s with class 2 and prio > 7 in ioprio config block", v.Name) + default: + for _, processName := range v.Name { + ioreniceClass[processName] = v.Class + ioreniceClassdata[processName] = v.Prio + + if cfg.Debug { + log.Printf("Add %s to list of ionice class %d processes.", processName, v.Class) + log.Printf("Add %s to list of ionice prio %d processes.", processName, v.Prio) + } + } + } + } + for _, v := range cfg.Kill { if len(v.Name) != 0 { switch v.Sig {