Posts Tagged ‘PageTable’

内核页表和进程页表

May 28th, 2015

最近在看vmalloc()分配代码,我们知道当通过alloc_page()分配出来page后,需要将这些分散的物理页框page映射到vmalloc区,这里我们就要修改内核页表,以前我学页表是把内核空间与用户空间割裂学习的,导致二者无法很好地衔接,这里我会把两个概念重新解释清楚。

下面代码映射到vmalloc区的关键就是map_vm_area()函数,

for (i = 0; i < area->nr_pages; i++) {
        struct page *page;

        if (node == NUMA_NO_NODE)
            page = alloc_page(alloc_mask);
        else
            page = alloc_pages_node(node, alloc_mask, order);
...
    }

    if (map_vm_area(area, prot, pages))
        goto fail;
    return area->addr;

拿IA32架构的虚拟地址来说,0~3GB属于用户空间,用户空间是由每个进程的task_struct.mm_struct.pgd的成员变量,这个指向的就是进程页表。而3G~4GB属于内核空间,这个页表是由内核页目录表管理,存放在主内核页全局目录init_mm.pgd(swapper_pg_dir)中

struct mm_struct init_mm = {
         .mm_rb          = RB_ROOT,
         .pgd            = swapper_pg_dir,
         .mm_users       = ATOMIC_INIT(2),
         .mm_count       = ATOMIC_INIT(1),
         .mmap_sem       = __RWSEM_INITIALIZER(init_mm.mmap_sem),
         .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
         .mmlist         = LIST_HEAD_INIT(init_mm.mmlist),
         INIT_MM_CONTEXT(init_mm)
};

进程切换切换的是进程页表:即将新进程的pgd(页目录)加载到CR3寄存器中。而内核页表是所有进程共享的,每个进程的“进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝。

vmalloc区发生page fault

在vmalloc区发生page fault时,将“内核页表”同步到“进程页表”中。这部分区域对应的线性地址在内核使用vmalloc分配内存时,其实就已经分配了相应的物理内存,并做了相应的映射,建立了相应的页表项,但相关页表项仅写入了“内核页表”,并没有实时更新到“进程页表中”,内核在这里使用了“延迟更新”的策略,将“进程页表”真正更新推迟到第一次访问相关线性地址,发生page fault时,此时在page fault的处理流程中进行“进程页表”的更新。

所以linux中,只有进程的页表是时刻在更换的,而内核页表全局只有一份,所有进程共享内核页表!

IA32下高端内存页框(ZONE_HIGHMEM)永久内核映射的分析与实现

March 6th, 2015

一、背景

在AMD64平台下,可用的线性地址空间远远大于目前安装的RAM大小,所以在这种体系中,ZONE_HIGHMEM总是空的。

而在32位平台上,Linux设计者必须找到一种方式允许内核使用所有4GB的RAM,如果IA32支持PAE的话,则使内核可以支持64GB的RAM。

IA32架构下,之所以有这些约束,主要是为了兼容前代固有的硬件架构(i386),例如DMA直接读取,i386 内存大于900MB的情况。

ZONE_HIGHMEM属于高于896MB的内存区域,需要通过高端内存页框分配的方式映射到内核线性地址空间,从而使得内核可以对高端内存进行访问。而低于896MB直接通过(PAGE_OFFSET)偏移映射到物理内存上。详细查看http://www.lizhaozhong.info/archives/1193

二、原理

高端内存页框分配alloc_pages()函数返回的是page frame 页描述符(page description)的线性地址,因为这些页描述符地址一旦被分配,便不再改变,所以他是全局唯一的。这些页描述符在free_area_init_nodes()初始化时,已建立好。

注:free_area_init_nodes()之前的初始化与体系结构相关,当系统调用这个函数后,开始初始化每个pg_data_t的结构体,便于体系结构再无相关,http://lxr.free-electrons.com/source/mm/page_alloc.c#L5386。。

页描述符我们可以想象成系统将所有内存按照4k等分划分形成的数组下标。所有的page descriptor组成一个大的连续的数组 ,每个节点起始地址存放在 struct page *node_mem_map中,因此如果知道了page descriptor的地址pd,pd-node_mem_map就得到了pd是哪个page frame的描述符,也可以知道这个页描述符是否高于896MB。

 

已知alloc_pages()函数分配一个page descriptor的线性地址,可由它得到它所描述的物理页是整个内存的第几页:

