Strukturen för en kärnmodul och dess kompileringsmetoder. Funktioner för att kompilera ett program med en modulär struktur. Introduktion och bakgrund

Varför kompilera kärnan själv?
Huvudfrågan som ställs om att kompilera en kärna är kanske: "Varför ska jag göra det här?"
Många anser att detta är ett meningslöst slöseri med tid för att visa sig som en smart och avancerad Linuxanvändare. Faktum är att kompilering av kärnan är en mycket viktig fråga. Låt oss säga att du har köpt en ny bärbar dator och din webbkamera inte fungerar. Dina handlingar? Du tittar in i sökmotorn och letar efter en lösning på problemet i denna fråga. Ganska ofta kan det visa sig att din webbkamera körs på en mer ny versionän din. Om du inte vet vilken version du har, skriv in uname -r i terminalen, som ett resultat får du kärnversionen (till exempel linux-2.6.31-10). Kärnkompilering används också i stor utsträckning för att öka prestandan: faktum är att kärndistributioner som standard kompilerar "för alla", vilket är anledningen till att den innehåller ett stort antal drivrutiner som du kanske inte behöver. Så om du känner till hårdvaran du använder väl kan du inaktivera onödiga drivrutiner i konfigurationsstadiet. Det är också möjligt att aktivera stöd för mer än 4 GB RAM utan att ändra systemets bitdjup. Så, om du fortfarande behöver ha din egen kärna, låt oss börja kompilera!

Skaffa kärnans källkod.
Det första du behöver göra är att skaffa källkoden för den nödvändiga kärnversionen. Vanligtvis behöver du skaffa den senaste stabila versionen. Alla officiella kärnversioner finns tillgängliga på kernel.org. Om du redan har X-server installerad ( hemdator), sedan kan du gå till webbplatsen i din favoritwebbläsare och ladda ner önskad version i tar.gz-arkivet (gzip-komprimerat). Om du arbetar i konsolen (till exempel har du inte installerat X-servern ännu eller håller på att konfigurera servern) kan du använda en textwebbläsare (till exempel elinks). Du kan också använda standard nedladdningshanteraren wget:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.33.1.tar.gz
Men kom ihåg att du måste känna till det exakta versionsnumret du behöver.

Packar upp källkodsarkivet.
När du har fått källkodsarkivet måste du extrahera arkivet till en mapp. Detta kan göras från grafiskt filhanterare(delfin, nautilus, etc.) eller via mc. Eller använd det traditionella tar-kommandot:
tar -zxvf sökväg_till_arkiv
Nu har du en mapp med källkoden, gå till den med kommandot cd kernel_source_directory(för att lista katalogerna i en mapp, använd kommandot ls).

Kärnkonfiguration.
När du har navigerat till kärnans källkatalog måste du utföra en "20 minuters" kärnkonfiguration. Dess mål är att bara lämna de nödvändiga drivrutinerna och funktionerna. Alla kommandon måste redan köras som en superanvändare.

göra config - konsolläge för konfiguratorn.

gör menuconfig - konsolläge i form av en lista.

make xconfig - grafiskt läge.

När du har gjort de nödvändiga ändringarna, spara inställningarna och avsluta konfiguratorn.

Kompilering.
Det är dags för slutskedet av monteringen - sammanställning. Detta görs med två kommandon:
gör && gör installera
Det första kommandot kommer att kompilera alla filer till maskinkod, och det andra kommer att installera den nya kärnan på ditt system.
Vi väntar från 20 minuter till flera timmar (beroende på datorns kraft). Kärnan är installerad. För att få det att visas i grub(2)-listan, skriv in (som superanvändare)
update-grub
Nu efter omstart, tryck på "Escape" och du kommer att se den nya kärnan i listan. Om kärnan inte slås på, starta helt enkelt med den gamla kärnan och konfigurera den mer noggrant.

KernelCheck - kompilerar kärnan utan att gå till konsolen.
låter dig bygga kärnan i helt grafiskt läge för Debian och distributioner baserade på det. Efter lanseringen kommer KernelCheck att erbjuda de senaste kärnversionerna och patcharna, och efter ditt samtycke, ladda ner källkoden och starta den grafiska konfiguratorn. Programmet kommer att kompilera kärnan till .deb-paket och installera dem. Allt du behöver göra är att starta om.

Om: "Baserat på översättningen" Linux Device Driver 2nd edition. Översättning: Knyazev Alexey [e-postskyddad] Senaste ändringsdatum: 08/03/2004 Plats: http://lug.kmv.ru/index.php?page=knz_ldd2

Nu börjar vi programmera! Det här kapitlet ger grunderna om moduler och kärnprogrammering.
Här kommer vi att bygga och lansera en fullfjädrad modul, vars struktur motsvarar vilken riktig moduldrivrutin som helst.
Samtidigt kommer vi att koncentrera oss på huvudpositionerna utan att ta hänsyn till detaljerna hos riktiga enheter.

Alla delar av kärnan såsom funktioner, variabler, rubrikfiler och makron som nämns här kommer att vara
beskrivs i detalj i slutet av kapitlet.

Hej världen!

I processen att bekanta mig med originalmaterialet skrivet av Alessndro Rubini & Jonathan Corbet, föreföll exemplet som gavs som Hello world för mig vara något misslyckat. Därför vill jag ge läsaren, enligt min mening, en mer framgångsrik version av den första modulen. Jag hoppas att det inte kommer att bli några problem med dess kompilering och installation under 2.4.x-kärnan. Den föreslagna modulen och sättet den är kompilerad gör att den kan användas i kärnor som stöder och inte stöder versionskontroll. Du kommer att bli bekant med alla detaljer och terminologi senare, så öppna nu vim och börja arbeta!

================================================== === //fil hello_knz.c #include #omfatta <1>Hej världen\n"); return 0; ); void cleanup_module(void) ( printk("<1>Hejdå grym värld\n"); ) MODULE_LICENSE(“GPL”); ================================= ==================

För att kompilera en sådan modul kan du använda följande Makefile. Glöm inte att sätta ett tabbtecken före raden som börjar med $(CC) ... .

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

Detta använder två funktioner jämfört med den ursprungliga Hello world-koden som föreslagits av Rubini & Corbet. För det första kommer modulen att ha samma version som kärnversionen. Detta uppnås genom att ställa in PARAM-variabeln i kompileringsskriptet. För det andra kommer modulen nu att licensieras under GPL (med makrot MODULE_LICENSE()). Om detta inte görs kan du se något i stil med följande varning när du installerar modulen i kärnan:

# insmod hello_knz.o Varning: laddning av hello_knz.o kommer att fläcka kärnan: ingen licens Se http://www.tux.org/lkml/#export-tainted för information om fläckade moduler Modul hello_knz laddad, med varningar

Låt oss nu förklara modulkompileringsalternativen (makrodefinitioner kommer att förklaras senare):

-Med- med det här alternativet kommer gcc-kompilatorn att stoppa filkompileringsprocessen omedelbart efter att objektfilen skapats, utan att försöka skapa en körbar binärfil.

-Vägg- maximal varningsnivå när gcc är igång.

-D— Definitioner av makrosymboler. Samma som #define-direktivet i den kompilerade filen. Det spelar absolut ingen roll hur man definierar makrosymbolerna som används i denna modul, med #define i källfilen eller med -D-alternativet för kompilatorn.

-Jag- Ytterligare sökvägar för inkluderade filer. Notera användningen av "uname -r"-ersättningen, som kommer att bestämma det exakta namnet på kärnversionen som för närvarande används.

Nästa avsnitt ger ytterligare ett exempel på en modul. Den förklarar också i detalj hur man installerar den och laddar ner den från kärnan.

Original Hello world!

Låt oss nu titta på originalkoden för den enkla "Hello, World"-modulen som erbjuds av Rubini & Corbet. Denna kod kan kompileras under kärnversionerna 2.0 till 2.4. Det här exemplet, och alla andra som presenteras i boken, finns tillgängliga på O'Reilly FTP-webbplatsen (se kapitel 1).

//fil hello.c #define MODUL #inkludera int init_module(void) ( printk("<1>Hej världen\n"); return 0; ) void cleanup_module(void) ( printk("<1>Farväl grymma världen\n"); )

Fungera printk() definieras i Linux-kärnan och fungerar som en standardbiblioteksfunktion printf() på C-språk. Kärnan behöver en egen, helst liten, slutledningsfunktion, som finns direkt i kärnan och inte i bibliotek på användarnivå. En modul kan anropa en funktion printk() eftersom efter att ha laddat modulen med kommandot insmod Modulen kommunicerar med kärnan och har tillgång till publicerade (exporterade) kärnfunktioner och variabler.

Strängparameter "<1>” som skickas till funktionen printk() är prioritet för meddelandet. De ursprungliga engelska källorna använder termen loglevel, vilket betyder nivån för meddelandeloggning. Här kommer vi att använda termen prioritet istället för den ursprungliga "loggnivån". I det här exemplet använder vi hög prioritet för meddelandet som har ett lågt nummer. Den höga meddelandeprioriteten ställs in avsiktligt eftersom ett meddelande med standardprioritet kanske inte visas i konsolen från vilken modulen installerades. Utmatningsriktningen för kärnmeddelanden med standardprioritet beror på versionen av den körande kärnan, versionen av demonen klogd och din konfiguration. Mer detaljerat, arbeta med funktionen printk() vi kommer att förklara i kapitel 4, Felsökningstekniker.

Du kan testa modulen med kommandot insmod för att installera modulen i kärnan och kommandon rmmod för att ta bort en modul från kärnan. Nedan visar vi hur detta kan göras. I det här fallet exekveras ingångspunkten init_module() när en modul installeras i kärnan, och cleanup_module() exekveras när den tas bort från kärnan. Kom ihåg att endast en privilegierad användare kan ladda och ladda ur moduler.

Modulexemplet ovan kan endast användas med en kärna som byggdes med flaggan "modulversion support" avstängd. Tyvärr använder de flesta distributioner versionskontrollerade kärnor (detta diskuteras i avsnittet "Versionskontroll i moduler" i kapitel 11, "kmod och avancerad modularisering"). Och även om äldre versioner av paketet moduler tillåter att sådana moduler laddas in i versionskontrollerade kärnor, vilket inte längre är möjligt. Kom ihåg att modutils-paketet innehåller en uppsättning program som inkluderar programmen insmod och rmmod.

Uppgift: Bestäm versionsnumret och sammansättningen av modulpaketet från din distribution.

När du försöker infoga en sådan modul i en kärna som stöder versionskontroll kan du se ett felmeddelande som liknar följande:

# insmod hello.o hello.o: kernel-modul version mismatch hello.o kompilerades för kernel version 2.4.20 medan denna kärna är version 2.4.20-9asp.

I katalogen diverse-moduler exempel från ftp.oreilly.com hittar du det ursprungliga exempelprogrammet hello.c, som innehåller lite fler rader, och kan installeras i både versionskontrollerade och icke-versionerade kärnor. Vi rekommenderar dock starkt att du bygger din egen kärna utan stöd för versionskontroll. Samtidigt rekommenderas det att ta de ursprungliga kärnkällorna på webbplatsen www.kernel.org

Om du är ny på att sätta samman kärnor, försök att läsa artikeln som Alessandro Rubini (en av författarna till originalboken) postade på http://www.linux.it/kerneldocs/kconf, som borde hjälpa dig att bemästra processen.

Kör följande kommandon i en textkonsol för att kompilera och testa den ursprungliga exempelmodulen ovan.

Root# gcc -c hello.c root# insmod ./hello.o Hej, världsrot# rmmod hej hej grym världsrot#

Beroende på vilken mekanism ditt system använder för att skicka meddelandesträngar, utmatningsriktningen för meddelanden som skickas av funktionen printk(), kan skilja sig åt. I exemplet ovan med att kompilera och testa en modul matades meddelanden som skickades från funktionen printk() ut till samma konsol från vilken kommandona för att installera och köra modulerna gavs. Detta exempel togs från en textkonsol. Om du kör kommandona insmod Och rmmod från under programmet xterm, då kommer du troligtvis inte att se något på din terminal. Istället kan meddelandet hamna i någon av systemloggarna, till exempel i /var/log/meddelanden. Det exakta namnet på filen beror på distributionen. Titta på tidpunkten för ändringar i loggfiler. Mekanismen som används för att skicka meddelanden från printk()-funktionen beskrivs i avsnittet "Hur meddelanden loggas" i kapitel 4 "Tekniker"
felsökning".

För att se modulmeddelanden i systemloggfilen /val/log/messages är det bekvämt att använda systemverktyg tail, som som standard visar de sista 10 raderna i filen som skickas till den. Ett intressant alternativ för detta verktyg är alternativet -f, som kör verktyget i läget för att övervaka de sista raderna i filen, dvs. När nya rader dyker upp i filen skrivs de ut automatiskt. För att stoppa exekveringen av kommandot i detta fall måste du trycka på Ctrl+C. För att se de sista tio raderna i systemloggfilen, skriv in följande på kommandoraden:

Rot# svans /var/log/meddelanden

Som du kan se är det inte så svårt att skriva en modul som det kan verka. Det svåraste är att förstå hur din enhet fungerar och hur du kan förbättra modulens prestanda. När vi fortsätter det här kapitlet kommer vi att lära oss mer om att skriva enkla moduler, och lämna enhetsspecifikationerna för senare kapitel.

Skillnader mellan kärnmoduler och applikationer

Applikationen har en ingångspunkt, som börjar köras direkt efter placeringen kör applikation i datorns RAM. Denna ingångspunkt beskrivs i C som main()-funktionen. Att avsluta funktionen main() innebär att man avslutar applikationen. Modulen har flera ingångspunkter som exekveras vid installation och borttagning av modulen från kärnan, samt vid bearbetning av förfrågningar från användaren. Således exekveras ingångspunkten init_module() när modulen laddas in i kärnan. Funktionen cleanup_module() exekveras när en modul laddas ur. I framtiden kommer vi att bekanta oss med andra ingångspunkter till modulen, som exekveras vid exekvering av olika förfrågningar till modulen.

Möjligheten att ladda och lossa moduler är två pelare i modulariseringsmekanismen. De kan bedömas på olika sätt. För utvecklaren innebär detta först och främst en minskning av utvecklingstiden, eftersom du kan testa drivrutinsfunktionalitet utan en lång omstartsprocess.

