Strukturen til en kjernemodul og dens kompileringsmetoder. Funksjoner ved å kompilere et program med en modulær struktur. Introduksjon og bakgrunn

Hvorfor kompilere kjernen selv?
Kanskje hovedspørsmålet som stilles om kompilering av en kjerne er: "Hvorfor skal jeg gjøre dette?"
Mange anser dette som et meningsløst sløsing med tid for å vise seg som en smart og avansert Linux-bruker. Faktisk er kompilering av kjernen en veldig viktig sak. La oss si at du har kjøpt en ny bærbar datamaskin og webkameraet ditt ikke fungerer. Dine handlinger? Du ser inn i søkemotoren og ser etter en løsning på problemet på dette problemet. Ganske ofte kan det vise seg at webkameraet ditt kjører på en mer ny verson enn din. Hvis du ikke vet hvilken versjon du har, skriv inn uname -r i terminalen, som et resultat vil du få opp kjerneversjonen (for eksempel linux-2.6.31-10). Kjernekompilering er også mye brukt for å øke ytelsen: faktum er at kjernedistribusjoner som standard kompileres "for alle", og det er grunnen til at det inkluderer et stort antall drivere som du kanskje ikke trenger. Så hvis du kjenner maskinvaren du bruker godt, kan du deaktivere unødvendige drivere på konfigurasjonsstadiet. Det er også mulig å aktivere støtte for mer enn 4 GB RAM uten å endre systembitdybden. Så hvis du fortsatt trenger å ha din egen kjerne, la oss begynne å kompilere!

Innhenting av kjernekildekoden.
Det første du må gjøre er å få kildekoden for den nødvendige kjerneversjonen. Vanligvis må du få den siste stabile versjonen. Alle offisielle kjerneversjoner er tilgjengelige på kernel.org. Hvis du allerede har X-server installert ( hjemmedatamaskin), så kan du gå til nettstedet i favorittnettleseren din og laste ned ønsket versjon i tar.gz-arkivet (gzip-komprimert). Hvis du jobber i konsollen (du har for eksempel ikke installert X-serveren ennå eller konfigurerer serveren), kan du bruke en tekstleser (for eksempel elinks). Du kan også bruke standard nedlastingsbehandling wget:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.33.1.tar.gz
Men husk at du må vite det nøyaktige versjonsnummeret du trenger.

Pakker ut kildekodearkivet.
Når du har mottatt kildekodearkivet, må du pakke ut arkivet til en mappe. Dette kan gjøres fra grafisk filbehandlere(delfin, nautilus, etc.) eller via mc. Eller bruk den tradisjonelle tar-kommandoen:
tar -zxvf path_to_archive
Nå har du en mappe med kildekoden, gå til den ved å bruke kommandoen cd kjernekildekatalog(for å liste opp katalogene i en mappe, bruk ls-kommandoen).

Kjernekonfigurasjon.
Når du har navigert til kjernekildekatalogen, må du utføre en "20 minutters" kjernekonfigurasjon. Målet er å bare la de nødvendige driverne og funksjonene være igjen. Alle kommandoer må allerede utføres som en superbruker.

make config - konsollmodus for konfiguratoren.

lag menuconfig - konsollmodus i form av en liste.

make xconfig - grafisk modus.

Etter å ha gjort de nødvendige endringene, lagre innstillingene og gå ut av konfiguratoren.

Samling.
Tiden er inne for den siste fasen av monteringen - kompilering. Dette gjøres med to kommandoer:
lag && lag installer
Den første kommandoen vil kompilere alle filene til maskinkode, og den andre vil installere den nye kjernen på systemet ditt.
Vi venter fra 20 minutter til flere timer (avhengig av kraften til datamaskinen). Kjernen er installert. For å få det til å vises i grub(2)-listen, skriv inn (som superbruker)
update-grub
Nå etter omstart, trykk "Escape" og du vil se den nye kjernen i listen. Hvis kjernen ikke slår seg på, starter du bare opp med den gamle kjernen og konfigurerer den mer nøye.

KernelCheck - kompilerer kjernen uten å gå til konsollen.
lar deg bygge kjernen i fullstendig grafisk modus for Debian og distribusjoner basert på det. Etter lansering vil KernelCheck tilby de nyeste kjerneversjonene og oppdateringene, og etter ditt samtykke, last ned kildekoden og start den grafiske konfiguratoren. Programmet vil kompilere kjernen til .deb-pakker og installere dem. Alt du trenger å gjøre er å starte på nytt.

Om: "Basert på oversettelsen" Linux Device Driver 2. utgave. Oversettelse: Knyazev Alexey [e-postbeskyttet] Sist endret dato: 08/03/2004 Sted: http://lug.kmv.ru/index.php?page=knz_ldd2

La oss nå begynne å programmere! Dette kapittelet gir det grunnleggende om moduler og kjerneprogrammering.
Her skal vi bygge og lansere en fullverdig modul, hvis struktur tilsvarer enhver ekte modulær driver.
Samtidig vil vi konsentrere oss om hovedposisjonene uten å ta hensyn til spesifikasjonene til ekte enheter.

Alle deler av kjernen som funksjoner, variabler, header-filer og makroer som er nevnt her vil være
er beskrevet i detalj på slutten av kapittelet.

Hei Verden!

I prosessen med å bli kjent med det originale materialet skrevet av Alessndro Rubini & Jonathan Corbet, virket eksemplet gitt som Hello world for meg å være noe mislykket. Derfor ønsker jeg å gi leseren, etter min mening, en mer vellykket versjon av den første modulen. Jeg håper at det ikke vil være noen problemer med kompilering og installasjon under 2.4.x-kjernen. Den foreslåtte modulen og måten den er kompilert på gjør at den kan brukes i kjerner som støtter og ikke støtter versjonskontroll. Du vil bli kjent med alle detaljene og terminologien senere, så åpne vim nå og begynn å jobbe!

================================================== === //fil hello_knz.c #include #inkludere <1>Hei, verden\n"); return 0; ); void cleanup_module(void) ( printk("<1>Farvel grusom verden\n"); ) MODULE_LICENSE(“GPL”); ================================= ==================

For å kompilere en slik modul kan du bruke følgende Makefile. Ikke glem å sette et tabulatortegn før linjen som begynner med $(CC) ... .

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

Denne bruker to funksjoner sammenlignet med den originale Hello world-koden foreslått av Rubini & Corbet. For det første vil modulen ha samme versjon som kjerneversjonen. Dette oppnås ved å sette PARAM-variabelen i kompileringsskriptet. For det andre vil modulen nå bli lisensiert under GPL (ved å bruke MODULE_LICENSE()-makroen). Hvis dette ikke er gjort, kan du se noe sånt som følgende advarsel når du installerer modulen i kjernen:

# insmod hello_knz.o Advarsel: lasting av hello_knz.o vil skjemme kjernen: ingen lisens Se http://www.tux.org/lkml/#export-tainted for informasjon om tainted moduler Modul hello_knz lastet, med advarsler

La oss nå forklare modulkompileringsalternativene (makrodefinisjoner vil bli forklart senere):

-Med- med dette alternativet vil gcc-kompilatoren stoppe filkompileringsprosessen umiddelbart etter å ha opprettet objektfilen, uten å forsøke å lage en kjørbar binærfil.

-Vegg- maksimalt advarselsnivå når gcc kjører.

-D— definisjoner av makrosymboler. Samme som #define-direktivet i den kompilerte filen. Det spiller ingen rolle hvordan du definerer makrosymbolene som brukes i denne modulen, ved å bruke #define i kildefilen eller bruke -D-alternativet for kompilatoren.

-JEG- Ytterligere søkestier for inkluderende filer. Legg merke til bruken av "uname -r"-erstatningen, som vil bestemme det nøyaktige navnet på kjerneversjonen som er i bruk.

Den neste delen gir et annet eksempel på en modul. Den forklarer også i detalj hvordan du installerer den og laster den ut fra kjernen.

Original Hello world!

La oss nå se på den originale koden for den enkle "Hello, World"-modulen som tilbys av Rubini & Corbet. Denne koden kan kompileres under kjerneversjoner 2.0 til 2.4. Dette eksemplet, og alle andre presentert i boken, er tilgjengelig på O'Reilly FTP-nettstedet (se kapittel 1).

//file hello.c #define MODULE #include int init_module(void) ( printk("<1>Hei, verden\n"); return 0; ) void cleanup_module(void) ( printk("<1>Farvel grusom verden\n"); )

Funksjon printk() definert i Linux-kjernen og fungerer som en standard bibliotekfunksjon printf() på C-språk. Kjernen trenger sin egen, fortrinnsvis liten, inferensfunksjon, inneholdt direkte i kjernen, og ikke i biblioteker på brukernivå. En modul kan kalle en funksjon printk() fordi etter å ha lastet modulen ved hjelp av kommandoen insmod Modulen kommuniserer med kjernen og har tilgang til publiserte (eksporterte) kjernefunksjoner og variabler.

String parameter "<1>” sendt til printk()-funksjonen er prioritet for meldingen. De originale engelske kildene bruker begrepet loglevel, som betyr nivået på meldingslogging. Her vil vi bruke begrepet prioritet i stedet for det opprinnelige "loggnivået". I dette eksemplet bruker vi høy prioritet for meldingen som har et lavt nummer. Den høye meldingsprioriteten er satt med vilje fordi en melding med standardprioritet kanskje ikke vises i konsollen som modulen ble installert fra. Utgangsretningen til kjernemeldinger med standardprioritet avhenger av versjonen av kjernen som kjører, versjonen av daemonen klogd, og konfigurasjonen din. Mer detaljert, arbeider med funksjonen printk() vi vil forklare i kapittel 4, Feilsøkingsteknikker.

Du kan teste modulen ved å bruke kommandoen insmod for å installere modulen i kjernen og kommandoer rmmod for å fjerne en modul fra kjernen. Nedenfor vil vi vise hvordan dette kan gjøres. I dette tilfellet kjøres inngangspunktet init_module() når en modul er installert i kjernen, og cleanup_module() kjøres når den fjernes fra kjernen. Husk at bare en privilegert bruker kan laste og losse moduler.

Moduleksemplet ovenfor kan bare brukes med en kjerne som ble bygget med flagget "modulversjonsstøtte" slått av. Dessverre bruker de fleste distribusjoner versjonskontrollerte kjerner (dette er diskutert i delen "Versjonskontroll i moduler" i kapittel 11, "kmod og avansert modularisering"). Og selv om eldre versjoner av pakken moduler tillate at slike moduler lastes inn i versjonskontrollerte kjerner, noe som ikke lenger er mulig. Husk at modutils-pakken inneholder et sett med programmer som inkluderer programmene insmod og rmmod.

Oppgave: Bestem versjonsnummeret og sammensetningen av modulpakken fra distribusjonen din.

Når du prøver å sette inn en slik modul i en kjerne som støtter versjonskontroll, kan du se en feilmelding som ligner på følgende:

# insmod hello.o hello.o: Kjernemodulversjon samsvarer ikke hello.o ble kompilert for kjerneversjon 2.4.20 mens denne kjernen er versjon 2.4.20-9asp.

I katalogen diverse moduler eksempler fra ftp.oreilly.com finner du det originale eksempelprogrammet hello.c, som inneholder litt flere linjer, og kan installeres i både versjonskontrollerte og ikke-versjonsstyrte kjerner. Vi anbefaler imidlertid sterkt at du bygger din egen kjerne uten støtte for versjonskontroll. Samtidig anbefales det å ta de originale kjernekildene på nettstedet www.kernel.org

Hvis du er ny på å sette sammen kjerner, prøv å lese artikkelen som Alessandro Rubini (en av forfatterne av den originale boken) la ut på http://www.linux.it/kerneldocs/kconf, som burde hjelpe deg med å mestre prosessen.

Kjør følgende kommandoer i en tekstkonsoll for å kompilere og teste den originale eksempelmodulen ovenfor.

Root# gcc -c hello.c root# insmod ./hello.o Hei, verdensrot# rmmod hello Farvel grusom verdensrot#

Avhengig av mekanismen systemet bruker for å sende meldingsstrenger, utdataretningen til meldinger sendt av funksjonen printk(), kan variere. I det gitte eksemplet på kompilering og testing av en modul, ble meldinger sendt fra printk()-funksjonen sendt ut til den samme konsollen som kommandoene for å installere og kjøre modulene ble gitt fra. Dette eksemplet er hentet fra en tekstkonsoll. Hvis du utfører kommandoene insmod Og rmmod fra under programmet xterm, da vil du mest sannsynlig ikke se noe på terminalen din. I stedet kan meldingen havne i en av systemloggene, for eksempel i /var/log/meldinger. Det nøyaktige navnet på filen avhenger av distribusjonen. Se på tidspunktet for endringer i loggfiler. Mekanismen som brukes til å sende meldinger fra printk()-funksjonen er beskrevet i delen "Hvordan meldinger blir logget" i kapittel 4 "Teknikker"
feilsøking".

For å se modulmeldinger i systemloggfilen /val/log/messages er det praktisk å bruke systemverktøy tail, som som standard viser de siste 10 linjene i filen som er sendt til den. Et interessant alternativ for dette verktøyet er -f-alternativet, som kjører verktøyet i modusen for å overvåke de siste linjene i filen, dvs. Når nye linjer vises i filen, vil de automatisk bli skrevet ut. For å stoppe utførelsen av kommandoen i dette tilfellet, må du trykke Ctrl+C. For å se de siste ti linjene i systemloggfilen, skriv inn følgende på kommandolinjen:

Rot# hale /var/log/messages

Som du kan se, er det ikke så vanskelig å skrive en modul som det kan virke. Den vanskeligste delen er å forstå hvordan enheten din fungerer og hvordan du kan forbedre ytelsen til modulen. Når vi fortsetter dette kapittelet, vil vi lære mer om å skrive enkle moduler, og overlate enhetsspesifikasjonene til senere kapitler.

Forskjeller mellom kjernemoduler og applikasjoner

Applikasjonen har ett inngangspunkt, som begynner å kjøre umiddelbart etter plassering kjører applikasjonen i datamaskinens RAM. Dette inngangspunktet er beskrevet i C som hovedfunksjonen (). Å avslutte main()-funksjonen betyr å avslutte applikasjonen. Modulen har flere inngangspunkter som kjøres ved installasjon og fjerning av modulen fra kjernen, samt ved behandling av forespørsler fra brukeren. Dermed blir inngangspunktet init_module() utført når modulen lastes inn i kjernen. Cleanup_module()-funksjonen utføres når en modul er avlastet. I fremtiden vil vi bli kjent med andre inngangspunkter til modulen, som utføres når man utfører ulike forespørsler til modulen.