假设是第N个物理页,那么这个物理页的物理地址是physAddr = N << PAGE_SHIFT   ,一般情况PAGE_SHIFT   为12 也就是4k。在得知该物理页的物理地址是physAddr后,就可以视physAddr的大小得到它的虚拟地址,然后对这个虚拟地址进行判断:

1.physAddr < 896M  对应虚拟地址是 physAddr + PAGE_OFFSET   (PAGE_OFFSET=3G)
2.physAddr >= 896M 对应虚拟地址不是静态映射的,通过内核的高端虚拟地址映射得到一个虚拟地址(也就是内核页表映射)。

在得到该页的虚拟地址之后,内核就可以正常访问这个物理页了。

所以,我们可以将physAddr高于896M的这128M看做成为一个内核页表,这个内核页表在paging_init()初始化完成。

三、实现

内核采取三种不同的机制将page frame映射到高端内存:永久内核映射、临时内核映射、非连续内存分配。

alloc_page()函数集返回的是全是struct page* 类型的数据,返回的也都是page descriptor的线性地址。然后系统要判断该page descriptor是否是highmem,也就是void *kmap(struct page *page)
http://lxr.free-electrons.com/source/arch/x86/mm/highmem_32.c#L6 。

 

系统调用PageHighMem(page),判断当前page是否是highmem,如果是返回kmap_high(page)。如果不是返回page_address(page)  http://lxr.free-electrons.com/source/mm/highmem.c#L412

 

void *kmap(struct page *page)
{
         might_sleep();
         if (!PageHighMem(page))
                 return page_address(page);
         return kmap_high(page);
}
void *page_address(const struct page *page)
{
         unsigned long flags;
         void *ret;
         struct page_address_slot *pas;

         if (!PageHighMem(page))
                 return lowmem_page_address(page);

         pas = page_slot(page);
         ret = NULL;
         spin_lock_irqsave(&pas->lock, flags);
         if (!list_empty(&pas->lh)) {
                 struct page_address_map *pam;

                 list_for_each_entry(pam, &pas->lh, list) {
                         if (pam->page == page) {
                                 ret = pam->virtual;
                                 goto done;
                         }
                 }
         }
 done:
         spin_unlock_irqrestore(&pas->lock, flags);
         return ret;
}

在page_address()函数中,我们要再次检查该page是否属于HIGHMEM,如果不属于,则直接计算偏移量__va(PFN_PHYS(page_to_pfn(page)))。

而pas = page_slot(page);之后的代码,是在永久映射中,系统为了方便查找,建立了一个page_address_htable散列表,系统可以很快的对于已经存放在散列表中的永久映射的page descriptor的线性地址进行查找,如果找到就返回内核线性地址,否则为NULL。

如果判断该page属于HIGHMEM,进入kmap_high(page) http://lxr.free-electrons.com/source/mm/highmem.c#L279

void *kmap_high(struct page *page)
{
         unsigned long vaddr;

/*
* For highmem pages, we can't trust "virtual" until
* after we have the lock.
*/
         lock_kmap();
         vaddr = (unsigned long)page_address(page);
         if (!vaddr)
                 vaddr = map_new_virtual(page);
         pkmap_count[PKMAP_NR(vaddr)]++;
         BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
         unlock_kmap();
         return (void*) vaddr;
}

虽然判断了HIGHMEM,调用这个函数,但是我们不能信任,仍需要再次检查,顺便检查是否在page_address_htable散列表中存在该page,如果不存在,则进行映射( map_new_virtual(page)),然后将pkmap_count数组中,这个page引用+1,然会内核就可以正常使用这个高端内存的线性地址了。

map_new_virtual()函数是进行HIGHMEM映射的核心函数,其中for(;;)循环是寻找可用的内核页表项进行高端内存页框映射,这里涉及到了页表color问题,主要为了提速查找效率。

当系统找到可用的页表项时,从line261~line268就是核心

  • 以PKMAP_BASE为基址last_pkmap_nr为偏移在永久映射区创建内核线性地址vaddr
  • 将vaddr加入pkmap_page_table
  • 引用赋值为1,之后有可能会++
  • 然后将该内核线性地址vaddr加入page,并返回内核线性地址
