Ускорение запуска приложений в Linux

power_up_linux

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

На примере собранной статистики с утилиты bootchart или подобных можно увидеть количество запускаемых утилит и сервисов при загрузке системы: www.bootchart.org/samples.html

Каждый запускаемы бинарник, обычно динамически, слинкован минимум с glibc, и другими ему нужными библиотеками. По правильной схеме, приложение открывает библиотеку через вызов dlopen(),

handle = dlopen("libm.so", RTLD_LAZY);
          if (!handle) {
               fprintf(stderr, "%s\n", dlerror());
               exit(EXIT_FAILURE);
}

Дальше подключаются функции из libdl.so и поиск нужно библиотеки идет с учетом строк в файле /etc/ld.so.conf/etc/ld.so.preload, переменных LD_LIBRARY_PATH иLD_PRELOAD и в кэше /etc/ld.so.cache, и некоторых других, зависящих от системы и glibc, условий. Напомню, ld.so.cache формируется утилитой ldconfig, с учетом путей указанных в /etc/ld.so.conf. Порядок обхода путей поиска совпадает с последовательностью, указанной в этом файле.

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

# strace -e open ps

open("/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib64/tls/x86_64/libprocps.so.0", O_RDONLY|O_CLOEXEC) = -1  ENOENT (No such file or directory)
open("/usr/lib64/tls/libprocps.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/usr/lib64/libprocps.so.0", O_RDONLY|O_CLOEXEC) = 0
open("/usr/lib64/tls/x86_64/librt.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/usr/lib64/tls/librt.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/usr/lib64/librt.so.1", O_RDONLY|O_CLOEXEC) = 0

… итд.

Как видите, библиотеки libprocps и librt нашлись только с третьего раза, выделил жирным. (… почему glibc не использует комбинацию stat+open, не ясно, но не об этом …).

Уже наверно догадались, что для поиска подобных ошибок, мы будем использовать утилиту strace. Не вдаваясь в подробности, strace — отслеживает системные вызовы вызываемой исследуемой программой. У неё есть два, нас интересующих параметра -e c аргументом, имени интересующего нас системного вызова, и флаг -f — отслеживающий системные вызовы у дочерних процессов.
(-ff и -o, но оних позже).

1. Библиотеки

Сама оптимизация, если это можно так назвать, заключается в подсовывании нужных каталогов нашим бинарникам. Плодить копии одних и тех же библиотек мы не будем, а просто создадим ссылки с нужными на каталоги или на файлы с нужными на библиотеками. Из примера выше, мы видим, что приложение в первую очередь ищет в каталоге /usr/lib64/tls/x86_64, которого у нас нет. Нужно его создать, но только на один уровень меньше, т.е /usr/lib64/tls, а в нем ссылку x86_64 на каталог /usr/lib64:

# mkdir /usr/lib64/tls/ 
# ln -s /usr/lib64/tls/x86_64 /usr/lib64

посмотрим результат

# ls -l /usr/lib64/tls/x86_64 

/usr/lib64/tls/x86_64 -> /usr/lib64

Запустим ещё раз проверку

$ strace -e open ps

open("/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib64/libprocps.so.0", O_RDONLY|O_CLOEXEC) = 0
open("/usr/lib64/librt.so.1", O_RDONLY|O_CLOEXEC) = 0

Прекрасно, минус четыре системных вызова и это только с двух библиотек и одного бинарника.

2. Локали.

Аналогичная ситуация с файлами локализаций и локальными настройками LC_CTYPE,LC_MESSAGES и остальные. Эта бяда тянется ещё с лохматых 90-х, по крайней мере в SuSE. Данные файлы и настройки должны лежать в каталоге $LOCALES/$USER_LOCALE(имена условные).
LOCALES — это общая свалка для всех поддерживаемых локалей. В реальности это переменная I18NPATH (у кого в дистрибутиве она установлена? :) (используемые каталоги локалей можно посмотреть командой localedef —help )
USER_LOCALE — это пользовательская настройка, которую можно узнать командой locale.