Muligheten til å laste og losse moduler er to pilarer i modulariseringsmekanismen. De kan vurderes på forskjellige måter. For utbygger betyr dette for det første en reduksjon i utviklingstiden, pga du kan teste driverfunksjonaliteten uten en langvarig omstartsprosess.

Som programmerer vet du at en applikasjon kan kalle en funksjon som ikke ble deklarert i applikasjonen. Ved stadier av statisk eller dynamisk kobling bestemmes adressene til slike funksjoner fra de tilsvarende bibliotekene. Funksjon printf() en av disse kallbare funksjonene som er definert i biblioteket libc. En modul på den annen side er bare assosiert med kjernen og kan kun kalle opp funksjoner som eksporteres av kjernen. Kode som kjøres i kjernen kan ikke bruke eksterne biblioteker. Så for eksempel funksjonen printk(), som ble brukt i eksemplet Hei C, er en analog av den velkjente funksjonen printf(), tilgjengelig i applikasjoner på brukernivå. Funksjon printk() plassert i kjernen og bør være så liten som mulig. Derfor, i motsetning til printf(), har den svært begrenset støtte for datatyper, og støtter for eksempel ikke flyttall i det hele tatt.

2.0 og 2.2 kjerneimplementeringer støttet ikke typespesifikasjoner L Og Z. De ble introdusert bare i kjerneversjon 2.4.

Figur 2-1 viser implementeringen av mekanismen for å kalle opp funksjoner som er inngangspunkter til modulen. Denne figuren viser også mekanismen for interaksjon av en installert eller installert modul med kjernen.

Ris. 2-1. Kommunikasjon mellom modulen og kjernen

En av funksjonene til Unix/Linux-operativsystemer er mangelen på biblioteker som kan kobles til kjernemoduler. Som du allerede vet, er moduler, når de er lastet, koblet inn i kjernen, så alle funksjoner utenfor modulen må deklareres i kjerneoverskriftsfilene og finnes i kjernen. Modulkilder aldri bør ikke inkludere vanlige topptekstfiler fra brukerromsbiblioteker. I kjernemoduler kan du bare bruke funksjoner som faktisk er en del av kjernen.

Hele kjernegrensesnittet er beskrevet i overskriftsfiler som ligger i katalogene include/linux Og inkludere/asm inne i kjernekildene (vanligvis plassert i /usr/src/linux-x.y.z(x.y.z er din kjerneversjon)). Eldre distribusjoner (basert på libc versjon 5 eller mindre) brukte symbolske lenker /usr/include/linux Og /usr/include/asm til de tilsvarende katalogene i kjernekildene. Disse symbolske lenkene gjør det mulig, om nødvendig, å bruke kjernegrensesnitt i brukerapplikasjoner.

Selv om grensesnittet til brukerromsbiblioteker nå er atskilt fra kjernegrensesnittet, må noen ganger brukerprosesser bruke kjernegrensesnitt. Imidlertid refererer mange referanser i kjerneoverskriftsfiler kun til selve kjernen og skal ikke være tilgjengelige for brukerapplikasjoner. Derfor er disse annonsene beskyttet #ifdef __KERNEL__ blokker. Dette er grunnen til at driveren din, som annen kjernekode, må kompileres med en makro deklarert __KERNEL__.

Rollen til individuelle kjernehodefiler vil bli diskutert etter behov gjennom hele boken.

Utviklere som jobber med store programvareprosjekter (som kjernen) bør være oppmerksomme på og unngå "navneområdeforurensning". Dette problemet oppstår når det er et stort antall funksjoner og globale variabler hvis navn ikke er uttrykksfulle nok (kan skilles). Programmereren som senere må forholde seg til slike applikasjoner, er tvunget til å bruke mye mer tid på å huske «reserverte» navn og komme opp med unike navn for nye elementer. Navnekollisjoner (tvetydigheter) kan skape et bredt spekter av problemer, alt fra feil ved lasting av en modul til ustabil eller uforklarlig programatferd som kan oppstå for brukere som bruker en kjerne innebygd i en annen konfigurasjon.

Utviklere har ikke råd til slike feil når de skriver kjernekode, fordi selv den minste modulen vil være knyttet til hele kjernen. Den beste løsningen for å unngå navnekollisjoner er først å erklære programobjektene dine som statisk, og for det andre bruken av et unikt prefiks i systemet for å navngi globale objekter. I tillegg kan du som modulutvikler kontrollere omfanget av objekter i koden din, som beskrevet senere i delen "Kernel Link Table".

De fleste (men ikke alle) versjoner av kommandoen insmod eksporter alle modulobjekter som ikke er deklarert som statisk, som standard, dvs. med mindre modulen definerer spesielle instruksjoner for dette formålet. Derfor er det ganske rimelig å erklære modulobjekter du ikke har tenkt å eksportere som statisk.

Å bruke et unikt prefiks for lokale objekter i en modul kan være en god praksis da det gjør feilsøking enklere. Mens du tester driveren din, må du kanskje eksportere flere objekter til kjernen. Ved å bruke et unikt prefiks for å angi navn, risikerer du ikke å introdusere kollisjoner i kjernenavneområdet. Prefikser brukt i kjernen er, etter konvensjon, små bokstaver, og vi vil holde oss til den konvensjonen.

En annen betydelig forskjell mellom kjerne- og brukerprosesser er feilhåndteringsmekanismen. Kjernen styrer utførelsen av brukerprosessen, så en feil i brukerprosessen resulterer i en melding som er ufarlig for systemet: segmenteringsfeil. Samtidig kan en debugger alltid brukes til å spore feil i kildekoden til brukerapplikasjonen. Feil som oppstår i kjernen er fatale - om ikke for hele systemet, så i det minste for den nåværende prosessen. I avsnittet "Feilsøkingssystemfeil" i kapittel 4, "Feilsøkingsteknikker", skal vi se på måter å spore kjernefeil.

Brukerplass og kjerneplass

Modulen kjører i den såkalte kjerneplass, mens applikasjoner kjører i . Dette konseptet er grunnlaget for operativsystemteori.

Et av hovedformålene med operativsystemet er å gi bruker- og brukerprogrammer dataressurser, hvorav de fleste er representert av eksterne enheter. Operativsystemet skal ikke bare gi tilgang til ressurser, men også kontrollere tildelingen og bruken av dem, forhindre kollisjoner og uautorisert tilgang. I tillegg til denne, operativsystem kan lage uavhengige operasjoner for programmer og beskytte mot uautorisert tilgang til ressurser. Å løse dette ikke-trivielle problemet er bare mulig hvis prosessoren beskytter systemprogrammer fra brukerapplikasjoner.

Nesten hver moderne prosessor er i stand til å gi slik separasjon ved å implementere forskjellige nivåer av privilegier for den utførende koden (minst to nivåer kreves). For eksempel har I32-arkitekturprosessorer fire rettighetsnivåer fra 0 til 3. Dessuten har nivå 0 de høyeste rettighetene. For slike prosessorer er det en klasse med privilegerte instruksjoner som bare kan utføres på privilegerte nivåer. Unix-systemer bruker to nivåer av prosessorprivilegier. Hvis en prosessor har mer enn to rettighetsnivåer, brukes det laveste og høyeste. Unix-kjernen kjører på høyeste nivå privilegier, som sikrer kontroll over brukerens utstyr og prosesser.

Når vi snakker om kjerneplass Og brukerens prosessrom Dette betyr ikke bare forskjellige nivåer av privilegier for den kjørbare koden, men også forskjellige adresseområder.

Unix overfører kjøring fra brukerens prosessrom til kjerneplass i to tilfeller. For det første når en brukerapplikasjon ringer til kjernen (systemanrop), og for det andre, mens service på maskinvare avbrytes. Kjernekode som kjøres under et systemanrop, kjøres i konteksten av en prosess, dvs. arbeider på vegne av anropsprosessen, den har tilgang til prosessens adresseromsdata. På den annen side er koden som kjøres ved service på et maskinvareavbrudd asynkron med hensyn til prosessen, og tilhører ikke noen spesiell prosess.

Hensikten med moduler er å utvide funksjonaliteten til kjernen. Modulkoden kjøres i kjerneplass. Vanligvis utfører en modul begge oppgavene nevnt tidligere: noen modulfunksjoner utføres som en del av systemanrop, og noen er ansvarlige for å administrere avbrudd.

Parallellisering i kjernen

Når du programmerer enhetsdrivere, i motsetning til programmeringsapplikasjoner, er problemet med parallellisering av kjørbar kode spesielt akutt. Vanligvis kjører en applikasjon sekvensielt fra start til slutt uten å bekymre deg for endringer i miljøet. Kjernekoden må fungere med den forståelse at den kan nås flere ganger samtidig.

Det er mange grunner til å parallellisere kjernekode. Linux har vanligvis mange prosesser som kjører, og noen av dem kan prøve å få tilgang til modulkoden din samtidig. Mange enheter kan forårsake maskinvareavbrudd på prosessoren. Avbruddsbehandlere kalles asynkront og kan kalles mens sjåføren utfører en annen forespørsel. Noen programvareabstraksjoner (som kjernetidtakere, forklart i kapittel 6, "Flow of Time") kjører også asynkront. I tillegg kan Linux kjøres på et system med symmetriske multiprosessorer (SMP), noe som betyr at driverkoden din kan kjøres parallelt på flere prosessorer samtidig.

Av disse grunner må Linux-kjernekode, inkludert driverkode, være reentrant, dvs. må kunne jobbe med mer enn én datakontekst samtidig. Datastrukturer må utformes for å imøtekomme parallell utførelse av flere tråder. På sin side må kjernekoden kunne håndtere flere parallelle datastrømmer uten å skade dem. Å skrive kode som kan kjøres parallelt og unngå situasjoner der en annen utførelsessekvens ville føre til uønsket systematferd krever mye tid, og kanskje mye lureri. Hvert drivereksempel i denne boken er skrevet med tanke på parallell utførelse. Om nødvendig vil vi forklare detaljene ved teknikken for å skrive slik kode.

Mest generell feil Problemet som programmerere lager er at de antar at samtidighet ikke er et problem fordi noen kodesegmenter ikke kan gå i dvale. Faktisk er Linux-kjernen ikke-søkt, med det viktige unntaket av avbruddsbehandlere, som ikke kan skaffe CPU mens kritisk kjernekode kjøres. I det siste har manglende sidebarhet vært tilstrekkelig til å forhindre uønsket parallellisering i de fleste tilfeller. På SMP-systemer er imidlertid ikke kodenedlasting nødvendig på grunn av parallell beregning.

Hvis koden din antar at den ikke vil bli lastet ut, vil den ikke fungere riktig på SMP-systemer. Selv om du ikke har et slikt system, kan noen andre som bruker koden din ha et. Det er også mulig at kjernen i fremtiden vil bruke sidebarhet, så selv enkeltprosessorsystemer vil måtte håndtere samtidighet hele veien. Det finnes allerede alternativer for å implementere slike kjerner. Dermed vil en fornuftig programmerer skrive kjernekode med antagelsen om at den vil kjøre på et system som kjører SMP.

Merk oversetter: Beklager, men de to siste avsnittene er ikke klare for meg. Dette kan være et resultat av en feiloversettelse. Derfor presenterer jeg originalteksten.

En vanlig feil gjort av driverprogrammerere er å anta at samtidighet ikke er et problem så lenge et bestemt kodesegment
gjør ikke gå i dvale (eller "blokkere"). Det er sant at Linux-kjernen ikke er forebyggende; med det viktige unntaket
serviceavbrudd, vil det ikke ta prosessoren bort fra kjernekode som ikke gir etter. I tidligere tider, dette ikke-preemptive
oppførsel var nok til å forhindre uønsket samtidighet mesteparten av tiden. På SMP-systemer er det imidlertid ikke nødvendig med forkjøp for å forårsake
samtidig utførelse.

Hvis koden din antar at den ikke vil bli forhåndsaktivert, vil den ikke kjøre ordentlig på SMP-systemer. Selv om du ikke har et slikt system,
andre som kjører koden din kan ha en. I fremtiden er det også mulig at kjernen vil gå over til en forebyggende driftsmodus,
da vil selv enprosessorsystemer måtte håndtere samtidighet overalt (noen varianter av kjernen er allerede implementert
den).

Informasjon om gjeldende prosess

Selv om kjernemodulkoden ikke kjøres sekvensielt som applikasjoner, blir de fleste kall til kjernen utført i forhold til prosessen som kaller den. Kjernekode kan identifisere prosessen som kalte den ved å få tilgang til en global peker som peker til strukturen struct task_struct, definert for kjerner versjon 2.4, i filen inkludert i . Peker nåværende indikerer brukerprosessen som kjører for øyeblikket. Ved utføring av systemanrop som f.eks åpen() eller Lukk(), må det være en prosess som forårsaket dem. Kjernekode kan om nødvendig hente frem spesifikk informasjon om anropsprosessen via en peker nåværende. For eksempler på bruk av denne pekeren, se avsnittet "Device File Access Control" i kapittel 5, "Enhanced Char Driver Operations".

I dag er indeksen nåværende er ikke lenger en global variabel, som i tidligere versjoner av kjernen. Utviklerne optimaliserte tilgangen til strukturen som beskriver den nåværende prosessen ved å flytte den til stabelsiden. Du kan se på gjeldende implementeringsdetaljer i filen . Koden du ser der virker kanskje ikke enkel for deg. Husk at Linux er et SMP-sentrisk system, og en global variabel vil rett og slett ikke fungere når du har å gjøre med flere CPUer. Implementeringsdetaljer forblir skjult for andre kjernedelsystemer, og enhetsdriveren kan få tilgang til pekeren nåværende kun gjennom grensesnittet .

Fra et modulsynspunkt, nåværende ser ut som en ekstern lenke printk(). Modulen kan bruke nåværende der det trengs. Følgende kodestykke skriver for eksempel ut prosess-ID (PID) og kommandonavnet til prosessen som kalte modulen, og henter dem gjennom de tilsvarende feltene i strukturen struct task_struct:

Printk("Prosessen er \"%s\" (pid %i)\n", current->comm, current->pid);

Current->comm-feltet er navnet på kommandofilen som skapte den gjeldende prosessen.

Kompilere og laste inn moduler

Resten av dette kapittelet er viet til å skrive en komplett, om enn atypisk, modul. De. Modulen tilhører ikke noen av klassene beskrevet i avsnittet "Enhets- og modulklasser" i kapittel 1, "Introduksjon til enhetsdrivere." Eksempeldriveren vist i dette kapittelet vil bli kalt skull (Simple Kernel Utility for Loading Localities). Du kan bruke scullmodulen som en mal for å skrive din egen lokale kode.

