Структурата на модулот на јадрото и неговите методи на компилација. Карактеристики на составување програма со модуларна структура. Вовед и позадина

Зошто сами да го составувате кернелот?
Можеби главното прашање што се поставува за составување кернел е: „Зошто да го направам ова?“
Многумина сметаат дека ова е бесмислено губење време за да се покажат како паметен и напреден корисник на Линукс. Всушност, составувањето на кернелот е многу важна работа. Да речеме дека сте купиле нов лаптоп и вашата веб камера не работи. Вашите постапки? Гледате во пребарувачот и барате решение за проблемот по ова прашање. Доста често може да испадне дека вашата веб камера работи на повеќе нова верзијаод твоето. Ако не знаете која верзија ја имате, внесете uname -r во терминалот, како резултат ќе ја добиете верзијата на кернелот (на пример, linux-2.6.31-10). Компилацијата на кернелот исто така широко се користи за зголемување на перформансите: факт е дека стандардно, дистрибуциите на кернелот се компајлираат „за секого“, поради што вклучува огромен број двигатели што можеби нема да ви требаат. Значи, ако добро го знаете хардверот што го користите, можете да ги оневозможите непотребните драјвери во фазата на конфигурација. Исто така, можно е да се овозможи поддршка за повеќе од 4 GB RAM без промена на длабочината на системскиот бит. Значи, ако сè уште треба да имате сопствен кернел, ајде да започнеме со компајлирање!

Добивање на изворниот код на кернелот.
Првото нешто што треба да направите е да го добиете изворниот код за потребната верзија на кернелот. Обично треба да ја добиете најновата стабилна верзија. Сите официјални верзии на кернелот се достапни на kernel.org. Ако веќе имате инсталирано X сервер ( домашен компјутер), потоа можете да отидете на страницата во вашиот омилен прелистувач и да ја преземете саканата верзија во архивата tar.gz (gzip компресирана). Ако работите во конзолата (на пример, сè уште не сте го инсталирале серверот X или го конфигурирате серверот), можете да користите прелистувач за текст (на пример, линкови). Можете исто така да го користите стандардниот менаџер за преземање wget:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.33.1.tar.gz
Но, имајте на ум дека мора да го знаете точниот број на верзијата што ви треба.

Отпакување на архивата на изворниот код.
Откако ќе ја примите архивата на изворниот код, треба да ја извадите архивата во папка. Ова може да се направи од графички менаџери на датотеки(делфин, наутилус итн.) или преку mc. Или користете ја традиционалната команда tar:
tar -zxvf path_to_archive
Сега имате папка со изворниот код, одете до неа користејќи ја командата cd kernel_source_directory(за да ги наведете директориумите во папка, користете ја командата ls).

Конфигурација на кернелот.
Откако ќе отидете до изворниот директориум на јадрото, треба да извршите конфигурација на кернелот „20 минути“. Неговата цел е да ги остави само потребните двигатели и функции. Сите команди мора веќе да се извршат како суперкорисник.

направи конфигурација - конзолен режим на конфигураторот.

направи menuconfig - режим на конзола во форма на листа.

направи xconfig - графички режим.

Откако ќе ги направите потребните промени, зачувајте ги поставките и излезете од конфигураторот.

Компилација.
Дојде време за последната фаза на склопување - компилација. Ова се прави со две команди:
направи && направи инсталирање
Првата команда ќе ги собере сите датотеки во машински код, а втората ќе го инсталира новото јадро на вашиот систем.
Чекаме од 20 минути до неколку часа (во зависност од моќноста на компјутерот). Јадрото е инсталирано. За да се појави во списокот grub(2), внесете (како суперкорисник)
ажурирање-grub
Сега по рестартирањето, притиснете „Escape“ и ќе го видите новото јадро во списокот. Ако јадрото не се вклучи, тогаш едноставно подигнете го со стариот кернел и повнимателно конфигурирајте го.

KernelCheck - го компајлира кернелот без да оди во конзолата.
ви овозможува да го изградите кернелот во целосно графички режим за Debian и дистрибуции базирани на него. По стартувањето, KernelCheck ќе ги понуди најновите верзии и закрпи на кернелот, а по ваша согласност, преземете го изворниот код и стартувајте го графичкиот конфигуратор. Програмата ќе го компајлира кернелот во .deb пакети и ќе ги инсталира. Сè што треба да направите е да се рестартира.

За: „Врз основа на преводот“ второ издание на двигател на уреди за Linux. Превод: Књазев Алексеј [заштитена е-пошта]Датум на последна измена: 08/03/2004 Локација: http://lug.kmv.ru/index.php?page=knz_ldd2

Сега да започнеме со програмирање! Ова поглавје ги дава основите за модулите и програмирањето на јадрото.
Овде ќе изградиме и лансираме полноправен модул, чија структура одговара на кој било вистински модуларен двигател.
Во исто време, ќе се концентрираме на главните позиции без да ги земеме предвид спецификите на вистинските уреди.

Сите делови од кернелот како што се функциите, променливите, датотеките со заглавието и макроата што се споменати овде ќе бидат
детално се опишани на крајот од поглавјето.

Здраво свету!

Во процесот на запознавање со оригиналниот материјал напишан од Alessndro Rubini & Jonathan Corbet, примерот даден како Hello world ми се чинеше донекаде неуспешен. Затоа, сакам да му дадам на читателот, според мое мислење, поуспешна верзија на првиот модул. Се надевам дека нема да има проблеми со неговата компилација и инсталација под кернелот 2.4.x. Предложениот модул и начинот на кој е компајлиран му овозможуваат да се користи во кернели кои поддржуваат и не поддржуваат контрола на верзијата. Подоцна ќе се запознаете со сите детали и терминологија, па сега отворете го vim и почнете да работите!

=================================================== === //датотека hello_knz.c #include #вклучи <1>Здраво, свет\n"); врати 0; ); void cleanup_module(void) (printk("<1>Збогум суров свет\n"); ) MODULE_LICENSE ("GPL"); ================================ ==================

За да составите таков модул, можете да го користите следниов Makefile. Не заборавајте да ставите знак на јазиче пред линијата што започнува со $(CC) ... .

=================================================== === ЗНАМЕЊА = -c -Wall -D__KERNEL__ -DMODULE PARAM = -I/lib/modules/$(shell unname -r)/build/include hello_knz.o: hello_knz.c $(CC) $(ЗНАМЕ) $( ПАРАМ) - o $@ $^ ========================================== ======================

Ова користи две функции во споредба со оригиналниот Hello world код предложен од Rubini & Corbet. Прво, модулот ќе ја има истата верзија како верзијата на кернелот. Ова се постигнува со поставување на променливата PARAM во скриптата за компилација. Второ, модулот сега ќе биде лиценциран според GPL (со користење на макрото MODULE_LICENSE()). Ако ова не е направено, тогаш при инсталирање на модулот во кернелот може да видите нешто како следново предупредување:

# insmod hello_knz.o Предупредување: вчитувањето hello_knz.o ќе го извалка кернелот: нема лиценца Видете http://www.tux.org/lkml/#export-tainted за информации за расипани модули. Модулот hello_knz е вчитан, со предупредувања

Сега да ги објасниме опциите за компилација на модулот (макро дефинициите ќе бидат објаснети подоцна):

-Со- со оваа опција, компајлерот gcc ќе го прекине процесот на компилација на датотека веднаш по создавањето на објектната датотека, без да се обиде да создаде извршна бинарна верзија.

-Ѕид- максимално ниво на предупредување кога работи gcc.

— дефиниции за макро симболи. Исто како и директивата #define во компајлираната датотека. Апсолутно нема разлика како да ги дефинирате макро симболите што се користат во овој модул, користејќи #define во изворната датотека или користејќи ја опцијата -D за компајлерот.

-Јас- дополнителни патеки за пребарување за вклучи датотеки. Забележете ја употребата на замена „uname -r“, која ќе го одреди точното име на верзијата на кернелот што моментално се користи.

Следниот дел дава уште еден пример на модул. Исто така, детално објаснува како да го инсталирате и да го растоварате од кернелот.

Оригинален Здраво светот!

Сега да го погледнеме оригиналниот код за едноставниот модул „Здраво, свет“ понуден од Рубини и Корбет. Овој код може да се компајлира под верзии на кернелот од 2.0 до 2.4. Овој пример, и сите други претставени во книгата, се достапни на страницата на О'Рајли FTP (види Поглавје 1).

//датотека hello.c #define MODULE #include int init_module(void) (printk("<1>Здраво, свет\n"); врати 0; ) void cleanup_module(void) (printk("<1>Збогум суров свет\n");)

Функција printk()дефинирана во кернелот на Линукс и работи како стандардна библиотечна функција printf()на јазик Ц. На кернелот му е потребна сопствена, по можност мала, функција за заклучување, содржана директно во кернелот, а не во библиотеки на ниво на корисник. Модулот може да повика функција printk()бидејќи по вчитувањето на модулот користејќи ја командата несовесенМодулот комуницира со кернелот и има пристап до објавените (извезени) функции и променливи на кернелот.

Параметар на низа “<1>” предадена на функцијата printk() е приоритет на пораката. Оригиналните англиски извори го користат терминот loglevel, што значи ниво на евиденција на пораки. Овде, ќе го користиме терминот приоритет наместо оригиналното „loglevel“. Во овој пример, ние користиме висок приоритет за пораката што има мал број. Високиот приоритет на пораките е поставен намерно, бидејќи пораката со стандарден приоритет може да не се прикаже во конзолата од која е инсталиран модулот. Насоката на излезот на пораките на јадрото со стандарден приоритет зависи од верзијата на кернелот што работи, верзијата на демонот кггд, и вашата конфигурација. Подетално, работа со функцијата printk()ќе објасниме во Поглавје 4, Техники за дебагирање.

Можете да го тестирате модулот користејќи ја командата несовесенда го инсталирате модулот во кернелот и командите rmmodза отстранување на модул од јадрото. Подолу ќе покажеме како тоа може да се направи. Во овој случај, влезната точка init_module() се извршува кога модул е ​​инсталиран во јадрото, а cleanup_module() се извршува кога ќе се отстрани од кернелот. Запомнете дека само привилегиран корисник може да вчитува и растоварува модули.

Примерот на модулот погоре може да се користи само со јадро што е изградено со исклучено знаменце „поддршка на верзијата на модулот“. За жал, повеќето дистрибуции користат кернели контролирани од верзијата (ова е дискутирано во делот „Контрола на верзии во модули“ од Поглавје 11, „kmod и напредна модуларизација“). И иако постарите верзии на пакетот модутилидозволи таквите модули да се вчитаат во кернели контролирани од верзијата, што повеќе не е можно. Потсетиме дека пакетот modutils содржи збир на програми кои ги вклучуваат програмите insmod и rmmod.

Задача: Одредете го бројот на верзијата и составот на пакетот modutils од вашата дистрибуција.

Кога се обидувате да вметнете таков модул во јадро што поддржува контрола на верзијата, може да видите порака за грешка слична на следната:

# insmod hello.o hello.o: Несовпаѓање на верзијата на кернелот-модул hello.o беше компајлиран за верзијата на кернелот 2.4.20 додека ова јадро е верзија 2.4.20-9asp.

Во каталогот разно-модулипримери од ftp.oreilly.com ќе ја најдете оригиналната програма за пример hello.c, која содржи малку повеќе линии и може да се инсталира и во кернели контролирани од верзија и во неверзии. Сепак, силно препорачуваме да изградите сопствено јадро без поддршка за контрола на верзијата. Во исто време, се препорачува да се земат оригиналните извори на кернелот на веб-страницата www.kernel.org

Ако сте нови за составување кернели, обидете се да ја прочитате статијата што Алесандро Рубини (еден од авторите на оригиналната книга) ја објави на http://www.linux.it/kerneldocs/kconf, што треба да ви помогне да го совладате процесот.

Извршете ги следните команди во текстуална конзола за да го компајлирате и тестирате оригиналниот примерен модул погоре.

Root# gcc -c hello.c root# insmod ./hello.o Здраво, world root# rmmod здраво Збогум суров светски корен#

Во зависност од механизмот што го користи вашиот систем за пренесување низи на пораки, излезната насока на пораките испратени од функцијата printk(), може да се разликуваат. Во горниот пример за компајлирање и тестирање на модул, пораките испратени од функцијата printk() беа излезни на истата конзола од која беа дадени командите за инсталирање и стартување на модулите. Овој пример е земен од конзола за текст. Ако ги извршите командите несовесенИ rmmodод под програмата xterm, тогаш најверојатно нема да видите ништо на вашиот терминал. Наместо тоа, пораката може да заврши во еден од системските дневници, на пример во /var/log/messages.Точното име на датотеката зависи од дистрибуцијата. Погледнете го времето на промени во лог датотеките. Механизмот што се користи за пренесување пораки од функцијата printk() е опишан во делот „Како се регистрираат пораките“ во Поглавје 4 „Техники“
дебагирање“.

За да ги видите пораките на модулот во системската датотека за евиденција /val/log/messages, погодно е да се користи системска алаткаопашка, која, стандардно, ги прикажува последните 10 линии од датотеката предадена до неа. Интересна опција на оваа алатка е опцијата -f, која ја извршува алатката во режим на следење на последните линии од датотеката, т.е. Кога ќе се појават нови линии во датотеката, тие автоматски ќе се испечатат. За да го запрете извршувањето на командата во овој случај, мора да притиснете Ctrl+C. Така, за да ги видите последните десет линии од системската датотека за дневник, внесете го следново во командната линија:

