Новий інтерфейс для отримання атрибутів процесів в Linux

Розробляючи CRIU, ми зрозуміли, що поточний інтерфейс отримання інформації про процеси не ідеальний. До того ж, подібна проблема була успішно вирішена для сокетів. Ми спробували перенести ці напрацювання на процеси і отримали досить хороші результати, про які ви дізнаєтеся, дочитавши цю статтю до кінця.

Недоліки поточного інтерфейсу
Прочитавши заголовок, виникає питання:«А чим же старий інтерфейс не догодив»? Багато хто з вас знають, що зараз інформація про процеси збирається файлової системи procfs. Тут кожному процесу відповідає директорія, яка містить кілька десятків файлів.

$ ls /proc/self/ 
attr cwd loginuid numa_maps schedstat task
autogroup environ map_files oom_adj sessionid timers
auxv exe maps oom_score setgroups uid_map
cgroup fd mem oom_score_adj smaps wchan
clear_refs fdinfo mountinfo pagemap stack
cmdline gid_map mounts personality stat
comm io mountstats projid_map statm
coredump_filter latency net root status
cpuset limits ns sched syscall


Кожен файл містить деяку кількість атрибутів. Перша проблема полягає в тому, що нам доведеться на кожен процес прочитати мінімум один файл, тобто зробити три системних виклику. Якщо ж потрібно зібрати дані про сотні тисяч процесів, це може зайняти тривалий час навіть на потужній машині. Деякі напевно можуть згадати, що утиліта ps або top працює повільно на завантажених машинах.

Друга проблема пов'язана з тим, як властивості процесів розбиті файлів. У нас є хороший приклад, який показує, що поточне розбиття не дуже хороше. У CRIU є завдання отримати дані про всіх регіонах пам'яті процесу. Якщо ми подивимося в файл /proc/PID/maps, то ми виявимо, що він не містить прапорів, які необхідні для відновлення регіонів пам'яті. На наше щастя, є ще один файл /proc/PID/smaps, який містить необхідну інформацію, а так само статистику по витраченої фізичної пам'яті (яка нам не потрібна). Простий експеримент показує, що формування першого файлу займає на порядок менше часу.

$ time cat /proc/*/maps > /dev/null
real 0m0.061s
user 0m0.002s
sys 0m0.059s

$ time cat /proc/*/smaps > /dev/null
real 0m0.253s
user 0m0.004s
sys 0m0.247s


Ймовірно, ви вже здогадалися, що у всьому винна статистика споживання пам'яті — на її збір йде велика частина часу.

Третю проблему можна побачити у форматі файлів. По-перше, немає єдиного формату. По-друге, формат деяких файлів принципово не можна розширити (саме з цієї причини ми не можемо додати поле з прапорами в /proc/PID/maps). По-третє, багато файлів в текстовому форматі, які легко читаються людиною. Це зручно, коли ви хочете подивитися на сам процес. Однак, коли стоїть завдання проаналізувати тисячі процесів, то ви не будете переглядати їх очима, а напишете якийсь код. Розбирати файли різних форматів — не саме приємне проведення часу. Бінарний формат зазвичай зручніше для обробки в програмному коді, а його генерація часто вимагає менше ресурсів.

Інтерфейс отримання інформації про сокети socket-diag
Коли ми почали робити CRIU, постала проблема з отриманням інформації про сокети. Для більшості типів сокетів, як зазвичай, використовувалися файли в /proc (/proc/net/unix, /proc/net/netlink тощо), що містять досить обмежений набір параметрів. Для INET сокетів існував netlink інтерфейс, який представляв інформацію в бінарному вигляді і легко розширюваному форматі. Цей інтерфейс вдалося узагальнити всі типи сокетів.
Працює він таким чином. Спочатку формується запит, який задає набір груп параметрів і набір сокетів, для яких вони потрібні. На виході ми отримуємо необхідні дані, розбиті на повідомлення. Одне повідомлення описує один сокет. Всі параметри розбиті на групи, які можуть бути досить малими, так як несуть накладні витрати тільки на розмір повідомлення. Кожна група описується типом і розміром. При цьому у нас є можливість розширювати існуючі групи або додавати нові.

Новий інтерфейс отримання атрибутів процесів task-diag
Коли ми побачили проблеми з отриманням даних про процеси, то тут же прийшла аналогія з сокетами, і виникла ідея використовувати той же інтерфейс для процесів.

Всі атрибути потрібно розбити на групи. Тут є одне важливе правило — жодної з атрибутів не повинен надавати помітного впливу на час, необхідний для генерації всіх атрибутів в групі. Пам'ятаєте, я розповідав про /proc/PID/smaps? В новому інтерфейсі ми винесли цю статистику в окрему групу.

