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

Навіщо взагалі самому компілювати ядро?
Мабуть, головне питання, яке ставлять із приводу компіляції ядра: "А навіщо мені це робити?".
Багато хто вважає це безглуздою тратою часу для того, щоб показати себе розумним і просунутим "лінуксоїдом". Насправді компіляція ядра це дуже важлива справа. Допустимо, ви купили новий ноутбук, у якому у вас не працює веб-камера. Ваші події? Ви заглядаєте в пошукову систему і шукайте вирішення проблеми з цього питання. Досить часто може виявитися, що ваша веб-камера працює на ядрі. нової версіїніж у вас. Якщо ви не знаєте, яка у вас версія - введіть у терміналі uname -r, в результаті ви отримаєте версію ядра (наприклад, linux-2.6.31-10). Також компіляція ядра широко застосовується збільшення продуктивності: річ у тому, що за умовчанням в дистрибутивах ядра компілюються " всім " , через це у ньому включено дуже багато драйверів, які вам можуть знадобитися. Так що якщо ви добре знаєте обладнання, що використовується, то можете відключити непотрібні драйвера на етапі конфігурування. Також є можливість включити підтримку більш ніж 4 Гб оперативної пам'яті, не змінюючи розрядність системи. Отже, якщо вам все ж таки необхідно мати своє ядро, приступимо до компіляції!

Отримання вихідного коду ядра.
Перше, що потрібно зробити - отримати вихідний код потрібної версії ядра. Зазвичай необхідно отримати найновішу стабільну версію. Всі офіційні версії ядра доступні на kernel.org. Якщо у вас вже встановлений сервер X ( домашній комп'ютер), то ви можете перейти на сайт у вашому улюбленому браузері та завантажити потрібну версію в архіві tar.gz (стислий gzip). Якщо ж ви працюєте в консолі (наприклад, ще не встановлювали X сервер або конфігуруєте сервер), можете скористатися текстовим браузером (наприклад elinks). Також можна скористатися стандартним менеджером завантажень wget:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.33.1.tar.gz
Але майте на увазі, що ви повинні знати точний номер потрібної версії.

Розпакування архіву вихідного коду.
Після того, як ви отримали архів з вихідним кодом, вам необхідно розпакувати архів у папку. Це можна зробити з графічних файлових менеджерів(dolphin, nautilus тощо) або через mc. Або скористайтеся традиційною командою tar:
tar -zxvf шлях_до_архіву
Тепер у вас є папка та вихідним кодом, перейдіть до неї, використовуючи команду cd каталог_вихідного_коду_ядра(щоб переглянути список каталогів у папці, використовуйте команду ls ).

Конфігурація ядра.
Після того як ви перейшли до каталогу з вихідним кодом ядра, необхідно виконати "20 хвилинну" конфігурацію ядра. Мета її - залишити лише потрібні драйвера та функції. Усі команди вже потрібно виконувати від імені суперкористувача.

make config – консольний режим конфігуратора.

make menuconfig – консольний режим у вигляді списку.

make xconfig – графічний режим.

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

Компіляція.
Настав час завершального етапу збирання - компіляція. Це робиться двома командами:
make && make install
Перша команда скомпілює у машинний код усі файли, а друга встановить нове ядро ​​у вашу систему.
Чекаємо від 20 хвилин до кількох годин (залежно від потужності комп'ютера). Ядро встановлено. Щоб воно з'явилося у списку grub(2), введіть (від суперкористувача)
update-grub
Тепер після перезавантаження натисніть Escape, і ви побачите нове ядро ​​в списку. Якщо ж ядро ​​не вмикається, просто завантажтеся зі старим ядром і конфігуруйте акуратніше.

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

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 $@ $^ ============================================= ====

Тут використовуються дві особливості порівняно з кодом оригінального 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

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

- за наявності даної опції, компілятор 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 був 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.

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

Як загальний ліхтар зроблений driver programmers is to assume that concurrency is not problem as long as a particular segment of code
does not go to sleep (або "block"). It is true that the Linux kernel is nonpreemptive; with the important exception of
serveing ​​interrupts, it не буде приймати процесора від kernel code, що не буде лишитися. In past times, це nonpreemptive
behavior був enough to prevent unwanted concurrency most of the time. На SMP systems, however, preemption is not required to cause
Concurrent execution.

Якщо ваш код усвідомлює, що він не буде переотриманий, він не буде керувати належним чином на SMP системах. Even if you do not have such a system,
інші, які мають ваш код, може бути один. У майбутньому, це також може бути те, що kernel буде переміщатися до приємного режиму роботи,
у яких пункті even uniprocessor systems буде мати спільний з певною мірою,де
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 $@ clean: rm -f * .o *~ core

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

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

Деяким читачам будуть цікаві подробиці роботи утиліти вmod. Реалізація 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значення якого
порівнюється з версією поточного ядра утилітою вmod. Цей символ розташований у секції .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. Така вказівка ​​положення модуля спрацює для будь-яких версій утиліти вmod.

Іноді можна зіткнутися з інтерфейсами ядра, які мають відмінності у версіях 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. 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" вимагає оновлення. 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, ви маєте stále module entries. Ви збираєтесь разом з SUBDIRS, /usr/src/linux-2.6.3/scripts/Makefile.modpost:18: ти не збираєшся, коли деякі поїдуть. 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 така, як і 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"); 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) (arguments);item2 = allocate_thing2(arguments2);if (!item2 || !item2) goto fail; err = register_stuff(item1, item2);if (!err) stuff_ok = 1; 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 pm

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

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

