下面介绍一下Linux内存映射的实现
一、基础概念
1、mmap文件映射
mmap是一种内存映射文件的方法,将一个文件映射到进程的地址空间,建立文件磁盘地址和进程虚拟地址的一种对应关系,如此进程通过读取相应的虚拟地址就可以直接读取相应文件中的内容。
这样映射的最大好处是进程可用直接访问内存,避免了频繁的使用read/write等文件系统的系统调用。需要注意的是mmap并不分配物理内存,它所做的最重要的工作就是为进程映射区的虚拟地址建立页表项
从图上可以看出进程的虚拟地址空间,是由多个虚拟内存区域构成的。如图所示的text数据段,初始数据段,bss数据段,堆,栈都是一个个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆和栈之间的空余部分。
2、进程的虚拟地址空间
Linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个虚拟内存区域功能和内部机制都不尽相同,因此一个进程会使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。即我们在上一页看到的text数据段,bss数据段等等。每个vm_area_struct都对应虚拟地址空间上一段连续的地址,它们之间使用链表或者树形结构链接,方便进程进行快速的查找/访问。
这里我们可用看到vm_area_struct结构中的一些字段,其中包括虚拟内存区的起始和结束的地址;vm_flags是该虚拟内存区的标志位。如果虚拟区域映射的是磁盘文件或者设备文件的话,那么vm_inode指向该文件的inode索引节点
这里有一个重要的vm_ops的字段,它是一个指向vm_operations_struct结构体的指针,在vm_operation_struct结构体中,定义了与该虚拟内存区操作相关的接口,其中包括了:open,close,fault等等这些操作。
每个虚拟内存区域都必须在vm_operations_struct结构中实现这些操作
一个进程的全部虚拟地址空间由mm_struct结构体来管理的,它里面包括了进程虚拟空间的一些管理的信息,包括进程pgd页表的地址等等,另外它还有一个指向,虚拟内存区链表的指针mmap。
最好,在进程描述符中有一个mm字段,指向mm_struct结构,这些共同组成了linux中进程虚拟地址空间的抽象描述
3、Linux中的字符设备驱动
最后是Linux中有关设备驱动的概念。
我们知道,所有的设备在linxu里面都是以设备文件的形式存在的,设备文件允许应用程序通过标准输入输出系统调用,与驱动程序进行交互,既然都是文件,当然也可以进行mmap映射,这是一种操作设备的方法。
有关设备和驱动的东西是一块很大的领域,此处以今天要使用的字符设备驱动为例,简单介绍它的基本内容。在用户程序看来,操作一个设备就是对设备文件的读写,而其具体的实现过程则是相应的驱动程序来完成的。
如上图示,方框中就是一个设备驱动的主要内容,它其中自然少不了对模块的加载和卸载函数,它们主要完成设备的初始化和删除的。它使用struct_cdev结构体来抽象描述一个字符设备,而每个cdev结构体就由一个dev_t类型的设备号来唯一指定,设备号分为主(major)设备号和次设备号,主设备号用来表明设备类型,次设备号用来表明其编号
这里可以看到由一个file_operations结构体,该结构体是linux文件系统中的一个非常重要的结构体,linux的VFS虚拟文件系统能够将不同类型的文件系统统一管理,并且为用户提供一个统一的接口,就是通过file_operations结构体实现的。
而在设备文件中,它主要用来存储驱动模块提供的对设备进行各种操作的函数,对于普通文件的read、write,驱动程序需要将其转化为对应的对设备的操作,就是通过该结构体(file_operations)完成的。它其中包括许多的钩子函数,包括read,release,mmap等等。read是进程在读设备文件时要做的,release则是在进程调用close时所要做的工作,它用来释放一些系统资源。最后是mmap,不同的文件有自己定义的mmap钩子,比如ext3文件系统对应了一个叫做generic_file_mmap的一个钩子函数
今天要做的主要工作,就是为一个虚拟字符设备编写其驱动模块,在其驱动中完成设备空间,即内核空间到用户空间的映射。
二、具体实现
进入源码,看内存映射具体的实现过程。 驱动程序源码map_driver.c
完整源码如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <asm/io.h>
#include <linux/mman.h>
#define MAP_PAGE_COUNT 10
#define MAPLEN (PAGE_SIZE*MAP_PAGE_COUNT)
#define MAP_DEV_MAJOR 240
#define MAP_DEV_NAME "mapnopage"
extern struct mm_struct init_mm;
void map_vopen(struct vm_area_struct *vma);
void map_vclose(struct vm_area_struct *vma);
/*device mmap */
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma);
static int mapdrv_open(struct inode *inode, struct file *file);
/* vm area nopage */
int map_fault(struct vm_fault *vmf);
static struct file_operations mapdrvo_fops = {
.owner = THIS_MODULE,
.mmap = mapdrv_mmap,
.open = mapdrv_open,
};
static struct vm_operations_struct map_vm_ops = {
.open = map_vopen,
.close = map_vclose,
.fault = map_fault,
};
static char *vmalloc_area = NULL;
MODULE_LICENSE("GPL");
static int __init mapdrv_init(void)
{
int result;
unsigned long virt_addr;
int i = 1;
result=register_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME,&mapdrvo_fops);
if(result<0){
return result;
}
vmalloc_area=vmalloc(MAPLEN);
virt_addr = (unsigned long)vmalloc_area;
for(virt_addr = (unsigned long)vmalloc_area; virt_addr < (unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE)
{
SetPageReserved(vmalloc_to_page((void *)virt_addr));
sprintf((char *)virt_addr, "test %d",i++);
}
/* printk("vmalloc_area at 0x%lx (phys 0x%lx)\n",(unsigned long)vmalloc_area,(unsigned long)vmalloc_to_pfn((void *)vmalloc_area) << PAGE_SHIFT); */
printk("vmalloc area apply complate!");
return 0;
}
static void __exit mapdrv_exit(void)
{
unsigned long virt_addr;
/* unreserve all pages */
for(virt_addr = (unsigned long)vmalloc_area; virt_addr < ( unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE)
{
ClearPageReserved(vmalloc_to_page((void *)virt_addr));
}
/* and free the two areas */
if (vmalloc_area)
vfree(vmalloc_area);
unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME);
}
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > MAPLEN) {
printk("size too big\n");
return -ENXIO;
}
/* only support shared mappings. */
if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {
printk("writeable mappings must be shared, rejecting\n");
return -EINVAL;
}
/* do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKONFAULT;
if (offset == 0) {
vma->vm_ops = &map_vm_ops;
} else {
printk("offset out of range\n");
return -ENXIO;
}
return 0;
}
static int mapdrv_open(struct inode *inoe, struct file *file)
{
printk("process: %s (%d)\n", current->comm, current->pid);
return 0;
}
/* open handler for vm area */
void map_vopen(struct vm_area_struct *vma)
{
printk("mapping vma is opened..\n");
}
/* close handler form vm area */
void map_vclose(struct vm_area_struct *vma)
{
printk("mapping vma is closed..\n");
}
/* page fault handler */
int map_fault(struct vm_fault *vmf)
{
struct page *page;
void *page_ptr;
unsigned long offset, virt_start, pfn_start;
offset = vmf->address-vmf->vma->vm_start;
virt_start = (unsigned long)vmalloc_area + (unsigned long)(vmf->pgoff << PAGE_SHIFT);
pfn_start = (unsigned long)vmalloc_to_pfn((void *)virt_start);
printk("\n");
/*printk("%-25s %d\n","7)PAGE_SHIFT",PAGE_SHIFT);*/
page_ptr=NULL;
if((vmf->vma==NULL)||(vmalloc_area==NULL)){
printk("return VM_FAULT_SIGBUS!\n");
return VM_FAULT_SIGBUS;
}
if(offset >=MAPLEN){
printk("return VM_FAULT_SIGBUS!");
return VM_FAULT_SIGBUS;
}
page_ptr=vmalloc_area + offset;
page=vmalloc_to_page(page_ptr);
get_page(page);
vmf->page=page;
printk("%s: map 0x%lx (0x%016lx) to 0x%lx , size: 0x%lx, page:%ld \n", __func__, virt_start, pfn_start << PAGE_SHIFT, vmf->address,PAGE_SIZE,vmf->pgoff);
return 0;
}
module_init(mapdrv_init);
module_exit(mapdrv_exit);
驱动程序大概有三部分组成,1-模块的装载卸载; 2-file_operations结构体和mmap函数;3-vm_operations_struct结构体和fault函数。
首先是模块的装载函数,它所要完成的工作是两个,一是设备的注册,二是在内核中为设备申请一块内存。
设备的注册由register_chrdev这个函数来实现,这里需要指定设备的主设备号MAP_DEV_MAJOR, 设备的名称MAP_DEV_NAME, 还有它所链接的file_operations 结构 &mapdrvo_fops);
这里如果主设备号为零,该设备将自己分配一个主设备号,返回给result。如果返回值为0,表示分配成功;返回值为负表示设备注册失败。
接下来是申请内存,此处用的是vmalloc函数,vmalloc函数的特点是申请的内存区域在内核的线性地址是连续的,但物理地址不连续。
这里我们看到这里还有为申请到的页框的PageReserved标志位置位,这样做是告诉系统,该物理页框已经被我使用。
static int __init mapdrv_init(void)
{
int result;
unsigned long virt_addr;
int i = 1;
result=register_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME,&mapdrvo_fops);
if(result<0){
return result;
}
vmalloc_area=vmalloc(MAPLEN);
virt_addr = (unsigned long)vmalloc_area;
for(virt_addr = (unsigned long)vmalloc_area; virt_addr < (unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE)
{
SetPageReserved(vmalloc_to_page((void *)virt_addr));
sprintf((char *)virt_addr, "test %d",i++);
}
/* printk("vmalloc_area at 0x%lx (phys 0x%lx)\n",(unsigned long)vmalloc_area,(unsigned long)vmalloc_to_pfn((void *)vmalloc_area) << PAGE_SHIFT); */
printk("vmalloc area apply complate!");
return 0;
}
下面是模块的卸载函数。在模块的卸载函数中,要做的正相反。
首先是清理PageReserved标志位,接着是通过vfree释放我们申请的vmalloc线性区的线性地址,最后是通过unregister_chrdev注销掉这个设备。
static void __exit mapdrv_exit(void)
{
unsigned long virt_addr;
/*unreserve all pages*/
for(virt_addr = (unsigned long)vmalloc_area;virt_addr < (unsigned long)vmalloc_area + MAPLEN;virt_addr += PAGE_SIZE)
{
ClearPageReserved(vmalloc_to_page((void *)virt_addr));
}
/* and free the two areas */
if(vmalloc_area)
vfree(vmalloc_area);
unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME);
}
接下来介绍的是file_operations结构体和mmap函数。如下为驱动程序中定义的file_operations结构体
驱动模块中只实现了三个功能,owner是用来指向该驱动module结构的指针
open函数在这里,我们用它来打印了调用该模块进程的pid
再就是mmap函数,具体分析过程在下面
static struct file_operations mapdrov_fops = {
.owner = THIS_MODULE,
.mmap = mapdrv_mmap,
.open = mapdrv_open,
};
include/linux/fs.h
内核源码中有关file_operations结构体的完整定义如下,设备的读取,写入,保持等等这些操作,都是由存储在file_operations结构体中的这些函数指针来处理的,这些函数指针所指向的函数都需要我们在驱动模块中将其实现,在这里我们可以看到file_operations结构体中提到了许多的函数指针,包括read,write,mmap,open等。但是我们可以不全使用它们。对于那些指向未实现函数的只是可以简单的设置为空。操作系统将负责实现该功能。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
在分析mapdrv_mmap这个函数之前,还要补充mmap系统调用的执行过程。一个用户进程调用了mmap系统调用之后,它需要依次执行下图那么多函数
首先用户进程在调用mmap系统调用之后,系统会为其在当前进程的虚拟地址空间中寻址一段连续的空闲地址,这是通过遍历vm_area_struct链表来实现。当找到了合适的这样的一段区间之后,会为其建立一个vm_area_struct结构,完成这些之后,该进程就有了一个专门用于mmap映射的虚拟内存区了。但是这样还不够,因为在进程页表中,这个区域的线性地址都没有对应的物理页框,接着系统会调用内核空间的系统调用函数mmap,也就是需要我们在file operations(f_op)结构体中定义的这个mmap,它将要完成对vm_area_struct结构中的虚拟地址建立其相应的页表项
而建立页表项的方法有两种,一种是remap_pfn_range方法,一种叫fault的方法,两者的区别是remap_pfn_range方法是在内核函数mmap,即file_operations中要完成的mmap函数,它在被调用时一次性的为vm_area_struct结构中的这些线性地址建立页表项,所以这也就要求了这些页表项所要映射的物理地址是连续的。
后者fault函数则不同,它是在进程访问到这个映射空间中的虚拟地址时,发现虚拟地址的页表项为空,引起了缺页时才被调用,所以它更适合于这里,我们对vmalloc分配的这些不连续的物理地址来进行映射,所以这里我们将使用fault方法。
回到驱动程序中,如下的一些判断,都是对异常情况的判断。
首先MAPLEN是之前定义的宏,它用来表示在内核中申请的物理页框的大小,如果进程中用来映射的线性地址区大于我们申请的物理地址if(size > MAPLEN),那么就可能访问到一些不该访问的地方,所以我们需要防止这种情况的发生。
接着这个判断if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)),如果我们的vm区是可写入的,它的标志位是可写入的,vm_flags用来保存进程对该虚存空间的访问权限。如果我们当前的vma是可写入的话VM_WRITE,那么他也就必须有另一个标志位VM_SHARED,也就是可共享的,如果没有可共享的标志位的话,那么写入操作就应当是非法的,我们也要把这种情况判断出来。
vma->vm_flags |= VM_LOCKONFAULT;
给vm_flags标志位增加了一个标志位,叫做VM_LOCKONFAULT,它是用来锁住该区域所映射的这些物理页框的。让它不用被操作系统交换出去
整个mmap函数最重要的,其实是这里,“vm->vm_ops = &map_vm_ops”,它将我们在驱动模块中定义的vm_operations_struct结构的地址赋值给了当前vma的vm_ops指针,也就是说它用我们在驱动模块中定义的vm_operations结构体替换掉了当前进程vma中的这样一个结构体,为什么要这么做呢?想想刚才提到的fault方法,fault方法在建立页表的时候,他是在进程访问到线性地址产生缺页时才建立页表的,所以我们在mmap函数被调用的时候,可能我们的用户进程还没有进行访问,所以呢,我们建立页表还不用现在就立即建立,所以将这个任务交给它的下一环节,即vm_operations结构体。
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > MAPLEN) {
printk("size too big\n");
return -ENXIO;
}
/* only support shared mappings. */
if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {
printk("writeable mappings must be shared, rejecting\n");
return -EINVAL;
}
/* do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKONFAULT;
if (offset == 0) {
vma->vm_ops = &map_vm_ops;
} else {
printk("offset out of range\n");
return -ENXIO;
}
return 0;
}
最后介绍的是vmoperation_struct结构体和fault 函数,以下是驱动模块中定义的vm_operation_struct结构体
static struct vm_operations_struct map_vm_ops = {
.open = map_vopen,
.close = map_vclose,
.fault = map_fault,
};
在内核源码中的完整定义,在include/linux/mm.h中
如下,发现vm_operations_struct结构体与file_operations结构体非常类似,其中也定义很多函数指针,可以有选择的实现它们。我们这里主要实现fault函数。vm_fault_t (*fault)(struct vm_fault *vmf);
关于fault函数的功能前面已经说过,它是用来为进程中用来映射的线性地址建立相应的页表项的,这里看出,传入的参数只是一个指针,在不同内核版本中,fault函数的表现形式有所不同。在更早的内核版本中可能没有这个函数,但其实与nopage函数功能类似。
fault函数体,传入指向vm_fault结构体的指针,
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*split)(struct vm_area_struct * area, unsigned long addr);
int (*mremap)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf);
vm_fault_t (*huge_fault)(struct vm_fault *vmf,
enum page_entry_size pe_size);
void (*map_pages)(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff);
unsigned long (*pagesize)(struct vm_area_struct * area);
/* notification that a previously read-only page is about to become
* writable, if an error is returned it will cause a SIGBUS */
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);
/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);
/* called by access_process_vm when get_user_pages() fails, typically
* for use by special VMAs that can switch between memory and hardware
*/
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
/* Called by the /proc/PID/maps code to ask the vma whether it
* has a special name. Returning non-NULL will also cause this
* vma to be dumped unconditionally. */
const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
/*
* set_policy() op must add a reference to any non-NULL @new mempolicy
* to hold the policy upon return. Caller should pass NULL @new to
* remove a policy and fall back to surrounding context--i.e. do not
* install a MPOL_DEFAULT policy, nor the task or system default
* mempolicy.
*/
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
/*
* get_policy() op must add reference [mpol_get()] to any policy at
* (vma,addr) marked as MPOL_SHARED. The shared policy infrastructure
* in mm/mempolicy.c will do this automatically.
* get_policy() must NOT add a ref if the policy at (vma,addr) is not
* marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
* If no [shared/vma] mempolicy exists at the addr, get_policy() op
* must return NULL--i.e., do not "fallback" to task or system default
* policy.
*/
struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
unsigned long addr);
#endif
/*
* Called by vm_normal_page() for special PTEs to find the
* page for @addr. This is useful if the default behavior
* (using pte_page()) would not find the correct page.
*/
struct page *(*find_special_page)(struct vm_area_struct *vma,
unsigned long addr);
};
下面是内核中关于vm_fault结构体的定义,它是用来存放与缺页相关的参数。
这里我们可以看到首先是映射区vma指针struct vm_area_struct *vma
unsigned long address 就是产生缺页的线性地址
pmd/pud 就是线性地址所对应的页目录项
pte就是它所对应的页表项
这个结构体中,最重要的是page字段,struct page *page; 我们在fault函数中要为这个用来映射的线性地址建立相应的页表项,那么我们首先要找到它所对应的物理地址,找到这个物理地址后,我们取到它的页描述符,将它的页描述符填写到vm_fault结构体的page字段中,剩下的建立页表项的工作就可以交由操作系统自动完成了。
struct vm_fault {
struct vm_area_struct *vma; /* Target VMA */
unsigned int flags; /* FAULT_FLAG_xxx flags */
gfp_t gfp_mask; /* gfp mask to be used for allocations */
pgoff_t pgoff; /* Logical page offset based on vma */
unsigned long address; /* Faulting virtual address */
pmd_t *pmd; /* Pointer to pmd entry matching
* the 'address' */
pud_t *pud; /* Pointer to pud entry matching
* the 'address'
*/
pte_t orig_pte; /* Value of PTE at the time of fault */
struct page *cow_page; /* Page handler may use for COW fault */
struct mem_cgroup *memcg; /* Cgroup cow_page belongs to */
struct page *page; /* ->fault handlers should return a
* page here, unless VM_FAULT_NOPAGE
* is set (which is also implied by
* VM_FAULT_ERROR).
*/
/* These three entries are valid only while holding ptl lock */
pte_t *pte; /* Pointer to pte entry matching
* the 'address'. NULL if the page
* table hasn't been allocated.
*/
spinlock_t *ptl; /* Page table lock.
* Protects pte page table if 'pte'
* is not NULL, otherwise pmd.
*/
pgtable_t prealloc_pte; /* Pre-allocated pte page table.
* vm_ops->map_pages() calls
* alloc_set_pte() from atomic context.
* do_fault_around() pre-allocates
* page table to avoid allocation from
* atomic context.
*/
};
了解了这些之后,再回到驱动代码
可以看到fault函数在驱动中是这样一个名字“.fault = map_fault,”
static struct vm_operations_struct map_vm_ops = {
.open = map_vopen,
.close = map_vclose,
.fault = map_fault,
};
这个驱动程序的目的是将内核空间的线性地址所对应的物理地址映射到用户空间的某一个线性地址中。所以我们首先需要找到这些物理页框。那么我们怎么找呢?用到的方法就是这两个函数,vmalloc_to_pfn和vmalloc_to_page,顾名思义,这两个函数可以将内核vmalloc区线性地址所指向的那个物理页框它找到,我们想要的是页描述符,那么我们就用vmalloc_to_page函数,我们想要找到页帧号,那么我们就有vmalloc_to_pfn函数。
我们从头看这个fault函数,首先offset(offset = vmf->address-vmf->vma->vm_start;),它是我们产生缺页的线性地址在这个vma映射区中的偏移量,我们用当前的缺页地址“vmf->address"减去vma的起始地址"vmf->vma->vm_start”得到它的offse
接下啦的virt_strat,它是内核中的线性地址,我们用的是vmalloc_area,也就是我们在模块装载函数中申请到的那个线性区的起始地址让它加上每一页的偏移“(vmf->pgoff << PAGE_SHIFT)”,就得到了内核中每一页的线性地址
pfn_start就是这些对应的内核中的线性地址所对应的那些物理页框的页帧号,这里我们使用的是vmalloc_to_pfn函数来找到它们
下面的if语句,就是对异常情况的判断, 比如当前进程的vma为空(“if((vmf->vmaNULL)||(vmalloc_areaNULL))”),申请到的内核中的内存区域为空。那么肯定是一种异常情况,我们将它判断出来。
接着,这个判断“if(offset >= MAPLEN)”是如果我们当前产生缺页的线性地址,它超过了我们在内核中用于映射的物理页框的大小,这样的异常我们需要将它判断出来。
这里的page_ptr(page_ptr=vmalloc_area + offset),它是内核中的线性地址,其实跟virt_start的值是一样的这里我们用vmalloc_area,即它的起始地址加上offset每一页的偏移量得到的
这里的page(page=vmalloc_to_page(page_ptr);),因为我们要取得是页描述符,所以我们就用vmalloc_to_page函数,将内核中的线性地址,找到它所对应的物理页框的页描述符,
接着需要将找到的每一页(get_page(page);),物理页框增加它的引用次数,用get_page(page)这样一个函数来实现
最后(vmf->page = page;)我们将找到的页描述符填写到vmf的page字段中,就完成了我们对一个产生缺页的线性地址的页表项的建立
“printk("%s: map 0x%lx (0x%016lx) to 0x%lx, sie:0x%lx, page:%ld \n",func,virt_start,pfn_start << PAGE_SJIFT, vmf->address,PAGE_SIZE,vmf->pgoff);” 取出内核中的线性地址所对应的物理地址,还有映射后的进程中的线性地址,统统打印出来
int map_fault(struct vm_fault *vmf)
{
struct page *page;
void *page_ptr;
unsigned long offset, virt_start, pfn_start;
offset = vmf->address-vmf->vma->vm_start;
virt_start = (unsigned long)vmalloc_area + (unsigned long)(vmf->pgoff << PAGE_SHIFT);
pfn_start = (unsigned long)vmalloc_to_pfn((void *)virt_start);
printk("\n");
/*printk("%-25s %d\n","7)PAGE_SHIFT",PAGE_SHIFT);*/
page_ptr=NULL;
if((vmf->vma==NULL)||(vmalloc_area==NULL)){
printk("return VM_FAULT_SIGBUS!\n");
return VM_FAULT_SIGBUS;
}
if(offset >=MAPLEN){
printk("return VM_FAULT_SIGBUS!");
return VM_FAULT_SIGBUS;
}
page_ptr=vmalloc_area + offset;
page=vmalloc_to_page(page_ptr);
get_page(page);
vmf->page=page;
printk("%s: map 0x%lx (0x%016lx) to 0x%lx , size: 0x%lx, page:%ld \n", __func__, virt_start, pfn_start << PAGE_SHIFT, vmf->address,PAGE_SIZE,vmf->pgoff);
return 0;
}
到这里就是有关于驱动模块的主要内容了,在结束之前再看一下前面有关一些宏的定义
我们在内核空间中申请了10个 页面大小的内存空间,主设备号是240,设备名称是mapnopage
#define MAP_PAGE_COUNT 10
#define MAPLEN (PAGE_SIZE*MAP_PAGE_COUNT)
#define MAP_DEV_MAJOR 240
#define MAP_DEV_NAME "mapnopage"
在设备注册函数中,我们不光申请了10个页面的内存“vmalloc_area=vmalloc(MAPLEN)”,并且在这里“sprintf((char *)virt_addr,“test %d”,i++);}” ,在每一页的内存中都写入了一个test字符串,从第一页开始依次是test1,test2,test3一直到test10
static int __init mapdrv_init(void)
{
int result;
unsigned long virt_addr;
int i = 1;
result = register_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME,&mapdrvo_fops);
if(result<0){
return result;
}
vmalloc_area=vmalloc(MAPLEN);
virt_addr = (unsigned long)vmalloc_area;
for(virt_addr =(unsigned long)vmalloc_area;virt_addr < (unsigned long)vmalloc_area + MAPLEN; virt_addr += PAGE_SIZE);
{
SetPageReserved(vmalloc_to_page(void *)virt_addr));
sprintf((char *)virt_addr,"test %d",i++);}
}
/* printk("vmalloc_area at 0x%lx (phys 0x%lx)\n",(unsigned long)vmalloc_area,(unsigned long)vmalloc_to_pfn(void *)vmalloc_area << PAGE_SHIFT); */
printk("vmalloc area apply complete!");
return 0;
}
好了,接下来我们来看一下有关于用户测试进程的内容
三、内存映射测试
下面为用户测试程序的代码,分为两部分,一个是读程序,一个是写程序 。
读程序中,将展示进程在用户态直接读取到我们在内核态写入内存的数据。而在写程序中,我们将展示进程在用户态直接向内核态申请的内存区里写入数据。
下面是读程序代码
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#define LEN (10*4096)
int main(void)
{
int fd,loop;
char *vadr;
if ((fd = open("/dev/mapnopage", O_RDWR)) < 0) {
return 0;
}
vadr = mmap(0, LEN, PROT_READ, MAP_PRIVATE | MAP_LOCKED, fd, 0);
for(loop=0;loop<2;loop++){
printf("[%-10s----%lx]\n",vadr+4096*loop,vadr+4096*loop);
}
while(1)
{
sleep(1);
}
}
读程序中,首先要做的是打开设备文件,open()
紧接着调用mmap系统调用,这是用户态的mmap函数,它第一个参数是映射区的地址,第二位是映射区的长度(LEN),接下啦是prot是一个标志位,表示期望的内存保护标志,PROT_READ表示页内容可以被读取;紧接着是flags标志位(MAP_PRIVATE | MAP_LOCKED),它可以是多个标志位的组合,用来指定映射对象的类型,MAP_PRIVATE表示建立的是一个写入时拷贝的私有映射区,MAP_LOCKED表示的是锁定映射区的页面,从而防止页表被交换出内存。
fd就是刚刚打开的文件描述符
最后一位offset位表示被映射的对象从哪里开始,也就是从该文件中的哪一个文件偏移量开始。这里我们从0开始
这里的循环loop++,我们可以看到,我们读取了映射区中2页的数据,并将它打印了出来
最后是一个死循环让进程执行完先暂时不要退出
接着看写入程序
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#define LEN (10*4096)
int main(void)
{
int fd;
char *vadr;
if ((fd = open("/dev/mapnopage", O_RDWR)) < 0) {
return 0;
}
vadr = mmap(0, LEN, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0);
sprintf(vadr, "write from userspace");
while(1)
{
sleep(1);
}
return 0;
}
写入程序中,我们可以看到与读取程序类似的代码。
区别仅在于写程序中,mmap标志位是MAP_SHARED,而在读程序中用的是MAP_PRIVATE
MAP_SHARED就表示该映射区可以和其它所有映射这个对象的进程共享空间,因为我们要写入,如果我们还用的是私有的映射区,我们写入的数据将只能被写入的进程看到,其它映射该内存区的进程将无法看到
接着在该区域的第一页中写入了一段字符串
最后是一个死循环也是防止进程执行完退出
我们看看它的执行情况,在开始之前,首先要将我们的驱动模块编译,并插入内核,如下为Makefile文件。与之前做的内核模块的Makefile很类似
先make,然后用insmode插入.ko文件
以上都是用于4.15.0版本的内核,不同版本的内核函数可能会略有差异,比如这里我用的是5.14.17版本的内核,要想make成功应该将map_driver.c中的如下地方由int改为vm_fault_t
make成功后如下
然后插入.ko文件
然后需要创建一个虚拟字符设备的设备节点,使用mknod命令,在/dev/目录下,这里设备文件名要与我们在驱动模块中声明的设备名一致,mapnopage,设备类型位字符c,主设备号也要跟驱动模块中定义的一致,次设备号为0
完成这些之后,我们就在系统中创建好了我们的设备,
先看读程序的运行情况,可以发现它成功的在用户态读到了两个页面的数据
我们来看以下dmesg信息
我们可以看到读程序进程pid=12595,然后分别是映射前的线性地址,(映射前的物理地址),to 映射后的线性地址
可以发现,映射前的线性地址是连续的,映射后的在进程中的线性地址也是连续的,而物理页框号/物理地址是不连续的。而且我们注意到,fault函数被调用了两次,也就意味着它只映射我们在用户进程中访问的那两页,而其它映射区的页表项还没有被建立
有了进程的pid之后,我们可以再使用命令查看该进程的内存映射信息
我们使用
cat /proc/12595/maps
在白色背景高亮的区域可以看到,/dev/mapnopage这个设备文件已经被映射到了该进程的虚拟地址空间中,而且映射区的大小页与我们在mmap系统调用中设置的一样
我们再执行一个写的程序
执行完后用dmesg中查看信息,在dmesg中可以看到写程序的pid = 12762
而且我们注意到,我们在写的程序中只写了一页,所以fault函数也就被调用了一次。而且我们注意到,在映射前的线性地址与我们在读进程中看到的是一样的,物理地址当然也是一样的,但是映射后的线性地址是不一样的。这是因为这是在两个完全不同的进程地址空间中完成的映射
我们再来执行一次读程序,看看刚才的数据有没有成功写入。
我们发现第一页的数据已经发生了改变。这三个进程映射了同一块物理内存,实现了在用户态直接读写内核空间的数据,并且在这三个进程之间实现了共享内存,到这里我们的mmap内存程序演示就全部结束了
这里补充一点
mmap是内存映射文件的一种方法,但我们在程序演示中好像除了打开设备文件,再没提到过文件的事情了。mmap也只是对内存物理地址进行了映射,而如果我们映射了一个普通文件,那么必然涉及到磁盘数据到内存数据之间的传输,下图展示了我们没有提到的那一部分,在内存中存在一个叫做文件cache的区域,在打开文件之后,磁盘上的一部分文件数据会首先被读取到内存的文件cache中,当我们对文件读取或写入时,实际上是对文件cache中的数据进行读写的,而当我们读取的数据不在内存中时,或者要向文件中写入新的数据时,操作系统就会调用磁盘IO,将数据读写到磁盘上。具体的操作由文件系统负责。
如果以上内容对您有帮助,麻烦点赞、转发或者收藏哦~!