1、mmap的引入

我们都知道,应用程序是不能直接访问驱动程序的。
对于数据量较小的数据交互,我们可以使用copy\_to\_usr()(read())和copy\_from\_usr()(write())来进行用户态buffer和内核态buffer之间的拷贝。
但是,对于数据量较大的情况,还用这种方式进行buffer的拷贝,效率低下。为了解决这个问题,我们使用mmap()。将内核态的buffer映射到用户态,让APP直接在用户态进行读写。

2、什么叫内存映射

问题的引入:
当我们同时运行两个程序:
mmap(内存映射)教程
我们可以看出,程序中的变量a的地址是一样的,而a的值是不一样的。
这样,就引出了一个问题:
两个程序中,a的地址一定是不一样的(因为a的值不一样)。但是,为什么会显示他们的地址是一样的呢?
这就引出了虚拟地址的概念。
mmap(内存映射)教程

扩展:

1)、在CPU里面,有一个很重要的单元名叫MMU,内存管理单元。
mmap(内存映射)教程
(2)、存放进程的状态:有就绪(要是说五种,就是“初始化+就绪”)、运行、挂起(暂停)(主动放弃CPU的使用权)—sleep()、停止等状态。

mmap(内存映射)教程
mmap(内存映射)教程


3、应用程序访问内核中的内存

首先,我们来总结一下,应用程序要想访问内核中的内存,需要做三件事情:
1)、获得虚拟地址-内核已经帮我们做好
2)、获得物理低地址—》内核并不知道我们要使用哪块地址,所以需要我们自己书写,在驱动程序中。
3)、映射。APP通过告诉MMU,他想访问的虚拟地址,MMU会将这个地址映射到指定的物理地址。----》内核已经提供了相应的函数。
下面,我们将分别论述这三步骤。
3.1、获得虚拟地址
应用程序使用mmap()函数(得到一个 vm\_area\_struct,它表示 APP 的一块虚拟内存空间)。
1)、应用程序调用mmap()函数
2)、内核就会分配出一块可以使用的虚拟地址。
3)、分配新的vm\_area\_struct结构体
4)、设置vm\_area\_struct结构体(APP 调用 mmap 系统函数时,内核就帮我们构造了一个 vm\_area\_stuct 结构体。里面含有虚拟地址的地址范围、权限)。

扩展1:

使用 mmap 时,需要有 cache、 buffer 的知识。
下图是 CPU 和内存之间的关系,有 cache、 buffer(写缓
冲器)。 Cache 是一块高速内存;写缓冲器相当于一个 FIFO,可以把多个写操作集合起来一次写入内存。
mmap(内存映射)教程

扩展2:

程序运行的时候,有“局部性原理”。这又分为“时间局部性”和“空间局部性”。
时间局部性:在某个时间点,访问了存储器的指定位置。很可能在未来一下段时间里,会反复访问这个位置。eg:

for(i=0;i<100;i++){
}

空间局部性:访问了存储器的特定位置,很可能在不久的将来,访问他附近的位置。
使用了i,会有很大的概率使用a。

for(i=0;i<100;i++){
     a++;
}

这两个“局部性原理”的应用:
而 CPU 的速度非常快,内存的速度相对来说很慢。 CPU 要读写比较慢的内存时,怎样可以加快速度?根据“局部性原理”,可以引入 cache。
①、读取内存addr处的数据
CPU会先看,cache中有没有addr的数据。如果有,就直接从cache中返回数据。这种被称为cache命中。
如果cache中没有addr的数据,CPU就会从内存中,将数据读入。
注意:
他不仅仅是读一个数据,而是读一行数据(因为CPU很可能会再次用到这个数据或者是他附近的数据,这时,就可以快速的从cache中,获得数据)。
②、向内存写数据
CPU要写数据的时候,可以直接向内存写入数据,这很慢!!!也可以先把数据写入cache,这很快。但是!!!!,数据终归要写入内存。
这有两种写策略:
a、(写通)在内存和CPU之间,使用“写缓冲器(buffer)”。写缓冲器具有“写合并”功能—》将多条写指令合并成一个写操作。CPU同时将数据写入cache和“写缓冲器”,写缓冲器再将写指令合并成一个写操作写入内存中。
此方法存在的问题:
利用“写缓冲器”,对于内存来说,可以提高效率。但是,对于寄存器操作来说,不能允许!!!!因为,eg:控制灯的亮灭,如果不能直接写给寄存器,而是在“写缓冲器”中,等待其他写指令的到来,执行合并操作,将会导致硬件不执行相应的操作。这是不能允许的。
所以:对于寄存器操作,不启动是buffer(写缓冲器)功能;对于内存操作,比如LCD,可以启动buffer操作。
注:对于arm架构,对于寄存器的访问和对内存的访问,是一样的,都是直接通过读写某个地址来实现。
b(写回)、新数据只是写入 cache,不会立刻写入内存, cache 和内存中的数据并不一致。
新数据写入 cache 时,这一行 cache 被标为“脏” (dirty);当 cache 不够用时,才需要把脏的数据写入内存。
此方法存在的问题:
注意 cache 和内存中的数据很可能不一致。这在很多时间要小心处理:比如 CPU 产生了新数据, DMA 把数据从内存搬到网卡,这时候就要 CPU 执行命令先把新数据从cache 刷到内存。反过来也是一样的, DMA 从网卡得过了新数据存在内存里, CPU 读数据之前先把 cache 中的数据丢弃。
在这里插入图片描述

