Linux驱动:再看静态映射和动态映射
静态映射表建立过程分析
静态映射表的建立通常分为三个阶段:编译时配置、内核启动初期初始化、运行时使用。
编译时配置
- 内存布局定义
内核通过链接脚本(如vmlinux.lds)和配置文件(如arch//mm/mmu.c)定义静态映射的物理地址范围和虚拟地址范围。
/* arch/arm/kernel/vmlinux.lds.S */
.text : {_text = .;*(.text.hot)*(.text.unlikely)/* ... */
}.init : {*(.init.text)/* ... */
} at __init_begin/* 定义外设映射区域 */
__peripheral_base = 0xFE000000;
- 映射关系配置
内核通过宏定义或数据结构描述物理地址与虚拟地址的映射关系。
/* arch/arm/mm/mmu.c */
static struct map_desc early_iomap[] __initdata = {{.virtual = 0xFE000000, /* 虚拟地址基址 */.pfn = __phys_to_pfn(0x10000000), /* 物理页帧号 */.length = 0x01000000, /* 映射长度(16MB) */.type = MT_DEVICE, /* 映射类型(设备内存) */},/* 更多映射项... */
};
内核启动初期初始化
- 页表初始化
early_trap_init():
初始化早期异常处理表,建立最低限度的映射(如异常向量表)。
paging_init():
基于编译时配置的映射关系,创建初始页表
/* arch/arm/mm/mmu.c */
void __init paging_init(void)
{/* 创建初始页表 */create_mapping(early_iomap, ARRAY_SIZE(early_iomap));/* 激活MMU */enable_mmu();
}
- 映射函数
内核使用特定函数(如create_mapping())将物理地址范围映射到虚拟地址。
/* arch/arm/mm/mmu.c */
static void __init create_mapping(struct map_desc *map, int count)
{for (i = 0; i < count; i++) {struct map_desc *mdesc = &map[i];unsigned long vaddr = mdesc->virtual;unsigned long pfn = mdesc->pfn;unsigned long length = mdesc->length;unsigned int type = mdesc->type;/* 创建页表项 */create_page_tables(vaddr, pfn, length, type);}
}
运行时使用
静态映射建立后,内核可直接通过预定义的虚拟地址访问硬件资源。
#define GPIO_BASE_PHYS 0x10010000 /* 物理基址 */
#define GPIO_BASE_VIRT 0xFE010000 /* 虚拟基址(静态映射) *//* 直接通过虚拟地址访问寄存器 */
void gpio_set_value(int pin, int value)
{if (value)__raw_writel(1 << pin, GPIO_BASE_VIRT + 0x04); /* 设置DATA寄存器 */else__raw_writel(0 << pin, GPIO_BASE_VIRT + 0x04);
}
关键数据结构与函数
- struct map_desc
描述一段连续的物理地址到虚拟地址的映射关系:
struct map_desc {unsigned long virtual; /* 虚拟地址基址 */unsigned long pfn; /* 物理页帧号(PFN) */unsigned long length; /* 映射长度(字节) */unsigned int type; /* 映射类型(如MT_DEVICE、MT_MEMORY) */
};
-
映射类型(type)
不同的映射类型影响内存访问特性:
MT_DEVICE:设备 I/O 内存,禁止缓存和写合并。
MT_MEMORY:普通内存,启用缓存和写回。
MT_DEVICE_NONSHARED:非共享设备内存。 -
静态映射表的优化与扩展
- 延迟映射(Deferred Mapping)
对于不常用的内存区域,内核可采用延迟映射策略,在首次访问时才建立映射,减少初始内存占用。 - 大页映射(Huge Pages)
对于连续的大块内存(如 DMA 缓冲区),使用大页映射(如 2MB、1GB 页)可减少页表项数量,提高 TLB 命中率。 - 动态调整
部分架构支持在运行时动态调整静态映射(如通过vmalloc区域扩展),但需谨慎处理同步问题。
- 典型应用场景
内核启动代码:
在内存管理系统初始化前,通过静态映射访问必要的硬件(如串口、计时器)。
中断控制器:
GIC(通用中断控制器)等关键硬件通常使用静态映射,确保中断处理的高效性。
系统计时器:
访问系统时钟(如 ARM 的 SP804 计时器),确保计时精度。
内存控制器:
配置 DRAM 参数的寄存器通常需要静态映射。
动态映射控制LED
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/kernel.h>
#include <mach/gpio-bank.h>
#include <mach/regs-gpio.h>
#include <linux/io.h>
#include <linux/ioport.h>#define GPJ0_BACE 0xE0200240
//物理地址
#define GPJ0CON_PA 0xE0200240
#define GPJ0DAT_PA 0xE0200244#define BUFFER_SIZE 100
#define MYMAJOR 250
#define MYNAME "mychartest"int mymajor;
static char buffer[BUFFER_SIZE];// 映射后的虚拟地址指针
static volatile unsigned int *gpj0con_virt;
static volatile unsigned int *gpj0dat_virt;struct gpj0_virt_t{unsigned int gpj0con_virt;unsigned int gpj0dat_virt;
};struct gpj0_virt_t* gpj0_virt_t_p;// 打开设备函数
static int my_open(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device opened\n");*gpj0con_virt |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20);//*gpj0dat_virt &= ~((1 << 3) | (1 << 4) | (1 << 5)); // 点亮 LED3-5(低电平点亮)*gpj0dat_virt = (0 << 3) | (0 << 4) | (0 << 5);return 0;
}// 释放设备函数
static int my_release(struct inode *inode, struct file *filp) {printk(KERN_INFO "Device released\n");// 熄灭LED//*gpj0dat_virt |= (1 << 3) | (1 << 4) | (1 << 5); // 熄灭 LED3-5(高电平熄灭)*gpj0dat_virt = (1 << 3) | (1 << 4) | (1 << 5);return 0;
}// 定义file_operations结构体实例
static const struct file_operations my_fops = {.owner = THIS_MODULE,//全部一样,都这样写.open = my_open,.release = my_release,
};// 模块安装函数
static int __init chrdev_init(void)
{ printk(KERN_INFO "chrdev_init helloworld init\n");//注册字符设备驱动//将第一个参数填写0 成功:返回主设备号 失败:返回负数mymajor = register_chrdev(MYMAJOR, MYNAME, &my_fops);if(mymajor < 0){printk(KERN_ERR "register_chrdev fail\n");return -EINVAL;}printk(KERN_INFO "chrdev_init helloworld successs... MYMAJOR = %d\n",MYMAJOR);// 请求连续内存区域(GPJ0CON + GPJ0DAT共8字节)if (!request_mem_region(GPJ0_BACE, sizeof(struct gpj0_virt_t), "GPJ0_BACE")) {printk(KERN_ERR "%s: Failed to request memory region\n", MYNAME);unregister_chrdev(mymajor, MYNAME);return -EBUSY;}// 映射物理地址gpj0_virt_t_p = (struct gpj0_virt_t *)ioremap(GPJ0_BACE,sizeof(struct gpj0_virt_t));if (!gpj0_virt_t_p) {printk(KERN_ERR "%s: Failed to map GPJ0CON\n", MYNAME);release_mem_region(GPJ0_BACE, sizeof(struct gpj0_virt_t)); // 释放内存区域unregister_chrdev(MYMAJOR, MYNAME); // 注销字符设备return -ENOMEM;}gpj0_virt_t_p->gpj0con_virt |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20);gpj0_virt_t_p->gpj0dat_virt = (0 << 3) | (0 << 4) | (0 << 5);printk(KERN_INFO "%s: Memory mapped successfully\n", "GPJ0_BACE");return 0;
}// 模块下载函数
static void __exit chrdev_exit(void)
{printk(KERN_INFO "chrdev_exit helloworld exit\n");gpj0_virt_t_p->gpj0dat_virt = (1 << 3) | (1 << 4) | (1 << 5);// 解除映射iounmap(gpj0_virt_t_p);// 释放资源release_mem_region(GPJ0_BACE, sizeof(struct gpj0_virt_t));//注销字符设备驱动unregister_chrdev(MYMAJOR, MYNAME);}module_init(chrdev_init);
module_exit(chrdev_exit);// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
内核读写寄存器的接口
writel 和 readl
功能与原型
writel:向指定的物理地址写入一个 32 位值(4 字节)。
void writel(unsigned int value, void *addr);
readl:从指定的物理地址读取一个 32 位值。
unsigned int readl(const void *addr);
使用场景
直接操作物理地址:当需要直接访问硬件寄存器的物理地址时(如通过 ioremap 映射后的地址)。
void __iomem *reg_base = ioremap(0x40000000, 0x1000); // 映射物理地址
writel(0x12345678, reg_base + 0x04); // 向偏移0x04处写入32位值
unsigned int val = readl(reg_base + 0x08); // 从偏移0x08处读取32位值
特性
自动处理地址转换:
函数内部会根据架构自动处理物理地址到虚拟地址的转换(如 ARM 架构可能需要添加特定偏移)。
隐式内存屏障:
部分架构下,writel/readl 可能包含内存屏障(memory barrier),确保读写操作按顺序执行。
iowrite32 和 ioread32
功能与原型
iowrite32:向指定的 I/O 地址写入一个 32 位值。
void iowrite32(u32 value, void __iomem *addr);
ioread32:从指定的 I/O 地址读取一个 32 位值。
u32 ioread32(const void __iomem *addr);
使用场景
访问 I/O 映射的寄存器:
当需要访问通过 ioremap 映射后的 I/O 空间时,推荐使用这组函数。
void __iomem *gpio_base = ioremap(GPIO_BASE_ADDR, 0x100);
iowrite32(0x0000000F, gpio_base + GPIO_DIR_OFFSET); // 设置GPIO方向
u32 status = ioread32(gpio_base + GPIO_STATUS_OFFSET); // 读取状态
特性
显式类型安全:
函数参数使用 void __iomem * 类型,强制要求使用通过 ioremap 映射的地址,增强类型安全性。
架构适配:
针对不同架构(如 x86、ARM、PowerPC)提供优化实现,确保正确的内存访问行为。
内存屏障选项:
部分架构下,可通过 iowrite32be/ioread32be 等变体指定字节序或添加内存屏障。
例子:
// gpj0_virt_t_p->gpj0con_virt |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20);// gpj0_virt_t_p->gpj0dat_virt = (0 << 3) | (0 << 4) | (0 << 5);writel( readl(&gpj0_virt_t_p->gpj0con_virt) | (0x1 << 12) | (0x1 << 16) | (0x1 << 20),&gpj0_virt_t_p->gpj0con_virt);writel( (0 << 3) | (0 << 4) | (0 << 5),&gpj0_virt_t_p->gpj0dat_virt);
//gpj0_virt_t_p->gpj0dat_virt = (1 << 3) | (1 << 4) | (1 << 5);writel( (1 << 3) | (1 << 4) | (1 << 5),
// 配置 GPJ0_3/4/5 引脚为输出模式 (0x1 = 输出)
u32 con_value = ioread32(&gpj0_virt_t_p->gpj0con_virt);
con_value |= (0x1 << 12) | (0x1 << 16) | (0x1 << 20); // 配置位 12/16/20 为 1
iowrite32(con_value, &gpj0_virt_t_p->gpj0con_virt);// 输出低电平点亮 LED (假设 LED 是低电平有效)
iowrite32((0 << 3) | (0 << 4) | (0 << 5), &gpj0_virt_t_p->gpj0dat_virt);// 后续代码:输出高电平熄灭 LED
iowrite32((1 << 3) | (1 << 4) | (1 << 5), &gpj0_virt_t_p->gpj0dat_virt);