Memory Mapping and DMA [LDD3 15]

本文详细介绍了Linux内核的内存管理,包括内存类型、页管理、虚拟内存区、直接内存访问(DMA)以及DMA缓冲区的分配和管理。主要内容包括:内存映射的实现、地址类型、物理地址与页面、高低内存概念、内存映射区域的结构、设备映射操作、直接I/O以及DMA数据传输的各个阶段。文章深入探讨了DMA缓冲区的分配策略,如自定义分配、总线地址、通用DMA层及其在不同硬件架构上的应用。

Table of Contents

15.1. Memory Management in Linux

15.1.1. Address Types

15.1.2. Physical Addresses and Pages

15.1.3. High and Low Memory

15.1.4. The Memory Map and Struct Page

15.1.5. Page Tables

15.1.6. Virtual Memory Areas

15.1.6.1 The vm_area_struct structure

15.1.7. The Process Memory Map

15.2. The mmap Device Operation

15.2.1. Using remap_pfn_range

15.2.2. A Simple Implementation

15.2.3. Adding VMA Operations

15.2.4. Mapping Memory with nopage

15.2.5. Remapping Specific I/O Regions

15.2.6. Remapping RAM

15.2.6.1 Remapping RAM with the nopage method

15.2.7. Remapping Kernel Virtual Addresses

15.3. Performing Direct I/O

15.3.1. Asynchronous I/O

15.4. Direct Memory Access

15.4.1. Overview of a DMA Data Transfer

15.4.2. Allocating the DMA Buffer

15.4.2.1 Do-it-yourself allocation

15.4.3. Bus Addresses

15.4.4. The Generic DMA Layer

15.4.4.1 Dealing with difficult hardware

15.4.4.2 DMA mappings

15.4.4.3 Setting up coherent DMA mappings

15.4.4.4 DMA pools

15.4.4.5 Setting up streaming DMA mappings

15.4.4.6 Single-page streaming mappings

15.4.4.7 Scatter/gather mappings

15.4.4.8 PCI double-address cycle mappings

15.4.4.9 A simple PCI DMA example

15.4.5. DMA for ISA Devices

15.4.5.1 Registering DMA usage

15.4.5.2 Talking to the DMA controller


本章的内容分为三节:

1, mmap的实现,也就是把device memory map到user process里,这样可以提高performance。

2, kernel driver如何访问user space的page。

3, DMA,也就是device直接访问system memory。

15.1. Memory Management in Linux


15.1.1. Address Types

Linux kernel中有很多的地址类型,主要分为两类:虚拟地址,物理地址。在用户程序中看到的地址都是虚拟地址,和硬件使用的物理地址不同,虚拟地址并不直接对应物理地址,需要中转,采用虚拟地址的机制,可以让程序使用比物理地址多得多的内存。

kernel中的地址类型其实还需要细分,虚拟地址包括几种类型如下:

User virtual addresses

user space programmer看到的地址就是用户态虚拟地址,一般是32bit或者64bit,取决于当前的硬件架构,每个进程都有自己的虚拟地址空间。

Physical addresses

在处理器和系统内存之间,使用的就是物理地址。物理地址也分为32bit或者64bit,即便32bit系统,在某些条件下也能使用很大的物理内存。

Bus addresses

在外设总线和物理内存之间,使用的就是bus address。通常和CPU使用的物理地址相同,但是如果有IOMMU,就不一样了。IOMMU会把物理地址做一个map,拿到的地址就是bus address,device可以通过这个bus address做DMA。

Kernel logical addresses

这个就是kernel自己的normal地址空间,一般会把物理内存map成normal地址空间,并且可以当作物理地址空间来使用,某些架构上,kernel的逻辑地址和物理地址差一个offset,逻辑地址通常使用硬件相关的native pointer,指针能访问多少memory,逻辑地址空间就能支持多少memory,通常这个pointer是一个unsigned long或者void *类型,因此如果在32bit系统上,就可能访问不了大的内存地址。kmalloc返回的就是逻辑地址。

