Функциональное программирование в шелле на примере xargs

в 23:25, , рубрики: bash, linux, повышение квалификации, системное администрирование, метки: , ,

Abstract: рассказ о том, как быстро и красиво делать обработку списков в шелле, немного манула по xargs и много воды про философию то ли программирования, то ли администрирования.

Немного SEO-оптимизации: карринг, лямбда-функция, композиция функций, map, фильтрация списка, работа с множествами в шелле.

Пример

Функциональное программирование в шелле на примере xargs
Системные администраторы часто оказываются в ситуации, когда нужно взять вывод одной программы, и к каждому элементу вывода применить другую программу. Или даже не одну. В качестве забавного (и бесполезного) примера мы возьмём следующий: требуется посчитать суммарный размер всех исполняемых файлов, запущенных в настоящий момент в системе вместе со всеми динамическими библиотеками, которые они используют.

Это не реальная «задача», это учебный пример, решая который (в решении будет однострочник) я расскажу про очень необычный и мощный инструмент системного администрирования — линейное функциональное программирование. Линейное оно, потому что использование пайпа "|" это линейное программирование, а использование xargs позволяет превратить сложную программу с вложенными циклами в однострочник. Целью статьи будет не показать «как найти размер библиотек» и не пересказать аргументы xargs, а объяснить дух решения, пояснить стоящую за ним философию.

Лирика

Существует несколько стилей программирования. Один из них выглядит так: для каждого элемента списка сделать цикл, в котором для каждого элемента списка, если он не является пустой строкой, взять имя файла, и если размер файла не равен нулю, то прибавить к счётчику. Ах, да, сначала счётчик надо сделать нулём.

Другой выглядит так:
Применить к списку функцию, которая применяется к каждому элементу списка, если этот элемент непустая строка и размер файла, с этим именем не нулевой, добавить к сумме.

Даже словами видно, что второй вариант короче.

С учётом выразительных средств языков программирования конструкции получаются ёмкие и лишённые проблем обычных циклов — зацикливаний, неверного выхода из цикла и т.д.

Фактически, в контексте обсуждаемой задачи мы говорим про указание функции, которую нужно применить к каждому элементу списка. Сама функция, в свою очередь, может быть так же обработчиком списка, с своей обрабатывающей функцией.

Данные для решения

Для решения задачи нам нужно получить список запущенных процессов. Это проще, чем кажется — все процессы находятся в /proc/[1-9]+. Далее, нам нужны пути к бинарнику. И это тоже просто: /proc/PID/exe для всех процессов, кроме ядерных, указывает на путь к процессу. Следующая задача: нам нужен список библиотек для файла. Это делает команда ldd, которая ожидает путь к файлу и выводит (в хитром формате) список библиотек. Дальше вопрос техники и хождения по симлинкам — нам нужно пройти по симлинкам библиотек до самого упора, а потом посчитать размер каждого из файлов.

Таким образом, высокоуровневое описание задачи выглядит так: взять списки исполняемых файлов и библиотек, и для каждого из них узнать размер.

Заметим, мы в процессе список исполняемых файлов у нас будет использоваться два раза — один раз «сам по себе», второй раз — для получения списка библиотек каждого файла.

Императивное решение

(утрирую и опускаю детали)

