Linux内存映射

一、《Linux内核分析与应用》的4.5~4.7节视频

(一)4.5动手实践-Linux内存映射基础

在这里插入图片描述

mmap文件映射:mmap是一种内存映射文件的方法,即将一个文件映射到进程的地址空间,建立文件磁盘地址和进程虚拟地址的一种对应关系。

如此,进程就可以通过读写相应的虚拟地址,而直接读写相应文件中的内容了

这样映射最大的好处是进程可以直接直接访问内存

从图上可以看出进程的虚拟地址空间是由多个虚拟内存区域构成的

在这里插入图片描述

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个虚拟内存区域功能和内部机制都不尽相同,因此一个进程会使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域

每个vm_area_struct都对应虚拟地址空间上一段连续的地址,它们之间使用链表或树形结构链接,方便进程进行快速查找访问

vm_flags是该虚拟内存区的标志位

如果虚拟内存区域映射的是磁盘文件或者设备文件的话,那么vm_inode指向该文件的inode索引节点

vm_ops字段是一个指向vm_operations_struct结构体的指针,在vm_operations_struct结构体中定义了与该虚拟内存区操作相关的接口,每个虚拟内存区域都必须在vm_operations_struct结构中实现操作

一个进程的全部虚拟地址空间由mm_struct结构体来管理的,它里面包括了进程虚拟空间的一些管理信息

在这里插入图片描述

所有的设备在Linux里都是以设备文件的形式存在的

在用户程序看来操控一个设备就是对设备文件的读写,而其具体的实现过程则是由相应的驱动程序来完成的

Linux中的字符设备驱动:

​ 使用struct_cdev结构体来抽象描述一个字符设备

​ 每个cdev结构体由dev_t类型的设备号来唯一指定

​ 设备号分为主设备号和次设备号,主设备号用来表明设备类型,而次设备号用来表明其编号

​ file_operations结构体在设备文件中,它主要用来存储驱动模块提供的对设备进行各种操作的函数

(二)4.6动手实践-Linux内存映射实现

驱动程序大致由这么三部分组成:

  1. 模块的装载和卸载函数
  2. file_operations结构体和mmap函数
  3. vm_operations_struct结构体和fault函数

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	//在内核空间中申请10个页面大小的内存空间
#define MAPLEN (PAGE_SIZE*MAP_PAGE_COUNT)  
#define MAP_DEV_MAJOR 240	//主设备号是240
#define MAP_DEV_NAME "mapnopage"	//设备名是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 */  
vm_fault_t map_fault(struct vm_fault *vmf);  
  
static struct file_operations mapdrvo_fops = {  
    .owner = THIS_MODULE,	//owner是指向该驱动module结构的指针
    .mmap = mapdrv_mmap,	//用于将设备内存映射到进程地址空间
    .open = mapdrv_open,	//open函数在这里用来打印调用该模块进程的pid
}; 

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");  

/*模块的装载函数,它要完成:一是注册设备,链接file_operations结构;二是使vma(虚拟内存区域)链接vm_operations_struct结构,实现在内核中为设备申请一块内存*/
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);	//注册设备,第一个参数为设备的主设备号,第二个参数为设备的名称,第三个参数为它所链接的file_operations结构,返回值为0则分配成功,返回值为负则表示注册设备失败
   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));	//对申请到的物理页框的pagereserved标志位置位,这样做是告诉系统,该物理页框已经被我们使用了
       sprintf((char *)virt_addr, "test %d",i++);   //在每一页内存中写入一个test字符串,从第一页开始依次是test1、test2、test3一直到test10          
   }
   /* 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));	//清理pagereserved标志位  
    }  
    /* and free the two areas */  
    if (vmalloc_area)
        vfree(vmalloc_area);	//释放掉申请的vmalloc线性区的线性地址
    unregister_chrdev(MAP_DEV_MAJOR,MAP_DEV_NAME);	//注销掉设备
  
}  

