14 地址映射

1、地址划分

  • 明确:在linux系统中,不管是应用程序还是驱动程序,都不允许直接访问外设的物理地址,要想访问必须将物理地址映射到用户虚拟地址或者内核虚拟地址,一旦映射完毕,应用或者内核程序访问映射的虚拟地址,就是在访问实际的物理地址
  • 在linux系统中,4G虚拟地址空间的划分
    用户虚拟地址范围:0x00000000~0xBFFFFFFF
    内核虚拟地址范围:0xC0000000~0xFFFFFFFF
  • 如何将物理地址映射到内核虚拟地址?- 利用ioremap函数
  • 如何将物理地址映射到用户虚拟地址?- 利用mmap
  • 一个物理地址可以有多个虚拟地址,一个虚拟地址不能有多个物理地址

2、相关函数

2.1 ioremap/iounmap

void *ioremap(unsigned long phy_address, unsigned long len)
- 功能:将物理地址映射到内核虚拟地址
- 参数:
	- phy_address:传递要映射的起始的物理地址
	- len:传递要映射的物理地址空间的大小
- 返回值:返回映射的起始内核虚拟地址
void iounmap(void *vir_address)
- 功能:解除物理地址和内核虚拟地址的映射关系
- 参数:vir_address:传递映射好的起始内核虚拟地址
  • 案例:使用地址映射操作gpio
  寄存器     		  物理地址		 内核虚拟地址 
GPIOCALTFN0			0xC001C020		  gpiocaltfn0
GPIOCOUTENB			0xC001C004		  gpiocoutenb
GPIOCOUTENB			0xC001C000		  gpiocout 
//地址映射,两种方案:
方案1unsigned long *gpiocout, *gpiocoutenb, *gpiocaltfn0;
	gpiocout = ioremap(0xC001C000, 4);
	gpiocoutenb = ioremap(0xC001C004, 4);
	gpiocaltfn0 = ioremap(0xC001C020, 4);
	
方案2:由于寄存器的物理地址空间都是连续的,所以连续映射:
	void *gpiobase;
	unsigned long *gpiocout, *gpiocoutenb, *gpiocaltfn0;
	gpiobase = ioremap(0xC001C000, 0x24);
	//地址换算
	gpiocout = (unsigned long *)(gpiobase + 0x00);
	gpiocoutenb = (unsigned long *)(gpiobase + 0x04);
	gpiocaltfn0 = (unsigned long *)(gpiobase + 0x20);
		
//配置模式
*gpiocaltfn0 &= ~(3 << 24);
*gpiocaltfn0 |= (1 << 24);

//使能
*gpiocoutenb |= (1 << 12);

//输出
*gpiocout |= (1 << 12);
*gpiocout &= ~(1 << 12);

案例:实现一个软件能够编辑处理器任意一个寄存器
驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/io.h> //ioremap/iounmap
#include <linux/uaccess.h>
//描述寄存器操作信息的结构体
struct reg_info {
	unsigned long phy_address; //寄存器物理地址
	unsigned long data;		   //寄存器数据
};

#define REG_WRITE 	0x100001 	//写寄存器命令
#define REG_READ	0x100002	//读寄存器命令
static long reg_ioctl(struct file *file,unsigned int cmd,unsigned long buf) {
	unsigned long *gpiobase;
	struct reg_info kreg;
	copy_from_user(&kreg,(struct reg_info *)buf,sizeof(kreg));
	//结果:kreg.phy_address=物理地址,kreg.data=?
	//将寄存器物理地址映射到内核虚拟地址
	gpiobase = ioremap(kreg.phy_address, 4);
	if(gpiobase == NULL) {
		printk("ioremap failed.\n");
		return -1;
	}
	switch(cmd) {
		case REG_WRITE:
				*gpiobase = kreg.data;
				break;
		case REG_READ:
				kreg.data = *gpiobase;
				copy_to_user((struct reg_info *)buf,&kreg, sizeof(kreg));
				break;
	}
	//解除地址映射
	iounmap(gpiobase);
	return 0;
}
static struct file_operations reg_fops = {
		.unlocked_ioctl = reg_ioctl
};
static struct miscdevice reg_misc = {
	.name = "reg",
	.minor = MISC_DYNAMIC_MINOR,
	.fops = &reg_fops
};
static int reg_init(void) {
	misc_register(&reg_misc);
	return 0;
};
static void reg_exit(void){
	misc_deregister(&reg_misc);
};

