Kernel那些事儿之内存管理(12) --- 内核映射(中)

内核地址空间中后面这128MB的最后一部分,是固定映射 (fixed mappings)。

固定映射是什么意思?为什么要有固定映射?Kernel源代码的注释里有一句话,可谓一语中的:The point is to have a constant address at compile time, but to set the physical address only in the boot process.

一个固定映射的线性地址是个常量,例如0xffffc000,且该常量在编译阶段就可以确定。不过该常量线性地址所映射的物理地址,则需系统启动之后才能确定。

从某种意义上说,固定映射的线性地址,与指针变量有相同的作用,但是要比指针变量效率高。原因有二:

  • 解析一个指针变量,要比解析固定映射的线性地址多一次内存访问,毕竟要先从内存中读出指针变量的值,而固定映射的线性地址本身就是个常量。
  • 作为一个好的编程习惯,在使用指针变量之前,一般都会检查一下指针值。而对于地址常量,就没必要做这种检查了。

那都有哪些固定映射的线性地址可用呢?Kernel 定义了一个 enum 列表:

 54 enum fixed_addresses {
 55     FIX_HOLE,
 56     FIX_VDSO,
 57     FIX_DBGP_BASE,
 58     FIX_EARLYCON_MEM_BASE,
 59 #ifdef CONFIG_X86_LOCAL_APIC
 60     FIX_APIC_BASE,  /* local (CPU) APIC) -- required for SMP or not */
 61 #endif
 62 #ifdef CONFIG_X86_IO_APIC
 63     FIX_IO_APIC_BASE_0,
 64     FIX_IO_APIC_BASE_END = FIX_IO_APIC_BASE_0 + MAX_IO_APICS-1,
 65 #endif
 66 #ifdef CONFIG_X86_VISWS_APIC
 67     FIX_CO_CPU, /* Cobalt timer */
 68     FIX_CO_APIC,    /* Cobalt APIC Redirection Table */
 69     FIX_LI_PCIA,    /* Lithium PCI Bridge A */
 70     FIX_LI_PCIB,    /* Lithium PCI Bridge B */
 71 #endif
 72 #ifdef CONFIG_X86_F00F_BUG
 73     FIX_F00F_IDT,   /* Virtual mapping for IDT */
 74 #endif
 75 #ifdef CONFIG_X86_CYCLONE_TIMER
 76     FIX_CYCLONE_TIMER, /*cyclone timer register*/
 77 #endif
 78 #ifdef CONFIG_HIGHMEM
 79     FIX_KMAP_BEGIN, /* reserved pte‘s for temporary kernel mappings */
 80     FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
 81 #endif
 82 #ifdef CONFIG_ACPI
 83     FIX_ACPI_BEGIN,
 84     FIX_ACPI_END = FIX_ACPI_BEGIN + FIX_ACPI_PAGES - 1,
 85 #endif
 86 #ifdef CONFIG_PCI_MMCONFIG
 87     FIX_PCIE_MCFG,
 88 #endif
 89 #ifdef CONFIG_PARAVIRT
 90     FIX_PARAVIRT_BOOTMAP,
 91 #endif
 92     __end_of_permanent_fixed_addresses,
 93     /* temporary boot-time mappings, used before ioremap() is functional */
 94 #define NR_FIX_BTMAPS   16
 95     FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
 96     FIX_BTMAP_BEGIN = FIX_BTMAP_END + NR_FIX_BTMAPS - 1,
 97     FIX_WP_TEST,
 98     __end_of_fixed_addresses
 99 };

这些固定映射的线性地址,都位于4GB线性地址空间的最后部分。函数fix_to_virt()用来把上面列表中的一个地址索引转换为线性地址。

133 static __always_inline unsigned long fix_to_virt(const unsigned int idx)
134 {

144     if (idx >= __end_of_fixed_addresses)
145         __this_fixmap_does_not_exist();
146
147         return __fix_to_virt(idx);
148 }
150 unsigned long __FIXADDR_TOP = 0xfffff000;

116 #define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)

123 #define __fix_to_virt(x)    (FIXADDR_TOP - ((x) << PAGE_SHIFT))

这个函数有几个很有意思的地方。

首先,这是一个内联函数。编译器会把该函数的代码直接插入到调用它的地方。

其次,该函数中没有变量,使用的都是常量。因此,在编译阶段,编译器即可判断144行的if语句成不成立。如果成立,则在编译阶段编译器就会报错,因为函数__this_fixmap_does_not_exist()并没有在Kernel中定义。如果不成立,则编译器就会把 144 ~ 145 行直接删掉。

最后,在编译阶段,该函数就可以计算出最后的线性地址值,假如说是0xffffc000。那么调用该函数的地方就会用常量 0xffffc000 来替代。

虽然线性地址在编译时可以确定,但是物理地址却需要系统运行时来映射。Kernel提供了两个函数来完成这种映射:set_fixmap(idx, phys) 和 set_fixmap_nocache(idx, phys)。当然,这两个函数也是通过修改 kernel page tables来完成映射。

