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
apt-get install perl perl-json-xs
dnf install perl perl-App-cpanminus
```
Для наследников redhat-а
Для rhel-образых - командами:
```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 или как обычный
процесс. Второй вариант нужен для запуска сервиса через 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
@@ -50,7 +76,8 @@ dnf install perl perl-json-xs
sudo reniced
```
в каталоге с reniced. Кроме того, под MacOS конфиг всегда ищется в ~/.reniced.json
в каталоге с reniced. Кроме того, под MacOS конфиг всегда ищется в ~/.reniced.json, а при таком запуске нужно, чтобы
была выставлена переменная окружения PERL5LIB, например в ~/.zprofile.
В линуксах такой проблемы пока нет (но возможно будет, когда придут "атомарные" обДНОвления).
@@ -68,5 +95,5 @@ sudo reniced
При запуске reniced от обычного пользователя, niceness процесса может идти только в плюс. То есть, процесс с nice=11 не
получится сделать с nice=9, например (без перезапуска процесса, конечно). Зато от суперпользователя такое работает.
## Special thans
## Special thanks
Авторам 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": {
# 0 - запустить как обычный прооцесс (для systemd, например)
# > 0 - запустить как unix daemon
"foreground" : 0
"foreground" : 1,
# куда складываем process id нашего демоночка, пустая строка - не складываем никуда (pid-файл не нужен)
"pidfile": "/run/reniced.pid",
# Задержка между итерациями скрипта. По-умолчанию, 2 секунды. Измеряется в секундах.
# Не может быть менее 2-х секунд.
"loopdelay": "10",
# задержка между применением команд по renice-у. Измеряется в миллисекундах, по-умолчанию 0
"cmddelay": "1000"
},
"kill" : {
@@ -98,7 +108,7 @@
"com.apple.siri-distributed\\-evaluation",
"com.apple.CloudPhotosConfiguration"
],
"12": ["bash"],
# IO hogs and other "bad" services
"19":[
"coreduetd",
@@ -107,7 +117,7 @@
"backupd",
"com.apple.CodeSigningHelper",
"distnoted",
"backupd\\-helper",
"backupd\\-helper",
"JamfDaemon",
"osqueryd",
"commvault",

216
reniced
View File

@@ -1,38 +1,57 @@
#!/usr/bin/env perl
# Renice some processes, to make system more respositive
# TODO: Add -c parameter to pointout where config actually is
# TODO: Add perlcritic rules :)
# TODO: Запользовать модуль Proc::Find для получения списка pid-ов искомых процессов и их текущих парметров.
use strict;
use warnings;
use utf8;
use open qw (:std :utf8);
use Encode qw (encode);
use English qw (-no_match_vars);
use JSON::XS ();
use POSIX qw (setsid);
use open qw (:std :utf8);
use Encode qw (encode);
use English qw (-no_match_vars);
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{TERM} = \&__signal_handler;
my $list;
my $F;
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 $pidfile = '/run/reniced.pid'; # Пусть будет глобальной переменной, чтобы обратиться к ней в хэндлерах сигналов
if ($uid > 0) {
$pidfile = sprintf '/tmp/reniced-%s.pid', $uid;
unless (defined $pidfile) {
$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 '')) {
$pidfile = $config->{patameters}->{pidfile};
}
unless ($config->{foreground}) {
if (! $config->{parameters}->{foreground}) {
daemonize ();
my $pidfilehandler;
@@ -44,36 +63,20 @@ unless ($config->{foreground}) {
}
}
while (sleep 2) {
open $F, qq{ps xau |} or do { warn "Can't 'run ps xau': $!" ; next; };
binmode $F, ':utf8';
if ((defined $config->{parameters}->{loopdelay}) && ($config->{parameters}->{loopdelay} > 2)) {
$loopdelay = $config->{parameters}->{loopdelay};
} else {
$loopdelay = 2;
}
my $cmd;
while (sleep $loopdelay) {
# Конечно, хотелось бы не искать по регулярке, ибо это относительно затратно, а просто собрать список... но что поделать
my $cmd = find_proc (
name => qr /./,
detail => 1,
);
while (my $str = readline $F) {
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) {
if ((defined $cmd) && ($#{$cmd} > 0)) {
# bulk chanage niceness :)
my $prio = $config->{prio};
my $kill = $config->{kill};
@@ -81,49 +84,93 @@ while (sleep 2) {
foreach my $kill_signal (keys %{$kill}) {
my @pids;
foreach my $command (keys %{$cmd}) {
foreach my $command (@{$cmd}) {
foreach my $command_pattern (@{$kill->{$kill_signal}}) {
if ($command =~ /$command_pattern/) {
push @pids, @{$cmd->{$command}};
if ($command->{cmdline}->[0] =~ /$command_pattern/) {
push @pids, $command->{pid};
}
}
}
if ($#pids >= 0) {
my $killcmd = sprintf 'kill -%s %s', $kill_signal, join (' ', @pids);
`$killcmd`;
# This op is a bit racy, so eval() it
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}) {
my @pids;
foreach my $command (keys %{$cmd}) {
foreach my $command (@{$cmd}) {
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) {
my $renice;
# If we're not superuser we cannot set prio to negative value, for sure, so skip it
if (($uid > 0) && ($priority < 0)) {
$renice = sprintf "renice 0 -p %s", join (' ', @pids);
# skip it
} 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) {
if ($OSNAME eq 'darwin') {
my $taskpol = sprintf 'taskpolicy -b -p %s', join (' '. @pids);
`$taskpol`;
} elsif ($OSNAME eq 'linux') {
foreach my $pid (@pids) {
`ionice -c 3 -p $pid`;
foreach my $pid (@pids) {
if (defined ($ionice_pids{$pid})) {
$ionice_pids_new{$pid} = 1;
} else {
# set ionice for pid
$setflag = 1;
$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;
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 {
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
# 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});
if ((defined $configfile) && ($configfile ne '')) {
$file = $configfile;
} else {
my $uid = $<;
$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()
binmode $FILEHANDLE;