詳解ARM-linux的啟動流程[轉帖]

NO IMAGE

首先,porting linux的時候要規劃記憶體影像,如小弟的系統有64m SDRAM, 地址從0x 0800 0000 -0x0bff ffff,32m flash,地址從0x0c00 0000-0x0dff ffff.
規劃如下:bootloader, linux kernel, rootdisk放在flash裡。具體從 0x0c00 0000開始的第一個1M放bootloader,0x0c10 0000開始的2m放linux kernel,從 0x0c30 0000開始都給rootdisk。
 

啟動:
首先,啟動後arm920T將地址0x0c00 0000對映到0(可通過跳線設定), 實際上從0x0c00 0000啟動,進入我們的bootloader,但由於flash速度慢, 所以bootloader前面有一小段程式把bootloader拷貝到SDRAM 中的0x0AFE0100, 再從0x 0800 0000 執行bootloader,我們叫這段小程式為flashloader, flashloader必須要首先初始化SDRAM,不然往那放那些東東:
 

.equ SOURCE, 0x0C000100 bootloader的存放地址
.equ TARGET, 0x0AFE0100 目標地址
.equ SDCTL0, 0x221000 SDRAM控制器暫存器
// size is stored in location 0x0C0000FC
 

.global _start
_start: file://入口點
file://;***************************************
file://;* Init SDRAM
file://;***************************************

// ;***************
// ;* SDRAM
// ;***************
LDR r1, =SDCTL0 //
// ; Set Precharge Command
LDR r3, =0x92120200
file://ldr r3,=0x92120251
STR r3, [r1]
// ; Issue Precharge All Commad
LDR r3, =0x8200000
LDR r2, [r3]
// ; Set AutoRefresh Command
LDR r3, =0xA2120200
STR r3, [r1]
// ; Issue AutoRefresh Command
LDR r3, =0x8000000
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
LDR r2, [r3]
// ; Set Mode Register
LDR r3, =0xB2120200
STR r3, [r1]
// ; Issue Mode Register Command
LDR r3, =0x08111800 //; Mode Register value
LDR r2, [r3]
// ; Set Normal Mode
LDR r3, =0x82124200
STR r3, [r1]
file://;***************************************
file://;* End of SDRAM and SyncFlash Init *
file://;***************************************

// copy code from FLASH to SRAM
_CopyCodes:
ldr r0,=SOURCE
ldr r1,=TARGET
sub r3,r0,#4
ldr r2,[r3]

_CopyLoop:
ldr r3,[r0]
str r3,[r1]
add r0,r0,#4
add r1,r1,#4
sub r2,r2,#4
teq r2,#0
beq _EndCopy
b _CopyLoop
 

_EndCopy:
ldr r0,=TARGET
mov pc,r0

在flashloader把bootloader load到0x0AFE0100後, 然回跳了過去,其實0x0AFE0100 就是燒在flash 0x0C000100中的真正的bootloader: bootloader 有幾個檔案組成,先是START.s,也是唯一的一個彙編程式,其餘的都是C寫成的,START.s主要初始化堆疊:

_start:
ldr r1,=StackInit
ldr sp,[r1]
b main
//此處我們跳到了C程式碼的main函式,當C程式碼執行完後,還要呼叫
//下面的JumpToKernel0x跳到LINXU kernel執行
.equ StackInitvalue, __end_data 0x1000 // 4K __end_data在連結指令碼中指定
StackInit:
.long StackInitvalue
.global JumpToKernel
JumpToKernel:
// jump to the copy code (get the arguments right)
mov pc, r0
.global JumpToKernel0x
// r0 = jump address
// r1-r4 = arguments to use (these get shifted)
JumpToKernel0x:
// jump to the copy code (get the arguments right)
mov r8, r0
mov r0, r1
mov r1, r2
mov r2, r3
mov r3, r4
mov pc, r8
.section “.data.boot”
.section “.bss.boot”

下面讓我們看看bootloader的c程式碼幹了些什麼。main函式比較長,讓我們分段慢慢看。

