《Linux Device Drivers》第十五章 内存映射和DMA——note

2022-10-15,,,,

简单介绍

很多类型的驱动程序编程都须要了解一些虚拟内存子系统怎样工作的知识
当遇到更为复杂、性能要求更为苛刻的子系统时,本章所讨论的内容迟早都要用到
本章的内容分成三个部分
讲述mmap系统调用的实现过程
讲述怎样跨越边界直接訪问用户空间的内存页
讲述了直接内存訪问(DMA)I/O操作,它使得外设具有直接訪问系统内存的能力
Linux的内存管理
地址类型

Linux是一个虚拟内存系统,这意味着用户程序所使用的地址与硬件使用的物理地址是不等同的
有了虚拟内存,在系统中执行的程序能够分配比物理内存很多其它的内存,甚至一个单独的进程都能拥有比系统物理内存很多其它的虚拟地址空间
以下是一个Linux使用的地址类型列表
用户虚拟地址

这是在用户空间程序所能看到的常规地址
物理地址
该地址在处理器和系统内存之间使用
总线地址
该地址在外围总线和内存之间使用,通常它们与处理器使用的物理地址同样
内核逻辑地址
内核逻辑地址组成了内核的常规地址空间
在大多数体系架构中。逻辑地址和与其相关联的物理地址不同,只在它们之间存在一个固定的偏移量
kmalloc返回的内存就是内核逻辑地址
内核虚拟地址
和内核逻辑地址的同样之处在于。它们都将内核空间的地址映射到物理地址上
内核虚拟地址与物理地址的映射不必是线性的一对一的
全部的逻辑地址都是内核虚拟地址。可是非常多内核虚拟地址不是逻辑地址
vmalloc分配的内存具有一个虚拟地址
<asm/page.h>
__pa()

返回其相应的物理地址
__va()
将物理地址逆向映射到逻辑地址,但这仅仅对低端内存页有效
物理地址和页
物理地址被分成离散的单元。称之为页
<asm/page.h>
PAGE_SIZE
眼下大多数系统都使用每页4096个字节
高端与低端内存
使用32位系统仅仅能在4GB的内存中寻址
内核将4GB的虚拟地址空间切割为用户空间和内核空间,一个典型的切割是将3GB分配给用户空间。1GB分配给内核空间
低端内存
存在于内核空间上的逻辑地址内存
高端内存
那些不存在逻辑地址的内存
内存映射和页结构
<linux/mm.h>
struct page
atomic_t count;

对该页的訪问计数。

当计数值为0时,该页将返回给空暇链表
void *virtual;
假设页面被映射。则指向页的内核虚拟地址;假设未被映射则为NULL
unsigned long flags;
描写叙述页状态的一系列标志
PG_locked表示内存中的页已经被锁住
PG_reserved表示禁止内存管理系统訪问该页
struct page *virt_to_page(void *kaddr);
struct page *pfn_to_page(int pfn);
针对给定的页帧号,返回page结构指针
void *page_address(struct page *page);
返回页的内核虚拟地址
<linux/highmem.h>
<asm/kmap_types.h>
void *kmap(struct page *page);
对于低端内存页来说,返回页的逻辑地址
对于高端内存,在专用的内核地址空间创建特殊的映射
void kunmap(struct page *page);
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
页表
处理器必须使用某种机制同,将虚拟地址转换为对应的物理地址。这样的机制被称为页表
它基本上是一个多层树形结构。结构化的数据中包括了虚拟地址到物理地址的映射和相关的标志位
虚拟内存区
虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构
进程的内存映射包括以下这些区域
程序的可运行代码区域
多个数据区,当中包括初始化数据、非初始化数据以及程序堆栈
与每一个活动的内存映射相应的区域
/proc/<pid>/maps
start-end perm offset major:minor inode image
vm_area_struct结构
<linux/mm.h>
struct vm_area_struct
unsigned long vm_start;
unsigned long vm_end;
struct file *vm_file;
unsigned long vm_pgoff;
unsigned long vm_flags;
struct vm_operations_struct *vm_ops;
void *vm_private_data;
struct vm_operations_struct
void (*open) (struct vm_area_struct *vma);
void (*close) (struct vm_area_struct *vma);
struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
int (*populate) (struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
内存映射处理
<linux/sched.h>

struct mm_struct
current->mm
mmap设备操作

内存映射能够提供给用户程序直接訪问设备内存的能力
映射一个设备意味着将用户空间的一段内存与设备内存关联起来
像串口和其它面向流的设备就不能进行mmap抽象
必须以PAGE_SIZE为单位进行映射
mmap方法是file_operations结构的一部分
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
有两种建立页表的方法
使用remap_pfn_range函数一次所有建立
通过nopage VMA方法每次建立一个页表
使用remap_pfn_range
int rempa_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot);
vma
虚拟内存区域
virt_addr
又一次映射时的起始用户虚拟地址
pfn
与物理内存相应的页帧号。虚拟内存将要被映射到该物理内存
页帧号仅仅是将物理地址右移PAGE_SHIFT位
size
以字节为单位
prot
新VMA要求的“保护(protection)”属性
一个简单的实现
drivers/char/mem.c
remap_pfn_range(vma, vma->vm_start, vm_.vm_pgoff, vma->vm_end – vma->vm_start, vma->vm_page_prot)
为VMA加入操作
struct vm_operations_struct simple_remap_vm_ops = {.open = simple_vma_open, .close = simple_vma_close,}
使用nopage映射内存
假设要支持mremap系统调用。就必须实现nopage函数
struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);
get_page(struct page *pageptr);
static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
if (offset >= __pa(high_memory) || (filp->f_flags & O_SYNC))
vm->vm_flags |= VM_IO
vm->vm_flags |= VM_RESERVED;
vm->vm_ops = &simple_nopage_vm_ops;
simple_vma_open(vma);
return 0;
}
struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{
struct page *pageptr;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physaddr = address – vma->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;
}
又一次映射RAM
对remap_pfn_range函数的一个限制是:它仅仅能訪问保留页和超出物理内存的物理地址
remap_pfn_range不同意又一次映射常规地址
使用nopage方法又一次映射RAM
使用vm_ops->nopage一次处理一个页错误
又一次映射内核虚拟地址
page = vmalloc_to_page(pageptr);
get_page(page);
运行直接I/O訪问
假设须要传输的数据量很大。直接进行传输数据。而不须要额外地从内核空间拷贝数据操作的參与,这将会大大提快速度
设置直接I/O的开销很巨大
使用直接I/O须要write系统调用同步运行
在每一个写操作完毕之前不能停止应用程序
<linux/mm.h>
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm-area_struct **vmas);

