diff --git a/.gitignore b/.gitignore index 91b4ee2..d1f3790 100644 --- a/.gitignore +++ b/.gitignore @@ -107,11 +107,3 @@ venv.bak/ # Kate .kate-swp -# man -*.1.gz - - - - - - diff --git a/README.md b/README.md index 6ad4059..d1515ac 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ https://2ch.hk/s/res/2310304.html#2311483, https://archive.li/idixk - verbosity: опциональность печати параметров конфига при старте программы, опциональность печати результатов проверки памяти и времени между проверками памяти - возможность предотвращения избыточного убийства процессов с помощью задания миниального `oom_score` для убиваемых процессов и установка минимальной задержки просле отправки сигналов (параметры конфига `min_delay_after_sigkill` и `min_delay_after_sigterm`) - возможность показа десктопных уведомлений c помощью `notify-send`, с показом сигнала (`SIGTERM` или `SIGKILL`), который отправлен процессу, а также `Pid`, `oom_score`, `VmRSS`, `VmSwap`, которыми обладал процесс перед получением сигнала. +- поддержка white, black, prefer и avoid списков с использованием Perl-compatible regular expressions - наличие `man` страницы - наличие установщика для пользователей `systemd` - протестировано на `Debian 9 x86_64`, `Debian 8 i386`, `Fedora 28 x86_64` diff --git a/nohang b/nohang index c508b19..b3f14f0 100755 --- a/nohang +++ b/nohang @@ -10,6 +10,7 @@ import os from operator import itemgetter from time import sleep from argparse import ArgumentParser +from re import fullmatch ########################################################################## @@ -147,35 +148,58 @@ def pid_to_name(pid): def send_notify(signal, name, pid, oom_score, vm_rss, vm_swap): - # текст отправляемого уведомления info = '"Nohang sent {} \nto the process {} \nP' \ - 'id: {} \noom_score: {} \nVmRSS: {} \nBadness: {} \nVmRSS: {} MiB \nVmSwap: {} MiB" &'.format( sig_dict[signal], name, pid, oom_score, vm_rss, vm_swap) - if root: - # отправляем уведомление всем залогиненным пользователям for uid in os.listdir('/run/user'): - root_notify_command = 'sudo -u {} DISPLAY={} notify-send {} "Pr' \ 'eventing OOM" '.format( users_dict[uid], display, notify_options) - os.system(root_notify_command + info) - else: - # отправляем уведомление пользователю, который запустил nohang user_notify_command = 'notify-send {} "Preventing OOM" '.format( notify_options) - os.system(user_notify_command + info) + +def send_notify_black(signal, name, pid): + # текст отправляемого уведомления + info = '"Nohang sent {}\nto blacklisted proce' \ + 'ss {}, Pid {}" &'.format( + sig_dict[signal], name, pid) + if root: + # отправляем уведомление всем залогиненным пользователям + for uid in os.listdir('/run/user'): + root_notify_command = 'sudo -u {} DISPLAY={} notify-send {} "Pr' \ + 'eventing OOM" '.format( + users_dict[uid], display, notify_options) + os.system(root_notify_command + info) + else: + # отправляем уведомление пользователю, который запустил nohang + user_notify_command = 'notify-send {} "Preventing OOM" '.format( + notify_options) + os.system(user_notify_command + info) + + +def sleep_after_send_signal(signal): + if signal is 9: + if print_sleep_periods: + print(' sleep', min_delay_after_sigkill) + sleep(min_delay_after_sigterm) + else: + if print_sleep_periods: + print(' sleep', min_delay_after_sigterm) + sleep(min_delay_after_sigterm) + + # принимает int (9 или 15) -def find_victim_and_send_signal(signal): +def find_victim_and_send_signal_without_regexp_lists(signal): # выставляем потолок для oom_score_adj всех процессов if decrease_oom_score_adj and root: @@ -247,18 +271,160 @@ def find_victim_and_send_signal(signal): else: print(' oom_score {} < oom_score_min {}'.format( - oom_score, oom_score_min) - ) + oom_score, oom_score_min)) + + sleep_after_send_signal(signal) + + +# принимает int (9 или 15) +def find_victim_and_send_signal_with_regexp_lists(signal): + + # выставляем потолок для oom_score_adj всех процессов + if decrease_oom_score_adj and root: + func_decrease_oom_score_adj(oom_score_adj_max) + + # получаем список процессов ((pid, badness)) + oom_list = [] + + # pid list + oom_black_list = [] + + for pid in os.listdir('/proc'): + if pid.isdigit() is not True: + continue + try: + + # пошла итерация для каждого существующего процесса + + oom_score = int(rline1('/proc/' + pid + '/oom_score')) + + name = pid_to_name(pid) + + # если имя в списке,то пропускаем! + res = fullmatch(white_list, name) + if res is not None: + #print(' {} (Pid: {}) is in white list'.format(name, pid)), + continue + + # если имя в черном списке - добавляем Pid в список для убийства + res = fullmatch(black_list, name) + if res is not None: + oom_black_list.append(pid) + #print(' {} (Pid: {}) is in black list'.format(name, pid)), + + res = fullmatch(avoid_list, name) + if res is not None: + # тут уже получаем badness + oom_score = int(oom_score / avoid_factor) + #print(' {} (Pid: {}) is in avoid list'.format(name, pid)), + + res = fullmatch(prefer_list, name) + if res is not None: + oom_score = int((oom_score + 1) * prefer_factor) + #print(' {} (Pid: {}) is in prefer list'.format(name, pid)), + + + except FileNotFoundError: + oom_score = 0 + except ProcessLookupError: + oom_score = 0 + oom_list.append((pid, oom_score)) + + + if oom_black_list != []: + + print(' Black list is not empty') + + for pid in oom_black_list: + + name = pid_to_name(pid) + print(' Try to send the {} signal to blacklisted process {}, Pid {}'.format( + sig_dict[signal], name, pid)) + + try: + os.kill(int(pid), signal) + print(' Success') + + if desktop_notifications: + send_notify_black(signal, name, pid) + + except FileNotFoundError: + print(' No such process') + except ProcessLookupError: + print(' No such process') + except PermissionError: + print(' Operation not permitted') + + # после отправки сигнала процессам из черного списка поспать и выйти из функции + #sleep_after_send_signal(signal) + #sleep(0.1) + return 0 + + + # получаем отсортированный по oom_score (по badness!) список пар (pid, oom_score) + pid_tuple_list = sorted(oom_list, key=itemgetter(1), reverse=True)[0] + + # получаем максимальный oom_score + oom_score = pid_tuple_list[1] + + # посылаем сигнал + if oom_score >= oom_score_min: + + pid = pid_tuple_list[0] + + name = pid_to_name(pid) + + # находим VmRSS и VmSwap процесса, которому попытаемся послать сигнал + try: + with open('/proc/' + pid + '/status') as f: + for n, line in enumerate(f): + if n is vm_rss_index: + vm_rss = kib_to_mib(int( + line.split('\t')[1][:-4])) + continue + if n is vm_swap_index: + vm_swap = kib_to_mib(int( + line.split('\t')[1][:-4])) + break + except FileNotFoundError: + vm_rss = 0 + vm_swap = 0 + except ProcessLookupError: + vm_rss = 0 + vm_swap = 0 + except IndexError: + vm_rss = 0 + vm_swap = 0 + + print(' Try to send the {} signal to {},\n Pid {}, Badness {}, V' + 'mRSS {} MiB, VmSwap {} MiB'.format( + sig_dict[signal], name, pid, oom_score, vm_rss, vm_swap)) + + try: + os.kill(int(pid), signal) + print(' Success') + + if desktop_notifications: + send_notify(signal, name, pid, oom_score, vm_rss, vm_swap) + + except FileNotFoundError: + print(' No such process') + except ProcessLookupError: + print(' No such process') - # спать всегда или только при успешной отправке сигнала? - if signal is 9: - if print_sleep_periods: - print(' sleep', min_delay_after_sigkill) - sleep(min_delay_after_sigterm) else: - if print_sleep_periods: - print(' sleep', min_delay_after_sigterm) - sleep(min_delay_after_sigterm) + print(' oom_score {} < oom_score_min {}'.format( + oom_score, oom_score_min)) + + sleep_after_send_signal(signal) + + +def find_victim_and_send_signal(signal): + if use_regexp_lists: + find_victim_and_send_signal_with_regexp_lists(signal) + else: + find_victim_and_send_signal_without_regexp_lists(signal) + ########################################################################## @@ -370,6 +536,8 @@ except IndexError: # валидация всех параметров + +# OK if 'print_config' in config_dict: print_config = config_dict['print_config'] if print_config == 'True': @@ -377,15 +545,15 @@ if 'print_config' in config_dict: elif print_config == 'False': print_config = False else: - print('Invalid print_config value {} (should be True or False)\nE' - 'xit'.format( - print_config)) + print('Invalid print_config value {} (shou' \ + 'ld be True or False)\nExit'.format(print_config)) exit() else: print('Print_config not in config\nExit') exit() +# OK if 'print_mem_check_results' in config_dict: print_mem_check_results = config_dict['print_mem_check_results'] if print_mem_check_results == 'True': @@ -393,15 +561,16 @@ if 'print_mem_check_results' in config_dict: elif print_mem_check_results == 'False': print_mem_check_results = False else: - print('Invalid print_mem_check_results value {} (should be Tr' - 'ue or False)\nExit'.format( - print_mem_check_results)) + print('Invalid print_mem_check_result' \ + 's value {} (should be True or False)\nExit'.format( + print_mem_check_results)) exit() else: print('print_mem_check_results not in config\nExit') exit() +# OK if 'print_sleep_periods' in config_dict: print_sleep_periods = config_dict['print_sleep_periods'] if print_sleep_periods == 'True': @@ -409,15 +578,15 @@ if 'print_sleep_periods' in config_dict: elif print_sleep_periods == 'False': print_sleep_periods = False else: - print('Invalid print_sleep_periods value {} (should be True or F' - 'alse)\nExit'.format( - print_sleep_periods)) + print('Invalid print_sleep_periods value {} (shou' \ + 'ld be True or False)\nExit'.format(print_sleep_periods)) exit() else: print('print_sleep_periods not in config\nExit') exit() +# OK if 'mlockall' in config_dict: mlockall = config_dict['mlockall'] if mlockall == 'True': @@ -427,51 +596,48 @@ if 'mlockall' in config_dict: else: print( 'Invalid mlockall value {} (should be True or False)\nExit'.format( - mlockall - ) - ) + mlockall)) exit() else: print('mlockall not in config\nExit') exit() +# OK if 'self_nice' in config_dict: - self_nice = config_dict['self_nice'] - if string_to_int_convert_test(self_nice) is None: + self_nice = string_to_int_convert_test(config_dict['self_nice']) + if self_nice is None: print('Invalid self_nice value, not integer\nExit') exit() - else: - self_nice = int(self_nice) - if self_nice < -20 or self_nice > 19: - print('Недопустимое значение self_nice\nExit') - exit() + if self_nice < -20 or self_nice > 19: + print('Недопустимое значение self_nice\nExit') + exit() else: print('self_nice not in config\nExit') exit() +# OK if 'self_oom_score_adj' in config_dict: - self_oom_score_adj = config_dict['self_oom_score_adj'] - self_oom_score_adj = string_to_int_convert_test(self_oom_score_adj) + self_oom_score_adj = string_to_int_convert_test( + config_dict['self_oom_score_adj']) if self_oom_score_adj is None: print('Invalid self_oom_score_adj value, not integer\nExit') exit() - else: - if self_oom_score_adj < -1000 or self_oom_score_adj > 1000: - print('Недопустимое значение self_oom_score_adj\nExit') - exit() + if self_oom_score_adj < -1000 or self_oom_score_adj > 1000: + print('Недопустимое значение self_oom_score_adj\nExit') + exit() else: print('self_oom_score_adj not in config\nExit') exit() +# OK if 'rate_mem' in config_dict: rate_mem = string_to_float_convert_test(config_dict['rate_mem']) if rate_mem is None: print('Invalid rate_mem value, not float\nExit') exit() - rate_mem = float(rate_mem) if rate_mem <= 0: print('rate_mem должен быть положительным\nExit') exit() @@ -480,12 +646,12 @@ else: exit() +# OK if 'rate_swap' in config_dict: rate_swap = string_to_float_convert_test(config_dict['rate_swap']) if rate_swap is None: print('Invalid rate_swap value, not float\nExit') exit() - rate_swap = float(rate_swap) if rate_swap <= 0: print('rate_swap должен быть положительным\nExit') exit() @@ -494,12 +660,12 @@ else: exit() +# OK if 'rate_zram' in config_dict: rate_zram = string_to_float_convert_test(config_dict['rate_zram']) if rate_zram is None: print('Invalid rate_zram value, not float\nExit') exit() - rate_zram = float(rate_zram) if rate_zram <= 0: print('rate_zram должен быть положительным\nExit') exit() @@ -508,6 +674,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'mem_min_sigterm' in config_dict: mem_min_sigterm = config_dict['mem_min_sigterm'] else: @@ -515,6 +682,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'mem_min_sigkill' in config_dict: mem_min_sigkill = config_dict['mem_min_sigkill'] else: @@ -522,6 +690,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'swap_min_sigterm' in config_dict: swap_min_sigterm = config_dict['swap_min_sigterm'] else: @@ -529,6 +698,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'swap_min_sigkill' in config_dict: swap_min_sigkill = config_dict['swap_min_sigkill'] else: @@ -536,6 +706,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'zram_max_sigterm' in config_dict: zram_max_sigterm = config_dict['zram_max_sigterm'] else: @@ -543,6 +714,7 @@ else: exit() +# НУЖНА ВАЛИДАЦИЯ НА МЕСТЕ! if 'zram_max_sigkill' in config_dict: zram_max_sigkill = config_dict['zram_max_sigkill'] else: @@ -550,13 +722,13 @@ else: exit() +# OK if 'min_delay_after_sigterm' in config_dict: min_delay_after_sigterm = string_to_float_convert_test( - config_dict['m' 'in_delay_after_sigterm']) + config_dict['min_delay_after_sigterm']) if min_delay_after_sigterm is None: print('Invalid min_delay_after_sigterm value, not float\nExit') exit() - min_delay_after_sigterm = float(min_delay_after_sigterm) if min_delay_after_sigterm < 0: print('min_delay_after_sigterm должен быть неотрицательным\nExit') exit() @@ -565,13 +737,13 @@ else: exit() +# OK if 'min_delay_after_sigkill' in config_dict: min_delay_after_sigkill = string_to_float_convert_test( - config_dict['mi' 'n_delay_after_sigkill']) + config_dict['min_delay_after_sigkill']) if min_delay_after_sigkill is None: print('Invalid min_delay_after_sigkill value, not float\nExit') exit() - min_delay_after_sigkill = float(min_delay_after_sigkill) if min_delay_after_sigkill < 0: print('min_delay_after_sigkill должен быть неотрицательным\nExit') exit() @@ -580,21 +752,22 @@ else: exit() +# OK if 'oom_score_min' in config_dict: - oom_score_min = config_dict['oom_score_min'] - if string_to_int_convert_test(oom_score_min) is None: + oom_score_min = string_to_int_convert_test( + config_dict['oom_score_min']) + if oom_score_min is None: print('Invalid oom_score_min value, not integer\nExit') exit() - else: - oom_score_min = int(oom_score_min) - if oom_score_min < 0 or oom_score_min > 1000: - print('Недопустимое значение oom_score_min\nExit') - exit() + if oom_score_min < 0 or oom_score_min > 1000: + print('Недопустимое значение oom_score_min\nExit') + exit() else: print('oom_score_min not in config\nExit') exit() +# OK if 'decrease_oom_score_adj' in config_dict: decrease_oom_score_adj = config_dict['decrease_oom_score_adj'] if decrease_oom_score_adj == 'True': @@ -602,19 +775,18 @@ if 'decrease_oom_score_adj' in config_dict: elif decrease_oom_score_adj == 'False': decrease_oom_score_adj = False else: - print('invalid decrease_oom_score_adj value {} (should be Tru' - 'e or False)\nExit'.format( - decrease_oom_score_adj)) + print('invalid decrease_oom_score_adj value {} (should b' \ + 'e True or False\nExit'.format(decrease_oom_score_adj)) exit() else: print('decrease_oom_score_adj not in config\nExit') exit() +# OK if 'oom_score_adj_max' in config_dict: - oom_score_adj_max = config_dict['oom_score_adj_max'] - - oom_score_adj_max = string_to_int_convert_test(oom_score_adj_max) + oom_score_adj_max = string_to_int_convert_test( + config_dict['oom_score_adj_max']) if oom_score_adj_max is None: print('Invalid oom_score_adj_max value, not integer\nExit') exit() @@ -626,6 +798,7 @@ else: exit() +# OK if 'desktop_notifications' in config_dict: desktop_notifications = config_dict['desktop_notifications'] if desktop_notifications == 'True': @@ -640,15 +813,16 @@ if 'desktop_notifications' in config_dict: elif desktop_notifications == 'False': desktop_notifications = False else: - print('Invalid desktop_notifications value {} (should be Tru' - 'e or False)\nExit'.format( - desktop_notifications)) + print('Invalid desktop_notifications value {} (shoul' \ + 'd be True or False)\nExit'.format( + desktop_notifications)) exit() else: print('desktop_notifications not in config\nExit') exit() +# OK if 'notify_options' in config_dict: notify_options = config_dict['notify_options'].strip() else: @@ -656,12 +830,90 @@ else: exit() +# OK if 'display' in config_dict: display = config_dict['display'].strip() else: print('display not in config\nExit') exit() + +# OK +if 'use_regexp_lists' in config_dict: + use_regexp_lists = config_dict['use_regexp_lists'] + if use_regexp_lists == 'True': + use_regexp_lists = True + elif use_regexp_lists == 'False': + use_regexp_lists = False + else: + print('invalid use_regexp_lists value {} (shoul' \ + 'd be True or False)\nExit'.format(use_regexp_lists)) + exit() +else: + print('use_regexp_lists not in config\nExit') + exit() + + +# OK +if 'white_list' in config_dict: + white_list = config_dict['white_list'].strip() +else: + print('white_list not in config\nExit') + exit() + + +# OK +if 'black_list' in config_dict: + black_list = config_dict['black_list'].strip() +else: + print('black_list not in config\nExit') + exit() + + +# OK +if 'prefer_list' in config_dict: + prefer_list = config_dict['prefer_list'].strip() +else: + print('prefer_list not in config\nExit') + exit() + + +# OK +if 'prefer_factor' in config_dict: + prefer_factor = string_to_float_convert_test(config_dict['prefer_factor']) + if prefer_factor is None: + print('Invalid prefer_factor value, not float\nExit') + exit() + if prefer_factor <= 0: + print('prefer_factor должен быть положительным\nExit') + exit() +else: + print('prefer_factor not in config\nExit') + exit() + + +# OK +if 'avoid_list' in config_dict: + avoid_list = config_dict['avoid_list'].strip() +else: + print('avoid_list not in config\nExit') + exit() + + +# OK +if 'avoid_factor' in config_dict: + avoid_factor = string_to_float_convert_test(config_dict['avoid_factor']) + if avoid_factor is None: + print('Invalid avoid_factor value, not float\nExit') + exit() + if avoid_factor <= 0: + print('avoid_factor должен быть положительным\nExit') + exit() +else: + print('avoid_factor not in config\nExit') + exit() + + ########################################################################## # получение уровней в кибибайтах @@ -813,6 +1065,17 @@ if print_config: print('desktop_notifications: {}'.format(desktop_notifications)) if desktop_notifications: print('notify_options: {}'.format(notify_options)) + print('display: {}'.format(display)) + + print('\nVII. BLACK, WHITE, AVOID AND PREFER LISTS') + print('use_regexp_lists: {}'.format(use_regexp_lists)) + if use_regexp_lists: + print('white_list: {}'.format(white_list)) + print('black_list: {}'.format(black_list)) + print('prefer_list: {}'.format(prefer_list)) + print('prefer_factor: {}'.format(prefer_factor)) + print('avoid_list: {}'.format(avoid_list)) + print('avoid_factor: {}'.format(avoid_factor)) # для рассчета ширины столбцов при печати mem и zram @@ -950,11 +1213,11 @@ while True: find_victim_and_send_signal(15) # задание периода в зависимости от рейтов и уровней доступной памяти - t_mem = mem_available / 1000000.0 / rate_mem + t_mem = mem_available / rate_mem - t_swap = swap_free / 10000000.0 / rate_swap + t_swap = swap_free / rate_swap - t_zram = (mem_total * 0.8 - mem_used_zram) / 1000000.0 / rate_zram + t_zram = (mem_total * 0.8 - mem_used_zram) / rate_zram if t_zram < 0.01: t_zram = 0.01 @@ -972,3 +1235,4 @@ while True: sleep(t) except KeyboardInterrupt: exit() + diff --git a/nohang.conf b/nohang.conf index ee5c6e1..d8994c0 100644 --- a/nohang.conf +++ b/nohang.conf @@ -24,7 +24,7 @@ print_mem_check_results = True Допустимые значения: True и False (В этой ветке по дефолту True) -print_sleep_periods = True +print_sleep_periods = False ##################################################################### @@ -69,11 +69,11 @@ self_oom_score_adj = -1000 В дефолтных настройках на данной интенсивности демон работает достаточно хорошо, успешно справляясь с резкими скачками потребления - памяти. + памяти. -rate_mem = 6 -rate_swap = 3 -rate_zram = 1 +rate_mem = 6000000 +rate_swap = 3000000 +rate_zram = 1000000 ##################################################################### @@ -115,6 +115,11 @@ zram_max_sigkill = 60 % Значение должно быть целым числом из диапазона [0; 1000] + Процессы из black_list (см ниже) получат сигнал вне зависимости + от значения их oom_score. + + Может min_badness с учетом списков? + oom_score_min = 20 Минимальная задержка после отправки соответствующих сигналов @@ -136,11 +141,11 @@ min_delay_after_sigkill = 3 Требует root прав. -decrease_oom_score_adj = True +decrease_oom_score_adj = False Допустимые значения - целые числа из диапазона [0; 1000] -oom_score_adj_max = 30 +oom_score_adj_max = 20 ##################################################################### @@ -170,3 +175,41 @@ notify_options = display = :0 +##################################################################### + + VII. BLACK, WHITE, AVOID AND PREFER LISTS + + Можно задать списки с помощью Perl-compatible regular expressions. + + Включение этой опции замедляет поиск жертвы, так как + имена всех процессов сравниваются с regexp паттернами всех + списков. + +use_regexp_lists = False + + Процессы из белого списка не получат сигнал. + +white_list = ^(Xorg|sshd)$ + + При нехватке памяти все процессы из черного списка получат сигнал. + +black_list = ^()$ + + Список предпочтительных для убийства процессов. + Badness процессов из prefer_list будет умножено на + prefer_factor перед выбором жертвы. На самом деле формула + для нахождения badness такая: + badness = (oom_score + 1) * prefer_factor + + prefer_factor и avoid_factor должны быть положительными числами. + +prefer_list = ^()$ +prefer_factor = 2 + + Список нежелательных для убийства процессов. + Badness процессов из avoid_list будет поделено на + avoid_factor перед выбором жертвы. + +avoid_list = ^()$ +avoid_factor = 2 + diff --git a/purge.sh b/purge.sh index a57e6cc..72a774a 100755 --- a/purge.sh +++ b/purge.sh @@ -1,7 +1,7 @@ #!/bin/bash -v systemctl stop nohang systemctl disable nohang -rm -f /usr/local/share/man/man1/nohang.1.gz -rm -f /etc/systemd/system/nohang.service -rm -rf /etc/nohang -rm -f /usr/local/bin/nohang +rm /usr/local/share/man/man1/nohang.1.gz +rm /etc/systemd/system/nohang.service +rm -r /etc/nohang +rm /usr/local/bin/nohang