int main()
{
U32 *pSource, *pDestin, count;
U8 countDown, bootOption;
U32 delayCount;
U32 fileSize, i;
char c;
char *pCmdLine;
char *pMem;

init(); //初始化FLASH控制器和CPU時鐘

EUARTinit(); //串列埠初始化
EUARTputString(“/n/nDBMX1 Linux Bootloader ver 0.2.0/n”);
EUARTputString(“Copyright (C) 2002 Motorola Ltd./n/n”);
EUARTputString((U8 *)cmdLine);
EUARTputString(“/n/n”);

EUARTputString(“Press any key for alternate boot-up options … “);

小弟的bootloader主要幹這麼幾件事:init(); 初始化硬體,列印一些資訊和提供一些操作選項:
0. Program bootloader image
1. Program kernel image
2. Program root-disk image
3. Download kernel and boot from RAM
4. Download kernel and boot with ver 0.1.x bootloader format
5. Boot a ver0.1.x kernel
6. Boot with a different command line
也就是說,可以在bootloader裡選擇重新下載kernel,rootdisk並寫入flash,
下載的方法是用usb連線,10m的rootdisk也就刷的一下。關於usb下載的討論請參看先前的貼子“為arm開發平臺增加usb下載介面“。 如果不選,直接回車,就開始把整個linux的核心拷貝到SDRAM中執行。列位看官,可能有人要問,在flashloader中不是已經初始化過sdram控制器了嗎?怎麼init(); 中還要初始化呢,各位有所不知,小弟用的是syncflash,可以直接使用sdram控制器的介面,切記:在flash中執行的程式碼是不能初始化連線flash的sdram控制器的,不然絕對死掉了。所以,當程式在flash中執行的時候,去初始化sdram,而現在在sdram中執行,可放心大膽地初始化flash了,主要是設定字寬,行列延時,因為預設都是最大的。
另外,如果列位看官的cpu有足夠的片內ram,完全可以先把bootloader放在片內ram,幹完一切後再跳到LINUX,小弟著也是不得已而為之啊。如果直接輸入回車,進入kernel拷貝工作:

EUARTputString(“Copying kernel from Flash to RAM …/n”);
count = 0x200000; // 2 Mbytes
pSource = (U32 *)0x0C100000;
pDestin = (U32 *)0x08008000;
do
{
*(pDestin ) = *(pSource );
count -= 4;
} while (count > 0);
}

EUARTputString(“Booting kernel …/n/n”);

這一段沒有什麼可說的,執行完後kernel就在0x08008000了,至於為什麼要空出0x8000的一段,主要是放kelnel的一些全域性資料結構,如核心頁表,arm的頁目錄要有16k大。
我們知道,linux核心啟動的時候可以傳入引數,如在PC上,如果使用LILO, 當出現LILO:,我們可以輸入root=/dev/hda1.或mem=128M等指定檔案系統的裝置或記憶體大小,在嵌入式系統上,引數的傳入是要靠bootloader完成的,
pMem = (char *)0x083FF000; //引數字串的目標存放地址
pCmdLine = (char *)&cmdLine; //定義的靜態字串
while ((*(pMem )=*(pCmdLine )) != 0);//拷貝

JumpToKernel((void *)0x8008000, 0x083FF000) ;//跳轉到核心

return (0);
JumpToKernel在前文中的start.S定義過:

JumpToKernel:
// jump to the copy code (get the arguments right)
mov pc, r0

.global JumpToKernel0x
// r0 = jump address
// r1 = arguments to use (these get shifted)

由於arm-GCC的c引數呼叫的順序是從左到右R0開始,所以R0是KERNKEL的地址,r1是引數字串的地址:

到此為止,為linux引導做的準備工作就結束了,下一回我們就正式進入linux的程式碼。

好,從本節開始,我們走過了bootloader的漫長征途,開始進入linux的核心: 說實話,linux寶典的確高深莫測,洋人花了十幾年**,各種內功心法層處不窮。有些地方反覆推敲也領悟不了其中奧妙,煉不到第九重啊。
linux的入口是一段彙編程式碼,用於基本的硬體設定和建立臨時頁表,對於
ARM LINUX是 linux/arch/arm/kernle/head-armv.S, 走!