Som programmerare vet du att en applikation kan anropa en funktion som inte deklarerades i applikationen. I stadierna av statisk eller dynamisk länkning bestäms adresserna för sådana funktioner från motsvarande bibliotek. Fungera printf() en av dessa anropsbara funktioner som definieras i biblioteket libc. En modul å andra sidan är bara associerad med kärnan och kan bara anropa funktioner som exporteras av kärnan. Kod som körs i kärnan kan inte använda externa bibliotek. Så till exempel funktionen printk(), som användes i exemplet Hej C, är en analog till den välkända funktionen printf(), tillgängligt i applikationer på användarnivå. Fungera printk() placerad i kärnan och bör vara så liten som möjligt. Därför har den, till skillnad från printf(), mycket begränsat stöd för datatyper, och stöder till exempel inte flyttal alls.

2.0- och 2.2-kärnimplementeringarna stödde inte typspecifikationer L Och Z. De introducerades endast i kärnversion 2.4.

Figur 2-1 visar implementeringen av mekanismen för att anropa funktioner som är ingångspunkter till modulen. Den här bilden visar också mekanismen för interaktion mellan en installerad eller installerad modul med kärnan.

Ris. 2-1. Kommunikation mellan modulen och kärnan

En av funktionerna i Unix/Linux operativsystem är bristen på bibliotek som kan länkas till kärnmoduler. Som du redan vet, är moduler, när de laddas, länkade till kärnan, så alla funktioner utanför din modul måste deklareras i kärnhuvudfilerna och finnas i kärnan. Modulkällor aldrig bör inte inkludera vanliga rubrikfiler från användarutrymmesbibliotek. I kärnmoduler kan du bara använda funktioner som faktiskt är en del av kärnan.

Hela kärnans gränssnitt beskrivs i rubrikfiler som finns i katalogerna include/linux Och inkludera/asm inuti kärnkällorna (vanligtvis placerade i /usr/src/linux-x.y.z(x.y.z är din kärnversion)). Äldre distributioner (baserat på libc version 5 eller lägre) använde symboliska länkar /usr/include/linux Och /usr/include/asm till motsvarande kataloger i kärnkällorna. Dessa symboliska länkar gör det möjligt att vid behov använda kärngränssnitt i användarapplikationer.

Även om gränssnittet för användarutrymmesbibliotek nu är skilt från kärngränssnittet, behöver ibland användarprocesser använda kärngränssnitt. Men många referenser i kärnhuvudfiler hänvisar endast till själva kärnan och bör inte vara tillgängliga för användarapplikationer. Därför är dessa annonser skyddade #ifdef __KERNEL__ block. Det är därför din drivrutin, liksom annan kärnkod, måste kompileras med ett deklarerat makro __KÄRNA__.

Rollen för enskilda kärnhuvudfiler kommer att diskuteras på lämpligt sätt genom hela boken.

Utvecklare som arbetar med stora programvaruprojekt (som kärnan) bör vara medvetna om och undvika "namnutrymme förorening". Detta problem uppstår när det finns ett stort antal funktioner och globala variabler vars namn inte är tillräckligt uttrycksfulla (särskiljbara). Programmeraren som senare måste ta itu med sådana applikationer tvingas lägga mycket mer tid på att komma ihåg "reserverade" namn och komma på unika namn för nya element. Namnkollisioner (tvetydigheter) kan skapa ett brett spektrum av problem, allt från fel när en modul laddas till instabilt eller oförklarat programbeteende som kan uppstå för användare som använder en kärna inbyggd i en annan konfiguration.

Utvecklare har inte råd med sådana misstag när de skriver kärnkod, eftersom även den minsta modulen kommer att vara länkad till hela kärnan. Den bästa lösningen för att undvika namnkollisioner är att först deklarera dina programobjekt som statisk, och för det andra användningen av ett unikt, inom systemet, prefix för att namnge globala objekt. Dessutom kan du som modulutvecklare styra omfattningen av objekt i din kod, som beskrivs senare i avsnittet "Kernel Link Table".

De flesta (men inte alla) versioner av kommandot insmod exportera alla modulobjekt som inte är deklarerade som statisk, som standard, dvs. om inte modulen definierar särskilda instruktioner för detta ändamål. Därför är det ganska rimligt att deklarera modulobjekt som du inte tänker exportera som statisk.

Att använda ett unikt prefix för lokala objekt inom en modul kan vara bra eftersom det gör felsökningen enklare. När du testar din drivrutin kan du behöva exportera ytterligare objekt till kärnan. Genom att använda ett unikt prefix för att ange namn riskerar du inte att införa kollisioner i kärnans namnutrymme. Prefix som används i kärnan är, enligt konvention, gemener, och vi kommer att hålla oss till den konventionen.

En annan betydande skillnad mellan kärnan och användarprocesserna är felhanteringsmekanismen. Kärnan styr exekveringen av användarprocessen, så ett fel i användarprocessen resulterar i ett meddelande som är ofarligt för systemet: segmenteringsfel. Samtidigt kan en debugger alltid användas för att spåra fel i användarapplikationens källkod. Fel som uppstår i kärnan är dödliga - om inte för hela systemet, så åtminstone för den aktuella processen. I avsnittet "Felsökning av systemfel" i kapitel 4, "Felsökningstekniker", kommer vi att titta på sätt att spåra kärnfel.

Användarutrymme och kärnutrymme

Modulen körs i den sk kärna utrymme, medan applikationer körs i . Detta koncept är grunden för operativsystemteori.

Ett av huvudsyften med operativsystemet är att förse användaren och användarprogrammen med datorresurser, varav de flesta representeras av externa enheter. Operativsystemet måste inte bara ge tillgång till resurser utan också kontrollera deras tilldelning och användning, förhindra kollisioner och obehörig åtkomst. Dessutom, operativ system kan skapa oberoende operationer för program och skydda mot obehörig åtkomst till resurser. Att lösa detta icke-triviala problem är endast möjligt om processorn skyddar systemprogram från användarapplikationer.

Nästan varje modern processor kan tillhandahålla sådan separation genom att implementera olika nivåer av privilegier för den exekverande koden (minst två nivåer krävs). Till exempel har I32-arkitekturprocessorer fyra nivåer av privilegier från 0 till 3. Dessutom har nivå 0 de högsta privilegierna. För sådana processorer finns det en klass av privilegierade instruktioner som endast kan utföras på privilegierade nivåer. Unix-system använder två nivåer av processorbehörighet. Om en processor har fler än två behörighetsnivåer används den lägsta och den högsta. Unix-kärnan körs på högsta nivån privilegier, vilket säkerställer kontroll över användarens utrustning och processer.

När vi pratar om kärna utrymme Och användarens processutrymme Detta innebär inte bara olika nivåer av privilegier för den körbara koden, utan också olika adressutrymmen.

Unix överför exekvering från användarens processutrymme till kärnutrymmet i två fall. För det första, när en användarapplikation gör ett anrop till kärnan (systemanrop), och för det andra, under service av hårdvaran avbryts. Kärnkod som exekveras under ett systemanrop körs i samband med en process, dvs. arbetar på uppdrag av den anropande processen, den har tillgång till processens adressutrymmesdata. Å andra sidan är koden som exekveras vid service av ett hårdvaruavbrott asynkron med avseende på processen och tillhör inte någon speciell process.

Syftet med moduler är att utöka funktionaliteten hos kärnan. Modulkoden exekveras i kärnutrymmet. Vanligtvis utför en modul båda de uppgifter som nämnts tidigare: vissa modulfunktioner exekveras som en del av systemanrop, och några är ansvariga för att hantera avbrott.

Parallellisering i kärnan

Vid programmering av drivrutiner, i motsats till programmeringsapplikationer, är frågan om parallellisering av körbar kod särskilt akut. Vanligtvis körs ett program sekventiellt från början till slut utan att behöva oroa sig för förändringar i dess miljö. Kärnkoden måste fungera med förståelsen att den kan nås flera gånger samtidigt.

Det finns många anledningar till att parallellisera kärnkod. Linux har vanligtvis många processer igång, och vissa av dem kan försöka komma åt din modulkod samtidigt. Många enheter kan orsaka hårdvaruavbrott på processorn. Avbrottshanterare anropas asynkront och kan anropas medan din förare kör en annan begäran. Vissa mjukvaruabstraktioner (som kärntimers, förklarade i kapitel 6, "Tidsflöde") körs också asynkront. Dessutom kan Linux köras på ett system med symmetriska multiprocessorer (SMP), vilket innebär att din drivrutinskod kan köras parallellt på flera processorer samtidigt.

Av dessa skäl måste Linux-kärnkoden, inklusive drivrutinskod, vara återinträdande, dvs. måste kunna arbeta med mer än en datakontext samtidigt. Datastrukturer måste utformas för att rymma parallell exekvering av flera trådar. I sin tur måste kärnkoden kunna hantera flera parallella dataströmmar utan att skada dem. Att skriva kod som kan exekveras parallellt och undvika situationer där en annan exekveringssekvens skulle leda till oönskat systembeteende kräver mycket tid, och kanske en hel del knep. Varje drivrutinsexempel i den här boken är skrivet med parallellt utförande i åtanke. Om det behövs kommer vi att förklara detaljerna för tekniken för att skriva sådan kod.

Mest generellt fel Problemet som programmerare gör är att de antar att samtidighet inte är ett problem eftersom vissa kodsegment inte kan gå i vila. Faktum är att Linux-kärnan är icke-sökt, med det viktiga undantaget av avbrottshanterare, som inte kan förvärva CPU:n medan kritisk kärnkod körs. På senare tid har icke-pageability varit tillräcklig för att förhindra oönskad parallellisering i de flesta fall. På SMP-system krävs dock ingen kodnedladdning på grund av parallell beräkning.

Om din kod antar att den inte kommer att laddas ur, kommer den inte att fungera korrekt på SMP-system. Även om du inte har ett sådant system kan någon annan som använder din kod ha ett. Det är också möjligt att kärnan i framtiden kommer att använda sökbarhet, så även system med en processor kommer att behöva hantera samtidighet hela tiden. Det finns redan alternativ för att implementera sådana kärnor. Således kommer en försiktig programmerare att skriva kärnkod med antagandet att den kommer att köras på ett system som kör SMP.

Notera översättare: Förlåt, men de två sista styckena är inte tydliga för mig. Detta kan vara resultatet av en felöversättning. Därför presenterar jag originaltexten.

Ett vanligt misstag som görs av förarprogrammerare är att anta att samtidighet inte är ett problem så länge som ett visst segment av kod
gör inte gå och sova (eller "blockera"). Det är sant att Linux-kärnan är icke-förebyggande; med det viktiga undantaget
serviceavbrott, kommer det inte att ta bort processorn från kärnkod som inte ger efter. I tidigare tider, detta icke förebyggande
beteende var tillräckligt för att förhindra oönskad samtidighet för det mesta. På SMP-system krävs dock inte förköp för att orsaka
samtidigt utförande.

Om din kod antar att den inte kommer att förebyggas, kommer den inte att köras korrekt på SMP-system. Även om du inte har ett sådant system,
andra som kör din kod kan ha en. I framtiden är det också möjligt att kärnan kommer att gå över till ett förebyggande driftläge,
vid vilken tidpunkt även enprocessorsystem kommer att behöva hantera samtidighet överallt (vissa varianter av kärnan har redan implementerats
Det).

Information om den aktuella processen

Även om kärnmodulens kod inte exekveras sekventiellt som applikationer, exekveras de flesta anrop till kärnan i förhållande till processen som anropar den. Kärnkod kan identifiera processen som kallade den genom att komma åt en global pekare som pekar på strukturen struct task_struct, definierad för kärnor version 2.4, i filen ingår i . Pekare nuvarande indikerar den för närvarande pågående användarprocessen. Vid exekvering av systemanrop som t.ex öppen() eller stänga(), det måste finnas en process som orsakade dem. Kärnkod kan vid behov anropa specifik information om anropsprocessen via en pekare nuvarande. För exempel på hur du använder den här pekaren, se avsnittet "Device File Access Control" i kapitel 5, "Enhanced Char Driver Operations".

Idag, index nuvarandeär inte längre en global variabel, som i tidigare versioner av kärnan. Utvecklarna optimerade åtkomsten till strukturen som beskriver den aktuella processen genom att flytta den till stacksidan. Du kan titta på de aktuella implementeringsdetaljerna i filen . Koden du ser där kanske inte verkar enkel för dig. Tänk på att Linux är ett SMP-centrerat system, och en global variabel fungerar helt enkelt inte när du har att göra med flera processorer. Implementeringsdetaljer förblir dolda för andra kärndelsystem och drivrutinen kan komma åt pekaren nuvarande endast via gränssnittet .

Ur modulsynpunkt, nuvarande ser ut som en extern länk printk(). Modulen kan använda nuvarande varhelst det behövs. Till exempel skriver följande kod ut process-ID (PID) och kommandonamnet för processen som anropade modulen, och hämtar dem genom motsvarande fält i strukturen struct task_struct:

Printk("Processen är \"%s\" (pid %i)\n", current->comm, current->pid);

Fältet current->comm är namnet på kommandofilen som skapade den aktuella processen.

Kompilera och ladda moduler

Resten av detta kapitel ägnas åt att skriva en komplett, om än atypisk, modul. De där. Modulen tillhör inte någon av de klasser som beskrivs i avsnittet "Enhets- och modulklasser" i kapitel 1, "Introduktion till enhetsdrivrutiner." Exemplet på drivrutinen som visas i det här kapitlet kommer att kallas skull (Simple Kernel Utility for Loading Localities). Du kan använda scullmodulen som en mall för att skriva din egen lokala kod.

Vi använder begreppet "lokal kod" (lokal) för att betona dina personliga kodändringar, i den gamla goda Unix-traditionen (/usr/local).

Men innan vi fyller i funktionerna init_module() och cleanup_module() kommer vi att skriva ett Makefile-skript som make kommer att använda för att bygga modulens objektkod.

Innan förprocessorn kan bearbeta inkluderingen av någon rubrikfil måste makrosymbolen __KERNEL__ definieras med ett #define-direktiv. Som nämnts tidigare kan en kärnspecifik kontext definieras i kärngränssnittsfiler, endast synlig om symbolen __KERNEL__ är fördefinierad i förbearbetningen.

