Linux内核mmap内存映射详解及例子实现

news2024/11/15 1:52:20

mmap在linux哪里?

什么是mmap?

上图说了,mmap是操作这些设备的一种方法,所谓操作设备,比如IO端口(点亮一个LED)、LCD控制器、磁盘控制器,实际上就是往设备的物理地址读写数据。

但是,由于应用程序不能直接操作设备硬件地址,所以操作系统提供了这样的一种机制——内存映射,把设备地址映射到进程虚拟地址,mmap就是实现内存映射的接口。

操作设备还有很多方法,如ioctl、ioremap

mmap的好处是,mmap把设备内存映射到虚拟内存,则用户操作虚拟内存相当于直接操作设备了,省去了用户空间到内核空间的复制过程,相对IO操作来说,增加了数据的吞吐量。

什么是内存映射?

既然mmap是实现内存映射的接口,那么内存映射是什么呢?看下图

每个进程都有独立的进程地址空间,通过页表和MMU,可将虚拟地址转换为物理地址,每个进程都有独立的页表数据,这可解释为什么两个不同进程相同的虚拟地址,却对应不同的物理地址。

什么是虚拟地址空间?

每个进程都有4G的虚拟地址空间,其中3G用户空间,1G内核空间(linux),每个进程共享内核空间,独立的用户空间,下图形象地表达了这点

驱动程序运行在内核空间,所以驱动程序是面向所有进程的。

用户空间切换到内核空间有两种方法:

(1)系统调用,即软中断

(2)硬件中断

虚拟地址空间里面是什么?

了解了什么是虚拟地址空间,那么虚拟地址空间里面装的是什么?看下图

虚拟空间装的大概是上面那些数据了,内存映射大概就是把设备地址映射到上图的红色段了,暂且称其为“内存映射段”,至于映射到哪个地址,是由操作系统分配的,操作系统会把进程空间划分为三个部分:

(1)未分配的,即进程还未使用的地址

(2)缓存的,缓存在ram中的页

(3)未缓存的,没有缓存在ram中

操作系统会在未分配的地址空间分配一段虚拟地址,用来和设备地址建立映射,至于怎么建立映射,后面再揭晓。

现在大概明白了“内存映射”是什么了,那么内核是怎么管理这些地址空间的呢?任何复杂的理论最终也是通过各种数据结构体现出来的,而这里这个数据结构就是进程描述符。从内核看,进程是分配系统资源(CPU、内存)的载体,为了管理进程,内核必须对每个进程所做的事情进行清楚的描述,这就是进程描述符,内核用task_struct结构体来表示进程,并且维护一个该结构体链表来管理所有进程。该结构体包含一些进程状态、调度信息等上千个成员,我们这里主要关注进程描述符里面的内存描述符(struct mm_struct mm)

相关视频推荐

90分钟搞懂Linux内核MMU机制

Linux内核源码之地址映射机制

剖析Linux内核设备驱动程序

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

内存描述符

具体的结构,请参考下图

现在已经知道了内存映射是把设备地址映射到进程空间地址(注意:并不是所有内存映射都是映射到进程地址空间的,ioremap是映射到内核虚拟空间的,mmap是映射到进程虚拟地址的),实质上是分配了一个vm_area_struct结构体加入到进程的地址空间,也就是说,把设备地址映射到这个结构体,映射过程就是驱动程序要做的事了。

内存映射的实现

以字符设备驱动为例,一般对字符设备的操作都如下框图

而内存映射的主要任务就是实现内核空间中的mmap()函数,先来了解一下字符设备驱动程序的框架

以下是mmap_driver.c的源代码

//所有的模块代码都包含下面两个头文件  
#include <linux/module.h>  
#include <linux/init.h>  
  
#include <linux/types.h> //定义dev_t类型  
#include <linux/cdev.h> //定义struct cdev结构体及相关操作  
#include <linux/slab.h> //定义kmalloc接口  
#include <asm/io.h>//定义virt_to_phys接口  
#include <linux/mm.h>//remap_pfn_range  
#include <linux/fs.h>  
  