Vi bruker konseptet "lokal kode" (lokal) for å understreke dine personlige kodeendringer, i den gode gamle Unix-tradisjonen (/usr/local).

Før vi fyller ut funksjonene init_module() og cleanup_module(), vil vi imidlertid skrive et Makefile-skript som make vil bruke til å bygge modulens objektkode.

Før forprosessoren kan behandle inkluderingen av noen overskriftsfil, må makrosymbolet __KERNEL__ defineres med et #define-direktiv. Som nevnt tidligere kan en kjernespesifikk kontekst defineres i kjernegrensesnittfiler, kun synlig hvis __KERNEL__-symbolet er forhåndsdefinert i forhåndsbehandlingen.

Et annet viktig symbol definert av #define-direktivet er MODUL-symbolet. Må defineres før du aktiverer grensesnittet (unntatt de driverne som vil bli kompilert med kjernen). Drivere satt sammen i kjernen vil ikke bli beskrevet i denne boken, så MODUL-symbolet vil være til stede i alle eksemplene våre.

Hvis du bygger en modul for et system med SMP, må du også definere makrosymbolet __SMP__ før du aktiverer kjernegrensesnittene. I kjerneversjon 2.2 introduserte et eget element i kjernekonfigurasjonen et valg mellom en enkeltprosessor og et multiprosessorsystem. Derfor vil det å inkludere følgende linjer som de aller første linjene i modulen resultere i multiprosessorstøtte.

#inkludere #ifdef CONFIG_SMP # definer __SMP__ #endif

Modulutviklere bør også definere -O-optimaliseringsflagget for kompilatoren fordi mange funksjoner er erklært innebygd i kjerneoverskriftsfilene. Gcc-kompilatoren utfører ikke innebygd utvidelse av funksjoner med mindre optimalisering er aktivert. Å tillate at innebygde erstatninger utvides ved å bruke -g og -O alternativene vil tillate deg å senere feilsøke kode som bruker innebygde funksjoner i debuggeren. Siden kjernen bruker utstrakt bruk av innebygde funksjoner, er det svært viktig at de utvides riktig.

Vær imidlertid oppmerksom på at bruk av optimalisering over -O2-nivået er risikabelt fordi kompilatoren kan utvide funksjoner som ikke er deklarert inline. Dette kan føre til problemer fordi... Noen funksjonskoder forventer å finne standardstabelen til kallet. En innebygd utvidelse forstås som å sette inn en funksjonskode ved anropspunktet i stedet for den tilsvarende funksjonsanropsinstruksjonen. Følgelig, i dette tilfellet, siden det ikke er noe funksjonskall, er det ingen stabel av anropet.

Du må kanskje sørge for at du bruker samme kompilator for å kompilere moduler som ble brukt til å bygge kjernen som modulen skal installeres i. For detaljer, se originaldokumentet fra filen Dokumentasjon/Endringer ligger i kjernekildekatalogen. Kjerne- og kompilatorutvikling er vanligvis synkronisert på tvers av utviklingsteam. Det kan være tilfeller der oppdatering av ett av disse elementene avslører feil i et annet. Noen distribusjonsprodusenter leverer ultranye versjoner av kompilatoren som ikke samsvarer med kjernen de bruker. I dette tilfellet gir de vanligvis en egen pakke (ofte kalt kgcc) med en kompilator spesielt designet for
kjernekompilering.

Til slutt, for å forhindre ekle feil, foreslår vi at du bruker kompileringsalternativet -Vegg(alle advarsler - alle advarsler). For å tilfredsstille alle disse advarslene, må du kanskje endre din vanlige programmeringsstil. Når du skriver kjernekode, er det å foretrekke å bruke kodestilen foreslått av Linus Torvalds. Ja, dokument Dokumentasjon/Kodingstil, fra kjernekildekatalogen, er ganske interessant og anbefales til alle som er interessert i programmering på kjernenivå.

Det anbefales å plassere et sett med modulkompileringsflagg, som vi nylig ble kjent med, i en variabel CFLAGS din Makefile. For make-verktøyet er dette en spesiell variabel, hvis bruk vil fremgå av den følgende beskrivelsen.

I tillegg til flaggene i variabelen CFLAGS, kan det hende du trenger et mål i Makefilen som kombinerer forskjellige objektfiler. Et slikt mål er bare nødvendig når modulkoden er delt inn i flere kildefiler, noe som generelt ikke er uvanlig. Objektfiler kombineres med kommandoen ld -r, som ikke er en koblingsoperasjon i allment akseptert forstand, til tross for bruken av en linker( ld). Resultatet av kommandoutførelsen ld -r er en annen objektfil som kombinerer objektkodene til linkerinndatafilene. Alternativ -r midler " flyttbar - flyttbar", dvs. Vi flytter utdatafilen til kommandoen i adressefeltet, fordi den inneholder ennå ikke absolutte adresser til funksjonskall.

Følgende eksempel viser minimum Makefile som kreves for å kompilere en modul som består av to kildefiler. Hvis modulen din består av en enkelt kildefil, må du fra eksemplet som er gitt, fjerne målet som inneholder kommandoen ld -r.

# Banen til kjernekildekatalogen din kan endres her, # eller du kan sende den som en parameter når du kaller "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 *~ kjerne

Hvis du er ny på hvordan make fungerer, kan du bli overrasket over at det ikke er noen regler for å kompilere *.c-filer til *.o-objektfiler. Å definere slike regler er ikke nødvendig, fordi make-verktøyet, om nødvendig, konverterer selv *.c-filer til *.o-filer ved å bruke standardkompilatoren eller kompilatoren spesifisert av en variabel $(CC). I dette tilfellet innholdet i variabelen $(CFLAGS) brukes til å spesifisere kompilasjonsflagg.

Det neste trinnet etter å ha bygget en modul er å laste den inn i kjernen. Vi har allerede sagt at for dette vil vi bruke insmod-verktøyet, som assosierer alle udefinerte symboler (funksjonskall, etc.) til modulen med symboltabellen til den kjørende kjernen. Men i motsetning til en linker (for eksempel som ld), endrer den ikke moduldiskfilen, men laster inn modulobjektet som er koblet til kjernen til RAM. Insmod-verktøyet kan godta noen kommandolinjealternativer. Detaljer kan sees via mann insmod. Ved å bruke disse alternativene kan du for eksempel tilordne spesifikke heltalls- og strengvariabler i modulen din til spesifiserte verdier før du kobler modulen til kjernen. Derfor, hvis modulen er utformet riktig, kan den konfigureres ved oppstart. Denne metoden for å konfigurere en modul gir brukeren større fleksibilitet enn konfigurasjon på kompileringstidspunktet. Konfigurasjon av oppstartstid er forklart i delen "Manuell og automatisk konfigurasjon" senere i dette kapittelet.

Noen lesere vil være interessert i detaljene om hvordan insmod-verktøyet fungerer. Implementeringen av insmod er basert på flere systemkall definert i kernel/module.c. Sys_create_module()-funksjonen tildeler den nødvendige mengden minne i kjerneadresserommet for å laste modulen. Dette minnet tildeles ved hjelp av vmalloc()-funksjonen (se delen "vmalloc og venner" i kapittel 7, "Få tak i minnet"). Systemkallet get_kernel_sysms() returnerer kjernesymboltabellen, som vil bli brukt til å bestemme de virkelige adressene til objekter ved kobling. Sys_init_module()-funksjonen kopierer modulobjektkoden inn i kjerneadresserommet og kaller opp modulens initialiseringsfunksjon.

Hvis du ser på kjernekodekildene, vil du finne systemkallnavn som begynner med sys_-prefikset. Dette prefikset brukes bare for systemanrop. Ingen andre funksjoner skal bruke den. Husk dette når du behandler kjernekodekilder med grep-søkeverktøyet.

Versjonsavhengigheter

Hvis du ikke vet noe mer enn det som er dekket her, må sannsynligvis modulene du oppretter rekompileres for hver versjon av kjernen de er koblet til. Hver modul må definere et symbol kalt __module_kernel_version, hvis verdi
sammenlignes med versjonen av gjeldende kjerne som bruker insmod-verktøyet. Dette symbolet er plassert i seksjonen .modinfo ELF (Executable and Linking Format) filer. Dette er forklart mer detaljert i kapittel 11 "kmod og avansert modularisering". Vær oppmerksom på at denne versjonskontrollmetoden kun gjelder for kjerneversjoner 2.2 og 2.4. I 2.0-kjernen gjøres dette på en litt annen måte.

Kompilatoren vil definere dette symbolet uansett hvor overskriftsfilen er inkludert . Derfor, i hello.c-eksemplet gitt tidligere, beskrev vi ikke dette symbolet. Dette betyr også at hvis modulen din består av mange kildefiler, må du inkludere filen inn i koden din bare én gang. Et unntak er tilfellet ved bruk av definisjonen __NO_VERSION__, som vi møter senere.

Nedenfor er definisjonen av det beskrevne symbolet fra filen module.h ekstrahert fra 2.4.25-kjernekoden.

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

Hvis en modul ikke klarer å laste på grunn av en versjonsfeil, kan du prøve å laste denne modulen ved å sende insmod-nøkkelen til parameterlinjen til verktøyet -f(makt). Denne metoden for å laste en modul er ikke sikker og er ikke alltid vellykket. Det er ganske vanskelig å forklare årsakene til mulige feil. Det er mulig at modulen ikke vil lastes fordi symbolene ikke kan løses under kobling. I dette tilfellet vil du motta en passende feilmelding. Årsakene til feil kan også ligge i endringer i driften eller strukturen til kjernen. I dette tilfellet kan lasting av modulen føre til alvorlige kjøretidsfeil, samt systempanikk. Sistnevnte bør tjene som et godt insentiv til å bruke et versjonskontrollsystem. Versjonsfeil kan håndteres mer elegant ved å bruke versjonskontroll i kjernen. Vi vil snakke om dette i detalj i delen "Versjonskontroll i moduler" i kapittel 11 "kmod og avansert modularisering".

Hvis du vil kompilere modulen din for en spesifikk kjerneversjon, må du inkludere headerfilene for den aktuelle kjerneversjonen. I Makefile-eksemplet beskrevet ovenfor, ble variabelen brukt til å bestemme katalogen for disse filene KERNELDIR. Slik tilpasset kompilering er ikke uvanlig når kjernekilder er tilgjengelige. Dessuten er det ikke uvanlig at det er forskjellige versjoner av kjernen i katalogtreet. Alle moduleksemplene i denne boken bruker variabelen KERNELDIR for å indikere plasseringen av kildekatalogen for versjonen av kjernen som den sammensatte modulen skal kobles til. Du kan bruke en systemvariabel for å spesifisere denne katalogen, eller du kan sende dens plassering gjennom kommandolinjealternativer for å lage.

Når du laster en modul, bruker insmod-verktøyet sine egne søkestier for modulens objektfiler, og ser gjennom versjonsavhengige kataloger som starter kl. /lib/moduler. Og selv om eldre versjoner av verktøyet inkluderte gjeldende katalog i søkebanen, anses denne oppførselen nå som uakseptabel av sikkerhetsgrunner (de samme problemene som å bruke systemvariabelen STI). Så hvis du ønsker å laste en modul fra gjeldende katalog, kan du spesifisere den i stilen ./modul.o. Denne indikasjonen av modulposisjonen vil fungere for alle versjoner av insmod-verktøyet.

Noen ganger kan du støte på kjernegrensesnitt som er forskjellige mellom 2.0.x og 2.4.x. I dette tilfellet må du ty til en makro som bestemmer gjeldende kjerneversjon. Denne makroen er plassert i overskriftsfilen . Vi vil indikere tilfeller av forskjeller i grensesnitt når du bruker dem. Dette kan gjøres enten umiddelbart langs beskrivelsen, eller på slutten av seksjonen, i en spesiell seksjon dedikert til versjonsavhengigheter. I noen tilfeller vil det å plassere detaljene i en egen seksjon tillate deg å unngå å komplisere beskrivelsen av kjerneversjon 2.4.x som er relevant for denne boken.

I overskriftsfilen linux/versjon.h Følgende makroer er definert relatert til å bestemme kjerneversjonen.

UTS_RELEASE Makro som utvides til en streng som beskriver gjeldende kjerneversjon
kildetre. For eksempel kan en makro utvides til noe som dette:
linje: "2.3.48" . LINUX_VERSION_CODE Denne makroen utvides til en binær representasjon av kjerneversjonen, ved
én byte for hver del av tallet. For eksempel binær
representasjon for versjon 2.3.48 vil være 131888 (desimal
representasjon for hex 0x020330). Muligens binær
Du vil finne representasjonen mer praktisk enn strengrepresentasjonen. Legg merke til hva som er
representasjon lar deg ikke beskrive mer enn 256 alternativer i hver
deler av nummeret. KERNEL_VERSION(dur, moll, utgivelse) Denne makrodefinisjonen lar deg bygge "kernel_version_code"
fra de individuelle elementene som utgjør kjerneversjonen. For eksempel,
neste makro KERNEL_VERSION(2; 3; 48)
vil utvide til 131888. Denne makrodefinisjonen er veldig praktisk når
sammenligne gjeldende kjerneversjon med den nødvendige. Vi vil bli gjentatte ganger
bruk denne makrodefinisjonen gjennom hele boken.

Her er innholdet i filen: linux/versjon.h for kjerne 2.4.25 (teksten til overskriftsfilen er gitt 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))

Versjon.h-headerfilen er inkludert i module.h-filen, så du trenger vanligvis ikke å inkludere versjon.h eksplisitt i modulkoden din. På den annen side kan du forhindre at version.h header-filen blir inkludert i module.h ved å deklarere en makro __NO_VERSION__. Du vil bruke __NO_VERSION__, for eksempel i tilfelle du må aktivere inn i flere kildefiler, som deretter vil bli koblet til én modul. Kunngjøring __NO_VERSION__ før inkludert module.h header-filen forhindrer
automatisk strengbeskrivelse __module_kernel_version eller tilsvarende i kildefiler. Du kan trenge dette for å tilfredsstille linkerens klager når ld -r, som ikke vil like flere beskrivelser av symboler i lenketabeller. Vanligvis hvis modulkoden er delt opp i flere kildefiler, inkludert en overskriftsfil , deretter kunngjøringen __NO_VERSION__ gjøres i alle disse filene unntatt én. På slutten av boken er det et eksempel på en modul som bruker __NO_VERSION__.