//将设备内存映射到进程地址空间
static int mapdrv_mmap(struct file *file, struct vm_area_struct *vma)  
{ 
	//通过下面的代码,很明显可以看出这个驱动程序对应这个vma
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;  
    unsigned long size = vma->vm_end - vma->vm_start;  
    //对异常情况进行判断
    if (size > MAPLEN) {  //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;	//|= 意思为:按位或后赋值,这里是给vm_flags增加一个标志位VM_LOCKONFAULT,VM_LOCKONFAULT是用来锁住该区域所映射的这些物理页框的
    if (offset == 0) {  
        vma->vm_ops = &map_vm_ops;	//将在驱动模块中定义的vm_operation_struct结构的地址赋给了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,将内核空间的线性地址所对应的物理地址,映射到进程的某一个线性地址中*/ 
vm_fault_t 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;	//offset是在vma映射区中的偏移量,当前缺页地址 - vma的起始地址
    virt_start = (unsigned long)vmalloc_area + (unsigned long)(vmf->pgoff << PAGE_SHIFT);	//virt_start是内核中的线性地址
     pfn_start = (unsigned long)vmalloc_to_pfn((void *)virt_start);	//pfn_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_ptr是内核中的线性地址
	page=vmalloc_to_page(page_ptr);	//page是物理页框的页描述符
	get_page(page);	//增加页框的引用次数
	vmf->page=page; 	//将找到的页描述符填写到vmf的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);  

(三)4.7动手实践-Linux内存映射测试

用户测试程序的代码分为一下两部分:

  1. 读程序,将展示进程在用户态直接读取到我们在内核态写入内存的数据
  2. 写程序,将展示进程在用户态直接向内核态申请的内存区里写入数据

maptest_read.c代码如下:

#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);	//调用mmap系统调用,第一个参数是映射区的地址;第二个参数是映射区的长度;第三个参数是一个标志位,表示期望的内存保护标志,PROT_READ表示页内容可以被读取;第四个参数是flags标志位,它可以是多个标志位的组合,用来指定映射对象的类型,MAP_PRIVATE表示建立的是一个写入时拷贝的私有映射区,MAP_LOCKED表示的是锁定映射区的页面,从而防止页表被交换出内存;第五个参数fd就是刚刚打开的文件描述符;第六个参数是offset位表示被映射的对象从哪里开始,也就是从该文件中的哪一个文件偏移量开始
    for(loop=0;loop<2;loop++){		//读取映射区中2页的数据,并将它打印出来
        printf("[%-10s----%lx]\n",vadr+4096*loop,vadr+4096*loop);
    }
    while(1)	//最后是一个死循环让进程执行完先暂时不要退出
    {
        sleep(1);
    }
     
}

maptest_write.c代码如下:

#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);	//MAP_SHARED就表示该映射区可以和其它所有映射这个对象的进程共享空间,因为我们要写入,如果我们还用的是私有的映射区,我们写入的数据将只能被写入的进程看到,其它映射该内存区的进程将无法看到
    
    sprintf(vadr, "write from userspace");	//在该区域的第一页中写入了一段字符串
    
    while(1)	//最后是一个死循环也是防止进程执行完退出
    {
       sleep(1);
    }     
    return 0;
}  

二、作业

(一)进入Linux 内核(4.19)源代码, 给出task_struct 结构,mm_struct 结构和 vm_area_struct 结构的关系图,这样的数据结构设计对你有什么启发?每个数据结构至少分析2个以上字段

在这里插入图片描述

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个虚拟内存区域功能和内部机制都不尽相同,因此一个进程会使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域

每个vm_area_struct都对应虚拟地址空间上一段连续的地址,它们之间使用链表或树形结构链接,方便进程进行快速查找访问

vm_flags是该虚拟内存区的标志位

如果虚拟内存区域映射的是磁盘文件或者设备文件的话,那么vm_inode指向该文件的inode索引节点

vm_ops字段是一个指向vm_operations_struct结构体的指针,在vm_operations_struct结构体中定义了与该虚拟内存区操作相关的接口,每个虚拟内存区域都必须在vm_operations_struct结构中实现操作

一个进程的全部虚拟地址空间由mm_struct结构体来管理的,它里面包括了进程虚拟空间的一些管理信息

**启发:**task_struct结构体真是一个强大的结构体,像网一样将进程的信息体系化,控制着进程,在以后遇到有关进程的问题时,可以从这一结构体出发去寻找和解决问题

(二)对实验中涉及的相关内核数据结构、函数进行深入分析,并画出流程图。至少分析6个数据结构和6个函数

流程图:

在这里插入图片描述

数据结构(这里仅涉及该程序所用到的代码):

//file_operations结构体完成设备读取、写入、保存等等这些操作,都是由存储在file_operations结构体中的这些函数指针来处理的,这些函数指针所指向的函数都需要我们在驱动模块将其实现。
struct file_operations {
	struct module *owner;    //拥有该结构的模块的指针,一般为THIS_MODULES
	......
	int (*mmap) (struct file *, struct vm_area_struct *);    //用于请求将设备内存映射到进程地址空间
	......
	int (*open) (struct inode *, struct file *);    //打开
	......
}
//vm_operations_struct结构体建立页表项
struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);		//打开
	void (*close)(struct vm_area_struct * area);	//关闭
	......
	vm_fault_t (*fault)(struct vm_fault *vmf);	//fault函数用来为进程中那些用来映射的线性地址建立相应的页表项。vm_fault这个结构体用来存放域缺页有关的参数
	......
}
//vm_fault结构体用来存放缺页有关的参数。
struct vm_fault {
	struct vm_area_struct *vma;	//映射区vma指针
	unsigned int flags;		/* FAULT_FLAG_xxx flags */
	gfp_t gfp_mask;			/*用于分配的gfp掩码*/
	pgoff_t pgoff;			/*基于vma的逻辑页偏移 */
	unsigned long address;		//adress是产生缺页的线性地址
	pmd_t *pmd;			//pmd pud就是这个线性地址所对应的页,目录项。
	pud_t *pud;						 
	pte_t orig_pte;			//pte是它所对应的页表项