#define MAJOR_NUM 990  
#define MM_SIZE 4096  
  
static char driver_name[] = "mmap_driver1";//驱动模块名字  
static int dev_major = MAJOR_NUM;  
static int dev_minor = 0;  
char *buf = NULL;  
struct cdev *cdev = NULL;  
  
static int device_open(struct inode *inode, struct file *file)  
{  
    printk(KERN_ALERT"device open\n");  
    buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//内核申请内存只能按页申请,申请该内存以便后面把它当作虚拟设备  
    return 0;  
}  
  
static int device_close(struct inode *indoe, struct file *file)  
{  
    printk("device close\n");  
    if(buf)  
    {  
        kfree(buf);  
    }  
    return 0;  
}  
  
static int device_mmap(struct file *file, struct vm_area_struct *vma)  
{  
    vma->vm_flags |= VM_IO;//表示对设备IO空间的映射  
    vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出  
    if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里  
                       vma->vm_start,//虚拟空间的起始地址  
                       virt_to_phys(buf)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移12位  
                       vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍  
                       vma->vm_page_prot))//保护属性,  
    {  
        return -EAGAIN;  
    }  
    return 0;  
}  
  
static struct file_operations device_fops =  
{  
    .owner = THIS_MODULE,  
    .open  = device_open,  
    .release = device_close,  
    .mmap = device_mmap,  
};  
  
static int __init char_device_init( void )  
{  
    int result;  
    dev_t dev;//高12位表示主设备号,低20位表示次设备号  
    printk(KERN_ALERT"module init2323\n");  
    printk("dev=%d", dev);  
    dev = MKDEV(dev_major, dev_minor);  
    cdev = cdev_alloc();//为字符设备cdev分配空间  
    printk(KERN_ALERT"module init\n");  
    if(dev_major)  
    {  
        result = register_chrdev_region(dev, 1, driver_name);//静态分配设备号  
        printk("result = %d\n", result);  
    }  
    else  
    {  
        result = alloc_chrdev_region(&dev, 0, 1, driver_name);//动态分配设备号  
        dev_major = MAJOR(dev);  
    }  
      
    if(result < 0)  
    {  
        printk(KERN_WARNING"Cant't get major %d\n", dev_major);  
        return result;  
    }  
      
      
    cdev_init(cdev, &device_fops);//初始化字符设备cdev  
    cdev->ops = &device_fops;  
    cdev->owner = THIS_MODULE;  
      
    result = cdev_add(cdev, dev, 1);//向内核注册字符设备  
    printk("dffd = %d\n", result);  
    return 0;  
}  
  
static void __exit char_device_exit( void )  
{  
    printk(KERN_ALERT"module exit\n");  
    cdev_del(cdev);  
    unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);  
}  
  
module_init(char_device_init);//模块加载  
module_exit(char_device_exit);//模块退出  
  
MODULE_LICENSE("GPL");  
MODULE_AUTHOR("ChenShengfa");  

下面是测试代码test_mmap.c

#include <stdio.h>  
#include <fcntl.h>  
#include <sys/mman.h>  
#include <stdlib.h>  
#include <string.h>  
  
int main( void )  
{  
    int fd;  
    char *buffer;  
    char *mapBuf;  
    fd = open("/dev/mmap_driver", O_RDWR);//打开设备文件,内核就能获取设备文件的索引节点,填充inode结构  
    if(fd<0)  
    {  
        printf("open device is error,fd = %d\n",fd);  
        return -1;  
    }  
    /*测试一:查看内存映射段*/  
    printf("before mmap\n");  
    sleep(15);//睡眠15秒,查看映射前的内存图cat /proc/pid/maps  
    buffer = (char *)malloc(1024);  
    memset(buffer, 0, 1024);  
    mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//内存映射,会调用驱动的mmap函数  
    printf("after mmap\n");  
    sleep(15);//睡眠15秒,在命令行查看映射后的内存图,如果多出了映射段,说明映射成功  
      
    /*测试二:往映射段读写数据,看是否成功*/  
    strcpy(mapBuf, "Driver Test");//向映射段写数据  
    memset(buffer, 0, 1024);  
    strcpy(buffer, mapBuf);//从映射段读取数据  
    printf("buf = %s\n", buffer);//如果读取出来的数据和写入的数据一致,说明映射段的确成功了  
      
      
    munmap(mapBuf, 1024);//去除映射  
    free(buffer);  
    close(fd);//关闭文件,最终调用驱动的close  
    return 0;  
}  

