mmap底层驱动实现(remap_pfn_range函数)

mmap底层驱动实现

myfb.c(申请了128K空间)
c 复制代码
#include <linux/init.h>
#include <linux/tty.h>
#include <linux/device.h>
#include <linux/export.h>
#include <linux/types.h>
#include <linux/module.h>
#include <linux/export.h>
#include <linux/mm_types.h>
#include <linux/mm.h>
#include <linux/slab.h>

#define 	BUFF_SIZE 		(32 * 4 * 1024)

static char *buff;
static int major;
static struct class * myfb_class;

static int myfb_mmap (struct file *fp, struct vm_area_struct *vm)
{
	int res;
	//表示该vma在虚拟地址空间中的偏移地址,单位是页(4K)
	//unsigned long offset = vm->vm_pgoff << PAGE_SHIFT;   
	
	//计算被映射的物理内存的物理页帧号(物理地址+偏移),以页为单位, virt_to_phys将虚拟地址转成物理地址
	//vm->pgoff表示的是用户空间映射时在VMA中的偏移(即mmap最后一个参数,单位是字节,但vm->pgoff自动转成页单位)
	unsigned long pfn_start = (virt_to_phys(buff) >> PAGE_SHIFT);   

	res = remap_pfn_range(vm, vm->vm_start, 
				pfn_start + vm->vm_pgoff,   //在物理页帧号上加上偏移
				vm->vm_end - vm->vm_start, 
				vm->vm_page_prot);
	if(res){
		printk("remap_pfn_range failed\n");
		return -1;
	}

	printk("[kernel] pfn_start = 0x%lx, vm->vm_pgoff = 0x%lx, \
		\n[kernel] vm->vm_start = 0x%lx, vm->vm_end = 0x%lx, vir_ker_start = 0x%lx\n",  \
		pfn_start, vm->vm_pgoff, vm->vm_start, vm->vm_end, (unsigned long)buff);
	return 0;
}

static struct file_operations myfb_fops = {
	.owner = THIS_MODULE,
	.mmap  = myfb_mmap,
};

static int myfb_init(void)
{
	buff = kzalloc(BUFF_SIZE, GFP_KERNEL);
	if (!buff){
		printk("kzalloc failed!\n");
		return -ENOMEM;
	}
	printk("kzalloc success!\n");

	major = register_chrdev(0, "myfb", &myfb_fops);
	myfb_class = class_create(THIS_MODULE, "myfb_class");
	device_create(myfb_class, NULL, MKDEV(major, 0), NULL, "myfb");
	
	return 0;
}

static void myfb_exit(void)
{
	device_destroy(myfb_class, MKDEV(major, 0));
	class_destroy(myfb_class);
	unregister_chrdev(major, "myfb");
	
	kfree(buff);
}


module_init(myfb_init);
module_exit(myfb_exit);
MODULE_LICENSE("GPL");
mmap_read.c
c 复制代码
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define   PAGE_SIZE    (4*1024)
#define   BUFF_SIZE    (1 * PAGE_SIZE)
#define   OFFSET       (2 * PAGE_SIZE)

char *p;
int fd;

void ctrlc(int signum)
{
	munmap(p, BUFF_SIZE);
	close(fd);
}

int main(void)
{
	signal(SIGINT, ctrlc);
	fd = open("/dev/myfb", O_RDWR);

       	p = (char *)mmap(NULL, BUFF_SIZE, PROT_READ | PROT_WRITE , MAP_SHARED, fd, OFFSET);

	if(p){
		printf("mmap addr = 0x%x\n", p);
		printf("data = %s\n", p);
	}else{
		printf("mmap failed\n");
	}

	while(1){
		sleep(1);
	}

	return 0;
}
mmap_write.c
c 复制代码
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define   PAGE_SIZE    (4*1024)
#define   BUFF_SIZE    (1 * PAGE_SIZE)
#define   OFFSET       (0 * PAGE_SIZE)

char *p;
int fd;

void ctrlc(int signum)
{
	munmap(p, BUFF_SIZE);
	close(fd);
}

int main(void)
{
	signal(SIGINT, ctrlc);

	fd = open("/dev/myfb", O_RDWR);

       	p = (char *)mmap(NULL, BUFF_SIZE, PROT_READ | PROT_WRITE , MAP_SHARED, fd, OFFSET);

	if(p){
		printf("mmap addr = 0x%x\n", p);
		memcpy(p, "hello world", 20);
	}else{
		printf("mmap failed\n");
	}

	while(1){
		sleep(1);
	}
	return 0;
}

Makefile

c 复制代码
KERNEL_DIR = /home/me/Kernel_Uboot/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

all:
	make -C $(KERNEL_DIR) M=`pwd` modules
	$(CROSS_COMPILE)gcc mmap_read.c -o mmap_read
	$(CROSS_COMPILE)gcc mmap_write.c -o mmap_write