#if defined(CONFIG_MX1)
mov r1, #MACH_TYPE_MX1
#endif

這第一句話好像就讓人看不懂,好像葵花寶典開頭的八個字:欲練神功。。。。
那來的MACH_TYPE_MX1?其實,在head-armv.S 中的一項重要工作就是設定核心的臨時頁表,不然mmu開起來也玩不轉,但是核心怎麼知道如何對映記憶體呢?linux的核心將對映到虛地址0xCxxx xxxx處,但他怎麼知道把哪一片ram對映過去呢?
因為不通的系統有不通的記憶體影像,所以,LINUX約定,核心程式碼開始的時候,R1放的是系統目標平臺的代號,對於一些常見的,標準的平臺,核心已經提供了支援,只要在編譯的時候選中就行了,例如對X86平臺,核心是從實體地址1M開始對映的。如果老兄是自己攢的平臺,只好麻煩你自己寫了。
小弟拿人錢財,與人消災,用的是摩托的MX1,只好自己寫了,定義了#MACH_TYPE_MX1,當然,還要寫一個描述平臺的資料結構:
MACHINE_START(MX1ADS, “Motorola MX1ADS”)
MAINTAINER(“SPS Motorola”)

BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)

FIXUP(mx1ads_fixup)
MAPIO(mx1ads_map_io)
INITIRQ(mx1ads_init_irq)
MACHINE_END

看起來怪怪的,但現在大家只要知道他定義了基本的記憶體映象:RAM從0x08000000開始,i/o空間從0x00200000開始,i/o空間對映到虛擬地址空間0xf0200000開始處。摩托的晶片i/o和記憶體是統一編址的。
其他的項,在下面的初始化過程中會逐個介紹到。
好了好了,再看下面的指令:

mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode //設定為SVC模式,允許中斷和快速中斷
//此處設定系統的工作狀態,arm有7種狀態
//每種狀態有自己的堆疊

msr cpsr_c, r0 @ and all irqs diabled
bl __lookup_processor_type

//定義處理器相關資訊,如value, mask, mmuflags,
//放在proc.info段中
//__lookup_processor_type 取得這些資訊,在下面
//__lookup_architecture_type 中用

這一段是查詢處理器的種類,大家知道arm有arm7, arm9等型別,如何區分呢?
在arm協處理器中有一個只讀暫存器,存放處理器相關資訊。__lookup_processor_type將返回如下的結構:

__arm920_proc_info:
.long 0x41009200 //CPU id
.long 0xff00fff0 //cpu mask
.long 0x00000c1e @ mmuflags
b __arm920_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_26BIT
.long cpu_arm920_info
.long arm920_processor_functions

第一項是CPU id,將與協處理器中讀出的id作比較,其餘的都是與處理器相關的資訊,到下面初始化的過程中自然會用到。
查詢到了處理器型別和系統的記憶體映像後就要進入初始化過程中比較關鍵的一步了,開始設定mmu,但首先要設定一個臨時的核心頁表,對映4m的記憶體,這在初始化過程中是足夠了:

//r5=0800 0000 ram起始地址 r6=0020 0000 io地址,r7=f020 0000 虛io
teq r7, #0 @ invalid architecture?
moveq r0, #’a’ @ yes, error ‘a’
beq __error
bl __create_page_tables

其中__create_page_tables為:
__create_page_tables:
pgtbl r4
//r4=0800 4000 臨時頁表的起始地址
//r5=0800 0000, ram的起始地址
//r6=0020 0000, i/o暫存器空間的起始地址
//r7=0000 3c08
//r8=0000 0c1e

//the page table in 0800 4000 is just temp base page, when init_task’s sweaper_page_dir ready,
// the temp page will be useless
// the high 12 bit of virtual address is base table index, so we need 4kx4 = 16k temp base page,