临时内核映射 (Temporary Kernel Mappings)

前面讲过,创建持久内核映射的函数kmap()可能会阻塞当前进程,因此不能用在中断上下文中。于是,Kernel在固定映射的基础上,提供了另一种映射机制 —— 临时内核映射。与持久内核映射相比,它更快,而且不会阻塞当前进程,因此可以用在中断上下文中。不过,它也有一个弱点,就是使用它的代码不能睡眠。

如果仔细观察前面的fixed_addresses列表,你会发现,在 79 ~ 80 行,有一组地址索引,这些地址索引从 FIX_KMAP_BEGIN 到 FIX_KMAP_END,共有KM_TYPE_NR*NR_CPUS个。这些索引对应的线性地址,正是临时内核映射之所在。

这些地址索引具体分布如下:

 10 enum km_type {
 11 D(0)    KM_BOUNCE_READ,
 12 D(1)    KM_SKB_SUNRPC_DATA,
 13 D(2)    KM_SKB_DATA_SOFTIRQ,
 14 D(3)    KM_USER0,
 15 D(4)    KM_USER1,
 16 D(5)    KM_BIO_SRC_IRQ,
 17 D(6)    KM_BIO_DST_IRQ,
 18 D(7)    KM_PTE0,
 19 D(8)    KM_PTE1,
 20 D(9)    KM_IRQ0,
 21 D(10)   KM_IRQ1,
 22 D(11)   KM_SOFTIRQ0,
 23 D(12)   KM_SOFTIRQ1,
 24 D(13)   KM_TYPE_NR
 25 };

在固定映射的地址索引列表中,每个CPU都有13个这样的地址索引。每一个地址索引代表一种映射类型。分布如下所示:

建立临时内核映射是由函数kmap_atomic()完成的。

 49 void *kmap_atomic(struct page *page, enum km_type type)
 50 {
 51     return kmap_atomic_prot(page, type, kmap_prot);
 52 }
 29 void *kmap_atomic_prot(struct page *page, enum km_type type, pgprot_t prot)
 30 {
 31     enum fixed_addresses idx;
 32     unsigned long vaddr;
 33
 34     /* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
 35     pagefault_disable();
 36
 37     if (!PageHighMem(page))
 38         return page_address(page);
 39
 40     idx = type + KM_TYPE_NR*smp_processor_id();
 41     vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
 42     BUG_ON(!pte_none(*(kmap_pte-idx)));
 43     set_pte(kmap_pte-idx, mk_pte(page, prot));
 44     arch_flush_lazy_mmu_mode();
 45
 46     return (void *)vaddr;
 47 }

40 ~ 41,根据type和当前CPU,计算出临时内核映射中的地址索引,然后该索引值加上 FIX_KMAP_BEGIN,得到的便是固定映射中的地址索引。最后通过 __fix_to_virt() 转换成线性地址。

43行,kmap_pte是kernel page tables中的一个页表,该页表初始化为线性地址fix_to_virt(FIX_KMAP_BEGIN)所对应的页表。

该函数会关闭内核抢占,这个和调用该函数的代码不能睡眠是同样的原因:如果建立了临时内核映射的进程被调度出去,另一个进程可能会创建相同类型的临时内核映射,这样就把之前的映射给覆盖了。

解除临时内核映射是由函数kunmap_atomic()完成的。

 54 void kunmap_atomic(void *kvaddr, enum km_type type)
 55 {
 56     unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;
 57     enum fixed_addresses idx = type + KM_TYPE_NR*smp_processor_id();
 58
 59     /*
 60      * Force other mappings to Oops if they‘ll try to access this pte
 61      * without first remap it.  Keeping stale mappings around is a bad idea
 62      * also, in case the page changes cacheability attributes or becomes
 63      * a protected page in a hypervisor.
 64      */
 65     if (vaddr == __fix_to_virt(FIX_KMAP_BEGIN+idx))
 66         kpte_clear_flush(kmap_pte-idx, vaddr);
 67     else {
 68 #ifdef CONFIG_DEBUG_HIGHMEM
 69         BUG_ON(vaddr < PAGE_OFFSET);
 70         BUG_ON(vaddr >= (unsigned long)high_memory);
 71 #endif
 72     }
 73
 74     arch_flush_lazy_mmu_mode();
 75     pagefault_enable();
 76 }

如果线性地址确实是对应于type的临时内核映射地址,则通过修改页表来解除映射。

最后该函数会递减当前进程的preempt_count,并检查是否有pending的调度请求。

时间: 2024-08-05 00:00:47

Kernel那些事儿之内存管理(12) --- 内核映射(中)的相关文章

Kernel那些事儿之内存管理(11) --- 内核映射(上)

