diff --git a/README.md b/README.md index 30ad742..cdf7cce 100644 --- a/README.md +++ b/README.md @@ -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. Без их мессенджера, с его "недостатками", не было бы этого костыля. diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..538a638 --- /dev/null +++ b/bootstrap.sh @@ -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: \ No newline at end of file diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..7bd2d8a --- /dev/null +++ b/cpanfile @@ -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'; diff --git a/example_config.json b/example_config.json index 8998d38..271ccee 100644 --- a/example_config.json +++ b/example_config.json @@ -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", diff --git a/reniced b/reniced index 60cfc27..3f9504e 100644 --- a/reniced +++ b/reniced @@ -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;