首页 > 系统相关 >【Linux驱动设备开发详解】11.内存与I/O访问

【Linux驱动设备开发详解】11.内存与I/O访问

时间:2024-06-07 14:54:50浏览次数:18  
标签:11 DMA struct vm unsigned 详解 内存 Linux vma

1.内存管理单元

高性能处理器一般会提供一个内存管理单元(MMU),用于辅助操作系统尽心修改内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。

1.1MMU基本概念

1.1.1 概念含义

1.TLB(Translation Lookaside Buffer):

旁路转换缓存,TLB是MMU的核心不见,它缓存少量的虚拟地址和物理地址的转换关系,是转换表的cache,也经常被称为"快表"

2.TTW(Translation Table walk):

转换表漫游,当TLB中没有缓冲对应的地址转换关系时,需要通过内对内存中转换表的访问来获得虚拟地址和物理地址的对应关系。TTW成功后,结果会写入TLB中。

1.1.2 ARM处理器访问内存的过程

(1)当ARM要访问存储器时,MMU先查找TLB中的虚拟地址表。如果ARM的结构支持分开的数据TLB(DTLB)和指令TLB(ITLB),则除了取指令使用ITLB外,其他的都使用DTLB。

image

(2)若TLB中没有虚拟地址的入口,则转换表遍历硬件并从存放于主存储器内的转换表中获取地址转换信息和访问权限(即执行TTW),同时将这些信息放入TLB。之后,在TLB条目中控制信息的控制下,当访问权限允许时,对真实物理地址的访问将在Cache或者内存中发生。

image

2.Linux内存管理

2.1 内存空间

在Linux系统中,进程的4GB内存空间被分为两个部分 -- 用户空间与内核空间。用户空间一般分布为0-3GB,剩下的3-4GB为内核空间。用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只有通过系统调用等方式才能访问到内核空间。

image

每个进程的用户空间完全独立,互不相干,用户进程各自有不同的页表。而内核空间是由内核负责映射,他并不会跟着进程改变,是固定的。

内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其他程序

2.2 内核空间

Linux中1GB的内核地址空间又被划分为物理内存映射区虚拟内存分配区高端页面映射区专用页面映射区系统保留映射区

image

2.2.1 在x86系统下内存区划分

物理内存映射区: 最大长度为896M,系统的物理内存被顺序映射在内核空间的这个区域中。

高端内存映射区:当系统物理内存大于896MB时,超过物理内存映射区的那部分内存称为高端内存,而未超过物理内存映射区的内存通常被称为常规内存,内核在存取高端内存时必须将他们映射到高端内存区

保留区:Linux保留内核空间最顶部FIXADDR_TOP-4GB的区域

专用页面映射区:地址空间位于FIXADDR_START-FIXADDR_TOP,它的总尺寸和每一页的用途由fixed_address枚举结构在编译时预定义,用__fix_to_virt(index)可获取专用区预定义页面的逻辑地址。开始地址和结束地址宏定义如下:

#define FIXADDR_START     (FIXADDR_TOP - __FIXADDR_SIZE)
#define FIXADDR_TOP        ((unsigned long)__FIXADDR_TOP)
#define __FIXADDR_TOP      0xfffff000

高端内存映射区:如果系统配置了高端内存,则位于专用页面映射区之下,起始地址为PKMAP_BASE,定义如下:

#define PKMAP_BASE   ((FIXADDR_BOOT_START - PAGE_SIZE*(LAST_PKMAP+1)) & PMD_MASK)

2.2.2 在32位ARM下内存区划分

ARM系统Linux内核地址空间中的内核模块区域、高端内存映射区、vmalloc、向量表区域等。

image

内存区有几种DMA、常规、高端内存区域可能的分布:

  1. 有硬件的DMA引擎不能访问全部地址,且内存较大而无法全部在内核空间虚拟地址映射下,存放有3个区域
  2. 没有硬件的DMA引擎不能访问全部地址,且内存较大而无法全部在内核空间虚拟地址映射下,则常规区域实际退化为0
  3. 有硬件的DMA引擎不能全部访问全部地址,且内存较小可以全部在内核空间虚拟地址映射下,则高端内存区域实际退化为0
  4. 没有硬件的DMA引擎不能访问全部地址,且内存较小可以全部在内核空间虚拟地址映射下,则常规和高端内存区域实际退化为0

image

DMA、常规、高端内存这3个区域都采用buddy算法进行管理,把空闲的页面以2的n次方位单位进行管理,因此Linux最底层的内存申请都是以2n次方位单位的。Buddy算法的最主要的优点是避免了外部碎片,任何时候区域里的空闲内存都能以2的n次方进行拆分或合并。

image

/proc/buddyinfo会显示每个区域里面2n的空闲页面分布情况,比如:

$cat /proc/buddyinfo
Node 0, zone DMA 8 5 2 7 8 3 0 0
0 1 0
Node 0, zone Normal 2002 1252 524 187 183 71 7 0
0 1 1

上述结果显示:

DMA区域里1页空闲的内存还有8个,连续2页空闲的有5个,连续4页空闲的有2个

常规区域里面1页空闲的还有2002个,连续2页空闲的有1252个,以此类推

对于内核物理内存映射区得到虚拟内存(从DMA和常规区域映射过来,高端内存的虚拟地址与物理地址之间不存在如此简单的换算关系),使用virt_to_phys()可以实现内核虚拟地址转化为物理地址。与之对应的函数为phys_to_virt(),它将物理地址转化为内核虚拟地址。