mov r0, r4
mov r3, #0
add r2, r0, #0x4000 @ 16k of page table
1: str r3, [r0], #4 @ Clear page table
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r2
bne 1b
/*
* Create identity mapping for first MB of kernel.
* This is marked cacheable and bufferable.
*
* The identity mapping will be removed by
*/
// 由於linux編譯的地址是0xC0008000,load的地址是0x08008000,我們需要將虛地址0xC0008000對映到0800800一段
//同時,由於部分程式碼也要直接訪問0x08008000,所以0x08008000對應的表項也要填充
// 頁表中的表象為section,AP=11表示任何模式下可訪問,domain為0。
add r3, r8, r5 @ mmuflags start of RAM
//r3=0800 0c1e
add r0, r4, r5, lsr #18
//r0=0800 4200
str r3, [r0] @ identity mapping
//*0800 4200 = 0800 0c1e 0x200表象 對應的是0800 0000 的1m
/*
* Now setup the pagetables for our kernel direct
* mapped region. We round TEXTADDR down to the
* nearest megabyte boundary.
*/
//下面是對映4M

add r0, r4, #(TEXTADDR & 0xfff00000) >> 18 @ start of kernel
//r0 = r4 0x3000 = 0800 4000 3000 = 0800 7000
str r3, [r0], #4 @ PAGE_OFFSET 0MB
//*0800 7004 = 0800 0c1e
add r3, r3, #1 << 20
//r3=0810 0c1e
str r3, [r0], #4 @ PAGE_OFFSET 1MB
//*0800 7008 = 0810 0c1e
add r3, r3, #1 << 20
str r3, [r0], #4
//*0800 700c = 0820 0c1e @ PAGE_OFFSET 2MB
add r3, r3, #1 << 20
str r3, [r0], #4 @ PAGE_OFFSET 3MB
//*0800 7010 = 0830 0c1e

bic r8, r8, #0x0c @ turn off cacheable
//r8=0000 0c12 @ and bufferable bits
mov pc, lr //子程式返回。
下一回就要開始開啟mmu的操作了

上回書講到已經設定好了核心的頁表,然後要跳轉到__arm920_setup,這個函式在arch/arm/mm/proc-arm929.s

__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, [email protected] drain write buffer on v4
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
mcr p15, 0, r4, c2, c0 @ load page table pointer
mov r0, #0x1f @ Domains 0, 1 = client
mcr p15, 0, r0, c3, c0 @ load domain access register
mrc p15, 0, r0, c1, c0 @ get control register v4
/*
* Clear out ‘unwanted’ bits (then put them in if we need them)
*/
@ VI ZFRS BLDP WCAM
bic r0, r0, #0x0e00
bic r0, r0, #0x0002
bic r0, r0, #0x000c
bic r0, r0, #0x1000 @ …0 000. …. 000.
/*
* Turn on what we want
*/
orr r0, r0, #0x0031
orr r0, r0, #0x2100 @ ..1. …1 ..11 …1

#ifdef CONFIG_CPU_ARM920_D_CACHE_ON
orr r0, r0, #0x0004 @ …. …. …. .1..
#endif
#ifdef CONFIG_CPU_ARM920_I_CACHE_ON
orr r0, r0, #0x1000 @ …1 …. …. ….
#endif
mov pc, lr

這一段首先關閉i,d cache,清除write buffer ,然後設定頁目錄地址,設定domain的保護,在上節中,注意到頁目錄項的domain都是0,domain暫存器中的domain 0 對應的是0b11,表示訪問模式為manager,不受限制。
接下來設定控制暫存器,開啟d,i cache和mmu 注意arm的d cache必須和mmu一起開啟,而i cache可以單獨開啟其實,cache和mmu的關係實在是緊密,每一個頁表項都有標誌標示是否是cacheable的,可以說本來就是設計一起使用的最後,自函式返回後,有一句mcr p15, 0, r0, c1, c0 使設定生效。

上回我們講到arm靠初始化完成了,開啟了cache, 到此為止,彙編部分的初始化程式碼就差不多了,最後還有幾件事情做:

