linux dma_alloc_writecombine 和mmap

本文探讨了缓存一致性原理及其在DMA操作中的重要性。详细分析了DMA操作可能导致的缓存与内存数据不一致问题,并介绍了两种不同的DMA映射方法:一致性DMA映射和流式DMA映射,以确保数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址:https://blog.youkuaiyun.com/manshq163com/article/details/18451135

先理解cache的作用
CPU在访问内存时,首先判断所要访问的内容是否在Cache中,如果在,就称为“命中(hit)”,此时CPU直接从Cache中调用该内容;否则,就 称为“ 不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从Cache中读出内容,也可以直接往其中写入内容。由于Cache的存取速 率相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。

Cache的一致性就是直Cache中的数据,与对应的内存中的数据是一致的。

DMA是直接操作总线地址的,这里先当作物理地址来看待吧(系统总线地址和物理地址只是观察内存的角度不同)。如果cache缓存的内存区域不包括DMA分配到的区域,那么就没有一致性的问题。但是如果cache缓存包括了DMA目的地址的话,会出现什么什么问题呢?

问题出在,经过DMA操作,cache缓存对应的内存数据已经被修改了,而CPU本身不知道(DMA传输是不通过CPU的),它仍然认为cache中的数 据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用旧的Cache数据。这样就发生Cache与内存的数据“不一致性”错误。

题外话:好像2.6.29内核中,6410的总线地址和物理地址是一样的,因为我在查看vir_to_bus函数的时候,发现在/arch/arm/linux/asm/memory.h中这样定义:
#ifndef __virt_to_bus
#define __virt_to_bus    __virt_to_phys
#define __bus_to_virt    __phys_to_virt
#endif
而且用source Insight搜索了一遍,没有发现6410相关的代码中,重新定义__vit_to_bus,因此擅自认为2.6内核中,6410的总线地址就是物理地址。希望高手指点。

顺便提一下,总线地址是从设备角度上看到的内存,物理地址是CPU的角度看到的未经过转换的内存(经过转换的是虚拟地址)

由上面可以看出,DMA如果使用cache,那么一定要考虑cache的一致性。解决DMA导致的一致性的方法最简单的就是禁止DMA目标地址范围内的cache功能。但是这样就会牺牲性能。

因此在DMA是否使用cache的问题上,可以根据DMA缓冲区期望保留的的时间长短来决策。DAM的映射就分为:一致性DMA映射和流式DMA映射。

一致性DMA映射申请的缓存区能够使用cache,并且保持cache一致性。一致性映射具有很长的生命周期,在这段时间内占用的映射寄存器,即使不使用也不会释放。生命周期为该驱动的生命周期

流式DMA映射实现比较复杂,因为没具体了解,就不说明了。只知道种方式的生命周期比较短,而且禁用cache。一些硬件对流式映射有优化。建立流式DMA映射,需要告诉内核数据的流动方向。


因为LCD随时都在使用,因此在Frame buffer驱动中,使用一致性DMA映射
上面的代码中用到 dma_alloc_writecombine函数,另外还有一个一致性DMA映射函数dma_alloc_coherent

两者的区别在于:
查看两者的源代码
[cpp] view plaincopy

    /*
    * Allocate DMA-coherent memory space and return both the kernel remapped
    * virtual and bus address for that space.
    */  
    void *  
    dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)  
    {  
    void *memory;  
      
    if (dma_alloc_from_coherent(dev, size, handle, &memory))  
    return memory;  
      
    if (arch_is_coherent()) {  
    void *virt;  
      
    virt = kmalloc(size, gfp);  
    if (!virt)  
    return NULL;  
    *handle =  virt_to_dma(dev, virt);  
      
    return virt;  
    }  
      
    return __dma_alloc(dev, size, handle, gfp,  
    pgprot_noncached(pgprot_kernel));  
    }  
      
    /*
    * Allocate a writecombining region, in much the same way as
    * dma_alloc_coherent above.
    */  
    void *  
    dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)  
    {  
    return __dma_alloc(dev, size, handle, gfp,  
    pgprot_writecombine(pgprot_kernel));  
    }  
      
      
    #define pgprot_noncached(prot)  __pgprot(pgprot_val(prot) &~(L_PTE_CACHEABLE | L_PTE_BUFFERABLE))  
    #define pgprot_writecombine(prot) __pgprot(pgprot_val(prot) &~L_PTE_CACHEABLE)  