На першому етапі ми не ставили завдання покрити всі атрибути. Хотілося зрозуміти, наскільки новий інтерфейс зручний для використання. Тому ми вирішили зробити інтерфейс, достатній для потреб CRIU. В результаті вийшов наступний набір груп атрибутів:
TASK_DIAG_BASE /* основна інформація pid, tid, sig, pgid, comm */
TASK_DIAG_CRED, /* права доступу */
TASK_DIAG_STAT, /* те ж, що представляє taskstats інтерфейс */
TASK_DIAG_VMA, /* опис регіонів пам'яті */
TASK_DIAG_VMA_STAT, /* доповнити опис регіонів пам'яті статистикою споживання ресурсів */
TASK_DIAG_PID = 64, /* ідентифікатор нитки */
TASK_DIAG_TGID, /* ідентифікатор процесу */


Насправді тут представлена поточна версія розбиття на групи. Зокрема, ми бачимо тут TASK_DIAG_STAT, який з'явився в другій версії в рамках інтеграції інтерфейсу з вже існуючим taskstats, побудованим на базі netlink сокетів. Останній використовує netlink протокол і має ряд відомих проблем, які ми ще торкнемося в цій статті.

І пара слів про те, як задається група процесів, про яких потрібна інформація.
#define TASK_DIAG_DUMP_ALL 0 /* про всі процеси в системі*/
#define TASK_DIAG_DUMP_ALL_THREAD 1 /* про всі нитки в системі */
#define TASK_DIAG_DUMP_CHILDREN 2 /* про всіх дітей зазначеного процесу */
#define TASK_DIAG_DUMP_THREAD 3 /* про всіх нитках зазначеного процесу */
#define TASK_DIAG_DUMP_ONE 4 /* про одному заданому процесі */


У процесі реалізації виникло кілька питань. Інтерфейс повинен бути доступний для звичайних користувачів, тобто нам потрібно було десь зберігати права доступу. Друге питання — звідки брати посилання на простір імен процесів (pidns)?

Почнемо з другого. Ми використовуємо netlink інтерфейс, який базується на сокетах і використовується в основному в мережеву підсистему. Посилання на мережеве простір імен беруть з сокета. У нашому випадку посилання потрібно взяти на простір імен процесів. Почитавши трохи код ядра, з'ясувалося, що кожне повідомлення містить інформацію про відправника (SCM_CREDENTIALS). Воно містить ідентифікатор процесу, що дозволяє нам взяти посилання на простір імен процесів з відправника. Це йде врозріз c мережевим простором імен, т. к. сокет прив'язується до простору імен, в якому він був створений. Брати посилання на pidns з процесу, запитів інформацію, напевно, допустимо, до того ж ми отримуємо можливість задати потрібний нам неймспейс, т. к. інформацію про відправника можна поставити на стадії відправки.

Перша проблема виявилася набагато цікавіше, хоча її деталі ми довго не могли зрозуміти. У файлових дескрипторів в Linux є одна особливість. Ми можемо відкрити файл і знизити собі привілеї, при цьому файловий дескриптор залишиться повністю функціональним. Це ж в якійсь мірі вірно і для netlink сокетів, але тут є проблема, на яку вказав мені Енді Лютомирский (Andy Lutomirski). Полягає вона в тому, що у нас немає можливості точно визначити, для чого саме цей сокет буде використовуватися. Тобто, якщо у нас є програма, яка створює netlink сокет і потім знижує свої привілеї, то це додаток зможе використовувати сокет для будь-якої функціональності, яка доступна для netlink сокетів. Іншими словами, зниження привілеїв не впливає на netlink сокет. Коли ми додаємо нову функціональність до netlink сокетам, ми відкриваємо нові можливості для додатків, що їх використовують, що є серйозною проблемою безпеки.

Були й інші пропозиції щодо інтерфейсу. Зокрема, була ідея додати новий системний виклик. Але вона мені не дуже подобалася, т. к. даних може бути занадто багато, щоб записати їх цілком в один буфер. Файловий дескриптор передбачає вичитування даних порціями, що, на мій погляд, виглядає більш розумним.

Так само була пропозиція зробити транзакционый файл у файловій системі procfs. Ідея схожа з тим, що ми робили для netlink сокетів. Відкриваємо файл, записуємо запит, читаємо відповідь. Саме на цій ідеї ми і зупинилися, як на робочому варіанті для наступної версії.

Кілька слів про продуктивності
Перша версія не викликала великого обговорення, але допомогла знайти ще одну групу людей, зацікавлених в новому, більш швидкому інтерфейсі для отримання властивостей процесів. Одного разу ввечері я поділився свій роботою з Павлом Одинцовим (@pavelodintsov), і він розповів, що у нього нещодавно були проблеми з perf-му, і пов'язані вони були теж зі швидкістю збору атрибутів процесів. Ось так він звів нас з Девідом Аерном (David Ahern), який вніс свій чималий внесок у розвиток інтерфейсу. Він також довів ще на одному прикладі, що дана робота потрібна не тільки нам.

Порівняння продуктивності можна почати з простого прикладу. Припустимо, що нам треба для всіх процесів отримати номер сесії, групи та інші параметри файли з /proc/pid/stat.

