42
Design von Hardwarekomponenten und Treibern für Linux Systeme © Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 1 Struktur eines Unix-Kerns - - - - - - - - - - - - - - - - - - - -3 Stuktur der Linux-Sourcen - - - - - - - - - - - - - - - - - - -4 Glue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -5 System-Call, Interrupt, Exception - - - - - - - - - - - - - - - - - - - -5 Hardware-Mechanismen - - - - - - - - - - - - - - - - - - - - - -6 System-Call - - - - - - - - - - - - - - - - - - - - - - - - - - - -6 Interrupts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 Exception - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 13 Signale - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 14 Context-Switching- - - - - - - - - - - - - - - - - - - - - - - - - - - - 15 Timer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 18 IO-Operationen - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 19 printk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 21 Memory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23 Heap-Verwaltung - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23 User-Adreßraum <-> System-Adreßraum - - - - - - - - - - - - - - - - - 23 Datei-System - - - - - - - - - - - - - - - - - - - - - - - - - - - 25 Kernel-Datenstrukturen - - - - - - - - - - - - - - - - - - - - - - - - - 25 Gerätetreiber - - - - - - - - - - - - - - - - - - - - - - - - - - 28 Allgemeines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 28 Module - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 29 Initialisierungs- und Cleanup-Funktion - - - - - - - - - - - - - - - - - - 30 Open- und Close-Funktion - - - - - - - - - - - - - - - - - - - - - - - - 32 Read- und Write-Funktion - - - - - - - - - - - - - - - - - - - - - - - - 32 Ioctl-Funktion - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 33 Poll-Funktion - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 34 Neuen Kern konfigurieren, compilieren, installieren, booten - - - 36 Literatur - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 41

Memory ... · Stuktur der Linux-Sourcen. ... CPU einige Zustandsdaten auf den Stack und führt eine Interrupt-Routine aus (siehe…/arch/ i386/kernel/irq.c): IRQ1_interrupt:

Embed Size (px)

Citation preview

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 1

Struktur eines Unix-Kerns - - - - - - - - - - - - - - - - - - - - 3Stuktur der Linux-Sourcen - - - - - - - - - - - - - - - - - - - 4Glue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5

System-Call, Interrupt, Exception - - - - - - - - - - - - - - - - - - - - 5Hardware-Mechanismen - - - - - - - - - - - - - - - - - - - - - - 6System-Call - - - - - - - - - - - - - - - - - - - - - - - - - - - - 6Interrupts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10Exception - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 13

Signale - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 14Context-Switching- - - - - - - - - - - - - - - - - - - - - - - - - - - - 15Timer- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 18IO-Operationen - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 19printk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 21

Memory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23Heap-Verwaltung - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23User-Adreßraum <-> System-Adreßraum - - - - - - - - - - - - - - - - - 23

Datei-System - - - - - - - - - - - - - - - - - - - - - - - - - - - 25Kernel-Datenstrukturen - - - - - - - - - - - - - - - - - - - - - - - - - 25

Gerätetreiber - - - - - - - - - - - - - - - - - - - - - - - - - - 28Allgemeines- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 28Module - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 29Initialisierungs- und Cleanup-Funktion - - - - - - - - - - - - - - - - - - 30Open- und Close-Funktion - - - - - - - - - - - - - - - - - - - - - - - - 32Read- und Write-Funktion - - - - - - - - - - - - - - - - - - - - - - - - 32Ioctl-Funktion- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 33Poll-Funktion - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 34

Neuen Kern konfigurieren, compilieren, installieren, booten - - - 36Literatur - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 41

2

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 3

1. Struktur eines Unix-KernsStruktur

HardwareHardwareHardware

Network

Net-DeviceBlock-DeviceChar-Device

Filesystem

Char-Driver MemoryNet-DriverBlock-Driver

NetworkFilesystem

Process

IPC

Hardware

User-Program

Glue

Hardware Hardware

Hardware

2. Stuktur der Linux-Sourcen

4

2. Stuktur der Linux-Sourcen

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 5

3. GlueUnix besteht aus einer Reihe von verschiedenen, relativ gut voneinander separierbaren Modulen(z.B. Prozeßverwaltung, Speicherverwaltung, Dateisysteme, usw.). Alle diese Module benöti-gen jedoch eine Grundfunktionalität. Z.B. verwenden alle Module gemeinsam eine print-Funk-tion, um Fehler auf die Console zu schreiben. Diese Grundfunktionalität soll in diesem Kapitelvorgestellt werden. Sind diese Funktionen bekannt, ist es relativ einfach, mit ihrer Hilfe ein Be-triebssystem (und damit auch Device-Treiber) zu schreiben. Sie sind jedoch in ihrer Funktiona-lität so miteinander verwoben, daß ein Verständnis erst mit dem Gesamtüberblick kommt. Da-her sollte zunächst ein grober Überblick da sein, bevor man sich in die einzelnen Funktionenvertieft.

3.1 System-Call, Interrupt, ExceptionDas Betriebssystem ist „nur“ eine Library. Es läuft/laufen nur einzelne Prozesse, die bei BedarfFunktionen des OS aufrufen. Ferner generiert die Hardware Interrupts, die den normalen Pro-zeßablauf unterbrechen und bestimmte Funktionen des OS aufrufen sollen. Daraus stellen sichdie Fragen:

– Wie ruft ein Benutzer-Prozeß eine Betriebssystemfunktion auf?

– Wie werden Interrupt Routinen aufgerufen?

– Was geschieht bei Ausnahmen (Exceptions)?

Dieses wird in den folgenden Abschnitten erläutert.

3. Glue

6

3.1.1 Hardware-Mechanismen

Zusandvor undnach dem Auftreten eines System-Calls, einer Exception oder eines Interrupts(generelles Prinzip):

3.1.2 System-Call

Ruft ein Programmierer in seinem C-Programm die Funktionwrite(2) auf, so generiert derC-Compiler etwa folgendes Code-Fragment.

UserText

UserStack

UserData

SuperText

SuperStack

SuperData

ExcTable

Memory CPU

PC

UserSP

SuperSP

IVT

Mode=user

UserText

UserStack

UserData

SuperText

SuperStack

SuperData

ExcTable

Memory CPU

PC

UserSP

SuperSP

IVT

Mode=super

vorher nachher

… …Irq=enabled Irq=disabled

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 7

int fd, count, ret; /* 4-Byte-Werte */char *buffer; /* 4-Byte-Zeiger */

C-Version:

...ret = write(fd, buffer, count);...

Ix86-Assembler-Version:

...movl count, %eaxpushl %eaxmovl buffer, %eaxpushl %eaxmovl fd, %eaxpushl %eaxcall write

next: addl $12, %espmovl %eax, ret...

D.h. die Funktion write(2) wird mit folgender Stack- und Registerbelegung aufgerufen:

Die von C aus aufrufbare Funktionint write(int fd, char *buffer, intcount) ist folgendermaßen in Assembler implementiert (vergleiche glibc-Sourcen):

.globl writewrite:

pushl %ebp # set up stackmovl %esp,%ebppushl %ebxmovl $4 %eax # number for write system callmovl 8(%ebp),%ebx # fdmovl 12(%ebp),%ecx # buffermovl 16(%ebp),%edx # countint $0x80 # call operating systemtest %eax,%eax # test return valuejge 1f # return code > 0 => oknegl %eaxmovl %eax, errno # errno = -return codemovl $-1,%eax # return code = -1

nextfd

buffercount

%esp

3. Glue

8

1:popl %ebx # restore caller stackmovl %ebp,%esppopl %ebpret

Die Parameter werden dem Betriebssystem in den Registern %ebx-%edx, %esi, %edi, die Funk-tionsnummer im Register %eax übergeben. Der Assembler-Befehlint $80 generiert danneine synchrone Ausnahmebehandlung. Nach Beendigung der Ausnahmebehandlung wird imRegister %eax der Funktionswert zurückgegeben. Ist er negativ, gilt er als negative Fehlernum-mer.

