Компілювання ядра і модулів. Конфігурація і компіляція ядра Linux Тестування поліпшеного прикладу

Жарознижуючі засоби для дітей призначаються педіатром. Але бувають ситуації невідкладної допомоги при лихоманці, коли дитині потрібно дати ліки негайно. Тоді батьки беруть на себе відповідальність і застосовують жарознижуючі препарати. Що дозволено давати дітям грудного віку? Чим можна збити температуру у дітей старшого віку? Які ліки найбезпечніші?

Коли виникає необхідність створення потужної і надійної системи на основі Linux (будь то обслуговування технологічних процесів, Веб-хостингу і т. Д.), То дуже часто доводиться налаштовувати системне ядро ​​таким чином, щоб вся система працювала більш ефективно і надійно. Ядро Linux хоч і є універсальним, проте бувають ситуації, коли його необхідно «подтюнінговать» з об'єктивних причин. Та й сама архітектура ядра це передбачає завдяки своїй відкритості. Таким чином, системні адміністратори Linux - це ті люди, яким важливо знати і розуміти деякі загальні аспекти конфігурації ядра Linux.

Способи конфігурації ядра Linux

За час розвитку Linux поступово склалися чотири основних способи для конфігурації її ядра:

  • модифікація параметрів, що настроюються ядра;
  • збірка ядра з вихідних кодів з внесенням потрібних змін і / або доповнень в тексти вихідних кодів ядра;
  • динамічне підключення нових компонентів (функціональних модулів, драйверів) до існуючої збірці ядра;
  • передача спеціальних інструкцій ядру під час початкового завантаження і / або використовуючи завантажувач (наприклад GRUB).

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

Параметри ядра

Системне ядро ​​Linux розроблялося таким чином, щоб завжди була можливість його максимально гнучко (втім, як і все в системах UNIX та Linux) налаштувати, адаптуючи його до необхідних умов експлуатації та апаратного оточення. Причому так, щоб це було можливо динамічно на готової збірці ядра. Іншими словами, системні адміністратори можуть в будь-який момент часу вносити коригувальні параметри, що впливають на роботу як самого ядра, так і його окремих компонентів.

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

Але як і все в системах UNIX та Linux, настройка параметрів ядра по інформаційних каналах зав'язана на файлової системи. Щоб переглядати конфігурацію ядра і керувати нею, в файлової системі в каталозі / proc / sys існують спеціальні файли. Це звичайні файли, але вони грають роль посередників в надання інтерфейсу для динамічного взаємодії з ядром. Однак документація, що стосується цього аспекту, зокрема про опис конкретних параметрів і їх значень досить мізерна. Одним з джерел, з якого можна почерпнути деякі відомості з цієї теми, є підкаталог Documentation / sysent в каталозі з вихідними кодами ядра.

Для наочності варто розглянути невеликий приклад, що показує, як через параметр ядра налаштувати максимальну кількість одночасно відкритих файлівв системі:

$ Cat / рrос / sys / fs / file-max 34916 $ sudo sh -c "echo 32768> / proc / sys / fs / file-max"

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

Також можна використовувати спеціалізовану утиліту sysctl. Вона дозволяє отримати значення змінних прямо з командного рядка, або список пар виду змінна = значення з файлу. На етапі початкового завантаження утиліта прочитує початкові значення деяких параметрів, які задані у файлі /etc/sysctl.conf. Більш детальну інформацію про утиліту sysctlможна знайти на сторінках.

У наступній таблиці наводяться деякі настроюються параметри ядра:

Каталог Файл / параметр призначення
З autoeject Автоматичне відкривання лотка з компакт-диском при размонтировании пристрою CD-ROM
F file-max максимальне число відкритих файлів. Для систем, яким доводиться працювати з великою кількістю файлів, можна збільшувати це значення до 16384
F inode-max Максимальне число відкритих індексних дескрипторів в одному процесі. Корисно для додатків, які відкривають десятки тисяч дескрипторів файлів
До
До printk ratelimit Мінімальний інтервал між повідомленнями ядра, в секундах
До printk_ratelimi_burst Кількість повідомлень, які повинні бути отримані, перед тим як значення мінімального інтервалу між повідомленнями printk стане активним
До shmmax Максимальний розмір спільно використовуваної пам'яті
N conf / default / rp_filter Включає механізм перевірки маршруту до вихідного файлу
N icmp_echo_ Ігнорування ICMP-запитів, якщо значення дорівнює 1
N icmp_echo_ Ігнорування широкомовних ICMP-запитів, якщо значення дорівнює 1.
N ip_forward Перенаправлення IP-пакетів, якщо значення дорівнює 1. Наприклад, коли машина на Linux використовується як маршрутизатор, то це значення потрібно встановлювати рівним 1
N ip_local_port_ Діапазон локальних портів, що виділяється при конфігуруванні з'єднань. Для підвищення продуктивності серверів, що ініціюють багато вихідних з'єднань, цей параметр потрібно розширити до 1024-65000
N tcp_fin_timeout Інтервал для очікування (в секундах) заключного RN-пакета. З метою підвищення продуктивності серверів, які пропускають великі обсяги трафіку, потрібно встановлювати більш низькі значення (близько 20)
N tcp_syncookies Захист від атак хвильового поширення SYN-пакетів. Потрібно включати при наявності ймовірності DOS-атак

Умовні позначення: F - / proc / sys / fs, N - / proc / sys / net / ipv4, К - / proc / sys / kernel, С - / proc / sys / dev / cdrom.

$ Sudo sysctl net.ipv4.ip_forward = 0

В результаті виконання цієї команди буде відключено перенаправлення IP-пакетів. Є одна особливість для синтаксису цієї команди: символи точки в «net.ipv4.ip_forward» замінюють символи косою риси в шляху до файлу ip_forward.

Коли потрібно збирати нову версію ядра?

В даний час ядро ​​Linux розвивається дуже швидко і бурхливо. Найчастіше виробники дистрибутивів не встигають впроваджувати в свої системи нові версії ядер. Як правило всі новомодні «фішки» більше знадобляться любителям екзотики, ентузіастам, власникам новинок пристроїв і устаткування і просто допитливим - т. Е. Переважно тим, в чиєму розпорядженні є звичайний комп'ютер користувача.

Для серверних машин, проте, мода навряд чи має якесь значення, а нові технології впроваджуються тільки після того як на практиці довели свою надійність і ефективність на тестових стендах або навіть платформах. Кожен досвідчений системний адміністратор знає, що набагато надійніше один раз налаштувати те, що може і повинно добре і безвідмовно працювати, ніж намагатися нескінченно модернізувати систему. Адже для цього необхідні багато годин роботи (адже доводиться збирати ядро ​​з вихідних кодів, що досить складно) і обслуговування, що досить дороге заняття, оскільки, на додачу до всього іншого вимагає глибокого резервування - сервера не повинні зупинятися без організації «гарячого» (а вже тим більше без «холодного») резерву.

Тому завжди варто зважувати всі чинники і оцінити, чи варто взагалі оновлюватися заради несуттєвих латок, які не впливають на роботу системи або впроваджених нових драйверів для пристроїв, яких в даний момент в системі немає і нескоро передбачається.

Якщо ж прийнято рішення оновити версію ядра шляхом його самостійної збірки, то потрібно з'ясувати, чи є дана версія стабільною. Раніше система нумерування версій ядра Linux була організована таким чином, що парні номери версій означали стабільний випуск, непарні - ще «сирий». В даний час цей принцип дотримується далеко не завжди і з'ясовувати цей момент випливає з інформації на офіційному сайті kernel.org.

Конфігурація параметрів ядра

Конфігурація для майбутньої збірки ядра Linux зберігається в файле.config. Мало хто займається ручним створенням і редагуванням цього файлу, оскільки, по-перше: це складний синтаксис, який далеко не самий «человекопонятний», і по-друге: існують способи для автоматичної генерації конфігурації збірки ядра зі зручним графічним (або псевдографічним) інтерфейсом. список основних команддля конфігурації збірки ядра:

  • make xconfig - рекомендується, якщо використовується графічне середовище KDE. Вельми зручний інструмент;
  • make gconfig - кращий варіант для використання в графічному середовищі GNOME;
  • make menuconfig - дану утиліту слід використовувати в псевдографічному режимі. Вона не так зручна, як дві попередні, однак зі своїми функціями справляється гідно;
  • make config - найбільш незручний «консольний» варіант, що виводить запити на встановлення значень кожного параметра ядра. Не дозволяє змінити вже задані параметри.

Практично всі варіанти (за винятком останнього) дозволяють отримувати коротку довідку по кожному параметру, проводити пошук потрібного параметра (або конфігураційного розділу), додавати в конфігурацію додаткові компоненти, драйвери, а також показують, яким чином конкретний компонент може бути налаштований - як компонент, вбудований в ядро ​​або як завантажуваний модуль, а також підтримує він взагалі варіант компіляції як завантаження модуля.

Дуже корисною може виявитися команда make oldconfig, призначена для перенесення існуючої конфігурації з іншою версією (збірки) ядра в новий білд. Ця команда читає конфігурацію з перенесеного з іншої збірки файла.config зі старою збіркою, визначає, які нові параметри доступні для актуальною збірки і пропонує їх включити або залишити як є.

Для виконання конфігурації збірки ядра Linux потрібно перейти в каталог з вихідними кодами і запустити одну з команд генерації конфігурації.

В результаті роботи вищевказаних команд буде згенеровано файл.conf, фрагмент вмісту з якого може бути наступним:

# # Automatically generated file; DO NOT EDIT. # Linux / x86 4.20.7 Kernel Configuration # # # Compiler: gcc (Ubuntu 7.3.0-27ubuntu1 ~ 18.04) 7.3.0 # CONFIG_CC_IS_GCC = y CONFIG_GCC_VERSION = 70300 CONFIG_CLANG_VERSION = 0 CONFIG_IRQ_WORK = y CONFIG_BUILDTIME_EXTABLE_SORT = y CONFIG_THREAD_INFO_IN_TASK = y # # General setup # CONFIG_INIT_ENV_ARG_LIMIT = 32 # CONFIG_COMPILE_TEST is not set CONFIG_LOCALVERSION = "" # CONFIG_LOCALVERSION_AUTO is not set CONFIG_BUILD_SALT = "" CONFIG_HAVE_KERNEL_GZIP = y CONFIG_HAVE_KERNEL_BZIP2 = y CONFIG_HAVE_KERNEL_LZMA = y CONFIG_HAVE_KERNEL_XZ = y CONFIG_HAVE_KERNEL_LZO = y CONFIG_HAVE_KERNEL_LZ4 = y CONFIG_KERNEL_GZIP = y # CONFIG_KERNEL_BZIP2 is not set # CONFIG_KERNEL_LZMA is not set # CONFIG_KERNEL_XZ is not set # CONFIG_KERNEL_LZO is not set # CONFIG_KERNEL_LZ4 is not set CONFIG_DEFAULT_HOSTNAME = "(none)" CONFIG_SWAP = y CONFIG_SYSVIPC = y CONFIG_SYSVIPC_SYSCTL = y CONFIG_POSIX_MQUEUE = y CONFIG_POSIX_MQUEUE_SYSCTL = y CONFIG_CROSS_MEMORY_ATTACH = y CONFIG_USELIB = y

Як можна бачити, в даному коді немає нічого привабливого для ручного редагування, про що навіть згадує запис коментаря на початку файла.config. Символ «y» в кінці будь-якої з рядків вказує, що відповідний компонент буде скомпільовано в складі ядра, «m» - як модуль. Розшифровки або опису про кожному компоненті або параметрі в файле.config не міститься - з цих питань слід вивчати відповідну документацію.

компіляція ядра

Найскладніше в компіляції ядра Linux - це створення конфігурації збірки, оскільки потрібно знати, які компоненти підключати. Хоча використання команд make xconfig, make gconfig, make menuconfig і забезпечує завдання стандартної робочої конфігурації, з якої система буде працювати на більшості апаратних платформ і конфігурацій. Питання лише в тому, щоб грамотно поставити конфігурацію ядра без непотрібних і займають зайві ресурси компонентів при його роботі.

Отже, для успішного конфігурації і компіляції ядра потрібно виконати наступні дії:

  • перейти в каталог з вихідними кодами ядра. Зазвичай «вихідні» для ядра Linux поміщаються в каталог / usr / src, або можна завантажити з сайту kernel.org в будь-який зручний місце;
  • виконати команду make xconfig, make gconfig або make menuconfig;
  • виконати команду make dep (можна не виконувати для ядер версії 2.6.x і більш пізніх);
  • виконати команду make clean (для очищення від усього того, що може перешкоджати успішній збірці);
  • виконати команду make;
  • виконати команду make modules_install;
  • скопіювати файл / arch / імя_архітектури / boot / bzImage в / boot / vmlinuz. Тут каталог / arch знаходиться в каталозі з вихідними кодами ядра Linux, імя_архітектури - каталог, який має ім'я відповідної архітектури (зазначеної на етапі конфігурації). Ім'я зібраного бінарного образу ядра bzImage може бути іншим;
  • скопіювати файл /arch/імя_архітектури/boot/System.map в /boot/System.map;
  • внести зміни в конфігураційні файли завантажувач /etc/lilo.conf (для LILO) або /boot/grub/grub.conf - для GRUB, а також додати в них відповідні конфігураційні і параметри завантаження для нового ядра.

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту і натисніть Ctrl + Enter.

Деякі особливості модульного програмування та загальні рекомендаціїз побудови підпрограм модульної структури.

Підключення модулів до основної програми проводиться в порядку їх оголошення USES, в цьому ж порядку блоки ініціалізації модулів, що підключаються до основної програми до початку виконання програми.

Порядок виконання модулів може впливати на доступ бібліотечних даних і підпрограму.

Наприклад, якщо модулі з іменами М1, М2 містять однойменні тип А, змінна В і підпрограма С, то після підключення цих моделей USES звернення до А, В, С в цій ПЕ будуть еквівалентні зверненнями до об'єктів до модуля М2.

Але щоб характеризувати коректність звернень до потрібних однойменною об'єктів різних підключених модулів доцільно при зверненні до модуля спочатку вказувати ім'я модуля, а через точку ім'я об'єкта: М1.А М1.В М1.С М2.В.

Очевидно, що досить легко поділити велику програмуна дві частини (ПЕ), тобто основна програма + модулі.

Розміщуючи кожну ПЕ в свій сегмент пам'яті і в свій дисковий файл.

Всі оголошення типів, а також тих змінних, які повинні бути доступні окремим ПЕ (основній програмі і майбутнім модулів) слід помістити в окремий модуль з порожньою виконуваної частиною. При цьому не слід звертати увагу на те, що якась ПЕ (наприклад, модуль) не використовує всіх цих оголошень. В ініціації частини такого модуля можуть бути включені оператори, що зв'язують файлові змінні з нестандартними текстовими файлами(ASSIGN) і які ініціюють ці файли, тобто вказують для них звернення до передачі даних (RESET і REWRITE).

Першу групу інших підпрограм, наприклад, кілька компактних функцій слід помістити в 3 (по черзі) модуль, інші, наприклад, процедури загального призначення - в 4 модуль, і т.д.

При розподілі підпрограм по модулях в складному проекті особлива увага повинна бути відведено на черговість і місце їх написання.

У середовищі ТР є кошти, що керують різними способами компіляції модулів.

Compile Alt + F9 RUN Cntr + F9

Destination Memory

Ці режими відрізняються тільки способом зв'язку і основною програмою.

режим Compile

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



При наявності Destination збережені в пам'яті, то ці файли залишаються тільки в пам'яті, а дисковий файл не створюється.

Однак набагато простіше створювати tpu-файли заодно з компілятором всієї програми за допомогою інших режимів, які не вимагають завдання Disk для опції destination.

режим Make

При компіляції в цьому режимі попередньо (до компіляції основної програми) для кожного модуля перевіряється:

1) Існування дискового tpu-файлу; якщо його немає, то він створюється автоматично шляхом компіляції вихідного тексту модуля, тобто його pas-файл

2) Відповідність знайденого tpu-файлу вихідного тексту модуля, куди могли бути внесені зміни; в іншому випадку tpu-файл автоматично створюється заново

3) Незмінність интерфейсного розділу модуля: якщо він змінився, то перекомпілюються також всі ті модулі (їх вихідні pas-файли), в яких даний модуль вказано в пропозиції USES.

Якщо зміна в початкових текстах модулів не було, то компілятор роботи взаємодіє з цими tpu -Файл і використовує час для компіляції.

режим Build

На відміну від режиму Make обов'язково вимагає наявність вихідних pas-файлів; проводить компіляцію (перекомпіляцію) кожного модуля і тим самим гарантує облік всіх змін в текстах pas-файлів. Це збільшує час компіляції програми в цілому.

На відміну від режиму сompile режим Make і Build дозволяють починати компілювати програму модульної структури з будь-якого заданого pas-файлу (його і називають первинним) незалежно від того, який файл (або частина програми) знаходиться в активному вікні редактора. Для цього в пункті сompile вибирають опцію Primary file натискають Enter і записують ім'я первинного pas-файлу і тоді компіляція почнеться саме з цього файлу.

Якщо ж первинний файл так не вказується, то компіляція в режимах Make, Build і RUN можливо тільки в тому випадку, якщо в активному вікні редактора знаходиться основна програма.

About: "За мотивами перекладу" Linux Device Driver 2-nd edition. Переклад: Князєв Олексій [Email protected]Дата останньої зміни: 03.08.2004 Розміщення: http://lug.kmv.ru/index.php?page=knz_ldd2

Тепер починаємо програмувати! У цьому розділі містяться основні положення про модулях і про програмування в ядрі.
Тут ми зберемо і запустимо повноцінний модуль, структура якого відповідає будь-якому реальному модульному драйверу.
При цьому, ми сконцентруємося на головних позиціях не враховуючи специфіку реальних пристроїв.

Всі частини ядра, такі як функції, змінні, заголовки та макроси, які згадуються тут, будуть
детально описані в кінці розділу.

Hello world!

У процесі ознайомлення з оригінальним матеріалом написаним Alessndro Rubini & Jonathan Corbet мені здався дещо невдалим приклад, наведений як Hello world! Тому, я хочу надати читачеві, на мій погляд більш вдалий варіант першого модуля. Сподіваюся, що з його компіляції та установки під ядро ​​версій 2.4.x не виникне ніяких проблем. Пропонований модуль і спосіб його компіляції дозволяють використовувати його в ядрах, як підтримують, так і не підтримують контроль версій. Пізніше ви ознайомитеся з усіма деталями і термінологією, я зараз відкривайте vim і починайте працювати!

================================================== // файл hello_knz.c #include #include <1>Hello, world \ n "); return 0;); void cleanup_module (void) (printk ("<1>Good bye cruel world \ n ");) MODULE_LICENSE (" GPL "); ================================= =================

Для компіляції такого модуля можна використовувати наступний Makefile. Не забудьте поставити символ табуляції перед рядком, що починається з $ (CC) ....

================================================== FLAGS = -c -Wall -D__KERNEL__ -DMODULE PARAM = -I / lib / modules / $ (shell uname -r) / build / include hello_knz.o: hello_knz.c $ (CC) $ (FLAGS) $ (PARAM) - o [Email protected] $^ =================================================

Тут використовуються дві особливості, в порівнянні з кодом оригінального Hello world, запропонованого Rubini & Corbet. По-перше, модуль буде мати версію, збігається з версією ядра. Це досягається значенням змінної PARAM в сценарії компіляції. По-друге, тепер модуль буде ліцензований в GPL (використання макросу MODULE_LICENSE ()). Якщо цього не зробити, то при установці модуля в ядро ​​ви можете побачити, приблизно, наступне попередження:

# Insmod hello_knz.o Warning: loading hello_knz.o will taint the kernel: no license See http://www.tux.org/lkml/#export-tainted for information about tainted modules Module hello_knz loaded, with warnings

Пояснимо тепер опції компіляції модуля (макроозначення будуть пояснені пізніше):

- при наявності даної опції, компілятор gcc зупинить процес компіляції файлу відразу після створення об'єктного файлу, не роблячи спробу створити виконуваний бінарник.

-Wall- максимальний рівень виведення попереджень в процесі роботи gcc.

-D- визначення макросімволов. Те ж, що і директива #define в компільовані файлі. Абсолютно без різниці, яким способом визначати, які використовуються в даному модулі, макросімволи, за допомогою #define в файлі исходнике або за допомогою опції -D для компілятора.

-I- додаткові шляхи пошуку include-файлів. Зверніть увагу на використання підстановки "uname -r", яка визначить точну назву використовуваної в даний момент версії ядра.

У наступному розділі наведено інший приклад модуля. Там же докладно пояснюється спосіб його установки і вивантаження з ядра.

Оригінальний Hello world!

Тепер наведемо оригінальний код простого модуля "Hello, World" пропонованого Rubini & Corbet. Цей код може бути скомпільований під ядрами версій з 2.0 по 2.4. Цей приклад, як і всі інші, представлені в книзі, доступні на O'Reilly FTP сайті (див. Главу 1).

// файл hello.c #define MODULE #include int init_module (void) (printk ( "<1>Hello, world \ n "); return 0;) void cleanup_module (void) (printk ("<1>Goodbye cruel world \ n ");)

функція printk ()визначена в Linux ядрі і працює як стандартна бібліотечна функція printf ()в мові Сі. Ядру потрібна своя власна, бажано невелика за розмірами, функція виведення, що міститься безпосередньо в ядрі, а не в бібліотеках призначеного для користувача рівня. Модуль може викликати функцію printk (), Тому що після завантаження модуля за допомогою команди insmodмодуль зв'язується з ядром і має доступ до опублікованих (експортованих) функцій і змінним ядра.

Строковий параметр "<1>", Рухаючись в функцію printk () - це пріоритет. В оригінальних англійських джерелах використовується термін loglevel, що означає рівень логування повідомлень. Тут, ми будемо користуватися терміном пріоритет, замість оригінального "loglevel". В даному прикладіми використовуємо високий пріоритет для повідомлення, якому відповідав би маленький номер. Високий пріоритет повідомлення задається навмисне, бо повідомлення з пріоритетом прийнятим за замовчуванням може не вивестися в консолі, з якої модуль був встановлений. Напрямок виведення повідомлень ядра з пріоритетом за умовчанням залежить від версії запущеного ядра, версії демона klogd, І вашої конфігурації. Більш детально, роботу з функцією printk ()ми пояснимо в Главі 4, "Техніка налагодження".

Ви можете протестувати модуль, за допомогою команди insmodдля установки модуля в ядро ​​і команди rmmodдля видалення модуля з ядра. Нижче ми покажемо як це можна зробити. При цьому точка входу init_module () виповнюється при установці модуля в ядро, а cleanup_module () при його вилученні з ядра. Пам'ятайте, що тільки привілейований користувач може завантажувати і вивантажувати модулі.

Приклад модуля, наведений вище, може бути використаний тільки з ядром, яке було зібрано з вимкненим прапором "module version support". На жаль, більшість дистрибутивів використовують ядра з контролем версій (це обговорюється в розділі "Контроль версії в модулях" глави 11, "kmod and Advanced Modularization"). І хоча більш старі версії пакету modutilsдозволяють завантажувати такі модулі в ядра, зібрані з контролем версій, тепер це неможливо. Нагадаємо, що пакет modutils містить набір програм, до якого входять програми insmod і rmmod.

Завдання: Визначте номер версії і склад пакета modutils з вашого дистрибутива.

При спробі вставити такий модуль в ядро, що підтримує контроль версій, ви можете побачити приблизно наступне повідомлення про помилку:

# Insmod hello.o hello.o: kernel-module version mismatch hello.o was compiled for kernel version 2.4.20 while this kernel is version 2.4.20-9asp.

В каталозі misc-modulesприкладів з ftp.oreilly.com ви знайдете оригінальний приклад програми hello.c, яка містить трохи більше рядків, і може бути встановлено в ядра як підтримують, так і не підтримують контроль версій. Як би там не було, ми настійно рекомендуємо вам зібрати власне ядро ​​без підтримки контролю версій. При цьому, рекомендується взяти оригінальні джерела ядра на сайті www.kernel.org

Якщо ви новачок в збірці ядер, то спробуйте прочитати статтю, яку Alessandro Rubini (один з авторів оригінальної книги) розмістив на http://www.linux.it/kerneldocs/kconf, і яка повинна допомогти вам в освоєнні цього процесу.

Виконайте в текстовій консолі наступні команди для компіляції і тестування наведеного вище оригінального прикладу модуля.

Root # gcc -c hello.c root # insmod ./hello.o Hello, world root # rmmod hello Goodbye cruel world root #

Залежно від механізму, який використовує ваша система для передачі рядків повідомлення, напрямок виведення повідомлень, що посилаються функцією printk (), Може відрізнятися. У наведеному прикладі компіляції та тестування модуля, повідомлення передані з функції printk () виявилися виведеними в ту ж консоль, звідки були дані команди на установку і запуск модулів. Це приклад був знятий з текстовою консолі. Якщо ж ви виконуєте команди insmodі rmmodз під програми xterm, То, швидше за все, ви нічого не побачите на своєму терміналі. Замість цього, повідомлення може опинитися в одному з системних логів, наприклад в / Var / log / messages.Точна назва файлу залежить від дистрибутива. Дивіться за часом зміни log-файлів. Механізм, який використовується для передачі повідомлень з функції printk (), описаний в розділі "How Messages Get Logged" в розділі 4 "Техніка
налагодження ".

Для перегляду повідомлень модуля у файлі системних логів / val / log / messages зручно користуватися системної утилітою tail, яка, за замовчуванням, виводить останні 10 рядків переданого в неї файлу. Цікавою опцією цієї утиліти є опція -f яка запускає утиліту в режимі стеження за останніми рядками файлу, тобто при появі в файлі нових рядків вони будуть автоматично виводитися. Щоб зупинити виконання команди в цьому випадку, необхідно натиснути Ctrl + C. Таким чином, для перегляду останніх десяти рядок файлу системних логів введіть у командному рядку наступне:

Root # tail / var / log / messages

Як ви можете бачити, написання модуля не так складно, як може здатися. Найважча частина - це зрозуміти, як працює ваш пристрій і як збільшити швидкодію модуля. У продовження цієї глави ми дізнаємося більше про написання простих модулів, а специфіку пристроїв залишимо для наступних глав.

Відмінності між модулями ядра і додатками

Додаток має одну точку входу, яка починає виповнюється відразу ж після розміщення запущеного додаткув оперативній пам'яті комп'ютера. Ця точка входу описується на мові Сі як функція main (). Завершення функції main () означає завершення програми. Модуль має кілька точок входу, виконуваних при установці і видаленні модуля з ядра, а також при обробці вступників, від користувача, запитів. Так, точка входу init_module () здійснюється за завантаженні модуля в ядро. Функція cleanup_module () виповнюється при вивантаженні модуля. Надалі ми познайомимося з іншими точками входу в модуль, які виконуються при виконанні різних запитів до модуля.

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

Як програміст ви знаєте, що додаток може викликати функцію, яку не було визнано в додатку. На стадіях статичної або динамічної компонування визначаються адреси таких функцій з відповідних бібліотек. функція printf ()одна з таких викликаються функцій, яка визначена в бібліотеці libc. Модуль, з іншого боку, пов'язаний тільки з ядром і може викликати тільки ті функції, які експортуються ядром. Код виконуваний в ядрі не може використовувати зовнішні бібліотеки. Так, наприклад, функція printk (), Яка використовувалася в прикладі hello.c, Являє собою аналог відомої функції printf (), Доступною в додатках для користувача рівня. функція printk ()розміщена в ядрі і повинна мати, по можливості, мінімальний розмір. Тому, на відміну від printf (), вона має дуже обмежену підтримку типів даних, і, наприклад, взагалі не підтримує чисел з плаваючою точкою.