En annan viktig symbol som definieras av #define-direktivet är MODUL-symbolen. Måste definieras innan gränssnittet aktiveras (exklusive de drivrutiner som kommer att kompileras med kärnan). Drivrutiner som är sammansatta i kärnan kommer inte att beskrivas i den här boken, så MODUL-symbolen kommer att finnas i alla våra exempel.

Om du bygger en modul för ett system med SMP måste du också definiera makrosymbolen __SMP__ innan du aktiverar kärngränssnitten. I kärnversion 2.2 introducerade ett separat objekt i kärnkonfigurationen ett val mellan en enprocessor och ett multiprocessorsystem. Att inkludera följande rader som de allra första raderna i din modul kommer därför att resultera i stöd för flera processorer.

#omfatta #ifdef CONFIG_SMP # definiera __SMP__ #endif

Modulutvecklare bör också definiera -O-optimeringsflaggan för kompilatorn eftersom många funktioner deklareras inline i kärnhuvudfilerna. Gcc-kompilatorn utför inte inline-expansion på funktioner om inte optimering är aktiverad. Genom att tillåta att inline-ersättningar utökas med alternativen -g och -O kan du senare felsöka kod som använder inline-funktioner i debuggern. Eftersom kärnan i stor utsträckning använder inline-funktioner är det mycket viktigt att de utökas korrekt.

Observera dock att det är riskabelt att använda någon optimering över -O2-nivån eftersom kompilatorn kan utöka funktioner som inte deklareras inline. Detta kan leda till problem eftersom... Vissa funktionskoder förväntar sig att hitta standardstacken för sitt anrop. En inline-anknytning förstås som att infoga en funktionskod vid punkten för dess anrop istället för motsvarande funktionsanropsinstruktion. Följaktligen, i detta fall, eftersom det inte finns något funktionsanrop, så finns det ingen stack av dess anrop.

Du kan behöva se till att du använder samma kompilator för att kompilera moduler som användes för att bygga in kärnan som modulen kommer att installeras i. För detaljer, se originaldokumentet från filen Dokumentation/Ändringar finns i kärnans källkodskatalog. Utveckling av kärnor och kompilatorer synkroniseras vanligtvis mellan utvecklingsteam. Det kan finnas fall där uppdatering av ett av dessa element avslöjar fel i ett annat. Vissa distributionstillverkare tillhandahåller ultranya versioner av kompilatorn som inte matchar kärnan de använder. I det här fallet tillhandahåller de vanligtvis ett separat paket (ofta kallat kgcc) med en kompilator speciellt designad för
kärnkompilering.

Slutligen, för att förhindra otäcka fel, föreslår vi att du använder kompileringsalternativet -Vägg(alla varningar - alla varningar). För att uppfylla alla dessa varningar kan du behöva ändra din vanliga programmeringsstil. När du skriver kärnkod är det att föredra att använda den kodningsstil som Linus Torvalds föreslagit. Ja, dokument Dokumentation/kodningsstil, från kärnans källkatalog, är ganska intressant och rekommenderas till alla som är intresserade av programmering på kärnnivå.

Det rekommenderas att placera en uppsättning modulkompileringsflaggor, som vi nyligen blev bekanta med, i en variabel CFLAGS din Makefile. För make-verktyget är detta en speciell variabel, vars användning kommer att framgå av följande beskrivning.

Förutom flaggorna i variabeln CFLAGS, kan du behöva ett mål i din Makefile som kombinerar olika objektfiler. Ett sådant mål är bara nödvändigt när modulkoden är uppdelad i flera källfiler, vilket i allmänhet inte är ovanligt. Objektfiler kombineras med kommandot ld -r, som inte är en länkningsoperation i allmänt accepterad mening, trots användningen av en länk( ld). Resultatet av kommandoexekveringen ld -rär en annan objektfil som kombinerar objektkoderna för länkningsindatafilerna. Alternativ -r betyder " flyttbar - flyttbar”, dvs. Vi flyttar utdatafilen för kommandot i adressutrymmet, eftersom den innehåller ännu inte absoluta adresser för funktionsanrop.

Följande exempel visar den minsta Makefile som krävs för att kompilera en modul som består av två källfiler. Om din modul består av en enda källfil måste du från exemplet ta bort målet som innehåller kommandot ld -r.

# Sökvägen till din kärnkällkatalog kan ändras här, # eller så kan du skicka den som en parameter när du anropar "make" KERNELDIR = /usr/src/linux include $(KERNELDIR)/.config CFLAGS = -D__KERNEL__ -DMODULE - I$(KERNELDIR) /include \ -O -Wall ifdef CONFIG_SMP CFLAGS += -D__SMP__ -DSMP endif all: skull.o skull.o: skull_init.o skull_clean.o $(LD) -r $^ -o $@ clean : rm -f * .o *~ kärna

Om du är ny på hur make fungerar kan du bli förvånad över att det inte finns några regler för att kompilera *.c-filer till *.o-objektfiler. Att definiera sådana regler är inte nödvändigt, eftersom Make-verktyget konverterar vid behov själv *.c-filer till *.o-filer med standardkompilatorn eller kompilatorn som anges av en variabel $(CC). I detta fall innehållet i variabeln $(CFLAGS) används för att ange kompileringsflaggor.

Nästa steg efter att ha byggt en modul är att ladda den i kärnan. Vi har redan sagt att för detta kommer vi att använda verktyget insmod, som associerar alla odefinierade symboler (funktionsanrop, etc.) i modulen med symboltabellen för den körande kärnan. Men till skillnad från en länkare (till exempel, såsom ld), ändrar den inte moduldiskfilen, utan laddar modulobjektet som är länkat till kärnan till RAM. Verktyget insmod kan acceptera vissa kommandoradsalternativ. Detaljer kan ses via man insmod. Med dessa alternativ kan du till exempel tilldela specifika heltals- och strängvariabler i din modul till specificerade värden innan du länkar modulen till kärnan. Om modulen är korrekt designad kan den alltså konfigureras vid uppstart. Denna metod för att konfigurera en modul ger användaren större flexibilitet än konfiguration vid kompilering. Konfiguration av uppstartstid förklaras i avsnittet "Manuell och automatisk konfiguration" längre fram i det här kapitlet.

Vissa läsare kommer att vara intresserade av detaljerna om hur insmod-verktyget fungerar. Implementeringen av insmod är baserad på flera systemanrop definierade i kernel/module.c. Funktionen sys_create_module() allokerar den nödvändiga mängden minne i kärnans adressutrymme för att ladda modulen. Detta minne allokeras med hjälp av vmalloc()-funktionen (se avsnittet "vmalloc and Friends" i kapitel 7, "Få tag i minnet"). Systemanropet get_kernel_sysms() returnerar kärnsymboltabellen, som kommer att användas för att bestämma objektens verkliga adresser vid länkning. Funktionen sys_init_module() kopierar modulobjektkoden till kärnans adressutrymme och anropar modulens initialiseringsfunktion.

Om du tittar på kärnkodskällorna hittar du systemanropsnamn som börjar med sys_-prefixet. Detta prefix används endast för systemsamtal. Inga andra funktioner bör använda den. Tänk på detta när du bearbetar kärnkodskällor med sökverktyget grep.

Versionsberoende

Om du inte vet något mer än vad som tas upp här, så kommer troligen modulerna du skapar att behöva kompileras om för varje version av kärnan som de är länkade till. Varje modul måste definiera en symbol som kallas __module_kernel_version, vars värde
jämförs med versionen av den aktuella kärnan som använder insmod-verktyget. Denna symbol finns i avsnittet .modinfo ELF-filer (Executable and Linking Format). Detta förklaras mer i detalj i kapitel 11 "kmod och avancerad modularisering". Observera att denna versionskontrollmetod endast är tillämplig för kärnversionerna 2.2 och 2.4. I 2.0-kärnan görs detta på ett lite annorlunda sätt.

Kompilatorn kommer att definiera denna symbol varhelst rubrikfilen ingår . Därför beskrev vi inte denna symbol i hello.c-exemplet som gavs tidigare. Detta betyder också att om din modul består av många källfiler måste du inkludera filen i din kod bara en gång. Ett undantag är fallet när man använder definitionen __NO_VERSION__, som vi kommer att träffas senare.

Nedan är definitionen av den beskrivna symbolen från filen module.h extraherad från 2.4.25-kärnkoden.

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

Om en modul inte kan laddas på grund av en versionsfel, kan du försöka ladda denna modul genom att skicka insmod-nyckeln till parameterraden för verktyget -f(tvinga). Denna metod att ladda en modul är inte säker och är inte alltid framgångsrik. Det är ganska svårt att förklara orsakerna till eventuella misslyckanden. Det är möjligt att modulen inte laddas eftersom symbolerna inte går att lösa under länkningen. I det här fallet kommer du att få ett lämpligt felmeddelande. Orsakerna till misslyckanden kan också ligga i förändringar i kärnans funktion eller struktur. I det här fallet kan laddning av modulen leda till allvarliga körtidsfel, såväl som systempanik. Det senare bör fungera som ett bra incitament att använda ett versionskontrollsystem. Versionsfel kan hanteras mer elegant genom att använda versionskontroll i kärnan. Vi kommer att prata om detta i detalj i avsnittet "Versionskontroll i moduler" i kapitel 11 "kmod och avancerad modularisering".

Om du vill kompilera din modul för en specifik kärnversion måste du inkludera rubrikfilerna för just den kärnversionen. I Makefile-exemplet som beskrivs ovan användes variabeln för att bestämma katalogen för dessa filer KERNELDIR. Sådan anpassad kompilering är inte ovanlig när kärnkällor är tillgängliga. Dessutom är det inte ovanligt att det finns olika versioner av kärnan i katalogträdet. Alla modulexempel i den här boken använder variabeln KERNELDIR för att ange platsen för källkatalogen för versionen av kärnan till vilken den sammansatta modulen ska länkas. Du kan använda en systemvariabel för att specificera den här katalogen, eller så kan du skicka dess plats genom kommandoradsalternativ för att skapa.

När du laddar en modul använder insmod-verktyget sina egna sökvägar för modulens objektfiler och tittar igenom versionsberoende kataloger med start kl. /lib/moduler. Och även om äldre versioner av verktyget inkluderade den aktuella katalogen i sökvägen, anses detta beteende nu vara oacceptabelt av säkerhetsskäl (samma problem som att använda systemvariabeln VÄG). Så om du vill ladda en modul från den aktuella katalogen kan du ange den i stilen ./modul.o. Denna indikation på modulpositionen fungerar för alla versioner av insmod-verktyget.

Ibland kan du stöta på kärngränssnitt som skiljer sig mellan 2.0.x och 2.4.x. I det här fallet måste du tillgripa ett makro som bestämmer den aktuella kärnversionen. Detta makro finns i rubrikfilen . Vi kommer att indikera fall av skillnader i gränssnitt när vi använder dem. Detta kan göras antingen omedelbart längs beskrivningen, eller i slutet av avsnittet, i ett speciellt avsnitt dedikerat till versionsberoende. I vissa fall kan du genom att placera detaljerna i ett separat avsnitt undvika att komplicera beskrivningen av kärnversionen 2.4.x som är relevant för den här boken.

I rubrikfilen linux/version.h Följande makron definieras för att fastställa kärnversionen.

UTS_RELEASE Makro som expanderar till en sträng som beskriver den aktuella kärnversionen
källträd. Till exempel kan ett makro expandera till något så här:
linje: "2.3.48" . LINUX_VERSION_CODE Detta makro expanderar till en binär representation av kärnversionen, av
en byte för varje del av numret. Till exempel binär
representationen för version 2.3.48 kommer att vara 131888 (decimal
representation för hex 0x020330). Möjligen binär
Du kommer att tycka att representationen är bekvämare än strängrepresentationen. Lägg märke till vad som är
representation låter dig beskriva högst 256 alternativ i varje
delar av numret. KERNEL_VERSION(dur, moll, release) Denna makrodefinition låter dig bygga "kernel_version_code"
från de individuella elementen som utgör kärnversionen. Till exempel,
nästa makro KERNEL_VERSION(2, 3, 48)
kommer att expandera till 131888. Denna makrodefinition är mycket bekväm när
jämför den aktuella kärnversionen med den nödvändiga. Vi kommer att göra det upprepade gånger
använd denna makrodefinition genom hela boken.

Här är innehållet i filen: linux/version.h för kärnan 2.4.25 (texten i rubrikfilen ges i sin helhet).

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

Versions.h-huvudfilen ingår i modulen.h-filen, så du behöver vanligtvis inte inkludera version.h uttryckligen i din modulkod. Å andra sidan kan du förhindra att version.h-huvudfilen inkluderas i module.h genom att deklarera ett makro __NO_VERSION__. Du kommer att använda __NO_VERSION__, till exempel i det fall du behöver aktivera till flera källfiler, som sedan kommer att länkas till en modul. Meddelande __NO_VERSION__ innan inkludering av module.h header-filen förhindrar
automatisk strängbeskrivning __module_kernel_version eller motsvarande i källfiler. Du kan behöva detta för att tillfredsställa länkarens klagomål när ld -r, som inte kommer att gilla flera beskrivningar av symboler i länktabeller. Vanligtvis om modulkoden är uppdelad i flera källfiler, inklusive en rubrikfil , sedan tillkännagivandet __NO_VERSION__ görs i alla dessa filer utom en. I slutet av boken finns ett exempel på en modul som använder __NO_VERSION__.

De flesta kärnversionsberoenden kan hanteras med hjälp av logik byggd på förprocessordirektiv med hjälp av makrodefinitioner KERNEL_VERSION Och LINUX_VERSION_CODE. Att kontrollera versionsberoende kan dock avsevärt komplicera läsbarheten av modulkoden på grund av heterogena direktiv #ifdef. Därför är kanske den bästa lösningen att placera beroendekontrollen i en separat rubrikfil. Det är därför vårt exempel innehåller en rubrikfil sysdep.h, används för att hysa alla makrodefinitioner som är associerade med versionsberoendekontroller.

Det första versionsberoendet vi vill representera finns i måldeklarationen" göra installera" vårt kompileringsskript för drivrutiner. Som du kanske förväntar dig, väljs installationskatalogen, som ändras beroende på vilken kärnversion som används, baserat på visning av version.h-filen. Här är ett kodavsnitt från filen Regler.gör, som används av alla kärnans Makefiler.

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