前面简单地介绍了三种不同的地址空间,接下来重点讲述线性地址空间到物理地址空间的映射. 我们先从32位系统开始. 在32位系统中,线性地址空间的大小为 2^32,即4GB.Kernel一般会按照 3:1 的比例,把线性地址空间分为两部分: 0~3GB 用户地址空间 3GB~4GB 内核地址空间. 用户地址空间的管理和映射是个大的topic.我们后面再详细讲述. 内核地址空间只有1GB大小,最多只能映射1GB的物理内存.那问题来了:1GB之外物理内存怎么办? Kernel 给出的解决办法是,把1GB

Kernel那些事儿之内存管理(13) --- 内核映射(下)

前面讲过,针对于内核地址空间中后面的128MB空间,Kernel提供了三种机制来映射物理内存.之前讲过了两种,即持久内核映射和临时内核映射.这两种机制的目的都是一样的:使Kernel能够访问到高端内存. 今天讲一下第三种机制:非连续内存分配,也就是vmalloc.这个机制同样可以使Kernel能够访问到高端内存,不过这不是该机制的主要目的.该机制的主要目的是:把物理上不连续的页面映射到连续的内核线性地址空间中. 非连续内存区域管理 既然是映射,肯定会涉及到三个元素:集合L,集合P,映射M. 集合

Kernel那些事儿之内存管理(8) --- Slab(中)

上篇讲了Slab中的数据结构,这篇该讲Slab中的操作了. 既然是内存管理,那操作无非就两点:allocate 和 free. 1. 申请一个object 在Slab中,申请一个object是通过函数 kmem_cache_alloc() 来完成的. 3618 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 3619 { 3620     return __cache_alloc(cachep, flags, __bu

Kernel那些事儿之内存管理(3) --- 久别重逢

上次我们讲到page frame是物理内存的基本组成单位.那Kernel就必须要有一套机制来管理空闲的page frames.这一点不难理解.每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱. 这个问题看似简单,实则不然.因为这里面有一个外碎片的问题. 在物理内存中,连续的物理内存页有时是很重要的.例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续:再例如,使用连续的物理内存页,可

Kernel那些事儿之内存管理(1)

有人的地方就有江湖.要介绍内存管理这个江湖,首先还得从这里面的主要人物讲起. 在NUMA结构中,物理内存首先被分成若干nodes.每一个node进一步被分成若干zones.每一个zone又关联了一个描述page frames的数组,该数组包含了属于该zone的所有page frame的描述符. 不难看出,在这个江湖里主要有三位重要人物:nodes, zones 和 page frames.这三者的关系和地位大体可以用下图来描述(该图取自"Professional Linux Kernel Arc

Kernel那些事儿之内存管理(6) --- 衣带渐宽终不悔(下)

接着上篇写,继续介绍zone allocator.上一篇介绍了周边,现在来看看它的全貌 --- 函数__alloc_pages(). Kernel源代码里是这样注释函数__alloc_pages()的.其重要地位可见一斑. 1451 /* 1452  * This is the 'heart' of the zoned buddy allocator. 1453  */ __alloc_pages()的工作模式很清晰:利用函数get_page_from_freelist()多次遍历zonelis

Kernel那些事儿之内存管理(5) --- 衣带渐宽终不悔(上)

Kernel中负责分配一个连续内存页块的子系统一般被称为zoned page frame allocator.前面讲了函数 buffered_rmqueue() 是如何从指定zone的buddy system中分配一个连续内存页块的.这个函数貌似完成了内存页块分配相关的所有工作,然而实际上,这个函数只是zone allocator的冰山一角. 记得我刚上大学那会,拥有一个MP3还是一件能够令男生羡慕.令女生着迷的事情.于是我咬咬牙,花了近一个月的生活费买了一个漂亮的MP3,从此过上了非凡的生活.

Kernel那些事儿之内存管理(2) --- 百闻不如一见

上次介绍了物理内存管理中三位主要人物中的node 和zone.这两位是当官的,一个是县长,一个是里长,不敢不先介绍啊.接下来出场的就是我们的老百姓了 --- page frame. Page frame是物理内存的基本组成单位,在Kernel中由结构体 struct page 来描述. struct page {     unsigned long flags;          atomic_t _count;          union {         atomic_t _mapcou

Kernel那些事儿之内存管理(7) --- Slab(上)

前面讲的buddy system算法,分配内存的最小单位是一个页面(例如 4K).这对于大的内存申请比较适用.可是实际生活中,Kernel经常需要分配小的内存空间,比如几十个字节,这个时候怎么办呢? 不同的人可能会想到不同的解决办法. 对于财大气粗的富人一族,办法很简单:申请一个页面,只用其中的几十字节,剩下的直接丢掉. 对于锱铢必较的穷人一族,就不敢这么挥霍了:申请一个页面,然后记录好页面内哪些内存区用了,哪些还没用,没用的部分可以用来满足其他的内存分配请求.总之务必做到物尽其用. 很不幸,K