欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux内核mmap机制

程序员文章站 2022-05-05 10:27:49
...

1. 问:如何将物理地址映射到用户空间的虚拟地址上?

Linux内核mmap机制

2.linux内核mmap机制

2.1.回顾LED驱动数据流的操作过程

通过分析LED驱动,得出以下结论:
如果利用read,write,ioctl三个系统调用函数实现对LED硬件进行操作,这三个系统调用函数操作数据最终要经过两次数据拷贝,
分别是用户空间到内核空间,内核空间到硬件或者硬件到内核,内核到用户;

如果操作访问的数据量比较小,对系统性能的影响几乎可以忽略不计,如果数据量比较大,这种影响不能忽略,例如显卡,摄像头,声卡等;

明确:数据的最终走向要不是用户到硬件,或者硬件到用户;

2.2.linux对于这类设备,在数据访问的时候,为了提供性能,
利用mmap机制将硬件设备的物理地址直接映射到用户空间的虚拟地址上,
将来用户在用户空间访问这个虚拟地址就是在访问对应的物理地址,不再经过内核空间,
将两次数据拷贝转换成一次数据拷贝!

3.mmap实现机制

3.1本质目的:就是将设备物理地址(物理内存)映射到用户空间的虚拟地址(虚拟内存)上,
将来用户在用户空间访问映射的虚拟地址就是在访问物理地址;不再经过内核空间,将原先的两次数据拷贝(read,write,ioctl)转换成一 次

3.2.回忆linux的mmap应用编程:
void *addr;
int fd = open("a.txt", O_RDWR);
addr = mmap(0, 文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 文件偏移量);
memcpy(addr, "hello,world", 12);

参数说明:
0:让内核帮你在MMAP内存映射区找一块空闲的内存区域用来映射文件;
addr:内核将找到的空闲的虚拟内存区域的首地址告诉给用户,那么即可通过这个地址来访问硬件

说明:“文件”:太抽象,“文件”最终对应的就是一个硬件设备(内存,闪存,硬盘),对应的一个硬件设备的物理地址;
利用mmap将文件映射到用户3G虚拟内存的MMAP内存映射区,存映射区的虚拟地址上,
将来用户访问这个虚拟地址就是在访问硬件(文件);不再经过内核空间
Linux内核mmap机制
3.3.mmap系统调用过程:
1.当应用程序调用mmap,首先调用到C库的mmap函数
2.C库的mmap函数将会做以下事情:
2.1.保存mmap系统调用号到R7寄存器
2.2.调用svc触发软中断异常
3.跳转到内核准备的异常向量表软中断的处理入口
3.1.从R7寄存器取出mmap的系统调用号
3.2.在以系统调用号为索引,在系统调用表中找到对应的内核函数sys_mmap(由内核实现)
4.内核的sys_mmap将会做以下事情:
4.1.内核在用户3G虚拟内存的MMAP内存映射区找一块空闲的内存区域,将来映射某个硬件设备;
4.2.一旦找到,内核用struct vm_area_struct创建一个对象来描述这块空闲的内存区域;
struct vm_area_struct {
unsigned long vm_start; //空闲内存区域的首地址
unsigned long vm_end;//结束地址
pgprot_t vm_page_prot; //访问权限
unsigned long vm_pgoff; //偏移量
...
};

4.3.最后内核调用底层驱动的mmap函数,
并且内核将描述这块空闲内存区域的信息传递给底层驱动的mmap(对象的首地址传递给底层驱动的mmap),
就等价于内核将空闲内存区域的所有属性告知给底层驱动的mmap

此时此刻,物理地址和用户虚拟地址做好映射了吗?内核没有做映射,直接去调用底层驱动的mmap了

5.底层驱动的mmap函数
struct file_operations {
int (*mmap)(struct file *file, struct vm_area_struct *vma)
}