下面是makefile文件

ifneq ($(KERNELRELEASE),)  
  
obj-m := mmap_driver.o  
  
else  
KDIR := /lib/modules/3.2.0-52-generic/build  
  
all:  
    make -C $(KDIR) M=$(PWD) modules  
clean:  
    rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.order  
  
endif

下面命令演示一下驱动程序的编译、安装、测试过程(注:其他用户在mknod之后还需要chmod改变权限)

# make    //编译驱动
 
# insmod mmap_driver.ko    //安装驱动
 
# mknod /dev/mmap_driver c 999 0    //创建设备文件
 
# gcc test_mmap.c -o test.o    //编译应用程序
 
# ./test.o    //运行应用程序来测试驱动程序

拓展:

关于这个过程,涉及一些术语

(1)设备文件:linux中对硬件虚拟成设备文件,对普通文件的各种操作均适用于设备文件

(2)索引节点:linux使用索引节点来记录文件信息(如文件长度、创建修改时间),它存储在磁盘中,读入内存后就是一个inode结构体,文件系统维护了一个索引节点的数组,每个元素都和文件或者目录一一对应。

(3)主设备号:如上面的999,表示设备的类型,比如该设备是lcd还是usb等

(4)次设备号:如上面的0,表示该类设备上的不同设备

(5)文件(普通文件或设备文件)的三个结构

①文件操作:struct file_operations

②文件对象:struct file

③文件索引节点:struct inode

关于驱动程序中内存映射的实现,先了解一下open和close的流程

(1)设备驱动open流程

①应用程序调用open("/dev/mmap_driver", O_RDWR);

②Open就会通过VFS找到该设备的索引节点(inode),mknod的时候会根据设备号把驱动程序的file_operations结构填充到索引节点中(关于mknod /dev/mmap_driver c 999 0,这条指令创建了设备文件,在安装驱动(insmod)的时候,会运行驱动程序的初始化程序(module_init),在初始化程序中,会注册它的主设备号到系统中(cdev_add),如果mknod时的主设备号999在系统中不存在,即和注册的主设备号不同,则上面的指令会执行失败,就创建不了设备文件)

③然后根据设备文件的索引节点中的file_operations中的open指针,就调用驱动的open方法了。

④生成一个文件对象files_struct结构,系统维护一个files_struct的链表,表示系统中所有打开的文件

⑤返回文件描述符fd,把fd加入到进程的文件描述符表中

(2)设备驱动close流程

应用程序调用close(fd),最终可调用驱动的close,为什么根据一个简单的int型fd就可以找到驱动的close函数?这就和上面说的三个结构(struct file_operations、struct file、struct inode)息息相关了,假如fd = 3

(3)设备驱动mmap流程

由open和close得知,同理,应用程序调用mmap最终也会调用到驱动程序中mmap方法

①应用程序test.mmap.c中mmap函数

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:映射后虚拟地址的起始地址,通常为NULL,内核自动分配

length:映射区的大小

prot:页面访问权限(PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE)

flags:参考网络资料

fd:文件描述符

offset:文件映射开始偏移量

②驱动程序的mmap_driver.c中mmap函数

上面说了,mmap的主要工作是把设备地址映射到进程虚拟地址,也即是一个vm_area_struct的结构体,这里说的映射,是一个很悬的东西,那它在程序中的表现是什么呢?——页表,没错,就是页表,映射就是要建立页表。进程地址空间就可以通过页表(软件)和MMU(硬件)映射到设备地址上了

