在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