Observera att vi använder katalogen misc för att installera alla våra drivrutiner (INSTALLDIR-deklarationen i exemplet Makefile ovan). Från och med kärnversion 2.4 är denna katalog den rekommenderade katalogen för att placera anpassade drivrutiner. Dessutom innehåller både gamla och nya versioner av modutils-paketet en misc-katalog i sina sökvägar.

Med hjälp av INSTALLDIR-definitionen ovan kan installationsmålet i Makefilen se ut så här:

Installera: installera -d $(INSTALLDIR) installera -c $(OBJS) $(INSTALLDIR)

Plattformsberoende

Varje datorplattform har sina egna egenskaper som måste beaktas av kärnutvecklare för att uppnå högsta prestanda.

Kärnutvecklare har mycket större frihet i val och beslutsfattande än applikationsutvecklare. Det är denna frihet som gör att du kan optimera din kod och få ut det mesta av varje specifik plattform.

Modulkoden måste kompileras med samma kompileringsalternativ som användes för att kompilera kärnan. Detta gäller både att använda samma processorregisteranvändningsmönster och att utföra samma optimeringsnivå. Fil Regler.gör, som finns i roten av kärnkällträdet, innehåller plattformsspecifika definitioner som måste inkluderas i alla kompilerings-Makefiler. Alla plattformsspecifika kompileringsskript kallas Makefiles. plattform och innehåller värdena för variabler för make-verktyget enligt den aktuella kärnkonfigurationen.

En annan intressant funktion hos Makefile är dess stöd för plattformsoberoende eller helt enkelt korskompilering. Denna term används när du behöver kompilera kod för en annan plattform. Till exempel, med hjälp av i86-plattformen kommer du att skapa kod för M68000-plattformen. Om du ska korskompilera måste du byta ut dina kompileringsverktyg ( gcc, ld, etc.) med en annan uppsättning motsvarande verktyg
(Till exempel, m68k-linux-gcc, m68k-linux-ld). Prefixet som används kan specificeras antingen av $(CROSS_COMPILE) Makefile-variabeln, med ett kommandoradsalternativ till make-verktyget eller av en systemmiljövariabel.

SPARC-arkitekturen är ett specialfall som måste hanteras därefter i Makefilen. Användarprogram som körs på SPARC64 (SPARC V9)-plattformen är binära filer, vanligtvis utformade för SPARC32 (SPARC V8)-plattformen. Därför genererar standardkompilatorn på SPARC64-plattformen (gcc) objektkod för SPARC32. Å andra sidan måste en kärna designad för att köras på SPARC V9 innehålla objektkod för SPARC V9, så även då krävs en korskompilator. Alla GNU/Linux-distributioner designade för SPARC64 inkluderar en lämplig korskompilator, som måste väljas i Makefile för kärnkompileringsskriptet.

Och även om den fullständiga listan över versions- och plattformsberoenden är lite mer komplex än vad som beskrivs här, är det tillräckligt för att utföra korskompilering. För mer information kan du titta på Makefile-kompileringsskripten och kärnkällfilerna.

Funktioner i kärnan 2.6

Tiden står inte stilla. Och nu bevittnar vi uppkomsten av en ny generation av kernel 2.6. Tyvärr täcker inte originalet av denna bok den nya kärnan, så översättaren tar sig friheten att komplettera översättningen med ny kunskap.

Du kan använda integrerade utvecklingsmiljöer som TimeSys TimeStorm, som korrekt genererar skelettet och kompileringsskriptet för din modul beroende på vilken kärnversion som krävs. Om du ska skriva allt detta själv, kommer du att behöva lite ytterligare information om de viktigaste skillnaderna som introduceras av den nya kärnan.

En av funktionerna i 2.6-kärnan är behovet av att använda makron module_init() och module_exit() för att explicit registrera namnen på initierings- och avslutningsfunktionerna.

Makrot MODULE_LISENCE(), introducerat i 2.4-kärnan, behövs fortfarande om du inte vill se motsvarande varningar när du laddar en modul. Du kan välja att följande licenssträngar ska överföras till makrot: "GPL", "GPL v2", "GPL och ytterligare rättigheter", "Dual BSD/GPL" (val mellan BSD- eller GPL-licenser), "Dual MPL/GPL " (val mellan Mozilla- eller GPL-licenser) och
"Proprietär".

Mer betydelsefullt för den nya kärnan är ett nytt modulkompileringsschema, som inte bara innebär ändringar i själva modulens kod utan även i Makefile-skriptet för dess kompilering.

Således krävs inte längre definitionen av MODUL-makrosymbolen, varken i modulkoden eller i Makefilen. Om det behövs kommer det nya kompileringsschemat självt att bestämma denna makrosymbol. Du behöver inte heller explicit definiera makrosymbolerna __KERNEL__, eller nyare som KBUILD_BASENAME och KBUILD_MODNAME.

Du bör inte heller ange optimeringsnivån vid kompilering (-O2 eller andra), eftersom din modul kommer att kompileras med hela uppsättningen av flaggor, inklusive optimeringsflaggor, med vilka alla andra moduler i din kärna är kompilerade - verktyget make använder automatiskt hela den nödvändiga uppsättningen av flaggor.

Av dessa skäl är Makefilen för att kompilera en modul för 2.6-kärnan mycket enklare. Så för hello.c-modulen kommer Makefilen att se ut så här:

Obj-m:= hej.o

Men för att kompilera modulen behöver du skrivåtkomst till kärnans källkodsträd, där temporära filer och kataloger skapas. Därför bör kommandot för att kompilera en modul för 2.6-kärnan, specificerat från den aktuella katalogen som innehåller modulens källkod, se ut så här:

# make -C /usr/src/linux-2.6.1 SUBDIRS=`pwd`-moduler

Så vi har källan till modulen hej-2.6.c, för kompilering i kärnan 2.6:

//hello-2.6.c #inkludera #omfatta #omfatta MODULE_LICENSE("GPL"); static int __init my_init(void) ( printk("Hej värld\n"); return 0; ); static void __exit my_cleanup(void) ( printk("Hejdå\n"); ); module_init(min_init); module_exit(min_rensning);

Följaktligen har vi en Makefile:

Obj-m:= hej-2.6.o

Vi anropar verktyget make för att bearbeta vår Makefile med följande parametrar:

# make -C/usr/src/linux-2.6.3 SUBDIRS=`pwd`-moduler

Den normala kompileringsprocessen kommer att producera följande standardutdata:

Make: Ange katalogen `/usr/src/linux-2.6.3" *** Varning: Åsidosättande av SUBDIRS på kommandoraden kan orsaka *** inkonsekvenser gör: `arch/i386/kernel/asm-offsets.s" inte kräver uppdatering. CHK include/asm-i386/asm_offsets.h CC [M] /home/knz/j.kernel/3/hello-2.6.o Bygga moduler, steg 2. /usr/src/linux-2.6.3/scripts/Makefile .modpost:17: *** Uh-oh, du har inaktuella modulposter. Du bråkade med SUBDIRS, /usr/src/linux-2.6.3/scripts/Makefile.modpost:18: klaga inte om något går fel. MODPOST CC /home/knz/j.kernel/3/hello-2.6.mod.o LD [M] /home/knz/j.kernel/3/hello-2.6.ko make: Avsluta katalogen `/usr/src / linux-2.6.3"

Det slutliga resultatet av kompileringen blir en modulfil hello-2.6.ko som kan installeras i kärnan.

Observera att i 2.6-kärnan har modulfiler suffix med .ko snarare än .o som i 2.4-kärnan.

Kärna symbol bord

Vi har redan pratat om hur insmod-verktyget använder kärnans publika symboltabell när man länkar en modul till kärnan. Den här tabellen innehåller adresserna till globala kärnobjekt - funktioner och variabler - som krävs för att implementera modulära drivrutinsalternativ. Kärnans publika symboltabell kan läsas i textform från filen /proc/ksyms, förutsatt att din kärna stöder filsystemet /proc.

I kärnan 2.6 döptes /proc/ksyms om till /proc/modules.

När en modul laddas blir symbolerna som exporteras av modulen en del av kärnsymboltabellen, och du kan se dem i /proc/ksyms.

Nya moduler kan använda symboler som exporteras av din modul. Till exempel förlitar sig msdos-modulen på tecken som exporteras av fettmodulen, och varje USB-enhet som används i läsläge använder tecken från usbcore- och inmatningsmodulerna. Detta förhållande, realiserat genom sekventiell laddning av moduler, kallas en modulstack.

Modulstacken är bekväm att använda när du skapar komplexa modulprojekt. Denna abstraktion är användbar för att separera enhetsdrivrutinskod i hårdvaruberoende och hårdvaruoberoende delar. Till exempel består video-for-linux-drivrutinuppsättningen av en kärnmodul som exporterar symboler för en lågnivådrivrutin som tar hänsyn till detaljerna hos den hårdvara som används. Enligt din konfiguration laddar du huvudvideomodulen och en modul som är specifik för din hårdvara. På samma sätt implementeras stöd för parallellportar och en bred klass av anslutna enheter, såsom USB-enheter. Parallellportsystemstacken visas i fig. 2-2. Pilarna visar interaktionen mellan moduler och kärnans programmeringsgränssnitt. Interaktion kan utföras både på funktionsnivå och på nivån för datastrukturer som hanteras av funktioner.

Bild 2-2. Parallellportmodulstack

När du använder stackmoduler är det bekvämt att använda modprobe-verktyget. Funktionaliteten hos modprobe-verktyget liknar på många sätt insmod-verktyget, men när man laddar en modul kontrollerar det dess underliggande beroenden och, om nödvändigt, laddar de nödvändiga modulerna tills den önskade modulstacken är fylld. Således kan ett modprobe-kommando resultera i flera anrop till insmod-kommandot. Man kan säga att modprobe-kommandot är ett intelligent omslag runt insmod. Du kan använda modprobe istället för insmod överallt, förutom när du laddar dina egna moduler från den aktuella katalogen, eftersom modprobe tittar bara på specifika modulkataloger och kommer inte att kunna tillfredsställa eventuella beroenden.

Att dela upp moduler i delar hjälper till att minska utvecklingstiden genom att förenkla problemdefinitionen. Detta liknar åtskillnaden mellan implementeringsmekanism och kontrollpolicy, som diskuteras i kapitel 1, "Introduktion till enhetsdrivrutiner."

Vanligtvis implementerar en modul sin funktionalitet utan att behöva exportera symboler alls. Du måste exportera symboler om andra moduler kan dra nytta av det. Du kan behöva inkludera ett speciellt direktiv för att förhindra export av icke-statiska tecken, eftersom De flesta implementeringar av moduler exporterar alla som standard.

Linux-kärnhuvudfilerna erbjuder ett bekvämt sätt att kontrollera synligheten för dina symboler, och förhindrar därigenom att namnutrymmet för kärnsymboltabellen förorenas. Mekanismen som beskrivs i det här kapitlet fungerar i kärnor från och med version 2.1.18. Kernel 2.0 hade en helt annan kontrollmekanism
symbolsynlighet, som kommer att beskrivas i slutet av kapitlet.

Om din modul inte behöver exportera symboler alls, kan du uttryckligen placera följande makroanrop i modulens källfil:

EXPORT_NO_SYMBOLS;

Detta makroanrop, definierat i linux/module.h-filen, expanderar till ett assembler-direktiv och kan specificeras var som helst i modulen. Men när man skapar kod som är portabel till olika kärnor är det nödvändigt att placera detta makroanrop i modulens initieringsfunktion (init_module), eftersom versionen av detta makro som vi definierade i vår sysdep.h-fil för äldre kärnversioner bara kommer att fungera här.

Å andra sidan, om du behöver exportera några av symbolerna från din modul, måste du använda en makrosymbol
EXPORT_SYMTAB. Denna makrosymbol måste definieras innan genom att inkludera rubrikfilen module.h. Det är vanlig praxis
definiera detta makrotecken via en flagga -D i Makefile.

Om makrosymbolen EXPORT_SYMTAB definieras, sedan kan individuella symboler exporteras med hjälp av ett par makron:

EXPORT_SYMBOL(namn); EXPORT_SYMBOL_NOVERS(namn);

Båda dessa två makron kommer att göra den givna symbolen tillgänglig utanför modulen. Skillnaden är att makrot EXPORT_SYMBOL_NOVERS exporterar symbolen utan versionsinformation (se kapitel 11 "kmod och avancerad modularisering"). För mer detaljer
kolla in rubrikfilen , även om det angivna är fullt tillräckligt för praktiskt bruk
makron.

Initiera och slutföra moduler

Som nämnts registrerar funktionen init_module() de funktionella komponenterna i en modul med kärnan. Efter sådan registrering kommer applikationen som använder modulen att ha tillgång till modulens ingångspunkter via gränssnittet som tillhandahålls av kärnan.

Moduler kan registrera många olika komponenter, som när de är registrerade är namnen på modulfunktionerna. En pekare till en datastruktur som innehåller pekare till funktioner som implementerar den föreslagna funktionen skickas till kärnregistreringsfunktionen.

I kapitel 1, "Introduktion till enhetsdrivrutiner", nämndes klassificeringen av huvudtyperna av enheter. Du kan registrera inte bara de enhetstyper som nämns där, utan även alla andra, till och med mjukvaruabstraktioner, som till exempel /proc-filer, etc. Allt som kan fungera i kärnan genom drivrutinens programmeringsgränssnitt kan registreras som en drivrutin .

Om du vill lära dig mer om vilka typer av drivrutiner som registreras med din kärna som exempel, kan du implementera en sökning efter EXPORT_SYMBOL-delsträngen i kärnkällorna och hitta ingångspunkterna som erbjuds av de olika drivrutinerna. Som regel använder registreringsfunktioner ett prefix i sitt namn Registrera_,
så ett annat sätt att hitta dem är att söka efter en delsträng Registrera_ i filen /proc/ksyms med hjälp av verktyget grep. Som redan nämnts, i 2.6.x-kärnan ersattes /proc/ksyms-filen med /proc/modules.

Felhantering i init_module

Om någon typ av fel uppstår när en modul initieras måste du ångra initieringen som redan har slutförts innan du stoppar modulen från att laddas. Felet kan till exempel uppstå på grund av otillräckligt minne i systemet vid allokering av datastrukturer. Tyvärr kan detta hända, och bra kod borde kunna hantera sådana situationer.

