Для Windows драйвер писать в QtC никто в здравом уме и трезвой памяти не будет. Поэтому речь дальше по процессу разработки драйвера для Linux. Я не буду касаться вопросов использования отладчика (KGDB), в основном посмотрим на вопросы запуска модуля ядра на удалённой системе.
Итак, не открою секрета, что ядро Linux является хоть и модульным, но монолитом. И если вы где-то напишите утечку или страшными темпами начнёте затирать чужую память, то систему можно легко и непринуждённо привести в состояние фарша. Логично, что при таких радужных перспективах, проверять драйвер на своей рабочей машине, где находится среда разработки - не совсем правильное решение.
Правильным решением же будет использовать какого-нибудь qemu, virtualbox или иже с ними. Но не всегда можно отладить модуль в виртуальном окружении, особенно при работе с реальным железом (можно так его запрограммировать, что убить много чего рядом). Поэтому, в таких случаях стоит использовать отладочную машинку, желательно без сохранения состояния (stateless - это тема отдельного разговора). Да, и в общем, подход с удалённым запуском и/или отладкой будет применим и к виртуальным окружениям.
Попробуем разобраться, что нам нужно для решения задачи:
- Ядро может отличаться от того, что у вас локально. Можно подтягивать собранное ядро целевой машины, и его заголовочники и собирать под него. Это будет работать, если архитектура идентична. Кросс-сборкой модуля стоит озадачиться, если ресурсов на целевой машине совсем кот наплакал. Наш вариант - собирать модуль на целевой машине.
- Раз мы собираем модуль на целевой машине, то нам нужно для начала залить туда исходные файлы (процесс настроки и установки необходимых пакетов для разработки и сборки модуля я опущу). Хорошо бы не заливать полностью исходники, а только изменённые файлы. Наш выбор
rsync
+ssh
. В современных дистрибутивах особо ничего делать не нужно. - После сборки нужно загрузить модуль
- И всё это интегрировать в IDE, дабы не делать кучу мелких движений.
Qt Creator позволяет подключаться к удалённым системам для развёртывания кода. Воспользуемся данной возможностью. Удалённая машина будет иметь имя ts.local
(используется zeroconf
/avahi
что бы сообщать свои имена в сеть).
Хочу отметить, что реализация подключения в QtC поддерживает алисы из ~/.ssh/config
, поэтому для различных хостов можно будет прописать дополнительные настройки через него.
Начнём.
Для начала идём в “Tools” -> “Options…” -> “Devices”. Жмём “Add…” и выбираем “Generic Linux Device”. После чего жмём “Start Wizard” и заполняем очевидные поля в диалоге:
- Имя для пользователя
- Имя хоста или IP адрес
- Имя пользователя на удалённом хосте
На следующем шаге предлагается настроить аутентификацию по ключу. Здесь вам лучше знать, что делать, и можно сразу нажать “Next>”.
После нажатия кнопки “Finish” будет произведена попытка подключения к хосту. Поэтому лучше, если он будет запущен.
Теперь нужно набору оснастки дать знать, что можно использовать удалённых хост. Выбираем раздел “Kits”. Для простоты можно взять набор, используемый по умолчанию (локальный запуск) и нажать “Clone” и переименовать его во что-то вроде “Default (remote)”.
Следующим шагом в новой оснастке задаём “Device type:” в “Genric Linux Device” и выбираем устройство (“Device:”), созданное ранее (на скрине выбрано “Pearl2 Ubuntu”, но сути не меняет):
Тут отмечу, что если нужно переключаться между несколькими устройствами, то создавать оснастку для каждого их них дело муторное. Поэтому кликаем правой кнопкой мыши по выпадающему списку “Device:” и ставим галочку “Mark as mutable”:
Это позволит в выпадающем списке выбора типа оснастки, конфигурации билда и деплоя/запуска указывать и устройство для подключения:
Всё, теперь открываем проект, выбираем созданную нами оснастку. После чего в настройках проекта (точнее в настройках запуска) станут доступны настройки Deployment:
Доступны различные предопределённые шаги, но мне проще было использовать один “Custom Process Step” и вызывать скрипт с параметрами, формируемыми при помощи подстановочных переменных:
- Command:
%{sourceDir}/deploy-driver-remote.sh
- Arguments:
%{CurrentDevice:HostAddress} %{CurrentDevice:SshPort} %{CurrentDevice:UserName} %{CurrentDevice:PrivateKeyFile} %-CurrentRun:Name}
- Working directory:
%{sourceDir}
Смысл переменных, думаю, понятен. Отмечу, что %{CurrentRun:Name}
это ровно то имя, которое показывается ниже в выпадающем списке “Run”/“Run configuration:”. Оно формируется при первом запуске проекта автоматически и может не соответствовать вашим критериям прекрасного. Тогда переименуйте.
Для чего я вообще использую имя конфирурации? У меня в одном проекте два драйвера: для PCI и для USB устройств. Выбирая конфигурацию я влияю на шаги по загрузке нужного драйвера на удалённой стороне.
Ещё одним нюансом является то, что я указал “Alternate executable on device:” в “/bin/true” и параметр “Use this command instead”. Я просто физически не могу “запустить” модуль ядра, как обычный бинарник, а вся процедура по его загрузке в область памяти ядра выполняется в скрипте, поэтому я выбрал для запуска программу гарантированно присутствующую на удалённой системе и всегда возвращающую статус успешного выполнения (в противовес /bin/false
). Вы можете вписать сюда что-то своё.
Ну а теперь самое вкусное, скрипт деплоя. Можно вручную прописывать шаги загрузки в IDE, но это муторно. Плюс я использую GIT сборки QtC и шаги загрузки запросто могут слететь. Поэтому я объединил их в скрипт, который может лежать прямо в репозитории и иметь идентичные настройки в IDE на разных машинах (вся кастомизация может быть выполнена через настроки конкретных Remote Devices, как описано выше).
Мой скрипт выглядит так (комментарии инлайном, что бы избежать дублирования):
#!/usr/bin/env bash
# прервёт выполнение скрипта, как только очередная команда завершится недачно
# позволяет избежать лишних проверок на успешность выполнения предыдущих операций
set -e
#
REMOTE_HOST=$1
REMOTE_PORT=$2
REMOTE_USER=$3
REMOTE_KEY=$4
#
# %{CurrentRun:Name}
TARGET=$5
# assume, that work dir is top level source dir
LOCAL_SOURCE_DIR=$(pwd)
# просто выведем то, как мы вызваны. Чисто для отладки, если что-то работает не так
echo $@
# Detect driver: как я писал выше, у меня два драйвера, основываясь на имени
# Run Configuration я выбираю то, с каким драйвером имею дело.
# Разные вариации из-за смены того, что подставляется вместо %{CurrentRun:Name} в QtC.
drv=
jobs=1
case "$TARGET" in
*_usb|*_usb-build)
drv=usb
jobs=8
;;
*_pci|*_pci-build)
drv=pci
jobs=1
;;
*)
echo "Unknown target: ${TARGET}"
exit 1
;;
esac
# Небольшой хелпер для запуска удалённой команды через SSH. С пробросом X11 (Trusted)
ssh_cmd()
{
ssh -YC -i "$REMOTE_KEY" -p ${REMOTE_PORT} ${REMOTE_USER}@${REMOTE_HOST} "$@"
}
MAKE_OPTS="DRIVER_DEBUG=1"
# Ещё больше информативности
set -x
# Ну а дальше логические шаги
# Step 1: подготовка директорий на удалённой стороны для загрузки кода
ssh_cmd "mkdir -p build/driver/remote-work/{vga2usb,helpers}"
# Step 2: синхронизируем локальное дерево исходников и удалённое.
# Именно синхронизируем: удаляем удалённое, создаём созданное
# и копируем только изменённое. Про файл rsync_excludes.txt я расскажу чуть ниже.
rsync -azv --delete --exclude-from="${LOCAL_SOURCE_DIR}/rsync_excludes.txt" "${LOCAL_SOURCE_DIR}/" -e "ssh -p ${REMOTE_PORT} -i \"${REMOTE_KEY}\"" ${REMOTE_USER}@${REMOTE_HOST}:build/driver/remote-work/vga2usb/
# Step 3: дерево исходного кода состоит из двух частей, поэтому повторяем для второй части тоже
rsync -azv --delete --exclude-from="${LOCAL_SOURCE_DIR}/rsync_excludes.txt" "${LOCAL_SOURCE_DIR}/../helpers/" -e "ssh -p ${REMOTE_PORT} -i \"${REMOTE_KEY}\"" ${REMOTE_USER}@${REMOTE_HOST}:build/driver/remote-work/helpers/
# Step 4: очистим дерево. Ваша билд система может позволить вам
# не делать этого шага и немного сэкономить времени
ssh_cmd "make -C build/driver/remote-work/vga2usb/linux/driver/${drv} -j8 clean"
# Step 5: стоим драйвер удалённо
ssh_cmd "make -C build/driver/remote-work/vga2usb/linux/driver/${drv} -j$jobs $MAKE_OPTS"
# Step 6: так как у нас используется билд инфраструктура ядра Linux, этим шагом
# мы установим модуль в /lib/modules/KERNEL_VERSION/..., можно обойтись
# и использовать insmod/rmmod и ручное разруливание зависимостей модулей.
# Обратите внимание: sudo настроен для пользователя для запуска без пароля!
# Так можно сделать только на конкретные программы и/или скрипты, но если
# система state-less, то это, IMHO, излишне.
ssh_cmd "sudo make -C build/driver/remote-work/vga2usb/linux/driver/${drv} $MAKE_OPTS install"
# Step 6.1: sign, точно работает и реализовано на Ubuntu и производных. Имеет смысл
# если включен Secure Boot, иначе модуль ядра не загрузится. По большому
# счёту на тестовых системах не нужно. У меня просто как часть проверки.
ssh_cmd "which kmodsign >/dev/null 2>&1 && \
test -f /var/lib/shim-signed/mok/MOK.priv && \
test -f /var/lib/shim-signed/mok/MOK.der && \
sudo kmodsign sha512 \
/var/lib/shim-signed/mok/MOK.priv \
/var/lib/shim-signed/mok/MOK.der \
/lib/modules/\$(uname -r)/extra/vga2${drv}.ko || true"
# Step 7: ну однострочник для выгрузки старого модуля и загрузки нового. У нас
# реализовано устройство ALSA и на современных дистрибутивах оно может
# быть захвачено pulseaudio, в таком случае выгрузить модуль не получится,
# поэтому нужно прибить пульсу (sudo - она может быть запущена от другого пользователя)
ssh_cmd "sudo depmod -aA; sudo killall -9 pulseaudio; sudo modprobe -r vga2${drv} && sudo modprobe vga2${drv} && echo OK || echo FAIL"
Если какой-то шаг завершится неудачей - скрипт прервёт работу. Вывод сообщений будет в панели “Compile Output”.
Для “запуска” драйвера достаточно нажать Ctrl+R
в QtC, если есть изменения, то код сначала отстроится локально, далее, если нет ошибок, запустится процедура деплоя, в нашем случае - вызовется скрипт выше с нужным набором параметров, синхронизируются файлы на удалённой машине, отстроится и загрузится новый драйвер. Как проверять работоспособность - уже индивидуальный подход. Возможно, отстраивать какие-то утилиты в одном из шагов, а вместо /bin/true
вызывать какой-то скрипт, что вызовет серию тестов. Я же просто смотрю dmesg -w
(с sudo
на последних дистрибутивах/ядрах) и выполняю нужные шаги.
На этом всё. Есть некая избыточность, но, в целом, задачу решает. Возможно, стоит рассмотреть возможность интеграции с KGDB и отладкой удалённого драйвера. Но это, когда возникнет острая необходимость.