Для чесного порівняння напишемо невелику програму, яка буде зачитувати /proc/PID/status для кожного процесу. Нижче ми побачимо, що вона працює швидше, ніж утиліта ps.

while ((de = readdir(d))) {
if (de->d_name[0] < '0' || de->d_name[0] > '9')
continue;
snprintf(buf, sizeof(buf), "/proc/%s/stat", de->d_name);
fd = open(buf, O_RDONLY);
read(fd, buf, sizeof(buf));
close(fd);
tasks++;
}


Програма для task-diag більш об'ємна. Її можна буде знайти в моєму репозиторії в директорії tools/testing/selftests/task_diag/.

$ ps a -o pid,ppid,pgid,sid,comm | wc -l
50006
$ time ps a -o pid,ppid,pgid,sid,comm > /dev/null

real 0m1.256s
user 0m0.367s
sys 0m0.871s

$ time ./task_proc_all a
завдання: 50085

real 0m0.279s
user 0m0.013s
sys 0m0.255s

$ time ./task_diag_all a

real 0m0.051s
user 0m0.001s
sys 0m0.049s



Навіть на такому простому прикладі видно, що task_diag працює в кілька разів швидше. Утиліта ps працює повільніше, так як читає більше файлів на один процес.

Давайте подивимося, що нам покаже pref trace --summary для обох варіантів.

$ perf trace --summary ./task_proc_all a
завдання: 50086

Summary of events:

task_proc_all (72414), 300753 events, 100.0%, 0.000 msec

syscall calls min avg max stddev
(msec) (msec) (msec) (%)
--------------- -------- --------- --------- --------- ------
read 50091 0.003 0.005 0.925 0.40%
write 1 0.011 0.011 0.011 0.00%
open 50092 0.003 0.004 0.992 0.49%
close 50093 0.002 0.002 0.061 0.15%
fstat 7 0.002 0.003 0.008 25.95%
mmap 18 0.002 0.006 0.026 19.70%
mprotect 10 0.006 0.010 0.020 13.28%
munmap 2 0.012 0.020 0.028 40.18%
brk 3 0.003 0.007 0.010 30.28%
rt_sigaction 2 0.003 0.003 0.004 18.81%
rt_sigprocmask 1 0.003 0.003 0.003 0.00%
access 1 0.005 0.005 0.005 0.00%
getdents 50 0.003 0.940 2.023 4.51%
getrlimit 1 0.003 0.003 0.003 0.00%
arch_prctl 1 0.002 0.002 0.002 0.00%
set_tid_address 1 0.003 0.003 0.003 0.00%
openat 1 0.022 0.022 0.022 0.00%
set_robust_list 1 0.003 0.003 0.003 0.00%


$ perf trace --summary ./task_diag_all a

Summary of events:

task_diag_all (72481), 183 events, 94.8%, 0.000 msec

syscall calls min avg max stddev
(msec) (msec) (msec) (%)
--------------- -------- --------- --------- --------- ------
read 31 0.003 1.471 6.364 14.43%
write 1 0.003 0.003 0.003 0.00%
open 7 0.005 0.008 0.020 26.21%
close 6 0.002 0.002 0.003 3.96%
fstat 6 0.002 0.002 0.003 4.67%
mmap 17 0.002 0.006 0.030 25.38%
mprotect 10 0.005 0.007 0.010 6.33%
munmap 2 0.007 0.006 0.008 13.84%
brk 3 0.003 0.004 0.004 9.08%
rt_sigaction 2 0.002 0.002 0.002 9.57%
rt_sigprocmask 1 0.002 0.002 0.002 0.00%
access 1 0.006 0.006 0.006 0.00%
getrlimit 1 0.002 0.002 0.002 0.00%
arch_prctl 1 0.002 0.002 0.002 0.00%
set_tid_address 1 0.002 0.002 0.002 0.00%
set_robust_list 1 0.002 0.002 0.002 0.00%


Кількість системних викликів у випадку з task_diag серйозно зменшується.

Результати для утиліти perf (цитата з листа Девіда Аерна (David Ahern)).
> Using the fork test command:
> 10,000 processes; 10k proc with 5 threads = 50,000 tasks
> reading /proc: 11.3 sec
> task_diag: 2.2 sec
>
> @7,440 tasks, reading /proc is at 0.77 sec and task_diag at 0.096
>
> 128 instances of sepcjbb, 80,000+ tasks:
> reading /proc: 32.1 sec
> task_diag: 3.9 sec
>
> So overall much snappier startup times.


Тут ми бачимо приріст продуктивності на порядок.

Висновок
Даний проект знаходиться в розробці, і все ще може багато разів змінитися. Але вже зараз у нас є два реальних проекту, на прикладі яких можна бачити серйозний приріст продуктивності. Я майже впевнений, що в тому чи іншому вигляді ця робота рано чи пізно потрапить в основну гілку ядра.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.