内核-mmap

mmap

应用程序和驱动程序之间传递数据时,可以通过 readwrite 函数进行。这涉及在用户态 buffer和内核态 buffer 之间传数据,如下图所示:
在这里插入图片描述
应用程序不能直接读写驱动程序中的buffer,需要在用户态 buffer 和内核态 buffer 之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。

比如更新 LCD 显示时,如果每次都让 APP传递一帧数据给内核,假设 LCD 采用102460032bpp 的格式,一帧数据就有102460032/8=2.3MB 左右,这无法忍受。

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

内存映射现象与数据结构

现在有一个测试程序:test.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int a;
int main(int argc, char **argv)
{
	if (argc != 2)
	{
		printf("Usage: %s <number>\n", argv[0]);
		return -1;
		}	
	a = strtol(argv[1], NULL, 0);
	printf("a's address = 0x%lx, a's value = %d\n", &a, a);
	while (1)
	{
		sleep(10);
	}
	return 0;
}

PC 上如下编译(必须静态编译):

gcc -o test test.c -staitc

分别执行 test 程序 2 次,最后执行 ps,可以看到这 2 个程序同时存在,这 2个程序里 a 变量的地址相同,但是值不同。如下图
在这里插入图片描述
观察到这些现象:

  • 2 个程序同时运行,它们的变量 a 的地址都是一样的: 0x6bc3a0;
  • 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 负责把虚拟地址映射为物理地址,虚拟地址映射到哪个物理地址去?

可以执行 ps 命令查看进程 ID,然后执行“ cat /proc/ID/maps”得到映射关系。(进程使用的虚拟地址空间)

在这里插入图片描述
这里是虚拟地址:物理地址 = 1:n,当然也可以,虚拟地址:物理地址 = n:1。合起来就是mmap
在这里插入图片描述

  1. get vaddr(应用程序获得一个应用程序可以使用的虚拟地址);
  2. get 物理地址paddr(获得这块内存的物理地址,需要做);
  3. 映射map(虚拟地址和物理地址映射)。

每一个APP在内核里都有一个tast_struct,这个结构体中保存有内存信息:mm_struct。而虚拟地址、物理地址的映射关系保存在页目录表中,如下图所示:
在这里插入图片描述

解析如下:

  • 每个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 中。

ARM 架构内存映射简介

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

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

一级页表项里的内容,决定了它是指向一块物理内存,还是指向二级页表,如下图:
在这里插入图片描述

一级页表映射过程

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

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

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

依次类推。

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

以下图为例介绍地址映射过程:
在这里插入图片描述

  • CPU 发出虚拟地址 vaddr,假设为 0x12345678
  • MMU 根据 vaddr[31:20]找到一级页表项:

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

段内偏移是 0x45678。

  • 从这个表项里取出物理基地址: Section Base Address,假设是0x81000000
  • 物理基地址加上段内偏移得到: 0x81045678

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

二级页表映射过程

首先设置好一级页表、二级页表,并且把一级页表的首地址告诉 MMU。以下图为例介绍地址映射过程:

  • CPU 发出虚拟地址 vaddr,假设为 0x12345678
  • MMU 根据 vaddr[31:20]找到一级页表项:虚拟地址 0x12345678 是虚拟地址空间里第 0x123 个 1M,所以找到页表里第 0x123 项。根据此项内容知道它是一个二级页表项。
  • 从这个表项里取出地址,假设是 address,这表示的是二级页表项的物理地址;
  • vaddr[19:12]表示的是二级页表项中的索引 index 即 0x45,在二级页表项中找到第 0x45 项;
  • 二级页表项格式如下
    在这里插入图片描述
    里面含有这 4K 或 1K 物理空间的基地址 page base addr,假设是0x81889000:它跟 vaddr[11:0]组合得到物理地址: 0x81889000 + 0x678 =0x81889678。

所以 CPU 要访问虚拟地址 0x12345678 时,实际上访问的是 0x81889678的物理地址
在这里插入图片描述

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