De fleste kjerneversjonsavhengigheter kan håndteres ved hjelp av logikk bygget på preprosessordirektiver ved bruk av makrodefinisjoner KERNEL_VERSJON Og LINUX_VERSION_CODE. Men å sjekke versjonsavhengigheter kan komplisere lesbarheten til modulkoden i stor grad på grunn av heterogene direktiver #ifdef. Derfor er kanskje den beste løsningen å plassere avhengighetssjekken i en egen overskriftsfil. Dette er grunnen til at vårt eksempel inkluderer en header-fil sysdep.h, brukes til å inneholde alle makrodefinisjoner knyttet til versjonsavhengighetssjekker.

Den første versjonsavhengigheten vi ønsker å representere er i målerklæringen" gjøre installer" vårt driverkompileringsskript. Som du kanskje forventer, velges installasjonsmappen, som endres i henhold til kjerneversjonen som brukes, basert på visning av version.h-filen. Her er en kodebit fra filen Regler.lag, som brukes av alle kjerne Makefiles.

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

Merk at vi bruker misc-katalogen (INSTALLDIR-erklæringen i eksempelet Makefile ovenfor) for å installere alle våre drivere. Fra og med kjerneversjon 2.4 er denne katalogen den anbefalte katalogen for å plassere egendefinerte drivere. I tillegg inneholder både gamle og nye versjoner av modutils-pakken en diverse katalog i søkebanene deres.

Ved å bruke INSTALLDIR-definisjonen ovenfor, kan installasjonsmålet i Makefilen se slik ut:

Installer: installer -d $(INSTALLDIR) installer -c $(OBJS) $(INSTALLDIR)

Plattformavhengighet

Hver datamaskinplattform har sine egne egenskaper som må tas i betraktning av kjerneutviklere for å oppnå den høyeste ytelsen.

Kjerneutviklere har mye mer frihet i valg og beslutninger enn applikasjonsutviklere. Det er denne friheten som lar deg optimalisere koden din og få mest mulig ut av hver spesifikke plattform.

Modulkoden må kompileres ved å bruke de samme kompileringsalternativene som ble brukt til å kompilere kjernen. Dette gjelder både å bruke samme prosessorregisterbruksmønster og utføre samme optimaliseringsnivå. Fil Regler.lag, som ligger ved roten av kjernekildetreet, inkluderer plattformspesifikke definisjoner som må inkluderes i alle kompileringsmakefiler. Alle plattformspesifikke kompileringsskript kalles Makefiles. plattform og inneholder verdiene til variabler for make-verktøyet i henhold til gjeldende kjernekonfigurasjon.

En annen interessant funksjon ved Makefile er støtten for kryssplattform eller rett og slett krysskompilering. Dette begrepet brukes når du trenger å kompilere kode for en annen plattform. For eksempel, ved å bruke i86-plattformen skal du lage kode for M68000-plattformen. Hvis du skal krysskompilere, må du erstatte kompileringsverktøyene ( gcc, ld, etc.) med et annet sett med tilsvarende verktøy
(For eksempel, m68k-linux-gcc, m68k-linux-ld). Prefikset som brukes kan spesifiseres enten av $(CROSS_COMPILE) Makefile-variabelen, ved et kommandolinjealternativ til make-verktøyet, eller av en systemmiljøvariabel.

SPARC-arkitekturen er en spesiell sak som må håndteres deretter i Makefilen. Brukerprogrammer som kjøres på SPARC64 (SPARC V9)-plattformen er binære filer, vanligvis designet for SPARC32 (SPARC V8)-plattformen. Derfor genererer standardkompilatoren på SPARC64-plattformen (gcc) objektkode for SPARC32. På den annen side må en kjerne designet for å kjøre på SPARC V9 inneholde objektkode for SPARC V9, så selv da kreves det en krysskompilator. Alle GNU/Linux-distribusjoner designet for SPARC64 inkluderer en passende krysskompilator, som må velges i Makefile for kjernekompileringsskriptet.

Og selv om hele listen over versjons- og plattformavhengigheter er litt mer kompleks enn beskrevet her, er det nok til å utføre krysskompilering. For mer informasjon kan du se på Makefile-kompileringsskriptene og kjernekildefilene.

Funksjoner i kjernen 2.6

Tiden står ikke stille. Og nå er vi vitne til fremveksten av en ny generasjon kjerne 2.6. Dessverre dekker ikke originalen til denne boken den nye kjernen, så oversetteren tar seg friheten til å supplere oversettelsen med ny kunnskap.

Du kan bruke integrerte utviklingsmiljøer som TimeSys' TimeStorm, som vil generere skjelettet og kompileringsskriptet for modulen din, avhengig av den nødvendige kjerneversjonen. Hvis du skal skrive alt dette selv, vil du trenge litt tilleggsinformasjon om hovedforskjellene introdusert av den nye kjernen.

En av funksjonene til 2.6-kjernen er behovet for å bruke makroene module_init() og module_exit() for å eksplisitt registrere navnene på initialiserings- og avslutningsfunksjonene.

MODULE_LISENCE()-makroen, introdusert i 2.4-kjernen, er fortsatt nødvendig hvis du ikke vil se tilsvarende advarsler når du laster en modul. Du kan velge følgende lisensstrenger som skal overføres til makroen: "GPL", "GPL v2", "GPL og tilleggsrettigheter", "Dual BSD/GPL" (valg mellom BSD- eller GPL-lisenser), "Dual MPL/GPL " (valg mellom Mozilla- eller GPL-lisenser) og
"Proprietær".

Mer viktig for den nye kjernen er et nytt modulkompileringsskjema, som ikke bare innebærer endringer i koden til selve modulen, men også i Makefile-skriptet for kompileringen.

Dermed er definisjonen av MODUL-makrosymbolet ikke lenger nødvendig verken i modulkoden eller i Makefilen. Om nødvendig vil det nye kompileringsskjemaet selv bestemme dette makrosymbolet. Du trenger heller ikke å eksplisitt definere __KERNEL__ makrosymbolene, eller nyere som KBUILD_BASENAME og KBUILD_MODNAME.

Du bør heller ikke spesifisere optimaliseringsnivået ved kompilering (-O2 eller andre), fordi modulen din vil bli kompilert med hele settet med flagg, inkludert optimaliseringsflagg, som alle andre moduler i kjernen din er kompilert med - make-verktøyet bruker automatisk hele det nødvendige settet med flagg.

Av disse grunnene er Makefilen for å kompilere en modul for 2.6-kjernen mye enklere. Så for hello.c-modulen vil Makefile se slik ut:

Obj-m:= hallo.o

Men for å kompilere modulen, trenger du skrivetilgang til kjernekildetreet, hvor midlertidige filer og kataloger vil bli opprettet. Derfor bør kommandoen for å kompilere en modul for 2.6-kjernen, spesifisert fra gjeldende katalog som inneholder modulkildekoden, se slik ut:

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

Så vi har kilden til modulen hei-2.6.c, for kompilering i kjerne 2.6:

//hello-2.6.c #include #inkludere #inkludere MODULE_LICENSE("GPL"); static int __init my_init(void) ( printk("Hei verden\n"); return 0; ); static void __exit my_cleanup(void) ( printk("Farvel\n"); ); module_init(min_init); module_exit(min_opprydding);

Følgelig har vi en Makefile:

Obj-m:= hei-2.6.o

Vi kaller make-verktøyet for å behandle vår Makefile med følgende parametere:

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

Den normale kompileringsprosessen vil produsere følgende standardutgang:

Lag: Gå inn i katalogen `/usr/src/linux-2.6.3" *** Advarsel: Overstyring av SUBDIRS på kommandolinjen kan forårsake *** inkonsekvenser gjør: `arch/i386/kernel/asm-offsets.s" ikke krever oppdatering. CHK include/asm-i386/asm_offsets.h CC [M] /home/knz/j.kernel/3/hello-2.6.o Byggemoduler, trinn 2. /usr/src/linux-2.6.3/scripts/Makefile .modpost:17: *** Uh-oh, du har foreldede moduloppføringer. Du rotet med SUBDIRS, /usr/src/linux-2.6.3/scripts/Makefile.modpost:18: ikke klag hvis noe går galt. MODPOST CC /home/knz/j.kernel/3/hello-2.6.mod.o LD [M] /home/knz/j.kernel/3/hello-2.6.ko make: Avslutt katalogen `/usr/src / linux-2.6.3"

Det endelige resultatet av kompileringen vil være en modulfil hello-2.6.ko som kan installeres i kjernen.

Merk at i 2.6-kjernen er modulfiler suffikset med .ko i stedet for .o som i 2.4-kjernen.

Kjernesymboltabell

Vi har allerede snakket om hvordan insmod-verktøyet bruker den offentlige kjernesymboltabellen når du kobler en modul til kjernen. Denne tabellen inneholder adressene til globale kjerneobjekter - funksjoner og variabler - som kreves for å implementere modulære driveralternativer. Kjernens offentlige symboltabell kan leses i tekstform fra /proc/ksyms-filen, forutsatt at kjernen din støtter /proc-filsystemet.

I kjerne 2.6 ble /proc/ksyms omdøpt til /proc/modules.

Når en modul er lastet inn, blir symbolene eksportert av modulen en del av kjernesymboltabellen, og du kan se dem i /proc/ksyms.

Nye moduler kan bruke symboler eksportert av modulen din. For eksempel er msdos-modulen avhengig av tegn eksportert av fettmodulen, og hver USB-enhet som brukes i lesemodus bruker tegn fra usbcore- og inngangsmodulene. Dette forholdet, realisert ved sekvensiell lasting av moduler, kalles en modulstabel.

Modulstabelen er praktisk å bruke når du lager komplekse modulprosjekter. Denne abstraksjonen er nyttig for å skille enhetsdriverkode i maskinvareavhengige og maskinvareuavhengige deler. For eksempel består video-for-linux-driversettet av en kjernemodul som eksporterer symboler for en lavnivådriver som tar hensyn til spesifikasjonene til maskinvaren som brukes. I henhold til konfigurasjonen din laster du hovedvideomodulen og en modul som er spesifikk for maskinvaren din. På samme måte er støtte for parallellporter og en bred klasse av tilkoblede enheter, for eksempel USB-enheter, implementert. Parallellportsystemstabelen er vist i fig. 2-2. Pilene viser interaksjonen mellom moduler ogt. Interaksjon kan utføres både på funksjonsnivå og på nivå med datastrukturer som administreres av funksjoner.

Figur 2-2. Parallell portmodulstabel

Når du bruker stabelmoduler, er det praktisk å bruke modprobe-verktøyet. Funksjonaliteten til modprobe-verktøyet ligner på mange måter insmod-verktøyet, men når du laster en modul, sjekker det dens underliggende avhengigheter, og om nødvendig laster de nødvendige moduler til den nødvendige modulstakken er fylt. Dermed kan en modprobe-kommando resultere i flere kall til insmod-kommandoen. Du kan si at modprobe-kommandoen er en intelligent innpakning rundt insmod. Du kan bruke modprobe i stedet for insmod overalt, bortsett fra når du laster inn dine egne moduler fra gjeldende katalog, fordi modprobe ser kun på spesifikke modulkataloger, og vil ikke være i stand til å tilfredsstille mulige avhengigheter.

Å dele moduler i deler bidrar til å redusere utviklingstiden ved å forenkle problemdefinisjonen. Dette ligner på skillet mellom implementeringsmekanisme og kontrollpolicy, som er diskutert i kapittel 1, "Introduksjon til enhetsdrivere."

Vanligvis implementerer en modul funksjonaliteten uten å måtte eksportere symboler i det hele tatt. Du må eksportere symboler hvis andre moduler kan dra nytte av det. Du må kanskje inkludere et spesielt direktiv for å forhindre eksport av ikke-statiske tegn, fordi De fleste implementeringer av moduler eksporterer alle som standard.

Linux-kjerneoverskriftsfilene tilbyr en praktisk måte å kontrollere synligheten til symbolene dine, og forhindrer dermed at navneområdet til kjernesymboltabellen blir forurenset. Mekanismen beskrevet i dette kapittelet fungerer i kjerner fra og med versjon 2.1.18. Kernel 2.0 hadde en helt annen kontrollmekanisme
symbolsynlighet, som vil bli beskrevet på slutten av kapittelet.

Hvis modulen din ikke trenger å eksportere symboler i det hele tatt, kan du eksplisitt plassere følgende makrokall i modulens kildefil:

EXPORT_NO_SYMBOLS;

Dette makrokallet, definert i linux/module.h-filen, utvides til et assembler-direktiv og kan spesifiseres hvor som helst i modulen. Men når du lager kode som er portabel til forskjellige kjerner, er det nødvendig å plassere dette makrokallet i modulens initialiseringsfunksjon (init_module), fordi versjonen av denne makroen vi definerte i vår sysdep.h-fil for eldre kjerneversjoner vil kun fungere her.

På den annen side, hvis du trenger å eksportere noen av symbolene fra modulen din, må du bruke et makrosymbol
EXPORT_SYMTAB. Dette makrosymbolet må defineres før ved å inkludere module.h header-filen. Det er vanlig praksis
definere dette makrotegnet via et flagg -D i Makefile.

Hvis makrosymbolet EXPORT_SYMTAB definert, kan individuelle symboler eksporteres ved hjelp av et par makroer:

EXPORT_SYMBOL(navn); EXPORT_SYMBOL_NOVERS(navn);

Hver av disse to makroene vil gjøre det gitte symbolet tilgjengelig utenfor modulen. Forskjellen er at makroen EXPORT_SYMBOL_NOVERS eksporterer symbolet uten versjonsinformasjon (se kapittel 11 "kmod og avansert modularisering"). For flere detaljer
sjekk ut header-filen , selv om det som er oppgitt er ganske tilstrekkelig for praktisk bruk
makroer.

Initialisere og fullføre moduler

Som nevnt registrerer init_module() funksjonen de funksjonelle komponentene til en modul med kjernen. Etter slik registrering vil applikasjonen som bruker modulen ha tilgang til modulens inngangspunkter gjennom grensesnittet fra kjernen.

Moduler kan registrere mange forskjellige komponenter, som, når de er registrert, er navnene på modulfunksjonene. En peker til en datastruktur som inneholder pekere til funksjoner som implementerer den foreslåtte funksjonaliteten, sendes til kjerneregistreringsfunksjonen.

I kapittel 1, "Introduksjon til enhetsdrivere", ble klassifiseringen av hovedtypene enheter nevnt. Du kan registrere ikke bare enhetstypene som er nevnt der, men også andre, til og med programvareabstraksjoner, som for eksempel /proc-filer osv. Alt som kan fungere i kjernen gjennom dkan registreres som driver .