3.内存存取

3.1用户空间内存动态申请

用户空间中动态申请内存的函数为malloc(),对应的释放函数free()。对于Linux来说,C库的malloc()函数一般通过brk()和mmap()这两个系统调用从内核申请内存。

因为用户空间C库的malloc算法实际上具备二次管理能力,所以并不是每次申请和释放内存都一定伴随着对内核的系统调用。

用户空间内存申请以及mallopt:

#include <malloc.h>
#include <sys/mman.h>

#define SOMESIZE   (100*1024*1024)

int main(int argc,char *argv[])
{
    unsigned char *buffer;
    int i;
  
    if(mlockall(MCL_CURRENT | MCL_FUTURE))
        mallopt(M_TRIM_THERSHOLD,-1);
    mallopt(M_MMAP_MAX,0);

    buffer = malloc(SOMESIZE);
    if(!buffer)
        exit(-1);

    // 将每个页面都实际映射到RAM
    for(i=0;i < SOMESIZE;i+=page_size)
    {
        buffer[i] = 0;
    }
    free(buffer);

    return 0;
}

这个free()并不会把内存还给内核,而只是还给了C库的分配算法,因此之后所有的动态内存申请和释放都在用户态下进行。

另外,Linux内核总是按需调页(Demand paging),因此当malloc()返回的时候,虽然是成功返回,但是内核并没有真正给这个进程内存,这个时候如果去读申请的内存,内容全部是0,这个页面的映射是只读的,只有当写到某个页面的时候,内核才在页错误后,真正把这个页面给这个进程。

3.2 内核空间内存动态申请

Linux内核空间中申请内存涉及的函数主要包括kmalloc(),__get_free_pages()和vmalloc()等。

kmalloc()和__get_free_pages():申请的内存位于DMA和常规区域的映射区,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移。

vmalloc():在虚拟内存空间给出的一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,申请出来的虚拟内存和物理内存之间也没有简单的换算关系

3.2.1.kmalloc()

函数原型

void *kmalloc(size_t size,int flags);

参数说明

size:分配块的大小

flags:分配标志,用于控制kmalloc()的行为

分配标志

kmalloc()的底层依赖于__get_free_pages()来实现,分配标志的前缀GFP正好时这个底层函数的缩写。

GFP_KERNEL:表示在内核空间的进程中申请内存,若暂时不能满足,则进程会睡眠等待页,即会引起阻塞,因此不能在中断上下文或持有自旋锁的时候使用GFP_KERNE申请内存。

GFP_ATOMIC:用于在中断处理函数,tasklet等非进程上下文中不能阻塞的场景下使用。在申请内存时,若不存在空闲页,则不等待,直接返回

GFP_USER:为用户空间页分配内存,可能阻塞

GFP_HIGHUSER:类似GFP_USER,但是它从高端内存分配

GFP_DMA:从DMA区域分配内存

GFP_NOIO:不允许任何I/O初始化

GFP_NOFS:不允许进行任何文件系统调用

使用kmalloc()申请的内存应使用kfree()释放。

3.2.1. __get_free_pages()

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

__get_free_pages()系列函数/宏包含get_zerod _page() , __get _free _page()和__get _free _pages()。

返回一个指向新页的指针并将该页清零:

get_zeroed_page(unsigned int flags);

返回一个指向新页的指针但是该页不清零

__get_free_page(unsigned int flags);

get_zerod _page()和 __get _free _page()在实现中调用了alloc _pages(),alloc _pages()既可以在内核空间分配,也可以在用户空间分配,其原型为:

struct page *alloc_page(int gfp_mask,unsigned long order);

它返回分配的第一个页的描述符而非首地址。

使用__get_free_pages()系列函数/宏申请的内存应使用下列对函数释放:

void free_page(unsigned long addr);
void free_page(unsigned long addr,unsigned long order);

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

3.2.5 vmalloc()

vmalloc()一般置为存在于软件中较大的顺序缓冲区分配内存,它没有对应硬件的意义,是纯软件的内存分配。

为了完成vmalloc(),需要建立新的页表项,所以它不适合用来分配少量的内存(如1页以内)

vmalloc()申请的内存应使用vfree()释放,vmalloc和vfree的函数原型如下:

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

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

vmalloc()在申请内存时,会进行内存的映射,改变页表项,不像kmalloc()实际用的开机过程中就映射好的DMA和常规区域的页表项。因此vmalloc()虚拟地址和物理地址不是一个简单的线性映射。

3.2.4 slab与内存池

slab

slab算法是针对涉及大量对象的重复生成、使用和释放内存的场景设计的,它可以使对象在前后两次被使用时分配在同一块内存或同一类内存空间且保留了基本的数据结构,大大提高效率。kmalloc()就是使用slab机制实现的。

slab算法建立在buddy算法之上,它从buddy算法拿到2n页面后再次进行二次管理。slab申请的内存以及基于slab的kmalloc()申请的内存,与物理内存之间也是简单的线性偏移。

(1) 创建slab缓存

struct kmem_cache *kmem_cache_create(const char *name,size_t size,size_t align,unsigned long flags,
					void (*ctor)(void*,struct kmem_cache *,unsigned long),
					void (*dtor)(void*,struct kmem_cache *,unsigned long));

kmem_cache_create()用于创建一个slab缓存,它可以保留任意数目且全部同样大小的后备缓存。

参数size:要分配的每个数据结构的大小