Die Ix86-CPU reagiert auf denint $0x80 -Befehl mit einer Exceptionbehandlung. Dazu spei-chert sie einige Zustandsdaten auf den Stack und springt danach zu einer vorgebbare Adresse(siehe Abschnitt 3.1.1 und…/arch/i386/kernel/traps.c ). Dieser Funktionspointerzeigt auf folgende –etwas vereinfachte– Funktion (vergleiche…/arch/i386/kernel/entry.S ):

sys_call_table:.long sys_setup.long sys_exit.long sys_fork.long sys_read.long sys_write...

system_call:pushl %eax # error code (not needed here)

# ************ ## enter kernel ## ************ #cldpush %es # save registerspush %dspushl %eaxpushl %ebppushl %edipushl %esipushl %edxpushl %ecxpushl %ebx

movl $(KERNEL_DS),%edx # init segment pointermov %dx,%dsmov %dx,%es

movl %esp, %ebx # get "current" pointerandl $0xffffe000, %ebx

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 9

# ************** ## do system call ## ************** #movl $-ENOSYS,EAX(%esp) # test function numbercmpl $NR_syscalls,%eaxjae 1f # illegal function numbercall *(sys_call_table,%eax,4)# call function (use saved

# registers on stack as params)movl %eax,EAX(%esp) # save the return value

1:

# *************** ## do rescheduling ## *************** #cmpl $0, need_resched(%ebx)je 1f

call schedule # reschedule1:

# ******************** ## do signal processing ## ******************** #cmpl $0, sigpending(%ebx) # signal pending?je 1f # jump if no active signals

movl %esp, %eax # address of saved reg-setxorl %edx, %edx # clear %edxcall do_signal # do signal processing

1:

# ************ ## leave kernel ## ************ #popl %ebx # restore registerspopl %ecxpopl %edxpopl %esipopl %edipopl %ebppopl %eaxpop %dspop %espop %fspop %gsaddl $4,%espiret

Diese Funktion ruft dann die eigentliche Betriebssystemfunktion (sys_exit , sys_fork ,sys_read , sys_write , usw.) auf. Als Beispiel sei hiersys_read undsys_write in …/fs/read_write.c genannt.

3. Glue

10

3.1.3 Interrupts

Interrupt-Handler

Beim Anliegen eines Interrupt-Anforderung der externen Hardware-Komponenten legt dieCPU einige Zustandsdaten auf den Stack und führt eine Interrupt-Routine aus (siehe…/arch/i386/kernel/irq.c ):

IRQ1_interrupt:pushl $1-256jmp common_interrupt

IRQ2_interrupt:pushl $2-256jmp common_interrupt

...

IRQ15_interrupt:pushl $15-256jmp common_interrupt

common_interrupt:# ************ ## enter kernel ## ************ #... # see above

# *********************** ## do interrupt processing ## *********************** #call do_IRQ

# *************** ## do rescheduling ## *************** #... # see above

# ******************** ## do signal processing ## ******************** #... # see above

# ************ ## leave kernel ## ************ #... # see above

Die mit dieser Assembler-Routine aufgerufene Procedurdo_IRQ sieht sehr vereinfacht1 wiefolgt aus (Original in…/arch/i386/kernel/irq.c ):

1. Die Original-Funktion ist wesentlich komplizierter, da sie Multiprozessor-fähig ist, verschiedene Inter-rupt-Controller und gemeinsam genutzte Interrupts unterstützt. Das Verständnis der hier angegebenenVersion reicht zum Schreiben von Device-Treibern aus.

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 11

void do_IRQ(struct pt_regs regs){

int irq = regs.orig_eax & 0xff;

/* mask and ack interrupt controller */if (irq < 8) {

cached_21 |= 1 << irq;inb(0x21); outb(cached_21, 0x21); outb(0x20, 0x20);

} else {...

}

/* do interrupt processing */... (*handler[irq])(irq, dev_id[irq], &regs); ...

/* restore interrupt controller mask */if (irq < 8) {

cached_21 &= ~(1 << irq);outb(cached_21, 0x21);

} else {...

}}

Die Funktionspointerhandler [] und Daten-Pointerdev_id [] können über die Funktionrequest_irq beschrieben und durch die Funktionfree_irq wieder gelöscht werden (sie-he…/arch/i386/kernel/irq.c ):

int request_irq(unsigned int irq,void (*handler)(int irq, void *dev_id, struct pt_regs *r),unsigned long irqflags,const char *devname,void *dev_id);

irqflags ist im Normalfall 0. Die weiteren Möglichkeiten (Interrupt-SharingSA_SHIRQ,Fast-InterruptsSA_INTERRUPT, Random-GeneratorSA_SAMPLE_RANDOMusw.) sollenhier nicht behandelt werden.

void free_irq(unsigned int irq, void *dev_id);

Registrierte Interrupts können durch „cat /proc/interrupts “ dem Benutzer angezeigtwerden.

CPU-Interrupt-Enable-Bit

Eine Möglichkeit, während der Abarbeitung von CPU-Instruktionen von Interrupts nicht unter-brochen zu werden, ist, das Interrupt-Erlaubnis-Bit in der CPU zu löschen (Assembler-Instruk-tion cli ). Nach dem kritischen Abschnitt muß dies Bit dann wieder gesetzt werden (sti ). Deralte Zustand dieses (und anderer) Bits kann beim Ix386-Prozessor nur über die Instruktions-kombination „pushfl; popl … “ abgefragt bzw. über „pushl …; popfl “) restauriertwerden. Daher wird um kritische Abschnitte herum meist folgender Code programmiert:

3. Glue

12

…pushfl # save status registerpopl %edxcli # clear interrupt enable bit… # do critical sectionpushl %edx # restore status registerpopfl…

Um die C-Programmierung zu erleichtern, existieren Macros. Mit Hilfe dieser C-Macros kön-nen diese Assembler-Befehle direkt in den C-Code eingebaut werden. Sie finden sich in…/include/asm-i386/system.h :

Flags speichern (entspricht Ix86: pushfl; popl flags)

void save_flags(unsigned long &flags);

Flags restaurieren (entspricht Ix86: pushl flags; popfl)

void restore_flags(unsigned long &flags);

Interrupts für CPU disablen (entspricht Ix86: cli)

void cli(void);

Interrupts für CPU enablen (entspricht Ix86: sti)

void sti(void);

In C sieht das obige Code-Fragment damit wie folgt aus:

unsigned long flags;…save_flags(flags); /* NOT save_flags(&flags); !! */cli();… /* do critical section */restore_flags(flags); /* NOT restore_flags(&flags); */…

Interrupt-Controller-Masken-Register

Das Interrupt-Enable-Bit im Statusregister des Prozessors kann sehr schnell gelöscht bzw. ge-setzt werden. Mit dieser Methode kann jedoch nur sichergestellt werden, daß der entsprechendeProzessor, der „cli “ abgearbeitet hat, nicht durch einen Interrupt unterbrochen werden kann.Während der Zeit, in der das Interrupt-Bit gelöscht ist, werden jedoch keinerlei Interrupts bear-beitet. Soll ein bestimmter Interrupt über einen längeren Zeitraum gesperrt bleiben, ist es sinn-voll, nicht alle Interrupts über „cli“ zu sperren, sondern nur den einen Interrupt durch den Inter-rupt-Controller sperren zu lassen. Dazu kann der Interrupt-Controller des Systems umprogram-miert werden. In diesem Controller können Bits gelöscht bzw. gesetzt werden, um Interruptsbestimmter Geräte zu sperren bzw. zu erlauben. Die Prozeduren finden sich unter…/arch/i386/kernel/irq.c :

Bestimmten Interrupt disablen:

void disable_irq(unsigned int irq_nr);

Bestimmten Interrupt enablen:

void enable_irq(unsigned int irq_nr);

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 13

3.1.4 Exception

Tritt während des Programmlaufes eine Division durch Null, ein Page-Fault oder ähnliches auf,legt die CPU einige Status-Bytes auf den Stack und bearbeitet eine entsprechende Exception-Behandlungs-Routine. Dies soll am Beispiel der Division dargestellt werden (vergleiche…/arch/i386/kernel/entry.S ):