$ locale
LANG=ru_RU.UTF-8
LC_CENGINE=ru_RU.UTF-8
LC_MESSAGES=ru_RU.UTF-8
…

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

$ strace -e open ps

open("/usr/lib/locale/ru_RU.UTF-8/LC_MESSAGES", O_RDONLY|O_CLOEXEC) = -1
open("/usr/lib/locale/ru_RU/LC_MESSAGES", O_RDONLY|O_CLOEXEC) =  -1 
open("/usr/lib/locale/ru/LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3

То есть, сначала в /usr/lib/locale/ru_RU.UTF-8, потом в /usr/lib/locale/ru_RU, затем в /usr/lib/locale/ru
Исправляем по предыдущей схеме — создаём ссылку /usr/lib/locale/ru_RU.UTF-8 на каталог /usr/lib/locale/ru 

# ln -s /usr/lib/locale/ru_RU.UTF-8 /usr/lib/locale/ru;

и скажем glibc, что мы используем ru_RU.UTF-8, а не ru.
(кто не знает как исправить в случае глюков, лучше не делайте)

# localedef -f UTF-8 -i ru_RU ru_RU.UTF-8;

и проверим:

$ strace -e open ps

open("/usr/lib/locale/ru_RU.UTF-8/LC_MEASUREMENT", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/ru_RU.UTF-8/LC_TELEPHONE", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/ru_RU.UTF-8/LC_ADDRESS", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/ru_RU.UTF-8/LC_NAME", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/ru_RU.UTF-8/LC_PAPER", O_RDONLY|O_CLOEXEC) = 3
open("/usr/lib/locale/ru_RU.UTF-8/LC_MESSAGES", O_RDONLY|O_CLOEXEC) = 3
…

замечательно, минус по два open на каждый параметр!

3. Пользовательские и графические приложения.

Начнём с классики — xterm. Работает в полном соответствии с POSIX, LSB, X/Open, IEEE, ISO, ГОСТ, DIN :) Лезет в shared library, в локали, локали Xorg и далее специфичные для Xorg/Xfree86 конфиги. Опять же без вникания в суть, Xorg дублирует конфиги, точнее их имена: Но в отличии от glibc, Xorg иногда использует функцию stat()

$ strace -e open,stat xterm  # (через запятую - список нужны сискалов)

stat("/home/pavel/XTerm", {st_mode=S_IFREG|0600, st_size=333, ...}) = 0 
open("/home/pavel/XTerm", O_RDONLY)     = 4
open("/usr/share/X11/app-defaults/XTerm", O_RDONLY) = 4

Первый файл это мои настройки, второй — общесистемный.

Тут медаль о двух сторонах, независимо от наличия или отсутствия моего файла, мы получим два системных вызова, но один из них менее ресурсоёмкий — stat(). Получается, если у Вас нет каких-то специфичных настроек для xterm, то лучше выкинуть такие файлы из домашней папки. Это могу быть .Xdefaults-$(hostname), .Xdefaults, .terminfo (файлы со словами auth, лучше не трогать :))
Похожая, но отличная ситуация с курсорами мыши. Если приложение, а это почти все графические, используют библиотеку libXcursor, значит оно будет искать курсоры и иконки сначала в $HOME/.icons/ и затем в /usr/share/icons 

$ strace -e open,stat xterm 

open("/home/pavel/.icons/DMZ/cursors/top_left_arrow", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/pavel/.icons/DMZ/index.theme", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/icons/DMZ/cursors/top_left_arrow", O_RDONLY) = 5
open("/home/pavel/.icons/DMZ/cursors/sb_v_double_arrow", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/home/pavel/.icons/DMZ/index.theme", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/icons/DMZ/cursors/sb_v_double_arrow", O_RDONLY) = 5