Allt som registrerades eller allokerades innan felet inträffade i initieringsfunktionen init_module() måste avbrytas eller frigöras, eftersom Linux-kärnan inte spårar initialiseringsfel och inte ångrar lån och beviljande av resurser med modulkod. Om du inte rullade tillbaka, eller inte kunde rulla tillbaka den slutförda registreringen, kommer kärnan att förbli i ett instabilt tillstånd, och när modulen laddas igen
du kommer inte att kunna upprepa registreringen av redan registrerade element, och du kommer inte att kunna avbryta en tidigare gjord registrering, eftersom i den nya instansen av funktionen init_module() kommer du inte att ha rätt värde på adresserna till de registrerade funktionerna. Att återställa systemet till dess tidigare tillstånd kommer att kräva användning av olika komplexa knep, och detta görs ofta genom att helt enkelt starta om systemet.

Implementeringen av att återställa systemets tidigare tillstånd när modulinitieringsfel uppstår implementeras bäst med hjälp av goto-operatorn. Vanligtvis behandlas denna operatör extremt negativt, och till och med med hat, men det är i den här situationen som han visar sig vara mycket användbar. Därför, i kärnan, används goto-satsen ofta för att hantera modulinitieringsfel.

Följande enkla kod, med dummy-registrerings- och avregistreringsfunktioner som exempel, visar detta sätt att hantera fel.

Int init_module(void) ( int err; /* registrering tar en pekare och ett namn */ err = register_this(ptr1, "skull"); if (err) goto fail_this; err = register_that(ptr2, "skull"); if (err) goto fail_that; err = register_those(ptr3, "skull"); if (err) goto fail_those; return 0; /* success */ fail_those: unregister_that(ptr2, "skull"); fail_that: unregister_this(ptr1, " skalle"); fail_this: returnera fel; /* sprida felet */ )

Detta exempel försöker registrera tre modulkomponenter. Goto-satsen används när ett registreringsfel uppstår och gör att registrerade komponenter avregistreras innan modulens laddning stoppas.

Ett annat exempel på att använda en goto-sats för att göra koden lättare att läsa är tricket att "komma ihåg" framgångsrika modulregistreringar och anropa cleanup_module() för att skicka denna information när ett fel uppstår. Cleanup_module()-funktionen är utformad för att återställa slutförda initieringsoperationer och anropas automatiskt när modulen avlastas. Värdet som funktionen init_module() returnerar måste vara
representerar modulinitieringsfelkoden. I Linux-kärnan är felkoden ett negativt tal från en uppsättning definitioner som gjorts i rubrikfilen . Inkludera den här rubrikfilen i din modul för att använda symboliska mnemonics för reserverade felkoder som -ENODEV, -ENOMEM, etc. Att använda sådan mnemonics anses vara bra programmeringsstil. Det bör dock noteras att vissa versioner av verktygen från modutils-paketet inte behandlar returnerade felkoder korrekt och visar meddelandet "Enhet upptagen"
som svar på en hel grupp fel av en helt annan karaktär som returneras av funktionen init_modules(). I de senaste versionerna av paketet detta
Det irriterande felet har åtgärdats.

Funktionskoden cleanup_module() för ovanstående fall kan till exempel vara så här:

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

Om din initierings- och avslutningskod är mer komplex än vad som beskrivs här, kan användning av en goto-sats resultera i svårläst programtext eftersom avslutningskoden måste upprepas i funktionen init_module() med flera etiketter för goto-övergångar. Av denna anledning är ett smartare knep att använda ett anrop till cleanup_module()-funktionen i funktionen init_module() och skicka information om omfattningen av framgångsrik initiering när ett modulladdningsfel inträffar.

Nedan är ett exempel på hur man skriver funktionerna init_module() och cleanup_module(). Det här exemplet använder globalt definierade pekare som bär information om omfattningen av framgångsrik initiering.

Strukturera något *item1; strukturera något annat *objekt2; 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 (argument); item2 = allocate_thing2(arguments2); if (!item2 || !item2) goto fail; err = register_stuff(item1, item2); if (!err) stuff_ok = 1; else goto fail; return 0; /* framgång */ fail: cleanup_module(); return err; )

Beroende på komplexiteten i din moduls initieringsoperationer, kanske du vill använda en av metoderna som listas här för att kontrollera modulinitieringsfel.

Modulanvändningsräknare

Systemet innehåller en användningsräknare för varje modul för att avgöra om modulen kan laddas ur säkert. Systemet behöver denna information eftersom en modul inte kan laddas ur om den är upptagen av någon eller något - du kan inte ta bort en filsystemdrivrutin om det filsystemet är monterat, eller så kan du inte ladda ur en teckenenhetsmodul om någon process använder den här enheten. Annat,
detta kan leda till en systemkrasch - segmenteringsfel eller kärnpanik.

I moderna kärnor kan systemet förse dig med en automatisk modulanvändningsräknare med hjälp av en mekanism som vi kommer att titta på i nästa kapitel. Oavsett kärnversion kan du använda manuell kontroll av denna räknare. Således bör kod som är tänkt att användas i äldre versioner av kärnan använda en modulanvändningsredovisningsmodell byggd på följande tre makron:

MOD_INC_USE_COUNTÖkar den aktuella modulens användningsräknare MOD_DEC_USE_COUNT Minskar den aktuella modulens användningsräknare MOD_IN_USE Returnerar sant om användningsräknaren för denna modul är noll

Dessa makron definieras i , och de manipulerar en speciell intern datastruktur till vilken direkt tillgång inte är önskvärd. Faktum är att den interna strukturen och sättet att hantera dessa data kan ändras från version till version, medan det externa gränssnittet för att använda dessa makron förblir oförändrat.

Observera att du inte behöver kontrollera MOD_IN_USE i funktionskoden cleanup_module(), eftersom denna kontroll utförs automatiskt innan cleanup_module() anropas i systemanropet sys_delete_module(), som definieras i kernel/module.c.

Korrekt hantering av modulanvändningsräknaren är avgörande för systemets stabilitet. Kom ihåg att kärnan kan bestämma sig för att automatiskt ta bort en oanvänd modul när som helst. Ett vanligt misstag vid modulprogrammering är felaktig styrning av denna räknare. Till exempel, som svar på en viss förfrågan, utför modulkoden några åtgärder och, när bearbetningen är slutförd, ökar modulanvändningsräknaren. De där. en sådan programmerare antar att denna räknare är avsedd att samla in modulanvändningsstatistik, medan den i själva verket är en räknare för den aktuella beläggningen av modulen, dvs. håller reda på antalet processer med hjälp av modulkoden för tillfället. När du behandlar en förfrågan till en modul måste du alltså ringa MOD_INC_USE_COUNT innan du utför några åtgärder, och MOD_DEC_USE_COUNT efter att de är klara.

Det kan finnas situationer där du av uppenbara skäl inte kommer att kunna ladda ur en modul om du tappar kontrollen över dess användningsräknare. Denna situation uppstår ofta vid modulutvecklingsstadiet. Till exempel kan en process avbryta när du försöker avreferens en NULL-pekare, och du kommer inte att kunna ladda ur en sådan modul förrän du återställer dess användningsräknare till noll. En av de möjliga lösningarna på detta problem vid modulfelsökningsstadiet är att helt överge kontrollen av modulanvändningsräknaren genom att omdefiniera MOD_INC_USE_COUNT Och MOD_DEC_USE_COUNT till tom kod. En annan lösning är att skapa ett ioctl()-anrop som tvingar modulens användningsräknare till noll. Vi kommer att täcka detta i avsnittet "Använda ioctl-argumentet" i kapitel 5, "Förbättrade Char Driver Operations". Naturligtvis, i en färdig att använda drivrutin, bör sådana bedrägliga manipulationer med räknaren uteslutas, men i felsökningsstadiet sparar de utvecklaren tid och är ganska acceptabla.

Du hittar den aktuella systemanvändningsräknaren för varje modul i det tredje fältet för varje post i filen /proc/modules. Den här filen innehåller information om de för närvarande laddade modulerna - en rad per modul. Det första fältet på raden innehåller namnet på modulen, det andra fältet är storleken som upptas av modulen i minnet, och det tredje fältet är det aktuella värdet på användningsräknaren. Denna information, i formaterad form,
kan erhållas genom att anropa verktyget lsmod. Nedan är ett exempel på en /proc/modules-fil:

Parport_pc 7604 1 (autoclean) lp 4800 0 (oanvänd) parport 8084 1 lockd 33256 1 (autoclean) sunrpc 56612 1 (autoclean) ds 6252 1 i82365 22304 1 pcm

Här ser vi flera moduler inlästa i systemet. I flaggfältet (det sista fältet på raden) visas stapeln av modulberoenden inom hakparenteser. Bland annat kan du märka att parallellportsmodulerna kommunicerar genom en modulstack, som visas i Fig. 2-2. (Autoclean) flaggan markerar moduler som kontrolleras av kmod eller kerneld. Detta kommer att behandlas i kapitel 11 "kmod och avancerad modularisering"). Den (oanvända) flaggan betyder att modulen inte används för närvarande. I kärnan 2.0 visade storleksfältet information inte i byte, utan i sidor, som för de flesta plattformar är 4kB i storlek.

Avlastning av en modul

För att ladda ur en modul, använd verktyget rmmod. Att ladda ur en modul är en enklare uppgift än att ladda den, vilket innebär att dynamiskt länka den till kärnan. När en modul avlastas exekveras systemanropet delete_module() som antingen anropar cleanup_module()-funktionen för den avlastade modulen om dess användningsantal är noll, eller avslutas med ett fel.

Som redan nämnts rullar funktionen cleanup_module() tillbaka initieringsoperationerna som utförs när modulen laddas med funktionen cleanup_module(). Dessutom raderas exporterade modulsymboler automatiskt.

Explicit definiera avslutnings- och initieringsfunktioner

Som redan nämnts, när en modul laddas anropar kärnan funktionen init_module() och när den laddas ur anropar den cleanup_module(). Men i moderna versioner av kärnan har dessa funktioner ofta olika namn. Från och med kärnan 2.3.23 blev det möjligt att uttryckligen definiera ett namn för funktionen att ladda och ta bort en modul. Nuförtiden är denna explicita namngivning av dessa funktioner den rekommenderade programmeringsstilen.

Låt oss ge ett exempel. Om du vill deklarera funktionen my_init() som initieringsfunktionen för din modul, och funktionen my_cleanup() som den slutliga funktionen, istället för init_module() respektive cleanup_module(), måste du lägga till följande två makron till modultexten (vanligtvis infogas de i slutet
modulkod källfil):

Module_init(min_init); module_exit(min_rensning);

Observera att för att använda dessa makron måste du inkludera en rubrikfil i din modul .

Bekvämligheten med att använda denna stil är att varje modulinitierings- och avslutningsfunktion i kärnan kan ha sitt eget unika namn, vilket i hög grad hjälper till vid felsökning. Dessutom förenklar användningen av dessa funktioner felsökningen, oavsett om du implementerar din drivrutinskod som en modul eller ska bädda in den direkt i kärnan. Naturligtvis är det inte nödvändigt att använda makron module_init och module_exit om dina initialiserings- och avslutningsfunktioner har reserverade namn, dvs. init_module() och cleanup_module() respektive.

Om du tittar på 2.2 eller senare kärnkällor kan du se en något annorlunda form av beskrivning av initierings- och avslutningsfunktionerna. Till exempel:

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

Användning av attribut __i det kommer att göra att initieringsfunktionen laddas ur minnet efter att initieringen är klar. Detta fungerar dock bara för kärnans inbyggda drivrutiner och kommer att ignoreras för moduler. Dessutom, för drivrutiner inbyggda i kärnan, attributet __utgång kommer att göra att hela funktionen markerad med detta attribut ignoreras. För moduler kommer även denna flagga att ignoreras.

Använda attribut __i det(Och __initdata för att beskriva data) kan minska mängden minne som används av kärnan. Flagga __i det Modulens initieringsfunktion kommer varken att medföra fördelar eller skada. Styrning av denna typ av initiering har ännu inte implementerats för moduler, även om det kan vara möjligt i framtiden.

Sammanfattande

Så, som ett resultat av det presenterade materialet, kan vi presentera följande version av modulen "Hello world":

Modulens källfilkod =============================================== = #inkludera #omfatta #omfatta static int __init my_init_module (void) ( EXPORT_NO_SYMBOLS; printk("<1>Hej världen\n"); return 0; ); static void __exit my_cleanup_module (void) ( printk("<1>Hej då\n"); ); module_init(my_init_module); module_exit(my_cleanup_module); MODULE_LICENSE("GPL"); ======================== ====================== Makefile för att kompilera modulen ======================== =============== ==================== CFLAGS = -Vägg -D__KERNEL__ -DMODULE -I/lib/modules/ $(shell uname -r)/build/include hello.o: ==================================== ===========================

Observera att när vi skrev Makefilen använde vi konventionen att GNU make-verktyget oberoende kan bestämma hur man genererar en objektfil baserat på CFLAGS-variabeln och kompilatorn som är tillgänglig på systemet.

Resursanvändning

En modul kan inte slutföra sin uppgift utan att använda systemresurser som minne, I/O-portar, I/O-minne, avbrottslinjer och DMA-kanaler.

Som programmerare bör du redan vara bekant med dynamisk minneshantering. Dynamisk minneshantering i kärnan är inte fundamentalt annorlunda. Ditt program kan få minne med funktionen kmalloc() och befria henne med hjälp kfree(). Dessa funktioner påminner mycket om funktionerna malloc() och free() som du är bekant med, förutom att funktionen kmalloc() skickas ett extra argument - prioritet. Prioriteten är vanligtvis GFP_KERNEL eller GFP_USER. GFP är en akronym för "få gratis sida." Hantera dynamiskt minne i kärnan beskrivs i detalj i kapitel 7, "Få tag i minnet."

En nybörjare som utvecklar drivrutiner kan bli förvånad över behovet av att explicit allokera I/O-portar, I/O-minne och avbrottslinjer. Först då kan kärnmodulen enkelt komma åt dessa resurser. Även om systemminnet kan allokeras var som helst, spelar I/O-minne, portar och avbrottslinjer en speciell roll och tilldelas olika. Till exempel måste drivrutinen allokera vissa portar, inte
allt, utom de som han behöver för att styra enheten. Men föraren kan inte använda dessa resurser förrän den är säker på att de inte används av någon annan.