divide_error:pushl $0 # error code

# ************ ## enter kernel ## ************ #... # see above

# *********************** ## do exception processing ## *********************** #movl ORIG_EAX(%esp),%eax # error codemovl %esp,%edx # address of register setpushl %eaxpushl %edxcall do_divide_error # call do_divide_error(regs)addl $8,%esp

# *************** ## do rescheduling ## *************** #... # see above

# ******************** ## do signal processing ## ******************** #... # see above

# ************ ## leave kernel ## ************ #... # see above

do_divide_error behandelt dann den Fehler (vergleiche…/arch/i386/kernel/traps.c ):

voiddo_divide_error(struct pt_regs *regs, long error_code){

current->tss.error_code = error_code;current->tss.trap_no = 0;force_sig(SIGFPE, current);die_if_kernel("divide error", regs, error_code);

}

3. Glue

14

3.2 SignaleEin wichtiges Konzept unter Unix sind die Signale. Mit ihnen ist es möglich, Interrupts aufHardware-Ebene auf die Software-Ebene abzubilden. Signale können (je nach Wunsch desempfangenden Prozesses, siehesigaction (2)) bestimmte Signal-Handler aufrufen. Stan-dardmäßig sind die Signal-Handler jedoch so eingestellt, daß ein zugestelltes Signal den Prozeßabbricht. Die_exit (2)-Funktion ist sozusagen der Default-Signal-Handler. Die Signalbehand-lung ist –wie die Interrupt-Behandlung auch– vollständig asynchron. Der empfangende Prozeßkann jedoch eine Signalbehandlung für eine Zeit unterdrücken (sigprocmask (2)) oder be-stimmte Signale komplett ignorieren (sigaction (2)). Werden Signale zeitweilig unter-drückt, wird die Zustellung der Signale aber gespeichert. D.h. daß die Signale im Moment derRücknahme der Unterdrückung wirksam werden. Es wird jedoch maximal ein einzelnes Signal(pro Typ) gespeichert, selbst wenn mehrere Signale eines Typs zugestellt wurden. Dies ent-spricht der Hardware-Interrupt-Behandlung. Die Zustellung eines Signales aus einem Benutzer-Programmes geschieht durch die Funktionkill (2).

Im Betriebssystem-Kern verschickt ein Aufruf der Funktionkill_proc ein Signal an einenbestimmten Prozeß.

int kill_proc(int pid, int sig, int priv);

pid gibt den empfangenden Prozeß an;sig ist das zuzustellende Signal (siehe<asm/si-gnal.h> ). priv gibt an, ob eine Rechteüberprüfung stattfinden soll. Istpriv = 1 , gilt dieseÜberprüfung als bereits durchgeführt.

Soll einer ganzen Prozeßgruppe ein Signal zugestellt werden (Beispiel: Benutzer tippt Control-C auf der Console), kann dies durch Aufruf der Funktionkill_pg erreicht werden.

int kill_pg(int pgrp, int sig, int priv);

Die Parameter entsprechen der Funktionkill_proc . Statt des Parameterspid für die Pro-zeß-ID gibt es jetzt jedoch den Parameterpgrp für die ID der Prozeßgruppe.

Die Signale, die einem Prozeß zugestellt werden, sind zunächst im Prozeßkontrollblock einge-tragen. Die Abarbeitung der zugestellten Signale geschieht durch den empfangenden Prozeßselbst. Ein Prozeß, der im Kern in einer Endlosschleife hängt und seine Signale nicht aktiv ab-fragt, kann nicht terminiert werden!

Ein Code-Fragment zur Abfrage der zur Zeit zugestellten und zu bearbeitenden Signale lautetdaher wie folgt:

if (signal_pending(current)) {/* Signal zugestellt und zu bearbeiten */

} else {/* kein Signal zugestellt und zu bearbeiten */

}

(current ist ein Zeiger auf den Kontrollblock des aktuell laufenden Prozesses.)

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 15

3.3 Context-Switching

schedule

Die Schedule-Routine ist vielleicht der Schlüssel zum Verständnis, wie Multitasking funktio-niert. Leider ist diese Routine vielfach mit Scheduling-Policies, Locking usw. aufgebläht unddurch Optimierungsmaßnahmen so „verunstaltet“, daß die eigentliche Funktion nicht mehrsichtbar ist. Deshalb an dieser Stelle eine Routine in Pseudo-Code, um das Verständnis zu er-leichtern. Das Original findet sich im Linux-Kernel in…/kernel/sched.c .

voidschedule(void){

unsigned long flags;struct task_struct *prev;struct task_struct *next;

/* disable interrupts */save_flags(flags); cli;

/* lookup last process */prev = current;

/* lookup next process (highest priority) */next = ...;

if (prev != next) {/* save registers to process structure *prev */if (_setjmp(prev->regs)) goto end;

/* unmap all user pages [and system stack] */...

current = next;

/* map all user pages [and system stack] */...

/* load registers from process structure *next */_longjmp(next->regs, 1);

end:;}

/* restore interrupts */restore_flags(flags);

}

sleep_on

Wenn ein Prozeß auf ein bestimmtes Ereignis wartet, sollte er die CPU abgeben, damit andererechenbereite Prozesse weiterarbeiten können. Die Bereitschaft zur Aufgabe der CPU kanndurch Aufruf der Prozedur sleep_on , sleep_on_timer ,interruptible_sleep_on bzw. interruptible_sleep_on_timer geschehen.Als Parameter dieser Routine wird eine Adresse übergeben, die stellvertretend für das Ereignisist, auf das gewartet wird. Hier wird eine leicht modifizierte Version dieser Routine angegeben(vergleiche…/kernel/schedul.c ):

3. Glue

16

voidsleep_on(struct wait_queue **wqa){

unsigned long flags;

current->state = TASK_UNINTERRUPTIBLE;current->wqa = wqa;save_flags(flags); sti();schedule();restore_flags(flags);

}

Mit der Prozedurinterruptible_sleep_on geht der Prozeß nicht in den ZustandTASK_UNINTERRUPTIBLEsondernTASK_INTERRUPTIBLE. Er kann dann sowohl durchdie zugehörigewake_up_interruptible -Prozedur als auch durch ein zugestelltes Signalaufgeweckt werden. Den Unterschied kann ein Prozeß anhand der ihm zugestellten Signale undder aktuellen Signal-Maske feststellen (signal_pending (current) ).

wake_up

Die wake_up bzw.wake_up_interruptible -Routine weckt alle(!) Prozesse wieder auf,die auf ein bestimmtes Ereignis gewartet haben. Welcher der aufgeweckten Prozesse als ersteranläuft, ist von den eingestellten Prioritäten abhängig. Eine vereinfachte Version der Prozedurlautet (Original in…/kernel/sched.c ):

voidwake_up(struct wait_queue **wqa){

struct task_struct *p;

for (p = &task[0]; p != 0; p = p->next) {if (p->state == TASK_UNINTERRUPTIBLE && p->wqa == wqa) {

p->state = TASK_RUNNING;}

}}

Beispiele

sleep_on und wake_up können sowohl für die Interprozeßkoordinierung als auch für dieSynchronisierung mit Hardware-Interrupts verwendet werden. Zunächst ein Beispiel für die In-terprozeßkoordinierung:

static int buffer_used = 0;static struct wait_queue *buffer_used_changed = 0;…

/* wait for buffer */while (buffer_used) {

sleep_on(&buffer_used_changed);}

/* allocate buffer */buffer_used = 1;

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 17

/* use buffer */...

/* free buffer */buffer_used = 0;wake_up(&buffer_used_changed);

Hier ein Beispiel für eine Funktion, die aus einem Puffer liest, der in einer Interrupt-Routine mitDaten gefüllt wird:

static int count;static struct wait_queue *count_changed = 0;

voidxxx_intr(...){

/* put byte into buffer */...count++;wake_up_interruptible(&count_changed);

}

