内核态与用户态进行数据交互通常是这样一种模型:内核利用自身的特权通过特定的服务程序采集、接收和处理数据;接着,用户态程序和内核服务程序进行数据交互,或接收内核态的数据,或向内核态写入数据。通过传统的那些对文件操作的系统调用就可以完成这样的工作,但是我们有时候需要通过访问用户空间的内存来直接读取内核数据,因为这样可以免去数据在内核态与用户态之间拷贝所花费的时间。

本文基于以上背景,以Linux字符设备驱动为基础,通过内存映射将内核中的一部分虚拟内存直接映射到用户空间,使得用户在访问内存时等同于直接访问内核空间,从而直接获取内核空间的数据。

1.实现原理

不管进程是在用户空间访问数据还是在内核空间访问数据,它所面临的都是虚拟地址。由于Linux对分段机制进行了特殊处理,因此这里的虚拟地址就等同于线性地址。按照一开始我们提出的要求,进程通过访问用户虚拟地址A来达到直接访问内核虚拟地址B中所存储数据的目的,这里的地址A和B必然不相同。那么,如何通过不同的虚拟地址来访问相同的数据?我们可以将虚拟地址A和B都映射到同一块物理内存,就可以实现内核空间和用户空间之间的数据共享。

示意图如下:

虚拟内存和物理内存之间如何联系?当然是通过页表了。我们在内核空间提前分配好缓冲区,并且向该缓冲区写入数据,此时内核会自动将该缓冲区对应的内核虚拟地址与实际的某一快物理内存进行关联,并将它们的映射关系保存在内核页表中。当在内核空间分配内存时时,上述工作自动被完成,比如通过kmalloc()分配内存时。

一旦在内核空间中分配了内存,随之就确定了物理内存。现在我们需要做的是将用户虚拟地址与物理内存进行关联,也就是说我们要将这个映射关系写入进程的用户页表。整个关联的过程是内核缺页异常处理程序完成的,这个处理过程比较复杂。我们要在内核中实现的并不是缺页异常处理程序,因为内核已经实现的很完美,而只需完成其中的一小部门。具体如何实现下问会详细说明。

2.用户态程序的实现

在本文所描述的内存管理试验,用户态程序首先通过open()打开字符设备文件mapdrv,该系统调用执行成功时返回文件描述符;通过mmap()将该设备文件映射到当前进程的用户空间中,该系统调用执行成功时返回指向映射区域的指针;最后通过该指针打印数据。用户态程序的实现如下所示:

#define LEN (10 * 4096)

int main(void)

{

int fd, ret = 0;

char *vadr;

int i;

if ((fd = open("/dev/mapdrv_k", O_RDWR)) < 0) {

perror("open");

ret = -1;

goto fail;

}

vadr = mmap(0, LEN, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, fd, 0);

if (vadr == MAP_FAILED) {

perror("mmap");

ret = -1;

goto fail_close;

}

printf("%s\n", vadr);

if (-1 == munmap(vadr, LEN))

ret = -1;

fail_close:

close(fd);

fail:

exit(ret);

}

用户态程序的实现并不复杂,因为它的主要作用是对内核模块程序的测试。由于用户态程序是对特定的字符设备文件mapdrv进行操作,所以程序中所使用的系统调用将会调用file_operations结构中对应的钩子函数。比如mmap系统调用在执行时会调用mapdrv设备驱动中的mmap钩子函数,虽然两者同名,但是mmap钩子函数所实现的功能只是mmap系统调用执行过程中的一部门,该钩子函数是file_operations结构中的成员。两者的函数原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

int (*mmap) (struct file *, struct vm_area_struct *);

如何实例化open和mmap等钩子函数便是整个内核模块程序实现的关键。

3.字符设备驱动程序的实现

整个内核模块程序是以字符设备驱动为基础进行实现的。该程序模块加载函数与一般字符设备驱动程序完成的工作一致:

1.申请设备号;

2.为描述字符设备的数据结构分配空间,并进行初始化;

3.将该字符设备注册到内核中;

在本文所描述的实验中,模块加载函数除了完成上述功能,还要完成以下功能:

kmalloc_area = kmalloc(MAPLEN, GFP_KERNEL);

if (!kmalloc_area)

goto fail4;

for (page = virt_to_page(kmalloc_area);

page < virt_to_page(kmalloc_area + MAPLEN); page++) {

SetPageReserved(page);

}

首先,通过kmalloc()分配一块内存用于在内核空间保存数据;通过SetPageReserved()将缓存数据的页面常驻内存,防止被换出到磁盘;将一段字符串拷贝到这片内存中。完成初始化函数后,字符设备驱动中最重要的就是实现file_operations结构中的钩子函数。在本文所述的实验中,我们只需实现三个钩子函数。

static struct file_operations mapdrv_fops = {

.owner = THIS_MODULE,

.mmap = mapdrv_mmap,

.open = mapdrv_open,

.release = mapdrv_release,

};

int mapdrv_open(struct inode *inode, struct file *file)

{

struct mapdrv *md;

printk("device is opened..\n");

md = container_of(inode->i_cdev, struct mapdrv, mapdev);

atomic_inc(&md->usage);

return 0;

}

