Heim  >  Artikel  >  System-Tutorial  >  Ausführliche Erläuterung der Speicherverwaltung von Linux-Gerätetreibern

Ausführliche Erläuterung der Speicherverwaltung von Linux-Gerätetreibern

WBOY
WBOYnach vorne
2024-02-15 13:09:09533Durchsuche

Haben Sie sich jemals gefragt, wie Sie Treiber für Ihre Geräte unter Linux schreiben können? Haben Sie sich jemals gefragt, wie Sie Ihren Treiber unter Linux dazu bringen können, Speicherressourcen effizient zu nutzen? Haben Sie jemals darüber nachgedacht, wie Sie Ihren Treiber in die Lage versetzen können, einige erweiterte Funktionen in Linux-Systemen zu implementieren, wie z. B. Speicherzuordnung, Speicherzuweisung, Speicherschutz usw.? Wenn Sie sich für diese Probleme interessieren, stellt Ihnen dieser Artikel eine effektive Methode zum Erreichen dieser Ziele vor: die Speicherverwaltung von Linux-Gerätetreibern. Bei der Speicherverwaltung handelt es sich um eine Datenstruktur und einen Algorithmus zur Beschreibung und Steuerung von Speicherressourcen. Sie ermöglichen die einfache und einheitliche Übertragung von Speicherinformationen und -attributen an den Kernel und ermöglichen so die Zuweisung und Freigabe von Speicher. Die Speicherverwaltung ist auch ein Mechanismus zur effektiven Nutzung. Sie ermöglicht die standardmäßige und universelle Definition und Verwendung verschiedener Speicheroperationen und -befehle und ermöglicht so die Realisierung von Funktionen wie Speicherzuordnung, Kopieren und Schutz. Die Speicherverwaltung ist auch ein Framework zur Implementierung erweiterter Funktionen. Sie ermöglicht die flexible und erweiterbare Definition und Verwendung verschiedener Speicherschnittstellen und -protokolle und ermöglicht so die gemeinsame Nutzung von Speicher, Sperrung, Pufferung und andere Funktionen. Dieser Artikel gibt Ihnen eine detaillierte Einführung in die Anwendung und Rolle der Speicherverwaltung in Linux-Gerätetreibern anhand der Grundkonzepte der Speicherverwaltung, grammatikalischer Regeln, Schreibmethoden, Aufrufprozesse, Betriebsmethoden usw. und hilft Ihnen, diese nützlichen und nützlichen Aspekte zu beherrschen leistungsstarke Methode.

  • Für Prozessoren mit MMU bietet das Linux-System ein komplexes Speicherverwaltungssystem, mit dem der Speicher, auf den der Prozess zugreifen kann, 4 GB erreichen kann. Der 4-GB-Speicherplatz des Prozesses ist in zwei Teile unterteilt: Benutzerraum und Kernelraum. Benutzerraumadressen werden im Allgemeinen zwischen 0 und 3 GB verteilt (d. h. PAGE_OFFSET). Auf diese Weise sind die verbleibenden 3 bis 4 GB Kernelraum.
    Zu den im Kernel-Space-Anwendungsspeicher beteiligten Funktionen gehören hauptsächlich kmalloc (), __get_free_pages () und vmalloc () usw.
    Durch Speicherzuordnung können Benutzerprozesse direkt auf Geräte im Benutzerbereich zugreifen.


    Kernel-Adressraum

    Der Benutzerbereich jedes Prozesses ist völlig unabhängig und hat keine Beziehung zueinander. Jeder Benutzerprozess verfügt über eine andere Seitentabelle. Der Kernelraum wird vom Kernel zugeordnet. Er ändert sich nicht mit dem Prozess und ist fest. Kernelraumadressen verfügen über eigene entsprechende Seitentabellen, und der virtuelle Raum des Kernels ist unabhängig von anderen Programmen. Benutzerprozesse können nur über Systemaufrufe (die im Namen des Benutzerprozesses im Kernelmodus ausgeführt werden) auf den Kernelraum zugreifen.

    Der 1-GB-Kernel-Adressraum unter Linux ist in einen physischen Speicherzuordnungsbereich, einen virtuellen Speicherzuordnungsbereich, einen High-End-Seitenzuordnungsbereich, einen dedizierten Seitenzuordnungsbereich und einen vom System reservierten Zuordnungsbereich unterteilt, wie in der Abbildung dargestellt.
    Ausführliche Erläuterung der Speicherverwaltung von Linux-Gerätetreibern

    • Reservierter Bereich

      Linux 保留内核空间最顶部 FIXADDR_TOP~4GB 的区域作为保留区。
      
    • Eigener Seitenzuordnungsbereich

      紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START~
      
      FIXADDR_TOP),它的总尺寸和每一页的用途由 fixed_address 枚举结构在编译时预定
      
      义,用__fix_to_virt(index)可获取专用区内预定义页面的逻辑地址。
      
    • High-End-Speicherzuordnungsbereich

      当系统物理内存大于 896MB 时,超过物理内存映射区的那部分内存称为高端内存(而未
      
      超过物理内存映射区的内存通常被称为常规内存),内核在存取高端内存时必须将它们映
      
      射到高端页面映射区。
      
    • Virtueller Speicherzuweisungsbereich

      用于 vmalloc()函数,它的前部与物理内存映射区有一个隔离带,后部与高端映射区也有
      
      一个隔离带。
      
    • Physischer Speicherzuordnungsbereich

      一般情况下,物理内存映射区最大长度为 896MB,系统的物理内存被顺序映射在内核空间
      
      的这个区域中。
      

    Die Beziehung zwischen virtueller Adresse und physischer Adresse

    Für den virtuellen Speicher des Kernels (physischer Speicherzuordnungsbereich) können Sie virt_to_phys() verwenden, um die virtuelle Kerneladresse in eine physische Adresse umzuwandeln. Die Implementierung von virt_to_phys() hängt mit der Architektur zusammen virt_to_phys() lautet wie folgt: :

        static inline unsigned long virt_to_phys(void *x)
        {
            return __virt_to_phys((unsigned long)(x));
        }
    
        /* PAGE_OFFSET 通常为 3GB,而 PHYS_OFFSET 则定于为系统 DRAM 内存的基地址 */
    
        #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)
    

    内存分配

    在 Linux 内核空间申请内存涉及的函数主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。kmalloc()和__get_free_pages()( 及其类似函数) 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmalloc()在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而 vmalloc()申请的虚拟内存和物理内存之间也没有简单的换算关系。

    kmalloc()

        void *kmalloc(size_t size, int flags);
    

    给 kmalloc()的第一个参数是要分配的块的大小,第二个参数为分配标志,用于控制 kmalloc()的行为。

    flags

    • 最常用的分配标志是 GFP_KERNEL,其含义是在内核空间的进程中申请内存。 kmalloc()的底层依赖__get_free_pages()实现,分配标志的前缀 GFP 正好是这个底层函数的缩写。使用 GFP_KERNEL 标志申请内存时,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用 GFP_KERNEL 申请内存。
    • 在中断处理函数、 tasklet 和内核定时器等非进程上下文中不能阻塞,此时驱动应当使用GFP_ATOMIC 标志来申请内存。当使用 GFP_ATOMIC 标志申请内存时,若不存在空闲页,则不等待,直接返回。
    • 其他的相对不常用的申请标志还包括 GFP_USER(用来为用户空间页分配内存,可能阻塞)、GFP_HIGHUSER(类似 GFP_USER,但是从高端内存分配)、 GFP_NOIO(不允许任何 I/O 初始化)、 GFP_NOFS(不允许进行任何文件系统调用)、 __GFP_DMA(要求分配在能够 DMA 的内存区)、 __GFP_HIGHMEM(指示分配的内存可以位于高端内存)、 __GFP_COLD(请求一个较长时间不访问的页)、 __GFP_NOWARN(当一个分配无法满足时,阻止内核发出警告)、 __GFP_HIGH(高优先级请求,允许获得被内核保留给紧急状况使用的最后的内存页)、 __GFP_REPEAT(分配失败则尽力重复尝试)、 __GFP_NOFAIL(标志只许申请成功,不推荐)和__GFP_NORETRY(若申请不到,则立即放弃)。

    使用 kmalloc()申请的内存应使用 kfree()释放,这个函数的用法和用户空间的 free()类似。

    __get_free_pages ()

    __get_free_pages()系列函数/宏是 Linux 内核本质上最底层的用于获取空闲内存的方法,因为底层的伙伴算法以 page 的 2 的 n 次幂为单位管理空闲内存,所以最底层的内存申请总是以页为单位的。

    __get_free_pages()系列函数/宏包括 get_zeroed_page()、 __get_free_page()和__get_free_pages()。

        /* 该函数返回一个指向新页的指针并且将该页清零 */
        get_zeroed_page(unsigned int flags);
        /* 该宏返回一个指向新页的指针但是该页不清零 */
        __get_free_page(unsigned int flags);
        /* 该函数可分配多个页并返回分配内存的首地址,分配的页数为 2^order,分配的页也
    
    不清零 */
        __get_free_pages(unsigned int flags, unsigned int order);
    
        /* 释放 */
        void free_page(unsigned long addr);
        void free_pages(unsigned long addr, unsigned long order);
    

    __get_free_pages 等函数在使用时,其申请标志的值与 kmalloc()完全一样,各标志的含义也与kmalloc()完全一致,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。

    vmalloc()

    vmalloc()一般用在为只存在于软件中(没有对应的硬件意义)的较大的顺序缓冲区分配内存,vmalloc()远大于__get_free_pages()的开销,为了完成 vmalloc(),新的页表需要被建立。因此,只是调用 vmalloc()来分配少量的内存(如 1 页)是不妥的。
    vmalloc()申请的内存应使用 vfree()释放, vmalloc()和 vfree()的函数原型如下:

        void *vmalloc(unsigned long size);
        void vfree(void * addr);
    

    vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为 GFP_KERNEL 的 kmalloc()。

    slab

    一方面,完全使用页为单元申请和释放内存容易导致浪费(如果要申请少量字节也需要 1 页);另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。在Linux 系统中所用到的对象,比较典型的例子是 inode、 task_struct 等。如果我们能够用合适的方法使得在对象前后两次被使用时分配在同一块内存或同一类内存空间且保留了基本的数据结构,就可以大大提高效率。 内核的确实现了这种类型的内存池,通常称为后备高速缓存(lookaside cache)。内核对高速缓存的管理称为slab分配器。实际上 kmalloc()即是使用 slab 机制实现的。
    注意, slab 不是要代替__get_free_pages(),其在最底层仍然依赖于__get_free_pages(), slab在底层每次申请 1 页或多页,之后再分隔这些页为更小的单元进行管理,从而节省了内存,也提高了 slab 缓冲对象的访问效率。

        #include 
    
        /* 创建一个新的高速缓存对象,其中可容纳任意数目大小相同的内存区域 */
        struct kmem_cache *kmem_cache_create(const char *name, /* 一般为将要高速缓
    
    存的结构类型的名字 */
                size_t size, /* 每个内存区域的大小 */
                size_t offset, /* 第一个对象的偏移量,一般为0 */
                unsigned long flags, /* 一个位掩码:
                                        SLAB_NO_REAP 即使内存紧缩也不自动收缩这块缓
    
    存,不建议使用 
                                        SLAB_HWCACHE_ALIGN 每个数据对象被对齐到一个
    
    缓存行
                                        SLAB_CACHE_DMA 要求数据对象在DMA内存区分配
                                      */
    
                /* 可选参数,用于初始化新分配的对象,多用于一组对象的内存分配时使
    
    用 */
                void (*constructor)(void*, struct kmem_cache *, unsigned long), 
                void (*destructor)(void*, struct kmem_cache *, unsigned long)
                );
    
        /* 在 kmem_cache_create()创建的 slab 后备缓冲中分配一块并返回首地址指针 */
        void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
    
        /* 释放 slab 缓存 */
        void kmem_cache_free(struct kmem_cache *cachep, void *objp);
    
        /* 收回 slab 缓存,如果失败则说明内存泄漏 */
        int kmem_cache_destroy(struct kmem_cache *cachep);
    

    Tip: 高速缓存的使用统计情况可以从/proc/slabinfo获得。

    内存池(mempool)

    内核中有些地方的内存分配是不允许失败的,内核开发者建立了一种称为内存池的抽象。内存池其实就是某种形式的高速后备缓存,它试图始终保持空闲的内存以便在紧急状态下使用。mempool很容易浪费大量内存,应尽量避免使用。

        #include 
    
        /* 创建 */
        mempool_t *mempool_create(int min_nr, /* 需要预分配对象的数目 */
                mempool_alloc_t *alloc_fn, /* 分配函数,一般直接使用内核提供的
    
    mempool_alloc_slab */
                mempool_free_t *free_fn, /* 释放函数,一般直接使用内核提供的
    
    mempool_free_slab */
                void *pool_data); /* 传给alloc_fn/free_fn的参数,一般为
    
    kmem_cache_create创建的cache */
    
        /* 分配释放 */
        void *mempool_alloc(mempool_t *pool, int gfp_mask);
        void mempool_free(void *element, mempool_t *pool);
    
        /* 回收 */
        void mempool_destroy(mempool_t *pool);
    

    内存映射

    一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间直能接访问设备的物理地址。
    这种能力对于显示适配器一类的设备非常有意义,如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点的像素将不再需要一个从用户空间到内核空间的复制的过程。
    从 file_operations 文件操作结构体可以看出,驱动中 mmap()函数的原型如下:

        int(*mmap)(struct file *, struct vm_area_struct*);
    

    驱动程序中 mmap()的实现机制是建立页表,并填充 VMA 结构体中 vm_operations_struct 指针。VMA 即 vm_area_struct,用于描述一个虚拟内存区域:

        struct vm_area_struct {
            unsigned long vm_start; /* 开始虚拟地址 */
            unsigned long vm_end; /* 结束虚拟地址 */
    
            unsigned long vm_flags; /* VM_IO 设置一个内存映射I/O区域;
                                       VM_RESERVED 告诉内存管理系统不要将VMA交换出去
     */
    
            struct vm_operations_struct *vm_ops; /* 操作 VMA 的函数集指针 */
    
            unsigned long vm_pgoff; /* 偏移(页帧号) */
    
            void *vm_private_data;
            ...
        }
    
        struct vm_operations_struct {
            void(*open)(struct vm_area_struct *area); /*打开 VMA 的函数*/
            void(*close)(struct vm_area_struct *area); /*关闭 VMA 的函数*/
            struct page *(*nopage)(struct vm_area_struct *area, unsigned
     long address, int *type); /*访问的页不在内存时调用*/
    
            /* 当用户访问页前,该函数允许内核将这些页预先装入内存。
    驱动程序一般不必实现 */
            int(*populate)(struct vm_area_struct *area, unsigned long address, unsigned 
    long len, pgprot_t prot, unsigned long pgoff, int nonblock);
            ...
    

    建立页表的方法有两种:使用remap_pfn_range函数一次全部建立或者通过nopage VMA方法每次建立一个页表。

    • remap_pfn_range
      remap_pfn_range负责为一段物理地址建立新的页表,原型如下:

      int remap_pfn_range(struct vm_area_struct *vma, /* 虚拟内存区域,一定范围的页
      
      将被映射到该区域 */
              unsigned long addr, /* 重新映射时的起始用户虚拟地址。该函数为处于addr
      
      
      和addr+size之间的虚拟地址建立页表 */
              unsigned long pfn, /* 与物理内存对应的页帧号,实际上就是物理地址右
      
      移 PAGE_SHIFT 位 */
              unsigned long size, /* 被重新映射的区域大小,以字节为单位 */
              pgprot_t prot); /* 新页所要求的保护属性 */
      

      demo:

         static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)
         {
          if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma-
      
      >vm_start, vma->vm_page_prot)) /* 建立页表 */
              return - EAGAIN;
          vma->vm_ops = &xxx_remap_vm_ops; 
          xxx_vma_open(vma);
          return 0;
         }
      
      /* VMA 打开函数 */
      void xxx_vma_open(struct vm_area_struct *vma) 
      {
          ...
          printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma-
      
      >vm_start, vma->vm_pgoff "xxx VMA close.\n");
      }
      
      static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA 操作结构体 */
          .open = xxx_vma_open,
          .close = xxx_vma_close,
          ...
      };
      
    • nopage
      除了 remap_pfn_range()以外,在驱动程序中实现 VMA 的 nopage()函数通常可以为设备提供更加灵活的内存映射途径。当访问的页不在内存,即发生缺页异常时, nopage()会被内核自动调用。

      static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)
      {
          unsigned long offset = vma->vm_pgoff if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC))
              vma->vm_flags |= VM_IO;
          vma->vm_flags |= VM_RESERVED; /* 预留 */
          vma->vm_ops = &xxx_nopage_vm_ops;
          xxx_vma_open(vma);
          return 0;
      }
      
      struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long
       address, int *type)
      {
          struct page *pageptr;
          unsigned long offset = vma->vm_pgoff vm_start + offset; /* 物理地
      
      址 */
          unsigned long pageframe = physaddr >> PAGE_SHIFT; /* 页帧号 */
          if (!pfn_valid(pageframe)) /* 页帧号有效? */
              return NOPAGE_SIGBUS;
          pageptr = pfn_to_page(pageframe); /* 页帧号->页描述符 */
          get_page(pageptr); /* 获得页,增加页的使用计数 */
          if (type)
              *type = VM_FAULT_MINOR;
          return pageptr; /*返回页描述符 */
      }
      

      上述函数对常规内存进行映射, 返回一个页描述符,可用于扩大或缩小映射的内存区域。

    由此可见, nopage()与 remap_pfn_range()的一个较大区别在于 remap_pfn_range()一般用于设备内存映射,而 nopage()还可用于 RAM 映射,其调用发生在缺页异常时。

