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的参数决定,例如可读可写,共享等
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言