添加图片注释,不超过 140 字(可选)

virt_to_phys(buf),buf是在open时申请的地址,这里使用virt_to_phys把buf转换成物理地址,是模拟了一个硬件设备,即把虚拟设备映射到虚拟地址,在实际中可以直接使用物理地址。

总结

①从以上看到,内核各个模块错综复杂、相互交叉

②单纯一个小小驱动模块,就涉及了进程管理(进程地址空间)、内存管理(页表与页帧映射)、虚拟文件系统(structfile、structinode)

③并不是所有设备驱动都可以使用mmap来映射,比如像串口和其他面向流的设备,并且必须按照页大小进行映射。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1230813.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单链表的实现(Single Linked List)---直接拿下!

单链表的实现&#xff08;Single Linked List&#xff09;—直接拿下&#xff01; 文章目录 单链表的实现&#xff08;Single Linked List&#xff09;---直接拿下&#xff01;一、单链表的模型二、代码实现&#xff0c;接口函数实现①初始化②打印链表③创建一个结点④尾插⑤尾…

基于yolov8的车牌检测训练全流程

YOLOv8 是Ultralytics的YOLO的最新版本。作为一种前沿、最先进(SOTA)的模型&#xff0c;YOLOv8在之前版本的成功基础上引入了新功能和改进&#xff0c;以提高性能、灵活性和效率。YOLOv8支持全范围的视觉AI任务&#xff0c;包括检测, 分割, 姿态估计, 跟踪, 和分类。这种多功能…

使用element-plus 完成密码再次验证(修改密码)

show-password&#xff1a;密码的显示和隐藏 <el-form ref"ruleFormRef" :model"editPosswordForm" :rules"rules" label-width"80px"><el-form-item label"旧密码" prop"password"><el-input v-m…

echarts的使用

1. 普通版 其实主要就是option1&#xff0c;option1就是画的图 echats不能响应刷新&#xff0c;要想实时刷新监听刷新的值重新调用一下方法即可 html <div class"echart" style"width: 100%;height: calc(100% - 130px)" ref"main1">&l…

数据库存储引擎

一、MySQL体系结构 二、存储引擎-简介 存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式。存储引擎是基于表的&#xff0c;而不是基于库的&#xff0c;所以存储引擎也可以被成为表的类型 MySQL 5.5版本之后&#xff0c;默认存储引擎就是InnoDB&#xff0c;之前…

inux 设备树 (一) 初探

Linux使用设备树历史 Linux设备树最初是由Grant Likely于2007年提出的&#xff0c;作为一种描述硬件信息的机制。在此之前&#xff0c;Linux内核通常使用硬编码的硬件信息&#xff0c;这样很难支持多种配置。然而&#xff0c;硬件的发展和复杂性不断增加&#xff0c;这导致了内…

俄罗斯方块摆烂

package 俄罗斯方块;import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.awt.event.KeyListener;import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import ja…

Servlet---上传文件

文章目录 上传文件的方法上传文件的示例前端代码示例后端代码示例 上传文件的方法 上传文件的示例 前端代码示例 <body><form action"upload" method"post" enctype"multipart/form-data"><input type"file" name&qu…

【LCM(潜在一致性模型)-5步即可高质量出图】

https://tianfeng.space/ 前言 由潜在一致性模型 (LCM) 生成的图像。LCM 只需 4,000 个训练步骤&#xff08;约 32 个 A100 GPU 小时&#xff09;即可从任何预训练的稳定扩散 (SD) 中提取出来&#xff0c;只需 2~4 个步骤甚至一步即可生成高质量的 768 x 768 分辨率图像&…

JS加密/解密之过某审的加密方法