mmap调用过程

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

  1. 得到一个vm_area_struct,它表示 APP 的一块虚拟内存空间;很幸运 , APP调用mmap系统 函数时,内核就帮我们构造了一个vm_area_stuct 结构体。里面含有虚拟地址的地址范围、权限。
  2. 确定物理地址:你想映射某个内核 buffer,你需要得到它的物理地址,这得由你提供。
  3. 给 vm_area_struct 和物理地址建立映射关系:也很幸运,内核提供有相关函数。APP 里调用 mmap 时,导致的内核相关函数调用过程如下

addr是期望使用的虚拟地址。
在这里插入图片描述

cache 和 buffer

使用 mmap 时,需要有cache、 buffer的知识。下图是CPU 和内存之间的关系,有 cache、 buffer(写缓冲器)。 Cache 是一块高速内存;写缓冲器相当于一个 FIFO,可以把多个写操作集合起来一次写入内存。
在这里插入图片描述
程序运行时有“局部性原理”,这又分为时间局部性、空间局部性。

  • 时间局部性:
    在某个时间点访问了存储器的特定位置,很可能在一小段时间里,会反复地访问这个位置。
  • 空间局部性:
    访问了存储器的特定位置,很可能在不久的将来访问它附近的位置。

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

读取内存 addr 处的数据时:

  1. 先看看 cache 中有没有 addr 的数据,如果有就直接从 cache 里返回数据:这被称为 cache 命中。
  2. 如果 cache 中没有 addr 的数据,则从内存里把数据读入, 注意:它不是仅仅读入一个数据,而是读入一行数据(cache line)。
  3. 而 CPU 很可能会再次用到这个 addr 的数据,或是会用到它附近的数据,这时就可以快速地从 cache 中获得数据

写数据

  1. CPU 要写数据时,可以直接写内存,这很慢;也可以先把数据写入cache,这很快。
  2. 但是 cache 中的数据终究是要写入内存的啊,这有 2 种写策略:

写通(write through):

  • 数据要同时写入 cache 和内存,所以 cache 和内存中的数据保持一致,但是它的效率很低。能改进吗?可以!使用“写缓冲器”: cache大哥,你把数据给我就可以了,我来慢慢写,保证帮你写完。
  • 有些写缓冲器有“写合并”的功能,比如 CPU 执行了 4 条写指令:写第 0、 1、 2、 3 个字节,每次写 1 字节;写缓冲器会把这 4 个写操作合并成一个写操作:写 word。对于内存来说,这没什么差别,但是对于硬件寄存器,这就有可能导致问题。
  • 所以对于寄存器操作,不会启动 buffer 功能;对于内存操作,比如LCD 的显存,可以启用 buffer 功能。

写回(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):

#define L_PTE_MT_UNCACHED	(_AT(pteval_t, 0x00) << 2)	/* 0000 */
#define L_PTE_MT_BUFFERABLE	(_AT(pteval_t, 0x01) << 2)	/* 0001 */
#define L_PTE_MT_WRITETHROUGH	(_AT(pteval_t, 0x02) << 2)	/* 0010 */
#define L_PTE_MT_WRITEBACK	(_AT(pteval_t, 0x03) << 2)	/* 0011 */

上面 4 种组合对应下表中的各项,一一对应(下表来自 s3c2410 芯片手册,高架构的 cache、 buffer 更复杂,但是这些基础知识没变):

是否启用 cache是否启用 buffer说明
00Non-cached, non-buffered (NCNB)读、写都直达外设硬件
01Non-cached buffered (NCB) 读、写都直达外设硬件;
写操作通过 buffer 实现, CPU 不等待写操作完成, CPU 会马上执行下一条指令
10Cached, write-through mode (WT),写通;读: cache hit 时从 cahce 读数据; cache miss 时已入一行数据到 cache;写:通过 buffer 实现, CPU 不等待写操作完成, CPU 会马上执行下一条指令
11Cached, write-back mode (WB),写回读: cache hit 时从 cahce 读数据; cache miss 时已入一行数据到 cache;写:通过 buffer 实现, cache hit 时新数据不会到达硬件,而是在 cahce 中被标为“脏”; cache miss 时,通过buffer 写入硬件, CPU 不等待写操作完成, CPU 会马上执行下一条指令