Просто делаем ссылку с /usr/share/icons на $HOME/.icons и проверяем:

$ ln -s /usr/share/icons $HOME/.icons
$ strace -e open,stat xterm

open("/home/pavel/.icons/DMZ/cursors/top_left_arrow", O_RDONLY) = 5
open("/home/pavel/.icons/DMZ/cursors/sb_v_double_arrow", O_RDONLY) = 5

Великолепно, минус 4 опена()!

Далее добавим к strace флаг -f, для отслеживания дочерних процессов.

$ strace -f -e open,stat xterm

Вывод очень громоздкий, оставляю вам для изучения в качестве домашнего задания. Скажу лишь, что для многонитиевых приложений лучше делать вывод в файл или даже в отдельные файлы для каждой «дочки».

$ mkdir /tmp/Xterm (Отдельный каталог для чистоты феншуя)  
$ cd /tmp/Xterm
$ strace -o Xterm.trace -ff xterm 
$ ls 

Xterm.trace.6001  Xterm.trace.6009  Xterm.trace.6017 Xterm.trace.6025  Xterm.trace.6034  
Xterm.trace.6042  Xterm.trace.6050  Xterm.trace.6002  Xterm.trace.6010  Xterm.trace.6018  
Xterm.trace.6026  Xterm.trace.6035  Xterm.trace.6043  Xterm.trace.6051  Xterm.trace.6003  
Xterm.trace.6011  Xterm.trace.6019  Xterm.trace.6027  Xterm.trace.6036  Xterm.trace.6044  
Xterm.trace.6052  Xterm.trace.6004  Xterm.trace.6012  Xterm.trace.6020  Xterm.trace.6028  
...

изучайте :)

все сразу

$ egrep "open|stat" ./Xterm.trace.* | grep --color " = -"

или по одному

$ egrep "open|stat" ./Xterm.trace.6022 | grep --color " = -"

Даже сейчас, нашел, что кто-то упорно ищет libreadline.so.6

./Xterm.trace.28465:open("/usr/lib64/tls/x86_64/libreadline.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
./Xterm.trace.28465:open("/usr/lib64/tls/libreadline.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
./Xterm.trace.28465:open("/usr/lib64/x86_64/libreadline.so.6", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied)
./Xterm.trace.28465:open("/usr/lib64/libreadline.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

В данном случае косяк в том, что программеры указали явно (специально или через парсер) версию библиотеки. Вместо libreadline.so, захаркодили libreadline.so.6. Исправляем.

$ cd /usr/lib64/tls/x86_64/
# ln -s libreadline.so libreadline.so.6 
$ cd /tmp/Xterm && rm ./*;
$ strace -o Xterm.trace -ff xterm
$ egrep "open|stat" ./Xterm.trace.* | grep --color "libreadline" 
./Xterm.trace.21278:open("/usr/lib64/tls/x86_64/libreadline.so.6", O_RDONLY|O_CLOEXEC) = 3

Заключение

Для оптимизации KDE, GNOME, Xfce приложений не обязательно настраивать сами менеджеры gdm/kdm/xfwm4, достаточно любого приложения из набора к вашему десктопу, схема работы практически идентичная.

Займитесь в первую очередь всеми бинарниками запускаемыми при загрузке.

Таким способом время загрузки компьютера можно легко уменьшить до 10-15 сек. Время загрузки — это от загрузчика до открытия страницы в фейсбуке, а не появление обоины на экране, как думает Microsoft и Поттеринг.

Потом можно взяться за большие программы Google Chrome, Firefox, Gimp, LibreOffice, etc.

Возможные косяки

Как и в любом деле — важно знать меру, создание ссылок на каталоги может привести к «зацикленным каталогам» и вызов, скажем find ./, может устроить локальный DoS системе. (вроде в ядре уже исправили, но тем не менее ). Года два назад, на SuSE были проблемы при обновлении glibc. Дополнительно рекомендую почитать про TLS (Thread Local Storage)