Hvis du ønsker å lære mer om hvilke typer drivere som registreres med kjernen din som eksempel, kan du implementere et søk etter EXPORT_SYMBOL-delstrengen i kjernekildene og finne inngangspunktene som tilbys av de forskjellige driverne. Som regel bruker registreringsfunksjoner et prefiks i navnet registrere_,
så en annen mulig måte å finne dem på er å søke etter en understreng registrere_ i /proc/ksyms-filen ved å bruke grep-verktøyet. Som allerede nevnt, i 2.6.x-kjernen ble /proc/ksyms-filen erstattet med /proc/modules.

Feilhåndtering i init_module

Hvis det oppstår noen form for feil under initialisering av en modul, må du angre initialiseringen som allerede er fullført før du stopper lasting av modulen. Feilen kan for eksempel oppstå på grunn av utilstrekkelig minne i systemet ved tildeling av datastrukturer. Dessverre kan dette skje, og god kode bør kunne håndtere slike situasjoner.

Alt som ble registrert eller tildelt før feilen oppsto i initialiseringsfunksjonen init_module() må kanselleres eller frigjøres selv, fordi Linux-kjernen ikke sporer initialiseringsfeil og ikke angre lån og tildeling av ressurser med modulkode. Hvis du ikke rullet tilbake, eller ikke var i stand til å rulle tilbake den fullførte registreringen, vil kjernen forbli i en ustabil tilstand, og når modulen er lastet inn igjen
du vil ikke kunne gjenta registreringen av allerede registrerte elementer, og du vil ikke kunne kansellere en tidligere foretatt registrering, fordi i den nye forekomsten av init_module()-funksjonen vil du ikke ha riktig verdi på adressene til de registrerte funksjonene. Å gjenopprette systemet til sin forrige tilstand vil kreve bruk av ulike komplekse triks, og dette gjøres ofte ved å starte systemet på nytt.

Implementeringen av å gjenopprette den forrige tilstanden til systemet når modulinitieringsfeil oppstår implementeres best ved å bruke goto-operatøren. Vanligvis blir denne operatøren behandlet ekstremt negativt, og til og med med hat, men det er i denne situasjonen han viser seg å være veldig nyttig. Derfor, i kjernen, brukes goto-setningen ofte til å håndtere modulinitieringsfeil.

Følgende enkle kode, som bruker dummy-registrerings- og avregistreringsfunksjoner som eksempel, demonstrerer denne måten å håndtere feil på.

Int init_module(void) ( int err; /* registrering tar en peker og et navn */ err = register_this(ptr1, "skull"); if (err) goto fail_this; err = register_that(ptr2, "skull"); if (feil) goto fail_that; err = register_those(ptr3, "skull"); if (err) goto fail_those; return 0; /* suksess */ fail_those: unregister_that(ptr2, "skull"); fail_that: unregister_this(ptr1, " skull"); fail_this: returner feil; /* forplant feilen */ )

Dette eksemplet prøver å registrere tre modulkomponenter. Goto-setningen brukes når en registreringsfeil oppstår og fører til at registrerte komponenter avregistreres før modullasting stoppes.

Et annet eksempel på bruk av en goto-setning for å gjøre koden lettere å lese er trikset med å "huske" vellykkede modulregistreringer og kalle cleanup_module() for å sende denne informasjonen når en feil oppstår. Cleanup_module()-funksjonen er designet for å tilbakestille fullførte initialiseringsoperasjoner og kalles automatisk når modulen er avlastet. Verdien som init_module()-funksjonen returnerer må være
representerer modulinitieringsfeilkoden. I Linux-kjernen er feilkoden et negativt tall fra et sett med definisjoner laget i overskriftsfilen . Inkluder denne overskriftsfilen i modulen din for å bruke symbolsk mnemonikk for reserverte feilkoder som -ENODEV, -ENOMEM, etc. Å bruke slike mnemonics anses som god programmeringsstil. Det skal imidlertid bemerkes at noen versjoner av verktøyene fra modutils-pakken ikke behandler returnerte feilkoder riktig og viser meldingen "Enhet opptatt"
som svar på en hel gruppe feil av en helt annen karakter returnert av funksjonen init_modules(). I de nyeste versjonene av pakken dette
Den irriterende feilen er fikset.

Cleanup_module() funksjonskoden for tilfellet ovenfor kan for eksempel være slik:

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

Hvis initialiserings- og termineringskoden din er mer kompleks enn beskrevet her, kan bruk av en goto-setning resultere i vanskelig å lese programtekst fordi termineringskoden må gjentas i init_module()-funksjonen ved å bruke flere etiketter for goto-overganger. Av denne grunn er et mer smart triks å bruke et kall til cleanup_module()-funksjonen i init_module()-funksjonen, og sende informasjon om omfanget av vellykket initialisering når en modullastingsfeil oppstår.

Nedenfor er et eksempel på hvordan du skriver funksjonene init_module() og cleanup_module(). Dette eksemplet bruker globalt definerte pekere som inneholder informasjon om omfanget av vellykket initialisering.

Strukturer noe *item1; strukturere noe annet *element2; int ting_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 (argumenter); item2 = allocate_thing2(arguments2); if (!item2 || !item2) goto fail; err = register_stuff(item1, item2); if (!err) stuff_ok = 1; else goto fail; return 0; /* suksess */ fail: cleanup_module(); return feil; )

Avhengig av kompleksiteten til modulens initialiseringsoperasjoner, kan det være lurt å bruke en av metodene som er oppført her for å kontrollere modulinitieringsfeil.

Modulbrukteller

Systemet inneholder en bruksteller for hver modul for å avgjøre om modulen trygt kan losses. Systemet trenger denne informasjonen fordi en modul ikke kan lastes ut hvis den er okkupert av noen eller noe - du kan ikke fjerne en filsystemdriver hvis det filsystemet er montert, eller du kan ikke laste ut en tegnenhetsmodul hvis en prosess bruker denne enheten. Ellers,
dette kan føre til systemkrasj - segmenteringsfeil eller kjernepanikk.

I moderne kjerner kan systemet gi deg en automatisk modulbrukteller ved hjelp av en mekanisme vi skal se på i neste kapittel. Uavhengig av kjerneversjonen kan du bruke manuell kontroll av denne telleren. Dermed bør kode som skal brukes i eldre versjoner av kjernen bruke en modulbruksregnskapsmodell bygget på følgende tre makroer:

MOD_INC_USE_COUNTØker gjeldende moduls bruksteller MOD_DEC_USE_COUNT Reduserer gjeldende moduls bruksteller MOD_IN_USE Returnerer sann hvis brukstelleren for denne modulen er null

Disse makroene er definert i , og de manipulerer en spesiell intern datastruktur som direkte tilgang ikke er ønskelig til. Faktum er at den interne strukturen og måten å administrere disse dataene på kan endre seg fra versjon til versjon, mens det eksterne grensesnittet for bruk av disse makroene forblir uendret.

Merk at du ikke trenger å sjekke MOD_IN_USE i funksjonskoden cleanup_module(), fordi denne kontrollen utføres automatisk før cleanup_module() kalles opp i systemkallet sys_delete_module(), som er definert i kernel/module.c.

Riktig styring av modulbruktelleren er avgjørende for systemstabiliteten. Husk at kjernen kan bestemme seg for å laste ut en ubrukt modul automatisk når som helst. En vanlig feil i modulprogrammering er feil kontroll av denne telleren. For eksempel, som svar på en viss forespørsel, utfører modulkoden noen handlinger og, når behandlingen er fullført, øker modulbruktelleren. De. en slik programmerer antar at denne telleren er ment å samle modulbruksstatistikk, mens den faktisk er en teller for gjeldende bruk av modulen, dvs. holder styr på antall prosesser ved å bruke modulkoden for øyeblikket. Når du behandler en forespørsel til en modul, må du derfor ringe MOD_INC_USE_COUNT før du utfører noen handlinger, og MOD_DEC_USE_COUNT etter at de er fullført.

Det kan oppstå situasjoner der du av åpenbare grunner ikke vil kunne laste ut en modul hvis du mister kontrollen over brukstelleren. Denne situasjonen oppstår ofte på modulutviklingsstadiet. For eksempel kan en prosess avbryte mens du prøver å derifisere en NULL-peker, og du vil ikke kunne laste ut en slik modul før du returnerer dens bruksteller til null. En av de mulige løsningene på dette problemet på modulfeilsøkingsstadiet er å fullstendig forlate kontrollen av modulbrukstelleren ved å omdefinere MOD_INC_USE_COUNT Og MOD_DEC_USE_COUNT inn i tom kode. En annen løsning er å lage et ioctl()-kall som tvinger modulens bruksteller til null. Vi vil dekke dette i delen "Bruke ioctl-argumentet" i kapittel 5, "Forbedrede Char Driver-operasjoner." Selvfølgelig, i en klar til bruk driver, bør slike uredelige manipulasjoner med telleren utelukkes, men på feilsøkingsstadiet sparer de utvikleren tid og er ganske akseptable.

Du finner gjeldende systembrukteller for hver modul i det tredje feltet for hver oppføring i /proc/modules-filen. Denne filen inneholder informasjon om de for øyeblikket lastede modulene - én linje per modul. Det første feltet på linjen inneholder navnet på modulen, det andre feltet er størrelsen som er okkupert av modulen i minnet, og det tredje feltet er gjeldende verdi for brukstelleren. Denne informasjonen, i formatert form,
kan fås ved å ringe lsmod-verktøyet. Nedenfor er et eksempel på en /proc/modules-fil:

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

Her ser vi flere moduler lastet inn i systemet. I flaggfeltet (det siste feltet på linjen) vises stabelen med modulavhengigheter i hakeparenteser. Blant annet kan du legge merke til at parallellportmodulene kommuniserer gjennom en modulstabel, som vist i fig. 2-2. (autoclean) flagget markerer moduler kontrollert av kmod eller kerneld. Dette vil bli dekket i kapittel 11 "kmod og avansert modularisering"). Det (ubrukte) flagget betyr at modulen ikke er i bruk for øyeblikket. I kjerne 2.0 viste størrelsesfeltet informasjon ikke i byte, men i sider, som for de fleste plattformer er 4kB i størrelse.

Loser en modul

For å laste ut en modul, bruk rmmod-verktøyet. Å laste ut en modul er en enklere oppgave enn å laste den, som innebærer å dynamisk koble den til kjernen. Når en modul er avlastet, utføres delete_module() systemkallet, som enten kaller opp cleanup_module() funksjonen til den avlastede modulen hvis bruksantallet er null, eller avsluttes med en feil.

Som allerede nevnt, ruller funksjonen cleanup_module() tilbake initialiseringsoperasjonene som ble utført når modulen lastes inn med funksjonen cleanup_module(). Også eksporterte modulsymboler slettes automatisk.

Eksplisitt definering av terminerings- og initialiseringsfunksjoner

Som allerede nevnt, når du laster en modul, kaller kjernen init_module() funksjonen, og når den losses kaller den cleanup_module(). Men i moderne versjoner av kjernen har disse funksjonene ofte forskjellige navn. Fra og med kjerne 2.3.23 ble det mulig å eksplisitt definere et navn for funksjonen for å laste og losse en modul. I dag er denne eksplisitte navngivningen av disse funksjonene den anbefalte programmeringsstilen.

La oss gi et eksempel. Hvis du vil erklære my_init()-funksjonen som initialiseringsfunksjonen til modulen din, og my_cleanup()-funksjonen som den endelige funksjonen, i stedet for henholdsvis init_module() og cleanup_module(), så må du legge til følgende to makroer til modulteksten (vanligvis settes de inn på slutten
modulkodekildefil):

Module_init(min_init); module_exit(min_opprydding);

Merk at for å bruke disse makroene må du inkludere en overskriftsfil i modulen din .

Det praktiske med å bruke denne stilen er at hver modulinitierings- og termineringsfunksjon i kjernen kan ha sitt eget unike navn, noe som i stor grad hjelper til med feilsøking. Dessuten forenkler bruken av disse funksjonene feilsøkingen, uavhengig av om du implementerer driverkoden din som en modul eller skal bygge den direkte inn i kjernen. Det er selvfølgelig ikke nødvendig å bruke makroene module_init og module_exit hvis initialiserings- og avslutningsfunksjonene dine har reserverte navn, dvs. henholdsvis init_module() og cleanup_module().

Hvis du ser på kjernekildene 2.2 eller nyere, kan du se en litt annen form for beskrivelse av initialiserings- og avslutningsfunksjonene. For eksempel:

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

Attributtbruk __i det vil føre til at initialiseringsfunksjonen blir lastet ut fra minnet etter at initialiseringen er fullført. Dette fungerer imidlertid bare for kjerne innebygde drivere, og vil bli ignorert for moduler. Også, for drivere innebygd i kjernen, attributtet __exit vil føre til at hele funksjonen merket med dette attributtet ignoreres. For moduler vil dette flagget også bli ignorert.

Bruke attributter __i det(Og __initdata for å beskrive data) kan redusere mengden minne som brukes av kjernen. Flagg __i det Modulens initialiseringsfunksjon vil verken gi fordel eller skade. Kontroll av denne typen initialisering er ennå ikke implementert for moduler, selv om det kan være mulig i fremtiden.

Oppsummering

Så, som et resultat av det presenterte materialet, kan vi presentere følgende versjon av "Hello world"-modulen:

Modulens kildefilkode ============================================== = #inkluder #inkludere #inkludere static int __init my_init_module (void) ( EXPORT_NO_SYMBOLS; printk("<1>Hei verden\n"); return 0; ); static void __exit my_cleanup_module (void) ( printk("<1>Farvel\n"); ); module_init(my_init_module); module_exit(my_cleanup_module); MODULE_LICENSE("GPL"); ======================= ====================== Makefil for kompilering av modulen ======================== =============== ==================== CFLAGS = -Vegg -D__KERNEL__ -DMODULE -I/lib/modules/ $(shell uname -r)/build/include hello.o: =================================== ===========================

Vær oppmerksom på at når vi skrev Makefilen, brukte vi konvensjonen om at GNU make-verktøyet uavhengig kan bestemme hvordan en objektfil skal genereres basert på CFLAGS-variabelen og kompilatoren som er tilgjengelig på systemet.

Ressursbruk

En modul kan ikke fullføre oppgaven sin uten å bruke systemressurser som minne, I/O-porter, I/O-minne, avbruddslinjer og DMA-kanaler.

Som programmerer bør du allerede være kjent med dynamisk minnebehandling. Dynamisk minneadministrasjon i kjernen er ikke fundamentalt forskjellig. Programmet ditt kan få minne ved å bruke funksjonen kmalloc() og fri henne med hjelpen kfree(). Disse funksjonene ligner veldig på malloc()- og free()-funksjonene du er kjent med, bortsett fra at kmalloc()-funksjonen sendes et ekstra argument - prioritet. Prioriteten er vanligvis GFP_KERNEL eller GFP_USER. GFP er et akronym for "få gratis side." Administrering av dynamisk minne i kjernen er dekket i detalj i kapittel 7, "Få tak i minnet."