Det minnesområde som ägs av en kringutrustning kallas vanligtvis I/O-minne, för att skilja det från system-RAM (RAM), som helt enkelt kallas minne.

Portar och I/O-minne

En typisk förares arbete består till stor del av läs- och skrivportar och I/O-minne. Portar och I/O-minne förenas av ett gemensamt namn - I/O-region (eller område).

Tyvärr kan inte varje bussarkitektur tydligt definiera I/O-regionen som tillhör varje enhet, och det är möjligt att föraren måste gissa platsen för regionen den tillhör, eller till och med försöka läs-/skrivoperationer på möjlig adress mellanslag. Detta problem är särskilt
syftar på ISA-bussen, som fortfarande används för att installera enkla enheter i en persondator och som är mycket populär i industrivärlden vid implementering av PC/104 (se avsnittet "PC/104 och PC/104+" i kapitel 15 "Översikt över perifera bussar" ).

Vilken buss som än används för att ansluta en hårdvaruenhet, måste enhetsdrivrutinen garanteras exklusiv åtkomst till sin I/O-region för att förhindra kollisioner mellan förare. Om en modul, som kommer åt sin egen enhet, skriver till en enhet som inte tillhör den, kan detta leda till ödesdigra konsekvenser.

Linux-utvecklare implementerade en mekanism för att begära/släppa I/O-regioner främst för att förhindra kollisioner mellan olika enheter. Denna mekanism har länge använts för I/O-portar och har nyligen generaliserats till resurshantering i allmänhet. Observera att denna mekanism representerar en mjukvaruabstraktion och inte omfattar hårdvarufunktioner. Till exempel orsakar inte obehörig åtkomst till I/O-portar på hårdvarunivå något fel som liknar ett "segmenteringsfel", eftersom hårdvaran inte allokerar och auktoriserar sina resurser.

Information om registrerade resurser finns i textform i filerna /proc/ioports och /proc/iomem. Denna information har introducerats i Linux sedan kärnan 2.3. Som en påminnelse fokuserar den här boken främst på 2.4-kärnan, och kompatibilitetsanteckningar kommer att presenteras i slutet av kapitlet.

Hamnar

Följande är det typiska innehållet i filen /proc/ioports:

0000-001f: dma1 0020-003f: pic1 0040-005f: timer 0060-006f: tangentbord 0080-008f: dma sidreg. 00a0-00bf: pic2 00c0-00df: dma-1000: dma-1000: if-1000: i 01f0-01f7 : ide0 02f8-02ff: seriell(uppsättning) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga+ 03f6-03f6: ide0 03f8-03ff: seriell(uppsättning) 103fIX Corporation- 103fIX Corporation- 1030IX Corporation 1030IX: Intel 1030IX Corporation 1020IX 000 -1003 : acpi 1004-1005: acpi 1008-100b: acpi 100c-100f: acpi 1100-110f: Intel Corporation 82371AB PIIX4 IDE 1300-131f: pcnet_cs 1400-141f: ACPI1AB 141f: Intel Corporation 82371AB 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

Varje rad i den här filen visar hexadecimalt utbudet av portar som är associerade med drivrutinen eller enhetens ägare. I tidigare versioner av kärnan hade filen samma format, förutom att porthierarkin inte visades.

Filen kan användas för att undvika portkollisioner när en ny enhet läggs till i systemet. Detta är särskilt praktiskt när man manuellt konfigurerar installerad utrustning genom att byta byglar. I det här fallet kan användaren enkelt se listan över använda portar och välja ett fritt intervall för enheten som ska installeras. Och även om de flesta moderna enheter inte använder manuella byglar alls, används de fortfarande vid tillverkning av småskaliga komponenter.

Vad som är viktigare är att filen /proc/ioports har en programmatiskt tillgänglig datastruktur associerad med den. Därför, när enhetsdrivrutinen initieras, kan den känna till det upptagna området av I/O-portar. Detta innebär att om det är nödvändigt att skanna portar för att leta efter en ny enhet, kan föraren undvika situationen att skriva till portar som är upptagna av andra enheter.

Att skanna ISA-bussen är känt för att vara en riskfylld uppgift. Därför undviker vissa drivrutiner som distribueras med den officiella Linux-kärnan sådan genomsökning när modulen laddas. Genom att göra det undviker de risken att skada ett körande system genom att skriva till portar som används av annan utrustning. Lyckligtvis är moderna bussarkitekturer immuna mot dessa problem.

Mjukvarugränssnittet som används för att komma åt I/O-register består av följande tre funktioner:

Int check_region(osignerad lång start, osignerad lång len); struct resurs *request_region(osignerad lång start, osignerad lång len, char *namn); void release_region(osignerad lång start, osignerad lång len);

Fungera check_region() kan anropas för att kontrollera om ett visst antal portar är upptaget. Den returnerar en negativ felkod (som -EBUSY eller -EINVAL) om svaret är negativt.

Fungera request_region() utför tilldelningen av ett givet intervall av adresser och returnerar, om framgångsrikt, en icke-null-pekare. Föraren behöver inte lagra eller använda den returnerade pekaren. Allt du behöver göra är att kontrollera NULL. Kod som bara ska fungera med en 2.4 (eller högre) kärna behöver inte alls anropa check_region()-funktionen. Det råder ingen tvekan om fördelen med denna distributionsmetod, eftersom
det är okänt vad som kan hända mellan anrop till check_region() och request_region(). Om du vill bibehålla kompatibilitet med äldre versioner av kärnan, är det nödvändigt att anropa check_region() före request_region().

Fungera release_region() måste anropas när drivrutinen släpper tidigare använda portar.

Det faktiska värdet på pekaren som returneras av request_region() används endast av resursallokeringsundersystemet som körs i kärnan.

Dessa tre funktioner är faktiskt makron definierade i .

Nedan är ett exempel på anropssekvensen som används för att registrera portar. Exemplet är hämtat från förarkoden för skallträning. (Funktionskoden skull_probe_hw() visas inte här eftersom den innehåller hårdvaruberoende kod.)

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

Det här exemplet kontrollerar först tillgängligheten för det nödvändiga utbudet av portar. Om portarna inte är tillgängliga är åtkomst till utrustningen inte möjlig.
Den faktiska platsen för enhetsportarna kan klargöras genom att skanna. Funktionen request_region() bör inte, i det här exemplet,
kommer att sluta i misslyckande. Kärnan kan inte ladda mer än en modul åt gången, så portanvändningskollisioner kommer inte att inträffa
måste.

Eventuella I/O-portar som tilldelats av drivrutinen måste släppas i efterhand. Vår skull-drivrutin gör detta i cleanup_module()-funktionen:

Statisk void skull_release(osignerad int port, osignerad int range) ( release_region(port,range); )

Mekanismen för resursbegäran/frisättning liknar mekanismen för modulregistrering/avregistrering och är perfekt implementerad baserat på goto-operatörens användningsschema som beskrivs ovan.

Minne

Information om I/O-minne är tillgänglig via filen /proc/iomem. Nedan är ett typiskt exempel på en sådan fil för en persondator:

00000000-0009fbff: System RAM 0009fc00-0009ffff: reserverat 000a0000-000bffff: Video RAM område 000c0000-000c7fff: Video ROM 000f0000-000f0000-000f0000-000ff RAM: 000ff RAM 0 0100000-0022c557: Kärnkod 0022c558-0024455f: Kärndata 20000000 - 2ffffffff: Intel Corporation 440BX/ZX - 82443BX/ZX Värdbrygga 68000000-68000fff: Texas Instruments PCI1225 68001000-68001fff: Texas Instruments PCI1225 (#2) #300ffff:0e PCI1225 (#2) #2000000-0 Buss 0-e7 ffffff: PCI Bus #01 e4000000 -e4ffffff : ATI Technologies Inc 3D Rage LT Pro AGP-133 e6000000-e6000fff: ATI Technologies Inc 3D Rage LT Pro AGP-133 fffc0000-ffffffff: reserverad

Adressintervallsvärdena visas i hexadecimal notation. För varje aresområde visas dess ägare.

Registrering av I/O-minnesåtkomster liknar att registrera I/O-portar och bygger på samma mekanism i kärnan.

För att erhålla och frigöra det erforderliga området av I/O-minnesadresser måste föraren använda följande anrop:

Int check_mem_region(osignerad lång start, osignerad lång len); int request_mem_region(osignerad lång start, osignerad lång len, char *namn); int release_mem_region(osignerad lång start, osignerad lång len);

Vanligtvis känner föraren till intervallet för I/O-minnesadresser, så koden för allokering av denna resurs kan reduceras jämfört med exemplet för tilldelning av ett intervall av portar:

If (check_mem_region(mem_addr, mem_size)) ( printk("drivrutinsnamn: minne som redan används\n"); returnera -EBUSY; ) request_mem_region(mem_addr, mem_size, "drivrutinsnamn");

Resursallokering i Linux 2.4

Den nuvarande resursallokeringsmekanismen introducerades i Linux-kärnan 2.3.11 och ger flexibel åtkomst till systemresurshantering. Detta avsnitt beskriver kortfattat denna mekanism. Grundläggande resursallokeringsfunktioner (som request_region() etc.) är dock fortfarande implementerade som makron och används för bakåtkompatibilitet med tidigare versioner av kärnan. I de flesta fall behöver du inte veta något om själva distributionsmekanismen, men det kan vara intressant när du skapar mer komplexa drivrutiner.

Resurshanteringssystemet implementerat i Linux kan hantera godtyckliga resurser på ett enhetligt hierarkiskt sätt. Globala systemresurser (till exempel I/O-portar) kan delas in i delmängder - till exempel de som är relaterade till en viss hårdvarubussplats. Vissa drivrutiner kan också valfritt dela upp infångade resurser baserat på deras logiska struktur.

Omfånget av allokerade resurser beskrivs genom strukturresursstrukturen, som deklareras i rubrikfilen :

Strukturresurs ( const char *namn; osignerad lång start, slut; osignerad långa flaggor; strukturresurs *förälder, *syskon, *barn; );

Ett globalt (root) utbud av resurser skapas vid uppstart. Till exempel skapas en resursstruktur som beskriver I/O-portar enligt följande:

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

En resurs som kallas PCI IO beskrivs här, som täcker adressintervallet från noll till IO_SPACE_LIMIT. Värdet på denna variabel beror på vilken plattform som används och kan vara lika med 0xFFFF (16-bitars adressutrymme, för x86, IA-64, Alpha, M68k och MIPS-arkitekturer), 0xFFFFFFFF (32-bitars adressutrymme, för SPARC, PPC , SH) eller 0xFFFFFFFFFFFFFFFF (64-bitars, SPARC64).

Delområden för denna resurs kan skapas med ett anrop till allocate_resource(). Till exempel, under PCI-bussinitiering skapas en ny resurs för adressregionen för denna buss och tilldelas en fysisk enhet. När PCI-kärnkoden bearbetar port- och minnestilldelningar, skapar den en ny resurs för endast dessa regioner och allokerar dem med anrop till ioport_resource() eller iomem_resource().

Föraren kan sedan begära en delmängd av en resurs (vanligtvis en del av en global resurs) och markera den som upptagen. Resursförvärvning åstadkommes genom att anropa request_region(), som returnerar antingen en pekare till en ny strukturresursstruktur som beskriver den begärda resursen, eller NULL vid fel. Denna struktur är en del av det globala resursträdet. Som redan nämnts, efter att ha erhållit resursen, kommer föraren inte att behöva värdet av denna pekare.

Den intresserade läsaren kan njuta av att se detaljerna för detta resurshanteringsschema i filen kernel/resource.c som finns i kärnans källkodskatalog. Men för de flesta utvecklare kommer den kunskap som redan presenteras att vara tillräcklig.

Den skiktade resursallokeringsmekanismen ger dubbla fördelar. Å ena sidan ger det en visuell representation av kärndatastrukturer. Låt oss ta en titt på exempelfilen /proc/ioports igen:

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

e800-e8ff-serien är allokerad till Adaptec-adaptern, som utsett sig som en drivrutin på PCI-bussen. Det mesta av detta intervall efterfrågades av aic7xxx-drivrutinen.

En annan fördel med denna resurshantering är uppdelningen av adressutrymmet i delområden som återspeglar den faktiska sammankopplingen av utrustningen. Resurshanteraren kan inte tilldela överlappande adressunderområden, vilket kan förhindra installationen av en felaktig drivrutin.

Automatisk och manuell konfiguration

Vissa parametrar som krävs av drivrutinen kan variera från system till system. Till exempel måste föraren vara medveten om giltiga I/O-adresser och minnesintervall. För välorganiserade bussgränssnitt är detta inget problem. Men ibland måste du skicka parametrar till föraren för att hjälpa den att hitta sin egen enhet, eller aktivera/avaktivera vissa av dess funktioner.

Dessa inställningar som påverkar drivrutinsdriften varierar beroende på enhet. Detta kan till exempel vara versionsnumret för den installerade enheten. Naturligtvis är sådan information nödvändig för att föraren ska fungera korrekt med enheten. Att definiera sådana parametrar (drivrutinskonfiguration) är ganska svårt
en knepig uppgift som utförs när drivrutinen initieras.

Vanligtvis finns det två sätt att få de korrekta värdena för denna parameter - antingen definierar användaren dem explicit eller så bestämmer föraren dem oberoende, baserat på polling av utrustningen. Även om automatisk identifiering utan tvekan är den bästa lösningen för drivrutinskonfiguration,
anpassad konfiguration är mycket lättare att implementera. Drivrutinsutvecklaren bör implementera automatisk konfiguration av drivrutiner där det är möjligt, men samtidigt ska han förse användaren med en manuell konfigurationsmekanism. Självklart bör manuell konfiguration ha högre prioritet än automatisk konfiguration. I de inledande stadierna av utvecklingen implementeras vanligtvis endast manuell överföring av parametrar till föraren. Automatisk konfiguration, om möjligt, läggs till senare.

Många drivrutiner, bland deras konfigurationsparametrar, har parametrar som styr drivrutinsoperationer. Till exempel tillåter IDE-gränssnittsdrivrutiner (Integrated Device Electronics) användaren att styra DMA-operationer. Så om din drivrutin gör ett bra jobb med att automatiskt upptäcka hårdvara, kanske du vill ge användaren kontroll över förarens funktionalitet.

Parametervärden kan skickas under modulladdning med insmod- eller modprobe-kommandona. Nyligen har det blivit möjligt att läsa värdet på parametrar från en konfigurationsfil (vanligtvis /etc/modules.conf). Heltals- och strängvärden kan skickas som parametrar. Således, om du behöver skicka ett heltalsvärde för parametern skull_ival och ett strängvärde för parametern skull_sval, kan du skicka dem under modulladdning med ytterligare parametrar till kommandot insmod:

Insmod skull skull_ival=666 skull_sval="odjuret"

Men innan insmod-kommandot kan ändra värdena för en moduls parametrar, måste modulen göra dessa parametrar tillgängliga. Parametrar deklareras med hjälp av makrodefinitionen MODULE_PARM, som definieras i rubrikfilen module.h. Makrot MODULE_PARM tar två parametrar: namnet på variabeln och en sträng som definierar dess typ. Denna makrodefinition måste placeras utanför alla funktioner och är vanligtvis placerad i början av filen efter att variablerna har definierats. Så de två parametrarna som nämns ovan kan deklareras enligt följande:

Int skull_ival=0; röding *skull_sval; MODULE_PARM(skull_ival, "i"); MODULE_PARM(skull_sval, "s");

Det finns för närvarande fem typer av modulparametrar som stöds:

  • b - ett-byte värde;
  • h - (kort) två-byte värde;
  • i - heltal;
  • l - långt heltal;
  • s - sträng (char *);

När det gäller strängparametrar måste en pekare (char *) deklareras i modulen. Kommandot insmod allokerar minne för strängen som skickas in och initierar den med det önskade värdet. Med hjälp av makrot MODULE_PARM kan du initiera arrayer av parametrar. I det här fallet bestämmer det heltal som föregår typtecknet längden på matrisen. När två heltal är specificerade, separerade med ett bindestreck, bestämmer de det minsta och maximala antalet värden som ska överföras. För en mer detaljerad förståelse av hur detta makro fungerar, se rubrikfilen .

Anta till exempel att en array av parametrar måste initieras med minst två och minst fyra heltalsvärden. Då kan det beskrivas så här:

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

Dessutom har programmerarens verktygslåda makrodefinitionen MODULE_PARM_DESC, som låter dig kommentera modulparametrarna som skickas. Dessa kommentarer lagras i modulobjektfilen och kan ses med till exempel objdump-verktyget eller med hjälp av automatiserade systemadministrationsverktyg. Här är ett exempel på hur man använder denna makrodefinition:

Int bas_port = 0x300; MODULE_PARM(bas_port, "i"); MODULE_PARM_DESC (base_port, "Bas I/O-port (standard 0x300)");

Det är önskvärt att alla modulparametrar har standardvärden. Ändring av dessa värden med insmod bör endast krävas vid behov. Modulen kan kontrollera den explicita inställningen av parametrar genom att kontrollera deras nuvarande värden med standardvärdena. Därefter kan du implementera en automatisk konfigurationsmekanism baserat på följande diagram. Om parametervärdena har standardvärden, utförs automatisk konfiguration. Annars används de aktuella värdena. För att detta schema ska fungera är det nödvändigt att standardvärdena inte motsvarar någon av de möjliga verkliga systemkonfigurationerna. Det skulle då antas att sådana värden inte kan ställas in av användaren i manuell konfiguration.

Följande exempel visar hur skalldrivrutinen automatiskt upptäcker adressutrymmet för enhetsportar. I exemplet ovan tittar automatisk identifiering på flera enheter, medan manuell konfiguration begränsar föraren till en enda enhet. Du har redan träffat skull_detect-funktionen tidigare i avsnittet som beskriver I/O-portar. Implementeringen av skull_init_board() visas inte eftersom det
Utför hårdvaruberoende initiering.

/* * portintervall: enheten kan ligga mellan * 0x280 och 0x300, i steg om 0x10. Den använder 0x10 portar. */ #define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 /* * följande funktion utför autodetektion, om inte ett specifikt *värde tilldelades av insmod till "skull_port_base" */ static int skull; /* 0 tvingar fram autodetektion */ MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC(skull_port_base, "Bas I/O-port för skalle"); static int skull_find_hw(void) /* returnerar antalet enheter */ ( /* basen är antingen laddningstidsvärdet eller den första testversionen */ int bas = skull_port_base ? skull_port_base: SKULL_PORT_FLOOR; int resultat = 0; /* loop ett tid om värde tilldelas; prova alla om du automatiskt upptäcker */ do ( if (skull_detect(base, SKULL_PORT_RANGE) == 0) ( skull_init_board(base); result++; ) base += SKULL_PORT_RANGE; /* förbered för nästa testversion */ ) medan (skull_port_base == 0 && bas< SKULL_PORT_CEIL); return result; }

Om konfigurationsvariabler endast används inuti drivrutinen (d.v.s. inte publicerade i kärnsymbolstabellen), kan programmeraren göra livet lite lättare för användaren genom att inte använda prefix i variabelnamnen (i vårt fall skull_-prefixet) . Dessa prefix betyder lite för användaren, och deras frånvaro förenklar att skriva kommandot från tangentbordet.

För fullständighetens skull kommer vi att ge en beskrivning av ytterligare tre makrodefinitioner som låter dig lägga några kommentarer i objektfilen.

MODULE_AUTHOR (namn) Placerar en rad med författarens namn i objektfilen. MODULE_DESCRIPTION(desc) Placerar en rad med en allmän beskrivning av modulen i objektfilen. MODULE_SUPPORTED_DEVICE(dev) Placerar en rad som beskriver enheten som stöds av modulen. Linux tillhandahåller ett kraftfullt och omfattande API för applikationer, men ibland räcker det inte. För att interagera med hårdvara eller utföra operationer med tillgång till privilegierad information i systemet behövs en kärndrivrutin.

En Linux-kärnmodul är en kompilerad binär kod som infogas direkt i Linux-kärnan och körs i ring 0, den inre och minst säkra instruktionskörningsringen i x86–64-processorn. Här exekveras koden helt utan några kontroller, men med otrolig hastighet och med tillgång till alla systemresurser.

Inte för bara dödliga

Att skriva en Linux-kärnmodul är inte för svaga hjärtan. Genom att ändra kärnan riskerar du att förlora data. Det finns ingen standardsäkerhet i kärnkoden som det finns i vanliga Linux-applikationer. Om du gör ett misstag, lägg sedan på hela systemet.

Det som gör situationen värre är att problemet inte nödvändigtvis dyker upp direkt. Om modulen hänger systemet omedelbart efter laddning, är detta det bästa felscenariot. Ju mer kod det finns, desto högre är risken för oändliga loopar och minnesläckor. Om du inte är försiktig kommer problemen gradvis att öka allt eftersom maskinen fungerar. Så småningom kan viktiga datastrukturer och till och med buffertar skrivas över.

Traditionella applikationsutvecklingsparadigm kan till stor del glömmas bort. Förutom att ladda och ladda ur en modul, kommer du att skriva kod som reagerar på systemhändelser snarare än att följa ett sekventiellt mönster. När du arbetar med kärnan skriver du API:t, inte själva applikationerna.

Du har inte heller tillgång till standardbiblioteket. Även om kärnan tillhandahåller vissa funktioner som printk (som är en ersättning för printf) och kmalloc (som fungerar på samma sätt som malloc), är du för det mesta utlämnad till dina egna enheter. Dessutom bör du städa helt efter dig efter att du har lossat modulen. Här finns ingen sophämtning.

Nödvändiga komponenter

Innan du börjar bör du se till att du har alla nödvändiga verktyg för jobbet. Viktigast av allt, du behöver en Linux-maskin. Jag vet att detta är oväntat! Även om alla Linux-distributioner fungerar, använder jag Ubuntu 16.04 LTS i det här exemplet, så du kan behöva ändra installationskommandona något om du använder andra distributioner.

För det andra behöver du antingen en separat fysisk maskin eller en virtuell maskin. Personligen föredrar jag att arbeta på en virtuell maskin, men välj själv. Jag rekommenderar inte att du använder din huvudmaskin på grund av dataförlust när du gör ett misstag. Jag säger "när" och inte "om" eftersom du definitivt kommer att hänga bilen åtminstone några gånger under processen. Dina senaste kodändringar kan fortfarande finnas i skrivbufferten när kärnan får panik, så dina källor kan också vara skadade. Att testa i en virtuell maskin eliminerar dessa risker.

Slutligen måste du kunna åtminstone lite C. C++-körtiden är för stor för kärnan, så du måste skriva i ren, ren C. Viss kunskap om assemblerspråk är också till hjälp för att interagera med hårdvaran.

Installera utvecklingsmiljön

På Ubuntu måste du köra:

Apt-get install build-essential linux-headers-`uname -r`
Vi installerar de viktigaste utvecklingsverktygen och kärnrubriken som krävs för detta exempel.

Exemplen nedan förutsätter att du kör som en vanlig användare snarare än root, men att du har sudo-privilegier. Sudo krävs för att ladda kärnmoduler, men vi vill arbeta utanför root när det är möjligt.

Börja

Låt oss börja skriva kod. Låt oss förbereda vår miljö:

Mkdir ~/src/lkm_example cd ~/src/lkm_example
Starta din favoritredigerare (vim i mitt fall) och skapa en fil lkm_example.c med följande innehåll:

#omfatta #omfatta #omfatta MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION("Ett enkelt exempel på Linux-modul."); MODULE_VERSION("0.01"); static int __init lkm_example_init(void) ( printk(KERN_INFO “Hej, världen!\n”); return 0; ) static void __exit lkm_example_exit(void) ( printk(KERN_INFO “Adjö, världen!\n”); ) modul__init(void) ); module_exit(lkm_example_exit);
Vi har designat den enklaste möjliga modulen, låt oss ta en närmare titt på dess viktigaste delar:

  • include listar huvudfilerna som behövs för att utveckla Linux-kärnan.
  • MODULE_LICENSE kan ställas in på olika värden beroende på modulens licens. För att se hela listan, kör:

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

  • Vi ställer in init (laddning) och exit (avlastning) som statiska funktioner som returnerar heltal.
  • Notera användningen av printk istället för printf . Alternativen för printk skiljer sig också från printf . Till exempel specificeras KERN_INFO-flaggan för att deklarera loggningsprioritet för en viss rad utan komma. Kärnan hanterar dessa saker i printk-funktionen för att spara stackminne.
  • I slutet av filen kan du anropa module_init och module_exit och specificera laddnings- och lossningsfunktionerna. Detta gör det möjligt att godtyckligt namnge funktioner.
Vi kan dock inte kompilera den här filen ännu. Makefile behövs. Detta grundläggande exempel kommer att räcka för nu. Observera att make är väldigt kräsen med mellanslag och tabbar, så se till att använda tabbar istället för blanksteg där det är lämpligt.

Obj-m += lkm_example.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r) )/bygg M=$(PWD) ren
Om vi ​​kör gör det borde kompilera vår modul framgångsrikt. Resultatet blir filen lkm_example.ko. Om några fel uppstår, kontrollera att citattecken i källkoden är korrekt inställda och inte av misstag i UTF-8-kodning.

Nu kan du implementera modulen och testa den. För att göra detta kör vi:

Sudo insmod lkm_example.ko
Om allt är bra, kommer du inte att se någonting. Printk-funktionen ger utdata inte till konsolen, utan till kärnloggen. För att se måste du köra:

Sudo dmesg
Du borde se raden "Hello, World!" med en tidsstämpel i början. Detta betyder att vår kärnmodul har laddats och framgångsrikt skrivits till kärnloggen. Vi kan också kontrollera att modulen fortfarande finns i minnet:

lsmod | grep "lkm_example"
För att ta bort en modul, kör:

Sudo rmmod lkm_example
Om du kör dmesg igen kommer du att se posten "Goodbye, World!" i loggen. Du kan köra lsmod igen och se till att modulen är urladdad.

Som du kan se är denna testprocedur lite tråkig, men den kan automatiseras genom att lägga till:

Test: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
i slutet av Makefilen och kör sedan:

Gör test
för att testa modulen och kontrollera utdata till kärnloggen utan att behöva köra separata kommandon.

Vi har nu en fullt fungerande, om än helt trivial, kärnmodul!

Låt oss gräva lite djupare. Även om kärnmoduler kan utföra alla typer av uppgifter, är gränssnitt med applikationer ett av de vanligaste användningsfallen.

Eftersom applikationer inte tillåts visa minne i kärnutrymmet måste de använda API:et för att kommunicera med dem. Även om det tekniskt finns flera sätt att göra detta, är det vanligaste att skapa en enhetsfil.

Du har förmodligen hanterat enhetsfiler tidigare. Kommandon som nämner /dev/zero , /dev/null och liknande interagerar med "noll"- och "null"-enheterna, som returnerar de förväntade värdena.

I vårt exempel returnerar vi "Hello, World". Även om detta inte är en särskilt användbar funktion för applikationer, demonstrerar den fortfarande processen att interagera med en applikation genom en enhetsfil.

Här är hela listan:

#omfatta #omfatta #omfatta #omfatta #omfatta MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION("Ett enkelt exempel på Linux-modul."); MODULE_VERSION("0.01"); #define DEVICE_NAME “lkm_example” #define EXAMPLE_MSG “Hej världen!\n” #define MSG_BUFFER_LEN 15 /* Prototyper för enhetsfunktioner */ static int device_open(struct inode *, struct file *); static int device_release(struct inode *, struct file *); static ssize_t device_read(struct file *, char *, size_t, loff_t *); statisk ssize_t device_write(struct file *, const char *, size_t, loff_t *); statisk int major_num; static int device_open_count = 0; statisk char msg_buffer; statiskt tecken *msg_ptr; /* Denna struktur pekar på alla enhetens funktioner */ static struct file_operations file_ops = (.read = device_read, .write = device_write, .open = device_open, .release = device_release ); /* När en process läser från vår enhet anropas denna. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) ( int bytes_read = 0; /* Om vi ​​är i slutet, gå tillbaka till början */ if (*msg_ptr = = 0) ( msg_ptr = msg_buffer; ) /* Lägg data i bufferten */ while (len && *msg_ptr) ( /* Bufferten finns i användardata, inte kärnan, så du kan inte bara referera * med en pekare. funktion put_user hanterar detta för oss */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; ) return bytes_read; ) /* Anropas när en process försöker skriva till vår enhet */ static ssize_t device_write(struct file * flip, const char *buffer, size_t len, loff_t *offset) ( /* Detta är en skrivskyddad enhet */ printk(KERN_ALERT “Denna operation stöds inte.\n”); return -EINVAL; ) /* Anropas när en process öppnar vår enhet */ static int device_open(struct inode *inode, structfil *fil) ( /* Om enheten är öppen, returnera busy */ if (device_open_count) (retur -EBUSY; ) device_open_count++; try_module_get(THIS_MODULE); returnera 0; ) /* Anropas när en process stänger vår enhet */ static int device_release(struct inode *inode, struct file *file) ( /* Minska antalet öppna räknare och användningsräkning. Utan detta skulle modulen inte laddas ur. */ device_open_count- -; modul_put(THIS_MODULE); return 0; ) static int __init lkm_example_init(void) ( /* Fyll buffert med vårt meddelande */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Ställ in msg_ptr =buffert_buffert */ msg buffert_buffer ; /* Försök att registrera teckenenhet */ major_num = register_chrdev(0, “lkm_example”, &file_ops); if (major_num< 0) { printk(KERN_ALERT “Could not register device: %d\n”, major_num); return major_num; } else { printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num); return 0; } } static void __exit lkm_example_exit(void) { /* Remember - we have to clean up after ourselves. Unregister the character device. */ unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO “Goodbye, World!\n”); } /* Register module functions */ module_init(lkm_example_init); module_exit(lkm_example_exit);