参数flags:如何进行分配的位掩码,包括SLAB_HWCACHE_ALIGN(每个数据对象被对齐到一个缓存行)、SLAB_CACHE_DMA(要求数据对象在DMA区域中分配)等。

(2) 分配slab缓存

void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags);

kmem_cache_alloc()在kmem_cache_create()创建的slab后备缓存中分配一块并返回首地址指针。

(3) 释放slab缓存

void kmem_cache_free(struct kmem_cache *cachep,void *objp);

用于释放kmem_cache_alloc()分配的缓存

(4) 回收slab缓存

int kmem_cache_destroy(struct kmem_cache *cachep);

(5) slab缓存使用示例

// 创建slab缓存
static kmem_cache_t  *xxx_cachep;
xxx_cachep = kmem_cache_create("xxx",sizeof(struct xxx),0,SLAB_HWCACHE_ALIGN|SLAB_PANIC,NULL,NULL);

// 分配slab缓存
struct xxx *ctx;
ctx = kmem_cache_alloc(xxx_cachep,GFP_KERNEL);

// 使用slab缓存
...

// 释放slab缓存
kmem_cache_free(xxx_cachep *cachep);
kmem_cache_destroy(xxx_cachep);

(6)获取当前系统中的slab的分配和使用情况

# sudo cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
nf_conntrack         500    500    320   25    2 : tunables    0    0    0 : slabdata     20     20      0
au_finfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
au_icntnr              0      0    832   19    4 : tunables    0    0    0 : slabdata      0      0      0
au_dinfo               0      0    192   21    1 : tunables    0    0    0 : slabdata      0      0      0
ovl_inode           2459   2668    688   23    4 : tunables    0    0    0 : slabdata    116    116      0
nfs4_layout_stateid      0      0    280   29    2 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_delegations      0      0    232   17    1 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_stateids         0      0    168   24    1 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_files            0      0    288   28    2 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_lockowners       0      0    392   20    2 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_openowners       0      0    432   18    2 : tunables    0    0    0 : slabdata      0      0      0
nfsd4_clients          0      0   1296   25    8 : tunables    0    0    0 : slabdata      0      0      0
rpc_inode_cache       46     46    704   23    4 : tunables    0    0    0 : slabdata      2      2      0
ext4_groupinfo_4k   2268   2268    144   28    1 : tunables    0    0    0 : slabdata     81     81      0
fsverity_info          0      0    248   16    1 : tunables    0    0    0 : slabdata      0      0      0
ip6-frags              0      0    184   22    1 : tunables    0    0    0 : slabdata      0      0      0
PINGv6                 0      0   1216   26    8 : tunables    0    0    0 : slabdata      0      0      0
RAWv6                286    286   1216   26    8 : tunables    0    0    0 : slabdata     11     11      0

slab的最底层仍然依赖于__get_free_pages(),slab在底层每次申请一页或多页,之后再分隔这些页位更小的单元进行管理,从而节省了内存,也提高了slab的缓冲对象的访问效率。

内存池

内存池技术是一种非常经典用于分配大量小对象的后备缓存技术。

(1)创建内存池

mempool_t *mempool_create(int min_nr,mempool_alloc_t *alloc_fn,mempool_free_t *free_fn,void *pool_data);

mempool_create用于创建一个内存池

min_nr:预分配对象的数目

alloc_fn:标准对象分配函数的指针

free_fn:标准对象回收函数的指针

(2) 分配和回收对象

在内存池中分配和回收对象需由以下函数来完成:

void *mempool_alloc(mempool_t *pool,int gfp_mask);     // 用来分配对象,如果内存池分配器无法提供内存,那么就可以用预分配的池
void *mempool_free(void *element,mempool_t *pool);

(3) 回收内存池

void mempool_destroy(mempool_t *pool);                        // 回收mempool_create创建的内存池

4.设备I/O端口和I/O内存的访问

设备会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。这些寄存器既可以位于内存空间,也可以位于I/O空间。

当位于内存空间时,对应的内存空间被称为I/O内存。

当位于I/O空间时,通常被称为I/O端口

4.1 Linux I/O端口和I/O内存访问接口

4.1.1 I/O端口

Linux设备驱动中,应使用Linux内核提供的函数来访问定位于I/O空间的端口,包括如下几种:

1.读写字节端口(8位宽)

unsigned inb(unsigned port);
void outb(unsigned char byte,unsigned port);

2.读写字节端口(16位宽)

unsigned inw(unsigned port);
void outw(unsigned short byte,unsigned port);

3.读写字节端口(32位宽)

unsigned inl(unsigned port);
void outl(unsigned longword,unsigned port);

4.读写一串字节

void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);

5.insb()从端口port开始读count个字节端口,并将读取结果写入addr指向的内存;outsb()将addr指向的内存中的count个字节连续写入以port开始的端口

6.读写一串字

void insw(unsigned port,void *addr,unsigned long count);
void outsw(unsigned port,void *addr,unsigned long count);

7.读写一串长字

void insl(unsigned port,void *addr,unsigned long count);
void outsl(unsigned port,void *addr,unsigned long count);

以上的函数中I/O端口号port类型高度依赖于具体的硬件平台,因此这里只写出了unsigned,具体类型以硬件平台为准

4.1.2 I/O内存

在内核中访问I/O内存之前,需要先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。ioremap()的原型如下:

void *ioremap(unsigned long offset,unsigned long size);

ioremap()与vmalloc()类似,也需要建立新的页表,但是它并不进行vmalloc()中所执行的内存分配行为。