En nybegynner driverutvikler kan bli overrasket over behovet for å eksplisitt allokere I/O-porter, I/O-minne og avbruddslinjer. Først da kan kjernemodulen enkelt få tilgang til disse ressursene. Selv om systemminne kan tildeles hvor som helst, spiller I/O-minne, porter og avbruddslinjer en spesiell rolle og tildeles annerledes. For eksempel må driveren tildele visse porter, ikke
alt, men de han trenger for å kontrollere enheten. Men sjåføren kan ikke bruke disse ressursene før den er sikker på at de ikke blir brukt av noen andre.

Området med minne som eies av en perifer enhet kalles vanligvis I/O-minne, for å skille det fra system-RAM (RAM), som ganske enkelt kalles minne.

Porter og I/O-minne

Arbeidet til en typisk driver består i stor grad av lese- og skriveporter og I/O-minne. Porter og I/O-minne er forent med et felles navn - I/O-region (eller område).

Dessverre kan ikke alle bussarkitekturer klart definere I/O-regionen som tilhører hver enhet, og det er mulig at sjåføren må gjette plasseringen til regionen den tilhører, eller til og med forsøke lese-/skriveoperasjoner på mulig adresse mellomrom. Dette problemet er spesielt
refererer til ISA-bussen, som fortsatt brukes til å installere enkle enheter i en personlig datamaskin og er veldig populær i den industrielle verden ved implementering av PC/104 (se avsnittet "PC/104 og PC/104+" i kapittel 15 "Oversikt over perifere busser" ).

Uansett hvilken buss som brukes til å koble til en maskinvareenhet, må enhetsdriveren garanteres eksklusiv tilgang til sin I/O-region for å forhindre kollisjoner mellom drivere. Hvis en modul, som får tilgang til sin egen enhet, skriver til en enhet som ikke tilhører den, kan dette føre til fatale konsekvenser.

Linux-utviklere implementerte en mekanisme for å be om/frigi I/O-regioner primært for å forhindre kollisjoner mellom forskjellige enheter. Denne mekanismen har lenge vært brukt for I/O-porter og har nylig blitt generalisert til ressursstyring generelt. Merk at denne mekanismen representerer en programvareabstraksjon og ikke omfatter maskinvarefunksjoner. For eksempel forårsaker ikke uautorisert tilgang til I/O-porter på maskinvarenivå noen feil som ligner på en "segmenteringsfeil", siden maskinvaren ikke allokerer og autoriserer ressursene sine.

Informasjon om registrerte ressurser er tilgjengelig i tekstform i filene /proc/ioports og /proc/iomem. Denne informasjonen har blitt introdusert i Linux siden kjernen 2.3. Som en påminnelse fokuserer denne boken først og fremst på 2.4-kjernen, og kompatibilitetsnotater vil bli presentert på slutten av kapittelet.

Havner

Følgende er det typiske innholdet i /proc/ioports-filen:

0000-001f: dma1 0020-003f: pic1 0040-005f: tidtaker 0060-006f: tastatur 0080-008f: dma sidereg 00a0-00bf: pic2 00c0-00df: dma-1000: dma-1000: dma-1000: i 01f0-01f7 : ide0 02f8-02ff: seriell(sett) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga+ 03f6-03f6: ide0 03f8-03ff: seriell(sett) 103fIX Corporation- 103PIIX Corporation-1030IX Corporation-1030IX 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: Intel Corporation 82371AB 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

Hver linje i denne filen viser i heksadesimal rekkevidde av porter knyttet til driveren eller enhetseieren. I tidligere versjoner av kjernen hadde filen samme format, bortsett fra at porthierarkiet ikke ble vist.

Filen kan brukes til å unngå portkollisjoner når en ny enhet legges til systemet. Dette er spesielt praktisk når du manuelt konfigurerer installert utstyr ved å bytte jumpere. I dette tilfellet kan brukeren enkelt se listen over brukte porter og velge en ledig rekkevidde for enheten som skal installeres. Og selv om de fleste moderne enheter ikke bruker manuelle hoppere i det hele tatt, brukes de fortsatt til produksjon av småskala komponenter.

Det som er viktigere er at /proc/ioports-filen har en programmatisk tilgjengelig datastruktur knyttet til seg. Derfor, når enhetsdriveren initialiseres, kan den kjenne det okkuperte området av I/O-porter. Dette betyr at hvis det er nødvendig å skanne porter på jakt etter en ny enhet, kan driveren unngå situasjonen med å skrive til porter okkupert av andre enheter.

Å skanne ISA-bussen er kjent for å være en risikabel oppgave. Derfor unngår noen drivere distribuert med den offisielle Linux-kjernen slik skanning når modulen lastes inn. Ved å gjøre det unngår de risikoen for å skade et kjørende system ved å skrive til porter som brukes av annet utstyr. Heldigvis er moderne bussarkitektur immun mot disse problemene.

Programvaregrensesnittet som brukes for å få tilgang til I/O-registre består av følgende tre funksjoner:

Int check_region(usignert lang start, usignert lang len); struct ressurs *request_region(usignert lang start, usignert lang len, char *navn); void release_region(usignert lang start, usignert lang len);

Funksjon check_region() kan kalles for å sjekke om et spesifisert utvalg av porter er opptatt. Den returnerer en negativ feilkode (som -EBUSY eller -EINVAL) hvis svaret er negativt.

Funksjon request_region() utfører tildelingen av et gitt adresseområde, og returnerer, hvis vellykket, en ikke-null-peker. Sjåføren trenger ikke å lagre eller bruke den returnerte pekeren. Alt du trenger å gjøre er å se etter NULL. Kode som bare skal fungere med en 2.4 (eller høyere) kjerne trenger ikke kalle funksjonen check_region() i det hele tatt. Det er ingen tvil om fordelen med denne distribusjonsmetoden, fordi
det er ukjent hva som kan skje mellom anrop til check_region() og request_region(). Hvis du vil opprettholde kompatibilitet med eldre versjoner av kjernen, er det nødvendig å kalle check_region() før request_region().

Funksjon release_region() må kalles når driveren slipper tidligere brukte porter.

Den faktiske verdien av pekeren returnert av request_region() brukes bare av ressursallokeringsundersystemet som kjører i kjernen.

Disse tre funksjonene er faktisk makroer definert i .

Nedenfor er et eksempel på anropssekvensen som brukes til å registrere porter. Eksemplet er hentet fra skull training driver code. (Skull_probe_hw()-funksjonskoden vises ikke her fordi den inneholder maskinvareavhengig kode.)

#inkludere #inkludere statisk int skull_detect (usignert int port, usignert 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; }

Dette eksemplet kontrollerer først tilgjengeligheten til det nødvendige portutvalget. Hvis portene ikke er tilgjengelige, er tilgang til utstyret ikke mulig.
Den faktiske plasseringen av enhetsportene kan avklares ved å skanne. request_region()-funksjonen skal ikke, i dette eksemplet,
vil ende i fiasko. Kjernen kan ikke laste mer enn én modul om gangen, så portbrukskollisjoner vil ikke forekomme
må.

Eventuelle I/O-porter som er tildelt av driveren, må senere frigis. Vår hodeskalledriver gjør dette i funksjonen cleanup_module():

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

Mekanismen for ressursforespørsel/frigjøring ligner på modulregistrerings-/avregistreringsmekanismen og er perfekt implementert basert på goto-operatørens bruksskjema beskrevet ovenfor.

Hukommelse

Informasjon om I/O-minne er tilgjengelig gjennom /proc/iomem-filen. Nedenfor er et typisk eksempel på en slik fil for en personlig datamaskin:

00000000-0009fbff: System RAM 0009fc00-0009ffff: reservert 000a0000-000bffff: Video RAM-område 000c0000-000c7fff: Video ROM 000f0000-000f0000-000fff: 000ff RAM: 000ff RAM 0 0100000-0022c557: Kjernekode 0022c558-0024455f: Kjernedata 20000000 - 2ffffffff: Intel Corporation 440BX/ZX - 82443BX/ZX Vertsbro 68000000-68000fff: Texas Instruments PCI1225 68001000-68001fff: Texas Instruments PCI1225 (#2) #300ffff:0e PCI1000000 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: reservert

Adresseområdeverdier vises i heksadesimal notasjon. For hvert aresområde vises eieren.

Registrering av I/O-minnetilganger ligner på å registrere I/O-porter og er bygget på samme mekanisme i kjernen.

For å få og frigi det nødvendige området med I/O-minneadresser, må sjåføren bruke følgende anrop:

Int check_mem_region(usignert lang start, usignert lang len); int request_mem_region(usignert lang start, usignert lang len, char *navn); int release_mem_region(usignert lang start, usignert lang len);

Vanligvis kjenner driveren utvalget av I/O-minneadresser, så koden for tildeling av denne ressursen kan reduseres sammenlignet med eksemplet for tildeling av en rekke porter:

If (check_mem_region(mem_addr, mem_size)) ( printk("drivernavn: minne som allerede er i bruk\n"); return -EBUSY; ) request_mem_region(mem_addr, mem_size, "drivernavn");

Ressurstildeling i Linux 2.4

Den nåværende ressursallokeringsmekanismen ble introdusert i Linux-kjernen 2.3.11 og gir fleksibel tilgang til systemressursadministrasjon. Denne delen beskriver kort denne mekanismen. Grunnleggende ressursallokeringsfunksjoner (som request_region() osv.) er imidlertid fortsatt implementert som makroer og brukes for bakoverkompatibilitet med tidligere versjoner av kjernen. I de fleste tilfeller trenger du ikke å vite noe om selve distribusjonsmekanismen, men det kan være interessant når du lager mer komplekse drivere.

Ressursstyringssystemet implementert i Linux kan administrere vilkårlige ressurser på en enhetlig hierarkisk måte. Globale systemressurser (for eksempel I/O-porter) kan deles inn i delsett - for eksempel de som er relatert til et bestemt maskinvarebussspor. Enkelte drivere kan også valgfritt dele inn fangede ressurser basert på deres logiske struktur.

Utvalget av tildelte ressurser er beskrevet gjennom strukturressursstrukturen, som er deklarert i overskriftsfilen :

Strukturressurs ( const char *navn; usignert lang start, slutt; usignerte lange flagg; strukturressurs *forelder, *søsken, *barn; );

Et globalt (root) utvalg av ressurser opprettes ved oppstart. For eksempel opprettes en ressursstruktur som beskriver I/O-porter som følger:

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

En ressurs kalt PCI IO er beskrevet her, som dekker adresseområdet fra null til IO_SPACE_LIMIT. Verdien av denne variabelen avhenger av plattformen som brukes og kan være lik 0xFFFF (16-bit adresserom, for x86, IA-64, Alpha, M68k og MIPS arkitekturer), 0xFFFFFFFF (32-bit adresserom, for SPARC, PPC , SH) eller 0xFFFFFFFFFFFFFFFF (64-bit, SPARC64).

Underområder av denne ressursen kan opprettes ved å bruke et kall til allocate_resource(). For eksempel, under initialisering av PCI-buss, opprettes en ny ressurs for adresseregionen til denne bussen og tilordnes en fysisk enhet. Når PCI-kjernekoden behandler port- og minnetilordninger, oppretter den en ny ressurs for bare disse regionene og tildeler dem ved å bruke kall til ioport_resource() eller iomem_resource().

Sjåføren kan deretter be om et undersett av en ressurs (vanligvis en del av en global ressurs) og merke den som opptatt. Ressursinnhenting oppnås ved å kalle request_region(), som returnerer enten en peker til en ny strukturressursstruktur som beskriver den forespurte ressursen, eller NULL ved feil. Denne strukturen er en del av det globale ressurstreet. Som allerede nevnt, etter å ha skaffet ressursen, vil ikke driveren trenge verdien av denne pekeren.

Den interesserte leser kan ha glede av å se detaljene i denne ressursstyringsordningen i kernel/resource.c-filen som ligger i kjernekildekatalogen. For de fleste utviklere vil imidlertid kunnskapen som allerede er presentert være tilstrekkelig.

Den lagdelte ressursallokeringsmekanismen gir doble fordeler. På den ene siden gir det en visuell representasjon av kjernedatastrukturer. La oss ta en titt på eksempelfilen /proc/ioports igjen:

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

e800-e8ff-serien er allokert til Adaptec-adapteren, som utpekte seg selv som en driver på PCI-bussen. Det meste av dette området ble forespurt av aic7xxx-driveren.

En annen fordel med denne ressursstyringen er oppdelingen av adresserommet i underområder som gjenspeiler den faktiske sammenkoblingen av utstyret. Ressursbehandlingen kan ikke tildele overlappende adresseunderområder, noe som kan forhindre installasjon av en driver som ikke fungerer.

Automatisk og manuell konfigurasjon

Noen parametere som kreves av driveren kan variere fra system til system. For eksempel må sjåføren være klar over gyldige I/O-adresser og minneområder. For godt organiserte bussgrensesnitt er ikke dette et problem. Noen ganger må du imidlertid sende parametere til driveren for å hjelpe den med å finne sin egen enhet, eller aktivere/deaktivere noen av funksjonene.

Disse innstillingene som påvirker driverdriften varierer fra enhet til enhet. Dette kan for eksempel være versjonsnummeret til den installerte enheten. Selvfølgelig er slik informasjon nødvendig for at sjåføren skal fungere riktig med enheten. Å definere slike parametere (driverkonfigurasjon) er ganske en
en vanskelig oppgave som utføres når driveren initialiseres.

Vanligvis er det to måter å få de riktige verdiene til denne parameteren på - enten definerer brukeren dem eksplisitt, eller sjåføren bestemmer dem uavhengig, basert på polling av utstyret. Selv om automatisk gjenkjenning utvilsomt er den beste løsningen for driverkonfigurasjon,
tilpasset konfigurasjon er mye enklere å implementere. Driverutvikleren bør implementere driverautokonfigurasjon der det er mulig, men samtidig skal han gi brukeren en manuell konfigurasjonsmekanisme. Selvfølgelig bør manuell konfigurasjon ha høyere prioritet enn automatisk konfigurasjon. På de innledende stadiene av utviklingen implementeres vanligvis bare manuell overføring av parametere til driveren. Automatisk konfigurasjon, hvis mulig, legges til senere.

Mange drivere, blant deres konfigurasjonsparametere, har parametere som kontrollerer driveroperasjoner. For eksempel lar Integrated Device Electronics (IDE) grensesnittdrivere brukeren kontrollere DMA-operasjoner. Så hvis driveren din gjør en god jobb med å automatisk oppdage maskinvare, vil du kanskje gi brukeren kontroll over driverens funksjonalitet.

Parameterverdier kan sendes under modullasting ved å bruke insmod- eller modprobe-kommandoene. Nylig har det blitt mulig å lese verdien av parametere fra en konfigurasjonsfil (vanligvis /etc/modules.conf). Heltalls- og strengverdier kan sendes som parametere. Derfor, hvis du trenger å sende en heltallsverdi for skull_ival-parameteren og en strengverdi for skull_sval-parameteren, kan du sende dem under modullasting med tilleggsparametere til insmod-kommandoen:

Insmod skull skull_ival=666 skull_sval="beistet"

Men før insmod-kommandoen kan endre verdiene til en moduls parametere, må modulen gjøre disse parameterne tilgjengelige. Parametre er deklarert ved hjelp av MODULE_PARM makrodefinisjonen, som er definert i module.h header-filen. MODULE_PARM-makroen tar to parametere: navnet på variabelen og en streng som definerer typen. Denne makrodefinisjonen må plasseres utenfor alle funksjoner og er vanligvis plassert i begynnelsen av filen etter at variablene er definert. Så de to parametrene nevnt ovenfor kan deklareres som følger:

Int skull_ival=0; røye *skull_sval; MODULE_PARM(skull_ival, "i"); MODULE_PARM(skull_sval, "s");

Det er for øyeblikket fem typer modulparametere som støttes:

  • b - én-byte verdi;
  • h - (kort) to-byte verdi;
  • i - heltall;
  • l - langt heltall;
  • s - streng (char *);

Når det gjelder strengparametere, må en peker (char *) deklareres i modulen. Insmod-kommandoen tildeler minne for strengen som sendes inn og initialiserer den med den nødvendige verdien. Ved å bruke MODULE_PARM-makroen kan du initialisere arrays av parametere. I dette tilfellet bestemmer heltallet foran typetegnet lengden på matrisen. Når to heltall er spesifisert, atskilt med en strek, bestemmer de minimum og maksimum antall verdier som skal overføres. For en mer detaljert forståelse av hvordan denne makroen fungerer, se overskriftsfilen .

Anta for eksempel at en rekke parametere må initialiseres med minst to og minst fire heltallsverdier. Da kan det beskrives slik:

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

I tillegg har programmererens verktøysett makrodefinisjonen MODULE_PARM_DESC, som lar deg legge inn kommentarer til modulparametrene som sendes. Disse kommentarene lagres i modulobjektfilen og kan vises ved å bruke for eksempel objdump-verktøyet eller ved å bruke automatiserte systemadministrasjonsverktøy. Her er et eksempel på bruk av denne makrodefinisjonen:

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

Det er ønskelig at alle modulparametere har standardverdier. Endring av disse verdiene ved hjelp av insmod bør bare være nødvendig hvis nødvendig. Modulen kan sjekke den eksplisitte innstillingen av parametere ved å sjekke deres nåværende verdier med standardverdiene. Deretter kan du implementere en automatisk konfigurasjonsmekanisme basert på følgende diagram. Hvis parameterverdiene har standardverdier, utføres automatisk konfigurasjon. Ellers brukes gjeldende verdier. For at denne ordningen skal fungere, er det nødvendig at standardverdiene ikke samsvarer med noen av de mulige systemkonfigurasjonene i den virkelige verden. Det vil da antas at slike verdier ikke kan settes av brukeren i manuell konfigurasjon.

Følgende eksempel viser hvordan skalledriveren automatisk oppdager adresseområdet til enhetsportene. I eksemplet ovenfor ser automatisk deteksjon på flere enheter, mens manuell konfigurasjon begrenser driveren til en enkelt enhet. Du har allerede møtt skull_detect-funksjonen tidligere i avsnittet som beskriver I/O-porter. Implementeringen av skull_init_board() vises ikke fordi den
Utfører maskinvareavhengig initialisering.

/* * portområder: enheten kan ligge mellom * 0x280 og 0x300, i trinn på 0x10. Den bruker 0x10 porter. */ #define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 /* * følgende funksjon utfører autodeteksjon, med mindre en spesifikk * verdi ble tildelt av insmod til "skull_port_base" */ static_base skull; /* 0 tvinger frem autodeteksjon */ MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC(skull_port_base, "Base I/O-port for skull"); static int skull_find_hw(void) /* returnerer antall enheter */ ( /* base er enten lastetidsverdien eller den første prøveversjonen */ int base = skull_port_base ? skull_port_base: SKULL_PORT_FLOOR; int resultat = 0; /* loop en tid hvis verdien er tildelt; prøv alle hvis du automatisk oppdager */ do ( if (skull_detect(base, SKULL_PORT_RANGE) == 0) ( skull_init_board(base); result++; ) base += SKULL_PORT_RANGE; /* forberede seg på neste prøveversjon */ ) mens (skull_port_base == 0 && base< SKULL_PORT_CEIL); return result; }

Hvis konfigurasjonsvariabler bare brukes inne i driveren (dvs. ikke publisert i kjernesymboltabellen), kan programmereren gjøre livet litt enklere for brukeren ved å ikke bruke prefikser i variabelnavnene (i vårt tilfelle skull_-prefikset) . Disse prefiksene betyr lite for brukeren, og deres fravær gjør det enklere å skrive kommandoen fra tastaturet.

For fullstendighetens skyld vil vi gi en beskrivelse av ytterligere tre makrodefinisjoner som lar deg legge inn noen kommentarer i objektfilen.

MODULE_AUTHOR (navn) Plasserer en linje med forfatterens navn i objektfilen. MODULE_DESCRIPTION(desc) Plasserer en linje med en generell beskrivelse av modulen i objektfilen. MODULE_SUPPORTED_DEVICE(dev) Plasserer en linje som beskriver enheten som støttes av modulen. Linux gir et kraftig og omfattende API for applikasjoner, men noen ganger er det ikke nok. For å samhandle med maskinvare eller utføre operasjoner med tilgang til privilegert informasjon i systemet, er det nødvendig med en kjernedriver.

En Linux-kjernemodul er en kompilert binær kode som settes inn direkte i Linux-kjernen, og kjører i ring 0, den indre og minst sikre instruksjonsutførelsesringen i x86–64-prosessoren. Her kjøres koden helt uten noen kontroller, men med en utrolig hastighet og med tilgang til eventuelle systemressurser.

Ikke for bare dødelige

Å skrive en Linux-kjernemodul er ikke for sarte sjeler. Ved å endre kjernen risikerer du å miste data. Det er ingen standard sikkerhet i kjernekoden slik det er i vanlige Linux-applikasjoner. Hvis du gjør en feil, legg på hele systemet.

Det som gjør situasjonen verre er at problemet ikke nødvendigvis dukker opp umiddelbart. Hvis modulen henger systemet umiddelbart etter lasting, er dette det beste feilscenarioet. Jo mer kode det er, jo høyere er risikoen for uendelige løkker og minnelekkasjer. Hvis du ikke er forsiktig, vil problemene gradvis øke etter hvert som maskinen går. Etter hvert kan viktige datastrukturer og til og med buffere overskrives.

Tradisjonelle kan stort sett glemmes. I tillegg til å laste og losse en modul, vil du skrive kode som reagerer på systemhendelser i stedet for å følge et sekvensielt mønster. Når du arbeider med kjernen, skriver du API, ikke selve applikasjonene.

Du har heller ikke tilgang til standardbiblioteket. Selv om kjernen gir noen funksjoner som printk (som er en erstatning for printf) og kmalloc (som fungerer på samme måte som malloc), er du stort sett overlatt til dine egne enheter. I tillegg bør du rydde helt opp etter deg etter å ha losset modulen. Det er ingen søppelinnsamling her.

Nødvendige komponenter

Før du begynner, bør du sørge for at du har alle nødvendige verktøy for jobben. Det viktigste er at du trenger en Linux-maskin. Jeg vet at dette er uventet! Selv om enhver Linux-distribusjon vil gjøre det, bruker jeg Ubuntu 16.04 LTS i dette eksemplet, så du må kanskje endre installasjonskommandoene litt hvis du bruker andre distribusjoner.

For det andre trenger du enten en egen fysisk maskin eller en virtuell maskin. Personlig foretrekker jeg å jobbe på en virtuell maskin, men velg. Jeg anbefaler ikke å bruke hovedmaskinen din på grunn av tap av data når du gjør en feil. Jeg sier "når" og ikke "hvis" fordi du definitivt vil henge bilen minst et par ganger i løpet av prosessen. De siste kodeendringene dine kan fortsatt være i skrivebufferen når kjernen får panikk, så kildene dine kan også være ødelagt. Testing i en virtuell maskin eliminerer disse risikoene.

Til slutt må du i det minste vite litt C. C++-kjøringstiden er for stor for kjernen, så du må skrive i ren, ren C. Litt kunnskap om assemblerspråk er også nyttig for å samhandle med maskinvaren.

Installere utviklingsmiljøet

På Ubuntu må du kjøre:

Apt-get install build-essential linux-headers-`uname -r`
Vi installerer de viktigste utviklingsverktøyene og kjernehodene som kreves for dette eksemplet.

Eksemplene nedenfor antar at du kjører som en vanlig bruker i stedet for root, men at du har sudo-rettigheter. Sudo kreves for å laste inn kjernemoduler, men vi ønsker å jobbe utenfor root når det er mulig.

Begynne

La oss begynne å skrive kode. La oss forberede miljøet vårt:

Mkdir ~/src/lkm_example cd ~/src/lkm_example
Start favorittredigeringsprogrammet ditt (vim i mitt tilfelle) og lag en fil lkm_example.c med følgende innhold:

#inkludere #inkludere #inkludere MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“Et enkelt eksempel på Linux-modul.”); MODULE_VERSION(“0.01”); static int __init lkm_example_init(void) ( printk(KERN_INFO “Hei, verden!\n”); return 0; ) static void __exit lkm_example_exit(void) ( printk(KERN_INFO “Farvel, verden!\n”); ) modul__init(void) ); module_exit(lkm_example_exit);
Vi har designet den enklest mulige modulen, la oss se nærmere på de viktigste delene:

  • include viser overskriftsfilene som trengs for å utvikle Linux-kjernen.
  • MODULE_LICENSE kan settes til forskjellige verdier avhengig av lisensen til modulen. For å se hele listen, kjør:

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

  • Vi setter opp init (lasting) og exit (lossing) som statiske funksjoner som returnerer heltall.
  • Legg merke til bruken av printk i stedet for printf . Alternativene for printk er også forskjellige fra printf . For eksempel er KERN_INFO-flagget for å erklære loggingsprioriteten for en bestemt linje spesifisert uten komma. Kjernen håndterer disse tingene inne i printk-funksjonen for å lagre stabelminne.
  • På slutten av filen kan du kalle module_init og module_exit og spesifisere laste- og lossefunksjonene. Dette gjør det mulig å navngi funksjoner vilkårlig.
Vi kan imidlertid ikke kompilere denne filen ennå. Makefil nødvendig. Dette grunnleggende eksemplet vil være tilstrekkelig for nå. Merk at make er veldig kresen når det gjelder mellomrom og tabulatorer, så pass på å bruke tabulatorer i stedet for mellomrom der det er hensiktsmessig.

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
Hvis vi kjører, bør den kompilere modulen vår. Resultatet blir filen lkm_example.ko. Hvis det oppstår feil, kontroller at anførselstegnene i kildekoden er satt riktig og ikke ved et uhell i UTF-8-koding.

Nå kan du implementere modulen og teste den. For å gjøre dette kjører vi:

Sudo insmod lkm_example.ko
Hvis alt er bra, vil du ikke se noe. Printk-funksjonen gir utdata ikke til konsollen, men til kjerneloggen. For å se må du kjøre:

Sudo dmesg
Du bør se linjen "Hello, World!" med et tidsstempel i begynnelsen. Dette betyr at vår kjernemodul har lastet inn og skrevet til kjerneloggen. Vi kan også sjekke at modulen fortsatt er i minnet:

lsmod | grep "lkm_example"
For å fjerne en modul, kjør:

Sudo rmmod lkm_example
Hvis du kjører dmesg igjen, vil du se oppføringen "Goodbye, World!" i loggen. Du kan kjøre lsmod igjen og sørge for at modulen er avlastet.

Som du kan se, er denne testprosedyren litt kjedelig, men den kan automatiseres ved å legge til:

Test: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
på slutten av Makefilen og deretter kjører:

Gjør test
for å teste modulen og sjekke utdata til kjerneloggen uten å måtte kjøre separate kommandoer.

Vi har nå en fullt funksjonell, om enn helt triviell, kjernemodul!

La oss grave litt dypere. Selv om kjernemoduler er i stand til å utføre alle slags oppgaver, er grensesnitt med applikasjoner en av de vanligste brukstilfellene.

Fordi applikasjoner ikke har lov til å se minne i kjerneplass, må de bruke API for å kommunisere med dem. Selv om det teknisk sett er flere måter å gjøre dette på, er den vanligste å lage en enhetsfil.

Du har sannsynligvis jobbet med enhetsfiler før. Kommandoer som nevner /dev/zero , /dev/null og lignende samhandler med "null"- og "null"-enhetene, som returnerer de forventede verdiene.

I vårt eksempel returnerer vi "Hello, World". Selv om dette ikke er en spesielt nyttig funksjon for applikasjoner, demonstrerer den likevel prosessen med å samhandle med en applikasjon gjennom en enhetsfil.

Her er hele listen:

#inkludere #inkludere #inkludere #inkludere #inkludere MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“Et enkelt eksempel på Linux-modul.”); MODULE_VERSION(“0.01”); #define DEVICE_NAME “lkm_example” #define EXAMPLE_MSG “Hei, verden!\n” #define MSG_BUFFER_LEN 15 /* Prototyper for enhetsfunksjoner */ static int device_open(struct inode *, struct file *); static int device_release(struct inode *, struct file *); statisk 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 tegn msg_buffer; statisk tegn *msg_ptr; /* Denne strukturen peker på alle enhetsfunksjonene */ static struct file_operations file_ops = (.read = device_read, .write = device_write, .open = device_open, .release = device_release ); /* Når en prosess leser fra enheten vår, kalles denne opp. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) ( int bytes_read = 0; /* Hvis vi er på slutten, gå tilbake til begynnelsen */ if (*msg_ptr = = 0) ( msg_ptr = msg_buffer; ) /* Sett data i bufferen */ while (len && *msg_ptr) ( /* Buffer er i brukerdata, ikke kjerne, så du kan ikke bare referere * med en peker. funksjonen put_user håndterer dette for oss */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; ) return bytes_read; ) /* Kalles når en prosess prøver å skrive til enheten vår */ static ssize_t device_write(struct file * flip, const char *buffer, size_t len, loff_t *offset) ( /* Dette er en skrivebeskyttet enhet */ printk(KERN_ALERT “Denne operasjonen støttes ikke.\n”); return -EINVAL; ) /* Kalles når en prosess åpner enheten vår */ static int device_open(struct inode *inode, struct file *file) ( /* Hvis enheten er åpen, return busy */ if (device_open_count) (retur -EBUSY; ) device_open_count++; try_module_get(THIS_MODULE); returner 0; ) /* Kalles når en prosess lukker enheten vår */ static int device_release(struct inode *inode, struct file *file) ( /* Reduser den åpne telleren og antall bruk. Uten dette ville ikke modulen losset. */ device_open_count- -; module_put(THIS_MODULE); return 0; ) static int __init lkm_example_init(void) ( /* Fyll buffer med meldingen vår */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Sett msg_ptr =buffer_buffer */ msg_buffer ; /* Prøv å registrere tegnenhet */ 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);

Tester et forbedret eksempel

Nå gjør vårt eksempel mer enn bare å skrive ut en melding når du laster og losser, så en mindre streng testprosedyre vil være nødvendig. La oss endre Makefilen til kun å laste modulen, uten å laste den ut.

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) ren test: # Vi setter en - foran kommandoen rmmod for å fortelle make å ignorere # en feil i tilfelle modulen ikke er lastet. -sudo rmmod lkm_example # Tøm kjerneloggen uten ekko sudo dmesg -C # Sett inn modulen sudo insmod lkm_example.ko # Vis kjerneloggen dmesg
Nå etter å ha kjørt make-test vil du se hovedenhetsnummeret som sendes ut. I vårt eksempel blir det automatisk tildelt av kjernen. Dette nummeret er imidlertid nødvendig for å opprette en ny enhet.

