在Linux内核中,地址类型和它们的正确使用是理解内核内存管理的关键。本文将详细解释各种地址类型及其用法。
一、Linux内核中的主要地址类型
- 物理地址 (Physical Address)
phys_addr_t// 平台无关的物理地址类型· 定义:CPU总线上的实际内存地址,对应RAM芯片上的物理位置
· 使用场景:DMA操作、硬件寄存器映射、物理页面管理
· 包含的头文件:#include <linux/types.h>
· 示例:
phys_addr_tphys_addr=page_to_phys(page);// 将struct page转换为物理地址dma_addr_tdma_handle=dma_map_single(dev,virt_addr,size,direction);- 虚拟地址/内核逻辑地址 (Virtual/Kernel Logical Address)
void*// 通用指针unsignedlong// 用于计算的地址值· 定义:内核直接映射区域的地址(通常是3GB/4GB分割中的1GB内核空间)
· 特点:
· 与物理地址有固定的线性映射关系(通常:virt = phys + PAGE_OFFSET)
· 可以通过virt_to_phys()和phys_to_virt()与物理地址相互转换
· 始终有效,可直接解引用(不需要特殊访问函数)
· 示例:
void*virt_addr=kmalloc(size,GFP_KERNEL);// 返回内核逻辑地址unsignedlongaddr=(unsignedlong)virt_addr;- 内核虚拟地址 (Kernel Virtual Address)
void__iomem*// I/O内存的虚拟地址· 定义:通过vmalloc()或ioremap()创建的地址,不一定有连续的物理映射
· 特点:
· 需要通过iowrite32()/ioread32()等函数访问(I/O内存)
· 或直接访问(vmalloc分配的内存)
· 示例:
// I/O内存映射void__iomem*io_addr=ioremap(phys_addr,size);u32 value=ioread32(io_addr+offset);iounmap(io_addr);// vmalloc分配void*vm_addr=vmalloc(size);memcpy(vm_addr,src,len);vfree(vm_addr);- 用户空间地址 (User Space Address)
void__user*// 用户空间地址(需要特殊处理)· 定义:用户进程虚拟地址空间中的地址
· 特点:
· 在内核中不能直接解引用
· 必须通过专门的复制函数访问
· 使用__user标记有助于静态分析工具(如Sparse)检查
· 包含的头文件:#include <linux/uaccess.h>
· 示例:
longcopy_to_user(void__user*to,constvoid*from,unsignedlongn);longcopy_from_user(void*to,constvoid__user*from,unsignedlongn);// 在系统调用中的典型用法staticssize_tmy_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos){charkernel_buf[256];// ... 填充kernel_buf ...if(copy_to_user(buf,kernel_buf,min(count,sizeof(kernel_buf))))return-EFAULT;returnbytes_copied;}- 总线地址 (Bus Address)
dma_addr_t// 设备看到的DMA地址· 定义:设备进行DMA操作时使用的地址
· 特点:可能与物理地址不同(存在IOMMU时)
· 包含的头文件:#include <linux/dma-mapping.h>
· 示例:
dma_addr_tdma_handle;void*cpu_addr=dma_alloc_coherent(dev,size,&dma_handle,GFP_KERNEL);// 将dma_handle传递给设备进行DMAdma_free_coherent(dev,size,cpu_addr,dma_handle);- 各类地址及其kernel API
- 地址类型层次
- 内核地址空间布局
32位系统
高地址
0xFFFFFFFF ±-----------------+
| 内核代码/数据 | <- 内核自身代码0xFFC00000 ±-----------------+
| KMAP_ATOMIC区 | <- 每个CPU的原子映射槽(4KB/CPU)0xFEBFFFFF ±-----------------+
| 固定映射区 | <- 特殊用途固定映射0xFE000000 ±-----------------+
| 持久映射区 | <- kmap持久映射(4MB)0xF8000000 ±-----------------+
| vmalloc区 | <- vmalloc动态分配 | |0xF7FFFFFF ±-----------------+
| 空洞 |0xC0000000 ±-----------------+ <- PAGE_OFFSET(3GB)
| 直接映射区 | <- 896MB物理内存的线性映射 | (低端内存) | | |0x00000000 ±-----------------+
低地址
64位系统
0xFFFFFFFFFFFFFFFF ±-----------------+
| 规范地址空洞 |0xFFFF800000000000 ±-----------------+
| 内核模块 | | vmalloc区 | | vmemmap区 | <- 稀疏内存模型结构 | 直接映射所有物理内存 | <- 可映射TB级内存0xFFFF800000000000 ±-----------------+
| 用户空间 | <- 128TB用户地址空间0x00007FFFFFFFFFFF ±-----------------+
| 用户空间空洞 |0x0000000000000000 ±-----------------+
二、关键转换函数和宏
物理地址 ↔ 虚拟地址转换
#include<asm/io.h>// 内核逻辑地址转换(仅限直接映射区域)phys_addr_tphys=virt_to_phys(virt_addr);void*virt=phys_to_virt(phys_addr);// 页帧号转换structpage*page=virt_to_page(virt_addr);void*virt=page_to_virt(structpage*page);// 通用映射(适用于任何物理地址)void__iomem*ioremap(phys_addr_tphys_addr,size_tsize);voidiounmap(void__iomem*addr);用户空间地址访问
#include<linux/uaccess.h>// 基本复制函数unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongn);unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongn);// 验证用户空间指针是否可访问intaccess_ok(constvoid__user*addr,size_tsize);// 获取/设置单个值intget_user(x,ptr);// 从用户空间获取值intput_user(x,ptr);// 写入用户空间// 字符串操作longstrncpy_from_user(char*dst,constchar__user*src,longcount);longstrnlen_user(constchar__user*s,longn);DMA地址转换
#include<linux/dma-mapping.h>// 一致性DMA映射(缓存一致)void*dma_alloc_coherent(structdevice*dev,size_tsize,dma_addr_t*dma_handle,gfp_tflag);voiddma_free_coherent(structdevice*dev,size_tsize,void*cpu_addr,dma_addr_tdma_handle);// 流式DMA映射(需要同步)dma_addr_tdma_map_single(structdevice*dev,void*ptr,size_tsize,enumdma_data_directiondir);voiddma_unmap_single(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir);// DMA同步voiddma_sync_single_for_cpu(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir);voiddma_sync_single_for_device(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir);API使用与选择决策树
地址转换API使用
谢谢关注,后续会持续分享关于AI,GPU,Linux开发,操作系统,图形学,高性能计算,芯片行业讯息。欢迎感兴趣的伙伴关注微信公众号参与讨论沟通:
请关注公众号获取完整系统资料
请关注微信公众号:颇锐克科技共享
图片
上下文地址转换API
图片
三、实际使用示例
示例1:混合使用各种地址类型
#include<linux/module.h>#include<linux/kernel.h>#include<linux/fs.h>#include<linux/uaccess.h>#include<linux/slab.h>#include<linux/io.h>staticintmy_device_mmap(structfile*filp,structvm_area_struct*vma){structmy_device*dev=filp->private_data;unsignedlongoffset=vma->vm_pgoff<<PAGE_SHIFT;unsignedlongphys=dev->phys_base+offset;unsignedlongvsize=vma->vm_end-vma->vm_start;unsignedlongpsize=dev->mem_size-offset;// 将物理地址映射到用户空间if(vsize>psize)return-EINVAL;returnremap_pfn_range(vma,vma->vm_start,phys>>PAGE_SHIFT,vsize,vma->vm_page_prot);}staticssize_tmy_device_write(structfile*filp,constchar__user*buf,size_tcount,loff_t*pos){structmy_device*dev=filp->private_data;char*kernel_buf;intret;// 1. 在内核空间分配缓冲区(内核逻辑地址)kernel_buf=kmalloc(count,GFP_KERNEL);if(!kernel_buf)return-ENOMEM;// 2. 从用户空间复制数据(用户空间地址 -> 内核空间地址)if(copy_from_user(kernel_buf,buf,count)){kfree(kernel_buf);return-EFAULT;}// 3. 处理数据...// 4. 可能需要进行DMA操作dma_addr_tdma_addr=dma_map_single(dev->device,kernel_buf,count,DMA_TO_DEVICE);// 设置设备寄存器(使用I/O映射地址)iowrite32(dma_addr,dev->io_addr+REG_DMA_ADDR);iowrite32(count,dev->io_addr+REG_DMA_LEN);// 5. 清理dma_unmap_single(dev->device,dma_addr,count,DMA_TO_DEVICE);kfree(kernel_buf);returncount;}示例2:简单的字符设备驱动程序
#include<linux/fs.h>#include<linux/uaccess.h>#defineBUFFER_SIZE1024staticchardevice_buffer[BUFFER_SIZE];staticssize_tdevice_read(structfile*filp,char__user*buffer,size_tlength,loff_t*offset){intbytes_to_copy;intbytes_copied=0;// 验证用户空间缓冲区是否可访问if(!access_ok(buffer,length))return-EFAULT;bytes_to_copy=min(length,(size_t)BUFFER_SIZE);// 从内核缓冲区复制到用户空间if(bytes_to_copy){if(copy_to_user(buffer,device_buffer,bytes_to_copy))return-EFAULT;bytes_copied=bytes_to_copy;}returnbytes_copied;}最佳实践和注意事项
始终使用正确的类型:
· 用户空间指针 → void __user *
· I/O内存指针 → void __iomem *
· DMA地址 → dma_addr_t
地址验证:
// 对于用户空间指针if(!access_ok(VERIFY_READ,user_ptr,size))return-EFAULT;// 对于I/O映射地址if(!request_mem_region(phys_addr,size,"my_device"))return-EBUSY;错误处理:
· 复制函数返回未复制的字节数(0表示成功)
· 映射函数可能返回NULL或错误码
· 始终检查返回值
内存屏障:
// 在访问设备内存时需要iowrite32(value,addr);wmb();// 写内存屏障架构注意事项:
· 32位 vs 64位系统地址大小不同
· 大端序 vs 小端序系统
· 使用#ifdef CONFIG_64BIT等条件编译
四、调试工具
地址打印:
printk(KERN_INFO"Virtual address: %p\n",virt_addr);printk(KERN_INFO"Physical address: %pa\n",&phys_addr);printk(KERN_INFO"User address: %p\n",user_addr);地址检查:
#include<linux/vmalloc.h>// 检查地址是否在内核空间if(!is_vmalloc_addr(addr))// 是直接映射地址// 检查地址是否是有效用户空间地址if(!access_ok(VERIFY_READ,addr,size))// 无效地址
理解这些地址类型及其正确用法对于编写稳定、安全的内核代码至关重要。错误使用地址类型可能导致内核崩溃、安全漏洞或硬件损坏。
谢谢关注,后续会持续分享关于AI,GPU,Linux开发,操作系统,图形学,高性能计算,芯片行业讯息。欢迎感兴趣的伙伴关注微信公众号参与讨论沟通:
请关注公众号获取完整系统资料
请关注微信公众号:颇锐克科技共享