ioremap()返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围,这个虚拟地址位于vmalloc()映射区域。通过ioremap()获得的虚拟地址应该被iounmap()函数释放,函数原型为:

void iounmap(void *addr);

ioremap()有一个变体是devm_ioremap(),类似于其他以devm_开头的函数,通过devm_ioremap()进行的映射通常不需要在驱动退出和出错处理的时候进行iounmap()。devm_iorqmap()的原型为:

void __iomem *devm_ioremap(struct device *dev,resource_size_t offset,unsigned long size);

使用上述函数可以将设备的物理地址映射到虚拟地址,然后就可以直接通过指针访问这些地址了,但是Linux内核并不推荐这么用,内核更推荐用一组标准的API来完成设备内存映射的虚拟地址的读写。

读寄存器用readb_relaxed()、readw_relaxed()、readl_relaxed()、readb()、readw()、readl()这一组API,分别读8bit、16bit、32bit的寄存器,没有_relaxed后缀的版本相比有_relaxed后缀的版本多包含了一个内存屏障。如:

#define readb(c)   ({ u8 __v = readb_relaxed(c); __iormb() ; __v;})
#define readw(c)   ({ u16 __v = readw_relaxed(c); __iormb(); __v;})
#define readl(c)   ({ u32 __v = readl_relaxed(c); __iormb(); __v;})

写寄存器用writeb_relaxed()、writew_relaxed()、writel_relaxed()、writeb()、writew()、writel()这一组API,分别写8bit、16bit、32bit的寄存器,没有_relaxed后缀版本相比有_relaxed后缀多包含了一个内存屏障,如:

#define writeb(c)   ({ u8 __v = writeb_relaxed(c); __iormb() ; __v;})
#define writew(c)   ({ u16 __v = writew_relaxed(c); __iormb(); __v;})
#define writel(c)   ({ u32 __v = writel_relaxed(c); __iormb(); __v;})

4.2 申请与释放设备的I/O端口和I/O内存

4.2.1 I/O端口申请

Linux内核提供了一组函数用于申请和释放I/O端口,表明要访问这片区域

申请I/O端口

struct resource *request_region(unsigned long first,unsigned long n,const char *name);

这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名称。如果分配成功,则正常返回,如果分配失败,则返回NULL

释放I/O端口

void release_region(unsigned long start,unsigned long n);

当申请的I/O端口使用完成后,使用此函数将它们归还给系统

4.2.2 I/O内存申请

Linux内核也提供了一组函数以申请和释放I/O内存的范围,这里的"申请"表明该驱动要访问这片区域,它不会做任何内存映射的动作。

申请I/O内存:

struct resource *request_mem_region(unsigned long start,unsigned long len,char *name);

此函数向内核申请n个内存地址,这些地址从first开始,name参数为设备的名称。如果分配成功则正常返回,如果失败则返回NULL,意味着I/O内存失败

释放I/O内存

void release_mem_region(unsigned long start,unsigned long len);

当申请的I/O内存使用完成后,使用此函数将内存归还给系统。

4.3 设备I/O端口和I/O内存访问流程

通过以上内容可以归纳总结处设备驱动访问I/O端口和I/O内存的步骤

4.3.1 I/O端口的访问步骤

I/O端口访问的一种途径是直接使用I/O端口操作函数:

1.在设备打开或驱动模块被加载时申请I/O端口区域

2.使用inb()、outb()等进行端口访问

3.在设备关闭或驱动被卸载时释放I/O端口范围。

image

4.3.2 I/O内存的访问步骤

  1. 调用request_mem_region()申请资源,
  2. 将寄存器地址通过ioremap()映射到内核空间虚拟地址
  3. 通过Linux设备访问编程接口访问这些设备的寄存器
  4. 应对ioremap()申请的虚拟地址进行释放
  5. 释放release_mem_region()申请的I/O内存资源

image

4.4 将设备地址映射到用户空间

4.4.1 内存映射与VMA

通常情况下,用户空间无法直接访问设备,但是设备驱动程序可以实现mmap()函数,这个函数可以使得用户空间能直接访问设备得物理地址。

mmap():实现了一个映射过程,它将用户空间得一段内存与设备内存关联,当用户访问用户空间的这段地址时,实际上会转化为对设备的访问。

驱动中的mmap()将在用户进行mmap()系统调用时最终被调用

file_operations文件操作结构体中的驱动中mmap()函数原型

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

mmap()系统调用的原型

caddr_t mmap(caddr_t addr,size_t len,int prot,int flags,int fd,off_t offset);

fd:文件描述符,一般由open()返回,fd也可指定为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射

prot:指定访问权限,可取下面几个值的或:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)和PROT_NONE(不可访问)

addr:指定文件应被映射到用户空间的起始地址,一般被指定为NULL,这样选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型caddr_t实际上就是void*

len:映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起

offset:一般设为0,表示从文件头开始映射

当用户调用mmap()的时候,内核会进行如下处理

(1)在进程的虚拟空间查找一块VMA

(2)将这块VMA进行映射

(3)如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,则调用它。

(4)将这个VMA插入进程的VMA链表

file_operations中mmap()函数中第一个参数就是步骤(1)找到的VMA

由mmap()系统调用的内存可由munmap()解除映射,这个函数的原型是:

int munmap(caddr_t addr,size_t len);

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

struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

	atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