Корен # опашка /var/log/messages

Како што можете да видите, пишувањето модул не е толку тешко како што може да изгледа. Најтешкиот дел е да разберете како функционира вашиот уред и како да ги подобрите перформансите на модулот. Како што го продолжуваме ова поглавје, ќе дознаеме повеќе за пишување едноставни модули, оставајќи ги спецификите на уредот за подоцнежните поглавја.

Разлики помеѓу модулите на јадрото и апликациите

Апликацијата има една влезна точка, која започнува да се извршува веднаш по поставувањето апликација која работиво RAM меморијата на компјутерот. Оваа влезна точка е опишана во C како функција main(). Прекинувањето на функцијата main() значи прекинување на апликацијата. Модулот има неколку влезни точки кои се извршуваат при инсталирање и отстранување на модулот од кернелот, како и при обработка на барања од корисникот. Така, влезната точка init_module() се извршува кога модулот е вчитан во кернелот. Функцијата cleanup_module() се извршува кога модулот се растоварува. Во иднина ќе се запознаеме со други влезни точки во модулот, кои се извршуваат при извршување на различни барања до модулот.

Способноста за вчитување и растоварување на модули е два столба на механизмот за модуларизација. Тие можат да се проценат на различни начини. За инвеститорот, ова значи, пред сè, намалување на времето за развој, бидејќи можете да ја тестирате функционалноста на возачот без долг процес на рестартирање.

Како програмер, знаете дека апликацијата може да повика функција што не е декларирана во апликацијата. Во фазите на статичко или динамичко поврзување, се одредуваат адресите на таквите функции од соодветните библиотеки. Функција printf()една од овие функции за повикување што е дефинирана во библиотеката libc. Модулот, од друга страна, е поврзан само со кернелот и може да повикува само функции што се извезуваат од кернелот. Кодот извршен во кернелот не може да користи надворешни библиотеки. Така, на пример, функцијата printk(), што беше искористено во примерот здраво.в, е аналог на добро познатата функција printf(), достапно во апликации на ниво на корисник. Функција printk()се наоѓа во јадрото и треба да биде што помала. Затоа, за разлика од printf(), има многу ограничена поддршка за типови на податоци и, на пример, воопшто не поддржува броеви со подвижна запирка.

Имплементациите на кернелот 2.0 и 2.2 не поддржуваа спецификатори на тип ЛИ З. Тие беа воведени само во верзијата на јадрото 2.4.

Слика 2-1 ја прикажува имплементацијата на механизмот за повикување функции кои се влезни точки во модулот. Исто така, оваа слика го покажува механизмот на интеракција на инсталиран или инсталиран модул со кернелот.

Ориз. 2-1. Комуникација помеѓу модулот и кернелот

Една од карактеристиките на оперативните системи Unix/Linux е недостатокот на библиотеки кои можат да се поврзат со модули на јадрото. Како што веќе знаете, модулите, кога се вчитуваат, се поврзани во кернелот, така што сите функции надвор од вашиот модул мора да бидат декларирани во датотеките за заглавие на јадрото и присутни во кернелот. Изворите на модулите никогашне треба да вклучува обични датотеки со заглавија од библиотеки на кориснички простор. Во модулите на јадрото, можете да користите само функции кои всушност се дел од кернелот.

Целиот интерфејс на кернелот е опишан во датотеките за заглавија лоцирани во директориумите вклучи/линуксИ вклучуваат/асмвнатре во изворите на јадрото (обично лоцирани во /usr/src/linux-x.y.z(x.y.z е вашата верзија на кернелот)). Постари дистрибуции (врз основа на libcверзија 5 или помала) користеле симболични врски /usr/include/linuxИ /usr/include/asmдо соодветните директориуми во изворите на јадрото. Овие симболични врски овозможуваат, доколку е потребно, користење на интерфејси на јадрото во корисничките апликации.

Иако интерфејсот на библиотеките со кориснички простор сега е одделен од интерфејсот на кернелот, понекогаш корисничките процеси треба да користат интерфејси на јадрото. Сепак, многу референци во датотеките со заглавие на јадрото се однесуваат само на самиот кернел и не треба да бидат достапни за корисничките апликации. Затоа, овие огласи се заштитени #ifdef __KERNEL__блокови. Ова е причината зошто вашиот драјвер, како и другите кодови на јадрото, мора да се компајлира со декларирано макро __KERNEL__.

Улогата на поединечните датотеки за заглавие на кернелот ќе се дискутира соодветно низ книгата.

Програмерите кои работат на какви било големи софтверски проекти (како што е кернелот) треба да бидат свесни и да избегнуваат „загадување на именскиот простор“. Овој проблем се јавува кога има голем број на функции и глобални променливи чии имиња не се доволно експресивни (се разликуваат). Програмерот кој подоцна треба да се справи со такви апликации е принуден да потроши многу повеќе време за да се сеќава на „резервираните“ имиња и да смисли уникатни имиња за нови елементи. Судирите на имињата (нејаснотии) можат да создадат широк опсег на проблеми, кои се движат од грешки при вчитување на модул до нестабилно или необјаснето однесување на програмата што може да се појави кај корисници кои користат јадро изградено во различна конфигурација.

Програмерите не можат да си дозволат такви грешки при пишување код на кернелот, бидејќи и најмалиот модул ќе биде поврзан со целото јадро. Најдоброто решение за да се избегне судир на имиња е прво да ги декларирате објектите на програмата како статични, и, второ, употребата на единствен, во системот, префикс за именување на глобални објекти. Дополнително, како развивач на модули, можете да го контролирате опсегот на објектите во вашиот код, како што е опишано подоцна во делот „Табела за врски на јадрото“.

Повеќето (но не сите) верзии на командата несовесенизвезете ги сите објекти на модулот што не се декларирани како статични, стандардно, т.е. освен ако модулот не дефинира посебни инструкции за оваа намена. Затоа, сосема е разумно да декларирате модулни објекти кои немате намера да ги извезете како статични.

Користењето уникатен префикс за локални објекти во модул може да биде добра практика бидејќи го олеснува дебагирањето. Додека го тестирате вашиот драјвер, можеби ќе треба да извезете дополнителни објекти во јадрото. Со користење на единствен префикс за означување на имиња, не ризикувате да воведувате судири во именскиот простор на јадрото. Префиксите што се користат во кернелот се, по конвенција, мали букви, и ние ќе се држиме до таа конвенција.

Друга значајна разлика помеѓу процесите на јадрото и корисникот е механизмот за справување со грешки. Кернелот го контролира извршувањето на корисничкиот процес, така што грешка во корисничкиот процес резултира со порака која е безопасна за системот: дефект на сегментација. Во исто време, дебагерот секогаш може да се користи за следење на грешките во изворниот код на корисничката апликација. Грешките што се случуваат во кернелот се фатални - ако не за целиот систем, тогаш барем за тековниот процес. Во делот „Грешки на системот за дебагирање“ од Поглавје 4, „Техники за дебагирање“, ќе ги разгледаме начините за следење на грешките во јадрото.

Кориснички простор и простор на јадрото

Модулот работи во т.н простор на јадрото, додека апликациите работат во . Овој концепт е основа на теоријата на оперативни системи.

Една од главните цели на оперативниот систем е да им обезбеди на корисникот и корисничките програми компјутерски ресурси, од кои повеќето се претставени со надворешни уреди. Оперативниот систем не само што мора да обезбеди пристап до ресурсите, туку и да ја контролира нивната распределба и употреба, спречувајќи судири и неовластен пристап. Во прилог на ова, операционен системможе да креира независни операции за програмите и да штити од неовластен пристап до ресурсите. Решавањето на овој нетривијален проблем е можно само ако процесорот ги штити системските програми од кориснички апликации.

Речиси секој модерен процесор е во состојба да обезбеди такво раздвојување со имплементирање на различни нивоа на привилегии за кодот за извршување (потребни се најмалку две нивоа). На пример, процесорите за архитектура I32 имаат четири нивоа на привилегии од 0 до 3. Покрај тоа, нивото 0 ги има највисоките привилегии. За такви процесори, постои класа на привилегирани инструкции кои можат да се извршат само на привилегирани нивоа. Unix системите користат две нивоа на привилегии на процесорот. Ако процесорот има повеќе од две нивоа на привилегии, се користат најниското и највисокото. Јадрото на Unix работи највисоко нивопривилегии, обезбедувајќи контрола на опремата и процесите на корисникот.

Кога зборуваме за простор на јадротоИ кориснички процесен просторОва значи не само различни нивоа на привилегии за извршниот код, туку и различни адресни простори.

Unix го пренесува извршувањето од просторот на корисничкиот процес во просторот на јадрото во два случаи. Прво, кога корисничка апликација упатува повик до кернелот (системски повик), и второ, додека се сервисира хардверските прекини. Кернел кодот извршен за време на системски повик работи во контекст на процес, т.е. работи во име на процесот на повикување, има пристап до податоците за адресниот простор на процесот. Од друга страна, кодот кој се извршува при сервисирање на хардверски прекин е асинхрон во однос на процесот и не припаѓа на некој посебен процес.

Целта на модулите е да ја прошират функционалноста на кернелот. Кодот на модулот се извршува во просторот на јадрото. Вообичаено, модулот ги извршува двете задачи наведени претходно: некои функции на модулот се извршуваат како дел од системските повици, а некои се одговорни за управување со прекини.

Паралелизација во јадрото

Кога програмирате драјвери за уреди, за разлика од програмирачките апликации, прашањето за паралелизирање на извршниот код е особено акутно. Вообичаено, апликацијата работи последователно од почеток до крај без да се грижи за промените во нејзината околина. Кодот на јадрото мора да работи со разбирање дека може да се пристапи повеќе пати во исто време.

Постојат многу причини за паралелизирање на кодот на јадрото. Linux обично има многу процеси кои работат, а некои од нив може да се обидат да пристапат до кодот на вашиот модул во исто време. Многу уреди може да предизвикаат хардверски прекини на процесорот. Управувачите со прекини се повикуваат асинхроно и може да се повикаат додека вашиот возач извршува друго барање. Некои софтверски апстракции (како што се тајмерите на јадрото, објаснети во Поглавје 6, „Проток на времето“) исто така работат асинхроно. Дополнително, Linux може да се работи на систем со симетрични мултипроцесори (SMP), што значи дека кодот на вашиот возач може да работи паралелно на повеќе процесори во исто време.

Поради овие причини, кодот на кернелот на Линукс, вклучувајќи го и кодот на возачот, мора повторно да се внесе, т.е. мора да може да работи со повеќе од еден податочен контекст во исто време. Структурите на податоци мора да бидат дизајнирани за да се приспособат на паралелно извршување на повеќе нишки. За возврат, кодот на јадрото мора да биде способен да ракува со повеќе паралелни текови на податоци без да ги оштети. За пишување код што може да се изврши паралелно и избегнувајте ситуации во кои различна секвенца на извршување би довело до непожелно однесување на системот бара многу време, а можеби и многу измами. Секој пример на возачот во оваа книга е напишан со паралелно извршување на ум. Доколку е потребно, ќе ги објасниме спецификите на техниката за пишување таков код.

Повеќето општа грешкаПроблемот што го прават програмерите е тоа што тие претпоставуваат дека истовременоста не е проблем затоа што некои сегменти од кодот не можат да одат на спиење. Навистина, кернелот на Линукс не е страничен, со важен исклучок на ракувачите со прекини, кои не можат да го добијат процесорот додека се извршува критичниот код на кернелот. Во последно време, не-страницата е доволна за да се спречи несакана паралелизација во повеќето случаи. На SMP системите, сепак, преземањето код не е потребно поради паралелно пресметување.

Ако вашиот код претпоставува дека нема да се истовари, тогаш нема да работи правилно на SMP системите. Дури и ако немате таков систем, некој друг што го користи вашиот код може да има таков. Исто така, можно е во иднина кернелот да користи можност за страници, па дури и системите со еден процесор ќе мора да се справат со истовременост. Веќе има опции за имплементација на такви кернели. Така, внимателен програмер ќе напише код на јадрото со претпоставка дека ќе работи на систем кој работи на SMP.

Забелешка преведувач:Извинете, но последните два параграфи не ми се јасни. Ова може да е резултат на погрешен превод. Затоа, го презентирам оригиналниот текст.

Честа грешка направена од програмерите на возачите е да се претпостави дека истовременоста не е проблем се додека одреден сегмент од кодот
не еодете на спиење (или „блокирајте“). Вистина е дека кернелот на Линукс не е превентивен; со важен исклучок на
прекините на сервисирањето, нема да го оддалечат процесорот од кодот на јадрото што не попушта доброволно. Во минатите времиња, ова непревентивно
однесувањето беше доволно за да се спречи несакана истовременост поголемиот дел од времето. На SMP системите, сепак, не е потребна претпазливост за да се предизвика
истовремено извршување.