Для вивантаження модуля використовуйте утиліту 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: d0 1 01f0-01f7 : ide0 02f8-02ff: serial(set) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga+ 03f6-03f6: ide0 03f8-03ff: serial(set) 10001 03 : acpi 1004-1005: acpi 1008-100b: acpi 100c-100f: acpi 1100-110f: Intel 82371AB PIIX4 IDE 1300-131f: dBus #02 1c00- 1cff: PCI CardBus #04 5800-581f: Intel 82371AB Intel 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-000 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 (#3) 0 0000-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) або 0xFFFFFFFFFFFFFFFFFF (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: 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 /* * following function performs autodetection, unless a specific * value was assigned by inmod to skull /* 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 first trial */ int base = skull_port_base ? skull_port_base: SKULL_PORT_FLOOR; int result = 0 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 */ (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_exit(lkm_example_exit);
Ми сконструювали найпростіший модуль, розглянемо докладніше найважливіші його частини:

  • У ньому перераховані файли заголовків, необхідні для розробки ядра 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; /* Це структура пунктів для всіх функцій функції */ static struct file_operations file_ops = (. /* Коли процеси reads з нашого пристрою, це gets називається. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) ( int bytes_read = 0; = 0) ( msg_ptr = msg_buffer; ) /* Put data in buffer */ while (len && *msg_ptr) function put_user handles this for us */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; ) return bytes_read; 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(TH) return 0; ) /* Called when a process closes our device */ static int device_release(struct inode *inode, struct file *file) ( /* Decrement the open counter and using count. Without this, the module would not unload. */ device_open_count- -;module_put(THIS_MODULE); /* Try to register character device */ major_num = register_chrdev(0, "lkm_example", &file_ops); if (major_num< 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: # Ви почнете - в front of the rmmod command to tell make to ignore # an error in case the module isn't 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

Висновок

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

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

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

Підключення модулів до основної програми здійснюється в порядку їх оголошення 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-файлу (його і називають первинним) незалежно від того, який файл (або частина програми) знаходиться в активному вікні редактора. Для цього в пункті sompile вибирають опцію Primary file, натискають Enter і записують ім'я первинного pas-файлу і тоді компіляція почнеться саме з цього файлу.

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

Примітка: У майбутньому я планую використовувати систему T2, щоб компілювати ядро ​​і модулі для Puppy. T2 в даний час встановлюється, щоб компілювати ядро ​​і численні додаткові модулі, але не ту версію, яка в даний момент використовується в Puppy. Я маю на увазі синхронізувати в майбутніх версіях Puppy, таким чином ядро, відкомпільовані в T2, буде використовуватися в Puppy. Див. http://www.puppyos.net/pfs/ для подальшої інформації щодо Puppy та T2.

Puppy має дуже простий спосіб використання C/C++ компіляторів за допомогою додавання єдиного файлу devx_xxx.sfs , де xxx - номер версії. Наприклад, Puppy 2.12 мав би файл розробки відповідності названий devx_212.sfs . Працюючи в режимі LiveCD, помістіть файл devx_xxx.sfs в те саме місце, де знаходиться ваш персональний файл налаштувань pup_save.3fs , який, зазвичай, знаходиться в каталозі /mnt/home/ . Це також стосується і інших режимів інсталяції, які мають pup_save.3fs файл. Якщо Puppy встановлений на жорсткий диск з повною інсталяцією «Option 2», то в цьому випадку немає жодного особистого файлу, подивіться на веб-сторінках Puppy, щоб компілювати з різними опціями вибору конфігурації, таким чином модулі не сумісні. Ці версії вимагають лише одного виправлення для squashfs. Puppy 2.12 має ядро ​​2.6.18.1 та має три виправлення; squashfs , значення loglevel в консолі за промовчанням і виправлення з вимкненням комп'ютера.

Ці команди, що виправляють ядро, дано виключно для вашої самоосвіти, оскільки доступне вже виправлене ядро.

Перше, що ви повинні спочатку зробити це завантажити саме ядро. Воно знаходиться , щоб знайти посилання на відповідний сайт завантаження. Це має бути «стародавнє» джерело, доступне на kernel.org або його дзеркалах.

Підключіться до Інтернету, завантажте ядро ​​в папку /usr/src . Потім розпакуйте його:

cd / usr / src tar -jxf linux-2.6.16.7.tar.bz2 tar -zxf linux-2.6.16.7.tar.gz

Ви повинні бачити цю папку: /usr/src/linux-2.6.16.7. Ви повинні переконатися, що це посилання вказує на ядро:

ln-sf linux-2.6.16.7 linux ln-sf linux-2.6.16.7 linux-2.6.9

Ви повинні застосувати такі виправлення, так, щоб у Вас було точно те саме джерело, яке використовується при компілюванні ядра для Puppy. Інакше у вас будуть з'являтися повідомлення про помилки "unresolved symbols" (невирішеними символами), при компіляції драйвера і намагаєтеся використовувати його з ядром Puppy. Застосування виправлення squashfs

По-друге, застосуйте виправлення Squashfs. Виправлення Squashfs додає підтримку Squashfs файлову систему, що робить, тільки для читання.

Завантажте виправлення, squashfs2.1-patch-k2.6.9.gz у папку /usr/src . Зазначте, що це виправлення було зроблено для версії 2.6.9 ядра, але продовжує працювати і в 2.6.11.x версіях, доки існує посилання на linux-2.6.9. Потім застосуйте виправлення:

Cd/usr/src gunzip squashfs2.1-patch-k2.6.9.gz cd/usr/src/linux-2.6.11.11

Примітка, у p1 це число 1, не символ l ... (Відмінний жарт - прим. перев.)

patch -dry-run -p1< ../ squashfs2.1-patch patch -p1 < ../ squashfs2.1-patch

Готово! Ядро готове до компілювання!

Компілювання ядра

Необхідно отримати конфігураційний файл для ядра. Копія його знаходиться в папці /lib/modules.

Потім зробіть ці кроки:

Cd/usr/src/linux-2.6.18.1

Якщо є файл .config, скопіюйте його кудись тимчасово або перейменуйте.

make clean make mrproper

Скопіюйте файл.

make menuconfig

… зробіть будь-які зміни, які ви хочете та збережете їх. У Вас тепер буде новий.config файл, і Ви повинні скопіювати його кудись для збереження. Див. примітку нижче

make bzImage

Ось тепер ви скомпілювали ядро.

Відмінно, ви знайдете ядро ​​Linux у папці /usr/src/linux-2.6.18.1/arch/i386/boot/bzImage

Компілювання модулів

Тепер увійдіть в /lib/modules і якщо вже є папка, названа 2.6.18.1, перейменуйте папку 2.6.18.1 на 2.6.18.1-old.

Тепер встановлювати нові модулі:

CD/usr/src/linux-2.6.18.1 make modules make modules_install

… після цього ви повинні знайти нові модулі встановленими в папці /lib/modules/2.6.18.1.

Зауважте, що останній крок вище виконує програму «depmod» і це може дати повідомлення про помилки про символи, що відсутні, для деяких з модулів. Не хвилюйтеся про це - один із розробників лоханувся, і це означає, що ми не можемо використовувати той модуль.

Як використовувати нове ядро ​​та модулі

Краще, якщо Ви маєте встановлений Puppy Unleashed. Тоді tarball розширений і існують 2 каталоги: "boot" і "kernels".

"Boot" містить структуру файлу та скрипт, щоб створити початковий віртуальний диск. Ви повинні помістити туди деякі модулі ядра.

У «kernels» каталогу є каталог kernels/2.6.18.1/, і Ви повинні замінити модулі з Вашими оновленими. Можна і не замінювати якщо Ви перекомпілювали ту саму версію ядра (2.6.18.1).

Зауважте, що в kernels/2.6.18.1 є файл c іменем "System.map". Ви повинні перейменувати його та замінити новим з /usr/src/linux-2.6.18.1. Перегляньте папку kernels/2.6.18.1/ , і Ви повинні знати, що потрібно оновити.

Якщо ви компілювали ядро ​​в повній інсталяції Puppy, ви можете перезавантажити за допомогою нового ядра. make modules_install кроком вище, встановлювали нові модулі /lib/modules/2.6.18.1 , але ви повинні також встановити нове ядро. Я збираюсь з Grub, і просто скопіюю нове ядро ​​в каталог /boot (і перейменуйте файл з bzImage в vmlinuz).

Примітка щодо menuconfig (конфігурація меню). Я використав його цілу вічність, так що вважайте деякі речі само собою зрозумілим, проте новачок міг би бути спантеличений, бажаючи вийти від програми. У меню верхнього рівня є меню, щоб зберегти конфігурацію – ігноруйте його. Тільки натисніть клавішу TAB (або клавішу стрілки «вправо»), щоб підсвітити Exit «кнопку» і натисніть клавішу ENTER. Після цього вас запитають, чи Ви хочете зберегти нову конфігурацію, і ваша відповідь повинна бути Так (Yes)


Top