module_init(reg_init);
module_exit(reg_exit);
MODULE_LICENSE("GPL");

应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
struct reg_info {
	unsigned long phy_address; //寄存器物理地址
	unsigned long data;		   //寄存器数据
};
#define REG_WRITE	0x100001	//写寄存器命令
#define REG_READ	0x100002	//读寄存器命令
int main(int argc, char *argv[]) {
	int fd;
	struct reg_info reg; //暂存寄存器的操作信息
	if((argc != 3) && (argc != 4)) {
		printf("Usage: %s w address data\n", argv[0]);
		printf("%s r address\n", argv[0]);
		return -1;
	}
	fd = open("/dev/reg", O_RDWR);
	if(fd < 0) {
		printf("open reg test device failed.\n");
		return -1;
	}
	if(!strcmp(argv[1], "w")) {
		reg.phy_address = strtoul(argv[2], NULL, 0);
		reg.data = strtoul(argv[3], NULL, 0);
		ioctl(fd, REG_WRITE, &reg);
	} else if(!strcmp(argv[1], "r")) {
		reg.phy_address = strtoul(argv[2], NULL, 0);
		ioctl(fd, REG_READ, &reg);
		printf("%#x:%#x\n",reg.phy_address, reg.data);
	}
	close(fd);
	return 0;
}

测试

./btn_test w 0xC001C000 0xC84 # 开灯
./btn_test w 0xC001C000 0x1C84 # 关灯

  • 字符串转整形标准C库函数:strtoul
unsigned long strtoul(char *pstr, char **ppstr, int n);
- 功能:字符串转整形,例如:"100"->100, "0100"->0100, "0x100"->0x100 
- 参数: 
	- pstr: 传递转换的字符串首地址
	- ppstr:记录字符串中非整形数的子字符串首地址
	- n:指定转换进制
- 返回值:返回转换的整形数

例如: 
char *pstr = "100";
int a = strtoul(pstr, NULL, 0); //结果:a=100
char *pstr = "0100";
int a = strtoul(pstr, NULL, 0); //结果:a=0100
char *pstr = "0x100";
int a = strtoul(pstr, NULL, 0); //结果:a=0x100
char *pstr = "100";
int a = strtoul(pstr, NULL, 16); //结果:a=0x64
char *pstr = "100abcd";
char *pstr1 = NULL; 
int a = strtoul(pstr, &pstr1, 0); //结果:a=100,pstr1 = "abcd"

2.2 mmap地址映射

mmap就是完成物理地址映射到用户虚拟地址用的

用户3G虚拟地址空间划分:
0x00000000-------------------------------------------------------0xBFFFFFFF
			代码段  数据段  BSS段  堆区   MMAP虚拟内存区      栈区
							     ---->  <------       <----

系统调用函数原型:

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将物理地址空间映射到用户虚拟内存空间上			  
- 参数:
    - addr:NULL,让linux内核帮你在用户虚拟内存区域找一块空间内存用来映射物理地址
	- length:让linux内核帮你找空闲用户虚拟内存的大小;切记:大小必须是页面大小(4KB)的整数倍
	- prot:描述内核帮你找的空闲用户虚拟内存的访问权限
		 一般指定为:PROT_READ|PROT_WRITE
    - flags:其余属性,一般指定为:MAP_SHARED
    - fd:硬件外设
	- offset:偏移量,一般给0
- 返回值:linux内核将空闲的用户虚拟内存的首地址进行返回,这个起始用户虚拟地址同样也是4KB整数倍
	

参考代码:

void *addr;
int fd = open("a.txt", O_RDWR);
addr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
说明:将文件a.txt映射到以addr起始的用户虚拟内存上,将来访问映射的用户虚拟内存就是访问文件
//向映射的用户虚拟内存拷贝字符串数据
本质是向文件a.txt写入数据
memcpy(addr, "hello,world", 12);
  • 了解mmap系统调用函数所做的工作:
    1. 应用程序调用mmap,首先跑到C库的mmap函数定义
    2. C库的mmap函数作两件事:
      1.保存mmap系统调用号到r7寄存器
      2.调用swi/svc指令触发软中断异常
    3. 一旦触发软中断异常,CPU核立马处理软中断异常
      CPU核硬件自动做四件事:…
      软件进一步处理软中断异常(在内核空间完成)
    4. 最后进程跑到内核空间继续运行,跑到软中断异常的入口地址运行,做三件事:
      1.保护现场
      2.调用软中断异常处理函数,而此函数又做两件事:
      1.从r7寄存器中取出mmap系统调用号
      2.以mmap系统调用号为下标在内核的系统调用表中找到mmap对应的内核函数sys_mmap,而内核的sys_mmap做三件事:
      1. 内核的sys_mmap首先在当前进程的3G虚拟地址空间中找一块空闲的用户虚拟内存,将来用于和物理地址做映射
      2. 一旦找到空闲的用户虚拟内存,并且用户mmap本身也给用户虚拟内存指定了一堆的属性(大小,权限等)所以内核用struct vm_area_struct数据结构定义初始化一个对象来描述空闲的用户虚拟内存的属性
      3. 最后内核的sys_mmap调用底层驱动的mmap接口,并且内核sys_mmap将第2步创建的对象的地址也传递给底层驱动的mmap接口
      3.底层驱动mmap执行完毕,然后恢复现场状态恢复和跳转返回,至此mmap调用结束

struct vm_area_struct {
unsigned long vm_start; //空间用户虚拟内存的起始地址 等于mmap的返回值addr
unsigned long vm_end; //结束地址=vm_start+大小
pgprot_t vm_page_prot; //等于mmap传递的PROT_READ|PROT_READ|PROT_WRITE
unsigned long vm_flags; //等于mmap传递的MAP_SHARED
unsigned long vm_pgoff; //等于mmap传递的0

};

3.对应的底层驱动的mmap接口
```c
struct file_operations {
	int (*mmap) (struct file *file, struct vm_area_struct *vma);
};
- 功能:永远只能唯一做一件事:将已知的物理地址和已知的用户虚拟地址做映射,由于用户虚拟地址在用户空间,所以将来访问操作都是在应用程序完成,而不是在内核驱动完成,访问映射的用户虚拟地址就是在访问物理地址
- file:跟fd亲戚关系
- vma:指向内核sys_mmap创建的一个对象,此对象来描述空闲的用户虚拟内存的各种属性,将来底层驱动mmap接口利用此指针可以获取到用户虚拟内存的属性:
	  vma->vm_start //获取起始用户虚拟地址
	  vma->vm_end 
	  vma->vm_flags
	  vma->vm_page_prot

问:底层驱动的mmap接口到底如何最终完成映射呢?
因为已知物理地址可以看手册获取到,已知的用户虚拟地址通过vma指针能够获取到如何将两者关联在一起呢?
答:只需调用以下函数完成关联映射:

int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn, unsigned long size,pgprot_t prot);
- 功能:完成最终的地址映射
- 参数:
	- vma:传递内核sys_mmap创建的对象地址,也就是传递驱动mmap接口的第二个参数
	- addr:传递空闲的用户虚拟内存的首地址,也就是传递vma->vm_start
    - pfn:传递起始的物理地址>>12 切记:此物理地址大小必须是4KB(0x1000)整数倍
		   例如:
				0xC001C000>>12:合法
				0xC001C004>>12:不合法
	- size:传递映射的用户虚拟内存的大小,也就是传递:vma->vm_end - vma->vm_start 
	- prot:传递用户虚拟内存的访问权限,也就是传递:vma->vm_page_prot

案例:利用mmap实现开关灯操作
驱动代码

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

static int btn_mmap(struct file *file,struct vm_area_struct *vma){
	remap_pfn_range(
					vma,// 指向用户虚拟内存属性
					vma->vm_start,// 起始用户虚拟地址
					0xC001C000 >> 12,// 起始物理地址 (4KB为单位)
					vma->vm_end - vma->vm_start,// 大小
					vma->vm_page_prot // 读写访问权限
					);
	return 0;
}

static struct file_operations btn_fops = {
		.mmap = btn_mmap // 地址映射接口
};

static struct miscdevice btn_device={
	.name = "mybtn",
	.minor = MISC_DYNAMIC_MINOR,
	.fops = &btn_fops
};
static int btn_init(void){
	misc_register(&btn_device);
	return 0;
}
static void btn_exit(void){
	misc_deregister(&btn_device);
}

module_init(btn_init);
module_exit(btn_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;
	void* gpiobase;
	unsigned long *gpiocout;
	unsigned long *gpiocoutend;
	unsigned long *gpiocaltfn0;
	fd = open("/dev/mybtn",O_RDWR);
	if(fd<0){
		printf("open mybtn failed\n");
		return -1;
	}
	//将内核地址映射到用户虚拟地址
	gpiobase = mmap(NULL,0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
	// 地址换算
	gpiocout = (unsigned long *)(gpiobase + 0x00);
	gpiocoutend = (unsigned long *)(gpiobase + 0x04);
	gpiocaltfn0 = (unsigned long *)(gpiobase + 0x20);
	// 配置输出
	*gpiocaltfn0 &=~(3<<24);
	*gpiocaltfn0 |=(1<<24);
	// 使能
	*gpiocoutend |=(1<<12);
	// 输出为1
	*gpiocout |=(1<<12);
	while(1){
		*gpiocout |=(1<<12);
		sleep(5);
		*gpiocout &=~(1<<12);
		sleep(5);
	}
}

注意,如果出现了操作了寄存器,但是硬件不同步的现象,需要手动关闭cache功能

static int btn_mmap(struct file *file,struct vm_area_struct *vma){
	// 关闭cache功能
	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
	remap_pfn_range(
					vma,// 指向用户虚拟内存属性
					vma->vm_start,// 起始用户虚拟地址
					0xC001C000 >> 12,// 起始物理地址 (4KB为单位)
					vma->vm_end - vma->vm_start,// 大小
					vma->vm_page_prot // 读写访问权限
					);
	return 0;
}

3、总结

read,write,ioctl和mmap对比
什么时候用:read,write,ioctl,什么时候用: mmap

  • read,write,ioctl数据操作流程
    对设备读操作:read,ioctl
    数据流:硬件寄存器----->内核缓冲区------->用户缓冲区
    gpio_get_value copy_to_user
    对设备写操作:write,ioctl
    数据流:用户缓冲区----->内核缓冲区------->硬件寄存器
    copy_from_user gpio_set_value
    结论:read,write,ioctl数据操作势必要经过两次数据拷贝:
    用户-内核->硬件
    硬件->内核->用户

  • mmap数据操作流程:
    对设备读操作: 应用程序直接以指针形式读取寄存器
    data = *gpiocout;
    对设备写操作: 应用程序直接以指针的形式写入寄存器
    *gpiocout &= ~(1 << 12);
    结论:mmap数据操作只需一个数据拷贝:
    用户->硬件
    硬件->用户

  • 所以:

    1. 如果用户对硬件操作访问的数据量比较小,read,write,ioctl的两次数据拷贝对系统性能肯定有影响,但是这种影响几乎可以忽略不计,如果操作的数据量比较大,两次数据拷贝性能的影响是致命的,例如:摄像头,LCD显示屏,声卡等
    2. 如果访问操作的数据量比较大,用read,write,ioctl势必影响系统的性能,务必采用mmap,将两次数据拷贝变成一次提供系统的性能效率
    3. 由于mmap在使用的时候,分配的用户虚拟内存必须是4KB的整数倍,如果操作的数据量比较小,此时还用mmap即使提高了系统的性能(几乎体会不到),反而是浪费了宝贵的内存资源
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

启航zpyl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值