再结合网上的资料(不过我感觉那文章写的有些问题,我修改了一下),由上面代码可以看出,两个函数都调用了__dma_alloc函数,区别只在于最后一个参数。

dma_alloc_coherent 在 arm 平台上会禁止页表项中的 C (Cacheable) 域以及 B (Bufferable)域。而 dma_alloc_writecombine 只禁止 C (Cacheable) 域.

 C 代表是否使用高速缓冲存储器, 而 B 代表是否使用写缓冲区。

这样,dma_alloc_writecombine 分配出来的内存不使用缓存,但是会使用写缓冲区。而 dma_alloc_coherent  则二者都不使用。

C B 位的具体含义
0 0 无cache,无写缓冲;任何对memory的读写都反映到总线上。对 memory 的操作过程中CPU需要等待。
0 1 无cache,有写缓冲;读操作直接反映到总线上;写操作,CPU将数据写入到写缓冲后继续运行,由写缓冲进行写回操作。
1 0 有cache,写通模式;读操作首先考虑cache hit;写操作时直接将数据写入写缓冲,如果同时出现cache hit,那么也更新cache。
1 1 有cache,写回模式;读操作首先考虑cache hit;写操作也首先考虑cache hit。

这样,两者的区别就很清楚了。

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

含义:
A          : 内存的虚拟起始地址,在内核要用此地址来操作所分配的内存
dev      : 可以平台初始化里指定,主要是用到dma_mask之类参数,可参考framebuffer
size      : 实际分配大小,传入dma_map_size即可
handle: 返回的内存物理地址,dma就可以用。

A和hanle是一一对应的,A是虚拟地址,而handle是总线地址。对任意一个操作都将改变写缓冲区内容。

mmap函数的使用,与驱动中mmap函数的实现
mmap怎样使用,怎样实现,为什么mmap可以减少额外的拷贝?
下面简单介绍。

一、mmap的使用
*内存映射:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
        int fd, off_t offset);
int munmap(void *addr, size_t length);
[描述]
mmap
把文件或者设备映射到内存。这个函数在调用进程的虚拟地址空间中创建一块映射区域。映射区域的首地址在addr中指定,length指定映射区域的长度。如果addr是NULL,那么由内核来选择一个地址来创建映射的区域,否则创建的时候会尽可能地使用addr的地址。在linux系统中,创建映射的时候应该是在下一个页面的边界创建,addr是NULL的时候,程序的可移植性最好。
length指定文件被映射的长度,offset指定从文件的哪个偏移位置开始映射,offset必须是页面大小的整数倍页面的大小可以由sysconf(_SC_PAGE_SIZE)来返回.
prot指定内存的保护模式(具体参见man).
flags指定区域在不同进程之间的共享方式,以及区域是否同步到相应的文件等等(具体参见man).
这个函数返回新创建的页面的地址。
munmap
取消address指定地址范围的映射。以后再引用取消的映射的时候就会导致非法内存的访问。这里address应该是页面的整数倍。
成功的时候这个函数返回0。

失败的时候,两者都返回-1.
[举例]