tsk

指向运行I/O的任务指针,该參数差点儿是current
mm
指向描写叙述被映射地址空间的内存管理结构的指针
对驱动程序来说。该參数总是current->mm
force
假设write非零。对映射的页有写权限
驱动程序对该參数总是设置为0
pages
假设调用成功,pages中包括了一个描写叙述用户空间缓冲区page结构的指针列表
vmas
假设调用成功,vmas包括了对应VMA的指针
使用直接I/O的设备通常使用DMA操作
一旦直接I/O操作完毕,就必须释放用户内存页
<linux/page-flags.h>
void SetPageDirty(struct page *page);
void page_cache_release(struct page *page);
异步I/O
<linux/aio.h>
ssize_t (*aio_read) (struct kiocb *iocb, char *buffer, size_t count, loff_t offset);
ssize_t (*aio_write) (struct kiocb *iocb, const char *buffer, size_t count, loff_t offset);
int (*aio_fsync) (struct kiocb *iocb, int datasync);
int is_sync_kiocb(struct kiocb *iocb);
int aio_complete(struct kiocb *iocb, long res, long res2);
直接内存訪问
DMA是一种硬件机制同,它同意外围设备和主内存之间直接传输它们的I/O数据。而不须要系统处理器的參与
使用这样的机制能够大大提高与设备通信的吞吐量
DMA传输数据概览
有两种方式引发传输数据

软件对数据的请求

当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将传输数据到这个缓冲区中,进程处于睡眠状态
硬件将数据写入到DMA缓冲区中,当写入完成。产生一个中断
中断处理程序获得输入的数据,应答中断。而且唤醒进程,该进程如今就可以读取数据
硬件异步地将数据传递给系统
硬件产生中断,宣告新数据的到来
中断处理程序分配一个缓冲区,而且告诉硬件向哪里数据传输
外围设备将数据写入缓冲区,完毕后产生另外一个中断
处理程序分发新数据。唤醒不论什么相关进程,然后运行清理工作
分配DMA缓冲区
使用DMA缓冲区的主要问题是:当大于一页时。它们必须占领连接的物理页,这是由于设备使用ISA或者PCI系统总线数据传输,而这两种方式使用的都是物理地址
DIY分配
get_free_pages函数能够分配多达几M字节的内存,可是对较大数量的请求。甚至是远少于128KB的请求也一般会失败,这是由于此时系统内存中充满了内存碎片
当内核不能返回请求数量的内存或须要超过128KB内存时,除了返回-ENOMEM,另外一个方法是在引导时分配内存或是为缓冲区保留顶部物理RAM
另一个方法是使用GFP_NOFAIL分配标志来为缓冲区分配内存
总线地址
使用DMA的设备驱动程序将与连接到总线接口上的硬件通信,硬件使用的是物理地址,而程序代码使用的是虚拟地址
<asm/io.h>
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
通用DMA层
内核提供了一个与总线体系架构无关的DMA层
<linux/dma-mapping.h>
处理复杂的硬件
int dma_set_mask(struct device *dev, u64 mask);