3.2 获得物理地址

每一个 APP 在内核里都有一个 tast\_struct,这个结构体中保存有内存信息: mm\_struct。而虚拟地址、物理地址的映射关系保存在页目录表(PGD)中,如下图所示 :
在这里插入图片描述
① 每个 APP 在内核中都有一个 task\_struct 结构体,它用来描述一个进程;
② 每个 APP 都要占据内存,在 task\_struct 中用 mm\_struct 来管理进程占用的内存;
内存有虚拟地址、物理地址, mm\_struct 中用 mmap 来描述虚拟地址(用链表连接一个个结构体。每一个结构体,描述一段(代码段、数据段、 BSS 段、栈,共享库段…)的起始地址和结束地址);用 PGD(存放虚拟地址和内存地址之间的映射关系)来描述对应的物理地址。
注意: PGD, Page Global Directory,页目录。
③ 每个 APP 都有一系列的 VMA: virtual memory
比如 APP 含有代码段、数据段、 BSS 段、栈等等,还有共享库。这些单元会保存在内存里,它们的地址空间不同,权限不同(代码段是只读的可运行的、数据段可读可写),内核用一系列的
vm\_area\_struct 来描述它们。
vm\_area\_struct 中的 vm\_start、 vm\_end 是虚拟地址的始末地址。

3.3 虚拟地址和物理地址之间的映射

3.3.1页表

想要将虚拟地址映射到物理地址,首先要得到虚拟地址。虚拟地址的信息,就存放在mm\_struct 的mmap中。然后通过mm\_struct 中PGD(页表)成员中的信息进行映射。
IMX6系列的开发板,支持二级页表映射。为了了解二级页表映射,我们首先了解一下一级页表映射。
对于一级页表映射,映射的最小单位是1M(一级页表中,每个页表项用来设置1M空间)。对于32位系统,虚拟地址是2^32=4G,因此,一级页表要映射整个4G空间的话,需要4096个页表项。

3.3.2一级页表的映射流程

注:使用一级页表时,首先在内存中,设置好各个页表项。然后把页表的基地址(这样,就可以访问页表中的每一个页表项了)告诉MMU,就可以启动MMU了。
1)、CPU发出虚拟地址,假设虚拟地址是0X12345678.
2)、对于一级页表,MMU通过虚拟地址的高12位(123)来找到这个虚拟地址的映射的信息在第123个页表项中。段内的偏移就是0x45678.
3)、从这个页表项中,可得到物理基地址(这1M空间的起始地址)。假设:0x81000000
4)、则实际CPU访问的物理地址时:0x81000000+0x45678=0x81045678.
因此,当CPU要访问虚拟地址0X12345678的时候,实际上访问的物理地址是:0x81045678.

3.3.3二级页表的映射流程

注:首先,设置好一级页表和二级页表,并将一级页表的基地址告诉MMU。
1)、CPU发出虚拟地址,假设虚拟地址是0X12345678.
2)、MMU根据虚拟地址,找到第123个1M(第123个页表项)。如果是二级页表的映射,虚拟地址中,12-19位,是二级页表项中的索引。
3)、在二级页表项中,包含这4K物理空间的基地址(1024/4=256个二级页表)。假设是:0x81111000.
他跟虚拟地址的0X12345678组合,得实际上,访问得物理地址是:0x81111678.

3.4总结

我们在驱动程序中,要做的事情有有 3 点:
① 确定物理地址

unsigned long phy = virt_to_phys(虚拟地址);

② 确定属性:是否使用 cache、 buffer(实质就是指定这些宏中的一个)
mmap(内存映射)教程
③ 建立映射关系

remap_pfn_range(vma,vma->vm_start,psy/4096(因为是按页映射),vma->vm_end-vma->vm_start(长度),上面的属性信息);

注:vm\_area\_struct *vma;

扩展:

每个进程的页表目录可分成两部分,第一部分为“用户空间”,用来映射其整个进程空间(0x0000 0000-0xBFFF FFFF)即3G字节的虚拟地址;第二部分为“系统空间”,用来映射(0xC000 0000-0xFFFF FFFF)1G字节的虚拟地址。可以看出Linux系统中每个进程的页面目录的第二部分是相同的,所以从进程的角度来看,每个进程有4G字节的虚拟空间,较低的3G字节是自己的用户空间,最高的1G字节则为与所有进程以及内核共享的系统空间。每个进程有它自己的PGD( Page Global Directory),它是一个物理页,并包含一个pgd\_t数组。
关键字:

 PTE:  页表项(page table entry)
 PGD(Page Global Directory)
 PUD(Page Upper Directory)
 PMD(Page Middle Directory)
 PT(Page Table)

PGD中包含若干PUD的地址,PUD中包含若干PMD的地址,PMD中又包含若干PT的地址。每一个页表项指向一个页框,页框就是真正的物理内存页。

标签: 内存, CPU, cache, 虚拟地址, 映射, 页表, mmap

相关文章推荐

添加新评论,含*的栏目为必填