get_exe_list()
  for a in `ls /proc/*;
     do
         readlink -f $a/exe;
     done

get_lib()
  for a in `cat `;
  do
       ldd $a
  done |awk '{print $3}'

calc()
   sum=0
   sum=$(( $sum + `for a in $(cat);   do      du -b $a|awk '{print $1}';   done` ))

exe_list=`get_exe_list|sort -u`
lib_list=`for a in $exe_list; do get_lib $a;done|sort -u`
size=$(( calc_size $exe_list + calc_size $lib_list))
echo $size

Отвратительно, правда? Это, заметим, без регэкспов на правильную фильтрацию pid'ов (нам не нужно пытаться прочитать не существующий /proc/mdstat/exe) и без обработки многочисленных случаев ошибок.

Списки

Осознаем задачу. Поскольку у нас входные данные — это однородные файлы, то мы можем просто представить их как список, и обрабатывать так же. Капсом я буду писать «не написанные» участки кода.

Часть первая: Двойная обработка списка

Мы немного смухлюем, и используем stderr для дупликации списка.

(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC

Что этот код делает? Ненаписанная пока часть EXE_LIST генерирует список exe в системе. tee берёт этот список, пишет на stderr (поток №2) и пишет в stdout (поток №1). Поток №1 передаётся в LIB_LIST. Дальше мы объединяем вывод всех трёх команд (скобки) с stderr и выпихиваем в stdout единым списком и передаём в CALC.

Теперь нам надо сделать EXE_LIST

Часть вторая: фильтрация списков

(прим. чтобы видеть _все_ процессы в системе, нужно быть рутом).

Мы пойдём немного необычным путём, и вместо ls в цикле, используем find. В принципе, можно и ls, но там будет больше проблем с обработкой симлинков.

Итак: find /proc/ -maxdepth 2 -name "exe" -ls
maxdepth нам нужен, чтобы игнорировать треды, мы получаем вывод, аналогичный выводу ls. Нам надо его отфильтровать.

Итак, улучшаем EXE_LIST:

find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}'

Наблюдение: мы будем использовать stderr для передачи данных, так что флуд find насчёт проблем с всякого рода ядерными тредами нам не нужен.

Часть третья: LIB_LIST

Всё просто: для каждого переданного файла нам нужно использовать ldd, после чего причесать вывод.

xargs -n 32 -P 4 ldd 2>/dev/null|grep "/"|awk '{print $3}'

Что тут интересного? Мы лимитируем ldd максимум 32 файлами за раз, и запускаем 4 очереди ldd в параллель (да, такой у нас доморощенный хадуп получается). Опция -P позволяет распараллелить исполнение ldd, что даст нам некоторый прирост скорости на многоядерных машинах и хороших дисках (пижонство это, в данном случае, но если мы выполняем что-то, более тормознутое, чем ldd, то параллелизм может быть спасением...).

Часть четвёртая: CALC

На входе файлы, на выходе надо отдать цифру суммарного размера всех файлов. Размер будем определять… Впрочем, стоп. Кто сказал, что симлинки указывают на файлы? Разумеется, симлинки указывают на симлинки, которые указывают на симлинки или на файлы. А те симлинки… Ну выпоняли.

Добавляем readlink. А он, зараза, хочет один параметр за раз. Зато есть опция -f, которая нам сэкономит кучу усилий — она покажет имя файла вне зависимости от того, симлинк это или просто файл.

|xargs -n 1 -P 16 readlink -f|sort -u

… так вот, размер будем определять с помощью du. Заметим, мы могли бы тут просто использовать опцию -С, которая просуммирует цифры и даст ответ, но в учебном курсе мы простых путей не ищем. Значит, без мухлежа.

|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{printf "%in", sum}'

Зачем нам sort -u? Дело в том, что у нас будет очень много повторов. Нам надо из списка выбрать уникальные значения, то есть превратить список во множество (set). Делается это наивным методом: мы сортируем список и говорим, выкинуть при сортировке повторяющиеся строки.

Однострочник, ужасающий

Выписываем всё вместе:

(find /proc/ -name «exe» -ls 2>/dev/null|awk '{print $13}'|tee /dev/stderr| xargs -n 32 -P 4 ldd 2>/dev/null|grep /|awk '{print $3}') 2>&1|sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print sum}'

(я не поставил тега сырца в этом ужасе, чтобы строка автоматически переносилась, чтобы не порвать вам ленты rss-ридеров, любите меня).

Разумеется, ЭТО ничем не лучше того, что было приведено в начале. Страх и ужас, одним словом. Хотя, если вы гуру 98 левела и качаете 99ый, то такие однострочники могут быть обычным стилем работы…

Впрочем, назад, к штурму 10ого левела.

Приличный вид

Нам хочется порезать это. На читаемые и отлаживаемые куски кода. Вменяемые. Комментируемые.

Итак, вернёмся к начальной форме записи: (EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC

Читаемо? Ну, наверное, да.

Осталось придумать, как правильно записать EXE_LIST

Вариант первый: с использованием функций:

EXE_LIST ()
    find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}'
LIB_LIST ()
    xargs -n 32 -P 4 ldd 2>/dev/null|grep /|awk '{print $3}'
CALC()
   sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print  sum}'

(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC

И чуть-чуть фунционального благородства:

EXE_LIST () ( find /proc/ -name "exe" -ls 2>/dev/null|awk '{print $13}' )
LIB_LIST ()  ( xargs -n 32 -P 4 ldd 2>/dev/null|grep /|awk '{print $3}' )
CALC()  ( sort -u|xargs -n 1 -P 16 readlink -f|xargs -n 32 -P 4 du -b|awk '{sum+=$1}END{print  sum}' )
(EXE_LIST |tee /dev/stderr|LIB_LIST) 2>&1 | CALC

Разумеется, пуристы ФЯПов скажут, что тут нет ни выведения типов, ни их контроля, нет ленивых вычислений, и вообще, считать это list processing в функциональном стиле — безумие и порнография.

Однако, это код, он работающий, его легко писать. Он не должен быть реальной продакт-средой, но он запросто может быть средством, которое из трёх часов монотонной работы оставит 5 минут интересного программирования. Такова специфика работы сисадмина.

Основное, что я хотел показать: функциональный подход к обработке последовательностей в шелле даёт более читаемый и менее громоздкий код, чем прямая итерация. И работает быстрее, кстати (за счёт параллелизма xargs).

scalabilty

Дальнейшим развитием «хадупа на шелле» является утилита gnu parallels, позволяющая выполнять код на нескольких серверах в параллель.

Автор: amarao

Поделиться

* - обязательные к заполнению поля