第 1 种是不使用 cache 也不使用 buffer,读写时都直达硬件,这适合寄存器的读写。
第 2 种是不使用 cache 但是使用 buffer,写数据时会用 buffer 进行优化,可能会有“写合并”,这适合显存的操作。因为对显存很少有读操作,基本都是写操作,而写操作即使被“合并”也没有关系。
第 3 种是使用 cache 不使用 buffer,就是“write through”,适用于只读设备:在读数据时用 cache 加速,基本不需要写。
第 4 种是既使用 cache 又使用 buffer,适合一般的内存读写。

驱动程序要做的事

  1. 确定物理地址
  2. 确定属性:是否使用 cache、 buffer
  3. 建立映射关系

参考 Linux 源文件,示例代码如下:
在这里插入图片描述
还有一个更简单的函数:
在这里插入图片描述

程序

在这里插入图片描述
应用层有open函数、mmap函数、write函数、read函数,驱动层也有对应的函数;在入口函数分配8k内存,应用程序open之后,mmap这8k内存,通过mmap得到的地址write写数据,再read读数据

APP 怎么写? open 驱动、 buf=mmap(……)映射内存,直接读写 buf 就可以了,代码如下:

/*************************************************************************
 > File Name: hello_test.c
 > Author: Winter
 > Created Time: Sun 07 Jul 2024 01:39:39 AM EDT
 ************************************************************************/
 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
 
/*
 * ./hello_drv_test
 */
int main(int argc, char **argv)
{
	int fd;
	char* buf;
	int len;
	char str[1024];
	
	/* 1. 打开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}
 
	// 2mmap可读可写共享偏移地址是0
	buf = mmap(NULL, 1024 * 8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (buf == MAP_FAILED)
	{
		printf("can not mmap file /dev/hello\n");
		return -1;
	}
	printf("mmap address = 0x%x\n", buf);
 
	// 3写数据
	strcpy(buf, "winter");
 
 
	// 4读数据并且比较
	read(fd, str, 1024);
	if(strcmp(buf, str) == 0)
	{
		printf("compare ok\n");
	}
	else
	{
		printf("compare fail\n");
	}
 
	while (1)
	{
		// cat /proc/id/maps
		sleep(10);
	}
	
	// 5关闭
	munmap(buf, 1024 * 8);
	close(fd);
	
	return 0;
}
 

MAP_SHARED : 多个 APP 都调用 mmap 映射同一块内存时, 对内存的修改大家都可以看到。就是说多个 APP、驱动程序实际上访问的都是同一块内存。

MAP_PRIVATE : 创建一个 copy on write 的私有映射。当 APP 对该内存进行修改时,其他程序是看不到这些修改的。就是当 APP 写内存时, 内核会先创建一个拷贝给这个 APP,这个拷贝是这个 APP 私有的, 其他 APP、驱动无法访问。

最难理解的是 mmap 函数MAP_SHARED 、 MAP_PRIVATE 参 数 。 使用MAP_PRIVATE 映射时,在没有发生写操作时, APP、驱动访问的都是同一块内存;当 APP 发起写操作时,就会触发“ copy on write”,即内核会先创建该内存块的拷贝, APP 的写操作在这个新内存块上进行,这个新内存块是 APP 私有的,别的 APP、驱动看不到。

仅用 MAP_SHARED 参数时,多个 APP、驱动读、写时,操作的都是同一个内存块,“共享”。

MAP_PRIVATE 映射是很有用的, Linux 中多个 APP 都会使用同一个动态库,在没有写操作之前大家都使用内存中唯一一份代码。当 APP1 发起写操作时,内核会为它复制一份代码,再执行写操作, APP1 就有了专享的、私有的动态库,在里面做的修改只会影响到 APP1。其他程序仍然共享原先的、未修改的代码。

驱动编程

驱动程序要做什么?
分配一块 8K 的内存: 使用哪一个函数分配内存?

函数名说明
kmalloc分配到的内存物理地址是连续的
kzalloc分配到的内存物理地址是连续的,内容清 0
vmalloc分配到的内存物理地址不保证是连续的
vzalloc分配到的内存物理地址不保证是连续的,内容清 0

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

提供 mmap 函数

关键在于 mmap 函数,代码如下:
在这里插入图片描述
要 注 意 的 是 , remap_pfn_range 中 , pfn 的 意 思 是 “ Page Frame Number”。在 Linux 中,整个物理地址空间可以分为第 0 页、第1 页、第 2 页,诸如此类,这就是 pfn。假设每页大小是 4K,那么给定物理地址 phy,它的pfn = phy / 4096 = phy >> 12。内核的 page一般是 4K,但是也可以配置内核修改 page 的大小。所以为了通用, pfn = phy >> PAGE_SHIFT。

APP 调用 mmap 后,会导致驱动程序的 mmap 函数被调用,最终 APP 的虚拟地址和驱动程序中的物理地址就建立了映射关系。 APP 可以直接访问驱动程序的 buffer。

/*************************************************************************
 > File Name: hello.drv.c
 > Author: Winter
 > Created Time: Sun 07 Jul 2024 12:35:19 AM EDT
 ************************************************************************/
 
#include <linux/module.h>
 
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/pgtable.h>
#include <linux/mm.h>
#include <linux/slab.h>
 
 
 
 
 
// 1确定主设备号,也可以让内核分配
static int major = 0;				// 让内核分配
static char* kernel_buf;			// 保存应用程序的数据,kmalloc分配的数据
static struct class *hello_class;
static int bufSize = 1024 * 8;
 
#define MIN(a, b) (a < b ? a : b)
 
// 3 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 将kernel_buf区的数据拷贝到用户区数据buf中,即从内核kernel_buf中读数据
	err = copy_to_user(buf, kernel_buf, MIN(bufSize, size));
	return MIN(bufSize, size);
}
 