1 //hello hello hello
2 /*程序功能:
3  * 1)主要测试mmap和munmap的2)简单的write
4  * 具体为:
5  * 在命令中分别指定文件名,映射的长度,映射的起始地址.
6  * 将文件映射到内存中
7  * 把映射到内存中的内容用write写到标准输出。
8  * 注意,这里没有对越界进行检测。
9  * */
10 #include <sys/mman.h>//mmap
11 #include <unistd.h>//sysconf
12 #include <fcntl.h>//file open
13 #include <stdio.h>//printf
14 intmain(int argc, char *argv[])
15 {
16     if(argc != 4)
17     {
18         write(STDOUT_FILENO,"hello\n",6);
19         printf("usage:%s <filename> <offset> <length>\n",argv[0]);
20         return 1;
21     }
22     char *filename = argv[1];//1)指定文件
23     printf("the file to be mapped is:%s\n",filename);
24     int fd = open(filename,O_RDONLY);
25
26     int offset = atoi(argv[2]);//2)指定映射起始地址(页面的整数倍)
27     printf("start offset of file to be mapped is:%d\n",offset);
28     printf("page size is:%ld\n",sysconf(_SC_PAGE_SIZE));
29     int realOffset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);//转换成页面的整数倍
30     printf("real start offset of file to be mapped is:%d\n",realOffset);
31
32     int length = atoi(argv[3]);//3)指定映射长度
33     printf("the length to be map is:%d\n",length);
34     int realLen = length+offset-realOffset;//实际写入的字节数
35     printf("the real length to be map is:%d\n",realLen);
36
37     //mmap的参数分别是:
38     //NULL,让内核自己选择映射的地址;realLen指定映射的长度;
39     //PROT_READ只读;MAP_PRIVATE不和其他的进程之间共享映射区域,数据也不写入对应的文件中;
40     //realOffset映射文件的起始地址(页面的整数倍)。
41     char *addr = mmap(NULL, realLen,PROT_READ,MAP_PRIVATE,fd,realOffset);//4)开始映射
42
43     //关闭打开的文件,实际程序退出的时候会自动关闭。
44     //关闭文件之后,相应的映射内存仍旧存在,映射的内存用munmap关闭。
45     close(fd);
46     //write的参数分别是:
47     //STDOUT_FILENO:文件描述符号(这里是标准输出)
48     //addr,将要写入文件的内容的地址
49     //realLen,写入的长度,长度以addr作为起始地址
50     write(STDOUT_FILENO,addr,realLen);//将映射的内容写到标准输出
51     munmap(addr,realLen);//5)关闭映射的内存
52     //write(STDOUT_FILENO,addr,realLen);//不能使用了
53     printf("\n");
54 }

二、mmap实现
驱动中有对mmap的具体实现。用户调用mmap系统调用函数之后,最终会调用到驱动中的mmap函数接口。下面是一个例子:
static int commdrv_mmap(struct file* file, struct vm_area_struct* vma)
{
    long phy_addr;
    unsigned long offset;
    unsigned long size;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_flags |= VM_LOCKED;
    offset = vma->vm_pgoff << PAGE_SHIFT;/*XXX assume is 12*/
    size = vma->vm_end - vma->vm_start;
    if(BUF0_OFF == offset) {
        phy_addr = PHYS_BASE0;
    } else if(BUF1_OFF == offset) {
        phy_addr = PHYS_BASE1;
    } else if(START_OFF == offset) {
        phy_addr = PHYS_BASE;
    } else {
        return -ENXIO;
    }
    /*phy_addr must be 4k *n*/
    if(remap_pfn_range(vma, vma->vm_start, phy_addr >> PAGE_SHIFT, size, vma->vm_page_prot)) {
        return -ENXIO;
    }
    return 0;
}
对于以上代码,
"vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);"表示要映射的内存是非cached的,这样不会存在缓存中的数据和实际数据不一致的情况,但是速度会比cached的要慢。
offset表示要映射的数据的偏移,这个偏移量来自用户空间的mmap调用,用户空间传入的偏移在这里进行判断,虽然一般的文件就将这个偏移量做为文件偏移了,其实这个offset的含义,由驱动自己解释,不一定就是字节偏移,驱动根据这个偏移量来决定映射哪块内存,
size表示要映射的内存的大小。
"remap_pfn_range(vma, vma->vm_start, phy_addr >> PAGE_SHIFT, size, vma->vm_page_prot)"表示将根据被映射的物理地址,以及虚拟起始地址,和大小等信息,将相应的部分映射到用户空间。其中参数vma直接来自commdrv_mmap函数的参数,phy_addr是要映射的设备的物理地址(必须是页对齐的),只有少量的信息自己设置,大多来自外部。最后映射的地址,通过用户调用的mmap函数返回,用户可以直接操作。