intxxx_read(...){

unsigned long flags;int ret;

/* disable interrupts */save_flags(flags); cli();

/* wait for data */while (count==0 && !(signal_pending(current))) {

interruptible_sleep_on(&count_changed);}

/* abort current system call / get data */if (signal_pending(current)) {

ret = -ERESTARTSYS;

} else {/* copy byte from system buffer to user buffer */...count = 0;ret = 1;

}

/* restore interrupts */restore_flags(flags);

return (ret);}

Bemerkung 1:

3. Glue

18

Seit der Version 2.2.0 gibt es zusätzlich die Funktionenschedule_timout ,sleep_on_timout , interruptible_sleep_on_timeout . Mit Hilfe dieser Funktio-nen kann die maximale Wartezeit begrenzt werden. Timer (siehe unten) sind nicht mehr explizitnotwendig.

Bemerkung 2: Wichtig:Es istnichterlaubt, während einer Interrupt-Abarbeitung eine dersleep_on -Prozeduren auf-zurufen, da dies zu einem Deadlock führen kann. Es wird nämlich der Prozeß, in dessen Kontextdie Interrupt-Routine aufgerufen wurde, schlafen gelegt. Genau dieser Prozeß könnte jedoch dieUrsache beheben, aufgrund derer die Interrupt-Routine sich blockieren wollte. Beispiel: EineInterrupt-Routine möchte Zeichen in einen Puffer eintragen. Da dieser schon voll ist, versuchtdie Interrupt-Routine sich selbst zu blockieren. Tut sie dies, lägt sie eventuell genau den Prozeßschlafen, der den Puffer wieder leeren könnte…

3.4 TimerHäufig werden in Device-Treibern Timer gebraucht. Führt ein Gerät eine Operation nicht inner-halb einer bestimmten Zeit aus, ist ein Fehler aufgetreten, auf den reagiert werden muß. Dazumuß dann eine Routine aufgerufen werden, die die Fehlerbehandlung durchführt und eventuelleinen wartenden Prozeß aufweckt. Zu diesem Zweck verwaltet Linux eine verkettete Liste vonTimern (siehe…/kernel/sched.c bzw. …/include/linux/timer.h ). Jeder Timer-Listen-Eintrag enthält die folgenden Variablen:

struct timer_list {struct timer_list *next;struct timer_list *prev;unsigned long expires;unsigned long data;void (*function)(unsigned long);

};

Soll eine Funktion nach einer Zeit aufgerufen werden, so muß eine Stuktur des Typstimer_list angelegt, initialisiert und in die verkettete Liste eingefügt werden:

#include <linux/timer.h>

voidxxx_timeout(unsigned long data){}

…struct timer_list t;

init_timer(&t);t.expires = …;t.data = 0;t.function = xxx_timeout;add_timer(&t);…

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 19

Dasexpires -Feld enthält den Zeitpunkt, zu dem die Timeout-Routine aufgerufen werdensoll. Dies ist jedoch nicht die übliche Unix-Zeitmessung (Sekunden seit 1.1.1970 GMT), son-dern eine Zeitmessung, gestartet beim Booten des Rechners. Gezählt werden auch nicht Sekun-den, sondern die Anzahl der Uhr-Interrupts. Die aktuelle Zeit enthält die globale Variablejif-fies (siehe:…/include/linux/sched.h ). Die KonstanteHZ (siehe:…/include/asm/param.h ) beschreibt die Anzahl der Uhr-Interrupts pro Sekunde. Um z.B. nach20 Sekunden die Timeout-Routine zu starten, muß in dasexpire -Feld der Wertjif-fies+20*HZ geschrieben werden.

Wird die gleiche timeout-Funktion für mehrere (ähnliche) Zwecke verwendet, kann über dasdata -Feld der Routine noch ein (unsigned long -) Parameter mitgeteilt werden (z.B. dieNummer des Gerätes, dessen Timeout abläuft).

Wird der Timer nicht mehr gebraucht, muß er mit der Funktiondel_timer wieder aus derTimer-Liste entfernt werden. Als Return-Wert dieser Funktion erhält der Aufrufer eine 1, wennder Timer noch nicht abgelaufen war (ansonsten eine 0).

3.5 IO-Operationen

IO-Zugriffe

Je nach verwendeter Hardware kann auf die Hardware-Register der Einsteckkarten über norma-le Schreib-/Lese-Operationen oder nur über spezielle Hardware-Schreib-/Lese-Operationen zu-gegriffen werden.

Liegen die Hardware-Register im normalen Speicheradreßraum ist z.B. folgende C-Program-mierung möglich:

struct chipX {unsigned long reg0;unsigned short reg1;unsigned reg2: 1; /* bit-field */unsigned reg3: 7; /* bit-field */

…} *chipX = (struct chipX *) chipXaddr;

chipX->reg0 = 0;… = chipX->reg1;chipX->reg2 = 1;… = chipX->reg3;

Hier kann sehr sauber über die normalen C-Statements auf die Register zugegriffen werden.

Entspricht ein Hardware-Register-Zugriff nicht dem normalen Speicherzugriff (z.B. beimIx86), muß über spezielle IO-Befehle des Prozessors auf die Register zugegriffen werden. BeimIx86 sind dies die Befehleinb , outb , inw , outw , usw. Da diese Befehle nicht vom C-Com-piler verwendet werden, muß über (Inline-) Assembler-Funktionen zugegriffen werden. Hierfürexistieren Macros in…/include/asm-i386/io.h . Sie entsprechen den Ix86-IO-Befeh-len:

3. Glue

20

unsigned char inb(unsigned short port);unsigned short inw(unsigned short port);unsigned long inl(unsigned short port);void outb(unsigned char value, unsigned short port);void outw(unsigned short value, unsigned short port);void outl(unsigned long value, unsigned short port);

Obiges Beispiel in dieses Programmiermodell umgesetzt lautet:

#define chipX_reg0 (chipXaddr + 0)#define chipX_reg1 (chipXaddr + 4)#define chipX_reg2_3 (chipXaddr + 6)…

outl(0, chipX_reg0);… = inw(chipX_reg1);outb((1 << 7) | inb(chipX_reg2_3), chipX_rerg2_3);… = inb(chipX_reg2_3) & 0x7f;

DMA

Die DMA-Fähigkeiten der PC-Hardware kann mit Hilfe der DMA-Verwaltungsroutinen ausge-nutzt werden. Diese seien hier der Vollständigkeit halber aufgeführt (siehe…/kernel/dma.c und…/include/asm-i386/dma.h ). Außerrequest_dma undfree_dma , dieder Resource-Verwaltung dienen, sind dies alles Makros, die direkt den entsprechenden DMA-Contoller programmieren.

int request_dma(int dmanr, const char *device_id);

void free_dma(int dmanr);

void enable_dma(unsigned int dmanr);

void disable_dma(unsigned int dmanr);

void clear_dma_ff(unsigned int dmanr);

void set_dma_mode(unsigned int dmanr, char mode);

set_dma_page(unsigned int dmanr, char pagenr)

set_dma_addr(unsigned int dmanr, unsigned int a);

set_dma_count(unsigned int dmanr, unsigned int count);

get_dma_residue(unsigned int dmanr);

IO-Bereiche

Jede Hardware braucht zur Programmierung eine bestimmte Anzahl von Registern. Diese wer-den durch Zugriffe des Prozessors auf bestimmte (IO-) Adressen selektiert. Es versteht sich vonselbst, daß für einen geregelten Ablauf nicht mehrere Hardware-Register durch denselben Pro-zessor-Zugriff gleichzeitig gelesen bzw. gleichzeitig beschrieben werden dürfen. Daher müssenalle Hardware-Register an verschiedenen Adressen auf dem IO-Bus liegen. Da der IO-Adreß-bereich (speziell beim Ix86) sehr begrenzt ist, kann nicht für jeden Typ von Hardware ein be-stimmter Adreßbereich im Voraus reserviert werden. Aus dem Grund enthalten alle Einsteck-karten Jumper (o.ä.), um die Basisadresse der Register einzustellen.

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 21

