Linux 設備驅動開發中,內存分配與 I/O 訪問是兩大基石。本文系統梳理內核虛擬地址佈局、三大分配 API(kmalloc/get_free_page/vmalloc)差異、物理/虛擬地址轉換、I/O 端口與 I/O 內存訪問模型、ioremap 映射機制、以及 mmap 實現用戶態映射設備內存。
3GB (PAGE_OFFSET = 0xC0000000) ├── 物理內存線性映射區 (3GB ~ vmalloc_start) │ ├── 內核鏡像 │ ├── mem_map 頁框陣列 │ └── kmalloc/get_free_page 分配區(物理連續,邏輯地址 = 物理 + 3GB) ├── 8MB Gap(防越界) ├── vmalloc 區 (vmalloc_start ~ vmalloc_end) │ └── vmalloc/vmap 分配區(虛擬連續,物理不連續) ├── 4MB 持久內核映射區 ├── 固定映射區 └── 128KB 頂端保留區 4GB (0xFFFFFFFF)
| API | 物理連續 | 虛擬連續 | 地址轉換 | 適用場景 |
|---|---|---|---|---|
kmalloc | 是 | 是 | virt_to_phys() 簡單減法 | 小塊內存(<128KB)、DMA 緩衝區 |
get_free_page | 是 | 是 | 同 kmalloc | 頁級分配、DMA 大緩衝區 |
vmalloc | 否 | 是 | 需遍歷頁表 | 大塊內存(>128KB)、非 DMA |
#define __pa(x) ((unsigned long)(x) - PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
extern inline unsigned long virt_to_phys(volatile void *address) { return __pa(address); }
extern inline void *phys_to_virt(unsigned long address) { return __va(address); }
in/outLinux 統一抽象為 I/O 內存資源,驅動開發者無需關心底層差異。
void *ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void *addr);
vmalloc 區#define readb(addr) (*(volatile u8 *)__io_virt(addr))
#define readw(addr) (*(volatile u16 *)__io_virt(addr))
#define readl(addr) (*(volatile u32 *)__io_virt(addr))
#define writeb(v,addr) (*(volatile u8 *)__io_virt(addr) = (v))
#define writew(v,addr) (*(volatile u16 *)__io_virt(addr) = (v))
#define writel(v,addr) (*(volatile u32 *)__io_virt(addr) = (v))
ARM 等平台實現不同,但 API 統一,保證驅動可移植性。
#define ALMDAY bRTC(0x60)
#define ALMMON bRTC(0x64)
#define ALMYEAR bRTC(0x68)
#define bRTC(Nb) __REG(0x57000000 + (Nb))
#define __REG(x) io_p2v(x)
#define io_p2v(x) ((x) | 0xA0000000) // 物理→虛擬:置位 29-31bit
#define io_v2p(x) ((x) & ~0xA0000000) // 虛擬→物理:清位 29-31bit
這是 ARM 平台常見的「靜態映射」技巧:利用固定偏移(0xA0000000)直接計算虛擬地址,避免運行時 ioremap 開銷。但現代驅動推薦使用 ioremap + readl/writel。
驅動實現 file_operations.mmap,調用 remap_pfn_range 或 io_remap_pfn_range 將設備物理頁映射到用戶虛擬地址空間:
static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long pfn = phys_addr >> PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if (size > dev_mem_size) return -EINVAL;
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); // 設備內存不可緩存
return io_remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot);
}
kmalloc(小塊)、DMA 用 dma_alloc_coherent、大塊非 DMA 用 vmallocvirt_to_phys/phys_to_virt,vmalloc 區不可用ioremap + readb/writeb 族,保證跨架構可移植mmap 實現零拷貝用戶態訪問設備內存,需設置 pgprot_noncached