Ta nummeret generert av make test og bruk det til å lage en enhetsfil slik at vi kan kommunisere med vår kjernemodul fra brukerplass.

Sudo mknod /dev/lkm_example med MAJOR 0
(i dette eksemplet erstatter du MAJOR med verdien hentet fra make test eller dmesg)

Alternativet c i mknod-kommandoen forteller mknod at vi må lage en karakterenhetsfil.

Nå kan vi hente innhold fra enheten:

Katt /dev/lkm_eksempel
eller til og med gjennom dd-kommandoen:

Dd if=/dev/lkm_example of=test bs=14 count=100
Du kan også få tilgang til denne filen fra applikasjoner. Disse trenger ikke å være kompilerte applikasjoner - selv Python, Ruby og PHP-skript har tilgang til disse dataene.

Når vi er ferdige med enheten, fjerner vi den og laster ut modulen:

Sudo rm /dev/lkm_example sudo rmmod lkm_example

Konklusjon

Jeg håper du likte spøkene våre i kjerneområdet. Selv om eksemplene som vises er primitive, kan disse strukturene brukes til å lage dine egne moduler som utfører svært komplekse oppgaver.

Bare husk at i kjernerommet er alt ditt ansvar. Det er ingen støtte eller en ny sjanse for koden din. Hvis du gjør et prosjekt for en klient, planlegg på forhånd for dobbel, om ikke trippel, feilsøkingstid. Kjernekode må være så perfekt som mulig for å sikre integriteten og påliteligheten til systemene den kjører på.

Noen funksjoner ved modulær programmering og generelle anbefalinger for å konstruere underprogrammer av en modulær struktur.

Moduler kobles til hovedprogrammet i den rekkefølgen de er erklært USES, og i samme rekkefølge er initialiseringsblokkene til moduler koblet til hovedprogrammet før programmet begynner å kjøre.

Rekkefølgen som moduler utføres i kan påvirke bibliotekdatatilgang og rutinetilgang.

For eksempel, hvis moduler med navnene M1, M2 inneholder samme type A, variabel B og subrutine C, vil anrop til A, B, C i denne PU etter tilkobling av disse USES-modellene tilsvare anrop til objekter til modul M2 .

Men for å karakterisere riktigheten av anrop til de nødvendige objektene med samme navn fra forskjellige tilkoblede moduler, er det tilrådelig når du får tilgang til en modul å først angi navnet på modulen, etterfulgt av en prikk navnet på objektet: M1. A M1.B M1.C M2.B.

Det er åpenbart veldig enkelt å dele et stort program i to deler (PU), dvs. hovedprogram + moduler.

Plassering av hver PU i sitt eget minnesegment og i sin egen diskfil.

Alle typedeklarasjoner, samt de variablene som skal være tilgjengelige for individuelle PU-er (hovedprogrammet og fremtidige moduler) bør plasseres i en egen modul med en tom kjørbar del. Du bør imidlertid ikke ta hensyn til det faktum at noen PE (for eksempel en modul) ikke bruker alle disse erklæringene. Initieringsdelen av en slik modul kan inneholde setninger som assosierer filvariabler med ikke-standard tekstfiler (ASSIGN) og initierer disse filene, dvs. som indikerer dataoverføringsanrop for dem (RESET og REWRITE).

Den første gruppen av andre underrutiner, for eksempel, flere kompakte funksjoner bør plasseres i modul 3 (på sin side), andre, for eksempel generelle prosedyrer - i modul 4, etc.

Når du distribuerer subrutiner i moduler i et komplekst prosjekt, bør spesiell oppmerksomhet rettes mot rekkefølgen og stedet for skrivingen.

TP-miljøet inneholder verktøy som kontrollerer ulike måter å kompilere moduler på.

Kompiler Alt+F9 KJØR Cntr+F9

Destinasjonsminne

Disse modusene er bare forskjellige i kommunikasjonsmetoden og hovedprogrammet.

Kompileringsmodus

Kompilerer hovedprogrammet eller modulen som for øyeblikket er i det aktive redigeringsvinduet. Hvis denne PU-en inneholder tilgang til ikke-standardiserte brukermoduler, krever denne modusen tilstedeværelsen av diskfiler med samme navn med utvidelsen ___.tpu for hver slik plug-in-modul.



Hvis destinasjonen er lagret i minnet, forblir disse filene bare i minnet, og en diskfil opprettes ikke.

Det er imidlertid mye enklere å lage tpu-filer sammen med kompilatoren av hele programmet ved å bruke andre moduser som ikke krever å sette Disk for destinasjonsalternativet.

Lag-modus

Når du kompilerer i denne modusen, sjekkes følgende først (før kompilering av hovedprogrammet) for hver modul:

1) Eksistensen av en disk tpu-fil; hvis den ikke eksisterer, opprettes den automatisk ved å kompilere kildekoden til modulen, dvs. sin pas-fil

2) Korrespondanse av den funnet tpu-filen til kildeteksten til modulen, der endringer kunne ha blitt gjort; ellers opprettes tpu-filen automatisk igjen

3) Uforanderlighet av grensesnittdelen av modulen: hvis den har endret seg, blir alle modulene (deres kilde-pas-filer) der denne modulen er spesifisert i USES-klausulen også rekompilert.

Hvis det ikke var noen endring i kildekodene til modulene, samhandler kompilatoren med disse tpu-filene og bruker kompileringstid.

Byggemodus

I motsetning til Make-modus, krever det nødvendigvis tilstedeværelsen av kilde-pas-filer; kompilerer (rekompilerer) hver modul og sikrer dermed at alle endringer i tekstene til pas-filer blir tatt i betraktning. Dette øker kompileringstiden for programmet som helhet.

I motsetning til kompileringsmodus, lar Make og Build-modusene deg begynne å kompilere et program med en modulær struktur fra en gitt pas-fil (den kalles primærfilen), uavhengig av hvilken fil (eller del av programmet) som er i den aktive redigeringsvindu. For å gjøre dette, i kompileringselementet, velg alternativet Primær fil, trykk Enter og skriv ned navnet på den primære pas-filen, og så starter kompileringen fra denne filen.

Hvis primærfilen ikke er spesifisert på denne måten, er kompilering i modusene Make, Build og RUN kun mulig hvis hovedprogrammet er i det aktive redigeringsvinduet.

Merk: I fremtiden planlegger jeg å bruke T2-systemet til å kompilere kjernen og modulene for Puppy. T2 er for øyeblikket installert for å kompilere kjernen og en rekke tilleggsmoduler, men ikke versjonen som brukes i Puppy. Jeg har tenkt å synkronisere i fremtidige versjoner av Puppy, slik at kjernen kompilert i T2 vil bli brukt i Puppy. Se http://www.puppyos.net/pfs/ for mer informasjon om Puppy og T2.

Puppy har en veldig enkel måte å bruke C/C++ kompilatorer ved å legge til en enkelt fil, devx_xxx.sfs, der xxx er versjonsnummeret. For eksempel vil Puppy 2.12 ha en samsvarsutviklingsfil kalt devx_212.sfs. Når du kjører i LiveCD-modus, plasser filen devx_xxx.sfs på samme plassering som din personlige innstillingsfil pup_save.3fs, som vanligvis ligger i /mnt/home/-katalogen. Dette gjelder også andre installasjonsmoduser som har en pup_save.3fs-fil. Hvis Puppy er installert på en harddisk med full "Alternativ 2"-installasjon, så er det ingen personlig fil, se på Puppy-nettsidene for å bli kompilert med forskjellige konfigurasjonsalternativer, slik at modulene ikke er kompatible. Disse versjonene krever bare en patch for squashfs. Puppy 2.12 har kjerne 2.6.18.1 og har tre rettelser; squashfs, standard konsollloggnivå og shutdown fix.

Disse kommandoene for å lappe kjernen er gitt utelukkende for din egenopplæring, siden en lappet kjerne allerede er tilgjengelig...

Det første du bør gjøre først er å laste ned selve kjernen. Den er plassert for å finne en lenke til en passende nedlastingsside. Dette bør være en "gammel" kilde tilgjengelig på kernel.org eller dens speil.

Koble til Internett, last ned kjernen til /usr/src-mappen. Pakk den deretter ut:

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 denne mappen: /usr/src/linux-2.6.16.7. Du må da sørge for at denne lenken peker til kjernen:

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

Du må bruke følgende rettelser slik at du har nøyaktig samme kilde som brukes når du kompilerer kjernen for Puppy. Ellers vil du få "uløste symboler" feilmeldinger når du kompilerer driveren og deretter prøver å bruke den med valpekjernen. Påføring av squashfs-fiksen

For det andre, bruk Squashfs-plasteret. Squashfs-patchen legger til støtte for Squashfs, noe som gjør filsystemet skrivebeskyttet.

Last ned oppdateringen, squashfs2.1-patch-k2.6.9.gz, til mappen /usr/src. Merk at denne rettelsen ble laget for kjerneversjon 2.6.9, men fortsetter å fungere i 2.6.11.x-versjoner så lenge en referanse til linux-2.6.9 eksisterer. Bruk deretter rettelsen:

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

Merk, p1 har tallet 1, ikke symbolet l... (Flott spøk - ca. oversettelse)

lapp --tørrkjøring -p1< ../ squashfs2.1-patch patch -p1 < ../ squashfs2.1-patch

Klar! Kjernen er klar til å kompilere!

Kompilere kjernen

Du må skaffe en konfigurasjonsfil for kjernen. En kopi av den ligger i mappen /lib/modules.

Følg deretter disse trinnene:

Cd/ usr/ src/ linux-2.6.18.1

Hvis det er en .config-fil, kopier den midlertidig et sted eller endre navn på den.

gjøre rent gjør mrproper

Kopier .config-filen for Puppy til /usr/src/linux-2.6.18.1... Den vil ha forskjellige navn i /lib/modules, så endre navn til .config... Følgende trinn leser .config-filen og generer en ny en.

lage menuconfig

...gjør de endringene du ønsker og lagre dem. Du vil nå ha en ny .config-fil, og du bør kopiere den et sted for oppbevaring. Se merknad nedenfor

lag bzImage

Nå har du kompilert kjernen.

Flott, du finner Linux-kjernen i /usr/src/linux-2.6.18.1/arch/i386/boot/bzImage

Kompilere moduler

Gå nå inn i /lib/modules og hvis det allerede er en mappe som heter 2.6.18.1, gi nytt navn til 2.6.18.1-mappen til 2.6.18.1-gamle .

Installer nå nye moduler:

Cd/ usr/ src/ linux-2.6.18.1 lag moduler lag moduler_installer

...etter dette bør du finne de nye modulene installert i /lib/modules/2.6.18.1-mappen.

Merk at siste trinn ovenfor kjører "depmod"-programmet og dette kan gi feilmeldinger om manglende symboler for noen av modulene. Ikke bekymre deg for det – en av utviklerne har skrudd opp, og det betyr at vi ikke kan bruke den modulen.

Hvordan bruke den nye kjernen og modulene

Det er bedre hvis du har Puppy Unleashed installert. Deretter utvides tarballen og det er 2 kataloger: "boot" og "kernels".

"Boot" inneholder filstrukturen og skriptet for å lage den første virtuelle disken. Du må sette noen kjernemoduler der.

"Kjerner"-katalogen har en katalogkjerner/2.6.18.1/ , og du må erstatte modulene med de oppdaterte. Du trenger ikke å erstatte den hvis du rekompilerte den samme kjerneversjonen (2.6.18.1).

Merk at i kernels/2.6.18.1 er det en fil som heter "System.map". Du bør gi det nytt navn og erstatte det med det nye fra /usr/src/linux-2.6.18.1. Bla gjennom kernels/2.6.18.1/-mappen og du bør vite hva som må oppdateres.

Hvis du kompilerte kjernen i en full Puppy-installasjon, kan du starte på nytt med den nye kjernen. make modules_install er trinnet ovenfor for å installere nye moduler i /lib/modules/2.6.18.1 , men du må også installere en ny kjerne. Jeg starter opp med Grub, og kopierer bare den nye kjernen inn i /boot-katalogen (og gir nytt navn til filen fra bzImage til vmlinuz).

Merknad angående menykonfig. Jeg har brukt det i evigheter, så ta noen ting for gitt, men en nybegynner kan være forvirret som ønsker å avslutte programmet. Det er en meny i toppnivåmenyen for å lagre konfigurasjonen - ignorer den. Bare trykk på TAB-tasten (eller høyre piltast) for å markere "Avslutt"-knappen og trykk på ENTER-tasten. Du vil da bli spurt om du vil lagre den nye konfigurasjonen og svaret skal være Ja.


Topp