内存与I/O
MMU
-
功能
1.1 虚拟地址和物理地址的映射;
1.2 内存访问的权限保护;
1.3 Cache缓存控制。 -
相关概念
2.1 TLB:转化旁路缓存。缓存虚拟地址与物理地址的转换关系,也称“快表”。
2.2 TTW:转化表漫游。在TLB中没有所需的虚拟地址与物理地址的转换关系时,会去主存储器转换表中获取相应关系,并缓存到TLB中。
2.3 DTLB/ITLB:数据TLB/指令TLB。
2.4 C:高速缓存。
2.5 B:缓冲。 -
CPU数据访问流程
Linux内存管理
内存空间(4G) = 用户空间(0~3G) + 内核空间(3~4G)
-
用户空间
1.1 每个进程的用户空间时互相独立,互不相干的;
1.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 保留区
内存存取
-
用户空间的内存动态申请
申请函数:malloc();
释放函数:free();
NOTE:malloc的内存一定要free(),否则会造成内存泄漏;另外,两函数应成对出现。 -
内核空间的内存动态申请
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.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 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.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.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.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.2 使CPU从I/O数据传输中解脱出来,提高系统吞吐量。
实现: 数据传输由DMA控制器(DMAC)控制。数据传输完成后,再由DMAC通过中断通知CPU,然后由CPU执行相应的中断服务程序进行后处理。 -
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功能。 -
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通道