Die Aufgabe der Device-Treiber ist es jetzt, die Steckkarten „wiederzufinden“. Dazu testet einTreiber der Reihe nach alle Adressen, unten denen eine vom Treiber unterstützte Karte zu fin-den sein könnte. Der Treiber schreibt in die vermuteten Register bestimmte Werte und testet, obsich die übrigen Register entsprechend dem unterstützten Kartentyp verhalten. Wenn ja, ist ander Adresse eine unterstützte Karte gefunden. Diese kann dann entsprechend initialisiert wer-den.

Wenn eine Karte initialisiert ist, muß der Teiber den Zugriff anderer Treiber auf diesen IO-Be-reich sperren. Sonst versuchen andere Treiber unter der gleichen Adresse ihre Karte zu suchenund schreiben in die soeben initialisierten Register neue (meist falsche) Werte. IO-Bereiche las-sen sich unter Linux mit der Funktioncheck_region testen, ob sie schon belegt sind, mitrequest_region sperren bzw. mitrelease_region freigeben (Funktionen siehe…/kernel/resource.c und…/include/linux/ioport.h ):

int check_region(unsigned int from, unsigned int extent);

void request_region(unsigned int from, unsigned int extent,const char *name);

void release_region(unsigned int from, unsigned int extent);

Registrierte IO-Bereiche können mit „cat /proc/ioports “ von der Benutzerebene abge-fragt werden.

3.6 printkDie Wahrscheinlichkeit, daß ein Programm auf Anhieb das tut, was man von ihm erwartet, istbei den allermeisten Programmieren praktisch null. Selbst wenn der Compiler keine (syntakti-schen) Fehler mehr findet, werden meist noch (semantische) Fehler im Programm vorhandensein. Normale Benutzerprogramme können mit Debuggern untersucht werden. Debugger ver-wenden dazu jedoch Funktionen des Betriebssystems (siehe Unix-Manualptrace (2)). Umdas Betriebssystem selbst zu debuggen, kann dieptrace -Funktion natürlich nicht verwendetwerden.

Es ist nicht möglich, Prozesse zu stoppen und auf Tastendruck weiterlaufen zu lassen. Es kön-nen Nachrichten auf den Bildschirm ausgegeben werden. Dazu kann die Funktionprintk ver-wendet werden.

Der C-Prototyp lautet:

void printk(const char *fmt, ...);

Die Bedeutung der Parameter und die Semantik der Funktion stimmt im wesentlichen mit derprintf -Funktion der Standard-libc-Library überein.

Vielfach werden die Debug-Nachrichten jedoch nur während der Entwicklungsphase benötigt.Danach sollten sie nicht mehr auftreten, um nicht unnötig Daten auf die Console zu schreiben(Bildschirm des Benutzers gerät durcheinander und hoher Rechenaufwand). Daher entstehenhäufig Konstuktionen wie:

3. Glue

22

…#define DEBUG 0…#if DEBUG

printk("…");#endif…

Die „#if DEBUG … #endif “-Statements machen den Programmcode jedoch sehr unüber-sichtlich (was wird jetzt ausgeführt und was nicht?). Daher die Empfehlung:

…#define DEBUG 0#if DEBUG#define dprintk(args...) printk(## args)#else#define dprintk(args...)#endif…

dprintk("…");…

Alternativ können auch die Macros in…/include/linux/kernel.h (pr_info bzw.pr_debug ) verwendet werden.

Alle vom Kern überprintk ausgegebenen Meldungen können nochmals nachträglich sichtbargemacht werden durch:

vrsieh@faui31c:~$ dmesgConsole: 16 point font, 400 scansConsole: colour VGA+ 80x25, 1 virtual console (max 63)

>>> kernel: initializing PCI devices <<<pcibios_init : BIOS32 Service Directory structure at0x000faa80pcibios_init : BIOS32 Service Directory entry at 0xfaf00…>>> kernel: mounting root filesystem <<<VFS: Mounted root (ext2 filesystem) readonly.Adding Swap: 130748k swap-space (priority -1)

Voraussetzung für ein funktionierendesprintk sind jedoch:

– ein vorhandener Stack (nicht während der Prozeßumschaltung verwenden!)

– genügend Zeit (nicht in zeitkritischen Abschnitten verwenden!)

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 23

4. MemoryDas Memory-Subsystem behandelt alle mit der MMU zusammenhängenden Aufgaben. Diemeisten dieser Aufgaben sind für diese Vorlesung uninteressant (z.B. sbrk- und mmap-System-Calls). Für die Programmierung von Treibern sind jedoch einige Kenntnisse über die Speicher-verwaltung notwendig. Zum Beispiel benötigen viele Treiber extra Speicher, der dynamisch al-loziert und freigegeben werden kann. Weiterhin muß ein Treiber die Möglichkeit haben, mit ei-nem im User-Mode des Prozessors laufenden Benutzerprogramm Daten auszutauschen.

4.1 Heap-VerwaltungÄhnlich den normalen libc-Funktionen malloc und free existieren im Kern Funktionenkmal-loc bzw. kfree (siehe…/mm/kmalloc.c und …/include/linux/malloc.h ). MitHilfe dieser Funktionen kann Speicherplatz dynamisch angefordert und wieder freigegebenwerden. kmalloc muß zusätzlich zur normalen Angabe, wieviele Bytes benötigt werden, nocheine Priorität übergeben werden. Die verschiedenen möglichen Prioritäten sind in…/inclu-de/linux/mm.h definiert. Soll der allozierte Speicherbereich für DMA-Übertragung geeig-net sein, muß zusätzlich das BitGFP_DMA gesetzt sein.

In den normalen Treiber-Funktionen ist

… = kmalloc(size, GFP_KERNEL);

in Interrupt-Routinen

… = kmalloc(size, GFP_ATOMIC);

sinnvoll. Bei DMA-Betrieb „GFP_KERNEL | GFP_DMA“ bzw. „GFP_ATOMIC |GFP_DMA“. Da Kernel-Speicher jedoch nicht geswappt werden kann (sonst läuft eine Interrupt-Routine eventuell ins Leere) muß immer mit einem NULL-Pointer als Rückgabewert gerechnetwerden. Große Speicherbereiche sollten nicht bzw. nur kurzfristig alloziert werden.

Der Prototyp vonkfree lautet:

void kfree(void *block);

4.2 User-Adreßraum <-> System-AdreßraumAus Sicherheitsgründen werden User- und System-Adreßraum strikt voneinander getrennt. EinBenutzerprogramm hat dadurch keinerlei Zugriff auf die Variablen der Kernels. Umgekehrt be-deutet dies, daß aber auch der Kern nicht mit normalen Schreib- bzw. Lesezugriffen auf dieUser-Variablen zugreifen kann. Bedingt durch die System-Mode-Rechte im Betriebssystemkann man von dort aus jedoch über bestimmte Funktionen des Prozessors auf die User-Pageszugreifen (siehe…/include/asm-i386/uaccess.h ):

int copy_to_user(char *user, char *system, int cnt);

int copy_from_user(char *system, char *user, int cnt);

Die Prozedurcopy_to_user kopiert aus dem System-Adreßraum von Adressesystemcnt Bytes in den User-Adreßraum ab Adresseuser . copy_from_user kopiert entspre-chend aus dem User-Adreßraum in den System-Adreßraum. Die Funktionen geben die Anzahl

4. Memory

24

der nicht korrekt gelesenen Bytes zurück. Ist die Anzahl der nicht gelesenen bzw. nicht ge-schriebenen Bytes nicht 0, ist dies ein Zeichen für eine falsche Adresseuser . Normalerweisesollte dann dem Aufrufenden Programm eineEFAULT-Fehlermeldung zurückgegeben werden.

Möchte ein Prozeß im Kern aus einem Speicherbereich (Adressesystem ) in seinen eigenenUser-Adreßraum (Adresseuser ) etwas hineinschreiben (count Bytes), so wird er folgendenPseudo-Code ausführen:

retval = copy_to_user(user, system, count);if (retval != 0) {

... error ...}

Entsprechend gilt für’s Lesen aus dem User-Adreßraum:

retval = copy_from_user(system, user, count);if (retval != 0) {

... error ...}

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 25

5. Datei-System

5.1 Kernel-Datenstrukturen

Prozeß-Tabelle

