Maison > Tutoriel système > Linux > le corps du texte

Explication détaillée de la gestion de la mémoire du pilote de périphérique Linux

WBOY
Libérer: 2024-02-15 13:09:09
avant
567 Les gens l'ont consulté

Vous êtes-vous déjà demandé comment écrire des pilotes pour vos appareils sous Linux ? Vous êtes-vous déjà demandé comment faire en sorte que votre pilote utilise efficacement les ressources mémoire sous Linux ? Avez-vous déjà réfléchi à la manière de permettre à votre pilote d'implémenter certaines fonctions avancées dans un système Linux, telles que le mappage de la mémoire, l'allocation de mémoire, la protection de la mémoire, etc. ? Si ces problèmes vous intéressent, cet article vous présentera une méthode efficace pour atteindre ces objectifs : la gestion de la mémoire des pilotes de périphériques Linux. La gestion de la mémoire est une structure de données et un algorithme utilisés pour décrire et contrôler les ressources de mémoire. Elle vous permet de transférer des informations et des attributs de mémoire au noyau de manière simple et unifiée, allouant et libérant ainsi de la mémoire. La gestion de la mémoire est également un mécanisme d'utilisation efficace. Elle vous permet de définir et d'utiliser diverses opérations et commandes de mémoire de manière standard et universelle, réalisant ainsi des fonctions telles que le mappage de la mémoire, la copie et la protection. La gestion de la mémoire est également un cadre pour implémenter des fonctions avancées. Elle vous permet de définir et d'utiliser diverses interfaces et protocoles de mémoire de manière flexible et extensible, réalisant ainsi le partage de mémoire, le verrouillage, la mise en mémoire tampon et d'autres fonctions. Cet article vous donnera une introduction détaillée à l'application et au rôle de la gestion de la mémoire dans les pilotes de périphériques Linux à partir des concepts de base de la gestion de la mémoire, des règles grammaticales, des méthodes d'écriture, des processus d'appel, des méthodes de fonctionnement, etc., et vous aidera à maîtriser cet outil utile et méthode puissante.

  • Pour les processeurs contenant MMU, le système Linux fournit un système de gestion de stockage complexe, permettant à la mémoire à laquelle le processus peut accéder d'atteindre 4 Go. L'espace mémoire de 4 Go du processus est divisé en deux parties : l'espace utilisateur et l'espace noyau. Les adresses de l'espace utilisateur sont généralement réparties entre 0 et 3 Go (c'est-à-dire PAGE_OFFSET) De cette façon, les 3 à 4 Go restants sont de l'espace noyau.
    Les fonctions impliquées dans la mémoire des applications de l'espace noyau incluent principalement kmalloc(), __get_free_pages() et vmalloc(), etc.
    Grâce au mappage de la mémoire, les processus utilisateur peuvent accéder directement aux appareils dans l'espace utilisateur.


    Espace d'adressage du noyau

    L'espace utilisateur de chaque processus est complètement indépendant et sans rapport les uns avec les autres. Chaque processus utilisateur a une table de pages différente. L'espace du noyau est mappé par le noyau. Il ne change pas avec le processus et est fixe. Les adresses de l'espace du noyau ont leurs propres tables de pages correspondantes et l'espace virtuel du noyau est indépendant des autres programmes. Les processus utilisateur ne peuvent accéder à l'espace noyau que via des appels système (exécutés en mode noyau pour le compte du processus utilisateur).

    L'espace d'adressage du noyau de 1 Go sous Linux est divisé en zone de mappage de mémoire physique, zone d'allocation de mémoire virtuelle, zone de mappage de page haut de gamme, zone de mappage de page dédiée et zone de mappage réservée au système, comme le montre la figure.
    Explication détaillée de la gestion de la mémoire du pilote de périphérique Linux

    • Zone réservée

      Linux 保留内核空间最顶部 FIXADDR_TOP~4GB 的区域作为保留区。
      
      Copier après la connexion
    • Zone de mappage de pages dédiée

      紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START~
      
      FIXADDR_TOP),它的总尺寸和每一页的用途由 fixed_address 枚举结构在编译时预定
      
      义,用__fix_to_virt(index)可获取专用区内预定义页面的逻辑地址。
      
      Copier après la connexion
    • Zone de mappage de mémoire haut de gamme

      当系统物理内存大于 896MB 时,超过物理内存映射区的那部分内存称为高端内存(而未
      
      超过物理内存映射区的内存通常被称为常规内存),内核在存取高端内存时必须将它们映
      
      射到高端页面映射区。
      
      Copier après la connexion
    • Zone d'allocation de mémoire virtuelle

      用于 vmalloc()函数,它的前部与物理内存映射区有一个隔离带,后部与高端映射区也有
      
      一个隔离带。
      
      Copier après la connexion
    • Zone de cartographie de la mémoire physique

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

    Relation entre adresse virtuelle et adresse physique

    Pour la mémoire virtuelle du noyau zone de mappage de la mémoire physique, vous pouvez utiliser virt_to_phys() pour convertir l'adresse virtuelle du noyau en adresse physique. L'implémentation de virt_to_phys() est liée à l'architecture. Pour ARM, la définition de. virt_to_phys() est le suivant : :

        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)
    
    Copier après la connexion

    内存分配

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

    kmalloc()

        void *kmalloc(size_t size, int flags);
    
    Copier après la connexion

    给 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);
    
    Copier après la connexion

    __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);
    
    Copier après la connexion

    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);
    
    Copier après la connexion

    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);
    
    Copier après la connexion

    内存映射

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

        int(*mmap)(struct file *, struct vm_area_struct*);
    
    Copier après la connexion

    驱动程序中 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);
            ...
    
    Copier après la connexion

    建立页表的方法有两种:使用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); /* 新页所要求的保护属性 */
      
      Copier après la connexion

      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,
          ...
      };
      
      Copier après la connexion
    • 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; /*返回页描述符 */
      }
      
      Copier après la connexion

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

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

Grâce à cet article, nous comprenons l'application et le rôle de la gestion de la mémoire dans les pilotes de périphériques Linux, et apprenons à écrire, appeler, exploiter, modifier et déboguer la gestion de la mémoire. Nous avons constaté que la gestion de la mémoire est une méthode très adaptée au développement de systèmes embarqués. Elle nous permet de décrire et de contrôler facilement les ressources mémoire et d'obtenir une utilisation efficace et des fonctions avancées. Bien entendu, la gestion de la mémoire comporte également certaines précautions et restrictions, telles que la nécessité de suivre les spécifications de syntaxe, la nécessité de prêter attention aux problèmes d'autorisation, la nécessité de prêter attention aux impacts sur les performances, etc. Par conséquent, lorsque nous utilisons la gestion de la mémoire, nous devons avoir certaines connaissances et expériences en matière de matériel, ainsi que de bonnes habitudes de programmation et des compétences en débogage. J'espère que cet article pourra vous fournir un guide d'entrée de gamme et vous donner une compréhension préliminaire de la gestion de la mémoire. Si vous souhaitez en savoir plus sur la gestion de la mémoire, il est recommandé de vous référer à davantage de documents et d'exemples, ainsi que de pratiquer et d'explorer par vous-même.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:lxlinux.net
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal