Linux内核设备内存映射:ioremap机制深度解析

前言

在Linux内核开发中,设备驱动程序经常需要与硬件设备进行通信。这些设备可能拥有自己的寄存器、缓冲区或其他内存区域,它们通常位于物理地址空间的特定位置。然而,内核代码运行在虚拟地址空间中,无法直接访问物理地址。这就产生了一个关键问题:如何让运行在虚拟地址空间的内核代码安全、高效地访问位于物理地址空间的设备资源?

ioremap 就是解决这一问题的核心技术。它就像一座精心设计的桥梁,将设备的物理内存"映射"到内核的虚拟地址空间,使得驱动程序能够像访问普通内存一样访问设备寄存器。

本文将深入剖析ioremap的完整工作机制,从映射的建立、使用到最终的释放,揭示Linux内核如何安全地架设这座连接物理设备与内核虚拟世界的桥梁。通过理解ioremap,您将掌握设备驱动开发中最核心的内存管理技术,并领略Linux内核在性能与安全性之间的精妙平衡。

将物理内存地址映射到内核虚拟地址空间ioremap

static inline void __iomem * ioremap(unsigned long offset, unsigned long size)
{
	return __ioremap(offset, size, 0);
}
void __iomem * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
	void __iomem * addr;
	struct vm_struct * area;
	unsigned long offset, last_addr;

	/* Don't allow wraparound or zero size */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr)
		return NULL;

	/*
	 * Don't remap the low PCI/ISA area, it's always mapped..
	 */
	if (phys_addr >= 0xA0000 && last_addr < 0x100000)
		return (void __iomem *) phys_to_virt(phys_addr);

	/*
	 * Don't allow anybody to remap normal RAM that we're using..
	 */
	if (phys_addr < virt_to_phys(high_memory)) {
		char *t_addr, *t_end;
		struct page *page;

		t_addr = __va(phys_addr);
		t_end = t_addr + (size - 1);
	   
		for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
			if(!PageReserved(page))
				return NULL;
	}

	/*
	 * Mappings have to be page-aligned
	 */
	offset = phys_addr & ~PAGE_MASK;
	phys_addr &= PAGE_MASK;
	size = PAGE_ALIGN(last_addr+1) - phys_addr;

	/*
	 * Ok, go for it..
	 */
	area = get_vm_area(size, VM_IOREMAP);
	if (!area)
		return NULL;
	area->phys_addr = phys_addr;
	addr = (void __iomem *) area->addr;
	if (remap_area_pages((unsigned long) addr, phys_addr, size, flags)) {
		vunmap((void __force *) addr);
		return NULL;
	}
	return (void __iomem *) (offset + (char __iomem *)addr);
}

函数功能

将物理内存地址(通常是设备寄存器或设备内存)映射到内核虚拟地址空间,使得内核可以访问这些设备资源

代码详细解释

ioremap 包装函数

static inline void __iomem * ioremap(unsigned long offset, unsigned long size)
{
	return __ioremap(offset, size, 0);
}
  • inline:内联函数,减少函数调用开销
  • void __iomem *:返回指向IO内存的指针,带有类型检查属性
  • 参数
    • offset:物理地址偏移
    • size:映射区域大小
  • 调用核心函数 __ioremap,flags参数为0

参数有效性检查

void __iomem * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
	void __iomem * addr;
	struct vm_struct * area;
	unsigned long offset, last_addr;

	/* Don't allow wraparound or zero size */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr)
		return NULL;
  • 变量声明
    • addr:返回的虚拟地址
    • area:vmalloc区域结构
    • offset:页内偏移量
    • last_addr:结束地址
  • 地址回绕检查
    • last_addr = phys_addr + size - 1:计算映射的最后一个字节地址
    • !size:大小为0
    • last_addr < phys_addr:检测地址回绕(如0xFFFFFFFF + 1 = 0)
  • 任一条件满足则返回NULL

PCI/ISA区域特殊处理

	/*
	 * Don't remap the low PCI/ISA area, it's always mapped..
	 */
	if (phys_addr >= 0xA0000 && last_addr < 0x100000)
		return (void __iomem *) phys_to_virt(phys_addr);
  • 检查范围:0xA0000 - 0xFFFFF
    • 这是传统的PCI/ISA设备内存区域
  • 直接映射:如果在这个范围内,使用 phys_to_virt 直接转换
    • 这个区域在内核初始化时已经建立了固定的1:1映射
    • 无需再次映射,直接返回对应的虚拟地址