Jedem Prozeß ist ein Eintrag in der Prozeßtabelle zugeordnet. In dieser Struktur werden alleProzeßeigenschaften gespeichert. Im folgenden eine Vereinfachung (Original siehe…/inclu-de/linux/sched.h ):

struct task_struct {enum {

TASK_RUNNING,TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE

} state;int sigpending; /* got signal */pid_t pid; /* process id */…uid_t uid,euid,suid; /* user id */gid_t gid,egid,sgid; /* group id */int ngroups;gid_t groups[NGROUPS]; /* groups */…struct fs_struct {

…int umask;struct dentry *root;struct dentry *pwd;

} *fs;struct files_struct {

…struct file *fd[NR_OPEN];/* pointer to open files */

} *files;…

}

current ist ein Pointer auf die obige Struktur des aktuell laufenden Prozesses.

Open-File-Tabelle

(siehe …/include/linux/fs.h):

struct file_operations {loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char *, size_t, loff_t *);ssize_t (*write) (struct file *,const char *, size_t,

loff_t *);unsigned int (*poll) (struct file *,

struct poll_table_struct *);int (*ioctl) (struct inode *, struct file *,

5. Datei-System

26

unsigned int, unsigned long);int (*open) (struct inode *, struct file *);void (*release) (struct inode *, struct file *);…

};

struct file {loff_t f_pos; /* read/write position */unsigned short f_flags; /* flags O_* (fcntl.h) */unsigned short f_count; /* number of references */struct dentry *f_dentry; /* pointer to object */struct file_operations *f_op; /* pointer to possible */

/* operations */…

};

struct file_operations {loff_t (*llseek)(struct file *, loff_t, int);ssize_t (*read)(struct file *, char *, size_t, loff_t *);ssize_t (*write)(struct file *, const char *, size_t,

loff_t *);int (*readdir)(struct file *, void *, filldir_t);unsigned int (*poll)(struct file *,

struct poll_table_struct *);int (*ioctl)(struct inode *, struct file *,

unsigned int, unsigned long);...int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);...

};

struct dentry {struct inode *d_inode; /* where the name belongs to */struct dentry * d_parent; /* parent directory */struct list_head d_child; /* child of parent list */char d_iname[DNAME_INLINE_LEN]; /* name of object */…

};

Inode-Tabelle

Eine I-Node (internalnode) bezeichnet ein Objekt in Unix. Dieses Objekt kann eine Datei, einePipe, ein Hardware-Device o.ä. sein.

struct inode {kdev_t i_dev; /* disk device */unsigned long i_ino; /* location on disk */umode_t i_mode; /* type and rwx */nlink_t i_nlink; /* reference counter */uid_t i_uid; /* user id (owner) */gid_t i_gid; /* group id (owner) */kdev_t i_rdev; /* major/minor number */

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 27

off_t i_size; /* size of file */time_t i_atime; /* last access time */time_t i_mtime; /* last modification time */time_t i_ctime; /* last meta info change time */…

}

Die I-Nodes sind im normalen Dateisystem gespeichert und über Namen auffindbar. Ihr Inhaltkann (zum großen Teil) über dasls (1)-Kommando sichtbar gemacht werden. Beispiel:

~>ls -lin /dev/tty0141260 crw-rw---- 1 10341 10300 4, 0 Sep 30 16:16 /dev/tty0

/dev/tty0

• ist das 141260-te Objekt im Dateisystem (i_ino)

• ist ein character-Device und besitzt die Zugriffsrechte rw-rw---- (i_mode)

• hat eine einzelne Namensreferenz im Dateisystem (i_nlink)

• gehört dem Benutzer mit der Nummer 10341 (vrsieh) (i_uid)

• gehört der Gruppe mit der Nummer 10300 (i3wiss) (i_gid)

• wird verwaltet vom Device-Treiber mit der Nummer 4 (Parameter 0) (i_rdev)

• wurde zuletzt am 30. September beschrieben (i_mtime)

Die möglichen Operationen auf den Inodes werden durch die folgende Struktur (siehe…/include/linux/fs.h ) beschrieben. Ein Pointer auf eine derartige Struktur ist in jeder In-ode enthalten.

struct inode_operations {struct file_operations *default_file_ops;int (*create)(struct inode *, struct dentry *, int);struct dentry (*lookup)(struct inode *, struct dentry *);int (*link)(struct dentry *, struct inode *,

struct dentry *);int (*unlink)(struct inode *, struct dentry *);int (*symlink)(struct inode *, struct dentry *,

const char *);int (*mkdir)(struct inode *, struct dentry *, int);…

};

6. Gerätetreiber

28

6. Gerätetreiber

6.1 AllgemeinesGerätetreiber besitzen die Aufgabe, Kommandos des Benutzerprogramms auszuführen und An-forderungen der Hardware zu bedienen. Sie bilden damit die Schnittstelle zwischen der Hard-ware und den Benutzerprogrammen. Unter Unix werden alle Zugriffe des Benutzers auf dieHardware über das Dateisystem geleitet. Gerätetreiber bilden damit genauer das Interface zwi-schen der Hardware und dem Dateisystem. Es können grob drei Kategorien von Treibern unter-schieden werden:

1. Character-Gerätetreiber

Verwendung wie eine normale Datei (Dateiname normalerweise/dev/… ). Eine Ausnah-me besteht darin, daß in der Gerätedatei der Schreib- und Lesezeiger nicht vor- und zu-rückgesetzt werden kann. Beispiele für eine Zeichenorientierte Schnittstelle sind die seri-elle oder parallele Schnittstelle und die Console.

2. Block-Gerätetreiber

Auch Block-Devices entsprechen in gewissen Bereichen normalen Dateien (Dateinameebenfalls normalerweise/dev/… ). Hier kann der Schreib-/Lesezeiger mit lseek(2) mo-difiziert werden. Schreib- und Leseoperationen müssen jedoch in Blockgröße erfolgen(Platten normalerweise 512 Byte, CDROM’s 2048 Byte). Linux erlaubt es, eine beliebigeAnzahl von Bytes von blockorientierten Geräten zu lesen.

3. Netzwerk-Gerätetreiber

Netzwerk-Devices können Nachrichten über die verwaltete Hardware verschicken undempfangen. Der empfangene bzw. gesendete Datenstrom (Bytes) wird durch geeigneteMaßnahmen in einzelne Nachrichten unterteilt. Die Nachrichten stammen nicht direktvom Benutzerprogramm. Die vom Benutzerprogramm verschickten Nachrichten werdenvielmehr durch viele Protokollschichten über dem eigentlichen Treiber erweitert (z.B. umAbsender- und Empfänger-Adresse, sowie um eine Check-Summe).

Character- und Block-Devices unterstützen u.a. die folgenden Operationen:

loff_t dev_llseek(struct file *file,loff_t off,int whence

);

ssize_t dev_read(struct file *file,char *buffer,size_t count,loff_t *off

);

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 29

ssize_t dev_write(struct file *file,const char *buffer,size_t count,loff_t *off

);

unsigned int dev_poll(struct file *file,struct poll_table_struct *pts

);

int dev_ioctl(struct inode *inode,struct file *file,unsigned int cmd,unsigned long arg

);

int dev_open(struct inode *inode,struct file *file

);

int dev_release(struct inode *inode,struct file *file

);

void dev_intr(int irq,void *dev_id,struct pt_regs *regs

);

Nicht alle dieser Funktionen müssen unterstützt werden (z.B. unterstützen Character-Deviceskein lseek , read-only-Devices keinwrite ). Die Funktionen entsprechen in ihrer Funktiona-lität in etwa den bekannten C-Funktionenread (2),write (2), ioctl (2), usw.dev_intr istder Name der Routine, die die Interrupts der Hardware behandelt.

Netzwerk-Gerätetreiber haben eine vollkommen andere Schnittstelle. Sie werden an dieser Stel-le nicht behandelt.