mmap接口:
功能:底层驱动的mmap永远只做一件事:将物理地址映射到用户虚拟地址上;
参数:
file:文件指针;
vma:此指针指向内核创建的描述内核找的空闲内存区域的对象,底层驱动可以通过此指针获取空闲内区域的属性(起始地址,结束地址...)
目的:
通过芯片手册和原理图能够获取设备的物理地址
通过vma指针能够获取用户虚拟内存区域的信息

问:如何将物理地址最终映射到用户虚拟地址上呢?
答:
明确:此事由底层驱动的mmap来做!

通过调用remap_pfn_range函数来进行;

int remap_pfn_range(struct vm_area_struct *vma,
unsigned long addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot)
函数功能:
1.将已知的物理地址映射到已知的用户虚拟地址上;
参数:
vma:指向内核创建的描述空闲内存区域的对象
addr:空闲内存区域的首地址;vm_start
pfn:将物理地址右移12位
size:空闲虚拟内存的大小,vm_end - vm_start
prot:访问权限,bm_prot
切记:映射时指针的虚拟地址和物理地址必须是页面大小的整数倍;
0xe0200060:配置寄存器物理地址
0xe0200064:数据寄存器物理地址
通过阅读芯片手册,发现GPIO所有寄存器的基地址:0xE0200000,并且寄存器存储空间都是连续的;
其他寄存器的地址只需基地址加一个对应的偏移量即可;所以用mmap做地址映射时,物理地址可以采用0xe0200000,对应的虚拟地址将来也要加上对应的偏移量

物理地址 用户虚拟地址
0xe0200000 A (vm_start)
0xe0200060 A + 0x60
0xe0200064 A + 0x64

#include <linux/mm.h>

切记:对于GPIO操作的设备,利用mmap访问操作的时候,记得要把cache功能屏蔽掉!

案例

利用mmap实现开关灯 

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

//vma:指向空闲虚拟内存区域
static int led_mmap(struct file *file,
                        struct vm_area_struct *vma)
{
    //对于GPIO操作的设备,禁止cache功能
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    
    //只做一件事:将物理地址映射到用户虚拟地址上
    remap_pfn_range(vma, vma->vm_start,
                    0xe0200000 >> 12,
                    vma->vm_end - vma->vm_start,
                        vma->vm_page_prot);
    return 0;
}

//定义初始化硬件操作方法
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .mmap = led_mmap
};
//定义初始化混杂设备对象
static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "myled",
    .fops = &led_fops
};
static int led_init(void)
{
    //注册混杂设备对象
    misc_register(&led_misc);
    return 0;
}

static void led_exit(void)
{
    //卸载混杂设备对象
    misc_deregister(&led_misc);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char *argv[])
{
    int fd;
    unsigned char *gpio_vir_base; //0xe0200000对应的用户虚拟地址
    unsigned long *gpiocon, *gpiodata;//配置和数据的用户虚拟地址
    
    if (argc != 2) {
        printf("Usage:\n%s <on|off>\n", argv[0]);
        return -1;
    }
    
    fd = open("/dev/myled", O_RDWR);
    if (fd < 0)
        return -1;
    
    //将0xe0200000物理地址,物理内存大小为0x1000映射到用户虚拟内存上
    gpio_vir_base = mmap(0, 0x1000, PROT_READ|PROT_WRITE,
                            MAP_SHARED, fd, 0);
    
    //获取0xe0200060,0xe0200064的用户虚拟地址
    gpiocon = (unsigned long *)(gpio_vir_base + 0x60);
    gpiodata = (unsigned long *)(gpio_vir_base + 0x64);
   
    //配置GPIO为输出口
    *gpiocon &= ~((0xf << 12) | (0xf << 16));
    *gpiocon |= ((1 << 12) | (1 << 16));
    
    if (!strcmp(argv[1], "on")) 
        *gpiodata |= ((1 << 3) | (1 << 4));
    else
        *gpiodata &= ~((1 << 3) | (1 << 4));

    //解除地址映射
    munmap(gpio_vir_base, 0x1000);
    close(fd);
    return 0;
}