普通RAM保护检查

	/*
	 * Don't allow anybody to remap normal RAM that we're using..
	 */
	if (phys_addr < virt_to_phys(high_memory)) {
		char *t_addr, *t_end;
		struct page *page;

		t_addr = __va(phys_addr);
		t_end = t_addr + (size - 1);
	   
		for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
			if(!PageReserved(page))
				return NULL;
	}
  • 检查是否在普通RAM内phys_addr < virt_to_phys(high_memory)

    • high_memory:直接映射区的结束地址
    • 如果物理地址在普通RAM范围内,需要进一步检查
  • 转换为虚拟地址

    • t_addr = __va(phys_addr):物理地址转虚拟地址
    • t_end = t_addr + (size - 1):计算结束虚拟地址
  • 页面检查循环

    • virt_to_page(t_addr):虚拟地址转换为page结构
    • 遍历映射区域内的所有页面
    • !PageReserved(page)
      • 如果发现任何非保留页面,返回NULL(不允许重新映射正在使用的RAM,除非是保留页面)

地址对齐处理

	/*
	 * Mappings have to be page-aligned
	 */
	offset = phys_addr & ~PAGE_MASK;
	phys_addr &= PAGE_MASK;
	size = PAGE_ALIGN(last_addr+1) - phys_addr;
  • 计算页内偏移offset = phys_addr & ~PAGE_MASK

    • 保存原始地址在页面内的偏移量,防止丢失具体的物理地址
    • 示例:phys_addr=0x12345678 → offset=0x678
  • 对齐物理地址phys_addr &= PAGE_MASK

    • 将物理地址向下对齐到页面边界
    • 示例:0x12345678 → 0x12345000
  • 重新计算大小size = PAGE_ALIGN(last_addr+1) - phys_addr

    • PAGE_ALIGN(last_addr+1):将结束地址向上对齐到页面边界
    • 计算对齐后的实际映射大小

分配虚拟地址区域

	/*
	 * Ok, go for it..
	 */
	area = get_vm_area(size, VM_IOREMAP);
	if (!area)
		return NULL;
	area->phys_addr = phys_addr;
	addr = (void __iomem *) area->addr;
  • 获取vmalloc区域get_vm_area(size, VM_IOREMAP)

    • vmalloc空间中分配指定大小的虚拟地址区域
    • VM_IOREMAP 标志表示这是IO映射
  • 错误检查:如果分配失败返回NULL

  • 保存物理地址area->phys_addr = phys_addr

    • vm_struct中记录物理地址
  • 获取虚拟地址addr = (void __iomem *) area->addr

    • 获取分配到的虚拟起始地址

建立页表映射

	if (remap_area_pages((unsigned long) addr, phys_addr, size, flags)) {
		vunmap((void __force *) addr);
		return NULL;
	}
  • 建立映射remap_area_pages((unsigned long) addr, phys_addr, size, flags)

    • 核心函数:建立虚拟地址到物理地址的页表映射
    • 参数:虚拟地址、物理地址、大小、标志
  • 错误处理:如果映射失败

    • vunmap((void __force *) addr):释放之前分配的虚拟地址区域
    • 返回NULL

返回最终地址

	return (void __iomem *) (offset + (char __iomem *)addr);
}
  • 计算最终地址offset + (char __iomem *)addr
    • 将页内偏移加回到虚拟地址上
  • 类型转换:转换为 void __iomem * 类型

总结

ioremap 函数是Linux内核设备驱动开发中的关键函数,它:

  1. 提供安全映射:多层检查确保映射的安全性
  2. 处理地址对齐:自动处理页面对齐问题
  3. 保护系统内存:防止误映射正在使用的RAM
  4. 支持设备访问:为设备驱动提供访问硬件的能力
  5. 资源管理:完善的错误处理和资源清理

建立物理地址到虚拟地址的页表映射remap_area_pages

static int remap_area_pages(unsigned long address, unsigned long phys_addr,
				 unsigned long size, unsigned long flags)
{
	int error;
	pgd_t * dir;
	unsigned long end = address + size;

	phys_addr -= address;
	dir = pgd_offset(&init_mm, address);
	flush_cache_all();
	if (address >= end)
		BUG();
	spin_lock(&init_mm.page_table_lock);
	do {
		pmd_t *pmd;
		pmd = pmd_alloc(&init_mm, dir, address);
		error = -ENOMEM;
		if (!pmd)
			break;
		if (remap_area_pmd(pmd, address, end - address,
					 phys_addr + address, flags))
			break;
		error = 0;
		address = (address + PGDIR_SIZE) & PGDIR_MASK;
		dir++;
	} while (address && (address < end));
	spin_unlock(&init_mm.page_table_lock);
	flush_tlb_all();
	return error;
}