6.2 ModuleDer Kern besteht aus einem Rumpf, zu dem nachträglich aus dem Dateisystem Teile (z.B. Ge-rätetreiber) hinzugeladen werden können. Für den Lademechanismus muß der Rumpf natürlichin der Lage sein, aus dem Dateisystem etwas lesen zu können. Dazu müssen der Treiber für dasphysikalische Device (z.B. SCSI-Controller, SCSI-Disk), sowie für das entsprechende Dateisy-stem (z.B.ext2 ) im Rumpf enthalten sein.

6. Gerätetreiber

30

6.3 Initialisierungs- und Cleanup-FunktionDie Initialisierungsroutine dient dazu, benötigte Resourcen (Interrupts, DMA-Kanäle, Spei-cher) für den Treiber zu allozieren und die angeschlossene Hardware zu initialisieren. Soll derTreiber während der Laufzeit des Betriebssystems wieder entfernt werden können, muß aucheine Cleanup-Prozedur existieren, die alle allozierten Resourcen wieder freigibt. Weiterhin soll-te die Cleanup-Routine die angeschlossene Hardware so programmieren, daß sie sich soweitmöglich inaktiv verhält. Die Hardware darf beispielsweise keine Interrupts mehr erzeugen.

• Built-in Treiber

Soll der Treiber immer im Kern vorhanden sein, so muß er eine Initialisierungsroutine(vom Typvoid …_init(void) ) enthalten. Eine exit-Routine existiert nicht (das Be-triebssystem terminiert nicht!). Ein Aufruf dieser Routine muß dann in…/drivers/char/mem.c , Funktionchr_dev_init (Character-Device),…/drivers/char/tty_io.c , Funktiontty_init (tty-Character-Device) bzw.…/drivers/block/ll_rw_blk.c , Funktionblk_dev_init (Block-Device) eingetragen werden.

• Treiber als Modul

Ist der Treiber als Modul konfiguriert, muß er sowohl eine Initialisierungsroutine

int init_module(void) { … }

als auch eine Cleanup-Routine

void cleanup_module(void) { … }

enthalten. Diese Funktionen müssen nirgendwo expliziet aufgerufen werden. Sie werdennach dem Laden des Moduls bzw. vor dem Entfernen angesprungen.

Ein einfacher Build-in-Treiber sieht also wie folgt aus:

voidsimple_init(void){

printk("simple driver: init called\n");}

Die Routinechr_dev_init in …/drivers/char/mem.c muß einen Eintrag der folgen-den Form enthalten (entsprechend für Block-Treiber inblk_dev_init in …/drivers/block/ll_rw_blk.c ):

#ifdef CONFIG_SIMPLEsimple_init();

#endif

Ein einfacher Module-Treiber:

intinit_module(void){

printk("simple driver: init_module called\n");

return (0); /* no error */

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 31

}

voidcleanup_module(void){

printk("simple driver: cleanup_module called\n");}

(Keine Funktionsaufrufe in anderen Dateien notwendig.)

Sinnvoll wird ein Treiber jedoch erst, wenn er mehr Funktionen als die Init- und Cleanup-Funk-tionen enthält. Diese werden über die Funktionregister_chrdev bzw.register_blkdev dem Filesystem-Modul bekanntgegeben. Für nicht benötigte Funktionenkönnen NULL-Pointer übergeben werden. Beispiel:

#define SIMPLE_MAJOR 30/* gehört nach …/include/linux/major.h */

static ssize_tsimple_read(struct file *f, char *buf, size_t cnt, loff_t *off){

…}

intinit_module(void){

static struct file_operations simple_fops = {NULL, simple_read,simple_write, NULL,simple_poll, simple_ioctl,NULL, simple_open,NULL, simple_close,NULL, NULL,NULL, NULL,NULL

};

printk("simple driver …\n");

if (register_chrdev(SIMPLE_MAJOR, "simple", &simple_fops)) {printk("can’t register simple driver (major=%d)\n",

SIMPLE_MAJOR);return (-EIO);

}

return (0);}

6. Gerätetreiber

32

Soll das Modul wieder entfernt werden können, muß der Treiber dem Filesystem bekannt ge-ben, daß nachfolgend keine Aufrufe an den Treiber mehr erlaubt sind. Dies geschieht durchAufruf der Funktionunregister_chrdev bzw.unregister_blkdev . Beispiel:

voidcleanup_module(void){

if (unregister_chrdev(SIMPLE_MAJOR, "simple")) {printk("can’t unregister simple driver (major=%d)\n",

SIMPLE_MAJOR);}

}

6.4 Open- und Close-FunktionJedesmal, wenn ein Prozeß einopen (2) bzw. close (2) ausführt, werden die registriertenopen - bzw. close -Funktionen des Treibers aufgerufen. Sie sind dazu gedacht, zu Beginn(Ende) einer Kommunikation mit dem Benutzerprozeß spezielle Funktionen auszuführen. Sowird die Open-Funktion beispielsweise dazu benützt,

• potentielle Mehrfachbenutzungen eines Treibers durch mehrere Prozesse zu steuern,

• die Bereitschaft, Daten entgegenzunehmen, der Umwelt anzuzeigen (DTR).

• einen Referenz-Zähler zu aktualisieren (wann darf ein Modul wieder entfernt werden?)(siehe Abschnitt Module 6.2)

6.5 Read- und Write-FunktionDie Read- bzw. Write-Funktion wird verwendet, um größere Datenmengen zwischen Benutzer-programm und Treiber auszutauschen.

Vorsicht: die an die Funktionen übergebene Adresse (buf ) bezeichne Bytes im User-Adreß-raum! Konstruktionen wie

memcpy(buf, …, …); memcpy(…, buf, …);

bzw.

*buf = …; … = f(*buf);

funktionieren nicht. Der Compiler gibt keine Warnung aus. Das Betriebssystem wird aber mitziemlicher Sicherheit crashen! Siehe dazu auch Kapitel 5.1.

Eine Character-Device-read-Funktion sieht –von der Struktur her– meist folgendermaßen aus:

ssize_txxx_read(struct file *f, char *buf, size_t cnt, loff_t *off){

int retval;

/* wait for data */save_flags(flags); cli();while (xxx_cnt < cnt) {

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 33

interruptible_sleep_on(&xxx_cnt_event);if (signal_pending(current)) {

restore_flags(flags);return (-ERESTARTSYS);

}}

/* copy data to user */if (copy_to_user(buf, xxx_buf, cnt) != 0) {

restore_flags(flags);return (-EFAULT);

}

/* clear system buffer */xxx_cnt = 0;

restore_flags(flags);

return (cnt);}

6.6 Ioctl-FunktionDie read- und write-Funktionen sind für den reinen Datenaustausch zwischen dem Gerät unddem Benutzerprogramm vorgestehen. Einstellungen des Gerätes werden über die ioctl-Funktionvorgenommen. So werden beispielsweise die zu sendenden Bytes einer seriellen Schnittstelleüber die write-Funktion an den Treiber der seriellen Schnittstelle übergeben. Einstellung (An-zahl der Start-, Daten und Stop-Bits, Baud-Rate usw.) werden über die ioctl-Funktion dem Ge-rät mitgeteilt bzw. vom Gerät abgefragt. Auch Kommandos an das Gerät werden über die ioctl-Schnittstelle abgesetzt (z.B. das Formatierungskommando an SCSI-Platten, Zurückspul-Kom-mando an Bandlaufwerke).

Um diese Vielzahl von Funktionen über eine einzige Funktion abwickeln zu können, ist einevariable Anzahl von Parametern und Rückgabewerten dieser Funktion notwendig. Daher hatman folgende Art der Parameter-Über- und Rückgabe gewählt. Die ioctl-Funktion erhält einenZahlenwert, der eine bestimmte Aktion bezeichnet. Zusätzlich bekommt sie einen Pointer aufeinen Speicherbereich übergeben, der zum einen die weiteren für das entsprechende Komman-do notwendigen Parameter enthält und zum zweiten die Rückgabewerte aufnehmen kann.

Die ioctl-Funktion des Treibers ist verantwortlich für das dem Kommando entsprechende Lesenbzw. Schreiben der Parameter und Rückgabewerte aus bzw. in den User-Adreßraum (ähnlichden read- und write-Funktionen).