	struct page *cow_page;		/* Page handler may use for COW fault */
	struct mem_cgroup *memcg;	/* Cgroup cow_page belongs to */
	struct page *page;		//在fault函数中为用来映射的线性地址建立页表项,首先要找到它所对应的物理地址,找到物理地址后取它的页描述符,将其填写到vm_fault结构体的page字段中,剩下建立页表项工作可以交给os自动完成了。
	/* 这三个条目仅在持有ptl lock时有效 */
	pte_t *pte;  //指向与“地址”匹配的pte条目的指针。如果尚未分配页表,则为NULL。
	spinlock_t *ptl;		/* 页表锁。如果“pte”不为空,则保护pte页表,否则为pmd*/
	pgtable_t prealloc_pte;		
};
//mm_struct被称为内存描述符(memory descriptor),抽象并描述了Linux视角下管理进程地址空间的所有消息,其定义在include/linux/mm_types.h中
struct mm_struct {
	......
	//指向线性区对象的链表头
	struct vm_area_struct *mmap;		/* list of VMAs */
	......
}
//vm_area_struct此结构定义内存VM内存区域。每个VM区域/任务都有一个。VM区域是进程虚拟内存空间的任何部分,它对页面错误处理程序(即共享库、可执行区域等)具有特殊规则。
struct vm_area_struct {
	unsigned long vm_start;		/* 我们的起始地址在vm_mm内。 */
    unsigned long vm_end;		/* vm_mm中结束地址后的第一个字节。*/
    ......
    unsigned long vm_flags; /* Flags, see mm.h. */
    ......
    const struct vm_operations_struct *vm_ops;	/* 处理此结构的函数指针。 */
    ......
    unsigned long vm_pgoff; /* 以页面大小为单位的偏移量(在vm_file中)*/
    ......
}