1。初始化BSS段,全部清零,BSS是全域性變數區域。
2。儲存與系統相關的資訊:如
.long SYMBOL_NAME(compat)
.long SYMBOL_NAME(__bss_start)
.long SYMBOL_NAME(_end)
.long SYMBOL_NAME(processor_id)
.long SYMBOL_NAME(__machine_arch_type)
.long SYMBOL_NAME(cr_alignment)
.long SYMBOL_NAME(init_task_union) 8192
不用講,大家一看就明白意思
3。重新設定堆疊指標,指向init_task的堆疊。init_task是系統的第一個任務,init_task的堆疊在task structure的後8K,我們後面會看到。
4。最後就要跳到C程式碼的start_kernel。
b SYMBOL_NAME(start_kernel)
現在讓我們來回憶一下目前的系統狀態:
臨時頁表已經建立,在0X08004000處,對映了4M,虛地址0XC000000被對映到0X08000000.
CACHE,MMU都已經開啟。
堆疊用的是任務init_task的堆疊。
如果以為到了c程式碼可以鬆一口氣的話,就大錯特措了,linux的c也不比彙編好懂多少,相反到掩蓋了彙編的一些和機器相關的部分,有時候更難懂。其實作為編寫作業系統的c程式碼,只不過是彙編的另一種寫法,和機器程式碼的聯絡是很緊密的。

start_kernel在 /linux/init/main.c中定義:

asmlinkage void __init start_kernel(void)
{
char * command_line;
unsigned long mempages;
extern char saved_command_line[];
lock_kernel();
printk(linux_banner);
setup_arch(&command_line); //arm/kernel/setup.c
printk(“Kernel command line: %s/n”, saved_command_line);
parse_options(command_line);

trap_init(); // arm/kernle/traps.c install
。。。。。。。。。

start_kernel中的函式個個都是重量級的,首先用printk(linux_banner);打出系統版本號,這裡面就大有文章,系統才剛開張,你讓他列印到哪裡去呢?先給大家交個底,以後到console的部分自然清楚,printk和printf不同,他首先輸出到系統的一個緩衝區內,大約4k,如果登記了console,則呼叫console->wirte函式輸出,否則就一直在buffer裡呆著。所以,用printk輸出的資訊,如果超出了4k,會沖掉前面的。在系統引導起來後,用dmesg看的也就是這個buffer中的東東。

下面就是一個重量級的函式:
setup_arch(&command_line); //arm/kernel/setup.c
完成記憶體映像的初始化,其中command_line是從bootloader中傳下來的。

void __init setup_arch(char **cmdline_p)
{
struct param_struct *params = NULL;
struct machine_desc *mdesc; //arch structure, for your ads, defined in include/arm-asm/mach/arch.h very long
struct meminfo meminfo;
char *from = default_command_line;

memset(&meminfo, 0, sizeof(meminfo));

首先把meminfo清零,有個背景介紹一下,從linux 2.4的核心開始,支援記憶體的節點(node),也就是可支援不連續的實體記憶體區域。這一點在嵌入式系統中很有用,例如對於SDRAM和FALSH,性質不同,可作為不同的記憶體節點。

meminfo結構定義如下:

/******************************************************/
#define NR_BANKS 4
//define the systen mem region, not consistent
struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};
/******************************************************/

下面是:ROOT_DEV = MKDEV(0, 255); ROOT_DEV是巨集,指明啟動的裝置,嵌入式系統中通常是flash disk.
這裡面有一個有趣的悖論:linux的裝置都是在/dev/下,訪問這些裝置檔案需要裝置驅動程式支援,而訪問裝置檔案才能取得裝置號,才能載入驅動程式,那麼第一個裝置驅動程式是怎麼載入呢?就是ROOT_DEV, 不需要訪問裝置檔案,直接指定裝置號。下面我們準備初始化真正的核心頁表,而不再是臨時的了。
首先還是取得當前系統的記憶體映像:

mdesc = setup_architecture(machine_arch_type);
//find the machine type in mach-integrator/arch.c
//the ads name, mem map, io map

返回如下結構:
mach-integrator/arch.c

MACHINE_START(INTEGRATOR, “Motorola MX1ADS”)
MAINTAINER(“ARM Ltd/Deep Blue Solutions Ltd”)
BOOT_MEM(0x08000000, 0x00200000, 0xf0200000)
FIXUP(integrator_fixup)
MAPIO(integrator_map_io)
INITIRQ(integrator_init_irq)
MACHINE_END

我們在前面介紹過這個結構,不過這次用它可是玩真的了。

書接上回,
下面是init_mm的初始化,init_mm定義在/arch/arm/kernel/init_task.c:
struct mm_struct init_mm = INIT_MM(init_mm);

從本回開始的相當一部分內容是和記憶體管理相關的,憑心而論,作業系統的記憶體管理是很複雜的,牽扯到處理器的硬體細節和軟體演算法,限於篇幅所限制,請大家先仔細讀一讀arm mmu的部分,
中文參考資料:linux核心原始碼情景對話,
linux2.4.18原始碼分析。

init_mm.start_code = (unsigned long) &_text;
核心程式碼段開始
init_mm.end_code = (unsigned long) &_etext;
核心程式碼段結束
init_mm.end_data = (unsigned long) &_edata;
核心資料段開始
init_mm.brk = (unsigned long) &_end;
核心資料段結束

每一個任務都有一個mm_struct結構管理任務記憶體空間,init_mm 是核心的mm_struct,其中設定成員變數* mmap指向自己,
意味著核心只有一個記憶體管理結構,設定* pgd=swapper_pg_dir,swapper_pg_dir是核心的頁目錄,在arm體系結構有16k,
所以init_mm定義了整個kernel的記憶體空間,下面我們會碰到核心執行緒,所有的核心執行緒都使用核心空間,擁有和核心同樣的訪問
許可權。

memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
//clear command array

saved_command_line[COMMAND_LINE_SIZE-1] = ‘/0’;
//set the end flag

parse_cmdline(&meminfo, cmdline_p, from);
//將bootloader的引數拷貝到cmdline_p,

bootmem_init(&meminfo);
定義在arm/mm/init.c
這個函式在核心結尾分一頁出來作點陣圖,根據具體系統的記憶體大小 對映整個ram

下面是一個非常重要的函式
paging_init(&meminfo, mdesc);
定義在arm/mm/init.c
建立核心頁表,對映所有實體記憶體和io空間,對於不同的處理器,這個函式差別很大,

void __init paging_init(struct meminfo *mi, struct machine_desc *mdesc)
{
void *zero_page, *bad_page, *bad_table;
int node;

//static struct meminfo meminfo __initdata = { 0, };

memcpy(&meminfo, mi, sizeof(meminfo));

/*
* allocate what we need for the bad pages.
* note that we count on this going ok.
*/

zero_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_page = alloc_bootmem_low_pages(PAGE_SIZE);
bad_table = alloc_bootmem_low_pages(TABLE_SIZE);

分配三個頁出來,用於處理異常過程,在armlinux中,得到如下 地址:
zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000

上回我們說到在paging_init中分配了三個頁:
zero_page=0xc0000000
bad page=0xc0001000
bad_table=0xc0002000

但是奇怪的很,在更新的linux程式碼中只分配了一個 zero_page,而且在原始碼中找不到zero_page用在什麼地方了,大家討論討論吧。

paging_init的主要工作是在void __init memtable_init(struct meminfo *mi) 中完成的,為系統記憶體建立頁表:
meminfo結構如下:

struct meminfo {
int nr_banks;
unsigned long end;
struct {
unsigned long start;
unsigned long size;
int node;
} bank[NR_BANKS];
};

是用來紀錄系統中的記憶體區段的,因為在嵌入式系統中並不是所有的記憶體都能對映,例如sdram只有64m,flash 32m,而且不見得是連續的,所以用meminfo紀錄這些區段。

void __init memtable_init(struct meminfo *mi)
{
struct map_desc *init_maps, *p, *q;
unsigned long address = 0;
int i;

init_maps = p = alloc_bootmem_low_pages(PAGE_SIZE);

其中map_desc定義為:

struct map_desc {
unsigned long virtual;
unsigned long physical;
unsigned long length;
int domain:4, //頁表的domain
prot_read:1, //保護標誌
prot_write:1, //防寫標誌
cacheable:1, //是否cache
bufferable:1, //是否用write buffer
last:1; //空
};init_maps

map_desc是區段及其屬性的定義,屬性位的意義請參考ARM MMU的介紹。
下面對meminfo的區段進行遍歷,同時填寫init_maps 中的各項內容:
for (i = 0; i < mi->nr_banks; i ) {
if (mi->bank.size == 0)
continue;

p->physical = mi->bank.start;
p->virtual = __phys_to_virt(p->physical);
p->length = mi->bank.size;
p->domain = DOMAIN_KERNEL;
p->prot_read = 0;
p->prot_write = 1;
p->cacheable = 1; //可以CACHE
p->bufferable = 1; //使用write buffer
p ; //下一個區段
}

如果系統有flash,
#ifdef FLUSH_BASE
p->physical = FLUSH_BASE_PHYS;
p->virtual = FLUSH_BASE;
p->length = PGDIR_SIZE;
p->domain = DOMAIN_KERNEL;
p->prot_read = 1;
p->prot_write = 0;
p->cacheable = 1;
p->bufferable = 1;

p ;
#endif

其中的prot_read和prot_write是用來設定頁表的domain的,

下面就是逐個區段建立頁表:

q = init_maps;
do {
if (address < q->virtual || q == p) {
clear_mapping(address);
address = PGDIR_SIZE;
} else {
create_mapping(q);

address = q->virtual q->length;
address = (address PGDIR_SIZE – 1) & PGDIR_MASK;

q ;
}
} while (address != 0);

上次說到memtable_init中初始化頁表的迴圈,這個過程比較重要,我們看仔細些:

q = init_maps;
do {
if (address < q->virtual || q == p) {
//由於核心空間是從c000 0000開始,所以c000 0000
//以前的頁表項全部清空

clear_mapping(address);
address = PGDIR_SIZE;
//每個表項增加1m,這裡感到了section的好處
}

其中clear_mapping()是個巨集,根據處理器的
不同,在920下被展開為

cpu_arm920_set_pmd(((pmd_t *)(((&init_mm )->pgd
(( virt) >> 20 )))),((pmd_t){( 0 )}));

其中init_mm為核心的mm_struct,pgd指向
swapper_pg_dir,在arch/arm/kernel/init_task.c中定義

ENTRY(cpu_arm920_set_pmd)
#ifdef CONFIG_CPU_ARM920_WRITETHROUGH
eor r2, r1, #0x0a
tst r2, #0x0b
biceq r1, r1, #4
#endif
str r1, [r0]

把pmd_t填寫到頁表項中,由於pmd_t=0,實際等於清除了這一項,由於d cache開啟,這一條指令實際並沒有寫回記憶體,而是寫到cache中

mcr p15, 0, r0, c7, c10, 1

把cache中 地址r0對應的內容寫回記憶體中,這一條語句實際是寫到了write buffer中, 還沒有真正寫回記憶體。

mcr p15, 0, r0, c7, c10, 4

等待把write buffer中的內容寫回記憶體。在這之前core等待
mov pc, lr

在這裡我們看到,由於頁表的內容十分關鍵,為了確保寫回記憶體,採用了直接操作cache的方法。由於在arm core中,開啟了d cache 則必定要用write buffer.所以還有wb的回寫問題。由於考慮到效率,我們使用了cache和buffer, 所以在某些地方要用指令保證資料被及時寫回。

下面對映c000 0000後面的頁表

else {
create_mapping(q);

address = q->virtual q->length;
address = (address PGDIR_SIZE – 1) & PGDIR_MASK;

q ;
}
} while (address != 0);

create_mapping也在mm-armv.c中定義;

static void __init create_mapping(struct map_desc *md)
{
unsigned long virt, length;
int prot_sect, prot_pte;
long off;

prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read ? L_PTE_USER : 0) |
(md->prot_write ? L_PTE_WRITE : 0) |
(md->cacheable ? L_PTE_CACHEABLE : 0) |
(md->bufferable ? L_PTE_BUFFERABLE : 0);

prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read ? PMD_SECT_AP_READ : 0) |
(md->prot_write ? PMD_SECT_AP_WRITE : 0) |
(md->cacheable ? PMD_SECT_CACHEABLE : 0) |
(md->bufferable ? PMD_SECT_BUFFERABLE : 0);

由於arm中section表項的許可權位和page表項的位置不同,所以根據struct map_desc 中的保護標誌,分別計算頁表項中的,domain,CB標誌位。有一段時間沒有寫了,道歉先,前一段時間在做arm linux的xip,終於找到了 在flash中執行kernel的方法,同時對系統的儲存管理的理解更深了一層,我們繼續從上回的create_mapping往下看:

while ((virt & 0xfffff || (virt off) & 0xfffff) && length >= PAGE_SIZE) {
alloc_init_page(virt, virt off, md->domain, prot_pte);

virt = PAGE_SIZE;
length -= PAGE_SIZE;
}

while (length >= PGDIR_SIZE) {
alloc_init_section(virt, virt off, prot_sect);

virt = PGDIR_SIZE;
length -= PGDIR_SIZE;
}

while (length >= PAGE_SIZE) {
alloc_init_page(virt, virt off, md->domain, prot_pte);

virt = PAGE_SIZE;
length -= PAGE_SIZE;
}
這3個迴圈的設計還是很巧妙的,create_mapping的作用是設定虛地址virt 到實體地址virt off的對映頁目錄和頁表。arm提供了4種尺寸的頁表:1M,4K,16K,64K,armlinux只用到了1M和4K兩種。

這3個while的作用分別是“掐頭“,“去尾“,“砍中間“。第一個while是判斷要對映的地址長度是否大於1m,且是不是1m對齊,如果不是,則需要建立頁表,例如,如果要對映的長度為1m零4k,則先要將“零頭“ 去掉,4k的一段需要中間頁表,通過第一個while建立中間頁表,而剩下的1M則交給第二個while迴圈。最後剩下的交給第三個while迴圈。

alloc_init_page分配並填充中間頁表項
static inline void
alloc_init_page(unsigned long virt, unsigned long phys, int domain, int prot)
{
pmd_t *pmdp;
pte_t *ptep;

pmdp = pmd_offset(pgd_offset_k(virt), virt);//返回頁目錄中virt對應的表項

if (pmd_none(*pmdp)) {//如果表項是空的,則分配一箇中間頁表
pte_t *ptep = alloc_bootmem_low_pages(2 * PTRS_PER_PTE *
sizeof(pte_t));

ptep = PTRS_PER_PTE;
//設定頁目錄表項
set_pmd(pmdp, __mk_pmd(ptep, PMD_TYPE_TABLE | PMD_DOMAIN(domain)));
}
ptep = pte_offset(pmdp, virt);
//如果表項不是空的,則表項已經存在,只需要設定中間頁表表項
set_pte(ptep, mk_pte_phys(phys, __pgprot(prot)));
}

alloc_init_section只需要填充頁目錄項

alloc_init_section(unsigned long virt, unsigned long phys, int prot)
{
pmd_t pmd;

pmd_val(pmd) = phys | prot;//將實體地址和保護標誌合成頁目錄項

set_pmd(pmd_offset(pgd_offset_k(virt), virt), pmd);
}

通過create_mapping可為核心建立所有的地址對映,最後是對映中斷向量表所在的區域:

init_maps->physical = virt_to_phys(init_maps);
init_maps->virtual = vectors_base();
init_maps->length = PAGE_SIZE;
init_maps->domain = DOMAIN_USER;
init_maps->prot_read = 0;
init_maps->prot_write = 0;
init_maps->cacheable = 1;
init_maps->bufferable = 0;

create_mapping(init_maps);

中斷向量表的虛地址init_maps,是用alloc_bootmem_low_pages分配的,通常是在c000 8000前面的某一頁, vectors_base()是個巨集,arm規定中斷向量表的地址只能是0或ffff0000,在cp15中設定。所以上述程式碼對映一頁到0或ffff0000,下面我們還會看到,中斷處理程式中的彙編部分也被拷貝到這一頁中。