Folgender Pseudo-Code ist ein Beispiel für eine ioctl-Funktion in einem Device-Treiber imLinux-Kern:

6. Gerätetreiber

34

intxxx_ioctl(

struct inode *i,struct file *f,unsigned int cmd,unsigned long arg

) {struct xxxparam param;int retval;

switch (cmd) {case XXXSETPARAM:

retval = copy_from_user(&param, (void *) arg,sizeof(param));

if (retval != 0) return (-EFAULT);/* use structure ‘param’ to set parameter of device */...return (0);

case XXXGETPARAM:/* get parameter of device to structure ‘param’ */...retval = copy_to_user((void *) arg, &param,

sizeof(param));return (retval);

default:return (-EINVAL);

}}

6.7 Poll-FunktionIn vielen Fällen ist ein Programm gezwungen, auf mehrere Ereignisse gleichzeitig zu warten.So sollte beispielsweise es möglich sein, daß ein Programm gleichzeitig auf die Beendigung ei-ner bereits gestartete Übertragung und auf neue Benutzereingaben wartet. Während der Prozeßwartet, sollte er keine Rechenzeit benötigen. Zu diesem Zweck wurde dieselect - bzw.poll -Funktion eingeführt.

Den größten Teil derpoll -Funktion übernimmt eine Funktion des Dateisystems (siehedo_poll in …/fs/select.c ). An die einzelnen Treiber werden nur noch Teilaufgaben de-ligiert. Die Treiberfunktion hat nur noch die Aufgabe, den aktuellen Zustand des Treibers zu te-sten und, wenn Eingaben vorliegen bzw. Ausgabe möglich ist, ein entsprechendes Bitmuster zu-rückzuliefern. Zusätzlich ist die Funktionpoll_wait (siehe…/include/linux/poll.hbzw.…/fs/select.h ) mit der Wait-Queue für jede Ein- bzw. Ausgabemöglichkeit aufzuru-fen.

Beispiel:

static struct wait_queue *xxx_in_wait;static struct wait_queue *xxx_out_wait;

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 35

unsigned intxxx_poll(

struct file *f,poll_table *wait

) {unsigned int mask;

poll_wait(file, &xxx_in_wait, wait);poll_wait(file, &xxx_out_wait, wait);

mask = 0;

if (0 < xxx_in_count) mask |= POLLIN | POLLRDNORM;if (0 < xxx_out_count) mask |= POLLOUT | POLLWRNORM;if (xxx_error) mask |= POLLERR;

return mask;}

7. Neuen Kern konfigurieren, compilieren, installieren, booten

36

7. Neuen Kern konfigurieren, compilieren,installieren, booten

Grundlage ist Linux Version 2.2.14 (ca. 60 MByte Source-Code).

Kern konfigurieren:

~>cd /usr/src/linux/usr/src/linux>make xconfigrm -f include/asm( cd include ; ln -sf asm-i386 asm)make -C scripts kconfig.tk…chmod 755 kconfig.tkmake[1]: Leaving directory `/usr/src/vrsieh/scripts'wish -f scripts/kconfig.tk

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 37

Es erscheint ein XWindows-Fenster zum Klicken…:

Nach fertiger Einstellung Konfigurationsdatei abspeichern. Es erscheint:

7. Neuen Kern konfigurieren, compilieren, installieren, booten

38

Dependancies generieren:

/usr/src/linux> make depgcc -I/usr/src/linux/include -O2 -fomit-frame-pointer -oscripts/mkdep scripts/mkdep.cmake[1]: Entering directory `/usr/src/linux/arch/i386/boot'make[1]: Nothing to be done for `dep'.make[1]: Leaving directory `/usr/src/linux/arch/i386/boot'scripts/mkdep init/*.c > .tmpdependscripts/mkdep `find /usr/src/vrsieh/include/asm /usr/src/vrsieh/include/linux /usr/src/vrsieh/include/scsi /usr/src/vrsieh/include/net -follow -name \*.h ! -name modversions.h -print` > .hdepend…mv .tmpdepend .depend

Alte Reste löschen:

/usr/src/linux> make cleanmake[1]: Entering directory `/usr/src/linux/arch/i386/boot'rm -f bootsect setup…rm -f submenu*

Kern generieren:

/usr/src/linux> make zImagegcc -D__KERNEL__ -I/usr/src/linux/include -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -fno-strength-reduce -pipe -m386 -DCPU=386 -c -o init/main.o init/main.c…tools/build bootsect setup compressed/vmlinux.out CURRENT >zImageRoot device is (3, 3)Boot sector 512 bytes.Setup is 4340 bytes.System is 409 kBsyncmake[1]: Leaving directory `/usr/src/linux/arch/i386/boot'

Module generieren:

/usr/src/linux> make modulesmake[1]: Entering directory `/usr/src/linux/kernel'make[1]: Nothing to be done for `modules'.make[1]: Leaving directory `/usr/src/linux/kernel'make[1]: Entering directory `/usr/src/linux/drivers'set -e; for i in block char net scsi sound cdrom isdn streams;do make -C $i modules; donemake[2]: Entering directory `/usr/src/linux/drivers/block'gcc -D__KERNEL__ -I/usr/src/linux/include -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -fno-strength-reduce -pipe -

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 39

m386 -DCPU=386 -DMODULE -DMODVERSIONS -include /usr/src/linux/include/linux/modversions.h -c -o loop.o loop.c…make[1]: Entering directory `/usr/src/linux/arch/i386/math-emu'make[1]: Nothing to be done for `modules'.make[1]: Leaving directory `/usr/src/linux/arch/i386/math-emu'

Kern installieren:

/usr/src/linux> cp arch/i386/boot/zImage /boot/vmlinux-2.2.12-vrsieh

Kern bootfähig machen:

Die Datei /etc/lilo.conf muß einen Eintrag folgender Form enthalten:

## additional entries#image = /boot/vmlinuz-2.2.12-vrsieh

label = linux-vrsiehroot = /dev/hda3read-only

Nach jeder Änderung der Datei/etc/lilo.conf oder jeder Änderung der in dieser Dateibenutzten anderen Dateien, muß der Boot-Loader neu aktualisiert werden:

root@faui31c:/usr/src/linux #liloAdded linux *Added linux-vrsieh

Reboot:

Ein Reboot wird ausgelöst durch:

root@faui31c:~# rebootSwitching to runlevel: 6Sending processes the TERM signal…

Vorsicht:Vorher alles wichtige auf Diskette sichern! Daß ein neuer Kern auf Anhieb funktioniert, isteher selten…!

Im erscheinenden lilo-Boot-Prompt kann dann der zu bootende Kern ausgewählt werden. ImBeispiel:

>>> Press <TAB> to list available boot image labels

boot: linux-vrsiehLoading linux-vrsieh .......…

Module werden geladen durch:

root@faui31c:/usr/src/linux# insmod gpib.o... Meldungen des Treibers ...

7. Neuen Kern konfigurieren, compilieren, installieren, booten

40

Sie können (z.B. zum Ändern) durch folgendes Kommando wieder ausdem Kern entfernt werden:

root@faui31c:~# rmmod gpib... Meldungen des Treibers ...

Geladene Module können angezeigt werden durch:

root@faui31c:~# lsmodModule: #pages: Used by:gpib 1 0

Design von Hardwarekomponenten und Treibern für Linux Systeme

© Volkmar Sieh, Universität Erlangen-Nürnberg, Inst. f. Informatik III, 2001 41

8. LiteraturBücher:

M. Bach, „The design of the UNIX operating system“, Englewood Cliffs, N.J., Prentice-Hall,1986.

M. Beck, „Linux-Kernel-Programmierung“, Bonn, Addison-Wesley, 1994.

W. Matthes, „Intel’s i486“, Aachen, Elektor-Verlag, 1992.

A. Rubini, „Linux device drivers“, Cambridge, O’Rielly, 1998.

Online-Dokumente:

Linux Kernel Development: http://www.kernel.org/

HOWTO’s: /usr/doc/HOWTO oder ftp://sunsite.unc.edu/pub/Linux/docs/

8. Literatur

42