三、mmap优点
mmap实现了将设备驱动在内核空间的部分地址直接映射到用户空间,使得用户程序可以直接访问和操作相应的内容。减少了额外的拷贝,而一般的read,write函数虽然表面上直接向设备写入,其实还需要进行一次拷贝。
例如,下面是某个设备驱动中的的write实现,当外面用户程序调用write系统调用向相应设备文件写之后,最终会进入到这个函数进行真正的读取所需操作。
static ssize_t commdrv_write(struct file* filp, char __user* buf,
        size_t count, loff_t* ppos)
{
    char* wbuf;
    wbuf = (char*)vmalloc(count);
    if(!wbuf) {
        return 0;
    }
    ret = copy_from_user(wbuf, (char __user*)buf, count);
    if(0 != ret) {
        vfree(wbuf);
        return 0;
    }
    .....do others things with wbuf......
    vfree(wbuf);
    return count;
}
由上面的代码可知,用户传入的数据指针buf,在驱动中(也就是内核空间)不能直接访问,必须使用copy_from_user将其拷贝到内核空间的一块内存,然后才能进行后续的操作(内核中不能不经过copy_from_user,直接访问用户传下来的指针buf的地址的内容)。而mmap,使得将内核空间直接映射到了用户空间,让用户空间通过返回的指针直接访问,这样内核和用户空间直接操作同样的内存。也就是说,如果不使用mmap,那么由于在内核空间的代码,和外面用户空间的代码对应的地址空间不同,这样内核空间和用户空间不能互相访问其指针;如果想要访问,对方指针的内容,那么只能通过copy_from_user之类的函数先将其数据拷贝到内核空间(相应的read一般使用copy_to_user可以将内核空间内的指针数据拷贝给用户空间的指针所指)再访问。除非直接将内存映射,否则一定要拷贝才能访问用户空间数据。