Kernel virtual addresses

kernel虚拟地址和逻辑地址有些类似,都是最终map到物理地址,区别在于虚拟地址对应的物理地址可能不是连续的,不是一一映射。逻辑地址是虚拟地址的子集,即所有的逻辑地址都是虚拟地址,但不是所有的虚拟地址都是逻辑地址。例如,vmalloc和kmap返回的就是虚拟地址,它对应的物理内存可能不是连续的,kmalloc返回的地址就是逻辑地址,它分出来的page都是连续的。虚拟地址通常使用指针变量来存储。

如果你有一个逻辑地址,可以通过__pa()获取到它对应的物理地址;物理地址也可以通过__va()获取它的逻辑地址,但只限于low memory page,不能用于high-memory page。

在kernel的函数中,不同的接口可能需要不同的地址类型,这个需要自己特别注意。

15.1.2. Physical Addresses and Pages

kernel中的物理内存,都是按照page来管理,每一个page都是PAGE_SIZE这么大,PAGE_SIZE具体的值取决与硬件架构,一般是4096 byte。无论虚拟地址(逻辑地址?)还是物理地址,地址的组成都分成了两个部分:PFN(page frame number)和offset,假设先的PAGE_SIZE是4096byte,那么低12bit就是作为page中的offset,如果把这低12bit向右移出去,得到的值就是PFN。获取pfn的操作在kernel中很常见,具体移出去多少bit,取决于PAGE_SHIFT这个值。

15.1.3. High and Low Memory

kernel的虚拟地址和逻辑地址的区别在大容量物理内存的32位机器上笔记明显,理论上讲,32位机器上可以寻址4G的地址空间,但是因为kernel设置虚拟地址的方式,实际值比4G要小。

按照默认设置,32位的机器上,4G的地址空间划分为user space和kernel space,user space占用了3G的虚拟地址空间,kernel space占用1G的虚拟地址空间。在kernel 1G的地址空间里,除了kernel的code、data等占用的空间之外,能够map出来使用的地址空间不到1G。kernel里,如果没有对应的虚拟地址,kernel是无法访问这段memory的,因此kernel实际能够访问的内存实际上是1G减掉kernel code等自己占用的部分,也就是不到1G。如果是大容量的内存,就会导致很多的物理地址不能map到kernel的地址空间,从而不能使用。后来CPU中添加了feature,添加了内存扩展特性,从而使得CPU可以访问到超过4G的内存。但是kernel中的逻辑地址仍然有这个限制,只能map优先的物理内存,超过这部分的memory称为high memory,而kernel map过能直接访问的memory就是low memory,也就是在kernel中直接就存在逻辑地址。这里对high、low memory又做了定义:

Low memroy

kernel中可以使用的物理内存,这部分内存在kernel中有逻辑地址相对应,被称为low memory。

High memory

这部分内存在kernel中不存在直接能够访问的逻辑地址,因为地址范围超过了kernel的虚拟地址空间反问,被称为high memory。

kernel中low memory和hight memory的分界线在1G memory以下的某个位置。

15.1.4. The Memory Map and Struct Page

因为历史原因,kernel一直使用逻辑地址来访问物理内存中的page,因此对于hight memory,这种访问就有问题了,因为high memory在kernel中没有能直接访问的逻辑地址。因此kernel中对page的访问越来越多的使用struct page这个结构体,这个结构体中的成员有:

atomic_t count;

page的reference counter,如果变成了0,就被放入free page list。

void *virtual; 

如果这个page被map过,记录的就是map后得到的虚拟地址,否则就是NULL。low memory通常都是被map过的,high memory通常没有被map。这个成员在有些架构上没有,因为他们有更好的计算虚拟地址的方法。

unsigned long flags;

用于描述page的属性和状态,比如PG_locked,说明page在memory中已经被lock;PG_reserved,说明page已经被reserve,kernel的内存管理不应该再touch这个page。