Ако вашиот код претпоставува дека нема да биде превентиран, тој нема да работи правилно на SMP системите. Дури и ако немате таков систем,
други кои го водат вашиот код може да имаат еден. Во иднина, исто така, можно е кернелот да премине во превентивен режим на работа,
во кој момент дури и унипроцесорските системи ќе мора да се справат со истовременоста насекаде (некои варијанти на кернелот веќе имплементирани
тоа).

Информации за тековниот процес

Иако кодот на модулот на јадрото не се извршува последователно како апликациите, повеќето повици до кернелот се извршуваат во однос на процесот што го повикува. Кодот на јадрото може да го идентификува процесот што го повикал со пристап до глобален покажувач што укажува на структурата struct task_struct, дефинирана за кернелите верзија 2.4, во датотеката вклучени во . Покажувач струјаго означува тековниот кориснички процес. При извршување на системски повици како на пр отворено ()или затвори (), мора да има процес кој ги предизвикал. Кодот на јадрото може, доколку е потребно, да повика конкретни информации за процесот на повикување преку покажувач струја. За примери за користење на овој покажувач, видете го делот „Контрола на пристап до датотеки на уредот“ во Поглавје 5, „Подобрени операции на двигателот на знакот“.

Денес, индексот струјаповеќе не е глобална променлива, како во претходните верзии на кернелот. Програмерите го оптимизираа пристапот до структурата што го опишува тековниот процес со преместување на страницата на стек. Можете да ги погледнете тековните детали за имплементацијата во датотеката . Кодот што го гледате таму можеби не ви изгледа едноставен. Имајте на ум дека Линукс е систем кој се центрира на SMP, а глобалната променлива едноставно нема да работи кога имате работа со повеќе процесори. Деталите за имплементацијата остануваат скриени за другите потсистеми на јадрото, а двигателот на уредот може да пристапи до покажувачот струјасамо преку интерфејсот .

Од гледна точка на модулот, струјаизгледа како надворешна врска printk(). Модулот може да користи струјасекаде каде што е потребно. На пример, следниот дел од кодот ги печати ID на процесот (PID) и името на командата на процесот што го повика модулот, добивајќи ги преку соодветните полиња на структурата struct task_struct:

Printk("Процесот е \"%s\" (pid %i)\n", current->comm, current->pid);

Полето current->comm е името на командната датотека што го предизвика тековниот процес.

Составување и вчитување модули

Остатокот од ова поглавје е посветен на пишување целосен, иако нетипичен, модул. Оние. Модулот не припаѓа на ниту една од класите опишани во делот „Класи на уреди и модули“ во Поглавје 1, „Вовед во двигатели на уреди“. Примерниот двигател прикажан во ова поглавје ќе се вика череп (Simple Kernel Utility for Loading Localities). Можете да го користите модулот за череп како образец за да напишете сопствен локален код.

Го користиме концептот „локален код“ (локален) за да ги нагласиме промените на вашите лични кодови, во старата добра традиција на Unix (/usr/local).

Сепак, пред да ги пополниме функциите init_module() и cleanup_module(), ќе напишеме скрипта Makefile која make ќе ја користи за да го изгради објектниот код на модулот.

Пред да може препроцесорот да го обработи вклучувањето на која било датотека за заглавие, макро симболот __KERNEL__ мора да се дефинира со директива #define. Како што беше споменато претходно, контекстот специфичен за јадрото може да се дефинира во датотеките на интерфејсот на јадрото, видлив само ако симболот __KERNEL__ е претходно дефиниран при претпроцесирање.

Друг важен симбол дефиниран со директивата #define е симболот MODULE. Мора да се дефинира пред да се овозможи интерфејсот (со исклучок на оние драјвери што ќе се компајлираат со кернелот). Во оваа книга нема да бидат опишани двигатели собрани во јадрото, така што симболот MODULE ќе биде присутен во сите наши примери.

Ако градите модул за систем со SMP, исто така треба да го дефинирате макро симболот __SMP__ пред да ги овозможите интерфејсите на јадрото. Во верзијата 2.2 на јадрото, посебна ставка во конфигурацијата на кернелот воведе избор помеѓу еден процесорски и мултипроцесорски систем. Затоа, вклучувањето на следните линии како први линии на вашиот модул ќе резултира со мултипроцесорска поддршка.

#вклучи #ifdef CONFIG_SMP # дефинира __SMP__ #endif

Развивачите на модули, исто така, треба да го дефинираат знамето за оптимизација -O за компајлерот бидејќи многу функции се декларирани на линија во датотеките за заглавие на јадрото. Компајлерот gcc не врши внатрешно проширување на функциите освен ако не е овозможена оптимизација. Дозволувањето да се прошират вградните замени со користење на опциите -g и -O ќе ви овозможи подоцна да го отстраните кодот што користи вградени функции во дебагерот. Бидејќи кернелот широко ги користи вградените функции, многу е важно тие да се прошират правилно.

Забележете, сепак, дека користењето на која било оптимизација над нивото -O2 е ризично бидејќи компајлерот може да ги прошири функциите што не се декларирани во линија. Ова може да доведе до проблеми бидејќи ... Некои функциски кодови очекуваат да го најдат стандардниот куп на неговиот повик. Вградената екстензија се подразбира како вметнување функционален код на точката на неговото повикување наместо соодветната инструкција за повикување функција. Според тоа, во овој случај, бидејќи нема повик на функција, тогаш нема оџак од неговиот повик.

Можеби ќе треба да се осигурате дека го користите истиот компајлер за компајлирање модули како што се користеше за да се изгради кернелот во кој ќе се инсталира модулот. За детали, видете го оригиналниот документ од датотеката Документација/Променилоциран во директориумот извори на јадрото. Развојот на јадрото и компајлерот обично се синхронизира низ тимовите за развој. Може да има случаи кога ажурирањето на еден од овие елементи открива грешки во друг. Некои производители на дистрибуција обезбедуваат ултра-нови верзии на компајлерот што не одговараат на кернелот што го користат. Во овој случај, тие обично обезбедуваат посебен пакет (често се нарекува kgcc) со компајлер специјално дизајниран за
компилација на јадрото.

Конечно, за да спречите непријатни грешки, ви предлагаме да ја користите опцијата за компајлирање -Ѕид(сите предупредувања - сите предупредувања). За да ги задоволите сите овие предупредувања, можеби ќе треба да го промените вашиот вообичаен стил на програмирање. Кога пишувате код на јадрото, пожелно е да се користи стилот на кодирање предложен од Линус Торвалдс. Да, документ Документација/Стил на кодирање,од директориумот извор на јадрото, е доста интересен и им се препорачува на сите заинтересирани за програмирање на ниво на кернел.

Се препорачува да поставите сет на знаменца за компилација на модули, со кои неодамна се запознавме, во променлива CFLAGSвашиот Makefile. За алатката make, ова е специјална променлива, чија употреба ќе стане јасна од следниот опис.

Покрај знаменцата во променливата CFLAGS, можеби ќе ви треба цел во вашиот Makefile што комбинира различни датотеки со објекти. Таквата цел е неопходна само кога кодот на модулот е поделен на неколку изворни датотеки, што генерално не е невообичаено. Објектните датотеки се комбинираат со командата ld -r, што не е операција за поврзување во општо прифатена смисла, и покрај употребата на поврзувач ( ld). Резултатот од извршувањето на командата ld -rе друга објектна датотека која ги комбинира објектните кодови на влезните датотеки на поврзувачот. Опција значи „ relocatable - преместување“, т.е. Излезната датотека на командата ја преместуваме во просторот за адреси, бидејќи сè уште не содржи апсолутни адреси на повици на функции.

Следниот пример го прикажува минималниот Makefile потребен за да се состави модул кој се состои од две изворни датотеки. Ако вашиот модул се состои од една изворна датотека, тогаш од дадениот пример треба да ја отстраните целта што ја содржи командата ld -r.

# Патеката до вашиот изворен директориум на кернелот може да се смени овде, # или можете да ја пренесете како параметар кога повикувате „make“ KERNELDIR = /usr/src/linux include $(KERNELDIR)/.config CFLAGS = -D__KERNEL__ -DMODULE - I$(KERNELDIR) /вклучи \ -O -Ѕид ifdef CONFIG_SMP CFLAGS += -D__SMP__ -DSMP endif all: skull.o skull.o: skull_init.o skull_clean.o $(LD) -r $^ -o $@ чист : rm -f * .o *~јадро

Ако сте нови за тоа како функционира make, може да се изненадите што нема правила за компајлирање *.c-датотеки во датотеки со објекти *.o. Дефинирањето на такви правила не е неопходно, бидејќи алатката make, доколку е потребно, самата ги конвертира датотеките *.c во датотеки *.o користејќи го стандардниот компајлер или компајлерот наведен со променлива $(CC). Во овој случај, содржината на променливата $ (CFLAGS)се користи за одредување знаменца за компилација.

Следниот чекор по изградбата на модулот е да се вчита во кернелот. Веќе рековме дека за ова ќе ја користиме алатката insmod, која ги поврзува сите недефинирани симболи (повици на функции итн.) на модулот со табелата со симболи на кернелот што работи. Меѓутоа, за разлика од поврзувачот (на пример, како ld), тој не ја менува датотеката на дискот на модулот, туку го вчитува објектот на модулот поврзан со јадрото во RAM меморијата. Услужната алатка insmod може да прифати некои опции на командната линија. Деталите може да се видат преку човек нерасположен. Користејќи ги овие опции, можете, на пример, да доделите специфични променливи со цели броеви и низа во вашиот модул на одредени вредности пред да го поврзете модулот во кернелот. Така, ако модулот е дизајниран правилно, може да се конфигурира при подигање. Овој метод на конфигурирање на модул му дава на корисникот поголема флексибилност отколку конфигурацијата во времето на компајлирање. Конфигурацијата за време на подигање е објаснета во делот „Рачна и автоматска конфигурација“ подоцна во ова поглавје.

Некои читатели ќе бидат заинтересирани за деталите за тоа како функционира алатката insmod. Имплементацијата на insmod се заснова на неколку системски повици дефинирани во kernel/module.c. Функцијата sys_create_module() ја распределува потребната количина на меморија во просторот за адреси на кернелот за да се вчита модулот. Оваа меморија се доделува со помош на функцијата vmalloc() (видете го делот „vmalloc и пријатели“ во Поглавје 7, „Добивање на меморијата“). Системскиот повик get_kernel_sysms() ја враќа табелата со симболи на јадрото, која ќе се користи за одредување на вистинските адреси на објектите при поврзување. Функцијата sys_init_module() го копира објектниот код на модулот во просторот за адреси на јадрото и ја повикува функцијата за иницијализација на модулот.

Ако ги погледнете изворите на кодот на јадрото, ќе најдете имиња на системски повици кои започнуваат со префиксот sys_. Овој префикс се користи само за системски повици. Ниту една друга функција не треба да ја користи. Имајте го ова на ум кога ги обработувате изворите на кодот на јадрото со алатката за пребарување grep.

Зависности на верзијата

Ако не знаете ништо повеќе од она што е опфатено овде, тогаш најверојатно модулите што ги креирате ќе треба да се прекомпајлираат за секоја верзија на кернелот во кој се поврзани. Секој модул мора да дефинира симбол наречен __module_kernel_version, чија вредност
се споредува со верзијата на тековното кернел користејќи ја алатката insmod. Овој симбол се наоѓа во делот .modinfo ELF (извршен и поврзувачки формат) датотеки. Ова е подетално објаснето во Поглавје 11 „kmod и напредна модуларизација“. Имајте предвид дека овој метод за контрола на верзијата е применлив само за верзии на кернелот 2.2 и 2.4. Во кернелот 2.0 ова се прави на малку поинаков начин.

Компајлерот ќе го дефинира овој симбол каде и да е вклучена датотеката за заглавие . Затоа, во примерот hello.c даден претходно, не го опишавме овој симбол. Ова исто така значи дека ако вашиот модул се состои од многу изворни датотеки, мора да ја вклучите датотеката во вашиот код само еднаш. Исклучок е случајот кога се користи дефиницијата __NO_VERSION__, кој ќе го запознаеме подоцна.

Подолу е дефиницијата на опишаниот симбол од датотеката модул.h извлечена од кодот на јадрото 2.4.25.

