>  기사  >  시스템 튜토리얼  >  Linux 장치 드라이버의 메모리 관리에 대한 자세한 설명

Linux 장치 드라이버의 메모리 관리에 대한 자세한 설명

WBOY
WBOY앞으로
2024-02-15 13:09:09537검색

Linux에서 장치용 드라이버를 작성하는 방법이 궁금하신가요? Linux에서 드라이버가 메모리 리소스를 효율적으로 사용하도록 만드는 방법에 대해 궁금한 적이 있습니까? 드라이버가 Linux 시스템에서 메모리 매핑, 메모리 할당, 메모리 보호 등과 같은 일부 고급 기능을 구현하도록 하는 방법에 대해 생각해 본 적이 있습니까? 이러한 문제에 관심이 있다면 이 기사에서는 이러한 목표를 달성하기 위한 효과적인 방법인 Linux 장치 드라이버 메모리 관리를 소개합니다. 메모리 관리는 메모리 자원을 설명하고 제어하는 ​​데 사용되는 데이터 구조 및 알고리즘으로, 메모리 정보와 속성을 간단하고 통일된 방식으로 커널에 전송하여 메모리를 할당하고 해제할 수 있습니다. 메모리 관리는 효과적인 사용을 위한 메커니즘이기도 합니다. 이를 통해 다양한 메모리 작업과 명령을 표준적이고 보편적인 방식으로 정의하고 사용할 수 있으므로 메모리 매핑, 복사, 보호 등의 기능을 실현할 수 있습니다. 메모리 관리는 고급 기능을 구현하기 위한 프레임워크이기도 하며 다양한 메모리 인터페이스와 프로토콜을 유연하고 확장 가능한 방식으로 정의하고 사용할 수 있도록 하여 메모리 공유, 잠금, 버퍼링 및 기타 기능을 실현합니다. 이 기사에서는 메모리 관리, 문법 규칙, 작성 방법, 호출 프로세스, 작업 방법 등의 기본 개념부터 Linux 장치 드라이버의 메모리 관리 응용 및 역할에 대해 자세히 소개하고 이러한 유용하고 유용한 기능을 익히는 데 도움을 줍니다. 강력한 방법.

  • MMU가 포함된 프로세서의 경우 Linux 시스템은 복잡한 스토리지 관리 시스템을 제공하여 프로세스가 액세스할 수 있는 메모리가 4GB에 달할 수 있도록 합니다. 프로세스의 4GB 메모리 공간은 사용자 공간과 커널 공간의 두 부분으로 나뉩니다. 사용자 공간 주소는 일반적으로 0~3GB(예: PAGE_OFFSET)로 분산되며, 나머지 3~4GB는 커널 공간입니다.
    커널 공간 애플리케이션 메모리와 관련된 함수에는 주로 kmalloc(), __get_free_pages() 및 vmalloc() 등이 포함됩니다.
    메모리 매핑을 통해 사용자 프로세스는 사용자 공간의 장치에 직접 액세스할 수 있습니다.


    커널 주소 공간

    각 프로세스의 사용자 공간은 완전히 독립적이며 서로 관련이 없습니다. 각 사용자 프로세스에는 서로 다른 페이지 테이블이 있습니다. 커널 공간은 커널에 의해 매핑되며 프로세스에 따라 변경되지 않고 고정됩니다. 커널 공간 주소에는 해당 페이지 테이블이 있으며 커널의 가상 공간은 다른 프로그램과 독립적입니다. 사용자 프로세스는 시스템 호출(사용자 프로세스를 대신하여 커널 모드에서 실행)을 통해서만 커널 공간에 액세스할 수 있습니다.

    Linux의 1GB 커널 주소 공간은 그림과 같이 물리적 메모리 매핑 영역, 가상 메모리 할당 영역, 고급 페이지 매핑 영역, 전용 페이지 매핑 영역 및 시스템 예약 매핑 영역으로 구분됩니다.
    Linux 장치 드라이버의 메모리 관리에 대한 자세한 설명

    • 예약구역

      으아악
    • 전용 페이지 매핑 영역

      으아악
    • 고급 메모리 매핑 영역

      으아악
    • 가상 메모리 할당 영역

      으아악
    • 물리적 메모리 매핑 영역

      으아악

    가상 주소와 실제 주소의 관계

    커널의 가상 메모리물리적 메모리 매핑 영역의 경우 virt_to_phys()를 사용하여 커널 가상 주소를 물리적 주소로 변환할 수 있습니다. ARM의 경우 virt_to_phys 정의는 아키텍처와 관련되어 있습니다. ()는 다음과 같습니다 :

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

이 기사를 통해 우리는 Linux 장치 드라이버에서 메모리 관리의 응용과 역할을 이해하고, 메모리 관리를 작성, 호출, 작동, 수정 및 디버깅하는 방법을 알아봅니다. 우리는 메모리 관리가 임베디드 시스템 개발에 매우 ​​적합한 방법이라는 것을 알았습니다. 이를 통해 메모리 리소스를 쉽게 설명하고 제어할 수 있으며 효과적인 사용과 고급 기능을 얻을 수 있습니다. 물론 메모리 관리에는 구문 사양을 따라야 하고, 권한 문제에 주의를 기울여야 하며, 성능에 미치는 영향에 주의해야 하는 등 몇 가지 예방 조치와 제한 사항도 있습니다. 따라서 메모리 관리를 사용할 때는 특정 하드웨어 지식과 경험은 물론 좋은 프로그래밍 습관과 디버깅 기술도 필요합니다. 이 기사가 여러분에게 입문 수준 가이드를 제공하고 메모리 관리에 대한 사전 이해를 제공할 수 있기를 바랍니다. 메모리 관리에 대해 더 자세히 알고 싶다면 더 많은 자료와 예제를 참고하고 스스로 연습하고 탐색해 보는 것이 좋습니다.

위 내용은 Linux 장치 드라이버의 메모리 관리에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 lxlinux.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제