Rumah  >  Artikel  >  Tutorial sistem  >  Penjelasan terperinci tentang pengurusan memori pemacu peranti Linux

Penjelasan terperinci tentang pengurusan memori pemacu peranti Linux

WBOY
WBOYke hadapan
2024-02-15 13:09:09537semak imbas

Pernahkah anda terfikir cara menulis pemacu untuk peranti anda di Linux? Pernahkah anda terfikir bagaimana untuk membuat pemandu anda menggunakan sumber memori dengan cekap dalam Linux? Pernahkah anda berfikir tentang cara untuk membolehkan pemandu anda melaksanakan beberapa fungsi lanjutan dalam sistem Linux, seperti pemetaan memori, peruntukan memori, perlindungan memori, dll.? Jika anda berminat dengan isu ini, maka artikel ini akan memperkenalkan anda kepada kaedah yang berkesan untuk mencapai matlamat ini-pengurusan memori pemacu peranti Linux. Pengurusan memori ialah struktur data dan algoritma yang digunakan untuk menerangkan dan mengawal sumber memori Ia membolehkan anda memindahkan maklumat memori dan atribut ke kernel dengan cara yang mudah dan bersatu, dengan itu memperuntukkan dan melepaskan memori. Pengurusan memori juga merupakan mekanisme untuk penggunaan yang berkesan Ia membolehkan anda mentakrifkan dan menggunakan pelbagai operasi dan arahan memori dengan cara yang standard dan universal, dengan itu merealisasikan fungsi seperti pemetaan memori, penyalinan dan perlindungan. Pengurusan memori juga merupakan rangka kerja untuk melaksanakan fungsi lanjutan Ia membolehkan anda mentakrif dan menggunakan pelbagai antara muka dan protokol memori dengan cara yang fleksibel dan boleh diperluaskan, dengan itu merealisasikan perkongsian memori, penguncian, penimbalan dan fungsi lain. Artikel ini akan memberi anda pengenalan terperinci kepada aplikasi dan peranan pengurusan memori dalam pemacu peranti Linux daripada konsep asas pengurusan memori, peraturan tatabahasa, kaedah penulisan, proses panggilan, kaedah operasi, dsb., dan membantu anda menguasai perkara ini yang berguna dan kaedah yang berkuasa.

  • Untuk pemproses yang mengandungi MMU, sistem Linux menyediakan sistem pengurusan storan yang kompleks, membolehkan memori yang boleh diakses oleh proses mencapai 4GB. Ruang memori 4GB proses dibahagikan kepada dua bahagian - ruang pengguna dan ruang kernel. Alamat ruang pengguna biasanya diedarkan antara 0 dan 3GB (iaitu PAGE_OFFSET Dengan cara ini, baki 3 hingga 4GB adalah ruang kernel.
    Fungsi yang terlibat dalam memori aplikasi ruang kernel terutamanya termasuk kmalloc(), __get_free_pages() dan vmalloc(), dsb.
    Melalui pemetaan memori, proses pengguna boleh terus mengakses peranti dalam ruang pengguna.


    Ruang alamat kernel

    Ruang pengguna setiap proses adalah bebas sepenuhnya dan tidak berkaitan antara satu sama lain. Setiap proses pengguna mempunyai jadual halaman yang berbeza. Ruang kernel dipetakan oleh kernel Ia tidak berubah dengan proses dan tetap. Alamat ruang kernel mempunyai jadual halaman yang sepadan, dan ruang maya kernel adalah bebas daripada program lain. Proses pengguna hanya boleh mengakses ruang kernel melalui panggilan sistem (melaksanakan dalam mod kernel bagi pihak proses pengguna).

    Ruang alamat kernel 1GB di Linux dibahagikan kepada kawasan pemetaan memori fizikal, kawasan peruntukan memori maya, kawasan pemetaan halaman mewah, kawasan pemetaan halaman khusus dan kawasan pemetaan simpanan sistem, seperti yang ditunjukkan dalam rajah.
    Penjelasan terperinci tentang pengurusan memori pemacu peranti Linux

    • Kawasan simpanan

      Linux 保留内核空间最顶部 FIXADDR_TOP~4GB 的区域作为保留区。
      
    • Kawasan pemetaan halaman khusus

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

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

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

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

    Hubungan antara alamat maya dan alamat fizikal

    Untuk memori maya kernel kawasan pemetaan memori fizikal, anda boleh menggunakan virt_to_phys() untuk menukar alamat maya kernel kepada alamat fizikal Pelaksanaan virt_to_phys() adalah berkaitan dengan seni bina virt_to_phys() adalah seperti berikut: :

        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 映射,其调用发生在缺页异常时。

Melalui artikel ini, kami memahami aplikasi dan peranan pengurusan memori dalam pemacu peranti Linux, dan mempelajari cara menulis, memanggil, mengendalikan, mengubah suai dan menyahpepijat pengurusan memori. Kami mendapati bahawa pengurusan memori adalah kaedah yang sangat sesuai untuk pembangunan sistem terbenam Ia membolehkan kami menerangkan dan mengawal sumber memori dengan mudah dan mencapai penggunaan yang berkesan dan fungsi lanjutan. Sudah tentu, pengurusan memori juga mempunyai beberapa langkah berjaga-jaga dan sekatan, seperti keperluan untuk mengikuti spesifikasi sintaks, keperluan untuk memberi perhatian kepada isu kebenaran, keperluan untuk memberi perhatian kepada kesan prestasi, dsb. Oleh itu, apabila menggunakan pengurusan memori, kita perlu mempunyai pengetahuan dan pengalaman perkakasan tertentu, serta tabiat pengaturcaraan yang baik dan kemahiran penyahpepijatan. Saya harap artikel ini dapat memberi anda panduan peringkat permulaan dan memberi anda pemahaman awal tentang pengurusan memori. Jika anda ingin mengetahui lebih lanjut tentang pengurusan memori, anda disyorkan untuk merujuk kepada lebih banyak bahan dan contoh, serta berlatih dan meneroka sendiri.

Atas ialah kandungan terperinci Penjelasan terperinci tentang pengurusan memori pemacu peranti Linux. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:lxlinux.net. Jika ada pelanggaran, sila hubungi admin@php.cn Padam