内核启动 startkernel.docx
《内核启动 startkernel.docx》由会员分享,可在线阅读,更多相关《内核启动 startkernel.docx(27页珍藏版)》请在冰点文库上搜索。
内核启动startkernel
内核启动start_kernel
一位大师级的人物写的,不看要后悔的哟!
!
如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反到掩盖了汇编的一些和机器相关的部分,有时候更难懂。
其实作为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。
start_kernel在/linux/init/main.c中定义:
asmlinkagevoid__initstart_kernel(void)
{
char*command_line;
unsignedlongmempages;
externcharsaved_command_line[];
lock_kernel();
printk(linux_banner);
setup_arch(&command_line);//arm/kernel/setup.c
printk("Kernelcommandline:
%s\n",saved_command_line);
parse_options(command_line);
trap_init();//arm/kernle/traps.cinstall
。
。
。
。
。
。
。
。
。
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__initsetup_arch(char**cmdline_p)
{
structparam_struct*params=NULL;
structmachine_desc*mdesc;//archstructure,foryourads,definedininclude/arm-asm/mach/arch.hverylong
structmeminfomeminfo;
char*from=default_command_line;
memset(&meminfo,0,sizeof(meminfo));
首先把meminfo清零,有个背景介绍一下,从linux2.4的内核开始,支持内存的节点(node),也就是可支持不连续的物理内存区域。
这一点在嵌入式系统中很有用,例如对于SDRAM和FALSH,性质不同,可作为不同的内存节点。
meminfo结构定义如下:
/******************************************************/
#defineNR_BANKS4
//definethesystenmemregion,notconsistent
structmeminfo{
intnr_banks;
unsignedlongend;
struct{
unsignedlongstart;
unsignedlongsize;
intnode;
}bank[NR_BANKS];
};
/******************************************************/
下面是:
ROOT_DEV=MKDEV(0,255);
ROOT_DEV是宏,指明启动的设备,嵌入式系统中通常是flashdisk.
这里面有一个有趣的悖论:
linux的设备都是在/dev/下,访问这些设备文件需要设备驱动程序支持,而访问设备文件才能取得设备号,才能加载驱动程序,那么第一个设备驱动程序是怎么加载呢?
就是ROOT_DEV,不需要访问设备文件,直接指定设备号。
下面我们准备初始化真正的内核页表,而不再是临时的了。
首先还是取得当前系统的内存映像:
mdesc=setup_architecture(machine_arch_type);
//findthemachinetypeinmach-integrator/arch.c
//theadsname,memmap,iomap
返回如下结构:
mach-integrator/arch.c
MACHINE_START(INTEGRATOR,"MotorolaMX1ADS")
MAINTAINER("ARMLtd/DeepBlueSolutionsLtd")
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:
structmm_structinit_mm=INIT_MM(init_mm);
从本回开始的相当一部分内容是和内存管理相关的,凭心而论,操作系统的
内存管理是很复杂的,牵扯到处理器的硬件细节和软件算法,
限于篇幅所限制,请大家先仔细读一读armmmu的部分,
中文参考资料:
linux内核源代码情景对话,
linux2.4.18原代码分析。
init_mm.start_code=(unsignedlong)&_text;
内核代码段开始
init_mm.end_code=(unsignedlong)&_etext;
内核代码段结束
init_mm.end_data=(unsignedlong)&_edata;
内核数据段开始
init_mm.brk=(unsignedlong)&_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);
//clearcommandarray
saved_command_line[COMMAND_LINE_SIZE-1]='\0';
//settheendflag
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__initpaging_init(structmeminfo*mi,structmachine_desc*mdesc)
{
void*zero_page,*bad_page,*bad_table;
intnode;
//staticstructmeminfomeminfo__initdata={0,};
memcpy(&meminfo,mi,sizeof(meminfo));
/*
*allocatewhatweneedforthebadpages.
*notethatwecountonthisgoingok.
*/
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
badpage=0xc0001000
bad_table=0xc0002000
上回我们说到在paging_init中分配了三个页:
zero_page=0xc0000000
badpage=0xc0001000
bad_table=0xc0002000
但是奇怪的很,在更新的linux代码中只分配了一个
zero_page,而且在源代码中找不到zero_page
用在什么地方了,大家讨论讨论吧。
paging_init的主要工作是在
void__initmemtable_init(structmeminfo*mi)
中完成的,为系统内存创建页表:
meminfo结构如下:
structmeminfo{
intnr_banks;
unsignedlongend;
struct{
unsignedlongstart;
unsignedlongsize;
intnode;
}bank[NR_BANKS];
};
是用来纪录系统中的内存区段的,因为在嵌入式
系统中并不是所有的内存都能映射,例如sdram只有
64m,flash32m,而且不见得是连续的,所以用
meminfo纪录这些区段。
void__initmemtable_init(structmeminfo*mi)
{
structmap_desc*init_maps,*p,*q;
unsignedlongaddress=0;
inti;
init_maps=p=alloc_bootmem_low_pages(PAGE_SIZE);
其中map_desc定义为:
structmap_desc{
unsignedlongvirtual;
unsignedlongphysical;
unsignedlonglength;
intdomain:
4,//页表的domain
prot_read:
1,//保护标志
prot_write:
1,//写保护标志
cacheable:
1,//是否cache
bufferable:
1,//是否用writebuffer
last:
1;//空
};init_maps
map_desc是区段及其属性的定义,属性位的意义请
参考ARMMMU的介绍。
下面对meminfo的区段进行遍历,同时填写init_maps
中的各项内容:
for(i=0;inr_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;//使用writebuffer
p++;//下一个区段
}
如果系统有flash,
#ifdefFLUSH_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(addressvirtual||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(addressvirtual||q==p){
//由于内核空间是从c0000000开始,所以c0000000
//以前的页表项全部清空
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)
#ifdefCONFIG_CPU_ARM920_WRITETHROUGH
eorr2,r1,#0x0a
tstr2,#0x0b
biceqr1,r1,#4
#endif
strr1,[r0]
把pmd_t填写到页表项中,由于pmd_t=0,
实际等于清除了这一项,由于dcache打开,
这一条指令实际并没有写回内存,而是写到cache中
mcrp15,0,r0,c7,c10,1
把cache中地址r0对应的内容写回内存中,
这一条语句实际是写到了writebuffer中,
还没有真正写回内存。
mcrp15,0,r0,c7,c10,4
等待把writebuffer中的内容写回内存。
在这之前core等待
movpc,lr
在这里我们看到,由于页表的内容十分关键,为了确保写回内存,
采用了直接操作cache的方法。
由于在armcore中,打开了dcache
则必定要用writebuffer.所以还有wb的回写问题。
由于考虑到效率,我们使用了cache和buffer,
所以在某些地方要用指令保证数据被及时写回。
下面映射c0000000后面的页表
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中定义;
staticvoid__initcreate_mapping(structmap_desc*md)
{
unsignedlongvirt,length;
intprot_sect,prot_pte;
longoff;
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表项的位置不同,
所以根据structmap_desc中的保护标志,分别计算页表项
中的AP,domain,CB标志位。
有一段时间没有写了,道歉先,前一段时间在做armlinux的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分配并填充中间页表项
staticinlinevoid
alloc_init_page(unsignedlongvirt,unsignedlongphys,intdomain,intprot)
{
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(unsignedlongvirt,unsignedlongphys,intprot)
{
pmd_tpmd;