函数功能

建立物理地址到虚拟地址的页表映射,用于ioremap等场景,将设备物理内存映射到内核虚拟地址空间

代码详细解释

参数初始化和计算

int error;
pgd_t * dir;
unsigned long end = address + size;

phys_addr -= address;
  • error:错误码变量,用于返回操作状态

  • dir:页全局目录指针,用于遍历页表

  • end:计算映射的结束虚拟地址

    • address + size:从起始地址加上大小得到结束地址
  • 关键操作phys_addr -= address

    • 计算的是物理地址相对于虚拟地址的偏移量
    • 转换公式:物理地址 = 虚拟地址 + (phys_addr - address)
    • 实际上相当于:offset = phys_addr - address
    • 这样在后续映射时,可以通过 虚拟地址 + offset 得到对应的物理地址

获取PGD和刷新缓存

dir = pgd_offset(&init_mm, address);
flush_cache_all();
if (address >= end)
	BUG();
  • 获取PGD指针pgd_offset(&init_mm, address)

    • init_mm:内核初始内存描述符
    • 根据虚拟地址找到对应的PGD条目指针
  • 刷新缓存flush_cache_all()

    • 在建立新映射前刷新所有CPU缓存
    • 在intel中是空操作,硬件自动保证缓存一致性
  • 边界检查if (address >= end) BUG()

    • 如果起始地址大于等于结束地址,触发内核错误
    • 这是严重的参数错误,不应该发生

加锁保护

spin_lock(&init_mm.page_table_lock);
  • 页表锁:保护页表操作不被并发访问
  • init_mm.page_table_lock:内核初始内存结构的页表自旋锁
  • 确保在修改页表时不会有其他CPU同时修改,防止页表损坏

循环处理每个PGD条目