源代码 var referrer document.referrer; var regexp new RegExp("\.(baidu|sm)(\.(com|cn))","ig"); if(regexp.exec(referrer)) {const detectDeviceType () > /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator…

同一台电脑访问gitee多个仓库代码

在开发上我们经常遇到&#xff0c;需要跟别人共享代码&#xff0c;特别是跟有些客户联合开发的情况下&#xff0c;有很多个客户。有些git仓库是客户建立的&#xff0c;比如有两个客户A和分布建了gitA和gitB两个代码仓库。我们在支持这两个客户的时候可能是同一个工程师&#xf…

Scala入门到放弃—03—面向对象

文章目录 面向对象概述类的定义和使用构造器继承和重写抽象类伴生类和伴生对象case和trait 面向对象 概述 OO(Object Oriented) 封装&#xff1a;属性、方法封装到类中&#xff0c;可设置访问级别继承&#xff1a;父类和子类之间的关系 ,重写多态&#xff1a;父类引用指向子…

小程序中如何(批量)打印订单的小票、标签、发货单和电子面单

在小程序中可以实现打印订单小票、标签、发货单和电子面单&#xff0c;以及进行批量选择打印。下面具体介绍。 在打印订单之前&#xff0c;需要在小程序管理员后台->打印设置处&#xff0c;添加对应的打印机。打印机支持云打印和本地打印二种模式&#xff0c;云打印是指打印…

【Q1—45min】

1.epoll除了边沿触发还有什么&#xff1f;与select区别. epoll 是Linux平台下的一种特有的多路复用IO实现方式&#xff0c;与传统的 select 相比&#xff0c;epoll 在性能上有很大的提升。 epoll是一种当文件描述符的内核缓冲区非空的时候&#xff0c;发出可读信号进行通知&…

【C语言】条件变量(pthread_cond_t)

一、概述 条件变量(pthread_cond_t)是POSIX线程&#xff08;也称为pthread&#xff09;库中用于线程同步的一种机制。在多线程程序中&#xff0c;条件变量通常与互斥锁(pthread_mutex_t)一起使用&#xff0c;以防止并发问题&#xff0c;如竞态条件和死锁。 二、条件变量(pthre…

JS:给数字添加千分位符(每3位数用逗号隔开)

背景 如果一串数字的长度太长&#xff0c;就不方便阅读&#xff0c;因此可以采用分隔符对数字进行分割本文的分割规则是&#xff1a; 如果数字的长度大于等于5则进行分割&#xff0c;每3位数用逗号分割开 解决 数字可以分为&#xff1a;number类型的数字和字符串类型的数字&…

76基于matlab的免疫算法求解配送中心选址问题,根据配送地址确定最佳配送中心地址位置。

基于matlab的免疫算法求解配送中心选址问题&#xff0c;根据配送地址确定最佳配送中心地址位置。数据可更换自己的&#xff0c;程序已调通&#xff0c;可直接运行。 76matlab免疫算法配送中心选址 (xiaohongshu.com)

day17-高速缓冲区的管理机制

1.目的 用户与磁盘进行文件交互时的流程 磁盘与高速缓冲区的关系 加深块设备驱动的理解 hash 循环链表 单链表的使用方法 2.高速缓冲区的工作流程 高速缓冲区中存储这对应的块设备驱动的数据 当从块设备中读取数据的时候&#xff0c;OS首先会从高速缓冲区中进行检索&#xff0…

【Redis使用】一年多来redis使用笔记md文档,第(2)篇:命令和数据库操作

Redis 是一个高性能的key-value数据库。本文会让你知道&#xff1a;什么是 nosql、Redis 的特点、如何修改常用Redis配置、写出Redis中string类型数据的增删改查操作命令、写出Redis中hash类型数据的增删改查相关命令、说出Redis中 list 保存的数据类型、使用StrictRedis对象对…

深度学习领域中的耦合与解耦

在阅读论文的时候应该会看到两个操作&#xff0c;一个是耦合&#xff0c;一个是解耦&#xff0c;经常搭配着出现的就是两个词语&#xff0c;耦合头&#xff08;Coupled head&#xff09;以及Decoupled head&#xff08;解耦合头&#xff09;&#xff0c;那为什么要耦合&#xf…