int mapdrv_release(struct inode *inode, struct file *file)

{

struct mapdrv* md;

printk("device is closed..\n");

md = container_of(inode->i_cdev, struct mapdrv, mapdev);

atomic_dec(&md->usage);

return 0;

}

可以看到,open和release钩子函数的实现十分简单,只是打印相应语句以及更新设备的引用计数。事实上,我们不实现这两个钩子函数对整个驱动程序也没有任何影响。因为他们分别在open和close系统调用执行的过程中被调用,而这两个系统调用已经完成了打开文件和关闭文件的所有工作。因此,open和release钩子函数只是打印一些日志信息,方便用户查看。因此,同名的钩子函数和系统调用并不是等价的关系。

4.通过fault()实现内存映射

mmap系统调用是将本地文件映射到进程的用户空间,如果执行成功,进程的地址空间中会新增一块虚拟内存区域(但并不是每次调用mmap()都会增加一个vma,因为可能会出现内存区域之间的合并)

mmap钩子函数的回调只是整个系统调用执行过程中的一部分,这个钩子函数完成的主要功能是将新增的vma中的操作集进行实例化。具体实现代码如下:

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 (offset & ~PAGE_MASK) {

printk("offset not aligned: %ld\n", offset);

return -ENXIO;

}

if (size > MAPLEN) {

printk("size too big\n");

return -ENXIO;

}

if ((vma->vm_flags & VM_WRITE) && !(vma->vm_flags & VM_SHARED)) {

printk("writeable mappings must be shared, rejecting\n");

return -EINVAL;

}

vma->vm_flags |= VM_LOCKED;

if (offset == 0) {

vma->vm_ops = &map_vm_ops;

map_vopen(vma);

} else {

printk("offset out of range\n");

return -ENXIO;

}

return 0;

}

首先,offset中保存映射的首页在文件中的偏移量,该偏移量必须是页大小的整数倍,否则将不能进行映射。这一点在实现上将offset与PAGE_MASK宏进行位运算即可判断。接着,判断映射区域的长度是否超出了本实验中预设的长度大小。

如果上述两个条件都合法,那么接下来将进行最为重要的操作,将vma中的操作集进行实例化。vma的操作集就是专门对所属vma进行操作的钩子函数集合,内核中通过vm_operations_struct结构对其描述:

struct vm_operations_struct {

void (*open)(struct vm_area_struct * area);

void (*close)(struct vm_area_struct * area);

int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

……

}

参考上述代码,具体的实例化操作就是定义该类型的变量map_vm_ops,实现所需钩子函数,并将该变量与vm中的操作集字段进行挂接。

我们这里用到的主要有以下三个钩子函数:

open:当指定的vma加入到一个地址空间时,该函数被调用。

close:当指定的vma从地址空间删除时,该函数被调用。

fault:当要访问的页不再物理内存时,该函数被缺页处理程序调用。

这三个钩子函数的实现代码如下:

static struct vm_operations_struct map_vm_ops = {

.open = map_vopen,

.close = map_vclose,

.fault = map_fault,

};

void map_vopen(struct vm_area_struct *vma)

{

printk("mapping vma is opened..\n");

}

void map_vclose(struct vm_area_struct *vma)

{

printk("mapping vma is closed..\n");

}

可以看到,vma的open和close两个钩子函数没有做什么具体工作,因为打开和关闭一个vma的工作全部由内核负责,但是在mmap钩子函数中我们必须显示的调用map_open()。

这里我们重点说明falut钩子函数的实现。当用户要访问vma中的页,而该页又不在内存时,将发生缺页异常,fault钩子函数会在整个缺页处理程序中被调用。整个过程大致如下:

1.找到缺页地址所在的vma。

2.如果有必要分配各级页表项。

3.如果页表项对应的物理页面不存在,则回调当前vma中的fault钩子函数,它返回物理页面描述符。

4.将物理页面地址填充到相应页表项中。

5.完毕。

可以看到,fault钩子函数所实现的主要功能就是返回所需的物理内存描述符。

根据本文第一部分所描述实现原理,我们通过kmalloc()分配一块虚拟内存,可以通过virt_to_page()获得该虚拟内存对应的物理页框描述符,最后将该物理页框描述符返回到缺页异常处理程序中。至于用户页表的更新,那是缺页异常处理程序负责的事情,我们不必理会。

int map_fault(struct vm_area_struct *vma, struct vm_fault *vmf)

{

struct page *page = virt_to_page(kmalloc_area);

get_page(page);

vmf->page = page;

printk("the requiring page is returned..\n");

return 0;

}

通过上述实现过程,我们就将用户虚拟地址A和内核虚拟地址B映射到了同一的物理内存上,从而实现进程访问用户地址时直接获得内核数据的功能。

本实验涉及的知识点比较多,比如字符设备驱动程序的基本模型,Linux内存管理等相关知识。感兴趣的童鞋可以参考:

2.Linux设备驱动程序,第十五章