do {
    pmd_t *pmd;
    pmd = pmd_alloc(&init_mm, dir, address);
    error = -ENOMEM;
    if (!pmd)
        break;
  • 循环开始:按PGD大小分块处理整个映射区域

  • 分配PMDpmd = pmd_alloc(&init_mm, dir, address)

    • 为当前PGD条目分配中间页目录
  • 错误码设置error = -ENOMEM

    • 预设错误码为内存不足,如果后续成功会重置为0
  • PMD分配检查if (!pmd) break

    • 如果PMD分配失败,跳出循环,保留error=-ENOMEM

建立PMD级别映射

    if (remap_area_pmd(pmd, address, end - address,
                 phys_addr + address, flags))
        break;
  • 核心映射函数remap_area_pmd(pmd, address, end - address, phys_addr + address, flags)

    • 参数分解
      • pmd:当前PMD指针
      • address:当前处理的虚拟起始地址
      • end - address:剩余需要映射的大小
      • phys_addr + address:计算对应的物理地址
        • 利用了之前的 phys_addr -= address 计算,这里``phys_addr `已经固化成最初物理地址和虚拟地址的偏移量
        • 现在给函数传的实际物理地址是这个偏移量加上当前每次对齐后的虚拟地址
      • flags:映射标志
  • 错误处理:如果remap_area_pmd返回非0(失败),跳出循环

成功处理和地址前进

    error = 0;
    address = (address + PGDIR_SIZE) & PGDIR_MASK;
    dir++;
} while (address && (address < end));
  • 重置错误码error = 0

    • 只有成功完成一个PGD块的处理后才设置成功
  • 地址前进address = (address + PGDIR_SIZE) & PGDIR_MASK

    • 前进一个PGD大小
    • 使用PGDIR_MASK确保地址对齐到PGD边界
  • 目录指针前进dir++

    • 移动到下一个PGD条目
  • 循环条件while (address && (address < end))

    • address:确保地址没有回绕到0
    • address < end:确保还没有处理完整个区域

清理和返回

spin_unlock(&init_mm.page_table_lock);
flush_tlb_all();
return error;
  • 释放锁spin_unlock(&init_mm.page_table_lock)

    • 页表操作完成,释放自旋锁
  • 刷新TLBflush_tlb_all()

    • 使所有CPU的TLB中旧的映射项失效
    • 确保新的页表映射立即生效
  • 返回错误码return error

    • 0表示成功,-ENOMEM表示内存不足

总结

remap_area_pages 函数是ioremap机制的核心,它:

  1. 建立设备映射:将设备物理内存映射到内核虚拟空间
  2. 处理地址转换:通过巧妙的偏移计算处理任意虚拟-物理地址对应
  3. 保证安全性:完善的锁保护和错误处理
  4. 维护一致性:适当的缓存和TLB刷新
  5. 支持大内存:分层处理大块设备内存映射

在PMD级别建立物理地址到虚拟地址的页表映射remap_area_pmd

static inline int remap_area_pmd(pmd_t * pmd, unsigned long address, unsigned long size,
	unsigned long phys_addr, unsigned long flags)
{
	unsigned long end;

	address &= ~PGDIR_MASK;
	end = address + size;
	if (end > PGDIR_SIZE)
		end = PGDIR_SIZE;
	phys_addr -= address;
	if (address >= end)
		BUG();
	do {
		pte_t * pte = pte_alloc_kernel(&init_mm, pmd, address);
		if (!pte)
			return -ENOMEM;
		remap_area_pte(pte, address, end - address, address + phys_addr, flags);
		address = (address + PMD_SIZE) & PMD_MASK;
		pmd++;
	} while (address && (address < end));
	return 0;
}

函数功能

在PMD级别建立物理地址到虚拟地址的页表映射,管理中间页目录并调用PTE级别的映射函数

代码详细解释

参数声明和地址计算

unsigned long end;

address &= ~PGDIR_MASK;
end = address + size;
if (end > PGDIR_SIZE)
	end = PGDIR_SIZE;
  • end:局部变量,存储当前PMD内的结束地址

  • 地址对齐address &= ~PGDIR_MASK

    • 得到在PGD内的偏移
  • 计算结束地址end = address + size

    • 计算在当前PMD内需要处理的结束地址
  • 边界检查if (end > PGDIR_SIZE) end = PGDIR_SIZE

    • 确保结束地址不超过一个PGD的大小
    • 防止跨PGD边界的越界访问

物理地址偏移计算和边界检查

phys_addr -= address;
if (address >= end)
	BUG();
  • 物理地址偏移计算phys_addr -= address

    • 计算的是物理地址相对于虚拟地址的偏移量
  • 边界检查if (address >= end) BUG()

    • 检查起始地址是否大于等于结束地址
    • 如果成立,说明参数错误,触发内核BUG
    • 这是防御性编程,确保参数合理性

循环处理PMD条目

do {
    pte_t * pte = pte_alloc_kernel(&init_mm, pmd, address);
    if (!pte)
        return -ENOMEM;
  • 循环开始:使用do-while循环处理PMD内的所有PTE

  • 分配PTE表pte_alloc_kernel(&init_mm, pmd, address)

    • 参数
      • &init_mm:内核内存描述符
      • pmd:当前PMD指针
      • address:虚拟地址(用于计算PTE索引)
    • 功能:为PMD分配页表项数组(PTE表)
  • 错误检查if (!pte) return -ENOMEM

    • 如果PTE表分配失败,立即返回内存不足错误
    • 错误码-ENOMEM表示无法分配所需内存

调用PTE级别映射

    remap_area_pte(pte, address, end - address, address + phys_addr, flags);
  • 核心映射调用remap_area_pte(pte, address, end - address, address + phys_addr, flags)

    • 参数分解
      • pte:PTE表指针
      • address:当前虚拟起始地址
      • end - address:剩余需要映射的大小
      • address + phys_addr:计算对应的物理地址
      • flags:映射标志(如缓存策略等)
  • 注意:这个函数没有错误检查,假设PTE级别映射总是成功

地址前进和循环继续

    address = (address + PMD_SIZE) & PMD_MASK;
    pmd++;
} while (address && (address < end));
  • 地址前进address = (address + PMD_SIZE) & PMD_MASK

    • 前进一个PMD大小
    • 使用PMD_MASK确保地址对齐到PMD边界
  • PMD指针前进pmd++

    • 移动到下一个PMD条目
    • 在页表中前进到下一个中间页目录项
  • 循环条件while (address && (address < end))

    • address:确保地址没有回绕到0(安全性检查)
    • address < end:确保还没有处理完PMD内的指定范围

成功返回

return 0;
  • 返回成功:函数执行完成,返回0表示成功
  • 只有在PTE分配失败时才会提前返回错误

vmalloc映射的区别

ioremap (remap_area_pmd)

// 物理地址连续(设备内存)
// 使用公式计算:物理地址 = 虚拟地址 + 固定偏移
// 需要特殊的设备内存属性

vmalloc (map_area_pmd)

// 物理地址不连续
// 使用页面指针数组:物理地址 = pages[i]
// 使用标准的内存属性

在PTE级别建立物理地址到虚拟地址的页表映射remap_area_pte

static inline void remap_area_pte(pte_t * pte, unsigned long address, unsigned long size,
	unsigned long phys_addr, unsigned long flags)
{
	unsigned long end;
	unsigned long pfn;

	address &= ~PMD_MASK;
	end = address + size;
	if (end > PMD_SIZE)
		end = PMD_SIZE;
	if (address >= end)
		BUG();
	pfn = phys_addr >> PAGE_SHIFT;
	do {
		if (!pte_none(*pte)) {
			printk("remap_area_pte: page already exists\n");
			BUG();
		}
		set_pte(pte, pfn_pte(pfn, __pgprot(_PAGE_PRESENT | _PAGE_RW | 
					_PAGE_DIRTY | _PAGE_ACCESSED | flags)));
		address += PAGE_SIZE;
		pfn++;
		pte++;
	} while (address && (address < end));
}

函数功能

在PTE级别建立物理地址到虚拟地址的页表映射,这是ioremap映射机制的最底层函数,直接设置页表项

代码详细解释

参数声明和地址计算

unsigned long end;
unsigned long pfn;

address &= ~PMD_MASK;
end = address + size;
if (end > PMD_SIZE)
	end = PMD_SIZE;
  • end:当前PMD内的结束地址

  • pfn:页帧号(Page Frame Number),物理页面的编号

  • 地址对齐address &= ~PMD_MASK

    • 获取在PMD中的偏移
  • 计算结束地址end = address + size

    • 计算在当前PTE表内需要处理的结束地址
  • 边界检查if (end > PMD_SIZE) end = PMD_SIZE

    • 确保结束地址不超过一个PMD的大小
    • 防止跨PMD边界的越界访问

边界检查和PFN计算

if (address >= end)
	BUG();
pfn = phys_addr >> PAGE_SHIFT;
  • 边界检查if (address >= end) BUG()

    • 检查起始地址是否大于等于结束地址
  • PFN计算pfn = phys_addr >> PAGE_SHIFT

    • 关键操作:将物理地址转换为页帧号
    • 计算原理:物理页号 = 物理地址 / 页面大小
    • PFN用于在页表项中标识物理页面

循环处理每个PTE条目

do {
    if (!pte_none(*pte)) {
        printk("remap_area_pte: page already exists\n");
        BUG();
    }
  • 循环开始:使用do-while循环处理PTE表内的所有条目

  • PTE占用检查if (!pte_none(*pte))

    • pte_none(*pte):检查当前PTE条目是否为空
    • !pte_none:如果PTE不为空,说明该页表项已经被使用
  • 错误处理

    • printk("remap_area_pte: page already exists\n"):打印错误信息
    • BUG():触发内核错误,停止系统运行
    • 原因:PTE被意外占用是严重错误,可能表示内存管理混乱

设置页表项

    set_pte(pte, pfn_pte(pfn, __pgprot(_PAGE_PRESENT | _PAGE_RW | 
                _PAGE_DIRTY | _PAGE_ACCESSED | flags)));
  • 核心映射操作:这是建立页表映射的关键步骤

  • 构造页表项pfn_pte(pfn, __pgprot(...))

    • pfn_pte(pfn, pgprot):将页帧号和页面保护标志组合成页表项
    • __pgprot(flags):创建页面保护标志
  • 页面保护标志

    • _PAGE_PRESENT:页面存在(必须设置)
    • _PAGE_RW:页面可读写
    • _PAGE_DIRTY:页面已被写入(便于页面换出处理)
    • _PAGE_ACCESSED:页面已被访问(便于页面换出处理)
    • flags:传入的额外标志
  • 设置PTEset_pte(pte, ...)

    • 将构造好的页表项写入PTE位置
    • 实际建立虚拟地址到物理页面的映射

前进到下一个条目

    address += PAGE_SIZE;
    pfn++;
    pte++;
} while (address && (address < end));
  • 地址前进address += PAGE_SIZE

    • 每次前进一个页面大小(4KB)
    • 移动到下一个虚拟页面
  • PFN前进pfn++

    • 页帧号加1,指向下一个物理页面
    • 这建立了连续的虚拟地址到连续的物理地址的映射
  • PTE指针前进pte++

    • 移动到PTE表中的下一个页表项
    • 准备设置下一个映射
  • 循环条件while (address && (address < end))

    • address:确保地址没有回绕到0(安全性检查)
    • address < end:确保还没有处理完PTE表内的指定范围

取消通过ioremap建立的设备内存映射iounmap

void iounmap(volatile void __iomem *addr)
{
	struct vm_struct *p;
	if ((void __force *) addr <= high_memory) 
		return; 
	p = remove_vm_area((void *) (PAGE_MASK & (unsigned long __force) addr));
	if (!p) { 
		printk("__iounmap: bad address %p\n", addr);
		return;
	} 

	if (p->flags && p->phys_addr < virt_to_phys(high_memory)) { 
		change_page_attr(virt_to_page(__va(p->phys_addr)),
				 p->size >> PAGE_SHIFT,
				 PAGE_KERNEL); 				 
		global_flush_tlb();
	} 
	kfree(p); 
}

函数功能

取消通过ioremap建立的设备内存映射,释放相关的虚拟地址空间资源

代码详细解释

直接映射区检查

struct vm_struct *p;
if ((void __force *) addr <= high_memory) 
	return; 
  • 变量声明p 用于存储找到的vmalloc区域指针

  • 直接映射区检查if ((void __force *) addr <= high_memory)

    • high_memory:直接映射区的结束地址(通常是896MB边界)
    • (void __force *) addr:强制类型转换,忽略IO地址空间属性
    • 条件含义:如果地址在直接映射区内
  • 直接返回return

    • 原因:直接映射区的地址是固定映射,不需要也不应该通过iounmap来取消

移除虚拟内存区域

p = remove_vm_area((void *) (PAGE_MASK & (unsigned long __force) addr));
if (!p) { 
	printk("__iounmap: bad address %p\n", addr);
	return;
} 
  • 地址对齐处理(void *) (PAGE_MASK & (unsigned long __force) addr)

    • PAGE_MASK & addr:将地址向下对齐到页面边界
    • 原因vmalloc区域是按页面管理的,需要基地址
  • 移除虚拟区域remove_vm_area(...)

    • vmlist链表中查找并移除对应的vm_struct
    • 同时取消该区域的页表映射
    • 返回找到的vm_struct指针
  • 错误检查if (!p)

    • 如果返回NULL,说明地址不对应任何ioremap区域
    • 错误处理
      • printk("__iounmap: bad address %p\n", addr):打印错误信息
      • return:直接返回,不继续执行

普通RAM映射的特殊处理

if (p->flags && p->phys_addr < virt_to_phys(high_memory)) { 
	change_page_attr(virt_to_page(__va(p->phys_addr)),
			 p->size >> PAGE_SHIFT,
			 PAGE_KERNEL); 				 
	global_flush_tlb();
} 
  • 条件检查if (p->flags && p->phys_addr < virt_to_phys(high_memory))

    • p->flags:检查是否有特殊标志(VM_IOREMAP等)
    • p->phys_addr < virt_to_phys(high_memory):检查物理地址是否在普通RAM范围内
    • 含义:如果映射的是普通RAM(而非设备内存)
  • 修改页面属性change_page_attr(...)

    • 参数
      • virt_to_page(__va(p->phys_addr)):物理地址转虚拟地址再转page结构
      • p->size >> PAGE_SHIFT:计算页面数量
      • PAGE_KERNEL:恢复为标准内核页面属性
    • 目的:将特殊的IO映射属性恢复为普通内核内存属性
  • 刷新TLBglobal_flush_tlb()

    • 使所有CPU的TLB中旧的映射项失效
    • 确保新的页面属性立即生效

资源释放

kfree(p); 
  • 释放vm_structkfree(p)
    • 释放之前分配的vm_struct结构体内存
    • 这个结构是在ioremap时通过kmalloc分配的
    • 完成资源的彻底清理

总结

iounmap 函数提供了安全、完整的ioremap映射取消机制:

  1. 安全性:多层检查防止误操作
  2. 完整性:正确处理各种类型的映射(设备内存 vs 普通RAM)
  3. 性能:适当的TLB刷新保证内存一致性
  4. 资源管理:彻底释放所有相关数据结构
  5. 错误恢复:完善的错误检测和报告

全局刷新所有CPU的TLB和缓存global_flush_tlb

void global_flush_tlb(void)
{ 
	LIST_HEAD(l);
	struct list_head* n;

	BUG_ON(irqs_disabled());

	spin_lock_irq(&cpa_lock);
	list_splice_init(&df_list, &l);
	spin_unlock_irq(&cpa_lock);
	flush_map();
	n = l.next;
	while (n != &l) {
		struct page *pg = list_entry(n, struct page, lru);
		n = n->next;
		__free_page(pg);
	}
}
static inline void flush_map(void)
{
	on_each_cpu(flush_kernel_map, NULL, 1, 1);
}
static inline int on_each_cpu(void (*func) (void *info), void *info,
			      int retry, int wait)
{
	int ret = 0;

	preempt_disable();
	ret = smp_call_function(func, info, retry, wait);
	func(info);
	preempt_enable();
	return ret;
}
int smp_call_function (void (*func) (void *info), void *info, int nonatomic,
			int wait)
{
	struct call_data_struct data;
	int cpus = num_online_cpus()-1;

	if (!cpus)
		return 0;

	/* Can deadlock when called with interrupts disabled */
	WARN_ON(irqs_disabled());

	data.func = func;
	data.info = info;
	atomic_set(&data.started, 0);
	data.wait = wait;
	if (wait)
		atomic_set(&data.finished, 0);

	spin_lock(&call_lock);
	call_data = &data;
	mb();
	
	/* Send a message to all other CPUs and wait for them to respond */
	send_IPI_allbutself(CALL_FUNCTION_VECTOR);

	/* Wait for response */
	while (atomic_read(&data.started) != cpus)
		cpu_relax();

	if (wait)
		while (atomic_read(&data.finished) != cpus)
			cpu_relax();
	spin_unlock(&call_lock);

	return 0;
}
static void flush_kernel_map(void *dummy) 
{ 
	/* Could use CLFLUSH here if the CPU supports it (Hammer,P4) */
	if (boot_cpu_data.x86_model >= 4) 
		asm volatile("wbinvd":::"memory"); 
	/* Flush all to work around Errata in early athlons regarding 
	 * large page flushing. 
	 */
	__flush_tlb_all(); 	
}

函数功能

全局刷新所有CPU的TLB和缓存,确保内存映射变化对所有处理器可见

代码详细解释

global_flush_tlb 函数

void global_flush_tlb(void)
{ 
	LIST_HEAD(l);
	struct list_head* n;

	BUG_ON(irqs_disabled());
  • 变量声明

    • LIST_HEAD(l):创建本地链表头,用于临时存储延迟释放的页面
    • struct list_head* n:链表遍历指针
  • 中断状态检查BUG_ON(irqs_disabled())

    • 检查当前是否在中断禁用状态下
    • 原因:这个函数可能睡眠,不能在原子上下文中调用
	spin_lock_irq(&cpa_lock);
	list_splice_init(&df_list, &l);
	spin_unlock_irq(&cpa_lock);
  • 加锁保护spin_lock_irq(&cpa_lock)

    • 获取CPA(Change Page Attribute)锁,并禁用本地中断
    • 保护全局数据结构 df_list(延迟释放列表)
  • 转移链表list_splice_init(&df_list, &l)

    • 将全局延迟释放列表 df_list 的所有节点移动到本地链表 l
    • list_splice_init 同时清空原链表
  • 释放锁spin_unlock_irq(&cpa_lock)

    • 释放锁并恢复中断状态
	flush_map();
  • 刷新映射:调用 flush_map() 刷新所有CPU的TLB和缓存
	n = l.next;
	while (n != &l) {
		struct page *pg = list_entry(n, struct page, lru);
		n = n->next;
		__free_page(pg);
	}
}
  • 遍历链表n = l.next 获取链表第一个节点
  • 循环释放页面
    • list_entry(n, struct page, lru):从链表节点获取page结构指针
    • n = n->next:移动到下一个节点
    • __free_page(pg):释放物理页面回伙伴系统
  • 循环条件while (n != &l) 直到遍历完整个链表

flush_map 函数

static inline void flush_map(void)
{
	on_each_cpu(flush_kernel_map, NULL, 1, 1);
}
  • 跨CPU调用on_each_cpu(flush_kernel_map, NULL, 1, 1)
    • 参数
      • flush_kernel_map:要在每个CPU上执行的函数
      • NULL:传递给函数的参数
      • 1:retry参数(重试次数)
      • 1:wait参数(等待完成)

on_each_cpu 函数

static inline int on_each_cpu(void (*func) (void *info), void *info,
			      int retry, int wait)
{
	int ret = 0;

	preempt_disable();
  • 参数

    • func:要在每个CPU上执行的函数指针
    • info:传递给函数的参数
    • retry:重试次数(当前未使用)
    • wait:是否等待所有CPU完成
  • 禁用抢占preempt_disable()

    • 防止进程被调度到其他CPU执行
	ret = smp_call_function(func, info, retry, wait);
	func(info);
	preempt_enable();
	return ret;
}
  • 调用其他CPUsmp_call_function(func, info, retry, wait)

    • 通过IPI(处理器间中断)让其他所有CPU执行func函数
  • 本地执行func(info)

    • 当前CPU也执行相同的函数
  • 恢复抢占preempt_enable()

    • 重新允许进程调度

smp_call_function 函数

int smp_call_function (void (*func) (void *info), void *info, int nonatomic,
			int wait)
{
	struct call_data_struct data;
	int cpus = num_online_cpus()-1;

	if (!cpus)
		return 0;
  • 参数说明

    • func:要执行的函数,必须快速且非阻塞
    • info:传递给函数的任意指针
    • nonatomic:当前未使用
    • wait:如果为真,等待其他CPU完成
  • 在线CPU检查int cpus = num_online_cpus()-1

    • 计算需要通知的其他CPU数量(排除当前CPU)
  • 单CPU检查if (!cpus) return 0

    • 如果没有其他在线CPU,直接返回
	/* Can deadlock when called with interrupts disabled */
	WARN_ON(irqs_disabled());

	data.func = func;
	data.info = info;
	atomic_set(&data.started, 0);
	data.wait = wait;
	if (wait)
		atomic_set(&data.finished, 0);
  • 中断状态警告WARN_ON(irqs_disabled())

    • 如果中断被禁用,发出警告
  • 初始化调用数据

    • data.func = func:设置要执行的函数
    • data.info = info:设置函数参数
    • atomic_set(&data.started, 0):初始化启动计数器
    • data.wait = wait:设置等待标志
    • 如果需要等待,初始化完成计数器
	spin_lock(&call_lock);
	call_data = &data;
	mb();
	
	/* Send a message to all other CPUs and wait for them to respond */
	send_IPI_allbutself(CALL_FUNCTION_VECTOR);
  • 加锁spin_lock(&call_lock) 保护全局调用数据
  • 设置全局数据call_data = &data 让其他CPU可以访问
  • 内存屏障mb() 确保写入对其他CPU可见
  • 发送IPIsend_IPI_allbutself(CALL_FUNCTION_VECTOR)
    • 向所有其他CPU发送处理器间中断
	/* Wait for response */
	while (atomic_read(&data.started) != cpus)
		cpu_relax();

	if (wait)
		while (atomic_read(&data.finished) != cpus)
			cpu_relax();
	spin_unlock(&call_lock);

	return 0;
}
  • 等待启动while (atomic_read(&data.started) != cpus)

    • 等待所有其他CPU开始执行函数
    • cpu_relax():降低CPU功耗的忙等待
  • 等待完成:如果设置了wait,等待所有CPU完成执行

  • 释放锁spin_unlock(&call_lock)

  • 返回成功return 0

flush_kernel_map 函数

static void flush_kernel_map(void *dummy) 
{ 
	/* Could use CLFLUSH here if the CPU supports it (Hammer,P4) */
	if (boot_cpu_data.x86_model >= 4) 
		asm volatile("wbinvd":::"memory"); 
	/* Flush all to work around Errata in early athlons regarding 
	 * large page flushing. 
	 */
	__flush_tlb_all(); 	
}
  • CPU型号检查if (boot_cpu_data.x86_model >= 4)

    • 检查CPU型号是否>=4(特定x86架构)
  • 写回并使缓存无效asm volatile("wbinvd":::"memory")

    • wbinvd指令:将缓存数据写回内存并使缓存无效
    • volatile:防止编译器优化
    • memory:编译器内存屏障
  • 刷新TLB__flush_tlb_all()

    • 体系结构相关的函数,刷新整个TLB
    • 使所有页表项缓存失效

总结

global_flush_tlb 提供了完整的系统范围内存一致性保证:

  1. 全局一致性:确保所有CPU看到相同的内存映射状态
  2. 缓存一致性:刷新CPU缓存和TLB
  3. 安全释放:在释放页面前确保所有映射已失效
  4. 正确同步:使用IPI和原子操作实现跨CPU同步
  5. 错误防护:多层检查防止在错误上下文中调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

---学无止境---

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

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

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

打赏作者

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

抵扣说明:

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

余额充值