Файловая система proc (в дальнейшем просто procfs) является виртуальной файловой системой, которая предоставляет информацию о процессах. Она — “прекрасный” пример интерфейсов следующих парадигме “все является файлом”. Procfs была разработана очень давно: во времена, когда серверы в среднем обслуживали несколько десятков процессов, когда открыть файл и вычитать информацию о процессе не было проблемой. Однако время не стоит на месте, и сейчас серверы обслуживают сотни тысяч, а то и больше процессов одновременно. В таком контексте идея “открыть файл для каждого процесса, чтобы вычитать интересующие данные” уже не выглядит такой привлекательной, и первое что приходит на ум чтобы ускорить чтение — это получение информации о группе процессов за одну итерацию. В этой статье мы попробуем найти элементы procfs которые можно оптимизировать.  Сама мысль улучшить procfs возникла когда мы обнаружили, что CRIU тратит значительное количество времени просто читая procfs файлы. Мы видели как подобная проблема была решена для сокетов, и решили сделать что-то похожее на sock-diag интерфейс, но только для procfs. Конечно мы предполагали, насколько сложно будет поменять давнишний и вполне устоявшийся интерфейс в ядре, убедить сообщество, что игра стоит свеч… и были приятно удивлены количеством людей, которые поддержали создание нового интерфейса. Строго говоря, никто не знал, как должен выглядеть новый интерфейс, но сомнений в том, что procfs не удовлетворяет текущим требованиям по производительности нет. Например такой сценарий: сервер отвечает на запросы слишком долго, vmstat показывает, что память ушла в своп, а запуск “ps ax” выполняется от 10 секунд и более, top и вовсе ничего не показывает. В этой статье мы не будем рассматривать какой-то конкретный новый интерфейс, скорее попробуем описать проблемы и пути их решения. Каждый исполняющийся процесс procfs представляет директорией /proc/<pid>.
В каждой такой директории множество файлов и поддиректорий, которые предоставляют доступ к определенной информации о процессе. Поддиректории группируют данные по признакам. Например ($$ это специальная переменная оболочки, которая раскрывается в pid — идентификатор текущего процесса): Все эти файлы выдают данные в разных форматах. Большинство в формате ASCII текста, который легко воспринимается человеком. Ну почти легко: Чтобы понять, что значит каждый элемент этого множества, читателю придется открыть man proc(5), либо документацию ядра. Например, второй элемент — это имя исполняемого файла в скобках, а девятнадцатый элемент — это текущее значение приоритета исполнения (nice). Некоторые файлы вполне читабельны сами по себе: Но как часто пользователи читают информацию напрямую из файлов procfs? Сколько времени нужно ядру чтобы перевести бинарные данные в текстовый формат? Какие накладные расходы у procfs? Насколько удобен такой интерфейс для программ мониторов состояния, и сколько времени они тратят чтобы обработать эти текстовые данные? Насколько критична такая медленная реализация в аварийных ситуациях? Скорее всего, не будет ошибкой сказать, что пользователи предпочитают программы типа top или ps, вместо того, чтобы читать данные из procfs напрямую. Для ответа на остальные вопросы проведем несколько экспериментов. Во-первых, найдем где именно ядро тратит время, чтобы сгенерировать файлы procfs. Чтобы получить определенную информацию у всех процессов в системе, нам придется пройти по директории /proc/ и выбрать все поддиректории, имя которых представлено десятичными цифрами. Затем, в каждой из них, нам необходимо открыть файл, прочитать его и закрыть. Суммарно мы исполним три системных вызова, причем один из них создаст файловый дескриптор (в ядре файловый дескриптор ассоциируется с набором внутренних объектов, для которых выделяется дополнительная память). Системные вызовы open() и close() сами по себе не дают нам никакой информации, так что их можно отнести к накладным расходам интерфейса procfs. Попробуем просто сделать open() и close() для каждого процесса в системе, но не будем читать содержимое файлов: task-proc-all — небольшая утилита, с кодом которой можно ознакомится по ссылке снизу Неважно какой именно файл открыть, поскольку реальные данные генерируются только в момент read(). А теперь посмотрим на вывод профилировщика ядра perf: Ядро тратит почти 75% времени просто чтобы создать и удалить файловый дескриптор, и около 16% чтобы вывести список процессов. Хотя мы и знаем сколько времени нужно на вызовы open() и close() для каждого процесса, мы пока не можем оценить насколько оно значительно. Нам надо сравнить полученные величины с чем-то. Попробуем сделать тоже самое с наиболее известными файлами. Обычно, когда надо вывести список процессов, используется утилита ps или top. Они обе читают /proc/<pid>/stat и /proc/<pid>/status для каждого процесса в системе. Начнем с /proc/<pid>/status — это массивный файл с фиксированным количеством полей: Видно, что только около 60% времени потрачено внутри системного вызова read(). Если же посмотреть профиль более внимательно, то обнаруживается, что 45% времени использовано внутри функций ядра seq_printf, seq_put_decimal_ull. А значит, конвертирование из бинарного формата в текстовый достаточно затратная операция. Что вызывает вполне обоснованный вопрос: а действительно ли нам нужен текстовый интерфейс, чтобы вытащить данные из ядра? Как часто пользователи хотят работать с сырыми данными? И почему утилитам top и ps приходится конвертировать эти текстовые данные обратно в бинарный вид? Наверное, интересно было бы узнать, насколько быстрее был бы вывод, если бы использовались бинарные данные напрямую, и если бы не требовалось три системных вызова. Попытки создать такой интерфейс уже были. В 2004 пробовали использовать netlink движок. К сожалению, сообщество не проявило большого интереса к этой работе. Одна из последних попыток исправить ситуацию произошла два года назад. Интерфейс task-diag базируется на следующих принципах: Этот интерфейс был презентован на нескольких конференциях. Его интегрировали в утилиты pstools, CRIU, а также David Ahern интегрировал task_diag в perf, в качестве эксперимента. Сообщество разработчиков ядра заинтересовалось интерфейсом task_diag. Основным предметом обсуждений стал выбор транспорта между ядром и пространством пользователя. Начальная идея использования netlink сокетов была отклонена. Частично из-за нерешенных проблем в коде самого netlink движка, а частично потому, что многие думают, что интерфейс netlink был разработан исключительно для сетевой подсистемы. Потом было предложено использовать транзакционные файлы внутри procfs, то есть пользователь открывает файл, записывает в него сам запрос, а затем просто читает ответ. Как обычно, оказались и противники данного подхода. Решения, которое понравилось бы всем, пока не найдено. Давайте сравним производительность task_diag с procfs. У task_diag движка есть тестовая утилита, которая удачно подходит для наших экспериментов. Предположим, что мы хотим запросить идентификаторы процесса и его права. Ниже приведен вывод для одного процесса:  А теперь для всех процессов в системе, то есть тоже самое, что мы делали для эксперимента с procfs, когда читали файл /proc/pid/status: Всего лишь 0.05 секунды потребовалось, чтобы получить данные для построения дерева процессов. А с procfs требовалось 0.177 секунды только на открытие одного файла для каждого процесса, причем без чтения данных. Вывод perf для task_diag интерфейса: В самом листинге нет ничего интересного, кроме факта, что здесь нет очевидных функций, подходящих для оптимизации. Посмотрим на вывод perf при чтении информации обо всех процессах в системе: Для procfs нам нужно выполнить более 150000 системных вызовов, чтобы вытащить информацию о всех процессах, а для task_diag — чуть более 50. Посмотрим на реальные ситуации из жизни. Например, мы хотим вывести дерево процессов вместе с аргументами командной строки для каждого. Для этого нам необходимо вытащить pid процесса, pid его родителя и непосредственно сами аргументы командной строки. Для интерфейса task_diag программа отправляет один запрос, чтобы получить все параметры разом: Для оригинального procfs нам необходимо читать /proc//status and /proc//cmdline у каждого процесса:
 Нетрудно заметить, что task_diag в 7 раз быстрее procfs (0.096 против 0.27 + 0.46). Обычно улучшение производительности на несколько процентов уже хороший результат, а тут скорость увеличилась почти на порядок. Стоит также упомянуть, что создание внутренних объектов ядра тоже сильно влияет на производительность. Особенно в случае, когда подсистема памяти под сильной нагрузкой. Сравним количество созданных объектов для procfs и task_diag: А также надо выяснить сколько создается объектов при запуске простого процесса, например утилиты true: Procfs создает в 600 раз больше объектов, чем task_diag. Это одна из причин, почему procfs работает так плохо, когда сильная нагрузка по памяти. Хотя бы поэтому стоит ее оптимизировать. Надеемся, что статья привлечёт больше разработчиков к оптимизации состояния procfs подсистемы ядра. Огромная благодарность David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman и многим другим, кто помогал разрабатывать и улучшать task_diag интерфейс. Спасибо cromer, k001 и Станиславу Кинсбурскому за помощь в написании этой статьи. Системный программист