Implemented commandline params, updated README.md, added cpanfile, fixed couple of bugs

- Use Proc::Find instead of ps utility
- Use builtin kill instead of commandline utility
- Use setpriority instead of renice utility
- Implemented config parameter loopdelay
- Implemented config parameter cmddelay
- Implemented commandline parameter --config
- Added --help parameter
- Implemented commandline parameter --pidfile
- Added cpanfile and bootstrap.sh to bootstrap this daemon
- Optimized algo - do not send signals to already "signaled" processes, do not ionice already ioniced pids
This commit is contained in:
eleksir
2023-05-01 16:33:27 +03:00
parent 4229e2f914
commit 3082dd2add
5 changed files with 217 additions and 91 deletions

View File

@@ -10,33 +10,59 @@
## Как это установить и заставить работать? ## Как это установить и заставить работать?
Для установки нам понадобятся пакеты с перлом и перловым модулем JSON::XS из текущего дистрибутива линукса, например, их ### Установка reniced
можно уставить командами:
Для вуишфт-образных дистрибутивов Для установки и работы нам понадобятся модули, которые скорее всего с дистрибутивом не идут. Чтобы их установить нам
понадобятся собственно perl и его пакетный менеджер cpanm.
Для debian-образных дистрибутивов эти пакеты можно установить командами:
```bash ```bash
apt-get install perl perl-json-xs dnf install perl perl-App-cpanminus
``` ```
Для наследников redhat-а Для rhel-образых - командами:
```bash ```bash
dnf install perl perl-json-xs apt install perl cpanminus
``` ```
Для всех остальных нужен будет perl, cpanm и переменная окружения PERL5LIB. За подробностями - добро пожаловать в гугл. И далее приложение надо будет "забутстрапить". С этим есть небольшая тонкость - если у вас определена переменная
PERL5LIBS, то при запуске модули будут браться из системы и каталогов, обозначенных в ней, если не определена, то только
из системы. Соответственно, предполагается, что у нас запуск команды reniced будет в окружении, в котором есть всё
необходимое (в cpanfile есть список всех нужных для работы библиотек), либо в системе, где все нужные либы уже
установлены штатным пакетным менеджером.
Как заставить это работать? Бутстрапится приложение командой
Вначале надо скопировать конфиг из **config_example.json** в **~/.reniced.json** и далее подправить все необходимые ```
значения в нём. bash bootstrap.sh
```
При этом все либы складываются в ~/perl5. У рута это будет /root/perl5, у обычного пользователя - ${HOME}/perl5. (при
этом $HOME обычно это /home/имя_пользователя). В случае MacOS пути будут иные: /var/root/perl5 для рута и ${HOME}/perl5
(при этом $HOME обычно это /Users/имя_пользователя). Собственно, тот каталог с либами далее можно переместить куда надо.
### Работа с reniced
Вначале надо скопировать конфиг из **example_config.json** в **~/.reniced.json**, если запуск планируется от
пользователя (что не рекомендуется см. Caveats ниже), либо скопировать конфиг из **example_config.json** в
**/etc/reniced.json**, если запуск будет от суперпользователя (рута, uid=0) и далее подправить все необходимые значения
в нём.
После настройки списка процессов и приоритетов их работы, можно запускать сервис как unix daemon или как обычный После настройки списка процессов и приоритетов их работы, можно запускать сервис как unix daemon или как обычный
процесс. Второй вариант нужен для запуска сервиса через systemd. процесс. Второй вариант нужен для запуска сервиса через systemd.
Если reniced планируется запускать от рута (uid=0), то конфиг будет находиться не в **~/.reniced.json**, а в #### Аргументы командной строки
**/etc/reniced.json**.
**--help** - краткая справка по запуску команды
**--pidfile** - указать положение pidfile, в который будет записываться process id (для рута по-умолчанию
/run/reniced.pid, для обычного пользователя по-умолчанию /tmp/reniced-UID.pid)
**--config** - указать положение файла с конфигом (для рута - /etc/reniced.json, для обычного
пользователя - ~/.reniced.json)
## Caveats ## Caveats
@@ -50,7 +76,8 @@ dnf install perl perl-json-xs
sudo reniced sudo reniced
``` ```
в каталоге с reniced. Кроме того, под MacOS конфиг всегда ищется в ~/.reniced.json в каталоге с reniced. Кроме того, под MacOS конфиг всегда ищется в ~/.reniced.json, а при таком запуске нужно, чтобы
была выставлена переменная окружения PERL5LIB, например в ~/.zprofile.
В линуксах такой проблемы пока нет (но возможно будет, когда придут "атомарные" обДНОвления). В линуксах такой проблемы пока нет (но возможно будет, когда придут "атомарные" обДНОвления).
@@ -68,5 +95,5 @@ sudo reniced
При запуске reniced от обычного пользователя, niceness процесса может идти только в плюс. То есть, процесс с nice=11 не При запуске reniced от обычного пользователя, niceness процесса может идти только в плюс. То есть, процесс с nice=11 не
получится сделать с nice=9, например (без перезапуска процесса, конечно). Зато от суперпользователя такое работает. получится сделать с nice=9, например (без перезапуска процесса, конечно). Зато от суперпользователя такое работает.
## Special thans ## Special thanks
Авторам VK Teams. Без их мессенджера, с его "недостатками", не было бы этого костыля. Авторам VK Teams. Без их мессенджера, с его "недостатками", не было бы этого костыля.