<think>嗯,用户想了解Linux中的dma_alloc_coherent函数,我得先回忆一下这个函数的作用使用场景。首先,DMA(直接内存访问)是外设直接访问内存的技术,不需要CPU介入,这样数据传输更高效。但是DMA需要物理地址连续的内存,所以在Linux内核中,分配这样的内存需要使用特定的函数,比如dma_alloc_coherent。 接下来,我需要解释这个函数的参数返回值。函数原型通常是这样的:void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)。参数包括设备结构体指针,分配的大小,存储DMA地址的指针,以及内存分配标志。返回值是内核虚拟地址,同时dma_handle会保存物理地址。用户可能需要知道每个参数的具体作用,比如dev的作用是什么,为什么需要传入设备结构体,可能DMA掩码有关,确保分配的地址在设备可访问的范围内。 然后,使用场景。这个函数主要用于需要DMA传输的情况,比如网络设备、存储控制器等。需要强调物理地址的连续性,因为DMA控制器可能无法处理离散的物理地址。此外,还要提到dma_alloc_coherent分配的内存是一致性(coherent)的,也就是CPU设备可以同时访问,不需要软件维护缓存一致性,这可能流式DMA有所区别。 使用时需要注意的事项也很重要。比如分配的大小要对齐,可能有一些硬件限制,比如对齐到页边界。还有,释放内存的时候必须使用对应的dma_free_coherent函数,不能直接用kfree,否则会导致内存泄漏或者错误。另外,这个函数可能在原子上下文中不能使用,因为内部可能会调用可能休眠的函数,但具体要看flags参数的设置,比如GFP_ATOMIC是否可以在这里使用。 可能用户遇到的问题包括如何正确获取设备的struct device指针,比如通过平台设备结构体或者PCI设备的函数来获取。另外,DMA地址的使用,比如如何将物理地址传递给设备驱动程序,可能需要通过特定的寄存器设置或者DMA描述符。 还要比较一下dma_alloc_coherent其他类似函数,比如dma_alloc_writecombine,或者使用dma_map_single等流式DMA接口的区别。一致性DMA映射通常用于长期存在的、需要频繁访问的内存,而流式映射可能更适合单次传输的情况。 另外,需要提到DMA API的通用使用步骤:分配内存,获取总线地址,配置设备进行DMA传输,传输完成后释放资源。可能用户在实际编程中需要遵循这些步骤,避免错误。 可能用户会混淆物理地址总线地址,特别是在不同的体系结构中,DMA地址可能物理地址不同,比如存在IOMMU的情况下。这时候dma_alloc_coherent返回的总线地址是经过IOMMU转换的,而物理地址可能不同。不过一般来说,开发者不需要关心这个,DMA API已经处理好了。 还有,内存的属性,比如是否可缓存,一致性映射通常是非缓存的,或者有缓存但保持一致性,这取决于架构实现。而流式映射可能需要手动同步缓存。 最后,给出一个简单的代码示例会有帮助,展示如何在一个设备驱动中使用这个函数,比如分配内存,然后进行DMA传输,最后释放内存。确保示例包括错误处理,比如检查返回值是否为NULL,以及正确释放资源。 总结一下,回答需要覆盖函数的作用、参数、使用场景、注意事项、与其他函数的对比,以及示例代码。这样用户能全面理解dma_alloc_coherent的用法内部机制。</think>在Linux内核开发中,`dma_alloc_coherent`是用于DMA(Direct Memory Access)内存分配的核心API。以下从5个维度解析其原理使用: 一、核心作用 $$ \text{物理连续} \xrightarrow{\text{dma\_alloc\_coherent}} \text{总线地址可寻址} $$ - 分配**物理连续**的内存块 - 返回**CPU虚拟地址****总线地址**(DMA地址) - 保证CPU设备访问的**缓存一致性** 二、函数原型 ```c void *dma_alloc_coherent( struct device *dev, // 关联的硬件设备 size_t size, // 请求分配的字节数 dma_addr_t *dma_handle, // 输出的DMA总线地址 gfp_t gfp // 内存分配标志(如GFP_KERNEL) ); ``` 三、典型应用场景 1. 网络设备接收/发送环形缓冲区 2. SCSI控制器命令描述符 3. USB URB控制请求 4. 需要长期存在的DMA内存区域 四、使用流程示例 ```c // 驱动初始化时 struct device *dev = &pdev->dev; dma_addr_t dma_addr; void *cpu_addr = dma_alloc_coherent(dev, BUFFER_SIZE, &dma_addr, GFP_KERNEL); // 配置硬件寄存器 writel(dma_addr, dev->reg_base + DMA_ADDR_REG); // 释放内存(通常在remove函数) dma_free_coherent(dev, BUFFER_SIZE, cpu_addr, dma_addr); ``` 五、关键注意事项 1. **对齐约束**:实际分配的内存总是页对齐(PAGE_SIZE) 2. **性能影响**:频繁分配/释放应使用内存池技术 3. **地址映射**: - 通过`dma_to_phys()`可获取物理地址 - 但直接操作物理地址违反DMA API规范 4. **替代方案选择**: | 场景 | 推荐API | |---------------------|-----------------------| | 短期传输 | dma_map_single() | | 分散/聚集传输 | dma_map_sg() | | 非缓存访问 | dma_alloc_writecombine| 5. **调试手段**: ```bash # 查看DMA内存分配情况 $ cat /proc/vmallocinfo | grep dma ``` 六、底层实现原理 当调用`dma_alloc_coherent`时: 1. 根据设备DMA掩码确定可寻址范围 2. 通过CMA(Contiguous Memory Allocator)或引导时预留内存获取物理连续区域 3. 建立页表映射,保证CPU虚拟地址到物理地址的转换 4. 对ARM体系结构会调用`__alloc_from_contiguous` 常见错误处理模式: ```c if (!cpu_addr) { dev_err(dev, "Failed to allocate DMA memory\n"); return -ENOMEM; } // 检查DMA地址有效性 if (dma_mapping_error(dev, dma_addr)) { dma_free_coherent(...); return -EFAULT; } ``` 通过深入理解这些机制,开发者可以正确实现高效可靠的DMA数据传输,避免内存泄漏或总线错误等常见问题。实际开发中应结合`DMA API`文档具体硬件手册进行验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值