专栏首页韦东山嵌入式韦东山:Linux驱动程序基石之mmap
原创

韦东山:Linux驱动程序基石之mmap

应用程序和驱动程序之间传递数据时,可以通过read、write函数进行。这涉及在用户态buffer和内核态buffer之间传数据,如下图所示:

应用程序不能直接读写驱动程序中的buffer,需要在用户态buffer和内核态buffer之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。比如更新LCD显示时,如果每次都让APP传递一帧数据给内核,假设LCD采用1024*600*32bpp的格式,一帧数据就有1024*600*32/8=2.3MB左右,这无法忍受。

改进的方法就是让程序可以直接读写驱动程序中的buffer,这可以通过mmap实现(memory map),把内核的buffer映射到用户态,让APP在用户态直接读写。

1.内存映射现象与数据结构

假设有这样的程序,名为test.c:

#include <stdio.h>
#include <unistd.h>
int a;
int main(int argc, char **argv)
{
	printf("enter a's value: \n");
	scanf("%d", &a);
	printf("a's address = 0x%x, a's value = %d\n", &a, a);
	while (1)
	{
		sleep(10);
	}
	return 0;
}

在ubuntu上如下编译:

gcc  -o test  test.c -static

在2个终端中分别执行test程序,在第3个终端执行ps -a,可以看到这2个程序同时存在,如下图:

观察到这些现象:

① 2个程序同时运行,它们的变量a的地址都是一样的:0x6d73c0;

② 2个程序同时运行,它们的变量a的值是不一样的,一个是12,另一个是123。

疑问来了:

① 这2个程序同时在内存中运行,它们在内存中的地址肯定不同,比如变量a的地址肯定不同;

② 但是打印出来的变量a的地址却是一样的。

怎么回事?

这里要引入虚拟地址的概念:CPU发出的地址是虚拟地址,它经过MMU(Memory Manage Unit,内存管理单元)映射到物理地址上,对于不同进程的同一个虚拟地址,MMU会把它们映射到不同的物理地址。如下图:

当前运行的是app1时,MMU会把CPU发出的虚拟地址addr映射为物理地址paddr1,用paddr1去访问内存。

当前运行的是app2时,MMU会把CPU发出的虚拟地址addr映射为物理地址paddr2,用paddr2去访问内存。

MMU负责把虚拟地址映射为物理地址,虚拟地址映射到哪个物理地址去?映射关系保存在页表中:

解析如下:

① 每个APP在内核中都有一个task_struct结构体,它用来描述一个进程;

② 每个APP都要占据内存,在task_struct中用mm_struct来管理进程占用的内存;

内存在虚拟地址、物理地址,mm_struct中用mmap来描述虚拟地址,用pgd来描述对应的物理地址。

注意:pgd,Page Global Directory,页目录。

③ 每个APP都有一系列的VMA:virtual memory

比如APP含有代码段、数据段、BSS段、栈等等,还有共享库。这些单元会保存在内存里,它们的地址空间不同,权限不同(代码段是只读的可运行的、数据段可读可写),内核用一系列的vm_area_struct来描述它们。

vm_area_struct中的vm_start、vm_end是虚拟地址。

④ vm_area_struct中虚拟地址如何映射到物理地址去?

每一个APP的虚拟地址可能相同,物理地址不相同,这些对应关系保存在pgd中。

2.ARM架构内存映射简介

ARM架构支持一级页表映射,也就是说MMU根据CPU发来的虚拟地址可以找到第1个页表,从第1个页表里就可以知道这个虚拟地址对应的物理地址。一级页表里地址映射的最小单位是1M。

ARM架构还支持二级页表映射,也就是说MMU根据CPU发来的虚拟地址先找到第1个页表,从第1个页表里就可以知道第2级页表在哪里;再取出第2级页表,从第2个页表里才能确定这个虚拟地址对应的物理地址。二级页表地址旺射的最小单位有4K、1K,Linux使用4K。

一级页表项里的内容,决定了它是指向一块物理内存,还是指问二级页表,如下图:

2.1, 一级页表映射过程

一线页表中每一个表项用来设置1M的空间,对于32位的系统,虚拟地址空间有4G,4G/1M=4096。所以一级页表要映射整个4G空间的话,需要4096个页表项。

