Библиотека сайта rus-linux.net
Минимизация системы
Оригинал: "System Minimization"
Автор: Gene Sally
Дата: 2 января 2008
Перевод: Александр Тарасов aka oioki
Дата перевода: 27 июня 2009
"Насколько сильно его можно сжать?" - такой вопрос часто задают разработчику встраиваемых систем в начале работы над проектом. В большинстве случаев клиент подразумевает, до какой степени можно уменьшить затраты оперативной и Flash-памяти в проектируемом устройстве, с целью уменьшения его стоимости и снижения энергозатрат.
Изначально операционная система Linux и ее соответствующее окружение были предназначены для настольных и серверных систем, поэтому в обычной конфигурации она не оптимизирована по объему занимаемой памяти. Однако когда Linux начал проникать во встраиваемые системы, задача сделать Linux "маленьким" стала весьма актуальной. Есть несколько подходов к уменьшению расхода памяти системы.
Многие разработчики начинают уменьшать размер ядра; но есть также и более простые способы. В этой статье будут подробно описаны подходы к уменьшению размера ядра за счет удаления кода, неиспользуемого во встраиваемых системах.
Пожалуй, больше всего памяти отъедает корневая файловая система (root filesystem, RFS). Корневая файловая система содержит код инфраструктуры, используемой приложениями и библиотекой C. Тип файловой системы RFS накладывает значительный отпечаток на итоговый размер системы. Стандартная файловая система типа ext3 страшно неэффективна при использовании во встраиваемых системах, но это уже тема для отдельной статьи.
На самом деле, насколько?
Даже в самом маленьком дистрибутиве Linux есть как минимум две части - ядро и корневая файловая система. Иногда эти компоненты размещают в одном и том же файле, но тем не менее они логически разделены и являются отдельными компонентами. Если убрать из ядра почти все функции (поддержка сети, журналирование ошибок и драйверы большинства устройств) и сделать корневую файловую систему простым приложением, размер системы можно с легкостью сократить до 1 Мб. Однако, многие пользователи выбирают Linux из-за широких возможностей работы с сетью и множеством устройств, поэтому это не совсем реалистичный сценарий.
Ядро
Ядро Linux интересно тем, что хотя оно зависит от GCC на стадии компиляции, но не имеет зависимостей во время исполнения. Инженерам, которые являются новичками в Linux, обычно непонятна концепция начального RAM-диска (так называемого initrd) с зависимостью от ядра времени выполнения. Сначала ядром монтируется initrd, и запускается программа, которая общается с системой и выясняет, какие модули нужно загрузить, чтобы заработали все устройства. Затем монтируется "настоящая" файловая система. На самом деле, такое двухуровневое монтирование, когда за initrd следует загрузка основной файловой системы, редко находит применение во встраиваемых системах, так как увеличение гибкости в системе, которая в течение своей жизни не будет изменяться, совершенно не стоит дополнительных расходов памяти и времени. Эта тема относится к корневой файловой системе и будет раскрыта далее.
Наибольший эффект в уменьшении размера ядра дает удаление ненужного кода. Обычно ядро настраивается для настольных и серверных систем, поэтому по умолчанию в нем включено множество функций, которые не понадобятся во встраиваемых системах.
Поддержка загружаемых модулей
Загружаемые модули ядра - это перемещаемый код, который ядро связывает с собой во время исполнения. Обычно это используется для загрузки драйверов устройств в ядро из пространства пользователя (как правило, после некоторых системных проверок), и позволяет обновлять драйвера устройств без перезагрузки всей системы. В большинстве встраиваемых систем смена корневой файловой системы непрактична либо невозможна, поэтому разработчик сразу связывает модули с ядром, таким образом необходимость в загружаемых модулях отпадает. Помимо кода поддержки загружаемых модулей в ядре, мы также можем сэкономить место и на пользовательских программах для загрузки модулей (insmod, rmmod и lsmod).
Патчи Linux-tiny
Набор патчей Linux-tiny - это периодически развивающийся проект, поддерживаемый Мэттом Макаллом (Matt Mackall). Consumer Electronics Linux Forum (CELF) приложило определенные усилия к возрождению проекта, и на момент написания статьи на вики CELF Developer были размещены патчи для ядра 2.6.22.5. В то же время, многие изменения из проекта Linux-tiny перетекли в основную ветку ядра. Итак, в самом наборе Linux-tiny есть следующие экономичные патчи:
- Тонкая настройка функции printk (текстовый вывод ядра): пользователь может указывать, какие файлы могут использовать printk. Таким образом, инженеры могут уменьшить размер системы за счет использования функции printk лишь в самых важных случаях, когда она действительно нужна.
- Вычисление CRC заменено на lookup-таблицы. CRC используется при проверке целостности пакетов Ethernet. Данная реализация алгоритма CRC использует lookup-таблицы вместо вычислений, что позволяет сэкономить 2Кб памяти.
- Настройка сети: несколько патчей, уменьшающих число поддерживаемых протоколов, изменяющих размеры буферов и открытых сокетов. Многие встраиваемые системы поддерживают малую часть протоколов, а также не требуется обработка тысяч подключений.
- Нет panic-отчетов: если у устройства есть три лампочки и один информационный провод, становится понятно, что panic-информация, которая отображается на (несуществующей) консоли, не нужна. Когда устройство действительно даст сбой, пользователь сразу об этом узнает и ему всего лишь надо будет перезагрузить устройство по питанию.
- Сокращение inline-функций: когда компилятор встречает inline-функцию, он не генерирует вызов этой функции, а интерпретирует ее как макрос, копируя код в то место, где эта функция вызывается. Хотя технически директива inline является лишь подсказкой, GCC по умолчанию поступает прямо, а именно включает inline-функцию как макрос. При подавлении же inline-функций код становится медленнее, так как компилятор генерирует вызов функции и возврат из нее; однако объектный файл в этом случае становится меньше.
Патчи Linux-tiny распространяются в tar-архиве и могут быть применены с помощью утилиты quilt, либо вручную.
Дополнительные рекомендации по настройке ядра
Проект Linux-tiny дает значительный выигрыш в объеме занимаемой памяти, но все равно можно еще уменьшить размер системы:
- Удалить поддержку файловых систем ext2/3 и использовать другую файловую систему: код поддержки ext2/3 занимает чуть больше 32 Кб. Многие инженеры используют файловую систему Flash, но не выключают ext2/3, таким образом бессмысленно тратя бесценное место в памяти.
- Удалить поддержку sysctl: sysctl позволяет пользователю настраивать параметры ядра в процессе работы системы. В большинстве встраиваемых систем ядро конфигурации заранее известно и не меняется. Эта функция занимает 1 Кб.
- Сократить опции IPC: большинству систем не нужны функции SysV IPC features (grep-фильтрация кода для msgget, msgct, msgsnd и msgrcv) и очереди сообщений POSIX (grep-фильтрация для mq_*[a-z]), их удаление сэкономит еще 18 Кб.
Как увидеть результат
Команда size печатает объем кода и данных в объектном файле. Это не то, что выводит ls (ls печатает количество байт, занимаемых файлом в файловой системе).
К примеру, для ядра, скомпилированного кросс-компилятором armv5l, объемы следующие:
# armv5l-linux-size vmlinx text data bss dec hex filename 2080300 99904 99312 2279516 22c85c vmlinux
Секция text - это исполняемый код, генерируемый компилятором (название text имеет исторические корни). Секция data содержит значения глобальных переменных и другие значения, используемые для инициализации статических символов. Секция bss содержит статические данные, которые зануляются в процессе инициализации.
Какие-то сведения есть, но не показано, сколько памяти занимает каждая часть системы. Получить более подробные сведения из файла ядра vmlinux невозможно. Но мы можем взглянуть на файлы, из которых это ядро собирается. Чтобы получить информацию о размерах компонентов системы, нужно найти все файлы built-in.o, содержащиеся в проекте ядра, и передать их все программе size:
# find . -name "built-in.o" | xargs armv5l-linux-size --totals | sort -n -k4
У меня вывод данной команды такой:
text data bss dec hex filename 189680 16224 33944 239848 3a8e8 ./kernel/built-in.o 257872 10056 5636 273564 42c9c ./net/ipv4/built-in.o 369396 9184 34824 413404 64edc ./fs/built-in.o 452116 15820 11632 479568 75150 ./net/built-in.o 484276 36744 14216 535236 82ac4 ./drivers/built-in.o 3110478 180000 159241 3449719 34a377 (TOTALS)
Таким образом, можно сразу увидеть, код какого компонента занимает больше всего места, и уже целенаправленно работать именно с этим компонентом, думать, какие функции можно убрать, а какие оставить. При таком подходе инженер не должен забывать делать make clean между сборками, ведь удаление функции из ядра не приводет к тому, что будет удален объектный файл из прошлой сборки.
У тех, кто в первый раз собирает ядро Linux, может возникнуть вопрос, как соединить файл built-in.o с опцией в программе конфигурирования ядра. Взгляните на файлы Makefile и Kconfig. Makefile содержит строку следующего вида:
obj-$(CONFIG_ATALK) += p8022.o psnap.o
Эта строка приводит к тому, что объектные файлы справа от знака равенства будут собраны, когда включена переменная опции CONFIG_ATALK. Однако, программа конфигурирования ядра обычно не показывает, какая переменная ассоциирована с данной опцией. Чтобы найти связь между именем переменной и опцией, возьмите имя переменной, уберите CONFIG_, и поищите оставшееся в файлах Kconfig (именно они используются редактором конфигурации ядра):
find . -name Kconfig -exec fgrep -H -C3 "config ATALK" {} \;
Команда дает следующий вывод:
./drivers/net/appletalk/Kconfig-# ./drivers/net/appletalk/Kconfig-# Appletalk driver configuration ./drivers/net/appletalk/Kconfig-# ./drivers/net/appletalk/Kconfig:config ATALK ./drivers/net/appletalk/Kconfig- tristate "Appletalk protocol support" ./drivers/net/appletalk/Kconfig- select LLC ./drivers/net/appletalk/Kconfig- ---help---
Еще правда нужно поискать, где в меню находится это самое "Appletalk protocol support", но общий порядок поиска, думаю, ясен.
Корневая файловая система
Для многих разработчиков встраиваемых систем, только начавших работать с Linux, корневая файловая система на встраиваемом устройстве - это незнакомое понятие. До Linux во встраиваемых системах стандартным решением было включение кода приложения в ядро. В Linux же присутствует явное разделение ядра и корневой файловой системы, поэтому работа над уменьшением системы не заканчивается на уменьшении размеров ядра. В общем, есть множество приемов по уменьшению размера этого компонента.
Первый вопрос, которым нужно задаться: "Нужна ли мне вообще файловая система?" В большинстве случаев, да. В конце процесса загрузки ядра идет поиск корневой файловой системы, она монтируется и запускается первый процесс (обычно это init; чтобы узнать какой, скомандуйте на своей системе ps aux | head -2
). Если отсутствует корневая файловая система или начальная программа, ядро паникует и перестает работать.
Наименьшей корневой файловой системой может быть один файл - приложение для устройства. В этом случае параметр ядра init указывает на файл, и это будет первый (и единственный) процесс в пространстве пользователя. Пока этот процесс запущен, система будет работать нормально. Однако если по какой-либо причине эта программа завершится, ядро будет паниковать и перестанет работать, а устройству для восстановления работоспособности потребуется перезапуск. Уже по одной этой причине даже системы с существенно ограниченным объемом памяти выбирают вариант с программой init. При небольших накладных расходах это обеспечивает перезапуск умершего процесса, таким образом предотвращаю панику ядра в случае краха приложения.
Все равно, большинство Linux-систем куда более сложные, содержат несколько исполняемых файлов и зачастую разделяемые библиотеки, общие для нескольких приложений. Для таких файловых систем существует множество способов существенно сократить размер RFS.
Смена библиотеки C
Стандартная библиотека C, как правило, распространяется вместе с компилятором GCC, поэтому пользователи обычно не отделяют их друг от друга. Тем не менее это отдельные компоненты. Сам по себе язык C состоит из 32 ключевых слов (плюс-минус несколько), поэтому основную часть кода в программе C все-таки занимают вызовы функций стандартной библиотеки. Обычная GNU-реализация стандартной библиотеки C - glibc - спроектирована с расчетом на совместимость и интернационализацию, и итоговый размер разработчиков не столь волнует. В то же время существует несколько альтернатив, создаваемых с прицелом на экономию занимаемой памяти:
- uClibc: этот проект начинался как реализация библиотеки C для процессоров без модуля управления памятью (memory management unit, MMU-less). С самого начала библиотека была миниатюрной, но в то же время предоставляла функциональность, сходную с glibs. Был отброшен код, отвечающий за интернационализацию, поддержку многобайтовых кодировок и бинарную совместимость. Более того, утилита конфигурации uClibc дает пользователю свободу выбора, какой код следует включать в библиотеку, а какой можно исключить, тем самым сэкономив на размере.
- uClibc++: стандартная библиотека C++, построенная по аналогичным принципам. С поддержкой большинства функций стандартной библиотеки C++, инженеры могут с легкостью создавать C++-приложения, располагая лишь несколькими мегабайтами памяти.
- Newlib: Newlib выросла при выходе Red Hat на рынок встраиваемых устройств. Newlib содержит очень полную реализацию математической библиотеки, и поэтому рекомендуется для использования в измерительных устройствах и устройствах управления.
- dietlibc: из перечисленных реализаций эта самая миниатюрная - занимает всего 70 Кб, за счет исключения поддержки множества функций, к примеру, поддержки динамически связываемых библиотек. Имеет замечательную поддержку архитектур ARM и MIPS.
Использование альтернативной библиотеки C
Как Newlib, так и dietlibc работают через скрипт-обертку, который вызывает компилятор с требуемым набором параметров. Благодаря этому игнорируется стандартная реализация библиотеки C, входящая в комплект компилятора, и вместо нее используется альтернативная реализация. uClibc немного отличается от этих реализаций, используемый для работы с ней инструментарий нужно собрать отдельно.
Теперь вы знаете, как вызывать GCC, чтобы использовался нужный компилятор. Следующий шаг - нужно подправить файлы Makefile или скрипты сборки проекта. В большинстве случаев, команды сборки содержатся в Makefile, а именно в строке следующего вида:
CC=CROSS_COMPILE-gcc
В этом случаев, все, что нужно сделать инженеру - это запустить make с измененным аргументом CC, примерно следующим образом:
make CC=dietc
Теперь для компиляции программы будет использована библиотека diet в связке с компилятором C. Если нужно будет добавить какие-либо параметры компиляции, нужно вставлять их не в эту команду, а в переменную CFLAGS. Например, вместо
make CC="gcc -Os"
нужно писать:
make CC=gcc CFLAGS="-Os"
Это важно, ведь в некоторых случаях CC вызывается не для компиляции, и эти аргументы не будут иметь смысла, произойдет ошибка.
Вернемся к корневой файловой системе
После выбора альтернативной библиотеки C, нужно перекомпилировать весь код в корневой файловой системе. На данном этапе будет важно оценить, что предпочтительнее - статические приложения либо разделяемые библиотеки. Разделяемые библиотеки нужно выбирать, когда устройство будет выполнять произвольный код, неизвестный на момент создания устройства; к примеру, у устройства будет какое-либо API, и для него, как задумывается, другие инженеры будут писать модули. В этом случае нужно иметь библиотеки на устройстве, таким образом для других разработчиков будет предоставляться гибкость в реализации новых функций.
Разделяемые библиотеки - хороший выбор в случае, когда в файловой системе устройства хранится множество отдельных программ, а не одна или две. Одна копия разделяемого кода будет занимать меньше места, чем две копии кода в разных файлах в случае статической сборки.
Системы с небольшим количеством программ заслуживают более подробного рассмотрения. Когда используется лишь несколько программ, лучше всего создать два варианта системы и сравнить получившийся результат. В большинстве случаев, меньшей системой окажется система без разделяемых библиотек. Дополнительным плюсом статического подхода является то, что программы будут запускаться быстрее (так как здесь нет стадии связывания).
Заключение
Хотя и не существует волшебного способа сделать систему меньше, но в то же время не наблюдается дефицита инструментария для достижения этой цели. Более того, сделать Linux меньше - это не только ужать ядро; также необходимо уменьшать размер корневой файловой системы. Нужно критически рассмотреть все ее компоненты и исключить все ненужное. В данной статье было рассмотрено, как уменьшить размер образа системы. Как уменьшить потребление памяти программами в процессе работы устройства - это тема для отдельной статьи.
Источники информации
Linux-tiny Patches: www.selenic.com/linux-tiny. Набор маленьких патчей ядра для уменьшения размера образа и потребления ресурсов во время работы ядра. Многие из этих патчей уже были приняты в основную ветку ядра.
GNU C Library: www.gnu.org/software/libc. Стандартная библиотека C от GNU - это каноническая реализация библиотеки C, идущая в комплекте с подавляющим большинством дистрибутивов. Она требуется практически на каждой машине.
uClibc: www.uclibc.org. Маленькая реализация библиотеки C с хорошей поддержкой сообщества.
Newlib: sourceware.org/newlib. Маленькая реализация библиотеки C от Red Hat.
dietlibc: www.fefe.de/dietlibc. Самая маленькая реализация из рассматриваемых. Хорошо работает с существующим кросс-компилятором, так как установка проводится через "обертку" для GCC, с помощью которых компилятор вызывается с нужным набором аргументов.
Gene Sally последние 7 лет работает в области встраиваемых устройств на Linux, является совладельцем LinuxLink Radio - самого популярного подкаста по встраиваемым устройствам на Linux. Gene можно написать на адрес gene.sally@timesys.com.