VMA结构体描述的虚地址介于vm_start和vm_end之间,而其vm_ops成员指向这个VMA的操作集,针对VMA的操作都被包含在vm_operations_struct结构体中,vm_operations_struct结构体的定义:

struct vm_operations_struct
{
     void (*open)(struct vm_area_struct *area);
     void (*close)(struct vm_area_struct *area);
     int (*fault)(struct vm_area_struct *vma,struct vm_fault *vmf);
     void (*map_pages)(struct vm_area_struct *vma,struct vm_fault *vmf);
     int (*page_mkwrite)(struct vm_area_struct *vma,struct vm_fault *vmf);
     int (*access)(struct vm_area_struct *vma,unsigned long addr,void *buf,int len,int write);
};

整个vm_operations_struct结构体的实体会在file_operations的mmap()成员函数里被赋值给相应的vma->vm_ops,而上述open()函数也通常在mmap()里调用,close()函数会在用户调用munmap()的时候被调用到。以下清单给出了vm_opertions_struct的操作范例:

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))/* 建立页表,映射地址范围vma->start到vma->vm_end*/
    return -EAGAIN;
    vma->vm_ops = &xxx_remap_vm_ops;
    xxx_vma_open(vma);
    return 0;
}

static 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 << PAGE_SHIFT);
}

static void xxx_vma_close(struct vm_area_struct *vma)
{
   ...
   printk(KERN_NOTICE "xxx VMA close.\n");
}

static struct vm_operations_struct xxx_remap_vm_ops = 
{
    .open = xxx_vma_open,
    .close = xxx_vma_close,
    ....
};

remap_pfn_range()创建页表项

函数原型

int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn,unsigned long size,pgprot_t prot);

参数说明

addr: 内存映射开始处的虚拟地址。remap_pfn_range为addr~addr+size的虚拟地址构造页表

pfn:虚拟地址应该映射到的页帧号,实际上是物理地址右移PAGE_SHIFT位。若PAGE_SIZE为4KB,则PAGE_SHIFT为12,因为PAGE_SIZE等于1<<PAGE_SHIFT。

prot:新页所要求的保护属性

在驱动程序中,可以使用remap_pfn_range()映射内存中的保留页、设备I/O,framebuffer、camera等内存。在remap_pfn_range()上又可以进一步封装出io_remap_pfn_range()、vm_iomap_memory()等API。

#define io_remap_pfn_range remap_pfn_reange
int vm_iomap_memory(struct vm_area_struct *vma,phys_addr_t start,unsigned long len)
{
    unsigned long vm_len, pfn, pages;
    ...
    len += start & ~PAGE_MASK;
    pfn = start >> PAGE_SHIFT;
    pages = (len + ~PAGE_MASK) >> PAGE_SHIFT;
    ...
    pfn += vma->vm_pgoff;
    pages -= vma->vm_pgoff;
    /* Can we fit all of the mapping */
    vm_len = vma->vm_end - vma->vm_start;
    ...
    /* Ok, let it rip */
     return io_remap_pfn_range(vma, vma->vm_start, pfn, vm_len, vma->vm_page_prot);
}

drivers/video/fbdev/core/fbmem.c中的fb_mmap将LCD驱动映射framebuffer物理地址到用户空间:

static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
	struct fb_info *info = file_fb_info(file);
	struct fb_ops *fb;
	unsigned long mmio_pgoff;
	unsigned long start;
	u32 len;

	if (!info)
		return -ENODEV;
	fb = info->fbops;
	if (!fb)
		return -ENODEV;
	mutex_lock(&info->mm_lock);
	if (fb->fb_mmap) {
		int res;

		/*
		 * The framebuffer needs to be accessed decrypted, be sure
		 * SME protection is removed ahead of the call
		 */
		vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
		res = fb->fb_mmap(info, vma);
		mutex_unlock(&info->mm_lock);
		return res;
	}

	/*
	 * Ugh. This can be either the frame buffer mapping, or
	 * if pgoff points past it, the mmio mapping.
	 */
	start = info->fix.smem_start;
	len = info->fix.smem_len;
	mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
	if (vma->vm_pgoff >= mmio_pgoff) {
		if (info->var.accel_flags) {
			mutex_unlock(&info->mm_lock);
			return -EINVAL;
		}

		vma->vm_pgoff -= mmio_pgoff;
		start = info->fix.mmio_start;
		len = info->fix.mmio_len;
	}
	mutex_unlock(&info->mm_lock);

	vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
	/*
	 * The framebuffer needs to be accessed decrypted, be sure
	 * SME protection is removed
	 */
	vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
	fb_pgprotect(file, vma, start);

	return vm_iomap_memory(vma, start, len);
}

通常I/O内存被映射时需要时nocache的,这时候应该对vma->vm_page_prot设置nocache标志之后再映射:

static int xxx_nocache_mmap(struct file *filp,struct vm_area_struct *vma)
{
     vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);    // 赋nocache标志
     vma->pgoff = ((u32)map_start >> PAGE_SHIFT);
     // 映射
     if(remap_pfn_range(vma,vma->start,vma->vm_pgoff,vma->vm_end - vma->vm_start,vma->page_prot))
	return _EAGAIN;
     return 0;
}

pgprot_noncached()是一个宏,它高度依赖于CPU的体系结构,ARM的pgprot_noncached()定义:

#define pgprot_noncached(prot)   \
	__pgprot_modify(prot,L_PTR_MT_MASK,L_PTE_MT_UNCACHED)

另一个比pgprot_noncached()稍微少一些限制的宏是pgfrot_writecombine(),它的定义如下:

#define pgprot_writecombine(prot)   \
	   __pgprot_modify(prot,L_PTE_MT_MASK,L_PTE_MT_BUFFERABLE)

pgprot_noncached()实际禁止了相关页的Cache和写缓冲(Write Buffer),pgprot_writecombine()则没有禁止写缓冲。ARM的写缓冲器是一个非常小的FIFO存储器,位于处理器核与主存之间,其目的在于将处理器核和Cache从较慢的主存写操作中解脱出来。写缓冲区与Cache再存储层次上处于同一层次,但是它只作用于写主存。

4.4.2 fault()函数

除了remap_pfn_range()以外,在驱动程序中实现VMA的fault()函数通常可以为设备提供更加灵活的内存映射途径。当访问的页不在内存里,即发生缺页异常时,fault会被内核自动调用。当发生缺页异常时,系统会经过如下处理过程:

1.找到缺页的虚拟地址所在的VMA

2.如果必要,分配中间页目录表和页表

3.如果页表项对应的物理页面不存在,则调用这个VMA的fault()方法,它返回物理页面的页描述符。

4.将物理页面的地址填充到页表中

fault()函数使用范例

static int xxx_fault(struct vm_area_struct *vma,struct vm_fault *vmf)
{
     unsigned long paddr;
     unsigned long pfn;
     pgoff_t index = vmf->pgoff;
     struct vma_data *vdata = vma->vm_private_data;
     ...
     pfn = paddr >> PAGE_SHIFT;
     vm_insert_pfn(vma,(unsigned long)vmf->virtual_address,pfn);
     return VM_FAULT_NOPAGE;
}

大部分设备驱动都不需要提供设备内存到用户空间的映射能力,因为,对于串口等面向流的设备而言,实现这种映射毫无意义。但是对于显示设备和视频设备而言,建立映射可减少用户间和内核空间之间的内存复制。

5. I/O内存静态映射

在将Linux移植到目标电路板的过程中,有的会建立外设I/O内存物理地址到虚拟地址的静态映射,通过在于电路板对应的map_desc结构体数组中添加新的成员来完成。

map_desc结构体:l

struct map_desc
{
    unsigned long virtual;        // 虚拟地址
    unsigned long pfn;            // __phys_to_pfn(phy_addr)
    unsigned long length;	  // 大小
    unsigned long type; 	  // 类型
};

例如,在内核arch/arm/mach-imx/mach-imx/mach-mx31ads.c就进行了静态映射:

static struct map_desc mx31ads_io_desc[] __initdata = {
	{
		.virtual	= (unsigned long)MX31_CS4_BASE_ADDR_VIRT,
		.pfn		= __phys_to_pfn(MX31_CS4_BASE_ADDR),
		.length		= CS4_CS8900_MMIO_START,
		.type		= MT_DEVICE
	},
};

static void __init mx31ads_map_io(void)
{
	mx31_map_io();
	iotable_init(mx31ads_io_desc, ARRAY_SIZE(mx31ads_io_desc));     // 最终建立页映射
}

但是该方法目前并不推荐使用。

6.DMA

DMA是一种无须CPU参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用DMA可以使系统CPU实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。

DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后由CPU执行相应的中断服务程序进行后处理。

6.1 DMA和Cache一致性

Cache被用作CPU针对内存的缓存,利用程序的空间局部性和时间局部性,达到较高的命中率,从而避免CPU每次都必须要与相对慢速的内存交互数据来提高数据的访问速率。

DMA可以作为内存与外设之间传输数据的方式,这种传输方式下,数据并不需要CPU中转。

DMA针对的目的地址与Cache缓存的对象分为由重叠区域和没有重叠区域两种情况:

1.有重叠区域:经过DMA操作,与Cache对应的内存中的数据已经被修改,而CPU本身不知,他仍然认为Cache中的数据就是内存中的数据,再以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据。这样就会发生Cache于内存之间数据"不一致性"的错误。

2.没有重叠区域:DMA和Cache之间相安无事

image

在发生Cache于内存不一致性错误后,驱动将无法正常运行。而且很难定位问题,因为此时所有程序看起来都完全正确。Cache的不一致性问题并不是只发生在DMA的情况下,实际上,它存在于Cache使能和关闭的时刻。例如,对于带MMU共嗯那个的ARM处理器,在开启MMU之前,需要先置Cache无效,对于TLB,也使如此,在ARM下可以通过如下汇编命令关闭ARM的Cache:

// 使Cache无效
"mov r0,#0\n"
"mcr p15,0,r0,c7,c7,0\n"           // 使数据和指令cache无效
"mcr p15,0,r0,c7,c10,4\n"          // 放空写缓冲
"mcr p15,0,r0,c8,c7,0\n"           // 使TLB无效

6.2 Linux下的DMA编程

由于DMA本身并不属于一种等同于字符设备、块设备和网络设备的外设,它只是一种外设与内存交互的方式。

内存中与外设交互数据的一块区域称为DMA缓冲区,在设备不支持scatter/gather(分散/聚集)操作下,DMA缓冲区在物理上必须是连续的。

6.2.1 DMA区域

对于x86系统的ISA设备而言,其DMA操作只能在16MB一下的内存中进行,在使用kmalloc、__get_free_pages()、以及类似函数申请DMA缓冲区时应使用GFP_DMA标志,这样能保证获得得内存位于DMA区域中,并具备DMA能力。

在内核中可以通过以下接口来申请DMA内存:

1.__get_dma_pages()

实质上是通过__get_free_pages(),在申请标志中添加GFP_DMA:

#deFine __get_dma_pages(gfp_mask,order)  \
	__get_free_pages((gfp_mask) | GFP_DMA,(order))

2.dma_mem_alloc()

static unsigned long dma_mem_alloc(int size)
{
     int order = get_order(size);
     return __get_dma_pages(GFP_KERNEL,order);
}

6.2.2 虚拟地址、物理地址和总线地址

基于DMA得硬件使用得是总线地址而不是物理地址,总线地址是从设备角度上看到得内存地址,物理地址则是从CPU MMU控制器外围角度上看到得内存地址。

地址与平台架构强相关,以PReP(PowerPC Reference Platform)系统为例,物理地址0在设备端看起来是0x80000000,而0通常又被映射为虚拟地址0xC0000000,所以同一地址就具备了三重身份:物理地址0、总线地址0x80000000及虚拟地址0xC0000000。

还有一些系统提供页面映射机制,它能将任意得页面映射为连续得外设总线地址。内核提供了如下函数以进行得虚拟地址/总线地址转换:

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

在使用IOMMU或反弹缓冲区得情况下,上述函数一般不会正常工作。IOMMU的工作原理与CPU内的MMU非常类似,不过它针对的是外设总线地址和内存地址之间的转化。由于IOMMU可以使得外设DMA引擎看到"虚拟地址",因此在使用IOMMU得情况下,在修改映射寄存器后,可以使得SG中分段得缓冲区地址对外设变得连续。

image

6.2.3 DMA地址掩码

设备并不一定能在所有得内存地址上执行DMA操作,这种情况下应该通过系统接口执行DMA地址掩码:

int dma_set_mask(struct device *dev,u64 mask);

例如:对于只能在24位地址上执行DMA操作得设备而言,就应该调用dma_set_mask(dev,0xffffff)。

该API本质上是修改device结构体中dma_mask成员,在ARM平台得定义为:

int arm_dma_set_mask(struct device *dev,u64 dma_mask)
{
    if(!dev->dma_mask || !dma_supported(dev,dma_mask))
    {
	return -EIO;
    }
    *dev->dma_mask = dma_mask; 
     return 0;
}

在device结构体中,除了有dma_mask以外,还有一个coherent_dma_mask成员。dma_mask是设备DMA可以寻址的范围,而coherent_dma_mask作用与申请一致性的DMA缓冲区。

6.2.4 一致性DMA缓冲区

DMA映射包括两个方面的工作

(1).分配一片DMA缓冲区

(2).为这片缓冲区产生设备可访问的地址

同时DMA映射也必须考虑Cache一致性问题。

1.申请一片DMA缓冲区,以进行地址映射并保证该缓冲区的Cache一致性

void *dma_alloc_coherent(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp);

上述函数的返回值为申请到的DMA缓冲区的虚拟地址,此外,此函数还可以通过参数handle返回DMA缓冲区的总线地址,dma_addr_t类型,代表的是总线地址。

2.释放申请到的DMA缓冲区

void *dma_free_coherent(struct device *dev,size_t size,void *cpu_addr,dma_addr_t handle);

3.分配一个写合并(Writecombining)的DMA缓冲区

void *dma_alloc_writecombine(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp);

4.释放写合并缓冲区

#define dma_free_writecombins(dev,size,cpu_addr,handler)    \
	  dma_free_coherent(dev,size,cpu_addr,handle)

5.PCI设备申请DMA缓冲区

void *pci_alloc_consistent(struct pci_dev *pdev,size_t size,dma_addr_t *dma_addrp);

6.PCI设备释放DMA缓冲区

void pci_free_consistent(struct pci_dev *pdev,size_t size,void *cpu_addr,dma_addr_t dma_addr);

这里需要说明以下,dma_alloc_xxx()函数虽然是以dma_alloc_开头,但是其申请的区域不一定在DMA区域里面。以32为ARM处理器为例,当coherent_dma_mask小于0xffffffff时,才会设置GFP_DMA标记,并从DMA区域中去申请内存。

6.2.5 流式DMA映射

并不是所有的DMA缓冲区都是驱动申请的,如果时驱动申请的,用一致性DMA缓冲区最方便,这直接考虑了Cache一致性的问题。

但是在很多情况下,缓冲区来自内核的较上层,如网卡驱动中的网络报文,块设备驱动的要写入的数据等,上层可能使用kmalloc()、__get_free_pages()等方法申请,这时候就要使用流式DMA映射。

流式DMA缓冲区使用的一般步骤如下:

1.进行流式DMA映射

2.执行DMA操作

3.进行流式DMA去映射

流式DMA映射操作在本质上大多就是进行Cache的使无效或清除操作,以解决Cache一致性问题。

6.2.6 dmaengine标准API

Linux内核目前推荐使用dmaengine的驱动架构来编写DMA控制器的驱动,同时外设的驱动使用标准dmaengineAPI进行DMA准备、发起和完成时的回调工作。

和中断一样,在使用DMA之前,设备驱动程序需首先向dmaengine系统申请DMA通道,使用完成后再释放掉该通道。

1.申请DMA通道的函数

struct dma_chan *dma_request_slave_channel(struct device *dev,const char *name);
struct dma_chan *__dma_request_channel(const dma_cap_mask_t,dma_filter_fn fn,void *fn_param);

2.释放DMA通道

void dma_release_channel(struct dma_chan *chan);

