Linux驱动开发---内存与I/O

内存与I/O

MMU

  1. 功能
    1.1 虚拟地址和物理地址的映射;
    1.2 内存访问的权限保护;
    1.3 Cache缓存控制。

  2. 相关概念
    2.1 TLB:转化旁路缓存。缓存虚拟地址与物理地址的转换关系,也称“快表”。
    2.2 TTW:转化表漫游。在TLB中没有所需的虚拟地址与物理地址的转换关系时,会去主存储器转换表中获取相应关系,并缓存到TLB中。
    2.3 DTLB/ITLB:数据TLB/指令TLB。
    2.4 C:高速缓存。
    2.5 B:缓冲。

  3. CPU数据访问流程
    CPU数据访问流程

Linux内存管理

内存空间(4G) = 用户空间(0~3G) + 内核空间(3~4G)

  1. 用户空间
    1.1 每个进程的用户空间时互相独立,互不相干的;
    1.2 用户进程各自有不同的页表。

  2. 内核空间
    2.1 内核空间由内核负责映射;
    2.2 内核空间不会跟着进程改变,是固定的;
    2.3 内核空间有独立的页表;
    2.4 内核的虚拟空间是独立于其他程序;
    2.5 内核空间(1G)被映射(从低地址到高地址)依次为以下几个区:
    2.5.1 物理内存映射区:映射的物理内存最大长度896MB
    2.5.2 虚拟内存分配区:位于物理内存映射区和高端内存映射区之间,并上下均有一段隔离带。用于vmalloc()函数。
    2.5.3 高端内存映射区:系统物理内存超过896MB的被称为高端内存,被映射到该区域。
    2.5.4 专用页面映射区
    2.5.5 保留区
    内核空间(1G)被映射

内存存取

  1. 用户空间的内存动态申请
    申请函数:malloc();
    释放函数:free();
    NOTE:malloc的内存一定要free(),否则会造成内存泄漏;另外,两函数应成对出现。

  2. 内核空间的内存动态申请
    2.1 kmalloc()/kfree、__get_free_pages()
    2.1.1 申请的内存位于物理内存映射区;
    2.1.2 物理上是连续的,与真实的物理地址只有一个固定的偏移地址。
    2.2 vmalloc()/vfree
    2.2.1 申请的内存位于vmalloc分配区;
    2.2.2 在vmalloc分配区里是连续的,但对应的物理内存却不一定是连续的。故虚拟内存与物理内存没有简单的换算关系;
    2.2.3 不能用在原子上下文;(原因是内部实现使用标志为GPF_KERANEL的kmalloc())
    2.2.4 使用该函数的典型例子:create_module(),利用vmalloc获取被创建模块所需的内存空间。
    2.3 slab缓存、内存池mempool
    2.3.1 功能:操作系统运作时,涉及大量对象重复生成、使用和释放的解决方案。
    2.3.2 slab缓存
    2.3.2.1 创建slab缓存:strcut kmem_cache *kmem_cache_create();
    2.3.2.2 分配slab缓存:void *kmem_cache_alloc();
    2.3.2.3 释放slab缓存:void kmem_cache_free();
    2.3.2.4 回收slab缓存:int kmem_cache_destory();
    2.3.3 内存池mempool
    2.3.3.1 创建内存池:mempool_t *mempool_create();
    2.3.3.2 分配内存池:void *mempool_alloc();
    2.3.3.3 释放内存池:void mempool_free();
    2.3.3.4 回收内存池:void mempool_destory();

  3. 虚拟地址与物理地址的关系
    3.1 虚拟地址 --> 物理地址:virt_to_phys()
    实现:__pa(address),将虚拟地址减去PAGE_OFFSET,通常是3G
    3.2 物理地址 --> 虚拟地址:phys_to_virt()
    实现:__pa(address),将物理地址加上PAGE_OFFSET,通常是3G
    NOTE: 仅限物理内存映射区! 物理内存映射区! 物理内存映射区!(常规内存)

(设备的)I/O端口与I/O内存的访问