</pre>
<pre>217 static inline unsigned long map_new_virtual(struct page *page)
218 {
...
224 start:
225         count = get_pkmap_entries_count(color);
226         /* Find an empty entry */
227         for (;;) {
228                 last_pkmap_nr = get_next_pkmap_nr(color);
229                 if (no_more_pkmaps(last_pkmap_nr, color)) {
230                         flush_all_zero_pkmaps();
231                         count = get_pkmap_entries_count(color);
232                 }
233                 if (!pkmap_count[last_pkmap_nr])
234                         break;  /* Found a usable entry */
235                 if (--count)
236                         continue;
237
238                 /*
239                  * Sleep for somebody else to unmap their entries
240                  */
241                 {
242                         DECLARE_WAITQUEUE(wait, current);
243                         wait_queue_head_t *pkmap_map_wait =
244                                 get_pkmap_wait_queue_head(color);
246                         __set_current_state(TASK_UNINTERRUPTIBLE);
247                         add_wait_queue(pkmap_map_wait, &wait);
248                         unlock_kmap();
249                         schedule();
250                         remove_wait_queue(pkmap_map_wait, &wait);
251                         lock_kmap();
253                         /* Somebody else might have mapped it while we slept */
254                         if (page_address(page))
255                                 return (unsigned long)page_address(page);
256
257                         /* Re-start */
258                         goto start;
259                 }
260         }
261         vaddr = PKMAP_ADDR(last_pkmap_nr);
262         set_pte_at(&init_mm, vaddr,
263                    &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
264
265         pkmap_count[last_pkmap_nr] = 1;
266         set_page_address(page, (void *)vaddr);
268         return vaddr;

中间line241~259 内核考虑一种情况:如果这个PKMAP都已经被映射,最少的count都是>=1的,那么需要将当前的映射操作阻塞,然后加入等待队列,然后主动让出cpu被调度,直到PKMAP中存在count=0的页表项存在,唤醒,重新执行一次。

pkmap_count引用的可能值:0/1/>1。

0意味着该内核页表项未被映射高端内存,是可用的

1意味着虽然未被映射高端内存,但是需要unmapped,如果没有任何可用的页表项了,需要调用flush_all_zero_pkmaps()刷新pkmap_count,并置零。

>1意味着该页表项被使用。

 

参考:

http://repo.hackerzvoice.net/depot_madchat/ebooks/Mem_virtuelle/linux-mm/zonealloc.html#INITIALIZE

http://blog.csdn.net/lcw_202/article/details/5955783

https://www.kernel.org/doc/gorman/html/understand/understand006.html#sec: Mapping addresses to struct pages

OS的分页分段(笔记)

September 2nd, 2014

我们从80386处理器入手。首先,到了80386时代,CPU有了四种运行模式,即实模式、保护模式、虚拟8086模式和SMM模式。

实模式其大致包括实模式1MB的线性地址空间、内存寻址方法、寄存器、端口读写以及中断处理方法等内容。到了80386时代,引进了一种沿用至今的CPU运行机制——保护模式(Protected Mode)。保护模式有一些新的特色,用来增强系统稳定度,比如内存保护,分页系统,以及硬件支持的虚拟内存等。

对CPU来讲,系统中的所有储存器中的储存单元都处于一个统一的逻辑储存器中,它的容量受CPU寻址能力的限制。

这个逻辑储存器就是我们所说的线性地址空间。8086有20位地址线,拥有1MB的线性地址空间。而80386有32位地址线,拥有4GB的线性地
址空间。但是80386依旧保留了8086采用的地址分段的方式,只是增加了一个折中的方案,即只分一个段,段基址0×00000000,段长0xFFFFFFFF(4GB),这样的话整个线性空间可以看作就一个段,这就是所谓的平坦模型(Flat Mode)» Read more: OS的分页分段(笔记)

JOS的物理页表分配实现

March 4th, 2014

最近将JOS的启动流程学习了一遍,细化了系统从real mode到32-bit的转变。
但是在系统真正要接管内存,分配真正页表之前,要有一个简单的页表将kernel code载入到内存中。

我们先使用一个static void i386_detect_memory(void) 函数来探测物理内存一共有多少pages,一个page为4K.
另外我们看一下JOS的memery map就可以发现我们做的工作,写这部分一定要有好的大局观。知道每部分初始化位于内存的哪个位置。

» Read more: JOS的物理页表分配实现

打印Frame Info值

February 24th, 2014

最近在做JOS的系统实现,遇到一个小小的问题在于%.*s的用法。
» Read more: 打印Frame Info值