函数:

/*mmap系统调用的执行过程*/
//一个用户进程,它调用mmap系统调用后,它需要依次执行以下函数。
//首先系统会为其在当前进程的虚拟地址空间中寻找一段连续的空闲地址,这是通过遍历vm_area_struct链表实现的,当找到这样一个合适的地址区间后,为其建立一个vm_area_struct结构,完成这些之后,该进程就有一个专门用于mmap映射的虚拟内存区了。
//但在进程的页表中,这个区域中的线性地址没有对应的物理页框,接着系统会调用内核空间的系统调用函数mmap,也就是我们在file_operations结构中定义的mmap,它将要完成对vm_area_struct结构中的虚拟地址,为它们建立其对应的页表项。
//建立页表项方法有两种,一种是remap_pfn_range方法,一种叫fault方法。前者在内核函数mmap也就是在file_operations中要完成的mmap函数。它在被调用时一次性的为vm_area_struct结构中的这些线性地址建立页表项,要求页表项所要映射的物理地址是连续的,fault函数是在进程访问到这个映射空间中的虚拟地址时,发现该虚拟地址的页表项为空,引起缺页时才被调用,更适合vmalloc这种不连续地址的映射。
SYSCALL_DEFINE6(mmap_pgoff,...
	sys_mmap_pgoff
		SYSCALL_DEFINE6(mmap_pgoff,...
			vm_mmap_pgoff
				do_mmap_pgoff
					mmap_rgion
						File->f_op->mmap()

remap_pfn_range()
fault()

其他函数见上文第一部分程序代码

(三)对实验结果进行分析,用调试工具对内核进行调试(截图),遇到的问题,以及心得体会

终端1:读程序的运行结果:

在这里插入图片描述

可以发现它成功的在用户态读到了两个页面的数据

终端2:执行指令dmesg,查看信息:

在这里插入图片描述

可以看到读程序进程pid=3228,然后是 map_fault: map 映射前的线性地址(映射前的物理地址),to 映射后的线性地址

可以发现,映射前的线性地址是连续的,映射后的在进程中的线性地址也是连续的,而物理页框号/物理地址是不连续的。而且我们注意到,fault函数被调用了两次,也就意味着它只映射我们在用户进程中访问的那两页,而其它映射区的页表项还没有被建立

终端2:执行指令cat /proc/3228/maps,查看读进程的内存映射信息

在这里插入图片描述

在白色背景高亮的区域可以看到,/dev/mapnopage这个设备文件已经被映射到了该进程的虚拟地址空间中,而且映射区的大小页与我们在mmap系统调用中设置的一样

终端1:关闭读程序,运行写程序

终端2:执行指令dmesg,查看信息:

在这里插入图片描述

在这里插入图片描述

在dmesg中可以看到写程序的pid = 3316

而且我们注意到,我们在写的程序中只写了一页,所以fault函数也就被调用了一次。而且我们注意到,在映射前的线性地址与我们在读进程中看到的是一样的,物理地址当然也是一样的,但是映射后的线性地址是不一样的。这是因为这是在两个完全不同的进程地址空间中完成的映射

终端1:关闭写程序,运行读程序,运行结果:

在这里插入图片描述

我们发现第一页的数据已经发生了改变。这三个进程映射了同一块物理内存,实现了在用户态直接读写内核空间的数据,并且在这三个进程之间实现了共享内存

**遇到问题:**make时编译会报错,如下图:

在这里插入图片描述

解决:把map_fault函数的返回值类型改为vm_fault_t即可,因为在map_fault函数中对异常情况判断部分的代码,返会值类型为vm_fault_t

**心得体会:**这个过程让我真切的感受到了线性地址和物理地址的映射关系、内存空间的分配,以及在内存中读写信息的过程

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值