问:什么是I/O端口和I/O内存?
答:设备拥有一组寄存器(控制/数据/状态寄存器)。如果这组寄存器位于I/O空间,则称为I/O端口;如果位于内存空间,则称为I/O内存

  1. 申请/释放
    1.1 I/O端口:struct resource *request_region() / void release_region();
    1.2 I/O内存:struct resource *request_mem_region() / void release_mem_region();
    NOTE:若request_region/request_mem_region返回值为NULL,则说明资源不可用,申请失败。

  2. 访问接口
    2.1 I/O端口
    2.1.1 读写字节/字/长字:
    unsigned inb/inw/inl(unsigned port); //读
    void outb/outw/outl(…,unsigned port); //写
    2.1.2 读写一串字节/字/长字:
    void insb/insw/insl(unsigned port, void *add, unsigned long count); //读
    void outsb/outsw/outsl(unsigned port, void *addr, unsigned long count); //写
    2.2 I/O内存
    NOTE1:访问I/O内存之前,需要用ioremap(void *addr)将设备所处的物理地址映射到虚拟地址。
    NOTE2:用ioremap映射后,是可以直接使用指针访问这些地址的。但是Linux仍提供下面一组函数来访问。
    2.2.1 读I/O内存
    unsigned int ioread8/ioread16/ioread32(void *addr);
    unsigned int readb/readw/readl(address); //较早版本
    2.2.2 写I/O内存
    void iowrite8/iowrite16/iowrite32(u8/u16/u32 value , void *addr);
    void writeb/writew/writel(unsigned value, address); //较早版本
    2.2.3 读一串I/O内存
    void ioread8/16/32_rep(void *addr, void *buf, unsigned long count);
    2.2.4 写一串I/O内存
    void iowrite8/16/32_rep(void *addr, const void *buf, unsigned long count);
    2.2.5 复制I/O内存
    memcpy_fromio(void *dest, void *src, unsigned int count);
    memcpy_toio(void *dest, void *src, unsigned int count);
    2.2.6 设置I/O内存
    memset_io(void *addr, u8 value, unsigned int count);
    2.3 I/O端口映射到内存空间
    void *ioport_map(unsigned long port, unsigned int count); /* 将端口port上count个连续I/O端口映射到内存空间 */
    void ioport_unmap(void *addr); /* 撤销上述所作的映射 */
    NOTE: 使用ioport_map后,就可以使用访问I/O内存的接口访问这些I/O端口。

  3. 访问流程
    3.1 I/O端口:两种方式
    1)申请I/O端口资源 –> 利用访问I/O端口的接口访问 –> 释放I/O端口资源
    2)申请I/O端口资源 –> 将I/O端口映射到内存空间 --> 利用访问I/O内存的接口访问 --> 撤销映射 --> 释放I/O端口资源
    3.2 I/O内存
    申请I/O内存资源 –> ioremap映射到虚拟地址 –> 利用访问I/O内存的接口访问 –> iounmap --> 释放I/O内存资源

  4. 设备内存映射到用户空间
    注意: 大多数设备驱动并不需要提供此项能力。但对于显示适配器一类的设备却非常有意义(用户空间可以直接访问显存,则不再需要从用户空间到内核空间的复制过程)。
    4.1 VMA: vm_area_struct —用于描述一个虚拟内存区域
    4.1.1 mmap
    实现映射:将用户空间的一段内存与设备内存相关联
    实现机制:建立页表
    驱动)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);
    4.1.2 用户调用mmap时,内核会做如下处理
    在进程的虚拟空间找一块VMA –> 将这块VMA进行映射 –> 如果设备驱动程序/文件系统file_operations定义了mmap()操作,则调用它 –> 将这个VMA插入到进程的 VMA链表
    NOTE: 在用户进行mmap系统调用后,内核不会调用VMA的open函数,故需要在驱动中的mmap函数中调用VM->VM_ops->open().
    4.1.3 remap_pfn_range()
    创建页表,以VMA结构体成员作为其参数,映射的虚拟地址范围为vma->vm_start至vma->vm_end。
    4.2 nopage: 为设备提供更加灵活的内存映射途径
    4.2.1 当发生缺页异常(访问的页不在内存中)时,nopage会被内核自动调用。实现nopage()后,用户空间可以通过mremap()系统调用重新绑定映射区域所绑定的地址。
    4.2.2 当发生缺页异常时,系统会有如下处理:
    1)首先,找到缺页的虚拟地址所在的VMA;
    2)如果必要,分配中间页目录表和页表;
    3)如果页表项对应的物理页面不存在,则调用该VMA的nopage(),他返回物理页面的页描述符;
    4)将物理页面的地址填充到页表。
    NOTE: nopage与remap_pfn_range的区别是,remap_pfn_range一般用于设备内存映射,而nopage还可用于RAM映射。

DMA

  1. 功能
    1.1 实现外设与系统内存之间进行双向传输。
    1.2 使CPU从I/O数据传输中解脱出来,提高系统吞吐量。
    实现: 数据传输由DMA控制器(DMAC)控制。数据传输完成后,再由DMAC通过中断通知CPU,然后由CPU执行相应的中断服务程序进行后处理。

  2. Cache与内存的一致性问题
    2.1 Cache: CPU针对内存的缓存,提高数据的访问速率。
    2.2 具体原因: 如果DMA的目的地址范围与Cache所缓存的内存地址没有交集,该问题则不会出现;一旦前面二者存在交集,经过DMA操作,Cache缓存的内存数据已发生变化,但CPU并不知道,仍以为Cache中的数据就是内存中的数据,之后CPU访问Cache映射的内存时,仍会使用陈旧的Cache数据,这样就发生了Cache与内存之间的数据“不一致性”。
    2.3 简而言之: 对于采用Cache的系统, Cache中的数据与主存的数据一致则说明具有一致性;数据若不一致,则具有不一致性。
    2.4 最直接的解决方法(由DMA引起的):直接禁止DMA目的地址范围内存的Cache功能。

  3. DMA编程(Linux)
    3.1 相关概念
    1)DMA缓冲区: 内存中用于与外设交互数据的一块区域。
    2)总线地址:从设备角度上看到的内存地址。基于DMA的硬件使用总线地址。
    3)物理地址:从CPU角度上看到的未经转换的内存地址。
    4)虚拟地址:从CPU角度上看到的经过转换的内存地址。
    NOTE: 以上均是内存地址。
    3.2 申请/释放DMA通道
    int request_dma(unsigned int dmanr, const char * device_id);
    void free_dma((unsigned int dmanr);
    3.3 申请DMA缓冲区
    kmalloc() / __get_free_pages():均需使用GFP_DMA标志。
    __get_dma_pages(gfp_mask, order) / dma_mem_alloc(int size);
    3.4 申请/释放具有一致性的DMA缓冲区
    void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);
    /* 上述函数返回为申请到的DMA缓冲区的虚拟地址。参数handle可以返回总线地址 */
    void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
    3.5 DMA地址掩码:设备不一定在所有内存地址上执行DMA
    int dma_set_mask(struct device *dev, u64 mask);
    例:dma_set_mask(dev,0xffffff); /* 可在24位地址上执行DMA操作 */
    3.6 流式DMA映射
    dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction); /* 返回虚拟地址 */
    3.7 获取DMA缓冲区拥有权
    void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
    3.8 将DMA拥有权返还给设备
    void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
    3.9 (支持SG模式的情况下)申请多个不连续的、相对较小的DMA缓冲区:
    3.9.1 int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); /* 函数返回DMA缓存区的数量 */
    3.9.2 dma_addr_t sg_dma_address(structscatterlist *sg); /* 返回scatterlist 对应缓冲区的总线地址 */
    3.9.3 unsigned int sg_dma_len(struct scatterlist *sg); /*返回scatterlist 对应缓冲区的长度 */
    3.10 SG映射属于流式DMA映射,与单一缓冲区的流式DMA映射类似
    3.10.1 设备驱动获取SG缓冲区的拥有权:
    void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
    3.10.2 将拥有权返还给设备:
    void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
    3.11 DMA代码流程
    申请中断、DMA通道和初始化DMAC –> 申请DMA缓冲区 –> DMA传输 –> DMA传输后的中断处理 –> 释放DMA缓冲区 –> 释放DMA通道

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值