linux内存映射原理,Linux内存管理实践-使用fault()实现内存映射相关推荐

  1. 11、Linux系统基础原理、进程管理工具、任务计划

    Linux进程及作业管理 ​ 内核的功用:进程管理.文件系统.网络功能.内存管理.驱动程序.安全功能 ​ Process: 运行中的程序的一个副本: ​ 存在生命周期 Linux内核存储进程信息的固定 ...

  2. linux efi 启动原理,Linux(RHEL6)启动过程详解

    Linux(RHEL6)启动过程详解 Linux(红帽RHEL6)启动过程详解: RHEL的一个重要和强大的方面是它是开源的,并且系统的启动过程是用户可配置的.用户可以自由的配置启动过程的许多方面,包 ...

  3. linux efi 启动原理,Linux系统启动过程

    了解Linux系统的启动过程有助于我们深入理解Linux系统运行原理,下面我们将介绍一些系统启动过程中一些重要的细节.在这里,我们将Linux系统启动过程分成7个步骤介绍,这个过程如下图所示. 1.启 ...

  4. 【Linux 内核 内存管理】内存映射原理 ① ( 物理地址空间 | 外围设备寄存器 | 外围设备寄存器的物理地址 映射到 虚拟地址空间 )

    文章目录 一.物理地址空间 二.外围设备寄存器 三.外围设备寄存器物理地址 映射到 虚拟地址空间 一.物理地址空间 " 物理地址空间 " 是 CPU 处理器 在 " 总线 ...

  5. linux 内存使用原理,linux中内存使用原理

    首先介绍一下linux中内存是如何使用的. 当有应用需要读写磁盘数据时,由系统把相关数据从磁盘读取到内存,如果物理内存不够,则把内存中的部分数据导入到磁盘,从而把磁盘的部分空间当作虚拟内存 来使用,也 ...

  6. linux 桥接stp原理,Linux操作系统网桥源码框架初步分析

    今天处理网桥的STP的问题遇到了麻烦,对这个东东理论的倒是看了不少,没有真真学习到它的源理,来看Linux的实现,手头没有资料,看了两个钟头,只把网桥的框架结构看完,所以想先贴出来,希望有研究这块的大 ...

  7. linux init进程原理,Linux 系统下 init 进程的前世今生

    原标题:Linux 系统下 init 进程的前世今生 Linux系统中的 init 进程 (pid=1) 是除了 idle 进程 (pid=0,也就是 init_task) 之外另一个比较特殊的进程, ...

  8. linux应用程序原理,LINUX原理及应用:第15章 XWindow及Genie应用程序

    <LINUX原理及应用:第15章 XWindow及Genie应用程序>由会员分享,可在线阅读,更多相关<LINUX原理及应用:第15章 XWindow及Genie应用程序(12页珍藏 ...

  9. linux for循环原理,linux for循环

    for循环是Linux shell 中最常用的结构.for 循环有三种结构:一种结构是列表for循环:第二种结构是不带列表for循环:第三种结构是类C风格的for循环. for var in list ...

最新文章

  1. 突袭HTML5之SVG 2D入门1 - SVG综述
  2. [云炬创业管理笔记]第二章成为创业者讨论3
  3. 《集体智慧编程》数学公式
  4. docker maven 打包jar_maven 打包 spring boot 生成docker 镜像
  5. 汇编语言:实验10 根据材料编程—3.数值显示,编程,将data段中的数据以十进制形式显示出来
  6. tde数据库加密_如何在TDE加密的数据库上配置SQL Server镜像
  7. Hibernate关联关系映射-----基于连接表的单向一对多映射配置
  8. OSPF综合实验(有点难哦!)
  9. linux将分区从目录上卸载,Linux CentOS 硬盘分区、格式化、挂载与卸载
  10. 看完这篇 你就能完全操作git 远程分支的增、删、改、查了
  11. Ansys模态计算结果图片批量导出命令流
  12. 电路自学2-储能元件(电容+电感+储能元件的串并联)
  13. Chrome 浏览器架构
  14. 梦幻手游最新服务器,《梦幻西游》手游服务器如何选择 新区还是老区
  15. 关于手机常见音频POP音产生的原因以及解决思路(一)——耳机插入与拔出
  16. 通过图轻松了解各种协议
  17. 开局一张图帮你充分理解哈希表(散列表)
  18. 每天5分钟,定投聊通透-学习笔记01
  19. qt 环境下mapx组件的鼠标跟踪
  20. ubuntu 开机引导文件说明(/etc/default/grub)

热门文章

  1. 计算机类年度考核表,涉密人员年度考核表(科研军工类).doc
  2. 如何实现丝滑般的数据库扩容
  3. 河南计算机二级报名的流程,2020年9月河南计算机等级报名程序是什么
  4. 微信公众号 H5 jssdk 分享卡片功能实现(亲测使用)
  5. 关于shaderLab中 tags(标签)
  6. Python股票交易决策 - 计算收益率并控制风险
  7. 【CloudXNS教您几招】如何让多ip域名配置游刃有余?(1)
  8. LeetCode算法题-Goat Latin Easy(Java实现)
  9. CVX文档(Release 2.2)(自翻中文)
  10. Bacterial seed endophyte shapes disease resistance in rice种子内生菌与水稻抗病性的关系