clean:
	make -C $(KERNEL_DIR) M=`pwd` modules clean

obj-m += myfb.o

当读和写的进程内存映射地址的偏移都为0时 ,读进程能把写进程写入的数据读出

当写进程内存映射地址偏移为0 ,读进程内存映射地址**偏移为2(单位页)**时,读进程读出数据为空

PS:注意到读写进程的pfn_start相同,这个值是映射的物理内存地址,vm->vm_pgoff 是偏移(单位页,一页=4K(4096))

但是两个进程映射的虚拟地址结果不一定相同,虚拟地址是进程自己独有的,这点很容易理解。


1. 查看虚拟内存分布

查看读写进程的pid

写进程的虚拟内存分布

读进程的虚拟内存分布

1.1 分析虚拟内存映射部分

以读进程为例,/dev/myfb所在行即是内存映射的部分

76ffa000: vm->vm_start 的值

76ffb000: vm->vm_end 的值

rw-s: 表示的是 vm->vm_flags,"rw"表示可读可写,"s"表示 share共享,"p"表示 private 私有

00000000: 表示偏移量,即 vm->vm_pgoff(单位页,此处的偏移量单位是字节,需要做一下换算)

00:06 : 表示主次设备号

2564: 表示 inode 值

/dev/myfb: 表示设备节点名

1.2 关于偏移量

将读进程中mmap函数最后一个参数改为2*4096(2页)后,进程的虚拟内存映射部分的地址分布如下。

可以看到偏移量为 00002000 ,即 2*4096字节 = 2页 = 8K

偏移量指的是mmap最后一个参数、同样也是vm->vm_pgoff(单位页),指的是映射时在文件的物理内存上的偏移,只映射了文件的部分内容,单位是页4K


2. remap_pfn_range函数
2.1 remap_pfn_range函数原型
c 复制代码
/**
 * remap_pfn_range - remap kernel memory to userspace
 * @vma: user vma to map to
 * @addr: target user address to start at
 * @pfn: physical address of kernel memory
 * @size: size of map area
 * @prot: page protection flags for this mapping
 *
 *  Note: this is only safe if the mm semaphore is held when called.
 */
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
		    unsigned long pfn, unsigned long size, pgprot_t prot)
参数 含义
vm 虚拟内存区域描述符,用于表示映射的虚拟内存区域
addr 映射的虚拟内存区域的首地址
pfn 物理页帧号
size 映射区域的大小
prot 物理页面的操作属性,例如读/写/执行权限
2.2 remap_pfn_range函数使用

这里主要搞懂 myfb.c 驱动代码中的 remap_pfn_range 中的如下代码

C 复制代码
unsigned long pfn_start = (virt_to_phys(buff) >> PAGE_SHIFT);   

res = remap_pfn_range(vm, vm->vm_start, 
                      pfn_start + vm->vm_pgoff,   //在物理页帧号上加上偏移
                      vm->vm_end - vm->vm_start, 
                      vm->vm_page_prot);
  • vm: 调用mmap时内核自动生成的VMA(虚拟内存描述符)
  • vm->vm_start: 该VMA的起始地址
  • vm->_end: 该VMA的结束地址
  • virt_to_phys: 将虚拟地址转成物理地址
  • PAGE_SHIFT: 宏,值为12,1<<PAGE_SHIFT表示4096,即4K,一页的大小
  • vm->vm_pgoff: 指的是映射时在文件的物理内存上的偏移,只映射了文件的部分内容,单位是页(4K)
  • vm->vm_page_prot: 该虚拟内存的访问权限,由mmap的参数决定,例如可读可写,共享等
相关推荐
CoolTiger、44 分钟前
【Vmware16安装教程】
linux·虚拟机·vmware16
学习3人组2 小时前
CentOS 中配置 OpenJDK以及多版本管理
linux·运维·centos
厨 神2 小时前
vmware中的ubuntu系统扩容分区
linux·运维·ubuntu
Karoku0662 小时前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
geek_Chen012 小时前
虚拟机共享文件夹开启后mnt/hgfs/下无sharefiles? --已解决
linux·运维·服务器
(⊙o⊙)~哦3 小时前
linux 解压缩
linux·运维·服务器
牧小七4 小时前
Linux命令---查看端口是否被占用
linux
鸡鸭扣5 小时前
虚拟机:3、(待更)WSL2安装Ubuntu系统+实现GPU直通
linux·运维·ubuntu
友友马5 小时前
『 Linux 』HTTP(一)
linux·运维·服务器·网络·c++·tcp/ip·http
记得开心一点嘛6 小时前
在Linux系统上使用Docker部署javaweb项目
linux·运维·docker