// mmap函数
int hello_drv_mmap (struct file* file, struct vm_area_struct* vma)
{
	// 1获得物理地址-虚拟地址转物理地址
	unsigned long phy = virt_to_phys(kernel_buf);
 
	// 2设置属性cache/buff
	// 不使用cache,使用buffer
	vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
 
	// 3映射mmap
	if (remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT, 
		vma->vm_end - vma->vm_start, vma->vm_page_prot))
	{
		printk("mmap remap_pfn_range failed\n");
		return -ENOBUFS;
	}
	return 0;
}
 
 
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 把用户区的数据buf拷贝到内核区kernel_buf,即向写到内核kernel_buf中写数据
	err = copy_from_user(kernel_buf, buf, MIN(bufSize, size));
	return MIN(1024, size);
}
 
static int hello_drv_open (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}
 
static int hello_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}
 
 
// 2定义自己的 file_operations 结构体
static struct file_operations hello_drv = {
	.owner = THIS_MODULE,
	.open = hello_drv_open,
	.read = hello_drv_read,
	.write = hello_drv_write,
	.mmap = hello_drv_mmap,
	.release = hello_drv_close,
};
 
 
// 4把 file_operations 结构体告诉内核: register_chrdev
// 5谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
	int err;
	// 分配内存
	kernel_buf = kmalloc(bufSize, GFP_KERNEL);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	// 注册hello_drv,返回主设备号
	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */
	// 创建class
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "hello");
		return -1;
	}
	// 创建device
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return 0;
}
 
 
 
// 6有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	device_destroy(hello_class, MKDEV(major, 0));
	class_destroy(hello_class);
	// 卸载
	unregister_chrdev(major, "hello");
	// 释放内存
	kfree(kernel_buf);
}
 
 
// 7其他完善:提供设备信息,自动创建设备节点: class_create,device_create
module_init(hello_init);
module_exit(hello_exit);
 
MODULE_LICENSE("GPL");
 

编译

在这里插入图片描述

测试

安装驱动,强制安装

insmod -f hello_drv.ko

在这里插入图片描述

执行测试程序

在这里插入图片描述
这里主要private、shared参数
在这里插入图片描述
使用MAP_PRIVATE 映射时,在没有发生写操作时, APP、驱动访问的都是同一块内存;当 APP 发起写操作时,就会触发“ copy onwrite”,即内核会先创建该内存块的拷贝, APP 的写操作在这个新内存块上进行,这个新内存块是 APP 私有的,别的 APP、驱动看不到。

仅用 MAP_SHARED 参数时,多个 APP、驱动读、写时,操作的都是同一个内存块,“共享”。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值