技术交流

好好学习,天天向上。

0%

第十五章——内存映射和DMA

跨越内核边界

进程VMA

进程并不是进程一启动就给足全部的虚拟地址空间的,进程的地址空间是由若干个vma(struct vm_area_struct)来描述的。vma用于描述一段连续的、有相同属性的虚拟地址空间。内核为每个进程在/proc导出了VMA信息。

1
2
cat /proc/<pid>/maps # 通过pid查询某个进程的
cat /proc/self # 总是指向当前进程

mmap

mmap允许进程通过vfs将文件、设备内存等映射到进程的虚拟地址空间。当进程执行mmap时,内核会为进程创建一个新的vma,提交给文件或者设备驱动完成mmap。

1
2
3
4
5
6
// 用户态的接口
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

// 对应到内核态的,设备驱动需要实现的文件操作接口
int (*mmap) (struct file *, struct vm_area_struct *);

需要内核驱动做的事情很明确,就是建立从内存(设备内存或者RAM)到进程vma的映射关系。这里存在两种建立方式,一种类似于一步到位,就是用remap_pfn_range接口。需要注意的是remap_pfn_range只能映射reserved内存和设备内存,无法映射通用内存

1
2
3
4
5
6
7
8
9
10
11
12
/**
* remap_pfn_range - remap kernel memory to userspace
* @vma: user vma to map to
* @addr: target user address to start at
* @pfn: physical address of kernel memory
* @size: size of map area
* @prot: page protection flags for this mapping
*
* Note: this is only safe if the mm semaphore is held when called.
*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);

每个vma有自己的struct vm_operations_struct *vm_ops,用于当这片虚拟地址在被打开、关闭、切割或者重映射时的操作等场景,当进程fork、启动、休眠时可能会触发这些操作。下面是一个利用remap_pfn_range建立映射并为添加vma添加内存操作的例子

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
void simple_vma_open(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx\n",
vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
void simple_vma_close(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA close.\n");
}
static struct vm_operations_struct simple_remap_vm_ops = {
.open = simple_vma_open,
.close = simple_vma_close,
};

// 这里利用remap_pfn_range实现驱动内存映射的一个例子,可以看到大部分参数都在传入的vma中了,当然也可以调整
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff,
vma->vm_end - vma->vm_start,
vma->vm_page_prot))
return -EAGAIN;
vma->vm_ops = &simple_remap_vm_ops; // 通常映射完一片设备内存后,要提供这片内存的操作方式 struct vm_operations_struct
simple_vma_open(vma); // 首次映射完后,直接执行打开
return 0;
}

还有一种建立vma映射的方式,就是类似于按需分配,有点像利用page_fault的小步快跑。进程对内存的映射也许不是一成不变的,映射区域可能变大也可能变小,也可能变换虚拟地址范围。当进程需要执行这些映射变化动作的时候,就会使用vma的mremap操作。当vma尺寸变大时,可能出现某片设备内存没有映射到进程地址空间的情况(类似于page_fault),这又会进一步触发nopage操作的调用。所以提供mremap操作实现时也应当一同提供nopage操作实现。需要注意的是nopage可以映射通用RAM,包括哪些非reserved内存

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

下面是一个利用nopage实现mmap的示例

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
// 此时mmap实现,仅仅是替换了vma的struct vm_operations_struct
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))
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
vma->vm_ops = &simple_nopage_vm_ops;
simple_vma_open(vma);
return 0;
}
// nopage操作的实现,在这里向内核提供正确的设备内存页面。若返回NULL,则内核将该vma请求的位置映射成全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); // 注意看这里的get_page接口,nopage操作有义务维护被映射的页面的引用计数
if (type)
*type = VM_FAULT_MINOR;
return pageptr;
}

直接I/O访问

内核具备在内核空间映射用户态内存页面的能力。调用下面接口,可以获得一个指向用户空间缓冲区的页数组。当用户需要访问页面内容的时候,需要调用kmap或者kmap_atomic完成地址映射。使用下列接口时,还要处理映射关系的释放、swap分区的缓冲保护等,具体使用时应参考demo程序。

1
2
3
4
5
6
7
long get_user_pages(unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas);
int get_user_pages_fast(unsigned long start,
int nr_pages,
int write,
struct page ** pages);

异步IO操作

ldd3讲得不是很细致,但是必须得知道,内核提供异步IO操作的能力的。使用异步操作,用户态程序可以提交IO命令,但是不阻塞用户态进程。

DMA操作

建议直接看内核文档(https://www.kernel.org/doc/html/latest/core-api/dma-api-howto.html),LDD3没有详细描述IOMMU的情况。事实上,如今的X86平台都基本配备了IOMMU