linux中mmap系统调用原理分析与实现
1、mmap系统调用(功能)
void* mmap ( void * addr , size_t len , int prot , int flags ,int fd , off_t offset )
内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。
2、mmap系统调用(参数)
1)addr: 指定映射的起始地址, 通常设为NULL, 由系统指定。
2)length: 映射到内存的文件长度。
3) prot: 映射区的保护方式, 可以是:
PROT_EXEC: 映射区可被执行
PROT_READ: 映射区可被读取
PROT_WRITE: 映射区可被写入
4)flags: 映射区的特性, 可以是:
MAP_SHARED:写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
5)fd: 由open返回的文件描述符, 代表要映射的文件。
6)offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射。
3、解除映射
int munmap(void *start,size_t length)
功能:取消参数start所指向的映射内存,参数length表示欲取消的内存大小。
返回值:解除成功返回0,否则返回-1,错误原因存于errno中。
实例分析
mmap系统调用
4、虚拟内存区域
虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。一个进程的内存映象由下面几部分组成:程序代码、数据、BSS
和栈区域,以及内存映射的区域。
一个进程的内存区域可以通过查看:/proc/pid/maps
08048000-0804f000 r-xp 00000000 08:01 573748 /sbin/rpc.statd #text
0804f000-08050000 rw-p 00007000 08:01 573748 /sbin/rpc.statd #data
08050000-08055000 rwxp 00000000 00:00 0 #bss
040000000-40015000 r-xp 00000000 08:01 933965 /lib/ld2.3.2.so #text
40015000-40016000 rw-p 00014000 08:01 933965 /lib/ld-2.3.2.so #data
每一行的域为:start_end perm offset major:minor inode
1) Start: 该区域起始虚拟地址
2) End: 该区域结束虚拟地址
3) Perm: 读、写和执行权限;表示对这个区域,允许进程做什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。
4) Offset: 被映射部分在文件中的起始地址
5) Major、minor:主次设备号
6) Inode:索引结点
5、vm_area_struct
Linux内核使用结构vm_area_struct(<linux/mm_types.h>)来描述虚拟内存区域,其中几个主要成员如下:
1)unsigned long vm_start 虚拟内存区域起始地址
2)unsigned long vm_end 虚拟内存区域结束地址
3)unsigned long vm_flags 该区域的标记。如:VM_IO和VM_RESERVED。VM_IO将该VMA标记为内存映射的IO区域,VM_IO会阻止系统将该区域包含在进程的存放转
存(core dump )中,VM_RESERVED标志内存区域不能被换出。
6、mmap设备操作
映射一个设备是指把用户空间的一段地址关联到设备内存上。当程序读写这段用户空间的地址时,它实际上是在访问设备。
mmap设备方法需要完成什么功能?
mmap方法是file_oprations结构的成员,在mmap系统调用发出时被调用。在此之前,内核已经完成了很多工作。mmap设备方法所需要做的就是建立
虚拟地址到物理地址的页表。
int (*mmap) (struct file *, struct vm_area_struct *)
mmap如何完成页表的建立?
方法有二:
1)使用remap_pfn_range一次建立所有页表;
2)使用nopage VMA方法每次建立一个页表。
构造页表的工作可由remap_pfn_range函数完成,原型如下:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
vma: 虚拟内存区域指针
virt_addr: 虚拟地址的起始值
pfn: 要映射的物理地址所在的物理页帧号,可将物理地址>>PAGE_SHIFT得到。
size: 要映射的区域的大小。
prot: VMA的保护属性。
int memdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
Vma->vm_flags |= VM_IO;
Vma->vm_flags |= VM_RESERVED;
if (remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev- >data)>> PAGE_SHIFT,
size,
vma->vm_page_prot))
return -EAGAIN;
return 0;
}
7、mmap设备方法实例
1)memdev.源码
#ifndef _MEMDEV_H_
#define _MEMDEV_H_#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 0 /*预设的mem的主设备号*/
#endif#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*设备数*/
#endif#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
};#endif /* _MEMDEV_H_ */
2)memdev.c源码
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>#include <linux/kernel.h>
#include "memdev.h"static int mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/
struct cdev cdev;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
return 0;
}/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
static int memdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0;
}/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.open = mem_open,
.release = mem_release,
.mmap = memdev_mmap,
};/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;dev_t devno = MKDEV(mem_major, 0);
/* 静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}
if (result < 0)
return result;/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;
/* 注册字符设备 */
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/* 为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!mem_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}
return 0;fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
kfree(mem_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}MODULE_AUTHOR("yinjiabin);
MODULE_LICENSE("GPL");module_init(memdev_init);
module_exit(memdev_exit);
3)测试程序源码
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>int main()
{
int fd;
char *start;
//char buf[100];
char *buf;
/*打开文件*/
fd = open("/dev/memdev0",O_RDWR);
buf = (char *)malloc(100);
memset(buf, 0, 100);
start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
/* 读出数据 */
strcpy(buf,start);
sleep (1);
printf("buf 1 = %s\n",buf);/* 写入数据 */
strcpy(start,"Buf Is Not Null!");
memset(buf, 0, 100);
strcpy(buf,start);
sleep (1);
printf("buf 2 = %s\n",buf);
munmap(start,100); /*解除映射*/
free(buf);
close(fd);
return 0;
}
----------------------------------------------------------------------------------------------
简单粗暴有效的mmap与remap_pfn_range – 小叮当
2016-10-24 乌云drops - 永久存档 7265次访问
原文地址:http://drops.wooyun.org/papers/13127
0x00 背景
众所周知,现代操作系统为了安全和统筹硬件的原因,采用了一套非常复杂的管理内存的方式,并由此产生了物理地址,逻辑地址,虚拟地址等概念。这部分内容不负累述,简单来说如下图
kernel与用户态进程拥有不同的逻辑地址空间,kernel所在的页面拥有更高的权限,用户权限是不可以随意更改的,否则岂不是可以改掉自己的权限,为所欲为。
不过这也不是完全密不透风的墙,内核提供了多种途径供用户态交流数据。其中如果需要在短时间内交换大量数据,并且有实时的要求,linux kernel 提供了一种简单有效的方式:共享内存——mmap syscall。
原理也非常简单,映射给kernel的物理页面也同样映射一份给用户进程,并且修改掉权限属性。这样的话分属不同地址空间的两块内存实际上对应的是同一个物理页面,一方修改数据,另一方也能够实时看到变化。
一般的应用场景是在嵌入式设备的外设中,比如要实施刷新LED显示屏,实施记录大量传感器数据,等。这需要开发人员在自己的驱动程序和用户代码中同时实现mmap的逻辑才能够实现。
首先,在自定义的驱动文件中要提供这样的接口函数:
内核函数remap_pfn_range()
会根据需求映射页面进用户进程。
详见:http://www.makelinux.net/ldd3/chp-15-sect-2
随后在用户程序中简单掉用这个自定义的mmap函数就可以建立起页面映射,达到共享内存的目的了。
0x01 漏洞
越是简单好用的,就越容易出现问题。
函数remap_pfn_range()
并不会检查传入的参数,它会完全按照需求的内存起始位置,所需长度,访问权限,去映射页面进用户态。所有这些检查都需要自定义的驱动中实现的mmap函数去完成。
上面的截图是官方文档中给出的例子,当然不能用这段代码直接用到产品中!
#!cpp
/*------------------------------------------------------------------------------
Function name : hx170dec_mmap
Description : mmap method
Return type : int
------------------------------------------------------------------------------*/
static int hx170dec_mmap(struct file *file, struct vm_area_struct *vma)
{
if (vma->vm_end - vma->vm_start >
((DEC_IO_SIZE + PAGE_SIZE - 1) & PAGE_MASK))
return -EINVAL;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */
if (remap_pfn_range(vma,
vma->vm_start,
(DEC_IO_BASE >> PAGE_SHIFT),
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
pr_err("%s(): remap_pfn_range() failed\n", __FUNCTION__);
return -EINVAL;
}
return 0;
}
只是稍微对申请的长度做了检查,其他的理所当然地相信了用户代码。
造成的后果很严重:
- https://labs.mwrinfosecurity.com/system/assets/762/original/mwri_advisory_huawei_driver-root-exploit.pdf
- WooYun: MTK相机内核驱动缺陷导致的权限提升
- WooYun: 华为最新Ascend P6手机内核缺陷造成本地权限提升
- WooYun: 华为海思平台解码器驱动缺陷以及权限设置不当
我们再来看一个比较好的例子:
https://github.com/nirodg/android_device_huawei_hwp7/blob/master/kernel/huawei/hwp7/drivers/hik3/g1dec/hx170dec.c
#!cpp
static int hx170dec_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long start = vma->vm_start;
unsigned long size = vma->vm_end - vma->vm_start;
int retval = 0;
unsigned long pyhs_start = vma->vm_pgoff << PAGE_SHIFT;
unsigned long pyhs_end = pyhs_start + size;
if(!(pyhs_start >= hisi_reserved_codec_phymem//not codec memory
&& pyhs_end <= hisi_reserved_codec_phymem + HISI_MEM_CODEC_SIZE)
&& !(pyhs_start >= hx170dec_data.iobaseaddr//not io address
&& pyhs_end <= hx170dec_data.iobaseaddr + hx170dec_data.iosize)) {
printk(KERN_ERR "%s(%d) failed map:0x%lx-0x%lx\n", __FUNCTION__, __LINE__, pyhs_start, pyhs_end);
return -EFAULT;
}
/* make buffers write-thru cacheable */
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
if (remap_pfn_range(vma, start, vma->vm_pgoff, size, vma->vm_page_prot))
retval = -ENOBUFS;
return retval;
}
在调用remap_pfn_range
函数之前做了诸多检查,限制了申请地址的起始和结束位置。
0x02 其他
android系统的整体安全架构是很繁复庞大的,上文中出现的漏洞如果发生在某个驱动中是不能单独被利用提权的。因为其他应用没有权限去访问这个有问题的驱动。但是如果这个驱动的访问权限再出错,比如:
那么连续的犯错下来,对于攻击者来说,就真的是666了。
0x03 结语
本文所讨论的安全问题在某厂商的设备中经常出现,并且时间跨度长达几年,希望贵厂能够提高安全意识,加班赶工的同时也注意下代码质量。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
内存映射函数remap_pfn_range学习——示例分析(1)
https://www.cnblogs.com/pengdonglin137/p/8149859.html 一共有2篇,这里就放一篇
阅读目录(Content)
作者
彭东林
QQ 405728433
平台
Linux-4.10.17
Qemu-2.8 + vexpress-a9
DDR:1GB
参考
Linux进程分配内存的两种方式--brk() 和mmap()
概述
Linux内核提供了remap_pfn_range函数来实现将内核空间的内存映射到用户空间:
1 /** 2 * remap_pfn_range - remap kernel memory to userspace 3 * @vma: user vma to map to 4 * @addr: target user address to start at 5 * @pfn: physical address of kernel memory 6 * @size: size of map area 7 * @prot: page protection flags for this mapping 8 * 9 * Note: this is only safe if the mm semaphore is held when called. 10 */ 11 int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, 12 unsigned long pfn, unsigned long size, pgprot_t prot);
上面的注释对参数进行了说明。当用户调用mmap时,驱动中的file_operations->mmap会被调用,可以在mmap中调用remap_pfn_range,它的大部分参数的值都由VMA提供。具体可以参考LDD3的P420.
正文
下面结合一个简单的例子学习一下。在驱动中申请一个32个Page的缓冲区,这里的PAGE_SIZE是4KB,所以内核中的缓冲区大小是128KB。user_1和user_2将前64KB映射到自己的用户空间,其中user_1向缓冲区中写入字符串,user_2去读取。user_3和user_4将后64KB映射到自己的用户空间,其中user_3向缓冲区中写入字符串,user_4读取字符串。user_5将整个128KB映射到自己的用户空间,然后将缓冲区清零。此外,在驱动中申请缓冲区的方式有多种,可以用kmalloc、也可以用alloc_pages,当然也可用vmalloc,下面会分别针对这三个接口实现驱动。
涉及到的测试程序和驱动程序可以到下面的链接下载:
https://github.com/pengdonglin137/remap_pfn_demo
一、驱动程序
下面先以kzalloc申请缓冲区的方式为例介绍,调用kmalloc申请32个页,我们知道kzalloc返回的虚拟地址的特点是对应的物理地址也是连续的,所以在调用remap_pfn_range的时候很方便。首先在驱动init的时候申请128KB的缓冲区:
1 static int __init remap_pfn_init(void) 2 { 3 int ret = 0; 4 5 kbuff = kzalloc(BUF_SIZE, GFP_KERNEL); // 这里的BUF_SIZE是128KB 6 if (!kbuff) { 7 ret = -ENOMEM; 8 goto err; 9 } 10 11 ret = misc_register(&remap_pfn_misc); // 注册一个misc设备 12 if (unlikely(ret)) { 13 pr_err("failed to register misc device!\n"); 14 goto err; 15 } 16 17 return 0; 18 19 err: 20 return ret; 21 }
第11行注册了一个misc设备,相关信息如下:
1 static struct miscdevice remap_pfn_misc = { 2 .minor = MISC_DYNAMIC_MINOR, 3 .name = "remap_pfn", 4 .fops = &remap_pfn_fops, 5 };
这样加载驱动后会在/dev下生成一个名为remap_pfn的节点,用户程序可以通过这个节点跟驱动通信。其中remap_pfn_fops的定义如下:
1 static const struct file_operations remap_pfn_fops = { 2 .owner = THIS_MODULE, 3 .open = remap_pfn_open, 4 .mmap = remap_pfn_mmap, 5 };
第3行的open函数这里没有做什么实际的工作,只是打印一些log,比如将进程的内存布局信息输出
第4行,负责处理用户的mmap请求,这是需要关心的。
先看一下open函数具体打印了那些内容:
1 static int remap_pfn_open(struct inode *inode, struct file *file) 2 { 3 struct mm_struct *mm = current->mm; 4 5 printk("client: %s (%d)\n", current->comm, current->pid); 6 printk("code section: [0x%lx 0x%lx]\n", mm->start_code, mm->end_code); 7 printk("data section: [0x%lx 0x%lx]\n", mm->start_data, mm->end_data); 8 printk("brk section: s: 0x%lx, c: 0x%lx\n", mm->start_brk, mm->brk); 9 printk("mmap section: s: 0x%lx\n", mm->mmap_base); 10 printk("stack section: s: 0x%lx\n", mm->start_stack); 11 printk("arg section: [0x%lx 0x%lx]\n", mm->arg_start, mm->arg_end); 12 printk("env section: [0x%lx 0x%lx]\n", mm->env_start, mm->env_end); 13 14 return 0; 15 }
第5行将进程的名字以及pid打印出来
第6行打印进程的代码段的范围
第7行打印进程的data段的范围,其中存放的是已初始化全局变量。而bss段存放的是未初始化全局变量,存放位置紧跟在data段后面,堆区之前
第8行打印进程的堆区的起始地址和当前地址
第9行打印进程的mmap区的基地址,这里的mmap区是向下增长的。具体mmap区的基地址跟系统允许的当前进程的用户栈的大小有关,用户栈的最大size越大,mmap区的基地址就越小。修改用户栈的最大尺寸需要用到ulimit -s xxx命令,单位是KB,表示用户栈的最大尺寸,用户栈的尺寸可以上G,而内核栈却只有区区的2个页。
第10行打印进程的用户栈的起始地址,向下增长
第11行和第12行的暂不关心。
下面是remap_pfn_mmap的实现:
1 static int remap_pfn_mmap(struct file *file, struct vm_area_struct *vma) 2 { 3 unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; 4 unsigned long pfn_start = (virt_to_phys(kbuff) >> PAGE_SHIFT) + vma->vm_pgoff; 5 unsigned long virt_start = (unsigned long)kbuff + offset; 6 unsigned long size = vma->vm_end - vma->vm_start; 7 int ret = 0; 8 9 printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\n", pfn_start << PAGE_SHIFT, offset, size); 10 11 ret = remap_pfn_range(vma, vma->vm_start, pfn_start, size, vma->vm_page_prot); 12 if (ret) 13 printk("%s: remap_pfn_range failed at [0x%lx 0x%lx]\n", 14 __func__, vma->vm_start, vma->vm_end); 15 else 16 printk("%s: map 0x%lx to 0x%lx, size: 0x%lx\n", __func__, virt_start, 17 vma->vm_start, size); 18 19 return ret; 20 }
第3行的vma_pgoff表示的是该vma表示的区间在缓冲区中的偏移地址,单位是页。这个值是用户调用mmap时传入的最后一个参数,不过用户空间的offset的单位是字节(当然必须是页对齐),进入内核后,内核会将该值右移PAGE_SHIFT(12),也就是转换为以页为单位。因为要在第9行打印这个编译地址,所以这里将其再左移PAGE_SHIFT,然后赋值给offset。
第4行计算内核缓冲区中将被映射到用户空间的地址对应的物理页帧号。virt_to_phys接受的虚拟地址必须在低端内存范围内,用于将虚拟地址转换为物理地址,而vmaloc返回的虚拟地址不在低端内存范围内,所以需要用专门的函数。
第5行计算内核缓冲区中将被映射到用户空间的地址对应的虚拟地址
第6行计算该vma表示的内存区间的大小
第11行调用remap_pfn_range将物理页帧号pfn_start对应的物理内存映射到用户空间的vm->vm_start处,映射长度为该虚拟内存区的长度。由于这里的内核缓冲区是用kzalloc分配的,保证了物理地址的连续性,所以会将物理页帧号从pfn_start开始的(size >> PAGE_SHIFT)个连续的物理页帧依次按序映射到用户空间。
将驱动编译成模块后,insmod到内核。
二、用户测试程序
这里的五个测试程序都很简单,只是为了证明他们之间确实共享了同一块内存。
user_1.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 sprintf(addr, "I am %s\n", argv[0]); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }
第10和第12行,打开设备节点,然后从内核空间映射64KB的内存到用户空间,首地址存放在addr中,由于后面既要写入也要共享,所以设置了对应的flags。这里指定的offset是0,即映射前64KB。
第14行输出字符串到addr指向的虚拟地址空间
user_2.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 printf("%s", addr); 15 16 while(1) 17 sleep(1); 18 19 return 0; 20 }
user_2跟user_1实现一般一样,不同之处是将addr指向的虚拟地址空间的内容打印出来。
user_3.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (16*PAGE_SIZE) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 sprintf(addr, "I am %s\n", argv[0]); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }
第12行的OFFSET设置的是64KB,表示将内核缓冲区的后64KB映射到用户空间
第14行,向缓冲区中输入字符串
user_4.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (16*PAGE_SIZE) 3 #define OFFSET (16*PAGE_SIZE) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 10 fd = open("/dev/remap_pfn", O_RDWR); 11 12 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET); 13 14 printf("%s", addr); 15 16 while(1) 17 sleep(1); 18 return 0; 19 }
第12行的OFFSET设置的是64KB,表示将内核缓冲区的后64KB映射到用户空间
第14行,输出缓冲区中内容
user_5.c:
1 #define PAGE_SIZE (4*1024) 2 #define BUF_SIZE (32*PAGE_SIZE) 3 #define OFFSET (0) 4 5 int main(int argc, const char *argv[]) 6 { 7 int fd; 8 char *addr = NULL; 9 int *brk; 10 11 fd = open("/dev/remap_pfn", O_RDWR); 12 13 addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0); 14 memset(addr, 0x0, BUF_SIZE); 15 16 printf("Clear Finished\n"); 17 18 while(1) 19 sleep(1); 20 return 0; 21 }
第13行,将内核缓冲区的整个128KB都映射到用户空间
第14行,清除缓冲区中内容
三、测试
1、内核空间的虚拟内存布局
在内核的启动log里可以看到内核空间的虚拟内存布局信息:
1 [ 0.000000] Virtual kernel memory layout: 2 [ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB) 3 [ 0.000000] fixmap : 0xffc00000 - 0xfff00000 (3072 kB) 4 [ 0.000000] vmalloc : 0xf0800000 - 0xff800000 ( 240 MB) 5 [ 0.000000] lowmem : 0xc0000000 - 0xf0000000 ( 768 MB) 6 [ 0.000000] pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB) 7 [ 0.000000] modules : 0xbf000000 - 0xbfe00000 ( 14 MB) 8 [ 0.000000] .text : 0xc0008000 - 0xc0800000 (8160 kB) 9 [ 0.000000] .init : 0xc0b00000 - 0xc0c00000 (1024 kB) 10 [ 0.000000] .data : 0xc0c00000 - 0xc0c7696c ( 475 kB) 11 [ 0.000000] .bss : 0xc0c78000 - 0xc0cc9b8c ( 327 kB)
用kzalloc分配的内存会落在第5行表示的虚拟内存范围内
用vmalloc分配的内存会落在第4行表示的虚拟内存范围内
2、用户虚拟地址空间的布局
下面是Linux系统下用户的虚拟内存布局大致信息:
这里需要注意的是:
当调用malloc分配内存的时候,如果传给malloc的参数小于128KB时,系统会在heap区分配内存,分配的方式是向高地址调整brk指针的位置。当传给malloc的参数大于128KB时,系统会在mmap区分配,即分配一块新的vma,其中可能会涉及到vma的合并扩展等操作。
可以参考:Linux进程分配内存的两种方式--brk() 和mmap()
3、user_1和user_2
运行user1:
[root@vexpress mnt]# ./user_1
可以看到如下内核log:
1 [ 2494.835749] client: user_1 (870) 2 [ 2494.835918] code section: [0x8000 0x87f4] 3 [ 2494.836047] data section: [0x107f4 0x1092c] 4 [ 2494.836165] brk section: s: 0x11000, c: 0x11000 5 [ 2494.836307] mmap section: s: 0xb6f17000 6 [ 2494.836441] stack section: s: 0xbe909e20 7 [ 2494.836569] arg section: [0xbe909f23 0xbe909f2c] 8 [ 2494.836689] env section: [0xbe909f2c 0xbe909ff3] 9 [ 2494.836943] phy: 0x8eb60000, offset: 0x0, size: 0x10000 10 [ 2494.837176] remap_pfn_mmap: map 0xeeb60000 to 0xb6d75000, size: 0x10000
进程号是870,可以分别用下面的查看一下该进程的地址空间的map信息:
1 [root@vexpress mnt]# cat /proc/870/maps 2 00008000-00009000 r-xp 00000000 00:12 1179664 /mnt/user_1 3 00010000-00011000 rw-p 00000000 00:12 1179664 /mnt/user_1 4 b6d75000-b6d85000 rw-s 00000000 00:10 8765 /dev/remap_pfn 5 b6d85000-b6eb8000 r-xp 00000000 b3:01 143 /lib/libc-2.18.so 6 b6eb8000-b6ebf000 ---p 00133000 b3:01 143 /lib/libc-2.18.so 7 b6ebf000-b6ec1000 r--p 00132000 b3:01 143 /lib/libc-2.18.so 8 b6ec1000-b6ec2000 rw-p 00134000 b3:01 143 /lib/libc-2.18.so 9 b6ec2000-b6ec5000 rw-p 00000000 00:00 0 10 b6ec5000-b6ee6000 r-xp 00000000 b3:01 188 /lib/libgcc_s.so.1 11 b6ee6000-b6eed000 ---p 00021000 b3:01 188 /lib/libgcc_s.so.1 12 b6eed000-b6eee000 rw-p 00020000 b3:01 188 /lib/libgcc_s.so.1 13 b6eee000-b6f0e000 r-xp 00000000 b3:01 165 /lib/ld-2.18.so 14 b6f13000-b6f15000 rw-p 00000000 00:00 0 15 b6f15000-b6f16000 r--p 0001f000 b3:01 165 /lib/ld-2.18.so 16 b6f16000-b6f17000 rw-p 00020000 b3:01 165 /lib/ld-2.18.so 17 be8e9000-be90a000 rw-p 00000000 00:00 0 [stack] 18 bed1c000-bed1d000 r-xp 00000000 00:00 0 [sigpage] 19 bed1d000-bed1e000 r--p 00000000 00:00 0 [vvar] 20 bed1e000-bed1f000 r-xp 00000000 00:00 0 [vdso] 21 ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
上面的每一行都可以表示一个vma的映射信息,其中第4行是需要关心的:
1 b6d75000-b6d85000 rw-s 00000000 00:10 8765 /dev/remap_pfn
含义:
"b6d75000"是vma->vm_start的值,"b6d85000"是vma->vm_end的值,b6d85000减b6d75000是64KB,即给vma表示的虚拟内存区域的大小。
"rw-s"表示的是vma->vm_flags,其中's'表示share,'p'表示private
"00000000"表示偏移量,也就是vma->vm_pgoff的值
"00:10"表示该设备节点的主次设备号
"8765"表示该设备节点的inode值
"/dev/remap_pfn"表示设备节点的名字。
也可以用pmap查看该进程的虚拟地址空间映射信息:
1 [root@vexpress mnt]# pmap -x 870 2 870: {no such process} ./user_1 3 Address Kbytes PSS Dirty Swap Mode Mapping 4 00008000 4 4 0 0 r-xp /mnt/user_1 5 00010000 4 4 4 0 rw-p /mnt/user_1 6 b6d75000 64 0 0 0 rw-s /dev/remap_pfn 7 b6d85000 1228 424 0 0 r-xp /lib/libc-2.18.so 8 b6eb8000 28 0 0 0 ---p /lib/libc-2.18.so 9 b6ebf000 8 8 8 0 r--p /lib/libc-2.18.so 10 b6ec1000 4 4 4 0 rw-p /lib/libc-2.18.so 11 b6ec2000 12 8 8 0 rw-p [ anon ] 12 b6ec5000 132 64 0 0 r-xp /lib/libgcc_s.so.1 13 b6ee6000 28 0 0 0 ---p /lib/libgcc_s.so.1 14 b6eed000 4 4 4 0 rw-p /lib/libgcc_s.so.1 15 b6eee000 128 122 0 0 r-xp /lib/ld-2.18.so 16 b6f13000 8 8 8 0 rw-p [ anon ] 17 b6f15000 4 4 4 0 r--p /lib/ld-2.18.so 18 b6f16000 4 4 4 0 rw-p /lib/ld-2.18.so 19 be8e9000 132 4 4 0 rw-p [stack] 20 bed1c000 4 0 0 0 r-xp [sigpage] 21 bed1d000 4 0 0 0 r--p [vvar] 22 bed1e000 4 0 0 0 r-xp [vdso] 23 ffff0000 4 0 0 0 r-xp [vectors] 24 -------- ------ ------ ------ ------ 25 total 1808 662 48 0
然后运行user_2:
1 [root@vexpress mnt]# ./user_2 2 I am ./user_1
可以看到user_1写入的信息,下面是内核log以及虚拟地址空间映射信息:
1 [ 2545.832903] client: user_2 (873) 2 [ 2545.833087] code section: [0x8000 0x87e0] 3 [ 2545.833178] data section: [0x107e0 0x10918] 4 [ 2545.833262] brk section: s: 0x11000, c: 0x11000 5 [ 2545.833346] mmap section: s: 0xb6fb5000 6 [ 2545.833423] stack section: s: 0xbea0ee20 7 [ 2545.833499] arg section: [0xbea0ef23 0xbea0ef2c] 8 [ 2545.833590] env section: [0xbea0ef2c 0xbea0eff3] 9 [ 2545.833761] phy: 0x8eb60000, offset: 0x0, size: 0x10000 10 [ 2545.833900] remap_pfn_mmap: map 0xeeb60000 to 0xb6e13000, size: 0x10000 11 12 [root@vexpress mnt]# cat /proc/873/maps 13 00008000-00009000 r-xp 00000000 00:12 1179665 /mnt/user_2 14 00010000-00011000 rw-p 00000000 00:12 1179665 /mnt/user_2 15 b6e13000-b6e23000 rw-s 00000000 00:10 8765 /dev/remap_pfn 16 b6e23000-b6f56000 r-xp 00000000 b3:01 143 /lib/libc-2.18.so 17 b6f56000-b6f5d000 ---p 00133000 b3:01 143 /lib/libc-2.18.so 18 b6f5d000-b6f5f000 r--p 00132000 b3:01 143 /lib/libc-2.18.so 19 b6f5f000-b6f60000 rw-p 00134000 b3:01 143 /lib/libc-2.18.so 20 b6f60000-b6f63000 rw-p 00000000 00:00 0 21 b6f63000-b6f84000 r-xp 00000000 b3:01 188 /lib/libgcc_s.so.1 22 b6f84000-b6f8b000 ---p 00021000 b3:01 188 /lib/libgcc_s.so.1 23 b6f8b000-b6f8c000 rw-p 00020000 b3:01 188 /lib/libgcc_s.so.1 24 b6f8c000-b6fac000 r-xp 00000000 b3:01 165 /lib/ld-2.18.so 25 b6fb0000-b6fb3000 rw-p 00000000 00:00 0 26 b6fb3000-b6fb4000 r--p 0001f000 b3:01 165 /lib/ld-2.18.so 27 b6fb4000-b6fb5000 rw-p 00020000 b3:01 165 /lib/ld-2.18.so 28 be9ee000-bea0f000 rw-p 00000000 00:00 0 [stack] 29 beedf000-beee0000 r-xp 00000000 00:00 0 [sigpage] 30 beee0000-beee1000 r--p 00000000 00:00 0 [vvar] 31 beee1000-beee2000 r-xp 00000000 00:00 0 [vdso] 32 ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
上面的的log信息可以查看: https://github.com/pengdonglin137/remap_pfn_demo/blob/master/log/user2
根据上面的空间映射信息可以得到下面的示意图:
4、user_3和user_4
相关的log信息可以查看:
https://github.com/pengdonglin137/remap_pfn_demo/blob/master/log/user3
https://github.com/pengdonglin137/remap_pfn_demo/blob/master/log/user4
下面是运行user3的log和映射信息:
1 [ 4938.000918] client: user_3 (884) 2 [ 4938.001117] code section: [0x8000 0x87f4] 3 [ 4938.001205] data section: [0x107f4 0x1092c] 4 [ 4938.001281] brk section: s: 0x11000, c: 0x11000 5 [ 4938.001410] mmap section: s: 0xb6ff1000 6 [ 4938.001485] stack section: s: 0xbea10e20 7 [ 4938.001549] arg section: [0xbea10f23 0xbea10f2c] 8 [ 4938.001606] env section: [0xbea10f2c 0xbea10ff3] 9 [ 4938.001793] phy: 0x8eb70000, offset: 0x10000, size: 0x10000 10 [ 4938.001996] remap_pfn_mmap: map 0xeeb70000 to 0xb6e4f000, size: 0x10000 11 12 [root@vexpress mnt]# 13 [root@vexpress mnt]# cat /proc/884/maps 14 00008000-00009000 r-xp 00000000 00:12 1179666 /mnt/user_3 15 00010000-00011000 rw-p 00000000 00:12 1179666 /mnt/user_3 16 b6e4f000-b6e5f000 rw-s 00010000 00:10 8765 /dev/remap_pfn 17 b6e5f000-b6f92000 r-xp 00000000 b3:01 143 /lib/libc-2.18.so 18 b6f92000-b6f99000 ---p 00133000 b3:01 143 /lib/libc-2.18.so 19 b6f99000-b6f9b000 r--p 00132000 b3:01 143 /lib/libc-2.18.so 20 b6f9b000-b6f9c000 rw-p 00134000 b3:01 143 /lib/libc-2.18.so 21 b6f9c000-b6f9f000 rw-p 00000000 00:00 0 22 b6f9f000-b6fc0000 r-xp 00000000 b3:01 188 /lib/libgcc_s.so.1 23 b6fc0000-b6fc7000 ---p 00021000 b3:01 188 /lib/libgcc_s.so.1 24 b6fc7000-b6fc8000 rw-p 00020000 b3:01 188 /lib/libgcc_s.so.1 25 b6fc8000-b6fe8000 r-xp 00000000 b3:01 165 /lib/ld-2.18.so 26 b6fed000-b6fef000 rw-p 00000000 00:00 0 27 b6fef000-b6ff0000 r--p 0001f000 b3:01 165 /lib/ld-2.18.so 28 b6ff0000-b6ff1000 rw-p 00020000 b3:01 165 /lib/ld-2.18.so 29 be9f0000-bea11000 rw-p 00000000 00:00 0 [stack] 30 bebe9000-bebea000 r-xp 00000000 00:00 0 [sigpage] 31 bebea000-bebeb000 r--p 00000000 00:00 0 [vvar] 32 bebeb000-bebec000 r-xp 00000000 00:00 0 [vdso] 33 ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
需要关注的是第16行,其中的"00010000"表示offset,大小是64KB,也就是vma->vm_pgoff的值。
下面是user_3和user_4的共享内存的示意图:
5、user_5
user_5负责将128KB的内核缓冲区映射到自己的用户空间,并清除其中的内容。
log信息可以查看:https://github.com/pengdonglin137/remap_pfn_demo/blob/master/log/user5
下面是映射示意图:
未完待续...
----------------------------------------------------------------------------------------------
linux内核学习笔记-struct vm_area_struct
Linux内核中,关于虚存管理的最基本的管理单元应该是struct vm_area_struct了,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。
下面是struct vm_area_struct结构体的定义:
/* * This struct defines a memory VMM memory area. There is color: black; background-color: #a0ffff;">vm_area_struct { struct mm_struct * vm_mm; /* VM area parameters */ unsigned long vm_start; unsigned long vm_end; /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next; pgprot_t vm_page_prot; unsigned long vm_flags; /* AVL tree of VM areas per task, sorted by address */ short vm_avl_height; struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right; /* For areas with an address space and backing store, * font-size: 10px;">vm_area_struct *vm_next_share; struct vm_area_struct **vm_pprev_share; struct vm_operations_struct * vm_ops; unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; unsigned long vm_raend; void * vm_private_data; /* was vm_pte (shared mem) */ }; |
vm_area_struct结构所描述的虚存空间以vm_start、vm_end成员表示,它们分别保存了该虚存空间的首地址和末地址后第一个字节的地址,以字节为单位,所以虚存空间范围可以用[vm_start, vm_end)表示。
通常,进程所使用到的虚存空间不连续,且各部分虚存空间的访问属性也可能不同。所以一个进程的虚存空间需要多个vm_area_struct结构来描述。在vm_area_struct结构的数目较少的时候,各个vm_area_struct按照升序排序,以单链表的形式组织数据(通过vm_next指针指向下一个vm_area_struct结构)。但是当vm_area_struct结构的数据较多的时候,仍然采用链表组织的化,势必会影响到它的搜索速度。针对这个问题,vm_area_struct还添加了vm_avl_hight(树高)、vm_avl_left(左子节点)、vm_avl_right(右子节点)三个成员来实现AVL树,以提高vm_area_struct的搜索速度。
假如该vm_area_struct描述的是一个文件映射的虚存空间,成员vm_file便指向被映射的文件的file结构,vm_pgoff是该虚存空间起始地址在vm_file文件里面的文件偏移,单位为物理页面。
一个程序可以选择MAP_SHARED或MAP_PRIVATE共享模式将一个文件的某部分数据映射到自己的虚存空间里面。这两种映射方式的区别在于:MAP_SHARED映射后在内存中对该虚存空间的数据进行修改会影响到其他以同样方式映射该部分数据的进程,并且该修改还会被写回文件里面去,也就是这些进程实际上是在共用这些数据。而MAP_PRIVATE映射后对该虚存空间的数据进行修改不会影响到其他进程,也不会被写入文件中。
来自不同进程,所有映射同一个文件的vm_area_struct结构都会根据其共享模式分别组织成两个链表。链表的链头分别是:vm_file->f_dentry->d_inode->i_mapping->i_mmap_shared, vm_file->f_dentry->d_inode->i_mapping->i_mmap。而vm_area_struct结构中的vm_next_share指向链表中的下一个节点;vm_pprev_share是一个指针的指针,它的值是链表中上一个节点(头节点)结构的vm_next_share(i_mmap_shared或i_mmap)的地址。
进程建立vm_area_struct结构后,只是说明进程可以访问这个虚存空间,但有可能还没有分配相应的物理页面并建立好页面映射。在这种情况下,若是进程执行中有指令需要访问该虚存空间中的内存,便会产生一次缺页异常。这时候,就需要通过vm_area_struct结构里面的vm_ops->nopage所指向的函数来将产生缺页异常的地址对应的文件数据读取出来。
vm_flags主要保存了进程对该虚存空间的访问权限,然后还有一些其他的属性。vm_page_prot是新映射的物理页面的页表项pgprot的默认值。
-------------------------------------------------------------------------------------------------------------------------
进程的虚拟空间
如前所述,每个进程拥有3G字节的用户虚存空间。但是,这并不意味着用户进程在这3G的范围内可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用。
那么,内核怎样管理每个进程3G的虚存空间呢?概括地说,用户进程经过编译、链接后形成的映象文件有一个代码段和数据段(包括data段和bss段),其中代码段在下,数据段在上。数据段中包括了所有静态分配的数据空间,即全局变量和所有申明为static的局部变量,这些空间是进程所必需的基本要求,这些空间是在建立一个进程的运行映像时就分配好的。除此之外,堆栈使用的空间也属于基本要求,所以也是在建立进程时就分配好的,如图2.6所示:

由图可以看出,堆栈空间安排在虚存空间的顶部,运行时由顶向下延伸;代码段和数据段则在低部,运行时并不向上延伸。从数据段的顶部到堆栈段地址的下沿这个区间是一个巨大的空洞,这就是进程在运行时可以动态分配的空间(也叫动态内存)。
进程在运行过程中,可能会通过系统调用mmap动态申请虚拟内存或释放已分配的内存,新分配的虚拟内存必须和进程已有的虚拟地址链接起来才能使用;Linux 进程可以使用共享的程序库代码或数据,这样,共享库的代码和数据也需要链接到进程已有的虚拟地址中。在后面我们还会看到,系统利用了请页机制来避免对物理内存的过分使用。因为进程可能会访问当前不在物理内存中的虚拟内存,这时,操作系统通过请页机制把数据从磁盘装入到物理内存。为此,系统需要修改进程的页表,以便标志虚拟页已经装入到物理内存中,同时,Linux 还需要知道进程虚拟空间中任何一个虚拟地址区间的来源和当前所在位置,以便能够装入物理内存。
由于上面这些原因,Linux 采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的 task_struct 结构中包含一个指向 mm_struct 结构的指针。进程的 mm_struct 则包含装入的可执行映象信息以及进程的页目录指针pgd。该结构还包含有指向 vm_area_struct 结构的几个指针,每个 vm_area_struct 代表进程的一个虚拟地址区间。
图6.17 进程虚拟地址示意图
图 6.17是某个进程的虚拟内存简化布局以及相应的几个数据结构之间的关系。从图中可以看出,系统以虚拟内存地址的降序排列 vm_area_struct。在进程的运行过程中,Linux 要经常为进程分配虚拟地址区间,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,vm_area_struct 结构的访问时间就成了性能的关键因素。为此,除链表结构外,Linux 还利用 红黑(Red_black)树来组织 vm_area_struct。通过这种树结构,Linux 可以快速定位某个虚拟内存地址。
当进程利用系统调用动态分配内存时,Linux 首先分配一个 vm_area_struct 结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区间时,因为 Linux 尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生缺页异常(请看请页机制),当 Linux 处理这一缺页异常时,就可以为新的虚拟内存区分配实际的物理内存。
在内核中,经常会用到这样的操作:给定一个属于某个进程的虚拟地址,要求找到其所属的区间以及vma_area_struct结构,这是由find_vma()来实现的,其实现代码在mm/mmap.c中:
* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
rb_node_t * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
这个函数比较简单,我们对其主要点给予解释:
· 参数的含义:函数有两个参数,一个是指向mm_struct结构的指针,这表示一个进程的虚拟地址空间;一个是地址,表示该进程虚拟地址空间中的一个地址。
· 条件检查:首先检查这个地址是否恰好落在上一次(最近一次)所访问的区间中。根据代码作者的注释,命中率一般达到35%,这也是mm_struct结构中设置mmap_cache指针的原因。如果没有命中,那就要在红黑树中进行搜索,红黑树与AVL树类似。
· 查找节点:如果已经建立了红黑树结构(rb_rode不为空),就在红黑树中搜索。
· 如果找到指定地址所在的区间,就把mmap_cache指针设置成指向所找到的vm_area_struct结构。
· 如果没有找到,说明该地址所在的区间还没有建立,此时,就得建立一个新的虚拟区间,再调用insert_vm_struct()函数将新建立的区间插入到vm_struct中的线性队列或红黑树中。