11
bootstrap.sh Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Защита от дурака
if [[ -z $HOME ]]; then
echo "HOME variable is not defined, please set it up and re-run bootstrap script"
exit 1
fi
cpanm --installdeps --local-lib "$HOME/perl5"
# vim: set ft=sh noet ai ts=4 sw=4 sts=4:

10
cpanfile Normal file
View File

@@ -0,0 +1,10 @@
## no critic
# Почему-то perlcritic пытается критиковать и cpanfile тоже, хотя он к перловым программам и либам не относится.
# Проставим версии зависимостей в то, что по идее должно работать с текущим перлом. Типа "как у взрослых".
requires Encode, '>=3.19';
requires Getopt::Long, '>=2.54';
requires JSON::XS, '>=4.03';
# Built-in perl distribution
requires POSIX, '0';
requires Proc::Find, '>=0.051';
requires Time::HiRes, '>=1.9764';

View File

@@ -2,7 +2,17 @@
"parameters": { "parameters": {
# 0 - запустить как обычный прооцесс (для systemd, например) # 0 - запустить как обычный прооцесс (для systemd, например)
# > 0 - запустить как unix daemon # > 0 - запустить как unix daemon
"foreground" : 0 "foreground" : 1,
# куда складываем process id нашего демоночка, пустая строка - не складываем никуда (pid-файл не нужен)
"pidfile": "/run/reniced.pid",
# Задержка между итерациями скрипта. По-умолчанию, 2 секунды. Измеряется в секундах.
# Не может быть менее 2-х секунд.
"loopdelay": "10",
# задержка между применением команд по renice-у. Измеряется в миллисекундах, по-умолчанию 0
"cmddelay": "1000"
}, },
"kill" : { "kill" : {
@@ -98,7 +108,7 @@
"com.apple.siri-distributed\\-evaluation", "com.apple.siri-distributed\\-evaluation",
"com.apple.CloudPhotosConfiguration" "com.apple.CloudPhotosConfiguration"
], ],
"12": ["bash"],
# IO hogs and other "bad" services # IO hogs and other "bad" services
"19":[ "19":[
"coreduetd", "coreduetd",

216
reniced
View File

@@ -1,38 +1,57 @@
#!/usr/bin/env perl #!/usr/bin/env perl
# Renice some processes, to make system more respositive # Renice some processes, to make system more respositive
# TODO: Add -c parameter to pointout where config actually is
# TODO: Add perlcritic rules :) # TODO: Add perlcritic rules :)
# TODO: Запользовать модуль Proc::Find для получения списка pid-ов искомых процессов и их текущих парметров.
use strict; use strict;
use warnings; use warnings;
use utf8; use utf8;
use open qw (:std :utf8); use open qw (:std :utf8);
use Encode qw (encode); use Encode qw (encode);
use English qw (-no_match_vars); use English qw (-no_match_vars);
use JSON::XS ();
use POSIX qw (setsid); use Getopt::Long qw (GetOptions);
use JSON::XS ();
use POSIX qw (setsid :sys_resource_h);
use Proc::Find qw (find_proc);
use Time::HiRes qw (usleep);
local $SIG{INT} = \&__signal_handler; local $SIG{INT} = \&__signal_handler;
local $SIG{TERM} = \&__signal_handler; local $SIG{TERM} = \&__signal_handler;
my $list;
my $F;
my $uid = $<; my $uid = $<;
# We need it empty on first iteration
my %ionice_pids = ();
my %ionice_pids_new = ();
my ($pidfile, $configfile, $help, $loopdelay);
GetOptions (
'pidfile=s' => \$pidfile, # Пусть будет глобальной переменной, чтобы обратиться к ней в хэндлерах сигналов
'config=s' => \$configfile,
'help' => \$help,
);
if (defined $help) {
help ();
exit 0;
}
my $config = LoadConf(); my $config = LoadConf();
my $pidfile = '/run/reniced.pid'; # Пусть будет глобальной переменной, чтобы обратиться к ней в хэндлерах сигналов
if ($uid > 0) { unless (defined $pidfile) {
$pidfile = sprintf '/tmp/reniced-%s.pid', $uid; $pidfile = '/run/reniced.pid';
if ($uid > 0) {
$pidfile = sprintf '/tmp/reniced-%s.pid', $uid;
}
if ((defined $config->{pidfile})) {
$pidfile = $config->{parameters}->{pidfile};
}
} }
if ((defined $config->{patameters}->{pidfile}) && ($config->{patameters}->{pidfile} ne '')) { if (! $config->{parameters}->{foreground}) {
$pidfile = $config->{patameters}->{pidfile};
}
unless ($config->{foreground}) {
daemonize (); daemonize ();
my $pidfilehandler; my $pidfilehandler;
@@ -44,36 +63,20 @@ unless ($config->{foreground}) {
} }
} }
while (sleep 2) { if ((defined $config->{parameters}->{loopdelay}) && ($config->{parameters}->{loopdelay} > 2)) {
open $F, qq{ps xau |} or do { warn "Can't 'run ps xau': $!" ; next; }; $loopdelay = $config->{parameters}->{loopdelay};
binmode $F, ':utf8'; } else {
$loopdelay = 2;
}
my $cmd; while (sleep $loopdelay) {
# Конечно, хотелось бы не искать по регулярке, ибо это относительно затратно, а просто собрать список... но что поделать
my $cmd = find_proc (
name => qr /./,
detail => 1,
);
while (my $str = readline $F) { if ((defined $cmd) && ($#{$cmd} > 0)) {
chomp $str;
my ($user, $pid, $cpu, $mem, $vsz, $rss, $tt, $stat, $started, $time, $command) = split /\s+/, $str, 11;
if ($uid > 0) { # i'm not root!
my $u = getpwuid ($<);
if (defined $pid && defined $command && defined $user) {
if (($u eq $user) && ($pid =~ /^\d+$/)) {
push @{$cmd->{$command}}, $pid;
}
}
} else { # fuck, yeah! i'm root!
# since we interested in changing niceness of processes, we need to make map command->[pids]
if (defined $pid && defined $command) {
if ($pid =~ /^\d+$/) {
push @{$cmd->{$command}}, $pid;
}
}
}
}
close $F;
if (defined $cmd) {
# bulk chanage niceness :) # bulk chanage niceness :)
my $prio = $config->{prio}; my $prio = $config->{prio};
my $kill = $config->{kill}; my $kill = $config->{kill};
@@ -81,49 +84,93 @@ while (sleep 2) {
foreach my $kill_signal (keys %{$kill}) { foreach my $kill_signal (keys %{$kill}) {
my @pids; my @pids;
foreach my $command (keys %{$cmd}) { foreach my $command (@{$cmd}) {
foreach my $command_pattern (@{$kill->{$kill_signal}}) { foreach my $command_pattern (@{$kill->{$kill_signal}}) {
if ($command =~ /$command_pattern/) { if ($command->{cmdline}->[0] =~ /$command_pattern/) {
push @pids, @{$cmd->{$command}}; push @pids, $command->{pid};
} }
} }
} }
if ($#pids >= 0) { if ($#pids >= 0) {
my $killcmd = sprintf 'kill -%s %s', $kill_signal, join (' ', @pids); # This op is a bit racy, so eval() it
`$killcmd`; eval { kill $kill_signal, join (' ', @pids) };
# Use our cmd_delay here, but convert it from milliseconds to microseconds: 1k ms = 1 sec, 1M us = 1 sec
# Not sure if we can handle properly delays < 10 ms, but noone really cares about such little delays.
if (defined $config->{parameters}->{cmddelay}) {
my $sleeptime = 1000 * $config->{parameters}->{cmddelay};
usleep ($sleeptime);
}
} }
} }
foreach my $priority (keys %{$prio}) { foreach my $priority (keys %{$prio}) {
my @pids; my @pids;
foreach my $command (keys %{$cmd}) { foreach my $command (@{$cmd}) {
foreach my $command_pattern (@{$prio->{$priority}}) { foreach my $command_pattern (@{$prio->{$priority}}) {
if ($command =~ /$command_pattern/u) {
push @pids, @{$cmd->{$command}}; if ($command->{cmdline}->[0] =~ /$command_pattern/u) {
push @pids, $command->{pid};
} }
} }
} }
if ($#pids >= 0) { if ($#pids >= 0) {
my $renice; # If we're not superuser we cannot set prio to negative value, for sure, so skip it
if (($uid > 0) && ($priority < 0)) { if (($uid > 0) && ($priority < 0)) {
$renice = sprintf "renice 0 -p %s", join (' ', @pids); # skip it
} else { } else {
$renice = sprintf "renice %s -p %s", $priority, join (' ', @pids); foreach (@pids) {
eval { setpriority PRIO_PROCESS, $_, $priority; };
if (defined $config->{parameters}->{cmddelay}) {
my $sleeptime = 1000 * $config->{parameters}->{cmddelay};
usleep ($sleeptime);
}
}
} }
`$renice`; # Менять политику io процессам можно только руту :)
if ($uid == 0) {
if ($priority > 2) {
if ($OSNAME eq 'darwin') {
my $setflag = 0;
my $taskpol = 'taskpolicy -b -p';
if ($priority > 2) { foreach my $pid (@pids) {
if ($OSNAME eq 'darwin') { if (defined ($ionice_pids{$pid})) {
my $taskpol = sprintf 'taskpolicy -b -p %s', join (' '. @pids); $ionice_pids_new{$pid} = 1;
`$taskpol`; } else {
} elsif ($OSNAME eq 'linux') { # set ionice for pid
foreach my $pid (@pids) { $setflag = 1;
`ionice -c 3 -p $pid`; $ionice_pids_new{$pid} = 1;
$taskpol .= " $pid";
}
}
if ($setflag) {
`$taskpol`;
}
} elsif ($OSNAME eq 'linux') {
foreach my $pid (@pids) {
if (defined ($ionice_pids{$pid})) {
# do nothing
$ionice_pids_new{$pid} = 1;
} else {
# set ionice for pid
`ionice -c 3 -p $pid`;
$ionice_pids_new{$pid} = 1;
}
%ionice_pids = %ionice_pids_new;
}
}
if (defined $config->{parameters}->{cmddelay}) {
my $sleeptime = 1000 * $config->{parameters}->{cmddelay};
usleep ($sleeptime);
} }
} }
} }
@@ -145,20 +192,41 @@ if (-f $pidfile) {
exit 0; exit 0;
sub help {
my $msg = "Usage: reniced [--config /etc/reniced.json] [--pidfile /run/reniced.pid] [--help]\n";
$msg .= "\n";
$msg .= " --config overrides default value for config file, which is /etc/reniced.json for root and ~/.reniced.json for user\n";
$msg .= " --pidfile overrides default value for pid-file file, which is /run/reniced.pid for root and /tmp/reniced-uid.pid for user\n";
$msg .= " --help displays this help\n\n";
if (defined $msg) {
print $msg;
exit 0;
} else {
warn $msg;
exit 1;
}
}
sub LoadConf { sub LoadConf {
my $file = '/etc/reniced.json'; my $file = '/etc/reniced.json';
my $uid = $<;
# On recent MacOS we have no direct ability to run reniced as system service, so we have to run it as user, probably if ((defined $configfile) && ($configfile ne '')) {
# via sudo, so we have to check OS here and use $HOME/.reniced.json for MacOS. $file = $configfile;
if (($OSNAME eq 'darwin') || ($uid != 0)) { } else {
# Assume that script is not running under completely sanitized environment my $uid = $<;
die "No HOME variable found.\n" unless (defined $ENV{HOME});
$file = sprintf '%s/.reniced.json', $ENV{HOME}; # On recent MacOS we have no direct ability to run reniced as system service, so we have to run it as user, probably
# via sudo, so we have to check OS here and use $HOME/.reniced.json for MacOS.
if (($OSNAME eq 'darwin') || ($uid != 0)) {
# Assume that script is not running under completely sanitized environment
die "No HOME variable found.\n" unless (defined $ENV{HOME});
$file = sprintf '%s/.reniced.json', $ENV{HOME};
}
} }
open my $FILEHANDLE, '<', $file || die "[FATA] No conf at $file: $OS_ERROR\n"; open my $FILEHANDLE, '<', $file || die "No config file at $file: $OS_ERROR\n";
# Use binmode in order to get amount of read bytes (not chars!) from read() # Use binmode in order to get amount of read bytes (not chars!) from read()
binmode $FILEHANDLE; binmode $FILEHANDLE;