1、mmap的引入
我们都知道,应用程序是不能直接访问驱动程序的。
对于数据量较小的数据交互,我们可以使用copy_to_usr()(read())和copy_from_usr()(write())来进行用户态buffer和内核态buffer之间的拷贝。
但是,对于数据量较大的情况,还用这种方式进行buffer的拷贝,效率低下。为了解决这个问题,我们使用mmap()。将内核态的buffer映射到用户态,让APP直接在用户态进行读写。
2、什么叫内存映射
问题的引入:
当我们同时运行两个程序:
我们可以看出,程序中的变量a的地址是一样的,而a的值是不一样的。
这样,就引出了一个问题:
两个程序中,a的地址一定是不一样的(因为a的值不一样)。但是,为什么会显示他们的地址是一样的呢?
这就引出了虚拟地址的概念。
程序运行时,CPU发出的地址是虚拟地址,他会经过内存管理单元MMU映射到物理地址上。具体的映射关系如下:
扩展:
1)、在CPU里面,有一个很重要的单元名叫MMU,内存管理单元。
关于内存管理单元,有如下介绍:
2)、进程控制块:
PCB进程控制块(也叫做进程描述符)(位于内核空间):本质就是结构体(上百行)。
注:每个程序都是一个进程,每个进程都有一个进程控制块。进程控制块位于虚拟内存(3G-4G(对于 32位系统来说))
里面包含很多的内容,重点掌握以下部分:
(1)、存放进程ID。系统的每一个进程有唯一的id。在c语言中,用pid_t类型表示。其实就是一个非负整数。ps aux(查看进程id) ps ajx(查看进程id,父进程id…)
(2)、存放进程的状态:有就绪(要是说五种,就是“初始化+就绪”)、运行、挂起(暂停)(主动放弃CPU的使用权)—sleep()、停止等状态。
(3)、进行进程切换的时候,需要保存和恢复的一些CPU寄存器。(进程切换的时候,保存已经执行好的内容,等到下次CPU 过来执行的时候,可以接着进行(分时复用))
(4)、存放用来描述虚拟地址空间的信息。
虚拟内存通过MMU映射到物理内存。这个映射的活,是MMU干。而这个映射规则(eg:虚拟地址8000对应的物理地址是多少),是在PCB进程控制块里面存储。
(5)、存储当前工作目录:
cd … 操作,就是改变当前工作目录。通过ls,把当前工作目录下的文件显示出来。这都得益于这个功能,因为你要知道当前文件所在的目录。
(6)、存放文件描述符表,里面包含了很多指向file的结构体指针。
(7)、存放会话和进程组
(8)、存放进程可以使用的资源上限。
(9)、和信号相关的信息
(10)、用户id和组id。
3)、
内核在创建进程的时候,在创建task_struct(PCB进程控制块)的同时,会为进程创建相应的堆栈。一个内核栈,一个用户栈。内核栈和PCB进程控制块都位于内核空间,实际上是两个连续的物理页面,底部用作task_struct结构体,结构体上面的用作堆栈。
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,可以把多个写操作集合起来一次写入内存。
扩展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(实质就是指定这些宏中的一个)
③ 建立映射关系
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的地址。每一个页表项指向一个页框,页框就是真正的物理内存页。