第0个页表项用来表示虚拟地址第0个1M(虚拟地址为0~0x1FFFFF)对应哪一块物理内存,并且有一些权限设置;

第1个页表项用来表示虚拟地址第1个1M(虚拟地址为0x100000~0x2FFFFF)对应哪一块物理内存,并且有一些权限设置;

依次类推。

使用一级页表时,先在内存里设置好各个页表项,然后把页表基地址告诉MMU,就可以加动MMU了。

以下图为例介绍地址映射过程:

① CPU发出虚拟地址vaddr,假设为0x12345678

② MMU根据vaddr[31:20]找到一级页表项:

虚拟地址0x12345678是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项,根据此项内容知道它是一个段页表项。

段内偏移是0x45678。

③ 从这个表项里取出物理基地址:Section Base Address,假设是0x81000000

④ 物理基地址加上段内偏移得到:0x81045678

所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81045678的物理地址

2.2, 二级页表映射过程

首先设置好一级页表、二级页表,并且把一级页表的首地址告诉MMU。

以下图为例介绍地址映射过程:

① CPU发出虚拟地址vaddr,假设为0x12345678

② MMU根据vaddr[31:20]找到一级页表项:

虚拟地址0x12345678是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项。根据此项内容知道它是一个二级页表项。

③ 从这个表项里取出地址,假设是address,这表示的是二级页表项的物理地址;

④ vaddr[19:12]表示的是二级页表项中的索引index即0x45,在二级页表项中找到第0x45项;

⑤ 二级页表项中含有页基地址page base addr,假设是0x81889000:

它跟vaddr[11:0]组合得到物理地址:0x81889000 + 0x678 = 0x81889678。

所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81889678的物理地址

3, 怎么给APP新建一块内存映射

3.1, mmap调用过程

从上面内存映射的过程可以知道,要给APP端新开劈一块虚拟内存,并且让它指向某块内核buffer,我们要做这些事:

① 得到一个vm_area_struct,它表示APP的一块虚拟内存空间;

很幸运,APP调用mmap系统函数时,内核就帮我们构造了一个vm_area_stuct结构体。里面含有虚拟地址的地址范围、权限。

② 确定物理地址:

你想映射某个内核buffer,你需要得到它的物理地址,这得由你提供。

③ 给vm_area_struct和物理地址建立映射关系:

也很幸运,内核提供有相关函数。

APP里调用mmap时,导致的内核相关函数调用过程如下:

3.2 cache和buffer

本小节参考:

ARM的cache和写缓冲器(write buffer)

ARM的cache和写缓冲器(write buffer)​blog.csdn.net

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

程序运行时有“局部性原理”,这又分为时间局部性、空间局部性。

① 时间局部性:

在某个时间点访问了存储器的特定位置,很可能在一小段时间里,会反复地访问这个位置。

② 空间局部性:

访问了存储器的特定位置,很可能在不久的将来访问它附近的位置。

而CPU的速度非常快,内存的速度相对来说很慢。CPU要读写比较慢的内存时,怎样可以加快速度?根据“局部性原理”,可以引入cache。

① 读取内存addr处的数据时:

先看看cache中有没有addr的数据,如果有就直接从cache里返回数据:这被称为cache命中。

如果cache中没有addr的数据,则从内存里把数据读入,注意:它不是仅仅读入一个数据,而是读入一行数据(cache line)。

而CPU很可能会再次用到这个addr的数据,或是会用到它附近的数据,这时就可以快速地从cache中获得数据。

② 写数据:

CPU要写数据时,可以直接写内存,这很慢;也可以先把数据写入cache,这很快。

但是cache中的数据终究是要写入内存的啊,这有2种写策略:

a. 写通(write through):

数据要同时写入cache和内存,所以cache和内存中的数据保持一致,但是它的效率很低。能改进吗?可以!使用“写缓冲器”:cache大哥,你把数据给我就可以了,我来慢慢写,保证帮你写完。

有些写缓冲器有“写合并”的功能,比如CPU执行了4条写指令:写第0、1、2、3个字节,每次写1字节;写缓冲器会把这4个写操作合并成一个写操作:写word。对于内存来说,这没什么差别,但是对于硬件寄存器,这就有可能导致问题。

所以对于寄存器操作,不会启动buffer功能;对于内存操作,比如LCD的显存,可以启用buffer功能。

b. 写回(write back):

