start_kernel分析

start_kernel分析

如果以爲到了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 中的保護標誌,分別計算頁表項
中的AP,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,下面我們還會看到,中斷處理程序中的彙編部分也被拷貝到
這一頁中。

發佈了1 篇原創文章 · 獲贊 2 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章