Mit Hilfe der Mmap-Funktion lassen sich Kontextwechsel und Kopieraktionen einsparen. Diese Folge der Kern-Technik verrät Treiber- und Anwendungsentwicklern, wie es funktioniert.
Um handelsüblicher Hardware die auf der Packung angegebenen Höchstleistungen zu entlocken, muss der Programmierer einiges an Wissen über den Kernel mitbringen. Kennt er Komponenten und Betriebsabläufe, kann er in die Trickkiste greifen, Kontextwechsel-Zeiten vermeiden und Kopiervorgänge einsparen. Die Basistechnologie hierfür stellt »mmap()« bereit. Mmap ermöglicht es der Applikation, Speicherbereiche aus dem Kernelspace in den eigenen Adressraum einzublenden (siehe Abbildung 1).

Abbildung 1: Die Funktion »mmap()« blendet die Speicherseite aus dem Kernelspace in den Userspace ein. Die Applikation kann nun direkt auf den Kernelspeicher zugreifen.
Getrennte Adressbereiche
Aus Stabilitäts- und Sicherheitsgründen besitzen Applikationen und Kernel jeweils eigene Adressräume (zum Begriff Adresse siehe Kasten “Adresstypen”). Daher ist es einer Applikation nicht möglich, die (physischen) Speicherbereiche anzusprechen, in denen der Betriebssystemkern seine Variablen ablegt oder in denen Register von Peripheriegeräten liegen (Kernelspace). Der Applikation bleibt also nichts anderes übrig, als den Betriebssystemkern per Systemcall damit zu beauftragen, Manipulationen an den Speicherzellen vorzunehmen oder Daten zwischen dem eigenen Adressraum (Userspace) und dem Kernelspace zu transferieren. Die Nachteile liegen auf der Hand: unnötige Kopieraktionen beziehungsweise zeitlich teure Systemcalls durch den Kontextwechsel.
Wenn es beim Programmieren auf jede Millisekunde ankommt, hilft oft »mmap()« weiter. Es blendet Teile des Kernelspeichers in den Adressraum der Applikation ein. So kann die Applikation ohne Umwege auf die Daten respektive auf die hinter einem (Dual-Port-)Memory liegende Hardware zugreifen. Auf diese Art blendet beispielsweise auch der X-Server den Speicher der Grafikkarte in den eigenen Adressraum ein.
Interface: Gerätedatei
Applikationen können den Beschleunigungstrick allerdings nur dann nutzen, wenn der zugehörige Gerätetreiber »mmap()« unterstützt. Der Treiber übernimmt also die Verantwortung dafür, dass bestimmte Speicherbereiche einer Applikation direkt zugänglich gemacht werden. Listing 1 zeigt, wie Anwendungsentwickler den direkten Weg zur Hardware einschlagen. Sie öffnen dazu in gewohnter Weise den zugehörigen Treiber durch Angabe der Gerätedatei (in Listing 1 ist dies die Datei »/dev/mmap«). Die Gerätedatei repräsentiert den Treiber, der die Freigabe auf den Speicher erteilen soll. Danach rufen sie »mmap()« auf, wobei die Applikation mit dem ersten Parameter einen Vorschlag machen darf, an welche Adresse im eigenen Adressraum der Speicher eingeblendet werden soll. Der zweite Parameter spezifiziert die Länge des Speicherbereichs. Vorteilhaft ist es, wenn diese ein Vielfaches einer Seitengröße, also typischerweise ein Vielfaches von 4096 Byte ist.
|
Listing 1: Mmap einer |
|---|
01 #include <stdio.h>
02 #include <fcntl.h>
03 #include <unistd.h>
04 #include <sys/mman.h>
05
06 int main( int argc, char **argv )
07 {
08 int fd;
09 void *pageaddr;
10 int *ptr;
11
12 fd = open( "/dev/mmap", O_RDWR );
13 if( fd<0 ) {
14 perror( "/dev/mmap" );
15 return -1;
16 }
17 pageaddr = mmap( NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
18
19 printf("pageaddr mmap: %pn", pageaddr );
20 if( pageaddr ) {
21 ptr = (int *)pageaddr;
22 printf("*pageaddr: %dn", *ptr );
23 *ptr = 55;
24 }
25 munmap( pageaddr, 4096 );
26
27 return 0;
28 }
|
Weiterhin lassen sich noch die Speicherschutzbits, die so genannten Protections, festlegen: »PROT_READ«, »PROT_ WRITE«, »PROT_EXEC« und »PROT_NONE«. Der vierte Parameter »MAP_SHARED« schließlich legt fest, dass der Kernel den Speicherbereich schützt (lockt), sodass er die zugehörigen Speicherseiten nicht auslagert (swappt). Zum Schluss folgt der Offset innerhalb des Speicherbereichs, ab dem das Mapping passieren soll.
Um Treiber-seitig »mmap()« zu unterstützen, ist vom Treiber im Wesentlichen nur eine Funktion »drv_mmap()« zu implementieren. Diese Funktion bekommt zwei Parameter übergeben. Der erste repräsentiert die zugreifende Applikation, die Treiberinstanz. Der zweite zeigt auf eine so genannte Virtual Memory Area (VMA). Sie beschreibt als zentrale Datenstruktur einen semantisch zusammengehörigen Speicherbereich der Applikation. Ein solcher Speicherbereich ist durch identische Zugriffsrechte gekennzeichnet. Ein Rechenprozess hat unter Linux mindestens vier VMAs: das Codesegment, den Bereich der initialisierten Daten, den Bereich der uninitialisierte Daten (BSS, Block Started by Symbol) und den Stack.
Listing 2 zeigt die Speicherregionen eines Hello-World-Beispiels, ausgegeben durch »cat /proc/Hello-World-Programm-PID/maps«. Die erste Spalte gibt den Adressbereich im Adressraum der Applikation an, die zweite Spalte bezeichnet die Zugriffsrechte, die dritte Spalte den Offset innerhalb des Segments, die vierte schließlich die Major- und Minor-Nummer, als fünfte folgt die Inode. Ist der Inhalt des Speicherbereichs nicht aus einer normalen Datei heraus entstanden, wird im Feld »Dev« die Major-Nummer zu Null gesetzt.
|
Listing 2: Zwölf |
|---|
01 # Adressbereich Perms Offset Dev Inode Bezeichnung 02 quade@ezs-mobil:/tmp$ cat /proc/21452/maps 03 08048000-08049000 r-xp 00000000 03:02 2829122 /tmp/hello_world 04 08049000-0804a000 rw-p 00000000 03:02 2829122 /tmp/hello_world 05 b7ddf000-b7de0000 rw-p b7ddf000 00:00 0 06 b7de0000-b7f0d000 r-xp 00000000 03:02 736410 /lib/tls/i686/cmov/libc-2.4.so 07 b7f0d000-b7f0f000 r--p 0012c000 03:02 736410 /lib/tls/i686/cmov/libc-2.4.so 08 b7f0f000-b7f11000 rw-p 0012e000 03:02 736410 /lib/tls/i686/cmov/libc-2.4.so 09 b7f11000-b7f14000 rw-p b7f11000 00:00 0 10 b7f23000-b7f26000 rw-p b7f23000 00:00 0 11 b7f26000-b7f27000 r-xp b7f26000 00:00 0 [vdso] 12 b7f27000-b7f40000 r-xp 00000000 03:02 703158 /lib/ld-2.4.so 13 b7f40000-b7f42000 rw-p 00018000 03:02 703158 /lib/ld-2.4.so 14 bf887000-bf89c000 rw-p bf887000 00:00 0 [stack] |
Bei den Rechten bedeutet »r« Lesen, »w« Schreiben, »x« Ausführen, »s« eine gemeinsame Nutzung mit anderen Prozessen und »p«, dass der Speicherbereich bei einem ersten Schreibzugriff kopiert wird (COW, Copy on Write). Die in Listing 2 dargestellte Region »[vdso]« (Virtually Dynamically Shared Object) repräsentiert übrigens die Speicherseite, über die eine Applikation Systemcalls aufruft (siehe hierzu auch [1]).
Ruft eine Applikation die Funktion »mmap()« auf, erzeugt und initialisiert der Kernel ein VMA-Objekt. Dieses Objekt repräsentiert die neue Speicherregion und wird von der Funktion »drv_ mmap()« vervollständigt. Dazu kann das Programm dem Kernel – durch Aufruf von »remap_pfn_range()« – die physikalische Adresse des einzublendenden Speicherbereichs plus dessen Länge übergeben. Der Kernel richtet dann das entsprechende Mapping ein.
Die physische Adresse muss sich an einer Seitengrenze orientieren und wird als so genannte Page Frame Number (PFN) übergeben. Diese Nummer erhält man durch Einteilung des Speichers in gleich große Blöcke – die Pages – und deren Zählung, beginnend mit 0.
Die PFN kann der Entwickler sehr einfach berechnen, indem er die physische Adresse um die Bits, die zur Adressierung innerhalb einer Page benötigt werden, nach rechts schiebt (»PAGE_SHIFT«, siehe Listing 3). Um die logische Adresse, über die die Applikation später auf die Seite zugreifen kann, muss er sich übrigens nicht kümmern. Der Kernel hat sie bereits bei Aufruf der Funktion »drv_mmap()« ausgewählt.
|
Listing 3: Einblenden von |
|---|
01 ...
02 static int drv_mmap( struct file *instance,
03 struct vma_area_struct *vma )
04 {
05 if( (vma->vm_end-vma->vm_start) > hw_mem_area_size )
06 return -EINVAL;
07 if( remap_pfn_range( vma,
08 vma->start, // user virtual address
09 hw_mem_area>>PAGE_SHIFT, // PFN - physical address
10 vma->vm_end - vma->vm_start, // length
11 vma->vm_page_prot ) ) // page protection
12 return -EAGAIN;
13
14 return 0;
15 }
16 ...
|
|
Adresstypen |
|---|
|
Der Begriff Adresse ist nicht eindeutig. Innerhalb des Kernels sind fünf Varianten zu unterscheiden (siehe Abbildung 2). Logische oder User Virtual Address nennen die Entwickler jene Adresse, die innerhalb einer Applikation verwendet wird. Die physische Adresse dagegen ist jene, mit der die CPU auf den physischen Speicher zugreift, also den von der Hardware zur Verfügung gestellten linearen Speicherbereich. In einigen Fällen verwendet Peripherie für den Zugriff auf den physischen Speicher eine andere Adresse. Diese Bus-Adresse wird beispielsweise für den Zugriff per DMA benötigt. Bei den meisten Plattformen sind Bus-Adresse und physische Adresse identisch. Als Kernel-logische Adresse bezeichnen die Kernelhacker Adressen, die sie innerhalb des Kernels für den Zugriff auf den Speicher verwenden. Oft sind diese Adressen eins zu eins auf den physischen Speicher abgebildet. Kernel Virtual Address heißen Adressen im Kernel, die nicht direkt auf den physischen Speicher abgebildet sind. Die Kernelfunktion »vmalloc()« beispielsweise gibt bei Aufruf eine solche Kernel-virtuelle Adresse zurück. |
Probleme beim Verändern
Es gibt allerdings noch zwei Fallstricke. Die erste Warnung betrifft den Systemcall »mremap()«. Hiermit können Applikationsprogrammierer ein bestehendes Mapping in seiner Größe verkleinern oder erweitern. Der Kernel akzeptiert eine solche Anfrage zunächst klaglos und erweitert in seinen Datenstrukturen den eingeblendeten Speicherbereich. Erst wenn die Applikation auf den erweiterten Bereich zugreift, wird es kritisch. Der Kernel stellt in diesem Fall fest, dass es zu der verwendeten logischen Adresse noch kein physisches Pendant gibt, das heißt, es gibt keine Zuordnung des Adressbereichs zu einer physischen Speicherseite (mit der Datenstruktur »struct page«).
Der Kernel benötigt für den Zugriff die zur logischen Adresse gehörende »struct page«. Um sie zu erhalten, ruft er die zur VMA gehörende Methode »nopage()« auf. Diese Funktion bekommt neben der VMA-Datenstruktur die Adresse im Adressraum der Applikation übergeben. Es ist also Aufgabe des Entwicklers, die Methode zu implementieren und innerhalb der Funktion »drv_mmap()« dem VMA-Objekt zuzuordnen. Im einfachsten Fall – der Treiber kann den eingeblendeten Speicherbereich nicht erweitern – ist die Methode so zu implementieren:
struct page *drv_vma_nopage( struct vm_area_struct *vma, unsigned long address, int *type )
{
return NOPAGE_SIGBUS;
}
Die Applikation bekommt dann beim Zugriff das Signal »SIGBUS« geschickt. Unterstützt ein Treiber auch die nachträgliche Erweiterung des Speicherbereichs, sollte er die »struct page« zurückgeben, die den zusätzlichen physikalischen Speicherbereich repräsentiert.
Listing 4 zeigt, wie es geht. Zuerst bestimmt das Modul aus der übergebenen logischen Adresse die physische Adresse, auf die die Applikation zugreifen möchte. Dazu rechnet es den Offset im logischen Adressraum der Applikation aus und addiert diesen zur Startadresse des physischen Speicherbereichs (siehe Abbildung 3). Danach berechnet es die Page Frame Number, indem es das Ergebnis des ersten Schritts um »PAGE_SHIFT« Bits nach rechts schiebt.
|
Listing 4: Implementierung von |
|---|
01 struct page *drv_vma_nopage( struct vm_area_struct *vma,
02 unsigned long address, int *type )
03 {
04 unsigned long phys_address, pfn;
05 struct page *page_wanted;
06
07 phys_address = hw_mem_area + (address-vma->vm_start);
08 if( (vma->vm_end-vma->vm_start) > HW_MEM_AREA_SIZE )
09 return NOPAGE_SIGBUS;
10 pfn = phys_address >> PAGE_SHIFT;
11 if( !pfn_valid( pfn ) )
12 return NOPAGE_SIGBUS;
13 page_wanted = pfn_to_page( pfn );
14 get_page( page_wanted );
15 if( type )
16 *type = VM_FAULT_MINOR;
17 return page_wanted;
18 }
|

Abbildung 3: Eine Offset-Verschiebung berechnet die physische Adresse, auf die eine Applikation zugreifen möchte.
Die erhaltene PFN überprüft es durch Aufruf der Funktion »pfn_valid()« auf Gültigkeit. Ist die PFN gültig, ruft es »pfn_to_page()« auf und erhält schließlich die gesuchte »struct page«. Bevor der Treiber diese dem Kernel übergibt, muss er durch Aufruf von »get_page()« unbedingt den Zugriffszähler auf die Seite erhöhen und »type« auf den Wert »VM_FAULT_MINOR« setzen.
Ohne Swapping
Vergisst der Treiberentwickler die Methode »drv_vma_nopage()« zu implementieren, bekommt die Applikation beim Zugriff auf den erweiterten Adressbereich keine Fehlermeldung, sondern eine Zero-Page eingeblendet. Sie greift dann also auf ganz normalen Hauptspeicher zu. Die Adresse der Methode »drv_vma_nopage()« assoziiert man übrigens mit der »struct vm_area_struct« wie in Listing 5, Zeile 23.
|
Listing 5: Einblenden einer |
|---|
01 #include <linux/module.h>
02 #include <linux/device.h>
03 #include <linux/fs.h>
04 #include <linux/mm.h>
05
06 static int major;
07 static struct class *demo_class;
08 static struct page *common_page;
09
10 struct page *drv_vma_nopage( struct vm_area_struct *vma,
11 unsigned long address, int *type )
12 {
13 printk("drv_vma_nopage( %p, %lx, %p )n", vma, address, type );
14 if( (address-vma->vm_start) > 4095 )
15 return NOPAGE_SIGBUS;
16 get_page( common_page );
17 if( type )
18 *type = VM_FAULT_MINOR;
19 return common_page;
20 }
21
22 static struct vm_operations_struct drv_vma_ops = {
23 .nopage = drv_vma_nopage,
24 };
25
26 static int drv_mmap( struct file *instance, struct vm_area_struct *vma )
27 {
28 printk("drv_mmap(%p, %p)n", instance, vma);
29 vma->vm_ops = &drv_vma_ops;
30 vma->vm_flags |= VM_RESERVED;
31 return 0;
32 }
33
34 static struct file_operations fops = {
35 .mmap = drv_mmap,
36 };
37
38 static int __init drv_init(void)
39 {
40 int *ptr;
41
42 if( (major=register_chrdev(0,"mmap",&fops))==0 ) {
43 printk("no major number availablen");
44 return -EIO;
45 }
46 demo_class = class_create(THIS_MODULE, "demo");
47 if( IS_ERR(demo_class) ) {
48 printk("no udev supportn");
49 } else {
50 printk("class_device_create...n");
51 class_device_create(demo_class,NULL,MKDEV(major,0),NULL,"mmap");
52 }
53 common_page = alloc_page( GFP_USER );
54 if( !common_page ) {
55 if( !IS_ERR(demo_class) ) {
56 class_device_destroy( demo_class, MKDEV(major,0) );
57 class_destroy( demo_class );
58 }
59 unregister_chrdev( major, "mmap" );
60 return -ENOMEM;
61 }
62 ptr = (int *)page_address( common_page );
63 *ptr = 99; // Nur zur Demonstration!
64 return 0;
65 }
66
67 static void __exit drv_exit(void)
68 {
69 unregister_chrdev( major, "mmap" );
70 if( !IS_ERR(demo_class) ) {
71 class_device_destroy( demo_class, MKDEV(major,0) );
72 class_destroy( demo_class );
73 }
74 }
75
76 module_init( drv_init );
77 module_exit( drv_exit );
78 MODULE_LICENSE("GPL");
|
Die zweite Warnung betrifft den Adressraum, den die Funktion »remap_pfn_range()« einblendet. Es lassen sich nur jene Pages einblenden, die der normalen Speicherverwaltung nicht zur Verfügung stehen. Diese Seiten sind als reserviert markiert und werden daher bei der Auslagerung nicht berücksichtigt. Auf einer PC-Plattform beispielsweise sind dies der Adressbereich zwischen 640 KByte und 1 MByte (ISA), der Adressbereich oberhalb des physischen Speichers (von PCI-Geräten) sowie der Codebereich des Kernels selbst.
Shared Mem zum Kernel
Wie das Relay-Subsystem [2] zeigt, ist es in einigen Situation dennoch sinnvoll, normalen Kernelspace in den Adressraum der Applikation einzublenden. Auch damit lassen sich sehr häufig Kopieraktionen vermeiden. Einen derart gemeinsamen Speicher zwischen Kernel- und Userland implementieren Treiberentwickler unter Auslassung der Funktion »remap_pfn_range()« wiederum mit Hilfe der Methode »drv_vma_nopage()«. Wird »drv_vma_nopage()« aufgerufen, genügt es, die entsprechende Page zurückzuliefern. Listing 5 zeigt dies in Form eines Treibers, der das virtuelle Gerät »/dev/mmap« implementiert. Als kleines Bonbon erstellt die Funktion »drv_init()« im Sys-Filesystem übrigens einen Eintrag, sodass Udev die Gerätedatei automatisch anlegt.
Der Code aus Listing 5 lässt sich leicht mit Hilfe des Makefile von [3] kompilieren. Wer das so entstehende Modul »mmap.ko« installiert, kann anschließend in der Applikation aus Listing 1 auf den eingeblendeten Kernelspeicher zugreifen. Beim ersten Zugriff auf die Speicherseite liest die Anwendung die vom Treiber abgelegte »99«. Danach schreibt sie dorthin den Wert »55« und liest diesen Wert beim Neustart direkt aus.
Viele Möglichkeiten
Das letzte Beispiel demonstriert, wie Anwendungen auf eingeblendete Speicherbereiche des Kernels zugreifen können. Richtige Programme werden normalerweise mehr Nutzdaten als nur einfache Zahlen austauschen, das Prinzip bleibt jedoch gleich. Gerade wer größere Datenmengen zwischen User- und Kernelspace austauschen und dabei Kopiervorgänge vermeiden möchte, sollte einen Blick auf Mmap werfen. (ofr)
|
Infos |
|---|
|
[1] Johan Petersson, “What is linux-gate-so.1?”: [http://www.trilithium.com/johan/2005/08/linux-gate] [2] Eva-Katharina Kunst, Jürgen Quade, “Kern-Technik”, Folge 29: Linux-Magazin 9/06, S. 94 [3] Listings online: [https://www.linux-magazin.de/Service/Listings/2007/03/Kern-Technik] |
Copyright © 2002 Linux New Media AG