新数据只是写入cache,不会立刻写入内存,cache和内存中的数据并不一致。

新数据写入cache时,这一行cache被标为“脏”(dirty);当cache不够用时,才需要把脏的数据写入内存。

使用写回功能,可以大幅提高效率。但是要注意cache和内存中的数据很可能不一致。这在很多时间要小心处理:比如CPU产生了新数据,DMA把数据从内存搬到网卡,这时候就要CPU执行命令先把新数据从cache刷到内存。反过来也是一样的,DMA从网卡得过了新数据存在内存里,CPU读数据之前先把cache中的数据丢弃。

是否使用cache、是否使用buffer,就有4种组合(Linux内核文件arch\arm\include\asm\pgtable-2level.h):

第1种是不使用cache也不使用buffer,读写时都直达硬件,这适合寄存器的读写。

第2种是不使用cache但是使用buffer,写数据时会用buffer进行优化,可能会有“写合并”,这适合显存的操作。因为对显存很少有读操作,基本都是写操作,而写操作即使被“合并”也没有关系。

第3种是使用cache不使用buffer,就是“write through”,适用于只读设备:在读数据时用cache加速,基本不需要写。

第4种是既使用cache又使用buffer,适合一般的内存读写。

3.3, 驱动程序要做的事

驱动程序要做的事情有3点:

① 确定物理地址

② 确定属性:是否使用cache、buffer

③ 建立映射关系

参考Linux源文件,示例代码如下:

还有一个更简单的函数:

9.4 驱动编程

我们在驱动程序中申请一个8K的buffer,让APP通过mmap能直接访问。

① 使用哪一个函数分配内存?

我们应该使用kmalloc或kzalloc,这样得到的内存物理地址是连续的,在mmap时后APP才可以使用同一个基地址去访问这块内存。(如果物理地址不连续,就要执行多次mmap了)。

关键代码现场编写,再完善文档。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 韦东山:6000字长文教你如何学习嵌入式开发

    我在100ASK_IMX6ULL售后群里,发现很多初学者只有单片机基础,甚至没有单片机基础。在学习Linux时,对很多概念比较陌生,导致不知道学什么,也不知道学...

    韦东山
  • 【韦东山】嵌入式全系统:单片机-linux-Android对硬件操作的不同侧重点

    我是韦东山,一直从事嵌入式Linux培训,最近打算连载一系列文章。 正在录制全新的嵌入式Linux视频,使用新路线,不再从裸机/uboot开始,效率更高。 对应...

    韦东山
  • Linux应用程序开发之CAN编程

    CAN,全称为“Controller Area Network”,即控制器局域网,是国际上应用最广泛的现场总线之一。

    韦东山
  • 腾讯云服务器租用与价格表预览

    腾讯云服务器租赁需要多少钱?腾讯云服务器租用价格是如何计算的呢?下面笔者介绍腾讯云服务器租用流程、价格、以及如何购买更加实惠!

    用户2416682
  • Roslyn 将这个文件放在你的项目文件夹,无论哪个控制台项目都会输出林德熙是逗比

    虽然已经通过很多篇博客告诉大家如何通过 Directory.Build.props 文件修改编译的方法,但是本文还是提供一个新的思路 只需要在项目文件夹,或者磁...

    林德熙
  • 怎么替换修改WordPress中的资源调用地址

    Twitter 官方提供了来自MaxCDN的 CDN 服务:http://twemoji.maxcdn.com/

    沈唁
  • c++之函数重载(函数匹配)

    Case void f(); void f(int); void f(int, int); void f(double, double = 3.14); 匹配原...

    互联网金融打杂
  • PostgreSQL 自己的 DB buffer & 与别的人的OS cache 之 回答问题

    最近周四的一篇文字中的分享引起了部分同学的注意,私信我,并问了一些问题,这里挑拣了 红旗迎东风(希望名字没有敲错)同学的问题。他的问题主要是围绕 POSTGRE...

    AustinDatabases
  • RabbitMQ技术详解

    RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、A...

    烂猪皮
  • 用C语言撸了个DBProxy 顶

    笔者在阅读了一大堆源码后,就会情不自禁产生造轮子的想法。于是花了数个周末的时间用C语言撸了一个DBProxy(MySQL协议)。在笔者的github中给这个DB...

    无毁的湖光-Al

扫码关注云+社区

领取腾讯云代金券