Реалізація ядер 2.0 і 2.2 не підтримувала специфікатор типів Lі Z. Вони були введені тільки в версії ядра 2.4.

На ріс.2-1 зображена реалізація механізму виклику функцій, які є точками входу в модуль. Також, на цьому малюнку зображений механізм взаємодії встановленого або встановлюється модуля з ядром.

Мал. 2-1. Зв'язок модуля з ядром

Одна з особливостей операційних систем Unix / Linux полягає у відсутності бібліотек, які можуть бути слінковани з модулями ядра. Як ви вже знаєте, модулі, при їх завантаженні, лінкуются в ядро, тому все, зовнішні для вашого модуля, функції повинні бути оголошені в заголовних файлах ядра і бути присутнім в ядрі. Вихідні тексти модулів ніколине повинні включати звичайні заголовки з бібліотек користувацького простору. У модулях ядра ви можете використовувати тільки функції, які дійсно є частиною ядра.

Весь інтерфейс ядра, описаний в заголовних файлах, що знаходяться в каталогах include / linuxі include / asmвсередині початкових кодів ядра (зазвичай знаходяться в /usr/src/linux-x.y.z(X.y.z - версія вашого ядра)). Більш старі дистрибутиви (засновані на libcверсії 5 або менше) використовували символічні посилання / Usr / include / linuxі / Usr / include / asmна відповідні каталоги в исходниках ядра. Ці символічні посилання дають можливість, при необхідності, використовувати інтерфейси ядра в призначених для користувача додатках.

Незважаючи на те, що інтерфейс бібліотек користувацького простору тепер відділений від інтерфейсу ядра, іноді, в призначених для користувача процесах виникає необхідність використання інтерфейсів ядра. Однак, багато посилання в заголовних файлах ядра відносяться тільки до самого ядра і не повинні бути доступні додаткам користувача. Тому, ці оголошення захищені #ifdef __KERNEL__блоками. Ось чому ваш драйвер, як і інший код ядра, повинен бути скомпільовано з оголошеним макросімволом __KERNEL__.

Роль окремих заголовків файлів ядра буде обговорюватися в книзі в міру необхідності.

Розробники, що працюють з будь-якими великими програмними проектами (наприклад, таким як ядро), повинні враховувати і уникати "Забруднення простору імен". Ця проблема виникає при наявності великої кількостіфункцій і глобальних змінних чиї імена мало виразні (помітні). Програміст, якому згодом доводиться мати справу з такими додатками, змушений витрачати набагато більше часу на запам'ятовування "зарезервованих" імен і придумування унікальних імен для нових елементів. Колізії імен (неоднозначності) можуть створити широке коло проблем, починаючи з помилок при завантаженні модуля, закінчуючи нестабільним або незрозумілим поведінкою програм, яке може проявитися у користувачів, що використовують ядро, зібране в іншій конфігурації.

Розробники не можуть дозволити собі таких помилок при написанні коду ядра, тому що навіть самий маленький модуль буде слінкован з усім ядром. Кращим рішенням для запобігання колізій імен є, по-перше, оголошення ваших об'єктів програми як static, А, по-друге, використання для іменування глобальних об'єктів унікальний, в межах системи, префікс. Крім того, як розробник модуля, ви можете управляти областями видимості об'єктів вашого коду, як це описано пізніше в розділі "Таблиця лінковки ядра".

Більшість (але не всі) версії команди insmodекспортують всі об'єкти модуля, які не оголошені як static, За замовчуванням, тобто якщо в модулі не визначені спеціальні інструкції на цей рахунок. Тому, цілком розумно оголошувати додаткові компоненти, які ви не збираєтеся експортувати, як static.

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

Ще одна істотна відмінність між ядром і призначеними для користувача процесами полягає в механізмі обробки помилок. Ядро контролює виконання призначеного для користувача процесу, тому помилка в призначеному для користувача процесі призводить до виникнення нешкідливого для системи повідомлення: segmentation fault. При цьому, завжди може бути використаний відладчик для відстеження помилки у вихідному коді призначеного для користувача програми. Помилки виникають в ядрі фатальні - якщо не для всієї системи, то, по крайней мере, пов'язаних з поточною діяльністю. У розділі "Налагодження помилок системи" глави 4 "Техніка налагодження" ми розглянемо способи стеження за вадами ядра.

Користувача простір і простір ядра

Модуль виконується в так званому просторі ядра, Тоді як додатки працюють в. Ця концепція - основа теорії операційних систем.

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

Практично кожен сучасний процесор в змозі забезпечити такий поділ, за рахунок реалізації різних рівнів привілеїв для виконуваного коду (потрібно не менше двох рівнів). Наприклад, процесори архітектури I32 мають чотири рівні привілеїв від 0 до 3. Причому, рівень 0 має найвищі привілеї. Для таких процесорів існує клас привілейованих інструкцій, які можуть виконуватися тільки на привілейованих рівнях. Unix системи використовують два рівня привілеїв процесора. Якщо процесор має більше двох рівнів привілеїв, то використовуються наинизший і найвищий. Ядро Unix працює на найвищому рівніпривілеїв, забезпечуючи управління обладнанням і процесами користувача.

Коли ми говоримо про просторі ядраі просторі призначеного для користувача процесумаються на увазі не тільки різні рівні привілеїв виконуваного коду, але і різні адресні простори.

Unix передає виконання з простору призначеного для користувача процесу в простір ядра в двох випадках. По-перше, коли користувальницький додаток виконує звернення до ядру (системний виклик), і, по-друге, під час обслуговування апаратних переривань. Код ядра, що виконується при системному виклику працює в контексті процесу, Тобто працюючи в інтересах викликав його процесу від має доступ до даних адресного простору процесу. З іншого боку, код виконуваний при обслуговуванні апаратного переривання є асинхронним, по відношенню до процесу, і не відноситься до якого то особливому процесу.

Призначення модулів полягає в розширенні функціональності ядра. Код модулів виповнюється в просторі ядра. Зазвичай, модуль здійснює обидва завдання, відмічені раніше: деякі функції модуля виконуються як частина системних викликів, а деякі відповідальні за управління переривань.

Розпаралелювання в ядрі

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

Існує безліч причин розпаралелювання коду ядра. Зазвичай в Linux запущено безліч процесів, і деякі з них можуть спробувати звернутися до коду вашого модуля одночасно. Багато пристроїв можуть викликати апаратні переривання процесора. Обробники переривань викликаються асинхронно і можуть бути викликані в той момент, коли ваш драйвер займається виконанням іншого запиту. Деякі програмні абстракції (такі як таймери ядра, що пояснюється в главі 6 "Flow of Time") також запускаються асинхронно. Крім того, Linux може бути запущений на системі з симетричними Мультіпроцесори (SMP), в результаті чого, код вашого драйвера може паралельно виконуватися на декількох процесорах одночасно.

З цих причин, код Linux ядра, включаючи коди драйверів, повинен бути реєнтерабельним, тобто повинен бути здатний працювати з більш ніж одним контекстом даних одночасно. Структури даних повинні бути розроблені з урахуванням паралельного виконання декількох потоків. У свою чергу, код ядра, повинен мати здатність обробляти кілька паралельних потоків даних не пошкоджуючи їх. Написання такого коду, який може виконуватися паралельно і уникати ситуацій, в яких інша послідовність виконання може привести до небажаного поводження системи, вимагає багато часу, і, можливо, хитрості. Кожен приклад драйвера в цій книзі написано з урахуванням можливого паралельного виконання. При необхідності, ми будемо пояснювати особливості техніки написання такого коду.

Найбільш загальна помилка, яку допускають програмісти полягає в їх припущенні, що паралельність не є проблемою, оскільки деякі сегменти коду не можуть піти в "сплячий стан". І дійсно, ядро ​​Linux є невивантажуваного, з важливим винятком щодо обробників переривань, які не можуть отримати процесор під час виконання важливого коду ядра. Останнім часом, невигружаемості було досить для запобігання небажаного розпаралелювання в більшості випадків. На SMP системах, однак, вивантаження код непотрібно через паралельного обчислення.

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

Прим. перекладача:Вибачте, але останні два абзаци, мені не зрозумілі. Можливо, це результат неправильного перекладу. Тому наводжу оригінальний текст.

A common mistake made by driver programmers is to assume that concurrency is not a problem as long as a particular segment of code
does not go to sleep (or "block"). It is true that the Linux kernel is nonpreemptive; with the important exception of
servicing interrupts, it will not take the processor away from kernel code that does not yield willingly. In past times, this nonpreemptive
behavior was enough to prevent unwanted concurrency most of the time. On SMP systems, however, preemption is not required to cause
concurrent execution.

If your code assumes that it will not be preempted, it will not run properly on SMP systems. Even if you do not have such a system,
others who run your code may have one. In the future, it is also possible that the kernel will move to a preemptive mode of operation,
at which point even uniprocessor systems will have to deal with concurrency everywhere (some variants of the kernel already implement
it).

Інформація про поточний процес

Хоча код модуля ядра не виконується послідовно, як додатки, але більшість звернень до ядра виконуються щодо, який звернувся до нього, процесу. Код ядра може впізнати викликав його процес звернувшись до глобального вказівником який вказує на структуру struct task_struct, Певну, для ядер версії 2.4, в файлі , Включеному в . покажчик currentвказує на поточний що виконується користувальницький процес. При виконанні таких системних викликів як open ()або close (), Обов'язково існує процес викликав їх. Код ядра, при необхідності, може викликати специфічну інформацію по викликав його процесу через покажчик current. Приклади використання цього покажчика ви знайдете в розділі "Управління доступом до файлу пристрою" в розділі 5 "Enhanced Char Driver Operations".

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

З точки зору модуля, currentсхожий на зовнішнє посилання printk (). Модуль може використовувати currentвсюди, де потрібно. Наприклад, наступний шматок коду друкує ідентифікатор (process ID - PID) і ім'я команди викликав модуль процесу, отримуючи їх через відповідні поля структури struct task_struct:

Printk ( "The process is \"% s \ "(pid% i) \ n", current-> comm, current-> pid);

Поле current-> comm є ім'я файлу команди породила поточний процес.

Компіляція і завантаження модулів

Залишок цієї глави присвячений написання закінченого, хоча і нетипового, модуля. Тобто модуль не належить до жодного з класів, описаних в розділі "Класи пристроїв і модулів" в розділі 1 "Введення в драйвера пристроїв". Приклад драйвера, показаного в цьому розділі буде носити назву skull (Simple Kernel Utility for Loading Localities). Ви можете використовувати модуль scull як шаблон для написання власного локального коду.

Ми використовуємо поняття "локального коду" (local) для підкреслення ваших персональних змін коду, в старих добрих традиціях Unix (/ usr / local).

Однак, перед тим як ми наповнимо змістом функції init_module () і cleanup_module (), ми напишемо сценарій Makefile, який будемо використовувати утилітою make для побудови об'єктного коду модуля.

Перед тим, як препроцесор обробить включення будь-якого заголовки, необхідно, щоб директивою #define був визначений макросімвол __KERNEL__. Як згадувалося раніше, в інтерфейсних файлах ядра може бути визначений специфічний для ядра контекст, видимий тільки в разі якщо символ __KERNEL__ визначено в стадії препроцессінга заздалегідь.

Інший важливим символом, який визначається директивою #define, є символ MODULE. Від повинен бути визначений до включення інтерфейсу (Виключаючи ті драйвера які будуть зібрані разом з ядром). Драйвера, що збираються в ядро ​​НЕ будуть описані в даній книзі, тому символ MODULE буде присутній у всіх наших прикладах.

Якщо ви збираєте модуль для системи з SMP, вам, також, необхідно визначити макросімвол __SMP__ перед включенням інтерфейсів ядра. У версії ядра 2.2 окремим пунктом в конфігурацію ядра був внесений вибір між однопроцессорной і многопроцессорной системою. Тому, включення наступних рядків найпершими рядками вашого модуля призведе до підтримки багатопроцесорної системи.

#include #ifdef CONFIG_SMP # define __SMP__ #endif

Розробники модуля, також повинні визначити прапор оптимізації -O для компілятора, тому що багато функцій оголошені як inline в заголовних файлах ядра. Компілятор gcc не виконує розширення inline для функцій до тих пір поки не дозволена оптимізація. Дозвіл розширення підстановок inline за допомогою опцій -g і -O дозволить вам, в подальшому, налагоджувати код використовує inline-функції в отладчике. Так як ядро ​​широко використовує inline-функції, дуже важливо, щоб вони були розширені правильно.

Зауважте, однак, що використання будь-оптимізації вище рівня -O2 ризиковано, тому, що компілятор може розширити і ті функції, які не описані як inline. Це може привести до проблем, тому що код деяких функцій очікує знайти стандартний стек свого виклику. Під inline-розширенням розуміється вставка коду функції в точку її виклику замість відповідної інструкції виклику функції. Відповідно, при цьому, раз немає виклику функції, то немає і стека її виклику.

Можливо, вам потрібно буде перевірити, що для компіляції модулів ви використовуєте той же самий компілятор, який був використаний для збірки ядра, в яке даний модуль передбачається встановлювати. Подробиці дивіться в оригінальному документі з файлу Documentation / Changesрозташованого в каталозі джерел ядра. Розробки ядра і компілятора, як правило, синхронізовані між групами розробників. Можливі випадки, коли оновлення одного з цих елементів розкриває помилки в іншому. Деякі виробники дистрибутивів поставляють ультра-нові версії компілятора, що не відповідають вибраному ядру. У цьому випадку, вони зазвичай надають окремий пакет (часто званий kgcc) З компілятором, спеціально призначеним для
компіляції ядра.

Нарешті, для того, щоб запобігти неприємним помилки, ми пропонуємо вам використовувати опцію компіляції -Wall(All warning - все попередження). Можливо, для задоволення всіх цих попереджень, вам буде потрібно змінити ваш звичайний стиль програмування. При написанні коду ядра краще використовувати стиль кодування пропонований Лінус Торвальдс. Так, документ Documentation / CodingStyle,з каталогу джерел ядра, досить цікавий і рекомендований всім тим, хто цікавиться програмуванням рівня ядра.

Набір прапорів компіляції модуля, з якими ми познайомилися недавно, рекомендується розміщувати в змінної CFLAGSвашого Makefile. Для утиліти make це особлива змінна, використання якої стане зрозуміло з подальшого опису.

Крім прапорів в змінної CFLAGS, В вашому Makefile може знадобитися мета, яка об'єднує різні об'єктні файли. Така мета необхідна тільки в тому випадку, коли код модуля розділений на кілька файлів джерел, що, взагалі, не є рідкістю. Об'єктні файли об'єднуються командою ld -r, Яка не є лінковочной операцією в загальноприйнятому сенсі, не дивлячись на використання лінковщік ( ld). Результатом виконання команди ld -rє інший об'єктний файл, який об'єднує об'єктні коди вхідних файлів лінковщік. опція -rозначає " relocatable - перемещаемость", Тобто вихідний файл команди переміщаємо в адресному просторі, тому що в ньому ще не проставлені абсолютні адреси виклику функцій.

У наступному прикладі представлений мінімальний Makefile необхідний для компіляції модуля, що складається з двох файлів джерел. Якщо ваш модуль складається з одного файлу джерела, то з наведеного прикладу необхідно прибрати мета містить команду ld -r.

# Шлях до вашого каталогу джерел ядра можна змінити тут, # а можна передати його параметром при виклику "make" KERNELDIR = / usr / src / linux include $ (KERNELDIR) /. Config CFLAGS = -D__KERNEL__ -DMODULE -I $ (KERNELDIR) / include \ -O -Wall ifdef CONFIG_SMP CFLAGS + = -D__SMP__ -DSMP endif all: skull.o skull.o: skull_init.o skull_clean.o $ (LD) -r $ ^ -o [Email protected] clean: rm -f * .o * ~ core

Якщо ви погано знайомі з роботою утиліти make, то ви, можливо, здивуєтеся відсутністю правил компіляції * .c файлів в об'єктні * .o файли. Визначення таких правил не є необхідними, тому що утиліта make, при необхідності, сама перетворює * .c файли в * .o файли використовуючи прийнятий за замовчуванням компілятор або компілятор заданий змінною $ (CC). При цьому вміст змінної $ (CFLAGS)використовується для вказівки прапорів компіляції.

Сpедой кроком після побудови модуля, є завантаження його в ядро. Ми вже говорили, що для цього ми будемо використовувати утиліту insmod, яка пов'язує всі невизначені символи (виклики функцій та ін.) Модуля з символьного таблицею запущеного ядра. Однак, на відміну від лінковщік (наприклад такого як ld) вона не змінює дисковий файл модуля, а завантажує слінкованний з ядром об'єкт модуля в оперативну пам'ять. Утиліта insmod може приймати деякі опції командного рядка. Подробиці можна подивитися через man insmod. Використовуючи ці опції можна, наприклад, призначити певним цілим і строковим змінним вашого модуля задані значенняперед лінковкою модуля в ядро. Таким чином, якщо модуль правильно розроблений, він може бути налаштований на етапі завантаження. Такий спосіб конфігурації модуля дає користувачеві велику гнучкість ніж конфігурація на етапі компіляції. Конфігурація на етапі завантаження пояснюється в розділі "Ручне і автоматичне конфігурування" пізніше в цьому розділі.

Деяким читачам будуть цікаві подробиці роботи утиліти insmod. Реалізація insmod заснована не кількох системних викликах, визначених у kernel / module.c. Функція sys_create_module () розподіляє в адресному просторі ядра необхідну кількість пам'яті для завантаження модуля. Ця пам'ять розподіляється за допомогою функції vmalloc () (див. Розділ "vmalloc and Friends" в розділі 7 "Getting Hold of Memory"). Системний виклик get_kernel_sysms () повертає символьну таблицю ядра, яка буде використана для визначення реальних адрес об'єктів при лінковке. Функція sys_init_module () копіює об'єктний код модуля в адресний простір ядра і викликає ініціалізацій функцію модуля.

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

залежно версій

Якщо ви не знаєте нічого більше того, що тут було розказано, то, швидше за все, створювані вами модулі повинні будуть перекомпілювати для кожної версії ядра, в яке вони будуть слінковани. У кожному модулі повинен бути визначений символ, званий __module_kernel_version, Значення якого
порівнюється з версією поточного ядра утилітою insmod. Цей символ розташований в секції .modinfoфайлів формату ELF (Executable and Linking Format). Більш детально це пояснюється в главі 11 "kmod and Advanced Modularization". Будь ласка зауважте, що цей спосіб контролю версій можна застосовувати тільки для версій ядра 2.2 і 2.4. В ядрі версії 2.0 це виконується дещо іншим способом.

Компілятор визначить цей символ всюди, де буде включений заголовки . Тому, в наведеному раніше прикладі hello.c ми не описували цей символ. Це також означає, що якщо ваш модуль складається з безлічі файлів джерел, ви повинні включити файл в свій код тільки один раз. Винятком є ​​випадок використання визначення __NO_VERSION__, З яким ми познайомимося пізніше.

Нижче наведено визначення описуваного символу з файлу module.h витягнуте з коду ядра 2.4.25.

Static const char __module_kernel_versio / PRE__attribute __ ((section ( ". Modinfo"))) = "kernel_version =" UTS_RELEASE;

У разі відмови завантаження модуля через невідповідність версій, можна спробувати завантажити цей модуль передавши в рядок параметрів утиліти insmod ключ -f(Force). Такий спосіб завантаження модуля не безпечний, і не завжди успішний. Пояснити причини можливих невдач досить важко. Можливо, завантаження модуля не буде виконана через нерозв'язності символів при лінковке. В цьому випадку ви отримаєте відповідне повідомлення про помилку. Причини невдачі можуть ховатися і в зміні роботи або структури ядра. В цьому випадку, завантаження модуля може привести до серйозних помилок періоду виконання, а також до краху системи (system panic). Останнє має послужити добрим стимулом для використання системи контролю версій. Невідповідність версій може управлятися елегантніше при використанні контролю версій в ядрі. Про це ми детально поговоримо в розділі "Version Control in Modules" в главі 11 "kmod and Advanced Modularization".

Якщо ви хочете скомпілювати ваш модуль для особливої ​​версії ядра, ви повинні включити заголовні файл саме від цієї версії ядра. У вище описаному прикладі Makefile для визначення каталогу розміщення цих файлів використовувалася змінна KERNELDIR. Така індивідуальна компіляція не є рідкістю, при наявності джерел ядра. Також, частої є ситуація, наявності різних версій ядра в дереві каталогів. Всі наведені в цій книзі приклади модулів використовують змінну KERNELDIRдля вказівки розміщення каталогу джерел тієї версії ядра, в яке передбачається проводити лінковку зібраного модуля. Для вказівки цього каталогу можна використовувати системну змінну, або передавати його розташування через параметри командного рядка для утиліти make.

При завантаженні модуля, утиліта insmod використовує свої власні шляхи пошуку об'єктних файлів модуля, переглядаючи версії-залежні каталоги починаючи від точки / Lib / modules. І хоча старі версії утиліти включали в шляху пошуку поточний каталог, зараз така поведінка вважається неприпустимим з причин безпеки (ті ж проблеми, що і з використанням системної змінної PATH). Таким чином, якщо ви хочете завантажити модуль з поточного каталогу ви можете вказати його в стилі ./module.o. Така вказівка ​​положення модуля спрацює для будь-яких версій утиліти insmod.

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

В заголовки linux / version.hвизначені наступні макроси, пов'язані з визначенням версії ядра.

UTS_RELEASEМакрос, розширюваний в рядок, що описує версію ядра поточного
дерева початкових кодів. Наприклад, макрос може розширитися в таку
рядок: "2.3.48" . LINUX_VERSION_CODEЦей макрос розширюється в бінарне представлення версії ядра, по
одному байту на кожну частину номера. Наприклад, бінарне
уявлення для версії 2.3.48 буде 131888 (десяткове
уявлення для шістнадцятирічного 0x020330). Можливо, бінарне
уявлення здасться вам зручніше строкового. Зауважте, що таке
уявлення дозволяє описати не більше 256 варіантів в кожній
частини номера. KERNEL_VERSION (major, minor, release)Це макроозначень дозволяє побудувати "kernel_version_code"
з індивідуальних елементів складових версію ядра. наприклад,
наступне макро KERNEL_VERSION (2, 3, 48)
розшириться до 131888. Це макроозначень дуже зручно при
порівнянні поточної версії ядра з необхідним. Ми будемо неодноразово
використовувати це макроозначень протягом всієї книги.

Наведемо вміст файлу linux / version.hдля ядра 2.4.25 (текст заголовки наведено повністю).

#define UTS_RELEASE "2.4.25" #define LINUX_VERSION_CODE 132121 #define KERNEL_VERSION (a, b, c) (((a)<< 16) + ((b) << 8) + (c))

Заголовки version.h включається в файл module.h, тому, як правило, у вас не виникає необхідності включати version.h в код вашого модуля явно. З іншого боку, ви можете запобігти включення заголовки version.h в module.h оголошенням макро __NO_VERSION__. Ви будете використовувати __NO_VERSION__, Наприклад в разі, коли вам необхідно включити в кілька файлів джерел, які, згодом, будуть слінковани в один модуль. оголошення __NO_VERSION__перед включенням заголовки module.h запобігає
автоматичне опис рядка __module_kernel_versionабо її еквівалента в файлах джерелах. Можливо, вам це знадобитися для задоволення скарг лінковщік при ld -r, Якому не сподобається множинний опис символів в таблицях лінковки. Зазвичай, якщо код модуля розділений на кілька файлів джерел, що включають заголовки , То оголошення __NO_VERSION__робиться у всіх цих файлах крім одного. В кінці книги наведено приклад модуля, що використовує __NO_VERSION__.

Більшість залежностей пов'язаних з версією ядра, може бути оброблено за допомогою логіки, побудованої на директивах препроцесора, з використанням макроозначень KERNEL_VERSIONі LINUX_VERSION_CODE. Однак перевірка залежностей версій може сильно ускладнити читаність коду модуля за рахунок різношерстих директив #ifdef. Тому, напевно найкращим рішенням є приміщення перевірки залежностей в окремий заголовний файл. Ось чому наш приклад включає заголовки sysdep.h, Який використовується для розміщення в ньому всіх макроозначень, пов'язаних з перевірками залежностей версій.

Перша залежність версій, яку ми хочемо представити знаходиться в оголошенні мети " make install"Сценарію компіляції нашого драйвера. Як ви могли очікувати, інсталяційний каталог, який змінюється відповідно до використовуваної версії ядра, вибирається на основі перегляду файлу version.h. Наведемо фрагмент коду з файлу Rules.make, Який використовується усіма Makefile ядра.

VERSIONFILE = $ (INCLUDEDIR) /linux/version.h VREION = $ (shell awk -F \ "" / REL / (print $$ 2) "$ (VERSIONFILE)) INSTALLDIR = / lib / modules / $ (VERSION) / misc

Зверніть увагу, що для інсталяції всіх наших драйверів ми використовуємо каталог misc (оголошення INSTALLDIR в наведеному вище прикладі Makefile). Починаючи з версії ядра 2.4 цей каталог є рекомендованим для розміщення призначених для користувача драйверів. Крім того, і старі і нові версії пакету modutils містять каталог misc в своїх шляхах пошуку.

Використовуючи дане вище визначення INSTALLDIR, мета install в Makefile може виглядати наступним чином:

Install: install -d $ (INSTALLDIR) install -c $ (OBJS) $ (INSTALLDIR)

Залежність від платформи

Кожна комп'ютерна платформа має свої особливості, які повинні бути враховані розробниками ядра для досягнення найвищої продуктивності.

Розробники ядра мають набагато більше свободи у виборі і прийнятті рішень неж / PCLASS = "western» і розробники додатків. Саме така свобода дозволяє оптимізувати код, вичавлюючи максимум з кожної конкретної платформи.

Код модуля повинен бути скомпільовано з використанням тих же самих опцій компілятора, які були використані при компіляції ядра. Це відноситься і до використання однакових схем використання регістрів процесора, і до виконання одного і того ж рівня оптимізації. файл Rules.make, Розташований в корені дерева джерел ядра, включає переносних залежні визначення, які повинні бути включені в усі Makefile компіляції. Все переносних залежні сценарії компіляції називаються Makefile. platformі містять значення змінних для утиліти make згідно поточної конфігурації ядра.

Іншою цікавою особливістю Makefile є підтримка крос-платформної або просто крос компіляції. Цей термін використовується при необхідності компіляції коду для іншої платформи. Наприклад, використовуючи платформу i86 ви збираєтеся створити код для платформи M68000. Якщо ви збираєтеся використовувати крос компіляцію, то вам буде потрібно замінити ваші інструменти компіляції ( gcc, ld, Та ін.) Іншим набором відповідних інструментів
(Наприклад, m68k-linux-gcc, m68k-linux-ld). Використовуваний префікс можна визначити або змінної $ (CROSS_COMPILE) Makefile, або параметром командного рядка для утиліти make, або змінної оточення системи.

Архітектура SPARC являє собою особливий випадок, який повинен бути оброблений відповідним чином в Makefile. Призначені для користувача програми запускаються на SPARC64 (SPARC V9) платформі є бінарники, як правило, призначені для платформи SPARC32 (SPARC V8). Тому, компілятор буде використовуватися під на платформі SPARC64 (gcc) генерує об'єктний код для SPARC32. З іншого боку, ядро ​​призначене для роботи на SPARC V9 повинно містити об'єктний код для SPARC V9, тому, навіть в цьому випадку, потрібно крос компілятор. Все GNU / Linux дистрибутиви призначені для SPARC64 включають в себе відповідний крос компілятор, який необхідно вибрати в Makefile сценарії компіляції ядра.

І хоча повний список залежностей від версій і платформ трохи складніший, ніж описаний тут, але цього цілком достатньо для виконання крос компіляції. Для отримання додаткової інформації ви можете подивитися Makefile сценарії компіляції і файли джерела ядра.

Особливості ядра 2.6

Час не стоїть на місці. І зараз ми є свідками появи нового покоління ядра 2.6. На жаль, в оригіналі даної книги не розглядається нове ядро, тому перекладач візьме на себе сміливість доповнити переклад новими знаннями.

Ви можете користуватися інтегрованими середовищами розробки, такими як TimeSys 'TimeStorm, які правильно сформують кістяк і сценарій компіляції для вашого модуля в залежності від необхідної версії ядра. Якщо ж ви збираєтеся писати все це самостійно, то вам знадобиться деяка додаткова інформація про основні відмінності, привнесених новим ядром.

Одна з особливостей ядра 2.6 полягає в необхідності використання макросів module_init () і module_exit () для явної реєстрації імен функцій ініціалізації і завершення.

Макроозначення MODULE_LISENCE (), введене в ядрі 2.4 як і раніше необхідно, якщо ви не хочете спостерігати відповідних попереджень при завантаженні модуля. Ви можете вибрати наступні, що позначають ліцензії рядки, для передачі в макро: "GPL", "GPL v2", "GPL and additional rights", "Dual BSD / GPL" (вибір між BSD або GPL ліцензіями), "Dual MPL / GPL "(вибір між Mozilla або GPL ліцензіями) і
"Proprietary".

Більш суттєвим для нового ядра є нова схема компіляції модулів, що тягне за собою не тільки зміни в коді самого модуля але і в Makefile сценарії його компіляції.

Так, визначення макросімвола MODULE тепер не потрібно ні в коді модуля ні в Makefile. При необхідності, нова схема компіляції сама визначить даний макросімвол. Також, вам не знадобиться явне визначення макросімволов __KERNEL__, або новіших, таких як KBUILD_BASENAME і KBUILD_MODNAME.

Також, ви не повинні визначати рівень оптимізації при компіляції (-O2 або інші), тому що ваш модуль буде скомпільовано з усім тим набором прапорів, в тому числі і прапори оптимізації, з якими компілюються всі інші модулі вашого ядра - утиліта make автоматично використовує весь необхідний набір прапорів.

З цих причин, Makefile для компіляції модуля для ядра 2.6 набагато простіше. Так для модуля hello.c Makefile буде виглядати наступним чином:

Obj-m: = hello.o

Однак, для того, щоб скомпілювати модуль, вам знадобиться доступ по запису до дерева джерел ядра, де будуть створені тимчасові файли і каталоги. Тому команда компіляції модуля до ядра 2.6, що задається з поточного каталогу, що містить код джерела модуля, повинна виглядати таким чином:

# Make -C /usr/src/linux-2.6.1 SUBDIRS = `pwd` modules

Отже, маємо джерело модуля hello-2.6.c, Для компіляції в ядрі 2.6:

//hello-2.6.c #include #include #include MODULE_LICENSE ( "GPL"); static int __init my_init (void) (printk ( "Hello world \ n"); return 0;); static void __exit my_cleanup (void) (printk ( "Good bye \ n");); module_init (my_init); module_exit (my_cleanup);

Відповідно, маємо Makefile:

Obj-m: = hello-2.6.o

Викликаємо утиліту make для обробки нашого Makefile з наступними параметрами:

# Make -C / usr / src / linux-2.6.3 SUBDIRS = `pwd` modules

Нормальний процес компіляції пройде з наступним стандартним висновком:

Make: Вхід в каталог `/usr/src/linux-2.6.3" *** Warning: Overriding SUBDIRS on the command line can cause *** inconsistencies make: `arch / i386 / kernel / asm-offsets.s» не потребує оновлення. CHK include / asm-i386 / asm_offsets.h CC [M] /home/knz/j.kernel/3/hello-2.6.o Building modules, stage 2. /usr/src/linux-2.6.3/scripts/Makefile .modpost: 17: *** Uh-oh, you have stale module entries. You messed with SUBDIRS, /usr/src/linux-2.6.3/scripts/Makefile.modpost:18: do not complain if something goes wrong. MODPOST CC /home/knz/j.kernel/3/hello-2.6.mod.o LD [M] /home/knz/j.kernel/3/hello-2.6.ko make: Вихід з теки `/ usr / src /linux-2.6.3 "

Кінцевим результатом компіляції буде файл модуля hello-2.6.ko який можна встановлювати в ядро.

Зверніть увагу, що в ядрі 2.6 файли модулів мають суффікс.ko, а не.o як в ядрі 2.4.

Таблиця символів ядра

Ми вже говорили про те як утиліта insmod використовує таблицю public-символів ядра при лінковке модуля з ядром. Ця таблиця містить адреси глобальних об'єктів ядра - функцій і змінних - які потрібні для реалізації модульних варіантів драйвера. Таблиця public-символів ядра може бути прочитана в текстовій формі з файлу / proc / ksyms, за умови, що ваше ядро ​​підтримує файлову систему / proc.

В ядрі 2.6 файл / proc / ksyms перейменований в / proc / modules.

При завантаженні модуля, символи експортовані модулем стають частиною таблиці символів ядра, і ви зможете переглянути з в / proc / ksyms.

Нові модулі можуть використовувати символи експортовані вашим модулем. Так, наприклад, модуль msdos покладається на символи експортовані модулем fat, а кожен пристрій USB використовується в режимі читання використовує символи модулів usbcore і input. Такий взаємозв'язок реалізується послідовної завантаженням модулів називається стеком модулів.

Стек модулів зручно використовувати при створенні складних проектів модулів. Така абстракція зручна для поділу коду драйвера пристрою на апаратно-залежну та апаратно-незалежну частини. Наприклад, набір драйверів video-for-linux складається з основного модуля, який експортує символи для низькорівневого драйвера, що враховує специфіку використовуваного обладнання. Згідно вашої конфігурації, ви завантажуєте основний відео-модуль і модуль специфічний для вашої апаратної частини. Таким же чином реалізується підтримка паралельних портів і широкого класу пристроїв, що підключаються, таких як USB-пристроїв. Стек системи паралельного порту показаний на рис. 2-2. Стрілками показано взаємодію між модулями і програмним інтерфейсом ядра. Взаємодія може здійснюватися як на рівні функцій, так і на рівні структур даних, керованих функціями.

Рис 2-2. Стек модулів паралельного порту

При використанні стекових модулів зручно користуватися утилітою modprobe. Функціональність утиліти modprobe багато в чому схожа на утиліту insmod, але при завантаженні модуля перевіряє його нижележащие залежності, і, при необхідності, підвантажує необхідні модулі до необхідного заповнення стека модулів. Таким чином, одна команда modprobe може призводити до кількох викликам команди insmod. Можна сказати, що команда modprobe є інтелектуальною оболонкою над insmod. Ви можете використовувати modprobe замість insmod всюди, за винятком випадків завантаження власних модулів з поточного каталогу, тому що modprobe переглядає тільки спеціальні каталоги розміщення модулів, і не зможе задовольнити можливі залежності.

Поділ модулів на частини допомагає зменшити час розробки за рахунок спрощення постановки задачі. Це схоже на поділ між механізмом реалізації та політикою управління, яке обговорено в розділі 1 "Введення в драйвера пристроїв".

Зазвичай модуль реалізує свою функціональність не потребуючи експортуванні символів взагалі. Експорт символів вам знадобиться в тому випадку, якщо інші модулі зможуть отримати з цього користь. Вам може знадобиться включення спеціальної директиви для запобігання експортування НЕ static символів, тому що в більшості реалізацій утиліти modutils всі вони експортуються за умовчанням.

Заголовки ядра Linux пропонують зручний спосіб для управління видимістю ваших символів запобігаючи, таким чином, забруднення простору імен таблиці символів ядра. Механізм описаний в цьому розділі працює в ядрах починаючи з версії 2.1.18. Ядро 2.0 мало зовсім інший механізм управління
видимості символів, який буде описаний в кінці розділу.

Якщо ваш модуль не повинен експортувати символи взагалі, ви можете явно розмістити наступний макровиклик в файлі джерелі модуля:

EXPORT_NO_SYMBOLS;

Цей макровиклик, визначених у файлі linux / module.h розширюється в директиву асемблера і може бути зазначений в будь-якій точці модуля. Однак, при створенні коду портіруемость на різні ядра необхідно розміщувати цей макровиклик в ініціалізацій функції модуля (init_module), тому що версія цього макроозначення певна нами в нашому файлі sysdep.h для старих версій ядра буде працювати тільки тут.

З іншого боку, якщо вам необхідно експортувати якусь частину символів з вашого модуля, то необхідно використовувати макросімвол
EXPORT_SYMTAB. Цей макросімвол повинен бути визначений передвключенням заголовки module.h. Загальноприйнятою практикою є
визначення цього макросімвола через прапор -Dв Makefile.

якщо макросімвол EXPORT_SYMTABвизначено, то індивідуальні символи можна експортувати за допомогою пари макросів:

EXPORT_SYMBOL (name); EXPORT_SYMBOL_NOVERS (name);

Будь-який з цих двох макросів зробить даний символ доступним за межами модуля. Відмінність полягає в тому, що макрос EXPORT_SYMBOL_NOVERSекспортує символ без інформації про версії (див. розділ 11 "kmod and Advanced Modularization"). Для отримання більш докладної інформації
ознайомтеся з заголовним файлом , Хоча викладеного цілком достатньо для практичного використання
макросів.

Ініціалізація і завершення модулів

Як уже згадувалося, функція init_module () реєструє функціональні компоненти модуля в ядрі. Після такої реєстрації, для використовує модуль програми, будуть доступні точки входу в модуль через інтерфейс, що надається ядром.

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

У розділі 1 "Введення в драйвера пристроїв" була згадана класифікація основних типів пристроїв. Ви можете зареєструвати не тільки згадані там типи пристроїв, але і будь-які інші, аж до програмних абстракцій, таких як, наприклад, файли файлової системи / proc та ін. Все, що може працювати в ядрі через програмний інтерфейс драйвера може бути зареєстровано як драйвер.

Якщо ви хочете дізнатися більше про типи реєстрованих драйверів на прикладі вашого ядра, ви можете реалізувати пошук підрядка EXPORT_SYMBOL в джерелах ядра і знайти точки входу, пропоновані різними драйверами. Як правило функції реєстрації використовують у своєму імені префікс register_,
тому інший можливий шлях їх пошуку - пошук підрядка register_в файлі / proc / ksyms за допомогою утиліти grep. Як вже говорилося, в ядрі 2.6.x файл / proc / ksyms замінений на / proc / modules.

Обробка помилок в init_module

Якщо при ініціалізації модуля виникає будь-якого роду помилка, то ви повинні скасувати вже досконалу ініціалізацію перед зупинкою завантаження модуля. Помилка може виникнути, наприклад, через нестачу пам'яті в системі при розподілі структур даних. На жаль, таке може трапитися, і хороший програмний код повинен вміти обробляти такі ситуації.

Все, що було зареєстровано або розподілено до виникнення помилки в ініціалізацій функції init_module () необхідно скасувати або звільнити самостійно, тому що ядро ​​Linux не відстежує помилки ініціалізації і не скасовує, вже виконаний кодом модуля, позику та надання ресурсів. Якщо ви не відкотили, або не змогли відкотити виконану реєстрацію, то ядро ​​залишиться в нестабільному стані, і при повторному завантаженні модуля
ви не зможете повторити реєстрацію вже зареєстрованих елементів, і не зможете скасувати раніше зроблену реєстрацію, тому що в новому екземплярі функції init_module () ви не будете мати правильного значення адрес зареєстрованих функцій. Для відновлення колишнього стану системи буде потрібно використання різних складних трюків, і частіше це робиться простий перезавантаженням системи.

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

Наступний простий код, на прикладі фіктивних функцій реєстрації і її скасування, демонструє такий спосіб обробки помилок.

Int init_module (void) (int err; / * registration takes a pointer and a name * / err = register_this (ptr1, "skull"); if (err) goto fail_this; err = register_that (ptr2, "skull"); if (err) goto fail_that; err = register_those (ptr3, "skull"); if (err) goto fail_those; return 0; / * success * / fail_those: unregister_that (ptr2, "skull"); fail_that: unregister_this (ptr1, " skull "); fail_this: return err; / * propagate the error * /)

У цьому прикладі проводиться спроба реєстрації трьох компонентів модуля. Оператор goto використовується при виникненні помилки реєстрації та призводить до скасування реєстрації зареєстрованих компонентів перед зупинкою завантаження модуля.

Іншим прикладом використання оператора goto не ускладнювати читання коду є трюк з "запам'ятовуванням" успішно виконаних реєстраційних операцій модуля і виклик cleanup_module () з передачею цієї інформації при виникненні помилки. Функція cleanup_module () призначена для відкату виконаних ініціалізацій операцій і автоматично викликається при вивантаженні модуля. Значення яке повертає функція init_module () повинна
являти собою код помилки ініціалізації модуля. У ядрі Linux, код помилки являє собою негативне число з безлічі визначень зроблених в заголовки . Увімкніть цей заголовки в свій модуль для того, щоб використовувати символічну мнемоніку зарезервованих кодів помилок, таких як -ENODEV, -ENOMEM і т.п. Використання такої мнемоніки вважається хорошим стилем програмування. Однак слід зауважити, що деякі версії утиліт з пакета modutils неправильно обробляють повертаються коди помилок і видають повідомлення "Device busy"
у відповідь на цілу групу помилок абсолютно різного характеру, що повертаються функцією init_modules (). В останніх версіях пакета ця
прикра помилка була виправлена.

Код функції cleanup_module () для наведеного вище випадку може бути, наприклад, таким:

Void cleanup_module (void) (unregister_those (ptr3, "skull"); unregister_that (ptr2, "skull"); unregister_this (ptr1, "skull"); return;)

Якщо ваш код ініціалізації і завершення складніший, ніж описаний тут, то використання оператора goto може привести до важко читається тексту програми, тому що код завершення повинен бути повторений в функції init_module () з використанням безлічі міток для goto переходів. З цієї причини використовують більш хитрий прийом використання виклику функції cleanup_module () у функції init_module () з передачею інформації про обсяг успішної ініціалізації при виникненні помилки завантаження модуля.

Нижче наведено приклад такого написання функцій init_module () і cleanup_module (). У цьому прикладі використовуються глобально певні покажчики, що несуть інформацію про обсяг успішної ініціалізації.

Struct something * item1; struct somethingelse * item2; int stuff_ok; void cleanup_module (void) (if (item1) release_thing (item1); if (item2) release_thing2 (item2); if (stuff_ok) unregister_stuff (); return;) int init_module (void) (int err = -ENOMEM; item1 = allocate_thing (arguments); item2 = allocate_thing2 (arguments2); if (! item2 ||! item2) goto fail; err = register_stuff (item1, item2); if (! err) stuff_ok = 1; else goto fail; return 0; / * success * / fail: cleanup_module (); return err;)

Залежно від складності ініціалізацій операцій вашого модуля ви можете використовувати один з наведених тут способів контролю помилок ініціалізації модуля.

Лічильник використання модуля

Система містить лічильник використання кожного модуля для того, щоб визначити можливість безпечного вивантаження модуля. Системі потрібна ця інформація, тому що модуль не може бути вивантажений, якщо він ким небудь або чим небудь зайнятий - ви не можете видалити драйвер файлової системи, якщо ця файлова система примонтировать, або ви не можете вивантажити модуль символьного пристрою, якщо який-небудь процес використовує цей пристрій. В іншому випадку,
це може привести до краху системи - segmentation fault або kernel panic.

В сучасних ядрах, система може надати вам автоматичний лічильник використання модуля використовуючи механізм, який ми розглянемо в наступному розділі. Незалежно від версії ядра ви можете використовувати ручне управління даними лічильником. Так, код, який передбачається використовувати в старих версіях ядра повинен використовувати модель обліку використовуваного модуля побудовану на наступних трьох макросах:

MOD_INC_USE_COUNTЗбільшує лічильник використання поточного модуля MOD_DEC_USE_COUNTЗменшує лічильник використання поточного модуля MOD_IN_USEПовертає істину якщо лічильник використання даного модуля дорівнює нулю

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

Зауважте, що вам не потрібно перевіряти MOD_IN_USEв коді функції cleanup_module (), тому, що ця перевірка виконується автоматично до виклику cleanup_module () в системному виклику sys_delete_module (), який визначений в kernel / module.c.

Коректне управління лічильником використання модуля критично для стабільності системи. Пам'ятайте, що ядро ​​може вирішити вивантажити невикористаний модуль автоматично в будь-який час. Часта помилка в програмуванні модулів полягає в неправильному управлінні цим лічильником. Наприклад, у відповідь на якийсь запит, код модуля виконує деякі дії і при завершенні обробки збільшує лічильник використання модуля. Тобто такий програміст передбачає, що даний лічильник призначений для збору статистики використання модуля, в той час як, насправді, він є, фактично, лічильником поточної зайнятості модуля, тобто веде рахунок кількості процесів використовують код модуля в даний момент. Таким чином, при обробці запиту до модуля, ви повинні викликати MOD_INC_USE_COUNTперед виконанням будь-яких дій, і MOD_DEC_USE_COUNTпісля їх виконання.

Можливі ситуації, в яких, зі зрозумілих причин, ви не зможете вивантажити модуль якщо втратите управління лічильником його використання. Така ситуація часто зустрічається на етапі розробки модуля. Наприклад, процес може перерватися при спробі разименованія NULL покажчика, і ви не зможете вивантажити такий модуль, поки не повернете лічильник його використання до нуля. Одне з можливих рішень такої проблеми на етапі налагодження модуля полягає в повній відмові від управління лічильником використання модуля шляхом перевизначення MOD_INC_USE_COUNTі MOD_DEC_USE_COUNTв порожній код. Інше рішення полягає в створенні ioctl () виклику примусово скидає лічильник використання модуля в нуль. Ми розглянемо це в розділі "Using the ioctl Argument" в розділі 5 "Enhanced Char Driver Operations". Звичайно, в готовому для використання драйвера подібні обманні маніпуляції з лічильником повинні бути виключені, проте, на етапі налагодження, вони дозволяють заощадити час розробника і цілком припустимі.

Поточне значення системного лічильника використання кожного модуля ви знайдете в третьому полі кожного запису файлу / proc / modules. Цей файл містить інформацію про завантажених в даний момент модулях - по одному рядку на кожен модуль. Перше поле рядка містить назву модуля, друге поле - розмір займаний модулем в пам'яті, і третє поле - поточне значення лічильника використання. Цю інформацію, в отформатированном вигляді,
можна отримати викликом утиліти lsmod. Нижче наведено приклад файлу / proc / modules:

Parport_pc 7604 1 (autoclean) lp 4800 0 (unused) parport 8084 1 lockd 33256 1 (autoclean) sunrpc 56612 1 (autoclean) ds 6252 1 i82365 22304 1 pcmcia_core 41280 0

Тут ми бачимо кілька модулів, завантажених в систему. В поле прапорів (останнє поле рядка), в квадратних дужках відображено стек залежності модулів. Серед іншого можна помітити, що модулі паралельного порту взаємодіють через стек модулів, як показано на рис. 2-2. Прапором (autoclean) позначені модулі керовані kmod або kerneld. Про це йтиметься у розділі 11 "kmod and Advanced Modularization"). Прапор (unused) означає, що модуль не використовується в даний момент. В ядрі 2.0 поле розміру відображала інформацію не в байтах, а в сторінках, розмір якої для більшості платформ становить 4кБт.

вивантаження модуля

Дpя вивантаження модуля використовуйте утиліту rmmod. Вивантаження модуля більш просте завдання ніж його завантаження, при якій виконується його динамічна лінковка з ядром. При вивантаженні модуля виконується системний виклик delete_module (), який або виконує виклик функції cleanup_module () вивантажується модуля в разі, якщо його лічильник використання дорівнює нулю, або припиняє роботу з помилкою.

Як вже говорилося, в функції cleanup_module () виконується відкат ініціалізацій операцій виконаних при завантаженні модуля функцією cleanup_module (). Також, автоматично видаляється експортованих символів модуля.

Явна визначення функцій завершення і ініціалізації

Як вже говорилося, при завантаженні модуля ядро ​​викликає функцію init_module (), а при вивантаженні - cleanup_module (). Однак, в сучасних версіях ядра ці функції часто мають інші імена. Починаючи з ядра 2.3.23 з'явилася можливість явного визначення імені для функції завантаження і вивантаження модуля. Зараз, таке явне визначення імен для цих функцій є рекомендум стилем програмування.

Наведемо приклад. Якщо ви хочете оголосити ініціалізацій функцією вашого модуля функцію my_init (), а завершальній - функцію my_cleanup (), замість init_module () і cleanup_module () відповідно, то вам необхідно буде додати наступні два макроси з тексту модуля (зазвичай їх вставляють в кінець
файлу джерела коду модуля):

Module_init (my_init); module_exit (my_cleanup);

Зауважте, що для використання цих макроозначень ви повинні будете включити в ваш модуль заголовки .

Зручність використання такого стилю полягає в тому, що кожна функція ініціалізації і завершення модулів в ядрі може мати своє унікальне ім'я, що значно допомагає в налагодженні. Причому, використання цих функцій спрощують налагодження незалежно від того, реалізуєте ви код вашого драйвера у вигляді модуля, або ж збираєтеся вбудовувати його прямо в ядро. Звичайно ж, використання макроозначень module_init і module_exit не потрібно, якщо ваші функції ініціалізації і завершення мають зарезервовані імена, тобто init_module () і cleanup_module () соответствено.

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

Static int __init my_init (void) (....) static void __exit my_cleanup (void) (....)

Використання атрибута __initпризведе до того, що після завершення ініціалізації, ініціалізації функція будуть вивантажено з пам'яті. Однак це працює тільки для вбудованих в ядро ​​драйверів, і буде проігноровано для модулів. Також, для драйверів вбудованих в ядро, атрибут __exitпризведе до ігнорування цілої функції з позначкою цим атрибутом. Для модулів цей прапор, також буде проігнорований.

Використання атрибутів __init__initdataдля опису даних) може зменшити кількість пам'яті використовуваної ядром. позначка прапором __initініціалізацій функції модуля не принесе ні вигоди ні шкоди. Управління таким способом ініціалізації ще не реалізовано для модулів, хоча, можливо, це буде зроблено в майбутньому.

Підведення підсумків

Отже, в результаті представленого матеріалу ми можемо уявити такий варіант "Hello world" модуля:

Код файлу джерела модуля ============================================== #include #include #include static int __init my_init_module (void) (EXPORT_NO_SYMBOLS; printk ( "<1>Hello world \ n "); return 0;); static void __exit my_cleanup_module (void) (printk ("<1>Good bye \ n ");); module_init (my_init_module); module_exit (my_cleanup_module); MODULE_LICENSE (" GPL "); ======================== ===================== Makefile для компіляції модуля ========================= ==================== CFLAGS = -Wall -D__KERNEL__ -DMODULE -I / lib / modules / $ (shell uname -r) / build / include hello.o: =============================================

Зверніть увагу, що при написанні Makefile ми використовували угоду про здатність утиліти GNU make самостійно визначити спосіб формування об'єктного файлу на основі змінної CFLAGS і наявного в системі компілятора.

Використання ресурсів

Модуль не може виконати своє завдання без використання системних ресурсів, таких як пам'ять, порти вводу / виводу, пам'ять введення / виведення, лінії переривання, а також, канали DMA.

Як програміст, ви вже повинні бути знайомі з керуванням динамічною пам'яттю. Управління динамічної пам'яттю в ядрі не має принципових відмінностей. Ваша програма може отримати пам'ять використовуючи функцію kmalloc ()і звільнити її, за допомогою kfree (). Ці функції дуже схожі на знайомі вам malloc () і free (), за тим винятком, що в функцію kmalloc () передається додатковий аргумент - пріоритет. Зазвичай пріоритет приймає значення GFP_KERNEL або GFP_USER. GFP є акронім від "get free page" - взяти вільну сторінку. Управління динамічної пам'яттю в ядрі детально викладається в розділі 7 "Getting Hold of Memory".

Початківець розробник драйверів може бути здивований необхідністю явного розподілу портів введення / виводу, пам'яті введення / виводу і ліній переривань. Тільки після цього, модуль ядра може отримати простий доступ до цих ресурсів. І хоча системна пам'ять може бути розподілена звідки завгодно, пам'ять введення / виведення, порти і лінії переривання відіграють особливу роль і розподіляються інакше. Для прикладу, драйверу необхідно розподілити певні порти, які не
все, а ті, які йому потрібні для управління пристроєм. Але драйвер не може використовувати ці ресурси до тих пір, поки не переконається, що вони не використовуються кимось ще.

Область пам'яті належить периферійних пристроїв зазвичай називається пам'яттю вводу / виводу, для того щоб відрізняти її від системного ОЗУ (RAM), звану просто пам'яттю.

Порти і пам'ять введення / виведення

Робота звичайного драйвера здебільшого складається з читання і запису портів і пам'яті вводу / виводу. Порти і пам'ять введення / виведення об'єднані загальною назвою - регіон (або область) введення / виводу.

На жаль, не на кожній шинної архітектури можна ясно визначити регіон введення / виведення належить кожному пристрою, і можливо, що драйверу доведеться припускати розміщення належного йому регіону, або, навіть, пробувати операції читання / запису можливих адресних просторів. Ця проблема особливо
відноситься до шини ISA, яка ще досі використовується для установки простих пристроїв в персональний комп'ютер і дуже популярна в індустріальному світі в реалізації PC / 104 (див. розділ "PC / 104 і PC / 104 +" глави 15 "Огляд периферійних шин" ).

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

Розробники Linux реалізували механізм запиту / вивільнення регіонів введення / виведення головним чином для запобігання колізій між різними пристроями. Цей механізм давно використовується для портів введення / виводу і був недавно узагальнено на механізм управління ресурсами взагалі. Зауважте, що цей механізм представляє програмну абстракцію і не поширюється на апаратні можливості. Наприклад, неавторизований доступ до портів введення / виводу на рівні апаратури не викликає будь-яку помилку аналогічну "segmentation fault", так як апаратура не займається виділенням і авторизацією своїх ресурсів.

Інформація про зареєстровані ресурсах доступна в текстовій формі в файлах / proc / ioports і / proc / iomem. Ця інформація представлена ​​в Linux починаючи з ядра 2.3. Нагадаємо, що дана книга присвячена переважно ядру 2.4, і зауваження про сумісність будуть представлені в кінці розділу.

порти

Нижче представлено типове вміст файлу / proc / ioports:

0000-001f: dma1 0020-003f: pic1 0040-005f: timer 0060-006f: keyboard 0080-008f: dma page reg 00a0-00bf: pic2 00c0-00df: dma2 00f0-00ff: fpu 0170-0177: ide1 01f0-01f7 : ide0 02f8-02ff: serial (set) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga + 03f6-03f6: ide0 03f8-03ff: serial (set) 1000-103f: Intel Corporation 82371AB PIIX4 ACPI 1000-1003 : acpi 1004-1005: acpi 1008-100b: acpi 100c-100f: acpi 1100-110f: Intel Corporation 82371AB PIIX4 IDE 1300-131f: pcnet_cs 1400-141f: Intel Corporation 82371AB PIIX4 ACPI 1800-18ff: PCI CardBus # 02 1c00- 1cff: PCI CardBus # 04 5800-581f: Intel Corporation 82371AB PIIX4 USB d000-dfff: PCI Bus # 01 d000-d0ff: ATI Technologies Inc 3D Rage LT Pro AGP-133

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

Файл може бути використаний для уникнення колізій портів при додаванні в систему нового пристрою. Особливо це зручно при ручному налаштуванні встановленого обладнання шляхом перемикання перемичок (jampers - джамперів). У цьому випадку користувач може легко подивитися список використовуваних портів і вибрати вільний діапазон для встановлюваного пристрою. І хоча більшість сучасних пристроїв не використовують перемичок ручної настройки взагалі, тим не менш, вони ще використовуються при виготовленні дрібносерійних компонентів.

Що ще більш важливо, так це те, що з файлом / proc / ioports пов'язана структура даних, доступна програмним шляхом. Тому, коли драйвер пристрою виконує ініціалізацію він може дізнатися зайнятий діапазон портів введення / виводу. Значить, при необхідності просканувати порти в пошуках нового пристрою, драйвер в змозі уникнути ситуації записи в порти, зайняті чужими пристроями.

Відомо, що сканування шини ISA є ризикованою завданням. Тому деякі драйвера, поширювані з офіційним Linux ядром, уникають такого сканування при завантаженні модуля. Тим самим, вони уникають ризику пошкодження запущеної системи за рахунок записи в порти, використовувані іншим обладнанням. На щастя, сучасні архітектури шин несприйнятливі до цих проблем.

Програмний інтерфейс використовується для доступу до регістрів введення / виводу складається з наступних трьох функцій:

Int check_region (unsigned long start, unsigned long len); struct resource * request_region (unsigned long start, unsigned long len, char * name); void release_region (unsigned long start, unsigned long len);

функція check_region ()може бути викликана для перевірки зайнятості заданого діапазону портів. Вона повертає негативний код помилки (такий як -EBUSY або -EINVAL) при негативній відповіді.

функція request_region ()виконує розподіл заданого діапазону адрес повертаючи, в разі успіху, ненульовий покажчик. Драйверу немає потреби зберігати або використовувати повернутий покажчик. Все, що необхідно зробити, це зробити його перевірку на NULL. Код який повинен працювати тільки з ядром 2.4 (або вище) взагалі не потребує виклику функції check_region (). Не підлягає сумніву перевага такого способу розподілу, тому що
невідомо, що може статися між викликами функцій check_region () і request_region (). Якщо ж ви хочете зберегти сумісність зі старими версіями ядра, то виклик check_region () перед request_region () необхідний.

функція release_region ()повинна бути викликана при звільненні драйвером раніше використовуваних портів.

Справжнє значення покажчика повертається функцією request_region () використовується тільки підсистемою виділення ресурсів, що працює в ядрі.

Ці три функції, в дійсності, є макросами певними в .

Нижче наведено приклад використання послідовності викликів, що застосовується для реєстрації портів. Приклад взятий з коду навчального драйвера skull. (Тут не показаний код функції skull_probe_hw (), тому що вона містить апаратно-залежний код.)

#include #include static int skull_detect (unsigned int port, unsigned int range) (int err; if ((err = check_region (port, range))< 0) return err; /* busy */ if (skull_probe_hw(port,range) != 0) return -ENODEV; /* not found */ request_region(port,range,"skull"); /* "Can"t fail" */ return 0; }

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

Будь-які порти введення / виводу розподілені драйвером повинні бути згодом звільнені. Наш драйвер skull робить це в функції cleanup_module ():

Static void skull_release (unsigned int port, unsigned int range) (release_region (port, range);)

Механізм запиту / вивільнення ресурсів схожий на механізм реєстрації / дерегістраціі модулів і відмінно реалізується на основі описаної вище схемою використання оператора goto.

пам'ять

Інформація про пам'ять введення / виведення доступна через файл / proc / iomem. Нижче наведено типовий приклад такого файлу для персонального комп'ютера:

00000000-0009fbff: System RAM 0009fc00-0009ffff: reserved 000a0000-000bffff: Video RAM area 000c0000-000c7fff: Video ROM 000f0000-000fffff: System ROM 00100000-03feffff: System RAM 00100000-0022c557: Kernel code 0022c558-0024455f: Kernel data 20000000- 2fffffff: Intel Corporation 440BX / ZX - 82443BX / ZX Host bridge 68000000-68000fff: Texas Instruments PCI1225 68001000-68001fff: Texas Instruments PCI1225 (# 2) e0000000-e3ffffff: PCI Bus # 01 e4000000-e7ffffff: PCI Bus # 01 e4000000-e4ffffff : ATI Technologies Inc 3D Rage LT Pro AGP-133 e6000000-e6000fff: ATI Technologies Inc 3D Rage LT Pro AGP-133 fffc0000-ffffffff: reserved

Значення діапазонів адрес показані в шістнадцятковій запису. Для кожного діапазону Арес показаний його власник.

Реєстрація доступу до пам'яті введення / виводу схожа на реєстрацію портів введення / виводу і побудована в ядрі на тому ж самому механізмі.

Для отримання і вивільнення необхідного діапазону адрес пам'яті введення / виводу, драйвер повинен використовувати такі виклики:

Int check_mem_region (unsigned long start, unsigned long len); int request_mem_region (unsigned long start, unsigned long len, char * name); int release_mem_region (unsigned long start, unsigned long len);

Зазвичай, драйверу відомий діапазон адрес пам'яті введення / виводу, тому код розподілу даного ресурсу може бути зменшений, порівняно з прикладом для розподілу діапазону портів:

If (check_mem_region (mem_addr, mem_size)) (printk ( "drivername: memory already in use \ n"); return -EBUSY;) request_mem_region (mem_addr, mem_size, "drivername");

Розподіл ресурсів в Linux 2.4

Поточний механізм розподілу ресурсів був введений в ядрі Linux 2.3.11 і забезпечує гнучкий доступ управління системними ресурсами. У цьому розділі коротко описано даний механізм. Однак, функції базового розподілу ресурсів (такі як request_region () і ін.) Ще поки реалізовані у вигляді макросів і використовуються для забезпечення сумісності з ранніми версіями ядра. У більшості випадків не потрібно нічого знати про реальний механізм розподілу, але це може бути цікаво при створенні більш складних драйверів.

Система управління ресурсами реалізована в Linux може керувати довільними ресурсами в єдиній ієрархічній манері. Глобальні ресурси системи (наприклад, порти введення / виводу) можуть бути поділені на підмножини - наприклад відносяться до якого-небудь слоту апаратної шини. Певні драйвери, також, при бажанні, можуть поділяти захоплювані ресурси на основі своєї логічної структури.

Діапазон виділених ресурсів описується через структуру struct resource, яка оголошена в заголовки :

Struct resource (const char * name; unsigned long start, end; unsigned long flags; struct resource * parent, * sibling, * child;);

Глобальний (кореневої) діапазон ресурсів створюється під час завантаження. Наприклад, структура ресурсів, що описує порти введення / виводу створюється наступним чином:

Struct resource ioport_resource = ( "PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO);

Тут описаний ресурс з ім'ям PCI IO, який покриває діапазон адрес від нуля до IO_SPACE_LIMIT. Значення цієї змінної залежить від використовуваної платформи і може дорівнювати 0xFFFF (16-ти бітове адресний простір, для архітектур x86, IA-64, Alpha, M68k і MIPS), 0xFFFFFFFF (32-х бітове простір, для SPARC, PPC, SH) або 0xFFFFFFFFFFFFFFFF (64-х бітове, SPARC64).

Піддіапазони цього ресурсу можуть бути створені за допомогою виклику allocate_resource (). Наприклад, під час ініціалізації PCI шини, для регіону адрес цієї шини, створюється новий ресурс, який призначається фізичному пристрою. Коли код ядра керуючий шиною PCI обробляє призначення портів і пам'яті, він створює новий ресурс тільки для цих регіонів і розподіляє їх за допомогою викликів ioport_resource () або iomem_resource ().

Драйвер може потім запросити підмножина нікого ресурсу (зазвичай частина глобального ресурсу) і позначити його як зайнятий. Захоплення ресурсу здійснюється викликом request_region (), що повертає або покажчик на нову структуру struct resource, яка описує запитуваний ресурс, або NULL в разі помилки. Ця структура є частиною глобального дерева ресурсів. Як вже говорилося, після отримання ресурсу, драйверу не знадобиться значення цього покажчика.

Цікавиться читач може отримати задоволення від перегляду деталей цієї схеми управління ресурсами в файлі kernel / resource.c, розташованому в каталозі джерел ядра. Однак, більшості розробників буде досить уже викладених знань.

Шаровий механізм розподілу ресурсів приносить подвійну вигоду. З одного боку, він дає наочне уявлення про структури даних ядра. Ще раз звернемося до прикладу файлу / proc / ioports:

E800-e8ff: Adaptec AHA-2940U2 / W / 7890 e800-e8be: aic7xxx

Діапазон e800-e8ff розподілений для адаптера Adaptec, який позначив себе як драйвер на шині PCI. Велику частину цього діапазону запросив драйвер aic7xxx.

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

Автоматичне і ручне конфігурування

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

Ці параметри, що впливають на роботу драйвера, залежать від пристрою. Наприклад, це може бути номер версії встановленого пристрою. Звичайно, така інформація необхідна драйверу для правильної роботи з пристроєм. Визначення таких параметрів (конфігурація драйвера) являє собою досить
хитру завдання, що виконується при ініціалізації драйвера.

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

Багато драйвера, серед своїх конфігураційних параметрів, мають параметри керуючі операціями драйвера. Наприклад, драйвера IDE інтерфейсу (Integrated Device Electronics) дозволяють користувачеві управляти операціями DMA. Таким чином, якщо ваш драйвер добре виконує автоопределение обладнання, можливо, ви захочете надати користувачеві можливість управління функціональністю драйвера.

Значення параметрів можуть бути передані в процесі завантаження модуля командами insmod або modprobe. Останнім часом стало можливим читати значення параметрів з конфігураційного файлу (зазвичай /etc/modules.conf). Як параметри можна передавати цілі і строкові значення. Таким чином, якщо вам необхідно зрадити ціле значення параметра skull_ival і строкове значення параметра skull_sval, ви можете передати їх під час завантаження модуля додатковими параметрами команди insmod:

Insmod skull skull_ival = 666 skull_sval = "the beast"

Однак, перш ніж команда insmod може змінити значення параметрів модуля, модуль повинен зробити ці параметри доступними. Параметри оголошуються за допомогою макроозначення MODULE_PARM, яке визначено в заголовки module.h. Макро MODULE_PARM приймає два параметри: ім'я змінної і рядок, що визначає її тип. Дане макровизначеннями має бути розміщено за межами будь-яких функцій і зазвичай розташовується на початку файлу після визначення змінних. Так, два згаданих вище параметра, можуть бути оголошені в такий спосіб:

Int skull_ival = 0; char * skull_sval; MODULE_PARM (skull_ival, "i"); MODULE_PARM (skull_sval, "s");

На даний момент підтримуються п'ять типів параметрів модуля:

  • b - однобайтового величина;
  • h - (short) двухбайтовая величина;
  • i - ціле;
  • l - довге ціле;
  • s - рядок (char *);

У разі строкових параметрів, в модулі повинен бути оголошений покажчик (char *). Команда insmod розподіляє пам'ять для переданої рядки і ініціалізує її необхідним значенням. За допомогою макро MODULE_PARM можна форматувати масиви параметрів. В цьому випадку, ціле число, що передує літері типу визначає довжину масиву. При вказівці двох цілих чисел розділених знаком тире, вони визначають мінімальну та максимальну кількість переданих значень. Для більш детального розуміння роботи даного макроозначення зверніться до заголовків файлів .

Наприклад, нехай масив параметрів слід буде почати не менше ніж двома, і не менше ніж чотирма цілими значеннями. Тоді він може бути описаний таким чином:

Int skull_array; MODULE_PARM (skull_array, "2-4i");

Крім цього, в інструментарії програміста є макроозначень MODULE_PARM_DESC, яке дозволяє поміщати коментарі до переданих параметрів модуля. Ці коментарі зберігаються в об'єктному файлі модуля і можуть бути переглянуті за допомогою, наприклад, утиліти objdump, або за допомогою автоматизованих інструментів адміністрування системи. Наведемо приклад використання даного макроозначення:

Int base_port = 0x300; MODULE_PARM (base_port, "i"); MODULE_PARM_DESC (base_port, "The base I / O port (default 0x300)");

Бажано, щоб всі параметри модуля мали значення за замовчуванням. Зміна цих значень за допомогою insmod має турбуватися тільки в разі потреби. Модуль може перевірити явне завдання параметрів перевіривши їх поточні значення зі значеннями за замовчуванням. Згодом ви можете реалізувати механізм автоконфігурірованія на основі такої схеми. Якщо значення параметрів мають значення за замовчуванням, то виконується Автоконфігурірованіе. В іншому випадку - використовуються поточні значення. Для того, щоб дана схема працювала, необхідно, щоб значення за замовчуванням не відповідали ніякої з можливих реальних змін системи. Тоді можна буде припустити, що такі значення не можуть бути встановлені користувачем в ручному конфігуруванні.

Наступний приклад показує як драйвер skull виробляє автоопределение адресного простору портів пристрою. У наведеному прикладі, в автовизначенням використовується перегляд безлічі пристроїв, в той час як при ручному конфігуруванні драйвер обмежується одним пристроєм. З функцією skull_detect ви вже зустрічалися раніше в розділі опису портів введення / виводу. Реалізація функції skull_init_board () не відображено, так як вона
проводить апаратно-залежну ініціалізацію.

/ * * Port ranges: the device can reside between * 0x280 and 0x300, in steps of 0x10. It uses 0x10 ports. * / #Define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 / * * the following function performs autodetection, unless a specific * value was assigned by insmod to "skull_port_base" * / static int skull_port_base = 0; / * 0 forces autodetection * / MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC (skull_port_base, "Base I / O port for skull"); static int skull_find_hw (void) / * returns the # of devices * / (/ * base is either the load-time value or the first trial * / int base = skull_port_base? skull_port_base: SKULL_PORT_FLOOR; int result = 0; / * loop one time if value assigned; try them all if autodetecting * / do (if (skull_detect (base, SKULL_PORT_RANGE) == 0) (skull_init_board (base); result ++;) base + = SKULL_PORT_RANGE; / * prepare for next trial * /) while (skull_port_base == 0 && base< SKULL_PORT_CEIL); return result; }

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

Для повноти опису ми наведемо опис ще трьох макроозначень, що дозволяють розміщувати деякі коментарі в об'єктному файлі.

MODULE_AUTHOR (name)Розміщує рядок з ім'ям автора в об'єктному файлі. MODULE_DESCRIPTION (desc)Розміщує рядок із загальним описом до модуля в об'єктному файлі. MODULE_SUPPORTED_DEVICE (dev)Розміщує рядок, з описом підтримуваного модулем пристрою. У Linux надає потужний і великий API для додатків, але іноді його недостатньо. Для взаємодії з обладнанням або здійснення операцій з доступом до привілейованої інформації в системі потрібен драйвер ядра.

Модуль ядра Linux - це скомпільований двійковий код, який вставляється безпосередньо в ядро ​​Linux, працюючи в кільці 0, внутрішньому і найменш захищеному кільці виконання команд в процесорі x86-64. Тут код виповнюється абсолютно без всяких перевірок, але зате на неймовірній швидкості і з доступом до будь-яких ресурсів системи.

Чи не для простих смертних

Написання модуля ядра Linux - заняття не для людей зі слабкими нервами. Змінюючи ядро, ви ризикуєте втратити дані. У коді ядра немає стандартної захисту, як в звичайних додатках Linux. Якщо зробити помилку, то повісите всю систему.

Ситуація погіршується тим, що проблема не обов'язково проявляється відразу. Якщо модуль вішає систему відразу після завантаження, то це найкращий сценарій збою. Чим більше там коду, тим вище ризик нескінченних циклів і витоків пам'яті. Якщо ви необережні, то проблеми стануть поступово наростати в міру роботи машини. Зрештою важливі структури даних і навіть буфера можуть бути перезаписані.

Можна в основному забути традиційні парадигми розробки додатків. Крім завантаження і вивантаження модуля, ви будете писати код, який реагує на системні події, а не працює по послідовному шаблоном. При роботі з ядром ви пишете API, а не самі додатки.

У вас також немає доступу до стандартної бібліотеці. Хоча ядро ​​надає деякі функції на кшталт printk (яка служить заміною printf) і kmalloc (працює схоже на malloc), в основному ви залишаєтеся наодинці з залізом. До того ж, після вивантаження модуля слід повністю почистити за собою. Тут немає збірки сміття.

необхідні компоненти

Перш ніж почати, слід переконатися в наявності всіх необхідних інструментів для роботи. Найголовніше, потрібна машина під Linux. Знаю, це несподівано! Хоча підійде будь-який дистрибутив Linux, в цьому прикладі я використовую Ubuntu 16.04 LTS, так що в разі використання інших дистрибутивів може знадобитися злегка змінити команди установки.

По-друге, потрібна або окрема фізична машина, або віртуальна машина. Особисто я вважаю за краще працювати на віртуальній машині, але вибирайте самі. Не раджу використовувати свою основну машину через втрату даних, коли зробите помилку. Я кажу «коли», а не «якщо», тому що ви обов'язково підвісити машину хоча б кілька разів на процесі. Ваші останні зміни в коді можуть ще знаходитися в буфері записи в момент паніки ядра, так що можуть пошкодитися і ваші вихідні. Тестування в віртуальній машині усуває ці ризики.

І нарешті, потрібно хоча б трохи знати C. Робоче середовище C ++ занадто велика для ядра, так що необхідно писати на чистому голом C. Для взаємодії з обладнанням не завадить і деяке знання асемблера.

Установка середовища розробки

На Ubuntu потрібно запустити:

Apt-get install build-essential linux-headers-`uname -r`
Встановлюємо найважливіші інструменти розробки і заголовки ядра, необхідні для даного прикладу.

Приклади нижче припускають, що ви працюєте з-під звичайного користувача, а не рута, але що у вас є привілеї sudo. Sudo необхідна для завантаження модулів ядра, але ми хочемо працювати по можливості за межами рута.

починаємо

Приступимо до написання коду. Підготуємо нашу середу:

Mkdir ~ / src / lkm_example cd ~ / src / lkm_example
Запустіть улюблений редактор (в моєму випадку це vim) і створіть файл lkm_example.c такого змісту:

#include #include #include MODULE_LICENSE ( "GPL"); MODULE_AUTHOR ( "Robert W. Oliver II"); MODULE_DESCRIPTION ( "A simple example Linux module."); MODULE_VERSION ( "0.01"); static int __init lkm_example_init (void) (printk (KERN_INFO "Hello, World! \ n"); return 0;) static void __exit lkm_example_exit (void) (printk (KERN_INFO "Goodbye, World! \ n");) module_init (lkm_example_init ); module_exit (lkm_example_exit);
Ми сконструювали найпростіший можливий модуль, розглянемо докладніше найважливіші його частини:

  • У include перераховані файли заголовків, необхідні для розробки ядра Linux.
  • У MODULE_LICENSE можна встановити різні значення, залежно від ліцензії модуля. Для перегляду повного списку запустіть:

    Grep "MODULE_LICENSE" -B 27 / usr / src / linux-headers-`uname -r` / include / linux / module.h

  • Ми встановлюємо init (завантаження) і exit (вивантаження) як статичні функції, які повертають цілі числа.
  • Зверніть увагу на використання printk замість printf. Також параметри printk відрізняються від printf. Наприклад, прапор KERN_INFO для оголошення пріоритету журналирования для конкретної рядки вказується без коми. Ядро розбирається з цими речами всередині функції printk для економії пам'яті стека.
  • В кінці файлу можна викликати module_init і module_exit і вказати функції завантаження і вивантаження. Це дає можливість довільного іменування функцій.
Втім, поки ми не можемо скомпілювати цей файл. Потрібен Makefile. Такого базового прикладу поки достатньо. Зверніть увагу, що make дуже вибагливий до прогалин і табам, так що переконаєтеся, що використовуєте таби замість пробілів де належить.

Obj-m + = lkm_example.o all: make -C / lib / modules / $ (shell uname -r) / build M = $ (PWD) modules clean: make -C / lib / modules / $ (shell uname -r ) / build M = $ (PWD) clean
Якщо ми запускаємо make, він повинен успішно скомпілювати наш модуль. Результатом стане файл lkm_example.ko. Якщо вискакують якісь помилки, перевірте, що лапки в вихідному коді встановлені коректно, а не випадково в кодуванні UTF-8.

Тепер можна впровадити модуль і перевірити його. Для цього запускаємо:

Sudo insmod lkm_example.ko
Якщо все нормально, то ви нічого не побачите. Функція printk забезпечує видачу не в консоль, а в журнал ядра. Для перегляду потрібно запустити:

Sudo dmesg
Ви повинні побачити рядок "Hello, World!" з міткою часу на початку. Це означає, що наш модуль ядра завантажився і успішно зробив запис у журнал ядра. Ми можемо також перевірити, що модуль ще в пам'яті:

Lsmod | grep "lkm_example"
Для видалення модуля запускаємо:

Sudo rmmod lkm_example
Якщо ви знову запустіть dmesg, то побачите в журналі запис "Goodbye, World!". Можна знову запустити lsmod і переконатися, що модуль вивантажився.

Як бачите, ця процедура тестування трохи втомлює, але її можна автоматизувати, додавши:

Test: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
в кінці Makefile, а потім запустивши:

Make test
для тестування модуля і перевірки видачі в журнал ядра без необхідності запускати окремі команди.

Тепер у нас є повністю функціональний, хоча і абсолютно тривіальний модуль ядра!

Копнём трохи глибше. Хоча модулі ядра здатні виконувати всі види завдань, взаємодія з додатками - один з найпоширеніших варіантів використання.

Оскільки додатків заборонено переглядати пам'ять в просторі ядра, для взаємодії з ними доводиться використовувати API. Хоча технічно є кілька способів такого взаємодії, найбільш звичний - створення файлу пристрою.

Ймовірно, раніше ви вже мали справу з файлами пристроїв. Команди зі згадуванням / dev / zero, / dev / null і тому подібного взаємодіють з пристроями "zero" і "null", які повертають очікувані значення.

У нашому прикладі ми повертаємо "Hello, World". Хоча це не дуже корисна функція для додатків, вона все одно демонструє процес взаємодії з додатком через файл пристрою.

Ось повний лістинг:

#include #include #include #include #include MODULE_LICENSE ( "GPL"); MODULE_AUTHOR ( "Robert W. Oliver II"); MODULE_DESCRIPTION ( "A simple example Linux module."); MODULE_VERSION ( "0.01"); #define DEVICE_NAME "lkm_example" #define EXAMPLE_MSG "Hello, World! \ n" #define MSG_BUFFER_LEN 15 / * Prototypes for device functions * / static int device_open (struct inode *, struct file *); static int device_release (struct inode *, struct file *); static ssize_t device_read (struct file *, char *, size_t, loff_t *); static ssize_t device_write (struct file *, const char *, size_t, loff_t *); static int major_num; static int device_open_count = 0; static char msg_buffer; static char * msg_ptr; / * This structure points to all of the device functions * / static struct file_operations file_ops = (.read = device_read, .write = device_write, .open = device_open, .release = device_release); / * When a process reads from our device, this gets called. * / Static ssize_t device_read (struct file * flip, char * buffer, size_t len, loff_t * offset) (int bytes_read = 0; / * If we're at the end, loop back to the beginning * / if (* msg_ptr = = 0) (msg_ptr = msg_buffer;) / * Put data in the buffer * / while (len && * msg_ptr) (/ * Buffer is in user data, not kernel, so you can not just reference * with a pointer. The function put_user handles this for us * / put_user (* (msg_ptr ++), buffer ++); len--; bytes_read ++;) return bytes_read;) / * Called when a process tries to write to our device * / static ssize_t device_write (struct file * flip, const char * buffer, size_t len, loff_t * offset) (/ * This is a read-only device * / printk (KERN_ALERT "This operation is not supported. \ n"); return -EINVAL;) / * Called when a process opens our device * / static int device_open (struct inode * inode, struct file * file) (/ * If device is open, return busy * / if (device_open_count) (return -EBUSY;) device_open_count ++; try_module_get (THIS_MODULE);< 0) { printk(KERN_ALERT “Could not register device: %d\n”, major_num); return major_num; } else { printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num); return 0; } } static void __exit lkm_example_exit(void) { /* Remember - we have to clean up after ourselves. Unregister the character device. */ unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO “Goodbye, World!\n”); } /* Register module functions */ module_init(lkm_example_init); module_exit(lkm_example_exit);

Тестування поліпшеного прикладу

Тепер наш приклад робить щось більше, ніж просто висновок повідомлення при завантаженні і вивантаженні, так що знадобиться менше сувора процедура тестування. Змінимо Makefile тільки для завантаження модуля, без його вивантаження.

Obj-m + = lkm_example.o all: make -C / lib / modules / $ (shell uname -r) / build M = $ (PWD) modules clean: make -C / lib / modules / $ (shell uname -r ) / build M = $ (PWD) clean test: # We put a - in front of the rmmod command to tell make to ignore # an error in case the module is not loaded. -sudo rmmod lkm_example # Clear the kernel log without echo sudo dmesg -C # Insert the module sudo insmod lkm_example.ko # Display the kernel log dmesg
Тепер після запуску make test ви побачите видачу старшого номера пристрою. У нашому прикладі його автоматично присвоює ядро. Однак цей номер потрібен для створення нового пристрою.

Візьміть номер, отриманий в результаті виконання make test, і використовуйте його для створення файлу пристрою, щоб можна було встановити комунікацію з нашим модулем ядра з простору користувача.

Sudo mknod / dev / lkm_example c MAJOR 0
(В цьому прикладі замініть MAJOR значенням, отриманим в результаті виконання make test або dmesg)

Параметр c в команді mknod говорить mknod, що нам потрібно створити файл символьного пристрою.

Тепер ми можемо отримати вміст із пристрою:

Cat / dev / lkm_example
або навіть через команду dd:

Dd if = / dev / lkm_example of = test bs = 14 count = 100
Ви також можете отримати доступ до цього файлу з додатків. Це необов'язково повинні бути скомпільовані додатки - навіть у скриптів Python, Ruby і PHP є доступ до цих даних.

Коли ми закінчили з пристроєм, видаляємо його і вивантажуємо модуль:

Sudo rm / dev / lkm_example sudo rmmod lkm_example

висновок

Сподіваюся, вам сподобалися наші пустощі в просторі ядра. Хоча показані приклади примітивні, ці структури можна використовувати для створення власних модулів, що виконують дуже складні завдання.

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

Підтримайте проект - поділіться посиланням, спасибі!
Читайте також
Сбебранк (він же Ощадбанк) Сбебранк (він же Ощадбанк) Рішення проблем з ліцензією у Autocad Чи не запускається autocad windows 7 Рішення проблем з ліцензією у Autocad Чи не запускається autocad windows 7 Інструкція з використання PIN коду КріптоПро, під час підписання кількох документів - Інструкції - АТ Інструкція з використання PIN коду КріптоПро, під час підписання кількох документів - Інструкції - АТ "ПЕК-Торг"