Любой программист, решивший заняться разработкой под Embedded Linux, придя буть-то из высокоуровневых языков программирования, либо из программирования микроконтроллеров на С/С++, неизбежно оказывается удивлен крайней недружелюбностью embedded linux. Текстовый блокнот и консольные утилиты вместо столь привычных IDE, и отладка по логам вместо отладки программатором сильно замедляют процесс разработки. В статье описывается, как мне удалось снизить время доставки изменений до целевого железа при кросс-компиляции в 10 раз.
В этом случае, типовой процесс обновления программного кода на целевой платформе состоит из трех этапов:
На все три этапа (полная пересборка прошивки и перепрошивка железки) у нас уходило 150 минут. Наиболее редко применяемый сценарий.
Второй этап в случае повторной пересборки прошивки занимал 30 минут. В случае пересборки тяжеловесного приложения, применящего нейронки, - 13 минут.
Третий этап сам по себе (при обновлении прошивки) занимал минут 5: вначале прошивка копировалась с хоста на целевое железо утилитой scp. После чего производилось обновление прошивки утилитой sysupgrade.
В какой-то момент времени такое положение дел стало вымораживать, поскольку разработка выливалась в многочасовые потери времени на компиляцию. В итоге, нам удалось получить:
попутно приобретя видео-карту NVIDIA GeForce 3080TI для работы с нейронками, 128 Гб оперативной памяти и 1 Тб ssd диск. Закончил сборку системника уже глубокой ночи, установил Ubuntu и запустил сборку прошивки, естественно, не забыв про make -j64... Разочарованию не было предела, когда получил 30 минут, вместо обещанных бенчмарками 2 минут. Плюнул на все и лег спать.
С утра принялся выжимать все соки из дорогостоящей покупки. Первым делом запустил htop параллельно с процессом компиляции. Лишь изредка наблюдал подобную картину:
иногда такую:
но, чаще всего, такую:
Это говорило о том, что задействуются не все ядра. Ответ в интернете нашел быстро: Enable parallel builds by default?. Недолго думая закоммитил соответствующие изменения к себе в репу и получил заветные 3 минуты на пересборку прошивки и 1,5 минуты на пересборку приложения. htop показывал полную загрузку. Радости не было предела! Деньги не ушли на ветер!
Вдобавок к этому попробовал поиграться работой из RAMDISK:
$ sudo mkdir /mnt/ramdisk
$ sudo mount -t tmpfs -o rw,size=2G tmpfs /mnt/ramdisk
$ df -h
$ cd /mnt/ramdisk
$ git clone ...
но особого ускорения не заметил.
Единственный, омрачавший радость, нюанс, - сборка иногда, непредсказуемым образом падала на самым разных пакетах. Погуглив, выяснилось, что такое случается, - нестабильность при много ядерной работе.
Но этот кейс довольно легко закрыл костылем. В сборочном скрипте заменил:
make -j $(nproc);
на:
compiled_successful_flag=0;
compile() {
compiled_successful_flag=0;
if make -j $(nproc); then
compiled_successful_flag=1;
return
else
return
fi
}
...
while [ $compiled_successful_flag -eq 0 ];
do
compile;
sleep 1;
done
т.е., повторный запуск сборки при падении. Замедление в таких случаях было незначительных и даже при падениях удавалось собрать образ за 3-4 минуты.
Сразу же возникло желание включить и tftp-сервер и nfs-сервер в состав репозитория, чтобы отладив все один раз в будущем не тратить на это время при переезде на другой хост. И в этом помогли великие и могучие docker и docker-compose. Если вкратце, - то это волшебная пилюля, которая позволяет разработчику запускать свой софт на любой другой машине одним нажатием кнопки ($ docker-compose up -d) не тратя время на бесконечные ошибки, неизбежно возникающие при установке и запуске каких-либо программ в первый раз или на другом ПК.
Итак,
FROM debian:stretch-slim
MAINTAINER danrue drue@therub.org
# https://github.com/danrue/docker-tftpd-hpa
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tftpd-hpa && \
rm -rf /var/lib/apt/lists/*
CMD echo -n "Starting " && in.tftpd --version && in.tftpd -L --user tftp -a 0.0.0.0:69 -s -B1468 -v /srv/tftp
docker-compose.yml файл:
version: '3.4'
services:
tftpd-hpa:
container_name: tftp
build: .
volumes:
- ./volume:/srv/tftp
ports:
- 69:69/udp
restart: always
Запуск на хосте командой:
$ docker-compose up -d
После этого остается положить в каталог volume образ ядра (uImage). В моем случае это делается командой:
$ cp bin/targets/imx6ull/cortexa7/openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage _utilities/tftpd-hpa/volume/
Исходники можно найти тут.
Все готово для загрузки ядра загрузчиком (u-boot, в нашем случае). Но перед тем как переключиться в консоль целевой железки проверьте состояние фаерволла:
$ sudo ufw status
и если он включен, то временно отключите его:
$ sudo ufw disable
Итак, первым делом сохраняем текущие настройки загрузки:
=> setenv defbootcmd "$bootcmd"
=> saveenv
Saving Environment to SPI Flash...
board_spi_cs_gpio bus 2 cs 0
SF: Detected w25q256 with page size 256 Bytes, erase size 4 KiB, total 32 MiB
Erasing SPI flash...Writing to SPI flash...done
к которым вы в дальнейшем сможете вернуться в любой момент:
=> setenv bootcmd "$defbootcmd"
=> saveenv
Теперь задаем сетевые настройки, чтобы u-boot знал, куда стучаться за образом ядра:
=> setenv ipaddr 192.168.31.99
=> setenv ethaddr 86:72:04:c5:7e:83
=> setenv serverip 192.168.31.37
=> setenv uimage openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage
=> printenv loaddadr
где 192.168.31.99 - это IP-адрес, присваиваемый железке, а 192.168.31.37 - это IP-адрес хоста, на котором вы ранее запустили tftp-сервер. openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage - название файла (образ ядра, который вы ранее положили в каталог volume на хосте), который нужно скачать с tftp-сервера. loadaddr - адрес, по которому нужно его расположить для дальнейшей загрузки (у вас он, скорее всего уже задан).
Попробуйте пингануть сервер:
=> ping ${serverip}
Using FEC device
host 192.168.31.37 is alive
Теперь попробуйте скачать образ с сервера:
=> tftp ${loadaddr} ${serverip}:${uimage}
Using FEC device
TFTP from server 192.168.31.37; our IP address is 192.168.31.99
Filename 'openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage'.
Load address: 0x82000000
Loading: #################################################################
#################################################################
#########################################################
345.7 KiB/s
done
Bytes transferred = 2742194 (29d7b2 hex)
Отлично. Значит пока нигде не напартачили и можно сохранять настройки и запускать ядро:
=> saveenv
=> bootm ${loadaddr}
## Booting kernel from Legacy Image at 82000000 ...
Image Name: ARM OpenWrt Linux-4.14.199
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 2742130 Bytes = 2.6 MiB
Load Address: 80008000
Entry Point: 80008000
Verifying Checksum ... OK
Loading Kernel Image ... OKStarting kernel ...[ 0.004494] /cpus/cpu@0 missing clock-frequency property
[ 0.128440] imx6ul-pinctrl 2290000.iomuxc-snvs: no groups defined in /soc/aips-bus@02200000/iomuxc-snvs@02290000
[ 0.917942] fec 20b4000.ethernet: Invalid MAC address: 00:00:00:00:00:00
[ 1.800403] mxs-dcp 2280000.dcp: Failed to register sha1 hash!
[ 7.239994] DHCP/BOOTP: Reply not for us on eth1, op[2] xid[e2dbd4aa]
Press the [f] key and hit [enter] to enter failsafe mode
Press the [1], [2], [3] or [4] key and hit [enter] to select the debug level
Please press Enter to activate this console.
Теперь снова возвращайтесь в консоль U-boot и настройте автоматическую загрузку ядра с TFTP-сервера с последующей загрузкой:
=> setenv devbootcmd "tftp ${loadaddr} ${serverip}:${uimage}; bootm ${loadaddr}"
=> setenv bootcmd "$defbootcmd"
=> saveenv
Теперь u-boot должен будет автоматически грузить образ с сервера и стартовать с него. Файловая система, при этом, пока еще, будет использоваться дефолтная. Та, что у вашей железки во флеш-памяти/microSD-карте.
Для начала, аналогично tftp-серверу соберем и запустим в контейнере NFS-сервер.
Dockerfile NFS-сервера:
FROM alpine:3.6
MAINTAINER Tang Jiujun <jiujun.tang@gmail.com>
# https://github.com/tangjiujun/docker-nfs-server
RUN set -ex && { \
echo 'http://mirrors.aliyun.com/alpine/v3.6/main'; \
echo 'http://mirrors.aliyun.com/alpine/v3.6/community'; \
} > /etc/apk/repositories \
&& apk update && apk add bash nfs-utils && rm -rf /var/cache/apk/*
EXPOSE 111 111/udp 2049 2049/udp \
32765 32765/udp 32766 32766/udp 32767 32767/udp 32768 32768/udp
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh, используемые в Dockerfile:
#!/bin/bash
set -ex
: ${EXPORT_DIR:="/nfsshare"}
: ${EXPORT_OPTS:="*(rw,fsid=0,insecure,no_root_squash,no_subtree_check,sync)"}
mkdir -p $EXPORT_DIR
echo "$EXPORT_DIR $EXPORT_OPTS" > /etc/exports
mount -t nfsd nfsd /proc/fs/nfsd
# Fixed nlockmgr port
echo 'fs.nfs.nlm_tcpport=32768' >> /etc/sysctl.conf
echo 'fs.nfs.nlm_udpport=32768' >> /etc/sysctl.conf
sysctl -p > /dev/null
rpcbind -w
rpc.nfsd -N 2 -V 3 -N 4 -N 4.1 8
exportfs -arfv
rpc.statd -p 32765 -o 32766
rpc.mountd -N 2 -V 3 -N 4 -N 4.1 -p 32767 -F
и docker-compose.yml:
version: '3.4'
services:
nfs:
build: .
container_name: nfs
volumes:
- ./volume:/nfsshare
ports:
- 111:111
- 111:111/udp
- 2049:2049
- 2049:2049/udp
- 32765-32768:32765-32768
- 32765-32768:32765-32768/udp
privileged: true
restart: always
запускаем точно также как и TFTP-сервер:
$ docker-compose up -d
Программы разные (tftp-hpa и nfs-kernel-server), а запускаются одной и той же командой).
Осталось распаковать в каталог volume архив с файловой системой, который у вас должен был быть скомпилировать вместе с образом ядра. В моем случае:
$ tar -xzvf bin/targets/imx6ull/cortexa7/openwrt-imx6ull-cortexa7-device-tensorflow-wifi-dev-rootfs.tar.gz –directory _utilities/nfs/volume/
Протестировать сервер можно с другой машины в той же сети командой:
$ sudo mount -v -o vers=3 192.168.31.37:/nfsshare /home/al/mnt
mount.nfs: timeout set for Fri Aug 20 23:28:36 2021
mount.nfs: trying text-based options 'vers=3,addr=192.168.31.37'
mount.nfs: prog 100003, trying vers=3, prot=6
mount.nfs: trying 192.168.31.37 prog 100003 vers 3 prot TCP port 2049
mount.nfs: prog 100005, trying vers=3, prot=17
mount.nfs: trying 192.168.31.37 prog 100005 vers 3 prot UDP port 32767
$ ls mnt/
bin etc mnt overlay rom sbin tmp var
dev lib mobilenet_v1_0.25_128_quant.tflite proc root sys usr www
где 192.168.31.37 - IP-адрес машины, на которой вы запустили сервер, а /home/al/nfs - каталог, к которому вы примонтируете удаленную файловую систему.
make menuconfig
В открывшемся окне нужно найти опцию Compile the kernel with rootfs on NFS. В моем случае она расположена по пути: Global build settings-->Kernel build options-->Compile the kernel with rootfs on NFS:
Если видите значок *, значит вы можете загрузиться с NFS задав лишь дополнительные настройки в u-boot. Если нет (как было в моем случае), то вам потребуется:
=> printenv bootargs
=> setenv defbootargs "${bootargs}"
=> setenv devbootargs "ip=dhcp console=ttymxc1 rootwait rw root=/dev/nfs nfsroot=${serverip}:/nfsshare,nolock,v3,intr,hard,noacl"
=> setenv bootargs "${devbootargs}"
=> saveenv
Обратите внимание на вывод первой команды и запомните его.
После этого введите:
=> reset
U-boot перезагрузится, передаст управление ядру. После этого введите:
# dmesg | grep nfs
[ 0.000000] Kernel command line: console=ttymxc0,115200 rootwait fixrtc quiet ip=dhcp console=ttymxc1 rootwait rw root=/dev/nfs nfsroot=192.168.31.37:/nfsshare,nolock,v3,intr,hard,noacl
[ 4.175865] VFS: Mounted root (nfs filesystem) on device 0:10
Отлично! Сетевая файловая система примонтирована. Теперь любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Для проверки можете в каталоге volume NFS-сервера (на хосте) создать какой-нибудь файл:
$ touch nfs/volume/hello
Он должен сразу же отобразиться в файловой системе железки:
# ls /
=> setenv bootcmd "${devbootcmd}"
=> setenv bootargs "${devbootargs}"
=> saveenv
таки и "продакшн" режим загрузки:
=> setenv bootcmd "${defbootcmd}"
=> setenv bootargs "${defbootargs}"
=> saveenv
или:
=> setenv bootcmd "${defbootcmd}"
=> setenv bootargs
=> saveenv
если ранее => printenv bootargs вам ничего не выдало.
Даже прошив флешку утилитой sysupgrade, но с флагом -c вы сохраните переменные загрузчика U-boot и сохраните возможность в любой момент переключиться в отладочных режим. Можно пойти еще дальше и разработать отладочную и продуктовую конфигурации прошивки. Коммиты, реализующие данный функционал.
В чем же его преимущества? В том, что любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Дело осталось за малым написать скрипт, который будет копировать свеже-собранный образ ядра и распаковывать свеже-собранный архив файловой системы в соответствующие каталоги volume TFTP- и NFS-серверов. Таким образом, длительность третьего этапа (доставки изменений в коде) до целевой железки сокращается до 10 секунд в худшем случае (время перезагрузки ядра) при внесении изменений в ядро Линукс и до 0 секунд при внесении изменений в приложения пользовательского пространства!
Кросс-компиляция
Под кросс-компиляцией подразумевается процесс, когда сборка прошивки целевой железки (target device) под управлением Embedded Linux производится на удаленной машине (host machine), которой, как правило, является обычный ПК. С этим приходится сталкиваться при разработке кода под относительно слабые микропроцессоры с ограниченными объемами доступной оперативной и постоянной памяти (128 Мб и 32 Мб соответственно в нашем случае).В этом случае, типовой процесс обновления программного кода на целевой платформе состоит из трех этапов:
- доставка исходников программного кода до хоста (проводится лишь один раз при первой сборке прошивки). git clone в нашем случае.
- сборка всей прошивки целевой железки либо отдельного исполняемого файла на хосте. Утилитой make в нашем случае.
- доставка свеже-собранной прошивки/исполняемого файла до целевой платформы. Утилиты scp и sysupgrade в нашем случае.
На все три этапа (полная пересборка прошивки и перепрошивка железки) у нас уходило 150 минут. Наиболее редко применяемый сценарий.
Второй этап в случае повторной пересборки прошивки занимал 30 минут. В случае пересборки тяжеловесного приложения, применящего нейронки, - 13 минут.
Третий этап сам по себе (при обновлении прошивки) занимал минут 5: вначале прошивка копировалась с хоста на целевое железо утилитой scp. После чего производилось обновление прошивки утилитой sysupgrade.
В какой-то момент времени такое положение дел стало вымораживать, поскольку разработка выливалась в многочасовые потери времени на компиляцию. В итоге, нам удалось получить:
- 16 минут вместо 150 минут для всех трех этапов вместе при первой сборке прошивки;
- 3 минуты вместо 30 минут для второго этапа в случае пересборки всей прошивки и 1,5 минуты вместо 13 минут при перекомпиляции отдельного приложения.
- 10 секунд вместо 5 минут для третьего этапа самого по себе.
Ускоряем процесс кросс-компиляции
Долго сомневались, будет ли смысл, но все же пошли ва-банк, начитавшись статей о вдохновляющих бенчмарках, и раскошелились на рабочую станцию на базе процессора AMD Threadripper 3970X (64 ядра):попутно приобретя видео-карту NVIDIA GeForce 3080TI для работы с нейронками, 128 Гб оперативной памяти и 1 Тб ssd диск. Закончил сборку системника уже глубокой ночи, установил Ubuntu и запустил сборку прошивки, естественно, не забыв про make -j64... Разочарованию не было предела, когда получил 30 минут, вместо обещанных бенчмарками 2 минут. Плюнул на все и лег спать.
С утра принялся выжимать все соки из дорогостоящей покупки. Первым делом запустил htop параллельно с процессом компиляции. Лишь изредка наблюдал подобную картину:
иногда такую:
но, чаще всего, такую:
Это говорило о том, что задействуются не все ядра. Ответ в интернете нашел быстро: Enable parallel builds by default?. Недолго думая закоммитил соответствующие изменения к себе в репу и получил заветные 3 минуты на пересборку прошивки и 1,5 минуты на пересборку приложения. htop показывал полную загрузку. Радости не было предела! Деньги не ушли на ветер!
Вдобавок к этому попробовал поиграться работой из RAMDISK:
$ sudo mkdir /mnt/ramdisk
$ sudo mount -t tmpfs -o rw,size=2G tmpfs /mnt/ramdisk
$ df -h
$ cd /mnt/ramdisk
$ git clone ...
но особого ускорения не заметил.
Единственный, омрачавший радость, нюанс, - сборка иногда, непредсказуемым образом падала на самым разных пакетах. Погуглив, выяснилось, что такое случается, - нестабильность при много ядерной работе.
Но этот кейс довольно легко закрыл костылем. В сборочном скрипте заменил:
make -j $(nproc);
на:
compiled_successful_flag=0;
compile() {
compiled_successful_flag=0;
if make -j $(nproc); then
compiled_successful_flag=1;
return
else
return
fi
}
...
while [ $compiled_successful_flag -eq 0 ];
do
compile;
sleep 1;
done
т.е., повторный запуск сборки при падении. Замедление в таких случаях было незначительных и даже при падениях удавалось собрать образ за 3-4 минуты.
Ускоряем процесс обновления прошивки целевой железки
Погуляв на просторах интернета, узнал, что вместо копирования прошивки утилитой scp и ее обновления утилитой sysupgrade можно грузить и ядро, и файловую систему по сети, с помошью tftp и nfs.Сразу же возникло желание включить и tftp-сервер и nfs-сервер в состав репозитория, чтобы отладив все один раз в будущем не тратить на это время при переезде на другой хост. И в этом помогли великие и могучие docker и docker-compose. Если вкратце, - то это волшебная пилюля, которая позволяет разработчику запускать свой софт на любой другой машине одним нажатием кнопки ($ docker-compose up -d) не тратя время на бесконечные ошибки, неизбежно возникающие при установке и запуске каких-либо программ в первый раз или на другом ПК.
Итак,
Запуск TFTP сервера для загрузки ядра
Его Dockerfile:FROM debian:stretch-slim
MAINTAINER danrue drue@therub.org
# https://github.com/danrue/docker-tftpd-hpa
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tftpd-hpa && \
rm -rf /var/lib/apt/lists/*
CMD echo -n "Starting " && in.tftpd --version && in.tftpd -L --user tftp -a 0.0.0.0:69 -s -B1468 -v /srv/tftp
docker-compose.yml файл:
version: '3.4'
services:
tftpd-hpa:
container_name: tftp
build: .
volumes:
- ./volume:/srv/tftp
ports:
- 69:69/udp
restart: always
Запуск на хосте командой:
$ docker-compose up -d
После этого остается положить в каталог volume образ ядра (uImage). В моем случае это делается командой:
$ cp bin/targets/imx6ull/cortexa7/openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage _utilities/tftpd-hpa/volume/
Исходники можно найти тут.
Все готово для загрузки ядра загрузчиком (u-boot, в нашем случае). Но перед тем как переключиться в консоль целевой железки проверьте состояние фаерволла:
$ sudo ufw status
и если он включен, то временно отключите его:
$ sudo ufw disable
Загрузка ядра с tftp-сервера
Ядро грузит загрузчик. В случае, если у вас это u-boot, то он, скорее всего умеет это делать из коробки. Для этого, в консоли вашей целевой железки введите reboot. Дождитесь перезагрузки и, увидев обратный отсчет таймера, нажмите Enter. Если увидите значок стрелочки: =>, то вы в консоли U-boot.Итак, первым делом сохраняем текущие настройки загрузки:
=> setenv defbootcmd "$bootcmd"
=> saveenv
Saving Environment to SPI Flash...
board_spi_cs_gpio bus 2 cs 0
SF: Detected w25q256 with page size 256 Bytes, erase size 4 KiB, total 32 MiB
Erasing SPI flash...Writing to SPI flash...done
к которым вы в дальнейшем сможете вернуться в любой момент:
=> setenv bootcmd "$defbootcmd"
=> saveenv
Теперь задаем сетевые настройки, чтобы u-boot знал, куда стучаться за образом ядра:
=> setenv ipaddr 192.168.31.99
=> setenv ethaddr 86:72:04:c5:7e:83
=> setenv serverip 192.168.31.37
=> setenv uimage openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage
=> printenv loaddadr
где 192.168.31.99 - это IP-адрес, присваиваемый железке, а 192.168.31.37 - это IP-адрес хоста, на котором вы ранее запустили tftp-сервер. openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage - название файла (образ ядра, который вы ранее положили в каталог volume на хосте), который нужно скачать с tftp-сервера. loadaddr - адрес, по которому нужно его расположить для дальнейшей загрузки (у вас он, скорее всего уже задан).
Попробуйте пингануть сервер:
=> ping ${serverip}
Using FEC device
host 192.168.31.37 is alive
Теперь попробуйте скачать образ с сервера:
=> tftp ${loadaddr} ${serverip}:${uimage}
Using FEC device
TFTP from server 192.168.31.37; our IP address is 192.168.31.99
Filename 'openwrt-imx6ull-cortexa7-tensorflow_wifi_dev-uImage'.
Load address: 0x82000000
Loading: #################################################################
#################################################################
#########################################################
345.7 KiB/s
done
Bytes transferred = 2742194 (29d7b2 hex)
Отлично. Значит пока нигде не напартачили и можно сохранять настройки и запускать ядро:
=> saveenv
=> bootm ${loadaddr}
## Booting kernel from Legacy Image at 82000000 ...
Image Name: ARM OpenWrt Linux-4.14.199
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 2742130 Bytes = 2.6 MiB
Load Address: 80008000
Entry Point: 80008000
Verifying Checksum ... OK
Loading Kernel Image ... OKStarting kernel ...[ 0.004494] /cpus/cpu@0 missing clock-frequency property
[ 0.128440] imx6ul-pinctrl 2290000.iomuxc-snvs: no groups defined in /soc/aips-bus@02200000/iomuxc-snvs@02290000
[ 0.917942] fec 20b4000.ethernet: Invalid MAC address: 00:00:00:00:00:00
[ 1.800403] mxs-dcp 2280000.dcp: Failed to register sha1 hash!
[ 7.239994] DHCP/BOOTP: Reply not for us on eth1, op[2] xid[e2dbd4aa]
Press the [f] key and hit [enter] to enter failsafe mode
Press the [1], [2], [3] or [4] key and hit [enter] to select the debug level
Please press Enter to activate this console.
Теперь снова возвращайтесь в консоль U-boot и настройте автоматическую загрузку ядра с TFTP-сервера с последующей загрузкой:
=> setenv devbootcmd "tftp ${loadaddr} ${serverip}:${uimage}; bootm ${loadaddr}"
=> setenv bootcmd "$defbootcmd"
=> saveenv
Теперь u-boot должен будет автоматически грузить образ с сервера и стартовать с него. Файловая система, при этом, пока еще, будет использоваться дефолтная. Та, что у вашей железки во флеш-памяти/microSD-карте.
Запуск NFS-сервера для загрузки файловой системы
Следующий шаг - научиться монтировать сетевую файловую систему.Для начала, аналогично tftp-серверу соберем и запустим в контейнере NFS-сервер.
Dockerfile NFS-сервера:
FROM alpine:3.6
MAINTAINER Tang Jiujun <jiujun.tang@gmail.com>
# https://github.com/tangjiujun/docker-nfs-server
RUN set -ex && { \
echo 'http://mirrors.aliyun.com/alpine/v3.6/main'; \
echo 'http://mirrors.aliyun.com/alpine/v3.6/community'; \
} > /etc/apk/repositories \
&& apk update && apk add bash nfs-utils && rm -rf /var/cache/apk/*
EXPOSE 111 111/udp 2049 2049/udp \
32765 32765/udp 32766 32766/udp 32767 32767/udp 32768 32768/udp
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh, используемые в Dockerfile:
#!/bin/bash
set -ex
: ${EXPORT_DIR:="/nfsshare"}
: ${EXPORT_OPTS:="*(rw,fsid=0,insecure,no_root_squash,no_subtree_check,sync)"}
mkdir -p $EXPORT_DIR
echo "$EXPORT_DIR $EXPORT_OPTS" > /etc/exports
mount -t nfsd nfsd /proc/fs/nfsd
# Fixed nlockmgr port
echo 'fs.nfs.nlm_tcpport=32768' >> /etc/sysctl.conf
echo 'fs.nfs.nlm_udpport=32768' >> /etc/sysctl.conf
sysctl -p > /dev/null
rpcbind -w
rpc.nfsd -N 2 -V 3 -N 4 -N 4.1 8
exportfs -arfv
rpc.statd -p 32765 -o 32766
rpc.mountd -N 2 -V 3 -N 4 -N 4.1 -p 32767 -F
и docker-compose.yml:
version: '3.4'
services:
nfs:
build: .
container_name: nfs
volumes:
- ./volume:/nfsshare
ports:
- 111:111
- 111:111/udp
- 2049:2049
- 2049:2049/udp
- 32765-32768:32765-32768
- 32765-32768:32765-32768/udp
privileged: true
restart: always
запускаем точно также как и TFTP-сервер:
$ docker-compose up -d
Программы разные (tftp-hpa и nfs-kernel-server), а запускаются одной и той же командой).
Осталось распаковать в каталог volume архив с файловой системой, который у вас должен был быть скомпилировать вместе с образом ядра. В моем случае:
$ tar -xzvf bin/targets/imx6ull/cortexa7/openwrt-imx6ull-cortexa7-device-tensorflow-wifi-dev-rootfs.tar.gz –directory _utilities/nfs/volume/
Протестировать сервер можно с другой машины в той же сети командой:
$ sudo mount -v -o vers=3 192.168.31.37:/nfsshare /home/al/mnt
mount.nfs: timeout set for Fri Aug 20 23:28:36 2021
mount.nfs: trying text-based options 'vers=3,addr=192.168.31.37'
mount.nfs: prog 100003, trying vers=3, prot=6
mount.nfs: trying 192.168.31.37 prog 100003 vers 3 prot TCP port 2049
mount.nfs: prog 100005, trying vers=3, prot=17
mount.nfs: trying 192.168.31.37 prog 100005 vers 3 prot UDP port 32767
$ ls mnt/
bin etc mnt overlay rom sbin tmp var
dev lib mobilenet_v1_0.25_128_quant.tflite proc root sys usr www
где 192.168.31.37 - IP-адрес машины, на которой вы запустили сервер, а /home/al/nfs - каталог, к которому вы примонтируете удаленную файловую систему.
Загрузка с NFS-сервера
Теперь пришло время загрузиться с NFS-сервера. Первым делом необходимо выяснить, включена ли в вашем дистрибутиве поддержка NFS. Для этого, на хосте, в корне репозитория с вашими исходниками Linux нужно ввести:make menuconfig
В открывшемся окне нужно найти опцию Compile the kernel with rootfs on NFS. В моем случае она расположена по пути: Global build settings-->Kernel build options-->Compile the kernel with rootfs on NFS:
Если видите значок *, значит вы можете загрузиться с NFS задав лишь дополнительные настройки в u-boot. Если нет (как было в моем случае), то вам потребуется:
- включить данную опцию, нажав кнопку Y
- выйдите из конфигуратора, сохранив изменения.
- пересоберите образ ядра по новой и сохраните результирующий uImage файл в каталоге volume вашего TFTP-сервера.
- перезагрузите u-boot на всякий случай, если вдруг вы уже успели загрузить образ ядра командой tftp
=> printenv bootargs
=> setenv defbootargs "${bootargs}"
=> setenv devbootargs "ip=dhcp console=ttymxc1 rootwait rw root=/dev/nfs nfsroot=${serverip}:/nfsshare,nolock,v3,intr,hard,noacl"
=> setenv bootargs "${devbootargs}"
=> saveenv
Обратите внимание на вывод первой команды и запомните его.
После этого введите:
=> reset
U-boot перезагрузится, передаст управление ядру. После этого введите:
# dmesg | grep nfs
[ 0.000000] Kernel command line: console=ttymxc0,115200 rootwait fixrtc quiet ip=dhcp console=ttymxc1 rootwait rw root=/dev/nfs nfsroot=192.168.31.37:/nfsshare,nolock,v3,intr,hard,noacl
[ 4.175865] VFS: Mounted root (nfs filesystem) on device 0:10
Отлично! Сетевая файловая система примонтирована. Теперь любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Для проверки можете в каталоге volume NFS-сервера (на хосте) создать какой-нибудь файл:
$ touch nfs/volume/hello
Он должен сразу же отобразиться в файловой системе железки:
# ls /
Development и production режимы загрузки
В U-boot вы теперь можете любой момент включать отладочный (сетевой) режим загрузки:=> setenv bootcmd "${devbootcmd}"
=> setenv bootargs "${devbootargs}"
=> saveenv
таки и "продакшн" режим загрузки:
=> setenv bootcmd "${defbootcmd}"
=> setenv bootargs "${defbootargs}"
=> saveenv
или:
=> setenv bootcmd "${defbootcmd}"
=> setenv bootargs
=> saveenv
если ранее => printenv bootargs вам ничего не выдало.
Даже прошив флешку утилитой sysupgrade, но с флагом -c вы сохраните переменные загрузчика U-boot и сохраните возможность в любой момент переключиться в отладочных режим. Можно пойти еще дальше и разработать отладочную и продуктовую конфигурации прошивки. Коммиты, реализующие данный функционал.
В чем же его преимущества? В том, что любое изменение на хосте в каталоге volume TFTP-сервера будет моментально отображаться в файловой системе целевой железки. Дело осталось за малым написать скрипт, который будет копировать свеже-собранный образ ядра и распаковывать свеже-собранный архив файловой системы в соответствующие каталоги volume TFTP- и NFS-серверов. Таким образом, длительность третьего этапа (доставки изменений в коде) до целевой железки сокращается до 10 секунд в худшем случае (время перезагрузки ядра) при внесении изменений в ядро Линукс и до 0 секунд при внесении изменений в приложения пользовательского пространства!
Итоги
В общем, длительность наиболее типового сценария разработки кода (второй + третий этап): пересборки ядра либо пользовательского приложения с последующей доставкой на целевое железо удалось сократь с 30+ минут до 2-3 минут! Это дало возможность тестировать порядка 20 изменений в коде на целевой платформе, вместо 1-2 изменений ранее. Скорость разработки возрасла на порядок! Даже не верится, что раньше я мирился с такими потерями времени!Ускорение процесса разработки под Embedded Linux
Любой программист, решивший заняться разработкой под Embedded Linux, придя будь то из высокоуровневых языков программирования, либо из программирования микроконтроллеров на С/С++, неизбежно...
habr.com