kernel中使用struct page的数组来管理物理内存,有些系统上只有一个数组mem_map,有些架构的系统上,比如NUMA,因为有大量不连续的物理内存,那就可能需要多个page 数组来管理这些内存。幸运的是,driver只需要使用struct page即可,不需要关心这个page从哪里来的。kernel提供了一些函数,可以方便的根据page获取virtual address:

#include <linux/mm.h>
#include <linux/highmem.h>
#include <asm/kmap_types.h>

//根据逻辑地址获取对应的page结构体,如果kaddr是从vmalloc或者high memory过来的,这个不能用。
struct page *virt_to_page(void *kaddr);
//根据pfn,获取它对应的page 结构体。
struct page *pfn_to_page(int pfn);
//返回这个page对应的kernel虚拟地址。如果是high memory,需要事先map过才行。一般不用,而是用kmap。
void *page_address(struct page *page);

#include <linux/highmem.h>
//kmap返回这个page对应的虚拟地址,如果是low memory,直接就是逻辑地址
//如果是high memory并且没有map过,kernel就会给它在专用的space里做一次map,然后返回虚拟地址。
//kmap对同一个page有reference counter,所以要和kunmap配对调用。
void *kmap(struct page *page);
void kunmap(struct page *page);

//是kmap的高性能版本,有些架构上会reserver一些专用的slots(PTE),给atomic用。
//参数type用来表明需要哪个slot,driver能够使用的slot一般是KM_USER0和KM_USR1(如果是在user space的系统调用),以及KM_IRQ0和KM_IRQ1(interrupt handler)。
//调用atomic的driver code必须是atomic的,不能sleep。
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);

15.1.5. Page Tables

kernel中既然使用了虚拟地址,必然存在某种机制,可以通过虚拟地址得到物理地址,这个机制就是page table,page table也许是多级数据结构来实现,并且包含一些对应的flag。device driver的很多操作可能都会涉及page table,但是kernel已经做了封装,driver不需要和page table直接打交道,这里也不再赘述。

15.1.6. Virtual Memory Areas

virtual memory area (VMA)是kernel中的结构体,用于区分和管理进程地址空间中的不同区域。一个VMA中代表了一类有相同访问权限或者底层对应了同一个object的连续虚拟地址空间,大概类似于segment的概念。一个进程地址空间中通常包含以下几个部分:

1, 存储程序代码的区域。

2, 存放数据的区域,通常有多个,比如已经初始化的变量数据,还有未初始化的数据变量,以及程序的stack等。

3, 活动中的memory mapping的一个区域。

通过/proc/<pid/maps>可以看到这个进程虚拟地址空间的状态,读取maps,打印出来的东西有几个部分:

start-end perm offset major:minor inode image

看个例子:

# cat /proc/1/maps     look at init
08048000-0804e000 r-xp 00000000 03:01 64652      /sbin/init   text
0804e000-0804f000 rw-p 00006000 03:01 64652      /sbin/init   data
0804f000-08053000 rwxp 00000000 00:00 0           zero-mapped BSS
40000000-40015000 r-xp 00000000 03:01 96278      /lib/ld-2.3.2.so   text
40015000-40016000 rw-p 00014000 03:01 96278      /lib/ld-2.3.2.so   data
40016000-40017000 rw-p 00000000 00:00 0           BSS for ld.so
42000000-4212e000 r-xp 00000000 03:01 80290      /lib/tls/libc-2.3.2.so   text
4212e000-42131000 rw-p 0012e000 03:01 80290      /lib/tls/libc-2.3.2.so   data
42131000-42133000 rw-p 00000000 00:00 0           BSS for libc
bffff000-c0000000 rwxp 00000000 00:00 0           Stack segment
ffffe000-fffff000 ---p 00000000 00:00 0           vsyscall page