Static const char __module_kernel_versio/PRE__attribute__((section(.modinfo"))) = "kernel_version=" UTS_RELEASE;

Ако модулот не се вчита поради несовпаѓање на верзијата, можете да се обидете да го вчитате овој модул со предавање на копчето insmod на линијата за параметри на алатката (сила). Овој метод на вчитување модул не е безбеден и не е секогаш успешен. Прилично е тешко да се објаснат причините за можните неуспеси. Можно е модулот да не се вчита бидејќи симболите не се решаваат при поврзувањето. Во овој случај, ќе добиете соодветна порака за грешка. Причините за неуспех може да лежат и во промените во работата или структурата на кернелот. Во овој случај, вчитувањето на модулот може да доведе до сериозни грешки во времето на работа, како и до системска паника. Последново треба да послужи како добар поттик за користење на систем за контрола на верзии. Неусогласеноста на верзиите може да се постапува поелегантно со користење на контрола на верзијата во кернелот. Детално ќе разговараме за ова во делот „Контрола на верзии во модули“ во Поглавје 11 „kmod и напредна модуларизација“.

Ако сакате да го компајлирате вашиот модул за одредена верзија на кернелот, мора да ги вклучите датотеките за заглавие за таа конкретна верзија на кернелот. Во примерот Makefile опишан погоре, променливата беше искористена за одредување на директориумот за овие датотеки КЕРНЕЛДИР. Таквата сопствена компилација не е невообичаена кога се достапни извори на јадрото. Исто така, не е невообичаено да има различни верзии на кернелот во дрвото на директориуми. Сите примери на модулите во оваа книга ја користат променливата КЕРНЕЛДИРда се означи локацијата на изворниот директориум за верзијата на кернелот во кој треба да се поврзе собраниот модул. Можете да користите системска променлива за да го одредите овој директориум или можете да ја пренесете неговата локација преку опциите на командната линија за правење.

Кога вчитува модул, алатката insmod користи свои патеки за пребарување за објектите на модулот, гледајќи низ директориуми зависни од верзијата, почнувајќи од /lib/модули. И иако постарите верзии на алатката го вклучија тековниот директориум во патеката за пребарување, ова однесување сега се смета за неприфатливо од безбедносни причини (истите проблеми како и користењето на системската променлива ПАТ). Значи, ако сакате да вчитате модул од тековниот директориум, можете да го наведете во стилот ./модул.о. Оваа индикација за позицијата на модулот ќе работи за која било верзија на алатката insmod.

Понекогаш може да наидете на интерфејси на јадрото кои се различни помеѓу 2.0.x и 2.4.x. Во овој случај, ќе треба да прибегнете кон макро што ја одредува тековната верзија на кернелот. Ова макро се наоѓа во заглавието на датотеката . Ќе посочиме случаи на разлики во интерфејсите кога ги користите. Ова може да се направи или веднаш по описот, или на крајот од делот, во посебен дел посветен на зависностите од верзијата. Во некои случаи, поставувањето на деталите во посебен дел ќе ви овозможи да избегнете комплицирање на описот на верзијата 2.4.x на јадрото што е релевантно за оваа книга.

Во заглавието на датотеката линукс/верзија.чСледниве макроа се дефинирани поврзани со одредување на верзијата на кернелот.

UTS_RELEASEМакро што се проширува во низа што ја опишува тековната верзија на кернелот
изворно дрво. На пример, макрото може да се прошири на нешто како ова:
линија: "2.3.48" . LINUX_VERSION_CODEОва макро се проширува во бинарна претстава на верзијата на кернелот, од
по еден бајт за секој дел од бројот. На пример, бинарни
претставата за верзијата 2.3.48 ќе биде 131888 (децимална
претстава за хексадецимален 0x020330). Можно е бинарно
Претставата ќе ја најдете попогодна од претставата на стрингот. Забележете што е
претставувањето ви овозможува да опишете не повеќе од 256 опции во секоја
делови од бројот. KERNEL_VERSION (главно, помало, издание)Оваа макро дефиниција ви овозможува да изградите „kernel_version_code“
од поединечните елементи што ја сочинуваат верзијата на кернелот. На пример,
следно макро KERNEL_VERSION(2, 3, 48)
ќе се прошири на 131888. Оваа макро дефиниција е многу погодна кога
споредувајќи ја тековната верзија на кернелот со потребната. Ќе бидеме постојано
користете ја оваа макро дефиниција низ целата книга.

Еве ја содржината на датотеката: линукс/верзија.чза кернелот 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. Сепак, проверката на зависноста од верзијата може во голема мера да ја комплицира читливоста на кодот на модулот поради хетерогени директиви #ифдеф. Затоа, можеби најдоброто решение е да ја ставите проверката на зависноста во посебна датотека со заглавие. Ова е причината зошто нашиот пример вклучува датотека за заглавие sysdep.h, се користи за сместување на сите макро дефиниции поврзани со проверки на зависност од верзијата.

Првата зависност од верзијата што сакаме да ја претставиме е во целната декларација" направи инсталирање" нашата скрипта за компилација на драјвери. Како што може да очекувате, директориумот за инсталација, кој се менува според користената верзија на кернелот, се избира врз основа на прегледување на датотеката version.h. Еве дел од кодот од датотеката Правила.направи, кој се користи од сите кернелски датотеки.

VERSIONFILE = $(INCLUDEDIR)/linux/version.h VREION = $(школка awk -F\" "/REL/ (печати $2)" $(VERSIONFILE)) INSTALLDIR = /lib/modules/$(VERSION)/различно

Забележете дека го користиме директориумот misc (декларацијата INSTALLDIR во примерот Makefile погоре) за да ги инсталираме сите наши драјвери. Почнувајќи од верзијата 2.4 на јадрото, овој директориум е препорачаниот директориум за поставување сопствени драјвери. Дополнително, и старите и новите верзии на пакетот modutils содржат различен директориум во нивните патеки за пребарување.

Користејќи ја горенаведената дефиниција INSTALLDIR, целта за инсталирање во Makefile може да изгледа вака:

Инсталирај: инсталирај -d $(INSTALLDIR) инсталирај -c $(OBJS) $(INSTALLDIR)

Зависност од платформа

Секоја компјутерска платформа има свои карактеристики кои мора да ги земат предвид развивачите на кернелот за да постигнат највисоки перформанси.

Програмерите на кернелот имаат многу поголема слобода во изборот и одлучувањето отколку развивачите на апликации. Токму оваа слобода ви овозможува да го оптимизирате вашиот код, извлекувајќи го максимумот од секоја специфична платформа.

Кодот на модулот мора да се компајлира со користење на истите опции за компајлер што беа користени за компајлирање на кернелот. Ова важи и за користење на исти обрасци за користење на регистарот на процесорот и за извршување на исто ниво на оптимизација. Датотека Правила.направи, лоциран во коренот на дрвото на изворот на јадрото, вклучува дефиниции специфични за платформата кои мора да бидат вклучени во сите датотеки на компилација. Сите скрипти за компилација специфични за платформата се нарекуваат Makefiles. платформаи ги содржи вредностите на променливите за алатката make според тековната конфигурација на кернелот.

Друга интересна карактеристика на Makefile е неговата поддршка за крос-платформа или едноставно вкрстена компилација. Овој термин се користи кога треба да компајлирате код за друга платформа. На пример, користејќи ја платформата i86, ќе креирате код за платформата M68000. Ако сакате вкрстено компајлирање, тогаш ќе треба да ги замените вашите алатки за компилација ( gcc, ld, итн.) со друг сет на соодветни алатки
(На пример, m68k-linux-gcc, m68k-linux-ld). Користениот префикс може да се одреди или со променливата Makefile $(CROSS_COMPILE), со опцијата на командната линија до алатката 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. За жал, оригиналот на оваа книга не го опфаќа новото јадро, па преведувачот ќе си земе слобода да го надополни преводот со ново знаење.

Можете да користите интегрирани развојни околини како TimeStorm на TimeSys, кои правилно ќе генерираат скелет и скрипта за компилација за вашиот модул во зависност од потребната верзија на кернелот. Ако сакате да го напишете сето ова сами, тогаш ќе ви требаат дополнителни информации за главните разлики што ги воведува новиот кернел.

Една од карактеристиките на кернелот 2.6 е потребата да се користат макроата module_init() и module_exit() за експлицитно регистрирање на имињата на функциите за иницијализација и завршување.

Макрото MODULE_LISENCE(), воведено во кернелот 2.4, сè уште е потребно ако не сакате да ги гледате соодветните предупредувања при вчитување на модулот. Можете да ги изберете следните низи на лиценци што ќе се префрлат на макрото: „GPL“, „GPL v2“, „GPL и дополнителни права“, „Dual BSD/GPL“ (избор помеѓу BSD или GPL лиценци), „Dual MPL/GPL " (избор помеѓу лиценци за Mozilla или GPL) и
„Сопственост“.

Позначајно за новиот кернел е новата шема за компилација на модул, која подразбира не само промени во кодот на самиот модул, туку и во скриптата Makefile за неговата компилација.

Така, дефиницијата на макро симболот MODULE повеќе не е потребна ниту во кодот на модулот ниту во Makefile. Доколку е потребно, самата нова шема за компилација ќе го одреди овој макросимбол. Исто така, нема да треба експлицитно да ги дефинирате __KERNEL__ макросимболите или поновите како KBUILD_BASENAME и KBUILD_MODNAME.

Исто така, не треба да го одредувате нивото на оптимизација при компилацијата (-O2 или други), бидејќи вашиот модул ќе биде компајлиран со целиот сет на знаменца, вклучувајќи ги и знаменцата за оптимизација, со кои се компајлираат сите други модули од вашиот кернел - алатката make автоматски го користи целиот неопходен сет на знаменца.

Поради овие причини, Makefile за составување модул за кернелот 2.6 е многу поедноставен. Така, за модулот hello.c, Makefile ќе изгледа вака:

Obj-m:= здраво.о

Меѓутоа, за да го компајлирате модулот, ќе ви треба пристап за пишување до дрвото на изворот на јадрото, каде што ќе се креираат привремени датотеки и директориуми. Затоа, командата за компајлирање на модул за кернелот 2.6, наведен од тековниот директориум што го содржи изворниот код на модулот, треба да изгледа вака:

# make -C /usr/src/linux-2.6.1 SUBDIRS=`pwd` модули

Значи, го имаме изворот на модулот здраво-2.6.в, за компилација во кернелот 2.6:

//hello-2.6.c #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:= здраво-2.6.о

Ја повикуваме алатката make за да ја обработи нашата Makefile со следните параметри:

# make -C/usr/src/linux-2.6.3 SUBDIRS=`pwd` модули

Нормалниот процес на компилација ќе го произведе следниов стандарден излез:

Направете: внесете го директориумот `/usr/src/linux-2.6.3" *** Предупредување: Надминувањето на SUBDIRS на командната линија може да предизвика *** недоследности прават: `arch/i386/kernel/asm-offsets.s“ не бара ажурирање. CHK include/asm-i386/asm_offsets.h CC [M] /home/knz/j.kernel/3/hello-2.6.o Градење модули, фаза 2. /usr/src/linux-2.6.3/scripts/Makefile .modpost:17: *** Ух-о, имате застарени записи во модулот. Се плеткавте со 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 направи: Излезете од директориумот `/usr/src / Linux-2.6.3"

Конечниот резултат од компилацијата ќе биде датотека со модул hello-2.6.ko што може да се инсталира во кернелот.

Забележете дека во кернелот 2.6, датотеките на модулите се ставаат со суфикс со .ko наместо со .o како во кернелот 2.4.

Табела со симболи на јадрото

Веќе разговаравме за тоа како алатката insmod ја користи табелата со јавни симболи на кернелот кога поврзува модул со јадрото. Оваа табела ги содржи адресите на глобалните објекти на кернелот - функции и променливи - кои се потребни за да се имплементираат модуларните опции на двигателот. Табелата со јавни симболи на кернелот може да се чита во текстуална форма од датотеката /proc/ksyms, под услов вашиот кернел да го поддржува датотечен систем /proc.

Во кернелот 2.6, /proc/ksyms беше преименуван во /proc/modules.

Кога модулот е вчитан, симболите извезени од модулот стануваат дел од табелата со симболи на јадрото и можете да ги прегледате во /proc/ksyms.

Новите модули можат да користат симболи извезени од вашиот модул. На пример, модулот msdos се потпира на знаци извезени од модулот за масти, а секој USB-уред што се користи во режимот за читање користи знаци од USB-core и влезните модули. Овој однос, реализиран со секвенцијално вчитување на модулите, се нарекува стек на модули.

Магацинот на модули е погодно да се користи при креирање сложени проекти на модули. Оваа апстракција е корисна за одвојување на кодот на двигателот на уредот на делови зависни од хардвер и хардверски независни. На пример, множеството на двигатели видео-за-линукс се состои од основен модул кој извезува симболи за драјвер на ниско ниво што ги зема предвид спецификите на хардверот што се користи. Според вашата конфигурација, го вчитувате главниот видео модул и модулот специфичен за вашиот хардвер. На ист начин, имплементирана е поддршка за паралелни порти и широка класа на поврзани уреди, како што се USB-уреди. Паралелниот систем за пристаниште е прикажан на сл. 2-2. Стрелките ја прикажуваат интеракцијата помеѓу модулите и програмскиот интерфејс на кернелот. Интеракцијата може да се изврши и на ниво на функции и на ниво на структури на податоци управувани од функции.

Слика 2-2. Паралелна порта модул магацинот

Кога користите модули на стек, погодно е да се користи алатката modprobe. Функционалноста на алатката modprobe на многу начини е слична на алатката insmod, но кога се вчитува модул, таа ги проверува неговите основни зависности и, доколку е потребно, ги вчитува потребните модули додека не се пополни бараниот куп на модули. Така, една команда modprobe може да резултира со повеќе повици до командата insmod. Може да се каже дека командата modprobe е интелигентна обвивка околу insmod. Можете да користите modprobe наместо insmod насекаде, освен кога вчитувате сопствени модули од тековниот директориум, бидејќи modprobe гледа само во одредени директориуми на модули и нема да може да ги задоволи можните зависности.

Поделбата на модулите на делови помага да се намали времето за развој со поедноставување на дефиницијата на проблемот. Ова е слично на поделбата помеѓу механизмот за имплементација и контролната политика, што е дискутирано во Поглавје 1, „Вовед во двигатели на уреди“.

Вообичаено, модулот ја имплементира својата функционалност без воопшто да има потреба да извезува симболи. Ќе треба да извезувате симболи ако другите модули можат да имаат корист од тоа. Можеби ќе треба да вклучите посебна директива за да спречите извоз на нестатични знаци, бидејќи Повеќето имплементации на модутили стандардно ги извезуваат сите.

Датотеките за заглавие на кернелот на Линукс нудат пригоден начин за контрола на видливоста на вашите симболи, со што се спречува загадувањето на просторот за имиња на табелата со симболи на јадрото. Механизмот опишан во ова поглавје работи во кернели почнувајќи од верзијата 2.1.18. Кернел 2.0 имаше сосема поинаков контролен механизам
видливоста на симболот, која ќе биде опишана на крајот од поглавјето.

Ако вашиот модул воопшто не треба да извезува симболи, можете експлицитно да го поставите следниов макро повик во изворната датотека на модулот:

EXPORT_NO_SYMBOLS;

Овој макро повик, дефиниран во датотеката linux/module.h, се проширува во директива за асемблер и може да се специфицира каде било во модулот. Меѓутоа, кога креирате код што е пренослив на различни кернели, неопходно е да се постави овој макро повик во функцијата за иницијализација на модулот (init_module), бидејќи верзијата на ова макро што ја дефиниравме во нашата датотека sysdep.h за постари верзии на кернелот ќе работи само овде.

Од друга страна, ако треба да извезете некои од симболите од вашиот модул, тогаш треба да користите макро симбол
EXPORT_SYMTAB. Овој макро симбол мора да биде дефиниран предсо вклучување на датотеката за заглавие module.h. Тоа е вообичаена практика
дефинирање на овој макро знак преку знаменце во Makefile.

Ако макро симболот EXPORT_SYMTABдефинирано, тогаш поединечните симболи може да се извезат со користење на пар макроа:

EXPORT_SYMBOL(име); EXPORT_SYMBOL_NOVERS (име);

Секое од овие две макроа ќе го направи дадениот симбол достапен надвор од модулот. Разликата е во тоа што макро EXPORT_SYMBOL_NOVERSго извезува симболот без информации за верзијата (види Поглавје 11 „kmod и напредна модуларизација“). За повеќе детали
проверете ја датотеката со заглавие , иако наведеното е сосема доволно за практична употреба
макроа.

Иницијализирање и комплетирање на модулите

Како што споменавме, функцијата init_module() ги регистрира функционалните компоненти на модулот со кернелот. По таквата регистрација, апликацијата што го користи модулот ќе има пристап до влезните точки на модулот преку интерфејсот обезбеден од кернелот.

Модулите можат да регистрираат многу различни компоненти, кои, кога се регистрирани, се имињата на функциите на модулот. Покажувач до структура на податоци што содржи покажувачи на функции кои ја имплементираат предложената функционалност се пренесува до функцијата за регистрација на јадрото.

Во Поглавје 1, „Вовед во драјвери за уреди“, беше спомната класификацијата на главните типови уреди. Можете да ги регистрирате не само типовите уреди споменати таму, туку и сите други, дури и софтверски апстракции, како што се, на пример, /proc датотеки, итн. Сè што може да работи во кернелот преку програмскиот интерфејс на драјверот може да се регистрира како драјвер .

Ако сакате да дознаете повеќе за типовите на драјвери што се регистрираат користејќи го вашиот кернел како пример, можете да спроведете пребарување за поднизата EXPORT_SYMBOL во изворите на јадрото и да ги најдете влезните точки понудени од различните двигатели. Како по правило, функциите за регистрација користат префикс во нивното име регистрирај_,
така што друг можен начин да ги најдете е да барате подниза регистрирај_во датотеката /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; /* регистрацијата зема покажувач и име */ err = register_this(ptr1, „череп“); ако (err) goto fail_this; err = register_that(ptr2, „череп“); ако (грешка) goto fail_that; err = register_those (ptr3, "череп"); ако (греш) goto fail_those; врати 0; /* успех */ fail_those: unregister_that(ptr2, "череп"); fail_that: unregister_this(ptr1, " череп"); fail_this: врати грешка; /* пропагирајте ја грешката */ )

Овој пример се обидува да регистрира три компоненти на модулот. Изјавата goto се користи кога ќе се појави грешка при регистрација и предизвикува регистрираните компоненти да се одјават пред да се запре вчитувањето на модулот.

Друг пример за користење на изјавата goto за да се олесни читањето на кодот е трикот со „запомнување“ на успешни регистрации на модули и повикување cleanup_module() за да се пренесе оваа информација кога ќе се појави грешка. Функцијата cleanup_module() е дизајнирана да ги врати завршените операции за иницијализација и автоматски се повикува кога модулот се растоварува. Вредноста што ја враќа функцијата init_module() мора да биде
претставуваат код за грешка за иницијализација на модулот. Во кернелот на Линукс, кодот за грешка е негативен број од збир на дефиниции направени во датотеката за заглавие . Вклучете ја оваа датотека со заглавие во вашиот модул за да користите симболична мнемоника за резервирани кодови за грешки како што се -ENODEV, -ENOMEM, итн. Користењето на такви мнемоници се смета за добар стил на програмирање. Сепак, треба да се забележи дека некои верзии на услужните програми од пакетот modutils не ги обработуваат правилно вратените кодови за грешка и ја прикажуваат пораката „Уредот е зафатен“
како одговор на цела група грешки од сосема поинаква природа вратени од функцијата init_modules(). Во најновите верзии на пакетот ова
Досадната грешка е поправена.

Кодот на функцијата cleanup_module() за горенаведениот случај може, на пример, да биде вака:

Void cleanup_module(void) ( unregister_those (ptr3, „череп“); unregister_that(ptr2, „череп“); unregister_this(ptr1, „skull“); return; )

Ако вашиот код за иницијализација и завршување е покомплексен отколку што е опишано овде, тогаш користењето на изјавата goto може да резултира со тешко читлив текст на програмата бидејќи завршниот код мора да се повтори во функцијата init_module() со користење на повеќе ознаки за goto транзиции. Поради оваа причина, попаметен трик е да се користи повик до функцијата cleanup_module() во функцијата init_module(), пренесувајќи информации за степенот на успешна иницијализација кога ќе се појави грешка при вчитување на модулот.

Подолу е пример за тоа како се пишуваат функциите init_module() и cleanup_module(). Овој пример користи глобално дефинирани покажувачи кои носат информации за опсегот на успешната иницијализација.

Структирај нешто *точка1; struct somethingelse *item2; int stuff_ok; void cleanup_module(void) ( if (item1) release_thing(item1); if (item2) release_thing2(item2); if (stuff_ok) unregister_stuff(); return; ) int init_module(void) ( int err = -ENOMEM; item1 = allocate_thing (аргументи); item2 = allocate_thing2(arguments2); ако (!item2 || !item2) не успее; err = register_stuff(item1, item2); ако (!err) stuff_ok = 1; друго не успее; врати 0; /* успех */ неуспех: cleanup_module(); врати грешка; )

Во зависност од сложеноста на операциите за иницијализација на вашиот модул, можеби ќе сакате да користите еден од методите наведени овде за да ги контролирате грешките при иницијализацијата на модулот.

Бројач за користење на модулот

Системот содржи бројач на употреба за секој модул со цел да се утврди дали модулот може безбедно да се истовари. На системот му се потребни овие информации бидејќи модулот не може да се растовари ако е окупиран од некој или нешто - не можете да отстраните двигател на датотечен систем ако тој датотечен систем е монтиран или не можете да го растоварате модулот на уредот со знаци ако некој процес го користи овој уред. Во спротивно,
ова може да доведе до пад на системот - дефект на сегментација или паника на јадрото.

Во современите кернели, системот може да ви обезбеди автоматски бројач за користење на модулот користејќи механизам што ќе го разгледаме во следното поглавје. Без оглед на верзијата на јадрото, можете да користите рачна контрола на овој бројач. Така, кодот што треба да се користи во постарите верзии на кернелот треба да користи сметководствен модел за користење на модул изграден на следните три макроа:

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() кој го принудува бројачот на употреба на модулот на нула. Ова ќе го покриеме во делот „Користење на аргументот ioctl“ во Поглавје 5, „Подобрени операции на двигател на карактери“. Се разбира, во двигател подготвен за употреба, таквите измамнички манипулации со бројачот треба да се исклучат, меѓутоа, во фазата на дебагирање, тие заштедуваат време на развивачот и се сосема прифатливи.

Тековниот бројач за користење на системот за секој модул ќе го најдете во третото поле од секој запис во датотеката /proc/modules. Оваа датотека содржи информации за моментално вчитаните модули - една линија по модул. Првото поле од линијата го содржи името на модулот, второто поле е големината зафатена од модулот во меморијата, а третото поле е моменталната вредност на бројачот за употреба. Оваа информација, во форматирана форма,
може да се добие со повикување на алатката lsmod. Подолу е пример за датотека /proc/modules:

Parport_pc 7604 1 (autoclean) lp 4800 0 (неискористен) parport 8084 1 lockd 33256 1 (autoclean) sunrpc 56612 1 (autoclean) ds 6252 1 i82365 140 pc. 220

Овде гледаме неколку модули вчитани во системот. Во полето за знаменца (последното поле од линијата), купот на зависности на модулите се прикажува во квадратни загради. Меѓу другото, можете да забележите дека модулите за паралелна порта комуницираат преку стек на модули, како што е прикажано на сл. 2-2. Знамето (авточисто) ги означува модулите контролирани од kmod или кернелд. Ова ќе биде опфатено во Поглавје 11 „kmod и напредна модуларизација“). Знамето (неискористено) значи дека модулот моментално не се користи. Во кернелот 2.0, полето за големина прикажува информации не во бајти, туку на страници, кои за повеќето платформи се со големина од 4 kB.

Растоварување на модул

За истовар на модул, користете ја алатката 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) ( .... )

Употреба на атрибути __во тоаќе предизвика функцијата за иницијализација да се истовари од меморијата откако ќе заврши иницијализацијата. Сепак, ова работи само за вградени драјвери во кернелот и ќе биде игнорирано за модулите. Исто така, за драјверите вградени во кернелот, атрибутот __излезќе предизвика да се игнорира целата функција означена со овој атрибут. За модули, ова знаме исто така ќе биде игнорирано.

Користење на атрибути __во тоа__почетни податоциза опишување податоци) може да ја намали количината на меморија што ја користи кернелот. Знаме __во тоаФункцијата за иницијализација на модулот нема да донесе ниту корист ниту штета. Контролирањето на овој тип на иницијализација сè уште не е имплементирано за модулите, иако тоа може да биде можно во иднина.

Сумирајќи

Значи, како резултат на презентираниот материјал, можеме да ја претставиме следната верзија на модулот „Здраво свет“:

Код на изворната датотека на модулот ============================================== = #вклучи #вклучи #вклучи статички int __init my_init_module (празнина) (EXPORT_NO_SYMBOLS; printk("<1>Здраво свет\n"); врати 0; ); статична празнина __излез од my_cleanup_module (празнина) ( printk("<1>Збогум\n"); ); module_init (my_init_module); module_exit (my_cleanup_module); MODULE_LICENSE ("GPL"); ======================== ====================== Макефајл за составување на модулот ======================== =============== ==================== CFLAGS = -Ѕид -D__KERNEL__ -DMODULE -I/lib/modules/ $(shell unname -r)/build/include hello.o: =================================== ============================

Ве молиме имајте предвид дека при пишувањето на Makefile, ја користевме конвенцијата дека GNU make utility може самостојно да определи како да генерира објектна датотека врз основа на променливата CFLAGS и компајлерот достапен на системот.

Користење на ресурси

Модулот не може да ја заврши својата задача без користење на системски ресурси како што се меморија, влезни/излезни порти, В/И меморија, линии за прекин и канали DMA.

Како програмер, веќе треба да сте запознаени со динамичкото управување со меморијата. Динамичкото управување со меморијата во кернелот не е фундаментално различно. Вашата програма може да добие меморија користејќи ја функцијата kmalloc()и ослободи ја со помош kfree (). Овие функции се многу слични на функциите malloc() и free() што ви се познати, освен што на функцијата kmalloc() и се доставува дополнителен аргумент - приоритет. Обично приоритет е GFP_KERNEL или GFP_USER. GFP е акроним за „земи бесплатна страница“. Управувањето со динамичната меморија во кернелот е детално опфатено во Поглавје 7, „Добивање на меморијата“.

Почетен развивач на драјвери може да биде изненаден од потребата експлицитно да се распределат влезни/излезни порти, влезна/излезна меморија и линии за прекинување. Само тогаш модулот на јадрото може лесно да пристапи до овие ресурси. Иако системската меморија може да се распредели насекаде, В/И меморијата, портите и линиите за прекин играат посебна улога и се распределуваат поинаку. На пример, возачот треба да одвои одредени порти, не
сè, освен оние што му се потребни за да го контролира уредот. Но, возачот не може да ги користи овие ресурси додека не биде сигурен дека не ги користи некој друг.

Областа на меморија во сопственост на периферен уред обично се нарекува В/И меморија, за да се разликува од системската RAM (RAM), која едноставно се нарекува меморија.

Порти и I/O меморија

Работата на типичен двигател во голема мера се состои од порти за читање и пишување и I/O меморија. Портите и I/O меморијата се обединети со заедничко име - I/O регион (или област).

За жал, не секоја архитектура на автобусот може јасно да го дефинира I/O регионот што му припаѓа на секој уред, и можно е возачот да треба да ја погоди локацијата на регионот на кој припаѓа, па дури и да се обиде со операции за читање/запишување на можна адреса простори. Овој проблем е особено
се однесува на магистралата ISA, која сè уште се користи за инсталирање едноставни уреди во персонален компјутер и е многу популарна во индустрискиот свет во имплементацијата на PC/104 (видете го делот „PC/104 и PC/104+“ во Поглавје 15 „Преглед на периферни автобуси“).

Без оглед на магистралата што се користи за поврзување на хардверски уред, на двигателот на уредот мора да му се гарантира ексклузивен пристап до неговиот I/O регион за да се спречат судири меѓу драјверите. Ако модулот, пристапувајќи до сопствениот уред, пишува на уред што не му припаѓа, тоа може да доведе до фатални последици.

Програмерите на Linux имплементираа механизам за барање/ослободување I/O региони првенствено за да се спречат судири помеѓу различни уреди. Овој механизам долго време се користи за I/O порти и неодамна е генерализиран за управување со ресурсите воопшто. Забележете дека овој механизам претставува софтверска апстракција и не се проширува на хардверските способности. На пример, неовластениот пристап до влезните/излезни порти на ниво на хардвер не предизвикува грешка слична на „дефект на сегментација“, бидејќи хардверот не ги распределува и овластува своите ресурси.

Информациите за регистрираните ресурси се достапни во текстуална форма во датотеките /proc/ioports и /proc/iomem. Оваа информација е воведена во Linux уште од кернелот 2.3. За потсетување, оваа книга се фокусира првенствено на кернелот 2.4, а белешките за компатибилност ќе бидат претставени на крајот од поглавјето.

Пристаништа

Следното е типичната содржина на датотеката /proc/ioports:

0000-001f: dma1 0020-003f: pic1 0040-005f: тајмер 0060-006f: тастатура 0080-008f: dma страница reg 00a0-00bf: pic2 00c0-0000df: 77: ide1 01f0-01f7 : ide0 02f8-02ff: сериски (сет) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga+ 03f6-03f6: ide0 03f8-03ff: сериски (сет) 1000-1000: Intelligence (множество) 1000-1000: 000 -1003 : acpi 1004-1005: acpi 1008-100b: acpi 100c-100f: acpi 1100-110f: Intel Corporation 82371AB PIIX4 IDE 1300-131f: pcnet_cs 1400: Intel3PI70ABation -18ff: PCI CardBus #02 1c00- 1cff: PCI CardBus #04 5800-581f: Intel Corporation 82371AB PIIX4 USB d000-dfff: PCI Bus #01 d000-d0ff: ATI Technologies Inc 3D Rage LT Pro AGP-133

Секоја линија од оваа датотека го прикажува хексадецимално опсегот на порти поврзани со возачот или сопственикот на уредот. Во претходните верзии на кернелот, датотеката го имаше истиот формат, освен што хиерархијата на портата не беше прикажана.

Датотеката може да се користи за да се избегнат судири на пристаништа при додавање нов уред во системот. Ова е особено погодно кога рачно ја конфигурирате инсталираната опрема со префрлување џемпери. Во овој случај, корисникот може лесно да ја види листата на користени порти и да избере слободен опсег за уредот што треба да се инсталира. И иако повеќето модерни уреди воопшто не користат рачни џемпери, тие сè уште се користат во производството на компоненти од мали размери.

Она што е поважно е дека датотеката /proc/ioports има програмски достапна структура на податоци поврзана со неа. Затоа, кога двигателот на уредот ќе се иницијализира, може да го знае зафатениот опсег на I/O порти. Ова значи дека ако е неопходно да се скенираат пристаништа во потрага по нов уред, возачот може да ја избегне ситуацијата на пишување на пристаништа окупирани од други уреди.

Скенирањето на автобусот ISA е познато дека е ризична задача. Затоа, некои драјвери дистрибуирани со официјалниот кернел на Линукс избегнуваат такво скенирање при вчитување на модулот. Со тоа, тие го избегнуваат ризикот од оштетување на системот што работи со запишување на пристаништа што ги користи друга опрема. За среќа, модерните автобуски архитектури се имуни на овие проблеми.

Софтверскиот интерфејс што се користи за пристап до I/O регистрите се состои од следните три функции:

Int check_region (непотпишан долг почеток, непотпишан долг len); struct ресурс *request_region(непотпишан долг почеток, непотпишан долг len, char *име); void release_region (непотпишан долг почеток, непотпишан долг len);

Функција check_region()може да се повика да провери дали одреден опсег на порти е окупиран. Тој враќа негативен код за грешка (како -EBUSY или -EINVAL) ако одговорот е негативен.

Функција барање_регион()врши распределба на даден опсег на адреси, враќајќи, доколку е успешна, не-нулти покажувач. Возачот не треба да го складира или користи вратениот покажувач. Сè што треба да направите е да проверите дали има NULL. Кодот што треба да работи само со 2.4 (или повисоко) јадро не треба воопшто да ја повикува функцијата check_region(). Несомнено е предноста на овој начин на дистрибуција, бидејќи
не е познато што може да се случи помеѓу повиците до check_region() и request_region(). Ако сакате да ја одржите компатибилноста со постарите верзии на кернелот, тогаш е неопходно да се повика check_region() пред request_region().

Функција release_region()мора да се повика кога возачот ги ослободува претходно користените порти.

Вистинската вредност на покажувачот вратен од request_region() се користи само од потсистемот за распределба на ресурси што работи во кернелот.

Овие три функции се всушност макроа дефинирани во .

Подолу е пример за низата на повици што се користи за регистрирање на порти. Примерот е земен од кодот на возачот за обука на черепот. (Функцискиот код skull_probe_hw() не е прикажан овде бидејќи содржи код кој зависи од хардверот.)

#вклучи #вклучи статичен инт skull_detect (неозначен влезен порт, неозначен внатрешен опсег) ( int er; ако ((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() не треба, во овој пример,
ќе заврши со неуспех. Јадрото не може да вчита повеќе од еден модул истовремено, така што нема да се појават судири за користење на портата
мора.

Сите влезни/излезни порти доделени од возачот мора последователно да се ослободат. Нашиот двигател на черепот го прави ова во функцијата cleanup_module():

Статичка празнина череп_издавање (непотпишана влезна порта, неозначен внатрешен опсег) (real_region (порта, опсег);

Механизмот за барање/ослободување на ресурси е сличен на механизмот за регистрација/одјавување на модулот и е совршено имплементиран врз основа на шемата за користење на операторот goto опишана погоре.

Меморија

Информациите за I/O меморијата се достапни преку датотеката /proc/iomem. Подолу е типичен пример за таква датотека за персонален компјутер:

00000000-0009fbff: Системска RAM 0009fc00-0009ffff: резервирани 000a0000-000bffff: Област на RAM за видео 000c0000-000c7fff: Видео ROM 000f0000-0000-00000-00000 Систем 0000ff 0 0100000-0022c557: код на јадрото 0022c558-0024455f: Податоци за јадрото 20000000 - 2fffffff: Intel Corporation 440BX/ZX - 82443BX/ZX Host bridge 68000000-68000fff: Texas Instruments PCI1225 68001000-68001fff: Texas Instruments PCI12010: Texas Instruments #00ff 00000-e7 ffffff: PCI автобус #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: резервирано

Вредностите на опсегот на адреси се прикажани во хексадецимална нотација. За секој опсег на ари, е прикажан неговиот сопственик.

Регистрирањето на влезните/излезни пристапи во меморијата е слично на регистрирањето на влезните/излезни порти и е изградено на истиот механизам во кернелот.

За да го добие и ослободи потребниот опсег на адреси на В/И меморија, возачот мора да ги користи следните повици:

Int check_mem_region (непотпишан долг почеток, непотпишан долг len); int request_mem_region(непотпишан долг почеток, непотпишан долг len, char *име); int release_mem_region (непотпишан долг почеток, непотпишан долг len);

Вообичаено, возачот го знае опсегот на адреси на I/O меморија, така што кодот за доделување на овој ресурс може да се намали во споредба со примерот за доделување опсег на порти:

Ако (check_mem_region(mem_addr, mem_size)) ( printk("име на возачот: меморија веќе се користи\n"); врати -EBUSY; ) request_mem_region(mem_addr, mem_size, "drivername");

Распределба на ресурси во Linux 2.4

Тековниот механизам за распределба на ресурси беше воведен во Linux кернелот 2.3.11 и обезбедува флексибилен пристап до управувањето со системските ресурси. Овој дел накратко го опишува овој механизам. Сепак, основните функции за распределба на ресурси (како што е request_region(), итн.) сè уште се имплементираат како макроа и се користат за компатибилност наназад со претходните верзии на кернелот. Во повеќето случаи не треба да знаете ништо за вистинскиот механизам за дистрибуција, но може да биде интересно кога креирате посложени двигатели.

Системот за управување со ресурси имплементиран во Linux може да управува со произволни ресурси на унифициран хиерархиски начин. Глобалните системски ресурси (на пример, I/O порти) може да се поделат на подмножества - на пример, оние поврзани со одреден хардверски отвор за автобус. Одредени двигатели, исто така, може опционално да ги поделат заробените ресурси врз основа на нивната логичка структура.

Опсегот на доделени ресурси е опишан преку структурата на ресурсите на структурата, која е декларирана во датотеката за заглавие :

Struct ресурс ( const char *име; непотпишан долг почеток, крај; непотпишан долги знаменца; struct ресурс *родител, *брат или сестра, *дете; );

Глобален (root) опсег на ресурси се создава при подигање. На пример, структура на ресурси што ги опишува влезните/излезни порти е креирана на следниов начин:

Структурен ресурс ioport_resource = ("PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO);

Овде е опишан ресурс наречен PCI IO, кој го покрива опсегот на адреси од нула до IO_SPACE_LIMIT. Вредноста на оваа променлива зависи од платформата што се користи и може да биде еднаква на 0xFFFF (16-битен адресен простор, за архитектури x86, IA-64, Alpha, M68k и MIPS), 0xFFFFFFFF (32-битен адресен простор, за SPARC, PPC , SH) или 0xFFFFFFFFFFFFFFFF (64-битна, SPARC64).

Подопсезите на овој ресурс може да се креираат со помош на повик до allocate_resource(). На пример, при иницијализација на магистралата PCI, се креира нов ресурс за адресниот регион на оваа магистрала и се доделува на физички уред. Кога кодот на кернелот PCI ги обработува задачите на портите и меморијата, тој создава нов ресурс само за тие региони и ги распределува користејќи повици до ioport_resource() или iomem_resource().

Возачот потоа може да побара подмножество на ресурс (обично дел од глобален ресурс) и да го означи како зафатен. Стекнувањето на ресурсите се постигнува со повикување request_region(), што враќа или покажувач на нова структура на ресурс што го опишува бараниот ресурс, или NULL на грешка. Оваа структура е дел од дрвото на глобалните ресурси. Како што веќе беше споменато, по добивањето на ресурсот, на возачот нема да му треба вредноста на овој покажувач.

Заинтересираниот читател може да ужива во прегледувањето на деталите за оваа шема за управување со ресурси во датотеката kernel/resource.c која се наоѓа во директориумот извори на јадрото. Сепак, за повеќето програмери веќе презентираното знаење ќе биде доволно.

Слоевит механизам за распределба на ресурси носи двојни придобивки. Од една страна, тој дава визуелна претстава на структурите на податоци на јадрото. Ајде повторно да ја разгледаме примерната датотека /proc/ioports:

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

Опсегот e800-e8ff е доделен на адаптерот Adaptec, кој се назначи себеси како двигател на магистралата PCI. Поголемиот дел од овој опсег беше побаран од возачот aic7xxx.

Друга предност на ова управување со ресурсите е поделбата на адресниот простор на подопсези кои ја рефлектираат вистинската меѓусебна поврзаност на опремата. Управувачот со ресурси не може да додели преклопувачки подопсези на адреси, што може да спречи инсталирање на двигател што не функционира.

Автоматска и рачна конфигурација

Некои параметри што ги бара возачот може да варираат од систем до систем. На пример, возачот мора да биде свесен за валидни I/O адреси и опсег на меморија. За добро организирани автобуски интерфејси ова не е проблем. Сепак, понекогаш ќе треба да му пренесете параметри на драјверот за да му помогнете да го пронајде сопствениот уред или да овозможите/оневозможите некои од неговите функции.

Овие поставки кои влијаат на работата на возачот се разликуваат во зависност од уредот. На пример, ова може да биде бројот на верзијата на инсталираниот уред. Се разбира, таквите информации се неопходни за возачот да работи правилно со уредот. Дефинирањето на таквите параметри (конфигурација на двигателот) е сосема неверојатно
незгодна задача што се изведува кога возачот е иницијализиран.

Вообичаено, постојат два начини да се добијат точните вредности на овој параметар - или корисникот ги дефинира експлицитно, или возачот ги одредува независно, врз основа на анкетирање на опремата. Иако автоматското откривање е несомнено најдоброто решение за конфигурација на возачот,
прилагодената конфигурација е многу полесна за имплементација. Развивачот на драјверот треба да имплементира автоматска конфигурација на драјверот секогаш кога е можно, но во исто време, тој треба да му обезбеди на корисникот механизам за рачна конфигурација. Се разбира, рачната конфигурација треба да има поголем приоритет од автоматската конфигурација. Во почетните фази на развој, обично се спроведува само рачен пренос на параметри до возачот. Автоматската конфигурација, ако е можно, се додава подоцна.

Многу драјвери, меѓу нивните конфигурациски параметри, имаат параметри кои ги контролираат операциите на драјверите. На пример, двигателите за интерфејс за интегрирана електроника на уреди (IDE) му дозволуваат на корисникот да ги контролира операциите на DMA. Значи, ако вашиот возач добро работи со автоматско откривање на хардверот, можеби ќе сакате да му дадете на корисникот контрола врз функционалноста на возачот.

Вредностите на параметрите може да се пренесат при вчитување на модулот со помош на командите insmod или modprobe. Неодамна стана возможно да се прочита вредноста на параметрите од конфигурациската датотека (обично /etc/modules.conf). Цели броеви и стринг вредности може да се пренесат како параметри. Така, ако треба да пренесете цел број за параметарот skull_ival и вредност на низа за параметарот skull_sval, можете да ги пренесете при вчитување на модулот со дополнителни параметри на командата insmod:

Insmod череп skull_ival=666 skull_sval="ѕверот"

Сепак, пред командата insmod да ги промени вредностите на параметрите на модулот, модулот мора да ги направи достапни тие параметри. Параметрите се декларираат со користење на макро дефиницијата MODULE_PARM, која е дефинирана во датотеката за заглавие module.h. Макрото MODULE_PARM зема два параметри: името на променливата и низа што го дефинира нејзиниот тип. Оваа макро дефиниција мора да биде поставена надвор од која било функција и обично се наоѓа на почетокот на датотеката откако ќе се дефинираат променливите. Значи, двата параметри споменати погоре може да се декларираат на следниов начин:

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

Во моментов има поддржани пет типа параметри на модулот:

  • б - вредност од еден бајт;
  • h - (кратка) вредност од два бајти;
  • i - цел број;
  • l - долг цел број;
  • s - стринг (char *);

Во случај на параметри на стрингот, во модулот мора да се декларира покажувач (char *). Командата insmod распределува меморија за низата што е пренесена и ја иницијализира со потребната вредност. Користејќи го макрото MODULE_PARM, можете да иницијализирате низи од параметри. Во овој случај, цел број што му претходи на карактерот на типот ја одредува должината на низата. Кога ќе се наведат два цели броеви, разделени со цртичка, тие го одредуваат минималниот и максималниот број вредности што треба да се пренесат. За подетално разбирање за тоа како функционира ова макро, погледнете ја датотеката со заглавие .

На пример, да претпоставиме дека низа од параметри мора да се иницијализираат со најмалку две и најмалку четири цели броеви. Потоа може да се опише на следниов начин:

Int skull_array; MODULE_PARM (низа_череп, „2-4i“);

Дополнително, комплетот со алатки на програмерот има макро дефиниција MODULE_PARM_DESC, што ви овозможува да ставате коментари на параметрите на модулот што се пренесуваат. Овие коментари се зачувани во датотеката со објект на модулот и може да се гледаат со, на пример, алатката objdump или со користење на алатки за автоматска администрација на системот. Еве пример за користење на оваа макро дефиниција:

Int base_port = 0x300; MODULE_PARM(base_port, "i"); MODULE_PARM_DESC (base_port, "Основната влезна/излезна порта (стандардно 0x300)");

Пожелно е сите параметри на модулот да имаат стандардни вредности. Промената на овие вредности со помош на insmod треба да биде потребна само доколку е потребно. Модулот може да го провери експлицитното поставување на параметрите со проверка на нивните тековни вредности со стандардните вредности. Последователно, можете да имплементирате механизам за автоматска конфигурација врз основа на следниот дијаграм. Ако вредностите на параметрите имаат стандардни вредности, тогаш се врши автоматска конфигурација. Во спротивно, се користат тековните вредности. За да може оваа шема да функционира, неопходно е стандардните вредности да не одговараат на ниту една од можните конфигурации на системот во реалниот свет. Тогаш ќе се претпостави дека таквите вредности не може да ги постави корисникот во рачна конфигурација.

Следниот пример покажува како двигателот на черепот автоматски го открива адресниот простор на портите на уредот. Во горниот пример, автоматското откривање гледа на повеќе уреди, додека рачната конфигурација го ограничува драјверот на еден уред. Веќе ја запознавте функцијата skull_detect претходно во делот што ги опишува влезните/излезни порти. Имплементацијата на skull_init_board() не е прикажана затоа што
Врши иницијализација зависна од хардвер.

/* * опсег на портите: уредот може да биде помеѓу * 0x280 и 0x300, во чекори од 0x10. Користи порти 0x10. */ #define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 /* * следнава функција врши автоматско откривање, освен ако специфична * вредност е доделена од insmod на "skull_port_static_base" */ /* 0 присилува автоматско откривање */ MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC(skull_port_base, "Base I/O порта за череп"); static int skull_find_hw(void) /* го враќа # на уреди */ ( /* базата е или вредноста на време на вчитување или првата проба */ int base = skull_port_base ? skull_port_base: SKULL_PORT_FLOOR; int result = 0; /* loop one време ако е доделена вредност; испробајте ги сите ако автоматски открива */ направи ( ако (skull_detect(base, SKULL_PORT_RANGE) == 0) (skull_init_board(base); result++; ) base += SKULL_PORT_RANGE; /* подгответе се за следното пробно */ ) додека (череп_порта_база == 0 && основа< SKULL_PORT_CEIL); return result; }

Ако конфигурациските променливи се користат само внатре во драјверот (т.е. не се објавени во табелата со симболи на јадрото), тогаш програмерот може малку да му го олесни животот на корисникот со тоа што не користи префикси во имињата на променливите (во нашиот случај, префиксот skull_) . Овие префикси му значат малку на корисникот, а нивното отсуство го поедноставува пишувањето на командата од тастатурата.

За комплетност, ќе обезбедиме опис на уште три макро дефиниции кои ви дозволуваат да поставите некои коментари во датотеката со објектот.

MODULE_AUTHOR (име)Става линија со името на авторот во објектната датотека. MODULE_DESCRIPTION (опаѓање)Става линија со општ опис на модулот во објектната датотека. MODULE_SUPPORTED_DEVICE (dev)Поставува линија што го опишува уредот поддржан од модулот. Linux обезбедува моќен и обемен API за апликации, но понекогаш тоа не е доволно. За да комуницирате со хардверот или да извршите операции со пристап до привилегирани информации во системот, потребен е двигател на јадрото.

Модулот за кернелот на Линукс е компајлиран бинарен код кој се вметнува директно во кернелот на Линукс, кој работи во прстенот 0, внатрешниот и најмалку безбедниот прстен за извршување на инструкциите во процесорот x86–64. Овде кодот се извршува целосно без никакви проверки, но со неверојатна брзина и со пристап до сите системски ресурси.

Не за обични смртници

Пишувањето модул на кернелот на Линукс не е за оние со слабо срце. Со менување на кернелот, ризикувате да изгубите податоци. Нема стандардна безбедност во кодот на кернелот како што е во обичните апликации на Линукс. Ако направите грешка, тогаш спуштете го целиот систем.

Она што ја влошува ситуацијата е тоа што проблемот не мора да се појави веднаш. Ако модулот го закачи системот веднаш по вчитувањето, тогаш ова е најдоброто сценарио за неуспех. Колку повеќе код има, толку е поголем ризикот од бесконечни циклуси и протекување на меморијата. Ако не сте внимателни, проблемите постепено ќе се зголемуваат додека машината работи. На крајот, важни структури на податоци, па дури и бафери може да се презапишат.

Традиционалните парадигми за развој на апликации може во голема мера да се заборават. Покрај вчитувањето и растоварувањето на модулот, ќе пишувате код кој реагира на системските настани наместо да следи последователна шема. Кога работите со кернелот, го пишувате API, а не самите апликации.

Исто така, немате пристап до стандардната библиотека. Иако кернелот обезбедува некои функции како printk (што е замена за printf) и kmalloc (кој работи слично на malloc), најмногу сте препуштени на хардверот. Дополнително, треба целосно да се исчистите себеси по растоварувањето на модулот. Тука нема собирање ѓубре.

Потребни компоненти

Пред да започнете, треба да бидете сигурни дека ги имате сите потребни алатки за работата. Што е најважно, ви треба Линукс машина. Знам дека ова е неочекувано! Иако секоја дистрибуција на Линукс ќе го направи тоа, јас користам Ubuntu 16.04 LTS во овој пример, па можеби ќе треба малку да ги измените командите за инсталација ако користите други дистрибуции.

Второ, потребна ви е или посебна физичка машина или виртуелна машина. Лично, претпочитам да работам на виртуелна машина, но изберете го вашиот избор. Не препорачувам да ја користите вашата главна машина поради губење на податоци кога ќе направите грешка. Велам „кога“, а не „ако“ затоа што дефинитивно ќе го закачите автомобилот барем неколку пати во текот на процесот. Вашите најнови промени на кодот може сè уште да бидат во баферот за запишување кога кернелот е во паника, така што вашите извори исто така може да бидат оштетени. Тестирањето во виртуелна машина ги елиминира овие ризици.

Конечно, треба да знаете барем малку C. Времето на извршување на C++ е преголемо за кернелот, така што треба да пишувате во чиста, гола C. Некое познавање на асемблерскиот јазик е исто така корисно за интеракција со хардверот.

Инсталирање на развојната средина

На Ubuntu треба да извршите:

Apt-get install build-essential Linux-headers-`uname -r`
Ги инсталираме најважните развојни алатки и заглавија на јадрото потребни за овој пример.

Примерите подолу претпоставуваат дека работите како обичен корисник наместо како root, но дека имате привилегии за sudo. Потребно е Sudo за вчитување на модулите на јадрото, но ние сакаме да работиме надвор од root секогаш кога е можно.

Започнете

Да почнеме да пишуваме код. Да ја подготвиме нашата околина:

Mkdir ~/src/lkm_example cd ~/src/lkm_example
Стартувајте го вашиот омилен уредник (во мојот случај vim) и креирајте датотека lkm_example.c со следнава содржина:

#вклучи #вклучи #вклучи MODULE_LICENSE („GPL“); MODULE_AUTHOR („Роберт В. Оливер II“); MODULE_DESCRIPTION („Едноставен пример за Linux модул.“); MODULE_VERSION („0.01“); static int __init lkm_example_init(void) ( printk(KERN_INFO „Здраво, свето!\n“); врати 0; ) статична празнина __exit lkm_example_exit(void) (printk(KERN_INFO „Збогум, Свет!\n“); ) ); module_exit (lkm_example_exit);
Го дизајниравме наједноставниот можен модул, ајде внимателно да ги разгледаме неговите најважни делови:

  • вклучи списоци со датотеките за заглавие потребни за развој на кернелот Линукс.
  • MODULE_LICENSE може да се постави на различни вредности во зависност од лиценцата на модулот. За да ја видите целата листа, стартувајте:

    Греп „MODULE_LICENSE“ -B 27 /usr/src/linux-headers-`unname -r`/include/linux/module.h

  • Ги поставуваме init (loading) и exit (unloading) како статични функции кои враќаат цели броеви.
  • Забележете ја употребата на printk наместо printf. Исто така, опциите за printk се различни од printf. На пример, знаменцето KERN_INFO за прогласување приоритет на евиденција за одредена линија е наведено без запирка. Јадрото се справува со овие работи во функцијата printk за да ја зачува меморијата на магацинот.
  • На крајот од датотеката, можете да ги повикате module_init и module_exit и да ги наведете функциите за вчитување и растоварување. Ова овозможува произволно именување на функциите.
Сепак, сè уште не можеме да ја составиме оваа датотека. Потребен е макетафил. Овој основен пример засега ќе биде доволен. Забележете дека марка е многу пребирлива за празни места и картички, затоа не заборавајте да користите картички наместо празни места каде што е соодветно.

Obj-m += lkm_example.o сите: направи -C /lib/modules/$(shell uname -r)/build M=$(PWD) модули чисти: направи -C /lib/modules/$(shell uname -r )/изгради M=$(PWD) чист
Ако го извршиме make, треба успешно да го компајлира нашиот модул. Резултатот ќе биде датотеката lkm_example.ko. Ако се појават грешки, проверете дали наводниците во изворниот код се поставени правилно и не случајно во кодирањето UTF-8.

Сега можете да го имплементирате модулот и да го тестирате. За да го направите ова трчаме:

Sudo insmod lkm_пример.ко
Ако сè е во ред, тогаш нема да видите ништо. Функцијата printk обезбедува излез не до конзолата, туку до дневникот на јадрото. За да видите, треба да извршите:

Судо dmesg
Треба да ја видите линијата "Здраво, свет!" со временски печат на почетокот. Ова значи дека нашиот модул на кернелот е вчитан и успешно запишан во дневникот на јадрото. Можеме да провериме и дали модулот е сè уште во меморија:

lsmod | grep „lkm_example“
За да отстраните модул, извршете:

Судо rmmod lkm_пример
Ако повторно го извршите dmesg, ќе го видите записот „Збогум, свет!“ во дневникот. Можете повторно да извршите lsmod и да бидете сигурни дека модулот е истоварен.

Како што можете да видите, оваа процедура за тестирање е малку досадна, но може да се автоматизира со додавање:

Тест: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
на крајот од Makefile и потоа работи:

Направете тест
да го тестирате модулот и да го проверите излезот во дневникот на јадрото без да мора да извршувате посебни команди.

Сега имаме целосно функционален, иако целосно тривијален, модул на јадрото!

Ајде да копаме малку подлабоко. Иако модулите на јадрото се способни да извршуваат секакви задачи, интерфејсот со апликациите е еден од најчестите случаи на употреба.

Бидејќи на апликациите не им е дозволено да гледаат меморија во просторот на јадрото, тие мора да користат API за да комуницираат со нив. Иако технички има неколку начини да го направите ова, најчестиот е да креирате датотека на уред.

Веројатно претходно сте се занимавале со датотеки на уредот. Наредбите кои ги спомнуваат /dev/zero , /dev/null и слично комуницираат со уредите „zero“ и „null“, кои ги враќаат очекуваните вредности.

Во нашиот пример враќаме „Здраво, свето“. Иако ова не е особено корисна карактеристика за апликациите, сепак го демонстрира процесот на интеракција со апликација преку датотека на уред.

Еве го целосниот список:

#вклучи #вклучи #вклучи #вклучи #вклучи MODULE_LICENSE („GPL“); MODULE_AUTHOR („Роберт В. Оливер II“); MODULE_DESCRIPTION („Едноставен пример за Linux модул.“); MODULE_VERSION („0.01“); #define DEVICE_NAME „lkm_example“ #define EXAMPLE_MSG „Здраво, свето!\n“ #define MSG_BUFFER_LEN 15 /* Прототипови за функции на уредот */ static int device_open(struct inode *, struct file *); static int device_release (struct inode *, struct file *); статична ssize_t уред_читање (структурна датотека *, char *, size_t, loff_t *); статична ssize_t device_write (struct file *, const char *, size_t, loff_t *); статички int major_num; статички int уред_отворен_број = 0; статичен знак msg_buffer; статичен знак *msg_ptr; /* Оваа структура укажува на сите функции на уредот */ статична структура file_operations file_ops = ( .read = уред_читање, .write = уред_пишување, .open = device_open, .release = device_release ); /* Кога некој процес чита од нашиот уред, тој се повикува. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) ( int bytes_read = 0; /* Ако сме на крајот, врати се на почетокот */ ако (*msg_ptr = = 0) ( msg_ptr = msg_buffer; ) /* Ставете податоци во баферот */ додека (len && *msg_ptr) ( /* Баферот е во податоците на корисникот, не во јадрото, така што не можете само да упатувате * со покажувач. функцијата put_user се справува со ова за нас */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; ) return bytes_read; ) /* Се повикува кога процесот се обидува да напише на нашиот уред */ static ssize_t device_write(struct file * flip, const char *buffer, size_t len, loff_t *offset) ( /* Ова е уред само за читање */ printk(KERN_ALERT „Оваа операција не е поддржана.\n“); врати -EINVAL; ) /* Се повикува кога процес го отвора нашиот уред */ static int device_open(struct inode *inode, struct file *file) ( /* Ако уредот е отворен, врати зафатен */ if (device_open_count) ( return -EBUSY; ) device_open_count++; try_module_get(THIS_MODULE); врати 0; ) /* Се повикува кога некој процес го затвора нашиот уред */ static int device_release(struct inode *inode, struct file *file) ( /* Намалете го отворениот бројач и бројот на користење. Без ова, модулот нема да се растоварува. */ device_open_count- -; module_put (THIS_MODULE); врати 0; ) статички int __init lkm_example_init(void) ( /* Пополнете го тампонот со нашата порака */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Поставете ја msg_ptr =buffer_msgms ; /* Обидете се да регистрирате уред со знаци */ major_num = register_chrdev(0, „lkm_example“, &file_ops); ако (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 сите: направи -C /lib/modules/$(shell uname -r)/build M=$(PWD) модули чисти: направи -C /lib/modules/$(shell uname -r )/build M=$(PWD) чист тест: # Ставаме - пред командата rmmod за да кажеме да игнорира # грешка во случај модулот да не е вчитан. -sudo rmmod lkm_example # Избришете го дневникот на јадрото без ехо sudo dmesg -C # Вметнете го модулот sudo insmod lkm_example.ko # Прикажете го дневникот на јадрото dmesg
Сега, откако ќе го извршите тестот за правење, ќе видите како излегува главниот број на уредот. Во нашиот пример, тој автоматски се доделува од кернелот. Сепак, овој број е потребен за да се создаде нов уред.

Земете го бројот генериран од тестот за правење и искористете го за да креирате датотека на уред за да можеме да комуницираме со нашиот модул на јадрото од корисничкиот простор.

Судо mknod /dev/lkm_example со ГОЛЕМИ 0
(во овој пример, заменете го MAJOR со вредноста добиена од тестот за правење или dmesg)

Опцијата c во командата mknod му кажува на mknod дека треба да создадеме датотека за уред со знаци.

Сега можеме да добиеме содржина од уредот:

Мачка /dev/lkm_example
или дури и преку командата dd:

Dd if=/dev/lkm_example of=тест bs=14 count=100
Може да пристапите до оваа датотека и од апликации. Овие не мора да бидат компајлирани апликации - дури и Python, Ruby и PHP скриптите имаат пристап до овие податоци.

Кога ќе завршиме со уредот, го отстрануваме и го растовараме модулот:

Судо rm /dev/lkm_пример sudo rmmod lkm_example

Заклучок

Се надевам дека уживавте во нашите шеги во основниот простор. Иако прикажаните примери се примитивни, овие структури може да се користат за креирање на сопствени модули кои извршуваат многу сложени задачи.

Само запомнете дека во просторот на јадрото сè е ваша одговорност. Нема поддршка или втора шанса за вашиот код. Ако правите проект за клиент, планирајте однапред за двојно, ако не и тројно време за дебагирање. Кодот на јадрото мора да биде што е можно посовршен за да обезбеди интегритет и доверливост на системите на кои работи.

Некои карактеристики на модуларното програмирање и општи препораки за конструирање на потпрограми на модуларна структура.

Модулите се поврзани со главната програма по редоследот по кој се прогласени за USES, а по истиот редослед се и иницијализирачките блокови на модулите поврзани со главната програма пред програмата да започне со извршување.

Редоследот по кој се извршуваат модулите може да влијае на пристапот до библиотечните податоци и рутинскиот пристап.

На пример, ако модулите со имињата M1, M2 содржат ист тип A, променлива B и потпрограма C, тогаш по поврзувањето на овие модели USES, повиците до A, B, C во оваа PU ќе бидат еквивалентни на повици кон објекти до модулот M2 .

Но, за да се карактеризира исправноста на повиците до потребните објекти со исто име од различни поврзани модули, препорачливо е кога пристапувате до модул прво да го наведете името на модулот, а потоа со точка името на објектот: M1. A M1.B M1.C M2.B.

Очигледно, многу е лесно да се подели голема програма на два дела (PU), т.е. главна програма + модули.

Поставување на секој PU во сопствен мемориски сегмент и во сопствена датотека на дискот.

Сите декларации на типови, како и оние променливи што треба да им бидат достапни на поединечни PU (главната програма и идните модули) треба да се стават во посебен модул со празен извршен дел. Сепак, не треба да обрнувате внимание на фактот дека некои ЈП (на пример, модул) не ги користат сите овие декларации. Делот за иницијација на таков модул може да вклучува изјави кои ги поврзуваат променливите на датотеки со нестандардни текстуални датотеки (ASSIGN) и ги иницираат овие датотеки, т.е. што укажува на повици за пренос на податоци за нив (RESET и REWRITE).

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

При дистрибуција на потпрограми во модули во сложен проект, посебно внимание треба да се посвети на редоследот и местото на нивното пишување.

Околината TP содржи алатки кои контролираат различни начини на компајлирање модули.

Компајлирај Alt+F9 RUN Cntr+F9

Меморија на дестинација

Овие режими се разликуваат само во начинот на комуникација и главната програма.

Режим на компајлирање

Ја составува главната програма или модул што моментално се наоѓа во прозорецот на активниот уредувач. Ако овој PU содржи пристап до нестандардни кориснички модули, тогаш овој режим бара присуство на диск-датотеки со исто име со наставката ___.tpu за секој таков приклучок модул.



Ако Дестинацијата е зачувана во меморија, тогаш овие датотеки остануваат само во меморијата, а датотеката на дискот не се креира.

Сепак, многу е полесно да се креираат tpu-датотеки заедно со компајлерот на целата програма користејќи други режими кои не бараат поставување на Disk за опцијата за дестинација.

Направете режим

При компајлирање во овој режим, прво се проверува следново (пред да се состави главната програма) за секој модул:

1) Постоење на датотека tpu на дискот; ако не постои, тогаш се креира автоматски со компајлирање на изворниот код на модулот, т.е. неговата pas датотека

2) Кореспонденција на пронајдената tpu датотека со изворниот текст на модулот, каде што можело да се направат промени; инаку датотеката tpu автоматски повторно се креира

3) Непроменливост на делот за интерфејс на модулот: ако е променет, тогаш се прекомпајлираат и сите оние модули (нивните изворни пас-датотеки) во кои овој модул е ​​наведен во клаузулата USES.

Ако немаше промена во изворните кодови на модулите, тогаш компајлерот комуницира со овие tpu датотеки и користи време на компилација.

Режим на градење

За разлика од режимот Направи, тој нужно бара присуство на датотеки со изворен пас; го компајлира (прекомпајлира) секој модул и на тој начин осигурува дека се земени предвид сите промени во текстовите на pas-датотеките. Ова го зголемува времето на компилација на програмата како целина.

За разлика од режимот за компајлирање, режимите Make и Build ви дозволуваат да започнете со компајлирање програма со модуларна структура од која било дадена датотека pas (тоа се нарекува примарна датотека), без оглед на тоа која датотека (или дел од програмата) е во активната прозорец на уредувачот. За да го направите ова, во ставката за компајлирање, изберете ја опцијата Примарна датотека, притиснете 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 е инсталиран на хард диск со целосна инсталација „Опција 2“, тогаш нема лична датотека, погледнете на веб-страниците на Puppy што треба да се компајлираат со различни опции за конфигурација, така што модулите не се компатибилни. Овие верзии бараат само еден лепенка за squashfs. Puppy 2.12 има кернел 2.6.18.1 и има три поправки; squashfs, стандардно логно ниво на конзола и поправка на исклучување.

Овие команди за да се закрпи кернелот се дадени исклучиво за ваше самообразование, бидејќи веќе е достапно закрпено јадро...

Првото нешто што треба да направите е да го преземете самиот кернел. Се наоѓа за да пронајдете врска до соодветната локација за преземање. Ова треба да биде „древен“ извор достапен на 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. Во спротивно, ќе добиете пораки за грешка „нерешени симболи“ кога ќе го компајлирате драјверот и потоа ќе се обидете да го користите со јадрото на 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, копирајте ја некаде привремено или преименувајте ја.

направи чиста направи mrproper

Копирајте ја датотеката .config за Puppy на /usr/src/linux-2.6.18.1... Ќе има различни имиња во /lib/modules, па преименувајте во .config... Следните чекори ја читаат датотеката .config и генерираат нов.

направи конфигурација на менито

...направете какви било промени што сакате и зачувајте ги. Сега ќе имате нова .config датотека и треба да ја копирате некаде за чување. Видете ја белешката подолу

направи 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 направи модули направи modules_install

...по ова треба да ги најдете новите модули инсталирани во папката /lib/modules/2.6.18.1.

Забележете дека последниот чекор погоре ја извршува програмата „depmod“ и тоа може да даде пораки за грешка за исчезнати симболи за некои од модулите. Не грижете се за тоа - еден од програмерите се зафркна и тоа значи дека не можеме да го користиме тој модул.

Како да го користите новиот кернел и модули

Подобро е ако имате инсталирано Puppy Unleashed. Потоа tarball се проширува и има 2 директориуми: "boot" и "kernels".

„Подигање“ ја содржи структурата на датотеката и скриптата за создавање на почетниот виртуелен диск. Ќе мора да ставите некои модули на кернелот таму.

Директориумот „кернели“ има директориум кернели/2.6.18.1/ и ќе треба да ги замените модулите со ваши ажурирани. Не мора да го замените ако повторно ја компајлирате истата верзија на кернелот (2.6.18.1).

Забележете дека во кернелите/2.6.18.1 постои датотека наречена „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).

Забелешка за конфигурацијата на менито. Го користам со векови, затоа земајте некои работи здраво за готово, но почетник може да биде збунет кога сака да ја напушти програмата. Во менито на највисоко ниво има мени за зачувување на конфигурацијата - игнорирајте го. Само притиснете го копчето TAB (или копчето со стрелка надесно) за да го означите копчето „Излез“ и притиснете го копчето ENTER. Потоа ќе бидете прашани дали сакате да ја зачувате новата конфигурација и вашиот одговор треба да биде Да.


Врв