Linux內核啓動-開啓頁面映射

 在setup的幫助下,我們順利地從16位實地址模式過渡到32位段式尋址的保護模式。又在arch/i386/boot/compressed/head.S的幫助下實現了內核的自解壓,並且從arch/i386/kernel/head.S中的startup_32開始。現在在線性地址0x100000(1M)處開始就是我們的解壓後的內核了。而startup_32()的地址恰好是0x100000。由於還沒有開啓頁面映射,所以必須引用變量的線性地址(即變量的虛擬地址-PAGE_OFFSET),帶來了很多不便。所以下一步的任務,就是建立頁表,開啓頁面映射了。我們不妨從arch/i386/kernel/head.S入手。

由於在Linux中,每個進程擁有一個頁表,那麼,第一個頁表也應該有一個對應的進程。通常情況下,Linux下通過fork()系統調用,複製原有進程,來產生新進程。然而第一個進程該如何產生呢?既然不能複製,那就只能像女媧造人一樣,以全局變量的方式捏造一個出來。它就是init_thread_union。傳說中的0號進程,名叫swapper。只要swapper進程運行起來,調用start_kernel(),剩下的事就好辦了。不過,現在離運行swapper進程還差得很遠。關鍵的一步,我們還沒有爲該進程設置頁表。

爲了保持可移植性,Linux採用了三級頁表。不過x86處理器只使用兩級頁表。所以,我們需要一個頁目錄和很多個頁表(最多達1024個頁表),頁目錄和頁表的大小均爲4k。swapper的頁目錄的創建與該進程的創建思維類似,也是捏造一個頁表,叫swapper_pg_dir.

417 ENTRY(swapper_pg_dir)

418         .fill 1024,4,0

它的意思是從swapper_pg_dir開始,填充1024項,每項爲4字節,值爲0,正好是4K一個頁面。

頁目錄有了,接下去看頁表。一個問題產生了。該映射幾個頁表呢?儘管一個頁目錄最多能映射1024個頁表,每個頁表映射4M虛擬地址,所以總共可以映射4G虛擬地址空間。但是,通常應用程序用不了這麼多。最簡單的想法是,夠用就行。先映射用到的代碼和數據。還有一個問題:如何映射呢?運行cat /proc/$pid/maps可以看到,用戶態進程的地址映射是斷斷續續的,相當複雜。這是由於不同進程的用戶空間相互獨立。但是,由於所有進程共享內核態代碼和數據,所以映射關係可以大大簡化。既然內核態虛擬地址從3G開始,而內核代碼和數據事實上是從物理地址0x100000開始,那麼本着KISS原則,一切從簡,加上3G就作爲對應的虛擬地址好了。由此可見,對內核態代碼和數據來說:虛擬地址=物理地址+PAGE_OFFSET(3G)

內核中有變量pg0,表示對應的頁表。建立頁表的過程如下:

091 page_pde_offset = (__PAGE_OFFSET >> 20);

092

093         movl $(pg0 - __PAGE_OFFSET), %edi

094         movl $(swapper_pg_dir - __PAGE_OFFSET), %edx

095         movl $0x007, %eax                       /* 0x007 = PRESENT+RW+USER */

096 10:

097         leal 0x007(%edi),%ecx                   /* Create PDE entry */

098         movl %ecx,(%edx)                        /* Store identity PDE entry */

099         movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */

100         addl $4,%edx

101         movl $1024, %ecx

102 11:

103         stosl

104         addl $0x1000,%eax

105         loop 11b

106         /* End condition: we must map up to and including INIT_MAP_BEYOND_END */

107         /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */

108         leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp

109         cmpl %ebp,%eax

110         jb 10b

111         movl %edi,(init_pg_tables_end - __PAGE_OFFSET)

用僞代碼表示就是:

typedef unsigned int PTE;

PTE *pg=pg0;

PTE pte=0x007;

for(i=0;;i++){//把線性地址i*4MB~(i+1)*4MB-1(用戶空間地址)和3G+i*4MB~3G+(i+1)*4MB-1(內核空間地址)映射到物理地址i*4MB~(i+1)*4MB-1

swapper_pg_dir[i]=pg+0x007;

swapper_pg_dir[i+page_pde_offset]=pg+0x007;

for(j=0;j<1024;j++){

pte+=0x1000;

pg[i*1024+j]=pte;

}

if(pte>=((char*)pg+i*1024+j)*4+0x007+INIT_MAP_BEYOND_END)

{

init_pg_tables_end=pg+i*0x1000+j;

break;

}

}

大致意思是從0開始,把連續的線性地址映射到物理地址。這裏的0x007是什麼意思呢?由於每個頁表項有32位,但其實只需保存物理地址的高20位就夠了,所以剩下的低12位可以用來表示頁的屬性。0x007正好表示PRESENT+RW+USER(在內存中,可讀寫,用戶頁面,這樣在用戶態和內核態都可讀寫,從而實現平滑過渡)。

那麼結束條件是什麼呢?從代碼中可知,當映射到當前所操作的頁表項往下INIT_MAP_BEYOND_END(128K)處映射結束。nm vmlinux|grep pg0得c0595000。據此可以計算總共映射了多少頁(小學計算題:P)

所以映射了2個頁表,映射地址從0x0~0x2000-1,大小爲8M。

最後,關鍵時刻到來了:


183 /*

184  * Enable paging

185  */

186         movl $swapper_pg_dir-__PAGE_OFFSET,%eax

187         movl %eax,%cr3          /* set the page table pointer.. */

188         movl %cr0,%eax

189         orl $0x80000000,%eax

190         movl %eax,%cr0          /* ..and set paging (PG) bit */

開啓頁面映射後,可以直接引用內核中的所有變量了。不過離start_kernel還有點距離。要啓動swapper進程,得首先設置內核堆棧。

193         /* Set up the stack pointer */

194         lss stack_start,%esp

然後設置中斷向量表,看到久違的"call"了

215         call setup_idt

檢查CPU類型

載入gdt(原來的gdt是臨時的)和ldt

302         lgdt cpu_gdt_descr

303         lidt idt_descr

最後,調用start_kernel

327         call start_kernel

到這一步,我們的目的地終於走到了。在擺脫了晦澀的彙編之後,接下去的代碼,雖然與用戶態程序相比,還有中斷,同步等等的干擾,但相比較而言就好懂很多了。


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/TopEmbedded/archive/2009/02/24/3933646.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章