高端内存之“走马楼台类转蓬”
一、高端内存
这个是Linux中的一个实现机制,当物理内存大于1G的时候,高于896M之上的内存就属于高端内存。这里的问题是:物理内存大于1G,但是内核可以使用的逻辑地址空间只有1G,所以内存物理地址空间多于内核可以使用的逻辑地址空间,这就相当于有些人钱多的花不完,也苦恼(请让我尽情的苦恼把),或者说像俄罗斯一样,那是一个女多男少的国度,也挺幸福。
这样,内核就不能将所有的物理内存都映射到自己使用的1G地址空间中。即使用户态有3G地址空间,那也是私有财产,不能因为内核态运行于特权级就可以抢占用户态空间(“强拆”这种事在内核中还是比较少发生的)。那么内核为什么要把所有的空间都映射到自己的逻辑地址空间呢?举个最简单的例子,当内核为用户新分配一个页面的时候,内核要把这个页面清零,也就是memset(pagestart,0,pagesize),内核虽然是运行于特权模式,但是它同样是运行在保护模式基础之上,这意味着内核使用的同样是逻辑地址,要经过地址映射。现在如果内核决定把高于1G的内存分配给用户(修改用户页表让用户的逻辑地址指向页面物理地址),那么memset的第一个逻辑地址从哪里来呢?
这里内核就预留了一些空间,一般是从876M开始到1G地址空间中为高端内存,这段内存用于动态中转。打个比方,小于876M的映射属于住宅楼、之上的为旅馆,住宅楼里都是常驻居民,里面的人和单元号是固定对应的,而旅馆中的人可能经常会变化,它的目的就是为了应对一些流动性变化。
现在考虑开始的那个问题。内核可以从这个高端内存中选择一个临时地址,先把这个页面映射入该地址,然后执行memset清零,清零之后取消内核态映射,再映射入用户态地址,因为此时虽然内核态地址紧张,单用户态地址有3G呢,这就是一个“藏富于民”的架构,如果天朝来设计内核,那可能是用户态1G,内核态3G吧。
但是还有问题,假设有些人占用了旅馆之后不走了呢?假如有些人出差到某个地方,要进行一个项目支持,所以它要在这个房间里呆上很久,偏偏最近比较多,全是这样的人,那会不会内核的这个机动空间也没有了呢,就是这高端内存的逻辑地址也被用完了?
二、kmap
对于第一节说明的那个问题,的确是有可能发生的,当你kmap要把一个高端内存映射入内核空间的时候,可能发现的确内核的高端空间也被占用完呢,那怎么办呢?还能怎么办!只能等待了,所以执行kmap的时候是可能会阻塞的,这一点之后出问题别说人间警告过你。我们看一下kmap的执行流程
kmap=__kmap-->>kmap_high--->>>map_new_virtual
/*
* Sleep for somebody else to unmap their entries
*/
{
DECLARE_WAITQUEUE(wait, current);
__set_current_state(TASK_UNINTERRUPTIBLE);
add_wait_queue(&pkmap_map_wait, &wait);
spin_unlock(&kmap_lock);
schedule();
remove_wait_queue(&pkmap_map_wait, &wait);
spin_lock(&kmap_lock);
/* Somebody else might have mapped it while we slept */
if (page_address(page))
return (unsigned long)page_address(page);
/* Re-start */
goto start;
}
三、kmap_atomic
但是有时候需要在中断中执行,例如之前说的写时复制,此时就是在中断中完成对新页面的分配和清零的,此时你要在中断中睡眠,可能不是太好,而且也没有这个必要。我们看一下从匿名页面到清零的操作,大致应该走到这个流程
get_page_from_freelist-->>buffered_rmqueue-->>prep_new_page--->>prep_new_page--->>>prep_zero_page--->>>clear_highpage
static inline void clear_highpage(struct page *page)
{
void *kaddr = kmap_atomic(page, KM_USER0);
clear_page(kaddr);
kunmap_atomic(kaddr, KM_USER0);
}
同样以大家喜闻乐见的386实现说明一下
void *kmap_atomic(struct page *page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
pagefault_disable();
idx = type + KM_TYPE_NR*smp_processor_id();
BUG_ON(!pte_none(*(kmap_pte-idx)));
if (!PageHighMem(page))
return page_address(page);
vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx); 这里的操作非常霸道,直接通过FIX_KMAP_BEGIN计算出逻辑地址,然后再“霸王硬上弓”的把
set_pte(kmap_pte-idx, mk_pte(page, kmap_prot)); 待漂白的物理页面映射入该地址,然后进行操作
arch_flush_lazy_mmu_mode();
return (void*) vaddr;
}
这个atomic要满足的功能就是不能睡眠,那么它是如何保证这个逻辑地址空间现在没有人在用呢?
1、单CPU如何避免并发和竞争
秘密在于开始的时候使用的pagefault_disable调用,这个函数最重要的是会调用inc_preempt_count函数,也就是禁止抢占,所以就避免了对这个资源的进程抢占问题,而且还保证了当如果kmap_atomic即使出现了访问异常,也不会发生切换。这只是制度上的保证,但是还需要使用者的自觉性,那就是这个kmap_atomic之后必须尽快调用kunmap_atomic,并且在这对调用之间不能再次调用这个函数,所以可以看到一般这种映射执行之后进行简单操作之后马上执行kunmap_atomic,以避免中间调用的函数再次执行该函数。
那么假设有些同志就是不自觉,或者一不小心犯了错误,递归调用了这个函数,那内核也会给出帮助,其中的BUG_ON(!pte_none(*(kmap_pte-idx)));就是为这个错误准备的。
2、多核中如何避免并发和竞争
抢占只是保证该线程在当前CPU上不被抢占,假设说另一个CPU上在执行这段代码,同样有可能会出现竞争问题(这里是指逻辑地址空间竞争)。此时如何保证呢?这里有一个比较严谨的比喻(相对于该日志中之前那些不恰当的必须):多CPU相当于用户态的多线程,CPU私有变量相当于线程私有变量。有了这个假设,大家就知道,可以为不同的CPU分配不同的逻辑地址空间,从而避免不同的CPU带来的并发和竞争。对应代码为
idx = type + KM_TYPE_NR*smp_processor_id();
这样不同的CPU就不会使用相同的地址来进行映射。
在linux-2.6.21\include\asm-i386fixmap.h文件中,定义了一些不同功能的宏,这样的每个宏有一个专用的、编译时确定的、永远不会被kmap或者其它映射使用的逻辑地址空间,这样保证了这个逻辑地址空间始终可用。
四、内核中一个小技巧
这里说类型必须编译时确定,那如果编译时不确定呢?
static __always_inline unsigned long fix_to_virt(const unsigned int idx)
{
/*
* this branch gets completely eliminated after inlining,
* except when someone tries to use fixaddr indices in an
* illegal way. (such as mixing up address types or using
* out-of-range indices).
*
* If it doesn't get removed, the linker will complain
* loudly with a reasonably clear error message..
*/
if (idx >= __end_of_fixed_addresses)
__this_fixmap_does_not_exist();这个函数__this_fixmap_does_not_exist正如它名字所说,的确是没有在内核的任何一个地方实现。所以如果你给这个函数传入一个非编译时常量,那么此时这个代码就不会被保留,从而出现链接错误,反过来,如果调用fix_to_virt的参数是编译时常量,这个条件是编译时就可以确定不满足的,所以这个__this_fixmap_does_not_exist调用被编译时进行“死代码删除”了,所以不会出现连接错误。
return __fix_to_virt(idx);
}
为了更直观一些,写个demo代码吧
[tsecer@Harry deadcodedelete]$ cat delete.c
int main()
{
if (3 > 4)
hehe();
}
[tsecer@Harry deadcodedelete]$ gcc delete.c -c
[tsecer@Harry deadcodedelete]$ objdump -d delete.o
delete.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp 可以看到这里根本没有调用hehe函数,它的调用被编译时优化掉了,这里甚至没有开任何优化级别。
3: 5d pop %ebp
4: c3 ret
当然这不是唯一的编译时检测方法,这里只是一种展示gcc运作的思路,其实使用内核的BUILD_BUG_ON宏更加合理,所以大家的思路一定要活啊,问渠那得清如许,为有源头活水来。