目录
I/O内存(访问I/O内存时,需要先把物理地址映射到虚拟地址上)
11.1 CPU与内存、I/O
11.1.1 内存空间与I/O空间
- X86中存在I/O空间,ARM中不存在I/O空间,只有内存空间
- 内存空间可以直接通过地址、指针进行访问,程序中的变量和其他数据都存在于内存空间。
11.1.2 内存管理单元
- 内存管理单元(MMU)提供虚拟地址和物理地址的映射、内存访问权限保护、cache缓存控制等硬件支持
- TLB:即转换旁路缓存,TLB是MMU的核心部件,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,因此也经常被称为“快表”。
- TTW:即转换表漫游,当TLB中没有缓冲对应的地址转换关系时,需要通过对内存中转换表(大多数处理器的转换表为多级页表)的访问来获得虚拟地址和物理地址的对应关系。TTW成功后,结果应写入TLB中。
- ARM CPU进行数据访问的流程
11.2 Linux内存管理
- Linux进程能访问的内存为4GB,用户空间为0~3GB,内核空间为3~4GB
- 用户空间只能通过系统调用的方式访问内核空间
- 32位ARM系统中Linux内核的地址空间
- 0xffff0000 ~ 0xffff0fff 是中断向量表的地址 “CPU vector page”
- VMALLOC_START ~ VMALLOC_END-1 是vmalloc和ioremap的位置
- PAGE_OFFSET ~ high_memory-1 是DMA和常规区域的映射区域
- MODULES_VADDR ~ MODULES_END-1 是内核模块区域
- PKMAP_BASE ~ PAGE_OFFSET-1 是高端内存映射区
- ARM把Linux内核模块安置在3GB或2GB附近的16MB范围内,主要是为了实现内核模块和内核本身的代码段之间的短跳转。
- 对于ARM,DMA访问地址有限制的话,例如假设UART控制器的DMA只能访问32MB,那么这个低32MB就是DMA区域;32MB到高端内存地址的这段称为常规区域;再之上的称为高端内存区域。
- DMA、常规区域、高端内存区域分布图
- DMA、常规区域、高端内存区域采用buddy算法管理,把空闲的页面以2的n次方位单位进行管理,因此Linux底层内存申请都是以2的n次方为单位,优点是避免了外部碎片,任何区域里的空闲内存都可以以2的n次方进行拆分或者合并。
- 使用virt_to_phys()和phys_to_virt()可以实现虚拟内存和物理内存之间的转换,仅适用于DMA和常规区域。
11.3 内存存取
11.3.1 用户空间内存动态申请
- 用户空间中使用malloc()和free()进行内存分配和释放
- 并不是每次申请内存和释放内存都伴随着系统调用。
11.3.2 内核空间内存动态申请
- kmalloc()
-
void
*kmalloc(
size_t
size,
int
flags);
/*size位要分配的块的大小,flags位分配标志(常用的分配标志位GFP_KERNEL,表示在内核空间进程中申请内存)*/
-
该函数底层依赖于 __get_free_pages(),若暂时不能满足,进程会睡眠等待页,引起阻塞,不能在中断上下文或者持有自旋锁时使用GFP_KERNEL申请内存。
- 在不能引起阻塞的地方要使用GFP_ATOMIC标志进行内存申请
- 其他标志:
- GFP_USER 用来为用户空间页分配内存
- GFP_HIGHUSER(类似GFP_USER,但是它从高端内存分配)
- GFP_DMA(从DMA区域分配内存)
- GFP_NOIO(不允许任何I/O初始化)
- GFP_NOFS(不允许进行任何文件系统调用)
- __GFP_HIGHMEM(指示分配的内存可以位于高端内存)
- __GFP_COLD(请求一个较长时间不访问的页)
- __GFP_NOWARN(当一个分配无法满足时,阻止内核发出警告)
- __GFP_HIGH(高优先级请求,允许获得被内核保留给紧急状况使用的最后的内存页)
- __GFP_REPEAT(分配失败,则尽力重复尝试)
- __GFP_NOFAIL(标志只许申请成功,不推荐)
- __GFP_NORETRY(若申请不到,则立即放弃)
-
void
kfree();
/*和free()用法类似*/
-
- __get_free_pages()
-
返回一个指向新页的指针并将该页清零
get_zeroed_page(unsigned
int
flags);
-
返回一个指向新页的指针但该页不清零
__get_free_page(unsigned
int
flags);
-
分配多个页并返回分配内存的首地址,分配的页数为2的order次方,order最大为10或者11(1024或2048页,由硬件平台决定)
__get_free_pages(unsigned
int
flags, unsigned
int
order);
-
内存释放:
1
2
void
free_page(unsigned
long
addr);
void
free_pages(unsigned
long
addr, unsigned
long
order);
-
- vmalloc()
- 用于软件中的较大的顺序缓冲区分配内存,开销远大于__get_free_pages(),因此分配少量内存不建议使用该函数。
-
函数原型
1
2
void
*vmalloc(unsigned
long
size);
void
vfree(
void
*addr);
-
vmalloc()不能用于原子上下文,因为内部实现了标志位GFP_KERNEL的kmalloc()
- vmalloc()在申请内存时,会进行内存的映射,改变页表项,而kmalloc()时开机映射好的DMA和常规区域的页表项。
- slab与内存池
-
解决问题:
- 使用页为单位申请和释放容易导致浪费
- Linux中用到的对象,如果前后两次被使用时分配在同一块内存或者同一类内存空间且保留了相同的数据结构,可以提高效率。
-
创建slab缓存,可以保留任意数目且全部同样大小的后备缓存
/*size是要分配的每个数据结构的大小,flags是控制如何分配的位掩码*/
struct
kmem_cache *kmem_cache_creat(
const
char
*name,
size_t
size,
size_t
align, unsigned longflags,
void
(*ctor)(
void
*,
struct
kmem_cache *, unsigned
long
),
void
(*dtor)(
void
*,
struct
kmem_cache *, unsigned
long
));
-
分配slab缓存
void
*kmem_cache_alloc(
struct
kmem_cache *cachep, gfp_t flags);
/*在创建的slab后备缓存中分配一块并返回首地址指针*/
-
释放slab缓存
void
kmem_cache_free(
struct
kmem_cache *cachep,
void
*objp);
-
收回slab缓存
int
kmem_cache_destory(
struct
kmem_cache *cachep);
-
slab使用方法
1
2
3
4
5
6
7
8
9
10
11
/* 创建slab缓存 */
static
kmem_cache_t *xxx_cachep;
xxx_cachep = kmem_cache_create(
"xxx"
,
sizeof
(
struct
xxx),
0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);
/* 分配slab缓存 */
struct
xxx *ctx;
ctx = kmem_cache_alloc(xxx_cachep, GFP_KERNEL);
...
/* 使用slab缓存 */
/* 释放slab缓存 */
kmem_cache_free(xxx_cachep, ctx);
kmem_cache_destroy(xxx_cachep);
-
在系统中通过/proc/slabinfo节点可以获知当前slab的分配和使用情况:cat /proc/slabinfo
-
slab在底层依然依赖于__get_free_pages(),slab在底层每次申请1页和多页,之后再分隔这些页为更小的单元进行管理,节省了内存,提高了slab缓冲对象的访问效率。
-
内存池技术用于分配大量小对象的后备缓存技术。
-
创建内存池
/* params: min_nr 需要与分配对象的数目
alloc_fn/free_fn 指向内存池机制提供的标准对象分配和回收函数的指针
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
*/
mempool_t *mempool_creat(
int
min_nr, mempool_alloc_t *alloc_fn,
mempool_free_t *free_fn,
void
*pool_data);
-
分配和回收对象
void
*mempool_alloc(mempool_t *pool,
int
gfp_mask);
void
mempool_free(
void
*element, mempool_t *pool);
-
回收内存池
void
mempool_destroy(mempool_t *pool);
-
11.4 设备I/O端口和I/O内存的访问
11.4.1 Linux I/O端口和I/O内存访问接口
- 设备的控制寄存器、数据寄存器、状态寄存器,当位于I/O空间时,称为I/O端口,位于内存空间时,称为I/O内存
-
I/O端口的读写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 读写字节端口(8位宽) */
unsigned inb(unsigned port);
void
outb(unsigned
char
byte, unsigned port);
/* 读写字节端口(16位宽) */
unsigned inw(unsigned port);
void
outw(unsigned
char
byte, unsigned port);
/* 读写字节端口(32位宽) */
unsigned inl(unsigned port);
void
outl(unsigned
char
byte, unsigned port);
/* 读写一串字节 */
/* insb()从端口port开始读count个字节并写入addr指向的内存 */
/* outsw将addr指向内存中的count个字节连续写入port端口中 */
void
insb(unsigned port,
void
*addr, unsigned
long
count);
void
outsb(unsigned port,
void
*addr, unsigned
long
count);
/* 读写一串字 */
void
insw(unsigned port,
void
*addr, unsigned
long
count);
void
outsw(unsigned port,
void
*addr, unsigned
long
count);
/* 读写一串长字 */
void
insl(unsigned port,
void
*addr, unsigned
long
count);
void
outsl(unsigned port,
void
*addr, unsigned
long
count);
-
I/O内存(访问I/O内存时,需要先把物理地址映射到虚拟地址上)
1
2
3
4
void
*ioremap(unsigned
long
offset, unsigned
long
size);
//返回一个虚拟地址
void
iounmap(
void
*addr);
void
__iomem *devm_iomap(
struct
device *dev, resource_size_t offset,
unsigned
long
size);
//与其他的devm_开头的函数类似,该函数不需要在
-
物理地址映射完成后,可用过指针(不推荐)或者API进行访问I/O内存
I/O访问 API
1
2
3
4
5
6
7
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c) ({ u16__v = readw_relaxed(c); __iormb(); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })
#define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
11.4.2 申请与释放设备的I/O端口和I/O内存
-
I/O端口申请与释放
1
2
3
4
5
6
7
8
9
10
11
12
/***
\brief:向内核申请n个端口,这些端口从first开始,name为设备的名称
\return: 不是NULL 分配成功
NULL 失败
***/
struct
resource *request_region(unsigned
long
first, unsigned
long
n,
const
char
*name);
/* 将request_region()申请的I/O端口归还系统 */
void
release_region(unsigned
long
start, unsigned
long
n);
/* 无需释放变体 */
devm_request_region();
-
I/O内存申请
1
2
3
4
5
6
7
8
9
10
11
12
/***
\brief:向内核申请n个内存地址,这些地址从first开始,name为设备的名称
\return: 不是NULL 分配成功
NULL 失败
***/
struct
resource *request_mem_region(unsigned
long
start, unsigned
long
len,
char
*name);
/* 将request_mem_region()申请的I/O内存释放 */
void
release_mem_region(unsigned
long
start, unsigned
long
len);
/* 无需释放变体 */
devm_request_mem_region();
11.4.3 设备I/O端口和I/O内存访问流程
-
I/O端口访问流程:
-
I/O内存访问步骤:
- 有时候驱动会在访问寄存器或者I/O端口前,省略request_mem_region()或者request_region()这样的调用
11.4.4 将设备地址映射到用户空间
- 一般情况下,不允许用户空间直接访问设备的寄存器或者地址空间,但设备驱动中实现mmap()函数,可以使用户空间直接访问设备的寄存器或者地址空间。
- mmap()必须以PAGE_SIZE为单位进行映射,若映射非PAGE_SIZE的整数倍,要先进行页对齐,强行以PAGE_SIZE的倍数大小映射。
-
mmap()和munmap()函数映射
1
2
3
4
5
6
7
8
/* 内核态的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);
/* munmap()解除映射 */
int
munmap(caddr_t addr,
size_t
len );
-
当用户调用mmap()时,内核会做如下处理:
- 在进程的虚拟空间查找一块VMA。
- 将这块VMA进行映射。
- 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,则调用它。
- 将这个VMA插入进程的VMA链表中。
- VMA是vm_area_struct结构体,用于描述一个虚拟内存区域
-
vm_operations_struct操作范例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct
vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned
long
vm_start;
/* Our start address within vm_mm. */
unsigned
long
vm_end;
/* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct
vm_area_struct *vm_next, *vm_prev;
struct
rb_node vm_rb;
...
/* Second cache line starts here. */
struct
mm_struct *vm_mm;
/* The address space we belong to. */
pgprot_t vm_page_prot;
/* Access permissions of this VMA. */
unsigned
long
vm_flags;
/* Flags, see mm.h. */
...
const
struct
vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned
long
vm_pgoff;
/* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct
file * vm_file;
/* File we map to (can be NULL). */
void
* vm_private_data;
/* was vm_pte (shared mem) */
...
};
-
VMA结构体描述的虚拟地址介于vm_start到vm_end之间
- fault()函数:在驱动程序中实现VMA的fault()函数通常可以为设备提供更加灵活的内存映射途径。当访问的页不在内存里,即发生缺页异常时,fault()会被内核自动调用,而fault()的具体行为可以自定义。
- 当发生缺页异常时,系统会经过如下处理过程:
- 找到缺页的虚拟地址所在的VMA。
- 如果必要,分配中间页目录表和页表。
- 如果页表项对应的物理页面不存在,则调用这个VMA的fault()方法,它返回物理页面的页描迏符。
- 将物理页面的地址填充到页表中
- 对于显示、视频等设备,建立映射可减少用户空间和内核空间之间的内存复制。
11.6 DMA
- DMA与cache一致性:
Cache被用作CPU针对内存的缓存,利用程序的空间局部性和时间局部性原理,达到较高的命中率,从而避免CPU每次都必须要与相对慢速的内存交互数据来提高数据的访问速率。
Cache数据与内存数据的不一致性,是指在采用Cache的系统中,同样一个数据可能既存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,数据若不一样则具有不一致性。 - Cache的不一致性问题并不是只发生在DMA的情况下,实际上,它还存在于Cache使能和关闭的时刻。例如,对于带MMU功能的ARM处理器,在开启MMU之前,需要先置Cache无效
- Linux下的DMA编程:
- DMA区域
- 使用kmalloc()和__get_free_pages()时 ,需要添加标志GFP_DMA(对于x86系统且小于16MB时)
-
如果不想使用log2size(即order)为参数申请DMA内存,则可以使用另一个函数dma_mem_alloc()
static
unsigned
long
dma_mem_alloc(
int
size)
-
对于大多数现代嵌入式处理器而言,DMA操作可以在整个常规内存区域进行,因此DMA区域就直接覆盖了常规内存。
- 虚拟地址、物理地址和总线地址
- 基于DMA的硬件使用的是总线地址而不是物理地址,总线地址是从设备角度上看到的内存地址,物理地址则是从CPU MMU控制器外围角度上看到的内存地址(从CPU核角度看到的是虚拟地址)
- DMA地址掩码:
设备并不一定能在所有的内存地址上执行DMA操作,在这种情况下应该通过下列函数执行DMA地址掩码:int
dma_set_mask(
struct
device *dev, u64 mask);
int
arm_dma_set_mask(
struct
device *dev, u64 dma_mask);
- 一致性DMA缓冲区:
- DMA映射包括两方面:
- 分配一片DMA缓冲区
- 为缓冲区产生设备可访问的地址
-
分配一个DMA一致性内存区域:
/* 返回值为申请到的DMA缓冲区的虚拟地址,通过参数handle返回DMA缓冲区的总线地址 */
void
* dma_alloc_coherent(
struct
device *dev,
size_t
size, dma_addr_t *handle,
gfp_t gfp);
/* 对应的释放函数 */
void
dma_free_coherent(
struct
device *dev,
size_t
size,
void
*cpu_addr,
dma_addr_t handle);
-
分配一个写合并的DMA缓冲区:
void
* dma_alloc_writecombine(
struct
device *dev,
size_t
size, dma_addr_t
*handle, gfp_t gfp);
/* 释放函数同dma_free_coherent() */
#define dma_free_writecombine(dev,size,cpu_addr,handle) \
dma_free_coherent(dev,size,cpu_addr,handle)
-
PCI设备申请DMA缓冲区的函数:
void
* pci_alloc_consistent(
struct
pci_dev *pdev,
size_t
size, dma_addr_t *dma_addrp);
-
dma_alloc_xxx()函数虽然是以dma_alloc_开头的,但是其申请的区域不一定在DMA区域里面。以32位ARM处理器为例,当coherent_dma_mask小于0xffffffff时,才会设置GFP_DMA标记,并从DMA区域去申请内存。
- DMA映射包括两方面:
- 流式DMA映射
- 流式DMA使用的步骤:
- 进行流式DMA映射
- 执行DMA操作
- 进行流式DMA去映射
-
流式DMA映射:
/*** 如果映射成功,返回的时地址总线,否则返回NULL
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);
/* dma_map_single()的反函数 */
void
dma_unmap_single(
struct
device *dev, dma_addr_t dma_addr,
size_t
size,
enum
dma_data_direction direction);
-
一般情况驱动设备驱动不应该访问unmap的流式DMA缓冲区,如果一定要这么做,可先使用如下函数获得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缓冲区,在其支持SG模式的情况下,申请多个相对较小的不连续的DMA缓冲区通常是防止申请太大的连续物理空间的方法。在Linux内核中,使用如下函数映射SG:
int
dma_map_sg(
struct
device *dev,
struct
scatterlist *sg,
int
nents,
enum
dma_data_direction direction);
/* 执行dma_map_sg()后,通过sg_dma_address()可返回scatterlist对应缓冲区的总线地址,sg_dma_len()可返回scatterlist对应缓冲区的长度 */
dma_addr_t sg_dma_address(
struct
scatterlist *sg);
unsigned
int
sg_dma_len(
struct
scatterlist *sg);
/* 在DMA传输结束后,可通过dma_map_sg()的反函数dma_unmap_sg()除去DMA映射 */
void
dma_unmap_sg(
struct
device *dev,
struct
scatterlist *list,
int
nents,
enum
dma_data_direction direction);
/* SG映射属于流式DMA映射,与单一缓冲区情况下的流式DMA映射类似,如果设备驱动一定要访问映射情况下的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);
- 流式DMA使用的步骤:
- dmaengine标准API
-
申请DMA通道
struct
dma_chan *dma_request_slave_channel(
struct
device *dev,
const
char
*name);
struct
dma_chan *__dma_request_channel(
const
dma_cap_mask_t *mask,
dma_filter_fn fn,
void
*fn_param);
-
释放DMA
void
dma_release_channel(
struct
dma_chan *chan);
-
- DMA区域