Testa ett förbättrat exempel

Nu gör vårt exempel mer än att bara skriva ut ett meddelande vid lastning och lossning, så en mindre rigorös testprocedur kommer att behövas. Låt oss ändra Makefilen för att bara ladda modulen, utan att ladda ur den.

Obj-m += lkm_example.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r) )/build M=$(PWD) rent test: # Vi sätter ett - framför kommandot rmmod för att tala om för make att ignorera # ett fel om modulen inte är laddad. -sudo rmmod lkm_example # Rensa kärnloggen utan eko sudo dmesg -C # Infoga modulen sudo insmod lkm_example.ko # Visa kärnloggen dmesg
Nu efter att ha kört gör test kommer du att se huvudenhetsnumret matas ut. I vårt exempel tilldelas den automatiskt av kärnan. Detta nummer behövs dock för att skapa en ny enhet.

Ta numret som genereras av make test och använd det för att skapa en enhetsfil så att vi kan kommunicera med vår kärnmodul från användarutrymmet.

Sudo mknod /dev/lkm_example med MAJOR 0
(i det här exemplet, ersätt MAJOR med värdet som erhålls från make test eller dmesg)

Alternativet c i kommandot mknod talar om för mknod att vi måste skapa en teckenenhetsfil.

Nu kan vi hämta innehåll från enheten:

Cat /dev/lkm_example
eller till och med genom kommandot dd:

Dd if=/dev/lkm_example of=test bs=14 count=100
Du kan också komma åt den här filen från applikationer. Dessa behöver inte vara kompilerade applikationer – även Python-, Ruby- och PHP-skript har tillgång till denna data.

När vi är klara med enheten tar vi bort den och laddar ur modulen:

Sudo rm /dev/lkm_example sudo rmmod lkm_example

Slutsats

Jag hoppas att du gillade våra upptåg i core space. Även om exemplen som visas är primitiva, kan dessa strukturer användas för att skapa dina egna moduler som utför mycket komplexa uppgifter.

Kom bara ihåg att i kärnutrymmet är allt ditt ansvar. Det finns ingen support eller andra chans för din kod. Om du gör ett projekt för en kund, planera i förväg för dubbel, om inte trippel, felsökningstid. Kärnkoden måste vara så perfekt som möjligt för att säkerställa integriteten och tillförlitligheten hos de system som den körs på.

Några funktioner i modulär programmering och allmänna rekommendationer för att konstruera underprogram av en modulär struktur.

Moduler ansluts till huvudprogrammet i den ordning som de deklareras som USES, och i samma ordning finns initieringsblocken för moduler som är anslutna till huvudprogrammet innan programmet börjar köras.

Ordningen i vilken moduler exekveras kan påverka biblioteksdataåtkomst och rutinåtkomst.

Till exempel, om moduler med namnen M1, M2 innehåller samma typ A, variabel B och subrutin C, kommer anrop till A, B, C i denna PU att efter anslutning av dessa USES-modeller vara likvärdiga med anrop till objekt till modul M2 .

Men för att karakterisera riktigheten av anrop till de nödvändiga objekten med samma namn från olika anslutna moduler, är det lämpligt att när du kommer åt en modul först ange modulens namn, följt av en punkt namnet på objektet: M1. A M1.B M1.C M2.B.

Uppenbarligen är det väldigt enkelt att dela upp ett stort program i två delar (PU), d.v.s. huvudprogram + moduler.

Placera varje PU i sitt eget minnessegment och i sin egen diskfil.

Alla deklarationer av typer, såväl som de variabler som ska vara tillgängliga för enskilda PU:er (huvudprogrammet och framtida moduler) ska placeras i en separat modul med en tom körbar del. Du bör dock inte vara uppmärksam på att vissa PE (till exempel en modul) inte använder alla dessa deklarationer. Initieringsdelen av en sådan modul kan innehålla satser som associerar filvariabler med icke-standardiserade textfiler (ASSIGN) och initierar dessa filer, d.v.s. indikerar dataöverföringsanrop för dem (RESET och REWRITE).

Den första gruppen av andra subrutiner, till exempel, flera kompakta funktioner bör placeras i modul 3 (i sin tur), andra, till exempel, allmänna förfaranden - i modul 4, etc.

När du distribuerar subrutiner i moduler i ett komplext projekt bör särskild uppmärksamhet ägnas åt ordningen och platsen för deras skrivning.

TP-miljön innehåller verktyg som styr olika sätt att kompilera moduler.

Kompilera Alt+F9 KÖR Cntr+F9

Destinationsminne

Dessa lägen skiljer sig endast i kommunikationsmetoden och huvudprogrammet.

Kompileringsläge

Kompilerar huvudprogrammet eller modulen som för närvarande finns i det aktiva redigeringsfönstret. Om denna PU innehåller åtkomst till icke-standardiserade användarmoduler, kräver detta läge närvaro av diskfiler med samma namn med tillägget ___.tpu för varje sådan insticksmodul.



Om destinationen är lagrad i minnet kommer dessa filer bara att finnas kvar i minnet och en diskfil skapas inte.

Det är dock mycket enklare att skapa tpu-filer tillsammans med kompilatorn för hela programmet med andra lägen som inte kräver inställning av Disk för destinationsalternativet.

Gör läge

Vid kompilering i detta läge kontrolleras följande först (innan huvudprogrammet kompileras) för varje modul:

1) Förekomsten av en disk tpu-fil; om den inte finns skapas den automatiskt genom att källkoden för modulen kompileras, dvs. dess pas-fil

2) Överensstämmelse mellan den hittade tpu-filen och modulens källtext, där ändringar kunde ha gjorts; annars skapas tpu-filen automatiskt igen

3) Immutability av gränssnittssektionen av modulen: om den har ändrats, så kompileras även alla dessa moduler (deras källkod-pas-filer) i vilka denna modul är specificerad i USES-satsen.

Om det inte var någon förändring i modulernas källkoder, interagerar kompilatorn med dessa tpu-filer och använder kompileringstid.

Byggläge

Till skillnad från Make-läget, kräver det nödvändigtvis närvaron av källkodsfiler; kompilerar (omkompilerar) varje modul och säkerställer därigenom att alla ändringar i texterna i pas-filer beaktas. Detta ökar kompileringstiden för programmet som helhet.

Till skillnad från kompileringsläget låter Make- och Build-lägena dig börja kompilera ett program med en modulär struktur från en given pas-fil (den kallas den primära filen), oavsett vilken fil (eller del av programmet) som finns i den aktiva filen. redigeringsfönster. För att göra detta, välj alternativet Primär fil i kompileringsobjektet, tryck på Enter och skriv ner namnet på den primära pas-filen, och sedan startar kompileringen från denna fil.

Om den primära filen inte är specificerad på detta sätt, är kompilering i lägena Make, Build och RUN endast möjlig om huvudprogrammet finns i det aktiva redigeringsfönstret.

Notera: I framtiden planerar jag att använda T2-systemet för att kompilera kärnan och modulerna för Puppy. T2 är för närvarande installerat för att kompilera kärnan och många ytterligare moduler, men inte den version som för närvarande används i Puppy. Jag tänker synkronisera i framtida versioner av Puppy, så att kärnan som kompileras i T2 kommer att användas i Puppy. Se http://www.puppyos.net/pfs/ för ytterligare information om Puppy och T2.

Puppy har ett väldigt enkelt sätt att använda C/C++-kompilatorer genom att lägga till en enda fil, devx_xxx.sfs, där xxx är versionsnumret. Till exempel skulle Puppy 2.12 ha en överensstämmelseutvecklingsfil med namnet devx_212.sfs. När du kör i LiveCD-läge, placera filen devx_xxx.sfs på samma plats som din personliga inställningsfil pup_save.3fs, som vanligtvis finns i katalogen /mnt/home/. Detta gäller även andra installationslägen som har en pup_save.3fs-fil. Om Puppy är installerad på en hårddisk med en fullständig "Alternativ 2"-installation, så finns det ingen personlig fil, titta på Puppy-webbsidorna för att kompileras med olika konfigurationsalternativ, så att modulerna inte är kompatibla. Dessa versioner kräver bara en patch för squashfs. Puppy 2.12 har kärna 2.6.18.1 och har tre fixar; squashfs, standard konsolloggnivå och avstängningsfix.

Dessa kommandon för att patcha kärnan ges enbart för din självutbildning, eftersom en patchad kärna redan är tillgänglig...

Det första du bör göra först är att ladda ner själva kärnan. Den finns för att hitta en länk till en lämplig nedladdningssida. Detta bör vara en "gammal" källa tillgänglig på kernel.org eller dess speglar.

Anslut till Internet, ladda ner kärnan till mappen /usr/src. Packa sedan upp den:

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

Du bör se denna mapp: /usr/src/linux-2.6.16.7. Du måste sedan se till att denna länk pekar till kärnan:

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

Du måste tillämpa följande korrigeringar så att du har exakt samma källa som används när du kompilerar kärnan för Puppy. Annars kommer du att få "olösta symboler" felmeddelanden när du kompilerar drivrutinen och sedan försöker använda den med valpkärnan. Applicera squashfs fix

För det andra, applicera Squashfs-plåstret. Squashfs-patchen lägger till stöd för Squashfs, vilket gör filsystemet skrivskyddat.

Ladda ner patchen, squashfs2.1-patch-k2.6.9.gz, till mappen /usr/src. Observera att denna korrigering gjordes för kärnversion 2.6.9, men fortsätter att fungera i version 2.6.11.x så länge som en referens till linux-2.6.9 finns. Tillämpa sedan korrigeringen:

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

Obs, p1 har siffran 1, inte symbolen l... (Bra skämt - cirka. översättning)

patch --torrkörning -p1< ../ squashfs2.1-patch patch -p1 < ../ squashfs2.1-patch

Redo! Kärnan är redo att kompileras!

Kompilerar kärnan

Du måste skaffa en konfigurationsfil för kärnan. En kopia av den finns i mappen /lib/modules.

Följ sedan dessa steg:

Cd/ usr/ src/ linux-2.6.18.1

Om det finns en .config-fil, kopiera den tillfälligt någonstans eller byt namn på den.

göra rent gör mrproper

Kopiera .config-filen för Puppy till /usr/src/linux-2.6.18.1... Den kommer att ha olika namn i /lib/modules, så byt namn till .config... Följande steg läser .config-filen och genererar en ny.

gör menuconfig

...gör alla ändringar du vill och spara dem. Du kommer nu att ha en ny .config-fil och du bör kopiera den någonstans för säker förvaring. Se not nedan

gör bzImage

Nu har du kompilerat kärnan.

Bra, du hittar Linux-kärnan i /usr/src/linux-2.6.18.1/arch/i386/boot/bzImage

Sammanställa moduler

Gå nu in i /lib/modules och om det redan finns en mapp som heter 2.6.18.1, byt namn på 2.6.18.1-mappen till 2.6.18.1-gammal .

Installera nu nya moduler:

Cd/ usr/ src/ linux-2.6.18.1 gör moduler gör modules_install

...efter detta bör du hitta de nya modulerna installerade i mappen /lib/modules/2.6.18.1.

Observera att det sista steget ovan kör programmet "depmod" och detta kan ge felmeddelanden om saknade symboler för några av modulerna. Oroa dig inte för det – en av utvecklarna har skruvat ihop och det betyder att vi inte kan använda den modulen.

Hur man använder den nya kärnan och modulerna

Det är bättre om du har Puppy Unleashed installerat. Sedan utökas tarballen och det finns 2 kataloger: "boot" och "kernels".

"Boot" innehåller filstrukturen och skriptet för att skapa den initiala virtuella disken. Du måste sätta några kärnmoduler där.

Katalogen "kärnor" har en katalog kernels/2.6.18.1/ , och du måste byta ut modulerna med dina uppdaterade. Du behöver inte ersätta den om du kompilerade om samma kärnversion (2.6.18.1).

Observera att i kernels/2.6.18.1 finns en fil som heter "System.map". Du bör byta namn på den och ersätta den med den nya från /usr/src/linux-2.6.18.1. Bläddra i mappen kernels/2.6.18.1/ och du bör veta vad som behöver uppdateras.

Om du kompilerade kärnan i en fullständig Puppy-installation kan du starta om med den nya kärnan. make modules_install är steget ovan för att installera nya moduler i /lib/modules/2.6.18.1, men du måste också installera en ny kärna. Jag startar med Grub och kopierar bara den nya kärnan till /boot-katalogen (och byter namn på filen från bzImage till vmlinuz).

Anmärkning angående menykonfig. Jag har använt det i evigheter så ta vissa saker för givet, men en nybörjare kan vara förvirrad och vilja avsluta programmet. Det finns en meny i toppnivåmenyn för att spara konfigurationen - ignorera den. Tryck bara på TAB-tangenten (eller högerpilen) för att markera "Avsluta"-knappen och tryck på ENTER-tangenten. Du kommer då att tillfrågas om du vill spara den nya konfigurationen och ditt svar bör vara Ja.


Topp