该掩码显示与设备能寻址能力相应的位
假设dma_set_mask返回0,则对该设备不能使用DMA
DMA映射
一个DMA映射是要分配的DMA缓冲区与为该缓冲区生成的、设备可訪问地址的组合
DMA映射建立了一个新的结构类型——dma_addr_t来表示总线地址
依据DMA缓冲区期望保留的时间长短。PCI代码区分两种类型的DMA映射
一致性DMA映射

这样的类型的映射存在于驱动程序生命周期中
一致性映射的缓冲区必须可同一时候被CPU和外围设备訪问
建立和使用一致性映射的开销是非常大的
流式DMA映射
通常为单独的操作建立流式映射
内核开发人员建议尽量使用流式映射,然后再考虑一致性映射
在支持映射寄存器的系统中,每一个DMA映射使用总线上的一个或者多个映射寄存器
在一些硬件中,流式映射能够被优化。但优化的方法对一致性映射无效
建立一致性DMA映射
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);

返回值是缓冲区的内核虚拟地址
与其相关的总线地址,保存在dma_handle中
void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
DMA池
DMA池是一个生成小型、一致性DMA映射的机制
<linux/dmapool.h>
struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);

allocation不为零,表示内存边界不能超越allocation
void dma_pool_destroy(struct dma_pool *pool);
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
返回的DMA缓冲区的地址是内核虚拟地址
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
建立流式DMA映射
当建立流式映射时。必须告诉内核数据流动的方向
enum dma_data_direction
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_BIDIRECTIONAL
DMA_NONE
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
有几条很重要的原则用于流式DMA映射
缓冲区仅仅能用于这种传送。即其传送方向匹配于映射时给定的方向wfhg
一旦缓冲区被映射,它将属于设备,而不是处理器
在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
单页流式映射
dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction direction);
void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);
分散/聚焦映射
这是一种特殊的流式DMA映射
如果有几个缓冲区,它们须要与设备双向数据传输
有几种方式能产生这样的情形
从raedv或者writev系统调用产生
从集群的磁盘I/O请求产生
从映射的内核I/O缓冲区中的页面链表产生
很多设备都能接受一个指针数组的分散表,以及它的长度,然后在一次DMA操作中把它们所有传输走
映射分散表的第一步是建立并填充一个描写叙述被传送缓冲区的scatterlist结构的数组
<linux/scatterlist.h>
struct scatterlist
struct page *page;
unsigned int length;
unsigned int offset;
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
nents是传入的分散表入口的数量
返回值是要传送的DMA缓冲区数
驱动程序应该传输由dma_map_sg函数返回的每一个缓冲区
dma_addr_t sg_dma_address(struct scatterlist *sg);
unsinged int sg_dma_len(struct scatterlist *sg);
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
nents一定是先前传递给dma_map_sg函数的入口项的数量
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
PCI双重地址周期映射
通常DMA支持层使用32位总线地址。其为设备的DMA掩码所约束
PCI总线还支持64位地址模式。既双重地址周期(DAC)
假设设备须要使用放在高端内存的大块缓冲区,能够考虑实现DAC支持
<linux/pci.h>
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
返回0时。才干使用DAC地址
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);
direction

PCI_DMA_TODEVICE
PCI_DMA_FROMDEVICE
PCI_DMA_BIDIRECTIONAL
void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);
void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);
ISA设备的DMA
ISA总线同意两种DMA传输:本地(native)DMA和ISA总线控制(bus-master)DMA
本地DMA使用主板上的标准DMA控制器电路来驱动ISA总线上的信号线
ISA总线控制DMA全然由外围设备控制
有三种实现涉及到ISA总线上的DMA传输数据
8237 DMA控制器(DMAC)
外围设备
当设备准备传送数据时,必须激活DMA请求信号
设备驱动程序
须要驱动程序完毕的工作非常少,它仅仅是负责提供DMA控制器的方向、总线地址、传输量的大小等等
注冊DMA
<asm/dma.h>

int request_dma(unsigned int channel, const char *name);

返回0表示运行成功
void free_dma(unsigned int channel);
与DMA控制器通信
unsigned long claim_dma_lock();
必须被装入控制器的信息包括三个部分:RAM的地址、必须被传输的原子项个数以及传输的方向
void set_dma_mode(unsigned int channel, char mode);
mode

DMA_MODE_READ
DMA_MODE_WRITE
DMA_MODE_CASCADE
释放对总线的控制
void set_dma_addr(unsigned int channel, unsigned int addr);
void set_dma_count(unsigned int channel, unsigned int count);
void disable_dma(unsigned int channel);
void enable_dma(unsigned int channel);
int get_dma_residue(unsigned int channel);
返回还未传输的字节数
void clear_dma_ff(unsigned int channel);

《Linux Device Drivers》第十五章 内存映射和DMA——note的相关教程结束。

《《Linux Device Drivers》第十五章 内存映射和DMA——note.doc》

下载本文的Word格式文档,以方便收藏与打印。