# rsh wolf cat /proc/self/maps  #### x86-64 (trimmed)
00400000-00405000 r-xp 00000000 03:01 1596291     /bin/cat     text
00504000-00505000 rw-p 00004000 03:01 1596291     /bin/cat     data
00505000-00526000 rwxp 00505000 00:00 0                        bss
3252200000-3252214000 r-xp 00000000 03:01 1237890 /lib64/ld-2.3.3.so
3252300000-3252301000 r--p 00100000 03:01 1237890 /lib64/ld-2.3.3.so
3252301000-3252302000 rw-p 00101000 03:01 1237890 /lib64/ld-2.3.3.so
7fbfffe000-7fc0000000 rw-p 7fbfffe000 00:00 0                  stack
ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0        vsyscall

除了image,其他每一列在kernel的struct vm_area_struct中都有对应的成员变量(除了image name):

start
end

这个memory area的start和end虚拟地址。

perm

这个memory area对应的读写或者执行权限,表示进程针对虚拟地址对应的page所能做的操作。最后的字符要么是p,表示private,要么是s,表示shared。

offset

表示当前的VMA,在这个被map的file中的起始offset,offset为0意味着VMA的start对应了file的start。

major
minor

代表了device的major/minor number,这个device就是使用了这个被map的file的device。

inode

被map的文件对应的inode。

image

被map的文件的文件名,通常是可执行文件。

15.1.6.1 The vm_area_struct structure

当用户态进程调用了mmap去map device memory的时候,kernel就会给它创建一个新的vm_area_struct结构体。底层的device driver需要实现mmap,mmap的功能其实就是帮助kernel初始化这个VMA。

下面我们就看一下vm_area_struct这个结构体里的成员变量,其中有些可能会被device driver用到。要注意的是vm_area_struct里有一些成员变量kernel用来存储VMA的list或者树形结构,因此这个结构体不能在device driver中创建,而是由kernel自己创建。其中,比较重要的member有:

unsigned long vm_start;
unsigned long vm_end;

这个VMA的start和end,也就是/proc/pid/maps里看到的start和end。

struct file *vm_file;

如果area有关联的 file,指向它。

unsigned long vm_pgoff;

被map的file的offset,按照page来算的,不是byte。当file后者device memory被map的时候,这个就是第一个被map的page的位置。

unsigned long vm_flags;

用来描述这个vma的flags。最重要的两个flag是VM_IO和VM_RESERVED。VM_IO表示这个vma是用来做I/O的memroy map,在做core dump的时候会跳过这个vma。VM_RESERVED告诉kernel不要把vma swap出去,在大部分device的map中都会设置这个flag。

struct vm_operations_struct *vm_ops;

kernel用来操作这个vma一系列函数。说明vma在kernel中也是类似于struct file的object(对应一系列的callback,类似于面向对象)。

void *vm_private_data;

driver用来存储自己的私有数据。

下面是vm_operations_struct中的callback:

void (*open)(struct vm_area_struct *vma);

当这个vma有新的reference时(比如fork),kernel会调用实现这个VMA的subsystem的open callback,用来对vma做一些初始化。如果这个vma第一次create是通过mmap产生的,open就不会被调用,而是去调用driver实现的mmap。

void (*close)(struct vm_area_struct *vma);

当vma被destroy的时候,kernel会调用vma的close callback。要注意的是,vma本身没有记reference count,process只会调用一次open和close。

struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

当进程试图访问一个在有效的vma里面,但是当前不在memory里的page时,kernel会调用nopage这个callback。如果这个page是被swap到了别的存储设备,nopage会把这个page再swap进来,并返回struct page的指针。如果nopage callback是NULL,kernel会分配一个空的page。

int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);

当user 访问memory,kernel可以提前发生fault,driver一般不实现。(没太理解)

The Process Memory Map

kernel中的每一个进程,个别kernel thread除外,都会有一个结构体struct mm_struct,这个mm_struct中记录了很多和memory相关的数据结构,比如virtual memory area list,page tables,以及其他的一些bitmask,mmap的semaphore,page table的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值