3.使用dmaengine API发起一次DMA操作

static voidc xxx_dma_fini_callback(void *data)
{
    struct completion *dma_complete = data;
    complete(dma_complete);
}

issue_xxx_dma(...)
{
    rx_desc = dmaengine_prep_slave_single(xxx->rx_chan,
			xxx->dst_start,t->len,DMA_DEV_TO_MEM,
			DMA_PREP_INTERRUPT | DMA_CTRL_ACK);        // 准备好DMA描述符
    rx_desc->callback = xxx_dma_fini_callback;                     // 填充回调函数
    rx_desc->callback_param = &xxx->rx_done;

    dmaengine_submit(rx_desc);                                     // 将DMA描述符插入队列
    dma_async_isssue_pending(xxx->rx_chan);                        // 发起DMA动作。DMA完成后,回调函数会被dmaengine驱动自动调用
}

7.总结

外设可处于CPU的I/O空间和内存空间,除x86外,嵌入式处理器一般只存在内存空间。在Linux系统中,为I/O内存和I/O端口的访问提供了一套同一的方法,访问流程一般为"申请资源->映射->访问->去映射->释放资源"

对于有MMU的处理器而言, Linux系统的内部布局比较复杂, 可直接映射的物理内存称为常规内存,超出部分为高端内存。 kmalloc() 和__get_free_pages() 申请的内存在物理上连续, 而vmalloc() 申请的内存在物理上不连续

DMA操作可能导致Cache的不一致性问题, 因此, 对于DMA缓冲, 应该使用dma_alloc_coherent() 等方法申请。 在DMA操作中涉及总线地址、 物理地址和虚拟地址等概念, 区分这3类地址非常重要。

标签:11,DMA,struct,vm,unsigned,详解,内存,Linux,vma
From: https://www.cnblogs.com/Wangzx000/p/18229427

相关文章

  • AI助手:Agent工作流程与应用场景详解
    引言智能体(Agent)是一种在特定环境中自主行动、感知环境、做出决策并与其他智能体或人类进行交互的计算机程序或实体。它们具备自主性、反应性、社交性和适应性等特点,能够根据环境的变化调整自己的行为,以达到预设的目标。本文将详细拆解智能体从提示词接收、LLM大模型理解识别、知......
  • Linux磁盘管理-LVM入门学习建议
    Linux磁盘管理-LVM入门学习建议准确掌握基础概念基础概念非常重要,以LVM逻辑卷为例,必须熟练掌握LV、PV以及VG的基本概念。之后才能进行更为复杂的管理操作。LVM基本大纲这里罗列出了学习LVM入门的基本大纲,供大家参考......
  • 【百万字详解Redis】集群部署
    文章目录Redis集群部署......
  • Shell脚本语言用法详解(超详细~)
    Shell目录Shell一、Shell是什么?二、Shell怎么使用?1.变量变量的命名和赋值变量类型变量的作用域只读变量删除变量环境变量系统预定义变量变量的使用特殊变量和位置参数2.运算符3.条件判断4.流程控制if判断case语句for循环while循环5.读取控制台输入6.函数系统函数自定......
  • C++11原子操作
    目录1.什么是原子操作2.为什么需要原子操作?3.C++中的原子操作4.原子操作使用及注意5.应用场景6.使用原子操作的最佳实践7.原子操作与锁机制的比较8.总结1.什么是原子操作        原子操作是一种不可分割的操作,即在多线程环境中,这些操作要么全部执行完成,要么......
  • 【Linux系统查看显卡支持的OpenGL版本】
    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录一、安装依赖项二、Linux系统查看显卡支持的OpenGL版本一、安装依赖项sudoapt-getupdate&&sudoapt-getinstallmesa-utils二、Linux系统查看显卡支持的OpenGL版本glxinfo|grepO......
  • 在Linux中,有哪些系统日志文件?
    在Linux系统中,系统日志文件是记录系统操作、运行状态、错误信息以及安全事件等的重要文件。以下是Linux中常见的系统日志文件及其简要描述:系统日志(SystemLogs):这些日志记录了系统整体运行情况、错误信息等。常见的系统日志文件包括/var/log/messages、/var/log/syslog等。这......
  • 在Linux中,BASH 和 DOS之间的区别是什么?
    BASH(BourneAgainSHell)和DOS(DiskOperatingSystem)之间存在显著的区别,这些差异不仅体现在它们的设计哲学、功能特性上,也反映在它们所服务的操作系统环境及其用途上。以下是一些主要的区别:性质和定位:BASH:是一种命令行解释器(shell),它是用户与Linux或其他类UNIX操作系统交互......
  • 在Linux中,进程间通信方式有哪些?
    在Linux中,进程间通信(IPC)是允许多个进程或线程交换数据或信号的机制。以下是一些常见的进程间通信方式:1.管道(Pipes)允许一个进程将输出发送到另一个进程的输入。可以是匿名管道或命名管道(FIFOs)。#创建匿名管道mkfifo/tmp/mypipe#使用管道echo"Hello">/tmp/mypipeca......
  • 在Linux中,系统缺省的运行级别是什么?
    在传统的Linux系统中,特别是使用SysVinit作为初始化系统的发行版,系统缺省的运行级别通常设为3或5,具体取决于系统是否配置为启动图形界面。运行级别3:多用户,完全多用户模式,不带图形界面。在这个级别下,系统启动所有多用户环境下的服务和守护进程,但不启动XWindow系统(图形界面)。这......