本文采用 Linux 内核 v3.10 版本 x86_64架构
一、前言
任何系统都免不了要有输入/输出,所以对 I/O 设备的访问是 CPU 的一个重要功能。一般来说,对 I/O 设备的访问有两种不同的形式:
- 通过端口映射(Port-mapped I/O,PMIO);
- 通过内存映射(Memory-Mapped I/O,MMIO);
在采用内存映射的方式时, I/O 设备的存储单元,如控制寄存器、数据寄存器、状态寄存器等等,是作为内存的一部分出现在系统中的。CPU 可以像访问内存单元一样访问外部设备的存储单元,所以不需要专门用于外设的 I/O 指令。而采用端口映射的系统则不同,外部设备的存储单元所在的地址空间与内存分属于两个不同的体系。访问内存的指令不能用来访问外部设备的存储单元,所以在 x86 架构的 CPU 中设立了专门的 IN 和 OUT 指令。
两种映射方式在地址空间上的比较:
二、早期 ioremap 初始化
I/O 内存映射将设备的寄存器和内存映射到主存地址空间。内核提供了 ioremap 函数执行此类操作,换句话说,ioremap 将 I/O 物理内存区域映射到内核虚拟内存空间以使内核可以访问它们。
但是,ioremap 函数需要 vmalloc 功能支持;在内核启动早期,vmalloc 功能尚未完成初始化,此时无法使用 ioremap 函数。为了能够在内核启动早期就可以通过 I/O 内存映射来访问 I/O设备,内核提供了 early_ioremap 函数来实现该功能。另外,在使用 early_ioremap 函数之前,需要对其使用的内存区域进行初始化。
特别提示:下文涉及大量的分页以及固定映射(Fixmap)相关内容(概念、数据结构、APIs 等),在阅读过程中可参考 Linux Kernel:内存管理之分页(Paging) 以及 Linux Kernel:内存管理之固定映射 (Fixmap) 这两篇文章。
2.1 early_ioremap_init
在 setup_arch 函数中,通过调用 early_ioremap_init 函数,来进行早期 ioremap 的初始化。
// file: arch/x86/kernel/setup.c
/*
* setup_arch - architecture-specific boot-time initializations
*
* Note: On x86_64, fixmaps are ready for use even before this is called.
*/
void __init setup_arch(char **cmdline_p)
{
...
early_ioremap_init();
...
}
early_ioremap_init 函数定义如下:
// file: arch/x86/mm/ioremap.c
void __init early_ioremap_init(void)
{
pmd_t *pmd;
int i;
if (early_ioremap_debug)
printk(KERN_INFO "early_ioremap_init()\n");
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
memset(bm_pte, 0, sizeof(bm_pte));
pmd_populate_kernel(&init_mm, pmd, bm_pte);
/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
#define __FIXADDR_TOP (-PAGE_SIZE)
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
#undef __FIXADDR_TOP
if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
printk(KERN_WARNING "pmd %p != %p\n",
pmd, early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)));
printk(KERN_WARNING "fix_to_virt(FIX_BTMAP_BEGIN): %08lx\n",
fix_to_virt(FIX_BTMAP_BEGIN));
printk(KERN_WARNING "fix_to_virt(FIX_BTMAP_END): %08lx\n",
fix_to_virt(FIX_BTMAP_END));
printk(KERN_WARNING "FIX_BTMAP_END: %d\n", FIX_BTMAP_END);
printk(KERN_WARNING "FIX_BTMAP_BEGIN: %d\n",
FIX_BTMAP_BEGIN);
}
}
函数内部,首先声明了 pmd_t 类型的指针变量 pmd 以及 int 类型的变量 i。
pmd_t *pmd;
int i;
pmd_t 表示中级页目录项,是一种结构体类型,其定义如下:
// file: arch/x86/include/asm/pgtable_types.h
typedef struct { pmdval_t pmd; } pmd_t;
其结构体成员 pmd 为 pmdval_t 类型,pmdval_t 是 unsigned long 的别名。
// file: arch/x86/include/asm/pgtable_64_types.h
typedef unsigned long pmdval_t;
接下来,检查变量 early_ioremap_debug,决定是否要打印调试信息。
if (early_ioremap_debug)
printk(KERN_INFO "early_ioremap_init()\n");
early_ioremap_debug是一个静态变量,由于未进行显式初始化,其默认值为 0。
// file: arch/x86/mm/ioremap.c
static int __initdata early_ioremap_debug;
通过命令行参数 early_ioremap_debug,可以修改上述变量的值。
// file: arch/x86/mm/ioremap.c
static int __init early_ioremap_debug_setup(char *str)
{
early_ioremap_debug = 1;
return 0;
}
early_param("early_ioremap_debug", early_ioremap_debug_setup);
可以看到,当设置了 early_ioremap_debug 参数后,参数处理函数 early_ioremap_debug_setup 会将变量的值设置为 1。
我们在前文提到过,在进行早期 ioremap 时,内核的内存管理子系统还没有准备好,没办法通过 vmalloc 为 I/O 映射分配虚拟内存。所以,在系统启动阶段,内核在固定映射区(Fixmap)内,为早期的 ioremap 分配了一段内存空间(从 FIX_BTMAP_BEGIN 到 FIX_BTMAP_END),即 Fixmap 中的临时映射区。
临时映射区和 Fixmap 相关内容,请参考:Linux Kernel:内存管理之固定映射 (Fixmap)。
接下来,对临时映射区进行初始化。
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
临时映射区分为 FIX_BTMAPS_SLOTS(扩展为 4) 个槽,每个槽 NR_FIX_BTMAPS (扩展为 64) 个元素,所以临时映射区总共可以容纳 TOTAL_FIX_BTMAPS (256) 个元素。每个元素对应着 Fixmap 区域中的一个页(4K),所以总大小为 256个页,即 1MB。
// file: arch/x86/include/asm/fixmap.h
/*
* 256 temporary boot-time mappings, used by early_ioremap(),
* before ioremap() is functional.
*
* If necessary we round it up to the next 256 pages boundary so
* that we can have a single pgd entry and a single pte table:
*/
#define NR_FIX_BTMAPS 64
#define FIX_BTMAPS_SLOTS 4
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
内核使用 slot_virt数组来表示临时映射区的空间,该数组有 FIX_BTMAPS_SLOTS(扩展为 4) 个元素,对应着临时映射区的 4 个槽。
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;
FIX_BTMAP_BEGIN 和 FIX_BTMAP_END 分别是临时映射区的起始索引和结束索引,__fix_to_virt用于将 Fixmap 中的索引转换为对应的虚拟地址。相关内容在 Fixmap 中有详细讲解,本文不再赘述。
FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i 获取到每个槽的起始索引,然后通过宏 __fix_to_virt将索引值转换成虚拟地址。for 循环执行完成后,slot_virt里保存的是每个槽区的起始地址。
此时,临时映射区示意图如下:
接下来,通过 early_ioremap_pmd 函数计算临时映射区起始地址对应的中层页目录项地址。
pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));
上文说过,FIX_BTMAP_BEGIN是临时映射区起始索引;fix_to_virt 函数把索引值转换为虚拟地址,所以,fix_to_virt(FIX_BTMAP_BEGIN)就得到了临时映射区的起始虚拟地址。
early_ioremap_pmd 函数会计算指定虚拟地址所对应的中层页目录项地址,具体实现见 early_ioremap_pmd 节。
再接着,通过 memset 函数将将变量 bm_pte 初始化为 0。
memset(bm_pte, 0, sizeof(bm_pte));
变量bm_pte 定义如下:
static pte_t bm_pte[PAGE_SIZE/sizeof(pte_t)] __page_aligned_bss;
可以看到,bm_pte 是 pte_t 类型的数组,PAGE_SIZE/sizeof(pte_t) 表示每页能容纳的 pte_t类型数据的数量。 我们知道,每个页表大小为 4KB,也就是一个 PAGE_SIZE 大小,而pte_t 代表的是页表项,所以 bm_pte 实际是一张页表。将 bm_pte 初始化为 0,意味着该页表中的每个页表项均为 0,都是无效页表项。
我们再来分析下宏 __page_aligned_bss 的作用,该宏定义如下:
// file: include/linux/linkage.h
#define __page_aligned_bss __section(.bss..page_aligned) __aligned(PAGE_SIZE)
__page_aligned_bss 内部又引用了宏 __section 和 __aligned ,他们分别定义如下:
// file: include/linux/compiler.h
# define __section(S) __attribute__ ((__section__(#S)))
// file: include/linux/compiler-gcc.h
#define __aligned(x) __attribute__((aligned(x)))
综上所述,宏 __page_aligned_bss 指示编译器将数组 bm_pte放入 .bss..page_aligned 节中,并对齐到页大小。
再下来,执行
pmd_populate_kernel(&init_mm, pmd, bm_pte);
现在我们已经得到了中层页目录项地址 pmd,页表基地址 bm_pte ,通过pmd_populate_kernel 函数,将 bm_pte的物理地址及页属性组合成表项数据,并写入 中层页目录项 pmd 中。pmd_populate_kernel 函数实现,参考这篇文章。
此行代码执行后,分页结构及变量 pmd 与 bm_pte 关系如下:
由于在(早期)页表初始化时,已经为 Fixmap 区域建立了各级页表,此步执行完后,各级页表结构如下图所示:
再来看下面一段代码:
/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
#define __FIXADDR_TOP (-PAGE_SIZE)
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));
#undef __FIXADDR_TOP
其中,宏 __FIXADDR_TOP 在 32 位系统才会用到,可以忽略。
接下来检查临时映射区是否在同一个中层页目录项里。检查方法就是将临时映射区的起始虚拟地址和结束地址,全部右移 PMD_SHIFT(扩展为 21)位,然后比较移位后的值是否相等。如果相等,则属于同一个中层页目录项,条件为假;否则,跨越了多个中层页目录项,条件为真。
宏 BUILD_BUG_ON 会在编译时检查给定条件是否为真,如果条件为真,会在打印错误信息后将进程挂起。该宏的实现细节,我们在 Linux Kernel:中断和异常处理程序的早期初始化(续) 的第 2.2 节详细介绍过,此处不再赘述。
最后一段代码如下:
if (pmd != early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
printk(KERN_WARNING "pmd %p != %p\n",
pmd, early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)));
...
}
变量pmd 是通过 pmd = early_ioremap_pmd(fix_to_virt(FIX_BTMAP_BEGIN));得到的,是临时映射区起始地址所对应的中层页目录项地址;early_ioremap_pmd(fix_to_virt(FIX_BTMAP_END)得到的是临时映射区结束地址所对应的中层页目录项的地址。如果两者不相等,说明临时映射区跨越了多个中层页目录项,就会打印警告信息。
至此,完成了 ioreamp 的早期初始化工作。
2.1.1 early_ioremap_pmd
early_ioremap_pmd 函数接收虚拟地址作为参数,返回虚拟地址对应的中层页目录项地址,其定义如下:
// file: arch/x86/mm/ioremap.c
static inline pmd_t * __init early_ioremap_pmd(unsigned long addr)
{
/* Don't assume we're using swapper_pg_dir at this point */
pgd_t *base = __va(read_cr3());
pgd_t *pgd = &base[pgd_index(addr)];
pud_t *pud = pud_offset(pgd, addr);
pmd_t *pmd = pmd_offset(pud, addr);
return pmd;
}
第一行代码,获取到全局页目录的虚拟地址。
pgd_t *base = __va(read_cr3());
我们知道,控制寄存器 CR3 中保存着顶级页表,即全局页目录(Page Global Directory,PGD)的物理地址。顾名思义,read_cr3()函数读取控制寄存器 CR3 的值。read_cr3 函数内部调用了 native_read_cr3 函数来实现读取功能。
// file: arch/x86/include/asm/special_insns.h
static inline unsigned long read_cr3(void)
{
return native_read_cr3();
}
native_read_cr3 使用内联汇编来读取 CR3 的值。
// file: arch/x86/include/asm/special_insns.h
static inline unsigned long native_read_cr3(void)
{
unsigned long val;
asm volatile("mov %%cr3,%0\n\t" : "=r" (val), "=m" (__force_order));
return val;
}
内联汇编代码就是一个简单的 mov 指令,但是我们看到在输出操作数中,除了需要的寄存器值外,还多了一个内存操作数 __force_order。
// file: arch/x86/include/asm/special_insns.h
/*
* Volatile isn't enough to prevent the compiler from reordering the
* read/write functions for the control registers and messing everything up.
* A memory clobber would solve the problem, but would prevent reordering of
* all loads stores around it, which can hurt performance. Solution is to
* use a variable and mimic reads and writes to it to enforce serialization
*/
static unsigned long __force_order;
之所以要增加一个变量,是为了阻止编译器的重排序。具体可参考变量注释。
通过 read_cr3() 函数获取到全局页目录的物理地址后,通过宏 __va 将物理地址转换成虚拟地址,该虚拟地址就是全局页目录的基地址。
接下来执行
pgd_t *pgd = &base[pgd_index(addr)];
pgd_index(addr) 获取到虚拟地址对应的全局页目录项索引;base[pgd_index(addr)]获取到全局页目录项的值;而 &base[pgd_index(addr)] 获取到全局页目录项的虚拟地址,并赋值给指针变量 pgd。
再接着,执行
pud_t *pud = pud_offset(pgd, addr);
pmd_t *pmd = pmd_offset(pud, addr);
return pmd;
pud_offset 和 pmd_offset 函数分别获取到虚拟地址 addr对应的上层页目录项的虚拟地址和中层页目录项的虚拟地址。
最后,把中层页目录项地址 pmd 返回。
early_ioremap_pmd 中所用到函数的具体实现,请参考这篇文章。
三、APIs
在早期 ioremap 初始化完成后,我们就可以使用它了。内核提供两个函数用于 I/O 物理地址到虚拟地址的映射/取消映射:
- early_ioremap
- early_iounmap
3.1 early_ioremap
early_ioremap 函数定义如下:
// file: arch/x86/mm/ioremap.c
/* Remap an IO device */
void __init __iomem *
early_ioremap(resource_size_t phys_addr, unsigned long size)
{
return __early_ioremap(phys_addr, size, PAGE_KERNEL_IO);
}
该函数内部调用了 __early_ioremap 函数,__early_ioremap 函数接收三个参数:
phys_addr-- 待映射的 I/O 物理地址size-- 待映射内存区域的大小prot-- 页属性
__early_ioremap 函数定义如下:
// file: arch/x86/mm/ioremap.c
static void __init __iomem *
__early_ioremap(resource_size_t phys_addr, unsigned long size, pgprot_t prot)
{
unsigned long offset;
resource_size_t last_addr;
unsigned int nrpages;
enum fixed_addresses idx0, idx;
int i, slot;
WARN_ON(system_state != SYSTEM_BOOTING);
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
if (slot < 0) {
printk(KERN_INFO "early_iomap(%08llx, %08lx) not found slot\n",
(u64)phys_addr, size);
WARN_ON(1);
return NULL;
}
if (early_ioremap_debug) {
printk(KERN_INFO "early_ioremap(%08llx, %08lx) [%d] => ",
(u64)phys_addr, size, slot);
dump_stack();
}
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (!size || last_addr < phys_addr) {
WARN_ON(1);
return NULL;
}
prev_size[slot] = size;
/*
* Mappings have to be page-aligned
*/
offset = phys_addr & ~PAGE_MASK;
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (nrpages > NR_FIX_BTMAPS) {
WARN_ON(1);
return NULL;
}
/*
* Ok, go for it..
*/
idx0 = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
idx = idx0;
while (nrpages > 0) {
early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
if (early_ioremap_debug)
printk(KERN_CONT "%08lx + %08lx\n", offset, slot_virt[slot]);
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
}
因为 __early_ioremap 本身是为内核启动时服务的,启动完成后,应该调用 ioremap 函数完成同样的功能。所以,函数一开始,检查内核当前是否处于启动状态,如果不是,则打印警告信息。
WARN_ON(system_state != SYSTEM_BOOTING);
system_state是枚举变量,而 SYSTEM_BOOTING 是枚举类型的一个成员。
// file: include/linux/kernel.h
/* Values used for system_state */
extern enum system_states {
SYSTEM_BOOTING,
SYSTEM_RUNNING,
SYSTEM_HALT,
SYSTEM_POWER_OFF,
SYSTEM_RESTART,
} system_state;
枚举变量 system_state 的初始值为 0,即 SYSTEM_BOOTING;该变量在 kernel_init 函数中被修改为 SYSTEM_RUNNING:
// file: init/main.c
static int __ref kernel_init(void *unused)
{
...
system_state = SYSTEM_RUNNING;
...
}
接下来,将槽号 slot 设置为默认值 -1,表示无槽可用;然后遍历prev_map数组,搜索数组中的第一个空闲槽。当找到空闲槽时,把槽号保存到变量 slot中:
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
其中,prev_map 是一个静态数组,数组元素类型为 void * ,该数组内保存的是物理地址映射的虚拟地址;类似的,prev_size 也是一个静态数组,其元素类型为 unsigned long,该数组内保存的是映射区域的原始大小。我们在早期 ioremap 的初始化时,还看到过另外一个类似的数组 slot_virt,此数组内保存的是每个槽的起始虚拟地址。
// file: arch/x86/mm/ioremap.c
static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
完成搜索后,如果变量 slot 小于 0,说明没找到空闲槽,打印错误信息并返回 NULL。
if (slot < 0) {
printk(KERN_INFO "early_iomap(%08llx, %08lx) not found slot\n",
(u64)phys_addr, size);
WARN_ON(1);
return NULL;
}
如果变量 early_ioremap_debug 为真,那么需要打印调试信息。变量early_ioremap_debug 的信息我们在上文中已经介绍过来,此处不再赘述。
if (early_ioremap_debug) {
printk(KERN_INFO "early_ioremap(%08llx, %08lx) [%d] => ",
(u64)phys_addr, size, slot);
dump_stack();
}
计算出待映射区域的最大物理地址,保存到变量 last_addr 中,然后进行参数检查。
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (!size || last_addr < phys_addr) {
WARN_ON(1);
return NULL;
}
如果 size 为 0 或者 I/O 物理区域的结束地址小于起始地址(说明地址发生了回绕),这两种都是异常情况,打印警告信息,并返回 NULL。
把搜索到的空闲槽位和区域大小,在数组 prev_size 中建立映射关系。
prev_size[slot] = size;
接下来,我们看到如下代码:
/*
* Mappings have to be page-aligned
*/
offset = phys_addr & ~PAGE_MASK;
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
宏 PAGE_MASK 是页掩码,该宏定义如下:
// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_SHIFT 12
我们知道一页的大小是 4096 字节或者二进制1000000000000;PAGE_SIZE - 1将会是 111111111111;~(PAGE_SIZE-1)将得到 000000000000,即 PAGE_MASK;~PAGE_MASK让我们再次得到 111111111111。
所以,变量 offset 中保存的是物理地址 phys_addr 中低 12 位的值,即页内偏移。在下一行,清除了物理地址 phys_addr 的低 12 位,保留的是页基地址。然后我们调整区域大小,让它的上下边界都对齐到页。调整完成后,该区域大小是页的整数倍。
接下来,需要计算新区域占用的页数。如果页数大于每个槽允许的最大页数 NR_FIX_BTMAPS(扩展为 64),说明新区域的大小超出了槽的容量,打印错误信息,并返回 NULL。
/*
* Mappings have to fit in the FIX_BTMAP area.
*/
nrpages = size >> PAGE_SHIFT;
if (nrpages > NR_FIX_BTMAPS) {
WARN_ON(1);
return NULL;
}
槽位已经确定了,接下来,我们计算出该槽位的起始索引,并保存到变量 idx 中。
idx0 = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
idx = idx0;
现在新区域的页面数量以及在 Fixmap 中的起始索引都已经确定了,我们就可以通过循环,把新区域中的每个页基地址与槽中的索引逐一建立映射关系。每次迭代,我们调用early_set_fixmap函数,将给定的物理地址映射到索引值,然后让物理地址增加页面大小(4096 字节),并更新索引和页面数:
while (nrpages > 0) {
early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
至此,我们已经建立好物理地址和虚拟地址的映射关系。
early_set_fixmap 函数的实现细节,见 early_set_fixmap 小节, 我们先继续往下看。
在通过循环,将物理内存区域映射到固定映射区后,会执行以下代码:
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
slot_virt 数组中,保存着每个槽的起始虚拟地址;变量 slot 中保存的是我们实际映射到的槽位;所以 slot_virt[slot] 就是物理页基地址映射到的虚拟地址,offset + slot_virt[slot] 就是指定物理地址映射到的虚拟地址。
最后,将映射后的虚拟地址保存到 prev_map[slot] 中,并返回。
early_ioremap 工作示意图:
3.1.1 early_set_fixmap
early_set_fixmap 函数实现如下:
// file: arch/x86/mm/ioremap.c
static inline void __init early_set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t prot)
{
if (after_paging_init)
__set_fixmap(idx, phys, prot);
else
__early_set_fixmap(idx, phys, prot);
}
如果 after_paging_init为真,那么调用 __set_fixmap 函数来完成映射;否则,调用__early_set_fixmap 函数来完成早期的映射。
after_paging_init是一个静态变量,由于未显式初始化,所以其初始值为 0。
// file: arch/x86/mm/ioremap.c
static __initdata int after_paging_init;
通过调用 early_ioremap_reset 函数,可以将其值修改为 1。
// file: arch/x86/mm/ioremap.c
void __init early_ioremap_reset(void)
{
after_paging_init = 1;
}
early_ioremap_reset 函数只有在 32 位系统中才会调用。所以,在 64 位系统下,最终会调用 __early_set_fixmap 函数完成映射。
3.1.2 __early_set_fixmap
__early_set_fixmap 函数的主要功能是填充页表项,建立页表项和物理页的映射关系,其定义如下,:
// file: arch/x86/mm/ioremap.c
static void __init __early_set_fixmap(enum fixed_addresses idx,
phys_addr_t phys, pgprot_t flags)
{
unsigned long addr = __fix_to_virt(idx);
pte_t *pte;
if (idx >= __end_of_fixed_addresses) {
BUG();
return;
}
pte = early_ioremap_pte(addr);
if (pgprot_val(flags))
set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
else
pte_clear(&init_mm, addr, pte);
__flush_tlb_one(addr);
}
__early_set_fixmap函数接收 3 个参数:
- idx -- 固定映射区的索引值
- phys -- 待映射的物理地址
- flags -- 页属性
首先调用 __fix_to_virt 将索引值转换为虚拟地址。
然后检查索引值 idx 是否越界。__end_of_fixed_addresses是固定映射区的边界索引,如果 idx 大于等于该边界值,说明索引越界,报错并返回。
接下来,调用 early_ioremap_pte 函数,获取虚拟地址的页表项地址。
pte = early_ioremap_pte(addr);
我们来看下 early_ioremap_pte 函数的具体实现:
// file: arch/x86/mm/ioremap.c
static inline pte_t * __init early_ioremap_pte(unsigned long addr)
{
return &bm_pte[pte_index(addr)];
}
还记得么,我们在早期 ioremap 初始化时将临时映射区的所有页表项保存在数组 bm_pte中。由于 bm_pte就是临时映射区的页表,所以先是通过 pte_index 计算出虚拟地址所对应的页表项索引,然后通过 bm_pte[pte_index(addr)]获取到页表项,最后返回该页表项的地址。
下一步,我们使用宏pgprot_val 获取到页属性,然后检查页属性是否为 0。如果页属性不为 0,说明页属性有效,调用 set_pte 函数设置页表项;否则,调用 pte_clear 函数解除页表项 pte 与页的映射关系。
if (pgprot_val(flags))
set_pte(pte, pfn_pte(phys >> PAGE_SHIFT, flags));
else
pte_clear(&init_mm, addr, pte);
在 early_ioremap 函数中,我们将PAGE_KERNEL_IO作为页属性传递给__early_ioremap。 PAGE_KERNEL_IO扩展为:
// file: arch/x86/include/asm/pgtable_types.h
#define PAGE_KERNEL_IO __pgprot(__PAGE_KERNEL_IO)
#define __PAGE_KERNEL_IO (__PAGE_KERNEL | _PAGE_IOMAP)
#define __PAGE_KERNEL (__PAGE_KERNEL_EXEC | _PAGE_NX)
宏 _PAGE_IOMAP定义如下:
// file: arch/x86/include/asm/pgtable_types.h
#define _PAGE_IOMAP (_AT(pteval_t, 1) << _PAGE_BIT_IOMAP)
#define _PAGE_BIT_IOMAP 10 /* flag used to indicate IO mapping */
注意这里的 _PAGE_IOMAP 位,从定义上看,这是表项标志位的第 10 位。但是,处理器并不支持该标志位,也就是说,这是内核自己用的,与处理器无关。
由于 flags 有效,所以我们会调用set_pte函数来设置页表项。
此步执行后,就建立了页表项和物理地址的映射关系:
关于表项标志位的说明以及 set_pte 函数的具体实现,请参考这篇文章。
由于我们手动更改了分页结构,处理器并不知晓,所以我们需要手动刷新 TLB。在 __early_set_fixmap 函数的最后,就调用__flush_tlb_one 函数来刷新 TLB,使 TLB 中的给定地址无效:
__flush_tlb_one(addr);
__flush_tlb_one 函数定义如下:
// file: arch/x86/include/asm/tlbflush.h
static inline void __flush_tlb_one(unsigned long addr)
{
__flush_tlb_single(addr);
}
__flush_tlb_one 函数内部调用了宏 __flush_tlb_single:
// file: arch/x86/include/asm/tlbflush.h
#define __flush_tlb_single(addr) __native_flush_tlb_single(addr)
宏 __flush_tlb_single 扩展为 __native_flush_tlb_single:
// file: arch/x86/include/asm/tlbflush.h
static inline void __native_flush_tlb_single(unsigned long addr)
{
asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}
__native_flush_tlb_single 函数调用了内联汇编,使用了汇编指令 invlpg使 TLB 中指定的地址失效。
3.2 early_iounmap
函数 early_iounmap 取消I/O内存区域的映射,函数定义如下:
// file: arch/x86/mm/ioremap.c
void __init early_iounmap(void __iomem *addr, unsigned long size)
{
unsigned long virt_addr;
unsigned long offset;
unsigned int nrpages;
enum fixed_addresses idx;
int i, slot;
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (prev_map[i] == addr) {
slot = i;
break;
}
}
if (slot < 0) {
printk(KERN_INFO "early_iounmap(%p, %08lx) not found slot\n",
addr, size);
WARN_ON(1);
return;
}
if (prev_size[slot] != size) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d] size not consistent %08lx\n",
addr, size, slot, prev_size[slot]);
WARN_ON(1);
return;
}
if (early_ioremap_debug) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d]\n", addr,
size, slot);
dump_stack();
}
virt_addr = (unsigned long)addr;
if (virt_addr < fix_to_virt(FIX_BTMAP_BEGIN)) {
WARN_ON(1);
return;
}
offset = virt_addr & ~PAGE_MASK;
nrpages = PAGE_ALIGN(offset + size) >> PAGE_SHIFT;
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
early_clear_fixmap(idx);
--idx;
--nrpages;
}
prev_map[slot] = NULL;
}
该函数接收两个参数:
- addr -- 取消映射的虚拟地址
- size -- 区域大小。
首先,将槽号 slot 初始化为 -1,表示未找到地址对应的槽位;然后遍历prev_map数组,查看哪个槽的数据与给定地址相等。还记得么,我们在 __early_ioremap 函数最后,把映射到的虚拟地址保存到了 prev_map[slot] 中。当找到对应的槽时,会把它保存到变量 slot 中:
slot = -1;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (prev_map[i] == addr) {
slot = i;
break;
}
}
如果 slot 小于 0,说明没找到对应的槽,打印错误信息并返回。
if (slot < 0) {
printk(KERN_INFO "early_iounmap(%p, %08lx) not found slot\n",
addr, size);
WARN_ON(1);
return;
}
如果已映射区域的大小与要释放的大小不一致,打印错误信息并返回。
if (prev_size[slot] != size) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d] size not consistent %08lx\n",
addr, size, slot, prev_size[slot]);
WARN_ON(1);
return;
}
如果设置了调试参数,那么会打印调式信息。
if (early_ioremap_debug) {
printk(KERN_INFO "early_iounmap(%p, %08lx) [%d]\n", addr,
size, slot);
dump_stack();
}
如果传入的虚拟地址小于临时映射区的最小地址,说明地址有误,打印警告信息并返回。
virt_addr = (unsigned long)addr;
if (virt_addr < fix_to_virt(FIX_BTMAP_BEGIN)) {
WARN_ON(1);
return;
}
计算页内偏移及内存区域对应的页数,可参考 __early_ioremap 函数中的计算过程。
offset = virt_addr & ~PAGE_MASK;
nrpages = PAGE_ALIGN(offset + size) >> PAGE_SHIFT;
计算出待释放区域的起始地址对应的固定映射区索引值。然后通过循序,调用 early_clear_fixmap 函数,取消索引和地址的映射关系,并更新索引值和页数。
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
early_clear_fixmap(idx);
--idx;
--nrpages;
}
最后,通过将数组 prev_map 中对应的槽位设置为 NULL,来释放该槽位。
prev_map[slot] = NULL;
3.2.1 early_clear_fixmap
取消地址映射的功能,主要是在 early_clear_fixmap 函数中执行的,我们来看下具体实现。
// file: arch/x86/mm/ioremap.c
static inline void __init early_clear_fixmap(enum fixed_addresses idx)
{
if (after_paging_init)
clear_fixmap(idx);
else
__early_set_fixmap(idx, 0, __pgprot(0));
}
我们在 early_set_fixmap 函数中介绍过变量 after_paging_init ,该变量在 32 位系统中才会为设置为 1。所以,此处我们会执行到 __early_set_fixmap 函数。
我们在上文已经介绍过 __early_set_fixmap 函数,该函数会把 idx 对应的虚拟地址映射到给定的物理地址,并设置页属性。在当前情况下,会把 idx 对应的虚拟地址映射到物理地址 0,且页属性也为 0。由于处理器是根据存在位(位 0 )来判断表项是否有效,当页属性为 0 时,该表项是无效的,也就意味着取消了映射关系。
四、参考资料
1、wikipedia:Memory-mapped I/O and port-mapped I/O
2、 Linux Kernel:内存管理之分页(Paging)
3、Linux Kernel:内存管理之固定映射 (Fixmap)
4、Fix-Mapped Addresses and ioremap