LINUX start_kernel 分析,一位大师级的人物写的,不看要后悔的哟!!

一位大师级的人物写的,不看要后悔的哟!!

如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反到掩盖了汇编的一些和机器相关的部分,有时候更难懂。其实作为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。
start_kernel在 /linux/init/main.c中定义:

[cpp] view plain copy print ?
  1. asmlinkage void __init start_kernel(void)  
  2. {  
  3. char * command_line;  
  4. unsigned long mempages;  
  5. extern char saved_command_line[];  
  6. lock_kernel();  
  7. printk(linux_banner);  
  8. setup_arch(&command_line); //arm/kernel/setup.c  
  9. printk("Kernel command line: %s/n", saved_command_line);  
  10. parse_options(command_line);  
  11. trap_init(); // arm/kernle/traps.c install  
  12. ...... 
  13.  
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中传下来的。

[cpp] view plain copy print ?
  1. void __init setup_arch(char **cmdline_p)  
  2. {  
  3. struct param_struct *params = NULL;  
  4. struct machine_desc *mdesc; //arch structure, for your ads, defined in include/arm-asm/mach/arch.h very long  
  5. struct meminfo meminfo;  
  6. char *from = default_command_line;  
  7. memset(&meminfo, 0, sizeof(meminfo));  
  8.  
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结构定义如下:
[cpp] view plain copy print ?
  1. /******************************************************/  
  2. #define NR_BANKS 4  
  3. //define the systen mem region, not consistent 
  4. struct meminfo {  
  5. int nr_banks;  
  6. unsigned long end;  
  7. struct {  
  8. unsigned long start;  
  9. unsigned long size;  
  10. int node;  
  11. } bank[NR_BANKS];  
  12. };  
  13. /******************************************************/  
/******************************************************/ #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空间,
对于不同的处理器,这个函数差别很大,
[cpp] view plain copy print ?
  1. void __init paging_init(struct meminfo *mi,struct machine_desc *mdesc)  
  2. {  
  3. void *zero_page, *bad_page, *bad_table;  
  4. int node;  
  5. //static struct meminfo meminfo __initdata = { 0, }; 
  6. memcpy(&meminfo, mi, sizeof(meminfo));  
  7. /*
  8. * allocate what we need for the bad pages.
  9. * note that we count on this going ok.
  10. */  
  11. zero_page = alloc_bootmem_low_pages(PAGE_SIZE);  
  12. bad_page = alloc_bootmem_low_pages(PAGE_SIZE);  
  13. bad_table = alloc_bootmem_low_pages(TABLE_SIZE);  
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 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 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 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后面的页表

[cpp] view plain copy print ?
  1. else {  
  2. create_mapping(q);  
  3. address = q->virtual + q->length;  
  4. address = (address + PGDIR_SIZE - 1) & PGDIR_MASK;  
  5. q ++;  
  6. }  
  7. } while (address != 0);  
  8. create_mapping也在mm-armv.c中定义;  
  9. static void __init create_mapping(struct map_desc *md)  
  10. {  
  11. unsigned long virt, length;  
  12. int prot_sect, prot_pte;  
  13. long off;  
  14. prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |  
  15. (md->prot_read ? L_PTE_USER : 0) |  
  16. (md->prot_write ? L_PTE_WRITE : 0) |  
  17. (md->cacheable ? L_PTE_CACHEABLE : 0) |  
  18. (md->bufferable ? L_PTE_BUFFERABLE : 0);  
  19. prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |  
  20. (md->prot_read ? PMD_SECT_AP_READ : 0) |  
  21. (md->prot_write ? PMD_SECT_AP_WRITE : 0) |  
  22. (md->cacheable ? PMD_SECT_CACHEABLE : 0) |  
  23. (md->bufferable ? PMD_SECT_BUFFERABLE : 0);  
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往下看:

[cpp] view plain copy print ?
  1. while ((virt & 0xfffff || (virt + off) & 0xfffff) && length >= PAGE_SIZE) {  
  2. alloc_init_page(virt, virt + off, md->domain, prot_pte);  
  3. virt += PAGE_SIZE;  
  4. length -= PAGE_SIZE;  
  5. }  
  6. while (length >= PGDIR_SIZE) {  
  7. alloc_init_section(virt, virt + off, prot_sect);  
  8. virt += PGDIR_SIZE;  
  9. length -= PGDIR_SIZE;  
  10. }  
  11. while (length >= PAGE_SIZE) {  
  12. alloc_init_page(virt, virt + off, md->domain, prot_pte);  
  13. virt += PAGE_SIZE;  
  14. length -= PAGE_SIZE;  
  15. }  
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分配并填充中间页表项

[cpp] view plain copy print ?
  1. static inlinevoid  
  2. alloc_init_page(unsigned long virt, unsigned long phys, int domain, int prot)  
  3. {  
  4. pmd_t *pmdp;  
  5. pte_t *ptep;  
  6. pmdp = pmd_offset(pgd_offset_k(virt), virt);//返回页目录中virt对应的表项 
  7. if (pmd_none(*pmdp)) {//如果表项是空的,则分配一个中间页表 
  8. pte_t *ptep = alloc_bootmem_low_pages(2 * PTRS_PER_PTE *  
  9. sizeof(pte_t));  
  10. ptep += PTRS_PER_PTE; 
  11. //设置页目录表项  
  12. set_pmd(pmdp, __mk_pmd(ptep, PMD_TYPE_TABLE | PMD_DOMAIN(domain)));  
  13. }  
  14. ptep = pte_offset(pmdp, virt);  
  15. //如果表项不是空的,则表项已经存在,只需要设置中间页表表项  
  16. set_pte(ptep, mk_pte_phys(phys, __pgprot(prot)));  
  17. }  
  18. alloc_init_section只需要填充页目录项  
  19. alloc_init_section(unsigned long virt, unsignedlong phys, int prot)  
  20. {  
  21. pmd_t pmd;  
  22. pmd_val(pmd) = phys | prot;//将物理地址和保护标志合成页目录项  
  23. set_pmd(pmd_offset(pgd_offset_k(virt), virt), pmd);  
  24. }  
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可为内核建立所有的地址映射,最后是映射中断向量表
所在的区域:

[cpp] view plain copy print ?
  1. init_maps->physical = virt_to_phys(init_maps);  
  2. init_maps->virtual = vectors_base();  
  3. init_maps->length = PAGE_SIZE;  
  4. init_maps->domain = DOMAIN_USER;  
  5. init_maps->prot_read = 0;  
  6. init_maps->prot_write = 0;  
  7. init_maps->cacheable = 1;  
  8. init_maps->bufferable = 0;  
  9. create_mapping(init_maps);  
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,下面我们还会看到,中断处理程序中的汇编部分也被拷贝到
这一页中。


你可能感兴趣的:(LINUX start_kernel 分析,一位大师级的人物写的,不看要后悔的哟!!)