Durch diesen Artikel verstehen wir die Anwendung und Rolle der Speicherverwaltung in Linux-Gerätetreibern und lernen, wie man die Speicherverwaltung schreibt, aufruft, betreibt, ändert und debuggt. Wir haben festgestellt, dass die Speicherverwaltung eine Methode ist, die sich sehr gut für die Entwicklung eingebetteter Systeme eignet. Sie ermöglicht es uns, Speicherressourcen einfach zu beschreiben und zu steuern sowie eine effektive Nutzung und erweiterte Funktionen zu erreichen. Natürlich gibt es auch bei der Speicherverwaltung einige Vorsichtsmaßnahmen und Einschränkungen, z. B. die Notwendigkeit, Syntaxspezifikationen zu befolgen, auf Berechtigungsprobleme zu achten, auf Leistungseinbußen zu achten usw. Daher müssen wir bei der Verwendung der Speicherverwaltung über bestimmte Hardware-Kenntnisse und -Erfahrung sowie gute Programmiergewohnheiten und Debugging-Fähigkeiten verfügen. Ich hoffe, dieser Artikel kann Ihnen einen Einstiegsleitfaden bieten und Ihnen ein erstes Verständnis der Speicherverwaltung vermitteln. Wenn Sie mehr über die Speicherverwaltung erfahren möchten, empfiehlt es sich, auf weitere Materialien und Beispiele zurückzugreifen sowie selbst zu üben und zu erkunden.

Das obige ist der detaillierte Inhalt vonAusführliche Erläuterung der Speicherverwaltung von Linux-Gerätetreibern. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:lxlinux.net. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen